Skip to content

Improve cargo backend + refactor to separate Bacon and Cargo backends - #113

Merged
crisidev merged 49 commits into
crisidev:mainfrom
tmontaigu:main
Apr 19, 2026
Merged

Improve cargo backend + refactor to separate Bacon and Cargo backends#113
crisidev merged 49 commits into
crisidev:mainfrom
tmontaigu:main

Conversation

@tmontaigu

@tmontaigu tmontaigu commented Apr 8, 2026

Copy link
Copy Markdown
Contributor

This chain of commit refactor bacon_ls

  • A BackendRuntime enum is introduced to have a clear split between what state is meant for the cargo backend, and what sate is used for the bacon backend, as well as enforcing the correct parts of the backend is used

  • The backend is initialized from what the user config gives, if the config changes what backend to use a restart must be done. There is one thing though, it sort of relies on the client to send its config after init, which seems to be the case for neovim but would need to be tested with other clients like vscode

  • The cargo backend has been refactored to allow more easily to configure the command that is run (check vs clippy, which args, what env var)

  • An async closure is "spawned" to parse the stderr from cargo to get progress and report to lsp client

  • The parsing of json diagnostics groups refactorings that are meant to be done in one go

  • The parsing of json diagnostics follows expansion (given in the json diagnostics) until it reaches a file that is in the project to properly report diagnostics tied to macros

  • The childrens of diagnostics are either sent as "relatedInformation" of the parent (children are often things like "help: a function name xxx exists) OR sent as their own parent depending on the capabilities advertised by the client. This can be overriden by a config flag

  • The function responsible for parsing now no longer collects diagnostics, but send them via a channel to a receiver that then collects and is able to periodically push to the client new diagnostics (every 5 secs by default, configurable and can be disabled). This was done because in one of my project, cargo check can print some diagnostics, but take few more seconds to finish, and so not having to wait for the command to be fully finished is great

  • the cargo backend is more "workspace/project" oriented this means:

    • It pushes all diagnostics even for files not opened
    • Only sends a cargo command on file save (no longer on file open)
    • Only one cargo command can be running, with 2 behavior when a new one needs to be launched:
      • Kill current if running and start new one (default)
      • wait for current to finish and then start new one

Fixes #48
Should fix #64

@crisidev

crisidev commented Apr 8, 2026

Copy link
Copy Markdown
Owner

I will have time to review this over the weekend! In the meantime, there is a conflict caused by 2 MRs I just merged that are fixing some issues caused by an old nix lockfile and Cargo.lock

@crisidev

Copy link
Copy Markdown
Owner

Thanks so much for this! There's a lot of really valuable work here. The dependency pruning, the cargo backend rewrite, the macro-expansion handling, and the channel-based diagnostic streaming are all clear wins, and the test coverage you added with real cargo JSON fixtures is great.

Before merging I'd like to get a few things sorted. The blockers are mostly small fixes, plus a doc cleanup:

  1. Command name mismatch: bacon_ls.run is registered as a server capability but execute_command only handles bacon_ls.check, so the "manually start a check" feature is unreachable. See inline comment.
  2. initializationOptions is no longer parsed at all.: Combined with pull_configuration() being commented out in initialized(), clients that don't proactively send workspace/didChangeConfiguration (vscode, helix, coc, and the README's own neovim example) end up with no backend initialized. initialized() logs "No backend initialized" and bails. Could you either (a) re-parse initialization_options in initialize() as a fallback, or (b) figure out what was breaking the bacon backend in the early-pull path and restore that? Happy to help debug if useful.
  3. Backwards-incompatible config schema with no migration story.: Every existing key (useBaconBackend, cargoCommandArguments, cargoEnv, updateOnSave, locationsFile, …) is either renamed, moved under cargo/bacon, or removed. Existing 0.26 users will silently fall back to defaults. We need at least a CHANGELOG note + a "Migrating from 0.26.x" section in the README before this can ship.
  4. README is in a half-migrated state: the old flat keys are still documented in the middle of the file and the Neovim Manual example still uses the old init_options = { updateOnSave = ..., useBaconBackend = ... } shape, which now does nothing. Details inline.

Other minor things I would like to address if possible:

  • BaconOptions has no reset() matching CargoOptions::reset(), so deletions of bacon keys don't take effect on config update.
  • tempfile = "3.26.0" in dev-deps is a downgrade vs main's 3.27.0, probably from the rebase.
  • Stray // self.pull_configuration().await; comment in initialized().

The overall direction is good! Let's get the bootstrap path solid, fix the command name, and tighten up the docs.

Comment thread src/lsp.rs
Comment on lines +82 to +85
execute_command_provider: Some(ExecuteCommandOptions {
commands: vec!["bacon_ls.run".to_string()],
..Default::default()
}),

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This registers bacon_ls.run, but execute_command below only handles bacon_ls.check, so the manually-triggered check is dead code. Pick one name and use it both here and at line 282.

