Skip to content

Commit f06c995

Browse files
committed
Merge remote-tracking branch 'origin/main' into copilot/add-intellisense-support
2 parents 9275486 + 514021f commit f06c995

File tree

7 files changed

+271
-1
lines changed

7 files changed

+271
-1
lines changed
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Microsoft.AspNetCore.Razor.Language.Syntax;
5+
6+
namespace Microsoft.AspNetCore.Razor.Language;
7+
8+
internal static partial class RazorCodeDocumentExtensions
9+
{
10+
/// <summary>
11+
/// Adjusts the position if it's on a component end tag to use the corresponding start tag position.
12+
/// This ensures that hover, go to definition, and find all references work consistently for both
13+
/// start and end tags, since only start tags have source mappings.
14+
/// </summary>
15+
/// <param name="codeDocument">The code document.</param>
16+
/// <param name="hostDocumentIndex">The position in the host document.</param>
17+
/// <returns>
18+
/// The adjusted position if on a component end tag's name, otherwise the original position.
19+
/// </returns>
20+
public static int AdjustPositionForComponentEndTag(this RazorCodeDocument codeDocument, int hostDocumentIndex)
21+
{
22+
var root = codeDocument.GetRequiredSyntaxRoot();
23+
var owner = root.FindInnermostNode(hostDocumentIndex, includeWhitespace: false);
24+
if (owner is null)
25+
{
26+
return hostDocumentIndex;
27+
}
28+
29+
// Check if we're on a component end tag and the position is within the tag name
30+
if (owner.FirstAncestorOrSelf<MarkupTagHelperEndTagSyntax>() is { } endTag &&
31+
endTag.Name.Span.IntersectsWith(hostDocumentIndex) &&
32+
endTag.GetStartTag() is MarkupTagHelperStartTagSyntax tagHelperStartTag)
33+
{
34+
// Calculate the offset within the end tag name
35+
var offsetInEndTag = hostDocumentIndex - endTag.Name.SpanStart;
36+
37+
// Apply the same offset to the start tag name
38+
// This preserves the relative position within the tag name
39+
return tagHelperStartTag.Name.SpanStart + offsetInEndTag;
40+
}
41+
42+
return hostDocumentIndex;
43+
}
44+
}

src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/FindAllReferences/RemoteFindAllReferencesService.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using System.Threading;
55
using System.Threading.Tasks;
6+
using Microsoft.AspNetCore.Razor.Language;
67
using Microsoft.CodeAnalysis.ExternalAccess.Razor;
78
using Microsoft.CodeAnalysis.Razor;
89
using Microsoft.CodeAnalysis.Razor.DocumentMapping;
@@ -55,6 +56,9 @@ protected override IRemoteFindAllReferencesService CreateService(in ServiceArgs
5556
return NoFurtherHandling;
5657
}
5758

59+
// Adjust position if on a component end tag to use the start tag position
60+
hostDocumentIndex = codeDocument.AdjustPositionForComponentEndTag(hostDocumentIndex);
61+
5862
var positionInfo = GetPositionInfo(codeDocument, hostDocumentIndex, preferCSharpOverHtml: true);
5963

6064
if (positionInfo.LanguageKind is not RazorLanguageKind.CSharp)

src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/GoToDefinition/RemoteGoToDefinitionService.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using System.Threading;
55
using System.Threading.Tasks;
6+
using Microsoft.AspNetCore.Razor.Language;
67
using Microsoft.AspNetCore.Razor.PooledObjects;
78
using Microsoft.CodeAnalysis.ExternalAccess.Razor;
89
using Microsoft.CodeAnalysis.Razor.DocumentMapping;
@@ -54,6 +55,9 @@ protected override IRemoteGoToDefinitionService CreateService(in ServiceArgs arg
5455
return NoFurtherHandling;
5556
}
5657

58+
// Adjust position if on a component end tag to use the start tag position
59+
hostDocumentIndex = codeDocument.AdjustPositionForComponentEndTag(hostDocumentIndex);
60+
5761
var positionInfo = GetPositionInfo(codeDocument, hostDocumentIndex, preferCSharpOverHtml: true);
5862

5963
// First, see if this is a tag helper. We ignore component attributes here, because they're better served by the C# handler.

