-
Notifications
You must be signed in to change notification settings - Fork 4.3k
Centralize Language Server workspace folder tracking #85105
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| // Licensed to the .NET Foundation under one or more agreements. | ||
| // The .NET Foundation licenses this file to you under the MIT license. | ||
| // See the LICENSE file in the project root for more information. | ||
|
|
||
| using Microsoft.CodeAnalysis.LanguageServer.FileBasedPrograms; | ||
| using Microsoft.CodeAnalysis.LanguageServer.Handler; | ||
| using Microsoft.CodeAnalysis.Test.Utilities; | ||
| using Roslyn.LanguageServer.Protocol; | ||
|
|
||
| namespace Microsoft.CodeAnalysis.LanguageServer.UnitTests.FileBasedPrograms; | ||
|
|
||
| public sealed class CsprojInConeCheckerTests : IDisposable | ||
| { | ||
| private readonly TempRoot _tempRoot = new(); | ||
|
|
||
| public void Dispose() | ||
| => _tempRoot.Dispose(); | ||
|
|
||
| [Fact] | ||
| public void UsesCurrentWorkspaceFolders() | ||
| { | ||
| var initialWorkspace = _tempRoot.CreateDirectory(); | ||
| var projectWorkspace = _tempRoot.CreateDirectory(); | ||
| projectWorkspace.CreateFile("Project.csproj"); | ||
| var sourceFile = projectWorkspace.CreateDirectory("src").CreateFile("Program.cs"); | ||
| var initialFolder = CreateWorkspaceFolder(initialWorkspace.Path); | ||
| var projectFolder = CreateWorkspaceFolder(projectWorkspace.Path); | ||
| var tracker = new WorkspaceFolderTracker(); | ||
| tracker.Update([initialFolder], removedFolders: null); | ||
| var checker = new CsprojInConeChecker(tracker); | ||
|
|
||
| Assert.False(checker.IsContainedInCsprojCone(sourceFile.Path)); | ||
|
|
||
| tracker.Update([projectFolder], [initialFolder]); | ||
|
|
||
| Assert.True(checker.IsContainedInCsprojCone(sourceFile.Path)); | ||
| } | ||
|
|
||
| private static WorkspaceFolder CreateWorkspaceFolder(string path) | ||
| => new() | ||
| { | ||
| DocumentUri = ProtocolConversions.CreateAbsoluteDocumentUri(path), | ||
| Name = Path.GetFileName(path), | ||
| }; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,4 @@ | ||
| // Licensed to the .NET Foundation under one or more agreements. | ||
| // Licensed to the .NET Foundation under one or more agreements. | ||
| // The .NET Foundation licenses this file to you under the MIT license. | ||
| // See the LICENSE file in the project root for more information. | ||
|
|
||
|
|
@@ -20,6 +20,7 @@ | |
| using Microsoft.CodeAnalysis.PooledObjects; | ||
| using Microsoft.CodeAnalysis.Shared.TestHooks; | ||
| using Microsoft.CodeAnalysis.Shared.Utilities; | ||
| using Microsoft.CodeAnalysis.Threading; | ||
| using Microsoft.Extensions.Logging; | ||
| using Roslyn.LanguageServer.Protocol; | ||
| using Roslyn.Utilities; | ||
|
|
@@ -38,7 +39,8 @@ public ILspService CreateILspService(LspServices lspServices, WellKnownLspServer | |
| globalOptionService, | ||
| listenerProvider.GetListener(FeatureAttribute.Workspace), | ||
| lspServices.GetRequiredService<IHostWorkspaceProvider>().Workspace.Services.GetRequiredService<IFileBasedProgramService>(), | ||
| lspServices.GetRequiredService<ILoggerFactory>(), | ||
| lspServices.GetRequiredService<ILoggerFactory>().CreateLogger<FileBasedProgramsEntryPointDiscovery>(), | ||
| lspServices.GetRequiredService<IWorkspaceFolderTracker>(), | ||
| lspServices); | ||
| } | ||
| } | ||
|
|
@@ -47,8 +49,9 @@ internal sealed partial class FileBasedProgramsEntryPointDiscovery( | |
| IGlobalOptionService globalOptionService, | ||
| IAsynchronousOperationListener listener, | ||
| IFileBasedProgramService fileBasedProgramService, | ||
| ILoggerFactory loggerFactory, | ||
| LspServices lspServices) : ILspService, IOnInitialized | ||
| ILogger logger, | ||
| IWorkspaceFolderTracker workspaceFolderTracker, | ||
| LspServices lspServices) : ILspService, IOnInitialized, IDisposable | ||
| { | ||
| private static readonly StringComparer s_pathComparer = StringComparer.OrdinalIgnoreCase; | ||
|
|
||
|
|
@@ -61,48 +64,56 @@ internal sealed partial class FileBasedProgramsEntryPointDiscovery( | |
| "node_modules" | ||
| ], StringComparison.OrdinalIgnoreCase); | ||
|
|
||
| private readonly ILogger _logger = loggerFactory.CreateLogger<FileBasedProgramsEntryPointDiscovery>(); | ||
| private ImmutableArray<string> _workspaceFolders; | ||
| private readonly AsyncBatchingWorkQueue _discoveryQueue = new( | ||
| TimeSpan.Zero, | ||
| cancellationToken => FindAndLoadEntryPointsAsync(globalOptionService, fileBasedProgramService, workspaceFolderTracker, lspServices, logger, cancellationToken), | ||
| listener); | ||
|
|
||
| public Task OnInitializedAsync(ClientCapabilities clientCapabilities, RequestContext context, CancellationToken cancellationToken) | ||
| { | ||
| var initializeManager = context.GetRequiredService<IInitializeManager>(); | ||
| _workspaceFolders = initializeManager.GetRequiredWorkspaceFolderPaths(); | ||
| Task.Run(async () => | ||
| { | ||
| try | ||
| { | ||
| using var token = listener.BeginAsyncOperation(nameof(FindAndLoadEntryPointsAsync)); | ||
| await FindAndLoadEntryPointsAsync(); | ||
| } | ||
| catch (Exception ex) when (FatalError.ReportAndCatch(ex)) | ||
| { | ||
| throw ExceptionUtilities.Unreachable(); | ||
| } | ||
| }, cancellationToken); | ||
| workspaceFolderTracker.WorkspaceFoldersChanged += OnWorkspaceFoldersChanged; | ||
| _discoveryQueue.AddWork(); | ||
|
|
||
| return Task.CompletedTask; | ||
| } | ||
|
|
||
| internal async Task FindAndLoadEntryPointsAsync() | ||
| private void OnWorkspaceFoldersChanged(ImmutableHashSet<string> _) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? |
||
| => _discoveryQueue.AddWork(); | ||
|
|
||
| public void Dispose() | ||
| { | ||
| workspaceFolderTracker.WorkspaceFoldersChanged -= OnWorkspaceFoldersChanged; | ||
| _discoveryQueue.Dispose(); | ||
| } | ||
|
|
||
| internal ValueTask FindAndLoadEntryPointsAsync(CancellationToken cancellationToken) | ||
| => FindAndLoadEntryPointsAsync(globalOptionService, fileBasedProgramService, workspaceFolderTracker, lspServices, logger, cancellationToken); | ||
|
|
||
| private static async ValueTask FindAndLoadEntryPointsAsync( | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Wasn't sure why this got made static.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
| IGlobalOptionService globalOptionService, | ||
| IFileBasedProgramService fileBasedProgramService, | ||
| IWorkspaceFolderTracker workspaceFolderTracker, | ||
| LspServices lspServices, | ||
| ILogger logger, | ||
| CancellationToken cancellationToken) | ||
| { | ||
| Contract.ThrowIfTrue(_workspaceFolders.IsDefault, $"{nameof(OnInitializedAsync)} must be called before {nameof(FindAndLoadEntryPointsAsync)}."); | ||
| var workspaceFolders = workspaceFolderTracker.GetRequiredWorkspaceFolderPaths(); | ||
|
|
||
| if (_workspaceFolders.IsEmpty) | ||
| if (workspaceFolders.IsEmpty) | ||
| { | ||
| _logger.LogTrace("No workspace folders to search for file-based apps."); | ||
| logger.LogTrace("No workspace folders to search for file-based apps."); | ||
| return; | ||
| } | ||
|
|
||
| if (!globalOptionService.GetOption(LanguageServerProjectSystemOptionsStorage.EnableFileBasedPrograms)) | ||
| { | ||
| _logger.LogTrace(@"""dotnet.projects.enableFileBasedPrograms"" is false. Not discovering entry points."); | ||
| logger.LogTrace(@"""dotnet.projects.enableFileBasedPrograms"" is false. Not discovering entry points."); | ||
| return; | ||
| } | ||
|
|
||
| if (!globalOptionService.GetOption(FileBasedAppsOptionsStorage.EnableAutomaticDiscovery)) | ||
| { | ||
| _logger.LogTrace(@"""dotnet.fileBasedApps.enableAutomaticDiscovery"" is false. Not discovering entry points."); | ||
| logger.LogTrace(@"""dotnet.fileBasedApps.enableAutomaticDiscovery"" is false. Not discovering entry points."); | ||
| return; | ||
| } | ||
|
|
||
|
|
@@ -111,10 +122,13 @@ internal async Task FindAndLoadEntryPointsAsync() | |
|
|
||
| // Note: the overwhelmingly common case is when there is just one workspace folder. | ||
| // For simplicity we orient our search around one workspace folder at a time. | ||
| foreach (var workspaceFolder in _workspaceFolders) | ||
| foreach (var workspaceFolder in workspaceFolders) | ||
| { | ||
| foreach (var fileBasedAppPath in FindEntryPoints(workspaceFolder)) | ||
| cancellationToken.ThrowIfCancellationRequested(); | ||
|
|
||
| foreach (var fileBasedAppPath in FindEntryPoints(workspaceFolder, fileBasedProgramService, logger)) | ||
| { | ||
| cancellationToken.ThrowIfCancellationRequested(); | ||
| await fileBasedProgramsProjectSystem.TryBeginLoadingFileBasedAppAsync(fileBasedAppPath); | ||
| } | ||
| } | ||
|
|
@@ -146,6 +160,9 @@ protected override bool ShouldIncludeEntry(ref FileSystemEntry entry) | |
| } | ||
|
|
||
| internal ImmutableArray<string> FindEntryPoints(string workspaceFolder) | ||
| => FindEntryPoints(workspaceFolder, fileBasedProgramService, logger); | ||
|
|
||
| private static ImmutableArray<string> FindEntryPoints(string workspaceFolder, IFileBasedProgramService fileBasedProgramService, ILogger logger) | ||
| { | ||
| var stopwatch = SharedStopwatch.StartNew(); | ||
| var cacheDirectory = fileBasedProgramService.GetDiscoveryCacheDirectory(workspaceFolder); | ||
|
|
@@ -170,7 +187,7 @@ internal ImmutableArray<string> FindEntryPoints(string workspaceFolder) | |
| } | ||
| catch (Exception ex) | ||
| { | ||
| _logger.LogDebug("Could not read cache file: {ex.Message}", ex.Message); | ||
| logger.LogDebug("Could not read cache file: {ex.Message}", ex.Message); | ||
| } | ||
|
|
||
| cache ??= new Cache(workspaceFolder, DateTimeOffset.MinValue, FileBasedAppFullPaths: [], DirectoriesContainingCsproj: []); | ||
|
|
@@ -193,10 +210,10 @@ internal ImmutableArray<string> FindEntryPoints(string workspaceFolder) | |
|
|
||
| var newFileBasedAppsBuilder = ArrayBuilder<string>.GetInstance(cache.FileBasedAppFullPaths.Length); | ||
| var directoriesContainingCsprojBuilder = ArrayBuilder<string>.GetInstance(cache.DirectoriesContainingCsproj.Length); | ||
| var visitor = new WorkspaceFolderVisitor(cache, newFileBasedAppsBuilder, directoriesContainingCsprojBuilder, _logger); | ||
| var visitor = new WorkspaceFolderVisitor(cache, newFileBasedAppsBuilder, directoriesContainingCsprojBuilder, logger); | ||
| visitor.Visit(); | ||
| var elapsedMilliseconds = Math.Round(stopwatch.Elapsed.TotalMilliseconds); | ||
| _logger.LogInformation("Finished discovery in '{workspaceFolder}' in {elapsedMilliseconds} milliseconds", workspaceFolder, elapsedMilliseconds); | ||
| logger.LogInformation("Finished discovery in '{workspaceFolder}' in {elapsedMilliseconds} milliseconds", workspaceFolder, elapsedMilliseconds); | ||
|
|
||
| // Ensure items go into the cache file in a stable order. | ||
| // This is useful for manual inspection and allows use of 'BinarySearch' to match directories against the cache. | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.