Comment thread src/lsp.rs Outdated
}

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

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This commented-out line is the heart of the bootstrap problem. Right now:

  1. initialize() no longer parses params.initialization_options (used to in main)
  2. pull_configuration() is disabled here
  3. did_change_configuration() is the only path that materializes a backend.

So a client that doesn't proactively push workspace/didChangeConfiguration ends up with state.backend == None, this function logs "No backend initialized", and no diagnostics are ever produced. AFAIK Neovim happens to work because nvim-lspconfig pushes settings. vscode/helix/coc are not guaranteed to.

Two options we can use here I think:

  • restore parsing of initialization_options in initialize() as a startup fallback
  • figure out what was breaking the bacon backend when pull_configuration was called early and fix that root cause.
    Either way, please remove the dead comment when you settle on a solution.

@tmontaigu tmontaigu Apr 14, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I commented it out because I had a problem with it. The short version is that in did_change_configuration the client sends

settings : {
  "bacon_ls: {
    //...
   }
}

But i was expection directly the object that is in bacon_ls :
But in a commit that is part of this history, I fixed, but did not restore this pull

There's still one thing, if the sends an empty config (or if we pull and empty config), we are still in a state where no backend is initialized. So atm, the minimal config is

settings : {
  "bacon_ls: { 
     // either "cargo" :{}, or "bacon": {}, or "backend": "cargo",
   }
}

So what we should probably do is that if after the pull there is still no backend, we init the cargo backend as a default

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Defaulting to the cargo backend seems to me like a good idea!

Comment thread src/lsp.rs Outdated
}

