Skip to content

Commit 34cc62f

Browse files
committed
Support local functions in breadcrumbs
1 parent ae499de commit 34cc62f

File tree

3 files changed

+153
-12
lines changed

3 files changed

+153
-12
lines changed

src/EditorFeatures/Test2/NavigationBar/CSharpNavigationBarTests.vb

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -390,5 +390,74 @@ static class C
390390
Item("C.extension(string)", Glyph.ClassPublic), False,
391391
Item("Goo()", Glyph.ExtensionMethodPublic), False)
392392
End Function
393+
394+
<Theory, CombinatorialData, WorkItem("https://github.com/dotnet/vscode-csharp/issues/6767")>
395+
Public Async Function TestLocalFunction(host As TestHost) As Task
396+
Await AssertItemsAreAsync(
397+
<Workspace>
398+
<Project Language="C#" CommonReferences="true">
399+
<Document>
400+
class C { void M() { void Local() { } } }
401+
</Document>
402+
</Project>
403+
</Workspace>,
404+
host,
405+
Item("C", Glyph.ClassInternal, children:={
406+
Item("M()", Glyph.MethodPrivate, children:={
407+
Item("Local()", Glyph.MethodPrivate)})}))
408+
End Function
409+
410+
<Theory, CombinatorialData, WorkItem("https://github.com/dotnet/vscode-csharp/issues/6767")>
411+
Public Async Function TestNestedLocalFunction(host As TestHost) As Task
412+
Await AssertItemsAreAsync(
413+
<Workspace>
414+
<Project Language="C#" CommonReferences="true">
415+
<Document>
416+
class C { void M() { void Local() { void NestedLocal() { } } } }
417+
</Document>
418+
</Project>
419+
</Workspace>,
420+
host,
421+
Item("C", Glyph.ClassInternal, children:={
422+
Item("M()", Glyph.MethodPrivate, children:={
423+
Item("Local()", Glyph.MethodPrivate, children:={
424+
Item("NestedLocal()", Glyph.MethodPrivate)})})}))
425+
End Function
426+
427+
<Theory, CombinatorialData, WorkItem("https://github.com/dotnet/vscode-csharp/issues/6767")>
428+
Public Async Function TestMultipleLocalFunction(host As TestHost) As Task
429+
Await AssertItemsAreAsync(
430+
<Workspace>
431+
<Project Language="C#" CommonReferences="true">
432+
<Document>
433+
class C { void M() { void Local1() { } void Local2() { } } }
434+
</Document>
435+
</Project>
436+
</Workspace>,
437+
host,
438+
Item("C", Glyph.ClassInternal, children:={
439+
Item("M()", Glyph.MethodPrivate, children:={
440+
Item("Local1()", Glyph.MethodPrivate),
441+
Item("Local2()", Glyph.MethodPrivate)})}))
442+
End Function
443+
444+
<Theory, CombinatorialData, WorkItem("https://github.com/dotnet/vscode-csharp/issues/6767")>
445+
Public Async Function TestTopLevelProgram(host As TestHost) As Task
446+
Await AssertItemsAreAsync(
447+
<Workspace>
448+
<Project Language="C#" CommonReferences="true">
449+
<Document>
450+
using System;
451+
Console.WriteLine("Hello World!");
452+
453+
void Method() { }
454+
</Document>
455+
</Project>
456+
</Workspace>,
457+
host,
458+
Item("Program", Glyph.ClassInternal, children:={
459+
Item("<top-level-statements-entry-point>", Glyph.MethodPrivate, children:={
460+
Item("Method()", Glyph.MethodPrivate)})}))
461+
End Function
393462
End Class
394463
End Namespace

src/Features/CSharp/Portable/NavigationBar/CSharpNavigationBarItemService.cs

Lines changed: 41 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,16 @@
77
using System.Collections.Immutable;
88
using System.Composition;
99
using System.Diagnostics;
10+
using System.Linq;
1011
using System.Threading;
1112
using System.Threading.Tasks;
13+
using Microsoft.CodeAnalysis.CSharp.Extensions;
14+
using Microsoft.CodeAnalysis.CSharp.LanguageService;
1215
using Microsoft.CodeAnalysis.CSharp.Syntax;
16+
using Microsoft.CodeAnalysis.Elfie.Model;
1317
using Microsoft.CodeAnalysis.Host.Mef;
1418
using Microsoft.CodeAnalysis.Internal.Log;
19+
using Microsoft.CodeAnalysis.LanguageService;
1520
using Microsoft.CodeAnalysis.NavigationBar;
1621
using Microsoft.CodeAnalysis.PooledObjects;
1722
using Microsoft.CodeAnalysis.Shared.Extensions;
@@ -54,11 +59,11 @@ protected override async Task<ImmutableArray<RoslynNavigationBarItem>> GetItemsI
5459
if (cancellationToken.IsCancellationRequested)
5560
return [];
5661

