Skip to content

Commit e03909a

Browse files
authored
Add metadata documents to the MAS workspace upfront (#78886)
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
2 parents 0fa9df0 + be27b60 commit e03909a

19 files changed

+255
-442
lines changed

src/EditorFeatures/CSharpTest/PdbSourceDocument/AbstractPdbSourceDocumentTests.cs

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -156,15 +156,7 @@ protected static async Task GenerateFileAndVerifyAsync(
156156

157157
var pdbService = (PdbSourceDocumentMetadataAsSourceFileProvider)workspace.ExportProvider.GetExportedValues<IMetadataAsSourceFileProvider>().Single(s => s is PdbSourceDocumentMetadataAsSourceFileProvider);
158158

159-
// Add the document to the workspace. We provide an empty static source text as the API requires it to open the document.
160-
// We're not really trying to verify that the source text the editor hands to us is the right encoding - just that the document we added has the right encoding.
161-
var result = pdbService.TryAddDocumentToWorkspace((MetadataAsSourceWorkspace)masWorkspace!, file.FilePath, new StaticSourceTextContainer(SourceText.From(string.Empty)), out _);
162-
Assert.True(result);
163-
164-
// Immediately close the document so that we get the source text provided by the workspace (instead of the empty one we passed).
165159
var info = pdbService.GetTestAccessor().Documents[file.FilePath];
166-
masWorkspace!.OnDocumentClosed(info.DocumentId, new WorkspaceFileTextLoader(workspace.Services.SolutionServices, file.FilePath, info.Encoding));
167-
168160
var document = masWorkspace!.CurrentSolution.GetRequiredDocument(info.DocumentId);
169161

170162
// Mapping the project from the generated document should map back to the original project

src/EditorFeatures/CSharpTest/PdbSourceDocument/NullResultMetadataAsSourceFileProvider.cs

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -47,17 +47,6 @@ public void CleanupGeneratedFiles(MetadataAsSourceWorkspace workspace)
4747
return null;
4848
}
4949

50-
public bool TryAddDocumentToWorkspace(MetadataAsSourceWorkspace workspace, string filePath, Text.SourceTextContainer sourceTextContainer, [NotNullWhen(true)] out DocumentId? documentId)
51-
{
52-
documentId = null!;
53-
return true;
54-
}
55-
56-
public bool TryRemoveDocumentFromWorkspace(MetadataAsSourceWorkspace workspace, string filePath)
57-
{
58-
return true;
59-
}
60-
6150
public bool ShouldCollapseOnOpen(MetadataAsSourceWorkspace workspace, string filePath, BlockStructureOptions options)
6251
{
6352
return true;

src/EditorFeatures/CSharpTest/PdbSourceDocument/PdbSourceDocumentTests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -911,7 +911,7 @@ public class C
911911
var file = await service.GetGeneratedFileAsync(project.Solution.Workspace, project, symbol, signaturesOnly: false, options: MetadataAsSourceOptions.Default, cancellationToken: CancellationToken.None);
912912

913913
var result = service.TryRemoveDocumentFromWorkspace(file.FilePath);
914-
Assert.False(result);
914+
Assert.True(result);
915915
});
916916

917917
[Fact, WorkItem("https://github.com/dotnet/vscode-csharp/issues/7514")]
@@ -942,6 +942,6 @@ public class C
942942

943943
// Opening should still throw (should never be called as we should be able to find the previously
944944
// opened document in the MAS workspace).
945-
Assert.Throws<System.InvalidOperationException>(() => service.TryAddDocumentToWorkspace(fileTwo.FilePath, new StaticSourceTextContainer(SourceText.From(string.Empty)), out var documentIdTwo));
945+
Assert.Throws<System.ArgumentException>(() => service.TryAddDocumentToWorkspace(fileTwo.FilePath, new StaticSourceTextContainer(SourceText.From(string.Empty)), out var documentIdTwo));
946946
});
947947
}

src/Features/Core/Portable/MetadataAsSource/DecompilationMetadataAsSourceFileProvider.cs

Lines changed: 176 additions & 167 deletions
Large diffs are not rendered by default.

src/Features/Core/Portable/MetadataAsSource/IMetadataAsSourceFileProvider.cs

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -32,18 +32,6 @@ internal interface IMetadataAsSourceFileProvider
3232
/// </summary>
3333
void CleanupGeneratedFiles(MetadataAsSourceWorkspace workspace);
3434