async fn execute_command(&self, params: ExecuteCommandParams) -> jsonrpc::Result<Option<LSPAny>> {
if params.command == "bacon_ls.check" {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

String mismatch with the registered command at line 83, see the comment there.

Comment thread src/lib.rs
}

impl BaconOptions {
pub(crate) fn update_from_json_obj(&mut self, bacon_obj: &Map<String, Value>) -> jsonrpc::Result<()> {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CargoOptions has a reset() that's called before re-parsing on config update, so deleting a key from the user's config takes effect on the next update. BaconOptions doesn't have an equivalent and isn't reset before re-parsing at line 694, so once a bacon key is set, removing it from the config silently keeps the prior value.

Could you add a matching BaconOptions::reset() and call it from the same code path?

Comment thread Cargo.toml Outdated

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

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
tempfile = "3.26.0"
tempfile = "3.27.0"

Comment thread README.md
* `runBaconInBackground`: Run `bacon` in background for the `bacon-ls` job (default: true)
* `runBaconInBackgroundCommand`: Path to the command used to run `bacon` in the background (defaults to find in `$PATH`).
* `runBaconInBackgroundCommandArguments`: Command line arguments to pass to `bacon` running in background (default "--headless -j bacon-ls")
* `synchronizeAllOpenFilesWaitMillis`: How many milliseconds to wait between background diagnostics check to synchronize all open files (default: 2000).

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This whole bullet list documents the old flat keys (useBaconBackend, updateOnSave, runBaconInBackground, validateBaconPreferences, …) which no longer exist after this PR. Please delete this block as the new schema is already documented in the JSON example at the top of the Configuration section.

Comment thread README.md
if config.name == "bacon_ls" then
local settings = codesettings.local_settings()["_settings"]["bacon_ls"]
if settings ~= nil then
config["settings"]["bacon_ls"] = sett6ings

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
config["settings"]["bacon_ls"] = sett6ings
config["settings"]["bacon_ls"] = settings

Comment thread README.md
}
```

### Configuration - Native Cargo Backend

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Configuration - Native Cargo Backend" / "Configuration - Bacon Backend" section headers are out of date.

These two subsection headers (and the "This works only with bacon-ls is configured with initOptions = { useBaconBackend = true }" line) reference the pre-PR config model. Either drop the headers entirely or rewrite them around the new backend: "cargo"|"bacon" key.

Comment thread README.md
Comment on lines 209 to 216
```lua
vim.lsp.config('bacon-ls', {
init_options = {
updateOnSave = true
updateOnSaveWaitMillis = 1000
...
}
})

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After this PR, init_options (== initializationOptions) is no longer parsed and those keys no longer exist. Please update this to use the new settings = { bacon_ls = { cargo = { ... } } } shape.

I have the save-trigger easy, leading to bacon-ls spawning a bunch of
cargo commands.

This adds 2 mode, 1 where if a cargo command is already running
it gets killed to let the new one run.

The second mode waits for the command to finish but 'queues'
another one to be launched once the former is done.
And parse these from the more common settings
This ensure deleted keys from user config are
not 'cached'
{
  "env1": "value1",
  "env2: 2,
}

is less confusing
it made neovim log an error about it
Push errors when the command failed to the client
Parse `Blocking waiting on lock` from cargo's output
as its a better message than just progress being 0
cargo wasn't even used
removing them allows to go from 458 transitive deps
to 121 a good win in a world of supply chain attacks
If it is ever needed again it could be re-added
in the cargo backend, add a set of files for which
we added diagnostics.

This allows to properly clears diagnostics for files
which previously had. e.g foo.rs and bar.rs had diagnostics
you fixed bar.rs hit saved, bacon-ls runs cargo which
says that there is no error. We now cleany clear both foo.rs and
bar.rs whereas previously, user had to trigged a save of foo.rs.

Another change is now that we publish diagnostics for all files
even non-opened ones.
This commit create two struct one per backend to better isolate
what are the things each backend needs.

On top of this an enum that contains the currently active backend
is used to make the code easier to write and follow.

The backend is now chosen on startup using the user's settings,
the settings for a backend can change while the server is running,
but to change backend, a restart must happen.
The progress for the cargo backend was not as good as it should have been.

This was because we used read_line to iterate on the lines
however, the output of cargo uses \r (something we split against
after the read_line).

Cargo outputs a newline when it prints `Checking xxx`,
for `Building xxx` it ends with a `\r`. This meant we only
updated the progress when a `Checking xxx` was emitted because we caught
the line `Building xxx\r Building yyy\r Checking zzzz\n`.

This was mostly problematic when the cargo check was ran on a non clean
build where the output was `Checking xxx\n Building xxx [98/100]\r`.
We were waiting for a newline which is not there, and thus only showed
progress as 0 when it was actually 98 and only when cargo was actually
done it would output lines with `\n`.

The fix is to simply read until we get either an `\r` or an `\n`.
since only the bacon bacon tracks opened files
we can do the match only once and not per file
also we now hold the lock for longer but that
is fine I would say
the cargo backend pushes diagnostics for all files
even not opened ones so this publish diagnostic thingis not needed
This adds a `refreshIntervalSeconds` option
that if Some(..) will make bacon publish/refresh
the diagnostics it has parsed so far to the client
approximately every refreshInterval seconds.

This allows to not have to wait for the cargo command to complete
to have the first feedbacks, as on some larger projects, the last steps
can be quite long.

default to 5 seconds, to disable the `refreshIntervalSeconds` option can
be set to null or a negative number
The code did not follow the expansion field of of the
span which meant that errors originating from
macros from a different crate would not show up be cause
the span used pointed to a dependency source in cargo's
registry
This refactor fixes a few problems the previous code had:

- Children were put as diagnostics, leading to having more
  things like `if this is intentional prefix with _` for unused variable
  (for example) as diagnostics
- Handle code actions more fully. The previous code
  put 1 action per replacement, with the generic 'Replace with bacon-ls
  suggestion'.

  The new code groups the replacement actions into one.
  This is because for example in `use std::io::{Cursor, Read}`,
  if Cursor is an unused import, there are 3 replacements to apply:
  `{`, `Cursor,`, `}`.
  Also the new code action tries to have a less generic name to be clearer for the users
it prevents bacon backend from being usable
We changed the child diagnostics from cargo
(the ones like "help a method name xx exists)
to be sent as related information with the hope
that the client would display them better
(i.e less pollution of the editor) + these
are already part of the rendered message

However it seems that neovim does not support them
out of the box (a plugin called tiny-inline-diagnostics does)
so we change the behavior to:
1) first detect if the client says it supports them
2) add a bacon_ls.cargo.separateChildDiagnostics (that overrides
the client advertised capabilities) in case the user still wants
these as their own diagnostics
This clears any diagnostics that we publish when a new check if
published
We where setting the state to Running before calling
publish_cargo_diagnostics which meant that the function
would just re-queue indefinitely
@tmontaigu

Copy link
Copy Markdown
Contributor Author

Comments regarding the code should be fixed, now i have to update the readme

@crisidev

Copy link
Copy Markdown
Owner

Comments regarding the code should be fixed, now i have to update the readme

I had a look at the changes and I think you can leave the README to me if that's ok. This looks great. I'll merge, do some tests, update the README and release a new version.

Thanks a million for the contribution!

@crisidev
crisidev merged commit 22d0437 into crisidev:main Apr 19, 2026
2 of 4 checks passed
crisidev added a commit that referenced this pull request Apr 19, 2026
* Fix CI/CD
* Update VsCode extension

Signed-off-by: Matteo Bigoi <bigo@crisidev.org>
crisidev added a commit that referenced this pull request Apr 19, 2026
@tmontaigu

tmontaigu commented Apr 19, 2026

Copy link
Copy Markdown
Contributor Author

If you prefer to do it that's fine, i was a bit busy and planned to do it this week
Saw it was done

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Not showing error in sqlx macros Feature: Progress during compilation (using cargo backend)

2 participants