Skip to content

Commit 225ca71

Browse files
authored
Ensure LspWorkspaceManager returns solutions without misc document when file moved (#80535)
* Add test demonstrating issue with document context in FBP * Fix GetTextDocumentAsync returning misc document preferentially over host document * fixes * Alternative approach: Remove from misc files and re-lookup document * Reword comment * Review feedback
1 parent 5b55ed4 commit 225ca71

File tree

6 files changed

+31
-21
lines changed

6 files changed

+31
-21
lines changed

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,10 +104,10 @@ TextDocument AddPrimordialDocument(DocumentUri uri, SourceText documentText, str
104104
}
105105
}
106106

107-
public async ValueTask TryRemoveMiscellaneousDocumentAsync(DocumentUri uri)
107+
public async ValueTask<bool> TryRemoveMiscellaneousDocumentAsync(DocumentUri uri)
108108
{
109109
var documentPath = GetDocumentFilePath(uri);
110-
await UnloadProjectAsync(documentPath);
110+
return await TryUnloadProjectAsync(documentPath);
111111
}
112112

113113
protected override async Task<RemoteProjectLoadResult?> TryLoadProjectInMSBuildHostAsync(

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -461,15 +461,15 @@ protected async Task BeginLoadingProjectAsync(string projectPath, string? projec
461461

462462
protected Task WaitForProjectsToFinishLoadingAsync() => _projectsToReload.WaitUntilCurrentBatchCompletesAsync();
463463

464-
protected async ValueTask UnloadProjectAsync(string projectPath)
464+
protected async ValueTask<bool> TryUnloadProjectAsync(string projectPath)
465465
{
466466
using (await _gate.DisposableWaitAsync(CancellationToken.None))
467467
{
468468
if (!_loadedProjects.Remove(projectPath, out var loadState))
469469
{
470470
// It is common to be called with a path to a project which is already not loaded.
471471
// In this case, we should do nothing.
472-
return;
472+
return false;
473473
}
474474

475475
if (loadState is ProjectLoadState.Primordial(var projectFactory, var projectId))
@@ -491,5 +491,6 @@ protected async ValueTask UnloadProjectAsync(string projectPath)
491491
}
492492

493493
await OnProjectUnloadedAsync(projectPath);
494+
return true;
494495
}
495496
}

src/LanguageServer/Protocol.TestUtilities/AbstractLspMiscellaneousFilesWorkspaceTests.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,7 @@ public async Task TestLspTransfersFromMiscellaneousFilesToHostWorkspaceAsync(boo
348348
AssertEx.NotNull(miscDocument);
349349
var miscText = await miscDocument.GetTextAsync(CancellationToken.None);
350350
Assert.Equal("More LSP textLSP text", miscText.ToString());
351+
Assert.True(await testLspServer.GetManagerAccessor().IsMiscellaneousFilesDocumentAsync(miscDocument));
351352

352353
// Update the registered workspace with the new document.
353354
var newDocumentId = (await AddDocumentAsync(testLspServer, newDocumentFilePath, "New Doc")).Id;
@@ -361,12 +362,16 @@ public async Task TestLspTransfersFromMiscellaneousFilesToHostWorkspaceAsync(boo
361362
// Verify we still are using the tracked LSP text for the document.
362363
var documentText = await document.GetTextAsync(CancellationToken.None);
363364
Assert.Equal("More LSP textLSP text", documentText.ToString());
365+
366+
// There should not be any other misc document in the solution anymore.
367+
var matchingDocuments = await document.Project.Solution.GetTextDocumentsAsync(newDocumentUri, CancellationToken.None);
368+
Assert.Single(matchingDocuments);
364369
}
365370

366371
private protected abstract ValueTask<Document> AddDocumentAsync(TestLspServer testLspServer, string filePath, string content);
367372
private protected abstract Workspace GetHostWorkspace(TestLspServer testLspServer);
368373

369-
private static async Task<(Workspace? workspace, Document? document)> GetLspWorkspaceAndDocumentAsync(DocumentUri uri, TestLspServer testLspServer)
374+
private protected static async Task<(Workspace? workspace, Document? document)> GetLspWorkspaceAndDocumentAsync(DocumentUri uri, TestLspServer testLspServer)
370375
{
371376
var (workspace, _, document) = await testLspServer.GetManager().GetLspDocumentInfoAsync(CreateTextDocumentIdentifier(uri), CancellationToken.None).ConfigureAwait(false);
372377
return (workspace, document as Document);

src/LanguageServer/Protocol/Workspaces/ILspMiscellaneousFilesWorkspaceProvider.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,5 +36,6 @@ internal interface ILspMiscellaneousFilesWorkspaceProvider : ILspService
3636
/// Note that the implementation of this method should not depend on anything expensive such as RPC calls.
3737
/// async is used here to allow taking locks asynchronously and "relatively fast" stuff like that.
3838
/// </summary>
39-
ValueTask TryRemoveMiscellaneousDocumentAsync(DocumentUri uri);
39+
/// <returns><see langword="true"/> when a document was found and removed</returns>
40+
ValueTask<bool> TryRemoveMiscellaneousDocumentAsync(DocumentUri uri);
4041
}

src/LanguageServer/Protocol/Workspaces/LspMiscellaneousFilesWorkspaceProvider.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ public ValueTask<bool> IsMiscellaneousFilesDocumentAsync(TextDocument document,
8383
/// Calls to this method and <see cref="AddMiscellaneousDocument(DocumentUri, SourceText, string, ILspLogger)"/> are made
8484
/// from LSP text sync request handling which do not run concurrently.
8585
/// </summary>
86-
public ValueTask TryRemoveMiscellaneousDocumentAsync(DocumentUri uri)
86+
public ValueTask<bool> TryRemoveMiscellaneousDocumentAsync(DocumentUri uri)
8787
{
8888
// We'll only ever have a single document matching this URI in the misc solution.
8989
var matchingDocument = CurrentSolution.GetDocumentIds(uri).SingleOrDefault();
@@ -102,9 +102,11 @@ public ValueTask TryRemoveMiscellaneousDocumentAsync(DocumentUri uri)
102102
// so it should never have other documents in it.
103103
var project = CurrentSolution.GetRequiredProject(matchingDocument.ProjectId);
104104
OnProjectRemoved(project.Id);
105+
106+
return ValueTask.FromResult(true);
105107
}
106108

107-
return ValueTask.CompletedTask;
109+
return ValueTask.FromResult(false);
108110
}
109111

110112
public ValueTask UpdateTextIfPresentAsync(DocumentId documentId, SourceText sourceText, CancellationToken cancellationToken)

src/LanguageServer/Protocol/Workspaces/LspWorkspaceManager.cs

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -251,28 +251,29 @@ public void UpdateTrackedDocument(DocumentUri uri, SourceText newSourceText, int
251251
foreach (var (workspace, lspSolution, isForked) in lspSolutions)
252252
{
253253
var documents = await lspSolution.GetTextDocumentsAsync(textDocumentIdentifier.DocumentUri, cancellationToken).ConfigureAwait(false);
254+
254255
if (documents.Length > 0)
255256
{
256-
// We have at least one document, so find the one in the right project context
257+
// We have at least one document, so find the one in the right project context.
257258
var document = documents.FindDocumentInProjectContext(textDocumentIdentifier, (sln, id) => sln.GetRequiredTextDocument(id));
258259

259260
if (_lspMiscellaneousFilesWorkspaceProvider is not null)
260261
{
261-
// If we started with multiple documents and didn't have specific context information, it's possible we picked a miscellaneous files document when
262-
// we could have picked a real one.
263-
if (documents.Length > 1 && await _lspMiscellaneousFilesWorkspaceProvider.IsMiscellaneousFilesDocumentAsync(document, cancellationToken).ConfigureAwait(false))
264-
{
265-
// Pick a different one; our choice here is arbitrary, since if we had a specified context in the first place we would have picked the right one.
266-
document = documents.First(d => d != document);
267-
}
268-
269-
// If we found the document in a non-misc workspace (either immediately or by the correction above), also attempt to remove it from the misc workspace
270-
// if it happens to be in there as well.
271-
if (_lspMiscellaneousFilesWorkspaceProvider is not null && !await _lspMiscellaneousFilesWorkspaceProvider.IsMiscellaneousFilesDocumentAsync(document, cancellationToken).ConfigureAwait(false))
262+
// It is possible that a document that was previously a misc file is now part of a real workspace (e.g. project system told us about a file we already had open).
263+
// If we found a non-misc document, we should clean up any references to it in the misc provider.
264+
var foundNonMiscDocument = await documents
265+
.AnyAsync(async doc => !await _lspMiscellaneousFilesWorkspaceProvider.IsMiscellaneousFilesDocumentAsync(doc, cancellationToken).ConfigureAwait(false))
266+
.ConfigureAwait(false);
267+
if (foundNonMiscDocument)
272268
{
273269
try
274270
{
275-
await _lspMiscellaneousFilesWorkspaceProvider.TryRemoveMiscellaneousDocumentAsync(uri).ConfigureAwait(false);
271+
var didRemove = await _lspMiscellaneousFilesWorkspaceProvider.TryRemoveMiscellaneousDocumentAsync(uri).ConfigureAwait(false);
272+
if (didRemove)
273+
{
274+
// If we actually removed something, lookup the document again to ensure we return updated solutions without the misc document.
275+
return await GetLspDocumentInfoAsync(textDocumentIdentifier, cancellationToken).ConfigureAwait(false);
276+
}
276277
}
277278
catch (Exception ex) when (FatalError.ReportAndCatch(ex))
278279
{

0 commit comments

Comments
 (0)