From 2382d8db8bf4cee98cc71b491b9b2d853b794763 Mon Sep 17 00:00:00 2001 From: Vlad Zarytovskii Date: Mon, 21 Nov 2022 19:22:51 +0100 Subject: [PATCH 01/17] WIP --- eng/Versions.props | 22 +-- src/Compiler/Symbols/Symbols.fs | 13 ++ .../LanguageService/WorkspaceExtensions.fs | 23 +++- .../Navigation/GoToDefinition.fs | 129 +++++++++++++++++- 4 files changed, 171 insertions(+), 16 deletions(-) diff --git a/eng/Versions.props b/eng/Versions.props index 979442cba5e..da937b7a5c7 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -95,11 +95,11 @@ 6.0.0 4.5.0 - 4.4.0-3.22470.1 - 17.4.196-preview - 17.4.0-preview-3-32916-145 - 17.4.342-pre - 17.4.23-alpha + 4.5.0-1.22520.13 + 17.5.49-preview + 17.5.0-preview-1-33020-520 + 17.5.202-pre-g89e17c9f72 + 17.4.27 17.4.0-preview-22469-04 $(RoslynVersion) @@ -115,7 +115,7 @@ $(MicrosoftVisualStudioShellPackagesVersion) $(MicrosoftVisualStudioShellPackagesVersion) - 17.4.0-preview-3-32916-053 + 17.5.0-preview-1-33019-447 $(MicrosoftVisualStudioShellPackagesVersion) $(MicrosoftVisualStudioShellPackagesVersion) $(MicrosoftVisualStudioShellPackagesVersion) @@ -132,8 +132,8 @@ $(MicrosoftVisualStudioShellPackagesVersion) $(MicrosoftVisualStudioShellPackagesVersion) $(MicrosoftVisualStudioShellPackagesVersion) - 17.4.0-preview-3-32916-053 - 17.4.0-preview-3-32916-053 + 17.5.0-preview-1-33019-447 + 17.5.0-preview-1-33019-447 $(MicrosoftVisualStudioShellPackagesVersion) $(MicrosoftVisualStudioShellPackagesVersion) $(MicrosoftVisualStudioShellPackagesVersion) @@ -170,7 +170,7 @@ 2.3.6152103 17.1.4054 - 17.4.7-alpha + 17.5.9-alpha-g84529e7115 17.0.0 17.0.64 9.0.30729 @@ -203,8 +203,8 @@ 3.11.0 2.1.80 1.0.0-beta2-dev3 - 2.13.23-alpha - 2.9.87-alpha + 2.14.6-alpha + 2.9.112 2.4.1 2.4.2 5.10.3 diff --git a/src/Compiler/Symbols/Symbols.fs b/src/Compiler/Symbols/Symbols.fs index 499654b029a..e047408eed1 100644 --- a/src/Compiler/Symbols/Symbols.fs +++ b/src/Compiler/Symbols/Symbols.fs @@ -2818,6 +2818,19 @@ type FSharpAssemblySignature (cenv, topAttribs: TopAttribs option, optViewedCcu: |> Option.map (fun e -> FSharpEntity(cenv, rescopeEntity optViewedCcu e)) | _ -> None + member _.FindMemberOrValByPath path = + let findNested name entity = + match entity with + | Some (e: Entity) -> e.ModuleOrNamespaceType.AllEntitiesByCompiledAndLogicalMangledNames.TryFind name + | _ -> None + + match path with + | hd :: tl -> + (mtyp.AllEntitiesByCompiledAndLogicalMangledNames.TryFind hd, tl) + ||> List.fold (fun a x -> findNested x a) + |> Option.map (fun e -> FSharpEntity(cenv, rescopeEntity optViewedCcu e)) + | _ -> None + member x.TryGetEntities() = try x.Entities :> _ seq with _ -> Seq.empty override x.ToString() = "" diff --git a/vsintegration/src/FSharp.Editor/LanguageService/WorkspaceExtensions.fs b/vsintegration/src/FSharp.Editor/LanguageService/WorkspaceExtensions.fs index 8b067f72a47..0e42de1166e 100644 --- a/vsintegration/src/FSharp.Editor/LanguageService/WorkspaceExtensions.fs +++ b/vsintegration/src/FSharp.Editor/LanguageService/WorkspaceExtensions.fs @@ -89,7 +89,7 @@ module private CheckerExtensions = } [] -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. @@ -97,9 +97,8 @@ module private ProjectCache = let Projects = ConditionalWeakTable() type Solution with - /// Get the instance of IFSharpWorkspaceService. - member private this.GetFSharpWorkspaceService() = + member internal this.GetFSharpWorkspaceService() = this.Workspace.Services.GetRequiredService() type Document with @@ -220,3 +219,21 @@ 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.")) + } \ No newline at end of file diff --git a/vsintegration/src/FSharp.Editor/Navigation/GoToDefinition.fs b/vsintegration/src/FSharp.Editor/Navigation/GoToDefinition.fs index 4bdfec0bf11..251c8db28ca 100644 --- a/vsintegration/src/FSharp.Editor/Navigation/GoToDefinition.fs +++ b/vsintegration/src/FSharp.Editor/Navigation/GoToDefinition.fs @@ -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 = @@ -726,4 +729,126 @@ 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 \ No newline at end of file + true + +type private SymbolPath = { EntityPath: string list; MemberOrValName: string } +[] +type private DocCommentId = + | Member of SymbolPath + | Type of EntityPath: string list + | Other of string + | None + +[] +type FSharpNavigableLocation(_a: bool) = + interface IFSharpNavigableLocation with + member _.NavigateToAsync(_options: FSharpNavigationOptions2, cancellationToken: CancellationToken) : Task = + asyncMaybe { + //let document = workspace.CurrentSolution.GetDocument(documentId) + return true + } + |> Async.map (Option.defaultValue false) + |> RoslynHelpers.StartAsyncAsTask cancellationToken + +[)>] +[)>] +type FSharpCrossLanguageSymbolNavigationService() = + let componentModel = Package.GetGlobalService(typeof) :?> ComponentModelHost.IComponentModel + let workspace = componentModel.GetService() + + // 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) + + let docCommentIdToPath (docId:string) = + // 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 + // "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 = + let result = + async { + let projects = workspace.CurrentSolution.Projects |> Seq.filter (fun p -> p.IsFSharp && p.AssemblyName = assemblyName) + + let project = projects.First() + let service = project.Solution.GetFSharpWorkspaceService() + let projectOptionsManager = service.FSharpProjectOptionsManager + match! projectOptionsManager.TryGetOptionsByProject(project, cancellationToken) with + | None -> return raise(OperationCanceledException("FSharp project options not found.")) + | Some(_, projectOptions) -> + let! result = service.Checker.ParseAndCheckProject(projectOptions) + return result + } |> Async.RunSynchronously + + let path = docCommentIdToPath documentationCommentId + + let _entity = + match path with + | DocCommentId.Member { EntityPath = entityPath; MemberOrValName = _memberOrVal} -> + result.AssemblySignature.FindEntityByPath (entityPath) + | _ -> Unchecked.defaultof<_> + + async { + let projects = workspace.CurrentSolution.Projects |> Seq.filter (fun p -> p.IsFSharp && p.AssemblyName = assemblyName) + + let project = projects.First() + let service = project.Solution.GetFSharpWorkspaceService() + let projectOptionsManager = service.FSharpProjectOptionsManager + match! projectOptionsManager.TryGetOptionsByProject(project, cancellationToken) with + | None -> return raise(OperationCanceledException("FSharp project options not found.")) + | Some(_, projectOptions) -> + let! result = service.Checker.ParseAndCheckProject(projectOptions) + let path = documentationCommentId.Split('.') |> List.ofArray + let _entity = result.AssemblySignature.FindEntityByPath(path) + () + + (*for project in projects do + let! checker, _, _, options = project.GetFSharpCompilationOptionsAsync() + let! result = checker.ParseAndCheckProject(options) + let path = documentationCommentId.Split('.') |> List.ofArray + let _entity = result.AssemblySignature.FindEntityByPath(path) + ()*) + return null + } |> RoslynHelpers.StartAsyncAsTask cancellationToken From aa1d49fc7d3a210a666db248f11a67a47d8eb23c Mon Sep 17 00:00:00 2001 From: Vlad Zarytovskii Date: Tue, 22 Nov 2022 19:51:03 +0100 Subject: [PATCH 02/17] WIP --- src/Compiler/Symbols/Symbols.fs | 15 +--- .../LanguageService/MetadataAsSource.fs | 2 +- .../LanguageService/WorkspaceExtensions.fs | 16 +++- .../Navigation/GoToDefinition.fs | 87 +++++++++---------- 4 files changed, 59 insertions(+), 61 deletions(-) diff --git a/src/Compiler/Symbols/Symbols.fs b/src/Compiler/Symbols/Symbols.fs index e047408eed1..480c58f3af6 100644 --- a/src/Compiler/Symbols/Symbols.fs +++ b/src/Compiler/Symbols/Symbols.fs @@ -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 = @@ -2818,19 +2818,6 @@ type FSharpAssemblySignature (cenv, topAttribs: TopAttribs option, optViewedCcu: |> Option.map (fun e -> FSharpEntity(cenv, rescopeEntity optViewedCcu e)) | _ -> None - member _.FindMemberOrValByPath path = - let findNested name entity = - match entity with - | Some (e: Entity) -> e.ModuleOrNamespaceType.AllEntitiesByCompiledAndLogicalMangledNames.TryFind name - | _ -> None - - match path with - | hd :: tl -> - (mtyp.AllEntitiesByCompiledAndLogicalMangledNames.TryFind hd, tl) - ||> List.fold (fun a x -> findNested x a) - |> Option.map (fun e -> FSharpEntity(cenv, rescopeEntity optViewedCcu e)) - | _ -> None - member x.TryGetEntities() = try x.Entities :> _ seq with _ -> Seq.empty override x.ToString() = "" diff --git a/vsintegration/src/FSharp.Editor/LanguageService/MetadataAsSource.fs b/vsintegration/src/FSharp.Editor/LanguageService/MetadataAsSource.fs index 6ac4e754c1f..0a74deb77cd 100644 --- a/vsintegration/src/FSharp.Editor/LanguageService/MetadataAsSource.fs +++ b/vsintegration/src/FSharp.Editor/LanguageService/MetadataAsSource.fs @@ -94,7 +94,7 @@ module internal MetadataAsSource = [] [); Composition.Shared>] -type internal FSharpMetadataAsSourceService() = +type FSharpMetadataAsSourceService() = let serviceProvider = ServiceProvider.GlobalProvider let projs = System.Collections.Concurrent.ConcurrentDictionary() diff --git a/vsintegration/src/FSharp.Editor/LanguageService/WorkspaceExtensions.fs b/vsintegration/src/FSharp.Editor/LanguageService/WorkspaceExtensions.fs index 0e42de1166e..38a229f461e 100644 --- a/vsintegration/src/FSharp.Editor/LanguageService/WorkspaceExtensions.fs +++ b/vsintegration/src/FSharp.Editor/LanguageService/WorkspaceExtensions.fs @@ -7,6 +7,7 @@ open System.Threading open Microsoft.CodeAnalysis open FSharp.Compiler open FSharp.Compiler.CodeAnalysis +open FSharp.Compiler.Symbols [] module private CheckerExtensions = @@ -236,4 +237,17 @@ type Project with return ProjectCache.Projects.GetValue(this, ConditionalWeakTable<_,_>.CreateValueCallback(fun _ -> result)) else return raise(OperationCanceledException("Project is not a FSharp project.")) - } \ No newline at end of file + } + +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 \ No newline at end of file diff --git a/vsintegration/src/FSharp.Editor/Navigation/GoToDefinition.fs b/vsintegration/src/FSharp.Editor/Navigation/GoToDefinition.fs index 251c8db28ca..631b5c31671 100644 --- a/vsintegration/src/FSharp.Editor/Navigation/GoToDefinition.fs +++ b/vsintegration/src/FSharp.Editor/Navigation/GoToDefinition.fs @@ -114,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() = @@ -158,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) = @@ -738,16 +737,27 @@ type private DocCommentId = | Type of EntityPath: string list | Other of string | None + -[] -type FSharpNavigableLocation(_a: bool) = +type FSharpNavigableLocation(statusBar: StatusBar, metadataAsSource: FSharpMetadataAsSourceService, symbolRange: range, project: Project) = interface IFSharpNavigableLocation with member _.NavigateToAsync(_options: FSharpNavigationOptions2, cancellationToken: CancellationToken) : Task = asyncMaybe { - //let document = workspace.CurrentSolution.GetDocument(documentId) - return true + 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 (Option.defaultValue false) + |> Async.map (fun a -> a.IsSome) |> RoslynHelpers.StartAsyncAsTask cancellationToken [)>] @@ -755,6 +765,8 @@ type FSharpNavigableLocation(_a: bool) = type FSharpCrossLanguageSymbolNavigationService() = let componentModel = Package.GetGlobalService(typeof) :?> ComponentModelHost.IComponentModel let workspace = componentModel.GetService() + let statusBar = StatusBar(ServiceProvider.GlobalProvider.GetService()) + let metadataAsSource = componentModel.DefaultExportProvider.GetExport().Value // So, the groups are following: // 1 - type (see below). @@ -799,7 +811,7 @@ type FSharpCrossLanguageSymbolNavigationService() = 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} + DocCommentId.Member { EntityPath = entityPath; MemberOrValName = memberOrVal } | true, "T" -> let entityPath = m.Groups[2].Value.Split('.') |> List.ofArray DocCommentId.Type entityPath @@ -808,47 +820,32 @@ type FSharpCrossLanguageSymbolNavigationService() = interface IFSharpCrossLanguageSymbolNavigationService with member _.TryGetNavigableLocationAsync(assemblyName: string, documentationCommentId: string, cancellationToken: CancellationToken) : Task = - let result = - async { - let projects = workspace.CurrentSolution.Projects |> Seq.filter (fun p -> p.IsFSharp && p.AssemblyName = assemblyName) - - let project = projects.First() - let service = project.Solution.GetFSharpWorkspaceService() - let projectOptionsManager = service.FSharpProjectOptionsManager - match! projectOptionsManager.TryGetOptionsByProject(project, cancellationToken) with - | None -> return raise(OperationCanceledException("FSharp project options not found.")) - | Some(_, projectOptions) -> - let! result = service.Checker.ParseAndCheckProject(projectOptions) - return result - } |> Async.RunSynchronously - let path = docCommentIdToPath documentationCommentId - - let _entity = - match path with - | DocCommentId.Member { EntityPath = entityPath; MemberOrValName = _memberOrVal} -> - result.AssemblySignature.FindEntityByPath (entityPath) - | _ -> Unchecked.defaultof<_> - async { let projects = workspace.CurrentSolution.Projects |> Seq.filter (fun p -> p.IsFSharp && p.AssemblyName = assemblyName) - let project = projects.First() - let service = project.Solution.GetFSharpWorkspaceService() - let projectOptionsManager = service.FSharpProjectOptionsManager - match! projectOptionsManager.TryGetOptionsByProject(project, cancellationToken) with - | None -> return raise(OperationCanceledException("FSharp project options not found.")) - | Some(_, projectOptions) -> - let! result = service.Checker.ParseAndCheckProject(projectOptions) - let path = documentationCommentId.Split('.') |> List.ofArray - let _entity = result.AssemblySignature.FindEntityByPath(path) - () - - (*for project in projects do + let mutable locations = Seq.empty + + for project in projects do let! checker, _, _, options = project.GetFSharpCompilationOptionsAsync() let! result = checker.ParseAndCheckProject(options) - let path = documentationCommentId.Split('.') |> List.ofArray - let _entity = result.AssemblySignature.FindEntityByPath(path) - ()*) - return null + + match path with + | DocCommentId.Member { EntityPath = entityPath; MemberOrValName = memberOrVal} -> + let entity = result.AssemblySignature.FindEntityByPath (entityPath) + match entity with + | Some e -> + locations <- (e.TryFindValByName(memberOrVal)) + |> Seq.map (fun e -> (e.DeclarationLocation, project)) + |> Seq.append locations + | None -> () + | _ -> () + + // TODO: Figure out the way of giving the user choice where to navigate, if there are more than one result + // For now, for testing, we only take 1st one. + 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 From 895e663d9f5efdccb1ce9c4e6dd4c45ad9629477 Mon Sep 17 00:00:00 2001 From: Vlad Zarytovskii Date: Wed, 23 Nov 2022 13:35:42 +0100 Subject: [PATCH 03/17] WIP --- vsintegration/src/FSharp.Editor/Navigation/GoToDefinition.fs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/vsintegration/src/FSharp.Editor/Navigation/GoToDefinition.fs b/vsintegration/src/FSharp.Editor/Navigation/GoToDefinition.fs index 631b5c31671..b2e084b1aef 100644 --- a/vsintegration/src/FSharp.Editor/Navigation/GoToDefinition.fs +++ b/vsintegration/src/FSharp.Editor/Navigation/GoToDefinition.fs @@ -842,8 +842,9 @@ type FSharpCrossLanguageSymbolNavigationService() = | _ -> () // TODO: Figure out the way of giving the user choice where to navigate, if there are more than one result - // For now, for testing, we only take 1st one. - if locations.Count() > 1 then + // 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 From b53c108d0c5dc241d3feefe4f0aae69910a80331 Mon Sep 17 00:00:00 2001 From: Vlad Zarytovskii Date: Wed, 23 Nov 2022 16:49:01 +0100 Subject: [PATCH 04/17] WIP --- .../LanguageService/WorkspaceExtensions.fs | 13 ---- .../Navigation/GoToDefinition.fs | 71 +++++++++++++++---- 2 files changed, 59 insertions(+), 25 deletions(-) diff --git a/vsintegration/src/FSharp.Editor/LanguageService/WorkspaceExtensions.fs b/vsintegration/src/FSharp.Editor/LanguageService/WorkspaceExtensions.fs index 38a229f461e..7abca989edf 100644 --- a/vsintegration/src/FSharp.Editor/LanguageService/WorkspaceExtensions.fs +++ b/vsintegration/src/FSharp.Editor/LanguageService/WorkspaceExtensions.fs @@ -238,16 +238,3 @@ type Project with 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 \ No newline at end of file diff --git a/vsintegration/src/FSharp.Editor/Navigation/GoToDefinition.fs b/vsintegration/src/FSharp.Editor/Navigation/GoToDefinition.fs index b2e084b1aef..f4a378b2cbd 100644 --- a/vsintegration/src/FSharp.Editor/Navigation/GoToDefinition.fs +++ b/vsintegration/src/FSharp.Editor/Navigation/GoToDefinition.fs @@ -730,14 +730,24 @@ type internal FSharpNavigation // Don't make them click twice. true -type private SymbolPath = { EntityPath: string list; MemberOrValName: string } [] -type private DocCommentId = - | Member of SymbolPath +type internal SymbolMemberType = + Event | Property | Method | Other + static member FromString(s: string) = + match s with + | "E" -> Event + | "P" -> Property + | "M" -> Method + | _ -> Other + +type internal SymbolPath = { EntityPath: string list; MemberOrValName: string; } + +[] +type internal DocCommentId = + | Member of SymbolPath * SymbolMemberType: SymbolMemberType + | Field of SymbolPath | Type of EntityPath: string list - | Other of string | None - type FSharpNavigableLocation(statusBar: StatusBar, metadataAsSource: FSharpMetadataAsSourceService, symbolRange: range, project: Project) = interface IFSharpNavigableLocation with @@ -805,19 +815,48 @@ type FSharpCrossLanguageSymbolNavigationService() = // "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") -> + let t = m.Groups[1].Value + match m.Success, t with + | true, ("M" | "P" | "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 } + DocCommentId.Member ({ EntityPath = entityPath; MemberOrValName = memberOrVal }, (SymbolMemberType.FromString t)) | true, "T" -> let entityPath = m.Groups[2].Value.Split('.') |> List.ofArray DocCommentId.Type entityPath + | true, "F" -> + let parts = m.Groups[2].Value.Split('.') + let entityPath = parts[..(parts.Length - 2)] |> List.ofArray + let memberOrVal = parts[parts.Length - 1] + DocCommentId.Field { EntityPath = entityPath; MemberOrValName = memberOrVal } | _ -> DocCommentId.None - - + + let tryFindValByNameAndType (name: string) (symbolMemberType: SymbolMemberType) (e: FSharpEntity) = + let memberTypePred: (FSharpMemberOrFunctionOrValue -> bool) = + match symbolMemberType with + | SymbolMemberType.Other + | SymbolMemberType.Method -> fun _ -> true + | SymbolMemberType.Event -> fun x -> x.IsEvent + | SymbolMemberType.Property -> fun x -> x.IsProperty + + e.TryGetMembersFunctionsAndValues() + |> Seq.filter ( + fun x -> + not x.IsPropertyGetterMethod + && not x.IsPropertySetterMethod + && not x.IsCompilerGenerated) + |> Seq.filter (fun x -> x.DisplayName = name) + |> Seq.filter memberTypePred + + let tryFindFieldByName (name: string) (e: FSharpEntity) = + e.FSharpFields + |> Seq.filter ( + fun x -> + x.DisplayName = name + && not x.IsCompilerGenerated) + interface IFSharpCrossLanguageSymbolNavigationService with member _.TryGetNavigableLocationAsync(assemblyName: string, documentationCommentId: string, cancellationToken: CancellationToken) : Task = let path = docCommentIdToPath documentationCommentId @@ -831,11 +870,19 @@ type FSharpCrossLanguageSymbolNavigationService() = let! result = checker.ParseAndCheckProject(options) match path with - | DocCommentId.Member { EntityPath = entityPath; MemberOrValName = memberOrVal} -> + | DocCommentId.Member ({ EntityPath = entityPath; MemberOrValName = memberOrVal }, memberType) -> + let entity = result.AssemblySignature.FindEntityByPath (entityPath) + match entity with + | Some e -> + locations <- e |> tryFindValByNameAndType memberOrVal memberType + |> Seq.map (fun e -> (e.DeclarationLocation, project)) + |> Seq.append locations + | None -> () + | DocCommentId.Field { EntityPath = entityPath; MemberOrValName = memberOrVal } -> let entity = result.AssemblySignature.FindEntityByPath (entityPath) match entity with | Some e -> - locations <- (e.TryFindValByName(memberOrVal)) + locations <- e |> tryFindFieldByName memberOrVal |> Seq.map (fun e -> (e.DeclarationLocation, project)) |> Seq.append locations | None -> () From 8c9d3ac1aef4232058028683ae6be0ca31a7436f Mon Sep 17 00:00:00 2001 From: Vlad Zarytovskii Date: Wed, 23 Nov 2022 17:11:49 +0100 Subject: [PATCH 05/17] WIP: get a .ctor fixup --- .../FSharp.Editor/Navigation/GoToDefinition.fs | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/vsintegration/src/FSharp.Editor/Navigation/GoToDefinition.fs b/vsintegration/src/FSharp.Editor/Navigation/GoToDefinition.fs index f4a378b2cbd..563467ba160 100644 --- a/vsintegration/src/FSharp.Editor/Navigation/GoToDefinition.fs +++ b/vsintegration/src/FSharp.Editor/Navigation/GoToDefinition.fs @@ -732,11 +732,12 @@ type internal FSharpNavigation [] type internal SymbolMemberType = - Event | Property | Method | Other + Event | Property | Method | Constructor | Other static member FromString(s: string) = match s with | "E" -> Event | "P" -> Property + | "CTOR" -> Constructor | "M" -> Method | _ -> Other @@ -822,7 +823,12 @@ type FSharpCrossLanguageSymbolNavigationService() = 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 }, (SymbolMemberType.FromString t)) + + // A hack/fixup for the constructor name (#ctor in doccommentid and ``.ctor`` in F#) + if memberOrVal = "#ctor" then + DocCommentId.Member ({ EntityPath = entityPath; MemberOrValName = "``.ctor``" }, (SymbolMemberType.FromString "CTOR")) + else + DocCommentId.Member ({ EntityPath = entityPath; MemberOrValName = memberOrVal }, (SymbolMemberType.FromString t)) | true, "T" -> let entityPath = m.Groups[2].Value.Split('.') |> List.ofArray DocCommentId.Type entityPath @@ -838,15 +844,11 @@ type FSharpCrossLanguageSymbolNavigationService() = match symbolMemberType with | SymbolMemberType.Other | SymbolMemberType.Method -> fun _ -> true + | SymbolMemberType.Constructor -> fun x -> x.IsConstructor | SymbolMemberType.Event -> fun x -> x.IsEvent | SymbolMemberType.Property -> fun x -> x.IsProperty - e.TryGetMembersFunctionsAndValues() - |> Seq.filter ( - fun x -> - not x.IsPropertyGetterMethod - && not x.IsPropertySetterMethod - && not x.IsCompilerGenerated) + e.TryGetMembersFunctionsAndValues() |> Seq.filter (fun x -> x.DisplayName = name) |> Seq.filter memberTypePred From e3b1daa54baa2dac839919664b547a79090c825a Mon Sep 17 00:00:00 2001 From: Vlad Zarytovskii Date: Wed, 23 Nov 2022 17:18:30 +0100 Subject: [PATCH 06/17] WIP: Events are just properties --- vsintegration/src/FSharp.Editor/Navigation/GoToDefinition.fs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vsintegration/src/FSharp.Editor/Navigation/GoToDefinition.fs b/vsintegration/src/FSharp.Editor/Navigation/GoToDefinition.fs index 563467ba160..2d0ee5a7dd8 100644 --- a/vsintegration/src/FSharp.Editor/Navigation/GoToDefinition.fs +++ b/vsintegration/src/FSharp.Editor/Navigation/GoToDefinition.fs @@ -843,9 +843,9 @@ type FSharpCrossLanguageSymbolNavigationService() = let memberTypePred: (FSharpMemberOrFunctionOrValue -> bool) = match symbolMemberType with | SymbolMemberType.Other + | SymbolMemberType.Event | SymbolMemberType.Method -> fun _ -> true | SymbolMemberType.Constructor -> fun x -> x.IsConstructor - | SymbolMemberType.Event -> fun x -> x.IsEvent | SymbolMemberType.Property -> fun x -> x.IsProperty e.TryGetMembersFunctionsAndValues() From e99ea51c0d753771c2fe8ba3159e1adac1bcf3a5 Mon Sep 17 00:00:00 2001 From: Vlad Zarytovskii Date: Wed, 23 Nov 2022 17:18:48 +0100 Subject: [PATCH 07/17] WIP: Events are just properties --- vsintegration/src/FSharp.Editor/Navigation/GoToDefinition.fs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vsintegration/src/FSharp.Editor/Navigation/GoToDefinition.fs b/vsintegration/src/FSharp.Editor/Navigation/GoToDefinition.fs index 2d0ee5a7dd8..b24555b706c 100644 --- a/vsintegration/src/FSharp.Editor/Navigation/GoToDefinition.fs +++ b/vsintegration/src/FSharp.Editor/Navigation/GoToDefinition.fs @@ -843,9 +843,9 @@ type FSharpCrossLanguageSymbolNavigationService() = let memberTypePred: (FSharpMemberOrFunctionOrValue -> bool) = match symbolMemberType with | SymbolMemberType.Other - | SymbolMemberType.Event | SymbolMemberType.Method -> fun _ -> true | SymbolMemberType.Constructor -> fun x -> x.IsConstructor + | SymbolMemberType.Event // Events are just properties | SymbolMemberType.Property -> fun x -> x.IsProperty e.TryGetMembersFunctionsAndValues() From 5a3b93eda91e3dbaf2cfcaa0fcf1f2a3586b0140 Mon Sep 17 00:00:00 2001 From: Vlad Zarytovskii Date: Wed, 23 Nov 2022 17:20:45 +0100 Subject: [PATCH 08/17] WIP --- vsintegration/src/FSharp.Editor/Navigation/GoToDefinition.fs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vsintegration/src/FSharp.Editor/Navigation/GoToDefinition.fs b/vsintegration/src/FSharp.Editor/Navigation/GoToDefinition.fs index b24555b706c..21f26632b0d 100644 --- a/vsintegration/src/FSharp.Editor/Navigation/GoToDefinition.fs +++ b/vsintegration/src/FSharp.Editor/Navigation/GoToDefinition.fs @@ -737,7 +737,7 @@ type internal SymbolMemberType = match s with | "E" -> Event | "P" -> Property - | "CTOR" -> Constructor + | "CTOR" -> Constructor // That one is "artificial one", so we distinguish constructors. | "M" -> Method | _ -> Other From 04f024cf8803a933cb9c28b0608f131f94d5c74c Mon Sep 17 00:00:00 2001 From: Vlad Zarytovskii Date: Wed, 23 Nov 2022 18:15:22 +0100 Subject: [PATCH 09/17] WIP: Strip generic parameters while searching --- .../FSharp.Editor/Navigation/GoToDefinition.fs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/vsintegration/src/FSharp.Editor/Navigation/GoToDefinition.fs b/vsintegration/src/FSharp.Editor/Navigation/GoToDefinition.fs index 21f26632b0d..7ea1ab017f2 100644 --- a/vsintegration/src/FSharp.Editor/Navigation/GoToDefinition.fs +++ b/vsintegration/src/FSharp.Editor/Navigation/GoToDefinition.fs @@ -786,6 +786,9 @@ type FSharpCrossLanguageSymbolNavigationService() = // 4 - return type, optional, only for methods. let docCommentIdRx = Regex(@"^(\w):([\w\d#`.]+)(\(.+\))?(?:~([\w\d.]+))?$", RegexOptions.Compiled) + // Parse generic args out of the function name + let fnGenericArgsRx = Regex(@"^(.+)``(\d+)$", RegexOptions.Compiled) + let docCommentIdToPath (docId:string) = // docCommentId is in the following format: // @@ -824,6 +827,14 @@ type FSharpCrossLanguageSymbolNavigationService() = let entityPath = parts[..(parts.Length - 2)] |> List.ofArray let memberOrVal = parts[parts.Length - 1] + // Try and parse generic params count from the name (e.g. NameOfTheFunction``1, where ``1 is amount of type parameters) + let genericM = fnGenericArgsRx.Match(memberOrVal) + let (memberOrVal, _genericArgsCount) = + if genericM.Success then + (genericM.Groups[1].Value, int genericM.Groups[2].Value) + else + memberOrVal, 0 + // A hack/fixup for the constructor name (#ctor in doccommentid and ``.ctor`` in F#) if memberOrVal = "#ctor" then DocCommentId.Member ({ EntityPath = entityPath; MemberOrValName = "``.ctor``" }, (SymbolMemberType.FromString "CTOR")) @@ -888,6 +899,12 @@ type FSharpCrossLanguageSymbolNavigationService() = |> Seq.map (fun e -> (e.DeclarationLocation, project)) |> Seq.append locations | None -> () + | DocCommentId.Type entityPath -> + let entity = result.AssemblySignature.FindEntityByPath (entityPath) + match entity with + | Some e -> + locations <- Seq.append locations [e.DeclarationLocation, project] + | None -> () | _ -> () // TODO: Figure out the way of giving the user choice where to navigate, if there are more than one result From 3d8fbfa9f33d3c8aebd2518432813e0e65138777 Mon Sep 17 00:00:00 2001 From: Vlad Zarytovskii Date: Wed, 23 Nov 2022 18:23:11 +0100 Subject: [PATCH 10/17] WIP: Add generic params when filtering for entity --- .../Navigation/GoToDefinition.fs | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/vsintegration/src/FSharp.Editor/Navigation/GoToDefinition.fs b/vsintegration/src/FSharp.Editor/Navigation/GoToDefinition.fs index 7ea1ab017f2..d955c4d6f83 100644 --- a/vsintegration/src/FSharp.Editor/Navigation/GoToDefinition.fs +++ b/vsintegration/src/FSharp.Editor/Navigation/GoToDefinition.fs @@ -741,7 +741,7 @@ type internal SymbolMemberType = | "M" -> Method | _ -> Other -type internal SymbolPath = { EntityPath: string list; MemberOrValName: string; } +type internal SymbolPath = { EntityPath: string list; MemberOrValName: string; GenericParameters: int } [] type internal DocCommentId = @@ -829,7 +829,7 @@ type FSharpCrossLanguageSymbolNavigationService() = // Try and parse generic params count from the name (e.g. NameOfTheFunction``1, where ``1 is amount of type parameters) let genericM = fnGenericArgsRx.Match(memberOrVal) - let (memberOrVal, _genericArgsCount) = + let (memberOrVal, genericParametersCount) = if genericM.Success then (genericM.Groups[1].Value, int genericM.Groups[2].Value) else @@ -837,9 +837,9 @@ type FSharpCrossLanguageSymbolNavigationService() = // A hack/fixup for the constructor name (#ctor in doccommentid and ``.ctor`` in F#) if memberOrVal = "#ctor" then - DocCommentId.Member ({ EntityPath = entityPath; MemberOrValName = "``.ctor``" }, (SymbolMemberType.FromString "CTOR")) + DocCommentId.Member ({ EntityPath = entityPath; MemberOrValName = "``.ctor``"; GenericParameters = 0 }, (SymbolMemberType.FromString "CTOR")) else - DocCommentId.Member ({ EntityPath = entityPath; MemberOrValName = memberOrVal }, (SymbolMemberType.FromString t)) + DocCommentId.Member ({ EntityPath = entityPath; MemberOrValName = memberOrVal; GenericParameters = genericParametersCount }, (SymbolMemberType.FromString t)) | true, "T" -> let entityPath = m.Groups[2].Value.Split('.') |> List.ofArray DocCommentId.Type entityPath @@ -847,10 +847,10 @@ type FSharpCrossLanguageSymbolNavigationService() = let parts = m.Groups[2].Value.Split('.') let entityPath = parts[..(parts.Length - 2)] |> List.ofArray let memberOrVal = parts[parts.Length - 1] - DocCommentId.Field { EntityPath = entityPath; MemberOrValName = memberOrVal } + DocCommentId.Field { EntityPath = entityPath; MemberOrValName = memberOrVal; GenericParameters = 0 } | _ -> DocCommentId.None - let tryFindValByNameAndType (name: string) (symbolMemberType: SymbolMemberType) (e: FSharpEntity) = + let tryFindValByNameAndType (name: string) (symbolMemberType: SymbolMemberType) (genericParametersCount: int) (e: FSharpEntity) = let memberTypePred: (FSharpMemberOrFunctionOrValue -> bool) = match symbolMemberType with | SymbolMemberType.Other @@ -860,7 +860,10 @@ type FSharpCrossLanguageSymbolNavigationService() = | SymbolMemberType.Property -> fun x -> x.IsProperty e.TryGetMembersFunctionsAndValues() - |> Seq.filter (fun x -> x.DisplayName = name) + |> Seq.filter ( + fun x -> + x.DisplayName = name + && x.GenericParameters.Count = genericParametersCount) |> Seq.filter memberTypePred let tryFindFieldByName (name: string) (e: FSharpEntity) = @@ -883,11 +886,11 @@ type FSharpCrossLanguageSymbolNavigationService() = let! result = checker.ParseAndCheckProject(options) match path with - | DocCommentId.Member ({ EntityPath = entityPath; MemberOrValName = memberOrVal }, memberType) -> + | DocCommentId.Member ({ EntityPath = entityPath; MemberOrValName = memberOrVal; GenericParameters = genericParametersCount }, memberType) -> let entity = result.AssemblySignature.FindEntityByPath (entityPath) match entity with | Some e -> - locations <- e |> tryFindValByNameAndType memberOrVal memberType + locations <- e |> tryFindValByNameAndType memberOrVal memberType genericParametersCount |> Seq.map (fun e -> (e.DeclarationLocation, project)) |> Seq.append locations | None -> () From 539bca60d6ac47f8892960ac5bc3f871a56de981 Mon Sep 17 00:00:00 2001 From: Vlad Zarytovskii Date: Mon, 28 Nov 2022 14:13:42 +0100 Subject: [PATCH 11/17] Added operators suport --- .../LanguageService/WorkspaceExtensions.fs | 5 ++--- .../src/FSharp.Editor/Navigation/GoToDefinition.fs | 10 +++++----- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/vsintegration/src/FSharp.Editor/LanguageService/WorkspaceExtensions.fs b/vsintegration/src/FSharp.Editor/LanguageService/WorkspaceExtensions.fs index 74412041c0b..b2fb1792cf5 100644 --- a/vsintegration/src/FSharp.Editor/LanguageService/WorkspaceExtensions.fs +++ b/vsintegration/src/FSharp.Editor/LanguageService/WorkspaceExtensions.fs @@ -247,15 +247,14 @@ type Project with |> RoslynHelpers.StartAsyncAsTask ct } - member this.GetFSharpCompilationOptionsAsync() = - async { + member this.GetFSharpCompilationOptionsAsync(ct: CancellationToken) = + backgroundTask { 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) -> diff --git a/vsintegration/src/FSharp.Editor/Navigation/GoToDefinition.fs b/vsintegration/src/FSharp.Editor/Navigation/GoToDefinition.fs index d955c4d6f83..3a678735a27 100644 --- a/vsintegration/src/FSharp.Editor/Navigation/GoToDefinition.fs +++ b/vsintegration/src/FSharp.Editor/Navigation/GoToDefinition.fs @@ -701,7 +701,7 @@ type internal FSharpNavigation ) |> Task.FromResult - member this.TryGoToDefinition(position, cancellationToken) = + member _.TryGoToDefinition(position, cancellationToken) = let gtd = GoToDefinition(metadataAsSource) let gtdTask = gtd.FindDefinitionTask(initialDoc, position, cancellationToken) @@ -862,7 +862,7 @@ type FSharpCrossLanguageSymbolNavigationService() = e.TryGetMembersFunctionsAndValues() |> Seq.filter ( fun x -> - x.DisplayName = name + (x.DisplayName = name || x.CompiledName = name) && x.GenericParameters.Count = genericParametersCount) |> Seq.filter memberTypePred @@ -876,13 +876,13 @@ type FSharpCrossLanguageSymbolNavigationService() = interface IFSharpCrossLanguageSymbolNavigationService with member _.TryGetNavigableLocationAsync(assemblyName: string, documentationCommentId: string, cancellationToken: CancellationToken) : Task = let path = docCommentIdToPath documentationCommentId - async { + backgroundTask { let projects = workspace.CurrentSolution.Projects |> Seq.filter (fun p -> p.IsFSharp && p.AssemblyName = assemblyName) let mutable locations = Seq.empty for project in projects do - let! checker, _, _, options = project.GetFSharpCompilationOptionsAsync() + let! checker, _, _, options = project.GetFSharpCompilationOptionsAsync(cancellationToken) let! result = checker.ParseAndCheckProject(options) match path with @@ -918,4 +918,4 @@ type FSharpCrossLanguageSymbolNavigationService() = 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 + } From c2ed043fbddb5be5a952d19219a10b3c4753f141 Mon Sep 17 00:00:00 2001 From: Vlad Zarytovskii Date: Mon, 28 Nov 2022 16:35:34 +0100 Subject: [PATCH 12/17] Added DUs and Records support --- .../Navigation/GoToDefinition.fs | 58 +++++++++++++------ 1 file changed, 40 insertions(+), 18 deletions(-) diff --git a/vsintegration/src/FSharp.Editor/Navigation/GoToDefinition.fs b/vsintegration/src/FSharp.Editor/Navigation/GoToDefinition.fs index 3a678735a27..010d31a944a 100644 --- a/vsintegration/src/FSharp.Editor/Navigation/GoToDefinition.fs +++ b/vsintegration/src/FSharp.Editor/Navigation/GoToDefinition.fs @@ -850,29 +850,51 @@ type FSharpCrossLanguageSymbolNavigationService() = DocCommentId.Field { EntityPath = entityPath; MemberOrValName = memberOrVal; GenericParameters = 0 } | _ -> DocCommentId.None - let tryFindValByNameAndType (name: string) (symbolMemberType: SymbolMemberType) (genericParametersCount: int) (e: FSharpEntity) = - let memberTypePred: (FSharpMemberOrFunctionOrValue -> bool) = - match symbolMemberType with - | SymbolMemberType.Other - | SymbolMemberType.Method -> fun _ -> true - | SymbolMemberType.Constructor -> fun x -> x.IsConstructor - | SymbolMemberType.Event // Events are just properties - | SymbolMemberType.Property -> fun x -> x.IsProperty - - e.TryGetMembersFunctionsAndValues() - |> Seq.filter ( - fun x -> - (x.DisplayName = name || x.CompiledName = name) - && x.GenericParameters.Count = genericParametersCount) - |> Seq.filter memberTypePred - let tryFindFieldByName (name: string) (e: FSharpEntity) = e.FSharpFields |> Seq.filter ( fun x -> x.DisplayName = name && not x.IsCompilerGenerated) + |> Seq.map (fun e -> e.DeclarationLocation) + + let tryFindValByNameAndType (name: string) (symbolMemberType: SymbolMemberType) (genericParametersCount: int) (e: FSharpEntity) = + + let entities = e.TryGetMembersFunctionsAndValues() + let defaultFilter (e: FSharpMemberOrFunctionOrValue) = + (e.DisplayName = name || e.CompiledName = name) && e.GenericParameters.Count = genericParametersCount + + let getLocation (e: FSharpMemberOrFunctionOrValue) = e.DeclarationLocation + + let filteredEntities: range seq = + match symbolMemberType with + | SymbolMemberType.Constructor when e.IsFSharpRecord -> + Seq.singleton e.DeclarationLocation + | SymbolMemberType.Other + | SymbolMemberType.Method -> + entities + |> Seq.filter defaultFilter + |> Seq.map getLocation + | SymbolMemberType.Constructor -> + entities + |> Seq.filter (fun x -> defaultFilter x && x.IsConstructor) + |> Seq.map getLocation + // When navigating to property for the record, it will be in members bag for custom ones, but will be in the fields in fields. + | SymbolMemberType.Property when e.IsFSharpRecord -> + let properties = + entities + |> Seq.filter (fun x -> defaultFilter x && x.IsProperty) + |> Seq.map getLocation + let fields = tryFindFieldByName name e + Seq.append properties fields + | SymbolMemberType.Event // Events are just properties + | SymbolMemberType.Property -> + entities + |> Seq.filter (fun x -> defaultFilter x && x.IsProperty) + |> Seq.map getLocation + filteredEntities + interface IFSharpCrossLanguageSymbolNavigationService with member _.TryGetNavigableLocationAsync(assemblyName: string, documentationCommentId: string, cancellationToken: CancellationToken) : Task = let path = docCommentIdToPath documentationCommentId @@ -891,7 +913,7 @@ type FSharpCrossLanguageSymbolNavigationService() = match entity with | Some e -> locations <- e |> tryFindValByNameAndType memberOrVal memberType genericParametersCount - |> Seq.map (fun e -> (e.DeclarationLocation, project)) + |> Seq.map (fun m -> (m, project)) |> Seq.append locations | None -> () | DocCommentId.Field { EntityPath = entityPath; MemberOrValName = memberOrVal } -> @@ -899,7 +921,7 @@ type FSharpCrossLanguageSymbolNavigationService() = match entity with | Some e -> locations <- e |> tryFindFieldByName memberOrVal - |> Seq.map (fun e -> (e.DeclarationLocation, project)) + |> Seq.map (fun m -> (m, project)) |> Seq.append locations | None -> () | DocCommentId.Type entityPath -> From b776347360da3a67ebd27dee3994722f0fd0603f Mon Sep 17 00:00:00 2001 From: Vlad Zarytovskii Date: Mon, 5 Dec 2022 14:07:24 +0100 Subject: [PATCH 13/17] Small refactoring --- .../Navigation/GoToDefinition.fs | 69 ++++++++++--------- 1 file changed, 36 insertions(+), 33 deletions(-) diff --git a/vsintegration/src/FSharp.Editor/Navigation/GoToDefinition.fs b/vsintegration/src/FSharp.Editor/Navigation/GoToDefinition.fs index 010d31a944a..6bbd355f5ac 100644 --- a/vsintegration/src/FSharp.Editor/Navigation/GoToDefinition.fs +++ b/vsintegration/src/FSharp.Editor/Navigation/GoToDefinition.fs @@ -784,10 +784,10 @@ type FSharpCrossLanguageSymbolNavigationService() = // 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) + let docCommentIdRx = Regex(@"^(?\w):(?[\w\d#`.]+)(?\(.+\))?(?:~([\w\d.]+))?$", RegexOptions.Compiled) // Parse generic args out of the function name - let fnGenericArgsRx = Regex(@"^(.+)``(\d+)$", RegexOptions.Compiled) + let fnGenericArgsRx = Regex(@"^(?.+)``(?\d+)$", RegexOptions.Compiled) let docCommentIdToPath (docId:string) = // docCommentId is in the following format: @@ -819,11 +819,11 @@ type FSharpCrossLanguageSymbolNavigationService() = // "P:N.X.prop" - property with getter and setter let m = docCommentIdRx.Match(docId) - let t = m.Groups[1].Value + let t = m.Groups["kind"].Value match m.Success, t with | true, ("M" | "P" | "E") -> // TODO: Probably, there's less janky way of dealing with those. - let parts = m.Groups[2].Value.Split('.') + let parts = m.Groups["entity"].Value.Split('.') let entityPath = parts[..(parts.Length - 2)] |> List.ofArray let memberOrVal = parts[parts.Length - 1] @@ -831,13 +831,13 @@ type FSharpCrossLanguageSymbolNavigationService() = let genericM = fnGenericArgsRx.Match(memberOrVal) let (memberOrVal, genericParametersCount) = if genericM.Success then - (genericM.Groups[1].Value, int genericM.Groups[2].Value) + (genericM.Groups["entity"].Value, int genericM.Groups["typars"].Value) else memberOrVal, 0 // A hack/fixup for the constructor name (#ctor in doccommentid and ``.ctor`` in F#) if memberOrVal = "#ctor" then - DocCommentId.Member ({ EntityPath = entityPath; MemberOrValName = "``.ctor``"; GenericParameters = 0 }, (SymbolMemberType.FromString "CTOR")) + DocCommentId.Member ({ EntityPath = entityPath; MemberOrValName = "``.ctor``"; GenericParameters = 0 },SymbolMemberType.Constructor) else DocCommentId.Member ({ EntityPath = entityPath; MemberOrValName = memberOrVal; GenericParameters = genericParametersCount }, (SymbolMemberType.FromString t)) | true, "T" -> @@ -849,19 +849,25 @@ type FSharpCrossLanguageSymbolNavigationService() = let memberOrVal = parts[parts.Length - 1] DocCommentId.Field { EntityPath = entityPath; MemberOrValName = memberOrVal; GenericParameters = 0 } | _ -> DocCommentId.None - + let tryFindFieldByName (name: string) (e: FSharpEntity) = - e.FSharpFields - |> Seq.filter ( - fun x -> - x.DisplayName = name - && not x.IsCompilerGenerated) - |> Seq.map (fun e -> e.DeclarationLocation) + let fields = + e.FSharpFields + |> Seq.filter ( + fun x -> + x.DisplayName = name + && not x.IsCompilerGenerated) + |> Seq.map (fun e -> e.DeclarationLocation) + + if fields.Count() <= 0 && (e.IsFSharpUnion || e.IsFSharpRecord) then + Seq.singleton e.DeclarationLocation + else + fields let tryFindValByNameAndType (name: string) (symbolMemberType: SymbolMemberType) (genericParametersCount: int) (e: FSharpEntity) = let entities = e.TryGetMembersFunctionsAndValues() - + let defaultFilter (e: FSharpMemberOrFunctionOrValue) = (e.DisplayName = name || e.CompiledName = name) && e.GenericParameters.Count = genericParametersCount @@ -869,18 +875,15 @@ type FSharpCrossLanguageSymbolNavigationService() = let filteredEntities: range seq = match symbolMemberType with - | SymbolMemberType.Constructor when e.IsFSharpRecord -> - Seq.singleton e.DeclarationLocation | SymbolMemberType.Other | SymbolMemberType.Method -> entities |> Seq.filter defaultFilter |> Seq.map getLocation - | SymbolMemberType.Constructor -> - entities - |> Seq.filter (fun x -> defaultFilter x && x.IsConstructor) - |> Seq.map getLocation - // When navigating to property for the record, it will be in members bag for custom ones, but will be in the fields in fields. + // F# record-specific logic, if navigating to the record's ctor, then navigate to record declaration. + // If we navigating to F# record property, we first check if it's "custom" property, if it's one of the record fields, we search for it in the fields. + | SymbolMemberType.Constructor when e.IsFSharpRecord -> + Seq.singleton e.DeclarationLocation | SymbolMemberType.Property when e.IsFSharpRecord -> let properties = entities @@ -888,6 +891,11 @@ type FSharpCrossLanguageSymbolNavigationService() = |> Seq.map getLocation let fields = tryFindFieldByName name e Seq.append properties fields + | SymbolMemberType.Constructor -> + entities + |> Seq.filter (fun x -> defaultFilter x && x.IsConstructor) + |> Seq.map getLocation + // When navigating to property for the record, it will be in members bag for custom ones, but will be in the fields in fields. | SymbolMemberType.Event // Events are just properties | SymbolMemberType.Property -> entities @@ -910,26 +918,21 @@ type FSharpCrossLanguageSymbolNavigationService() = match path with | DocCommentId.Member ({ EntityPath = entityPath; MemberOrValName = memberOrVal; GenericParameters = genericParametersCount }, memberType) -> let entity = result.AssemblySignature.FindEntityByPath (entityPath) - match entity with - | Some e -> + entity |> Option.iter (fun e -> locations <- e |> tryFindValByNameAndType memberOrVal memberType genericParametersCount |> Seq.map (fun m -> (m, project)) - |> Seq.append locations - | None -> () + |> Seq.append locations) + | DocCommentId.Field { EntityPath = entityPath; MemberOrValName = memberOrVal } -> let entity = result.AssemblySignature.FindEntityByPath (entityPath) - match entity with - | Some e -> + entity |> Option.iter (fun e -> locations <- e |> tryFindFieldByName memberOrVal |> Seq.map (fun m -> (m, project)) - |> Seq.append locations - | None -> () + |> Seq.append locations) | DocCommentId.Type entityPath -> let entity = result.AssemblySignature.FindEntityByPath (entityPath) - match entity with - | Some e -> - locations <- Seq.append locations [e.DeclarationLocation, project] - | None -> () + entity |> Option.iter (fun e -> + locations <- Seq.append locations [e.DeclarationLocation, project]) | _ -> () // TODO: Figure out the way of giving the user choice where to navigate, if there are more than one result From 927bc7ac5c0de7cb737ac20b0a4c913fd6966cea Mon Sep 17 00:00:00 2001 From: Vlad Zarytovskii Date: Tue, 27 Dec 2022 19:42:29 +0100 Subject: [PATCH 14/17] PR suggestions + possible short-circuit in search based on the xmlsig --- .../Navigation/GoToDefinition.fs | 180 ++++++++++-------- 1 file changed, 99 insertions(+), 81 deletions(-) diff --git a/vsintegration/src/FSharp.Editor/Navigation/GoToDefinition.fs b/vsintegration/src/FSharp.Editor/Navigation/GoToDefinition.fs index 6bbd355f5ac..c2dbfa6d418 100644 --- a/vsintegration/src/FSharp.Editor/Navigation/GoToDefinition.fs +++ b/vsintegration/src/FSharp.Editor/Navigation/GoToDefinition.fs @@ -93,7 +93,7 @@ module private ExternalSymbol = |> Option.map (fun args -> upcast methsym, FindDeclExternalSymbol.Constructor(fullTypeName, args)) ) |> List.ofSeq - + (symbol, FindDeclExternalSymbol.Type fullTypeName) :: constructors | :? IMethodSymbol as methsym -> @@ -119,9 +119,9 @@ type StatusBar(statusBar: IVsStatusbar) = let _clear() = // unfreeze the statusbar - statusBar.FreezeOutput 0 |> ignore + statusBar.FreezeOutput 0 |> ignore statusBar.Clear() |> ignore - + member _.Message(_msg: string) = () //let _, frozen = statusBar.IsFrozen() @@ -140,11 +140,11 @@ type StatusBar(statusBar: IVsStatusbar) = // | 0, currentText when currentText <> msg -> () // | _ -> clear() //}|> Async.Start - + member _.Clear() = () //clear() /// Animated magnifying glass that displays on the status bar while a symbol search is in progress. - member _.Animate() : IDisposable = + member _.Animate() : IDisposable = //statusBar.Animation (1, &searchIcon) |> ignore { new IDisposable with member _.Dispose() = () } //statusBar.Animation(0, &searchIcon) |> ignore } @@ -158,18 +158,18 @@ type internal FSharpGoToDefinitionResult = | ExternalAssembly of FSharpSymbolUse * MetadataReference seq type internal GoToDefinition(metadataAsSource: FSharpMetadataAsSourceService) = - /// Use an origin document to provide the solution & workspace used to + /// 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) = + let rangeToNavigableItem (range: range, document: Document) = async { let fileName = try System.IO.Path.GetFullPath range.FileName with _ -> range.FileName let refDocumentIds = document.Project.Solution.GetDocumentIdsWithFilePath fileName - if not refDocumentIds.IsEmpty then + if not refDocumentIds.IsEmpty then let refDocumentId = refDocumentIds.First() let refDocument = document.Project.Solution.GetDocument refDocumentId let! cancellationToken = Async.CancellationToken let! refSourceText = refDocument.GetTextAsync(cancellationToken) |> Async.AwaitTask - match RoslynHelpers.TryFSharpRangeToTextSpan (refSourceText, range) with + match RoslynHelpers.TryFSharpRangeToTextSpan (refSourceText, range) with | None -> return None | Some refTextSpan -> return Some (FSharpGoToDefinitionNavigableItem (refDocument, refTextSpan)) else return None @@ -184,7 +184,7 @@ type internal GoToDefinition(metadataAsSource: FSharpMetadataAsSourceService) = let! lexerSymbol = originDocument.TryFindFSharpLexerSymbolAsync(position, SymbolLookupKind.Greedy, false, false, userOpName) let textLinePos = sourceText.Lines.GetLinePosition position let fcsTextLineNumber = Line.fromZ textLinePos.Line - let lineText = (sourceText.Lines.GetLineFromPosition position).ToString() + let lineText = (sourceText.Lines.GetLineFromPosition position).ToString() let idRange = lexerSymbol.Ident.idRange let! _, checkFileResults = originDocument.GetFSharpParseAndCheckResultsAsync(nameof(GoToDefinition)) |> liftAsync @@ -193,14 +193,14 @@ type internal GoToDefinition(metadataAsSource: FSharpMetadataAsSourceService) = // if the tooltip was spawned in an implementation file and we have a range targeting // a signature file, try to find the corresponding implementation file and target the // desired symbol - if isSignatureFile fsSymbolUse.FileName && preferSignature = false then + if isSignatureFile fsSymbolUse.FileName && preferSignature = false then let fsfilePath = Path.ChangeExtension (originRange.FileName,"fs") if not (File.Exists fsfilePath) then return! None else let! implDoc = originDocument.Project.Solution.TryGetDocumentFromPath fsfilePath let! implSourceText = implDoc.GetTextAsync () let! _, checkFileResults = implDoc.GetFSharpParseAndCheckResultsAsync(userOpName) |> liftAsync let symbolUses = checkFileResults.GetUsesOfSymbolInFile symbol - let! implSymbol = symbolUses |> Array.tryHead + let! implSymbol = symbolUses |> Array.tryHead let! implTextSpan = RoslynHelpers.TryFSharpRangeToTextSpan (implSourceText, implSymbol.Range) return FSharpGoToDefinitionNavigableItem (implDoc, implTextSpan) else @@ -208,10 +208,10 @@ type internal GoToDefinition(metadataAsSource: FSharpMetadataAsSourceService) = return! rangeToNavigableItem (fsSymbolUse.Range, targetDocument) } - /// if the symbol is defined in the given file, return its declaration location, otherwise use the targetSymbol to find the first + /// if the symbol is defined in the given file, return its declaration location, otherwise use the targetSymbol to find the first /// instance of its presence in the provided source file. The first case is needed to return proper declaration location for /// recursive type definitions, where the first its usage may not be the declaration. - member _.FindSymbolDeclarationInDocument(targetSymbolUse: FSharpSymbolUse, document: Document) = + member _.FindSymbolDeclarationInDocument(targetSymbolUse: FSharpSymbolUse, document: Document) = asyncMaybe { let filePath = document.FilePath match targetSymbolUse.Symbol.DeclarationLocation with @@ -219,7 +219,7 @@ type internal GoToDefinition(metadataAsSource: FSharpMetadataAsSourceService) = | _ -> let! _, checkFileResults = document.GetFSharpParseAndCheckResultsAsync("FindSymbolDeclarationInDocument") |> liftAsync let symbolUses = checkFileResults.GetUsesOfSymbolInFile targetSymbolUse.Symbol - let! implSymbol = symbolUses |> Array.tryHead + let! implSymbol = symbolUses |> Array.tryHead return implSymbol.Range } @@ -231,10 +231,10 @@ type internal GoToDefinition(metadataAsSource: FSharpMetadataAsSourceService) = let textLinePos = sourceText.Lines.GetLinePosition position let textLineString = textLine.ToString() let fcsTextLineNumber = Line.fromZ textLinePos.Line - let lineText = (sourceText.Lines.GetLineFromPosition position).ToString() - + let lineText = (sourceText.Lines.GetLineFromPosition position).ToString() + let preferSignature = isSignatureFile originDocument.FilePath - + let! lexerSymbol = originDocument.TryFindFSharpLexerSymbolAsync(position, SymbolLookupKind.Greedy, false, false, userOpName) let idRange = lexerSymbol.Ident.idRange @@ -264,10 +264,10 @@ type internal GoToDefinition(metadataAsSource: FSharpMetadataAsSourceService) = let! location = symbol.Locations |> Seq.tryHead return (FSharpGoToDefinitionResult.NavigableItem(FSharpGoToDefinitionNavigableItem(project.GetDocument(location.SourceTree), location.SourceSpan)), idRange) | _ -> - let metadataReferences = originDocument.Project.MetadataReferences + let metadataReferences = originDocument.Project.MetadataReferences return (FSharpGoToDefinitionResult.ExternalAssembly(targetSymbolUse, metadataReferences), idRange) - | FindDeclResult.DeclFound targetRange -> + | FindDeclResult.DeclFound targetRange -> // If the file is not associated with a document, it's considered external. if not (originDocument.Project.Solution.ContainsDocumentWithFilePath(targetRange.FileName)) then let metadataReferences = originDocument.Project.MetadataReferences @@ -281,7 +281,7 @@ type internal GoToDefinition(metadataAsSource: FSharpMetadataAsSourceService) = let implFilePath = Path.ChangeExtension (originDocument.FilePath,"fs") if not (File.Exists implFilePath) then return! None else let! implDocument = originDocument.Project.Solution.TryGetDocumentFromPath implFilePath - + let! targetRange = this.FindSymbolDeclarationInDocument(targetSymbolUse, implDocument) let! implSourceText = implDocument.GetTextAsync(cancellationToken) |> liftTaskAsync let! implTextSpan = RoslynHelpers.TryFSharpRangeToTextSpan (implSourceText, targetRange) @@ -290,7 +290,7 @@ type internal GoToDefinition(metadataAsSource: FSharpMetadataAsSourceService) = else // jump from implementation to the corresponding signature let declarations = checkFileResults.GetDeclarationLocation (fcsTextLineNumber, lexerSymbol.Ident.idRange.EndColumn, textLineString, lexerSymbol.FullIsland, true) match declarations with - | FindDeclResult.DeclFound targetRange -> + | FindDeclResult.DeclFound targetRange -> let! sigDocument = originDocument.Project.Solution.TryGetDocumentFromPath targetRange.FileName let! sigSourceText = sigDocument.GetTextAsync(cancellationToken) |> liftTaskAsync let! sigTextSpan = RoslynHelpers.TryFSharpRangeToTextSpan (sigSourceText, targetRange) @@ -298,28 +298,28 @@ type internal GoToDefinition(metadataAsSource: FSharpMetadataAsSourceService) = return (FSharpGoToDefinitionResult.NavigableItem(navItem), idRange) | _ -> return! None - // when the target range is different follow the navigation convention of + // when the target range is different follow the navigation convention of // - gotoDefn origin = signature , gotoDefn destination = signature - // - gotoDefn origin = implementation, gotoDefn destination = implementation + // - gotoDefn origin = implementation, gotoDefn destination = implementation else let! sigDocument = originDocument.Project.Solution.TryGetDocumentFromPath targetRange.FileName let! sigSourceText = sigDocument.GetTextAsync(cancellationToken) |> liftTaskAsync let! sigTextSpan = RoslynHelpers.TryFSharpRangeToTextSpan (sigSourceText, targetRange) // if the gotodef call originated from a signature and the returned target is a signature, navigate there - if isSignatureFile targetRange.FileName && preferSignature then + if isSignatureFile targetRange.FileName && preferSignature then let navItem = FSharpGoToDefinitionNavigableItem (sigDocument, sigTextSpan) return (FSharpGoToDefinitionResult.NavigableItem(navItem), idRange) else // we need to get an FSharpSymbol from the targetRange found in the signature // that symbol will be used to find the destination in the corresponding implementation file let implFilePath = // Bugfix: apparently sigDocument not always is a signature file - if isSignatureFile sigDocument.FilePath then Path.ChangeExtension (sigDocument.FilePath, "fs") + if isSignatureFile sigDocument.FilePath then Path.ChangeExtension (sigDocument.FilePath, "fs") else sigDocument.FilePath let! implDocument = originDocument.Project.Solution.TryGetDocumentFromPath implFilePath - - let! targetRange = this.FindSymbolDeclarationInDocument(targetSymbolUse, implDocument) - + + let! targetRange = this.FindSymbolDeclarationInDocument(targetSymbolUse, implDocument) + let! implSourceText = implDocument.GetTextAsync () |> liftTaskAsync let! implTextSpan = RoslynHelpers.TryFSharpRangeToTextSpan (implSourceText, targetRange) let navItem = FSharpGoToDefinitionNavigableItem (implDocument, implTextSpan) @@ -328,7 +328,7 @@ type internal GoToDefinition(metadataAsSource: FSharpMetadataAsSourceService) = return! None } - /// find the declaration location (signature file/.fsi) of the target symbol if possible, fall back to definition + /// find the declaration location (signature file/.fsi) of the target symbol if possible, fall back to definition member this.FindDeclarationOfSymbolAtRange(targetDocument: Document, symbolRange: range, targetSource: SourceText) = this.FindSymbolHelper(targetDocument, symbolRange, targetSource, preferSignature=true) @@ -343,7 +343,7 @@ type internal GoToDefinition(metadataAsSource: FSharpMetadataAsSourceService) = >> Array.toSeq) |> RoslynHelpers.StartAsyncAsTask cancellationToken - /// Construct a task that will return a navigation target for the implementation definition of the symbol + /// Construct a task that will return a navigation target for the implementation definition of the symbol /// at the provided position in the document. member this.FindDefinitionTask(originDocument: Document, position: int, cancellationToken: CancellationToken) = this.FindDefinitionAtPosition(originDocument, position, cancellationToken) @@ -357,7 +357,7 @@ type internal GoToDefinition(metadataAsSource: FSharpMetadataAsSourceService) = let navigationService = workspace.Services.GetService() let navigationSucceeded = navigationService.TryNavigateToSpan(workspace, navigableItem.Document.Id, navigableItem.SourceSpan, cancellationToken) - if not navigationSucceeded then + if not navigationSucceeded then statusBar.TempMessage (SR.CannotNavigateUnknown()) member _.NavigateToItem(navigableItem: FSharpNavigableItem, statusBar: StatusBar, cancellationToken: CancellationToken) = @@ -370,13 +370,13 @@ type internal GoToDefinition(metadataAsSource: FSharpMetadataAsSourceService) = // Prefer open documents in the preview tab. let result = navigationService.TryNavigateToSpan(workspace, navigableItem.Document.Id, navigableItem.SourceSpan, cancellationToken) - - if result then + + if result then statusBar.Clear() - else + else statusBar.TempMessage (SR.CannotNavigateUnknown()) - /// Find the declaration location (signature file/.fsi) of the target symbol if possible, fall back to definition + /// Find the declaration location (signature file/.fsi) of the target symbol if possible, fall back to definition member this.NavigateToSymbolDeclarationAsync(targetDocument: Document, targetSourceText: SourceText, symbolRange: range, statusBar: StatusBar, cancellationToken: CancellationToken) = asyncMaybe { let! item = this.FindDeclarationOfSymbolAtRange(targetDocument, symbolRange, targetSourceText) @@ -408,10 +408,10 @@ type internal GoToDefinition(metadataAsSource: FSharpMetadataAsSourceService) = let result = match textOpt with | Some (text, fileName) -> - let tmpProjInfo, tmpDocInfo = + let tmpProjInfo, tmpDocInfo = MetadataAsSource.generateTemporaryDocument( - AssemblyIdentity(targetSymbolUse.Symbol.Assembly.QualifiedName), - fileName, + AssemblyIdentity(targetSymbolUse.Symbol.Assembly.QualifiedName), + fileName, metadataReferences) let tmpShownDocOpt = metadataAsSource.ShowDocument(tmpProjInfo, tmpDocInfo.FilePath, SourceText.From(text.ToString())) match tmpShownDocOpt with @@ -429,7 +429,7 @@ type internal GoToDefinition(metadataAsSource: FSharpMetadataAsSourceService) = ty1.GenericArguments.Count = ty2.GenericArguments.Count && (ty1.GenericArguments, ty2.GenericArguments) ||> Seq.forall2 areTypesEqual - ) + ) if generic then true else @@ -438,7 +438,7 @@ type internal GoToDefinition(metadataAsSource: FSharpMetadataAsSourceService) = namesEqual && accessPathsEqual // This tries to find the best possible location of the target symbol's location in the metadata source. - // We really should rely on symbol equality within FCS instead of doing it here, + // We really should rely on symbol equality within FCS instead of doing it here, // but the generated metadata as source isn't perfect for symbol equality. checkResults.GetAllUsesOfAllSymbolsInFile(cancellationToken) |> Seq.tryFind (fun x -> @@ -472,7 +472,7 @@ type internal GoToDefinition(metadataAsSource: FSharpMetadataAsSourceService) = | Some span -> span | _ -> TextSpan() - return span + return span } let span = @@ -480,7 +480,7 @@ type internal GoToDefinition(metadataAsSource: FSharpMetadataAsSourceService) = | Some span -> span | _ -> TextSpan() - let navItem = FSharpGoToDefinitionNavigableItem(tmpShownDoc, span) + let navItem = FSharpGoToDefinitionNavigableItem(tmpShownDoc, span) this.NavigateToItem(navItem, statusBar, cancellationToken) true | _ -> @@ -488,9 +488,9 @@ type internal GoToDefinition(metadataAsSource: FSharpMetadataAsSourceService) = | _ -> false - if result then + if result then statusBar.Clear() - else + else statusBar.TempMessage (SR.CannotNavigateUnknown()) type internal QuickInfo = @@ -528,7 +528,7 @@ module internal FSharpQuickInfo = let! extLexerSymbol = extDocument.TryFindFSharpLexerSymbolAsync(extSpan.Start, SymbolLookupKind.Greedy, true, true, userOpName) let! _, extCheckFileResults = extDocument.GetFSharpParseAndCheckResultsAsync(userOpName) |> liftAsync - let extQuickInfoText = + let extQuickInfoText = extCheckFileResults.GetToolTip (declRange.StartLine, extLexerSymbol.Ident.idRange.EndColumn, extLineText, extLexerSymbol.FullIsland, FSharpTokenTag.IDENT) @@ -560,7 +560,7 @@ module internal FSharpQuickInfo = let! lexerSymbol = document.TryFindFSharpLexerSymbolAsync(position, SymbolLookupKind.Greedy, true, true, userOpName) let! _, checkFileResults = document.GetFSharpParseAndCheckResultsAsync(userOpName) |> liftAsync let! sourceText = document.GetTextAsync cancellationToken - let idRange = lexerSymbol.Ident.idRange + let idRange = lexerSymbol.Ident.idRange let textLinePos = sourceText.Lines.GetLinePosition position let fcsTextLineNumber = Line.fromZ textLinePos.Line let lineText = (sourceText.Lines.GetLineFromPosition position).ToString() @@ -586,13 +586,13 @@ module internal FSharpQuickInfo = SymbolKind = lexerSymbol.Kind } } - match lexerSymbol.Kind with + match lexerSymbol.Kind with | LexerSymbolKind.Keyword | LexerSymbolKind.String -> let! targetQuickInfo = getTargetSymbolQuickInfo (None, FSharpTokenTag.STRING) return lexerSymbol.Range, None, Some targetQuickInfo - - | _ -> + + | _ -> let! symbolUse = checkFileResults.GetSymbolUseAtLocation (fcsTextLineNumber, idRange.EndColumn, lineText, lexerSymbol.FullIsland) // if the target is in a signature file, adjusting the quick info is unnecessary @@ -608,7 +608,7 @@ module internal FSharpQuickInfo = let! targetQuickInfo = getTargetSymbolQuickInfo (Some symbolUse.Symbol, FSharpTokenTag.IDENT) let! result = - match findSigDeclarationResult with + match findSigDeclarationResult with | FindDeclResult.DeclFound declRange when isSignatureFile declRange.FileName -> asyncMaybe { let! sigQuickInfo = getQuickInfoFromRange(document, declRange, cancellationToken) @@ -668,11 +668,11 @@ type internal FSharpNavigation // To ensure proper navigation decsions, we need to check the type of document the navigation call // is originating from and the target we're provided by default: - // - signature files (.fsi) should navigate to other signature files + // - signature files (.fsi) should navigate to other signature files // - implementation files (.fs) should navigate to other implementation files let (|Signature|Implementation|) filepath = if isSignatureFile filepath then Signature else Implementation - + match initialDoc.FilePath, targetPath with | Signature, Signature | Implementation, Implementation -> @@ -681,7 +681,7 @@ type internal FSharpNavigation // Adjust the target from signature to implementation. | Implementation, Signature -> return! gtd.NavigateToSymbolDefinitionAsync(targetDoc, targetSource, range, statusBar, cancellationToken) - + // Adjust the target from implmentation to signature. | Signature, Implementation -> return! gtd.NavigateToSymbolDeclarationAsync(targetDoc, targetSource, range, statusBar, cancellationToken) @@ -704,7 +704,7 @@ type internal FSharpNavigation member _.TryGoToDefinition(position, cancellationToken) = let gtd = GoToDefinition(metadataAsSource) let gtdTask = gtd.FindDefinitionTask(initialDoc, position, cancellationToken) - + // Wrap this in a try/with as if the user clicks "Cancel" on the thread dialog, we'll be cancelled. // Task.Wait throws an exception if the task is cancelled, so be sure to catch it. try @@ -720,12 +720,12 @@ type internal FSharpNavigation gtd.NavigateToExternalDeclaration(targetSymbolUse, metadataReferences, cancellationToken, statusBar) // 'true' means do it, like Sheev Palpatine would want us to. true - else + else statusBar.TempMessage (SR.CannotDetermineSymbol()) false - with exc -> + with exc -> statusBar.TempMessage(String.Format(SR.NavigateToFailed(), Exception.flattenMessage exc)) - + // Don't show the dialog box as it's most likely that the user cancelled. // Don't make them click twice. true @@ -761,7 +761,7 @@ type FSharpNavigableLocation(statusBar: StatusBar, metadataAsSource: FSharpMetad let (|Signature|Implementation|) filepath = if isSignatureFile filepath then Signature else Implementation - + match targetPath with | Signature -> return! gtd.NavigateToSymbolDefinitionAsync(targetDoc, targetSource, symbolRange, statusBar, cancellationToken) @@ -795,7 +795,7 @@ type FSharpCrossLanguageSymbolNavigationService() = // "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 @@ -811,7 +811,7 @@ type FSharpCrossLanguageSymbolNavigationService() = // "E:" prefix for events // // "E:N.X.d". - // + // // "F:" prefix for fields // "F:N.X.q" - field // @@ -827,7 +827,7 @@ type FSharpCrossLanguageSymbolNavigationService() = let entityPath = parts[..(parts.Length - 2)] |> List.ofArray let memberOrVal = parts[parts.Length - 1] - // Try and parse generic params count from the name (e.g. NameOfTheFunction``1, where ``1 is amount of type parameters) + // Try and parse generic params count from the name (e.g. NameOfTheFunction``1, where ``1 is amount of type parameters) let genericM = fnGenericArgsRx.Match(memberOrVal) let (memberOrVal, genericParametersCount) = if genericM.Success then @@ -841,7 +841,7 @@ type FSharpCrossLanguageSymbolNavigationService() = else DocCommentId.Member ({ EntityPath = entityPath; MemberOrValName = memberOrVal; GenericParameters = genericParametersCount }, (SymbolMemberType.FromString t)) | true, "T" -> - let entityPath = m.Groups[2].Value.Split('.') |> List.ofArray + let entityPath = m.Groups["entity"].Value.Split('.') |> List.ofArray DocCommentId.Type entityPath | true, "F" -> let parts = m.Groups[2].Value.Split('.') @@ -864,16 +864,18 @@ type FSharpCrossLanguageSymbolNavigationService() = else fields - let tryFindValByNameAndType (name: string) (symbolMemberType: SymbolMemberType) (genericParametersCount: int) (e: FSharpEntity) = - - let entities = e.TryGetMembersFunctionsAndValues() + let tryFindValByNameAndType (name: string) (symbolMemberType: SymbolMemberType) (genericParametersCount: int) (e: FSharpEntity) (entities: FSharpMemberOrFunctionOrValue seq) = let defaultFilter (e: FSharpMemberOrFunctionOrValue) = - (e.DisplayName = name || e.CompiledName = name) && e.GenericParameters.Count = genericParametersCount + (e.DisplayName = name || e.CompiledName = name) && + e.GenericParameters.Count = genericParametersCount + + let isProperty (e: FSharpMemberOrFunctionOrValue) = defaultFilter e && e.IsProperty + let isConstructor (e: FSharpMemberOrFunctionOrValue) = defaultFilter e && e.IsConstructor let getLocation (e: FSharpMemberOrFunctionOrValue) = e.DeclarationLocation - let filteredEntities: range seq = + let filteredEntities: range seq = match symbolMemberType with | SymbolMemberType.Other | SymbolMemberType.Method -> @@ -885,30 +887,47 @@ type FSharpCrossLanguageSymbolNavigationService() = | SymbolMemberType.Constructor when e.IsFSharpRecord -> Seq.singleton e.DeclarationLocation | SymbolMemberType.Property when e.IsFSharpRecord -> - let properties = + let properties = entities - |> Seq.filter (fun x -> defaultFilter x && x.IsProperty) + |> Seq.filter isProperty |> Seq.map getLocation let fields = tryFindFieldByName name e Seq.append properties fields | SymbolMemberType.Constructor -> entities - |> Seq.filter (fun x -> defaultFilter x && x.IsConstructor) + |> Seq.filter isConstructor |> Seq.map getLocation // When navigating to property for the record, it will be in members bag for custom ones, but will be in the fields in fields. - | SymbolMemberType.Event // Events are just properties + | SymbolMemberType.Event // Events are just properties` | SymbolMemberType.Property -> entities - |> Seq.filter (fun x -> defaultFilter x && x.IsProperty) + |> Seq.filter isProperty |> Seq.map getLocation + filteredEntities + let tryFindVal (name: string) (documentCommentId: string) (symbolMemberType: SymbolMemberType) (genericParametersCount: int) (e: FSharpEntity) = + let entities = e.TryGetMembersFunctionsAndValues() + + // First, try and find entity by exact xml signature, return if found, + // otherwise, just try and match by parsed name and number of arguments. + + let entitiesByXmlSig = + entities + |> Seq.filter (fun e -> e.XmlDocSig = documentCommentId) + |> Seq.map (fun e -> e.DeclarationLocation) + + if Seq.isEmpty entitiesByXmlSig then + tryFindValByNameAndType name symbolMemberType genericParametersCount e entities + else + entitiesByXmlSig + interface IFSharpCrossLanguageSymbolNavigationService with member _.TryGetNavigableLocationAsync(assemblyName: string, documentationCommentId: string, cancellationToken: CancellationToken) : Task = let path = docCommentIdToPath documentationCommentId backgroundTask { let projects = workspace.CurrentSolution.Projects |> Seq.filter (fun p -> p.IsFSharp && p.AssemblyName = assemblyName) - + let mutable locations = Seq.empty for project in projects do @@ -916,25 +935,24 @@ type FSharpCrossLanguageSymbolNavigationService() = let! result = checker.ParseAndCheckProject(options) match path with - | DocCommentId.Member ({ EntityPath = entityPath; MemberOrValName = memberOrVal; GenericParameters = genericParametersCount }, memberType) -> + | DocCommentId.Member ({ EntityPath = entityPath; MemberOrValName = memberOrVal; GenericParameters = genericParametersCount }, memberType) -> let entity = result.AssemblySignature.FindEntityByPath (entityPath) entity |> Option.iter (fun e -> - locations <- e |> tryFindValByNameAndType memberOrVal memberType genericParametersCount + locations <- e |> tryFindVal memberOrVal documentationCommentId memberType genericParametersCount |> Seq.map (fun m -> (m, project)) |> Seq.append locations) - | DocCommentId.Field { EntityPath = entityPath; MemberOrValName = memberOrVal } -> let entity = result.AssemblySignature.FindEntityByPath (entityPath) entity |> Option.iter (fun e -> - locations <- e |> tryFindFieldByName memberOrVal - |> Seq.map (fun m -> (m, project)) + locations <- e |> tryFindFieldByName memberOrVal + |> Seq.map (fun m -> (m, project)) |> Seq.append locations) | DocCommentId.Type entityPath -> let entity = result.AssemblySignature.FindEntityByPath (entityPath) entity |> Option.iter (fun e -> locations <- Seq.append locations [e.DeclarationLocation, project]) - | _ -> () - + | DocCommentId.None -> () + // 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. From 1a5e25ca7c63dd6a873e394a50fb5b856d42da99 Mon Sep 17 00:00:00 2001 From: Vlad Zarytovskii Date: Wed, 4 Jan 2023 18:57:02 +0100 Subject: [PATCH 15/17] wip --- .../Navigation/GoToDefinition.fs | 143 +++++++++--------- .../GoToDefinitionServiceTests.fs | 18 +++ .../UnitTests/DocCommentIdParserTests.fs | 32 ++++ .../UnitTests/VisualFSharp.UnitTests.fsproj | 3 + 4 files changed, 124 insertions(+), 72 deletions(-) create mode 100644 vsintegration/tests/UnitTests/DocCommentIdParserTests.fs diff --git a/vsintegration/src/FSharp.Editor/Navigation/GoToDefinition.fs b/vsintegration/src/FSharp.Editor/Navigation/GoToDefinition.fs index c2dbfa6d418..83aabd53486 100644 --- a/vsintegration/src/FSharp.Editor/Navigation/GoToDefinition.fs +++ b/vsintegration/src/FSharp.Editor/Navigation/GoToDefinition.fs @@ -779,77 +779,6 @@ type FSharpCrossLanguageSymbolNavigationService() = let statusBar = StatusBar(ServiceProvider.GlobalProvider.GetService()) let metadataAsSource = componentModel.DefaultExportProvider.GetExport().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) - - // Parse generic args out of the function name - let fnGenericArgsRx = Regex(@"^(?.+)``(?\d+)$", RegexOptions.Compiled) - - let docCommentIdToPath (docId:string) = - // 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 - // "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) - let t = m.Groups["kind"].Value - match m.Success, t with - | true, ("M" | "P" | "E") -> - // TODO: Probably, there's less janky way of dealing with those. - let parts = m.Groups["entity"].Value.Split('.') - let entityPath = parts[..(parts.Length - 2)] |> List.ofArray - let memberOrVal = parts[parts.Length - 1] - - // Try and parse generic params count from the name (e.g. NameOfTheFunction``1, where ``1 is amount of type parameters) - let genericM = fnGenericArgsRx.Match(memberOrVal) - let (memberOrVal, genericParametersCount) = - if genericM.Success then - (genericM.Groups["entity"].Value, int genericM.Groups["typars"].Value) - else - memberOrVal, 0 - - // A hack/fixup for the constructor name (#ctor in doccommentid and ``.ctor`` in F#) - if memberOrVal = "#ctor" then - DocCommentId.Member ({ EntityPath = entityPath; MemberOrValName = "``.ctor``"; GenericParameters = 0 },SymbolMemberType.Constructor) - else - DocCommentId.Member ({ EntityPath = entityPath; MemberOrValName = memberOrVal; GenericParameters = genericParametersCount }, (SymbolMemberType.FromString t)) - | true, "T" -> - let entityPath = m.Groups["entity"].Value.Split('.') |> List.ofArray - DocCommentId.Type entityPath - | true, "F" -> - let parts = m.Groups[2].Value.Split('.') - let entityPath = parts[..(parts.Length - 2)] |> List.ofArray - let memberOrVal = parts[parts.Length - 1] - DocCommentId.Field { EntityPath = entityPath; MemberOrValName = memberOrVal; GenericParameters = 0 } - | _ -> DocCommentId.None - let tryFindFieldByName (name: string) (e: FSharpEntity) = let fields = e.FSharpFields @@ -922,9 +851,79 @@ type FSharpCrossLanguageSymbolNavigationService() = else entitiesByXmlSig + static member internal DocCommentIdToPath (docId:string) = + // 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) + + // Parse generic args out of the function name + let fnGenericArgsRx = Regex(@"^(?.+)``(?\d+)$", RegexOptions.Compiled) + // 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 + // "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) + let t = m.Groups["kind"].Value + match m.Success, t with + | true, ("M" | "P" | "E") -> + // TODO: Probably, there's less janky way of dealing with those. + let parts = m.Groups["entity"].Value.Split('.') + let entityPath = parts[..(parts.Length - 2)] |> List.ofArray + let memberOrVal = parts[parts.Length - 1] + + // Try and parse generic params count from the name (e.g. NameOfTheFunction``1, where ``1 is amount of type parameters) + let genericM = fnGenericArgsRx.Match(memberOrVal) + let (memberOrVal, genericParametersCount) = + if genericM.Success then + (genericM.Groups["entity"].Value, int genericM.Groups["typars"].Value) + else + memberOrVal, 0 + + // A hack/fixup for the constructor name (#ctor in doccommentid and ``.ctor`` in F#) + if memberOrVal = "#ctor" then + DocCommentId.Member ({ EntityPath = entityPath; MemberOrValName = "``.ctor``"; GenericParameters = 0 },SymbolMemberType.Constructor) + else + DocCommentId.Member ({ EntityPath = entityPath; MemberOrValName = memberOrVal; GenericParameters = genericParametersCount }, (SymbolMemberType.FromString t)) + | true, "T" -> + let entityPath = m.Groups["entity"].Value.Split('.') |> List.ofArray + DocCommentId.Type entityPath + | true, "F" -> + let parts = m.Groups[2].Value.Split('.') + let entityPath = parts[..(parts.Length - 2)] |> List.ofArray + let memberOrVal = parts[parts.Length - 1] + DocCommentId.Field { EntityPath = entityPath; MemberOrValName = memberOrVal; GenericParameters = 0 } + | _ -> DocCommentId.None + interface IFSharpCrossLanguageSymbolNavigationService with member _.TryGetNavigableLocationAsync(assemblyName: string, documentationCommentId: string, cancellationToken: CancellationToken) : Task = - let path = docCommentIdToPath documentationCommentId + let path = FSharpCrossLanguageSymbolNavigationService.DocCommentIdToPath documentationCommentId backgroundTask { let projects = workspace.CurrentSolution.Projects |> Seq.filter (fun p -> p.IsFSharp && p.AssemblyName = assemblyName) diff --git a/vsintegration/tests/FSharp.Editor.Tests/GoToDefinitionServiceTests.fs b/vsintegration/tests/FSharp.Editor.Tests/GoToDefinitionServiceTests.fs index 2386e432c4d..3399636b80a 100644 --- a/vsintegration/tests/FSharp.Editor.Tests/GoToDefinitionServiceTests.fs +++ b/vsintegration/tests/FSharp.Editor.Tests/GoToDefinitionServiceTests.fs @@ -165,3 +165,21 @@ let f_IWSAM_flex_StaticProperty(x: #IStaticProperty<'T>) = let expected = Some(3, 3, 20, 34) GoToDefinitionTest(fileContents, caretMarker, expected, [| "/langversion:preview" |]) + + + [] + let ``Go to definition by documment comment id smoke test`` () = + let fileContents = + """ +""" + let filePath = Path.GetTempFileName() + ".fs" + File.WriteAllText(filePath, fileContents) + let options = makeOptions filePath opts + + + let document, sourceText = + RoslynTestHelpers.CreateSingleDocumentSolution(filePath, fileContents, options = options) + + let actual = + findDefinition (document, sourceText, caretPosition, []) + |> Option.map (fun range -> (range.StartLine, range.EndLine, range.StartColumn, range.EndColumn)) \ No newline at end of file diff --git a/vsintegration/tests/UnitTests/DocCommentIdParserTests.fs b/vsintegration/tests/UnitTests/DocCommentIdParserTests.fs new file mode 100644 index 00000000000..c0abcf83ed4 --- /dev/null +++ b/vsintegration/tests/UnitTests/DocCommentIdParserTests.fs @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information. +[] +module Tests.ServiceAnalysis.DocCommentIdParser + +open NUnit.Framework +open Microsoft.VisualStudio.FSharp.Editor + + +[] +let ``Test DocCommentId parser``() = + let testData = dict [ + "T:N.X.Nested", 0; + "M:N.X.#ctor", 0; + "M:N.X.#ctor(System.Int32)", 0; + "M:N.X.f", 0; + "M:N.X.bb(System.String,System.Int32@)", 0; + "M:N.X.gg(System.Int16[],System.Int32[0:,0:])", 0; + "M:N.X.op_Addition(N.X,N.X)", 0; + "M:N.X.op_Explicit(N.X)~System.Int32", 0; + "M:N.GenericMethod.WithNestedType``1(N.GenericType{``0}.NestedType)", 0; + "M:N.GenericMethod.WithIntOfNestedType``1(N.GenericType{System.Int32}.NestedType)", 0; + "M:N.X.N#IX{N#KVP{System#String,System#Int32}}#IXA(N.KVP{System.String,System.Int32})", 0; + "E:N.X.d", 0; + "F:N.X.q", 0; + "P:N.X.prop", 0; + ] + + for pair in testData do + let docId = pair.Key + let expected = pair.Value + let actual = FSharpCrossLanguageSymbolNavigationService.DocCommentIdToPath(docId) + printfn $"{docId} = {expected} = %A{actual}" \ No newline at end of file diff --git a/vsintegration/tests/UnitTests/VisualFSharp.UnitTests.fsproj b/vsintegration/tests/UnitTests/VisualFSharp.UnitTests.fsproj index 8bfcdc25ff1..1ee9d2b4a4a 100644 --- a/vsintegration/tests/UnitTests/VisualFSharp.UnitTests.fsproj +++ b/vsintegration/tests/UnitTests/VisualFSharp.UnitTests.fsproj @@ -57,6 +57,9 @@ + + CompilerService\DocCommentIdParserTests.fs + CompilerService\UnusedOpensTests.fs From 3f1b559bd524595446d871f23ddb1d9f9546650f Mon Sep 17 00:00:00 2001 From: Vlad Zarytovskii Date: Thu, 5 Jan 2023 16:11:14 +0100 Subject: [PATCH 16/17] wip --- .../GoToDefinitionServiceTests.fs | 18 ------------------ .../tests/UnitTests/DocCommentIdParserTests.fs | 2 +- 2 files changed, 1 insertion(+), 19 deletions(-) diff --git a/vsintegration/tests/FSharp.Editor.Tests/GoToDefinitionServiceTests.fs b/vsintegration/tests/FSharp.Editor.Tests/GoToDefinitionServiceTests.fs index 3399636b80a..2386e432c4d 100644 --- a/vsintegration/tests/FSharp.Editor.Tests/GoToDefinitionServiceTests.fs +++ b/vsintegration/tests/FSharp.Editor.Tests/GoToDefinitionServiceTests.fs @@ -165,21 +165,3 @@ let f_IWSAM_flex_StaticProperty(x: #IStaticProperty<'T>) = let expected = Some(3, 3, 20, 34) GoToDefinitionTest(fileContents, caretMarker, expected, [| "/langversion:preview" |]) - - - [] - let ``Go to definition by documment comment id smoke test`` () = - let fileContents = - """ -""" - let filePath = Path.GetTempFileName() + ".fs" - File.WriteAllText(filePath, fileContents) - let options = makeOptions filePath opts - - - let document, sourceText = - RoslynTestHelpers.CreateSingleDocumentSolution(filePath, fileContents, options = options) - - let actual = - findDefinition (document, sourceText, caretPosition, []) - |> Option.map (fun range -> (range.StartLine, range.EndLine, range.StartColumn, range.EndColumn)) \ No newline at end of file diff --git a/vsintegration/tests/UnitTests/DocCommentIdParserTests.fs b/vsintegration/tests/UnitTests/DocCommentIdParserTests.fs index c0abcf83ed4..671a6965891 100644 --- a/vsintegration/tests/UnitTests/DocCommentIdParserTests.fs +++ b/vsintegration/tests/UnitTests/DocCommentIdParserTests.fs @@ -29,4 +29,4 @@ let ``Test DocCommentId parser``() = let docId = pair.Key let expected = pair.Value let actual = FSharpCrossLanguageSymbolNavigationService.DocCommentIdToPath(docId) - printfn $"{docId} = {expected} = %A{actual}" \ No newline at end of file + failwith $"{docId} = {expected} = %A{actual}" From 908429830d0052682d79e081ef86553142c2c947 Mon Sep 17 00:00:00 2001 From: Vlad Zarytovskii Date: Thu, 5 Jan 2023 18:18:47 +0100 Subject: [PATCH 17/17] Added tests --- .../Navigation/GoToDefinition.fs | 2 +- .../UnitTests/DocCommentIdParserTests.fs | 37 +++++++++++-------- 2 files changed, 23 insertions(+), 16 deletions(-) diff --git a/vsintegration/src/FSharp.Editor/Navigation/GoToDefinition.fs b/vsintegration/src/FSharp.Editor/Navigation/GoToDefinition.fs index 83aabd53486..bb84e395b8d 100644 --- a/vsintegration/src/FSharp.Editor/Navigation/GoToDefinition.fs +++ b/vsintegration/src/FSharp.Editor/Navigation/GoToDefinition.fs @@ -915,7 +915,7 @@ type FSharpCrossLanguageSymbolNavigationService() = let entityPath = m.Groups["entity"].Value.Split('.') |> List.ofArray DocCommentId.Type entityPath | true, "F" -> - let parts = m.Groups[2].Value.Split('.') + let parts = m.Groups["entity"].Value.Split('.') let entityPath = parts[..(parts.Length - 2)] |> List.ofArray let memberOrVal = parts[parts.Length - 1] DocCommentId.Field { EntityPath = entityPath; MemberOrValName = memberOrVal; GenericParameters = 0 } diff --git a/vsintegration/tests/UnitTests/DocCommentIdParserTests.fs b/vsintegration/tests/UnitTests/DocCommentIdParserTests.fs index 671a6965891..b9cc39f66dc 100644 --- a/vsintegration/tests/UnitTests/DocCommentIdParserTests.fs +++ b/vsintegration/tests/UnitTests/DocCommentIdParserTests.fs @@ -6,27 +6,34 @@ open NUnit.Framework open Microsoft.VisualStudio.FSharp.Editor + + [] let ``Test DocCommentId parser``() = let testData = dict [ - "T:N.X.Nested", 0; - "M:N.X.#ctor", 0; - "M:N.X.#ctor(System.Int32)", 0; - "M:N.X.f", 0; - "M:N.X.bb(System.String,System.Int32@)", 0; - "M:N.X.gg(System.Int16[],System.Int32[0:,0:])", 0; - "M:N.X.op_Addition(N.X,N.X)", 0; - "M:N.X.op_Explicit(N.X)~System.Int32", 0; - "M:N.GenericMethod.WithNestedType``1(N.GenericType{``0}.NestedType)", 0; - "M:N.GenericMethod.WithIntOfNestedType``1(N.GenericType{System.Int32}.NestedType)", 0; - "M:N.X.N#IX{N#KVP{System#String,System#Int32}}#IXA(N.KVP{System.String,System.Int32})", 0; - "E:N.X.d", 0; - "F:N.X.q", 0; - "P:N.X.prop", 0; + "T:N.X.Nested", DocCommentId.Type ["N"; "X"; "Nested"]; + "M:N.X.#ctor", DocCommentId.Member ({ EntityPath = ["N"; "X"]; MemberOrValName = "``.ctor``"; GenericParameters = 0 }, SymbolMemberType.Constructor); + "M:N.X.#ctor(System.Int32)", DocCommentId.Member ({ EntityPath = ["N"; "X"]; MemberOrValName = "``.ctor``"; GenericParameters = 0 }, SymbolMemberType.Constructor); + "M:N.X.f", DocCommentId.Member ({ EntityPath = ["N"; "X"]; MemberOrValName = "f"; GenericParameters = 0 }, SymbolMemberType.Method); + "M:N.X.bb(System.String,System.Int32@)", DocCommentId.Member ({ EntityPath = ["N"; "X"]; MemberOrValName = "bb"; GenericParameters = 0 }, SymbolMemberType.Method); + "M:N.X.gg(System.Int16[],System.Int32[0:,0:])", DocCommentId.Member ({ EntityPath = ["N"; "X"]; MemberOrValName = "gg"; GenericParameters = 0 }, SymbolMemberType.Method); + "M:N.X.op_Addition(N.X,N.X)", DocCommentId.Member ({ EntityPath = ["N"; "X"]; MemberOrValName = "op_Addition"; GenericParameters = 0 }, SymbolMemberType.Method); + "M:N.X.op_Explicit(N.X)~System.Int32", DocCommentId.Member ({ EntityPath = ["N"; "X"]; MemberOrValName = "op_Explicit"; GenericParameters = 0 }, SymbolMemberType.Method); + "M:N.GenericMethod.WithNestedType``1(N.GenericType{``0}.NestedType)", DocCommentId.Member ({ EntityPath = ["N"; "GenericMethod"]; MemberOrValName = "WithNestedType"; GenericParameters = 1 }, SymbolMemberType.Method); + "M:N.GenericMethod.WithIntOfNestedType``1(N.GenericType{System.Int32}.NestedType)", DocCommentId.Member ({ EntityPath = ["N"; "GenericMethod"]; MemberOrValName = "WithIntOfNestedType"; GenericParameters = 1 }, SymbolMemberType.Method); + "E:N.X.d", DocCommentId.Member ({ EntityPath = ["N"; "X"]; MemberOrValName = "d"; GenericParameters = 0 }, SymbolMemberType.Event); + "F:N.X.q", DocCommentId.Field { EntityPath = ["N"; "X"]; MemberOrValName = "q"; GenericParameters = 0 }; + "P:N.X.prop", DocCommentId.Member ({ EntityPath = ["N"; "X"]; MemberOrValName = "prop"; GenericParameters = 0 }, SymbolMemberType.Property); ] + let mutable res = "" for pair in testData do let docId = pair.Key let expected = pair.Value let actual = FSharpCrossLanguageSymbolNavigationService.DocCommentIdToPath(docId) - failwith $"{docId} = {expected} = %A{actual}" + if actual <> expected then + res <- res + $"DocumentId: {docId}; Expected = %A{expected} = Actual = %A{actual}\n" + + if res <> "" then + failwith res + () \ No newline at end of file