Skip to content

Centralize Language Server workspace folder tracking - #85105

Merged
JoeRobich merged 1 commit into
mainfrom
dev/jorobich/lsp-workspace-folder-tracker
Sep 8, 2026
Merged

Centralize Language Server workspace folder tracking#85105
JoeRobich merged 1 commit into
mainfrom
dev/jorobich/lsp-workspace-folder-tracker

Conversation

@JoeRobich

@JoeRobich JoeRobich commented Aug 31, 2026

Copy link
Copy Markdown
Member

Summary

Introduce a synchronized Language Server service as the authoritative source of current local workspace folders.

  • Add initial folders directly from the LSP initialize handler.
  • Remove and add folders for workspace/didChangeWorkspaceFolders notifications.
  • Normalize paths and compare them with platform path semantics, and ignore non-file workspace-folder URIs.
  • Migrate existing file-based-program consumers away from IInitializeManager folder state.

Validation

  • WorkspaceFolderTrackerTests (2 passed)
  • Analyzer-enabled Language Server unit-test project build succeeded with 0 errors
  • Final stacked analyzer-enabled Language Server build succeeded with 0 errors
Microsoft Reviewers: Open in CodeFlow

@JoeRobich
JoeRobich requested a review from a team as a code owner August 31, 2026 16:10
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
2 pipeline(s) were filtered out due to trigger conditions.
There may be pipelines that require an authorized user to comment /azp run to run.

@JoeRobich
JoeRobich force-pushed the dev/jorobich/lsp-workspace-folder-tracker branch from 6e9de4b to 9145fff Compare August 31, 2026 16:38
@JoeRobich
JoeRobich force-pushed the dev/jorobich/lsp-workspace-folder-tracker branch from 9145fff to b6fb32a Compare August 31, 2026 16:53
@JoeRobich
JoeRobich force-pushed the dev/jorobich/lsp-workspace-folder-tracker branch from b6fb32a to c9ec94a Compare August 31, 2026 17:06
Comment thread src/LanguageServer/Protocol/Handler/InitializeManager.cs Outdated
Comment thread src/LanguageServer/Protocol/Handler/IWorkspaceFolderTracker.cs Outdated
Comment thread src/LanguageServer/ProtocolUnitTests/WorkspaceFolderTrackerTests.cs Outdated
@JoeRobich
JoeRobich force-pushed the dev/jorobich/lsp-workspace-folder-tracker branch from c9ec94a to 9523c9d Compare September 1, 2026 22:36
@JoeRobich
JoeRobich changed the base branch from dev/jorobich/lsp-case-insensitive-paths to main September 1, 2026 22:47
Copilot AI lite review requested due to automatic review settings September 2, 2026 05:24
@JoeRobich
JoeRobich force-pushed the dev/jorobich/lsp-workspace-folder-tracker branch from 9523c9d to d613ce0 Compare September 2, 2026 05:24

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

🟡 Changes recommended

Workspace folder path normalization currently doesn’t handle trailing directory separators, which can lead to duplicate/unremovable entries with PathUtilities.Comparer.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review tier: Lite
Findings: 1 Medium severity · 1 Low severity

New issues introduced by this change (2)
Severity Finding
Medium severity src/​LanguageServer/​Protocol/​Handler/​WorkspaceFolderTracker.cs — NormalizePath uses Path.GetFullPath but does not normalize trailing directory separators. Since…
Low severity src/​LanguageServer/​ProtocolUnitTests/​WorkspaceFolderTrackerTests.cs — Add a regression test that covers workspace-folder URIs that differ only by a trailing directory…
What changed in this PR

This PR introduces a new Language Server service (IWorkspaceFolderTracker) to centralize tracking of local workspace-folder paths, initializes it during initialize, and updates it on workspace/didChangeWorkspaceFolders notifications. It also migrates existing file-based-program components away from IInitializeManager’s previous folder-state.

