Skip to content

Commit efe5faf

Browse files
authored
Razor span mapping in LSP for cohosting (#79677)
Follow up to #79604 Part of dotnet/razor#9519 This is the follow up to the above PR to support rename in LSP, but thought I'd make the API slightly stronger while I was here since we haven't consumed it on Razor yet. I also misunderstood the span mapping API, and it needing to return an array of the same length as the input.
2 parents f9090f4 + 82e46f3 commit efe5faf

File tree

7 files changed

+76
-24
lines changed

7 files changed

+76
-24
lines changed

src/LanguageServer/Protocol/Extensions/ProtocolConversions.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1015,6 +1015,20 @@ static string GetStyledText(TaggedText taggedText, bool isInCodeBlock)
10151015
return null;
10161016
}
10171017

1018+
if (textDocument is SourceGeneratedDocument sourceGeneratedDocument &&
1019+
textDocument.Project.Solution.Services.GetService<ISourceGeneratedDocumentSpanMappingService>() is { } sourceGeneratedSpanMappingService)
1020+
{
1021+
var result = await sourceGeneratedSpanMappingService.MapSpansAsync(sourceGeneratedDocument, textSpans, cancellationToken).ConfigureAwait(false);
1022+
if (result.IsDefaultOrEmpty)
1023+
{
1024+
return null;
1025+
}
1026+
1027+
Contract.ThrowIfFalse(textSpans.Length == result.Length,
1028+
$"The number of input spans {textSpans.Length} should match the number of mapped spans returned {result.Length}");
1029+
return result;
1030+
}
1031+
10181032
var spanMappingService = document.DocumentServiceProvider.GetService<ISpanMappingService>();
10191033
if (spanMappingService == null)
10201034
{

src/LanguageServer/ProtocolUnitTests/Rename/RenameTests.cs

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,20 @@
44

55
#nullable disable
66

7+
using System;
78
using System.Collections.Immutable;
9+
using System.Composition;
810
using System.Linq;
911
using System.Threading;
1012
using System.Threading.Tasks;
11-
using Microsoft.CodeAnalysis.LanguageServer.Handler;
13+
using Microsoft.CodeAnalysis.Host;
14+
using Microsoft.CodeAnalysis.Host.Mef;
15+
using Microsoft.CodeAnalysis.Test.Utilities;
1216
using Microsoft.CodeAnalysis.Testing;
1317
using Microsoft.CodeAnalysis.Text;
18+
using Microsoft.VisualStudio.TestPlatform.ObjectModel;
1419
using Roslyn.LanguageServer.Protocol;
1520
using Roslyn.Test.Utilities;
16-
using Roslyn.Test.Utilities.TestGenerators;
1721
using Xunit;
1822
using Xunit.Abstractions;
1923
using LSP = Roslyn.LanguageServer.Protocol;
@@ -22,6 +26,8 @@ namespace Microsoft.CodeAnalysis.LanguageServer.UnitTests.Rename;
2226

2327
public sealed class RenameTests(ITestOutputHelper testOutputHelper) : AbstractLanguageServerProtocolTests(testOutputHelper)
2428
{
29+
protected override TestComposition Composition => base.Composition.AddParts(typeof(TestSourceGeneratedDocumentSpanMappingService));
30+
2531
[Theory, CombinatorialData]
2632
public async Task TestRenameAsync(bool mutatingLspWorkspace)
2733
{
@@ -269,6 +275,9 @@ void M2()
269275

270276
var results = await RunRenameAsync(testLspServer, CreateRenameParams(renameLocation, renameValue));
271277
AssertJsonEquals(expectedEdits.Concat(expectedGeneratedEdits), ((TextDocumentEdit[])results.DocumentChanges).SelectMany(e => e.Edits));
278+
279+
var service = Assert.IsType<TestSourceGeneratedDocumentSpanMappingService>(workspace.Services.GetService<ISourceGeneratedDocumentSpanMappingService>());
280+
Assert.True(service.DidMapSpans);
272281
}
273282

274283
[Theory, CombinatorialData]
@@ -316,6 +325,9 @@ void M2()
316325

317326
var results = await RunRenameAsync(testLspServer, CreateRenameParams(renameLocation, renameValue));
318327
AssertJsonEquals(expectedEdits.Concat(expectedGeneratedEdits), ((TextDocumentEdit[])results.DocumentChanges).SelectMany(e => e.Edits));
328+
329+
var service = Assert.IsType<TestSourceGeneratedDocumentSpanMappingService>(workspace.Services.GetService<ISourceGeneratedDocumentSpanMappingService>());
330+
Assert.True(service.DidMapSpans);
319331
}
320332

321333
private static LSP.RenameParams CreateRenameParams(LSP.Location location, string newName)
@@ -330,4 +342,26 @@ private static async Task<WorkspaceEdit> RunRenameAsync(TestLspServer testLspSer
330342
{
331343
return await testLspServer.ExecuteRequestAsync<LSP.RenameParams, LSP.WorkspaceEdit>(LSP.Methods.TextDocumentRenameName, renameParams, CancellationToken.None);
332344
}
345+
346+
[ExportWorkspaceService(typeof(ISourceGeneratedDocumentSpanMappingService))]
347+
[Shared]
348+
[method: ImportingConstructor]
349+
[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
350+
private class TestSourceGeneratedDocumentSpanMappingService() : ISourceGeneratedDocumentSpanMappingService
351+
{
352+
public bool DidMapSpans { get; private set; }
353+
354+
public Task<ImmutableArray<MappedTextChange>> GetMappedTextChangesAsync(SourceGeneratedDocument oldDocument, SourceGeneratedDocument newDocument, CancellationToken cancellationToken)
355+
{
356+
throw new System.NotImplementedException();
357+
}
358+
359+
public Task<ImmutableArray<MappedSpanResult>> MapSpansAsync(SourceGeneratedDocument document, ImmutableArray<TextSpan> spans, CancellationToken cancellationToken)
360+
{
361+
DidMapSpans = true;
362+
Assert.True(document.IsRazorSourceGeneratedDocument());
363+
364+
return Task.FromResult(ImmutableArray<MappedSpanResult>.Empty);
365+
}
366+
}
333367
}

src/Tools/ExternalAccess/Razor/Features/IRazorSourceGeneratedDocumentSpanMappingService.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ namespace Microsoft.CodeAnalysis.ExternalAccess.Razor
1111
{
1212
internal interface IRazorSourceGeneratedDocumentSpanMappingService
1313
{
14-
Task<ImmutableArray<RazorMappedEditResult>> GetMappedTextChangesAsync(Document oldDocument, Document newDocument, CancellationToken cancellationToken);
15-
Task<ImmutableArray<RazorMappedSpanResult>> MapSpansAsync(Document document, ImmutableArray<TextSpan> spans, CancellationToken cancellationToken);
14+
Task<ImmutableArray<RazorMappedEditResult>> GetMappedTextChangesAsync(SourceGeneratedDocument oldDocument, SourceGeneratedDocument newDocument, CancellationToken cancellationToken);
15+
Task<ImmutableArray<RazorMappedSpanResult>> MapSpansAsync(SourceGeneratedDocument document, ImmutableArray<TextSpan> spans, CancellationToken cancellationToken);
1616
}
1717
}

src/Tools/ExternalAccess/Razor/Features/RazorGeneratedDocumentIdentity.cs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,17 @@ namespace Microsoft.CodeAnalysis.ExternalAccess.Razor;
99
/// <summary>
1010
/// Wrapper for <see cref="SourceGeneratedDocumentIdentity" /> and <see cref="SourceGeneratorIdentity" />
1111
/// </summary>
12-
internal record struct RazorGeneratedDocumentIdentity(DocumentId DocumentId, string HintName, string FilePath, string GeneratorAssemblyName, string? GeneratorAssemblyPath, Version GeneratorAssemblyVersion, string GeneratorTypeName);
12+
internal record struct RazorGeneratedDocumentIdentity(DocumentId DocumentId, string HintName, string FilePath, string GeneratorAssemblyName, string? GeneratorAssemblyPath, Version GeneratorAssemblyVersion, string GeneratorTypeName)
13+
{
14+
internal static RazorGeneratedDocumentIdentity Create(SourceGeneratedDocument document)
15+
=> Create(document.Identity);
16+
17+
internal static RazorGeneratedDocumentIdentity Create(SourceGeneratedDocumentIdentity identity)
18+
=> new(identity.DocumentId,
19+
identity.HintName,
20+
identity.FilePath,
21+
identity.Generator.AssemblyName,
22+
identity.Generator.AssemblyPath,
23+
identity.Generator.AssemblyVersion,
24+
identity.Generator.TypeName);
25+
}

src/Tools/ExternalAccess/Razor/Features/RazorSourceGeneratedDocumentSpanMappingService.cs renamed to src/Tools/ExternalAccess/Razor/Features/RazorSourceGeneratedDocumentSpanMappingServiceWrapper.cs

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
// See the LICENSE file in the project root for more information.
55

66
using System;
7-
using System.Collections.Generic;
87
using System.Collections.Immutable;
98
using System.Composition;
109
using System.Threading;
@@ -19,12 +18,12 @@ namespace Microsoft.CodeAnalysis.ExternalAccess.Razor;
1918
[ExportWorkspaceService(typeof(ISourceGeneratedDocumentSpanMappingService)), Shared]
2019
[method: ImportingConstructor]
2120
[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
22-
internal sealed class RazorSourceGeneratedDocumentSpanMappingService(
21+
internal sealed class RazorSourceGeneratedDocumentSpanMappingServiceWrapper(
2322
[Import(AllowDefault = true)] IRazorSourceGeneratedDocumentSpanMappingService? implementation) : ISourceGeneratedDocumentSpanMappingService
2423
{
2524
private readonly IRazorSourceGeneratedDocumentSpanMappingService? _implementation = implementation;
2625

27-
public async Task<ImmutableArray<MappedTextChange>> GetMappedTextChangesAsync(Document oldDocument, Document newDocument, CancellationToken cancellationToken)
26+
public async Task<ImmutableArray<MappedTextChange>> GetMappedTextChangesAsync(SourceGeneratedDocument oldDocument, SourceGeneratedDocument newDocument, CancellationToken cancellationToken)
2827
{
2928
if (_implementation is null ||
3029
!oldDocument.IsRazorSourceGeneratedDocument() ||
@@ -56,7 +55,7 @@ public async Task<ImmutableArray<MappedTextChange>> GetMappedTextChangesAsync(Do
5655
return changesBuilder.ToImmutableAndClear();
5756
}
5857

59-
public async Task<ImmutableArray<MappedSpanResult>> MapSpansAsync(Document document, ImmutableArray<TextSpan> spans, CancellationToken cancellationToken)
58+
public async Task<ImmutableArray<MappedSpanResult>> MapSpansAsync(SourceGeneratedDocument document, ImmutableArray<TextSpan> spans, CancellationToken cancellationToken)
6059
{
6160
if (_implementation is null ||
6261
!document.IsRazorSourceGeneratedDocument())
@@ -65,18 +64,17 @@ public async Task<ImmutableArray<MappedSpanResult>> MapSpansAsync(Document docum
6564
}
6665

6766
var mappedSpans = await _implementation.MapSpansAsync(document, spans, cancellationToken).ConfigureAwait(false);
68-
if (mappedSpans.IsDefaultOrEmpty)
67+
if (mappedSpans.Length != spans.Length)
6968
{
7069
return [];
7170
}
7271

7372
using var _ = ArrayBuilder<MappedSpanResult>.GetInstance(out var spansBuilder);
7473
foreach (var span in mappedSpans)
7574
{
76-
if (!span.IsDefault)
77-
{
78-
spansBuilder.Add(new MappedSpanResult(span.FilePath, span.LinePositionSpan, span.Span));
79-
}
75+
spansBuilder.Add(span.IsDefault
76+
? default
77+
: new MappedSpanResult(span.FilePath, span.LinePositionSpan, span.Span));
8078
}
8179

8280
return spansBuilder.ToImmutableAndClear();

src/Tools/ExternalAccess/Razor/Features/RazorUri.cs

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,6 @@ public static RazorGeneratedDocumentIdentity GetIdentityOfGeneratedDocument(Solu
3535

3636
// Razor only cares about documents from its own generator, but it's better to just send them back the info they
3737
// need to check on their side, so we can avoid dual insertions if anything changes.
38-
return new RazorGeneratedDocumentIdentity(
39-
identity.DocumentId,
40-
identity.HintName,
41-
identity.FilePath,
42-
identity.Generator.AssemblyName,
43-
identity.Generator.AssemblyPath,
44-
identity.Generator.AssemblyVersion,
45-
identity.Generator.TypeName);
38+
return RazorGeneratedDocumentIdentity.Create(identity);
4639
}
4740
}

src/Workspaces/Core/Portable/Workspace/Host/DocumentService/ISourceGeneratedDocumentSpanMappingService.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@ namespace Microsoft.CodeAnalysis.Host;
1111

1212
internal interface ISourceGeneratedDocumentSpanMappingService : IWorkspaceService
1313
{
14-
Task<ImmutableArray<MappedTextChange>> GetMappedTextChangesAsync(Document oldDocument, Document newDocument, CancellationToken cancellationToken);
14+
Task<ImmutableArray<MappedTextChange>> GetMappedTextChangesAsync(SourceGeneratedDocument oldDocument, SourceGeneratedDocument newDocument, CancellationToken cancellationToken);
1515

16-
Task<ImmutableArray<MappedSpanResult>> MapSpansAsync(Document document, ImmutableArray<TextSpan> spans, CancellationToken cancellationToken);
16+
Task<ImmutableArray<MappedSpanResult>> MapSpansAsync(SourceGeneratedDocument document, ImmutableArray<TextSpan> spans, CancellationToken cancellationToken);
1717
}
1818

1919
internal record struct MappedTextChange(string MappedFilePath, TextChange TextChange);

0 commit comments

Comments
 (0)