Skip to content

Commit d448780

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 0ba91aa commit d448780

File tree

7 files changed

+84
-51
lines changed

7 files changed

+84
-51
lines changed

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

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ internal sealed class FileBasedProgramsProjectSystem : LanguageServerProjectLoad
2727
private readonly ILogger<FileBasedProgramsProjectSystem> _logger;
2828
private readonly IMetadataAsSourceFileService _metadataAsSourceFileService;
2929
private readonly VirtualProjectXmlProvider _projectXmlProvider;
30+
private readonly LanguageServerWorkspaceFactory _workspaceFactory;
3031

3132
public FileBasedProgramsProjectSystem(
3233
ILspServices lspServices,
@@ -41,7 +42,6 @@ public FileBasedProgramsProjectSystem(
4142
ServerConfigurationFactory serverConfigurationFactory,
4243
IBinLogPathProvider binLogPathProvider)
4344
: base(
44-
workspaceFactory.FileBasedProgramsProjectFactory,
4545
workspaceFactory.TargetFrameworkManager,
4646
workspaceFactory.ProjectSystemHostInfo,
4747
fileChangeWatcher,
@@ -56,12 +56,20 @@ public FileBasedProgramsProjectSystem(
5656
_logger = loggerFactory.CreateLogger<FileBasedProgramsProjectSystem>();
5757
_metadataAsSourceFileService = metadataAsSourceFileService;
5858
_projectXmlProvider = projectXmlProvider;
59+
_workspaceFactory = workspaceFactory;
5960
}
6061

61-
public Workspace Workspace => ProjectFactory.Workspace;
62-
6362
private string GetDocumentFilePath(DocumentUri uri) => uri.ParsedUri is { } parsedUri ? ProtocolConversions.GetDocumentFilePathFromUri(parsedUri) : uri.UriString;
6463

64+
public async ValueTask<bool> IsMiscellaneousFilesDocumentAsync(TextDocument document, CancellationToken cancellationToken)
65+
{
66+
// There are two cases here: if it's a primordial document, it'll be in the MiscellaneousFilesWorkspace and thus we definitely know it's
67+
// 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
68+
// is also the source file path, and that's what we consider the 'project' path that is loaded.
69+
return document.Project.Solution.Workspace == _workspaceFactory.FileBasedProgramsProjectFactory.Workspace ||
70+
document.Project.FilePath is not null && await IsProjectLoadedAsync(document.Project.FilePath, cancellationToken);
71+
}
72+
6573
public async ValueTask<TextDocument?> AddMiscellaneousDocumentAsync(DocumentUri uri, SourceText documentText, string languageId, ILspLogger logger)
6674
{
6775
var documentFilePath = GetDocumentFilePath(uri);
@@ -80,7 +88,7 @@ public FileBasedProgramsProjectSystem(
8088
var doDesignTimeBuild = uri.ParsedUri?.IsFile is true
8189
&& primordialDoc.Project.Language == LanguageNames.CSharp
8290
&& GlobalOptionService.GetOption(LanguageServerProjectSystemOptionsStorage.EnableFileBasedPrograms);
83-
await BeginLoadingProjectWithPrimordialAsync(primordialDoc.FilePath, primordialProjectId: primordialDoc.Project.Id, doDesignTimeBuild);
91+
await BeginLoadingProjectWithPrimordialAsync(primordialDoc.FilePath, _workspaceFactory.FileBasedProgramsProjectFactory, primordialProjectId: primordialDoc.Project.Id, doDesignTimeBuild);
8492

8593
return primordialDoc;
8694

@@ -92,12 +100,12 @@ TextDocument AddPrimordialDocument(DocumentUri uri, SourceText documentText, str
92100
Contract.Fail($"Could not find language information for {uri} with absolute path {documentFilePath}");
93101
}
94102

95-
var workspace = Workspace;
103+
var workspace = _workspaceFactory.FileBasedProgramsProjectFactory.Workspace;
96104
var sourceTextLoader = new SourceTextLoader(documentText, documentFilePath);
97105
var projectInfo = MiscellaneousFileUtilities.CreateMiscellaneousProjectInfoForDocument(
98106
workspace, documentFilePath, sourceTextLoader, languageInformation, documentText.ChecksumAlgorithm, workspace.Services.SolutionServices, []);
99107

100-
ProjectFactory.ApplyChangeToWorkspace(workspace => workspace.OnProjectAdded(projectInfo));
108+
_workspaceFactory.FileBasedProgramsProjectFactory.ApplyChangeToWorkspace(workspace => workspace.OnProjectAdded(projectInfo));
101109

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

148-
var loader = ProjectFactory.CreateFileTextLoader(documentPath);
156+
var loader = _workspaceFactory.FileBasedProgramsProjectFactory.CreateFileTextLoader(documentPath);
149157
var textAndVersion = await loader.LoadTextAsync(new LoadTextOptions(SourceHashAlgorithms.Default), cancellationToken);
150158
var isFileBasedProgram = VirtualProjectXmlProvider.IsFileBasedProgram(documentPath, textAndVersion.Text);
151159

152160
const BuildHostProcessKind buildHostKind = BuildHostProcessKind.NetCore;
153161
var buildHost = await buildHostProcessManager.GetBuildHostAsync(buildHostKind, cancellationToken);
154162
var loadedFile = await buildHost.LoadProjectAsync(virtualProjectPath, virtualProjectContent, languageName: LanguageNames.CSharp, cancellationToken);
155-
return new RemoteProjectLoadResult(loadedFile, HasAllInformation: isFileBasedProgram, Preferred: buildHostKind, Actual: buildHostKind);
163+
164+
return new RemoteProjectLoadResult(
165+
loadedFile,
166+
// 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.
167+
// Otherwise, we'll keep it in miscellaneous files.
168+
ProjectFactory: isFileBasedProgram ? _workspaceFactory.HostProjectFactory : _workspaceFactory.FileBasedProgramsProjectFactory,
169+
HasAllInformation: isFileBasedProgram,
170+
Preferred: buildHostKind,
171+
Actual: buildHostKind);
156172
}
157173
}

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

Lines changed: 23 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ internal abstract class LanguageServerProjectLoader
2929
{
3030
private readonly AsyncBatchingWorkQueue<ProjectToLoad> _projectsToReload;
3131

32-
protected readonly ProjectSystemProjectFactory ProjectFactory;
3332
private readonly ProjectTargetFrameworkManager _targetFrameworkManager;
3433
private readonly ProjectSystemHostInfo _projectSystemHostInfo;
3534
private readonly IFileChangeWatcher _fileChangeWatcher;
@@ -70,7 +69,7 @@ private ProjectLoadState() { }
7069
/// ID of the project which LSP uses to fulfill requests until the first design-time build is complete.
7170
/// The project with this ID is removed from the workspace when unloading or when transitioning to <see cref="LoadedTargets"/> state.
7271
/// </param>
73-
public sealed record Primordial(ProjectId PrimordialProjectId) : ProjectLoadState;
72+
public sealed record Primordial(ProjectSystemProjectFactory Factory, ProjectId PrimordialProjectId) : ProjectLoadState;
7473

7574
/// <summary>
7675
/// Represents a project for which we have loaded zero or more targets.
@@ -83,7 +82,6 @@ public sealed record LoadedTargets(ImmutableArray<LoadedProject> LoadedProjectTa
8382
}
8483

8584
protected LanguageServerProjectLoader(
86-
ProjectSystemProjectFactory projectFactory,
8785
ProjectTargetFrameworkManager targetFrameworkManager,
8886
ProjectSystemHostInfo projectSystemHostInfo,
8987
IFileChangeWatcher fileChangeWatcher,
@@ -94,7 +92,6 @@ protected LanguageServerProjectLoader(
9492
ServerConfigurationFactory serverConfigurationFactory,
9593
IBinLogPathProvider binLogPathProvider)
9694
{
97-
ProjectFactory = projectFactory;
9895
_targetFrameworkManager = targetFrameworkManager;
9996
_projectSystemHostInfo = projectSystemHostInfo;
10097
_fileChangeWatcher = fileChangeWatcher;
@@ -103,7 +100,6 @@ protected LanguageServerProjectLoader(
103100
_logger = loggerFactory.CreateLogger(nameof(LanguageServerProjectLoader));
104101
_projectLoadTelemetryReporter = projectLoadTelemetry;
105102
_binLogPathProvider = binLogPathProvider;
106-
var workspace = projectFactory.Workspace;
107103
var razorDesignTimePath = serverConfigurationFactory.ServerConfiguration?.RazorDesignTimePath;
108104

109105
AdditionalProperties = razorDesignTimePath is null
@@ -174,7 +170,7 @@ private async ValueTask ReloadProjectsAsync(ImmutableSegmentedList<ProjectToLoad
174170
}
175171
}
176172

177-
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);
178174

179175
/// <summary>Loads a project in the MSBuild host.</summary>
180176
/// <remarks>Caller needs to catch exceptions to avoid bringing down the project loader queue.</remarks>
@@ -209,7 +205,7 @@ private async Task<bool> ReloadProjectAsync(ProjectToLoad projectToLoad, ToastEr
209205
return false;
210206
}
211207

212-
(RemoteProjectFile remoteProjectFile, bool hasAllInformation, BuildHostProcessKind preferredBuildHostKind, BuildHostProcessKind actualBuildHostKind) = remoteProjectLoadResult;
208+
(RemoteProjectFile remoteProjectFile, ProjectSystemProjectFactory projectFactory, bool hasAllInformation, BuildHostProcessKind preferredBuildHostKind, BuildHostProcessKind actualBuildHostKind) = remoteProjectLoadResult;
213209
if (preferredBuildHostKind != actualBuildHostKind)
214210
preferredBuildHostKindThatWeDidNotGet = preferredBuildHostKind;
215211

@@ -226,7 +222,7 @@ private async Task<bool> ReloadProjectAsync(ProjectToLoad projectToLoad, ToastEr
226222
// The out-of-proc build host supports more languages than we may actually have Workspace binaries for, so ensure we can actually process that
227223
// language in-process.
228224
var projectLanguage = loadedProjectInfos.FirstOrDefault()?.Language;
229-
if (projectLanguage != null && ProjectFactory.Workspace.Services.GetLanguageService<ICommandLineParserService>(projectLanguage) == null)
225+
if (projectLanguage != null && projectFactory.Workspace.Services.GetLanguageService<ICommandLineParserService>(projectLanguage) == null)
230226
{
231227
return false;
232228
}
@@ -246,7 +242,7 @@ private async Task<bool> ReloadProjectAsync(ProjectToLoad projectToLoad, ToastEr
246242
var newProjectTargetsBuilder = ArrayBuilder<LoadedProject>.GetInstance(loadedProjectInfos.Length);
247243
foreach (var loadedProjectInfo in loadedProjectInfos)
248244
{
249-
var (target, targetAlreadyExists) = await GetOrCreateProjectTargetAsync(previousProjectTargets, loadedProjectInfo);
245+
var (target, targetAlreadyExists) = await GetOrCreateProjectTargetAsync(previousProjectTargets, projectFactory, loadedProjectInfo);
250246
newProjectTargetsBuilder.Add(target);
251247

252248
var (targetTelemetryInfo, targetNeedsRestore) = await target.UpdateWithNewProjectInfoAsync(loadedProjectInfo, hasAllInformation, _logger);
@@ -272,13 +268,13 @@ private async Task<bool> ReloadProjectAsync(ProjectToLoad projectToLoad, ToastEr
272268
await _projectLoadTelemetryReporter.ReportProjectLoadTelemetryAsync(telemetryInfos, projectToLoad, cancellationToken);
273269
}
274270

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

284280
_loadedProjects[projectPath] = new ProjectLoadState.LoadedTargets(newProjectTargets);
@@ -306,9 +302,9 @@ private async Task<bool> ReloadProjectAsync(ProjectToLoad projectToLoad, ToastEr
306302
return false;
307303
}
308304

309-
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)
310306
{
311-
var existingProject = previousProjectTargets.FirstOrDefault(p => p.GetTargetFramework() == loadedProjectInfo.TargetFramework);
307+
var existingProject = previousProjectTargets.FirstOrDefault(p => p.GetTargetFramework() == loadedProjectInfo.TargetFramework && p.ProjectFactory == projectFactory);
312308
if (existingProject != null)
313309
{
314310
return (existingProject, alreadyExists: true);
@@ -324,13 +320,13 @@ private async Task<bool> ReloadProjectAsync(ProjectToLoad projectToLoad, ToastEr
324320
CompilationOutputAssemblyFilePath = loadedProjectInfo.IntermediateOutputFilePath,
325321
};
326322

327-
var projectSystemProject = await ProjectFactory.CreateAndAddToWorkspaceAsync(
323+
var projectSystemProject = await projectFactory.CreateAndAddToWorkspaceAsync(
328324
projectSystemName,
329325
loadedProjectInfo.Language,
330326
projectCreationInfo,
331327
_projectSystemHostInfo);
332328

333-
var loadedProject = new LoadedProject(projectSystemProject, ProjectFactory.Workspace.Services.SolutionServices, _fileChangeWatcher, _targetFrameworkManager);
329+
var loadedProject = new LoadedProject(projectSystemProject, projectFactory, _fileChangeWatcher, _targetFrameworkManager);
334330
loadedProject.NeedsReload += (_, _) => _projectsToReload.AddWork(projectToLoad with { ReportTelemetry = false });
335331
return (loadedProject, alreadyExists: false);
336332
}
@@ -358,14 +354,22 @@ async Task LogDiagnosticsAsync(ImmutableArray<DiagnosticLogItem> diagnosticLogIt
358354
}
359355
}
360356

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+
361365
/// <summary>
362366
/// Begins loading a project with an associated primordial project. Must not be called for a project which has already begun loading.
363367
/// </summary>
364368
/// <param name="doDesignTimeBuild">
365369
/// If <see langword="true"/>, initiates a design-time build now, and starts file watchers to repeat the design-time build on relevant changes.
366370
/// If <see langword="false"/>, only tracks the primordial project.
367371
/// </param>
368-
protected async ValueTask BeginLoadingProjectWithPrimordialAsync(string projectPath, ProjectId primordialProjectId, bool doDesignTimeBuild)
372+
protected async ValueTask BeginLoadingProjectWithPrimordialAsync(string projectPath, ProjectSystemProjectFactory primordialProjectFactory, ProjectId primordialProjectId, bool doDesignTimeBuild)
369373
{
370374
using (await _gate.DisposableWaitAsync(CancellationToken.None))
371375
{
@@ -377,7 +381,7 @@ protected async ValueTask BeginLoadingProjectWithPrimordialAsync(string projectP
377381
Contract.Fail($"Cannot begin loading project '{projectPath}' because it has already begun loading.");
378382
}
379383

380-
_loadedProjects.Add(projectPath, new ProjectLoadState.Primordial(primordialProjectId));
384+
_loadedProjects.Add(projectPath, new ProjectLoadState.Primordial(primordialProjectFactory, primordialProjectId));
381385
if (doDesignTimeBuild)
382386
{
383387
_projectsToReload.AddWork(new ProjectToLoad(projectPath, ProjectGuid: null, ReportTelemetry: true));
@@ -416,9 +420,9 @@ protected async ValueTask UnloadProjectAsync(string projectPath)
416420
return;
417421
}
418422

419-
if (loadState is ProjectLoadState.Primordial(var projectId))
423+
if (loadState is ProjectLoadState.Primordial(var projectFactory, var projectId))
420424
{
421-
await ProjectFactory.ApplyChangeToWorkspaceAsync(workspace => workspace.OnProjectRemoved(projectId));
425+
await projectFactory.ApplyChangeToWorkspaceAsync(workspace => workspace.OnProjectRemoved(projectId));
422426
}
423427
else if (loadState is ProjectLoadState.LoadedTargets(var existingProjects))
424428
{

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

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
using Microsoft.CodeAnalysis.Options;
1111
using Microsoft.CodeAnalysis.ProjectSystem;
1212
using Microsoft.CodeAnalysis.Shared.TestHooks;
13+
using Microsoft.CodeAnalysis.Workspaces.ProjectSystem;
1314
using Microsoft.Extensions.Logging;
1415
using Microsoft.VisualStudio.Composition;
1516
using Roslyn.Utilities;
@@ -22,6 +23,7 @@ internal sealed class LanguageServerProjectSystem : LanguageServerProjectLoader
2223
{
2324
private readonly ILogger _logger;
2425
private readonly ProjectFileExtensionRegistry _projectFileExtensionRegistry;
26+
private readonly ProjectSystemProjectFactory _hostProjectFactory;
2527

2628
[ImportingConstructor]
2729
[Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
@@ -35,7 +37,6 @@ public LanguageServerProjectSystem(
3537
ServerConfigurationFactory serverConfigurationFactory,
3638
IBinLogPathProvider binLogPathProvider)
3739
: base(
38-
workspaceFactory.HostProjectFactory,
3940
workspaceFactory.TargetFrameworkManager,
4041
workspaceFactory.ProjectSystemHostInfo,
4142
fileChangeWatcher,
@@ -47,14 +48,15 @@ public LanguageServerProjectSystem(
4748
binLogPathProvider)
4849
{
4950
_logger = loggerFactory.CreateLogger(nameof(LanguageServerProjectSystem));
50-
var workspace = ProjectFactory.Workspace;
51+
_hostProjectFactory = workspaceFactory.HostProjectFactory;
52+
var workspace = workspaceFactory.HostWorkspace;
5153
_projectFileExtensionRegistry = new ProjectFileExtensionRegistry(workspace.CurrentSolution.Services, new DiagnosticReporter(workspace));
5254
}
5355

5456
public async Task OpenSolutionAsync(string solutionFilePath)
5557
{
5658
_logger.LogInformation(string.Format(LanguageServerResources.Loading_0, solutionFilePath));
57-
ProjectFactory.SolutionPath = solutionFilePath;
59+
_hostProjectFactory.SolutionPath = solutionFilePath;
5860

5961
var (_, projects) = await SolutionFileReader.ReadSolutionFileAsync(solutionFilePath, DiagnosticReportingMode.Throw, CancellationToken.None);
6062
foreach (var (path, guid) in projects)
@@ -88,6 +90,6 @@ public async Task OpenProjectsAsync(ImmutableArray<string> projectFilePaths)
8890
var (buildHost, actualBuildHostKind) = await buildHostProcessManager.GetBuildHostWithFallbackAsync(preferredBuildHostKind, projectPath, cancellationToken);
8991

9092
var loadedFile = await buildHost.LoadProjectFileAsync(projectPath, languageName, cancellationToken);
91-
return new RemoteProjectLoadResult(loadedFile, HasAllInformation: true, preferredBuildHostKind, actualBuildHostKind);
93+
return new RemoteProjectLoadResult(loadedFile, _hostProjectFactory, HasAllInformation: true, preferredBuildHostKind, actualBuildHostKind);
9294
}
9395
}

0 commit comments

Comments
 (0)