Skip to content
Open
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
32 changes: 32 additions & 0 deletions src/EditorFeatures/Core/Peek/PeekableItemFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -72,6 +73,7 @@ public async Task<IEnumerable<IPeekableItem>> GetPeekableItemsAsync(

var symbolNavigationService = solution.Services.GetService<ISymbolNavigationService>();
var result = await symbolNavigationService.GetExternalNavigationSymbolLocationAsync(definitionItem, cancellationToken).ConfigureAwait(false);
result ??= await GetCrossLanguageFileLocationAsync(solution, symbol, cancellationToken).ConfigureAwait(false);
Comment thread
xperiandri marked this conversation as resolved.

using var _ = ArrayBuilder<IPeekableItem>.GetInstance(out var results);
if (result is var (filePath, linePosition))
Expand All @@ -95,4 +97,34 @@ public async Task<IEnumerable<IPeekableItem>> GetPeekableItemsAsync(

return results.ToImmutableAndClear();
}

/// <summary>
/// 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 <c>VisualStudioSymbolNavigationService</c>.
/// </summary>
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<ICrossLanguageSymbolNavigationService>())
{
var location = await lazyService.Value.TryGetNavigableFileLocationAsync(
assemblyName, docCommentId, cancellationToken).ConfigureAwait(false);
if (location != null)
return location;
}

return null;
}
}
168 changes: 166 additions & 2 deletions src/EditorFeatures/Test2/Peek/PeekTests.vb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
<WpfTheory>
<InlineData("$$Counter c;", "T:Counter")>
<InlineData("void M() => new Box<int>().$$Set(1);", "M:Box`1.Set(`0)")>
<InlineData("void M() => 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

<WpfFact>
Public Sub TestPeekDefinitionShowsMetadataAsSourceWhenNoOtherLanguageOwnsTheSymbol()
Using workspace = CreateTestWorkspace(<Workspace>
<Project Language="C#" CommonReferences="true">
<Document>class C { string s = $$"Goo"; }</Document>
</Project>
</Workspace>, 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

<WpfFact>
Public Sub TestPeekDefinitionDoesNotAskAnotherLanguageForASourceSymbol()
Using workspace = CreateTestWorkspace(<Workspace>
<Project Language="C#" CommonReferences="true">
<Document>public class {|Identifier:D|} { } class C { $$D d; }</Document>
</Project>
</Workspace>, 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

<WpfFact>
Public Sub TestPeekDefinitionDoesNotAskAnotherLanguageForAMetadataSymbolItCannotShow()
Using workspace = CreateTestWorkspace(<Workspace>
<Project Language="C#" CommonReferences="true">
<Document>using $$System; class C { }</Document>
</Project>
</Workspace>, s_crossLanguageComposition)
Dim result = GetPeekResultCollection(workspace)

Assert.Empty(GetCrossLanguageService(workspace).Requests)
Assert.Null(result)
End Using
End Sub

<WpfFact>
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))

''' <summary>
''' A C# project referencing, as metadata, an assembly <see cref="CrossLanguageSymbolNavigationService"/> owns
''' the source of, the way F# owns the source of the F# assemblies a C# project references.
''' </summary>
Private Shared Function WorkspaceReferencingOtherLanguageLibrary(member As String) As XElement
Return <Workspace>
<Project Language="C#" CommonReferences="true">
<MetadataReferenceFromSource Language="C#" AssemblyName=<%= CrossLanguageSymbolNavigationService.OwnedAssemblyName %> CommonReferences="true">
<Document>public class Counter { public void Increment() { } } public class Box&lt;T&gt; { public void Set(T value) { } }</Document>
</MetadataReferenceFromSource>
<Document>class C { <%= member %> }</Document>
</Project>
</Workspace>
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
Expand Down Expand Up @@ -342,6 +440,62 @@ public partial class D
Return peekResult
End Function

<Export(GetType(ICrossLanguageSymbolNavigationService)), [Shared], PartNotDiscoverable>
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)

<ImportingConstructor>
<Obsolete(MefConstruction.ImportingConstructorMessage, True)>
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

<ExportWorkspaceService(GetType(ISymbolNavigationService), ServiceLayer.Test), [Shared], PartNotDiscoverable>
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)

<ImportingConstructor>
<Obsolete(MefConstruction.ImportingConstructorMessage, True)>
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

Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Text;

namespace Microsoft.CodeAnalysis.Navigation;

Expand All @@ -22,4 +23,14 @@ internal interface ICrossLanguageSymbolNavigationService
/// receiver to quickly filter down to the project/compilation search for the symbol.</param>
Task<INavigableLocation?> TryGetNavigableLocationAsync(
string assemblyName, string documentationCommentId, CancellationToken cancellationToken);

/// <summary>
/// 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 <see
/// langword="null"/> if the 3rd party language cannot provide a file for this particular symbol.
/// </summary>
/// <param name="assemblyName">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.</param>
Task<(string filePath, LinePosition linePosition)?> TryGetNavigableFileLocationAsync(
string assemblyName, string documentationCommentId, CancellationToken cancellationToken);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -16,6 +17,18 @@ internal interface IFSharpCrossLanguageSymbolNavigationService
string assemblyName, string documentationCommentId, CancellationToken cancellationToken);
}

/// <summary>
/// The part of <see cref="ICrossLanguageSymbolNavigationService"/> added after <see
/// cref="IFSharpCrossLanguageSymbolNavigationService"/> shipped. Kept apart so that an implementation compiled
/// against a version without it still loads.
/// </summary>
internal interface IFSharpCrossLanguageSymbolNavigationService2 : IFSharpCrossLanguageSymbolNavigationService
{
/// <inheritdoc cref="ICrossLanguageSymbolNavigationService.TryGetNavigableFileLocationAsync"/>
Task<(string filePath, LinePosition linePosition)?> TryGetNavigableFileLocationAsync(
string assemblyName, string documentationCommentId, CancellationToken cancellationToken);
}

/// <inheritdoc cref="NavigationOptions"/>
internal sealed record class FSharpNavigationOptions2(
bool PreferProvisionalTab,
Expand Down
Loading