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 3 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 @@ -292,32 +293,30 @@ 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

// Serial access is guaranteed by the caller.
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);
(var projectInfo, documentId) = fileInfo.GetProjectInfoAndDocumentId(workspace.Services.SolutionServices, loadFileFromDisk: true);

workspace.OnProjectAdded(projectInfo);
workspace.OnDocumentOpened(documentId, sourceTextContainer);

_openedDocumentIds = _openedDocumentIds.Add(fileInfo, documentId);

return true;
}

documentId = null;
return false;
}

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

// Serial access is guaranteed by the caller.
if (_generatedFilenameToInformation.TryGetValue(filePath, out var fileInfo))
{
if (_openedDocumentIds.ContainsKey(fileInfo))
Expand All @@ -329,8 +328,7 @@ public bool TryRemoveDocumentFromWorkspace(MetadataAsSourceWorkspace workspace,

private bool RemoveDocumentFromWorkspace(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
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 @@ -35,12 +36,16 @@ internal interface IMetadataAsSourceFileProvider
/// <summary>
/// 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.
///
/// Callers of this must guarantee serial access.
dibarbet marked this conversation as resolved.
Show resolved Hide resolved
/// </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
/// main thread of the workspace host.
///
/// Callers of this must guarantee serial access.
/// </summary>
bool TryRemoveDocumentFromWorkspace(MetadataAsSourceWorkspace workspace, string filePath);

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 @@ -355,23 +356,23 @@ public bool ShouldCollapseOnOpen(MetadataAsSourceWorkspace workspace, string fil
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);

// Serial access is guaranteed by the caller.
if (_fileToDocumentInfoMap.TryGetValue(filePath, out var info))
{
workspace.OnDocumentOpened(info.DocumentId, sourceTextContainer);
documentId = info.DocumentId;
return true;
}

documentId = null;
return false;
}

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

// Serial access is guaranteed by the caller.
if (_fileToDocumentInfoMap.TryGetValue(filePath, out var info))
{
workspace.OnDocumentClosed(info.DocumentId, new WorkspaceFileTextLoader(workspace.Services.SolutionServices, filePath, info.Encoding));
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
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Features.Workspaces;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.MetadataAsSource;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
using Microsoft.CommonLanguageServerProtocol.Framework;
Expand All @@ -24,27 +25,31 @@ namespace Microsoft.CodeAnalysis.LanguageServer
/// Future work for this workspace includes supporting basic metadata references (mscorlib, System dlls, etc),
/// but that is dependent on having a x-plat mechanism for retrieving those references from the framework / sdk.
/// </summary>
internal sealed class LspMiscellaneousFilesWorkspace : Workspace, ILspService, ILspWorkspace
internal sealed class LspMiscellaneousFilesWorkspace(ILspServices lspServices, IMetadataAsSourceFileService metadataAsSourceFileService, HostServices hostServices)
: Workspace(hostServices, WorkspaceKind.MiscellaneousFiles), ILspService, ILspWorkspace
{
private readonly ILspServices _lspServices;

public LspMiscellaneousFilesWorkspace(ILspServices lspServices, HostServices hostServices) : base(hostServices, WorkspaceKind.MiscellaneousFiles)
{
_lspServices = lspServices;
}

public bool SupportsMutation => true;

/// <summary>
/// Takes in a file URI and text and creates a misc project and document for the file.
///
/// Calls to this method and <see cref="TryRemoveMiscellaneousDocument(Uri)"/> are made
/// Calls to this method and <see cref="TryRemoveMiscellaneousDocument(Uri, bool)"/> are made
/// from LSP text sync request handling which do not run concurrently.
/// </summary>
public Document? AddMiscellaneousDocument(Uri uri, SourceText documentText, string languageId, ILspLogger logger)
{
var documentFilePath = ProtocolConversions.GetDocumentFilePathFromUri(uri);
var languageInfoProvider = _lspServices.GetRequiredService<ILanguageInfoProvider>();

var container = new StaticSourceTextContainer(documentText);
if (metadataAsSourceFileService.TryAddDocumentToWorkspace(documentFilePath, container, out var documentId))
{
var metadataWorkspace = metadataAsSourceFileService.TryGetWorkspace();
Contract.ThrowIfNull(metadataWorkspace);
var document = metadataWorkspace.CurrentSolution.GetRequiredDocument(documentId);
return document;
}

var languageInfoProvider = lspServices.GetRequiredService<ILanguageInfoProvider>();
var languageInformation = languageInfoProvider.GetLanguageInformation(documentFilePath, languageId);
if (languageInformation == null)
{
Expand All @@ -69,8 +74,14 @@ public LspMiscellaneousFilesWorkspace(ILspServices lspServices, HostServices hos
/// Calls to this method and <see cref="AddMiscellaneousDocument(Uri, SourceText, string, ILspLogger)"/> are made
/// from LSP text sync request handling which do not run concurrently.
/// </summary>
public void TryRemoveMiscellaneousDocument(Uri uri)
public void TryRemoveMiscellaneousDocument(Uri uri, bool removeFromMetadata)
dibarbet marked this conversation as resolved.
Show resolved Hide resolved
{
var documentFilePath = ProtocolConversions.GetDocumentFilePathFromUri(uri);
if (removeFromMetadata && metadataAsSourceFileService.TryRemoveDocumentFromWorkspace(documentFilePath))
{
return;
}

// We'll only ever have a single document matching this URI in the misc solution.
var matchingDocument = CurrentSolution.GetDocumentIds(uri).SingleOrDefault();
if (matchingDocument != null)
Expand All @@ -96,5 +107,19 @@ public ValueTask UpdateTextIfPresentAsync(DocumentId documentId, SourceText sour
this.OnDocumentTextChanged(documentId, sourceText, PreservationMode.PreserveIdentity, requireDocumentPresent: false);
return ValueTaskFactory.CompletedTask;
}

private class StaticSourceTextContainer(SourceText text) : SourceTextContainer
{
public override SourceText CurrentText => text;

/// <summary>
/// Text changes are handled by LSP forking the document, we don't need to actually update anything here.
/// </summary>
public override event EventHandler<TextChangeEventArgs> TextChanged
{
add { }
remove { }
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Licensed to the .NET Foundation under one or more agreements.
// 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;
using System.Composition;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.LanguageServer.Handler;
using Microsoft.CodeAnalysis.MetadataAsSource;
using Microsoft.CommonLanguageServerProtocol.Framework;

namespace Microsoft.CodeAnalysis.LanguageServer;

/// <summary>
/// Service to create <see cref="LspWorkspaceManager"/> instances.
/// This is not exported as a <see cref="ILspServiceFactory"/> as it requires
/// special base language server dependencies such as the <see cref="HostServices"/>
/// </summary>
[ExportCSharpVisualBasicStatelessLspService(typeof(LspMiscellaneousFilesWorkspaceProvider)), Shared]
[method: ImportingConstructor]
[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
internal class LspMiscellaneousFilesWorkspaceProvider(IMetadataAsSourceFileService metadataAsSourceFileService) : ILspService
dibarbet marked this conversation as resolved.
Show resolved Hide resolved
{
public LspMiscellaneousFilesWorkspace CreateLspMiscellaneousFilesWorkspace(ILspServices lspServices, HostServices hostServices)
{
return new LspMiscellaneousFilesWorkspace(lspServices, metadataAsSourceFileService, hostServices);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -145,8 +145,8 @@ public async ValueTask StopTrackingAsync(Uri uri, CancellationToken cancellation
// If LSP changed, we need to compare against the workspace again to get the updated solution.
_cachedLspSolutions.Clear();

// Also remove it from our loose files workspace if it is still there.
_lspMiscellaneousFilesWorkspace?.TryRemoveMiscellaneousDocument(uri);
// Also remove it from our loose files or metadata workspace if it is still there.
_lspMiscellaneousFilesWorkspace?.TryRemoveMiscellaneousDocument(uri, removeFromMetadata: true);

LspTextChanged?.Invoke(this, EventArgs.Empty);

Expand Down Expand Up @@ -238,7 +238,8 @@ public void UpdateTrackedDocument(Uri uri, SourceText newSourceText)
// As we found the document in a non-misc workspace, also attempt to remove it from the misc workspace
// if it happens to be in there as well.
if (workspace != _lspMiscellaneousFilesWorkspace)
_lspMiscellaneousFilesWorkspace?.TryRemoveMiscellaneousDocument(uri);
// Do not attempt to remove the file from the metadata workspace (the document is still open).
_lspMiscellaneousFilesWorkspace?.TryRemoveMiscellaneousDocument(uri, removeFromMetadata: false);
dibarbet marked this conversation as resolved.
Show resolved Hide resolved

return (workspace, document.Project.Solution, document);
}
Expand All @@ -255,7 +256,7 @@ public void UpdateTrackedDocument(Uri uri, SourceText newSourceText)
{
var miscDocument = _lspMiscellaneousFilesWorkspace?.AddMiscellaneousDocument(uri, trackedDocument.Text, trackedDocument.LanguageId, _logger);
if (miscDocument is not null)
return (_lspMiscellaneousFilesWorkspace, miscDocument.Project.Solution, miscDocument);
return (miscDocument.Project.Solution.Workspace, miscDocument.Project.Solution, miscDocument);
}

return default;
Expand Down
Loading
Loading