Skip to content

[ty] Support multiple workspace folders in a single ty LSP server instance#22953

Merged
BurntSushi merged 8 commits intomainfrom
ag/multi-workspace
Jan 30, 2026
Merged

[ty] Support multiple workspace folders in a single ty LSP server instance#22953
BurntSushi merged 8 commits intomainfrom
ag/multi-workspace

Conversation

@BurntSushi
Copy link
Member

This is also known as "multi-root" workspace support. Or more
imprecisely, "multiple workspace" support. (The LSP protocol doesn't
actually have a concept that one can call "multiple workspaces." There
is only ever one workspace, but that workspace may have multiple root
folders. i.e., Workspace folders.)

The approach taken here is to use a distinct Salsa database for each
workspace folder. It's not clear if we still stick with this approach
going forward. In particular, it's likely to exacerbate memory usage
in contexts where there are many open workspace folders. There are
likely some ways to claw back memory usage, for example, by sharing one
auto-import index instead of using an auto-import index for each Salsa
database. Regardless, this seems to be how other LSP servers (like
pyright) handle multiple workspace folders. So this is where we start.

This PR includes support for adding/removing workspace folders via the
workspace/didChangeWorkspaceFolders notification.

I've added a bunch of end-to-end tests covering multiple workspace
folder support, including handling of settings. (This PR makes a change
to how global settings are extracted in the presence of multiple
workspace folders. The change is made in its own commit with more
details.)

Reviewers are encouraged to read this PR commit-by-commit. They may
find it helpful to skip to reading the last commit, which includes
has a bunch of end-to-end tests. It might be helpful to see what the
intended observable behavior is.

Closes astral-sh/ty#89

@BurntSushi
Copy link
Member Author

Recording of what main is like:

ty-single-workspace-only.mp4

A recording of what it's like to support multiple workspace folders, but without support for workspace/didChangeWorkspaceFolders notifications:

ty-db-per-workspace-no-change-support.mp4

And finally, full support:

2026-01-29T14.54.42-05.00.mp4

@AlexWaygood AlexWaygood added the ty Multi-file analysis & type inference label Jan 29, 2026
Copy link
Member

@dhruvmanila dhruvmanila left a comment

Choose a reason for hiding this comment

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

This is great!

I'm a bit confused with how this works with "workspace" diagnostic mode.

Comment on lines +498 to +505
// Last setting wins.
global_options = Some(
self.initialization_options
.options
.global
.clone()
.combine(options.global),
);
Copy link
Member

Choose a reason for hiding this comment

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

I think this makes sense but just to confirm my understanding based on reading the above comment:

Considering a VS Code workspace containing two workspace folders and

  • User has defined some settings in their global settings.json (also known as User Settings)
  • User has also defined per-project settings in .vscode/settings.json for both the projects

When a server asks for configuration via workspace/configuration, VS Code should resolve the configuration values considering all the above locations. Given the restriction that we set on global settings in VS Code (the "window" scope), the values for all of the global settings are going to be the same for each workspace. This means that we don't need to combine them for each workspace and we can choose the last one.

Am I interpreting this correctly?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, your interpretation of what VS Code does matches what I've observed and is part of the motivation for doing this.

I think there's another side to the motivation here: what if we could get different global settings from each workspace folder? Well in that case, either way we slice it, we're going to get it wrong for at least one of the workspace folders because we can only have one value for each global setting. So I make the additional argument here that in that scenario, we shouldn't try to combine global settings across workspace folders and instead just use the most recent settings transmitted from the client.

Copy link
Member

Choose a reason for hiding this comment

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

Yeah, that seems like a reasonable argument 👍

