Skip to content

Commit 18d80b9

Browse files
authored
Improve debugging tooltip support (#11877)
Implement textdocument/_vs_dataTipRange lsp message which allows the vs debugger to better handle showing tooltips for the returned ranges. This is my first PR into razor since joining the team, so I'm expecting/hoping for lots of feedback about these changes as I'm still just getting a feel for the codebase. This change is fairly simple, adding a new endpoint for _vs_dataTipRange, and forwarding the request to the csharp lsp implementation. AbstractRazorDelegatingEndpoint is extended to hook into the existing delegating infrastructure. Fixes #6688, https://devdiv.visualstudio.com/DevDiv/_workitems/edit/2459195
1 parent 295d4f2 commit 18d80b9

File tree

7 files changed

+269
-2
lines changed

7 files changed

+269
-2
lines changed
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the MIT license. See License.txt in the project root for license information.
3+
4+
using System.Threading;
5+
using System.Threading.Tasks;
6+
using Microsoft.AspNetCore.Razor.Language;
7+
using Microsoft.AspNetCore.Razor.LanguageServer.EndpointContracts;
8+
using Microsoft.AspNetCore.Razor.LanguageServer.Hosting;
9+
using Microsoft.AspNetCore.Razor.Threading;
10+
using Microsoft.CodeAnalysis.Razor.DocumentMapping;
11+
using Microsoft.CodeAnalysis.Razor.Logging;
12+
using Microsoft.CodeAnalysis.Razor.Protocol;
13+
using Microsoft.CodeAnalysis.Razor.Workspaces;
14+
15+
namespace Microsoft.AspNetCore.Razor.LanguageServer.Debugging;
16+
17+
[RazorLanguageServerEndpoint(VSInternalMethods.TextDocumentDataTipRangeName)]
18+
internal sealed class DataTipRangeHandlerEndpoint(
19+
IDocumentMappingService documentMappingService,
20+
LanguageServerFeatureOptions languageServerFeatureOptions,
21+
IClientConnection clientConnection,
22+
ILoggerFactory loggerFactory)
23+
: AbstractRazorDelegatingEndpoint<TextDocumentPositionParams, VSInternalDataTip?>(
24+
languageServerFeatureOptions,
25+
documentMappingService,
26+
clientConnection,
27+
loggerFactory.GetOrCreateLogger<DataTipRangeHandlerEndpoint>()), ICapabilitiesProvider
28+
{
29+
protected override bool OnlySingleServer => false;
30+
31+
protected override string CustomMessageTarget => CustomMessageNames.RazorDataTipRangeName;
32+
33+
public void ApplyCapabilities(VSInternalServerCapabilities serverCapabilities, VSInternalClientCapabilities clientCapabilities)
34+
{
35+
serverCapabilities.EnableDataTipRangeProvider();
36+
}
37+
38+
protected override Task<IDelegatedParams?> CreateDelegatedParamsAsync(TextDocumentPositionParams request, RazorRequestContext requestContext, DocumentPositionInfo positionInfo, CancellationToken cancellationToken)
39+
{
40+
// only C# supports breakpoints
41+
if (positionInfo.LanguageKind != RazorLanguageKind.CSharp)
42+
{
43+
return SpecializedTasks.Null<IDelegatedParams>();
44+
}
45+
46+
var documentContext = requestContext.DocumentContext;
47+
if (documentContext is null)
48+
{
49+
return SpecializedTasks.Null<IDelegatedParams>();
50+
}
51+
52+
return Task.FromResult<IDelegatedParams?>(new DelegatedPositionParams(
53+
documentContext.GetTextDocumentIdentifierAndVersion(),
54+
positionInfo.Position,
55+
positionInfo.LanguageKind));
56+
}
57+
58+
protected override async Task<VSInternalDataTip?> HandleDelegatedResponseAsync(VSInternalDataTip? delegatedResponse, TextDocumentPositionParams originalRequest, RazorRequestContext requestContext, DocumentPositionInfo positionInfo, CancellationToken cancellationToken)
59+
{
60+
if (delegatedResponse is null)
61+
{
62+
return null;
63+
}
64+
65+
var documentContext = requestContext.DocumentContext;
66+
if (documentContext is null)
67+
{
68+
return null;
69+
}
70+
71+
var codeDocument = await documentContext.GetCodeDocumentAsync(cancellationToken).ConfigureAwait(false);
72+
var csharpDocument = codeDocument.GetCSharpDocument();
73+
74+
if (!DocumentMappingService.TryMapToHostDocumentRange(csharpDocument, delegatedResponse.HoverRange, out var hoverRange))
75+
{
76+
return null;
77+
}
78+
79+
LspRange? expressionRange = null;
80+
if (delegatedResponse.ExpressionRange != null && !DocumentMappingService.TryMapToHostDocumentRange(csharpDocument, delegatedResponse.ExpressionRange, out expressionRange))
81+
{
82+
return null;
83+
}
84+
85+
return new VSInternalDataTip()
86+
{
87+
HoverRange = hoverRange,
88+
ExpressionRange = expressionRange,
89+
DataTipTags = delegatedResponse.DataTipTags,
90+
};
91+
}
92+
}

src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorLanguageServer.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
using Microsoft.AspNetCore.Razor.LanguageServer.Extensions;
1515
using Microsoft.AspNetCore.Razor.LanguageServer.FindAllReferences;
1616
using Microsoft.AspNetCore.Razor.LanguageServer.Folding;
17-
using Microsoft.AspNetCore.Razor.LanguageServer.Formatting;
1817
using Microsoft.AspNetCore.Razor.LanguageServer.Hosting;
1918
using Microsoft.AspNetCore.Razor.LanguageServer.Implementation;
2019
using Microsoft.AspNetCore.Razor.LanguageServer.InlayHints;
@@ -26,7 +25,6 @@
2625
using Microsoft.AspNetCore.Razor.LanguageServer.WrapWithTag;
2726
using Microsoft.CodeAnalysis.Razor.AutoInsert;
2827
using Microsoft.CodeAnalysis.Razor.FoldingRanges;
29-
using Microsoft.CodeAnalysis.Razor.Formatting;
3028
using Microsoft.CodeAnalysis.Razor.GoToDefinition;
3129
using Microsoft.CodeAnalysis.Razor.Logging;
3230
using Microsoft.CodeAnalysis.Razor.Protocol.DocumentSymbols;
@@ -213,6 +211,7 @@ static void AddHandlers(IServiceCollection services)
213211
services.AddHandlerWithCapabilities<ValidateBreakpointRangeEndpoint>();
214212
services.AddHandler<RazorBreakpointSpanEndpoint>();
215213
services.AddHandler<RazorProximityExpressionsEndpoint>();
214+
services.AddHandlerWithCapabilities<DataTipRangeHandlerEndpoint>();
216215

217216
services.AddHandler<WrapWithTagEndpoint>();
218217

src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Protocol/CustomMessageNames.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ internal static class CustomMessageNames
1515
// VS Windows only
1616
public const string RazorInlineCompletionEndpoint = "razor/inlineCompletion";
1717
public const string RazorValidateBreakpointRangeName = "razor/validateBreakpointRange";
18+
public const string RazorDataTipRangeName = "razor/dataTipRange";
1819
public const string RazorOnAutoInsertEndpointName = "razor/onAutoInsert";
1920
public const string RazorSemanticTokensRefreshEndpoint = "razor/semanticTokensRefresh";
2021
public const string RazorTextPresentationEndpoint = "razor/textPresentation";

src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Protocol/LspInitializationHelpers.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,11 @@ public static void EnableValidateBreakpointRange(this VSInternalServerCapabiliti
8282
serverCapabilities.BreakableRangeProvider = true;
8383
}
8484

85+
public static void EnableDataTipRangeProvider(this VSInternalServerCapabilities serverCapabilities)
86+
{
87+
serverCapabilities.DataTipRangeProvider = true;
88+
}
89+
8590
public static void EnableMapCodeProvider(this VSInternalServerCapabilities serverCapabilities)
8691
{
8792
serverCapabilities.MapCodeProvider = true;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the MIT license. See License.txt in the project root for license information.
3+
4+
using System.Threading;
5+
using System.Threading.Tasks;
6+
using Microsoft.CodeAnalysis.Razor.Protocol;
7+
using StreamJsonRpc;
8+
9+
namespace Microsoft.VisualStudio.Razor.LanguageClient.Endpoints;
10+
11+
internal partial class RazorCustomMessageTarget
12+
{
13+
[JsonRpcMethod(CustomMessageNames.RazorDataTipRangeName, UseSingleObjectParameterDeserialization = true)]
14+
public async Task<VSInternalDataTip?> DataTipRangeAsync(DelegatedPositionParams request, CancellationToken cancellationToken)
15+
{
16+
var delegationDetails = await GetProjectedRequestDetailsAsync(request, cancellationToken).ConfigureAwait(false);
17+
if (delegationDetails is null)
18+
{
19+
return default;
20+
}
21+
22+
var dataTipRangeParams = new TextDocumentPositionParams
23+
{
24+
TextDocument = request.Identifier.TextDocumentIdentifier.WithUri(delegationDetails.Value.ProjectedUri),
25+
Position = request.ProjectedPosition,
26+
};
27+
28+
var response = await _requestInvoker.ReinvokeRequestOnServerAsync<TextDocumentPositionParams, VSInternalDataTip?>(
29+
delegationDetails.Value.TextBuffer,
30+
VSInternalMethods.TextDocumentDataTipRangeName,
31+
delegationDetails.Value.LanguageServerName,
32+
dataTipRangeParams,
33+
cancellationToken).ConfigureAwait(false);
34+
35+
return response?.Response;
36+
}
37+
}
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the MIT license. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Collections.Immutable;
6+
using System.Threading.Tasks;
7+
using Microsoft.AspNetCore.Razor.Language;
8+
using Microsoft.AspNetCore.Razor.LanguageServer.ProjectSystem;
9+
using Microsoft.CodeAnalysis.Testing;
10+
using Microsoft.CodeAnalysis.Text;
11+
using Xunit;
12+
using Xunit.Abstractions;
13+
14+
namespace Microsoft.AspNetCore.Razor.LanguageServer.Debugging;
15+
16+
public sealed class DataTipRangeHandlerEndpointTest(ITestOutputHelper testOutput) : SingleServerDelegatingEndpointTestBase(testOutput)
17+
{
18+
[Fact]
19+
public async Task Handle_CSharpInHtml_DataTipRange_FirstExpression()
20+
{
21+
var input = """
22+
@{
23+
{|expression:{|hover:a$$aa|}|}.bbb.ccc;
24+
}
25+
""";
26+
27+
await VerifyDataTipRangeAsync(input);
28+
}
29+
30+
[Fact]
31+
public async Task Handle_CSharpInHtml_DataTipRange_SecondExpression()
32+
{
33+
var input = """
34+
@{
35+
{|expression:{|hover:aaa.b$$bb|}|}.ccc;
36+
}
37+
""";
38+
39+
await VerifyDataTipRangeAsync(input);
40+
}
41+
42+
[Fact]
43+
public async Task Handle_CSharpInHtml_DataTipRange_LastExpression()
44+
{
45+
var input = """
46+
@{
47+
{|expression:{|hover:aaa.bbb.c$$cc|}|};
48+
}
49+
""";
50+
51+
await VerifyDataTipRangeAsync(input);
52+
}
53+
54+
[Fact]
55+
public async Task Handle_CSharpInHtml_DataTipRange_LinqExpression()
56+
{
57+
var input = """
58+
@using System.Linq;
59+
60+
@{
61+
int[] args;
62+
var v = {|expression:{|hover:args.Se$$lect|}(a => a.ToString())|}.Where(a => a.Length >= 0);
63+
}
64+
""";
65+
66+
await VerifyDataTipRangeAsync(input, VSInternalDataTipTags.LinqExpression);
67+
}
68+
69+
private async Task VerifyDataTipRangeAsync(string input, VSInternalDataTipTags dataTipTags = 0)
70+
{
71+
// Arrange
72+
TestFileMarkupParser.GetPositionAndSpans(input, out var output, out int position, out ImmutableDictionary<string, ImmutableArray<TextSpan>> spans);
73+
74+
Assert.True(spans.TryGetValue("expression", out var expressionSpans), "Test authoring failure: Expected at least one span named 'expression'.");
75+
Assert.True(expressionSpans.Length == 1, "Test authoring failure: Expected only one 'expression' span.");
76+
Assert.True(spans.TryGetValue("hover", out var hoverSpans), "Test authoring failure: Expected at least one span named 'hover'.");
77+
Assert.True(hoverSpans.Length == 1, "Test authoring failure: Expected only one 'hover' span.");
78+
79+
var codeDocument = CreateCodeDocument(output);
80+
var razorFilePath = "C:/path/to/file.razor";
81+
82+
// Act
83+
var result = await GetDataTipRangeAsync(codeDocument, razorFilePath, position);
84+
85+
// Assert
86+
var expectedExpressionRange = codeDocument.Source.Text.GetRange(expressionSpans[0]);
87+
Assert.Equal(expectedExpressionRange, result!.ExpressionRange);
88+
89+
var expectedHoverRange = codeDocument.Source.Text.GetRange(hoverSpans[0]);
90+
Assert.Equal(expectedHoverRange, result!.HoverRange);
91+
92+
Assert.Equal(dataTipTags, result!.DataTipTags);
93+
}
94+
95+
private async Task<VSInternalDataTip?> GetDataTipRangeAsync(RazorCodeDocument codeDocument, string razorFilePath, int position)
96+
{
97+
await using var languageServer = await CreateLanguageServerAsync(codeDocument, razorFilePath);
98+
99+
var endpoint = new DataTipRangeHandlerEndpoint(DocumentMappingService, LanguageServerFeatureOptions, languageServer, LoggerFactory);
100+
101+
var request = new TextDocumentPositionParams
102+
{
103+
TextDocument = new TextDocumentIdentifier
104+
{
105+
Uri = new Uri(razorFilePath)
106+
},
107+
Position = codeDocument.Source.Text.GetPosition(position)
108+
};
109+
110+
Assert.True(DocumentContextFactory.TryCreate(request.TextDocument, out var documentContext));
111+
var requestContext = CreateRazorRequestContext(documentContext, LspServices.Empty);
112+
113+
return await endpoint.HandleRequestAsync(request, requestContext, DisposalToken);
114+
}
115+
}

src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/SingleServerDelegatingEndpointTestBase.TestLanguageServer.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ public async Task<TResponse> SendRequestAsync<TParams, TResponse>(string method,
6363
CustomMessageNames.RazorRenameEndpointName => await HandleRenameAsync(@params, cancellationToken),
6464
CustomMessageNames.RazorOnAutoInsertEndpointName => await HandleOnAutoInsertAsync(@params, cancellationToken),
6565
CustomMessageNames.RazorValidateBreakpointRangeName => await HandleValidateBreakpointRangeAsync(@params, cancellationToken),
66+
CustomMessageNames.RazorDataTipRangeName => await HandleDataTipRangeAsync(@params, cancellationToken),
6667
CustomMessageNames.RazorReferencesEndpointName => await HandleReferencesAsync(@params, cancellationToken),
6768
CustomMessageNames.RazorProvideCodeActionsEndpoint => await HandleProvideCodeActionsAsync(@params, cancellationToken),
6869
CustomMessageNames.RazorResolveCodeActionsEndpoint => await HandleResolveCodeActionsAsync(@params, cancellationToken),
@@ -383,5 +384,22 @@ private Task<LspRange> HandleValidateBreakpointRangeAsync<T>(T @params, Cancella
383384
return _csharpServer.ExecuteRequestAsync<VSInternalValidateBreakableRangeParams, LspRange>(
384385
VSInternalMethods.TextDocumentValidateBreakableRangeName, delegatedRequest, cancellationToken);
385386
}
387+
388+
private Task<VSInternalDataTip> HandleDataTipRangeAsync<T>(T @params, CancellationToken cancellationToken)
389+
{
390+
var delegatedParams = Assert.IsType<DelegatedPositionParams>(@params);
391+
var delegatedRequest = new TextDocumentPositionParams()
392+
{
393+
TextDocument = new VSTextDocumentIdentifier()
394+
{
395+
Uri = _csharpDocumentUri,
396+
ProjectContext = delegatedParams.Identifier.TextDocumentIdentifier.GetProjectContext(),
397+
},
398+
Position = delegatedParams.ProjectedPosition,
399+
};
400+
401+
return _csharpServer.ExecuteRequestAsync<TextDocumentPositionParams, VSInternalDataTip>(
402+
VSInternalMethods.TextDocumentDataTipRangeName, delegatedRequest, cancellationToken);
403+
}
386404
}
387405
}

0 commit comments

Comments
 (0)