Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

VS Go to definition from C# to F# #14377

Merged
merged 21 commits into from Jan 9, 2023
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
22 changes: 11 additions & 11 deletions eng/Versions.props
Expand Up @@ -95,11 +95,11 @@
<SystemRuntimeCompilerServicesUnsafeVersion>6.0.0</SystemRuntimeCompilerServicesUnsafeVersion>
<SystemValueTupleVersion>4.5.0</SystemValueTupleVersion>
<!-- Versions for package groups -->
<RoslynVersion>4.4.0-3.22470.1</RoslynVersion>
<VisualStudioEditorPackagesVersion>17.4.196-preview</VisualStudioEditorPackagesVersion>
<MicrosoftVisualStudioShellPackagesVersion>17.4.0-preview-3-32916-145</MicrosoftVisualStudioShellPackagesVersion>
<VisualStudioProjectSystemPackagesVersion>17.4.342-pre</VisualStudioProjectSystemPackagesVersion>
<MicrosoftVisualStudioThreadingPackagesVersion>17.4.23-alpha</MicrosoftVisualStudioThreadingPackagesVersion>
<RoslynVersion>4.5.0-1.22520.13</RoslynVersion>
<VisualStudioEditorPackagesVersion>17.5.49-preview</VisualStudioEditorPackagesVersion>
<MicrosoftVisualStudioShellPackagesVersion>17.5.0-preview-1-33020-520</MicrosoftVisualStudioShellPackagesVersion>
<VisualStudioProjectSystemPackagesVersion>17.5.202-pre-g89e17c9f72</VisualStudioProjectSystemPackagesVersion>
<MicrosoftVisualStudioThreadingPackagesVersion>17.4.27</MicrosoftVisualStudioThreadingPackagesVersion>
<MicrosoftBuildOverallPackagesVersion>17.4.0-preview-22469-04</MicrosoftBuildOverallPackagesVersion>
<!-- Roslyn packages -->
<MicrosoftCodeAnalysisEditorFeaturesVersion>$(RoslynVersion)</MicrosoftCodeAnalysisEditorFeaturesVersion>
Expand All @@ -115,7 +115,7 @@
<!-- Visual Studio Shell packages -->
<MicrosoftVisualStudioInteropVersion>$(MicrosoftVisualStudioShellPackagesVersion)</MicrosoftVisualStudioInteropVersion>
<MicrosoftInternalVisualStudioInteropVersion>$(MicrosoftVisualStudioShellPackagesVersion)</MicrosoftInternalVisualStudioInteropVersion>
<MicrosoftVisualStudioImagingInterop140DesignTimeVersion>17.4.0-preview-3-32916-053</MicrosoftVisualStudioImagingInterop140DesignTimeVersion>
<MicrosoftVisualStudioImagingInterop140DesignTimeVersion>17.5.0-preview-1-33019-447</MicrosoftVisualStudioImagingInterop140DesignTimeVersion>
<MicrosoftVisualStudioShellInterop80Version>$(MicrosoftVisualStudioShellPackagesVersion)</MicrosoftVisualStudioShellInterop80Version>
<MicrosoftVisualStudioShellInterop90Version>$(MicrosoftVisualStudioShellPackagesVersion)</MicrosoftVisualStudioShellInterop90Version>
<MicrosoftVisualStudioShellInterop100Version>$(MicrosoftVisualStudioShellPackagesVersion)</MicrosoftVisualStudioShellInterop100Version>
Expand All @@ -132,8 +132,8 @@
<MicrosoftVisualStudioShellDesignVersion>$(MicrosoftVisualStudioShellPackagesVersion)</MicrosoftVisualStudioShellDesignVersion>
<MicrosoftVisualStudioShellFrameworkVersion>$(MicrosoftVisualStudioShellPackagesVersion)</MicrosoftVisualStudioShellFrameworkVersion>
<MicrosoftVisualStudioPackageLanguageService150Version>$(MicrosoftVisualStudioShellPackagesVersion)</MicrosoftVisualStudioPackageLanguageService150Version>
<MicrosoftVisualStudioManagedInterfacesVersion>17.4.0-preview-3-32916-053</MicrosoftVisualStudioManagedInterfacesVersion>
<MicrosoftVisualStudioProjectAggregatorVersion>17.4.0-preview-3-32916-053</MicrosoftVisualStudioProjectAggregatorVersion>
<MicrosoftVisualStudioManagedInterfacesVersion>17.5.0-preview-1-33019-447</MicrosoftVisualStudioManagedInterfacesVersion>
vzarytovskii marked this conversation as resolved.
Show resolved Hide resolved
<MicrosoftVisualStudioProjectAggregatorVersion>17.5.0-preview-1-33019-447</MicrosoftVisualStudioProjectAggregatorVersion>
<MicrosoftVisualStudioGraphModelVersion>$(MicrosoftVisualStudioShellPackagesVersion)</MicrosoftVisualStudioGraphModelVersion>
<MicrosoftVisualStudioImagingVersion>$(MicrosoftVisualStudioShellPackagesVersion)</MicrosoftVisualStudioImagingVersion>
<MicrosoftVisualStudioDesignerInterfacesVersion>$(MicrosoftVisualStudioShellPackagesVersion)</MicrosoftVisualStudioDesignerInterfacesVersion>
Expand Down Expand Up @@ -170,7 +170,7 @@
<MicrosoftVisualStudioProjectSystemManagedVersion>2.3.6152103</MicrosoftVisualStudioProjectSystemManagedVersion>
<!-- Misc. Visual Studio packages -->
<MicrosoftVSSDKBuildToolsVersion>17.1.4054</MicrosoftVSSDKBuildToolsVersion>
<MicrosoftVisualStudioRpcContractsVersion>17.4.7-alpha</MicrosoftVisualStudioRpcContractsVersion>
<MicrosoftVisualStudioRpcContractsVersion>17.5.9-alpha-g84529e7115</MicrosoftVisualStudioRpcContractsVersion>
<MicrosoftVisualFSharpMicrosoftVisualStudioShellUIInternalVersion>17.0.0</MicrosoftVisualFSharpMicrosoftVisualStudioShellUIInternalVersion>
<MicrosoftVisualStudioValidationVersion>17.0.64</MicrosoftVisualStudioValidationVersion>
<MicrosoftVisualStudioWCFReferenceInteropVersion>9.0.30729</MicrosoftVisualStudioWCFReferenceInteropVersion>
Expand Down Expand Up @@ -203,8 +203,8 @@
<NUnitLiteVersion>3.11.0</NUnitLiteVersion>
<NunitXmlTestLoggerVersion>2.1.80</NunitXmlTestLoggerVersion>
<RoslynToolsSignToolVersion>1.0.0-beta2-dev3</RoslynToolsSignToolVersion>
<StreamJsonRpcVersion>2.13.23-alpha</StreamJsonRpcVersion>
<NerdbankStreamsVersion>2.9.87-alpha</NerdbankStreamsVersion>
<StreamJsonRpcVersion>2.14.6-alpha</StreamJsonRpcVersion>
<NerdbankStreamsVersion>2.9.112</NerdbankStreamsVersion>
<XUnitVersion>2.4.1</XUnitVersion>
<XUnitRunnerVersion>2.4.2</XUnitRunnerVersion>
<FluentAssertionsVersion>5.10.3</FluentAssertionsVersion>
Expand Down
2 changes: 1 addition & 1 deletion src/Compiler/Symbols/Symbols.fs
Expand Up @@ -1777,7 +1777,7 @@ type FSharpMemberOrFunctionOrValue(cenv, d:FSharpMemberOrValData, item) =
if isUnresolved() then false else
match fsharpInfo() with
| None -> false
| Some v ->
| Some v ->
v.IsCompilerGenerated

