From 76ac59341fca0b21eeb28cdb3a084fd0ca4da800 Mon Sep 17 00:00:00 2001 From: Maxime Mangel Date: Mon, 13 Mar 2023 23:22:22 +0100 Subject: [PATCH 01/10] Support example tag inside of summary tag --- src/FsAutoComplete.Core/TipFormatter.fs | 44 ++++++++++++++++++++----- 1 file changed, 36 insertions(+), 8 deletions(-) diff --git a/src/FsAutoComplete.Core/TipFormatter.fs b/src/FsAutoComplete.Core/TipFormatter.fs index 14b7a4c82..72db11939 100644 --- a/src/FsAutoComplete.Core/TipFormatter.fs +++ b/src/FsAutoComplete.Core/TipFormatter.fs @@ -1,4 +1,4 @@ -// -------------------------------------------------------------------------------------- +// -------------------------------------------------------------------------------------- // (c) Tomas Petricek, http://tomasp.net/blog // -------------------------------------------------------------------------------------- module FsAutoComplete.TipFormatter @@ -163,18 +163,45 @@ module private Format = | None -> "forceNoHighlight" + // We need to trim the end of the text because the + // user write XML comments with a space between the '///' + // and the '' tag. Then it mess up identifation of new lines + // at the end of the code snippet. + // Example: + // /// + // /// var x = 1; + // /// + // ^ This space is the one we need to remove + let innerText = innerText.TrimEnd() + + // Try to detect how the code snippet is formatted + // so render the markdown code block the best way + // by avoid empty lines at the beginning or the end let formattedText = - if innerText.Contains("\n") then + match innerText.StartsWith("\n"), innerText.EndsWith("\n") with + | true, true -> + sprintf "```%s%s```" lang innerText + | true, false -> + sprintf "```%s%s\n```" lang innerText + | false, true -> + sprintf "```%s\n%s```" lang innerText + | false, false -> + sprintf "```%s\n%s\n```" lang innerText - if innerText.StartsWith("\n") then + Some formattedText - sprintf "```%s%s\n```" lang innerText + } + |> applyFormatter - else - sprintf "```%s\n%s\n```" lang innerText + let private example = + { TagName = "example" + Formatter = + function + | VoidElement _ -> None - else - sprintf "`%s`" innerText + | NonVoidElement(innerText, _) -> + let formattedText = + Section.addSection "Example" innerText Some formattedText @@ -668,6 +695,7 @@ module private Format = |> removeInvalidOrBlock // Start the transformation process |> paragraph + |> example |> block |> codeInline |> codeBlock From bcc820b45cc93050eb5c40c1973c33fed850559b Mon Sep 17 00:00:00 2001 From: Maxime Mangel Date: Tue, 14 Mar 2023 11:04:35 +0100 Subject: [PATCH 02/10] Improve visual of nested example inside of another section --- src/FsAutoComplete.Core/TipFormatter.fs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/FsAutoComplete.Core/TipFormatter.fs b/src/FsAutoComplete.Core/TipFormatter.fs index 72db11939..4f66e4449 100644 --- a/src/FsAutoComplete.Core/TipFormatter.fs +++ b/src/FsAutoComplete.Core/TipFormatter.fs @@ -201,7 +201,12 @@ module private Format = | NonVoidElement(innerText, _) -> let formattedText = - Section.addSection "Example" innerText + nl + nl + // This try to keep a visual consistency and indicate that this + // "Example section" is part of it parent section (summary, remarks, etc.) + + """Example:""" + + nl + nl + + innerText Some formattedText From f78450b1ae5b8e876e88f845c06f22d4c6d9f8ce Mon Sep 17 00:00:00 2001 From: Maxime Mangel Date: Fri, 7 Apr 2023 14:36:22 +0200 Subject: [PATCH 03/10] Various improvement around TipFormatter and Documentation - Use records instead of union for passing the data around - Truncated global example from the tooltip - Display a link to open the documentation from the tooltip - Reduce logic duplication by moving more into TipFormatter - Fix info panel rendering when navigating via symbol inside of the user code --- .../DocumentationFormatter.fs | 77 ++-- .../ParseAndCheckResults.fs | 49 ++- src/FsAutoComplete.Core/TipFormatter.fs | 399 ++++++++++++------ src/FsAutoComplete/CommandResponse.fs | 167 +++++--- .../LspServers/AdaptiveFSharpLspServer.fs | 101 +++-- .../LspServers/FsAutoComplete.Lsp.fs | 94 +++-- 6 files changed, 569 insertions(+), 318 deletions(-) diff --git a/src/FsAutoComplete.Core/DocumentationFormatter.fs b/src/FsAutoComplete.Core/DocumentationFormatter.fs index bc950398e..c7d985781 100644 --- a/src/FsAutoComplete.Core/DocumentationFormatter.fs +++ b/src/FsAutoComplete.Core/DocumentationFormatter.fs @@ -16,7 +16,22 @@ module DocumentationFormatter = let mutable lastDisplayContext: FSharpDisplayContext = FSharpDisplayContext.Empty - let emptyTypeTip = [||], [||], [||], [||], [||], [||] + type EntityInfo = + { Constructors: string array + Fields: string array + Functions: string array + Interfaces: string array + Attributes: string array + DeclaredTypes: string array } + + static member Empty = + + { Constructors = [||] + Fields = [||] + Functions = [||] + Interfaces = [||] + Attributes = [||] + DeclaredTypes = [||] } /// Concat two strings with a space between if both a and b are not IsNullOrWhiteSpace let internal (++) (a: string) (b: string) = @@ -719,8 +734,12 @@ module DocumentationFormatter = ++ fst (formatShowDocumentationLink ne.DisplayName ne.XmlDocSig ne.Assembly.SimpleName)) |> Seq.toArray - - constrc, fields, funcs, interfaces, attrs, types + { Constructors = constrc + Fields = fields + Functions = funcs + Interfaces = interfaces + Attributes = attrs + DeclaredTypes = types } let typeDisplay = let name = @@ -756,9 +775,9 @@ module DocumentationFormatter = if fse.IsFSharpUnion then (typeDisplay + uniontip ()), typeTip () elif fse.IsEnum then - (typeDisplay + enumtip ()), emptyTypeTip + (typeDisplay + enumtip ()), EntityInfo.Empty elif fse.IsDelegate then - (typeDisplay + delegateTip ()), emptyTypeTip + (typeDisplay + delegateTip ()), EntityInfo.Empty else typeDisplay, typeTip () @@ -867,60 +886,60 @@ module DocumentationFormatter = | Some ent when ent.IsValueType || ent.IsEnum -> //ValueTypes let signature = getFuncSignature symbol.DisplayContext func - Some((signature, emptyTypeTip), footerForType symbol, cn) + Some((signature, EntityInfo.Empty), footerForType symbol, cn) | _ -> //ReferenceType constructor let signature = getFuncSignature symbol.DisplayContext func - Some((signature, emptyTypeTip), footerForType symbol, cn) + Some((signature, EntityInfo.Empty), footerForType symbol, cn) | SymbolUse.Operator func -> let signature = getFuncSignature symbol.DisplayContext func - Some((signature, emptyTypeTip), footerForType symbol, cn) + Some((signature, EntityInfo.Empty), footerForType symbol, cn) | SymbolUse.Pattern func -> //Active pattern or operator let signature = getFuncSignature symbol.DisplayContext func - Some((signature, emptyTypeTip), footerForType symbol, cn) + Some((signature, EntityInfo.Empty), footerForType symbol, cn) | SymbolUse.Property prop -> let signature = getFuncSignature symbol.DisplayContext prop - Some((signature, emptyTypeTip), footerForType symbol, cn) + Some((signature, EntityInfo.Empty), footerForType symbol, cn) | SymbolUse.ClosureOrNestedFunction func -> //represents a closure or nested function let signature = getFuncSignature symbol.DisplayContext func - Some((signature, emptyTypeTip), footerForType symbol, cn) + Some((signature, EntityInfo.Empty), footerForType symbol, cn) | SymbolUse.Function func -> let signature = getFuncSignature symbol.DisplayContext func - Some((signature, emptyTypeTip), footerForType symbol, cn) + Some((signature, EntityInfo.Empty), footerForType symbol, cn) | SymbolUse.Val func -> //val name : Type let signature = getValSignature symbol.DisplayContext func - Some((signature, emptyTypeTip), footerForType symbol, cn) + Some((signature, EntityInfo.Empty), footerForType symbol, cn) | SymbolUse.Field fsf -> let signature = getFieldSignature symbol.DisplayContext fsf - Some((signature, emptyTypeTip), footerForType symbol, cn) + Some((signature, EntityInfo.Empty), footerForType symbol, cn) | SymbolUse.UnionCase uc -> let signature = getUnioncaseSignature symbol.DisplayContext uc - Some((signature, emptyTypeTip), footerForType symbol, cn) + Some((signature, EntityInfo.Empty), footerForType symbol, cn) | SymbolUse.ActivePatternCase apc -> let signature = getAPCaseSignature symbol.DisplayContext apc - Some((signature, emptyTypeTip), footerForType symbol, cn) + Some((signature, EntityInfo.Empty), footerForType symbol, cn) | SymbolUse.ActivePattern ap -> let signature = getFuncSignature symbol.DisplayContext ap - Some((signature, emptyTypeTip), footerForType symbol, cn) + Some((signature, EntityInfo.Empty), footerForType symbol, cn) | SymbolUse.GenericParameter gp -> let signature = $"'%s{gp.Name} (requires %s{formatGenericParameter false symbol.DisplayContext gp})" - Some((signature, emptyTypeTip), footerForType symbol, cn) + Some((signature, EntityInfo.Empty), footerForType symbol, cn) | _ -> None @@ -942,51 +961,51 @@ module DocumentationFormatter = | Some ent when ent.IsValueType || ent.IsEnum -> //ValueTypes let signature = getFuncSignature lastDisplayContext func - Some((signature, emptyTypeTip), footerForType' symbol, cn) + Some((signature, EntityInfo.Empty), footerForType' symbol, cn) | _ -> //ReferenceType constructor let signature = getFuncSignature lastDisplayContext func - Some((signature, emptyTypeTip), footerForType' symbol, cn) + Some((signature, EntityInfo.Empty), footerForType' symbol, cn) | SymbolPatterns.Operator func -> let signature = getFuncSignature lastDisplayContext func - Some((signature, emptyTypeTip), footerForType' symbol, cn) + Some((signature, EntityInfo.Empty), footerForType' symbol, cn) | Property prop -> let signature = getFuncSignature lastDisplayContext prop - Some((signature, emptyTypeTip), footerForType' symbol, cn) + Some((signature, EntityInfo.Empty), footerForType' symbol, cn) | ClosureOrNestedFunction func -> //represents a closure or nested function let signature = getFuncSignature lastDisplayContext func - Some((signature, emptyTypeTip), footerForType' symbol, cn) + Some((signature, EntityInfo.Empty), footerForType' symbol, cn) | Function func -> let signature = getFuncSignature lastDisplayContext func - Some((signature, emptyTypeTip), footerForType' symbol, cn) + Some((signature, EntityInfo.Empty), footerForType' symbol, cn) | Val func -> //val name : Type let signature = getValSignature lastDisplayContext func - Some((signature, emptyTypeTip), footerForType' symbol, cn) + Some((signature, EntityInfo.Empty), footerForType' symbol, cn) | Field(fsf, _) -> let signature = getFieldSignature lastDisplayContext fsf - Some((signature, emptyTypeTip), footerForType' symbol, cn) + Some((signature, EntityInfo.Empty), footerForType' symbol, cn) | UnionCase uc -> let signature = getUnioncaseSignature lastDisplayContext uc - Some((signature, emptyTypeTip), footerForType' symbol, cn) + Some((signature, EntityInfo.Empty), footerForType' symbol, cn) | ActivePatternCase apc -> let signature = getAPCaseSignature lastDisplayContext apc - Some((signature, emptyTypeTip), footerForType' symbol, cn) + Some((signature, EntityInfo.Empty), footerForType' symbol, cn) | GenericParameter gp -> let signature = $"'%s{gp.Name} (requires %s{formatGenericParameter false lastDisplayContext gp})" - Some((signature, emptyTypeTip), footerForType' symbol, cn) + Some((signature, EntityInfo.Empty), footerForType' symbol, cn) | _ -> None diff --git a/src/FsAutoComplete.Core/ParseAndCheckResults.fs b/src/FsAutoComplete.Core/ParseAndCheckResults.fs index 78d7dd609..54ad5a6fc 100644 --- a/src/FsAutoComplete.Core/ParseAndCheckResults.fs +++ b/src/FsAutoComplete.Core/ParseAndCheckResults.fs @@ -22,6 +22,21 @@ type FindDeclarationResult = /// The declaration refers to a file. | File of string +[] +module TryGetToolTipEnhancedResult = + + type SymbolInfo = + | Keyword of string + | Symbol of + {| XmlDocSig: string + Assembly: string |} + +type TryGetToolTipEnhancedResult = + { ToolTipText: ToolTipText + Signature: string + Footer: string + SymbolInfo: TryGetToolTipEnhancedResult.SymbolInfo } + type ParseAndCheckResults (parseResults: FSharpParseFileResults, checkResults: FSharpCheckFileResults, entityCache: EntityCache) = @@ -320,7 +335,10 @@ type ParseAndCheckResults | _ -> ResultOrString.Error "No tooltip information" | _ -> Ok(tip) - member x.TryGetToolTipEnhanced (pos: Position) (lineStr: LineStr) = + member x.TryGetToolTipEnhanced + (pos: Position) + (lineStr: LineStr) + : Result, string> = let (|EmptyTooltip|_|) (ToolTipText elems) = match elems with | [] -> Some() @@ -347,7 +365,13 @@ type ParseAndCheckResults match identIsland with | [ ident ] -> match KeywordList.keywordTooltips.TryGetValue ident with - | true, tip -> Ok(Some(tip, ident, "", None)) + | true, tip -> + { ToolTipText = tip + Signature = ident + Footer = "" + SymbolInfo = TryGetToolTipEnhancedResult.Keyword ident } + |> Some + |> Ok | _ -> Error "No tooltip information" | _ -> Error "No tooltip information" | _ -> @@ -355,13 +379,24 @@ type ParseAndCheckResults | None -> Error "No tooltip information" | Some symbol -> + // Retrieve the FSharpSymbol instance so we can find the XmlDocSig + // This mimic, the behavior of the Info Panel on hover + // 1. If this is a concrete type it returns that type reference + // 2. If this a type alias, it returns the aliases type reference + let resolvedType = symbol.Symbol.GetAbbreviatedParent() + match SignatureFormatter.getTooltipDetailsFromSymbolUse symbol with | None -> Error "No tooltip information" | Some(signature, footer) -> - let typeDoc = - getTypeIfConstructor symbol.Symbol |> Option.map (fun n -> n.XmlDocSig) - - Ok(Some(tip, signature, footer, typeDoc)) + { ToolTipText = tip + Signature = signature + Footer = footer + SymbolInfo = + TryGetToolTipEnhancedResult.Symbol + {| XmlDocSig = resolvedType.XmlDocSig + Assembly = symbol.Symbol.Assembly.SimpleName |} } + |> Some + |> Ok member __.TryGetFormattedDocumentation (pos: Position) (lineStr: LineStr) = match Lexer.findLongIdents (pos.Column, lineStr) with @@ -380,7 +415,7 @@ type ParseAndCheckResults match identIsland with | [ ident ] -> match KeywordList.keywordTooltips.TryGetValue ident with - | true, tip -> Ok(Some tip, None, (ident, (DocumentationFormatter.emptyTypeTip)), "", "") + | true, tip -> Ok(Some tip, None, (ident, DocumentationFormatter.EntityInfo.Empty), "", "") | _ -> Error "No tooltip information" | _ -> Error "No documentation information" | _ -> diff --git a/src/FsAutoComplete.Core/TipFormatter.fs b/src/FsAutoComplete.Core/TipFormatter.fs index 4f66e4449..4332084c7 100644 --- a/src/FsAutoComplete.Core/TipFormatter.fs +++ b/src/FsAutoComplete.Core/TipFormatter.fs @@ -12,6 +12,7 @@ open FSharp.Compiler.EditorServices open FSharp.Compiler.Symbols open FsAutoComplete.Logging open FSharp.Compiler.Text +open Ionide.LanguageServerProtocol.Types let inline nl<'T> = Environment.NewLine @@ -179,14 +180,10 @@ module private Format = // by avoid empty lines at the beginning or the end let formattedText = match innerText.StartsWith("\n"), innerText.EndsWith("\n") with - | true, true -> - sprintf "```%s%s```" lang innerText - | true, false -> - sprintf "```%s%s\n```" lang innerText - | false, true -> - sprintf "```%s\n%s```" lang innerText - | false, false -> - sprintf "```%s\n%s\n```" lang innerText + | true, true -> sprintf "```%s%s```" lang innerText + | true, false -> sprintf "```%s%s\n```" lang innerText + | false, true -> sprintf "```%s\n%s```" lang innerText + | false, false -> sprintf "```%s\n%s\n```" lang innerText Some formattedText @@ -201,11 +198,13 @@ module private Format = | NonVoidElement(innerText, _) -> let formattedText = - nl + nl + nl + + nl // This try to keep a visual consistency and indicate that this // "Example section" is part of it parent section (summary, remarks, etc.) + """Example:""" - + nl + nl + + nl + + nl + innerText Some formattedText @@ -715,6 +714,13 @@ module private Format = |> handleMicrosoftOrList |> unescapeSpecialCharacters +[] +type FormatCommentStyle = + | Legacy + | FullEnhanced + | SummaryOnly + | Documentation + // TODO: Improve this parser. Is there any other XmlDoc parser available? type private XmlDocMember(doc: XmlDocument, indentationSize: int, columnOffset: int) = /// References used to detect if we should remove meaningless spaces @@ -758,7 +764,17 @@ type private XmlDocMember(doc: XmlDocument, indentationSize: int, columnOffset: |> Seq.tryHead let rawExamples = - doc.DocumentElement.GetElementsByTagName "example" |> Seq.cast + doc.DocumentElement.GetElementsByTagName "example" + |> Seq.cast + // We need to filter out the examples node that are children + // of another "main" node + // This is because if the example node is inside a "main" node + // then we render it in place. + // So we don't need to render it independently in the Examples section + |> Seq.filter (fun node -> + [ "summary"; "param"; "returns"; "exception"; "remarks"; "typeparam" ] + |> List.contains node.ParentNode.Name + |> not) let readNamedContentAsKvPair (key, content) = KeyValuePair(key, readContentForTooltip content) @@ -803,6 +819,8 @@ type private XmlDocMember(doc: XmlDocument, indentationSize: int, columnOffset: else "**Description**" + nl + nl + summary + member __.HasTruncatedExamples = examples |> Seq.isEmpty |> not + member __.ToFullEnhancedString() = let content = summary @@ -811,7 +829,6 @@ type private XmlDocMember(doc: XmlDocument, indentationSize: int, columnOffset: + Section.fromKeyValueList "Parameters" parameters + Section.fromOption "Returns" returns + Section.fromKeyValueList "Exceptions" exceptions - + Section.fromList "Examples" examples + Section.fromList "See also" seeAlso // If we where unable to process the doc comment, then just output it as it is @@ -834,6 +851,14 @@ type private XmlDocMember(doc: XmlDocument, indentationSize: int, columnOffset: + Section.fromList "Examples" examples + Section.fromList "See also" seeAlso + member this.FormatComment(formatStyle: FormatCommentStyle) = + match formatStyle with + | FormatCommentStyle.Legacy -> this.ToString() + | FormatCommentStyle.SummaryOnly -> this.ToSummaryOnlyString() + | FormatCommentStyle.FullEnhanced -> this.ToFullEnhancedString() + | FormatCommentStyle.Documentation -> this.ToDocumentationString() + + let rec private readXmlDoc (reader: XmlReader) (indentationSize: int) (acc: Map) = let acc' = match reader.Read() with @@ -920,23 +945,30 @@ let private getXmlDoc dllFile = xmlDocCache.AddOrUpdate(xmlFile, xmlDoc, (fun _ _ -> xmlDoc)) |> ignore Some xmlDoc - with ex -> + with _ -> None // TODO: Remove the empty map from cache to try again in the next request? -[] -type FormatCommentStyle = - | Legacy - | FullEnhanced - | SummaryOnly - | Documentation - // -------------------------------------------------------------------------------------- // Formatting of tool-tip information displayed in F# IntelliSense // -------------------------------------------------------------------------------------- -let private buildFormatComment cmt (formatStyle: FormatCommentStyle) (typeDoc: string option) = - match cmt with - | FSharpXmlDoc.FromXmlText xmldoc -> - try + +[] +type private TryGetXmlDocMemberResult = + | Some of XmlDocMember + | None + | Error + +[] +type TipFormatterResult<'T> = + | Success of 'T + | Error of string + | None + +let private tryGetXmlDocMember (xmlDoc: FSharpXmlDoc) = + try + match xmlDoc with + | FSharpXmlDoc.FromXmlText xmldoc -> + let document = xmldoc.GetXmlText() // We create a "fake" XML document in order to use the same parser for both libraries and user code let xml = sprintf "%s" document @@ -960,49 +992,28 @@ let private buildFormatComment cmt (formatStyle: FormatCommentStyle) (typeDoc: s let xmlDoc = XmlDocMember(doc, indentationSize, 0) - match formatStyle with - | FormatCommentStyle.Legacy -> xmlDoc.ToString() - | FormatCommentStyle.SummaryOnly -> xmlDoc.ToSummaryOnlyString() - | FormatCommentStyle.FullEnhanced -> xmlDoc.ToFullEnhancedString() - | FormatCommentStyle.Documentation -> xmlDoc.ToDocumentationString() - - with ex -> - logger.warn ( - Log.setMessage "TipFormatter - Error while parsing the doc comment" - >> Log.addExn ex - ) - - sprintf - "An error occured when parsing the doc comment, please check that your doc comment is valid.\n\nMore info can be found LSP output" - - | FSharpXmlDoc.FromXmlFile(dllFile, memberName) -> - match getXmlDoc dllFile with - | Some doc when doc.ContainsKey memberName -> - let typeDoc = - match typeDoc with - | Some s when doc.ContainsKey s -> - match formatStyle with - | FormatCommentStyle.Legacy -> doc.[s].ToString() - | FormatCommentStyle.SummaryOnly -> doc.[s].ToSummaryOnlyString() - | FormatCommentStyle.FullEnhanced -> doc.[s].ToFullEnhancedString() - | FormatCommentStyle.Documentation -> doc.[s].ToDocumentationString() - | _ -> "" - - match formatStyle with - | FormatCommentStyle.Legacy -> doc.[memberName].ToString() + (if typeDoc <> "" then "\n\n" + typeDoc else "") - | FormatCommentStyle.SummaryOnly -> - doc.[memberName].ToSummaryOnlyString() - + (if typeDoc <> "" then "\n\n" + typeDoc else "") - | FormatCommentStyle.FullEnhanced -> - doc.[memberName].ToFullEnhancedString() - + (if typeDoc <> "" then "\n\n" + typeDoc else "") - | FormatCommentStyle.Documentation -> - doc.[memberName].ToDocumentationString() - + (if typeDoc <> "" then "\n\n" + typeDoc else "") - | _ -> "" - | _ -> "" - -let formatTaggedText (t: TaggedText) : string = + TryGetXmlDocMemberResult.Some xmlDoc + + | FSharpXmlDoc.FromXmlFile(dllFile, memberName) -> + match getXmlDoc dllFile with + | Some doc when doc.ContainsKey memberName -> TryGetXmlDocMemberResult.Some doc.[memberName] + + | _ -> TryGetXmlDocMemberResult.None + + | FSharpXmlDoc.None -> TryGetXmlDocMemberResult.None + with ex -> + logger.warn ( + Log.setMessage "TipFormatter - Error while parsing the doc comment" + >> Log.addExn ex + ) + + TryGetXmlDocMemberResult.Error + +[] +let private ERROR_WHILE_PARSING_DOC_COMMENT = + "An error occured when parsing the doc comment, please check that your doc comment is valid.\n\nMore info can be found LSP output" + +let private formatTaggedText (t: TaggedText) : string = match t.Tag with | TextTag.ActivePatternResult | TextTag.UnionCase @@ -1039,13 +1050,13 @@ let formatTaggedText (t: TaggedText) : string = | TextTag.Record | TextTag.TypeParameter -> $"`{t.Text}`" -let formatUntaggedText (t: TaggedText) = t.Text +let private formatUntaggedText (t: TaggedText) = t.Text -let formatUntaggedTexts = Array.map formatUntaggedText >> String.concat "" +let private formatUntaggedTexts = Array.map formatUntaggedText >> String.concat "" -let formatTaggedTexts = Array.map formatTaggedText >> String.concat "" +let private formatTaggedTexts = Array.map formatTaggedText >> String.concat "" -let formatGenericParameters (typeMappings: TaggedText[] list) = +let private formatGenericParameters (typeMappings: TaggedText[] list) = typeMappings |> List.map (fun typeMap -> $"* {formatTaggedTexts typeMap}") |> String.concat nl @@ -1059,7 +1070,12 @@ let formatCompletionItemTip (ToolTipText tips) : (string * string) = let makeTooltip (tipElement: ToolTipElementData) = let header = formatUntaggedTexts tipElement.MainDescription - let body = buildFormatComment tipElement.XmlDoc FormatCommentStyle.Legacy None + let body = + match tryGetXmlDocMember tipElement.XmlDoc with + | TryGetXmlDocMemberResult.Some xmlDoc -> xmlDoc.FormatComment(FormatCommentStyle.Legacy) + | TryGetXmlDocMemberResult.None -> "" + | TryGetXmlDocMemberResult.Error -> ERROR_WHILE_PARSING_DOC_COMMENT + header, body items |> List.tryHead |> Option.map makeTooltip @@ -1075,82 +1091,191 @@ let formatPlainTip (ToolTipText tips) : (string * string) = | ToolTipElement.Group items -> let t = items |> Seq.head let signature = formatUntaggedTexts t.MainDescription - let description = buildFormatComment t.XmlDoc FormatCommentStyle.Legacy None + + let description = + match tryGetXmlDocMember t.XmlDoc with + | TryGetXmlDocMemberResult.Some xmlDoc -> xmlDoc.FormatComment(FormatCommentStyle.Legacy) + | TryGetXmlDocMemberResult.None -> "" + | TryGetXmlDocMemberResult.Error -> ERROR_WHILE_PARSING_DOC_COMMENT + Some(signature, description) | ToolTipElement.CompositionError(error) -> Some("", error) | _ -> Some("", "No signature data")) -let formatTipEnhanced - (ToolTipText tips) - (signature: string) - (footer: string) - (typeDoc: string option) - (formatCommentStyle: FormatCommentStyle) - : (string * string * string) list list = - tips - |> List.choose (function - | ToolTipElement.Group items -> - Some( - items - |> List.map (fun i -> - let comment = - if i.TypeMapping.IsEmpty then - buildFormatComment i.XmlDoc formatCommentStyle typeDoc - else - buildFormatComment i.XmlDoc formatCommentStyle typeDoc - + nl - + nl - + "**Generic Parameters**" - + nl - + nl - + formatGenericParameters i.TypeMapping - - (signature, comment, footer)) - ) - | ToolTipElement.CompositionError(error) -> Some [ ("", error, "") ] - | _ -> None) - -let formatDocumentation - (ToolTipText tips) - ((signature, (constructors, fields, functions, interfaces, attrs, ts)): - string * (string[] * string[] * string[] * string[] * string[] * string[])) - (footer: string) - (cn: string) - = +let prepareSignature (signatureText: string) = + signatureText.Split Environment.NewLine + // Remove empty lines + |> Array.filter (not << String.IsNullOrWhiteSpace) + |> String.concat nl + +let prepareFooterLines (footerText: string) = + footerText.Split Environment.NewLine + // Remove empty lines + |> Array.filter (not << String.IsNullOrWhiteSpace) + // Mark each line as an individual string in italics + |> Array.map (fun n -> "*" + n + "*") + + +let private tryComputeTooltipInfo (ToolTipText tips) (formatCommentStyle: FormatCommentStyle) = + + // Note: In the previous code, we were returning a `(string * string * string) list list` + // but always discarding the tooltip later if the list had more than one element + // and only using the first element of the inner list. + // More over, I don't know in which case we can have several elements in the + // `(ToolTipText tips)` parameter. + // So I can't test why we have list of list stuff, but like I said, we were + // discarding the tooltip if it had more than one element. + // + // The new code should do the same thing, as before but instead of + // computing the rendered tooltip, and discarding some of them afterwards, + // we are discarding the things we don't want earlier and only compute the + // tooltip we want to display if we have the right data. + tips - |> List.choose (function - | ToolTipElement.Group items -> - Some( - items - |> List.map (fun i -> - let comment = - if i.TypeMapping.IsEmpty then - buildFormatComment i.XmlDoc FormatCommentStyle.Documentation None + // Render the first valid tooltip and return it + |> List.tryPick (function + | ToolTipElement.Group(head :: _) -> + let docComment, hasTruncatedExamples = + match tryGetXmlDocMember head.XmlDoc with + | TryGetXmlDocMemberResult.Some xmlDoc -> + // Format the doc comment + let docCommentText = xmlDoc.FormatComment formatCommentStyle + + // Handle generic parameters section + let genericParametersText = + // If there are no generic parameters, don't display the section + if head.TypeMapping.IsEmpty then + "" + // If there are generic parameters, display the section else - buildFormatComment i.XmlDoc FormatCommentStyle.Documentation None - + nl + nl + nl + "**Generic Parameters**" + nl + nl - + formatGenericParameters i.TypeMapping - - (signature, constructors, fields, functions, interfaces, attrs, ts, comment, footer, cn)) - ) - | ToolTipElement.CompositionError(error) -> Some [ ("", [||], [||], [||], [||], [||], [||], error, "", "") ] - | _ -> None) - -let formatDocumentationFromXmlSig - (xmlSig: string) - (assembly: string) - ((signature, (constructors, fields, functions, interfaces, attrs, ts)): - string * (string[] * string[] * string[] * string[] * string[] * string[])) - (footer: string) - (cn: string) - = + + formatGenericParameters head.TypeMapping + + // Concatenate the doc comment and the generic parameters section + let consolidatedDocCommentText = docCommentText + nl + nl + genericParametersText + + consolidatedDocCommentText, xmlDoc.HasTruncatedExamples + + | TryGetXmlDocMemberResult.None -> "", false + | TryGetXmlDocMemberResult.Error -> ERROR_WHILE_PARSING_DOC_COMMENT, false + + {| DocComment = docComment + HasTruncatedExamples = hasTruncatedExamples |} + |> Ok + |> Some + + | ToolTipElement.CompositionError error -> error |> Error |> Some + + | ToolTipElement.Group [] + | ToolTipElement.None -> None) + +/// +/// Try format the given tooltip with the requested style. +/// +/// Tooltip documentation to render in the middle +/// Style of tooltip +/// +/// - TipFormatterResult.Success {| DocComment; HasTruncatedExamples |} if the doc comment has been formatted +/// +/// Where DocComment is the format tooltip and HasTruncatedExamples is true if examples have been truncated +/// +/// - TipFormatterResult.None if the doc comment has not been found +/// - TipFormatterResult.Error string if an error occurred while parsing the doc comment +/// +let tryFormatTipEnhanced toolTipText (formatCommentStyle: FormatCommentStyle) = + + match tryComputeTooltipInfo toolTipText formatCommentStyle with + | Some(Ok tooltipResult) -> TipFormatterResult.Success tooltipResult + + | Some(Error error) -> TipFormatterResult.Error error + + | None -> TipFormatterResult.None + +/// +/// Generate the 'Show documentation' link for the tooltip. +/// +/// The link is rendered differently depending on if examples +/// have been truncated or not. +/// +/// true if the examples have been truncated +/// XmlDocSignature in the format of T:System.String.concat +/// Assembly name, example FSharp.Core +/// Returns a string which represent the show documentation link +let renderShowDocumentationLink (hasTruncatedExamples: bool) (xmlDocSig: string) (assemblyName: string) = + + // TODO: Refactor this code, to avoid duplicate with DocumentationFormatter.fs + let content = + Uri.EscapeDataString(sprintf """[{ "XmlDocSig": "%s", "AssemblyName": "%s" }]""" xmlDocSig assemblyName) + + let text = + if hasTruncatedExamples then + "Open the documentation to see the truncated examples" + else + "Open the documentation" + + $"%s{text}" + +/// +/// Try format the given tooltip as documentation. +/// +/// Tooltip to format +/// +/// - TipFormatterResult.Success string if the doc comment has been formatted +/// - TipFormatterResult.None if the doc comment has not been found +/// - TipFormatterResult.Error string if an error occurred while parsing the doc comment +/// +let tryFormatDocumentationFromTooltip toolTipText = + + match tryComputeTooltipInfo toolTipText FormatCommentStyle.Documentation with + | Some(Ok tooltipResult) -> TipFormatterResult.Success tooltipResult.DocComment + + | Some(Error error) -> TipFormatterResult.Error error + + | None -> TipFormatterResult.None + +/// +/// Try format the doc comment based on the XmlSignature and the assembly name. +/// +/// +/// XmlSignature used to identify the doc comment to format +/// +/// Example: T:System.String.concat +/// +/// +/// Assembly name used to identify the doc comment to format +/// +/// Example: FSharp.Core +/// +/// +/// - TipFormatterResult.Success string if the doc comment has been formatted +/// - TipFormatterResult.None if the doc comment has not been found +/// - TipFormatterResult.Error string if an error occurred while parsing the doc comment +/// +let tryFormatDocumentationFromXmlSig (xmlSig: string) (assembly: string) = let xmlDoc = FSharpXmlDoc.FromXmlFile(assembly, xmlSig) - let comment = buildFormatComment xmlDoc FormatCommentStyle.Documentation None - [ [ (signature, constructors, fields, functions, interfaces, attrs, ts, comment, footer, cn) ] ] + + match tryGetXmlDocMember xmlDoc with + | TryGetXmlDocMemberResult.Some xmlDoc -> + let formattedComment = xmlDoc.FormatComment(FormatCommentStyle.Documentation) + + TipFormatterResult.Success formattedComment + + | TryGetXmlDocMemberResult.None -> TipFormatterResult.None + | TryGetXmlDocMemberResult.Error -> TipFormatterResult.Error ERROR_WHILE_PARSING_DOC_COMMENT + +let formatDocumentationFromXmlDoc xmlDoc = + match tryGetXmlDocMember xmlDoc with + | TryGetXmlDocMemberResult.Some xmlDoc -> + let formattedComment = xmlDoc.FormatComment(FormatCommentStyle.Documentation) + + TipFormatterResult.Success formattedComment + + | TryGetXmlDocMemberResult.None -> TipFormatterResult.None + | TryGetXmlDocMemberResult.Error -> TipFormatterResult.Error ERROR_WHILE_PARSING_DOC_COMMENT let extractSignature (ToolTipText tips) = let getSignature (t: TaggedText[]) = diff --git a/src/FsAutoComplete/CommandResponse.fs b/src/FsAutoComplete/CommandResponse.fs index aa2c2dc73..55d3b1404 100644 --- a/src/FsAutoComplete/CommandResponse.fs +++ b/src/FsAutoComplete/CommandResponse.fs @@ -8,6 +8,7 @@ open FSharp.UMX open FSharp.Compiler.EditorServices open FSharp.Compiler.Diagnostics open FSharp.Compiler.Xml +open FSharp.Compiler.Symbols module internal CompletionUtils = let getIcon (glyph: FSharpGlyph) = @@ -170,15 +171,27 @@ module CommandResponse = type DocumentationDescription = { XmlKey: string - Constructors: string list - Fields: string list - Functions: string list - Interfaces: string list - Attributes: string list - Types: string list + Constructors: string array + Fields: string array + Functions: string array + Interfaces: string array + Attributes: string array + DeclaredTypes: string array Signature: string Comment: string - Footer: string } + FooterLines: string array } + + static member CreateFromError(error: string) = + { XmlKey = "" + Constructors = [||] + Fields = [||] + Functions = [||] + Interfaces = [||] + Attributes = [||] + DeclaredTypes = [||] + FooterLines = [||] + Signature = "" + Comment = error } type CompilerLocationResponse = { Fsc: string option @@ -544,67 +557,101 @@ module CommandResponse = serialize { Kind = "declarations"; Data = decls' } - let formattedDocumentation (serialize: Serializer) (tip, xmlSig, signature, footer, cn) = - let data = - match tip, xmlSig with + let formattedDocumentation + (serialize: Serializer) + (param: + {| Tip: ToolTipText option + XmlSig: (string * string) option + Signature: (string * DocumentationFormatter.EntityInfo) + Footer: string + XmlKey: string |}) + = + + let (signature, entity) = param.Signature + + let createDocumentationDescription formattedDocComment = + { XmlKey = param.XmlKey + Constructors = entity.Constructors + Fields = entity.Fields + Functions = entity.Functions + Interfaces = entity.Interfaces + Attributes = entity.Attributes + DeclaredTypes = entity.DeclaredTypes + FooterLines = TipFormatter.prepareFooterLines param.Footer + Signature = TipFormatter.prepareSignature signature + Comment = formattedDocComment } + + let data: DocumentationDescription = + match param.Tip, param.XmlSig with | Some tip, _ -> - TipFormatter.formatDocumentation tip signature footer cn - |> List.map ( - List.map (fun (n, cns, fds, funcs, intf, attrs, ts, m, f, cn) -> - { XmlKey = cn - Constructors = cns |> Seq.toList - Fields = fds |> Seq.toList - Functions = funcs |> Seq.toList - Interfaces = intf |> Seq.toList - Attributes = attrs |> Seq.toList - Types = ts |> Seq.toList - Footer = f - Signature = n - Comment = m }) - ) + match TipFormatter.tryFormatDocumentationFromTooltip tip with + | TipFormatter.TipFormatterResult.Success formattedDocComment -> + createDocumentationDescription formattedDocComment + + // The old behaviour never took into consideration a possible `None` + // It was using direct access into an array which would have thrown + // I decided to just return an empty string for now + | TipFormatter.TipFormatterResult.None -> createDocumentationDescription "" + + | TipFormatter.TipFormatterResult.Error error -> DocumentationDescription.CreateFromError error + | _, Some(xml, assembly) -> - TipFormatter.formatDocumentationFromXmlSig xml assembly signature footer cn - |> List.map ( - List.map (fun (n, cns, fds, funcs, intf, attrs, ts, m, f, cn) -> - { XmlKey = cn - Constructors = cns |> Seq.toList - Fields = fds |> Seq.toList - Functions = funcs |> Seq.toList - Interfaces = intf |> Seq.toList - Attributes = attrs |> Seq.toList - Types = ts |> Seq.toList - Footer = f - Signature = n - Comment = m }) - ) + match TipFormatter.tryFormatDocumentationFromXmlSig xml assembly with + | TipFormatter.TipFormatterResult.Success formattedDocComment -> + createDocumentationDescription formattedDocComment + + // The old behaviour never took into consideration a possible `None` + // It was using direct access into an array which would have thrown + // I decided to just return an empty string for now + | TipFormatter.TipFormatterResult.None -> createDocumentationDescription "" + + | TipFormatter.TipFormatterResult.Error error -> DocumentationDescription.CreateFromError error + | _ -> failwith "Shouldn't happen" serialize { Kind = "formattedDocumentation" Data = data } - let formattedDocumentationForSymbol (serialize: Serializer) xml assembly (xmldoc: string[]) (signature, footer, cn) = - let data = - TipFormatter.formatDocumentationFromXmlSig xml assembly signature footer cn - |> List.map ( - List.map (fun (n, cns, fds, funcs, intf, attrs, ts, m, f, cn) -> - let m = - if String.IsNullOrWhiteSpace m then - xmldoc |> String.concat Environment.NewLine - else - m - - { XmlKey = cn - Constructors = cns |> Seq.toList - Fields = fds |> Seq.toList - Functions = funcs |> Seq.toList - Interfaces = intf |> Seq.toList - Attributes = attrs |> Seq.toList - Types = ts |> Seq.toList - Footer = f - Signature = n - Comment = m }) - ) + let formattedDocumentationForSymbol + (serialize: Serializer) + (param: + {| Xml: string + Assembly: string + XmlDoc: FSharpXmlDoc + Signature: (string * DocumentationFormatter.EntityInfo) + Footer: string + XmlKey: string |}) + = + let (signature, entity) = param.Signature + + let createDocumentationDescription formattedDocComment = + { XmlKey = param.XmlKey + Constructors = entity.Constructors + Fields = entity.Fields + Functions = entity.Functions + Interfaces = entity.Interfaces + Attributes = entity.Attributes + DeclaredTypes = entity.DeclaredTypes + FooterLines = TipFormatter.prepareFooterLines param.Footer + Signature = TipFormatter.prepareSignature signature + Comment = formattedDocComment } + + let data: DocumentationDescription = + match TipFormatter.tryFormatDocumentationFromXmlSig param.Xml param.Assembly with + | TipFormatter.TipFormatterResult.Success formattedDocComment -> + createDocumentationDescription formattedDocComment + + | TipFormatter.TipFormatterResult.None -> + match TipFormatter.formatDocumentationFromXmlDoc param.XmlDoc with + | TipFormatter.TipFormatterResult.Success formattedDocComment -> + createDocumentationDescription formattedDocComment + + | TipFormatter.TipFormatterResult.None -> createDocumentationDescription "" + + | TipFormatter.TipFormatterResult.Error error -> DocumentationDescription.CreateFromError error + + | TipFormatter.TipFormatterResult.Error error -> DocumentationDescription.CreateFromError error serialize { Kind = "formattedDocumentation" diff --git a/src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fs b/src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fs index 2f5a6bcd4..2e179c99b 100644 --- a/src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fs +++ b/src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fs @@ -2514,10 +2514,10 @@ type AdaptiveFSharpLspServer(workspaceLoader: IWorkspaceLoader, lspClient: FShar and! tyRes = forceGetTypeCheckResultsStale filePath |> Result.ofStringErr match tyRes.TryGetToolTipEnhanced pos lineStr with - | Ok(Some(tip, signature, footer, typeDoc) as x) -> + | Ok(Some tooltipResult) -> logger.info ( Log.setMessage "TryGetToolTipEnhanced : {parms}" - >> Log.addContextDestructured "parms" x + >> Log.addContextDestructured "parms" tooltipResult ) let formatCommentStyle = @@ -2530,41 +2530,51 @@ type AdaptiveFSharpLspServer(workspaceLoader: IWorkspaceLoader, lspClient: FShar else TipFormatter.FormatCommentStyle.Legacy - match TipFormatter.formatTipEnhanced tip signature footer typeDoc formatCommentStyle with - | (sigCommentFooter :: _) :: _ -> - let signature, comment, footer = sigCommentFooter + match TipFormatter.tryFormatTipEnhanced tooltipResult.ToolTipText formatCommentStyle with + | TipFormatter.TipFormatterResult.Success tooltipInfo -> + + // Display the signature as a code block + let signature = + tooltipResult.Signature + |> TipFormatter.prepareSignature + |> (fun content -> MarkedString.WithLanguage { Language = "fsharp"; Value = content }) + + // Display each footer line as a separate line + let footerLines = + tooltipResult.Footer + |> TipFormatter.prepareFooterLines + |> Array.map MarkedString.String + + let contents = + [| signature + MarkedString.String tooltipInfo.DocComment + match tooltipResult.SymbolInfo with + | TryGetToolTipEnhancedResult.Keyword _ -> () + | TryGetToolTipEnhancedResult.Symbol symbolInfo -> + TipFormatter.renderShowDocumentationLink + tooltipInfo.HasTruncatedExamples + symbolInfo.XmlDocSig + symbolInfo.Assembly + |> MarkedString.String + yield! footerLines |] - let markStr lang (value: string) = - MarkedString.WithLanguage { Language = lang; Value = value } - - let fsharpBlock (lines: string[]) = - lines |> String.concat Environment.NewLine |> markStr "fsharp" - - let sigContent = - let lines = - signature.Split Environment.NewLine - |> Array.filter (not << String.IsNullOrWhiteSpace) - - match lines |> Array.splitAt (lines.Length - 1) with - | (h, [| StartsWith "Full name:" fullName |]) -> - [| yield fsharpBlock h; yield MarkedString.String("*" + fullName + "*") |] - | _ -> [| fsharpBlock lines |] - - - let commentContent = comment |> MarkedString.String + let response = + { Contents = MarkedStrings contents + Range = None } - let footerContent = - footer.Split Environment.NewLine - |> Array.filter (not << String.IsNullOrWhiteSpace) - |> Array.map (fun n -> MarkedString.String("*" + n + "*")) + return (Some response) + | TipFormatter.TipFormatterResult.Error error -> + let contents = [| MarkedString.String ""; MarkedString.String error |] let response = - { Contents = MarkedStrings [| yield! sigContent; yield commentContent; yield! footerContent |] + { Contents = MarkedStrings contents Range = None } return (Some response) - | _ -> return None + + | TipFormatter.TipFormatterResult.None -> return None + | Ok(None) -> return! LspResult.internalError $"No TryGetToolTipEnhanced results for {filePath}" @@ -4269,8 +4279,17 @@ type AdaptiveFSharpLspServer(workspaceLoader: IWorkspaceLoader, lspClient: FShar lastFSharpDocumentationTypeCheck <- Some tyRes match! Commands.FormattedDocumentation tyRes pos lineStr |> Result.ofCoreResponse with - | Some t -> - return Some { Content = CommandResponse.formattedDocumentation FsAutoComplete.JsonSerializer.writeJson t } + | Some(tip, xml, signature, footer, xmlKey) -> + return + Some + { Content = + CommandResponse.formattedDocumentation + JsonSerializer.writeJson + {| Tip = tip + XmlSig = xml + Signature = signature + Footer = footer + XmlKey = xmlKey |} } | None -> return None with e -> trace.SetStatusErrorSafe(e.Message).RecordExceptions(e) |> ignore @@ -4305,22 +4324,18 @@ type AdaptiveFSharpLspServer(workspaceLoader: IWorkspaceLoader, lspClient: FShar |> Result.ofCoreResponse with | None -> return None - | Some(xml, assembly, doc, signature, footer, cn) -> - - let xmldoc = - match doc with - | FSharpXmlDoc.None -> [||] - | FSharpXmlDoc.FromXmlFile _ -> [||] - | FSharpXmlDoc.FromXmlText d -> d.GetElaboratedXmlLines() + | Some(xml, assembly, xmlDoc, signature, footer, xmlKey) -> return { Content = CommandResponse.formattedDocumentationForSymbol - FsAutoComplete.JsonSerializer.writeJson - xml - assembly - xmldoc - (signature, footer, cn) } + JsonSerializer.writeJson + {| Xml = xml + Assembly = assembly + XmlDoc = xmlDoc + Signature = signature + Footer = footer + XmlKey = xmlKey |} } |> Some with e -> trace.SetStatusErrorSafe(e.Message).RecordExceptions(e) |> ignore diff --git a/src/FsAutoComplete/LspServers/FsAutoComplete.Lsp.fs b/src/FsAutoComplete/LspServers/FsAutoComplete.Lsp.fs index cda183565..68106917d 100644 --- a/src/FsAutoComplete/LspServers/FsAutoComplete.Lsp.fs +++ b/src/FsAutoComplete/LspServers/FsAutoComplete.Lsp.fs @@ -1563,7 +1563,7 @@ type FSharpLspServer(state: State, lspClient: FSharpLspClient) = | CoreResponse.InfoRes msg -> async.Return(success None) | CoreResponse.ErrorRes msg -> LspResult.internalError msg |> async.Return | CoreResponse.Res None -> async.Return(success None) - | CoreResponse.Res(Some(tip, signature, footer, typeDoc)) -> + | CoreResponse.Res(Some tooltipResult) -> let formatCommentStyle = if config.TooltipMode = "full" then TipFormatter.FormatCommentStyle.FullEnhanced @@ -1572,41 +1572,50 @@ type FSharpLspServer(state: State, lspClient: FSharpLspClient) = else TipFormatter.FormatCommentStyle.Legacy - match TipFormatter.formatTipEnhanced tip signature footer typeDoc formatCommentStyle with - | (sigCommentFooter :: _) :: _ -> - let signature, comment, footer = sigCommentFooter + match TipFormatter.tryFormatTipEnhanced tooltipResult.ToolTipText formatCommentStyle with + | TipFormatter.TipFormatterResult.Success tooltipInfo -> + + // Display the signature as a code block + let signature = + tooltipResult.Signature + |> TipFormatter.prepareSignature + |> (fun content -> MarkedString.WithLanguage { Language = "fsharp"; Value = content }) + + // Display each footer line as a separate line + let footerLines = + tooltipResult.Footer + |> TipFormatter.prepareFooterLines + |> Array.map MarkedString.String + + let contents = + [| signature + MarkedString.String tooltipInfo.DocComment + match tooltipResult.SymbolInfo with + | TryGetToolTipEnhancedResult.Keyword _ -> () + | TryGetToolTipEnhancedResult.Symbol symbolInfo -> + TipFormatter.renderShowDocumentationLink + tooltipInfo.HasTruncatedExamples + symbolInfo.XmlDocSig + symbolInfo.Assembly + |> MarkedString.String + yield! footerLines |] - let markStr lang (value: string) = - MarkedString.WithLanguage { Language = lang; Value = value } - - let fsharpBlock (lines: string[]) = - lines |> String.concat Environment.NewLine |> markStr "fsharp" - - let sigContent = - let lines = - signature.Split Environment.NewLine - |> Array.filter (not << String.IsNullOrWhiteSpace) - - match lines |> Array.splitAt (lines.Length - 1) with - | (h, [| StartsWith "Full name:" fullName |]) -> - [| yield fsharpBlock h; yield MarkedString.String("*" + fullName + "*") |] - | _ -> [| fsharpBlock lines |] - - - let commentContent = comment |> MarkedString.String + let response = + { Contents = MarkedStrings contents + Range = None } - let footerContent = - footer.Split Environment.NewLine - |> Array.filter (not << String.IsNullOrWhiteSpace) - |> Array.map (fun n -> MarkedString.String("*" + n + "*")) + async.Return(success (Some response)) + | TipFormatter.TipFormatterResult.Error error -> + let contents = [| MarkedString.String ""; MarkedString.String error |] let response = - { Contents = MarkedStrings [| yield! sigContent; yield commentContent; yield! footerContent |] + { Contents = MarkedStrings contents Range = None } async.Return(success (Some response)) - | _ -> async.Return(success None)) + + | TipFormatter.TipFormatterResult.None -> async.Return(success None)) override x.TextDocumentPrepareRename p = logger.info ( @@ -2662,12 +2671,16 @@ type FSharpLspServer(state: State, lspClient: FSharpLspClient) = (match Commands.FormattedDocumentation tyRes pos lineStr with | CoreResponse.InfoRes msg -> success None | CoreResponse.ErrorRes msg -> LspResult.internalError msg - | CoreResponse.Res(tip, xml, signature, footer, cm) -> + | CoreResponse.Res(tip, xml, signature, footer, xmlKey) -> let notification: PlainNotification = { Content = CommandResponse.formattedDocumentation - FsAutoComplete.JsonSerializer.writeJson - (tip, xml, signature, footer, cm) } + JsonSerializer.writeJson + {| Tip = tip + XmlSig = xml + Signature = signature + Footer = footer + XmlKey = xmlKey |} } success (Some notification)) |> async.Return) @@ -2685,21 +2698,18 @@ type FSharpLspServer(state: State, lspClient: FSharpLspClient) = match Commands.FormattedDocumentationForSymbol tyRes p.XmlSig p.Assembly with | (CoreResponse.InfoRes msg) -> return success None | (CoreResponse.ErrorRes msg) -> return! AsyncLspResult.internalError msg - | (CoreResponse.Res(xml, assembly, doc, signature, footer, cn)) -> - let xmldoc = - match doc with - | FSharpXmlDoc.None -> [||] - | FSharpXmlDoc.FromXmlFile _ -> [||] - | FSharpXmlDoc.FromXmlText d -> d.GetElaboratedXmlLines() + | (CoreResponse.Res(xml, assembly, xmlDoc, signature, footer, xmlKey)) -> return { Content = CommandResponse.formattedDocumentationForSymbol - FsAutoComplete.JsonSerializer.writeJson - xml - assembly - xmldoc - (signature, footer, cn) } + JsonSerializer.writeJson + {| Xml = xml + Assembly = assembly + XmlDoc = xmlDoc + Signature = signature + Footer = footer + XmlKey = xmlKey |} } |> Some |> success } From 91a45ba20df4417e800f2e2dca3aedc602a84eed Mon Sep 17 00:00:00 2001 From: Maxime Mangel Date: Fri, 7 Apr 2023 16:30:02 +0200 Subject: [PATCH 04/10] Generate "Generic Parameters" section even if doc comment was not found --- src/FsAutoComplete.Core/TipFormatter.fs | 38 +++++++++++++------------ 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/src/FsAutoComplete.Core/TipFormatter.fs b/src/FsAutoComplete.Core/TipFormatter.fs index 4332084c7..16a7c3774 100644 --- a/src/FsAutoComplete.Core/TipFormatter.fs +++ b/src/FsAutoComplete.Core/TipFormatter.fs @@ -1131,36 +1131,38 @@ let private tryComputeTooltipInfo (ToolTipText tips) (formatCommentStyle: Format // we are discarding the things we don't want earlier and only compute the // tooltip we want to display if we have the right data. + let computeGenericParametersText (tooltipData: ToolTipElementData) = + // If there are no generic parameters, don't display the section + if tooltipData.TypeMapping.IsEmpty then + "" + // If there are generic parameters, display the section + else + nl + + nl + + "**Generic Parameters**" + + nl + + nl + + formatGenericParameters tooltipData.TypeMapping + tips // Render the first valid tooltip and return it |> List.tryPick (function - | ToolTipElement.Group(head :: _) -> + | ToolTipElement.Group(tooltipData :: _) -> let docComment, hasTruncatedExamples = - match tryGetXmlDocMember head.XmlDoc with + match tryGetXmlDocMember tooltipData.XmlDoc with | TryGetXmlDocMemberResult.Some xmlDoc -> // Format the doc comment let docCommentText = xmlDoc.FormatComment formatCommentStyle - // Handle generic parameters section - let genericParametersText = - // If there are no generic parameters, don't display the section - if head.TypeMapping.IsEmpty then - "" - // If there are generic parameters, display the section - else - nl - + nl - + "**Generic Parameters**" - + nl - + nl - + formatGenericParameters head.TypeMapping - // Concatenate the doc comment and the generic parameters section - let consolidatedDocCommentText = docCommentText + nl + nl + genericParametersText + let consolidatedDocCommentText = + docCommentText + nl + nl + computeGenericParametersText tooltipData consolidatedDocCommentText, xmlDoc.HasTruncatedExamples - | TryGetXmlDocMemberResult.None -> "", false + | TryGetXmlDocMemberResult.None -> + // Even if a symbol doesn't have a doc comment, it can still have generic parameters + computeGenericParametersText tooltipData, false | TryGetXmlDocMemberResult.Error -> ERROR_WHILE_PARSING_DOC_COMMENT, false {| DocComment = docComment From 0124000d93c110917abda8f71ea5e822acb3f0b7 Mon Sep 17 00:00:00 2001 From: Maxime Mangel Date: Fri, 7 Apr 2023 16:30:13 +0200 Subject: [PATCH 05/10] Fixes some tests --- test/FsAutoComplete.Tests.Lsp/CoreTests.fs | 12 +++++++++++- test/FsAutoComplete.Tests.Lsp/InfoPanelTests.fs | 4 ++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/test/FsAutoComplete.Tests.Lsp/CoreTests.fs b/test/FsAutoComplete.Tests.Lsp/CoreTests.fs index 7d80f8aec..3d4d97783 100644 --- a/test/FsAutoComplete.Tests.Lsp/CoreTests.fs +++ b/test/FsAutoComplete.Tests.Lsp/CoreTests.fs @@ -176,7 +176,12 @@ let tooltipTests state = let (|Signature|_|) (hover: Hover) = match hover with | { Contents = MarkedStrings [| MarkedString.WithLanguage { Language = "fsharp"; Value = tooltip } - MarkedString.String newline + MarkedString.String docComment + MarkedString.String fullname + MarkedString.String assembly |] } -> Some tooltip + | { Contents = MarkedStrings [| MarkedString.WithLanguage { Language = "fsharp"; Value = tooltip } + MarkedString.String docComment + MarkedString.String showDocumentationLink MarkedString.String fullname MarkedString.String assembly |] } -> Some tooltip | _ -> None @@ -189,6 +194,11 @@ let tooltipTests state = MarkedString.String description MarkedString.String fullname MarkedString.String assembly |] } -> Some description + | { Contents = MarkedStrings [| MarkedString.WithLanguage { Language = "fsharp"; Value = tooltip } + MarkedString.String description + MarkedString.String showDocumentationLink + MarkedString.String fullname + MarkedString.String assembly |] } -> Some description | _ -> None let server = diff --git a/test/FsAutoComplete.Tests.Lsp/InfoPanelTests.fs b/test/FsAutoComplete.Tests.Lsp/InfoPanelTests.fs index c7dd38c16..ffb21047b 100644 --- a/test/FsAutoComplete.Tests.Lsp/InfoPanelTests.fs +++ b/test/FsAutoComplete.Tests.Lsp/InfoPanelTests.fs @@ -40,7 +40,7 @@ let docFormattingTest state = match doc with | Result.Error err -> failtest $"Doc error: {err.Message}" - | Result.Ok (Some(As ([ [ model: FsAutoComplete.CommandResponse.DocumentationDescription ] ]))) -> + | Result.Ok (Some(As (model: FsAutoComplete.CommandResponse.DocumentationDescription ))) -> Expect.stringContains model.Signature "'Key, 'U" "Formatted doc contains both params separated by (, )" | Result.Ok _ -> failtest "couldn't parse doc as the json type we expected" }) @@ -57,7 +57,7 @@ let docFormattingTest state = match doc with | Result.Error err -> failtest $"Doc error: {err.Message}" - | Result.Ok (Some (As ([ [ model: FsAutoComplete.CommandResponse.DocumentationDescription ] ]))) -> + | Result.Ok (Some (As (model: FsAutoComplete.CommandResponse.DocumentationDescription))) -> Expect.stringContains model.Signature "'T1 * 'T2 * 'T3" "Formatted doc contains 3 params separated by ( * )" | Result.Ok _ -> failtest "couldn't parse doc as the json type we expected" }) ] From f9cce3344429a3f2a97eeade241fb3799b2b199a Mon Sep 17 00:00:00 2001 From: Maxime Mangel Date: Fri, 7 Apr 2023 17:04:00 +0200 Subject: [PATCH 06/10] Don't add empty line if there are no generic parameters. This mimic the old behaviours and should fix the remaining tests --- src/FsAutoComplete.Core/TipFormatter.fs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/FsAutoComplete.Core/TipFormatter.fs b/src/FsAutoComplete.Core/TipFormatter.fs index 16a7c3774..296a0f48a 100644 --- a/src/FsAutoComplete.Core/TipFormatter.fs +++ b/src/FsAutoComplete.Core/TipFormatter.fs @@ -1134,7 +1134,7 @@ let private tryComputeTooltipInfo (ToolTipText tips) (formatCommentStyle: Format let computeGenericParametersText (tooltipData: ToolTipElementData) = // If there are no generic parameters, don't display the section if tooltipData.TypeMapping.IsEmpty then - "" + None // If there are generic parameters, display the section else nl @@ -1143,6 +1143,7 @@ let private tryComputeTooltipInfo (ToolTipText tips) (formatCommentStyle: Format + nl + nl + formatGenericParameters tooltipData.TypeMapping + |> Some tips // Render the first valid tooltip and return it @@ -1156,13 +1157,20 @@ let private tryComputeTooltipInfo (ToolTipText tips) (formatCommentStyle: Format // Concatenate the doc comment and the generic parameters section let consolidatedDocCommentText = - docCommentText + nl + nl + computeGenericParametersText tooltipData + match computeGenericParametersText tooltipData with + | Some genericParametersText -> docCommentText + nl + nl + genericParametersText + | None -> docCommentText consolidatedDocCommentText, xmlDoc.HasTruncatedExamples | TryGetXmlDocMemberResult.None -> // Even if a symbol doesn't have a doc comment, it can still have generic parameters - computeGenericParametersText tooltipData, false + let docComment = + match computeGenericParametersText tooltipData with + | Some genericParametersText -> genericParametersText + | None -> "" + + docComment, false | TryGetXmlDocMemberResult.Error -> ERROR_WHILE_PARSING_DOC_COMMENT, false {| DocComment = docComment From e613e7ac0c9a813e73fb69b31d1b18c278ec7596 Mon Sep 17 00:00:00 2001 From: Maxime Mangel Date: Fri, 7 Apr 2023 17:28:05 +0200 Subject: [PATCH 07/10] Try fix last failing test --- test/FsAutoComplete.Tests.Lsp/CoreTests.fs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/FsAutoComplete.Tests.Lsp/CoreTests.fs b/test/FsAutoComplete.Tests.Lsp/CoreTests.fs index 3d4d97783..2260e8ffe 100644 --- a/test/FsAutoComplete.Tests.Lsp/CoreTests.fs +++ b/test/FsAutoComplete.Tests.Lsp/CoreTests.fs @@ -303,7 +303,9 @@ let tooltipTests state = "" "**Generic Parameters**" "" - "* `'T` is `string`" ] // verify fancy descriptions for external library functions + "* `'T` is `string`" + "" + "" ] // verify fancy descriptions for external library functions verifyDescription 13 11 From a9384f9c209570c6c9dcb163d3c05e639751dd50 Mon Sep 17 00:00:00 2001 From: Maxime Mangel Date: Fri, 7 Apr 2023 18:01:52 +0200 Subject: [PATCH 08/10] Remove non useful new line when generating the "Generic parameters text" --- src/FsAutoComplete.Core/TipFormatter.fs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/FsAutoComplete.Core/TipFormatter.fs b/src/FsAutoComplete.Core/TipFormatter.fs index 296a0f48a..539e80f43 100644 --- a/src/FsAutoComplete.Core/TipFormatter.fs +++ b/src/FsAutoComplete.Core/TipFormatter.fs @@ -1137,9 +1137,7 @@ let private tryComputeTooltipInfo (ToolTipText tips) (formatCommentStyle: Format None // If there are generic parameters, display the section else - nl - + nl - + "**Generic Parameters**" + "**Generic Parameters**" + nl + nl + formatGenericParameters tooltipData.TypeMapping From bb4800facc11d49e10c49250f765b4e02926acda Mon Sep 17 00:00:00 2001 From: Maxime Mangel Date: Fri, 7 Apr 2023 20:43:20 +0200 Subject: [PATCH 09/10] Update tests --- test/FsAutoComplete.Tests.Lsp/CoreTests.fs | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/test/FsAutoComplete.Tests.Lsp/CoreTests.fs b/test/FsAutoComplete.Tests.Lsp/CoreTests.fs index 2260e8ffe..5a84ad03e 100644 --- a/test/FsAutoComplete.Tests.Lsp/CoreTests.fs +++ b/test/FsAutoComplete.Tests.Lsp/CoreTests.fs @@ -297,15 +297,9 @@ let tooltipTests state = "" "The formatted result." "" - "**Examples**" - "" - "See `Printf.sprintf` (link: ``Microsoft.FSharp.Core.PrintfModule.PrintFormatToStringThen``1``) for examples." - "" "**Generic Parameters**" "" - "* `'T` is `string`" - "" - "" ] // verify fancy descriptions for external library functions + "* `'T` is `string`" ] // verify fancy descriptions for external library functions verifyDescription 13 11 @@ -364,9 +358,7 @@ let tooltipTests state = verifyDescription 45 15 - [ "" - "" - "**Generic Parameters**" + [ "**Generic Parameters**" "" "* `'a` is `int`" "* `'b` is `int`" From d37f93f4ef882bfca9fe030bb952f415557332dc Mon Sep 17 00:00:00 2001 From: Maxime Mangel Date: Tue, 11 Apr 2023 10:14:39 +0200 Subject: [PATCH 10/10] Fix typos and wording Co-authored-by: Chet Husk --- src/FsAutoComplete.Core/TipFormatter.fs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/FsAutoComplete.Core/TipFormatter.fs b/src/FsAutoComplete.Core/TipFormatter.fs index 539e80f43..d0c3ce2bc 100644 --- a/src/FsAutoComplete.Core/TipFormatter.fs +++ b/src/FsAutoComplete.Core/TipFormatter.fs @@ -1011,7 +1011,7 @@ let private tryGetXmlDocMember (xmlDoc: FSharpXmlDoc) = [] let private ERROR_WHILE_PARSING_DOC_COMMENT = - "An error occured when parsing the doc comment, please check that your doc comment is valid.\n\nMore info can be found LSP output" + "An error occurred when parsing the doc comment, please check that your doc comment is valid.\n\nMore info can be found in the LSP output" let private formatTaggedText (t: TaggedText) : string = match t.Tag with