Skip to content

Commit ae22e33

Browse files
committed
Implement excerpt service so we can provide nice results in FAR and CodeLens etc.
1 parent 6379c6a commit ae22e33

File tree

5 files changed

+144
-4
lines changed

5 files changed

+144
-4
lines changed

src/Razor/src/Microsoft.CodeAnalysis.Razor.CohostingShared/Microsoft.CodeAnalysis.Razor.CohostingShared.projitems

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
<Compile Include="$(MSBuildThisFileDirectory)OnAutoInsert\CohostOnAutoInsertEndpoint.cs" />
3636
<Compile Include="$(MSBuildThisFileDirectory)OnAutoInsert\CohostOnAutoInsertTriggerCharacterProviders.cs" />
3737
<Compile Include="$(MSBuildThisFileDirectory)RazorAnalyzerAssemblyRedirector.cs" />
38+
<Compile Include="$(MSBuildThisFileDirectory)RazorSourceGeneratedDocumentExcerptService.cs" />
3839
<Compile Include="$(MSBuildThisFileDirectory)RazorSourceGeneratedDocumentSpanMappingService.cs" />
3940
<Compile Include="$(MSBuildThisFileDirectory)RazorSourceGeneratorLocator.cs" />
4041
<Compile Include="$(MSBuildThisFileDirectory)Rename\CohostRenameEndpoint.cs" />
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.ComponentModel.Composition;
5+
using System.Threading;
6+
using System.Threading.Tasks;
7+
using Microsoft.CodeAnalysis.ExternalAccess.Razor;
8+
using Microsoft.CodeAnalysis.Razor.DocumentExcerpt;
9+
using Microsoft.CodeAnalysis.Razor.Remote;
10+
using Microsoft.CodeAnalysis.Text;
11+
12+
namespace Microsoft.CodeAnalysis.Razor.CohostingShared;
13+
14+
[Export(typeof(IRazorSourceGeneratedDocumentExcerptService))]
15+
[method: ImportingConstructor]
16+
internal sealed class RazorSourceGeneratedDocumentExcerptService(IRemoteServiceInvoker remoteServiceInvoker) : IRazorSourceGeneratedDocumentExcerptService
17+
{
18+
private readonly IRemoteServiceInvoker _remoteServiceInvoker = remoteServiceInvoker;
19+
20+
public async Task<RazorExcerptResult?> TryExcerptAsync(SourceGeneratedDocument document, TextSpan span, RazorExcerptMode mode, RazorClassificationOptionsWrapper options, CancellationToken cancellationToken)
21+
{
22+
if (!document.IsRazorSourceGeneratedDocument())
23+
{
24+
return null;
25+
}
26+
27+
var result = await _remoteServiceInvoker.TryInvokeAsync<IRemoteSpanMappingService, RemoteExcerptResult?>(
28+
document.Project.Solution,
29+
(service, solutionInfo, cancellationToken) => service.TryExcerptAsync(solutionInfo, document.Id, span, mode, options, cancellationToken),
30+
cancellationToken).ConfigureAwait(false);
31+
32+
if (result is null)
33+
{
34+
return null;
35+
}
36+
37+
// Source text can't be sent back from OOP, so we have to do the translation here. Fortunately this doesn't need
38+
// anything we can't access
39+
var razorDocument = document.Project.GetAdditionalDocument(result.RazorDocumentId);
40+
if (razorDocument is null)
41+
{
42+
return null;
43+
}
44+
45+
var razorSourceText = await razorDocument.GetTextAsync(cancellationToken).ConfigureAwait(false);
46+
var builder = result.ClassifiedSpans.ToBuilder();
47+
48+
var razorDocumentSpan = result.RazorDocumentSpan;
49+
var excerptSpan = result.ExcerptSpan;
50+
var excerptText = DocumentExcerptHelper.GetTranslatedExcerptText(razorSourceText, ref razorDocumentSpan, ref excerptSpan, builder);
51+
52+
return new RazorExcerptResult(excerptText, razorDocumentSpan, builder.ToImmutable(), document, span);
53+
}
54+
}

