diff --git a/.vscode/tasks.json b/.vscode/tasks.json index ea68cf0efae7d..78745a3ada316 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -169,7 +169,7 @@ "type": "process", "options": { "env": { - "DOTNET_ROSLYN_SERVER_PATH": "${workspaceRoot}/artifacts/bin/Microsoft.CodeAnalysis.LanguageServer/Debug/net8.0/Microsoft.CodeAnalysis.LanguageServer.dll" + "DOTNET_ROSLYN_SERVER_PATH": "${workspaceRoot}/artifacts/bin/Microsoft.CodeAnalysis.LanguageServer/Debug/net9.0/Microsoft.CodeAnalysis.LanguageServer.dll" } }, "dependsOn": [ "build language server" ] diff --git a/docs/features/file-based-programs-vscode.md b/docs/features/file-based-programs-vscode.md new file mode 100644 index 0000000000000..dddaa13667d79 --- /dev/null +++ b/docs/features/file-based-programs-vscode.md @@ -0,0 +1,59 @@ +# File-based programs VS Code support + +See also [dotnet-run-file.md](https://github.com/dotnet/sdk/blob/main/documentation/general/dotnet-run-file.md). + +## Feature overview + +A file-based program embeds a subset of MSBuild project capabilities into C# code, allowing single files to stand alone as ordinary projects. + +The following is a file-based program: + +```cs +Console.WriteLine("Hello World!"); +``` + +So is the following: + +```cs +#!/usr/bin/env dotnet run +#:sdk Microsoft.Net.Sdk +#:package Newtonsoft.Json@13.0.3 +#:property LangVersion=preview + +using Newtonsoft.Json; + +Main(); + +void Main() +{ + if (args is not [_, var jsonPath, ..]) + { + Console.Error.WriteLine("Usage: app "); + return; + } + + var json = File.ReadAllText(jsonPath); + var data = JsonConvert.DeserializeObject(json); + // ... +} + +record Data(string field1, int field2); +``` + +This basically works by having the `dotnet` command line interpret the `#:` directives in source files, produce a C# project XML document in memory, and pass it off to MSBuild. The in-memory project is sometimes called a "virtual project". + +## Miscellaneous files changes + +There is a long-standing backlog item to enhance the experience of working with miscellaneous files ("loose files" not associated with any project). We think that as part of the "file-based program" work, we can enable the following in such files without substantial issues: +- Syntax diagnostics. +- Intellisense for the "default" set of references. e.g. those references which are included in the project created by `dotnet new console` with the current SDK. + +### Heuristic +The IDE considers a file to be a file-based program, if: +- It has any `#:` directives which configure the file-based program project, or, +- It has any top-level statements. +Any of the above is met, and, the file is not included in an ordinary `.csproj` project (i.e. it is not part of any ordinary project's list of `Compile` items). + +### Opt-out + +We added an opt-out flag with option name `dotnet.projects.enableFileBasedPrograms`. If issues arise with the file-based program experience, then VS Code users should set the corresponding setting `"dotnet.projects.enableFileBasedPrograms": false` to revert back to the old miscellaneous files experience. diff --git a/src/Features/Core/Portable/Workspace/MiscellaneousFileUtilities.cs b/src/Features/Core/Portable/Workspace/MiscellaneousFileUtilities.cs index f5457cf655138..d7784bcf900b7 100644 --- a/src/Features/Core/Portable/Workspace/MiscellaneousFileUtilities.cs +++ b/src/Features/Core/Portable/Workspace/MiscellaneousFileUtilities.cs @@ -46,6 +46,12 @@ internal static ProjectInfo CreateMiscellaneousProjectInfoForDocument( compilationOptions = GetCompilationOptionsWithScriptReferenceResolvers(services, compilationOptions, filePath); } + if (parseOptions != null && fileExtension != languageInformation.ScriptExtension) + { + // Any non-script misc file should not complain about usage of '#:' ignored directives. + parseOptions = parseOptions.WithFeatures([.. parseOptions.Features, new("FileBasedProgram", "true")]); + } + var projectId = ProjectId.CreateNewId(debugName: $"{workspace.GetType().Name} Files Project for {filePath}"); var documentId = DocumentId.CreateNewId(projectId, debugName: filePath); diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/TelemetryReporterTests.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/TelemetryReporterTests.cs index 0a678d0b4fd44..da5b1fadccbd4 100644 --- a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/TelemetryReporterTests.cs +++ b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/TelemetryReporterTests.cs @@ -14,13 +14,11 @@ public sealed class TelemetryReporterTests(ITestOutputHelper testOutputHelper) { private async Task CreateReporterAsync() { - var exportProvider = await LanguageServerTestComposition.CreateExportProviderAsync( + var (exportProvider, _) = await LanguageServerTestComposition.CreateExportProviderAsync( LoggerFactory, includeDevKitComponents: true, MefCacheDirectory.Path, - [], - out var _, - out var _); + []); // VS Telemetry requires this environment variable to be set. Environment.SetEnvironmentVariable("CommonPropertyBagPath", Path.GetTempFileName()); diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/Utilities/AbstractLanguageServerHostTests.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/Utilities/AbstractLanguageServerHostTests.cs index ae12838c614a2..02256570fb494 100644 --- a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/Utilities/AbstractLanguageServerHostTests.cs +++ b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/Utilities/AbstractLanguageServerHostTests.cs @@ -46,8 +46,8 @@ protected sealed class TestLspServer : ILspClient, IAsyncDisposable internal static async Task CreateAsync(ClientCapabilities clientCapabilities, ILoggerFactory loggerFactory, string cacheDirectory, bool includeDevKitComponents = true, string[]? extensionPaths = null) { - var exportProvider = await LanguageServerTestComposition.CreateExportProviderAsync( - loggerFactory, includeDevKitComponents, cacheDirectory, extensionPaths, out var _, out var assemblyLoader); + var (exportProvider, assemblyLoader) = await LanguageServerTestComposition.CreateExportProviderAsync( + loggerFactory, includeDevKitComponents, cacheDirectory, extensionPaths); var testLspServer = new TestLspServer(exportProvider, loggerFactory, assemblyLoader); var initializeResponse = await testLspServer.ExecuteRequestAsync(Methods.InitializeName, new InitializeParams { Capabilities = clientCapabilities }, CancellationToken.None); Assert.NotNull(initializeResponse?.Capabilities); diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/Utilities/LanguageServerTestComposition.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/Utilities/LanguageServerTestComposition.cs index 9215dfc08b82b..54e0171976633 100644 --- a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/Utilities/LanguageServerTestComposition.cs +++ b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/Utilities/LanguageServerTestComposition.cs @@ -10,16 +10,14 @@ namespace Microsoft.CodeAnalysis.LanguageServer.UnitTests; internal sealed class LanguageServerTestComposition { - public static Task CreateExportProviderAsync( + public static async Task<(ExportProvider exportProvider, IAssemblyLoader assemblyLoader)> CreateExportProviderAsync( ILoggerFactory loggerFactory, bool includeDevKitComponents, string cacheDirectory, - string[]? extensionPaths, - out ServerConfiguration serverConfiguration, - out IAssemblyLoader assemblyLoader) + string[]? extensionPaths) { var devKitDependencyPath = includeDevKitComponents ? TestPaths.GetDevKitExtensionPath() : null; - serverConfiguration = new ServerConfiguration(LaunchDebugger: false, + var serverConfiguration = new ServerConfiguration(LaunchDebugger: false, LogConfiguration: new LogConfiguration(LogLevel.Trace), StarredCompletionsPath: null, TelemetryLevel: null, @@ -32,8 +30,10 @@ public static Task CreateExportProviderAsync( ServerPipeName: null, UseStdIo: false); var extensionManager = ExtensionAssemblyManager.Create(serverConfiguration, loggerFactory); - assemblyLoader = new CustomExportAssemblyLoader(extensionManager, loggerFactory); + var assemblyLoader = new CustomExportAssemblyLoader(extensionManager, loggerFactory); - return LanguageServerExportProviderBuilder.CreateExportProviderAsync(extensionManager, assemblyLoader, devKitDependencyPath, cacheDirectory, loggerFactory, CancellationToken.None); + var exportProvider = await LanguageServerExportProviderBuilder.CreateExportProviderAsync(extensionManager, assemblyLoader, devKitDependencyPath, cacheDirectory, loggerFactory, CancellationToken.None); + exportProvider.GetExportedValue().InitializeConfiguration(serverConfiguration); + return (exportProvider, assemblyLoader); } } diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/WorkspaceProjectFactoryServiceTests.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/WorkspaceProjectFactoryServiceTests.cs index b5082c97af386..5b1a75909dc93 100644 --- a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/WorkspaceProjectFactoryServiceTests.cs +++ b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/WorkspaceProjectFactoryServiceTests.cs @@ -18,11 +18,10 @@ public sealed class WorkspaceProjectFactoryServiceTests(ITestOutputHelper testOu public async Task CreateProjectAndBatch() { var loggerFactory = new LoggerFactory(); - using var exportProvider = await LanguageServerTestComposition.CreateExportProviderAsync( - loggerFactory, includeDevKitComponents: false, MefCacheDirectory.Path, [], out var serverConfiguration, out var _); + var (exportProvider, _) = await LanguageServerTestComposition.CreateExportProviderAsync( + loggerFactory, includeDevKitComponents: false, MefCacheDirectory.Path, []); + using var _ = exportProvider; - exportProvider.GetExportedValue() - .InitializeConfiguration(serverConfiguration); await exportProvider.GetExportedValue().CreateAsync(); var workspaceFactory = exportProvider.GetExportedValue(); @@ -48,7 +47,7 @@ public async Task CreateProjectAndBatch() await batch.ApplyAsync(CancellationToken.None); // Verify it actually did something; we won't exclusively test each method since those are tested at lower layers - var project = workspaceFactory.Workspace.CurrentSolution.Projects.Single(); + var project = workspaceFactory.HostWorkspace.CurrentSolution.Projects.Single(); var document = Assert.Single(project.Documents); Assert.Equal(sourceFilePath, document.FilePath); diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/FileBasedProgramsProjectSystem.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/FileBasedProgramsProjectSystem.cs new file mode 100644 index 0000000000000..4b229d8497aee --- /dev/null +++ b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/FileBasedProgramsProjectSystem.cs @@ -0,0 +1,143 @@ +// 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 System.Collections.Immutable; +using System.Security; +using Microsoft.CodeAnalysis.Features.Workspaces; +using Microsoft.CodeAnalysis.Host; +using Microsoft.CodeAnalysis.LanguageServer.HostWorkspace.ProjectTelemetry; +using Microsoft.CodeAnalysis.MetadataAsSource; +using Microsoft.CodeAnalysis.MSBuild; +using Microsoft.CodeAnalysis.Options; +using Microsoft.CodeAnalysis.ProjectSystem; +using Microsoft.CodeAnalysis.Shared.Extensions; +using Microsoft.CodeAnalysis.Shared.TestHooks; +using Microsoft.CodeAnalysis.Text; +using Microsoft.CodeAnalysis.Workspaces.ProjectSystem; +using Microsoft.CommonLanguageServerProtocol.Framework; +using Microsoft.Extensions.Logging; +using Microsoft.VisualStudio.Composition; +using Roslyn.LanguageServer.Protocol; +using Roslyn.Utilities; +using static Microsoft.CodeAnalysis.MSBuild.BuildHostProcessManager; + +namespace Microsoft.CodeAnalysis.LanguageServer.HostWorkspace; + +/// Handles loading both miscellaneous files and file-based program projects. +internal sealed class FileBasedProgramsProjectSystem : LanguageServerProjectLoader, ILspMiscellaneousFilesWorkspaceProvider +{ + private readonly ILspServices _lspServices; + private readonly ILogger _logger; + private readonly IMetadataAsSourceFileService _metadataAsSourceFileService; + + public FileBasedProgramsProjectSystem( + ILspServices lspServices, + IMetadataAsSourceFileService metadataAsSourceFileService, + LanguageServerWorkspaceFactory workspaceFactory, + IFileChangeWatcher fileChangeWatcher, + IGlobalOptionService globalOptionService, + ILoggerFactory loggerFactory, + IAsynchronousOperationListenerProvider listenerProvider, + ProjectLoadTelemetryReporter projectLoadTelemetry, + ServerConfigurationFactory serverConfigurationFactory, + BinlogNamer binlogNamer) + : base( + workspaceFactory.FileBasedProgramsProjectFactory, + workspaceFactory.TargetFrameworkManager, + workspaceFactory.ProjectSystemHostInfo, + fileChangeWatcher, + globalOptionService, + loggerFactory, + listenerProvider, + projectLoadTelemetry, + serverConfigurationFactory, + binlogNamer) + { + _lspServices = lspServices; + _logger = loggerFactory.CreateLogger(); + _metadataAsSourceFileService = metadataAsSourceFileService; + } + + public Workspace Workspace => ProjectFactory.Workspace; + + private string GetDocumentFilePath(DocumentUri uri) => uri.ParsedUri is { } parsedUri ? ProtocolConversions.GetDocumentFilePathFromUri(parsedUri) : uri.UriString; + + public async ValueTask AddMiscellaneousDocumentAsync(DocumentUri uri, SourceText documentText, string languageId, ILspLogger logger) + { + var documentFilePath = GetDocumentFilePath(uri); + + // https://github.com/dotnet/roslyn/issues/78421: MetadataAsSource should be its own workspace + if (_metadataAsSourceFileService.TryAddDocumentToWorkspace(documentFilePath, documentText.Container, out var documentId)) + { + var metadataWorkspace = _metadataAsSourceFileService.TryGetWorkspace(); + Contract.ThrowIfNull(metadataWorkspace); + return metadataWorkspace.CurrentSolution.GetRequiredDocument(documentId); + } + + var primordialDoc = AddPrimordialDocument(uri, documentText, languageId); + Contract.ThrowIfNull(primordialDoc.FilePath); + + var doDesignTimeBuild = uri.ParsedUri?.IsFile is true + && primordialDoc.Project.Language == LanguageNames.CSharp + && GlobalOptionService.GetOption(LanguageServerProjectSystemOptionsStorage.EnableFileBasedPrograms); + await BeginLoadingProjectWithPrimordialAsync(primordialDoc.FilePath, primordialProjectId: primordialDoc.Project.Id, doDesignTimeBuild); + + return primordialDoc; + + TextDocument AddPrimordialDocument(DocumentUri uri, SourceText documentText, string languageId) + { + var languageInfoProvider = _lspServices.GetRequiredService(); + if (!languageInfoProvider.TryGetLanguageInformation(uri, languageId, out var languageInformation)) + { + Contract.Fail($"Could not find language information for {uri} with absolute path {documentFilePath}"); + } + + var workspace = Workspace; + var sourceTextLoader = new SourceTextLoader(documentText, documentFilePath); + var projectInfo = MiscellaneousFileUtilities.CreateMiscellaneousProjectInfoForDocument( + workspace, documentFilePath, sourceTextLoader, languageInformation, documentText.ChecksumAlgorithm, workspace.Services.SolutionServices, []); + + ProjectFactory.ApplyChangeToWorkspace(workspace => workspace.OnProjectAdded(projectInfo)); + + // https://github.com/dotnet/roslyn/pull/78267 + // Work around an issue where opening a Razor file in the misc workspace causes a crash. + if (languageInformation.LanguageName == LanguageInfoProvider.RazorLanguageName) + { + var docId = projectInfo.AdditionalDocuments.Single().Id; + return workspace.CurrentSolution.GetRequiredAdditionalDocument(docId); + } + + var id = projectInfo.Documents.Single().Id; + return workspace.CurrentSolution.GetRequiredDocument(id); + } + } + + public async ValueTask TryRemoveMiscellaneousDocumentAsync(DocumentUri uri, bool removeFromMetadataWorkspace) + { + var documentPath = GetDocumentFilePath(uri); + if (removeFromMetadataWorkspace && _metadataAsSourceFileService.TryRemoveDocumentFromWorkspace(documentPath)) + { + return; + } + + await UnloadProjectAsync(documentPath); + } + + protected override async Task<(RemoteProjectFile projectFile, bool hasAllInformation, BuildHostProcessKind preferred, BuildHostProcessKind actual)?> TryLoadProjectInMSBuildHostAsync( + BuildHostProcessManager buildHostProcessManager, string documentPath, CancellationToken cancellationToken) + { + const BuildHostProcessKind buildHostKind = BuildHostProcessKind.NetCore; + var buildHost = await buildHostProcessManager.GetBuildHostAsync(buildHostKind, cancellationToken); + + var loader = ProjectFactory.CreateFileTextLoader(documentPath); + var textAndVersion = await loader.LoadTextAsync(new LoadTextOptions(SourceHashAlgorithms.Default), cancellationToken); + var (virtualProjectContent, isFileBasedProgram) = VirtualCSharpFileBasedProgramProject.MakeVirtualProjectContent(documentPath, textAndVersion.Text); + + // When loading a virtual project, the path to the on-disk source file is not used. Instead the path is adjusted to end with .csproj. + // This is necessary in order to get msbuild to apply the standard c# props/targets to the project. + var virtualProjectPath = VirtualCSharpFileBasedProgramProject.GetVirtualProjectPath(documentPath); + var loadedFile = await buildHost.LoadProjectAsync(virtualProjectPath, virtualProjectContent, languageName: LanguageNames.CSharp, cancellationToken); + return (loadedFile, hasAllInformation: isFileBasedProgram, preferred: buildHostKind, actual: buildHostKind); + } +} diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/FileBasedProgramsWorkspaceProviderFactory.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/FileBasedProgramsWorkspaceProviderFactory.cs new file mode 100644 index 0000000000000..d7c63e9f2aed0 --- /dev/null +++ b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/FileBasedProgramsWorkspaceProviderFactory.cs @@ -0,0 +1,42 @@ +// 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 System.Composition; +using Microsoft.CodeAnalysis.Host; +using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.CodeAnalysis.LanguageServer.Handler; +using Microsoft.CodeAnalysis.LanguageServer.HostWorkspace.ProjectTelemetry; +using Microsoft.CodeAnalysis.MetadataAsSource; +using Microsoft.CodeAnalysis.Options; +using Microsoft.CodeAnalysis.ProjectSystem; +using Microsoft.CodeAnalysis.Shared.TestHooks; +using Microsoft.CommonLanguageServerProtocol.Framework; +using Microsoft.Extensions.Logging; + +namespace Microsoft.CodeAnalysis.LanguageServer.HostWorkspace; + +/// +/// Service to create instances. +/// This is not exported as a as it requires +/// special base language server dependencies such as the +/// +[ExportCSharpVisualBasicStatelessLspService(typeof(ILspMiscellaneousFilesWorkspaceProviderFactory), WellKnownLspServerKinds.CSharpVisualBasicLspServer), Shared] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal sealed class FileBasedProgramsWorkspaceProviderFactory( + IMetadataAsSourceFileService metadataAsSourceFileService, + LanguageServerWorkspaceFactory workspaceFactory, + IFileChangeWatcher fileChangeWatcher, + IGlobalOptionService globalOptionService, + ILoggerFactory loggerFactory, + IAsynchronousOperationListenerProvider listenerProvider, + ProjectLoadTelemetryReporter projectLoadTelemetry, + ServerConfigurationFactory serverConfigurationFactory, + BinlogNamer binlogNamer) : ILspMiscellaneousFilesWorkspaceProviderFactory +{ + public ILspMiscellaneousFilesWorkspaceProvider CreateLspMiscellaneousFilesWorkspaceProvider(ILspServices lspServices, HostServices hostServices) + { + return new FileBasedProgramsProjectSystem(lspServices, metadataAsSourceFileService, workspaceFactory, fileChangeWatcher, globalOptionService, loggerFactory, listenerProvider, projectLoadTelemetry, serverConfigurationFactory, binlogNamer); + } +} diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/LanguageServerProjectLoader.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/LanguageServerProjectLoader.cs index 56d127d86d716..d97745c2edb92 100644 --- a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/LanguageServerProjectLoader.cs +++ b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/LanguageServerProjectLoader.cs @@ -4,23 +4,27 @@ using System.Collections.Concurrent; using System.Collections.Immutable; -using System.Composition; using System.Diagnostics; -using System.Runtime.InteropServices; +using System.IO; +using System.Threading; +using System.Threading.Tasks; using Microsoft.CodeAnalysis.Collections; +using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.Host; -using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.LanguageServer.Handler.DebugConfiguration; using Microsoft.CodeAnalysis.LanguageServer.HostWorkspace.ProjectTelemetry; using Microsoft.CodeAnalysis.MSBuild; using Microsoft.CodeAnalysis.Options; +using Microsoft.CodeAnalysis.PooledObjects; using Microsoft.CodeAnalysis.ProjectSystem; +using Microsoft.CodeAnalysis.Shared.Collections; +using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Shared.TestHooks; using Microsoft.CodeAnalysis.Shared.Utilities; +using Microsoft.CodeAnalysis.Text; using Microsoft.CodeAnalysis.Threading; using Microsoft.CodeAnalysis.Workspaces.ProjectSystem; using Microsoft.Extensions.Logging; -using Microsoft.VisualStudio.Composition; using Roslyn.Utilities; using static Microsoft.CodeAnalysis.MSBuild.BuildHostProcessManager; using LSP = Roslyn.LanguageServer.Protocol; @@ -29,27 +33,60 @@ namespace Microsoft.CodeAnalysis.LanguageServer.HostWorkspace; internal abstract class LanguageServerProjectLoader { - protected readonly AsyncBatchingWorkQueue ProjectsToLoadAndReload; + private readonly AsyncBatchingWorkQueue _projectsToReload; protected readonly ProjectSystemProjectFactory ProjectFactory; private readonly ProjectTargetFrameworkManager _targetFrameworkManager; private readonly ProjectSystemHostInfo _projectSystemHostInfo; private readonly IFileChangeWatcher _fileChangeWatcher; - private readonly IGlobalOptionService _globalOptionService; + protected readonly IGlobalOptionService GlobalOptionService; protected readonly ILoggerFactory LoggerFactory; private readonly ILogger _logger; private readonly ProjectLoadTelemetryReporter _projectLoadTelemetryReporter; private readonly BinlogNamer _binlogNamer; - private readonly ProjectFileExtensionRegistry _projectFileExtensionRegistry; protected readonly ImmutableDictionary AdditionalProperties; /// - /// The list of loaded projects in the workspace, keyed by project file path. The outer dictionary is a concurrent dictionary since we may be loading - /// multiple projects at once; the key is a single List we just have a single thread processing any given project file. This is only to be used - /// in and downstream calls; any other updating of this (like unloading projects) should be achieved by adding - /// things to the . + /// Guards access to . + /// To keep the LSP queue responsive, must not be held while performing design-time builds. /// - private readonly ConcurrentDictionary> _loadedProjects = []; + private readonly SemaphoreSlim _gate = new(initialCount: 1); + + /// + /// Maps the file path of a tracked project to the load state for the project. + /// Absence of an entry indicates the project is not tracked, e.g. it was never loaded, or it was unloaded. + /// must be held when modifying the dictionary or objects contained in it. + /// + private readonly Dictionary _loadedProjects = []; + + /// + /// State transitions: + /// -> + /// Any state -> unloaded (which is denoted by removing the entry for the project) + /// + private abstract record ProjectLoadState + { + private ProjectLoadState() { } + + /// + /// Represents a project which has not yet had a design-time build performed for it, + /// and which has an associated "primordial project" in the workspace. + /// + /// + /// ID of the project which LSP uses to fulfill requests until the first design-time build is complete. + /// The project with this ID is removed from the workspace when unloading or when transitioning to state. + /// + public sealed record Primordial(ProjectId PrimordialProjectId) : ProjectLoadState; + + /// + /// Represents a project for which we have loaded zero or more targets. + /// Generally a project which has zero loaded targets has not had a design-time build completed for it yet. + /// Incrementally updated upon subsequent design-time builds. + /// The are disposed when unloading. + /// + /// List of target frameworks which have been loaded for this project so far. + public sealed record LoadedTargets(ImmutableArray LoadedProjectTargets) : ProjectLoadState; + } protected LanguageServerProjectLoader( ProjectSystemProjectFactory projectFactory, @@ -67,22 +104,21 @@ protected LanguageServerProjectLoader( _targetFrameworkManager = targetFrameworkManager; _projectSystemHostInfo = projectSystemHostInfo; _fileChangeWatcher = fileChangeWatcher; - _globalOptionService = globalOptionService; + GlobalOptionService = globalOptionService; LoggerFactory = loggerFactory; _logger = loggerFactory.CreateLogger(nameof(LanguageServerProjectLoader)); _projectLoadTelemetryReporter = projectLoadTelemetry; _binlogNamer = binlogNamer; var workspace = projectFactory.Workspace; - _projectFileExtensionRegistry = new ProjectFileExtensionRegistry(workspace.CurrentSolution.Services, new DiagnosticReporter(workspace)); var razorDesignTimePath = serverConfigurationFactory.ServerConfiguration?.RazorDesignTimePath; AdditionalProperties = razorDesignTimePath is null ? ImmutableDictionary.Empty : ImmutableDictionary.Empty.Add("RazorDesignTimeTargets", razorDesignTimePath); - ProjectsToLoadAndReload = new AsyncBatchingWorkQueue( + _projectsToReload = new AsyncBatchingWorkQueue( TimeSpan.FromMilliseconds(100), - LoadOrReloadProjectsAsync, + ReloadProjectsAsync, ProjectToLoad.Comparer, listenerProvider.GetListener(FeatureAttribute.Workspace), CancellationToken.None); // TODO: do we need to introduce a shutdown cancellation token for this? @@ -103,7 +139,7 @@ public async Task ReportErrorAsync(LSP.MessageType errorKind, string message, Ca } } - private async ValueTask LoadOrReloadProjectsAsync(ImmutableSegmentedList projectPathsToLoadOrReload, CancellationToken cancellationToken) + private async ValueTask ReloadProjectsAsync(ImmutableSegmentedList projectPathsToLoadOrReload, CancellationToken cancellationToken) { var stopwatch = Stopwatch.StartNew(); @@ -121,7 +157,7 @@ private async ValueTask LoadOrReloadProjectsAsync(ImmutableSegmentedList { var (@this, toastErrorReporter, buildHostProcessManager) = args; - var projectNeedsRestore = await @this.LoadOrReloadProjectAsync( + var projectNeedsRestore = await @this.ReloadProjectAsync( projectToLoad, toastErrorReporter, buildHostProcessManager, cancellationToken); if (projectNeedsRestore) @@ -130,7 +166,7 @@ private async ValueTask LoadOrReloadProjectsAsync(ImmutableSegmentedListLoads a project in the MSBuild host. + /// Caller needs to catch exceptions to avoid bringing down the project loader queue. + protected abstract Task<(RemoteProjectFile projectFile, bool hasAllInformation, BuildHostProcessKind preferred, BuildHostProcessKind actual)?> TryLoadProjectInMSBuildHostAsync( + BuildHostProcessManager buildHostProcessManager, string projectPath, CancellationToken cancellationToken); + /// True if the project needs a NuGet restore, false otherwise. - private async Task LoadOrReloadProjectAsync(ProjectToLoad projectToLoad, ToastErrorReporter toastErrorReporter, BuildHostProcessManager buildHostProcessManager, CancellationToken cancellationToken) + private async Task ReloadProjectAsync(ProjectToLoad projectToLoad, ToastErrorReporter toastErrorReporter, BuildHostProcessManager buildHostProcessManager, CancellationToken cancellationToken) { BuildHostProcessKind? preferredBuildHostKindThatWeDidNotGet = null; var projectPath = projectToLoad.Path; + Contract.ThrowIfFalse(PathUtilities.IsAbsolute(projectPath)); + + // Before doing any work, check if the project has already been unloaded. + using (await _gate.DisposableWaitAsync(cancellationToken)) + { + if (!_loadedProjects.ContainsKey(projectPath)) + { + return false; + } + } try { - var preferredBuildHostKind = GetKindForProject(projectPath); - var (buildHost, actualBuildHostKind) = await buildHostProcessManager.GetBuildHostWithFallbackAsync(preferredBuildHostKind, projectPath, cancellationToken); + if (await TryLoadProjectInMSBuildHostAsync(buildHostProcessManager, projectPath, cancellationToken) + is not var (remoteProjectFile, hasAllInformation, preferredBuildHostKind, actualBuildHostKind)) + { + _logger.LogWarning($"Unable to load project '{projectPath}'."); + return false; + } + if (preferredBuildHostKind != actualBuildHostKind) preferredBuildHostKindThatWeDidNotGet = preferredBuildHostKind; - if (!_projectFileExtensionRegistry.TryGetLanguageNameFromProjectPath(projectPath, DiagnosticReportingMode.Ignore, out var languageName)) - return false; - - var loadedFile = await buildHost.LoadProjectFileAsync(projectPath, languageName, cancellationToken); - var diagnosticLogItems = await loadedFile.GetDiagnosticLogItemsAsync(cancellationToken); + var diagnosticLogItems = await remoteProjectFile.GetDiagnosticLogItemsAsync(cancellationToken); if (diagnosticLogItems.Any(item => item.Kind is DiagnosticLogItemKind.Error)) { await LogDiagnosticsAsync(diagnosticLogItems); @@ -171,7 +223,7 @@ private async Task LoadOrReloadProjectAsync(ProjectToLoad projectToLoad, T return false; } - var loadedProjectInfos = await loadedFile.GetProjectFileInfosAsync(cancellationToken); + var loadedProjectInfos = await remoteProjectFile.GetProjectFileInfosAsync(cancellationToken); // The out-of-proc build host supports more languages than we may actually have Workspace binaries for, so ensure we can actually process that // language in-process. @@ -181,55 +233,65 @@ private async Task LoadOrReloadProjectAsync(ProjectToLoad projectToLoad, T return false; } - var existingProjects = _loadedProjects.GetOrAdd(projectPath, static _ => []); - Dictionary telemetryInfos = []; var needsRestore = false; - foreach (var loadedProjectInfo in loadedProjectInfos) + using (await _gate.DisposableWaitAsync(cancellationToken)) { - // If we already have the project with this same target framework, just update it - var existingProject = existingProjects.Find(p => p.GetTargetFramework() == loadedProjectInfo.TargetFramework); - bool targetNeedsRestore; - ProjectLoadTelemetryReporter.TelemetryInfo targetTelemetryInfo; - - if (existingProject != null) + if (!_loadedProjects.TryGetValue(projectPath, out var currentLoadState)) { - (targetTelemetryInfo, targetNeedsRestore) = await existingProject.UpdateWithNewProjectInfoAsync(loadedProjectInfo, _logger); + // Project was unloaded. Do not proceed with reloading it. + return false; } - else + + var previousProjectTargets = currentLoadState is ProjectLoadState.LoadedTargets loaded ? loaded.LoadedProjectTargets : []; + var newProjectTargetsBuilder = ArrayBuilder.GetInstance(loadedProjectInfos.Length); + foreach (var loadedProjectInfo in loadedProjectInfos) { - var projectSystemName = $"{projectPath} (${loadedProjectInfo.TargetFramework})"; - var projectCreationInfo = new ProjectSystemProjectCreationInfo - { - AssemblyName = projectSystemName, - FilePath = projectPath, - CompilationOutputAssemblyFilePath = loadedProjectInfo.IntermediateOutputFilePath - }; + var (target, targetAlreadyExists) = await GetOrCreateProjectTargetAsync(previousProjectTargets, loadedProjectInfo); + newProjectTargetsBuilder.Add(target); - var projectSystemProject = await ProjectFactory.CreateAndAddToWorkspaceAsync( - projectSystemName, - loadedProjectInfo.Language, - projectCreationInfo, - _projectSystemHostInfo); + if (targetAlreadyExists) + { + // https://github.com/dotnet/roslyn/issues/78561: Automatic restore should run even when the target is already loaded + _ = await target.UpdateWithNewProjectInfoAsync(loadedProjectInfo, hasAllInformation, _logger); + } + else + { + var (targetTelemetryInfo, targetNeedsRestore) = await target.UpdateWithNewProjectInfoAsync(loadedProjectInfo, hasAllInformation, _logger); + needsRestore |= targetNeedsRestore; + telemetryInfos[loadedProjectInfo] = targetTelemetryInfo with { IsSdkStyle = preferredBuildHostKind == BuildHostProcessKind.NetCore }; + } + } - var loadedProject = new LoadedProject(projectSystemProject, ProjectFactory.Workspace.Services.SolutionServices, _fileChangeWatcher, _targetFrameworkManager); - loadedProject.NeedsReload += (_, _) => ProjectsToLoadAndReload.AddWork(projectToLoad with { ReportTelemetry = false }); - existingProjects.Add(loadedProject); + var newProjectTargets = newProjectTargetsBuilder.ToImmutableAndFree(); + foreach (var target in previousProjectTargets) + { + // Unload targets which were present in a past design-time build, but absent in the current one. + if (!newProjectTargets.Contains(target)) + { + target.Dispose(); + } + } - (targetTelemetryInfo, targetNeedsRestore) = await loadedProject.UpdateWithNewProjectInfoAsync(loadedProjectInfo, _logger); + if (projectToLoad.ReportTelemetry) + { + await _projectLoadTelemetryReporter.ReportProjectLoadTelemetryAsync(telemetryInfos, projectToLoad, cancellationToken); + } - needsRestore |= targetNeedsRestore; - telemetryInfos[loadedProjectInfo] = targetTelemetryInfo with { IsSdkStyle = preferredBuildHostKind == BuildHostProcessKind.NetCore }; + if (currentLoadState is ProjectLoadState.Primordial(var projectId)) + { + // Remove the primordial project now that the design-time build pass is finished. This ensures that + // we have the new project in place before we remove the primordial project; otherwise for + // Miscellaneous Files we could have a case where we'd get another request to create a project + // for the project we're currently processing. + await ProjectFactory.ApplyChangeToWorkspaceAsync(workspace => workspace.OnProjectRemoved(projectId), cancellationToken); } - } - if (projectToLoad.ReportTelemetry) - { - await _projectLoadTelemetryReporter.ReportProjectLoadTelemetryAsync(telemetryInfos, projectToLoad, cancellationToken); + _loadedProjects[projectPath] = new ProjectLoadState.LoadedTargets(newProjectTargets); } - diagnosticLogItems = await loadedFile.GetDiagnosticLogItemsAsync(cancellationToken); + diagnosticLogItems = await remoteProjectFile.GetDiagnosticLogItemsAsync(cancellationToken); if (diagnosticLogItems.Any()) { await LogDiagnosticsAsync(diagnosticLogItems); @@ -251,6 +313,35 @@ private async Task LoadOrReloadProjectAsync(ProjectToLoad projectToLoad, T return false; } + async Task<(LoadedProject, bool alreadyExists)> GetOrCreateProjectTargetAsync(ImmutableArray previousProjectTargets, ProjectFileInfo loadedProjectInfo) + { + var existingProject = previousProjectTargets.FirstOrDefault(p => p.GetTargetFramework() == loadedProjectInfo.TargetFramework); + if (existingProject != null) + { + return (existingProject, alreadyExists: true); + } + + var targetFramework = loadedProjectInfo.TargetFramework; + var projectSystemName = targetFramework is null ? projectPath : $"{projectPath} (${targetFramework})"; + + var projectCreationInfo = new ProjectSystemProjectCreationInfo + { + AssemblyName = projectSystemName, + FilePath = projectPath, + CompilationOutputAssemblyFilePath = loadedProjectInfo.IntermediateOutputFilePath, + }; + + var projectSystemProject = await ProjectFactory.CreateAndAddToWorkspaceAsync( + projectSystemName, + loadedProjectInfo.Language, + projectCreationInfo, + _projectSystemHostInfo); + + var loadedProject = new LoadedProject(projectSystemProject, ProjectFactory.Workspace.Services.SolutionServices, _fileChangeWatcher, _targetFrameworkManager); + loadedProject.NeedsReload += (_, _) => _projectsToReload.AddWork(projectToLoad with { ReportTelemetry = false }); + return (loadedProject, alreadyExists: false); + } + async Task LogDiagnosticsAsync(ImmutableArray diagnosticLogItems) { foreach (var logItem in diagnosticLogItems) @@ -273,4 +364,81 @@ async Task LogDiagnosticsAsync(ImmutableArray diagnosticLogIt await toastErrorReporter.ReportErrorAsync(worstLspMessageKind, message, cancellationToken); } } + + /// + /// Begins loading a project with an associated primordial project. Must not be called for a project which has already begun loading. + /// + /// + /// If , initiates a design-time build now, and starts file watchers to repeat the design-time build on relevant changes. + /// If , only tracks the primordial project. + /// + protected async ValueTask BeginLoadingProjectWithPrimordialAsync(string projectPath, ProjectId primordialProjectId, bool doDesignTimeBuild) + { + using (await _gate.DisposableWaitAsync(CancellationToken.None)) + { + // If this project has already begun loading, we need to throw. + // This is because we can't ensure that the workspace and project system will remain in a consistent state after this call. + // For example, there could be a need for the project system to track both a primordial project and list of loaded targets, which we don't support. + if (_loadedProjects.ContainsKey(projectPath)) + { + Contract.Fail($"Cannot begin loading project '{projectPath}' because it has already begun loading."); + } + + _loadedProjects.Add(projectPath, new ProjectLoadState.Primordial(primordialProjectId)); + if (doDesignTimeBuild) + { + _projectsToReload.AddWork(new ProjectToLoad(projectPath, ProjectGuid: null, ReportTelemetry: true)); + } + } + } + + /// + /// Begins loading a project. If the project has already begun loading, returns without doing any additional work. + /// + protected async Task BeginLoadingProjectAsync(string projectPath, string? projectGuid) + { + using (await _gate.DisposableWaitAsync(CancellationToken.None)) + { + // If project has already begun loading, no need to do any further work. + if (_loadedProjects.ContainsKey(projectPath)) + { + return; + } + + _loadedProjects.Add(projectPath, new ProjectLoadState.LoadedTargets(LoadedProjectTargets: [])); + _projectsToReload.AddWork(new ProjectToLoad(Path: projectPath, ProjectGuid: projectGuid, ReportTelemetry: true)); + } + } + + protected Task WaitForProjectsToFinishLoadingAsync() => _projectsToReload.WaitUntilCurrentBatchCompletesAsync(); + + protected async ValueTask UnloadProjectAsync(string projectPath) + { + using (await _gate.DisposableWaitAsync(CancellationToken.None)) + { + if (!_loadedProjects.Remove(projectPath, out var loadState)) + { + // It is common to be called with a path to a project which is already not loaded. + // In this case, we should do nothing. + return; + } + + if (loadState is ProjectLoadState.Primordial(var projectId)) + { + await ProjectFactory.ApplyChangeToWorkspaceAsync(workspace => workspace.OnProjectRemoved(projectId)); + } + else if (loadState is ProjectLoadState.LoadedTargets(var existingProjects)) + { + foreach (var existingProject in existingProjects) + { + // Disposing a LoadedProject unloads it and removes it from the workspace. + existingProject.Dispose(); + } + } + else + { + throw ExceptionUtilities.UnexpectedValue(loadState); + } + } + } } diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/LanguageServerProjectSystem.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/LanguageServerProjectSystem.cs index caea9bff27f62..47145013cfb9e 100644 --- a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/LanguageServerProjectSystem.cs +++ b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/LanguageServerProjectSystem.cs @@ -2,27 +2,19 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using System.Collections.Concurrent; using System.Collections.Immutable; using System.Composition; -using System.Diagnostics; using System.Runtime.InteropServices; -using Microsoft.CodeAnalysis.Collections; -using Microsoft.CodeAnalysis.Host; using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.LanguageServer.HostWorkspace.ProjectTelemetry; using Microsoft.CodeAnalysis.MSBuild; using Microsoft.CodeAnalysis.Options; using Microsoft.CodeAnalysis.ProjectSystem; using Microsoft.CodeAnalysis.Shared.TestHooks; -using Microsoft.CodeAnalysis.Shared.Utilities; -using Microsoft.CodeAnalysis.Threading; -using Microsoft.CodeAnalysis.Workspaces.ProjectSystem; using Microsoft.Extensions.Logging; using Microsoft.VisualStudio.Composition; using Roslyn.Utilities; using static Microsoft.CodeAnalysis.MSBuild.BuildHostProcessManager; -using LSP = Roslyn.LanguageServer.Protocol; namespace Microsoft.CodeAnalysis.LanguageServer.HostWorkspace; @@ -30,7 +22,7 @@ namespace Microsoft.CodeAnalysis.LanguageServer.HostWorkspace; internal sealed class LanguageServerProjectSystem : LanguageServerProjectLoader { private readonly ILogger _logger; - private readonly SemaphoreSlim _gate = new SemaphoreSlim(initialCount: 1); + private readonly ProjectFileExtensionRegistry _projectFileExtensionRegistry; [ImportingConstructor] [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] @@ -44,7 +36,7 @@ public LanguageServerProjectSystem( ServerConfigurationFactory serverConfigurationFactory, BinlogNamer binlogNamer) : base( - workspaceFactory.ProjectSystemProjectFactory, + workspaceFactory.HostProjectFactory, workspaceFactory.TargetFrameworkManager, workspaceFactory.ProjectSystemHostInfo, fileChangeWatcher, @@ -56,36 +48,34 @@ public LanguageServerProjectSystem( binlogNamer) { _logger = loggerFactory.CreateLogger(nameof(LanguageServerProjectSystem)); + var workspace = ProjectFactory.Workspace; + _projectFileExtensionRegistry = new ProjectFileExtensionRegistry(workspace.CurrentSolution.Services, new DiagnosticReporter(workspace)); } public async Task OpenSolutionAsync(string solutionFilePath) { - using (await _gate.DisposableWaitAsync()) - { - _logger.LogInformation(string.Format(LanguageServerResources.Loading_0, solutionFilePath)); - ProjectFactory.SolutionPath = solutionFilePath; - - // We'll load solutions out-of-proc, since it's possible we might be running on a runtime that doesn't have a matching SDK installed, - // and we don't want any MSBuild registration to set environment variables in our process that might impact child processes. - await using var buildHostProcessManager = new BuildHostProcessManager(globalMSBuildProperties: AdditionalProperties, loggerFactory: LoggerFactory); - var buildHost = await buildHostProcessManager.GetBuildHostAsync(BuildHostProcessKind.NetCore, CancellationToken.None); + _logger.LogInformation(string.Format(LanguageServerResources.Loading_0, solutionFilePath)); + ProjectFactory.SolutionPath = solutionFilePath; - // If we don't have a .NET Core SDK on this machine at all, try .NET Framework - if (!await buildHost.HasUsableMSBuildAsync(solutionFilePath, CancellationToken.None)) - { - var kind = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? BuildHostProcessKind.NetFramework : BuildHostProcessKind.Mono; - buildHost = await buildHostProcessManager.GetBuildHostAsync(kind, CancellationToken.None); - } + // We'll load solutions out-of-proc, since it's possible we might be running on a runtime that doesn't have a matching SDK installed, + // and we don't want any MSBuild registration to set environment variables in our process that might impact child processes. + await using var buildHostProcessManager = new BuildHostProcessManager(globalMSBuildProperties: AdditionalProperties, loggerFactory: LoggerFactory); + var buildHost = await buildHostProcessManager.GetBuildHostAsync(BuildHostProcessKind.NetCore, CancellationToken.None); - foreach (var project in await buildHost.GetProjectsInSolutionAsync(solutionFilePath, CancellationToken.None)) - { - ProjectsToLoadAndReload.AddWork(new ProjectToLoad(project.ProjectPath, project.ProjectGuid, ReportTelemetry: true)); - } + // If we don't have a .NET Core SDK on this machine at all, try .NET Framework + if (!await buildHost.HasUsableMSBuildAsync(solutionFilePath, CancellationToken.None)) + { + var kind = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? BuildHostProcessKind.NetFramework : BuildHostProcessKind.Mono; + buildHost = await buildHostProcessManager.GetBuildHostAsync(kind, CancellationToken.None); + } - // Wait for the in progress batch to complete and send a project initialized notification to the client. - await ProjectsToLoadAndReload.WaitUntilCurrentBatchCompletesAsync(); - await ProjectInitializationHandler.SendProjectInitializationCompleteNotificationAsync(); + var projects = await buildHost.GetProjectsInSolutionAsync(solutionFilePath, CancellationToken.None); + foreach (var (path, guid) in projects) + { + await BeginLoadingProjectAsync(path, guid); } + await WaitForProjectsToFinishLoadingAsync(); + await ProjectInitializationHandler.SendProjectInitializationCompleteNotificationAsync(); } public async Task OpenProjectsAsync(ImmutableArray projectFilePaths) @@ -93,13 +83,24 @@ public async Task OpenProjectsAsync(ImmutableArray projectFilePaths) if (!projectFilePaths.Any()) return; - using (await _gate.DisposableWaitAsync()) + foreach (var path in projectFilePaths) { - ProjectsToLoadAndReload.AddWork(projectFilePaths.Select(p => new ProjectToLoad(p, ProjectGuid: null, ReportTelemetry: true))); - - // Wait for the in progress batch to complete and send a project initialized notification to the client. - await ProjectsToLoadAndReload.WaitUntilCurrentBatchCompletesAsync(); - await ProjectInitializationHandler.SendProjectInitializationCompleteNotificationAsync(); + await BeginLoadingProjectAsync(path, projectGuid: null); } + await WaitForProjectsToFinishLoadingAsync(); + await ProjectInitializationHandler.SendProjectInitializationCompleteNotificationAsync(); + } + + protected override async Task<(RemoteProjectFile projectFile, bool hasAllInformation, BuildHostProcessKind preferred, BuildHostProcessKind actual)?> TryLoadProjectInMSBuildHostAsync( + BuildHostProcessManager buildHostProcessManager, string projectPath, CancellationToken cancellationToken) + { + if (!_projectFileExtensionRegistry.TryGetLanguageNameFromProjectPath(projectPath, DiagnosticReportingMode.Ignore, out var languageName)) + return null; + + var preferredBuildHostKind = GetKindForProject(projectPath); + var (buildHost, actualBuildHostKind) = await buildHostProcessManager.GetBuildHostWithFallbackAsync(preferredBuildHostKind, projectPath, cancellationToken); + + var loadedFile = await buildHost.LoadProjectFileAsync(projectPath, languageName, cancellationToken); + return (loadedFile, hasAllInformation: true, preferredBuildHostKind, actualBuildHostKind); } } diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/LanguageServerWorkspace.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/LanguageServerWorkspace.cs index 78d8546e280ab..26f2eb2fae27b 100644 --- a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/LanguageServerWorkspace.cs +++ b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/LanguageServerWorkspace.cs @@ -2,9 +2,11 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System.Diagnostics; using Microsoft.CodeAnalysis.Host; using Microsoft.CodeAnalysis.Text; using Microsoft.CodeAnalysis.Workspaces.ProjectSystem; +using Roslyn.Utilities; using LSP = Roslyn.LanguageServer.Protocol; namespace Microsoft.CodeAnalysis.LanguageServer.HostWorkspace; @@ -36,6 +38,7 @@ namespace Microsoft.CodeAnalysis.LanguageServer.HostWorkspace; /// it will use the local information it has outside of the workspace to ensure it is always matched with the lsp /// client. /// +[DebuggerDisplay("{GetDebuggerDisplay(),nq}")] internal sealed class LanguageServerWorkspace : Workspace, ILspWorkspace { /// @@ -44,8 +47,8 @@ internal sealed class LanguageServerWorkspace : Workspace, ILspWorkspace /// public ProjectSystemProjectFactory ProjectSystemProjectFactory { private get; set; } = null!; - public LanguageServerWorkspace(HostServices host) - : base(host, WorkspaceKind.Host) + public LanguageServerWorkspace(HostServices host, string workspaceKind) + : base(host, workspaceKind) { } @@ -106,7 +109,9 @@ internal override ValueTask TryOnDocumentClosedAsync(DocumentId documentId, Canc { TextLoader loader; var document = textDocument as Document; - if (document?.DocumentState.Attributes.DesignTimeOnly == true) + + // 'DesignTimeOnly == true' or 'filePath' not being absolute indicates the document is for a virtual file (in-memory, not on-disk). + if (document is not null && (document.DocumentState.Attributes.DesignTimeOnly || !PathUtilities.IsAbsolute(filePath))) { // Dynamic files don't exist on disk so if we were to use the FileTextLoader we'd effectively be emptying out the document. // We also assume they're not user editable, and hence can't have "unsaved" changes that are expected to go away on close. @@ -133,4 +138,9 @@ internal override ValueTask TryOnDocumentClosedAsync(DocumentId documentId, Canc }, cancellationToken); } + + private string GetDebuggerDisplay() + { + return $"""LanguageServerWorkspace(Kind: "{Kind}")"""; + } } diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/LanguageServerWorkspaceFactory.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/LanguageServerWorkspaceFactory.cs index 2bbe0368ccbc8..e00375533db78 100644 --- a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/LanguageServerWorkspaceFactory.cs +++ b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/LanguageServerWorkspaceFactory.cs @@ -45,14 +45,22 @@ public LanguageServerWorkspaceFactory( .ToImmutableArray(); // Create the workspace and set analyzer references for it - var workspace = new LanguageServerWorkspace(hostServicesProvider.HostServices); + var workspace = new LanguageServerWorkspace(hostServicesProvider.HostServices, WorkspaceKind.Host); workspace.SetCurrentSolution(s => s.WithAnalyzerReferences(CreateSolutionLevelAnalyzerReferencesForWorkspace(workspace)), WorkspaceChangeKind.SolutionChanged); - Workspace = workspace; - ProjectSystemProjectFactory = new ProjectSystemProjectFactory( - Workspace, fileChangeWatcher, static (_, _) => Task.CompletedTask, _ => { }, + HostProjectFactory = new ProjectSystemProjectFactory( + workspace, fileChangeWatcher, static (_, _) => Task.CompletedTask, _ => { }, CancellationToken.None); // TODO: do we need to introduce a shutdown cancellation token for this? - workspace.ProjectSystemProjectFactory = ProjectSystemProjectFactory; + workspace.ProjectSystemProjectFactory = HostProjectFactory; + + // https://github.com/dotnet/roslyn/issues/78560: Move this workspace creation to 'FileBasedProgramsWorkspaceProviderFactory'. + // 'CreateSolutionLevelAnalyzerReferencesForWorkspace' needs to be broken out into its own service for us to be able to move this. + var fileBasedProgramsWorkspace = new LanguageServerWorkspace(hostServicesProvider.HostServices, WorkspaceKind.MiscellaneousFiles); + fileBasedProgramsWorkspace.SetCurrentSolution(s => s.WithAnalyzerReferences(CreateSolutionLevelAnalyzerReferencesForWorkspace(fileBasedProgramsWorkspace)), WorkspaceChangeKind.SolutionChanged); + + FileBasedProgramsProjectFactory = new ProjectSystemProjectFactory( + fileBasedProgramsWorkspace, fileChangeWatcher, static (_, _) => Task.CompletedTask, _ => { }, CancellationToken.None); + fileBasedProgramsWorkspace.ProjectSystemProjectFactory = FileBasedProgramsProjectFactory; var razorSourceGenerator = serverConfigurationFactory?.ServerConfiguration?.RazorSourceGenerator; ProjectSystemHostInfo = new ProjectSystemHostInfo( @@ -63,9 +71,11 @@ public LanguageServerWorkspaceFactory( TargetFrameworkManager = projectTargetFrameworkManager; } - public Workspace Workspace { get; } + public Workspace HostWorkspace => HostProjectFactory.Workspace; + + public ProjectSystemProjectFactory HostProjectFactory { get; } + public ProjectSystemProjectFactory FileBasedProgramsProjectFactory { get; } - public ProjectSystemProjectFactory ProjectSystemProjectFactory { get; } public ProjectSystemHostInfo ProjectSystemHostInfo { get; } public ProjectTargetFrameworkManager TargetFrameworkManager { get; } diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/LoadedProject.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/LoadedProject.cs index c2cae1e5418ee..7a2c74612d7ad 100644 --- a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/LoadedProject.cs +++ b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/LoadedProject.cs @@ -100,13 +100,17 @@ private void FileChangedContext_FileChanged(object? sender, string filePath) return _mostRecentFileInfo.TargetFramework; } + /// + /// Unloads the project and removes it from the workspace. + /// public void Dispose() { + _fileChangeContext.Dispose(); _optionsProcessor.Dispose(); _projectSystemProject.RemoveFromWorkspace(); } - public async ValueTask<(ProjectLoadTelemetryReporter.TelemetryInfo, bool NeedsRestore)> UpdateWithNewProjectInfoAsync(ProjectFileInfo newProjectInfo, ILogger logger) + public async ValueTask<(ProjectLoadTelemetryReporter.TelemetryInfo, bool NeedsRestore)> UpdateWithNewProjectInfoAsync(ProjectFileInfo newProjectInfo, bool hasAllInformation, ILogger logger) { if (_mostRecentFileInfo != null) { @@ -134,6 +138,7 @@ public void Dispose() _projectSystemProject.GeneratedFilesOutputDirectory = newProjectInfo.GeneratedFilesOutputDirectory; _projectSystemProject.CompilationOutputAssemblyFilePath = newProjectInfo.IntermediateOutputFilePath; _projectSystemProject.DefaultNamespace = newProjectInfo.DefaultNamespace; + _projectSystemProject.HasAllInformation = hasAllInformation; if (newProjectInfo.TargetFrameworkIdentifier != null) { diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/ProjectToLoad.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/ProjectToLoad.cs index f3d72668e1113..41d767d74f9fe 100644 --- a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/ProjectToLoad.cs +++ b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/ProjectToLoad.cs @@ -7,7 +7,7 @@ namespace Microsoft.CodeAnalysis.LanguageServer.HostWorkspace; /// -/// The project path (and the guid if it game from a solution) of the project to load. +/// The project path (and the guid if it came from a solution) of the project to load. /// internal sealed record ProjectToLoad(string Path, string? ProjectGuid, bool ReportTelemetry) { diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/Razor/RazorDynamicFileInfoProvider.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/Razor/RazorDynamicFileInfoProvider.cs index 9b3e7b297838c..6eb90d6d0efba 100644 --- a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/Razor/RazorDynamicFileInfoProvider.cs +++ b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/Razor/RazorDynamicFileInfoProvider.cs @@ -37,7 +37,7 @@ internal sealed partial class RazorDynamicFileInfoProvider(Lazy +/// This will be replaced invoke dotnet run-api command implemented in https://github.com/dotnet/sdk/pull/48749 +/// +internal static class VirtualCSharpFileBasedProgramProject +{ + /// + /// Adjusts a path to a file-based program for use in passing the virtual project to msbuild. + /// (msbuild needs the path to end in .csproj to recognize as a C# project and apply all the standard props/targets to it.) + /// + internal static string GetVirtualProjectPath(string documentFilePath) + => Path.ChangeExtension(documentFilePath, ".csproj"); + + internal static (string virtualProjectXml, bool isFileBasedProgram) MakeVirtualProjectContent(string documentFilePath, SourceText text) + { + Contract.ThrowIfFalse(PathUtilities.IsAbsolute(documentFilePath)); + // NB: this is a temporary solution for running our heuristic. + // When we adopt the dotnet run-api, we need to get rid of this or adjust it to be more sustainable (e.g. using the appropriate document to get a syntax tree) + var tree = CSharpSyntaxTree.ParseText(text, options: CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.Preview), path: documentFilePath); + var root = tree.GetRoot(); + var isFileBasedProgram = root.GetLeadingTrivia().Any(SyntaxKind.IgnoredDirectiveTrivia) || root.ChildNodes().Any(node => node.IsKind(SyntaxKind.GlobalStatement)); + + var virtualProjectXml = $""" + + + + Exe + net8.0 + enable + enable + $(Features);FileBasedProgram + false + + + + + + + + + + + + + + + + + <_RestoreProjectPathItems Include="@(FilteredRestoreGraphProjectInputItems)" /> + + + + + + + + """; + + return (virtualProjectXml, isFileBasedProgram); + } +} diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/WorkspaceProjectFactoryService.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/WorkspaceProjectFactoryService.cs index b3f28c5651205..90b2b00d53ab2 100644 --- a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/WorkspaceProjectFactoryService.cs +++ b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/WorkspaceProjectFactoryService.cs @@ -48,16 +48,16 @@ public async Task CreateAndAddProjectAsync(WorkspaceProjectCr { if (creationInfo.BuildSystemProperties.TryGetValue("SolutionPath", out var solutionPath)) { - _workspaceFactory.ProjectSystemProjectFactory.SolutionPath = solutionPath; + _workspaceFactory.HostProjectFactory.SolutionPath = solutionPath; } - var project = await _workspaceFactory.ProjectSystemProjectFactory.CreateAndAddToWorkspaceAsync( + var project = await _workspaceFactory.HostProjectFactory.CreateAndAddToWorkspaceAsync( creationInfo.DisplayName, creationInfo.Language, new Workspaces.ProjectSystem.ProjectSystemProjectCreationInfo { FilePath = creationInfo.FilePath }, _workspaceFactory.ProjectSystemHostInfo); - var workspaceProject = new WorkspaceProject(project, _workspaceFactory.Workspace.Services.SolutionServices, _workspaceFactory.TargetFrameworkManager, _loggerFactory); + var workspaceProject = new WorkspaceProject(project, _workspaceFactory.HostWorkspace.Services.SolutionServices, _workspaceFactory.TargetFrameworkManager, _loggerFactory); // We've created a new project, so initialize properties we have await workspaceProject.SetBuildSystemPropertiesAsync(creationInfo.BuildSystemProperties, CancellationToken.None); diff --git a/src/LanguageServer/Protocol/Extensions/ProtocolConversions.cs b/src/LanguageServer/Protocol/Extensions/ProtocolConversions.cs index 1d0be6244de58..9bf1788dc8849 100644 --- a/src/LanguageServer/Protocol/Extensions/ProtocolConversions.cs +++ b/src/LanguageServer/Protocol/Extensions/ProtocolConversions.cs @@ -832,7 +832,9 @@ public static LSP.VSProjectContext ProjectToProjectContext(Project project) { Id = ProjectIdToProjectContextId(project.Id), Label = project.Name, - IsMiscellaneous = project.Solution.WorkspaceKind == WorkspaceKind.MiscellaneousFiles, + // IsMiscellaneous controls whether a toast appears which warns that editor features are not available. + // In case HasAllInformation is true, though, we do actually have all information needed to light up any features user is trying to use related to the project. + IsMiscellaneous = project.Solution.WorkspaceKind == WorkspaceKind.MiscellaneousFiles && !project.State.HasAllInformation, }; if (project.Language == LanguageNames.CSharp) diff --git a/src/LanguageServer/Protocol/Features/Options/LanguageServerProjectSystemOptionsStorage.cs b/src/LanguageServer/Protocol/Features/Options/LanguageServerProjectSystemOptionsStorage.cs index 04364827cd664..05b65aa324737 100644 --- a/src/LanguageServer/Protocol/Features/Options/LanguageServerProjectSystemOptionsStorage.cs +++ b/src/LanguageServer/Protocol/Features/Options/LanguageServerProjectSystemOptionsStorage.cs @@ -19,4 +19,9 @@ internal static class LanguageServerProjectSystemOptionsStorage /// Whether or not automatic nuget restore is enabled. /// public static readonly Option2 EnableAutomaticRestore = new Option2("dotnet_enable_automatic_restore", defaultValue: true, s_optionGroup); + + /// + /// Whether to use the new 'dotnet run app.cs' (file-based programs) experience. + /// + public static readonly Option2 EnableFileBasedPrograms = new Option2("dotnet_enable_file_based_programs", defaultValue: true, s_optionGroup); } diff --git a/src/LanguageServer/Protocol/Handler/Configuration/DidChangeConfigurationNotificationHandler_OptionList.cs b/src/LanguageServer/Protocol/Handler/Configuration/DidChangeConfigurationNotificationHandler_OptionList.cs index 2a9a61828b0b3..89fa870411f15 100644 --- a/src/LanguageServer/Protocol/Handler/Configuration/DidChangeConfigurationNotificationHandler_OptionList.cs +++ b/src/LanguageServer/Protocol/Handler/Configuration/DidChangeConfigurationNotificationHandler_OptionList.cs @@ -58,6 +58,7 @@ internal sealed partial class DidChangeConfigurationNotificationHandler LspOptionsStorage.LspEnableAutoInsert, LanguageServerProjectSystemOptionsStorage.BinaryLogPath, LanguageServerProjectSystemOptionsStorage.EnableAutomaticRestore, + LanguageServerProjectSystemOptionsStorage.EnableFileBasedPrograms, MetadataAsSourceOptionsStorage.NavigateToSourceLinkAndEmbeddedSources, LspOptionsStorage.LspOrganizeImportsOnFormat, ]; diff --git a/src/LanguageServer/Protocol/Workspaces/ILspMiscellaneousFilesWorkspaceProvider.cs b/src/LanguageServer/Protocol/Workspaces/ILspMiscellaneousFilesWorkspaceProvider.cs index 2b3a00bd63dc6..6635fd32f127c 100644 --- a/src/LanguageServer/Protocol/Workspaces/ILspMiscellaneousFilesWorkspaceProvider.cs +++ b/src/LanguageServer/Protocol/Workspaces/ILspMiscellaneousFilesWorkspaceProvider.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System; +using System.Threading.Tasks; using Microsoft.CodeAnalysis.Text; using Microsoft.CommonLanguageServerProtocol.Framework; using Roslyn.LanguageServer.Protocol; @@ -15,6 +16,18 @@ internal interface ILspMiscellaneousFilesWorkspaceProvider : ILspService /// Returns the actual workspace that the documents are added to or removed from. /// Workspace Workspace { get; } - TextDocument? AddMiscellaneousDocument(DocumentUri uri, SourceText documentText, string languageId, ILspLogger logger); - void TryRemoveMiscellaneousDocument(DocumentUri uri, bool removeFromMetadataWorkspace); + + /// + /// Adds a document to the workspace. Note that the implementation of this method should not depend on anything expensive such as RPC calls. + /// async is used here to allow taking locks asynchronously and "relatively fast" stuff like that. + /// + ValueTask AddMiscellaneousDocumentAsync(DocumentUri uri, SourceText documentText, string languageId, ILspLogger logger); + + /// + /// Removes the document with the given from the workspace. + /// If the workspace already does not contain such a document, does nothing. + /// Note that the implementation of this method should not depend on anything expensive such as RPC calls. + /// async is used here to allow taking locks asynchronously and "relatively fast" stuff like that. + /// + ValueTask TryRemoveMiscellaneousDocumentAsync(DocumentUri uri, bool removeFromMetadataWorkspace); } diff --git a/src/LanguageServer/Protocol/Workspaces/LspMiscellaneousFilesWorkspaceProvider.cs b/src/LanguageServer/Protocol/Workspaces/LspMiscellaneousFilesWorkspaceProvider.cs index db2940f358596..d8ead3e1210c1 100644 --- a/src/LanguageServer/Protocol/Workspaces/LspMiscellaneousFilesWorkspaceProvider.cs +++ b/src/LanguageServer/Protocol/Workspaces/LspMiscellaneousFilesWorkspaceProvider.cs @@ -37,10 +37,13 @@ internal sealed class LspMiscellaneousFilesWorkspaceProvider(ILspServices lspSer /// /// Takes in a file URI and text and creates a misc project and document for the file. /// - /// Calls to this method and are made + /// Calls to this method and are made /// from LSP text sync request handling which do not run concurrently. /// - public TextDocument? AddMiscellaneousDocument(DocumentUri uri, SourceText documentText, string languageId, ILspLogger logger) + public ValueTask AddMiscellaneousDocumentAsync(DocumentUri uri, SourceText documentText, string languageId, ILspLogger logger) + => ValueTaskFactory.FromResult(AddMiscellaneousDocument(uri, documentText, languageId, logger)); + + private TextDocument? AddMiscellaneousDocument(DocumentUri uri, SourceText documentText, string languageId, ILspLogger logger) { var documentFilePath = uri.UriString; if (uri.ParsedUri is not null) @@ -87,11 +90,11 @@ internal sealed class LspMiscellaneousFilesWorkspaceProvider(ILspServices lspSer /// Calls to this method and are made /// from LSP text sync request handling which do not run concurrently. /// - public void TryRemoveMiscellaneousDocument(DocumentUri uri, bool removeFromMetadataWorkspace) + public ValueTask TryRemoveMiscellaneousDocumentAsync(DocumentUri uri, bool removeFromMetadataWorkspace) { if (removeFromMetadataWorkspace && uri.ParsedUri is not null && metadataAsSourceFileService.TryRemoveDocumentFromWorkspace(ProtocolConversions.GetDocumentFilePathFromUri(uri.ParsedUri))) { - return; + return ValueTaskFactory.CompletedTask; } // We'll only ever have a single document matching this URI in the misc solution. @@ -112,6 +115,8 @@ public void TryRemoveMiscellaneousDocument(DocumentUri uri, bool removeFromMetad var project = CurrentSolution.GetRequiredProject(matchingDocument.ProjectId); OnProjectRemoved(project.Id); } + + return ValueTaskFactory.CompletedTask; } public ValueTask UpdateTextIfPresentAsync(DocumentId documentId, SourceText sourceText, CancellationToken cancellationToken) diff --git a/src/LanguageServer/Protocol/Workspaces/LspWorkspaceManager.cs b/src/LanguageServer/Protocol/Workspaces/LspWorkspaceManager.cs index 2201573441f12..2f006afe59b8e 100644 --- a/src/LanguageServer/Protocol/Workspaces/LspWorkspaceManager.cs +++ b/src/LanguageServer/Protocol/Workspaces/LspWorkspaceManager.cs @@ -9,6 +9,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; +using Microsoft.CodeAnalysis.ErrorReporting; using Microsoft.CodeAnalysis.LanguageServer.Handler; using Microsoft.CodeAnalysis.LanguageServer.Handler.DocumentChanges; using Microsoft.CodeAnalysis.PooledObjects; @@ -153,7 +154,17 @@ public async ValueTask StopTrackingAsync(DocumentUri uri, CancellationToken canc _cachedLspSolutions.Clear(); // Also remove it from our loose files or metadata workspace if it is still there. - _lspMiscellaneousFilesWorkspaceProvider?.TryRemoveMiscellaneousDocument(uri, removeFromMetadataWorkspace: true); + if (_lspMiscellaneousFilesWorkspaceProvider is not null) + { + try + { + await _lspMiscellaneousFilesWorkspaceProvider.TryRemoveMiscellaneousDocumentAsync(uri, removeFromMetadataWorkspace: true).ConfigureAwait(false); + } + catch (Exception ex) when (FatalError.ReportAndCatch(ex)) + { + this._logger.LogException(ex); + } + } LspTextChanged?.Invoke(this, EventArgs.Empty); @@ -253,8 +264,18 @@ public void UpdateTrackedDocument(DocumentUri uri, SourceText newSourceText) // if it happens to be in there as well. if (workspace != _lspMiscellaneousFilesWorkspaceProvider?.Workspace) { - // Do not attempt to remove the file from the metadata workspace (the document is still open). - _lspMiscellaneousFilesWorkspaceProvider?.TryRemoveMiscellaneousDocument(uri, removeFromMetadataWorkspace: false); + if (_lspMiscellaneousFilesWorkspaceProvider is not null) + { + try + { + // Do not attempt to remove the file from the metadata workspace (the document is still open). + await _lspMiscellaneousFilesWorkspaceProvider.TryRemoveMiscellaneousDocumentAsync(uri, removeFromMetadataWorkspace: false).ConfigureAwait(false); + } + catch (Exception ex) when (FatalError.ReportAndCatch(ex)) + { + _logger.LogException(ex); + } + } } return (workspace, document.Project.Solution, document); @@ -268,11 +289,18 @@ public void UpdateTrackedDocument(DocumentUri uri, SourceText newSourceText) _requestTelemetryLogger.UpdateFindDocumentTelemetryData(success: false, workspaceKind: null); // Add the document to our loose files workspace (if we have one) if it is open. - if (_trackedDocuments.TryGetValue(uri, out var trackedDocument)) + if (_trackedDocuments.TryGetValue(uri, out var trackedDocument) && _lspMiscellaneousFilesWorkspaceProvider is not null) { - var miscDocument = _lspMiscellaneousFilesWorkspaceProvider?.AddMiscellaneousDocument(uri, trackedDocument.Text, trackedDocument.LanguageId, _logger); - if (miscDocument is not null) - return (miscDocument.Project.Solution.Workspace, miscDocument.Project.Solution, miscDocument); + try + { + var miscDocument = await _lspMiscellaneousFilesWorkspaceProvider.AddMiscellaneousDocumentAsync(uri, trackedDocument.Text, trackedDocument.LanguageId, _logger).ConfigureAwait(false); + if (miscDocument is not null) + return (miscDocument.Project.Solution.Workspace, miscDocument.Project.Solution, miscDocument); + } + catch (Exception ex) when (FatalError.ReportAndCatch(ex)) + { + _logger.LogException(ex); + } } return default; diff --git a/src/LanguageServer/ProtocolUnitTests/Configuration/DidChangeConfigurationNotificationHandlerTest.cs b/src/LanguageServer/ProtocolUnitTests/Configuration/DidChangeConfigurationNotificationHandlerTest.cs index e0afe76a84119..b69153c799cf7 100644 --- a/src/LanguageServer/ProtocolUnitTests/Configuration/DidChangeConfigurationNotificationHandlerTest.cs +++ b/src/LanguageServer/ProtocolUnitTests/Configuration/DidChangeConfigurationNotificationHandlerTest.cs @@ -147,6 +147,7 @@ public void VerifyLspClientOptionNames() "auto_insert.dotnet_enable_auto_insert", "projects.dotnet_binary_log_path", "projects.dotnet_enable_automatic_restore", + "projects.dotnet_enable_file_based_programs", "navigation.dotnet_navigate_to_source_link_and_embedded_sources", "formatting.dotnet_organize_imports_on_format", }; diff --git a/src/VisualStudio/Core/Test.Next/Options/VisualStudioOptionStorageTests.cs b/src/VisualStudio/Core/Test.Next/Options/VisualStudioOptionStorageTests.cs index 69bdea12dfb32..45277c3e2aaec 100644 --- a/src/VisualStudio/Core/Test.Next/Options/VisualStudioOptionStorageTests.cs +++ b/src/VisualStudio/Core/Test.Next/Options/VisualStudioOptionStorageTests.cs @@ -236,6 +236,7 @@ public void OptionHasStorageIfNecessary(string configName) "dotnet_style_prefer_foreach_explicit_cast_in_source", // For a small customer segment, doesn't warrant VS UI. "dotnet_binary_log_path", // VSCode only option for the VS Code project system; does not apply to VS "dotnet_enable_automatic_restore", // VSCode only option for the VS Code project system; does not apply to VS + "dotnet_enable_file_based_programs", // VSCode only option for the VS Code project system; does not apply to VS "dotnet_lsp_using_devkit", // VSCode internal only option. Does not need any UI. "dotnet_enable_references_code_lens", // VSCode only option. Does not apply to VS. "dotnet_enable_tests_code_lens", // VSCode only option. Does not apply to VS.