Skip to content

Remove the CanonicalForked state from LanguageServerProjectLoader#82755

Closed
jasonmalinowski wants to merge 2 commits intodotnet:mainfrom
jasonmalinowski:get-rid-of-canonical-state
Closed

Remove the CanonicalForked state from LanguageServerProjectLoader#82755
jasonmalinowski wants to merge 2 commits intodotnet:mainfrom
jasonmalinowski:get-rid-of-canonical-state

Conversation

@jasonmalinowski
Copy link
Member

@jasonmalinowski jasonmalinowski commented Mar 13, 2026

This simplifies the approach being used for forked canonical projects and brings them back inline with how we do regular projects. A project where we want a forked canonical project for is initialized to being a primordial project like before, and then we update it to a real project via the usual mechanism. The trick is to treat the build host parameter to TryLoadProjectInMSBuildHostAsync as optional -- once we've built the canonical state we just don't need to use it again.

Microsoft Reviewers: Open in CodeFlow
This simplifies the approach being used for forked canonical projects and brings them back inline with how we do regular projects. A project where we want a forked canonical project for is initialized to being a primordial project like before, and then we update it to a real project via the usual mechanism. The trick is to treat the build host parameter to TryLoadProjectInMSBuildHostAsync as optional -- once we've built the canonical state we just don't need to use it again.
@jasonmalinowski jasonmalinowski force-pushed the get-rid-of-canonical-state branch from fdf605d to 2eca09a Compare March 14, 2026 00:25
var primordialDoc = AddPrimordialDocument(uri, documentText, languageId);
Contract.ThrowIfNull(primordialDoc.FilePath);

var doDesignTimeBuild = uri.ParsedUri?.IsFile is true && supportsDesignTimeBuild;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we now want to do DTBs on documents which are not on disk, so that we can hit the path to use a canonical project in TryLoadProjectInMSBuildHostAsync.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the canonical case it's not actually doing a DTB, so the condition could just be updated as needed.

// For example, loose files which don't look like file-based programs, are put in projects forked from the canonical project loader, only when the setting is enabled, etc.
// We anticipate that changing this setting will be infrequent, and, the cost of needing to reload will be acceptable given that.
_logger.LogInformation($"Detected enableFileBasedPrograms changed to '{value}'. Unloading loose file projects.");
// TODO: just reload now
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we expect TryLoadProjectInMSBuildHostAsync to now also handle the case where "enableFileBasedPrograms": false is set? e.g. should it also be able to cook up a misc project with no references in there, use that, and expect ReloadProjectAsync to do whatever necessary updates to the workspace?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That was my expectation, especially since the state to create there is pretty small.

protected override async Task<RemoteProjectLoadResult?> TryLoadProjectInMSBuildHostAsync(
BuildHostProcessManager buildHostProcessManager, string documentPath, CancellationToken cancellationToken)
{
if (!VirtualProjectXmlProvider.IsFileBasedProgram(SourceText.From(File.ReadAllText(documentPath))))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am expecting this check to be replaced with a call to 'ClassifyDocumentAsync', which will properly handle the case where the file doesn't exist.

That check will need to handle the case where the file doesn't exist and also isn't tracked by LSP. I believe unloading the project is the right thing to do in that case.

Rather than having two separate loaders one for canonical-project- based projects and one for other projects, it's easier if we unify down to one, and just return what the project should look like. This creates a separate BuildHostProcessManager but the simplification there makes that OK.
@jasonmalinowski jasonmalinowski force-pushed the get-rid-of-canonical-state branch from 2eca09a to 6683f84 Compare March 16, 2026 21:43
var virtualProjectXml = $"""
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net$(BundledNETCoreAppTargetFrameworkVersion)</TargetFramework>
Copy link
Member

@JoeRobich JoeRobich Mar 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

framework is already a property https://github.com/dotnet/sdk/blob/a6af77909c46799080943ee15c028462867c60f5/src/Layout/redist/targets/GenerateBundledVersions.targets#L647C6-L647C38

Suggested change
<TargetFramework>net$(BundledNETCoreAppTargetFrameworkVersion)</TargetFramework>
<TargetFramework>$(BundledNETCoreAppTargetFramework)</TargetFramework>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll make sure to apply this to #82509

var forkedInfos = canonicalInfos.SelectAsArray(info => info with
{
FilePath = miscDocumentPath,
Documents = info.Documents.Add(miscDocFileInfo),
Copy link
Member

@RikkiGibson RikkiGibson Mar 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this doesn't work when the document doesn't exist on disk. The downstream handling of this data appears to expect to be able to read the file off disk.

 [05:41:50.926] [Error] [LanguageServerProjectLoader] Error while loading untitled:Untitled-1: Exception thrown: System.ArgumentException: Absolute path expected. (Parameter 'path') at Roslyn.Utilities.CompilerPathUtilities.RequireAbsolutePath(String path, String argumentName) in C:\Users\rikki\src\roslyn\src\Workspaces\SharedUtilitiesAndExtensions\Compiler\Core\Utilities\CompilerUtilities\CompilerPathUtilities.cs:line 21 at Microsoft.CodeAnalysis.FileTextLoader..ctor(String path, Encoding defaultEncoding) in C:\Users\rikki\src\roslyn\src\Workspaces\Core\Portable\Workspace\Solution\FileTextLoader.cs:line 49 at Microsoft.CodeAnalysis.WorkspaceFileTextLoader..ctor(SolutionServices services, String path, Encoding defaultEncoding) in C:\Users\rikki\src\roslyn\src\Workspaces\Core\Portable\Workspace\WorkspaceFileTextLoader.cs:line 24 at Microsoft.CodeAnalysis.Workspaces.ProjectSystem.ProjectSystemProjectFactory.CreateFileTextLoader(String fullPath) in C:\Users\rikki\src\roslyn\src\Workspaces\Core\Portable\Workspace\ProjectSystem\ProjectSystemProjectFactory.cs:line 98 at Microsoft.CodeAnalysis.Workspaces.ProjectSystem.ProjectSystemProject.BatchingDocumentCollection.AddFile(String fullPath, SourceCodeKind sourceCodeKind, ImmutableArray`1 folders) in C:\Users\rikki\src\roslyn\src\Workspaces\Core\Portable\Workspace\ProjectSystem\ProjectSystemProject.BatchingDocumentCollection.cs:line 107 at Microsoft.CodeAnalysis.Workspaces.ProjectSystem.ProjectSystemProject.AddSourceFile(String fullPath, SourceCodeKind sourceCodeKind, ImmutableArray`1 folders) in C:\Users\rikki\src\roslyn\src\Workspaces\Core\Portable\Workspace\ProjectSystem\ProjectSystemProject.cs:line 849 at Microsoft.CodeAnalysis.LanguageServer.HostWorkspace.LoadedProject.<>c__DisplayClass28_0.<UpdateWithNewProjectInfoAsync>b__0(DocumentFileInfo document) in C:\Users\rikki\src\roslyn\src\LanguageServer\Microsoft.CodeAnalysis.LanguageServer\HostWorkspace\LoadedProject.cs:line 180 at Microsoft.CodeAnalysis.LanguageServer.HostWorkspace.LoadedProject.<>c__DisplayClass28_1.<UpdateWithNewProjectInfoAsync>g__UpdateProjectSystemProjectCollection|17[T](IEnumerable`1 loadedCollection, IEnumerable`1 oldLoadedCollection, IEqualityComparer`1 comparer, Action`1 addItem, Action`1 removeItem, String logMessage) in C:\Users\rikki\src\roslyn \src\LanguageServer\Microsoft.CodeAnalysis.LanguageServer\HostWorkspace\LoadedProject.cs:line 282 at Microsoft.CodeAnalysis.LanguageServer.HostWorkspace.LoadedProject.UpdateWithNewProjectInfoAsync(ProjectFileInfo newProjectInfo, Boolean isMiscellaneousFile, Boolean hasAllInformation, ILogger logger) in C:\Users\rikki\src\roslyn\src\LanguageServer\Microsoft.CodeAnalysis.LanguageServer\HostWorkspace\LoadedProject.cs:line 176 at Microsoft.CodeAnalysis.LanguageServer.HostWorkspace.LoadedProject.UpdateWithNewProjectInfoAsync(ProjectFileInfo newProjectInfo, Boolean isMiscellaneousFile, Boolean hasAllInformation, ILogger logger) in C:\Users\rikki\src\roslyn\src\LanguageServer\Microsoft.CodeAnalysis.LanguageServer\HostWorkspace\LoadedProject.cs:line 267 at Microsoft.CodeAnalysis.LanguageServer.HostWorkspace.LanguageServerProjectLoader.ReloadProjectAsync(ProjectToLoad projectToLoad, ToastErrorReporter toastErrorReporter, BuildHostProcessManager buildHostProcessManager, CancellationToken cancellationToken) in C:\Users\rikki\src\roslyn\src\LanguageServer\Microsoft.CodeAnalysis.LanguageServer\HostWorks pace\LanguageServerProjectLoader.cs:line 286 

I considered whether ProjectFileInfo or DocumentFileInfo could be adjusted to also hold a SourceTextContainer so that we could ultimately pass a possibly-not-on-disk file to ProjectSystemProject.AddSourceTextContainer.

But, those types are declared in a layer which doesn't have a reference to MS.CA. It felt like adding that reference is not part of the intended layering there.

So, I was considering whether to thread the SourceTextContainer through on the side, and ultimately pass it in to LoadedProject.UpdateWithNewProjectInfoAsync, so that it can call AddSourceTextContainer with it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alternatively, I wonder if handling the "file doesn't exist" case, by inserting a "stub" document or something, and letting the GetLspSolutionForWorkspaceAsync replace it with the up-to-date LSP text, would work.

I don't know if that solution would be felt to be too magical.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But, those types are declared in a layer which doesn't have a reference to MS.CA. It felt like adding that reference is not part of the intended layering there.

Ah, right, we don't put the SourceTexts in that since those are also the types we use to marshal across to the BuildHost, so practically they couldn't fit in there.

by inserting a "stub" document or something, and letting the GetLspSolutionForWorkspaceAsync replace it with the up-to-date LSP text, would work.

That's what I'd expect to happen here -- it doesn't actually matter what text you really put in this document; it'll get replaced by the LSP snapshot anyways. I'd say a simple fix might be that in UpdateWithNewProjectInfoAsync we just say non-absolute paths instead result in a call to AddSourceTextContainer with an empty content.

}
finally
{
Directory.Delete(tempDirectory, recursive: true);
Copy link
Member

@RikkiGibson RikkiGibson Mar 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems like deleting the directory and then applying project updates based on the contents later breaks things fairly deeply as we can't load the files we were interested in when it comes time to actually load those files in the workspaces project.

I suspect that the right thing to do is change back to IDisposable and delete this on shutdown. Let me know if I missed something.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as we can't load the files we were interested in when it comes time to actually load those files in the workspaces project.

Can you clarify what files those are? The only files I could imagine might be getting pulled in would be things like the project.assets.json or maybe output paths, but we should likely be nulling those out anyways or it's strange that we have a bunch of projects with the same information.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For example, Canonical.AssemblyInfo.cs and other files which may be generated during DTB.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah right, I didn't think about those. I'd be OK with either saying:

  1. We just drop all source files (since we don't really need those files)
  2. We just leave the files around until shutdown.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm going with (2) as some of the content feels useful, such as default set of global usings.

@jasonmalinowski
Copy link
Member Author

This PR was ultimately incorporated into #82509.

@jasonmalinowski jasonmalinowski deleted the get-rid-of-canonical-state branch March 20, 2026 22:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

3 participants