diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/FileBasedProgramsWorkspaceTests.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/FileBasedProgramsWorkspaceTests.cs
index 6e503fd6839f6..c118bc3ff1009 100644
--- a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/FileBasedProgramsWorkspaceTests.cs
+++ b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/FileBasedProgramsWorkspaceTests.cs
@@ -105,7 +105,7 @@ void M()
// Should have the appropriate generated files now that we ran a design time build
Assert.Contains(canonicalDocumentOne.Project.Documents, d => d.Name == "Canonical.AssemblyInfo.cs");
- // Add another loose virtual document and verify it goes into the same canonical project.
+ // Add another loose virtual document and verify it goes into a forked canonical project.
var looseFileUriTwo = ProtocolConversions.CreateAbsoluteDocumentUri(@"vscode-notebook-cell://dev-container/test.cs");
await testLspServer.OpenDocumentAsync(looseFileUriTwo, """
class Other
@@ -116,16 +116,111 @@ void OtherMethod()
}
""").ConfigureAwait(false);
- // Add another misc file and verify it gets added to the same canonical project.
var (_, canonicalDocumentTwo) = await GetLspWorkspaceAndDocumentAsync(looseFileUriTwo, testLspServer).ConfigureAwait(false);
Assert.NotNull(canonicalDocumentTwo);
- Assert.Equal(canonicalDocumentOne.Project.Id, canonicalDocumentTwo.Project.Id);
- // The project should also contain the other misc document.
- Assert.Contains(canonicalDocumentTwo.Project.Documents, d => d.Name == looseDocumentOne.Name);
- // Should have the appropriate generated files now that we ran a design time build
+ Assert.NotEqual(canonicalDocumentOne.Project.Id, canonicalDocumentTwo.Project.Id);
+ Assert.DoesNotContain(canonicalDocumentTwo.Project.Documents, d => d.Name == looseDocumentOne.Name);
+ // Semantic diagnostics are not expected due to absence of top-level statements
+ Assert.False(canonicalDocumentTwo.Project.State.HasAllInformation);
+ // Should have the appropriate generated files from the base misc files project now that we ran a design time build
Assert.Contains(canonicalDocumentTwo.Project.Documents, d => d.Name == "Canonical.AssemblyInfo.cs");
}
+ /// Test that a document which does not have an on-disk path, is never treated as a file-based program.
+ [Theory, CombinatorialData]
+ public async Task TestNonFileDocumentsAreNotFileBasedPrograms(bool mutatingLspWorkspace)
+ {
+ await using var testLspServer = await CreateTestLspServerAsync(string.Empty, mutatingLspWorkspace, new InitializationOptions { ServerKind = WellKnownLspServerKinds.CSharpVisualBasicLspServer });
+ Assert.Null(await GetMiscellaneousDocumentAsync(testLspServer));
+
+ var nonFileUri = ProtocolConversions.CreateAbsoluteDocumentUri(@"vscode-notebook-cell://dev-container/test.cs");
+ await testLspServer.OpenDocumentAsync(nonFileUri, """
+ #:sdk Microsoft.Net.Sdk
+ Console.WriteLine("Hello World");
+ """).ConfigureAwait(false);
+
+ // File should be initially added as a primordial document in the canonical misc files project with no metadata references.
+ var (_, primordialDocument) = await GetRequiredLspWorkspaceAndDocumentAsync(nonFileUri, testLspServer).ConfigureAwait(false);
+ // Should have the primordial canonical document and the loose document.
+ Assert.Equal(2, primordialDocument.Project.Documents.Count());
+ Assert.Empty(primordialDocument.Project.MetadataReferences);
+
+ var primordialSyntaxTree = await primordialDocument.GetRequiredSyntaxTreeAsync(CancellationToken.None);
+ // TODO: we probably don't want to report syntax errors for '#:' in the primordial non-file document.
+ // The logic which decides whether to add '-features:FileBasedProgram' probably needs to be adjusted.
+ primordialSyntaxTree.GetDiagnostics(CancellationToken.None).Verify(
+ // vscode-notebook-cell://dev-container/test.cs(1,2): error CS9298: '#:' directives can be only used in file-based programs ('-features:FileBasedProgram')"
+ // #:sdk Microsoft.Net.Sdk
+ TestHelpers.Diagnostic(code: 9298, squiggledText: ":").WithLocation(1, 2));
+
+ // Wait for the canonical project to finish loading.
+ await testLspServer.TestWorkspace.GetService().GetWaiter(FeatureAttribute.Workspace).ExpeditedWaitAsync();
+
+ // Verify the document is loaded in the canonical project.
+ var (miscWorkspace, canonicalDocument) = await GetRequiredLspWorkspaceAndDocumentAsync(nonFileUri, testLspServer).ConfigureAwait(false);
+ Assert.Equal(WorkspaceKind.MiscellaneousFiles, miscWorkspace.Kind);
+ Assert.NotNull(canonicalDocument);
+ Assert.NotEqual(primordialDocument, canonicalDocument);
+ // Should have the appropriate generated files now that we ran a design time build
+ Assert.Contains(canonicalDocument.Project.Documents, d => d.Name == "Canonical.AssemblyInfo.cs");
+
+ var canonicalSyntaxTree = await canonicalDocument.GetRequiredSyntaxTreeAsync(CancellationToken.None);
+ // TODO: we probably don't want to report syntax errors for '#:' in the canonical non-file document.
+ // The logic which decides whether to add '-features:FileBasedProgram' probably needs to be adjusted.
+ canonicalSyntaxTree.GetDiagnostics(CancellationToken.None).Verify(
+ // vscode-notebook-cell://dev-container/test.cs(1,2): error CS9298: '#:' directives can be only used in file-based programs ('-features:FileBasedProgram')"
+ // #:sdk Microsoft.Net.Sdk
+ TestHelpers.Diagnostic(code: 9298, squiggledText: ":").WithLocation(1, 2));
+ }
+
+ [Theory, CombinatorialData]
+ public async Task TestSemanticDiagnosticsEnabledWhenTopLevelStatementsAdded(bool mutatingLspWorkspace)
+ {
+ // Create a server that supports LSP misc files and verify no misc files present.
+ await using var testLspServer = await CreateTestLspServerAsync(string.Empty, mutatingLspWorkspace, new InitializationOptions { ServerKind = WellKnownLspServerKinds.CSharpVisualBasicLspServer });
+ Assert.Null(await GetMiscellaneousDocumentAsync(testLspServer));
+
+ var looseFileUriOne = ProtocolConversions.CreateAbsoluteDocumentUri(@"C:\SomeFile.cs");
+ await testLspServer.OpenDocumentAsync(looseFileUriOne, """
+ class C { }
+ """).ConfigureAwait(false);
+
+ // File should be initially added as a primordial document in the canonical misc files project with no metadata references.
+ var (miscFilesWorkspace, looseDocumentOne) = await GetRequiredLspWorkspaceAndDocumentAsync(looseFileUriOne, testLspServer).ConfigureAwait(false);
+ Assert.Equal(WorkspaceKind.MiscellaneousFiles, miscFilesWorkspace.Kind);
+ // Should have the primordial canonical document and the loose document.
+ Assert.Equal(2, looseDocumentOne.Project.Documents.Count());
+ Assert.Empty(looseDocumentOne.Project.MetadataReferences);
+ // Semantic diagnostics are not expected because we haven't loaded references
+ Assert.False(looseDocumentOne.Project.State.HasAllInformation);
+
+ // Wait for the canonical project to finish loading.
+ await testLspServer.TestWorkspace.GetService().GetWaiter(FeatureAttribute.Workspace).ExpeditedWaitAsync();
+
+ // Verify the document is loaded in the canonical project.
+ var (_, canonicalDocumentOne) = await GetRequiredLspWorkspaceAndDocumentAsync(looseFileUriOne, testLspServer).ConfigureAwait(false);
+ Assert.NotEqual(looseDocumentOne, canonicalDocumentOne);
+ // Should have the appropriate generated files now that we ran a design time build
+ Assert.Contains(canonicalDocumentOne.Project.Documents, d => d.Name == "Canonical.AssemblyInfo.cs");
+ // There are no top-level statements, so semantic errors are still not expected.
+ Assert.False(canonicalDocumentOne.Project.State.HasAllInformation);
+
+ // Adding a top-level statement to a misc file causes it to report semantic errors.
+ var textToInsert = $"""Console.WriteLine("Hello World!");{Environment.NewLine}""";
+ await testLspServer.InsertTextAsync(looseFileUriOne, (Line: 0, Column: 0, Text: textToInsert));
+ var (workspace, canonicalDocumentTwo) = await GetRequiredLspWorkspaceAndDocumentAsync(looseFileUriOne, testLspServer).ConfigureAwait(false);
+ Assert.Equal("""
+ Console.WriteLine("Hello World!");
+ class C { }
+ """,
+ (await canonicalDocumentTwo.GetSyntaxRootAsync())!.ToFullString());
+ Assert.Equal(WorkspaceKind.MiscellaneousFiles, workspace.Kind);
+ // When presence of top-level statements changes, the misc project is forked again in order to change attributes.
+ Assert.NotEqual(canonicalDocumentOne.Project.Id, canonicalDocumentTwo.Project.Id);
+ // Now that it has top-level statements, it should be considered to have all information.
+ Assert.True(canonicalDocumentTwo.Project.State.HasAllInformation);
+ }
+
[Theory, CombinatorialData]
public async Task TestFileBecomesFileBasedProgramWhenDirectiveAdded(bool mutatingLspWorkspace)
{
@@ -144,15 +239,21 @@ await testLspServer.OpenDocumentAsync(looseFileUriOne, """
// Should have the primordial canonical document and the loose document.
Assert.Equal(2, looseDocumentOne.Project.Documents.Count());
Assert.Empty(looseDocumentOne.Project.MetadataReferences);
+ // Semantic diagnostics are not expected because we haven't loaded references
+ Assert.False(looseDocumentOne.Project.State.HasAllInformation);
// Wait for the canonical project to finish loading.
await testLspServer.TestWorkspace.GetService().GetWaiter(FeatureAttribute.Workspace).ExpeditedWaitAsync();
// Verify the document is loaded in the canonical project.
- var (_, canonicalDocumentOne) = await GetRequiredLspWorkspaceAndDocumentAsync(looseFileUriOne, testLspServer).ConfigureAwait(false);
+ (miscFilesWorkspace, var canonicalDocumentOne) = await GetRequiredLspWorkspaceAndDocumentAsync(looseFileUriOne, testLspServer).ConfigureAwait(false);
Assert.NotEqual(looseDocumentOne, canonicalDocumentOne);
// Should have the appropriate generated files now that we ran a design time build
Assert.Contains(canonicalDocumentOne.Project.Documents, d => d.Name == "Canonical.AssemblyInfo.cs");
+ // This is not loaded as a file-based program (no dedicated restore done for it etc.), so it should be in the misc workspace.
+ Assert.Equal(WorkspaceKind.MiscellaneousFiles, miscFilesWorkspace.Kind);
+ // Because we have top-level statements, it should be considered to have all information (semantic diagnostics should be reported etc.)
+ Assert.True(canonicalDocumentOne.Project.State.HasAllInformation);
// Adding a #! directive to a misc file causes it to move to a file-based program project.
var textToInsert = $"#!/usr/bin/env dotnet{Environment.NewLine}";
@@ -180,5 +281,7 @@ await testLspServer.OpenDocumentAsync(looseFileUriOne, """
Assert.Equal(WorkspaceKind.Host, hostWorkspace!.Kind);
Assert.NotEqual(fileBasedProject.Id, fullFileBasedDocumentOne!.Project.Id);
Assert.Contains(fullFileBasedDocumentOne!.Project.Documents, d => d.Name == "SomeFile.AssemblyInfo.cs");
+ // Because it is loaded as a file-based program, it should be considered to have all information (semantic diagnostics should be reported etc.)
+ Assert.True(canonicalDocumentOne.Project.State.HasAllInformation);
}
}
diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/FileBasedPrograms/CanonicalMiscFilesProjectLoader.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/FileBasedPrograms/CanonicalMiscFilesProjectLoader.cs
index 74eed1693aa54..a9fcc49c0fc96 100644
--- a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/FileBasedPrograms/CanonicalMiscFilesProjectLoader.cs
+++ b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/FileBasedPrograms/CanonicalMiscFilesProjectLoader.cs
@@ -3,7 +3,7 @@
// See the LICENSE file in the project root for more information.
using System.Collections.Immutable;
-using System.Security;
+using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Features.Workspaces;
using Microsoft.CodeAnalysis.LanguageServer.HostWorkspace;
using Microsoft.CodeAnalysis.LanguageServer.HostWorkspace.ProjectTelemetry;
@@ -82,24 +82,22 @@ public async ValueTask AddMiscellaneousDocumentAsync(string docume
{
if (loadState is ProjectLoadState.LoadedTargets loadedTargets)
{
- // Case 1: Project is fully loaded with targets
-
+ // Case 1: Canonical 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().ProjectId;
- return await AddDocumentToExistingProject_NoLockAsync(documentPath, documentText, loadedProjectId, cancellationToken);
+ return await ForkCanonicalProjectAndAddDocument_NoLockAsync(documentPath, documentText, cancellationToken);
}
else
{
- // Case 2: Primordial project was already created, but hasn't finished loading.
+ // Case 2: Primordial canonical project was already created, but hasn't finished loading.
var primordialTarget = loadState as ProjectLoadState.Primordial;
Contract.ThrowIfNull(primordialTarget, "Expected primordial target");
- return await AddDocumentToExistingProject_NoLockAsync(documentPath, documentText, primordialTarget.PrimordialProjectId, cancellationToken);
+ return await AddDocumentToPrimordialProject_NoLockAsync(documentPath, documentText, primordialTarget.PrimordialProjectId, cancellationToken);
}
}
else
{
- // Case 3: Project doesn't exist at all
+ // Case 3: Canonical project doesn't exist at all
return CreatePrimordialProjectAndAddDocument_NoLock(documentPath, documentText);
}
}, cancellationToken);
@@ -138,7 +136,38 @@ await _workspaceFactory.MiscellaneousFilesWorkspaceProjectFactory.ApplyChangeToW
}, cancellationToken);
}
- private async ValueTask AddDocumentToExistingProject_NoLockAsync(string documentPath, SourceText documentText, ProjectId existingProjectId, CancellationToken cancellationToken)
+ private async ValueTask ForkCanonicalProjectAndAddDocument_NoLockAsync(string documentPath, SourceText documentText, CancellationToken cancellationToken)
+ {
+ var newProjectId = ProjectId.CreateNewId(debugName: $"Forked Misc Project for '{documentPath}'");
+ var newDocumentInfo = DocumentInfo.Create(
+ DocumentId.CreateNewId(newProjectId),
+ name: Path.GetFileName(documentPath),
+ loader: TextLoader.From(TextAndVersion.Create(documentText, VersionStamp.Create())),
+ filePath: documentPath);
+
+ var forkedProjectInfo = await GetForkedProjectInfoAsync(GetCanonicalProject(), newDocumentInfo, documentText, cancellationToken);
+
+ await _workspaceFactory.MiscellaneousFilesWorkspaceProjectFactory.ApplyChangeToWorkspaceAsync(workspace =>
+ {
+ workspace.OnProjectAdded(forkedProjectInfo);
+ }, cancellationToken);
+
+ var miscWorkspace = _workspaceFactory.MiscellaneousFilesWorkspaceProjectFactory.Workspace;
+ var addedDocument = miscWorkspace.CurrentSolution.GetRequiredDocument(newDocumentInfo.Id);
+ return addedDocument;
+ }
+
+ internal async ValueTask IsCanonicalProjectLoadedAsync(CancellationToken cancellationToken)
+ {
+ return await ExecuteUnderGateAsync(async loadedProjects =>
+ {
+ var canonicalDocumentPath = _canonicalDocumentPath.Value;
+ return loadedProjects.TryGetValue(canonicalDocumentPath, out var loadState)
+ && loadState is ProjectLoadState.LoadedTargets;
+ }, cancellationToken);
+ }
+
+ private async ValueTask AddDocumentToPrimordialProject_NoLockAsync(string documentPath, SourceText documentText, ProjectId existingProjectId, CancellationToken cancellationToken)
{
var miscWorkspace = _workspaceFactory.MiscellaneousFilesWorkspaceProjectFactory.Workspace;
var documentInfo = DocumentInfo.Create(
@@ -242,12 +271,15 @@ protected override ValueTask OnProjectUnloadedAsync(string projectFilePath)
return ValueTask.CompletedTask;
}
- protected override async ValueTask TransitionPrimordialProjectToLoadedAsync(
+ protected override async ValueTask TransitionPrimordialProjectToLoaded_NoLockAsync(
string projectPath,
ProjectSystemProjectFactory primordialProjectFactory,
ProjectId primordialProjectId,
CancellationToken cancellationToken)
{
+ // We only pass 'doDesignTimeBuild: true' for the canonical project. So that's the only time we should get called back for this.
+ Contract.ThrowIfFalse(projectPath == _canonicalDocumentPath.Value);
+
// Transfer any misc documents from the primordial project to the loaded canonical project
var primordialWorkspace = primordialProjectFactory.Workspace;
var primordialProject = primordialWorkspace.CurrentSolution.GetRequiredProject(primordialProjectId);
@@ -262,17 +294,8 @@ protected override async ValueTask TransitionPrimordialProjectToLoadedAsync(
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);
+ Contract.ThrowIfNull(miscDoc.FilePath);
+ await ForkCanonicalProjectAndAddDocument_NoLockAsync(miscDoc.FilePath, await miscDoc.GetTextAsync(cancellationToken), cancellationToken);
}
// Now remove the primordial project
@@ -281,6 +304,51 @@ await primordialProjectFactory.ApplyChangeToWorkspaceAsync(
cancellationToken);
}
+ ///
+ /// Creates a new project based on the canonical project with a new document added.
+ /// This should only be called when the canonical project is in the FullyLoaded state.
+ ///
+ private static async Task GetForkedProjectInfoAsync(Project canonicalProject, DocumentInfo newDocumentInfo, SourceText documentText, CancellationToken cancellationToken)
+ {
+ var newDocumentPath = newDocumentInfo.FilePath;
+ Contract.ThrowIfNull(newDocumentPath);
+
+ var forkedProjectId = ProjectId.CreateNewId(debugName: $"Forked Misc Project for '{newDocumentPath}'");
+ var syntaxTree = CSharpSyntaxTree.ParseText(text: documentText, canonicalProject.ParseOptions as CSharpParseOptions, path: newDocumentPath, cancellationToken);
+ var hasAllInformation = await VirtualProjectXmlProvider.HasTopLevelStatementsAsync(syntaxTree, cancellationToken);
+ var forkedProjectAttributes = new ProjectInfo.ProjectAttributes(
+ newDocumentInfo.Id.ProjectId,
+ version: VersionStamp.Create(),
+ name: canonicalProject.Name,
+ assemblyName: canonicalProject.AssemblyName,
+ language: canonicalProject.Language,
+ compilationOutputInfo: default,
+ checksumAlgorithm: SourceHashAlgorithm.Sha1,
+ filePath: newDocumentPath,
+ outputFilePath: canonicalProject.OutputFilePath,
+ outputRefFilePath: canonicalProject.OutputRefFilePath,
+ hasAllInformation: hasAllInformation);
+
+ var forkedProjectInfo = ProjectInfo.Create(
+ attributes: forkedProjectAttributes,
+ compilationOptions: canonicalProject.CompilationOptions,
+ parseOptions: canonicalProject.ParseOptions,
+ documents: [newDocumentInfo, .. await Task.WhenAll(canonicalProject.Documents.Select(document => GetDocumentInfoAsync(document, document.FilePath)))],
+ projectReferences: canonicalProject.ProjectReferences,
+ metadataReferences: canonicalProject.MetadataReferences,
+ analyzerReferences: canonicalProject.AnalyzerReferences,
+ analyzerConfigDocuments: await canonicalProject.AnalyzerConfigDocuments.SelectAsArrayAsync(async document => await GetDocumentInfoAsync(document, document.FilePath)),
+ additionalDocuments: await canonicalProject.AdditionalDocuments.SelectAsArrayAsync(async document => await GetDocumentInfoAsync(document, document.FilePath)));
+ return forkedProjectInfo;
+
+ async Task GetDocumentInfoAsync(TextDocument document, string? documentPath) =>
+ DocumentInfo.Create(
+ DocumentId.CreateNewId(forkedProjectId),
+ name: Path.GetFileName(documentPath) ?? "",
+ loader: TextLoader.From(TextAndVersion.Create(await document.GetTextAsync(cancellationToken).ConfigureAwait(false), VersionStamp.Create())),
+ filePath: documentPath);
+ }
+
private Project GetCanonicalProject()
{
var miscWorkspace = _workspaceFactory.MiscellaneousFilesWorkspaceProjectFactory.Workspace;
diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/FileBasedPrograms/FileBasedProgramsProjectSystem.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/FileBasedPrograms/FileBasedProgramsProjectSystem.cs
index 57ce37294adcb..643f7ae0ce796 100644
--- a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/FileBasedPrograms/FileBasedProgramsProjectSystem.cs
+++ b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/FileBasedPrograms/FileBasedProgramsProjectSystem.cs
@@ -71,7 +71,7 @@ public FileBasedProgramsProjectSystem(
private string GetDocumentFilePath(DocumentUri uri) => uri.ParsedUri is { } parsedUri ? ProtocolConversions.GetDocumentFilePathFromUri(parsedUri) : uri.UriString;
- public async ValueTask IsMiscellaneousFilesDocumentAsync(TextDocument document, CancellationToken cancellationToken)
+ public async ValueTask IsMiscellaneousFilesDocumentAsync(TextDocument textDocument, CancellationToken cancellationToken)
{
// There are a few cases here:
// 1. The document is a primordial document (either not loaded yet or doesn't support design time build) - it will be in the misc files workspace.
@@ -79,17 +79,36 @@ public async ValueTask IsMiscellaneousFilesDocumentAsync(TextDocument docu
// 3. The document is loaded as a file based program - then it will be in the main workspace where the project path matches the source file path.
// NB: The FileBasedProgramsProjectSystem uses the document file path (the on-disk path) as the projectPath in 'IsProjectLoadedAsync'.
- var isLoadedAsFileBasedProgram = document.FilePath is { } filePath && await IsProjectLoadedAsync(filePath, cancellationToken);
+ var isLoadedAsFileBasedProgram = textDocument.FilePath is { } filePath && await IsProjectLoadedAsync(filePath, cancellationToken);
// If this document has a file-based program syntactic marker, but we aren't loading it in a file-based programs project,
// we need the caller to remove and re-add this document, so that it gets put in a file-based programs project instead.
// See the check in 'LspWorkspaceManager.GetLspDocumentInfoAsync', which removes a document based on 'IsMiscellaneousFilesDocumentAsync' result,
// then calls 'GetLspDocumentInfoAsync' again for the same request.
- if (!isLoadedAsFileBasedProgram && VirtualProjectXmlProvider.IsFileBasedProgram(await document.GetTextAsync(cancellationToken)))
+ if (!isLoadedAsFileBasedProgram && VirtualProjectXmlProvider.IsFileBasedProgram(await textDocument.GetTextAsync(cancellationToken)))
return false;
- if (document.Project.Solution.Workspace == _workspaceFactory.MiscellaneousFilesWorkspaceProjectFactory.Workspace)
+ if (textDocument.Project.Solution.Workspace == _workspaceFactory.MiscellaneousFilesWorkspaceProjectFactory.Workspace)
+ {
+ // Do a check to determine if the misc project needs to be re-created with a new HasAllInformation flag value.
+ if (!isLoadedAsFileBasedProgram
+ && await _canonicalMiscFilesLoader.IsCanonicalProjectLoadedAsync(cancellationToken)
+ && textDocument is Document document
+ && await document.GetSyntaxTreeAsync(cancellationToken) is { } syntaxTree)
+ {
+ var newHasAllInformation = await VirtualProjectXmlProvider.HasTopLevelStatementsAsync(syntaxTree, cancellationToken);
+ if (newHasAllInformation != document.Project.State.HasAllInformation)
+ {
+ // TODO: replace this method and the call site in LspWorkspaceManager,
+ // with a mechanism for "updating workspace state if needed" based on changes to a document.
+ // Perhaps this could be based on actually listening for changes to particular documents, rather than whenever an LSP request related to a document comes in.
+ // We should be able to do more incremental updates in more cases, rather than needing to throw things away and start over.
+ return false;
+ }
+ }
+
return true;
+ }
if (isLoadedAsFileBasedProgram)
return true;
@@ -205,7 +224,7 @@ protected override ValueTask OnProjectUnloadedAsync(string projectFilePath)
return ValueTask.CompletedTask;
}
- protected override async ValueTask TransitionPrimordialProjectToLoadedAsync(
+ protected override async ValueTask TransitionPrimordialProjectToLoaded_NoLockAsync(
string projectPath,
ProjectSystemProjectFactory primordialProjectFactory,
ProjectId primordialProjectId,
diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/FileBasedPrograms/VirtualProjectXmlProvider.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/FileBasedPrograms/VirtualProjectXmlProvider.cs
index 9375c8c2e6611..dc55c129c7ca7 100644
--- a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/FileBasedPrograms/VirtualProjectXmlProvider.cs
+++ b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/FileBasedPrograms/VirtualProjectXmlProvider.cs
@@ -12,6 +12,7 @@
using System.Text.Json;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.Text;
@@ -119,6 +120,15 @@ internal static bool IsFileBasedProgram(SourceText text)
return false;
}
+ internal static async Task HasTopLevelStatementsAsync(SyntaxTree tree, CancellationToken cancellationToken)
+ {
+ var root = await tree.GetRootAsync(cancellationToken);
+ if (root is CompilationUnitSyntax compilationUnit)
+ return compilationUnit.Members.Any(member => member.IsKind(SyntaxKind.GlobalStatement));
+
+ return false;
+ }
+
#region Temporary copy of subset of dotnet run-api behavior for fallback: https://github.com/dotnet/roslyn/issues/78618
// See https://github.com/dotnet/sdk/blob/b5dbc69cc28676ac6ea615654c8016a11b75e747/src/Cli/Microsoft.DotNet.Cli.Utils/Sha256Hasher.cs#L10
private static class Sha256Hasher
diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/LanguageServerProjectLoader.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/LanguageServerProjectLoader.cs
index d0ff16d0b192d..16682a61fe3de 100644
--- a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/LanguageServerProjectLoader.cs
+++ b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/LanguageServerProjectLoader.cs
@@ -217,7 +217,7 @@ internal sealed record RemoteProjectLoadResult
/// Called when transitioning from a primordial project to loaded targets.
/// Subclasses can override this to transfer documents or perform other operations before the primordial project is removed.
///
- protected abstract ValueTask TransitionPrimordialProjectToLoadedAsync(
+ protected abstract ValueTask TransitionPrimordialProjectToLoaded_NoLockAsync(
string projectPath,
ProjectSystemProjectFactory primordialProjectFactory,
ProjectId primordialProjectId,
@@ -328,7 +328,7 @@ private async Task ReloadProjectAsync(ProjectToLoad projectToLoad, ToastEr
if (currentLoadState is ProjectLoadState.Primordial(var primordialProjectFactory, var projectId))
{
// Transition from primordial to loaded state
- await TransitionPrimordialProjectToLoadedAsync(projectPath, primordialProjectFactory, projectId, cancellationToken);
+ await TransitionPrimordialProjectToLoaded_NoLockAsync(projectPath, primordialProjectFactory, 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.
diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/LanguageServerProjectSystem.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/LanguageServerProjectSystem.cs
index 2e566ee82cfd9..33f11671c83e4 100644
--- a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/LanguageServerProjectSystem.cs
+++ b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/LanguageServerProjectSystem.cs
@@ -106,7 +106,7 @@ protected override ValueTask OnProjectUnloadedAsync(string projectFilePath)
return ValueTask.CompletedTask;
}
- protected override async ValueTask TransitionPrimordialProjectToLoadedAsync(
+ protected override async ValueTask TransitionPrimordialProjectToLoaded_NoLockAsync(
string projectPath,
ProjectSystemProjectFactory primordialProjectFactory,
ProjectId primordialProjectId,