Skip to content

Commit 5556916

Browse files
authored
Port wrap with tag to cohosting (#11997)
Fixes #11993 Part of #9519 Copilot [did the boring work](#11994), I just did a bit of clean up and testing. Left its commit history intact to preserve credit :)
2 parents f35980b + f05466d commit 5556916

File tree

10 files changed

+523
-72
lines changed

10 files changed

+523
-72
lines changed

eng/targets/Services.props

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,5 +39,6 @@
3939
<ServiceHubService Include="Microsoft.VisualStudio.Razor.FindAllReferences" ClassName="Microsoft.CodeAnalysis.Remote.Razor.RemoteFindAllReferencesService+Factory" />
4040
<ServiceHubService Include="Microsoft.VisualStudio.Razor.InlineCompletion" ClassName="Microsoft.CodeAnalysis.Remote.Razor.RemoteInlineCompletionService+Factory" />
4141
<ServiceHubService Include="Microsoft.VisualStudio.Razor.DebugInfo" ClassName="Microsoft.CodeAnalysis.Remote.Razor.RemoteDebugInfoService+Factory" />
42+
<ServiceHubService Include="Microsoft.VisualStudio.Razor.WrapWithTag" ClassName="Microsoft.CodeAnalysis.Remote.Razor.RemoteWrapWithTagService+Factory" />
4243
</ItemGroup>
4344
</Project>

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

Lines changed: 8 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,11 @@
33

44
using System.Threading;
55
using System.Threading.Tasks;
6-
using Microsoft.AspNetCore.Razor.Language;
7-
using Microsoft.AspNetCore.Razor.Language.Syntax;
86
using Microsoft.AspNetCore.Razor.LanguageServer.EndpointContracts;
9-
using Microsoft.AspNetCore.Razor.LanguageServer.Formatting;
107
using Microsoft.AspNetCore.Razor.LanguageServer.Hosting;
118
using Microsoft.CodeAnalysis.Razor.Formatting;
129
using Microsoft.CodeAnalysis.Razor.Logging;
13-
using Microsoft.CodeAnalysis.Razor.Protocol;
14-
using Microsoft.CodeAnalysis.Text;
10+
using Microsoft.CodeAnalysis.Razor.Utilities;
1511

1612
namespace Microsoft.AspNetCore.Razor.LanguageServer.WrapWithTag;
1713

@@ -40,82 +36,22 @@ public TextDocumentIdentifier GetTextDocumentIdentifier(WrapWithTagParams reques
4036
cancellationToken.ThrowIfCancellationRequested();
4137

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

45-
if (request.Range?.Start is not { } start ||
46-
!sourceText.TryGetAbsoluteIndex(start, out var hostDocumentIndex))
40+
if (request.Range is null)
4741
{
42+
_logger.LogInformation($"WrapWithTag request for {request.TextDocument.DocumentUri} has a null range.");
4843
return null;
4944
}
5045

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)
46+
if (!WrapWithTagHelper.TryGetValidWrappingRange(codeDocument, request.Range.ToLinePositionSpan(), out var adjustedRange))
6247
{
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}.");
48+
_logger.LogInformation($"Unsupported language at the requested range.");
11649
return null;
11750
}
11851

52+
// Update the request range if it was adjusted
53+
request.Range = adjustedRange.ToRange();
54+
11955
cancellationToken.ThrowIfCancellationRequested();
12056

12157
var versioned = new VersionedTextDocumentIdentifier

src/Razor/src/Microsoft.CodeAnalysis.Razor.CohostingShared/RazorLSPConstants.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,14 @@ namespace Microsoft.VisualStudio.Razor.LanguageClient;
55

