-
Notifications
You must be signed in to change notification settings - Fork 199
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
Cohost inlay hint support #10672
Cohost inlay hint support #10672
Changes from 8 commits
5191c83
6e4cf07
2437736
f179fb9
3a21f34
51756c1
ed53daf
0fe459e
08f7362
02854ec
227b0c9
2a06fcc
240a123
e4f1677
788988b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
using System; | ||
using Microsoft.CodeAnalysis.Text; | ||
|
||
namespace Microsoft.AspNetCore.Razor.Language; | ||
|
||
internal static class IRazorGeneratedDocumentExtensions | ||
{ | ||
public static SourceText GetGeneratedSourceText(this IRazorGeneratedDocument generatedDocument) | ||
{ | ||
if (generatedDocument.CodeDocument is not { } codeDocument) | ||
{ | ||
throw new InvalidOperationException("Cannot use document mapping service on a generated document that has a null CodeDocument."); | ||
} | ||
|
||
return codeDocument.GetGeneratedSourceText(generatedDocument); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
// Copyright (c) .NET Foundation. All rights reserved. | ||
// Licensed under the MIT license. See License.txt in the project root for license information. | ||
|
||
using Microsoft.AspNetCore.Razor; | ||
using Microsoft.CodeAnalysis.Text; | ||
|
||
namespace Roslyn.LanguageServer.Protocol; | ||
|
||
internal static partial class RoslynLspExtensions | ||
{ | ||
public static Range GetRange(this SourceText text, TextSpan span) | ||
=> text.GetLinePositionSpan(span).ToRange(); | ||
|
||
public static TextSpan GetTextSpan(this SourceText text, Range range) | ||
=> text.GetTextSpan(range.Start.Line, range.Start.Character, range.End.Line, range.End.Character); | ||
|
||
public static TextChange GetTextChange(this SourceText text, TextEdit edit) | ||
=> new(text.GetTextSpan(edit.Range), edit.NewText); | ||
|
||
public static TextEdit GetTextEdit(this SourceText text, TextChange change) | ||
=> RoslynLspFactory.CreateTextEdit(text.GetRange(change.Span), change.NewText.AssumeNotNull()); | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
// Copyright (c) .NET Foundation. All rights reserved. | ||
// Licensed under the MIT license. See License.txt in the project root for license information. | ||
|
||
using Roslyn.LanguageServer.Protocol; | ||
|
||
namespace Microsoft.CodeAnalysis.Razor.Protocol.InlayHints; | ||
|
||
internal record class InlayHintDataWrapper(TextDocumentIdentifier TextDocument, object? OriginalData, Position OriginalPosition); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
// Copyright (c) .NET Foundation. All rights reserved. | ||
// Licensed under the MIT license. See License.txt in the project root for license information. | ||
|
||
using System.Threading; | ||
using System.Threading.Tasks; | ||
using Microsoft.CodeAnalysis.ExternalAccess.Razor; | ||
using Roslyn.LanguageServer.Protocol; | ||
|
||
namespace Microsoft.CodeAnalysis.Razor.Remote; | ||
|
||
internal interface IRemoteInlayHintService : IRemoteJsonService | ||
{ | ||
ValueTask<InlayHint[]?> GetInlayHintsAsync(JsonSerializableRazorPinnedSolutionInfoWrapper solutionInfo, JsonSerializableDocumentId razorDocumentId, InlayHintParams inlayHintParams, bool displayAllOverride, CancellationToken cancellationToken); | ||
|
||
ValueTask<InlayHint> ResolveHintAsync(JsonSerializableRazorPinnedSolutionInfoWrapper solutionInfo, JsonSerializableDocumentId razorDocumentId, InlayHint inlayHint, CancellationToken cancellationToken); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,115 @@ | ||
// Copyright (c) .NET Foundation. All rights reserved. | ||
// Licensed under the MIT license. See License.txt in the project root for license information. | ||
|
||
using System.Linq; | ||
using System.Threading; | ||
using System.Threading.Tasks; | ||
using Microsoft.AspNetCore.Razor.Language; | ||
using Microsoft.AspNetCore.Razor.Language.Syntax; | ||
using Microsoft.AspNetCore.Razor.PooledObjects; | ||
using Microsoft.CodeAnalysis.ExternalAccess.Razor; | ||
using Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost.Handlers; | ||
Check failure on line 11 in src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/InlayHints/RemoteInlayHintService.cs
|
||
using Microsoft.CodeAnalysis.Razor.DocumentMapping; | ||
using Microsoft.CodeAnalysis.Razor.Protocol.InlayHints; | ||
using Microsoft.CodeAnalysis.Razor.Remote; | ||
using Microsoft.CodeAnalysis.Razor.Workspaces; | ||
using Microsoft.CodeAnalysis.Remote.Razor.ProjectSystem; | ||
using Microsoft.CodeAnalysis.Text; | ||
using Roslyn.LanguageServer.Protocol; | ||
|
||
namespace Microsoft.CodeAnalysis.Remote.Razor; | ||
|
||
internal sealed partial class RemoteInlayHintService(in ServiceArgs args) : RazorDocumentServiceBase(in args), IRemoteInlayHintService | ||
{ | ||
internal sealed class Factory : FactoryBase<IRemoteInlayHintService> | ||
{ | ||
protected override IRemoteInlayHintService CreateService(in ServiceArgs args) | ||
=> new RemoteInlayHintService(in args); | ||
} | ||
|
||
private readonly IRazorDocumentMappingService _documentMappingService = args.ExportProvider.GetExportedValue<IRazorDocumentMappingService>(); | ||
private readonly IFilePathService _filePathService = args.ExportProvider.GetExportedValue<IFilePathService>(); | ||
|
||
public ValueTask<InlayHint[]?> GetInlayHintsAsync(JsonSerializableRazorPinnedSolutionInfoWrapper solutionInfo, JsonSerializableDocumentId razorDocumentId, InlayHintParams inlayHintParams, bool displayAllOverride, CancellationToken cancellationToken) | ||
=> RunServiceAsync( | ||
solutionInfo, | ||
razorDocumentId, | ||
context => GetInlayHintsAsync(context, inlayHintParams, displayAllOverride, cancellationToken), | ||
cancellationToken); | ||
|
||
private async ValueTask<InlayHint[]?> GetInlayHintsAsync(RemoteDocumentContext context, InlayHintParams inlayHintParams, bool displayAllOverride, CancellationToken cancellationToken) | ||
{ | ||
var codeDocument = await context.GetCodeDocumentAsync(cancellationToken).ConfigureAwait(false); | ||
var csharpDocument = codeDocument.GetCSharpDocument(); | ||
|
||
var span = inlayHintParams.Range.ToLinePositionSpan(); | ||
|
||
// We are given a range by the client, but our mapping only succeeds if the start and end of the range can both be mapped | ||
// to C#. Since that doesn't logically match what we want from inlay hints, we instead get the minimum range of mappable | ||
// C# to get hints for. We'll filter that later, to remove the sections that can't be mapped back. | ||
if (!_documentMappingService.TryMapToGeneratedDocumentRange(csharpDocument, span, out var projectedLinePositionSpan) && | ||
!codeDocument.TryGetMinimalCSharpRange(span, out projectedLinePositionSpan)) | ||
{ | ||
// There's no C# in the range. | ||
return null; | ||
} | ||
|
||
var generatedDocument = await context.GetGeneratedDocumentAsync(_filePathService, cancellationToken).ConfigureAwait(false); | ||
|
||
var textDocument = inlayHintParams.TextDocument.WithUri(generatedDocument.CreateUri()); | ||
var range = projectedLinePositionSpan.ToRange(); | ||
|
||
var hints = await InlayHints.GetInlayHintsAsync(generatedDocument, textDocument, range, displayAllOverride, cancellationToken).ConfigureAwait(false); | ||
Check failure on line 62 in src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/InlayHints/RemoteInlayHintService.cs
|
||
|
||
if (hints is null) | ||
{ | ||
return null; | ||
} | ||
|
||
using var inlayHintsBuilder = new PooledArrayBuilder<InlayHint>(); | ||
var razorSourceText = codeDocument.Source.Text; | ||
var csharpSourceText = codeDocument.GetCSharpSourceText(); | ||
var syntaxTree = codeDocument.GetSyntaxTree(); | ||
foreach (var hint in hints) | ||
{ | ||
if (csharpSourceText.TryGetAbsoluteIndex(hint.Position.ToLinePosition(), out var absoluteIndex) && | ||
_documentMappingService.TryMapToHostDocumentPosition(csharpDocument, absoluteIndex, out var hostDocumentPosition, out var hostDocumentIndex)) | ||
{ | ||
// We know this C# maps to Razor, but does it map to Razor that we like? | ||
var node = syntaxTree.Root.FindInnermostNode(hostDocumentIndex); | ||
if (node?.FirstAncestorOrSelf<MarkupTagHelperAttributeValueSyntax>() is not null) | ||
{ | ||
continue; | ||
} | ||
|
||
if (hint.TextEdits is not null) | ||
{ | ||
var changes = hint.TextEdits.Select(csharpSourceText.GetTextChange); | ||
var mappedChanges = _documentMappingService.GetHostDocumentEdits(csharpDocument, changes); | ||
hint.TextEdits = mappedChanges.Select(razorSourceText.GetTextEdit).ToArray(); | ||
} | ||
|
||
hint.Data = new InlayHintDataWrapper(inlayHintParams.TextDocument, hint.Data, hint.Position); | ||
hint.Position = hostDocumentPosition.ToPosition(); | ||
|
||
inlayHintsBuilder.Add(hint); | ||
Check failure on line 95 in src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/InlayHints/RemoteInlayHintService.cs
|
||
} | ||
} | ||
|
||
return inlayHintsBuilder.ToArray(); | ||
} | ||
|
||
public ValueTask<InlayHint> ResolveHintAsync(JsonSerializableRazorPinnedSolutionInfoWrapper solutionInfo, JsonSerializableDocumentId razorDocumentId, InlayHint inlayHint, CancellationToken cancellationToken) | ||
=> RunServiceAsync( | ||
solutionInfo, | ||
razorDocumentId, | ||
context => ResolveInlayHintAsync(context, inlayHint, cancellationToken), | ||
cancellationToken); | ||
|
||
private async ValueTask<InlayHint> ResolveInlayHintAsync(RemoteDocumentContext context, InlayHint inlayHint, CancellationToken cancellationToken) | ||
{ | ||
var generatedDocument = await context.GetGeneratedDocumentAsync(_filePathService, cancellationToken).ConfigureAwait(false); | ||
|
||
return await InlayHints.ResolveInlayHintAsync(generatedDocument, inlayHint, cancellationToken).ConfigureAwait(false); | ||
Check failure on line 113 in src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/InlayHints/RemoteInlayHintService.cs
|
||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Annoyingly, this bit of code is pretty much the only logic that could be shared with the existing inlay hints code, because everything else operates on VS LSP types, and here we're using Roslyn LSP types. The code duplication isn't great, but I'm somewhat okay with it because we have good tests for both.
Perhaps we can re-review if/when we can move the language server to Roslyn LSP types, which could be as early as preview 2.