member _.InlineAnnotation =
Expand Down
Expand Up @@ -94,7 +94,7 @@ module internal MetadataAsSource =

[<Sealed>]
[<Export(typeof<FSharpMetadataAsSourceService>); Composition.Shared>]
type internal FSharpMetadataAsSourceService() =
type FSharpMetadataAsSourceService() =

let serviceProvider = ServiceProvider.GlobalProvider
let projs = System.Collections.Concurrent.ConcurrentDictionary<string, IFSharpWorkspaceProjectContext>()
Expand Down
Expand Up @@ -7,6 +7,7 @@ open System.Threading
open Microsoft.CodeAnalysis
open FSharp.Compiler
open FSharp.Compiler.CodeAnalysis
open FSharp.Compiler.Symbols

[<AutoOpen>]
module private CheckerExtensions =
Expand Down Expand Up @@ -89,17 +90,16 @@ module private CheckerExtensions =
}

[<RequireQualifiedAccess>]
module private ProjectCache =
module internal ProjectCache =

/// This is a cache to maintain FSharpParsingOptions and FSharpProjectOptions per Roslyn Project.
/// The Roslyn Project is held weakly meaning when it is cleaned up by the GC, the FSharParsingOptions and FSharpProjectOptions will be cleaned up by the GC.
/// At some point, this will be the main caching mechanism for FCS projects instead of FCS itself.
let Projects = ConditionalWeakTable<Project, FSharpChecker * FSharpProjectOptionsManager * FSharpParsingOptions * FSharpProjectOptions>()

