diff --git a/src/EditorFeatures/Core/Peek/PeekableItemFactory.cs b/src/EditorFeatures/Core/Peek/PeekableItemFactory.cs index f5bd137394204..067ad6ed03df6 100644 --- a/src/EditorFeatures/Core/Peek/PeekableItemFactory.cs +++ b/src/EditorFeatures/Core/Peek/PeekableItemFactory.cs @@ -19,6 +19,7 @@ using Microsoft.CodeAnalysis.Navigation; using Microsoft.CodeAnalysis.Options; using Microsoft.CodeAnalysis.PooledObjects; +using Microsoft.CodeAnalysis.Text; using Microsoft.VisualStudio.Language.Intellisense; namespace Microsoft.CodeAnalysis.Editor.Implementation.Peek; @@ -72,6 +73,7 @@ public async Task> GetPeekableItemsAsync( var symbolNavigationService = solution.Services.GetService(); var result = await symbolNavigationService.GetExternalNavigationSymbolLocationAsync(definitionItem, cancellationToken).ConfigureAwait(false); + result ??= await GetCrossLanguageFileLocationAsync(solution, symbol, cancellationToken).ConfigureAwait(false); using var _ = ArrayBuilder.GetInstance(out var results); if (result is var (filePath, linePosition)) @@ -95,4 +97,34 @@ public async Task> GetPeekableItemsAsync( return results.ToImmutableAndClear(); } + + /// + /// The source file of a symbol another .Net language owns, for example F#. It is metadata to us, and + /// metadata-as-source would show it decompiled, so that language is asked first - as navigating to the + /// symbol asks it in VisualStudioSymbolNavigationService. + /// + private async Task<(string filePath, LinePosition linePosition)?> GetCrossLanguageFileLocationAsync( + Solution solution, ISymbol symbol, CancellationToken cancellationToken) + { + if (symbol.Locations.Any(static location => location.IsInSource) || + !_metadataAsSourceFileService.IsNavigableMetadataSymbol(symbol)) + { + return null; + } + + var docCommentId = symbol.GetDocumentationCommentId(); + var assemblyName = symbol.ContainingAssembly.Identity.Name; + if (docCommentId == null || assemblyName == null) + return null; + + foreach (var lazyService in solution.Services.ExportProvider.GetExports()) + { + var location = await lazyService.Value.TryGetNavigableFileLocationAsync( + assemblyName, docCommentId, cancellationToken).ConfigureAwait(false); + if (location != null) + return location; + } + + return null; + } } diff --git a/src/EditorFeatures/Test2/Peek/PeekTests.vb b/src/EditorFeatures/Test2/Peek/PeekTests.vb index aa6ecfa142406..2ac7da9bb58de 100644 --- a/src/EditorFeatures/Test2/Peek/PeekTests.vb +++ b/src/EditorFeatures/Test2/Peek/PeekTests.vb @@ -2,11 +2,16 @@ ' The .NET Foundation licenses this file to you under the MIT license. ' See the LICENSE file in the project root for more information. +Imports System.Composition Imports System.IO Imports System.Threading Imports Microsoft.CodeAnalysis.Collections Imports Microsoft.CodeAnalysis.Editor.Implementation.Peek Imports Microsoft.CodeAnalysis.Editor.Shared.Utilities +Imports Microsoft.CodeAnalysis.FindUsages +Imports Microsoft.CodeAnalysis.Host.Mef +Imports Microsoft.CodeAnalysis.Navigation +Imports Microsoft.CodeAnalysis.Text Imports Microsoft.VisualStudio.Imaging.Interop Imports Microsoft.VisualStudio.Language.Intellisense Imports Microsoft.VisualStudio.Text @@ -286,8 +291,101 @@ public partial class D End Using End Sub - Private Shared Function CreateTestWorkspace(element As XElement) As EditorTestWorkspace - Return EditorTestWorkspace.Create(element, composition:=EditorTestCompositions.EditorFeatures) + + + new Box().$$Set(1);", "M:Box`1.Set(`0)")> + new Counter().$$Increment();", "M:Counter.Increment")> + Public Sub TestPeekDefinitionShowsTheFileAnotherLanguageOwnsForAMetadataSymbol(member As String, documentationCommentId As String) + Using workspace = CreateTestWorkspace(WorkspaceReferencingOtherLanguageLibrary(member), s_crossLanguageComposition) + Dim result = GetPeekResultCollection(workspace) + + Assert.Equal({$"{CrossLanguageSymbolNavigationService.OwnedAssemblyName}:{documentationCommentId}"}, GetCrossLanguageService(workspace).Requests) + Assert.Equal(1, result.Items.Count) + result.AssertShowsFile(index:=0, CrossLanguageSymbolNavigationService.FilePath, CrossLanguageSymbolNavigationService.Position) + End Using + End Sub + + + Public Sub TestPeekDefinitionShowsMetadataAsSourceWhenNoOtherLanguageOwnsTheSymbol() + Using workspace = CreateTestWorkspace( + + class C { string s = $$"Goo"; } + + , s_crossLanguageComposition) + Dim result = GetPeekResultCollection(workspace) + + Assert.EndsWith(":T:System.String", Assert.Single(GetCrossLanguageService(workspace).Requests)) + Assert.Equal(1, result.Items.Count) + Assert.Equal($"String [{FeaturesResources.Decompiled}]", result(0).DisplayInfo.Label) + End Using + End Sub + + + Public Sub TestPeekDefinitionDoesNotAskAnotherLanguageForASourceSymbol() + Using workspace = CreateTestWorkspace( + + public class {|Identifier:D|} { } class C { $$D d; } + + , s_crossLanguageComposition) + Dim result = GetPeekResultCollection(workspace) + + Assert.Empty(GetCrossLanguageService(workspace).Requests) + Assert.Equal(1, result.Items.Count) + result.AssertNavigatesToIdentifier(index:=0, name:="Identifier") + End Using + End Sub + + + Public Sub TestPeekDefinitionDoesNotAskAnotherLanguageForAMetadataSymbolItCannotShow() + Using workspace = CreateTestWorkspace( + + using $$System; class C { } + + , s_crossLanguageComposition) + Dim result = GetPeekResultCollection(workspace) + + Assert.Empty(GetCrossLanguageService(workspace).Requests) + Assert.Null(result) + End Using + End Sub + + + Public Sub TestPeekDefinitionPrefersTheExternalNavigationLocationToAnotherLanguage() + Using workspace = CreateTestWorkspace( + WorkspaceReferencingOtherLanguageLibrary("$$Counter c;"), + s_crossLanguageComposition.AddParts(GetType(ExternalNavigationSymbolNavigationService))) + Dim result = GetPeekResultCollection(workspace) + + Assert.Empty(GetCrossLanguageService(workspace).Requests) + Assert.Equal(1, result.Items.Count) + result.AssertShowsFile(index:=0, ExternalNavigationSymbolNavigationService.FilePath, ExternalNavigationSymbolNavigationService.Position) + End Using + End Sub + + Private Shared ReadOnly s_crossLanguageComposition As TestComposition = + EditorTestCompositions.EditorFeatures.AddParts(GetType(CrossLanguageSymbolNavigationService)) + + ''' + ''' A C# project referencing, as metadata, an assembly owns + ''' the source of, the way F# owns the source of the F# assemblies a C# project references. + ''' + Private Shared Function WorkspaceReferencingOtherLanguageLibrary(member As String) As XElement + Return + + CommonReferences="true"> + public class Counter { public void Increment() { } } public class Box<T> { public void Set(T value) { } } + + class C { <%= member %> } + + + End Function + + Private Shared Function GetCrossLanguageService(workspace As EditorTestWorkspace) As CrossLanguageSymbolNavigationService + Return DirectCast(workspace.ExportProvider.GetExportedValue(Of ICrossLanguageSymbolNavigationService)(), CrossLanguageSymbolNavigationService) + End Function + + Private Shared Function CreateTestWorkspace(element As XElement, Optional composition As TestComposition = Nothing) As EditorTestWorkspace + Return EditorTestWorkspace.Create(element, composition:=If(composition, EditorTestCompositions.EditorFeatures)) End Function Private Shared Function GetPeekResultCollection(element As XElement) As PeekResultCollection @@ -342,6 +440,62 @@ public partial class D Return peekResult End Function + + Private NotInheritable Class CrossLanguageSymbolNavigationService + Implements ICrossLanguageSymbolNavigationService + + Public Const OwnedAssemblyName = "OtherLanguageLibrary" + Public Shared ReadOnly FilePath As String = Path.Combine(TestWorkspace.RootDirectory, "Library.fs") + Public Shared ReadOnly Position As New LinePosition(2, 4) + + Public ReadOnly Property Requests As New List(Of String) + + + + Public Sub New() + End Sub + + Public Function TryGetNavigableLocationAsync(assemblyName As String, documentationCommentId As String, cancellationToken As CancellationToken) As Task(Of INavigableLocation) Implements ICrossLanguageSymbolNavigationService.TryGetNavigableLocationAsync + Throw New NotImplementedException() + End Function + + Public Function TryGetNavigableFileLocationAsync(assemblyName As String, documentationCommentId As String, cancellationToken As CancellationToken) As Task(Of (filePath As String, linePosition As LinePosition)?) Implements ICrossLanguageSymbolNavigationService.TryGetNavigableFileLocationAsync + Requests.Add($"{assemblyName}:{documentationCommentId}") + + Dim location As (filePath As String, linePosition As LinePosition)? = Nothing + If assemblyName = OwnedAssemblyName Then + location = (FilePath, Position) + End If + + Return Task.FromResult(location) + End Function + End Class + + + Private NotInheritable Class ExternalNavigationSymbolNavigationService + Implements ISymbolNavigationService + + Public Shared ReadOnly FilePath As String = Path.Combine(TestWorkspace.RootDirectory, "External.cs") + Public Shared ReadOnly Position As New LinePosition(5, 1) + + + + Public Sub New() + End Sub + + Public Function GetNavigableLocationAsync(symbol As ISymbol, project As Project, cancellationToken As CancellationToken) As Task(Of INavigableLocation) Implements ISymbolNavigationService.GetNavigableLocationAsync + Throw New NotImplementedException() + End Function + + Public Function TrySymbolNavigationNotifyAsync(symbol As ISymbol, project As Project, cancellationToken As CancellationToken) As Task(Of Boolean) Implements ISymbolNavigationService.TrySymbolNavigationNotifyAsync + Throw New NotImplementedException() + End Function + + Public Function GetExternalNavigationSymbolLocationAsync(definitionItem As DefinitionItem, cancellationToken As CancellationToken) As Task(Of (filePath As String, linePosition As LinePosition)?) Implements ISymbolNavigationService.GetExternalNavigationSymbolLocationAsync + Return Task.FromResult(Of (filePath As String, linePosition As LinePosition)?)((FilePath, Position)) + End Function + End Class + Private Class MockPeekResultFactory Implements IPeekResultFactory @@ -476,6 +630,16 @@ public partial class D Return buffer.CurrentSnapshot.GetText(line.Start + startIndex, line.Length - startIndex) End Function + Friend Sub AssertShowsFile(index As Integer, filePath As String, position As LinePosition) + Dim documentResult = DirectCast(Items(index), IDocumentPeekResult) + Assert.Equal(filePath, documentResult.FilePath) + + Dim startLine As Integer + Dim startIndex As Integer + Assert.True(documentResult.IdentifyingSpan.TryGetStartLineIndex(startLine, startIndex), "Unable to get span for the file.") + Assert.Equal(position, New LinePosition(startLine, startIndex)) + End Sub + Friend Sub AssertNavigatesToIdentifier(index As Integer, name As String) Dim documentResult = DirectCast(Items(index), IDocumentPeekResult) Dim document = _workspace.Documents.FirstOrDefault(Function(d) d.FilePath = documentResult.FilePath) diff --git a/src/Features/Core/Portable/Navigation/ICrossLanguageSymbolNavigationService.cs b/src/Features/Core/Portable/Navigation/ICrossLanguageSymbolNavigationService.cs index a975cfaec849d..f7435ddaef1e7 100644 --- a/src/Features/Core/Portable/Navigation/ICrossLanguageSymbolNavigationService.cs +++ b/src/Features/Core/Portable/Navigation/ICrossLanguageSymbolNavigationService.cs @@ -4,6 +4,7 @@ using System.Threading; using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Text; namespace Microsoft.CodeAnalysis.Navigation; @@ -22,4 +23,14 @@ internal interface ICrossLanguageSymbolNavigationService /// receiver to quickly filter down to the project/compilation search for the symbol. Task TryGetNavigableLocationAsync( string assemblyName, string documentationCommentId, CancellationToken cancellationToken); + + /// + /// Attempts to get the file and position of the source definition of a particular symbol id, for a feature + /// that shows that source in place rather than navigating to it, such as Peek Definition. Should return if the 3rd party language cannot provide a file for this particular symbol. + /// + /// The name of the assembly the symbol was defined in. Can be used by the + /// receiver to quickly filter down to the project/compilation search for the symbol. + Task<(string filePath, LinePosition linePosition)?> TryGetNavigableFileLocationAsync( + string assemblyName, string documentationCommentId, CancellationToken cancellationToken); } diff --git a/src/VisualStudio/ExternalAccess/Core/FSharp/Internal/Navigation/FSharpCrossLanguageSymbolNavigationService.cs b/src/VisualStudio/ExternalAccess/Core/FSharp/Internal/Navigation/FSharpCrossLanguageSymbolNavigationService.cs index dc065a7816a40..d5b7d35fda857 100644 --- a/src/VisualStudio/ExternalAccess/Core/FSharp/Internal/Navigation/FSharpCrossLanguageSymbolNavigationService.cs +++ b/src/VisualStudio/ExternalAccess/Core/FSharp/Internal/Navigation/FSharpCrossLanguageSymbolNavigationService.cs @@ -9,6 +9,7 @@ using Microsoft.CodeAnalysis.ExternalAccess.FSharp.Navigation; using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.Navigation; +using Microsoft.CodeAnalysis.Text; namespace Microsoft.CodeAnalysis.ExternalAccess.FSharp.Internal.Navigation; @@ -45,4 +46,15 @@ public FSharpCrossLanguageSymbolNavigationService( return new NavigableLocation((options, cancellationToken) => location.NavigateToAsync(new FSharpNavigationOptions2(options.PreferProvisionalTab, options.ActivateTab), cancellationToken)); } + + public async Task<(string filePath, LinePosition linePosition)?> TryGetNavigableFileLocationAsync( + string assemblyName, string documentationCommentId, CancellationToken cancellationToken) + { + // Only defer to an F# service that can name a file; one that can only navigate has nothing to show in place. + if (_underlyingService is not IFSharpCrossLanguageSymbolNavigationService2 fileLocationService) + return null; + + return await fileLocationService.TryGetNavigableFileLocationAsync( + assemblyName, documentationCommentId, cancellationToken).ConfigureAwait(false); + } } diff --git a/src/VisualStudio/ExternalAccess/Core/FSharp/Navigation/IFSharpCrossLanguageSymbolNavigationService.cs b/src/VisualStudio/ExternalAccess/Core/FSharp/Navigation/IFSharpCrossLanguageSymbolNavigationService.cs index 83a6b0dc604e6..9f865a70f626f 100644 --- a/src/VisualStudio/ExternalAccess/Core/FSharp/Navigation/IFSharpCrossLanguageSymbolNavigationService.cs +++ b/src/VisualStudio/ExternalAccess/Core/FSharp/Navigation/IFSharpCrossLanguageSymbolNavigationService.cs @@ -5,6 +5,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.Navigation; +using Microsoft.CodeAnalysis.Text; namespace Microsoft.CodeAnalysis.ExternalAccess.FSharp.Navigation; @@ -16,6 +17,18 @@ internal interface IFSharpCrossLanguageSymbolNavigationService string assemblyName, string documentationCommentId, CancellationToken cancellationToken); } +/// +/// The part of added after shipped. Kept apart so that an implementation compiled +/// against a version without it still loads. +/// +internal interface IFSharpCrossLanguageSymbolNavigationService2 : IFSharpCrossLanguageSymbolNavigationService +{ + /// + Task<(string filePath, LinePosition linePosition)?> TryGetNavigableFileLocationAsync( + string assemblyName, string documentationCommentId, CancellationToken cancellationToken); +} + /// internal sealed record class FSharpNavigationOptions2( bool PreferProvisionalTab, diff --git a/src/VisualStudio/ExternalAccess/Core/InternalAPI.Unshipped.txt b/src/VisualStudio/ExternalAccess/Core/InternalAPI.Unshipped.txt index 5ce7cf210fc14..ce841aa0a296c 100644 --- a/src/VisualStudio/ExternalAccess/Core/InternalAPI.Unshipped.txt +++ b/src/VisualStudio/ExternalAccess/Core/InternalAPI.Unshipped.txt @@ -445,6 +445,7 @@ Microsoft.CodeAnalysis.ExternalAccess.Unified.FSharp.Internal.NavigateTo.Interna Microsoft.CodeAnalysis.ExternalAccess.Unified.FSharp.Internal.Navigation.FSharpCrossLanguageSymbolNavigationService Microsoft.CodeAnalysis.ExternalAccess.Unified.FSharp.Internal.Navigation.FSharpCrossLanguageSymbolNavigationService.FSharpCrossLanguageSymbolNavigationService(Microsoft.CodeAnalysis.ExternalAccess.Unified.FSharp.Navigation.IFSharpCrossLanguageSymbolNavigationService! underlyingService) -> void Microsoft.CodeAnalysis.ExternalAccess.Unified.FSharp.Internal.Navigation.FSharpCrossLanguageSymbolNavigationService.TryGetNavigableLocationAsync(string! assemblyName, string! documentationCommentId, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +Microsoft.CodeAnalysis.ExternalAccess.Unified.FSharp.Internal.Navigation.FSharpCrossLanguageSymbolNavigationService.TryGetNavigableFileLocationAsync(string! assemblyName, string! documentationCommentId, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task<(string! filePath, Microsoft.CodeAnalysis.Text.LinePosition linePosition)?>! Microsoft.CodeAnalysis.ExternalAccess.Unified.FSharp.Internal.Navigation.FSharpNavigableItemsService Microsoft.CodeAnalysis.ExternalAccess.Unified.FSharp.Internal.Navigation.FSharpNavigableItemsService.FSharpNavigableItemsService(Microsoft.CodeAnalysis.ExternalAccess.Unified.FSharp.GoToDefinition.IFSharpFindDefinitionService! service) -> void Microsoft.CodeAnalysis.ExternalAccess.Unified.FSharp.Internal.Navigation.FSharpNavigableItemsService.GetNavigableItemsAsync(Microsoft.CodeAnalysis.Document! document, int position, bool forSymbolType, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task>! @@ -503,6 +504,8 @@ Microsoft.CodeAnalysis.ExternalAccess.Unified.FSharp.Navigation.FSharpNavigation Microsoft.CodeAnalysis.ExternalAccess.Unified.FSharp.Navigation.FSharpNavigationOptions2.PreferProvisionalTab.init -> void Microsoft.CodeAnalysis.ExternalAccess.Unified.FSharp.Navigation.IFSharpCrossLanguageSymbolNavigationService Microsoft.CodeAnalysis.ExternalAccess.Unified.FSharp.Navigation.IFSharpCrossLanguageSymbolNavigationService.TryGetNavigableLocationAsync(string! assemblyName, string! documentationCommentId, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +Microsoft.CodeAnalysis.ExternalAccess.Unified.FSharp.Navigation.IFSharpCrossLanguageSymbolNavigationService2 +Microsoft.CodeAnalysis.ExternalAccess.Unified.FSharp.Navigation.IFSharpCrossLanguageSymbolNavigationService2.TryGetNavigableFileLocationAsync(string! assemblyName, string! documentationCommentId, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task<(string! filePath, Microsoft.CodeAnalysis.Text.LinePosition linePosition)?>! Microsoft.CodeAnalysis.ExternalAccess.Unified.FSharp.Navigation.IFSharpDocumentNavigationService Microsoft.CodeAnalysis.ExternalAccess.Unified.FSharp.Navigation.IFSharpNavigableLocation Microsoft.CodeAnalysis.ExternalAccess.Unified.FSharp.Navigation.IFSharpNavigableLocation.NavigateToAsync(Microsoft.CodeAnalysis.ExternalAccess.Unified.FSharp.Navigation.FSharpNavigationOptions2! options, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! @@ -852,6 +855,7 @@ Microsoft.VisualStudio.ExternalAccess.FSharp.Internal.NavigateTo.InternalFSharpN Microsoft.VisualStudio.ExternalAccess.FSharp.Internal.Navigation.FSharpCrossLanguageSymbolNavigationService Microsoft.VisualStudio.ExternalAccess.FSharp.Internal.Navigation.FSharpCrossLanguageSymbolNavigationService.FSharpCrossLanguageSymbolNavigationService(Microsoft.VisualStudio.ExternalAccess.FSharp.Navigation.IFSharpCrossLanguageSymbolNavigationService! underlyingService) -> void Microsoft.VisualStudio.ExternalAccess.FSharp.Internal.Navigation.FSharpCrossLanguageSymbolNavigationService.TryGetNavigableLocationAsync(string! assemblyName, string! documentationCommentId, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +Microsoft.VisualStudio.ExternalAccess.FSharp.Internal.Navigation.FSharpCrossLanguageSymbolNavigationService.TryGetNavigableFileLocationAsync(string! assemblyName, string! documentationCommentId, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task<(string! filePath, Microsoft.CodeAnalysis.Text.LinePosition linePosition)?>! Microsoft.VisualStudio.ExternalAccess.FSharp.Internal.Navigation.FSharpNavigableItemsService Microsoft.VisualStudio.ExternalAccess.FSharp.Internal.Navigation.FSharpNavigableItemsService.FSharpNavigableItemsService(Microsoft.VisualStudio.ExternalAccess.FSharp.GoToDefinition.IFSharpFindDefinitionService! service) -> void Microsoft.VisualStudio.ExternalAccess.FSharp.Internal.Navigation.FSharpNavigableItemsService.GetNavigableItemsAsync(Microsoft.CodeAnalysis.Document! document, int position, bool forSymbolType, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task>! @@ -910,6 +914,8 @@ Microsoft.VisualStudio.ExternalAccess.FSharp.Navigation.FSharpNavigationOptions2 Microsoft.VisualStudio.ExternalAccess.FSharp.Navigation.FSharpNavigationOptions2.PreferProvisionalTab.init -> void Microsoft.VisualStudio.ExternalAccess.FSharp.Navigation.IFSharpCrossLanguageSymbolNavigationService Microsoft.VisualStudio.ExternalAccess.FSharp.Navigation.IFSharpCrossLanguageSymbolNavigationService.TryGetNavigableLocationAsync(string! assemblyName, string! documentationCommentId, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +Microsoft.VisualStudio.ExternalAccess.FSharp.Navigation.IFSharpCrossLanguageSymbolNavigationService2 +Microsoft.VisualStudio.ExternalAccess.FSharp.Navigation.IFSharpCrossLanguageSymbolNavigationService2.TryGetNavigableFileLocationAsync(string! assemblyName, string! documentationCommentId, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task<(string! filePath, Microsoft.CodeAnalysis.Text.LinePosition linePosition)?>! Microsoft.VisualStudio.ExternalAccess.FSharp.Navigation.IFSharpDocumentNavigationService Microsoft.VisualStudio.ExternalAccess.FSharp.Navigation.IFSharpNavigableLocation Microsoft.VisualStudio.ExternalAccess.FSharp.Navigation.IFSharpNavigableLocation.NavigateToAsync(Microsoft.VisualStudio.ExternalAccess.FSharp.Navigation.FSharpNavigationOptions2! options, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task!