57-
return GetMembersInTypes(document.Project.Solution, semanticModel.SyntaxTree, typesInFile, cancellationToken);
62+
return GetMembersInTypes(document.Project.Solution, semanticModel.SyntaxTree, typesInFile, semanticModel, cancellationToken);
5863
}
5964

6065
private static ImmutableArray<RoslynNavigationBarItem> GetMembersInTypes(
61-
Solution solution, SyntaxTree tree, HashSet<INamedTypeSymbol> types, CancellationToken cancellationToken)
66+
Solution solution, SyntaxTree tree, HashSet<INamedTypeSymbol> types, SemanticModel semanticModel, CancellationToken cancellationToken)
6267
{
6368
using (Logger.LogBlock(FunctionId.NavigationBar_ItemService_GetMembersInTypes_CSharp, cancellationToken))
6469
{
@@ -79,29 +84,29 @@ private static ImmutableArray<RoslynNavigationBarItem> GetMembersInTypes(
7984

8085
if (member is IMethodSymbol { PartialImplementationPart: { } } methodSymbol)
8186
{
82-
memberItems.AddIfNotNull(CreateItemForMember(solution, methodSymbol, tree, cancellationToken));
83-
memberItems.AddIfNotNull(CreateItemForMember(solution, methodSymbol.PartialImplementationPart, tree, cancellationToken));
87+
memberItems.AddIfNotNull(CreateItemForMember(solution, methodSymbol, tree, semanticModel, cancellationToken));
88+
memberItems.AddIfNotNull(CreateItemForMember(solution, methodSymbol.PartialImplementationPart, tree, semanticModel, cancellationToken));
8489
}
8590
else if (member is IPropertySymbol { PartialImplementationPart: { } } propertySymbol)
8691
{
87-
memberItems.AddIfNotNull(CreateItemForMember(solution, propertySymbol, tree, cancellationToken));
88-
memberItems.AddIfNotNull(CreateItemForMember(solution, propertySymbol.PartialImplementationPart, tree, cancellationToken));
92+
memberItems.AddIfNotNull(CreateItemForMember(solution, propertySymbol, tree, semanticModel, cancellationToken));
93+
memberItems.AddIfNotNull(CreateItemForMember(solution, propertySymbol.PartialImplementationPart, tree, semanticModel, cancellationToken));
8994
}
9095
else if (member is IEventSymbol { PartialImplementationPart: { } } eventSymbol)
9196
{
92-
memberItems.AddIfNotNull(CreateItemForMember(solution, eventSymbol, tree, cancellationToken));
93-
memberItems.AddIfNotNull(CreateItemForMember(solution, eventSymbol.PartialImplementationPart, tree, cancellationToken));
97+
memberItems.AddIfNotNull(CreateItemForMember(solution, eventSymbol, tree, semanticModel, cancellationToken));
98+
memberItems.AddIfNotNull(CreateItemForMember(solution, eventSymbol.PartialImplementationPart, tree, semanticModel, cancellationToken));
9499
}
95100
else if (member is IMethodSymbol or IPropertySymbol or IEventSymbol)
96101
{
97102
Debug.Assert(member is IMethodSymbol { PartialDefinitionPart: null } or IPropertySymbol { PartialDefinitionPart: null } or IEventSymbol { PartialDefinitionPart: null },
98103
$"NavBar expected GetMembers to return partial method/property/event definition parts but the implementation part was returned.");
99104

100-
memberItems.AddIfNotNull(CreateItemForMember(solution, member, tree, cancellationToken));
105+
memberItems.AddIfNotNull(CreateItemForMember(solution, member, tree, semanticModel, cancellationToken));
101106
}
102107
else
103108
{
104-
memberItems.AddIfNotNull(CreateItemForMember(solution, member, tree, cancellationToken));
109+
memberItems.AddIfNotNull(CreateItemForMember(solution, member, tree, semanticModel, cancellationToken));
105110
}
106111
}
107112

@@ -166,6 +171,7 @@ StatementSyntax or
166171
{
167172
BaseTypeDeclarationSyntax t => semanticModel.GetDeclaredSymbol(t, cancellationToken),
168173
DelegateDeclarationSyntax d => semanticModel.GetDeclaredSymbol(d, cancellationToken),
174+
CompilationUnitSyntax c => c.IsTopLevelProgram() ? semanticModel.GetDeclaredSymbol(c, cancellationToken)?.ContainingType : null,
169175
_ => null,
170176
};
171177

@@ -182,18 +188,41 @@ private static bool IsAccessor(ISymbol member)
182188
}
183189

184190
private static SymbolItem? CreateItemForMember(
185-
Solution solution, ISymbol member, SyntaxTree tree, CancellationToken cancellationToken)
191+
Solution solution, ISymbol member, SyntaxTree tree, SemanticModel semanticModel, CancellationToken cancellationToken)
186192
{
187193
var location = GetSymbolLocation(solution, member, tree, cancellationToken);
188194
if (location == null)
189195
return null;
190196

197+
using var _ = ArrayBuilder<RoslynNavigationBarItem>.GetInstance(out var localFunctionItems);
198+
foreach (var syntaxReference in member.DeclaringSyntaxReferences)
199+
{
200+
if (syntaxReference.SyntaxTree != tree)
201+
{
202+
// The reference is not in this file, no need to include in the outline view.
203+
continue;
204+
}
205+
206+
var node = syntaxReference.GetSyntax(cancellationToken);
207+
foreach (var localFunction in node.DescendantNodes().Where(CSharpSyntaxFacts.Instance.IsLocalFunctionStatement))
208+
{
209+
var localFunctionSymbol = semanticModel.GetDeclaredSymbol(localFunction, cancellationToken);
210+
// Check to make sure we only include local functions that are directly contained in the current member.
211+
// We'll recursively add any nested local functions when we traverse the direct descendent.
212+
if (localFunctionSymbol is IMethodSymbol && localFunctionSymbol.ContainingSymbol == member)
213+
{
214+
localFunctionItems.AddIfNotNull(CreateItemForMember(solution, localFunctionSymbol, tree, semanticModel, cancellationToken));
215+
}
216+
}
217+
}
218+
191219
return new SymbolItem(
192220
member.ToDisplayString(s_memberNameFormat),
193221
member.ToDisplayString(s_memberDetailsFormat),
194222
member.GetGlyph(),
195223
member.IsObsolete(),
196-
location.Value);
224+
location.Value,
225+
localFunctionItems.ToImmutable());
197226
}
198227