src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/IRemoteSpanMappingService.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,12 @@ ValueTask<ImmutableArray<RazorMappedSpanResult>> MapSpansAsync(
2222
DocumentId generatedDocumentId,
2323
ImmutableArray<TextSpan> spans,
2424
CancellationToken cancellationToken);
25+
26+
ValueTask<RemoteExcerptResult?> TryExcerptAsync(
27+
RazorPinnedSolutionInfoWrapper solutionInfo,
28+
DocumentId id,
29+
TextSpan span,
30+
RazorExcerptMode mode,
31+
RazorClassificationOptionsWrapper options,
32+
CancellationToken cancellationToken);
2533
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Collections.Immutable;
5+
using System.Runtime.Serialization;
6+
using Microsoft.CodeAnalysis.Classification;
7+
using Microsoft.CodeAnalysis.Text;
8+
9+
namespace Microsoft.CodeAnalysis.Razor.Remote;
10+
11+
[DataContract]
12+
internal sealed record RemoteExcerptResult(
13+
[property: DataMember(Order = 0)] DocumentId RazorDocumentId,
14+
[property: DataMember(Order = 1)] TextSpan RazorDocumentSpan,
15+
[property: DataMember(Order = 2)] TextSpan ExcerptSpan,
16+
[property: DataMember(Order = 3)] ImmutableArray<ClassifiedSpan> ClassifiedSpans,
17+
[property: DataMember(Order = 4)] TextSpan Span);

src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/WrapWithTag/RemoteSpanMappingService.cs renamed to src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/DocumentMapping/RemoteSpanMappingService.cs

Lines changed: 64 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,10 @@
77
using System.Threading;
88
using System.Threading.Tasks;
99
using Microsoft.AspNetCore.Razor;
10+
using Microsoft.AspNetCore.Razor.Language;
1011
using Microsoft.AspNetCore.Razor.PooledObjects;
1112
using Microsoft.CodeAnalysis.ExternalAccess.Razor;
13+
using Microsoft.CodeAnalysis.Razor.DocumentExcerpt;
1214
using Microsoft.CodeAnalysis.Razor.DocumentMapping;
1315
using Microsoft.CodeAnalysis.Razor.Protocol;
1416
using Microsoft.CodeAnalysis.Razor.Remote;
@@ -30,6 +32,54 @@ protected override IRemoteSpanMappingService CreateService(in ServiceArgs args)
3032
private readonly IDocumentMappingService _documentMappingService = args.ExportProvider.GetExportedValue<IDocumentMappingService>();
3133
private readonly ITelemetryReporter _telemetryReporter = args.ExportProvider.GetExportedValue<ITelemetryReporter>();
3234