src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Hover/RemoteHoverService.cs

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System;
45
using System.Diagnostics;
56
using System.Threading;
67
using System.Threading.Tasks;
78
using Microsoft.AspNetCore.Razor;
9+
using Microsoft.AspNetCore.Razor.Language;
810
using Microsoft.CodeAnalysis.ExternalAccess.Razor;
911
using Microsoft.CodeAnalysis.Razor.DocumentMapping;
1012
using Microsoft.CodeAnalysis.Razor.Hover;
@@ -46,11 +48,17 @@ protected override IRemoteHoverService CreateService(in ServiceArgs args)
4648
{
4749
var codeDocument = await context.GetCodeDocumentAsync(cancellationToken).ConfigureAwait(false);
4850

49-
if (!codeDocument.Source.Text.TryGetAbsoluteIndex(position, out var hostDocumentIndex))
51+
var sourceText = codeDocument.Source.Text;
52+
if (!sourceText.TryGetAbsoluteIndex(position, out var hostDocumentIndex))
5053
{
5154
return NoFurtherHandling;
5255
}
5356

57+
var originalHostDocumentIndex = hostDocumentIndex;
58+
59+
// Adjust position if on a component end tag to use the start tag position
60+
hostDocumentIndex = codeDocument.AdjustPositionForComponentEndTag(hostDocumentIndex);
61+
5462
var clientCapabilities = _clientCapabilitiesService.ClientCapabilities;
5563
var positionInfo = GetPositionInfo(codeDocument, hostDocumentIndex, preferCSharpOverHtml: true);
5664

@@ -82,6 +90,27 @@ protected override IRemoteHoverService CreateService(in ServiceArgs args)
8290
csharpHover.Range = LspFactory.CreateRange(hostDocumentSpan);
8391
}
8492

93+
// If we adjusted from an end tag to a start tag, we need to make sure the range covers the end tag,
94+
// not just the start tag, or VS won't show the hover info
95+
if (originalHostDocumentIndex > hostDocumentIndex &&
96+
csharpHover.Range is not null)
97+
{
98+
// We were originally on the end tag somewhere, then redirected to the start tag to get the hover.
99+
// We now need to translate the range we got back, and mapped, over to the end tag again. This is as
100+
// easy as just offsetting the range by the difference between our original and adjusted index.
101+
if (sourceText.TryGetAbsoluteIndex(csharpHover.Range.Start, out var adjustedStart) &&
102+
sourceText.TryGetAbsoluteIndex(csharpHover.Range.End, out var adjustedEnd))
103+
{
104+
var offset = originalHostDocumentIndex - hostDocumentIndex;
105+
106+
// Make sure we don't fall off the end of the document, though it should be impossible
107+
adjustedStart = Math.Min(adjustedStart + offset, sourceText.Length - 1);
108+
adjustedEnd = Math.Min(adjustedEnd + offset, sourceText.Length - 1);
109+
110+
csharpHover.Range = sourceText.GetRange(adjustedStart, adjustedEnd);
111+
}
112+
}
113+
85114
return Results(csharpHover);
86115
}
87116

src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostHoverEndpointTest.cs

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,26 @@ public async Task Html()
7373
await VerifyHoverAsync(code, htmlResponse, h => Assert.Same(htmlResponse, h));
7474
}
7575

76+
[Fact]
77+
public async Task Html_EndTag()
78+
{
79+
TestCode code = """
80+
<PageTitle></PageTitle>
81+
<div></d$$iv>
82+
83+
@{
84+
var myVariable = "Hello";
85+
86+
var length = myVariable.Length;
87+
}
88+
""";
89+
90+
// This simply verifies that Hover will call into HTML.
91+
var htmlResponse = new VSInternalHover();
92+
93+
await VerifyHoverAsync(code, htmlResponse, h => Assert.Same(htmlResponse, h));
94+
}
95+
7696
[Fact]
7797
public async Task CSharp()
7898
{
@@ -185,6 +205,111 @@ await VerifyHoverAsync(code, async (hover, document) =>
185205
});
186206
}
187207