66
internal static class RazorLSPConstants
77
{
8+
public const string WebToolsWrapWithTagServerNameProperty = "_wrap_with_tag_lsp_server_name";
9+
810
public const string RazorCSharpLanguageServerName = "Razor C# Language Server Client";
911

1012
public const string RazorLanguageServerName = "Razor Language Server Client";
1113

14+
public const string RoslynLanguageServerName = "Roslyn Language Server Client";
15+
1216
public const string CohostLanguageServerName = "Cohosted Razor Language Server Client";
1317

1418
public const string HtmlLanguageServerName = "HtmlDelegationLanguageServerClient";
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
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.CodeAnalysis.ExternalAccess.Razor;
7+
using Microsoft.CodeAnalysis.Text;
8+
9+
namespace Microsoft.CodeAnalysis.Razor.Remote;
10+
11+
internal interface IRemoteWrapWithTagService
12+
{
13+
ValueTask<RemoteResponse<LinePositionSpan>> GetValidWrappingRangeAsync(RazorPinnedSolutionInfoWrapper solutionInfo, DocumentId razorDocumentId, LinePositionSpan range, CancellationToken cancellationToken);
14+
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ internal static class RazorServices
2525
(typeof(IRemoteSpellCheckService), null),
2626
(typeof(IRemoteInlineCompletionService), null),
2727
(typeof(IRemoteDebugInfoService), null),
28+
(typeof(IRemoteWrapWithTagService), null),
2829
];
2930

3031
// Internal for testing
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
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;
5+
using Microsoft.AspNetCore.Razor.Language.Syntax;
6+
using Microsoft.CodeAnalysis.Razor.Protocol;
7+
using Microsoft.CodeAnalysis.Text;
8+
9+
namespace Microsoft.CodeAnalysis.Razor.Utilities;
10+
11+
internal static class WrapWithTagHelper
12+
{
13+
/// <summary>
14+
/// Returns the range to use for wrapping a selection with a tag.
15+
/// </summary>
16+
/// <returns>Returns null if the range specified is not valid</returns>
17+
public static bool TryGetValidWrappingRange(RazorCodeDocument codeDocument, LinePositionSpan range, out LinePositionSpan wrappingRange)
18+
{
19+
wrappingRange = range;
20+
21+
var sourceText = codeDocument.Source.Text;
22+
23+
if (!sourceText.TryGetAbsoluteIndex(range.Start, out var hostDocumentIndex))
24+
{
25+
return false;
26+
}
27+
28+
// First thing we do is make sure we start at a non-whitespace character. This is important because in some
29+
// situations the whitespace can be technically C#, but move one character to the right and it's HTML. eg
30+
//
31+
// @if (true) {
32+
// | <p></p>
33+
// }
34+
//
35+
// Limiting this to only whitespace on the same line, as it's not clear what user expectation would be otherwise.
36+
var requestSpan = sourceText.GetTextSpan(range);
37+
if (sourceText.TryGetFirstNonWhitespaceOffset(requestSpan, out var offset, out var newLineCount) &&
38+
newLineCount == 0)
39+
{
40+
wrappingRange = new LinePositionSpan(
41+
start: new LinePosition(
42+
line: range.Start.Line,
43+
character: range.Start.Character + offset),
44+
end: range.End);
45+
requestSpan = sourceText.GetTextSpan(wrappingRange);
46+
hostDocumentIndex += offset;
47+
}
48+
49+
// Since we're at the start of the selection, lets prefer the language to the right of the cursor if possible.
50+
// That way with the following situation:
51+
//
52+
// @if (true) {
53+
// |<p></p>
54+
// }
55+
//
56+
// Instead of C#, which certainly would be expected to go in an if statement, we'll see HTML, which obviously
57+
// is the better choice for this operation.
58+
var languageKind = codeDocument.GetLanguageKind(hostDocumentIndex, rightAssociative: true);
59+
60+
// However, reverse scenario is possible as well, when we have
61+
// <div>
62+
// |@if (true) {}
63+
// <p></p>
64+
// </div>
65+
// in which case right-associative GetLanguageKind will return Razor and left-associative will return HTML
66+
// We should hand that case as well, see https://github.com/dotnet/razor/issues/10819
67+
if (languageKind is RazorLanguageKind.Razor)
68+
{
69+
languageKind = codeDocument.GetLanguageKind(hostDocumentIndex, rightAssociative: false);
70+
}
71+
72+
if (languageKind is not RazorLanguageKind.Html)
73+
{
74+
// In general, we don't support C# for obvious reasons, but we can support implicit expressions. ie
75+
//
76+
// <p>@curr$$entCount</p>
77+
//
78+
// We can expand the range to encompass the whole implicit expression, and then it will wrap as expected.
79+
// Similarly if they have selected the implicit expression, then we can continue. ie
80+
//
81+
// <p>[|@currentCount|]</p>
82+
83+
var root = codeDocument.GetRequiredSyntaxRoot();
84+
var node = root.FindNode(requestSpan, includeWhitespace: false, getInnermostNodeForTie: true);
85+
if (node?.FirstAncestorOrSelf<CSharpImplicitExpressionSyntax>() is { Parent: CSharpCodeBlockSyntax codeBlock } &&
86+
(requestSpan == codeBlock.Span || requestSpan.Length == 0))
87+
{
88+
// Pretend we're in Html so the rest of the logic can continue
89+
wrappingRange = sourceText.GetLinePositionSpan(codeBlock.Span);
90+
return true;
91+
}
92+
93+
return false;
94+
}
95+
96+
return true;
97+
}
98+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
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.CodeAnalysis.ExternalAccess.Razor;
7+
using Microsoft.CodeAnalysis.Razor.Remote;
8+
using Microsoft.CodeAnalysis.Razor.Utilities;
9+
using Microsoft.CodeAnalysis.Remote.Razor.ProjectSystem;
10+
using Microsoft.CodeAnalysis.Text;
11+
using Response = Microsoft.CodeAnalysis.Razor.Remote.RemoteResponse<Microsoft.CodeAnalysis.Text.LinePositionSpan>;
12+
13+
namespace Microsoft.CodeAnalysis.Remote.Razor;
14+
15+
internal sealed partial class RemoteWrapWithTagService(in ServiceArgs args) : RazorDocumentServiceBase(in args), IRemoteWrapWithTagService
16+
{
17+
internal sealed class Factory : FactoryBase<IRemoteWrapWithTagService>
18+
{
19+
protected override IRemoteWrapWithTagService CreateService(in ServiceArgs args)
20+
=> new RemoteWrapWithTagService(in args);
21+
}
22+
23+
public ValueTask<Response> GetValidWrappingRangeAsync(
24+
RazorPinnedSolutionInfoWrapper solutionInfo,
25+
DocumentId razorDocumentId,
26+
LinePositionSpan range,
27+
CancellationToken cancellationToken)
28+
=> RunServiceAsync(
29+
solutionInfo,
30+
razorDocumentId,
31+
context => GetValidWrappingRangeAsync(context, range, cancellationToken),
32+
cancellationToken);
33+
34+
private static async ValueTask<Response> GetValidWrappingRangeAsync(
35+
RemoteDocumentContext context,
36+
LinePositionSpan range,
37+
CancellationToken cancellationToken)
38+
{
39+
var codeDocument = await context.GetCodeDocumentAsync(cancellationToken).ConfigureAwait(false);
40+
if (WrapWithTagHelper.TryGetValidWrappingRange(codeDocument, range, out var adjustedRange))
41+
{
42+
return Response.Results(adjustedRange);
43+
}
44+
45+
return Response.NoFurtherHandling;
46+
}
47+
}

0 commit comments

Comments
 (0)