Repository navigation
Conversation
test/index.ts looked for *.test.ts files in out/, which only contains the compiled *.test.js files, so no test ran. Also, run() returned before mocha finished, so VS Code exited before any test could report a failure. Look for *.test.js and resolve run() once mocha is done, rejecting if tests failed. The two InactiveRegionsFeature tests have been failing unnoticed since the decoration type is created only once the visible editors change: they now create it before sending the notifications.
vscode-languageclient declares handleFailedRequest(type, token, error, defaultValue, ...), but the override declared (type, error, token, defaultValue). The error therefore arrived in the token parameter, so the ResponseError check never matched and failed workspace/executeCommand requests (e.g. applyTweak) were never reported to the user. The swapped arguments were also passed on to the base implementation. Use the library's parameter order and forward the optional showNotification and throwOnCancel arguments.
Per-project clangd instances need to read settings in the scope of the
folder they serve, and ${workspaceFolder} must then refer to that
folder rather than to the first workspace folder.
config.get() takes an optional folder. With a folder, the value is read
from the folder's configuration scope, ${workspaceFolder} (and its
aliases) resolves to the folder's path and ${workspaceFolderBasename}
to its name. Without a folder, the substitutions are unchanged, and the
value is read for no specific resource (scope null): that gives the same
values as before, and VS Code expects it for resource-scoped settings,
which it otherwise reads with a warning in the log.
Co-authored-by: Daan De Meyer <daan.j.demeyer@gmail.com>
planger
force-pushed
the
upstream-multi-project
branch
from
November 9, 2026 12:39
d3410c8 to
6374428
Compare
planger
marked this pull request as ready for review
November 9, 2026 12:49
The activation code owned a single ClangdContext, and the context ran the installer and started its own client from the constructor. Supporting more than one clangd instance needs a single owner for the session state, and a context whose wiring is complete before its client starts. Introduce ClangdContextManager, which runs install.activate, creates the context and implements restart and shutdown. extension.ts registers the commands and delegates to it, and the API reads the current client from the manager. API v1 returns no client while clangd is stopped, where it used to return the stopped client. Restart and shutdown keep the guard of before: they do nothing while the client is starting. They now also do nothing while the session itself starts (install.activate and the creation of the context), where a restart used to create a second context concurrently. ClangdContext now takes the clangd path and its manager, exposes the documentSelector it serves, and starts its client only from an explicit startClient(). dispose() no longer calls stop() on a client that is starting, which the language client rejects: it waits until the client runs or failed to start, also when the client restarts after a crash, and ignores stop() failures, which the client logs. A client disposed while it starts still initializes its features; what they subscribe to from then on is disposed right away. Contexts are owned by the manager and are no longer pushed into the extension's subscriptions on every restart. API v1's languageClient is now undefined while there is no client, e.g. after clangd.shutdown or while a restart reinstalls clangd, where it used to be the stopped client.
Each feature registered its commands, tree views and the status bar item while a context started its client, so a second context would fail with duplicate command and view ids. Split every feature into a UI part, registered once per session through registerFeatureCommands(manager), which sits next to the list of per-client activations in clangd-context.ts, and a per-client part that stays a StaticFeature of the context. The UI part looks up the context to act on through the manager: - editor commands (AST, type hierarchy, switch header/source) use the context serving the document; - the type hierarchy view remembers the client that produced its root and closes when that client's context goes away, also while its request is in flight; - the memory usage view has one top-level node per client and leaves out clients whose request fails; if all fail, the command fails with the error, as before; - the file status keeps the statuses per context; - the legacy inlay hints provider uses the context's document selector. The commands exist exactly while a session exists, as before. With the single context of today, they behave as before, except when the client has stopped for good (e.g. it crashed too often): the editor commands and the memory usage command then say that clangd is not running instead of failing with a request error. Co-authored-by: Daan De Meyer <daan.j.demeyer@gmail.com>
Running one clangd per project needs a description of the projects and their roots, independent of where the projects come from. A ClangdProject is a root directory with an optional name, and a ClangdProjectsChangeEvent lists added, removed and updated projects. Provide the helpers the manager needs: - uriContains() compares paths segment-wise (so /a/p does not contain /a/p2) and requires the same scheme and authority; - projectKey() identifies a project by its root; - both ignore case on Windows and macOS, as VS Code does when it matches the RelativePatterns of document selectors; - projectLabel() names a project in the UI: its name, or its path if another project has the same name; - normalizeProjects() drops projects that are not on disk, duplicates and nested projects; the outermost project wins, because document selectors can't exclude a sub-directory and a client receives every document below its root; - diffProjects() reports added, removed and updated (renamed) projects.
Every clangd advertises the same executeCommandProvider commands (clangd.applyFix and clangd.applyTweak). vscode-languageclient's ExecuteCommandFeature registers each of them as a VS Code command, so a second client fails to register them. ServerCommandRegistry registers each command once and keeps the set of clients that advertised it; the registration goes away with the last of them. An invocation goes to the client serving the document named in the arguments (TweakArgs.file for applyTweak, the first document of the WorkspaceEdit for applyFix, see ClangdLSPServer.cpp), else to the client of the active editor, else to the only owner. Failures are reported through the client's handleFailedRequest(). SharedServerCommandsFeature moves a client's commands to the registry in preInitialize(), before the built-in feature sees them. It replaces executeCommandProvider in the capabilities copy handed to the features, so the client's initializeResult still lists all commands. Once detached, e.g. from a client that is disposed while it starts, it still hides the commands from the built-in feature but no longer adds them to the registry.
A single clangd serving all workspace folders can't use per-folder compilation databases, arguments or working directories, and a crash or a large index in one folder affects all of them. With the new window-scoped setting clangd.enablePerFolderServer (default false, changing it restarts clangd), the manager runs one clangd per project, i.e. per workspace folder: - install.activate runs once; the commands and views are registered once per session. - A ProjectSet in project.ts follows the workspace folders, normalizes them (the outermost of nested folders wins) and reports the differences. The manager only reacts to them: removed projects lose their context and output channel, renamed ones restart their client, and added ones start for visible editors. A channel is disposed right away; the writes of its clients that are still stopping are dropped. The projects are kept across restarts and cleared when the default mode starts. - A project's context is created lazily when one of its files is shown in a visible editor (initially and on onDidChangeVisibleTextEditors), not when a document is merely opened. The pending creation is stored per project key before anything is awaited, so a project gets one context, and a creation that completes after the project was removed, stopped or the session was torn down is disposed. A caller waiting for an abandoned creation gets the context of the one that superseded it. - clangd.restart and clangd.shutdown take an optional Uri and then act on the owning project only; the config file watcher passes the project's root. A project shut down this way stays stopped until it is restarted. Without a Uri, all clients restart or stop. A restart recreates every context that existed before, also those without a visible file. - In multi-project mode, nothing blocks a restart or shutdown of all clients: a context whose client is starting stops it once it has started, its server commands never reach the shared registry, and the session can be torn down while it starts. In the default mode, a starting session or client still blocks them, as before. A restart requested while the session starts or restarts is served by that start or restart, unless it was abandoned, e.g. by a shutdown, or clangd.enablePerFolderServer changed meanwhile; it then runs afterwards, so that two sessions never start at the same time. Each project's client selects only the clangd documents below its root through a RelativePattern, is locked to its workspace folder, runs in the root directory with clangd.arguments (read in the folder scope), and logs to its own channel "clangd (<name>)", or "clangd (<root path>)" if another project has the same name. clangd.trace gets the project's name and a hash of its root inserted before the extension (trace.json becomes trace.<name>-<hash>.json), so that the instances don't write to the same trace file, even if projects have the same name. The server commands of all clients go through the shared registry, also in the default mode, where the single client owns them; a mode change therefore never registers a command twice. The UI follows the project: the file status shows the project's label, the memory usage view shows one node per project, clangd.projectConfig opens the active project's .clangd, and API v1 and the file status' "open output" command use the client of the active editor if it shows a C/C++ file, else the one of the clangd editor focused last. The context keys that enable the AST, memory usage, inlay hints and type hierarchy commands are owned by the shared part of each feature and true while any running client's server supports the feature; the manager's new onDidChangeClients event tells when to recompute them. Files outside all projects are not served; a command invoked on such a file says so. With the setting off, the behavior is unchanged, except that the context keys of the AST, memory usage and inlay hints commands, like the one of the type hierarchy before, are false while clangd is not running. Co-authored-by: Daan De Meyer <daan.j.demeyer@gmail.com>
With clangd.enablePerFolderServer, every clangd instance belongs to a workspace folder, but most of its settings were window-scoped and read without a scope, so folders couldn't configure their clangd differently. Move the settings that concern a single clangd instance to resource scope and read them in the context's workspace folder: arguments, fallbackFlags, trace, restartAfterCrash, onConfigChanged, onConfigChangedForceEnable, enableCodeCompletion, enableHover, serverCompletionRanking and inactiveRegions.*. The completion and hover middleware read them in the folder of the client that serves the document. Settings of the binary and of the extension as a whole (enable, checkUpdates, path, useScriptAsExecutable) and the deprecated semanticHighlighting stay as they are. In the default mode, the context has no folder, so the settings are read for no specific resource (config.get passes the scope null), which gives the values of before and logs no warning; VS Code ignores folder values of resource-scoped settings read that way. The description of clangd.enablePerFolderServer and docs/settings.md say so; the descriptions of the moved settings are unchanged. Co-authored-by: Daan De Meyer <daan.j.demeyer@gmail.com>
API v1 offers a single languageClient snapshot, which can't express the clients of the multi-project mode. Extensions need to look up the client serving a file, e.g. to send clangd's custom requests to it. getApi(2) returns ClangdApiV2, a superset of the version 2 proposed in clangd#810: - getLanguageClient(uri?) returns the client serving uri, or without uri the one of the active editor if it shows a C/C++ file, else of the clangd editor focused last; in the default mode, the single client. - getAllLanguageClients() returns all clients, and onDidChangeClients fires when a client is added or removed, or starts or stops. - startLanguageClient(uri) starts the client serving uri if needed and resolves to it once it runs, or to undefined if there is none or it fails to start. It waits for a start or restart of the session in progress. - multiProject tells whether clangd.enablePerFolderServer is in effect. All of them return only clients that are starting or running: requests to a starting client are delayed until it runs, and clients that have stopped, e.g. after crashing too often, are left out. getApi(1) is unchanged, and unknown versions still throw. The declarations now refer to the vscode module, so @types/vscode becomes a peer dependency of the API package.
The projects can only be the workspace folders. Build-system extensions need to contribute projects that are sub-folders, e.g. sibling build directories in one workspace folder, each with its own arguments. API v2 gains: - projects, getProject(uri), activeProject, onDidChangeActiveProject and onDidChangeProjects, which describe the projects; they are empty in the default mode, and kept while clangd restarts or is shut down, so a global restart reports no changes. - registerProjectProvider(id, provider), which adds a source of projects. While any provider is registered, the ProjectSet takes the union of the results of all providers, normalized as before (outermost wins, non-file roots and duplicates are dropped), instead of the workspace folders. A union doesn't depend on the order in which providers activate. Registering and unregistering a provider, and its onDidChangeProjects, re-query the providers and apply the difference: a provider registered after the workspace-folder clients started replaces them once, and an updated project restarts only its client. Providers are kept but not queried in the default mode. A provider whose onDidChangeProjects throws is not registered. - ClangdProject.arguments, passed to the project's clangd after clangd.arguments. ClangdProject and ClangdProjectsChangeEvent move from project.ts to the API declarations. Querying the providers is asynchronous and not part of the session's startup, so a slow provider never blocks restart or shutdown. If a provider fails, doesn't respond within 10 seconds or returns something other than a list of projects, its projects of the last successful query are kept (none if there was none), which is logged: a transient failure, e.g. during a build-system reconfigure, doesn't tear down its clients. An answer after the 10 seconds is still applied, unless the provider was queried again meanwhile: the provider has no reason to fire onDidChangeProjects again. A query is cancelled when a newer one starts or the session is torn down. startLanguageClient() waits for the projects to be up to date. Providers are never waited for during activation: they can only register once the extension has returned its API.
planger
force-pushed
the
upstream-multi-project
branch
from
November 9, 2026 13:40
6374428 to
abd146a
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
So far vscode-clangd runs a single clangd for all workspace folders. In workspaces with several independent projects, this mixes their indexes. For instance, go to definition and find references jump into the wrong project if several projects define a function with the same signature, and a folder can't use its own clangd arguments (see #498 and clangd/clangd#1400).
With this change, the new setting
clangd.enablePerFolderServer(off by default) runs a separate clangd for each project. By default, every workspace folder is a project, and its clangd starts once one of its files is shown in an editor. The settings that concern a single clangd instance (clangd.arguments,clangd.fallbackFlags, etc.) become resource-scoped, so each folder can configure its own. Commands, views and the server commandsclangd.applyFixandclangd.applyTweakare registered only once and go to the client that serves the document. Also,getApi(2)gives extensions access to the clients and lets build system extensions contribute their own projects, e.g. one per build directory. With the setting off, the extension behaves as before, with a few small differences:languageClientis undefined while clangd is stopped (e.g. afterclangd.shutdown), where it used to be the stopped client.workspace/executeCommandrequests (e.g.applyTweak) are shown to the user, as intended. So far, swapped parameters inhandleFailedRequestprevented that, which a separate commit fixes.The commits can be reviewed one by one. The setting name, the resource-scoped settings and API v2 follow #810 by @daandemeyer, and the corresponding commits list Daan as co-author. Unlike #810,
clangd.enable,clangd.checkUpdatesandclangd.semanticHighlightingstay window-scoped, and clangd is located (and installed or updated) once for all projects, so all projects use the same clangd binary.To test it:
compile_commands.jsonand a function with the same signature."clangd.enablePerFolderServer": trueto the workspace settings.clangd (<folder name>).clangd.argumentsin the.vscode/settings.jsonof one folder and observe that only the clangd of that folder gets them.Note that
npm testdidn't run any test so far:test/index.tslooked for*.test.tsinout/and didn't wait for mocha to finish. The first commit fixes that, together with the twoInactiveRegionsFeaturetests that have been failing unnoticed since. I'm happy to move it to a separate PR.The same functionality is also available in our fork (eclipse-cdt-cloud#11). However, we'd much prefer to have it upstream instead of maintaining a fork. WDYT?