From 57ba04bc60bb416d6ea8cdb4aeb2a430d07b3801 Mon Sep 17 00:00:00 2001 From: David Wengier Date: Thu, 25 Jul 2024 16:11:17 +1000 Subject: [PATCH 1/2] Expose inlay hints to Razor cohosting --- .../Handler/InlayHint/InlayHintHandler.cs | 18 ++++--- .../InlayHint/InlayHintResolveHandler.cs | 15 ++++-- ...odeAnalysis.LanguageServer.Protocol.csproj | 5 +- .../Razor/Cohost/Handlers/InlayHints.cs | 49 +++++++++++++++++++ ...t.CodeAnalysis.ExternalAccess.Razor.csproj | 1 + .../ExternalAccess/Razor/Api/TestHelpers.cs | 18 +++++++ 6 files changed, 94 insertions(+), 12 deletions(-) create mode 100644 src/Tools/ExternalAccess/Razor/Cohost/Handlers/InlayHints.cs create mode 100644 src/Workspaces/Remote/ServiceHub/ExternalAccess/Razor/Api/TestHelpers.cs diff --git a/src/LanguageServer/Protocol/Handler/InlayHint/InlayHintHandler.cs b/src/LanguageServer/Protocol/Handler/InlayHint/InlayHintHandler.cs index 08025830a4bca..8ce98a031b551 100644 --- a/src/LanguageServer/Protocol/Handler/InlayHint/InlayHintHandler.cs +++ b/src/LanguageServer/Protocol/Handler/InlayHint/InlayHintHandler.cs @@ -38,18 +38,24 @@ public InlayHintHandler(IGlobalOptionService optionsService) public TextDocumentIdentifier GetTextDocumentIdentifier(InlayHintParams request) => request.TextDocument; - public async Task HandleRequestAsync(InlayHintParams request, RequestContext context, CancellationToken cancellationToken) + public Task HandleRequestAsync(InlayHintParams request, RequestContext context, CancellationToken cancellationToken) { var document = context.GetRequiredDocument(); + var inlayHintCache = context.GetRequiredLspService(); + var options = _optionsService.GetInlineHintsOptions(document.Project.Language); + + return GetInlayHintsAsync(document, request.TextDocument, request.Range, options, displayAllOverride: false, inlayHintCache, cancellationToken); + } + + internal static async Task GetInlayHintsAsync(Document document, TextDocumentIdentifier textDocumentIdentifier, LSP.Range range, InlineHintsOptions options, bool displayAllOverride, InlayHintCache inlayHintCache, CancellationToken cancellationToken) + { var text = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); - var textSpan = ProtocolConversions.RangeToTextSpan(request.Range, text); + var textSpan = ProtocolConversions.RangeToTextSpan(range, text); var inlineHintService = document.GetRequiredLanguageService(); - var options = _optionsService.GetInlineHintsOptions(document.Project.Language); - var hints = await inlineHintService.GetInlineHintsAsync(document, textSpan, options, displayAllOverride: false, cancellationToken).ConfigureAwait(false); + var hints = await inlineHintService.GetInlineHintsAsync(document, textSpan, options, displayAllOverride, cancellationToken).ConfigureAwait(false); var syntaxVersion = await document.GetSyntaxVersionAsync(cancellationToken).ConfigureAwait(false); - var inlayHintCache = context.GetRequiredLspService(); // Store the members in the resolve cache so that when we get a resolve request for a particular // member we can re-use the inline hint. @@ -83,7 +89,7 @@ public TextDocumentIdentifier GetTextDocumentIdentifier(InlayHintParams request) ToolTip = null, PaddingLeft = leftPadding, PaddingRight = rightPadding, - Data = new InlayHintResolveData(resultId, i, request.TextDocument) + Data = new InlayHintResolveData(resultId, i, textDocumentIdentifier) }; inlayHints[i] = inlayHint; diff --git a/src/LanguageServer/Protocol/Handler/InlayHint/InlayHintResolveHandler.cs b/src/LanguageServer/Protocol/Handler/InlayHint/InlayHintResolveHandler.cs index 122bdcc25e622..468e908a507e6 100644 --- a/src/LanguageServer/Protocol/Handler/InlayHint/InlayHintResolveHandler.cs +++ b/src/LanguageServer/Protocol/Handler/InlayHint/InlayHintResolveHandler.cs @@ -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.Text.Json; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.InlineHints; @@ -9,7 +10,6 @@ using Roslyn.Utilities; using StreamJsonRpc; using LSP = Roslyn.LanguageServer.Protocol; -using System.Text.Json; namespace Microsoft.CodeAnalysis.LanguageServer.Handler.InlayHint { @@ -30,11 +30,16 @@ public InlayHintResolveHandler(InlayHintCache inlayHintCache) public TextDocumentIdentifier GetTextDocumentIdentifier(LSP.InlayHint request) => GetInlayHintResolveData(request).TextDocument; - public async Task HandleRequestAsync(LSP.InlayHint request, RequestContext context, CancellationToken cancellationToken) + public Task HandleRequestAsync(LSP.InlayHint request, RequestContext context, CancellationToken cancellationToken) { var document = context.GetRequiredDocument(); + return ResolveInlayHintAsync(document, request, _inlayHintCache, cancellationToken); + } + + internal static async Task ResolveInlayHintAsync(Document document, LSP.InlayHint request, InlayHintCache inlayHintCache, CancellationToken cancellationToken) + { var resolveData = GetInlayHintResolveData(request); - var (cacheEntry, inlineHintToResolve) = GetCacheEntry(resolveData); + var (cacheEntry, inlineHintToResolve) = GetCacheEntry(resolveData, inlayHintCache); var currentSyntaxVersion = await document.GetSyntaxVersionAsync(cancellationToken).ConfigureAwait(false); var cachedSyntaxVersion = cacheEntry.SyntaxVersion; @@ -53,9 +58,9 @@ public TextDocumentIdentifier GetTextDocumentIdentifier(LSP.InlayHint request) return request; } - private (InlayHintCache.InlayHintCacheEntry CacheEntry, InlineHint InlineHintToResolve) GetCacheEntry(InlayHintResolveData resolveData) + private static (InlayHintCache.InlayHintCacheEntry CacheEntry, InlineHint InlineHintToResolve) GetCacheEntry(InlayHintResolveData resolveData, InlayHintCache inlayHintCache) { - var cacheEntry = _inlayHintCache.GetCachedEntry(resolveData.ResultId); + var cacheEntry = inlayHintCache.GetCachedEntry(resolveData.ResultId); Contract.ThrowIfNull(cacheEntry, "Missing cache entry for inlay hint resolve request"); return (cacheEntry, cacheEntry.InlayHintMembers[resolveData.ListIndex]); } diff --git a/src/LanguageServer/Protocol/Microsoft.CodeAnalysis.LanguageServer.Protocol.csproj b/src/LanguageServer/Protocol/Microsoft.CodeAnalysis.LanguageServer.Protocol.csproj index c85748a90fe20..73945f4ec700e 100644 --- a/src/LanguageServer/Protocol/Microsoft.CodeAnalysis.LanguageServer.Protocol.csproj +++ b/src/LanguageServer/Protocol/Microsoft.CodeAnalysis.LanguageServer.Protocol.csproj @@ -37,9 +37,12 @@ - + + + + diff --git a/src/Tools/ExternalAccess/Razor/Cohost/Handlers/InlayHints.cs b/src/Tools/ExternalAccess/Razor/Cohost/Handlers/InlayHints.cs new file mode 100644 index 0000000000000..1c7fe47a2ea68 --- /dev/null +++ b/src/Tools/ExternalAccess/Razor/Cohost/Handlers/InlayHints.cs @@ -0,0 +1,49 @@ +// 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.InlineHints; +using Microsoft.CodeAnalysis.LanguageServer.Handler.InlayHint; +using Roslyn.LanguageServer.Protocol; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost.Handlers +{ + internal static class InlayHints + { + // In the Roslyn LSP server this cache has the same lifetime as the LSP server. For Razor, running OOP, we don't have + // that same lifetime anywhere, everything is just static. This is likely not ideal, but the inlay hint cache has a + // max size of 3 items, so it's not a huge deal. + private static InlayHintCache? s_resolveCache; + + public static Task GetInlayHintsAsync(Document document, TextDocumentIdentifier textDocumentIdentifier, Range range, bool displayAllOverride, CancellationToken cancellationToken) + { + s_resolveCache ??= new(); + + // Currently Roslyn options don't sync to OOP so trying to get the real options out of IGlobalOptionsService will + // always just result in the defaults, which for inline hints are to not show anything. However, the editor has a + // setting for LSP inlay hints, so we can assume that if we get a request from the client, the user wants hints. + // When overriding however, Roslyn does a nicer job if type hints are off. + var options = InlineHintsOptions.Default; + if (!displayAllOverride) + { + options = options with + { + TypeOptions = options.TypeOptions with { EnabledForTypes = true }, + ParameterOptions = options.ParameterOptions with { EnabledForParameters = true }, + }; + } + + return InlayHintHandler.GetInlayHintsAsync(document, textDocumentIdentifier, range, options, displayAllOverride, s_resolveCache, cancellationToken); + } + + public static Task ResolveInlayHintAsync(Document document, InlayHint request, CancellationToken cancellationToken) + { + Contract.ThrowIfNull(s_resolveCache, "Cache should never be null for resolve, since it should have been created by the original request"); + + return InlayHintResolveHandler.ResolveInlayHintAsync(document, request, s_resolveCache, cancellationToken); + } + } +} diff --git a/src/Tools/ExternalAccess/Razor/Microsoft.CodeAnalysis.ExternalAccess.Razor.csproj b/src/Tools/ExternalAccess/Razor/Microsoft.CodeAnalysis.ExternalAccess.Razor.csproj index 41dd9af02dfb1..6164b50074dd8 100644 --- a/src/Tools/ExternalAccess/Razor/Microsoft.CodeAnalysis.ExternalAccess.Razor.csproj +++ b/src/Tools/ExternalAccess/Razor/Microsoft.CodeAnalysis.ExternalAccess.Razor.csproj @@ -31,6 +31,7 @@ + diff --git a/src/Workspaces/Remote/ServiceHub/ExternalAccess/Razor/Api/TestHelpers.cs b/src/Workspaces/Remote/ServiceHub/ExternalAccess/Razor/Api/TestHelpers.cs new file mode 100644 index 0000000000000..beb16c57e5c9a --- /dev/null +++ b/src/Workspaces/Remote/ServiceHub/ExternalAccess/Razor/Api/TestHelpers.cs @@ -0,0 +1,18 @@ +// 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.Remote; + +namespace Microsoft.CodeAnalysis.ExternalAccess.Razor.Api +{ + internal static class TestHelpers + { + /// + /// Allows tests to create a new remote workspace, rather than use RemoteWorkspaceManager.Default, so things can be isolated + /// + public static Workspace CreateTestWorkspace() + => new RemoteWorkspaceManager(workspace => new SolutionAssetCache(workspace, cleanupInterval: TimeSpan.FromSeconds(30), purgeAfter: TimeSpan.FromMinutes(1))).GetWorkspace(); + } +} From e6daea2fcf539b0d98c8d1b6e3920a51917d2268 Mon Sep 17 00:00:00 2001 From: David Wengier Date: Mon, 29 Jul 2024 07:55:57 +1000 Subject: [PATCH 2/2] Remove file --- .../ExternalAccess/Razor/Api/TestHelpers.cs | 18 ------------------ 1 file changed, 18 deletions(-) delete mode 100644 src/Workspaces/Remote/ServiceHub/ExternalAccess/Razor/Api/TestHelpers.cs diff --git a/src/Workspaces/Remote/ServiceHub/ExternalAccess/Razor/Api/TestHelpers.cs b/src/Workspaces/Remote/ServiceHub/ExternalAccess/Razor/Api/TestHelpers.cs deleted file mode 100644 index beb16c57e5c9a..0000000000000 --- a/src/Workspaces/Remote/ServiceHub/ExternalAccess/Razor/Api/TestHelpers.cs +++ /dev/null @@ -1,18 +0,0 @@ -// 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.Remote; - -namespace Microsoft.CodeAnalysis.ExternalAccess.Razor.Api -{ - internal static class TestHelpers - { - /// - /// Allows tests to create a new remote workspace, rather than use RemoteWorkspaceManager.Default, so things can be isolated - /// - public static Workspace CreateTestWorkspace() - => new RemoteWorkspaceManager(workspace => new SolutionAssetCache(workspace, cleanupInterval: TimeSpan.FromSeconds(30), purgeAfter: TimeSpan.FromMinutes(1))).GetWorkspace(); - } -}