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() {