[
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "content": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n**Describe the bug**\nA clear and concise description of what the bug is.\n\n**Expected behavior**\nA clear and concise description of what you expected to happen.\n\n**Attatch `bacon-ls.log`**\nExport `RUST_LOG=debug` before running vim / neovim to generate `bacon-ls.log` with debugging info.\n"
  },
  {
    "path": ".github/dependabot.yml",
    "content": "version: 2\n\nupdates:\n  - package-ecosystem: \"cargo\"\n    directory: \"/\"\n    schedule:\n      interval: \"monthly\"\n    ignore:\n      - dependency-name: \"bacon-ls*\"\n    groups:\n      lsp:\n        applies-to: version-updates\n        patterns:\n         - \"*\"\n  - package-ecosystem: \"npm\"\n    directory: \"/\"\n    schedule:\n      interval: \"monthly\"\n    groups:\n      vscode:\n        applies-to: version-updates\n        patterns:\n         - \"*\"\n    ignore:\n      # this needs to match engines.vscode and defines the minimum version of vscode\n      - dependency-name: '@types/vscode'\n"
  },
  {
    "path": ".github/workflows/auto-merge.yml",
    "content": "name: Dependabot Auto-merge\non:\n  pull_request:\n    types: [opened]\n\npermissions:\n  contents: write\n  pull-requests: write\n\njobs:\n  dependabot:\n    runs-on: ubuntu-latest\n    if: github.event.pull_request.user.login == 'dependabot[bot]'\n    steps:\n      - name: Dependabot metadata\n        id: dependabot-metadata\n        uses: dependabot/fetch-metadata@v2\n\n      - name: Enable auto-merge for Dependabot PRs\n        run: gh pr merge --auto --squash \"$PR_URL\" --body \"\"\n        env:\n          PR_URL: ${{github.event.pull_request.html_url}}\n          GH_TOKEN: ${{secrets.GITHUB_TOKEN}}\n"
  },
  {
    "path": ".github/workflows/ci.yml",
    "content": "name: ci\n\non:\n  push:\n    branches: [ \"main\" ]\n  pull_request:\n    branches: [ \"main\" ]\n  workflow_dispatch:\n\nenv:\n  CARGO_TERM_COLOR: always\n  RUST_LOG: bacon-ls\n\njobs:\n  # security-audit:\n  #   runs-on: ubuntu-latest\n  #   steps:\n  #     - uses: actions/checkout@v4\n  #     - uses: rustsec/audit-check@v2\n  #       with:\n  #         token: ${{ secrets.ACTIONS_TOKEN }}\n  build-linux:\n    runs-on: ubuntu-latest\n    steps:\n    - uses: actions/checkout@v4\n    - uses: Swatinem/rust-cache@v2\n    - run: cargo test\n      env:\n        RUST_LOG: debug,globset=warn\n    - run: cargo fmt --all -- --check\n    - run: cargo clippy --all-targets\n    - uses: actions/setup-node@v4\n      with:\n        node-version-file: package.json\n    - run: npm ci\n    - run: npm run lint\n    # vscode requires an X Server\n    # - name: npm test\n      # run: xvfb-run npm test\n    # ensure package can be built when package.json changes\n    - name: npx vsce package\n      run: npx vsce package\n  build-mac:\n    runs-on: macos-latest\n    steps:\n    - uses: actions/checkout@v4\n    - uses: Swatinem/rust-cache@v2\n    - run: cargo test\n      env:\n        RUST_LOG: debug,globset=warn\n        RUST_BACKTRACE: 1\n  build-windows:\n    runs-on: windows-latest\n    steps:\n    - uses: actions/checkout@v4\n    - uses: Swatinem/rust-cache@v2\n    - run: cargo test\n      env:\n        RUST_LOG: debug,globset=warn\n        RUST_BACKTRACE: 1\n"
  },
  {
    "path": ".github/workflows/code-coverage.yml",
    "content": "name: code-coverage\n\non: [pull_request, push]\n\njobs:\n  coverage:\n    runs-on: ubuntu-latest\n    env:\n      CARGO_TERM_COLOR: always\n    steps:\n      - uses: actions/checkout@v4\n      - name: Install Rust\n        run: rustup update stable\n      - name: Install cargo-llvm-cov\n        uses: taiki-e/install-action@cargo-llvm-cov\n      - name: Generate code coverage\n        run: cargo llvm-cov --all-features --workspace --codecov --output-path codecov.json\n      - name: Upload coverage to Codecov\n        uses: codecov/codecov-action@v3\n        with:\n          token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos\n          files: codecov.json\n          fail_ci_if_error: true\n"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "name: release\n\non:\n  release:\n    types: [published]\n  workflow_dispatch:\n\njobs:\n  wait:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Wait for checks to succeed\n        uses: poseidon/wait-for-status-checks@v0.4.1\n        with:\n          token: ${{ secrets.GITHUB_TOKEN }}\n          interval: 5\n\n  dist:\n    strategy:\n      # don't cancel other jobs when one fails\n      fail-fast: false\n      matrix:\n        include:\n          - os: windows-latest\n            target: x86_64-pc-windows-msvc\n            code-target: win32-x64\n          - os: windows-latest\n            target: i686-pc-windows-msvc\n            code-target: win32-x64\n          - os: windows-latest\n            target: aarch64-pc-windows-msvc\n            code-target: win32-arm64\n          - os: ubuntu-latest\n            target: x86_64-unknown-linux-gnu\n            code-target: linux-x64\n          - os: ubuntu-latest\n            target: x86_64-unknown-linux-musl\n            code-target: linux-x64\n          # - os: ubuntu-latest\n          #   target: i686-unknown-linux-musl\n          #   code-target: linux-x64\n          - os: ubuntu-latest\n            target: aarch64-unknown-linux-gnu\n            code-target: linux-arm64\n          - os: ubuntu-latest\n            target: aarch64-unknown-linux-musl\n            code-target: linux-arm64\n          - os: ubuntu-latest\n            target: arm-unknown-linux-gnueabihf\n            code-target: linux-armhf\n          - os: macos-latest\n            target: x86_64-apple-darwin\n            code-target: darwin-x64\n          - os: macos-latest\n            target: aarch64-apple-darwin\n            code-target: darwin-arm64\n\n    name: dist (${{ matrix.target }})\n    runs-on: ${{ matrix.os }}\n    needs: wait\n\n    steps:\n      - uses: actions/checkout@v4\n      # needed for arm targets\n      - name: Install Rust\n        uses: dtolnay/rust-toolchain@v1\n        with:\n          toolchain: stable\n          target: ${{ matrix.target }}\n      - uses: Swatinem/rust-cache@v2\n      - name: Install zlib-dev and openssl-dev (linux)\n        if: contains(matrix.target, 'unknown-linux')\n        run: sudo apt-get update && sudo apt-get install zlib1g-dev libssl-dev musl-tools\n      - name: Install GCC arm64 (linux)\n        if: startsWith(matrix.target, 'aarch64-unknown-linux')\n        run: sudo apt-get update && sudo apt-get install gcc-aarch64-linux-gnu\n      - name: Install GCC armhf (linux)\n        if: matrix.target == 'arm-unknown-linux-gnueabihf'\n        run: sudo apt-get update && sudo apt-get install gcc-arm-linux-gnueabihf\n      - name: Install i686 glibc (linux)\n        if: matrix.target == 'i686-unknown-linux-musl'\n        run: sudo apt-get install gcc-multilib g++-multilib libc6-dev-i386\n      - run: cargo build --target ${{ matrix.target }} --release\n        env:\n          PKG_CONFIG_ALLOW_CROSS: 1\n          CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc\n          CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_LINKER: aarch64-linux-gnu-gcc\n          CARGO_TARGET_ARM_UNKNOWN_LINUX_GNUEABIHF_LINKER: arm-linux-gnueabihf-gcc\n      - run: npm ci\n      - name: vsce package\n        # we can only publish a single linux-x64 and linux-arm64 package\n        # so we skip the gnu target and package the musl target\n        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'\n        # use bash on windows\n        shell: bash\n        run: |\n          mkdir -p bundled dist\n          cp target/${{ matrix.target }}/release/bacon-ls* bundled/\n          npx vsce package -o dist/ --target ${{ matrix.code-target }}\n      - name: vsce package for alpine\n        # package the alpine-x64 target with the musl binary\n        if: matrix.target == 'x86_64-unknown-linux-musl'\n        shell: bash\n        run: npx vsce package -o dist/ --target alpine-x64\n      - name: Archive\n        shell: bash\n        run: |\n          ver=${GITHUB_REF/refs\\/*\\//}\n          archive=\"dist/bacon-ls-$ver-${{ matrix.target }}\"\n          mkdir -p dist\n\n          if [ \"${{ matrix.os }}\" == \"windows-latest\" ]; then\n            7z a \"${archive}.zip\" target/${{ matrix.target }}/release/bacon-ls.exe\n          else\n            tar czf \"${archive}.tar.gz\" -C target/${{ matrix.target }}/release bacon-ls\n          fi\n\n          ls -al dist/*\n      - name: Upload artifacts\n        uses: actions/upload-artifact@v4\n        with:\n          name: ${{ matrix.target }}\n          path: dist\n      - name: Start sshx session on failed manual run or retry\n        if: ${{ failure() && (github.event_name == 'workflow_dispatch' || github.run_attempt > 1) }}\n        run: curl -sSf https://sshx.io/get | sh && sshx\n\n  publish:\n    runs-on: ubuntu-latest\n    needs: dist\n    permissions:\n      contents: write\n    steps:\n      - uses: actions/checkout@v4\n      - uses: actions/setup-node@v4\n        with:\n          node-version-file: package.json\n      # download each artifact into its own self-named directory\n      - uses: actions/download-artifact@v4\n        with:\n          path: dist\n      - run: npm ci\n      - name: Inspect dist\n        run: |\n          ls -al dist/*\n          find dist -name \"*.vsix\" -type f\n      - name: vsce publish\n        run: npx vsce publish --packagePath $(find dist -name \"*.vsix\" -type f)\n        env:\n          VSCE_PAT: ${{ secrets.VSCE_PAT }}\n      - name: ovsx publish\n        run: npx ovsx publish --packagePath $(find dist -name \"*.vsix\" -type f)\n        env:\n          OVSX_PAT: ${{ secrets.OVSX_PAT }}\n      - name: Upload to GH release\n        uses: softprops/action-gh-release@v2\n        with:\n          # unset the prerelease flag and make it the latest release\n          prerelease: false\n          make_latest: true\n          files: dist/**/*\n      - name: Start sshx session on failed manual run or retry\n        if: ${{ failure() && (github.event_name == 'workflow_dispatch' || github.run_attempt > 1) }}\n        run: curl -sSf https://sshx.io/get | sh && sshx\n"
  },
  {
    "path": ".gitignore",
    "content": ".bacon-locations\ntarpaulin-report.html\nout\ndist\nnode_modules\n.vscode-test/\n*.vsix\ntarget/\nbundled/\nvscode/extension.js.*\nbacon-ls.log\nresult\n.coverage\n.direnv\nREVIEW.md\n"
  },
  {
    "path": ".prettierignore",
    "content": ".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",
    "content": "**\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",
    "content": "[package]\nname = \"bacon-ls\"\nversion = \"0.29.0\"\nedition = \"2024\"\nauthors = [\"Matteo Bigoi <bigo@crisidev.org>\"]\ndescription = \"Bacon Language Server\"\ndocumentation = \"https://github.com/crisidev/bacon-ls\"\nreadme = \"README.md\"\nhomepage = \"https://github.com/crisidev/bacon-ls\"\nrepository = \"https://github.com/crisidev/bacon-ls\"\nlicense-file = \"LICENSE\"\nkeywords = [\"lsp\", \"bacon\", \"tokio\", \"neovim\", \"vim\"]\ncategories = [\"command-line-utilities\", \"text-editors\", \"asynchronous\"]\nrust-version = \"1.94\"\n\n[features]\ndefault = []\n\n[dependencies]\nanyhow = \"1\"\nansi-regex = \"0.1.0\"\nargh = \"0.1.19\"\nflume = \"0.12.0\"\nignore = \"0.4\"\nls-types = \"0.0.6\"\nnotify = \"8.2.0\"\nnotify-debouncer-full = \"0.7.0\"\npercent-encoding = \"2.3.2\"\nserde = { version = \"1.0.228\", features = [\"derive\"] }\nserde_json = \"1.0.149\"\ntokio = { version = \"1.50.0\", features = [\n  \"fs\",\n  \"io-std\",\n  \"io-util\",\n  \"macros\",\n  \"process\",\n  \"rt-multi-thread\",\n  \"time\",\n] }\ntokio-util = \"0.7.18\"\ntoml = \"1.0\"\ntower-lsp-server = \"0.23.0\"\ntracing = \"0.1.44\"\ntracing-subscriber = { version = \"0.3.23\", default-features = false, features = [\n  \"env-filter\",\n  \"fmt\",\n] }\n\n[dev-dependencies]\npretty_assertions = \"1.4.1\"\ntempfile = \"3.27.0\"\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2024 Matteo Bigoi\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# 🐽 Bacon Language Server 🐽\n\n[![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)\n[![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)\n[![Crates.io](https://img.shields.io/crates/v/bacon-ls?style=for-the-badge)](https://crates.io/crates/bacon-ls)\n[![Crates.io](https://img.shields.io/crates/d/bacon-ls?style=for-the-badge)](https://crates.io/crates/bacon-ls)\n[![License](https://img.shields.io/badge/license-MIT-blue?style=for-the-badge)](https://github.com/crisidev/bacon-ls/blob/main/LICENSE)\n[![Codecov](https://img.shields.io/codecov/c/github/crisidev/bacon-ls?style=for-the-badge&token=42UR7SSSPB)](https://codecov.io/github/crisidev/bacon-ls)\n\n**Are you tired of [rust-analyzer](https://rust-analyzer.github.io/) diagnostics being slow?**\n\nLSP 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.\n\n`bacon-ls` 🐽 does not substitute `rust-analyzer`, it's a companion tool that can help with large \ncodebases where `rust-analyzer` can become slow dealing with diagnostics. \n\n**`bacon-ls` 🐽 does not help with completion, analysis, refactor, etc... For these, `rust-analyzer` must be running.**\n\n![Bacon screenshot](./img/screenshot.png)\n\n<!-- vim-markdown-toc Marked -->\n\n* [Features](#features)\n    * [Limitations](#limitations)\n* [Installation](#installation)\n    * [VSCode](#vscode)\n    * [Mason.nvim](#mason.nvim)\n    * [Manual](#manual)\n    * [Nix](#nix)\n* [Configuration](#configuration)\n    * [Choosing a backend](#choosing-a-backend)\n    * [Cargo backend options](#cargo-backend-options)\n    * [Live diagnostics as you type (cargo backend only)](#live-diagnostics-as-you-type-(cargo-backend-only))\n    * [Bacon backend options](#bacon-backend-options)\n    * [Manually triggering diagnostics](#manually-triggering-diagnostics)\n    * [Changing configuration at runtime](#changing-configuration-at-runtime)\n* [Migrating from 0.26.x and earlier](#migrating-from-0.26.x-and-earlier)\n* [Editor setup](#editor-setup)\n    * [Neovim - LazyVim](#neovim---lazyvim)\n    * [Neovim - Manual](#neovim---manual)\n    * [VSCode](#vscode)\n    * [Coc.nvim](#coc.nvim)\n    * [Helix](#helix)\n* [Troubleshooting](#troubleshooting)\n    * [Bacon preferences](#bacon-preferences)\n    * [Vim - Neovim](#vim---neovim)\n    * [VSCode](#vscode)\n* [How does it work?](#how-does-it-work?)\n* [Thanks](#thanks)\n* [Roadmap to 1.0 - ✅ done 🕖 in progress 🌍 future](#roadmap-to-1.0---✅-done-🕖-in-progress-🌍-future)\n\n<!-- vim-markdown-toc -->\n\nSee `bacon-ls` 🐽 blog post: https://lmno.lol/crisidev/bacon-language-server\n\n`bacon-ls` 🐽 is meant to be easy to include in your IDE configuration.\n\n![Bacon gif](./img/bacon-ls.gif)\n\n## Features\n\n* Two backends to produce diagnostics:\n  * **Cargo** (default since 0.23.0): runs `cargo check` (or `cargo clippy`) directly with\n    JSON output, parses the messages and publishes them. Faster, lighter and zero\n    extra dependencies.\n  * **Bacon**: reads the export file produced by [Bacon](https://dystroy.org/bacon/)\n    and publishes those diagnostics. Useful when you already have `bacon` running.\n* Push diagnostics to the LSP client on file save, open, close and rename.\n* Precise diagnostic positions and macro-expanded spans pointed back at the\n  call-site.\n* Replacement code actions as suggested by `cargo` / `clippy`.\n* Unused / dead / deprecated code tagged with the LSP `UNNECESSARY` and\n  `DEPRECATED` diagnostic tags (cargo backend only) so editors render\n  unused variables and imports faded, and deprecated items struck through.\n* Streaming partial publishes during a long `cargo` run (configurable refresh\n  interval) so the editor lights up as soon as the first errors are known.\n* Manual `bacon_ls.run` LSP command to re-trigger a check on demand.\n* Bacon backend extras: automatic validation of `bacon` preferences, optional\n  creation of the preferences file, optional automatic background `bacon`\n  process (requires `bacon` 3.8.0), open-file diagnostic synchronization.\n* Support for [cargo workspaces](https://doc.rust-lang.org/book/ch14-03-cargo-workspaces.html).\n\n### Limitations\n\n* Windows support is not tested and probably broken - [#10](https://github.com/crisidev/bacon-ls/issues/10)\n\n## Installation\n\n### VSCode\n\nFirst, install [Bacon](https://dystroy.org/bacon/#installation).\n\nThe VSCode extension is available on both VSCE and OVSX:\n\n* `VSCE` [https://marketplace.visualstudio.com/items?itemName=MatteoBigoi.bacon-ls-vscode](https://marketplace.visualstudio.com/items?itemName=MatteoBigoi.bacon-ls-vscode)\n* `OVSX` [https://open-vsx.org/extension/MatteoBigoi/bacon-ls-vscode](https://open-vsx.org/extension/MatteoBigoi/bacon-ls-vscode)\n\n### Mason.nvim\n\nBoth Bacon and Bacon-ls are installable via [mason.nvim](https://github.com/williamboman/mason.nvim):\n\n```vim\n:MasonInstall bacon bacon-ls\n```\n\n### Manual\n\nFirst, install [Bacon](https://dystroy.org/bacon/#installation) and `bacon-ls` 🐽\n\n```bash\n❯❯❯ cargo install --locked bacon bacon-ls\n❯❯❯ bacon --version\nbacon 3.8.0  # make sure you have at least 3.8.0\n❯❯❯ bacon-ls --version\n0.14.0        # make sure you have at least 0.14.0\n```\n\n### Nix\n\nBoth [bacon](https://github.com/Canop/bacon/blob/main/flake.nix) and [bacon-ls](./flake.nix) can be consumed from their Nix flakes.\n\n## Configuration\n\n`bacon-ls` 🐽 reads its configuration from the `bacon_ls` section of the LSP\nclient settings. All fields are optional — if you provide nothing the cargo\nbackend starts with sensible defaults. The complete schema is:\n\n```jsonc\n{\n  \"bacon_ls\": {\n    // \"cargo\" or \"bacon\". Optional — see \"Choosing a backend\" below.\n    \"backend\": \"cargo\",\n\n    \"cargo\": {\n      \"command\": \"check\",                 // \"check\" or \"clippy\"\n      \"features\": [],                     // cargo --features list, [\"feat1\", \"feat2\"] or \"all\"\n      \"package\": null,                    // cargo -p <package>\n      \"allTargets\": false,                // cargo --all-targets\n      \"noDefaultFeatures\": false,         // cargo --no-default-features\n      \"extraArgs\": [],                    // appended verbatim after the cargo command\n      \"env\": {},                          // extra environment variables (string -> string)\n      \"cancelRunning\": true,              // cancel an in-flight run when a new one is triggered\n      \"refreshIntervalSeconds\": 1,        // partial publish interval; null/negative = wait until done\n      \"separateChildDiagnostics\": null,   // override \"related information\" support; null = follow client\n      \"checkOnSave\": true,                // trigger cargo on textDocument/didSave\n      \"clearDiagnosticsOnCheck\": false,   // clear existing diagnostics before each run\n      \"updateOnInsertDebounceMillis\": 500 // debounce for live diagnostics; updateOnInsert itself is in init_options\n    },\n\n    \"bacon\": {\n      \"locationsFile\": \".bacon-locations\",\n      \"runInBackground\": true,\n      \"runInBackgroundCommand\": \"bacon\",\n      \"runInBackgroundCommandArguments\": \"--headless -j bacon-ls\",\n      \"validatePreferences\": true,\n      \"createPreferencesFile\": true,\n      \"synchronizeAllOpenFilesWaitMillis\": 2000,\n      \"updateOnSave\": true,\n      \"updateOnSaveWaitMillis\": 1000\n    }\n  }\n}\n```\n\n### Choosing a backend\n\nThe backend is chosen once, when the server initializes, and cannot be switched\nat runtime (you have to restart the server). The choice is resolved as follows:\n\n1. If `bacon_ls.backend` is set to `\"cargo\"` or `\"bacon\"`, that wins.\n2. Otherwise, if only one of `bacon_ls.cargo` or `bacon_ls.bacon` is present in\n   the settings, that backend is selected.\n3. Otherwise (both sections present without an explicit `backend`, or no\n   settings at all), the default is **cargo**.\n\nProviding both `cargo` and `bacon` sections without an explicit `backend`\nkey is reported as a configuration error.\n\n### Cargo backend options\n\nAvailable since `bacon-ls` 0.23.0, default since 0.26.0. Runs cargo directly with\n`--message-format=json-diagnostic-rendered-ansi`, parses the stream and publishes\ndiagnostics — no `bacon` process required.\n\n* `command` (default `\"check\"`): which cargo subcommand to run. Most useful values\n  are `\"check\"` and `\"clippy\"`.\n* `features`: list of features passed as `--features a,b,c`.\n* `package`: when set, passed as `-p <package>` (useful in workspaces).\n* `extraArgs`: appended verbatim after the subcommand. Use this for\n  e.g. `[\"--workspace\", \"--all-targets\", \"--all-features\"]`.\n* `env`: map of additional environment variables for the cargo invocation.\n* `cancelRunning` (default `true`): when a new run is requested while another is\n  still running, cancel the in-flight one. Set to `false` to instead queue at most\n  one follow-up run after the current one completes.\n* `refreshIntervalSeconds` (default `1`): how often to publish a partial snapshot\n  of the diagnostics gathered so far while cargo is still running. The very\n  first diagnostic of a run is always published immediately so the editor lights\n  up as soon as cargo emits something; this interval governs the cadence of\n  refreshes after that. Set to `null` or a negative number to only publish once\n  cargo has finished.\n* `separateChildDiagnostics` (default `null`): cargo emits some hints as children\n  of a parent diagnostic. When `null` we follow the client's\n  `relatedInformation` capability; set to `true` to always emit children as\n  standalone diagnostics, `false` to always nest them.\n* `checkOnSave` (default `true`): trigger a cargo run on `textDocument/didSave`.\n  Set to `false` if you only want to drive runs manually via `bacon_ls.run`.\n* `clearDiagnosticsOnCheck` (default `false`): publish empty diagnostics for all\n  files that previously had any before starting the new run. Useful if you want\n  the editor's diagnostic counters to drop to zero immediately at the start of\n  a check.\n* `updateOnInsertDebounceMillis` (default `500`): when live diagnostics are on\n  (see below), how long the server waits after the last keystroke before\n  triggering a cargo run against the shadow workspace. Lower values feel\n  snappier; higher values reduce the number of cargo invocations during a\n  burst of edits.\n\n### Live diagnostics as you type (cargo backend only)\n\nThe cargo backend can publish diagnostics on every keystroke instead of\nwaiting for a save. This is opt-in and turned off by default: when it's off,\nthe server doesn't even ask the editor for change events.\n\nHow it works: on the first dirty buffer, `bacon-ls` builds a \"shadow\"\nworkspace at `target/bacon-ls-live/shadow/` by hardlinking every\n`.gitignore`-respected file from the real workspace. Subsequent keystrokes\nwrite only the dirty buffer's bytes into the shadow (breaking the hardlink\nso the real file stays untouched), and a debounced cargo run targets the\nshadow with `--target-dir=target/bacon-ls-live/target` and\n`--remap-path-prefix=<shadow>=<real>` so diagnostics open the user's source\nfile rather than a `target/` copy. On `didSave` / `didClose` the file's\nshadow entry is replaced with a fresh hardlink to disk.\n\nTo enable it the flag has to come through **`initialization_options`**, not\nworkspace settings. The reason is timing: the LSP `textDocument/didChange`\nsync capability has to be advertised statically before workspace\nconfiguration arrives, and clients (Neovim in particular) don't reliably\nretrofit already-attached buffers when the server tries to register that\ncapability dynamically after `initialized`.\n\nFor Neovim's `vim.lsp.config`:\n\n```lua\nvim.lsp.config('bacon-ls', {\n    init_options = {\n        cargo = { updateOnInsert = true },\n    },\n    settings = {\n        bacon_ls = {\n            backend = \"cargo\",\n            cargo = {\n                command = \"clippy\",\n                -- updateOnInsert lives in init_options above; only the\n                -- runtime knob lives here:\n                updateOnInsertDebounceMillis = 500,\n            },\n        },\n    },\n})\n```\n\nFor LazyVim:\n\n```lua\nbacon_ls = {\n    enabled = true,\n    init_options = {\n        cargo = { updateOnInsert = true },\n    },\n    settings = {\n        bacon_ls = {\n            backend = \"cargo\",\n            cargo = {\n                command = \"clippy\",\n                updateOnInsertDebounceMillis = 500,\n            },\n        },\n    },\n},\n```\n\nTradeoffs and caveats:\n\n* **Linux-first.** Hardlinking and `--remap-path-prefix` work cross-platform,\n  but the integration tests cover Linux only. Mileage on macOS/Windows may\n  vary.\n* **Separate target directory.** The shadow run uses its own\n  `target/bacon-ls-live/target/` so the live cargo invocation doesn't\n  invalidate caches for the real `cargo build` you might run in a terminal.\n  Cost: extra disk space (typically the size of one debug build).\n* **First run is cold.** Building the shadow workspace and the\n  separate-target cargo cache is a one-shot cost that can take a few seconds\n  on a large project. Subsequent runs are incremental.\n* **No filesystem watcher.** Files added or deleted on disk while the editor\n  is open won't be reflected in the shadow until the next time the server\n  rebuilds it (currently: an LSP restart). Touching a file you've already\n  opened works, because we mirror its dirty state through `didChange`.\n* **`.gitignore` is respected.** The shadow walker uses the same logic as\n  ripgrep (`ignore` crate) with `require_git(false)`, so non-git workspaces\n  are handled too. Hidden files (`.cargo/`, `.git/`, etc.) are skipped.\n\nThis feature complements rather than replaces `rust-analyzer`: keeping\n`rust-analyzer` running alongside (with its own diagnostics turned off, see\nthe editor setup sections) gives you completion, hover, and go-to-definition\non top of bacon-ls's live diagnostics.\n\n### Bacon backend options\n\nReads diagnostics from the file produced by Bacon's `export-locations` feature.\nConfigure Bacon with the `bacon-ls` 🐽 export format in the `bacon` preference\nfile (`bacon --prefs` shows where it lives):\n\n```toml\n[jobs.bacon-ls]\ncommand = [\n  \"cargo\", \"clippy\",\n  \"--workspace\", \"--all-targets\", \"--all-features\",\n  \"--message-format\", \"json-diagnostic-rendered-ansi\",\n]\nanalyzer = \"cargo_json\"\nneed_stdout = true\n\n[exports.cargo-json-spans]\nauto = true\nexporter = \"analyzer\"\nline_format = \"\"\"\\\n  {diagnostic.level}|:|{span.file_name}|:|{span.line_start}|:|{span.line_end}|:|\\\n  {span.column_start}|:|{span.column_end}|:|{diagnostic.message}|:|{diagnostic.rendered}|:|\\\n  {span.suggested_replacement}\\\n\"\"\"\npath = \".bacon-locations\"\n```\n\n`bacon` itself must be running to keep the export file fresh\n(`bacon -j bacon-ls`). When `runInBackground` is `true` (the default since\n0.10.0), `bacon-ls` starts and supervises it for you.\n\n* `locationsFile` (default `\".bacon-locations\"`): bacon export file to read.\n* `runInBackground` (default `true`): start `bacon` automatically and tear it\n  down on shutdown.\n* `runInBackgroundCommand` (default `\"bacon\"`): command to spawn. Override if\n  `bacon` is not in `$PATH`.\n* `runInBackgroundCommandArguments` (default `\"--headless -j bacon-ls\"`):\n  command-line arguments passed to the background `bacon` process.\n* `validatePreferences` (default `true`): verify the bacon preferences file\n  contains a working `bacon-ls` job and matching export configuration. Errors\n  are surfaced to the LSP client.\n* `createPreferencesFile` (default `true`): if validation fails because the\n  preferences file is missing, generate one with the `bacon-ls` job and export\n  defined.\n* `synchronizeAllOpenFilesWaitMillis` (default `2000`): how often the background\n  loop re-publishes diagnostics for every open file (so a fix in file A also\n  clears the now-stale error in file B).\n* `updateOnSave` (default `true`): re-publish diagnostics on\n  `textDocument/didSave`.\n* `updateOnSaveWaitMillis` (default `1000`): delay before reading the locations\n  file after a save, to give bacon time to finish its run.\n\n### Manually triggering diagnostics\n\n`bacon-ls` 🐽 registers a single `workspace/executeCommand` named `bacon_ls.run`.\nInvoking it triggers an immediate cargo run when the cargo backend is active\n(the bacon backend ignores it — there is nothing for it to drive directly).\n\nThis is how clients can offer a \"run check now\" command without relying on save\nevents. Example from a Neovim mapping:\n\n```lua\nvim.keymap.set(\"n\", \"<leader>cb\", function()\n  vim.lsp.buf.execute_command({ command = \"bacon_ls.run\" })\nend, { desc = \"bacon-ls: run check\" })\n```\n\n### Changing configuration at runtime\n\n`bacon-ls` honours `workspace/didChangeConfiguration` and re-reads its settings,\nbut with one important constraint: **the backend choice is fixed for the\nlifetime of the process**. Trying to switch from `cargo` to `bacon` (or vice\nversa) without restarting the server is reported as an error to the client and\nignored. All other options (cargo command, features, bacon update interval, …)\ncan be changed live.\n\n## Migrating from 0.26.x and earlier\n\nPR [#113](https://github.com/crisidev/bacon-ls/pull/113) reorganised the\nconfiguration into per-backend sections. If you were on 0.26.x or earlier, the\nfollowing changes apply:\n\n* `useBaconBackend` is gone. Replace it with either the explicit\n  `\"backend\": \"bacon\"` or simply by providing a `\"bacon\": { ... }` section.\n* All `runBaconInBackground*`, `validateBaconPreferences`,\n  `createBaconPreferencesFile`, `synchronizeAllOpenFilesWaitMillis`,\n  `updateOnSave`, `updateOnSaveWaitMillis` and `locationsFile` keys have moved\n  inside `bacon_ls.bacon.*` and dropped the `Bacon` prefix where it was\n  redundant (e.g. `runBaconInBackground` → `bacon.runInBackground`,\n  `validateBaconPreferences` → `bacon.validatePreferences`).\n* All cargo-related keys live under `bacon_ls.cargo.*`.\n* The backend can no longer be changed live — restart the server to switch.\n\nOld config:\n\n```jsonc\n{\n  \"bacon_ls\": {\n    \"useBaconBackend\": true,\n    \"runBaconInBackground\": true,\n    \"validateBaconPreferences\": true,\n    \"updateOnSave\": true\n  }\n}\n```\n\nNew equivalent:\n\n```jsonc\n{\n  \"bacon_ls\": {\n    \"backend\": \"bacon\",\n    \"bacon\": {\n      \"runInBackground\": true,\n      \"validatePreferences\": true,\n      \"updateOnSave\": true\n    }\n  }\n}\n```\n\n## Editor setup\n\n### Neovim - LazyVim\n\n```lua\nvim.g.lazyvim_rust_diagnostics = \"bacon-ls\"\n```\n\n### Neovim - Manual\n\nNeoVim requires [nvim-lspconfig](https://github.com/neovim/nvim-lspconfig/) to be configured\nand [rust-analyzer](https://rust-analyzer.github.io/) diagnostics must be turned off for `bacon-ls` 🐽\nto properly function.\n\n`bacon-ls` is part of `nvim-lspconfig` from commit\n[6d2ae9f](https://github.com/neovim/nvim-lspconfig/commit/6d2ae9fdc3111a6e8fd5db2467aca11737195a30)\nand it can be configured like any other LSP server works best when\n[vim.diagnostics.opts.update_in_insert](https://neovim.io/doc/user/diagnostic.html#vim.diagnostic.Opts)\nis set to `true`.\n\n```lua\nvim.lsp.config('bacon-ls', {\n    settings = {\n        bacon_ls = {\n            backend = \"cargo\",\n            cargo = {\n                command = \"clippy\",\n                checkOnSave = true,\n            },\n        },\n    },\n})\n```\n\nAll runtime settings live under the `settings.bacon_ls` table above. The one\nsetting that has to be in `init_options` instead is `cargo.updateOnInsert`\n(see [Live diagnostics as you type](#live-diagnostics-as-you-type-cargo-backend-only)\nfor why and the exact shape).\n\nWhen using [codesettings](https://github.com/mrjones2014/codesettings.nvim)\nto manage project local settings\n\n```\nvim.lsp.config(\"*\", {\n  before_init = function(_, config)\n    local codesettings = require(\"codesettings\")\n    if config.name == \"bacon_ls\" then\n      local settings = codesettings.local_settings()[\"_settings\"][\"bacon_ls\"]\n      if settings ~= nil then\n        config[\"settings\"][\"bacon_ls\"] = settings\n        vim.print(config[\"settings\"][\"bacon_ls\"])\n      end\n      return config\n    end\n\n    return codesettings.with_local_settings(config.name, config)\n  end,\n})\n```\n\nFor `rust-analyzer`, these 2 options must be turned off:\n\n```lua\nrust-analyzer.checkOnSave.enable = false\nrust-analyzer.diagnostics.enable = false\n```\n\n### VSCode\n\nThe extension can be configured using the VSCode settings interface.\n\n**It is very important that rust-analyzer `Check On Save` and `Diagnostics` are turned off for `bacon-ls` to work properly:**\n\n* Untick `Rust-analyzer -> general -> Check On Save`\n* Untick `Rust-analyzer -> diagnostics -> Enable`\n\n### Coc.nvim\n\n```vim\ncall coc#config('languageserver', {\n      \\ 'bacon-ls': {\n      \\   'command': '~/.cargo/bin/bacon-ls',\n      \\   'filetypes': ['rust'],\n      \\   'rootPatterns': ['.git/', 'Cargo.lock', 'Cargo.toml'],\n      \\   'settings': {\n      \\    'cargo': {\n      \\      'cancelRunning': true,\n      \\    }\n      \\   }\n      \\  }\n      \\ }\n\\ })\n```\n\n### Helix\n\nExtend your `languages.toml` with the following:\n\n```toml\n[[language]]\nname = \"rust\"\nlanguage-servers = [\"rust-analyzer\", \"bacon-ls\"]\n\n[language-server.rust-analyzer.config]\ncheckOnSave = { enable = false }\ndiagnostics = { enable = false }\n\n[language-server.bacon-ls]\ncommand = \"bacon-ls\"\n\n[language-server.bacon-ls.config.bacon_ls]\nbackend = \"cargo\"\n\n[language-server.bacon-ls.config.bacon_ls.cargo]\ncommand = \"clippy\"\n```\n\n## Troubleshooting\n\n`bacon-ls` 🐽 can produce a log file in the folder where its running by exporting the `RUST_LOG` variable in the shell:\n\n### Bacon preferences\n\nIf the `bacon` preference are not correct, an error message will be published to the LSP client, advising the user to\ncheck the README.\n\n### Vim - Neovim\n\n```bash\n❯❯❯ export RUST_LOG=debug\n❯❯❯ nvim src/some-file.rs                 # or vim src/some-file.rs\n# the variable can also be exported for the current command and not for the whole shell\n❯❯❯ RUST_LOG=debug nvim src/some-file.rs  # or RUST_LOG=debug vim src/some-file.rs\n❯❯❯ tail -F ./bacon-ls.log\n```\n\n### VSCode\n\nEnable debug logging in the extension options.\n\n```bash\n❯❯❯ tail -F ./bacon-ls.log\n```\n\n## How does it work?\n\n`bacon-ls` 🐽 speaks LSP over STDIO and publishes diagnostics to the client via\n`textDocument/publishDiagnostics`. How those diagnostics are produced depends on\nthe active backend.\n\n**Cargo backend (default).** On each trigger (initial start, file save, or a\nmanual `bacon_ls.run`), `bacon-ls` runs `cargo check` (or `cargo clippy`) with\n`--message-format=json-diagnostic-rendered-ansi` from the project root. The JSON\nstream is parsed as it arrives, spans from macro expansions are walked back to\nthe original call site, and diagnostics are published per file. With\n`refreshIntervalSeconds` set, partial snapshots are pushed while cargo is still\nrunning so the editor shows errors as soon as they are known. The previous run\nis cancelled when a newer one starts (or queued, depending on `cancelRunning`).\n\n**Bacon backend.** [Bacon](https://dystroy.org/bacon/) runs in a watch loop and\nwrites diagnostics to its export file (default `.bacon-locations`) using a\ncustom `line_format`. `bacon-ls` reads that file on save / open / close / rename\nevents and on a periodic open-file synchronization tick, parses the lines, and\npublishes the resulting diagnostics. When `runInBackground` is on, `bacon-ls`\nalso spawns and supervises the `bacon` process itself.\n\nBoth backends share the same code-actions pipeline: when a diagnostic carries a\nsuggested replacement, it is exposed as a `quickfix` code action via\n`textDocument/codeAction`.\n\n## Thanks\n\n`bacon-ls` 🐽 has been inspired by [typos-lsp](https://github.com/tekumara/typos-lsp).\n\n## Roadmap to 1.0 - ✅ done 🕖 in progress 🌍 future\n\n- ✅ Implement LSP server interface for `textDocument/diagnostic` and `workspace/diagnostic`\n- ✅ Manual Neovim configuration\n- ✅ Manual [LazyVim](https://www.lazyvim.org) configuration\n- ✅ Automatic NeoVim configuration\n  - ✅ Add `bacon-ls` to [nvim-lspconfig](https://github.com/neovim/nvim-lspconfig/) - https://github.com/neovim/nvim-lspconfig/pull/3160\n  - ✅ Add `bacon` and `bacon-ls` to [mason.nvim](https://github.com/williamboman/mason.nvim) - https://github.com/mason-org/mason-registry/pull/5774\n  - ✅ 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\n- ✅ 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\n- ✅ Support correct span in [Bacon](https://dystroy.org/bacon/) export locations - working from `bacon` 3.7 and `bacon-ls` 0.6.0\n- ✅ VSCode extension and configuration - available on the [release](https://github.com/crisidev/bacon-ls/releases) page from 0.6.0\n- ✅ VSCode extension published available on Marketplace\n- ✅ Add `bacon-ls` to `bacon` website - https://github.com/Canop/bacon/pull/289\n- ✅ Smarter handling of parsing the Bacon locations file\n- ✅ Faster response after a save event\n- ✅ Replacement code actions\n- ✅ 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\n- ✅ Create `bacon` preferences file if not found on disk - working from `bacon-ls` 0.10.0\n- ✅ Start `bacon` in background based on user preferences - working from `bacon-ls` 0.10.0\n- ✅ Synchronize diagnostics for all open files - working from `bacon-ls` 0.10.0\n- ✅ Support Helix editor - working from `bacon-ls` 0.12.0\n- ✅ Nix flake support\n- ✅ Support [cargo workspaces](https://doc.rust-lang.org/book/ch14-03-cargo-workspaces.html) - working from `bacon-ls` 0.14.0\n- ✅ Faster native cargo backend - default from `bacon-ls` 0.23.0\n- 🌍 Emacs configuration\n"
  },
  {
    "path": "eslint.config.mjs",
    "content": "import typescriptEslint from \"@typescript-eslint/eslint-plugin\";\nimport stylistic from \"@stylistic/eslint-plugin\";\nimport tsParser from \"@typescript-eslint/parser\";\n\nexport default [\n  {\n    ignores: [\"**/out\", \"**/dist\", \"**/*.d.ts\"],\n  },\n  {\n    plugins: {\n      \"@typescript-eslint\": typescriptEslint,\n      \"@stylistic\": stylistic,\n    },\n\n    languageOptions: {\n      parser: tsParser,\n      ecmaVersion: 6,\n      sourceType: \"module\",\n    },\n\n    rules: {\n      \"@typescript-eslint/naming-convention\": \"warn\",\n      \"@stylistic/semi\": \"warn\",\n      curly: \"warn\",\n      eqeqeq: \"warn\",\n      \"no-throw-literal\": \"warn\",\n      semi: \"off\",\n    },\n  },\n];\n"
  },
  {
    "path": "flake.nix",
    "content": "{\n  inputs = {\n    flake-utils.url = \"github:numtide/flake-utils\";\n    naersk.url = \"github:nix-community/naersk\";\n    nixpkgs.url = \"github:NixOS/nixpkgs/nixpkgs-unstable\";\n  };\n\n  outputs =\n    {\n      self,\n      flake-utils,\n      naersk,\n      nixpkgs,\n    }:\n    flake-utils.lib.eachDefaultSystem (\n      system:\n      let\n        pkgs = (import nixpkgs) {\n          inherit system;\n        };\n\n        naersk' = pkgs.callPackage naersk { };\n        bacon-ls = naersk'.buildPackage {\n          buildInputs = with pkgs; [ perl openssl ];\n          nativeBuildInputs = with pkgs; [ perl openssl ];\n          src = ./.;\n        };\n\n      in {\n        # For `nix build` & `nix run`:\n        defaultPackage = bacon-ls;\n\n        # For `nix develop` (optional, can be skipped):\n        devShell = pkgs.mkShell {\n          nativeBuildInputs = with pkgs; [\n            cargo-audit\n            cargo-nextest\n            grcov\n            llvmPackages_19.libllvm\n            rust-analyzer\n          ];\n        };\n\n        # Overlay for package usage in other Nix configurations\n        overlay = _: _: {\n          inherit bacon-ls;\n        };\n      }\n    );\n}\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"bacon-ls-vscode\",\n  \"displayName\": \"Bacon Language Server\",\n  \"description\": \"Rust diagnostic based on Bacon\",\n  \"publisher\": \"MatteoBigoi\",\n  \"version\": \"0.29.0\",\n  \"private\": true,\n  \"icon\": \"img/icon.png\",\n  \"repository\": {\n    \"url\": \"https://github.com/crisidev/bacon-ls.git\",\n    \"type\": \"git\"\n  },\n  \"license\": \"MIT\",\n  \"keywords\": [\n    \"diagnostic\",\n    \"rust\",\n    \"lsp\"\n  ],\n  \"categories\": [\n    \"Linters\",\n    \"Other\"\n  ],\n  \"engines\": {\n    \"node\": \"^20.18.0\",\n    \"vscode\": \"^1.84.0\"\n  },\n  \"activationEvents\": [\n    \"onStartupFinished\"\n  ],\n  \"main\": \"./out/main.js\",\n  \"contributes\": {\n    \"commands\": [\n      {\n        \"category\": \"BaconLs\",\n        \"command\": \"bacon-ls.restart\",\n        \"title\": \"Restart\"\n      },\n      {\n        \"category\": \"BaconLs\",\n        \"command\": \"bacon-ls.run\",\n        \"title\": \"Run check\"\n      }\n    ],\n    \"configuration\": {\n      \"type\": \"object\",\n      \"title\": \"BaconLs\",\n      \"properties\": {\n        \"bacon-ls.path\": {\n          \"scope\": \"machine-overridable\",\n          \"type\": \"string\",\n          \"description\": \"Path to the `bacon-ls` binary. If empty the bundled binary will be used.\"\n        },\n        \"bacon-ls.logLevel\": {\n          \"scope\": \"window\",\n          \"type\": \"string\",\n          \"enum\": [\n            \"off\",\n            \"error\",\n            \"warn\",\n            \"info\",\n            \"debug\",\n            \"trace\"\n          ],\n          \"default\": \"off\",\n          \"markdownDescription\": \"Logging level of the language server. Logs will be saved in a file called bacon-ls.log if the level is not 'off'.\"\n        },\n        \"bacon_ls.backend\": {\n          \"scope\": \"machine-overridable\",\n          \"type\": \"string\",\n          \"enum\": [\n            \"cargo\",\n            \"bacon\"\n          ],\n          \"default\": \"cargo\",\n          \"markdownDescription\": \"Which backend produces diagnostics. Cannot be changed at runtime — restart the server to switch.\"\n        },\n        \"bacon_ls.cargo.command\": {\n          \"scope\": \"machine-overridable\",\n          \"type\": \"string\",\n          \"enum\": [\n            \"check\",\n            \"clippy\"\n          ],\n          \"default\": \"check\",\n          \"description\": \"Cargo backend: which cargo subcommand to run.\"\n        },\n        \"bacon_ls.cargo.features\": {\n          \"scope\": \"machine-overridable\",\n          \"type\": \"array\",\n          \"items\": {\n            \"type\": \"string\"\n          },\n          \"default\": [],\n          \"description\": \"Cargo backend: list of cargo features (passed as --features a,b,c).\"\n        },\n        \"bacon_ls.cargo.package\": {\n          \"scope\": \"machine-overridable\",\n          \"type\": [\n            \"string\",\n            \"null\"\n          ],\n          \"default\": null,\n          \"description\": \"Cargo backend: when set, passed as -p <package> (useful in workspaces).\"\n        },\n        \"bacon_ls.cargo.extraArgs\": {\n          \"scope\": \"machine-overridable\",\n          \"type\": \"array\",\n          \"items\": {\n            \"type\": \"string\"\n          },\n          \"default\": [],\n          \"description\": \"Cargo backend: extra arguments appended verbatim after the cargo subcommand.\"\n        },\n        \"bacon_ls.cargo.env\": {\n          \"scope\": \"machine-overridable\",\n          \"type\": \"object\",\n          \"additionalProperties\": {\n            \"type\": \"string\"\n          },\n          \"default\": {},\n          \"description\": \"Cargo backend: extra environment variables (string -> string) for the cargo invocation.\"\n        },\n        \"bacon_ls.cargo.cancelRunning\": {\n          \"scope\": \"machine-overridable\",\n          \"type\": \"boolean\",\n          \"default\": true,\n          \"description\": \"Cargo backend: cancel an in-flight run when a new one is triggered. When false, queue at most one follow-up run.\"\n        },\n        \"bacon_ls.cargo.refreshIntervalSeconds\": {\n          \"scope\": \"machine-overridable\",\n          \"type\": [\n            \"integer\",\n            \"null\"\n          ],\n          \"default\": 5,\n          \"description\": \"Cargo backend: partial publish interval in seconds. Set to null (or negative) to only publish once cargo has finished.\"\n        },\n        \"bacon_ls.cargo.separateChildDiagnostics\": {\n          \"scope\": \"machine-overridable\",\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ],\n          \"default\": null,\n          \"description\": \"Cargo backend: override 'related information' support. null = follow client capability; true = always emit children as standalone diagnostics; false = always nest them.\"\n        },\n        \"bacon_ls.cargo.checkOnSave\": {\n          \"scope\": \"machine-overridable\",\n          \"type\": \"boolean\",\n          \"default\": true,\n          \"description\": \"Cargo backend: trigger cargo on file save.\"\n        },\n        \"bacon_ls.cargo.clearDiagnosticsOnCheck\": {\n          \"scope\": \"machine-overridable\",\n          \"type\": \"boolean\",\n          \"default\": false,\n          \"description\": \"Cargo backend: clear existing diagnostics before each run.\"\n        },\n        \"bacon_ls.bacon.locationsFile\": {\n          \"scope\": \"machine-overridable\",\n          \"type\": \"string\",\n          \"default\": \".bacon-locations\",\n          \"description\": \"Bacon backend: bacon export file to read.\"\n        },\n        \"bacon_ls.bacon.runInBackground\": {\n          \"scope\": \"machine-overridable\",\n          \"type\": \"boolean\",\n          \"default\": true,\n          \"description\": \"Bacon backend: start bacon automatically and tear it down on shutdown.\"\n        },\n        \"bacon_ls.bacon.runInBackgroundCommand\": {\n          \"scope\": \"machine-overridable\",\n          \"type\": \"string\",\n          \"default\": \"bacon\",\n          \"description\": \"Bacon backend: command used to start bacon in the background.\"\n        },\n        \"bacon_ls.bacon.runInBackgroundCommandArguments\": {\n          \"scope\": \"machine-overridable\",\n          \"type\": \"string\",\n          \"default\": \"--headless -j bacon-ls\",\n          \"description\": \"Bacon backend: command-line arguments passed to the background bacon process.\"\n        },\n        \"bacon_ls.bacon.validatePreferences\": {\n          \"scope\": \"machine-overridable\",\n          \"type\": \"boolean\",\n          \"default\": true,\n          \"description\": \"Bacon backend: verify that bacon preferences contain a working bacon-ls job and matching export configuration.\"\n        },\n        \"bacon_ls.bacon.createPreferencesFile\": {\n          \"scope\": \"machine-overridable\",\n          \"type\": \"boolean\",\n          \"default\": true,\n          \"description\": \"Bacon backend: when validation finds no preferences file, generate one with the bacon-ls job and export defined.\"\n        },\n        \"bacon_ls.bacon.synchronizeAllOpenFilesWaitMillis\": {\n          \"scope\": \"machine-overridable\",\n          \"type\": \"integer\",\n          \"default\": 2000,\n          \"description\": \"Bacon backend: how often (in ms) the background loop re-publishes diagnostics for every open file.\"\n        },\n        \"bacon_ls.bacon.updateOnSave\": {\n          \"scope\": \"machine-overridable\",\n          \"type\": \"boolean\",\n          \"default\": true,\n          \"description\": \"Bacon backend: re-publish diagnostics on file save.\"\n        },\n        \"bacon_ls.bacon.updateOnSaveWaitMillis\": {\n          \"scope\": \"machine-overridable\",\n          \"type\": \"integer\",\n          \"default\": 1000,\n          \"description\": \"Bacon backend: delay in ms before reading the locations file after a save.\"\n        }\n      }\n    }\n  },\n  \"scripts\": {\n    \"vscode:prepublish\": \"npm run esbuild-base -- --minify\",\n    \"package\": \"vsce package\",\n    \"esbuild-base\": \"esbuild ./vscode/extension.ts --bundle --outfile=out/main.js --external:vscode --format=cjs --platform=node\",\n    \"build\": \"npm run esbuild-base -- --sourcemap\",\n    \"watch\": \"npm run esbuild-base -- --sourcemap --watch\",\n    \"lint\": \"prettier --check . && eslint\",\n    \"fix\": \"prettier --write . && eslint --fix\",\n    \"pretest\": \"tsc && npm run build\",\n    \"test\": \"cross-env BACON_LS_PATH=$PWD/target/debug/bacon-ls node ./out/test/runTest.js\"\n  },\n  \"devDependencies\": {\n    \"@stylistic/eslint-plugin\": \"^5.10.0\",\n    \"@types/glob\": \"^9.0.0\",\n    \"@types/mocha\": \"^10.0.10\",\n    \"@types/node\": \"25.x\",\n    \"@types/vscode\": \"^1.84.0\",\n    \"@typescript-eslint/eslint-plugin\": \"^8.59.1\",\n    \"@typescript-eslint/parser\": \"^8.31.1\",\n    \"@vscode/test-electron\": \"^2.5.2\",\n    \"@vscode/vsce\": \"^3.9.1\",\n    \"cross-env\": \"^10.1.0\",\n    \"esbuild\": \"^0.28.0\",\n    \"eslint\": \"^10.2.1\",\n    \"glob\": \"^13.0.6\",\n    \"mocha\": \"^11.7.5\",\n    \"ovsx\": \"^0.10.11\",\n    \"prettier\": \"^3.8.3\",\n    \"typescript\": \"^6.0.3\"\n  },\n  \"dependencies\": {\n    \"vscode-languageclient\": \"^9.0.1\"\n  }\n}\n"
  },
  {
    "path": "rustfmt.toml",
    "content": "edition = \"2024\"\nmax_width = 120\nreorder_imports = true\nuse_field_init_shorthand = true\n"
  },
  {
    "path": "src/bacon.rs",
    "content": "use std::borrow::Cow;\nuse std::env;\nuse std::path::{Path, PathBuf};\nuse std::process::Stdio;\nuse std::sync::Arc;\nuse std::time::Duration;\n\nuse ls_types::{Diagnostic, DiagnosticSeverity, Position, Range, Uri, WorkspaceFolder};\nuse notify_debouncer_full::{DebounceEventResult, new_debouncer};\nuse serde::{Deserialize, Serialize};\nuse tokio::fs::File;\nuse tokio::io::{AsyncBufReadExt, BufReader};\nuse tokio::process::Command;\nuse tokio::sync::RwLock;\nuse tokio::task::JoinHandle;\nuse tokio_util::sync::CancellationToken;\nuse tower_lsp_server::Client;\n\nuse std::collections::HashSet;\n\nuse crate::{\n    BackendRuntime, BaconLs, Correction, DiagKey, DiagnosticData, LOCATIONS_FILE, PKG_NAME, State, diag_key,\n    path_to_file_uri,\n};\n\n#[derive(Debug, Deserialize, Serialize)]\nstruct BaconConfig {\n    jobs: Jobs,\n    exports: Exports,\n}\n\n#[derive(Debug, Deserialize, Serialize)]\nstruct Jobs {\n    #[serde(rename = \"bacon-ls\")]\n    bacon_ls: BaconLsJob,\n}\n\n#[derive(Debug, Deserialize, Serialize)]\nstruct BaconLsJob {\n    #[serde(skip_deserializing)]\n    command: Vec<String>,\n    analyzer: String,\n    need_stdout: bool,\n}\n\n#[derive(Debug, Deserialize, Serialize)]\nstruct Exports {\n    #[serde(rename = \"cargo-json-spans\")]\n    cargo_json_spans: CargoJsonSpans,\n}\n\n#[derive(Debug, Deserialize, Serialize)]\nstruct CargoJsonSpans {\n    auto: bool,\n    exporter: String,\n    line_format: String,\n    path: String,\n}\n\nconst 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\";\nconst BACON_ANALYZER: &str = \"cargo_json\";\nconst BACON_EXPORTER: &str = \"analyzer\";\nconst BACON_COMMAND: [&str; 7] = [\n    \"cargo\",\n    \"clippy\",\n    \"--tests\",\n    \"--all-targets\",\n    \"--all-features\",\n    \"--message-format\",\n    \"json-diagnostic-rendered-ansi\",\n];\nconst 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}\";\n\npub(crate) struct Bacon;\n\nimpl Bacon {\n    async fn validate_preferences_file(path: &Path) -> Result<(), String> {\n        let toml_content = tokio::fs::read_to_string(path)\n            .await\n            .map_err(|e| format!(\"{ERROR_MESSAGE}: {e}\"))?;\n        let config: BaconConfig = toml::from_str(&toml_content).map_err(|e| format!(\"{ERROR_MESSAGE}: {e}\"))?;\n        tracing::debug!(\"bacon config is {config:#?}\");\n        if config.jobs.bacon_ls.analyzer == BACON_ANALYZER\n            && config.jobs.bacon_ls.need_stdout\n            && config.exports.cargo_json_spans.auto\n            && config.exports.cargo_json_spans.exporter == BACON_EXPORTER\n            && config.exports.cargo_json_spans.line_format == LINE_FORMAT\n            && config.exports.cargo_json_spans.path == LOCATIONS_FILE\n        {\n            tracing::info!(\"bacon configuration {} is valid\", path.display());\n            Ok(())\n        } else {\n            Err(ERROR_MESSAGE.to_string())\n        }\n    }\n\n    async fn create_preferences_file(filename: &str) -> Result<(), String> {\n        let bacon_config = BaconConfig {\n            jobs: Jobs {\n                bacon_ls: BaconLsJob {\n                    command: BACON_COMMAND.map(|c| c.to_string()).into_iter().collect(),\n                    analyzer: BACON_ANALYZER.to_string(),\n                    need_stdout: true,\n                },\n            },\n            exports: Exports {\n                cargo_json_spans: CargoJsonSpans {\n                    auto: true,\n                    exporter: BACON_EXPORTER.to_string(),\n                    line_format: LINE_FORMAT.to_string(),\n                    path: LOCATIONS_FILE.to_string(),\n                },\n            },\n        };\n        tracing::info!(\"creating new bacon preference file {filename}\",);\n        let toml_string = toml::to_string_pretty(&bacon_config)\n            .map_err(|e| format!(\"error serializing bacon preferences {filename} content: {e}\"))?;\n        // `tokio::fs::write` is open + write_all + flush + close in one shot,\n        // so the bytes are durable by the time the future resolves. The\n        // previous `File::create + write_all` form left flushing to drop and\n        // could race a subsequent read on busy CI runners (causing the\n        // freshly-created file to be observed empty during validation).\n        tokio::fs::write(filename, toml_string)\n            .await\n            .map_err(|e| format!(\"error creating bacon preferences {filename}: {e}\"))?;\n        Ok(())\n    }\n\n    async fn validate_preferences_impl(bacon_prefs: &[u8], create_prefs_file: bool) -> Result<(), String> {\n        let bacon_prefs_files = String::from_utf8_lossy(bacon_prefs);\n        let bacon_prefs_files_split: Vec<&str> = bacon_prefs_files.split(\"\\n\").collect();\n        let mut preference_file_exists = false;\n        for prefs_file in bacon_prefs_files_split.iter() {\n            let prefs_file_path = Path::new(prefs_file);\n            if prefs_file_path.exists() {\n                preference_file_exists = true;\n                Self::validate_preferences_file(prefs_file_path).await?;\n            } else {\n                tracing::debug!(\"skipping non existing bacon preference file {prefs_file}\");\n            }\n        }\n\n        if !preference_file_exists && create_prefs_file {\n            Self::create_preferences_file(bacon_prefs_files_split[0]).await?;\n        }\n\n        Ok(())\n    }\n\n    /// Walks `root` recursively for files named `locations_file_name`. Iterative\n    /// (stack-based) so it doesn't grow the async stack on deep trees, and uses\n    /// `tokio::fs` so the directory walk yields to the runtime instead of\n    /// blocking the executor on large workspaces.\n    pub(crate) async fn find_bacon_locations(root: &Path, locations_file_name: &str) -> std::io::Result<Vec<PathBuf>> {\n        let mut results = Vec::new();\n        let mut stack: Vec<PathBuf> = vec![root.to_path_buf()];\n        while let Some(dir) = stack.pop() {\n            let mut entries = tokio::fs::read_dir(&dir).await?;\n            while let Some(entry) = entries.next_entry().await? {\n                let path = entry.path();\n                let file_type = entry.file_type().await?;\n                if file_type.is_dir() {\n                    stack.push(path);\n                } else if path.file_name().is_some_and(|name| name == locations_file_name) {\n                    results.push(path);\n                }\n            }\n        }\n        Ok(results)\n    }\n\n    fn parse_severity(severity_str: &str) -> DiagnosticSeverity {\n        match severity_str {\n            \"error\" => DiagnosticSeverity::ERROR,\n            \"warning\" => DiagnosticSeverity::WARNING,\n            \"info\" | \"information\" | \"note\" | \"failure-note\" => DiagnosticSeverity::INFORMATION,\n            \"hint\" | \"help\" => DiagnosticSeverity::HINT,\n            other => {\n                tracing::warn!(\"unknown bacon severity level {other:?}, defaulting to INFORMATION\");\n                DiagnosticSeverity::INFORMATION\n            }\n        }\n    }\n\n    fn parse_positions(fields: &[&str]) -> Option<(u32, u32, u32, u32)> {\n        let line_start = fields.first()?.parse().ok()?;\n        let line_end = fields.get(1)?.parse().ok()?;\n        let column_start = fields.get(2)?.parse().ok()?;\n        let column_end = fields.get(3)?.parse().ok()?;\n        Some((line_start, line_end, column_start, column_end))\n    }\n\n    fn parse_bacon_diagnostic_line(line: &str, folder_path: &Path) -> Option<(Uri, Diagnostic)> {\n        // Split line into parts; expect exactly 7 parts in the format specified.\n        let line_split: Vec<_> = line.splitn(9, \"|:|\").collect();\n\n        if line_split.len() != 9 {\n            tracing::error!(\n                \"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 {}: {}\",\n                line_split.len(),\n                line\n            );\n            return None;\n        }\n\n        // Parse elements from the split line\n        let severity = Self::parse_severity(line_split[0]);\n        let file_path = folder_path.join(line_split[1]);\n\n        // Handle potential parse errors\n        let (line_start, line_end, column_start, column_end) = match Self::parse_positions(&line_split[2..6]) {\n            Some(values) => values,\n            None => {\n                tracing::error!(\"error parsing diagnostic position {:?}\", &line_split[2..6]);\n                return None;\n            }\n        };\n\n        let Some(file_path_str) = file_path.to_str() else {\n            tracing::error!(\"file path is not valid UTF-8: {}\", file_path.display());\n            return None;\n        };\n        let path = match str::parse::<Uri>(&path_to_file_uri(file_path_str)) {\n            Ok(url) => url,\n            Err(e) => {\n                tracing::error!(\"error parsing file path {}: {}\", file_path.display(), e);\n                return None;\n            }\n        };\n\n        let mut message = line_split[6].replace(\"\\\\n\", \"\\n\").trim_end_matches('\\n').to_string();\n        let range = Range::new(\n            Position::new(line_start.saturating_sub(1), column_start.saturating_sub(1)),\n            Position::new(line_end.saturating_sub(1), column_end.saturating_sub(1)),\n        );\n        let replacement = line_split[8];\n        let data = if replacement != \"none\" {\n            tracing::debug!(\"storing potential quick fix code action to replace word with {replacement}\");\n            Some(serde_json::json!(DiagnosticData {\n                corrections: vec![Correction::from_single(range, replacement)]\n            }))\n        } else {\n            None\n        };\n\n        tracing::debug!(\n            \"new diagnostic: severity: {severity:?}, path: {path:?}, line_start: {line_start}, line_end: {line_end}, column_start: {column_start}, column_end: {column_end}, message: {message}\",\n        );\n\n        // Create the Diagnostic object\n        let rendered_message = line_split[7];\n        if rendered_message != \"none\" {\n            message = ansi_regex::ansi_regex()\n                .replace_all(rendered_message, \"\")\n                .trim_end_matches('\\n')\n                .to_string()\n        }\n        let diagnostic = Diagnostic {\n            range,\n            severity: Some(severity),\n            source: Some(PKG_NAME.to_string()),\n            message,\n            data,\n            ..Diagnostic::default()\n        };\n\n        Some((path, diagnostic))\n    }\n\n    fn deduplicate_diagnostics(\n        path: Uri,\n        uri: &Uri,\n        diagnostic: Diagnostic,\n        diagnostics: &mut Vec<(Uri, Diagnostic)>,\n        seen: &mut HashSet<DiagKey>,\n    ) {\n        if &path != uri {\n            return;\n        }\n        if seen.insert(diag_key(&diagnostic)) {\n            diagnostics.push((path, diagnostic));\n        }\n    }\n\n    pub(crate) async fn validate_preferences(bacon_command: &str, create_prefs_file: bool) -> Result<(), String> {\n        let bacon_prefs = Command::new(bacon_command)\n            .arg(\"--prefs\")\n            .output()\n            .await\n            .map_err(|e| e.to_string())?;\n        Self::validate_preferences_impl(&bacon_prefs.stdout, create_prefs_file).await\n    }\n\n    pub(crate) async fn run_in_background(\n        bacon_command: &str,\n        bacon_command_args: &str,\n        current_dir: Option<&PathBuf>,\n        cancel_token: CancellationToken,\n    ) -> Result<JoinHandle<()>, String> {\n        tracing::info!(\"starting bacon in background with arguments `{bacon_command_args}`\");\n        let log_bacon = env::var(\"BACON_LS_LOG_BACON\").unwrap_or(\"on\".to_string());\n        let mut command = Command::new(bacon_command);\n        command\n            .args(bacon_command_args.split_whitespace().collect::<Vec<&str>>())\n            .stdin(Stdio::null())\n            .stdout(Stdio::piped())\n            .stderr(Stdio::piped())\n            .kill_on_drop(true);\n        if let Some(current_dir) = current_dir {\n            command.current_dir(current_dir);\n        }\n\n        match command.spawn() {\n            Ok(mut child) => {\n                // Handle stdout\n                if log_bacon != \"off\"\n                    && let Some(stdout) = child.stdout.take()\n                {\n                    let reader = BufReader::new(stdout).lines();\n                    tokio::spawn(async move {\n                        let mut reader = reader;\n                        while let Ok(Some(line)) = reader.next_line().await {\n                            tracing::info!(\"[bacon stdout]: {}\", line);\n                        }\n                    });\n                }\n\n                // Handle stderr\n                if log_bacon != \"off\"\n                    && let Some(stderr) = child.stderr.take()\n                {\n                    let reader = BufReader::new(stderr).lines();\n                    tokio::spawn(async move {\n                        let mut reader = reader;\n                        while let Ok(Some(line)) = reader.next_line().await {\n                            tracing::error!(\"[bacon stderr]: {}\", line);\n                        }\n                    });\n                }\n\n                // Wait for the child process to finish\n                Ok(tokio::spawn(async move {\n                    tracing::debug!(\"waiting for bacon to terminate\");\n                    tokio::select! {\n                        _ = child.wait() => {},\n                        _ = cancel_token.cancelled() => {},\n                    };\n                }))\n            }\n            Err(e) => Err(format!(\"failed to start bacon: {e}\")),\n        }\n    }\n\n    async fn diagnostics(\n        uri: &Uri,\n        locations_file_name: &str,\n        workspace_folders: Option<&[WorkspaceFolder]>,\n    ) -> Vec<(Uri, Diagnostic)> {\n        let mut diagnostics: Vec<(Uri, Diagnostic)> = vec![];\n        let mut seen: HashSet<DiagKey> = HashSet::new();\n\n        if let Some(workspace_folders) = workspace_folders {\n            for folder in workspace_folders.iter() {\n                let Some(mut folder_path) = folder.uri.to_file_path() else {\n                    tracing::warn!(\"skipping workspace folder with non-file URI: {}\", folder.uri.as_str());\n                    continue;\n                };\n                if let Some(git_root) = BaconLs::find_git_root_directory(&folder_path).await\n                    && git_root.join(\"Cargo.toml\").exists()\n                {\n                    tracing::debug!(\n                        \"found git root directory {}, using it for files base path\",\n                        git_root.display()\n                    );\n                    folder_path = Cow::Owned(git_root);\n                }\n                let bacon_locations = match Bacon::find_bacon_locations(&folder_path, locations_file_name).await {\n                    Ok(v) => v,\n                    Err(e) => {\n                        tracing::warn!(\"unable to find valid bacon location files: {e}\");\n                        Vec::new()\n                    }\n                };\n                for bacon_location in bacon_locations.iter() {\n                    tracing::info!(\"found bacon locations file to parse {}\", bacon_location.display());\n                    match File::open(&bacon_location).await {\n                        Ok(fd) => {\n                            let reader = BufReader::new(fd);\n                            let mut lines = reader.lines();\n                            let mut buffer = String::new();\n\n                            while let Some(line) = lines.next_line().await.unwrap_or_else(|e| {\n                                tracing::error!(\"error reading line from file {}: {e}\", bacon_location.display());\n                                None\n                            }) {\n                                let trimmed = line.trim_end();\n\n                                // Use the first word to determine the start of a new diagnostic\n                                let is_new_diagnostic = trimmed.starts_with(\"warning\")\n                                    || trimmed.starts_with(\"error\")\n                                    || trimmed.starts_with(\"info\")\n                                    || trimmed.starts_with(\"note\")\n                                    || trimmed.starts_with(\"failure-note\")\n                                    || trimmed.starts_with(\"help\");\n\n                                if is_new_diagnostic {\n                                    // Process the collected buffer before starting a new entry\n                                    if !buffer.is_empty()\n                                        && let Some((path, diagnostic)) =\n                                            Self::parse_bacon_diagnostic_line(&buffer, &folder_path)\n                                    {\n                                        tracing::debug!(\"found diagnostic for {}\", path.as_str());\n                                        Self::deduplicate_diagnostics(\n                                            path.clone(),\n                                            uri,\n                                            diagnostic,\n                                            &mut diagnostics,\n                                            &mut seen,\n                                        );\n                                    }\n                                    // Reset buffer for new diagnostic entry\n                                    buffer.clear();\n                                }\n\n                                // Append current line to buffer\n                                if !buffer.is_empty() {\n                                    buffer.push('\\n'); // Preserve multiline structure\n                                }\n                                buffer.push_str(trimmed);\n                            }\n\n                            // Flush the remaining buffer after loop ends\n                            if !buffer.is_empty()\n                                && let Some((path, diagnostic)) =\n                                    Self::parse_bacon_diagnostic_line(&buffer, &folder_path)\n                            {\n                                Self::deduplicate_diagnostics(\n                                    path.clone(),\n                                    uri,\n                                    diagnostic,\n                                    &mut diagnostics,\n                                    &mut seen,\n                                );\n                            }\n                        }\n                        Err(e) => {\n                            tracing::error!(\"unable to read file {}: {e}\", bacon_location.display())\n                        }\n                    }\n                }\n            }\n        }\n        diagnostics\n    }\n\n    async fn diagnostics_vec(\n        uri: &Uri,\n        locations_file_name: &str,\n        workspace_folders: Option<&[WorkspaceFolder]>,\n    ) -> Vec<Diagnostic> {\n        Self::diagnostics(uri, locations_file_name, workspace_folders)\n            .await\n            .into_iter()\n            .map(|(_, y)| y)\n            .collect::<Vec<Diagnostic>>()\n    }\n\n    pub(crate) async fn synchronize_diagnostics(state: Arc<RwLock<State>>, client: Arc<Client>) {\n        tracing::info!(\"starting background task in charge of syncronizing diagnostics for all open files\");\n        let (tx, rx) = flume::unbounded::<DebounceEventResult>();\n\n        let (locations_file, proj_root, wait_time, shutdown_token) = {\n            let state = state.read().await;\n            let Some(BackendRuntime::Bacon { config, runtime }) = &state.backend else {\n                tracing::error!(\"synchronize_diagnostics called without bacon backend\");\n                return;\n            };\n            (\n                config.locations_file.clone(),\n                state.project_root.clone(),\n                config.synchronize_all_open_files_wait,\n                runtime.shutdown_token.clone(),\n            )\n        };\n\n        let mut watcher = match new_debouncer(wait_time, None, move |ev: DebounceEventResult| {\n            // Returns an error if all senders are dropped.\n            let _res = tx.send(ev);\n        }) {\n            Ok(watcher) => watcher,\n            Err(e) => {\n                let msg = format!(\n                    \"bacon-ls could not create a file watcher: {e}. \\\n                     Diagnostics will still update on save but open-file \\\n                     synchronization is disabled.\"\n                );\n                tracing::error!(\"{msg}\");\n                client.show_message(ls_types::MessageType::WARNING, msg).await;\n                return;\n            }\n        };\n\n        let locations_file_path =\n            proj_root.map_or_else(|| PathBuf::from(&locations_file), |root| root.join(&locations_file));\n        loop {\n            match watcher.watch(PathBuf::from(&locations_file_path), notify::RecursiveMode::Recursive) {\n                Ok(_) => {\n                    tracing::info!(\"watching '{}' for changes...\", locations_file_path.display());\n                    break;\n                }\n                Err(e) => {\n                    tracing::warn!(\n                        \"unable to watch '{}', retrying in 1 second\",\n                        locations_file_path.display()\n                    );\n                    tracing::error!(\".bacon_locations watcher error: {e}\");\n                    tokio::time::sleep(Duration::from_secs(1)).await;\n                }\n            }\n        }\n\n        while let Some(Ok(res)) = tokio::select! {\n            ev = rx.recv_async() => {\n                Some(ev)\n            }\n            _ = shutdown_token.cancelled() => {\n                None\n            }\n        } {\n            let events = match res {\n                Ok(events) => events,\n                Err(err) => {\n                    tracing::error!(?err, \"watch error\");\n                    continue;\n                }\n            };\n            // Only publish if the file was modified.\n            if !events.iter().any(|ev| ev.kind.is_modify()) {\n                continue;\n            }\n\n            let mut loop_state = state.write().await;\n            let Some(BackendRuntime::Bacon { runtime, .. }) = &mut loop_state.backend else {\n                tracing::error!(\"backend changed during sync loop\");\n                return;\n            };\n            runtime.diagnostics_version = runtime.diagnostics_version.wrapping_add(1);\n            let version = runtime.diagnostics_version;\n            let open_files = runtime.open_files.clone();\n            let workspace_folders = loop_state.workspace_folders.clone();\n            drop(loop_state);\n            tracing::debug!(\n                \"running periodic diagnostic publish for open files `{}`\",\n                open_files.iter().map(|f| f.to_string()).collect::<Vec<_>>().join(\",\")\n            );\n            for uri in open_files.iter() {\n                Self::publish_diagnostics(&client, uri, &locations_file, workspace_folders.as_deref(), version).await;\n            }\n        }\n    }\n\n    pub(crate) async fn publish_diagnostics(\n        client: &Arc<Client>,\n        uri: &Uri,\n        locations_file_name: &str,\n        workspace_folders: Option<&[WorkspaceFolder]>,\n        version: i32,\n    ) {\n        let diagnostics_vec = Self::diagnostics_vec(uri, locations_file_name, workspace_folders).await;\n        tracing::info!(\"sent {} bacon diagnostics for {uri:?}\", diagnostics_vec.len());\n        client\n            .publish_diagnostics(uri.clone(), diagnostics_vec, Some(version))\n            .await;\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use std::io::Write;\n\n    use super::*;\n    use pretty_assertions::assert_eq;\n    use tempfile::TempDir;\n\n    #[tokio::test]\n    async fn test_valid_bacon_preferences() {\n        let valid_toml = format!(\n            r#\"\n            [jobs.bacon-ls]\n            analyzer = \"{BACON_ANALYZER}\"\n            need_stdout = true\n\n            [exports.cargo-json-spans]\n            auto = true\n            exporter = \"{BACON_EXPORTER}\"\n            line_format = \"{LINE_FORMAT}\"\n            path = \"{LOCATIONS_FILE}\"\n        \"#\n        );\n        let tmp_dir = TempDir::new().unwrap();\n        let file_path = tmp_dir.path().join(\"prefs.toml\");\n        let mut file = std::fs::File::create(&file_path).unwrap();\n        write!(file, \"{}\", valid_toml).unwrap();\n        assert!(Bacon::validate_preferences_file(&file_path).await.is_ok());\n    }\n\n    #[tokio::test]\n    async fn test_invalid_analyzer() {\n        let invalid_toml = format!(\n            r#\"\n            [jobs.bacon-ls]\n            analyzer = \"incorrect_analyzer\"\n            need_stdout = true\n\n            [exports.cargo-json-spans]\n            auto = true\n            exporter = \"{BACON_EXPORTER}\"\n            line_format = \"{LINE_FORMAT}\"\n            path = \"{LOCATIONS_FILE}\"\n        \"#\n        );\n\n        let tmp_dir = TempDir::new().unwrap();\n        let file_path = tmp_dir.path().join(\"prefs.toml\");\n        let mut file = std::fs::File::create(&file_path).unwrap();\n        write!(file, \"{}\", invalid_toml).unwrap();\n        assert!(Bacon::validate_preferences_file(&file_path).await.is_err());\n    }\n\n    #[tokio::test]\n    async fn test_invalid_line_format() {\n        let invalid_toml = format!(\n            r#\"\n            [jobs.bacon-ls]\n            analyzer = \"{BACON_ANALYZER}\"\n            need_stdout = true\n\n            [exports.cargo-json-spans]\n            auto = true\n            exporter = \"{BACON_EXPORTER}\"\n            line_format = \"invalid_line_format\"\n            path = \"{LOCATIONS_FILE}\"\n        \"#\n        );\n\n        let tmp_dir = TempDir::new().unwrap();\n        let file_path = tmp_dir.path().join(\"prefs.toml\");\n        let mut file = std::fs::File::create(&file_path).unwrap();\n        write!(file, \"{}\", invalid_toml).unwrap();\n        assert!(Bacon::validate_preferences_file(&file_path).await.is_err());\n    }\n\n    #[tokio::test]\n    async fn test_validate_preferences() {\n        let valid_toml = format!(\n            r#\"\n            [jobs.bacon-ls]\n            analyzer = \"{BACON_ANALYZER}\"\n            need_stdout = true\n\n            [exports.cargo-json-spans]\n            auto = true\n            exporter = \"{BACON_EXPORTER}\"\n            line_format = \"{LINE_FORMAT}\"\n            path = \"{LOCATIONS_FILE}\"\n        \"#\n        );\n        assert!(\n            Bacon::validate_preferences_impl(valid_toml.as_bytes(), false)\n                .await\n                .is_ok()\n        );\n    }\n\n    #[tokio::test]\n    async fn test_file_creation_failure() {\n        let invalid_path = \"/invalid/path/to/file.toml\";\n        let result = Bacon::create_preferences_file(invalid_path).await;\n        assert!(result.is_err());\n        assert!(result.unwrap_err().contains(\"error creating bacon preferences\"));\n    }\n\n    #[tokio::test]\n    async fn test_file_write_failure() {\n        let tmp_dir = TempDir::new().unwrap();\n        let file_path = tmp_dir.path().join(\"prefs.toml\");\n        // Simulate write failure by closing the file prematurely\n        let file = File::create(&file_path).await.unwrap();\n        drop(file); // Close the file to simulate failure\n        let result = Bacon::create_preferences_file(file_path.to_str().unwrap()).await;\n        assert!(result.is_ok());\n    }\n\n    #[tokio::test]\n    async fn test_empty_bacon_preferences_file() {\n        let tmp_dir = TempDir::new().unwrap();\n        let file_path = tmp_dir.path().join(\"empty_prefs.toml\");\n        std::fs::File::create(&file_path).unwrap();\n        assert!(Bacon::validate_preferences_file(&file_path).await.is_err());\n    }\n\n    #[tokio::test]\n    async fn test_run_in_background() {\n        let cancel_token = CancellationToken::new();\n        let handle = Bacon::run_in_background(\"cargo\", \"--version\", None, cancel_token.clone()).await;\n        assert!(handle.is_ok());\n        cancel_token.cancel();\n        handle.unwrap().await.unwrap();\n    }\n\n    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\";\n\n    #[test]\n    fn test_parse_bacon_diagnostic_line_with_spans_ok() {\n        let result = Bacon::parse_bacon_diagnostic_line(ERROR_LINE, Path::new(\"/app/github/bacon-ls\"));\n        let (url, diagnostic) = result.unwrap();\n        assert_eq!(url.to_string(), \"file:///app/github/bacon-ls/src/lib.rs\");\n        assert_eq!(diagnostic.severity, Some(DiagnosticSeverity::ERROR));\n        assert_eq!(diagnostic.source, Some(PKG_NAME.to_string()));\n        assert_eq!(\n            diagnostic.message,\n            r#\"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\"#\n        );\n        let result = Bacon::parse_bacon_diagnostic_line(ERROR_LINE, Path::new(\"/app/github/bacon-ls\"));\n        let (url, diagnostic) = result.unwrap();\n        assert_eq!(url.to_string(), \"file:///app/github/bacon-ls/src/lib.rs\");\n        assert_eq!(diagnostic.severity, Some(DiagnosticSeverity::ERROR));\n        assert_eq!(diagnostic.source, Some(PKG_NAME.to_string()));\n    }\n\n    #[test]\n    fn test_parse_bacon_diagnostic_line_with_spans_ko() {\n        // Unparsable line\n        let result = Bacon::parse_bacon_diagnostic_line(\"warning:/file:1:1\", Path::new(\"/app/github/bacon-ls\"));\n        assert_eq!(result, None);\n\n        // Empty line\n        let result = Bacon::parse_bacon_diagnostic_line(\"\", Path::new(\"/app/github/bacon-ls\"));\n        assert_eq!(result, None);\n    }\n\n    #[tokio::test]\n    #[cfg(not(target_os = \"windows\"))]\n    async fn test_bacon_multiline_diagnostics_production() {\n        let tmp_dir = TempDir::new().unwrap();\n        let file_path = tmp_dir.path().join(\".bacon-locations\");\n        let mut tmp_file = std::fs::File::create(file_path).unwrap();\n        let error_path = format!(\"{}/src/lib.rs\", tmp_dir.path().display());\n        let error_path_url = str::parse::<Uri>(&format!(\"file://{error_path}\")).unwrap();\n        writeln!(\n            tmp_file,\n            \"warning|:|src/lib.rs|:|130|:|142|:|33|:|34|:|this if statement can be collapsed|:|none|:|none\"\n        )\n        .unwrap();\n        writeln!(\n            tmp_file,\n            r#\"help|:|{error_path}|:|130|:|142|:|33|:|34|:|collapse nested if block|:|none|:|if Some(&the_path) == uri && !diagnostics.iter().any(\n                                        |(existing_path, existing_diagnostic)| {{\n                                            existing_path.path() == the_path.path()\n                                                && diagnostic.range == existing_diagnostic.range\n                                                && diagnostic.severity\n                                                    == existing_diagnostic.severity\n                                                && diagnostic.message == existing_diagnostic.message\n                                        }},\n                                    ) {{\n                                    diagnostics.push((path, diagnostic));\n                                }}\"#\n        ).unwrap();\n        writeln!(\n            tmp_file,\n            \"warning|:|{error_path}|:|150|:|162|:|33|:|34|:|this if statement can be collapsed again|:|none|:|none\"\n        )\n        .unwrap();\n        writeln!(\n            tmp_file,\n            r#\"warning|:|{error_path}|:|150|:|162|:|33|:|34|:|collapse nested if block|:|if Some(&other_path) == uri && !diagnostics.iter().any(\n                                        |(existing_path, existing_diagnostic)| {{\n                                            existing_path.path() == other_path.path()\n                                                && diagnostic.range == existing_diagnostic.range\n                                                && diagnostic.severity\n                                                    == existing_diagnostic.severity\n                                                && diagnostic.message == existing_diagnostic.message\n                                        }},\n                                    ) {{\n                                    diagnostics.push((path, diagnostic));\n                                }}|:|none\"#\n        ).unwrap();\n\n        let workspace_folders = Some(vec![WorkspaceFolder {\n            name: tmp_dir.path().display().to_string(),\n            uri: str::parse::<Uri>(&format!(\"file://{}\", tmp_dir.path().display())).unwrap(),\n        }]);\n        let diagnostics = Bacon::diagnostics(&error_path_url, LOCATIONS_FILE, workspace_folders.as_deref()).await;\n        assert_eq!(diagnostics.len(), 4);\n        assert!(diagnostics[0].1.data.is_none());\n        assert_eq!(diagnostics[0].1.message.len(), 34);\n        assert!(diagnostics[1].1.data.is_some());\n        assert_eq!(diagnostics[1].1.message.len(), 24);\n        assert!(diagnostics[2].1.data.is_none());\n        assert_eq!(diagnostics[2].1.message.len(), 40);\n        assert!(diagnostics[3].1.data.is_none());\n        assert_eq!(diagnostics[3].1.message.len(), 766);\n    }\n\n    #[tokio::test]\n    #[cfg(not(target_os = \"windows\"))]\n    async fn test_bacon_diagnostics_production_and_deduplication() {\n        let tmp_dir = TempDir::new().unwrap();\n        let file_path = tmp_dir.path().join(\".bacon-locations\");\n        let mut tmp_file = std::fs::File::create(file_path).unwrap();\n        let error_path = format!(\"{}/src/lib.rs\", tmp_dir.path().display());\n        let error_path_url = str::parse::<Uri>(&format!(\"file://{error_path}\")).unwrap();\n        writeln!(\n            tmp_file,\n            \"error|:|{error_path}|:|352|:|352|:|9|:|20|:|cannot find value `one` in this scope|:|none|:|none\"\n        )\n        .unwrap();\n        // duplicate the line\n        writeln!(\n            tmp_file,\n            \"error|:|{error_path}|:|352|:|352|:|9|:|20|:|cannot find value `one` in this scope|:|none|:|none\"\n        )\n        .unwrap();\n        writeln!(\n            tmp_file,\n            \"warning|:|{error_path}|:|354|:|354|:|9|:|20|:|cannot find value `two` in this scope|:|some|:|none\"\n        )\n        .unwrap();\n        writeln!(\n            tmp_file,\n            \"help|:|{error_path}|:|356|:|356|:|9|:|20|:|cannot find value `three` in this scope|:|none|:|some other\"\n        )\n        .unwrap();\n\n        let workspace_folders = Some(vec![WorkspaceFolder {\n            name: tmp_dir.path().display().to_string(),\n            uri: str::parse::<Uri>(&format!(\"file://{}\", tmp_dir.path().display())).unwrap(),\n        }]);\n        let diagnostics = Bacon::diagnostics(&error_path_url, LOCATIONS_FILE, workspace_folders.as_deref()).await;\n        assert_eq!(diagnostics.len(), 3);\n        let diagnostics_vec =\n            Bacon::diagnostics_vec(&error_path_url, LOCATIONS_FILE, workspace_folders.as_deref()).await;\n        assert_eq!(diagnostics_vec.len(), 3);\n    }\n\n    #[test]\n    fn test_parse_severity_known_levels() {\n        assert_eq!(Bacon::parse_severity(\"error\"), DiagnosticSeverity::ERROR);\n        assert_eq!(Bacon::parse_severity(\"warning\"), DiagnosticSeverity::WARNING);\n        assert_eq!(Bacon::parse_severity(\"note\"), DiagnosticSeverity::INFORMATION);\n        assert_eq!(Bacon::parse_severity(\"info\"), DiagnosticSeverity::INFORMATION);\n        assert_eq!(Bacon::parse_severity(\"information\"), DiagnosticSeverity::INFORMATION);\n        assert_eq!(Bacon::parse_severity(\"failure-note\"), DiagnosticSeverity::INFORMATION);\n        assert_eq!(Bacon::parse_severity(\"help\"), DiagnosticSeverity::HINT);\n        assert_eq!(Bacon::parse_severity(\"hint\"), DiagnosticSeverity::HINT);\n    }\n\n    #[test]\n    fn test_parse_severity_unknown_level_defaults_to_information() {\n        assert_eq!(\n            Bacon::parse_severity(\"future-rustc-level\"),\n            DiagnosticSeverity::INFORMATION\n        );\n    }\n\n    #[test]\n    fn test_parse_positions_valid() {\n        let parts = [\"1\", \"2\", \"3\", \"4\"];\n        assert_eq!(Bacon::parse_positions(&parts), Some((1, 2, 3, 4)));\n    }\n\n    #[test]\n    fn test_parse_positions_non_numeric_returns_none() {\n        let parts = [\"1\", \"x\", \"3\", \"4\"];\n        assert_eq!(Bacon::parse_positions(&parts), None);\n    }\n\n    #[test]\n    fn test_parse_positions_too_few_fields_returns_none() {\n        let parts = [\"1\", \"2\", \"3\"];\n        assert_eq!(Bacon::parse_positions(&parts), None);\n    }\n\n    #[test]\n    #[cfg(not(target_os = \"windows\"))]\n    fn test_parse_bacon_diagnostic_line_with_replacement_attaches_correction() {\n        // Skipped on Windows: this test asserts the produced URI as a unix-style\n        // string. On Windows `Path::new(\"/proj\").join(\"src/lib.rs\")` produces\n        // backslashes which percent-encode to `%5C` in the URI.\n        let line = \"warning|:|src/lib.rs|:|10|:|10|:|5|:|8|:|unused import|:|none|:|use foo::bar;\";\n        let (uri, diag) = Bacon::parse_bacon_diagnostic_line(line, Path::new(\"/proj\")).expect(\"must parse\");\n        assert_eq!(uri.to_string(), \"file:///proj/src/lib.rs\");\n        assert!(\n            diag.data.is_some(),\n            \"non-`none` replacement should attach correction data\"\n        );\n        // Position is converted from 1-based to 0-based.\n        assert_eq!(diag.range.start.line, 9);\n        assert_eq!(diag.range.start.character, 4);\n    }\n\n    #[test]\n    fn test_parse_bacon_diagnostic_line_zero_position_saturates() {\n        // Defensive: 0 in any position field should saturate to 0 rather than\n        // panic on `0 - 1`.\n        let line = \"error|:|src/lib.rs|:|0|:|0|:|0|:|0|:|boom|:|none|:|none\";\n        let (_, diag) = Bacon::parse_bacon_diagnostic_line(line, Path::new(\"/p\")).unwrap();\n        assert_eq!(diag.range.start.line, 0);\n        assert_eq!(diag.range.start.character, 0);\n        assert_eq!(diag.range.end.line, 0);\n        assert_eq!(diag.range.end.character, 0);\n    }\n\n    #[test]\n    fn test_parse_bacon_diagnostic_line_strips_ansi_from_rendered() {\n        let line = \"error|:|src/lib.rs|:|1|:|1|:|1|:|1|:|raw|:|\\u{1b}[31mbright red\\u{1b}[0m|:|none\";\n        let (_, diag) = Bacon::parse_bacon_diagnostic_line(line, Path::new(\"/p\")).unwrap();\n        // When the rendered slot is non-`none`, it replaces the message and\n        // ANSI codes are stripped.\n        assert_eq!(diag.message, \"bright red\");\n    }\n\n    #[test]\n    fn test_parse_bacon_diagnostic_line_too_few_fields_returns_none() {\n        let line = \"error|:|/file|:|1|:|2|:|3|:|4|:|message\";\n        assert_eq!(Bacon::parse_bacon_diagnostic_line(line, Path::new(\"/p\")), None);\n    }\n\n    #[test]\n    fn test_deduplicate_diagnostics_skips_when_path_does_not_match() {\n        let other_uri = str::parse::<Uri>(\"file:///other.rs\").unwrap();\n        let target_uri = str::parse::<Uri>(\"file:///target.rs\").unwrap();\n        let mut diagnostics = Vec::new();\n        let mut seen = HashSet::new();\n        let diag = Diagnostic {\n            range: Range::default(),\n            severity: Some(DiagnosticSeverity::ERROR),\n            message: \"msg\".into(),\n            ..Diagnostic::default()\n        };\n        Bacon::deduplicate_diagnostics(other_uri, &target_uri, diag, &mut diagnostics, &mut seen);\n        assert!(diagnostics.is_empty(), \"diagnostic for a different URI must be dropped\");\n        assert!(seen.is_empty(), \"seen set must not record skipped entries\");\n    }\n\n    #[test]\n    fn test_deduplicate_diagnostics_drops_exact_duplicate() {\n        let uri = str::parse::<Uri>(\"file:///x.rs\").unwrap();\n        let mut diagnostics = Vec::new();\n        let mut seen = HashSet::new();\n        let make = || Diagnostic {\n            range: Range::default(),\n            severity: Some(DiagnosticSeverity::ERROR),\n            message: \"same\".into(),\n            ..Diagnostic::default()\n        };\n        Bacon::deduplicate_diagnostics(uri.clone(), &uri, make(), &mut diagnostics, &mut seen);\n        Bacon::deduplicate_diagnostics(uri.clone(), &uri, make(), &mut diagnostics, &mut seen);\n        assert_eq!(diagnostics.len(), 1, \"duplicate should not be re-added\");\n    }\n\n    #[test]\n    fn test_deduplicate_diagnostics_keeps_distinct_severity() {\n        let uri = str::parse::<Uri>(\"file:///x.rs\").unwrap();\n        let mut diagnostics = Vec::new();\n        let mut seen = HashSet::new();\n        let mut diag = Diagnostic {\n            range: Range::default(),\n            severity: Some(DiagnosticSeverity::ERROR),\n            message: \"m\".into(),\n            ..Diagnostic::default()\n        };\n        Bacon::deduplicate_diagnostics(uri.clone(), &uri, diag.clone(), &mut diagnostics, &mut seen);\n        diag.severity = Some(DiagnosticSeverity::WARNING);\n        Bacon::deduplicate_diagnostics(uri.clone(), &uri, diag, &mut diagnostics, &mut seen);\n        assert_eq!(diagnostics.len(), 2, \"different severity ⇒ different diagnostic\");\n    }\n\n    #[tokio::test]\n    async fn test_find_bacon_locations_finds_nested_files() {\n        let tmp = TempDir::new().unwrap();\n        // Create root/.bacon-locations and root/sub/sub2/.bacon-locations,\n        // plus an unrelated file that must not match.\n        let root = tmp.path();\n        std::fs::write(root.join(\".bacon-locations\"), \"\").unwrap();\n        let nested = root.join(\"sub\").join(\"sub2\");\n        std::fs::create_dir_all(&nested).unwrap();\n        std::fs::write(nested.join(\".bacon-locations\"), \"\").unwrap();\n        std::fs::write(root.join(\"sub\").join(\"other.txt\"), \"\").unwrap();\n\n        let mut found = Bacon::find_bacon_locations(root, \".bacon-locations\").await.unwrap();\n        found.sort();\n        assert_eq!(found.len(), 2, \"should find both .bacon-locations files\");\n        assert!(found.iter().all(|p| p.file_name().unwrap() == \".bacon-locations\"));\n    }\n\n    #[tokio::test]\n    async fn test_find_bacon_locations_empty_dir_returns_empty() {\n        let tmp = TempDir::new().unwrap();\n        let found = Bacon::find_bacon_locations(tmp.path(), \".bacon-locations\")\n            .await\n            .unwrap();\n        assert!(found.is_empty());\n    }\n\n    #[tokio::test]\n    async fn test_find_bacon_locations_missing_root_errors() {\n        let tmp = TempDir::new().unwrap();\n        let missing = tmp.path().join(\"does-not-exist\");\n        let result = Bacon::find_bacon_locations(&missing, \".bacon-locations\").await;\n        assert!(result.is_err(), \"missing root must surface an io error\");\n    }\n\n    #[tokio::test]\n    async fn test_validate_preferences_impl_creates_when_missing_and_requested() {\n        let tmp = TempDir::new().unwrap();\n        let target = tmp.path().join(\"nested-prefs.toml\");\n        // bacon prefs lookup returns one path, file does not exist, create_prefs_file=true.\n        let prefs_list = target.to_str().unwrap();\n        Bacon::validate_preferences_impl(prefs_list.as_bytes(), true)\n            .await\n            .expect(\"should create prefs file when missing\");\n        assert!(target.exists(), \"prefs file should be created\");\n        // And the freshly-created file must validate cleanly.\n        Bacon::validate_preferences_file(&target)\n            .await\n            .expect(\"freshly created prefs file should validate\");\n    }\n\n    #[tokio::test]\n    async fn test_validate_preferences_impl_no_create_when_disabled() {\n        let tmp = TempDir::new().unwrap();\n        let target = tmp.path().join(\"absent-prefs.toml\");\n        let prefs_list = target.to_str().unwrap();\n        Bacon::validate_preferences_impl(prefs_list.as_bytes(), false)\n            .await\n            .expect(\"missing prefs with create=false is not an error\");\n        assert!(!target.exists());\n    }\n}\n"
  },
  {
    "path": "src/lib.rs",
    "content": "//! Bacon Language Server\nuse std::collections::{HashMap, HashSet};\nuse std::env;\nuse std::path::{Path, PathBuf};\nuse std::sync::Arc;\nuse std::time::{Duration, Instant};\n\nuse argh::FromArgs;\nuse bacon::Bacon;\nuse flume::RecvError;\nuse ls_types::{Diagnostic, DiagnosticSeverity, MessageType, ProgressToken, Range, Uri, WorkspaceFolder};\nuse native::Cargo;\nuse percent_encoding::{AsciiSet, CONTROLS, utf8_percent_encode};\nuse serde_json::{Map, Value};\nuse shadow::ShadowWorkspace;\nuse tokio::sync::{RwLock, RwLockWriteGuard};\nuse tokio::task::JoinHandle;\nuse tokio_util::sync::CancellationToken;\nuse tower_lsp_server::{Client, LspService, Server, jsonrpc};\nuse tracing_subscriber::fmt::format::FmtSpan;\n\nmod bacon;\nmod lsp;\nmod native;\nmod shadow;\n\nconst PKG_NAME: &str = env!(\"CARGO_PKG_NAME\");\npub const PKG_VERSION: &str = env!(\"CARGO_PKG_VERSION\");\nconst LOCATIONS_FILE: &str = \".bacon-locations\";\nconst BACON_BACKGROUND_COMMAND: &str = \"bacon\";\nconst BACON_BACKGROUND_COMMAND_ARGS: &str = \"--headless -j bacon-ls\";\n\n// Characters that must be percent-encoded when putting an OS path into a\n// `file://` URI. We keep `/` unencoded so it continues to split the path into\n// segments (clients expect multi-segment URIs). This covers the reserved URI\n// characters plus a few that break `Uri` parsing in practice (space, `#`,\n// `?`, `%`, `[`/`]`, backslash, etc.).\nconst PATH_ENCODE_SET: &AsciiSet = &CONTROLS\n    .add(b' ')\n    .add(b'\"')\n    .add(b'#')\n    .add(b'<')\n    .add(b'>')\n    .add(b'?')\n    .add(b'[')\n    .add(b'\\\\')\n    .add(b']')\n    .add(b'^')\n    .add(b'`')\n    .add(b'{')\n    .add(b'|')\n    .add(b'}')\n    .add(b'%');\n\n/// Build a `file://...` URI string from an OS path. Percent-encodes any\n/// characters that would otherwise break URI parsing (spaces, `#`, `?`, `%`,\n/// etc.), while leaving `/` intact so path segments survive.\npub(crate) fn path_to_file_uri(path: &str) -> String {\n    format!(\"file://{}\", utf8_percent_encode(path, PATH_ENCODE_SET))\n}\n\n/// Hash key for deduplicating diagnostics that share the same range, severity,\n/// and message. `DiagnosticSeverity` is `Eq` but not `Hash` in `ls-types`, so we\n/// project it down to a small integer tag.\npub(crate) type DiagKey = (Range, i32, String);\n\npub(crate) fn diag_key(d: &Diagnostic) -> DiagKey {\n    (d.range, severity_tag(d.severity), d.message.clone())\n}\n\nfn severity_tag(s: Option<DiagnosticSeverity>) -> i32 {\n    match s {\n        None => 0,\n        Some(s) if s == DiagnosticSeverity::ERROR => 1,\n        Some(s) if s == DiagnosticSeverity::WARNING => 2,\n        Some(s) if s == DiagnosticSeverity::INFORMATION => 3,\n        Some(s) if s == DiagnosticSeverity::HINT => 4,\n        Some(_) => -1,\n    }\n}\n\n/// bacon-ls - https://github.com/crisidev/bacon-ls\n#[derive(Debug, FromArgs)]\npub struct Args {\n    /// display version information\n    #[argh(switch, short = 'v')]\n    pub version: bool,\n}\n\n#[derive(Debug, Clone, Copy, PartialEq, Eq)]\nenum BackendChoice {\n    Cargo,\n    Bacon,\n}\n\n#[derive(Debug)]\nenum BackendRuntime {\n    Bacon {\n        config: BaconOptions,\n        runtime: BaconRuntime,\n    },\n    Cargo {\n        config: CargoOptions,\n        runtime: CargoRuntime,\n    },\n}\n\nimpl BackendRuntime {\n    fn backend_choice(&self) -> BackendChoice {\n        match self {\n            Self::Bacon { .. } => BackendChoice::Bacon,\n            Self::Cargo { .. } => BackendChoice::Cargo,\n        }\n    }\n}\n\n#[derive(Debug, Clone, Copy, PartialEq, Eq)]\npub(crate) enum CargoRunState {\n    Idle,\n    Running,\n    RunningPending,\n}\n\n#[derive(Debug, Copy, Clone)]\npub(crate) enum PublishMode {\n    CancelRunning,\n    QueueIfRunning,\n}\n\nfn invalid_option(name: &str) -> jsonrpc::Error {\n    jsonrpc::Error {\n        code: jsonrpc::ErrorCode::InvalidParams,\n        message: format!(\"Invalid value for option \\\"{name}\\\"\").into(),\n        data: None,\n    }\n}\n\n#[derive(Debug, Clone, PartialEq, Eq)]\nenum CargoFeatures {\n    /// use `--all-features`\n    All,\n    /// pass the feature list as `--features=...`\n    List(Vec<String>),\n}\n\nimpl Default for CargoFeatures {\n    fn default() -> Self {\n        Self::List(vec![])\n    }\n}\n\nimpl CargoFeatures {\n    fn from_json_value(value: &Value) -> jsonrpc::Result<Self> {\n        match value {\n            Value::Null => Ok(Self::List(vec![])),\n            Value::String(str) if str == \"all\" => Ok(Self::All),\n            Value::Array(values) => {\n                let features = values\n                    .iter()\n                    .map(|item| {\n                        item.as_str()\n                            .map(|s| s.to_string())\n                            .ok_or(jsonrpc::Error::new(jsonrpc::ErrorCode::InvalidParams))\n                    })\n                    .collect::<jsonrpc::Result<Vec<_>>>()?;\n\n                Ok(Self::List(features))\n            }\n            _ => Err(jsonrpc::Error {\n                code: jsonrpc::ErrorCode::InvalidParams,\n                message: \"features must be a list of strings or the string \\\"all\\\"\".into(),\n                data: None,\n            }),\n        }\n    }\n}\n\n#[derive(Debug)]\npub(crate) struct CargoOptions {\n    // \"check\" or \"clippy\"\n    pub(crate) command: String,\n    pub(crate) features: CargoFeatures,\n    // `-p crate_name`\n    pub(crate) package: Option<String>,\n    pub(crate) all_targets: bool,\n    pub(crate) no_default_features: bool,\n    // Extra arguments which do not have a nice wrapper\n    pub(crate) extra_command_args: Vec<String>,\n    pub(crate) env: Vec<(String, String)>,\n    pub(crate) publish_mode: PublishMode,\n    // Interval at which we refresh (send) cargo diagnostics we have so far\n    // None means wait until the cargo command is fully done\n    pub(crate) refresh_interval_seconds: Option<Duration>,\n    /// User override: when `Some(true)`, always emit children as separate\n    /// diagnostics instead of related information, regardless of client\n    /// capability. When `None`, follow the client advertisement.\n    pub(crate) separate_child_diagnostics: Option<bool>,\n    pub(crate) check_on_save: bool,\n    pub(crate) clear_diagnostics_on_check: bool,\n    /// Live-as-you-type diagnostics. When true, the server mirrors the\n    /// workspace into a hardlinked shadow under\n    /// `target/bacon-ls-live/shadow/`, replaces dirty buffers in the shadow\n    /// on `did_change`, and runs cargo against the shadow with a separate\n    /// target dir. Off by default.\n    pub(crate) update_on_insert: bool,\n    /// Quiet period after the most recent `did_change` before the live\n    /// cargo run is triggered. Coalesces bursts of keystrokes into a single\n    /// run.\n    pub(crate) update_on_insert_debounce: Duration,\n}\n\nimpl CargoOptions {\n    pub(crate) fn build_command_args(&self) -> Vec<String> {\n        let mut args = vec![self.command.clone()];\n        args.push(\"--message-format=json-diagnostic-rendered-ansi\".to_string());\n\n        match &self.features {\n            CargoFeatures::All => {\n                args.push(\"--all-features\".to_string());\n            }\n            CargoFeatures::List(features) if !features.is_empty() => {\n                args.push(\"--features\".to_string());\n                let mut features_list = String::new();\n                for feature in features[..features.len() - 1].iter() {\n                    features_list += feature;\n                    features_list += \",\";\n                }\n                features_list += &features[features.len() - 1];\n                args.push(features_list);\n            }\n            _ => {}\n        }\n\n        if let Some(pkg) = self.package.clone() {\n            args.push(\"-p\".to_string());\n            args.push(pkg);\n        }\n\n        if self.all_targets {\n            args.push(\"--all-targets\".to_string());\n        }\n\n        if self.no_default_features {\n            args.push(\"--no-default-features\".to_string());\n        }\n\n        for arg in self.extra_command_args.iter().cloned() {\n            args.push(arg);\n        }\n\n        args\n    }\n\n    pub(crate) fn update_from_json_obj(&mut self, cargo_obj: &Map<String, Value>) -> jsonrpc::Result<()> {\n        if let Some(value) = cargo_obj.get(\"command\") {\n            self.command = value.as_str().ok_or_else(|| invalid_option(\"command\"))?.to_string();\n        }\n\n        if let Some(value) = cargo_obj.get(\"features\") {\n            self.features = CargoFeatures::from_json_value(value)?;\n        }\n\n        if let Some(value) = cargo_obj.get(\"package\") {\n            self.package = Some(value.as_str().ok_or_else(|| invalid_option(\"package\"))?.to_string());\n        }\n\n        if let Some(value) = cargo_obj.get(\"allTargets\") {\n            self.all_targets = value.as_bool().ok_or_else(|| invalid_option(\"allTargets\"))?;\n        }\n\n        if let Some(value) = cargo_obj.get(\"noDefaultFeatures\") {\n            self.no_default_features = value.as_bool().ok_or_else(|| invalid_option(\"noDefaultFeatures\"))?;\n        }\n\n        if let Some(value) = cargo_obj.get(\"extraArgs\") {\n            self.extra_command_args = value\n                .as_array()\n                .ok_or_else(|| invalid_option(\"extraArgs\"))?\n                .iter()\n                .map(|item| {\n                    item.as_str()\n                        .map(|s| s.to_string())\n                        .ok_or_else(|| invalid_option(\"extraArgs\"))\n                })\n                .collect::<jsonrpc::Result<Vec<_>>>()?;\n        }\n\n        if let Some(value) = cargo_obj.get(\"env\") {\n            self.env = value\n                .as_object()\n                .ok_or_else(|| invalid_option(\"env\"))?\n                .iter()\n                .map(|(k, v)| {\n                    let val = v.as_str().ok_or_else(|| invalid_option(\"env\"))?;\n                    Ok((k.clone(), val.to_string()))\n                })\n                .collect::<jsonrpc::Result<Vec<_>>>()?;\n        }\n\n        if let Some(value) = cargo_obj.get(\"cancelRunning\") {\n            let cancel = value.as_bool().ok_or_else(|| invalid_option(\"cancelRunning\"))?;\n            self.publish_mode = if cancel {\n                PublishMode::CancelRunning\n            } else {\n                PublishMode::QueueIfRunning\n            };\n        }\n\n        if let Some(value) = cargo_obj.get(\"refreshIntervalSeconds\") {\n            if value.is_null() {\n                self.refresh_interval_seconds = None;\n            } else {\n                let seconds = value.as_i64().ok_or_else(|| invalid_option(\"refreshIntervalSeconds\"))?;\n                if seconds < 0 {\n                    self.refresh_interval_seconds = None;\n                } else {\n                    self.refresh_interval_seconds = Some(Duration::from_secs(seconds as u64));\n                }\n            }\n        }\n\n        if let Some(value) = cargo_obj.get(\"separateChildDiagnostics\") {\n            self.separate_child_diagnostics = if value.is_null() {\n                None\n            } else {\n                Some(\n                    value\n                        .as_bool()\n                        .ok_or_else(|| invalid_option(\"separateChildDiagnostics\"))?,\n                )\n            };\n        }\n\n        if let Some(value) = cargo_obj.get(\"checkOnSave\") {\n            self.check_on_save = value.as_bool().ok_or_else(|| invalid_option(\"checkOnSave\"))?;\n        }\n\n        if let Some(value) = cargo_obj.get(\"clearDiagnosticsOnCheck\") {\n            self.clear_diagnostics_on_check = value\n                .as_bool()\n                .ok_or_else(|| invalid_option(\"clearDiagnosticsOnCheck\"))?;\n        }\n\n        if let Some(value) = cargo_obj.get(\"updateOnInsertDebounceMillis\") {\n            let millis = value\n                .as_u64()\n                .ok_or_else(|| invalid_option(\"updateOnInsertDebounceMillis\"))?;\n            self.update_on_insert_debounce = Duration::from_millis(millis);\n        }\n\n        Ok(())\n    }\n\n    pub(crate) fn reset(&mut self) {\n        *self = Self::default();\n    }\n}\n\nimpl Default for CargoOptions {\n    fn default() -> Self {\n        Self {\n            env: Vec::new(),\n            publish_mode: PublishMode::CancelRunning,\n            command: \"check\".to_string(),\n            features: CargoFeatures::default(),\n            all_targets: false,\n            extra_command_args: vec![],\n            package: None,\n            refresh_interval_seconds: Some(Duration::from_secs(1)),\n            separate_child_diagnostics: None,\n            check_on_save: true,\n            clear_diagnostics_on_check: false,\n            update_on_insert: false,\n            update_on_insert_debounce: Duration::from_millis(500),\n            no_default_features: false,\n        }\n    }\n}\n\n#[derive(Debug)]\npub(crate) struct BaconOptions {\n    pub(crate) locations_file: String,\n    pub(crate) run_in_background: bool,\n    pub(crate) run_in_background_command: String,\n    pub(crate) run_in_background_command_args: String,\n    pub(crate) validate_preferences: bool,\n    pub(crate) create_preferences_file: bool,\n    pub(crate) synchronize_all_open_files_wait: Duration,\n    pub(crate) update_on_save: bool,\n    pub(crate) update_on_save_wait: Duration,\n}\n\nimpl BaconOptions {\n    pub(crate) fn update_from_json_obj(&mut self, bacon_obj: &Map<String, Value>) -> jsonrpc::Result<()> {\n        if let Some(value) = bacon_obj.get(\"locationsFile\") {\n            self.locations_file = value\n                .as_str()\n                .ok_or(jsonrpc::Error::new(jsonrpc::ErrorCode::InvalidParams))?\n                .to_string();\n        }\n        if let Some(value) = bacon_obj.get(\"runInBackground\") {\n            self.run_in_background = value\n                .as_bool()\n                .ok_or(jsonrpc::Error::new(jsonrpc::ErrorCode::InvalidParams))?;\n        }\n        if let Some(value) = bacon_obj.get(\"runInBackgroundCommand\") {\n            self.run_in_background_command = value\n                .as_str()\n                .ok_or(jsonrpc::Error::new(jsonrpc::ErrorCode::InvalidParams))?\n                .to_string();\n        }\n        if let Some(value) = bacon_obj.get(\"runInBackgroundCommandArguments\") {\n            self.run_in_background_command_args = value\n                .as_str()\n                .ok_or(jsonrpc::Error::new(jsonrpc::ErrorCode::InvalidParams))?\n                .to_string();\n        }\n        if let Some(value) = bacon_obj.get(\"validatePreferences\") {\n            self.validate_preferences = value\n                .as_bool()\n                .ok_or(jsonrpc::Error::new(jsonrpc::ErrorCode::InvalidParams))?;\n        }\n        if let Some(value) = bacon_obj.get(\"createPreferencesFile\") {\n            self.create_preferences_file = value\n                .as_bool()\n                .ok_or(jsonrpc::Error::new(jsonrpc::ErrorCode::InvalidParams))?;\n        }\n        if let Some(value) = bacon_obj.get(\"synchronizeAllOpenFilesWaitMillis\") {\n            self.synchronize_all_open_files_wait = Duration::from_millis(\n                value\n                    .as_u64()\n                    .ok_or(jsonrpc::Error::new(jsonrpc::ErrorCode::InvalidParams))?,\n            );\n        }\n        if let Some(value) = bacon_obj.get(\"updateOnSave\") {\n            self.update_on_save = value\n                .as_bool()\n                .ok_or(jsonrpc::Error::new(jsonrpc::ErrorCode::InvalidParams))?;\n        }\n        if let Some(value) = bacon_obj.get(\"updateOnSaveWaitMillis\") {\n            self.update_on_save_wait = Duration::from_millis(\n                value\n                    .as_u64()\n                    .ok_or(jsonrpc::Error::new(jsonrpc::ErrorCode::InvalidParams))?,\n            );\n        }\n\n        Ok(())\n    }\n\n    pub fn reset(&mut self) {\n        *self = Self::default();\n    }\n}\n\nimpl Default for BaconOptions {\n    fn default() -> Self {\n        Self {\n            locations_file: LOCATIONS_FILE.to_string(),\n            run_in_background: true,\n            run_in_background_command: BACON_BACKGROUND_COMMAND.to_string(),\n            run_in_background_command_args: BACON_BACKGROUND_COMMAND_ARGS.to_string(),\n            validate_preferences: true,\n            create_preferences_file: true,\n            synchronize_all_open_files_wait: Duration::from_millis(2000),\n            update_on_save: true,\n            update_on_save_wait: Duration::from_millis(1000),\n        }\n    }\n}\n\n/// Per-invocation overrides used to redirect a cargo run from the real\n/// workspace into the hardlinked shadow workspace for live diagnostics.\n#[derive(Debug)]\npub(crate) struct LiveCheckContext {\n    pub(crate) shadow_root: PathBuf,\n    pub(crate) shadow_target_dir: PathBuf,\n    pub(crate) real_root: PathBuf,\n}\n\n#[derive(Debug)]\npub(crate) struct CargoRuntime {\n    cancel_token: CancellationToken,\n    run_state: CargoRunState,\n    files_with_diags: HashSet<Uri>,\n    diagnostics_version: i32,\n    build_folder: PathBuf,\n    // Timestamp of the most recent publish_cargo_diagnostics invocation.\n    // Used by did_open to avoid kicking off a redundant run when one was\n    // just triggered (e.g. the initial run from `initialized` immediately\n    // followed by the client's first `didOpen`).\n    last_run_started: Option<Instant>,\n    /// Hardlinked shadow of the workspace used for live \"as you type\"\n    /// diagnostics. None until the first did_change with `update_on_insert`\n    /// enabled — building it eagerly at backend init would block startup on\n    /// large workspaces for users who never trigger live mode.\n    pub(crate) shadow: Option<ShadowWorkspace>,\n    /// File URIs that currently have a dirty buffer overlaid in the shadow.\n    /// On did_save / did_close we restore each entry to a hardlink so the\n    /// next live run reads the on-disk version.\n    pub(crate) dirty_files: HashSet<Uri>,\n    /// Pending debounced live-cargo trigger. Each `did_change` cancels the\n    /// prior handle and schedules a new one so only the last keystroke fires\n    /// a check.\n    pub(crate) live_debounce: Option<JoinHandle<()>>,\n}\n\nimpl Default for CargoRuntime {\n    fn default() -> Self {\n        Self {\n            cancel_token: CancellationToken::new(),\n            run_state: CargoRunState::Idle,\n            files_with_diags: HashSet::new(),\n            diagnostics_version: 0,\n            build_folder: PathBuf::new(),\n            last_run_started: None,\n            shadow: None,\n            dirty_files: HashSet::new(),\n            live_debounce: None,\n        }\n    }\n}\n\n#[derive(Debug)]\npub(crate) struct BaconRuntime {\n    pub(crate) shutdown_token: CancellationToken,\n    pub(crate) open_files: HashSet<Uri>,\n    // Some(..) if we have to run bacon in the background ourselves\n    pub(crate) command_handle: Option<JoinHandle<()>>,\n    pub(crate) sync_files_handle: JoinHandle<()>,\n    // Monotonic counter stamped onto each publishDiagnostics call so clients\n    // can discard stale results if publishes arrive out of order.\n    pub(crate) diagnostics_version: i32,\n}\n\n#[derive(Debug, Default)]\nstruct State {\n    project_root: Option<PathBuf>,\n    workspace_folders: Option<Vec<WorkspaceFolder>>,\n    diagnostics_data_supported: bool,\n    related_information_supported: bool,\n    backend: Option<BackendRuntime>,\n    /// Set by `initialize()` from `initialization_options.cargo.updateOnInsert`.\n    /// We need this at initialize-time to advertise a `Full` text-document\n    /// sync capability, because dynamic `client/registerCapability` for\n    /// `textDocument/didChange` after `initialized` doesn't reliably retrofit\n    /// already-attached buffers (Neovim, in particular, ignores it). A\n    /// statically-advertised capability is honored at attach.\n    init_update_on_insert: bool,\n}\n\n#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]\npub(crate) struct CorrectionEdit {\n    pub(crate) range: Range,\n    pub(crate) new_text: String,\n}\n\n// A single logical fix can require several disjoint byte-range edits. For\n// example, removing `Compact` from `use …::{Compact, FmtSpan}` produces three\n// edits: remove `{`, remove `Compact, `, remove `}`, leaving `use …::FmtSpan`.\n// All edits must be applied atomically so the file stays valid.\n#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]\npub(crate) struct Correction {\n    pub(crate) label: String,\n    pub(crate) edits: Vec<CorrectionEdit>,\n}\n\nimpl Correction {\n    pub(crate) fn from_single(range: Range, new_text: &str) -> Self {\n        let label = if new_text.is_empty() {\n            \"Remove\".to_string()\n        } else {\n            format!(\"Replace with: {new_text}\")\n        };\n        Self {\n            label,\n            edits: vec![CorrectionEdit {\n                range,\n                new_text: new_text.to_string(),\n            }],\n        }\n    }\n\n    pub(crate) fn from_multi(edits: Vec<CorrectionEdit>) -> Self {\n        let label = match edits.iter().find(|e| !e.new_text.is_empty()) {\n            None => \"Remove\".to_string(),\n            Some(e) => format!(\"Replace with: {}\", e.new_text),\n        };\n        Self { label, edits }\n    }\n}\n\n#[derive(Debug, serde::Serialize, serde::Deserialize)]\nstruct DiagnosticData {\n    corrections: Vec<Correction>,\n}\n\n#[derive(Debug, Clone)]\npub struct BaconLs {\n    client: Arc<Client>,\n    state: Arc<RwLock<State>>,\n}\n\nimpl BaconLs {\n    fn new(client: Client) -> Self {\n        Self {\n            client: Arc::new(client),\n            state: Arc::new(RwLock::new(State::default())),\n        }\n    }\n\n    fn configure_tracing(log_level: Option<String>, log_path: Option<&Path>) {\n        // Configure logging to file.\n        let level = log_level.unwrap_or_else(|| env::var(\"RUST_LOG\").unwrap_or(\"off\".to_string()));\n        if level == \"off\" {\n            return;\n        }\n        let default_path = PathBuf::from(format!(\"{PKG_NAME}.log\"));\n        let log_path = log_path.unwrap_or(&default_path);\n        let file = match std::fs::OpenOptions::new()\n            .create(true)\n            .write(true)\n            .truncate(true)\n            .open(log_path)\n        {\n            Ok(file) => file,\n            Err(e) => {\n                // stdin/stdout are the LSP jsonrpc pipes; stderr is usually\n                // captured by the client's trace window. One line there is the\n                // best we can do to tell the user why logging is silent.\n                eprintln!(\n                    \"{PKG_NAME}: could not open log file {}: {e} (tracing disabled)\",\n                    log_path.display()\n                );\n                return;\n            }\n        };\n        // try_init: tests may install the subscriber more than once across the\n        // process lifetime (cargo runs them in a single binary). Don't panic\n        // if a global subscriber is already set — the first one wins.\n        let _ = tracing_subscriber::fmt()\n            .with_env_filter(level)\n            .with_writer(file)\n            .with_thread_names(true)\n            .with_span_events(FmtSpan::CLOSE)\n            .with_target(true)\n            .with_file(true)\n            .with_line_number(true)\n            .try_init();\n    }\n\n    /// Run the LSP server.\n    pub async fn serve() {\n        Self::configure_tracing(None, None);\n        // Lock stdin / stdout.\n        let stdin = tokio::io::stdin();\n        let stdout = tokio::io::stdout();\n        // Start the service.\n        let (service, socket) = LspService::new(Self::new);\n        Server::new(stdin, stdout, socket).serve(service).await;\n        // Force the process to terminate instead of waiting for the tokio\n        // runtime to drain. Some background tasks (bacon subprocess readers,\n        // file watchers) can linger past the `exit` notification; if the\n        // process doesn't die promptly, `:LspRestart` in Neovim gives up\n        // before starting a fresh instance.\n        std::process::exit(0);\n    }\n\n    async fn find_git_root_directory(path: &Path) -> Option<PathBuf> {\n        let output = tokio::process::Command::new(\"git\")\n            .arg(\"-C\")\n            .arg(path)\n            .arg(\"rev-parse\")\n            .arg(\"--show-toplevel\")\n            .output()\n            .await\n            .ok()?;\n\n        if output.status.success() {\n            String::from_utf8(output.stdout).ok().map(|v| PathBuf::from(v.trim()))\n        } else {\n            None\n        }\n    }\n\n    fn detect_backend(values: &Map<String, Value>) -> Result<BackendChoice, String> {\n        if let Some(value) = values.get(\"backend\") {\n            let backend = value.as_str().ok_or(\"'backend' must be a string\")?;\n            match backend {\n                \"cargo\" => Ok(BackendChoice::Cargo),\n                \"bacon\" => Ok(BackendChoice::Bacon),\n                other => Err(format!(\"Invalid backend value '{other}'. Must be 'cargo' or 'bacon'.\")),\n            }\n        } else {\n            let has_cargo = values.get(\"cargo\").and_then(|v| v.as_object()).is_some();\n            let has_bacon = values.get(\"bacon\").and_then(|v| v.as_object()).is_some();\n            match (has_cargo, has_bacon) {\n                (true, true) => Err(\n                    \"Both 'cargo' and 'bacon' config sections present without a 'backend' key. \\\n                     Set 'backend' to 'cargo' or 'bacon'.\"\n                        .to_string(),\n                ),\n                (_, true) => Ok(BackendChoice::Bacon),\n                _ => Ok(BackendChoice::Cargo),\n            }\n        }\n    }\n\n    async fn pull_configuration(&self) {\n        tracing::debug!(\"pull_configuration\");\n\n        let configuration_fut = self.client.configuration(vec![ls_types::ConfigurationItem {\n            scope_uri: None,\n            section: Some(\"bacon_ls\".to_string()),\n        }]);\n        // A client that never answers `workspace/configuration` (e.g. one\n        // mid-teardown) would otherwise keep this await alive forever, which\n        // in turn pins the `initialized` future inside the server loop and\n        // blocks a clean shutdown.\n        let response = match tokio::time::timeout(std::time::Duration::from_secs(5), configuration_fut).await {\n            Ok(Ok(response)) => response,\n            Ok(Err(e)) => {\n                tracing::error!(\"failed to pull configuration: {e}\");\n                return;\n            }\n            Err(_) => {\n                tracing::warn!(\"workspace/configuration request timed out; proceeding with defaults\");\n                return;\n            }\n        };\n\n        let Some(settings) = response.into_iter().next() else {\n            tracing::warn!(\"empty configuration response from client\");\n            return;\n        };\n\n        tracing::trace!(\"pulled configuration: {settings:#?}\");\n        self.adapt_to_settings(&settings).await;\n    }\n\n    async fn adapt_to_settings(&self, settings: &Value) {\n        let mut state = self.state.write().await;\n        let Some(values) = settings.as_object() else {\n            tracing::warn!(\"configuration is not a JSON object\");\n            return;\n        };\n\n        if state.backend.is_none() {\n            let backend_choice = match Self::detect_backend(values) {\n                Ok(choice) => {\n                    tracing::info!(backend = ?choice, \"backend detected\");\n                    choice\n                }\n                Err(msg) => {\n                    tracing::error!(\"{msg}\");\n                    self.client.show_message(MessageType::ERROR, &msg).await;\n                    return;\n                }\n            };\n\n            match backend_choice {\n                BackendChoice::Bacon => {\n                    let mut config = BaconOptions::default();\n                    if let Some(bacon_obj) = values.get(\"bacon\").and_then(|v| v.as_object())\n                        && let Err(e) = config.update_from_json_obj(bacon_obj)\n                    {\n                        tracing::error!(\"invalid bacon configuration: {e}\");\n                        self.client\n                            .show_message(MessageType::ERROR, format!(\"Error in \\\"bacon\\\" section: {e}\"))\n                            .await;\n                    }\n\n                    if config.validate_preferences {\n                        if let Err(e) = Bacon::validate_preferences(\n                            &config.run_in_background_command,\n                            config.create_preferences_file,\n                        )\n                        .await\n                        {\n                            tracing::error!(\"{e}\");\n                            self.client.show_message(MessageType::ERROR, e).await;\n                        }\n                    } else {\n                        tracing::warn!(\"skipping validation of bacon preferences, validateBaconPreferences is false\");\n                    }\n\n                    let proj_root = state.project_root.clone();\n                    let shutdown_token = CancellationToken::new();\n                    let command_handle = if config.run_in_background {\n                        let mut current_dir = None;\n                        if let Ok(cwd) = env::current_dir() {\n                            current_dir = Self::find_git_root_directory(&cwd).await;\n                            if let Some(dir) = &current_dir {\n                                if !dir.join(\"Cargo.toml\").exists() {\n                                    current_dir = proj_root;\n                                }\n                            } else {\n                                current_dir = proj_root;\n                            }\n                        }\n\n                        match Bacon::run_in_background(\n                            &config.run_in_background_command,\n                            &config.run_in_background_command_args,\n                            current_dir.as_ref(),\n                            shutdown_token.clone(),\n                        )\n                        .await\n                        {\n                            Ok(command) => {\n                                tracing::info!(\"bacon was started successfully and is running in the background\");\n                                Some(command)\n                            }\n                            Err(e) => {\n                                tracing::error!(\"{e}\");\n                                self.client.show_message(MessageType::ERROR, e).await;\n                                None\n                            }\n                        }\n                    } else {\n                        tracing::warn!(\"skipping background bacon startup, runBaconInBackground is false\");\n                        None\n                    };\n\n                    let task_state = self.state.clone();\n                    let task_client = self.client.clone();\n                    state.backend = Some(BackendRuntime::Bacon {\n                        config,\n                        runtime: BaconRuntime {\n                            shutdown_token,\n                            open_files: HashSet::new(),\n                            command_handle,\n                            sync_files_handle: tokio::task::spawn(Self::synchronize_diagnostics(\n                                task_state,\n                                task_client,\n                            )),\n                            diagnostics_version: 0,\n                        },\n                    });\n                    tracing::info!(\"bacon backend initialized\");\n                }\n                BackendChoice::Cargo => {\n                    let mut config = CargoOptions::default();\n                    // `update_on_insert` is sourced exclusively from\n                    // `initialization_options.cargo.updateOnInsert` (read in\n                    // `initialize` and stashed on `State`). The static\n                    // `textDocument/didChange` capability has to be decided\n                    // before workspace settings even arrive, so the runtime\n                    // gate has to come from the same place.\n                    if state.init_update_on_insert {\n                        config.update_on_insert = true;\n                    }\n                    if let Some(cargo_obj) = values.get(\"cargo\").and_then(|v| v.as_object())\n                        && let Err(e) = config.update_from_json_obj(cargo_obj)\n                    {\n                        tracing::error!(\"invalid cargo configuration: {e}\");\n                        self.client\n                            .show_message(MessageType::ERROR, format!(\"Error in \\\"cargo\\\" section: {e}\"))\n                            .await;\n                    }\n                    if let Err(e) = Self::init_cargo_backend(&mut state, config) {\n                        tracing::error!(\"{e}\");\n                        drop(state);\n                        self.client.show_message(MessageType::ERROR, e).await;\n                        return;\n                    }\n                    drop(state);\n                }\n            }\n        } else {\n            let current_choice = match &state.backend {\n                Some(BackendRuntime::Bacon { .. }) => BackendChoice::Bacon,\n                Some(BackendRuntime::Cargo { .. }) => BackendChoice::Cargo,\n                None => unreachable!(\"backend is Some in this branch\"),\n            };\n            let desired = match Self::detect_backend(values) {\n                Ok(choice) => choice,\n                Err(err) => {\n                    tracing::error!(\"invalid backend configuration on reload: {err}\");\n                    self.client.show_message(MessageType::ERROR, &err).await;\n                    return;\n                }\n            };\n\n            if desired != current_choice {\n                let msg = \"Backend cannot be changed while the server is running. \\\n                           Restart the server to switch backends.\";\n                tracing::error!(\"{msg}\");\n                self.client.show_message(MessageType::ERROR, msg).await;\n                return;\n            }\n\n            let project_root = state.project_root.clone();\n            let init_update_on_insert = state.init_update_on_insert;\n            match &mut state.backend {\n                Some(BackendRuntime::Cargo { config, runtime }) => {\n                    config.reset();\n                    if init_update_on_insert {\n                        config.update_on_insert = true;\n                    }\n                    if let Some(cargo_obj) = values.get(\"cargo\").and_then(|v| v.as_object())\n                        && let Err(e) = config.update_from_json_obj(cargo_obj)\n                    {\n                        tracing::error!(\"invalid cargo configuration: {e}\");\n                        self.client\n                            .show_message(MessageType::ERROR, format!(\"Error in \\\"cargo\\\" section: {e}\"))\n                            .await;\n                    }\n                    if let Some(root) = project_root {\n                        runtime.build_folder = root;\n                    }\n                    tracing::debug!(\"cargo configuration updated\");\n                }\n                Some(BackendRuntime::Bacon { config, .. }) => {\n                    config.reset();\n                    if let Some(bacon_obj) = values.get(\"bacon\").and_then(|v| v.as_object())\n                        && let Err(e) = config.update_from_json_obj(bacon_obj)\n                    {\n                        tracing::error!(\"invalid bacon configuration: {e}\");\n                        self.client\n                            .show_message(MessageType::ERROR, format!(\"Error in \\\"bacon\\\" section: {e}\"))\n                            .await;\n                    }\n                    tracing::debug!(\"bacon configuration updated\");\n                }\n                None => unreachable!(\"backend is Some in this branch\"),\n            }\n        }\n    }\n\n    fn init_cargo_backend(state: &mut RwLockWriteGuard<'_, State>, config: CargoOptions) -> Result<(), String> {\n        let build_folder = match &state.project_root {\n            Some(root) => root.clone(),\n            None => match env::current_dir() {\n                Ok(cwd) => {\n                    tracing::warn!(\n                        \"no Cargo project root detected; falling back to current working directory: {}\",\n                        cwd.display()\n                    );\n                    cwd\n                }\n                Err(e) => {\n                    return Err(format!(\n                        \"cargo backend cannot start: no project root detected and current working \\\n                         directory is unavailable ({e}). Open a folder containing a Cargo.toml and \\\n                         restart the server.\"\n                    ));\n                }\n            },\n        };\n        let runtime = CargoRuntime {\n            build_folder,\n            ..CargoRuntime::default()\n        };\n        tracing::info!(build_folder = ?runtime.build_folder, \"cargo backend initialized\");\n        state.backend = Some(BackendRuntime::Cargo { config, runtime });\n        Ok(())\n    }\n\n    /// Trigger a save-time cargo run against the real workspace.\n    async fn publish_cargo_diagnostics(&self) {\n        self.publish_cargo_diagnostics_inner(None).await;\n    }\n\n    /// Trigger a live \"as you type\" cargo run against the hardlinked shadow\n    /// workspace. Builds the shadow on first call. Returns silently if\n    /// `update_on_insert` isn't on or the shadow can't be built.\n    pub(crate) async fn publish_cargo_diagnostics_live(&self) {\n        let live_on = {\n            let state = self.state.read().await;\n            matches!(\n                &state.backend,\n                Some(BackendRuntime::Cargo { config, .. }) if config.update_on_insert\n            )\n        };\n        if !live_on {\n            return;\n        }\n        let Some(shadow) = self.ensure_shadow_built().await else {\n            return;\n        };\n        let ctx = LiveCheckContext {\n            shadow_root: shadow.shadow_root().to_path_buf(),\n            shadow_target_dir: shadow.target_dir().to_path_buf(),\n            real_root: shadow.real_root().to_path_buf(),\n        };\n        self.publish_cargo_diagnostics_inner(Some(&ctx)).await;\n    }\n\n    async fn publish_cargo_diagnostics_inner(&self, live: Option<&LiveCheckContext>) {\n        tracing::info!(live = live.is_some(), \"starting cargo diagnostics run\");\n        let mut guard = self.state.write().await;\n        let project_root = guard.project_root.clone();\n        let related_information_supported = guard.related_information_supported;\n\n        let Some(BackendRuntime::Cargo { config, runtime }) = &mut guard.backend else {\n            return;\n        };\n        let use_related_information = !config\n            .separate_child_diagnostics\n            .unwrap_or(!related_information_supported);\n        let cargo_command = config.command.clone();\n        let mut cargo_env = config.env.clone();\n        let mut cmd_args = config.build_command_args();\n        let publish_mode = config.publish_mode;\n        let clear_diagnostics_on_check = config.clear_diagnostics_on_check;\n        let build_folder = match live {\n            Some(ctx) => {\n                cmd_args.push(format!(\"--target-dir={}\", ctx.shadow_target_dir.display()));\n                // `--remap-path-prefix` makes rustc emit diagnostic spans with\n                // the real workspace path in place of the shadow path, so the\n                // editor opens the user's source file instead of a target/ copy.\n                let rustflags = format!(\n                    \"--remap-path-prefix={}={}\",\n                    ctx.shadow_root.display(),\n                    ctx.real_root.display()\n                );\n                // Honor any RUSTFLAGS the user already set in their config.\n                if let Some(slot) = cargo_env.iter_mut().find(|(k, _)| k == \"RUSTFLAGS\") {\n                    slot.1.push(' ');\n                    slot.1.push_str(&rustflags);\n                } else {\n                    cargo_env.push((\"RUSTFLAGS\".to_string(), rustflags));\n                }\n                ctx.shadow_root.clone()\n            }\n            None => runtime.build_folder.clone(),\n        };\n        runtime.diagnostics_version = runtime.diagnostics_version.wrapping_add(1);\n        runtime.last_run_started = Some(Instant::now());\n        let version = runtime.diagnostics_version;\n        let refresh_interval = config.refresh_interval_seconds;\n\n        let cancel_token = match publish_mode {\n            PublishMode::CancelRunning => {\n                runtime.cancel_token.cancel();\n                runtime.cancel_token = CancellationToken::new();\n                runtime.cancel_token.clone()\n            }\n            PublishMode::QueueIfRunning => match runtime.run_state {\n                CargoRunState::Running | CargoRunState::RunningPending => {\n                    runtime.run_state = CargoRunState::RunningPending;\n                    tracing::debug!(\"cargo already running, marking pending\");\n                    drop(guard);\n                    return;\n                }\n                CargoRunState::Idle => {\n                    runtime.run_state = CargoRunState::Running;\n                    runtime.cancel_token.clone()\n                }\n            },\n        };\n\n        // Drain the URIs we need to clear into a local Vec, then drop the\n        // state lock BEFORE doing any LSP IO. Holding the write guard across\n        // awaited publishes blocks every other handler (did_open, did_close,\n        // codeAction, …) for the duration of the round-trips.\n        let files_to_clear: Vec<Uri> = if clear_diagnostics_on_check {\n            runtime.files_with_diags.drain().collect()\n        } else {\n            Vec::new()\n        };\n\n        drop(guard);\n\n        for file in files_to_clear {\n            self.client.publish_diagnostics(file, vec![], Some(version)).await;\n        }\n\n        let token = ProgressToken::Number(version);\n        let progress = self\n            .client\n            .progress(token, \"checking\")\n            .with_message(format!(\"cargo {cargo_command}\"))\n            .with_percentage(0)\n            .begin()\n            .await;\n\n        let (tx, rx) = flume::unbounded();\n\n        let cargo_future = Cargo::cargo_diagnostics(\n            cmd_args,\n            &cargo_env,\n            project_root.as_ref(),\n            &build_folder,\n            use_related_information,\n            &progress,\n            tx,\n        );\n\n        let consumer_client = self.client.clone();\n        let diagnostic_consumer = async move {\n            // Per-URI bucket: the diagnostics to publish, a `seen` set keyed by\n            // (range, severity, message) for O(1) dedup, and a dirty flag for\n            // partial publishes during the cargo run.\n            let mut diagnostics_map = HashMap::<Uri, (Vec<Diagnostic>, HashSet<DiagKey>, bool)>::new();\n\n            enum AccumulateResult {\n                Closed,\n                NewDiagnostic,\n                Duplicate,\n            }\n\n            fn accumulate_diagnostics(\n                recv_result: Result<(Uri, Diagnostic), RecvError>,\n                diagnostics_map: &mut HashMap<Uri, (Vec<Diagnostic>, HashSet<DiagKey>, bool)>,\n            ) -> AccumulateResult {\n                let Ok((url, diagnostic)) = recv_result else {\n                    return AccumulateResult::Closed;\n                };\n                let (diagnostics, seen, dirty) = diagnostics_map.entry(url).or_default();\n                if seen.insert(diag_key(&diagnostic)) {\n                    diagnostics.push(diagnostic);\n                    *dirty = true;\n                    AccumulateResult::NewDiagnostic\n                } else {\n                    AccumulateResult::Duplicate\n                }\n            }\n\n            if let Some(refresh_interval) = refresh_interval {\n                // The very first diagnostic of a run is published immediately\n                // — waiting up to `refresh_interval` for the editor to show\n                // *something* is the most user-visible source of latency. After\n                // the first publish, subsequent diagnostics accumulate and are\n                // flushed every `refresh_interval`.\n                let mut first_published = false;\n                let mut t = std::time::Instant::now();\n                loop {\n                    let mut got_new = false;\n                    tokio::select! {\n                        result = rx.recv_async() => {\n                            match accumulate_diagnostics(result, &mut diagnostics_map) {\n                                AccumulateResult::Closed => break,\n                                AccumulateResult::NewDiagnostic => got_new = true,\n                                AccumulateResult::Duplicate => {}\n                            }\n                        }\n                        _ = tokio::time::sleep_until(tokio::time::Instant::from_std(t + refresh_interval)) => {}\n                    }\n\n                    let publish_first = got_new && !first_published;\n                    if publish_first || t.elapsed() >= refresh_interval {\n                        for (url, (diagnostics, _seen, dirty)) in diagnostics_map.iter_mut() {\n                            if *dirty {\n                                consumer_client\n                                    .publish_diagnostics(url.clone(), diagnostics.clone(), Some(version))\n                                    .await;\n                                *dirty = false;\n                            }\n                        }\n                        if publish_first {\n                            tracing::debug!(\"first diagnostic published; switching to refresh-interval cadence\");\n                            first_published = true;\n                        }\n                        t = std::time::Instant::now();\n                    }\n                }\n            } else {\n                loop {\n                    if matches!(\n                        accumulate_diagnostics(rx.recv_async().await, &mut diagnostics_map),\n                        AccumulateResult::Closed\n                    ) {\n                        break;\n                    }\n                }\n            }\n\n            diagnostics_map\n        };\n\n        let consumer_handle = tokio::spawn(diagnostic_consumer);\n\n        let result = tokio::select! {\n            result = cargo_future => {\n                result.map(|_| false)\n            },\n            () = cancel_token.cancelled() => {\n                tracing::info!(\"cargo run cancelled by newer request\");\n                Ok(true)\n            }\n        };\n\n        let was_cancelled = match result {\n            Ok(t) => t,\n            Err(error) => {\n                // We know there wont be any diagnostics as they way we detect cargo errors is\n                // if it exists with non 0 exit code and no diagnostics were found\n                tracing::error!(?error, \"error building diagnostics\");\n                progress.finish().await;\n                let _ = consumer_handle.await;\n                self.client.log_message(MessageType::ERROR, format!(\"{error}\")).await;\n                self.client.show_message(MessageType::ERROR, format!(\"{error}\")).await;\n                return;\n            }\n        };\n\n        if was_cancelled {\n            // The newer run that triggered cancellation owns publishing. Touching\n            // files_with_diags or publishing partial results here would race with\n            // it and could push stale diagnostics on top of correct ones.\n            let _ = consumer_handle.await;\n            progress.finish_with_message(\"cancelled by user\").await;\n            return;\n        }\n\n        tracing::info!(\"cargo run finished, collecting diagnostics\");\n\n        let mut diagnostics = match consumer_handle.await {\n            Ok(d) => d,\n            Err(error) => {\n                tracing::error!(?error, \"diagnostics fetching task panicked\");\n                progress.finish().await;\n                self.client.log_message(MessageType::ERROR, format!(\"{error}\")).await;\n                self.client.show_message(MessageType::ERROR, format!(\"{error}\")).await;\n                return;\n            }\n        };\n\n        let mut state = self.state.write().await;\n        let Some(BackendRuntime::Cargo {\n            config,\n            runtime: cargo_rt,\n        }) = &mut state.backend\n        else {\n            // This should be impossible to land here, if we do there a logic error\n            tracing::error!(\"backend changed during cargo run\");\n            return;\n        };\n        let publish_mode = config.publish_mode;\n\n        // In CancelRunning mode a newer run may have started after our cargo\n        // process finished but before we reached this point. If so our results\n        // are stale — skip publishing so we don't overwrite the newer run's\n        // output with old data.\n        if let PublishMode::CancelRunning = publish_mode\n            && version != cargo_rt.diagnostics_version\n        {\n            tracing::info!(\n                version,\n                current = cargo_rt.diagnostics_version,\n                \"skipping stale publish\"\n            );\n            progress.finish_with_message(\"superseded by newer run\").await;\n            return;\n        }\n\n        for file in cargo_rt.files_with_diags.drain() {\n            // Add empty diagnostics so that it get cleared later\n            let _ = diagnostics.entry(file).or_insert((vec![], HashSet::new(), true));\n        }\n\n        let mut num_warnings = 0;\n        let mut num_errors = 0;\n        for (uri, (diagnostics, _seen, is_dirty)) in diagnostics.into_iter() {\n            tracing::debug!(uri = uri.to_string(), \"sent {} cargo diagnostics\", diagnostics.len());\n            for diagnostic in &diagnostics {\n                match diagnostic.severity {\n                    Some(DiagnosticSeverity::ERROR) => num_errors += 1,\n                    Some(DiagnosticSeverity::WARNING) => num_warnings += 1,\n                    Some(_) | None => {}\n                }\n            }\n            if !diagnostics.is_empty() {\n                let _ = cargo_rt.files_with_diags.insert(uri.clone());\n            }\n            if is_dirty {\n                self.client.publish_diagnostics(uri, diagnostics, Some(version)).await;\n            }\n        }\n        let message = format!(\"done, errors: {num_errors}, warnings: {num_warnings}\");\n        progress.finish_with_message(message).await;\n\n        if let PublishMode::QueueIfRunning = publish_mode {\n            match cargo_rt.run_state {\n                CargoRunState::RunningPending => {\n                    cargo_rt.run_state = CargoRunState::Idle;\n                    drop(state);\n                    tracing::info!(\"re-running cargo after queued request\");\n                    Box::pin(self.publish_cargo_diagnostics()).await;\n                }\n                _ => {\n                    cargo_rt.run_state = CargoRunState::Idle;\n                    drop(state);\n                }\n            }\n        }\n    }\n\n    /// Lazy-build (or fetch) the live shadow workspace. Returns `None` if the\n    /// project root isn't known or the build fails — callers should treat\n    /// that as \"skip this live update\", not as a hard error.\n    pub(crate) async fn ensure_shadow_built(&self) -> Option<ShadowWorkspace> {\n        // Fast path: shadow already built.\n        {\n            let state = self.state.read().await;\n            if let Some(BackendRuntime::Cargo { runtime, .. }) = &state.backend\n                && let Some(shadow) = &runtime.shadow\n            {\n                return Some(shadow.clone());\n            }\n        }\n\n        let project_root = {\n            let state = self.state.read().await;\n            state.project_root.clone()\n        };\n        let Some(root) = project_root else {\n            tracing::warn!(\"updateOnInsert: no project root; cannot build live shadow\");\n            return None;\n        };\n\n        tracing::info!(root = ?root, \"updateOnInsert: building live shadow workspace\");\n        // Surface this to the user — it's a one-time, multi-second cost\n        // (tree walk + hardlink fan-out + cold cargo target dir) and\n        // without a heads-up they'd just see the editor go quiet on the\n        // first keystroke.\n        self.client\n            .show_message(\n                MessageType::INFO,\n                \"bacon-ls: building live diagnostics shadow workspace (first run only)…\",\n            )\n            .await;\n        let shadow = match ShadowWorkspace::build(root).await {\n            Ok(s) => s,\n            Err(e) => {\n                tracing::error!(\"updateOnInsert: failed to build shadow: {e}\");\n                self.client\n                    .show_message(\n                        MessageType::ERROR,\n                        format!(\"bacon-ls: failed to build live shadow workspace: {e}\"),\n                    )\n                    .await;\n                return None;\n            }\n        };\n\n        // Stash; if a parallel did_change raced us and built one too, ours\n        // overwrites — both reflect the same on-disk tree.\n        let mut state = self.state.write().await;\n        if let Some(BackendRuntime::Cargo { runtime, .. }) = &mut state.backend {\n            runtime.shadow = Some(shadow.clone());\n        }\n        drop(state);\n        // Quieter signal that the shadow is ready — goes to the LSP trace\n        // pane rather than popping a second toast.\n        self.client\n            .log_message(\n                MessageType::INFO,\n                \"bacon-ls: live diagnostics shadow ready; subsequent edits will be checked as you type.\",\n            )\n            .await;\n        Some(shadow)\n    }\n\n    /// Apply a dirty buffer (from `did_change`) to the shadow workspace.\n    /// Tracks the URI in `dirty_files` so we can revert it later via\n    /// `restore_shadow_link_if_dirty` on `did_save` / `did_close`.\n    pub(crate) async fn live_update_dirty(&self, uri: Uri, content: String) {\n        let Some(real_path_cow) = uri.to_file_path() else {\n            tracing::warn!(uri = uri.as_str(), \"updateOnInsert: did_change uri is not a file path\");\n            return;\n        };\n        let real_path = real_path_cow.into_owned();\n\n        let Some(shadow) = self.ensure_shadow_built().await else {\n            tracing::warn!(\"updateOnInsert: shadow workspace not available; skipping live update\");\n            return;\n        };\n        if let Err(e) = shadow.write_dirty(&real_path, &content).await {\n            tracing::warn!(path = ?real_path, ?e, \"updateOnInsert: shadow write failed (file outside workspace?)\");\n            return;\n        }\n\n        let debounce = {\n            let mut state = self.state.write().await;\n            let Some(BackendRuntime::Cargo { config, runtime }) = &mut state.backend else {\n                return;\n            };\n            runtime.dirty_files.insert(uri.clone());\n            config.update_on_insert_debounce\n        };\n\n        tracing::info!(\n            uri = uri.as_str(),\n            debounce_ms = debounce.as_millis() as u64,\n            \"updateOnInsert: shadow updated, scheduling live cargo run\"\n        );\n        self.schedule_live_run(debounce).await;\n    }\n\n    /// Schedule (or reschedule) a live cargo run to fire after `delay` of\n    /// idle time. Cancels any previously-scheduled live trigger so a burst of\n    /// keystrokes coalesces into a single run.\n    pub(crate) async fn schedule_live_run(&self, delay: Duration) {\n        let mut state = self.state.write().await;\n        let Some(BackendRuntime::Cargo { runtime, .. }) = &mut state.backend else {\n            return;\n        };\n        if let Some(prev) = runtime.live_debounce.take() {\n            prev.abort();\n        }\n        let bacon = self.clone();\n        runtime.live_debounce = Some(tokio::spawn(async move {\n            tokio::time::sleep(delay).await;\n            bacon.publish_cargo_diagnostics_live().await;\n        }));\n    }\n\n    /// Cancel any pending debounced live trigger. Called on `did_save` so\n    /// the on-save cargo run (against the real workspace) is the canonical\n    /// one and a soon-to-be-stale live run doesn't race it.\n    pub(crate) async fn cancel_live_debounce(&self) {\n        let mut state = self.state.write().await;\n        if let Some(BackendRuntime::Cargo { runtime, .. }) = &mut state.backend\n            && let Some(handle) = runtime.live_debounce.take()\n        {\n            handle.abort();\n        }\n    }\n\n    /// On `did_save` / `did_close`, replace the (possibly dirty) shadow file\n    /// with a fresh hardlink to the on-disk version, and forget the URI.\n    pub(crate) async fn restore_shadow_link_if_dirty(&self, uri: &Uri) {\n        let (shadow, real_path) = {\n            let mut state = self.state.write().await;\n            let Some(BackendRuntime::Cargo { runtime, .. }) = &mut state.backend else {\n                return;\n            };\n            if !runtime.dirty_files.remove(uri) {\n                return;\n            }\n            let Some(shadow) = runtime.shadow.clone() else {\n                return;\n            };\n            let Some(path_cow) = uri.to_file_path() else {\n                return;\n            };\n            (shadow, path_cow.into_owned())\n        };\n        if let Err(e) = shadow.restore_link(&real_path).await {\n            tracing::warn!(path = ?real_path, ?e, \"updateOnInsert: failed to restore shadow link\");\n        }\n    }\n\n    async fn publish_bacon_diagnostics(&self, uri: &Uri) {\n        let mut guard = self.state.write().await;\n        let workspace_folders = guard.workspace_folders.clone();\n\n        let Some(BackendRuntime::Bacon { config, runtime }) = &mut guard.backend else {\n            return;\n        };\n        tracing::info!(uri = uri.to_string(), \"publish bacon diagnostics\");\n        let locations_file_name = config.locations_file.clone();\n        runtime.diagnostics_version = runtime.diagnostics_version.wrapping_add(1);\n        let version = runtime.diagnostics_version;\n        drop(guard);\n        Bacon::publish_diagnostics(\n            &self.client,\n            uri,\n            &locations_file_name,\n            workspace_folders.as_deref(),\n            version,\n        )\n        .await;\n    }\n\n    async fn synchronize_diagnostics(state: Arc<RwLock<State>>, client: Arc<Client>) {\n        Bacon::synchronize_diagnostics(state, client).await;\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn test_can_configure_tracing() {\n        // Direct the test's log file into a tempdir so we don't clobber the\n        // developer's `bacon-ls.log` in the workspace root (which is what\n        // `cargo run` / a live editor session writes to).\n        let tmp = tempfile::tempdir().expect(\"tempdir\");\n        let log_path = tmp.path().join(\"bacon-ls.log\");\n        BaconLs::configure_tracing(Some(\"info\".to_string()), Some(&log_path));\n    }\n\n    #[test]\n    fn test_path_to_file_uri_plain_ascii() {\n        let uri = path_to_file_uri(\"/home/me/src/lib.rs\");\n        assert_eq!(uri, \"file:///home/me/src/lib.rs\");\n        let parsed = uri.parse::<Uri>().expect(\"must parse as Uri\");\n        assert_eq!(parsed.path().as_str(), \"/home/me/src/lib.rs\");\n    }\n\n    #[test]\n    fn test_path_to_file_uri_escapes_space_and_hash_and_percent() {\n        let uri = path_to_file_uri(\"/home/me/My Projects/tests#1/file%.rs\");\n        assert_eq!(uri, \"file:///home/me/My%20Projects/tests%231/file%25.rs\");\n        let parsed = uri.parse::<Uri>().expect(\"must parse as Uri\");\n        // Uri preserves the encoded form on the wire; clients are responsible\n        // for decoding. We only need to confirm the parse succeeds.\n        assert_eq!(parsed.path().as_str(), \"/home/me/My%20Projects/tests%231/file%25.rs\");\n    }\n\n    #[test]\n    fn test_path_to_file_uri_preserves_path_separators() {\n        // The `/` separator must NOT be encoded, or clients can't recognize\n        // segment structure.\n        let uri = path_to_file_uri(\"/a/b/c\");\n        assert_eq!(uri, \"file:///a/b/c\");\n    }\n\n    #[test]\n    fn test_path_to_file_uri_relative_path_preserves_segments() {\n        // Cargo emits relative paths (e.g. \"src/lib.rs\") in JSON output. The\n        // current `deserialize_url` hack turns those into URIs with the first\n        // segment as \"host\" — percent-encoding must not break that.\n        let uri = path_to_file_uri(\"src/lib.rs\");\n        assert_eq!(uri, \"file://src/lib.rs\");\n        let parsed = uri.parse::<Uri>().expect(\"must parse as Uri\");\n        assert_eq!(\n            parsed.authority().map(|a| a.host().to_string()),\n            Some(\"src\".to_string())\n        );\n        assert_eq!(parsed.path().as_str(), \"/lib.rs\");\n    }\n\n    #[test]\n    fn test_cancel_mode_replaces_token() {\n        let original = CancellationToken::new();\n        let token = original.clone();\n        token.cancel();\n        assert!(original.is_cancelled());\n        let new_token = CancellationToken::new();\n        assert!(!new_token.is_cancelled());\n    }\n\n    #[test]\n    fn test_detect_backend_explicit_cargo() {\n        let values: Map<String, Value> = serde_json::from_str(r#\"{\"backend\": \"cargo\"}\"#).unwrap();\n        assert_eq!(BaconLs::detect_backend(&values).unwrap(), BackendChoice::Cargo);\n    }\n\n    #[test]\n    fn test_detect_backend_explicit_bacon() {\n        let values: Map<String, Value> = serde_json::from_str(r#\"{\"backend\": \"bacon\"}\"#).unwrap();\n        assert_eq!(BaconLs::detect_backend(&values).unwrap(), BackendChoice::Bacon);\n    }\n\n    #[test]\n    fn test_detect_backend_invalid_value() {\n        let values: Map<String, Value> = serde_json::from_str(r#\"{\"backend\": \"invalid\"}\"#).unwrap();\n        assert!(BaconLs::detect_backend(&values).is_err());\n    }\n\n    #[test]\n    fn test_detect_backend_infer_from_cargo_key() {\n        let values: Map<String, Value> = serde_json::from_str(r#\"{\"cargo\": {\"command\": \"check\"}}\"#).unwrap();\n        assert_eq!(BaconLs::detect_backend(&values).unwrap(), BackendChoice::Cargo);\n    }\n\n    #[test]\n    fn test_detect_backend_infer_from_bacon_key() {\n        let values: Map<String, Value> =\n            serde_json::from_str(r#\"{\"bacon\": {\"locationsFile\": \".bacon-locations\"}}\"#).unwrap();\n        assert_eq!(BaconLs::detect_backend(&values).unwrap(), BackendChoice::Bacon);\n    }\n\n    #[test]\n    fn test_detect_backend_both_keys_error() {\n        let values: Map<String, Value> = serde_json::from_str(r#\"{\"cargo\": {}, \"bacon\": {}}\"#).unwrap();\n        assert!(BaconLs::detect_backend(&values).is_err());\n    }\n\n    #[test]\n    fn test_detect_backend_no_keys_defaults_to_cargo() {\n        let values: Map<String, Value> = serde_json::from_str(r#\"{}\"#).unwrap();\n        assert_eq!(BaconLs::detect_backend(&values).unwrap(), BackendChoice::Cargo);\n    }\n\n    #[test]\n    fn test_detect_backend_explicit_overrides_keys() {\n        let values: Map<String, Value> = serde_json::from_str(r#\"{\"backend\": \"cargo\", \"bacon\": {}}\"#).unwrap();\n        assert_eq!(BaconLs::detect_backend(&values).unwrap(), BackendChoice::Cargo);\n    }\n\n    #[test]\n    fn test_cargo_options_build_args_default() {\n        let args = CargoOptions::default().build_command_args();\n        assert_eq!(args, vec![\"check\", \"--message-format=json-diagnostic-rendered-ansi\"]);\n    }\n\n    #[test]\n    fn test_cargo_options_build_args_with_features() {\n        let opts = CargoOptions {\n            features: CargoFeatures::List(vec![\"a\".into(), \"b\".into(), \"c\".into()]),\n            ..CargoOptions::default()\n        };\n        let args = opts.build_command_args();\n        assert_eq!(\n            args,\n            vec![\n                \"check\",\n                \"--message-format=json-diagnostic-rendered-ansi\",\n                \"--features\",\n                \"a,b,c\"\n            ]\n        );\n    }\n\n    #[test]\n    fn test_cargo_options_build_args_single_feature() {\n        let opts = CargoOptions {\n            features: CargoFeatures::List(vec![\"only\".into()]),\n            ..CargoOptions::default()\n        };\n        let args = opts.build_command_args();\n        assert_eq!(\n            args,\n            vec![\n                \"check\",\n                \"--message-format=json-diagnostic-rendered-ansi\",\n                \"--features\",\n                \"only\"\n            ]\n        );\n    }\n\n    #[test]\n    fn test_cargo_options_build_args_with_all_features() {\n        let opts = CargoOptions {\n            features: CargoFeatures::All,\n            ..CargoOptions::default()\n        };\n        let args = opts.build_command_args();\n        assert_eq!(\n            args,\n            vec![\n                \"check\",\n                \"--message-format=json-diagnostic-rendered-ansi\",\n                \"--all-features\",\n            ]\n        );\n    }\n\n    #[test]\n    fn test_cargo_options_build_args_with_package_and_extras() {\n        let opts = CargoOptions {\n            command: \"clippy\".into(),\n            package: Some(\"my-crate\".into()),\n            extra_command_args: vec![\"--workspace\".into(), \"--all-targets\".into()],\n            ..CargoOptions::default()\n        };\n        let args = opts.build_command_args();\n        assert_eq!(\n            args,\n            vec![\n                \"clippy\",\n                \"--message-format=json-diagnostic-rendered-ansi\",\n                \"-p\",\n                \"my-crate\",\n                \"--workspace\",\n                \"--all-targets\",\n            ]\n        );\n    }\n\n    #[test]\n    fn test_cargo_options_update_from_json_full_roundtrip() {\n        let mut opts = CargoOptions::default();\n        let json = serde_json::json!({\n            \"command\": \"clippy\",\n            \"features\": [\"a\", \"b\"],\n            \"package\": \"pkg\",\n            \"extraArgs\": [\"--workspace\"],\n            \"env\": {\"RUST_LOG\": \"trace\"},\n            \"cancelRunning\": false,\n            \"refreshIntervalSeconds\": 10,\n            \"separateChildDiagnostics\": true,\n            \"checkOnSave\": false,\n            \"clearDiagnosticsOnCheck\": true,\n            \"updateOnInsertDebounceMillis\": 250,\n        });\n        let obj = json.as_object().unwrap();\n        opts.update_from_json_obj(obj).expect(\"should parse\");\n        assert_eq!(opts.command, \"clippy\");\n        assert_eq!(\n            opts.features,\n            CargoFeatures::List(vec![\"a\".to_string(), \"b\".to_string()])\n        );\n        assert_eq!(opts.package.as_deref(), Some(\"pkg\"));\n        assert_eq!(opts.extra_command_args, vec![\"--workspace\".to_string()]);\n        assert_eq!(opts.env, vec![(\"RUST_LOG\".into(), \"trace\".into())]);\n        assert!(matches!(opts.publish_mode, PublishMode::QueueIfRunning));\n        assert_eq!(opts.refresh_interval_seconds, Some(Duration::from_secs(10)));\n        assert_eq!(opts.separate_child_diagnostics, Some(true));\n        assert!(!opts.check_on_save);\n        assert!(opts.clear_diagnostics_on_check);\n        assert_eq!(opts.update_on_insert_debounce, Duration::from_millis(250));\n    }\n\n    #[test]\n    fn test_cargo_options_update_on_insert_defaults_off() {\n        let opts = CargoOptions::default();\n        assert!(!opts.update_on_insert);\n        assert_eq!(opts.update_on_insert_debounce, Duration::from_millis(500));\n    }\n\n    #[test]\n    fn test_cargo_options_update_on_insert_debounce_rejects_negative() {\n        let mut opts = CargoOptions::default();\n        let json = serde_json::json!({\"updateOnInsertDebounceMillis\": -50});\n        assert!(opts.update_from_json_obj(json.as_object().unwrap()).is_err());\n    }\n\n    #[test]\n    fn test_cargo_options_update_from_json_refresh_null_means_no_partial() {\n        let mut opts = CargoOptions::default();\n        let json = serde_json::json!({\"refreshIntervalSeconds\": null});\n        opts.update_from_json_obj(json.as_object().unwrap()).unwrap();\n        assert_eq!(opts.refresh_interval_seconds, None);\n    }\n\n    #[test]\n    fn test_cargo_options_update_from_json_refresh_negative_means_no_partial() {\n        let mut opts = CargoOptions::default();\n        let json = serde_json::json!({\"refreshIntervalSeconds\": -1});\n        opts.update_from_json_obj(json.as_object().unwrap()).unwrap();\n        assert_eq!(opts.refresh_interval_seconds, None);\n    }\n\n    #[test]\n    fn test_cargo_options_update_from_json_rejects_wrong_type() {\n        let mut opts = CargoOptions::default();\n        let json = serde_json::json!({\"command\": 42});\n        assert!(opts.update_from_json_obj(json.as_object().unwrap()).is_err());\n    }\n\n    #[test]\n    fn test_cargo_options_update_from_json_partial_leaves_others_unchanged() {\n        let mut opts = CargoOptions {\n            command: \"clippy\".into(),\n            ..CargoOptions::default()\n        };\n        let json = serde_json::json!({\"checkOnSave\": false});\n        opts.update_from_json_obj(json.as_object().unwrap()).unwrap();\n        assert_eq!(opts.command, \"clippy\");\n        assert!(!opts.check_on_save);\n    }\n\n    #[test]\n    fn test_cargo_options_reset_restores_defaults() {\n        let mut opts = CargoOptions {\n            command: \"clippy\".into(),\n            features: CargoFeatures::List(vec![\"foo\".into()]),\n            check_on_save: false,\n            ..CargoOptions::default()\n        };\n        opts.reset();\n        let defaults = CargoOptions::default();\n        assert_eq!(opts.command, defaults.command);\n        assert_eq!(opts.features, defaults.features);\n        assert_eq!(opts.check_on_save, defaults.check_on_save);\n    }\n\n    #[test]\n    fn test_bacon_options_update_from_json_full_roundtrip() {\n        let mut opts = BaconOptions::default();\n        let json = serde_json::json!({\n            \"locationsFile\": \"custom.locations\",\n            \"runInBackground\": false,\n            \"runInBackgroundCommand\": \"/usr/local/bin/bacon\",\n            \"runInBackgroundCommandArguments\": \"--headless -j custom\",\n            \"validatePreferences\": false,\n            \"createPreferencesFile\": false,\n            \"synchronizeAllOpenFilesWaitMillis\": 500,\n            \"updateOnSave\": false,\n            \"updateOnSaveWaitMillis\": 250,\n        });\n        opts.update_from_json_obj(json.as_object().unwrap()).unwrap();\n        assert_eq!(opts.locations_file, \"custom.locations\");\n        assert!(!opts.run_in_background);\n        assert_eq!(opts.run_in_background_command, \"/usr/local/bin/bacon\");\n        assert_eq!(opts.run_in_background_command_args, \"--headless -j custom\");\n        assert!(!opts.validate_preferences);\n        assert!(!opts.create_preferences_file);\n        assert_eq!(opts.synchronize_all_open_files_wait, Duration::from_millis(500));\n        assert!(!opts.update_on_save);\n        assert_eq!(opts.update_on_save_wait, Duration::from_millis(250));\n    }\n\n    #[test]\n    fn test_bacon_options_update_from_json_rejects_wrong_type() {\n        let mut opts = BaconOptions::default();\n        let json = serde_json::json!({\"runInBackground\": \"yes\"});\n        assert!(opts.update_from_json_obj(json.as_object().unwrap()).is_err());\n    }\n\n    #[test]\n    fn test_bacon_options_reset_restores_defaults() {\n        let mut opts = BaconOptions {\n            run_in_background: false,\n            locations_file: \"foo\".into(),\n            ..BaconOptions::default()\n        };\n        opts.reset();\n        let defaults = BaconOptions::default();\n        assert_eq!(opts.run_in_background, defaults.run_in_background);\n        assert_eq!(opts.locations_file, defaults.locations_file);\n    }\n\n    #[test]\n    fn test_correction_from_single_empty_is_remove() {\n        let range = Range::default();\n        let c = Correction::from_single(range, \"\");\n        assert_eq!(c.label, \"Remove\");\n        assert_eq!(c.edits.len(), 1);\n        assert_eq!(c.edits[0].new_text, \"\");\n    }\n\n    #[test]\n    fn test_correction_from_single_nonempty_is_replace() {\n        let range = Range::default();\n        let c = Correction::from_single(range, \"foo\");\n        assert_eq!(c.label, \"Replace with: foo\");\n        assert_eq!(c.edits.len(), 1);\n    }\n\n    #[test]\n    fn test_correction_from_multi_all_empty_is_remove() {\n        let edits = vec![\n            CorrectionEdit {\n                range: Range::default(),\n                new_text: \"\".into(),\n            },\n            CorrectionEdit {\n                range: Range::default(),\n                new_text: \"\".into(),\n            },\n        ];\n        let c = Correction::from_multi(edits);\n        assert_eq!(c.label, \"Remove\");\n        assert_eq!(c.edits.len(), 2);\n    }\n\n    #[test]\n    fn test_correction_from_multi_labels_by_first_nonempty() {\n        let edits = vec![\n            CorrectionEdit {\n                range: Range::default(),\n                new_text: \"\".into(),\n            },\n            CorrectionEdit {\n                range: Range::default(),\n                new_text: \"new\".into(),\n            },\n        ];\n        let c = Correction::from_multi(edits);\n        assert_eq!(c.label, \"Replace with: new\");\n    }\n\n    #[test]\n    fn test_severity_tag_distinguishes_levels() {\n        assert_eq!(severity_tag(None), 0);\n        assert_eq!(severity_tag(Some(DiagnosticSeverity::ERROR)), 1);\n        assert_eq!(severity_tag(Some(DiagnosticSeverity::WARNING)), 2);\n        assert_eq!(severity_tag(Some(DiagnosticSeverity::INFORMATION)), 3);\n        assert_eq!(severity_tag(Some(DiagnosticSeverity::HINT)), 4);\n        // All four constants must hash to distinct tags or dedup will fold\n        // legitimately-different diagnostics together.\n        let tags = [\n            severity_tag(Some(DiagnosticSeverity::ERROR)),\n            severity_tag(Some(DiagnosticSeverity::WARNING)),\n            severity_tag(Some(DiagnosticSeverity::INFORMATION)),\n            severity_tag(Some(DiagnosticSeverity::HINT)),\n        ];\n        let unique: HashSet<_> = tags.iter().collect();\n        assert_eq!(unique.len(), tags.len());\n    }\n\n    #[test]\n    fn test_diag_key_collides_for_equal_diagnostics() {\n        let a = Diagnostic {\n            range: Range::default(),\n            severity: Some(DiagnosticSeverity::ERROR),\n            message: \"hi\".into(),\n            ..Diagnostic::default()\n        };\n        let b = a.clone();\n        assert_eq!(diag_key(&a), diag_key(&b));\n    }\n\n    #[test]\n    fn test_diag_key_differs_when_message_differs() {\n        let mut a = Diagnostic {\n            range: Range::default(),\n            severity: Some(DiagnosticSeverity::ERROR),\n            message: \"first\".into(),\n            ..Diagnostic::default()\n        };\n        let b = a.clone();\n        a.message = \"second\".into();\n        assert_ne!(diag_key(&a), diag_key(&b));\n    }\n\n    #[test]\n    fn test_path_to_file_uri_empty_path() {\n        // Empty path yields the trivial `file://` URI. Useful guard against\n        // future regressions in the encoding helper when fed degenerate input.\n        assert_eq!(path_to_file_uri(\"\"), \"file://\");\n    }\n\n    #[test]\n    fn test_correction_from_single_label_replaces_with_text() {\n        let c = Correction::from_single(Range::default(), \"x\");\n        assert_eq!(c.label, \"Replace with: x\");\n        assert_eq!(c.edits.len(), 1);\n        assert_eq!(c.edits[0].new_text, \"x\");\n    }\n\n    #[test]\n    fn test_correction_from_multi_empty_edits_is_remove() {\n        let c = Correction::from_multi(vec![]);\n        assert_eq!(c.label, \"Remove\");\n        assert!(c.edits.is_empty());\n    }\n\n    #[test]\n    fn test_cargo_options_env_roundtrip_preserves_order_in_serde_iteration() {\n        // serde_json::Map preserves insertion order. We rely on that for\n        // reproducible env propagation into cargo.\n        let mut opts = CargoOptions::default();\n        let json = serde_json::json!({\n            \"env\": {\"A\": \"1\", \"B\": \"2\", \"C\": \"3\"}\n        });\n        opts.update_from_json_obj(json.as_object().unwrap()).unwrap();\n        assert_eq!(opts.env.len(), 3);\n        let keys: Vec<_> = opts.env.iter().map(|(k, _)| k.as_str()).collect();\n        assert_eq!(keys, vec![\"A\", \"B\", \"C\"]);\n    }\n\n    #[test]\n    fn test_cargo_options_update_rejects_non_object_env() {\n        let mut opts = CargoOptions::default();\n        let json = serde_json::json!({\"env\": [\"A=1\"]});\n        assert!(opts.update_from_json_obj(json.as_object().unwrap()).is_err());\n    }\n\n    #[test]\n    fn test_cargo_options_update_rejects_non_string_env_value() {\n        let mut opts = CargoOptions::default();\n        let json = serde_json::json!({\"env\": {\"A\": 1}});\n        assert!(opts.update_from_json_obj(json.as_object().unwrap()).is_err());\n    }\n\n    #[test]\n    fn test_cargo_options_update_rejects_non_string_feature_item() {\n        let mut opts = CargoOptions::default();\n        let json = serde_json::json!({\"features\": [\"a\", 2, \"c\"]});\n        assert!(opts.update_from_json_obj(json.as_object().unwrap()).is_err());\n    }\n\n    #[test]\n    fn test_cargo_options_publish_mode_toggle_via_cancel_running() {\n        let mut opts = CargoOptions::default();\n        // Default is CancelRunning.\n        assert!(matches!(opts.publish_mode, PublishMode::CancelRunning));\n        opts.update_from_json_obj(serde_json::json!({\"cancelRunning\": false}).as_object().unwrap())\n            .unwrap();\n        assert!(matches!(opts.publish_mode, PublishMode::QueueIfRunning));\n        opts.update_from_json_obj(serde_json::json!({\"cancelRunning\": true}).as_object().unwrap())\n            .unwrap();\n        assert!(matches!(opts.publish_mode, PublishMode::CancelRunning));\n    }\n\n    #[test]\n    fn test_cargo_options_separate_child_diagnostics_can_unset() {\n        let mut opts = CargoOptions {\n            separate_child_diagnostics: Some(true),\n            ..CargoOptions::default()\n        };\n        // `as_bool()` on a non-bool returns None — and we feed that through\n        // unchanged, so a `null` (or anything non-bool) clears the override.\n        opts.update_from_json_obj(\n            serde_json::json!({\"separateChildDiagnostics\": null})\n                .as_object()\n                .unwrap(),\n        )\n        .unwrap();\n        assert_eq!(opts.separate_child_diagnostics, None);\n    }\n\n    #[tokio::test]\n    async fn test_find_git_root_directory_returns_none_outside_git() {\n        let tmp = tempfile::TempDir::new().unwrap();\n        let root = BaconLs::find_git_root_directory(tmp.path()).await;\n        assert_eq!(root, None);\n    }\n\n    #[tokio::test]\n    async fn test_find_git_root_directory_finds_top_of_repo() {\n        // `git -C <subdir> rev-parse --show-toplevel` should resolve to the\n        // crate's own repo root regardless of which subdirectory we point at.\n        let crate_root = std::path::Path::new(env!(\"CARGO_MANIFEST_DIR\"));\n        let src = crate_root.join(\"src\");\n        let from_subdir = BaconLs::find_git_root_directory(&src).await;\n        assert!(from_subdir.is_some(), \"src/ is inside a git repo\");\n        let from_root = BaconLs::find_git_root_directory(crate_root).await.unwrap();\n        // Both lookups should resolve to the same toplevel.\n        assert_eq!(from_subdir.unwrap(), from_root);\n    }\n\n    #[test]\n    fn test_init_cargo_backend_uses_existing_project_root() {\n        let tmp = tempfile::TempDir::new().unwrap();\n        let root = tmp.path().to_path_buf();\n        let mut state = State {\n            project_root: Some(root.clone()),\n            ..State::default()\n        };\n        // Normally we'd hold an RwLockWriteGuard, but for this unit test we\n        // adapt the API by going through a real lock.\n        let lock = RwLock::new(std::mem::take(&mut state));\n        let mut guard = lock.try_write().unwrap();\n        BaconLs::init_cargo_backend(&mut guard, CargoOptions::default())\n            .expect(\"init should succeed with explicit project root\");\n        match &guard.backend {\n            Some(BackendRuntime::Cargo { runtime, .. }) => {\n                assert_eq!(runtime.build_folder, root);\n                assert_eq!(runtime.run_state, CargoRunState::Idle);\n                assert_eq!(runtime.diagnostics_version, 0);\n            }\n            other => panic!(\"expected Cargo backend, got {other:?}\"),\n        }\n    }\n\n    #[test]\n    fn test_init_cargo_backend_falls_back_to_cwd_when_no_project_root() {\n        let mut state = State::default();\n        let lock = RwLock::new(std::mem::take(&mut state));\n        let mut guard = lock.try_write().unwrap();\n        BaconLs::init_cargo_backend(&mut guard, CargoOptions::default())\n            .expect(\"init should fall back to CWD when project root is unset\");\n        match &guard.backend {\n            Some(BackendRuntime::Cargo { runtime, .. }) => {\n                let cwd = std::env::current_dir().unwrap();\n                assert_eq!(runtime.build_folder, cwd, \"should fall back to CWD\");\n            }\n            other => panic!(\"expected Cargo backend, got {other:?}\"),\n        }\n    }\n\n    #[test]\n    fn test_cargo_options_build_args_with_env_does_not_leak_into_args() {\n        // Sanity: env values are not added as command-line args.\n        let opts = CargoOptions {\n            env: vec![(\"A\".into(), \"1\".into())],\n            ..CargoOptions::default()\n        };\n        let args = opts.build_command_args();\n        assert!(args.iter().all(|a| !a.contains(\"A=1\") && !a.contains(\"=1\")));\n    }\n}\n"
  },
  {
    "path": "src/lsp.rs",
    "content": "use std::collections::HashMap;\n\nuse ls_types::{\n    CodeAction, CodeActionKind, CodeActionOptions, CodeActionOrCommand, CodeActionParams, CodeActionProviderCapability,\n    CodeActionResponse, DeleteFilesParams, DidChangeConfigurationParams, DidChangeTextDocumentParams,\n    DidCloseTextDocumentParams, DidOpenTextDocumentParams, DidSaveTextDocumentParams, ExecuteCommandOptions,\n    ExecuteCommandParams, FileOperationFilter, FileOperationPattern, FileOperationRegistrationOptions,\n    InitializeParams, InitializeResult, InitializedParams, LSPAny, MessageType, PositionEncodingKind,\n    PublishDiagnosticsClientCapabilities, RenameFilesParams, ServerCapabilities, ServerInfo,\n    TextDocumentClientCapabilities, TextDocumentSyncCapability, TextDocumentSyncKind, TextDocumentSyncOptions,\n    TextDocumentSyncSaveOptions, TextEdit, Uri, WorkDoneProgressOptions, WorkspaceEdit,\n    WorkspaceFileOperationsServerCapabilities, WorkspaceServerCapabilities,\n};\nuse tower_lsp_server::{LanguageServer, jsonrpc};\n\nuse crate::{\n    BackendChoice, BackendRuntime, BaconLs, Cargo, CargoOptions, CorrectionEdit, DiagnosticData, PKG_NAME, PKG_VERSION,\n};\n\nimpl LanguageServer for BaconLs {\n    async fn initialize(&self, params: InitializeParams) -> jsonrpc::Result<InitializeResult> {\n        tracing::info!(\"initializing {PKG_NAME} v{PKG_VERSION}\",);\n        tracing::debug!(\"initializing with input parameters: {params:#?}\");\n        let project_root = Cargo::find_project_root(&params).await;\n        tracing::debug!(\"Found project root: {project_root:?}\");\n\n        if let Some(TextDocumentClientCapabilities {\n            publish_diagnostics: Some(PublishDiagnosticsClientCapabilities { .. }),\n            ..\n        }) = params.capabilities.text_document\n        {\n            tracing::info!(\"client supports diagnostics\");\n        } else {\n            tracing::warn!(\"client does not support diagnostics\");\n            return Err(jsonrpc::Error::new(jsonrpc::ErrorCode::InvalidRequest));\n        }\n\n        let mut diagnostics_data_supported = false;\n        let mut related_information_supported = false;\n        if let Some(TextDocumentClientCapabilities {\n            publish_diagnostics:\n                Some(PublishDiagnosticsClientCapabilities {\n                    data_support,\n                    related_information,\n                    ..\n                }),\n            ..\n        }) = params.capabilities.text_document\n        {\n            if data_support == Some(true) {\n                tracing::info!(\"client supports diagnostics data\");\n                diagnostics_data_supported = true;\n            } else {\n                tracing::warn!(\"client does not support diagnostics data\");\n            }\n            if related_information == Some(true) {\n                tracing::info!(\"client supports related information\");\n                related_information_supported = true;\n            } else {\n                tracing::info!(\"client does not support related information\");\n            }\n        } else {\n            tracing::warn!(\"client does not support diagnostics data\");\n        }\n\n        // Initialization options are the only place we can read user\n        // configuration before responding to `initialize`. We need that for\n        // `cargo.updateOnInsert`: the LSP capability `textDocument/didChange`\n        // sync mode has to be advertised statically — clients (Neovim\n        // included) don't reliably retrofit already-attached buffers when we\n        // try to register it dynamically post-`initialized`.\n        let init_update_on_insert = params\n            .initialization_options\n            .as_ref()\n            .and_then(|v| v.get(\"cargo\"))\n            .and_then(|v| v.get(\"updateOnInsert\"))\n            .and_then(|v| v.as_bool())\n            .unwrap_or(false);\n        if init_update_on_insert {\n            tracing::info!(\n                \"initialization_options.cargo.updateOnInsert = true; advertising textDocument/didChange (Full sync)\"\n            );\n        }\n\n        let mut state = self.state.write().await;\n        state.project_root = project_root;\n        state.workspace_folders = params.workspace_folders;\n        state.diagnostics_data_supported = diagnostics_data_supported;\n        state.related_information_supported = related_information_supported;\n        state.init_update_on_insert = init_update_on_insert;\n        tracing::trace!(\"loaded state from lsp settings: {state:#?}\");\n        drop(state);\n\n        // Declare didDelete/didRename so clients actually send those events\n        // (handlers live in this file). The bacon backend tracks open files\n        // and needs these to keep its set in sync when the user renames/deletes\n        // through the file explorer. Cargo backend is unaffected but the\n        // capability is cheap to advertise.\n        let rust_file_filter = FileOperationFilter {\n            scheme: Some(\"file\".to_string()),\n            pattern: FileOperationPattern {\n                glob: \"**/*.rs\".to_string(),\n                matches: None,\n                options: None,\n            },\n        };\n        let file_ops_registration = FileOperationRegistrationOptions {\n            filters: vec![rust_file_filter],\n        };\n        Ok(InitializeResult {\n            capabilities: ServerCapabilities {\n                // Only support UTF-16 positions for now, which is the default when unspecified\n                position_encoding: Some(PositionEncodingKind::UTF16),\n                // Default: no change events — diagnostics come from bacon's\n                // locations file or from cargo's JSON output. The cargo\n                // backend's `updateOnInsert` mode flips this to Full when the\n                // user opts in via `initialization_options.cargo.updateOnInsert`,\n                // so non-users never pay for buffer-shipping.\n                text_document_sync: Some(TextDocumentSyncCapability::Options(TextDocumentSyncOptions {\n                    open_close: Some(true),\n                    change: Some(if init_update_on_insert {\n                        TextDocumentSyncKind::FULL\n                    } else {\n                        TextDocumentSyncKind::NONE\n                    }),\n                    save: Some(TextDocumentSyncSaveOptions::Supported(true)),\n                    ..Default::default()\n                })),\n                code_action_provider: Some(CodeActionProviderCapability::Options(CodeActionOptions {\n                    code_action_kinds: Some(vec![CodeActionKind::QUICKFIX]),\n                    work_done_progress_options: WorkDoneProgressOptions {\n                        work_done_progress: Some(false),\n                    },\n                    resolve_provider: None,\n                })),\n                execute_command_provider: Some(ExecuteCommandOptions {\n                    commands: vec![\"bacon_ls.run\".to_string()],\n                    ..Default::default()\n                }),\n                workspace: Some(WorkspaceServerCapabilities {\n                    workspace_folders: None,\n                    file_operations: Some(WorkspaceFileOperationsServerCapabilities {\n                        did_rename: Some(file_ops_registration.clone()),\n                        did_delete: Some(file_ops_registration),\n                        ..Default::default()\n                    }),\n                }),\n                ..Default::default()\n            },\n            server_info: Some(ServerInfo {\n                name: PKG_NAME.to_string(),\n                version: Some(PKG_VERSION.to_string()),\n            }),\n            // See <https://clangd.llvm.org/extensions.html#utf-8-offsets>.\n            // which says:\n            // ```\n            // This extension has been deprecated with clangd-21 in favor of\n            // the positionEncoding introduced in LSP 3.17. It’ll go away with clangd-23\n            // ```\n            // So None should be fine\n            offset_encoding: None,\n        })\n    }\n\n    async fn initialized(&self, _: InitializedParams) {\n        self.pull_configuration().await;\n\n        let mut state = self.state.write().await;\n        if state.backend.is_none() {\n            // No workspace/configuration response (or empty). Still honor\n            // the init-options seed so live mode works for clients that only\n            // provide settings via `initialization_options`.\n            let mut config = CargoOptions::default();\n            if state.init_update_on_insert {\n                config.update_on_insert = true;\n            }\n            if let Err(e) = Self::init_cargo_backend(&mut state, config) {\n                tracing::error!(\"{e}\");\n                drop(state);\n                self.client.show_message(MessageType::ERROR, e).await;\n                return;\n            }\n        }\n        let backend_chosen = state\n            .backend\n            .as_ref()\n            .expect(\"backend initialized above\")\n            .backend_choice();\n        drop(state);\n\n        tracing::info!(\"{PKG_NAME} v{PKG_VERSION} lsp server initialized with backend: {backend_chosen:?}\");\n        self.client\n            .log_message(\n                MessageType::INFO,\n                format!(\"{PKG_NAME} v{PKG_VERSION} lsp server initialized with backend: {backend_chosen:?}\"),\n            )\n            .await;\n\n        tracing::info!(\"initialized complete\");\n\n        if backend_chosen == BackendChoice::Cargo {\n            tracing::info!(\"triggering initial cargo diagnostics\");\n            self.publish_cargo_diagnostics().await\n        }\n    }\n\n    async fn did_change_configuration(&self, params: DidChangeConfigurationParams) {\n        tracing::info!(\"client sent didChangeConfiguration\");\n        if let Some(settings) = params.settings.as_object()\n            && !settings.is_empty()\n        {\n            if let Some(settings) = settings.get(\"bacon_ls\") {\n                tracing::debug!(\"using client provided settings\");\n                self.adapt_to_settings(settings).await;\n            }\n        } else {\n            tracing::debug!(\"settings is either not an object or is empty\");\n            self.pull_configuration().await;\n        }\n    }\n\n    async fn did_open(&self, params: DidOpenTextDocumentParams) {\n        tracing::trace!(\"client sent didOpen request\");\n        let mut state = self.state.write().await;\n        match &mut state.backend {\n            Some(BackendRuntime::Bacon { runtime, .. }) => {\n                runtime.open_files.insert(params.text_document.uri.clone());\n                drop(state);\n                self.publish_bacon_diagnostics(&params.text_document.uri).await;\n            }\n            Some(BackendRuntime::Cargo { runtime, .. }) => {\n                // Debounce against the initial cargo run: on client startup,\n                // `initialized` kicks off a run and the first `didOpen`\n                // arrives in the same flurry. Skipping here lets the in-flight\n                // run complete instead of being cancelled and restarted.\n                if let Some(ts) = runtime.last_run_started\n                    && ts.elapsed() < std::time::Duration::from_secs(1)\n                {\n                    tracing::trace!(\"did_open within debounce window of last cargo trigger; skipping\");\n                    return;\n                }\n                drop(state);\n                self.publish_cargo_diagnostics().await;\n            }\n            None => {}\n        }\n    }\n\n    async fn did_close(&self, params: DidCloseTextDocumentParams) {\n        tracing::trace!(\"client sent didClose request\");\n        let mut state = self.state.write().await;\n        if let Some(BackendRuntime::Bacon { runtime, .. }) = &mut state.backend {\n            runtime.open_files.remove(&params.text_document.uri);\n            drop(state);\n            self.publish_bacon_diagnostics(&params.text_document.uri).await;\n            return;\n        }\n        drop(state);\n        // Cargo backend with live shadow: revert any dirty buffer for the\n        // closed file back to a hardlink so subsequent live runs read the\n        // on-disk version.\n        self.restore_shadow_link_if_dirty(&params.text_document.uri).await;\n    }\n\n    async fn did_save(&self, params: DidSaveTextDocumentParams) {\n        tracing::debug!(\"client sent didSave request\");\n        let state = self.state.read().await;\n        let Some(backend) = &state.backend else {\n            return;\n        };\n        match backend {\n            BackendRuntime::Bacon { config, .. } => {\n                if config.update_on_save {\n                    if !config.update_on_save_wait.is_zero() {\n                        tokio::time::sleep(config.update_on_save_wait).await;\n                    }\n                    drop(state);\n                    self.publish_bacon_diagnostics(&params.text_document.uri).await;\n                }\n            }\n            BackendRuntime::Cargo { config, .. } => {\n                let check_on_save = config.check_on_save;\n                drop(state);\n                // A pending live run would race with the canonical save run\n                // and publish stale (pre-save) shadow diagnostics on top.\n                // Cancel it before doing anything else.\n                self.cancel_live_debounce().await;\n                // Save makes the shadow's dirty override stale: the on-disk\n                // file now matches what the user wants checked. Restore the\n                // hardlink before the cargo run so the live target dir picks\n                // up the saved content next time it's used.\n                self.restore_shadow_link_if_dirty(&params.text_document.uri).await;\n                if check_on_save {\n                    self.publish_cargo_diagnostics().await;\n                }\n            }\n        }\n    }\n\n    async fn did_change(&self, params: DidChangeTextDocumentParams) {\n        // Live mode is only meaningful for the cargo backend; the bacon\n        // backend reads diagnostics from a file written by an external bacon\n        // process. Bail early to keep this hot path cheap when disabled.\n        let live_on = {\n            let state = self.state.read().await;\n            matches!(\n                &state.backend,\n                Some(BackendRuntime::Cargo { config, .. }) if config.update_on_insert\n            )\n        };\n        if !live_on {\n            tracing::debug!(\"did_change ignored: updateOnInsert is off\");\n            return;\n        }\n        tracing::info!(\n            uri = params.text_document.uri.as_str(),\n            changes = params.content_changes.len(),\n            \"did_change received (live mode)\"\n        );\n\n        // We register the change capability dynamically with `Full` sync\n        // (one entry, range = None, full text). Anything else is a client\n        // mismatch — log and skip rather than guess.\n        let Some(content) = params.content_changes.into_iter().find(|c| c.range.is_none()) else {\n            tracing::warn!(\"did_change without full-sync content; client may not honor dynamic registration\");\n            return;\n        };\n\n        self.live_update_dirty(params.text_document.uri, content.text).await;\n    }\n\n    async fn did_delete_files(&self, params: DeleteFilesParams) {\n        tracing::debug!(\"client sent didDeleteFiles request for {:?}\", params.files);\n        let mut state = self.state.write().await;\n        if let Some(BackendRuntime::Bacon { runtime, .. }) = &mut state.backend {\n            for file in params.files {\n                if let Ok(uri) = str::parse::<Uri>(&file.uri) {\n                    runtime.open_files.remove(&uri);\n                }\n            }\n        }\n        drop(state);\n    }\n\n    async fn did_rename_files(&self, params: RenameFilesParams) {\n        tracing::debug!(\"client sent didRenameFiles request for {:?}\", params.files);\n        for file in params.files {\n            if let (Ok(old_uri), Ok(new_uri)) = (str::parse::<Uri>(&file.old_uri), str::parse::<Uri>(&file.new_uri)) {\n                let mut state = self.state.write().await;\n                if let Some(BackendRuntime::Bacon { runtime, .. }) = &mut state.backend {\n                    runtime.open_files.remove(&old_uri);\n                    runtime.open_files.insert(new_uri.clone());\n                }\n                drop(state);\n                self.publish_bacon_diagnostics(&new_uri).await;\n            }\n        }\n    }\n\n    async fn code_action(&self, params: CodeActionParams) -> jsonrpc::Result<Option<CodeActionResponse>> {\n        tracing::trace!(\"client sent codeActions request\");\n        let state = self.state.read().await;\n        let diagnostics_data_supported = state.diagnostics_data_supported;\n        drop(state);\n\n        if !diagnostics_data_supported {\n            return Ok(None);\n        }\n\n        let bacon_ls = \"bacon-ls\".to_string();\n        let actions = params\n            .context\n            .diagnostics\n            .iter()\n            .filter(|diag| diag.source.as_ref() == Some(&bacon_ls))\n            .flat_map(|diag| match &diag.data {\n                Some(data) => {\n                    if let Ok(DiagnosticData { corrections }) = serde_json::from_value::<DiagnosticData>(data.clone()) {\n                        corrections\n                            .iter()\n                            .map(|c| {\n                                CodeActionOrCommand::CodeAction(CodeAction {\n                                    title: c.label.clone(),\n                                    kind: Some(CodeActionKind::QUICKFIX),\n                                    diagnostics: Some(vec![diag.clone()]),\n                                    edit: Some(WorkspaceEdit {\n                                        changes: Some(HashMap::from([(\n                                            params.text_document.uri.clone(),\n                                            c.edits\n                                                .iter()\n                                                .map(|e: &CorrectionEdit| TextEdit {\n                                                    range: e.range,\n                                                    new_text: e.new_text.clone(),\n                                                })\n                                                .collect(),\n                                        )])),\n                                        ..WorkspaceEdit::default()\n                                    }),\n                                    is_preferred: if corrections.len() == 1 { Some(true) } else { None },\n                                    ..CodeAction::default()\n                                })\n                            })\n                            .collect()\n                    } else {\n                        tracing::error!(\"deserialization failed: received {data:?} as diagnostic data\",);\n                        vec![]\n                    }\n                }\n                None => {\n                    tracing::debug!(\"client doesn't support diagnostic data\");\n                    vec![]\n                }\n            })\n            .collect::<Vec<_>>();\n\n        Ok(Some(actions))\n    }\n\n    async fn execute_command(&self, params: ExecuteCommandParams) -> jsonrpc::Result<Option<LSPAny>> {\n        if params.command == \"bacon_ls.run\" {\n            let state = self.state.read().await;\n            if let Some(BackendRuntime::Cargo { .. }) = state.backend.as_ref() {\n                drop(state);\n                self.publish_cargo_diagnostics().await;\n            }\n            return Ok(None);\n        }\n\n        Err(jsonrpc::Error::method_not_found())\n    }\n\n    async fn shutdown(&self) -> jsonrpc::Result<()> {\n        tracing::info!(\"shutdown requested\");\n        let mut state = self.state.write().await;\n        let backend = state.backend.take();\n        drop(state);\n\n        if let Some(backend) = backend {\n            match backend {\n                BackendRuntime::Bacon { mut runtime, .. } => {\n                    runtime.shutdown_token.cancel();\n                    // Cap each await so a stuck bacon subprocess or watcher can't\n                    // keep the LSP alive past the client's restart deadline.\n                    let deadline = std::time::Duration::from_secs(2);\n                    if let Some(handle) = runtime.command_handle.take() {\n                        tracing::info!(\"terminating bacon from running in background\");\n                        match tokio::time::timeout(deadline, handle).await {\n                            Ok(Ok(())) => {}\n                            Ok(Err(e)) => {\n                                tracing::warn!(\"bacon command task failed during shutdown: {e}\")\n                            }\n                            Err(_) => tracing::warn!(\"bacon command task timed out during shutdown\"),\n                        }\n                    }\n                    let sync_handle = runtime.sync_files_handle;\n                    match tokio::time::timeout(deadline, sync_handle).await {\n                        Ok(Ok(())) => {}\n                        Ok(Err(e)) => tracing::warn!(\"sync files task failed during shutdown: {e}\"),\n                        Err(_) => tracing::warn!(\"sync files task timed out during shutdown\"),\n                    }\n                }\n                BackendRuntime::Cargo { mut runtime, .. } => {\n                    runtime.cancel_token.cancel();\n                    // Abort any pending live debounced trigger so the spawned\n                    // sleep doesn't outlive the server and try to invoke\n                    // cargo against a torn-down backend.\n                    if let Some(handle) = runtime.live_debounce.take() {\n                        handle.abort();\n                    }\n                }\n            }\n        }\n\n        tracing::info!(\"{PKG_NAME} v{PKG_VERSION} lsp server stopped\");\n        self.client\n            .log_message(\n                MessageType::INFO,\n                format!(\"{PKG_NAME} v{PKG_VERSION} lsp server stopped\"),\n            )\n            .await;\n\n        // Force-exit watchdog. An in-flight server-to-client request (e.g. the\n        // `workspace/configuration` we issue from `initialized`) can keep\n        // `Server::serve()` alive past the `exit` notification, because there's\n        // no way to cancel a waiter on a client response that will never\n        // arrive. Without this, `:LspRestart` in Neovim sees the server never\n        // die and gives up on spawning a fresh instance.\n        tokio::spawn(async {\n            tokio::time::sleep(std::time::Duration::from_millis(500)).await;\n            std::process::exit(0);\n        });\n\n        Ok(())\n    }\n}\n"
  },
  {
    "path": "src/main.rs",
    "content": "//! Bacon Language Server\nuse bacon_ls::BaconLs;\n\n#[tokio::main(flavor = \"multi_thread\", worker_threads = 4)]\nasync fn main() {\n    let args: bacon_ls::Args = argh::from_env();\n    if args.version {\n        println!(\"{}\", bacon_ls::PKG_VERSION);\n    } else {\n        BaconLs::serve().await;\n    }\n}\n"
  },
  {
    "path": "src/native.rs",
    "content": "use std::{\n    env,\n    path::{Path, PathBuf},\n    process::Stdio,\n};\n\nuse anyhow::Context;\nuse ls_types::{\n    Diagnostic, DiagnosticRelatedInformation, DiagnosticSeverity, DiagnosticTag, InitializeParams, Location, Position,\n    Range, Uri,\n};\nuse serde::{Deserialize, Deserializer};\nuse tokio::io::{AsyncBufRead, AsyncBufReadExt};\nuse tokio::process::Command;\nuse tower_lsp_server::{Bounded, NotCancellable, OngoingProgress};\n\nuse crate::{BaconLs, Correction, CorrectionEdit, DiagnosticData, PKG_NAME, path_to_file_uri};\n\n/// Like `read_until` but stops at either `\\r` or `\\n`.\n/// Returns the number of bytes read (0 = EOF).\n/// The delimiter byte is consumed but not included in `buf`.\nasync fn read_until_cr_or_lf<R: AsyncBufRead + Unpin>(reader: &mut R, buf: &mut Vec<u8>) -> std::io::Result<usize> {\n    let mut total = 0;\n    loop {\n        let available = reader.fill_buf().await?;\n        if available.is_empty() {\n            return Ok(total);\n        }\n        if let Some(pos) = available.iter().position(|&b| b == b'\\r' || b == b'\\n') {\n            buf.extend_from_slice(&available[..pos]);\n            let consumed = pos + 1;\n            total += consumed;\n            reader.consume(consumed);\n            return Ok(total);\n        }\n        buf.extend_from_slice(available);\n        let len = available.len();\n        total += len;\n        reader.consume(len);\n    }\n}\n\n#[derive(Debug, Deserialize)]\nstruct CargoExpansion {\n    span: CargoSpan,\n}\n\n#[derive(Debug, Deserialize)]\nstruct CargoSpan {\n    #[serde(deserialize_with = \"deserialize_url\")]\n    file_name: Uri,\n    line_start: u32,\n    line_end: u32,\n    column_start: u32,\n    column_end: u32,\n    suggested_replacement: Option<String>,\n    suggestion_applicability: Option<String>,\n    #[serde(default)]\n    is_primary: bool,\n    #[serde(default)]\n    expansion: Option<Box<CargoExpansion>>,\n}\n\nimpl CargoSpan {\n    fn is_machine_applicable(&self) -> bool {\n        self.suggestion_applicability.as_deref() == Some(\"MachineApplicable\")\n    }\n\n    /// Returns the innermost span in the macro expansion chain that points to a\n    /// project-local (relative-path) file. Falls back to `self` when no such\n    /// span exists (e.g. errors purely within a proc-macro).\n    fn project_span(&self) -> &Self {\n        let is_relative = self\n            .file_name\n            .authority()\n            .map(|a| !a.host().is_empty())\n            .unwrap_or(false);\n        if is_relative {\n            return self;\n        }\n        if let Some(exp) = &self.expansion {\n            return exp.span.project_span();\n        }\n        self\n    }\n}\n\nfn deserialize_url<'de, D>(deserializer: D) -> Result<Uri, D::Error>\nwhere\n    D: Deserializer<'de>,\n{\n    let url_str: &str = Deserialize::deserialize(deserializer)?;\n    str::parse::<Uri>(&path_to_file_uri(url_str)).map_err(serde::de::Error::custom)\n}\n\n#[derive(Debug, Deserialize)]\nstruct CargoChildren {\n    message: String,\n    level: String,\n    spans: Vec<CargoSpan>,\n}\n\n#[derive(Debug, Deserialize)]\nstruct CargoCode {\n    code: String,\n}\n\n#[derive(Debug, Deserialize)]\nstruct CargoMessage {\n    #[serde(rename(deserialize = \"$message_type\"))]\n    message_type: String,\n    rendered: String,\n    level: String,\n    spans: Vec<CargoSpan>,\n    children: Vec<CargoChildren>,\n    #[serde(default)]\n    code: Option<CargoCode>,\n}\n\n/// Maps a rustc/clippy lint code to LSP diagnostic tags. Lints that fade in\n/// editors (`unused_*`, `dead_code`, `unreachable_*`) get\n/// `DiagnosticTag::UNNECESSARY`; the `deprecated` lint gets\n/// `DiagnosticTag::DEPRECATED`. Returns `None` for codes that should render\n/// normally (compile errors, clippy lints we don't classify, etc.).\nfn tags_from_code(code: &str) -> Option<Vec<DiagnosticTag>> {\n    let mut tags = Vec::new();\n    if code.starts_with(\"unused_\")\n        || matches!(\n            code,\n            \"dead_code\" | \"unreachable_code\" | \"unreachable_patterns\" | \"redundant_imports\" | \"redundant_semicolons\"\n        )\n    {\n        tags.push(DiagnosticTag::UNNECESSARY);\n    }\n    if code == \"deprecated\" {\n        tags.push(DiagnosticTag::DEPRECATED);\n    }\n    if tags.is_empty() { None } else { Some(tags) }\n}\n\n#[derive(Debug, Deserialize)]\nstruct CargoLine {\n    message: Option<CargoMessage>,\n}\n\n#[derive(Debug, Default)]\npub(crate) struct Cargo;\n\n/// Parses the ouput line from cargo command that looks like:\n/// ```text\n/// Building [====       ] 1/400: thing, thig2\n/// ```\nfn parse_building_line(line: &str) -> Option<(String, u32)> {\n    if !line.starts_with(\"Building\") {\n        return None;\n    }\n    let after_bracket = line.split(\"] \").nth(1)?;\n    let clean: String = after_bracket.chars().filter(|c| !c.is_control()).collect();\n\n    let (fraction, _crates) = clean.split_once(\": \")?;\n    let (n_str, total_str) = fraction.split_once('/')?;\n    let n: u32 = n_str.parse().ok()?;\n    let total: u32 = total_str.parse().ok()?;\n    if total == 0 {\n        return None;\n    }\n    let pct = (n * 100 / total).min(99);\n\n    Some((clean, pct))\n}\n\nimpl Cargo {\n    fn parse_severity(severity_str: &str) -> DiagnosticSeverity {\n        match severity_str {\n            \"error\" => DiagnosticSeverity::ERROR,\n            \"warning\" | \"failure-note\" => DiagnosticSeverity::WARNING,\n            \"info\" | \"information\" | \"note\" => DiagnosticSeverity::INFORMATION,\n            \"hint\" | \"help\" => DiagnosticSeverity::HINT,\n            other => {\n                tracing::warn!(\"unknown cargo severity level {other:?}, defaulting to INFORMATION\");\n                DiagnosticSeverity::INFORMATION\n            }\n        }\n    }\n\n    fn span_to_uri(project_root: Option<&PathBuf>, span: &CargoSpan) -> anyhow::Result<Option<Uri>> {\n        let Some(host) = span.file_name.authority().map(|auth| auth.host()) else {\n            return Ok(None);\n        };\n        let root_dir = match project_root {\n            Some(r) => r.clone(),\n            None => env::current_dir().context(\"getting current dir\")?,\n        };\n        tracing::trace!(?root_dir, ?host, file_name = ?span.file_name, \"building uri\");\n        // If host is empty, the span.file_name is an absolute path.\n        let path = if host.is_empty() {\n            PathBuf::from(span.file_name.path().as_str())\n        } else {\n            let tmp = root_dir.join(host);\n            // For first level paths, e.g., `build.rs`, this ensures that we dont join an\n            // empty string (because `file_name` is empty), creating a non-existent\n            // `build.rs/` directory in the source root, and therefore failing\n            // canonicalization.\n            if span.file_name.path().as_str().is_empty() {\n                tmp\n            } else {\n                tmp.join(span.file_name.path().as_str().replacen(\"/\", \"\", 1))\n            }\n        };\n        // Canonicalization is important, otherwise the file path cannot be compared with the\n        // paths we get passed from the LSP server. A canonicalize failure here means this\n        // single span can't be resolved (e.g. file deleted between cargo emitting and us\n        // reading): skip it rather than aborting the whole diagnostics run.\n        let canonical = match path.canonicalize() {\n            Ok(c) => c,\n            Err(e) => {\n                tracing::warn!(\n                    path = %path.display(),\n                    error = %e,\n                    \"skipping diagnostic span: cannot canonicalize path\"\n                );\n                return Ok(None);\n            }\n        };\n        let file_name = canonical\n            .into_os_string()\n            .into_string()\n            .map_err(|_orig| std::io::Error::other(\"cannot convert file name to string\"))?;\n        Ok(Some(\n            str::parse::<Uri>(&path_to_file_uri(&file_name)).context(\"parsing filename\")?,\n        ))\n    }\n\n    async fn maybe_add_diagnostic(\n        project_root: Option<&PathBuf>,\n        message: &CargoMessage,\n        use_related_information: bool,\n        tx: &flume::Sender<(Uri, Diagnostic)>,\n    ) -> anyhow::Result<bool> {\n        let rendered = ansi_regex::ansi_regex().replace_all(&message.rendered, \"\").into_owned();\n        let rendered = rendered.trim_end_matches('\\n');\n\n        // The lint code lives on the parent message; help/note children inherit\n        // their semantics from it. Propagating the parent's tags to children\n        // keeps the rendering uniform when we emit them as separate\n        // diagnostics on clients that don't support relatedInformation.\n        let tags = message.code.as_ref().and_then(|c| tags_from_code(&c.code));\n\n        // When the client supports related information (e.g. VS Code), attach\n        // children with primary spans to the parent diagnostic as clickable\n        // links. Otherwise emit them as separate diagnostics so editors like\n        // neovim can display them directly.\n        let related_information = if use_related_information {\n            let info: Vec<DiagnosticRelatedInformation> = message\n                .children\n                .iter()\n                .flat_map(|child| {\n                    child\n                        .spans\n                        .iter()\n                        .filter(|s| s.is_primary)\n                        .filter_map(|s| {\n                            Self::span_to_uri(project_root, s.project_span())\n                                .ok()\n                                .flatten()\n                                .map(|uri| DiagnosticRelatedInformation {\n                                    location: Location {\n                                        uri,\n                                        range: Range::new(\n                                            Position::new(\n                                                s.line_start.saturating_sub(1),\n                                                s.column_start.saturating_sub(1),\n                                            ),\n                                            Position::new(s.line_end.saturating_sub(1), s.column_end.saturating_sub(1)),\n                                        ),\n                                    },\n                                    message: child.message.clone(),\n                                })\n                        })\n                        .collect::<Vec<_>>()\n                })\n                .collect();\n            if info.is_empty() { None } else { Some(info) }\n        } else {\n            None\n        };\n\n        // A cargo message can have several primary spans pointing to different\n        // project locations (e.g. conflicting trait impls, lifetime conflicts\n        // across multiple binding sites). We emit a diagnostic for every span\n        // that resolves to a project-local URI; spans that don't (pure-macro\n        // or registry paths) are skipped via the `?` below.\n        let mut at_least_one = false;\n        for span in message.spans.iter().filter(|s| s.is_primary) {\n            let resolved = span.project_span();\n            let Some(url) = Self::span_to_uri(project_root, resolved)? else {\n                continue;\n            };\n            tracing::trace!(uri = url.to_string(), \"adding diagnostic\");\n            let range = Range::new(\n                Position::new(\n                    resolved.line_start.saturating_sub(1),\n                    resolved.column_start.saturating_sub(1),\n                ),\n                Position::new(\n                    resolved.line_end.saturating_sub(1),\n                    resolved.column_end.saturating_sub(1),\n                ),\n            );\n            let corrections: Vec<Correction> = resolved\n                .is_machine_applicable()\n                .then_some(resolved.suggested_replacement.as_deref())\n                .flatten()\n                .map(|text| Correction::from_single(range, text))\n                .into_iter()\n                .collect();\n            let data = if corrections.is_empty() {\n                None\n            } else {\n                Some(serde_json::json!(DiagnosticData { corrections }))\n            };\n            let diagnostic = Diagnostic {\n                range,\n                severity: Some(Self::parse_severity(&message.level)),\n                source: Some(PKG_NAME.to_string()),\n                message: rendered.to_string(),\n                related_information: related_information.clone(),\n                data,\n                tags: tags.clone(),\n                ..Diagnostic::default()\n            };\n            tracing::trace!(uri = url.to_string(), ?diagnostic, \"sending diagnostic\");\n            tx.send_async((url, diagnostic)).await?;\n            at_least_one = true;\n        }\n\n        // When the client does not support related information, emit children\n        // with primary spans as separate diagnostics.\n        if !use_related_information {\n            for child in &message.children {\n                let Some(first_primary) = child.spans.iter().find(|s| s.is_primary) else {\n                    continue;\n                };\n                let resolved = first_primary.project_span();\n                let Some(url) = Self::span_to_uri(project_root, resolved)? else {\n                    continue;\n                };\n                let range = Range::new(\n                    Position::new(\n                        resolved.line_start.saturating_sub(1),\n                        resolved.column_start.saturating_sub(1),\n                    ),\n                    Position::new(\n                        resolved.line_end.saturating_sub(1),\n                        resolved.column_end.saturating_sub(1),\n                    ),\n                );\n\n                // Group all MachineApplicable primary spans within this child\n                // into a single Correction so editors can apply them atomically\n                // (e.g. removing \"Compact\" from \"{Compact, FmtSpan}\" requires\n                // three separate byte-range deletions in one edit).\n                let edits: Vec<CorrectionEdit> = child\n                    .spans\n                    .iter()\n                    .filter(|s| s.is_primary && s.is_machine_applicable())\n                    .filter_map(|s| {\n                        let new_text = s.suggested_replacement.as_deref()?;\n                        let r = s.project_span();\n                        Some(CorrectionEdit {\n                            range: Range::new(\n                                Position::new(r.line_start.saturating_sub(1), r.column_start.saturating_sub(1)),\n                                Position::new(r.line_end.saturating_sub(1), r.column_end.saturating_sub(1)),\n                            ),\n                            new_text: new_text.to_string(),\n                        })\n                    })\n                    .collect();\n                let corrections = if edits.is_empty() {\n                    vec![]\n                } else {\n                    vec![Correction::from_multi(edits)]\n                };\n                let data = if corrections.is_empty() {\n                    None\n                } else {\n                    Some(serde_json::json!(DiagnosticData { corrections }))\n                };\n                let diagnostic = Diagnostic {\n                    range,\n                    severity: Some(Self::parse_severity(&child.level)),\n                    source: Some(PKG_NAME.to_string()),\n                    message: child.message.clone(),\n                    data,\n                    tags: tags.clone(),\n                    ..Diagnostic::default()\n                };\n                tracing::trace!(uri = url.to_string(), ?diagnostic, \"sending child diagnostic\");\n                tx.send_async((url, diagnostic)).await?;\n                at_least_one = true;\n            }\n        }\n\n        Ok(at_least_one)\n    }\n\n    pub(crate) async fn cargo_diagnostics(\n        command_args: Vec<String>,\n        cargo_env: &[(String, String)],\n        project_root: Option<&PathBuf>,\n        destination_folder: &Path,\n        use_related_information: bool,\n        progress: &OngoingProgress<Bounded, NotCancellable>,\n        tx: flume::Sender<(Uri, Diagnostic)>,\n    ) -> anyhow::Result<()> {\n        tracing::info!(cwd = ?destination_folder, \"running cargo {}\", command_args.join(\" \"));\n        let mut cmd = Command::new(\"cargo\");\n        cmd.env(\"CARGO_TERM_PROGRESS_WHEN\", \"always\");\n        cmd.env(\"CARGO_TERM_PROGRESS_WIDTH\", \"80\");\n        for (key, val) in cargo_env {\n            cmd.env(key, val);\n        }\n        let mut child = cmd\n            .args(command_args)\n            .current_dir(destination_folder)\n            // Close stdin explicitly: the LSP server's stdin is the jsonrpc\n            // pipe, and we must not let cargo (or any tool it invokes) inherit\n            // and read from it.\n            .stdin(Stdio::null())\n            .stdout(Stdio::piped())\n            .stderr(Stdio::piped())\n            .kill_on_drop(true)\n            .spawn()?;\n\n        let stdout = child.stdout.take().context(\"taking stdout\")?;\n        let stderr = child.stderr.take().context(\"taking stderr\")?;\n\n        let log_cargo = env::var(\"BACON_LS_LOG_CARGO\").unwrap_or(\"off\".to_string());\n\n        let stdout_future = async {\n            let mut at_least_one_diag = false;\n            let mut reader = tokio::io::BufReader::new(stdout).lines();\n            while let Some(line) = reader.next_line().await? {\n                match serde_json::from_str::<CargoLine>(&line) {\n                    Ok(message) => {\n                        if let Some(message) = message.message\n                            && message.message_type == \"diagnostic\"\n                        {\n                            at_least_one_diag |=\n                                Self::maybe_add_diagnostic(project_root, &message, use_related_information, &tx)\n                                    .await\n                                    .context(\"adding diagnostic\")?;\n                        }\n                    }\n                    Err(e) => tracing::error!(\"error deserializing cargo line:\\n{line}\\n{e}\"),\n                }\n            }\n            anyhow::Ok(at_least_one_diag)\n        };\n\n        let stderr_future = async {\n            let mut errors = String::new();\n            let mut reader = tokio::io::BufReader::new(stderr);\n            let mut buf = Vec::new();\n            // Read until \\r or \\n so cargo progress updates arrive immediately\n            // instead of being buffered until the next \\n\n            loop {\n                buf.clear();\n                let bytes_read = read_until_cr_or_lf(&mut reader, &mut buf).await?;\n                if bytes_read == 0 {\n                    break;\n                }\n                let segment = String::from_utf8_lossy(&buf);\n                let segment = segment.trim();\n                if segment.is_empty() {\n                    continue;\n                }\n                if log_cargo != \"off\" {\n                    tracing::trace!(\"[cargo stderr]{segment}\");\n                }\n                let trimmed = segment.trim_start();\n                if let Some((message, pct)) = parse_building_line(trimmed) {\n                    tracing::trace!(msg = message, pct = pct, \"reported Building\");\n                    progress.report_with_message(message, pct).await;\n                } else if let Some(msg) = trimmed.strip_prefix(\"Blocking\") {\n                    tracing::trace!(msg = msg, \"reported Blocking\");\n                    progress.report_with_message(msg.trim_start(), 0).await;\n                } else if let Some(msg) = trimmed.strip_prefix(\"error:\") {\n                    // This will catch things that looks like this\n                    // 1) `error: no such command: `fake`\n                    // 2) `error: expected `;`, found keyword `let`\n                    //\n                    // However we are only really interested in the `1)`\n                    errors.push_str(msg);\n                }\n            }\n            anyhow::Ok(errors)\n        };\n\n        let (stdout_result, stderr_result) = tokio::join!(stdout_future, stderr_future);\n\n        // If something failed when parsing stdout we consider this an error as\n        // diagnostics are parsed from stdout\n        let at_least_one_diag = stdout_result?;\n\n        // However we don't consider failing to parse stderr as bad\n        let logged_errors = match stderr_result {\n            Ok(logged_errors) => logged_errors,\n            Err(e) => {\n                tracing::warn!(\"error reading cargo stderr: {e}\");\n                String::new()\n            }\n        };\n\n        let status = child.wait().await?;\n        tracing::info!(\"cargo finished with status {status}\");\n\n        // We can't rely on exit code, as cargo exit with the same code regardless if its because\n        // of the args / invalid command or because the check fails due to the code being check\n        // has errors.\n        //\n        // So we do hacky thing we consider that the command was likely invalid if there are some\n        // error logs but no diagnostics\n        if !logged_errors.is_empty() && !at_least_one_diag {\n            anyhow::bail!(\"cargo exited with {status}:{logged_errors}\");\n        }\n\n        Ok(())\n    }\n\n    pub(crate) async fn find_project_root(params: &InitializeParams) -> Option<PathBuf> {\n        // Build an ordered list of candidate paths to probe:\n        // client-authoritative workspace folders first, then the deprecated\n        // root_uri/root_path fields, then the server's own CWD as a last\n        // resort. This avoids ever picking the LSP server's CWD over an\n        // explicitly-specified workspace folder.\n        let mut candidates: Vec<PathBuf> = Vec::new();\n        if let Some(workspace_folders) = &params.workspace_folders {\n            for folder in workspace_folders {\n                candidates.push(PathBuf::from(folder.uri.path().as_str()));\n            }\n        }\n        #[allow(deprecated)]\n        if let Some(root_uri) = &params.root_uri {\n            candidates.push(PathBuf::from(root_uri.path().as_str()));\n        }\n        #[allow(deprecated)]\n        if let Some(root_path) = &params.root_path {\n            candidates.push(PathBuf::from(root_path));\n        }\n        if let Ok(cwd) = std::env::current_dir() {\n            candidates.push(cwd);\n        }\n\n        for candidate in &candidates {\n            // Prefer the git root containing this candidate when it has a\n            // Cargo.toml — this lets a nested crate resolve to the workspace\n            // root, which is where cargo needs to run.\n            if let Some(git_root) = BaconLs::find_git_root_directory(candidate).await\n                && git_root.join(\"Cargo.toml\").exists()\n            {\n                return Some(git_root);\n            }\n            if candidate.join(\"Cargo.toml\").exists() {\n                return Some(candidate.clone());\n            }\n        }\n\n        None\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    // Real cargo JSON line captured from a tokio::select! type error. The\n    // primary spans all point into the tokio registry source; only the\n    // innermost expansion span resolves back to `src/lib.rs`.\n    const TOKIO_SELECT_EXPANSION: &str = include_str!(\"testdata/expansion-needed.json\");\n\n    // Real cargo JSON line for an \"unused variable\" warning. Has two children:\n    // one with no spans (dropped into rendered message) and one help child with\n    // a primary span carrying a suggested_replacement (becomes relatedInformation).\n    const UNUSED_VARIABLE: &str = include_str!(\"testdata/unused-variable.json\");\n\n    // Unused import of a whole `use` item — child span covers the full line\n    // including newline, suggested_replacement=\"\" → \"Remove\" correction label.\n    const UNUSED_IMPORT_LINE: &str = include_str!(\"testdata/unused-import-line.json\");\n\n    // Unused import inside a grouped `use` — the help child has three primary\n    // spans (identifier + surrounding punctuation), all suggested_replacement=\"\"\n    // → three \"Remove\" corrections.\n    const UNUSED_IMPORT_GROUPED: &str = include_str!(\"testdata/unused-import-compact.json\");\n\n    #[test]\n    fn test_project_span_follows_macro_expansion_chain() {\n        let line: CargoLine = serde_json::from_str(TOKIO_SELECT_EXPANSION).unwrap();\n        let message = line.message.unwrap();\n        assert_eq!(message.message_type, \"diagnostic\");\n\n        for span in &message.spans {\n            let resolved = span.project_span();\n            // The innermost expansion span points to src/lib.rs lines 708-715.\n            let host = resolved.file_name.authority().map(|a| a.host().to_string());\n            assert_eq!(\n                host,\n                Some(\"src\".to_string()),\n                \"expected project-local span, got {:?}\",\n                resolved.file_name\n            );\n            assert_eq!(resolved.line_start, 708);\n            assert_eq!(resolved.line_end, 715);\n        }\n    }\n\n    #[test]\n    fn test_project_span_returns_self_when_already_project_local() {\n        // A span whose file_name is already a relative (project-local) path\n        // should be returned as-is without traversing the expansion chain.\n        let json = r#\"{\n            \"file_name\": \"src/main.rs\",\n            \"byte_start\": 0, \"byte_end\": 10,\n            \"line_start\": 1, \"line_end\": 1,\n            \"column_start\": 1, \"column_end\": 10,\n            \"is_primary\": true,\n            \"text\": [],\n            \"label\": null,\n            \"suggested_replacement\": null,\n            \"suggestion_applicability\": null,\n            \"expansion\": null\n        }\"#;\n        let span: CargoSpan = serde_json::from_str(json).unwrap();\n        let resolved = span.project_span();\n        assert!(std::ptr::eq(resolved, &span));\n    }\n\n    #[test]\n    fn test_children_structure_for_separate_diagnostics() {\n        let line: CargoLine = serde_json::from_str(UNUSED_VARIABLE).unwrap();\n        let message = line.message.unwrap();\n        assert_eq!(message.message_type, \"diagnostic\");\n        assert_eq!(message.level, \"warning\");\n\n        // One primary span pointing directly to project source — no expansion needed.\n        let primary_spans: Vec<_> = message.spans.iter().filter(|s| s.is_primary).collect();\n        assert_eq!(primary_spans.len(), 1);\n        let span = primary_spans[0];\n        assert_eq!(span.project_span().line_start, 719);\n        assert!(\n            span.file_name\n                .authority()\n                .map(|a| !a.host().is_empty())\n                .unwrap_or(false),\n            \"primary span should already be project-local\"\n        );\n\n        // Two children: first has no spans (skipped — text in rendered message),\n        // second has a primary span with a suggested_replacement and becomes\n        // its own diagnostic.\n        assert_eq!(message.children.len(), 2);\n        assert!(message.children[0].spans.is_empty());\n        assert_eq!(message.children[0].level, \"note\");\n        let help_child = &message.children[1];\n        assert_eq!(help_child.level, \"help\");\n        assert_eq!(\n            help_child.message,\n            \"if this is intentional, prefix it with an underscore\"\n        );\n        let help_spans: Vec<_> = help_child.spans.iter().filter(|s| s.is_primary).collect();\n        assert_eq!(help_spans.len(), 1);\n        assert_eq!(help_spans[0].suggested_replacement.as_deref(), Some(\"_lol\"));\n\n        // The child's MachineApplicable span becomes a correction on the\n        // child's own diagnostic (not the parent's).\n        let help_child_spans: Vec<_> = help_child\n            .spans\n            .iter()\n            .filter(|s| s.is_primary && s.is_machine_applicable())\n            .collect();\n        assert_eq!(help_child_spans.len(), 1);\n        assert_eq!(help_child_spans[0].suggested_replacement.as_deref(), Some(\"_lol\"));\n    }\n\n    #[test]\n    fn test_unused_import_whole_line_produces_remove_correction() {\n        let line: CargoLine = serde_json::from_str(UNUSED_IMPORT_LINE).unwrap();\n        let message = line.message.unwrap();\n        assert_eq!(message.level, \"warning\");\n\n        let primary_spans: Vec<_> = message.spans.iter().filter(|s| s.is_primary).collect();\n        assert_eq!(primary_spans.len(), 1);\n        // Primary span has no replacement; it's the child that carries the fix.\n        assert!(primary_spans[0].suggested_replacement.is_none());\n\n        // One child with one primary span carrying an empty replacement → one\n        // Correction with one edit, labelled \"Remove\".\n        assert_eq!(message.children.len(), 2);\n        let help_child = &message.children[1];\n        let primary_spans: Vec<_> = help_child.spans.iter().filter(|s| s.is_primary).collect();\n        assert_eq!(primary_spans.len(), 1);\n        assert_eq!(primary_spans[0].suggested_replacement.as_deref(), Some(\"\"));\n        let resolved = primary_spans[0].project_span();\n        let range = Range::new(\n            Position::new(\n                resolved.line_start.saturating_sub(1),\n                resolved.column_start.saturating_sub(1),\n            ),\n            Position::new(\n                resolved.line_end.saturating_sub(1),\n                resolved.column_end.saturating_sub(1),\n            ),\n        );\n        let correction = Correction::from_single(range, \"\");\n        assert_eq!(correction.label, \"Remove\");\n        assert_eq!(correction.edits.len(), 1);\n    }\n\n    #[test]\n    fn test_unused_import_grouped_produces_three_remove_corrections() {\n        let line: CargoLine = serde_json::from_str(UNUSED_IMPORT_GROUPED).unwrap();\n        let message = line.message.unwrap();\n        assert_eq!(message.level, \"warning\");\n\n        // One child with three primary spans (identifier + comma + surrounding\n        // space), all with empty suggested_replacement. They must be grouped\n        // into a single Correction with three CorrectionEdits so the editor\n        // applies all three byte-range deletions atomically.\n        assert_eq!(message.children.len(), 2);\n        let help_child = &message.children[1];\n        let primary_spans: Vec<_> = help_child\n            .spans\n            .iter()\n            .filter(|s| s.is_primary && s.is_machine_applicable())\n            .collect();\n        assert_eq!(primary_spans.len(), 3, \"three spans for grouped import removal\");\n        let edits: Vec<CorrectionEdit> = primary_spans\n            .iter()\n            .filter_map(|s| {\n                let new_text = s.suggested_replacement.as_deref()?;\n                let resolved = s.project_span();\n                Some(CorrectionEdit {\n                    range: Range::new(\n                        Position::new(\n                            resolved.line_start.saturating_sub(1),\n                            resolved.column_start.saturating_sub(1),\n                        ),\n                        Position::new(\n                            resolved.line_end.saturating_sub(1),\n                            resolved.column_end.saturating_sub(1),\n                        ),\n                    ),\n                    new_text: new_text.to_string(),\n                })\n            })\n            .collect();\n        assert_eq!(edits.len(), 3);\n        let correction = Correction::from_multi(edits);\n        assert_eq!(correction.label, \"Remove\");\n        assert_eq!(correction.edits.len(), 3);\n    }\n\n    #[test]\n    fn test_project_span_falls_back_to_self_when_no_project_span_in_chain() {\n        // When every span in the expansion chain points to an external\n        // (absolute-path) file, project_span falls back to self.\n        let json = r#\"{\n            \"file_name\": \"/home/user/.cargo/registry/src/foo/lib.rs\",\n            \"byte_start\": 0, \"byte_end\": 10,\n            \"line_start\": 5, \"line_end\": 5,\n            \"column_start\": 1, \"column_end\": 10,\n            \"is_primary\": true,\n            \"text\": [],\n            \"label\": null,\n            \"suggested_replacement\": null,\n            \"suggestion_applicability\": null,\n            \"expansion\": null\n        }\"#;\n        let span: CargoSpan = serde_json::from_str(json).unwrap();\n        let resolved = span.project_span();\n        assert!(std::ptr::eq(resolved, &span));\n    }\n\n    #[test]\n    fn test_parse_building_line_basic() {\n        // The returned message is the portion after \"] \" (i.e. fraction + crate list),\n        // not the full original line — that's the piece suitable for a progress report.\n        let line = \"Building [====       ] 7/10: foo, bar\";\n        let (msg, pct) = parse_building_line(line).unwrap();\n        assert_eq!(msg, \"7/10: foo, bar\");\n        assert_eq!(pct, 70);\n    }\n\n    #[test]\n    fn test_parse_building_line_caps_percentage_at_99() {\n        let line = \"Building [========] 10/10: last\";\n        let (_, pct) = parse_building_line(line).unwrap();\n        assert_eq!(\n            pct, 99,\n            \"100% should cap at 99 to keep the bar indeterminate until Finished\"\n        );\n    }\n\n    #[test]\n    fn test_parse_building_line_rejects_non_building_prefix() {\n        assert!(parse_building_line(\"Finished `dev` profile\").is_none());\n        assert!(parse_building_line(\"Compiling foo v0.1.0\").is_none());\n    }\n\n    #[test]\n    fn test_parse_building_line_zero_total_returns_none() {\n        assert!(parse_building_line(\"Building [] 0/0: nothing\").is_none());\n    }\n\n    #[test]\n    fn test_parse_building_line_malformed_returns_none() {\n        assert!(parse_building_line(\"Building [====] no-fraction-here\").is_none());\n        assert!(parse_building_line(\"Building [====] abc/def: foo\").is_none());\n    }\n\n    #[test]\n    fn test_parse_severity_known_levels() {\n        assert_eq!(Cargo::parse_severity(\"error\"), DiagnosticSeverity::ERROR);\n        assert_eq!(Cargo::parse_severity(\"warning\"), DiagnosticSeverity::WARNING);\n        assert_eq!(Cargo::parse_severity(\"failure-note\"), DiagnosticSeverity::WARNING);\n        assert_eq!(Cargo::parse_severity(\"note\"), DiagnosticSeverity::INFORMATION);\n        assert_eq!(Cargo::parse_severity(\"info\"), DiagnosticSeverity::INFORMATION);\n        assert_eq!(Cargo::parse_severity(\"help\"), DiagnosticSeverity::HINT);\n        assert_eq!(Cargo::parse_severity(\"hint\"), DiagnosticSeverity::HINT);\n    }\n\n    #[test]\n    fn test_parse_severity_unknown_level_defaults_to_information() {\n        assert_eq!(\n            Cargo::parse_severity(\"something-brand-new\"),\n            DiagnosticSeverity::INFORMATION\n        );\n    }\n\n    #[test]\n    fn test_tags_from_code_unused_lints_get_unnecessary() {\n        for code in [\n            \"unused_variables\",\n            \"unused_imports\",\n            \"unused_mut\",\n            \"unused_assignments\",\n            \"unused_must_use\",\n        ] {\n            assert_eq!(\n                tags_from_code(code),\n                Some(vec![DiagnosticTag::UNNECESSARY]),\n                \"{code} should be tagged UNNECESSARY\"\n            );\n        }\n    }\n\n    #[test]\n    fn test_tags_from_code_dead_and_unreachable_get_unnecessary() {\n        for code in [\n            \"dead_code\",\n            \"unreachable_code\",\n            \"unreachable_patterns\",\n            \"redundant_imports\",\n            \"redundant_semicolons\",\n        ] {\n            assert_eq!(\n                tags_from_code(code),\n                Some(vec![DiagnosticTag::UNNECESSARY]),\n                \"{code} should be tagged UNNECESSARY\"\n            );\n        }\n    }\n\n    #[test]\n    fn test_tags_from_code_deprecated_gets_deprecated_tag() {\n        assert_eq!(tags_from_code(\"deprecated\"), Some(vec![DiagnosticTag::DEPRECATED]));\n    }\n\n    #[test]\n    fn test_tags_from_code_unrelated_returns_none() {\n        // Compile error code, clippy lint, custom code → no tag.\n        assert_eq!(tags_from_code(\"E0425\"), None);\n        assert_eq!(tags_from_code(\"clippy::needless_pass_by_value\"), None);\n        assert_eq!(tags_from_code(\"non_snake_case\"), None);\n        assert_eq!(tags_from_code(\"\"), None);\n    }\n\n    #[tokio::test]\n    async fn test_read_until_lf_consumes_terminator() {\n        let mut reader = tokio::io::BufReader::new(&b\"hello\\nworld\"[..]);\n        let mut buf = Vec::new();\n        let n = read_until_cr_or_lf(&mut reader, &mut buf).await.unwrap();\n        // 6 bytes: \"hello\" + the LF (LF is consumed but not pushed into buf)\n        assert_eq!(n, 6);\n        assert_eq!(buf, b\"hello\");\n    }\n\n    #[tokio::test]\n    async fn test_read_until_cr_consumes_terminator() {\n        // Cargo emits progress lines terminated only by `\\r` — that's the case\n        // this helper exists for.\n        let mut reader = tokio::io::BufReader::new(&b\"progress\\rnext\"[..]);\n        let mut buf = Vec::new();\n        let n = read_until_cr_or_lf(&mut reader, &mut buf).await.unwrap();\n        assert_eq!(n, 9);\n        assert_eq!(buf, b\"progress\");\n    }\n\n    #[tokio::test]\n    async fn test_read_until_cr_or_lf_returns_zero_at_eof() {\n        let mut reader = tokio::io::BufReader::new(&b\"\"[..]);\n        let mut buf = Vec::new();\n        let n = read_until_cr_or_lf(&mut reader, &mut buf).await.unwrap();\n        assert_eq!(n, 0);\n        assert!(buf.is_empty());\n    }\n\n    #[tokio::test]\n    async fn test_read_until_cr_or_lf_no_terminator_reads_to_eof() {\n        let mut reader = tokio::io::BufReader::new(&b\"trailing\"[..]);\n        let mut buf = Vec::new();\n        let n = read_until_cr_or_lf(&mut reader, &mut buf).await.unwrap();\n        assert_eq!(n, 8);\n        assert_eq!(buf, b\"trailing\");\n    }\n\n    #[tokio::test]\n    async fn test_read_until_cr_or_lf_spans_multiple_internal_buffers() {\n        // BufReader with capacity 4 forces multiple `fill_buf` rounds before\n        // we hit the terminator at byte 9.\n        let mut reader = tokio::io::BufReader::with_capacity(4, &b\"abcdefghi\\nrest\"[..]);\n        let mut buf = Vec::new();\n        let n = read_until_cr_or_lf(&mut reader, &mut buf).await.unwrap();\n        assert_eq!(n, 10);\n        assert_eq!(buf, b\"abcdefghi\");\n    }\n\n    fn make_relative_span(file_name_str: &str, is_primary: bool) -> CargoSpan {\n        // Construct via JSON to mirror real cargo output (and avoid duplicating\n        // the deserialize_url logic).\n        let json = format!(\n            r#\"{{\n                \"file_name\": \"{file_name_str}\",\n                \"byte_start\": 0, \"byte_end\": 0,\n                \"line_start\": 1, \"line_end\": 1,\n                \"column_start\": 1, \"column_end\": 1,\n                \"is_primary\": {is_primary},\n                \"text\": [],\n                \"label\": null,\n                \"suggested_replacement\": null,\n                \"suggestion_applicability\": null,\n                \"expansion\": null\n            }}\"#\n        );\n        serde_json::from_str(&json).unwrap()\n    }\n\n    #[test]\n    fn test_span_to_uri_returns_none_when_no_authority() {\n        // file_name without `file://` scheme → no authority → not a project\n        // span we can resolve. The deserialize_url helper prepends `file://`,\n        // so we go through deserialization explicitly.\n        let span = make_relative_span(\"/absolute/path/that/will/not/exist\", true);\n        // Authority is present (host=\"\"); span_to_uri proceeds, hits\n        // canonicalize on the absolute path, fails, returns Ok(None).\n        let result = Cargo::span_to_uri(None, &span).unwrap();\n        assert_eq!(result, None);\n    }\n\n    #[tokio::test]\n    async fn test_span_to_uri_returns_none_when_canonicalize_fails() {\n        // Relative path under a project root that does not exist on disk —\n        // canonicalize fails and we skip rather than aborting.\n        let tmp = tempfile::TempDir::new().unwrap();\n        let span = make_relative_span(\"does/not/exist.rs\", true);\n        let result = Cargo::span_to_uri(Some(&tmp.path().to_path_buf()), &span).unwrap();\n        assert_eq!(result, None, \"missing file should be skipped, not error\");\n    }\n\n    // Skipped on Windows: canonicalize returns a `\\\\?\\C:\\…` extended-length\n    // path whose backslashes percent-encode in the produced URI, breaking the\n    // string-equality assertion. The behaviour itself is unaffected.\n    #[tokio::test]\n    #[cfg(not(target_os = \"windows\"))]\n    async fn test_span_to_uri_resolves_existing_relative_path() {\n        let tmp = tempfile::TempDir::new().unwrap();\n        let src_dir = tmp.path().join(\"src\");\n        std::fs::create_dir_all(&src_dir).unwrap();\n        let lib_rs = src_dir.join(\"lib.rs\");\n        std::fs::write(&lib_rs, \"// content\").unwrap();\n\n        let span = make_relative_span(\"src/lib.rs\", true);\n        let uri = Cargo::span_to_uri(Some(&tmp.path().to_path_buf()), &span)\n            .unwrap()\n            .expect(\"existing path should resolve\");\n\n        let canonical = lib_rs.canonicalize().unwrap();\n        let expected = format!(\"file://{}\", canonical.display());\n        assert_eq!(uri.to_string(), expected);\n    }\n\n    // Skipped on Windows: span resolution goes through canonicalize, which\n    // produces extended-length paths that don't round-trip through our\n    // unix-shaped `path_to_file_uri` helper.\n    #[tokio::test]\n    #[cfg(not(target_os = \"windows\"))]\n    async fn test_maybe_add_diagnostic_emits_per_primary_span() {\n        let tmp = tempfile::TempDir::new().unwrap();\n        let src_dir = tmp.path().join(\"src\");\n        std::fs::create_dir_all(&src_dir).unwrap();\n        std::fs::write(src_dir.join(\"lib.rs\"), \"// content\").unwrap();\n        let project_root = tmp.path().to_path_buf();\n\n        let line: CargoLine = serde_json::from_str(UNUSED_VARIABLE).unwrap();\n        let message = line.message.unwrap();\n\n        let (tx, rx) = flume::unbounded();\n        // use_related_information=true: the help-child span attaches as\n        // related info on the parent diagnostic, not its own diagnostic.\n        let any = Cargo::maybe_add_diagnostic(Some(&project_root), &message, true, &tx)\n            .await\n            .unwrap();\n        drop(tx);\n\n        assert!(any, \"should emit at least one diagnostic for the unused variable\");\n        let received: Vec<_> = rx.drain().collect();\n        assert_eq!(received.len(), 1, \"one primary span → one diagnostic\");\n        let (_uri, diag) = &received[0];\n        assert_eq!(diag.severity, Some(DiagnosticSeverity::WARNING));\n        assert_eq!(diag.source, Some(PKG_NAME.to_string()));\n        assert!(\n            diag.related_information.as_ref().is_some_and(|r| !r.is_empty()),\n            \"help child should attach as related information when supported\"\n        );\n        // The fixture's lint code is `unused_variables` → editors render the\n        // diagnostic faded/grey via the UNNECESSARY tag.\n        assert_eq!(\n            diag.tags.as_deref(),\n            Some(&[DiagnosticTag::UNNECESSARY][..]),\n            \"unused_variables warning must be tagged UNNECESSARY\"\n        );\n    }\n\n    // Skipped on Windows for the same reason as\n    // `test_maybe_add_diagnostic_emits_per_primary_span`.\n    #[tokio::test]\n    #[cfg(not(target_os = \"windows\"))]\n    async fn test_maybe_add_diagnostic_separate_children_when_unsupported() {\n        let tmp = tempfile::TempDir::new().unwrap();\n        let src_dir = tmp.path().join(\"src\");\n        std::fs::create_dir_all(&src_dir).unwrap();\n        std::fs::write(src_dir.join(\"lib.rs\"), \"// content\").unwrap();\n        let project_root = tmp.path().to_path_buf();\n\n        let line: CargoLine = serde_json::from_str(UNUSED_VARIABLE).unwrap();\n        let message = line.message.unwrap();\n\n        let (tx, rx) = flume::unbounded();\n        // use_related_information=false: the help child is emitted as its own\n        // diagnostic.\n        let any = Cargo::maybe_add_diagnostic(Some(&project_root), &message, false, &tx)\n            .await\n            .unwrap();\n        drop(tx);\n\n        assert!(any);\n        let received: Vec<_> = rx.drain().collect();\n        // 1 parent (warning) + 1 help child with primary span = 2 diagnostics.\n        assert_eq!(received.len(), 2, \"parent + help child as separate diagnostics\");\n        let levels: Vec<_> = received.iter().map(|(_, d)| d.severity).collect();\n        assert!(levels.contains(&Some(DiagnosticSeverity::WARNING)));\n        assert!(levels.contains(&Some(DiagnosticSeverity::HINT)));\n\n        // The help child carries a MachineApplicable replacement → child\n        // diagnostic should expose the correction in `data`.\n        let help_diag = received\n            .iter()\n            .find(|(_, d)| d.severity == Some(DiagnosticSeverity::HINT))\n            .unwrap();\n        assert!(help_diag.1.data.is_some(), \"help child should carry quick-fix data\");\n        // Both the parent warning AND the help child must carry the parent's\n        // UNNECESSARY tag so the editor renders the same range uniformly.\n        for (_, d) in &received {\n            assert_eq!(\n                d.tags.as_deref(),\n                Some(&[DiagnosticTag::UNNECESSARY][..]),\n                \"unused_variables tag must propagate to the help child\"\n            );\n        }\n    }\n\n    // Skipped on Windows: this test builds a workspace folder URI from\n    // `format!(\"file://{}\", tempdir.display())`, which produces a malformed\n    // Windows file URI (`file://C:\\Users\\...`). Encoding that correctly is\n    // outside the scope of this unit test.\n    #[tokio::test]\n    #[cfg(not(target_os = \"windows\"))]\n    async fn test_find_project_root_picks_workspace_folder_with_cargo_toml() {\n        let tmp = tempfile::TempDir::new().unwrap();\n        std::fs::write(tmp.path().join(\"Cargo.toml\"), \"[package]\\nname = \\\"x\\\"\").unwrap();\n\n        let folder_uri = format!(\"file://{}\", tmp.path().display());\n        let params: InitializeParams = serde_json::from_value(serde_json::json!({\n            \"processId\": null,\n            \"rootUri\": null,\n            \"capabilities\": {},\n            \"workspaceFolders\": [{\n                \"uri\": folder_uri,\n                \"name\": \"test\"\n            }]\n        }))\n        .unwrap();\n\n        let root = Cargo::find_project_root(&params).await;\n        let canonical = tmp.path().canonicalize().unwrap();\n        assert_eq!(\n            root.map(|p| p.canonicalize().unwrap()),\n            Some(canonical),\n            \"workspace folder containing Cargo.toml should win\"\n        );\n    }\n\n    // Skipped on Windows for the same URI-formatting reason as\n    // `test_find_project_root_picks_workspace_folder_with_cargo_toml`.\n    #[tokio::test]\n    #[cfg(not(target_os = \"windows\"))]\n    async fn test_find_project_root_returns_none_when_no_cargo_toml_anywhere() {\n        // Empty tempdir: no Cargo.toml in any candidate.\n        let tmp = tempfile::TempDir::new().unwrap();\n        let folder_uri = format!(\"file://{}\", tmp.path().display());\n        let params: InitializeParams = serde_json::from_value(serde_json::json!({\n            \"processId\": null,\n            \"rootUri\": folder_uri,\n            \"capabilities\": {},\n        }))\n        .unwrap();\n        let root = Cargo::find_project_root(&params).await;\n        // The CWD fallback may still find a Cargo.toml when tests run from the\n        // repo, so we only assert: if Some, it's not the empty tempdir.\n        if let Some(p) = root {\n            assert_ne!(\n                p.canonicalize().ok(),\n                Some(tmp.path().canonicalize().unwrap()),\n                \"tempdir without Cargo.toml must not be picked\"\n            );\n        }\n    }\n}\n"
  },
  {
    "path": "src/shadow.rs",
    "content": "//! Hardlinked shadow workspace used for live \"as you type\" diagnostics.\n//!\n//! cargo can only check files on disk. To surface diagnostics for unsaved\n//! editor buffers, we mirror the workspace into\n//! `<workspace>/target/bacon-ls-live/shadow/` using **hardlinks** (one inode\n//! per file, no data copy), then on `did_change` we replace the hardlink for\n//! the dirty file with a real file containing the buffer content. Cargo runs\n//! against the shadow with its own target dir at\n//! `<workspace>/target/bacon-ls-live/target/` so it can't deadlock with the\n//! user's regular `cargo build` against the real `target/`.\n//!\n//! What gets mirrored: everything that wouldn't be excluded by `git status`\n//! — the `ignore` crate (same engine `ripgrep` uses) walks the workspace\n//! respecting `.gitignore`, `.ignore`, `.git/info/exclude`, the global\n//! gitignore, and the hidden-file filter. Plus a hardcoded skip for our own\n//! `target/bacon-ls-live/` so the shadow can't recursively mirror itself if\n//! a workspace happens not to gitignore `target/`.\n//!\n//! Path remapping back to the real workspace happens at the cargo invocation\n//! site via `--remap-path-prefix`, so diagnostics published to the editor\n//! still carry the user's real source paths.\n\nuse std::path::{Path, PathBuf};\n\n#[derive(Debug, Clone)]\npub(crate) struct ShadowWorkspace {\n    real_root: PathBuf,\n    shadow_root: PathBuf,\n    target_dir: PathBuf,\n}\n\nimpl ShadowWorkspace {\n    /// Build (or rebuild) the shadow tree for `real_root`. Wipes any stale\n    /// shadow contents from a previous LSP session — keeping them risks\n    /// resurrecting a forgotten dirty buffer — but **preserves** the live\n    /// target dir so cargo's incremental cache survives across restarts.\n    pub(crate) async fn build(real_root: PathBuf) -> std::io::Result<Self> {\n        let live_root = real_root.join(\"target\").join(\"bacon-ls-live\");\n        let shadow_root = live_root.join(\"shadow\");\n        let target_dir = live_root.join(\"target\");\n\n        if tokio::fs::try_exists(&shadow_root).await? {\n            tokio::fs::remove_dir_all(&shadow_root).await?;\n        }\n        tokio::fs::create_dir_all(&shadow_root).await?;\n        tokio::fs::create_dir_all(&target_dir).await?;\n\n        // Off-thread the filesystem walk: `ignore`'s walker is sync, and on\n        // a large workspace we'd otherwise stall the LSP runtime.\n        let real = real_root.clone();\n        let shadow = shadow_root.clone();\n        let live = live_root.clone();\n        tokio::task::spawn_blocking(move || mirror_blocking(&real, &shadow, &live))\n            .await\n            .map_err(std::io::Error::other)??;\n\n        Ok(Self {\n            real_root,\n            shadow_root,\n            target_dir,\n        })\n    }\n\n    pub(crate) fn real_root(&self) -> &Path {\n        &self.real_root\n    }\n\n    pub(crate) fn shadow_root(&self) -> &Path {\n        &self.shadow_root\n    }\n\n    pub(crate) fn target_dir(&self) -> &Path {\n        &self.target_dir\n    }\n\n    /// Translate a real-workspace path into its position inside the shadow.\n    /// Errors if `real_path` is not inside the workspace root.\n    pub(crate) fn shadow_path_for(&self, real_path: &Path) -> std::io::Result<PathBuf> {\n        let rel = real_path.strip_prefix(&self.real_root).map_err(|_| {\n            std::io::Error::new(\n                std::io::ErrorKind::InvalidInput,\n                format!(\n                    \"path {} is not inside workspace {}\",\n                    real_path.display(),\n                    self.real_root.display(),\n                ),\n            )\n        })?;\n        Ok(self.shadow_root.join(rel))\n    }\n\n    /// Replace the shadow entry for `real_path` with a real file containing\n    /// `content`. Implemented as write-tmp-then-rename so the rename is atomic\n    /// and so the real file's inode is **not** modified — only the directory\n    /// entry inside the shadow.\n    pub(crate) async fn write_dirty(&self, real_path: &Path, content: &str) -> std::io::Result<()> {\n        let shadow_path = self.shadow_path_for(real_path)?;\n        if let Some(parent) = shadow_path.parent() {\n            tokio::fs::create_dir_all(parent).await?;\n        }\n        let tmp = shadow_path.with_extension(\"bacon-ls-tmp\");\n        tokio::fs::write(&tmp, content).await?;\n        tokio::fs::rename(&tmp, &shadow_path).await?;\n        Ok(())\n    }\n\n    /// Restore the shadow entry for `real_path` back to a hardlink of the\n    /// on-disk file. Used after `did_save` / `did_close` so subsequent cargo\n    /// runs see the saved content.\n    pub(crate) async fn restore_link(&self, real_path: &Path) -> std::io::Result<()> {\n        let shadow_path = self.shadow_path_for(real_path)?;\n        if tokio::fs::try_exists(&shadow_path).await? {\n            tokio::fs::remove_file(&shadow_path).await?;\n        }\n        if let Some(parent) = shadow_path.parent() {\n            tokio::fs::create_dir_all(parent).await?;\n        }\n        tokio::fs::hard_link(real_path, &shadow_path).await?;\n        Ok(())\n    }\n}\n\n/// Sync mirror walk. Runs inside `spawn_blocking`. Uses the `ignore` crate so\n/// gitignored / hidden / `.ignore`'d files are skipped, and adds a hardcoded\n/// guard against descending into our own shadow dir (`live_root`).\nfn mirror_blocking(real: &Path, shadow: &Path, live_root: &Path) -> std::io::Result<()> {\n    use ignore::WalkBuilder;\n\n    let walker = WalkBuilder::new(real)\n        // `ignore`'s defaults already enable .gitignore / .ignore /\n        // .git/info/exclude / global gitignore + hidden-file filtering, but\n        // make the intent explicit so a future audit doesn't have to read\n        // the crate's source.\n        .hidden(true)\n        .git_ignore(true)\n        .git_exclude(true)\n        .git_global(true)\n        // Crucially: don't require the workspace to be a git repo before\n        // applying .gitignore rules. Users with jj / hg / no VCS still write\n        // .gitignore files, and the rules are exactly what we want.\n        .require_git(false)\n        .parents(true)\n        .filter_entry({\n            let live_root = live_root.to_path_buf();\n            move |e| !e.path().starts_with(&live_root)\n        })\n        .build();\n\n    for result in walker {\n        let entry = match result {\n            Ok(e) => e,\n            Err(err) => {\n                tracing::warn!(?err, \"skipping entry while mirroring shadow\");\n                continue;\n            }\n        };\n        let path = entry.path();\n        // The walker yields the root itself first; skip it (the shadow root\n        // already exists).\n        let rel = match path.strip_prefix(real) {\n            Ok(r) if !r.as_os_str().is_empty() => r,\n            _ => continue,\n        };\n        let shadow_path = shadow.join(rel);\n        let ft = match entry.file_type() {\n            Some(ft) => ft,\n            None => continue,\n        };\n        if ft.is_dir() {\n            std::fs::create_dir_all(&shadow_path)?;\n        } else if ft.is_file() {\n            if let Some(parent) = shadow_path.parent() {\n                std::fs::create_dir_all(parent)?;\n            }\n            // If shadow_path already exists from a partially-completed previous\n            // run, `hard_link` would error with EEXIST. We removed the shadow\n            // root at the top of `build`, so this shouldn't happen, but be\n            // defensive: replace if present.\n            if std::fs::metadata(&shadow_path).is_ok() {\n                std::fs::remove_file(&shadow_path)?;\n            }\n            std::fs::hard_link(path, &shadow_path)?;\n        }\n        // Symlinks: skipped in v1. Rare in Rust workspaces and the semantics\n        // (target inside vs outside the workspace, broken vs valid) need more\n        // thought than they're worth right now.\n    }\n    Ok(())\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n    use tempfile::TempDir;\n\n    #[cfg(unix)]\n    fn inode(path: &Path) -> u64 {\n        use std::os::unix::fs::MetadataExt;\n        std::fs::metadata(path).unwrap().ino()\n    }\n\n    fn mk_file(path: &Path, content: &str) {\n        if let Some(parent) = path.parent() {\n            std::fs::create_dir_all(parent).unwrap();\n        }\n        std::fs::write(path, content).unwrap();\n    }\n\n    #[tokio::test]\n    #[cfg(unix)]\n    async fn test_build_mirrors_files_via_hardlink_same_inode() {\n        let tmp = TempDir::new().unwrap();\n        let lib_rs = tmp.path().join(\"src/lib.rs\");\n        mk_file(&lib_rs, \"// content\");\n\n        let shadow = ShadowWorkspace::build(tmp.path().to_path_buf()).await.unwrap();\n        let shadow_lib_rs = shadow.shadow_root().join(\"src/lib.rs\");\n        assert!(shadow_lib_rs.exists());\n        assert_eq!(\n            inode(&lib_rs),\n            inode(&shadow_lib_rs),\n            \"shadow file must hardlink to the real file (same inode)\"\n        );\n    }\n\n    #[tokio::test]\n    async fn test_build_excludes_gitignored_paths() {\n        let tmp = TempDir::new().unwrap();\n        // Put `target/` in .gitignore explicitly so the test doesn't rely on\n        // the user's global gitignore. Also gitignore a custom build output.\n        mk_file(&tmp.path().join(\".gitignore\"), \"target/\\nbuild-output/\\n\");\n        mk_file(&tmp.path().join(\"Cargo.toml\"), \"[package]\\nname=\\\"x\\\"\");\n        mk_file(&tmp.path().join(\"src/lib.rs\"), \"// real\");\n        mk_file(&tmp.path().join(\"target/release/big.rlib\"), \"binary\");\n        mk_file(&tmp.path().join(\"build-output/snapshot.bin\"), \"junk\");\n\n        let shadow = ShadowWorkspace::build(tmp.path().to_path_buf()).await.unwrap();\n\n        assert!(shadow.shadow_root().join(\"Cargo.toml\").exists());\n        assert!(shadow.shadow_root().join(\"src/lib.rs\").exists());\n        // gitignored content must NOT be mirrored.\n        assert!(!shadow.shadow_root().join(\"target\").exists());\n        assert!(!shadow.shadow_root().join(\"build-output\").exists());\n    }\n\n    #[tokio::test]\n    async fn test_build_excludes_hidden_dirs() {\n        let tmp = TempDir::new().unwrap();\n        mk_file(&tmp.path().join(\"Cargo.toml\"), \"x\");\n        mk_file(&tmp.path().join(\".git/HEAD\"), \"ref:\");\n        mk_file(&tmp.path().join(\".idea/workspace.xml\"), \"<x/>\");\n\n        let shadow = ShadowWorkspace::build(tmp.path().to_path_buf()).await.unwrap();\n        assert!(!shadow.shadow_root().join(\".git\").exists());\n        assert!(!shadow.shadow_root().join(\".idea\").exists());\n    }\n\n    #[tokio::test]\n    async fn test_build_does_not_recurse_into_its_own_live_dir() {\n        // A workspace that doesn't gitignore target/ at all (unusual but\n        // possible): we must still skip target/bacon-ls-live/ to avoid the\n        // shadow recursively containing itself.\n        let tmp = TempDir::new().unwrap();\n        mk_file(&tmp.path().join(\"Cargo.toml\"), \"x\");\n        mk_file(&tmp.path().join(\"src/lib.rs\"), \"x\");\n        // No .gitignore at all → ignore crate's defaults still skip `.git`\n        // (hidden) and respect any global gitignore, but won't filter\n        // target/. We populate target/bacon-ls-live before calling build to\n        // simulate a prior run.\n        mk_file(\n            &tmp.path().join(\"target/bacon-ls-live/shadow/should-not-recurse.rs\"),\n            \"x\",\n        );\n\n        let shadow = ShadowWorkspace::build(tmp.path().to_path_buf()).await.unwrap();\n\n        // The shadow root must not contain a nested `target/bacon-ls-live/`\n        // (which would mean we recursed into our own output).\n        assert!(\n            !shadow.shadow_root().join(\"target/bacon-ls-live\").exists(),\n            \"shadow must not recurse into its own live dir\"\n        );\n    }\n\n    #[tokio::test]\n    async fn test_build_wipes_stale_shadow_from_prior_session() {\n        let tmp = TempDir::new().unwrap();\n        mk_file(&tmp.path().join(\".gitignore\"), \"target/\\n\");\n        mk_file(&tmp.path().join(\"src/lib.rs\"), \"fresh\");\n        // Simulate leftover shadow with a stale dirty buffer.\n        mk_file(\n            &tmp.path().join(\"target/bacon-ls-live/shadow/src/lib.rs\"),\n            \"stale dirty content\",\n        );\n        mk_file(\n            &tmp.path().join(\"target/bacon-ls-live/shadow/src/gone.rs\"),\n            \"deleted in real workspace\",\n        );\n\n        let shadow = ShadowWorkspace::build(tmp.path().to_path_buf()).await.unwrap();\n\n        assert!(\n            !shadow.shadow_root().join(\"src/gone.rs\").exists(),\n            \"stale shadow entries from prior runs must not survive a rebuild\"\n        );\n        let mirrored = std::fs::read_to_string(shadow.shadow_root().join(\"src/lib.rs\")).unwrap();\n        assert_eq!(mirrored, \"fresh\", \"stale dirty content must be replaced\");\n    }\n\n    #[tokio::test]\n    async fn test_build_preserves_target_dir_for_cache_reuse() {\n        let tmp = TempDir::new().unwrap();\n        mk_file(&tmp.path().join(\".gitignore\"), \"target/\\n\");\n        mk_file(&tmp.path().join(\"src/lib.rs\"), \"x\");\n        let cache_marker = tmp.path().join(\"target/bacon-ls-live/target/CACHE_MARKER\");\n        mk_file(&cache_marker, \"previous build artifacts\");\n\n        ShadowWorkspace::build(tmp.path().to_path_buf()).await.unwrap();\n        assert!(\n            cache_marker.exists(),\n            \"live target dir must persist across rebuilds so cargo can reuse its cache\"\n        );\n    }\n\n    #[tokio::test]\n    #[cfg(unix)]\n    async fn test_write_dirty_replaces_hardlink_with_distinct_inode() {\n        let tmp = TempDir::new().unwrap();\n        mk_file(&tmp.path().join(\".gitignore\"), \"target/\\n\");\n        let lib_rs = tmp.path().join(\"src/lib.rs\");\n        mk_file(&lib_rs, \"saved\");\n        let shadow = ShadowWorkspace::build(tmp.path().to_path_buf()).await.unwrap();\n        let shadow_lib_rs = shadow.shadow_root().join(\"src/lib.rs\");\n        assert_eq!(inode(&lib_rs), inode(&shadow_lib_rs));\n\n        shadow.write_dirty(&lib_rs, \"dirty buffer\").await.unwrap();\n\n        assert_ne!(\n            inode(&lib_rs),\n            inode(&shadow_lib_rs),\n            \"write_dirty must break the hardlink\"\n        );\n        assert_eq!(\n            std::fs::read_to_string(&shadow_lib_rs).unwrap(),\n            \"dirty buffer\",\n            \"shadow now carries dirty content\"\n        );\n        assert_eq!(\n            std::fs::read_to_string(&lib_rs).unwrap(),\n            \"saved\",\n            \"real file must be untouched\"\n        );\n    }\n\n    #[tokio::test]\n    #[cfg(unix)]\n    async fn test_restore_link_reverts_to_hardlink() {\n        let tmp = TempDir::new().unwrap();\n        mk_file(&tmp.path().join(\".gitignore\"), \"target/\\n\");\n        let lib_rs = tmp.path().join(\"src/lib.rs\");\n        mk_file(&lib_rs, \"saved\");\n        let shadow = ShadowWorkspace::build(tmp.path().to_path_buf()).await.unwrap();\n        let shadow_lib_rs = shadow.shadow_root().join(\"src/lib.rs\");\n\n        shadow.write_dirty(&lib_rs, \"dirty\").await.unwrap();\n        assert_ne!(inode(&lib_rs), inode(&shadow_lib_rs));\n\n        shadow.restore_link(&lib_rs).await.unwrap();\n        assert_eq!(\n            inode(&lib_rs),\n            inode(&shadow_lib_rs),\n            \"restore_link must hardlink shadow back to the real file\"\n        );\n        assert_eq!(std::fs::read_to_string(&shadow_lib_rs).unwrap(), \"saved\");\n    }\n\n    #[tokio::test]\n    async fn test_write_dirty_creates_missing_parent_dirs() {\n        let tmp = TempDir::new().unwrap();\n        mk_file(&tmp.path().join(\".gitignore\"), \"target/\\n\");\n        mk_file(&tmp.path().join(\"src/lib.rs\"), \"x\");\n        let shadow = ShadowWorkspace::build(tmp.path().to_path_buf()).await.unwrap();\n\n        // A new file the user just created in the editor — parent shadow dir\n        // doesn't exist yet (file was added after the initial mirror).\n        let new_file = tmp.path().join(\"src/new/nested/mod.rs\");\n        std::fs::create_dir_all(new_file.parent().unwrap()).unwrap();\n        std::fs::write(&new_file, \"real\").unwrap();\n\n        shadow.write_dirty(&new_file, \"in-editor draft\").await.unwrap();\n        let shadow_path = shadow.shadow_root().join(\"src/new/nested/mod.rs\");\n        assert_eq!(std::fs::read_to_string(&shadow_path).unwrap(), \"in-editor draft\");\n    }\n\n    #[tokio::test]\n    async fn test_shadow_path_for_outside_workspace_errors() {\n        let tmp = TempDir::new().unwrap();\n        mk_file(&tmp.path().join(\".gitignore\"), \"target/\\n\");\n        mk_file(&tmp.path().join(\"src/lib.rs\"), \"x\");\n        let shadow = ShadowWorkspace::build(tmp.path().to_path_buf()).await.unwrap();\n\n        let outside = std::path::PathBuf::from(\"/etc/passwd\");\n        let err = shadow.shadow_path_for(&outside).unwrap_err();\n        assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput);\n    }\n}\n"
  },
  {
    "path": "src/testdata/expansion-needed.json",
    "content": "{\"reason\":\"compiler-message\",\"package_id\":\"path+file:///home/thomas/Projects/bacon-ls-bug#bacon-ls@0.26.0\",\"manifest_path\":\"/home/thomas/Projects/bacon-ls-bug/Cargo.toml\",\"target\":{\"kind\":[\"lib\"],\"crate_types\":[\"lib\"],\"name\":\"bacon_ls\",\"src_path\":\"/home/thomas/Projects/bacon-ls-bug/src/lib.rs\",\"edition\":\"2024\",\"doc\":true,\"doctest\":true,\"test\":true},\"message\":{\"$message_type\":\"diagnostic\",\"message\":\"`(Result<(), anyhow::Error>, HashMap<Uri, (Vec<ls_types::Diagnostic>, bool)>)` is not a future\",\"code\":{\"code\":\"E0277\",\"explanation\":\"You tried to use a type which doesn't implement some trait in a place which\\nexpected that trait.\\n\\nErroneous code example:\\n\\n```compile_fail,E0277\\n// here we declare the Foo trait with a bar method\\ntrait Foo {\\n    fn bar(&self);\\n}\\n\\n// we now declare a function which takes an object implementing the Foo trait\\nfn some_func<T: Foo>(foo: T) {\\n    foo.bar();\\n}\\n\\nfn main() {\\n    // we now call the method with the i32 type, which doesn't implement\\n    // the Foo trait\\n    some_func(5i32); // error: the trait bound `i32 : Foo` is not satisfied\\n}\\n```\\n\\nIn order to fix this error, verify that the type you're using does implement\\nthe trait. Example:\\n\\n```\\ntrait Foo {\\n    fn bar(&self);\\n}\\n\\n// we implement the trait on the i32 type\\nimpl Foo for i32 {\\n    fn bar(&self) {}\\n}\\n\\nfn some_func<T: Foo>(foo: T) {\\n    foo.bar(); // we can now use this method since i32 implements the\\n               // Foo trait\\n}\\n\\nfn main() {\\n    some_func(5i32); // ok!\\n}\\n```\\n\\nOr in a generic context, an erroneous code example would look like:\\n\\n```compile_fail,E0277\\nfn some_func<T>(foo: T) {\\n    println!(\\\"{:?}\\\", foo); // error: the trait `core::fmt::Debug` is not\\n                           //        implemented for the type `T`\\n}\\n\\nfn main() {\\n    // We now call the method with the i32 type,\\n    // which *does* implement the Debug trait.\\n    some_func(5i32);\\n}\\n```\\n\\nNote that the error here is in the definition of the generic function. Although\\nwe only call it with a parameter that does implement `Debug`, the compiler\\nstill rejects the function. It must work with all possible input types. In\\norder to make this example compile, we need to restrict the generic type we're\\naccepting:\\n\\n```\\nuse std::fmt;\\n\\n// Restrict the input type to types that implement Debug.\\nfn some_func<T: fmt::Debug>(foo: T) {\\n    println!(\\\"{:?}\\\", foo);\\n}\\n\\nfn main() {\\n    // Calling the method is still fine, as i32 implements Debug.\\n    some_func(5i32);\\n\\n    // This would fail to compile now:\\n    // struct WithoutDebug;\\n    // some_func(WithoutDebug);\\n}\\n```\\n\\nRust only looks at the signature of the called function, as such it must\\nalready specify all requirements that will be used for every type parameter.\\n\"},\"level\":\"error\",\"spans\":[{\"file_name\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tokio-1.50.0/src/macros/select.rs\",\"byte_start\":28098,\"byte_end\":28144,\"line_start\":650,\"line_end\":650,\"column_start\":25,\"column_end\":71,\"is_primary\":true,\"text\":[{\"text\":\"                        $crate::count_field!( futures_init.$($skip)* )\",\"highlight_start\":25,\"highlight_end\":71}],\"label\":\"`(Result<(), anyhow::Error>, HashMap<Uri, (Vec<ls_types::Diagnostic>, bool)>)` is not a future\",\"suggested_replacement\":null,\"suggestion_applicability\":null,\"expansion\":{\"span\":{\"file_name\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tokio-1.50.0/src/macros/select.rs\",\"byte_start\":33218,\"byte_end\":33325,\"line_start\":760,\"line_end\":760,\"column_start\":9,\"column_end\":116,\"is_primary\":false,\"text\":[{\"text\":\"        $crate::select!(@{ start=$start; $($t)*; panic!(\\\"all branches are disabled and there is no else branch\\\") })\",\"highlight_start\":9,\"highlight_end\":116}],\"label\":null,\"suggested_replacement\":null,\"suggestion_applicability\":null,\"expansion\":{\"span\":{\"file_name\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tokio-1.50.0/src/macros/select.rs\",\"byte_start\":34226,\"byte_end\":34319,\"line_start\":775,\"line_end\":775,\"column_start\":9,\"column_end\":102,\"is_primary\":false,\"text\":[{\"text\":\"        $crate::select!(@{ start=$start; ($($s)* _) $($t)* ($($s)*) $p = $f, if true => $h, } $($r)*)\",\"highlight_start\":9,\"highlight_end\":102}],\"label\":null,\"suggested_replacement\":null,\"suggestion_applicability\":null,\"expansion\":{\"span\":{\"file_name\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tokio-1.50.0/src/macros/select.rs\",\"byte_start\":35046,\"byte_end\":35139,\"line_start\":787,\"line_end\":787,\"column_start\":9,\"column_end\":102,\"is_primary\":false,\"text\":[{\"text\":\"        $crate::select!(@{ start=$start; ($($s)* _) $($t)* ($($s)*) $p = $f, if true => $h, } $($r)*)\",\"highlight_start\":9,\"highlight_end\":102}],\"label\":null,\"suggested_replacement\":null,\"suggestion_applicability\":null,\"expansion\":{\"span\":{\"file_name\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tokio-1.50.0/src/macros/select.rs\",\"byte_start\":35533,\"byte_end\":35628,\"line_start\":803,\"line_end\":803,\"column_start\":9,\"column_end\":104,\"is_primary\":false,\"text\":[{\"text\":\"        $crate::select!(@{ start={ $crate::macros::support::thread_rng_n(BRANCHES) }; () } $p = $($t)*)\",\"highlight_start\":9,\"highlight_end\":104}],\"label\":null,\"suggested_replacement\":null,\"suggestion_applicability\":null,\"expansion\":{\"span\":{\"file_name\":\"src/lib.rs\",\"byte_start\":25953,\"byte_end\":26250,\"line_start\":708,\"line_end\":715,\"column_start\":22,\"column_end\":10,\"is_primary\":false,\"text\":[{\"text\":\"        let result = tokio::select! {\",\"highlight_start\":22,\"highlight_end\":38},{\"text\":\"            result = combined =>  result,\",\"highlight_start\":1,\"highlight_end\":42},{\"text\":\"            () = cancel_token.cancelled() => {\",\"highlight_start\":1,\"highlight_end\":47},{\"text\":\"                tracing::info!(\\\"cargo run cancelled by newer request\\\");\",\"highlight_start\":1,\"highlight_end\":72},{\"text\":\"                progress.finish_with_message(\\\"canceled by user\\\").await;\",\"highlight_start\":1,\"highlight_end\":72},{\"text\":\"                return;\",\"highlight_start\":1,\"highlight_end\":24},{\"text\":\"            }\",\"highlight_start\":1,\"highlight_end\":14},{\"text\":\"        };\",\"highlight_start\":1,\"highlight_end\":10}],\"label\":null,\"suggested_replacement\":null,\"suggestion_applicability\":null,\"expansion\":null},\"macro_decl_name\":\"tokio::select!\",\"def_site_span\":{\"file_name\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tokio-1.50.0/src/macros/select.rs\",\"byte_start\":24368,\"byte_end\":24387,\"line_start\":571,\"line_end\":571,\"column_start\":7,\"column_end\":26,\"is_primary\":false,\"text\":[{\"text\":\"doc! {macro_rules! select {\",\"highlight_start\":7,\"highlight_end\":26}],\"label\":null,\"suggested_replacement\":null,\"suggestion_applicability\":null,\"expansion\":null}}},\"macro_decl_name\":\"$crate::select!\",\"def_site_span\":{\"file_name\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tokio-1.50.0/src/macros/select.rs\",\"byte_start\":24368,\"byte_end\":24387,\"line_start\":571,\"line_end\":571,\"column_start\":7,\"column_end\":26,\"is_primary\":false,\"text\":[{\"text\":\"doc! {macro_rules! select {\",\"highlight_start\":7,\"highlight_end\":26}],\"label\":null,\"suggested_replacement\":null,\"suggestion_applicability\":null,\"expansion\":null}}},\"macro_decl_name\":\"$crate::select!\",\"def_site_span\":{\"file_name\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tokio-1.50.0/src/macros/select.rs\",\"byte_start\":24368,\"byte_end\":24387,\"line_start\":571,\"line_end\":571,\"column_start\":7,\"column_end\":26,\"is_primary\":false,\"text\":[{\"text\":\"doc! {macro_rules! select {\",\"highlight_start\":7,\"highlight_end\":26}],\"label\":null,\"suggested_replacement\":null,\"suggestion_applicability\":null,\"expansion\":null}}},\"macro_decl_name\":\"$crate::select!\",\"def_site_span\":{\"file_name\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tokio-1.50.0/src/macros/select.rs\",\"byte_start\":24368,\"byte_end\":24387,\"line_start\":571,\"line_end\":571,\"column_start\":7,\"column_end\":26,\"is_primary\":false,\"text\":[{\"text\":\"doc! {macro_rules! select {\",\"highlight_start\":7,\"highlight_end\":26}],\"label\":null,\"suggested_replacement\":null,\"suggestion_applicability\":null,\"expansion\":null}}},\"macro_decl_name\":\"$crate::select!\",\"def_site_span\":{\"file_name\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tokio-1.50.0/src/macros/select.rs\",\"byte_start\":24368,\"byte_end\":24387,\"line_start\":571,\"line_end\":571,\"column_start\":7,\"column_end\":26,\"is_primary\":false,\"text\":[{\"text\":\"doc! {macro_rules! select {\",\"highlight_start\":7,\"highlight_end\":26}],\"label\":null,\"suggested_replacement\":null,\"suggestion_applicability\":null,\"expansion\":null}}},{\"file_name\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tokio-1.50.0/src/macros/select.rs\",\"byte_start\":28024,\"byte_end\":28072,\"line_start\":649,\"line_end\":649,\"column_start\":35,\"column_end\":83,\"is_primary\":false,\"text\":[{\"text\":\"            let mut futures = ($( $crate::macros::support::IntoFuture::into_future(\",\"highlight_start\":35,\"highlight_end\":83}],\"label\":\"required by a bound introduced by this call\",\"suggested_replacement\":null,\"suggestion_applicability\":null,\"expansion\":{\"span\":{\"file_name\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tokio-1.50.0/src/macros/select.rs\",\"byte_start\":33218,\"byte_end\":33325,\"line_start\":760,\"line_end\":760,\"column_start\":9,\"column_end\":116,\"is_primary\":false,\"text\":[{\"text\":\"        $crate::select!(@{ start=$start; $($t)*; panic!(\\\"all branches are disabled and there is no else branch\\\") })\",\"highlight_start\":9,\"highlight_end\":116}],\"label\":null,\"suggested_replacement\":null,\"suggestion_applicability\":null,\"expansion\":{\"span\":{\"file_name\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tokio-1.50.0/src/macros/select.rs\",\"byte_start\":34226,\"byte_end\":34319,\"line_start\":775,\"line_end\":775,\"column_start\":9,\"column_end\":102,\"is_primary\":false,\"text\":[{\"text\":\"        $crate::select!(@{ start=$start; ($($s)* _) $($t)* ($($s)*) $p = $f, if true => $h, } $($r)*)\",\"highlight_start\":9,\"highlight_end\":102}],\"label\":null,\"suggested_replacement\":null,\"suggestion_applicability\":null,\"expansion\":{\"span\":{\"file_name\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tokio-1.50.0/src/macros/select.rs\",\"byte_start\":35046,\"byte_end\":35139,\"line_start\":787,\"line_end\":787,\"column_start\":9,\"column_end\":102,\"is_primary\":false,\"text\":[{\"text\":\"        $crate::select!(@{ start=$start; ($($s)* _) $($t)* ($($s)*) $p = $f, if true => $h, } $($r)*)\",\"highlight_start\":9,\"highlight_end\":102}],\"label\":null,\"suggested_replacement\":null,\"suggestion_applicability\":null,\"expansion\":{\"span\":{\"file_name\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tokio-1.50.0/src/macros/select.rs\",\"byte_start\":35533,\"byte_end\":35628,\"line_start\":803,\"line_end\":803,\"column_start\":9,\"column_end\":104,\"is_primary\":false,\"text\":[{\"text\":\"        $crate::select!(@{ start={ $crate::macros::support::thread_rng_n(BRANCHES) }; () } $p = $($t)*)\",\"highlight_start\":9,\"highlight_end\":104}],\"label\":null,\"suggested_replacement\":null,\"suggestion_applicability\":null,\"expansion\":{\"span\":{\"file_name\":\"src/lib.rs\",\"byte_start\":25953,\"byte_end\":26250,\"line_start\":708,\"line_end\":715,\"column_start\":22,\"column_end\":10,\"is_primary\":false,\"text\":[{\"text\":\"        let result = tokio::select! {\",\"highlight_start\":22,\"highlight_end\":38},{\"text\":\"            result = combined =>  result,\",\"highlight_start\":1,\"highlight_end\":42},{\"text\":\"            () = cancel_token.cancelled() => {\",\"highlight_start\":1,\"highlight_end\":47},{\"text\":\"                tracing::info!(\\\"cargo run cancelled by newer request\\\");\",\"highlight_start\":1,\"highlight_end\":72},{\"text\":\"                progress.finish_with_message(\\\"canceled by user\\\").await;\",\"highlight_start\":1,\"highlight_end\":72},{\"text\":\"                return;\",\"highlight_start\":1,\"highlight_end\":24},{\"text\":\"            }\",\"highlight_start\":1,\"highlight_end\":14},{\"text\":\"        };\",\"highlight_start\":1,\"highlight_end\":10}],\"label\":null,\"suggested_replacement\":null,\"suggestion_applicability\":null,\"expansion\":null},\"macro_decl_name\":\"tokio::select!\",\"def_site_span\":{\"file_name\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tokio-1.50.0/src/macros/select.rs\",\"byte_start\":24368,\"byte_end\":24387,\"line_start\":571,\"line_end\":571,\"column_start\":7,\"column_end\":26,\"is_primary\":false,\"text\":[{\"text\":\"doc! {macro_rules! select {\",\"highlight_start\":7,\"highlight_end\":26}],\"label\":null,\"suggested_replacement\":null,\"suggestion_applicability\":null,\"expansion\":null}}},\"macro_decl_name\":\"$crate::select!\",\"def_site_span\":{\"file_name\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tokio-1.50.0/src/macros/select.rs\",\"byte_start\":24368,\"byte_end\":24387,\"line_start\":571,\"line_end\":571,\"column_start\":7,\"column_end\":26,\"is_primary\":false,\"text\":[{\"text\":\"doc! {macro_rules! select {\",\"highlight_start\":7,\"highlight_end\":26}],\"label\":null,\"suggested_replacement\":null,\"suggestion_applicability\":null,\"expansion\":null}}},\"macro_decl_name\":\"$crate::select!\",\"def_site_span\":{\"file_name\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tokio-1.50.0/src/macros/select.rs\",\"byte_start\":24368,\"byte_end\":24387,\"line_start\":571,\"line_end\":571,\"column_start\":7,\"column_end\":26,\"is_primary\":false,\"text\":[{\"text\":\"doc! {macro_rules! select {\",\"highlight_start\":7,\"highlight_end\":26}],\"label\":null,\"suggested_replacement\":null,\"suggestion_applicability\":null,\"expansion\":null}}},\"macro_decl_name\":\"$crate::select!\",\"def_site_span\":{\"file_name\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tokio-1.50.0/src/macros/select.rs\",\"byte_start\":24368,\"byte_end\":24387,\"line_start\":571,\"line_end\":571,\"column_start\":7,\"column_end\":26,\"is_primary\":false,\"text\":[{\"text\":\"doc! {macro_rules! select {\",\"highlight_start\":7,\"highlight_end\":26}],\"label\":null,\"suggested_replacement\":null,\"suggestion_applicability\":null,\"expansion\":null}}},\"macro_decl_name\":\"$crate::select!\",\"def_site_span\":{\"file_name\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tokio-1.50.0/src/macros/select.rs\",\"byte_start\":24368,\"byte_end\":24387,\"line_start\":571,\"line_end\":571,\"column_start\":7,\"column_end\":26,\"is_primary\":false,\"text\":[{\"text\":\"doc! {macro_rules! select {\",\"highlight_start\":7,\"highlight_end\":26}],\"label\":null,\"suggested_replacement\":null,\"suggestion_applicability\":null,\"expansion\":null}}}],\"children\":[{\"message\":\"the trait `Future` is not implemented for `(Result<(), anyhow::Error>, HashMap<Uri, (Vec<ls_types::Diagnostic>, bool)>)`\",\"code\":null,\"level\":\"help\",\"spans\":[],\"children\":[],\"rendered\":null},{\"message\":\"(Result<(), anyhow::Error>, HashMap<Uri, (Vec<ls_types::Diagnostic>, bool)>) must be a future or must implement `IntoFuture` to be awaited\",\"code\":null,\"level\":\"note\",\"spans\":[],\"children\":[],\"rendered\":null},{\"message\":\"required for `(Result<(), anyhow::Error>, HashMap<Uri, (Vec<ls_types::Diagnostic>, bool)>)` to implement `IntoFuture`\",\"code\":null,\"level\":\"note\",\"spans\":[],\"children\":[],\"rendered\":null}],\"rendered\":\"\\u001b[1m\\u001b[91merror[E0277]\\u001b[0m\\u001b[1m: `(Result<(), anyhow::Error>, HashMap<Uri, (Vec<ls_types::Diagnostic>, bool)>)` is not a future\\u001b[0m\\n   \\u001b[1m\\u001b[94m--> \\u001b[0msrc/lib.rs:708:22\\n    \\u001b[1m\\u001b[94m|\\u001b[0m\\n\\u001b[1m\\u001b[94m708\\u001b[0m \\u001b[1m\\u001b[94m|\\u001b[0m           let result = tokio::select! {\\n    \\u001b[1m\\u001b[94m|\\u001b[0m \\u001b[1m\\u001b[91m ______________________^\\u001b[0m\\n\\u001b[1m\\u001b[94m709\\u001b[0m \\u001b[1m\\u001b[94m|\\u001b[0m \\u001b[1m\\u001b[91m|\\u001b[0m             result = combined =>  result,\\n\\u001b[1m\\u001b[94m710\\u001b[0m \\u001b[1m\\u001b[94m|\\u001b[0m \\u001b[1m\\u001b[91m|\\u001b[0m             () = cancel_token.cancelled() => {\\n\\u001b[1m\\u001b[94m711\\u001b[0m \\u001b[1m\\u001b[94m|\\u001b[0m \\u001b[1m\\u001b[91m|\\u001b[0m                 tracing::info!(\\\"cargo run cancelled by newer request\\\");\\n\\u001b[1m\\u001b[94m...\\u001b[0m   \\u001b[1m\\u001b[91m|\\u001b[0m\\n\\u001b[1m\\u001b[94m715\\u001b[0m \\u001b[1m\\u001b[94m|\\u001b[0m \\u001b[1m\\u001b[91m|\\u001b[0m         };\\n    \\u001b[1m\\u001b[94m|\\u001b[0m \\u001b[1m\\u001b[91m|\\u001b[0m         \\u001b[1m\\u001b[91m^\\u001b[0m\\n    \\u001b[1m\\u001b[94m|\\u001b[0m \\u001b[1m\\u001b[91m|\\u001b[0m         \\u001b[1m\\u001b[91m|\\u001b[0m\\n    \\u001b[1m\\u001b[94m|\\u001b[0m \\u001b[1m\\u001b[91m|_________`(Result<(), anyhow::Error>, HashMap<Uri, (Vec<ls_types::Diagnostic>, bool)>)` is not a future\\u001b[0m\\n    \\u001b[1m\\u001b[94m|\\u001b[0m           \\u001b[1m\\u001b[91mrequired by a bound introduced by this call\\u001b[0m\\n    \\u001b[1m\\u001b[94m|\\u001b[0m\\n    \\u001b[1m\\u001b[94m= \\u001b[0m\\u001b[1mhelp\\u001b[0m: the trait `Future` is not implemented for `(Result<(), anyhow::Error>, HashMap<Uri, (Vec<ls_types::Diagnostic>, bool)>)`\\n    \\u001b[1m\\u001b[94m= \\u001b[0m\\u001b[1mnote\\u001b[0m: (Result<(), anyhow::Error>, HashMap<Uri, (Vec<ls_types::Diagnostic>, bool)>) must be a future or must implement `IntoFuture` to be awaited\\n    \\u001b[1m\\u001b[94m= \\u001b[0m\\u001b[1mnote\\u001b[0m: required for `(Result<(), anyhow::Error>, HashMap<Uri, (Vec<ls_types::Diagnostic>, bool)>)` to implement `IntoFuture`\\n    \\u001b[1m\\u001b[94m= \\u001b[0m\\u001b[1mnote\\u001b[0m: this error originates in the macro `$crate::select` which comes from the expansion of the macro `tokio::select` (in Nightly builds, run with -Z macro-backtrace for more info)\\n\\n\"}}\n"
  },
  {
    "path": "src/testdata/unused-import-compact.json",
    "content": "{\"reason\":\"compiler-message\",\"package_id\":\"path+file:///home/thomas/Projects/bacon-ls-bug#bacon-ls@0.26.0\",\"manifest_path\":\"/home/thomas/Projects/bacon-ls-bug/Cargo.toml\",\"target\":{\"kind\":[\"lib\"],\"crate_types\":[\"lib\"],\"name\":\"bacon_ls\",\"src_path\":\"/home/thomas/Projects/bacon-ls-bug/src/lib.rs\",\"edition\":\"2024\",\"doc\":true,\"doctest\":true,\"test\":true},\"message\":{\"$message_type\":\"diagnostic\",\"message\":\"unused import: `Compact`\",\"code\":{\"code\":\"unused_imports\",\"explanation\":null},\"level\":\"warning\",\"spans\":[{\"file_name\":\"src/lib.rs\",\"byte_start\":563,\"byte_end\":570,\"line_start\":19,\"line_end\":19,\"column_start\":39,\"column_end\":46,\"is_primary\":true,\"text\":[{\"text\":\"use tracing_subscriber::fmt::format::{Compact, FmtSpan};\",\"highlight_start\":39,\"highlight_end\":46}],\"label\":null,\"suggested_replacement\":null,\"suggestion_applicability\":null,\"expansion\":null}],\"children\":[{\"message\":\"`#[warn(unused_imports)]` (part of `#[warn(unused)]`) on by default\",\"code\":null,\"level\":\"note\",\"spans\":[],\"children\":[],\"rendered\":null},{\"message\":\"remove the unused import\",\"code\":null,\"level\":\"help\",\"spans\":[{\"file_name\":\"src/lib.rs\",\"byte_start\":563,\"byte_end\":572,\"line_start\":19,\"line_end\":19,\"column_start\":39,\"column_end\":48,\"is_primary\":true,\"text\":[{\"text\":\"use tracing_subscriber::fmt::format::{Compact, FmtSpan};\",\"highlight_start\":39,\"highlight_end\":48}],\"label\":null,\"suggested_replacement\":\"\",\"suggestion_applicability\":\"MachineApplicable\",\"expansion\":null},{\"file_name\":\"src/lib.rs\",\"byte_start\":562,\"byte_end\":563,\"line_start\":19,\"line_end\":19,\"column_start\":38,\"column_end\":39,\"is_primary\":true,\"text\":[{\"text\":\"use tracing_subscriber::fmt::format::{Compact, FmtSpan};\",\"highlight_start\":38,\"highlight_end\":39}],\"label\":null,\"suggested_replacement\":\"\",\"suggestion_applicability\":\"MachineApplicable\",\"expansion\":null},{\"file_name\":\"src/lib.rs\",\"byte_start\":579,\"byte_end\":580,\"line_start\":19,\"line_end\":19,\"column_start\":55,\"column_end\":56,\"is_primary\":true,\"text\":[{\"text\":\"use tracing_subscriber::fmt::format::{Compact, FmtSpan};\",\"highlight_start\":55,\"highlight_end\":56}],\"label\":null,\"suggested_replacement\":\"\",\"suggestion_applicability\":\"MachineApplicable\",\"expansion\":null}],\"children\":[],\"rendered\":null}],\"rendered\":\"\\u001b[1m\\u001b[33mwarning\\u001b[0m\\u001b[1m: unused import: `Compact`\\u001b[0m\\n  \\u001b[1m\\u001b[94m--> \\u001b[0msrc/lib.rs:19:39\\n   \\u001b[1m\\u001b[94m|\\u001b[0m\\n\\u001b[1m\\u001b[94m19\\u001b[0m \\u001b[1m\\u001b[94m|\\u001b[0m use tracing_subscriber::fmt::format::{Compact, FmtSpan};\\n   \\u001b[1m\\u001b[94m|\\u001b[0m                                       \\u001b[1m\\u001b[33m^^^^^^^\\u001b[0m\\n   \\u001b[1m\\u001b[94m|\\u001b[0m\\n   \\u001b[1m\\u001b[94m= \\u001b[0m\\u001b[1mnote\\u001b[0m: `#[warn(unused_imports)]` (part of `#[warn(unused)]`) on by default\\n\\n\"}}\n"
  },
  {
    "path": "src/testdata/unused-import-line.json",
    "content": "{\"reason\":\"compiler-message\",\"package_id\":\"path+file:///home/thomas/Projects/bacon-ls-bug#bacon-ls@0.26.0\",\"manifest_path\":\"/home/thomas/Projects/bacon-ls-bug/Cargo.toml\",\"target\":{\"kind\":[\"lib\"],\"crate_types\":[\"lib\"],\"name\":\"bacon_ls\",\"src_path\":\"/home/thomas/Projects/bacon-ls-bug/src/lib.rs\",\"edition\":\"2024\",\"doc\":true,\"doctest\":true,\"test\":true},\"message\":{\"$message_type\":\"diagnostic\",\"message\":\"unused import: `std::io::Cursor`\",\"code\":{\"code\":\"unused_imports\",\"explanation\":null},\"level\":\"warning\",\"spans\":[{\"file_name\":\"src/lib.rs\",\"byte_start\":880,\"byte_end\":895,\"line_start\":30,\"line_end\":30,\"column_start\":5,\"column_end\":20,\"is_primary\":true,\"text\":[{\"text\":\"use std::io::Cursor;\",\"highlight_start\":5,\"highlight_end\":20}],\"label\":null,\"suggested_replacement\":null,\"suggestion_applicability\":null,\"expansion\":null}],\"children\":[{\"message\":\"`#[warn(unused_imports)]` (part of `#[warn(unused)]`) on by default\",\"code\":null,\"level\":\"note\",\"spans\":[],\"children\":[],\"rendered\":null},{\"message\":\"remove the whole `use` item\",\"code\":null,\"level\":\"help\",\"spans\":[{\"file_name\":\"src/lib.rs\",\"byte_start\":876,\"byte_end\":897,\"line_start\":30,\"line_end\":31,\"column_start\":1,\"column_end\":1,\"is_primary\":true,\"text\":[{\"text\":\"use std::io::Cursor;\",\"highlight_start\":1,\"highlight_end\":21},{\"text\":\"\",\"highlight_start\":1,\"highlight_end\":1}],\"label\":null,\"suggested_replacement\":\"\",\"suggestion_applicability\":\"MachineApplicable\",\"expansion\":null}],\"children\":[],\"rendered\":null}],\"rendered\":\"\\u001b[1m\\u001b[33mwarning\\u001b[0m\\u001b[1m: unused import: `std::io::Cursor`\\u001b[0m\\n  \\u001b[1m\\u001b[94m--> \\u001b[0msrc/lib.rs:30:5\\n   \\u001b[1m\\u001b[94m|\\u001b[0m\\n\\u001b[1m\\u001b[94m30\\u001b[0m \\u001b[1m\\u001b[94m|\\u001b[0m use std::io::Cursor;\\n   \\u001b[1m\\u001b[94m|\\u001b[0m     \\u001b[1m\\u001b[33m^^^^^^^^^^^^^^^\\u001b[0m\\n   \\u001b[1m\\u001b[94m|\\u001b[0m\\n   \\u001b[1m\\u001b[94m= \\u001b[0m\\u001b[1mnote\\u001b[0m: `#[warn(unused_imports)]` (part of `#[warn(unused)]`) on by default\\n\\n\"}}\n"
  },
  {
    "path": "src/testdata/unused-import.json",
    "content": "{\"reason\":\"compiler-artifact\",\"package_id\":\"registry+https://github.com/rust-lang/crates.io-index#proc-macro2@1.0.106\",\"manifest_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/proc-macro2-1.0.106/Cargo.toml\",\"target\":{\"kind\":[\"custom-build\"],\"crate_types\":[\"bin\"],\"name\":\"build-script-build\",\"src_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/proc-macro2-1.0.106/build.rs\",\"edition\":\"2021\",\"doc\":false,\"doctest\":false,\"test\":false},\"profile\":{\"opt_level\":\"0\",\"debuginfo\":0,\"debug_assertions\":true,\"overflow_checks\":true,\"test\":false},\"features\":[\"default\",\"proc-macro\"],\"filenames\":[\"/home/thomas/Projects/bacon-ls-bug/target/debug/build/proc-macro2-59fb65d883d68442/build-script-build\"],\"executable\":null,\"fresh\":true}\n{\"reason\":\"build-script-executed\",\"package_id\":\"registry+https://github.com/rust-lang/crates.io-index#proc-macro2@1.0.106\",\"linked_libs\":[],\"linked_paths\":[],\"cfgs\":[\"wrap_proc_macro\",\"proc_macro_span_location\",\"proc_macro_span_file\"],\"env\":[],\"out_dir\":\"/home/thomas/Projects/bacon-ls-bug/target/debug/build/proc-macro2-eca6b6b3659d092d/out\"}\n{\"reason\":\"compiler-artifact\",\"package_id\":\"registry+https://github.com/rust-lang/crates.io-index#quote@1.0.45\",\"manifest_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/quote-1.0.45/Cargo.toml\",\"target\":{\"kind\":[\"custom-build\"],\"crate_types\":[\"bin\"],\"name\":\"build-script-build\",\"src_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/quote-1.0.45/build.rs\",\"edition\":\"2021\",\"doc\":false,\"doctest\":false,\"test\":false},\"profile\":{\"opt_level\":\"0\",\"debuginfo\":0,\"debug_assertions\":true,\"overflow_checks\":true,\"test\":false},\"features\":[\"default\",\"proc-macro\"],\"filenames\":[\"/home/thomas/Projects/bacon-ls-bug/target/debug/build/quote-522f3ff9ee457532/build-script-build\"],\"executable\":null,\"fresh\":true}\n{\"reason\":\"compiler-artifact\",\"package_id\":\"registry+https://github.com/rust-lang/crates.io-index#unicode-ident@1.0.24\",\"manifest_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/unicode-ident-1.0.24/Cargo.toml\",\"target\":{\"kind\":[\"lib\"],\"crate_types\":[\"lib\"],\"name\":\"unicode_ident\",\"src_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/unicode-ident-1.0.24/src/lib.rs\",\"edition\":\"2021\",\"doc\":true,\"doctest\":true,\"test\":true},\"profile\":{\"opt_level\":\"0\",\"debuginfo\":0,\"debug_assertions\":true,\"overflow_checks\":true,\"test\":false},\"features\":[],\"filenames\":[\"/home/thomas/Projects/bacon-ls-bug/target/debug/deps/libunicode_ident-2046154b3dc3f7c4.rlib\",\"/home/thomas/Projects/bacon-ls-bug/target/debug/deps/libunicode_ident-2046154b3dc3f7c4.rmeta\"],\"executable\":null,\"fresh\":true}\n{\"reason\":\"build-script-executed\",\"package_id\":\"registry+https://github.com/rust-lang/crates.io-index#quote@1.0.45\",\"linked_libs\":[],\"linked_paths\":[],\"cfgs\":[],\"env\":[],\"out_dir\":\"/home/thomas/Projects/bacon-ls-bug/target/debug/build/quote-e7e3cb3fa7dec76d/out\"}\n{\"reason\":\"compiler-artifact\",\"package_id\":\"registry+https://github.com/rust-lang/crates.io-index#libc@0.2.183\",\"manifest_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/libc-0.2.183/Cargo.toml\",\"target\":{\"kind\":[\"custom-build\"],\"crate_types\":[\"bin\"],\"name\":\"build-script-build\",\"src_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/libc-0.2.183/build.rs\",\"edition\":\"2021\",\"doc\":false,\"doctest\":false,\"test\":false},\"profile\":{\"opt_level\":\"0\",\"debuginfo\":0,\"debug_assertions\":true,\"overflow_checks\":true,\"test\":false},\"features\":[\"default\",\"std\"],\"filenames\":[\"/home/thomas/Projects/bacon-ls-bug/target/debug/build/libc-c5f1dd3bf733624d/build-script-build\"],\"executable\":null,\"fresh\":true}\n{\"reason\":\"compiler-artifact\",\"package_id\":\"registry+https://github.com/rust-lang/crates.io-index#memchr@2.8.0\",\"manifest_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/memchr-2.8.0/Cargo.toml\",\"target\":{\"kind\":[\"lib\"],\"crate_types\":[\"lib\"],\"name\":\"memchr\",\"src_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/memchr-2.8.0/src/lib.rs\",\"edition\":\"2021\",\"doc\":true,\"doctest\":true,\"test\":true},\"profile\":{\"opt_level\":\"0\",\"debuginfo\":2,\"debug_assertions\":true,\"overflow_checks\":true,\"test\":false},\"features\":[\"alloc\",\"default\",\"std\"],\"filenames\":[\"/home/thomas/Projects/bacon-ls-bug/target/debug/deps/libmemchr-02031ae4878bcf1e.rmeta\"],\"executable\":null,\"fresh\":true}\n{\"reason\":\"compiler-artifact\",\"package_id\":\"registry+https://github.com/rust-lang/crates.io-index#serde_core@1.0.228\",\"manifest_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/serde_core-1.0.228/Cargo.toml\",\"target\":{\"kind\":[\"custom-build\"],\"crate_types\":[\"bin\"],\"name\":\"build-script-build\",\"src_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/serde_core-1.0.228/build.rs\",\"edition\":\"2021\",\"doc\":false,\"doctest\":false,\"test\":false},\"profile\":{\"opt_level\":\"0\",\"debuginfo\":0,\"debug_assertions\":true,\"overflow_checks\":true,\"test\":false},\"features\":[\"alloc\",\"result\",\"std\"],\"filenames\":[\"/home/thomas/Projects/bacon-ls-bug/target/debug/build/serde_core-96cd29d23555b9de/build-script-build\"],\"executable\":null,\"fresh\":true}\n{\"reason\":\"compiler-artifact\",\"package_id\":\"registry+https://github.com/rust-lang/crates.io-index#pin-project-lite@0.2.17\",\"manifest_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/pin-project-lite-0.2.17/Cargo.toml\",\"target\":{\"kind\":[\"lib\"],\"crate_types\":[\"lib\"],\"name\":\"pin_project_lite\",\"src_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/pin-project-lite-0.2.17/src/lib.rs\",\"edition\":\"2018\",\"doc\":true,\"doctest\":true,\"test\":true},\"profile\":{\"opt_level\":\"0\",\"debuginfo\":2,\"debug_assertions\":true,\"overflow_checks\":true,\"test\":false},\"features\":[],\"filenames\":[\"/home/thomas/Projects/bacon-ls-bug/target/debug/deps/libpin_project_lite-650d64e1ce7efc14.rmeta\"],\"executable\":null,\"fresh\":true}\n{\"reason\":\"compiler-artifact\",\"package_id\":\"registry+https://github.com/rust-lang/crates.io-index#futures-core@0.3.32\",\"manifest_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/futures-core-0.3.32/Cargo.toml\",\"target\":{\"kind\":[\"lib\"],\"crate_types\":[\"lib\"],\"name\":\"futures_core\",\"src_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/futures-core-0.3.32/src/lib.rs\",\"edition\":\"2018\",\"doc\":true,\"doctest\":true,\"test\":true},\"profile\":{\"opt_level\":\"0\",\"debuginfo\":2,\"debug_assertions\":true,\"overflow_checks\":true,\"test\":false},\"features\":[\"alloc\",\"default\",\"std\"],\"filenames\":[\"/home/thomas/Projects/bacon-ls-bug/target/debug/deps/libfutures_core-5583206b8c49eb29.rmeta\"],\"executable\":null,\"fresh\":true}\n{\"reason\":\"compiler-artifact\",\"package_id\":\"registry+https://github.com/rust-lang/crates.io-index#futures-sink@0.3.32\",\"manifest_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/futures-sink-0.3.32/Cargo.toml\",\"target\":{\"kind\":[\"lib\"],\"crate_types\":[\"lib\"],\"name\":\"futures_sink\",\"src_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/futures-sink-0.3.32/src/lib.rs\",\"edition\":\"2018\",\"doc\":true,\"doctest\":true,\"test\":true},\"profile\":{\"opt_level\":\"0\",\"debuginfo\":2,\"debug_assertions\":true,\"overflow_checks\":true,\"test\":false},\"features\":[\"alloc\",\"default\",\"std\"],\"filenames\":[\"/home/thomas/Projects/bacon-ls-bug/target/debug/deps/libfutures_sink-ce53412c0c73e956.rmeta\"],\"executable\":null,\"fresh\":true}\n{\"reason\":\"compiler-artifact\",\"package_id\":\"registry+https://github.com/rust-lang/crates.io-index#log@0.4.29\",\"manifest_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/log-0.4.29/Cargo.toml\",\"target\":{\"kind\":[\"lib\"],\"crate_types\":[\"lib\"],\"name\":\"log\",\"src_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/log-0.4.29/src/lib.rs\",\"edition\":\"2021\",\"doc\":true,\"doctest\":true,\"test\":true},\"profile\":{\"opt_level\":\"0\",\"debuginfo\":2,\"debug_assertions\":true,\"overflow_checks\":true,\"test\":false},\"features\":[],\"filenames\":[\"/home/thomas/Projects/bacon-ls-bug/target/debug/deps/liblog-eb59abea57a4ada2.rmeta\"],\"executable\":null,\"fresh\":true}\n{\"reason\":\"compiler-artifact\",\"package_id\":\"registry+https://github.com/rust-lang/crates.io-index#bitflags@2.11.0\",\"manifest_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/bitflags-2.11.0/Cargo.toml\",\"target\":{\"kind\":[\"lib\"],\"crate_types\":[\"lib\"],\"name\":\"bitflags\",\"src_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/bitflags-2.11.0/src/lib.rs\",\"edition\":\"2021\",\"doc\":true,\"doctest\":true,\"test\":true},\"profile\":{\"opt_level\":\"0\",\"debuginfo\":2,\"debug_assertions\":true,\"overflow_checks\":true,\"test\":false},\"features\":[],\"filenames\":[\"/home/thomas/Projects/bacon-ls-bug/target/debug/deps/libbitflags-7be3b8ecea4247a3.rmeta\"],\"executable\":null,\"fresh\":true}\n{\"reason\":\"compiler-artifact\",\"package_id\":\"registry+https://github.com/rust-lang/crates.io-index#serde@1.0.228\",\"manifest_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/serde-1.0.228/Cargo.toml\",\"target\":{\"kind\":[\"custom-build\"],\"crate_types\":[\"bin\"],\"name\":\"build-script-build\",\"src_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/serde-1.0.228/build.rs\",\"edition\":\"2021\",\"doc\":false,\"doctest\":false,\"test\":false},\"profile\":{\"opt_level\":\"0\",\"debuginfo\":0,\"debug_assertions\":true,\"overflow_checks\":true,\"test\":false},\"features\":[\"default\",\"derive\",\"serde_derive\",\"std\"],\"filenames\":[\"/home/thomas/Projects/bacon-ls-bug/target/debug/build/serde-3da16be2fc31a942/build-script-build\"],\"executable\":null,\"fresh\":true}\n{\"reason\":\"compiler-artifact\",\"package_id\":\"registry+https://github.com/rust-lang/crates.io-index#once_cell@1.21.4\",\"manifest_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/once_cell-1.21.4/Cargo.toml\",\"target\":{\"kind\":[\"lib\"],\"crate_types\":[\"lib\"],\"name\":\"once_cell\",\"src_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/once_cell-1.21.4/src/lib.rs\",\"edition\":\"2021\",\"doc\":true,\"doctest\":true,\"test\":true},\"profile\":{\"opt_level\":\"0\",\"debuginfo\":2,\"debug_assertions\":true,\"overflow_checks\":true,\"test\":false},\"features\":[\"alloc\",\"default\",\"race\",\"std\"],\"filenames\":[\"/home/thomas/Projects/bacon-ls-bug/target/debug/deps/libonce_cell-f031b7db5ef08372.rmeta\"],\"executable\":null,\"fresh\":true}\n{\"reason\":\"compiler-artifact\",\"package_id\":\"registry+https://github.com/rust-lang/crates.io-index#proc-macro2@1.0.106\",\"manifest_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/proc-macro2-1.0.106/Cargo.toml\",\"target\":{\"kind\":[\"lib\"],\"crate_types\":[\"lib\"],\"name\":\"proc_macro2\",\"src_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/proc-macro2-1.0.106/src/lib.rs\",\"edition\":\"2021\",\"doc\":true,\"doctest\":true,\"test\":true},\"profile\":{\"opt_level\":\"0\",\"debuginfo\":0,\"debug_assertions\":true,\"overflow_checks\":true,\"test\":false},\"features\":[\"default\",\"proc-macro\"],\"filenames\":[\"/home/thomas/Projects/bacon-ls-bug/target/debug/deps/libproc_macro2-0454c554b14b2896.rlib\",\"/home/thomas/Projects/bacon-ls-bug/target/debug/deps/libproc_macro2-0454c554b14b2896.rmeta\"],\"executable\":null,\"fresh\":true}\n{\"reason\":\"build-script-executed\",\"package_id\":\"registry+https://github.com/rust-lang/crates.io-index#libc@0.2.183\",\"linked_libs\":[],\"linked_paths\":[],\"cfgs\":[\"freebsd12\"],\"env\":[],\"out_dir\":\"/home/thomas/Projects/bacon-ls-bug/target/debug/build/libc-3871bc5681dc8fd0/out\"}\n{\"reason\":\"build-script-executed\",\"package_id\":\"registry+https://github.com/rust-lang/crates.io-index#serde_core@1.0.228\",\"linked_libs\":[],\"linked_paths\":[],\"cfgs\":[],\"env\":[],\"out_dir\":\"/home/thomas/Projects/bacon-ls-bug/target/debug/build/serde_core-63846a40900fd271/out\"}\n{\"reason\":\"compiler-artifact\",\"package_id\":\"registry+https://github.com/rust-lang/crates.io-index#aho-corasick@1.1.4\",\"manifest_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/aho-corasick-1.1.4/Cargo.toml\",\"target\":{\"kind\":[\"lib\"],\"crate_types\":[\"lib\"],\"name\":\"aho_corasick\",\"src_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/aho-corasick-1.1.4/src/lib.rs\",\"edition\":\"2021\",\"doc\":true,\"doctest\":true,\"test\":true},\"profile\":{\"opt_level\":\"0\",\"debuginfo\":2,\"debug_assertions\":true,\"overflow_checks\":true,\"test\":false},\"features\":[\"perf-literal\",\"std\"],\"filenames\":[\"/home/thomas/Projects/bacon-ls-bug/target/debug/deps/libaho_corasick-d3d730d9b397baa2.rmeta\"],\"executable\":null,\"fresh\":true}\n{\"reason\":\"build-script-executed\",\"package_id\":\"registry+https://github.com/rust-lang/crates.io-index#serde@1.0.228\",\"linked_libs\":[],\"linked_paths\":[],\"cfgs\":[\"if_docsrs_then_no_serde_core\"],\"env\":[],\"out_dir\":\"/home/thomas/Projects/bacon-ls-bug/target/debug/build/serde-c80382b27f70d824/out\"}\n{\"reason\":\"compiler-artifact\",\"package_id\":\"registry+https://github.com/rust-lang/crates.io-index#scopeguard@1.2.0\",\"manifest_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/scopeguard-1.2.0/Cargo.toml\",\"target\":{\"kind\":[\"lib\"],\"crate_types\":[\"lib\"],\"name\":\"scopeguard\",\"src_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/scopeguard-1.2.0/src/lib.rs\",\"edition\":\"2015\",\"doc\":true,\"doctest\":true,\"test\":true},\"profile\":{\"opt_level\":\"0\",\"debuginfo\":2,\"debug_assertions\":true,\"overflow_checks\":true,\"test\":false},\"features\":[],\"filenames\":[\"/home/thomas/Projects/bacon-ls-bug/target/debug/deps/libscopeguard-f9270d062a6d4284.rmeta\"],\"executable\":null,\"fresh\":true}\n{\"reason\":\"compiler-artifact\",\"package_id\":\"registry+https://github.com/rust-lang/crates.io-index#zmij@1.0.21\",\"manifest_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/zmij-1.0.21/Cargo.toml\",\"target\":{\"kind\":[\"custom-build\"],\"crate_types\":[\"bin\"],\"name\":\"build-script-build\",\"src_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/zmij-1.0.21/build.rs\",\"edition\":\"2021\",\"doc\":false,\"doctest\":false,\"test\":false},\"profile\":{\"opt_level\":\"0\",\"debuginfo\":0,\"debug_assertions\":true,\"overflow_checks\":true,\"test\":false},\"features\":[],\"filenames\":[\"/home/thomas/Projects/bacon-ls-bug/target/debug/build/zmij-e0f30f748dd677a6/build-script-build\"],\"executable\":null,\"fresh\":true}\n{\"reason\":\"compiler-artifact\",\"package_id\":\"registry+https://github.com/rust-lang/crates.io-index#ref-cast@1.0.25\",\"manifest_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/ref-cast-1.0.25/Cargo.toml\",\"target\":{\"kind\":[\"custom-build\"],\"crate_types\":[\"bin\"],\"name\":\"build-script-build\",\"src_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/ref-cast-1.0.25/build.rs\",\"edition\":\"2021\",\"doc\":false,\"doctest\":false,\"test\":false},\"profile\":{\"opt_level\":\"0\",\"debuginfo\":0,\"debug_assertions\":true,\"overflow_checks\":true,\"test\":false},\"features\":[],\"filenames\":[\"/home/thomas/Projects/bacon-ls-bug/target/debug/build/ref-cast-7673e3654dec34fc/build-script-build\"],\"executable\":null,\"fresh\":true}\n{\"reason\":\"compiler-artifact\",\"package_id\":\"registry+https://github.com/rust-lang/crates.io-index#regex-syntax@0.8.10\",\"manifest_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/regex-syntax-0.8.10/Cargo.toml\",\"target\":{\"kind\":[\"lib\"],\"crate_types\":[\"lib\"],\"name\":\"regex_syntax\",\"src_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/regex-syntax-0.8.10/src/lib.rs\",\"edition\":\"2021\",\"doc\":true,\"doctest\":true,\"test\":true},\"profile\":{\"opt_level\":\"0\",\"debuginfo\":2,\"debug_assertions\":true,\"overflow_checks\":true,\"test\":false},\"features\":[\"default\",\"std\",\"unicode\",\"unicode-age\",\"unicode-bool\",\"unicode-case\",\"unicode-gencat\",\"unicode-perl\",\"unicode-script\",\"unicode-segment\"],\"filenames\":[\"/home/thomas/Projects/bacon-ls-bug/target/debug/deps/libregex_syntax-aba6049db0c56027.rmeta\"],\"executable\":null,\"fresh\":true}\n{\"reason\":\"compiler-artifact\",\"package_id\":\"registry+https://github.com/rust-lang/crates.io-index#cfg-if@1.0.4\",\"manifest_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/cfg-if-1.0.4/Cargo.toml\",\"target\":{\"kind\":[\"lib\"],\"crate_types\":[\"lib\"],\"name\":\"cfg_if\",\"src_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/cfg-if-1.0.4/src/lib.rs\",\"edition\":\"2018\",\"doc\":true,\"doctest\":true,\"test\":true},\"profile\":{\"opt_level\":\"0\",\"debuginfo\":2,\"debug_assertions\":true,\"overflow_checks\":true,\"test\":false},\"features\":[],\"filenames\":[\"/home/thomas/Projects/bacon-ls-bug/target/debug/deps/libcfg_if-b8c685c3ec20d4c2.rmeta\"],\"executable\":null,\"fresh\":true}\n{\"reason\":\"compiler-artifact\",\"package_id\":\"registry+https://github.com/rust-lang/crates.io-index#futures-channel@0.3.32\",\"manifest_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/futures-channel-0.3.32/Cargo.toml\",\"target\":{\"kind\":[\"lib\"],\"crate_types\":[\"lib\"],\"name\":\"futures_channel\",\"src_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/futures-channel-0.3.32/src/lib.rs\",\"edition\":\"2018\",\"doc\":true,\"doctest\":true,\"test\":true},\"profile\":{\"opt_level\":\"0\",\"debuginfo\":2,\"debug_assertions\":true,\"overflow_checks\":true,\"test\":false},\"features\":[\"alloc\",\"futures-sink\",\"sink\",\"std\"],\"filenames\":[\"/home/thomas/Projects/bacon-ls-bug/target/debug/deps/libfutures_channel-7b469bc4b2d0bab2.rmeta\"],\"executable\":null,\"fresh\":true}\n{\"reason\":\"compiler-artifact\",\"package_id\":\"registry+https://github.com/rust-lang/crates.io-index#serde_json@1.0.149\",\"manifest_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/serde_json-1.0.149/Cargo.toml\",\"target\":{\"kind\":[\"custom-build\"],\"crate_types\":[\"bin\"],\"name\":\"build-script-build\",\"src_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/serde_json-1.0.149/build.rs\",\"edition\":\"2021\",\"doc\":false,\"doctest\":false,\"test\":false},\"profile\":{\"opt_level\":\"0\",\"debuginfo\":0,\"debug_assertions\":true,\"overflow_checks\":true,\"test\":false},\"features\":[\"default\",\"std\"],\"filenames\":[\"/home/thomas/Projects/bacon-ls-bug/target/debug/build/serde_json-bfdb398b92b2aace/build-script-build\"],\"executable\":null,\"fresh\":true}\n{\"reason\":\"compiler-artifact\",\"package_id\":\"registry+https://github.com/rust-lang/crates.io-index#quote@1.0.45\",\"manifest_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/quote-1.0.45/Cargo.toml\",\"target\":{\"kind\":[\"lib\"],\"crate_types\":[\"lib\"],\"name\":\"quote\",\"src_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/quote-1.0.45/src/lib.rs\",\"edition\":\"2021\",\"doc\":true,\"doctest\":true,\"test\":true},\"profile\":{\"opt_level\":\"0\",\"debuginfo\":0,\"debug_assertions\":true,\"overflow_checks\":true,\"test\":false},\"features\":[\"default\",\"proc-macro\"],\"filenames\":[\"/home/thomas/Projects/bacon-ls-bug/target/debug/deps/libquote-ca0183ca5fc2f7d2.rlib\",\"/home/thomas/Projects/bacon-ls-bug/target/debug/deps/libquote-ca0183ca5fc2f7d2.rmeta\"],\"executable\":null,\"fresh\":true}\n{\"reason\":\"compiler-artifact\",\"package_id\":\"registry+https://github.com/rust-lang/crates.io-index#libc@0.2.183\",\"manifest_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/libc-0.2.183/Cargo.toml\",\"target\":{\"kind\":[\"lib\"],\"crate_types\":[\"lib\"],\"name\":\"libc\",\"src_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/libc-0.2.183/src/lib.rs\",\"edition\":\"2021\",\"doc\":true,\"doctest\":true,\"test\":true},\"profile\":{\"opt_level\":\"0\",\"debuginfo\":2,\"debug_assertions\":true,\"overflow_checks\":true,\"test\":false},\"features\":[\"default\",\"std\"],\"filenames\":[\"/home/thomas/Projects/bacon-ls-bug/target/debug/deps/liblibc-e7d3cec3bfcf91a0.rmeta\"],\"executable\":null,\"fresh\":true}\n{\"reason\":\"compiler-artifact\",\"package_id\":\"registry+https://github.com/rust-lang/crates.io-index#serde_core@1.0.228\",\"manifest_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/serde_core-1.0.228/Cargo.toml\",\"target\":{\"kind\":[\"lib\"],\"crate_types\":[\"lib\"],\"name\":\"serde_core\",\"src_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/serde_core-1.0.228/src/lib.rs\",\"edition\":\"2021\",\"doc\":true,\"doctest\":true,\"test\":true},\"profile\":{\"opt_level\":\"0\",\"debuginfo\":2,\"debug_assertions\":true,\"overflow_checks\":true,\"test\":false},\"features\":[\"alloc\",\"result\",\"std\"],\"filenames\":[\"/home/thomas/Projects/bacon-ls-bug/target/debug/deps/libserde_core-6df122f7891690db.rmeta\"],\"executable\":null,\"fresh\":true}\n{\"reason\":\"build-script-executed\",\"package_id\":\"registry+https://github.com/rust-lang/crates.io-index#zmij@1.0.21\",\"linked_libs\":[],\"linked_paths\":[],\"cfgs\":[],\"env\":[],\"out_dir\":\"/home/thomas/Projects/bacon-ls-bug/target/debug/build/zmij-61ed2ab881b5b6b1/out\"}\n{\"reason\":\"build-script-executed\",\"package_id\":\"registry+https://github.com/rust-lang/crates.io-index#ref-cast@1.0.25\",\"linked_libs\":[],\"linked_paths\":[],\"cfgs\":[],\"env\":[],\"out_dir\":\"/home/thomas/Projects/bacon-ls-bug/target/debug/build/ref-cast-8a2fc01194888fc3/out\"}\n{\"reason\":\"compiler-artifact\",\"package_id\":\"registry+https://github.com/rust-lang/crates.io-index#lock_api@0.4.14\",\"manifest_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/lock_api-0.4.14/Cargo.toml\",\"target\":{\"kind\":[\"lib\"],\"crate_types\":[\"lib\"],\"name\":\"lock_api\",\"src_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/lock_api-0.4.14/src/lib.rs\",\"edition\":\"2021\",\"doc\":true,\"doctest\":true,\"test\":true},\"profile\":{\"opt_level\":\"0\",\"debuginfo\":2,\"debug_assertions\":true,\"overflow_checks\":true,\"test\":false},\"features\":[\"atomic_usize\",\"default\"],\"filenames\":[\"/home/thomas/Projects/bacon-ls-bug/target/debug/deps/liblock_api-1c3bbf92d1550d7f.rmeta\"],\"executable\":null,\"fresh\":true}\n{\"reason\":\"compiler-artifact\",\"package_id\":\"registry+https://github.com/rust-lang/crates.io-index#regex-automata@0.4.14\",\"manifest_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/regex-automata-0.4.14/Cargo.toml\",\"target\":{\"kind\":[\"lib\"],\"crate_types\":[\"lib\"],\"name\":\"regex_automata\",\"src_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/regex-automata-0.4.14/src/lib.rs\",\"edition\":\"2021\",\"doc\":true,\"doctest\":true,\"test\":true},\"profile\":{\"opt_level\":\"0\",\"debuginfo\":2,\"debug_assertions\":true,\"overflow_checks\":true,\"test\":false},\"features\":[\"alloc\",\"dfa-build\",\"dfa-onepass\",\"dfa-search\",\"hybrid\",\"meta\",\"nfa-backtrack\",\"nfa-pikevm\",\"nfa-thompson\",\"perf-inline\",\"perf-literal\",\"perf-literal-multisubstring\",\"perf-literal-substring\",\"std\",\"syntax\",\"unicode\",\"unicode-age\",\"unicode-bool\",\"unicode-case\",\"unicode-gencat\",\"unicode-perl\",\"unicode-script\",\"unicode-segment\",\"unicode-word-boundary\"],\"filenames\":[\"/home/thomas/Projects/bacon-ls-bug/target/debug/deps/libregex_automata-67a952d5933f90a9.rmeta\"],\"executable\":null,\"fresh\":true}\n{\"reason\":\"compiler-artifact\",\"package_id\":\"registry+https://github.com/rust-lang/crates.io-index#futures-task@0.3.32\",\"manifest_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/futures-task-0.3.32/Cargo.toml\",\"target\":{\"kind\":[\"lib\"],\"crate_types\":[\"lib\"],\"name\":\"futures_task\",\"src_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/futures-task-0.3.32/src/lib.rs\",\"edition\":\"2018\",\"doc\":true,\"doctest\":true,\"test\":true},\"profile\":{\"opt_level\":\"0\",\"debuginfo\":2,\"debug_assertions\":true,\"overflow_checks\":true,\"test\":false},\"features\":[\"alloc\",\"std\"],\"filenames\":[\"/home/thomas/Projects/bacon-ls-bug/target/debug/deps/libfutures_task-3b1ef84e90015ea9.rmeta\"],\"executable\":null,\"fresh\":true}\n{\"reason\":\"compiler-artifact\",\"package_id\":\"registry+https://github.com/rust-lang/crates.io-index#crossbeam-utils@0.8.21\",\"manifest_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-utils-0.8.21/Cargo.toml\",\"target\":{\"kind\":[\"custom-build\"],\"crate_types\":[\"bin\"],\"name\":\"build-script-build\",\"src_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-utils-0.8.21/build.rs\",\"edition\":\"2021\",\"doc\":false,\"doctest\":false,\"test\":false},\"profile\":{\"opt_level\":\"0\",\"debuginfo\":0,\"debug_assertions\":true,\"overflow_checks\":true,\"test\":false},\"features\":[\"default\",\"std\"],\"filenames\":[\"/home/thomas/Projects/bacon-ls-bug/target/debug/build/crossbeam-utils-8e128d333107d4af/build-script-build\"],\"executable\":null,\"fresh\":true}\n{\"reason\":\"compiler-artifact\",\"package_id\":\"registry+https://github.com/rust-lang/crates.io-index#futures-io@0.3.32\",\"manifest_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/futures-io-0.3.32/Cargo.toml\",\"target\":{\"kind\":[\"lib\"],\"crate_types\":[\"lib\"],\"name\":\"futures_io\",\"src_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/futures-io-0.3.32/src/lib.rs\",\"edition\":\"2018\",\"doc\":true,\"doctest\":true,\"test\":true},\"profile\":{\"opt_level\":\"0\",\"debuginfo\":2,\"debug_assertions\":true,\"overflow_checks\":true,\"test\":false},\"features\":[\"std\"],\"filenames\":[\"/home/thomas/Projects/bacon-ls-bug/target/debug/deps/libfutures_io-886780a8b5419d3a.rmeta\"],\"executable\":null,\"fresh\":true}\n{\"reason\":\"compiler-artifact\",\"package_id\":\"registry+https://github.com/rust-lang/crates.io-index#slab@0.4.12\",\"manifest_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/slab-0.4.12/Cargo.toml\",\"target\":{\"kind\":[\"lib\"],\"crate_types\":[\"lib\"],\"name\":\"slab\",\"src_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/slab-0.4.12/src/lib.rs\",\"edition\":\"2018\",\"doc\":true,\"doctest\":true,\"test\":true},\"profile\":{\"opt_level\":\"0\",\"debuginfo\":2,\"debug_assertions\":true,\"overflow_checks\":true,\"test\":false},\"features\":[\"std\"],\"filenames\":[\"/home/thomas/Projects/bacon-ls-bug/target/debug/deps/libslab-200ef58dd2001770.rmeta\"],\"executable\":null,\"fresh\":true}\n{\"reason\":\"compiler-artifact\",\"package_id\":\"registry+https://github.com/rust-lang/crates.io-index#parking_lot_core@0.9.12\",\"manifest_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/parking_lot_core-0.9.12/Cargo.toml\",\"target\":{\"kind\":[\"custom-build\"],\"crate_types\":[\"bin\"],\"name\":\"build-script-build\",\"src_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/parking_lot_core-0.9.12/build.rs\",\"edition\":\"2021\",\"doc\":false,\"doctest\":false,\"test\":false},\"profile\":{\"opt_level\":\"0\",\"debuginfo\":0,\"debug_assertions\":true,\"overflow_checks\":true,\"test\":false},\"features\":[],\"filenames\":[\"/home/thomas/Projects/bacon-ls-bug/target/debug/build/parking_lot_core-d8b900c1eeb10f02/build-script-build\"],\"executable\":null,\"fresh\":true}\n{\"reason\":\"compiler-artifact\",\"package_id\":\"registry+https://github.com/rust-lang/crates.io-index#syn@2.0.117\",\"manifest_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-2.0.117/Cargo.toml\",\"target\":{\"kind\":[\"lib\"],\"crate_types\":[\"lib\"],\"name\":\"syn\",\"src_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-2.0.117/src/lib.rs\",\"edition\":\"2021\",\"doc\":true,\"doctest\":true,\"test\":true},\"profile\":{\"opt_level\":\"0\",\"debuginfo\":0,\"debug_assertions\":true,\"overflow_checks\":true,\"test\":false},\"features\":[\"clone-impls\",\"default\",\"derive\",\"extra-traits\",\"full\",\"parsing\",\"printing\",\"proc-macro\",\"visit-mut\"],\"filenames\":[\"/home/thomas/Projects/bacon-ls-bug/target/debug/deps/libsyn-39beda1e2c05b94c.rlib\",\"/home/thomas/Projects/bacon-ls-bug/target/debug/deps/libsyn-39beda1e2c05b94c.rmeta\"],\"executable\":null,\"fresh\":true}\n{\"reason\":\"compiler-artifact\",\"package_id\":\"registry+https://github.com/rust-lang/crates.io-index#mio@1.1.1\",\"manifest_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/mio-1.1.1/Cargo.toml\",\"target\":{\"kind\":[\"lib\"],\"crate_types\":[\"lib\"],\"name\":\"mio\",\"src_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/mio-1.1.1/src/lib.rs\",\"edition\":\"2021\",\"doc\":true,\"doctest\":true,\"test\":true},\"profile\":{\"opt_level\":\"0\",\"debuginfo\":2,\"debug_assertions\":true,\"overflow_checks\":true,\"test\":false},\"features\":[\"default\",\"log\",\"net\",\"os-ext\",\"os-poll\"],\"filenames\":[\"/home/thomas/Projects/bacon-ls-bug/target/debug/deps/libmio-f27a8ca24206ce10.rmeta\"],\"executable\":null,\"fresh\":true}\n{\"reason\":\"compiler-artifact\",\"package_id\":\"registry+https://github.com/rust-lang/crates.io-index#errno@0.3.14\",\"manifest_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/errno-0.3.14/Cargo.toml\",\"target\":{\"kind\":[\"lib\"],\"crate_types\":[\"lib\"],\"name\":\"errno\",\"src_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/errno-0.3.14/src/lib.rs\",\"edition\":\"2018\",\"doc\":true,\"doctest\":true,\"test\":true},\"profile\":{\"opt_level\":\"0\",\"debuginfo\":2,\"debug_assertions\":true,\"overflow_checks\":true,\"test\":false},\"features\":[\"default\",\"std\"],\"filenames\":[\"/home/thomas/Projects/bacon-ls-bug/target/debug/deps/liberrno-3776ea9775340287.rmeta\"],\"executable\":null,\"fresh\":true}\n{\"reason\":\"build-script-executed\",\"package_id\":\"registry+https://github.com/rust-lang/crates.io-index#parking_lot_core@0.9.12\",\"linked_libs\":[],\"linked_paths\":[],\"cfgs\":[],\"env\":[],\"out_dir\":\"/home/thomas/Projects/bacon-ls-bug/target/debug/build/parking_lot_core-0b9c9b58b128f550/out\"}\n{\"reason\":\"build-script-executed\",\"package_id\":\"registry+https://github.com/rust-lang/crates.io-index#crossbeam-utils@0.8.21\",\"linked_libs\":[],\"linked_paths\":[],\"cfgs\":[],\"env\":[],\"out_dir\":\"/home/thomas/Projects/bacon-ls-bug/target/debug/build/crossbeam-utils-b6cad324b555061c/out\"}\n{\"reason\":\"compiler-artifact\",\"package_id\":\"registry+https://github.com/rust-lang/crates.io-index#inotify-sys@0.1.5\",\"manifest_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/inotify-sys-0.1.5/Cargo.toml\",\"target\":{\"kind\":[\"lib\"],\"crate_types\":[\"lib\"],\"name\":\"inotify_sys\",\"src_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/inotify-sys-0.1.5/src/lib.rs\",\"edition\":\"2015\",\"doc\":true,\"doctest\":true,\"test\":true},\"profile\":{\"opt_level\":\"0\",\"debuginfo\":2,\"debug_assertions\":true,\"overflow_checks\":true,\"test\":false},\"features\":[],\"filenames\":[\"/home/thomas/Projects/bacon-ls-bug/target/debug/deps/libinotify_sys-90e14662d2b4a708.rmeta\"],\"executable\":null,\"fresh\":true}\n{\"reason\":\"compiler-artifact\",\"package_id\":\"registry+https://github.com/rust-lang/crates.io-index#zmij@1.0.21\",\"manifest_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/zmij-1.0.21/Cargo.toml\",\"target\":{\"kind\":[\"lib\"],\"crate_types\":[\"lib\"],\"name\":\"zmij\",\"src_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/zmij-1.0.21/src/lib.rs\",\"edition\":\"2021\",\"doc\":true,\"doctest\":true,\"test\":true},\"profile\":{\"opt_level\":\"0\",\"debuginfo\":2,\"debug_assertions\":true,\"overflow_checks\":true,\"test\":false},\"features\":[],\"filenames\":[\"/home/thomas/Projects/bacon-ls-bug/target/debug/deps/libzmij-53a24a856e0aa1fb.rmeta\"],\"executable\":null,\"fresh\":true}\n{\"reason\":\"build-script-executed\",\"package_id\":\"registry+https://github.com/rust-lang/crates.io-index#serde_json@1.0.149\",\"linked_libs\":[],\"linked_paths\":[],\"cfgs\":[\"fast_arithmetic=\\\"64\\\"\"],\"env\":[],\"out_dir\":\"/home/thomas/Projects/bacon-ls-bug/target/debug/build/serde_json-6018d5b507f4f795/out\"}\n{\"reason\":\"compiler-artifact\",\"package_id\":\"registry+https://github.com/rust-lang/crates.io-index#tracing-core@0.1.36\",\"manifest_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tracing-core-0.1.36/Cargo.toml\",\"target\":{\"kind\":[\"lib\"],\"crate_types\":[\"lib\"],\"name\":\"tracing_core\",\"src_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tracing-core-0.1.36/src/lib.rs\",\"edition\":\"2018\",\"doc\":true,\"doctest\":true,\"test\":true},\"profile\":{\"opt_level\":\"0\",\"debuginfo\":2,\"debug_assertions\":true,\"overflow_checks\":true,\"test\":false},\"features\":[\"once_cell\",\"std\"],\"filenames\":[\"/home/thomas/Projects/bacon-ls-bug/target/debug/deps/libtracing_core-26e50f3bf5ddb8ff.rmeta\"],\"executable\":null,\"fresh\":true}\n{\"reason\":\"compiler-artifact\",\"package_id\":\"registry+https://github.com/rust-lang/crates.io-index#bytes@1.11.1\",\"manifest_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/bytes-1.11.1/Cargo.toml\",\"target\":{\"kind\":[\"lib\"],\"crate_types\":[\"lib\"],\"name\":\"bytes\",\"src_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/bytes-1.11.1/src/lib.rs\",\"edition\":\"2021\",\"doc\":true,\"doctest\":true,\"test\":true},\"profile\":{\"opt_level\":\"0\",\"debuginfo\":2,\"debug_assertions\":true,\"overflow_checks\":true,\"test\":false},\"features\":[\"default\",\"std\"],\"filenames\":[\"/home/thomas/Projects/bacon-ls-bug/target/debug/deps/libbytes-73077db55e69a9d1.rmeta\"],\"executable\":null,\"fresh\":true}\n{\"reason\":\"compiler-artifact\",\"package_id\":\"registry+https://github.com/rust-lang/crates.io-index#smallvec@1.15.1\",\"manifest_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/smallvec-1.15.1/Cargo.toml\",\"target\":{\"kind\":[\"lib\"],\"crate_types\":[\"lib\"],\"name\":\"smallvec\",\"src_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/smallvec-1.15.1/src/lib.rs\",\"edition\":\"2018\",\"doc\":true,\"doctest\":true,\"test\":true},\"profile\":{\"opt_level\":\"0\",\"debuginfo\":2,\"debug_assertions\":true,\"overflow_checks\":true,\"test\":false},\"features\":[],\"filenames\":[\"/home/thomas/Projects/bacon-ls-bug/target/debug/deps/libsmallvec-9a1bf4e4f89d02f3.rmeta\"],\"executable\":null,\"fresh\":true}\n{\"reason\":\"compiler-artifact\",\"package_id\":\"registry+https://github.com/rust-lang/crates.io-index#httparse@1.10.1\",\"manifest_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/httparse-1.10.1/Cargo.toml\",\"target\":{\"kind\":[\"custom-build\"],\"crate_types\":[\"bin\"],\"name\":\"build-script-build\",\"src_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/httparse-1.10.1/build.rs\",\"edition\":\"2018\",\"doc\":false,\"doctest\":false,\"test\":false},\"profile\":{\"opt_level\":\"0\",\"debuginfo\":0,\"debug_assertions\":true,\"overflow_checks\":true,\"test\":false},\"features\":[\"default\",\"std\"],\"filenames\":[\"/home/thomas/Projects/bacon-ls-bug/target/debug/build/httparse-4499966ee4ebfc03/build-script-build\"],\"executable\":null,\"fresh\":true}\n{\"reason\":\"compiler-artifact\",\"package_id\":\"registry+https://github.com/rust-lang/crates.io-index#serde_derive@1.0.228\",\"manifest_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/serde_derive-1.0.228/Cargo.toml\",\"target\":{\"kind\":[\"proc-macro\"],\"crate_types\":[\"proc-macro\"],\"name\":\"serde_derive\",\"src_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/serde_derive-1.0.228/src/lib.rs\",\"edition\":\"2021\",\"doc\":true,\"doctest\":true,\"test\":true},\"profile\":{\"opt_level\":\"0\",\"debuginfo\":0,\"debug_assertions\":true,\"overflow_checks\":true,\"test\":false},\"features\":[\"default\"],\"filenames\":[\"/home/thomas/Projects/bacon-ls-bug/target/debug/deps/libserde_derive-42ced28887c84640.so\"],\"executable\":null,\"fresh\":true}\n{\"reason\":\"compiler-artifact\",\"package_id\":\"registry+https://github.com/rust-lang/crates.io-index#ref-cast-impl@1.0.25\",\"manifest_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/ref-cast-impl-1.0.25/Cargo.toml\",\"target\":{\"kind\":[\"proc-macro\"],\"crate_types\":[\"proc-macro\"],\"name\":\"ref_cast_impl\",\"src_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/ref-cast-impl-1.0.25/src/lib.rs\",\"edition\":\"2021\",\"doc\":true,\"doctest\":true,\"test\":true},\"profile\":{\"opt_level\":\"0\",\"debuginfo\":0,\"debug_assertions\":true,\"overflow_checks\":true,\"test\":false},\"features\":[],\"filenames\":[\"/home/thomas/Projects/bacon-ls-bug/target/debug/deps/libref_cast_impl-1d613e3cfb7cc1bc.so\"],\"executable\":null,\"fresh\":true}\n{\"reason\":\"compiler-artifact\",\"package_id\":\"registry+https://github.com/rust-lang/crates.io-index#futures-macro@0.3.32\",\"manifest_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/futures-macro-0.3.32/Cargo.toml\",\"target\":{\"kind\":[\"proc-macro\"],\"crate_types\":[\"proc-macro\"],\"name\":\"futures_macro\",\"src_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/futures-macro-0.3.32/src/lib.rs\",\"edition\":\"2018\",\"doc\":true,\"doctest\":true,\"test\":true},\"profile\":{\"opt_level\":\"0\",\"debuginfo\":0,\"debug_assertions\":true,\"overflow_checks\":true,\"test\":false},\"features\":[],\"filenames\":[\"/home/thomas/Projects/bacon-ls-bug/target/debug/deps/libfutures_macro-d6c78019574eb8a9.so\"],\"executable\":null,\"fresh\":true}\n{\"reason\":\"compiler-artifact\",\"package_id\":\"registry+https://github.com/rust-lang/crates.io-index#tracing-attributes@0.1.31\",\"manifest_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tracing-attributes-0.1.31/Cargo.toml\",\"target\":{\"kind\":[\"proc-macro\"],\"crate_types\":[\"proc-macro\"],\"name\":\"tracing_attributes\",\"src_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tracing-attributes-0.1.31/src/lib.rs\",\"edition\":\"2018\",\"doc\":true,\"doctest\":true,\"test\":true},\"profile\":{\"opt_level\":\"0\",\"debuginfo\":0,\"debug_assertions\":true,\"overflow_checks\":true,\"test\":false},\"features\":[],\"filenames\":[\"/home/thomas/Projects/bacon-ls-bug/target/debug/deps/libtracing_attributes-0fdd7b56f14f42ef.so\"],\"executable\":null,\"fresh\":true}\n{\"reason\":\"compiler-artifact\",\"package_id\":\"registry+https://github.com/rust-lang/crates.io-index#tokio-macros@2.6.1\",\"manifest_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tokio-macros-2.6.1/Cargo.toml\",\"target\":{\"kind\":[\"proc-macro\"],\"crate_types\":[\"proc-macro\"],\"name\":\"tokio_macros\",\"src_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tokio-macros-2.6.1/src/lib.rs\",\"edition\":\"2021\",\"doc\":true,\"doctest\":true,\"test\":true},\"profile\":{\"opt_level\":\"0\",\"debuginfo\":0,\"debug_assertions\":true,\"overflow_checks\":true,\"test\":false},\"features\":[],\"filenames\":[\"/home/thomas/Projects/bacon-ls-bug/target/debug/deps/libtokio_macros-7c7742e40aa03e35.so\"],\"executable\":null,\"fresh\":true}\n{\"reason\":\"compiler-artifact\",\"package_id\":\"registry+https://github.com/rust-lang/crates.io-index#signal-hook-registry@1.4.8\",\"manifest_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/signal-hook-registry-1.4.8/Cargo.toml\",\"target\":{\"kind\":[\"lib\"],\"crate_types\":[\"lib\"],\"name\":\"signal_hook_registry\",\"src_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/signal-hook-registry-1.4.8/src/lib.rs\",\"edition\":\"2015\",\"doc\":true,\"doctest\":true,\"test\":true},\"profile\":{\"opt_level\":\"0\",\"debuginfo\":2,\"debug_assertions\":true,\"overflow_checks\":true,\"test\":false},\"features\":[],\"filenames\":[\"/home/thomas/Projects/bacon-ls-bug/target/debug/deps/libsignal_hook_registry-23b727d5ded52006.rmeta\"],\"executable\":null,\"fresh\":true}\n{\"reason\":\"compiler-artifact\",\"package_id\":\"registry+https://github.com/rust-lang/crates.io-index#borrow-or-share@0.2.4\",\"manifest_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/borrow-or-share-0.2.4/Cargo.toml\",\"target\":{\"kind\":[\"lib\"],\"crate_types\":[\"lib\"],\"name\":\"borrow_or_share\",\"src_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/borrow-or-share-0.2.4/src/lib.rs\",\"edition\":\"2021\",\"doc\":true,\"doctest\":true,\"test\":true},\"profile\":{\"opt_level\":\"0\",\"debuginfo\":2,\"debug_assertions\":true,\"overflow_checks\":true,\"test\":false},\"features\":[\"alloc\"],\"filenames\":[\"/home/thomas/Projects/bacon-ls-bug/target/debug/deps/libborrow_or_share-bdd41b7e285ced78.rmeta\"],\"executable\":null,\"fresh\":true}\n{\"reason\":\"compiler-artifact\",\"package_id\":\"registry+https://github.com/rust-lang/crates.io-index#itoa@1.0.18\",\"manifest_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/itoa-1.0.18/Cargo.toml\",\"target\":{\"kind\":[\"lib\"],\"crate_types\":[\"lib\"],\"name\":\"itoa\",\"src_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/itoa-1.0.18/src/lib.rs\",\"edition\":\"2021\",\"doc\":true,\"doctest\":true,\"test\":true},\"profile\":{\"opt_level\":\"0\",\"debuginfo\":2,\"debug_assertions\":true,\"overflow_checks\":true,\"test\":false},\"features\":[],\"filenames\":[\"/home/thomas/Projects/bacon-ls-bug/target/debug/deps/libitoa-f51922049d476d8b.rmeta\"],\"executable\":null,\"fresh\":true}\n{\"reason\":\"compiler-artifact\",\"package_id\":\"registry+https://github.com/rust-lang/crates.io-index#same-file@1.0.6\",\"manifest_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/same-file-1.0.6/Cargo.toml\",\"target\":{\"kind\":[\"lib\"],\"crate_types\":[\"lib\"],\"name\":\"same_file\",\"src_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/same-file-1.0.6/src/lib.rs\",\"edition\":\"2018\",\"doc\":true,\"doctest\":true,\"test\":true},\"profile\":{\"opt_level\":\"0\",\"debuginfo\":2,\"debug_assertions\":true,\"overflow_checks\":true,\"test\":false},\"features\":[],\"filenames\":[\"/home/thomas/Projects/bacon-ls-bug/target/debug/deps/libsame_file-13c19624acbb40c7.rmeta\"],\"executable\":null,\"fresh\":true}\n{\"reason\":\"compiler-artifact\",\"package_id\":\"registry+https://github.com/rust-lang/crates.io-index#parking_lot_core@0.9.12\",\"manifest_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/parking_lot_core-0.9.12/Cargo.toml\",\"target\":{\"kind\":[\"lib\"],\"crate_types\":[\"lib\"],\"name\":\"parking_lot_core\",\"src_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/parking_lot_core-0.9.12/src/lib.rs\",\"edition\":\"2021\",\"doc\":true,\"doctest\":true,\"test\":true},\"profile\":{\"opt_level\":\"0\",\"debuginfo\":2,\"debug_assertions\":true,\"overflow_checks\":true,\"test\":false},\"features\":[],\"filenames\":[\"/home/thomas/Projects/bacon-ls-bug/target/debug/deps/libparking_lot_core-f3897c75372e727e.rmeta\"],\"executable\":null,\"fresh\":true}\n{\"reason\":\"compiler-artifact\",\"package_id\":\"registry+https://github.com/rust-lang/crates.io-index#inotify@0.11.1\",\"manifest_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/inotify-0.11.1/Cargo.toml\",\"target\":{\"kind\":[\"lib\"],\"crate_types\":[\"lib\"],\"name\":\"inotify\",\"src_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/inotify-0.11.1/src/lib.rs\",\"edition\":\"2018\",\"doc\":true,\"doctest\":true,\"test\":true},\"profile\":{\"opt_level\":\"0\",\"debuginfo\":2,\"debug_assertions\":true,\"overflow_checks\":true,\"test\":false},\"features\":[],\"filenames\":[\"/home/thomas/Projects/bacon-ls-bug/target/debug/deps/libinotify-6ca1921b30888926.rmeta\"],\"executable\":null,\"fresh\":true}\n{\"reason\":\"compiler-artifact\",\"package_id\":\"registry+https://github.com/rust-lang/crates.io-index#crossbeam-utils@0.8.21\",\"manifest_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-utils-0.8.21/Cargo.toml\",\"target\":{\"kind\":[\"lib\"],\"crate_types\":[\"lib\"],\"name\":\"crossbeam_utils\",\"src_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-utils-0.8.21/src/lib.rs\",\"edition\":\"2021\",\"doc\":true,\"doctest\":true,\"test\":true},\"profile\":{\"opt_level\":\"0\",\"debuginfo\":2,\"debug_assertions\":true,\"overflow_checks\":true,\"test\":false},\"features\":[\"default\",\"std\"],\"filenames\":[\"/home/thomas/Projects/bacon-ls-bug/target/debug/deps/libcrossbeam_utils-b5ed45e46132c12c.rmeta\"],\"executable\":null,\"fresh\":true}\n{\"reason\":\"compiler-artifact\",\"package_id\":\"registry+https://github.com/rust-lang/crates.io-index#serde@1.0.228\",\"manifest_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/serde-1.0.228/Cargo.toml\",\"target\":{\"kind\":[\"lib\"],\"crate_types\":[\"lib\"],\"name\":\"serde\",\"src_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/serde-1.0.228/src/lib.rs\",\"edition\":\"2021\",\"doc\":true,\"doctest\":true,\"test\":true},\"profile\":{\"opt_level\":\"0\",\"debuginfo\":2,\"debug_assertions\":true,\"overflow_checks\":true,\"test\":false},\"features\":[\"default\",\"derive\",\"serde_derive\",\"std\"],\"filenames\":[\"/home/thomas/Projects/bacon-ls-bug/target/debug/deps/libserde-5031f6731326eb3f.rmeta\"],\"executable\":null,\"fresh\":true}\n{\"reason\":\"compiler-artifact\",\"package_id\":\"registry+https://github.com/rust-lang/crates.io-index#futures-util@0.3.32\",\"manifest_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/futures-util-0.3.32/Cargo.toml\",\"target\":{\"kind\":[\"lib\"],\"crate_types\":[\"lib\"],\"name\":\"futures_util\",\"src_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/futures-util-0.3.32/src/lib.rs\",\"edition\":\"2018\",\"doc\":true,\"doctest\":true,\"test\":true},\"profile\":{\"opt_level\":\"0\",\"debuginfo\":2,\"debug_assertions\":true,\"overflow_checks\":true,\"test\":false},\"features\":[\"alloc\",\"async-await\",\"async-await-macro\",\"channel\",\"futures-channel\",\"futures-io\",\"futures-macro\",\"futures-sink\",\"io\",\"memchr\",\"sink\",\"slab\",\"std\"],\"filenames\":[\"/home/thomas/Projects/bacon-ls-bug/target/debug/deps/libfutures_util-b5efcc286e3e2327.rmeta\"],\"executable\":null,\"fresh\":true}\n{\"reason\":\"compiler-artifact\",\"package_id\":\"registry+https://github.com/rust-lang/crates.io-index#ref-cast@1.0.25\",\"manifest_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/ref-cast-1.0.25/Cargo.toml\",\"target\":{\"kind\":[\"lib\"],\"crate_types\":[\"lib\"],\"name\":\"ref_cast\",\"src_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/ref-cast-1.0.25/src/lib.rs\",\"edition\":\"2021\",\"doc\":true,\"doctest\":true,\"test\":true},\"profile\":{\"opt_level\":\"0\",\"debuginfo\":2,\"debug_assertions\":true,\"overflow_checks\":true,\"test\":false},\"features\":[],\"filenames\":[\"/home/thomas/Projects/bacon-ls-bug/target/debug/deps/libref_cast-36207b72fc555cee.rmeta\"],\"executable\":null,\"fresh\":true}\n{\"reason\":\"compiler-artifact\",\"package_id\":\"registry+https://github.com/rust-lang/crates.io-index#tokio@1.50.0\",\"manifest_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tokio-1.50.0/Cargo.toml\",\"target\":{\"kind\":[\"lib\"],\"crate_types\":[\"lib\"],\"name\":\"tokio\",\"src_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tokio-1.50.0/src/lib.rs\",\"edition\":\"2021\",\"doc\":true,\"doctest\":true,\"test\":true},\"profile\":{\"opt_level\":\"0\",\"debuginfo\":2,\"debug_assertions\":true,\"overflow_checks\":true,\"test\":false},\"features\":[\"bytes\",\"default\",\"fs\",\"io-std\",\"io-util\",\"libc\",\"macros\",\"mio\",\"process\",\"rt\",\"rt-multi-thread\",\"signal-hook-registry\",\"sync\",\"time\",\"tokio-macros\"],\"filenames\":[\"/home/thomas/Projects/bacon-ls-bug/target/debug/deps/libtokio-5c662ac04191f8f5.rmeta\"],\"executable\":null,\"fresh\":true}\n{\"reason\":\"compiler-artifact\",\"package_id\":\"registry+https://github.com/rust-lang/crates.io-index#walkdir@2.5.0\",\"manifest_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/walkdir-2.5.0/Cargo.toml\",\"target\":{\"kind\":[\"lib\"],\"crate_types\":[\"lib\"],\"name\":\"walkdir\",\"src_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/walkdir-2.5.0/src/lib.rs\",\"edition\":\"2018\",\"doc\":true,\"doctest\":true,\"test\":true},\"profile\":{\"opt_level\":\"0\",\"debuginfo\":2,\"debug_assertions\":true,\"overflow_checks\":true,\"test\":false},\"features\":[],\"filenames\":[\"/home/thomas/Projects/bacon-ls-bug/target/debug/deps/libwalkdir-ba98e1399a2483cf.rmeta\"],\"executable\":null,\"fresh\":true}\n{\"reason\":\"compiler-artifact\",\"package_id\":\"registry+https://github.com/rust-lang/crates.io-index#tracing@0.1.44\",\"manifest_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tracing-0.1.44/Cargo.toml\",\"target\":{\"kind\":[\"lib\"],\"crate_types\":[\"lib\"],\"name\":\"tracing\",\"src_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tracing-0.1.44/src/lib.rs\",\"edition\":\"2018\",\"doc\":true,\"doctest\":true,\"test\":true},\"profile\":{\"opt_level\":\"0\",\"debuginfo\":2,\"debug_assertions\":true,\"overflow_checks\":true,\"test\":false},\"features\":[\"attributes\",\"default\",\"std\",\"tracing-attributes\"],\"filenames\":[\"/home/thomas/Projects/bacon-ls-bug/target/debug/deps/libtracing-5d7f5aba1b542d12.rmeta\"],\"executable\":null,\"fresh\":true}\n{\"reason\":\"compiler-artifact\",\"package_id\":\"registry+https://github.com/rust-lang/crates.io-index#serde_json@1.0.149\",\"manifest_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/serde_json-1.0.149/Cargo.toml\",\"target\":{\"kind\":[\"lib\"],\"crate_types\":[\"lib\"],\"name\":\"serde_json\",\"src_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/serde_json-1.0.149/src/lib.rs\",\"edition\":\"2021\",\"doc\":true,\"doctest\":true,\"test\":true},\"profile\":{\"opt_level\":\"0\",\"debuginfo\":2,\"debug_assertions\":true,\"overflow_checks\":true,\"test\":false},\"features\":[\"default\",\"std\"],\"filenames\":[\"/home/thomas/Projects/bacon-ls-bug/target/debug/deps/libserde_json-26574dbded5a10f5.rmeta\"],\"executable\":null,\"fresh\":true}\n{\"reason\":\"build-script-executed\",\"package_id\":\"registry+https://github.com/rust-lang/crates.io-index#httparse@1.10.1\",\"linked_libs\":[],\"linked_paths\":[],\"cfgs\":[\"httparse_simd_neon_intrinsics\",\"httparse_simd\"],\"env\":[],\"out_dir\":\"/home/thomas/Projects/bacon-ls-bug/target/debug/build/httparse-f8ac398a02999c5f/out\"}\n{\"reason\":\"compiler-artifact\",\"package_id\":\"registry+https://github.com/rust-lang/crates.io-index#notify-types@2.1.0\",\"manifest_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/notify-types-2.1.0/Cargo.toml\",\"target\":{\"kind\":[\"lib\"],\"crate_types\":[\"lib\"],\"name\":\"notify_types\",\"src_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/notify-types-2.1.0/src/lib.rs\",\"edition\":\"2021\",\"doc\":true,\"doctest\":true,\"test\":true},\"profile\":{\"opt_level\":\"0\",\"debuginfo\":2,\"debug_assertions\":true,\"overflow_checks\":true,\"test\":false},\"features\":[],\"filenames\":[\"/home/thomas/Projects/bacon-ls-bug/target/debug/deps/libnotify_types-4c3dfa79d19255bc.rmeta\"],\"executable\":null,\"fresh\":true}\n{\"reason\":\"compiler-artifact\",\"package_id\":\"registry+https://github.com/rust-lang/crates.io-index#tower-layer@0.3.3\",\"manifest_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tower-layer-0.3.3/Cargo.toml\",\"target\":{\"kind\":[\"lib\"],\"crate_types\":[\"lib\"],\"name\":\"tower_layer\",\"src_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tower-layer-0.3.3/src/lib.rs\",\"edition\":\"2018\",\"doc\":true,\"doctest\":true,\"test\":true},\"profile\":{\"opt_level\":\"0\",\"debuginfo\":2,\"debug_assertions\":true,\"overflow_checks\":true,\"test\":false},\"features\":[],\"filenames\":[\"/home/thomas/Projects/bacon-ls-bug/target/debug/deps/libtower_layer-633b42abb2a04f1e.rmeta\"],\"executable\":null,\"fresh\":true}\n{\"reason\":\"compiler-artifact\",\"package_id\":\"registry+https://github.com/rust-lang/crates.io-index#sync_wrapper@1.0.2\",\"manifest_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/sync_wrapper-1.0.2/Cargo.toml\",\"target\":{\"kind\":[\"lib\"],\"crate_types\":[\"lib\"],\"name\":\"sync_wrapper\",\"src_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/sync_wrapper-1.0.2/src/lib.rs\",\"edition\":\"2021\",\"doc\":true,\"doctest\":true,\"test\":true},\"profile\":{\"opt_level\":\"0\",\"debuginfo\":2,\"debug_assertions\":true,\"overflow_checks\":true,\"test\":false},\"features\":[],\"filenames\":[\"/home/thomas/Projects/bacon-ls-bug/target/debug/deps/libsync_wrapper-488a86995281c30d.rmeta\"],\"executable\":null,\"fresh\":true}\n{\"reason\":\"compiler-artifact\",\"package_id\":\"registry+https://github.com/rust-lang/crates.io-index#hashbrown@0.14.5\",\"manifest_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/hashbrown-0.14.5/Cargo.toml\",\"target\":{\"kind\":[\"lib\"],\"crate_types\":[\"lib\"],\"name\":\"hashbrown\",\"src_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/hashbrown-0.14.5/src/lib.rs\",\"edition\":\"2021\",\"doc\":true,\"doctest\":true,\"test\":true},\"profile\":{\"opt_level\":\"0\",\"debuginfo\":2,\"debug_assertions\":true,\"overflow_checks\":true,\"test\":false},\"features\":[\"raw\"],\"filenames\":[\"/home/thomas/Projects/bacon-ls-bug/target/debug/deps/libhashbrown-270820bcd875383b.rmeta\"],\"executable\":null,\"fresh\":true}\n{\"reason\":\"compiler-artifact\",\"package_id\":\"registry+https://github.com/rust-lang/crates.io-index#fluent-uri@0.4.1\",\"manifest_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/fluent-uri-0.4.1/Cargo.toml\",\"target\":{\"kind\":[\"lib\"],\"crate_types\":[\"lib\"],\"name\":\"fluent_uri\",\"src_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/fluent-uri-0.4.1/src/lib.rs\",\"edition\":\"2021\",\"doc\":true,\"doctest\":true,\"test\":true},\"profile\":{\"opt_level\":\"0\",\"debuginfo\":2,\"debug_assertions\":true,\"overflow_checks\":true,\"test\":false},\"features\":[\"alloc\",\"default\",\"impl-error\",\"std\"],\"filenames\":[\"/home/thomas/Projects/bacon-ls-bug/target/debug/deps/libfluent_uri-95f791872c0adc94.rmeta\"],\"executable\":null,\"fresh\":true}\n{\"reason\":\"compiler-artifact\",\"package_id\":\"registry+https://github.com/rust-lang/crates.io-index#lazy_static@1.5.0\",\"manifest_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/lazy_static-1.5.0/Cargo.toml\",\"target\":{\"kind\":[\"lib\"],\"crate_types\":[\"lib\"],\"name\":\"lazy_static\",\"src_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/lazy_static-1.5.0/src/lib.rs\",\"edition\":\"2015\",\"doc\":true,\"doctest\":true,\"test\":true},\"profile\":{\"opt_level\":\"0\",\"debuginfo\":2,\"debug_assertions\":true,\"overflow_checks\":true,\"test\":false},\"features\":[],\"filenames\":[\"/home/thomas/Projects/bacon-ls-bug/target/debug/deps/liblazy_static-5efa11a4cdace67d.rmeta\"],\"executable\":null,\"fresh\":true}\n{\"reason\":\"compiler-artifact\",\"package_id\":\"registry+https://github.com/rust-lang/crates.io-index#percent-encoding@2.3.2\",\"manifest_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/percent-encoding-2.3.2/Cargo.toml\",\"target\":{\"kind\":[\"lib\"],\"crate_types\":[\"lib\"],\"name\":\"percent_encoding\",\"src_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/percent-encoding-2.3.2/src/lib.rs\",\"edition\":\"2018\",\"doc\":true,\"doctest\":true,\"test\":true},\"profile\":{\"opt_level\":\"0\",\"debuginfo\":2,\"debug_assertions\":true,\"overflow_checks\":true,\"test\":false},\"features\":[\"alloc\",\"default\",\"std\"],\"filenames\":[\"/home/thomas/Projects/bacon-ls-bug/target/debug/deps/libpercent_encoding-b6764e71103d5f71.rmeta\"],\"executable\":null,\"fresh\":true}\n{\"reason\":\"compiler-artifact\",\"package_id\":\"registry+https://github.com/rust-lang/crates.io-index#argh_shared@0.1.19\",\"manifest_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/argh_shared-0.1.19/Cargo.toml\",\"target\":{\"kind\":[\"lib\"],\"crate_types\":[\"lib\"],\"name\":\"argh_shared\",\"src_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/argh_shared-0.1.19/src/lib.rs\",\"edition\":\"2018\",\"doc\":true,\"doctest\":true,\"test\":true},\"profile\":{\"opt_level\":\"0\",\"debuginfo\":0,\"debug_assertions\":true,\"overflow_checks\":true,\"test\":false},\"features\":[],\"filenames\":[\"/home/thomas/Projects/bacon-ls-bug/target/debug/deps/libargh_shared-5f34a8d3e03b33cf.rlib\",\"/home/thomas/Projects/bacon-ls-bug/target/debug/deps/libargh_shared-5f34a8d3e03b33cf.rmeta\"],\"executable\":null,\"fresh\":true}\n{\"reason\":\"compiler-artifact\",\"package_id\":\"registry+https://github.com/rust-lang/crates.io-index#tower-service@0.3.3\",\"manifest_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tower-service-0.3.3/Cargo.toml\",\"target\":{\"kind\":[\"lib\"],\"crate_types\":[\"lib\"],\"name\":\"tower_service\",\"src_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tower-service-0.3.3/src/lib.rs\",\"edition\":\"2018\",\"doc\":true,\"doctest\":true,\"test\":true},\"profile\":{\"opt_level\":\"0\",\"debuginfo\":2,\"debug_assertions\":true,\"overflow_checks\":true,\"test\":false},\"features\":[],\"filenames\":[\"/home/thomas/Projects/bacon-ls-bug/target/debug/deps/libtower_service-335bb054a2edba08.rmeta\"],\"executable\":null,\"fresh\":true}\n{\"reason\":\"compiler-artifact\",\"package_id\":\"registry+https://github.com/rust-lang/crates.io-index#anyhow@1.0.102\",\"manifest_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/anyhow-1.0.102/Cargo.toml\",\"target\":{\"kind\":[\"custom-build\"],\"crate_types\":[\"bin\"],\"name\":\"build-script-build\",\"src_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/anyhow-1.0.102/build.rs\",\"edition\":\"2021\",\"doc\":false,\"doctest\":false,\"test\":false},\"profile\":{\"opt_level\":\"0\",\"debuginfo\":0,\"debug_assertions\":true,\"overflow_checks\":true,\"test\":false},\"features\":[\"default\",\"std\"],\"filenames\":[\"/home/thomas/Projects/bacon-ls-bug/target/debug/build/anyhow-9d0d680a8f9a50b5/build-script-build\"],\"executable\":null,\"fresh\":true}\n{\"reason\":\"compiler-artifact\",\"package_id\":\"registry+https://github.com/rust-lang/crates.io-index#winnow@1.0.0\",\"manifest_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/winnow-1.0.0/Cargo.toml\",\"target\":{\"kind\":[\"lib\"],\"crate_types\":[\"lib\"],\"name\":\"winnow\",\"src_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/winnow-1.0.0/src/lib.rs\",\"edition\":\"2021\",\"doc\":true,\"doctest\":true,\"test\":true},\"profile\":{\"opt_level\":\"0\",\"debuginfo\":2,\"debug_assertions\":true,\"overflow_checks\":true,\"test\":false},\"features\":[],\"filenames\":[\"/home/thomas/Projects/bacon-ls-bug/target/debug/deps/libwinnow-29353a79a1f00957.rmeta\"],\"executable\":null,\"fresh\":true}\n{\"reason\":\"compiler-artifact\",\"package_id\":\"registry+https://github.com/rust-lang/crates.io-index#tokio-util@0.7.18\",\"manifest_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tokio-util-0.7.18/Cargo.toml\",\"target\":{\"kind\":[\"lib\"],\"crate_types\":[\"lib\"],\"name\":\"tokio_util\",\"src_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tokio-util-0.7.18/src/lib.rs\",\"edition\":\"2021\",\"doc\":true,\"doctest\":true,\"test\":true},\"profile\":{\"opt_level\":\"0\",\"debuginfo\":2,\"debug_assertions\":true,\"overflow_checks\":true,\"test\":false},\"features\":[\"codec\",\"default\"],\"filenames\":[\"/home/thomas/Projects/bacon-ls-bug/target/debug/deps/libtokio_util-0988b4432fd5e7fd.rmeta\"],\"executable\":null,\"fresh\":true}\n{\"reason\":\"compiler-artifact\",\"package_id\":\"registry+https://github.com/rust-lang/crates.io-index#dashmap@6.1.0\",\"manifest_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/dashmap-6.1.0/Cargo.toml\",\"target\":{\"kind\":[\"lib\"],\"crate_types\":[\"lib\"],\"name\":\"dashmap\",\"src_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/dashmap-6.1.0/src/lib.rs\",\"edition\":\"2018\",\"doc\":true,\"doctest\":true,\"test\":true},\"profile\":{\"opt_level\":\"0\",\"debuginfo\":2,\"debug_assertions\":true,\"overflow_checks\":true,\"test\":false},\"features\":[],\"filenames\":[\"/home/thomas/Projects/bacon-ls-bug/target/debug/deps/libdashmap-c9e3b46ef9d2124f.rmeta\"],\"executable\":null,\"fresh\":true}\n{\"reason\":\"compiler-artifact\",\"package_id\":\"registry+https://github.com/rust-lang/crates.io-index#argh_shared@0.1.19\",\"manifest_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/argh_shared-0.1.19/Cargo.toml\",\"target\":{\"kind\":[\"lib\"],\"crate_types\":[\"lib\"],\"name\":\"argh_shared\",\"src_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/argh_shared-0.1.19/src/lib.rs\",\"edition\":\"2018\",\"doc\":true,\"doctest\":true,\"test\":true},\"profile\":{\"opt_level\":\"0\",\"debuginfo\":2,\"debug_assertions\":true,\"overflow_checks\":true,\"test\":false},\"features\":[\"serde\"],\"filenames\":[\"/home/thomas/Projects/bacon-ls-bug/target/debug/deps/libargh_shared-cd766776f309c6d2.rmeta\"],\"executable\":null,\"fresh\":true}\n{\"reason\":\"compiler-artifact\",\"package_id\":\"registry+https://github.com/rust-lang/crates.io-index#futures@0.3.32\",\"manifest_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/futures-0.3.32/Cargo.toml\",\"target\":{\"kind\":[\"lib\"],\"crate_types\":[\"lib\"],\"name\":\"futures\",\"src_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/futures-0.3.32/src/lib.rs\",\"edition\":\"2018\",\"doc\":true,\"doctest\":true,\"test\":true},\"profile\":{\"opt_level\":\"0\",\"debuginfo\":2,\"debug_assertions\":true,\"overflow_checks\":true,\"test\":false},\"features\":[\"alloc\",\"async-await\",\"std\"],\"filenames\":[\"/home/thomas/Projects/bacon-ls-bug/target/debug/deps/libfutures-a5b4612fa66f9e55.rmeta\"],\"executable\":null,\"fresh\":true}\n{\"reason\":\"compiler-artifact\",\"package_id\":\"registry+https://github.com/rust-lang/crates.io-index#notify@8.2.0\",\"manifest_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/notify-8.2.0/Cargo.toml\",\"target\":{\"kind\":[\"lib\"],\"crate_types\":[\"lib\"],\"name\":\"notify\",\"src_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/notify-8.2.0/src/lib.rs\",\"edition\":\"2021\",\"doc\":true,\"doctest\":true,\"test\":true},\"profile\":{\"opt_level\":\"0\",\"debuginfo\":2,\"debug_assertions\":true,\"overflow_checks\":true,\"test\":false},\"features\":[\"default\",\"fsevent-sys\",\"macos_fsevent\"],\"filenames\":[\"/home/thomas/Projects/bacon-ls-bug/target/debug/deps/libnotify-ae3ca1f6da0b7ed4.rmeta\"],\"executable\":null,\"fresh\":true}\n{\"reason\":\"compiler-artifact\",\"package_id\":\"registry+https://github.com/rust-lang/crates.io-index#tower@0.5.3\",\"manifest_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tower-0.5.3/Cargo.toml\",\"target\":{\"kind\":[\"lib\"],\"crate_types\":[\"lib\"],\"name\":\"tower\",\"src_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tower-0.5.3/src/lib.rs\",\"edition\":\"2018\",\"doc\":true,\"doctest\":true,\"test\":true},\"profile\":{\"opt_level\":\"0\",\"debuginfo\":2,\"debug_assertions\":true,\"overflow_checks\":true,\"test\":false},\"features\":[\"futures-core\",\"futures-util\",\"pin-project-lite\",\"sync_wrapper\",\"util\"],\"filenames\":[\"/home/thomas/Projects/bacon-ls-bug/target/debug/deps/libtower-0eef260ba048ac0e.rmeta\"],\"executable\":null,\"fresh\":true}\n{\"reason\":\"compiler-artifact\",\"package_id\":\"registry+https://github.com/rust-lang/crates.io-index#sharded-slab@0.1.7\",\"manifest_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/sharded-slab-0.1.7/Cargo.toml\",\"target\":{\"kind\":[\"lib\"],\"crate_types\":[\"lib\"],\"name\":\"sharded_slab\",\"src_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/sharded-slab-0.1.7/src/lib.rs\",\"edition\":\"2018\",\"doc\":true,\"doctest\":true,\"test\":true},\"profile\":{\"opt_level\":\"0\",\"debuginfo\":2,\"debug_assertions\":true,\"overflow_checks\":true,\"test\":false},\"features\":[],\"filenames\":[\"/home/thomas/Projects/bacon-ls-bug/target/debug/deps/libsharded_slab-106bf90f9ca40f33.rmeta\"],\"executable\":null,\"fresh\":true}\n{\"reason\":\"compiler-artifact\",\"package_id\":\"registry+https://github.com/rust-lang/crates.io-index#ls-types@0.0.6\",\"manifest_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/ls-types-0.0.6/Cargo.toml\",\"target\":{\"kind\":[\"lib\"],\"crate_types\":[\"lib\"],\"name\":\"ls_types\",\"src_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/ls-types-0.0.6/src/lib.rs\",\"edition\":\"2024\",\"doc\":true,\"doctest\":true,\"test\":true},\"profile\":{\"opt_level\":\"0\",\"debuginfo\":2,\"debug_assertions\":true,\"overflow_checks\":true,\"test\":false},\"features\":[],\"filenames\":[\"/home/thomas/Projects/bacon-ls-bug/target/debug/deps/libls_types-4d1f5d84ee3e9643.rmeta\"],\"executable\":null,\"fresh\":true}\n{\"reason\":\"build-script-executed\",\"package_id\":\"registry+https://github.com/rust-lang/crates.io-index#anyhow@1.0.102\",\"linked_libs\":[],\"linked_paths\":[],\"cfgs\":[],\"env\":[],\"out_dir\":\"/home/thomas/Projects/bacon-ls-bug/target/debug/build/anyhow-eb4960e4a6ea2272/out\"}\n{\"reason\":\"compiler-artifact\",\"package_id\":\"registry+https://github.com/rust-lang/crates.io-index#toml_parser@1.0.10+spec-1.1.0\",\"manifest_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/toml_parser-1.0.10+spec-1.1.0/Cargo.toml\",\"target\":{\"kind\":[\"lib\"],\"crate_types\":[\"lib\"],\"name\":\"toml_parser\",\"src_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/toml_parser-1.0.10+spec-1.1.0/src/lib.rs\",\"edition\":\"2021\",\"doc\":true,\"doctest\":true,\"test\":true},\"profile\":{\"opt_level\":\"0\",\"debuginfo\":2,\"debug_assertions\":true,\"overflow_checks\":true,\"test\":false},\"features\":[\"alloc\",\"std\"],\"filenames\":[\"/home/thomas/Projects/bacon-ls-bug/target/debug/deps/libtoml_parser-0e89ffc8d6227084.rmeta\"],\"executable\":null,\"fresh\":true}\n{\"reason\":\"compiler-artifact\",\"package_id\":\"registry+https://github.com/rust-lang/crates.io-index#argh_derive@0.1.19\",\"manifest_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/argh_derive-0.1.19/Cargo.toml\",\"target\":{\"kind\":[\"proc-macro\"],\"crate_types\":[\"proc-macro\"],\"name\":\"argh_derive\",\"src_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/argh_derive-0.1.19/src/lib.rs\",\"edition\":\"2018\",\"doc\":true,\"doctest\":true,\"test\":true},\"profile\":{\"opt_level\":\"0\",\"debuginfo\":0,\"debug_assertions\":true,\"overflow_checks\":true,\"test\":false},\"features\":[\"help\"],\"filenames\":[\"/home/thomas/Projects/bacon-ls-bug/target/debug/deps/libargh_derive-511dc3cf298e694a.so\"],\"executable\":null,\"fresh\":true}\n{\"reason\":\"compiler-artifact\",\"package_id\":\"registry+https://github.com/rust-lang/crates.io-index#httparse@1.10.1\",\"manifest_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/httparse-1.10.1/Cargo.toml\",\"target\":{\"kind\":[\"lib\"],\"crate_types\":[\"lib\"],\"name\":\"httparse\",\"src_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/httparse-1.10.1/src/lib.rs\",\"edition\":\"2018\",\"doc\":true,\"doctest\":true,\"test\":true},\"profile\":{\"opt_level\":\"0\",\"debuginfo\":2,\"debug_assertions\":true,\"overflow_checks\":true,\"test\":false},\"features\":[\"default\",\"std\"],\"filenames\":[\"/home/thomas/Projects/bacon-ls-bug/target/debug/deps/libhttparse-e62c569fcfe7d237.rmeta\"],\"executable\":null,\"fresh\":true}\n{\"reason\":\"compiler-artifact\",\"package_id\":\"registry+https://github.com/rust-lang/crates.io-index#matchers@0.2.0\",\"manifest_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/matchers-0.2.0/Cargo.toml\",\"target\":{\"kind\":[\"lib\"],\"crate_types\":[\"lib\"],\"name\":\"matchers\",\"src_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/matchers-0.2.0/src/lib.rs\",\"edition\":\"2018\",\"doc\":true,\"doctest\":true,\"test\":true},\"profile\":{\"opt_level\":\"0\",\"debuginfo\":2,\"debug_assertions\":true,\"overflow_checks\":true,\"test\":false},\"features\":[],\"filenames\":[\"/home/thomas/Projects/bacon-ls-bug/target/debug/deps/libmatchers-bb8bef4071e75991.rmeta\"],\"executable\":null,\"fresh\":true}\n{\"reason\":\"compiler-artifact\",\"package_id\":\"registry+https://github.com/rust-lang/crates.io-index#toml_datetime@1.0.1+spec-1.1.0\",\"manifest_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/toml_datetime-1.0.1+spec-1.1.0/Cargo.toml\",\"target\":{\"kind\":[\"lib\"],\"crate_types\":[\"lib\"],\"name\":\"toml_datetime\",\"src_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/toml_datetime-1.0.1+spec-1.1.0/src/lib.rs\",\"edition\":\"2021\",\"doc\":true,\"doctest\":true,\"test\":true},\"profile\":{\"opt_level\":\"0\",\"debuginfo\":2,\"debug_assertions\":true,\"overflow_checks\":true,\"test\":false},\"features\":[\"alloc\",\"serde\",\"std\"],\"filenames\":[\"/home/thomas/Projects/bacon-ls-bug/target/debug/deps/libtoml_datetime-a9c0a8b911483b6c.rmeta\"],\"executable\":null,\"fresh\":true}\n{\"reason\":\"compiler-artifact\",\"package_id\":\"registry+https://github.com/rust-lang/crates.io-index#spin@0.9.8\",\"manifest_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/spin-0.9.8/Cargo.toml\",\"target\":{\"kind\":[\"lib\"],\"crate_types\":[\"lib\"],\"name\":\"spin\",\"src_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/spin-0.9.8/src/lib.rs\",\"edition\":\"2015\",\"doc\":true,\"doctest\":true,\"test\":true},\"profile\":{\"opt_level\":\"0\",\"debuginfo\":2,\"debug_assertions\":true,\"overflow_checks\":true,\"test\":false},\"features\":[\"barrier\",\"default\",\"lazy\",\"lock_api\",\"lock_api_crate\",\"mutex\",\"once\",\"rwlock\",\"spin_mutex\"],\"filenames\":[\"/home/thomas/Projects/bacon-ls-bug/target/debug/deps/libspin-90b9e75b96b76635.rmeta\"],\"executable\":null,\"fresh\":true}\n{\"reason\":\"compiler-artifact\",\"package_id\":\"registry+https://github.com/rust-lang/crates.io-index#regex@1.12.3\",\"manifest_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/regex-1.12.3/Cargo.toml\",\"target\":{\"kind\":[\"lib\"],\"crate_types\":[\"lib\"],\"name\":\"regex\",\"src_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/regex-1.12.3/src/lib.rs\",\"edition\":\"2021\",\"doc\":true,\"doctest\":true,\"test\":true},\"profile\":{\"opt_level\":\"0\",\"debuginfo\":2,\"debug_assertions\":true,\"overflow_checks\":true,\"test\":false},\"features\":[\"default\",\"perf\",\"perf-backtrack\",\"perf-cache\",\"perf-dfa\",\"perf-inline\",\"perf-literal\",\"perf-onepass\",\"std\",\"unicode\",\"unicode-age\",\"unicode-bool\",\"unicode-case\",\"unicode-gencat\",\"unicode-perl\",\"unicode-script\",\"unicode-segment\"],\"filenames\":[\"/home/thomas/Projects/bacon-ls-bug/target/debug/deps/libregex-0984f1b3b6a66e66.rmeta\"],\"executable\":null,\"fresh\":true}\n{\"reason\":\"compiler-artifact\",\"package_id\":\"registry+https://github.com/rust-lang/crates.io-index#serde_spanned@1.0.4\",\"manifest_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/serde_spanned-1.0.4/Cargo.toml\",\"target\":{\"kind\":[\"lib\"],\"crate_types\":[\"lib\"],\"name\":\"serde_spanned\",\"src_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/serde_spanned-1.0.4/src/lib.rs\",\"edition\":\"2021\",\"doc\":true,\"doctest\":true,\"test\":true},\"profile\":{\"opt_level\":\"0\",\"debuginfo\":2,\"debug_assertions\":true,\"overflow_checks\":true,\"test\":false},\"features\":[\"alloc\",\"serde\",\"std\"],\"filenames\":[\"/home/thomas/Projects/bacon-ls-bug/target/debug/deps/libserde_spanned-42c32e30c6c8d2e1.rmeta\"],\"executable\":null,\"fresh\":true}\n{\"reason\":\"compiler-artifact\",\"package_id\":\"registry+https://github.com/rust-lang/crates.io-index#thread_local@1.1.9\",\"manifest_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/thread_local-1.1.9/Cargo.toml\",\"target\":{\"kind\":[\"lib\"],\"crate_types\":[\"lib\"],\"name\":\"thread_local\",\"src_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/thread_local-1.1.9/src/lib.rs\",\"edition\":\"2021\",\"doc\":true,\"doctest\":true,\"test\":true},\"profile\":{\"opt_level\":\"0\",\"debuginfo\":2,\"debug_assertions\":true,\"overflow_checks\":true,\"test\":false},\"features\":[],\"filenames\":[\"/home/thomas/Projects/bacon-ls-bug/target/debug/deps/libthread_local-a7600d40293617d8.rmeta\"],\"executable\":null,\"fresh\":true}\n{\"reason\":\"compiler-artifact\",\"package_id\":\"registry+https://github.com/rust-lang/crates.io-index#file-id@0.2.3\",\"manifest_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/file-id-0.2.3/Cargo.toml\",\"target\":{\"kind\":[\"lib\"],\"crate_types\":[\"lib\"],\"name\":\"file_id\",\"src_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/file-id-0.2.3/src/lib.rs\",\"edition\":\"2021\",\"doc\":true,\"doctest\":true,\"test\":true},\"profile\":{\"opt_level\":\"0\",\"debuginfo\":2,\"debug_assertions\":true,\"overflow_checks\":true,\"test\":false},\"features\":[],\"filenames\":[\"/home/thomas/Projects/bacon-ls-bug/target/debug/deps/libfile_id-8b9c99432cf0e622.rmeta\"],\"executable\":null,\"fresh\":true}\n{\"reason\":\"compiler-artifact\",\"package_id\":\"registry+https://github.com/rust-lang/crates.io-index#fastrand@2.3.0\",\"manifest_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/fastrand-2.3.0/Cargo.toml\",\"target\":{\"kind\":[\"lib\"],\"crate_types\":[\"lib\"],\"name\":\"fastrand\",\"src_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/fastrand-2.3.0/src/lib.rs\",\"edition\":\"2018\",\"doc\":true,\"doctest\":true,\"test\":true},\"profile\":{\"opt_level\":\"0\",\"debuginfo\":2,\"debug_assertions\":true,\"overflow_checks\":true,\"test\":false},\"features\":[\"alloc\",\"default\",\"getrandom\",\"js\",\"std\"],\"filenames\":[\"/home/thomas/Projects/bacon-ls-bug/target/debug/deps/libfastrand-549f2b23f9b3bbaa.rmeta\"],\"executable\":null,\"fresh\":true}\n{\"reason\":\"compiler-artifact\",\"package_id\":\"registry+https://github.com/rust-lang/crates.io-index#toml_writer@1.0.7+spec-1.1.0\",\"manifest_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/toml_writer-1.0.7+spec-1.1.0/Cargo.toml\",\"target\":{\"kind\":[\"lib\"],\"crate_types\":[\"lib\"],\"name\":\"toml_writer\",\"src_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/toml_writer-1.0.7+spec-1.1.0/src/lib.rs\",\"edition\":\"2021\",\"doc\":true,\"doctest\":true,\"test\":true},\"profile\":{\"opt_level\":\"0\",\"debuginfo\":2,\"debug_assertions\":true,\"overflow_checks\":true,\"test\":false},\"features\":[\"alloc\",\"std\"],\"filenames\":[\"/home/thomas/Projects/bacon-ls-bug/target/debug/deps/libtoml_writer-69ee4c827dd47f6a.rmeta\"],\"executable\":null,\"fresh\":true}\n{\"reason\":\"compiler-artifact\",\"package_id\":\"registry+https://github.com/rust-lang/crates.io-index#tower-lsp-server@0.23.0\",\"manifest_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tower-lsp-server-0.23.0/Cargo.toml\",\"target\":{\"kind\":[\"lib\"],\"crate_types\":[\"lib\"],\"name\":\"tower_lsp_server\",\"src_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tower-lsp-server-0.23.0/src/lib.rs\",\"edition\":\"2024\",\"doc\":true,\"doctest\":true,\"test\":true},\"profile\":{\"opt_level\":\"0\",\"debuginfo\":2,\"debug_assertions\":true,\"overflow_checks\":true,\"test\":false},\"features\":[\"default\",\"runtime-tokio\",\"tokio\",\"tokio-util\"],\"filenames\":[\"/home/thomas/Projects/bacon-ls-bug/target/debug/deps/libtower_lsp_server-1d93c01febbba2a2.rmeta\"],\"executable\":null,\"fresh\":true}\n{\"reason\":\"compiler-artifact\",\"package_id\":\"registry+https://github.com/rust-lang/crates.io-index#argh@0.1.19\",\"manifest_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/argh-0.1.19/Cargo.toml\",\"target\":{\"kind\":[\"lib\"],\"crate_types\":[\"lib\"],\"name\":\"argh\",\"src_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/argh-0.1.19/src/lib.rs\",\"edition\":\"2018\",\"doc\":true,\"doctest\":true,\"test\":true},\"profile\":{\"opt_level\":\"0\",\"debuginfo\":2,\"debug_assertions\":true,\"overflow_checks\":true,\"test\":false},\"features\":[\"default\",\"help\",\"serde\"],\"filenames\":[\"/home/thomas/Projects/bacon-ls-bug/target/debug/deps/libargh-1f5999bbaffe20a5.rmeta\"],\"executable\":null,\"fresh\":true}\n{\"reason\":\"compiler-artifact\",\"package_id\":\"registry+https://github.com/rust-lang/crates.io-index#anyhow@1.0.102\",\"manifest_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/anyhow-1.0.102/Cargo.toml\",\"target\":{\"kind\":[\"lib\"],\"crate_types\":[\"lib\"],\"name\":\"anyhow\",\"src_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/anyhow-1.0.102/src/lib.rs\",\"edition\":\"2021\",\"doc\":true,\"doctest\":true,\"test\":true},\"profile\":{\"opt_level\":\"0\",\"debuginfo\":2,\"debug_assertions\":true,\"overflow_checks\":true,\"test\":false},\"features\":[\"default\",\"std\"],\"filenames\":[\"/home/thomas/Projects/bacon-ls-bug/target/debug/deps/libanyhow-f0caf68a99ec25e0.rmeta\"],\"executable\":null,\"fresh\":true}\n{\"reason\":\"compiler-artifact\",\"package_id\":\"registry+https://github.com/rust-lang/crates.io-index#ansi-regex@0.1.0\",\"manifest_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/ansi-regex-0.1.0/Cargo.toml\",\"target\":{\"kind\":[\"lib\"],\"crate_types\":[\"lib\"],\"name\":\"ansi_regex\",\"src_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/ansi-regex-0.1.0/src/lib.rs\",\"edition\":\"2015\",\"doc\":true,\"doctest\":true,\"test\":true},\"profile\":{\"opt_level\":\"0\",\"debuginfo\":2,\"debug_assertions\":true,\"overflow_checks\":true,\"test\":false},\"features\":[],\"filenames\":[\"/home/thomas/Projects/bacon-ls-bug/target/debug/deps/libansi_regex-0fa8b6a2cbd390e0.rmeta\"],\"executable\":null,\"fresh\":true}\n{\"reason\":\"compiler-artifact\",\"package_id\":\"registry+https://github.com/rust-lang/crates.io-index#toml@1.0.7+spec-1.1.0\",\"manifest_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/toml-1.0.7+spec-1.1.0/Cargo.toml\",\"target\":{\"kind\":[\"lib\"],\"crate_types\":[\"lib\"],\"name\":\"toml\",\"src_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/toml-1.0.7+spec-1.1.0/src/lib.rs\",\"edition\":\"2021\",\"doc\":true,\"doctest\":true,\"test\":true},\"profile\":{\"opt_level\":\"0\",\"debuginfo\":2,\"debug_assertions\":true,\"overflow_checks\":true,\"test\":false},\"features\":[\"default\",\"display\",\"parse\",\"serde\",\"std\"],\"filenames\":[\"/home/thomas/Projects/bacon-ls-bug/target/debug/deps/libtoml-f44b7af59402e1be.rmeta\"],\"executable\":null,\"fresh\":true}\n{\"reason\":\"compiler-artifact\",\"package_id\":\"registry+https://github.com/rust-lang/crates.io-index#notify-debouncer-full@0.7.0\",\"manifest_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/notify-debouncer-full-0.7.0/Cargo.toml\",\"target\":{\"kind\":[\"lib\"],\"crate_types\":[\"lib\"],\"name\":\"notify_debouncer_full\",\"src_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/notify-debouncer-full-0.7.0/src/lib.rs\",\"edition\":\"2021\",\"doc\":true,\"doctest\":true,\"test\":true},\"profile\":{\"opt_level\":\"0\",\"debuginfo\":2,\"debug_assertions\":true,\"overflow_checks\":true,\"test\":false},\"features\":[\"default\",\"macos_fsevent\"],\"filenames\":[\"/home/thomas/Projects/bacon-ls-bug/target/debug/deps/libnotify_debouncer_full-16e42315c18df8aa.rmeta\"],\"executable\":null,\"fresh\":true}\n{\"reason\":\"compiler-artifact\",\"package_id\":\"registry+https://github.com/rust-lang/crates.io-index#flume@0.12.0\",\"manifest_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/flume-0.12.0/Cargo.toml\",\"target\":{\"kind\":[\"lib\"],\"crate_types\":[\"lib\"],\"name\":\"flume\",\"src_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/flume-0.12.0/src/lib.rs\",\"edition\":\"2018\",\"doc\":true,\"doctest\":true,\"test\":true},\"profile\":{\"opt_level\":\"0\",\"debuginfo\":2,\"debug_assertions\":true,\"overflow_checks\":true,\"test\":false},\"features\":[\"async\",\"default\",\"eventual-fairness\",\"fastrand\",\"futures-core\",\"futures-sink\",\"select\"],\"filenames\":[\"/home/thomas/Projects/bacon-ls-bug/target/debug/deps/libflume-e10958550acae6b6.rmeta\"],\"executable\":null,\"fresh\":true}\n{\"reason\":\"compiler-artifact\",\"package_id\":\"registry+https://github.com/rust-lang/crates.io-index#tracing-subscriber@0.3.23\",\"manifest_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tracing-subscriber-0.3.23/Cargo.toml\",\"target\":{\"kind\":[\"lib\"],\"crate_types\":[\"lib\"],\"name\":\"tracing_subscriber\",\"src_path\":\"/home/thomas/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tracing-subscriber-0.3.23/src/lib.rs\",\"edition\":\"2018\",\"doc\":true,\"doctest\":true,\"test\":true},\"profile\":{\"opt_level\":\"0\",\"debuginfo\":2,\"debug_assertions\":true,\"overflow_checks\":true,\"test\":false},\"features\":[\"alloc\",\"env-filter\",\"fmt\",\"matchers\",\"once_cell\",\"registry\",\"sharded-slab\",\"std\",\"thread_local\",\"tracing\"],\"filenames\":[\"/home/thomas/Projects/bacon-ls-bug/target/debug/deps/libtracing_subscriber-a6e2ef3c0f94fe16.rmeta\"],\"executable\":null,\"fresh\":true}\n{\"reason\":\"compiler-message\",\"package_id\":\"path+file:///home/thomas/Projects/bacon-ls-bug#bacon-ls@0.26.0\",\"manifest_path\":\"/home/thomas/Projects/bacon-ls-bug/Cargo.toml\",\"target\":{\"kind\":[\"lib\"],\"crate_types\":[\"lib\"],\"name\":\"bacon_ls\",\"src_path\":\"/home/thomas/Projects/bacon-ls-bug/src/lib.rs\",\"edition\":\"2024\",\"doc\":true,\"doctest\":true,\"test\":true},\"message\":{\"$message_type\":\"diagnostic\",\"message\":\"unused import: `Compact`\",\"code\":{\"code\":\"unused_imports\",\"explanation\":null},\"level\":\"warning\",\"spans\":[{\"file_name\":\"src/lib.rs\",\"byte_start\":563,\"byte_end\":570,\"line_start\":19,\"line_end\":19,\"column_start\":39,\"column_end\":46,\"is_primary\":true,\"text\":[{\"text\":\"use tracing_subscriber::fmt::format::{Compact, FmtSpan};\",\"highlight_start\":39,\"highlight_end\":46}],\"label\":null,\"suggested_replacement\":null,\"suggestion_applicability\":null,\"expansion\":null}],\"children\":[{\"message\":\"`#[warn(unused_imports)]` (part of `#[warn(unused)]`) on by default\",\"code\":null,\"level\":\"note\",\"spans\":[],\"children\":[],\"rendered\":null},{\"message\":\"remove the unused import\",\"code\":null,\"level\":\"help\",\"spans\":[{\"file_name\":\"src/lib.rs\",\"byte_start\":563,\"byte_end\":572,\"line_start\":19,\"line_end\":19,\"column_start\":39,\"column_end\":48,\"is_primary\":true,\"text\":[{\"text\":\"use tracing_subscriber::fmt::format::{Compact, FmtSpan};\",\"highlight_start\":39,\"highlight_end\":48}],\"label\":null,\"suggested_replacement\":\"\",\"suggestion_applicability\":\"MachineApplicable\",\"expansion\":null},{\"file_name\":\"src/lib.rs\",\"byte_start\":562,\"byte_end\":563,\"line_start\":19,\"line_end\":19,\"column_start\":38,\"column_end\":39,\"is_primary\":true,\"text\":[{\"text\":\"use tracing_subscriber::fmt::format::{Compact, FmtSpan};\",\"highlight_start\":38,\"highlight_end\":39}],\"label\":null,\"suggested_replacement\":\"\",\"suggestion_applicability\":\"MachineApplicable\",\"expansion\":null},{\"file_name\":\"src/lib.rs\",\"byte_start\":579,\"byte_end\":580,\"line_start\":19,\"line_end\":19,\"column_start\":55,\"column_end\":56,\"is_primary\":true,\"text\":[{\"text\":\"use tracing_subscriber::fmt::format::{Compact, FmtSpan};\",\"highlight_start\":55,\"highlight_end\":56}],\"label\":null,\"suggested_replacement\":\"\",\"suggestion_applicability\":\"MachineApplicable\",\"expansion\":null}],\"children\":[],\"rendered\":null}],\"rendered\":\"\\u001b[1m\\u001b[33mwarning\\u001b[0m\\u001b[1m: unused import: `Compact`\\u001b[0m\\n  \\u001b[1m\\u001b[94m--> \\u001b[0msrc/lib.rs:19:39\\n   \\u001b[1m\\u001b[94m|\\u001b[0m\\n\\u001b[1m\\u001b[94m19\\u001b[0m \\u001b[1m\\u001b[94m|\\u001b[0m use tracing_subscriber::fmt::format::{Compact, FmtSpan};\\n   \\u001b[1m\\u001b[94m|\\u001b[0m                                       \\u001b[1m\\u001b[33m^^^^^^^\\u001b[0m\\n   \\u001b[1m\\u001b[94m|\\u001b[0m\\n   \\u001b[1m\\u001b[94m= \\u001b[0m\\u001b[1mnote\\u001b[0m: `#[warn(unused_imports)]` (part of `#[warn(unused)]`) on by default\\n\\n\"}}\n{\"reason\":\"compiler-artifact\",\"package_id\":\"path+file:///home/thomas/Projects/bacon-ls-bug#bacon-ls@0.26.0\",\"manifest_path\":\"/home/thomas/Projects/bacon-ls-bug/Cargo.toml\",\"target\":{\"kind\":[\"lib\"],\"crate_types\":[\"lib\"],\"name\":\"bacon_ls\",\"src_path\":\"/home/thomas/Projects/bacon-ls-bug/src/lib.rs\",\"edition\":\"2024\",\"doc\":true,\"doctest\":true,\"test\":true},\"profile\":{\"opt_level\":\"0\",\"debuginfo\":2,\"debug_assertions\":true,\"overflow_checks\":true,\"test\":false},\"features\":[\"default\"],\"filenames\":[\"/home/thomas/Projects/bacon-ls-bug/target/debug/deps/libbacon_ls-7742a8b80671e78f.rmeta\"],\"executable\":null,\"fresh\":true}\n{\"reason\":\"compiler-artifact\",\"package_id\":\"path+file:///home/thomas/Projects/bacon-ls-bug#bacon-ls@0.26.0\",\"manifest_path\":\"/home/thomas/Projects/bacon-ls-bug/Cargo.toml\",\"target\":{\"kind\":[\"bin\"],\"crate_types\":[\"bin\"],\"name\":\"bacon-ls\",\"src_path\":\"/home/thomas/Projects/bacon-ls-bug/src/main.rs\",\"edition\":\"2024\",\"doc\":true,\"doctest\":false,\"test\":true},\"profile\":{\"opt_level\":\"0\",\"debuginfo\":2,\"debug_assertions\":true,\"overflow_checks\":true,\"test\":false},\"features\":[\"default\"],\"filenames\":[\"/home/thomas/Projects/bacon-ls-bug/target/debug/deps/libbacon_ls-f02f2118cb9fcf8e.rmeta\"],\"executable\":null,\"fresh\":true}\n{\"reason\":\"build-finished\",\"success\":true}\n"
  },
  {
    "path": "src/testdata/unused-variable.json",
    "content": "{\"reason\":\"compiler-message\",\"package_id\":\"path+file:///home/thomas/Projects/bacon-ls#0.26.0\",\"manifest_path\":\"/home/thomas/Projects/bacon-ls/Cargo.toml\",\"target\":{\"kind\":[\"lib\"],\"crate_types\":[\"lib\"],\"name\":\"bacon_ls\",\"src_path\":\"/home/thomas/Projects/bacon-ls/src/lib.rs\",\"edition\":\"2024\",\"doc\":true,\"doctest\":true,\"test\":true},\"message\":{\"$message_type\":\"diagnostic\",\"message\":\"unused variable: `lol`\",\"code\":{\"code\":\"unused_variables\",\"explanation\":null},\"level\":\"warning\",\"spans\":[{\"file_name\":\"src/lib.rs\",\"byte_start\":26584,\"byte_end\":26587,\"line_start\":719,\"line_end\":719,\"column_start\":21,\"column_end\":24,\"is_primary\":true,\"text\":[{\"text\":\"                let lol = 1;\",\"highlight_start\":21,\"highlight_end\":24}],\"label\":null,\"suggested_replacement\":null,\"suggestion_applicability\":null,\"expansion\":null}],\"children\":[{\"message\":\"`#[warn(unused_variables)]` (part of `#[warn(unused)]`) on by default\",\"code\":null,\"level\":\"note\",\"spans\":[],\"children\":[],\"rendered\":null},{\"message\":\"if this is intentional, prefix it with an underscore\",\"code\":null,\"level\":\"help\",\"spans\":[{\"file_name\":\"src/lib.rs\",\"byte_start\":26584,\"byte_end\":26587,\"line_start\":719,\"line_end\":719,\"column_start\":21,\"column_end\":24,\"is_primary\":true,\"text\":[{\"text\":\"                let lol = 1;\",\"highlight_start\":21,\"highlight_end\":24}],\"label\":null,\"suggested_replacement\":\"_lol\",\"suggestion_applicability\":\"MachineApplicable\",\"expansion\":null}],\"children\":[],\"rendered\":null}],\"rendered\":\"\\u001b[1m\\u001b[33mwarning\\u001b[0m\\u001b[1m: unused variable: `lol`\\u001b[0m\\n   \\u001b[1m\\u001b[94m--> \\u001b[0msrc/lib.rs:719:21\\n    \\u001b[1m\\u001b[94m|\\u001b[0m\\n\\u001b[1m\\u001b[94m719\\u001b[0m \\u001b[1m\\u001b[94m|\\u001b[0m                 let lol = 1;\\n    \\u001b[1m\\u001b[94m|\\u001b[0m                     \\u001b[1m\\u001b[33m^^^\\u001b[0m \\u001b[1m\\u001b[33mhelp: if this is intentional, prefix it with an underscore: `_lol`\\u001b[0m\\n    \\u001b[1m\\u001b[94m|\\u001b[0m\\n    \\u001b[1m\\u001b[94m= \\u001b[0m\\u001b[1mnote\\u001b[0m: `#[warn(unused_variables)]` (part of `#[warn(unused)]`) on by default\\n\\n\"}}\n"
  },
  {
    "path": "tests/cargo_backend.rs",
    "content": "//! End-to-end integration tests for the cargo (native) backend. Spawns the\n//! real `bacon-ls` binary against a tempdir Cargo project and drives a full\n//! LSP session by writing framed JSON-RPC to its stdin / parsing its stdout.\n//!\n//! Linux-only: same rationale as `tests/lsp_restart.rs` — the harness reads\n//! framed bytes off pipes whose timing/buffering varies enough on macOS and\n//! Windows to add flake risk that has nothing to do with what's being tested.\n//! The cargo backend logic itself is platform-agnostic and exercised by unit\n//! tests on every CI runner.\n#![cfg(target_os = \"linux\")]\n\nuse std::io::{Read, Write};\nuse std::path::Path;\nuse std::process::{Child, ChildStdin, ChildStdout, Command, Stdio};\nuse std::sync::mpsc;\nuse std::thread;\nuse std::time::{Duration, Instant};\n\nuse serde_json::{Value, json};\nuse tempfile::TempDir;\n\nconst BIN: &str = env!(\"CARGO_BIN_EXE_bacon-ls\");\n\nfn write_fixture(dir: &Path, lib_rs: &str) {\n    std::fs::write(\n        dir.join(\"Cargo.toml\"),\n        \"[package]\\nname = \\\"fixture\\\"\\nversion = \\\"0.0.1\\\"\\nedition = \\\"2021\\\"\\n[lib]\\npath = \\\"src/lib.rs\\\"\\n\",\n    )\n    .unwrap();\n    std::fs::create_dir_all(dir.join(\"src\")).unwrap();\n    std::fs::write(dir.join(\"src\").join(\"lib.rs\"), lib_rs).unwrap();\n}\n\nfn frame(body: &str) -> Vec<u8> {\n    format!(\"Content-Length: {}\\r\\n\\r\\n{body}\", body.len()).into_bytes()\n}\n\nfn send(stdin: &mut ChildStdin, msg: &Value) {\n    let body = msg.to_string();\n    stdin.write_all(&frame(&body)).unwrap();\n    stdin.flush().unwrap();\n}\n\nfn spawn_reader(mut stdout: ChildStdout) -> mpsc::Receiver<Value> {\n    let (tx, rx) = mpsc::channel();\n    thread::spawn(move || {\n        let mut buf = Vec::new();\n        let mut tmp = [0u8; 4096];\n        loop {\n            // Drain all complete frames currently in `buf`.\n            while let Some(hdr_end) = buf.windows(4).position(|w| w == b\"\\r\\n\\r\\n\") {\n                let header = std::str::from_utf8(&buf[..hdr_end]).unwrap_or(\"\");\n                let len: usize = header\n                    .lines()\n                    .find_map(|l| l.strip_prefix(\"Content-Length: \"))\n                    .and_then(|v| v.trim().parse().ok())\n                    .unwrap_or(0);\n                let body_start = hdr_end + 4;\n                if buf.len() < body_start + len {\n                    break;\n                }\n                if let Ok(s) = std::str::from_utf8(&buf[body_start..body_start + len])\n                    && let Ok(v) = serde_json::from_str::<Value>(s)\n                    && tx.send(v).is_err()\n                {\n                    return;\n                }\n                buf.drain(..body_start + len);\n            }\n            match stdout.read(&mut tmp) {\n                Ok(0) | Err(_) => return,\n                Ok(n) => buf.extend_from_slice(&tmp[..n]),\n            }\n        }\n    });\n    rx\n}\n\n/// Auto-respond to server-initiated requests so the server doesn't stall.\n/// `workspace/configuration` is answered with `config_response`; everything\n/// else (e.g. `window/workDoneProgress/create`, `client/registerCapability`)\n/// gets a `null` result.\nfn auto_respond(stdin: &mut ChildStdin, msg: &Value, config_response: &Value) {\n    if msg.get(\"method\").is_none() || msg.get(\"id\").is_none() {\n        return;\n    }\n    let id = &msg[\"id\"];\n    let method = msg[\"method\"].as_str().unwrap_or(\"\");\n    let result = match method {\n        \"workspace/configuration\" => config_response.clone(),\n        _ => Value::Null,\n    };\n    send(stdin, &json!({\"jsonrpc\": \"2.0\", \"id\": id, \"result\": result}));\n}\n\n/// Read messages off `rx` until `pred` matches, auto-responding to every\n/// server-initiated request along the way. Returns the matched message or\n/// `None` on timeout.\nfn pump<F>(\n    rx: &mpsc::Receiver<Value>,\n    stdin: &mut ChildStdin,\n    timeout: Duration,\n    config_response: &Value,\n    mut pred: F,\n) -> Option<Value>\nwhere\n    F: FnMut(&Value) -> bool,\n{\n    let deadline = Instant::now() + timeout;\n    while let Some(remaining) = deadline.checked_duration_since(Instant::now()) {\n        let Ok(msg) = rx.recv_timeout(remaining) else {\n            return None;\n        };\n        auto_respond(stdin, &msg, config_response);\n        if pred(&msg) {\n            return Some(msg);\n        }\n    }\n    None\n}\n\n/// Default `workspace/configuration` reply: empty object → server uses\n/// cargo backend defaults. Existing tests rely on this.\nfn empty_config() -> Value {\n    json!([{}])\n}\n\nfn root_uri(dir: &Path) -> String {\n    format!(\"file://{}\", dir.display())\n}\n\nfn spawn_server(workdir: &Path) -> Child {\n    spawn_server_with_log(workdir, None)\n}\n\nfn spawn_server_with_log(workdir: &Path, rust_log: Option<&str>) -> Child {\n    let mut cmd = Command::new(BIN);\n    cmd.current_dir(workdir)\n        .stdin(Stdio::piped())\n        .stdout(Stdio::piped())\n        .stderr(Stdio::null());\n    if let Some(level) = rust_log {\n        cmd.env(\"RUST_LOG\", level);\n    } else {\n        cmd.env_remove(\"RUST_LOG\");\n    }\n    cmd.spawn().expect(\"spawn bacon-ls\")\n}\n\nfn read_server_log(workdir: &Path) -> String {\n    std::fs::read_to_string(workdir.join(\"bacon-ls.log\")).unwrap_or_else(|e| format!(\"<no log: {e}>\"))\n}\n\nfn initialize(\n    stdin: &mut ChildStdin,\n    rx: &mpsc::Receiver<Value>,\n    workdir: &Path,\n    related_info_support: bool,\n    config_response: &Value,\n) {\n    initialize_with_init_options(stdin, rx, workdir, related_info_support, config_response, None);\n}\n\nfn initialize_with_init_options(\n    stdin: &mut ChildStdin,\n    rx: &mpsc::Receiver<Value>,\n    workdir: &Path,\n    related_info_support: bool,\n    config_response: &Value,\n    init_options: Option<&Value>,\n) {\n    let mut params = json!({\n        \"processId\": null,\n        \"rootUri\": root_uri(workdir),\n        \"workspaceFolders\": [{\"uri\": root_uri(workdir), \"name\": \"fixture\"}],\n        \"capabilities\": {\n            \"textDocument\": {\n                \"publishDiagnostics\": {\n                    \"dataSupport\": true,\n                    \"relatedInformation\": related_info_support\n                },\n                \"synchronization\": {\n                    \"dynamicRegistration\": true\n                }\n            }\n        }\n    });\n    if let Some(opts) = init_options {\n        params[\"initializationOptions\"] = opts.clone();\n    }\n    let init = json!({\n        \"jsonrpc\": \"2.0\",\n        \"id\": 1,\n        \"method\": \"initialize\",\n        \"params\": params,\n    });\n    send(stdin, &init);\n    pump(rx, stdin, Duration::from_secs(5), config_response, |m| {\n        m.get(\"id\") == Some(&json!(1))\n    })\n    .expect(\"initialize response\");\n    send(stdin, &json!({\"jsonrpc\": \"2.0\", \"method\": \"initialized\", \"params\": {}}));\n}\n\nfn shutdown_and_wait(stdin: &mut ChildStdin, rx: &mpsc::Receiver<Value>, child: &mut Child, config_response: &Value) {\n    send(stdin, &json!({\"jsonrpc\": \"2.0\", \"id\": 999, \"method\": \"shutdown\"}));\n    let _ = pump(rx, stdin, Duration::from_secs(5), config_response, |m| {\n        m.get(\"id\") == Some(&json!(999))\n    });\n    send(stdin, &json!({\"jsonrpc\": \"2.0\", \"method\": \"exit\"}));\n    // Best-effort wait so the harness doesn't leave zombies behind on\n    // assertion failures.\n    let deadline = Instant::now() + Duration::from_secs(5);\n    while Instant::now() < deadline {\n        if let Ok(Some(_)) = child.try_wait() {\n            return;\n        }\n        thread::sleep(Duration::from_millis(50));\n    }\n    let _ = child.kill();\n    let _ = child.wait();\n}\n\n/// Returns the diagnostics array if `msg` is a `publishDiagnostics`\n/// notification for a URI containing `file_uri_substring` and the array is\n/// non-empty.\nfn diagnostics_for(msg: &Value, file_uri_substring: &str) -> Option<Vec<Value>> {\n    if msg.get(\"method\")?.as_str()? != \"textDocument/publishDiagnostics\" {\n        return None;\n    }\n    let params = msg.get(\"params\")?;\n    let uri = params.get(\"uri\")?.as_str()?;\n    if !uri.contains(file_uri_substring) {\n        return None;\n    }\n    let diags = params.get(\"diagnostics\")?.as_array()?.clone();\n    if diags.is_empty() {\n        return None;\n    }\n    Some(diags)\n}\n\n#[test]\nfn cargo_backend_publishes_error_diagnostic() {\n    let tmp = TempDir::new().expect(\"tempdir\");\n    write_fixture(tmp.path(), \"pub fn boom() { undefined_symbol; }\\n\");\n\n    let mut child = spawn_server(tmp.path());\n    let stdout = child.stdout.take().expect(\"stdout\");\n    let mut stdin = child.stdin.take().expect(\"stdin\");\n    let rx = spawn_reader(stdout);\n\n    initialize(&mut stdin, &rx, tmp.path(), true, &empty_config());\n\n    let msg = pump(&rx, &mut stdin, Duration::from_secs(60), &empty_config(), |m| {\n        diagnostics_for(m, \"lib.rs\").is_some()\n    })\n    .expect(\"publishDiagnostics for src/lib.rs\");\n\n    let diags = diagnostics_for(&msg, \"lib.rs\").unwrap();\n    let has_error = diags.iter().any(|d| {\n        let severity = d.get(\"severity\").and_then(|s| s.as_i64());\n        let message = d.get(\"message\").and_then(|s| s.as_str()).unwrap_or(\"\");\n        let source = d.get(\"source\").and_then(|s| s.as_str());\n        severity == Some(1)\n            && source == Some(\"bacon-ls\")\n            && (message.contains(\"undefined_symbol\") || message.contains(\"cannot find\"))\n    });\n    assert!(\n        has_error,\n        \"expected an ERROR diagnostic mentioning the undefined symbol; got {diags:#?}\"\n    );\n\n    // Compile errors (E0425) must NOT carry the UNNECESSARY tag — that's\n    // reserved for unused/dead-code lints. Guards against `tags_from_code`\n    // ever over-matching.\n    for d in &diags {\n        let tags = d.get(\"tags\").and_then(|t| t.as_array());\n        assert!(\n            tags.is_none_or(|t| t.is_empty()),\n            \"compile error must not be tagged; got {tags:?}\"\n        );\n    }\n\n    shutdown_and_wait(&mut stdin, &rx, &mut child, &empty_config());\n}\n\n#[test]\nfn cargo_backend_code_action_replaces_unused_variable() {\n    let tmp = TempDir::new().expect(\"tempdir\");\n    // Reading the binding once silences the \"unused variable\" hint into a\n    // pure unused-variable warning whose help-child carries the\n    // MachineApplicable replacement we want to surface as a QuickFix.\n    write_fixture(tmp.path(), \"pub fn warn_me() -> i32 { let unused_var = 42; 0 }\\n\");\n\n    let mut child = spawn_server(tmp.path());\n    let stdout = child.stdout.take().expect(\"stdout\");\n    let mut stdin = child.stdin.take().expect(\"stdin\");\n    let rx = spawn_reader(stdout);\n\n    // related_info_support=false forces the server to emit the help-child\n    // span as its own diagnostic with a `data` payload (corrections),\n    // which is what powers the QuickFix code action.\n    initialize(&mut stdin, &rx, tmp.path(), false, &empty_config());\n\n    let msg = pump(&rx, &mut stdin, Duration::from_secs(60), &empty_config(), |m| {\n        let Some(diags) = diagnostics_for(m, \"lib.rs\") else {\n            return false;\n        };\n        diags.iter().any(|d| d.get(\"data\").is_some())\n    })\n    .expect(\"publishDiagnostics with `data` for src/lib.rs\");\n\n    let params = msg.get(\"params\").unwrap();\n    let uri = params.get(\"uri\").unwrap().as_str().unwrap().to_string();\n    let diags = params.get(\"diagnostics\").unwrap().as_array().unwrap().clone();\n    let target = diags\n        .iter()\n        .find(|d| d.get(\"data\").is_some())\n        .expect(\"a diagnostic with corrections must be present\");\n    let range = target.get(\"range\").unwrap().clone();\n\n    // Issue #119: every diagnostic for the unused_variables lint should\n    // carry the LSP `UNNECESSARY` tag (= 1) so editors render it dimmed.\n    for d in &diags {\n        let tags = d\n            .get(\"tags\")\n            .and_then(|t| t.as_array())\n            .unwrap_or_else(|| panic!(\"expected `tags` array on diagnostic from unused_variables lint, got {d:#?}\"));\n        assert!(\n            tags.iter().any(|t| t.as_i64() == Some(1)),\n            \"expected DiagnosticTag::UNNECESSARY (1) in tags={tags:?}\"\n        );\n    }\n\n    let req = json!({\n        \"jsonrpc\": \"2.0\",\n        \"id\": 2,\n        \"method\": \"textDocument/codeAction\",\n        \"params\": {\n            \"textDocument\": {\"uri\": uri},\n            \"range\": range,\n            \"context\": {\"diagnostics\": [target]}\n        }\n    });\n    send(&mut stdin, &req);\n\n    let resp = pump(&rx, &mut stdin, Duration::from_secs(10), &empty_config(), |m| {\n        m.get(\"id\") == Some(&json!(2))\n    })\n    .expect(\"codeAction response\");\n    let actions = resp\n        .get(\"result\")\n        .and_then(|r| r.as_array())\n        .expect(\"codeAction result must be an array\");\n    assert!(!actions.is_empty(), \"code action list must be non-empty\");\n    let titles: Vec<&str> = actions.iter().filter_map(|a| a.get(\"title\")?.as_str()).collect();\n    assert!(\n        titles.iter().any(|t| t.starts_with(\"Replace with: _\")),\n        \"expected a 'Replace with: _<name>' QuickFix; got {titles:?}\"\n    );\n\n    // Confirm the action carries an actual workspace edit pointing at our URI.\n    let action_with_edit = actions\n        .iter()\n        .find(|a| {\n            a.get(\"title\")\n                .and_then(|t| t.as_str())\n                .is_some_and(|t| t.starts_with(\"Replace with: _\"))\n        })\n        .unwrap();\n    let edit = action_with_edit\n        .get(\"edit\")\n        .and_then(|e| e.get(\"changes\"))\n        .and_then(|c| c.as_object())\n        .expect(\"workspace edit with changes\");\n    assert!(edit.contains_key(&uri), \"edit must target the diagnostic's URI\");\n\n    shutdown_and_wait(&mut stdin, &rx, &mut child, &empty_config());\n}\n\n#[test]\nfn cargo_backend_live_diagnostics_without_save() {\n    let tmp = TempDir::new().expect(\"tempdir\");\n    // Start with code that compiles cleanly. The live path must turn the\n    // diagnostic on the moment we tell the server about a dirty buffer\n    // containing broken code, *without* a save.\n    let clean_source = \"pub fn ok() -> i32 { 42 }\\n\";\n    write_fixture(tmp.path(), clean_source);\n\n    let mut child = spawn_server_with_log(tmp.path(), Some(\"bacon_ls=debug\"));\n    let stdout = child.stdout.take().expect(\"stdout\");\n    let mut stdin = child.stdin.take().expect(\"stdin\");\n    let rx = spawn_reader(stdout);\n\n    // `cargo.updateOnInsert` MUST come through `initialization_options`:\n    // it drives the static `textDocument/didChange` sync capability advertised\n    // in the `initialize` response, so the client knows to ship change events\n    // from buffer attach onwards. The runtime debounce is still a workspace\n    // setting (it's safe to tweak at runtime).\n    let init_options = json!({\n        \"cargo\": { \"updateOnInsert\": true }\n    });\n    let live_config = json!([{\n        \"cargo\": {\n            \"updateOnInsertDebounceMillis\": 100,\n        }\n    }]);\n\n    initialize_with_init_options(&mut stdin, &rx, tmp.path(), false, &live_config, Some(&init_options));\n\n    // `initialized` runs `pull_configuration` then `init_cargo_backend` and\n    // *only then* logs the \"lsp server initialized with backend: …\" line.\n    // Until that's done, `state.backend = None` and `did_change` would bail\n    // with `updateOnInsert is off`. Wait for the log_message notification\n    // before we send any change events.\n    pump(&rx, &mut stdin, Duration::from_secs(10), &live_config, |m| {\n        if m.get(\"method\").and_then(|s| s.as_str()) != Some(\"window/logMessage\") {\n            return false;\n        }\n        m.get(\"params\")\n            .and_then(|p| p.get(\"message\"))\n            .and_then(|s| s.as_str())\n            .is_some_and(|s| s.contains(\"lsp server initialized with backend\"))\n    })\n    .expect(\"server should announce post-initialized state via window/logMessage\");\n\n    let lib_uri = format!(\"{}/src/lib.rs\", root_uri(tmp.path()));\n\n    // didOpen mirrors what real LSP clients do; the buffer matches disk so\n    // there's nothing dirty yet.\n    send(\n        &mut stdin,\n        &json!({\n            \"jsonrpc\": \"2.0\",\n            \"method\": \"textDocument/didOpen\",\n            \"params\": {\n                \"textDocument\": {\n                    \"uri\": lib_uri,\n                    \"languageId\": \"rust\",\n                    \"version\": 1,\n                    \"text\": clean_source,\n                }\n            }\n        }),\n    );\n\n    // didChange with broken code. Full sync (range = None on the single\n    // change) is what we registered for via dynamic capability.\n    let dirty_source = \"pub fn ok() -> i32 { undefined_symbol_for_live_test }\\n\";\n    send(\n        &mut stdin,\n        &json!({\n            \"jsonrpc\": \"2.0\",\n            \"method\": \"textDocument/didChange\",\n            \"params\": {\n                \"textDocument\": {\n                    \"uri\": lib_uri,\n                    \"version\": 2,\n                },\n                \"contentChanges\": [{ \"text\": dirty_source }]\n            }\n        }),\n    );\n\n    // Wait for an ERROR publishDiagnostics for src/lib.rs. The initial\n    // real-workspace run (kicked off in `initialized`) may publish empty\n    // diagnostics for the clean source first; we ignore those and keep\n    // waiting. Live mode goes through a cold-cache cargo build the first\n    // time so the timeout is generous.\n    let result = pump(&rx, &mut stdin, Duration::from_secs(120), &live_config, |m| {\n        let Some(diags) = diagnostics_for(m, \"lib.rs\") else {\n            return false;\n        };\n        diags\n            .iter()\n            .any(|d| d.get(\"severity\").and_then(|s| s.as_i64()) == Some(1))\n    });\n    let msg = match result {\n        Some(m) => m,\n        None => {\n            eprintln!(\"=== bacon-ls.log ===\\n{}\", read_server_log(tmp.path()));\n            panic!(\"live ERROR publishDiagnostics for src/lib.rs (no save was sent)\");\n        }\n    };\n\n    let diags = diagnostics_for(&msg, \"lib.rs\").unwrap();\n    let has_expected_error = diags.iter().any(|d| {\n        let severity = d.get(\"severity\").and_then(|s| s.as_i64());\n        let message = d.get(\"message\").and_then(|s| s.as_str()).unwrap_or(\"\");\n        severity == Some(1) && (message.contains(\"undefined_symbol_for_live_test\") || message.contains(\"cannot find\"))\n    });\n    assert!(\n        has_expected_error,\n        \"expected an ERROR diagnostic mentioning the dirty-buffer symbol; got {diags:#?}\"\n    );\n\n    // The proof the diagnostic is from the live path, not a phantom save:\n    // the on-disk file is still the clean version we wrote at the start.\n    let on_disk = std::fs::read_to_string(tmp.path().join(\"src/lib.rs\")).unwrap();\n    assert_eq!(\n        on_disk, clean_source,\n        \"on-disk source must remain clean; the dirty diagnostic must have come from the live shadow\"\n    );\n\n    shutdown_and_wait(&mut stdin, &rx, &mut child, &live_config);\n}\n"
  },
  {
    "path": "tests/lsp_restart.rs",
    "content": "//! Regression test for issue #47 (`:LspRestart` hung the old bacon-ls\n//! forever). Simulates a client that completes `initialize` properly\n//! but never responds to the server's follow-up\n//! `workspace/configuration` request, then sends `shutdown` and `exit`.\n//! Without the shutdown-watchdog fix, that keeps `Server::serve()`\n//! alive indefinitely because the `initialized` future stays blocked\n//! waiting on a response that will never arrive.\n//!\n//! Linux-only: stdio/pipe behaviour and timing differ enough on\n//! macOS/Windows that this regression guard would flake there without\n//! adding harness complexity that has nothing to do with what's being\n//! tested. The fix under test isn't platform-specific, so a single-OS\n//! run is enough.\n#![cfg(target_os = \"linux\")]\n\nuse std::io::{Read, Write};\nuse std::process::{ChildStdout, Command, Stdio};\nuse std::sync::mpsc;\nuse std::thread;\nuse std::time::{Duration, Instant};\n\nconst BIN: &str = env!(\"CARGO_BIN_EXE_bacon-ls\");\n\nfn frame(body: &str) -> Vec<u8> {\n    format!(\"Content-Length: {}\\r\\n\\r\\n{body}\", body.len()).into_bytes()\n}\n\n/// Read LSP-framed messages off a child stdout, forwarding each JSON\n/// body onto a channel. Returns when the pipe closes. Swallowing\n/// errors is fine; the test decides success by polling the process.\nfn spawn_reader(mut stdout: ChildStdout) -> mpsc::Receiver<String> {\n    let (tx, rx) = mpsc::channel();\n    thread::spawn(move || {\n        let mut buf = Vec::new();\n        let mut tmp = [0u8; 4096];\n        'outer: loop {\n            while let Some(hdr_end) = buf.windows(4).position(|w| w == b\"\\r\\n\\r\\n\") {\n                let header = std::str::from_utf8(&buf[..hdr_end]).unwrap_or(\"\");\n                let len: usize = header\n                    .lines()\n                    .find_map(|l| l.strip_prefix(\"Content-Length: \"))\n                    .and_then(|v| v.trim().parse().ok())\n                    .unwrap_or(0);\n                let body_start = hdr_end + 4;\n                if buf.len() < body_start + len {\n                    break;\n                }\n                let body = String::from_utf8_lossy(&buf[body_start..body_start + len]).into_owned();\n                if tx.send(body).is_err() {\n                    return;\n                }\n                buf.drain(..body_start + len);\n            }\n            match stdout.read(&mut tmp) {\n                Ok(0) | Err(_) => break 'outer,\n                Ok(n) => buf.extend_from_slice(&tmp[..n]),\n            }\n        }\n    });\n    rx\n}\n\nfn wait_for<F: Fn(&str) -> bool>(rx: &mpsc::Receiver<String>, timeout: Duration, pred: F) -> Option<String> {\n    let deadline = Instant::now() + timeout;\n    while let Some(remaining) = deadline.checked_duration_since(Instant::now()) {\n        match rx.recv_timeout(remaining) {\n            Ok(msg) if pred(&msg) => return Some(msg),\n            Ok(_) => continue,\n            Err(_) => return None,\n        }\n    }\n    None\n}\n\n#[test]\nfn unresponsive_client_still_exits_after_shutdown_and_exit() {\n    let tempdir = tempfile::tempdir().expect(\"tempdir\");\n\n    let mut child = Command::new(BIN)\n        .current_dir(tempdir.path())\n        .env_remove(\"RUST_LOG\")\n        .stdin(Stdio::piped())\n        .stdout(Stdio::piped())\n        .stderr(Stdio::null())\n        .spawn()\n        .expect(\"spawn bacon-ls\");\n\n    let stdout = child.stdout.take().expect(\"stdout\");\n    let rx = spawn_reader(stdout);\n    let mut stdin = child.stdin.take().expect(\"stdin\");\n\n    let init = r#\"{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"initialize\",\"params\":{\"processId\":null,\"rootUri\":null,\"capabilities\":{\"textDocument\":{\"publishDiagnostics\":{\"dataSupport\":true,\"relatedInformation\":true}}}}}\"#;\n    stdin.write_all(&frame(init)).unwrap();\n    stdin.flush().unwrap();\n\n    // Wait for the initialize response so the server's state transitions to\n    // Initialized — otherwise the Normal middleware in tower-lsp-server\n    // rejects the notification and our handler never runs.\n    wait_for(&rx, Duration::from_secs(5), |msg| msg.contains(\"\\\"id\\\":1\")).expect(\"initialize response\");\n\n    let initialized = r#\"{\"jsonrpc\":\"2.0\",\"method\":\"initialized\",\"params\":{}}\"#;\n    stdin.write_all(&frame(initialized)).unwrap();\n    stdin.flush().unwrap();\n\n    // Wait until the server fires `workspace/configuration`. At that point\n    // the `initialized` future is parked on a response that our \"broken\"\n    // client will never send — precisely the state that used to hang\n    // `:LspRestart`.\n    wait_for(&rx, Duration::from_secs(5), |msg| {\n        msg.contains(\"workspace/configuration\")\n    })\n    .expect(\"server should issue workspace/configuration\");\n\n    let shutdown = r#\"{\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"shutdown\"}\"#;\n    stdin.write_all(&frame(shutdown)).unwrap();\n    stdin.flush().unwrap();\n\n    wait_for(&rx, Duration::from_secs(5), |msg| msg.contains(\"\\\"id\\\":2\")).expect(\"shutdown response\");\n\n    let start = Instant::now();\n    let exit = r#\"{\"jsonrpc\":\"2.0\",\"method\":\"exit\"}\"#;\n    stdin.write_all(&frame(exit)).unwrap();\n    stdin.flush().unwrap();\n    drop(stdin);\n\n    // Watchdog fires ~500ms after shutdown; leave slack for CI.\n    let deadline = Instant::now() + Duration::from_secs(5);\n    loop {\n        match child.try_wait().expect(\"try_wait\") {\n            Some(status) => {\n                let elapsed = start.elapsed();\n                assert!(status.success(), \"bacon-ls exited with {status}\");\n                assert!(\n                    elapsed < Duration::from_secs(3),\n                    \"process took {elapsed:?} to exit — expected <3s\"\n                );\n                return;\n            }\n            None if Instant::now() > deadline => {\n                let _ = child.kill();\n                let _ = child.wait();\n                panic!(\"bacon-ls did not exit within 5s after shutdown+exit — regression of #47\");\n            }\n            None => thread::sleep(Duration::from_millis(50)),\n        }\n    }\n}\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"module\": \"commonjs\",\n    \"target\": \"ES2020\",\n    \"outDir\": \"out\",\n    \"lib\": [\"ES2020\"],\n    \"sourceMap\": true,\n    \"rootDir\": \".\",\n    \"skipLibCheck\": true,\n    \"strict\": true /* enable all strict type-checking options */\n    /* Additional Checks */\n    // \"noImplicitReturns\": true, /* Report error when not all code paths in function return a value. */\n    // \"noFallthroughCasesInSwitch\": true, /* Report errors for fallthrough cases in switch statement. */\n    // \"noUnusedParameters\": true,  /* Report errors on unused parameters. */\n  }\n}\n"
  },
  {
    "path": "vscode/extension.ts",
    "content": "import * as vscode from \"vscode\";\nimport * as os from \"os\";\n\nimport {\n  ExecuteCommandRequest,\n  LanguageClient,\n  LanguageClientOptions,\n  ServerOptions,\n  Executable,\n} from \"vscode-languageclient/node\";\n\nlet client: LanguageClient | undefined;\n\n// Settings that change how the server itself is launched. These cannot be\n// applied without a restart.\nconst RESTART_ON_CHANGE = [\n  \"bacon-ls.path\",\n  \"bacon-ls.logLevel\",\n  // The server cannot switch backends at runtime — restart is the only path.\n  \"bacon_ls.backend\",\n];\n\nexport async function activate(\n  context: vscode.ExtensionContext,\n): Promise<void> {\n  let name = \"Bacon-ls\";\n\n  const outputChannel = vscode.window.createOutputChannel(name);\n\n  context.subscriptions.push(outputChannel);\n\n  context.subscriptions.push(\n    vscode.workspace.onDidChangeConfiguration(\n      async (e: vscode.ConfigurationChangeEvent) => {\n        if (RESTART_ON_CHANGE.some((s) => e.affectsConfiguration(s))) {\n          await vscode.commands.executeCommand(\"bacon-ls.restart\");\n        }\n        // All other bacon_ls.* changes are picked up by the server through\n        // workspace/didChangeConfiguration, sent automatically by\n        // vscode-languageclient because of synchronize.configurationSection.\n      },\n    ),\n  );\n\n  context.subscriptions.push(\n    vscode.commands.registerCommand(\"bacon-ls.restart\", async () => {\n      if (client && client.needsStop()) {\n        await client.stop();\n      }\n\n      try {\n        client = await createClient(context, name, outputChannel);\n      } catch (err) {\n        vscode.window.showErrorMessage(\n          `${err instanceof Error ? err.message : err}`,\n        );\n        return;\n      }\n\n      await client.start();\n    }),\n  );\n\n  context.subscriptions.push(\n    vscode.commands.registerCommand(\"bacon-ls.run\", async () => {\n      if (!client || !client.isRunning()) {\n        vscode.window.showWarningMessage(\"bacon-ls is not running\");\n        return;\n      }\n      try {\n        await client.sendRequest(ExecuteCommandRequest.type, {\n          command: \"bacon_ls.run\",\n        });\n      } catch (err) {\n        vscode.window.showErrorMessage(\n          `bacon-ls.run failed: ${err instanceof Error ? err.message : err}`,\n        );\n      }\n    }),\n  );\n\n  await vscode.commands.executeCommand(\"bacon-ls.restart\");\n}\n\nasync function createClient(\n  context: vscode.ExtensionContext,\n  name: string,\n  outputChannel: vscode.OutputChannel,\n): Promise<LanguageClient> {\n  const env = { ...process.env };\n\n  const extensionConfig = vscode.workspace.getConfiguration(\"bacon-ls\");\n  const path = await getServerPath(context, extensionConfig);\n\n  outputChannel.appendLine(\"Using bacon-ls server \" + path);\n\n  env.RUST_LOG = extensionConfig.get(\"logLevel\");\n\n  const run: Executable = {\n    command: path,\n    options: { env: env },\n  };\n\n  const serverOptions: ServerOptions = {\n    run: run,\n    debug: run,\n  };\n\n  const clientOptions: LanguageClientOptions = {\n    documentSelector: [\n      { scheme: \"untitled\" },\n      { scheme: \"file\", pattern: \"**\" },\n      { scheme: \"vscode-scm\" },\n    ],\n    outputChannel: outputChannel,\n    traceOutputChannel: outputChannel,\n    // Forward server-side settings through the standard\n    // workspace/configuration pull (which vscode-languageclient handles by\n    // mapping section -> getConfiguration(section)) and notify the server on\n    // changes via workspace/didChangeConfiguration.\n    synchronize: {\n      configurationSection: \"bacon_ls\",\n    },\n  };\n\n  return new LanguageClient(\n    name.toLowerCase(),\n    name,\n    serverOptions,\n    clientOptions,\n  );\n}\n\nasync function getServerPath(\n  context: vscode.ExtensionContext,\n  config: vscode.WorkspaceConfiguration,\n): Promise<string> {\n  let path = process.env.BACON_LS_PATH ?? config.get<null | string>(\"path\");\n\n  if (path) {\n    if (path.startsWith(\"~/\")) {\n      path = os.homedir() + path.slice(\"~\".length);\n    }\n    const pathUri = vscode.Uri.file(path);\n\n    return await vscode.workspace.fs.stat(pathUri).then(\n      () => pathUri.fsPath,\n      () => {\n        throw new Error(\n          `${path} does not exist. Please check bacon-ls.path in Settings.`,\n        );\n      },\n    );\n  }\n\n  const ext = process.platform === \"win32\" ? \".exe\" : \"\";\n  const bundled = vscode.Uri.joinPath(\n    context.extensionUri,\n    \"bundled\",\n    `bacon-ls${ext}`,\n  );\n\n  return await vscode.workspace.fs.stat(bundled).then(\n    () => bundled.fsPath,\n    () => {\n      throw new Error(\n        \"Unfortunately we don't ship binaries for your platform yet. \" +\n          \"Try specifying bacon-ls.path in Settings. \" +\n          \"Or raise an issue [here](https://github.com/crisidev/bacon-ls/issues) \" +\n          \"to request a binary for your platform.\",\n      );\n    },\n  );\n}\n\nexport function deactivate(): Thenable<void> | undefined {\n  if (!client) {\n    return undefined;\n  }\n  return client.stop();\n}\n"
  }
]