Skip to content

Commit 8bac290

Browse files
committed
Allow source generated documents to be mapped by Razor
1 parent 6903eab commit 8bac290

File tree

6 files changed

+159
-4
lines changed

6 files changed

+159
-4
lines changed
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+
// See the LICENSE file in the project root for more information.
4+
5+
using System.Collections.Immutable;
6+
using System.Threading;
7+
using System.Threading.Tasks;
8+
using Microsoft.CodeAnalysis.Text;
9+
10+
namespace Microsoft.CodeAnalysis.ExternalAccess.Razor
11+
{
12+
internal interface IRazorSourceGeneratedDocumentSpanMappingService
13+
{
14+
Task<ImmutableArray<RazorMappedEditResult>> GetMappedTextChangesAsync(Document oldDocument, Document newDocument, CancellationToken cancellationToken);
15+
Task<ImmutableArray<RazorMappedSpanResult>> MapSpansAsync(Document document, ImmutableArray<TextSpan> spans, CancellationToken cancellationToken);
16+
}
17+
}

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

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,21 @@
33
// See the LICENSE file in the project root for more information.
44

55
using System;
6+
using System.Runtime.Serialization;
67
using Microsoft.CodeAnalysis.Text;
78

89
namespace Microsoft.CodeAnalysis.ExternalAccess.Razor;
910