Comment on lines 152 to 154
Action::InitializeWorkspaces(workspaces_with_options) => {
self.session
.initialize_workspace_folders(workspaces_with_options, &client);
.initialize_workspace_folders(&client, workspaces_with_options);
Copy link
Member

Choose a reason for hiding this comment

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

nit: it might be useful to rename the action to be similar in line with the invoked function, so Action::InitializeWorkspaceFolders

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah I was trying to avoid doing too much renaming. I would like to do a follow-up PR where I do a more consistent rename from "workspace" to "workspace folder."

Comment on lines +168 to +182
// Now remove one of the workspaces and assert that the
// diagnostics are now limited only to `root1`.
server.change_workspace_folders([], [root2]);
// We don't need to wait for workspace initialization
// since we are only removing a workspace. That is, the
// server is not expected to send a `workspace/configuration`
// request.

let workspace_diagnostics = server.workspace_diagnostic_request(None, None);
assert_snapshot!(
condensed_workspace_diagnostic_snapshot(workspace_diagnostics), @r"
file://<temp_dir>/root1/main.py
0:0..0:14[ERROR]: Name `does_not_exist` used when not defined
"
);
Copy link
Member

Choose a reason for hiding this comment

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

Oh, this is interesting, I'm not sure how this is working when the logic for removing the workspace folders only clears diagnostics for open files? I'm probably missing something 😅

Copy link
Member Author

Choose a reason for hiding this comment

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

I think for this test specifically, it works because of this:

let mut reporter = WorkspaceDiagnosticsProgressReporter::new(work_done_progress, writer);
for db in snapshot.projects() {
db.check_with_reporter(&mut reporter);
}
Ok(reporter.into_final_report())

Since the workspace folder was removed, so to was its project database. So it's never actually checked and then isn't included in the response. The server itself doesn't seem to keep around state of diagnostics present on the client, so this all works.

I think the fact that the workspace diagnostics get cleared in VS Code is perhaps a little more surprising? I think there's an expectation that a diagnostic sticks around until it has been cleared. But it seems like the VS Code client is doing some clearing for us perhaps.


server.change_workspace_folders([], [root1]);

let workspace_diagnostics = get_expected_empty_workspace_diagnostics_and_shutdown(server);
Copy link
Member

Choose a reason for hiding this comment

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

VS Code seems to be going brrr on my home (or root?) directory after I removed the last workspace, it might be worth taking a look at what's happening in this scenario as a follow-up

Image

Copy link
Member

Choose a reason for hiding this comment

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

This might be related to #17944 so I wouldn't look too deep into it.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah it looks like I can make VS Code peg the CPU even on main by doing the same thing. Basically, opening a file with no workspace folders added.

@dhruvmanila
Copy link
Member

It might be useful to try this out on the pyx codebase, maybe something that @zsol could help with?

@zsol
Copy link
Member

zsol commented Jan 30, 2026

For sure!

forsure

This is a small code change meant to implement a simpler semantic in
preparation for supporting multiple workspace folders. Previously, we
were combining global options across all workspace folders. In this
change, we now only combine global options from initialization and each
workspace folder independently. We then take the *most recent*
combination and use that for our global settings.

It's not clear that this actually changes anything in practice. I've
been unable to make VS Code send different LSP configurations for each
workspace folder within a single workspace. (It's plausible our ty VS
Code extension is actually doing this.)
… server instance

This is pretty much all we need to support multiple workspace folders.
Previously, we were going out of our way to force only allowing a single
workspace. This commit just removes that restriction.

This works by making each workspace folder use its own project state
(i.e., salsa database).

We chose to go this route for now since it's the simplest to implement
and unlocks folks using multi-root workspaces inside of VS Code. We
hadn't done this before because we were undecided on how this would
impact memory usage. In particular, this will result in a separate
auto-import cache for each workspace folder. We live with this
limitation for now.

This does not add support for responding to
`workspace/didChangeWorkspaceFolders` notifications. We'll do that in a
subsequent commit.
I find this is a bit easier to reason about.
Previously, we only kept a count of the number of workspaces
initialized, but we had no way of knowing *which* workspaces were
initialized. Knowing which are initialized is useful for avoiding
initializing a workspace again.

This is relevant because previously we only called
`initialize_workspace_folder` at actual initialization time. But soon
we're going to want to call it at arbitrary points in response to
`workspace/didChangeWorkspaceFolders` notifications. Making
initialization a no-op if a workspace is already initialized seems more
flexible and harder to misuse. With that said, presumably, LSP clients
won't try to re-initialize workspace folders.

This does change a couple checks that were previously constant time to
be linear over the number of workspace folders. I think this is fine
given that the unit of work here is tiny and the number of workspace
folders is not expected to be enormous. (The point at which a linear
scan over all workspaces is problematic likely means we have much bigger
problems.)
This basically moves the logic out of `Server` and into `Session`.
Moreover, instead of iterating over all registered workspace folders,
it only iterates over registered workspace folders that haven't yet
been initialized. This in turn makes the routine idempotent and easier
to call (for when we register new workspace folders).
This lifts the already existing helper routine to a place where more
things can use it. We'll want this for when we remove a workspace
folder.
@BurntSushi BurntSushi force-pushed the ag/multi-workspace branch 2 times, most recently from bd2436a to 6fc5f32 Compare January 30, 2026 14:51
…cations

With all of the refactoring done in previous commits, this mostly comes
down to adding a new notification type and a new method for removing a
workspace folder (which requires some clean-up).
This tests various permutations for adding and removing workspace
folders both during and after server initialization. We also add some
tests checking the behavior of settings as passed from the LSP client.

We make use of workspace diagnostics here as a way to check whether the
server is "aware" of a workspace folder or not.
@BurntSushi BurntSushi merged commit 1d9c84d into main Jan 30, 2026
45 checks passed
@BurntSushi BurntSushi deleted the ag/multi-workspace branch January 30, 2026 15:15
BurntSushi added a commit to astral-sh/ty-vscode that referenced this pull request Jan 30, 2026
With astral-sh/ruff#22953 merged, the ty server
now supports multiple workspace folders. This means we can deal with
workspace settings diverging between folders.

This change specifically only applies to workspace settings. The other
global settings are left with a scope of `window`, since the ty server
expects there to be only one value for global settings.

Closes #319
BurntSushi added a commit to astral-sh/ty-vscode that referenced this pull request Jan 30, 2026
With astral-sh/ruff#22953 merged, the ty server
now supports multiple workspace folders. This means we can deal with
workspace settings diverging between folders.

This change specifically only applies to workspace settings. The other
global settings are left with a scope of `window`, since the ty server
expects there to be only one value for global settings.

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

Labels

ty Multi-file analysis & type inference

Projects

None yet

Development

Successfully merging this pull request may close these issues.

LSP: Support Workspace folders

4 participants