Skip to content

Add metadata documents to the MAS workspace upfront #78886

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from

Conversation

dibarbet
Copy link
Member

@dibarbet dibarbet commented Jun 10, 2025

Precursor to changes to allow LSP to use virtual files for MAS. This modifies the MAS providers to put the generated document into the workspace upfront, instead of waiting for someone to attempt to open it. This does mean that we may keep files in the workspace for longer, but generally we were keeping the project and file info around anyway in order to service requests when the document is re-opened. We can remove those separate stores and just use the workspace as the document holder.

Additionally, this simplifies the current LSP MAS implementation (now the MAS workspace is just a normal workspace which we pull documents from), and will simplify the virtual file implementation (virtual files will be stored as documents in the workspace that LSP can query as it normally does). Will be working on that in a followup

Resolves #78421

Clean RPS - https://dev.azure.com/devdiv/DevDiv/_git/VS/pullrequest/644402

_keyToInformation.Clear();
}

private static (ProjectInfo, DocumentId) GenerateProjectAndDocumentInfo(
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

moved from metadataassourcegeneratedfileinfo

@dibarbet dibarbet marked this pull request as ready for review June 10, 2025 19:16
@dibarbet dibarbet requested a review from a team as a code owner June 10, 2025 19:16
@dibarbet dibarbet requested a review from jasonmalinowski June 10, 2025 19:16
@@ -50,15 +46,10 @@ internal sealed class DecompilationMetadataAsSourceFileProvider(IImplementationA
/// generally run concurrently. However, to be safe, we make this a concurrent dictionary to be safe to that
/// potentially happening.
/// </summary>
private readonly ConcurrentDictionary<string, MetadataAsSourceGeneratedFileInfo> _generatedFilenameToInformation = new(StringComparer.OrdinalIgnoreCase);
private readonly ConcurrentDictionary<string, (MetadataAsSourceGeneratedFileInfo Metadata, DocumentId DocumentId)> _generatedFilenameToInformation = new(StringComparer.OrdinalIgnoreCase);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

might it make sense for MetadataAsSourceGeneratedFileInfo to contain the DocId? or is that not possible?

Copy link
Member Author

@dibarbet dibarbet Jun 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It isn't immediately straightforward as the MetadataAsSourceGeneratedFileInfo is created before we create the document (and stored in a separate map). However, I am changing all this a bit more for the virtual file part - so I'll include this feedback in that PR if it ends up working.

@@ -383,50 +371,6 @@ public bool ShouldCollapseOnOpen(MetadataAsSourceWorkspace workspace, string fil
return _fileToDocumentInfoMap.TryGetValue(filePath, out _) && blockStructureOptions.CollapseMetadataImplementationsWhenFirstOpened;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm beginning to wonder why _fileToDocumentInfoMap needs to exist at all -- couldn't that now just be centrally held?

@@ -51,15 +50,6 @@ internal sealed class LspMiscellaneousFilesWorkspaceProvider(ILspServices lspSer
documentFilePath = ProtocolConversions.GetDocumentFilePathFromUri(uri.ParsedUri);
}

var container = new StaticSourceTextContainer(documentText);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we delete StaticSourceTextContainer too?

workspace.GetService<LspWorkspaceRegistrationService>().Register(workspace);
// By default, workspace event listeners are disabled in tests. Explicitly enable the LSP workspace registration event listener
// to ensure that the lsp workspace registration service sees all workspaces.
var lspWorkspaceRegistrationListener = (LspWorkspaceRegistrationEventListener)workspace.ExportProvider.GetExports<IEventListener>().Single(e => e.Value is LspWorkspaceRegistrationEventListener).Value;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
var lspWorkspaceRegistrationListener = (LspWorkspaceRegistrationEventListener)workspace.ExportProvider.GetExports<IEventListener>().Single(e => e.Value is LspWorkspaceRegistrationEventListener).Value;
var lspWorkspaceRegistrationListener = (LspWorkspaceRegistrationEventListener)workspace.ExportProvider.GetExportedValues<IEventListener>().Single(e => e is LspWorkspaceRegistrationEventListener);

Since there's not any metadata you're checking before creating everything anyways.

@@ -364,7 +365,11 @@ internal LspTestWorkspace CreateWorkspace(
composition ?? Composition, workspaceKind, configurationOptions: new WorkspaceConfigurationOptions(ValidateCompilationTrackerStates: true), supportsLspMutation: mutatingLspWorkspace);
options?.OptionUpdater?.Invoke(workspace.GetService<IGlobalOptionService>());

workspace.GetService<LspWorkspaceRegistrationService>().Register(workspace);
// By default, workspace event listeners are disabled in tests. Explicitly enable the LSP workspace registration event listener
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feels a bit fishy that we're not enabling the listeners generally, but I appreciate this makes the code behave a bit more like the product rather than the explicit registration going on. Should we have a tracking bug to investigate this further though?

@@ -1022,7 +1022,7 @@ await RunTestAsync(async path =>
var file = await service.GetGeneratedFileAsync(project.Solution.Workspace, project, symbol, signaturesOnly: false, options: MetadataAsSourceOptions.Default, cancellationToken: CancellationToken.None);

var result = service.TryRemoveDocumentFromWorkspace(file.FilePath);
Assert.False(result);
Assert.True(result);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this still be returning false? I'd assume we don't say we removed something if we didn't add it in the first place? It seems like this is missing a different return for the "if not open" case.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is deleted in the followup (open doesn't always make sense here)
#79023


// We do own the file, so let's open it up in our workspace
(var projectInfo, documentId) = fileInfo.GetProjectInfoAndDocumentId(workspace.Services.SolutionServices, loadFileFromDisk: true);
workspace.OnDocumentClosed(documentIdInfo.DocumentId, new WorkspaceFileTextLoader(workspace.Services.SolutionServices, fileInfo.TemporaryFilePath, MetadataAsSourceGeneratedFileInfo.Encoding));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have to do that check for open files here?

.GetRequiredDocument(temporaryDocumentId);

if (useDecompiler)
// Generate the file if it doesn't exist (we may still have it if there was a previous request for it that was then closed).
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I admit it's not clear to me the scenario for this comment...

}

// Mark read-only
new FileInfo(fileInfo.TemporaryFilePath).IsReadOnly = true;
if (!skipWritingFile && !File.Exists(fileInfo.TemporaryFilePath))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're still under the !File.Exists() check you added on line 100 in the new version; isn't that part always true?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is guarding against the file going away in between (though there is technically a mutex held there so not sure if it could)? I changed this in the followup though

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Decouple Metadata as Source from the LSP miscellaneous files workspace
3 participants