Changes:

  • Add IWorkspaceFolderTracker/WorkspaceFolderTracker and register it as a server service.
  • Populate initial workspace folders during initialize and update them during workspace/didChangeWorkspaceFolders.
  • Remove workspace-folder tracking from IInitializeManager and update file-based-program consumers to use the new tracker.
File Description
src/​LanguageServer/​ProtocolUnitTests/​WorkspaceFolderTrackerTests.cs Adds unit tests for tracker behavior (path semantics, non-file URIs).
src/​LanguageServer/​Protocol/​RoslynLanguageServer.cs Registers IWorkspaceFolderTracker as a base service.
src/​LanguageServer/​Protocol/​Handler/​WorkspaceFolderTracker.cs Implements synchronized add/remove/query of normalized workspace-folder paths.
src/​LanguageServer/​Protocol/​Handler/​ServerLifetime/​InitializeHandler.cs Seeds the tracker with InitializeParams.WorkspaceFolders.
src/​LanguageServer/​Protocol/​Handler/​ServerLifetime/​DidChangeWorkspaceFoldersNotificationHandler.cs Handles workspace-folder change notifications by removing/adding entries in the tracker.
src/​LanguageServer/​Protocol/​Handler/​IWorkspaceFolderTracker.cs Defines the tracker service contract used across handlers/components.
src/​LanguageServer/​Protocol/​Handler/​InitializeManager.cs Removes workspace-folder state; keeps only initialize params/capabilities.
src/​LanguageServer/​Protocol/​Handler/​IInitializeManager.cs Drops the workspace-folder API from the initialize manager interface.
src/​LanguageServer/​Microsoft.CodeAnalysis.LanguageServer/​FileBasedPrograms/​FileBasedProgramsEntryPointDiscovery.cs Switches to reading workspace folders from IWorkspaceFolderTracker.
src/​LanguageServer/​Microsoft.CodeAnalysis.LanguageServer/​FileBasedPrograms/​CsprojInConeChecker.cs Switches to reading workspace folders from IWorkspaceFolderTracker.
src/​LanguageServer/​Microsoft.CodeAnalysis.LanguageServer.UnitTests/​Daemon/​ClientProcessMonitorTests.cs Mechanical file header/BOM update only.

Comment thread src/LanguageServer/Protocol/Handler/WorkspaceFolderTracker.cs
Comment thread src/LanguageServer/ProtocolUnitTests/WorkspaceFolderTrackerTests.cs Outdated
Copilot AI review requested due to automatic review settings September 2, 2026 20:25
@JoeRobich
JoeRobich force-pushed the dev/jorobich/lsp-workspace-folder-tracker branch from d613ce0 to a244fa3 Compare September 2, 2026 20:25

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

🔵 Needs a closer look

Workspace folder “normalization” currently preserves trailing directory separators, which can lead to duplicate entries and failed removals unless canonicalized (and ideally covered by a regression test).

Review tier: Lite
Findings: 1 Medium severity · 1 Low severity

Pre-existing issues (2)
Severity Finding
Medium severity src/​LanguageServer/​Protocol/​Handler/​WorkspaceFolderTracker.cs — NormalizePath uses Path.GetFullPath but does not normalize trailing directory separators. Since… View comment
Low severity src/​LanguageServer/​ProtocolUnitTests/​WorkspaceFolderTrackerTests.cs — Add a regression test that covers workspace-folder URIs that differ only by a trailing directory… View comment
Suppressed comments (2)

src/LanguageServer/Protocol/Handler/WorkspaceFolderTracker.cs:81

  • NormalizePath only calls Path.GetFullPath, which preserves trailing directory separators. That means the tracker can treat the same workspace folder as distinct entries (e.g. "C:\Repo" vs "C:\Repo\" or "/repo" vs "/repo/") and Remove may fail to match if the client switches representations. Since this type is intended to normalize/compare workspace folders with platform path semantics, it should canonicalize directory paths by trimming trailing separators (except for the root).
    private static string NormalizePath(string path)
        => Path.GetFullPath(path);

src/LanguageServer/ProtocolUnitTests/WorkspaceFolderTrackerTests.cs:67

  • The current tests cover case-sensitivity differences and non-file URIs, but they don’t cover normalization of trailing directory separators. Without trimming trailing separators, the tracker can retain duplicate entries for the same folder or fail to remove a folder when the client changes URI formatting (with/without a trailing '/'). Adding a regression test here would lock in the intended normalization behavior.
    private static WorkspaceFolder CreateWorkspaceFolder(DocumentUri uri)
        => new() { DocumentUri = uri, Name = "Workspace" };
}

Copilot AI review requested due to automatic review settings September 2, 2026 22:27
@JoeRobich
JoeRobich force-pushed the dev/jorobich/lsp-workspace-folder-tracker branch from a244fa3 to 24dc278 Compare September 2, 2026 22:27

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

🟡 Changes recommended

The new workspace-folder notification handler has an incorrect MutatesSolutionState setting (unnecessarily serializing work) and needs additional wiring-level coverage to prevent composition/dispatch regressions.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review tier: Lite
Findings: 2 Medium severity · 2 Low severity

New issues introduced by this change (2)
Severity Finding
Medium severity src/​LanguageServer/​Protocol/​Handler/​ServerLifetime/​DidChangeWorkspaceFoldersNotificationHandler.cs — MutatesSolutionState is set to true, but this handler only updates the workspace-folder tracker and…
Low severity src/​LanguageServer/​Protocol/​Handler/​ServerLifetime/​DidChangeWorkspaceFoldersNotificationHandler.cs — This new notification handler introduces observable behavior (updating the authoritative…
Pre-existing issues (2)
Severity Finding
Medium severity src/​LanguageServer/​Protocol/​Handler/​WorkspaceFolderTracker.cs — NormalizePath uses Path.GetFullPath but does not normalize trailing directory separators. Since… View comment
Low severity src/​LanguageServer/​ProtocolUnitTests/​WorkspaceFolderTrackerTests.cs — Add a regression test that covers workspace-folder URIs that differ only by a trailing directory… View comment

Copilot AI review requested due to automatic review settings September 2, 2026 23:11
@JoeRobich
JoeRobich force-pushed the dev/jorobich/lsp-workspace-folder-tracker branch from 24dc278 to 844b90b Compare September 2, 2026 23:11

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

🟢 Approval recommended

The behavioral change is well-scoped, includes targeted tests, and only has minor follow-up nits noted in review comments.

Review tier: Lite
Findings: 1 Medium severity · 4 Low severity

New issues introduced by this change (2)
Severity Finding
Low severity src/​LanguageServer/​Protocol/​Handler/​WorkspaceFolderTracker.csusing System; is unnecessary here and will trigger unused-using analyzers (the file doesn’t…
Low severity src/​LanguageServer/​Protocol/​Handler/​WorkspaceFolderTracker.csRemove allocates a builder and re-materializes the immutable array even when the input is…
Pre-existing issues (3)
Severity Finding
Medium severity src/​LanguageServer/​Protocol/​Handler/​WorkspaceFolderTracker.cs — NormalizePath uses Path.GetFullPath but does not normalize trailing directory separators. Since… View comment
Low severity src/​LanguageServer/​Protocol/​Handler/​ServerLifetime/​DidChangeWorkspaceFoldersNotificationHandler.cs — This new notification handler introduces observable behavior (updating the authoritative… View comment
Low severity src/​LanguageServer/​ProtocolUnitTests/​WorkspaceFolderTrackerTests.cs — Add a regression test that covers workspace-folder URIs that differ only by a trailing directory… View comment
Issues resolved since last review (1)
Severity Finding
Medium severity src/​LanguageServer/​Protocol/​Handler/​ServerLifetime/​DidChangeWorkspaceFoldersNotificationHandler.cs — MutatesSolutionState is set to true, but this handler only updates the workspace-folder tracker and… View resolved comment

Comment thread src/LanguageServer/Protocol/Handler/WorkspaceFolderTracker.cs
Comment thread src/LanguageServer/Protocol/Handler/WorkspaceFolderTracker.cs Outdated
@JoeRobich
JoeRobich force-pushed the dev/jorobich/lsp-workspace-folder-tracker branch from 844b90b to 11c5f2b Compare September 2, 2026 23:29
@JoeRobich
JoeRobich force-pushed the dev/jorobich/lsp-workspace-folder-tracker branch from 505e91b to c30997d Compare September 4, 2026 03:32
Copilot AI review requested due to automatic review settings September 4, 2026 05:14
@JoeRobich
JoeRobich force-pushed the dev/jorobich/lsp-workspace-folder-tracker branch from c30997d to 9cb43f6 Compare September 4, 2026 05:14

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

🟡 Changes recommended

A small but concrete test-utility robustness issue was found (missing null guard for the new response-factory delegate) that should be fixed before approval.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review tier: Lite
Findings: 1 High severity · 1 Medium severity

New issues introduced by this change (1)
Severity Finding
Medium severity src/​LanguageServer/​ProtocolUnitTests/​TestConfigurableDocumentHandler.csConfigureHandler accepts a responseFactory delegate but doesn’t guard against a null value. If…
Pre-existing issues (1)
Severity Finding
High severity src/​LanguageServer/​Protocol/​Handler/​WorkspaceFolderTracker.csPath.GetFullPath can throw (e.g., invalid/unsupported/too-long paths). Since workspace folders… View comment

Comment on lines +40 to 44
public void ConfigureHandler(bool mutatesSolutionState, bool requiresLspSolution, Func<RequestContext, CancellationToken, Task<TestConfigurableResponse>> responseFactory)
{
if (_mutatesSolutionState is not null || _requiresLSPSolution is not null || _response is not null)
if (_mutatesSolutionState is not null || _requiresLSPSolution is not null || _responseFactory is not null)
{
throw new InvalidOperationException($"{nameof(ConfigureHandler)} has already been called");
Copilot AI review requested due to automatic review settings September 4, 2026 06:13
@JoeRobich
JoeRobich force-pushed the dev/jorobich/lsp-workspace-folder-tracker branch from 9cb43f6 to 949d262 Compare September 4, 2026 06:13

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

🟡 Changes recommended

WorkspaceFolderTracker.Update can still raise WorkspaceFoldersChanged for net-no-op remove+add updates, which can trigger unnecessary downstream work and should be addressed before merging.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review tier: Lite
Findings: 1 High severity · 2 Medium severity

New issues introduced by this change (1)
Severity Finding
Medium severity src/​LanguageServer/​Protocol/​Handler/​WorkspaceFolderTracker.csUpdate uses ReferenceEquals to detect no-op updates, but a remove+add sequence that results in…
Pre-existing issues (2)
Severity Finding
High severity src/​LanguageServer/​Protocol/​Handler/​WorkspaceFolderTracker.csPath.GetFullPath can throw (e.g., invalid/unsupported/too-long paths). Since workspace folders… View comment
Medium severity src/​LanguageServer/​ProtocolUnitTests/​TestConfigurableDocumentHandler.csConfigureHandler accepts a responseFactory delegate but doesn’t guard against a null value. If… View comment

Comment thread src/LanguageServer/Protocol/Handler/WorkspaceFolderTracker.cs Outdated
Copilot AI review requested due to automatic review settings September 4, 2026 06:35

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

🔵 Needs a closer look

Workspace-folder path normalization can throw on malformed client-provided file URIs (via Path.GetFullPath), risking a language-server crash during initialize or folder-change handling.

Review tier: Lite
Findings: 1 High severity · 2 Medium severity

Pre-existing issues (3)
Severity Finding
High severity src/​LanguageServer/​Protocol/​Handler/​WorkspaceFolderTracker.csPath.GetFullPath can throw (e.g., invalid/unsupported/too-long paths). Since workspace folders… View comment
Medium severity src/​LanguageServer/​Protocol/​Handler/​WorkspaceFolderTracker.csUpdate uses ReferenceEquals to detect no-op updates, but a remove+add sequence that results in… View comment
Medium severity src/​LanguageServer/​ProtocolUnitTests/​TestConfigurableDocumentHandler.csConfigureHandler accepts a responseFactory delegate but doesn’t guard against a null value. If… View comment
Suppressed comments (1)

src/LanguageServer/Protocol/Handler/WorkspaceFolderTracker.cs:69

  • NormalizePath uses Path.GetFullPath, which can throw (e.g., malformed/empty file URI -> invalid path). Since workspace folders come from the client, a single bad workspace-folder URI could crash the language server during initialize or didChangeWorkspaceFolders. Consider treating path-normalization failures as non-fatal and ignoring that folder (return null) instead of throwing.
    private static string? GetNormalizedFilePath(WorkspaceFolder workspaceFolder)
        => workspaceFolder.DocumentUri.ParsedDocumentUri?.IsFile == true
            ? NormalizePath(workspaceFolder.DocumentUri.GetDocumentFilePathFromUri())
            : null;

Copilot AI review requested due to automatic review settings September 4, 2026 06:48
@JoeRobich
JoeRobich force-pushed the dev/jorobich/lsp-workspace-folder-tracker branch from f3d4ccb to 931c338 Compare September 4, 2026 06:49

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

🔵 Needs a closer look

It changes core LSP lifetime/workspace-folder state flow and introduces new cross-component update notifications that warrant maintainer validation in broader scenarios beyond the added tests.

Review tier: Lite
Findings: 1 High severity · 1 Medium severity

Pre-existing issues (2)
Severity Finding
High severity src/​LanguageServer/​Protocol/​Handler/​WorkspaceFolderTracker.csPath.GetFullPath can throw (e.g., invalid/unsupported/too-long paths). Since workspace folders… View comment
Medium severity src/​LanguageServer/​ProtocolUnitTests/​TestConfigurableDocumentHandler.csConfigureHandler accepts a responseFactory delegate but doesn’t guard against a null value. If… View comment
Issues resolved since last review (1)
Severity Finding
Medium severity src/​LanguageServer/​Protocol/​Handler/​WorkspaceFolderTracker.csUpdate uses ReferenceEquals to detect no-op updates, but a remove+add sequence that results in… View resolved comment

@jasonmalinowski jasonmalinowski left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Some small comments but could be done in a mop-up since everything else is stacked on this.


internal sealed class WorkspaceFolderTracker : IWorkspaceFolderTracker
{
// Mutations are serialized by the request queue, but non-mutating requests may read the current folders concurrently.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Nit: use an XML doc comment here to mention the other field, so less of a chance this gets out of sync.

Comment on lines +27 to +29
if (removedFolders is not null)
{
foreach (var workspaceFolder in removedFolders)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Sometimes doing in removedFolders ?? [] is a nice shortcut...

Comment on lines +60 to +61
lock (_gate)
{

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

No reason to have the lock, especially since the comment said it wasn't needed.

}

internal async Task FindAndLoadEntryPointsAsync()
private void OnWorkspaceFoldersChanged(ImmutableHashSet<string> _)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

It's a bit strange the event handler is giving us the set and we're ignoring it. That's fine since we'll look at the "final" output, but maybe we just don't need the argument at all?

internal ValueTask FindAndLoadEntryPointsAsync(CancellationToken cancellationToken)
=> FindAndLoadEntryPointsAsync(globalOptionService, fileBasedProgramService, workspaceFolderTracker, lspServices, logger, cancellationToken);

private static async ValueTask FindAndLoadEntryPointsAsync(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Wasn't sure why this got made static.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Other than to make the AsyncBatchingWorkQueue code duplicate the logic of the non-static overload.

@JoeRobich

Copy link
Copy Markdown
Member Author

/azp run roslyn-CI

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 1 pipeline(s).

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants