Skip to content

Commit 2d7fd4c

Browse files
Allow LanguageServerProjectSystems to load into more than one workspace
This allows us to place file-based programs in the host workspace so project-to-project references work; truly "miscellaneous files" projects will still stay in the MiscellaneousFiles workspace.
1 parent 6ae6778 commit 2d7fd4c

File tree

7 files changed

+80
-63
lines changed

7 files changed

+80
-63
lines changed

src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/FileBasedPrograms/FileBasedProgramsProjectSystem.cs

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,8 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33
// See the LICENSE file in the project root for more information.
44

5-
using System.Collections.Immutable;
6-
using System.Security;
75
using Microsoft.CodeAnalysis;
86
using Microsoft.CodeAnalysis.Features.Workspaces;
9-
using Microsoft.CodeAnalysis.Host;
107
using Microsoft.CodeAnalysis.LanguageServer.HostWorkspace;
118
using Microsoft.CodeAnalysis.LanguageServer.HostWorkspace.ProjectTelemetry;
129
using Microsoft.CodeAnalysis.MetadataAsSource;
@@ -16,12 +13,9 @@
1613
using Microsoft.CodeAnalysis.Shared.Extensions;
1714
using Microsoft.CodeAnalysis.Shared.TestHooks;
1815
using Microsoft.CodeAnalysis.Text;
19-
using Microsoft.CodeAnalysis.Workspaces.ProjectSystem;
2016
using Microsoft.CommonLanguageServerProtocol.Framework;
2117
using Microsoft.Extensions.Logging;
22-
using Microsoft.VisualStudio.Composition;
2318
using Roslyn.LanguageServer.Protocol;
24-
using Roslyn.Utilities;
2519
using static Microsoft.CodeAnalysis.MSBuild.BuildHostProcessManager;
2620

2721
namespace Microsoft.CodeAnalysis.LanguageServer.FileBasedPrograms;
@@ -33,6 +27,7 @@ internal sealed class FileBasedProgramsProjectSystem : LanguageServerProjectLoad
3327
private readonly ILogger<FileBasedProgramsProjectSystem> _logger;
3428
private readonly IMetadataAsSourceFileService _metadataAsSourceFileService;
3529
private readonly VirtualProjectXmlProvider _projectXmlProvider;
30+
private readonly LanguageServerWorkspaceFactory _workspaceFactory;
3631

3732
public FileBasedProgramsProjectSystem(
3833
ILspServices lspServices,
@@ -47,7 +42,6 @@ public FileBasedProgramsProjectSystem(
4742
ServerConfigurationFactory serverConfigurationFactory,
4843
IBinLogPathProvider binLogPathProvider)
4944
: base(
50-
workspaceFactory.FileBasedProgramsProjectFactory,
5145
workspaceFactory.TargetFrameworkManager,
5246
workspaceFactory.ProjectSystemHostInfo,
5347
fileChangeWatcher,
@@ -62,12 +56,16 @@ public FileBasedProgramsProjectSystem(
6256
_logger = loggerFactory.CreateLogger<FileBasedProgramsProjectSystem>();
6357
_metadataAsSourceFileService = metadataAsSourceFileService;
6458
_projectXmlProvider = projectXmlProvider;
59+
_workspaceFactory = workspaceFactory;
6560
}
6661

67-
public Workspace Workspace => ProjectFactory.Workspace;
68-
6962
private string GetDocumentFilePath(DocumentUri uri) => uri.ParsedUri is { } parsedUri ? ProtocolConversions.GetDocumentFilePathFromUri(parsedUri) : uri.UriString;
7063

64+
public async ValueTask<bool> IsMiscellaneousFilesDocumentAsync(TextDocument document, CancellationToken cancellationToken)
65+
{
66+
return document.Project.FilePath is not null && await IsProjectLoadedAsync(document.Project.FilePath, cancellationToken);
67+
}
68+
7169
public async ValueTask<TextDocument?> AddMiscellaneousDocumentAsync(DocumentUri uri, SourceText documentText, string languageId, ILspLogger logger)
7270
{
7371
var documentFilePath = GetDocumentFilePath(uri);
@@ -86,7 +84,7 @@ public FileBasedProgramsProjectSystem(
8684
var doDesignTimeBuild = uri.ParsedUri?.IsFile is true
8785
&& primordialDoc.Project.Language == LanguageNames.CSharp
8886
&& GlobalOptionService.GetOption(LanguageServerProjectSystemOptionsStorage.EnableFileBasedPrograms);
89-
await BeginLoadingProjectWithPrimordialAsync(primordialDoc.FilePath, primordialProjectId: primordialDoc.Project.Id, doDesignTimeBuild);
87+
await BeginLoadingProjectWithPrimordialAsync(primordialDoc.FilePath, _workspaceFactory.FileBasedProgramsProjectFactory, primordialProjectId: primordialDoc.Project.Id, doDesignTimeBuild);
9088

9189
return primordialDoc;
9290

@@ -98,12 +96,12 @@ TextDocument AddPrimordialDocument(DocumentUri uri, SourceText documentText, str
9896
Contract.Fail($"Could not find language information for {uri} with absolute path {documentFilePath}");
9997
}
10098

101-
var workspace = Workspace;
99+
var workspace = _workspaceFactory.FileBasedProgramsProjectFactory.Workspace;
102100
var sourceTextLoader = new SourceTextLoader(documentText, documentFilePath);
103101
var projectInfo = MiscellaneousFileUtilities.CreateMiscellaneousProjectInfoForDocument(
104102
workspace, documentFilePath, sourceTextLoader, languageInformation, documentText.ChecksumAlgorithm, workspace.Services.SolutionServices, []);
105103

106-
ProjectFactory.ApplyChangeToWorkspace(workspace => workspace.OnProjectAdded(projectInfo));
104+
_workspaceFactory.FileBasedProgramsProjectFactory.ApplyChangeToWorkspace(workspace => workspace.OnProjectAdded(projectInfo));
107105

108106
// https://github.com/dotnet/roslyn/pull/78267
109107
// Work around an issue where opening a Razor file in the misc workspace causes a crash.
@@ -151,13 +149,21 @@ public async ValueTask TryRemoveMiscellaneousDocumentAsync(DocumentUri uri, bool
151149
// This is necessary in order to get msbuild to apply the standard c# props/targets to the project.
152150
var virtualProjectPath = VirtualProjectXmlProvider.GetVirtualProjectPath(documentPath);
153151

154-
var loader = ProjectFactory.CreateFileTextLoader(documentPath);
152+
var loader = _workspaceFactory.FileBasedProgramsProjectFactory.CreateFileTextLoader(documentPath);
155153
var textAndVersion = await loader.LoadTextAsync(new LoadTextOptions(SourceHashAlgorithms.Default), cancellationToken);
156154
var isFileBasedProgram = VirtualProjectXmlProvider.IsFileBasedProgram(documentPath, textAndVersion.Text);
157155

158156
const BuildHostProcessKind buildHostKind = BuildHostProcessKind.NetCore;
159157
var buildHost = await buildHostProcessManager.GetBuildHostAsync(buildHostKind, cancellationToken);
160158
var loadedFile = await buildHost.LoadProjectAsync(virtualProjectPath, virtualProjectContent, languageName: LanguageNames.CSharp, cancellationToken);
161-
return new RemoteProjectLoadResult(loadedFile, HasAllInformation: isFileBasedProgram, Preferred: buildHostKind, Actual: buildHostKind);
159+
160+
return new RemoteProjectLoadResult(
161+
loadedFile,
162+
// If it's a proper file based program, we'll put it in the main host workspace factory since we want cross-project references to work.
163+
// Otherwise, we'll keep it in miscellaneous files.
164+
ProjectFactory: isFileBasedProgram ? _workspaceFactory.HostProjectFactory : _workspaceFactory.FileBasedProgramsProjectFactory,
165+
HasAllInformation: isFileBasedProgram,
166+
Preferred: buildHostKind,
167+
Actual: buildHostKind);
162168
}
163169
}

src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/LanguageServerProjectLoader.cs

Lines changed: 23 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,9 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33
// See the LICENSE file in the project root for more information.
44

5-
using System.Collections.Concurrent;
65
using System.Collections.Immutable;
76
using System.Diagnostics;
8-
using System.IO;
9-
using System.Threading;
10-
using System.Threading.Tasks;
117
using Microsoft.CodeAnalysis.Collections;
12-
using Microsoft.CodeAnalysis.CSharp;
138
using Microsoft.CodeAnalysis.Host;
149
using Microsoft.CodeAnalysis.LanguageServer.Handler.DebugConfiguration;
1510
using Microsoft.CodeAnalysis.LanguageServer.HostWorkspace.ProjectTelemetry;
@@ -21,7 +16,6 @@
2116
using Microsoft.CodeAnalysis.Shared.Extensions;
2217
using Microsoft.CodeAnalysis.Shared.TestHooks;
2318
using Microsoft.CodeAnalysis.Shared.Utilities;
24-
using Microsoft.CodeAnalysis.Text;
2519
using Microsoft.CodeAnalysis.Threading;
2620
using Microsoft.CodeAnalysis.Workspaces.ProjectSystem;
2721
using Microsoft.Extensions.Logging;
@@ -35,7 +29,6 @@ internal abstract class LanguageServerProjectLoader
3529
{
3630
private readonly AsyncBatchingWorkQueue<ProjectToLoad> _projectsToReload;
3731

38-
protected readonly ProjectSystemProjectFactory ProjectFactory;
3932
private readonly ProjectTargetFrameworkManager _targetFrameworkManager;
4033
private readonly ProjectSystemHostInfo _projectSystemHostInfo;
4134
private readonly IFileChangeWatcher _fileChangeWatcher;
@@ -76,7 +69,7 @@ private ProjectLoadState() { }
7669
/// ID of the project which LSP uses to fulfill requests until the first design-time build is complete.
7770
/// The project with this ID is removed from the workspace when unloading or when transitioning to <see cref="LoadedTargets"/> state.
7871
/// </param>
79-
public sealed record Primordial(ProjectId PrimordialProjectId) : ProjectLoadState;
72+
public sealed record Primordial(ProjectSystemProjectFactory Factory, ProjectId PrimordialProjectId) : ProjectLoadState;
8073

8174
/// <summary>
8275
/// Represents a project for which we have loaded zero or more targets.
@@ -89,7 +82,6 @@ public sealed record LoadedTargets(ImmutableArray<LoadedProject> LoadedProjectTa
8982
}
9083

9184
protected LanguageServerProjectLoader(
92-
ProjectSystemProjectFactory projectFactory,
9385
ProjectTargetFrameworkManager targetFrameworkManager,
9486
ProjectSystemHostInfo projectSystemHostInfo,
9587
IFileChangeWatcher fileChangeWatcher,
@@ -100,7 +92,6 @@ protected LanguageServerProjectLoader(
10092
ServerConfigurationFactory serverConfigurationFactory,
10193
IBinLogPathProvider binLogPathProvider)
10294
{
103-
ProjectFactory = projectFactory;
10495
_targetFrameworkManager = targetFrameworkManager;
10596
_projectSystemHostInfo = projectSystemHostInfo;
10697
_fileChangeWatcher = fileChangeWatcher;
@@ -109,7 +100,6 @@ protected LanguageServerProjectLoader(
109100
_logger = loggerFactory.CreateLogger(nameof(LanguageServerProjectLoader));
110101
_projectLoadTelemetryReporter = projectLoadTelemetry;
111102
_binLogPathProvider = binLogPathProvider;
112-
var workspace = projectFactory.Workspace;
113103
var razorDesignTimePath = serverConfigurationFactory.ServerConfiguration?.RazorDesignTimePath;
114104

115105
AdditionalProperties = razorDesignTimePath is null
@@ -180,7 +170,7 @@ private async ValueTask ReloadProjectsAsync(ImmutableSegmentedList<ProjectToLoad
180170
}
181171
}
182172

183-
protected sealed record RemoteProjectLoadResult(RemoteProjectFile ProjectFile, bool HasAllInformation, BuildHostProcessKind Preferred, BuildHostProcessKind Actual);
173+
protected sealed record RemoteProjectLoadResult(RemoteProjectFile ProjectFile, ProjectSystemProjectFactory ProjectFactory, bool HasAllInformation, BuildHostProcessKind Preferred, BuildHostProcessKind Actual);
184174

185175
/// <summary>Loads a project in the MSBuild host.</summary>
186176
/// <remarks>Caller needs to catch exceptions to avoid bringing down the project loader queue.</remarks>
@@ -215,7 +205,7 @@ private async Task<bool> ReloadProjectAsync(ProjectToLoad projectToLoad, ToastEr
215205
return false;
216206
}
217207

218-
(RemoteProjectFile remoteProjectFile, bool hasAllInformation, BuildHostProcessKind preferredBuildHostKind, BuildHostProcessKind actualBuildHostKind) = remoteProjectLoadResult;
208+
(RemoteProjectFile remoteProjectFile, ProjectSystemProjectFactory projectFactory, bool hasAllInformation, BuildHostProcessKind preferredBuildHostKind, BuildHostProcessKind actualBuildHostKind) = remoteProjectLoadResult;
219209
if (preferredBuildHostKind != actualBuildHostKind)
220210
preferredBuildHostKindThatWeDidNotGet = preferredBuildHostKind;
221211

@@ -232,7 +222,7 @@ private async Task<bool> ReloadProjectAsync(ProjectToLoad projectToLoad, ToastEr
232222
// The out-of-proc build host supports more languages than we may actually have Workspace binaries for, so ensure we can actually process that
233223
// language in-process.
234224
var projectLanguage = loadedProjectInfos.FirstOrDefault()?.Language;
235-
if (projectLanguage != null && ProjectFactory.Workspace.Services.GetLanguageService<ICommandLineParserService>(projectLanguage) == null)
225+
if (projectLanguage != null && projectFactory.Workspace.Services.GetLanguageService<ICommandLineParserService>(projectLanguage) == null)
236226
{
237227
return false;
238228
}
@@ -252,7 +242,7 @@ private async Task<bool> ReloadProjectAsync(ProjectToLoad projectToLoad, ToastEr
252242
var newProjectTargetsBuilder = ArrayBuilder<LoadedProject>.GetInstance(loadedProjectInfos.Length);
253243
foreach (var loadedProjectInfo in loadedProjectInfos)
254244
{
255-
var (target, targetAlreadyExists) = await GetOrCreateProjectTargetAsync(previousProjectTargets, loadedProjectInfo);
245+
var (target, targetAlreadyExists) = await GetOrCreateProjectTargetAsync(previousProjectTargets, projectFactory, loadedProjectInfo);
256246
newProjectTargetsBuilder.Add(target);
257247

258248
var (targetTelemetryInfo, targetNeedsRestore) = await target.UpdateWithNewProjectInfoAsync(loadedProjectInfo, hasAllInformation, _logger);
@@ -278,13 +268,13 @@ private async Task<bool> ReloadProjectAsync(ProjectToLoad projectToLoad, ToastEr
278268
await _projectLoadTelemetryReporter.ReportProjectLoadTelemetryAsync(telemetryInfos, projectToLoad, cancellationToken);
279269
}
280270

281-
if (currentLoadState is ProjectLoadState.Primordial(var projectId))
271+
if (currentLoadState is ProjectLoadState.Primordial(var primordialProjectFactory, var projectId))
282272
{
283273
// Remove the primordial project now that the design-time build pass is finished. This ensures that
284274
// we have the new project in place before we remove the primordial project; otherwise for
285275
// Miscellaneous Files we could have a case where we'd get another request to create a project
286276
// for the project we're currently processing.
287-
await ProjectFactory.ApplyChangeToWorkspaceAsync(workspace => workspace.OnProjectRemoved(projectId), cancellationToken);
277+
await primordialProjectFactory.ApplyChangeToWorkspaceAsync(workspace => workspace.OnProjectRemoved(projectId), cancellationToken);
288278
}
289279

290280
_loadedProjects[projectPath] = new ProjectLoadState.LoadedTargets(newProjectTargets);
@@ -312,9 +302,9 @@ private async Task<bool> ReloadProjectAsync(ProjectToLoad projectToLoad, ToastEr
312302
return false;
313303
}
314304

315-
async Task<(LoadedProject, bool alreadyExists)> GetOrCreateProjectTargetAsync(ImmutableArray<LoadedProject> previousProjectTargets, ProjectFileInfo loadedProjectInfo)
305+
async Task<(LoadedProject, bool alreadyExists)> GetOrCreateProjectTargetAsync(ImmutableArray<LoadedProject> previousProjectTargets, ProjectSystemProjectFactory projectFactory, ProjectFileInfo loadedProjectInfo)
316306
{
317-
var existingProject = previousProjectTargets.FirstOrDefault(p => p.GetTargetFramework() == loadedProjectInfo.TargetFramework);
307+
var existingProject = previousProjectTargets.FirstOrDefault(p => p.GetTargetFramework() == loadedProjectInfo.TargetFramework && p.ProjectFactory == projectFactory);
318308
if (existingProject != null)
319309
{
320310
return (existingProject, alreadyExists: true);
@@ -330,13 +320,13 @@ private async Task<bool> ReloadProjectAsync(ProjectToLoad projectToLoad, ToastEr
330320
CompilationOutputAssemblyFilePath = loadedProjectInfo.IntermediateOutputFilePath,
331321
};
332322

333-
var projectSystemProject = await ProjectFactory.CreateAndAddToWorkspaceAsync(
323+
var projectSystemProject = await projectFactory.CreateAndAddToWorkspaceAsync(
334324
projectSystemName,
335325
loadedProjectInfo.Language,
336326
projectCreationInfo,
337327
_projectSystemHostInfo);
338328

339-
var loadedProject = new LoadedProject(projectSystemProject, ProjectFactory.Workspace.Services.SolutionServices, _fileChangeWatcher, _targetFrameworkManager);
329+
var loadedProject = new LoadedProject(projectSystemProject, projectFactory, _fileChangeWatcher, _targetFrameworkManager);
340330
loadedProject.NeedsReload += (_, _) => _projectsToReload.AddWork(projectToLoad with { ReportTelemetry = false });
341331
return (loadedProject, alreadyExists: false);
342332
}
@@ -364,14 +354,22 @@ async Task LogDiagnosticsAsync(ImmutableArray<DiagnosticLogItem> diagnosticLogIt
364354
}
365355
}
366356

357+
protected async ValueTask<bool> IsProjectLoadedAsync(string projectPath, CancellationToken cancellationToken)
358+
{
359+
using (await _gate.DisposableWaitAsync(cancellationToken))
360+
{
361+
return _loadedProjects.ContainsKey(projectPath);
362+
}
363+
}
364+
367365
/// <summary>
368366
/// Begins loading a project with an associated primordial project. Must not be called for a project which has already begun loading.
369367
/// </summary>
370368
/// <param name="doDesignTimeBuild">
371369
/// If <see langword="true"/>, initiates a design-time build now, and starts file watchers to repeat the design-time build on relevant changes.
372370
/// If <see langword="false"/>, only tracks the primordial project.
373371
/// </param>
374-
protected async ValueTask BeginLoadingProjectWithPrimordialAsync(string projectPath, ProjectId primordialProjectId, bool doDesignTimeBuild)
372+
protected async ValueTask BeginLoadingProjectWithPrimordialAsync(string projectPath, ProjectSystemProjectFactory primordialProjectFactory, ProjectId primordialProjectId, bool doDesignTimeBuild)
375373
{
376374
using (await _gate.DisposableWaitAsync(CancellationToken.None))
377375
{
@@ -383,7 +381,7 @@ protected async ValueTask BeginLoadingProjectWithPrimordialAsync(string projectP
383381
Contract.Fail($"Cannot begin loading project '{projectPath}' because it has already begun loading.");
384382
}
385383

386-
_loadedProjects.Add(projectPath, new ProjectLoadState.Primordial(primordialProjectId));
384+
_loadedProjects.Add(projectPath, new ProjectLoadState.Primordial(primordialProjectFactory, primordialProjectId));
387385
if (doDesignTimeBuild)
388386
{
389387
_projectsToReload.AddWork(new ProjectToLoad(projectPath, ProjectGuid: null, ReportTelemetry: true));
@@ -422,9 +420,9 @@ protected async ValueTask UnloadProjectAsync(string projectPath)
422420
return;
423421
}
424422

425-
if (loadState is ProjectLoadState.Primordial(var projectId))
423+
if (loadState is ProjectLoadState.Primordial(var projectFactory, var projectId))
426424
{
427-
await ProjectFactory.ApplyChangeToWorkspaceAsync(workspace => workspace.OnProjectRemoved(projectId));
425+
await projectFactory.ApplyChangeToWorkspaceAsync(workspace => workspace.OnProjectRemoved(projectId));
428426
}
429427
else if (loadState is ProjectLoadState.LoadedTargets(var existingProjects))
430428
{

0 commit comments

Comments
 (0)