diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/SourceGenerators/RazorGeneratorResult.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/SourceGenerators/RazorGeneratorResult.cs index e44cfa7684d..943913aa799 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/SourceGenerators/RazorGeneratorResult.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/SourceGenerators/RazorGeneratorResult.cs @@ -7,12 +7,14 @@ namespace Microsoft.NET.Sdk.Razor.SourceGenerators { - internal sealed class RazorGeneratorResult(IReadOnlyList tagHelpers, ImmutableDictionary documents) + internal sealed class RazorGeneratorResult(IReadOnlyList tagHelpers, ImmutableDictionary filePathToDocument, ImmutableDictionary hintNameToFilePath) { public IReadOnlyList TagHelpers => tagHelpers; - public RazorCodeDocument? GetCodeDocument(string physicalPath) => documents.TryGetValue(physicalPath, out var pair) ? pair.document : null; + public RazorCodeDocument? GetCodeDocument(string physicalPath) => filePathToDocument.TryGetValue(physicalPath, out var pair) ? pair.document : null; - public string? GetHintName(string physicalPath) => documents.TryGetValue(physicalPath, out var pair) ? pair.hintName : null; + public string? GetHintName(string physicalPath) => filePathToDocument.TryGetValue(physicalPath, out var pair) ? pair.hintName : null; + + public string? GetFilePath(string hintName) => hintNameToFilePath.TryGetValue(hintName, out var filePath) ? filePath : null; } } diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/SourceGenerators/RazorSourceGenerator.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/SourceGenerators/RazorSourceGenerator.cs index 5c1d6482e54..2fdb76a1686 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/SourceGenerators/RazorSourceGenerator.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/SourceGenerators/RazorSourceGenerator.cs @@ -3,10 +3,11 @@ using System; using System.Collections.Generic; -using System.Collections.Immutable; using System.IO; using System.Linq; +using Microsoft.AspNetCore.Razor; using Microsoft.AspNetCore.Razor.Language; +using Microsoft.AspNetCore.Razor.PooledObjects; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; @@ -348,8 +349,16 @@ public void Initialize(IncrementalGeneratorInitializationContext context) if (!isGeneratorSuppressed) { - var documentDictionary = documents.Select(p => KeyValuePair.Create(p.codeDocument.Source.FilePath!, (p.hintName, p.codeDocument))).ToImmutableDictionary(); - context.AddOutput(nameof(RazorGeneratorResult), new RazorGeneratorResult(tagHelpers, documentDictionary)); + using var filePathToDocument = new PooledDictionaryBuilder(); + using var hintNameToFilePath = new PooledDictionaryBuilder(); + + foreach (var (hintName, codeDocument, _) in documents) + { + filePathToDocument.Add(codeDocument.Source.FilePath!, (hintName, codeDocument)); + hintNameToFilePath.Add(hintName, codeDocument.Source.FilePath!); + } + + context.AddOutput(nameof(RazorGeneratorResult), new RazorGeneratorResult(tagHelpers, filePathToDocument.ToImmutable(), hintNameToFilePath.ToImmutable())); } }); } diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/ProjectExtensions.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/ProjectExtensions.cs index 879059f3d9b..e1664aad2c7 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/ProjectExtensions.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/ProjectExtensions.cs @@ -7,6 +7,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Razor; +using Microsoft.CodeAnalysis.ExternalAccess.Razor; namespace Microsoft.CodeAnalysis; @@ -40,7 +41,7 @@ public static bool TryGetCSharpDocument(this Project project, Uri csharpDocument /// /// Finds source generated documents by iterating through all of them. In OOP there are better options! /// - public static async Task TryGetSourceGeneratedDocumentFromHintNameAsync(this Project project, string? hintName, CancellationToken cancellationToken) + public static async Task TryGetSourceGeneratedDocumentFromHintNameAsync(this Project project, string? hintName, CancellationToken cancellationToken) { // TODO: use this when the location is case-insensitive on windows (https://github.com/dotnet/roslyn/issues/76869) //var generator = typeof(RazorSourceGenerator); @@ -51,4 +52,16 @@ public static bool TryGetCSharpDocument(this Project project, Uri csharpDocument var generatedDocuments = await project.GetSourceGeneratedDocumentsAsync(cancellationToken).ConfigureAwait(false); return generatedDocuments.SingleOrDefault(d => d.HintName == hintName); } + + /// + /// Finds source generated documents by iterating through all of them. In OOP there are better options! + /// + public static async Task TryGetHintNameFromGeneratedDocumentUriAsync(this Project project, Uri generatedDocumentUri, CancellationToken cancellationToken) + { + // TODO: Call SourceGeneratedDocumentUri.DeserializeIdentity: https://github.com/dotnet/razor/issues/11557 + + var generatedDocuments = await project.GetSourceGeneratedDocumentsAsync(cancellationToken).ConfigureAwait(false); + var document = generatedDocuments.SingleOrDefault(d => generatedDocumentUri.Equals(d.CreateUri())); + return document?.HintName; + } } diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Completion/RemoteCompletionService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Completion/RemoteCompletionService.cs index b5878950d4b..ef6a031d8eb 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Completion/RemoteCompletionService.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Completion/RemoteCompletionService.cs @@ -171,7 +171,7 @@ private async ValueTask GetCompletionAsync( var generatedText = await generatedDocument.GetTextAsync(cancellationToken).ConfigureAwait(false); var change = generatedText.GetTextChange(provisionalTextEdit); generatedText = generatedText.WithChanges([change]); - generatedDocument = generatedDocument.WithText(generatedText); + generatedDocument = (SourceGeneratedDocument)generatedDocument.WithText(generatedText); } // This is, to say the least, not ideal. In future we're going to normalize on to Roslyn LSP types, and this can go. diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/DocumentMapping/RemoteDocumentMappingService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/DocumentMapping/RemoteDocumentMappingService.cs index e4f6acf3615..be70330e9fc 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/DocumentMapping/RemoteDocumentMappingService.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/DocumentMapping/RemoteDocumentMappingService.cs @@ -2,11 +2,13 @@ // Licensed under the MIT license. See License.txt in the project root for license information. using System; +using System.Collections.Generic; using System.Composition; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Razor; using Microsoft.AspNetCore.Razor.Language; +using Microsoft.CodeAnalysis.ExternalAccess.Razor; using Microsoft.CodeAnalysis.Razor.DocumentMapping; using Microsoft.CodeAnalysis.Razor.Logging; using Microsoft.CodeAnalysis.Razor.Workspaces; @@ -32,11 +34,10 @@ internal sealed class RemoteDocumentMappingService( LinePositionSpan generatedDocumentRange, CancellationToken cancellationToken) { - var razorDocumentUri = _filePathService.GetRazorDocumentUri(generatedDocumentUri); - // For Html we just map the Uri, the range will be the same if (_filePathService.IsVirtualHtmlFile(generatedDocumentUri)) { + var razorDocumentUri = _filePathService.GetRazorDocumentUri(generatedDocumentUri); return (razorDocumentUri, generatedDocumentRange); } @@ -46,31 +47,20 @@ internal sealed class RemoteDocumentMappingService( return (generatedDocumentUri, generatedDocumentRange); } - var solution = originSnapshot.TextDocument.Project.Solution; - if (!solution.TryGetRazorDocument(razorDocumentUri, out var razorDocument)) - { - return (generatedDocumentUri, generatedDocumentRange); - } - - var razorDocumentSnapshot = _snapshotManager.GetSnapshot(razorDocument); - - var razorCodeDocument = await razorDocumentSnapshot - .GetGeneratedOutputAsync(cancellationToken) - .ConfigureAwait(false); - + var project = originSnapshot.TextDocument.Project; + var razorCodeDocument = await _snapshotManager.GetSnapshot(project).TryGetCodeDocumentFromGeneratedDocumentUriAsync(generatedDocumentUri, cancellationToken).ConfigureAwait(false); if (razorCodeDocument is null) { return (generatedDocumentUri, generatedDocumentRange); } - if (!razorCodeDocument.TryGetGeneratedDocument(generatedDocumentUri, _filePathService, out var generatedDocument)) - { - return Assumed.Unreachable<(Uri, LinePositionSpan)>(); - } - - if (TryMapToHostDocumentRange(generatedDocument, generatedDocumentRange, MappingBehavior.Strict, out var mappedRange)) + if (TryMapToHostDocumentRange(razorCodeDocument.GetCSharpDocument(), generatedDocumentRange, MappingBehavior.Strict, out var mappedRange)) { - return (razorDocumentUri, mappedRange); + var solution = project.Solution; + var filePath = razorCodeDocument.Source.FilePath; + var documentId = solution.GetDocumentIdsWithFilePath(filePath).First(); + var document = solution.GetAdditionalDocument(documentId).AssumeNotNull(); + return (document.CreateUri(), mappedRange); } return (generatedDocumentUri, generatedDocumentRange); diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/ProjectSystem/RemoteDocumentSnapshot.cs b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/ProjectSystem/RemoteDocumentSnapshot.cs index 7a8ea98f4e7..84009a57db4 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/ProjectSystem/RemoteDocumentSnapshot.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/ProjectSystem/RemoteDocumentSnapshot.cs @@ -22,7 +22,7 @@ internal sealed class RemoteDocumentSnapshot : IDocumentSnapshot public RemoteProjectSnapshot ProjectSnapshot { get; } private RazorCodeDocument? _codeDocument; - private Document? _generatedDocument; + private SourceGeneratedDocument? _generatedDocument; public RemoteDocumentSnapshot(TextDocument textDocument, RemoteProjectSnapshot projectSnapshot) { @@ -103,7 +103,7 @@ public IDocumentSnapshot WithText(SourceText text) return snapshotManager.GetSnapshot(newDocument); } - public async ValueTask GetGeneratedDocumentAsync(CancellationToken cancellationToken) + public async ValueTask GetGeneratedDocumentAsync(CancellationToken cancellationToken) { if (_generatedDocument is not null) { diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/ProjectSystem/RemoteProjectSnapshot.cs b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/ProjectSystem/RemoteProjectSnapshot.cs index 3bfb4d3eb25..c7f9d255ebd 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/ProjectSystem/RemoteProjectSnapshot.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/ProjectSystem/RemoteProjectSnapshot.cs @@ -169,7 +169,7 @@ public bool TryGetDocument(string filePath, [NotNullWhen(true)] out IDocumentSna return generatorResult.GetCodeDocument(documentSnapshot.FilePath); } - internal async Task GetGeneratedDocumentAsync(IDocumentSnapshot documentSnapshot, CancellationToken cancellationToken) + internal async Task GetGeneratedDocumentAsync(IDocumentSnapshot documentSnapshot, CancellationToken cancellationToken) { var generatorResult = await GetRazorGeneratorResultAsync(cancellationToken).ConfigureAwait(false); if (generatorResult is null) @@ -184,6 +184,29 @@ public bool TryGetDocument(string filePath, [NotNullWhen(true)] out IDocumentSna return generatedDocument ?? throw new InvalidOperationException("Couldn't get the source generated document for a hint name that we got from the generator?"); } + public async Task TryGetCodeDocumentFromGeneratedDocumentUriAsync(Uri generatedDocumentUri, CancellationToken cancellationToken) + { + if (await _project.TryGetHintNameFromGeneratedDocumentUriAsync(generatedDocumentUri, cancellationToken).ConfigureAwait(false) is not { } hintName) + { + return null; + } + + return await TryGetCodeDocumentFromGeneratedHintNameAsync(hintName, cancellationToken).ConfigureAwait(false); + } + + public async Task TryGetCodeDocumentFromGeneratedHintNameAsync(string generatedDocumentHintName, CancellationToken cancellationToken) + { + var runResult = await GetRazorGeneratorResultAsync(cancellationToken).ConfigureAwait(false); + if (runResult is null) + { + return null; + } + + return runResult.GetFilePath(generatedDocumentHintName) is { } razorFilePath + ? runResult.GetCodeDocument(razorFilePath) + : null; + } + private async Task GetRazorGeneratorResultAsync(CancellationToken cancellationToken) { var result = await _project.GetSourceGeneratorRunResultAsync(cancellationToken).ConfigureAwait(false); diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/RemoteFilePathService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/RemoteFilePathService.cs index d73148a321d..cf92013ab83 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/RemoteFilePathService.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/RemoteFilePathService.cs @@ -23,6 +23,6 @@ public override Uri GetRazorDocumentUri(Uri virtualDocumentUri) public override bool IsVirtualCSharpFile(Uri uri) { - return uri.Scheme == "source-generated"; + return uri.Scheme == "roslyn-source-generated"; } } diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CodeActions/ExtractToCodeBehindTests.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CodeActions/ExtractToCodeBehindTests.cs index c6e3b6323a0..b5a479aff17 100644 --- a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CodeActions/ExtractToCodeBehindTests.cs +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CodeActions/ExtractToCodeBehindTests.cs @@ -1,7 +1,6 @@ // 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.AspNetCore.Razor.Test.Common; using Xunit.Abstractions; @@ -11,7 +10,7 @@ namespace Microsoft.VisualStudio.Razor.LanguageClient.Cohost.CodeActions; public class ExtractToCodeBehindTests(FuseTestContext context, ITestOutputHelper testOutputHelper) : CohostCodeActionsEndpointTestBase(context, testOutputHelper) { - [FuseFact(Skip = "Need to map uri back to source generated document")] + [FuseFact] public async Task ExtractToCodeBehind() { await VerifyCodeActionAsync( diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostFindAllReferencesEndpointTest.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostFindAllReferencesEndpointTest.cs index 80011167cd7..e08fccbd385 100644 --- a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostFindAllReferencesEndpointTest.cs +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostFindAllReferencesEndpointTest.cs @@ -17,7 +17,7 @@ namespace Microsoft.VisualStudio.Razor.LanguageClient.Cohost; public class CohostFindAllReferencesEndpointTest(FuseTestContext context, ITestOutputHelper testOutputHelper) : CohostEndpointTestBase(testOutputHelper), IClassFixture { - [FuseTheory(Skip = "IFilePathService does not yet map generated documents")] + [FuseTheory] [CombinatorialData] public Task FindCSharpMember(bool supportsVSExtensions) => VerifyFindAllReferencesAsync(""" @@ -36,7 +36,7 @@ string M() """, supportsVSExtensions); - [FuseTheory(Skip = "IFilePathService does not yet map generated documents")] + [FuseTheory] [CombinatorialData] public async Task ComponentAttribute(bool supportsVSExtensions) { @@ -60,7 +60,7 @@ await VerifyFindAllReferencesAsync(input, supportsVSExtensions, (FilePath("SurveyPrompt.razor"), surveyPrompt)); } - [FuseTheory(Skip = "IFilePathService does not yet map generated documents")] + [FuseTheory] [CombinatorialData] public async Task OtherCSharpFile(bool supportsVSExtensions) { diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostGoToDefinitionEndpointTest.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostGoToDefinitionEndpointTest.cs index 1bfd5c1165d..9ab960df2b0 100644 --- a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostGoToDefinitionEndpointTest.cs +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostGoToDefinitionEndpointTest.cs @@ -21,7 +21,7 @@ namespace Microsoft.VisualStudio.Razor.LanguageClient.Cohost; public class CohostGoToDefinitionEndpointTest(FuseTestContext context, ITestOutputHelper testOutputHelper) : CohostEndpointTestBase(testOutputHelper), IClassFixture { - [FuseFact(Skip = "IFilePathService does not yet map generated documents")] + [FuseFact] public async Task CSharp_Method() { var input = """ @@ -40,7 +40,7 @@ public async Task CSharp_Method() await VerifyGoToDefinitionAsync(input); } - [FuseFact(Skip = "IFilePathService does not yet map generated documents")] + [FuseFact] public async Task CSharp_Local() { var input = """ @@ -87,7 +87,7 @@ public async Task CSharp_MetadataReference() Assert.Contains("public sealed class String", line); } - [FuseTheory(Skip = "IFilePathService does not yet map generated documents")] + [FuseTheory] [InlineData("$$IncrementCount")] [InlineData("In$$crementCount")] [InlineData("IncrementCount$$")] @@ -107,7 +107,7 @@ public async Task Attribute_SameFile(string method) await VerifyGoToDefinitionAsync(input, FileKinds.Component); } - [FuseFact(Skip = "IFilePathService does not yet map generated documents")] + [FuseFact] public async Task AttributeValue_BindAfter() { var input = """ @@ -157,7 +157,7 @@ public async Task Component() Assert.Equal(range, location.Range); } - [FuseTheory(Skip = "IFilePathService does not yet map generated documents")] + [FuseTheory] [InlineData("Ti$$tle")] [InlineData("$$@bind-Title")] [InlineData("@$$bind-Title")] diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostGoToImplementationEndpointTest.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostGoToImplementationEndpointTest.cs index 6db852ad742..7bc1983423e 100644 --- a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostGoToImplementationEndpointTest.cs +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostGoToImplementationEndpointTest.cs @@ -19,7 +19,7 @@ namespace Microsoft.VisualStudio.Razor.LanguageClient.Cohost; public class CohostGoToImplementationEndpointTest(FuseTestContext context, ITestOutputHelper testOutputHelper) : CohostEndpointTestBase(testOutputHelper), IClassFixture { - [FuseFact(Skip = "IFilePathService does not yet map generated documents")] + [FuseFact] public async Task CSharp_Method() { var input = """ @@ -40,7 +40,7 @@ public async Task CSharp_Method() await VerifyCSharpGoToImplementationAsync(input); } - [FuseFact(Skip = "IFilePathService does not yet map generated documents")] + [FuseFact] public async Task CSharp_Field() { var input = """ @@ -64,7 +64,7 @@ string GetX() await VerifyCSharpGoToImplementationAsync(input); } - [FuseFact(Skip = "IFilePathService does not yet map generated documents")] + [FuseFact] public async Task CSharp_Multiple() { var input = """