11+
[DataContract]
1012
internal readonly struct RazorMappedSpanResult
1113
{
14+
[DataMember(Order = 0)]
1215
public readonly string FilePath;
1316

17+
[DataMember(Order = 1)]
1418
public readonly LinePositionSpan LinePositionSpan;
1519

20+
[DataMember(Order = 2)]
1621
public readonly TextSpan Span;
1722

1823
public RazorMappedSpanResult(string filePath, LinePositionSpan linePositionSpan, TextSpan span)
@@ -30,7 +35,10 @@ public RazorMappedSpanResult(string filePath, LinePositionSpan linePositionSpan,
3035
public bool IsDefault => FilePath == null;
3136
}
3237

33-
internal readonly record struct RazorMappedEditResult(string FilePath, TextChange[] TextChanges)
38+
[DataContract]
39+
internal readonly record struct RazorMappedEditResult(
40+
[property: DataMember(Order = 0)] string FilePath,
41+
[property: DataMember(Order = 1)] TextChange[] TextChanges)
3442
{
3543
public bool IsDefault => FilePath == null || TextChanges == null;
3644
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+

2+
// Licensed to the .NET Foundation under one or more agreements.
3+
// The .NET Foundation licenses this file to you under the MIT license.
4+
// See the LICENSE file in the project root for more information.
5+
6+
using System;
7+
using System.Collections.Generic;
8+
using System.Collections.Immutable;
9+
using System.Composition;
10+
using System.Threading;
11+
using System.Threading.Tasks;
12+
using Microsoft.CodeAnalysis.Host;
13+
using Microsoft.CodeAnalysis.Host.Mef;
14+
using Microsoft.CodeAnalysis.PooledObjects;
15+
using Microsoft.CodeAnalysis.Text;
16+
17+
namespace Microsoft.CodeAnalysis.ExternalAccess.Razor;
18+
19+
[ExportWorkspaceService(typeof(ISourceGeneratedDocumentSpanMappingService)), Shared]
20+
[method: ImportingConstructor]
21+
[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
22+
internal sealed class RazorSourceGeneratedDocumentSpanMappingService(
23+
[Import(AllowDefault = true)] IRazorSourceGeneratedDocumentSpanMappingService? implementation) : ISourceGeneratedDocumentSpanMappingService
24+
{
25+
private readonly IRazorSourceGeneratedDocumentSpanMappingService? _implementation = implementation;
26+
27+
public async Task<ImmutableArray<MappedTextChange>> GetMappedTextChangesAsync(Document oldDocument, Document newDocument, CancellationToken cancellationToken)
28+
{
29+
if (_implementation is null ||
30+
!oldDocument.IsRazorSourceGeneratedDocument() ||
31+
!newDocument.IsRazorSourceGeneratedDocument())
32+
{
33+
return [];
34+
}
35+
36+
var mappedChanges = await _implementation.GetMappedTextChangesAsync(oldDocument, newDocument, cancellationToken).ConfigureAwait(false);
37+
if (mappedChanges.IsDefaultOrEmpty)
38+
{
39+
return [];
40+
}
41+
42+
using var _ = ArrayBuilder<MappedTextChange>.GetInstance(out var changesBuilder);
43+
foreach (var change in mappedChanges)
44+
{
45+
if (change.IsDefault)
46+
{
47+
continue;
48+
}
49+
50+
foreach (var textChange in change.TextChanges)
51+
{
52+
changesBuilder.Add(new MappedTextChange(change.FilePath, textChange));
53+
}
54+
}
55+
56+
return changesBuilder.ToImmutableAndClear();
57+
58+
}
59+
60+
public async Task<ImmutableArray<MappedSpanResult>> MapSpansAsync(Document document, ImmutableArray<TextSpan> spans, CancellationToken cancellationToken)
61+
{
62+
if (_implementation is null ||
63+
!document.IsRazorSourceGeneratedDocument())
64+
{
65+
return [];
66+
}
67+
68+
var mappedSpans = await _implementation.MapSpansAsync(document, spans, cancellationToken).ConfigureAwait(false);
69+
if (mappedSpans.IsDefaultOrEmpty)
70+
{
71+
return [];
72+
}
73+
74+
using var _ = ArrayBuilder<MappedSpanResult>.GetInstance(out var spansBuilder);
75+
foreach (var span in mappedSpans)
76+
{
77+
if (!span.IsDefault)
78+
{
79+
spansBuilder.Add(new MappedSpanResult(span.FilePath, span.LinePositionSpan, span.Span));
80+
}
81+
}
82+
83+
return spansBuilder.ToImmutableAndClear();
84+
}
85+
}

src/VisualStudio/Core/Def/ProjectSystem/VisualStudioWorkspaceImpl.cs

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
using EnvDTE;
1616
using Microsoft.CodeAnalysis;
1717
using Microsoft.CodeAnalysis.Diagnostics;
18+
using Microsoft.CodeAnalysis.EditAndContinue;
1819
using Microsoft.CodeAnalysis.Editor;
1920
using Microsoft.CodeAnalysis.Editor.Shared.Extensions;
2021
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
@@ -626,7 +627,7 @@ internal override void ApplyMappedFileChanges(SolutionChanges solutionChanges)
626627
// Instead, we invoke this in JTF run which will mitigate deadlocks when the ConfigureAwait(true)
627628
// tries to switch back to the main thread in the LSP client.
628629
// Link to LSP client bug for ConfigureAwait(true) - https://devdiv.visualstudio.com/DevDiv/_workitems/edit/1216657
629-
var mappedChanges = _threadingContext.JoinableTaskFactory.Run(() => GetMappedTextChangesAsync(solutionChanges));
630+
var mappedChanges = _threadingContext.JoinableTaskFactory.Run(() => GetMappedTextChangesAsync(solutionChanges, CancellationToken.None));
630631

631632
// Group the mapped text changes by file, then apply all mapped text changes for the file.
632633
foreach (var changesForFile in mappedChanges)
@@ -641,7 +642,7 @@ internal override void ApplyMappedFileChanges(SolutionChanges solutionChanges)
641642

642643
return;
643644

644-
async Task<MultiDictionary<string, (TextChange TextChange, ProjectId ProjectId)>> GetMappedTextChangesAsync(SolutionChanges solutionChanges)
645+
async Task<MultiDictionary<string, (TextChange TextChange, ProjectId ProjectId)>> GetMappedTextChangesAsync(SolutionChanges solutionChanges, CancellationToken cancellationToken)
645646
{
646647
var filePathToMappedTextChanges = new MultiDictionary<string, (TextChange TextChange, ProjectId ProjectId)>();
647648
foreach (var projectChanges in solutionChanges.GetProjectChanges())
@@ -656,14 +657,36 @@ internal override void ApplyMappedFileChanges(SolutionChanges solutionChanges)
656657

657658
var newDocument = projectChanges.NewProject.GetRequiredDocument(changedDocumentId);
658659
var mappedTextChanges = await mappingService.GetMappedTextChangesAsync(
659-
oldDocument, newDocument, CancellationToken.None).ConfigureAwait(false);
660+
oldDocument, newDocument, cancellationToken).ConfigureAwait(false);
660661
foreach (var (filePath, textChange) in mappedTextChanges)
661662
{
662663
filePathToMappedTextChanges.Add(filePath, (textChange, projectChanges.ProjectId));
663664
}
664665
}
665666
}
666667

668+
var sourceGeneratedDocumentMappingService = Services.GetService<ISourceGeneratedDocumentSpanMappingService>();
669+
if (sourceGeneratedDocumentMappingService is not null)
670+
{
671+
// Since we're mapping changes to source generated documents, we have to ensure the old solution has run the generators
672+
// so the mapper has something to compare to.
673+
foreach (var (docId, state) in solutionChanges.NewSolution.CompilationState.FrozenSourceGeneratedDocumentStates.States)
674+
{
675+
_ = await solutionChanges.OldSolution.GetRequiredDocumentAsync(docId, includeSourceGenerated: true, cancellationToken).ConfigureAwait(false);
676+
}
677+
678+
foreach (var docId in solutionChanges.GetExplicitlyChangedSourceGeneratedDocuments())
679+
{
680+
var oldDocument = solutionChanges.OldSolution.GetRequiredSourceGeneratedDocumentForAlreadyGeneratedId(docId);
681+
var newDocument = solutionChanges.NewSolution.GetRequiredSourceGeneratedDocumentForAlreadyGeneratedId(docId);
682+
var mappedTextChanges = await sourceGeneratedDocumentMappingService.GetMappedTextChangesAsync(oldDocument, newDocument, cancellationToken).ConfigureAwait(false);
683+
foreach (var (filePath, textChange) in mappedTextChanges)
684+
{
685+
filePathToMappedTextChanges.Add(filePath, (textChange, docId.ProjectId));
686+
}
687+
}
688+
}
689+
667690
return filePathToMappedTextChanges;
668691
}
669692

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
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+
// See the LICENSE file in the project root for more information.
4+
5+
using System.Collections.Immutable;
6+
using System.Threading;
7+
using System.Threading.Tasks;
8+
using Microsoft.CodeAnalysis.Text;
9+
10+
namespace Microsoft.CodeAnalysis.Host;
11+
12+
internal interface ISourceGeneratedDocumentSpanMappingService : IWorkspaceService
13+
{
14+
Task<ImmutableArray<MappedTextChange>> GetMappedTextChangesAsync(Document oldDocument, Document newDocument, CancellationToken cancellationToken);
15+
16+
Task<ImmutableArray<MappedSpanResult>> MapSpansAsync(Document document, ImmutableArray<TextSpan> spans, CancellationToken cancellationToken);
17+
}
18+
19+
internal record struct MappedTextChange(string MappedFilePath, TextChange TextChange);

src/Workspaces/Core/Portable/Workspace/Solution/SolutionChanges.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ public readonly struct SolutionChanges
1616
private readonly Solution _newSolution;
1717
private readonly Solution _oldSolution;
1818

19+
internal Solution OldSolution => _oldSolution;
20+
internal Solution NewSolution => _newSolution;
21+
1922
internal SolutionChanges(Solution newSolution, Solution oldSolution)
2023
{
2124
_newSolution = newSolution;

0 commit comments

Comments
 (0)