35-
/// <summary>
36-
/// Called when the file returned from <see cref="GetGeneratedFileAsync"/> needs to be added to the workspace,
37-
/// to be opened. Will be called on the main thread of the workspace host.
38-
/// </summary>
39-
bool TryAddDocumentToWorkspace(MetadataAsSourceWorkspace workspace, string filePath, SourceTextContainer sourceTextContainer, [NotNullWhen(true)] out DocumentId? documentId);
40-
41-
/// <summary>
42-
/// Called when the file is being closed, and so needs to be removed from the workspace. Will be called on the
43-
/// main thread of the workspace host.
44-
/// </summary>
45-
bool TryRemoveDocumentFromWorkspace(MetadataAsSourceWorkspace workspace, string filePath);
46-
4735
/// <summary>
4836
/// Called to determine if the file should be collapsed by default when opened for the first time. Will be
4937
/// called on the main thread of the workspace host.

src/Features/Core/Portable/MetadataAsSource/MetadataAsSourceFileService.cs

Lines changed: 36 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -167,47 +167,56 @@ private static void AssertIsMainThread(MetadataAsSourceWorkspace workspace)
167167

168168
public bool TryAddDocumentToWorkspace(string filePath, SourceTextContainer sourceTextContainer, [NotNullWhen(true)] out DocumentId? documentId)
169169
{
170-
// If we haven't even created a MetadataAsSource workspace yet, then this file definitely cannot be added to
171-
// it. This happens when the MiscWorkspace calls in to just see if it can attach this document to the
172-
// MetadataAsSource instead of itself.
173170
var workspace = _workspace;
174-
if (workspace != null)
171+
if (workspace is null)
175172
{
176-
foreach (var provider in _providers.Value)
177-
{
178-
if (!provider.IsValueCreated)
179-
continue;
173+
// If we haven't even created a MetadataAsSource workspace yet, then this file definitely cannot be added to
174+
// it. This happens when the MiscWorkspace calls in to just see if it can attach this document to the
175+
// MetadataAsSource instead of itself.
176+
documentId = null;
177+
return false;
178+
}
180179

181-
if (provider.Value.TryAddDocumentToWorkspace(workspace, filePath, sourceTextContainer, out documentId))
182-
{
183-
return true;
184-
}
185-
}
180+
// There are no linked files in the MetadataAsSource workspace, so we can just use the first document id
181+
documentId = workspace.CurrentSolution.GetDocumentIdsWithFilePath(filePath).SingleOrDefault();
182+
if (documentId is null)
183+
{
184+
return false;
186185
}
187186

188-
documentId = null;
189-
return false;
187+
workspace.OnDocumentOpened(documentId, sourceTextContainer);
188+
return true;
190189
}
191190

192191
public bool TryRemoveDocumentFromWorkspace(string filePath)
193192
{
194-
// If we haven't even created a MetadataAsSource workspace yet, then this file definitely cannot be removed
195-
// from it. This happens when the MiscWorkspace is hearing about a doc closing, and calls into the
196-
// MetadataAsSource system to see if it owns the file and should handle that event.
197193
var workspace = _workspace;
198-
if (workspace != null)
194+
if (workspace is null)
199195
{
200-
foreach (var provider in _providers.Value)
201-
{
202-
if (!provider.IsValueCreated)
203-
continue;
196+
// If we haven't even created a MetadataAsSource workspace yet, then this file definitely cannot be removed
197+
// from it. This happens when the MiscWorkspace is hearing about a doc closing, and calls into the
198+
// MetadataAsSource system to see if it owns the file and should handle that event.
199+
return false;
200+
}
204201

205-
if (provider.Value.TryRemoveDocumentFromWorkspace(workspace, filePath))
206-
return true;
207-
}
202+
// There are no linked files in the MetadataAsSource workspace, so we can just use the first document id
203+
var documentId = workspace.CurrentSolution.GetDocumentIdsWithFilePath(filePath).FirstOrDefault();
204+
if (documentId is null)
205+
{
206+
return false;
208207
}
209208

210-
return false;
209+
// In LSP, while calls to TryAddDocumentToWorkspace and TryRemoveDocumentFromWorkspace are handled
210+
// serially, it is possible that TryRemoveDocumentFromWorkspace called without TryAddDocumentToWorkspace first.
211+
// This can happen if the document is immediately closed after opening - only feature requests that force us
212+
// to materialize a solution will trigger TryAddDocumentToWorkspace, if none are made it is never called.
213+
// However TryRemoveDocumentFromWorkspace is always called on close.
214+
if (workspace.GetOpenDocumentIds().Contains(documentId))
215+
{
216+
workspace.OnDocumentClosed(documentId, new WorkspaceFileTextLoader(workspace.Services.SolutionServices, filePath, defaultEncoding: null));
217+
}
218+
219+
return true;
211220
}
212221

