diff --git a/src/fsharp/NicePrint.fs b/src/fsharp/NicePrint.fs index 02fec8e3d2795b44df2375c511c6e67b25ced6f8..f39aadc883ad2f156665ca86aeae7e0c9224c1a0 100755 --- a/src/fsharp/NicePrint.fs +++ b/src/fsharp/NicePrint.fs @@ -83,7 +83,10 @@ module internal PrintUtilities = if isAttribute then defaultArg (String.tryDropSuffix name "Attribute") name else name - let tyconTextL = NavigableTaggedText.Create(tagEntityRefName tcref demangled, tcref.DefinitionRange) |> wordL + let tyconTextL = + tagEntityRefName tcref demangled + |> mkNav tcref.DefinitionRange + |> wordL if denv.shortTypeNames then tyconTextL else @@ -1104,8 +1107,9 @@ module private PrintTastMemberOrVals = let stat = PrintTypes.layoutMemberFlags membInfo.MemberFlags let _tps,argInfos,rty,_ = GetTypeOfMemberInFSharpForm denv.g v - let mkNameL niceMethodTypars tagFunction name = - let nameL = DemangleOperatorNameAsLayout tagFunction name + let mkNameL niceMethodTypars tagFunction name = + let nameL = + DemangleOperatorNameAsLayout (tagFunction >> mkNav v.DefinitionRange) name let nameL = if denv.showMemberContainers then layoutTyconRef denv v.MemberApparentParent ^^ SepL.dot ^^ nameL @@ -1157,7 +1161,10 @@ module private PrintTastMemberOrVals = let env = SimplifyTypes.CollectInfo true [tau] cxs let cxs = env.postfixConstraints let argInfos,rty = GetTopTauTypeInFSharpForm denv.g (arityOfVal v).ArgInfos tau v.Range - let nameL = wordL ((if v.IsModuleBinding then tagModuleBinding else tagUnknownEntity) v.DisplayName) + let nameL = + (if v.IsModuleBinding then tagModuleBinding else tagUnknownEntity) v.DisplayName + |> mkNav v.DefinitionRange + |> wordL let nameL = layoutAccessibility denv v.Accessibility nameL let nameL = if v.IsMutable && not denv.suppressMutableKeyword then @@ -1389,7 +1396,7 @@ module private TastDefinitionPrinting = let layoutExtensionMember denv (v:Val) = let tycon = v.MemberApparentParent.Deref - let nameL = wordL (tagMethod tycon.DisplayName) + let nameL = tagMethod tycon.DisplayName |> mkNav v.DefinitionRange |> wordL let nameL = layoutAccessibility denv tycon.Accessibility nameL // "type-accessibility" let tps = match PartitionValTyparsForApparentEnclosingType denv.g v with @@ -1402,7 +1409,10 @@ module private TastDefinitionPrinting = aboveListL (List.map (layoutExtensionMember denv) vs) let layoutRecdField addAccess denv (fld:RecdField) = - let lhs = wordL (tagRecordField fld.Name) + let lhs = + tagRecordField fld.Name + |> mkNav fld.DefinitionRange + |> wordL let lhs = (if addAccess then layoutAccessibility denv fld.Accessibility lhs else lhs) let lhs = if fld.IsMutable then wordL (tagKeyword "mutable") --- lhs else lhs (lhs ^^ RightL.colon) --- layoutType denv fld.FormalType @@ -1426,7 +1436,7 @@ module private TastDefinitionPrinting = sepListL (wordL (tagPunctuation "*")) (List.mapi (layoutUnionOrExceptionField denv isGenerated) fields) let layoutUnionCase denv prefixL (ucase:UnionCase) = - let nmL = DemangleOperatorNameAsLayout tagUnionCase ucase.Id.idText + let nmL = DemangleOperatorNameAsLayout (tagUnionCase >> mkNav ucase.DefinitionRange) ucase.Id.idText //let nmL = layoutAccessibility denv ucase.Accessibility nmL match ucase.RecdFields with | [] -> (prefixL ^^ nmL) @@ -1611,6 +1621,7 @@ module private TastDefinitionPrinting = elif isInterfaceTy g ty then Some "interface", tagInterface n elif isClassTy g ty then (if simplified then None else Some "class" ), tagClass n else None, tagUnknownType n + let name = mkNav tycon.DefinitionRange name let nameL = layoutAccessibility denv tycon.Accessibility (wordL name) let denv = denv.AddAccessibility tycon.Accessibility let lhsL = diff --git a/src/fsharp/TastOps.fs b/src/fsharp/TastOps.fs index e6886ad0be2dc4f7d9737ace071e5a3aa4c90ffc..319c42bc6e4c521ba8824eeb8fad76fd8fa7a8ba 100644 --- a/src/fsharp/TastOps.fs +++ b/src/fsharp/TastOps.fs @@ -2535,11 +2535,16 @@ let tagEntityRefName (xref: EntityRef) name = elif xref.IsRecordTycon then tagRecord name else tagClass name +let fullDisplayTextOfTyconRef r = fullNameOfEntityRef (fun (tc:TyconRef) -> tc.DisplayNameWithStaticParametersAndUnderscoreTypars) r + let fullNameOfEntityRefAsLayout nmF (xref: EntityRef) = - let n = NavigableTaggedText.Create(tagEntityRefName xref (nmF xref), xref.DefinitionRange) |> wordL + let navigableText = + tagEntityRefName xref (nmF xref) + |> mkNav xref.DefinitionRange + |> wordL match fullNameOfParentOfEntityRefAsLayout xref with - | None -> n - | Some pathText -> pathText ^^ SepL.dot ^^ n + | None -> navigableText + | Some pathText -> pathText ^^ SepL.dot ^^ navigableText let fullNameOfParentOfValRef vref = match vref with @@ -2563,7 +2568,6 @@ let fullNameOfParentOfValRefAsLayout vref = let fullDisplayTextOfParentOfModRef r = fullNameOfParentOfEntityRef r let fullDisplayTextOfModRef r = fullNameOfEntityRef (fun (x:EntityRef) -> x.DemangledModuleOrNamespaceName) r -let fullDisplayTextOfTyconRef r = fullNameOfEntityRef (fun (tc:TyconRef) -> tc.DisplayNameWithStaticParametersAndUnderscoreTypars) r let fullDisplayTextOfTyconRefAsLayout r = fullNameOfEntityRefAsLayout (fun (tc:TyconRef) -> tc.DisplayNameWithStaticParametersAndUnderscoreTypars) r let fullDisplayTextOfExnRef r = fullNameOfEntityRef (fun (tc:TyconRef) -> tc.DisplayNameWithStaticParametersAndUnderscoreTypars) r let fullDisplayTextOfExnRefAsLayout r = fullNameOfEntityRefAsLayout (fun (tc:TyconRef) -> tc.DisplayNameWithStaticParametersAndUnderscoreTypars) r diff --git a/src/fsharp/layout.fs b/src/fsharp/layout.fs index 2375bb93f8bac9566a05f7cd47892b18a8a9cec2..68b17e05a7263008e9715ed39b9f90453ce61092 100644 --- a/src/fsharp/layout.fs +++ b/src/fsharp/layout.fs @@ -14,13 +14,12 @@ type layout = Internal.Utilities.StructuredFormat.Layout type LayoutTag = Internal.Utilities.StructuredFormat.LayoutTag type TaggedText = Internal.Utilities.StructuredFormat.TaggedText -type NavigableTaggedText(tag, text, range: Range.range) = +type NavigableTaggedText(taggedText: TaggedText, range: Range.range) = member val Range = range interface TaggedText with - member x.Tag = tag - member x.Text = text - static member Create(tt: TaggedText, range) = - NavigableTaggedText(tt.Tag, tt.Text, range) + member x.Tag = taggedText.Tag + member x.Text = taggedText.Text +let mkNav r t = NavigableTaggedText(t, r) let spaces n = new String(' ',n) diff --git a/src/fsharp/layout.fsi b/src/fsharp/layout.fsi index 513afa868b754b7e6e4d5b0b13e02da69e6cfdac..4b965612eb972e94e0c1f6e6f73fef23698d597a 100644 --- a/src/fsharp/layout.fsi +++ b/src/fsharp/layout.fsi @@ -13,10 +13,10 @@ type LayoutTag = Internal.Utilities.StructuredFormat.LayoutTag type TaggedText = Internal.Utilities.StructuredFormat.TaggedText type NavigableTaggedText = - new : LayoutTag * string * Range.range -> NavigableTaggedText + new : TaggedText * Range.range -> NavigableTaggedText member Range: Range.range - static member Create: TaggedText * Range.range -> NavigableTaggedText interface TaggedText +val mkNav : Range.range -> TaggedText -> NavigableTaggedText module TaggedTextOps = Internal.Utilities.StructuredFormat.TaggedTextOps diff --git a/src/fsharp/vs/ServiceDeclarations.fs b/src/fsharp/vs/ServiceDeclarations.fs index b061af4dd4160fa0c2388cdb9caddc81af014d58..e202463e41040d1933748b099ab3cd2d59a9fa60 100644 --- a/src/fsharp/vs/ServiceDeclarations.fs +++ b/src/fsharp/vs/ServiceDeclarations.fs @@ -834,7 +834,7 @@ module internal ItemDescriptionsImpl = wordL (tagText (FSComp.SR.typeInfoUnionCase())) ^^ NicePrint.layoutTyconRef denv ucinfo.TyconRef ^^ sepL (tagPunctuation ".") ^^ - wordL (tagUnionCase (DecompileOpName uc.Id.idText)) ^^ + wordL (tagUnionCase (DecompileOpName uc.Id.idText) |> mkNav uc.DefinitionRange) ^^ RightL.colon ^^ (if List.isEmpty recd then emptyL else NicePrint.layoutUnionCases denv recd ^^ WordL.arrow) ^^ NicePrint.layoutTy denv rty @@ -845,7 +845,7 @@ module internal ItemDescriptionsImpl = let items = apinfo.ActiveTags let layout = wordL (tagText ((FSComp.SR.typeInfoActivePatternResult()))) ^^ - wordL (tagActivePatternResult (List.item idx items)) ^^ + wordL (tagActivePatternResult (List.item idx items) |> mkNav apinfo.Range) ^^ RightL.colon ^^ NicePrint.layoutTy denv ty FSharpStructuredToolTipElement.Single(layout, xml) @@ -859,7 +859,7 @@ module internal ItemDescriptionsImpl = let _, ptau, _cxs = PrettyTypes.PrettifyTypes1 denv.g tau let layout = wordL (tagText (FSComp.SR.typeInfoActiveRecognizer())) ^^ - wordL (tagActivePatternCase apref.Name) ^^ + wordL (tagActivePatternCase apref.Name |> mkNav v.DefinitionRange) ^^ RightL.colon ^^ NicePrint.layoutTy denv ptau ^^ OutputFullName isDecl pubpath_of_vref fullDisplayTextOfValRefAsLayout v @@ -879,7 +879,7 @@ module internal ItemDescriptionsImpl = let layout = NicePrint.layoutTyconRef denv rfinfo.TyconRef ^^ SepL.dot ^^ - wordL (tagRecordField (DecompileOpName rfield.Name)) ^^ + wordL (tagRecordField (DecompileOpName rfield.Name) |> mkNav rfield.DefinitionRange) ^^ RightL.colon ^^ NicePrint.layoutTy denv ty ^^ ( @@ -1001,7 +1001,9 @@ module internal ItemDescriptionsImpl = let layout = wordL (tagKeyword kind) ^^ - wordL (if definiteNamespace then tagNamespace (fullDisplayTextOfModRef modref) else (tagModule modref.DemangledModuleOrNamespaceName)) + (if definiteNamespace then tagNamespace (fullDisplayTextOfModRef modref) else (tagModule modref.DemangledModuleOrNamespaceName) + |> mkNav modref.DefinitionRange + |> wordL) if not definiteNamespace then let namesToAdd = ([],modrefs) diff --git a/vsintegration/src/FSharp.Editor/Common/CodeAnalysisExtensions.fs b/vsintegration/src/FSharp.Editor/Common/CodeAnalysisExtensions.fs new file mode 100644 index 0000000000000000000000000000000000000000..f240665cab068ff43ec2a754f21f6b072cd7f558 --- /dev/null +++ b/vsintegration/src/FSharp.Editor/Common/CodeAnalysisExtensions.fs @@ -0,0 +1,108 @@ +[] +module internal Microsoft.VisualStudio.FSharp.Editor.CodeAnalysisExtensions + +open Microsoft.CodeAnalysis +open Microsoft.FSharp.Compiler.Range + +type Project with + + /// Returns the projectIds of all projects within the same solution that directly reference this project + member this.GetDependentProjectIds () = + this.Solution.GetProjectDependencyGraph().GetProjectsThatDirectlyDependOnThisProject this.Id + + + /// Returns all projects within the same solution that directly reference this project. + member this.GetDependentProjects () = + this.Solution.GetProjectDependencyGraph().GetProjectsThatDirectlyDependOnThisProject this.Id + |> Seq.map this.Solution.GetProject + + + /// Returns the ProjectIds of all of the projects that this project directly or transitively depneds on + member this.GetProjectIdsOfAllProjectsThisProjectDependsOn () = + let graph = this.Solution.GetProjectDependencyGraph() + let transitiveDependencies = graph.GetProjectsThatThisProjectTransitivelyDependsOn this.Id + let directDependencies = graph.GetProjectsThatThisProjectDirectlyDependsOn this.Id + Seq.append directDependencies transitiveDependencies + + + /// The list all of the projects that this project directly or transitively depneds on + member this.GetAllProjectsThisProjectDependsOn () = + this.GetProjectIdsOfAllProjectsThisProjectDependsOn () + |> Seq.map this.Solution.GetProject + + +type Solution with + + /// Try to get a document inside the solution using the document's name + member self.TryGetDocumentNamed docName = + self.Projects |> Seq.tryPick (fun proj -> + proj.Documents |> Seq.tryFind (fun doc -> doc.Name = docName)) + + + /// Try to find the documentId corresponding to the provided filepath within this solution + member self.TryGetDocumentFromPath filePath = + self.GetDocumentIdsWithFilePath filePath + |> Seq.tryHead |> Option.map (fun docId -> self.GetDocument docId) + + + /// Try to get a project inside the solution using the project's id + member self.TryGetProject (projId:ProjectId) = + if self.ContainsProject projId then Some (self.GetProject projId) else None + + + /// Returns the projectIds of all projects within this solution that directly reference the provided project + member self.GetDependentProjects (projectId:ProjectId) = + self.GetProjectDependencyGraph().GetProjectsThatDirectlyDependOnThisProject projectId + |> Seq.map self.GetProject + + + /// Returns the projectIds of all projects within this solution that directly reference the provided project + member self.GetDependentProjectIds (projectId:ProjectId) = + self.GetProjectDependencyGraph().GetProjectsThatDirectlyDependOnThisProject projectId + + + /// Returns the ProjectIds of all of the projects that directly or transitively depends on + member self.GetProjectIdsOfAllProjectReferences (projectId:ProjectId) = + let graph = self.GetProjectDependencyGraph() + let transitiveDependencies = graph.GetProjectsThatThisProjectTransitivelyDependsOn projectId + let directDependencies = graph.GetProjectsThatThisProjectDirectlyDependsOn projectId + Seq.append directDependencies transitiveDependencies + + + /// Returns all of the projects that this project that directly or transitively depends on + member self.GetAllProjectsThisProjectDependsOn (projectId:ProjectId) = + self.GetProjectIdsOfAllProjectReferences projectId + |> Seq.map self.GetProject + + + /// Try to retrieve the corresponding DocumentId for the range's file in the solution + /// and if a projectId is provided, only try to find the document within that project + /// or a project referenced by that project + member self.TryGetDocumentIdFromFSharpRange (range:range,?projectId:ProjectId) = + + let filePath = System.IO.Path.GetFullPathSafe range.FileName + let checkProjectId (docId:DocumentId) = + if projectId.IsSome then docId.ProjectId = projectId.Value else false + //The same file may be present in many projects. We choose one from current or referenced project. + let rec matchingDoc = function + | [] -> None + | (docId:DocumentId)::_ when checkProjectId docId -> Some docId + | docId::tail -> + match projectId with + | Some projectId -> + if self.GetDependentProjectIds docId.ProjectId |> Seq.contains projectId + then Some docId + else matchingDoc tail + | None -> Some docId + + self.GetDocumentIdsWithFilePath filePath |> List.ofSeq |> matchingDoc + + + /// Try to retrieve the corresponding Document for the range's file in the solution + /// and if a projectId is provided, only try to find the document within that project + /// or a project referenced by that project + member self.TryGetDocumentFromFSharpRange (range:range,?projectId:ProjectId) = + match projectId with + | Some projectId -> self.TryGetDocumentIdFromFSharpRange (range, projectId) + | None -> self.TryGetDocumentIdFromFSharpRange range + |> Option.map self.GetDocument diff --git a/vsintegration/src/FSharp.Editor/Common/CommonConstants.fs b/vsintegration/src/FSharp.Editor/Common/CommonConstants.fs index 2367420403c99336933420cd0f272b37f42a7793..5b1db191019f24ed02169770004792d11c3a7856 100644 --- a/vsintegration/src/FSharp.Editor/Common/CommonConstants.fs +++ b/vsintegration/src/FSharp.Editor/Common/CommonConstants.fs @@ -9,21 +9,46 @@ open Microsoft.CodeAnalysis.Classification [] module internal FSharpCommonConstants = + [] + /// "871D2A70-12A2-4e42-9440-425DD92A4116" let packageGuidString = "871D2A70-12A2-4e42-9440-425DD92A4116" + [] + /// "BC6DD5A5-D4D6-4dab-A00D-A51242DBAF1B" let languageServiceGuidString = "BC6DD5A5-D4D6-4dab-A00D-A51242DBAF1B" + [] + /// "4EB7CCB7-4336-4FFD-B12B-396E9FD079A9" let editorFactoryGuidString = "4EB7CCB7-4336-4FFD-B12B-396E9FD079A9" + [] + /// "9B164E40-C3A2-4363-9BC5-EB4039DEF653" let svsSettingsPersistenceManagerGuidString = "9B164E40-C3A2-4363-9BC5-EB4039DEF653" + [] + /// "F#" let FSharpLanguageName = "F#" + [] + /// "F#" let FSharpContentTypeName = "F#" + [] + /// "F# Signature Help" let FSharpSignatureHelpContentTypeName = "F# Signature Help" + [] + /// "F# Language Service" let FSharpLanguageServiceCallbackName = "F# Language Service" + + [] + /// "FSharp" + let FSharpLanguageLongName = "FSharp" + +[] +module internal FSharpProviderConstants = + [] - let FSharpLanguageLongName = "FSharp" \ No newline at end of file + /// "Session Capturing Quick Info Source Provider" + let SessionCapturingProvider = "Session Capturing Quick Info Source Provider" \ No newline at end of file diff --git a/vsintegration/src/FSharp.Editor/Common/CommonHelpers.fs b/vsintegration/src/FSharp.Editor/Common/CommonHelpers.fs index cd997b85c3a65e92a33633d67d42ecc32e08b790..66776327267c5db5c4cf74487489105791ad7fdc 100644 --- a/vsintegration/src/FSharp.Editor/Common/CommonHelpers.fs +++ b/vsintegration/src/FSharp.Editor/Common/CommonHelpers.fs @@ -410,8 +410,12 @@ module internal Extensions = open Microsoft.VisualStudio.FSharp.Editor.Logging type System.IServiceProvider with + + /// Retrieve a MEF Visual Studio Service of type 'T member x.GetService<'T>() = x.GetService(typeof<'T>) :?> 'T - member x.GetService<'T, 'S>() = x.GetService(typeof<'S>) :?> 'T + + /// Retrieve a SVs MEF Service of type 'S and cast it to type 'T + member x.GetService<'S,'T>() = x.GetService(typeof<'S>) :?> 'T type Path with static member GetFullPathSafe path = @@ -526,7 +530,7 @@ module internal Extensions = match declarationLocation with | Some loc -> let filePath = Path.GetFullPathSafe loc.FileName - let isScript = String.Equals(Path.GetExtension(filePath), ".fsx", StringComparison.OrdinalIgnoreCase) + let isScript = isScriptFile filePath if isScript && filePath = currentDocument.FilePath then Some SymbolDeclarationLocation.CurrentDocument elif isScript then diff --git a/vsintegration/src/FSharp.Editor/Common/CommonRoslynHelpers.fs b/vsintegration/src/FSharp.Editor/Common/CommonRoslynHelpers.fs index 1cc695f6328d2cdfb757f5ba5a6b6b3197493f5b..c88459b4e8e4ede6d2f60fb9f4f28ef898a2ce0b 100644 --- a/vsintegration/src/FSharp.Editor/Common/CommonRoslynHelpers.fs +++ b/vsintegration/src/FSharp.Editor/Common/CommonRoslynHelpers.fs @@ -82,13 +82,6 @@ module internal CommonRoslynHelpers = let CollectTaggedText (list: List<_>) (t:TaggedText) = list.Add(TaggedText(roslynTag t.Tag, t.Text)) - let CollectNavigableText (list: List<_>) (t: TaggedText) = - let rangeOpt = - match t with - | :? NavigableTaggedText as n -> Some n.Range - | _ -> None - list.Add(roslynTag t.Tag, t.Text, rangeOpt) - let StartAsyncAsTask cancellationToken computation = let computation = async { @@ -349,10 +342,3 @@ module internal OpenDeclarationHelper = else sourceText sourceText, minPos |> Option.defaultValue 0 -[] -module internal RoslynExtensions = - type Project with - /// The list of all other projects within the same solution that reference this project. - member this.GetDependentProjects() = - this.Solution.GetProjectDependencyGraph().GetProjectsThatDirectlyDependOnThisProject(this.Id) - |> Seq.map this.Solution.GetProject \ No newline at end of file diff --git a/vsintegration/src/FSharp.Editor/Common/Logging.fs b/vsintegration/src/FSharp.Editor/Common/Logging.fs index e2b8c90d20cedf6867c2b0f1363cf36680b08dd9..cc37f948f42803a756676b5be5268573c917b8c9 100644 --- a/vsintegration/src/FSharp.Editor/Common/Logging.fs +++ b/vsintegration/src/FSharp.Editor/Common/Logging.fs @@ -1,6 +1,7 @@ namespace Microsoft.VisualStudio.FSharp.Editor.Logging open System +open System.Diagnostics open System.ComponentModel.Composition open Microsoft.VisualStudio.Shell open Microsoft.VisualStudio.Shell.Interop @@ -70,6 +71,8 @@ type [] Logger [] [] module Logging = + let inline debug msg = Printf.kprintf Debug.WriteLine msg + let private logger = lazy Logger(Logger.GlobalServiceProvider) let private log logType msg = logger.Value.Log(logType,msg) diff --git a/vsintegration/src/FSharp.Editor/Common/Pervasive.fs b/vsintegration/src/FSharp.Editor/Common/Pervasive.fs index 8aa55cd16fc0ca3ce8282e74304ee78d3d373b3a..fdb0fc85502d0d09b38f016e49706d8c56162a64 100644 --- a/vsintegration/src/FSharp.Editor/Common/Pervasive.fs +++ b/vsintegration/src/FSharp.Editor/Common/Pervasive.fs @@ -2,11 +2,37 @@ module Microsoft.VisualStudio.FSharp.Editor.Pervasive open System +open System.IO +open System.Threading +open System.Threading.Tasks open System.Diagnostics + +/// Checks if the filePath ends with ".fsi" +let isSignatureFile (filePath:string) = + Path.GetExtension filePath = ".fsi" + +/// Checks if the file paht ends with '.fsx' or '.fsscript' +let isScriptFile (filePath:string) = + let ext = Path.GetExtension filePath + String.Equals (ext,".fsi",StringComparison.OrdinalIgnoreCase) || String.Equals (ext,".fsscript",StringComparison.OrdinalIgnoreCase) + +/// Path combination operator +let () path1 path2 = Path.Combine (path1, path2) + + +type Path with + static member GetFullPathSafe path = + try Path.GetFullPath path + with _ -> path + + static member GetFileNameSafe path = + try Path.GetFileName path + with _ -> path + + [] module String = - open System.IO let getLines (str: string) = use reader = new StringReader(str) @@ -178,6 +204,8 @@ let inline liftAsync (computation : Async<'T>) : Async<'T option> = return Some a } +let liftTaskAsync task = task |> Async.AwaitTask |> liftAsync + module Async = let map (f: 'T -> 'U) (a: Async<'T>) : Async<'U> = async { @@ -199,6 +227,40 @@ module Async = } async { return! agent.PostAndAsyncReply id } + +type Async with + + /// Better implementation of Async.AwaitTask that correctly passes the exception of a failed task to the async mechanism + static member AwaitTaskCorrect (task:Task) : Async = + Async.FromContinuations (fun (successCont,exceptionCont,_cancelCont) -> + task.ContinueWith (fun (task:Task) -> + if task.IsFaulted then + let e = task.Exception + if e.InnerExceptions.Count = 1 then + exceptionCont e.InnerExceptions.[0] + else exceptionCont e + elif task.IsCanceled then + exceptionCont(TaskCanceledException ()) + else successCont ()) + |> ignore) + + /// Better implementation of Async.AwaitTask that correctly passes the exception of a failed task to the async mechanism + static member AwaitTaskCorrect (task:'T Task) : Async<'T> = + Async.FromContinuations( fun (successCont,exceptionCont,_cancelCont) -> + task.ContinueWith (fun (task:'T Task) -> + if task.IsFaulted then + let e = task.Exception + if e.InnerExceptions.Count = 1 then + exceptionCont e.InnerExceptions.[0] + else exceptionCont e + elif task.IsCanceled then + exceptionCont (TaskCanceledException ()) + else successCont task.Result) + |> ignore) + static member RunTaskSynchronously task = + task |> Async.AwaitTask |> Async.RunSynchronously + + type AsyncBuilder with member __.Bind(computation: System.Threading.Tasks.Task<'a>, binder: 'a -> Async<'b>): Async<'b> = async { diff --git a/vsintegration/src/FSharp.Editor/Diagnostics/UnusedOpensDiagnosticAnalyzer.fs b/vsintegration/src/FSharp.Editor/Diagnostics/UnusedOpensDiagnosticAnalyzer.fs index f3d2383bb6e31557f6e54b15b06a16d7deae6b7f..4b2b2a84431892ae3d48d2ad13524cd04cc21977 100644 --- a/vsintegration/src/FSharp.Editor/Diagnostics/UnusedOpensDiagnosticAnalyzer.fs +++ b/vsintegration/src/FSharp.Editor/Diagnostics/UnusedOpensDiagnosticAnalyzer.fs @@ -71,7 +71,10 @@ module private UnusedOpens = let symbolIsFullyQualified (sourceText: SourceText) (sym: FSharpSymbolUse) (fullName: string) = match CommonRoslynHelpers.TryFSharpRangeToTextSpan(sourceText, sym.RangeAlternate) with - | Some span -> sourceText.ToString(span) = fullName + | Some span // check that the symbol hasn't provided an invalid span + when sourceText.Length < span.Start + || sourceText.Length < span.End -> false + | Some span -> sourceText.ToString span = fullName | None -> false let getUnusedOpens (sourceText: SourceText) (parsedInput: ParsedInput) (symbolUses: FSharpSymbolUse[]) = @@ -102,7 +105,7 @@ module private UnusedOpens = Some ([apc.FullName], apc.Group.EnclosingEntity) | SymbolUse.UnionCase uc when not (isQualified uc.FullName) -> Some ([uc.FullName], Some uc.ReturnType.TypeDefinition) - | SymbolUse.Parameter p when not (isQualified p.FullName) -> + | SymbolUse.Parameter p when not (isQualified p.FullName) && p.Type.HasTypeDefinition -> Some ([p.FullName], Some p.Type.TypeDefinition) | _ -> None diff --git a/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj b/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj index 7f24a92f3de6aa53531d490544d1b55437e9e5ee..f93de2ab6dfa0edcf21507ba9d7795ed5bc86e3c 100644 --- a/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj +++ b/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj @@ -37,6 +37,7 @@ + @@ -57,13 +58,13 @@ - + @@ -113,6 +114,7 @@ + diff --git a/vsintegration/src/FSharp.Editor/Navigation/GoToDefinitionService.fs b/vsintegration/src/FSharp.Editor/Navigation/GoToDefinitionService.fs index 9857ab1e1d18b99cb9d814ec11df3542f2835ce1..714e3a5481da6ca454e88e981299273fa8dd65e1 100644 --- a/vsintegration/src/FSharp.Editor/Navigation/GoToDefinitionService.fs +++ b/vsintegration/src/FSharp.Editor/Navigation/GoToDefinitionService.fs @@ -2,6 +2,7 @@ namespace Microsoft.VisualStudio.FSharp.Editor +open System.IO open System.Composition open System.Collections.Generic open System.Collections.Immutable @@ -18,6 +19,9 @@ open Microsoft.CodeAnalysis.Text open Microsoft.FSharp.Compiler.Range open Microsoft.FSharp.Compiler.SourceCodeServices +open Microsoft.VisualStudio.FSharp.Editor.Logging +open Microsoft.VisualStudio.Shell +open Microsoft.VisualStudio.Shell.Interop type internal FSharpNavigableItem(document: Document, textSpan: TextSpan) = @@ -30,74 +34,370 @@ type internal FSharpNavigableItem(document: Document, textSpan: TextSpan) = member this.DisplayTaggedParts = ImmutableArray.Empty member this.ChildItems = ImmutableArray.Empty -[] -[, FSharpCommonConstants.FSharpLanguageName)>] -type internal FSharpGoToDefinitionService - [] - ( - checkerProvider: FSharpCheckerProvider, - projectInfoManager: ProjectInfoManager, - []presenters: IEnumerable - ) = - - static member FindDefinition(checker: FSharpChecker, documentKey: DocumentId, sourceText: SourceText, filePath: string, position: int, defines: string list, options: FSharpProjectOptions, textVersionHash: int) : Async> = - asyncMaybe { - let textLine = sourceText.Lines.GetLineFromPosition(position) - let textLinePos = sourceText.Lines.GetLinePosition(position) + +module internal FSharpGoToDefinition = + + /// Parse and check the provided document and try to find the defition of the symbol at the position + // this is only used in Roslyn GotoDefinition calls + let checkAndFindDefinition + (checker: FSharpChecker, documentKey: DocumentId, sourceText: SourceText, filePath: string, position: int, + defines: string list, options: FSharpProjectOptions, preferSignature:bool, textVersionHash: int) = asyncMaybe { + let textLine = sourceText.Lines.GetLineFromPosition position + let textLinePos = sourceText.Lines.GetLinePosition position let fcsTextLineNumber = Line.fromZ textLinePos.Line - let! symbol = CommonHelpers.getSymbolAtPosition(documentKey, sourceText, position, filePath, defines, SymbolLookupKind.Greedy) - let! _, _, checkFileResults = checker.ParseAndCheckDocument(filePath, textVersionHash, sourceText.ToString(), options, allowStaleResults = true) - let! declarations = checkFileResults.GetDeclarationLocationAlternate (fcsTextLineNumber, symbol.Ident.idRange.EndColumn, textLine.ToString(), symbol.FullIsland, false) |> liftAsync - + let! lexerSymbol = CommonHelpers.getSymbolAtPosition(documentKey, sourceText, position, filePath, defines, SymbolLookupKind.Greedy) + let! _, _, checkFileResults = + checker.ParseAndCheckDocument + (filePath, textVersionHash, sourceText.ToString(), options, allowStaleResults = preferSignature) + + let! declarations = + checkFileResults.GetDeclarationLocationAlternate + (fcsTextLineNumber, lexerSymbol.Ident.idRange.EndColumn, textLine.ToString(), lexerSymbol.FullIsland, preferSignature)|>liftAsync + match declarations with - | FSharpFindDeclResult.DeclFound(range) -> return range + | FSharpFindDeclResult.DeclFound range -> + return (lexerSymbol, range,checkFileResults) | _ -> return! None + } + + + + /// 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) = 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 + let refDocumentId = refDocumentIds.First() + let refDocument = document.Project.Solution.GetDocument refDocumentId + let! refSourceText = refDocument.GetTextAsync() + let refTextSpan = CommonRoslynHelpers.FSharpRangeToTextSpan (refSourceText, range) + return Some (FSharpNavigableItem (refDocument, refTextSpan)) + else return None + } + + /// helper function that used to determine the navigation strategy to apply, can be tuned towards signatures or implementation files + let private findSymbolHelper + (originDocument:Document, originRange:range, sourceText:SourceText, preferSignature:bool, checker: FSharpChecker, projectInfoManager: ProjectInfoManager) = + asyncMaybe { + let! projectOptions = projectInfoManager.TryGetOptionsForEditingDocumentOrProject originDocument + let defines = CompilerEnvironment.GetCompilationDefinesForEditing (originDocument.FilePath, projectOptions.OtherOptions |> Seq.toList) + + let originTextSpan = CommonRoslynHelpers.FSharpRangeToTextSpan (sourceText, originRange) + let position = originTextSpan.Start + + let! lexerSymbol = + CommonHelpers.getSymbolAtPosition + (originDocument.Id, sourceText, position, originDocument.FilePath, defines, SymbolLookupKind.Greedy) + + let textLinePos = sourceText.Lines.GetLinePosition position + let fcsTextLineNumber = Line.fromZ textLinePos.Line + let lineText = (sourceText.Lines.GetLineFromPosition position).ToString() + + let! _, _, checkFileResults = + checker.ParseAndCheckDocument (originDocument,projectOptions,allowStaleResults=true,sourceText=sourceText) + let idRange = lexerSymbol.Ident.idRange + + let! fsSymbolUse = checkFileResults.GetSymbolUseAtLocation (fcsTextLineNumber, idRange.EndColumn, lineText, lexerSymbol.FullIsland) + let symbol = fsSymbolUse.Symbol + // 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 + 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! projectOptions = projectInfoManager.TryGetOptionsForEditingDocumentOrProject implDoc + let! _, _, checkFileResults = + checker.ParseAndCheckDocument (implDoc, projectOptions, allowStaleResults=true, sourceText=implSourceText) + + + let! symbolUses = checkFileResults.GetUsesOfSymbolInFile symbol |> liftAsync + let! implSymbol = symbolUses |> Array.tryHead + let implTextSpan = CommonRoslynHelpers.FSharpRangeToTextSpan (implSourceText, implSymbol.RangeAlternate) + return FSharpNavigableItem (implDoc, implTextSpan) + else + let! targetDocument = originDocument.Project.Solution.TryGetDocumentFromFSharpRange fsSymbolUse.RangeAlternate + return! rangeToNavigableItem (fsSymbolUse.RangeAlternate, targetDocument) + } + + /// find the declaration location (signature file/.fsi) of the target symbol if possible, fall back to definition + let findDeclarationOfSymbolAtRange + (targetDocument:Document, symbolRange:range, targetSource:SourceText, checker: FSharpChecker, projectInfoManager: ProjectInfoManager) = + findSymbolHelper (targetDocument, symbolRange, targetSource,true, checker, projectInfoManager) + + + /// find the definition location (implementation file/.fs) of the target symbol + let findDefinitionOfSymbolAtRange + (targetDocument:Document, symbolRange:range, targetSourceText:SourceText, checker: FSharpChecker, projectInfoManager: ProjectInfoManager) = + findSymbolHelper (targetDocument, symbolRange, targetSourceText,false, checker, projectInfoManager) + + + /// use the targetSymbol to find the first instance of its presence in the provided source file + let findSymbolDeclarationInFile + (targetSymbolUse:FSharpSymbolUse, filePath:string, source:string, checker:FSharpChecker, projectOptions:FSharpProjectOptions, fileVersion:int) = + asyncMaybe { + let! (_parseResults, checkFileAnswer) = + checker.ParseAndCheckFileInProject (filePath, fileVersion,source,projectOptions)|> liftAsync //(implDoc, projectOptions, allowStaleResults=true, sourceText=implSourceText) + match checkFileAnswer with + | FSharpCheckFileAnswer.Aborted -> return! None + | FSharpCheckFileAnswer.Succeeded checkFileResults -> + let! symbolUses = checkFileResults.GetUsesOfSymbolInFile targetSymbolUse.Symbol |> liftAsync + let! implSymbol = symbolUses |> Array.tryHead + return implSymbol.RangeAlternate } + + +open FSharpGoToDefinition + +[] +[, FSharpCommonConstants.FSharpLanguageName)>] +[)>] +type internal FSharpGoToDefinitionService [] + (checkerProvider: FSharpCheckerProvider, + projectInfoManager: ProjectInfoManager, + [] presenters: IEnumerable) = + + let serviceProvider = ServiceProvider.GlobalProvider + let statusBar = serviceProvider.GetService() + + let statusBarMessage (msg:string) = + let (_,frozen) = statusBar.IsFrozen() + // unfreeze the status bar + if frozen <> 0 then statusBar.FreezeOutput 0 |> ignore + statusBar.SetText msg |> ignore + // freeze the status bar + statusBar.FreezeOutput 1 |> ignore - // FSROSLYNTODO: Since we are not integrated with the Roslyn project system yet, the below call - // document.Project.Solution.GetDocumentIdsWithFilePath() will only access files in the same project. - // Either Roslyn INavigableItem needs to be extended to allow arbitary full paths, or we need to - // fully integrate with their project system. - member this.FindDefinitionsAsyncAux(document: Document, position: int, cancellationToken: CancellationToken) = + let clearStatusBar () = + // unfreeze the statusbar + statusBar.FreezeOutput 0 |> ignore + statusBar.Clear () |> ignore + + let clearStatusBarAfter (timeoutMs:int) = + Async.Sleep timeoutMs + |> Async.map (fun _ -> clearStatusBar ()) + |> Async.Start + + /// Animated Magnifying glass that displays on the status bar while a symbol search is in progress + let mutable searchIcon = int16 Microsoft.VisualStudio.Shell.Interop.Constants.SBAI_Find :> obj + + let startSearchAnimation () = statusBar.Animation (1, &searchIcon) |> ignore + + let stopSearchAnimation () = statusBar.Animation (0,&searchIcon) |> ignore + + let tryNavigateToItem (navigableItem:#INavigableItem option) = + startSearchAnimation () + statusBarMessage "Trying to locate symbol..." + + match navigableItem with + | Some navigableItem -> + let workspace = navigableItem.Document.Project.Solution.Workspace + let navigationService = workspace.Services.GetService() + // prefer open documents in the preview tab + let options = workspace.Options.WithChangedOption (NavigationOptions.PreferProvisionalTab, true) + let result = navigationService.TryNavigateToSpan (workspace, navigableItem.Document.Id, navigableItem.SourceSpan, options) + stopSearchAnimation () + if result then + clearStatusBar () + result + else + statusBarMessage "Could Not Navigate to Definition of Symbol Under Caret" + clearStatusBarAfter 4000 + result + | None -> + // stop the animation + stopSearchAnimation () + statusBarMessage "Could Not Navigate to Definition of Symbol Under Caret" + clearStatusBarAfter 4000 + true + + + /// Navigate to the positon of the textSpan in the provided document + // used by quickinfo link navigation when the tooltip contains the correct destination range + member this.TryNavigateToTextSpan (document:Document, textSpan:TextSpan) = + let navigableItem = FSharpNavigableItem (document, textSpan) :> INavigableItem + let workspace = document.Project.Solution.Workspace + let navigationService = workspace.Services.GetService() + let options = workspace.Options.WithChangedOption (NavigationOptions.PreferProvisionalTab, true) + let result = navigationService.TryNavigateToSpan (workspace, navigableItem.Document.Id, navigableItem.SourceSpan, options) + if result then true else + statusBarMessage "Could Not Navigate to Definition of Symbol Under Caret" + clearStatusBarAfter 4000 + false + + + /// 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) = async { + + let! navresult = + FSharpGoToDefinition.findDeclarationOfSymbolAtRange + (targetDocument, symbolRange, targetSourceText, checkerProvider.Checker, projectInfoManager) + return tryNavigateToItem navresult + } + + + /// find the definition location (implementation file/.fs) of the target symbol + member this.NavigateToSymbolDefinitionAsync (targetDocument:Document, targetSourceText:SourceText, symbolRange:range)= async{ + let! navresult = + FSharpGoToDefinition.findDefinitionOfSymbolAtRange + (targetDocument, symbolRange, targetSourceText, checkerProvider.Checker, projectInfoManager) + return tryNavigateToItem navresult + } + + + static member FindDefinition + (checker: FSharpChecker, documentKey: DocumentId, sourceText: SourceText, filePath: string, position: int, + defines: string list, options: FSharpProjectOptions, textVersionHash: int) : Option = maybe { + let textLine = sourceText.Lines.GetLineFromPosition position + let textLinePos = sourceText.Lines.GetLinePosition position + let fcsTextLineNumber = Line.fromZ textLinePos.Line + let! lexerSymbol = CommonHelpers.getSymbolAtPosition(documentKey, sourceText, position, filePath, defines, SymbolLookupKind.Greedy) + let! _, _, checkFileResults = + checker.ParseAndCheckDocument + (filePath, textVersionHash, sourceText.ToString(), options, allowStaleResults = true) |> Async.RunSynchronously + + let declarations = + checkFileResults.GetDeclarationLocationAlternate + (fcsTextLineNumber, lexerSymbol.Ident.idRange.EndColumn, textLine.ToString(), lexerSymbol.FullIsland, false) |> Async.RunSynchronously + + match declarations with + | FSharpFindDeclResult.DeclFound range -> return range + | _ -> return! None + } + + + /// 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.FindDefinitionsTask (originDocument:Document, position:int, cancellationToken:CancellationToken) = asyncMaybe { let results = List() - let! options = projectInfoManager.TryGetOptionsForEditingDocumentOrProject(document) - let! sourceText = document.GetTextAsync(cancellationToken) - let! textVersion = document.GetTextVersionAsync(cancellationToken) - let defines = CompilerEnvironment.GetCompilationDefinesForEditing(document.Name, options.OtherOptions |> Seq.toList) - let! range = FSharpGoToDefinitionService.FindDefinition(checkerProvider.Checker, document.Id, sourceText, document.FilePath, position, defines, options, textVersion.GetHashCode()) - // REVIEW: - let fileName = try System.IO.Path.GetFullPath(range.FileName) with _ -> range.FileName - let refDocumentIds = document.Project.Solution.GetDocumentIdsWithFilePath(fileName) - if not refDocumentIds.IsEmpty then - let refDocumentId = refDocumentIds.First() - let refDocument = document.Project.Solution.GetDocument(refDocumentId) - let! refSourceText = refDocument.GetTextAsync(cancellationToken) - let refTextSpan = CommonRoslynHelpers.FSharpRangeToTextSpan(refSourceText, range) - results.Add(FSharpNavigableItem(refDocument, refTextSpan)) - return results.AsEnumerable() - } - |> Async.map (Option.defaultValue Seq.empty) - |> CommonRoslynHelpers.StartAsyncAsTask cancellationToken + let! projectOptions = projectInfoManager.TryGetOptionsForEditingDocumentOrProject originDocument + let! sourceText = originDocument.GetTextAsync () |> liftTaskAsync + let defines = CompilerEnvironment.GetCompilationDefinesForEditing (originDocument.FilePath, projectOptions.OtherOptions |> Seq.toList) + let textLine = sourceText.Lines.GetLineFromPosition position + let textLinePos = sourceText.Lines.GetLinePosition position + let fcsTextLineNumber = Line.fromZ textLinePos.Line + let lineText = (sourceText.Lines.GetLineFromPosition position).ToString() + + let preferSignature = isSignatureFile originDocument.FilePath + let! _, _, checkFileResults = + checkerProvider.Checker.ParseAndCheckDocument (originDocument, projectOptions, allowStaleResults=true, sourceText=sourceText) + + let! lexerSymbol = + CommonHelpers.getSymbolAtPosition + (originDocument.Id, sourceText, position,originDocument.FilePath, defines, SymbolLookupKind.Greedy) + let idRange = lexerSymbol.Ident.idRange + + let! declarations = + checkFileResults.GetDeclarationLocationAlternate + (fcsTextLineNumber, lexerSymbol.Ident.idRange.EndColumn, textLine.ToString(), lexerSymbol.FullIsland, preferSignature) + |> liftAsync + let! targetSymbolUse = + checkFileResults.GetSymbolUseAtLocation (fcsTextLineNumber, idRange.EndColumn, lineText, lexerSymbol.FullIsland) + + match declarations with + | FSharpFindDeclResult.DeclFound targetRange -> + // if goto definition is called at we are alread at the declaration location of a symbol in + // either a signature or an implementation file then we jump to it's respective postion in thethe + if lexerSymbol.Range = targetRange then + // jump from signature to the corresponding implementation + if isSignatureFile originDocument.FilePath then + let implFilePath = Path.ChangeExtension (originDocument.FilePath,"fs") + if not (File.Exists implFilePath) then return! None else + let! implDocument = originDocument.Project.Solution.TryGetDocumentFromPath implFilePath + let! implSourceText = implDocument.GetTextAsync () |> liftTaskAsync + let! implVersion = implDocument.GetTextVersionAsync () |> liftTaskAsync + let! targetRange = + findSymbolDeclarationInFile + (targetSymbolUse, implFilePath, implSourceText.ToString(), checkerProvider.Checker, projectOptions, implVersion.GetHashCode()) + + let implTextSpan = CommonRoslynHelpers.FSharpRangeToTextSpan (implSourceText, targetRange) + let navItem = FSharpNavigableItem (implDocument, implTextSpan) + results.Add navItem + return results.AsEnumerable() + else // jump from implementation to the corresponding signature + let! declarations = + checkFileResults.GetDeclarationLocationAlternate + (fcsTextLineNumber, lexerSymbol.Ident.idRange.EndColumn, textLine.ToString(), lexerSymbol.FullIsland, true) + |> liftAsync + match declarations with + | FSharpFindDeclResult.DeclFound targetRange -> + let! sigDocument = originDocument.Project.Solution.TryGetDocumentFromPath targetRange.FileName + let! sigSourceText = sigDocument.GetTextAsync () |> liftTaskAsync + let sigTextSpan = CommonRoslynHelpers.FSharpRangeToTextSpan (sigSourceText, targetRange) + let navItem = FSharpNavigableItem (sigDocument, sigTextSpan) + results.Add navItem + return results.AsEnumerable() + | _ -> return! None + // when the target range is different follow the navigation convention of + // - gotoDefn origin = signature , gotoDefn destination = signature + // - gotoDefn origin = implementation, gotoDefn destination = implementation + else + let! sigDocument = originDocument.Project.Solution.TryGetDocumentFromPath targetRange.FileName + let! sigSourceText = sigDocument.GetTextAsync () |> liftTaskAsync + let sigTextSpan = CommonRoslynHelpers.FSharpRangeToTextSpan (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 + let navItem = FSharpNavigableItem (sigDocument, sigTextSpan) + results.Add navItem + return results.AsEnumerable() + 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 = Path.ChangeExtension (sigDocument.FilePath,"fs") + let! implDocument = originDocument.Project.Solution.TryGetDocumentFromPath implFilePath + let! implVersion = implDocument.GetTextVersionAsync () |> liftTaskAsync + let! implSourceText = implDocument.GetTextAsync () |> liftTaskAsync + let! projectOptions = projectInfoManager.TryGetOptionsForEditingDocumentOrProject implDocument + let! targetRange = + findSymbolDeclarationInFile + (targetSymbolUse, implFilePath, implSourceText.ToString(), checkerProvider.Checker, projectOptions, implVersion.GetHashCode()) + let implTextSpan = CommonRoslynHelpers.FSharpRangeToTextSpan (implSourceText, targetRange) + let navItem = FSharpNavigableItem (implDocument, implTextSpan) + results.Add navItem + return results.AsEnumerable() + | _ -> return! None + } |> Async.map (Option.defaultValue Seq.empty) + |> CommonRoslynHelpers.StartAsyncAsTask cancellationToken + + interface IGoToDefinitionService with - member this.FindDefinitionsAsync(document: Document, position: int, cancellationToken: CancellationToken) = - this.FindDefinitionsAsyncAux(document, position, cancellationToken) + + // used for 'definition peek' + member this.FindDefinitionsAsync (document: Document, position: int, cancellationToken: CancellationToken) = + this.FindDefinitionsTask (document, position, cancellationToken) + + // used for 'goto definition' proper + /// Try to navigate to the definiton of the symbol at the symbolRange in the originDocument member this.TryGoToDefinition(document: Document, position: int, cancellationToken: CancellationToken) = - let definitionTask = this.FindDefinitionsAsyncAux(document, position, cancellationToken) + let definitionTask = this.FindDefinitionsTask (document, position, cancellationToken) + statusBarMessage "Trying to locate symbol..." + startSearchAnimation () + definitionTask.Wait () + // REVIEW: document this use of a blocking wait on the cancellation token, explaining why it is ok - definitionTask.Wait(cancellationToken) - if definitionTask.Status = TaskStatus.RanToCompletion && definitionTask.Result.Any() then let navigableItem = definitionTask.Result.First() // F# API provides only one INavigableItem let workspace = document.Project.Solution.Workspace let navigationService = workspace.Services.GetService() ignore presenters // prefer open documents in the preview tab - let options = workspace.Options.WithChangedOption(NavigationOptions.PreferProvisionalTab, true) - navigationService.TryNavigateToSpan(workspace, navigableItem.Document.Id, navigableItem.SourceSpan, options) + let options = workspace.Options.WithChangedOption (NavigationOptions.PreferProvisionalTab, true) + let result = navigationService.TryNavigateToSpan (workspace, navigableItem.Document.Id, navigableItem.SourceSpan, options) + + // stop the animation + stopSearchAnimation () + if result then + clearStatusBar () + true // we always return true to prevent the dialog box from appearing + else + statusBarMessage "Could Not Navigate to Definition of Symbol Under Caret" + clearStatusBarAfter 4000 + true // we always return true to prevent the dialog box from appearing // FSROSLYNTODO: potentially display multiple results here // If GotoDef returns one result then it should try to jump to a discovered location. If it returns multiple results then it should use @@ -109,4 +409,8 @@ type internal FSharpGoToDefinitionService // presenter.DisplayResult(navigableItem.DisplayString, definitionTask.Result) //true - else false + else + stopSearchAnimation () + statusBarMessage "Could Not Navigate to Definition of Symbol Under Caret" + clearStatusBarAfter 4000 + true // we always return true to prevent the dialog box from appearing diff --git a/vsintegration/src/FSharp.Editor/QuickInfo/QuickInfoProvider.fs b/vsintegration/src/FSharp.Editor/QuickInfo/QuickInfoProvider.fs index bf0a9eb18d36e0aff5f1ecadaea19293da3e105c..82ea00e5ab4948d26aea266eb1985a49465cf168 100644 --- a/vsintegration/src/FSharp.Editor/QuickInfo/QuickInfoProvider.fs +++ b/vsintegration/src/FSharp.Editor/QuickInfo/QuickInfoProvider.fs @@ -7,6 +7,8 @@ open System.Threading open System.Threading.Tasks open System.Windows open System.Windows.Controls +open System.Windows.Data +open System.Windows.Media open System.ComponentModel.Composition open Microsoft.CodeAnalysis @@ -15,7 +17,6 @@ open Microsoft.CodeAnalysis.Editor open Microsoft.CodeAnalysis.Editor.Shared.Utilities open Microsoft.CodeAnalysis.Editor.Shared.Extensions open Microsoft.CodeAnalysis.Editor.Implementation.IntelliSense.QuickInfo -open Microsoft.CodeAnalysis.Navigation open Microsoft.CodeAnalysis.Text open Microsoft.VisualStudio.FSharp.LanguageService @@ -26,109 +27,172 @@ open Microsoft.VisualStudio.Language.Intellisense open Microsoft.FSharp.Compiler.SourceCodeServices open Microsoft.FSharp.Compiler.Range -open Microsoft.FSharp.Compiler.CompileOps +open Microsoft.FSharp.Compiler +open Internal.Utilities.StructuredFormat open CommonRoslynHelpers +open System.Text -module internal FSharpQuickInfo = - - [] - let SessionCapturingProviderName = "Session Capturing Quick Info Source Provider" - +module private SessionHandling = let mutable currentSession = None - - [)>] - [] - [] - [] - type SourceProviderForCapturingSession() = + + [)>] + [] + [] + [] + type SourceProviderForCapturingSession () = interface IQuickInfoSourceProvider with - member __.TryCreateQuickInfoSource _ = + member x.TryCreateQuickInfoSource _ = { new IQuickInfoSource with - member __.AugmentQuickInfoSession(session,_,_) = currentSession <- Some session - member __.Dispose() = () } - - let fragment(content, typemap: ClassificationTypeMap, thisDoc: Document) = - - let workspace = thisDoc.Project.Solution.Workspace - let documentNavigationService = workspace.Services.GetService() - let solution = workspace.CurrentSolution - - let documentId (range: range) = - let filePath = System.IO.Path.GetFullPathSafe range.FileName - let projectOf (id : DocumentId) = solution.GetDocument(id).Project + member __.AugmentQuickInfoSession(session,_,_) = currentSession <- Some session + member __.Dispose() = () } - //The same file may be present in many projects. We choose one from current or referenced project. - let rec matchingDoc = function - | [] -> None - | id::_ when projectOf id = thisDoc.Project || IsScript thisDoc.FilePath -> Some id - | id::tail -> - if (projectOf id).GetDependentProjects() |> Seq.contains thisDoc.Project then Some id - else matchingDoc tail - solution.GetDocumentIdsWithFilePath(filePath) |> List.ofSeq |> matchingDoc - - let canGoTo range = - range <> rangeStartup && documentId range |> Option.isSome - - let goTo range = - asyncMaybe { - let! id = documentId range - let! src = solution.GetDocument(id).GetTextAsync() - let! span = CommonRoslynHelpers.TryFSharpRangeToTextSpan(src, range) - if documentNavigationService.TryNavigateToSpan(workspace, id, span) then - let! session = currentSession - session.Dismiss() - } |> Async.Ignore |> Async.StartImmediate - - let formatMap = typemap.ClassificationFormatMapService.GetClassificationFormatMap("tooltip") - - let props = - ClassificationTags.GetClassificationTypeName - >> typemap.GetClassificationType - >> formatMap.GetTextProperties - - let inlines = seq { - for (tag, text, rangeOpt) in content do - let run = - match rangeOpt with - | Some(range) when canGoTo range -> - let h = Documents.Hyperlink(Documents.Run(text), ToolTip = range.FileName) - h.Click.Add <| fun _ -> goTo range - h :> Documents.Inline - | _ -> - Documents.Run(text) :> Documents.Inline - DependencyObjectExtensions.SetTextProperties(run, props tag) - yield run - } +type internal SourceLink(run) as this = + inherit Documents.Hyperlink(run) + + let lessOpacity = + { new IValueConverter with + member this.Convert(value, targetType, _, _) = + match value with + | :? Color as c when targetType = typeof -> + // return same color but slightly transparent + Color.FromArgb(70uy, c.R, c.G, c.B) :> _ + | _ -> DependencyProperty.UnsetValue + member this.ConvertBack(_,_,_,_) = DependencyProperty.UnsetValue } + + let underlineBrush = Media.SolidColorBrush() + + do BindingOperations.SetBinding(underlineBrush, SolidColorBrush.ColorProperty, Binding("Foreground.Color", Source = this, Converter = lessOpacity)) |> ignore + + let normalUnderline = TextDecorationCollection [TextDecoration(Location = TextDecorationLocation.Underline, PenOffset = 1.0)] + let slightUnderline = TextDecorationCollection [TextDecoration(Location = TextDecorationLocation.Underline, PenOffset = 1.0, Pen = Pen(Brush = underlineBrush))] + do this.TextDecorations <- slightUnderline - let create() = - let tb = TextBlock(TextWrapping = TextWrapping.Wrap, TextTrimming = TextTrimming.None) - DependencyObjectExtensions.SetDefaultTextProperties(tb, formatMap) - tb.Inlines.AddRange(inlines) - if tb.Inlines.Count = 0 then tb.Visibility <- Visibility.Collapsed - tb :> FrameworkElement - - { new IDeferredQuickInfoContent with member x.Create() = create() } + override this.OnMouseEnter(e) = + base.OnMouseEnter(e) + this.TextDecorations <- normalUnderline + override this.OnMouseLeave(e) = + base.OnMouseLeave(e) + this.TextDecorations <- slightUnderline - let tooltip(symbolGlyph, mainDescription, documentation) = +module private FSharpQuickInfo = + let tooltip (symbolGlyph, mainDescription, documentation) = let empty = { new IDeferredQuickInfoContent with - member x.Create() = TextBlock(Visibility = Visibility.Collapsed) :> FrameworkElement } + member x.Create() = TextBlock(Visibility = Visibility.Collapsed) :> FrameworkElement } - let roslynQuickInfo = QuickInfoDisplayDeferredContent(symbolGlyph, null, mainDescription, documentation, empty, empty, empty, empty) + QuickInfoDisplayDeferredContent(symbolGlyph, null, mainDescription, documentation, empty, empty, empty, empty) - let create() = - let qi = roslynQuickInfo.Create() - let style = Style(typeof) - style.Setters.Add(Setter(Documents.Inline.TextDecorationsProperty, null)) - let trigger = DataTrigger(Binding = Data.Binding("IsMouseOver", Source = qi), Value = true) - trigger.Setters.Add(Setter(Documents.Inline.TextDecorationsProperty, TextDecorations.Underline)) - style.Triggers.Add(trigger) - qi.Resources.Add(typeof, style) - qi + /// Get tooltip combined from doccom of Signature and definition + let getCompoundTooltipInfo (checker: FSharpChecker, position: int, document: Document, projectInfoManager: ProjectInfoManager, cancellationToken) = + asyncMaybe { + let solution = document.Project.Solution + + // when a construct has been declared in a signature file the documentation comments that are + // written in that file are the ones that go into the generated xml when the project is compiled + // therefore we should include these doccoms in our design time tooltips + let getTooltipFromRange (declRange: range) = + asyncMaybe { + // ascertain the location of the target declaration in the signature file + let! extDocId = solution.GetDocumentIdsWithFilePath declRange.FileName |> Seq.tryHead + let extDocument = solution.GetProject(extDocId.ProjectId).GetDocument extDocId + let! extSourceText = extDocument.GetTextAsync cancellationToken + + let extSpan = CommonRoslynHelpers.FSharpRangeToTextSpan (extSourceText, declRange) + let extLineText = (extSourceText.Lines.GetLineFromPosition extSpan.Start).ToString() + + // project options need to be retrieved because the signature file could be in another project + let extProjectOptions = projectInfoManager.TryGetOptionsForProject extDocId.ProjectId |> Option.get + let extDefines = + CompilerEnvironment.GetCompilationDefinesForEditing + (extDocument.FilePath, extProjectOptions.OtherOptions |> Seq.toList) + + let! extLexerSymbol = + CommonHelpers.getSymbolAtPosition + (extDocId, extSourceText, extSpan.Start, declRange.FileName, extDefines, SymbolLookupKind.Greedy) + + let! _, _, extCheckFileResults = + checker.ParseAndCheckDocument (extDocument,extProjectOptions,allowStaleResults=true,sourceText=extSourceText) + + let! extTooltipText = + extCheckFileResults.GetStructuredToolTipTextAlternate + (declRange.StartLine, extLexerSymbol.Ident.idRange.EndColumn, extLineText, extLexerSymbol.FullIsland, FSharpTokenTag.IDENT) |> liftAsync + + match extTooltipText with + | FSharpToolTipText [] + | FSharpToolTipText [FSharpStructuredToolTipElement.None] -> + return! None + | extTooltipText -> + let! extSymbolUse = + extCheckFileResults.GetSymbolUseAtLocation(declRange.StartLine, extLexerSymbol.Ident.idRange.EndColumn, extLineText, extLexerSymbol.FullIsland) + + let extTextSpan = CommonRoslynHelpers.FSharpRangeToTextSpan (extSourceText, extLexerSymbol.Range) + return! Some (extTooltipText, extTextSpan, extSymbolUse, extLexerSymbol.Kind) + } + + let! sourceText = document.GetTextAsync cancellationToken + let! projectOptions = projectInfoManager.TryGetOptionsForEditingDocumentOrProject document + let defines = CompilerEnvironment.GetCompilationDefinesForEditing(document.FilePath, projectOptions.OtherOptions |> Seq.toList) + let! lexerSymbol = CommonHelpers.getSymbolAtPosition(document.Id, sourceText, position, document.FilePath, defines, SymbolLookupKind.Greedy) + let idRange = lexerSymbol.Ident.idRange + let! _, _, checkFileResults = checker.ParseAndCheckDocument(document, projectOptions, allowStaleResults = true, sourceText=sourceText) + let textLinePos = sourceText.Lines.GetLinePosition position + let fcsTextLineNumber = Line.fromZ textLinePos.Line + let lineText = (sourceText.Lines.GetLineFromPosition position).ToString() + + /// Gets the tooltip information for the orignal target + let getTargetSymbolTooltip () = + asyncMaybe { + let! targetTooltip = + checkFileResults.GetStructuredToolTipTextAlternate + (fcsTextLineNumber, idRange.EndColumn, lineText, lexerSymbol.FullIsland, FSharpTokenTag.IDENT) |> liftAsync + + match targetTooltip with + | FSharpToolTipText [] + | FSharpToolTipText [FSharpStructuredToolTipElement.None] -> return! None + | _ -> + let! symbolUse = checkFileResults.GetSymbolUseAtLocation (fcsTextLineNumber, idRange.EndColumn, lineText, lexerSymbol.FullIsland) + let targetTextSpan = CommonRoslynHelpers.FSharpRangeToTextSpan (sourceText, lexerSymbol.Range) + return! Some (targetTooltip, targetTextSpan, symbolUse, lexerSymbol.Kind) + } - { new IDeferredQuickInfoContent with member x.Create() = create() } + // if the target is in a signature file, adjusting the tooltip info is unnecessary + if isSignatureFile document.FilePath then + let! targetTooltipInfo = getTargetSymbolTooltip() + return (None ,Some targetTooltipInfo) + else + // find the declaration location of the target symbol, with a preference for signature files + let! findSigDeclarationResult = + checkFileResults.GetDeclarationLocationAlternate + (idRange.StartLine, idRange.EndColumn, lineText, lexerSymbol.FullIsland, preferFlag=true) |> liftAsync + + // it is necessary to retrieve the backup tooltip info because this acquires + // the textSpan designating where we want the tooltip to appear. + let! backupTooltipInfo & (_, targetTextSpan, _, _) = getTargetSymbolTooltip() + + match findSigDeclarationResult with + | FSharpFindDeclResult.DeclNotFound _failReason -> return None, Some backupTooltipInfo + | FSharpFindDeclResult.DeclFound declRange -> + if isSignatureFile declRange.FileName then + let! sigTooltipInfo = getTooltipFromRange declRange + // if the target was declared in a signature file, and the current file + // is not the corresponding module implementation file for that signature, + // the doccoms from the signature will overwrite any doccoms that might be + // present on the definition/implementation + + let! findImplDefinitionResult = + checkFileResults.GetDeclarationLocationAlternate + (idRange.StartLine, idRange.EndColumn, lineText, lexerSymbol.FullIsland, preferFlag=false) |> liftAsync + + match findImplDefinitionResult with + | FSharpFindDeclResult.DeclNotFound _failReason -> return (Some sigTooltipInfo , None) + | FSharpFindDeclResult.DeclFound declRange -> + let! (implTooltip, _, implSymbol, implLex) = getTooltipFromRange declRange + return (Some sigTooltipInfo, Some (implTooltip, targetTextSpan, implSymbol, implLex)) + else + return (None, Some backupTooltipInfo) + } [] type internal FSharpQuickInfoProvider @@ -138,53 +202,160 @@ type internal FSharpQuickInfoProvider checkerProvider: FSharpCheckerProvider, projectInfoManager: ProjectInfoManager, typeMap: Shared.Utilities.ClassificationTypeMap, + gotoDefinitionService:FSharpGoToDefinitionService, glyphService: IGlyphService ) = + let fragment (content: Layout.TaggedText seq, typemap: ClassificationTypeMap, initialDoc: Document, thisSymbolUseRange: range) : IDeferredQuickInfoContent = + + let workspace = initialDoc.Project.Solution.Workspace + let solution = workspace.CurrentSolution + + let isTargetValid range = + range <> rangeStartup && solution.TryGetDocumentIdFromFSharpRange (range,initialDoc.Project.Id) |> Option.isSome + + let navigateTo (range:range) = + asyncMaybe { + let targetPath = range.FileName + let! targetDoc = solution.TryGetDocumentFromFSharpRange (range,initialDoc.Project.Id) + let! targetSource = targetDoc.GetTextAsync() + let! targetTextSpan = CommonRoslynHelpers.TryFSharpRangeToTextSpan (targetSource, range) + // 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 + // - 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 -> + return (gotoDefinitionService.TryNavigateToTextSpan (targetDoc, targetTextSpan)) + // adjust the target from signature to implementation + | Implementation, Signature -> + return! gotoDefinitionService.NavigateToSymbolDefinitionAsync (targetDoc, targetSource, range)|>liftAsync + // adjust the target from implmentation to signature + | Signature, Implementation -> + return! gotoDefinitionService.NavigateToSymbolDeclarationAsync (targetDoc, targetSource, range)|>liftAsync + } |> Async.map (Option.map (fun res -> + if res then + SessionHandling.currentSession + |> Option.iter (fun session -> session.Dismiss ()) + )) |> Async.Ignore |> Async.StartImmediate + + let formatMap = typemap.ClassificationFormatMapService.GetClassificationFormatMap "tooltip" + + let layoutTagToFormatting (layoutTag: LayoutTag) = + layoutTag + |> roslynTag + |> ClassificationTags.GetClassificationTypeName + |> typemap.GetClassificationType + |> formatMap.GetTextProperties + + let inlines = seq { + for taggedText in content do + let run = Documents.Run taggedText.Text + let inl = + match taggedText with + | :? Layout.NavigableTaggedText as nav when thisSymbolUseRange <> nav.Range && isTargetValid nav.Range -> + let h = SourceLink (run, ToolTip = nav.Range.FileName) + h.Click.Add <| fun _ -> navigateTo nav.Range + h :> Documents.Inline + | _ -> run :> _ + DependencyObjectExtensions.SetTextProperties (inl, layoutTagToFormatting taggedText.Tag) + yield inl + } + + let createTextLinks () = + let tb = TextBlock(TextWrapping = TextWrapping.Wrap, TextTrimming = TextTrimming.None) + DependencyObjectExtensions.SetDefaultTextProperties(tb, formatMap) + tb.Inlines.AddRange inlines + if tb.Inlines.Count = 0 then tb.Visibility <- Visibility.Collapsed + tb :> FrameworkElement + + { new IDeferredQuickInfoContent with member x.Create () = createTextLinks () } + let xmlMemberIndexService = serviceProvider.GetService(typeof) :?> IVsXMLMemberIndexService let documentationBuilder = XmlDocumentation.CreateDocumentationBuilder(xmlMemberIndexService, serviceProvider.DTE) static member ProvideQuickInfo(checker: FSharpChecker, documentId: DocumentId, sourceText: SourceText, filePath: string, position: int, options: FSharpProjectOptions, textVersionHash: int) = asyncMaybe { - let! _, _, checkFileResults = checker.ParseAndCheckDocument(filePath, textVersionHash, sourceText.ToString(), options, allowStaleResults = true) - let textLine = sourceText.Lines.GetLineFromPosition(position) + let! _, _, checkFileResults = checker.ParseAndCheckDocument (filePath, textVersionHash, sourceText.ToString(), options, allowStaleResults = true) + let textLine = sourceText.Lines.GetLineFromPosition position let textLineNumber = textLine.LineNumber + 1 // Roslyn line numbers are zero-based - let defines = CompilerEnvironment.GetCompilationDefinesForEditing(filePath, options.OtherOptions |> Seq.toList) - let! symbol = CommonHelpers.getSymbolAtPosition(documentId, sourceText, position, filePath, defines, SymbolLookupKind.Precise) - let! res = checkFileResults.GetStructuredToolTipTextAlternate(textLineNumber, symbol.Ident.idRange.EndColumn, textLine.ToString(), symbol.FullIsland, FSharpTokenTag.IDENT) |> liftAsync + let defines = CompilerEnvironment.GetCompilationDefinesForEditing (filePath, options.OtherOptions |> Seq.toList) + let! symbol = CommonHelpers.getSymbolAtPosition (documentId, sourceText, position, filePath, defines, SymbolLookupKind.Precise) + let! res = checkFileResults.GetStructuredToolTipTextAlternate (textLineNumber, symbol.Ident.idRange.EndColumn, textLine.ToString(), symbol.FullIsland, FSharpTokenTag.IDENT) |> liftAsync match res with | FSharpToolTipText [] | FSharpToolTipText [FSharpStructuredToolTipElement.None] -> return! None | _ -> - let! symbolUse = checkFileResults.GetSymbolUseAtLocation(textLineNumber, symbol.Ident.idRange.EndColumn, textLine.ToString(), symbol.FullIsland) - return! Some(res, CommonRoslynHelpers.FSharpRangeToTextSpan(sourceText, symbol.Range), symbolUse.Symbol, symbol.Kind) + let! symbolUse = checkFileResults.GetSymbolUseAtLocation (textLineNumber, symbol.Ident.idRange.EndColumn, textLine.ToString(), symbol.FullIsland) + return! Some (res, CommonRoslynHelpers.FSharpRangeToTextSpan (sourceText, symbol.Range), symbolUse.Symbol, symbol.Kind) } interface IQuickInfoProvider with override this.GetItemAsync(document: Document, position: int, cancellationToken: CancellationToken): Task = asyncMaybe { - let! sourceText = document.GetTextAsync(cancellationToken) - let defines = projectInfoManager.GetCompilationDefinesForEditingDocument(document) - let! _ = CommonHelpers.getSymbolAtPosition(document.Id, sourceText, position, document.FilePath, defines, SymbolLookupKind.Precise) - let! options = projectInfoManager.TryGetOptionsForEditingDocumentOrProject(document) - let! textVersion = document.GetTextVersionAsync(cancellationToken) - let! toolTipElement, textSpan, symbol, symbolKind = - FSharpQuickInfoProvider.ProvideQuickInfo(checkerProvider.Checker, document.Id, sourceText, document.FilePath, position, options, textVersion.GetHashCode()) - let mainDescription = Collections.Generic.List() - let documentation = Collections.Generic.List() - XmlDocumentation.BuildDataTipText( - documentationBuilder, - CommonRoslynHelpers.CollectNavigableText mainDescription, - CommonRoslynHelpers.CollectNavigableText documentation, - toolTipElement) - let content = - FSharpQuickInfo.tooltip - ( - SymbolGlyphDeferredContent(CommonRoslynHelpers.GetGlyphForSymbol(symbol, symbolKind), glyphService), - FSharpQuickInfo.fragment(mainDescription, typeMap, document), - FSharpQuickInfo.fragment(documentation, typeMap, document) - ) - return QuickInfoItem(textSpan, content) - } - |> Async.map Option.toObj - |> CommonRoslynHelpers.StartAsyncAsTask(cancellationToken) + let! sigTooltipInfo, targetTooltipInfo = + FSharpQuickInfo.getCompoundTooltipInfo(checkerProvider.Checker, position, document, projectInfoManager, cancellationToken) + + match sigTooltipInfo, targetTooltipInfo with + | None, None -> return null + | Some (toolTipElement, textSpan, symbolUse, symbolKind), None + | None, Some (toolTipElement, textSpan, symbolUse, symbolKind) -> + let mainDescription = Collections.Generic.List () + let documentation = Collections.Generic.List () + XmlDocumentation.BuildDataTipText(documentationBuilder, mainDescription.Add, documentation.Add, toolTipElement) + let content = + FSharpQuickInfo.tooltip + (SymbolGlyphDeferredContent(CommonRoslynHelpers.GetGlyphForSymbol(symbolUse.Symbol, symbolKind), glyphService), + fragment (mainDescription, typeMap, document, symbolUse.RangeAlternate), + fragment (documentation, typeMap, document, symbolUse.RangeAlternate)) + return QuickInfoItem (textSpan, content) + + | Some (sigToolTipElement, _, _, _), Some (targetToolTipElement, targetTextSpan, targetSymbolUse, targetSymbolKind) -> + let description, targetDocumentation, sigDocumentation = ResizeArray(), ResizeArray(), ResizeArray() + XmlDocumentation.BuildDataTipText(documentationBuilder, ignore, sigDocumentation.Add, sigToolTipElement) + XmlDocumentation.BuildDataTipText(documentationBuilder, description.Add, targetDocumentation.Add, targetToolTipElement) + + let width = + description + |> Seq.append targetDocumentation + |> Seq.append sigDocumentation + |> Seq.map (fun x -> x.Text.Length) + |> Seq.max + + let seperator = TaggedTextOps.tag LayoutTag.Text (String.replicate width "-") + let lineBreak = TaggedTextOps.tag LayoutTag.LineBreak "\n" + + // get whitespace nomalized documentation text + let getText (tts: seq) = + ((StringBuilder(), tts) ||> Seq.fold (fun sb tt -> + if String.IsNullOrWhiteSpace tt.Text then sb else sb.Append tt.Text)).ToString() + + let documentation = + let implText, sigText = getText targetDocumentation, getText sigDocumentation + let implDocsEmpty, sigDocsEmpty = String.IsNullOrWhiteSpace implText, String.IsNullOrWhiteSpace sigText + + [ match implDocsEmpty, sigDocsEmpty with + | true, true -> () + | true, false -> yield! sigDocumentation + | false, true -> yield! targetDocumentation + | false, false when implText.Equals (sigText, StringComparison.OrdinalIgnoreCase) -> yield! sigDocumentation + | false, false -> + yield! sigDocumentation + yield lineBreak + yield seperator + yield lineBreak + yield! targetDocumentation ] + + let content = + FSharpQuickInfo.tooltip + (SymbolGlyphDeferredContent (CommonRoslynHelpers.GetGlyphForSymbol (targetSymbolUse.Symbol, targetSymbolKind), glyphService), + fragment (description, typeMap, document, targetSymbolUse.RangeAlternate), + fragment (documentation, typeMap, document, targetSymbolUse.RangeAlternate)) + + return QuickInfoItem (targetTextSpan, content) + } |> Async.map Option.toObj + |> CommonRoslynHelpers.StartAsyncAsTask cancellationToken diff --git a/vsintegration/tests/unittests/GoToDefinitionServiceTests.fs b/vsintegration/tests/unittests/GoToDefinitionServiceTests.fs index 25a2f7134e6911a8b971f3008467bd5e7f0094e9..32c90f8aa90e525fa0792e32baa11dcb56fe1676 100644 --- a/vsintegration/tests/unittests/GoToDefinitionServiceTests.fs +++ b/vsintegration/tests/unittests/GoToDefinitionServiceTests.fs @@ -101,7 +101,6 @@ let _ = Module1.foo 1 let documentId = DocumentId.CreateNewId(ProjectId.CreateNewId()) let actual = FSharpGoToDefinitionService.FindDefinition(FSharpChecker.Instance, documentId, SourceText.From(fileContents), filePath, caretPosition, [], options, 0) - |> Async.RunSynchronously |> Option.map (fun range -> (range.StartLine, range.EndLine, range.StartColumn, range.EndColumn)) if actual <> expected then