Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
182 changes: 182 additions & 0 deletions src/EditorFeatures/Test2/NavigationBar/CSharpNavigationBarTests.vb
Original file line number Diff line number Diff line change
Expand Up @@ -390,5 +390,187 @@ static class C
Item("C.extension(string)", Glyph.ClassPublic), False,
Item("Goo()", Glyph.ExtensionMethodPublic), False)
End Function

<Theory, CombinatorialData, WorkItem("https://github.com/dotnet/vscode-csharp/issues/6767")>
Public Async Function TestLocalFunction(host As TestHost) As Task
Await AssertItemsAreAsync(
<Workspace>
<Project Language="C#" CommonReferences="true">
<Document>
class C { void M() { void Local() { } } }
</Document>
</Project>
</Workspace>,
host,
Item("C", Glyph.ClassInternal, children:={
Item("M()", Glyph.MethodPrivate, children:={
Item("Local()", Glyph.MethodPrivate)})}))
End Function

<Theory, CombinatorialData, WorkItem("https://github.com/dotnet/vscode-csharp/issues/6767")>
Public Async Function TestNestedLocalFunction(host As TestHost) As Task
Await AssertItemsAreAsync(
<Workspace>
<Project Language="C#" CommonReferences="true">
<Document>
class C { void M() { void Local() { void NestedLocal() { } } } }
</Document>
</Project>
</Workspace>,
host,
Item("C", Glyph.ClassInternal, children:={
Item("M()", Glyph.MethodPrivate, children:={
Item("Local()", Glyph.MethodPrivate, children:={
Item("NestedLocal()", Glyph.MethodPrivate)})})}))
End Function

<Theory, CombinatorialData, WorkItem("https://github.com/dotnet/vscode-csharp/issues/6767")>
Public Async Function TestMultipleLocalFunction(host As TestHost) As Task
Await AssertItemsAreAsync(
<Workspace>
<Project Language="C#" CommonReferences="true">
<Document>
class C { void M() { void Local1() { } void Local2() { } } }
</Document>
</Project>
</Workspace>,
host,
Item("C", Glyph.ClassInternal, children:={
Item("M()", Glyph.MethodPrivate, children:={
Item("Local1()", Glyph.MethodPrivate),
Item("Local2()", Glyph.MethodPrivate)})}))
End Function

<Theory, CombinatorialData, WorkItem("https://github.com/dotnet/vscode-csharp/issues/6767")>
Public Async Function TestMultipleAndNestedLocalFunction(host As TestHost) As Task
Await AssertItemsAreAsync(
<Workspace>
<Project Language="C#" CommonReferences="true">
<Document>
class C
{
void M()
{
void Local()
{
void NestedLocal() { }
}
void Local2()
{
void NestedLocal2() { }
}
}
}
</Document>
</Project>
</Workspace>,
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

<Theory, CombinatorialData, WorkItem("https://github.com/dotnet/vscode-csharp/issues/6767")>
Public Async Function TestTopLevelProgram(host As TestHost) As Task
Await AssertItemsAreAsync(
<Workspace>
<Project Language="C#" CommonReferences="true">
<Document>
using System;
Console.WriteLine("Hello World!");

void Method() { }
</Document>
</Project>
</Workspace>,
host,
Item("Program", Glyph.ClassInternal, children:={
Item("<top-level-statements-entry-point>", Glyph.MethodPrivate, children:={
Item("Method()", Glyph.MethodPrivate)})}))
End Function

<Theory, CombinatorialData, WorkItem("https://github.com/dotnet/vscode-csharp/issues/6767")>
Public Async Function TestLocalFunctionInProperty(host As TestHost) As Task
Await AssertItemsAreAsync(
<Workspace>
<Project Language="C#" CommonReferences="true">
<Document>
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;
}
}
}
}
</Document>
</Project>
</Workspace>,
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

<Theory, CombinatorialData, WorkItem("https://github.com/dotnet/vscode-csharp/issues/6767")>
Public Async Function TestNestedLocalFunctionInProperty(host As TestHost) As Task
Await AssertItemsAreAsync(
<Workspace>
<Project Language="C#" CommonReferences="true">
<Document>
class C
{
private string _field = string.Empty;
public string Prop
{
get
{
return _field;
}
set
{
_field = value;
void Local()
{
void NestedLocal() { }
}
}
}
}
</Document>
</Project>
</Workspace>,
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
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -54,14 +56,15 @@ protected override async Task<ImmutableArray<RoslynNavigationBarItem>> 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<RoslynNavigationBarItem> GetMembersInTypes(
Solution solution, SyntaxTree tree, HashSet<INamedTypeSymbol> types, CancellationToken cancellationToken)
Solution solution, SemanticModel semanticModel, HashSet<INamedTypeSymbol> types, CancellationToken cancellationToken)
{
using (Logger.LogBlock(FunctionId.NavigationBar_ItemService_GetMembersInTypes_CSharp, cancellationToken))
{
var tree = semanticModel.SyntaxTree;
using var _1 = ArrayBuilder<RoslynNavigationBarItem>.GetInstance(out var items);

foreach (var type in types)
Expand All @@ -79,29 +82,29 @@ private static ImmutableArray<RoslynNavigationBarItem> 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));
}
}

Expand Down Expand Up @@ -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,
};

Expand All @@ -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<RoslynNavigationBarItem>.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<RoslynNavigationBarItem> 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<RoslynNavigationBarItem>.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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<LSP.DocumentSymbol[]>(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<TReturn?> RunGetDocumentSymbolsAsync<TReturn>(TestLspServer testLspServer)
{
var document = testLspServer.GetCurrentSolution().Projects.First().Documents.First();
Expand Down
Loading