diff --git a/src/EditorFeatures/Core/LanguageServer/AbstractInProcLanguageClient.cs b/src/EditorFeatures/Core/LanguageServer/AbstractInProcLanguageClient.cs index abe00f37ee641..b22dfda918ede 100644 --- a/src/EditorFeatures/Core/LanguageServer/AbstractInProcLanguageClient.cs +++ b/src/EditorFeatures/Core/LanguageServer/AbstractInProcLanguageClient.cs @@ -63,7 +63,7 @@ internal abstract partial class AbstractInProcLanguageClient( /// Unused, implementing . /// Gets the optional target object for receiving custom messages not covered by the language server protocol. /// - public object? CustomMessageTarget => null; + public virtual object? CustomMessageTarget => null; /// /// An enum representing this server instance. @@ -256,5 +256,22 @@ public virtual AbstractLanguageServer Create( /// This method is called after the language server has been activated, but connection has not been established. /// public Task AttachForCustomMessageAsync(JsonRpc rpc) => Task.CompletedTask; + + internal TestAccessor GetTestAccessor() + { + return new TestAccessor(this); + } + + internal readonly struct TestAccessor + { + private readonly AbstractInProcLanguageClient _instance; + + internal TestAccessor(AbstractInProcLanguageClient instance) + { + _instance = instance; + } + + public AbstractLanguageServer? LanguageServer => _instance._languageServer; + } } } diff --git a/src/EditorFeatures/Core/Microsoft.CodeAnalysis.EditorFeatures.csproj b/src/EditorFeatures/Core/Microsoft.CodeAnalysis.EditorFeatures.csproj index 8c7d44ae3356d..7924e0bd7f73d 100644 --- a/src/EditorFeatures/Core/Microsoft.CodeAnalysis.EditorFeatures.csproj +++ b/src/EditorFeatures/Core/Microsoft.CodeAnalysis.EditorFeatures.csproj @@ -79,6 +79,7 @@ + diff --git a/src/EditorFeatures/TestUtilities/Microsoft.CodeAnalysis.EditorFeatures.Test.Utilities.csproj b/src/EditorFeatures/TestUtilities/Microsoft.CodeAnalysis.EditorFeatures.Test.Utilities.csproj index dc006033f6ca1..478f9de53cc99 100644 --- a/src/EditorFeatures/TestUtilities/Microsoft.CodeAnalysis.EditorFeatures.Test.Utilities.csproj +++ b/src/EditorFeatures/TestUtilities/Microsoft.CodeAnalysis.EditorFeatures.Test.Utilities.csproj @@ -92,6 +92,7 @@ + diff --git a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/AbstractLanguageServer.cs b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/AbstractLanguageServer.cs index 7196583560a2c..408b4f1cee7f5 100644 --- a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/AbstractLanguageServer.cs +++ b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/AbstractLanguageServer.cs @@ -353,6 +353,11 @@ internal TestAccessor(AbstractLanguageServer server) return null; } + internal Task ExecuteRequestAsync(string methodName, TRequest request, CancellationToken cancellationToken) + { + return _server._queue.Value.ExecuteAsync(request, methodName, _server._lspServices.Value, cancellationToken); + } + internal JsonRpc GetServerRpc() => _server._jsonRpc; internal bool HasShutdownStarted() diff --git a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/Microsoft.CommonLanguageServerProtocol.Framework.csproj b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/Microsoft.CommonLanguageServerProtocol.Framework.csproj index 8c0513a6b0158..3c304c0719c78 100644 --- a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/Microsoft.CommonLanguageServerProtocol.Framework.csproj +++ b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/Microsoft.CommonLanguageServerProtocol.Framework.csproj @@ -19,5 +19,6 @@ + diff --git a/src/Features/LanguageServer/Protocol/Extensions/Extensions.cs b/src/Features/LanguageServer/Protocol/Extensions/Extensions.cs index 130f3014c414e..31e88ccc68d83 100644 --- a/src/Features/LanguageServer/Protocol/Extensions/Extensions.cs +++ b/src/Features/LanguageServer/Protocol/Extensions/Extensions.cs @@ -76,6 +76,21 @@ public static ImmutableArray GetDocuments(this Solution solution, stri return documents; } + /// + /// Get all regular and additional s for the given . + /// + public static ImmutableArray GetTextDocuments(this Solution solution, Uri documentUri) + { + var documentIds = GetDocumentIds(solution, documentUri); + + var documents = documentIds + .Select(solution.GetDocument) + .Concat(documentIds.Select(solution.GetAdditionalDocument)) + .WhereNotNull() + .ToImmutableArray(); + return documents; + } + public static ImmutableArray GetDocumentIds(this Solution solution, Uri documentUri) => solution.GetDocumentIdsWithFilePath(ProtocolConversions.GetDocumentFilePathFromUri(documentUri)); diff --git a/src/Features/LanguageServer/Protocol/Handler/ProjectContext/GetTextDocumentWithContextHandler.cs b/src/Features/LanguageServer/Protocol/Handler/ProjectContext/GetTextDocumentWithContextHandler.cs index 3fb63ca69e04d..1c1e201e3698d 100644 --- a/src/Features/LanguageServer/Protocol/Handler/ProjectContext/GetTextDocumentWithContextHandler.cs +++ b/src/Features/LanguageServer/Protocol/Handler/ProjectContext/GetTextDocumentWithContextHandler.cs @@ -35,19 +35,20 @@ public GetTextDocumentWithContextHandler() Contract.ThrowIfNull(context.Workspace); Contract.ThrowIfNull(context.Solution); - // We specifically don't use context.Document here because we want multiple - var documents = context.Solution.GetDocuments(request.TextDocument.Uri); + // We specifically don't use context.Document here because we want multiple. We also don't need + // all of the document info, just the Id is enough + var documentIds = context.Solution.GetDocumentIds(request.TextDocument.Uri); - if (!documents.Any()) + if (!documentIds.Any()) { return SpecializedTasks.Null(); } var contexts = new List(); - foreach (var document in documents) + foreach (var documentId in documentIds) { - var project = document.Project; + var project = context.Solution.GetRequiredProject(documentId.ProjectId); var projectContext = ProtocolConversions.ProjectToProjectContext(project); contexts.Add(projectContext); } @@ -57,13 +58,13 @@ public GetTextDocumentWithContextHandler() // GetDocumentIdInCurrentContext will just return the same ID back, which means we're going to pick the first // ID in GetDocumentIdsWithFilePath, but there's really nothing we can do since we don't have contexts for // close documents anyways. - var openDocument = documents.First(); - var currentContextDocumentId = context.Workspace.GetDocumentIdInCurrentContext(openDocument.Id); + var openDocumentId = documentIds.First(); + var currentContextDocumentId = context.Workspace.GetDocumentIdInCurrentContext(openDocumentId); return Task.FromResult(new VSProjectContextList { ProjectContexts = contexts.ToArray(), - DefaultIndex = documents.IndexOf(d => d.Id == currentContextDocumentId) + DefaultIndex = documentIds.IndexOf(d => d == currentContextDocumentId) }); } } diff --git a/src/Features/LanguageServer/Protocol/Handler/RequestContext.cs b/src/Features/LanguageServer/Protocol/Handler/RequestContext.cs index e1a1678d30a49..f85b0291b3bb8 100644 --- a/src/Features/LanguageServer/Protocol/Handler/RequestContext.cs +++ b/src/Features/LanguageServer/Protocol/Handler/RequestContext.cs @@ -53,7 +53,7 @@ internal readonly struct RequestContext /// /// This field is only initialized for handlers that request solution context. /// - private readonly StrongBox<(Workspace Workspace, Solution Solution, Document? Document)>? _lspSolution; + private readonly StrongBox<(Workspace Workspace, Solution Solution, TextDocument? Document)>? _lspSolution; /// /// The workspace this request is for, if applicable. This will be present if is @@ -116,6 +116,43 @@ public Document? Document throw new InvalidOperationException(); } + if (_lspSolution.Value.Document is null) + { + return null; + } + + if (_lspSolution.Value.Document is Document document) + { + return document; + } + + // Explicitly throw for attempts to get a Document when only a TextDocument is available. + throw new InvalidOperationException($"Attempted to retrieve a Document but a TextDocument was found instead."); + } + } + + /// + /// The text document that the request is for, if applicable. This comes from the returned from the handler itself via a call to + /// . + /// + public TextDocument? TextDocument + { + get + { + if (_lspSolution is null) + { + // This request context never had a solution instance + return null; + } + + // The solution is available unless it has been cleared by a call to ClearSolutionContext. Explicitly throw + // for attempts to access this property after it has been manually cleared. Note that we can't rely on + // Document being null for this check, because it is not always provided as part of the solution context. + if (_lspSolution.Value.Workspace is null) + { + throw new InvalidOperationException(); + } + return _lspSolution.Value.Document; } } @@ -149,7 +186,7 @@ public RequestContext( string method, ClientCapabilities? clientCapabilities, WellKnownLspServerKinds serverKind, - Document? document, + TextDocument? document, IDocumentChangeTracker documentChangeTracker, ImmutableDictionary trackedDocuments, ImmutableArray supportedLanguages, @@ -159,7 +196,7 @@ public RequestContext( if (workspace is not null) { RoslynDebug.Assert(solution is not null); - _lspSolution = new StrongBox<(Workspace Workspace, Solution Solution, Document? Document)>((workspace, solution, document)); + _lspSolution = new StrongBox<(Workspace Workspace, Solution Solution, TextDocument? Document)>((workspace, solution, document)); } else { @@ -228,7 +265,7 @@ public static async Task CreateAsync( { Workspace? workspace = null; Solution? solution = null; - Document? document = null; + TextDocument? document = null; if (textDocument is not null) { // we were given a request associated with a document. Find the corresponding roslyn document for this. diff --git a/src/Features/LanguageServer/Protocol/Microsoft.CodeAnalysis.LanguageServer.Protocol.csproj b/src/Features/LanguageServer/Protocol/Microsoft.CodeAnalysis.LanguageServer.Protocol.csproj index c01c28c61523f..6bfd8f20ad908 100644 --- a/src/Features/LanguageServer/Protocol/Microsoft.CodeAnalysis.LanguageServer.Protocol.csproj +++ b/src/Features/LanguageServer/Protocol/Microsoft.CodeAnalysis.LanguageServer.Protocol.csproj @@ -74,6 +74,7 @@ + diff --git a/src/Features/LanguageServer/Protocol/ProtocolConstants.cs b/src/Features/LanguageServer/Protocol/ProtocolConstants.cs index cd072452be53e..04e1a666c9f0f 100644 --- a/src/Features/LanguageServer/Protocol/ProtocolConstants.cs +++ b/src/Features/LanguageServer/Protocol/ProtocolConstants.cs @@ -15,5 +15,7 @@ internal class ProtocolConstants public const string RoslynLspLanguagesContract = "RoslynLspLanguages"; public const string TypeScriptLanguageContract = "TypeScriptLspLanguage"; + + public const string RazorCohostContract = "RazorLanguageServer"; } } diff --git a/src/Features/LanguageServer/Protocol/WellKnownLspServerKinds.cs b/src/Features/LanguageServer/Protocol/WellKnownLspServerKinds.cs index a99f84b743393..da3f8390d0ac8 100644 --- a/src/Features/LanguageServer/Protocol/WellKnownLspServerKinds.cs +++ b/src/Features/LanguageServer/Protocol/WellKnownLspServerKinds.cs @@ -8,6 +8,11 @@ namespace Microsoft.CodeAnalysis.LanguageServer; internal enum WellKnownLspServerKinds { + /// + /// Razor LSP server for Razor document requests (.razor and .cshtml files) + /// + RazorCohostServer, + /// /// Roslyn LSP server for razor c# requests. /// @@ -52,6 +57,7 @@ public static string ToUserVisibleString(this WellKnownLspServerKinds server) { return server switch { + WellKnownLspServerKinds.RazorCohostServer => "Razor Cohost Language Server Client", WellKnownLspServerKinds.RazorLspServer => "Razor C# Language Server Client", WellKnownLspServerKinds.LiveShareLspServer => "Live Share C#/Visual Basic Language Server Client", WellKnownLspServerKinds.AlwaysActiveVSLspServer => "Roslyn Language Server Client", @@ -69,6 +75,8 @@ public static string ToTelemetryString(this WellKnownLspServerKinds server) { return server switch { + WellKnownLspServerKinds.RazorCohostServer => "RazorCohostLanguageClient", + // Telemetry was previously reported as RazorInProcLanguageClient.GetType().Name WellKnownLspServerKinds.RazorLspServer => "RazorInProcLanguageClient", @@ -96,6 +104,7 @@ public static string GetContractName(this WellKnownLspServerKinds server) { return server switch { + WellKnownLspServerKinds.RazorCohostServer => ProtocolConstants.RazorCohostContract, WellKnownLspServerKinds.RazorLspServer => ProtocolConstants.RoslynLspLanguagesContract, WellKnownLspServerKinds.LiveShareLspServer => ProtocolConstants.RoslynLspLanguagesContract, WellKnownLspServerKinds.AlwaysActiveVSLspServer => ProtocolConstants.RoslynLspLanguagesContract, diff --git a/src/Features/LanguageServer/Protocol/Workspaces/LspWorkspaceManager.cs b/src/Features/LanguageServer/Protocol/Workspaces/LspWorkspaceManager.cs index d6d04e0edfd8e..15cf5611badc9 100644 --- a/src/Features/LanguageServer/Protocol/Workspaces/LspWorkspaceManager.cs +++ b/src/Features/LanguageServer/Protocol/Workspaces/LspWorkspaceManager.cs @@ -213,7 +213,7 @@ public void UpdateTrackedDocument(Uri uri, SourceText newSourceText) /// /// This is always called serially in the when creating the . /// - public async Task<(Workspace?, Solution?, Document?)> GetLspDocumentInfoAsync(TextDocumentIdentifier textDocumentIdentifier, CancellationToken cancellationToken) + public async Task<(Workspace?, Solution?, TextDocument?)> GetLspDocumentInfoAsync(TextDocumentIdentifier textDocumentIdentifier, CancellationToken cancellationToken) { // Get the LSP view of all the workspace solutions. var uri = textDocumentIdentifier.Uri; @@ -222,10 +222,10 @@ public void UpdateTrackedDocument(Uri uri, SourceText newSourceText) // Find the matching document from the LSP solutions. foreach (var (workspace, lspSolution, isForked) in lspSolutions) { - var documents = lspSolution.GetDocuments(textDocumentIdentifier.Uri); + var documents = lspSolution.GetTextDocuments(textDocumentIdentifier.Uri); if (documents.Any()) { - var document = documents.FindDocumentInProjectContext(textDocumentIdentifier, (sln, id) => sln.GetRequiredDocument(id)); + var document = documents.FindDocumentInProjectContext(textDocumentIdentifier, (sln, id) => sln.GetRequiredTextDocument(id)); // Record metadata on how we got this document. var workspaceKind = document.Project.Solution.WorkspaceKind; @@ -378,7 +378,7 @@ await workspace.TryOnDocumentOpenedAsync( /// /// Given a set of documents from the workspace current solution, verify that the LSP text is the same as the document contents. /// - private async Task DoesAllTextMatchWorkspaceSolutionAsync(ImmutableDictionary> documentsInWorkspace, CancellationToken cancellationToken) + private async Task DoesAllTextMatchWorkspaceSolutionAsync(ImmutableDictionary> documentsInWorkspace, CancellationToken cancellationToken) { foreach (var (uriInWorkspace, documentsForUri) in documentsInWorkspace) { @@ -396,7 +396,7 @@ private async Task DoesAllTextMatchWorkspaceSolutionAsync(ImmutableDiction return true; } - private static async ValueTask AreChecksumsEqualAsync(Document document, SourceText lspText, CancellationToken cancellationToken) + private static async ValueTask AreChecksumsEqualAsync(TextDocument document, SourceText lspText, CancellationToken cancellationToken) { var documentText = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); if (documentText == lspText) @@ -410,12 +410,12 @@ private static async ValueTask AreChecksumsEqualAsync(Document document, S /// /// Using the workspace's current solutions, find the matching documents in for each URI. /// - private static ImmutableDictionary> GetDocumentsForUris(ImmutableArray trackedDocuments, Solution workspaceCurrentSolution) + private static ImmutableDictionary> GetDocumentsForUris(ImmutableArray trackedDocuments, Solution workspaceCurrentSolution) { - using var _ = PooledDictionary>.GetInstance(out var documentsInSolution); + using var _ = PooledDictionary>.GetInstance(out var documentsInSolution); foreach (var trackedDoc in trackedDocuments) { - var documents = workspaceCurrentSolution.GetDocuments(trackedDoc); + var documents = workspaceCurrentSolution.GetTextDocuments(trackedDoc); if (documents.Any()) { documentsInSolution[trackedDoc] = documents; diff --git a/src/Features/LanguageServer/ProtocolUnitTests/Workspaces/LspWorkspaceManagerTests.cs b/src/Features/LanguageServer/ProtocolUnitTests/Workspaces/LspWorkspaceManagerTests.cs index 4357dedb32d48..8ee5c538f8e53 100644 --- a/src/Features/LanguageServer/ProtocolUnitTests/Workspaces/LspWorkspaceManagerTests.cs +++ b/src/Features/LanguageServer/ProtocolUnitTests/Workspaces/LspWorkspaceManagerTests.cs @@ -710,7 +710,7 @@ private static bool IsWorkspaceRegistered(Workspace workspace, TestLspServer tes private static async Task<(Workspace? workspace, Document? document)> GetLspWorkspaceAndDocumentAsync(Uri uri, TestLspServer testLspServer) { var (workspace, _, document) = await testLspServer.GetManager().GetLspDocumentInfoAsync(CreateTextDocumentIdentifier(uri), CancellationToken.None).ConfigureAwait(false); - return (workspace, document); + return (workspace, document as Document); } private static Task<(Workspace?, Solution?)> GetLspHostWorkspaceAndSolutionAsync(TestLspServer testLspServer) diff --git a/src/Tools/ExternalAccess/Razor/Cohost/AbstractRazorCohostDocumentRequestHandler.cs b/src/Tools/ExternalAccess/Razor/Cohost/AbstractRazorCohostDocumentRequestHandler.cs new file mode 100644 index 0000000000000..e0fa705ee48b1 --- /dev/null +++ b/src/Tools/ExternalAccess/Razor/Cohost/AbstractRazorCohostDocumentRequestHandler.cs @@ -0,0 +1,43 @@ +// 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 Microsoft.CommonLanguageServerProtocol.Framework; +using Microsoft.VisualStudio.LanguageServer.Protocol; + +namespace Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost; + +internal abstract class AbstractRazorCohostDocumentRequestHandler : AbstractRazorCohostRequestHandler, ITextDocumentIdentifierHandler +{ + TextDocumentIdentifier? ITextDocumentIdentifierHandler.GetTextDocumentIdentifier(TRequestType request) + { + var razorIdentifier = GetRazorTextDocumentIdentifier(request); + if (razorIdentifier == null) + { + return null; + } + + var textDocumentIdentifier = new VSTextDocumentIdentifier + { + Uri = razorIdentifier.Value.Uri, + }; + + if (razorIdentifier.Value.ProjectContextId != null) + { + textDocumentIdentifier.ProjectContext = new VSProjectContext + { + Id = razorIdentifier.Value.ProjectContextId + }; + } + + return textDocumentIdentifier; + } + + protected abstract RazorTextDocumentIdentifier? GetRazorTextDocumentIdentifier(TRequestType request); +} + +/// +/// Custom type containing information in a to avoid coupling LSP protocol versions. +/// +internal record struct RazorTextDocumentIdentifier(Uri Uri, string? ProjectContextId); diff --git a/src/Tools/ExternalAccess/Razor/Cohost/AbstractRazorLspService.cs b/src/Tools/ExternalAccess/Razor/Cohost/AbstractRazorLspService.cs new file mode 100644 index 0000000000000..fe2161116ec4b --- /dev/null +++ b/src/Tools/ExternalAccess/Razor/Cohost/AbstractRazorLspService.cs @@ -0,0 +1,15 @@ +// 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 Microsoft.CodeAnalysis.LanguageServer; + +namespace Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost; + +/// +/// Base class for services that need to live in Razor but be exported using +/// since those services must implement but the Razor code doesn't have IVT to it. +/// +internal abstract class AbstractRazorLspService : ILspService +{ +} diff --git a/src/Tools/ExternalAccess/Razor/Cohost/AbstractRazorRequestHandler.cs b/src/Tools/ExternalAccess/Razor/Cohost/AbstractRazorRequestHandler.cs new file mode 100644 index 0000000000000..33492c7b05fa7 --- /dev/null +++ b/src/Tools/ExternalAccess/Razor/Cohost/AbstractRazorRequestHandler.cs @@ -0,0 +1,35 @@ +// 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.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.LanguageServer.Handler; +using Microsoft.CommonLanguageServerProtocol.Framework; + +namespace Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost; + +internal abstract class AbstractRazorCohostRequestHandler : ILspServiceRequestHandler +{ + bool IMethodHandler.MutatesSolutionState => MutatesSolutionState; + + bool ISolutionRequiredHandler.RequiresLSPSolution => RequiresLSPSolution; + + Task IRequestHandler.HandleRequestAsync(TRequestType request, RequestContext context, CancellationToken cancellationToken) + { + // We have to wrap the RequestContext in order to expose it to Roslyn. We could create our own (by exporting + // and IRequestContextFactory) but that would not be possible if/when we live in the same server as Roslyn + // so may as well deal with it now. + // This does mean we can't nicely pass through the original Uri, which would have ProjectContext info, but + // we get the Project so that will have to do. + + var razorRequestContext = new RazorCohostRequestContext(context); + return HandleRequestAsync(request, razorRequestContext, cancellationToken); + } + + protected abstract bool MutatesSolutionState { get; } + + protected abstract bool RequiresLSPSolution { get; } + + protected abstract Task HandleRequestAsync(TRequestType request, RazorCohostRequestContext context, CancellationToken cancellationToken); +} diff --git a/src/Tools/ExternalAccess/Razor/Cohost/Constants.cs b/src/Tools/ExternalAccess/Razor/Cohost/Constants.cs new file mode 100644 index 0000000000000..520faed5e9018 --- /dev/null +++ b/src/Tools/ExternalAccess/Razor/Cohost/Constants.cs @@ -0,0 +1,17 @@ +// 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.Collections.Immutable; +using Microsoft.CodeAnalysis.LanguageServer; + +namespace Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost; + +internal static class Constants +{ + public const string RazorLSPContentType = "Razor"; + + public const string RazorLanguageContract = ProtocolConstants.RazorCohostContract; + + public static readonly ImmutableArray RazorLanguage = ImmutableArray.Create("Razor"); +} diff --git a/src/Tools/ExternalAccess/Razor/Cohost/ExportRazorLspServiceFactoryAttribute.cs b/src/Tools/ExternalAccess/Razor/Cohost/ExportRazorLspServiceFactoryAttribute.cs new file mode 100644 index 0000000000000..8c23bcce2509c --- /dev/null +++ b/src/Tools/ExternalAccess/Razor/Cohost/ExportRazorLspServiceFactoryAttribute.cs @@ -0,0 +1,14 @@ +// 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.LanguageServer; + +namespace Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost; + +[AttributeUsage(AttributeTargets.Class), MetadataAttribute] +internal class ExportRazorLspServiceFactoryAttribute(Type handlerType) : ExportLspServiceFactoryAttribute(handlerType, Constants.RazorLanguageContract, WellKnownLspServerKinds.RazorCohostServer) +{ +} diff --git a/src/Tools/ExternalAccess/Razor/Cohost/ExportRazorStatelessLspServiceAttribute.cs b/src/Tools/ExternalAccess/Razor/Cohost/ExportRazorStatelessLspServiceAttribute.cs new file mode 100644 index 0000000000000..3728932cce877 --- /dev/null +++ b/src/Tools/ExternalAccess/Razor/Cohost/ExportRazorStatelessLspServiceAttribute.cs @@ -0,0 +1,14 @@ +// 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.LanguageServer; + +namespace Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost; + +[AttributeUsage(AttributeTargets.Class), MetadataAttribute] +internal class ExportRazorStatelessLspServiceAttribute(Type handlerType) : ExportStatelessLspServiceAttribute(handlerType, Constants.RazorLanguageContract, WellKnownLspServerKinds.RazorCohostServer) +{ +} diff --git a/src/Tools/ExternalAccess/Razor/Cohost/IRazorCohostCapabilitiesProvider.cs b/src/Tools/ExternalAccess/Razor/Cohost/IRazorCohostCapabilitiesProvider.cs new file mode 100644 index 0000000000000..1679c16c61651 --- /dev/null +++ b/src/Tools/ExternalAccess/Razor/Cohost/IRazorCohostCapabilitiesProvider.cs @@ -0,0 +1,10 @@ +// 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. + +namespace Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost; + +internal interface IRazorCohostCapabilitiesProvider +{ + string GetCapabilities(string clientCapabilities); +} diff --git a/src/Tools/ExternalAccess/Razor/Cohost/IRazorCohostClientLanguageServerManager.cs b/src/Tools/ExternalAccess/Razor/Cohost/IRazorCohostClientLanguageServerManager.cs new file mode 100644 index 0000000000000..40f56f01e0c85 --- /dev/null +++ b/src/Tools/ExternalAccess/Razor/Cohost/IRazorCohostClientLanguageServerManager.cs @@ -0,0 +1,19 @@ +// 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.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.LanguageServer; + +namespace Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost +{ + internal interface IRazorCohostClientLanguageServerManager : ILspService + { + ValueTask SendNotificationAsync(string methodName, CancellationToken cancellationToken); + ValueTask SendNotificationAsync(string methodName, TParams @params, CancellationToken cancellationToken); + ValueTask SendRequestAsync(string methodName, CancellationToken cancellationToken); + Task SendRequestAsync(string methodName, TParams @params, CancellationToken cancellationToken); + ValueTask SendRequestAsync(string methodName, TParams @params, CancellationToken cancellationToken); + } +} diff --git a/src/Tools/ExternalAccess/Razor/Cohost/IRazorCohostLanguageClientActivationService.cs b/src/Tools/ExternalAccess/Razor/Cohost/IRazorCohostLanguageClientActivationService.cs new file mode 100644 index 0000000000000..4b1a3fc34067e --- /dev/null +++ b/src/Tools/ExternalAccess/Razor/Cohost/IRazorCohostLanguageClientActivationService.cs @@ -0,0 +1,13 @@ +// 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. + +namespace Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost; + +internal interface IRazorCohostLanguageClientActivationService +{ + /// + /// Returns whether the Razor cohost server should activate + /// + bool ShouldActivateCohostServer(); +} diff --git a/src/Tools/ExternalAccess/Razor/Cohost/IRazorCustomMessageTarget.cs b/src/Tools/ExternalAccess/Razor/Cohost/IRazorCustomMessageTarget.cs new file mode 100644 index 0000000000000..28e11a4a5cad4 --- /dev/null +++ b/src/Tools/ExternalAccess/Razor/Cohost/IRazorCustomMessageTarget.cs @@ -0,0 +1,9 @@ +// 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. + +namespace Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost; + +internal interface IRazorCustomMessageTarget +{ +} diff --git a/src/Tools/ExternalAccess/Razor/Cohost/RazorCohostClientLanguageServerManagerFactory.cs b/src/Tools/ExternalAccess/Razor/Cohost/RazorCohostClientLanguageServerManagerFactory.cs new file mode 100644 index 0000000000000..4f5b630e193c7 --- /dev/null +++ b/src/Tools/ExternalAccess/Razor/Cohost/RazorCohostClientLanguageServerManagerFactory.cs @@ -0,0 +1,43 @@ +// 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 System.Threading.Tasks; +using System.Threading; +using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.CodeAnalysis.LanguageServer; + +namespace Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost; + +[ExportRazorLspServiceFactory(typeof(IRazorCohostClientLanguageServerManager)), Shared] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal class RazorCohostClientLanguageServerManagerFactory() : ILspServiceFactory +{ + public ILspService CreateILspService(LspServices lspServices, WellKnownLspServerKinds serverKind) + { + var notificationManager = lspServices.GetRequiredService(); + + return new RazorCohostClientLanguageServerManager(notificationManager); + } + + internal class RazorCohostClientLanguageServerManager(IClientLanguageServerManager clientLanguageServerManager) : IRazorCohostClientLanguageServerManager + { + public Task SendRequestAsync(string methodName, TParams @params, CancellationToken cancellationToken) + => clientLanguageServerManager.SendRequestAsync(methodName, @params, cancellationToken); + + public ValueTask SendRequestAsync(string methodName, CancellationToken cancellationToken) + => clientLanguageServerManager.SendRequestAsync(methodName, cancellationToken); + + public ValueTask SendRequestAsync(string methodName, TParams @params, CancellationToken cancellationToken) + => clientLanguageServerManager.SendRequestAsync(methodName, @params, cancellationToken); + + public ValueTask SendNotificationAsync(string methodName, CancellationToken cancellationToken) + => clientLanguageServerManager.SendNotificationAsync(methodName, cancellationToken); + + public ValueTask SendNotificationAsync(string methodName, TParams @params, CancellationToken cancellationToken) + => clientLanguageServerManager.SendNotificationAsync(methodName, @params, cancellationToken); + } +} diff --git a/src/Tools/ExternalAccess/Razor/Cohost/RazorCohostDidChangeEndpoint.cs b/src/Tools/ExternalAccess/Razor/Cohost/RazorCohostDidChangeEndpoint.cs new file mode 100644 index 0000000000000..1a7d0d17ca431 --- /dev/null +++ b/src/Tools/ExternalAccess/Razor/Cohost/RazorCohostDidChangeEndpoint.cs @@ -0,0 +1,56 @@ +// 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 System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.CodeAnalysis.LanguageServer; +using Microsoft.CodeAnalysis.LanguageServer.Handler; +using Microsoft.CodeAnalysis.Text; +using Microsoft.CommonLanguageServerProtocol.Framework; +using Microsoft.VisualStudio.LanguageServer.Protocol; + +namespace Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost; + +[LanguageServerEndpoint(Methods.TextDocumentDidChangeName)] +[ExportRazorStatelessLspService(typeof(RazorCohostDidChangeEndpoint)), Shared] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal sealed class RazorCohostDidChangeEndpoint([Import(AllowDefault = true)] IRazorCohostDidChangeHandler? didChangeHandler) : ILspServiceDocumentRequestHandler +{ + public bool MutatesSolutionState => true; + public bool RequiresLSPSolution => false; + + public TextDocumentIdentifier GetTextDocumentIdentifier(DidChangeTextDocumentParams request) + => request.TextDocument; + + public async Task HandleRequestAsync(DidChangeTextDocumentParams request, RequestContext context, CancellationToken cancellationToken) + { + var text = context.GetTrackedDocumentSourceText(request.TextDocument.Uri); + + // Per the LSP spec, each text change builds upon the previous, so we don't need to translate any text + // positions between changes, which makes this quite easy. See + // https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#didChangeTextDocumentParams + // for more details. + foreach (var change in request.ContentChanges) + text = text.WithChanges(ProtocolConversions.ContentChangeEventToTextChange(change, text)); + + context.UpdateTrackedDocument(request.TextDocument.Uri, text); + + // Razor can't handle this request because they don't have access to the RequestContext, but they might want to do something with it + if (didChangeHandler is not null) + { + await didChangeHandler.HandleAsync(request.TextDocument.Uri, request.TextDocument.Version, text, cancellationToken).ConfigureAwait(false); + } + + return null; + } +} + +internal interface IRazorCohostDidChangeHandler +{ + Task HandleAsync(Uri uri, int version, SourceText sourceText, CancellationToken cancellationToken); +} diff --git a/src/Tools/ExternalAccess/Razor/Cohost/RazorCohostDidCloseEndpoint.cs b/src/Tools/ExternalAccess/Razor/Cohost/RazorCohostDidCloseEndpoint.cs new file mode 100644 index 0000000000000..fccc2a100a2b5 --- /dev/null +++ b/src/Tools/ExternalAccess/Razor/Cohost/RazorCohostDidCloseEndpoint.cs @@ -0,0 +1,45 @@ +// 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 System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.CodeAnalysis.LanguageServer.Handler; +using Microsoft.CommonLanguageServerProtocol.Framework; +using Microsoft.VisualStudio.LanguageServer.Protocol; + +namespace Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost; + +[LanguageServerEndpoint(Methods.TextDocumentDidCloseName)] +[ExportRazorStatelessLspService(typeof(RazorCohostDidCloseEndpoint)), Shared] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal sealed class RazorCohostDidCloseEndpoint([Import(AllowDefault = true)] IRazorCohostDidCloseHandler? didCloseHandler) : ILspServiceNotificationHandler, ITextDocumentIdentifierHandler +{ + public bool MutatesSolutionState => true; + public bool RequiresLSPSolution => false; + + public TextDocumentIdentifier GetTextDocumentIdentifier(DidCloseTextDocumentParams request) + => request.TextDocument; + + public async Task HandleNotificationAsync(DidCloseTextDocumentParams request, RequestContext context, CancellationToken cancellationToken) + { + context.TraceInformation($"didClose for {request.TextDocument.Uri}"); + + await context.StopTrackingAsync(request.TextDocument.Uri, cancellationToken).ConfigureAwait(false); + + // Razor can't handle this request because they don't have access to the RequestContext, but they might want to do something with it + if (didCloseHandler is not null) + { + await didCloseHandler.HandleAsync(request.TextDocument.Uri, cancellationToken).ConfigureAwait(false); + } + } +} + +internal interface IRazorCohostDidCloseHandler +{ + Task HandleAsync(Uri uri, CancellationToken cancellationToken); +} diff --git a/src/Tools/ExternalAccess/Razor/Cohost/RazorCohostDidOpenEndpoint.cs b/src/Tools/ExternalAccess/Razor/Cohost/RazorCohostDidOpenEndpoint.cs new file mode 100644 index 0000000000000..cf04d3c08f08f --- /dev/null +++ b/src/Tools/ExternalAccess/Razor/Cohost/RazorCohostDidOpenEndpoint.cs @@ -0,0 +1,50 @@ +// 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 System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.CodeAnalysis.LanguageServer.Handler; +using Microsoft.CodeAnalysis.Text; +using Microsoft.CommonLanguageServerProtocol.Framework; +using Microsoft.VisualStudio.LanguageServer.Protocol; + +namespace Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost; + +[LanguageServerEndpoint(Methods.TextDocumentDidOpenName)] +[ExportRazorStatelessLspService(typeof(RazorCohostDidOpenEndpoint)), Shared] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal sealed class RazorCohostDidOpenEndpoint( + [Import(AllowDefault = true)] IRazorCohostDidOpenHandler? didOpenHandler) + : ILspServiceNotificationHandler, ITextDocumentIdentifierHandler +{ + public bool MutatesSolutionState => true; + public bool RequiresLSPSolution => false; + + public Uri GetTextDocumentIdentifier(DidOpenTextDocumentParams request) + => request.TextDocument.Uri; + + public async Task HandleNotificationAsync(DidOpenTextDocumentParams request, RequestContext context, CancellationToken cancellationToken) + { + context.TraceInformation($"didOpen for {request.TextDocument.Uri}"); + + var sourceText = SourceText.From(request.TextDocument.Text, System.Text.Encoding.UTF8, SourceHashAlgorithms.OpenDocumentChecksumAlgorithm); + + await context.StartTrackingAsync(request.TextDocument.Uri, sourceText, request.TextDocument.LanguageId, cancellationToken).ConfigureAwait(false); + + // Razor can't handle this request because they don't have access to the RequestContext, but they might want to do something with it + if (didOpenHandler is not null) + { + await didOpenHandler.HandleAsync(request.TextDocument.Uri, request.TextDocument.Version, sourceText, cancellationToken).ConfigureAwait(false); + } + } +} + +internal interface IRazorCohostDidOpenHandler +{ + Task HandleAsync(Uri uri, int version, SourceText sourceText, CancellationToken cancellationToken); +} diff --git a/src/Tools/ExternalAccess/Razor/Cohost/RazorCohostLanguageClient.cs b/src/Tools/ExternalAccess/Razor/Cohost/RazorCohostLanguageClient.cs new file mode 100644 index 0000000000000..81149a07a2ea7 --- /dev/null +++ b/src/Tools/ExternalAccess/Razor/Cohost/RazorCohostLanguageClient.cs @@ -0,0 +1,82 @@ +// 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.Collections.Immutable; +using System.ComponentModel.Composition; +using Microsoft.CodeAnalysis.Editor.Implementation.LanguageClient; +using Microsoft.CodeAnalysis.Editor.Shared.Utilities; +using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.CodeAnalysis.LanguageServer; +using Microsoft.CodeAnalysis.Options; +using Microsoft.VisualStudio.Composition; +using Microsoft.VisualStudio.LanguageServer.Client; +using Microsoft.VisualStudio.LanguageServer.Protocol; +using Microsoft.VisualStudio.Utilities; +using Newtonsoft.Json; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost; + +/// +/// A language server that handles requests .razor and .cshtml files. Endpoints and required services are supplied +/// by the Razor tooling team in the Razor tooling repo. +/// +[ContentType(Constants.RazorLSPContentType)] +[Export(typeof(ILanguageClient))] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal sealed class RazorCohostLanguageClient( + RazorLspServiceProvider lspServiceProvider, + IGlobalOptionService globalOptions, + IThreadingContext threadingContext, + ILspServiceLoggerFactory lspLoggerFactory, + ExportProvider exportProvider, + [Import(AllowDefault = true)] IRazorCohostCapabilitiesProvider? razorCapabilitiesProvider = null, + [Import(AllowDefault = true)] IRazorCustomMessageTarget? razorCustomMessageTarget = null, + [Import(AllowDefault = true)] IRazorCohostLanguageClientActivationService? razorCohostLanguageClientActivationService = null) + : AbstractInProcLanguageClient(lspServiceProvider, globalOptions, lspLoggerFactory, threadingContext, exportProvider, middleLayer: null) +{ + protected override ImmutableArray SupportedLanguages => Constants.RazorLanguage; + + public override object? CustomMessageTarget => razorCustomMessageTarget; + + public override ServerCapabilities GetCapabilities(ClientCapabilities clientCapabilities) + { + if (razorCohostLanguageClientActivationService?.ShouldActivateCohostServer() != true) + { + // A default return would still opt us in to TextDocumentSync for open files, but even that's + // wasteful if we don't want to handle anything, as we'd track document content. + return new() + { + TextDocumentSync = new TextDocumentSyncOptions { OpenClose = false, Change = TextDocumentSyncKind.None, Save = false, WillSave = false, WillSaveWaitUntil = false } + }; + } + + Contract.ThrowIfNull(razorCapabilitiesProvider); + + // We use a string to pass capabilities to/from Razor to avoid version issues with the Protocol DLL + var serializedClientCapabilities = JsonConvert.SerializeObject(clientCapabilities); + var serializedServerCapabilities = razorCapabilitiesProvider.GetCapabilities(serializedClientCapabilities); + var razorCapabilities = JsonConvert.DeserializeObject(serializedServerCapabilities); + Contract.ThrowIfNull(razorCapabilities); + + // We support a few things on this side, so lets make sure they're set + razorCapabilities.ProjectContextProvider = true; + razorCapabilities.TextDocumentSync = new TextDocumentSyncOptions + { + OpenClose = true, + Change = TextDocumentSyncKind.Incremental + }; + + return razorCapabilities; + } + + /// + /// If the cohost server is expected to activate then any failures are catastrophic as no razor features will work. + /// + public override bool ShowNotificationOnInitializeFailed => razorCohostLanguageClientActivationService?.ShouldActivateCohostServer() == true; + + public override WellKnownLspServerKinds ServerKind => WellKnownLspServerKinds.RazorCohostServer; +} diff --git a/src/Tools/ExternalAccess/Razor/Cohost/RazorCohostProjectContextEndpoint.cs b/src/Tools/ExternalAccess/Razor/Cohost/RazorCohostProjectContextEndpoint.cs new file mode 100644 index 0000000000000..8c2606a48cdf6 --- /dev/null +++ b/src/Tools/ExternalAccess/Razor/Cohost/RazorCohostProjectContextEndpoint.cs @@ -0,0 +1,17 @@ +// 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.Mef; +using Microsoft.CodeAnalysis.LanguageServer.Handler; + +namespace Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost; + +[ExportRazorStatelessLspService(typeof(RazorCohostProjectContextEndpoint)), Shared] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal sealed class RazorCohostProjectContextEndpoint() : GetTextDocumentWithContextHandler +{ +} diff --git a/src/Tools/ExternalAccess/Razor/Cohost/RazorCohostRequestContext.cs b/src/Tools/ExternalAccess/Razor/Cohost/RazorCohostRequestContext.cs new file mode 100644 index 0000000000000..8f39d61464558 --- /dev/null +++ b/src/Tools/ExternalAccess/Razor/Cohost/RazorCohostRequestContext.cs @@ -0,0 +1,23 @@ +// 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 Microsoft.CodeAnalysis.LanguageServer; +using Microsoft.CodeAnalysis.LanguageServer.Handler; + +namespace Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost; + +internal readonly struct RazorCohostRequestContext(RequestContext context) +{ + internal string Method => context.Method; + internal Uri? Uri => context.TextDocument?.GetURI(); + /// + internal Workspace? Workspace => context.Workspace; + /// + internal Solution? Solution => context.Solution; + /// + internal TextDocument? TextDocument => context.TextDocument; + + internal T GetRequiredService() where T : class => context.GetRequiredService(); +} diff --git a/src/Tools/ExternalAccess/Razor/Cohost/RazorLspServiceProvider.cs b/src/Tools/ExternalAccess/Razor/Cohost/RazorLspServiceProvider.cs new file mode 100644 index 0000000000000..dcf4b2487dab8 --- /dev/null +++ b/src/Tools/ExternalAccess/Razor/Cohost/RazorLspServiceProvider.cs @@ -0,0 +1,21 @@ +// 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.Collections.Generic; +using System.Composition; +using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.CodeAnalysis.LanguageServer; + +namespace Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost; + +[Export(typeof(RazorLspServiceProvider)), Shared] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal sealed class RazorLspServiceProvider( + [ImportMany(Constants.RazorLanguageContract)] IEnumerable> lspServices, + [ImportMany(Constants.RazorLanguageContract)] IEnumerable> lspServiceFactories) + : AbstractLspServiceProvider(lspServices, lspServiceFactories) +{ +} diff --git a/src/Tools/ExternalAccess/Razor/Cohost/RazorLspWorkspaceManagerFactory.cs b/src/Tools/ExternalAccess/Razor/Cohost/RazorLspWorkspaceManagerFactory.cs new file mode 100644 index 0000000000000..2f74e31c45fca --- /dev/null +++ b/src/Tools/ExternalAccess/Razor/Cohost/RazorLspWorkspaceManagerFactory.cs @@ -0,0 +1,17 @@ +// 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.Mef; +using Microsoft.CodeAnalysis.LanguageServer; + +namespace Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost; + +[ExportRazorLspServiceFactory(typeof(LspWorkspaceManager)), Shared] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal sealed class RazorLspWorkspaceManagerFactory(LspWorkspaceRegistrationService lspWorkspaceRegistrationService) : LspWorkspaceManagerFactory(lspWorkspaceRegistrationService) +{ +} diff --git a/src/Tools/ExternalAccess/Razor/Cohost/RazorRequestExecutionQueueProvider.cs b/src/Tools/ExternalAccess/Razor/Cohost/RazorRequestExecutionQueueProvider.cs new file mode 100644 index 0000000000000..5d3e8fb4bb23e --- /dev/null +++ b/src/Tools/ExternalAccess/Razor/Cohost/RazorRequestExecutionQueueProvider.cs @@ -0,0 +1,25 @@ +// 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.Mef; +using Microsoft.CodeAnalysis.LanguageServer; +using Microsoft.CodeAnalysis.LanguageServer.Handler; +using Microsoft.CommonLanguageServerProtocol.Framework; + +namespace Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost; + +[ExportRazorStatelessLspService(typeof(IRequestExecutionQueueProvider)), Shared] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal sealed class RazorRequestExecutionQueueProvider() : IRequestExecutionQueueProvider +{ + public IRequestExecutionQueue CreateRequestExecutionQueue(AbstractLanguageServer languageServer, ILspLogger logger, IHandlerProvider handlerProvider) + { + var queue = new RoslynRequestExecutionQueue(languageServer, logger, handlerProvider); + queue.Start(); + return queue; + } +} diff --git a/src/Tools/ExternalAccess/Razor/Cohost/RazorRequestTelemetryLoggerFactory.cs b/src/Tools/ExternalAccess/Razor/Cohost/RazorRequestTelemetryLoggerFactory.cs new file mode 100644 index 0000000000000..9c761d6da9ef6 --- /dev/null +++ b/src/Tools/ExternalAccess/Razor/Cohost/RazorRequestTelemetryLoggerFactory.cs @@ -0,0 +1,17 @@ +// 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.Mef; +using Microsoft.CodeAnalysis.LanguageServer.Handler; + +namespace Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost; + +[ExportRazorLspServiceFactory(typeof(RequestTelemetryLogger)), Shared] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal class RazorRequestTelemetryLoggerFactory() : RequestTelemetryLoggerFactory() +{ +} diff --git a/src/Tools/ExternalAccess/Razor/InternalAPI.Unshipped.txt b/src/Tools/ExternalAccess/Razor/InternalAPI.Unshipped.txt index 4c2d35bcacd92..32ea52d163dd9 100644 --- a/src/Tools/ExternalAccess/Razor/InternalAPI.Unshipped.txt +++ b/src/Tools/ExternalAccess/Razor/InternalAPI.Unshipped.txt @@ -1,4 +1,58 @@ #nullable enable +abstract Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost.AbstractRazorCohostDocumentRequestHandler.GetRazorTextDocumentIdentifier(TRequestType request) -> Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost.RazorTextDocumentIdentifier? +abstract Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost.AbstractRazorCohostDocumentRequestHandler.GetRazorTextDocumentIdentifier(TRequestType request) -> Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost.RazorTextDocumentIdentifier? +abstract Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost.AbstractCohostRazorRequestHandler.HandleRequestAsync(TRequestType request, Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost.RazorCohostRequestContext context, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +abstract Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost.AbstractCohostRazorRequestHandler.MutatesSolutionState.get -> bool +abstract Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost.AbstractCohostRazorRequestHandler.RequiresLSPSolution.get -> bool +const Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost.Constants.RazorLanguageContract = "RazorLanguageServer" -> string! +const Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost.Constants.RazorLSPContentType = "Razor" -> string! +Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost.AbstractRazorCohostDocumentRequestHandler +Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost.AbstractRazorCohostDocumentRequestHandler.AbstractRazorCohostDocumentRequestHandler() -> void +Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost.AbstractCohostRazorRequestHandler +Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost.AbstractCohostRazorRequestHandler.AbstractCohostRazorRequestHandler() -> void +Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost.Constants +Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost.ExportRazorLspServiceFactoryAttribute +Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost.ExportRazorLspServiceFactoryAttribute.ExportRazorLspServiceFactoryAttribute(System.Type! handlerType) -> void +Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost.ExportRazorStatelessLspServiceAttribute +Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost.ExportRazorStatelessLspServiceAttribute.ExportRazorStatelessLspServiceAttribute(System.Type! handlerType) -> void +Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost.IRazorCohostCapabilitiesProvider +Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost.IRazorCohostCapabilitiesProvider.GetCapabilities(string! clientCapabilities) -> string! +Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost.IRazorCohostLanguageClientActivationService +Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost.IRazorCohostLanguageClientActivationService.ShouldActivateCohostServer() -> bool +Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost.IRazorCustomMessageTarget +Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost.RazorCohostLanguageClient +Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost.RazorCohostLanguageClient.RazorCohostLanguageClient(Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost.RazorLspServiceProvider! lspServiceProvider, Microsoft.CodeAnalysis.Options.IGlobalOptionService! globalOptions, Microsoft.CodeAnalysis.Editor.Shared.Utilities.IThreadingContext! threadingContext, Microsoft.CodeAnalysis.LanguageServer.ILspServiceLoggerFactory! lspLoggerFactory, Microsoft.VisualStudio.Composition.ExportProvider! exportProvider, Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost.IRazorCohostCapabilitiesProvider? razorCapabilitiesProvider = null, Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost.IRazorCustomMessageTarget? razorCustomMessageTarget = null, Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost.IRazorCohostLanguageClientActivationService? razorCohostLanguageClientActivationService = null) -> void +Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost.RazorLspServiceProvider +Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost.RazorLspServiceProvider.RazorLspServiceProvider(System.Collections.Generic.IEnumerable!>! lspServices, System.Collections.Generic.IEnumerable!>! lspServiceFactories) -> void +Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost.RazorLspWorkspaceManagerFactory +Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost.RazorLspWorkspaceManagerFactory.RazorLspWorkspaceManagerFactory(Microsoft.CodeAnalysis.LanguageServer.LspWorkspaceRegistrationService! lspWorkspaceRegistrationService) -> void +Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost.RazorCohostRequestContext +Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost.RazorCohostRequestContext.Equals(Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost.RazorCohostRequestContext other) -> bool +Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost.RazorCohostRequestContext.Method.get -> string! +Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost.RazorCohostRequestContext.Method.set -> void +Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost.RazorCohostRequestContext.RazorCohostRequestContext(string! Method, System.Uri? Uri, Microsoft.CodeAnalysis.Solution? Solution, Microsoft.CodeAnalysis.TextDocument? TextDocument) -> void +Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost.RazorCohostRequestContext.Deconstruct(out string! Method, out System.Uri? Uri, out Microsoft.CodeAnalysis.Solution? Solution, out Microsoft.CodeAnalysis.TextDocument? TextDocument) -> void +Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost.RazorCohostRequestContext.TextDocument.get -> Microsoft.CodeAnalysis.TextDocument? +Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost.RazorCohostRequestContext.TextDocument.set -> void +Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost.RazorCohostRequestContext.RazorCohostRequestContext() -> void +Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost.RazorCohostRequestContext.Solution.get -> Microsoft.CodeAnalysis.Solution? +Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost.RazorCohostRequestContext.Solution.set -> void +Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost.RazorCohostRequestContext.Uri.get -> System.Uri? +Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost.RazorCohostRequestContext.Uri.set -> void +Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost.RazorRequestExecutionQueueProvider +Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost.RazorRequestExecutionQueueProvider.CreateRequestExecutionQueue(Microsoft.CommonLanguageServerProtocol.Framework.AbstractLanguageServer! languageServer, Microsoft.CommonLanguageServerProtocol.Framework.ILspLogger! logger, Microsoft.CommonLanguageServerProtocol.Framework.IHandlerProvider! handlerProvider) -> Microsoft.CommonLanguageServerProtocol.Framework.IRequestExecutionQueue! +Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost.RazorRequestExecutionQueueProvider.RazorRequestExecutionQueueProvider() -> void +Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost.RazorRequestTelemetryLoggerFactory +Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost.RazorRequestTelemetryLoggerFactory.RazorRequestTelemetryLoggerFactory() -> void +Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost.RazorTextDocumentIdentifier +Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost.RazorTextDocumentIdentifier.Equals(Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost.RazorTextDocumentIdentifier other) -> bool +Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost.RazorTextDocumentIdentifier.Deconstruct(out System.Uri! Uri, out string? ProjectContextId) -> void +Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost.RazorTextDocumentIdentifier.ProjectContextId.get -> string? +Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost.RazorTextDocumentIdentifier.ProjectContextId.set -> void +Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost.RazorTextDocumentIdentifier.RazorTextDocumentIdentifier() -> void +Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost.RazorTextDocumentIdentifier.RazorTextDocumentIdentifier(System.Uri! Uri, string? ProjectContextId) -> void +Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost.RazorTextDocumentIdentifier.Uri.get -> System.Uri! +Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost.RazorTextDocumentIdentifier.Uri.set -> void Microsoft.CodeAnalysis.ExternalAccess.Razor.IRazorAsynchronousOperationListenerProviderAccessor Microsoft.CodeAnalysis.ExternalAccess.Razor.IRazorAsynchronousOperationListenerProviderAccessor.GetListener(string! featureName) -> Microsoft.CodeAnalysis.ExternalAccess.Razor.RazorAsynchronousOperationListenerWrapper Microsoft.CodeAnalysis.ExternalAccess.Razor.IRazorCapabilitiesProvider @@ -169,6 +223,13 @@ Microsoft.CodeAnalysis.ExternalAccess.Razor.RazorTestWorkspaceRegistrationServic Microsoft.CodeAnalysis.ExternalAccess.Razor.RazorTestWorkspaceRegistrationService.RazorTestWorkspaceRegistrationService() -> void Microsoft.CodeAnalysis.ExternalAccess.Razor.RazorTestWorkspaceRegistrationService.Register(Microsoft.CodeAnalysis.Workspace! workspace) -> void Microsoft.CodeAnalysis.ExternalAccess.Razor.RazorUri +override Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost.RazorCohostLanguageClient.ActivateAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +override Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost.RazorCohostLanguageClient.CustomMessageTarget.get -> object? +override Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost.RazorCohostLanguageClient.GetCapabilities(Microsoft.VisualStudio.LanguageServer.Protocol.ClientCapabilities! clientCapabilities) -> Microsoft.VisualStudio.LanguageServer.Protocol.ServerCapabilities! +override Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost.RazorCohostLanguageClient.ServerKind.get -> Microsoft.CodeAnalysis.LanguageServer.WellKnownLspServerKinds +override Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost.RazorCohostLanguageClient.ShowNotificationOnInitializeFailed.get -> bool +override Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost.RazorCohostRequestContext.GetHashCode() -> int +override Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost.RazorTextDocumentIdentifier.GetHashCode() -> int override Microsoft.CodeAnalysis.ExternalAccess.Razor.RazorCSharpInterceptionMiddleLayerWrapper.CanHandle(string! methodName) -> bool override Microsoft.CodeAnalysis.ExternalAccess.Razor.RazorCSharpInterceptionMiddleLayerWrapper.HandleNotificationAsync(string! methodName, Newtonsoft.Json.Linq.JToken! methodParam, System.Func! sendNotification) -> System.Threading.Tasks.Task! override Microsoft.CodeAnalysis.ExternalAccess.Razor.RazorCSharpInterceptionMiddleLayerWrapper.HandleRequestAsync(string! methodName, Newtonsoft.Json.Linq.JToken! methodParam, System.Func!>! sendRequest) -> System.Threading.Tasks.Task! @@ -191,6 +252,10 @@ readonly Microsoft.CodeAnalysis.ExternalAccess.Razor.RazorMappedSpanResult.Span readonly Microsoft.CodeAnalysis.ExternalAccess.Razor.RazorPinnedSolutionInfoWrapper.UnderlyingObject -> Microsoft.CodeAnalysis.Checksum readonly Microsoft.CodeAnalysis.ExternalAccess.Razor.RazorRemoteCallbackWrapper.UnderlyingObject -> Microsoft.CodeAnalysis.Remote.RemoteCallback readonly Microsoft.CodeAnalysis.ExternalAccess.Razor.RazorServiceDescriptorsWrapper.UnderlyingObject -> Microsoft.CodeAnalysis.Remote.ServiceDescriptors! +static Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost.RazorCohostRequestContext.operator !=(Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost.RazorCohostRequestContext left, Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost.RazorCohostRequestContext right) -> bool +static Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost.RazorCohostRequestContext.operator ==(Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost.RazorCohostRequestContext left, Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost.RazorCohostRequestContext right) -> bool +static Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost.RazorTextDocumentIdentifier.operator !=(Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost.RazorTextDocumentIdentifier left, Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost.RazorTextDocumentIdentifier right) -> bool +static Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost.RazorTextDocumentIdentifier.operator ==(Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost.RazorTextDocumentIdentifier left, Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost.RazorTextDocumentIdentifier right) -> bool static Microsoft.CodeAnalysis.ExternalAccess.Razor.RazorBreakpointSpans.TryGetBreakpointSpan(Microsoft.CodeAnalysis.SyntaxTree! tree, int position, System.Threading.CancellationToken cancellationToken, out Microsoft.CodeAnalysis.Text.TextSpan breakpointSpan) -> bool static Microsoft.CodeAnalysis.ExternalAccess.Razor.RazorClassificationOptionsWrapper.Default -> Microsoft.CodeAnalysis.ExternalAccess.Razor.RazorClassificationOptionsWrapper static Microsoft.CodeAnalysis.ExternalAccess.Razor.RazorClassifierAccessor.GetClassifiedSpansAsync(Microsoft.CodeAnalysis.Document! document, Microsoft.CodeAnalysis.Text.TextSpan textSpan, Microsoft.CodeAnalysis.ExternalAccess.Razor.RazorClassificationOptionsWrapper options, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task!>! @@ -414,6 +479,11 @@ static Microsoft.CodeAnalysis.ExternalAccess.Razor.RazorSemanticTokensAccessor.R static Microsoft.CodeAnalysis.ExternalAccess.Razor.RazorTestAnalyzerLoader.CreateAnalyzerAssemblyLoader() -> Microsoft.CodeAnalysis.IAnalyzerAssemblyLoader! static Microsoft.CodeAnalysis.ExternalAccess.Razor.RazorUri.CreateAbsoluteUri(string! absolutePath) -> System.Uri! static Microsoft.CodeAnalysis.ExternalAccess.Razor.RazorUri.CreateUri(this Microsoft.CodeAnalysis.TextDocument! document) -> System.Uri! +static readonly Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost.Constants.RazorLanguage -> System.Collections.Immutable.ImmutableArray static readonly Microsoft.CodeAnalysis.ExternalAccess.Razor.RazorRemoteServiceCallbackDispatcherRegistry.Empty -> Microsoft.CodeAnalysis.ExternalAccess.Razor.RazorRemoteServiceCallbackDispatcherRegistry! +~override Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost.RazorCohostRequestContext.Equals(object obj) -> bool +~override Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost.RazorCohostRequestContext.ToString() -> string +~override Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost.RazorTextDocumentIdentifier.Equals(object obj) -> bool +~override Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost.RazorTextDocumentIdentifier.ToString() -> string ~override Microsoft.CodeAnalysis.ExternalAccess.Razor.RazorIndentationOptions.Equals(object obj) -> bool ~override Microsoft.CodeAnalysis.ExternalAccess.Razor.RazorIndentationOptions.ToString() -> string diff --git a/src/Tools/ExternalAccess/Razor/Microsoft.CodeAnalysis.ExternalAccess.Razor.csproj b/src/Tools/ExternalAccess/Razor/Microsoft.CodeAnalysis.ExternalAccess.Razor.csproj index 6dc1c93c9f238..b1f95a80d4c85 100644 --- a/src/Tools/ExternalAccess/Razor/Microsoft.CodeAnalysis.ExternalAccess.Razor.csproj +++ b/src/Tools/ExternalAccess/Razor/Microsoft.CodeAnalysis.ExternalAccess.Razor.csproj @@ -10,7 +10,7 @@ Microsoft.CodeAnalysis.ExternalAccess.Razor A supporting package for Razor: - https://github.com/aspnet/AspNetCore-Tooling + https://github.com/dotnet/razor @@ -32,7 +32,6 @@ - @@ -44,6 +43,10 @@ + + + + diff --git a/src/Tools/ExternalAccess/RazorTest/Cohost/RazorCohostTests.cs b/src/Tools/ExternalAccess/RazorTest/Cohost/RazorCohostTests.cs new file mode 100644 index 0000000000000..9fafd66553e07 --- /dev/null +++ b/src/Tools/ExternalAccess/RazorTest/Cohost/RazorCohostTests.cs @@ -0,0 +1,237 @@ +// 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 System.Linq; +using System.Threading; +using System.Threading.Tasks; +using System.Xml.Linq; +using Microsoft.CodeAnalysis.Editor.UnitTests.Workspaces; +using Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost; +using Microsoft.CodeAnalysis.LanguageServer; +using Microsoft.CodeAnalysis.LanguageServer.Handler; +using Microsoft.CodeAnalysis.Test.Utilities; +using Microsoft.CommonLanguageServerProtocol.Framework; +using Microsoft.VisualStudio.LanguageServer.Client; +using Microsoft.VisualStudio.LanguageServer.Protocol; +using Roslyn.Test.Utilities; +using StreamJsonRpc; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.CodeAnalysis.ExternalAccess.Razor.UnitTests; + +public class RazorCohostTests(ITestOutputHelper testOutputHelper) : AbstractLanguageServerProtocolTests(testOutputHelper) +{ + protected override TestComposition Composition => base.Composition + .AddAssemblies(typeof(RazorCohostLanguageClient).Assembly) + .AddParts( + typeof(RazorHandler), + typeof(RazorCohostCapabilitiesProvider), + typeof(RazorCohostLanguageClientActivationService), + typeof(NoOpLspLoggerFactory)); + + [WpfFact] + public async Task TestExternalAccessRazorHandlerInvoked() + { + var workspaceXml = """ + + + + + + """; + var testWorkspace = CreateWorkspace(workspaceXml); + var server = await InitializeLanguageServerAsync(testWorkspace); + + var document = testWorkspace.CurrentSolution.Projects.Single().AdditionalDocuments.Single(); + var request = new TextDocumentPositionParams + { + TextDocument = new VSTextDocumentIdentifier + { + Uri = document.GetURI(), + ProjectContext = new VSProjectContext + { + Id = document.Project.Id.Id.ToString() + } + } + }; + + var response = await server.GetTestAccessor().ExecuteRequestAsync(RazorHandler.MethodName, request, CancellationToken.None); + + Assert.NotNull(response); + Assert.Equal(document.GetURI(), response.DocumentUri); + Assert.Equal(document.Project.Id.Id, response.ProjectId); + } + + [WpfFact] + public async Task TestProjectContextHandler() + { + var workspaceXml = """ + + + + + + """; + var testWorkspace = CreateWorkspace(workspaceXml); + var server = await InitializeLanguageServerAsync(testWorkspace); + + var document = testWorkspace.CurrentSolution.Projects.Single().AdditionalDocuments.Single(); + var request = new VSGetProjectContextsParams + { + TextDocument = new TextDocumentItem + { + Uri = document.GetURI(), + } + }; + + var response = await server.GetTestAccessor().ExecuteRequestAsync(VSMethods.GetProjectContextsName, request, CancellationToken.None); + + Assert.NotNull(response); + var projectContext = Assert.Single(response?.ProjectContexts); + Assert.Equal(ProtocolConversions.ProjectIdToProjectContextId(document.Project.Id), projectContext.Id); + } + + [WpfFact] + public async Task TestDocumentSync() + { + var workspaceXml = """ + + + + + + """; + var testWorkspace = CreateWorkspace(workspaceXml); + var server = await InitializeLanguageServerAsync(testWorkspace); + + var document = testWorkspace.CurrentSolution.Projects.Single().AdditionalDocuments.Single(); + var didOpenRequest = new DidOpenTextDocumentParams + { + TextDocument = new TextDocumentItem + { + Uri = document.GetURI(), + Text = "Original text" + } + }; + + await server.GetTestAccessor().ExecuteRequestAsync(Methods.TextDocumentDidOpenName, didOpenRequest, CancellationToken.None); + + var workspaceManager = server.GetLspServices().GetRequiredService(); + Assert.True(workspaceManager.GetTrackedLspText().TryGetValue(document.GetURI(), out var trackedText)); + Assert.Equal("Original text", trackedText.Text.ToString()); + + var didChangeRequest = new DidChangeTextDocumentParams + { + TextDocument = new VersionedTextDocumentIdentifier + { + Uri = document.GetURI() + }, + ContentChanges = + [ + new TextDocumentContentChangeEvent + { + Range = new VisualStudio.LanguageServer.Protocol.Range + { + Start = new Position(0, 0), + End = new Position(0, 0) + }, + Text = "Not The " + } + ] + }; + + await server.GetTestAccessor().ExecuteRequestAsync(Methods.TextDocumentDidChangeName, didChangeRequest, CancellationToken.None); + + Assert.True(workspaceManager.GetTrackedLspText().TryGetValue(document.GetURI(), out trackedText)); + Assert.Equal("Not The Original text", trackedText.Text.ToString()); + } + + private TestWorkspace CreateWorkspace(string workspaceXml) + { + var testWorkspace = CreateWorkspace(options: null, mutatingLspWorkspace: false, workspaceKind: null); + testWorkspace.InitializeDocuments(XElement.Parse(workspaceXml), openDocuments: false); + return testWorkspace; + } + + private static async Task> InitializeLanguageServerAsync(TestWorkspace testWorkspace) + { + var languageClient = testWorkspace.ExportProvider.GetExportedValues().OfType().Single(); + await languageClient.ActivateAsync(CancellationToken.None); + + var server = languageClient.GetTestAccessor().LanguageServer; + Assert.NotNull(server); + + var serverAccessor = server!.GetTestAccessor(); + + await serverAccessor.ExecuteRequestAsync(Methods.InitializeName, new InitializeParams { Capabilities = new() }, CancellationToken.None); + + return server; + } + + internal class TestRequest(Uri documentUri, Guid projectId) + { + public Uri DocumentUri => documentUri; + public Guid ProjectId => projectId; + } + + [PartNotDiscoverable] + [LanguageServerEndpoint(MethodName)] + [ExportRazorStatelessLspService(typeof(RazorHandler)), Shared] + [method: ImportingConstructor] + [method: Obsolete("This exported object must be obtained through the MEF export provider.", error: true)] + internal class RazorHandler() : AbstractRazorCohostDocumentRequestHandler + { + internal const string MethodName = "testMethod"; + + protected override bool MutatesSolutionState => false; + + protected override bool RequiresLSPSolution => true; + + protected override RazorTextDocumentIdentifier? GetRazorTextDocumentIdentifier(TextDocumentPositionParams request) + { + return new RazorTextDocumentIdentifier(request.TextDocument.Uri, (request.TextDocument as VSTextDocumentIdentifier)?.ProjectContext?.Id); + } + + protected override Task HandleRequestAsync(TextDocumentPositionParams request, RazorCohostRequestContext context, CancellationToken cancellationToken) + { + Assert.NotNull(context.Solution); + AssertEx.NotNull(context.TextDocument); + return Task.FromResult(new TestRequest(context.TextDocument.GetURI(), context.TextDocument.Project.Id.Id)); + } + } + + [PartNotDiscoverable] + [Export(typeof(ILspServiceLoggerFactory)), Shared] + [method: ImportingConstructor] + [method: Obsolete("This exported object must be obtained through the MEF export provider.", error: true)] + private class NoOpLspLoggerFactory() : ILspServiceLoggerFactory + { + public Task CreateLoggerAsync(string serverTypeName, JsonRpc jsonRpc, CancellationToken cancellationToken) + => Task.FromResult((AbstractLspLogger)NoOpLspLogger.Instance); + } + + [PartNotDiscoverable] + [Export(typeof(IRazorCohostCapabilitiesProvider)), Shared] + [method: ImportingConstructor] + [method: Obsolete("This exported object must be obtained through the MEF export provider.", error: true)] + private class RazorCohostCapabilitiesProvider() : IRazorCohostCapabilitiesProvider + { + public string GetCapabilities(string clientCapabilities) + { + return "{ }"; + } + } + + [PartNotDiscoverable] + [Export(typeof(IRazorCohostLanguageClientActivationService)), Shared] + [method: ImportingConstructor] + [method: Obsolete("This exported object must be obtained through the MEF export provider.", error: true)] + private class RazorCohostLanguageClientActivationService() : IRazorCohostLanguageClientActivationService + { + public bool ShouldActivateCohostServer() => true; + } +} diff --git a/src/Tools/ExternalAccess/RazorTest/Microsoft.CodeAnalysis.ExternalAccess.Razor.UnitTests.csproj b/src/Tools/ExternalAccess/RazorTest/Microsoft.CodeAnalysis.ExternalAccess.Razor.UnitTests.csproj index 4f3f41e9705c6..9b95cd9e85d37 100644 --- a/src/Tools/ExternalAccess/RazorTest/Microsoft.CodeAnalysis.ExternalAccess.Razor.UnitTests.csproj +++ b/src/Tools/ExternalAccess/RazorTest/Microsoft.CodeAnalysis.ExternalAccess.Razor.UnitTests.csproj @@ -7,7 +7,12 @@ net472 + + + + + \ No newline at end of file diff --git a/src/Workspaces/Core/Portable/Microsoft.CodeAnalysis.Workspaces.csproj b/src/Workspaces/Core/Portable/Microsoft.CodeAnalysis.Workspaces.csproj index f246af2aa5f88..31f7ed8f9aedb 100644 --- a/src/Workspaces/Core/Portable/Microsoft.CodeAnalysis.Workspaces.csproj +++ b/src/Workspaces/Core/Portable/Microsoft.CodeAnalysis.Workspaces.csproj @@ -125,6 +125,7 @@ +