Skip to content

Commit fdbcebd

Browse files
Copilotdavidwengier
andcommitted
Create shared WrapWithTagHelper and update interfaces to use LinePositionSpan
Co-authored-by: davidwengier <[email protected]>
1 parent 252a33b commit fdbcebd

File tree

5 files changed

+133
-149
lines changed

5 files changed

+133
-149
lines changed

src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/WrapWithTag/WrapWithTagEndpoint.cs

Lines changed: 7 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,13 @@
44
using System.Threading;
55
using System.Threading.Tasks;
66
using Microsoft.AspNetCore.Razor.Language;
7-
using Microsoft.AspNetCore.Razor.Language.Syntax;
87
using Microsoft.AspNetCore.Razor.LanguageServer.EndpointContracts;
98
using Microsoft.AspNetCore.Razor.LanguageServer.Formatting;
109
using Microsoft.AspNetCore.Razor.LanguageServer.Hosting;
1110
using Microsoft.CodeAnalysis.Razor.Formatting;
1211
using Microsoft.CodeAnalysis.Razor.Logging;
1312
using Microsoft.CodeAnalysis.Razor.Protocol;
13+
using Microsoft.CodeAnalysis.Razor.Workspaces.Utilities;
1414
using Microsoft.CodeAnalysis.Text;
1515

