diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/FileBasedPrograms/CanonicalMiscFilesProject.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/FileBasedPrograms/CanonicalMiscFilesProject.cs
new file mode 100644
index 0000000000000..2eface64befc9
--- /dev/null
+++ b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/FileBasedPrograms/CanonicalMiscFilesProject.cs
@@ -0,0 +1,297 @@
+// 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.Runtime.InteropServices;
+using System.Security;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.Host;
+using Microsoft.CodeAnalysis.LanguageServer.HostWorkspace;
+using Microsoft.CodeAnalysis.MSBuild;
+using Microsoft.CodeAnalysis.ProjectSystem;
+using Microsoft.CodeAnalysis.Text;
+using Microsoft.CodeAnalysis.Threading;
+using Microsoft.CodeAnalysis.Workspaces.ProjectSystem;
+using Microsoft.Extensions.Logging;
+using Roslyn.Utilities;
+using static Microsoft.CodeAnalysis.MSBuild.BuildHostProcessManager;
+
+namespace Microsoft.CodeAnalysis.LanguageServer.FileBasedPrograms;
+
+///
+/// Manages a canonical miscellaneous files project that is shared across all genuine miscellaneous files.
+/// This avoids running a design-time build for each individual misc file.
+///
+internal sealed class CanonicalMiscFilesProject
+{
+ private readonly LanguageServerWorkspaceFactory _workspaceFactory;
+ private readonly ILogger _logger;
+ private readonly SemaphoreSlim _gate = new(initialCount: 1);
+ private readonly string _canonicalProjectPath;
+ private readonly string _emptyFilePath;
+
+ // State protected by _gate
+ private LoadedProject? _loadedProject;
+ private bool _hasBeenInitialized;
+
+ public ProjectId? Id => _loadedProject?.ProjectSystemProject.Id;
+
+ public CanonicalMiscFilesProject(
+ LanguageServerWorkspaceFactory workspaceFactory,
+ ILogger logger)
+ {
+ _workspaceFactory = workspaceFactory;
+ _logger = logger;
+
+ // Create a temp location for the canonical project
+ var tempDirectory = GetCanonicalProjectDirectory();
+ Directory.CreateDirectory(tempDirectory);
+
+ _emptyFilePath = Path.Combine(tempDirectory, "EmptyFile.cs");
+ _canonicalProjectPath = Path.Combine(tempDirectory, "CanonicalMiscFiles.csproj");
+
+ // Create an empty file for the initial build
+ File.WriteAllText(_emptyFilePath, string.Empty);
+ }
+
+ private static string GetCanonicalProjectDirectory()
+ {
+ string baseDirectory = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
+ ? Path.GetTempPath()
+ : Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
+
+ return Path.Combine(baseDirectory, "roslyn-lsp", "canonical-misc-files");
+ }
+
+ ///
+ /// Ensures the canonical project is initialized with a design-time build.
+ ///
+ public async Task EnsureInitializedAsync(
+ BuildHostProcessManager buildHostProcessManager,
+ IFileChangeWatcher fileChangeWatcher,
+ ImmutableDictionary additionalProperties,
+ CancellationToken cancellationToken)
+ {
+ using (await _gate.DisposableWaitAsync(cancellationToken))
+ {
+ if (_hasBeenInitialized)
+ {
+ return _loadedProject;
+ }
+
+ _hasBeenInitialized = true;
+
+ try
+ {
+ // Create the project XML content
+ var projectContent = CreateCanonicalProjectContent();
+
+ // Write the project file to disk
+ File.WriteAllText(_canonicalProjectPath, projectContent);
+
+ // Perform the design-time build
+ const BuildHostProcessKind buildHostKind = BuildHostProcessKind.NetCore;
+ var buildHost = await buildHostProcessManager.GetBuildHostAsync(buildHostKind, _canonicalProjectPath, dotnetPath: null, cancellationToken);
+ var loadedFile = await buildHost.LoadProjectAsync(_canonicalProjectPath, projectContent, languageName: LanguageNames.CSharp, cancellationToken);
+
+ var diagnosticLogItems = await loadedFile.GetDiagnosticLogItemsAsync(cancellationToken);
+ if (diagnosticLogItems.Any(item => item.Kind is DiagnosticLogItemKind.Error))
+ {
+ foreach (var diagnostic in diagnosticLogItems)
+ {
+ _logger.LogError($"Error loading canonical misc files project: {diagnostic.Message}");
+ }
+ return null;
+ }
+
+ var loadedProjectInfos = await loadedFile.GetProjectFileInfosAsync(cancellationToken);
+ if (loadedProjectInfos.Length == 0)
+ {
+ _logger.LogError("No project info loaded for canonical misc files project");
+ return null;
+ }
+
+ var projectInfo = loadedProjectInfos[0];
+ var projectFactory = _workspaceFactory.MiscellaneousFilesWorkspaceProjectFactory;
+
+ // Create the project in the workspace
+ var projectSystemName = "Canonical Miscellaneous Files";
+ var projectCreationInfo = new ProjectSystemProjectCreationInfo
+ {
+ AssemblyName = projectSystemName,
+ FilePath = _canonicalProjectPath,
+ CompilationOutputAssemblyFilePath = projectInfo.IntermediateOutputFilePath,
+ };
+
+ var projectSystemProject = await projectFactory.CreateAndAddToWorkspaceAsync(
+ projectSystemName,
+ LanguageNames.CSharp,
+ projectCreationInfo,
+ _workspaceFactory.ProjectSystemHostInfo);
+
+ _loadedProject = new LoadedProject(
+ projectSystemProject,
+ projectFactory,
+ fileChangeWatcher,
+ _workspaceFactory.TargetFrameworkManager);
+
+ // Update the project with the build information
+ await _loadedProject.UpdateWithNewProjectInfoAsync(projectInfo, isMiscellaneousFile: true, _logger);
+
+ // Remove the empty file that was used for the initial build
+ var workspace = projectFactory.Workspace;
+ var project = workspace.CurrentSolution.GetProject(_loadedProject.ProjectSystemProject.Id);
+ if (project != null)
+ {
+ var emptyDocument = project.Documents.FirstOrDefault(d => d.FilePath == _emptyFilePath);
+ if (emptyDocument != null)
+ {
+ await projectFactory.ApplyChangeToWorkspaceAsync(
+ w => w.OnDocumentRemoved(emptyDocument.Id),
+ cancellationToken);
+ }
+ }
+
+ _logger.LogInformation("Canonical miscellaneous files project initialized successfully");
+ return _loadedProject;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Failed to initialize canonical miscellaneous files project");
+ return null;
+ }
+ }
+ }
+
+ ///
+ /// Adds a document to the canonical project.
+ ///
+ public async Task AddDocumentAsync(
+ string filePath,
+ SourceText sourceText,
+ CancellationToken cancellationToken)
+ {
+ using (await _gate.DisposableWaitAsync(cancellationToken))
+ {
+ if (_loadedProject == null)
+ {
+ return null;
+ }
+
+ var projectFactory = _workspaceFactory.MiscellaneousFilesWorkspaceProjectFactory;
+ var workspace = projectFactory.Workspace;
+ var project = workspace.CurrentSolution.GetProject(_loadedProject.ProjectSystemProject.Id);
+ if (project == null)
+ {
+ _logger.LogError("Canonical project not found in workspace");
+ return null;
+ }
+
+ // Add the source file to the project
+ _loadedProject.ProjectSystemProject.AddSourceFile(filePath);
+
+ // Get the updated project and document
+ var updatedProject = workspace.CurrentSolution.GetProject(_loadedProject.ProjectSystemProject.Id);
+ var document = updatedProject?.Documents.FirstOrDefault(d => d.FilePath == filePath);
+
+ return document;
+ }
+ }
+
+ ///
+ /// Removes a document from the canonical project.
+ ///
+ public async Task RemoveDocumentAsync(string filePath, CancellationToken cancellationToken)
+ {
+ using (await _gate.DisposableWaitAsync(cancellationToken))
+ {
+ if (_loadedProject == null)
+ {
+ return false;
+ }
+
+ var projectFactory = _workspaceFactory.MiscellaneousFilesWorkspaceProjectFactory;
+ var workspace = projectFactory.Workspace;
+ var project = workspace.CurrentSolution.GetProject(_loadedProject.ProjectSystemProject.Id);
+ if (project == null)
+ {
+ return false;
+ }
+
+ var document = project.Documents.FirstOrDefault(d => d.FilePath == filePath);
+ if (document == null)
+ {
+ return false;
+ }
+
+ // Remove the source file from the project
+ _loadedProject.ProjectSystemProject.RemoveSourceFile(filePath);
+
+ return true;
+ }
+ }
+
+ private string CreateCanonicalProjectContent()
+ {
+ var artifactsPath = GetCanonicalProjectDirectory();
+ var targetFramework = Environment.GetEnvironmentVariable("DOTNET_RUN_FILE_TFM") ?? "net$(BundledNETCoreAppTargetFrameworkVersion)";
+
+ var virtualProjectXml = $"""
+
+
+ {SecurityElement.Escape(artifactsPath)}\obj\
+ {SecurityElement.Escape(artifactsPath)}\bin\
+
+
+
+
+ Library
+ {SecurityElement.Escape(targetFramework)}
+ enable
+ enable
+
+
+ false
+
+
+ preview
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <_RestoreProjectPathItems Include="@(FilteredRestoreGraphProjectInputItems)" />
+
+
+
+
+
+
+ """;
+
+ return virtualProjectXml;
+ }
+
+ public void Dispose()
+ {
+ _loadedProject?.Dispose();
+ }
+}
diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/FileBasedPrograms/FileBasedProgramsProjectSystem.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/FileBasedPrograms/FileBasedProgramsProjectSystem.cs
index e49078d1fe8b9..5a8770441e1d3 100644
--- a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/FileBasedPrograms/FileBasedProgramsProjectSystem.cs
+++ b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/FileBasedPrograms/FileBasedProgramsProjectSystem.cs
@@ -12,9 +12,11 @@
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Shared.TestHooks;
using Microsoft.CodeAnalysis.Text;
+using Microsoft.CodeAnalysis.Threading;
using Microsoft.CommonLanguageServerProtocol.Framework;
using Microsoft.Extensions.Logging;
using Roslyn.LanguageServer.Protocol;
+using Roslyn.Utilities;
using static Microsoft.CodeAnalysis.MSBuild.BuildHostProcessManager;
namespace Microsoft.CodeAnalysis.LanguageServer.FileBasedPrograms;
@@ -25,6 +27,9 @@ internal sealed class FileBasedProgramsProjectSystem : LanguageServerProjectLoad
private readonly ILspServices _lspServices;
private readonly ILogger _logger;
private readonly VirtualProjectXmlProvider _projectXmlProvider;
+ private readonly IFileChangeWatcher _fileChangeWatcher;
+ private CanonicalMiscFilesProject? _canonicalMiscFilesProject;
+ private readonly SemaphoreSlim _canonicalProjectGate = new(initialCount: 1);
public FileBasedProgramsProjectSystem(
ILspServices lspServices,
@@ -50,17 +55,56 @@ public FileBasedProgramsProjectSystem(
_lspServices = lspServices;
_logger = loggerFactory.CreateLogger();
_projectXmlProvider = projectXmlProvider;
+ _fileChangeWatcher = fileChangeWatcher;
}
private string GetDocumentFilePath(DocumentUri uri) => uri.ParsedUri is { } parsedUri ? ProtocolConversions.GetDocumentFilePathFromUri(parsedUri) : uri.UriString;
+ private async ValueTask AddMiscellaneousDocumentUsingCanonicalProjectAsync(string documentFilePath, SourceText documentText, CancellationToken cancellationToken)
+ {
+ using (await _canonicalProjectGate.DisposableWaitAsync(cancellationToken))
+ {
+ // Initialize the canonical project on first use
+ if (_canonicalMiscFilesProject == null)
+ {
+ _canonicalMiscFilesProject = new CanonicalMiscFilesProject(_workspaceFactory, _logger);
+
+ // Perform the initial design-time build
+ // Note: We pass null for binaryLogPathProvider since it's private in the base class.
+ // Binary logging is optional and primarily used for diagnostics.
+ await using var buildHostProcessManager = new BuildHostProcessManager(globalMSBuildProperties: AdditionalProperties, binaryLogPathProvider: null, loggerFactory: LoggerFactory);
+ var loadedProject = await _canonicalMiscFilesProject.EnsureInitializedAsync(buildHostProcessManager, _fileChangeWatcher, AdditionalProperties, cancellationToken);
+
+ if (loadedProject == null)
+ {
+ _logger.LogError("Failed to initialize canonical miscellaneous files project. Falling back to per-file approach.");
+ _canonicalMiscFilesProject = null;
+ return null;
+ }
+ }
+
+ // Add the document to the canonical project
+ var document = await _canonicalMiscFilesProject.AddDocumentAsync(documentFilePath, documentText, cancellationToken);
+ return document;
+ }
+ }
+
public async ValueTask IsMiscellaneousFilesDocumentAsync(TextDocument document, CancellationToken cancellationToken)
{
- // There are two cases here: if it's a primordial document, it'll be in the MiscellaneousFilesWorkspace and thus we definitely know it's
- // a miscellaneous file. Otherwise, it might be a file-based program that we loaded in the main workspace; in this case, the project's path
+ // Check if it's in the miscellaneous files workspace (either canonical project or primordial)
+ if (document.Project.Solution.Workspace == _workspaceFactory.MiscellaneousFilesWorkspaceProjectFactory.Workspace)
+ {
+ return true;
+ }
+
+ // Check if it's a file-based program that we loaded in the main workspace; in this case, the project's path
// is also the source file path, and that's what we consider the 'project' path that is loaded.
- return document.Project.Solution.Workspace == _workspaceFactory.MiscellaneousFilesWorkspaceProjectFactory.Workspace ||
- document.Project.FilePath is not null && await IsProjectLoadedAsync(document.Project.FilePath, cancellationToken);
+ if (document.Project.FilePath is not null && await IsProjectLoadedAsync(document.Project.FilePath, cancellationToken))
+ {
+ return true;
+ }
+
+ return false;
}
public async ValueTask AddMiscellaneousDocumentAsync(DocumentUri uri, SourceText documentText, string languageId, ILspLogger logger)
@@ -72,12 +116,32 @@ public async ValueTask IsMiscellaneousFilesDocumentAsync(TextDocument docu
Contract.Fail($"Could not find language information for {uri} with absolute path {documentFilePath}");
}
- var primordialDoc = AddPrimordialDocument(uri, documentText, languageId);
- Contract.ThrowIfNull(primordialDoc.FilePath);
-
var doDesignTimeBuild = uri.ParsedUri?.IsFile is true
&& languageInformation.LanguageName == LanguageNames.CSharp
&& GlobalOptionService.GetOption(LanguageServerProjectSystemOptionsStorage.EnableFileBasedPrograms);
+
+ // Check if this is a file-based program by parsing the text
+ bool isFileBasedProgram = false;
+ if (doDesignTimeBuild)
+ {
+ isFileBasedProgram = VirtualProjectXmlProvider.IsFileBasedProgram(documentFilePath, documentText);
+ }
+
+ // For genuine miscellaneous files (not file-based programs), try to use the canonical project
+ if (doDesignTimeBuild && !isFileBasedProgram)
+ {
+ var canonicalDocument = await AddMiscellaneousDocumentUsingCanonicalProjectAsync(documentFilePath, documentText, cancellationToken: CancellationToken.None);
+ if (canonicalDocument != null)
+ {
+ return canonicalDocument;
+ }
+ // If canonical project initialization failed, fall through to use the existing approach
+ }
+
+ // For file-based programs or when the feature is disabled, use the existing approach
+ var primordialDoc = AddPrimordialDocument(uri, documentText, languageId);
+ Contract.ThrowIfNull(primordialDoc.FilePath);
+
await BeginLoadingProjectWithPrimordialAsync(primordialDoc.FilePath, _workspaceFactory.MiscellaneousFilesWorkspaceProjectFactory, primordialProjectId: primordialDoc.Project.Id, doDesignTimeBuild);
return primordialDoc;
@@ -107,6 +171,21 @@ TextDocument AddPrimordialDocument(DocumentUri uri, SourceText documentText, str
public async ValueTask TryRemoveMiscellaneousDocumentAsync(DocumentUri uri)
{
var documentPath = GetDocumentFilePath(uri);
+
+ // First try to remove from the canonical project
+ using (await _canonicalProjectGate.DisposableWaitAsync(CancellationToken.None))
+ {
+ if (_canonicalMiscFilesProject != null)
+ {
+ var removedFromCanonical = await _canonicalMiscFilesProject.RemoveDocumentAsync(documentPath, CancellationToken.None);
+ if (removedFromCanonical)
+ {
+ return true;
+ }
+ }
+ }
+
+ // If not in canonical project, try the per-file approach
return await TryUnloadProjectAsync(documentPath);
}
diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/LoadedProject.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/LoadedProject.cs
index 6f5dc4fdf3300..0f75d44c351b9 100644
--- a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/LoadedProject.cs
+++ b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/LoadedProject.cs
@@ -26,6 +26,7 @@ internal sealed class LoadedProject : IDisposable
private readonly string _projectDirectory;
private readonly ProjectSystemProject _projectSystemProject;
+ public ProjectSystemProject ProjectSystemProject => _projectSystemProject;
public ProjectSystemProjectFactory ProjectFactory { get; }
private readonly ProjectSystemProjectOptionsProcessor _optionsProcessor;
private readonly IFileChangeContext _sourceFileChangeContext;