Skip to content

Commit 9f8e62d

Browse files
Fix Code Lens around source generated files (#79992)
To best understand this fix, we first need to discuss how CodeLens works in Visual Studio. The editor calls our implementation of IAsyncCodeLensDataPointProvider which exists in a ServiceHub process created for CodeLens which is not our usual ServiceHub process. That implementation uses the callback feature to marshal a call back to devenv, where from there we then do an OOP operation to our ServiceHub process via the usual mechanism. This is where we compute the actual results, which then get marshalled back to devenv and then back to the CodeLens ServiceHub process, where we finally send the data via the CodeLens APIs. The core type of this is a ReferenceLocationDescriptor which represented a reference location in a specific location in a document. However, because this struct returned from our ServiceHub to devenv, and then from devenv to the CodeLens service hub, it needs to be serialized and deserialized twice. The serialization channels are different though, so whereas a DocumentId can serialize between devenv and our ServiceHub process, it would get tripped up when it serializes between devenv and the CodeLens ServiceHub process. Because of this, the type instead decomposed a DocumentId into the GUIDs and manually serialized/deserialized it. This serialization/deserialization was broken once DocumentIds got a "is source generated" bit, since we'd lose that bit which would break things. I could have added that bit in and updated the serialization but the question we hit was "why can't I just pass a DocumentId?" Well, the answer was because we might try sending that DocumentId to the CodeLens ServiceHub process, but upon closer inspection, the result of that was never used. The flow was that we'd take the results with document IDs out of our ServiceHub process, then do some filtering and mapping for Razor scenarios (which used those document IDs); at this point the results don't need document IDs anymore. So to do this I split out the "mapping" as a separate operation for clarity, and then used different types, one with the ID and one without, for the different flows. There is still one bug left here: code lens will now show references in source generated documents, but you can't open those since we aren't returning a file path VS will understand there. That's fixable too, but this is a significant improvement so let's get this done first. I'm not sure if fixing that might require other code changes in the CodeLens system... Fixes #79699 Fixes #76608 Fixes #74104
2 parents 5bfb034 + 8416cd5 commit 9f8e62d

File tree

11 files changed

+374
-379
lines changed

11 files changed

+374
-379
lines changed

src/EditorFeatures/TestUtilities/CodeLens/AbstractCodeLensTest.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ protected static async Task RunCountTest(XElement input, int cap = 0)
3232
foreach (var span in annotatedSpan.Value)
3333
{
3434
var declarationSyntaxNode = syntaxNode.FindNode(span);
35-
var result = await new CodeLensReferencesService().GetReferenceCountAsync(workspace.CurrentSolution, annotatedDocument.Id,
35+
var result = await workspace.Services.GetRequiredService<ICodeLensReferencesService>().GetReferenceCountAsync(workspace.CurrentSolution, annotatedDocument.Id,
3636
declarationSyntaxNode, cap, CancellationToken.None);
3737
Assert.NotNull(result);
3838
Assert.Equal(expected, result.Value.Count);
@@ -59,7 +59,7 @@ protected static async Task RunReferenceTest(XElement input)
5959
foreach (var span in annotatedSpan.Value)
6060
{
6161
var declarationSyntaxNode = syntaxNode.FindNode(span);
62-
var result = await new CodeLensReferencesService().FindReferenceLocationsAsync(workspace.CurrentSolution,
62+
var result = await workspace.Services.GetRequiredService<ICodeLensReferencesService>().FindReferenceLocationsAsync(workspace.CurrentSolution,
6363
annotatedDocument.Id, declarationSyntaxNode, CancellationToken.None);
6464
Assert.True(result.HasValue);
6565
Assert.Equal(expected, result.Value.Length);
@@ -85,7 +85,7 @@ protected static async Task RunMethodReferenceTest(XElement input)
8585
foreach (var span in annotatedSpan.Value)
8686
{
8787
var declarationSyntaxNode = syntaxNode.FindNode(span);
88-
var result = await new CodeLensReferencesService().FindReferenceMethodsAsync(workspace.CurrentSolution,
88+
var result = await workspace.Services.GetRequiredService<ICodeLensReferencesService>().FindReferenceMethodsAsync(workspace.CurrentSolution,
8989
annotatedDocument.Id, declarationSyntaxNode, CancellationToken.None);
9090
Assert.True(result.HasValue);
9191
Assert.Equal(expected, result.Value.Length);
@@ -111,7 +111,7 @@ protected static async Task RunFullyQualifiedNameTest(XElement input)
111111
foreach (var span in annotatedSpan.Value)
112112
{
113113
var declarationSyntaxNode = syntaxNode.FindNode(span);
114-
var actual = await new CodeLensReferencesService().GetFullyQualifiedNameAsync(workspace.CurrentSolution,
114+
var actual = await workspace.Services.GetRequiredService<ICodeLensReferencesService>().GetFullyQualifiedNameAsync(workspace.CurrentSolution,
115115
annotatedDocument.Id, declarationSyntaxNode, CancellationToken.None);
116116
Assert.Equal(expected, actual);
117117
}

src/Features/Core/Portable/CodeLens/CodeLensHelpers.cs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,16 @@ internal static class CodeLensHelpers
1616
if (TryGetGuid("RoslynDocumentIdGuid", out var documentIdGuid) &&
1717
TryGetGuid("RoslynProjectIdGuid", out var projectIdGuid))
1818
{
19+
var isSourceGenerated = false;
20+
21+
if (descriptorProperties.TryGetValue("RoslynDocumentIsSourceGenerated", out var isSourceGeneratedObj))
22+
{
23+
if (isSourceGeneratedObj is bool isSourceGeneratedBoolean)
24+
isSourceGenerated = isSourceGeneratedBoolean;
25+
}
26+
1927
var projectId = ProjectId.CreateFromSerialized(projectIdGuid);
20-
return DocumentId.CreateFromSerialized(projectId, documentIdGuid);
28+
return DocumentId.CreateFromSerialized(projectId, documentIdGuid, isSourceGenerated, debugName: null);
2129
}
2230

2331
return null;

src/Features/Core/Portable/CodeLens/CodeLensReferencesService.cs

Lines changed: 160 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,11 @@
1010
using System.Linq;
1111
using System.Threading;
1212
using System.Threading.Tasks;
13+
using Microsoft.CodeAnalysis.Classification;
1314
using Microsoft.CodeAnalysis.FindSymbols;
1415
using Microsoft.CodeAnalysis.Host;
1516
using Microsoft.CodeAnalysis.LanguageService;
17+
using Microsoft.CodeAnalysis.Options;
1618
using Microsoft.CodeAnalysis.PooledObjects;
1719
using Microsoft.CodeAnalysis.Shared.Extensions;
1820
using Microsoft.CodeAnalysis.Text;
@@ -98,7 +100,7 @@ public async ValueTask<VersionStamp> GetProjectCodeLensVersionAsync(Solution sol
98100
maxSearchResults, cancellationToken).ConfigureAwait(false);
99101
}
100102

101-
private static async Task<ReferenceLocationDescriptor> GetDescriptorOfEnclosingSymbolAsync(Solution solution, Location location, CancellationToken cancellationToken)
103+
private static async Task<ReferenceLocationDescriptorAndDocument> GetDescriptorOfEnclosingSymbolAsync(Solution solution, Location location, CancellationToken cancellationToken)
102104
{
103105
var document = solution.GetDocument(location.SourceTree);
104106

@@ -130,41 +132,36 @@ private static async Task<ReferenceLocationDescriptor> GetDescriptorOfEnclosingS
130132
var spanStart = token.Span.Start - textLine.Span.Start;
131133
var line = textLine.ToString();
132134

133-
var beforeLine1 = textLine.LineNumber > 0 ? text.Lines[textLine.LineNumber - 1].ToString() : string.Empty;
134-
var beforeLine2 = textLine.LineNumber - 1 > 0
135-
? text.Lines[textLine.LineNumber - 2].ToString()
136-
: string.Empty;
137-
var afterLine1 = textLine.LineNumber < text.Lines.Count - 1
138-
? text.Lines[textLine.LineNumber + 1].ToString()
139-
: string.Empty;
140-
var afterLine2 = textLine.LineNumber + 1 < text.Lines.Count - 1
141-
? text.Lines[textLine.LineNumber + 2].ToString()
142-
: string.Empty;
135+
var beforeLine1 = GetLineTextOrEmpty(text.Lines, textLine.LineNumber - 1);
136+
var beforeLine2 = GetLineTextOrEmpty(text.Lines, textLine.LineNumber - 2);
137+
var afterLine1 = GetLineTextOrEmpty(text.Lines, textLine.LineNumber + 1);
138+
var afterLine2 = GetLineTextOrEmpty(text.Lines, textLine.LineNumber + 2);
143139
var referenceSpan = new TextSpan(spanStart, token.Span.Length);
144140

145141
var symbol = semanticModel.GetDeclaredSymbol(node, cancellationToken);
146142
var glyph = symbol?.GetGlyph();
147143
var startLinePosition = location.GetLineSpan().StartLinePosition;
148-
var documentId = solution.GetDocument(location.SourceTree)?.Id;
149-
150-
return new ReferenceLocationDescriptor(
151-
longName,
152-
semanticModel.Language,
153-
glyph,
154-
token.Span.Start,
155-
token.Span.Length,
156-
startLinePosition.Line,
157-
startLinePosition.Character,
158-
documentId.ProjectId.Id,
159-
documentId.Id,
160-
document.FilePath,
161-
line.TrimEnd(),
162-
referenceSpan.Start,
163-
referenceSpan.Length,
164-
beforeLine1.TrimEnd(),
165-
beforeLine2.TrimEnd(),
166-
afterLine1.TrimEnd(),
167-
afterLine2.TrimEnd());
144+
145+
return new ReferenceLocationDescriptorAndDocument
146+
{
147+
Descriptor = new ReferenceLocationDescriptor(
148+
longName,
149+
semanticModel.Language,
150+
glyph,
151+
token.Span.Start,
152+
token.Span.Length,
153+
startLinePosition.Line,
154+
startLinePosition.Character,
155+
document.FilePath,
156+
line.TrimEnd(),
157+
referenceSpan.Start,
158+
referenceSpan.Length,
159+
beforeLine1.TrimEnd(),
160+
beforeLine2.TrimEnd(),
161+
afterLine1.TrimEnd(),
162+
afterLine2.TrimEnd()),
163+
DocumentId = document.Id
164+
};
168165
}
169166

170167
private static SyntaxNode GetEnclosingCodeElementNode(Document document, SyntaxToken token, ICodeLensDisplayInfoService langServices, CancellationToken cancellationToken)
@@ -199,7 +196,7 @@ private static SyntaxNode GetEnclosingCodeElementNode(Document document, SyntaxT
199196
return langServices.GetDisplayNode(node);
200197
}
201198

202-
public async Task<ImmutableArray<ReferenceLocationDescriptor>?> FindReferenceLocationsAsync(Solution solution, DocumentId documentId, SyntaxNode syntaxNode, CancellationToken cancellationToken)
199+
public async Task<ImmutableArray<ReferenceLocationDescriptorAndDocument>?> FindReferenceLocationsAsync(Solution solution, DocumentId documentId, SyntaxNode syntaxNode, CancellationToken cancellationToken)
203200
{
204201
return await FindAsync(solution, documentId, syntaxNode,
205202
async progress =>
@@ -214,6 +211,137 @@ private static SyntaxNode GetEnclosingCodeElementNode(Document document, SyntaxT
214211
}, onCapped: null, searchCap: 0, cancellationToken: cancellationToken).ConfigureAwait(false);
215212
}
216213

214+
public async Task<ImmutableArray<ReferenceLocationDescriptor>> MapReferenceLocationsAsync(Solution solution, ImmutableArray<ReferenceLocationDescriptorAndDocument> referenceLocations, ClassificationOptions classificationOptions, CancellationToken cancellationToken)
215+
{
216+
using var _ = ArrayBuilder<ReferenceLocationDescriptor>.GetInstance(out var list);
217+
foreach (var descriptorAndDocument in referenceLocations)
218+
{
219+
var document = await solution.GetDocumentAsync(descriptorAndDocument.DocumentId, includeSourceGenerated: true, cancellationToken).ConfigureAwait(false);
220+
if (document == null)
221+
{
222+
continue;
223+
}
224+
225+
var descriptor = descriptorAndDocument.Descriptor;
226+
var span = new TextSpan(descriptor.SpanStart, descriptor.SpanLength);
227+
var results = await SpanMappingHelper.TryGetMappedSpanResultAsync(document, [span], cancellationToken).ConfigureAwait(false);
228+
if (results is null)
229+
{
230+
// for normal document, just add one as they are
231+
list.Add(descriptor);
232+
continue;
233+
}
234+
235+
var mappedSpans = results.GetValueOrDefault();
236+
237+
// external component violated contracts. the mapper should preserve input order/count.
238+
// since we gave in 1 span, it should return 1 span back
239+
Contract.ThrowIfTrue(mappedSpans.IsDefaultOrEmpty);
240+
241+
var result = mappedSpans[0];
242+
if (result.IsDefault)
243+
{
244+
// it is allowed for mapper to return default
245+
// if it can't map the given span to any usable span
246+
continue;
247+
}
248+
249+
var excerpter = document.DocumentServiceProvider.GetService<IDocumentExcerptService>();
250+
if (excerpter == null)
251+
{
252+
if (document.IsRazorSourceGeneratedDocument())
253+
{
254+
// HACK: Razor doesn't have has a workspace level excerpt service, but if we just return a simple descriptor here,
255+
// the user at least sees something, can navigate, and Razor can improve this later if necessary. Until
256+
// https://github.com/dotnet/roslyn/issues/79699 is fixed this won't get hit anyway.
257+
list.Add(new ReferenceLocationDescriptor(
258+
descriptor.LongDescription,
259+
descriptor.Language,
260+
descriptor.Glyph,
261+
result.Span.Start,
262+
result.Span.Length,
263+
result.LinePositionSpan.Start.Line,
264+
result.LinePositionSpan.Start.Character,
265+
result.FilePath,
266+
descriptor.ReferenceLineText,
267+
descriptor.ReferenceStart,
268+
descriptor.ReferenceLength,
269+
"",
270+
"",
271+
"",
272+
""));
273+
}
274+
continue;
275+
}
276+
277+
var referenceExcerpt = await excerpter.TryExcerptAsync(document, span, ExcerptMode.SingleLine, classificationOptions, cancellationToken).ConfigureAwait(false);
278+
var tooltipExcerpt = await excerpter.TryExcerptAsync(document, span, ExcerptMode.Tooltip, classificationOptions, cancellationToken).ConfigureAwait(false);
279+
280+
var (text, start, length) = GetReferenceInfo(referenceExcerpt, descriptor);
281+
var (before1, before2, after1, after2) = GetReferenceTexts(referenceExcerpt, tooltipExcerpt, descriptor);
282+
283+
list.Add(new ReferenceLocationDescriptor(
284+
descriptor.LongDescription,
285+
descriptor.Language,
286+
descriptor.Glyph,
287+
result.Span.Start,
288+
result.Span.Length,
289+
result.LinePositionSpan.Start.Line,
290+
result.LinePositionSpan.Start.Character,
291+
result.FilePath,
292+
text,
293+
start,
294+
length,
295+
before1,
296+
before2,
297+
after1,
298+
after2));
299+
}
300+
301+
return list.ToImmutableAndClear();
302+
}
303+
304+
private static (string text, int start, int length) GetReferenceInfo(ExcerptResult? reference, ReferenceLocationDescriptor descriptor)
305+
{
306+
if (reference.HasValue)
307+
{
308+
return (reference.Value.Content.ToString().TrimEnd(),
309+
reference.Value.MappedSpan.Start,
310+
reference.Value.MappedSpan.Length);
311+
}
312+
313+
return (descriptor.ReferenceLineText, descriptor.ReferenceStart, descriptor.ReferenceLength);
314+
}
315+
316+
private static (string before1, string before2, string after1, string after2) GetReferenceTexts(ExcerptResult? reference, ExcerptResult? tooltip, ReferenceLocationDescriptor descriptor)
317+
{
318+
if (reference == null || tooltip == null)
319+
{
320+
return (descriptor.BeforeReferenceText1, descriptor.BeforeReferenceText2, descriptor.AfterReferenceText1, descriptor.AfterReferenceText2);
321+
}
322+
323+
var lines = tooltip.Value.Content.Lines;
324+
var mappedLine = lines.GetLineFromPosition(tooltip.Value.MappedSpan.Start);
325+
var index = mappedLine.LineNumber;
326+
if (index < 0)
327+
{
328+
return (descriptor.BeforeReferenceText1, descriptor.BeforeReferenceText2, descriptor.AfterReferenceText1, descriptor.AfterReferenceText2);
329+
}
330+
331+
return (GetLineTextOrEmpty(lines, index - 1), GetLineTextOrEmpty(lines, index - 2),
332+
GetLineTextOrEmpty(lines, index + 1), GetLineTextOrEmpty(lines, index + 2));
333+
}
334+
335+
private static string GetLineTextOrEmpty(TextLineCollection lines, int index)
336+
{
337+
if (index < 0 || index >= lines.Count)
338+
{
339+
return string.Empty;
340+
}
341+
342+
return lines[index].ToString().TrimEnd();
343+
}
344+
217345
private static ISymbol GetEnclosingMethod(SemanticModel semanticModel, Location location, CancellationToken cancellationToken)
218346
{
219347
var enclosingSymbol = semanticModel.GetEnclosingSymbol(location.SourceSpan.Start, cancellationToken);

src/Features/Core/Portable/CodeLens/ICodeLensReferencesService.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Collections.Immutable;
66
using System.Threading;
77
using System.Threading.Tasks;
8+
using Microsoft.CodeAnalysis.Classification;
89
using Microsoft.CodeAnalysis.Host;
910

1011
namespace Microsoft.CodeAnalysis.CodeLens;
@@ -25,7 +26,12 @@ internal interface ICodeLensReferencesService : IWorkspaceService
2526
/// <summary>
2627
/// Given a document and syntax node, returns a collection of locations where the located node is referenced.
2728
/// </summary>
28-
Task<ImmutableArray<ReferenceLocationDescriptor>?> FindReferenceLocationsAsync(Solution solution, DocumentId documentId, SyntaxNode? syntaxNode, CancellationToken cancellationToken);
29+
Task<ImmutableArray<ReferenceLocationDescriptorAndDocument>?> FindReferenceLocationsAsync(Solution solution, DocumentId documentId, SyntaxNode? syntaxNode, CancellationToken cancellationToken);
30+
31+
/// <summary>
32+
/// Maps the list of referenced locations through span mapping services to the final list of locations; locations may be in files that aren't regular documents.
33+
/// </summary>
34+
Task<ImmutableArray<ReferenceLocationDescriptor>> MapReferenceLocationsAsync(Solution solution, ImmutableArray<ReferenceLocationDescriptorAndDocument> referenceLocations, ClassificationOptions classificationOptions, CancellationToken cancellationToken);
2935

3036
/// <summary>
3137
/// Given a document and syntax node, returns a collection of locations of methods that refer to the located node.

src/Features/Core/Portable/CodeLens/IRemoteCodeLensReferencesService.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ namespace Microsoft.CodeAnalysis.CodeLens;
1212
internal interface IRemoteCodeLensReferencesService
1313
{
1414
ValueTask<ReferenceCount?> GetReferenceCountAsync(Checksum solutionChecksum, DocumentId documentId, TextSpan textSpan, int maxResultCount, CancellationToken cancellationToken);
15-
ValueTask<ImmutableArray<ReferenceLocationDescriptor>?> FindReferenceLocationsAsync(Checksum solutionChecksum, DocumentId documentId, TextSpan textSpan, CancellationToken cancellationToken);
15+
ValueTask<ImmutableArray<ReferenceLocationDescriptorAndDocument>?> FindReferenceLocationsAsync(Checksum solutionChecksum, DocumentId documentId, TextSpan textSpan, CancellationToken cancellationToken);
1616
ValueTask<ImmutableArray<ReferenceMethodDescriptor>?> FindReferenceMethodsAsync(Checksum solutionChecksum, DocumentId documentId, TextSpan textSpan, CancellationToken cancellationToken);
1717
ValueTask<string?> GetFullyQualifiedNameAsync(Checksum solutionChecksum, DocumentId documentId, TextSpan textSpan, CancellationToken cancellationToken);
1818
}

0 commit comments

Comments
 (0)