-
Notifications
You must be signed in to change notification settings - Fork 4.2k
Implement canonical miscellaneous files project loader for non-file-based programs #80748
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
Merged
Merged
Changes from 6 commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
3ee0d2f
Initial plan
Copilot 536e568
Implement canonical misc files project loader infrastructure
Copilot e0caa35
Apply whitespace formatting
Copilot bea4add
Address PR feedback: refactor canonical loader implementation
Copilot f290d4f
Cleanup and fix behavior
dibarbet a442418
Derive tfm from loaded sdk
dibarbet b94d56c
review feedback
dibarbet e3ef2f0
more review feedback
dibarbet 20ac9f1
make canonical loader not lazy
dibarbet 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
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
317 changes: 317 additions & 0 deletions
317
...icrosoft.CodeAnalysis.LanguageServer/FileBasedPrograms/CanonicalMiscFilesProjectLoader.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,317 @@ | ||
| // 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.LanguageServer.HostWorkspace; | ||
| using Microsoft.CodeAnalysis.LanguageServer.HostWorkspace.ProjectTelemetry; | ||
| 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.Extensions.Logging; | ||
| using Roslyn.Utilities; | ||
| using static Microsoft.CodeAnalysis.MSBuild.BuildHostProcessManager; | ||
|
|
||
| namespace Microsoft.CodeAnalysis.LanguageServer.FileBasedPrograms; | ||
|
|
||
| /// <summary> | ||
| /// Handles loading miscellaneous files that are not file-based programs. | ||
| /// These files are loaded into a canonical project backed by an empty .cs file in temp. | ||
| /// </summary> | ||
| internal sealed class CanonicalMiscFilesProjectLoader : LanguageServerProjectLoader | ||
| { | ||
| private readonly Lazy<string> _canonicalDocumentPath; | ||
|
|
||
| public CanonicalMiscFilesProjectLoader( | ||
| LanguageServerWorkspaceFactory workspaceFactory, | ||
| IFileChangeWatcher fileChangeWatcher, | ||
| IGlobalOptionService globalOptionService, | ||
| ILoggerFactory loggerFactory, | ||
| IAsynchronousOperationListenerProvider listenerProvider, | ||
| ProjectLoadTelemetryReporter projectLoadTelemetry, | ||
| ServerConfigurationFactory serverConfigurationFactory, | ||
| IBinLogPathProvider binLogPathProvider) | ||
| : base( | ||
| workspaceFactory, | ||
| fileChangeWatcher, | ||
| globalOptionService, | ||
| loggerFactory, | ||
| listenerProvider, | ||
| projectLoadTelemetry, | ||
| serverConfigurationFactory, | ||
| binLogPathProvider) | ||
| { | ||
| _canonicalDocumentPath = new Lazy<string>(() => | ||
| { | ||
| // Create a temp directory for the canonical project | ||
| var tempDirectory = Path.Combine(Path.GetTempPath(), "roslyn-canonical-misc", Guid.NewGuid().ToString()); | ||
dibarbet marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| Directory.CreateDirectory(tempDirectory); | ||
|
|
||
| var documentPath = Path.Combine(tempDirectory, "Canonical.cs"); | ||
|
|
||
| // Create the empty canonical document | ||
| File.WriteAllText(documentPath, string.Empty); | ||
|
|
||
| return documentPath; | ||
| }); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Adds a miscellaneous document to the canonical project. | ||
| /// If the canonical project doesn't exist, creates a primordial project and starts loading the canonical project. | ||
| /// </summary> | ||
| /// <remarks> | ||
| /// The LSP workspace manager and queue ensure that <see cref="AddMiscellaneousDocumentAsync"/> and <see cref="TryRemoveMiscellaneousDocumentAsync"/> are not called concurrently. | ||
| /// </remarks> | ||
| public async ValueTask<TextDocument> AddMiscellaneousDocumentAsync(string documentPath, SourceText documentText, CancellationToken cancellationToken) | ||
| { | ||
| // Project loading happens asynchronously, so we need to execute this under the load gate to ensure consistency. | ||
| return await ExecuteUnderGateAsync(async loadedProjects => | ||
| { | ||
| var canonicalDocumentPath = _canonicalDocumentPath.Value; | ||
|
|
||
| // Check the current state of the canonical project | ||
| if (loadedProjects.TryGetValue(canonicalDocumentPath, out var loadState)) | ||
| { | ||
| if (loadState is ProjectLoadState.LoadedTargets loadedTargets) | ||
| { | ||
| // Case 1: Project is fully loaded with targets | ||
|
|
||
| // We always expect that the canonical project is either Primordial, or loaded with exactly 1 target (1 TFM). | ||
| Contract.ThrowIfFalse(loadedTargets.LoadedProjectTargets.Length == 1, "Expected exactly one loaded target for canonical project"); | ||
| var loadedProjectId = loadedTargets.LoadedProjectTargets.Single().GetProjectSystemProject().Id; | ||
| return await AddDocumentToLoadedProjectAsync(documentPath, documentText, loadedProjectId, cancellationToken); | ||
| } | ||
| else | ||
| { | ||
| // Case 2: Primordial project was already created, but hasn't finished loading. | ||
| var primordialTarget = loadState as ProjectLoadState.Primordial; | ||
| Contract.ThrowIfNull(primordialTarget, "Expected primordial target"); | ||
| return await AddDocumentToPrimordialProjectAsync(documentPath, documentText, primordialTarget.PrimordialProjectId, cancellationToken); | ||
| } | ||
| } | ||
| else | ||
| { | ||
| // Case 3: Project doesn't exist at all | ||
| return CreatePrimordialProjectAndAddDocument(documentPath, documentText); | ||
| } | ||
| }, cancellationToken); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Removes a miscellaneous document from the canonical project. | ||
| /// The canonical project itself is never removed. | ||
| /// </summary> | ||
| /// <remarks> | ||
| /// The LSP workspace manager and queue ensure that <see cref="AddMiscellaneousDocumentAsync"/> and <see cref="TryRemoveMiscellaneousDocumentAsync"/> are not called concurrently. | ||
| /// </remarks> | ||
| public async ValueTask<bool> TryRemoveMiscellaneousDocumentAsync(string documentPath, CancellationToken cancellationToken) | ||
| { | ||
| // Project loading happens asynchronously, so we need to execute this under the load gate to ensure consistency. | ||
| return await ExecuteUnderGateAsync(async loadedProjects => | ||
| { | ||
| // Try to find and remove the document from the miscellaneous workspace only | ||
| var miscWorkspace = _workspaceFactory.MiscellaneousFilesWorkspaceProjectFactory.Workspace; | ||
|
|
||
| var documents = miscWorkspace.CurrentSolution.GetDocumentIdsWithFilePath(documentPath); | ||
| if (documents.Any()) | ||
| { | ||
| await _workspaceFactory.MiscellaneousFilesWorkspaceProjectFactory.ApplyChangeToWorkspaceAsync(workspace => | ||
| { | ||
| foreach (var documentId in documents) | ||
| { | ||
| workspace.OnDocumentRemoved(documentId); | ||
| } | ||
| }, cancellationToken); | ||
|
|
||
| return true; | ||
| } | ||
|
|
||
| return false; | ||
| }, cancellationToken); | ||
| } | ||
|
|
||
| private async ValueTask<TextDocument> AddDocumentToLoadedProjectAsync(string documentPath, SourceText documentText, ProjectId loadedProjectId, CancellationToken cancellationToken) | ||
| { | ||
| var miscWorkspace = _workspaceFactory.MiscellaneousFilesWorkspaceProjectFactory.Workspace; | ||
| var project = miscWorkspace.CurrentSolution.GetProject(loadedProjectId); | ||
|
|
||
| Contract.ThrowIfNull(project, "Canonical project must exist in workspace"); | ||
|
|
||
| var documentInfo = DocumentInfo.Create( | ||
| DocumentId.CreateNewId(project.Id), | ||
| name: Path.GetFileName(documentPath), | ||
| loader: TextLoader.From(TextAndVersion.Create(documentText, VersionStamp.Create())), | ||
| filePath: documentPath); | ||
|
|
||
| Document? addedDocument = null; | ||
dibarbet marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| await _workspaceFactory.MiscellaneousFilesWorkspaceProjectFactory.ApplyChangeToWorkspaceAsync(workspace => | ||
| { | ||
| workspace.OnDocumentAdded(documentInfo); | ||
| addedDocument = workspace.CurrentSolution.GetRequiredDocument(documentInfo.Id); | ||
| }, cancellationToken); | ||
|
|
||
| Contract.ThrowIfNull(addedDocument); | ||
| return addedDocument; | ||
| } | ||
|
|
||
| private async ValueTask<TextDocument> AddDocumentToPrimordialProjectAsync(string documentPath, SourceText documentText, ProjectId primordialProjectId, CancellationToken cancellationToken) | ||
dibarbet marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| { | ||
| var canonicalDocumentPath = _canonicalDocumentPath.Value; | ||
| var miscWorkspace = _workspaceFactory.MiscellaneousFilesWorkspaceProjectFactory.Workspace; | ||
|
|
||
| var documentInfo = DocumentInfo.Create( | ||
| DocumentId.CreateNewId(primordialProjectId), | ||
| name: Path.GetFileName(documentPath), | ||
| loader: TextLoader.From(TextAndVersion.Create(documentText, VersionStamp.Create())), | ||
| filePath: documentPath); | ||
|
|
||
| await _workspaceFactory.MiscellaneousFilesWorkspaceProjectFactory.ApplyChangeToWorkspaceAsync(workspace => | ||
| { | ||
| workspace.OnDocumentAdded(documentInfo); | ||
| }, cancellationToken); | ||
|
|
||
| var addedDocument = miscWorkspace.CurrentSolution.GetRequiredDocument(documentInfo.Id); | ||
| return addedDocument; | ||
| } | ||
|
|
||
| private TextDocument CreatePrimordialProjectAndAddDocument(string documentPath, SourceText documentText) | ||
| { | ||
| var canonicalDocumentPath = _canonicalDocumentPath.Value; | ||
|
|
||
| // Create primordial project with the canonical document | ||
| var canonicalText = SourceText.From(string.Empty); | ||
| var canonicalLoader = new SourceTextLoader(canonicalText, canonicalDocumentPath); | ||
|
|
||
| var projectInfo = MiscellaneousFileUtilities.CreateMiscellaneousProjectInfoForDocument( | ||
| _workspaceFactory.MiscellaneousFilesWorkspaceProjectFactory.Workspace, | ||
| canonicalDocumentPath, | ||
| canonicalLoader, | ||
| new LanguageInformation(LanguageNames.CSharp, scriptExtension: null), | ||
| canonicalText.ChecksumAlgorithm, | ||
| _workspaceFactory.MiscellaneousFilesWorkspaceProjectFactory.Workspace.Services.SolutionServices, | ||
| metadataReferences: []); | ||
|
|
||
| // Add the project first, then add the requested document | ||
| _workspaceFactory.MiscellaneousFilesWorkspaceProjectFactory.ApplyChangeToWorkspace(workspace => | ||
| { | ||
| workspace.OnProjectAdded(projectInfo); | ||
| }); | ||
|
|
||
| // Now add the requested document | ||
| var documentInfo = DocumentInfo.Create( | ||
| DocumentId.CreateNewId(projectInfo.Id), | ||
| name: Path.GetFileName(documentPath), | ||
| loader: new SourceTextLoader(documentText, documentPath), | ||
| filePath: documentPath); | ||
|
|
||
| _workspaceFactory.MiscellaneousFilesWorkspaceProjectFactory.ApplyChangeToWorkspace(workspace => | ||
| { | ||
| workspace.OnDocumentAdded(documentInfo); | ||
| }); | ||
|
|
||
| // Begin loading the canonical project with a design-time build | ||
| BeginLoadingProjectWithPrimordial_NoLock( | ||
dibarbet marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| canonicalDocumentPath, | ||
| _workspaceFactory.MiscellaneousFilesWorkspaceProjectFactory, | ||
| projectInfo.Id, | ||
| doDesignTimeBuild: true); | ||
|
|
||
| // Return the requested document (not the canonical one) | ||
| var miscWorkspace = _workspaceFactory.MiscellaneousFilesWorkspaceProjectFactory.Workspace; | ||
| var addedDocument = miscWorkspace.CurrentSolution.GetRequiredDocument(documentInfo.Id); | ||
| return addedDocument; | ||
| } | ||
|
|
||
| protected override async Task<RemoteProjectLoadResult?> TryLoadProjectInMSBuildHostAsync( | ||
| BuildHostProcessManager buildHostProcessManager, string documentPath, CancellationToken cancellationToken) | ||
| { | ||
| var virtualProjectXml = $""" | ||
| <Project Sdk="Microsoft.NET.Sdk"> | ||
| <PropertyGroup> | ||
| <TargetFramework>net$(BundledNETCoreAppTargetFrameworkVersion)</TargetFramework> | ||
| <ImplicitUsings>enable</ImplicitUsings> | ||
| <Nullable>enable</Nullable> | ||
| </PropertyGroup> | ||
| </Project> | ||
| """; | ||
|
|
||
| // 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 = VirtualProjectXmlProvider.GetVirtualProjectPath(documentPath); | ||
|
|
||
| const BuildHostProcessKind buildHostKind = BuildHostProcessKind.NetCore; | ||
| var buildHost = await buildHostProcessManager.GetBuildHostAsync(buildHostKind, virtualProjectPath, dotnetPath: null, cancellationToken); | ||
| var loadedFile = await buildHost.LoadProjectAsync(virtualProjectPath, virtualProjectXml, languageName: LanguageNames.CSharp, cancellationToken); | ||
|
|
||
| return new RemoteProjectLoadResult | ||
| { | ||
| ProjectFile = loadedFile, | ||
| ProjectFactory = _workspaceFactory.MiscellaneousFilesWorkspaceProjectFactory, | ||
| IsFileBasedProgram = false, | ||
| IsMiscellaneousFile = true, | ||
| PreferredBuildHostKind = buildHostKind, | ||
| ActualBuildHostKind = buildHostKind, | ||
| }; | ||
| } | ||
|
|
||
| protected override ValueTask OnProjectUnloadedAsync(string projectFilePath) | ||
| { | ||
| // Nothing special to do on unload for canonical project | ||
| return ValueTask.CompletedTask; | ||
dibarbet marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| protected override async ValueTask TransitionPrimordialProjectToLoadedAsync( | ||
| string projectPath, | ||
| ProjectSystemProjectFactory primordialProjectFactory, | ||
| ProjectId primordialProjectId, | ||
| CancellationToken cancellationToken) | ||
| { | ||
| // Transfer any misc documents from the primordial project to the loaded canonical project | ||
| var primordialWorkspace = primordialProjectFactory.Workspace; | ||
| var primordialProject = primordialWorkspace.CurrentSolution.GetRequiredProject(primordialProjectId); | ||
|
|
||
| // Get all misc documents (excluding the canonical document) | ||
| var miscDocuments = primordialProject.Documents | ||
| .Where(d => !PathUtilities.Comparer.Equals(d.FilePath, _canonicalDocumentPath.Value)) | ||
| .ToImmutableArray(); | ||
|
|
||
| // Add all misc documents to the loaded project | ||
| var loadedProjectId = GetCanonicalProject().Id; | ||
|
|
||
| foreach (var miscDoc in miscDocuments) | ||
| { | ||
| var text = await miscDoc.GetTextAsync(cancellationToken); | ||
| var documentInfo = DocumentInfo.Create( | ||
| DocumentId.CreateNewId(loadedProjectId), | ||
| name: miscDoc.Name, | ||
| loader: TextLoader.From(TextAndVersion.Create(text, VersionStamp.Create())), | ||
| filePath: miscDoc.FilePath); | ||
|
|
||
| await _workspaceFactory.MiscellaneousFilesWorkspaceProjectFactory.ApplyChangeToWorkspaceAsync(workspace => | ||
| { | ||
| workspace.OnDocumentAdded(documentInfo); | ||
| }, cancellationToken); | ||
| } | ||
|
|
||
| // Now remove the primordial project | ||
| await primordialProjectFactory.ApplyChangeToWorkspaceAsync( | ||
| workspace => workspace.OnProjectRemoved(primordialProjectId), | ||
| cancellationToken); | ||
| } | ||
|
|
||
| private Project GetCanonicalProject() | ||
| { | ||
| var miscWorkspace = _workspaceFactory.MiscellaneousFilesWorkspaceProjectFactory.Workspace; | ||
| var project = miscWorkspace.CurrentSolution.Projects | ||
| .Single(p => PathUtilities.Comparer.Equals(p.FilePath, _canonicalDocumentPath.Value)); | ||
|
|
||
| return project; | ||
| } | ||
| } | ||
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.
Uh oh!
There was an error while loading. Please reload this page.