199228
private static SymbolItemLocation? GetSymbolLocation(

src/LanguageServer/ProtocolUnitTests/Symbols/DocumentSymbolsTests.cs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,49 @@ public async Task TestGetDocumentSymbolsAsync__NoSymbols(bool mutatingLspWorkspa
175175
Assert.Empty(results);
176176
}
177177

178+
[Theory, CombinatorialData]
179+
public async Task TestGetDocumentSymbolsAsync_LocalFunction(bool mutatingLspWorkspace)
180+
{
181+
var markup =
182+
"""
183+
namespace Test;
184+
{|class:class {|classSelection:A|}
185+
{
186+
{|method:void {|methodSelection:M|}()
187+
{
188+
{|localFunction:void {|localFunctionSelection:LocalFunction|}()
189+
{
190+
}|}
191+
}|}
192+
}|}
193+
""";
194+
var clientCapabilities = new LSP.ClientCapabilities()
195+
{
196+
TextDocument = new LSP.TextDocumentClientCapabilities()
197+
{
198+
DocumentSymbol = new LSP.DocumentSymbolSetting()
199+
{
200+
HierarchicalDocumentSymbolSupport = true
201+
}
202+
}
203+
};
204+
205+
await using var testLspServer = await CreateTestLspServerAsync(markup, mutatingLspWorkspace, clientCapabilities);
206+
var classSymbol = CreateDocumentSymbol(LSP.SymbolKind.Class, "A", "Test.A", testLspServer.GetLocations("class").Single(), testLspServer.GetLocations("classSelection").Single());
207+
var methodSymbol = CreateDocumentSymbol(LSP.SymbolKind.Method, "M", "M()", testLspServer.GetLocations("method").Single(), testLspServer.GetLocations("methodSelection").Single(), classSymbol);
208+
var localFunctionSymbol = CreateDocumentSymbol(LSP.SymbolKind.Method, "LocalFunction", "LocalFunction()", testLspServer.GetLocations("localFunction").Single(), testLspServer.GetLocations("localFunctionSelection").Single(), methodSymbol);
209+
210+
LSP.DocumentSymbol[] expected = [classSymbol];
211+
212+
var results = await RunGetDocumentSymbolsAsync<LSP.DocumentSymbol[]>(testLspServer);
213+
Assert.NotNull(results);
214+
Assert.Equal(expected.Length, results.Length);
215+
for (var i = 0; i < results.Length; i++)
216+
{
217+
AssertDocumentSymbolEquals(expected[i], results[i]);
218+
}
219+
}
220+
178221
private static async Task<TReturn?> RunGetDocumentSymbolsAsync<TReturn>(TestLspServer testLspServer)
179222
{
180223
var document = testLspServer.GetCurrentSolution().Projects.First().Documents.First();

0 commit comments

Comments
 (0)