-
Notifications
You must be signed in to change notification settings - Fork 4.2k
Use a canonical misc files project for misc files in VSCode #80744
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
Closed
Closed
Changes from 3 commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
a7afe75
Initial plan
Copilot 4e9bb98
Implement canonical misc files project for genuine misc files
Copilot 2119d43
Fix encoding and formatting for canonical misc files implementation
Copilot cda4db7
Add documentation comment for binary log provider
Copilot File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
297 changes: 297 additions & 0 deletions
297
...rver/Microsoft.CodeAnalysis.LanguageServer/FileBasedPrograms/CanonicalMiscFilesProject.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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; | ||
|
|
||
| /// <summary> | ||
| /// 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. | ||
| /// </summary> | ||
| 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"); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Ensures the canonical project is initialized with a design-time build. | ||
| /// </summary> | ||
| public async Task<LoadedProject?> EnsureInitializedAsync( | ||
| BuildHostProcessManager buildHostProcessManager, | ||
| IFileChangeWatcher fileChangeWatcher, | ||
| ImmutableDictionary<string, string> 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; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Adds a document to the canonical project. | ||
| /// </summary> | ||
| public async Task<Document?> 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; | ||
| } | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Removes a document from the canonical project. | ||
| /// </summary> | ||
| public async Task<bool> 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 = $""" | ||
|
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. I believe that since the canonical project and all its on-disk contents are in a temp directory, there shouldn't be any need to do most/all the virtual project stuff that is done for file-based apps. i.e. the project file used for |
||
| <Project> | ||
| <PropertyGroup> | ||
| <BaseIntermediateOutputPath>{SecurityElement.Escape(artifactsPath)}\obj\</BaseIntermediateOutputPath> | ||
| <BaseOutputPath>{SecurityElement.Escape(artifactsPath)}\bin\</BaseOutputPath> | ||
| </PropertyGroup> | ||
| <!-- We need to explicitly import Sdk props/targets so we can override the targets below. --> | ||
| <Import Project="Sdk.props" Sdk="Microsoft.NET.Sdk" /> | ||
| <PropertyGroup> | ||
| <OutputType>Library</OutputType> | ||
| <TargetFramework>{SecurityElement.Escape(targetFramework)}</TargetFramework> | ||
| <ImplicitUsings>enable</ImplicitUsings> | ||
| <Nullable>enable</Nullable> | ||
| </PropertyGroup> | ||
| <PropertyGroup> | ||
| <EnableDefaultItems>false</EnableDefaultItems> | ||
| </PropertyGroup> | ||
| <PropertyGroup> | ||
| <LangVersion>preview</LangVersion> | ||
| </PropertyGroup> | ||
| <ItemGroup> | ||
| <Compile Include="{SecurityElement.Escape(_emptyFilePath)}" /> | ||
| </ItemGroup> | ||
| <Import Project="Sdk.targets" Sdk="Microsoft.NET.Sdk" /> | ||
| <!-- | ||
| Override targets which don't work with project files that are not present on disk. | ||
| See https://github.com/NuGet/Home/issues/14148. | ||
| --> | ||
| <Target Name="_FilterRestoreGraphProjectInputItems" | ||
| DependsOnTargets="_LoadRestoreGraphEntryPoints" | ||
| Returns="@(FilteredRestoreGraphProjectInputItems)"> | ||
| <ItemGroup> | ||
| <FilteredRestoreGraphProjectInputItems Include="@(RestoreGraphProjectInputItems)" /> | ||
| </ItemGroup> | ||
| </Target> | ||
| <Target Name="_GetAllRestoreProjectPathItems" | ||
| DependsOnTargets="_FilterRestoreGraphProjectInputItems" | ||
| Returns="@(_RestoreProjectPathItems)"> | ||
| <ItemGroup> | ||
| <_RestoreProjectPathItems Include="@(FilteredRestoreGraphProjectInputItems)" /> | ||
| </ItemGroup> | ||
| </Target> | ||
| <Target Name="_GenerateRestoreGraph" | ||
| DependsOnTargets="_FilterRestoreGraphProjectInputItems;_GetAllRestoreProjectPathItems;_GenerateRestoreGraphProjectEntry;_GenerateProjectRestoreGraph" | ||
| Returns="@(_RestoreGraphEntry)"> | ||
| <!-- Output from dependency _GenerateRestoreGraphProjectEntry and _GenerateProjectRestoreGraph --> | ||
| </Target> | ||
| </Project> | ||
| """; | ||
|
|
||
| return virtualProjectXml; | ||
| } | ||
|
|
||
| public void Dispose() | ||
| { | ||
| _loadedProject?.Dispose(); | ||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks like the wrong thing. We want to obtain the "Workspaces-layer" Project instance, which is produced as a result/output of the LoadedProject, and create a new derived Project instance containing the user's document, without making any changes to
_loadedProject. Any "blank/meaningless" C# documents which we included in the canonical project, should also not be present in the derived Project instance.