Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ internal sealed class FileBasedProgramsProjectSystem : LanguageServerProjectLoad
private readonly ILogger<FileBasedProgramsProjectSystem> _logger;
private readonly IMetadataAsSourceFileService _metadataAsSourceFileService;
private readonly VirtualProjectXmlProvider _projectXmlProvider;
private readonly LanguageServerWorkspaceFactory _workspaceFactory;

public FileBasedProgramsProjectSystem(
ILspServices lspServices,
Expand All @@ -41,7 +42,6 @@ public FileBasedProgramsProjectSystem(
ServerConfigurationFactory serverConfigurationFactory,
IBinLogPathProvider binLogPathProvider)
: base(
workspaceFactory.FileBasedProgramsProjectFactory,
workspaceFactory.TargetFrameworkManager,
workspaceFactory.ProjectSystemHostInfo,
fileChangeWatcher,
Expand All @@ -56,12 +56,20 @@ public FileBasedProgramsProjectSystem(
_logger = loggerFactory.CreateLogger<FileBasedProgramsProjectSystem>();
_metadataAsSourceFileService = metadataAsSourceFileService;
_projectXmlProvider = projectXmlProvider;
_workspaceFactory = workspaceFactory;
}

public Workspace Workspace => ProjectFactory.Workspace;

private string GetDocumentFilePath(DocumentUri uri) => uri.ParsedUri is { } parsedUri ? ProtocolConversions.GetDocumentFilePathFromUri(parsedUri) : uri.UriString;

public async ValueTask<bool> IsMiscellaneousFilesDocumentAsync(TextDocument document, CancellationToken cancellationToken)
{
// There are two cases here: if it's a primordial document, it'll be in the MiscellaneousFilesWorkspace and thus we definitely know it's
// 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
// is also the source file path, and that's what we consider the 'project' path that is loaded.
return document.Project.Solution.Workspace == _workspaceFactory.MiscellaneousFilesWorkspaceProjectFactory.Workspace ||
document.Project.FilePath is not null && await IsProjectLoadedAsync(document.Project.FilePath, cancellationToken);
}

public async ValueTask<TextDocument?> AddMiscellaneousDocumentAsync(DocumentUri uri, SourceText documentText, string languageId, ILspLogger logger)
{
var documentFilePath = GetDocumentFilePath(uri);
Expand All @@ -80,7 +88,7 @@ public FileBasedProgramsProjectSystem(
var doDesignTimeBuild = uri.ParsedUri?.IsFile is true
&& primordialDoc.Project.Language == LanguageNames.CSharp
&& GlobalOptionService.GetOption(LanguageServerProjectSystemOptionsStorage.EnableFileBasedPrograms);
await BeginLoadingProjectWithPrimordialAsync(primordialDoc.FilePath, primordialProjectId: primordialDoc.Project.Id, doDesignTimeBuild);
await BeginLoadingProjectWithPrimordialAsync(primordialDoc.FilePath, _workspaceFactory.MiscellaneousFilesWorkspaceProjectFactory, primordialProjectId: primordialDoc.Project.Id, doDesignTimeBuild);

return primordialDoc;

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

var workspace = Workspace;
var workspace = _workspaceFactory.MiscellaneousFilesWorkspaceProjectFactory.Workspace;
var sourceTextLoader = new SourceTextLoader(documentText, documentFilePath);
var projectInfo = MiscellaneousFileUtilities.CreateMiscellaneousProjectInfoForDocument(
workspace, documentFilePath, sourceTextLoader, languageInformation, documentText.ChecksumAlgorithm, workspace.Services.SolutionServices, []);

ProjectFactory.ApplyChangeToWorkspace(workspace => workspace.OnProjectAdded(projectInfo));
_workspaceFactory.MiscellaneousFilesWorkspaceProjectFactory.ApplyChangeToWorkspace(workspace => workspace.OnProjectAdded(projectInfo));

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

var loader = ProjectFactory.CreateFileTextLoader(documentPath);
var loader = _workspaceFactory.MiscellaneousFilesWorkspaceProjectFactory.CreateFileTextLoader(documentPath);
var textAndVersion = await loader.LoadTextAsync(new LoadTextOptions(SourceHashAlgorithms.Default), cancellationToken);
var isFileBasedProgram = VirtualProjectXmlProvider.IsFileBasedProgram(documentPath, textAndVersion.Text);

const BuildHostProcessKind buildHostKind = BuildHostProcessKind.NetCore;
var buildHost = await buildHostProcessManager.GetBuildHostAsync(buildHostKind, cancellationToken);
var loadedFile = await buildHost.LoadProjectAsync(virtualProjectPath, virtualProjectContent, languageName: LanguageNames.CSharp, cancellationToken);
return new RemoteProjectLoadResult(loadedFile, HasAllInformation: isFileBasedProgram, Preferred: buildHostKind, Actual: buildHostKind);

return new RemoteProjectLoadResult(
loadedFile,
// 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.
// Otherwise, we'll keep it in miscellaneous files.
ProjectFactory: isFileBasedProgram ? _workspaceFactory.HostProjectFactory : _workspaceFactory.MiscellaneousFilesWorkspaceProjectFactory,
HasAllInformation: isFileBasedProgram,
Preferred: buildHostKind,
Actual: buildHostKind);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ internal abstract class LanguageServerProjectLoader
{
private readonly AsyncBatchingWorkQueue<ProjectToLoad> _projectsToReload;

protected readonly ProjectSystemProjectFactory ProjectFactory;
private readonly ProjectTargetFrameworkManager _targetFrameworkManager;
private readonly ProjectSystemHostInfo _projectSystemHostInfo;
private readonly IFileChangeWatcher _fileChangeWatcher;
Expand Down Expand Up @@ -66,11 +65,15 @@ private ProjectLoadState() { }
/// Represents a project which has not yet had a design-time build performed for it,
/// and which has an associated "primordial project" in the workspace.
/// </summary>
/// <param name="PrimordialProjectFactory">
/// The project factory for the workspace that the primordial project lives within. This
/// factory was not used to create the project, but still needs to be used during removal to avoid locking issues.
/// </param>
/// <param name="PrimordialProjectId">
/// ID of the project which LSP uses to fulfill requests until the first design-time build is complete.
/// The project with this ID is removed from the workspace when unloading or when transitioning to <see cref="LoadedTargets"/> state.
/// </param>
public sealed record Primordial(ProjectId PrimordialProjectId) : ProjectLoadState;
public sealed record Primordial(ProjectSystemProjectFactory PrimordialProjectFactory, ProjectId PrimordialProjectId) : ProjectLoadState;

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

protected LanguageServerProjectLoader(
ProjectSystemProjectFactory projectFactory,
ProjectTargetFrameworkManager targetFrameworkManager,
ProjectSystemHostInfo projectSystemHostInfo,
IFileChangeWatcher fileChangeWatcher,
Expand All @@ -94,7 +96,6 @@ protected LanguageServerProjectLoader(
ServerConfigurationFactory serverConfigurationFactory,
IBinLogPathProvider binLogPathProvider)
{
ProjectFactory = projectFactory;
_targetFrameworkManager = targetFrameworkManager;
_projectSystemHostInfo = projectSystemHostInfo;
_fileChangeWatcher = fileChangeWatcher;
Expand All @@ -103,7 +104,6 @@ protected LanguageServerProjectLoader(
_logger = loggerFactory.CreateLogger(nameof(LanguageServerProjectLoader));
_projectLoadTelemetryReporter = projectLoadTelemetry;
_binLogPathProvider = binLogPathProvider;
var workspace = projectFactory.Workspace;
var razorDesignTimePath = serverConfigurationFactory.ServerConfiguration?.RazorDesignTimePath;

AdditionalProperties = razorDesignTimePath is null
Expand Down Expand Up @@ -174,7 +174,7 @@ private async ValueTask ReloadProjectsAsync(ImmutableSegmentedList<ProjectToLoad
}
}

protected sealed record RemoteProjectLoadResult(RemoteProjectFile ProjectFile, bool HasAllInformation, BuildHostProcessKind Preferred, BuildHostProcessKind Actual);
protected sealed record RemoteProjectLoadResult(RemoteProjectFile ProjectFile, ProjectSystemProjectFactory ProjectFactory, bool HasAllInformation, BuildHostProcessKind Preferred, BuildHostProcessKind Actual);

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

(RemoteProjectFile remoteProjectFile, bool hasAllInformation, BuildHostProcessKind preferredBuildHostKind, BuildHostProcessKind actualBuildHostKind) = remoteProjectLoadResult;
(RemoteProjectFile remoteProjectFile, ProjectSystemProjectFactory projectFactory, bool hasAllInformation, BuildHostProcessKind preferredBuildHostKind, BuildHostProcessKind actualBuildHostKind) = remoteProjectLoadResult;
if (preferredBuildHostKind != actualBuildHostKind)
preferredBuildHostKindThatWeDidNotGet = preferredBuildHostKind;

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

var (targetTelemetryInfo, targetNeedsRestore) = await target.UpdateWithNewProjectInfoAsync(loadedProjectInfo, hasAllInformation, _logger);
Expand All @@ -272,15 +272,19 @@ private async Task<bool> ReloadProjectAsync(ProjectToLoad projectToLoad, ToastEr
await _projectLoadTelemetryReporter.ReportProjectLoadTelemetryAsync(telemetryInfos, projectToLoad, cancellationToken);
}

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

// At this point we expect that all the loaded projects are now in the project factory returned, and any previous ones have been removed.
// this is a Debug.Assert() because if this expectation fails, the user's probably still in a state where things will work just fine;
// throwing here would mean we don't remember the LoadedProjects we created, and the next update will create more and things will get really broken.
Debug.Assert(newProjectTargets.All(target => target.ProjectFactory == projectFactory));
_loadedProjects[projectPath] = new ProjectLoadState.LoadedTargets(newProjectTargets);
}

Expand All @@ -306,9 +310,9 @@ private async Task<bool> ReloadProjectAsync(ProjectToLoad projectToLoad, ToastEr
return false;
}

async Task<(LoadedProject, bool alreadyExists)> GetOrCreateProjectTargetAsync(ImmutableArray<LoadedProject> previousProjectTargets, ProjectFileInfo loadedProjectInfo)
async Task<(LoadedProject, bool alreadyExists)> GetOrCreateProjectTargetAsync(ImmutableArray<LoadedProject> previousProjectTargets, ProjectSystemProjectFactory projectFactory, ProjectFileInfo loadedProjectInfo)
{
var existingProject = previousProjectTargets.FirstOrDefault(p => p.GetTargetFramework() == loadedProjectInfo.TargetFramework);
var existingProject = previousProjectTargets.FirstOrDefault(p => p.GetTargetFramework() == loadedProjectInfo.TargetFramework && p.ProjectFactory == projectFactory);
if (existingProject != null)
{
return (existingProject, alreadyExists: true);
Expand All @@ -324,13 +328,13 @@ private async Task<bool> ReloadProjectAsync(ProjectToLoad projectToLoad, ToastEr
CompilationOutputAssemblyFilePath = loadedProjectInfo.IntermediateOutputFilePath,
};

var projectSystemProject = await ProjectFactory.CreateAndAddToWorkspaceAsync(
var projectSystemProject = await projectFactory.CreateAndAddToWorkspaceAsync(
projectSystemName,
loadedProjectInfo.Language,
projectCreationInfo,
_projectSystemHostInfo);

var loadedProject = new LoadedProject(projectSystemProject, ProjectFactory.Workspace.Services.SolutionServices, _fileChangeWatcher, _targetFrameworkManager);
var loadedProject = new LoadedProject(projectSystemProject, projectFactory, _fileChangeWatcher, _targetFrameworkManager);
loadedProject.NeedsReload += (_, _) => _projectsToReload.AddWork(projectToLoad with { ReportTelemetry = false });
return (loadedProject, alreadyExists: false);
}
Expand Down Expand Up @@ -358,14 +362,22 @@ async Task LogDiagnosticsAsync(ImmutableArray<DiagnosticLogItem> diagnosticLogIt
}
}

protected async ValueTask<bool> IsProjectLoadedAsync(string projectPath, CancellationToken cancellationToken)
{
using (await _gate.DisposableWaitAsync(cancellationToken))
{
return _loadedProjects.ContainsKey(projectPath);
}
}

/// <summary>
/// Begins loading a project with an associated primordial project. Must not be called for a project which has already begun loading.
/// </summary>
/// <param name="doDesignTimeBuild">
/// If <see langword="true"/>, initiates a design-time build now, and starts file watchers to repeat the design-time build on relevant changes.
/// If <see langword="false"/>, only tracks the primordial project.
/// </param>
protected async ValueTask BeginLoadingProjectWithPrimordialAsync(string projectPath, ProjectId primordialProjectId, bool doDesignTimeBuild)
protected async ValueTask BeginLoadingProjectWithPrimordialAsync(string projectPath, ProjectSystemProjectFactory primordialProjectFactory, ProjectId primordialProjectId, bool doDesignTimeBuild)
{
using (await _gate.DisposableWaitAsync(CancellationToken.None))
{
Expand All @@ -377,7 +389,7 @@ protected async ValueTask BeginLoadingProjectWithPrimordialAsync(string projectP
Contract.Fail($"Cannot begin loading project '{projectPath}' because it has already begun loading.");
}

_loadedProjects.Add(projectPath, new ProjectLoadState.Primordial(primordialProjectId));
_loadedProjects.Add(projectPath, new ProjectLoadState.Primordial(primordialProjectFactory, primordialProjectId));
if (doDesignTimeBuild)
{
_projectsToReload.AddWork(new ProjectToLoad(projectPath, ProjectGuid: null, ReportTelemetry: true));
Expand Down Expand Up @@ -416,9 +428,9 @@ protected async ValueTask UnloadProjectAsync(string projectPath)
return;
}

if (loadState is ProjectLoadState.Primordial(var projectId))
if (loadState is ProjectLoadState.Primordial(var projectFactory, var projectId))
{
await ProjectFactory.ApplyChangeToWorkspaceAsync(workspace => workspace.OnProjectRemoved(projectId));
await projectFactory.ApplyChangeToWorkspaceAsync(workspace => workspace.OnProjectRemoved(projectId));
}
else if (loadState is ProjectLoadState.LoadedTargets(var existingProjects))
{
Expand Down
Loading
Loading