Skip to content

Commit 9177306

Browse files
authored
Merge pull request #74488 from dibarbet/metadata_language_features
Adds support for metadata LSP language features in VSCode
2 parents 1ff0474 + 3940a4f commit 9177306

File tree

13 files changed

+301
-91
lines changed

13 files changed

+301
-91
lines changed

src/EditorFeatures/CSharpTest/PdbSourceDocument/NullResultMetadataAsSourceFileProvider.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
using System;
66
using System.Composition;
7+
using System.Diagnostics.CodeAnalysis;
78
using System.Threading;
89
using System.Threading.Tasks;
910
using Microsoft.CodeAnalysis.Formatting;
@@ -47,8 +48,9 @@ public void CleanupGeneratedFiles(MetadataAsSourceWorkspace workspace)
4748
return null;
4849
}
4950

50-
public bool TryAddDocumentToWorkspace(MetadataAsSourceWorkspace workspace, string filePath, Text.SourceTextContainer sourceTextContainer)
51+
public bool TryAddDocumentToWorkspace(MetadataAsSourceWorkspace workspace, string filePath, Text.SourceTextContainer sourceTextContainer, [NotNullWhen(true)] out DocumentId? documentId)
5152
{
53+
documentId = null!;
5254
return true;
5355
}
5456

src/EditorFeatures/Test/MetadataAsSource/AbstractMetadataAsSourceTests.TestContext.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -300,7 +300,7 @@ internal Document GetDocument(MetadataAsSourceFile file)
300300
using var reader = File.OpenRead(file.FilePath);
301301
var stringText = EncodedStringText.Create(reader);
302302

303-
Assert.True(_metadataAsSourceService.TryAddDocumentToWorkspace(file.FilePath, stringText.Container));
303+
Assert.True(_metadataAsSourceService.TryAddDocumentToWorkspace(file.FilePath, stringText.Container, out var _));
304304

305305
return stringText.Container.GetRelatedDocuments().Single();
306306
}

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

