Full Code of crisidev/bacon-ls for AI

main d3b084cec8d5 cached
31 files
387.4 KB
101.0k tokens
279 symbols
1 requests
Download .txt
Showing preview only (402K chars total). Download the full file or copy to clipboard to get everything.
Repository: crisidev/bacon-ls
Branch: main
Commit: d3b084cec8d5
Files: 31
Total size: 387.4 KB

Directory structure:
gitextract_sv33u4e_/

├── .github/
│   ├── ISSUE_TEMPLATE/
│   │   └── bug_report.md
│   ├── dependabot.yml
│   └── workflows/
│       ├── auto-merge.yml
│       ├── ci.yml
│       ├── code-coverage.yml
│       └── release.yml
├── .gitignore
├── .prettierignore
├── .vscodeignore
├── Cargo.toml
├── LICENSE
├── README.md
├── eslint.config.mjs
├── flake.nix
├── package.json
├── rustfmt.toml
├── src/
│   ├── bacon.rs
│   ├── lib.rs
│   ├── lsp.rs
│   ├── main.rs
│   ├── native.rs
│   ├── shadow.rs
│   └── testdata/
│       ├── expansion-needed.json
│       ├── unused-import-compact.json
│       ├── unused-import-line.json
│       ├── unused-import.json
│       └── unused-variable.json
├── tests/
│   ├── cargo_backend.rs
│   └── lsp_restart.rs
├── tsconfig.json
└── vscode/
    └── extension.ts

================================================
FILE CONTENTS
================================================

================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.md
================================================
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''

---

**Describe the bug**
A clear and concise description of what the bug is.

**Expected behavior**
A clear and concise description of what you expected to happen.

**Attatch `bacon-ls.log`**
Export `RUST_LOG=debug` before running vim / neovim to generate `bacon-ls.log` with debugging info.


================================================
FILE: .github/dependabot.yml
================================================
version: 2

updates:
  - package-ecosystem: "cargo"
    directory: "/"
    schedule:
      interval: "monthly"
    ignore:
      - dependency-name: "bacon-ls*"
    groups:
      lsp:
        applies-to: version-updates
        patterns:
         - "*"
  - package-ecosystem: "npm"
    directory: "/"
    schedule:
      interval: "monthly"
    groups:
      vscode:
        applies-to: version-updates
        patterns:
         - "*"
    ignore:
      # this needs to match engines.vscode and defines the minimum version of vscode
      - dependency-name: '@types/vscode'


================================================
FILE: .github/workflows/auto-merge.yml
================================================
name: Dependabot Auto-merge
on:
  pull_request:
    types: [opened]

permissions:
  contents: write
  pull-requests: write

jobs:
  dependabot:
    runs-on: ubuntu-latest
    if: github.event.pull_request.user.login == 'dependabot[bot]'
    steps:
      - name: Dependabot metadata
        id: dependabot-metadata
        uses: dependabot/fetch-metadata@v2

      - name: Enable auto-merge for Dependabot PRs
        run: gh pr merge --auto --squash "$PR_URL" --body ""
        env:
          PR_URL: ${{github.event.pull_request.html_url}}
          GH_TOKEN: ${{secrets.GITHUB_TOKEN}}


================================================
FILE: .github/workflows/ci.yml
================================================
name: ci

on:
  push:
    branches: [ "main" ]
  pull_request:
    branches: [ "main" ]
  workflow_dispatch:

env:
  CARGO_TERM_COLOR: always
  RUST_LOG: bacon-ls

jobs:
  # security-audit:
  #   runs-on: ubuntu-latest
  #   steps:
  #     - uses: actions/checkout@v4
  #     - uses: rustsec/audit-check@v2
  #       with:
  #         token: ${{ secrets.ACTIONS_TOKEN }}
  build-linux:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v4
    - uses: Swatinem/rust-cache@v2
    - run: cargo test
      env:
        RUST_LOG: debug,globset=warn
    - run: cargo fmt --all -- --check
    - run: cargo clippy --all-targets
    - uses: actions/setup-node@v4
      with:
        node-version-file: package.json
    - run: npm ci
    - run: npm run lint
    # vscode requires an X Server
    # - name: npm test
      # run: xvfb-run npm test
    # ensure package can be built when package.json changes
    - name: npx vsce package
      run: npx vsce package
  build-mac:
    runs-on: macos-latest
    steps:
    - uses: actions/checkout@v4
    - uses: Swatinem/rust-cache@v2
    - run: cargo test
      env:
        RUST_LOG: debug,globset=warn
        RUST_BACKTRACE: 1
  build-windows:
    runs-on: windows-latest
    steps:
    - uses: actions/checkout@v4
    - uses: Swatinem/rust-cache@v2
    - run: cargo test
      env:
        RUST_LOG: debug,globset=warn
        RUST_BACKTRACE: 1


================================================
FILE: .github/workflows/code-coverage.yml
================================================
name: code-coverage

on: [pull_request, push]

jobs:
  coverage:
    runs-on: ubuntu-latest
    env:
      CARGO_TERM_COLOR: always
    steps:
      - uses: actions/checkout@v4
      - name: Install Rust
        run: rustup update stable
      - name: Install cargo-llvm-cov
        uses: taiki-e/install-action@cargo-llvm-cov
      - name: Generate code coverage
        run: cargo llvm-cov --all-features --workspace --codecov --output-path codecov.json
      - name: Upload coverage to Codecov
        uses: codecov/codecov-action@v3
        with:
          token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos
          files: codecov.json
          fail_ci_if_error: true


================================================
FILE: .github/workflows/release.yml
================================================
name: release

on:
  release:
    types: [published]
  workflow_dispatch:

jobs:
  wait:
    runs-on: ubuntu-latest
    steps:
      - name: Wait for checks to succeed
        uses: poseidon/wait-for-status-checks@v0.4.1
        with:
          token: ${{ secrets.GITHUB_TOKEN }}
          interval: 5

  dist:
    strategy:
      # don't cancel other jobs when one fails
      fail-fast: false
      matrix:
        include:
          - os: windows-latest
            target: x86_64-pc-windows-msvc
            code-target: win32-x64
          - os: windows-latest
            target: i686-pc-windows-msvc
            code-target: win32-x64
          - os: windows-latest
            target: aarch64-pc-windows-msvc
            code-target: win32-arm64
          - os: ubuntu-latest
            target: x86_64-unknown-linux-gnu
            code-target: linux-x64
          - os: ubuntu-latest
            target: x86_64-unknown-linux-musl
            code-target: linux-x64
          # - os: ubuntu-latest
          #   target: i686-unknown-linux-musl
          #   code-target: linux-x64
          - os: ubuntu-latest
            target: aarch64-unknown-linux-gnu
            code-target: linux-arm64
          - os: ubuntu-latest
            target: aarch64-unknown-linux-musl
            code-target: linux-arm64
          - os: ubuntu-latest
            target: arm-unknown-linux-gnueabihf
            code-target: linux-armhf
          - os: macos-latest
            target: x86_64-apple-darwin
            code-target: darwin-x64
          - os: macos-latest
            target: aarch64-apple-darwin
            code-target: darwin-arm64

    name: dist (${{ matrix.target }})
    runs-on: ${{ matrix.os }}
    needs: wait

    steps:
      - uses: actions/checkout@v4
      # needed for arm targets
      - name: Install Rust
        uses: dtolnay/rust-toolchain@v1
        with:
          toolchain: stable
          target: ${{ matrix.target }}
      - uses: Swatinem/rust-cache@v2
      - name: Install zlib-dev and openssl-dev (linux)
        if: contains(matrix.target, 'unknown-linux')
        run: sudo apt-get update && sudo apt-get install zlib1g-dev libssl-dev musl-tools
      - name: Install GCC arm64 (linux)
        if: startsWith(matrix.target, 'aarch64-unknown-linux')
        run: sudo apt-get update && sudo apt-get install gcc-aarch64-linux-gnu
      - name: Install GCC armhf (linux)
        if: matrix.target == 'arm-unknown-linux-gnueabihf'
        run: sudo apt-get update && sudo apt-get install gcc-arm-linux-gnueabihf
      - name: Install i686 glibc (linux)
        if: matrix.target == 'i686-unknown-linux-musl'
        run: sudo apt-get install gcc-multilib g++-multilib libc6-dev-i386
      - run: cargo build --target ${{ matrix.target }} --release
        env:
          PKG_CONFIG_ALLOW_CROSS: 1
          CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc
          CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_LINKER: aarch64-linux-gnu-gcc
          CARGO_TARGET_ARM_UNKNOWN_LINUX_GNUEABIHF_LINKER: arm-linux-gnueabihf-gcc
      - run: npm ci
      - name: vsce package
        # we can only publish a single linux-x64 and linux-arm64 package
        # so we skip the gnu target and package the musl target
        if: matrix.target != 'x86_64-unknown-linux-gnu' && matrix.target != 'aarch64-unknown-linux-gnu' && matrix.target != 'i686-unknown-linux-musl' && matrix.target != 'i686-pc-windows-msvc'
        # use bash on windows
        shell: bash
        run: |
          mkdir -p bundled dist
          cp target/${{ matrix.target }}/release/bacon-ls* bundled/
          npx vsce package -o dist/ --target ${{ matrix.code-target }}
      - name: vsce package for alpine
        # package the alpine-x64 target with the musl binary
        if: matrix.target == 'x86_64-unknown-linux-musl'
        shell: bash
        run: npx vsce package -o dist/ --target alpine-x64
      - name: Archive
        shell: bash
        run: |
          ver=${GITHUB_REF/refs\/*\//}
          archive="dist/bacon-ls-$ver-${{ matrix.target }}"
          mkdir -p dist

          if [ "${{ matrix.os }}" == "windows-latest" ]; then
            7z a "${archive}.zip" target/${{ matrix.target }}/release/bacon-ls.exe
          else
            tar czf "${archive}.tar.gz" -C target/${{ matrix.target }}/release bacon-ls
          fi

          ls -al dist/*
      - name: Upload artifacts
        uses: actions/upload-artifact@v4
        with:
          name: ${{ matrix.target }}
          path: dist
      - name: Start sshx session on failed manual run or retry
        if: ${{ failure() && (github.event_name == 'workflow_dispatch' || github.run_attempt > 1) }}
        run: curl -sSf https://sshx.io/get | sh && sshx

  publish:
    runs-on: ubuntu-latest
    needs: dist
    permissions:
      contents: write
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version-file: package.json
      # download each artifact into its own self-named directory
      - uses: actions/download-artifact@v4
        with:
          path: dist
      - run: npm ci
      - name: Inspect dist
        run: |
          ls -al dist/*
          find dist -name "*.vsix" -type f
      - name: vsce publish
        run: npx vsce publish --packagePath $(find dist -name "*.vsix" -type f)
        env:
          VSCE_PAT: ${{ secrets.VSCE_PAT }}
      - name: ovsx publish
        run: npx ovsx publish --packagePath $(find dist -name "*.vsix" -type f)
        env:
          OVSX_PAT: ${{ secrets.OVSX_PAT }}
      - name: Upload to GH release
        uses: softprops/action-gh-release@v2
        with:
          # unset the prerelease flag and make it the latest release
          prerelease: false
          make_latest: true
          files: dist/**/*
      - name: Start sshx session on failed manual run or retry
        if: ${{ failure() && (github.event_name == 'workflow_dispatch' || github.run_attempt > 1) }}
        run: curl -sSf https://sshx.io/get | sh && sshx


================================================
FILE: .gitignore
================================================
.bacon-locations
tarpaulin-report.html
out
dist
node_modules
.vscode-test/
*.vsix
target/
bundled/
vscode/extension.js.*
bacon-ls.log
result
.coverage
.direnv
REVIEW.md


================================================
FILE: .prettierignore
================================================
.github
node_modules
.vscode
.vscode-test
out
target
src/testdata
flake.lock
CHANGELOG.md
README.md
README-0.4.md


================================================
FILE: .vscodeignore
================================================
**
!img/icon.png
!LICENSE
!out/main.js
!package-lock.json
!package.json
!bundled
!README.md


================================================
FILE: Cargo.toml
================================================
[package]
name = "bacon-ls"
version = "0.29.0"
edition = "2024"
authors = ["Matteo Bigoi <bigo@crisidev.org>"]
description = "Bacon Language Server"
documentation = "https://github.com/crisidev/bacon-ls"
readme = "README.md"
homepage = "https://github.com/crisidev/bacon-ls"
repository = "https://github.com/crisidev/bacon-ls"
license-file = "LICENSE"
keywords = ["lsp", "bacon", "tokio", "neovim", "vim"]
categories = ["command-line-utilities", "text-editors", "asynchronous"]
rust-version = "1.94"

[features]
default = []

[dependencies]
anyhow = "1"
ansi-regex = "0.1.0"
argh = "0.1.19"
flume = "0.12.0"
ignore = "0.4"
ls-types = "0.0.6"
notify = "8.2.0"
notify-debouncer-full = "0.7.0"
percent-encoding = "2.3.2"
serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.149"
tokio = { version = "1.50.0", features = [
  "fs",
  "io-std",
  "io-util",
  "macros",
  "process",
  "rt-multi-thread",
  "time",
] }
tokio-util = "0.7.18"
toml = "1.0"
tower-lsp-server = "0.23.0"
tracing = "0.1.44"
tracing-subscriber = { version = "0.3.23", default-features = false, features = [
  "env-filter",
  "fmt",
] }

[dev-dependencies]
pretty_assertions = "1.4.1"
tempfile = "3.27.0"


================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2024 Matteo Bigoi

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: README.md
================================================
# 🐽 Bacon Language Server 🐽

[![Ci](https://img.shields.io/github/actions/workflow/status/crisidev/bacon-ls/ci.yml?style=for-the-badge)](https://github.com/crisidev/bacon-ls/actions?query=workflow%3Aci)
[![Release](https://img.shields.io/github/actions/workflow/status/crisidev/bacon-ls/release.yml?style=for-the-badge)](https://github.com/crisidev/bacon-ls/actions?query=workflow%3Arelease)
[![Crates.io](https://img.shields.io/crates/v/bacon-ls?style=for-the-badge)](https://crates.io/crates/bacon-ls)
[![Crates.io](https://img.shields.io/crates/d/bacon-ls?style=for-the-badge)](https://crates.io/crates/bacon-ls)
[![License](https://img.shields.io/badge/license-MIT-blue?style=for-the-badge)](https://github.com/crisidev/bacon-ls/blob/main/LICENSE)
[![Codecov](https://img.shields.io/codecov/c/github/crisidev/bacon-ls?style=for-the-badge&token=42UR7SSSPB)](https://codecov.io/github/crisidev/bacon-ls)

**Are you tired of [rust-analyzer](https://rust-analyzer.github.io/) diagnostics being slow?**

LSP Server wrapper for the exceptional [Bacon](https://dystroy.org/bacon/) exposing [textDocument/diagnostic](https://microsoft.github.io/language-server-protocol/specification#textDocument_diagnostic) and [workspace/diagnostic](https://microsoft.github.io/language-server-protocol/specification#workspace_diagnostic) capabilities.

`bacon-ls` 🐽 does not substitute `rust-analyzer`, it's a companion tool that can help with large 
codebases where `rust-analyzer` can become slow dealing with diagnostics. 

**`bacon-ls` 🐽 does not help with completion, analysis, refactor, etc... For these, `rust-analyzer` must be running.**

![Bacon screenshot](./img/screenshot.png)

<!-- vim-markdown-toc Marked -->

* [Features](#features)
    * [Limitations](#limitations)
* [Installation](#installation)
    * [VSCode](#vscode)
    * [Mason.nvim](#mason.nvim)
    * [Manual](#manual)
    * [Nix](#nix)
* [Configuration](#configuration)
    * [Choosing a backend](#choosing-a-backend)
    * [Cargo backend options](#cargo-backend-options)
    * [Live diagnostics as you type (cargo backend only)](#live-diagnostics-as-you-type-(cargo-backend-only))
    * [Bacon backend options](#bacon-backend-options)
    * [Manually triggering diagnostics](#manually-triggering-diagnostics)
    * [Changing configuration at runtime](#changing-configuration-at-runtime)
* [Migrating from 0.26.x and earlier](#migrating-from-0.26.x-and-earlier)
* [Editor setup](#editor-setup)
    * [Neovim - LazyVim](#neovim---lazyvim)
    * [Neovim - Manual](#neovim---manual)
    * [VSCode](#vscode)
    * [Coc.nvim](#coc.nvim)
    * [Helix](#helix)
* [Troubleshooting](#troubleshooting)
    * [Bacon preferences](#bacon-preferences)
    * [Vim - Neovim](#vim---neovim)
    * [VSCode](#vscode)
* [How does it work?](#how-does-it-work?)
* [Thanks](#thanks)
* [Roadmap to 1.0 - ✅ done 🕖 in progress 🌍 future](#roadmap-to-1.0---✅-done-🕖-in-progress-🌍-future)

<!-- vim-markdown-toc -->

See `bacon-ls` 🐽 blog post: https://lmno.lol/crisidev/bacon-language-server

`bacon-ls` 🐽 is meant to be easy to include in your IDE configuration.

![Bacon gif](./img/bacon-ls.gif)

## Features

* Two backends to produce diagnostics:
  * **Cargo** (default since 0.23.0): runs `cargo check` (or `cargo clippy`) directly with
    JSON output, parses the messages and publishes them. Faster, lighter and zero
    extra dependencies.
  * **Bacon**: reads the export file produced by [Bacon](https://dystroy.org/bacon/)
    and publishes those diagnostics. Useful when you already have `bacon` running.
* Push diagnostics to the LSP client on file save, open, close and rename.
* Precise diagnostic positions and macro-expanded spans pointed back at the
  call-site.
* Replacement code actions as suggested by `cargo` / `clippy`.
* Unused / dead / deprecated code tagged with the LSP `UNNECESSARY` and
  `DEPRECATED` diagnostic tags (cargo backend only) so editors render
  unused variables and imports faded, and deprecated items struck through.
* Streaming partial publishes during a long `cargo` run (configurable refresh
  interval) so the editor lights up as soon as the first errors are known.
* Manual `bacon_ls.run` LSP command to re-trigger a check on demand.
* Bacon backend extras: automatic validation of `bacon` preferences, optional
  creation of the preferences file, optional automatic background `bacon`
  process (requires `bacon` 3.8.0), open-file diagnostic synchronization.
* Support for [cargo workspaces](https://doc.rust-lang.org/book/ch14-03-cargo-workspaces.html).

### Limitations

* Windows support is not tested and probably broken - [#10](https://github.com/crisidev/bacon-ls/issues/10)

## Installation

### VSCode

First, install [Bacon](https://dystroy.org/bacon/#installation).

The VSCode extension is available on both VSCE and OVSX:

* `VSCE` [https://marketplace.visualstudio.com/items?itemName=MatteoBigoi.bacon-ls-vscode](https://marketplace.visualstudio.com/items?itemName=MatteoBigoi.bacon-ls-vscode)
* `OVSX` [https://open-vsx.org/extension/MatteoBigoi/bacon-ls-vscode](https://open-vsx.org/extension/MatteoBigoi/bacon-ls-vscode)

### Mason.nvim

Both Bacon and Bacon-ls are installable via [mason.nvim](https://github.com/williamboman/mason.nvim):

```vim
:MasonInstall bacon bacon-ls
```

### Manual

First, install [Bacon](https://dystroy.org/bacon/#installation) and `bacon-ls` 🐽

```bash
❯❯❯ cargo install --locked bacon bacon-ls
❯❯❯ bacon --version
bacon 3.8.0  # make sure you have at least 3.8.0
❯❯❯ bacon-ls --version
0.14.0        # make sure you have at least 0.14.0
```

### Nix

Both [bacon](https://github.com/Canop/bacon/blob/main/flake.nix) and [bacon-ls](./flake.nix) can be consumed from their Nix flakes.

## Configuration

`bacon-ls` 🐽 reads its configuration from the `bacon_ls` section of the LSP
client settings. All fields are optional — if you provide nothing the cargo
backend starts with sensible defaults. The complete schema is:

```jsonc
{
  "bacon_ls": {
    // "cargo" or "bacon". Optional — see "Choosing a backend" below.
    "backend": "cargo",

    "cargo": {
      "command": "check",                 // "check" or "clippy"
      "features": [],                     // cargo --features list, ["feat1", "feat2"] or "all"
      "package": null,                    // cargo -p <package>
      "allTargets": false,                // cargo --all-targets
      "noDefaultFeatures": false,         // cargo --no-default-features
      "extraArgs": [],                    // appended verbatim after the cargo command
      "env": {},                          // extra environment variables (string -> string)
      "cancelRunning": true,              // cancel an in-flight run when a new one is triggered
      "refreshIntervalSeconds": 1,        // partial publish interval; null/negative = wait until done
      "separateChildDiagnostics": null,   // override "related information" support; null = follow client
      "checkOnSave": true,                // trigger cargo on textDocument/didSave
      "clearDiagnosticsOnCheck": false,   // clear existing diagnostics before each run
      "updateOnInsertDebounceMillis": 500 // debounce for live diagnostics; updateOnInsert itself is in init_options
    },

    "bacon": {
      "locationsFile": ".bacon-locations",
      "runInBackground": true,
      "runInBackgroundCommand": "bacon",
      "runInBackgroundCommandArguments": "--headless -j bacon-ls",
      "validatePreferences": true,
      "createPreferencesFile": true,
      "synchronizeAllOpenFilesWaitMillis": 2000,
      "updateOnSave": true,
      "updateOnSaveWaitMillis": 1000
    }
  }
}
```

### Choosing a backend

The backend is chosen once, when the server initializes, and cannot be switched
at runtime (you have to restart the server). The choice is resolved as follows:

1. If `bacon_ls.backend` is set to `"cargo"` or `"bacon"`, that wins.
2. Otherwise, if only one of `bacon_ls.cargo` or `bacon_ls.bacon` is present in
   the settings, that backend is selected.
3. Otherwise (both sections present without an explicit `backend`, or no
   settings at all), the default is **cargo**.

Providing both `cargo` and `bacon` sections without an explicit `backend`
key is reported as a configuration error.

### Cargo backend options

Available since `bacon-ls` 0.23.0, default since 0.26.0. Runs cargo directly with
`--message-format=json-diagnostic-rendered-ansi`, parses the stream and publishes
diagnostics — no `bacon` process required.

* `command` (default `"check"`): which cargo subcommand to run. Most useful values
  are `"check"` and `"clippy"`.
* `features`: list of features passed as `--features a,b,c`.
* `package`: when set, passed as `-p <package>` (useful in workspaces).
* `extraArgs`: appended verbatim after the subcommand. Use this for
  e.g. `["--workspace", "--all-targets", "--all-features"]`.
* `env`: map of additional environment variables for the cargo invocation.
* `cancelRunning` (default `true`): when a new run is requested while another is
  still running, cancel the in-flight one. Set to `false` to instead queue at most
  one follow-up run after the current one completes.
* `refreshIntervalSeconds` (default `1`): how often to publish a partial snapshot
  of the diagnostics gathered so far while cargo is still running. The very
  first diagnostic of a run is always published immediately so the editor lights
  up as soon as cargo emits something; this interval governs the cadence of
  refreshes after that. Set to `null` or a negative number to only publish once
  cargo has finished.
* `separateChildDiagnostics` (default `null`): cargo emits some hints as children
  of a parent diagnostic. When `null` we follow the client's
  `relatedInformation` capability; set to `true` to always emit children as
  standalone diagnostics, `false` to always nest them.
* `checkOnSave` (default `true`): trigger a cargo run on `textDocument/didSave`.
  Set to `false` if you only want to drive runs manually via `bacon_ls.run`.
* `clearDiagnosticsOnCheck` (default `false`): publish empty diagnostics for all
  files that previously had any before starting the new run. Useful if you want
  the editor's diagnostic counters to drop to zero immediately at the start of
  a check.
* `updateOnInsertDebounceMillis` (default `500`): when live diagnostics are on
  (see below), how long the server waits after the last keystroke before
  triggering a cargo run against the shadow workspace. Lower values feel
  snappier; higher values reduce the number of cargo invocations during a
  burst of edits.

### Live diagnostics as you type (cargo backend only)

The cargo backend can publish diagnostics on every keystroke instead of
waiting for a save. This is opt-in and turned off by default: when it's off,
the server doesn't even ask the editor for change events.

How it works: on the first dirty buffer, `bacon-ls` builds a "shadow"
workspace at `target/bacon-ls-live/shadow/` by hardlinking every
`.gitignore`-respected file from the real workspace. Subsequent keystrokes
write only the dirty buffer's bytes into the shadow (breaking the hardlink
so the real file stays untouched), and a debounced cargo run targets the
shadow with `--target-dir=target/bacon-ls-live/target` and
`--remap-path-prefix=<shadow>=<real>` so diagnostics open the user's source
file rather than a `target/` copy. On `didSave` / `didClose` the file's
shadow entry is replaced with a fresh hardlink to disk.

To enable it the flag has to come through **`initialization_options`**, not
workspace settings. The reason is timing: the LSP `textDocument/didChange`
sync capability has to be advertised statically before workspace
configuration arrives, and clients (Neovim in particular) don't reliably
retrofit already-attached buffers when the server tries to register that
capability dynamically after `initialized`.

For Neovim's `vim.lsp.config`:

```lua
vim.lsp.config('bacon-ls', {
    init_options = {
        cargo = { updateOnInsert = true },
    },
    settings = {
        bacon_ls = {
            backend = "cargo",
            cargo = {
                command = "clippy",
                -- updateOnInsert lives in init_options above; only the
                -- runtime knob lives here:
                updateOnInsertDebounceMillis = 500,
            },
        },
    },
})
```

For LazyVim:

```lua
bacon_ls = {
    enabled = true,
    init_options = {
        cargo = { updateOnInsert = true },
    },
    settings = {
        bacon_ls = {
            backend = "cargo",
            cargo = {
                command = "clippy",
                updateOnInsertDebounceMillis = 500,
            },
        },
    },
},
```

Tradeoffs and caveats:

* **Linux-first.** Hardlinking and `--remap-path-prefix` work cross-platform,
  but the integration tests cover Linux only. Mileage on macOS/Windows may
  vary.
* **Separate target directory.** The shadow run uses its own
  `target/bacon-ls-live/target/` so the live cargo invocation doesn't
  invalidate caches for the real `cargo build` you might run in a terminal.
  Cost: extra disk space (typically the size of one debug build).
* **First run is cold.** Building the shadow workspace and the
  separate-target cargo cache is a one-shot cost that can take a few seconds
  on a large project. Subsequent runs are incremental.
* **No filesystem watcher.** Files added or deleted on disk while the editor
  is open won't be reflected in the shadow until the next time the server
  rebuilds it (currently: an LSP restart). Touching a file you've already
  opened works, because we mirror its dirty state through `didChange`.
* **`.gitignore` is respected.** The shadow walker uses the same logic as
  ripgrep (`ignore` crate) with `require_git(false)`, so non-git workspaces
  are handled too. Hidden files (`.cargo/`, `.git/`, etc.) are skipped.

This feature complements rather than replaces `rust-analyzer`: keeping
`rust-analyzer` running alongside (with its own diagnostics turned off, see
the editor setup sections) gives you completion, hover, and go-to-definition
on top of bacon-ls's live diagnostics.

### Bacon backend options

Reads diagnostics from the file produced by Bacon's `export-locations` feature.
Configure Bacon with the `bacon-ls` 🐽 export format in the `bacon` preference
file (`bacon --prefs` shows where it lives):

```toml
[jobs.bacon-ls]
command = [
  "cargo", "clippy",
  "--workspace", "--all-targets", "--all-features",
  "--message-format", "json-diagnostic-rendered-ansi",
]
analyzer = "cargo_json"
need_stdout = true

[exports.cargo-json-spans]
auto = true
exporter = "analyzer"
line_format = """\
  {diagnostic.level}|:|{span.file_name}|:|{span.line_start}|:|{span.line_end}|:|\
  {span.column_start}|:|{span.column_end}|:|{diagnostic.message}|:|{diagnostic.rendered}|:|\
  {span.suggested_replacement}\
"""
path = ".bacon-locations"
```

`bacon` itself must be running to keep the export file fresh
(`bacon -j bacon-ls`). When `runInBackground` is `true` (the default since
0.10.0), `bacon-ls` starts and supervises it for you.

* `locationsFile` (default `".bacon-locations"`): bacon export file to read.
* `runInBackground` (default `true`): start `bacon` automatically and tear it
  down on shutdown.
* `runInBackgroundCommand` (default `"bacon"`): command to spawn. Override if
  `bacon` is not in `$PATH`.
* `runInBackgroundCommandArguments` (default `"--headless -j bacon-ls"`):
  command-line arguments passed to the background `bacon` process.
* `validatePreferences` (default `true`): verify the bacon preferences file
  contains a working `bacon-ls` job and matching export configuration. Errors
  are surfaced to the LSP client.
* `createPreferencesFile` (default `true`): if validation fails because the
  preferences file is missing, generate one with the `bacon-ls` job and export
  defined.
* `synchronizeAllOpenFilesWaitMillis` (default `2000`): how often the background
  loop re-publishes diagnostics for every open file (so a fix in file A also
  clears the now-stale error in file B).
* `updateOnSave` (default `true`): re-publish diagnostics on
  `textDocument/didSave`.
* `updateOnSaveWaitMillis` (default `1000`): delay before reading the locations
  file after a save, to give bacon time to finish its run.

### Manually triggering diagnostics

`bacon-ls` 🐽 registers a single `workspace/executeCommand` named `bacon_ls.run`.
Invoking it triggers an immediate cargo run when the cargo backend is active
(the bacon backend ignores it — there is nothing for it to drive directly).

This is how clients can offer a "run check now" command without relying on save
events. Example from a Neovim mapping:

```lua
vim.keymap.set("n", "<leader>cb", function()
  vim.lsp.buf.execute_command({ command = "bacon_ls.run" })
end, { desc = "bacon-ls: run check" })
```

### Changing configuration at runtime

`bacon-ls` honours `workspace/didChangeConfiguration` and re-reads its settings,
but with one important constraint: **the backend choice is fixed for the
lifetime of the process**. Trying to switch from `cargo` to `bacon` (or vice
versa) without restarting the server is reported as an error to the client and
ignored. All other options (cargo command, features, bacon update interval, …)
can be changed live.

## Migrating from 0.26.x and earlier

PR [#113](https://github.com/crisidev/bacon-ls/pull/113) reorganised the
configuration into per-backend sections. If you were on 0.26.x or earlier, the
following changes apply:

* `useBaconBackend` is gone. Replace it with either the explicit
  `"backend": "bacon"` or simply by providing a `"bacon": { ... }` section.
* All `runBaconInBackground*`, `validateBaconPreferences`,
  `createBaconPreferencesFile`, `synchronizeAllOpenFilesWaitMillis`,
  `updateOnSave`, `updateOnSaveWaitMillis` and `locationsFile` keys have moved
  inside `bacon_ls.bacon.*` and dropped the `Bacon` prefix where it was
  redundant (e.g. `runBaconInBackground` → `bacon.runInBackground`,
  `validateBaconPreferences` → `bacon.validatePreferences`).
* All cargo-related keys live under `bacon_ls.cargo.*`.
* The backend can no longer be changed live — restart the server to switch.

Old config:

```jsonc
{
  "bacon_ls": {
    "useBaconBackend": true,
    "runBaconInBackground": true,
    "validateBaconPreferences": true,
    "updateOnSave": true
  }
}
```

New equivalent:

```jsonc
{
  "bacon_ls": {
    "backend": "bacon",
    "bacon": {
      "runInBackground": true,
      "validatePreferences": true,
      "updateOnSave": true
    }
  }
}
```

## Editor setup

### Neovim - LazyVim

```lua
vim.g.lazyvim_rust_diagnostics = "bacon-ls"
```

### Neovim - Manual

NeoVim requires [nvim-lspconfig](https://github.com/neovim/nvim-lspconfig/) to be configured
and [rust-analyzer](https://rust-analyzer.github.io/) diagnostics must be turned off for `bacon-ls` 🐽
to properly function.

`bacon-ls` is part of `nvim-lspconfig` from commit
[6d2ae9f](https://github.com/neovim/nvim-lspconfig/commit/6d2ae9fdc3111a6e8fd5db2467aca11737195a30)
and it can be configured like any other LSP server works best when
[vim.diagnostics.opts.update_in_insert](https://neovim.io/doc/user/diagnostic.html#vim.diagnostic.Opts)
is set to `true`.

```lua
vim.lsp.config('bacon-ls', {
    settings = {
        bacon_ls = {
            backend = "cargo",
            cargo = {
                command = "clippy",
                checkOnSave = true,
            },
        },
    },
})
```

All runtime settings live under the `settings.bacon_ls` table above. The one
setting that has to be in `init_options` instead is `cargo.updateOnInsert`
(see [Live diagnostics as you type](#live-diagnostics-as-you-type-cargo-backend-only)
for why and the exact shape).

When using [codesettings](https://github.com/mrjones2014/codesettings.nvim)
to manage project local settings

```
vim.lsp.config("*", {
  before_init = function(_, config)
    local codesettings = require("codesettings")
    if config.name == "bacon_ls" then
      local settings = codesettings.local_settings()["_settings"]["bacon_ls"]
      if settings ~= nil then
        config["settings"]["bacon_ls"] = settings
        vim.print(config["settings"]["bacon_ls"])
      end
      return config
    end

    return codesettings.with_local_settings(config.name, config)
  end,
})
```

For `rust-analyzer`, these 2 options must be turned off:

```lua
rust-analyzer.checkOnSave.enable = false
rust-analyzer.diagnostics.enable = false
```

### VSCode

The extension can be configured using the VSCode settings interface.

**It is very important that rust-analyzer `Check On Save` and `Diagnostics` are turned off for `bacon-ls` to work properly:**

* Untick `Rust-analyzer -> general -> Check On Save`
* Untick `Rust-analyzer -> diagnostics -> Enable`

### Coc.nvim

```vim
call coc#config('languageserver', {
      \ 'bacon-ls': {
      \   'command': '~/.cargo/bin/bacon-ls',
      \   'filetypes': ['rust'],
      \   'rootPatterns': ['.git/', 'Cargo.lock', 'Cargo.toml'],
      \   'settings': {
      \    'cargo': {
      \      'cancelRunning': true,
      \    }
      \   }
      \  }
      \ }
\ })
```

### Helix

Extend your `languages.toml` with the following:

```toml
[[language]]
name = "rust"
language-servers = ["rust-analyzer", "bacon-ls"]

[language-server.rust-analyzer.config]
checkOnSave = { enable = false }
diagnostics = { enable = false }

[language-server.bacon-ls]
command = "bacon-ls"

[language-server.bacon-ls.config.bacon_ls]
backend = "cargo"

[language-server.bacon-ls.config.bacon_ls.cargo]
command = "clippy"
```

## Troubleshooting

`bacon-ls` 🐽 can produce a log file in the folder where its running by exporting the `RUST_LOG` variable in the shell:

### Bacon preferences

If the `bacon` preference are not correct, an error message will be published to the LSP client, advising the user to
check the README.

### Vim - Neovim

```bash
❯❯❯ export RUST_LOG=debug
❯❯❯ nvim src/some-file.rs                 # or vim src/some-file.rs
# the variable can also be exported for the current command and not for the whole shell
❯❯❯ RUST_LOG=debug nvim src/some-file.rs  # or RUST_LOG=debug vim src/some-file.rs
❯❯❯ tail -F ./bacon-ls.log
```

### VSCode

Enable debug logging in the extension options.

```bash
❯❯❯ tail -F ./bacon-ls.log
```

## How does it work?

`bacon-ls` 🐽 speaks LSP over STDIO and publishes diagnostics to the client via
`textDocument/publishDiagnostics`. How those diagnostics are produced depends on
the active backend.

**Cargo backend (default).** On each trigger (initial start, file save, or a
manual `bacon_ls.run`), `bacon-ls` runs `cargo check` (or `cargo clippy`) with
`--message-format=json-diagnostic-rendered-ansi` from the project root. The JSON
stream is parsed as it arrives, spans from macro expansions are walked back to
the original call site, and diagnostics are published per file. With
`refreshIntervalSeconds` set, partial snapshots are pushed while cargo is still
running so the editor shows errors as soon as they are known. The previous run
is cancelled when a newer one starts (or queued, depending on `cancelRunning`).

**Bacon backend.** [Bacon](https://dystroy.org/bacon/) runs in a watch loop and
writes diagnostics to its export file (default `.bacon-locations`) using a
custom `line_format`. `bacon-ls` reads that file on save / open / close / rename
events and on a periodic open-file synchronization tick, parses the lines, and
publishes the resulting diagnostics. When `runInBackground` is on, `bacon-ls`
also spawns and supervises the `bacon` process itself.

Both backends share the same code-actions pipeline: when a diagnostic carries a
suggested replacement, it is exposed as a `quickfix` code action via
`textDocument/codeAction`.

## Thanks

`bacon-ls` 🐽 has been inspired by [typos-lsp](https://github.com/tekumara/typos-lsp).

## Roadmap to 1.0 - ✅ done 🕖 in progress 🌍 future

- ✅ Implement LSP server interface for `textDocument/diagnostic` and `workspace/diagnostic`
- ✅ Manual Neovim configuration
- ✅ Manual [LazyVim](https://www.lazyvim.org) configuration
- ✅ Automatic NeoVim configuration
  - ✅ Add `bacon-ls` to [nvim-lspconfig](https://github.com/neovim/nvim-lspconfig/) - https://github.com/neovim/nvim-lspconfig/pull/3160
  - ✅ Add `bacon` and `bacon-ls` to [mason.nvim](https://github.com/williamboman/mason.nvim) - https://github.com/mason-org/mason-registry/pull/5774
  - ✅ Add `bacon-ls` to LazyVim [Rust extras](https://github.com/LazyVim/LazyVim/blob/main/lua/lazyvim/plugins/extras/lang/rust.lua) - https://github.com/LazyVim/LazyVim/pull/3212
- ✅ Add compiler hints to [Bacon](https://dystroy.org/bacon/) export locations - https://github.com/Canop/bacon/pull/187 https://github.com/Canop/bacon/pull/188
- ✅ Support correct span in [Bacon](https://dystroy.org/bacon/) export locations - working from `bacon` 3.7 and `bacon-ls` 0.6.0
- ✅ VSCode extension and configuration - available on the [release](https://github.com/crisidev/bacon-ls/releases) page from 0.6.0
- ✅ VSCode extension published available on Marketplace
- ✅ Add `bacon-ls` to `bacon` website - https://github.com/Canop/bacon/pull/289
- ✅ Smarter handling of parsing the Bacon locations file
- ✅ Faster response after a save event
- ✅ Replacement code actions
- ✅ Validate `bacon` preferences and return an error to the LSP client if they are not compatible with `bacon` - working from `bacon-ls` 0.9.0
- ✅ Create `bacon` preferences file if not found on disk - working from `bacon-ls` 0.10.0
- ✅ Start `bacon` in background based on user preferences - working from `bacon-ls` 0.10.0
- ✅ Synchronize diagnostics for all open files - working from `bacon-ls` 0.10.0
- ✅ Support Helix editor - working from `bacon-ls` 0.12.0
- ✅ Nix flake support
- ✅ Support [cargo workspaces](https://doc.rust-lang.org/book/ch14-03-cargo-workspaces.html) - working from `bacon-ls` 0.14.0
- ✅ Faster native cargo backend - default from `bacon-ls` 0.23.0
- 🌍 Emacs configuration


================================================
FILE: eslint.config.mjs
================================================
import typescriptEslint from "@typescript-eslint/eslint-plugin";
import stylistic from "@stylistic/eslint-plugin";
import tsParser from "@typescript-eslint/parser";

export default [
  {
    ignores: ["**/out", "**/dist", "**/*.d.ts"],
  },
  {
    plugins: {
      "@typescript-eslint": typescriptEslint,
      "@stylistic": stylistic,
    },

    languageOptions: {
      parser: tsParser,
      ecmaVersion: 6,
      sourceType: "module",
    },

    rules: {
      "@typescript-eslint/naming-convention": "warn",
      "@stylistic/semi": "warn",
      curly: "warn",
      eqeqeq: "warn",
      "no-throw-literal": "warn",
      semi: "off",
    },
  },
];


================================================
FILE: flake.nix
================================================
{
  inputs = {
    flake-utils.url = "github:numtide/flake-utils";
    naersk.url = "github:nix-community/naersk";
    nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
  };

  outputs =
    {
      self,
      flake-utils,
      naersk,
      nixpkgs,
    }:
    flake-utils.lib.eachDefaultSystem (
      system:
      let
        pkgs = (import nixpkgs) {
          inherit system;
        };

        naersk' = pkgs.callPackage naersk { };
        bacon-ls = naersk'.buildPackage {
          buildInputs = with pkgs; [ perl openssl ];
          nativeBuildInputs = with pkgs; [ perl openssl ];
          src = ./.;
        };

      in {
        # For `nix build` & `nix run`:
        defaultPackage = bacon-ls;

        # For `nix develop` (optional, can be skipped):
        devShell = pkgs.mkShell {
          nativeBuildInputs = with pkgs; [
            cargo-audit
            cargo-nextest
            grcov
            llvmPackages_19.libllvm
            rust-analyzer
          ];
        };

        # Overlay for package usage in other Nix configurations
        overlay = _: _: {
          inherit bacon-ls;
        };
      }
    );
}


================================================
FILE: package.json
================================================
{
  "name": "bacon-ls-vscode",
  "displayName": "Bacon Language Server",
  "description": "Rust diagnostic based on Bacon",
  "publisher": "MatteoBigoi",
  "version": "0.29.0",
  "private": true,
  "icon": "img/icon.png",
  "repository": {
    "url": "https://github.com/crisidev/bacon-ls.git",
    "type": "git"
  },
  "license": "MIT",
  "keywords": [
    "diagnostic",
    "rust",
    "lsp"
  ],
  "categories": [
    "Linters",
    "Other"
  ],
  "engines": {
    "node": "^20.18.0",
    "vscode": "^1.84.0"
  },
  "activationEvents": [
    "onStartupFinished"
  ],
  "main": "./out/main.js",
  "contributes": {
    "commands": [
      {
        "category": "BaconLs",
        "command": "bacon-ls.restart",
        "title": "Restart"
      },
      {
        "category": "BaconLs",
        "command": "bacon-ls.run",
        "title": "Run check"
      }
    ],
    "configuration": {
      "type": "object",
      "title": "BaconLs",
      "properties": {
        "bacon-ls.path": {
          "scope": "machine-overridable",
          "type": "string",
          "description": "Path to the `bacon-ls` binary. If empty the bundled binary will be used."
        },
        "bacon-ls.logLevel": {
          "scope": "window",
          "type": "string",
          "enum": [
            "off",
            "error",
            "warn",
            "info",
            "debug",
            "trace"
          ],
          "default": "off",
          "markdownDescription": "Logging level of the language server. Logs will be saved in a file called bacon-ls.log if the level is not 'off'."
        },
        "bacon_ls.backend": {
          "scope": "machine-overridable",
          "type": "string",
          "enum": [
            "cargo",
            "bacon"
          ],
          "default": "cargo",
          "markdownDescription": "Which backend produces diagnostics. Cannot be changed at runtime — restart the server to switch."
        },
        "bacon_ls.cargo.command": {
          "scope": "machine-overridable",
          "type": "string",
          "enum": [
            "check",
            "clippy"
          ],
          "default": "check",
          "description": "Cargo backend: which cargo subcommand to run."
        },
        "bacon_ls.cargo.features": {
          "scope": "machine-overridable",
          "type": "array",
          "items": {
            "type": "string"
          },
          "default": [],
          "description": "Cargo backend: list of cargo features (passed as --features a,b,c)."
        },
        "bacon_ls.cargo.package": {
          "scope": "machine-overridable",
          "type": [
            "string",
            "null"
          ],
          "default": null,
          "description": "Cargo backend: when set, passed as -p <package> (useful in workspaces)."
        },
        "bacon_ls.cargo.extraArgs": {
          "scope": "machine-overridable",
          "type": "array",
          "items": {
            "type": "string"
          },
          "default": [],
          "description": "Cargo backend: extra arguments appended verbatim after the cargo subcommand."
        },
        "bacon_ls.cargo.env": {
          "scope": "machine-overridable",
          "type": "object",
          "additionalProperties": {
            "type": "string"
          },
          "default": {},
          "description": "Cargo backend: extra environment variables (string -> string) for the cargo invocation."
        },
        "bacon_ls.cargo.cancelRunning": {
          "scope": "machine-overridable",
          "type": "boolean",
          "default": true,
          "description": "Cargo backend: cancel an in-flight run when a new one is triggered. When false, queue at most one follow-up run."
        },
        "bacon_ls.cargo.refreshIntervalSeconds": {
          "scope": "machine-overridable",
          "type": [
            "integer",
            "null"
          ],
          "default": 5,
          "description": "Cargo backend: partial publish interval in seconds. Set to null (or negative) to only publish once cargo has finished."
        },
        "bacon_ls.cargo.separateChildDiagnostics": {
          "scope": "machine-overridable",
          "type": [
            "boolean",
            "null"
          ],
          "default": null,
          "description": "Cargo backend: override 'related information' support. null = follow client capability; true = always emit children as standalone diagnostics; false = always nest them."
        },
        "bacon_ls.cargo.checkOnSave": {
          "scope": "machine-overridable",
          "type": "boolean",
          "default": true,
          "description": "Cargo backend: trigger cargo on file save."
        },
        "bacon_ls.cargo.clearDiagnosticsOnCheck": {
          "scope": "machine-overridable",
          "type": "boolean",
          "default": false,
          "description": "Cargo backend: clear existing diagnostics before each run."
        },
        "bacon_ls.bacon.locationsFile": {
          "scope": "machine-overridable",
          "type": "string",
          "default": ".bacon-locations",
          "description": "Bacon backend: bacon export file to read."
        },
        "bacon_ls.bacon.runInBackground": {
          "scope": "machine-overridable",
          "type": "boolean",
          "default": true,
          "description": "Bacon backend: start bacon automatically and tear it down on shutdown."
        },
        "bacon_ls.bacon.runInBackgroundCommand": {
          "scope": "machine-overridable",
          "type": "string",
          "default": "bacon",
          "description": "Bacon backend: command used to start bacon in the background."
        },
        "bacon_ls.bacon.runInBackgroundCommandArguments": {
          "scope": "machine-overridable",
          "type": "string",
          "default": "--headless -j bacon-ls",
          "description": "Bacon backend: command-line arguments passed to the background bacon process."
        },
        "bacon_ls.bacon.validatePreferences": {
          "scope": "machine-overridable",
          "type": "boolean",
          "default": true,
          "description": "Bacon backend: verify that bacon preferences contain a working bacon-ls job and matching export configuration."
        },
        "bacon_ls.bacon.createPreferencesFile": {
          "scope": "machine-overridable",
          "type": "boolean",
          "default": true,
          "description": "Bacon backend: when validation finds no preferences file, generate one with the bacon-ls job and export defined."
        },
        "bacon_ls.bacon.synchronizeAllOpenFilesWaitMillis": {
          "scope": "machine-overridable",
          "type": "integer",
          "default": 2000,
          "description": "Bacon backend: how often (in ms) the background loop re-publishes diagnostics for every open file."
        },
        "bacon_ls.bacon.updateOnSave": {
          "scope": "machine-overridable",
          "type": "boolean",
          "default": true,
          "description": "Bacon backend: re-publish diagnostics on file save."
        },
        "bacon_ls.bacon.updateOnSaveWaitMillis": {
          "scope": "machine-overridable",
          "type": "integer",
          "default": 1000,
          "description": "Bacon backend: delay in ms before reading the locations file after a save."
        }
      }
    }
  },
  "scripts": {
    "vscode:prepublish": "npm run esbuild-base -- --minify",
    "package": "vsce package",
    "esbuild-base": "esbuild ./vscode/extension.ts --bundle --outfile=out/main.js --external:vscode --format=cjs --platform=node",
    "build": "npm run esbuild-base -- --sourcemap",
    "watch": "npm run esbuild-base -- --sourcemap --watch",
    "lint": "prettier --check . && eslint",
    "fix": "prettier --write . && eslint --fix",
    "pretest": "tsc && npm run build",
    "test": "cross-env BACON_LS_PATH=$PWD/target/debug/bacon-ls node ./out/test/runTest.js"
  },
  "devDependencies": {
    "@stylistic/eslint-plugin": "^5.10.0",
    "@types/glob": "^9.0.0",
    "@types/mocha": "^10.0.10",
    "@types/node": "25.x",
    "@types/vscode": "^1.84.0",
    "@typescript-eslint/eslint-plugin": "^8.59.1",
    "@typescript-eslint/parser": "^8.31.1",
    "@vscode/test-electron": "^2.5.2",
    "@vscode/vsce": "^3.9.1",
    "cross-env": "^10.1.0",
    "esbuild": "^0.28.0",
    "eslint": "^10.2.1",
    "glob": "^13.0.6",
    "mocha": "^11.7.5",
    "ovsx": "^0.10.11",
    "prettier": "^3.8.3",
    "typescript": "^6.0.3"
  },
  "dependencies": {
    "vscode-languageclient": "^9.0.1"
  }
}


================================================
FILE: rustfmt.toml
================================================
edition = "2024"
max_width = 120
reorder_imports = true
use_field_init_shorthand = true


================================================
FILE: src/bacon.rs
================================================
use std::borrow::Cow;
use std::env;
use std::path::{Path, PathBuf};
use std::process::Stdio;
use std::sync::Arc;
use std::time::Duration;

use ls_types::{Diagnostic, DiagnosticSeverity, Position, Range, Uri, WorkspaceFolder};
use notify_debouncer_full::{DebounceEventResult, new_debouncer};
use serde::{Deserialize, Serialize};
use tokio::fs::File;
use tokio::io::{AsyncBufReadExt, BufReader};
use tokio::process::Command;
use tokio::sync::RwLock;
use tokio::task::JoinHandle;
use tokio_util::sync::CancellationToken;
use tower_lsp_server::Client;

use std::collections::HashSet;

use crate::{
    BackendRuntime, BaconLs, Correction, DiagKey, DiagnosticData, LOCATIONS_FILE, PKG_NAME, State, diag_key,
    path_to_file_uri,
};

#[derive(Debug, Deserialize, Serialize)]
struct BaconConfig {
    jobs: Jobs,
    exports: Exports,
}

#[derive(Debug, Deserialize, Serialize)]
struct Jobs {
    #[serde(rename = "bacon-ls")]
    bacon_ls: BaconLsJob,
}

#[derive(Debug, Deserialize, Serialize)]
struct BaconLsJob {
    #[serde(skip_deserializing)]
    command: Vec<String>,
    analyzer: String,
    need_stdout: bool,
}

#[derive(Debug, Deserialize, Serialize)]
struct Exports {
    #[serde(rename = "cargo-json-spans")]
    cargo_json_spans: CargoJsonSpans,
}

#[derive(Debug, Deserialize, Serialize)]
struct CargoJsonSpans {
    auto: bool,
    exporter: String,
    line_format: String,
    path: String,
}

const ERROR_MESSAGE: &str = "bacon configuration is not compatible with bacon-ls: please take a look to https://github.com/crisidev/bacon-ls?tab=readme-ov-file#configuration and adapt your bacon configuration";
const BACON_ANALYZER: &str = "cargo_json";
const BACON_EXPORTER: &str = "analyzer";
const BACON_COMMAND: [&str; 7] = [
    "cargo",
    "clippy",
    "--tests",
    "--all-targets",
    "--all-features",
    "--message-format",
    "json-diagnostic-rendered-ansi",
];
const LINE_FORMAT: &str = "{diagnostic.level}|:|{span.file_name}|:|{span.line_start}|:|{span.line_end}|:|{span.column_start}|:|{span.column_end}|:|{diagnostic.message}|:|{diagnostic.rendered}|:|{span.suggested_replacement}";

pub(crate) struct Bacon;

impl Bacon {
    async fn validate_preferences_file(path: &Path) -> Result<(), String> {
        let toml_content = tokio::fs::read_to_string(path)
            .await
            .map_err(|e| format!("{ERROR_MESSAGE}: {e}"))?;
        let config: BaconConfig = toml::from_str(&toml_content).map_err(|e| format!("{ERROR_MESSAGE}: {e}"))?;
        tracing::debug!("bacon config is {config:#?}");
        if config.jobs.bacon_ls.analyzer == BACON_ANALYZER
            && config.jobs.bacon_ls.need_stdout
            && config.exports.cargo_json_spans.auto
            && config.exports.cargo_json_spans.exporter == BACON_EXPORTER
            && config.exports.cargo_json_spans.line_format == LINE_FORMAT
            && config.exports.cargo_json_spans.path == LOCATIONS_FILE
        {
            tracing::info!("bacon configuration {} is valid", path.display());
            Ok(())
        } else {
            Err(ERROR_MESSAGE.to_string())
        }
    }

    async fn create_preferences_file(filename: &str) -> Result<(), String> {
        let bacon_config = BaconConfig {
            jobs: Jobs {
                bacon_ls: BaconLsJob {
                    command: BACON_COMMAND.map(|c| c.to_string()).into_iter().collect(),
                    analyzer: BACON_ANALYZER.to_string(),
                    need_stdout: true,
                },
            },
            exports: Exports {
                cargo_json_spans: CargoJsonSpans {
                    auto: true,
                    exporter: BACON_EXPORTER.to_string(),
                    line_format: LINE_FORMAT.to_string(),
                    path: LOCATIONS_FILE.to_string(),
                },
            },
        };
        tracing::info!("creating new bacon preference file {filename}",);
        let toml_string = toml::to_string_pretty(&bacon_config)
            .map_err(|e| format!("error serializing bacon preferences {filename} content: {e}"))?;
        // `tokio::fs::write` is open + write_all + flush + close in one shot,
        // so the bytes are durable by the time the future resolves. The
        // previous `File::create + write_all` form left flushing to drop and
        // could race a subsequent read on busy CI runners (causing the
        // freshly-created file to be observed empty during validation).
        tokio::fs::write(filename, toml_string)
            .await
            .map_err(|e| format!("error creating bacon preferences {filename}: {e}"))?;
        Ok(())
    }

    async fn validate_preferences_impl(bacon_prefs: &[u8], create_prefs_file: bool) -> Result<(), String> {
        let bacon_prefs_files = String::from_utf8_lossy(bacon_prefs);
        let bacon_prefs_files_split: Vec<&str> = bacon_prefs_files.split("\n").collect();
        let mut preference_file_exists = false;
        for prefs_file in bacon_prefs_files_split.iter() {
            let prefs_file_path = Path::new(prefs_file);
            if prefs_file_path.exists() {
                preference_file_exists = true;
                Self::validate_preferences_file(prefs_file_path).await?;
            } else {
                tracing::debug!("skipping non existing bacon preference file {prefs_file}");
            }
        }

        if !preference_file_exists && create_prefs_file {
            Self::create_preferences_file(bacon_prefs_files_split[0]).await?;
        }

        Ok(())
    }

    /// Walks `root` recursively for files named `locations_file_name`. Iterative
    /// (stack-based) so it doesn't grow the async stack on deep trees, and uses
    /// `tokio::fs` so the directory walk yields to the runtime instead of
    /// blocking the executor on large workspaces.
    pub(crate) async fn find_bacon_locations(root: &Path, locations_file_name: &str) -> std::io::Result<Vec<PathBuf>> {
        let mut results = Vec::new();
        let mut stack: Vec<PathBuf> = vec![root.to_path_buf()];
        while let Some(dir) = stack.pop() {
            let mut entries = tokio::fs::read_dir(&dir).await?;
            while let Some(entry) = entries.next_entry().await? {
                let path = entry.path();
                let file_type = entry.file_type().await?;
                if file_type.is_dir() {
                    stack.push(path);
                } else if path.file_name().is_some_and(|name| name == locations_file_name) {
                    results.push(path);
                }
            }
        }
        Ok(results)
    }

    fn parse_severity(severity_str: &str) -> DiagnosticSeverity {
        match severity_str {
            "error" => DiagnosticSeverity::ERROR,
            "warning" => DiagnosticSeverity::WARNING,
            "info" | "information" | "note" | "failure-note" => DiagnosticSeverity::INFORMATION,
            "hint" | "help" => DiagnosticSeverity::HINT,
            other => {
                tracing::warn!("unknown bacon severity level {other:?}, defaulting to INFORMATION");
                DiagnosticSeverity::INFORMATION
            }
        }
    }

    fn parse_positions(fields: &[&str]) -> Option<(u32, u32, u32, u32)> {
        let line_start = fields.first()?.parse().ok()?;
        let line_end = fields.get(1)?.parse().ok()?;
        let column_start = fields.get(2)?.parse().ok()?;
        let column_end = fields.get(3)?.parse().ok()?;
        Some((line_start, line_end, column_start, column_end))
    }

    fn parse_bacon_diagnostic_line(line: &str, folder_path: &Path) -> Option<(Uri, Diagnostic)> {
        // Split line into parts; expect exactly 7 parts in the format specified.
        let line_split: Vec<_> = line.splitn(9, "|:|").collect();

        if line_split.len() != 9 {
            tracing::error!(
                "malformed line: expected 9 parts in the format of `severity|:|path|:|line_start|:|line_end|:|column_start|:|column_end|:|message|:|rendered_message|:|replacement` but found {}: {}",
                line_split.len(),
                line
            );
            return None;
        }

        // Parse elements from the split line
        let severity = Self::parse_severity(line_split[0]);
        let file_path = folder_path.join(line_split[1]);

        // Handle potential parse errors
        let (line_start, line_end, column_start, column_end) = match Self::parse_positions(&line_split[2..6]) {
            Some(values) => values,
            None => {
                tracing::error!("error parsing diagnostic position {:?}", &line_split[2..6]);
                return None;
            }
        };

        let Some(file_path_str) = file_path.to_str() else {
            tracing::error!("file path is not valid UTF-8: {}", file_path.display());
            return None;
        };
        let path = match str::parse::<Uri>(&path_to_file_uri(file_path_str)) {
            Ok(url) => url,
            Err(e) => {
                tracing::error!("error parsing file path {}: {}", file_path.display(), e);
                return None;
            }
        };

        let mut message = line_split[6].replace("\\n", "\n").trim_end_matches('\n').to_string();
        let range = Range::new(
            Position::new(line_start.saturating_sub(1), column_start.saturating_sub(1)),
            Position::new(line_end.saturating_sub(1), column_end.saturating_sub(1)),
        );
        let replacement = line_split[8];
        let data = if replacement != "none" {
            tracing::debug!("storing potential quick fix code action to replace word with {replacement}");
            Some(serde_json::json!(DiagnosticData {
                corrections: vec![Correction::from_single(range, replacement)]
            }))
        } else {
            None
        };

        tracing::debug!(
            "new diagnostic: severity: {severity:?}, path: {path:?}, line_start: {line_start}, line_end: {line_end}, column_start: {column_start}, column_end: {column_end}, message: {message}",
        );

        // Create the Diagnostic object
        let rendered_message = line_split[7];
        if rendered_message != "none" {
            message = ansi_regex::ansi_regex()
                .replace_all(rendered_message, "")
                .trim_end_matches('\n')
                .to_string()
        }
        let diagnostic = Diagnostic {
            range,
            severity: Some(severity),
            source: Some(PKG_NAME.to_string()),
            message,
            data,
            ..Diagnostic::default()
        };

        Some((path, diagnostic))
    }

    fn deduplicate_diagnostics(
        path: Uri,
        uri: &Uri,
        diagnostic: Diagnostic,
        diagnostics: &mut Vec<(Uri, Diagnostic)>,
        seen: &mut HashSet<DiagKey>,
    ) {
        if &path != uri {
            return;
        }
        if seen.insert(diag_key(&diagnostic)) {
            diagnostics.push((path, diagnostic));
        }
    }

    pub(crate) async fn validate_preferences(bacon_command: &str, create_prefs_file: bool) -> Result<(), String> {
        let bacon_prefs = Command::new(bacon_command)
            .arg("--prefs")
            .output()
            .await
            .map_err(|e| e.to_string())?;
        Self::validate_preferences_impl(&bacon_prefs.stdout, create_prefs_file).await
    }

    pub(crate) async fn run_in_background(
        bacon_command: &str,
        bacon_command_args: &str,
        current_dir: Option<&PathBuf>,
        cancel_token: CancellationToken,
    ) -> Result<JoinHandle<()>, String> {
        tracing::info!("starting bacon in background with arguments `{bacon_command_args}`");
        let log_bacon = env::var("BACON_LS_LOG_BACON").unwrap_or("on".to_string());
        let mut command = Command::new(bacon_command);
        command
            .args(bacon_command_args.split_whitespace().collect::<Vec<&str>>())
            .stdin(Stdio::null())
            .stdout(Stdio::piped())
            .stderr(Stdio::piped())
            .kill_on_drop(true);
        if let Some(current_dir) = current_dir {
            command.current_dir(current_dir);
        }

        match command.spawn() {
            Ok(mut child) => {
                // Handle stdout
                if log_bacon != "off"
                    && let Some(stdout) = child.stdout.take()
                {
                    let reader = BufReader::new(stdout).lines();
                    tokio::spawn(async move {
                        let mut reader = reader;
                        while let Ok(Some(line)) = reader.next_line().await {
                            tracing::info!("[bacon stdout]: {}", line);
                        }
                    });
                }

                // Handle stderr
                if log_bacon != "off"
                    && let Some(stderr) = child.stderr.take()
                {
                    let reader = BufReader::new(stderr).lines();
                    tokio::spawn(async move {
                        let mut reader = reader;
                        while let Ok(Some(line)) = reader.next_line().await {
                            tracing::error!("[bacon stderr]: {}", line);
                        }
                    });
                }

                // Wait for the child process to finish
                Ok(tokio::spawn(async move {
                    tracing::debug!("waiting for bacon to terminate");
                    tokio::select! {
                        _ = child.wait() => {},
                        _ = cancel_token.cancelled() => {},
                    };
                }))
            }
            Err(e) => Err(format!("failed to start bacon: {e}")),
        }
    }

    async fn diagnostics(
        uri: &Uri,
        locations_file_name: &str,
        workspace_folders: Option<&[WorkspaceFolder]>,
    ) -> Vec<(Uri, Diagnostic)> {
        let mut diagnostics: Vec<(Uri, Diagnostic)> = vec![];
        let mut seen: HashSet<DiagKey> = HashSet::new();

        if let Some(workspace_folders) = workspace_folders {
            for folder in workspace_folders.iter() {
                let Some(mut folder_path) = folder.uri.to_file_path() else {
                    tracing::warn!("skipping workspace folder with non-file URI: {}", folder.uri.as_str());
                    continue;
                };
                if let Some(git_root) = BaconLs::find_git_root_directory(&folder_path).await
                    && git_root.join("Cargo.toml").exists()
                {
                    tracing::debug!(
                        "found git root directory {}, using it for files base path",
                        git_root.display()
                    );
                    folder_path = Cow::Owned(git_root);
                }
                let bacon_locations = match Bacon::find_bacon_locations(&folder_path, locations_file_name).await {
                    Ok(v) => v,
                    Err(e) => {
                        tracing::warn!("unable to find valid bacon location files: {e}");
                        Vec::new()
                    }
                };
                for bacon_location in bacon_locations.iter() {
                    tracing::info!("found bacon locations file to parse {}", bacon_location.display());
                    match File::open(&bacon_location).await {
                        Ok(fd) => {
                            let reader = BufReader::new(fd);
                            let mut lines = reader.lines();
                            let mut buffer = String::new();

                            while let Some(line) = lines.next_line().await.unwrap_or_else(|e| {
                                tracing::error!("error reading line from file {}: {e}", bacon_location.display());
                                None
                            }) {
                                let trimmed = line.trim_end();

                                // Use the first word to determine the start of a new diagnostic
                                let is_new_diagnostic = trimmed.starts_with("warning")
                                    || trimmed.starts_with("error")
                                    || trimmed.starts_with("info")
                                    || trimmed.starts_with("note")
                                    || trimmed.starts_with("failure-note")
                                    || trimmed.starts_with("help");

                                if is_new_diagnostic {
                                    // Process the collected buffer before starting a new entry
                                    if !buffer.is_empty()
                                        && let Some((path, diagnostic)) =
                                            Self::parse_bacon_diagnostic_line(&buffer, &folder_path)
                                    {
                                        tracing::debug!("found diagnostic for {}", path.as_str());
                                        Self::deduplicate_diagnostics(
                                            path.clone(),
                                            uri,
                                            diagnostic,
                                            &mut diagnostics,
                                            &mut seen,
                                        );
                                    }
                                    // Reset buffer for new diagnostic entry
                                    buffer.clear();
                                }

                                // Append current line to buffer
                                if !buffer.is_empty() {
                                    buffer.push('\n'); // Preserve multiline structure
                                }
                                buffer.push_str(trimmed);
                            }

                            // Flush the remaining buffer after loop ends
                            if !buffer.is_empty()
                                && let Some((path, diagnostic)) =
                                    Self::parse_bacon_diagnostic_line(&buffer, &folder_path)
                            {
                                Self::deduplicate_diagnostics(
                                    path.clone(),
                                    uri,
                                    diagnostic,
                                    &mut diagnostics,
                                    &mut seen,
                                );
                            }
                        }
                        Err(e) => {
                            tracing::error!("unable to read file {}: {e}", bacon_location.display())
                        }
                    }
                }
            }
        }
        diagnostics
    }

    async fn diagnostics_vec(
        uri: &Uri,
        locations_file_name: &str,
        workspace_folders: Option<&[WorkspaceFolder]>,
    ) -> Vec<Diagnostic> {
        Self::diagnostics(uri, locations_file_name, workspace_folders)
            .await
            .into_iter()
            .map(|(_, y)| y)
            .collect::<Vec<Diagnostic>>()
    }

    pub(crate) async fn synchronize_diagnostics(state: Arc<RwLock<State>>, client: Arc<Client>) {
        tracing::info!("starting background task in charge of syncronizing diagnostics for all open files");
        let (tx, rx) = flume::unbounded::<DebounceEventResult>();

        let (locations_file, proj_root, wait_time, shutdown_token) = {
            let state = state.read().await;
            let Some(BackendRuntime::Bacon { config, runtime }) = &state.backend else {
                tracing::error!("synchronize_diagnostics called without bacon backend");
                return;
            };
            (
                config.locations_file.clone(),
                state.project_root.clone(),
                config.synchronize_all_open_files_wait,
                runtime.shutdown_token.clone(),
            )
        };

        let mut watcher = match new_debouncer(wait_time, None, move |ev: DebounceEventResult| {
            // Returns an error if all senders are dropped.
            let _res = tx.send(ev);
        }) {
            Ok(watcher) => watcher,
            Err(e) => {
                let msg = format!(
                    "bacon-ls could not create a file watcher: {e}. \
                     Diagnostics will still update on save but open-file \
                     synchronization is disabled."
                );
                tracing::error!("{msg}");
                client.show_message(ls_types::MessageType::WARNING, msg).await;
                return;
            }
        };

        let locations_file_path =
            proj_root.map_or_else(|| PathBuf::from(&locations_file), |root| root.join(&locations_file));
        loop {
            match watcher.watch(PathBuf::from(&locations_file_path), notify::RecursiveMode::Recursive) {
                Ok(_) => {
                    tracing::info!("watching '{}' for changes...", locations_file_path.display());
                    break;
                }
                Err(e) => {
                    tracing::warn!(
                        "unable to watch '{}', retrying in 1 second",
                        locations_file_path.display()
                    );
                    tracing::error!(".bacon_locations watcher error: {e}");
                    tokio::time::sleep(Duration::from_secs(1)).await;
                }
            }
        }

        while let Some(Ok(res)) = tokio::select! {
            ev = rx.recv_async() => {
                Some(ev)
            }
            _ = shutdown_token.cancelled() => {
                None
            }
        } {
            let events = match res {
                Ok(events) => events,
                Err(err) => {
                    tracing::error!(?err, "watch error");
                    continue;
                }
            };
            // Only publish if the file was modified.
            if !events.iter().any(|ev| ev.kind.is_modify()) {
                continue;
            }

            let mut loop_state = state.write().await;
            let Some(BackendRuntime::Bacon { runtime, .. }) = &mut loop_state.backend else {
                tracing::error!("backend changed during sync loop");
                return;
            };
            runtime.diagnostics_version = runtime.diagnostics_version.wrapping_add(1);
            let version = runtime.diagnostics_version;
            let open_files = runtime.open_files.clone();
            let workspace_folders = loop_state.workspace_folders.clone();
            drop(loop_state);
            tracing::debug!(
                "running periodic diagnostic publish for open files `{}`",
                open_files.iter().map(|f| f.to_string()).collect::<Vec<_>>().join(",")
            );
            for uri in open_files.iter() {
                Self::publish_diagnostics(&client, uri, &locations_file, workspace_folders.as_deref(), version).await;
            }
        }
    }

    pub(crate) async fn publish_diagnostics(
        client: &Arc<Client>,
        uri: &Uri,
        locations_file_name: &str,
        workspace_folders: Option<&[WorkspaceFolder]>,
        version: i32,
    ) {
        let diagnostics_vec = Self::diagnostics_vec(uri, locations_file_name, workspace_folders).await;
        tracing::info!("sent {} bacon diagnostics for {uri:?}", diagnostics_vec.len());
        client
            .publish_diagnostics(uri.clone(), diagnostics_vec, Some(version))
            .await;
    }
}

#[cfg(test)]
mod tests {
    use std::io::Write;

    use super::*;
    use pretty_assertions::assert_eq;
    use tempfile::TempDir;

    #[tokio::test]
    async fn test_valid_bacon_preferences() {
        let valid_toml = format!(
            r#"
            [jobs.bacon-ls]
            analyzer = "{BACON_ANALYZER}"
            need_stdout = true

            [exports.cargo-json-spans]
            auto = true
            exporter = "{BACON_EXPORTER}"
            line_format = "{LINE_FORMAT}"
            path = "{LOCATIONS_FILE}"
        "#
        );
        let tmp_dir = TempDir::new().unwrap();
        let file_path = tmp_dir.path().join("prefs.toml");
        let mut file = std::fs::File::create(&file_path).unwrap();
        write!(file, "{}", valid_toml).unwrap();
        assert!(Bacon::validate_preferences_file(&file_path).await.is_ok());
    }

    #[tokio::test]
    async fn test_invalid_analyzer() {
        let invalid_toml = format!(
            r#"
            [jobs.bacon-ls]
            analyzer = "incorrect_analyzer"
            need_stdout = true

            [exports.cargo-json-spans]
            auto = true
            exporter = "{BACON_EXPORTER}"
            line_format = "{LINE_FORMAT}"
            path = "{LOCATIONS_FILE}"
        "#
        );

        let tmp_dir = TempDir::new().unwrap();
        let file_path = tmp_dir.path().join("prefs.toml");
        let mut file = std::fs::File::create(&file_path).unwrap();
        write!(file, "{}", invalid_toml).unwrap();
        assert!(Bacon::validate_preferences_file(&file_path).await.is_err());
    }

    #[tokio::test]
    async fn test_invalid_line_format() {
        let invalid_toml = format!(
            r#"
            [jobs.bacon-ls]
            analyzer = "{BACON_ANALYZER}"
            need_stdout = true

            [exports.cargo-json-spans]
            auto = true
            exporter = "{BACON_EXPORTER}"
            line_format = "invalid_line_format"
            path = "{LOCATIONS_FILE}"
        "#
        );

        let tmp_dir = TempDir::new().unwrap();
        let file_path = tmp_dir.path().join("prefs.toml");
        let mut file = std::fs::File::create(&file_path).unwrap();
        write!(file, "{}", invalid_toml).unwrap();
        assert!(Bacon::validate_preferences_file(&file_path).await.is_err());
    }

    #[tokio::test]
    async fn test_validate_preferences() {
        let valid_toml = format!(
            r#"
            [jobs.bacon-ls]
            analyzer = "{BACON_ANALYZER}"
            need_stdout = true

            [exports.cargo-json-spans]
            auto = true
            exporter = "{BACON_EXPORTER}"
            line_format = "{LINE_FORMAT}"
            path = "{LOCATIONS_FILE}"
        "#
        );
        assert!(
            Bacon::validate_preferences_impl(valid_toml.as_bytes(), false)
                .await
                .is_ok()
        );
    }

    #[tokio::test]
    async fn test_file_creation_failure() {
        let invalid_path = "/invalid/path/to/file.toml";
        let result = Bacon::create_preferences_file(invalid_path).await;
        assert!(result.is_err());
        assert!(result.unwrap_err().contains("error creating bacon preferences"));
    }

    #[tokio::test]
    async fn test_file_write_failure() {
        let tmp_dir = TempDir::new().unwrap();
        let file_path = tmp_dir.path().join("prefs.toml");
        // Simulate write failure by closing the file prematurely
        let file = File::create(&file_path).await.unwrap();
        drop(file); // Close the file to simulate failure
        let result = Bacon::create_preferences_file(file_path.to_str().unwrap()).await;
        assert!(result.is_ok());
    }

    #[tokio::test]
    async fn test_empty_bacon_preferences_file() {
        let tmp_dir = TempDir::new().unwrap();
        let file_path = tmp_dir.path().join("empty_prefs.toml");
        std::fs::File::create(&file_path).unwrap();
        assert!(Bacon::validate_preferences_file(&file_path).await.is_err());
    }

    #[tokio::test]
    async fn test_run_in_background() {
        let cancel_token = CancellationToken::new();
        let handle = Bacon::run_in_background("cargo", "--version", None, cancel_token.clone()).await;
        assert!(handle.is_ok());
        cancel_token.cancel();
        handle.unwrap().await.unwrap();
    }

    const ERROR_LINE: &str = "error|:|/app/github/bacon-ls/src/lib.rs|:|352|:|352|:|9|:|20|:|cannot find value `one` in this scope\n    |\n352 |         one\n    |         ^^^ help: a unit variant with a similar name exists: `None`\n    |\n   ::: /Users/matteobigoi/.rustup/toolchains/stable-aarch64-apple-darwin/lib/rustlib/src/rust/library/core/src/option.rs:576:5\n    |\n576 |     None,\n    |     ---- similarly named unit variant `None` defined here\n\nFor more information about this error, try `rustc --explain E0425`.\nerror: could not compile `bacon-ls` (lib) due to 1 previous error|:|none|:|none";

    #[test]
    fn test_parse_bacon_diagnostic_line_with_spans_ok() {
        let result = Bacon::parse_bacon_diagnostic_line(ERROR_LINE, Path::new("/app/github/bacon-ls"));
        let (url, diagnostic) = result.unwrap();
        assert_eq!(url.to_string(), "file:///app/github/bacon-ls/src/lib.rs");
        assert_eq!(diagnostic.severity, Some(DiagnosticSeverity::ERROR));
        assert_eq!(diagnostic.source, Some(PKG_NAME.to_string()));
        assert_eq!(
            diagnostic.message,
            r#"cannot find value `one` in this scope
    |
352 |         one
    |         ^^^ help: a unit variant with a similar name exists: `None`
    |
   ::: /Users/matteobigoi/.rustup/toolchains/stable-aarch64-apple-darwin/lib/rustlib/src/rust/library/core/src/option.rs:576:5
    |
576 |     None,
    |     ---- similarly named unit variant `None` defined here

For more information about this error, try `rustc --explain E0425`.
error: could not compile `bacon-ls` (lib) due to 1 previous error"#
        );
        let result = Bacon::parse_bacon_diagnostic_line(ERROR_LINE, Path::new("/app/github/bacon-ls"));
        let (url, diagnostic) = result.unwrap();
        assert_eq!(url.to_string(), "file:///app/github/bacon-ls/src/lib.rs");
        assert_eq!(diagnostic.severity, Some(DiagnosticSeverity::ERROR));
        assert_eq!(diagnostic.source, Some(PKG_NAME.to_string()));
    }

    #[test]
    fn test_parse_bacon_diagnostic_line_with_spans_ko() {
        // Unparsable line
        let result = Bacon::parse_bacon_diagnostic_line("warning:/file:1:1", Path::new("/app/github/bacon-ls"));
        assert_eq!(result, None);

        // Empty line
        let result = Bacon::parse_bacon_diagnostic_line("", Path::new("/app/github/bacon-ls"));
        assert_eq!(result, None);
    }

    #[tokio::test]
    #[cfg(not(target_os = "windows"))]
    async fn test_bacon_multiline_diagnostics_production() {
        let tmp_dir = TempDir::new().unwrap();
        let file_path = tmp_dir.path().join(".bacon-locations");
        let mut tmp_file = std::fs::File::create(file_path).unwrap();
        let error_path = format!("{}/src/lib.rs", tmp_dir.path().display());
        let error_path_url = str::parse::<Uri>(&format!("file://{error_path}")).unwrap();
        writeln!(
            tmp_file,
            "warning|:|src/lib.rs|:|130|:|142|:|33|:|34|:|this if statement can be collapsed|:|none|:|none"
        )
        .unwrap();
        writeln!(
            tmp_file,
            r#"help|:|{error_path}|:|130|:|142|:|33|:|34|:|collapse nested if block|:|none|:|if Some(&the_path) == uri && !diagnostics.iter().any(
                                        |(existing_path, existing_diagnostic)| {{
                                            existing_path.path() == the_path.path()
                                                && diagnostic.range == existing_diagnostic.range
                                                && diagnostic.severity
                                                    == existing_diagnostic.severity
                                                && diagnostic.message == existing_diagnostic.message
                                        }},
                                    ) {{
                                    diagnostics.push((path, diagnostic));
                                }}"#
        ).unwrap();
        writeln!(
            tmp_file,
            "warning|:|{error_path}|:|150|:|162|:|33|:|34|:|this if statement can be collapsed again|:|none|:|none"
        )
        .unwrap();
        writeln!(
            tmp_file,
            r#"warning|:|{error_path}|:|150|:|162|:|33|:|34|:|collapse nested if block|:|if Some(&other_path) == uri && !diagnostics.iter().any(
                                        |(existing_path, existing_diagnostic)| {{
                                            existing_path.path() == other_path.path()
                                                && diagnostic.range == existing_diagnostic.range
                                                && diagnostic.severity
                                                    == existing_diagnostic.severity
                                                && diagnostic.message == existing_diagnostic.message
                                        }},
                                    ) {{
                                    diagnostics.push((path, diagnostic));
                                }}|:|none"#
        ).unwrap();

        let workspace_folders = Some(vec![WorkspaceFolder {
            name: tmp_dir.path().display().to_string(),
            uri: str::parse::<Uri>(&format!("file://{}", tmp_dir.path().display())).unwrap(),
        }]);
        let diagnostics = Bacon::diagnostics(&error_path_url, LOCATIONS_FILE, workspace_folders.as_deref()).await;
        assert_eq!(diagnostics.len(), 4);
        assert!(diagnostics[0].1.data.is_none());
        assert_eq!(diagnostics[0].1.message.len(), 34);
        assert!(diagnostics[1].1.data.is_some());
        assert_eq!(diagnostics[1].1.message.len(), 24);
        assert!(diagnostics[2].1.data.is_none());
        assert_eq!(diagnostics[2].1.message.len(), 40);
        assert!(diagnostics[3].1.data.is_none());
        assert_eq!(diagnostics[3].1.message.len(), 766);
    }

    #[tokio::test]
    #[cfg(not(target_os = "windows"))]
    async fn test_bacon_diagnostics_production_and_deduplication() {
        let tmp_dir = TempDir::new().unwrap();
        let file_path = tmp_dir.path().join(".bacon-locations");
        let mut tmp_file = std::fs::File::create(file_path).unwrap();
        let error_path = format!("{}/src/lib.rs", tmp_dir.path().display());
        let error_path_url = str::parse::<Uri>(&format!("file://{error_path}")).unwrap();
        writeln!(
            tmp_file,
            "error|:|{error_path}|:|352|:|352|:|9|:|20|:|cannot find value `one` in this scope|:|none|:|none"
        )
        .unwrap();
        // duplicate the line
        writeln!(
            tmp_file,
            "error|:|{error_path}|:|352|:|352|:|9|:|20|:|cannot find value `one` in this scope|:|none|:|none"
        )
        .unwrap();
        writeln!(
            tmp_file,
            "warning|:|{error_path}|:|354|:|354|:|9|:|20|:|cannot find value `two` in this scope|:|some|:|none"
        )
        .unwrap();
        writeln!(
            tmp_file,
            "help|:|{error_path}|:|356|:|356|:|9|:|20|:|cannot find value `three` in this scope|:|none|:|some other"
        )
        .unwrap();

        let workspace_folders = Some(vec![WorkspaceFolder {
            name: tmp_dir.path().display().to_string(),
            uri: str::parse::<Uri>(&format!("file://{}", tmp_dir.path().display())).unwrap(),
        }]);
        let diagnostics = Bacon::diagnostics(&error_path_url, LOCATIONS_FILE, workspace_folders.as_deref()).await;
        assert_eq!(diagnostics.len(), 3);
        let diagnostics_vec =
            Bacon::diagnostics_vec(&error_path_url, LOCATIONS_FILE, workspace_folders.as_deref()).await;
        assert_eq!(diagnostics_vec.len(), 3);
    }

    #[test]
    fn test_parse_severity_known_levels() {
        assert_eq!(Bacon::parse_severity("error"), DiagnosticSeverity::ERROR);
        assert_eq!(Bacon::parse_severity("warning"), DiagnosticSeverity::WARNING);
        assert_eq!(Bacon::parse_severity("note"), DiagnosticSeverity::INFORMATION);
        assert_eq!(Bacon::parse_severity("info"), DiagnosticSeverity::INFORMATION);
        assert_eq!(Bacon::parse_severity("information"), DiagnosticSeverity::INFORMATION);
        assert_eq!(Bacon::parse_severity("failure-note"), DiagnosticSeverity::INFORMATION);
        assert_eq!(Bacon::parse_severity("help"), DiagnosticSeverity::HINT);
        assert_eq!(Bacon::parse_severity("hint"), DiagnosticSeverity::HINT);
    }

    #[test]
    fn test_parse_severity_unknown_level_defaults_to_information() {
        assert_eq!(
            Bacon::parse_severity("future-rustc-level"),
            DiagnosticSeverity::INFORMATION
        );
    }

    #[test]
    fn test_parse_positions_valid() {
        let parts = ["1", "2", "3", "4"];
        assert_eq!(Bacon::parse_positions(&parts), Some((1, 2, 3, 4)));
    }

    #[test]
    fn test_parse_positions_non_numeric_returns_none() {
        let parts = ["1", "x", "3", "4"];
        assert_eq!(Bacon::parse_positions(&parts), None);
    }

    #[test]
    fn test_parse_positions_too_few_fields_returns_none() {
        let parts = ["1", "2", "3"];
        assert_eq!(Bacon::parse_positions(&parts), None);
    }

    #[test]
    #[cfg(not(target_os = "windows"))]
    fn test_parse_bacon_diagnostic_line_with_replacement_attaches_correction() {
        // Skipped on Windows: this test asserts the produced URI as a unix-style
        // string. On Windows `Path::new("/proj").join("src/lib.rs")` produces
        // backslashes which percent-encode to `%5C` in the URI.
        let line = "warning|:|src/lib.rs|:|10|:|10|:|5|:|8|:|unused import|:|none|:|use foo::bar;";
        let (uri, diag) = Bacon::parse_bacon_diagnostic_line(line, Path::new("/proj")).expect("must parse");
        assert_eq!(uri.to_string(), "file:///proj/src/lib.rs");
        assert!(
            diag.data.is_some(),
            "non-`none` replacement should attach correction data"
        );
        // Position is converted from 1-based to 0-based.
        assert_eq!(diag.range.start.line, 9);
        assert_eq!(diag.range.start.character, 4);
    }

    #[test]
    fn test_parse_bacon_diagnostic_line_zero_position_saturates() {
        // Defensive: 0 in any position field should saturate to 0 rather than
        // panic on `0 - 1`.
        let line = "error|:|src/lib.rs|:|0|:|0|:|0|:|0|:|boom|:|none|:|none";
        let (_, diag) = Bacon::parse_bacon_diagnostic_line(line, Path::new("/p")).unwrap();
        assert_eq!(diag.range.start.line, 0);
        assert_eq!(diag.range.start.character, 0);
        assert_eq!(diag.range.end.line, 0);
        assert_eq!(diag.range.end.character, 0);
    }

    #[test]
    fn test_parse_bacon_diagnostic_line_strips_ansi_from_rendered() {
        let line = "error|:|src/lib.rs|:|1|:|1|:|1|:|1|:|raw|:|\u{1b}[31mbright red\u{1b}[0m|:|none";
        let (_, diag) = Bacon::parse_bacon_diagnostic_line(line, Path::new("/p")).unwrap();
        // When the rendered slot is non-`none`, it replaces the message and
        // ANSI codes are stripped.
        assert_eq!(diag.message, "bright red");
    }

    #[test]
    fn test_parse_bacon_diagnostic_line_too_few_fields_returns_none() {
        let line = "error|:|/file|:|1|:|2|:|3|:|4|:|message";
        assert_eq!(Bacon::parse_bacon_diagnostic_line(line, Path::new("/p")), None);
    }

    #[test]
    fn test_deduplicate_diagnostics_skips_when_path_does_not_match() {
        let other_uri = str::parse::<Uri>("file:///other.rs").unwrap();
        let target_uri = str::parse::<Uri>("file:///target.rs").unwrap();
        let mut diagnostics = Vec::new();
        let mut seen = HashSet::new();
        let diag = Diagnostic {
            range: Range::default(),
            severity: Some(DiagnosticSeverity::ERROR),
            message: "msg".into(),
            ..Diagnostic::default()
        };
        Bacon::deduplicate_diagnostics(other_uri, &target_uri, diag, &mut diagnostics, &mut seen);
        assert!(diagnostics.is_empty(), "diagnostic for a different URI must be dropped");
        assert!(seen.is_empty(), "seen set must not record skipped entries");
    }

    #[test]
    fn test_deduplicate_diagnostics_drops_exact_duplicate() {
        let uri = str::parse::<Uri>("file:///x.rs").unwrap();
        let mut diagnostics = Vec::new();
        let mut seen = HashSet::new();
        let make = || Diagnostic {
            range: Range::default(),
            severity: Some(DiagnosticSeverity::ERROR),
            message: "same".into(),
            ..Diagnostic::default()
        };
        Bacon::deduplicate_diagnostics(uri.clone(), &uri, make(), &mut diagnostics, &mut seen);
        Bacon::deduplicate_diagnostics(uri.clone(), &uri, make(), &mut diagnostics, &mut seen);
        assert_eq!(diagnostics.len(), 1, "duplicate should not be re-added");
    }

    #[test]
    fn test_deduplicate_diagnostics_keeps_distinct_severity() {
        let uri = str::parse::<Uri>("file:///x.rs").unwrap();
        let mut diagnostics = Vec::new();
        let mut seen = HashSet::new();
        let mut diag = Diagnostic {
            range: Range::default(),
            severity: Some(DiagnosticSeverity::ERROR),
            message: "m".into(),
            ..Diagnostic::default()
        };
        Bacon::deduplicate_diagnostics(uri.clone(), &uri, diag.clone(), &mut diagnostics, &mut seen);
        diag.severity = Some(DiagnosticSeverity::WARNING);
        Bacon::deduplicate_diagnostics(uri.clone(), &uri, diag, &mut diagnostics, &mut seen);
        assert_eq!(diagnostics.len(), 2, "different severity ⇒ different diagnostic");
    }

    #[tokio::test]
    async fn test_find_bacon_locations_finds_nested_files() {
        let tmp = TempDir::new().unwrap();
        // Create root/.bacon-locations and root/sub/sub2/.bacon-locations,
        // plus an unrelated file that must not match.
        let root = tmp.path();
        std::fs::write(root.join(".bacon-locations"), "").unwrap();
        let nested = root.join("sub").join("sub2");
        std::fs::create_dir_all(&nested).unwrap();
        std::fs::write(nested.join(".bacon-locations"), "").unwrap();
        std::fs::write(root.join("sub").join("other.txt"), "").unwrap();

        let mut found = Bacon::find_bacon_locations(root, ".bacon-locations").await.unwrap();
        found.sort();
        assert_eq!(found.len(), 2, "should find both .bacon-locations files");
        assert!(found.iter().all(|p| p.file_name().unwrap() == ".bacon-locations"));
    }

    #[tokio::test]
    async fn test_find_bacon_locations_empty_dir_returns_empty() {
        let tmp = TempDir::new().unwrap();
        let found = Bacon::find_bacon_locations(tmp.path(), ".bacon-locations")
            .await
            .unwrap();
        assert!(found.is_empty());
    }

    #[tokio::test]
    async fn test_find_bacon_locations_missing_root_errors() {
        let tmp = TempDir::new().unwrap();
        let missing = tmp.path().join("does-not-exist");
        let result = Bacon::find_bacon_locations(&missing, ".bacon-locations").await;
        assert!(result.is_err(), "missing root must surface an io error");
    }

    #[tokio::test]
    async fn test_validate_preferences_impl_creates_when_missing_and_requested() {
        let tmp = TempDir::new().unwrap();
        let target = tmp.path().join("nested-prefs.toml");
        // bacon prefs lookup returns one path, file does not exist, create_prefs_file=true.
        let prefs_list = target.to_str().unwrap();
        Bacon::validate_preferences_impl(prefs_list.as_bytes(), true)
            .await
            .expect("should create prefs file when missing");
        assert!(target.exists(), "prefs file should be created");
        // And the freshly-created file must validate cleanly.
        Bacon::validate_preferences_file(&target)
            .await
            .expect("freshly created prefs file should validate");
    }

    #[tokio::test]
    async fn test_validate_preferences_impl_no_create_when_disabled() {
        let tmp = TempDir::new().unwrap();
        let target = tmp.path().join("absent-prefs.toml");
        let prefs_list = target.to_str().unwrap();
        Bacon::validate_preferences_impl(prefs_list.as_bytes(), false)
            .await
            .expect("missing prefs with create=false is not an error");
        assert!(!target.exists());
    }
}


================================================
FILE: src/lib.rs
================================================
//! Bacon Language Server
use std::collections::{HashMap, HashSet};
use std::env;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::time::{Duration, Instant};

use argh::FromArgs;
use bacon::Bacon;
use flume::RecvError;
use ls_types::{Diagnostic, DiagnosticSeverity, MessageType, ProgressToken, Range, Uri, WorkspaceFolder};
use native::Cargo;
use percent_encoding::{AsciiSet, CONTROLS, utf8_percent_encode};
use serde_json::{Map, Value};
use shadow::ShadowWorkspace;
use tokio::sync::{RwLock, RwLockWriteGuard};
use tokio::task::JoinHandle;
use tokio_util::sync::CancellationToken;
use tower_lsp_server::{Client, LspService, Server, jsonrpc};
use tracing_subscriber::fmt::format::FmtSpan;

mod bacon;
mod lsp;
mod native;
mod shadow;

const PKG_NAME: &str = env!("CARGO_PKG_NAME");
pub const PKG_VERSION: &str = env!("CARGO_PKG_VERSION");
const LOCATIONS_FILE: &str = ".bacon-locations";
const BACON_BACKGROUND_COMMAND: &str = "bacon";
const BACON_BACKGROUND_COMMAND_ARGS: &str = "--headless -j bacon-ls";

// Characters that must be percent-encoded when putting an OS path into a
// `file://` URI. We keep `/` unencoded so it continues to split the path into
// segments (clients expect multi-segment URIs). This covers the reserved URI
// characters plus a few that break `Uri` parsing in practice (space, `#`,
// `?`, `%`, `[`/`]`, backslash, etc.).
const PATH_ENCODE_SET: &AsciiSet = &CONTROLS
    .add(b' ')
    .add(b'"')
    .add(b'#')
    .add(b'<')
    .add(b'>')
    .add(b'?')
    .add(b'[')
    .add(b'\\')
    .add(b']')
    .add(b'^')
    .add(b'`')
    .add(b'{')
    .add(b'|')
    .add(b'}')
    .add(b'%');

/// Build a `file://...` URI string from an OS path. Percent-encodes any
/// characters that would otherwise break URI parsing (spaces, `#`, `?`, `%`,
/// etc.), while leaving `/` intact so path segments survive.
pub(crate) fn path_to_file_uri(path: &str) -> String {
    format!("file://{}", utf8_percent_encode(path, PATH_ENCODE_SET))
}

/// Hash key for deduplicating diagnostics that share the same range, severity,
/// and message. `DiagnosticSeverity` is `Eq` but not `Hash` in `ls-types`, so we
/// project it down to a small integer tag.
pub(crate) type DiagKey = (Range, i32, String);

pub(crate) fn diag_key(d: &Diagnostic) -> DiagKey {
    (d.range, severity_tag(d.severity), d.message.clone())
}

fn severity_tag(s: Option<DiagnosticSeverity>) -> i32 {
    match s {
        None => 0,
        Some(s) if s == DiagnosticSeverity::ERROR => 1,
        Some(s) if s == DiagnosticSeverity::WARNING => 2,
        Some(s) if s == DiagnosticSeverity::INFORMATION => 3,
        Some(s) if s == DiagnosticSeverity::HINT => 4,
        Some(_) => -1,
    }
}

/// bacon-ls - https://github.com/crisidev/bacon-ls
#[derive(Debug, FromArgs)]
pub struct Args {
    /// display version information
    #[argh(switch, short = 'v')]
    pub version: bool,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum BackendChoice {
    Cargo,
    Bacon,
}

#[derive(Debug)]
enum BackendRuntime {
    Bacon {
        config: BaconOptions,
        runtime: BaconRuntime,
    },
    Cargo {
        config: CargoOptions,
        runtime: CargoRuntime,
    },
}

impl BackendRuntime {
    fn backend_choice(&self) -> BackendChoice {
        match self {
            Self::Bacon { .. } => BackendChoice::Bacon,
            Self::Cargo { .. } => BackendChoice::Cargo,
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum CargoRunState {
    Idle,
    Running,
    RunningPending,
}

#[derive(Debug, Copy, Clone)]
pub(crate) enum PublishMode {
    CancelRunning,
    QueueIfRunning,
}

fn invalid_option(name: &str) -> jsonrpc::Error {
    jsonrpc::Error {
        code: jsonrpc::ErrorCode::InvalidParams,
        message: format!("Invalid value for option \"{name}\"").into(),
        data: None,
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
enum CargoFeatures {
    /// use `--all-features`
    All,
    /// pass the feature list as `--features=...`
    List(Vec<String>),
}

impl Default for CargoFeatures {
    fn default() -> Self {
        Self::List(vec![])
    }
}

impl CargoFeatures {
    fn from_json_value(value: &Value) -> jsonrpc::Result<Self> {
        match value {
            Value::Null => Ok(Self::List(vec![])),
            Value::String(str) if str == "all" => Ok(Self::All),
            Value::Array(values) => {
                let features = values
                    .iter()
                    .map(|item| {
                        item.as_str()
                            .map(|s| s.to_string())
                            .ok_or(jsonrpc::Error::new(jsonrpc::ErrorCode::InvalidParams))
                    })
                    .collect::<jsonrpc::Result<Vec<_>>>()?;

                Ok(Self::List(features))
            }
            _ => Err(jsonrpc::Error {
                code: jsonrpc::ErrorCode::InvalidParams,
                message: "features must be a list of strings or the string \"all\"".into(),
                data: None,
            }),
        }
    }
}

#[derive(Debug)]
pub(crate) struct CargoOptions {
    // "check" or "clippy"
    pub(crate) command: String,
    pub(crate) features: CargoFeatures,
    // `-p crate_name`
    pub(crate) package: Option<String>,
    pub(crate) all_targets: bool,
    pub(crate) no_default_features: bool,
    // Extra arguments which do not have a nice wrapper
    pub(crate) extra_command_args: Vec<String>,
    pub(crate) env: Vec<(String, String)>,
    pub(crate) publish_mode: PublishMode,
    // Interval at which we refresh (send) cargo diagnostics we have so far
    // None means wait until the cargo command is fully done
    pub(crate) refresh_interval_seconds: Option<Duration>,
    /// User override: when `Some(true)`, always emit children as separate
    /// diagnostics instead of related information, regardless of client
    /// capability. When `None`, follow the client advertisement.
    pub(crate) separate_child_diagnostics: Option<bool>,
    pub(crate) check_on_save: bool,
    pub(crate) clear_diagnostics_on_check: bool,
    /// Live-as-you-type diagnostics. When true, the server mirrors the
    /// workspace into a hardlinked shadow under
    /// `target/bacon-ls-live/shadow/`, replaces dirty buffers in the shadow
    /// on `did_change`, and runs cargo against the shadow with a separate
    /// target dir. Off by default.
    pub(crate) update_on_insert: bool,
    /// Quiet period after the most recent `did_change` before the live
    /// cargo run is triggered. Coalesces bursts of keystrokes into a single
    /// run.
    pub(crate) update_on_insert_debounce: Duration,
}

impl CargoOptions {
    pub(crate) fn build_command_args(&self) -> Vec<String> {
        let mut args = vec![self.command.clone()];
        args.push("--message-format=json-diagnostic-rendered-ansi".to_string());

        match &self.features {
            CargoFeatures::All => {
                args.push("--all-features".to_string());
            }
            CargoFeatures::List(features) if !features.is_empty() => {
                args.push("--features".to_string());
                let mut features_list = String::new();
                for feature in features[..features.len() - 1].iter() {
                    features_list += feature;
                    features_list += ",";
                }
                features_list += &features[features.len() - 1];
                args.push(features_list);
            }
            _ => {}
        }

        if let Some(pkg) = self.package.clone() {
            args.push("-p".to_string());
            args.push(pkg);
        }

        if self.all_targets {
            args.push("--all-targets".to_string());
        }

        if self.no_default_features {
            args.push("--no-default-features".to_string());
        }

        for arg in self.extra_command_args.iter().cloned() {
            args.push(arg);
        }

        args
    }

    pub(crate) fn update_from_json_obj(&mut self, cargo_obj: &Map<String, Value>) -> jsonrpc::Result<()> {
        if let Some(value) = cargo_obj.get("command") {
            self.command = value.as_str().ok_or_else(|| invalid_option("command"))?.to_string();
        }

        if let Some(value) = cargo_obj.get("features") {
            self.features = CargoFeatures::from_json_value(value)?;
        }

        if let Some(value) = cargo_obj.get("package") {
            self.package = Some(value.as_str().ok_or_else(|| invalid_option("package"))?.to_string());
        }

        if let Some(value) = cargo_obj.get("allTargets") {
            self.all_targets = value.as_bool().ok_or_else(|| invalid_option("allTargets"))?;
        }

        if let Some(value) = cargo_obj.get("noDefaultFeatures") {
            self.no_default_features = value.as_bool().ok_or_else(|| invalid_option("noDefaultFeatures"))?;
        }

        if let Some(value) = cargo_obj.get("extraArgs") {
            self.extra_command_args = value
                .as_array()
                .ok_or_else(|| invalid_option("extraArgs"))?
                .iter()
                .map(|item| {
                    item.as_str()
                        .map(|s| s.to_string())
                        .ok_or_else(|| invalid_option("extraArgs"))
                })
                .collect::<jsonrpc::Result<Vec<_>>>()?;
        }

        if let Some(value) = cargo_obj.get("env") {
            self.env = value
                .as_object()
                .ok_or_else(|| invalid_option("env"))?
                .iter()
                .map(|(k, v)| {
                    let val = v.as_str().ok_or_else(|| invalid_option("env"))?;
                    Ok((k.clone(), val.to_string()))
                })
                .collect::<jsonrpc::Result<Vec<_>>>()?;
        }

        if let Some(value) = cargo_obj.get("cancelRunning") {
            let cancel = value.as_bool().ok_or_else(|| invalid_option("cancelRunning"))?;
            self.publish_mode = if cancel {
                PublishMode::CancelRunning
            } else {
                PublishMode::QueueIfRunning
            };
        }

        if let Some(value) = cargo_obj.get("refreshIntervalSeconds") {
            if value.is_null() {
                self.refresh_interval_seconds = None;
            } else {
                let seconds = value.as_i64().ok_or_else(|| invalid_option("refreshIntervalSeconds"))?;
                if seconds < 0 {
                    self.refresh_interval_seconds = None;
                } else {
                    self.refresh_interval_seconds = Some(Duration::from_secs(seconds as u64));
                }
            }
        }

        if let Some(value) = cargo_obj.get("separateChildDiagnostics") {
            self.separate_child_diagnostics = if value.is_null() {
                None
            } else {
                Some(
                    value
                        .as_bool()
                        .ok_or_else(|| invalid_option("separateChildDiagnostics"))?,
                )
            };
        }

        if let Some(value) = cargo_obj.get("checkOnSave") {
            self.check_on_save = value.as_bool().ok_or_else(|| invalid_option("checkOnSave"))?;
        }

        if let Some(value) = cargo_obj.get("clearDiagnosticsOnCheck") {
            self.clear_diagnostics_on_check = value
                .as_bool()
                .ok_or_else(|| invalid_option("clearDiagnosticsOnCheck"))?;
        }

        if let Some(value) = cargo_obj.get("updateOnInsertDebounceMillis") {
            let millis = value
                .as_u64()
                .ok_or_else(|| invalid_option("updateOnInsertDebounceMillis"))?;
            self.update_on_insert_debounce = Duration::from_millis(millis);
        }

        Ok(())
    }

    pub(crate) fn reset(&mut self) {
        *self = Self::default();
    }
}

impl Default for CargoOptions {
    fn default() -> Self {
        Self {
            env: Vec::new(),
            publish_mode: PublishMode::CancelRunning,
            command: "check".to_string(),
            features: CargoFeatures::default(),
            all_targets: false,
            extra_command_args: vec![],
            package: None,
            refresh_interval_seconds: Some(Duration::from_secs(1)),
            separate_child_diagnostics: None,
            check_on_save: true,
            clear_diagnostics_on_check: false,
            update_on_insert: false,
            update_on_insert_debounce: Duration::from_millis(500),
            no_default_features: false,
        }
    }
}

#[derive(Debug)]
pub(crate) struct BaconOptions {
    pub(crate) locations_file: String,
    pub(crate) run_in_background: bool,
    pub(crate) run_in_background_command: String,
    pub(crate) run_in_background_command_args: String,
    pub(crate) validate_preferences: bool,
    pub(crate) create_preferences_file: bool,
    pub(crate) synchronize_all_open_files_wait: Duration,
    pub(crate) update_on_save: bool,
    pub(crate) update_on_save_wait: Duration,
}

impl BaconOptions {
    pub(crate) fn update_from_json_obj(&mut self, bacon_obj: &Map<String, Value>) -> jsonrpc::Result<()> {
        if let Some(value) = bacon_obj.get("locationsFile") {
            self.locations_file = value
                .as_str()
                .ok_or(jsonrpc::Error::new(jsonrpc::ErrorCode::InvalidParams))?
                .to_string();
        }
        if let Some(value) = bacon_obj.get("runInBackground") {
            self.run_in_background = value
                .as_bool()
                .ok_or(jsonrpc::Error::new(jsonrpc::ErrorCode::InvalidParams))?;
        }
        if let Some(value) = bacon_obj.get("runInBackgroundCommand") {
            self.run_in_background_command = value
                .as_str()
                .ok_or(jsonrpc::Error::new(jsonrpc::ErrorCode::InvalidParams))?
                .to_string();
        }
        if let Some(value) = bacon_obj.get("runInBackgroundCommandArguments") {
            self.run_in_background_command_args = value
                .as_str()
                .ok_or(jsonrpc::Error::new(jsonrpc::ErrorCode::InvalidParams))?
                .to_string();
        }
        if let Some(value) = bacon_obj.get("validatePreferences") {
            self.validate_preferences = value
                .as_bool()
                .ok_or(jsonrpc::Error::new(jsonrpc::ErrorCode::InvalidParams))?;
        }
        if let Some(value) = bacon_obj.get("createPreferencesFile") {
            self.create_preferences_file = value
                .as_bool()
                .ok_or(jsonrpc::Error::new(jsonrpc::ErrorCode::InvalidParams))?;
        }
        if let Some(value) = bacon_obj.get("synchronizeAllOpenFilesWaitMillis") {
            self.synchronize_all_open_files_wait = Duration::from_millis(
                value
                    .as_u64()
                    .ok_or(jsonrpc::Error::new(jsonrpc::ErrorCode::InvalidParams))?,
            );
        }
        if let Some(value) = bacon_obj.get("updateOnSave") {
            self.update_on_save = value
                .as_bool()
                .ok_or(jsonrpc::Error::new(jsonrpc::ErrorCode::InvalidParams))?;
        }
        if let Some(value) = bacon_obj.get("updateOnSaveWaitMillis") {
            self.update_on_save_wait = Duration::from_millis(
                value
                    .as_u64()
                    .ok_or(jsonrpc::Error::new(jsonrpc::ErrorCode::InvalidParams))?,
            );
        }

        Ok(())
    }

    pub fn reset(&mut self) {
        *self = Self::default();
    }
}

impl Default for BaconOptions {
    fn default() -> Self {
        Self {
            locations_file: LOCATIONS_FILE.to_string(),
            run_in_background: true,
            run_in_background_command: BACON_BACKGROUND_COMMAND.to_string(),
            run_in_background_command_args: BACON_BACKGROUND_COMMAND_ARGS.to_string(),
            validate_preferences: true,
            create_preferences_file: true,
            synchronize_all_open_files_wait: Duration::from_millis(2000),
            update_on_save: true,
            update_on_save_wait: Duration::from_millis(1000),
        }
    }
}

/// Per-invocation overrides used to redirect a cargo run from the real
/// workspace into the hardlinked shadow workspace for live diagnostics.
#[derive(Debug)]
pub(crate) struct LiveCheckContext {
    pub(crate) shadow_root: PathBuf,
    pub(crate) shadow_target_dir: PathBuf,
    pub(crate) real_root: PathBuf,
}

#[derive(Debug)]
pub(crate) struct CargoRuntime {
    cancel_token: CancellationToken,
    run_state: CargoRunState,
    files_with_diags: HashSet<Uri>,
    diagnostics_version: i32,
    build_folder: PathBuf,
    // Timestamp of the most recent publish_cargo_diagnostics invocation.
    // Used by did_open to avoid kicking off a redundant run when one was
    // just triggered (e.g. the initial run from `initialized` immediately
    // followed by the client's first `didOpen`).
    last_run_started: Option<Instant>,
    /// Hardlinked shadow of the workspace used for live "as you type"
    /// diagnostics. None until the first did_change with `update_on_insert`
    /// enabled — building it eagerly at backend init would block startup on
    /// large workspaces for users who never trigger live mode.
    pub(crate) shadow: Option<ShadowWorkspace>,
    /// File URIs that currently have a dirty buffer overlaid in the shadow.
    /// On did_save / did_close we restore each entry to a hardlink so the
    /// next live run reads the on-disk version.
    pub(crate) dirty_files: HashSet<Uri>,
    /// Pending debounced live-cargo trigger. Each `did_change` cancels the
    /// prior handle and schedules a new one so only the last keystroke fires
    /// a check.
    pub(crate) live_debounce: Option<JoinHandle<()>>,
}

impl Default for CargoRuntime {
    fn default() -> Self {
        Self {
            cancel_token: CancellationToken::new(),
            run_state: CargoRunState::Idle,
            files_with_diags: HashSet::new(),
            diagnostics_version: 0,
            build_folder: PathBuf::new(),
            last_run_started: None,
            shadow: None,
            dirty_files: HashSet::new(),
            live_debounce: None,
        }
    }
}

#[derive(Debug)]
pub(crate) struct BaconRuntime {
    pub(crate) shutdown_token: CancellationToken,
    pub(crate) open_files: HashSet<Uri>,
    // Some(..) if we have to run bacon in the background ourselves
    pub(crate) command_handle: Option<JoinHandle<()>>,
    pub(crate) sync_files_handle: JoinHandle<()>,
    // Monotonic counter stamped onto each publishDiagnostics call so clients
    // can discard stale results if publishes arrive out of order.
    pub(crate) diagnostics_version: i32,
}

#[derive(Debug, Default)]
struct State {
    project_root: Option<PathBuf>,
    workspace_folders: Option<Vec<WorkspaceFolder>>,
    diagnostics_data_supported: bool,
    related_information_supported: bool,
    backend: Option<BackendRuntime>,
    /// Set by `initialize()` from `initialization_options.cargo.updateOnInsert`.
    /// We need this at initialize-time to advertise a `Full` text-document
    /// sync capability, because dynamic `client/registerCapability` for
    /// `textDocument/didChange` after `initialized` doesn't reliably retrofit
    /// already-attached buffers (Neovim, in particular, ignores it). A
    /// statically-advertised capability is honored at attach.
    init_update_on_insert: bool,
}

#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub(crate) struct CorrectionEdit {
    pub(crate) range: Range,
    pub(crate) new_text: String,
}

// A single logical fix can require several disjoint byte-range edits. For
// example, removing `Compact` from `use …::{Compact, FmtSpan}` produces three
// edits: remove `{`, remove `Compact, `, remove `}`, leaving `use …::FmtSpan`.
// All edits must be applied atomically so the file stays valid.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub(crate) struct Correction {
    pub(crate) label: String,
    pub(crate) edits: Vec<CorrectionEdit>,
}

impl Correction {
    pub(crate) fn from_single(range: Range, new_text: &str) -> Self {
        let label = if new_text.is_empty() {
            "Remove".to_string()
        } else {
            format!("Replace with: {new_text}")
        };
        Self {
            label,
            edits: vec![CorrectionEdit {
                range,
                new_text: new_text.to_string(),
            }],
        }
    }

    pub(crate) fn from_multi(edits: Vec<CorrectionEdit>) -> Self {
        let label = match edits.iter().find(|e| !e.new_text.is_empty()) {
            None => "Remove".to_string(),
            Some(e) => format!("Replace with: {}", e.new_text),
        };
        Self { label, edits }
    }
}

#[derive(Debug, serde::Serialize, serde::Deserialize)]
struct DiagnosticData {
    corrections: Vec<Correction>,
}

#[derive(Debug, Clone)]
pub struct BaconLs {
    client: Arc<Client>,
    state: Arc<RwLock<State>>,
}

impl BaconLs {
    fn new(client: Client) -> Self {
        Self {
            client: Arc::new(client),
            state: Arc::new(RwLock::new(State::default())),
        }
    }

    fn configure_tracing(log_level: Option<String>, log_path: Option<&Path>) {
        // Configure logging to file.
        let level = log_level.unwrap_or_else(|| env::var("RUST_LOG").unwrap_or("off".to_string()));
        if level == "off" {
            return;
        }
        let default_path = PathBuf::from(format!("{PKG_NAME}.log"));
        let log_path = log_path.unwrap_or(&default_path);
        let file = match std::fs::OpenOptions::new()
            .create(true)
            .write(true)
            .truncate(true)
            .open(log_path)
        {
            Ok(file) => file,
            Err(e) => {
                // stdin/stdout are the LSP jsonrpc pipes; stderr is usually
                // captured by the client's trace window. One line there is the
                // best we can do to tell the user why logging is silent.
                eprintln!(
                    "{PKG_NAME}: could not open log file {}: {e} (tracing disabled)",
                    log_path.display()
                );
                return;
            }
        };
        // try_init: tests may install the subscriber more than once across the
        // process lifetime (cargo runs them in a single binary). Don't panic
        // if a global subscriber is already set — the first one wins.
        let _ = tracing_subscriber::fmt()
            .with_env_filter(level)
            .with_writer(file)
            .with_thread_names(true)
            .with_span_events(FmtSpan::CLOSE)
            .with_target(true)
            .with_file(true)
            .with_line_number(true)
            .try_init();
    }

    /// Run the LSP server.
    pub async fn serve() {
        Self::configure_tracing(None, None);
        // Lock stdin / stdout.
        let stdin = tokio::io::stdin();
        let stdout = tokio::io::stdout();
        // Start the service.
        let (service, socket) = LspService::new(Self::new);
        Server::new(stdin, stdout, socket).serve(service).await;
        // Force the process to terminate instead of waiting for the tokio
        // runtime to drain. Some background tasks (bacon subprocess readers,
        // file watchers) can linger past the `exit` notification; if the
        // process doesn't die promptly, `:LspRestart` in Neovim gives up
        // before starting a fresh instance.
        std::process::exit(0);
    }

    async fn find_git_root_directory(path: &Path) -> Option<PathBuf> {
        let output = tokio::process::Command::new("git")
            .arg("-C")
            .arg(path)
            .arg("rev-parse")
            .arg("--show-toplevel")
            .output()
            .await
            .ok()?;

        if output.status.success() {
            String::from_utf8(output.stdout).ok().map(|v| PathBuf::from(v.trim()))
        } else {
            None
        }
    }

    fn detect_backend(values: &Map<String, Value>) -> Result<BackendChoice, String> {
        if let Some(value) = values.get("backend") {
            let backend = value.as_str().ok_or("'backend' must be a string")?;
            match backend {
                "cargo" => Ok(BackendChoice::Cargo),
                "bacon" => Ok(BackendChoice::Bacon),
                other => Err(format!("Invalid backend value '{other}'. Must be 'cargo' or 'bacon'.")),
            }
        } else {
            let has_cargo = values.get("cargo").and_then(|v| v.as_object()).is_some();
            let has_bacon = values.get("bacon").and_then(|v| v.as_object()).is_some();
            match (has_cargo, has_bacon) {
                (true, true) => Err(
                    "Both 'cargo' and 'bacon' config sections present without a 'backend' key. \
                     Set 'backend' to 'cargo' or 'bacon'."
                        .to_string(),
                ),
                (_, true) => Ok(BackendChoice::Bacon),
                _ => Ok(BackendChoice::Cargo),
            }
        }
    }

    async fn pull_configuration(&self) {
        tracing::debug!("pull_configuration");

        let configuration_fut = self.client.configuration(vec![ls_types::ConfigurationItem {
            scope_uri: None,
            section: Some("bacon_ls".to_string()),
        }]);
        // A client that never answers `workspace/configuration` (e.g. one
        // mid-teardown) would otherwise keep this await alive forever, which
        // in turn pins the `initialized` future inside the server loop and
        // blocks a clean shutdown.
        let response = match tokio::time::timeout(std::time::Duration::from_secs(5), configuration_fut).await {
            Ok(Ok(response)) => response,
            Ok(Err(e)) => {
                tracing::error!("failed to pull configuration: {e}");
                return;
            }
            Err(_) => {
                tracing::warn!("workspace/configuration request timed out; proceeding with defaults");
                return;
            }
        };

        let Some(settings) = response.into_iter().next() else {
            tracing::warn!("empty configuration response from client");
            return;
        };

        tracing::trace!("pulled configuration: {settings:#?}");
        self.adapt_to_settings(&settings).await;
    }

    async fn adapt_to_settings(&self, settings: &Value) {
        let mut state = self.state.write().await;
        let Some(values) = settings.as_object() else {
            tracing::warn!("configuration is not a JSON object");
            return;
        };

        if state.backend.is_none() {
            let backend_choice = match Self::detect_backend(values) {
                Ok(choice) => {
                    tracing::info!(backend = ?choice, "backend detected");
                    choice
                }
                Err(msg) => {
                    tracing::error!("{msg}");
                    self.client.show_message(MessageType::ERROR, &msg).await;
                    return;
                }
            };

            match backend_choice {
                BackendChoice::Bacon => {
                    let mut config = BaconOptions::default();
                    if let Some(bacon_obj) = values.get("bacon").and_then(|v| v.as_object())
                        && let Err(e) = config.update_from_json_obj(bacon_obj)
                    {
                        tracing::error!("invalid bacon configuration: {e}");
                        self.client
                            .show_message(MessageType::ERROR, format!("Error in \"bacon\" section: {e}"))
                            .await;
                    }

                    if config.validate_preferences {
                        if let Err(e) = Bacon::validate_preferences(
                            &config.run_in_background_command,
                            config.create_preferences_file,
                        )
                        .await
                        {
                            tracing::error!("{e}");
                            self.client.show_message(MessageType::ERROR, e).await;
                        }
                    } else {
                        tracing::warn!("skipping validation of bacon preferences, validateBaconPreferences is false");
                    }

                    let proj_root = state.project_root.clone();
                    let shutdown_token = CancellationToken::new();
                    let command_handle = if config.run_in_background {
                        let mut current_dir = None;
                        if let Ok(cwd) = env::current_dir() {
                            current_dir = Self::find_git_root_directory(&cwd).await;
                            if let Some(dir) = &current_dir {
                                if !dir.join("Cargo.toml").exists() {
                                    current_dir = proj_root;
                                }
                            } else {
                                current_dir = proj_root;
                            }
                        }

                        match Bacon::run_in_background(
                            &config.run_in_background_command,
                            &config.run_in_background_command_args,
                            current_dir.as_ref(),
                            shutdown_token.clone(),
                        )
                        .await
                        {
                            Ok(command) => {
                                tracing::info!("bacon was started successfully and is running in the background");
                                Some(command)
                            }
                            Err(e) => {
                                tracing::error!("{e}");
                                self.client.show_message(MessageType::ERROR, e).await;
                                None
                            }
                        }
                    } else {
                        tracing::warn!("skipping background bacon startup, runBaconInBackground is false");
                        None
                    };

                    let task_state = self.state.clone();
                    let task_client = self.client.clone();
                    state.backend = Some(BackendRuntime::Bacon {
                        config,
                        runtime: BaconRuntime {
                            shutdown_token,
                            open_files: HashSet::new(),
                            command_handle,
                            sync_files_handle: tokio::task::spawn(Self::synchronize_diagnostics(
                                task_state,
                                task_client,
                            )),
                            diagnostics_version: 0,
                        },
                    });
                    tracing::info!("bacon backend initialized");
                }
                BackendChoice::Cargo => {
                    let mut config = CargoOptions::default();
                    // `update_on_insert` is sourced exclusively from
                    // `initialization_options.cargo.updateOnInsert` (read in
                    // `initialize` and stashed on `State`). The static
                    // `textDocument/didChange` capability has to be decided
                    // before workspace settings even arrive, so the runtime
                    // gate has to come from the same place.
                    if state.init_update_on_insert {
                        config.update_on_insert = true;
                    }
                    if let Some(cargo_obj) = values.get("cargo").and_then(|v| v.as_object())
                        && let Err(e) = config.update_from_json_obj(cargo_obj)
                    {
                        tracing::error!("invalid cargo configuration: {e}");
                        self.client
                            .show_message(MessageType::ERROR, format!("Error in \"cargo\" section: {e}"))
                            .await;
                    }
                    if let Err(e) = Self::init_cargo_backend(&mut state, config) {
                        tracing::error!("{e}");
                        drop(state);
                        self.client.show_message(MessageType::ERROR, e).await;
                        return;
                    }
                    drop(state);
                }
            }
        } else {
            let current_choice = match &state.backend {
                Some(BackendRuntime::Bacon { .. }) => BackendChoice::Bacon,
                Some(BackendRuntime::Cargo { .. }) => BackendChoice::Cargo,
                None => unreachable!("backend is Some in this branch"),
            };
            let desired = match Self::detect_backend(values) {
                Ok(choice) => choice,
                Err(err) => {
                    tracing::error!("invalid backend configuration on reload: {err}");
                    self.client.show_message(MessageType::ERROR, &err).await;
                    return;
                }
            };

            if desired != current_choice {
                let msg = "Backend cannot be changed while the server is running. \
                           Restart the server to switch backends.";
                tracing::error!("{msg}");
                self.client.show_message(MessageType::ERROR, msg).await;
                return;
            }

            let project_root = state.project_root.clone();
            let init_update_on_insert = state.init_update_on_insert;
            match &mut state.backend {
                Some(BackendRuntime::Cargo { config, runtime }) => {
                    config.reset();
                    if init_update_on_insert {
                        config.update_on_insert = true;
                    }
                    if let Some(cargo_obj) = values.get("cargo").and_then(|v| v.as_object())
                        && let Err(e) = config.update_from_json_obj(cargo_obj)
                    {
                        tracing::error!("invalid cargo configuration: {e}");
                        self.client
                            .show_message(MessageType::ERROR, format!("Error in \"cargo\" section: {e}"))
                            .await;
                    }
                    if let Some(root) = project_root {
                        runtime.build_folder = root;
                    }
                    tracing::debug!("cargo configuration updated");
                }
                Some(BackendRuntime::Bacon { config, .. }) => {
                    config.reset();
                    if let Some(bacon_obj) = values.get("bacon").and_then(|v| v.as_object())
                        && let Err(e) = config.update_from_json_obj(bacon_obj)
                    {
                        tracing::error!("invalid bacon configuration: {e}");
                        self.client
                            .show_message(MessageType::ERROR, format!("Error in \"bacon\" section: {e}"))
                            .await;
                    }
                    tracing::debug!("bacon configuration updated");
                }
                None => unreachable!("backend is Some in this branch"),
            }
        }
    }

    fn init_cargo_backend(state: &mut RwLockWriteGuard<'_, State>, config: CargoOptions) -> Result<(), String> {
        let build_folder = match &state.project_root {
            Some(root) => root.clone(),
            None => match env::current_dir() {
                Ok(cwd) => {
                    tracing::warn!(
                        "no Cargo project root detected; falling back to current working directory: {}",
                        cwd.display()
                    );
                    cwd
                }
                Err(e) => {
                    return Err(format!(
                        "cargo backend cannot start: no project root detected and current working \
                         directory is unavailable ({e}). Open a folder containing a Cargo.toml and \
                         restart the server."
                    ));
                }
            },
        };
        let runtime = CargoRuntime {
            build_folder,
            ..CargoRuntime::default()
        };
        tracing::info!(build_folder = ?runtime.build_folder, "cargo backend initialized");
        state.backend = Some(BackendRuntime::Cargo { config, runtime });
        Ok(())
    }

    /// Trigger a save-time cargo run against the real workspace.
    async fn publish_cargo_diagnostics(&self) {
        self.publish_cargo_diagnostics_inner(None).await;
    }

    /// Trigger a live "as you type" cargo run against the hardlinked shadow
    /// workspace. Builds the shadow on first call. Returns silently if
    /// `update_on_insert` isn't on or the shadow can't be built.
    pub(crate) async fn publish_cargo_diagnostics_live(&self) {
        let live_on = {
            let state = self.state.read().await;
            matches!(
                &state.backend,
                Some(BackendRuntime::Cargo { config, .. }) if config.update_on_insert
            )
        };
        if !live_on {
            return;
        }
        let Some(shadow) = self.ensure_shadow_built().await else {
            return;
        };
        let ctx = LiveCheckContext {
            shadow_root: shadow.shadow_root().to_path_buf(),
            shadow_target_dir: shadow.target_dir().to_path_buf(),
            real_root: shadow.real_root().to_path_buf(),
        };
        self.publish_cargo_diagnostics_inner(Some(&ctx)).await;
    }

    async fn publish_cargo_diagnostics_inner(&self, live: Option<&LiveCheckContext>) {
        tracing::info!(live = live.is_some(), "starting cargo diagnostics run");
        let mut guard = self.state.write().await;
        let project_root = guard.project_root.clone();
        let related_information_supported = guard.related_information_supported;

        let Some(BackendRuntime::Cargo { config, runtime }) = &mut guard.backend else {
            return;
        };
        let use_related_information = !config
            .separate_child_diagnostics
            .unwrap_or(!related_information_supported);
        let cargo_command = config.command.clone();
        let mut cargo_env = config.env.clone();
        let mut cmd_args = config.build_command_args();
        let publish_mode = config.publish_mode;
        let clear_diagnostics_on_check = config.clear_diagnostics_on_check;
        let build_folder = match live {
            Some(ctx) => {
                cmd_args.push(format!("--target-dir={}", ctx.shadow_target_dir.display()));
                // `--remap-path-prefix` makes rustc emit diagnostic spans with
                // the real workspace path in place of the shadow path, so the
                // editor opens the user's source file instead of a target/ copy.
                let rustflags = format!(
                    "--remap-path-prefix={}={}",
                    ctx.shadow_root.display(),
                    ctx.real_root.display()
                );
                // Honor any RUSTFLAGS the user already set in their config.
                if let Some(slot) = cargo_env.iter_mut().find(|(k, _)| k == "RUSTFLAGS") {
                    slot.1.push(' ');
                    slot.1.push_str(&rustflags);
                } else {
                    cargo_env.push(("RUSTFLAGS".to_string(), rustflags));
                }
                ctx.shadow_root.clone()
            }
            None => runtime.build_folder.clone(),
        };
        runtime.diagnostics_version = runtime.diagnostics_version.wrapping_add(1);
        runtime.last_run_started = Some(Instant::now());
        let version = runtime.diagnostics_version;
        let refresh_interval = config.refresh_interval_seconds;

        let cancel_token = match publish_mode {
            PublishMode::CancelRunning => {
                runtime.cancel_token.cancel();
                runtime.cancel_token = CancellationToken::new();
                runtime.cancel_token.clone()
            }
            PublishMode::QueueIfRunning => match runtime.run_state {
                CargoRunState::Running | CargoRunState::RunningPending => {
                    runtime.run_state = CargoRunState::RunningPending;
                    tracing::debug!("cargo already running, marking pending");
                    drop(guard);
                    return;
                }
                CargoRunState::Idle => {
                    runtime.run_state = CargoRunState::Running;
                    runtime.cancel_token.clone()
                }
            },
        };

        // Drain the URIs we need to clear into a local Vec, then drop the
        // state lock BEFORE doing any LSP IO. Holding the write guard across
        // awaited publishes blocks every other handler (did_open, did_close,
        // codeAction, …) for the duration of the round-trips.
        let files_to_clear: Vec<Uri> = if clear_diagnostics_on_check {
            runtime.files_with_diags.drain().collect()
        } else {
            Vec::new()
        };

        drop(guard);

        for file in files_to_clear {
            self.client.publish_diagnostics(file, vec![], Some(version)).await;
        }

        let token = ProgressToken::Number(version);
        let progress = self
            .client
            .progress(token, "checking")
            .with_message(format!("cargo {cargo_command}"))
            .with_percentage(0)
            .begin()
            .await;

        let (tx, rx) = flume::unbounded();

        let cargo_future = Cargo::cargo_diagnostics(
            cmd_args,
            &cargo_env,
            project_root.as_ref(),
            &build_folder,
            use_related_information,
            &progress,
            tx,
        );

        let consumer_client = self.client.clone();
        let diagnostic_consumer = async move {
            // Per-URI bucket: the diagnostics to publish, a `seen` set keyed by
            // (range, severity, message) for O(1) dedup, and a dirty flag for
            // partial publishes during the cargo run.
            let mut diagnostics_map = HashMap::<Uri, (Vec<Diagnostic>, HashSet<DiagKey>, bool)>::new();

            enum AccumulateResult {
                Closed,
                NewDiagnostic,
                Duplicate,
            }

            fn accumulate_diagnostics(
                recv_result: Result<(Uri, Diagnostic), RecvError>,
                diagnostics_map: &mut HashMap<Uri, (Vec<Diagnostic>, HashSet<DiagKey>, bool)>,
            ) -> AccumulateResult {
                let Ok((url, diagnostic)) = recv_result else {
                    return AccumulateResult::Closed;
                };
                let (diagnostics, seen, dirty) = diagnostics_map.entry(url).or_default();
                if seen.insert(diag_key(&diagnostic)) {
                    diagnostics.push(diagnostic);
                    *dirty = true;
                    AccumulateResult::NewDiagnostic
                } else {
                    AccumulateResult::Duplicate
                }
            }

            if let Some(refresh_interval) = refresh_interval {
                // The very first diagnostic of a run is published immediately
                // — waiting up to `refresh_interval` for the editor to show
                // *something* is the most user-visible source of latency. After
                // the first publish, subsequent diagnostics accumulate and are
                // flushed every `refresh_interval`.
                let mut first_published = false;
                let mut t = std::time::Instant::now();
                loop {
                    let mut got_new = false;
                    tokio::select! {
                        result = rx.recv_async() => {
                            match accumulate_diagnostics(result, &mut diagnostics_map) {
                                AccumulateResult::Closed => break,
                                AccumulateResult::NewDiagnostic => got_new = true,
                                AccumulateResult::Duplicate => {}
                            }
                        }
                        _ = tokio::time::sleep_until(tokio::time::Instant::from_std(t + refresh_interval)) => {}
                    }

                    let publish_first = got_new && !first_published;
                    if publish_first || t.elapsed() >= refresh_interval {
                        for (url, (diagnostics, _seen, dirty)) in diagnostics_map.iter_mut() {
                            if *dirty {
                                consumer_client
                                    .publish_diagnostics(url.clone(), diagnostics.clone(), Some(version))
                                    .await;
                                *dirty = false;
                            }
                        }
                        if publish_first {
                            tracing::debug!("first diagnostic published; switching to refresh-interval cadence");
                            first_published = true;
                        }
                        t = std::time::Instant::now();
                    }
                }
            } else {
                loop {
                    if matches!(
                        accumulate_diagnostics(rx.recv_async().await, &mut diagnostics_map),
                        AccumulateResult::Closed
                    ) {
                        break;
                    }
                }
            }

            diagnostics_map
        };

        let consumer_handle = tokio::spawn(diagnostic_consumer);

        let result = tokio::select! {
            result = cargo_future => {
                result.map(|_| false)
            },
            () = cancel_token.cancelled() => {
                tracing::info!("cargo run cancelled by newer request");
                Ok(true)
            }
        };

        let was_cancelled = match result {
            Ok(t) => t,
            Err(error) => {
                // We know there wont be any diagnostics as they way we detect cargo errors is
                // if it exists with non 0 exit code and no diagnostics were found
                tracing::error!(?error, "error building diagnostics");
                progress.finish().await;
                let _ = consumer_handle.await;
                self.client.log_message(MessageType::ERROR, format!("{error}")).await;
                self.client.show_message(MessageType::ERROR, format!("{error}")).await;
                return;
            }
        };

        if was_cancelled {
            // The newer run that triggered cancellation owns publishing. Touching
            // files_with_diags or publishing partial results here would race with
            // it and could push stale diagnostics on top of correct ones.
            let _ = consumer_handle.await;
            progress.finish_with_message("cancelled by user").await;
            return;
        }

        tracing::info!("cargo run finished, collecting diagnostics");

        let mut diagnostics = match consumer_handle.await {
            Ok(d) => d,
            Err(error) => {
                tracing::error!(?error, "diagnostics fetching task panicked");
                progress.finish().await;
                self.client.log_message(MessageType::ERROR, format!("{error}")).await;
                self.client.show_message(MessageType::ERROR, format!("{error}")).await;
                return;
            }
        };

        let mut state = self.state.write().await;
        let Some(BackendRuntime::Cargo {
            config,
            runtime: cargo_rt,
        }) = &mut state.backend
        else {
            // This should be impossible to land here, if we do there a logic error
            tracing::error!("backend changed during cargo run");
            return;
        };
        let publish_mode = config.publish_mode;

        // In CancelRunning mode a newer run may have started after our cargo
        // process finished but before we reached this point. If so our results
        // are stale — skip publishing so we don't overwrite the newer run's
        // output with old data.
        if let PublishMode::CancelRunning = publish_mode
            && version != cargo_rt.diagnostics_version
        {
            tracing::info!(
                version,
                current = cargo_rt.diagnostics_version,
                "skipping stale publish"
            );
            progress.finish_with_message("superseded by newer run").await;
            return;
        }

        for file in cargo_rt.files_with_diags.drain() {
            // Add empty diagnostics so that it get cleared later
            let _ = diagnostics.entry(file).or_insert((vec![], HashSet::new(), true));
        }

        let mut num_warnings = 0;
        let mut num_errors = 0;
        for (uri, (diagnostics, _seen, is_dirty)) in diagnostics.into_iter() {
            tracing::debug!(uri = uri.to_string(), "sent {} cargo diagnostics", diagnostics.len());
            for diagnostic in &diagnostics {
                match diagnostic.severity {
                    Some(DiagnosticSeverity::ERROR) => num_errors += 1,
                    Some(DiagnosticSeverity::WARNING) => num_warnings += 1,
                    Some(_) | None => {}
                }
            }
            if !diagnostics.is_empty() {
                let _ = cargo_rt.files_with_diags.insert(uri.clone());
            }
            if is_dirty {
                self.client.publish_diagnostics(uri, diagnostics, Some(version)).await;
            }
        }
        let message = format!("done, errors: {num_errors}, warnings: {num_warnings}");
        progress.finish_with_message(message).await;

        if let PublishMode::QueueIfRunning = publish_mode {
            match cargo_rt.run_state {
                CargoRunState::RunningPending => {
                    cargo_rt.run_state = CargoRunState::Idle;
                    drop(state);
                    tracing::info!("re-running cargo after queued request");
                    Box::pin(self.publish_cargo_diagnostics()).await;
                }
                _ => {
                    cargo_rt.run_state = CargoRunState::Idle;
                    drop(state);
                }
            }
        }
    }

    /// Lazy-build (or fetch) the live shadow workspace. Returns `None` if the
    /// project root isn't known or the build fails — callers should treat
    /// that as "skip this live update", not as a hard error.
    pub(crate) async fn ensure_shadow_built(&self) -> Option<ShadowWorkspace> {
        // Fast path: shadow already built.
        {
            let state = self.state.read().await;
            if let Some(BackendRuntime::Cargo { runtime, .. }) = &state.backend
                && let Some(shadow) = &runtime.shadow
            {
                return Some(shadow.clone());
            }
        }

        let project_root = {
            let state = self.state.read().await;
            state.project_root.clone()
        };
        let Some(root) = project_root else {
            tracing::warn!("updateOnInsert: no project root; cannot build live shadow");
            return None;
        };

        tracing::info!(root = ?root, "updateOnInsert: building live shadow workspace");
        // Surface this to the user — it's a one-time, multi-second cost
        // (tree walk + hardlink fan-out + cold cargo target dir) and
        // without a heads-up they'd just see the editor go quiet on the
        // first keystroke.
        self.client
            .show_message(
                MessageType::INFO,
                "bacon-ls: building live diagnostics shadow workspace (first run only)…",
            )
            .await;
        let shadow = match ShadowWorkspace::build(root).await {
            Ok(s) => s,
            Err(e) => {
                tracing::error!("updateOnInsert: failed to build shadow: {e}");
                self.client
                    .show_message(
                        MessageType::ERROR,
                        format!("bacon-ls: failed to build live shadow workspace: {e}"),
                    )
                    .await;
                return None;
            }
        };

        // Stash; if a parallel did_change raced us and built one too, ours
        // overwrites — both reflect the same on-disk tree.
        let mut state = self.state.write().await;
        if let Some(BackendRuntime::Cargo { runtime, .. }) = &mut state.backend {
            runtime.shadow = Some(shadow.clone());
        }
        drop(state);
        // Quieter signal that the shadow is ready — goes to the LSP trace
        // pane rather than popping a second toast.
        self.client
            .log_message(
                MessageType::INFO,
                "bacon-ls: live diagnostics shadow ready; subsequent edits will be checked as you type.",
            )
            .await;
        Some(shadow)
    }

    /// Apply a dirty buffer (from `did_change`) to the shadow workspace.
    /// Tracks the URI in `dirty_files` so we can revert it later via
    /// `restore_shadow_link_if_dirty` on `did_save` / `did_close`.
    pub(crate) async fn live_update_dirty(&self, uri: Uri, content: String) {
        let Some(real_path_cow) = uri.to_file_path() else {
            tracing::warn!(uri = uri.as_str(), "updateOnInsert: did_change uri is not a file path");
            return;
        };
        let real_path = real_path_cow.into_owned();

        let Some(shadow) = self.ensure_shadow_built().await else {
            tracing::warn!("updateOnInsert: shadow workspace not available; skipping live update");
            return;
        };
        if let Err(e) = shadow.write_dirty(&real_path, &content).await {
            tracing::warn!(path = ?real_path, ?e, "updateOnInsert: shadow write failed (file outside workspace?)");
            return;
        }

        let debounce = {
            let mut state = self.state.write().await;
            let Some(BackendRuntime::Cargo { config, runtime }) = &mut state.backend else {
                return;
            };
            runtime.dirty_files.insert(uri.clone());
            config.update_on_insert_debounce
        };

        tracing::info!(
            uri = uri.as_str(),
            debounce_ms = debounce.as_millis() as u64,
            "updateOnInsert: shadow updated, scheduling live cargo run"
        );
        self.schedule_live_run(debounce).await;
    }

    /// Schedule (or reschedule) a live cargo run to fire after `delay` of
    /// idle time. Cancels any previously-scheduled live trigger so a burst of
    /// keystrokes coalesces into a single run.
    pub(crate) async fn schedule_live_run(&self, delay: Duration) {
        let mut state = self.state.write().await;
        let Some(BackendRuntime::Cargo { runtime, .. }) = &mut state.backend else {
            return;
        };
        if let Some(prev) = runtime.live_debounce.take() {
            prev.abort();
        }
        let bacon = self.clone();
        runtime.live_debounce = Some(tokio::spawn(async move {
            tokio::time::sleep(delay).await;
            bacon.publish_cargo_diagnostics_live().await;
        }));
    }

    /// Cancel any pending debounced live trigger. Called on `did_save` so
    /// the on-save cargo run (against the real workspace) is the canonical
    /// one and a soon-to-be-stale live run doesn't race it.
    pub(crate) async fn cancel_live_debounce(&self) {
        let mut state = self.state.write().await;
        if let Some(BackendRuntime::Cargo { runtime, .. }) = &mut state.backend
            && let Some(handle) = runtime.live_debounce.take()
        {
            handle.abort();
        }
    }

    /// On `did_save` / `did_close`, replace the (possibly dirty) shadow file
    /// with a fresh hardlink to the on-disk version, and forget the URI.
    pub(crate) async fn restore_shadow_link_if_dirty(&self, uri: &Uri) {
        let (shadow, real_path) = {
            let mut state = self.state.write().await;
            let Some(BackendRuntime::Cargo { runtime, .. }) = &mut state.backend else {
                return;
            };
            if !runtime.dirty_files.remove(uri) {
                return;
            }
            let Some(shadow) = runtime.shadow.clone() else {
                return;
            };
            let Some(path_cow) = uri.to_file_path() else {
                return;
            };
            (shadow, path_cow.into_owned())
        };
        if let Err(e) = shadow.restore_link(&real_path).await {
            tracing::warn!(path = ?real_path, ?e, "updateOnInsert: failed to restore shadow link");
        }
    }

    async fn publish_bacon_diagnostics(&self, uri: &Uri) {
        let mut guard = self.state.write().await;
        let workspace_folders = guard.workspace_folders.clone();

        let Some(BackendRuntime::Bacon { config, runtime }) = &mut guard.backend else {
            return;
        };
        tracing::info!(uri = uri.to_string(), "publish bacon diagnostics");
        let locations_file_name = config.locations_file.clone();
        runtime.diagnostics_version = runtime.diagnostics_version.wrapping_add(1);
        let version = runtime.diagnostics_version;
        drop(guard);
        Bacon::publish_diagnostics(
            &self.client,
            uri,
            &locations_file_name,
            workspace_folders.as_deref(),
            version,
        )
        .await;
    }

    async fn synchronize_diagnostics(state: Arc<RwLock<State>>, client: Arc<Client>) {
        Bacon::synchronize_diagnostics(state, client).await;
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_can_configure_tracing() {
        // Direct the test's log file into a tempdir so we don't clobber the
        // developer's `bacon-ls.log` in the workspace root (which is what
        // `cargo run` / a live editor session writes to).
        let tmp = tempfile::tempdir().expect("tempdir");
        let log_path = tmp.path().join("bacon-ls.log");
        BaconLs::configure_tracing(Some("info".to_string()), Some(&log_path));
    }

    #[test]
    fn test_path_to_file_uri_plain_ascii() {
        let uri = path_to_file_uri("/home/me/src/lib.rs");
        assert_eq!(uri, "file:///home/me/src/lib.rs");
        let parsed = uri.parse::<Uri>().expect("must parse as Uri");
        assert_eq!(parsed.path().as_str(), "/home/me/src/lib.rs");
    }

    #[test]
    fn test_path_to_file_uri_escapes_space_and_hash_and_percent() {
        let uri = path_to_file_uri("/home/me/My Projects/tests#1/file%.rs");
        assert_eq!(uri, "file:///home/me/My%20Projects/tests%231/file%25.rs");
        let parsed = uri.parse::<Uri>().expect("must parse as Uri");
        // Uri preserves the encoded form on the wire; clients are responsible
        // for decoding. We only need to confirm the parse succeeds.
        assert_eq!(parsed.path().as_str(), "/home/me/My%20Projects/tests%231/file%25.rs");
    }

    #[test]
    fn test_path_to_file_uri_preserves_path_separators() {
        // The `/` separator must NOT be encoded, or clients can't recognize
        // segment structure.
        let uri = path_to_file_uri("/a/b/c");
        assert_eq!(uri, "file:///a/b/c");
    }

    #[test]
    fn test_path_to_file_uri_relative_path_preserves_segments() {
        // Cargo emits relative paths (e.g. "src/lib.rs") in JSON output. The
        // current `deserialize_url` hack turns those into URIs with the first
        // segment as "host" — percent-encoding must not break that.
        let uri = path_to_file_uri("src/lib.rs");
        assert_eq!(uri, "file://src/lib.rs");
        let parsed = uri.parse::<Uri>().expect("must parse as Uri");
        assert_eq!(
            parsed.authority().map(|a| a.host().to_string()),
            Some("src".to_string())
        );
        assert_eq!(parsed.path().as_str(), "/lib.rs");
    }

    #[test]
    fn test_cancel_mode_replaces_token() {
        let original = CancellationToken::new();
        let token = original.clone();
        token.cancel();
        assert!(original.is_cancelled());
        let new_token = CancellationToken::new();
        assert!(!new_token.is_cancelled());
    }

    #[test]
    fn test_detect_backend_explicit_cargo() {
        let values: Map<String, Value> = serde_json::from_str(r#"{"backend": "cargo"}"#).unwrap();
        assert_eq!(BaconLs::detect_backend(&values).unwrap(), BackendChoice::Cargo);
    }

    #[test]
    fn test_detect_backend_explicit_bacon() {
        let values: Map<String, Value> = serde_json::from_str(r#"{"backend": "bacon"}"#).unwrap();
        assert_eq!(BaconLs::detect_backend(&values).unwrap(), BackendChoice::Bacon);
    }

    #[test]
    fn test_detect_backend_invalid_value() {
        let values: Map<String, Value> = serde_json::from_str(r#"{"backend": "invalid"}"#).unwrap();
        assert!(BaconLs::detect_backend(&values).is_err());
    }

    #[test]
    fn test_detect_backend_infer_from_cargo_key() {
        let values: Map<String, Value> = serde_json::from_str(r#"{"cargo": {"command": "check"}}"#).unwrap();
        assert_eq!(BaconLs::detect_backend(&values).unwrap(), BackendChoice::Cargo);
    }

    #[test]
    fn test_detect_backend_infer_from_bacon_key() {
        let values: Map<String, Value> =
            serde_json::from_str(r#"{"bacon": {"locationsFile": ".bacon-locations"}}"#).unwrap();
        assert_eq!(BaconLs::detect_backend(&values).unwrap(), BackendChoice::Bacon);
    }

    #[test]
    fn test_detect_backend_both_keys_error() {
        let values: Map<String, Value> = serde_json::from_str(r#"{"cargo": {}, "bacon": {}}"#).unwrap();
        assert!(BaconLs::detect_backend(&values).is_err());
    }

    #[test]
    fn test_detect_backend_no_keys_defaults_to_cargo() {
        let values: Map<String, Value> = serde_json::from_str(r#"{}"#).unwrap();
        assert_eq!(BaconLs::detect_backend(&values).unwrap(), BackendChoice::Cargo);
    }

    #[test]
    fn test_detect_backend_explicit_overrides_keys() {
        let values: Map<String, Value> = serde_json::from_str(r#"{"backend": "cargo", "bacon": {}}"#).unwrap();
        assert_eq!(BaconLs::detect_backend(&values).unwrap(), BackendChoice::Cargo);
    }

    #[test]
    fn test_cargo_options_build_args_default() {
        let args = CargoOptions::default().build_command_args();
        assert_eq!(args, vec!["check", "--message-format=json-diagnostic-rendered-ansi"]);
    }

    #[test]
    fn test_cargo_options_build_args_with_features() {
        let opts = CargoOptions {
            features: CargoFeatures::List(vec!["a".into(), "b".into(), "c".into()]),
            ..CargoOptions::default()
        };
        let args = opts.build_command_args();
        assert_eq!(
            args,
            vec![
                "check",
                "--message-format=json-diagnostic-rendered-ansi",
                "--features",
                "a,b,c"
            ]
        );
    }

    #[test]
    fn test_cargo_options_build_args_single_feature() {
        let opts = CargoOptions {
            features: CargoFeatures::List(vec!["only".into()]),
            ..CargoOptions::default()
        };
        let args = opts.build_command_args();
        assert_eq!(
            args,
            vec![
                "check",
                "--message-format=json-diagnostic-rendered-ansi",
                "--features",
                "only"
            ]
        );
    }

    #[test]
    fn test_cargo_options_build_args_with_all_features() {
        let opts = CargoOptions {
            features: CargoFeatures::All,
            ..CargoOptions::default()
        };
        let args = opts.build_command_args();
        assert_eq!(
            args,
            vec![
                "check",
                "--message-format=json-diagnostic-rendered-ansi",
                "--all-features",
            ]
        );
    }

    #[test]
    fn test_cargo_options_build_args_with_package_and_extras() {
        let opts = CargoOptions {
            command: "clippy".into(),
            package: Some("my-crate".into()),
            extra_command_args: vec!["--workspace".into(), "--all-targets".into()],
            ..CargoOptions::default()
        };
        let args = opts.build_command_args();
        assert_eq!(
            args,
            vec![
                "clippy",
                "--message-format=json-diagnostic-rendered-ansi",
                "-p",
                "my-crate",
                "--workspace",
                "--all-targets",
            ]
        );
    }

    #[test]
    fn test_cargo_options_update_from_json_full_roundtrip() {
        let mut opts = CargoOptions::default();
        let json = serde_json::json!({
            "command": "clippy",
            "features": ["a", "b"],
            "package": "pkg",
            "extraArgs": ["--workspace"],
            "env": {"RUST_LOG": "trace"},
            "cancelRunning": false,
            "refreshIntervalSeconds": 10,
            "separateChildDiagnostics": true,
            "checkOnSave": false,
            "clearDiagnosticsOnCheck": true,
            "updateOnInsertDebounceMillis": 250,
        });
        let obj = json.as_object().unwrap();
        opts.update_from_json_obj(obj).expect("should parse");
        assert_eq!(opts.command, "clippy");
        assert_eq!(
            opts.features,
            CargoFeatures::List(vec!["a".to_string(), "b".to_string()])
        );
        assert_eq!(opts.package.as_deref(), Some("pkg"));
        assert_eq!(opts.extra_command_args, vec!["--workspace".to_string()]);
        assert_eq!(opts.env, vec![("RUST_LOG".into(), "trace".into())]);
        assert!(matches!(opts.publish_mode, PublishMode::QueueIfRunning));
        assert_eq!(opts.refresh_interval_seconds, Some(Duration::from_secs(10)));
        assert_eq!(opts.separate_child_diagnostics, Some(true));
        assert!(!opts.check_on_save);
        assert!(opts.clear_diagnostics_on_check);
        assert_eq!(opts.update_on_insert_debounce, Duration::from_millis(250));
    }

    #[test]
    fn test_cargo_options_update_on_insert_defaults_off() {
        let opts = CargoOptions::default();
        assert!(!opts.update_on_insert);
        assert_eq!(opts.update_on_insert_debounce, Duration::from_millis(500));
    }

    #[test]
    fn test_cargo_options_update_on_insert_debounce_rejects_negative() {
        let mut opts = CargoOptions::default();
        let json = serde_json::json!({"updateOnInsertDebounceMillis": -50});
        assert!(opts.update_from_json_obj(json.as_object().unwrap()).is_err());
    }

    #[test]
    fn test_cargo_options_update_from_json_refresh_null_means_no_partial() {
        let mut opts = CargoOptions::default();
        let json = serde_json::json!({"refreshIntervalSeconds": null});
        opts.update_from_json_obj(json.as_object().unwrap()).unwrap();
        assert_eq!(opts.refresh_interval_seconds, None);
    }

    #[test]
    fn test_cargo_options_update_from_json_refresh_negative_means_no_partial() {
        let mut opts = CargoOptions::default();
        let json = serde_json::json!({"refreshIntervalSeconds": -1});
        opts.update_from_json_obj(json.as_object().unwrap()).unwrap();
        assert_eq!(opts.refresh_interval_seconds, None);
    }

    #[test]
    fn test_cargo_options_update_from_json_rejects_wrong_type() {
        let mut opts = CargoOptions::default();
        let json = serde_json::json!({"command": 42});
        assert!(opts.update_from_json_obj(json.as_object().unwrap()).is_err());
    }

    #[test]
    fn test_cargo_options_update_from_json_partial_leaves_others_unchanged() {
        let mut opts = CargoOptions {
            command: "clippy".into(),
            ..CargoOptions::default()
        };
        let json = serde_json::json!({"checkOnSave": false});
        opts.update_from_json_obj(json.as_object().unwrap()).unwrap();
        assert_eq!(opts.command, "clippy");
        assert!(!opts.check_on_save);
    }

    #[test]
    fn test_cargo_options_reset_restores_defaults() {
        let mut opts = CargoOptions {
            command: "clippy".into(),
            features: CargoFeatures::List(vec!["foo".into()]),
            check_on_save: false,
            ..CargoOptions::default()
        };
        opts.reset();
        let defaults = CargoOptions::default();
        assert_eq!(opts.command, defaults.command);
        assert_eq!(opts.features, defaults.features);
        assert_eq!(opts.check_on_save, defaults.check_on_save);
    }

    #[test]
    fn test_bacon_options_update_from_json_full_roundtrip() {
        let mut opts = BaconOptions::default();
        let json = serde_json::json!({
            "locationsFile": "custom.locations",
            "runInBackground": false,
            "runInBackgroundCommand": "/usr/local/bin/bacon",
            "runInBackgroundCommandArguments": "--headless -j custom",
            "validatePreferences": false,
            "createPreferencesFile": false,
            "synchronizeAllOpenFilesWaitMillis": 500,
            "updateOnSave": false,
            "updateOnSaveWaitMillis": 250,
        });
        opts.update_from_json_obj(json.as_object().unwrap()).unwrap();
        assert_eq!(opts.locations_file, "custom.locations");
        assert!(!opts.run_in_background);
        assert_eq!(opts.run_in_background_command, "/usr/local/bin/bacon");
        assert_eq!(opts.run_in_background_command_args, "--headless -j custom");
        assert!(!opts.validate_preferences);
        assert!(!opts.create_preferences_file);
        assert_eq!(opts.synchronize_all_open_files_wait, Duration::from_millis(500));
        assert!(!opts.update_on_save);
        assert_eq!(opts.update_on_save_wait, Duration::from_millis(250));
    }

    #[test]
    fn test_bacon_options_update_from_json_rejects_wrong_type() {
        let mut opts = BaconOptions::default();
        let json = serde_json::json!({"runInBackground": "yes"});
        assert!(opts.update_from_json_obj(json.as_object().unwrap()).is_err());
    }

    #[test]
    fn test_bacon_options_reset_restores_defaults() {
        let mut opts = BaconOptions {
            run_in_background: false,
            locations_file: "foo".into(),
            ..BaconOptions::default()
        };
        opts.reset();
        let defaults = BaconOptions::default();
        assert_eq!(opts.run_in_background, defaults.run_in_background);
        assert_eq!(opts.locations_file, defaults.locations_file);
    }

    #[test]
    fn test_correction_from_single_empty_is_remove() {
        let range = Range::default();
        let c = Correction::from_single(range, "");
        assert_eq!(c.label, "Remove");
        assert_eq!(c.edits.len(), 1);
        assert_eq!(c.edits[0].new_text, "");
    }

    #[test]
    fn test_correction_from_single_nonempty_is_replace() {
        let range = Range::default();
        let c = Correction::from_single(range, "foo");
        assert_eq!(c.label, "Replace with: foo");
        assert_eq!(c.edits.len(), 1);
    }

    #[test]
    fn test_correction_from_multi_all_empty_is_remove() {
        let edits = vec![
            CorrectionEdit {
                range: Range::default(),
                new_text: "".into(),
            },
            CorrectionEdit {
                range: Range::default(),
                new_text: "".into(),
            },
        ];
        let c = Correction::from_multi(edits);
        assert_eq!(c.label, "Remove");
        assert_eq!(c.edits.len(), 2);
    }

    #[test]
    fn test_correction_from_multi_labels_by_first_nonempty() {
        let edits = vec![
            CorrectionEdit {
                range: Range::default(),
                new_text: "".into(),
            },
            CorrectionEdit {
                range: Range::default(),
                new_text: "new".into(),
            },
        ];
        let c = Correction::from_multi(edits);
        assert_eq!(c.label, "Replace with: new");
    }

    #[test]
    fn test_severity_tag_distinguishes_levels() {
        assert_eq!(severity_tag(None), 0);
        assert_eq!(severity_tag(Some(DiagnosticSeverity::ERROR)), 1);
        assert_eq!(severity_tag(Some(DiagnosticSeverity::WARNING)), 2);
        assert_eq!(severity_tag(Some(DiagnosticSeverity::INFORMATION)), 3);
        assert_eq!(severity_tag(Some(DiagnosticSeverity::HINT)), 4);
        // All four constants must hash to distinct tags or dedup will fold
        // legitimately-different diagnostics together.
        let tags = [
            severity_tag(Some(DiagnosticSeverity::ERROR)),
            severity_tag(Some(DiagnosticSeverity::WARNING)),
            severity_tag(Some(DiagnosticSeverity::INFORMATION)),
            severity_tag(Some(DiagnosticSeverity::HINT)),
        ];
        let unique: HashSet<_> = tags.iter().collect();
        assert_eq!(unique.len(), tags.len());
    }

    #[test]
    fn test_diag_key_collides_for_equal_diagnostics() {
        let a = Diagnostic {
            range: Range::default(),
            severity: Some(DiagnosticSeverity::ERROR),
            message: "hi".into(),
            ..Diagnostic::default()
        };
        let b = a.clone();
        assert_eq!(diag_key(&a), diag_key(&b));
    }

    #[test]
    fn test_diag_key_differs_when_message_differs() {
        let mut a = Diagnostic {
            range: Range::default(),
            severity: Some(DiagnosticSeverity::ERROR),
            message: "first".into(),
            ..Diagnostic::default()
        };
        let b = a.clone();
        a.message = "second".into();
        assert_ne!(diag_key(&a), diag_key(&b));
    }

    #[test]
    fn test_path_to_file_uri_empty_path() {
        // Empty path yields the trivial `file://` URI. Useful guard against
        // future regressions in the encoding helper when fed degenerate input.
        assert_eq!(path_to_file_uri(""), "file://");
    }

    #[test]
    fn test_correction_from_single_label_replaces_with_text() {
        let c = Correction::from_single(Range::default(), "x");
        assert_eq!(c.label, "Replace with: x");
        assert_eq!(c.edits.len(), 1);
        assert_eq!(c.edits[0].new_text, "x");
    }

    #[test]
    fn test_correction_from_multi_empty_edits_is_remove() {
        let c = Correction::from_multi(vec![]);
        assert_eq!(c.label, "Remove");
        assert!(c.edits.is_empty());
    }

    #[test]
    fn test_cargo_options_env_roundtrip_preserves_order_in_serde_iteration() {
        // serde_json::Map preserves insertion order. We rely on that for
        // reproducible env propagation into cargo.
        let mut opts = CargoOptions::default();
        let json = serde_json::json!({
            "env": {"A": "1", "B": "2", "C": "3"}
        });
        opts.update_from_json_obj(json.as_object().unwrap()).unwrap();
        assert_eq!(opts.env.len(), 3);
        let keys: Vec<_> = opts.env.iter().map(|(k, _)| k.as_str()).collect();
        assert_eq!(keys, vec!["A", "B", "C"]);
    }

    #[test]
    fn test_cargo_options_update_rejects_non_object_env() {
        let mut opts = CargoOptions::default();
        let json = serde_json::json!({"env": ["A=1"]});
        assert!(opts.update_from_json_obj(json.as_object().unwrap()).is_err());
    }

    #[test]
    fn test_cargo_options_update_rejects_non_string_env_value() {
        let mut opts = CargoOptions::default();
        let json = serde_json::json!({"env": {"A": 1}});
        assert!(opts.update_from_json_obj(json.as_object().unwrap()).is_err());
    }

    #[test]
    fn test_cargo_options_update_rejects_non_string_feature_item() {
        let mut opts = CargoOptions::default();
        let json = serde_json::json!({"features": ["a", 2, "c"]});
        assert!(opts.update_from_json_obj(json.as_object().unwrap()).is_err());
    }

    #[test]
    fn test_cargo_options_publish_mode_toggle_via_cancel_running() {
        let mut opts = CargoOptions::default();
        // Default is CancelRunning.
        assert!(matches!(opts.publish_mode, PublishMode::CancelRunning));
        opts.update_from_json_obj(serde_json::json!({"cancelRunning": false}).as_object().unwrap())
            .unwrap();
        assert!(matches!(opts.publish_mode, PublishMode::QueueIfRunning));
        opts.update_from_json_obj(serde_json::json!({"cancelRunning": true}).as_object().unwrap())
            .unwrap();
        assert!(matches!(opts.publish_mode, PublishMode::CancelRunning));
    }

    #[test]
    fn test_cargo_options_separate_child_diagnostics_can_unset() {
        let mut opts = CargoOptions {
            separate_child_diagnostics: Some(true),
            ..CargoOptions::default()
        };
        // `as_bool()` on a non-bool returns None — and we feed that through
        // unchanged, so a `null` (or anything non-bool) clears the override.
        opts.update_from_json_obj(
            serde_json::json!({"separateChildDiagnostics": null})
                .as_object()
                .unwrap(),
        )
        .unwrap();
        assert_eq!(opts.separate_child_diagnostics, None);
    }

    #[tokio::test]
    async fn test_find_git_root_directory_returns_none_outside_git() {
        let tmp = tempfile::TempDir::new().unwrap();
        let root = BaconLs::find_git_root_directory(tmp.path()).await;
        assert_eq!(root, None);
    }

    #[tokio::test]
    async fn test_find_git_root_directory_finds_top_of_repo() {
        // `git -C <subdir> rev-parse --show-toplevel` should resolve to the
        // crate's own repo root regardless of which subdirectory we point at.
        let crate_root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"));
        let src = crate_root.join("src");
        let from_subdir = BaconLs::find_git_root_directory(&src).await;
        assert!(from_subdir.is_some(), "src/ is inside a git repo");
        let from_root = BaconLs::find_git_root_directory(crate_root).await.unwrap();
        // Both lookups should resolve to the same toplevel.
        assert_eq!(from_subdir.unwrap(), from_root);
    }

    #[test]
    fn test_init_cargo_backend_uses_existing_project_root() {
        let tmp = tempfile::TempDir::new().unwrap();
        let root = tmp.path().to_path_buf();
        let mut state = State {
            project_root: Some(root.clone()),
            ..State::default()
        };
        // Normally we'd hold an RwLockWriteGuard, but for this unit test we
        // adapt the API by going through a real lock.
        let lock = RwLock::new(std::mem::take(&mut state));
        let mut guard = lock.try_write().unwrap();
        BaconLs::init_cargo_backend(&mut guard, CargoOptions::default())
            .expect("init should succeed with explicit project root");
        match &guard.backend {
            Some(BackendRuntime::Cargo { runtime, .. }) => {
                assert_eq!(runtime.build_folder, root);
                assert_eq!(runtime.run_state, CargoRunState::Idle);
                assert_eq!(runtime.diagnostics_version, 0);
            }
            other => panic!("expected Cargo backend, got {other:?}"),
        }
    }

    #[test]
    fn test_init_cargo_backend_falls_back_to_cwd_when_no_project_root() {
        let mut state = State::default();
        let lock = RwLock::new(std::mem::take(&mut state));
        let mut guard = lock.try_write().unwrap();
        BaconLs::init_cargo_backend(&mut guard, CargoOptions::default())
            .expect("init should fall back to CWD when project root is unset");
        match &guard.backend {
            Some(BackendRuntime::Cargo { runtime, .. }) => {
                let cwd = std::env::current_dir().unwrap();
                assert_eq!(runtime.build_folder, cwd, "should fall back to CWD");
            }
            other => panic!("expected Cargo backend, got {other:?}"),
        }
    }

    #[test]
    fn test_cargo_options_build_args_with_env_does_not_leak_into_args() {
        // Sanity: env values are not added as command-line args.
        let opts = CargoOptions {
            env: vec![("A".into(), "1".into())],
            ..CargoOptions::default()
        };
        let args = opts.build_command_args();
        assert!(args.iter().all(|a| !a.contains("A=1") && !a.contains("=1")));
    }
}


================================================
FILE: src/lsp.rs
================================================
use std::collections::HashMap;

use ls_types::{
    CodeAction, CodeActionKind, CodeActionOptions, CodeActionOrCommand, CodeActionParams, CodeActionProviderCapability,
    CodeActionResponse, DeleteFilesParams, DidChangeConfigurationParams, DidChangeTextDocumentParams,
    DidCloseTextDocumentParams, DidOpenTextDocumentParams, DidSaveTextDocumentParams, ExecuteCommandOptions,
    ExecuteCommandParams, FileOperationFilter, FileOperationPattern, FileOperationRegistrationOptions,
    InitializeParams, InitializeResult, InitializedParams, LSPAny, MessageType, PositionEncodingKind,
    PublishDiagnosticsClientCapabilities, RenameFilesParams, ServerCapabilities, ServerInfo,
    TextDocumentClientCapabilities, TextDocumentSyncCapability, TextDocumentSyncKind, TextDocumentSyncOptions,
    TextDocumentSyncSaveOptions, TextEdit, Uri, WorkDoneProgressOptions, WorkspaceEdit,
    WorkspaceFileOperationsServerCapabilities, WorkspaceServerCapabilities,
};
use tower_lsp_server::{LanguageServer, jsonrpc};

use crate::{
    BackendChoice, BackendRuntime, BaconLs, Cargo, CargoOptions, CorrectionEdit, DiagnosticData, PKG_NAME, PKG_VERSION,
};

impl LanguageServer for BaconLs {
    async fn initialize(&self, params: InitializeParams) -> jsonrpc::Result<InitializeResult> {
        tracing::info!("initializing {PKG_NAME} v{PKG_VERSION}",);
        tracing::debug!("initializing with input parameters: {params:#?}");
        let project_root = Cargo::find_project_root(&params).await;
        tracing::debug!("Found project root: {project_root:?}");

        if let Some(TextDocumentClientCapabilities {
            publish_diagnostics: Some(PublishDiagnosticsClientCapabilities { .. }),
            ..
        }) = params.capabilities.text_document
        {
            tracing::info!("client supports diagnostics");
        } else {
            tracing::warn!("client does not support diagnostics");
            return Err(jsonrpc::Error::new(jsonrpc::ErrorCode::InvalidRequest));
        }

        let mut diagnostics_data_supported = false;
        let mut related_information_supported = false;
        if let Some(TextDocumentClientCapabilities {
            publish_diagnostics:
                Some(PublishDiagnosticsClientCapabilities {
                    data_support,
                    related_information,
                    ..
                }),
            ..
        }) = params.capabilities.text_document
        {
            if data_support == Some(true) {
                tracing::info!("client supports diagnostics data");
                diagnostics_data_supported = true;
            } else {
                tracing::warn!("client does not support diagnostics data");
            }
            if related_information == Some(true) {
                tracing::info!("client supports related information");
                related_information_supported = true;
            } else {
                tracing::info!("client does not support related information");
            }
        } else {
            tracing::warn!("client does not support diagnostics data");
        }

        // Initialization options are the only place we can read user
        // configuration before responding to `initialize`. We need that for
        // `cargo.updateOnInsert`: the LSP capability `textDocument/didChange`
        // sync mode has to be advertised statically — clients (Neovim
        // included) don't reliably retrofit already-attached buffers when we
        // try to register it dynamically post-`initialized`.
        let init_update_on_insert = params
            .initialization_options
            .as_ref()
            .and_then(|v| v.get("cargo"))
            .and_then(|v| v.get("updateOnInsert"))
            .and_then(|v| v.as_bool())
            .unwrap_or(false);
        if init_update_on_insert {
            tracing::info!(
                "initialization_options.cargo.updateOnInsert = true; advertising textDocument/didChange (Full sync)"
            );
        }

        let mut state = self.state.write().await;
        state.project_root = project_root;
        state.workspace_folders = params.workspace_folders;
        state.diagnostics_data_supported = diagnostics_data_supported;
        state.related_information_supported = related_information_supported;
        state.init_update_on_insert = init_update_on_insert;
        tracing::trace!("loaded state from lsp settings: {state:#?}");
        drop(state);

        // Declare didDelete/didRename so clients actually send those events
        // (handlers live in this file). The bacon backend tracks open files
        // and needs these to keep its set in sync when the user renames/deletes
        // through the file explorer. Cargo backend is unaffected but the
        // capability is cheap to advertise.
        let rust_file_filter = FileOperationFilter {
            scheme: Some("file".to_string()),
            pattern: FileOperationPattern {
                glob: "**/*.rs".to_string(),
                matches: None,
                options: None,
            },
        };
        let file_ops_registration = FileOperationRegistrationOptions {
            filters: vec![rust_file_filter],
        };
        Ok(InitializeResult {
            capabilities: ServerCapabilities {
                // Only support UTF-16 positions for now, which is the default when unspecified
                position_encoding: Some(PositionEncodingKind::UTF16),
                // Default: no change events — diagnostics come from bacon's
                // locations file or from cargo's JSON output. The cargo
                // backend's `updateOnInsert` mode flips this to Full when the
                // user opts in via `initialization_options.cargo.updateOnInsert`,
                // so non-users never pay for buffer-shipping.
                text_document_sync: Some(TextDocumentSyncCapability::Options(TextDocumentSyncOptions {
                    open_close: Some(true),
                    change: Some(if init_update_on_insert {
                        TextDocumentSyncKind::FULL
                    } else {
                        TextDocumentSyncKind::NONE
                    }),
                    save: Some(TextDocumentSyncSaveOptions::Supported(true)),
                    ..Default::default()
                })),
                code_action_provider: Some(CodeActionProviderCapability::Options(CodeActionOptions {
                    code_action_kinds: Some(vec![CodeActionKind::QUICKFIX]),
                    work_done_progress_options: WorkDoneProgressOptions {
                        work_done_progress: Some(false),
                    },
                    resolve_provider: None,
                })),
                execute_command_provider: Some(ExecuteCommandOptions {
                    commands: vec!["bacon_ls.run".to_string()],
                    ..Default::default()
                }),
                workspace: Some(WorkspaceServerCapabilities {
                    workspace_folders: None,
                    file_operations: Some(WorkspaceFileOperationsServerCapabilities {
                        did_rename: Some(file_ops_registration.clone()),
                        did_delete: Some(file_ops_registration),
                        ..Default::default()
                    }),
                }),
                ..Default::default()
            },
            server_info: Some(ServerInfo {
                name: PKG_NAME.to_string(),
                version: Some(PKG_VERSION.to_string()),
            }),
            // See <https://clangd.llvm.org/extensions.html#utf-8-offsets>.
            // which says:
            // ```
            // This extension has been deprecated with clangd-21 in favor of
            // the positionEncoding introduced in LSP 3.17. It’ll go away with clangd-23
            // ```
            // So None should be fine
            offset_encoding: None,
        })
    }

    async fn initialized(&self, _: InitializedParams) {
        self.pull_configuration().await;

        let mut state = self.state.write().await;
        if state.backend.is_none() {
            // No workspace/configuration response (or empty). Still honor
            // the init-options seed so live mode works for clients that only
            // provide settings via `initialization_options`.
            let mut config = CargoOptions::default();
            if state.init_update_on_insert {
                config.update_on_insert = true;
            }
            if let Err(e) = Self::init_cargo_backend(&mut state, config) {
                tracing::error!("{e}");
                drop(state);
                self.client.show_message(MessageType::ERROR, e).await;
                return;
            }
        }
        let backend_chosen = state
            .backend
            .as_ref()
            .expect("backend initialized above")
            .backend_choice();
        drop(state);

        tracing::info!("{PKG_NAME} v{PKG_VERSION} lsp server initialized with backend: {backend_chosen:?}");
        self.client
            .log_message(
                MessageType::INFO,
                format!("{PKG_NAME} v{PKG_VERSION} lsp server initialized with backend: {backend_chosen:?}"),
            )
            .await;

        tracing::info!("initialized complete");

        if backend_chosen == BackendChoice::Cargo {
            tracing::info!("triggering initial cargo diagnostics");
            self.publish_cargo_diagnostics().await
        }
    }

    async fn did_change_configuration(&self, params: DidChangeConfigurationParams) {
        tracing::info!("client sent didChangeConfiguration");
        if let Some(settings) = params.settings.as_object()
            && !settings.is_empty()
        {
            if let Some(settings) = settings.get("bacon_ls") {
                tracing::debug!("using client provided settings");
                self.adapt_to_settings(settings).await;
            }
        } else {
            tracing::debug!("settings is either not an object or is empty");
            self.pull_configuration().await;
        }
    }

    async fn did_open(&self, params: DidOpenTextDocumentParams) {
        tracing::trace!("client sent didOpen request");
        let mut state = self.state.write().await;
        match &mut state.backend {
            Some(BackendRuntime::Bacon { runtime, .. }) => {
                runtime.open_files.insert(params.text_document.uri.clone());
                drop(state);
                self.publish_bacon_diagnostics(&params.text_document.uri).await;
            }
            Some(BackendRuntime::Cargo { runtime, .. }) => {
                // Debounce against the initial cargo run: on client startup,
                // `initialized` kicks off a run and the first `didOpen`
                // arrives in the same flurry. Skipping here lets the in-flight
                // run complete instead of being cancelled and restarted.
                if let Some(ts) = runtime.last_run_started
                    && ts.elapsed() < std::time::Duration::from_secs(1)
                {
                    tracing::trace!("did_open within debounce window of last cargo trigger; skipping");
                    return;
                }
                drop(state);
                self.publish_cargo_diagnostics().await;
            }
            None => {}
        }
    }

    async fn did_close(&self, params: DidCloseTextDocumentParams) {
        tracing::trace!("client sent didClose request");
        let mut state = self.state.write().await;
        if let Some(BackendRuntime::Bacon { runtime, .. }) = &mut state.backend {
            runtime.open_files.remove(&params.text_document.uri);
            drop(state);
            self.publish_bacon_diagnostics(&params.text_document.uri).await;
            return;
        }
        drop(state);
        // Cargo backend with live shadow: revert any dirty buffer for the
        // closed file back to a hardlink so subsequent live runs read the
        // on-disk version.
        self.restore_shadow_link_if_dirty(&params.text_document.uri).await;
    }

    async fn did_save(&self, params: DidSaveTextDocumentParams) {
        tracing::debug!("client sent didSave request");
        let state = self.state.read().await;
        let Some(backend) = &state.backend else {
            return;
        };
        match backend {
            BackendRuntime::Bacon { config, .. } => {
                if config.update_on_save {
                    if !config.update_on_save_wait.is_zero() {
                        tokio::time::sleep(config.update_on_save_wait).await;
                    }
                    drop(state);
                    self.publish_bacon_diagnostics(&params.text_document.uri).await;
                }
            }
            BackendRuntime::Cargo { config, .. } => {
                let check_on_save = config.check_on_save;
                drop(state);
                // A pending live run would race with the canonical save run
                // and publish stale (pre-save) shadow diagnostics on top.
                // Cancel it before doing anything else.
                self.cancel_live_debounce().await;
                // Save makes the shadow's dirty override stale: the on-disk
                // file now matches what the user wants checked. Restore the
                // hardlink before the cargo run so the live target dir picks
                // up the saved content next time it's used.
                self.restore_shadow_link_if_dirty(&params.text_document.uri).await;
                if check_on_save {
                    self.publish_cargo_diagnostics().await;
                }
            }
        }
    }

    async fn did_change(&self, params: DidChangeTextDocumentParams) {
        // Live mode is only meaningful for the cargo backend; the bacon
        // backend reads diagnostics from a file written by an external bacon
        // process. Bail early to keep this hot path cheap when disabled.
        let live_on = {
            let state = self.state.read().await;
            matches!(
                &state.backend,
                Some(BackendRuntime::Cargo { config, .. }) if config.update_on_insert
            )
        };
        if !live_on {
            tracing::debug!("did_change ignored: updateOnInsert is off");
            return;
        }
        tracing::info!(
            uri = params.text_document.uri.as_str(),
            changes = params.content_changes.len(),
            "did_change received (live mode)"
        );

        // We register the change capability dynamically with `Full` sync
        // (one entry, range = None, full text). Anything else is a client
        // mismatch — log and skip rather than guess.
        let Some(content) = params.content_changes.into_iter().find(|c| c.range.is_none()) else {
            tracing::warn!("did_change without full-sync content; client may not honor dynamic registration");
            return;
        };

        self.live_update_dirty(params.text_document.uri, content.text).await;
    }

    async fn did_delete_files(&self, params: DeleteFilesParams) {
        tracing::debug!("client sent didDeleteFiles request for {:?}", params.files);
        let mut state = self.state.write().await;
        if let Some(BackendRuntime::Bacon { runtime, .. }) = &mut state.backend {
            for file in params.files {
                if let Ok(uri) = str::parse::<Uri>(&file.uri) {
                    runtime.open_files.remove(&uri);
                }
            }
        }
        drop(state);
    }

    async fn did_rename_files(&self, params: RenameFilesParams) {
        tracing::debug!("client sent didRenameFiles request for {:?}", params.files);
        for file in params.files {
            if let (Ok(old_uri), Ok(new_uri)) = (str::parse::<Uri>(&file.old_uri), str::parse::<Uri>(&file.new_uri)) {
                let mut state = self.state.write().await;
                if let Some(BackendRuntime::Bacon { runtime, .. }) = &mut state.backend {
                    runtime.open_files.remove(&old_uri);
                    runtime.open_files.insert(new_uri.clone());
                }
                drop(state);
                self.publish_bacon_diagnostics(&new_uri).await;
            }
        }
    }

    async fn code_action(&self, params: CodeActionParams) -> jsonrpc::Result<Option<CodeActionResponse>> {
        tracing::trace!("client sent codeActions request");
        let state = self.state.read().await;
        let diagnostics_data_supported = state.diagnostics_data_supported;
        drop(state);

        if !diagnostics_data_supported {
            return Ok(None);
        }

        let bacon_ls = "bacon-ls".to_string();
        let actions = params
            .context
            .diagnostics
            .iter()
            .filter(|diag| diag.source.as_ref() == Some(&bacon_ls))
            .flat_map(|diag| match &diag.data {
                Some(data) => {
                    if let Ok(DiagnosticData { corrections }) = serde_json::from_value::<DiagnosticData>(data.clone()) {
                        corrections
                            .iter()
                            .map(|c| {
                                CodeActionOrCommand::CodeAction(CodeAction {
                                    title: c.label.clone(),
                                    kind: Some(CodeActionKind::QUICKFIX),
                                    diagnostics: Some(vec![diag.clone()]),
                                    edit: Some(WorkspaceEdit {
                                        changes: Some(HashMap::from([(
                                            params.text_document.uri.clone(),
                                            c.edits
                                                .iter()
                                                .map(|e: &CorrectionEdit| TextEdit {
                                                    range: e.range,
                                                    new_text: e.new_text.clone(),
                                                })
                                                .collect(),
                                        )])),
                                        ..WorkspaceEdit::default()
                                    }),
                                    is_preferred: if corrections.len() == 1 { Some(true) } else { None },
                                    ..CodeAction::default()
                                })
                            })
                            .collect()
                    } else {
                        tracing::error!("deserialization failed: received {data:?} as diagnostic data",);
                        vec![]
                    }
                }
                None => {
                    tracing::debug!("client doesn't support diagnostic data");
                    vec![]
                }
            })
            .collect::<Vec<_>>();

        Ok(Some(actions))
    }

    async fn execute_command(&self, params: ExecuteCommandParams) -> jsonrpc::Result<Option<LSPAny>> {
        if params.command == "bacon_ls.run" {
            let state = self.state.read().await;
            if let Some(BackendRuntime::Cargo { .. }) = state.backend.as_ref() {
                drop(state);
                self.publish_cargo_diagnostics().await;
            }
            return Ok(None);
        }

        Err(jsonrpc::Error::method_not_found())
    }

    async fn shutdown(&self) -> jsonrpc::Result<()> {
        tracing::info!("shutdown requested");
        let mut state = self.state.write().await;
        let backend = state.backend.take();
        drop(state);

        if let Some(backend) = backend {
            match backend {
                BackendRuntime::Bacon { mut runtime, .. } => {
                    runtime.shutdown_token.cancel();
                    // Cap each await so a stuck bacon subprocess or watcher can't
                    // keep the LSP alive past the client's restart deadline.
                    let deadline = std::time::Duration::from_secs(2);
                    if let Some(handle) = runtime.command_handle.take() {
                        tracing::info!("terminating bacon from running in background");
                        match tokio::time::timeout(deadline, handle).await {
                            Ok(Ok(())) => {}
                            Ok(Err(e)) => {
                                tracing::warn!("bacon command task failed during shutdown: {e}")
                            }
                            Err(_) => tracing::warn!("bacon command task timed out during shutdown"),
                        }
                    }
                    let sync_handle = runtime.sync_files_handle;
                    match tokio::time::timeout(deadline, sync_handle).await {
                        Ok(Ok(())) => {}
                        Ok(Err(e)) => tracing::warn!("sync files task failed during shutdown: {e}"),
                        Err(_) => tracing::warn!("sync files task timed out during shutdown"),
                    }
                }
                BackendRuntime::Cargo { mut runtime, .. } => {
                    runtime.cancel_token.cancel();
                    // Abort any pending live debounced trigger so the spawned
                    // sleep doesn't outlive the server and try to invoke
                    // cargo against a torn-down backend.
                    if let Some(handle) = runtime.live_debounce.take() {
                        handle.abort();
                    }
                }
            }
        }

        tracing::info!("{PKG_NAME} v{PKG_VERSION} lsp server stopped");
        self.client
            .log_message(
                MessageType::INFO,
                format!("{PKG_NAME} v{PKG_VERSION} lsp server stopped"),
            )
            .await;

        // Force-exit watchdog. An in-flight server-to-client request (e.g. the
        // `workspace/configuration` we issue from `initialized`) can keep
        // `Server::serve()` alive past the `exit` notification, because there's
        // no way to cancel a waiter on a client response that will never
        // arrive. Without this, `:LspRestart` in Neovim sees the server never
        // die and gives up on spawning a fresh instance.
        tokio::spawn(async {
            tokio::time::sleep(std::time::Duration::from_millis(500)).await;
            std::process::exit(0);
        });

        Ok(())
    }
}


================================================
FILE: src/main.rs
================================================
//! Bacon Language Server
use bacon_ls::BaconLs;

#[tokio::main(flavor = "multi_thread", worker_threads = 4)]
async fn main() {
    let args: bacon_ls::Args = argh::from_env();
    if args.version {
        println!("{}", bacon_ls::PKG_VERSION);
    } else {
        BaconLs::serve().await;
    }
}


================================================
FILE: src/native.rs
================================================
use std::{
    env,
    path::{Path, PathBuf},
    process::Stdio,
};

use anyhow::Context;
use ls_types::{
    Diagnostic, DiagnosticRelatedInformation, DiagnosticSeverity, DiagnosticTag, InitializeParams, Location, Position,
    Range, Uri,
};
use serde::{Deserialize, Deserializer};
use tokio::io::{AsyncBufRead, AsyncBufReadExt};
use tokio::process::Command;
use tower_lsp_server::{Bounded, NotCancellable, OngoingProgress};

use crate::{BaconLs, Correction, CorrectionEdit, DiagnosticData, PKG_NAM
Download .txt
gitextract_sv33u4e_/

├── .github/
│   ├── ISSUE_TEMPLATE/
│   │   └── bug_report.md
│   ├── dependabot.yml
│   └── workflows/
│       ├── auto-merge.yml
│       ├── ci.yml
│       ├── code-coverage.yml
│       └── release.yml
├── .gitignore
├── .prettierignore
├── .vscodeignore
├── Cargo.toml
├── LICENSE
├── README.md
├── eslint.config.mjs
├── flake.nix
├── package.json
├── rustfmt.toml
├── src/
│   ├── bacon.rs
│   ├── lib.rs
│   ├── lsp.rs
│   ├── main.rs
│   ├── native.rs
│   ├── shadow.rs
│   └── testdata/
│       ├── expansion-needed.json
│       ├── unused-import-compact.json
│       ├── unused-import-line.json
│       ├── unused-import.json
│       └── unused-variable.json
├── tests/
│   ├── cargo_backend.rs
│   └── lsp_restart.rs
├── tsconfig.json
└── vscode/
    └── extension.ts
Download .txt
SYMBOL INDEX (279 symbols across 9 files)

FILE: src/bacon.rs
  type BaconConfig (line 27) | struct BaconConfig {
  type Jobs (line 33) | struct Jobs {
  type BaconLsJob (line 39) | struct BaconLsJob {
  type Exports (line 47) | struct Exports {
  type CargoJsonSpans (line 53) | struct CargoJsonSpans {
  constant ERROR_MESSAGE (line 60) | const ERROR_MESSAGE: &str = "bacon configuration is not compatible with ...
  constant BACON_ANALYZER (line 61) | const BACON_ANALYZER: &str = "cargo_json";
  constant BACON_EXPORTER (line 62) | const BACON_EXPORTER: &str = "analyzer";
  constant BACON_COMMAND (line 63) | const BACON_COMMAND: [&str; 7] = [
  constant LINE_FORMAT (line 72) | const LINE_FORMAT: &str = "{diagnostic.level}|:|{span.file_name}|:|{span...
  type Bacon (line 74) | pub(crate) struct Bacon;
    method validate_preferences_file (line 77) | async fn validate_preferences_file(path: &Path) -> Result<(), String> {
    method create_preferences_file (line 97) | async fn create_preferences_file(filename: &str) -> Result<(), String> {
    method validate_preferences_impl (line 129) | async fn validate_preferences_impl(bacon_prefs: &[u8], create_prefs_fi...
    method find_bacon_locations (line 154) | pub(crate) async fn find_bacon_locations(root: &Path, locations_file_n...
    method parse_severity (line 172) | fn parse_severity(severity_str: &str) -> DiagnosticSeverity {
    method parse_positions (line 185) | fn parse_positions(fields: &[&str]) -> Option<(u32, u32, u32, u32)> {
    method parse_bacon_diagnostic_line (line 193) | fn parse_bacon_diagnostic_line(line: &str, folder_path: &Path) -> Opti...
    method deduplicate_diagnostics (line 270) | fn deduplicate_diagnostics(
    method validate_preferences (line 285) | pub(crate) async fn validate_preferences(bacon_command: &str, create_p...
    method run_in_background (line 294) | pub(crate) async fn run_in_background(
    method diagnostics (line 354) | async fn diagnostics(
    method diagnostics_vec (line 456) | async fn diagnostics_vec(
    method synchronize_diagnostics (line 468) | pub(crate) async fn synchronize_diagnostics(state: Arc<RwLock<State>>,...
    method publish_diagnostics (line 562) | pub(crate) async fn publish_diagnostics(
  function test_valid_bacon_preferences (line 586) | async fn test_valid_bacon_preferences() {
  function test_invalid_analyzer (line 608) | async fn test_invalid_analyzer() {
  function test_invalid_line_format (line 631) | async fn test_invalid_line_format() {
  function test_validate_preferences (line 654) | async fn test_validate_preferences() {
  function test_file_creation_failure (line 676) | async fn test_file_creation_failure() {
  function test_file_write_failure (line 684) | async fn test_file_write_failure() {
  function test_empty_bacon_preferences_file (line 695) | async fn test_empty_bacon_preferences_file() {
  function test_run_in_background (line 703) | async fn test_run_in_background() {
  constant ERROR_LINE (line 711) | const ERROR_LINE: &str = "error|:|/app/github/bacon-ls/src/lib.rs|:|352|...
  function test_parse_bacon_diagnostic_line_with_spans_ok (line 714) | fn test_parse_bacon_diagnostic_line_with_spans_ok() {
  function test_parse_bacon_diagnostic_line_with_spans_ko (line 743) | fn test_parse_bacon_diagnostic_line_with_spans_ko() {
  function test_bacon_multiline_diagnostics_production (line 755) | async fn test_bacon_multiline_diagnostics_production() {
  function test_bacon_diagnostics_production_and_deduplication (line 818) | async fn test_bacon_diagnostics_production_and_deduplication() {
  function test_parse_severity_known_levels (line 858) | fn test_parse_severity_known_levels() {
  function test_parse_severity_unknown_level_defaults_to_information (line 870) | fn test_parse_severity_unknown_level_defaults_to_information() {
  function test_parse_positions_valid (line 878) | fn test_parse_positions_valid() {
  function test_parse_positions_non_numeric_returns_none (line 884) | fn test_parse_positions_non_numeric_returns_none() {
  function test_parse_positions_too_few_fields_returns_none (line 890) | fn test_parse_positions_too_few_fields_returns_none() {
  function test_parse_bacon_diagnostic_line_with_replacement_attaches_correction (line 897) | fn test_parse_bacon_diagnostic_line_with_replacement_attaches_correction...
  function test_parse_bacon_diagnostic_line_zero_position_saturates (line 914) | fn test_parse_bacon_diagnostic_line_zero_position_saturates() {
  function test_parse_bacon_diagnostic_line_strips_ansi_from_rendered (line 926) | fn test_parse_bacon_diagnostic_line_strips_ansi_from_rendered() {
  function test_parse_bacon_diagnostic_line_too_few_fields_returns_none (line 935) | fn test_parse_bacon_diagnostic_line_too_few_fields_returns_none() {
  function test_deduplicate_diagnostics_skips_when_path_does_not_match (line 941) | fn test_deduplicate_diagnostics_skips_when_path_does_not_match() {
  function test_deduplicate_diagnostics_drops_exact_duplicate (line 958) | fn test_deduplicate_diagnostics_drops_exact_duplicate() {
  function test_deduplicate_diagnostics_keeps_distinct_severity (line 974) | fn test_deduplicate_diagnostics_keeps_distinct_severity() {
  function test_find_bacon_locations_finds_nested_files (line 991) | async fn test_find_bacon_locations_finds_nested_files() {
  function test_find_bacon_locations_empty_dir_returns_empty (line 1009) | async fn test_find_bacon_locations_empty_dir_returns_empty() {
  function test_find_bacon_locations_missing_root_errors (line 1018) | async fn test_find_bacon_locations_missing_root_errors() {
  function test_validate_preferences_impl_creates_when_missing_and_requested (line 1026) | async fn test_validate_preferences_impl_creates_when_missing_and_request...
  function test_validate_preferences_impl_no_create_when_disabled (line 1042) | async fn test_validate_preferences_impl_no_create_when_disabled() {

FILE: src/lib.rs
  constant PKG_NAME (line 27) | const PKG_NAME: &str = env!("CARGO_PKG_NAME");
  constant PKG_VERSION (line 28) | pub const PKG_VERSION: &str = env!("CARGO_PKG_VERSION");
  constant LOCATIONS_FILE (line 29) | const LOCATIONS_FILE: &str = ".bacon-locations";
  constant BACON_BACKGROUND_COMMAND (line 30) | const BACON_BACKGROUND_COMMAND: &str = "bacon";
  constant BACON_BACKGROUND_COMMAND_ARGS (line 31) | const BACON_BACKGROUND_COMMAND_ARGS: &str = "--headless -j bacon-ls";
  constant PATH_ENCODE_SET (line 38) | const PATH_ENCODE_SET: &AsciiSet = &CONTROLS
  function path_to_file_uri (line 58) | pub(crate) fn path_to_file_uri(path: &str) -> String {
  type DiagKey (line 65) | pub(crate) type DiagKey = (Range, i32, String);
  function diag_key (line 67) | pub(crate) fn diag_key(d: &Diagnostic) -> DiagKey {
  function severity_tag (line 71) | fn severity_tag(s: Option<DiagnosticSeverity>) -> i32 {
  type Args (line 84) | pub struct Args {
  type BackendChoice (line 91) | enum BackendChoice {
  type BackendRuntime (line 97) | enum BackendRuntime {
    method backend_choice (line 109) | fn backend_choice(&self) -> BackendChoice {
  type CargoRunState (line 118) | pub(crate) enum CargoRunState {
  type PublishMode (line 125) | pub(crate) enum PublishMode {
  function invalid_option (line 130) | fn invalid_option(name: &str) -> jsonrpc::Error {
  type CargoFeatures (line 139) | enum CargoFeatures {
    method from_json_value (line 153) | fn from_json_value(value: &Value) -> jsonrpc::Result<Self> {
  method default (line 147) | fn default() -> Self {
  type CargoOptions (line 179) | pub(crate) struct CargoOptions {
    method build_command_args (line 213) | pub(crate) fn build_command_args(&self) -> Vec<String> {
    method update_from_json_obj (line 254) | pub(crate) fn update_from_json_obj(&mut self, cargo_obj: &Map<String, ...
    method reset (line 354) | pub(crate) fn reset(&mut self) {
  method default (line 360) | fn default() -> Self {
  type BaconOptions (line 381) | pub(crate) struct BaconOptions {
    method update_from_json_obj (line 394) | pub(crate) fn update_from_json_obj(&mut self, bacon_obj: &Map<String, ...
    method reset (line 451) | pub fn reset(&mut self) {
  method default (line 457) | fn default() -> Self {
  type LiveCheckContext (line 475) | pub(crate) struct LiveCheckContext {
  type CargoRuntime (line 482) | pub(crate) struct CargoRuntime {
  method default (line 509) | fn default() -> Self {
  type BaconRuntime (line 525) | pub(crate) struct BaconRuntime {
  type State (line 537) | struct State {
  type CorrectionEdit (line 553) | pub(crate) struct CorrectionEdit {
  type Correction (line 563) | pub(crate) struct Correction {
    method from_single (line 569) | pub(crate) fn from_single(range: Range, new_text: &str) -> Self {
    method from_multi (line 584) | pub(crate) fn from_multi(edits: Vec<CorrectionEdit>) -> Self {
  type DiagnosticData (line 594) | struct DiagnosticData {
  type BaconLs (line 599) | pub struct BaconLs {
    method new (line 605) | fn new(client: Client) -> Self {
    method configure_tracing (line 612) | fn configure_tracing(log_level: Option<String>, log_path: Option<&Path...
    method serve (line 653) | pub async fn serve() {
    method find_git_root_directory (line 669) | async fn find_git_root_directory(path: &Path) -> Option<PathBuf> {
    method detect_backend (line 686) | fn detect_backend(values: &Map<String, Value>) -> Result<BackendChoice...
    method pull_configuration (line 709) | async fn pull_configuration(&self) {
    method adapt_to_settings (line 741) | async fn adapt_to_settings(&self, settings: &Value) {
    method init_cargo_backend (line 931) | fn init_cargo_backend(state: &mut RwLockWriteGuard<'_, State>, config:...
    method publish_cargo_diagnostics (line 961) | async fn publish_cargo_diagnostics(&self) {
    method publish_cargo_diagnostics_live (line 968) | pub(crate) async fn publish_cargo_diagnostics_live(&self) {
    method publish_cargo_diagnostics_inner (line 990) | async fn publish_cargo_diagnostics_inner(&self, live: Option<&LiveChec...
    method ensure_shadow_built (line 1294) | pub(crate) async fn ensure_shadow_built(&self) -> Option<ShadowWorkspa...
    method live_update_dirty (line 1360) | pub(crate) async fn live_update_dirty(&self, uri: Uri, content: String) {
    method schedule_live_run (line 1396) | pub(crate) async fn schedule_live_run(&self, delay: Duration) {
    method cancel_live_debounce (line 1414) | pub(crate) async fn cancel_live_debounce(&self) {
    method restore_shadow_link_if_dirty (line 1425) | pub(crate) async fn restore_shadow_link_if_dirty(&self, uri: &Uri) {
    method publish_bacon_diagnostics (line 1447) | async fn publish_bacon_diagnostics(&self, uri: &Uri) {
    method synchronize_diagnostics (line 1469) | async fn synchronize_diagnostics(state: Arc<RwLock<State>>, client: Ar...
  function test_can_configure_tracing (line 1479) | fn test_can_configure_tracing() {
  function test_path_to_file_uri_plain_ascii (line 1489) | fn test_path_to_file_uri_plain_ascii() {
  function test_path_to_file_uri_escapes_space_and_hash_and_percent (line 1497) | fn test_path_to_file_uri_escapes_space_and_hash_and_percent() {
  function test_path_to_file_uri_preserves_path_separators (line 1507) | fn test_path_to_file_uri_preserves_path_separators() {
  function test_path_to_file_uri_relative_path_preserves_segments (line 1515) | fn test_path_to_file_uri_relative_path_preserves_segments() {
  function test_cancel_mode_replaces_token (line 1530) | fn test_cancel_mode_replaces_token() {
  function test_detect_backend_explicit_cargo (line 1540) | fn test_detect_backend_explicit_cargo() {
  function test_detect_backend_explicit_bacon (line 1546) | fn test_detect_backend_explicit_bacon() {
  function test_detect_backend_invalid_value (line 1552) | fn test_detect_backend_invalid_value() {
  function test_detect_backend_infer_from_cargo_key (line 1558) | fn test_detect_backend_infer_from_cargo_key() {
  function test_detect_backend_infer_from_bacon_key (line 1564) | fn test_detect_backend_infer_from_bacon_key() {
  function test_detect_backend_both_keys_error (line 1571) | fn test_detect_backend_both_keys_error() {
  function test_detect_backend_no_keys_defaults_to_cargo (line 1577) | fn test_detect_backend_no_keys_defaults_to_cargo() {
  function test_detect_backend_explicit_overrides_keys (line 1583) | fn test_detect_backend_explicit_overrides_keys() {
  function test_cargo_options_build_args_default (line 1589) | fn test_cargo_options_build_args_default() {
  function test_cargo_options_build_args_with_features (line 1595) | fn test_cargo_options_build_args_with_features() {
  function test_cargo_options_build_args_single_feature (line 1613) | fn test_cargo_options_build_args_single_feature() {
  function test_cargo_options_build_args_with_all_features (line 1631) | fn test_cargo_options_build_args_with_all_features() {
  function test_cargo_options_build_args_with_package_and_extras (line 1648) | fn test_cargo_options_build_args_with_package_and_extras() {
  function test_cargo_options_update_from_json_full_roundtrip (line 1670) | fn test_cargo_options_update_from_json_full_roundtrip() {
  function test_cargo_options_update_on_insert_defaults_off (line 1704) | fn test_cargo_options_update_on_insert_defaults_off() {
  function test_cargo_options_update_on_insert_debounce_rejects_negative (line 1711) | fn test_cargo_options_update_on_insert_debounce_rejects_negative() {
  function test_cargo_options_update_from_json_refresh_null_means_no_partial (line 1718) | fn test_cargo_options_update_from_json_refresh_null_means_no_partial() {
  function test_cargo_options_update_from_json_refresh_negative_means_no_partial (line 1726) | fn test_cargo_options_update_from_json_refresh_negative_means_no_partial...
  function test_cargo_options_update_from_json_rejects_wrong_type (line 1734) | fn test_cargo_options_update_from_json_rejects_wrong_type() {
  function test_cargo_options_update_from_json_partial_leaves_others_unchanged (line 1741) | fn test_cargo_options_update_from_json_partial_leaves_others_unchanged() {
  function test_cargo_options_reset_restores_defaults (line 1753) | fn test_cargo_options_reset_restores_defaults() {
  function test_bacon_options_update_from_json_full_roundtrip (line 1768) | fn test_bacon_options_update_from_json_full_roundtrip() {
  function test_bacon_options_update_from_json_rejects_wrong_type (line 1794) | fn test_bacon_options_update_from_json_rejects_wrong_type() {
  function test_bacon_options_reset_restores_defaults (line 1801) | fn test_bacon_options_reset_restores_defaults() {
  function test_correction_from_single_empty_is_remove (line 1814) | fn test_correction_from_single_empty_is_remove() {
  function test_correction_from_single_nonempty_is_replace (line 1823) | fn test_correction_from_single_nonempty_is_replace() {
  function test_correction_from_multi_all_empty_is_remove (line 1831) | fn test_correction_from_multi_all_empty_is_remove() {
  function test_correction_from_multi_labels_by_first_nonempty (line 1848) | fn test_correction_from_multi_labels_by_first_nonempty() {
  function test_severity_tag_distinguishes_levels (line 1864) | fn test_severity_tag_distinguishes_levels() {
  function test_diag_key_collides_for_equal_diagnostics (line 1883) | fn test_diag_key_collides_for_equal_diagnostics() {
  function test_diag_key_differs_when_message_differs (line 1895) | fn test_diag_key_differs_when_message_differs() {
  function test_path_to_file_uri_empty_path (line 1908) | fn test_path_to_file_uri_empty_path() {
  function test_correction_from_single_label_replaces_with_text (line 1915) | fn test_correction_from_single_label_replaces_with_text() {
  function test_correction_from_multi_empty_edits_is_remove (line 1923) | fn test_correction_from_multi_empty_edits_is_remove() {
  function test_cargo_options_env_roundtrip_preserves_order_in_serde_iteration (line 1930) | fn test_cargo_options_env_roundtrip_preserves_order_in_serde_iteration() {
  function test_cargo_options_update_rejects_non_object_env (line 1944) | fn test_cargo_options_update_rejects_non_object_env() {
  function test_cargo_options_update_rejects_non_string_env_value (line 1951) | fn test_cargo_options_update_rejects_non_string_env_value() {
  function test_cargo_options_update_rejects_non_string_feature_item (line 1958) | fn test_cargo_options_update_rejects_non_string_feature_item() {
  function test_cargo_options_publish_mode_toggle_via_cancel_running (line 1965) | fn test_cargo_options_publish_mode_toggle_via_cancel_running() {
  function test_cargo_options_separate_child_diagnostics_can_unset (line 1978) | fn test_cargo_options_separate_child_diagnostics_can_unset() {
  function test_find_git_root_directory_returns_none_outside_git (line 1995) | async fn test_find_git_root_directory_returns_none_outside_git() {
  function test_find_git_root_directory_finds_top_of_repo (line 2002) | async fn test_find_git_root_directory_finds_top_of_repo() {
  function test_init_cargo_backend_uses_existing_project_root (line 2015) | fn test_init_cargo_backend_uses_existing_project_root() {
  function test_init_cargo_backend_falls_back_to_cwd_when_no_project_root (line 2039) | fn test_init_cargo_backend_falls_back_to_cwd_when_no_project_root() {
  function test_cargo_options_build_args_with_env_does_not_leak_into_args (line 2055) | fn test_cargo_options_build_args_with_env_does_not_leak_into_args() {

FILE: src/lsp.rs
  method initialize (line 21) | async fn initialize(&self, params: InitializeParams) -> jsonrpc::Result<...
  method initialized (line 165) | async fn initialized(&self, _: InitializedParams) {
  method did_change_configuration (line 207) | async fn did_change_configuration(&self, params: DidChangeConfigurationP...
  method did_open (line 222) | async fn did_open(&self, params: DidOpenTextDocumentParams) {
  method did_close (line 249) | async fn did_close(&self, params: DidCloseTextDocumentParams) {
  method did_save (line 265) | async fn did_save(&self, params: DidSaveTextDocumentParams) {
  method did_change (line 300) | async fn did_change(&self, params: DidChangeTextDocumentParams) {
  method did_delete_files (line 332) | async fn did_delete_files(&self, params: DeleteFilesParams) {
  method did_rename_files (line 345) | async fn did_rename_files(&self, params: RenameFilesParams) {
  method code_action (line 360) | async fn code_action(&self, params: CodeActionParams) -> jsonrpc::Result...
  method execute_command (line 419) | async fn execute_command(&self, params: ExecuteCommandParams) -> jsonrpc...
  method shutdown (line 432) | async fn shutdown(&self) -> jsonrpc::Result<()> {

FILE: src/main.rs
  function main (line 5) | async fn main() {

FILE: src/native.rs
  function read_until_cr_or_lf (line 22) | async fn read_until_cr_or_lf<R: AsyncBufRead + Unpin>(reader: &mut R, bu...
  type CargoExpansion (line 44) | struct CargoExpansion {
  type CargoSpan (line 49) | struct CargoSpan {
    method is_machine_applicable (line 65) | fn is_machine_applicable(&self) -> bool {
    method project_span (line 72) | fn project_span(&self) -> &Self {
  function deserialize_url (line 88) | fn deserialize_url<'de, D>(deserializer: D) -> Result<Uri, D::Error>
  type CargoChildren (line 97) | struct CargoChildren {
  type CargoCode (line 104) | struct CargoCode {
  type CargoMessage (line 109) | struct CargoMessage {
  function tags_from_code (line 125) | fn tags_from_code(code: &str) -> Option<Vec<DiagnosticTag>> {
  type CargoLine (line 142) | struct CargoLine {
  type Cargo (line 147) | pub(crate) struct Cargo;
    method parse_severity (line 173) | fn parse_severity(severity_str: &str) -> DiagnosticSeverity {
    method span_to_uri (line 186) | fn span_to_uri(project_root: Option<&PathBuf>, span: &CargoSpan) -> an...
    method maybe_add_diagnostic (line 234) | async fn maybe_add_diagnostic(
    method cargo_diagnostics (line 407) | pub(crate) async fn cargo_diagnostics(
    method find_project_root (line 531) | pub(crate) async fn find_project_root(params: &InitializeParams) -> Op...
  function parse_building_line (line 153) | fn parse_building_line(line: &str) -> Option<(String, u32)> {
  constant TOKIO_SELECT_EXPANSION (line 580) | const TOKIO_SELECT_EXPANSION: &str = include_str!("testdata/expansion-ne...
  constant UNUSED_VARIABLE (line 585) | const UNUSED_VARIABLE: &str = include_str!("testdata/unused-variable.jso...
  constant UNUSED_IMPORT_LINE (line 589) | const UNUSED_IMPORT_LINE: &str = include_str!("testdata/unused-import-li...
  constant UNUSED_IMPORT_GROUPED (line 594) | const UNUSED_IMPORT_GROUPED: &str = include_str!("testdata/unused-import...
  function test_project_span_follows_macro_expansion_chain (line 597) | fn test_project_span_follows_macro_expansion_chain() {
  function test_project_span_returns_self_when_already_project_local (line 618) | fn test_project_span_returns_self_when_already_project_local() {
  function test_children_structure_for_separate_diagnostics (line 639) | fn test_children_structure_for_separate_diagnostics() {
  function test_unused_import_whole_line_produces_remove_correction (line 686) | fn test_unused_import_whole_line_produces_remove_correction() {
  function test_unused_import_grouped_produces_three_remove_corrections (line 720) | fn test_unused_import_grouped_produces_three_remove_corrections() {
  function test_project_span_falls_back_to_self_when_no_project_span_in_chain (line 764) | fn test_project_span_falls_back_to_self_when_no_project_span_in_chain() {
  function test_parse_building_line_basic (line 785) | fn test_parse_building_line_basic() {
  function test_parse_building_line_caps_percentage_at_99 (line 795) | fn test_parse_building_line_caps_percentage_at_99() {
  function test_parse_building_line_rejects_non_building_prefix (line 805) | fn test_parse_building_line_rejects_non_building_prefix() {
  function test_parse_building_line_zero_total_returns_none (line 811) | fn test_parse_building_line_zero_total_returns_none() {
  function test_parse_building_line_malformed_returns_none (line 816) | fn test_parse_building_line_malformed_returns_none() {
  function test_parse_severity_known_levels (line 822) | fn test_parse_severity_known_levels() {
  function test_parse_severity_unknown_level_defaults_to_information (line 833) | fn test_parse_severity_unknown_level_defaults_to_information() {
  function test_tags_from_code_unused_lints_get_unnecessary (line 841) | fn test_tags_from_code_unused_lints_get_unnecessary() {
  function test_tags_from_code_dead_and_unreachable_get_unnecessary (line 858) | fn test_tags_from_code_dead_and_unreachable_get_unnecessary() {
  function test_tags_from_code_deprecated_gets_deprecated_tag (line 875) | fn test_tags_from_code_deprecated_gets_deprecated_tag() {
  function test_tags_from_code_unrelated_returns_none (line 880) | fn test_tags_from_code_unrelated_returns_none() {
  function test_read_until_lf_consumes_terminator (line 889) | async fn test_read_until_lf_consumes_terminator() {
  function test_read_until_cr_consumes_terminator (line 899) | async fn test_read_until_cr_consumes_terminator() {
  function test_read_until_cr_or_lf_returns_zero_at_eof (line 910) | async fn test_read_until_cr_or_lf_returns_zero_at_eof() {
  function test_read_until_cr_or_lf_no_terminator_reads_to_eof (line 919) | async fn test_read_until_cr_or_lf_no_terminator_reads_to_eof() {
  function test_read_until_cr_or_lf_spans_multiple_internal_buffers (line 928) | async fn test_read_until_cr_or_lf_spans_multiple_internal_buffers() {
  function make_relative_span (line 938) | fn make_relative_span(file_name_str: &str, is_primary: bool) -> CargoSpan {
  function test_span_to_uri_returns_none_when_no_authority (line 959) | fn test_span_to_uri_returns_none_when_no_authority() {
  function test_span_to_uri_returns_none_when_canonicalize_fails (line 971) | async fn test_span_to_uri_returns_none_when_canonicalize_fails() {
  function test_span_to_uri_resolves_existing_relative_path (line 985) | async fn test_span_to_uri_resolves_existing_relative_path() {
  function test_maybe_add_diagnostic_emits_per_primary_span (line 1007) | async fn test_maybe_add_diagnostic_emits_per_primary_span() {
  function test_maybe_add_diagnostic_separate_children_when_unsupported (line 1048) | async fn test_maybe_add_diagnostic_separate_children_when_unsupported() {
  function test_find_project_root_picks_workspace_folder_with_cargo_toml (line 1098) | async fn test_find_project_root_picks_workspace_folder_with_cargo_toml() {
  function test_find_project_root_returns_none_when_no_cargo_toml_anywhere (line 1127) | async fn test_find_project_root_returns_none_when_no_cargo_toml_anywhere...

FILE: src/shadow.rs
  type ShadowWorkspace (line 26) | pub(crate) struct ShadowWorkspace {
    method build (line 37) | pub(crate) async fn build(real_root: PathBuf) -> std::io::Result<Self> {
    method real_root (line 64) | pub(crate) fn real_root(&self) -> &Path {
    method shadow_root (line 68) | pub(crate) fn shadow_root(&self) -> &Path {
    method target_dir (line 72) | pub(crate) fn target_dir(&self) -> &Path {
    method shadow_path_for (line 78) | pub(crate) fn shadow_path_for(&self, real_path: &Path) -> std::io::Res...
    method write_dirty (line 96) | pub(crate) async fn write_dirty(&self, real_path: &Path, content: &str...
    method restore_link (line 110) | pub(crate) async fn restore_link(&self, real_path: &Path) -> std::io::...
  function mirror_blocking (line 126) | fn mirror_blocking(real: &Path, shadow: &Path, live_root: &Path) -> std:...
  function inode (line 197) | fn inode(path: &Path) -> u64 {
  function mk_file (line 202) | fn mk_file(path: &Path, content: &str) {
  function test_build_mirrors_files_via_hardlink_same_inode (line 211) | async fn test_build_mirrors_files_via_hardlink_same_inode() {
  function test_build_excludes_gitignored_paths (line 227) | async fn test_build_excludes_gitignored_paths() {
  function test_build_excludes_hidden_dirs (line 247) | async fn test_build_excludes_hidden_dirs() {
  function test_build_does_not_recurse_into_its_own_live_dir (line 259) | async fn test_build_does_not_recurse_into_its_own_live_dir() {
  function test_build_wipes_stale_shadow_from_prior_session (line 286) | async fn test_build_wipes_stale_shadow_from_prior_session() {
  function test_build_preserves_target_dir_for_cache_reuse (line 311) | async fn test_build_preserves_target_dir_for_cache_reuse() {
  function test_write_dirty_replaces_hardlink_with_distinct_inode (line 327) | async fn test_write_dirty_replaces_hardlink_with_distinct_inode() {
  function test_restore_link_reverts_to_hardlink (line 357) | async fn test_restore_link_reverts_to_hardlink() {
  function test_write_dirty_creates_missing_parent_dirs (line 378) | async fn test_write_dirty_creates_missing_parent_dirs() {
  function test_shadow_path_for_outside_workspace_errors (line 396) | async fn test_shadow_path_for_outside_workspace_errors() {

FILE: tests/cargo_backend.rs
  constant BIN (line 22) | const BIN: &str = env!("CARGO_BIN_EXE_bacon-ls");
  function write_fixture (line 24) | fn write_fixture(dir: &Path, lib_rs: &str) {
  function frame (line 34) | fn frame(body: &str) -> Vec<u8> {
  function send (line 38) | fn send(stdin: &mut ChildStdin, msg: &Value) {
  function spawn_reader (line 44) | fn spawn_reader(mut stdout: ChildStdout) -> mpsc::Receiver<Value> {
  function auto_respond (line 83) | fn auto_respond(stdin: &mut ChildStdin, msg: &Value, config_response: &V...
  function pump (line 99) | fn pump<F>(
  function empty_config (line 124) | fn empty_config() -> Value {
  function root_uri (line 128) | fn root_uri(dir: &Path) -> String {
  function spawn_server (line 132) | fn spawn_server(workdir: &Path) -> Child {
  function spawn_server_with_log (line 136) | fn spawn_server_with_log(workdir: &Path, rust_log: Option<&str>) -> Child {
  function read_server_log (line 150) | fn read_server_log(workdir: &Path) -> String {
  function initialize (line 154) | fn initialize(
  function initialize_with_init_options (line 164) | fn initialize_with_init_options(
  function shutdown_and_wait (line 205) | fn shutdown_and_wait(stdin: &mut ChildStdin, rx: &mpsc::Receiver<Value>,...
  function diagnostics_for (line 227) | fn diagnostics_for(msg: &Value, file_uri_substring: &str) -> Option<Vec<...
  function cargo_backend_publishes_error_diagnostic (line 244) | fn cargo_backend_publishes_error_diagnostic() {
  function cargo_backend_code_action_replaces_unused_variable (line 289) | fn cargo_backend_code_action_replaces_unused_variable() {
  function cargo_backend_live_diagnostics_without_save (line 383) | fn cargo_backend_live_diagnostics_without_save() {

FILE: tests/lsp_restart.rs
  constant BIN (line 22) | const BIN: &str = env!("CARGO_BIN_EXE_bacon-ls");
  function frame (line 24) | fn frame(body: &str) -> Vec<u8> {
  function spawn_reader (line 31) | fn spawn_reader(mut stdout: ChildStdout) -> mpsc::Receiver<String> {
  function wait_for (line 63) | fn wait_for<F: Fn(&str) -> bool>(rx: &mpsc::Receiver<String>, timeout: D...
  function unresponsive_client_still_exits_after_shutdown_and_exit (line 76) | fn unresponsive_client_still_exits_after_shutdown_and_exit() {

FILE: vscode/extension.ts
  constant RESTART_ON_CHANGE (line 16) | const RESTART_ON_CHANGE = [
  function activate (line 23) | async function activate(
  function createClient (line 85) | async function createClient(
  function getServerPath (line 134) | async function getServerPath(
  function deactivate (line 176) | function deactivate(): Thenable<void> | undefined {
Condensed preview — 31 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (419K chars).
[
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "chars": 392,
    "preview": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n**Describe the b"
  },
  {
    "path": ".github/dependabot.yml",
    "chars": 573,
    "preview": "version: 2\n\nupdates:\n  - package-ecosystem: \"cargo\"\n    directory: \"/\"\n    schedule:\n      interval: \"monthly\"\n    ignor"
  },
  {
    "path": ".github/workflows/auto-merge.yml",
    "chars": 587,
    "preview": "name: Dependabot Auto-merge\non:\n  pull_request:\n    types: [opened]\n\npermissions:\n  contents: write\n  pull-requests: wri"
  },
  {
    "path": ".github/workflows/ci.yml",
    "chars": 1402,
    "preview": "name: ci\n\non:\n  push:\n    branches: [ \"main\" ]\n  pull_request:\n    branches: [ \"main\" ]\n  workflow_dispatch:\n\nenv:\n  CAR"
  },
  {
    "path": ".github/workflows/code-coverage.yml",
    "chars": 692,
    "preview": "name: code-coverage\n\non: [pull_request, push]\n\njobs:\n  coverage:\n    runs-on: ubuntu-latest\n    env:\n      CARGO_TERM_CO"
  },
  {
    "path": ".github/workflows/release.yml",
    "chars": 6055,
    "preview": "name: release\n\non:\n  release:\n    types: [published]\n  workflow_dispatch:\n\njobs:\n  wait:\n    runs-on: ubuntu-latest\n    "
  },
  {
    "path": ".gitignore",
    "chars": 169,
    "preview": ".bacon-locations\ntarpaulin-report.html\nout\ndist\nnode_modules\n.vscode-test/\n*.vsix\ntarget/\nbundled/\nvscode/extension.js.*"
  },
  {
    "path": ".prettierignore",
    "chars": 114,
    "preview": ".github\nnode_modules\n.vscode\n.vscode-test\nout\ntarget\nsrc/testdata\nflake.lock\nCHANGELOG.md\nREADME.md\nREADME-0.4.md\n"
  },
  {
    "path": ".vscodeignore",
    "chars": 92,
    "preview": "**\n!img/icon.png\n!LICENSE\n!out/main.js\n!package-lock.json\n!package.json\n!bundled\n!README.md\n"
  },
  {
    "path": "Cargo.toml",
    "chars": 1193,
    "preview": "[package]\nname = \"bacon-ls\"\nversion = \"0.29.0\"\nedition = \"2024\"\nauthors = [\"Matteo Bigoi <bigo@crisidev.org>\"]\ndescripti"
  },
  {
    "path": "LICENSE",
    "chars": 1069,
    "preview": "MIT License\n\nCopyright (c) 2024 Matteo Bigoi\n\nPermission is hereby granted, free of charge, to any person obtaining a co"
  },
  {
    "path": "README.md",
    "chars": 25960,
    "preview": "# 🐽 Bacon Language Server 🐽\n\n[![Ci](https://img.shields.io/github/actions/workflow/status/crisidev/bacon-ls/ci.yml?style"
  },
  {
    "path": "eslint.config.mjs",
    "chars": 661,
    "preview": "import typescriptEslint from \"@typescript-eslint/eslint-plugin\";\nimport stylistic from \"@stylistic/eslint-plugin\";\nimpor"
  },
  {
    "path": "flake.nix",
    "chars": 1155,
    "preview": "{\n  inputs = {\n    flake-utils.url = \"github:numtide/flake-utils\";\n    naersk.url = \"github:nix-community/naersk\";\n    n"
  },
  {
    "path": "package.json",
    "chars": 8611,
    "preview": "{\n  \"name\": \"bacon-ls-vscode\",\n  \"displayName\": \"Bacon Language Server\",\n  \"description\": \"Rust diagnostic based on Baco"
  },
  {
    "path": "rustfmt.toml",
    "chars": 88,
    "preview": "edition = \"2024\"\nmax_width = 120\nreorder_imports = true\nuse_field_init_shorthand = true\n"
  },
  {
    "path": "src/bacon.rs",
    "chars": 44388,
    "preview": "use std::borrow::Cow;\nuse std::env;\nuse std::path::{Path, PathBuf};\nuse std::process::Stdio;\nuse std::sync::Arc;\nuse std"
  },
  {
    "path": "src/lib.rs",
    "chars": 80798,
    "preview": "//! Bacon Language Server\nuse std::collections::{HashMap, HashSet};\nuse std::env;\nuse std::path::{Path, PathBuf};\nuse st"
  },
  {
    "path": "src/lsp.rs",
    "chars": 22607,
    "preview": "use std::collections::HashMap;\n\nuse ls_types::{\n    CodeAction, CodeActionKind, CodeActionOptions, CodeActionOrCommand, "
  },
  {
    "path": "src/main.rs",
    "chars": 299,
    "preview": "//! Bacon Language Server\nuse bacon_ls::BaconLs;\n\n#[tokio::main(flavor = \"multi_thread\", worker_threads = 4)]\nasync fn m"
  },
  {
    "path": "src/native.rs",
    "chars": 47247,
    "preview": "use std::{\n    env,\n    path::{Path, PathBuf},\n    process::Stdio,\n};\n\nuse anyhow::Context;\nuse ls_types::{\n    Diagnost"
  },
  {
    "path": "src/shadow.rs",
    "chars": 16626,
    "preview": "//! Hardlinked shadow workspace used for live \"as you type\" diagnostics.\n//!\n//! cargo can only check files on disk. To "
  },
  {
    "path": "src/testdata/expansion-needed.json",
    "chars": 17499,
    "preview": "{\"reason\":\"compiler-message\",\"package_id\":\"path+file:///home/thomas/Projects/bacon-ls-bug#bacon-ls@0.26.0\",\"manifest_pat"
  },
  {
    "path": "src/testdata/unused-import-compact.json",
    "chars": 2784,
    "preview": "{\"reason\":\"compiler-message\",\"package_id\":\"path+file:///home/thomas/Projects/bacon-ls-bug#bacon-ls@0.26.0\",\"manifest_pat"
  },
  {
    "path": "src/testdata/unused-import-line.json",
    "chars": 1989,
    "preview": "{\"reason\":\"compiler-message\",\"package_id\":\"path+file:///home/thomas/Projects/bacon-ls-bug#bacon-ls@0.26.0\",\"manifest_pat"
  },
  {
    "path": "src/testdata/unused-import.json",
    "chars": 81581,
    "preview": "{\"reason\":\"compiler-artifact\",\"package_id\":\"registry+https://github.com/rust-lang/crates.io-index#proc-macro2@1.0.106\",\""
  },
  {
    "path": "src/testdata/unused-variable.json",
    "chars": 2081,
    "preview": "{\"reason\":\"compiler-message\",\"package_id\":\"path+file:///home/thomas/Projects/bacon-ls#0.26.0\",\"manifest_path\":\"/home/tho"
  },
  {
    "path": "tests/cargo_backend.rs",
    "chars": 18595,
    "preview": "//! End-to-end integration tests for the cargo (native) backend. Spawns the\n//! real `bacon-ls` binary against a tempdir"
  },
  {
    "path": "tests/lsp_restart.rs",
    "chars": 5932,
    "preview": "//! Regression test for issue #47 (`:LspRestart` hung the old bacon-ls\n//! forever). Simulates a client that completes `"
  },
  {
    "path": "tsconfig.json",
    "chars": 576,
    "preview": "{\n  \"compilerOptions\": {\n    \"module\": \"commonjs\",\n    \"target\": \"ES2020\",\n    \"outDir\": \"out\",\n    \"lib\": [\"ES2020\"],\n "
  },
  {
    "path": "vscode/extension.ts",
    "chars": 4903,
    "preview": "import * as vscode from \"vscode\";\nimport * as os from \"os\";\n\nimport {\n  ExecuteCommandRequest,\n  LanguageClient,\n  Langu"
  }
]

About this extraction

This page contains the full source code of the crisidev/bacon-ls GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 31 files (387.4 KB), approximately 101.0k tokens, and a symbol index with 279 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!