Skip to content

Commit c7222cd

Browse files
Include comments written above local variables when getting quick info for them. (#79580)
2 parents fcc8f10 + 820e7b0 commit c7222cd

File tree

9 files changed

+200
-39
lines changed

9 files changed

+200
-39
lines changed

src/EditorFeatures/CSharpTest/QuickInfo/SemanticQuickInfoSourceTests.cs

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9815,4 +9815,99 @@ public void Goo()
98159815
}
98169816
""",
98179817
MainDescription($"Extensions.extension(System.String)"));
9818+
9819+
[Fact, WorkItem("https://github.com/dotnet/roslyn/issues/72780")]
9820+
public Task TestLocalVariableComment1()
9821+
=> TestAsync(
9822+
"""
9823+
class C
9824+
{
9825+
void M()
9826+
{
9827+
// Comment on i
9828+
int i;
9829+
Console.WriteLine($$i);
9830+
}
9831+
}
9832+
""",
9833+
MainDescription($"({FeaturesResources.local_variable}) int i"),
9834+
Documentation("Comment on i"));
9835+
9836+
[Fact, WorkItem("https://github.com/dotnet/roslyn/issues/72780")]
9837+
public Task TestLocalVariableComment2()
9838+
=> TestAsync(
9839+
"""
9840+
class C
9841+
{
9842+
void M()
9843+
{
9844+
// Comment unrelated to i
9845+
9846+
int i;
9847+
Console.WriteLine($$i);
9848+
}
9849+
}
9850+
""",
9851+
MainDescription($"({FeaturesResources.local_variable}) int i"));
9852+
9853+
[Fact, WorkItem("https://github.com/dotnet/roslyn/issues/72780")]
9854+
public Task TestLocalVariableComment3()
9855+
=> TestAsync(
9856+
"""
9857+
class C
9858+
{
9859+
void M()
9860+
{
9861+
// Multi
9862+
// line
9863+
// comment for i
9864+
int i;
9865+
Console.WriteLine($$i);
9866+
}
9867+
}
9868+
""",
9869+
MainDescription($"({FeaturesResources.local_variable}) int i"),
9870+
Documentation("Multi line comment for i"));
9871+
9872+
[Fact, WorkItem("https://github.com/dotnet/roslyn/issues/72780")]
9873+
public Task TestLocalVariableComment4()
9874+
=> TestAsync(
9875+
"""
9876+
class C
9877+
{
9878+
void M()
9879+
{
9880+
// Comment for i. It is > 0
9881+
int i;
9882+
Console.WriteLine($$i);
9883+
}
9884+
}
9885+
""",
9886+
MainDescription($"({FeaturesResources.local_variable}) int i"),
9887+
Documentation("Comment for i. It is > 0"));
9888+
9889+
[Fact, WorkItem("https://github.com/dotnet/roslyn/issues/72780")]
9890+
public Task TestLocalVariableComment5()
9891+
=> TestWithOptionsAsync(
9892+
Options.Regular,
9893+
"""
9894+
// Comment for i. It is > 0
9895+
int i;
9896+
Console.WriteLine($$i);
9897+
""",
9898+
MainDescription($"({FeaturesResources.local_variable}) int i"),
9899+
Documentation("Comment for i. It is > 0"));
9900+
9901+
[Fact]
9902+
public Task TestLocalVariableComment6()
9903+
=> TestWithOptionsAsync(
9904+
Options.Regular,
9905+
"""
9906+
// <summary>Comment for i.
9907+
// It is &gt; 0</summary>
9908+
int i;
9909+
Console.WriteLine($$i);
9910+
""",
9911+
MainDescription($"({FeaturesResources.local_variable}) int i"),
9912+
Documentation("Comment for i. It is > 0"));
98189913
}

src/EditorFeatures/TestUtilities/QuickInfo/AbstractQuickInfoSourceTests.cs

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33
// See the LICENSE file in the project root for more information.
44

5-
#nullable disable
6-
75
using System.Linq;
86
using System.Threading.Tasks;
97
using Microsoft.CodeAnalysis.CSharp;
@@ -25,23 +23,23 @@ protected static string FormatCodeWithDocComments(params string[] code)
2523
return string.Concat(System.Environment.NewLine, formattedCode);
2624
}
2725

28-
protected async Task TestInMethodAndScriptAsync(string code, string expectedContent, string expectedDocumentationComment = null)
26+
protected async Task TestInMethodAndScriptAsync(string code, string expectedContent, string? expectedDocumentationComment = null)
2927
{
3028
await TestInMethodAsync(code, expectedContent, expectedDocumentationComment);
3129
await TestInScriptAsync(code, expectedContent, expectedDocumentationComment);
3230
}
3331

34-
protected abstract Task TestInClassAsync(string code, string expectedContent, string expectedDocumentationComment = null);
32+
protected abstract Task TestInClassAsync(string code, string expectedContent, string? expectedDocumentationComment = null);
3533

36-
protected abstract Task TestInMethodAsync(string code, string expectedContent, string expectedDocumentationComment = null);
34+
protected abstract Task TestInMethodAsync(string code, string expectedContent, string? expectedDocumentationComment = null);
3735

38-
protected abstract Task TestInScriptAsync(string code, string expectedContent, string expectedDocumentationComment = null);
36+
protected abstract Task TestInScriptAsync(string code, string expectedContent, string? expectedDocumentationComment = null);
3937

4038
protected abstract Task TestAsync(
4139
string code,
4240
string expectedContent,
43-
string expectedDocumentationComment = null,
44-
CSharpParseOptions parseOptions = null);
41+
string? expectedDocumentationComment = null,
42+
CSharpParseOptions? parseOptions = null);
4543

4644
protected abstract Task AssertNoContentAsync(
4745
EditorTestWorkspace workspace,
@@ -53,5 +51,5 @@ protected abstract Task AssertContentIsAsync(
5351
Document document,
5452
int position,
5553
string expectedContent,
56-
string expectedDocumentationComment = null);
54+
string? expectedDocumentationComment = null);
5755
}

src/EditorFeatures/TestUtilities/QuickInfo/AbstractSemanticQuickInfoSourceTests.cs

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,7 @@ protected static Tuple<string, string>[] NoClassifications()
3636
=> null;
3737

3838
internal static Action<QuickInfoItem> SymbolGlyph(Glyph expectedGlyph)
39-
{
40-
return qi =>
41-
{
42-
Assert.Contains(expectedGlyph, qi.Tags.GetGlyphs());
43-
};
44-
}
39+
=> qi => Assert.Contains(expectedGlyph, qi.Tags.GetGlyphs());
4540

4641
internal static Action<QuickInfoItem> WarningGlyph(Glyph expectedGlyph)
4742
=> SymbolGlyph(expectedGlyph);

src/Features/CSharp/Portable/DocumentationComments/CSharpDocumentationCommentFormattingService.cs

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33
// See the LICENSE file in the project root for more information.
44

5-
#nullable disable
6-
75
using System.Composition;
86
using System.Diagnostics.CodeAnalysis;
97
using Microsoft.CodeAnalysis.DocumentationComments;
@@ -12,11 +10,6 @@
1210
namespace Microsoft.CodeAnalysis.CSharp.DocumentationComments;
1311

1412
[ExportLanguageService(typeof(IDocumentationCommentFormattingService), LanguageNames.CSharp), Shared]
15-
internal sealed class CSharpDocumentationCommentFormattingService : AbstractDocumentationCommentFormattingService
16-
{
17-
[ImportingConstructor]
18-
[SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")]
19-
public CSharpDocumentationCommentFormattingService()
20-
{
21-
}
22-
}
13+
[method: ImportingConstructor]
14+
[method: SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")]
15+
internal sealed class CSharpDocumentationCommentFormattingService() : AbstractDocumentationCommentFormattingService;

src/Features/CSharp/Portable/LanguageServices/CSharpSymbolDisplayService.SymbolDescriptionBuilder.cs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -251,14 +251,15 @@ private async Task<ImmutableArray<SymbolDisplayPart>> GetInitializerSourcePartsA
251251

252252
protected override void AddCaptures(ISymbol symbol)
253253
{
254-
if (symbol is IMethodSymbol method && method.ContainingSymbol.IsKind(SymbolKind.Method))
254+
if (symbol is IMethodSymbol { ContainingSymbol.Kind: SymbolKind.Method } method)
255255
{
256256
var syntax = method.DeclaringSyntaxReferences.FirstOrDefault()?.GetSyntax();
257-
if (syntax.IsKind(SyntaxKind.LocalFunctionStatement) || syntax is AnonymousFunctionExpressionSyntax)
258-
{
257+
if (syntax is LocalFunctionStatementSyntax or AnonymousFunctionExpressionSyntax)
259258
AddCaptures(syntax);
260-
}
261259
}
262260
}
261+
262+
protected override string GetCommentText(SyntaxTrivia trivia)
263+
=> trivia.ToFullString()["//".Length..];
263264
}
264265
}

src/Features/Core/Portable/LanguageServices/SymbolDisplayService/AbstractSymbolDisplayService.AbstractSymbolDescriptionBuilder.cs

Lines changed: 79 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ protected AbstractSymbolDescriptionBuilder(
111111
protected abstract Task<ImmutableArray<SymbolDisplayPart>> GetInitializerSourcePartsAsync(ISymbol symbol);
112112
protected abstract ImmutableArray<SymbolDisplayPart> ToMinimalDisplayParts(ISymbol symbol, SemanticModel semanticModel, int position, SymbolDisplayFormat format);
113113
protected abstract string? GetNavigationHint(ISymbol? symbol);
114+
protected abstract string GetCommentText(SyntaxTrivia trivia);
114115

115116
protected abstract SymbolDisplayFormat MinimallyQualifiedFormat { get; }
116117
protected abstract SymbolDisplayFormat MinimallyQualifiedFormatWithConstants { get; }
@@ -157,7 +158,7 @@ private async Task AddPartsAsync(ImmutableArray<ISymbol> symbols)
157158

158159
// Grab the doc comment once as computing it for each portion we're concatenating can be expensive for
159160
// LSIF (which does this for every symbol in an entire solution).
160-
var firstSymbolDocumentationComment = firstSymbol.GetAppropriateDocumentationComment(Compilation, CancellationToken);
161+
var firstSymbolDocumentationComment = GetAppropriateDocumentationComment(firstSymbol, Compilation, CancellationToken);
161162

162163
await AddDescriptionPartAsync(firstSymbol).ConfigureAwait(false);
163164

@@ -169,6 +170,82 @@ private async Task AddPartsAsync(ImmutableArray<ISymbol> symbols)
169170
AddDocumentationContent(firstSymbol, firstSymbolDocumentationComment);
170171
}
171172

173+
private DocumentationComment GetAppropriateDocumentationComment(ISymbol firstSymbol, Compilation compilation, CancellationToken cancellationToken)
174+
{
175+
// For locals, we synthesize the documentation comment from the leading trivia of the local declaration.
176+
return firstSymbol is ILocalSymbol localSymbol
177+
? SynthesizeDocumentationCommentForLocal(localSymbol, cancellationToken)
178+
: firstSymbol.GetAppropriateDocumentationComment(compilation, cancellationToken);
179+
}
180+
181+
private DocumentationComment SynthesizeDocumentationCommentForLocal(
182+
ILocalSymbol localSymbol, CancellationToken cancellationToken)
183+
{
184+
if (localSymbol.DeclaringSyntaxReferences is not [var reference])
185+
return DocumentationComment.Empty;
186+
187+
var node = reference.GetSyntax(cancellationToken);
188+
189+
var syntaxFacts = LanguageServices.GetRequiredService<ISyntaxFactsService>();
190+
var statement = node.AncestorsAndSelf().FirstOrDefault(syntaxFacts.IsStatement);
191+
if (statement is null)
192+
return DocumentationComment.Empty;
193+
194+
var leadingTrivia = statement.GetLeadingTrivia();
195+
196+
var startIndex = leadingTrivia.Count;
197+
198+
// Consume any directly leading contiguous comments right before the statement.
199+
using var _1 = ArrayBuilder<SyntaxTrivia>.GetInstance(out var commentsAndNewLines);
200+
while (true)
201+
{
202+
// Skip indentation if present.
203+
if (syntaxFacts.IsWhitespaceTrivia(GetTrivia(startIndex - 1)))
204+
startIndex--;
205+
206+
if (!syntaxFacts.IsEndOfLineTrivia(GetTrivia(--startIndex)) ||
207+
!syntaxFacts.IsSingleLineCommentTrivia(GetTrivia(--startIndex)))
208+
{
209+
break;
210+
}
211+
212+
commentsAndNewLines.Add(GetTrivia(startIndex));
213+
commentsAndNewLines.Add(GetTrivia(startIndex + 1));
214+
}
215+
216+
if (commentsAndNewLines.Count == 0)
217+
return DocumentationComment.Empty;
218+
219+
// Remove the last trivia, as it's always an end of line trivia. Then reverse the array since we placed
220+
// the elements in reverse order as we walked backwards.
221+
commentsAndNewLines.Count--;
222+
commentsAndNewLines.ReverseContents();
223+
224+
// Concatenate the comment text and new lines into a single string.
225+
// The even items are the comments, and the odd items are the new lines.
226+
var text = commentsAndNewLines.Select((t, i) => i % 2 == 0 ? GetCommentText(t) : t.ToFullString()).Join("");
227+
228+
// Try seeing if the text is actually just an real xml doc comment written by the user.
229+
var docComment = DocumentationComment.FromXmlFragment(text);
230+
if (docComment.SummaryText != null)
231+
return docComment;
232+
233+
// Otherwise, try wrapping with <summary> tags to make it a valid doc comment.
234+
docComment = DocumentationComment.FromXmlFragment($"<summary>{text}</summary>");
235+
if (docComment.SummaryText != null)
236+
return docComment;
237+
238+
// Try one final time, this type escaping `<` and `>` characters so that they don't cause XML parse errors.
239+
docComment = DocumentationComment.FromXmlFragment($"<summary>{text.Replace("<", "&lt;").Replace(">", "&gt;")}</summary>");
240+
if (docComment.SummaryText != null)
241+
return docComment;
242+
243+
return DocumentationComment.Empty;
244+
245+
SyntaxTrivia GetTrivia(int index)
246+
=> index < 0 || index >= leadingTrivia.Count ? default : leadingTrivia[index];
247+
}
248+
172249
private void AddDocumentationContent(ISymbol symbol, DocumentationComment documentationComment)
173250
{
174251
var formatter = LanguageServices.GetRequiredService<IDocumentationCommentFormattingService>();
@@ -424,7 +501,7 @@ private static int GetPrecedingNewLineCount(SymbolDescriptionGroups group)
424501
}
425502
}
426503

427-
private IDictionary<SymbolDescriptionGroups, ImmutableArray<TaggedText>> BuildDescriptionSections()
504+
private Dictionary<SymbolDescriptionGroups, ImmutableArray<TaggedText>> BuildDescriptionSections()
428505
{
429506
var includeNavigationHints = Options.QuickInfoOptions.IncludeNavigationHintsInQuickInfo;
430507

src/Features/Core/Portable/LanguageServices/SymbolDisplayService/AbstractSymbolDisplayService.cs

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
using System.Threading.Tasks;
99
using Microsoft.CodeAnalysis.Collections;
1010
using Microsoft.CodeAnalysis.Host;
11+
using Roslyn.Utilities;
1112

1213
namespace Microsoft.CodeAnalysis.LanguageService;
1314

@@ -31,15 +32,13 @@ public async Task<string> ToDescriptionStringAsync(SemanticModel semanticModel,
3132
return parts.ToDisplayString();
3233
}
3334

34-
public async Task<ImmutableArray<SymbolDisplayPart>> ToDescriptionPartsAsync(SemanticModel semanticModel, int position, ImmutableArray<ISymbol> symbols, SymbolDescriptionOptions options, SymbolDescriptionGroups groups, CancellationToken cancellationToken)
35+
public Task<ImmutableArray<SymbolDisplayPart>> ToDescriptionPartsAsync(SemanticModel semanticModel, int position, ImmutableArray<ISymbol> symbols, SymbolDescriptionOptions options, SymbolDescriptionGroups groups, CancellationToken cancellationToken)
3536
{
3637
if (symbols.Length == 0)
37-
{
38-
return [];
39-
}
38+
return SpecializedTasks.EmptyImmutableArray<SymbolDisplayPart>();
4039

4140
var builder = CreateDescriptionBuilder(semanticModel, position, options, cancellationToken);
42-
return await builder.BuildDescriptionAsync(symbols, groups).ConfigureAwait(false);
41+
return builder.BuildDescriptionAsync(symbols, groups);
4342
}
4443

4544
public async Task<IDictionary<SymbolDescriptionGroups, ImmutableArray<TaggedText>>> ToDescriptionGroupsAsync(

src/Features/VisualBasic/Portable/DocumentationComments/VisualBasicDocumentationCommentFormattingService.vb

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,8 @@ Imports Microsoft.CodeAnalysis.DocumentationComments
88
Imports Microsoft.CodeAnalysis.Host.Mef
99

1010
Namespace Microsoft.CodeAnalysis.VisualBasic.DocumentationComments
11-
1211
<ExportLanguageService(GetType(IDocumentationCommentFormattingService), LanguageNames.VisualBasic), [Shared]>
13-
Friend Class VisualBasicDocumentationCommentFormattingService
12+
Friend NotInheritable Class VisualBasicDocumentationCommentFormattingService
1413
Inherits AbstractDocumentationCommentFormattingService
1514

1615
<ImportingConstructor>

src/Features/VisualBasic/Portable/LanguageServices/VisualBasicSymbolDisplayService.SymbolDescriptionBuilder.vb

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ Imports Microsoft.CodeAnalysis.VisualBasic.Syntax
1111

1212
Namespace Microsoft.CodeAnalysis.Editor.VisualBasic.LanguageServices
1313
Partial Friend Class VisualBasicSymbolDisplayService
14-
Protected Class SymbolDescriptionBuilder
14+
Protected NotInheritable Class SymbolDescriptionBuilder
1515
Inherits AbstractSymbolDescriptionBuilder
1616

1717
Private Shared ReadOnly s_minimallyQualifiedFormat As SymbolDisplayFormat = SymbolDisplayFormat.MinimallyQualifiedFormat _
@@ -172,6 +172,10 @@ Namespace Microsoft.CodeAnalysis.Editor.VisualBasic.LanguageServices
172172
End If
173173
End Sub
174174

175+
Protected Overrides Function GetCommentText(trivia As SyntaxTrivia) As String
176+
Return trivia.ToFullString().Substring(1)
177+
End Function
178+
175179
Protected Overrides ReadOnly Property MinimallyQualifiedFormat As SymbolDisplayFormat = s_minimallyQualifiedFormat
176180

177181
Protected Overrides ReadOnly Property MinimallyQualifiedFormatWithConstants As SymbolDisplayFormat = s_minimallyQualifiedFormatWithConstants

0 commit comments

Comments
 (0)