Lines changed: 41 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using System.Collections.Concurrent;
77
using System.Collections.Generic;
88
using System.Composition;
9+
using System.Diagnostics.CodeAnalysis;
910
using System.Globalization;
1011
using System.IO;
1112
using System.Linq;
@@ -33,6 +34,11 @@ internal class DecompilationMetadataAsSourceFileProvider(IImplementationAssembly
3334
{
3435
internal const string ProviderName = "Decompilation";
3536

37+
/// <summary>
38+
/// Guards access to <see cref="_openedDocumentIds"/> and workspace updates when opening / closing documents.
39+
/// </summary>
40+
private readonly object _gate = new();
41+
3642
/// <summary>
3743
/// Accessed only in <see cref="GetGeneratedFileAsync"/> and <see cref="CleanupGeneratedFiles"/>, both of which
3844
/// are called under a lock in <see cref="MetadataAsSourceFileService"/>. So this is safe as a plain
@@ -271,17 +277,8 @@ private async Task<Location> RelocateSymbol_NoLockAsync(Solution solution, Metad
271277
return await MetadataAsSourceHelpers.GetLocationInGeneratedSourceAsync(symbolId, temporaryDocument, cancellationToken).ConfigureAwait(false);
272278
}
273279

274-
private static void AssertIsMainThread(MetadataAsSourceWorkspace workspace)
275-
{
276-
Contract.ThrowIfNull(workspace);
277-
var threadingService = workspace.Services.GetRequiredService<IWorkspaceThreadingServiceProvider>().Service;
278-
Contract.ThrowIfFalse(threadingService.IsOnMainThread);
279-
}
280-
281280
public bool ShouldCollapseOnOpen(MetadataAsSourceWorkspace workspace, string filePath, BlockStructureOptions blockStructureOptions)
282281
{
283-
AssertIsMainThread(workspace);
284-
285282
if (_generatedFilenameToInformation.TryGetValue(filePath, out var info))
286283
{
287284
return info.SignaturesOnly
@@ -292,45 +289,46 @@ public bool ShouldCollapseOnOpen(MetadataAsSourceWorkspace workspace, string fil
292289
return false;
293290
}
294291

295-
public bool TryAddDocumentToWorkspace(MetadataAsSourceWorkspace workspace, string filePath, SourceTextContainer sourceTextContainer)
292+
public bool TryAddDocumentToWorkspace(MetadataAsSourceWorkspace workspace, string filePath, SourceTextContainer sourceTextContainer, [NotNullWhen(true)] out DocumentId? documentId)
296293
{
297-
AssertIsMainThread(workspace);
298-
299-
if (_generatedFilenameToInformation.TryGetValue(filePath, out var fileInfo))
294+
lock (_gate)
300295
{
301-
Contract.ThrowIfTrue(_openedDocumentIds.ContainsKey(fileInfo));
296+
if (_generatedFilenameToInformation.TryGetValue(filePath, out var fileInfo))
297+
{
298+
Contract.ThrowIfTrue(_openedDocumentIds.ContainsKey(fileInfo));
302299

303-
// We do own the file, so let's open it up in our workspace
304-
var (projectInfo, documentId) = fileInfo.GetProjectInfoAndDocumentId(workspace.Services.SolutionServices, loadFileFromDisk: true);
300+
// We do own the file, so let's open it up in our workspace
301+
(var projectInfo, documentId) = fileInfo.GetProjectInfoAndDocumentId(workspace.Services.SolutionServices, loadFileFromDisk: true);
305302

306-
workspace.OnProjectAdded(projectInfo);
307-
workspace.OnDocumentOpened(documentId, sourceTextContainer);
303+
workspace.OnProjectAdded(projectInfo);
304+
workspace.OnDocumentOpened(documentId, sourceTextContainer);
308305

309-
_openedDocumentIds = _openedDocumentIds.Add(fileInfo, documentId);
306+
_openedDocumentIds = _openedDocumentIds.Add(fileInfo, documentId);
307+
return true;
308+
}
310309

311-
return true;
310+
documentId = null;
311+
return false;
312312
}
313-
314-
return false;
315313
}
316314

317315
public bool TryRemoveDocumentFromWorkspace(MetadataAsSourceWorkspace workspace, string filePath)
318316
{
319-
AssertIsMainThread(workspace);
320-
321-
if (_generatedFilenameToInformation.TryGetValue(filePath, out var fileInfo))
317+
lock (_gate)
322318
{
323-
if (_openedDocumentIds.ContainsKey(fileInfo))
324-
return RemoveDocumentFromWorkspace(workspace, fileInfo);
325-
}
319+
if (_generatedFilenameToInformation.TryGetValue(filePath, out var fileInfo))
320+
{
321+
if (_openedDocumentIds.ContainsKey(fileInfo))
322+
return RemoveDocumentFromWorkspace_NoLock(workspace, fileInfo);
323+
}
326324

327-
return false;
325+
return false;
326+
}
328327
}
329328

330-
private bool RemoveDocumentFromWorkspace(MetadataAsSourceWorkspace workspace, MetadataAsSourceGeneratedFileInfo fileInfo)
329+
private bool RemoveDocumentFromWorkspace_NoLock(MetadataAsSourceWorkspace workspace, MetadataAsSourceGeneratedFileInfo fileInfo)
331330
{
332-
AssertIsMainThread(workspace);
333-
331+
// Serial access is guaranteed by the caller.
334332
var documentId = _openedDocumentIds.GetValueOrDefault(fileInfo);
335333
Contract.ThrowIfNull(documentId);
336334

@@ -359,16 +357,19 @@ private bool RemoveDocumentFromWorkspace(MetadataAsSourceWorkspace workspace, Me
359357

360358
public void CleanupGeneratedFiles(MetadataAsSourceWorkspace workspace)
361359
{
362-
// Clone the list so we don't break our own enumeration
363-
foreach (var generatedFileInfo in _generatedFilenameToInformation.Values.ToList())
360+
lock (_gate)
364361
{
365-
if (_openedDocumentIds.ContainsKey(generatedFileInfo))
366-
RemoveDocumentFromWorkspace(workspace, generatedFileInfo);
367-
}
362+
// Clone the list so we don't break our own enumeration
363+
foreach (var generatedFileInfo in _generatedFilenameToInformation.Values.ToList())
364+
{
365+
if (_openedDocumentIds.ContainsKey(generatedFileInfo))
366+
RemoveDocumentFromWorkspace_NoLock(workspace, generatedFileInfo);
367+
}
368368

369-
_generatedFilenameToInformation.Clear();
370-
_keyToInformation.Clear();
371-
Contract.ThrowIfFalse(_openedDocumentIds.IsEmpty);
369+
_generatedFilenameToInformation.Clear();
370+
_keyToInformation.Clear();
371+
Contract.ThrowIfFalse(_openedDocumentIds.IsEmpty);
372+
}
372373
}
373374

374375
private static async Task<UniqueDocumentKey> GetUniqueDocumentKeyAsync(Project project, INamedTypeSymbol topLevelNamedType, bool signaturesOnly, CancellationToken cancellationToken)

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33
// See the LICENSE file in the project root for more information.
44

5+
using System.Diagnostics.CodeAnalysis;
56
using System.Threading;
67
using System.Threading.Tasks;
78
using Microsoft.CodeAnalysis.Formatting;
@@ -36,7 +37,7 @@ internal interface IMetadataAsSourceFileProvider
3637
/// Called when the file returned from <see cref="GetGeneratedFileAsync"/> needs to be added to the workspace,
3738
/// to be opened. Will be called on the main thread of the workspace host.
3839
/// </summary>
39-
bool TryAddDocumentToWorkspace(MetadataAsSourceWorkspace workspace, string filePath, SourceTextContainer sourceTextContainer);
40+
bool TryAddDocumentToWorkspace(MetadataAsSourceWorkspace workspace, string filePath, SourceTextContainer sourceTextContainer, [NotNullWhen(true)] out DocumentId? documentId);
4041

4142
/// <summary>
4243
/// Called when the file is being closed, and so needs to be removed from the workspace. Will be called on the

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

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33
// See the LICENSE file in the project root for more information.
44

5+
using System.Diagnostics.CodeAnalysis;
56
using System.Threading;
67
using System.Threading.Tasks;
78
using Microsoft.CodeAnalysis.Formatting;
@@ -31,8 +32,16 @@ Task<MetadataAsSourceFile> GetGeneratedFileAsync(
3132
MetadataAsSourceOptions options,
3233
CancellationToken cancellationToken);
3334

34-
bool TryAddDocumentToWorkspace(string filePath, SourceTextContainer buffer);
35+
/// <summary>
36+
/// Checks if the given file path is a metadata as source file and adds to the metadata workspace if it is.
37+
/// Callers must ensure this is only called serially.
38+
/// </summary>
39+
bool TryAddDocumentToWorkspace(string filePath, SourceTextContainer sourceTextContainer, [NotNullWhen(true)] out DocumentId? documentId);
3540

41+
/// <summary>
42+
/// Checks if the given file path is a metadata as source file and removes from the metadata workspace if it is.
43+
/// Callers must ensure this is only called serially.
44+
/// </summary>
3645
bool TryRemoveDocumentFromWorkspace(string filePath);
3746

3847
bool IsNavigableMetadataSymbol(ISymbol symbol);

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

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@
66
using System.Collections.Generic;
77
using System.Collections.Immutable;
88
using System.Composition;
9+
using System.Diagnostics.CodeAnalysis;
910
using System.IO;
1011
using System.Linq;
1112
using System.Threading;
1213
using System.Threading.Tasks;
1314
using Microsoft.CodeAnalysis.ErrorReporting;
14-
using Microsoft.CodeAnalysis.Formatting;
1515
using Microsoft.CodeAnalysis.Host.Mef;
1616
using Microsoft.CodeAnalysis.Shared.Extensions;
1717
using Microsoft.CodeAnalysis.Shared.Utilities;
@@ -163,26 +163,27 @@ private static void AssertIsMainThread(MetadataAsSourceWorkspace workspace)
163163
Contract.ThrowIfFalse(threadingService.IsOnMainThread);
164164
}
165165

166-
public bool TryAddDocumentToWorkspace(string filePath, SourceTextContainer sourceTextContainer)
166+
public bool TryAddDocumentToWorkspace(string filePath, SourceTextContainer sourceTextContainer, [NotNullWhen(true)] out DocumentId? documentId)
167167
{
168168
// If we haven't even created a MetadataAsSource workspace yet, then this file definitely cannot be added to
169169
// it. This happens when the MiscWorkspace calls in to just see if it can attach this document to the
170170
// MetadataAsSource instead of itself.
171171
var workspace = _workspace;
172172
if (workspace != null)
173173
{
174-
AssertIsMainThread(workspace);
175-
176174
foreach (var provider in _providers.Value)
177175
{
178176
if (!provider.IsValueCreated)
179177
continue;
180178

181-
if (provider.Value.TryAddDocumentToWorkspace(workspace, filePath, sourceTextContainer))
179+
if (provider.Value.TryAddDocumentToWorkspace(workspace, filePath, sourceTextContainer, out documentId))
180+
{
182181
return true;
182+
}
183183
}
184184
}
185185

186+
documentId = null;
186187
return false;
187188
}
188189

@@ -194,8 +195,6 @@ public bool TryRemoveDocumentFromWorkspace(string filePath)
194195
var workspace = _workspace;
195196
if (workspace != null)
196197
{
197-
AssertIsMainThread(workspace);
198-
199198
foreach (var provider in _providers.Value)
200199
{
201200
if (!provider.IsValueCreated)

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

Lines changed: 25 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using System.Collections.Generic;
88
using System.Collections.Immutable;
99
using System.Composition;
10+
using System.Diagnostics.CodeAnalysis;
1011
using System.IO;
1112
using System.Linq;
1213
using System.Reflection.Metadata.Ecma335;
@@ -43,6 +44,11 @@ internal sealed class PdbSourceDocumentMetadataAsSourceFileProvider(
4344
private readonly IImplementationAssemblyLookupService _implementationAssemblyLookupService = implementationAssemblyLookupService;
4445
private readonly IPdbSourceDocumentLogger? _logger = logger;
4546

47+
/// <summary>
48+
/// Lock to guard access to workspace updates when opening / closing documents.
49+
/// </summary>
50+
private readonly object _gate = new();
51+
4652
/// <summary>
4753
/// Accessed only in <see cref="GetGeneratedFileAsync"/> and <see cref="CleanupGeneratedFiles"/>, both of which
4854
/// are called under a lock in <see cref="MetadataAsSourceFileService"/>. So this is safe as a plain
@@ -342,43 +348,39 @@ private ImmutableArray<DocumentInfo> CreateDocumentInfos(
342348
return documents.ToImmutableAndClear();
343349
}
344350

345-
private static void AssertIsMainThread(MetadataAsSourceWorkspace workspace)
346-
{
347-
Contract.ThrowIfNull(workspace);
348-
var threadingService = workspace.Services.GetRequiredService<IWorkspaceThreadingServiceProvider>().Service;
349-
Contract.ThrowIfFalse(threadingService.IsOnMainThread);
350-
}
351-
352351
public bool ShouldCollapseOnOpen(MetadataAsSourceWorkspace workspace, string filePath, BlockStructureOptions blockStructureOptions)
353352
{
354-
AssertIsMainThread(workspace);
355353
return _fileToDocumentInfoMap.TryGetValue(filePath, out _) && blockStructureOptions.CollapseMetadataImplementationsWhenFirstOpened;
356354
}
357355

358-
public bool TryAddDocumentToWorkspace(MetadataAsSourceWorkspace workspace, string filePath, SourceTextContainer sourceTextContainer)
356+
public bool TryAddDocumentToWorkspace(MetadataAsSourceWorkspace workspace, string filePath, SourceTextContainer sourceTextContainer, [NotNullWhen(true)] out DocumentId? documentId)
359357
{
360-
AssertIsMainThread(workspace);
361-
362-
if (_fileToDocumentInfoMap.TryGetValue(filePath, out var info))
358+
lock (_gate)
363359
{
364-
workspace.OnDocumentOpened(info.DocumentId, sourceTextContainer);
365-
return true;
366-
}
360+
if (_fileToDocumentInfoMap.TryGetValue(filePath, out var info))
361+
{
362+
workspace.OnDocumentOpened(info.DocumentId, sourceTextContainer);
363+
documentId = info.DocumentId;
364+
return true;
365+
}
367366

368-
return false;
367+
documentId = null;
368+
return false;
369+
}
369370
}
370371

371372
public bool TryRemoveDocumentFromWorkspace(MetadataAsSourceWorkspace workspace, string filePath)
372373
{
373-
AssertIsMainThread(workspace);
374-
375-
if (_fileToDocumentInfoMap.TryGetValue(filePath, out var info))
374+
lock (_gate)
376375
{
377-
workspace.OnDocumentClosed(info.DocumentId, new WorkspaceFileTextLoader(workspace.Services.SolutionServices, filePath, info.Encoding));
378-
return true;
379-
}
376+
if (_fileToDocumentInfoMap.TryGetValue(filePath, out var info))
377+
{
378+
workspace.OnDocumentClosed(info.DocumentId, new WorkspaceFileTextLoader(workspace.Services.SolutionServices, filePath, info.Encoding));
379+
return true;
380+
}
380381

381-
return false;
382+
return false;
383+
}
382384
}
383385

384386
public Project? MapDocument(Document document)

src/LanguageServer/Protocol/RoslynLanguageServer.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ private FrozenDictionary<string, ImmutableArray<BaseService>> GetBaseServices(
9999
// those cases, we do not need to add an additional workspace to manage new files we hear about. So only
100100
// add the LspMiscellaneousFilesWorkspace for hosts that have not already brought their own.
101101
if (serverKind == WellKnownLspServerKinds.CSharpVisualBasicLspServer)
102-
AddLazyService<LspMiscellaneousFilesWorkspace>(lspServices => new LspMiscellaneousFilesWorkspace(lspServices, hostServices));
102+
AddLazyService<LspMiscellaneousFilesWorkspace>(lspServices => lspServices.GetRequiredService<LspMiscellaneousFilesWorkspaceProvider>().CreateLspMiscellaneousFilesWorkspace(lspServices, hostServices));
103103

104104
return baseServiceMap.ToFrozenDictionary(
105105
keySelector: kvp => kvp.Key,

0 commit comments

Comments
 (0)