type Solution with

/// Get the instance of IFSharpWorkspaceService.
member private this.GetFSharpWorkspaceService() =
member internal this.GetFSharpWorkspaceService() =
this.Workspace.Services.GetRequiredService<IFSharpWorkspaceService>()

type Document with
Expand Down Expand Up @@ -220,3 +220,34 @@ type Project with
for doc in this.Documents do
do! doc.FindFSharpReferencesAsync(symbol, (fun textSpan range -> onFound doc textSpan range), userOpName)
}

member this.GetFSharpCompilationOptionsAsync() =
async {
if this.IsFSharp then
match ProjectCache.Projects.TryGetValue(this) with
| true, result -> return result
| _ ->
let service = this.Solution.GetFSharpWorkspaceService()
let projectOptionsManager = service.FSharpProjectOptionsManager
let! ct = Async.CancellationToken
match! projectOptionsManager.TryGetOptionsByProject(this, ct) with
| None -> return raise(OperationCanceledException("FSharp project options not found."))
| Some(parsingOptions, projectOptions) ->
let result = (service.Checker, projectOptionsManager, parsingOptions, projectOptions)
return ProjectCache.Projects.GetValue(this, ConditionalWeakTable<_,_>.CreateValueCallback(fun _ -> result))
else
return raise(OperationCanceledException("Project is not a FSharp project."))
}

type FSharpEntity with
member this.TryFindValByName(name: string) =
match this.TryGetMembersFunctionsAndValues() with
| xs when xs.Count > 0 ->
xs
|> Seq.filter (
fun x ->
not x.IsPropertyGetterMethod
&& not x.IsPropertySetterMethod
&& not x.IsCompilerGenerated
&& x.CompiledName = name)
| _ -> Seq.empty
131 changes: 127 additions & 4 deletions vsintegration/src/FSharp.Editor/Navigation/GoToDefinition.fs
Expand Up @@ -16,15 +16,18 @@ open Microsoft.CodeAnalysis.Text
open Microsoft.CodeAnalysis.ExternalAccess.FSharp.Navigation

open Microsoft.VisualStudio
open Microsoft.VisualStudio.Shell
open Microsoft.VisualStudio.Shell.Interop
open Microsoft.VisualStudio.LanguageServices

open FSharp.Compiler
open FSharp.Compiler.CodeAnalysis
open FSharp.Compiler.EditorServices
open FSharp.Compiler.Text
open FSharp.Compiler.Text.Range
open FSharp.Compiler.Symbols
open FSharp.Compiler.Tokenization
open System.Composition
open System.Text.RegularExpressions


module private Symbol =
Expand Down Expand Up @@ -111,7 +114,7 @@ module private ExternalSymbol =
| _ -> []

// TODO: Uncomment code when VS has a fix for updating the status bar.
type internal StatusBar(statusBar: IVsStatusbar) =
type StatusBar(statusBar: IVsStatusbar) =
let mutable _searchIcon = int16 Microsoft.VisualStudio.Shell.Interop.Constants.SBAI_Find :> obj

let _clear() =
Expand Down Expand Up @@ -155,7 +158,6 @@ type internal FSharpGoToDefinitionResult =
| ExternalAssembly of FSharpSymbolUse * MetadataReference seq

type internal GoToDefinition(metadataAsSource: FSharpMetadataAsSourceService) =

/// Use an origin document to provide the solution & workspace used to
/// find the corresponding textSpan and INavigableItem for the range
let rangeToNavigableItem (range: range, document: Document) =
Expand Down Expand Up @@ -726,4 +728,125 @@ type internal FSharpNavigation