213222
public bool ShouldCollapseOnOpen(string? filePath, BlockStructureOptions blockStructureOptions)

src/Features/Core/Portable/MetadataAsSource/MetadataAsSourceGeneratedFileInfo.cs

Lines changed: 10 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -3,27 +3,23 @@
33
// See the LICENSE file in the project root for more information.
44

55
using System;
6-
using System.Collections.Immutable;
76
using System.IO;
87
using System.Text;
9-
using Microsoft.CodeAnalysis.Host;
108
using Microsoft.CodeAnalysis.Text;
119

1210
namespace Microsoft.CodeAnalysis.MetadataAsSource;
1311

1412
internal sealed class MetadataAsSourceGeneratedFileInfo
1513
{
16-
public readonly ProjectId SourceProjectId;
17-
public readonly Workspace Workspace;
14+
public string TemporaryFilePath { get; }
15+
public Workspace Workspace { get; }
16+
public ProjectId SourceProjectId { get; }
17+
public bool SignaturesOnly { get; }
18+
public string LanguageName { get; }
19+
public string Extension { get; }
1820

19-
public readonly AssemblyIdentity AssemblyIdentity;
20-
public readonly string LanguageName;
21-
public readonly bool SignaturesOnly;
22-
public readonly ImmutableArray<MetadataReference> References;
23-
24-
public readonly string TemporaryFilePath;
25-
26-
private readonly ParseOptions? _parseOptions;
21+
public static Encoding Encoding => Encoding.UTF8;
22+
public static SourceHashAlgorithm ChecksumAlgorithm => SourceHashAlgorithms.Default;
2723

2824
public MetadataAsSourceGeneratedFileInfo(string rootPath, Workspace sourceWorkspace, Project sourceProject, INamedTypeSymbol topLevelNamedType, bool signaturesOnly)
2925
{
@@ -32,78 +28,9 @@ public MetadataAsSourceGeneratedFileInfo(string rootPath, Workspace sourceWorksp
3228
this.LanguageName = signaturesOnly ? sourceProject.Language : LanguageNames.CSharp;
3329
this.SignaturesOnly = signaturesOnly;
3430

35-
_parseOptions = sourceProject.Language == LanguageName
36-
? sourceProject.ParseOptions
37-
: sourceProject.Solution.Services.GetLanguageServices(LanguageName).GetRequiredService<ISyntaxTreeFactoryService>().GetDefaultParseOptionsWithLatestLanguageVersion();
38-
39-
this.References = [.. sourceProject.MetadataReferences];
40-
this.AssemblyIdentity = topLevelNamedType.ContainingAssembly.Identity;
41-
42-
var extension = LanguageName == LanguageNames.CSharp ? ".cs" : ".vb";
31+
this.Extension = LanguageName == LanguageNames.CSharp ? ".cs" : ".vb";
4332

4433
var directoryName = Guid.NewGuid().ToString("N");
45-
this.TemporaryFilePath = Path.Combine(rootPath, directoryName, topLevelNamedType.Name + extension);
46-
}
47-
48-
public static Encoding Encoding => Encoding.UTF8;
49-
public static SourceHashAlgorithm ChecksumAlgorithm => SourceHashAlgorithms.Default;
50-
51-
/// <summary>
52-
/// Creates a ProjectInfo to represent the fake project created for metadata as source documents.
53-
/// </summary>
54-
/// <param name="services">Solution services.</param>
55-
/// <param name="loadFileFromDisk">Whether the source file already exists on disk and should be included. If
56-
/// this is a false, a document is still created, but it's not backed by the file system and thus we won't
57-
/// try to load it.</param>
58-
public (ProjectInfo, DocumentId) GetProjectInfoAndDocumentId(SolutionServices services, bool loadFileFromDisk)
59-
{
60-
var projectId = ProjectId.CreateNewId();
61-
62-
// Just say it's always a DLL since we probably won't have a Main method
63-
var compilationOptions = services.GetRequiredLanguageService<ICompilationFactoryService>(LanguageName).GetDefaultCompilationOptions().WithOutputKind(OutputKind.DynamicallyLinkedLibrary);
64-
65-
var extension = LanguageName == LanguageNames.CSharp ? ".cs" : ".vb";
66-
67-
// We need to include the version information of the assembly so InternalsVisibleTo and stuff works
68-
var assemblyInfoDocumentId = DocumentId.CreateNewId(projectId);
69-
var assemblyInfoFileName = "AssemblyInfo" + extension;
70-
var assemblyInfoString = LanguageName == LanguageNames.CSharp
71-
? string.Format(@"[assembly: System.Reflection.AssemblyVersion(""{0}"")]", AssemblyIdentity.Version)
72-
: string.Format(@"<Assembly: System.Reflection.AssemblyVersion(""{0}"")>", AssemblyIdentity.Version);
73-
74-
var assemblyInfoSourceText = SourceText.From(assemblyInfoString, Encoding, ChecksumAlgorithm);
75-
76-
var assemblyInfoDocument = DocumentInfo.Create(
77-
assemblyInfoDocumentId,
78-
assemblyInfoFileName,
79-
loader: TextLoader.From(assemblyInfoSourceText.Container, VersionStamp.Default),
80-
filePath: null,
81-
isGenerated: true)
82-
.WithDesignTimeOnly(true);
83-
84-
var generatedDocumentId = DocumentId.CreateNewId(projectId);
85-
var generatedDocument = DocumentInfo.Create(
86-
generatedDocumentId,
87-
Path.GetFileName(TemporaryFilePath),
88-
loader: loadFileFromDisk ? new WorkspaceFileTextLoader(services, TemporaryFilePath, Encoding) : null,
89-
filePath: TemporaryFilePath,
90-
isGenerated: true)
91-
.WithDesignTimeOnly(true);
92-
93-
var projectInfo = ProjectInfo.Create(
94-
new ProjectInfo.ProjectAttributes(
95-
id: projectId,
96-
version: VersionStamp.Default,
97-
name: AssemblyIdentity.Name,
98-
assemblyName: AssemblyIdentity.Name,
99-
language: LanguageName,
100-
compilationOutputInfo: default,
101-
checksumAlgorithm: ChecksumAlgorithm),
102-
compilationOptions: compilationOptions,
103-
parseOptions: _parseOptions,
104-
documents: [assemblyInfoDocument, generatedDocument],
105-
metadataReferences: References);
106-
107-
return (projectInfo, generatedDocumentId);
34+
this.TemporaryFilePath = Path.Combine(rootPath, directoryName, topLevelNamedType.Name + Extension);
10835
}
10936
}

src/Features/Core/Portable/PdbSourceDocument/PdbSourceDocumentMetadataAsSourceFileProvider.cs

Lines changed: 6 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,6 @@ internal sealed class PdbSourceDocumentMetadataAsSourceFileProvider(
4444
private readonly IImplementationAssemblyLookupService _implementationAssemblyLookupService = implementationAssemblyLookupService;
4545
private readonly IPdbSourceDocumentLogger? _logger = logger;
4646

47-
/// <summary>
48-
/// Lock to guard access to workspace updates when opening / closing documents.
49-
/// </summary>
50-
private readonly object _gate = new();
51-
5247
/// <summary>
5348
/// Accessed only in <see cref="GetGeneratedFileAsync"/> and <see cref="CleanupGeneratedFiles"/>, both of which
5449
/// are called under a lock in <see cref="MetadataAsSourceFileService"/>. So this is safe as a plain
@@ -70,11 +65,6 @@ internal sealed class PdbSourceDocumentMetadataAsSourceFileProvider(
7065
/// </summary>
7166
private readonly ConcurrentDictionary<string, SourceDocumentInfo> _fileToDocumentInfoMap = new(StringComparer.OrdinalIgnoreCase);
7267

73-
/// <summary>
74-
/// Only accessed and mutated in serial calls either from the UI thread or LSP queue.
75-
/// </summary>
76-
private readonly HashSet<DocumentId> _openedDocumentIds = [];
77-
7868
public async Task<MetadataAsSourceFile?> GetGeneratedFileAsync(
7969
MetadataAsSourceWorkspace metadataWorkspace,
8070
Workspace sourceWorkspace,
@@ -262,24 +252,22 @@ internal sealed class PdbSourceDocumentMetadataAsSourceFileProvider(
262252

263253
var symbolId = SymbolKey.Create(symbol, cancellationToken);
264254

265-
// Get a view of the solution with the document added, but do not actually update the workspace.
266-
// TryAddDocumentToWorkspace is responsible for actually updating the solution with the new document(s).
267-
// We just need a view with the document added so we can find the right location in the generated source.
268-
var pendingSolution = metadataWorkspace.CurrentSolution;
255+
var navigateProject = metadataWorkspace.CurrentSolution.GetRequiredProject(projectId);
269256
var documentInfos = CreateDocumentInfos(sourceFileInfos, encoding, projectId, sourceWorkspace, sourceProject);
270257
if (documentInfos.Length > 0)
271258
{
272259
foreach (var documentInfo in documentInfos)
273260
{
274261
// The document might have already been added by a previous go to definition call.
275-
if (!pendingSolution.ContainsDocument(documentInfo.Id))
262+
if (!metadataWorkspace.CurrentSolution.ContainsDocument(documentInfo.Id))
276263
{
277-
pendingSolution = pendingSolution.AddDocument(documentInfo);
264+
metadataWorkspace.OnDocumentAdded(documentInfo);
278265
}
279266
}
280-
}
281267

282-
var navigateProject = pendingSolution.GetRequiredProject(projectId);
268+
// Get a new view of the project with the documents added.
269+
navigateProject = metadataWorkspace.CurrentSolution.GetRequiredProject(projectId);
270+
}
283271

284272
// If MetadataAsSourceHelpers.GetLocationInGeneratedSourceAsync can't find the actual document to navigate to, it will fall back
285273
// to the document passed in, which we just use the first document for.
@@ -383,50 +371,6 @@ public bool ShouldCollapseOnOpen(MetadataAsSourceWorkspace workspace, string fil
383371
return _fileToDocumentInfoMap.TryGetValue(filePath, out _) && blockStructureOptions.CollapseMetadataImplementationsWhenFirstOpened;
384372
}
385373

386-
public bool TryAddDocumentToWorkspace(MetadataAsSourceWorkspace workspace, string filePath, SourceTextContainer sourceTextContainer, [NotNullWhen(true)] out DocumentId? documentId)
387-
{
388-
lock (_gate)
389-
{
390-
if (_fileToDocumentInfoMap.TryGetValue(filePath, out var info))
391-
{
392-
Contract.ThrowIfTrue(_openedDocumentIds.Contains(info.DocumentId));
393-
394-
workspace.OnDocumentAdded(info.DocumentInfo);
395-
workspace.OnDocumentOpened(info.DocumentId, sourceTextContainer);
396-
documentId = info.DocumentId;
397-
_openedDocumentIds.Add(documentId);
398-
return true;
399-
}
400-
401-
documentId = null;
402-
return false;
403-
}
404-
}
405-
406-
public bool TryRemoveDocumentFromWorkspace(MetadataAsSourceWorkspace workspace, string filePath)
407-
{
408-
lock (_gate)
409-
{
410-
if (_fileToDocumentInfoMap.TryGetValue(filePath, out var info))
411-
{
412-
// In LSP, while calls to TryAddDocumentToWorkspace and TryRemoveDocumentFromWorkspace are handled
413-
// serially, it is possible that TryRemoveDocumentFromWorkspace called without TryAddDocumentToWorkspace first.
414-
// This can happen if the document is immediately closed after opening - only feature requests that force us
415-
// to materialize a solution will trigger TryAddDocumentToWorkspace, if none are made it is never called.
416-
// However TryRemoveDocumentFromWorkspace is always called on close.
417-
if (_openedDocumentIds.Contains(info.DocumentId))
418-
{
419-
workspace.OnDocumentClosed(info.DocumentId, new WorkspaceFileTextLoader(workspace.Services.SolutionServices, filePath, info.Encoding));
420-
workspace.OnDocumentRemoved(info.DocumentId);
421-
_openedDocumentIds.Remove(info.DocumentId);
422-
return true;
423-
}
424-
}
425-
426-
return false;
427-
}
428-
}
429-
430374
public Project? MapDocument(Document document)
431375
{
432376
if (document.FilePath is not null &&
@@ -462,7 +406,6 @@ public void CleanupGeneratedFiles(MetadataAsSourceWorkspace workspace)
462406

463407
// The MetadataAsSourceFileService will clean up the entire temp folder so no need to do anything here
464408
_fileToDocumentInfoMap.Clear();
465-
_openedDocumentIds.Clear();
466409
_sourceLinkEnabledProjects.Clear();
467410
_implementationAssemblyLookupService.Clear();
468411
}

0 commit comments

Comments
 (0)