diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/WrapWithTag/WrapWithTagEndpoint.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/WrapWithTag/WrapWithTagEndpoint.cs
index 2b2df489b6b..0a11741e6ce 100644
--- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/WrapWithTag/WrapWithTagEndpoint.cs
+++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/WrapWithTag/WrapWithTagEndpoint.cs
@@ -47,13 +47,30 @@ public TextDocumentIdentifier GetTextDocumentIdentifier(WrapWithTagParams reques
return null;
}
- var sourceText = await documentContext.GetSourceTextAsync(cancellationToken).ConfigureAwait(false);
+ var sourceText = codeDocument.Source.Text;
if (request.Range?.Start is not { } start ||
!sourceText.TryGetAbsoluteIndex(start, out var hostDocumentIndex))
{
return null;
}
+ // First thing we do is make sure we start at a non-whitespace character. This is important because in some
+ // situations the whitespace can be technically C#, but move one character to the right and it's HTML. eg
+ //
+ // @if (true) {
+ // |
+ // }
+ //
+ // Limiting this to only whitespace on the same line, as it's not clear what user expectation would be otherwise.
+ var requestSpan = sourceText.GetTextSpan(request.Range);
+ if (sourceText.TryGetFirstNonWhitespaceOffset(requestSpan, out var offset, out var newLineCount) &&
+ newLineCount == 0)
+ {
+ request.Range.Start.Character += offset;
+ requestSpan = sourceText.GetTextSpan(request.Range);
+ hostDocumentIndex += offset;
+ }
+
// Since we're at the start of the selection, lets prefer the language to the right of the cursor if possible.
// That way with the following situation:
//
@@ -89,7 +106,6 @@ public TextDocumentIdentifier GetTextDocumentIdentifier(WrapWithTagParams reques
// [|@currentCount|]
var tree = await documentContext.GetSyntaxTreeAsync(cancellationToken).ConfigureAwait(false);
- var requestSpan = sourceText.GetTextSpan(request.Range);
var node = tree.Root.FindNode(requestSpan, includeWhitespace: false, getInnermostNodeForTie: true);
if (node?.FirstAncestorOrSelf() is { Parent: CSharpCodeBlockSyntax codeBlock } &&
(requestSpan == codeBlock.FullSpan || requestSpan.Length == 0))
diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/WrapWithTag/WrapWithTagEndpointTests.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/WrapWithTag/WrapWithTagEndpointTests.cs
index db3a851b323..0930640afdd 100644
--- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/WrapWithTag/WrapWithTagEndpointTests.cs
+++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/WrapWithTag/WrapWithTagEndpointTests.cs
@@ -146,6 +146,114 @@ public async Task Handle_RazorBlockStart_ReturnsResult()
Mock.Get(clientConnection).Verify();
}
+ [Fact]
+ public async Task Handle_HtmlInCSharp()
+ {
+ // Arrange
+ var input = new TestCode("""
+ @if (true)
+ {
+ [||]
+ }
+ """);
+ var codeDocument = CreateCodeDocument(input.Text);
+ var uri = new Uri("file://path/test.razor");
+ var documentContext = CreateDocumentContext(uri, codeDocument);
+ var response = new WrapWithTagResponse();
+
+ var clientConnection = TestMocks.CreateClientConnection(builder =>
+ {
+ builder.SetupSendRequest(LanguageServerConstants.RazorWrapWithTagEndpoint, response: new(), verifiable: true);
+ });
+
+ var endpoint = new WrapWithTagEndpoint(clientConnection, LoggerFactory);
+
+ var range = codeDocument.Source.Text.GetRange(input.Span);
+ var wrapWithDivParams = new WrapWithTagParams(new TextDocumentIdentifier { Uri = uri })
+ {
+ Range = range
+ };
+ var requestContext = CreateRazorRequestContext(documentContext);
+
+ // Act
+ var result = await endpoint.HandleRequestAsync(wrapWithDivParams, requestContext, DisposalToken);
+
+ // Assert
+ Assert.NotNull(result);
+ Mock.Get(clientConnection).Verify();
+ }
+
+ [Fact]
+ public async Task Handle_HtmlInCSharp_WithWhitespace()
+ {
+ // Arrange
+ var input = new TestCode("""
+ @if (true)
+ {
+ [| |]
+ }
+ """);
+ var codeDocument = CreateCodeDocument(input.Text);
+ var uri = new Uri("file://path/test.razor");
+ var documentContext = CreateDocumentContext(uri, codeDocument);
+ var response = new WrapWithTagResponse();
+
+ var clientConnection = TestMocks.CreateClientConnection(builder =>
+ {
+ builder.SetupSendRequest(LanguageServerConstants.RazorWrapWithTagEndpoint, response: new(), verifiable: true);
+ });
+
+ var endpoint = new WrapWithTagEndpoint(clientConnection, LoggerFactory);
+
+ var range = codeDocument.Source.Text.GetRange(input.Span);
+ var wrapWithDivParams = new WrapWithTagParams(new TextDocumentIdentifier { Uri = uri })
+ {
+ Range = range
+ };
+ var requestContext = CreateRazorRequestContext(documentContext);
+
+ // Act
+ var result = await endpoint.HandleRequestAsync(wrapWithDivParams, requestContext, DisposalToken);
+
+ // Assert
+ Assert.NotNull(result);
+ Mock.Get(clientConnection).Verify();
+ }
+
+ [Fact]
+ public async Task Handle_HtmlInCSharp_WithNewline()
+ {
+ // Arrange
+ var input = new TestCode("""
+ @if (true)
+ {[|
+ |]
+ }
+ """);
+ var codeDocument = CreateCodeDocument(input.Text);
+ var uri = new Uri("file://path/test.razor");
+ var documentContext = CreateDocumentContext(uri, codeDocument);
+ var response = new WrapWithTagResponse();
+
+ var clientConnection = TestMocks.CreateClientConnection(builder => { });
+
+ var endpoint = new WrapWithTagEndpoint(clientConnection, LoggerFactory);
+
+ var range = codeDocument.Source.Text.GetRange(input.Span);
+ var wrapWithDivParams = new WrapWithTagParams(new TextDocumentIdentifier { Uri = uri })
+ {
+ Range = range
+ };
+ var requestContext = CreateRazorRequestContext(documentContext);
+
+ // Act
+ var result = await endpoint.HandleRequestAsync(wrapWithDivParams, requestContext, DisposalToken);
+
+ // Assert
+ Assert.Null(result);
+ Mock.Get(clientConnection).Verify();
+ }
+
[Fact]
public async Task Handle_CSharp_PartOfImplicitStatement_ReturnsNull()
{