// Don't show the dialog box as it's most likely that the user cancelled.
// Don't make them click twice.
true
true

type private SymbolPath = { EntityPath: string list; MemberOrValName: string }
[<RequireQualifiedAccess>]
type private DocCommentId =
| Member of SymbolPath
| Type of EntityPath: string list
| Other of string
| None


type FSharpNavigableLocation(statusBar: StatusBar, metadataAsSource: FSharpMetadataAsSourceService, symbolRange: range, project: Project) =
interface IFSharpNavigableLocation with
member _.NavigateToAsync(_options: FSharpNavigationOptions2, cancellationToken: CancellationToken) : Task<bool> =
asyncMaybe {
let targetPath = symbolRange.FileName
let! targetDoc = project.Solution.TryGetDocumentFromFSharpRange (symbolRange, project.Id)
let! targetSource = targetDoc.GetTextAsync(cancellationToken)
let gtd = GoToDefinition(metadataAsSource)

let (|Signature|Implementation|) filepath =
if isSignatureFile filepath then Signature else Implementation

match targetPath with
| Signature ->
return! gtd.NavigateToSymbolDefinitionAsync(targetDoc, targetSource, symbolRange, statusBar, cancellationToken)
| Implementation ->
return! gtd.NavigateToSymbolDeclarationAsync(targetDoc, targetSource, symbolRange, statusBar, cancellationToken)
}
|> Async.map (fun a -> a.IsSome)
|> RoslynHelpers.StartAsyncAsTask cancellationToken

[<Export(typeof<IFSharpCrossLanguageSymbolNavigationService>)>]
[<Export(typeof<FSharpCrossLanguageSymbolNavigationService>)>]
type FSharpCrossLanguageSymbolNavigationService() =
let componentModel = Package.GetGlobalService(typeof<ComponentModelHost.SComponentModel>) :?> ComponentModelHost.IComponentModel
let workspace = componentModel.GetService<VisualStudioWorkspace>()
let statusBar = StatusBar(ServiceProvider.GlobalProvider.GetService<SVsStatusbar,IVsStatusbar>())
let metadataAsSource = componentModel.DefaultExportProvider.GetExport<FSharpMetadataAsSourceService>().Value

// So, the groups are following:
// 1 - type (see below).
// 2 - Path - a dotted path to a symbol.
// 3 - parameters, opetional, only for methods and properties.
// 4 - return type, optional, only for methods.
let docCommentIdRx = Regex(@"^(\w):([\w\d#`.]+)(\(.+\))?(?:~([\w\d.]+))?$", RegexOptions.Compiled)
vzarytovskii marked this conversation as resolved.
Show resolved Hide resolved
vzarytovskii marked this conversation as resolved.
Show resolved Hide resolved

let docCommentIdToPath (docId:string) =
vzarytovskii marked this conversation as resolved.
Show resolved Hide resolved
// docCommentId is in the following format:
//
// "T:" prefix for types
// "T:N.X.Nested" - type
// "T:N.X.D" - delegate
//
// "M:" prefix is for methods
// "M:N.X.#ctor" - constructor
// "M:N.X.#ctor(System.Int32)" - constructor with one parameter
// "M:N.X.f" - method with unit parameter
// "M:N.X.bb(System.String,System.Int32@)" - method with two parameters
T-Gro marked this conversation as resolved.
Show resolved Hide resolved
// "M:N.X.gg(System.Int16[],System.Int32[0:,0:])" - method with two parameters, 1d and 2d array
// "M:N.X.op_Addition(N.X,N.X)" - operator
// "M:N.X.op_Explicit(N.X)~System.Int32" - operator with return type
// "M:N.GenericMethod.WithNestedType``1(N.GenericType{``0}.NestedType)" - generic type with one parameter
// "M:N.GenericMethod.WithIntOfNestedType``1(N.GenericType{System.Int32}.NestedType)" - generic type with one parameter
// "M:N.X.N#IX{N#KVP{System#String,System#Int32}}#IXA(N.KVP{System.String,System.Int32})" - explicit interface implementation
//
// "E:" prefix for events
//
// "E:N.X.d".
//
// "F:" prefix for fields
// "F:N.X.q" - field
//
// "P:" prefix for properties
// "P:N.X.prop" - property with getter and setter

