diff --git a/src/Compiler/Service/FSharpCheckerResults.fs b/src/Compiler/Service/FSharpCheckerResults.fs index b29d26f9944..7582e109849 100644 --- a/src/Compiler/Service/FSharpCheckerResults.fs +++ b/src/Compiler/Service/FSharpCheckerResults.fs @@ -1726,7 +1726,7 @@ type internal TypeCheckInfo } let toolTipElement = - FormatStructuredDescriptionOfItem displayFullName infoReader accessorDomain m denv itemWithInst None + FormatStructuredDescriptionOfItem displayFullName infoReader accessorDomain m denv itemWithInst (Some symbol) None ToolTipText [ toolTipElement ] @@ -1757,7 +1757,8 @@ type internal TypeCheckInfo ToolTipText( items |> List.map (fun x -> - FormatStructuredDescriptionOfItem false infoReader tcAccessRights m denv x.ItemWithInst width) + let symbol = Some(FSharpSymbol.Create(cenv, x.Item)) + FormatStructuredDescriptionOfItem false infoReader tcAccessRights m denv x.ItemWithInst symbol width) )) (fun err -> @@ -2716,12 +2717,7 @@ type FSharpCheckFileResults | None -> () | Some kwDescription -> let kwText = kw |> TaggedText.tagKeyword |> wordL |> LayoutRender.toArray - let kwTip = ToolTipElementData.Create(kwText, FSharpXmlDoc.None) - - let descText = kwDescription |> TaggedText.tagText |> wordL |> LayoutRender.toArray - let descTip = ToolTipElementData.Create(descText, FSharpXmlDoc.None) - - yield ToolTipElement.Group [ kwTip; descTip ] + yield ToolTipElement.Single(kwText, FSharpXmlDoc.FromXmlText(Xml.XmlDoc([| kwDescription |], range.Zero))) ] /// Resolve the names at the given location to give a data tip diff --git a/src/Compiler/Service/ServiceDeclarationLists.fs b/src/Compiler/Service/ServiceDeclarationLists.fs index c062ecec290..c6546c4f021 100644 --- a/src/Compiler/Service/ServiceDeclarationLists.fs +++ b/src/Compiler/Service/ServiceDeclarationLists.fs @@ -34,14 +34,16 @@ open FSharp.Compiler.TypedTreeOps /// A single data tip display element [] type ToolTipElementData = - { MainDescription: TaggedText[] + { + Symbol: FSharpSymbol option + MainDescription: TaggedText[] XmlDoc: FSharpXmlDoc TypeMapping: TaggedText[] list Remarks: TaggedText[] option ParamName : string option } - static member internal Create(layout, xml, ?typeMapping, ?paramName, ?remarks) = - { MainDescription=layout; XmlDoc=xml; TypeMapping=defaultArg typeMapping []; ParamName=paramName; Remarks=remarks } + static member internal Create(layout, xml, ?typeMapping, ?paramName, ?remarks, ?symbol) = + { MainDescription=layout; XmlDoc=xml; TypeMapping=defaultArg typeMapping []; ParamName=paramName; Remarks=remarks; Symbol = symbol } /// A single data tip display element [] @@ -54,8 +56,8 @@ type ToolTipElement = /// An error occurred formatting this element | CompositionError of errorText: string - static member Single(layout, xml, ?typeMapping, ?paramName, ?remarks) = - Group [ ToolTipElementData.Create(layout, xml, ?typeMapping=typeMapping, ?paramName=paramName, ?remarks=remarks) ] + static member Single(layout, xml, ?typeMapping, ?paramName, ?remarks, ?symbol) = + Group [ ToolTipElementData.Create(layout, xml, ?typeMapping=typeMapping, ?paramName=paramName, ?remarks=remarks, ?symbol = symbol) ] /// Information for building a data tip box. type ToolTipText = @@ -93,7 +95,7 @@ module DeclarationListHelpers = let emptyToolTip = ToolTipText [] /// Generate the structured tooltip for a method info - let FormatOverloadsToList (infoReader: InfoReader) m denv (item: ItemWithInst) minfos (width: int option) : ToolTipElement = + let FormatOverloadsToList (infoReader: InfoReader) m denv (item: ItemWithInst) minfos symbol (width: int option) : ToolTipElement = ToolTipFault |> Option.iter (fun msg -> let exn = Error((0, msg), range.Zero) let ph = PhasedDiagnostic.Create(exn, BuildPhase.TypeCheck) @@ -107,7 +109,7 @@ module DeclarationListHelpers = let layout = PrintUtilities.squashToWidth width layout let layout = toArray layout let tpsL = List.map toArray tpsL - ToolTipElementData.Create(layout, xml, tpsL) ] + ToolTipElementData.Create(layout, xml, tpsL, ?symbol = symbol) ] ToolTipElement.Group layouts @@ -149,15 +151,16 @@ module DeclarationListHelpers = let pubpathOfTyconRef (x: TyconRef) = x.PublicPath /// Output the quick info information of a language item - let rec FormatItemDescriptionToToolTipElement displayFullName (infoReader: InfoReader) ad m denv (item: ItemWithInst) (width: int option) = + let rec FormatItemDescriptionToToolTipElement displayFullName (infoReader: InfoReader) ad m denv (item: ItemWithInst) symbol (width: int option) = let g = infoReader.g let amap = infoReader.amap let denv = SimplerDisplayEnv denv let xml = GetXmlCommentForItem infoReader m item.Item + match item.Item with | Item.ImplicitOp(_, { contents = Some(TraitConstraintSln.FSMethSln(vref=vref)) }) -> // operator with solution - FormatItemDescriptionToToolTipElement displayFullName infoReader ad m denv { item with Item = Item.Value vref } width + FormatItemDescriptionToToolTipElement displayFullName infoReader ad m denv { item with Item = Item.Value vref } symbol width | Item.Value vref | Item.CustomBuilder (_, vref) -> let prettyTyparInst, resL = NicePrint.layoutQualifiedValOrMember denv infoReader item.TyparInstantiation vref @@ -167,7 +170,7 @@ module DeclarationListHelpers = let resL = PrintUtilities.squashToWidth width resL let resL = toArray resL let remarks = toArray remarks - ToolTipElement.Single(resL, xml, tpsL, remarks=remarks) + ToolTipElement.Single(resL, xml, tpsL, remarks=remarks, ?symbol = symbol) // Union tags (constructors) | Item.UnionCase(ucinfo, _) -> @@ -184,7 +187,7 @@ module DeclarationListHelpers = NicePrint.layoutType denv unionTy let layout = PrintUtilities.squashToWidth width layout let layout = toArray layout - ToolTipElement.Single (layout, xml) + ToolTipElement.Single (layout, xml, ?symbol = symbol) // Active pattern tag inside the declaration (result) | Item.ActivePatternResult(apinfo, ty, idx, _) -> @@ -196,7 +199,7 @@ module DeclarationListHelpers = NicePrint.layoutType denv ty let layout = PrintUtilities.squashToWidth width layout let layout = toArray layout - ToolTipElement.Single (layout, xml) + ToolTipElement.Single (layout, xml, ?symbol = symbol) // Active pattern tags | Item.ActivePatternCase apref -> @@ -217,7 +220,7 @@ module DeclarationListHelpers = let layout = toArray layout let tpsL = List.map toArray tpsL let remarks = toArray remarks - ToolTipElement.Single (layout, xml, tpsL, remarks=remarks) + ToolTipElement.Single (layout, xml, tpsL, remarks=remarks, ?symbol = symbol) // F# exception names | Item.ExnCase ecref -> @@ -226,7 +229,7 @@ module DeclarationListHelpers = let remarks = OutputFullName displayFullName pubpathOfTyconRef fullDisplayTextOfExnRefAsLayout ecref let layout = toArray layout let remarks = toArray remarks - ToolTipElement.Single (layout, xml, remarks=remarks) + ToolTipElement.Single (layout, xml, remarks=remarks, ?symbol = symbol) | Item.RecdField rfinfo when rfinfo.TyconRef.IsFSharpException -> let ty, _ = PrettyTypes.PrettifyType g rfinfo.FieldType @@ -238,7 +241,7 @@ module DeclarationListHelpers = NicePrint.layoutType denv ty let layout = PrintUtilities.squashToWidth width layout let layout = toArray layout - ToolTipElement.Single (layout, xml, paramName = id) + ToolTipElement.Single (layout, xml, paramName = id, ?symbol = symbol) // F# record field names | Item.RecdField rfinfo -> @@ -257,7 +260,7 @@ module DeclarationListHelpers = ) let layout = PrintUtilities.squashToWidth width layout let layout = toArray layout - ToolTipElement.Single (layout, xml) + ToolTipElement.Single (layout, xml, ?symbol = symbol) | Item.UnionCaseField (ucinfo, fieldIndex) -> let rfield = ucinfo.UnionCase.GetFieldByIndex(fieldIndex) @@ -270,7 +273,7 @@ module DeclarationListHelpers = NicePrint.layoutType denv fieldTy let layout = PrintUtilities.squashToWidth width layout let layout = toArray layout - ToolTipElement.Single (layout, xml, paramName = id.idText) + ToolTipElement.Single (layout, xml, paramName = id.idText, ?symbol = symbol) // Not used | Item.NewDef id -> @@ -279,7 +282,7 @@ module DeclarationListHelpers = wordL (tagUnknownEntity id.idText) let layout = PrintUtilities.squashToWidth width layout let layout = toArray layout - ToolTipElement.Single (layout, xml) + ToolTipElement.Single (layout, xml, ?symbol = symbol) // .NET fields | Item.ILField finfo -> @@ -299,7 +302,7 @@ module DeclarationListHelpers = ) let layout = PrintUtilities.squashToWidth width layout let layout = toArray layout - ToolTipElement.Single (layout, xml) + ToolTipElement.Single (layout, xml, ?symbol = symbol) // .NET events | Item.Event einfo -> @@ -314,14 +317,14 @@ module DeclarationListHelpers = NicePrint.layoutType denv eventTy let layout = PrintUtilities.squashToWidth width layout let layout = toArray layout - ToolTipElement.Single (layout, xml) + ToolTipElement.Single (layout, xml, ?symbol = symbol) // F# and .NET properties | Item.Property(_, pinfo :: _) -> let layout = NicePrint.prettyLayoutOfPropInfoFreeStyle g amap m denv pinfo let layout = PrintUtilities.squashToWidth width layout let layout = toArray layout - ToolTipElement.Single (layout, xml) + ToolTipElement.Single (layout, xml, ?symbol = symbol) // Custom operations in queries | Item.CustomOperation (customOpName, usageText, Some minfo) -> @@ -348,12 +351,12 @@ module DeclarationListHelpers = let layout = PrintUtilities.squashToWidth width layout let layout = toArray layout - ToolTipElement.Single (layout, xml) + ToolTipElement.Single (layout, xml, ?symbol = symbol) // F# constructors and methods | Item.CtorGroup(_, minfos) | Item.MethodGroup(_, minfos, _) -> - FormatOverloadsToList infoReader m denv item minfos width + FormatOverloadsToList infoReader m denv item minfos symbol width // The 'fake' zero-argument constructors of .NET interfaces. // This ideally should never appear in intellisense, but we do get here in repros like: @@ -365,7 +368,7 @@ module DeclarationListHelpers = let layout = NicePrint.layoutTyconRef denv (tcrefOfAppTy g ty) let layout = PrintUtilities.squashToWidth width layout let layout = toArray layout - ToolTipElement.Single(layout, xml) + ToolTipElement.Single(layout, xml, ?symbol = symbol) // The 'fake' representation of constructors of .NET delegate types | Item.DelegateCtor delTy -> @@ -378,7 +381,7 @@ module DeclarationListHelpers = RightL.rightParen let layout = PrintUtilities.squashToWidth width layout let layout = toArray layout - ToolTipElement.Single(layout, xml) + ToolTipElement.Single(layout, xml, ?symbol = symbol) // Types. | Item.Types(_, TType_app(tcref, _, _) :: _) @@ -395,20 +398,20 @@ module DeclarationListHelpers = let remarks = OutputFullName displayFullName pubpathOfTyconRef fullDisplayTextOfTyconRefAsLayout tcref let layout = toArray layout let remarks = toArray remarks - ToolTipElement.Single (layout, xml, remarks=remarks) + ToolTipElement.Single (layout, xml, remarks=remarks, ?symbol = symbol) // Type variables | Item.TypeVar (_, typar) -> let layout = NicePrint.prettyLayoutOfTypar denv typar let layout = PrintUtilities.squashToWidth width layout - ToolTipElement.Single (toArray layout, xml) + ToolTipElement.Single (toArray layout, xml, ?symbol = symbol) // Traits | Item.Trait traitInfo -> let denv = { denv with shortConstraints = false} let layout = NicePrint.prettyLayoutOfTrait denv traitInfo let layout = PrintUtilities.squashToWidth width layout - ToolTipElement.Single (toArray layout, xml) + ToolTipElement.Single (toArray layout, xml, ?symbol = symbol) // F# Modules and namespaces | Item.ModuleOrNamespaces(modref :: _ as modrefs) -> @@ -449,11 +452,11 @@ module DeclarationListHelpers = ) let layout = PrintUtilities.squashToWidth width layout let layout = toArray layout - ToolTipElement.Single (layout, xml) + ToolTipElement.Single (layout, xml, ?symbol = symbol) else let layout = PrintUtilities.squashToWidth width layout let layout = toArray layout - ToolTipElement.Single (layout, xml) + ToolTipElement.Single (layout, xml, ?symbol = symbol) | Item.AnonRecdField(anon, argTys, i, _) -> let argTy = argTys[i] @@ -466,7 +469,7 @@ module DeclarationListHelpers = NicePrint.layoutType denv argTy let layout = PrintUtilities.squashToWidth width layout let layout = toArray layout - ToolTipElement.Single (layout, FSharpXmlDoc.None) + ToolTipElement.Single (layout, FSharpXmlDoc.None, ?symbol = symbol) // Named parameters | Item.ArgName (Some id, argTy, _, _) -> @@ -478,10 +481,10 @@ module DeclarationListHelpers = NicePrint.layoutType denv argTy let layout = PrintUtilities.squashToWidth width layout let layout = toArray layout - ToolTipElement.Single (layout, xml, paramName = id.idText) + ToolTipElement.Single (layout, xml, paramName = id.idText, ?symbol = symbol) | Item.SetterArg (_, item) -> - FormatItemDescriptionToToolTipElement displayFullName infoReader ad m denv (ItemWithNoInst item) width + FormatItemDescriptionToToolTipElement displayFullName infoReader ad m denv (ItemWithNoInst item) symbol width | Item.ArgName (None, _, _, _) @@ -523,9 +526,9 @@ module DeclarationListHelpers = | Item.CustomOperation (_, _, None) -> ToolTipElement.None /// Format the structured version of a tooltip for an item - let FormatStructuredDescriptionOfItem isDecl infoReader ad m denv item width = + let FormatStructuredDescriptionOfItem isDecl infoReader ad m denv item symbol width = DiagnosticsScope.Protect m - (fun () -> FormatItemDescriptionToToolTipElement isDecl infoReader ad m denv item width) + (fun () -> FormatItemDescriptionToToolTipElement isDecl infoReader ad m denv item symbol width) (fun err -> ToolTipElement.CompositionError err) /// Represents one parameter for one method (or other item) in a group. @@ -1034,7 +1037,7 @@ type DeclarationListItem(textInDeclList: string, textInCode: string, fullName: s member _.Description = match info with | Choice1Of2 (items: CompletionItem list, infoReader, ad, m, denv) -> - ToolTipText(items |> List.map (fun x -> FormatStructuredDescriptionOfItem true infoReader ad m denv x.ItemWithInst None)) + ToolTipText(items |> List.map (fun x -> FormatStructuredDescriptionOfItem true infoReader ad m denv x.ItemWithInst None None)) | Choice2Of2 result -> result @@ -1297,7 +1300,7 @@ type MethodGroup( name: string, unsortedMethods: MethodGroupItem[] ) = (fun () -> PrettyParamsAndReturnTypeOfItem infoReader m denv { item with Item = flatItem }) (fun err -> [], wordL (tagText err)) - let description = ToolTipText [FormatStructuredDescriptionOfItem true infoReader ad m denv { item with Item = flatItem } None] + let description = ToolTipText [FormatStructuredDescriptionOfItem true infoReader ad m denv { item with Item = flatItem } None None] let hasParamArrayArg = match flatItem with diff --git a/src/Compiler/Service/ServiceDeclarationLists.fsi b/src/Compiler/Service/ServiceDeclarationLists.fsi index a3987d66841..c0de77f5251 100644 --- a/src/Compiler/Service/ServiceDeclarationLists.fsi +++ b/src/Compiler/Service/ServiceDeclarationLists.fsi @@ -17,6 +17,8 @@ open FSharp.Compiler.AccessibilityLogic [] type public ToolTipElementData = { + Symbol: FSharpSymbol option + MainDescription: TaggedText[] XmlDoc: FSharpXmlDoc @@ -31,7 +33,7 @@ type public ToolTipElementData = ParamName: string option } - static member internal Create: layout: TaggedText[] * xml: FSharpXmlDoc * ?typeMapping: TaggedText[] list * ?paramName: string * ?remarks: TaggedText[] -> ToolTipElementData + static member internal Create: layout: TaggedText[] * xml: FSharpXmlDoc * ?typeMapping: TaggedText[] list * ?paramName: string * ?remarks: TaggedText[] * ?symbol: FSharpSymbol -> ToolTipElementData /// A single tool tip display element // @@ -46,7 +48,7 @@ type public ToolTipElement = /// An error occurred formatting this element | CompositionError of errorText: string - static member Single: layout: TaggedText[] * xml: FSharpXmlDoc * ?typeMapping: TaggedText[] list * ?paramName: string * ?remarks: TaggedText[] -> ToolTipElement + static member Single: layout: TaggedText[] * xml: FSharpXmlDoc * ?typeMapping: TaggedText[] list * ?paramName: string * ?remarks: TaggedText[] * ?symbol: FSharpSymbol -> ToolTipElement /// Information for building a tool tip box. // @@ -226,7 +228,7 @@ type public MethodGroup = static member internal Empty: MethodGroup module internal DeclarationListHelpers = - val FormatStructuredDescriptionOfItem: isDecl:bool -> InfoReader -> AccessorDomain -> range -> DisplayEnv -> ItemWithInst -> int option -> ToolTipElement + val FormatStructuredDescriptionOfItem: isDecl:bool -> InfoReader -> AccessorDomain -> range -> DisplayEnv -> ItemWithInst -> FSharpSymbol option -> int option -> ToolTipElement val RemoveDuplicateCompletionItems: TcGlobals -> CompletionItem list -> CompletionItem list diff --git a/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.SurfaceArea.netstandard20.release.bsl b/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.SurfaceArea.netstandard20.release.bsl index c9de7544463..001af598b92 100644 --- a/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.SurfaceArea.netstandard20.release.bsl +++ b/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.SurfaceArea.netstandard20.release.bsl @@ -3970,7 +3970,7 @@ FSharp.Compiler.EditorServices.ToolTipElement: Boolean get_IsNone() FSharp.Compiler.EditorServices.ToolTipElement: FSharp.Compiler.EditorServices.ToolTipElement NewCompositionError(System.String) FSharp.Compiler.EditorServices.ToolTipElement: FSharp.Compiler.EditorServices.ToolTipElement NewGroup(Microsoft.FSharp.Collections.FSharpList`1[FSharp.Compiler.EditorServices.ToolTipElementData]) FSharp.Compiler.EditorServices.ToolTipElement: FSharp.Compiler.EditorServices.ToolTipElement None -FSharp.Compiler.EditorServices.ToolTipElement: FSharp.Compiler.EditorServices.ToolTipElement Single(FSharp.Compiler.Text.TaggedText[], FSharp.Compiler.Symbols.FSharpXmlDoc, Microsoft.FSharp.Core.FSharpOption`1[Microsoft.FSharp.Collections.FSharpList`1[FSharp.Compiler.Text.TaggedText[]]], Microsoft.FSharp.Core.FSharpOption`1[System.String], Microsoft.FSharp.Core.FSharpOption`1[FSharp.Compiler.Text.TaggedText[]]) +FSharp.Compiler.EditorServices.ToolTipElement: FSharp.Compiler.EditorServices.ToolTipElement Single(FSharp.Compiler.Text.TaggedText[], FSharp.Compiler.Symbols.FSharpXmlDoc, Microsoft.FSharp.Core.FSharpOption`1[Microsoft.FSharp.Collections.FSharpList`1[FSharp.Compiler.Text.TaggedText[]]], Microsoft.FSharp.Core.FSharpOption`1[System.String], Microsoft.FSharp.Core.FSharpOption`1[FSharp.Compiler.Text.TaggedText[]], Microsoft.FSharp.Core.FSharpOption`1[FSharp.Compiler.Symbols.FSharpSymbol]) FSharp.Compiler.EditorServices.ToolTipElement: FSharp.Compiler.EditorServices.ToolTipElement get_None() FSharp.Compiler.EditorServices.ToolTipElement: FSharp.Compiler.EditorServices.ToolTipElement+CompositionError FSharp.Compiler.EditorServices.ToolTipElement: FSharp.Compiler.EditorServices.ToolTipElement+Group @@ -3991,12 +3991,14 @@ FSharp.Compiler.EditorServices.ToolTipElementData: Int32 GetHashCode() FSharp.Compiler.EditorServices.ToolTipElementData: Int32 GetHashCode(System.Collections.IEqualityComparer) FSharp.Compiler.EditorServices.ToolTipElementData: Microsoft.FSharp.Collections.FSharpList`1[FSharp.Compiler.Text.TaggedText[]] TypeMapping FSharp.Compiler.EditorServices.ToolTipElementData: Microsoft.FSharp.Collections.FSharpList`1[FSharp.Compiler.Text.TaggedText[]] get_TypeMapping() +FSharp.Compiler.EditorServices.ToolTipElementData: Microsoft.FSharp.Core.FSharpOption`1[FSharp.Compiler.Symbols.FSharpSymbol] Symbol +FSharp.Compiler.EditorServices.ToolTipElementData: Microsoft.FSharp.Core.FSharpOption`1[FSharp.Compiler.Symbols.FSharpSymbol] get_Symbol() FSharp.Compiler.EditorServices.ToolTipElementData: Microsoft.FSharp.Core.FSharpOption`1[FSharp.Compiler.Text.TaggedText[]] Remarks FSharp.Compiler.EditorServices.ToolTipElementData: Microsoft.FSharp.Core.FSharpOption`1[FSharp.Compiler.Text.TaggedText[]] get_Remarks() FSharp.Compiler.EditorServices.ToolTipElementData: Microsoft.FSharp.Core.FSharpOption`1[System.String] ParamName FSharp.Compiler.EditorServices.ToolTipElementData: Microsoft.FSharp.Core.FSharpOption`1[System.String] get_ParamName() FSharp.Compiler.EditorServices.ToolTipElementData: System.String ToString() -FSharp.Compiler.EditorServices.ToolTipElementData: Void .ctor(FSharp.Compiler.Text.TaggedText[], FSharp.Compiler.Symbols.FSharpXmlDoc, Microsoft.FSharp.Collections.FSharpList`1[FSharp.Compiler.Text.TaggedText[]], Microsoft.FSharp.Core.FSharpOption`1[FSharp.Compiler.Text.TaggedText[]], Microsoft.FSharp.Core.FSharpOption`1[System.String]) +FSharp.Compiler.EditorServices.ToolTipElementData: Void .ctor(Microsoft.FSharp.Core.FSharpOption`1[FSharp.Compiler.Symbols.FSharpSymbol], FSharp.Compiler.Text.TaggedText[], FSharp.Compiler.Symbols.FSharpXmlDoc, Microsoft.FSharp.Collections.FSharpList`1[FSharp.Compiler.Text.TaggedText[]], Microsoft.FSharp.Core.FSharpOption`1[FSharp.Compiler.Text.TaggedText[]], Microsoft.FSharp.Core.FSharpOption`1[System.String]) FSharp.Compiler.EditorServices.ToolTipText: Boolean Equals(FSharp.Compiler.EditorServices.ToolTipText) FSharp.Compiler.EditorServices.ToolTipText: Boolean Equals(System.Object) FSharp.Compiler.EditorServices.ToolTipText: Boolean Equals(System.Object, System.Collections.IEqualityComparer) diff --git a/vsintegration/src/FSharp.Editor/Common/RoslynHelpers.fs b/vsintegration/src/FSharp.Editor/Common/RoslynHelpers.fs index 76aea5388b0..41dee649d81 100644 --- a/vsintegration/src/FSharp.Editor/Common/RoslynHelpers.fs +++ b/vsintegration/src/FSharp.Editor/Common/RoslynHelpers.fs @@ -298,3 +298,16 @@ module internal OpenDeclarationHelper = module internal TaggedText = let toString (tts: TaggedText[]) = tts |> Array.map (fun tt -> tt.Text) |> String.concat "" + +// http://www.fssnip.net/7S3/title/Intersperse-a-list +module List = + /// The intersperse function takes an element and a list and + /// 'intersperses' that element between the elements of the list. + let intersperse sep ls = + List.foldBack + (fun x -> + function + | [] -> [ x ] + | xs -> x :: sep :: xs) + ls + [] diff --git a/vsintegration/src/FSharp.Editor/DocComments/XMLDocumentation.fs b/vsintegration/src/FSharp.Editor/DocComments/XMLDocumentation.fs index b28e9d20a04..b48991fb350 100644 --- a/vsintegration/src/FSharp.Editor/DocComments/XMLDocumentation.fs +++ b/vsintegration/src/FSharp.Editor/DocComments/XMLDocumentation.fs @@ -324,7 +324,7 @@ module internal XmlDocumentation = /// Append Xml documentation contents into the StringBuilder override this.AppendDocumentation - ( /// ITaggedTextCollector to add to + ( // ITaggedTextCollector to add to xmlCollector: ITaggedTextCollector, exnCollector: ITaggedTextCollector, fileName: string, @@ -392,12 +392,105 @@ module internal XmlDocumentation = paramName ) + [] + let separatorText = "-------------" + let private AddSeparator (collector: ITaggedTextCollector) = if not collector.IsEmpty then EnsureHardLine collector - collector.Add(tagText "-------------") + collector.Add(tagText separatorText) AppendHardLine collector + type LineLimits = + { + LineLimit: int + TypeParameterLimit: int + OverLoadsLimit: int + } + + let DefaultLineLimits = + { + LineLimit = 45 + TypeParameterLimit = 6 + OverLoadsLimit = 5 + } + + let BuildSingleTipText (documentationProvider: IDocumentationBuilder, dataTipElement: ToolTipElement, limits: LineLimits) = + + let { + LineLimit = lineLimit + TypeParameterLimit = typeParameterLineLimit + OverLoadsLimit = overLoadsLimit + } = + limits + + let mainDescription, documentation, typeParameterMap, exceptions, usage = + ResizeArray(), ResizeArray(), ResizeArray(), ResizeArray(), ResizeArray() + + let textCollector: ITaggedTextCollector = + TextSanitizingCollector(mainDescription.Add, lineLimit = lineLimit) + + let xmlCollector: ITaggedTextCollector = + TextSanitizingCollector(documentation.Add, lineLimit = lineLimit) + + let typeParameterMapCollector: ITaggedTextCollector = + TextSanitizingCollector(typeParameterMap.Add, lineLimit = typeParameterLineLimit) + + let exnCollector: ITaggedTextCollector = + TextSanitizingCollector(exceptions.Add, lineLimit = lineLimit) + + let usageCollector: ITaggedTextCollector = + TextSanitizingCollector(usage.Add, lineLimit = lineLimit) + + let ProcessGenericParameters (tps: TaggedText[] list) = + if not tps.IsEmpty then + AppendHardLine typeParameterMapCollector + AppendOnNewLine typeParameterMapCollector (SR.GenericParametersHeader()) + + for tp in tps do + AppendHardLine typeParameterMapCollector + typeParameterMapCollector.Add(tagSpace " ") + tp |> Array.iter typeParameterMapCollector.Add + + let collectDocumentation () = + [ documentation; typeParameterMap; exceptions; usage ] + |> List.filter (Seq.isEmpty >> not) + |> List.map List.ofSeq + |> List.intersperse [ lineBreak ] + |> Seq.concat + |> List.ofSeq + + match dataTipElement with + | ToolTipElement.Group overloads when not overloads.IsEmpty -> + overloads[.. overLoadsLimit - 1] + |> List.map (fun item -> item.MainDescription) + |> List.intersperse [| lineBreak |] + |> Seq.concat + |> Seq.iter textCollector.Add + + if not overloads[overLoadsLimit..].IsEmpty then + AppendOnNewLine textCollector $"({(PrettyNaming.FormatAndOtherOverloadsString overloads[overLoadsLimit..].Length)})" + + let item0 = overloads.Head + + item0.Remarks + |> Option.iter (fun r -> + if TaggedText.toString r <> "" then + AppendHardLine usageCollector + r |> Seq.iter usageCollector.Add) + + AppendXmlComment(documentationProvider, xmlCollector, exnCollector, item0.XmlDoc, true, false, item0.ParamName) + + ProcessGenericParameters item0.TypeMapping + + item0.Symbol, mainDescription |> List.ofSeq, collectDocumentation () + + | ToolTipElement.CompositionError (errText) -> + textCollector.Add(tagText errText) + None, mainDescription |> List.ofSeq, collectDocumentation () + + | _ -> None, [], [] + /// Build a data tip text string with xml comments injected. let BuildTipText ( diff --git a/vsintegration/src/FSharp.Editor/Navigation/FindDefinitionService.fs b/vsintegration/src/FSharp.Editor/Navigation/FindDefinitionService.fs index cd938c41b83..884ea193da4 100644 --- a/vsintegration/src/FSharp.Editor/Navigation/FindDefinitionService.fs +++ b/vsintegration/src/FSharp.Editor/Navigation/FindDefinitionService.fs @@ -10,22 +10,15 @@ open FSharp.Compiler.Text.Range open Microsoft.CodeAnalysis open Microsoft.CodeAnalysis.ExternalAccess.FSharp.GoToDefinition -open Microsoft.VisualStudio.Shell -open Microsoft.VisualStudio.Shell.Interop open System.Collections.Immutable open System.Threading.Tasks [)>] [)>] type internal FSharpFindDefinitionService [] (metadataAsSource: FSharpMetadataAsSourceService) = - - let statusBar = - StatusBar(ServiceProvider.GlobalProvider.GetService()) - interface IFSharpFindDefinitionService with member _.FindDefinitionsAsync(document: Document, position: int, cancellationToken: CancellationToken) = - let navigation = - FSharpNavigation(statusBar, metadataAsSource, document, rangeStartup) + let navigation = FSharpNavigation(metadataAsSource, document, rangeStartup) let definitions = navigation.FindDefinitions(position, cancellationToken) ImmutableArray.CreateRange(definitions) |> Task.FromResult diff --git a/vsintegration/src/FSharp.Editor/Navigation/GoToDefinition.fs b/vsintegration/src/FSharp.Editor/Navigation/GoToDefinition.fs index 4901de036c4..e2e4d716900 100644 --- a/vsintegration/src/FSharp.Editor/Navigation/GoToDefinition.fs +++ b/vsintegration/src/FSharp.Editor/Navigation/GoToDefinition.fs @@ -109,7 +109,10 @@ module private ExternalSymbol = | _ -> [] // TODO: Uncomment code when VS has a fix for updating the status bar. -type StatusBar(statusBar: IVsStatusbar) = +type StatusBar() = + let statusBar = + ServiceProvider.GlobalProvider.GetService() + let mutable _searchIcon = int16 Microsoft.VisualStudio.Shell.Interop.Constants.SBAI_Find :> obj @@ -394,7 +397,6 @@ type internal GoToDefinition(metadataAsSource: FSharpMetadataAsSourceService) = ( document: Document, textSpan: Microsoft.CodeAnalysis.Text.TextSpan, - statusBar: StatusBar, cancellationToken: CancellationToken ) = let navigableItem = FSharpGoToDefinitionNavigableItem(document, textSpan) @@ -407,9 +409,10 @@ type internal GoToDefinition(metadataAsSource: FSharpMetadataAsSourceService) = navigationService.TryNavigateToSpan(workspace, navigableItem.Document.Id, navigableItem.SourceSpan, cancellationToken) if not navigationSucceeded then - statusBar.TempMessage(SR.CannotNavigateUnknown()) + StatusBar().TempMessage(SR.CannotNavigateUnknown()) - member _.NavigateToItem(navigableItem: FSharpNavigableItem, statusBar: StatusBar, cancellationToken: CancellationToken) = + member _.NavigateToItem(navigableItem: FSharpNavigableItem, cancellationToken: CancellationToken) = + let statusBar = StatusBar() use __ = statusBar.Animate() statusBar.Message(SR.NavigatingTo()) @@ -434,12 +437,11 @@ type internal GoToDefinition(metadataAsSource: FSharpMetadataAsSourceService) = targetDocument: Document, targetSourceText: SourceText, symbolRange: range, - statusBar: StatusBar, cancellationToken: CancellationToken ) = asyncMaybe { let! item = this.FindDeclarationOfSymbolAtRange(targetDocument, symbolRange, targetSourceText) - return this.NavigateToItem(item, statusBar, cancellationToken) + return this.NavigateToItem(item, cancellationToken) } /// Find the definition location (implementation file/.fs) of the target symbol @@ -448,12 +450,11 @@ type internal GoToDefinition(metadataAsSource: FSharpMetadataAsSourceService) = targetDocument: Document, targetSourceText: SourceText, symbolRange: range, - statusBar: StatusBar, cancellationToken: CancellationToken ) = asyncMaybe { let! item = this.FindDefinitionOfSymbolAtRange(targetDocument, symbolRange, targetSourceText) - return this.NavigateToItem(item, statusBar, cancellationToken) + return this.NavigateToItem(item, cancellationToken) } member this.NavigateToExternalDeclaration @@ -546,7 +547,7 @@ type internal GoToDefinition(metadataAsSource: FSharpMetadataAsSourceService) = | _ -> TextSpan() let navItem = FSharpGoToDefinitionNavigableItem(tmpShownDoc, span) - this.NavigateToItem(navItem, statusBar, cancellationToken) + this.NavigateToItem(navItem, cancellationToken) true | _ -> false | _ -> false @@ -556,201 +557,7 @@ type internal GoToDefinition(metadataAsSource: FSharpMetadataAsSourceService) = else statusBar.TempMessage(SR.CannotNavigateUnknown()) -type internal FSharpQuickInfo = - { - StructuredText: ToolTipText - Span: TextSpan - Symbol: FSharpSymbol option - SymbolKind: LexerSymbolKind - } - -module internal FSharpQuickInfo = - - let userOpName = "QuickInfo" - - // 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 quick info - let getQuickInfoFromRange - ( - document: Document, - declRange: range, - width: int option, - cancellationToken: CancellationToken - ) : Async = - - asyncMaybe { - let userOpName = "getQuickInfoFromRange" - let solution = document.Project.Solution - // 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 = RoslynHelpers.TryFSharpRangeToTextSpan(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! extLexerSymbol = extDocument.TryFindFSharpLexerSymbolAsync(extSpan.Start, SymbolLookupKind.Greedy, true, true, userOpName) - let! _, extCheckFileResults = extDocument.GetFSharpParseAndCheckResultsAsync(userOpName) |> liftAsync - - let extQuickInfoText = - extCheckFileResults.GetToolTip( - declRange.StartLine, - extLexerSymbol.Ident.idRange.EndColumn, - extLineText, - extLexerSymbol.FullIsland, - FSharpTokenTag.IDENT, - ?width = width - ) - - match extQuickInfoText with - | ToolTipText [] - | ToolTipText [ ToolTipElement.None ] -> return! None - | extQuickInfoText -> - let! extSymbolUse = - extCheckFileResults.GetSymbolUseAtLocation( - declRange.StartLine, - extLexerSymbol.Ident.idRange.EndColumn, - extLineText, - extLexerSymbol.FullIsland - ) - - let! span = RoslynHelpers.TryFSharpRangeToTextSpan(extSourceText, extLexerSymbol.Range) - - return - { - StructuredText = extQuickInfoText - Span = span - Symbol = Some extSymbolUse.Symbol - SymbolKind = extLexerSymbol.Kind - } - } - - /// Get QuickInfo combined from doccom of Signature and definition - let getQuickInfo - ( - document: Document, - position: int, - width: int option, - cancellationToken: CancellationToken - ) : Async<(range * FSharpQuickInfo option * FSharpQuickInfo option) option> = - - asyncMaybe { - let userOpName = "getQuickInfo" - 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 textLinePos = sourceText.Lines.GetLinePosition position - let fcsTextLineNumber = Line.fromZ textLinePos.Line - let lineText = (sourceText.Lines.GetLineFromPosition position).ToString() - - /// Gets the QuickInfo information for the orignal target - let getTargetSymbolQuickInfo (symbol, tag) = - asyncMaybe { - let targetQuickInfo = - match lexerSymbol.Kind with - | LexerSymbolKind.Keyword -> checkFileResults.GetKeywordTooltip(lexerSymbol.FullIsland) - | _ -> - checkFileResults.GetToolTip( - fcsTextLineNumber, - idRange.EndColumn, - lineText, - lexerSymbol.FullIsland, - tag, - ?width = width - ) - - match targetQuickInfo with - | ToolTipText [] - | ToolTipText [ ToolTipElement.None ] -> return! None - | _ -> - let! targetTextSpan = RoslynHelpers.TryFSharpRangeToTextSpan(sourceText, lexerSymbol.Range) - - return - { - StructuredText = targetQuickInfo - Span = targetTextSpan - Symbol = symbol - SymbolKind = lexerSymbol.Kind - } - } - - 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 - if isSignatureFile document.FilePath then - let! targetQuickInfo = getTargetSymbolQuickInfo (Some symbolUse.Symbol, FSharpTokenTag.IDENT) - return symbolUse.Range, None, Some targetQuickInfo - else - // find the declaration location of the target symbol, with a preference for signature files - let findSigDeclarationResult = - checkFileResults.GetDeclarationLocation( - idRange.StartLine, - idRange.EndColumn, - lineText, - lexerSymbol.FullIsland, - preferFlag = true - ) - - // it is necessary to retrieve the backup quick info because this acquires - // the textSpan designating where we want the quick info to appear. - let! targetQuickInfo = getTargetSymbolQuickInfo (Some symbolUse.Symbol, FSharpTokenTag.IDENT) - - let! result = - match findSigDeclarationResult with - | FindDeclResult.DeclFound declRange when isSignatureFile declRange.FileName -> - asyncMaybe { - let! sigQuickInfo = getQuickInfoFromRange (document, declRange, width, cancellationToken) - - // 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.GetDeclarationLocation( - idRange.StartLine, - idRange.EndColumn, - lineText, - lexerSymbol.FullIsland, - preferFlag = false - ) - - match findImplDefinitionResult with - | FindDeclResult.DeclNotFound _ - | FindDeclResult.ExternalDecl _ -> return symbolUse.Range, Some sigQuickInfo, None - | FindDeclResult.DeclFound declRange -> - let! implQuickInfo = getQuickInfoFromRange (document, declRange, width, cancellationToken) - - return - symbolUse.Range, - Some sigQuickInfo, - Some - { implQuickInfo with - Span = targetQuickInfo.Span - } - } - | _ -> async.Return None - |> liftAsync - - return result |> Option.defaultValue (symbolUse.Range, None, Some targetQuickInfo) - } - -type internal FSharpNavigation - ( - statusBar: StatusBar, - metadataAsSource: FSharpMetadataAsSourceService, - initialDoc: Document, - thisSymbolUseRange: range - ) = +type internal FSharpNavigation(metadataAsSource: FSharpMetadataAsSourceService, initialDoc: Document, thisSymbolUseRange: range) = let workspace = initialDoc.Project.Solution.Workspace let solution = workspace.CurrentSolution @@ -791,15 +598,13 @@ type internal FSharpNavigation match initialDoc.FilePath, targetPath with | Signature, Signature - | Implementation, Implementation -> return gtd.TryNavigateToTextSpan(targetDoc, targetTextSpan, statusBar, cancellationToken) + | Implementation, Implementation -> return gtd.TryNavigateToTextSpan(targetDoc, targetTextSpan, cancellationToken) // Adjust the target from signature to implementation. - | Implementation, Signature -> - return! gtd.NavigateToSymbolDefinitionAsync(targetDoc, targetSource, range, statusBar, cancellationToken) + | Implementation, Signature -> return! gtd.NavigateToSymbolDefinitionAsync(targetDoc, targetSource, range, cancellationToken) // Adjust the target from implmentation to signature. - | Signature, Implementation -> - return! gtd.NavigateToSymbolDeclarationAsync(targetDoc, targetSource, range, statusBar, cancellationToken) + | Signature, Implementation -> return! gtd.NavigateToSymbolDeclarationAsync(targetDoc, targetSource, range, cancellationToken) } |> Async.Ignore |> Async.StartImmediate @@ -818,6 +623,7 @@ type internal FSharpNavigation member _.TryGoToDefinition(position, cancellationToken) = let gtd = GoToDefinition(metadataAsSource) + let statusBar = StatusBar() 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. @@ -829,7 +635,7 @@ type internal FSharpNavigation if gtdTask.Status = TaskStatus.RanToCompletion && gtdTask.Result.IsSome then match gtdTask.Result.Value with | FSharpGoToDefinitionResult.NavigableItem (navItem), _ -> - gtd.NavigateToItem(navItem, statusBar, cancellationToken) + gtd.NavigateToItem(navItem, cancellationToken) // 'true' means do it, like Sheev Palpatine would want us to. true | FSharpGoToDefinitionResult.ExternalAssembly (targetSymbolUse, metadataReferences), _ -> @@ -876,7 +682,7 @@ type internal DocCommentId = | Type of EntityPath: string list | None -type FSharpNavigableLocation(statusBar: StatusBar, metadataAsSource: FSharpMetadataAsSourceService, symbolRange: range, project: Project) = +type FSharpNavigableLocation(metadataAsSource: FSharpMetadataAsSourceService, symbolRange: range, project: Project) = interface IFSharpNavigableLocation with member _.NavigateToAsync(_options: FSharpNavigationOptions2, cancellationToken: CancellationToken) : Task = asyncMaybe { @@ -892,10 +698,8 @@ type FSharpNavigableLocation(statusBar: StatusBar, metadataAsSource: FSharpMetad Implementation match targetPath with - | Signature -> - return! gtd.NavigateToSymbolDefinitionAsync(targetDoc, targetSource, symbolRange, statusBar, cancellationToken) - | Implementation -> - return! gtd.NavigateToSymbolDeclarationAsync(targetDoc, targetSource, symbolRange, statusBar, cancellationToken) + | Signature -> return! gtd.NavigateToSymbolDefinitionAsync(targetDoc, targetSource, symbolRange, cancellationToken) + | Implementation -> return! gtd.NavigateToSymbolDeclarationAsync(targetDoc, targetSource, symbolRange, cancellationToken) } |> Async.map (fun a -> a.IsSome) |> RoslynHelpers.StartAsyncAsTask cancellationToken @@ -908,9 +712,6 @@ type FSharpCrossLanguageSymbolNavigationService() = let workspace = componentModel.GetService() - let statusBar = - StatusBar(ServiceProvider.GlobalProvider.GetService()) - let metadataAsSource = componentModel .DefaultExportProvider @@ -1141,7 +942,7 @@ type FSharpCrossLanguageSymbolNavigationService() = // 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 + return FSharpNavigableLocation(metadataAsSource, location, project) :> IFSharpNavigableLocation else return Unchecked.defaultof<_> // returning null here, so Roslyn can fallback to default source-as-metadata implementation. } diff --git a/vsintegration/src/FSharp.Editor/Navigation/GoToDefinitionService.fs b/vsintegration/src/FSharp.Editor/Navigation/GoToDefinitionService.fs index 9588fc3a7bb..a3a800c9ef6 100644 --- a/vsintegration/src/FSharp.Editor/Navigation/GoToDefinitionService.fs +++ b/vsintegration/src/FSharp.Editor/Navigation/GoToDefinitionService.fs @@ -2,7 +2,6 @@ namespace Microsoft.VisualStudio.FSharp.Editor -open System open System.Composition open System.Threading open System.Threading.Tasks @@ -12,31 +11,24 @@ open FSharp.Compiler.Text.Range open Microsoft.CodeAnalysis open Microsoft.CodeAnalysis.ExternalAccess.FSharp.Editor -open Microsoft.VisualStudio.Shell -open Microsoft.VisualStudio.Shell.Interop - [)>] [)>] type internal FSharpGoToDefinitionService [] (metadataAsSource: FSharpMetadataAsSourceService) = - let statusBar = - StatusBar(ServiceProvider.GlobalProvider.GetService()) - interface IFSharpGoToDefinitionService with /// Invoked with Peek Definition. member _.FindDefinitionsAsync(document: Document, position: int, cancellationToken: CancellationToken) = - let navigation = - FSharpNavigation(statusBar, metadataAsSource, document, rangeStartup) + let navigation = FSharpNavigation(metadataAsSource, document, rangeStartup) navigation.FindDefinitions(position, cancellationToken) |> Task.FromResult /// Invoked with Go to Definition. /// Try to navigate to the definiton of the symbol at the symbolRange in the originDocument member _.TryGoToDefinition(document: Document, position: int, cancellationToken: CancellationToken) = + let statusBar = StatusBar() statusBar.Message(SR.LocatingSymbol()) use __ = statusBar.Animate() - let navigation = - FSharpNavigation(statusBar, metadataAsSource, document, rangeStartup) + let navigation = FSharpNavigation(metadataAsSource, document, rangeStartup) navigation.TryGoToDefinition(position, cancellationToken) diff --git a/vsintegration/src/FSharp.Editor/Navigation/NavigableSymbolsService.fs b/vsintegration/src/FSharp.Editor/Navigation/NavigableSymbolsService.fs index 5a87b2fc474..99a07f8fd1d 100644 --- a/vsintegration/src/FSharp.Editor/Navigation/NavigableSymbolsService.fs +++ b/vsintegration/src/FSharp.Editor/Navigation/NavigableSymbolsService.fs @@ -8,31 +8,28 @@ open System.Threading.Tasks open System.ComponentModel.Composition open Microsoft.CodeAnalysis.Text -open Microsoft.CodeAnalysis.Navigation open Microsoft.CodeAnalysis.ExternalAccess.FSharp.Navigation open Microsoft.VisualStudio.Language.Intellisense open Microsoft.VisualStudio.Text open Microsoft.VisualStudio.Text.Editor -open Microsoft.VisualStudio.Shell.Interop open Microsoft.VisualStudio.Utilities -open Microsoft.VisualStudio.Shell [] -type internal FSharpNavigableSymbol(item: FSharpNavigableItem, span: SnapshotSpan, gtd: GoToDefinition, statusBar: StatusBar) = +type internal FSharpNavigableSymbol(item: FSharpNavigableItem, span: SnapshotSpan, gtd: GoToDefinition) = interface INavigableSymbol with member _.Navigate(_: INavigableRelationship) = - gtd.NavigateToItem(item, statusBar, CancellationToken.None) + gtd.NavigateToItem(item, CancellationToken.None) member _.Relationships = seq { yield PredefinedNavigableRelationships.Definition } member _.SymbolSpan = span -type internal FSharpNavigableSymbolSource(metadataAsSource, serviceProvider: IServiceProvider) = +type internal FSharpNavigableSymbolSource(metadataAsSource) = let mutable disposed = false let gtd = GoToDefinition(metadataAsSource) - let statusBar = StatusBar(serviceProvider.GetService()) + let statusBar = StatusBar() interface INavigableSymbolSource with member _.GetNavigableSymbolAsync(triggerSpan: SnapshotSpan, cancellationToken: CancellationToken) = @@ -67,7 +64,7 @@ type internal FSharpNavigableSymbolSource(metadataAsSource, serviceProvider: ISe match result with | FSharpGoToDefinitionResult.NavigableItem (navItem) -> - return FSharpNavigableSymbol(navItem, symbolSpan, gtd, statusBar) :> INavigableSymbol + return FSharpNavigableSymbol(navItem, symbolSpan, gtd) :> INavigableSymbol | FSharpGoToDefinitionResult.ExternalAssembly (targetSymbolUse, metadataReferences) -> let nav = @@ -105,12 +102,8 @@ type internal FSharpNavigableSymbolSource(metadataAsSource, serviceProvider: ISe [] [] [] -type internal FSharpNavigableSymbolService [] - ( - [)>] serviceProvider: IServiceProvider, - metadataAsSource: FSharpMetadataAsSourceService - ) = +type internal FSharpNavigableSymbolService [] (metadataAsSource: FSharpMetadataAsSourceService) = interface INavigableSymbolSourceProvider with member _.TryCreateNavigableSymbolSource(_: ITextView, _: ITextBuffer) = - new FSharpNavigableSymbolSource(metadataAsSource, serviceProvider) :> INavigableSymbolSource + new FSharpNavigableSymbolSource(metadataAsSource) diff --git a/vsintegration/src/FSharp.Editor/QuickInfo/QuickInfoProvider.fs b/vsintegration/src/FSharp.Editor/QuickInfo/QuickInfoProvider.fs index 45d24fb9d56..c8e893070a5 100644 --- a/vsintegration/src/FSharp.Editor/QuickInfo/QuickInfoProvider.fs +++ b/vsintegration/src/FSharp.Editor/QuickInfo/QuickInfoProvider.fs @@ -2,194 +2,126 @@ namespace Microsoft.VisualStudio.FSharp.Editor.QuickInfo -open System -open System.IO open System.Threading open System.Threading.Tasks open System.ComponentModel.Composition -open System.Text open Microsoft.CodeAnalysis open Microsoft.CodeAnalysis.Text open Microsoft.VisualStudio.Language.Intellisense -open Microsoft.VisualStudio.Shell -open Microsoft.VisualStudio.Shell.Interop open Microsoft.VisualStudio.Text +open Microsoft.VisualStudio.Shell open Microsoft.VisualStudio.Utilities open Microsoft.VisualStudio.FSharp.Editor open FSharp.Compiler.Text open Microsoft.IO +open FSharp.Compiler.EditorServices type internal FSharpAsyncQuickInfoSource ( - statusBar: StatusBar, - xmlMemberIndexService: IVsXMLMemberIndexService, + xmlMemberIndexService, metadataAsSource: FSharpMetadataAsSourceService, textBuffer: ITextBuffer, editorOptions: EditorOptions ) = - // test helper - static member ProvideQuickInfo(document: Document, position: int, ?width: int) = + let getQuickInfoItem (sourceText, (document: Document), (lexerSymbol: LexerSymbol), (ToolTipText elements)) = asyncMaybe { - let! _, sigQuickInfo, targetQuickInfo = FSharpQuickInfo.getQuickInfo (document, position, width, CancellationToken.None) - return! sigQuickInfo |> Option.orElse targetQuickInfo - } + let documentationBuilder = + XmlDocumentation.CreateDocumentationBuilder(xmlMemberIndexService) + + let getSingleContent (data: ToolTipElement) = + + let symbol, description, documentation = + XmlDocumentation.BuildSingleTipText(documentationBuilder, data, XmlDocumentation.DefaultLineLimits) + + let getLinkTooltip filePath = + let solutionDir = Path.GetDirectoryName(document.Project.Solution.FilePath) + let projectDir = Path.GetDirectoryName(document.Project.FilePath) + + [ + Path.GetRelativePath(projectDir, filePath) + Path.GetRelativePath(solutionDir, filePath) + ] + |> List.minBy String.length + + QuickInfoViewProvider.provideContent ( + Tokenizer.GetImageIdForSymbol(symbol, lexerSymbol.Kind), + description, + documentation, + FSharpNavigation(metadataAsSource, document, lexerSymbol.Range), + getLinkTooltip + ) - static member BuildSingleQuickInfoItem (documentationBuilder: IDocumentationBuilder) (quickInfo: FSharpQuickInfo) = - let mainDescription, documentation, typeParameterMap, usage, exceptions = - ResizeArray(), ResizeArray(), ResizeArray(), ResizeArray(), ResizeArray() + let content = elements |> List.map getSingleContent + do! Option.guard (not content.IsEmpty) - XmlDocumentation.BuildDataTipText( - documentationBuilder, - mainDescription.Add, - documentation.Add, - typeParameterMap.Add, - usage.Add, - exceptions.Add, - quickInfo.StructuredText - ) + let! textSpan = RoslynHelpers.TryFSharpRangeToTextSpan(sourceText, lexerSymbol.Range) - let docs = - RoslynHelpers.joinWithLineBreaks [ documentation; typeParameterMap; usage; exceptions ] + let trackingSpan = + textBuffer.CurrentSnapshot.CreateTrackingSpan(textSpan.Start, textSpan.Length, SpanTrackingMode.EdgeInclusive) - (mainDescription, docs) + return QuickInfoItem(trackingSpan, QuickInfoViewProvider.stackWithSeparators content) + } + + static member TryGetToolTip(document: Document, position, ?width) = + asyncMaybe { + let userOpName = "getQuickInfo" + + let! lexerSymbol = document.TryFindFSharpLexerSymbolAsync(position, SymbolLookupKind.Greedy, true, true, userOpName) + let! _, checkFileResults = document.GetFSharpParseAndCheckResultsAsync(userOpName) |> liftAsync + let! cancellationToken = Async.CancellationToken |> liftAsync + let! sourceText = document.GetTextAsync cancellationToken + let range = lexerSymbol.Range + let textLinePos = sourceText.Lines.GetLinePosition position + let fcsTextLineNumber = Line.fromZ textLinePos.Line + let lineText = (sourceText.Lines.GetLineFromPosition position).ToString() + + let tooltip = + match lexerSymbol.Kind with + | LexerSymbolKind.Keyword -> checkFileResults.GetKeywordTooltip(lexerSymbol.FullIsland) + | LexerSymbolKind.String -> + checkFileResults.GetToolTip( + fcsTextLineNumber, + range.EndColumn, + lineText, + lexerSymbol.FullIsland, + FSharp.Compiler.Tokenization.FSharpTokenTag.String, + ?width = width + ) + | _ -> + checkFileResults.GetToolTip( + fcsTextLineNumber, + range.EndColumn, + lineText, + lexerSymbol.FullIsland, + FSharp.Compiler.Tokenization.FSharpTokenTag.IDENT, + ?width = width + ) + + return sourceText, document, lexerSymbol, tooltip + } interface IAsyncQuickInfoSource with override _.Dispose() = () // no cleanup necessary - // This method can be called from the background thread. - // Do not call IServiceProvider.GetService here. override _.GetQuickInfoItemAsync(session: IAsyncQuickInfoSession, cancellationToken: CancellationToken) : Task = - let triggerPoint = session.GetTriggerPoint(textBuffer.CurrentSnapshot) - - match triggerPoint.HasValue with - | false -> Task.FromResult(null) - | true -> - let triggerPoint = triggerPoint.GetValueOrDefault() - - let width = editorOptions.QuickInfo.DescriptionWidth - - asyncMaybe { - let document = - textBuffer.CurrentSnapshot.GetOpenDocumentInCurrentContextWithChanges() - - let! symbolUseRange, sigQuickInfo, targetQuickInfo = - FSharpQuickInfo.getQuickInfo (document, triggerPoint.Position, width, cancellationToken) - - let getTooltip filePath = - let solutionDir = Path.GetDirectoryName(document.Project.Solution.FilePath) - let projectDir = Path.GetDirectoryName(document.Project.FilePath) - - [ - Path.GetRelativePath(projectDir, filePath) - Path.GetRelativePath(solutionDir, filePath) - ] - |> List.minBy String.length - - let getTrackingSpan (span: TextSpan) = - textBuffer.CurrentSnapshot.CreateTrackingSpan(span.Start, span.Length, SpanTrackingMode.EdgeInclusive) - - let documentationBuilder = - XmlDocumentation.CreateDocumentationBuilder(xmlMemberIndexService) - - match sigQuickInfo, targetQuickInfo with - | None, None -> return null - | Some quickInfo, None - | None, Some quickInfo -> - let mainDescription, docs = - FSharpAsyncQuickInfoSource.BuildSingleQuickInfoItem documentationBuilder quickInfo - - let imageId = Tokenizer.GetImageIdForSymbol(quickInfo.Symbol, quickInfo.SymbolKind) - - let navigation = - FSharpNavigation(statusBar, metadataAsSource, document, symbolUseRange) - - let content = - QuickInfoViewProvider.provideContent ( - imageId, - mainDescription |> List.ofSeq, - [ docs |> List.ofSeq ], - navigation, - getTooltip - ) - - let span = getTrackingSpan quickInfo.Span - return QuickInfoItem(span, content) - - | Some sigQuickInfo, Some targetQuickInfo -> - let mainDescription, targetDocumentation, sigDocumentation, typeParameterMap, exceptions, usage = - ResizeArray(), ResizeArray(), ResizeArray(), ResizeArray(), ResizeArray(), ResizeArray() - - XmlDocumentation.BuildDataTipText( - documentationBuilder, - ignore, - sigDocumentation.Add, - ignore, - ignore, - ignore, - sigQuickInfo.StructuredText - ) - - XmlDocumentation.BuildDataTipText( - documentationBuilder, - mainDescription.Add, - targetDocumentation.Add, - typeParameterMap.Add, - exceptions.Add, - usage.Add, - targetQuickInfo.StructuredText - ) - // get whitespace nomalized documentation text - let getText (tts: seq) = - let text = - (StringBuilder(), tts) - ||> Seq.fold (fun sb tt -> - if String.IsNullOrWhiteSpace tt.Text then - sb - else - sb.Append tt.Text) - |> string - - if String.IsNullOrWhiteSpace text then None else Some text - - let documentationParts: TaggedText list list = - [ - match getText targetDocumentation, getText sigDocumentation with - | None, None -> () - | None, Some _ -> sigDocumentation |> List.ofSeq - | Some _, None -> targetDocumentation |> List.ofSeq - | Some implText, Some sigText when implText.Equals(sigText, StringComparison.OrdinalIgnoreCase) -> - sigDocumentation |> List.ofSeq - | Some _, Some _ -> - sigDocumentation |> List.ofSeq - targetDocumentation |> List.ofSeq - RoslynHelpers.joinWithLineBreaks [ typeParameterMap; usage; exceptions ] - |> List.ofSeq - ] - - let imageId = - Tokenizer.GetImageIdForSymbol(targetQuickInfo.Symbol, targetQuickInfo.SymbolKind) - - let navigation = - FSharpNavigation(statusBar, metadataAsSource, document, symbolUseRange) - - let content = - QuickInfoViewProvider.provideContent ( - imageId, - mainDescription |> List.ofSeq, - documentationParts, - navigation, - getTooltip - ) - - let span = getTrackingSpan targetQuickInfo.Span - return QuickInfoItem(span, content) - } - |> Async.map Option.toObj - |> RoslynHelpers.StartAsyncAsTask cancellationToken + asyncMaybe { + let document = + textBuffer.CurrentSnapshot.GetOpenDocumentInCurrentContextWithChanges() + + let! triggerPoint = session.GetTriggerPoint(textBuffer.CurrentSnapshot) |> Option.ofNullable + let position = triggerPoint.Position + + let! tipdata = + FSharpAsyncQuickInfoSource.TryGetToolTip(document, position, ?width = editorOptions.QuickInfo.DescriptionWidth) + + return! getQuickInfoItem tipdata + } + |> Async.map Option.toObj + |> RoslynHelpers.StartAsyncAsTask cancellationToken [)>] [] @@ -197,17 +129,12 @@ type internal FSharpAsyncQuickInfoSource [] type internal FSharpAsyncQuickInfoSourceProvider [] ( - [)>] serviceProvider: IServiceProvider, + [)>] serviceProvider: System.IServiceProvider, metadataAsSource: FSharpMetadataAsSourceService, editorOptions: EditorOptions ) = interface IAsyncQuickInfoSourceProvider with override _.TryCreateQuickInfoSource(textBuffer: ITextBuffer) : IAsyncQuickInfoSource = - // GetService calls must be made on the UI thread - // It is safe to do it here (see #4713) - let statusBar = StatusBar(serviceProvider.GetService()) let xmlMemberIndexService = serviceProvider.XMLMemberIndexService - - new FSharpAsyncQuickInfoSource(statusBar, xmlMemberIndexService, metadataAsSource, textBuffer, editorOptions) - :> IAsyncQuickInfoSource + new FSharpAsyncQuickInfoSource(xmlMemberIndexService, metadataAsSource, textBuffer, editorOptions) diff --git a/vsintegration/src/FSharp.Editor/QuickInfo/Views.fs b/vsintegration/src/FSharp.Editor/QuickInfo/Views.fs index f5830104ed3..415b38798b1 100644 --- a/vsintegration/src/FSharp.Editor/QuickInfo/Views.fs +++ b/vsintegration/src/FSharp.Editor/QuickInfo/Views.fs @@ -11,7 +11,6 @@ open Microsoft.VisualStudio.Text.Adornments open Microsoft.VisualStudio.FSharp.Editor module internal QuickInfoViewProvider = - let layoutTagToClassificationTag (layoutTag: TextTag) = match layoutTag with | TextTag.ActivePatternCase @@ -56,27 +55,20 @@ module internal QuickInfoViewProvider = | TaggedText (TextTag.LineBreak, _) -> Some() | _ -> None - let (|DocSeparator|_|) = - function - | LineBreak :: TaggedText (TextTag.Text, "-------------") :: LineBreak :: rest -> Some rest - | _ -> None - let wrapContent (elements: obj list) = ContainerElement(ContainerElementStyle.Wrapped, elements |> Seq.map box) let stackContent (elements: obj list) = ContainerElement(ContainerElementStyle.Stacked, elements |> Seq.map box) - let encloseRuns runs = wrapContent (runs |> List.rev) |> box - - let emptyLine = - wrapContent [ ClassifiedTextRun(ClassificationTypeNames.WhiteSpace, "") |> box ] + let encloseRuns runs = + ClassifiedTextElement(runs |> List.rev) |> box let provideContent ( imageId: ImageId option, description: TaggedText list, - documentation: TaggedText list list, + documentation: TaggedText list, navigation: FSharpNavigation, getTooltip ) = @@ -84,10 +76,10 @@ module internal QuickInfoViewProvider = let encloseText text = let rec loop text runs stack = match (text: TaggedText list) with + | [] when runs |> List.isEmpty -> stackContent (stack |> List.rev) | [] -> stackContent (encloseRuns runs :: stack |> List.rev) - | DocSeparator (LineBreak :: rest) - | DocSeparator rest -> loop rest [] (box Separator :: encloseRuns runs :: stack) - | LineBreak :: rest when runs |> List.isEmpty -> loop rest [] (emptyLine :: stack) + // smaller gap instead of huge double line break + | LineBreak :: rest when runs |> List.isEmpty -> loop rest [] (box (Separator false) :: stack) | LineBreak :: rest -> loop rest [] (encloseRuns runs :: stack) | :? NavigableTaggedText as item :: rest when navigation.IsTargetValid item.Range -> let classificationTag = layoutTagToClassificationTag item.Tag @@ -108,6 +100,10 @@ module internal QuickInfoViewProvider = | Some imageId -> wrapContent [ stackContent [ ImageElement(imageId) ]; encloseText description ] | None -> ContainerElement(ContainerElementStyle.Wrapped, encloseText description) - let separated = stackContent (documentation |> List.map encloseText) + wrapContent [ stackContent [ innerElement; encloseText documentation ] ] - wrapContent [ stackContent [ innerElement; separated ] ] + let stackWithSeparators elements = + elements + |> List.map box + |> List.intersperse (box (Separator true)) + |> stackContent diff --git a/vsintegration/src/FSharp.Editor/QuickInfo/WpfFactories.fs b/vsintegration/src/FSharp.Editor/QuickInfo/WpfFactories.fs index 607b1e2f3d1..1e13f13a344 100644 --- a/vsintegration/src/FSharp.Editor/QuickInfo/WpfFactories.fs +++ b/vsintegration/src/FSharp.Editor/QuickInfo/WpfFactories.fs @@ -11,43 +11,66 @@ open Microsoft.VisualStudio.Text.Editor open Microsoft.VisualStudio.Utilities open Microsoft.VisualStudio.FSharp.Editor +open Microsoft.VisualStudio.Text.Classification -type Separator = Separator +type Separator = + | Separator of visible: bool + // preserve old behavior on mac + override this.ToString() = + match this with + | Separator true -> XmlDocumentation.separatorText + | _ -> System.Environment.NewLine [)>] -[] -[, typeof)>] -type WpfNavigableTextRunFactory [] (viewElementFactoryService: IViewElementFactoryService, settings: EditorOptions) = +[] +[, typeof)>] +type WpfClassifiedTextElementFactory [] + ( + classificationformatMapService: IClassificationFormatMapService, + classificationTypeRegistry: IClassificationTypeRegistryService, + settings: EditorOptions + ) = let resources = Microsoft.VisualStudio.FSharp.UIResources.NavStyles().Resources + let formatMap = classificationformatMapService.GetClassificationFormatMap("tooltip") interface IViewElementFactory with - member _.CreateViewElement(textView: ITextView, model: obj) = + member _.CreateViewElement(_textView: ITextView, model: obj) = match model with - | :? ClassifiedTextRun as classifiedTextRun -> - // use the default converters to get a UIElement - let classifiedTextElement = ClassifiedTextElement([ classifiedTextRun ]) - - let convertedElement = - viewElementFactoryService.CreateViewElement(textView, classifiedTextElement) - // Apply custom underline. - match convertedElement with - | :? TextBlock as tb when classifiedTextRun.NavigationAction <> null && settings.QuickInfo.DisplayLinks -> - match tb.Inlines.FirstInline with - | :? Documents.Hyperlink as hyperlink -> + | :? ClassifiedTextElement as text -> + let tb = TextBlock() + tb.FontSize <- formatMap.DefaultTextProperties.FontRenderingEmSize + tb.FontFamily <- formatMap.DefaultTextProperties.Typeface.FontFamily + tb.TextWrapping <- TextWrapping.Wrap + + for run in text.Runs do + let ctype = + classificationTypeRegistry.GetClassificationType(run.ClassificationTypeName) + + let props = formatMap.GetTextProperties(ctype) + let inl = Documents.Run(run.Text, Foreground = props.ForegroundBrush) + + match run.NavigationAction |> Option.ofObj with + | Some action -> + let link = + { new Documents.Hyperlink(inl) with + override _.OnClick() = action.Invoke() + } + let key = match settings.QuickInfo.UnderlineStyle with | QuickInfoUnderlineStyle.Solid -> "solid_underline" | QuickInfoUnderlineStyle.Dash -> "dash_underline" | QuickInfoUnderlineStyle.Dot -> "dot_underline" - // Fix color and apply styles. - hyperlink.Foreground <- hyperlink.Inlines.FirstInline.Foreground - hyperlink.Style <- downcast resources[key] - | _ -> () - | _ -> () - box convertedElement :?> _ + link.Style <- downcast resources[key] + link.Foreground <- props.ForegroundBrush + tb.Inlines.Add(link) + | _ -> tb.Inlines.Add(inl) + + box tb :?> _ | _ -> - failwith $"Invalid type conversion. Supported conversion is {typeof.Name} to {typeof.Name}." + failwith + $"Invalid type conversion. Supported conversion is {typeof.Name} to {typeof.Name}." [)>] [] @@ -56,5 +79,11 @@ type WpfSeparatorFactory() = interface IViewElementFactory with member _.CreateViewElement(_, model: obj) = match model with - | :? Separator -> Controls.Separator(Opacity = 0.4, Margin = Thickness(0, 10, 0, 10)) |> box :?> _ + | :? Separator as Separator visible -> + if visible then + Controls.Separator(Opacity = 0.3, Margin = Thickness(0, 8, 0, 8)) + else + Controls.Separator(Opacity = 0) + |> box + :?> _ | _ -> failwith $"Invalid type conversion. Supported conversion is {typeof.Name} to {typeof.Name}." diff --git a/vsintegration/tests/FSharp.Editor.Tests/QuickInfoProviderTests.fs b/vsintegration/tests/FSharp.Editor.Tests/QuickInfoProviderTests.fs index 26402791e74..91e504577f8 100644 --- a/vsintegration/tests/FSharp.Editor.Tests/QuickInfoProviderTests.fs +++ b/vsintegration/tests/FSharp.Editor.Tests/QuickInfoProviderTests.fs @@ -16,71 +16,93 @@ type public AssemblyResolverTestFixture() = member public __.Init() = AssemblyResolver.addResolver () module QuickInfoProviderTests = + type Expected = + | QuickInfo of description: string * docs: string + | Desc of string + | Empty + | Error - let filePath = "C:\\test.fs" + type TestCase = TestCase of prompt: string * Expected let private normalizeLineEnds (s: string) = s.Replace("\r\n", "\n").Replace("\n\n", "\n") - let private tooltipTextToRawString (ToolTipText elements) : string = - let rec parseElement = - function - | ToolTipElement.None -> "" - | ToolTipElement.Group (xs) -> - let descriptions = xs |> List.map (fun item -> item.MainDescription) + let mkFull prompt desc docs = + TestCase(prompt, QuickInfo(normalizeLineEnds desc, normalizeLineEnds docs)) + + let mkDesc prompt desc = + TestCase(prompt, Desc(normalizeLineEnds desc)) + + let mkNone prompt = TestCase(prompt, Empty) + + let filePath = "C:\\test.fs" + + let private tooltipElementToExpected expected = + function + | ToolTipElement.None -> Empty + | ToolTipElement.Group (xs) -> + let descriptions = xs |> List.map (fun item -> item.MainDescription) - let descriptionTexts = - descriptions - |> List.map (fun taggedTexts -> taggedTexts |> Array.map (fun taggedText -> taggedText.Text)) + let descriptionTexts = + descriptions + |> List.map (fun taggedTexts -> taggedTexts |> Array.map (fun taggedText -> taggedText.Text)) - let descriptionText = descriptionTexts |> Array.concat |> String.concat "" + let descriptionText = descriptionTexts |> Array.concat |> String.concat "" - let remarks = xs |> List.choose (fun item -> item.Remarks) + let remarks = xs |> List.choose (fun item -> item.Remarks) - let remarkTexts = - remarks |> Array.concat |> Array.map (fun taggedText -> taggedText.Text) + let remarkTexts = + remarks |> Array.concat |> Array.map (fun taggedText -> taggedText.Text) - let remarkText = - (match remarks with - | [] -> "" - | _ -> "\n" + String.concat "" remarkTexts) + let remarkText = + (match remarks with + | [] -> "" + | _ -> "\n" + String.concat "" remarkTexts) - let tps = xs |> List.collect (fun item -> item.TypeMapping) + let tps = xs |> List.collect (fun item -> item.TypeMapping) - let tpTexts = - tps |> List.map (fun x -> x |> Array.map (fun y -> y.Text) |> String.concat "") + let tpTexts = + tps |> List.map (fun x -> x |> Array.map (fun y -> y.Text) |> String.concat "") - let tpText = - (match tps with - | [] -> "" - | _ -> "\n" + String.concat "\n" tpTexts) + let tpText = + (match tps with + | [] -> "" + | _ -> "\n" + String.concat "\n" tpTexts) - descriptionText + remarkText + tpText - | ToolTipElement.CompositionError (error) -> error + let collectDocs (element: ToolTipElementData) = + match element.XmlDoc with + | FSharp.Compiler.Symbols.FSharpXmlDoc.FromXmlText xmlDoc -> xmlDoc.UnprocessedLines |> String.concat "\n" + | _ -> "" - elements |> List.map parseElement |> String.concat "\n" |> normalizeLineEnds + let desc = + [ descriptionText; remarkText; tpText ] |> String.concat "" |> normalizeLineEnds + + let docs = xs |> List.map collectDocs |> String.concat "" |> normalizeLineEnds + + match expected with + | QuickInfo _ -> QuickInfo(desc, docs) + | _ -> Desc desc + + | ToolTipElement.CompositionError (error) -> Error let executeQuickInfoTest (programText: string) testCases = let document = RoslynTestHelpers.CreateSolution(programText) |> RoslynTestHelpers.GetSingleDocument - for (symbol: string, expected: string option) in testCases do - let expected = - expected - |> Option.map normalizeLineEnds - |> Option.map (fun s -> s.Replace("___", "")) - + for TestCase (symbol, expected) in testCases do let caretPosition = programText.IndexOf(symbol) + symbol.Length - 1 let quickInfo = - FSharpAsyncQuickInfoSource.ProvideQuickInfo(document, caretPosition) + FSharpAsyncQuickInfoSource.TryGetToolTip(document, caretPosition) |> Async.RunSynchronously let actual = - quickInfo |> Option.map (fun qi -> tooltipTextToRawString qi.StructuredText) + quickInfo + |> Option.map (fun (_, _, _, ToolTipText elements) -> elements |> List.map (tooltipElementToExpected expected)) + |> Option.defaultValue [ Empty ] - actual |> Assert.shouldBeEqualWith expected $"Symbol: {symbol}" + actual.Head |> Assert.shouldBeEqualWith expected $"Symbol: {symbol}" [] let ShouldShowQuickInfoAtCorrectPositions () = @@ -93,20 +115,20 @@ System.Console.WriteLine(x + y) let testCases = [ - "let", Some "let___Used to associate, or bind, a name to a value or function." - "x", Some "val x: int\nFull name: Test.x" - "y", Some "val y: int\nFull name: Test.y" - "1", None - "2", None - "x +", - Some + mkFull "let" "let" "Used to associate, or bind, a name to a value or function." + mkDesc "x" "val x: int\nFull name: Test.x" + mkDesc "y" "val y: int\nFull name: Test.y" + mkNone "1" + mkNone "2" + mkDesc + "x +" """val (+) : x: 'T1 -> y: 'T2 -> 'T3 (requires member (+)) Full name: Microsoft.FSharp.Core.Operators.(+) 'T1 is int 'T2 is int 'T3 is int""" - "System", Some "namespace System" - "WriteLine", Some "System.Console.WriteLine(value: int) : unit" + mkDesc "System" "namespace System" + mkDesc "WriteLine" "System.Console.WriteLine(value: int) : unit" ] executeQuickInfoTest fileContents testCases @@ -122,17 +144,20 @@ module internal MyModule = let testCases = [ - "namespace", - Some - "namespace___Used to associate a name with a group of related types and modules, to logically separate it from other code." - "module", - Some - "module___Used to associate a name with a group of related types, values, and functions, to logically separate it from other code." - "internal", Some "internal___Used to specify that a member is visible inside an assembly but not outside it." - "val", Some "val___Used in a signature to indicate a value, or in a type to declare a member, in limited situations." - "->", - Some - "->___In function types, delimits arguments and return values. Yields an expression (in sequence expressions); equivalent to the yield keyword. Used in match expressions" + mkFull + "namespace" + "namespace" + "Used to associate a name with a group of related types and modules, to logically separate it from other code." + mkFull + "module" + "module" + "Used to associate a name with a group of related types, values, and functions, to logically separate it from other code." + mkFull "internal" "internal" "Used to specify that a member is visible inside an assembly but not outside it." + mkFull "val" "val" "Used in a signature to indicate a value, or in a type to declare a member, in limited situations." + mkFull + "->" + "->" + "In function types, delimits arguments and return values. Yields an expression (in sequence expressions); equivalent to the yield keyword. Used in match expressions" ] executeQuickInfoTest fileContents testCases @@ -157,8 +182,8 @@ let x = let testCases = [ - "let!", Some "let!___Used in computation expressions to bind a name to the result of another computation expression." - "return", Some "return___Used to provide a value for the result of the containing computation expression." + mkFull "let!" "let!" "Used in computation expressions to bind a name to the result of another computation expression." + mkFull "return" "return" "Used to provide a value for the result of the containing computation expression." ] executeQuickInfoTest fileContents testCases @@ -167,7 +192,7 @@ let x = let ShouldShowQuickInfoForGenericParameters () = let fileContents = """ - + type C() = member x.FSharpGenericMethodExplitTypeParams<'T>(a:'T, y:'T) = (a,y) @@ -194,94 +219,94 @@ let res8 = abs 5.0 let testCases = [ - "GroupBy", - Some + mkDesc + "GroupBy" "(extension) System.Collections.Generic.IEnumerable.GroupBy<'TSource,'TKey>(keySelector: System.Func<'TSource,'TKey>) : System.Collections.Generic.IEnumerable> 'TSource is int * string 'TKey is int" - "Sort", - Some + mkDesc + "Sort" "System.Array.Sort<'T>(array: 'T array) : unit 'T is int" - "let test4 x = C().FSharpGenericMethodExplitTypeParams", - Some + mkDesc + "let test4 x = C().FSharpGenericMethodExplitTypeParams" "member C.FSharpGenericMethodExplitTypeParams: a: 'T0 * y: 'T0 -> 'T0 * 'T0 'T is 'a list" - "let test5<'U> (x: 'U) = C().FSharpGenericMethodExplitTypeParams", - Some + mkDesc + "let test5<'U> (x: 'U) = C().FSharpGenericMethodExplitTypeParams" "member C.FSharpGenericMethodExplitTypeParams: a: 'T0 * y: 'T0 -> 'T0 * 'T0 'T is 'U list" - "let test6 = C().FSharpGenericMethodExplitTypeParams", - Some + mkDesc + "let test6 = C().FSharpGenericMethodExplitTypeParams" "member C.FSharpGenericMethodExplitTypeParams: a: 'T0 * y: 'T0 -> 'T0 * 'T0 'T is int" - "let test7 x = C().FSharpGenericMethodInferredTypeParams", - Some + mkDesc + "let test7 x = C().FSharpGenericMethodInferredTypeParams" "member C.FSharpGenericMethodInferredTypeParams: a: 'a1 * y: 'b2 -> 'a1 * 'b2 'a is 'a0 list 'b is 'a0 list" - "let test8 = C().FSharpGenericMethodInferredTypeParams", - Some + mkDesc + "let test8 = C().FSharpGenericMethodInferredTypeParams" "member C.FSharpGenericMethodInferredTypeParams: a: 'a0 * y: 'b1 -> 'a0 * 'b1 'a is int 'b is int" - "let test9<'U> (x: 'U) = C().FSharpGenericMethodInferredTypeParams", - Some + mkDesc + "let test9<'U> (x: 'U) = C().FSharpGenericMethodInferredTypeParams" "member C.FSharpGenericMethodInferredTypeParams: a: 'a0 * y: 'b1 -> 'a0 * 'b1 'a is 'U list 'b is 'U list" - "let res3 = [1] |>", - Some + mkDesc + "let res3 = [1] |>" "val (|>) : arg: 'T1 -> func: ('T1 -> 'U) -> 'U Full name: Microsoft.FSharp.Core.Operators.(|>) 'T1 is int list 'U is int list" - "let res3 = [1] |> List.map id", - Some + mkDesc + "let res3 = [1] |> List.map id" "val id: x: 'T -> 'T Full name: Microsoft.FSharp.Core.Operators.id 'T is int" - "let res4 = (1.0,[1]) ||>", - Some + mkDesc + "let res4 = (1.0,[1]) ||>" "val (||>) : arg1: 'T1 * arg2: 'T2 -> func: ('T1 -> 'T2 -> 'U) -> 'U Full name: Microsoft.FSharp.Core.Operators.(||>) 'T1 is float 'T2 is int list 'U is float" - "let res4 = (1.0,[1]) ||> List.fold", - Some + mkDesc + "let res4 = (1.0,[1]) ||> List.fold" "val fold: folder: ('State -> 'T -> 'State) -> state: 'State -> list: 'T list -> 'State Full name: Microsoft.FSharp.Collections.List.fold 'T is int 'State is float" - "let res4 = (1.0,[1]) ||> List.fold (fun s x -> string s +", - Some + mkDesc + "let res4 = (1.0,[1]) ||> List.fold (fun s x -> string s +" "val (+) : x: 'T1 -> y: 'T2 -> 'T3 (requires member (+)) Full name: Microsoft.FSharp.Core.Operators.(+) 'T1 is string 'T2 is string 'T3 is float" - "let res5 = 1 +", - Some + mkDesc + "let res5 = 1 +" "val (+) : x: 'T1 -> y: 'T2 -> 'T3 (requires member (+)) Full name: Microsoft.FSharp.Core.Operators.(+) 'T1 is int 'T2 is int 'T3 is int" - "let res6 = System.DateTime.Now +", - Some + mkDesc + "let res6 = System.DateTime.Now +" "val (+) : x: 'T1 -> y: 'T2 -> 'T3 (requires member (+)) Full name: Microsoft.FSharp.Core.Operators.(+) 'T1 is System.DateTime 'T2 is System.TimeSpan 'T3 is System.DateTime" - "let res7 = sin", - Some + mkDesc + "let res7 = sin" "val sin: value: 'T -> 'T (requires member Sin) Full name: Microsoft.FSharp.Core.Operators.sin 'T is float" - "let res8 = abs", - Some + mkDesc + "let res8 = abs" "val abs: value: 'T -> 'T (requires member Abs) Full name: Microsoft.FSharp.Core.Operators.abs 'T is int" diff --git a/vsintegration/tests/FSharp.Editor.Tests/QuickInfoTests.fs b/vsintegration/tests/FSharp.Editor.Tests/QuickInfoTests.fs index 4d4730f82cc..706d79c59bf 100644 --- a/vsintegration/tests/FSharp.Editor.Tests/QuickInfoTests.fs +++ b/vsintegration/tests/FSharp.Editor.Tests/QuickInfoTests.fs @@ -8,6 +8,7 @@ open Xunit open FSharp.Editor.Tests.Helpers module QuickInfo = + open FSharp.Compiler.EditorServices let private GetCaretPosition (codeWithCaret: string) = let caretSentinel = "$$" @@ -26,11 +27,12 @@ module QuickInfo = cursorInfo let internal GetQuickInfo (code: string) caretPosition = - async { + asyncMaybe { let document = RoslynTestHelpers.CreateSolution(code) |> RoslynTestHelpers.GetSingleDocument - return! FSharpAsyncQuickInfoSource.ProvideQuickInfo(document, caretPosition) + let! _, _, _, tooltip = FSharpAsyncQuickInfoSource.TryGetToolTip(document, caretPosition) + return tooltip } |> Async.RunSynchronously @@ -39,15 +41,15 @@ module QuickInfo = let sigHelp = GetQuickInfo code caretPosition match sigHelp with - | Some (quickInfo) -> + | Some (ToolTipText elements) when not elements.IsEmpty -> let documentationBuilder = { new IDocumentationBuilder with override _.AppendDocumentationFromProcessedXML(_, _, _, _, _, _) = () override _.AppendDocumentation(_, _, _, _, _, _, _) = () } - let mainDescription, docs = - FSharpAsyncQuickInfoSource.BuildSingleQuickInfoItem documentationBuilder quickInfo + let _, mainDescription, docs = + XmlDocumentation.BuildSingleTipText(documentationBuilder, elements.Head, XmlDocumentation.DefaultLineLimits) let mainTextItems = mainDescription |> Seq.map (fun x -> x.Text) let docTextItems = docs |> Seq.map (fun x -> x.Text)