Skip to content
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

Adds support for metadata LSP language features in VSCode #74488

Merged
merged 4 commits into from
Jul 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

using System;
using System.Composition;
using System.Diagnostics.CodeAnalysis;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Formatting;
Expand Down Expand Up @@ -47,8 +48,9 @@ public void CleanupGeneratedFiles(MetadataAsSourceWorkspace workspace)
return null;
}

public bool TryAddDocumentToWorkspace(MetadataAsSourceWorkspace workspace, string filePath, Text.SourceTextContainer sourceTextContainer)
public bool TryAddDocumentToWorkspace(MetadataAsSourceWorkspace workspace, string filePath, Text.SourceTextContainer sourceTextContainer, [NotNullWhen(true)] out DocumentId? documentId)
{
documentId = null!;
return true;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -300,7 +300,7 @@ internal Document GetDocument(MetadataAsSourceFile file)
using var reader = File.OpenRead(file.FilePath);
var stringText = EncodedStringText.Create(reader);

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

return stringText.Container.GetRelatedDocuments().Single();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Composition;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using System.Linq;
Expand Down Expand Up @@ -33,6 +34,11 @@ internal class DecompilationMetadataAsSourceFileProvider(IImplementationAssembly
{
internal const string ProviderName = "Decompilation";

/// <summary>
/// Guards access to <see cref="_openedDocumentIds"/> and workspace updates when opening / closing documents.
/// </summary>
private readonly object _gate = new();

/// <summary>
/// Accessed only in <see cref="GetGeneratedFileAsync"/> and <see cref="CleanupGeneratedFiles"/>, both of which
/// are called under a lock in <see cref="MetadataAsSourceFileService"/>. So this is safe as a plain
Expand Down Expand Up @@ -271,17 +277,8 @@ private async Task<Location> RelocateSymbol_NoLockAsync(Solution solution, Metad
return await MetadataAsSourceHelpers.GetLocationInGeneratedSourceAsync(symbolId, temporaryDocument, cancellationToken).ConfigureAwait(false);
}

private static void AssertIsMainThread(MetadataAsSourceWorkspace workspace)
{
Contract.ThrowIfNull(workspace);
var threadingService = workspace.Services.GetRequiredService<IWorkspaceThreadingServiceProvider>().Service;
Contract.ThrowIfFalse(threadingService.IsOnMainThread);
}

public bool ShouldCollapseOnOpen(MetadataAsSourceWorkspace workspace, string filePath, BlockStructureOptions blockStructureOptions)
{
AssertIsMainThread(workspace);

if (_generatedFilenameToInformation.TryGetValue(filePath, out var info))
{
return info.SignaturesOnly
Expand All @@ -292,45 +289,46 @@ public bool ShouldCollapseOnOpen(MetadataAsSourceWorkspace workspace, string fil
return false;
}

public bool TryAddDocumentToWorkspace(MetadataAsSourceWorkspace workspace, string filePath, SourceTextContainer sourceTextContainer)
public bool TryAddDocumentToWorkspace(MetadataAsSourceWorkspace workspace, string filePath, SourceTextContainer sourceTextContainer, [NotNullWhen(true)] out DocumentId? documentId)
{
AssertIsMainThread(workspace);
dibarbet marked this conversation as resolved.
Show resolved Hide resolved

if (_generatedFilenameToInformation.TryGetValue(filePath, out var fileInfo))
lock (_gate)
{
Contract.ThrowIfTrue(_openedDocumentIds.ContainsKey(fileInfo));
if (_generatedFilenameToInformation.TryGetValue(filePath, out var fileInfo))
{
Contract.ThrowIfTrue(_openedDocumentIds.ContainsKey(fileInfo));

// We do own the file, so let's open it up in our workspace
var (projectInfo, documentId) = fileInfo.GetProjectInfoAndDocumentId(workspace.Services.SolutionServices, loadFileFromDisk: true);
// 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.OnProjectAdded(projectInfo);
workspace.OnDocumentOpened(documentId, sourceTextContainer);
workspace.OnProjectAdded(projectInfo);
workspace.OnDocumentOpened(documentId, sourceTextContainer);

_openedDocumentIds = _openedDocumentIds.Add(fileInfo, documentId);
_openedDocumentIds = _openedDocumentIds.Add(fileInfo, documentId);
return true;
}

return true;
documentId = null;
return false;
}

return false;
}

public bool TryRemoveDocumentFromWorkspace(MetadataAsSourceWorkspace workspace, string filePath)
{
AssertIsMainThread(workspace);

if (_generatedFilenameToInformation.TryGetValue(filePath, out var fileInfo))
lock (_gate)
{
if (_openedDocumentIds.ContainsKey(fileInfo))
return RemoveDocumentFromWorkspace(workspace, fileInfo);
}
if (_generatedFilenameToInformation.TryGetValue(filePath, out var fileInfo))
{
if (_openedDocumentIds.ContainsKey(fileInfo))
return RemoveDocumentFromWorkspace_NoLock(workspace, fileInfo);
}

return false;
return false;
}
}

private bool RemoveDocumentFromWorkspace(MetadataAsSourceWorkspace workspace, MetadataAsSourceGeneratedFileInfo fileInfo)
private bool RemoveDocumentFromWorkspace_NoLock(MetadataAsSourceWorkspace workspace, MetadataAsSourceGeneratedFileInfo fileInfo)
{
AssertIsMainThread(workspace);
dibarbet marked this conversation as resolved.
Show resolved Hide resolved

// Serial access is guaranteed by the caller.
var documentId = _openedDocumentIds.GetValueOrDefault(fileInfo);
Contract.ThrowIfNull(documentId);

Expand Down Expand Up @@ -359,16 +357,19 @@ private bool RemoveDocumentFromWorkspace(MetadataAsSourceWorkspace workspace, Me

public void CleanupGeneratedFiles(MetadataAsSourceWorkspace workspace)
{
// Clone the list so we don't break our own enumeration
foreach (var generatedFileInfo in _generatedFilenameToInformation.Values.ToList())
lock (_gate)
{
if (_openedDocumentIds.ContainsKey(generatedFileInfo))
RemoveDocumentFromWorkspace(workspace, generatedFileInfo);
}
// Clone the list so we don't break our own enumeration
foreach (var generatedFileInfo in _generatedFilenameToInformation.Values.ToList())
{
if (_openedDocumentIds.ContainsKey(generatedFileInfo))
RemoveDocumentFromWorkspace_NoLock(workspace, generatedFileInfo);
}

_generatedFilenameToInformation.Clear();
_keyToInformation.Clear();
Contract.ThrowIfFalse(_openedDocumentIds.IsEmpty);
_generatedFilenameToInformation.Clear();
_keyToInformation.Clear();
Contract.ThrowIfFalse(_openedDocumentIds.IsEmpty);
}
}

private static async Task<UniqueDocumentKey> GetUniqueDocumentKeyAsync(Project project, INamedTypeSymbol topLevelNamedType, bool signaturesOnly, CancellationToken cancellationToken)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

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

/// <summary>
/// Called when the file is being closed, and so needs to be removed from the workspace. Will be called on the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System.Diagnostics.CodeAnalysis;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Formatting;
Expand Down Expand Up @@ -31,8 +32,16 @@ Task<MetadataAsSourceFile> GetGeneratedFileAsync(
MetadataAsSourceOptions options,
CancellationToken cancellationToken);

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

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

bool IsNavigableMetadataSymbol(ISymbol symbol);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Composition;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.ErrorReporting;
using Microsoft.CodeAnalysis.Formatting;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Shared.Utilities;
Expand Down Expand Up @@ -163,26 +163,27 @@ private static void AssertIsMainThread(MetadataAsSourceWorkspace workspace)
Contract.ThrowIfFalse(threadingService.IsOnMainThread);
}

public bool TryAddDocumentToWorkspace(string filePath, SourceTextContainer sourceTextContainer)
public bool TryAddDocumentToWorkspace(string filePath, SourceTextContainer sourceTextContainer, [NotNullWhen(true)] out DocumentId? documentId)
{
// If we haven't even created a MetadataAsSource workspace yet, then this file definitely cannot be added to
// it. This happens when the MiscWorkspace calls in to just see if it can attach this document to the
// MetadataAsSource instead of itself.
var workspace = _workspace;
if (workspace != null)
{
AssertIsMainThread(workspace);

foreach (var provider in _providers.Value)
{
if (!provider.IsValueCreated)
continue;

if (provider.Value.TryAddDocumentToWorkspace(workspace, filePath, sourceTextContainer))
if (provider.Value.TryAddDocumentToWorkspace(workspace, filePath, sourceTextContainer, out documentId))
{
return true;
}
}
}

documentId = null;
return false;
}

Expand All @@ -194,8 +195,6 @@ public bool TryRemoveDocumentFromWorkspace(string filePath)
var workspace = _workspace;
if (workspace != null)
{
AssertIsMainThread(workspace);

foreach (var provider in _providers.Value)
{
if (!provider.IsValueCreated)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Composition;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Reflection.Metadata.Ecma335;
Expand Down Expand Up @@ -43,6 +44,11 @@ internal sealed class PdbSourceDocumentMetadataAsSourceFileProvider(
private readonly IImplementationAssemblyLookupService _implementationAssemblyLookupService = implementationAssemblyLookupService;
private readonly IPdbSourceDocumentLogger? _logger = logger;

/// <summary>
/// Lock to guard access to workspace updates when opening / closing documents.
/// </summary>
private readonly object _gate = new();

/// <summary>
/// Accessed only in <see cref="GetGeneratedFileAsync"/> and <see cref="CleanupGeneratedFiles"/>, both of which
/// are called under a lock in <see cref="MetadataAsSourceFileService"/>. So this is safe as a plain
Expand Down Expand Up @@ -342,43 +348,39 @@ private ImmutableArray<DocumentInfo> CreateDocumentInfos(
return documents.ToImmutableAndClear();
}

private static void AssertIsMainThread(MetadataAsSourceWorkspace workspace)
{
Contract.ThrowIfNull(workspace);
var threadingService = workspace.Services.GetRequiredService<IWorkspaceThreadingServiceProvider>().Service;
Contract.ThrowIfFalse(threadingService.IsOnMainThread);
}

public bool ShouldCollapseOnOpen(MetadataAsSourceWorkspace workspace, string filePath, BlockStructureOptions blockStructureOptions)
{
AssertIsMainThread(workspace);
return _fileToDocumentInfoMap.TryGetValue(filePath, out _) && blockStructureOptions.CollapseMetadataImplementationsWhenFirstOpened;
}

public bool TryAddDocumentToWorkspace(MetadataAsSourceWorkspace workspace, string filePath, SourceTextContainer sourceTextContainer)
public bool TryAddDocumentToWorkspace(MetadataAsSourceWorkspace workspace, string filePath, SourceTextContainer sourceTextContainer, [NotNullWhen(true)] out DocumentId? documentId)
{
AssertIsMainThread(workspace);

if (_fileToDocumentInfoMap.TryGetValue(filePath, out var info))
lock (_gate)
{
workspace.OnDocumentOpened(info.DocumentId, sourceTextContainer);
return true;
}
if (_fileToDocumentInfoMap.TryGetValue(filePath, out var info))
{
workspace.OnDocumentOpened(info.DocumentId, sourceTextContainer);
documentId = info.DocumentId;
return true;
}

return false;
documentId = null;
return false;
}
}

public bool TryRemoveDocumentFromWorkspace(MetadataAsSourceWorkspace workspace, string filePath)
{
AssertIsMainThread(workspace);

if (_fileToDocumentInfoMap.TryGetValue(filePath, out var info))
lock (_gate)
{
workspace.OnDocumentClosed(info.DocumentId, new WorkspaceFileTextLoader(workspace.Services.SolutionServices, filePath, info.Encoding));
return true;
}
if (_fileToDocumentInfoMap.TryGetValue(filePath, out var info))
{
workspace.OnDocumentClosed(info.DocumentId, new WorkspaceFileTextLoader(workspace.Services.SolutionServices, filePath, info.Encoding));
return true;
}

return false;
return false;
}
}

public Project? MapDocument(Document document)
Expand Down
2 changes: 1 addition & 1 deletion src/LanguageServer/Protocol/RoslynLanguageServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ private FrozenDictionary<string, ImmutableArray<BaseService>> GetBaseServices(
// those cases, we do not need to add an additional workspace to manage new files we hear about. So only
// add the LspMiscellaneousFilesWorkspace for hosts that have not already brought their own.
if (serverKind == WellKnownLspServerKinds.CSharpVisualBasicLspServer)
AddLazyService<LspMiscellaneousFilesWorkspace>(lspServices => new LspMiscellaneousFilesWorkspace(lspServices, hostServices));
AddLazyService<LspMiscellaneousFilesWorkspace>(lspServices => lspServices.GetRequiredService<LspMiscellaneousFilesWorkspaceProvider>().CreateLspMiscellaneousFilesWorkspace(lspServices, hostServices));
Copy link
Member

Choose a reason for hiding this comment

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

why this pattern?

Copy link
Member Author

Choose a reason for hiding this comment

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

The misc files workspace now requires dependencies from both regular mef (IMetadataAsSourceFileService) and directly from the base language server instance (HostServices) that aren't available as MEF services.

So the provider can import the MEF service normally, then gets passed in the HostServices by the instance.


return baseServiceMap.ToFrozenDictionary(
keySelector: kvp => kvp.Key,
Expand Down
Loading
Loading