From a5469810e1fc8e31d668776a985ef8b0c059c129 Mon Sep 17 00:00:00 2001 From: David Wengier Date: Thu, 18 Jul 2024 14:32:45 +1000 Subject: [PATCH 1/6] Initial tests --- .../Cohost/CohostUriPresentationEndpoint.cs | 22 ++-- .../Cohost/CohostEndpointTestBase.cs | 9 +- .../CohostUriPresentationEndpointTest.cs | 101 ++++++++++++++++++ 3 files changed, 124 insertions(+), 8 deletions(-) create mode 100644 src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostUriPresentationEndpointTest.cs diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostUriPresentationEndpoint.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostUriPresentationEndpoint.cs index b067987020a..2db3fa2908d 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostUriPresentationEndpoint.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostUriPresentationEndpoint.cs @@ -5,6 +5,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Razor; +using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost; using Microsoft.CodeAnalysis.Razor.Remote; using Microsoft.CodeAnalysis.Razor.Workspaces; @@ -58,14 +59,15 @@ internal class CohostUriPresentationEndpoint( protected override RazorTextDocumentIdentifier? GetRazorTextDocumentIdentifier(VSInternalUriPresentationParams request) => request.TextDocument.ToRazorTextDocumentIdentifier(); - protected override async Task HandleRequestAsync(VSInternalUriPresentationParams request, RazorCohostRequestContext context, CancellationToken cancellationToken) - { - var razorDocument = context.TextDocument.AssumeNotNull(); + protected override Task HandleRequestAsync(VSInternalUriPresentationParams request, RazorCohostRequestContext context, CancellationToken cancellationToken) + => HandleRequestAsync(request, context.TextDocument.AssumeNotNull(), cancellationToken); + private async Task HandleRequestAsync(VSInternalUriPresentationParams request, TextDocument razorDocument, CancellationToken cancellationToken) + { var data = await _remoteServiceInvoker.TryInvokeAsync( - razorDocument.Project.Solution, - (service, solutionInfo, cancellationToken) => service.GetPresentationAsync(solutionInfo, razorDocument.Id, request.Range.ToLinePositionSpan(), request.Uris, cancellationToken), - cancellationToken).ConfigureAwait(false); + razorDocument.Project.Solution, + (service, solutionInfo, cancellationToken) => service.GetPresentationAsync(solutionInfo, razorDocument.Id, request.Range.ToLinePositionSpan(), request.Uris, cancellationToken), + cancellationToken).ConfigureAwait(false); // If we got a response back, then either Razor or C# wants to do something with this, so we're good to go if (data is { } textChange) @@ -134,4 +136,12 @@ internal class CohostUriPresentationEndpoint( return workspaceEdit; } + + internal TestAccessor GetTestAccessor() => new(this); + + internal readonly struct TestAccessor(CohostUriPresentationEndpoint instance) + { + public Task HandleRequestAsync(VSInternalUriPresentationParams request, TextDocument razorDocument, CancellationToken cancellationToken) + => instance.HandleRequestAsync(request, razorDocument, cancellationToken); + } } diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostEndpointTestBase.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostEndpointTestBase.cs index 3a6e17f38fc..835c4ec4fc4 100644 --- a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostEndpointTestBase.cs +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostEndpointTestBase.cs @@ -12,6 +12,7 @@ using Microsoft.AspNetCore.Razor.Test.Common.Workspaces; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Razor.Remote; +using Microsoft.CodeAnalysis.Razor.Workspaces; using Microsoft.CodeAnalysis.Remote.Razor; using Microsoft.CodeAnalysis.Text; using Microsoft.VisualStudio.Composition; @@ -25,8 +26,11 @@ public abstract class CohostEndpointTestBase(ITestOutputHelper testOutputHelper) private ExportProvider? _exportProvider; private TestRemoteServiceInvoker? _remoteServiceInvoker; private RemoteClientInitializationOptions _clientInitializationOptions; + private IFilePathService? _filePathService; private protected TestRemoteServiceInvoker RemoteServiceInvoker => _remoteServiceInvoker.AssumeNotNull(); + private protected IFilePathService FilePathService => _filePathService.AssumeNotNull(); + private protected RemoteLanguageServerFeatureOptions FeatureOptions => OOPExportProvider.GetExportedValue(); /// /// The export provider for Razor OOP services (not Roslyn) @@ -54,13 +58,14 @@ protected override async Task InitializeAsync() UseRazorCohostServer = true }; UpdateClientInitializationOptions(c => c); + + _filePathService = new RemoteFilePathService(FeatureOptions); } private protected void UpdateClientInitializationOptions(Func mutation) { _clientInitializationOptions = mutation(_clientInitializationOptions); - var featureOptions = OOPExportProvider.GetExportedValue(); - featureOptions.SetOptions(_clientInitializationOptions); + FeatureOptions.SetOptions(_clientInitializationOptions); } protected TextDocument CreateProjectAndRazorDocument(string contents, string? fileKind = null) diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostUriPresentationEndpointTest.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostUriPresentationEndpointTest.cs new file mode 100644 index 00000000000..cda1c497657 --- /dev/null +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostUriPresentationEndpointTest.cs @@ -0,0 +1,101 @@ +// 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.Tasks; +using Microsoft.CodeAnalysis.ExternalAccess.Razor; +using Microsoft.CodeAnalysis.Razor.Workspaces; +using Microsoft.CodeAnalysis.Testing; +using Microsoft.VisualStudio.LanguageServer.Protocol; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.VisualStudio.Razor.LanguageClient.Cohost; + +public class CohostUriPresentationEndpointTest(ITestOutputHelper testOutputHelper) : CohostEndpointTestBase(testOutputHelper) +{ + [Fact] + public async Task RandomFile() + { + await VerifyUriPresentationAsync( + input: """ + This is a Razor document. + +
+ [||] +
+ + The end. + """, + uris: [new("file:///C:/path/to/SomeRandomFile.txt")], + // In reality this would actual insert the full path, but the Html server does that for us, and we + // have other tests that validate that we insert what the Html server tells us + expected: null); + } + + [Fact] + public async Task HtmlResponse_TranslatesVirtualDocumentUri() + { + var htmlTag = """"""; + + await VerifyUriPresentationAsync( + input: """ + This is a Razor document. + +
+ [||] +
+ + The end. + """, + uris: [new("file:///C:/path/to/site.css")], + htmlResponse: new WorkspaceEdit + { + DocumentChanges = new TextDocumentEdit[] + { + new() + { + TextDocument = new() + { + Uri = new("file:///c:/users/example/src/SomeProject/File1.razor.g.html") + }, + Edits = [new() { NewText = htmlTag}] + } + } + }, + expected: htmlTag); + } + + private async Task VerifyUriPresentationAsync(string input, Uri[] uris, string? expected, WorkspaceEdit? htmlResponse = null) + { + TestFileMarkupParser.GetSpan(input, out input, out var span); + var document = CreateProjectAndRazorDocument(input); + var sourceText = await document.GetTextAsync(DisposalToken); + + var requestInvoker = new TestLSPRequestInvoker([(VSInternalMethods.TextDocumentUriPresentationName, htmlResponse)]); + + var endpoint = new CohostUriPresentationEndpoint(RemoteServiceInvoker, TestHtmlDocumentSynchronizer.Instance, FilePathService, requestInvoker); + + var request = new VSInternalUriPresentationParams() + { + TextDocument = new TextDocumentIdentifier() + { + Uri = document.CreateUri() + }, + Range = span.ToRange(sourceText), + Uris = uris + }; + + var result = await endpoint.GetTestAccessor().HandleRequestAsync(request, document, DisposalToken); + + if (result is null) + { + Assert.Null(expected); + } + else + { + Assert.Equal(expected, result!.DocumentChanges!.Value.First[0].Edits[0].NewText); + Assert.Equal(document.CreateUri(), result?.DocumentChanges?.First[0].TextDocument.Uri); + } + } +} From b01f6a7b5b030e76838a217971690ea4f3c571ec Mon Sep 17 00:00:00 2001 From: David Wengier Date: Thu, 18 Jul 2024 15:47:37 +1000 Subject: [PATCH 2/6] Moar tests! --- .../TestProjectData.cs | 31 ++-- .../Cohost/CohostEndpointTestBase.cs | 13 +- .../CohostUriPresentationEndpointTest.cs | 172 +++++++++++++++++- 3 files changed, 198 insertions(+), 18 deletions(-) diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/TestProjectData.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/TestProjectData.cs index 15100f19680..32291f15f8d 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/TestProjectData.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/TestProjectData.cs @@ -19,22 +19,22 @@ internal static class TestProjectData static TestProjectData() { var baseDirectory = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "c:\\users\\example\\src" : "/home/example"; - var someProjectPath = Path.Combine(baseDirectory, "SomeProject"); - var someProjectObjPath = Path.Combine(someProjectPath, "obj"); + SomeProjectPath = Path.Combine(baseDirectory, "SomeProject"); + var someProjectObjPath = Path.Combine(SomeProjectPath, "obj"); - SomeProject = new HostProject(Path.Combine(someProjectPath, "SomeProject.csproj"), someProjectObjPath, RazorConfiguration.Default, "SomeProject"); - SomeProjectFile1 = new HostDocument(Path.Combine(someProjectPath, "File1.cshtml"), "File1.cshtml", FileKinds.Legacy); - SomeProjectFile2 = new HostDocument(Path.Combine(someProjectPath, "File2.cshtml"), "File2.cshtml", FileKinds.Legacy); - SomeProjectImportFile = new HostDocument(Path.Combine(someProjectPath, "_ViewImports.cshtml"), "_ViewImports.cshtml", FileKinds.Legacy); - SomeProjectNestedFile3 = new HostDocument(Path.Combine(someProjectPath, "Nested", "File3.cshtml"), "Nested\\File3.cshtml", FileKinds.Legacy); - SomeProjectNestedFile4 = new HostDocument(Path.Combine(someProjectPath, "Nested", "File4.cshtml"), "Nested\\File4.cshtml", FileKinds.Legacy); - SomeProjectNestedImportFile = new HostDocument(Path.Combine(someProjectPath, "Nested", "_ViewImports.cshtml"), "Nested\\_ViewImports.cshtml", FileKinds.Legacy); - SomeProjectComponentFile1 = new HostDocument(Path.Combine(someProjectPath, "File1.razor"), "File1.razor", FileKinds.Component); - SomeProjectComponentFile2 = new HostDocument(Path.Combine(someProjectPath, "File2.razor"), "File2.razor", FileKinds.Component); - SomeProjectComponentImportFile1 = new HostDocument(Path.Combine(someProjectPath, "_Imports.razor"), "_Imports.razor", FileKinds.Component); - SomeProjectNestedComponentFile3 = new HostDocument(Path.Combine(someProjectPath, "Nested", "File3.razor"), "Nested\\File3.razor", FileKinds.Component); - SomeProjectNestedComponentFile4 = new HostDocument(Path.Combine(someProjectPath, "Nested", "File4.razor"), "Nested\\File4.razor", FileKinds.Component); - SomeProjectCshtmlComponentFile5 = new HostDocument(Path.Combine(someProjectPath, "File5.cshtml"), "File5.cshtml", FileKinds.Component); + SomeProject = new HostProject(Path.Combine(SomeProjectPath, "SomeProject.csproj"), someProjectObjPath, RazorConfiguration.Default, "SomeProject"); + SomeProjectFile1 = new HostDocument(Path.Combine(SomeProjectPath, "File1.cshtml"), "File1.cshtml", FileKinds.Legacy); + SomeProjectFile2 = new HostDocument(Path.Combine(SomeProjectPath, "File2.cshtml"), "File2.cshtml", FileKinds.Legacy); + SomeProjectImportFile = new HostDocument(Path.Combine(SomeProjectPath, "_ViewImports.cshtml"), "_ViewImports.cshtml", FileKinds.Legacy); + SomeProjectNestedFile3 = new HostDocument(Path.Combine(SomeProjectPath, "Nested", "File3.cshtml"), "Nested\\File3.cshtml", FileKinds.Legacy); + SomeProjectNestedFile4 = new HostDocument(Path.Combine(SomeProjectPath, "Nested", "File4.cshtml"), "Nested\\File4.cshtml", FileKinds.Legacy); + SomeProjectNestedImportFile = new HostDocument(Path.Combine(SomeProjectPath, "Nested", "_ViewImports.cshtml"), "Nested\\_ViewImports.cshtml", FileKinds.Legacy); + SomeProjectComponentFile1 = new HostDocument(Path.Combine(SomeProjectPath, "File1.razor"), "File1.razor", FileKinds.Component); + SomeProjectComponentFile2 = new HostDocument(Path.Combine(SomeProjectPath, "File2.razor"), "File2.razor", FileKinds.Component); + SomeProjectComponentImportFile1 = new HostDocument(Path.Combine(SomeProjectPath, "_Imports.razor"), "_Imports.razor", FileKinds.Component); + SomeProjectNestedComponentFile3 = new HostDocument(Path.Combine(SomeProjectPath, "Nested", "File3.razor"), "Nested\\File3.razor", FileKinds.Component); + SomeProjectNestedComponentFile4 = new HostDocument(Path.Combine(SomeProjectPath, "Nested", "File4.razor"), "Nested\\File4.razor", FileKinds.Component); + SomeProjectCshtmlComponentFile5 = new HostDocument(Path.Combine(SomeProjectPath, "File5.cshtml"), "File5.cshtml", FileKinds.Component); var anotherProjectPath = Path.Combine(baseDirectory, "AnotherProject"); var anotherProjectObjPath = Path.Combine(anotherProjectPath, "obj"); @@ -53,6 +53,7 @@ static TestProjectData() } public static readonly HostProject SomeProject; + public static readonly string SomeProjectPath; public static readonly HostDocument SomeProjectFile1; public static readonly HostDocument SomeProjectFile2; public static readonly HostDocument SomeProjectImportFile; diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostEndpointTestBase.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostEndpointTestBase.cs index 835c4ec4fc4..46d95519203 100644 --- a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostEndpointTestBase.cs +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostEndpointTestBase.cs @@ -68,7 +68,7 @@ private protected void UpdateClientInitializationOptions(Func r.Reference)); var solution = Workspace.CurrentSolution.AddProject(projectInfo); @@ -124,6 +125,16 @@ @using Microsoft.AspNetCore.Components.Web """), filePath: TestProjectData.SomeProjectImportFile.FilePath); + if (additionalFiles is not null) + { + foreach (var file in additionalFiles) + { + solution = Path.GetExtension(file.fileName) == ".cs" + ? solution.AddDocument(DocumentId.CreateNewId(projectId), name: file.fileName, text: SourceText.From(file.contents), filePath: file.fileName) + : solution.AddAdditionalDocument(DocumentId.CreateNewId(projectId), name: file.fileName, text: SourceText.From(file.contents), filePath: file.fileName); + } + } + return solution.GetAdditionalDocument(documentId).AssumeNotNull(); } } diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostUriPresentationEndpointTest.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostUriPresentationEndpointTest.cs index cda1c497657..a0e7b742fd7 100644 --- a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostUriPresentationEndpointTest.cs +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostUriPresentationEndpointTest.cs @@ -2,7 +2,9 @@ // Licensed under the MIT license. See License.txt in the project root for license information. using System; +using System.IO; using System.Threading.Tasks; +using Microsoft.AspNetCore.Razor.Test.Common; using Microsoft.CodeAnalysis.ExternalAccess.Razor; using Microsoft.CodeAnalysis.Razor.Workspaces; using Microsoft.CodeAnalysis.Testing; @@ -66,10 +68,176 @@ The end. expected: htmlTag); } - private async Task VerifyUriPresentationAsync(string input, Uri[] uris, string? expected, WorkspaceEdit? htmlResponse = null) + [Fact] + public async Task Component() + { + await VerifyUriPresentationAsync( + input: """ + This is a Razor document. + +
+ [||] +
+ + The end. + """, + additionalFiles: [ + // The source generator isn't hooked up to our test project, so we have to manually "compile" the razor file + (Path.Combine(TestProjectData.SomeProjectPath, "Component.cs"), """ + namespace SomeProject; + + public class Component : Microsoft.AspNetCore.Components.ComponentBase + { + } + """), + // The above will make the component exist, but the .razor file needs to exist too for Uri presentation + (Path.Combine(TestProjectData.SomeProjectPath, "Component.razor"), """ + This doesn't matter + """) + ], + uris: [new(Path.Combine(TestProjectData.SomeProjectPath, "Component.razor"))], + expected: ""); + } + + [Fact] + public async Task Component_IntoCSharp_NoTag() + { + await VerifyUriPresentationAsync( + input: """ + This is a Razor document. + +
+
+ + @code { + [||] + } + """, + additionalFiles: [ + // The source generator isn't hooked up to our test project, so we have to manually "compile" the razor file + (Path.Combine(TestProjectData.SomeProjectPath, "Component.cs"), """ + namespace SomeProject; + + public class Component : Microsoft.AspNetCore.Components.ComponentBase + { + } + """), + // The above will make the component exist, but the .razor file needs to exist too for Uri presentation + (Path.Combine(TestProjectData.SomeProjectPath, "Component.razor"), """ + This doesn't matter + """) + ], + uris: [new(Path.Combine(TestProjectData.SomeProjectPath, "Component.razor"))], + expected: null); + } + + [Fact] + public async Task Component_WithChildFile() + { + await VerifyUriPresentationAsync( + input: """ + This is a Razor document. + +
+ [||] +
+ + The end. + """, + additionalFiles: [ + (Path.Combine(TestProjectData.SomeProjectPath, "Component.cs"), """ + namespace SomeProject; + + public class Component : Microsoft.AspNetCore.Components.ComponentBase + { + } + """), + (Path.Combine(TestProjectData.SomeProjectPath, "Component.razor"), """ + This doesn't matter + """) + ], + uris: [ + new(Path.Combine(TestProjectData.SomeProjectPath, "Component.razor")), + new(Path.Combine(TestProjectData.SomeProjectPath, "Component.razor.css")), + new(Path.Combine(TestProjectData.SomeProjectPath, "Component.razor.js")) + ], + expected: ""); + } + + [Fact] + public async Task Component_WithChildFile_RazorNotFirst() + { + await VerifyUriPresentationAsync( + input: """ + This is a Razor document. + +
+ [||] +
+ + The end. + """, + additionalFiles: [ + (Path.Combine(TestProjectData.SomeProjectPath, "Component.cs"), """ + namespace SomeProject; + + public class Component : Microsoft.AspNetCore.Components.ComponentBase + { + } + """), + (Path.Combine(TestProjectData.SomeProjectPath, "Component.razor"), """ + This doesn't matter + """) + ], + uris: [ + new(Path.Combine(TestProjectData.SomeProjectPath, "Component.razor.css")), + new(Path.Combine(TestProjectData.SomeProjectPath, "Component.razor")), + new(Path.Combine(TestProjectData.SomeProjectPath, "Component.razor.js")) + ], + expected: ""); + } + + [Fact] + public async Task Component_RequiredParameter() + { + await VerifyUriPresentationAsync( + input: """ + This is a Razor document. + +
+ [||] +
+ + The end. + """, + additionalFiles: [ + (Path.Combine(TestProjectData.SomeProjectPath, "Component.cs"), """ + using Microsoft.AspNetCore.Components; + + namespace SomeProject; + + public class Component : ComponentBase + { + [Parameter] + [EditorRequired] + public string RequiredParameter { get; set; } + + [Parameter] + public string NormalParameter { get; set; } + } + """), + (Path.Combine(TestProjectData.SomeProjectPath, "Component.razor"), """ + This doesn't matter + """) + ], + uris: [new(Path.Combine(TestProjectData.SomeProjectPath, "Component.razor"))], + expected: """"""); + } + + private async Task VerifyUriPresentationAsync(string input, Uri[] uris, string? expected, WorkspaceEdit? htmlResponse = null, (string fileName, string contents)[]? additionalFiles = null) { TestFileMarkupParser.GetSpan(input, out input, out var span); - var document = CreateProjectAndRazorDocument(input); + var document = CreateProjectAndRazorDocument(input, additionalFiles: additionalFiles); var sourceText = await document.GetTextAsync(DisposalToken); var requestInvoker = new TestLSPRequestInvoker([(VSInternalMethods.TextDocumentUriPresentationName, htmlResponse)]); From ed318950ca2cf90135c5db3b2d36c2dbe9d7944d Mon Sep 17 00:00:00 2001 From: David Wengier Date: Fri, 19 Jul 2024 07:17:48 +1000 Subject: [PATCH 3/6] PR Feedback --- .../CohostUriPresentationEndpointTest.cs | 68 +++++++++---------- 1 file changed, 33 insertions(+), 35 deletions(-) diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostUriPresentationEndpointTest.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostUriPresentationEndpointTest.cs index a0e7b742fd7..995dff293a2 100644 --- a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostUriPresentationEndpointTest.cs +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostUriPresentationEndpointTest.cs @@ -29,7 +29,7 @@ This is a Razor document. The end. """, - uris: [new("file:///C:/path/to/SomeRandomFile.txt")], + uris: [FileUri("SomeRandomFile.txt")], // In reality this would actual insert the full path, but the Html server does that for us, and we // have other tests that validate that we insert what the Html server tells us expected: null); @@ -59,7 +59,7 @@ The end. { TextDocument = new() { - Uri = new("file:///c:/users/example/src/SomeProject/File1.razor.g.html") + Uri = FileUri("File1.razor.g.html") }, Edits = [new() { NewText = htmlTag}] } @@ -83,7 +83,7 @@ The end. """, additionalFiles: [ // The source generator isn't hooked up to our test project, so we have to manually "compile" the razor file - (Path.Combine(TestProjectData.SomeProjectPath, "Component.cs"), """ + (File("Component.cs"), """ namespace SomeProject; public class Component : Microsoft.AspNetCore.Components.ComponentBase @@ -91,11 +91,9 @@ public class Component : Microsoft.AspNetCore.Components.ComponentBase } """), // The above will make the component exist, but the .razor file needs to exist too for Uri presentation - (Path.Combine(TestProjectData.SomeProjectPath, "Component.razor"), """ - This doesn't matter - """) + (File("Component.razor"), "") ], - uris: [new(Path.Combine(TestProjectData.SomeProjectPath, "Component.razor"))], + uris: [FileUri("Component.razor")], expected: ""); } @@ -115,7 +113,7 @@ This is a Razor document. """, additionalFiles: [ // The source generator isn't hooked up to our test project, so we have to manually "compile" the razor file - (Path.Combine(TestProjectData.SomeProjectPath, "Component.cs"), """ + (File("Component.cs"), """ namespace SomeProject; public class Component : Microsoft.AspNetCore.Components.ComponentBase @@ -123,11 +121,9 @@ public class Component : Microsoft.AspNetCore.Components.ComponentBase } """), // The above will make the component exist, but the .razor file needs to exist too for Uri presentation - (Path.Combine(TestProjectData.SomeProjectPath, "Component.razor"), """ - This doesn't matter - """) + (File("Component.razor"), "") ], - uris: [new(Path.Combine(TestProjectData.SomeProjectPath, "Component.razor"))], + uris: [FileUri("Component.razor")], expected: null); } @@ -145,21 +141,19 @@ This is a Razor document. The end. """, additionalFiles: [ - (Path.Combine(TestProjectData.SomeProjectPath, "Component.cs"), """ + (File("Component.cs"), """ namespace SomeProject; public class Component : Microsoft.AspNetCore.Components.ComponentBase { } """), - (Path.Combine(TestProjectData.SomeProjectPath, "Component.razor"), """ - This doesn't matter - """) + (File("Component.razor"), "") ], uris: [ - new(Path.Combine(TestProjectData.SomeProjectPath, "Component.razor")), - new(Path.Combine(TestProjectData.SomeProjectPath, "Component.razor.css")), - new(Path.Combine(TestProjectData.SomeProjectPath, "Component.razor.js")) + FileUri("Component.razor"), + FileUri("Component.razor.css"), + FileUri("Component.razor.js") ], expected: ""); } @@ -178,21 +172,19 @@ This is a Razor document. The end. """, additionalFiles: [ - (Path.Combine(TestProjectData.SomeProjectPath, "Component.cs"), """ + (File("Component.cs"), """ namespace SomeProject; public class Component : Microsoft.AspNetCore.Components.ComponentBase { } """), - (Path.Combine(TestProjectData.SomeProjectPath, "Component.razor"), """ - This doesn't matter - """) + (File("Component.razor"), "") ], uris: [ - new(Path.Combine(TestProjectData.SomeProjectPath, "Component.razor.css")), - new(Path.Combine(TestProjectData.SomeProjectPath, "Component.razor")), - new(Path.Combine(TestProjectData.SomeProjectPath, "Component.razor.js")) + FileUri("Component.razor.css"), + FileUri("Component.razor"), + FileUri("Component.razor.js") ], expected: ""); } @@ -211,7 +203,7 @@ This is a Razor document. The end. """, additionalFiles: [ - (Path.Combine(TestProjectData.SomeProjectPath, "Component.cs"), """ + (File("Component.cs"), """ using Microsoft.AspNetCore.Components; namespace SomeProject; @@ -226,14 +218,18 @@ public class Component : ComponentBase public string NormalParameter { get; set; } } """), - (Path.Combine(TestProjectData.SomeProjectPath, "Component.razor"), """ - This doesn't matter - """) + (File("Component.razor"), "") ], - uris: [new(Path.Combine(TestProjectData.SomeProjectPath, "Component.razor"))], + uris: [FileUri("Component.razor")], expected: """"""); } + private static Uri FileUri(string projectRelativeFileName) + => new(File(projectRelativeFileName)); + + private static string File(string projectRelativeFileName) + => Path.Combine(TestProjectData.SomeProjectPath, projectRelativeFileName); + private async Task VerifyUriPresentationAsync(string input, Uri[] uris, string? expected, WorkspaceEdit? htmlResponse = null, (string fileName, string contents)[]? additionalFiles = null) { TestFileMarkupParser.GetSpan(input, out input, out var span); @@ -256,14 +252,16 @@ private async Task VerifyUriPresentationAsync(string input, Uri[] uris, string? var result = await endpoint.GetTestAccessor().HandleRequestAsync(request, document, DisposalToken); - if (result is null) + if (expected is null) { - Assert.Null(expected); + Assert.Null(result); } else { - Assert.Equal(expected, result!.DocumentChanges!.Value.First[0].Edits[0].NewText); - Assert.Equal(document.CreateUri(), result?.DocumentChanges?.First[0].TextDocument.Uri); + Assert.NotNull(result); + Assert.NotNull(result.DocumentChanges); + Assert.Equal(expected, result.DocumentChanges.Value.First[0].Edits[0].NewText); + Assert.Equal(document.CreateUri(), result.DocumentChanges.Value.First[0].TextDocument.Uri); } } } From 2b48ebb7abf53bdb2e16f32277e10fc4a6b870d0 Mon Sep 17 00:00:00 2001 From: David Wengier Date: Fri, 19 Jul 2024 07:18:56 +1000 Subject: [PATCH 4/6] Minor cleanup --- .../Cohost/CohostUriPresentationEndpointTest.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostUriPresentationEndpointTest.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostUriPresentationEndpointTest.cs index 995dff293a2..4f28950eb6b 100644 --- a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostUriPresentationEndpointTest.cs +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostUriPresentationEndpointTest.cs @@ -38,7 +38,8 @@ The end. [Fact] public async Task HtmlResponse_TranslatesVirtualDocumentUri() { - var htmlTag = """"""; + var siteCssFileUriString = "file:///C:/path/to/site.css"; + var htmlTag = $""""""; await VerifyUriPresentationAsync( input: """ @@ -50,7 +51,7 @@ This is a Razor document. The end. """, - uris: [new("file:///C:/path/to/site.css")], + uris: [new(siteCssFileUriString)], htmlResponse: new WorkspaceEdit { DocumentChanges = new TextDocumentEdit[] @@ -112,7 +113,6 @@ This is a Razor document. } """, additionalFiles: [ - // The source generator isn't hooked up to our test project, so we have to manually "compile" the razor file (File("Component.cs"), """ namespace SomeProject; @@ -120,7 +120,6 @@ public class Component : Microsoft.AspNetCore.Components.ComponentBase { } """), - // The above will make the component exist, but the .razor file needs to exist too for Uri presentation (File("Component.razor"), "") ], uris: [FileUri("Component.razor")], From 4670d3ee1b11beecdd16b96f90c0bc6e170b8301 Mon Sep 17 00:00:00 2001 From: David Wengier Date: Fri, 19 Jul 2024 08:21:37 +1000 Subject: [PATCH 5/6] Don't call Html for Uri presentation if it's occuring in a C# region --- .../Remote/IRemoteUriPresentationService.cs | 8 ++++- .../RemoteUriPresentationService.cs | 25 +++++++------ .../Cohost/CohostUriPresentationEndpoint.cs | 28 ++++++++------- .../CohostUriPresentationEndpointTest.cs | 35 +++++++++++++++++++ 4 files changed, 73 insertions(+), 23 deletions(-) diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/IRemoteUriPresentationService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/IRemoteUriPresentationService.cs index 4c9e3707bef..8e8b0a39c98 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/IRemoteUriPresentationService.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/IRemoteUriPresentationService.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See License.txt in the project root for license information. using System; +using System.Runtime.Serialization; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.ExternalAccess.Razor; @@ -11,5 +12,10 @@ namespace Microsoft.CodeAnalysis.Razor.Remote; internal interface IRemoteUriPresentationService { - ValueTask GetPresentationAsync(RazorPinnedSolutionInfoWrapper solutionInfo, DocumentId razorDocumentId, LinePositionSpan span, Uri[]? uris, CancellationToken cancellationToken); + ValueTask GetPresentationAsync(RazorPinnedSolutionInfoWrapper solutionInfo, DocumentId razorDocumentId, LinePositionSpan span, Uri[]? uris, CancellationToken cancellationToken); + + [DataContract] + public record struct Response( + [property: DataMember(Order = 0)] bool ShouldCallHtml, + [property: DataMember(Order = 1)] TextChange? TextChange); } diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/UriPresentation/RemoteUriPresentationService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/UriPresentation/RemoteUriPresentationService.cs index a63f4c961e5..8ee5f41987b 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/UriPresentation/RemoteUriPresentationService.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/UriPresentation/RemoteUriPresentationService.cs @@ -25,7 +25,7 @@ protected override IRemoteUriPresentationService CreateService(in ServiceArgs ar private readonly IRazorDocumentMappingService _documentMappingService = args.ExportProvider.GetExportedValue(); - public ValueTask GetPresentationAsync( + public ValueTask GetPresentationAsync( RazorPinnedSolutionInfoWrapper solutionInfo, DocumentId razorDocumentId, LinePositionSpan span, @@ -37,7 +37,11 @@ protected override IRemoteUriPresentationService CreateService(in ServiceArgs ar context => GetPresentationAsync(context, span, uris, cancellationToken), cancellationToken); - private async ValueTask GetPresentationAsync( + private static IRemoteUriPresentationService.Response ShouldCallHtml => new(ShouldCallHtml: true, TextChange: null); + private static IRemoteUriPresentationService.Response DontCallHtml => new(ShouldCallHtml: false, TextChange: null); + private static IRemoteUriPresentationService.Response TextChange(TextChange textChange) => new(ShouldCallHtml: false, TextChange: textChange); + + private async ValueTask GetPresentationAsync( RemoteDocumentContext context, LinePositionSpan span, Uri[]? uris, @@ -46,7 +50,8 @@ protected override IRemoteUriPresentationService CreateService(in ServiceArgs ar var sourceText = await context.GetSourceTextAsync(cancellationToken).ConfigureAwait(false); if (!sourceText.TryGetAbsoluteIndex(span.Start.Line, span.Start.Character, out var index)) { - return null; + // If the position is invalid then we shouldn't expect to be able to handle a Html response + return DontCallHtml; } var codeDocument = await context.GetCodeDocumentAsync(cancellationToken).ConfigureAwait(false); @@ -58,13 +63,13 @@ protected override IRemoteUriPresentationService CreateService(in ServiceArgs ar // our support for Uri presentation is to insert a Html tag, so we only support Html // If Roslyn add support in future then this is where it would go. - return null; + return DontCallHtml; } var razorFileUri = UriPresentationHelper.GetComponentFileNameFromUriPresentationRequest(uris, Logger); if (razorFileUri is null) { - return null; + return ShouldCallHtml; } var solution = context.TextDocument.Project.Solution; @@ -74,14 +79,14 @@ protected override IRemoteUriPresentationService CreateService(in ServiceArgs ar var ids = solution.GetDocumentIdsWithFilePath(uriToFind); if (ids.Length == 0) { - return null; + return ShouldCallHtml; } // We assume linked documents would produce the same component tag so just take the first var otherDocument = solution.GetAdditionalDocument(ids[0]); if (otherDocument is null) { - return null; + return ShouldCallHtml; } var otherSnapshot = DocumentSnapshotFactory.GetOrCreate(otherDocument); @@ -89,15 +94,15 @@ protected override IRemoteUriPresentationService CreateService(in ServiceArgs ar if (descriptor is null) { - return null; + return ShouldCallHtml; } var tag = descriptor.TryGetComponentTag(); if (tag is null) { - return null; + return ShouldCallHtml; } - return new TextChange(span.ToTextSpan(sourceText), tag); + return TextChange(new TextChange(span.ToTextSpan(sourceText), tag)); } } diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostUriPresentationEndpoint.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostUriPresentationEndpoint.cs index 2db3fa2908d..93223c8d02b 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostUriPresentationEndpoint.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostUriPresentationEndpoint.cs @@ -9,7 +9,6 @@ using Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost; using Microsoft.CodeAnalysis.Razor.Remote; using Microsoft.CodeAnalysis.Razor.Workspaces; -using Microsoft.CodeAnalysis.Text; using Microsoft.VisualStudio.LanguageServer.ContainedLanguage; using Microsoft.VisualStudio.LanguageServer.Protocol; using Microsoft.VisualStudio.Razor.LanguageClient.Extensions; @@ -64,13 +63,13 @@ internal class CohostUriPresentationEndpoint( private async Task HandleRequestAsync(VSInternalUriPresentationParams request, TextDocument razorDocument, CancellationToken cancellationToken) { - var data = await _remoteServiceInvoker.TryInvokeAsync( + var data = await _remoteServiceInvoker.TryInvokeAsync( razorDocument.Project.Solution, (service, solutionInfo, cancellationToken) => service.GetPresentationAsync(solutionInfo, razorDocument.Id, request.Range.ToLinePositionSpan(), request.Uris, cancellationToken), cancellationToken).ConfigureAwait(false); - // If we got a response back, then either Razor or C# wants to do something with this, so we're good to go - if (data is { } textChange) + // If we got a response back, then we're good to go + if (data.TextChange is { } textChange) { var sourceText = await razorDocument.GetTextAsync(cancellationToken).ConfigureAwait(false); @@ -78,19 +77,24 @@ internal class CohostUriPresentationEndpoint( { DocumentChanges = new TextDocumentEdit[] { - new TextDocumentEdit + new TextDocumentEdit + { + TextDocument = new() { - TextDocument = new() - { - Uri = request.TextDocument.Uri - }, - Edits = [textChange.ToTextEdit(sourceText)] - } + Uri = request.TextDocument.Uri + }, + Edits = [textChange.ToTextEdit(sourceText)] + } } }; } - // If we didn't get anything from Razor or Roslyn, lets ask Html what they want to do + // If we didn't get anything from our logic, we might need to go and ask Html, but we also might have determined not to + if (!data.ShouldCallHtml) + { + return null; + } + var htmlDocument = await _htmlDocumentSynchronizer.TryGetSynchronizedHtmlDocumentAsync(razorDocument, cancellationToken).ConfigureAwait(false); if (htmlDocument is null) { diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostUriPresentationEndpointTest.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostUriPresentationEndpointTest.cs index 4f28950eb6b..9136cb1e8ea 100644 --- a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostUriPresentationEndpointTest.cs +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostUriPresentationEndpointTest.cs @@ -98,6 +98,41 @@ public class Component : Microsoft.AspNetCore.Components.ComponentBase expected: ""); } + [Fact] + public async Task Html_IntoCSharp_NoTag() + { + var siteCssFileUriString = "file:///C:/path/to/site.css"; + var htmlTag = $""""""; + + await VerifyUriPresentationAsync( + input: """ + This is a Razor document. + +
+
+ + @code { + [||] + } + """, + uris: [new(siteCssFileUriString)], + htmlResponse: new WorkspaceEdit + { + DocumentChanges = new TextDocumentEdit[] + { + new() + { + TextDocument = new() + { + Uri = FileUri("File1.razor.g.html") + }, + Edits = [new() { NewText = htmlTag}] + } + } + }, + expected: null); + } + [Fact] public async Task Component_IntoCSharp_NoTag() { From 3f524c0b6001c46a05b866856371490e9ee80596 Mon Sep 17 00:00:00 2001 From: David Wengier Date: Fri, 19 Jul 2024 10:52:05 +1000 Subject: [PATCH 6/6] Tweak names --- .../Remote/IRemoteUriPresentationService.cs | 2 +- .../RemoteUriPresentationService.cs | 20 +++++++++---------- .../Cohost/CohostUriPresentationEndpoint.cs | 2 +- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/IRemoteUriPresentationService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/IRemoteUriPresentationService.cs index 8e8b0a39c98..b5273b77ccb 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/IRemoteUriPresentationService.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/IRemoteUriPresentationService.cs @@ -16,6 +16,6 @@ internal interface IRemoteUriPresentationService [DataContract] public record struct Response( - [property: DataMember(Order = 0)] bool ShouldCallHtml, + [property: DataMember(Order = 0)] bool CallHtml, [property: DataMember(Order = 1)] TextChange? TextChange); } diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/UriPresentation/RemoteUriPresentationService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/UriPresentation/RemoteUriPresentationService.cs index 8ee5f41987b..290c61a0a5b 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/UriPresentation/RemoteUriPresentationService.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/UriPresentation/RemoteUriPresentationService.cs @@ -37,9 +37,9 @@ protected override IRemoteUriPresentationService CreateService(in ServiceArgs ar context => GetPresentationAsync(context, span, uris, cancellationToken), cancellationToken); - private static IRemoteUriPresentationService.Response ShouldCallHtml => new(ShouldCallHtml: true, TextChange: null); - private static IRemoteUriPresentationService.Response DontCallHtml => new(ShouldCallHtml: false, TextChange: null); - private static IRemoteUriPresentationService.Response TextChange(TextChange textChange) => new(ShouldCallHtml: false, TextChange: textChange); + private static IRemoteUriPresentationService.Response CallHtml => new(CallHtml: true, TextChange: null); + private static IRemoteUriPresentationService.Response NoFurtherHandling => new(CallHtml: false, TextChange: null); + private static IRemoteUriPresentationService.Response TextChange(TextChange textChange) => new(CallHtml: false, TextChange: textChange); private async ValueTask GetPresentationAsync( RemoteDocumentContext context, @@ -51,7 +51,7 @@ protected override IRemoteUriPresentationService CreateService(in ServiceArgs ar if (!sourceText.TryGetAbsoluteIndex(span.Start.Line, span.Start.Character, out var index)) { // If the position is invalid then we shouldn't expect to be able to handle a Html response - return DontCallHtml; + return NoFurtherHandling; } var codeDocument = await context.GetCodeDocumentAsync(cancellationToken).ConfigureAwait(false); @@ -63,13 +63,13 @@ protected override IRemoteUriPresentationService CreateService(in ServiceArgs ar // our support for Uri presentation is to insert a Html tag, so we only support Html // If Roslyn add support in future then this is where it would go. - return DontCallHtml; + return NoFurtherHandling; } var razorFileUri = UriPresentationHelper.GetComponentFileNameFromUriPresentationRequest(uris, Logger); if (razorFileUri is null) { - return ShouldCallHtml; + return CallHtml; } var solution = context.TextDocument.Project.Solution; @@ -79,14 +79,14 @@ protected override IRemoteUriPresentationService CreateService(in ServiceArgs ar var ids = solution.GetDocumentIdsWithFilePath(uriToFind); if (ids.Length == 0) { - return ShouldCallHtml; + return CallHtml; } // We assume linked documents would produce the same component tag so just take the first var otherDocument = solution.GetAdditionalDocument(ids[0]); if (otherDocument is null) { - return ShouldCallHtml; + return CallHtml; } var otherSnapshot = DocumentSnapshotFactory.GetOrCreate(otherDocument); @@ -94,13 +94,13 @@ protected override IRemoteUriPresentationService CreateService(in ServiceArgs ar if (descriptor is null) { - return ShouldCallHtml; + return CallHtml; } var tag = descriptor.TryGetComponentTag(); if (tag is null) { - return ShouldCallHtml; + return CallHtml; } return TextChange(new TextChange(span.ToTextSpan(sourceText), tag)); diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostUriPresentationEndpoint.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostUriPresentationEndpoint.cs index 93223c8d02b..ac01c73ecbd 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostUriPresentationEndpoint.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostUriPresentationEndpoint.cs @@ -90,7 +90,7 @@ internal class CohostUriPresentationEndpoint( } // If we didn't get anything from our logic, we might need to go and ask Html, but we also might have determined not to - if (!data.ShouldCallHtml) + if (!data.CallHtml) { return null; }