35+
public ValueTask<RemoteExcerptResult?> TryExcerptAsync(RazorPinnedSolutionInfoWrapper solutionInfo, DocumentId generatedDocumentId, TextSpan span, RazorExcerptMode mode, RazorClassificationOptionsWrapper options, CancellationToken cancellationToken)
36+
=> RunServiceAsync(
37+
solutionInfo,
38+
solution => TryExcerptAsync(solution, generatedDocumentId, span, mode, options, cancellationToken),
39+
cancellationToken);
40+
41+
private async ValueTask<RemoteExcerptResult?> TryExcerptAsync(Solution solution, DocumentId generatedDocumentId, TextSpan span, RazorExcerptMode mode, RazorClassificationOptionsWrapper options, CancellationToken cancellationToken)
42+
{
43+
var generatedDocument = await solution.GetSourceGeneratedDocumentAsync(generatedDocumentId, cancellationToken).ConfigureAwait(false);
44+
if (generatedDocument is null)
45+
{
46+
return null;
47+
}
48+
49+
var razorDocument = await TryGetRazorDocumentForGeneratedDocumentAsync(generatedDocument, cancellationToken).ConfigureAwait(false);
50+
if (razorDocument is null)
51+
{
52+
return null;
53+
}
54+
55+
var documentSnapshot = _snapshotManager.GetSnapshot(razorDocument);
56+
var codeDocument = await documentSnapshot.GetGeneratedOutputAsync(cancellationToken).ConfigureAwait(false);
57+
58+
var mappedSpans = MapSpans(codeDocument, [span]);
59+
if (mappedSpans is not [{ IsDefault: false } mappedSpan])
60+
{
61+
return null;
62+
}
63+
64+
var razorDocumentText = await razorDocument.GetTextAsync(cancellationToken).ConfigureAwait(false);
65+
var razorDocumentSpan = razorDocumentText.GetTextSpan(mappedSpan.LinePositionSpan);
66+
67+
// First compute the range of text we want to we to display relative to the primary document.
68+
var excerptSpan = DocumentExcerptHelper.ChooseExcerptSpan(razorDocumentText, razorDocumentSpan, (ExcerptModeInternal)mode);
69+
70+
// Then we'll classify the spans based on the primary document, since that's the coordinate
71+
// space that our output mappings use.
72+
var mappings = codeDocument.GetRequiredCSharpDocument().SourceMappings;
73+
var classifiedSpans = await DocumentExcerptHelper.ClassifyPreviewAsync(
74+
excerptSpan,
75+
generatedDocument,
76+
mappings,
77+
options,
78+
cancellationToken).ConfigureAwait(false);
79+
80+
return new RemoteExcerptResult(razorDocument.Id, razorDocumentSpan, excerptSpan, classifiedSpans.ToImmutable(), span);
81+
}
82+
3383
public ValueTask<ImmutableArray<RazorMappedSpanResult>> MapSpansAsync(RazorPinnedSolutionInfoWrapper solutionInfo, DocumentId generatedDocumentId, ImmutableArray<TextSpan> spans, CancellationToken cancellationToken)
3484
=> RunServiceAsync(
3585
solutionInfo,
@@ -45,12 +95,17 @@ private async ValueTask<ImmutableArray<RazorMappedSpanResult>> MapSpansAsync(Sol
4595
}
4696

4797
var documentSnapshot = _snapshotManager.GetSnapshot(razorDocument);
48-
var output = await documentSnapshot.GetGeneratedOutputAsync(cancellationToken).ConfigureAwait(false);
98+
var codeDocument = await documentSnapshot.GetGeneratedOutputAsync(cancellationToken).ConfigureAwait(false);
99+
100+
return MapSpans(codeDocument, spans);
101+
}
49102

50-
var source = output.Source.Text;
103+
private static ImmutableArray<RazorMappedSpanResult> MapSpans(RazorCodeDocument codeDocument, ImmutableArray<TextSpan> spans)
104+
{
105+
var source = codeDocument.Source.Text;
51106

52-
var csharpDocument = output.GetRequiredCSharpDocument();
53-
var filePath = output.Source.FilePath.AssumeNotNull();
107+
var csharpDocument = codeDocument.GetRequiredCSharpDocument();
108+
var filePath = codeDocument.Source.FilePath.AssumeNotNull();
54109

55110
using var results = new PooledArrayBuilder<RazorMappedSpanResult>();
56111

@@ -118,6 +173,11 @@ private async ValueTask<ImmutableArray<RazorMappedEditResult>> MapTextChangesAsy
118173
return null;
119174
}
120175

176+
return await TryGetRazorDocumentForGeneratedDocumentAsync(generatedDocument, cancellationToken).ConfigureAwait(false);
177+
}
178+
179+
private async Task<TextDocument?> TryGetRazorDocumentForGeneratedDocumentAsync(SourceGeneratedDocument generatedDocument, CancellationToken cancellationToken)
180+
{
121181
var identity = RazorGeneratedDocumentIdentity.Create(generatedDocument);
122182
if (!identity.IsRazorSourceGeneratedDocument())
123183
{

0 commit comments

Comments
 (0)