diff --git a/src/LanguageServer/Protocol/Extensions/ProtocolConversions.cs b/src/LanguageServer/Protocol/Extensions/ProtocolConversions.cs index 9c4c254d8ee7a..41b553370f397 100644 --- a/src/LanguageServer/Protocol/Extensions/ProtocolConversions.cs +++ b/src/LanguageServer/Protocol/Extensions/ProtocolConversions.cs @@ -1015,6 +1015,20 @@ static string GetStyledText(TaggedText taggedText, bool isInCodeBlock) return null; } + if (textDocument is SourceGeneratedDocument sourceGeneratedDocument && + textDocument.Project.Solution.Services.GetService() is { } sourceGeneratedSpanMappingService) + { + var result = await sourceGeneratedSpanMappingService.MapSpansAsync(sourceGeneratedDocument, textSpans, cancellationToken).ConfigureAwait(false); + if (result.IsDefaultOrEmpty) + { + return null; + } + + Contract.ThrowIfFalse(textSpans.Length == result.Length, + $"The number of input spans {textSpans.Length} should match the number of mapped spans returned {result.Length}"); + return result; + } + var spanMappingService = document.DocumentServiceProvider.GetService(); if (spanMappingService == null) { diff --git a/src/LanguageServer/ProtocolUnitTests/Rename/RenameTests.cs b/src/LanguageServer/ProtocolUnitTests/Rename/RenameTests.cs index 8c17d2331e89e..8a3a41f0f917d 100644 --- a/src/LanguageServer/ProtocolUnitTests/Rename/RenameTests.cs +++ b/src/LanguageServer/ProtocolUnitTests/Rename/RenameTests.cs @@ -4,16 +4,20 @@ #nullable disable +using System; using System.Collections.Immutable; +using System.Composition; using System.Linq; using System.Threading; using System.Threading.Tasks; -using Microsoft.CodeAnalysis.LanguageServer.Handler; +using Microsoft.CodeAnalysis.Host; +using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.CodeAnalysis.Test.Utilities; using Microsoft.CodeAnalysis.Testing; using Microsoft.CodeAnalysis.Text; +using Microsoft.VisualStudio.TestPlatform.ObjectModel; using Roslyn.LanguageServer.Protocol; using Roslyn.Test.Utilities; -using Roslyn.Test.Utilities.TestGenerators; using Xunit; using Xunit.Abstractions; using LSP = Roslyn.LanguageServer.Protocol; @@ -22,6 +26,8 @@ namespace Microsoft.CodeAnalysis.LanguageServer.UnitTests.Rename; public sealed class RenameTests(ITestOutputHelper testOutputHelper) : AbstractLanguageServerProtocolTests(testOutputHelper) { + protected override TestComposition Composition => base.Composition.AddParts(typeof(TestSourceGeneratedDocumentSpanMappingService)); + [Theory, CombinatorialData] public async Task TestRenameAsync(bool mutatingLspWorkspace) { @@ -269,6 +275,9 @@ void M2() var results = await RunRenameAsync(testLspServer, CreateRenameParams(renameLocation, renameValue)); AssertJsonEquals(expectedEdits.Concat(expectedGeneratedEdits), ((TextDocumentEdit[])results.DocumentChanges).SelectMany(e => e.Edits)); + + var service = Assert.IsType(workspace.Services.GetService()); + Assert.True(service.DidMapSpans); } [Theory, CombinatorialData] @@ -316,6 +325,9 @@ void M2() var results = await RunRenameAsync(testLspServer, CreateRenameParams(renameLocation, renameValue)); AssertJsonEquals(expectedEdits.Concat(expectedGeneratedEdits), ((TextDocumentEdit[])results.DocumentChanges).SelectMany(e => e.Edits)); + + var service = Assert.IsType(workspace.Services.GetService()); + Assert.True(service.DidMapSpans); } private static LSP.RenameParams CreateRenameParams(LSP.Location location, string newName) @@ -330,4 +342,26 @@ private static async Task RunRenameAsync(TestLspServer testLspSer { return await testLspServer.ExecuteRequestAsync(LSP.Methods.TextDocumentRenameName, renameParams, CancellationToken.None); } + + [ExportWorkspaceService(typeof(ISourceGeneratedDocumentSpanMappingService))] + [Shared] + [method: ImportingConstructor] + [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + private class TestSourceGeneratedDocumentSpanMappingService() : ISourceGeneratedDocumentSpanMappingService + { + public bool DidMapSpans { get; private set; } + + public Task> GetMappedTextChangesAsync(SourceGeneratedDocument oldDocument, SourceGeneratedDocument newDocument, CancellationToken cancellationToken) + { + throw new System.NotImplementedException(); + } + + public Task> MapSpansAsync(SourceGeneratedDocument document, ImmutableArray spans, CancellationToken cancellationToken) + { + DidMapSpans = true; + Assert.True(document.IsRazorSourceGeneratedDocument()); + + return Task.FromResult(ImmutableArray.Empty); + } + } } diff --git a/src/Tools/ExternalAccess/Razor/Features/IRazorSourceGeneratedDocumentSpanMappingService.cs b/src/Tools/ExternalAccess/Razor/Features/IRazorSourceGeneratedDocumentSpanMappingService.cs index b4a4414b1e0b5..51b91a0ad1694 100644 --- a/src/Tools/ExternalAccess/Razor/Features/IRazorSourceGeneratedDocumentSpanMappingService.cs +++ b/src/Tools/ExternalAccess/Razor/Features/IRazorSourceGeneratedDocumentSpanMappingService.cs @@ -11,7 +11,7 @@ namespace Microsoft.CodeAnalysis.ExternalAccess.Razor { internal interface IRazorSourceGeneratedDocumentSpanMappingService { - Task> GetMappedTextChangesAsync(Document oldDocument, Document newDocument, CancellationToken cancellationToken); - Task> MapSpansAsync(Document document, ImmutableArray spans, CancellationToken cancellationToken); + Task> GetMappedTextChangesAsync(SourceGeneratedDocument oldDocument, SourceGeneratedDocument newDocument, CancellationToken cancellationToken); + Task> MapSpansAsync(SourceGeneratedDocument document, ImmutableArray spans, CancellationToken cancellationToken); } } diff --git a/src/Tools/ExternalAccess/Razor/Features/RazorGeneratedDocumentIdentity.cs b/src/Tools/ExternalAccess/Razor/Features/RazorGeneratedDocumentIdentity.cs index 7c2290c9154ea..4e50c9137a69e 100644 --- a/src/Tools/ExternalAccess/Razor/Features/RazorGeneratedDocumentIdentity.cs +++ b/src/Tools/ExternalAccess/Razor/Features/RazorGeneratedDocumentIdentity.cs @@ -9,4 +9,17 @@ namespace Microsoft.CodeAnalysis.ExternalAccess.Razor; /// /// Wrapper for and /// -internal record struct RazorGeneratedDocumentIdentity(DocumentId DocumentId, string HintName, string FilePath, string GeneratorAssemblyName, string? GeneratorAssemblyPath, Version GeneratorAssemblyVersion, string GeneratorTypeName); +internal record struct RazorGeneratedDocumentIdentity(DocumentId DocumentId, string HintName, string FilePath, string GeneratorAssemblyName, string? GeneratorAssemblyPath, Version GeneratorAssemblyVersion, string GeneratorTypeName) +{ + internal static RazorGeneratedDocumentIdentity Create(SourceGeneratedDocument document) + => Create(document.Identity); + + internal static RazorGeneratedDocumentIdentity Create(SourceGeneratedDocumentIdentity identity) + => new(identity.DocumentId, + identity.HintName, + identity.FilePath, + identity.Generator.AssemblyName, + identity.Generator.AssemblyPath, + identity.Generator.AssemblyVersion, + identity.Generator.TypeName); +} diff --git a/src/Tools/ExternalAccess/Razor/Features/RazorSourceGeneratedDocumentSpanMappingService.cs b/src/Tools/ExternalAccess/Razor/Features/RazorSourceGeneratedDocumentSpanMappingServiceWrapper.cs similarity index 85% rename from src/Tools/ExternalAccess/Razor/Features/RazorSourceGeneratedDocumentSpanMappingService.cs rename to src/Tools/ExternalAccess/Razor/Features/RazorSourceGeneratedDocumentSpanMappingServiceWrapper.cs index 5263e24f79250..5a9a8e6cc2001 100644 --- a/src/Tools/ExternalAccess/Razor/Features/RazorSourceGeneratedDocumentSpanMappingService.cs +++ b/src/Tools/ExternalAccess/Razor/Features/RazorSourceGeneratedDocumentSpanMappingServiceWrapper.cs @@ -4,7 +4,6 @@ // See the LICENSE file in the project root for more information. using System; -using System.Collections.Generic; using System.Collections.Immutable; using System.Composition; using System.Threading; @@ -19,12 +18,12 @@ namespace Microsoft.CodeAnalysis.ExternalAccess.Razor; [ExportWorkspaceService(typeof(ISourceGeneratedDocumentSpanMappingService)), Shared] [method: ImportingConstructor] [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] -internal sealed class RazorSourceGeneratedDocumentSpanMappingService( +internal sealed class RazorSourceGeneratedDocumentSpanMappingServiceWrapper( [Import(AllowDefault = true)] IRazorSourceGeneratedDocumentSpanMappingService? implementation) : ISourceGeneratedDocumentSpanMappingService { private readonly IRazorSourceGeneratedDocumentSpanMappingService? _implementation = implementation; - public async Task> GetMappedTextChangesAsync(Document oldDocument, Document newDocument, CancellationToken cancellationToken) + public async Task> GetMappedTextChangesAsync(SourceGeneratedDocument oldDocument, SourceGeneratedDocument newDocument, CancellationToken cancellationToken) { if (_implementation is null || !oldDocument.IsRazorSourceGeneratedDocument() || @@ -56,7 +55,7 @@ public async Task> GetMappedTextChangesAsync(Do return changesBuilder.ToImmutableAndClear(); } - public async Task> MapSpansAsync(Document document, ImmutableArray spans, CancellationToken cancellationToken) + public async Task> MapSpansAsync(SourceGeneratedDocument document, ImmutableArray spans, CancellationToken cancellationToken) { if (_implementation is null || !document.IsRazorSourceGeneratedDocument()) @@ -65,7 +64,7 @@ public async Task> MapSpansAsync(Document docum } var mappedSpans = await _implementation.MapSpansAsync(document, spans, cancellationToken).ConfigureAwait(false); - if (mappedSpans.IsDefaultOrEmpty) + if (mappedSpans.Length != spans.Length) { return []; } @@ -73,10 +72,9 @@ public async Task> MapSpansAsync(Document docum using var _ = ArrayBuilder.GetInstance(out var spansBuilder); foreach (var span in mappedSpans) { - if (!span.IsDefault) - { - spansBuilder.Add(new MappedSpanResult(span.FilePath, span.LinePositionSpan, span.Span)); - } + spansBuilder.Add(span.IsDefault + ? default + : new MappedSpanResult(span.FilePath, span.LinePositionSpan, span.Span)); } return spansBuilder.ToImmutableAndClear(); diff --git a/src/Tools/ExternalAccess/Razor/Features/RazorUri.cs b/src/Tools/ExternalAccess/Razor/Features/RazorUri.cs index 8eb2e6972e94c..6841ab7a9a4bc 100644 --- a/src/Tools/ExternalAccess/Razor/Features/RazorUri.cs +++ b/src/Tools/ExternalAccess/Razor/Features/RazorUri.cs @@ -35,13 +35,6 @@ public static RazorGeneratedDocumentIdentity GetIdentityOfGeneratedDocument(Solu // Razor only cares about documents from its own generator, but it's better to just send them back the info they // need to check on their side, so we can avoid dual insertions if anything changes. - return new RazorGeneratedDocumentIdentity( - identity.DocumentId, - identity.HintName, - identity.FilePath, - identity.Generator.AssemblyName, - identity.Generator.AssemblyPath, - identity.Generator.AssemblyVersion, - identity.Generator.TypeName); + return RazorGeneratedDocumentIdentity.Create(identity); } } diff --git a/src/Workspaces/Core/Portable/Workspace/Host/DocumentService/ISourceGeneratedDocumentSpanMappingService.cs b/src/Workspaces/Core/Portable/Workspace/Host/DocumentService/ISourceGeneratedDocumentSpanMappingService.cs index 056dcf9cc3efb..a08e0bb58ac99 100644 --- a/src/Workspaces/Core/Portable/Workspace/Host/DocumentService/ISourceGeneratedDocumentSpanMappingService.cs +++ b/src/Workspaces/Core/Portable/Workspace/Host/DocumentService/ISourceGeneratedDocumentSpanMappingService.cs @@ -11,9 +11,9 @@ namespace Microsoft.CodeAnalysis.Host; internal interface ISourceGeneratedDocumentSpanMappingService : IWorkspaceService { - Task> GetMappedTextChangesAsync(Document oldDocument, Document newDocument, CancellationToken cancellationToken); + Task> GetMappedTextChangesAsync(SourceGeneratedDocument oldDocument, SourceGeneratedDocument newDocument, CancellationToken cancellationToken); - Task> MapSpansAsync(Document document, ImmutableArray spans, CancellationToken cancellationToken); + Task> MapSpansAsync(SourceGeneratedDocument document, ImmutableArray spans, CancellationToken cancellationToken); } internal record struct MappedTextChange(string MappedFilePath, TextChange TextChange);