208+
[Fact]
209+
public async Task ComponentEndTag()
210+
{
211+
TestCode code = """
212+
<PageTitle></[|Pa$$geTitle|]>
213+
<div></div>
214+
215+
@{
216+
var myVariable = "Hello";
217+
218+
var length = myVariable.Length;
219+
}
220+
""";
221+
222+
await VerifyHoverAsync(code, async (hover, document) =>
223+
{
224+
await hover.VerifyRangeAsync(code.Span, document);
225+
226+
hover.VerifyRawContent(
227+
Container(
228+
Container(
229+
Image,
230+
ClassifiedText( // class Microsoft.AspNetCore.Components.Web.PageTitle
231+
Keyword("class"),
232+
WhiteSpace(" "),
233+
Namespace("Microsoft"),
234+
Punctuation("."),
235+
Namespace("AspNetCore"),
236+
Punctuation("."),
237+
Namespace("Components"),
238+
Punctuation("."),
239+
Namespace("Web"),
240+
Punctuation("."),
241+
ClassName("PageTitle")))));
242+
});
243+
}
244+
245+
[Fact]
246+
public async Task ComponentEndTag_FullyQualified()
247+
{
248+
TestCode code = """
249+
<Microsoft.AspNetCore.Components.Web.PageTitle></Microsoft.AspNetCore.Components.Web.[|Pa$$geTitle|]>
250+
<div></div>
251+
252+
@{
253+
var myVariable = "Hello";
254+
255+
var length = myVariable.Length;
256+
}
257+
""";
258+
259+
await VerifyHoverAsync(code, async (hover, document) =>
260+
{
261+
await hover.VerifyRangeAsync(code.Span, document);
262+
263+
hover.VerifyRawContent(
264+
Container(
265+
Container(
266+
Image,
267+
ClassifiedText( // class Microsoft.AspNetCore.Components.Web.PageTitle
268+
Keyword("class"),
269+
WhiteSpace(" "),
270+
Namespace("Microsoft"),
271+
Punctuation("."),
272+
Namespace("AspNetCore"),
273+
Punctuation("."),
274+
Namespace("Components"),
275+
Punctuation("."),
276+
Namespace("Web"),
277+
Punctuation("."),
278+
ClassName("PageTitle")))));
279+
});
280+
}
281+
282+
[Fact]
283+
public async Task ComponentEndTag_FullyQualified_Namespace()
284+
{
285+
TestCode code = """
286+
<Microsoft.AspNetCore.Components.Web.PageTitle></Microsoft.[|AspNe$$tCore|].Components.Web.PageTitle>
287+
<div></div>
288+
289+
@{
290+
var myVariable = "Hello";
291+
292+
var length = myVariable.Length;
293+
}
294+
""";
295+
296+
await VerifyHoverAsync(code, async (hover, document) =>
297+
{
298+
await hover.VerifyRangeAsync(code.Span, document);
299+
300+
hover.VerifyRawContent(
301+
Container(
302+
Container(
303+
Image,
304+
ClassifiedText( // namespace Microsoft.AspNetCore
305+
Keyword("namespace"),
306+
WhiteSpace(" "),
307+
Namespace("Microsoft"),
308+
Punctuation("."),
309+
Namespace("AspNetCore")))));
310+
});
311+
}
312+
188313
private async Task VerifyHoverAsync(TestCode input, Func<Hover, TextDocument, Task> verifyHover)
189314
{
190315
var document = CreateProjectAndRazorDocument(input.Text);

src/Razor/test/Microsoft.VisualStudioCode.RazorExtension.Test/Endpoints/Shared/CohostFindAllReferencesEndpointTest.cs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,38 @@ await VerifyFindAllReferencesAsync(input,
118118
(FilePath("SurveyPrompt.cs"), surveyPrompt));
119119
}
120120

121+
[Fact]
122+
public async Task ComponentEndTag_DefinedInCSharp()
123+
{
124+
TestCode input = """
125+
<[|SurveyPrompt|] Title="InputValue"></Surv$$eyPrompt>
126+
""";
127+
128+
// lang=c#-test
129+
TestCode surveyPrompt = """
130+
using Microsoft.AspNetCore.Components;
131+
using Microsoft.AspNetCore.Components.Rendering;
132+
133+
namespace SomeProject;
134+
135+
public class [|SurveyPrompt|] : ComponentBase
136+
{
137+
[Parameter]
138+
public string Title { get; set; } = "Hello";
139+
140+
protected override void BuildRenderTree(RenderTreeBuilder builder)
141+
{
142+
builder.OpenElement(0, "div");
143+
builder.AddContent(1, Title + " from a C#-defined component!");
144+
builder.CloseElement();
145+
}
146+
}
147+
""";
148+
149+
await VerifyFindAllReferencesAsync(input,
150+
(FilePath("SurveyPrompt.cs"), surveyPrompt));
151+
}
152+
121153
private async Task VerifyFindAllReferencesAsync(TestCode input, params (string fileName, TestCode testCode)[] additionalFiles)
122154
{
123155
var document = CreateProjectAndRazorDocument(input.Text, additionalFiles: [.. additionalFiles.Select(f => (f.fileName, f.testCode.Text))]);

src/Razor/test/Microsoft.VisualStudioCode.RazorExtension.Test/Endpoints/Shared/CohostGoToDefinitionEndpointTest.cs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,38 @@ public async Task Component()
156156
Assert.Equal(range, location.Range);
157157
}
158158

159+
[Fact]
160+
public async Task ComponentEndTag()
161+
{
162+
TestCode input = """
163+
<SurveyPrompt Title="InputValue"></Surv$$eyPrompt>
164+
""";
165+
166+
TestCode surveyPrompt = """
167+
[||]@namespace SomeProject
168+
169+
<div></div>
170+
171+
@code
172+
{
173+
[Parameter]
174+
public string Title { get; set; }
175+
}
176+
""";
177+
178+
var result = await GetGoToDefinitionResultAsync(input, RazorFileKind.Component,
179+
additionalFiles: (FileName("SurveyPrompt.razor"), surveyPrompt.Text));
180+
181+
Assert.NotNull(result.Value.Second);
182+
var locations = result.Value.Second;
183+
var location = Assert.Single(locations);
184+
185+
Assert.Equal(FileUri("SurveyPrompt.razor"), location.DocumentUri.GetRequiredParsedUri());
186+
var text = SourceText.From(surveyPrompt.Text);
187+
var range = text.GetRange(surveyPrompt.Span);
188+
Assert.Equal(range, location.Range);
189+
}
190+
159191
[Fact]
160192
public async Task ComponentAttribute()
161193
{

0 commit comments

Comments
 (0)