1616
namespace Microsoft.AspNetCore.Razor.LanguageServer.WrapWithTag;
@@ -40,80 +40,18 @@ public TextDocumentIdentifier GetTextDocumentIdentifier(WrapWithTagParams reques
4040
cancellationToken.ThrowIfCancellationRequested();
4141

4242
var codeDocument = await documentContext.GetCodeDocumentAsync(cancellationToken).ConfigureAwait(false);
43-
var sourceText = codeDocument.Source.Text;
4443

45-
if (request.Range?.Start is not { } start ||
46-
!sourceText.TryGetAbsoluteIndex(start, out var hostDocumentIndex))
44+
var validationResult = await WrapWithTagHelper.ValidateAndAdjustRangeAsync(codeDocument, request.Range, cancellationToken).ConfigureAwait(false);
45+
if (!validationResult.IsValid)
4746
{
47+
_logger.LogInformation($"Unsupported language at the requested range.");
4848
return null;
4949
}
5050

51-
// First thing we do is make sure we start at a non-whitespace character. This is important because in some
52-
// situations the whitespace can be technically C#, but move one character to the right and it's HTML. eg
53-
//
54-
// @if (true) {
55-
// | <p></p>
56-
// }
57-
//
58-
// Limiting this to only whitespace on the same line, as it's not clear what user expectation would be otherwise.
59-
var requestSpan = sourceText.GetTextSpan(request.Range);
60-
if (sourceText.TryGetFirstNonWhitespaceOffset(requestSpan, out var offset, out var newLineCount) &&
61-
newLineCount == 0)
51+
// Update the request range if it was adjusted
52+
if (validationResult.AdjustedRange is not null)
6253
{
63-
request.Range.Start.Character += offset;
64-
requestSpan = sourceText.GetTextSpan(request.Range);
65-
hostDocumentIndex += offset;
66-
}
67-
68-
// Since we're at the start of the selection, lets prefer the language to the right of the cursor if possible.
69-
// That way with the following situation:
70-
//
71-
// @if (true) {
72-
// |<p></p>
73-
// }
74-
//
75-
// Instead of C#, which certainly would be expected to go in an if statement, we'll see HTML, which obviously
76-
// is the better choice for this operation.
77-
var languageKind = codeDocument.GetLanguageKind(hostDocumentIndex, rightAssociative: true);
78-
79-
// However, reverse scenario is possible as well, when we have
80-
// <div>
81-
// |@if (true) {}
82-
// <p></p>
83-
// </div>
84-
// in which case right-associative GetLanguageKind will return Razor and left-associative will return HTML
85-
// We should hand that case as well, see https://github.com/dotnet/razor/issues/10819
86-
if (languageKind is RazorLanguageKind.Razor)
87-
{
88-
languageKind = codeDocument.GetLanguageKind(hostDocumentIndex, rightAssociative: false);
89-
}
90-
91-
if (languageKind is not RazorLanguageKind.Html)
92-
{
93-
// In general, we don't support C# for obvious reasons, but we can support implicit expressions. ie
94-
//
95-
// <p>@curr$$entCount</p>
96-
//
97-
// We can expand the range to encompass the whole implicit expression, and then it will wrap as expected.
98-
// Similarly if they have selected the implicit expression, then we can continue. ie
99-
//
100-
// <p>[|@currentCount|]</p>
101-
102-
var tree = await documentContext.GetSyntaxTreeAsync(cancellationToken).ConfigureAwait(false);
103-
var node = tree.Root.FindNode(requestSpan, includeWhitespace: false, getInnermostNodeForTie: true);
104-
if (node?.FirstAncestorOrSelf<CSharpImplicitExpressionSyntax>() is { Parent: CSharpCodeBlockSyntax codeBlock } &&
105-
(requestSpan == codeBlock.Span || requestSpan.Length == 0))
106-
{
107-
// Pretend we're in Html so the rest of the logic can continue
108-
request.Range = sourceText.GetRange(codeBlock.Span);
109-
languageKind = RazorLanguageKind.Html;
110-
}
111-
}
112-
113-
if (languageKind is not RazorLanguageKind.Html)
114-
{
115-
_logger.LogInformation($"Unsupported language {languageKind:G}.");
116-
return null;
54+
request.Range = validationResult.AdjustedRange;
11755
}
11856

11957
cancellationToken.ThrowIfCancellationRequested();

src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/IRemoteWrapWithTagService.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,13 @@
44
using System.Threading;
55
using System.Threading.Tasks;
66
using Microsoft.CodeAnalysis.ExternalAccess.Razor;
7-
using Microsoft.CodeAnalysis.Razor.Protocol;
87
using Microsoft.CodeAnalysis.Text;
98

109
namespace Microsoft.CodeAnalysis.Razor.Remote;
1110

1211
internal interface IRemoteWrapWithTagService
1312
{
14-
ValueTask<RemoteResponse<bool>> IsValidWrapWithTagLocationAsync(RazorPinnedSolutionInfoWrapper solutionInfo, DocumentId razorDocumentId, LspRange range, CancellationToken cancellationToken);
13+
ValueTask<RemoteResponse<bool>> IsValidWrapWithTagLocationAsync(RazorPinnedSolutionInfoWrapper solutionInfo, DocumentId razorDocumentId, LinePositionSpan range, CancellationToken cancellationToken);
1514

1615
ValueTask<RemoteResponse<TextEdit[]?>> FixHtmlTextEditsAsync(RazorPinnedSolutionInfoWrapper solutionInfo, DocumentId razorDocumentId, TextEdit[] textEdits, CancellationToken cancellationToken);
1716
}
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
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 System.Threading;
5+
using System.Threading.Tasks;
6+
using Microsoft.AspNetCore.Razor.Language;
7+
using Microsoft.AspNetCore.Razor.Language.Syntax;
8+
using Microsoft.CodeAnalysis.Razor.Protocol;
9+
using Microsoft.CodeAnalysis.Text;
10+
using Roslyn.LanguageServer.Protocol;
11+
12+
namespace Microsoft.CodeAnalysis.Razor.Workspaces.Utilities;
13+
14+
internal static class WrapWithTagHelper
15+
{
16+
public static async Task<bool> IsValidWrappingRangeAsync(RazorCodeDocument codeDocument, LinePositionSpan range, CancellationToken cancellationToken)
17+
{
18+
var result = await ValidateAndAdjustRangeInternalAsync(codeDocument, range, cancellationToken).ConfigureAwait(false);
19+
return result.IsValid;
20+
}
21+
22+
public static async Task<(bool IsValid, LspRange? AdjustedRange)> ValidateAndAdjustRangeAsync(RazorCodeDocument codeDocument, LspRange range, CancellationToken cancellationToken)
23+
{
24+
var sourceText = codeDocument.Source.Text;
25+
26+
if (range?.Start is not { } start ||
27+
!sourceText.TryGetAbsoluteIndex(start, out var hostDocumentIndex))
28+
{
29+
return (false, null);
30+
}
31+
32+
// First thing we do is make sure we start at a non-whitespace character. This is important because in some
33+
// situations the whitespace can be technically C#, but move one character to the right and it's HTML. eg
34+
//
35+
// @if (true) {
36+
// | <p></p>
37+
// }
38+
//
39+
// Limiting this to only whitespace on the same line, as it's not clear what user expectation would be otherwise.
40+
var requestSpan = sourceText.GetTextSpan(range);
41+
var adjustedRange = range;
42+
if (sourceText.TryGetFirstNonWhitespaceOffset(requestSpan, out var offset, out var newLineCount) &&
43+
newLineCount == 0)
44+
{
45+
adjustedRange = new LspRange
46+
{
47+
Start = new Position
48+
{
49+
Line = range.Start.Line,
50+
Character = range.Start.Character + offset
51+
},
52+
End = range.End
53+
};
54+
requestSpan = sourceText.GetTextSpan(adjustedRange);
55+
hostDocumentIndex += offset;
56+
}
57+
58+
// Since we're at the start of the selection, lets prefer the language to the right of the cursor if possible.
59+
// That way with the following situation:
60+
//
61+
// @if (true) {
62+
// |<p></p>
63+
// }
64+
//
65+
// Instead of C#, which certainly would be expected to go in an if statement, we'll see HTML, which obviously
66+
// is the better choice for this operation.
67+
var languageKind = codeDocument.GetLanguageKind(hostDocumentIndex, rightAssociative: true);
68+
69+
// However, reverse scenario is possible as well, when we have
70+
// <div>
71+
// |@if (true) {}
72+
// <p></p>
73+
// </div>
74+
// in which case right-associative GetLanguageKind will return Razor and left-associative will return HTML
75+
// We should hand that case as well, see https://github.com/dotnet/razor/issues/10819
76+
if (languageKind is RazorLanguageKind.Razor)
77+
{
78+
languageKind = codeDocument.GetLanguageKind(hostDocumentIndex, rightAssociative: false);
79+
}
80+
81+
if (languageKind is not RazorLanguageKind.Html)
82+
{
83+
// In general, we don't support C# for obvious reasons, but we can support implicit expressions. ie
84+
//
85+
// <p>@curr$$entCount</p>
86+
//
87+
// We can expand the range to encompass the whole implicit expression, and then it will wrap as expected.
88+
// Similarly if they have selected the implicit expression, then we can continue. ie
89+
//
90+
// <p>[|@currentCount|]</p>
91+
92+
var tree = codeDocument.GetSyntaxTree();
93+
var node = tree.Root.FindNode(requestSpan, includeWhitespace: false, getInnermostNodeForTie: true);
94+
if (node?.FirstAncestorOrSelf<CSharpImplicitExpressionSyntax>() is { Parent: CSharpCodeBlockSyntax codeBlock } &&
95+
(requestSpan == codeBlock.Span || requestSpan.Length == 0))
96+
{
97+
// Pretend we're in Html so the rest of the logic can continue
98+
adjustedRange = sourceText.GetRange(codeBlock.Span);
99+
languageKind = RazorLanguageKind.Html;
100+
}
101+
}
102+
103+
if (languageKind is not RazorLanguageKind.Html)
104+
{
105+
return (false, null);
106+
}
107+
108+
return (true, adjustedRange);
109+
}
110+
111+
private static async Task<(bool IsValid, LinePositionSpan? AdjustedRange)> ValidateAndAdjustRangeInternalAsync(RazorCodeDocument codeDocument, LinePositionSpan range, CancellationToken cancellationToken)
112+
{
113+
var lspRange = range.ToRange();
114+
var result = await ValidateAndAdjustRangeAsync(codeDocument, lspRange, cancellationToken).ConfigureAwait(false);
115+
return (result.IsValid, result.AdjustedRange?.ToLinePositionSpan());
116+
}
117+
}

src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/WrapWithTag/RemoteWrapWithTagService.cs

Lines changed: 5 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,11 @@
44
using System.Threading;
55
using System.Threading.Tasks;
66
using Microsoft.AspNetCore.Razor.Language;
7-
using Microsoft.AspNetCore.Razor.Language.Syntax;
87
using Microsoft.CodeAnalysis.ExternalAccess.Razor;
98
using Microsoft.CodeAnalysis.Razor.Formatting;
10-
using Microsoft.CodeAnalysis.Razor.Protocol;
119
using Microsoft.CodeAnalysis.Razor.Remote;
1210
using Microsoft.CodeAnalysis.Razor.Workspaces;
11+
using Microsoft.CodeAnalysis.Razor.Workspaces.Utilities;
1312
using Microsoft.CodeAnalysis.Remote.Razor.ProjectSystem;
1413
using Microsoft.CodeAnalysis.Text;
1514
using Response = Microsoft.CodeAnalysis.Razor.Remote.RemoteResponse<bool>;
@@ -28,7 +27,7 @@ protected override IRemoteWrapWithTagService CreateService(in ServiceArgs args)
2827
public ValueTask<Response> IsValidWrapWithTagLocationAsync(
2928
RazorPinnedSolutionInfoWrapper solutionInfo,
3029
DocumentId razorDocumentId,
31-
LspRange range,
30+
LinePositionSpan range,
3231
CancellationToken cancellationToken)
3332
=> RunServiceAsync(
3433
solutionInfo,
@@ -49,83 +48,12 @@ public ValueTask<TextEditResponse> FixHtmlTextEditsAsync(
4948

5049
private async ValueTask<Response> IsValidWrapWithTagLocationAsync(
5150
RemoteDocumentContext context,
52-
LspRange range,
51+
LinePositionSpan range,
5352
CancellationToken cancellationToken)
5453
{
5554
var codeDocument = await context.GetCodeDocumentAsync(cancellationToken).ConfigureAwait(false);
56-
var sourceText = codeDocument.Source.Text;
57-
58-
if (range?.Start is not { } start ||
59-
!sourceText.TryGetAbsoluteIndex(start, out var hostDocumentIndex))
60-
{
61-
return Response.NoFurtherHandling;
62-
}
63-
64-
// First thing we do is make sure we start at a non-whitespace character. This is important because in some
65-
// situations the whitespace can be technically C#, but move one character to the right and it's HTML. eg
66-
//
67-
// @if (true) {
68-
// | <p></p>
69-
// }
70-
//
71-
// Limiting this to only whitespace on the same line, as it's not clear what user expectation would be otherwise.
72-
var requestSpan = sourceText.GetTextSpan(range);
73-
if (sourceText.TryGetFirstNonWhitespaceOffset(requestSpan, out var offset, out var newLineCount) &&
74-
newLineCount == 0)
75-
{
76-
hostDocumentIndex += offset;
77-
}
78-
79-
// Since we're at the start of the selection, lets prefer the language to the right of the cursor if possible.
80-
// That way with the following situation:
81-
//
82-
// @if (true) {
83-
// |<p></p>
84-
// }
85-
//
86-
// Instead of C#, which certainly would be expected to go in an if statement, we'll see HTML, which obviously
87-
// is the better choice for this operation.
88-
var languageKind = codeDocument.GetLanguageKind(hostDocumentIndex, rightAssociative: true);
89-
90-
// However, reverse scenario is possible as well, when we have
91-
// <div>
92-
// |@if (true) {}
93-
// <p></p>
94-
// </div>
95-
// in which case right-associative GetLanguageKind will return Razor and left-associative will return HTML
96-
// We should hand that case as well, see https://github.com/dotnet/razor/issues/10819
97-
if (languageKind is RazorLanguageKind.Razor)
98-
{
99-
languageKind = codeDocument.GetLanguageKind(hostDocumentIndex, rightAssociative: false);
100-
}
101-
102-
if (languageKind is not RazorLanguageKind.Html)
103-
{
104-
// In general, we don't support C# for obvious reasons, but we can support implicit expressions. ie
105-
//
106-
// <p>@curr$$entCount</p>
107-
//
108-
// We can expand the range to encompass the whole implicit expression, and then it will wrap as expected.
109-
// Similarly if they have selected the implicit expression, then we can continue. ie
110-
//
111-
// <p>[|@currentCount|]</p>
112-
113-
var tree = await context.GetSyntaxTreeAsync(cancellationToken).ConfigureAwait(false);
114-
var node = tree.Root.FindNode(requestSpan, includeWhitespace: false, getInnermostNodeForTie: true);
115-
if (node?.FirstAncestorOrSelf<CSharpImplicitExpressionSyntax>() is { Parent: CSharpCodeBlockSyntax codeBlock } &&
116-
(requestSpan == codeBlock.Span || requestSpan.Length == 0))
117-
{
118-
// Pretend we're in Html so the rest of the logic can continue
119-
languageKind = RazorLanguageKind.Html;
120-
}
121-
}
122-
123-
if (languageKind is not RazorLanguageKind.Html)
124-
{
125-
return Response.NoFurtherHandling;
126-
}
127-
128-
return Response.Results(true);
55+
var isValid = await WrapWithTagHelper.IsValidWrappingRangeAsync(codeDocument, range, cancellationToken).ConfigureAwait(false);
56+
return Response.Results(isValid);
12957
}
13058

13159
private async ValueTask<TextEditResponse> FixHtmlTextEditsAsync(

src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostWrapWithTagEndpoint.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
using Microsoft.CodeAnalysis.Razor.Workspaces;
1414
using Microsoft.CodeAnalysis.Text;
1515
using Microsoft.VisualStudio.Razor.LanguageClient.WrapWithTag;
16+
using Roslyn.LanguageServer.Protocol;
1617

1718
namespace Microsoft.VisualStudio.Razor.LanguageClient.Cohost;
1819

@@ -45,9 +46,10 @@ internal sealed class CohostWrapWithTagEndpoint(
4546
private async Task<VSInternalWrapWithTagResponse?> HandleRequestAsync(VSInternalWrapWithTagParams request, TextDocument razorDocument, CancellationToken cancellationToken)
4647
{
4748
// First, check if the position is valid for wrap with tag operation through the remote service
49+
var range = request.Range.ToLinePositionSpan();
4850
var isValidLocation = await _remoteServiceInvoker.TryInvokeAsync<IRemoteWrapWithTagService, RemoteResponse<bool>>(
4951
razorDocument.Project.Solution,
50-
(service, solutionInfo, cancellationToken) => service.IsValidWrapWithTagLocationAsync(solutionInfo, razorDocument.Id, request.Range, cancellationToken),
52+
(service, solutionInfo, cancellationToken) => service.IsValidWrapWithTagLocationAsync(solutionInfo, razorDocument.Id, range, cancellationToken),
5153
cancellationToken).ConfigureAwait(false);
5254

5355
// If the remote service says it's not a valid location or we should stop handling, return null

0 commit comments

Comments
 (0)