let m = docCommentIdRx.Match(docId)
match m.Success, m.Groups[1].Value with
| true, ("M" | "P" | "F" | "E") ->
// TODO: Probably, there's less janky way of dealing with those.
let parts = m.Groups[2].Value.Split('.')
let entityPath = parts[..(parts.Length - 2)] |> List.ofArray
let memberOrVal = parts[parts.Length - 1]
DocCommentId.Member { EntityPath = entityPath; MemberOrValName = memberOrVal }
| true, "T" ->
let entityPath = m.Groups[2].Value.Split('.') |> List.ofArray
DocCommentId.Type entityPath
| _ -> DocCommentId.None


interface IFSharpCrossLanguageSymbolNavigationService with
member _.TryGetNavigableLocationAsync(assemblyName: string, documentationCommentId: string, cancellationToken: CancellationToken) : Task<IFSharpNavigableLocation> =
let path = docCommentIdToPath documentationCommentId
async {
let projects = workspace.CurrentSolution.Projects |> Seq.filter (fun p -> p.IsFSharp && p.AssemblyName = assemblyName)
T-Gro marked this conversation as resolved.
Show resolved Hide resolved

let mutable locations = Seq.empty

for project in projects do
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could probably do this in parallel.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is just a fool-proof, we expect it always be only a single project, parallel then will give you a bunch of overhead

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, didn't notice where it's coming from. Is it even theoretically possible there will be multiple projects with the same assembly name? Because maybe you could replace filter with tryFind and it would be a bit simpler.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it even theoretically possible there will be multiple projects with the same assembly name?

Very unlikely, I have never seen it before.

Because maybe you could replace filter with tryFind and it would be a bit simpler.

I'll see how it looks with option

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With option it could look something like this (After also adding DocCommentId.EntityPath):

let project = workspace.CurrentSolution.Projects |> Seq.tryFind (fun p -> p.IsFSharp && p.AssemblyName = assemblyName)
let locations = 
    project
    |> Option.map (fun project -> 
        let! checker, _, _, options = project.GetFSharpCompilationOptionsAsync(cancellationToken)
        let! result = checker.ParseAndCheckProject(options)
        let entity = path.EntityPath |> result.AssemblySignature.FindEntityByPath
        match path with
        | DocCommentId.Member ({ MemberOrValName = memberOrVal; GenericParameters = genericParametersCount }, memberType) ->
            entity |> tryFindVal memberOrVal documentationCommentId memberType genericParametersCount
        | DocCommentId.Field { MemberOrValName = memberOrVal } ->
            entity |> tryFindFieldByName memberOrVal
        | DocCommentId.Type _ -> 
            Seq.singleton entity.DeclarationLocation
        | DocCommentId.None -> 
            Seq.empty    
        |> Seq.map (fun m -> (m, project)))
    |> Option.defaultValue Seq.empty

A bit easier to see what's going on with arrows only going in one direction :)

let! checker, _, _, options = project.GetFSharpCompilationOptionsAsync()
let! result = checker.ParseAndCheckProject(options)

match path with
| DocCommentId.Member { EntityPath = entityPath; MemberOrValName = memberOrVal} ->
let entity = result.AssemblySignature.FindEntityByPath (entityPath)
match entity with
| Some e ->
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

probably should replace with Option.map/iter or something

locations <- (e.TryFindValByName(memberOrVal))
|> Seq.map (fun e -> (e.DeclarationLocation, project))
|> Seq.append locations
| None -> ()
| _ -> ()
vzarytovskii marked this conversation as resolved.
Show resolved Hide resolved

// TODO: Figure out the way of giving the user choice where to navigate, if there are more than one result
// For now, we only take 1st one, since it's usually going to be only one result (given we process names correctly).
// More results can theoretically be returned in case of method overloads, or when we have both signature and implementation files.
if locations.Count() >= 1 then
let (location, project) = locations.First()
return FSharpNavigableLocation(statusBar, metadataAsSource, location, project) :> IFSharpNavigableLocation
else
return Unchecked.defaultof<_> // returning null here, so Roslyn can fallback to default source-as-metadata implementation.
} |> RoslynHelpers.StartAsyncAsTask cancellationToken