diff --git a/src/EditorFeatures/Test2/CodeDefinitionWindow/CrossLanguageCodeDefinitionWindowTests.vb b/src/EditorFeatures/Test2/CodeDefinitionWindow/CrossLanguageCodeDefinitionWindowTests.vb index 84c3fa5602650..e38537ca2c832 100644 --- a/src/EditorFeatures/Test2/CodeDefinitionWindow/CrossLanguageCodeDefinitionWindowTests.vb +++ b/src/EditorFeatures/Test2/CodeDefinitionWindow/CrossLanguageCodeDefinitionWindowTests.vb @@ -89,6 +89,10 @@ Namespace Microsoft.CodeAnalysis.Editor.CodeDefinitionWindow.UnitTests Public Function GetNavigableItemsAsync(document As Document, position As Integer, cancellationToken As CancellationToken) As Task(Of ImmutableArray(Of INavigableItem)) Implements INavigableItemsService.GetNavigableItemsAsync Return Task.FromResult(ImmutableArray.Create(Of INavigableItem)(New FakeNavigableItem(document))) End Function + + Public Function GetNavigableItemsAsync(document As Document, position As Integer, forSymbolType As Boolean, cancellationToken As CancellationToken) As Task(Of ImmutableArray(Of INavigableItem)) Implements INavigableItemsService.GetNavigableItemsAsync + Return Task.FromResult(ImmutableArray.Create(Of INavigableItem)(New FakeNavigableItem(document))) + End Function End Class diff --git a/src/Features/Core/Portable/Navigation/AbstractNavigableItemsService.cs b/src/Features/Core/Portable/Navigation/AbstractNavigableItemsService.cs index 11c66bdf46e6b..9e373be3ee6ec 100644 --- a/src/Features/Core/Portable/Navigation/AbstractNavigableItemsService.cs +++ b/src/Features/Core/Portable/Navigation/AbstractNavigableItemsService.cs @@ -14,8 +14,14 @@ namespace Microsoft.CodeAnalysis.Navigation; internal abstract class AbstractNavigableItemsService : INavigableItemsService { - public async Task> GetNavigableItemsAsync( + public Task> GetNavigableItemsAsync( Document document, int position, CancellationToken cancellationToken) + { + return GetNavigableItemsAsync(document, position, forSymbolType: false, cancellationToken); + } + + public async Task> GetNavigableItemsAsync( + Document document, int position, bool forSymbolType, CancellationToken cancellationToken) { var symbolService = document.GetRequiredLanguageService(); @@ -46,6 +52,22 @@ await GetSymbolAsync(document.WithFrozenPartialSemantics(cancellationToken)).Con if (symbol is null or IErrorTypeSymbol) return null; + if (forSymbolType) + { + // We have found the symbol at the position in the document. Now we need to find the symbol's type. + var typeSymbol = symbol.GetSymbolType() as ISymbol; + if (typeSymbol is null) + return null; + + typeSymbol = await SymbolFinder.FindSourceDefinitionAsync(typeSymbol, solution, cancellationToken).ConfigureAwait(false) ?? typeSymbol; + typeSymbol = await GoToDefinitionFeatureHelpers.TryGetPreferredSymbolAsync(solution, typeSymbol, cancellationToken).ConfigureAwait(false); + + if (typeSymbol is null or IErrorTypeSymbol) + return null; + + symbol = typeSymbol; + } + return (symbol, solution); } } diff --git a/src/Features/Core/Portable/Navigation/INavigableItemsService.cs b/src/Features/Core/Portable/Navigation/INavigableItemsService.cs index 6fef80259e280..d792ee79e48ec 100644 --- a/src/Features/Core/Portable/Navigation/INavigableItemsService.cs +++ b/src/Features/Core/Portable/Navigation/INavigableItemsService.cs @@ -10,10 +10,10 @@ namespace Microsoft.CodeAnalysis.Navigation; /// -/// Service used for features that want to find all the locations to potentially navigate to for a symbol at a -/// particular location, with enough information provided to display those locations in a rich fashion. Differs from -/// in that this can show a rich display of the items, not just navigate to -/// them. +/// Service used for features that want to find all the locations to potentially navigate to for a symbol or its type +/// at a particular location, with enough information provided to display those locations in a rich fashion. Differs +/// from in that this can show a rich display of the items, not just navigate +/// to them. /// internal interface INavigableItemsService : ILanguageService { @@ -21,4 +21,9 @@ internal interface INavigableItemsService : ILanguageService /// Finds the definitions for the symbol at the specific position in the document. /// Task> GetNavigableItemsAsync(Document document, int position, CancellationToken cancellationToken); + + /// + /// Finds the definitions for the symbol or its type at the specific position in the document. + /// + Task> GetNavigableItemsAsync(Document document, int position, bool forSymbolType, CancellationToken cancellationToken); } diff --git a/src/LanguageServer/Protocol/DefaultCapabilitiesProvider.cs b/src/LanguageServer/Protocol/DefaultCapabilitiesProvider.cs index 2dffadeb45af7..ae55d4bce2eff 100644 --- a/src/LanguageServer/Protocol/DefaultCapabilitiesProvider.cs +++ b/src/LanguageServer/Protocol/DefaultCapabilitiesProvider.cs @@ -53,6 +53,7 @@ public ServerCapabilities GetCapabilities(ClientCapabilities clientCapabilities) lz => CommonCompletionUtilities.GetTriggerCharacters(lz.Value)).Distinct().Select(c => c.ToString()).ToArray(); capabilities.DefinitionProvider = true; + capabilities.TypeDefinitionProvider = true; capabilities.DocumentHighlightProvider = true; capabilities.RenameProvider = new RenameOptions { diff --git a/src/LanguageServer/Protocol/Handler/Definitions/AbstractGoToDefinitionHandler.cs b/src/LanguageServer/Protocol/Handler/Definitions/AbstractGoToDefinitionHandler.cs index 2ff02c450461d..1e20de1c0703c 100644 --- a/src/LanguageServer/Protocol/Handler/Definitions/AbstractGoToDefinitionHandler.cs +++ b/src/LanguageServer/Protocol/Handler/Definitions/AbstractGoToDefinitionHandler.cs @@ -35,7 +35,7 @@ public AbstractGoToDefinitionHandler(IMetadataAsSourceFileService metadataAsSour public abstract Task HandleRequestAsync(TextDocumentPositionParams request, RequestContext context, CancellationToken cancellationToken); - protected Task GetDefinitionAsync(LSP.TextDocumentPositionParams request, bool typeOnly, RequestContext context, CancellationToken cancellationToken) + protected Task GetDefinitionAsync(LSP.TextDocumentPositionParams request, bool forSymbolType, RequestContext context, CancellationToken cancellationToken) { var workspace = context.Workspace; var document = context.Document; @@ -44,10 +44,10 @@ public AbstractGoToDefinitionHandler(IMetadataAsSourceFileService metadataAsSour var linePosition = ProtocolConversions.PositionToLinePosition(request.Position); - return GetDefinitionsAsync(_globalOptions, _metadataAsSourceFileService, workspace, document, typeOnly, linePosition, cancellationToken); + return GetDefinitionsAsync(_globalOptions, _metadataAsSourceFileService, workspace, document, forSymbolType, linePosition, cancellationToken); } - internal static async Task GetDefinitionsAsync(IGlobalOptionService globalOptions, IMetadataAsSourceFileService? metadataAsSourceFileService, Workspace workspace, Document document, bool typeOnly, LinePosition linePosition, CancellationToken cancellationToken) + internal static async Task GetDefinitionsAsync(IGlobalOptionService globalOptions, IMetadataAsSourceFileService? metadataAsSourceFileService, Workspace workspace, Document document, bool forSymbolType, LinePosition linePosition, CancellationToken cancellationToken) { var locations = ArrayBuilder.GetInstance(); var position = await document.GetPositionFromLinePositionAsync(linePosition, cancellationToken).ConfigureAwait(false); @@ -56,12 +56,12 @@ public AbstractGoToDefinitionHandler(IMetadataAsSourceFileService metadataAsSour if (service is null) return null; - var definitions = await service.GetNavigableItemsAsync(document, position, cancellationToken).ConfigureAwait(false); + var definitions = await service.GetNavigableItemsAsync(document, position, forSymbolType, cancellationToken).ConfigureAwait(false); if (definitions.Length > 0) { foreach (var definition in definitions) { - if (!ShouldInclude(definition, typeOnly)) + if (!ShouldInclude(definition, forSymbolType)) continue; var location = await ProtocolConversions.TextSpanToLocationAsync( @@ -76,27 +76,27 @@ await definition.Document.GetRequiredDocumentAsync(document.Project.Solution, ca { // No definition found - see if we can get metadata as source but that's only applicable for C#\VB. var symbol = await SymbolFinder.FindSymbolAtPositionAsync(document, position, cancellationToken).ConfigureAwait(false); + if (forSymbolType) + symbol = symbol?.GetSymbolType(); + if (symbol != null && metadataAsSourceFileService.IsNavigableMetadataSymbol(symbol)) { - if (!typeOnly || symbol is ITypeSymbol) + var options = globalOptions.GetMetadataAsSourceOptions(); + var declarationFile = await metadataAsSourceFileService.GetGeneratedFileAsync(workspace, document.Project, symbol, signaturesOnly: false, options: options, cancellationToken: cancellationToken).ConfigureAwait(false); + + var linePosSpan = declarationFile.IdentifierLocation.GetLineSpan().Span; + locations.Add(new LSP.Location { - var options = globalOptions.GetMetadataAsSourceOptions(); - var declarationFile = await metadataAsSourceFileService.GetGeneratedFileAsync(workspace, document.Project, symbol, signaturesOnly: false, options: options, cancellationToken: cancellationToken).ConfigureAwait(false); - - var linePosSpan = declarationFile.IdentifierLocation.GetLineSpan().Span; - locations.Add(new LSP.Location - { - Uri = ProtocolConversions.CreateAbsoluteUri(declarationFile.FilePath), - Range = ProtocolConversions.LinePositionToRange(linePosSpan), - }); - } + Uri = ProtocolConversions.CreateAbsoluteUri(declarationFile.FilePath), + Range = ProtocolConversions.LinePositionToRange(linePosSpan), + }); } } return locations.ToArrayAndFree(); // local functions - static bool ShouldInclude(INavigableItem item, bool typeOnly) + static bool ShouldInclude(INavigableItem item, bool forSymbolType) { if (item.Glyph is Glyph.Namespace) { @@ -104,7 +104,7 @@ static bool ShouldInclude(INavigableItem item, bool typeOnly) return false; } - if (!typeOnly) + if (!forSymbolType) { return true; } diff --git a/src/LanguageServer/Protocol/Handler/Definitions/GoToDefinitionHandler.cs b/src/LanguageServer/Protocol/Handler/Definitions/GoToDefinitionHandler.cs index b25d426dd96ff..a8396ff8fdd5b 100644 --- a/src/LanguageServer/Protocol/Handler/Definitions/GoToDefinitionHandler.cs +++ b/src/LanguageServer/Protocol/Handler/Definitions/GoToDefinitionHandler.cs @@ -25,6 +25,6 @@ public GoToDefinitionHandler(IMetadataAsSourceFileService metadataAsSourceFileSe } public override Task HandleRequestAsync(LSP.TextDocumentPositionParams request, RequestContext context, CancellationToken cancellationToken) - => GetDefinitionAsync(request, typeOnly: false, context, cancellationToken); + => GetDefinitionAsync(request, forSymbolType: false, context, cancellationToken); } } diff --git a/src/LanguageServer/Protocol/Handler/Definitions/GoToTypeDefinitionHandler.cs b/src/LanguageServer/Protocol/Handler/Definitions/GoToTypeDefinitionHandler.cs index 969b512be3f27..11c23a9751d9c 100644 --- a/src/LanguageServer/Protocol/Handler/Definitions/GoToTypeDefinitionHandler.cs +++ b/src/LanguageServer/Protocol/Handler/Definitions/GoToTypeDefinitionHandler.cs @@ -25,6 +25,6 @@ public GoToTypeDefinitionHandler(IMetadataAsSourceFileService metadataAsSourceFi } public override Task HandleRequestAsync(LSP.TextDocumentPositionParams request, RequestContext context, CancellationToken cancellationToken) - => GetDefinitionAsync(request, typeOnly: true, context, cancellationToken); + => GetDefinitionAsync(request, forSymbolType: true, context, cancellationToken); } } diff --git a/src/LanguageServer/ProtocolUnitTests/Definitions/GoToTypeDefinitionTests.cs b/src/LanguageServer/ProtocolUnitTests/Definitions/GoToTypeDefinitionTests.cs index a4a1aa043ee23..a18b449d3cd47 100644 --- a/src/LanguageServer/ProtocolUnitTests/Definitions/GoToTypeDefinitionTests.cs +++ b/src/LanguageServer/ProtocolUnitTests/Definitions/GoToTypeDefinitionTests.cs @@ -4,10 +4,12 @@ #nullable disable +using System; using System.Linq; using System.Threading; using System.Threading.Tasks; using Roslyn.Test.Utilities; +using Roslyn.Test.Utilities.TestGenerators; using Xunit; using Xunit.Abstractions; using LSP = Roslyn.LanguageServer.Protocol; @@ -21,7 +23,7 @@ public GoToTypeDefinitionTests(ITestOutputHelper testOutputHelper) : base(testOu } [Theory, CombinatorialData] - public async Task TestGotoTypeDefinitionAsync(bool mutatingLspWorkspace) + public async Task TestGotoTypeDefinitionAsync_WithTypeSymbol(bool mutatingLspWorkspace) { var markup = @"class {|definition:A|} @@ -37,6 +39,79 @@ class B AssertLocationsEqual(testLspServer.GetLocations("definition"), results); } + [Theory, CombinatorialData] + public async Task TestGotoTypeDefinitionAsync_WithPropertySymbol(bool mutatingLspWorkspace) + { + var markup = +@"class {|definition:A|} +{ +} +class B +{ + A class{|caret:|}A {; +}"; + await using var testLspServer = await CreateTestLspServerAsync(markup, mutatingLspWorkspace); + + var results = await RunGotoTypeDefinitionAsync(testLspServer, testLspServer.GetLocations("caret").Single()); + AssertLocationsEqual(testLspServer.GetLocations("definition"), results); + } + + [Theory, CombinatorialData] + public async Task TestGotoTypeDefinitionAsync_WithFieldSymbol(bool mutatingLspWorkspace) + { + var markup = +@"class {|definition:A|} +{ +} +class B +{ + A class{|caret:|}A; +}"; + await using var testLspServer = await CreateTestLspServerAsync(markup, mutatingLspWorkspace); + + var results = await RunGotoTypeDefinitionAsync(testLspServer, testLspServer.GetLocations("caret").Single()); + AssertLocationsEqual(testLspServer.GetLocations("definition"), results); + } + + [Theory, CombinatorialData] + public async Task TestGotoTypeDefinitionAsync_WithLocalSymbol(bool mutatingLspWorkspace) + { + var markup = +@"class {|definition:A|} +{ +} +class B +{ + void Method() + { + var class{|caret:|}A = new A(); + } +}"; + await using var testLspServer = await CreateTestLspServerAsync(markup, mutatingLspWorkspace); + + var results = await RunGotoTypeDefinitionAsync(testLspServer, testLspServer.GetLocations("caret").Single()); + AssertLocationsEqual(testLspServer.GetLocations("definition"), results); + } + + [Theory, CombinatorialData] + public async Task TestGotoTypeDefinitionAsync_WithParameterSymbol(bool mutatingLspWorkspace) + { + var markup = +@"class {|definition:A|} +{ +} +class B +{ + void Method(A class{|caret:|}A) + { + } +}"; + await using var testLspServer = await CreateTestLspServerAsync(markup, mutatingLspWorkspace); + + var results = await RunGotoTypeDefinitionAsync(testLspServer, testLspServer.GetLocations("caret").Single()); + AssertLocationsEqual(testLspServer.GetLocations("definition"), results); + } + [Theory, CombinatorialData] public async Task TestGotoTypeDefinitionAsync_DifferentDocument(bool mutatingLspWorkspace) { @@ -52,7 +127,7 @@ class {|definition:A|} { class B { - {|caret:|}A classA; + A class{|caret:|}A; } }" }; @@ -81,10 +156,133 @@ class B Assert.Empty(results); } + [Theory, CombinatorialData] + public async Task TestGotoTypeDefinitionAsync_MappedFile(bool mutatingLspWorkspace) + { + var source = + """ + namespace M + { + class A + { + public B b{|caret:|}; + } + } + """; + var mapped = + """ + namespace M + { + class B + { + } + } + """; + + await using var testLspServer = await CreateTestLspServerAsync(source, mutatingLspWorkspace); + + AddMappedDocument(testLspServer.TestWorkspace, mapped); + + var results = await RunGotoTypeDefinitionAsync(testLspServer, testLspServer.GetLocations("caret").Single()); + var result = Assert.Single(results); + AssertLocationsEqual([TestSpanMapper.MappedFileLocation], results); + } + + [Theory, CombinatorialData] + public async Task TestGotoTypeDefinitionAsync_SourceGeneratedDocument(bool mutatingLspWorkspace) + { + var source = + """ + namespace M + { + class A + { + public B b{|caret:|}; + } + } + """; + var generated = + """ + namespace M + { + class B + { + } + } + """; + + await using var testLspServer = await CreateTestLspServerAsync(source, mutatingLspWorkspace); + await AddGeneratorAsync(new SingleFileTestGenerator(generated), testLspServer.TestWorkspace); + + var results = await RunGotoTypeDefinitionAsync(testLspServer, testLspServer.GetLocations("caret").Single()); + var result = Assert.Single(results); + Assert.Equal(SourceGeneratedDocumentUri.Scheme, result.Uri.Scheme); + } + + [Theory, CombinatorialData] + public async Task TestGotoTypeDefinitionAsync_MetadataAsSource(bool mutatingLspWorkspace) + { + var source = + """ + using System; + class A + { + void Rethrow(NotImplementedException exception) + { + throw {|caret:exception|}; + } + } + """; + + // Create a server with LSP misc file workspace and metadata service. + await using var testLspServer = await CreateTestLspServerAsync(source, mutatingLspWorkspace, new InitializationOptions { ServerKind = WellKnownLspServerKinds.CSharpVisualBasicLspServer }); + + // Get the metadata definition. + var results = await RunGotoTypeDefinitionAsync(testLspServer, testLspServer.GetLocations("caret").Single()); + + // Open the metadata file and verify it gets added to the metadata workspace. + await testLspServer.OpenDocumentAsync(results.Single().Uri, text: string.Empty).ConfigureAwait(false); + + Assert.Equal(WorkspaceKind.MetadataAsSource, (await GetWorkspaceForDocument(testLspServer, results.Single().Uri)).Kind); + } + + [Theory, CombinatorialData] + public async Task TestGotoTypeDefinitionAsync_CrossLanguage(bool mutatingLspWorkspace) + { + var markup = +@" + + + public class {|definition:A|} + { + } + + + + Definition + + Class C + Dim {|caret:a|} As A + End Class + + +"; + await using var testLspServer = await CreateXmlTestLspServerAsync(markup, mutatingLspWorkspace); + + var results = await RunGotoTypeDefinitionAsync(testLspServer, testLspServer.GetLocations("caret").Single()); + AssertLocationsEqual(testLspServer.GetLocations("definition"), results); + } + private static async Task RunGotoTypeDefinitionAsync(TestLspServer testLspServer, LSP.Location caret) { return await testLspServer.ExecuteRequestAsync(LSP.Methods.TextDocumentTypeDefinitionName, CreateTextDocumentPositionParams(caret), CancellationToken.None); } + + private static async Task GetWorkspaceForDocument(TestLspServer testLspServer, Uri fileUri) + { + var (lspWorkspace, _, _) = await testLspServer.GetManager().GetLspDocumentInfoAsync(new LSP.TextDocumentIdentifier { Uri = fileUri }, CancellationToken.None); + return lspWorkspace!; + } } } diff --git a/src/VisualStudio/ExternalAccess/FSharp/Internal/Navigation/FSharpFindDefinitionService.cs b/src/VisualStudio/ExternalAccess/FSharp/Internal/Navigation/FSharpFindDefinitionService.cs index 7c38d20db56b3..d95506b9b278f 100644 --- a/src/VisualStudio/ExternalAccess/FSharp/Internal/Navigation/FSharpFindDefinitionService.cs +++ b/src/VisualStudio/ExternalAccess/FSharp/Internal/Navigation/FSharpFindDefinitionService.cs @@ -23,4 +23,9 @@ public async Task> GetNavigableItemsAsync(Documen var items = await service.FindDefinitionsAsync(document, position, cancellationToken).ConfigureAwait(false); return items.SelectAsArray(x => (INavigableItem)new InternalFSharpNavigableItem(x)); } + + public Task> GetNavigableItemsAsync(Document document, int position, bool forSymbolType, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } }