From 98ff0e139e24e6ee572cb954701696ec6f4048c3 Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Mon, 28 Oct 2024 12:14:20 -0700 Subject: [PATCH 01/12] Don't pass LSP Position into IHoverService.GetRazorHoverInfoAsync(...) IHoverService.GetRazorHoverInfoAsync(...) takes a DocumentPositionInfo and an LSP Position, but it only actually needs one of them. --- .../Hover/HoverEndpoint.cs | 6 +- .../Hover/HoverService.TestAccessor.cs | 4 +- .../Hover/HoverService.cs | 16 ++-- .../Hover/IHoverService.cs | 2 +- .../Hover/HoverServiceTest.cs | 77 ++++++------------- 5 files changed, 37 insertions(+), 68 deletions(-) diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hover/HoverEndpoint.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hover/HoverEndpoint.cs index 82455da7d44..b1c7395d7aa 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hover/HoverEndpoint.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hover/HoverEndpoint.cs @@ -64,11 +64,7 @@ public void ApplyCapabilities(VSInternalServerCapabilities serverCapabilities, V return SpecializedTasks.Null(); } - return _hoverService.GetRazorHoverInfoAsync( - documentContext, - positionInfo, - request.Position, - cancellationToken); + return _hoverService.GetRazorHoverInfoAsync(documentContext, positionInfo, cancellationToken); } protected override Task HandleDelegatedResponseAsync(VSInternalHover? response, TextDocumentPositionParams originalRequest, RazorRequestContext requestContext, DocumentPositionInfo positionInfo, CancellationToken cancellationToken) diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hover/HoverService.TestAccessor.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hover/HoverService.TestAccessor.cs index c78299255ad..ab0e0aaafbb 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hover/HoverService.TestAccessor.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hover/HoverService.TestAccessor.cs @@ -17,9 +17,9 @@ internal sealed class TestAccessor(HoverService instance) public Task GetHoverInfoAsync( string documentFilePath, RazorCodeDocument codeDocument, - SourceLocation location, + int absoluteIndex, VSInternalClientCapabilities clientCapabilities, CancellationToken cancellationToken) - => instance.GetHoverInfoAsync(documentFilePath, codeDocument, location, clientCapabilities, cancellationToken); + => instance.GetHoverInfoAsync(documentFilePath, codeDocument, absoluteIndex, clientCapabilities, cancellationToken); } } diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hover/HoverService.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hover/HoverService.cs index 8e45716a3b4..a6044083472 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hover/HoverService.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hover/HoverService.cs @@ -34,7 +34,7 @@ internal sealed partial class HoverService( private readonly IDocumentMappingService _documentMappingService = documentMappingService; private readonly IClientCapabilitiesService _clientCapabilitiesService = clientCapabilitiesService; - public async Task GetRazorHoverInfoAsync(DocumentContext documentContext, DocumentPositionInfo positionInfo, Position position, CancellationToken cancellationToken) + public async Task GetRazorHoverInfoAsync(DocumentContext documentContext, DocumentPositionInfo positionInfo, CancellationToken cancellationToken) { // HTML can still sometimes be handled by razor. For example hovering over // a component tag like will still be in an html context @@ -52,8 +52,8 @@ internal sealed partial class HoverService( return null; } - var location = new SourceLocation(positionInfo.HostDocumentIndex, position.Line, position.Character); - return await GetHoverInfoAsync(documentContext.FilePath, codeDocument, location, _clientCapabilitiesService.ClientCapabilities, cancellationToken).ConfigureAwait(false); + return await GetHoverInfoAsync( + documentContext.FilePath, codeDocument, positionInfo.HostDocumentIndex, _clientCapabilitiesService.ClientCapabilities, cancellationToken).ConfigureAwait(false); } public async Task TranslateDelegatedResponseAsync(VSInternalHover? response, DocumentContext documentContext, DocumentPositionInfo positionInfo, CancellationToken cancellationToken) @@ -96,13 +96,13 @@ internal sealed partial class HoverService( private async Task GetHoverInfoAsync( string documentFilePath, RazorCodeDocument codeDocument, - SourceLocation location, + int absoluteIndex, VSInternalClientCapabilities clientCapabilities, CancellationToken cancellationToken) { var syntaxTree = codeDocument.GetSyntaxTree(); - var owner = syntaxTree.Root.FindInnermostNode(location.AbsoluteIndex); + var owner = syntaxTree.Root.FindInnermostNode(absoluteIndex); if (owner is null) { Debug.Fail("Owner should never be null."); @@ -126,7 +126,7 @@ internal sealed partial class HoverService( var ownerStart = owner.SpanStart; if (HtmlFacts.TryGetElementInfo(owner, out var containingTagNameToken, out var attributes, closingForwardSlashOrCloseAngleToken: out _) && - containingTagNameToken.Span.IntersectsWith(location.AbsoluteIndex)) + containingTagNameToken.Span.IntersectsWith(absoluteIndex)) { if (owner is MarkupStartTagSyntax or MarkupEndTagSyntax && containingTagNameToken.Content.Equals(SyntaxConstants.TextTagName, StringComparison.OrdinalIgnoreCase)) @@ -169,7 +169,7 @@ internal sealed partial class HoverService( } if (HtmlFacts.TryGetAttributeInfo(owner, out containingTagNameToken, out _, out var selectedAttributeName, out var selectedAttributeNameLocation, out attributes) && - selectedAttributeNameLocation?.IntersectsWith(location.AbsoluteIndex) == true) + selectedAttributeNameLocation?.IntersectsWith(absoluteIndex) == true) { // When finding parents for attributes, we make sure to find the parent of the containing tag, otherwise these methods // would return the parent of the attribute, which is not helpful, as its just going to be the containing element @@ -203,7 +203,7 @@ internal sealed partial class HoverService( // Grab the first attribute that we find that intersects with this location. That way if there are multiple attributes side-by-side aka hovering over: // // Then we take the left most attribute (attributes are returned in source order). - var attribute = attributes.First(a => a.Span.IntersectsWith(location.AbsoluteIndex)); + var attribute = attributes.First(a => a.Span.IntersectsWith(absoluteIndex)); if (attribute is MarkupTagHelperAttributeSyntax thAttributeSyntax) { attribute = thAttributeSyntax.Name; diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hover/IHoverService.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hover/IHoverService.cs index b3ef864a76c..dfcf756f970 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hover/IHoverService.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hover/IHoverService.cs @@ -11,6 +11,6 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer.Hover; internal interface IHoverService { - Task GetRazorHoverInfoAsync(DocumentContext versionedDocumentContext, DocumentPositionInfo positionInfo, Position position, CancellationToken cancellationToken); + Task GetRazorHoverInfoAsync(DocumentContext versionedDocumentContext, DocumentPositionInfo positionInfo, CancellationToken cancellationToken); Task TranslateDelegatedResponseAsync(VSInternalHover? response, DocumentContext versionedDocumentContext, DocumentPositionInfo positionInfo, CancellationToken cancellationToken); } diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Hover/HoverServiceTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Hover/HoverServiceTest.cs index 6dc5f7eda36..d6fd287fe44 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Hover/HoverServiceTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Hover/HoverServiceTest.cs @@ -60,10 +60,9 @@ public async Task GetHoverInfo_TagHelper_Element() var codeDocument = CreateCodeDocument(code.Text, isRazorFile: false, DefaultTagHelpers); var service = GetHoverService(); var serviceAccessor = service.GetTestAccessor(); - var location = new SourceLocation(code.Position, lineIndex: -1, characterIndex: -1); // Act - var hover = await serviceAccessor.GetHoverInfoAsync("file.cshtml", codeDocument, location, CreateMarkDownCapabilities(), DisposalToken); + var hover = await serviceAccessor.GetHoverInfoAsync("file.cshtml", codeDocument, code.Position, CreateMarkDownCapabilities(), DisposalToken); // Assert Assert.NotNull(hover); @@ -87,10 +86,9 @@ public async Task GetHoverInfo_TagHelper_Element_WithParent() var codeDocument = CreateCodeDocument(code.Text, isRazorFile: false, DefaultTagHelpers); var service = GetHoverService(); var serviceAccessor = service.GetTestAccessor(); - var location = new SourceLocation(code.Position, lineIndex: -1, characterIndex: -1); // Act - var hover = await serviceAccessor.GetHoverInfoAsync("file.cshtml", codeDocument, location, CreateMarkDownCapabilities(), DisposalToken); + var hover = await serviceAccessor.GetHoverInfoAsync("file.cshtml", codeDocument, code.Position, CreateMarkDownCapabilities(), DisposalToken); // Assert Assert.NotNull(hover); @@ -114,10 +112,9 @@ public async Task GetHoverInfo_TagHelper_Attribute_WithParent() var codeDocument = CreateCodeDocument(code.Text, isRazorFile: false, DefaultTagHelpers); var service = GetHoverService(); var serviceAccessor = service.GetTestAccessor(); - var location = new SourceLocation(code.Position, lineIndex: -1, characterIndex: -1); // Act - var hover = await serviceAccessor.GetHoverInfoAsync("file.cshtml", codeDocument, location, CreateMarkDownCapabilities(), DisposalToken); + var hover = await serviceAccessor.GetHoverInfoAsync("file.cshtml", codeDocument, code.Position, CreateMarkDownCapabilities(), DisposalToken); // Assert Assert.NotNull(hover); @@ -139,10 +136,9 @@ public async Task GetHoverInfo_TagHelper_Element_EndTag() var codeDocument = CreateCodeDocument(code.Text, isRazorFile: false, DefaultTagHelpers); var service = GetHoverService(); var serviceAccessor = service.GetTestAccessor(); - var location = new SourceLocation(code.Position, lineIndex: -1, characterIndex: -1); // Act - var hover = await serviceAccessor.GetHoverInfoAsync("file.cshtml", codeDocument, location, CreateMarkDownCapabilities(), DisposalToken); + var hover = await serviceAccessor.GetHoverInfoAsync("file.cshtml", codeDocument, code.Position, CreateMarkDownCapabilities(), DisposalToken); // Assert Assert.NotNull(hover); @@ -164,10 +160,9 @@ public async Task GetHoverInfo_TagHelper_Attribute() var codeDocument = CreateCodeDocument(code.Text, isRazorFile: false, DefaultTagHelpers); var service = GetHoverService(); var serviceAccessor = service.GetTestAccessor(); - var location = new SourceLocation(code.Position, lineIndex: -1, characterIndex: -1); // Act - var hover = await serviceAccessor.GetHoverInfoAsync("file.cshtml", codeDocument, location, CreateMarkDownCapabilities(), DisposalToken); + var hover = await serviceAccessor.GetHoverInfoAsync("file.cshtml", codeDocument, code.Position, CreateMarkDownCapabilities(), DisposalToken); // Assert Assert.NotNull(hover); @@ -190,11 +185,9 @@ public async Task GetHoverInfo_TagHelper_AttributeTrailingEdge() var codeDocument = CreateCodeDocument(code.Text, isRazorFile: false, DefaultTagHelpers); var service = GetHoverService(); var serviceAccessor = service.GetTestAccessor(); - var edgeLocation = code.Position; - var location = new SourceLocation(edgeLocation, lineIndex: 0, edgeLocation); // Act - var hover = await serviceAccessor.GetHoverInfoAsync("file.cshtml", codeDocument, location, CreateMarkDownCapabilities(), DisposalToken); + var hover = await serviceAccessor.GetHoverInfoAsync("file.cshtml", codeDocument, code.Position, CreateMarkDownCapabilities(), DisposalToken); // Assert Assert.NotNull(hover); @@ -217,10 +210,9 @@ public async Task GetHoverInfo_TagHelper_AttributeValue_ReturnsNull() var codeDocument = CreateCodeDocument(code.Text, isRazorFile: false, DefaultTagHelpers); var service = GetHoverService(); var serviceAccessor = service.GetTestAccessor(); - var location = new SourceLocation(code.Position, lineIndex: -1, characterIndex: -1); // Act - var hover = await serviceAccessor.GetHoverInfoAsync("file.cshtml", codeDocument, location, CreateMarkDownCapabilities(), DisposalToken); + var hover = await serviceAccessor.GetHoverInfoAsync("file.cshtml", codeDocument, code.Position, CreateMarkDownCapabilities(), DisposalToken); // Assert Assert.Null(hover); @@ -238,10 +230,9 @@ public async Task GetHoverInfo_TagHelper_AfterAttributeEquals_ReturnsNull() var codeDocument = CreateCodeDocument(code.Text, isRazorFile: false, DefaultTagHelpers); var service = GetHoverService(); var serviceAccessor = service.GetTestAccessor(); - var location = new SourceLocation(code.Position, lineIndex: -1, characterIndex: -1); // Act - var hover = await serviceAccessor.GetHoverInfoAsync("file.cshtml", codeDocument, location, CreateMarkDownCapabilities(), DisposalToken); + var hover = await serviceAccessor.GetHoverInfoAsync("file.cshtml", codeDocument, code.Position, CreateMarkDownCapabilities(), DisposalToken); // Assert Assert.Null(hover); @@ -259,10 +250,9 @@ public async Task GetHoverInfo_TagHelper_AttributeEnd_ReturnsNull() var codeDocument = CreateCodeDocument(code.Text, isRazorFile: false, DefaultTagHelpers); var service = GetHoverService(); var serviceAccessor = service.GetTestAccessor(); - var location = new SourceLocation(code.Position, lineIndex: -1, characterIndex: -1); // Act - var hover = await serviceAccessor.GetHoverInfoAsync("file.cshtml", codeDocument, location, CreateMarkDownCapabilities(), DisposalToken); + var hover = await serviceAccessor.GetHoverInfoAsync("file.cshtml", codeDocument, code.Position, CreateMarkDownCapabilities(), DisposalToken); // Assert Assert.Null(hover); @@ -280,10 +270,9 @@ public async Task GetHoverInfo_TagHelper_MinimizedAttribute() var codeDocument = CreateCodeDocument(code.Text, isRazorFile: false, DefaultTagHelpers); var service = GetHoverService(); var serviceAccessor = service.GetTestAccessor(); - var location = new SourceLocation(code.Position, lineIndex: -1, characterIndex: -1); // Act - var hover = await serviceAccessor.GetHoverInfoAsync("file.cshtml", codeDocument, location, CreateMarkDownCapabilities(), DisposalToken); + var hover = await serviceAccessor.GetHoverInfoAsync("file.cshtml", codeDocument, code.Position, CreateMarkDownCapabilities(), DisposalToken); // Assert Assert.NotNull(hover); @@ -310,10 +299,9 @@ public void Increment(){ var codeDocument = CreateCodeDocument(code.Text, "text.razor", DefaultTagHelpers); var service = GetHoverService(); var serviceAccessor = service.GetTestAccessor(); - var location = new SourceLocation(code.Position, lineIndex: -1, characterIndex: -1); // Act - var hover = await serviceAccessor.GetHoverInfoAsync("file.cshtml", codeDocument, location, CreateMarkDownCapabilities(), DisposalToken); + var hover = await serviceAccessor.GetHoverInfoAsync("file.cshtml", codeDocument, code.Position, CreateMarkDownCapabilities(), DisposalToken); // Assert Assert.NotNull(hover); @@ -335,10 +323,9 @@ public async Task GetHoverInfo_TagHelper_MalformedElement() var codeDocument = CreateCodeDocument(code.Text, isRazorFile: false, DefaultTagHelpers); var service = GetHoverService(); var serviceAccessor = service.GetTestAccessor(); - var location = new SourceLocation(code.Position, lineIndex: -1, characterIndex: -1); // Act - var hover = await serviceAccessor.GetHoverInfoAsync("file.cshtml", codeDocument, location, CreateMarkDownCapabilities(), DisposalToken); + var hover = await serviceAccessor.GetHoverInfoAsync("file.cshtml", codeDocument, code.Position, CreateMarkDownCapabilities(), DisposalToken); // Assert Assert.NotNull(hover); @@ -360,10 +347,9 @@ public async Task GetHoverInfo_TagHelper_MalformedAttribute() var codeDocument = CreateCodeDocument(code.Text, isRazorFile: false, DefaultTagHelpers); var service = GetHoverService(); var serviceAccessor = service.GetTestAccessor(); - var location = new SourceLocation(code.Position, lineIndex: -1, characterIndex: -1); // Act - var hover = await serviceAccessor.GetHoverInfoAsync("file.cshtml", codeDocument, location, CreateMarkDownCapabilities(), DisposalToken); + var hover = await serviceAccessor.GetHoverInfoAsync("file.cshtml", codeDocument, code.Position, CreateMarkDownCapabilities(), DisposalToken); // Assert Assert.NotNull(hover); @@ -386,10 +372,9 @@ public async Task GetHoverInfo_HTML_MarkupElement() var codeDocument = CreateCodeDocument(code.Text, isRazorFile: false); var service = GetHoverService(); var serviceAccessor = service.GetTestAccessor(); - var location = new SourceLocation(code.Position, lineIndex: -1, characterIndex: -1); // Act - var hover = await serviceAccessor.GetHoverInfoAsync("file.cshtml", codeDocument, location, CreateMarkDownCapabilities(), DisposalToken); + var hover = await serviceAccessor.GetHoverInfoAsync("file.cshtml", codeDocument, code.Position, CreateMarkDownCapabilities(), DisposalToken); // Assert Assert.Null(hover); @@ -408,10 +393,9 @@ public async Task GetHoverInfo_TagHelper_PlainTextElement() var service = GetHoverService(); var serviceAccessor = service.GetTestAccessor(); - var location = new SourceLocation(code.Position, lineIndex: -1, characterIndex: -1); // Act - var hover = await serviceAccessor.GetHoverInfoAsync("file.cshtml", codeDocument, location, CreatePlainTextCapabilities(), DisposalToken); + var hover = await serviceAccessor.GetHoverInfoAsync("file.cshtml", codeDocument, code.Position, CreatePlainTextCapabilities(), DisposalToken); // Assert Assert.NotNull(hover); @@ -435,10 +419,9 @@ public async Task GetHoverInfo_TagHelper_PlainTextElement_EndTag() var service = GetHoverService(); var serviceAccessor = service.GetTestAccessor(); - var location = new SourceLocation(code.Position, lineIndex: -1, characterIndex: -1); // Act - var hover = await serviceAccessor.GetHoverInfoAsync("file.cshtml", codeDocument, location, CreatePlainTextCapabilities(), DisposalToken); + var hover = await serviceAccessor.GetHoverInfoAsync("file.cshtml", codeDocument, code.Position, CreatePlainTextCapabilities(), DisposalToken); // Assert Assert.NotNull(hover); @@ -461,10 +444,9 @@ public async Task GetHoverInfo_TagHelper_TextComponent() var service = GetHoverService(); var serviceAccessor = service.GetTestAccessor(); - var location = new SourceLocation(code.Position, lineIndex: -1, characterIndex: -1); // Act - var hover = await serviceAccessor.GetHoverInfoAsync("file.razor", codeDocument, location, CreatePlainTextCapabilities(), DisposalToken); + var hover = await serviceAccessor.GetHoverInfoAsync("file.razor", codeDocument, code.Position, CreatePlainTextCapabilities(), DisposalToken); // Assert Assert.NotNull(hover); @@ -489,10 +471,9 @@ public async Task GetHoverInfo_TagHelper_TextComponent_NestedInHtml() var service = GetHoverService(); var serviceAccessor = service.GetTestAccessor(); - var location = new SourceLocation(code.Position, lineIndex: -1, characterIndex: -1); // Act - var hover = await serviceAccessor.GetHoverInfoAsync("file.razor", codeDocument, location, CreatePlainTextCapabilities(), DisposalToken); + var hover = await serviceAccessor.GetHoverInfoAsync("file.razor", codeDocument, code.Position, CreatePlainTextCapabilities(), DisposalToken); // Assert Assert.NotNull(hover); @@ -518,10 +499,9 @@ public async Task GetHoverInfo_TagHelper_TextComponent_NestedInCSharp() var service = GetHoverService(); var serviceAccessor = service.GetTestAccessor(); - var location = new SourceLocation(code.Position, lineIndex: -1, characterIndex: -1); // Act - var hover = await serviceAccessor.GetHoverInfoAsync("file.razor", codeDocument, location, CreatePlainTextCapabilities(), DisposalToken); + var hover = await serviceAccessor.GetHoverInfoAsync("file.razor", codeDocument, code.Position, CreatePlainTextCapabilities(), DisposalToken); // Assert Assert.Null(hover); @@ -544,10 +524,9 @@ public async Task GetHoverInfo_TagHelper_TextComponent_NestedInCSharpAndText() var service = GetHoverService(); var serviceAccessor = service.GetTestAccessor(); - var location = new SourceLocation(code.Position, lineIndex: -1, characterIndex: -1); // Act - var hover = await serviceAccessor.GetHoverInfoAsync("file.razor", codeDocument, location, CreatePlainTextCapabilities(), DisposalToken); + var hover = await serviceAccessor.GetHoverInfoAsync("file.razor", codeDocument, code.Position, CreatePlainTextCapabilities(), DisposalToken); // Assert Assert.NotNull(hover); @@ -571,10 +550,9 @@ public async Task GetHoverInfo_TagHelper_PlainTextAttribute() var service = GetHoverService(); var serviceAccessor = service.GetTestAccessor(); - var location = new SourceLocation(code.Position, lineIndex: -1, characterIndex: -1); // Act - var hover = await serviceAccessor.GetHoverInfoAsync("file.cshtml", codeDocument, location, CreatePlainTextCapabilities(), DisposalToken); + var hover = await serviceAccessor.GetHoverInfoAsync("file.cshtml", codeDocument, code.Position, CreatePlainTextCapabilities(), DisposalToken); // Assert Assert.NotNull(hover); @@ -599,10 +577,9 @@ public async Task GetHoverInfo_HTML_PlainTextElement() var service = GetHoverService(); var serviceAccessor = service.GetTestAccessor(); - var location = new SourceLocation(code.Position, lineIndex: -1, characterIndex: -1); // Act - var hover = await serviceAccessor.GetHoverInfoAsync("file.cshtml", codeDocument, location, CreatePlainTextCapabilities(), DisposalToken); + var hover = await serviceAccessor.GetHoverInfoAsync("file.cshtml", codeDocument, code.Position, CreatePlainTextCapabilities(), DisposalToken); // Assert Assert.Null(hover); @@ -622,10 +599,8 @@ public async Task GetHoverInfo_HTML_PlainTextAttribute() var service = GetHoverService(); var serviceAccessor = service.GetTestAccessor(); - var location = new SourceLocation(code.Position, lineIndex: -1, characterIndex: -1); - // Act - var hover = await serviceAccessor.GetHoverInfoAsync("file.cshtml", codeDocument, location, CreatePlainTextCapabilities(), DisposalToken); + var hover = await serviceAccessor.GetHoverInfoAsync("file.cshtml", codeDocument, code.Position, CreatePlainTextCapabilities(), DisposalToken); // Assert Assert.Null(hover); @@ -643,12 +618,11 @@ public async Task GetHoverInfo_TagHelper_Element_VSClient_ReturnVSHover() var codeDocument = CreateCodeDocument(code.Text, isRazorFile: false, DefaultTagHelpers); var service = GetHoverService(); var serviceAccessor = service.GetTestAccessor(); - var location = new SourceLocation(code.Position, lineIndex: -1, characterIndex: -1); var clientCapabilities = CreateMarkDownCapabilities(); clientCapabilities.SupportsVisualStudioExtensions = true; // Act - var vsHover = await serviceAccessor.GetHoverInfoAsync("file.cshtml", codeDocument, location, clientCapabilities, DisposalToken); + var vsHover = await serviceAccessor.GetHoverInfoAsync("file.cshtml", codeDocument, code.Position, clientCapabilities, DisposalToken); // Assert Assert.NotNull(vsHover); @@ -686,12 +660,11 @@ public async Task GetHoverInfo_TagHelper_Attribute_VSClient_ReturnVSHover() var service = GetHoverService(); var serviceAccessor = service.GetTestAccessor(); - var location = new SourceLocation(code.Position, lineIndex: -1, characterIndex: -1); var clientCapabilities = CreateMarkDownCapabilities(); clientCapabilities.SupportsVisualStudioExtensions = true; // Act - var vsHover = await serviceAccessor.GetHoverInfoAsync("file.cshtml", codeDocument, location, clientCapabilities, DisposalToken); + var vsHover = await serviceAccessor.GetHoverInfoAsync("file.cshtml", codeDocument, code.Position, clientCapabilities, DisposalToken); // Assert Assert.NotNull(vsHover); From 001b43d9a1acba60f8f44e6d2015c9dd1c5bd35c Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Mon, 28 Oct 2024 12:59:58 -0700 Subject: [PATCH 02/12] Extract HoverDisplayOptions from ClientCabilities Rather than passing around ClientCapabilities all the time, use a HoverDisplayOptions record with the relevant information. --- .../Hover/HoverDisplayOptions.cs | 30 +++++++ .../Hover/HoverService.TestAccessor.cs | 4 +- .../Hover/HoverService.cs | 48 +++++------ .../Hover/HoverServiceTest.cs | 81 ++++++++----------- 4 files changed, 88 insertions(+), 75 deletions(-) create mode 100644 src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hover/HoverDisplayOptions.cs diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hover/HoverDisplayOptions.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hover/HoverDisplayOptions.cs new file mode 100644 index 00000000000..89523065950 --- /dev/null +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hover/HoverDisplayOptions.cs @@ -0,0 +1,30 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.VisualStudio.LanguageServer.Protocol; + +namespace Microsoft.AspNetCore.Razor.LanguageServer.Hover; + +internal readonly record struct HoverDisplayOptions(MarkupKind MarkupKind, bool SupportsVisualStudioExtensions) +{ + public static HoverDisplayOptions From(ClientCapabilities clientCapabilities) + { + var markupKind = MarkupKind.PlainText; + + // If MarkDown is supported, we'll use that. + if (clientCapabilities.TextDocument?.Hover?.ContentFormat is MarkupKind[] contentFormat && + Array.IndexOf(contentFormat, MarkupKind.Markdown) >= 0) + { + markupKind = MarkupKind.Markdown; + } + + var supportsVisualStudioExtensions = (clientCapabilities as VSInternalClientCapabilities)?.SupportsVisualStudioExtensions ?? false; + + return new(markupKind, supportsVisualStudioExtensions); + } +} diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hover/HoverService.TestAccessor.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hover/HoverService.TestAccessor.cs index ab0e0aaafbb..29c6005c3ed 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hover/HoverService.TestAccessor.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hover/HoverService.TestAccessor.cs @@ -18,8 +18,8 @@ internal sealed class TestAccessor(HoverService instance) string documentFilePath, RazorCodeDocument codeDocument, int absoluteIndex, - VSInternalClientCapabilities clientCapabilities, + HoverDisplayOptions options, CancellationToken cancellationToken) - => instance.GetHoverInfoAsync(documentFilePath, codeDocument, absoluteIndex, clientCapabilities, cancellationToken); + => instance.GetHoverInfoAsync(documentFilePath, codeDocument, absoluteIndex, options, cancellationToken); } } diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hover/HoverService.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hover/HoverService.cs index a6044083472..d2aefd30e48 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hover/HoverService.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hover/HoverService.cs @@ -52,8 +52,10 @@ internal sealed partial class HoverService( return null; } + var options = HoverDisplayOptions.From(_clientCapabilitiesService.ClientCapabilities); + return await GetHoverInfoAsync( - documentContext.FilePath, codeDocument, positionInfo.HostDocumentIndex, _clientCapabilitiesService.ClientCapabilities, cancellationToken).ConfigureAwait(false); + documentContext.FilePath, codeDocument, positionInfo.HostDocumentIndex, options, cancellationToken).ConfigureAwait(false); } public async Task TranslateDelegatedResponseAsync(VSInternalHover? response, DocumentContext documentContext, DocumentPositionInfo positionInfo, CancellationToken cancellationToken) @@ -97,7 +99,7 @@ internal sealed partial class HoverService( string documentFilePath, RazorCodeDocument codeDocument, int absoluteIndex, - VSInternalClientCapabilities clientCapabilities, + HoverDisplayOptions options, CancellationToken cancellationToken) { var syntaxTree = codeDocument.GetSyntaxTree(); @@ -163,8 +165,7 @@ internal sealed partial class HoverService( var range = containingTagNameToken.GetRange(codeDocument.Source); - var result = await ElementInfoToHoverAsync(documentFilePath, binding.Descriptors, range, clientCapabilities, cancellationToken).ConfigureAwait(false); - return result; + return await ElementInfoToHoverAsync(documentFilePath, binding.Descriptors, range, options, cancellationToken).ConfigureAwait(false); } } @@ -239,16 +240,18 @@ internal sealed partial class HoverService( break; } - var attributeHoverModel = AttributeInfoToHover(tagHelperAttributes, range, attributeName, clientCapabilities); - - return attributeHoverModel; + return AttributeInfoToHover(tagHelperAttributes, attributeName, range, options); } } return null; } - private VSInternalHover? AttributeInfoToHover(ImmutableArray boundAttributes, Range range, string attributeName, VSInternalClientCapabilities clientCapabilities) + private static VSInternalHover? AttributeInfoToHover( + ImmutableArray boundAttributes, + string attributeName, + Range range, + HoverDisplayOptions options) { var descriptionInfos = boundAttributes.SelectAsArray(boundAttribute => { @@ -258,8 +261,8 @@ internal sealed partial class HoverService( var attrDescriptionInfo = new AggregateBoundAttributeDescription(descriptionInfos); - var isVSClient = clientCapabilities.SupportsVisualStudioExtensions; - if (isVSClient && ClassifiedTagHelperTooltipFactory.TryCreateTooltip(attrDescriptionInfo, out ContainerElement? classifiedTextElement)) + if (options.SupportsVisualStudioExtensions && + ClassifiedTagHelperTooltipFactory.TryCreateTooltip(attrDescriptionInfo, out ContainerElement? classifiedTextElement)) { var vsHover = new VSInternalHover { @@ -272,9 +275,7 @@ internal sealed partial class HoverService( } else { - var hoverContentFormat = GetHoverContentFormat(clientCapabilities); - - if (!MarkupTagHelperTooltipFactory.TryCreateTooltip(attrDescriptionInfo, hoverContentFormat, out var vsMarkupContent)) + if (!MarkupTagHelperTooltipFactory.TryCreateTooltip(attrDescriptionInfo, options.MarkupKind, out var vsMarkupContent)) { return null; } @@ -295,13 +296,17 @@ internal sealed partial class HoverService( } } - private async Task ElementInfoToHoverAsync(string documentFilePath, IEnumerable descriptors, Range range, VSInternalClientCapabilities clientCapabilities, CancellationToken cancellationToken) + private async Task ElementInfoToHoverAsync( + string documentFilePath, + IEnumerable descriptors, + Range range, + HoverDisplayOptions options, + CancellationToken cancellationToken) { var descriptionInfos = descriptors.SelectAsArray(BoundElementDescriptionInfo.From); var elementDescriptionInfo = new AggregateBoundElementDescription(descriptionInfos); - var isVSClient = clientCapabilities.SupportsVisualStudioExtensions; - if (isVSClient) + if (options.SupportsVisualStudioExtensions) { var classifiedTextElement = await ClassifiedTagHelperTooltipFactory .TryCreateTooltipContainerAsync(documentFilePath, elementDescriptionInfo, _projectManager.GetQueryOperations(), cancellationToken) @@ -320,10 +325,8 @@ internal sealed partial class HoverService( } } - var hoverContentFormat = GetHoverContentFormat(clientCapabilities); - var vsMarkupContent = await MarkupTagHelperTooltipFactory - .TryCreateTooltipAsync(documentFilePath, elementDescriptionInfo, _projectManager.GetQueryOperations(), hoverContentFormat, cancellationToken) + .TryCreateTooltipAsync(documentFilePath, elementDescriptionInfo, _projectManager.GetQueryOperations(), options.MarkupKind, cancellationToken) .ConfigureAwait(false); if (vsMarkupContent is null) @@ -345,11 +348,4 @@ internal sealed partial class HoverService( return hover; } - - private static MarkupKind GetHoverContentFormat(ClientCapabilities clientCapabilities) - { - var hoverContentFormat = clientCapabilities.TextDocument?.Hover?.ContentFormat; - var hoverKind = hoverContentFormat?.Contains(MarkupKind.Markdown) == true ? MarkupKind.Markdown : MarkupKind.PlainText; - return hoverKind; - } } diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Hover/HoverServiceTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Hover/HoverServiceTest.cs index d6fd287fe44..d6595e1aabc 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Hover/HoverServiceTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Hover/HoverServiceTest.cs @@ -30,23 +30,10 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer.Test.Hover; public class HoverServiceTest(ITestOutputHelper testOutput) : TagHelperServiceTestBase(testOutput) { - private static VSInternalClientCapabilities CreateMarkDownCapabilities() - => CreateCapabilities(MarkupKind.Markdown); + private static HoverDisplayOptions UseMarkdown => new(MarkupKind.Markdown, SupportsVisualStudioExtensions: false); + private static HoverDisplayOptions UsePlainText => new(MarkupKind.PlainText, SupportsVisualStudioExtensions: false); - private static VSInternalClientCapabilities CreatePlainTextCapabilities() - => CreateCapabilities(MarkupKind.PlainText); - - private static VSInternalClientCapabilities CreateCapabilities(MarkupKind markupKind) - => new() - { - TextDocument = new() - { - Hover = new() - { - ContentFormat = [markupKind], - } - } - }; + private static HoverDisplayOptions UseVisualStudio => new(MarkupKind.Markdown, SupportsVisualStudioExtensions: true); [Fact] public async Task GetHoverInfo_TagHelper_Element() @@ -62,7 +49,7 @@ public async Task GetHoverInfo_TagHelper_Element() var serviceAccessor = service.GetTestAccessor(); // Act - var hover = await serviceAccessor.GetHoverInfoAsync("file.cshtml", codeDocument, code.Position, CreateMarkDownCapabilities(), DisposalToken); + var hover = await serviceAccessor.GetHoverInfoAsync("file.cshtml", codeDocument, code.Position, UseMarkdown, DisposalToken); // Assert Assert.NotNull(hover); @@ -88,7 +75,7 @@ public async Task GetHoverInfo_TagHelper_Element_WithParent() var serviceAccessor = service.GetTestAccessor(); // Act - var hover = await serviceAccessor.GetHoverInfoAsync("file.cshtml", codeDocument, code.Position, CreateMarkDownCapabilities(), DisposalToken); + var hover = await serviceAccessor.GetHoverInfoAsync("file.cshtml", codeDocument, code.Position, UseMarkdown, DisposalToken); // Assert Assert.NotNull(hover); @@ -114,7 +101,7 @@ public async Task GetHoverInfo_TagHelper_Attribute_WithParent() var serviceAccessor = service.GetTestAccessor(); // Act - var hover = await serviceAccessor.GetHoverInfoAsync("file.cshtml", codeDocument, code.Position, CreateMarkDownCapabilities(), DisposalToken); + var hover = await serviceAccessor.GetHoverInfoAsync("file.cshtml", codeDocument, code.Position, UseMarkdown, DisposalToken); // Assert Assert.NotNull(hover); @@ -138,7 +125,7 @@ public async Task GetHoverInfo_TagHelper_Element_EndTag() var serviceAccessor = service.GetTestAccessor(); // Act - var hover = await serviceAccessor.GetHoverInfoAsync("file.cshtml", codeDocument, code.Position, CreateMarkDownCapabilities(), DisposalToken); + var hover = await serviceAccessor.GetHoverInfoAsync("file.cshtml", codeDocument, code.Position, UseMarkdown, DisposalToken); // Assert Assert.NotNull(hover); @@ -162,7 +149,7 @@ public async Task GetHoverInfo_TagHelper_Attribute() var serviceAccessor = service.GetTestAccessor(); // Act - var hover = await serviceAccessor.GetHoverInfoAsync("file.cshtml", codeDocument, code.Position, CreateMarkDownCapabilities(), DisposalToken); + var hover = await serviceAccessor.GetHoverInfoAsync("file.cshtml", codeDocument, code.Position, UseMarkdown, DisposalToken); // Assert Assert.NotNull(hover); @@ -187,7 +174,7 @@ public async Task GetHoverInfo_TagHelper_AttributeTrailingEdge() var serviceAccessor = service.GetTestAccessor(); // Act - var hover = await serviceAccessor.GetHoverInfoAsync("file.cshtml", codeDocument, code.Position, CreateMarkDownCapabilities(), DisposalToken); + var hover = await serviceAccessor.GetHoverInfoAsync("file.cshtml", codeDocument, code.Position, UseMarkdown, DisposalToken); // Assert Assert.NotNull(hover); @@ -212,7 +199,7 @@ public async Task GetHoverInfo_TagHelper_AttributeValue_ReturnsNull() var serviceAccessor = service.GetTestAccessor(); // Act - var hover = await serviceAccessor.GetHoverInfoAsync("file.cshtml", codeDocument, code.Position, CreateMarkDownCapabilities(), DisposalToken); + var hover = await serviceAccessor.GetHoverInfoAsync("file.cshtml", codeDocument, code.Position, UseMarkdown, DisposalToken); // Assert Assert.Null(hover); @@ -232,7 +219,7 @@ public async Task GetHoverInfo_TagHelper_AfterAttributeEquals_ReturnsNull() var serviceAccessor = service.GetTestAccessor(); // Act - var hover = await serviceAccessor.GetHoverInfoAsync("file.cshtml", codeDocument, code.Position, CreateMarkDownCapabilities(), DisposalToken); + var hover = await serviceAccessor.GetHoverInfoAsync("file.cshtml", codeDocument, code.Position, UseMarkdown, DisposalToken); // Assert Assert.Null(hover); @@ -252,7 +239,7 @@ public async Task GetHoverInfo_TagHelper_AttributeEnd_ReturnsNull() var serviceAccessor = service.GetTestAccessor(); // Act - var hover = await serviceAccessor.GetHoverInfoAsync("file.cshtml", codeDocument, code.Position, CreateMarkDownCapabilities(), DisposalToken); + var hover = await serviceAccessor.GetHoverInfoAsync("file.cshtml", codeDocument, code.Position, UseMarkdown, DisposalToken); // Assert Assert.Null(hover); @@ -272,7 +259,7 @@ public async Task GetHoverInfo_TagHelper_MinimizedAttribute() var serviceAccessor = service.GetTestAccessor(); // Act - var hover = await serviceAccessor.GetHoverInfoAsync("file.cshtml", codeDocument, code.Position, CreateMarkDownCapabilities(), DisposalToken); + var hover = await serviceAccessor.GetHoverInfoAsync("file.cshtml", codeDocument, code.Position, UseMarkdown, DisposalToken); // Assert Assert.NotNull(hover); @@ -301,7 +288,7 @@ public void Increment(){ var serviceAccessor = service.GetTestAccessor(); // Act - var hover = await serviceAccessor.GetHoverInfoAsync("file.cshtml", codeDocument, code.Position, CreateMarkDownCapabilities(), DisposalToken); + var hover = await serviceAccessor.GetHoverInfoAsync("file.cshtml", codeDocument, code.Position, UseMarkdown, DisposalToken); // Assert Assert.NotNull(hover); @@ -325,7 +312,7 @@ public async Task GetHoverInfo_TagHelper_MalformedElement() var serviceAccessor = service.GetTestAccessor(); // Act - var hover = await serviceAccessor.GetHoverInfoAsync("file.cshtml", codeDocument, code.Position, CreateMarkDownCapabilities(), DisposalToken); + var hover = await serviceAccessor.GetHoverInfoAsync("file.cshtml", codeDocument, code.Position, UseMarkdown, DisposalToken); // Assert Assert.NotNull(hover); @@ -349,7 +336,7 @@ public async Task GetHoverInfo_TagHelper_MalformedAttribute() var serviceAccessor = service.GetTestAccessor(); // Act - var hover = await serviceAccessor.GetHoverInfoAsync("file.cshtml", codeDocument, code.Position, CreateMarkDownCapabilities(), DisposalToken); + var hover = await serviceAccessor.GetHoverInfoAsync("file.cshtml", codeDocument, code.Position, UseMarkdown, DisposalToken); // Assert Assert.NotNull(hover); @@ -374,7 +361,7 @@ public async Task GetHoverInfo_HTML_MarkupElement() var serviceAccessor = service.GetTestAccessor(); // Act - var hover = await serviceAccessor.GetHoverInfoAsync("file.cshtml", codeDocument, code.Position, CreateMarkDownCapabilities(), DisposalToken); + var hover = await serviceAccessor.GetHoverInfoAsync("file.cshtml", codeDocument, code.Position, UseMarkdown, DisposalToken); // Assert Assert.Null(hover); @@ -395,7 +382,7 @@ public async Task GetHoverInfo_TagHelper_PlainTextElement() var serviceAccessor = service.GetTestAccessor(); // Act - var hover = await serviceAccessor.GetHoverInfoAsync("file.cshtml", codeDocument, code.Position, CreatePlainTextCapabilities(), DisposalToken); + var hover = await serviceAccessor.GetHoverInfoAsync("file.cshtml", codeDocument, code.Position, UsePlainText, DisposalToken); // Assert Assert.NotNull(hover); @@ -421,7 +408,7 @@ public async Task GetHoverInfo_TagHelper_PlainTextElement_EndTag() var serviceAccessor = service.GetTestAccessor(); // Act - var hover = await serviceAccessor.GetHoverInfoAsync("file.cshtml", codeDocument, code.Position, CreatePlainTextCapabilities(), DisposalToken); + var hover = await serviceAccessor.GetHoverInfoAsync("file.cshtml", codeDocument, code.Position, UsePlainText, DisposalToken); // Assert Assert.NotNull(hover); @@ -446,7 +433,7 @@ public async Task GetHoverInfo_TagHelper_TextComponent() var serviceAccessor = service.GetTestAccessor(); // Act - var hover = await serviceAccessor.GetHoverInfoAsync("file.razor", codeDocument, code.Position, CreatePlainTextCapabilities(), DisposalToken); + var hover = await serviceAccessor.GetHoverInfoAsync("file.razor", codeDocument, code.Position, UsePlainText, DisposalToken); // Assert Assert.NotNull(hover); @@ -473,7 +460,7 @@ public async Task GetHoverInfo_TagHelper_TextComponent_NestedInHtml() var serviceAccessor = service.GetTestAccessor(); // Act - var hover = await serviceAccessor.GetHoverInfoAsync("file.razor", codeDocument, code.Position, CreatePlainTextCapabilities(), DisposalToken); + var hover = await serviceAccessor.GetHoverInfoAsync("file.razor", codeDocument, code.Position, UsePlainText, DisposalToken); // Assert Assert.NotNull(hover); @@ -501,7 +488,7 @@ public async Task GetHoverInfo_TagHelper_TextComponent_NestedInCSharp() var serviceAccessor = service.GetTestAccessor(); // Act - var hover = await serviceAccessor.GetHoverInfoAsync("file.razor", codeDocument, code.Position, CreatePlainTextCapabilities(), DisposalToken); + var hover = await serviceAccessor.GetHoverInfoAsync("file.razor", codeDocument, code.Position, UsePlainText, DisposalToken); // Assert Assert.Null(hover); @@ -526,7 +513,7 @@ public async Task GetHoverInfo_TagHelper_TextComponent_NestedInCSharpAndText() var serviceAccessor = service.GetTestAccessor(); // Act - var hover = await serviceAccessor.GetHoverInfoAsync("file.razor", codeDocument, code.Position, CreatePlainTextCapabilities(), DisposalToken); + var hover = await serviceAccessor.GetHoverInfoAsync("file.razor", codeDocument, code.Position, UsePlainText, DisposalToken); // Assert Assert.NotNull(hover); @@ -552,7 +539,7 @@ public async Task GetHoverInfo_TagHelper_PlainTextAttribute() var serviceAccessor = service.GetTestAccessor(); // Act - var hover = await serviceAccessor.GetHoverInfoAsync("file.cshtml", codeDocument, code.Position, CreatePlainTextCapabilities(), DisposalToken); + var hover = await serviceAccessor.GetHoverInfoAsync("file.cshtml", codeDocument, code.Position, UsePlainText, DisposalToken); // Assert Assert.NotNull(hover); @@ -579,7 +566,7 @@ public async Task GetHoverInfo_HTML_PlainTextElement() var serviceAccessor = service.GetTestAccessor(); // Act - var hover = await serviceAccessor.GetHoverInfoAsync("file.cshtml", codeDocument, code.Position, CreatePlainTextCapabilities(), DisposalToken); + var hover = await serviceAccessor.GetHoverInfoAsync("file.cshtml", codeDocument, code.Position, UsePlainText, DisposalToken); // Assert Assert.Null(hover); @@ -600,7 +587,7 @@ public async Task GetHoverInfo_HTML_PlainTextAttribute() var serviceAccessor = service.GetTestAccessor(); // Act - var hover = await serviceAccessor.GetHoverInfoAsync("file.cshtml", codeDocument, code.Position, CreatePlainTextCapabilities(), DisposalToken); + var hover = await serviceAccessor.GetHoverInfoAsync("file.cshtml", codeDocument, code.Position, UsePlainText, DisposalToken); // Assert Assert.Null(hover); @@ -618,11 +605,9 @@ public async Task GetHoverInfo_TagHelper_Element_VSClient_ReturnVSHover() var codeDocument = CreateCodeDocument(code.Text, isRazorFile: false, DefaultTagHelpers); var service = GetHoverService(); var serviceAccessor = service.GetTestAccessor(); - var clientCapabilities = CreateMarkDownCapabilities(); - clientCapabilities.SupportsVisualStudioExtensions = true; // Act - var vsHover = await serviceAccessor.GetHoverInfoAsync("file.cshtml", codeDocument, code.Position, clientCapabilities, DisposalToken); + var vsHover = await serviceAccessor.GetHoverInfoAsync("file.cshtml", codeDocument, code.Position, UseVisualStudio, DisposalToken); // Assert Assert.NotNull(vsHover); @@ -660,11 +645,9 @@ public async Task GetHoverInfo_TagHelper_Attribute_VSClient_ReturnVSHover() var service = GetHoverService(); var serviceAccessor = service.GetTestAccessor(); - var clientCapabilities = CreateMarkDownCapabilities(); - clientCapabilities.SupportsVisualStudioExtensions = true; // Act - var vsHover = await serviceAccessor.GetHoverInfoAsync("file.cshtml", codeDocument, code.Position, clientCapabilities, DisposalToken); + var vsHover = await serviceAccessor.GetHoverInfoAsync("file.cshtml", codeDocument, code.Position, UseVisualStudio, DisposalToken); // Assert Assert.NotNull(vsHover); @@ -959,8 +942,12 @@ private HoverService GetHoverService(IDocumentMappingService? mappingService = n { var projectManager = CreateProjectSnapshotManager(); - var clientCapabilities = CreateMarkDownCapabilities(); - clientCapabilities.SupportsVisualStudioExtensions = true; + var clientCapabilities = new VSInternalClientCapabilities() + { + TextDocument = new() { Hover = new() { ContentFormat = [MarkupKind.PlainText, MarkupKind.Markdown] } }, + SupportsVisualStudioExtensions = true + }; + var clientCapabilitiesService = new TestClientCapabilitiesService(clientCapabilities); mappingService ??= StrictMock.Of(); From 98261ef52d16f24bc128b79144651d1254af2c91 Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Mon, 28 Oct 2024 13:23:15 -0700 Subject: [PATCH 03/12] Pass LinePositionSpan instead of Range Change HoverService to pass around LinePositionSpan throughtout and only create an LSP Range when needed in the end. --- .../Hover/HoverDisplayOptions.cs | 4 --- .../Hover/HoverService.cs | 28 +++++++++---------- .../Extensions/LinePositionExtensions.cs | 14 ++++++++++ .../Extensions/LinePositionSpanExtensions.cs | 14 ++++++++++ 4 files changed, 42 insertions(+), 18 deletions(-) diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hover/HoverDisplayOptions.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hover/HoverDisplayOptions.cs index 89523065950..449d6b18afc 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hover/HoverDisplayOptions.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hover/HoverDisplayOptions.cs @@ -2,10 +2,6 @@ // Licensed under the MIT license. See License.txt in the project root for license information. using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; using Microsoft.VisualStudio.LanguageServer.Protocol; namespace Microsoft.AspNetCore.Razor.LanguageServer.Hover; diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hover/HoverService.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hover/HoverService.cs index d2aefd30e48..504f3287a38 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hover/HoverService.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hover/HoverService.cs @@ -17,11 +17,10 @@ using Microsoft.CodeAnalysis.Razor.ProjectSystem; using Microsoft.CodeAnalysis.Razor.Protocol; using Microsoft.CodeAnalysis.Razor.Tooltip; +using Microsoft.CodeAnalysis.Text; using Microsoft.VisualStudio.Editor.Razor; using Microsoft.VisualStudio.LanguageServer.Protocol; using Microsoft.VisualStudio.Text.Adornments; -using MarkupKind = Microsoft.VisualStudio.LanguageServer.Protocol.MarkupKind; -using Range = Microsoft.VisualStudio.LanguageServer.Protocol.Range; namespace Microsoft.AspNetCore.Razor.LanguageServer.Hover; @@ -163,9 +162,9 @@ internal sealed partial class HoverService( { Debug.Assert(binding.Descriptors.Any()); - var range = containingTagNameToken.GetRange(codeDocument.Source); + var span = containingTagNameToken.GetLinePositionSpan(codeDocument.Source); - return await ElementInfoToHoverAsync(documentFilePath, binding.Descriptors, range, options, cancellationToken).ConfigureAwait(false); + return await ElementInfoToHoverAsync(documentFilePath, binding.Descriptors, span, options, cancellationToken).ConfigureAwait(false); } } @@ -223,24 +222,25 @@ internal sealed partial class HoverService( } var attributeName = attribute.GetContent(); - var range = attribute.GetRange(codeDocument.Source); + var span = attribute.GetLinePositionSpan(codeDocument.Source); // Include the @ in the range switch (attribute.Parent.Kind) { case SyntaxKind.MarkupTagHelperDirectiveAttribute: var directiveAttribute = (MarkupTagHelperDirectiveAttributeSyntax)attribute.Parent; - range.Start.Character -= directiveAttribute.Transition.FullWidth; + span = span.WithStart(start => start.WithCharacter(ch => ch - directiveAttribute.Transition.FullWidth)); attributeName = "@" + attributeName; break; + case SyntaxKind.MarkupMinimizedTagHelperDirectiveAttribute: var minimizedAttribute = (MarkupMinimizedTagHelperDirectiveAttributeSyntax)containingTag; - range.Start.Character -= minimizedAttribute.Transition.FullWidth; + span = span.WithStart(start => start.WithCharacter(ch => ch - minimizedAttribute.Transition.FullWidth)); attributeName = "@" + attributeName; break; } - return AttributeInfoToHover(tagHelperAttributes, attributeName, range, options); + return AttributeInfoToHover(tagHelperAttributes, attributeName, span, options); } } @@ -250,7 +250,7 @@ internal sealed partial class HoverService( private static VSInternalHover? AttributeInfoToHover( ImmutableArray boundAttributes, string attributeName, - Range range, + LinePositionSpan span, HoverDisplayOptions options) { var descriptionInfos = boundAttributes.SelectAsArray(boundAttribute => @@ -267,7 +267,7 @@ internal sealed partial class HoverService( var vsHover = new VSInternalHover { Contents = Array.Empty>(), - Range = range, + Range = span.ToRange(), RawContent = classifiedTextElement, }; @@ -289,7 +289,7 @@ internal sealed partial class HoverService( var hover = new VSInternalHover { Contents = markupContent, - Range = range, + Range = span.ToRange(), }; return hover; @@ -299,7 +299,7 @@ internal sealed partial class HoverService( private async Task ElementInfoToHoverAsync( string documentFilePath, IEnumerable descriptors, - Range range, + LinePositionSpan span, HoverDisplayOptions options, CancellationToken cancellationToken) { @@ -317,7 +317,7 @@ internal sealed partial class HoverService( var vsHover = new VSInternalHover { Contents = Array.Empty>(), - Range = range, + Range = span.ToRange(), RawContent = classifiedTextElement, }; @@ -343,7 +343,7 @@ internal sealed partial class HoverService( var hover = new VSInternalHover { Contents = markupContent, - Range = range + Range = span.ToRange() }; return hover; diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/LinePositionExtensions.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/LinePositionExtensions.cs index 45ae3813eeb..007f8bf4a91 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/LinePositionExtensions.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/LinePositionExtensions.cs @@ -1,6 +1,8 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT license. See License.txt in the project root for license information. +using System; + namespace Microsoft.CodeAnalysis.Text; internal static class LinePositionExtensions @@ -10,4 +12,16 @@ public static void Deconstruct(this LinePosition linePosition, out int line, out public static LinePositionSpan ToZeroWidthSpan(this LinePosition linePosition) => new(linePosition, linePosition); + + public static LinePosition WithLine(this LinePosition linePosition, int newLine) + => new(newLine, linePosition.Character); + + public static LinePosition WithLine(this LinePosition linePosition, Func computeNewLine) + => new(computeNewLine(linePosition.Line), linePosition.Character); + + public static LinePosition WithCharacter(this LinePosition linePosition, int newCharacter) + => new(linePosition.Line, newCharacter); + + public static LinePosition WithCharacter(this LinePosition linePosition, Func computeNewCharacter) + => new(linePosition.Line, computeNewCharacter(linePosition.Character)); } diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/LinePositionSpanExtensions.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/LinePositionSpanExtensions.cs index 4ae8ede32f2..4301daefa2a 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/LinePositionSpanExtensions.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/LinePositionSpanExtensions.cs @@ -1,6 +1,8 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT license. See License.txt in the project root for license information. +using System; + namespace Microsoft.CodeAnalysis.Text; internal static class LinePositionSpanExtensions @@ -46,4 +48,16 @@ public static bool Contains(this LinePositionSpan span, LinePositionSpan other) { return span.Start <= other.Start && span.End >= other.End; } + + public static LinePositionSpan WithStart(this LinePositionSpan span, LinePosition newStart) + => new(newStart, span.End); + + public static LinePositionSpan WithStart(this LinePositionSpan span, Func computeNewStart) + => new(computeNewStart(span.Start), span.End); + + public static LinePositionSpan WithEnd(this LinePositionSpan span, LinePosition newEnd) + => new(span.Start, newEnd); + + public static LinePositionSpan WithEnd(this LinePositionSpan span, Func computeNewEnd) + => new(span.Start, computeNewEnd(span.End)); } From 44f3fd79d75a22105fc2b51940bace4403194c63 Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Mon, 28 Oct 2024 13:29:19 -0700 Subject: [PATCH 04/12] Pass ISolutionQueryOperations throughtout Rather than accessing the _projectManager instance field in HoverService, just pass an ISolutionQueryOperations instance throughout. --- .../Hover/HoverService.TestAccessor.cs | 5 ++++- .../Hover/HoverService.cs | 21 +++++++++++++------ 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hover/HoverService.TestAccessor.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hover/HoverService.TestAccessor.cs index 29c6005c3ed..7ce54e161cf 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hover/HoverService.TestAccessor.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hover/HoverService.TestAccessor.cs @@ -4,6 +4,8 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.Language; +using Microsoft.AspNetCore.Razor.LanguageServer.ProjectSystem; +using Microsoft.CodeAnalysis.Razor.ProjectSystem; using Microsoft.VisualStudio.LanguageServer.Protocol; namespace Microsoft.AspNetCore.Razor.LanguageServer.Hover; @@ -20,6 +22,7 @@ internal sealed class TestAccessor(HoverService instance) int absoluteIndex, HoverDisplayOptions options, CancellationToken cancellationToken) - => instance.GetHoverInfoAsync(documentFilePath, codeDocument, absoluteIndex, options, cancellationToken); + => HoverService.GetHoverInfoAsync( + documentFilePath, codeDocument, absoluteIndex, options, instance._projectManager.GetQueryOperations(), cancellationToken); } } diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hover/HoverService.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hover/HoverService.cs index 504f3287a38..762d36c09bf 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hover/HoverService.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hover/HoverService.cs @@ -54,7 +54,13 @@ internal sealed partial class HoverService( var options = HoverDisplayOptions.From(_clientCapabilitiesService.ClientCapabilities); return await GetHoverInfoAsync( - documentContext.FilePath, codeDocument, positionInfo.HostDocumentIndex, options, cancellationToken).ConfigureAwait(false); + documentContext.FilePath, + codeDocument, + positionInfo.HostDocumentIndex, + options, + _projectManager.GetQueryOperations(), + cancellationToken) + .ConfigureAwait(false); } public async Task TranslateDelegatedResponseAsync(VSInternalHover? response, DocumentContext documentContext, DocumentPositionInfo positionInfo, CancellationToken cancellationToken) @@ -94,11 +100,12 @@ internal sealed partial class HoverService( return response; } - private async Task GetHoverInfoAsync( + private static async Task GetHoverInfoAsync( string documentFilePath, RazorCodeDocument codeDocument, int absoluteIndex, HoverDisplayOptions options, + ISolutionQueryOperations solutionQueryOperations, CancellationToken cancellationToken) { var syntaxTree = codeDocument.GetSyntaxTree(); @@ -164,7 +171,8 @@ internal sealed partial class HoverService( var span = containingTagNameToken.GetLinePositionSpan(codeDocument.Source); - return await ElementInfoToHoverAsync(documentFilePath, binding.Descriptors, span, options, cancellationToken).ConfigureAwait(false); + return await ElementInfoToHoverAsync( + documentFilePath, binding.Descriptors, span, options, solutionQueryOperations, cancellationToken).ConfigureAwait(false); } } @@ -296,11 +304,12 @@ internal sealed partial class HoverService( } } - private async Task ElementInfoToHoverAsync( + private static async Task ElementInfoToHoverAsync( string documentFilePath, IEnumerable descriptors, LinePositionSpan span, HoverDisplayOptions options, + ISolutionQueryOperations solutionQueryOperations, CancellationToken cancellationToken) { var descriptionInfos = descriptors.SelectAsArray(BoundElementDescriptionInfo.From); @@ -309,7 +318,7 @@ internal sealed partial class HoverService( if (options.SupportsVisualStudioExtensions) { var classifiedTextElement = await ClassifiedTagHelperTooltipFactory - .TryCreateTooltipContainerAsync(documentFilePath, elementDescriptionInfo, _projectManager.GetQueryOperations(), cancellationToken) + .TryCreateTooltipContainerAsync(documentFilePath, elementDescriptionInfo, solutionQueryOperations, cancellationToken) .ConfigureAwait(false); if (classifiedTextElement is not null) @@ -326,7 +335,7 @@ internal sealed partial class HoverService( } var vsMarkupContent = await MarkupTagHelperTooltipFactory - .TryCreateTooltipAsync(documentFilePath, elementDescriptionInfo, _projectManager.GetQueryOperations(), options.MarkupKind, cancellationToken) + .TryCreateTooltipAsync(documentFilePath, elementDescriptionInfo, solutionQueryOperations, options.MarkupKind, cancellationToken) .ConfigureAwait(false); if (vsMarkupContent is null) From 08973b451307626a65babb9fa613d68fa59a4e68 Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Mon, 28 Oct 2024 14:15:17 -0700 Subject: [PATCH 05/12] Some tweaks and clean up in HoverService --- .../Hover/HoverService.cs | 104 +++++++++--------- 1 file changed, 50 insertions(+), 54 deletions(-) diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hover/HoverService.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hover/HoverService.cs index 762d36c09bf..6eea153bf14 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hover/HoverService.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hover/HoverService.cs @@ -165,15 +165,13 @@ internal sealed partial class HoverService( // Hovered over a HTML tag name but the binding matches an attribute return null; } - else - { - Debug.Assert(binding.Descriptors.Any()); - var span = containingTagNameToken.GetLinePositionSpan(codeDocument.Source); + Debug.Assert(binding.Descriptors.Any()); - return await ElementInfoToHoverAsync( - documentFilePath, binding.Descriptors, span, options, solutionQueryOperations, cancellationToken).ConfigureAwait(false); - } + var span = containingTagNameToken.GetLinePositionSpan(codeDocument.Source); + + return await ElementInfoToHoverAsync( + documentFilePath, binding.Descriptors, span, options, solutionQueryOperations, cancellationToken).ConfigureAwait(false); } if (HtmlFacts.TryGetAttributeInfo(owner, out containingTagNameToken, out _, out var selectedAttributeName, out var selectedAttributeNameLocation, out attributes) && @@ -200,56 +198,54 @@ internal sealed partial class HoverService( // No matching TagHelpers, it's just HTML return null; } - else + + Debug.Assert(binding.Descriptors.Any()); + var tagHelperAttributes = TagHelperFacts.GetBoundTagHelperAttributes( + tagHelperDocumentContext, + selectedAttributeName.AssumeNotNull(), + binding); + + // Grab the first attribute that we find that intersects with this location. That way if there are multiple attributes side-by-side aka hovering over: + // + // Then we take the left most attribute (attributes are returned in source order). + var attribute = attributes.First(a => a.Span.IntersectsWith(absoluteIndex)); + if (attribute is MarkupTagHelperAttributeSyntax thAttributeSyntax) { - Debug.Assert(binding.Descriptors.Any()); - var tagHelperAttributes = TagHelperFacts.GetBoundTagHelperAttributes( - tagHelperDocumentContext, - selectedAttributeName.AssumeNotNull(), - binding); - - // Grab the first attribute that we find that intersects with this location. That way if there are multiple attributes side-by-side aka hovering over: - // - // Then we take the left most attribute (attributes are returned in source order). - var attribute = attributes.First(a => a.Span.IntersectsWith(absoluteIndex)); - if (attribute is MarkupTagHelperAttributeSyntax thAttributeSyntax) - { - attribute = thAttributeSyntax.Name; - } - else if (attribute is MarkupMinimizedTagHelperAttributeSyntax thMinimizedAttribute) - { - attribute = thMinimizedAttribute.Name; - } - else if (attribute is MarkupTagHelperDirectiveAttributeSyntax directiveAttribute) - { - attribute = directiveAttribute.Name; - } - else if (attribute is MarkupMinimizedTagHelperDirectiveAttributeSyntax miniDirectiveAttribute) - { - attribute = miniDirectiveAttribute; - } + attribute = thAttributeSyntax.Name; + } + else if (attribute is MarkupMinimizedTagHelperAttributeSyntax thMinimizedAttribute) + { + attribute = thMinimizedAttribute.Name; + } + else if (attribute is MarkupTagHelperDirectiveAttributeSyntax directiveAttribute) + { + attribute = directiveAttribute.Name; + } + else if (attribute is MarkupMinimizedTagHelperDirectiveAttributeSyntax miniDirectiveAttribute) + { + attribute = miniDirectiveAttribute; + } - var attributeName = attribute.GetContent(); - var span = attribute.GetLinePositionSpan(codeDocument.Source); + var attributeName = attribute.GetContent(); + var span = attribute.GetLinePositionSpan(codeDocument.Source); - // Include the @ in the range - switch (attribute.Parent.Kind) - { - case SyntaxKind.MarkupTagHelperDirectiveAttribute: - var directiveAttribute = (MarkupTagHelperDirectiveAttributeSyntax)attribute.Parent; - span = span.WithStart(start => start.WithCharacter(ch => ch - directiveAttribute.Transition.FullWidth)); - attributeName = "@" + attributeName; - break; - - case SyntaxKind.MarkupMinimizedTagHelperDirectiveAttribute: - var minimizedAttribute = (MarkupMinimizedTagHelperDirectiveAttributeSyntax)containingTag; - span = span.WithStart(start => start.WithCharacter(ch => ch - minimizedAttribute.Transition.FullWidth)); - attributeName = "@" + attributeName; - break; - } - - return AttributeInfoToHover(tagHelperAttributes, attributeName, span, options); + // Include the @ in the range + switch (attribute.Parent.Kind) + { + case SyntaxKind.MarkupTagHelperDirectiveAttribute: + var directiveAttribute = (MarkupTagHelperDirectiveAttributeSyntax)attribute.Parent; + span = span.WithStart(start => start.WithCharacter(ch => ch - directiveAttribute.Transition.FullWidth)); + attributeName = "@" + attributeName; + break; + + case SyntaxKind.MarkupMinimizedTagHelperDirectiveAttribute: + var minimizedAttribute = (MarkupMinimizedTagHelperDirectiveAttributeSyntax)containingTag; + span = span.WithStart(start => start.WithCharacter(ch => ch - minimizedAttribute.Transition.FullWidth)); + attributeName = "@" + attributeName; + break; } + + return AttributeInfoToHover(tagHelperAttributes, attributeName, span, options); } return null; @@ -306,7 +302,7 @@ internal sealed partial class HoverService( private static async Task ElementInfoToHoverAsync( string documentFilePath, - IEnumerable descriptors, + ImmutableArray descriptors, LinePositionSpan span, HoverDisplayOptions options, ISolutionQueryOperations solutionQueryOperations, From 8d53309985fca6b155b3fd8c9a3f1ba1cdf8ba97 Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Mon, 28 Oct 2024 15:12:51 -0700 Subject: [PATCH 06/12] Move HoverDisplayOptions to Workspaces --- .../Hover/HoverService.TestAccessor.cs | 1 + .../Hover/HoverService.cs | 1 + .../Hover/HoverDisplayOptions.cs | 2 +- .../Hover/HoverServiceTest.cs | 1 + 4 files changed, 4 insertions(+), 1 deletion(-) rename src/Razor/src/{Microsoft.AspNetCore.Razor.LanguageServer => Microsoft.CodeAnalysis.Razor.Workspaces}/Hover/HoverDisplayOptions.cs (94%) diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hover/HoverService.TestAccessor.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hover/HoverService.TestAccessor.cs index 7ce54e161cf..f45a39aec2e 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hover/HoverService.TestAccessor.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hover/HoverService.TestAccessor.cs @@ -5,6 +5,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.LanguageServer.ProjectSystem; +using Microsoft.CodeAnalysis.Razor.Hover; using Microsoft.CodeAnalysis.Razor.ProjectSystem; using Microsoft.VisualStudio.LanguageServer.Protocol; diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hover/HoverService.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hover/HoverService.cs index 6eea153bf14..41fc5179cd0 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hover/HoverService.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hover/HoverService.cs @@ -14,6 +14,7 @@ using Microsoft.AspNetCore.Razor.LanguageServer.ProjectSystem; using Microsoft.CodeAnalysis.Razor; using Microsoft.CodeAnalysis.Razor.DocumentMapping; +using Microsoft.CodeAnalysis.Razor.Hover; using Microsoft.CodeAnalysis.Razor.ProjectSystem; using Microsoft.CodeAnalysis.Razor.Protocol; using Microsoft.CodeAnalysis.Razor.Tooltip; diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hover/HoverDisplayOptions.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Hover/HoverDisplayOptions.cs similarity index 94% rename from src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hover/HoverDisplayOptions.cs rename to src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Hover/HoverDisplayOptions.cs index 449d6b18afc..a038979a057 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hover/HoverDisplayOptions.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Hover/HoverDisplayOptions.cs @@ -4,7 +4,7 @@ using System; using Microsoft.VisualStudio.LanguageServer.Protocol; -namespace Microsoft.AspNetCore.Razor.LanguageServer.Hover; +namespace Microsoft.CodeAnalysis.Razor.Hover; internal readonly record struct HoverDisplayOptions(MarkupKind MarkupKind, bool SupportsVisualStudioExtensions) { diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Hover/HoverServiceTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Hover/HoverServiceTest.cs index d6595e1aabc..c1570b922f1 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Hover/HoverServiceTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Hover/HoverServiceTest.cs @@ -15,6 +15,7 @@ using Microsoft.AspNetCore.Razor.Test.Common.ProjectSystem; using Microsoft.CodeAnalysis.Classification; using Microsoft.CodeAnalysis.Razor.DocumentMapping; +using Microsoft.CodeAnalysis.Razor.Hover; using Microsoft.CodeAnalysis.Razor.ProjectSystem; using Microsoft.CodeAnalysis.Razor.Protocol; using Microsoft.CodeAnalysis.Razor.Tooltip; From 908ed8258455c3ff903baf928c4373e47e7fc908 Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Mon, 28 Oct 2024 15:40:02 -0700 Subject: [PATCH 07/12] Move GetHoverAsync implementation to Workspaces --- .../Hover/HoverService.TestAccessor.cs | 8 +- .../Hover/HoverService.cs | 269 +----------------- .../Hover/HoverFactory.cs | 266 +++++++++++++++++ .../Hover/HoverServiceTest.cs | 50 ++-- 4 files changed, 296 insertions(+), 297 deletions(-) create mode 100644 src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Hover/HoverFactory.cs diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hover/HoverService.TestAccessor.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hover/HoverService.TestAccessor.cs index f45a39aec2e..82ccdb86a82 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hover/HoverService.TestAccessor.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hover/HoverService.TestAccessor.cs @@ -6,7 +6,6 @@ using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.LanguageServer.ProjectSystem; using Microsoft.CodeAnalysis.Razor.Hover; -using Microsoft.CodeAnalysis.Razor.ProjectSystem; using Microsoft.VisualStudio.LanguageServer.Protocol; namespace Microsoft.AspNetCore.Razor.LanguageServer.Hover; @@ -17,13 +16,12 @@ internal sealed partial class HoverService internal sealed class TestAccessor(HoverService instance) { - public Task GetHoverInfoAsync( - string documentFilePath, + public Task GetHoverAsync( RazorCodeDocument codeDocument, + string documentFilePath, int absoluteIndex, HoverDisplayOptions options, CancellationToken cancellationToken) - => HoverService.GetHoverInfoAsync( - documentFilePath, codeDocument, absoluteIndex, options, instance._projectManager.GetQueryOperations(), cancellationToken); + => HoverFactory.GetHoverAsync(codeDocument, documentFilePath, absoluteIndex, options, instance._projectManager.GetQueryOperations(), cancellationToken); } } diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hover/HoverService.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hover/HoverService.cs index 41fc5179cd0..cb71215835c 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hover/HoverService.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hover/HoverService.cs @@ -1,27 +1,16 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT license. See License.txt in the project root for license information. -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Diagnostics; -using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.Language; -using Microsoft.AspNetCore.Razor.Language.Legacy; -using Microsoft.AspNetCore.Razor.Language.Syntax; using Microsoft.AspNetCore.Razor.LanguageServer.ProjectSystem; using Microsoft.CodeAnalysis.Razor; using Microsoft.CodeAnalysis.Razor.DocumentMapping; using Microsoft.CodeAnalysis.Razor.Hover; using Microsoft.CodeAnalysis.Razor.ProjectSystem; using Microsoft.CodeAnalysis.Razor.Protocol; -using Microsoft.CodeAnalysis.Razor.Tooltip; -using Microsoft.CodeAnalysis.Text; -using Microsoft.VisualStudio.Editor.Razor; using Microsoft.VisualStudio.LanguageServer.Protocol; -using Microsoft.VisualStudio.Text.Adornments; namespace Microsoft.AspNetCore.Razor.LanguageServer.Hover; @@ -54,9 +43,9 @@ internal sealed partial class HoverService( var options = HoverDisplayOptions.From(_clientCapabilitiesService.ClientCapabilities); - return await GetHoverInfoAsync( - documentContext.FilePath, + return await HoverFactory.GetHoverAsync( codeDocument, + documentContext.FilePath, positionInfo.HostDocumentIndex, options, _projectManager.GetQueryOperations(), @@ -100,258 +89,4 @@ internal sealed partial class HoverService( return response; } - - private static async Task GetHoverInfoAsync( - string documentFilePath, - RazorCodeDocument codeDocument, - int absoluteIndex, - HoverDisplayOptions options, - ISolutionQueryOperations solutionQueryOperations, - CancellationToken cancellationToken) - { - var syntaxTree = codeDocument.GetSyntaxTree(); - - var owner = syntaxTree.Root.FindInnermostNode(absoluteIndex); - if (owner is null) - { - Debug.Fail("Owner should never be null."); - return null; - } - - // For cases where the point in the middle of an attribute, - // such as - // the node desired is the *AttributeSyntax - if (owner.Kind is SyntaxKind.MarkupTextLiteral) - { - owner = owner.Parent; - } - - var tagHelperDocumentContext = codeDocument.GetTagHelperContext(); - - // We want to find the parent tag, but looking up ancestors in the tree can find other things, - // for example when hovering over a start tag, the first ancestor is actually the element it - // belongs to, or in other words, the exact same tag! To work around this we just make sure we - // only check nodes that are at a different location in the file. - var ownerStart = owner.SpanStart; - - if (HtmlFacts.TryGetElementInfo(owner, out var containingTagNameToken, out var attributes, closingForwardSlashOrCloseAngleToken: out _) && - containingTagNameToken.Span.IntersectsWith(absoluteIndex)) - { - if (owner is MarkupStartTagSyntax or MarkupEndTagSyntax && - containingTagNameToken.Content.Equals(SyntaxConstants.TextTagName, StringComparison.OrdinalIgnoreCase)) - { - // It's possible for there to be a component that is in scope, and would be found by the GetTagHelperBinding - // call below, but a text tag, regardless of casing, inside C# code, is always just a text tag, not a component. - return null; - } - - // Hovering over HTML tag name - var ancestors = owner.Ancestors().Where(n => n.SpanStart != ownerStart); - var (parentTag, parentIsTagHelper) = TagHelperFacts.GetNearestAncestorTagInfo(ancestors); - var stringifiedAttributes = TagHelperFacts.StringifyAttributes(attributes); - var binding = TagHelperFacts.GetTagHelperBinding( - tagHelperDocumentContext, - containingTagNameToken.Content, - stringifiedAttributes, - parentTag: parentTag, - parentIsTagHelper: parentIsTagHelper); - - if (binding is null) - { - // No matching tagHelpers, it's just HTML - return null; - } - else if (binding.IsAttributeMatch) - { - // Hovered over a HTML tag name but the binding matches an attribute - return null; - } - - Debug.Assert(binding.Descriptors.Any()); - - var span = containingTagNameToken.GetLinePositionSpan(codeDocument.Source); - - return await ElementInfoToHoverAsync( - documentFilePath, binding.Descriptors, span, options, solutionQueryOperations, cancellationToken).ConfigureAwait(false); - } - - if (HtmlFacts.TryGetAttributeInfo(owner, out containingTagNameToken, out _, out var selectedAttributeName, out var selectedAttributeNameLocation, out attributes) && - selectedAttributeNameLocation?.IntersectsWith(absoluteIndex) == true) - { - // When finding parents for attributes, we make sure to find the parent of the containing tag, otherwise these methods - // would return the parent of the attribute, which is not helpful, as its just going to be the containing element - var containingTag = containingTagNameToken.Parent; - var ancestors = containingTag.Ancestors().Where(n => n.SpanStart != containingTag.SpanStart); - var (parentTag, parentIsTagHelper) = TagHelperFacts.GetNearestAncestorTagInfo(ancestors); - - // Hovering over HTML attribute name - var stringifiedAttributes = TagHelperFacts.StringifyAttributes(attributes); - - var binding = TagHelperFacts.GetTagHelperBinding( - tagHelperDocumentContext, - containingTagNameToken.Content, - stringifiedAttributes, - parentTag: parentTag, - parentIsTagHelper: parentIsTagHelper); - - if (binding is null) - { - // No matching TagHelpers, it's just HTML - return null; - } - - Debug.Assert(binding.Descriptors.Any()); - var tagHelperAttributes = TagHelperFacts.GetBoundTagHelperAttributes( - tagHelperDocumentContext, - selectedAttributeName.AssumeNotNull(), - binding); - - // Grab the first attribute that we find that intersects with this location. That way if there are multiple attributes side-by-side aka hovering over: - // - // Then we take the left most attribute (attributes are returned in source order). - var attribute = attributes.First(a => a.Span.IntersectsWith(absoluteIndex)); - if (attribute is MarkupTagHelperAttributeSyntax thAttributeSyntax) - { - attribute = thAttributeSyntax.Name; - } - else if (attribute is MarkupMinimizedTagHelperAttributeSyntax thMinimizedAttribute) - { - attribute = thMinimizedAttribute.Name; - } - else if (attribute is MarkupTagHelperDirectiveAttributeSyntax directiveAttribute) - { - attribute = directiveAttribute.Name; - } - else if (attribute is MarkupMinimizedTagHelperDirectiveAttributeSyntax miniDirectiveAttribute) - { - attribute = miniDirectiveAttribute; - } - - var attributeName = attribute.GetContent(); - var span = attribute.GetLinePositionSpan(codeDocument.Source); - - // Include the @ in the range - switch (attribute.Parent.Kind) - { - case SyntaxKind.MarkupTagHelperDirectiveAttribute: - var directiveAttribute = (MarkupTagHelperDirectiveAttributeSyntax)attribute.Parent; - span = span.WithStart(start => start.WithCharacter(ch => ch - directiveAttribute.Transition.FullWidth)); - attributeName = "@" + attributeName; - break; - - case SyntaxKind.MarkupMinimizedTagHelperDirectiveAttribute: - var minimizedAttribute = (MarkupMinimizedTagHelperDirectiveAttributeSyntax)containingTag; - span = span.WithStart(start => start.WithCharacter(ch => ch - minimizedAttribute.Transition.FullWidth)); - attributeName = "@" + attributeName; - break; - } - - return AttributeInfoToHover(tagHelperAttributes, attributeName, span, options); - } - - return null; - } - - private static VSInternalHover? AttributeInfoToHover( - ImmutableArray boundAttributes, - string attributeName, - LinePositionSpan span, - HoverDisplayOptions options) - { - var descriptionInfos = boundAttributes.SelectAsArray(boundAttribute => - { - var isIndexer = TagHelperMatchingConventions.SatisfiesBoundAttributeIndexer(boundAttribute, attributeName.AsSpan()); - return BoundAttributeDescriptionInfo.From(boundAttribute, isIndexer); - }); - - var attrDescriptionInfo = new AggregateBoundAttributeDescription(descriptionInfos); - - if (options.SupportsVisualStudioExtensions && - ClassifiedTagHelperTooltipFactory.TryCreateTooltip(attrDescriptionInfo, out ContainerElement? classifiedTextElement)) - { - var vsHover = new VSInternalHover - { - Contents = Array.Empty>(), - Range = span.ToRange(), - RawContent = classifiedTextElement, - }; - - return vsHover; - } - else - { - if (!MarkupTagHelperTooltipFactory.TryCreateTooltip(attrDescriptionInfo, options.MarkupKind, out var vsMarkupContent)) - { - return null; - } - - var markupContent = new MarkupContent() - { - Value = vsMarkupContent.Value, - Kind = vsMarkupContent.Kind, - }; - - var hover = new VSInternalHover - { - Contents = markupContent, - Range = span.ToRange(), - }; - - return hover; - } - } - - private static async Task ElementInfoToHoverAsync( - string documentFilePath, - ImmutableArray descriptors, - LinePositionSpan span, - HoverDisplayOptions options, - ISolutionQueryOperations solutionQueryOperations, - CancellationToken cancellationToken) - { - var descriptionInfos = descriptors.SelectAsArray(BoundElementDescriptionInfo.From); - var elementDescriptionInfo = new AggregateBoundElementDescription(descriptionInfos); - - if (options.SupportsVisualStudioExtensions) - { - var classifiedTextElement = await ClassifiedTagHelperTooltipFactory - .TryCreateTooltipContainerAsync(documentFilePath, elementDescriptionInfo, solutionQueryOperations, cancellationToken) - .ConfigureAwait(false); - - if (classifiedTextElement is not null) - { - var vsHover = new VSInternalHover - { - Contents = Array.Empty>(), - Range = span.ToRange(), - RawContent = classifiedTextElement, - }; - - return vsHover; - } - } - - var vsMarkupContent = await MarkupTagHelperTooltipFactory - .TryCreateTooltipAsync(documentFilePath, elementDescriptionInfo, solutionQueryOperations, options.MarkupKind, cancellationToken) - .ConfigureAwait(false); - - if (vsMarkupContent is null) - { - return null; - } - - var markupContent = new MarkupContent() - { - Value = vsMarkupContent.Value, - Kind = vsMarkupContent.Kind, - }; - - var hover = new VSInternalHover - { - Contents = markupContent, - Range = span.ToRange() - }; - - return hover; - } } diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Hover/HoverFactory.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Hover/HoverFactory.cs new file mode 100644 index 00000000000..76c4bba22d9 --- /dev/null +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Hover/HoverFactory.cs @@ -0,0 +1,266 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Razor; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.AspNetCore.Razor.Language.Legacy; +using Microsoft.AspNetCore.Razor.Language.Syntax; +using Microsoft.AspNetCore.Razor.Threading; +using Microsoft.CodeAnalysis.Razor.ProjectSystem; +using Microsoft.CodeAnalysis.Razor.Tooltip; +using Microsoft.CodeAnalysis.Text; +using Microsoft.VisualStudio.Editor.Razor; +using Microsoft.VisualStudio.LanguageServer.Protocol; +using Microsoft.VisualStudio.Text.Adornments; + +namespace Microsoft.CodeAnalysis.Razor.Hover; + +internal static class HoverFactory +{ + public static Task GetHoverAsync( + RazorCodeDocument codeDocument, + string documentFilePath, + int absoluteIndex, + HoverDisplayOptions options, + ISolutionQueryOperations solutionQueryOperations, + CancellationToken cancellationToken) + { + var syntaxTree = codeDocument.GetSyntaxTree(); + + var owner = syntaxTree.Root.FindInnermostNode(absoluteIndex); + if (owner is null) + { + Debug.Fail("Owner should never be null."); + return SpecializedTasks.Null(); + } + + // For cases where the point in the middle of an attribute, + // such as + // the node desired is the *AttributeSyntax + if (owner.Kind is SyntaxKind.MarkupTextLiteral) + { + owner = owner.Parent; + } + + var tagHelperDocumentContext = codeDocument.GetTagHelperContext(); + + if (HtmlFacts.TryGetElementInfo(owner, out var containingTagNameToken, out var attributes, closingForwardSlashOrCloseAngleToken: out _) && + containingTagNameToken.Span.IntersectsWith(absoluteIndex)) + { + if (owner is MarkupStartTagSyntax or MarkupEndTagSyntax && + containingTagNameToken.Content.Equals(SyntaxConstants.TextTagName, StringComparison.OrdinalIgnoreCase)) + { + // It's possible for there to be a component that is in scope, and would be found by the GetTagHelperBinding + // call below, but a text tag, regardless of casing, inside C# code, is always just a text tag, not a component. + return SpecializedTasks.Null(); + } + + // We want to find the parent tag, but looking up ancestors in the tree can find other things, + // for example when hovering over a start tag, the first ancestor is actually the element it + // belongs to, or in other words, the exact same tag! To work around this we just make sure we + // only check nodes that are at a different location in the file. + var ownerStart = owner.SpanStart; + + // Hovering over HTML tag name + var ancestors = owner.Ancestors().Where(n => n.SpanStart != ownerStart); + var (parentTag, parentIsTagHelper) = TagHelperFacts.GetNearestAncestorTagInfo(ancestors); + var stringifiedAttributes = TagHelperFacts.StringifyAttributes(attributes); + var binding = TagHelperFacts.GetTagHelperBinding( + tagHelperDocumentContext, + containingTagNameToken.Content, + stringifiedAttributes, + parentTag: parentTag, + parentIsTagHelper: parentIsTagHelper); + + if (binding is null) + { + // No matching tagHelpers, it's just HTML + return SpecializedTasks.Null(); + } + else if (binding.IsAttributeMatch) + { + // Hovered over a HTML tag name but the binding matches an attribute + return SpecializedTasks.Null(); + } + + Debug.Assert(binding.Descriptors.Any()); + + var span = containingTagNameToken.GetLinePositionSpan(codeDocument.Source); + + return ElementInfoToHoverAsync( + documentFilePath, binding.Descriptors, span, options, solutionQueryOperations, cancellationToken); + } + + if (HtmlFacts.TryGetAttributeInfo(owner, out containingTagNameToken, out _, out var selectedAttributeName, out var selectedAttributeNameLocation, out attributes) && + selectedAttributeNameLocation?.IntersectsWith(absoluteIndex) == true) + { + // When finding parents for attributes, we make sure to find the parent of the containing tag, otherwise these methods + // would return the parent of the attribute, which is not helpful, as its just going to be the containing element + var containingTag = containingTagNameToken.Parent; + var ancestors = containingTag.Ancestors().Where(n => n.SpanStart != containingTag.SpanStart); + var (parentTag, parentIsTagHelper) = TagHelperFacts.GetNearestAncestorTagInfo(ancestors); + + // Hovering over HTML attribute name + var stringifiedAttributes = TagHelperFacts.StringifyAttributes(attributes); + + var binding = TagHelperFacts.GetTagHelperBinding( + tagHelperDocumentContext, + containingTagNameToken.Content, + stringifiedAttributes, + parentTag: parentTag, + parentIsTagHelper: parentIsTagHelper); + + if (binding is null) + { + // No matching TagHelpers, it's just HTML + return SpecializedTasks.Null(); + } + + Debug.Assert(binding.Descriptors.Any()); + var tagHelperAttributes = TagHelperFacts.GetBoundTagHelperAttributes( + tagHelperDocumentContext, + selectedAttributeName.AssumeNotNull(), + binding); + + // Grab the first attribute that we find that intersects with this location. That way if there are multiple attributes side-by-side aka hovering over: + // + // Then we take the left most attribute (attributes are returned in source order). + var attribute = attributes.First(a => a.Span.IntersectsWith(absoluteIndex)); + if (attribute is MarkupTagHelperAttributeSyntax thAttributeSyntax) + { + attribute = thAttributeSyntax.Name; + } + else if (attribute is MarkupMinimizedTagHelperAttributeSyntax thMinimizedAttribute) + { + attribute = thMinimizedAttribute.Name; + } + else if (attribute is MarkupTagHelperDirectiveAttributeSyntax directiveAttribute) + { + attribute = directiveAttribute.Name; + } + else if (attribute is MarkupMinimizedTagHelperDirectiveAttributeSyntax miniDirectiveAttribute) + { + attribute = miniDirectiveAttribute; + } + + var attributeName = attribute.GetContent(); + var span = attribute.GetLinePositionSpan(codeDocument.Source); + + // Include the @ in the range + switch (attribute.Parent.Kind) + { + case SyntaxKind.MarkupTagHelperDirectiveAttribute: + var directiveAttribute = (MarkupTagHelperDirectiveAttributeSyntax)attribute.Parent; + span = span.WithStart(start => start.WithCharacter(ch => ch - directiveAttribute.Transition.FullWidth)); + attributeName = "@" + attributeName; + break; + + case SyntaxKind.MarkupMinimizedTagHelperDirectiveAttribute: + var minimizedAttribute = (MarkupMinimizedTagHelperDirectiveAttributeSyntax)containingTag; + span = span.WithStart(start => start.WithCharacter(ch => ch - minimizedAttribute.Transition.FullWidth)); + attributeName = "@" + attributeName; + break; + } + + return Task.FromResult(AttributeInfoToHover(tagHelperAttributes, attributeName, span, options)); + } + + return SpecializedTasks.Null(); + } + + private static VSInternalHover? AttributeInfoToHover( + ImmutableArray boundAttributes, + string attributeName, + LinePositionSpan span, + HoverDisplayOptions options) + { + var descriptionInfos = boundAttributes.SelectAsArray(boundAttribute => + { + var isIndexer = TagHelperMatchingConventions.SatisfiesBoundAttributeIndexer(boundAttribute, attributeName.AsSpan()); + return BoundAttributeDescriptionInfo.From(boundAttribute, isIndexer); + }); + + var attrDescriptionInfo = new AggregateBoundAttributeDescription(descriptionInfos); + + if (options.SupportsVisualStudioExtensions && + ClassifiedTagHelperTooltipFactory.TryCreateTooltip(attrDescriptionInfo, out ContainerElement? classifiedTextElement)) + { + return new VSInternalHover + { + Contents = Array.Empty>(), + Range = span.ToRange(), + RawContent = classifiedTextElement, + }; + } + + if (!MarkupTagHelperTooltipFactory.TryCreateTooltip(attrDescriptionInfo, options.MarkupKind, out var tooltipContent)) + { + return null; + } + + return new VSInternalHover + { + Contents = new MarkupContent() + { + Value = tooltipContent.Value, + Kind = tooltipContent.Kind, + }, + Range = span.ToRange(), + }; + } + + private static async Task ElementInfoToHoverAsync( + string documentFilePath, + ImmutableArray descriptors, + LinePositionSpan span, + HoverDisplayOptions options, + ISolutionQueryOperations solutionQueryOperations, + CancellationToken cancellationToken) + { + var descriptionInfos = descriptors.SelectAsArray(BoundElementDescriptionInfo.From); + var elementDescriptionInfo = new AggregateBoundElementDescription(descriptionInfos); + + if (options.SupportsVisualStudioExtensions) + { + var classifiedTextElement = await ClassifiedTagHelperTooltipFactory + .TryCreateTooltipContainerAsync(documentFilePath, elementDescriptionInfo, solutionQueryOperations, cancellationToken) + .ConfigureAwait(false); + + if (classifiedTextElement is not null) + { + return new VSInternalHover + { + Contents = Array.Empty>(), + Range = span.ToRange(), + RawContent = classifiedTextElement, + }; + } + } + + var tooltipContent = await MarkupTagHelperTooltipFactory + .TryCreateTooltipAsync(documentFilePath, elementDescriptionInfo, solutionQueryOperations, options.MarkupKind, cancellationToken) + .ConfigureAwait(false); + + if (tooltipContent is null) + { + return null; + } + + return new VSInternalHover + { + Contents = new MarkupContent() + { + Value = tooltipContent.Value, + Kind = tooltipContent.Kind, + }, + Range = span.ToRange() + }; + } +} diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Hover/HoverServiceTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Hover/HoverServiceTest.cs index c1570b922f1..bf0fbaee519 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Hover/HoverServiceTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Hover/HoverServiceTest.cs @@ -50,7 +50,7 @@ public async Task GetHoverInfo_TagHelper_Element() var serviceAccessor = service.GetTestAccessor(); // Act - var hover = await serviceAccessor.GetHoverInfoAsync("file.cshtml", codeDocument, code.Position, UseMarkdown, DisposalToken); + var hover = await serviceAccessor.GetHoverAsync(codeDocument, "file.cshtml", code.Position, UseMarkdown, DisposalToken); // Assert Assert.NotNull(hover); @@ -76,7 +76,7 @@ public async Task GetHoverInfo_TagHelper_Element_WithParent() var serviceAccessor = service.GetTestAccessor(); // Act - var hover = await serviceAccessor.GetHoverInfoAsync("file.cshtml", codeDocument, code.Position, UseMarkdown, DisposalToken); + var hover = await serviceAccessor.GetHoverAsync(codeDocument, "file.cshtml", code.Position, UseMarkdown, DisposalToken); // Assert Assert.NotNull(hover); @@ -102,7 +102,7 @@ public async Task GetHoverInfo_TagHelper_Attribute_WithParent() var serviceAccessor = service.GetTestAccessor(); // Act - var hover = await serviceAccessor.GetHoverInfoAsync("file.cshtml", codeDocument, code.Position, UseMarkdown, DisposalToken); + var hover = await serviceAccessor.GetHoverAsync(codeDocument, "file.cshtml", code.Position, UseMarkdown, DisposalToken); // Assert Assert.NotNull(hover); @@ -126,7 +126,7 @@ public async Task GetHoverInfo_TagHelper_Element_EndTag() var serviceAccessor = service.GetTestAccessor(); // Act - var hover = await serviceAccessor.GetHoverInfoAsync("file.cshtml", codeDocument, code.Position, UseMarkdown, DisposalToken); + var hover = await serviceAccessor.GetHoverAsync(codeDocument, "file.cshtml", code.Position, UseMarkdown, DisposalToken); // Assert Assert.NotNull(hover); @@ -150,7 +150,7 @@ public async Task GetHoverInfo_TagHelper_Attribute() var serviceAccessor = service.GetTestAccessor(); // Act - var hover = await serviceAccessor.GetHoverInfoAsync("file.cshtml", codeDocument, code.Position, UseMarkdown, DisposalToken); + var hover = await serviceAccessor.GetHoverAsync(codeDocument, "file.cshtml", code.Position, UseMarkdown, DisposalToken); // Assert Assert.NotNull(hover); @@ -175,7 +175,7 @@ public async Task GetHoverInfo_TagHelper_AttributeTrailingEdge() var serviceAccessor = service.GetTestAccessor(); // Act - var hover = await serviceAccessor.GetHoverInfoAsync("file.cshtml", codeDocument, code.Position, UseMarkdown, DisposalToken); + var hover = await serviceAccessor.GetHoverAsync(codeDocument, "file.cshtml", code.Position, UseMarkdown, DisposalToken); // Assert Assert.NotNull(hover); @@ -200,7 +200,7 @@ public async Task GetHoverInfo_TagHelper_AttributeValue_ReturnsNull() var serviceAccessor = service.GetTestAccessor(); // Act - var hover = await serviceAccessor.GetHoverInfoAsync("file.cshtml", codeDocument, code.Position, UseMarkdown, DisposalToken); + var hover = await serviceAccessor.GetHoverAsync(codeDocument, "file.cshtml", code.Position, UseMarkdown, DisposalToken); // Assert Assert.Null(hover); @@ -220,7 +220,7 @@ public async Task GetHoverInfo_TagHelper_AfterAttributeEquals_ReturnsNull() var serviceAccessor = service.GetTestAccessor(); // Act - var hover = await serviceAccessor.GetHoverInfoAsync("file.cshtml", codeDocument, code.Position, UseMarkdown, DisposalToken); + var hover = await serviceAccessor.GetHoverAsync(codeDocument, "file.cshtml", code.Position, UseMarkdown, DisposalToken); // Assert Assert.Null(hover); @@ -240,7 +240,7 @@ public async Task GetHoverInfo_TagHelper_AttributeEnd_ReturnsNull() var serviceAccessor = service.GetTestAccessor(); // Act - var hover = await serviceAccessor.GetHoverInfoAsync("file.cshtml", codeDocument, code.Position, UseMarkdown, DisposalToken); + var hover = await serviceAccessor.GetHoverAsync(codeDocument, "file.cshtml", code.Position, UseMarkdown, DisposalToken); // Assert Assert.Null(hover); @@ -260,7 +260,7 @@ public async Task GetHoverInfo_TagHelper_MinimizedAttribute() var serviceAccessor = service.GetTestAccessor(); // Act - var hover = await serviceAccessor.GetHoverInfoAsync("file.cshtml", codeDocument, code.Position, UseMarkdown, DisposalToken); + var hover = await serviceAccessor.GetHoverAsync(codeDocument, "file.cshtml", code.Position, UseMarkdown, DisposalToken); // Assert Assert.NotNull(hover); @@ -289,7 +289,7 @@ public void Increment(){ var serviceAccessor = service.GetTestAccessor(); // Act - var hover = await serviceAccessor.GetHoverInfoAsync("file.cshtml", codeDocument, code.Position, UseMarkdown, DisposalToken); + var hover = await serviceAccessor.GetHoverAsync(codeDocument, "file.cshtml", code.Position, UseMarkdown, DisposalToken); // Assert Assert.NotNull(hover); @@ -313,7 +313,7 @@ public async Task GetHoverInfo_TagHelper_MalformedElement() var serviceAccessor = service.GetTestAccessor(); // Act - var hover = await serviceAccessor.GetHoverInfoAsync("file.cshtml", codeDocument, code.Position, UseMarkdown, DisposalToken); + var hover = await serviceAccessor.GetHoverAsync(codeDocument, "file.cshtml", code.Position, UseMarkdown, DisposalToken); // Assert Assert.NotNull(hover); @@ -337,7 +337,7 @@ public async Task GetHoverInfo_TagHelper_MalformedAttribute() var serviceAccessor = service.GetTestAccessor(); // Act - var hover = await serviceAccessor.GetHoverInfoAsync("file.cshtml", codeDocument, code.Position, UseMarkdown, DisposalToken); + var hover = await serviceAccessor.GetHoverAsync(codeDocument, "file.cshtml", code.Position, UseMarkdown, DisposalToken); // Assert Assert.NotNull(hover); @@ -362,7 +362,7 @@ public async Task GetHoverInfo_HTML_MarkupElement() var serviceAccessor = service.GetTestAccessor(); // Act - var hover = await serviceAccessor.GetHoverInfoAsync("file.cshtml", codeDocument, code.Position, UseMarkdown, DisposalToken); + var hover = await serviceAccessor.GetHoverAsync(codeDocument, "file.cshtml", code.Position, UseMarkdown, DisposalToken); // Assert Assert.Null(hover); @@ -383,7 +383,7 @@ public async Task GetHoverInfo_TagHelper_PlainTextElement() var serviceAccessor = service.GetTestAccessor(); // Act - var hover = await serviceAccessor.GetHoverInfoAsync("file.cshtml", codeDocument, code.Position, UsePlainText, DisposalToken); + var hover = await serviceAccessor.GetHoverAsync(codeDocument, "file.cshtml", code.Position, UsePlainText, DisposalToken); // Assert Assert.NotNull(hover); @@ -409,7 +409,7 @@ public async Task GetHoverInfo_TagHelper_PlainTextElement_EndTag() var serviceAccessor = service.GetTestAccessor(); // Act - var hover = await serviceAccessor.GetHoverInfoAsync("file.cshtml", codeDocument, code.Position, UsePlainText, DisposalToken); + var hover = await serviceAccessor.GetHoverAsync(codeDocument, "file.cshtml", code.Position, UsePlainText, DisposalToken); // Assert Assert.NotNull(hover); @@ -434,7 +434,7 @@ public async Task GetHoverInfo_TagHelper_TextComponent() var serviceAccessor = service.GetTestAccessor(); // Act - var hover = await serviceAccessor.GetHoverInfoAsync("file.razor", codeDocument, code.Position, UsePlainText, DisposalToken); + var hover = await serviceAccessor.GetHoverAsync(codeDocument, "file.razor", code.Position, UsePlainText, DisposalToken); // Assert Assert.NotNull(hover); @@ -461,7 +461,7 @@ public async Task GetHoverInfo_TagHelper_TextComponent_NestedInHtml() var serviceAccessor = service.GetTestAccessor(); // Act - var hover = await serviceAccessor.GetHoverInfoAsync("file.razor", codeDocument, code.Position, UsePlainText, DisposalToken); + var hover = await serviceAccessor.GetHoverAsync(codeDocument, "file.razor", code.Position, UsePlainText, DisposalToken); // Assert Assert.NotNull(hover); @@ -489,7 +489,7 @@ public async Task GetHoverInfo_TagHelper_TextComponent_NestedInCSharp() var serviceAccessor = service.GetTestAccessor(); // Act - var hover = await serviceAccessor.GetHoverInfoAsync("file.razor", codeDocument, code.Position, UsePlainText, DisposalToken); + var hover = await serviceAccessor.GetHoverAsync(codeDocument, "file.razor", code.Position, UsePlainText, DisposalToken); // Assert Assert.Null(hover); @@ -514,7 +514,7 @@ public async Task GetHoverInfo_TagHelper_TextComponent_NestedInCSharpAndText() var serviceAccessor = service.GetTestAccessor(); // Act - var hover = await serviceAccessor.GetHoverInfoAsync("file.razor", codeDocument, code.Position, UsePlainText, DisposalToken); + var hover = await serviceAccessor.GetHoverAsync(codeDocument, "file.razor", code.Position, UsePlainText, DisposalToken); // Assert Assert.NotNull(hover); @@ -540,7 +540,7 @@ public async Task GetHoverInfo_TagHelper_PlainTextAttribute() var serviceAccessor = service.GetTestAccessor(); // Act - var hover = await serviceAccessor.GetHoverInfoAsync("file.cshtml", codeDocument, code.Position, UsePlainText, DisposalToken); + var hover = await serviceAccessor.GetHoverAsync(codeDocument, "file.cshtml", code.Position, UsePlainText, DisposalToken); // Assert Assert.NotNull(hover); @@ -567,7 +567,7 @@ public async Task GetHoverInfo_HTML_PlainTextElement() var serviceAccessor = service.GetTestAccessor(); // Act - var hover = await serviceAccessor.GetHoverInfoAsync("file.cshtml", codeDocument, code.Position, UsePlainText, DisposalToken); + var hover = await serviceAccessor.GetHoverAsync(codeDocument, "file.cshtml", code.Position, UsePlainText, DisposalToken); // Assert Assert.Null(hover); @@ -588,7 +588,7 @@ public async Task GetHoverInfo_HTML_PlainTextAttribute() var serviceAccessor = service.GetTestAccessor(); // Act - var hover = await serviceAccessor.GetHoverInfoAsync("file.cshtml", codeDocument, code.Position, UsePlainText, DisposalToken); + var hover = await serviceAccessor.GetHoverAsync(codeDocument, "file.cshtml", code.Position, UsePlainText, DisposalToken); // Assert Assert.Null(hover); @@ -608,7 +608,7 @@ public async Task GetHoverInfo_TagHelper_Element_VSClient_ReturnVSHover() var serviceAccessor = service.GetTestAccessor(); // Act - var vsHover = await serviceAccessor.GetHoverInfoAsync("file.cshtml", codeDocument, code.Position, UseVisualStudio, DisposalToken); + var vsHover = await serviceAccessor.GetHoverAsync(codeDocument, "file.cshtml", code.Position, UseVisualStudio, DisposalToken); // Assert Assert.NotNull(vsHover); @@ -648,7 +648,7 @@ public async Task GetHoverInfo_TagHelper_Attribute_VSClient_ReturnVSHover() var serviceAccessor = service.GetTestAccessor(); // Act - var vsHover = await serviceAccessor.GetHoverInfoAsync("file.cshtml", codeDocument, code.Position, UseVisualStudio, DisposalToken); + var vsHover = await serviceAccessor.GetHoverAsync(codeDocument, "file.cshtml", code.Position, UseVisualStudio, DisposalToken); // Assert Assert.NotNull(vsHover); From 494e0f3df52db30852d73b6bf01da91a644a65bb Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Mon, 28 Oct 2024 16:33:03 -0700 Subject: [PATCH 08/12] Move GetHoverAsync tests to Workspaces Moving the existing hover tests requires a few test infrastructure changes. --- .../TagHelperCompletionProviderTest.cs | 31 +- .../Completion/TagHelperServiceTestBase.cs | 281 +------- .../Hover/HoverServiceTest.cs | 649 ------------------ .../RazorCodeDocumentFactory.cs | 41 ++ .../SimpleTagHelpers.cs | 240 +++++++ .../Hover/HoverFactoryTest.cs | 605 ++++++++++++++++ 6 files changed, 911 insertions(+), 936 deletions(-) create mode 100644 src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/RazorCodeDocumentFactory.cs create mode 100644 src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/SimpleTagHelpers.cs create mode 100644 src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Hover/HoverFactoryTest.cs diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/TagHelperCompletionProviderTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/TagHelperCompletionProviderTest.cs index 174c2a11c51..e066aae54c4 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/TagHelperCompletionProviderTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/TagHelperCompletionProviderTest.cs @@ -21,8 +21,11 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer.Completion; public class TagHelperCompletionProviderTest(ITestOutputHelper testOutput) : TagHelperServiceTestBase(testOutput) { - private TagHelperCompletionProvider CreateTagHelperCompletionProvider() - => new(RazorTagHelperCompletionService, TestRazorLSPOptionsMonitor.Create()); + private static TagHelperCompletionProvider CreateTagHelperCompletionProvider() + => new(CreateTagHelperCompletionService(), TestRazorLSPOptionsMonitor.Create()); + + private static LspTagHelperCompletionService CreateTagHelperCompletionService() + => new(); [Fact] public void GetNearestAncestorTagInfo_MarkupElement() @@ -69,7 +72,7 @@ public void GetNearestAncestorTagInfo_TagHelperElement() public void GetCompletionAt_AtEmptyTagName_ReturnsCompletions() { // Arrange - var service = new TagHelperCompletionProvider(RazorTagHelperCompletionService, TestRazorLSPOptionsMonitor.Create()); + var service = new TagHelperCompletionProvider(CreateTagHelperCompletionService(), TestRazorLSPOptionsMonitor.Create()); var context = CreateRazorCompletionContext( """ @addTagHelper *, TestAssembly @@ -92,7 +95,7 @@ public void GetCompletionAt_AtEmptyTagName_ReturnsCompletions() public void GetCompletionAt_InEmptyDocument_ReturnsEmptyCompletionArray() { // Arrange - var service = new TagHelperCompletionProvider(RazorTagHelperCompletionService, TestRazorLSPOptionsMonitor.Create()); + var service = new TagHelperCompletionProvider(CreateTagHelperCompletionService(), TestRazorLSPOptionsMonitor.Create()); var context = CreateRazorCompletionContext( "$$", isRazorFile: true, @@ -109,7 +112,7 @@ public void GetCompletionAt_InEmptyDocument_ReturnsEmptyCompletionArray() public void GetCompletionAt_OutsideOfTagName_DoesNotReturnCompletions() { // Arrange - var service = new TagHelperCompletionProvider(RazorTagHelperCompletionService, TestRazorLSPOptionsMonitor.Create()); + var service = new TagHelperCompletionProvider(CreateTagHelperCompletionService(), TestRazorLSPOptionsMonitor.Create()); var context = CreateRazorCompletionContext( """ @addTagHelper *, TestAssembly @@ -129,7 +132,7 @@ public void GetCompletionAt_OutsideOfTagName_DoesNotReturnCompletions() public void GetCompletionAt_OutsideOfTagName_InsideCSharp_DoesNotReturnCompletions() { // Arrange - var service = new TagHelperCompletionProvider(RazorTagHelperCompletionService, TestRazorLSPOptionsMonitor.Create()); + var service = new TagHelperCompletionProvider(CreateTagHelperCompletionService(), TestRazorLSPOptionsMonitor.Create()); var context = CreateRazorCompletionContext( """ @addTagHelper *, TestAssembly @@ -153,7 +156,7 @@ public void GetCompletionAt_OutsideOfTagName_InsideCSharp_DoesNotReturnCompletio public void GetCompletionAt_SelfClosingTag_NotAtEndOfName_DoesNotReturnCompletions() { // Arrange - var service = new TagHelperCompletionProvider(RazorTagHelperCompletionService, TestRazorLSPOptionsMonitor.Create()); + var service = new TagHelperCompletionProvider(CreateTagHelperCompletionService(), TestRazorLSPOptionsMonitor.Create()); var context = CreateRazorCompletionContext( """ @addTagHelper *, TestAssembly @@ -173,7 +176,7 @@ public void GetCompletionAt_SelfClosingTag_NotAtEndOfName_DoesNotReturnCompletio public void GetCompletionAt_SelfClosingTag_ReturnsCompletions() { // Arrange - var service = new TagHelperCompletionProvider(RazorTagHelperCompletionService, TestRazorLSPOptionsMonitor.Create()); + var service = new TagHelperCompletionProvider(CreateTagHelperCompletionService(), TestRazorLSPOptionsMonitor.Create()); var context = CreateRazorCompletionContext( """ @addTagHelper *, TestAssembly @@ -193,7 +196,7 @@ public void GetCompletionAt_SelfClosingTag_ReturnsCompletions() public void GetCompletionAt_SelfClosingTag_InsideCSharp_ReturnsCompletions() { // Arrange - var service = new TagHelperCompletionProvider(RazorTagHelperCompletionService, TestRazorLSPOptionsMonitor.Create()); + var service = new TagHelperCompletionProvider(CreateTagHelperCompletionService(), TestRazorLSPOptionsMonitor.Create()); var context = CreateRazorCompletionContext( """ @addTagHelper *, TestAssembly @@ -217,7 +220,7 @@ public void GetCompletionAt_SelfClosingTag_InsideCSharp_ReturnsCompletions() public void GetCompletionAt_MalformedElement() { // Arrange - var service = new TagHelperCompletionProvider(RazorTagHelperCompletionService, TestRazorLSPOptionsMonitor.Create()); + var service = new TagHelperCompletionProvider(CreateTagHelperCompletionService(), TestRazorLSPOptionsMonitor.Create()); var context = CreateRazorCompletionContext( """ @addTagHelper *, TestAssembly @@ -238,7 +241,7 @@ public void GetCompletionAt_MalformedElement() public void GetCompletionAt_AtHtmlElementNameEdge_ReturnsCompletions() { // Arrange - var service = new TagHelperCompletionProvider(RazorTagHelperCompletionService, TestRazorLSPOptionsMonitor.Create()); + var service = new TagHelperCompletionProvider(CreateTagHelperCompletionService(), TestRazorLSPOptionsMonitor.Create()); var context = CreateRazorCompletionContext( """ @addTagHelper *, TestAssembly @@ -260,7 +263,7 @@ public void GetCompletionAt_AtHtmlElementNameEdge_ReturnsCompletions() public void GetCompletionAt_AtTagHelperElementNameEdge_ReturnsCompletions() { // Arrange - var service = new TagHelperCompletionProvider(RazorTagHelperCompletionService, TestRazorLSPOptionsMonitor.Create()); + var service = new TagHelperCompletionProvider(CreateTagHelperCompletionService(), TestRazorLSPOptionsMonitor.Create()); var context = CreateRazorCompletionContext( """ @addTagHelper *, TestAssembly @@ -317,7 +320,7 @@ public void GetCompletionAt_AtTagHelperElementNameEdge_ReturnsCompletions() public void GetCompletionAt_AtAttributeEdge_BothAttribute_ReturnsCompletions(string documentText) { // Arrange - var service = new TagHelperCompletionProvider(RazorTagHelperCompletionService, TestRazorLSPOptionsMonitor.Create()); + var service = new TagHelperCompletionProvider(CreateTagHelperCompletionService(), TestRazorLSPOptionsMonitor.Create()); var context = CreateRazorCompletionContext( documentText, isRazorFile: false, @@ -444,7 +447,7 @@ public async Task GetCompletionAt_InBody_WithoutSpace_ReturnsCompletions() // Arrange var options = TestRazorLSPOptionsMonitor.Create(); await options.UpdateAsync(options.CurrentValue with { CommitElementsWithSpace = false }, CancellationToken.None); - var service = new TagHelperCompletionProvider(RazorTagHelperCompletionService, options); + var service = new TagHelperCompletionProvider(CreateTagHelperCompletionService(), options); var context = CreateRazorCompletionContext( """ diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/TagHelperServiceTestBase.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/TagHelperServiceTestBase.cs index b17b2e08952..cf32191f51d 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/TagHelperServiceTestBase.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/TagHelperServiceTestBase.cs @@ -1,289 +1,24 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT license. See License.txt in the project root for license information. -using System; using System.Collections.Immutable; -using Microsoft.AspNetCore.Mvc.Razor.Extensions; using Microsoft.AspNetCore.Razor.Language; -using Microsoft.AspNetCore.Razor.Language.Components; +using Microsoft.AspNetCore.Razor.Test.Common; using Microsoft.AspNetCore.Razor.Test.Common.LanguageServer; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.Razor.Completion; -using Microsoft.NET.Sdk.Razor.SourceGenerators; using Xunit.Abstractions; -using static Microsoft.AspNetCore.Razor.Language.CommonMetadata; namespace Microsoft.AspNetCore.Razor.LanguageServer.Completion; -public abstract class TagHelperServiceTestBase : LanguageServerTestBase +public abstract class TagHelperServiceTestBase(ITestOutputHelper testOutput) : LanguageServerTestBase(testOutput) { - protected const string CSHtmlFile = "test.cshtml"; - protected const string RazorFile = "test.razor"; - - protected ImmutableArray DefaultTagHelpers { get; } - private protected ITagHelperCompletionService RazorTagHelperCompletionService { get; } - - public TagHelperServiceTestBase(ITestOutputHelper testOutput) - : base(testOutput) - { - var builder1 = TagHelperDescriptorBuilder.Create("Test1TagHelper", "TestAssembly"); - builder1.TagMatchingRule(rule => rule.TagName = "test1"); - builder1.SetMetadata(TypeName("Test1TagHelper")); - builder1.BindAttribute(attribute => - { - attribute.Name = "bool-val"; - attribute.SetMetadata(PropertyName("BoolVal")); - attribute.TypeName = typeof(bool).FullName; - }); - builder1.BindAttribute(attribute => - { - attribute.Name = "int-val"; - attribute.SetMetadata(PropertyName("IntVal")); - attribute.TypeName = typeof(int).FullName; - }); - - var builder1WithRequiredParent = TagHelperDescriptorBuilder.Create("Test1TagHelper.SomeChild", "TestAssembly"); - builder1WithRequiredParent.TagMatchingRule(rule => - { - rule.TagName = "SomeChild"; - rule.ParentTag = "test1"; - }); - builder1WithRequiredParent.SetMetadata(TypeName("Test1TagHelper.SomeChild")); - builder1WithRequiredParent.BindAttribute(attribute => - { - attribute.Name = "attribute"; - attribute.SetMetadata(PropertyName("Attribute")); - attribute.TypeName = typeof(string).FullName; - }); - - var builder2 = TagHelperDescriptorBuilder.Create("Test2TagHelper", "TestAssembly"); - builder2.TagMatchingRule(rule => rule.TagName = "test2"); - builder2.SetMetadata(TypeName("Test2TagHelper")); - builder2.BindAttribute(attribute => - { - attribute.Name = "bool-val"; - attribute.SetMetadata(PropertyName("BoolVal")); - attribute.TypeName = typeof(bool).FullName; - }); - builder2.BindAttribute(attribute => - { - attribute.Name = "int-val"; - attribute.SetMetadata(PropertyName("IntVal")); - attribute.TypeName = typeof(int).FullName; - }); - - var builder3 = TagHelperDescriptorBuilder.Create(ComponentMetadata.Component.TagHelperKind, "Component1TagHelper", "TestAssembly"); - builder3.TagMatchingRule(rule => rule.TagName = "Component1"); - builder3.SetMetadata( - TypeName("Component1"), - TypeNamespace("System"), // Just so we can reasonably assume a using directive is in place - TypeNameIdentifier("Component1"), - new(ComponentMetadata.Component.NameMatchKey, ComponentMetadata.Component.FullyQualifiedNameMatch)); - builder3.BindAttribute(attribute => - { - attribute.Name = "bool-val"; - attribute.SetMetadata(PropertyName("BoolVal")); - attribute.TypeName = typeof(bool).FullName; - }); - builder3.BindAttribute(attribute => - { - attribute.Name = "int-val"; - attribute.SetMetadata(PropertyName("IntVal")); - attribute.TypeName = typeof(int).FullName; - }); - builder3.BindAttribute(attribute => - { - attribute.Name = "Title"; - attribute.SetMetadata(PropertyName("Title")); - attribute.TypeName = typeof(string).FullName; - }); - - var textComponent = TagHelperDescriptorBuilder.Create(ComponentMetadata.Component.TagHelperKind, "TextTagHelper", "TestAssembly"); - textComponent.TagMatchingRule(rule => rule.TagName = "Text"); - textComponent.SetMetadata( - TypeName("Text"), - TypeNamespace("System"), - TypeNameIdentifier("Text"), - new(ComponentMetadata.Component.NameMatchKey, ComponentMetadata.Component.FullyQualifiedNameMatch)); - - var directiveAttribute1 = TagHelperDescriptorBuilder.Create(ComponentMetadata.Component.TagHelperKind, "TestDirectiveAttribute", "TestAssembly"); - directiveAttribute1.TagMatchingRule(rule => - { - rule.TagName = "*"; - rule.RequireAttributeDescriptor(b => - { - b.Name = "@test"; - b.NameComparisonMode = RequiredAttributeDescriptor.NameComparisonMode.PrefixMatch; - }); - }); - directiveAttribute1.TagMatchingRule(rule => - { - rule.TagName = "*"; - rule.RequireAttributeDescriptor(b => - { - b.Name = "@test"; - b.NameComparisonMode = RequiredAttributeDescriptor.NameComparisonMode.FullMatch; - }); - }); - directiveAttribute1.BindAttribute(attribute => - { - attribute.Name = "@test"; - attribute.SetMetadata(PropertyName("Test"), IsDirectiveAttribute); - attribute.TypeName = typeof(string).FullName; - - attribute.BindAttributeParameter(parameter => - { - parameter.Name = "something"; - parameter.TypeName = typeof(string).FullName; - - parameter.SetMetadata(PropertyName("Something")); - }); - }); - directiveAttribute1.SetMetadata( - MakeTrue(TagHelperMetadata.Common.ClassifyAttributesOnly), - new(ComponentMetadata.Component.NameMatchKey, ComponentMetadata.Component.FullyQualifiedNameMatch), - TypeName("TestDirectiveAttribute")); - - var directiveAttribute2 = TagHelperDescriptorBuilder.Create(ComponentMetadata.Component.TagHelperKind, "MinimizedDirectiveAttribute", "TestAssembly"); - directiveAttribute2.TagMatchingRule(rule => - { - rule.TagName = "*"; - rule.RequireAttributeDescriptor(b => - { - b.Name = "@minimized"; - b.NameComparisonMode = RequiredAttributeDescriptor.NameComparisonMode.PrefixMatch; - }); - }); - directiveAttribute2.TagMatchingRule(rule => - { - rule.TagName = "*"; - rule.RequireAttributeDescriptor(b => - { - b.Name = "@minimized"; - b.NameComparisonMode = RequiredAttributeDescriptor.NameComparisonMode.FullMatch; - }); - }); - directiveAttribute2.BindAttribute(attribute => - { - attribute.Name = "@minimized"; - attribute.SetMetadata(PropertyName("Minimized"), IsDirectiveAttribute); - attribute.TypeName = typeof(bool).FullName; - - attribute.BindAttributeParameter(parameter => - { - parameter.Name = "something"; - parameter.TypeName = typeof(string).FullName; - - parameter.SetMetadata(PropertyName("Something")); - }); - }); - directiveAttribute2.SetMetadata( - MakeTrue(TagHelperMetadata.Common.ClassifyAttributesOnly), - new(ComponentMetadata.Component.NameMatchKey, ComponentMetadata.Component.FullyQualifiedNameMatch), - TypeName("TestDirectiveAttribute")); - - var directiveAttribute3 = TagHelperDescriptorBuilder.Create(ComponentMetadata.EventHandler.TagHelperKind, "OnClickDirectiveAttribute", "TestAssembly"); - directiveAttribute3.TagMatchingRule(rule => - { - rule.TagName = "*"; - rule.RequireAttributeDescriptor(b => - { - b.Name = "@onclick"; - b.SetMetadata(MetadataCollection.Create(IsDirectiveAttribute)); - b.NameComparisonMode = RequiredAttributeDescriptor.NameComparisonMode.FullMatch; - }); - }); - directiveAttribute3.TagMatchingRule(rule => - { - rule.TagName = "*"; - rule.RequireAttributeDescriptor(b => - { - b.Name = "@onclick"; - b.SetMetadata(MetadataCollection.Create(IsDirectiveAttribute)); - b.NameComparisonMode = RequiredAttributeDescriptor.NameComparisonMode.PrefixMatch; - }); - }); - directiveAttribute3.BindAttribute(attribute => - { - attribute.Name = "@onclick"; - attribute.SetMetadata(PropertyName("onclick"), IsDirectiveAttribute, IsWeaklyTyped); - attribute.TypeName = "Microsoft.AspNetCore.Components.EventCallback"; - }); - directiveAttribute3.SetMetadata( - RuntimeName(ComponentMetadata.EventHandler.RuntimeName), - SpecialKind(ComponentMetadata.EventHandler.TagHelperKind), - new(ComponentMetadata.EventHandler.EventArgsType, "Microsoft.AspNetCore.Components.Web.MouseEventArgs"), - new(ComponentMetadata.Component.NameMatchKey, ComponentMetadata.Component.FullyQualifiedNameMatch), - MakeTrue(TagHelperMetadata.Common.ClassifyAttributesOnly), - TypeName("OnClickDirectiveAttribute"), - TypeNamespace("Microsoft.AspNetCore.Components.Web"), - TypeNameIdentifier("EventHandlers")); - - var htmlTagMutator = TagHelperDescriptorBuilder.Create("HtmlMutator", "TestAssembly"); - htmlTagMutator.TagMatchingRule(rule => - { - rule.TagName = "title"; - rule.RequireAttributeDescriptor(attributeRule => - { - attributeRule.Name = "mutator"; - }); - }); - htmlTagMutator.SetMetadata(TypeName("HtmlMutator")); - htmlTagMutator.BindAttribute(attribute => - { - attribute.Name = "Extra"; - attribute.SetMetadata(PropertyName("Extra")); - attribute.TypeName = typeof(bool).FullName; - }); - - DefaultTagHelpers = ImmutableArray.Create( - builder1.Build(), - builder1WithRequiredParent.Build(), - builder2.Build(), - builder3.Build(), - textComponent.Build(), - directiveAttribute1.Build(), - directiveAttribute2.Build(), - directiveAttribute3.Build(), - htmlTagMutator.Build()); - - RazorTagHelperCompletionService = new LspTagHelperCompletionService(); - } + protected static ImmutableArray DefaultTagHelpers => SimpleTagHelpers.Default; protected static string GetFileName(bool isRazorFile) - => isRazorFile ? RazorFile : CSHtmlFile; - - internal static RazorCodeDocument CreateCodeDocument(string text, bool isRazorFile, ImmutableArray tagHelpers) - { - return CreateCodeDocument(text, GetFileName(isRazorFile), tagHelpers); - } - - internal static RazorCodeDocument CreateCodeDocument(string text, bool isRazorFile, params TagHelperDescriptor[] tagHelpers) - { - return CreateCodeDocument(text, GetFileName(isRazorFile), tagHelpers); - } - - internal static RazorCodeDocument CreateCodeDocument(string text, string filePath, ImmutableArray tagHelpers) - { - tagHelpers = tagHelpers.NullToEmpty(); - - var sourceDocument = TestRazorSourceDocument.Create(text, filePath: filePath, relativePath: filePath); - var projectEngine = RazorProjectEngine.Create(builder => - { - builder.Features.Add(new ConfigureRazorParserOptions(useRoslynTokenizer: true, CSharpParseOptions.Default)); - RazorExtensions.Register(builder); - }); - var fileKind = filePath.EndsWith(".razor", StringComparison.Ordinal) ? FileKinds.Component : FileKinds.Legacy; - var codeDocument = projectEngine.ProcessDesignTime(sourceDocument, fileKind, importSources: default, tagHelpers); - - return codeDocument; - } + => RazorCodeDocumentFactory.GetFileName(isRazorFile); - internal static RazorCodeDocument CreateCodeDocument(string text, string filePath, params TagHelperDescriptor[] tagHelpers) - { - tagHelpers ??= Array.Empty(); + protected static RazorCodeDocument CreateCodeDocument(string text, bool isRazorFile, params ImmutableArray tagHelpers) + => RazorCodeDocumentFactory.CreateCodeDocument(text, isRazorFile, tagHelpers); - return CreateCodeDocument(text, filePath, tagHelpers.ToImmutableArray()); - } + protected static RazorCodeDocument CreateCodeDocument(string text, string filePath, params ImmutableArray tagHelpers) + => RazorCodeDocumentFactory.CreateCodeDocument(text, filePath, tagHelpers); } diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Hover/HoverServiceTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Hover/HoverServiceTest.cs index bf0fbaee519..0663e8665ad 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Hover/HoverServiceTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Hover/HoverServiceTest.cs @@ -13,12 +13,9 @@ using Microsoft.AspNetCore.Razor.Test.Common; using Microsoft.AspNetCore.Razor.Test.Common.LanguageServer; using Microsoft.AspNetCore.Razor.Test.Common.ProjectSystem; -using Microsoft.CodeAnalysis.Classification; using Microsoft.CodeAnalysis.Razor.DocumentMapping; -using Microsoft.CodeAnalysis.Razor.Hover; using Microsoft.CodeAnalysis.Razor.ProjectSystem; using Microsoft.CodeAnalysis.Razor.Protocol; -using Microsoft.CodeAnalysis.Razor.Tooltip; using Microsoft.CodeAnalysis.Razor.Workspaces; using Microsoft.CodeAnalysis.Text; using Microsoft.VisualStudio.LanguageServer.Protocol; @@ -31,652 +28,6 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer.Test.Hover; public class HoverServiceTest(ITestOutputHelper testOutput) : TagHelperServiceTestBase(testOutput) { - private static HoverDisplayOptions UseMarkdown => new(MarkupKind.Markdown, SupportsVisualStudioExtensions: false); - private static HoverDisplayOptions UsePlainText => new(MarkupKind.PlainText, SupportsVisualStudioExtensions: false); - - private static HoverDisplayOptions UseVisualStudio => new(MarkupKind.Markdown, SupportsVisualStudioExtensions: true); - - [Fact] - public async Task GetHoverInfo_TagHelper_Element() - { - // Arrange - TestCode code = """ - @addTagHelper *, TestAssembly - <$$test1> - """; - - var codeDocument = CreateCodeDocument(code.Text, isRazorFile: false, DefaultTagHelpers); - var service = GetHoverService(); - var serviceAccessor = service.GetTestAccessor(); - - // Act - var hover = await serviceAccessor.GetHoverAsync(codeDocument, "file.cshtml", code.Position, UseMarkdown, DisposalToken); - - // Assert - Assert.NotNull(hover); - Assert.NotNull(hover.Contents); - Assert.Contains("**Test1TagHelper**", ((MarkupContent)hover.Contents).Value, StringComparison.Ordinal); - var expectedRange = VsLspFactory.CreateSingleLineRange(line: 1, character: 1, length: 5); - Assert.Equal(expectedRange, hover.Range); - } - - [Fact] - public async Task GetHoverInfo_TagHelper_Element_WithParent() - { - // Arrange - TestCode code = """ - @addTagHelper *, TestAssembly - - - - """; - - var codeDocument = CreateCodeDocument(code.Text, isRazorFile: false, DefaultTagHelpers); - var service = GetHoverService(); - var serviceAccessor = service.GetTestAccessor(); - - // Act - var hover = await serviceAccessor.GetHoverAsync(codeDocument, "file.cshtml", code.Position, UseMarkdown, DisposalToken); - - // Assert - Assert.NotNull(hover); - Assert.NotNull(hover.Contents); - Assert.Contains("**SomeChild**", ((MarkupContent)hover.Contents).Value, StringComparison.Ordinal); - var expectedRange = VsLspFactory.CreateSingleLineRange(line: 2, character: 5, length: 9); - Assert.Equal(expectedRange, hover.Range); - } - - [Fact] - public async Task GetHoverInfo_TagHelper_Attribute_WithParent() - { - // Arrange - TestCode code = """ - @addTagHelper *, TestAssembly - - - - """; - - var codeDocument = CreateCodeDocument(code.Text, isRazorFile: false, DefaultTagHelpers); - var service = GetHoverService(); - var serviceAccessor = service.GetTestAccessor(); - - // Act - var hover = await serviceAccessor.GetHoverAsync(codeDocument, "file.cshtml", code.Position, UseMarkdown, DisposalToken); - - // Assert - Assert.NotNull(hover); - Assert.NotNull(hover.Contents); - Assert.Contains("**Attribute**", ((MarkupContent)hover.Contents).Value, StringComparison.Ordinal); - var expectedRange = codeDocument.Source.Text.GetRange(code.Span); - Assert.Equal(expectedRange, hover.Range); - } - - [Fact] - public async Task GetHoverInfo_TagHelper_Element_EndTag() - { - // Arrange - TestCode code = """ - @addTagHelper *, TestAssembly - - """; - - var codeDocument = CreateCodeDocument(code.Text, isRazorFile: false, DefaultTagHelpers); - var service = GetHoverService(); - var serviceAccessor = service.GetTestAccessor(); - - // Act - var hover = await serviceAccessor.GetHoverAsync(codeDocument, "file.cshtml", code.Position, UseMarkdown, DisposalToken); - - // Assert - Assert.NotNull(hover); - Assert.NotNull(hover.Contents); - Assert.Contains("**Test1TagHelper**", ((MarkupContent)hover.Contents).Value, StringComparison.Ordinal); - var expectedRange = VsLspFactory.CreateSingleLineRange(line: 1, character: 9, length: 5); - Assert.Equal(expectedRange, hover.Range); - } - - [Fact] - public async Task GetHoverInfo_TagHelper_Attribute() - { - // Arrange - TestCode code = """ - @addTagHelper *, TestAssembly - - """; - - var codeDocument = CreateCodeDocument(code.Text, isRazorFile: false, DefaultTagHelpers); - var service = GetHoverService(); - var serviceAccessor = service.GetTestAccessor(); - - // Act - var hover = await serviceAccessor.GetHoverAsync(codeDocument, "file.cshtml", code.Position, UseMarkdown, DisposalToken); - - // Assert - Assert.NotNull(hover); - Assert.NotNull(hover.Contents); - Assert.Contains("**BoolVal**", ((MarkupContent)hover.Contents).Value, StringComparison.Ordinal); - Assert.DoesNotContain("**IntVal**", ((MarkupContent)hover.Contents).Value, StringComparison.Ordinal); - var expectedRange = VsLspFactory.CreateSingleLineRange(line: 1, character: 7, length: 8); - Assert.Equal(expectedRange, hover.Range); - } - - [Fact] - public async Task GetHoverInfo_TagHelper_AttributeTrailingEdge() - { - // Arrange - TestCode code = """ - @addTagHelper *, TestAssembly - - """; - - var codeDocument = CreateCodeDocument(code.Text, isRazorFile: false, DefaultTagHelpers); - var service = GetHoverService(); - var serviceAccessor = service.GetTestAccessor(); - - // Act - var hover = await serviceAccessor.GetHoverAsync(codeDocument, "file.cshtml", code.Position, UseMarkdown, DisposalToken); - - // Assert - Assert.NotNull(hover); - Assert.NotNull(hover.Contents); - Assert.Contains("**BoolVal**", ((MarkupContent)hover.Contents).Value, StringComparison.Ordinal); - Assert.DoesNotContain("**IntVal**", ((MarkupContent)hover.Contents).Value, StringComparison.Ordinal); - var expectedRange = VsLspFactory.CreateSingleLineRange(line: 1, character: 7, length: 8); - Assert.Equal(expectedRange, hover.Range); - } - - [Fact] - public async Task GetHoverInfo_TagHelper_AttributeValue_ReturnsNull() - { - // Arrange - TestCode code = """ - @addTagHelper *, TestAssembly - - """; - - var codeDocument = CreateCodeDocument(code.Text, isRazorFile: false, DefaultTagHelpers); - var service = GetHoverService(); - var serviceAccessor = service.GetTestAccessor(); - - // Act - var hover = await serviceAccessor.GetHoverAsync(codeDocument, "file.cshtml", code.Position, UseMarkdown, DisposalToken); - - // Assert - Assert.Null(hover); - } - - [Fact] - public async Task GetHoverInfo_TagHelper_AfterAttributeEquals_ReturnsNull() - { - // Arrange - TestCode code = """ - @addTagHelper *, TestAssembly - - """; - - var codeDocument = CreateCodeDocument(code.Text, isRazorFile: false, DefaultTagHelpers); - var service = GetHoverService(); - var serviceAccessor = service.GetTestAccessor(); - - // Act - var hover = await serviceAccessor.GetHoverAsync(codeDocument, "file.cshtml", code.Position, UseMarkdown, DisposalToken); - - // Assert - Assert.Null(hover); - } - - [Fact] - public async Task GetHoverInfo_TagHelper_AttributeEnd_ReturnsNull() - { - // Arrange - TestCode code = """ - @addTagHelper *, TestAssembly - - """; - - var codeDocument = CreateCodeDocument(code.Text, isRazorFile: false, DefaultTagHelpers); - var service = GetHoverService(); - var serviceAccessor = service.GetTestAccessor(); - - // Act - var hover = await serviceAccessor.GetHoverAsync(codeDocument, "file.cshtml", code.Position, UseMarkdown, DisposalToken); - - // Assert - Assert.Null(hover); - } - - [Fact] - public async Task GetHoverInfo_TagHelper_MinimizedAttribute() - { - // Arrange - TestCode code = """ - @addTagHelper *, TestAssembly - - """; - - var codeDocument = CreateCodeDocument(code.Text, isRazorFile: false, DefaultTagHelpers); - var service = GetHoverService(); - var serviceAccessor = service.GetTestAccessor(); - - // Act - var hover = await serviceAccessor.GetHoverAsync(codeDocument, "file.cshtml", code.Position, UseMarkdown, DisposalToken); - - // Assert - Assert.NotNull(hover); - Assert.NotNull(hover.Contents); - Assert.Contains("**BoolVal**", ((MarkupContent)hover.Contents).Value, StringComparison.Ordinal); - Assert.DoesNotContain("**IntVal**", ((MarkupContent)hover.Contents).Value, StringComparison.Ordinal); - var expectedRange = VsLspFactory.CreateSingleLineRange(line: 1, character: 7, length: 8); - Assert.Equal(expectedRange, hover.Range); - } - - [Fact] - public async Task GetHoverInfo_DirectiveAttribute_HasResult() - { - // Arrange - TestCode code = """ - @addTagHelper *, TestAssembly - - @code{ - public void Increment(){ - } - } - """; - - var codeDocument = CreateCodeDocument(code.Text, "text.razor", DefaultTagHelpers); - var service = GetHoverService(); - var serviceAccessor = service.GetTestAccessor(); - - // Act - var hover = await serviceAccessor.GetHoverAsync(codeDocument, "file.cshtml", code.Position, UseMarkdown, DisposalToken); - - // Assert - Assert.NotNull(hover); - Assert.NotNull(hover.Contents); - Assert.Contains("**Test**", ((MarkupContent)hover.Contents).Value, StringComparison.Ordinal); - var expectedRange = VsLspFactory.CreateSingleLineRange(line: 1, character: 5, length: 5); - Assert.Equal(expectedRange, hover.Range); - } - - [Fact] - public async Task GetHoverInfo_TagHelper_MalformedElement() - { - // Arrange - TestCode code = """ - @addTagHelper *, TestAssembly - <$$test1 - """; - - var codeDocument = CreateCodeDocument(code.Text, isRazorFile: false, DefaultTagHelpers); - var service = GetHoverService(); - var serviceAccessor = service.GetTestAccessor(); - - // Act - var hover = await serviceAccessor.GetHoverAsync(codeDocument, "file.cshtml", code.Position, UseMarkdown, DisposalToken); - - // Assert - Assert.NotNull(hover); - Assert.NotNull(hover.Contents); - Assert.Contains("**BoolVal**", ((MarkupContent)hover.Contents).Value, StringComparison.Ordinal); - Assert.DoesNotContain("**IntVal**", ((MarkupContent)hover.Contents).Value, StringComparison.Ordinal); - var expectedRange = VsLspFactory.CreateSingleLineRange(line: 1, character: 7, length: 8); - Assert.Equal(expectedRange, hover.Range); - } - - [Fact] - public async Task GetHoverInfo_HTML_MarkupElement() - { - // Arrange - TestCode code = """ - @addTagHelper *, TestAssembly -

<$$strong>

- """; - - var codeDocument = CreateCodeDocument(code.Text, isRazorFile: false); - var service = GetHoverService(); - var serviceAccessor = service.GetTestAccessor(); - - // Act - var hover = await serviceAccessor.GetHoverAsync(codeDocument, "file.cshtml", code.Position, UseMarkdown, DisposalToken); - - // Assert - Assert.Null(hover); - } - - [Fact] - public async Task GetHoverInfo_TagHelper_PlainTextElement() - { - // Arrange - TestCode code = """ - @addTagHelper *, TestAssembly - <$$test1>
- """; - - var codeDocument = CreateCodeDocument(code.Text, isRazorFile: false, DefaultTagHelpers); - - var service = GetHoverService(); - var serviceAccessor = service.GetTestAccessor(); - - // Act - var hover = await serviceAccessor.GetHoverAsync(codeDocument, "file.cshtml", code.Position, UsePlainText, DisposalToken); - - // Assert - Assert.NotNull(hover); - Assert.NotNull(hover.Contents); - Assert.Contains("Test1TagHelper", ((MarkupContent)hover.Contents).Value, StringComparison.Ordinal); - Assert.Equal(MarkupKind.PlainText, ((MarkupContent)hover.Contents).Kind); - var expectedRange = VsLspFactory.CreateSingleLineRange(line: 1, character: 1, length: 5); - Assert.Equal(expectedRange, hover.Range); - } - - [Fact] - public async Task GetHoverInfo_TagHelper_PlainTextElement_EndTag() - { - // Arrange - TestCode code = """ - @addTagHelper *, TestAssembly - - """; - - var codeDocument = CreateCodeDocument(code.Text, isRazorFile: false, DefaultTagHelpers); - - var service = GetHoverService(); - var serviceAccessor = service.GetTestAccessor(); - - // Act - var hover = await serviceAccessor.GetHoverAsync(codeDocument, "file.cshtml", code.Position, UsePlainText, DisposalToken); - - // Assert - Assert.NotNull(hover); - Assert.NotNull(hover.Contents); - Assert.Contains("Test1TagHelper", ((MarkupContent)hover.Contents).Value, StringComparison.Ordinal); - Assert.Equal(MarkupKind.PlainText, ((MarkupContent)hover.Contents).Kind); - var expectedRange = VsLspFactory.CreateSingleLineRange(line: 1, character: 9, length: 5); - Assert.Equal(expectedRange, hover.Range); - } - - [Fact] - public async Task GetHoverInfo_TagHelper_TextComponent() - { - // Arrange - TestCode code = """ - <$$Text>
- """; - - var codeDocument = CreateCodeDocument(code.Text, isRazorFile: true, DefaultTagHelpers); - - var service = GetHoverService(); - var serviceAccessor = service.GetTestAccessor(); - - // Act - var hover = await serviceAccessor.GetHoverAsync(codeDocument, "file.razor", code.Position, UsePlainText, DisposalToken); - - // Assert - Assert.NotNull(hover); - Assert.NotNull(hover.Contents); - Assert.Contains("Text", ((MarkupContent)hover.Contents).Value, StringComparison.Ordinal); - Assert.Equal(MarkupKind.PlainText, ((MarkupContent)hover.Contents).Kind); - var expectedRange = VsLspFactory.CreateSingleLineRange(line: 0, character: 1, length: 4); - Assert.Equal(expectedRange, hover.Range); - } - - [Fact] - public async Task GetHoverInfo_TagHelper_TextComponent_NestedInHtml() - { - // Arrange - TestCode code = """ -
- <$$Text> -
- """; - - var codeDocument = CreateCodeDocument(code.Text, isRazorFile: true, DefaultTagHelpers); - - var service = GetHoverService(); - var serviceAccessor = service.GetTestAccessor(); - - // Act - var hover = await serviceAccessor.GetHoverAsync(codeDocument, "file.razor", code.Position, UsePlainText, DisposalToken); - - // Assert - Assert.NotNull(hover); - Assert.NotNull(hover.Contents); - Assert.Contains("Text", ((MarkupContent)hover.Contents).Value, StringComparison.Ordinal); - Assert.Equal(MarkupKind.PlainText, ((MarkupContent)hover.Contents).Kind); - var expectedRange = VsLspFactory.CreateSingleLineRange(line: 1, character: 5, length: 4); - Assert.Equal(expectedRange, hover.Range); - } - - [Fact] - public async Task GetHoverInfo_TagHelper_TextComponent_NestedInCSharp() - { - // Arrange - TestCode code = """ - @if (true) - { - <$$Text>
- } - """; - - var codeDocument = CreateCodeDocument(code.Text, isRazorFile: true, DefaultTagHelpers); - - var service = GetHoverService(); - var serviceAccessor = service.GetTestAccessor(); - - // Act - var hover = await serviceAccessor.GetHoverAsync(codeDocument, "file.razor", code.Position, UsePlainText, DisposalToken); - - // Assert - Assert.Null(hover); - } - - [Fact] - public async Task GetHoverInfo_TagHelper_TextComponent_NestedInCSharpAndText() - { - // Arrange - TestCode code = """ - @if (true) - { - - <$$Text> - - } - """; - - var codeDocument = CreateCodeDocument(code.Text, isRazorFile: true, DefaultTagHelpers); - - var service = GetHoverService(); - var serviceAccessor = service.GetTestAccessor(); - - // Act - var hover = await serviceAccessor.GetHoverAsync(codeDocument, "file.razor", code.Position, UsePlainText, DisposalToken); - - // Assert - Assert.NotNull(hover); - Assert.NotNull(hover.Contents); - Assert.Contains("Text", ((MarkupContent)hover.Contents).Value, StringComparison.Ordinal); - Assert.Equal(MarkupKind.PlainText, ((MarkupContent)hover.Contents).Kind); - var expectedRange = VsLspFactory.CreateSingleLineRange(line: 3, character: 9, length: 4); - Assert.Equal(expectedRange, hover.Range); - } - - [Fact] - public async Task GetHoverInfo_TagHelper_PlainTextAttribute() - { - // Arrange - TestCode code = """ - @addTagHelper *, TestAssembly - - """; - - var codeDocument = CreateCodeDocument(code.Text, isRazorFile: false, DefaultTagHelpers); - - var service = GetHoverService(); - var serviceAccessor = service.GetTestAccessor(); - - // Act - var hover = await serviceAccessor.GetHoverAsync(codeDocument, "file.cshtml", code.Position, UsePlainText, DisposalToken); - - // Assert - Assert.NotNull(hover); - Assert.NotNull(hover.Contents); - Assert.Contains("BoolVal", ((MarkupContent)hover.Contents).Value, StringComparison.Ordinal); - Assert.DoesNotContain("IntVal", ((MarkupContent)hover.Contents).Value, StringComparison.Ordinal); - Assert.Equal(MarkupKind.PlainText, ((MarkupContent)hover.Contents).Kind); - var expectedRange = VsLspFactory.CreateSingleLineRange(line: 1, character: 7, length: 8); - Assert.Equal(expectedRange, hover.Range); - } - - [Fact] - public async Task GetHoverInfo_HTML_PlainTextElement() - { - // Arrange - TestCode code = """ - @addTagHelper *, TestAssembly -

<$$strong>

- """; - - var codeDocument = CreateCodeDocument(code.Text, isRazorFile: false); - - var service = GetHoverService(); - var serviceAccessor = service.GetTestAccessor(); - - // Act - var hover = await serviceAccessor.GetHoverAsync(codeDocument, "file.cshtml", code.Position, UsePlainText, DisposalToken); - - // Assert - Assert.Null(hover); - } - - [Fact] - public async Task GetHoverInfo_HTML_PlainTextAttribute() - { - // Arrange - TestCode code = """ - @addTagHelper *, TestAssembly -

- """; - - var codeDocument = CreateCodeDocument(code.Text, isRazorFile: false); - - var service = GetHoverService(); - var serviceAccessor = service.GetTestAccessor(); - - // Act - var hover = await serviceAccessor.GetHoverAsync(codeDocument, "file.cshtml", code.Position, UsePlainText, DisposalToken); - - // Assert - Assert.Null(hover); - } - - [Fact] - public async Task GetHoverInfo_TagHelper_Element_VSClient_ReturnVSHover() - { - // Arrange - TestCode code = """ - @addTagHelper *, TestAssembly - <$$test1> - """; - - var codeDocument = CreateCodeDocument(code.Text, isRazorFile: false, DefaultTagHelpers); - var service = GetHoverService(); - var serviceAccessor = service.GetTestAccessor(); - - // Act - var vsHover = await serviceAccessor.GetHoverAsync(codeDocument, "file.cshtml", code.Position, UseVisualStudio, DisposalToken); - - // Assert - Assert.NotNull(vsHover); - Assert.NotNull(vsHover.Contents); - Assert.False(vsHover.Contents.Value.TryGetFourth(out var _)); - Assert.True(vsHover.Contents.Value.TryGetThird(out var _) && !vsHover.Contents.Value.Third.Any()); - var expectedRange = VsLspFactory.CreateSingleLineRange(line: 1, character: 1, length: 5); - Assert.Equal(expectedRange, vsHover.Range); - - Assert.NotNull(vsHover.RawContent); - var container = (ContainerElement)vsHover.RawContent; - var containerElements = container.Elements.ToList(); - Assert.Equal(ContainerElementStyle.Stacked, container.Style); - Assert.Single(containerElements); - - // [TagHelper Glyph] Test1TagHelper - var innerContainer = ((ContainerElement)containerElements[0]).Elements.ToList(); - var classifiedTextElement = (ClassifiedTextElement)innerContainer[1]; - Assert.Equal(2, innerContainer.Count); - Assert.Equal(ClassifiedTagHelperTooltipFactory.ClassGlyph, innerContainer[0]); - Assert.Collection(classifiedTextElement.Runs, - run => run.AssertExpectedClassification("Test1TagHelper", ClassifiedTagHelperTooltipFactory.TypeClassificationName)); - } - - [Fact] - public async Task GetHoverInfo_TagHelper_Attribute_VSClient_ReturnVSHover() - { - // Arrange - TestCode code = """ - @addTagHelper *, TestAssembly - - """; - - var codeDocument = CreateCodeDocument(code.Text, isRazorFile: false, DefaultTagHelpers); - - var service = GetHoverService(); - var serviceAccessor = service.GetTestAccessor(); - - // Act - var vsHover = await serviceAccessor.GetHoverAsync(codeDocument, "file.cshtml", code.Position, UseVisualStudio, DisposalToken); - - // Assert - Assert.NotNull(vsHover); - Assert.NotNull(vsHover.Contents); - Assert.False(vsHover.Contents.Value.TryGetFourth(out _)); - Assert.True(vsHover.Contents.Value.TryGetThird(out var markedStrings) && !markedStrings.Any()); - var expectedRange = VsLspFactory.CreateSingleLineRange(line: 1, character: 7, length: 8); - Assert.Equal(expectedRange, vsHover.Range); - - Assert.NotNull(vsHover.RawContent); - var container = (ContainerElement)vsHover.RawContent; - var containerElements = container.Elements.ToList(); - Assert.Equal(ContainerElementStyle.Stacked, container.Style); - Assert.Single(containerElements); - - // [TagHelper Glyph] bool Test1TagHelper.BoolVal - var innerContainer = ((ContainerElement)containerElements[0]).Elements.ToList(); - var classifiedTextElement = (ClassifiedTextElement)innerContainer[1]; - Assert.Equal(2, innerContainer.Count); - Assert.Equal(ClassifiedTagHelperTooltipFactory.PropertyGlyph, innerContainer[0]); - Assert.Collection(classifiedTextElement.Runs, - run => run.AssertExpectedClassification("bool", ClassificationTypeNames.Keyword), - run => run.AssertExpectedClassification(" ", ClassificationTypeNames.WhiteSpace), - run => run.AssertExpectedClassification("Test1TagHelper", ClassifiedTagHelperTooltipFactory.TypeClassificationName), - run => run.AssertExpectedClassification(".", ClassificationTypeNames.Punctuation), - run => run.AssertExpectedClassification("BoolVal", ClassificationTypeNames.Identifier)); - } - [Fact] public async Task Handle_Hover_SingleServer_CallsDelegatedLanguageServer() { diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/RazorCodeDocumentFactory.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/RazorCodeDocumentFactory.cs new file mode 100644 index 00000000000..17eae58f694 --- /dev/null +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/RazorCodeDocumentFactory.cs @@ -0,0 +1,41 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +using System.Collections.Immutable; +using Microsoft.AspNetCore.Mvc.Razor.Extensions; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.NET.Sdk.Razor.SourceGenerators; + +namespace Microsoft.AspNetCore.Razor.Test.Common; + +internal static class RazorCodeDocumentFactory +{ + private const string CSHtmlFile = "test.cshtml"; + private const string RazorFile = "test.razor"; + + public static string GetFileName(bool isRazorFile) + => isRazorFile ? RazorFile : CSHtmlFile; + + public static RazorCodeDocument CreateCodeDocument(string text, bool isRazorFile, params ImmutableArray tagHelpers) + { + return CreateCodeDocument(text, GetFileName(isRazorFile), tagHelpers); + } + + public static RazorCodeDocument CreateCodeDocument(string text, string filePath, params ImmutableArray tagHelpers) + { + tagHelpers = tagHelpers.NullToEmpty(); + + var sourceDocument = TestRazorSourceDocument.Create(text, filePath: filePath, relativePath: filePath); + var projectEngine = RazorProjectEngine.Create(builder => + { + builder.Features.Add(new ConfigureRazorParserOptions(useRoslynTokenizer: true, CSharpParseOptions.Default)); + RazorExtensions.Register(builder); + }); + + var fileKind = FileKinds.GetFileKindFromFilePath(filePath); + var codeDocument = projectEngine.ProcessDesignTime(sourceDocument, fileKind, importSources: default, tagHelpers); + + return codeDocument; + } +} diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/SimpleTagHelpers.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/SimpleTagHelpers.cs new file mode 100644 index 00000000000..6d9153ee593 --- /dev/null +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/SimpleTagHelpers.cs @@ -0,0 +1,240 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +using System.Collections.Immutable; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.AspNetCore.Razor.Language.Components; +using static Microsoft.AspNetCore.Razor.Language.CommonMetadata; + +namespace Microsoft.AspNetCore.Razor.Test.Common; + +internal static class SimpleTagHelpers +{ + public static ImmutableArray Default { get; } + + static SimpleTagHelpers() + { + var builder1 = TagHelperDescriptorBuilder.Create("Test1TagHelper", "TestAssembly"); + builder1.TagMatchingRule(rule => rule.TagName = "test1"); + builder1.SetMetadata(TypeName("Test1TagHelper")); + builder1.BindAttribute(attribute => + { + attribute.Name = "bool-val"; + attribute.SetMetadata(PropertyName("BoolVal")); + attribute.TypeName = typeof(bool).FullName; + }); + builder1.BindAttribute(attribute => + { + attribute.Name = "int-val"; + attribute.SetMetadata(PropertyName("IntVal")); + attribute.TypeName = typeof(int).FullName; + }); + + var builder1WithRequiredParent = TagHelperDescriptorBuilder.Create("Test1TagHelper.SomeChild", "TestAssembly"); + builder1WithRequiredParent.TagMatchingRule(rule => + { + rule.TagName = "SomeChild"; + rule.ParentTag = "test1"; + }); + builder1WithRequiredParent.SetMetadata(TypeName("Test1TagHelper.SomeChild")); + builder1WithRequiredParent.BindAttribute(attribute => + { + attribute.Name = "attribute"; + attribute.SetMetadata(PropertyName("Attribute")); + attribute.TypeName = typeof(string).FullName; + }); + + var builder2 = TagHelperDescriptorBuilder.Create("Test2TagHelper", "TestAssembly"); + builder2.TagMatchingRule(rule => rule.TagName = "test2"); + builder2.SetMetadata(TypeName("Test2TagHelper")); + builder2.BindAttribute(attribute => + { + attribute.Name = "bool-val"; + attribute.SetMetadata(PropertyName("BoolVal")); + attribute.TypeName = typeof(bool).FullName; + }); + builder2.BindAttribute(attribute => + { + attribute.Name = "int-val"; + attribute.SetMetadata(PropertyName("IntVal")); + attribute.TypeName = typeof(int).FullName; + }); + + var builder3 = TagHelperDescriptorBuilder.Create(ComponentMetadata.Component.TagHelperKind, "Component1TagHelper", "TestAssembly"); + builder3.TagMatchingRule(rule => rule.TagName = "Component1"); + builder3.SetMetadata( + TypeName("Component1"), + TypeNamespace("System"), // Just so we can reasonably assume a using directive is in place + TypeNameIdentifier("Component1"), + new(ComponentMetadata.Component.NameMatchKey, ComponentMetadata.Component.FullyQualifiedNameMatch)); + builder3.BindAttribute(attribute => + { + attribute.Name = "bool-val"; + attribute.SetMetadata(PropertyName("BoolVal")); + attribute.TypeName = typeof(bool).FullName; + }); + builder3.BindAttribute(attribute => + { + attribute.Name = "int-val"; + attribute.SetMetadata(PropertyName("IntVal")); + attribute.TypeName = typeof(int).FullName; + }); + builder3.BindAttribute(attribute => + { + attribute.Name = "Title"; + attribute.SetMetadata(PropertyName("Title")); + attribute.TypeName = typeof(string).FullName; + }); + + var textComponent = TagHelperDescriptorBuilder.Create(ComponentMetadata.Component.TagHelperKind, "TextTagHelper", "TestAssembly"); + textComponent.TagMatchingRule(rule => rule.TagName = "Text"); + textComponent.SetMetadata( + TypeName("Text"), + TypeNamespace("System"), + TypeNameIdentifier("Text"), + new(ComponentMetadata.Component.NameMatchKey, ComponentMetadata.Component.FullyQualifiedNameMatch)); + + var directiveAttribute1 = TagHelperDescriptorBuilder.Create(ComponentMetadata.Component.TagHelperKind, "TestDirectiveAttribute", "TestAssembly"); + directiveAttribute1.TagMatchingRule(rule => + { + rule.TagName = "*"; + rule.RequireAttributeDescriptor(b => + { + b.Name = "@test"; + b.NameComparisonMode = RequiredAttributeDescriptor.NameComparisonMode.PrefixMatch; + }); + }); + directiveAttribute1.TagMatchingRule(rule => + { + rule.TagName = "*"; + rule.RequireAttributeDescriptor(b => + { + b.Name = "@test"; + b.NameComparisonMode = RequiredAttributeDescriptor.NameComparisonMode.FullMatch; + }); + }); + directiveAttribute1.BindAttribute(attribute => + { + attribute.Name = "@test"; + attribute.SetMetadata(PropertyName("Test"), IsDirectiveAttribute); + attribute.TypeName = typeof(string).FullName; + + attribute.BindAttributeParameter(parameter => + { + parameter.Name = "something"; + parameter.TypeName = typeof(string).FullName; + + parameter.SetMetadata(PropertyName("Something")); + }); + }); + directiveAttribute1.SetMetadata( + MakeTrue(TagHelperMetadata.Common.ClassifyAttributesOnly), + new(ComponentMetadata.Component.NameMatchKey, ComponentMetadata.Component.FullyQualifiedNameMatch), + TypeName("TestDirectiveAttribute")); + + var directiveAttribute2 = TagHelperDescriptorBuilder.Create(ComponentMetadata.Component.TagHelperKind, "MinimizedDirectiveAttribute", "TestAssembly"); + directiveAttribute2.TagMatchingRule(rule => + { + rule.TagName = "*"; + rule.RequireAttributeDescriptor(b => + { + b.Name = "@minimized"; + b.NameComparisonMode = RequiredAttributeDescriptor.NameComparisonMode.PrefixMatch; + }); + }); + directiveAttribute2.TagMatchingRule(rule => + { + rule.TagName = "*"; + rule.RequireAttributeDescriptor(b => + { + b.Name = "@minimized"; + b.NameComparisonMode = RequiredAttributeDescriptor.NameComparisonMode.FullMatch; + }); + }); + directiveAttribute2.BindAttribute(attribute => + { + attribute.Name = "@minimized"; + attribute.SetMetadata(PropertyName("Minimized"), IsDirectiveAttribute); + attribute.TypeName = typeof(bool).FullName; + + attribute.BindAttributeParameter(parameter => + { + parameter.Name = "something"; + parameter.TypeName = typeof(string).FullName; + + parameter.SetMetadata(PropertyName("Something")); + }); + }); + directiveAttribute2.SetMetadata( + MakeTrue(TagHelperMetadata.Common.ClassifyAttributesOnly), + new(ComponentMetadata.Component.NameMatchKey, ComponentMetadata.Component.FullyQualifiedNameMatch), + TypeName("TestDirectiveAttribute")); + + var directiveAttribute3 = TagHelperDescriptorBuilder.Create(ComponentMetadata.EventHandler.TagHelperKind, "OnClickDirectiveAttribute", "TestAssembly"); + directiveAttribute3.TagMatchingRule(rule => + { + rule.TagName = "*"; + rule.RequireAttributeDescriptor(b => + { + b.Name = "@onclick"; + b.SetMetadata(MetadataCollection.Create(IsDirectiveAttribute)); + b.NameComparisonMode = RequiredAttributeDescriptor.NameComparisonMode.FullMatch; + }); + }); + directiveAttribute3.TagMatchingRule(rule => + { + rule.TagName = "*"; + rule.RequireAttributeDescriptor(b => + { + b.Name = "@onclick"; + b.SetMetadata(MetadataCollection.Create(IsDirectiveAttribute)); + b.NameComparisonMode = RequiredAttributeDescriptor.NameComparisonMode.PrefixMatch; + }); + }); + directiveAttribute3.BindAttribute(attribute => + { + attribute.Name = "@onclick"; + attribute.SetMetadata(PropertyName("onclick"), IsDirectiveAttribute, IsWeaklyTyped); + attribute.TypeName = "Microsoft.AspNetCore.Components.EventCallback"; + }); + directiveAttribute3.SetMetadata( + RuntimeName(ComponentMetadata.EventHandler.RuntimeName), + SpecialKind(ComponentMetadata.EventHandler.TagHelperKind), + new(ComponentMetadata.EventHandler.EventArgsType, "Microsoft.AspNetCore.Components.Web.MouseEventArgs"), + new(ComponentMetadata.Component.NameMatchKey, ComponentMetadata.Component.FullyQualifiedNameMatch), + MakeTrue(TagHelperMetadata.Common.ClassifyAttributesOnly), + TypeName("OnClickDirectiveAttribute"), + TypeNamespace("Microsoft.AspNetCore.Components.Web"), + TypeNameIdentifier("EventHandlers")); + + var htmlTagMutator = TagHelperDescriptorBuilder.Create("HtmlMutator", "TestAssembly"); + htmlTagMutator.TagMatchingRule(rule => + { + rule.TagName = "title"; + rule.RequireAttributeDescriptor(attributeRule => + { + attributeRule.Name = "mutator"; + }); + }); + htmlTagMutator.SetMetadata(TypeName("HtmlMutator")); + htmlTagMutator.BindAttribute(attribute => + { + attribute.Name = "Extra"; + attribute.SetMetadata(PropertyName("Extra")); + attribute.TypeName = typeof(bool).FullName; + }); + + Default = + [ + builder1.Build(), + builder1WithRequiredParent.Build(), + builder2.Build(), + builder3.Build(), + textComponent.Build(), + directiveAttribute1.Build(), + directiveAttribute2.Build(), + directiveAttribute3.Build(), + htmlTagMutator.Build(), + ]; + } +} diff --git a/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Hover/HoverFactoryTest.cs b/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Hover/HoverFactoryTest.cs new file mode 100644 index 00000000000..f881fe5927b --- /dev/null +++ b/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Hover/HoverFactoryTest.cs @@ -0,0 +1,605 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Razor.Test.Common; +using Microsoft.CodeAnalysis.Classification; +using Microsoft.CodeAnalysis.Razor.ProjectSystem; +using Microsoft.CodeAnalysis.Razor.Tooltip; +using Microsoft.VisualStudio.LanguageServer.Protocol; +using Microsoft.VisualStudio.Text.Adornments; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.CodeAnalysis.Razor.Hover; + +public class HoverFactoryTest(ITestOutputHelper testOutput) : ToolingTestBase(testOutput) +{ + private static HoverDisplayOptions UseMarkdown => new(MarkupKind.Markdown, SupportsVisualStudioExtensions: false); + private static HoverDisplayOptions UsePlainText => new(MarkupKind.PlainText, SupportsVisualStudioExtensions: false); + + private static HoverDisplayOptions UseVisualStudio => new(MarkupKind.Markdown, SupportsVisualStudioExtensions: true); + + [Fact] + public async Task GetHoverAsync_TagHelper_Element() + { + // Arrange + TestCode code = """ + @addTagHelper *, TestAssembly + <$$test1> + """; + + var codeDocument = RazorCodeDocumentFactory.CreateCodeDocument(code.Text, isRazorFile: false, SimpleTagHelpers.Default); + + // Act + var hover = await HoverFactory.GetHoverAsync(codeDocument, "file.cshtml", code.Position, UseMarkdown, StrictMock.Of(), DisposalToken); + + // Assert + Assert.NotNull(hover); + Assert.NotNull(hover.Contents); + Assert.Contains("**Test1TagHelper**", ((MarkupContent)hover.Contents).Value, StringComparison.Ordinal); + var expectedRange = VsLspFactory.CreateSingleLineRange(line: 1, character: 1, length: 5); + Assert.Equal(expectedRange, hover.Range); + } + + [Fact] + public async Task GetHoverAsync_TagHelper_Element_WithParent() + { + // Arrange + TestCode code = """ + @addTagHelper *, TestAssembly + + + + """; + + var codeDocument = RazorCodeDocumentFactory.CreateCodeDocument(code.Text, isRazorFile: false, SimpleTagHelpers.Default); + + // Act + var hover = await HoverFactory.GetHoverAsync(codeDocument, "file.cshtml", code.Position, UseMarkdown, StrictMock.Of(), DisposalToken); + + // Assert + Assert.NotNull(hover); + Assert.NotNull(hover.Contents); + Assert.Contains("**SomeChild**", ((MarkupContent)hover.Contents).Value, StringComparison.Ordinal); + var expectedRange = VsLspFactory.CreateSingleLineRange(line: 2, character: 5, length: 9); + Assert.Equal(expectedRange, hover.Range); + } + + [Fact] + public async Task GetHoverAsync_TagHelper_Attribute_WithParent() + { + // Arrange + TestCode code = """ + @addTagHelper *, TestAssembly + + + + """; + + var codeDocument = RazorCodeDocumentFactory.CreateCodeDocument(code.Text, isRazorFile: false, SimpleTagHelpers.Default); + + // Act + var hover = await HoverFactory.GetHoverAsync(codeDocument, "file.cshtml", code.Position, UseMarkdown, StrictMock.Of(), DisposalToken); + + // Assert + Assert.NotNull(hover); + Assert.NotNull(hover.Contents); + Assert.Contains("**Attribute**", ((MarkupContent)hover.Contents).Value, StringComparison.Ordinal); + var expectedRange = codeDocument.Source.Text.GetRange(code.Span); + Assert.Equal(expectedRange, hover.Range); + } + + [Fact] + public async Task GetHoverAsync_TagHelper_Element_EndTag() + { + // Arrange + TestCode code = """ + @addTagHelper *, TestAssembly + + """; + + var codeDocument = RazorCodeDocumentFactory.CreateCodeDocument(code.Text, isRazorFile: false, SimpleTagHelpers.Default); + + // Act + var hover = await HoverFactory.GetHoverAsync(codeDocument, "file.cshtml", code.Position, UseMarkdown, StrictMock.Of(), DisposalToken); + + // Assert + Assert.NotNull(hover); + Assert.NotNull(hover.Contents); + Assert.Contains("**Test1TagHelper**", ((MarkupContent)hover.Contents).Value, StringComparison.Ordinal); + var expectedRange = VsLspFactory.CreateSingleLineRange(line: 1, character: 9, length: 5); + Assert.Equal(expectedRange, hover.Range); + } + + [Fact] + public async Task GetHoverAsync_TagHelper_Attribute() + { + // Arrange + TestCode code = """ + @addTagHelper *, TestAssembly + + """; + + var codeDocument = RazorCodeDocumentFactory.CreateCodeDocument(code.Text, isRazorFile: false, SimpleTagHelpers.Default); + + // Act + var hover = await HoverFactory.GetHoverAsync(codeDocument, "file.cshtml", code.Position, UseMarkdown, StrictMock.Of(), DisposalToken); + + // Assert + Assert.NotNull(hover); + Assert.NotNull(hover.Contents); + Assert.Contains("**BoolVal**", ((MarkupContent)hover.Contents).Value, StringComparison.Ordinal); + Assert.DoesNotContain("**IntVal**", ((MarkupContent)hover.Contents).Value, StringComparison.Ordinal); + var expectedRange = VsLspFactory.CreateSingleLineRange(line: 1, character: 7, length: 8); + Assert.Equal(expectedRange, hover.Range); + } + + [Fact] + public async Task GetHoverAsync_TagHelper_AttributeTrailingEdge() + { + // Arrange + TestCode code = """ + @addTagHelper *, TestAssembly + + """; + + var codeDocument = RazorCodeDocumentFactory.CreateCodeDocument(code.Text, isRazorFile: false, SimpleTagHelpers.Default); + + // Act + var hover = await HoverFactory.GetHoverAsync(codeDocument, "file.cshtml", code.Position, UseMarkdown, StrictMock.Of(), DisposalToken); + + // Assert + Assert.NotNull(hover); + Assert.NotNull(hover.Contents); + Assert.Contains("**BoolVal**", ((MarkupContent)hover.Contents).Value, StringComparison.Ordinal); + Assert.DoesNotContain("**IntVal**", ((MarkupContent)hover.Contents).Value, StringComparison.Ordinal); + var expectedRange = VsLspFactory.CreateSingleLineRange(line: 1, character: 7, length: 8); + Assert.Equal(expectedRange, hover.Range); + } + + [Fact] + public async Task GetHoverAsync_TagHelper_AttributeValue_ReturnsNull() + { + // Arrange + TestCode code = """ + @addTagHelper *, TestAssembly + + """; + + var codeDocument = RazorCodeDocumentFactory.CreateCodeDocument(code.Text, isRazorFile: false, SimpleTagHelpers.Default); + + // Act + var hover = await HoverFactory.GetHoverAsync(codeDocument, "file.cshtml", code.Position, UseMarkdown, StrictMock.Of(), DisposalToken); + + // Assert + Assert.Null(hover); + } + + [Fact] + public async Task GetHoverAsync_TagHelper_AfterAttributeEquals_ReturnsNull() + { + // Arrange + TestCode code = """ + @addTagHelper *, TestAssembly + + """; + + var codeDocument = RazorCodeDocumentFactory.CreateCodeDocument(code.Text, isRazorFile: false, SimpleTagHelpers.Default); + + // Act + var hover = await HoverFactory.GetHoverAsync(codeDocument, "file.cshtml", code.Position, UseMarkdown, StrictMock.Of(), DisposalToken); + + // Assert + Assert.Null(hover); + } + + [Fact] + public async Task GetHoverAsync_TagHelper_AttributeEnd_ReturnsNull() + { + // Arrange + TestCode code = """ + @addTagHelper *, TestAssembly + + """; + + var codeDocument = RazorCodeDocumentFactory.CreateCodeDocument(code.Text, isRazorFile: false, SimpleTagHelpers.Default); + + // Act + var hover = await HoverFactory.GetHoverAsync(codeDocument, "file.cshtml", code.Position, UseMarkdown, StrictMock.Of(), DisposalToken); + + // Assert + Assert.Null(hover); + } + + [Fact] + public async Task GetHoverAsync_TagHelper_MinimizedAttribute() + { + // Arrange + TestCode code = """ + @addTagHelper *, TestAssembly + + """; + + var codeDocument = RazorCodeDocumentFactory.CreateCodeDocument(code.Text, isRazorFile: false, SimpleTagHelpers.Default); + + // Act + var hover = await HoverFactory.GetHoverAsync(codeDocument, "file.cshtml", code.Position, UseMarkdown, StrictMock.Of(), DisposalToken); + + // Assert + Assert.NotNull(hover); + Assert.NotNull(hover.Contents); + Assert.Contains("**BoolVal**", ((MarkupContent)hover.Contents).Value, StringComparison.Ordinal); + Assert.DoesNotContain("**IntVal**", ((MarkupContent)hover.Contents).Value, StringComparison.Ordinal); + var expectedRange = VsLspFactory.CreateSingleLineRange(line: 1, character: 7, length: 8); + Assert.Equal(expectedRange, hover.Range); + } + + [Fact] + public async Task GetHoverAsync_DirectiveAttribute_HasResult() + { + // Arrange + TestCode code = """ + @addTagHelper *, TestAssembly + + @code{ + public void Increment(){ + } + } + """; + + var codeDocument = RazorCodeDocumentFactory.CreateCodeDocument(code.Text, "text.razor", SimpleTagHelpers.Default); + + // Act + var hover = await HoverFactory.GetHoverAsync(codeDocument, "file.cshtml", code.Position, UseMarkdown, StrictMock.Of(), DisposalToken); + + // Assert + Assert.NotNull(hover); + Assert.NotNull(hover.Contents); + Assert.Contains("**Test**", ((MarkupContent)hover.Contents).Value, StringComparison.Ordinal); + var expectedRange = VsLspFactory.CreateSingleLineRange(line: 1, character: 5, length: 5); + Assert.Equal(expectedRange, hover.Range); + } + + [Fact] + public async Task GetHoverAsync_TagHelper_MalformedElement() + { + // Arrange + TestCode code = """ + @addTagHelper *, TestAssembly + <$$test1(), DisposalToken); + + // Assert + Assert.NotNull(hover); + Assert.NotNull(hover.Contents); + Assert.Contains("**Test1TagHelper**", ((MarkupContent)hover.Contents).Value, StringComparison.Ordinal); + var expectedRange = VsLspFactory.CreateSingleLineRange(line: 1, character: 1, length: 5); + Assert.Equal(expectedRange, hover.Range); + } + + [Fact] + public async Task GetHoverAsync_TagHelper_MalformedAttribute() + { + // Arrange + TestCode code = """ + @addTagHelper *, TestAssembly + + """; + + var codeDocument = RazorCodeDocumentFactory.CreateCodeDocument(code.Text, isRazorFile: false, SimpleTagHelpers.Default); + + // Act + var hover = await HoverFactory.GetHoverAsync(codeDocument, "file.cshtml", code.Position, UseMarkdown, StrictMock.Of(), DisposalToken); + + // Assert + Assert.NotNull(hover); + Assert.NotNull(hover.Contents); + Assert.Contains("**BoolVal**", ((MarkupContent)hover.Contents).Value, StringComparison.Ordinal); + Assert.DoesNotContain("**IntVal**", ((MarkupContent)hover.Contents).Value, StringComparison.Ordinal); + var expectedRange = VsLspFactory.CreateSingleLineRange(line: 1, character: 7, length: 8); + Assert.Equal(expectedRange, hover.Range); + } + + [Fact] + public async Task GetHoverAsync_HTML_MarkupElement() + { + // Arrange + TestCode code = """ + @addTagHelper *, TestAssembly +

<$$strong>

+ """; + + var codeDocument = RazorCodeDocumentFactory.CreateCodeDocument(code.Text, isRazorFile: false); + + // Act + var hover = await HoverFactory.GetHoverAsync(codeDocument, "file.cshtml", code.Position, UseMarkdown, StrictMock.Of(), DisposalToken); + + // Assert + Assert.Null(hover); + } + + [Fact] + public async Task GetHoverAsync_TagHelper_PlainTextElement() + { + // Arrange + TestCode code = """ + @addTagHelper *, TestAssembly + <$$test1>
+ """; + + var codeDocument = RazorCodeDocumentFactory.CreateCodeDocument(code.Text, isRazorFile: false, SimpleTagHelpers.Default); + + // Act + var hover = await HoverFactory.GetHoverAsync(codeDocument, "file.cshtml", code.Position, UsePlainText, StrictMock.Of(), DisposalToken); + + // Assert + Assert.NotNull(hover); + Assert.NotNull(hover.Contents); + Assert.Contains("Test1TagHelper", ((MarkupContent)hover.Contents).Value, StringComparison.Ordinal); + Assert.Equal(MarkupKind.PlainText, ((MarkupContent)hover.Contents).Kind); + var expectedRange = VsLspFactory.CreateSingleLineRange(line: 1, character: 1, length: 5); + Assert.Equal(expectedRange, hover.Range); + } + + [Fact] + public async Task GetHoverAsync_TagHelper_PlainTextElement_EndTag() + { + // Arrange + TestCode code = """ + @addTagHelper *, TestAssembly + + """; + + var codeDocument = RazorCodeDocumentFactory.CreateCodeDocument(code.Text, isRazorFile: false, SimpleTagHelpers.Default); + + // Act + var hover = await HoverFactory.GetHoverAsync(codeDocument, "file.cshtml", code.Position, UsePlainText, StrictMock.Of(), DisposalToken); + + // Assert + Assert.NotNull(hover); + Assert.NotNull(hover.Contents); + Assert.Contains("Test1TagHelper", ((MarkupContent)hover.Contents).Value, StringComparison.Ordinal); + Assert.Equal(MarkupKind.PlainText, ((MarkupContent)hover.Contents).Kind); + var expectedRange = VsLspFactory.CreateSingleLineRange(line: 1, character: 9, length: 5); + Assert.Equal(expectedRange, hover.Range); + } + + [Fact] + public async Task GetHoverAsync_TagHelper_TextComponent() + { + // Arrange + TestCode code = """ + <$$Text> + """; + + var codeDocument = RazorCodeDocumentFactory.CreateCodeDocument(code.Text, isRazorFile: true, SimpleTagHelpers.Default); + + // Act + var hover = await HoverFactory.GetHoverAsync(codeDocument, "file.razor", code.Position, UsePlainText, StrictMock.Of(), DisposalToken); + + // Assert + Assert.NotNull(hover); + Assert.NotNull(hover.Contents); + Assert.Contains("Text", ((MarkupContent)hover.Contents).Value, StringComparison.Ordinal); + Assert.Equal(MarkupKind.PlainText, ((MarkupContent)hover.Contents).Kind); + var expectedRange = VsLspFactory.CreateSingleLineRange(line: 0, character: 1, length: 4); + Assert.Equal(expectedRange, hover.Range); + } + + [Fact] + public async Task GetHoverAsync_TagHelper_TextComponent_NestedInHtml() + { + // Arrange + TestCode code = """ +
+ <$$Text> +
+ """; + + var codeDocument = RazorCodeDocumentFactory.CreateCodeDocument(code.Text, isRazorFile: true, SimpleTagHelpers.Default); + + // Act + var hover = await HoverFactory.GetHoverAsync(codeDocument, "file.razor", code.Position, UsePlainText, StrictMock.Of(), DisposalToken); + + // Assert + Assert.NotNull(hover); + Assert.NotNull(hover.Contents); + Assert.Contains("Text", ((MarkupContent)hover.Contents).Value, StringComparison.Ordinal); + Assert.Equal(MarkupKind.PlainText, ((MarkupContent)hover.Contents).Kind); + var expectedRange = VsLspFactory.CreateSingleLineRange(line: 1, character: 5, length: 4); + Assert.Equal(expectedRange, hover.Range); + } + + [Fact] + public async Task GetHoverAsync_TagHelper_TextComponent_NestedInCSharp() + { + // Arrange + TestCode code = """ + @if (true) + { + <$$Text> + } + """; + + var codeDocument = RazorCodeDocumentFactory.CreateCodeDocument(code.Text, isRazorFile: true, SimpleTagHelpers.Default); + + // Act + var hover = await HoverFactory.GetHoverAsync(codeDocument, "file.razor", code.Position, UsePlainText, StrictMock.Of(), DisposalToken); + + // Assert + Assert.Null(hover); + } + + [Fact] + public async Task GetHoverAsync_TagHelper_TextComponent_NestedInCSharpAndText() + { + // Arrange + TestCode code = """ + @if (true) + { + + <$$Text> + + } + """; + + var codeDocument = RazorCodeDocumentFactory.CreateCodeDocument(code.Text, isRazorFile: true, SimpleTagHelpers.Default); + + // Act + var hover = await HoverFactory.GetHoverAsync(codeDocument, "file.razor", code.Position, UsePlainText, StrictMock.Of(), DisposalToken); + + // Assert + Assert.NotNull(hover); + Assert.NotNull(hover.Contents); + Assert.Contains("Text", ((MarkupContent)hover.Contents).Value, StringComparison.Ordinal); + Assert.Equal(MarkupKind.PlainText, ((MarkupContent)hover.Contents).Kind); + var expectedRange = VsLspFactory.CreateSingleLineRange(line: 3, character: 9, length: 4); + Assert.Equal(expectedRange, hover.Range); + } + + [Fact] + public async Task GetHoverAsync_TagHelper_PlainTextAttribute() + { + // Arrange + TestCode code = """ + @addTagHelper *, TestAssembly + + """; + + var codeDocument = RazorCodeDocumentFactory.CreateCodeDocument(code.Text, isRazorFile: false, SimpleTagHelpers.Default); + + // Act + var hover = await HoverFactory.GetHoverAsync(codeDocument, "file.cshtml", code.Position, UsePlainText, StrictMock.Of(), DisposalToken); + + // Assert + Assert.NotNull(hover); + Assert.NotNull(hover.Contents); + Assert.Contains("BoolVal", ((MarkupContent)hover.Contents).Value, StringComparison.Ordinal); + Assert.DoesNotContain("IntVal", ((MarkupContent)hover.Contents).Value, StringComparison.Ordinal); + Assert.Equal(MarkupKind.PlainText, ((MarkupContent)hover.Contents).Kind); + var expectedRange = VsLspFactory.CreateSingleLineRange(line: 1, character: 7, length: 8); + Assert.Equal(expectedRange, hover.Range); + } + + [Fact] + public async Task GetHoverAsync_HTML_PlainTextElement() + { + // Arrange + TestCode code = """ + @addTagHelper *, TestAssembly +

<$$strong>

+ """; + + var codeDocument = RazorCodeDocumentFactory.CreateCodeDocument(code.Text, isRazorFile: false); + + // Act + var hover = await HoverFactory.GetHoverAsync(codeDocument, "file.cshtml", code.Position, UsePlainText, StrictMock.Of(), DisposalToken); + + // Assert + Assert.Null(hover); + } + + [Fact] + public async Task GetHoverAsync_HTML_PlainTextAttribute() + { + // Arrange + TestCode code = """ + @addTagHelper *, TestAssembly +

+ """; + + var codeDocument = RazorCodeDocumentFactory.CreateCodeDocument(code.Text, isRazorFile: false); + + // Act + var hover = await HoverFactory.GetHoverAsync(codeDocument, "file.cshtml", code.Position, UsePlainText, StrictMock.Of(), DisposalToken); + + // Assert + Assert.Null(hover); + } + + [Fact] + public async Task GetHoverAsync_TagHelper_Element_VSClient_ReturnVSHover() + { + // Arrange + TestCode code = """ + @addTagHelper *, TestAssembly + <$$test1>
+ """; + + var codeDocument = RazorCodeDocumentFactory.CreateCodeDocument(code.Text, isRazorFile: false, SimpleTagHelpers.Default); + + // Act + var vsHover = await HoverFactory.GetHoverAsync(codeDocument, "file.cshtml", code.Position, UseVisualStudio, StrictMock.Of(), DisposalToken); + + // Assert + Assert.NotNull(vsHover); + Assert.NotNull(vsHover.Contents); + Assert.False(vsHover.Contents.Value.TryGetFourth(out var _)); + Assert.True(vsHover.Contents.Value.TryGetThird(out var _) && !vsHover.Contents.Value.Third.Any()); + var expectedRange = VsLspFactory.CreateSingleLineRange(line: 1, character: 1, length: 5); + Assert.Equal(expectedRange, vsHover.Range); + + Assert.NotNull(vsHover.RawContent); + var container = (ContainerElement)vsHover.RawContent; + var containerElements = container.Elements.ToList(); + Assert.Equal(ContainerElementStyle.Stacked, container.Style); + Assert.Single(containerElements); + + // [TagHelper Glyph] Test1TagHelper + var innerContainer = ((ContainerElement)containerElements[0]).Elements.ToList(); + var classifiedTextElement = (ClassifiedTextElement)innerContainer[1]; + Assert.Equal(2, innerContainer.Count); + Assert.Equal(ClassifiedTagHelperTooltipFactory.ClassGlyph, innerContainer[0]); + Assert.Collection(classifiedTextElement.Runs, + run => run.AssertExpectedClassification("Test1TagHelper", ClassifiedTagHelperTooltipFactory.TypeClassificationName)); + } + + [Fact] + public async Task GetHoverAsync_TagHelper_Attribute_VSClient_ReturnVSHover() + { + // Arrange + TestCode code = """ + @addTagHelper *, TestAssembly + + """; + + var codeDocument = RazorCodeDocumentFactory.CreateCodeDocument(code.Text, isRazorFile: false, SimpleTagHelpers.Default); + + // Act + var vsHover = await HoverFactory.GetHoverAsync(codeDocument, "file.cshtml", code.Position, UseVisualStudio, StrictMock.Of(), DisposalToken); + + // Assert + Assert.NotNull(vsHover); + Assert.NotNull(vsHover.Contents); + Assert.False(vsHover.Contents.Value.TryGetFourth(out _)); + Assert.True(vsHover.Contents.Value.TryGetThird(out var markedStrings) && !markedStrings.Any()); + var expectedRange = VsLspFactory.CreateSingleLineRange(line: 1, character: 7, length: 8); + Assert.Equal(expectedRange, vsHover.Range); + + Assert.NotNull(vsHover.RawContent); + var container = (ContainerElement)vsHover.RawContent; + var containerElements = container.Elements.ToList(); + Assert.Equal(ContainerElementStyle.Stacked, container.Style); + Assert.Single(containerElements); + + // [TagHelper Glyph] bool Test1TagHelper.BoolVal + var innerContainer = ((ContainerElement)containerElements[0]).Elements.ToList(); + var classifiedTextElement = (ClassifiedTextElement)innerContainer[1]; + Assert.Equal(2, innerContainer.Count); + Assert.Equal(ClassifiedTagHelperTooltipFactory.PropertyGlyph, innerContainer[0]); + Assert.Collection(classifiedTextElement.Runs, + run => run.AssertExpectedClassification("bool", ClassificationTypeNames.Keyword), + run => run.AssertExpectedClassification(" ", ClassificationTypeNames.WhiteSpace), + run => run.AssertExpectedClassification("Test1TagHelper", ClassifiedTagHelperTooltipFactory.TypeClassificationName), + run => run.AssertExpectedClassification(".", ClassificationTypeNames.Punctuation), + run => run.AssertExpectedClassification("BoolVal", ClassificationTypeNames.Identifier)); + } +} From b8a139270ff112ceebe76c0b74dac47657bcf239 Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Mon, 28 Oct 2024 16:45:34 -0700 Subject: [PATCH 09/12] Remove HoverService from the language server Now that the implementation of GetHoverAsync has moved to Workspaces, HoverService really doesn't need to carry on. --- .../AbstractRazorDelegatingEndpoint.cs | 8 +- .../IServiceCollectionExtensions.cs | 2 - .../Hover/HoverEndpoint.cs | 105 ++++++++++++++---- .../Hover/HoverService.TestAccessor.cs | 27 ----- .../Hover/HoverService.cs | 92 --------------- .../Hover/IHoverService.cs | 16 --- ...verServiceTest.cs => HoverEndpointTest.cs} | 41 +++---- 7 files changed, 106 insertions(+), 185 deletions(-) delete mode 100644 src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hover/HoverService.TestAccessor.cs delete mode 100644 src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hover/HoverService.cs delete mode 100644 src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hover/IHoverService.cs rename src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Hover/{HoverServiceTest.cs => HoverEndpointTest.cs} (94%) diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/AbstractRazorDelegatingEndpoint.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/AbstractRazorDelegatingEndpoint.cs index c84789d378c..60f8123e660 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/AbstractRazorDelegatingEndpoint.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/AbstractRazorDelegatingEndpoint.cs @@ -22,7 +22,7 @@ internal abstract class AbstractRazorDelegatingEndpoint : I where TRequest : ITextDocumentPositionParams { private readonly LanguageServerFeatureOptions _languageServerFeatureOptions; - private readonly IDocumentMappingService _documentMappingService; + protected readonly IDocumentMappingService DocumentMappingService; private readonly IClientConnection _clientConnection; protected readonly ILogger Logger; @@ -33,7 +33,7 @@ protected AbstractRazorDelegatingEndpoint( ILogger logger) { _languageServerFeatureOptions = languageServerFeatureOptions ?? throw new ArgumentNullException(nameof(languageServerFeatureOptions)); - _documentMappingService = documentMappingService ?? throw new ArgumentNullException(nameof(documentMappingService)); + DocumentMappingService = documentMappingService ?? throw new ArgumentNullException(nameof(documentMappingService)); _clientConnection = clientConnection ?? throw new ArgumentNullException(nameof(clientConnection)); Logger = logger ?? throw new ArgumentNullException(nameof(logger)); @@ -119,7 +119,7 @@ protected virtual Task HandleDelegatedResponseAsync(TResponse delegat return default; } - var positionInfo = DocumentPositionInfoStrategy.GetPositionInfo(_documentMappingService, codeDocument, absoluteIndex); + var positionInfo = DocumentPositionInfoStrategy.GetPositionInfo(DocumentMappingService, codeDocument, absoluteIndex); var response = await TryHandleAsync(request, requestContext, positionInfo, cancellationToken).ConfigureAwait(false); if (response is not null && response is not ISumType { Value: null }) @@ -143,7 +143,7 @@ protected virtual Task HandleDelegatedResponseAsync(TResponse delegat // Sometimes Html can actually be mapped to C#, like for example component attributes, which map to // C# properties, even though they appear entirely in a Html context. Since remapping is pretty cheap // it's easier to just try mapping, and see what happens, rather than checking for specific syntax nodes. - if (_documentMappingService.TryMapToGeneratedDocumentPosition(codeDocument.GetCSharpDocument(), positionInfo.HostDocumentIndex, out Position? csharpPosition, out _)) + if (DocumentMappingService.TryMapToGeneratedDocumentPosition(codeDocument.GetCSharpDocument(), positionInfo.HostDocumentIndex, out Position? csharpPosition, out _)) { // We're just gonna pretend this mapped perfectly normally onto C#. Moving this logic to the actual position info // calculating code is possible, but could have untold effects, so opt-in is better (for now?) diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Extensions/IServiceCollectionExtensions.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Extensions/IServiceCollectionExtensions.cs index 85a9650062a..f48af13a044 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Extensions/IServiceCollectionExtensions.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Extensions/IServiceCollectionExtensions.cs @@ -111,8 +111,6 @@ public static void AddDiagnosticServices(this IServiceCollection services) public static void AddHoverServices(this IServiceCollection services) { services.AddHandlerWithCapabilities(); - - services.AddSingleton(); } public static void AddSemanticTokensServices(this IServiceCollection services, LanguageServerFeatureOptions featureOptions) diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hover/HoverEndpoint.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hover/HoverEndpoint.cs index b1c7395d7aa..7ebdb1b9950 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hover/HoverEndpoint.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hover/HoverEndpoint.cs @@ -1,14 +1,18 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT license. See License.txt in the project root for license information. -using System; using System.Threading; using System.Threading.Tasks; +using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.LanguageServer.EndpointContracts; using Microsoft.AspNetCore.Razor.LanguageServer.Hosting; +using Microsoft.AspNetCore.Razor.LanguageServer.ProjectSystem; using Microsoft.AspNetCore.Razor.Threading; +using Microsoft.CodeAnalysis.Razor; using Microsoft.CodeAnalysis.Razor.DocumentMapping; +using Microsoft.CodeAnalysis.Razor.Hover; using Microsoft.CodeAnalysis.Razor.Logging; +using Microsoft.CodeAnalysis.Razor.ProjectSystem; using Microsoft.CodeAnalysis.Razor.Protocol; using Microsoft.CodeAnalysis.Razor.Workspaces; using Microsoft.VisualStudio.LanguageServer.Protocol; @@ -16,20 +20,21 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer.Hover; [RazorLanguageServerEndpoint(Methods.TextDocumentHoverName)] -internal sealed class HoverEndpoint : AbstractRazorDelegatingEndpoint, ICapabilitiesProvider +internal sealed class HoverEndpoint( + IProjectSnapshotManager projectManager, + IClientCapabilitiesService clientCapabilitiesService, + LanguageServerFeatureOptions languageServerFeatureOptions, + IDocumentMappingService documentMappingService, + IClientConnection clientConnection, + ILoggerFactory loggerFactory) + : AbstractRazorDelegatingEndpoint( + languageServerFeatureOptions, + documentMappingService, + clientConnection, + loggerFactory.GetOrCreateLogger()), ICapabilitiesProvider { - private readonly IHoverService _hoverService; - - public HoverEndpoint( - IHoverService hoverService, - LanguageServerFeatureOptions languageServerFeatureOptions, - IDocumentMappingService documentMappingService, - IClientConnection clientConnection, - ILoggerFactory loggerFactory) - : base(languageServerFeatureOptions, documentMappingService, clientConnection, loggerFactory.GetOrCreateLogger()) - { - _hoverService = hoverService ?? throw new ArgumentNullException(nameof(hoverService)); - } + private readonly IProjectSnapshotManager _projectManager = projectManager; + private readonly IClientCapabilitiesService _clientCapabilitiesService = clientCapabilitiesService; public void ApplyCapabilities(VSInternalServerCapabilities serverCapabilities, VSInternalClientCapabilities clientCapabilities) { @@ -56,29 +61,81 @@ public void ApplyCapabilities(VSInternalServerCapabilities serverCapabilities, V positionInfo.LanguageKind)); } - protected override Task TryHandleAsync(TextDocumentPositionParams request, RazorRequestContext requestContext, DocumentPositionInfo positionInfo, CancellationToken cancellationToken) + protected override async Task TryHandleAsync(TextDocumentPositionParams request, RazorRequestContext requestContext, DocumentPositionInfo positionInfo, CancellationToken cancellationToken) { var documentContext = requestContext.DocumentContext; if (documentContext is null) { - return SpecializedTasks.Null(); + return null; + } + + // HTML can still sometimes be handled by razor. For example hovering over + // a component tag like will still be in an html context + if (positionInfo.LanguageKind == RazorLanguageKind.CSharp) + { + return null; } - return _hoverService.GetRazorHoverInfoAsync(documentContext, positionInfo, cancellationToken); + var codeDocument = await documentContext.GetCodeDocumentAsync(cancellationToken).ConfigureAwait(false); + + // Sometimes what looks like a html attribute can actually map to C#, in which case its better to let Roslyn try to handle this. + // We can only do this if we're in single server mode though, otherwise we won't be delegating to Roslyn at all + if (DocumentMappingService.TryMapToGeneratedDocumentPosition(codeDocument.GetCSharpDocument(), positionInfo.HostDocumentIndex, out _, out _)) + { + return null; + } + + var options = HoverDisplayOptions.From(_clientCapabilitiesService.ClientCapabilities); + + return await HoverFactory.GetHoverAsync( + codeDocument, + documentContext.FilePath, + positionInfo.HostDocumentIndex, + options, + _projectManager.GetQueryOperations(), + cancellationToken) + .ConfigureAwait(false); } - protected override Task HandleDelegatedResponseAsync(VSInternalHover? response, TextDocumentPositionParams originalRequest, RazorRequestContext requestContext, DocumentPositionInfo positionInfo, CancellationToken cancellationToken) + protected override async Task HandleDelegatedResponseAsync(VSInternalHover? response, TextDocumentPositionParams originalRequest, RazorRequestContext requestContext, DocumentPositionInfo positionInfo, CancellationToken cancellationToken) { var documentContext = requestContext.DocumentContext; if (documentContext is null) { - return SpecializedTasks.Null(); + return null; + } + + if (response?.Range is null) + { + return response; + } + + var codeDocument = await documentContext.GetCodeDocumentAsync(cancellationToken).ConfigureAwait(false); + + // If we don't include the originally requested position in our response, the client may not show it, so we extend the range to ensure it is in there. + // eg for hovering at @bind-Value:af$$ter, we want to show people the hover for the Value property, so Roslyn will return to us the range for just the + // portion of the attribute that says "Value". + if (RazorSyntaxFacts.TryGetFullAttributeNameSpan(codeDocument, positionInfo.HostDocumentIndex, out var originalAttributeRange)) + { + response.Range = codeDocument.Source.Text.GetRange(originalAttributeRange); + } + else if (positionInfo.LanguageKind == RazorLanguageKind.CSharp) + { + if (DocumentMappingService.TryMapToHostDocumentRange(codeDocument.GetCSharpDocument(), response.Range, out var projectedRange)) + { + response.Range = projectedRange; + } + else + { + // We couldn't remap the range back from Roslyn, but we have to do something with it, because it definitely won't + // be correct, and if the Razor document is small, will be completely outside the valid range for the file, which + // would cause the client to error. + // Returning null here will still show the hover, just there won't be any extra visual indication, like + // a background color, applied by the client. + response.Range = null; + } } - return _hoverService.TranslateDelegatedResponseAsync( - response, - documentContext, - positionInfo, - cancellationToken); + return response; } } diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hover/HoverService.TestAccessor.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hover/HoverService.TestAccessor.cs deleted file mode 100644 index 82ccdb86a82..00000000000 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hover/HoverService.TestAccessor.cs +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the MIT license. See License.txt in the project root for license information. - -using System.Threading; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Razor.Language; -using Microsoft.AspNetCore.Razor.LanguageServer.ProjectSystem; -using Microsoft.CodeAnalysis.Razor.Hover; -using Microsoft.VisualStudio.LanguageServer.Protocol; - -namespace Microsoft.AspNetCore.Razor.LanguageServer.Hover; - -internal sealed partial class HoverService -{ - internal TestAccessor GetTestAccessor() => new(this); - - internal sealed class TestAccessor(HoverService instance) - { - public Task GetHoverAsync( - RazorCodeDocument codeDocument, - string documentFilePath, - int absoluteIndex, - HoverDisplayOptions options, - CancellationToken cancellationToken) - => HoverFactory.GetHoverAsync(codeDocument, documentFilePath, absoluteIndex, options, instance._projectManager.GetQueryOperations(), cancellationToken); - } -} diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hover/HoverService.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hover/HoverService.cs deleted file mode 100644 index cb71215835c..00000000000 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hover/HoverService.cs +++ /dev/null @@ -1,92 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the MIT license. See License.txt in the project root for license information. - -using System.Threading; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Razor.Language; -using Microsoft.AspNetCore.Razor.LanguageServer.ProjectSystem; -using Microsoft.CodeAnalysis.Razor; -using Microsoft.CodeAnalysis.Razor.DocumentMapping; -using Microsoft.CodeAnalysis.Razor.Hover; -using Microsoft.CodeAnalysis.Razor.ProjectSystem; -using Microsoft.CodeAnalysis.Razor.Protocol; -using Microsoft.VisualStudio.LanguageServer.Protocol; - -namespace Microsoft.AspNetCore.Razor.LanguageServer.Hover; - -internal sealed partial class HoverService( - IProjectSnapshotManager projectManager, - IDocumentMappingService documentMappingService, - IClientCapabilitiesService clientCapabilitiesService) : IHoverService -{ - private readonly IProjectSnapshotManager _projectManager = projectManager; - private readonly IDocumentMappingService _documentMappingService = documentMappingService; - private readonly IClientCapabilitiesService _clientCapabilitiesService = clientCapabilitiesService; - - public async Task GetRazorHoverInfoAsync(DocumentContext documentContext, DocumentPositionInfo positionInfo, CancellationToken cancellationToken) - { - // HTML can still sometimes be handled by razor. For example hovering over - // a component tag like will still be in an html context - if (positionInfo.LanguageKind == RazorLanguageKind.CSharp) - { - return null; - } - - var codeDocument = await documentContext.GetCodeDocumentAsync(cancellationToken).ConfigureAwait(false); - - // Sometimes what looks like a html attribute can actually map to C#, in which case its better to let Roslyn try to handle this. - // We can only do this if we're in single server mode though, otherwise we won't be delegating to Roslyn at all - if (_documentMappingService.TryMapToGeneratedDocumentPosition(codeDocument.GetCSharpDocument(), positionInfo.HostDocumentIndex, out _, out _)) - { - return null; - } - - var options = HoverDisplayOptions.From(_clientCapabilitiesService.ClientCapabilities); - - return await HoverFactory.GetHoverAsync( - codeDocument, - documentContext.FilePath, - positionInfo.HostDocumentIndex, - options, - _projectManager.GetQueryOperations(), - cancellationToken) - .ConfigureAwait(false); - } - - public async Task TranslateDelegatedResponseAsync(VSInternalHover? response, DocumentContext documentContext, DocumentPositionInfo positionInfo, CancellationToken cancellationToken) - { - if (response?.Range is null) - { - return response; - } - - var codeDocument = await documentContext.GetCodeDocumentAsync(cancellationToken).ConfigureAwait(false); - - // If we don't include the originally requested position in our response, the client may not show it, so we extend the range to ensure it is in there. - // eg for hovering at @bind-Value:af$$ter, we want to show people the hover for the Value property, so Roslyn will return to us the range for just the - // portion of the attribute that says "Value". - if (RazorSyntaxFacts.TryGetFullAttributeNameSpan(codeDocument, positionInfo.HostDocumentIndex, out var originalAttributeRange)) - { - var sourceText = await documentContext.GetSourceTextAsync(cancellationToken).ConfigureAwait(false); - response.Range = sourceText.GetRange(originalAttributeRange); - } - else if (positionInfo.LanguageKind == RazorLanguageKind.CSharp) - { - if (_documentMappingService.TryMapToHostDocumentRange(codeDocument.GetCSharpDocument(), response.Range, out var projectedRange)) - { - response.Range = projectedRange; - } - else - { - // We couldn't remap the range back from Roslyn, but we have to do something with it, because it definitely won't - // be correct, and if the Razor document is small, will be completely outside the valid range for the file, which - // would cause the client to error. - // Returning null here will still show the hover, just there won't be any extra visual indication, like - // a background color, applied by the client. - response.Range = null; - } - } - - return response; - } -} diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hover/IHoverService.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hover/IHoverService.cs deleted file mode 100644 index dfcf756f970..00000000000 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hover/IHoverService.cs +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the MIT license. See License.txt in the project root for license information. - -using System.Threading; -using System.Threading.Tasks; -using Microsoft.CodeAnalysis.Razor.DocumentMapping; -using Microsoft.CodeAnalysis.Razor.ProjectSystem; -using Microsoft.VisualStudio.LanguageServer.Protocol; - -namespace Microsoft.AspNetCore.Razor.LanguageServer.Hover; - -internal interface IHoverService -{ - Task GetRazorHoverInfoAsync(DocumentContext versionedDocumentContext, DocumentPositionInfo positionInfo, CancellationToken cancellationToken); - Task TranslateDelegatedResponseAsync(VSInternalHover? response, DocumentContext versionedDocumentContext, DocumentPositionInfo positionInfo, CancellationToken cancellationToken); -} diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Hover/HoverServiceTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Hover/HoverEndpointTest.cs similarity index 94% rename from src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Hover/HoverServiceTest.cs rename to src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Hover/HoverEndpointTest.cs index 0663e8665ad..f21ef8e76fe 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Hover/HoverServiceTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Hover/HoverEndpointTest.cs @@ -26,7 +26,7 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer.Test.Hover; -public class HoverServiceTest(ITestOutputHelper testOutput) : TagHelperServiceTestBase(testOutput) +public class HoverEndpointTest(ITestOutputHelper testOutput) : TagHelperServiceTestBase(testOutput) { [Fact] public async Task Handle_Hover_SingleServer_CallsDelegatedLanguageServer() @@ -199,10 +199,19 @@ public async Task Handle_Hover_SingleServer_AddTagHelper() var languageServer = new HoverLanguageServer(csharpServer, csharpDocumentUri, DisposalToken); var documentMappingService = new LspDocumentMappingService(FilePathService, documentContextFactory, LoggerFactory); - var service = GetHoverService(documentMappingService); + var projectManager = CreateProjectSnapshotManager(); + + var clientCapabilities = new VSInternalClientCapabilities() + { + TextDocument = new() { Hover = new() { ContentFormat = [MarkupKind.PlainText, MarkupKind.Markdown] } }, + SupportsVisualStudioExtensions = true + }; + + var clientCapabilitiesService = new TestClientCapabilitiesService(clientCapabilities); var endpoint = new HoverEndpoint( - service, + projectManager, + clientCapabilitiesService, languageServerFeatureOptions, documentMappingService, languageServer, @@ -221,7 +230,7 @@ public async Task Handle_Hover_SingleServer_AddTagHelper() return await endpoint.HandleRequestAsync(request, requestContext, DisposalToken); } - private (DocumentContext, Position) CreateDefaultDocumentContext() + private static (DocumentContext, Position) CreateDefaultDocumentContext() { TestCode code = """ @addTagHelper *, TestAssembly @@ -278,20 +287,6 @@ private HoverEndpoint CreateEndpoint( clientConnection ??= StrictMock.Of(); - var service = GetHoverService(); - - var endpoint = new HoverEndpoint( - service, - languageServerFeatureOptions, - documentMappingService, - clientConnection, - LoggerFactory); - - return endpoint; - } - - private HoverService GetHoverService(IDocumentMappingService? mappingService = null) - { var projectManager = CreateProjectSnapshotManager(); var clientCapabilities = new VSInternalClientCapabilities() @@ -302,9 +297,15 @@ private HoverService GetHoverService(IDocumentMappingService? mappingService = n var clientCapabilitiesService = new TestClientCapabilitiesService(clientCapabilities); - mappingService ??= StrictMock.Of(); + var endpoint = new HoverEndpoint( + projectManager, + clientCapabilitiesService, + languageServerFeatureOptions, + documentMappingService, + clientConnection, + LoggerFactory); - return new HoverService(projectManager, mappingService, clientCapabilitiesService); + return endpoint; } private class HoverLanguageServer : IClientConnection From e638ab6dcd5c3d08ff849214da91b1e881141de3 Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Mon, 28 Oct 2024 16:56:16 -0700 Subject: [PATCH 10/12] Fix up HoverFactoryTest --- .../Hover/HoverFactoryTest.cs | 60 +++++++++++-------- 1 file changed, 35 insertions(+), 25 deletions(-) diff --git a/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Hover/HoverFactoryTest.cs b/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Hover/HoverFactoryTest.cs index f881fe5927b..e2105c64c8d 100644 --- a/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Hover/HoverFactoryTest.cs +++ b/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Hover/HoverFactoryTest.cs @@ -10,6 +10,7 @@ using Microsoft.CodeAnalysis.Razor.Tooltip; using Microsoft.VisualStudio.LanguageServer.Protocol; using Microsoft.VisualStudio.Text.Adornments; +using Moq; using Xunit; using Xunit.Abstractions; @@ -22,6 +23,15 @@ public class HoverFactoryTest(ITestOutputHelper testOutput) : ToolingTestBase(te private static HoverDisplayOptions UseVisualStudio => new(MarkupKind.Markdown, SupportsVisualStudioExtensions: true); + private static ISolutionQueryOperations CreateSolutionQueryOperations() + { + var mock = new StrictMock(); + mock.Setup(x => x.GetProjectsContainingDocument(It.IsAny())) + .Returns([]); + + return mock.Object; + } + [Fact] public async Task GetHoverAsync_TagHelper_Element() { @@ -34,7 +44,7 @@ public async Task GetHoverAsync_TagHelper_Element() var codeDocument = RazorCodeDocumentFactory.CreateCodeDocument(code.Text, isRazorFile: false, SimpleTagHelpers.Default); // Act - var hover = await HoverFactory.GetHoverAsync(codeDocument, "file.cshtml", code.Position, UseMarkdown, StrictMock.Of(), DisposalToken); + var hover = await HoverFactory.GetHoverAsync(codeDocument, "file.cshtml", code.Position, UseMarkdown, CreateSolutionQueryOperations(), DisposalToken); // Assert Assert.NotNull(hover); @@ -58,7 +68,7 @@ public async Task GetHoverAsync_TagHelper_Element_WithParent() var codeDocument = RazorCodeDocumentFactory.CreateCodeDocument(code.Text, isRazorFile: false, SimpleTagHelpers.Default); // Act - var hover = await HoverFactory.GetHoverAsync(codeDocument, "file.cshtml", code.Position, UseMarkdown, StrictMock.Of(), DisposalToken); + var hover = await HoverFactory.GetHoverAsync(codeDocument, "file.cshtml", code.Position, UseMarkdown, CreateSolutionQueryOperations(), DisposalToken); // Assert Assert.NotNull(hover); @@ -82,7 +92,7 @@ public async Task GetHoverAsync_TagHelper_Attribute_WithParent() var codeDocument = RazorCodeDocumentFactory.CreateCodeDocument(code.Text, isRazorFile: false, SimpleTagHelpers.Default); // Act - var hover = await HoverFactory.GetHoverAsync(codeDocument, "file.cshtml", code.Position, UseMarkdown, StrictMock.Of(), DisposalToken); + var hover = await HoverFactory.GetHoverAsync(codeDocument, "file.cshtml", code.Position, UseMarkdown, CreateSolutionQueryOperations(), DisposalToken); // Assert Assert.NotNull(hover); @@ -104,7 +114,7 @@ public async Task GetHoverAsync_TagHelper_Element_EndTag() var codeDocument = RazorCodeDocumentFactory.CreateCodeDocument(code.Text, isRazorFile: false, SimpleTagHelpers.Default); // Act - var hover = await HoverFactory.GetHoverAsync(codeDocument, "file.cshtml", code.Position, UseMarkdown, StrictMock.Of(), DisposalToken); + var hover = await HoverFactory.GetHoverAsync(codeDocument, "file.cshtml", code.Position, UseMarkdown, CreateSolutionQueryOperations(), DisposalToken); // Assert Assert.NotNull(hover); @@ -126,7 +136,7 @@ public async Task GetHoverAsync_TagHelper_Attribute() var codeDocument = RazorCodeDocumentFactory.CreateCodeDocument(code.Text, isRazorFile: false, SimpleTagHelpers.Default); // Act - var hover = await HoverFactory.GetHoverAsync(codeDocument, "file.cshtml", code.Position, UseMarkdown, StrictMock.Of(), DisposalToken); + var hover = await HoverFactory.GetHoverAsync(codeDocument, "file.cshtml", code.Position, UseMarkdown, CreateSolutionQueryOperations(), DisposalToken); // Assert Assert.NotNull(hover); @@ -149,7 +159,7 @@ public async Task GetHoverAsync_TagHelper_AttributeTrailingEdge() var codeDocument = RazorCodeDocumentFactory.CreateCodeDocument(code.Text, isRazorFile: false, SimpleTagHelpers.Default); // Act - var hover = await HoverFactory.GetHoverAsync(codeDocument, "file.cshtml", code.Position, UseMarkdown, StrictMock.Of(), DisposalToken); + var hover = await HoverFactory.GetHoverAsync(codeDocument, "file.cshtml", code.Position, UseMarkdown, CreateSolutionQueryOperations(), DisposalToken); // Assert Assert.NotNull(hover); @@ -172,7 +182,7 @@ public async Task GetHoverAsync_TagHelper_AttributeValue_ReturnsNull() var codeDocument = RazorCodeDocumentFactory.CreateCodeDocument(code.Text, isRazorFile: false, SimpleTagHelpers.Default); // Act - var hover = await HoverFactory.GetHoverAsync(codeDocument, "file.cshtml", code.Position, UseMarkdown, StrictMock.Of(), DisposalToken); + var hover = await HoverFactory.GetHoverAsync(codeDocument, "file.cshtml", code.Position, UseMarkdown, CreateSolutionQueryOperations(), DisposalToken); // Assert Assert.Null(hover); @@ -190,7 +200,7 @@ public async Task GetHoverAsync_TagHelper_AfterAttributeEquals_ReturnsNull() var codeDocument = RazorCodeDocumentFactory.CreateCodeDocument(code.Text, isRazorFile: false, SimpleTagHelpers.Default); // Act - var hover = await HoverFactory.GetHoverAsync(codeDocument, "file.cshtml", code.Position, UseMarkdown, StrictMock.Of(), DisposalToken); + var hover = await HoverFactory.GetHoverAsync(codeDocument, "file.cshtml", code.Position, UseMarkdown, CreateSolutionQueryOperations(), DisposalToken); // Assert Assert.Null(hover); @@ -208,7 +218,7 @@ public async Task GetHoverAsync_TagHelper_AttributeEnd_ReturnsNull() var codeDocument = RazorCodeDocumentFactory.CreateCodeDocument(code.Text, isRazorFile: false, SimpleTagHelpers.Default); // Act - var hover = await HoverFactory.GetHoverAsync(codeDocument, "file.cshtml", code.Position, UseMarkdown, StrictMock.Of(), DisposalToken); + var hover = await HoverFactory.GetHoverAsync(codeDocument, "file.cshtml", code.Position, UseMarkdown, CreateSolutionQueryOperations(), DisposalToken); // Assert Assert.Null(hover); @@ -226,7 +236,7 @@ public async Task GetHoverAsync_TagHelper_MinimizedAttribute() var codeDocument = RazorCodeDocumentFactory.CreateCodeDocument(code.Text, isRazorFile: false, SimpleTagHelpers.Default); // Act - var hover = await HoverFactory.GetHoverAsync(codeDocument, "file.cshtml", code.Position, UseMarkdown, StrictMock.Of(), DisposalToken); + var hover = await HoverFactory.GetHoverAsync(codeDocument, "file.cshtml", code.Position, UseMarkdown, CreateSolutionQueryOperations(), DisposalToken); // Assert Assert.NotNull(hover); @@ -253,7 +263,7 @@ public void Increment(){ var codeDocument = RazorCodeDocumentFactory.CreateCodeDocument(code.Text, "text.razor", SimpleTagHelpers.Default); // Act - var hover = await HoverFactory.GetHoverAsync(codeDocument, "file.cshtml", code.Position, UseMarkdown, StrictMock.Of(), DisposalToken); + var hover = await HoverFactory.GetHoverAsync(codeDocument, "file.cshtml", code.Position, UseMarkdown, CreateSolutionQueryOperations(), DisposalToken); // Assert Assert.NotNull(hover); @@ -275,7 +285,7 @@ public async Task GetHoverAsync_TagHelper_MalformedElement() var codeDocument = RazorCodeDocumentFactory.CreateCodeDocument(code.Text, isRazorFile: false, SimpleTagHelpers.Default); // Act - var hover = await HoverFactory.GetHoverAsync(codeDocument, "file.cshtml", code.Position, UseMarkdown, StrictMock.Of(), DisposalToken); + var hover = await HoverFactory.GetHoverAsync(codeDocument, "file.cshtml", code.Position, UseMarkdown, CreateSolutionQueryOperations(), DisposalToken); // Assert Assert.NotNull(hover); @@ -297,7 +307,7 @@ public async Task GetHoverAsync_TagHelper_MalformedAttribute() var codeDocument = RazorCodeDocumentFactory.CreateCodeDocument(code.Text, isRazorFile: false, SimpleTagHelpers.Default); // Act - var hover = await HoverFactory.GetHoverAsync(codeDocument, "file.cshtml", code.Position, UseMarkdown, StrictMock.Of(), DisposalToken); + var hover = await HoverFactory.GetHoverAsync(codeDocument, "file.cshtml", code.Position, UseMarkdown, CreateSolutionQueryOperations(), DisposalToken); // Assert Assert.NotNull(hover); @@ -320,7 +330,7 @@ public async Task GetHoverAsync_HTML_MarkupElement() var codeDocument = RazorCodeDocumentFactory.CreateCodeDocument(code.Text, isRazorFile: false); // Act - var hover = await HoverFactory.GetHoverAsync(codeDocument, "file.cshtml", code.Position, UseMarkdown, StrictMock.Of(), DisposalToken); + var hover = await HoverFactory.GetHoverAsync(codeDocument, "file.cshtml", code.Position, UseMarkdown, CreateSolutionQueryOperations(), DisposalToken); // Assert Assert.Null(hover); @@ -338,7 +348,7 @@ public async Task GetHoverAsync_TagHelper_PlainTextElement() var codeDocument = RazorCodeDocumentFactory.CreateCodeDocument(code.Text, isRazorFile: false, SimpleTagHelpers.Default); // Act - var hover = await HoverFactory.GetHoverAsync(codeDocument, "file.cshtml", code.Position, UsePlainText, StrictMock.Of(), DisposalToken); + var hover = await HoverFactory.GetHoverAsync(codeDocument, "file.cshtml", code.Position, UsePlainText, CreateSolutionQueryOperations(), DisposalToken); // Assert Assert.NotNull(hover); @@ -361,7 +371,7 @@ public async Task GetHoverAsync_TagHelper_PlainTextElement_EndTag() var codeDocument = RazorCodeDocumentFactory.CreateCodeDocument(code.Text, isRazorFile: false, SimpleTagHelpers.Default); // Act - var hover = await HoverFactory.GetHoverAsync(codeDocument, "file.cshtml", code.Position, UsePlainText, StrictMock.Of(), DisposalToken); + var hover = await HoverFactory.GetHoverAsync(codeDocument, "file.cshtml", code.Position, UsePlainText, CreateSolutionQueryOperations(), DisposalToken); // Assert Assert.NotNull(hover); @@ -383,7 +393,7 @@ public async Task GetHoverAsync_TagHelper_TextComponent() var codeDocument = RazorCodeDocumentFactory.CreateCodeDocument(code.Text, isRazorFile: true, SimpleTagHelpers.Default); // Act - var hover = await HoverFactory.GetHoverAsync(codeDocument, "file.razor", code.Position, UsePlainText, StrictMock.Of(), DisposalToken); + var hover = await HoverFactory.GetHoverAsync(codeDocument, "file.razor", code.Position, UsePlainText, CreateSolutionQueryOperations(), DisposalToken); // Assert Assert.NotNull(hover); @@ -407,7 +417,7 @@ public async Task GetHoverAsync_TagHelper_TextComponent_NestedInHtml() var codeDocument = RazorCodeDocumentFactory.CreateCodeDocument(code.Text, isRazorFile: true, SimpleTagHelpers.Default); // Act - var hover = await HoverFactory.GetHoverAsync(codeDocument, "file.razor", code.Position, UsePlainText, StrictMock.Of(), DisposalToken); + var hover = await HoverFactory.GetHoverAsync(codeDocument, "file.razor", code.Position, UsePlainText, CreateSolutionQueryOperations(), DisposalToken); // Assert Assert.NotNull(hover); @@ -432,7 +442,7 @@ public async Task GetHoverAsync_TagHelper_TextComponent_NestedInCSharp() var codeDocument = RazorCodeDocumentFactory.CreateCodeDocument(code.Text, isRazorFile: true, SimpleTagHelpers.Default); // Act - var hover = await HoverFactory.GetHoverAsync(codeDocument, "file.razor", code.Position, UsePlainText, StrictMock.Of(), DisposalToken); + var hover = await HoverFactory.GetHoverAsync(codeDocument, "file.razor", code.Position, UsePlainText, CreateSolutionQueryOperations(), DisposalToken); // Assert Assert.Null(hover); @@ -454,7 +464,7 @@ public async Task GetHoverAsync_TagHelper_TextComponent_NestedInCSharpAndText() var codeDocument = RazorCodeDocumentFactory.CreateCodeDocument(code.Text, isRazorFile: true, SimpleTagHelpers.Default); // Act - var hover = await HoverFactory.GetHoverAsync(codeDocument, "file.razor", code.Position, UsePlainText, StrictMock.Of(), DisposalToken); + var hover = await HoverFactory.GetHoverAsync(codeDocument, "file.razor", code.Position, UsePlainText, CreateSolutionQueryOperations(), DisposalToken); // Assert Assert.NotNull(hover); @@ -477,7 +487,7 @@ public async Task GetHoverAsync_TagHelper_PlainTextAttribute() var codeDocument = RazorCodeDocumentFactory.CreateCodeDocument(code.Text, isRazorFile: false, SimpleTagHelpers.Default); // Act - var hover = await HoverFactory.GetHoverAsync(codeDocument, "file.cshtml", code.Position, UsePlainText, StrictMock.Of(), DisposalToken); + var hover = await HoverFactory.GetHoverAsync(codeDocument, "file.cshtml", code.Position, UsePlainText, CreateSolutionQueryOperations(), DisposalToken); // Assert Assert.NotNull(hover); @@ -501,7 +511,7 @@ public async Task GetHoverAsync_HTML_PlainTextElement() var codeDocument = RazorCodeDocumentFactory.CreateCodeDocument(code.Text, isRazorFile: false); // Act - var hover = await HoverFactory.GetHoverAsync(codeDocument, "file.cshtml", code.Position, UsePlainText, StrictMock.Of(), DisposalToken); + var hover = await HoverFactory.GetHoverAsync(codeDocument, "file.cshtml", code.Position, UsePlainText, CreateSolutionQueryOperations(), DisposalToken); // Assert Assert.Null(hover); @@ -519,7 +529,7 @@ public async Task GetHoverAsync_HTML_PlainTextAttribute() var codeDocument = RazorCodeDocumentFactory.CreateCodeDocument(code.Text, isRazorFile: false); // Act - var hover = await HoverFactory.GetHoverAsync(codeDocument, "file.cshtml", code.Position, UsePlainText, StrictMock.Of(), DisposalToken); + var hover = await HoverFactory.GetHoverAsync(codeDocument, "file.cshtml", code.Position, UsePlainText, CreateSolutionQueryOperations(), DisposalToken); // Assert Assert.Null(hover); @@ -537,7 +547,7 @@ public async Task GetHoverAsync_TagHelper_Element_VSClient_ReturnVSHover() var codeDocument = RazorCodeDocumentFactory.CreateCodeDocument(code.Text, isRazorFile: false, SimpleTagHelpers.Default); // Act - var vsHover = await HoverFactory.GetHoverAsync(codeDocument, "file.cshtml", code.Position, UseVisualStudio, StrictMock.Of(), DisposalToken); + var vsHover = await HoverFactory.GetHoverAsync(codeDocument, "file.cshtml", code.Position, UseVisualStudio, CreateSolutionQueryOperations(), DisposalToken); // Assert Assert.NotNull(vsHover); @@ -574,7 +584,7 @@ public async Task GetHoverAsync_TagHelper_Attribute_VSClient_ReturnVSHover() var codeDocument = RazorCodeDocumentFactory.CreateCodeDocument(code.Text, isRazorFile: false, SimpleTagHelpers.Default); // Act - var vsHover = await HoverFactory.GetHoverAsync(codeDocument, "file.cshtml", code.Position, UseVisualStudio, StrictMock.Of(), DisposalToken); + var vsHover = await HoverFactory.GetHoverAsync(codeDocument, "file.cshtml", code.Position, UseVisualStudio, CreateSolutionQueryOperations(), DisposalToken); // Assert Assert.NotNull(vsHover); From 31eacc3e0d2a443f65e9fb9eb82cfaee8cf3916c Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Wed, 30 Oct 2024 13:41:51 -0700 Subject: [PATCH 11/12] Add back SingleServerSupport check that was accidentally removed awhile ago --- .../Hover/HoverEndpoint.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hover/HoverEndpoint.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hover/HoverEndpoint.cs index 7ebdb1b9950..d58d32e8b43 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hover/HoverEndpoint.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hover/HoverEndpoint.cs @@ -80,7 +80,7 @@ public void ApplyCapabilities(VSInternalServerCapabilities serverCapabilities, V // Sometimes what looks like a html attribute can actually map to C#, in which case its better to let Roslyn try to handle this. // We can only do this if we're in single server mode though, otherwise we won't be delegating to Roslyn at all - if (DocumentMappingService.TryMapToGeneratedDocumentPosition(codeDocument.GetCSharpDocument(), positionInfo.HostDocumentIndex, out _, out _)) + if (SingleServerSupport && DocumentMappingService.TryMapToGeneratedDocumentPosition(codeDocument.GetCSharpDocument(), positionInfo.HostDocumentIndex, out _, out _)) { return null; } From aa3209ad0e5d5009535a3e4315848ee0808f449f Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Wed, 30 Oct 2024 13:45:26 -0700 Subject: [PATCH 12/12] Remove superfluous argument null checks --- .../AbstractRazorDelegatingEndpoint.cs | 29 +++++++------------ 1 file changed, 10 insertions(+), 19 deletions(-) diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/AbstractRazorDelegatingEndpoint.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/AbstractRazorDelegatingEndpoint.cs index 60f8123e660..32799f778a5 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/AbstractRazorDelegatingEndpoint.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/AbstractRazorDelegatingEndpoint.cs @@ -18,26 +18,17 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer; -internal abstract class AbstractRazorDelegatingEndpoint : IRazorRequestHandler - where TRequest : ITextDocumentPositionParams +internal abstract class AbstractRazorDelegatingEndpoint( + LanguageServerFeatureOptions languageServerFeatureOptions, + IDocumentMappingService documentMappingService, + IClientConnection clientConnection, + ILogger logger) + : IRazorRequestHandler where TRequest : ITextDocumentPositionParams { - private readonly LanguageServerFeatureOptions _languageServerFeatureOptions; - protected readonly IDocumentMappingService DocumentMappingService; - private readonly IClientConnection _clientConnection; - protected readonly ILogger Logger; - - protected AbstractRazorDelegatingEndpoint( - LanguageServerFeatureOptions languageServerFeatureOptions, - IDocumentMappingService documentMappingService, - IClientConnection clientConnection, - ILogger logger) - { - _languageServerFeatureOptions = languageServerFeatureOptions ?? throw new ArgumentNullException(nameof(languageServerFeatureOptions)); - DocumentMappingService = documentMappingService ?? throw new ArgumentNullException(nameof(documentMappingService)); - _clientConnection = clientConnection ?? throw new ArgumentNullException(nameof(clientConnection)); - - Logger = logger ?? throw new ArgumentNullException(nameof(logger)); - } + private readonly LanguageServerFeatureOptions _languageServerFeatureOptions = languageServerFeatureOptions; + protected readonly IDocumentMappingService DocumentMappingService = documentMappingService; + private readonly IClientConnection _clientConnection = clientConnection; + protected readonly ILogger Logger = logger; /// /// The strategy to use to project the incoming caret position onto the generated C#/Html document