diff --git a/src/EditorFeatures/Test2/NavigationBar/CSharpNavigationBarTests.vb b/src/EditorFeatures/Test2/NavigationBar/CSharpNavigationBarTests.vb index 081fc1bebde50..c4735ba11af8d 100644 --- a/src/EditorFeatures/Test2/NavigationBar/CSharpNavigationBarTests.vb +++ b/src/EditorFeatures/Test2/NavigationBar/CSharpNavigationBarTests.vb @@ -390,5 +390,187 @@ static class C Item("C.extension(string)", Glyph.ClassPublic), False, Item("Goo()", Glyph.ExtensionMethodPublic), False) End Function + + + Public Async Function TestLocalFunction(host As TestHost) As Task + Await AssertItemsAreAsync( + + + + class C { void M() { void Local() { } } } + + + , + host, + Item("C", Glyph.ClassInternal, children:={ + Item("M()", Glyph.MethodPrivate, children:={ + Item("Local()", Glyph.MethodPrivate)})})) + End Function + + + Public Async Function TestNestedLocalFunction(host As TestHost) As Task + Await AssertItemsAreAsync( + + + + class C { void M() { void Local() { void NestedLocal() { } } } } + + + , + host, + Item("C", Glyph.ClassInternal, children:={ + Item("M()", Glyph.MethodPrivate, children:={ + Item("Local()", Glyph.MethodPrivate, children:={ + Item("NestedLocal()", Glyph.MethodPrivate)})})})) + End Function + + + Public Async Function TestMultipleLocalFunction(host As TestHost) As Task + Await AssertItemsAreAsync( + + + + class C { void M() { void Local1() { } void Local2() { } } } + + + , + host, + Item("C", Glyph.ClassInternal, children:={ + Item("M()", Glyph.MethodPrivate, children:={ + Item("Local1()", Glyph.MethodPrivate), + Item("Local2()", Glyph.MethodPrivate)})})) + End Function + + + Public Async Function TestMultipleAndNestedLocalFunction(host As TestHost) As Task + Await AssertItemsAreAsync( + + + + class C + { + void M() + { + void Local() + { + void NestedLocal() { } + } + void Local2() + { + void NestedLocal2() { } + } + } + } + + + , + host, + Item("C", Glyph.ClassInternal, children:={ + Item("M()", Glyph.MethodPrivate, children:={ + Item("Local()", Glyph.MethodPrivate, children:={ + Item("NestedLocal()", Glyph.MethodPrivate)}), + Item("Local2()", Glyph.MethodPrivate, children:={ + Item("NestedLocal2()", Glyph.MethodPrivate)})})})) + End Function + + + Public Async Function TestTopLevelProgram(host As TestHost) As Task + Await AssertItemsAreAsync( + + + + using System; + Console.WriteLine("Hello World!"); + + void Method() { } + + + , + host, + Item("Program", Glyph.ClassInternal, children:={ + Item("", Glyph.MethodPrivate, children:={ + Item("Method()", Glyph.MethodPrivate)})})) + End Function + + + Public Async Function TestLocalFunctionInProperty(host As TestHost) As Task + Await AssertItemsAreAsync( + + + + class C + { + private string _field = string.Empty; + public string Prop + { + get + { + return GetField(); + + string GetField() + { + return _field; + } + } + set + { + if (IsValid()) + { + _field = value; + } + + bool IsValid() + { + return true; + } + } + } + } + + + , + host, + Item("C", Glyph.ClassInternal, children:={ + Item("_field", Glyph.FieldPrivate), + Item("Prop", Glyph.PropertyPublic, children:={ + Item("GetField()", Glyph.MethodPrivate), + Item("IsValid()", Glyph.MethodPrivate)})})) + End Function + + + Public Async Function TestNestedLocalFunctionInProperty(host As TestHost) As Task + Await AssertItemsAreAsync( + + + + class C + { + private string _field = string.Empty; + public string Prop + { + get + { + return _field; + } + set + { + _field = value; + void Local() + { + void NestedLocal() { } + } + } + } + } + + + , + host, + Item("C", Glyph.ClassInternal, children:={ + Item("_field", Glyph.FieldPrivate), + Item("Prop", Glyph.PropertyPublic, children:={ + Item("Local()", Glyph.MethodPrivate, children:={ + Item("NestedLocal()", Glyph.MethodPrivate)})})})) + End Function End Class End Namespace diff --git a/src/Features/CSharp/Portable/NavigationBar/CSharpNavigationBarItemService.cs b/src/Features/CSharp/Portable/NavigationBar/CSharpNavigationBarItemService.cs index a06070f6118b0..93201c7443565 100644 --- a/src/Features/CSharp/Portable/NavigationBar/CSharpNavigationBarItemService.cs +++ b/src/Features/CSharp/Portable/NavigationBar/CSharpNavigationBarItemService.cs @@ -7,8 +7,10 @@ using System.Collections.Immutable; using System.Composition; using System.Diagnostics; +using System.Linq; using System.Threading; using System.Threading.Tasks; +using Microsoft.CodeAnalysis.CSharp.Extensions; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.Internal.Log; @@ -54,14 +56,15 @@ protected override async Task> GetItemsI if (cancellationToken.IsCancellationRequested) return []; - return GetMembersInTypes(document.Project.Solution, semanticModel.SyntaxTree, typesInFile, cancellationToken); + return GetMembersInTypes(document.Project.Solution, semanticModel, typesInFile, cancellationToken); } private static ImmutableArray GetMembersInTypes( - Solution solution, SyntaxTree tree, HashSet types, CancellationToken cancellationToken) + Solution solution, SemanticModel semanticModel, HashSet types, CancellationToken cancellationToken) { using (Logger.LogBlock(FunctionId.NavigationBar_ItemService_GetMembersInTypes_CSharp, cancellationToken)) { + var tree = semanticModel.SyntaxTree; using var _1 = ArrayBuilder.GetInstance(out var items); foreach (var type in types) @@ -79,29 +82,29 @@ private static ImmutableArray GetMembersInTypes( if (member is IMethodSymbol { PartialImplementationPart: { } } methodSymbol) { - memberItems.AddIfNotNull(CreateItemForMember(solution, methodSymbol, tree, cancellationToken)); - memberItems.AddIfNotNull(CreateItemForMember(solution, methodSymbol.PartialImplementationPart, tree, cancellationToken)); + memberItems.AddIfNotNull(CreateItemForMember(solution, methodSymbol, semanticModel, cancellationToken)); + memberItems.AddIfNotNull(CreateItemForMember(solution, methodSymbol.PartialImplementationPart, semanticModel, cancellationToken)); } else if (member is IPropertySymbol { PartialImplementationPart: { } } propertySymbol) { - memberItems.AddIfNotNull(CreateItemForMember(solution, propertySymbol, tree, cancellationToken)); - memberItems.AddIfNotNull(CreateItemForMember(solution, propertySymbol.PartialImplementationPart, tree, cancellationToken)); + memberItems.AddIfNotNull(CreateItemForMember(solution, propertySymbol, semanticModel, cancellationToken)); + memberItems.AddIfNotNull(CreateItemForMember(solution, propertySymbol.PartialImplementationPart, semanticModel, cancellationToken)); } else if (member is IEventSymbol { PartialImplementationPart: { } } eventSymbol) { - memberItems.AddIfNotNull(CreateItemForMember(solution, eventSymbol, tree, cancellationToken)); - memberItems.AddIfNotNull(CreateItemForMember(solution, eventSymbol.PartialImplementationPart, tree, cancellationToken)); + memberItems.AddIfNotNull(CreateItemForMember(solution, eventSymbol, semanticModel, cancellationToken)); + memberItems.AddIfNotNull(CreateItemForMember(solution, eventSymbol.PartialImplementationPart, semanticModel, cancellationToken)); } else if (member is IMethodSymbol or IPropertySymbol or IEventSymbol) { Debug.Assert(member is IMethodSymbol { PartialDefinitionPart: null } or IPropertySymbol { PartialDefinitionPart: null } or IEventSymbol { PartialDefinitionPart: null }, $"NavBar expected GetMembers to return partial method/property/event definition parts but the implementation part was returned."); - memberItems.AddIfNotNull(CreateItemForMember(solution, member, tree, cancellationToken)); + memberItems.AddIfNotNull(CreateItemForMember(solution, member, semanticModel, cancellationToken)); } else { - memberItems.AddIfNotNull(CreateItemForMember(solution, member, tree, cancellationToken)); + memberItems.AddIfNotNull(CreateItemForMember(solution, member, semanticModel, cancellationToken)); } } @@ -166,6 +169,7 @@ StatementSyntax or { BaseTypeDeclarationSyntax t => semanticModel.GetDeclaredSymbol(t, cancellationToken), DelegateDeclarationSyntax d => semanticModel.GetDeclaredSymbol(d, cancellationToken), + CompilationUnitSyntax c => c.IsTopLevelProgram() ? semanticModel.GetDeclaredSymbol(c, cancellationToken)?.ContainingType : null, _ => null, }; @@ -182,18 +186,67 @@ private static bool IsAccessor(ISymbol member) } private static SymbolItem? CreateItemForMember( - Solution solution, ISymbol member, SyntaxTree tree, CancellationToken cancellationToken) + Solution solution, ISymbol member, SemanticModel semanticModel, CancellationToken cancellationToken) { - var location = GetSymbolLocation(solution, member, tree, cancellationToken); + var location = GetSymbolLocation(solution, member, semanticModel.SyntaxTree, cancellationToken); if (location == null) return null; + using var _ = ArrayBuilder.GetInstance(out var localFunctionItems); + foreach (var syntaxReference in member.DeclaringSyntaxReferences) + { + if (syntaxReference.SyntaxTree != semanticModel.SyntaxTree) + { + // The reference is not in this file, no need to include in the outline view. + continue; + } + + var referenceNode = syntaxReference.GetSyntax(cancellationToken); + localFunctionItems.AddRange(CreateLocalFunctionMembers(solution, referenceNode, semanticModel, cancellationToken)); + } + return new SymbolItem( member.ToDisplayString(s_memberNameFormat), member.ToDisplayString(s_memberDetailsFormat), member.GetGlyph(), member.IsObsolete(), - location.Value); + location.Value, + localFunctionItems.ToImmutable()); + + static ImmutableArray CreateLocalFunctionMembers( + Solution solution, SyntaxNode node, SemanticModel semanticModel, CancellationToken cancellationToken) + { + // Get only the local functions that are direct descendents of this method. + var localFunctions = node.DescendantNodes(descendIntoChildren: (n) => + // Always descend from the original node even if its a local function, but do not descend further into descendent local functions. + n == node || n is not LocalFunctionStatementSyntax).Where(n => n is LocalFunctionStatementSyntax); + using var _ = ArrayBuilder.GetInstance(out var items); + foreach (var localFunction in localFunctions) + { + var localFunctionSymbol = semanticModel.GetDeclaredSymbol(localFunction, cancellationToken); + if (localFunctionSymbol == null) + continue; + + var location = GetSymbolLocation(solution, localFunctionSymbol, semanticModel.SyntaxTree, cancellationToken); + if (location == null) + continue; + + // Check the child local functions to see if they have nested local functions. + var childItems = CreateLocalFunctionMembers(solution, localFunction, semanticModel, cancellationToken); + + var symbolItem = new SymbolItem( + localFunctionSymbol.ToDisplayString(s_memberNameFormat), + localFunctionSymbol.ToDisplayString(s_memberDetailsFormat), + localFunctionSymbol.GetGlyph(), + localFunctionSymbol.IsObsolete(), + location.Value, + childItems); + + items.Add(symbolItem); + } + + return items.ToImmutable(); + } } private static SymbolItemLocation? GetSymbolLocation( diff --git a/src/LanguageServer/ProtocolUnitTests/Symbols/DocumentSymbolsTests.cs b/src/LanguageServer/ProtocolUnitTests/Symbols/DocumentSymbolsTests.cs index 82a5ddc034804..c112b58ccc7b5 100644 --- a/src/LanguageServer/ProtocolUnitTests/Symbols/DocumentSymbolsTests.cs +++ b/src/LanguageServer/ProtocolUnitTests/Symbols/DocumentSymbolsTests.cs @@ -175,6 +175,49 @@ public async Task TestGetDocumentSymbolsAsync__NoSymbols(bool mutatingLspWorkspa Assert.Empty(results); } + [Theory, CombinatorialData] + public async Task TestGetDocumentSymbolsAsync_LocalFunction(bool mutatingLspWorkspace) + { + var markup = + """ + namespace Test; + {|class:class {|classSelection:A|} + { + {|method:void {|methodSelection:M|}() + { + {|localFunction:void {|localFunctionSelection:LocalFunction|}() + { + }|} + }|} + }|} + """; + var clientCapabilities = new LSP.ClientCapabilities() + { + TextDocument = new LSP.TextDocumentClientCapabilities() + { + DocumentSymbol = new LSP.DocumentSymbolSetting() + { + HierarchicalDocumentSymbolSupport = true + } + } + }; + + await using var testLspServer = await CreateTestLspServerAsync(markup, mutatingLspWorkspace, clientCapabilities); + var classSymbol = CreateDocumentSymbol(LSP.SymbolKind.Class, "A", "Test.A", testLspServer.GetLocations("class").Single(), testLspServer.GetLocations("classSelection").Single()); + var methodSymbol = CreateDocumentSymbol(LSP.SymbolKind.Method, "M", "M()", testLspServer.GetLocations("method").Single(), testLspServer.GetLocations("methodSelection").Single(), classSymbol); + var localFunctionSymbol = CreateDocumentSymbol(LSP.SymbolKind.Method, "LocalFunction", "LocalFunction()", testLspServer.GetLocations("localFunction").Single(), testLspServer.GetLocations("localFunctionSelection").Single(), methodSymbol); + + LSP.DocumentSymbol[] expected = [classSymbol]; + + var results = await RunGetDocumentSymbolsAsync(testLspServer); + Assert.NotNull(results); + Assert.Equal(expected.Length, results.Length); + for (var i = 0; i < results.Length; i++) + { + AssertDocumentSymbolEquals(expected[i], results[i]); + } + } + private static async Task RunGetDocumentSymbolsAsync(TestLspServer testLspServer) { var document = testLspServer.GetCurrentSolution().Projects.First().Documents.First();