diff --git a/package.json b/package.json index bfa02223..400acc31 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "fable-loader": "1.0.7", "fable-utils": "^1.0.2", "mocha": "*", + "showdown": "^1.9.0", "toml": "*", "vscode": "*", "webpack": "^3.5.5", diff --git a/paket.dependencies b/paket.dependencies index 102a4fbd..71c43281 100644 --- a/paket.dependencies +++ b/paket.dependencies @@ -21,6 +21,7 @@ github ionide/ionide-vscode-helpers src/Fable.Import.VSCode.fs github ionide/ionide-vscode-helpers src/Helpers.fs github ionide/ionide-vscode-helpers src/Fable.Import.Axios.fs github ionide/ionide-vscode-helpers src/Fable.Import.ws.fs +github ionide/ionide-vscode-helpers src/Fable.Import.Showdown.fs group build source https://www.nuget.org/api/v2 diff --git a/paket.lock b/paket.lock index ede4c79a..e734edab 100644 --- a/paket.lock +++ b/paket.lock @@ -39,7 +39,7 @@ NUGET System.Runtime.Loader (>= 4.0) - restriction: && (< net461) (>= netstandard2.0) System.Security.Cryptography.Algorithms (>= 4.3) - restriction: && (< net461) (>= netstandard2.0) FSharp.Core (4.6.2) - restriction: >= netstandard1.6 - Microsoft.NETCore.App (2.2.3) - restriction: >= netcoreapp2.0 + Microsoft.NETCore.App (2.2.4) - restriction: >= netcoreapp2.0 Microsoft.NETCore.Platforms (2.2) - restriction: || (&& (>= net45) (< netstandard1.3) (>= netstandard1.6)) (&& (< net45) (< netstandard1.2) (>= netstandard1.6) (< win8)) (&& (< net45) (< netstandard1.3) (>= netstandard1.6) (< win8) (< wpa81)) (&& (< net45) (< netstandard1.4) (>= netstandard1.6) (< win8) (< wpa81)) (&& (< net45) (< netstandard1.5) (>= netstandard1.6) (< win8) (< wpa81)) (&& (< net45) (>= netstandard1.6) (< netstandard2.0) (< win8) (< wpa81)) (&& (< net45) (>= netstandard2.0)) (&& (>= net46) (< netstandard1.4) (>= netstandard1.6)) (&& (>= net461) (>= netstandard1.6)) (>= netcoreapp2.0) (&& (< netstandard1.0) (>= netstandard1.6) (< portable-net45+win8)) (&& (< netstandard1.0) (>= netstandard1.6) (>= win8)) (&& (< netstandard1.0) (>= netstandard1.6) (< win8)) (&& (< netstandard1.3) (>= netstandard1.6) (< win8) (>= wpa81)) (&& (< netstandard1.5) (>= netstandard1.6) (>= uap10.0)) (&& (>= netstandard1.6) (< portable-net45+win8+wpa81)) (&& (>= netstandard1.6) (>= uap10.1)) (&& (>= netstandard1.6) (>= wp8)) Microsoft.NETCore.Targets (2.1) - restriction: || (&& (< monoandroid) (< monotouch) (< net45) (>= netstandard1.6) (< netstandard2.0) (< win8) (< wpa81) (< xamarinios) (< xamarinmac) (< xamarintvos) (< xamarinwatchos)) (&& (< monoandroid) (< net45) (< netstandard1.2) (>= netstandard1.6) (< win8)) (&& (< monoandroid) (< net45) (< netstandard1.3) (>= netstandard1.6) (< win8) (< wpa81)) (&& (< monoandroid) (< net45) (< netstandard1.4) (>= netstandard1.6) (< win8) (< wpa81)) (&& (< monoandroid) (< net45) (< netstandard1.5) (>= netstandard1.6) (< win8) (< wpa81)) (&& (< netstandard1.2) (>= netstandard1.6) (>= uap10.0) (< win8)) (&& (< netstandard1.3) (>= netstandard1.6) (>= uap10.0) (< win8) (< wpa81)) (&& (< netstandard1.5) (>= netstandard1.6) (>= uap10.0) (< win8) (< wpa81)) (&& (>= netstandard1.6) (< portable-net45+win8+wp8+wpa81)) (&& (>= netstandard1.6) (< portable-net45+win8+wpa81)) Microsoft.Win32.Primitives (4.3) - restriction: || (&& (< net45) (< netstandard1.4) (>= netstandard1.6) (< win8) (< wpa81)) (&& (< net45) (< netstandard1.5) (>= netstandard1.6) (< win8) (< wpa81)) (&& (< net45) (>= netstandard1.6) (< netstandard2.0) (< win8) (< wpa81)) (&& (>= net46) (< netstandard1.4) (>= netstandard1.6)) (>= netcoreapp2.0) (&& (< netstandard1.5) (>= netstandard1.6) (>= uap10.0)) @@ -95,7 +95,7 @@ NUGET System.Threading.Timer (>= 4.3) - restriction: || (&& (< net45) (>= netstandard1.2) (< netstandard1.3) (< win8) (< wpa81)) (&& (< net45) (>= netstandard1.3) (< netstandard1.4) (< win8) (< wpa81)) (&& (< net45) (>= netstandard1.4) (< netstandard1.5) (< win8) (< wpa81)) (&& (< net45) (>= netstandard1.5) (< netstandard1.6) (< win8) (< wpa81)) (&& (< net45) (>= netstandard1.6) (< netstandard2.0) (< win8) (< wpa81)) (&& (< netstandard1.5) (>= uap10.0) (< uap10.1)) System.Xml.ReaderWriter (>= 4.3) - restriction: || (&& (< net45) (>= netstandard1.1) (< netstandard1.2) (< win8)) (&& (< net45) (>= netstandard1.2) (< netstandard1.3) (< win8) (< wpa81)) (&& (< net45) (>= netstandard1.3) (< netstandard1.4) (< win8) (< wpa81)) (&& (< net45) (>= netstandard1.4) (< netstandard1.5) (< win8) (< wpa81)) (&& (< net45) (>= netstandard1.5) (< netstandard1.6) (< win8) (< wpa81)) (&& (< net45) (>= netstandard1.6) (< netstandard2.0) (< win8) (< wpa81)) (&& (>= net46) (< netstandard1.4)) (&& (>= netstandard1.0) (< portable-net45+win8+wpa81) (< wp8)) (&& (< netstandard1.5) (>= uap10.0) (< uap10.1)) System.Xml.XDocument (>= 4.3) - restriction: || (&& (< net45) (>= netstandard1.1) (< netstandard1.2) (< win8)) (&& (< net45) (>= netstandard1.2) (< netstandard1.3) (< win8) (< wpa81)) (&& (< net45) (>= netstandard1.3) (< netstandard1.4) (< win8) (< wpa81)) (&& (< net45) (>= netstandard1.4) (< netstandard1.5) (< win8) (< wpa81)) (&& (< net45) (>= netstandard1.5) (< netstandard1.6) (< win8) (< wpa81)) (&& (< net45) (>= netstandard1.6) (< netstandard2.0) (< win8) (< wpa81)) (&& (>= netstandard1.0) (< portable-net45+win8+wpa81) (< wp8)) (&& (< netstandard1.5) (>= uap10.0) (< uap10.1)) - Newtonsoft.Json (12.0.1) - restriction: >= netcoreapp2.0 + Newtonsoft.Json (12.0.2) - restriction: >= netcoreapp2.0 runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl (4.3.3) - restriction: || (&& (< monoandroid) (< monotouch) (< net45) (>= netstandard1.6) (< netstandard2.0) (< win8) (< wpa81) (< xamarinios) (< xamarinmac) (< xamarintvos) (< xamarinwatchos)) (&& (< monoandroid) (< net45) (< netstandard1.2) (>= netstandard1.6) (< win8)) (&& (< monoandroid) (< net45) (< netstandard1.3) (>= netstandard1.6) (< win8) (< wpa81)) (&& (< monoandroid) (< net45) (< netstandard1.4) (>= netstandard1.6) (< win8) (< wpa81)) (&& (< monoandroid) (< net45) (< netstandard1.5) (>= netstandard1.6) (< win8) (< wpa81)) (&& (< net45) (>= net46) (< netstandard1.4) (>= netstandard1.6)) (&& (< netstandard1.5) (>= netstandard1.6) (>= uap10.0)) runtime.debian.9-x64.runtime.native.System.Security.Cryptography.OpenSsl (4.3.3) - restriction: || (&& (< monoandroid) (< monotouch) (< net45) (>= netstandard1.6) (< netstandard2.0) (< win8) (< wpa81) (< xamarinios) (< xamarinmac) (< xamarintvos) (< xamarinwatchos)) (&& (< monoandroid) (< net45) (< netstandard1.2) (>= netstandard1.6) (< win8)) (&& (< monoandroid) (< net45) (< netstandard1.3) (>= netstandard1.6) (< win8) (< wpa81)) (&& (< monoandroid) (< net45) (< netstandard1.4) (>= netstandard1.6) (< win8) (< wpa81)) (&& (< monoandroid) (< net45) (< netstandard1.5) (>= netstandard1.6) (< win8) (< wpa81)) (&& (< net45) (>= net46) (< netstandard1.4) (>= netstandard1.6)) (&& (< netstandard1.5) (>= netstandard1.6) (>= uap10.0)) runtime.fedora.23-x64.runtime.native.System.Security.Cryptography.OpenSsl (4.3.3) - restriction: || (&& (< monoandroid) (< monotouch) (< net45) (>= netstandard1.6) (< netstandard2.0) (< win8) (< wpa81) (< xamarinios) (< xamarinmac) (< xamarintvos) (< xamarinwatchos)) (&& (< monoandroid) (< net45) (< netstandard1.2) (>= netstandard1.6) (< win8)) (&& (< monoandroid) (< net45) (< netstandard1.3) (>= netstandard1.6) (< win8) (< wpa81)) (&& (< monoandroid) (< net45) (< netstandard1.4) (>= netstandard1.6) (< win8) (< wpa81)) (&& (< monoandroid) (< net45) (< netstandard1.5) (>= netstandard1.6) (< win8) (< wpa81)) (&& (< net45) (>= net46) (< netstandard1.4) (>= netstandard1.6)) (&& (< netstandard1.5) (>= netstandard1.6) (>= uap10.0)) @@ -604,10 +604,10 @@ NUGET FSharp.Core (>= 4.3.4) - restriction: >= netstandard2.0 GIT remote: https://github.com/fsharp/FsAutoComplete.git - (630b6935b7913e7e126b1843235123b62906ca03) + (c9636b287d0c51b49ab4d0528e36f40392ddaf2b) build: build.cmd LocalRelease os: windows - (630b6935b7913e7e126b1843235123b62906ca03) + (c9636b287d0c51b49ab4d0528e36f40392ddaf2b) build: build.sh LocalRelease os: mono remote: https://github.com/fsharp-editing/Forge.git @@ -618,13 +618,14 @@ GIT build: build.sh Build os: mono remote: https://github.com/ionide/ionide-fsgrammar.git - (942f1a311f84a402cb397d7a9f2bc17e630f8834) + (b2100c95d7857c5421d111a860fcdd20954a0263) GITHUB remote: ionide/ionide-vscode-helpers - src/Fable.Import.Axios.fs (50c364c2e88acc2fec7094bfd3be0973cb2826aa) - src/Fable.Import.VSCode.fs (50c364c2e88acc2fec7094bfd3be0973cb2826aa) - src/Fable.Import.ws.fs (50c364c2e88acc2fec7094bfd3be0973cb2826aa) - src/Helpers.fs (50c364c2e88acc2fec7094bfd3be0973cb2826aa) + src/Fable.Import.Axios.fs (af96a42db5a5f518d66c50f46ad903955360028e) + src/Fable.Import.Showdown.fs (af96a42db5a5f518d66c50f46ad903955360028e) + src/Fable.Import.VSCode.fs (af96a42db5a5f518d66c50f46ad903955360028e) + src/Fable.Import.ws.fs (af96a42db5a5f518d66c50f46ad903955360028e) + src/Helpers.fs (af96a42db5a5f518d66c50f46ad903955360028e) GROUP build NUGET remote: https://www.nuget.org/api/v2 @@ -1140,5 +1141,5 @@ NUGET System.Xml.ReaderWriter (>= 4.3) - restriction: || (&& (< monoandroid) (< monotouch) (< net45) (>= netstandard1.3) (< win8) (< wpa81) (< xamarinios) (< xamarinmac) (< xamarintvos) (< xamarinwatchos)) (&& (< monoandroid) (< net45) (>= netstandard1.0) (< netstandard1.3) (< win8) (< wp8) (< wpa81)) (< portable-net45+win8+wp8+wpa81) GITHUB remote: fsharp/FAKE - modules/Octokit/Octokit.fsx (22668d1c0dbb43626c4d2abdc13e9614e8c9d04d) + modules/Octokit/Octokit.fsx (233a5ab4a51f5c6cf7bd2b1972d6bb5cccee9067) Octokit (>= 0.20) \ No newline at end of file diff --git a/release/images/lock-open-solid-light.svg b/release/images/lock-open-solid-light.svg new file mode 100644 index 00000000..f6c6d528 --- /dev/null +++ b/release/images/lock-open-solid-light.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/release/images/lock-open-solid.svg b/release/images/lock-open-solid.svg new file mode 100644 index 00000000..270e7ca5 --- /dev/null +++ b/release/images/lock-open-solid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/release/images/lock-solid-light.svg b/release/images/lock-solid-light.svg new file mode 100644 index 00000000..3ed55f75 --- /dev/null +++ b/release/images/lock-solid-light.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/release/images/lock-solid.svg b/release/images/lock-solid.svg new file mode 100644 index 00000000..d1c921d8 --- /dev/null +++ b/release/images/lock-solid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/release/package.json b/release/package.json index 17778c96..8cd07ecf 100644 --- a/release/package.json +++ b/release/package.json @@ -422,13 +422,47 @@ }, { "command": "fsharp.revealInSolutionExplorer", - "title": "F#: Reveal in solution explorer", + "title": "Reveal in solution explorer", "category": "F#", "icon": { "light": "./images/auto-reveal-light.svg", "dark": "./images/auto-reveal-dark.svg" } - } + }, + { + "command": "fsharp.openInfoPanel", + "title": "Open Info Panel", + "description": "Opens Info Panel displaying documentation", + "category": "F#" + }, + { + "command": "fsharp.updateInfoPanel", + "title": "Update Info Panel", + "description": "Updates Info Panel with documentation of current symbol", + "category": "F#" + }, + { + "command": "fsharp.showDocumentation", + "title": "Show Documentation for given symbol" + }, + { + "command": "fsharp.openInfoPanel.lock", + "title": "Lock Info Panel", + "category": "F#", + "icon": { + "light": "./images/lock-solid-light.svg", + "dark": "./images/lock-solid.svg" + } + }, + { + "command": "fsharp.openInfoPanel.unlock", + "title": "Unlock Info Panel", + "category": "F#", + "icon": { + "light": "./images/lock-open-solid-light.svg", + "dark": "./images/lock-open-solid.svg" + } + } ], "viewsContainers": { "activitybar": [ @@ -633,6 +667,10 @@ { "command": "MSBuild.cleanCurrent", "when": "editorLangId == 'fsharp'" + }, + { + "command": "fsharp.showDocumentation", + "when": "false" } ], "view/title": [ @@ -998,6 +1036,16 @@ "command": "fsharp.scriptrunner.run", "when": "editorLangId == 'fsharp' && resourceExtname == '.fsx'", "group": "navigation" + }, + { + "command": "fsharp.openInfoPanel.unlock", + "when": "infoPanelFocused && infoPanelLocked", + "group": "navigation" + }, + { + "command": "fsharp.openInfoPanel.lock", + "when": "infoPanelFocused && !infoPanelLocked", + "group": "navigation" } ], "touchBar": [ @@ -1060,6 +1108,16 @@ "command": "MSBuild.buildCurrentSolution", "key": "ctrl+alt+shift+b", "when": "fsharp.project.any" + }, + { + "command": "fsharp.openInfoPanel", + "key": "alt+,", + "when": "editorFocus && editorLangId == 'fsharp'" + }, + { + "command": "fsharp.updateInfoPanel", + "key": "alt+.", + "when": "editorFocus && editorLangId == 'fsharp'" } ], "configurationDefaults": { @@ -1348,6 +1406,32 @@ "type": "boolean", "default": false, "description": "Enables smart indent feature" + }, + "FSharp.infoPanelUpdate": { + "type": "string", + "description": "Controls when the info panel is updated", + "enum": [ + "onCursorMove", + "onHover", + "both", + "none" + ], + "default": "onCursorMove" + }, + "FSharp.infoPanelReplaceHover": { + "type": "boolean", + "description": "Controls whether the info panel replaces tooltips", + "default": false + }, + "FSharp.infoPanelStartLocked": { + "type": "boolean", + "description": "Controls whether the info panel should be locked at startup", + "default": false + }, + "FSharp.infoPanelShowOnStartup": { + "type": "boolean", + "description": "Controls whether the info panel should be displayed at startup", + "default": false } } }, diff --git a/src/Components/InfoPanel.fs b/src/Components/InfoPanel.fs new file mode 100644 index 00000000..e60e08bb --- /dev/null +++ b/src/Components/InfoPanel.fs @@ -0,0 +1,305 @@ +namespace Ionide.VSCode.FSharp + +open System +open Fable.Core +open Fable.Core.JsInterop +open Fable.Import +open Fable.Import.vscode +open Fable.Import.Node +open Ionide.VSCode.Helpers +open DTO +open Fable +open Fable.Import.vscode +module node = Fable.Import.Node.Exports + +module InfoPanel = + + module Panel = + + let showdown = Fable.Import.Showdown.showdown.Converter.Create() + + let mutable panel : WebviewPanel option = None + + let mutable locked = false + + let private isFsharpTextEditor (textEditor : TextEditor) = + if JS.isDefined textEditor && JS.isDefined textEditor.document then + let doc = textEditor.document + match doc with + | Document.FSharp -> true + | _ -> false + else + false + + + let setContent str = + panel |> Option.iter (fun p -> + let str = showdown.makeHtml str + let str = + sprintf """ + + + + + + %s + + + """ str + + p.webview.html <- str + ) + + let clear () = panel |> Option.iter (fun p -> p.webview.html <- "") + + let mapContent res = + if isNotNull res then + let res = (res.Data |> Array.concat).[0] + + let fsharpBlock lines = + let cnt = (lines |> String.concat "\n") + if String.IsNullOrWhiteSpace cnt then "" + else sprintf "
\n%s\n
" cnt + + let sigContent = + let lines = + res.Signature + |> String.split [|'\n'|] + |> Array.filter (not << String.IsNullOrWhiteSpace) + + match lines |> Array.splitAt (lines.Length - 1) with + | (h, [| StartsWith "Full name:" fullName |]) -> + [| yield fsharpBlock h + yield "*" + fullName + "*" |] + | _ -> [| fsharpBlock lines |] + |> String.concat "\n" + + let commentContent = + res.Comment + |> Markdown.createCommentString + + let footerContent = + res.Footer + |> String.split [|'\n' |] + |> Array.filter (not << String.IsNullOrWhiteSpace) + |> Array.map (fun n -> "*" + n + "*") + |> String.concat "\n\n" + + let ctors = + res.Constructors + |> List.filter (not << String.IsNullOrWhiteSpace) + |> List.distinct + |> fsharpBlock + + let intfs = + res.Interfaces + |> List.filter (not << String.IsNullOrWhiteSpace) + |> List.distinct + |> List.sort + |> fsharpBlock + + let attrs = + res.Attributes + |> List.filter (not << String.IsNullOrWhiteSpace) + |> List.distinct + |> List.sort + |> fsharpBlock + + let fncs = + res.Functions + |> List.filter (not << String.IsNullOrWhiteSpace) + |> List.distinct + |> fsharpBlock + + let fields = + res.Fields + |> List.filter (not << String.IsNullOrWhiteSpace) + |> List.distinct + |> fsharpBlock + + let res = + [| + yield sigContent + if not (String.IsNullOrWhiteSpace commentContent) then + yield "---" + yield commentContent + yield "\n" + if not (String.IsNullOrWhiteSpace attrs) then + yield "---" + yield "#### Attributes" + yield attrs + yield "\n" + if not (String.IsNullOrWhiteSpace intfs) then + yield "---" + yield "#### Implemented Interfaces" + yield intfs + yield "\n" + if not (String.IsNullOrWhiteSpace ctors) then + yield "---" + yield "#### Constructors" + yield ctors + yield "\n" + if not (String.IsNullOrWhiteSpace fncs) then + yield "---" + yield "#### Functions" + yield fncs + yield "\n" + if not (String.IsNullOrWhiteSpace fields) then + yield "---" + yield "#### Fields" + yield fields + yield "\n" + if not (String.IsNullOrWhiteSpace footerContent) then + yield "---" + yield (footerContent) + + |] |> String.concat "\n" + Some res + else + None + + let update (textEditor : TextEditor) (selections : ResizeArray) = + promise { + if isFsharpTextEditor textEditor && selections.Count > 0 then + let doc = textEditor.document + let pos = selections.[0].active + let! res = LanguageService.documentation doc.fileName (int pos.line + 1) (int pos.character + 1) + let res = mapContent res + match res with + | None -> () + | Some res -> + setContent res + else + return () + } |> ignore + + let update' (textEditor : TextEditor) (pos : Position) = + promise { + if isFsharpTextEditor textEditor then + let doc = textEditor.document + let! res = LanguageService.documentation doc.fileName (int pos.line + 1) (int pos.character + 1) + let res = mapContent res + match res with + | None -> () + | Some res -> + setContent res + else + return () + } |> ignore + + let updateOnLink xmlSig assemblyName = + promise { + let! res = LanguageService.documentationForSymbol xmlSig assemblyName + let res = mapContent res + match res with + | None -> () + | Some res -> + setContent res + } + + let mutable private timer = None + + let private clearTimer () = + match timer with + | Some t -> + clearTimeout t + timer <- None + | _ -> () + + let private openPanel () = + promise { + match Panel.panel with + | Some p -> + p.reveal (!! -2, true) + | None -> + let opts = + createObj [ + "enableCommandUris" ==> true + "enableFindWidget" ==> true + "retainContextWhenHidden" ==> true + ] + let viewOpts = + createObj [ + "preserveFocus" ==> true + "viewColumn" ==> -2 + ] + let p = window.createWebviewPanel("infoPanel", "Info Panel", !!viewOpts , opts) + let onChange (event : WebviewPanelOnDidChangeViewStateEvent) = + Context.set "infoPanelFocused" event.webviewPanel.active + + let onClose () = + clearTimer () + Panel.panel <- None + + p.onDidChangeViewState.Invoke(!!onChange) |> ignore + p.onDidDispose.Invoke(!!onClose) |> ignore + Panel.panel <- Some p + let textEditor = window.activeTextEditor + let selection = window.activeTextEditor.selections + do if not Panel.locked then Panel.update textEditor selection + } + + let private updatePanel () = + match Panel.panel with + | Some _ -> + let textEditor = window.activeTextEditor + let selection = window.activeTextEditor.selections + Panel.update textEditor selection + | None -> + openPanel () |> ignore + + let private showDocumentation o = + Panel.updateOnLink !!o?XmlDocSig !!o?AssemblyName + + + let private selectionChanged (event : TextEditorSelectionChangeEvent) = + let updateMode = "FSharp.infoPanelUpdate" |> Configuration.get "onCursorMove" + + if not Panel.locked && (updateMode = "onCursorMove" || updateMode = "both") then + clearTimer() + timer <- Some (setTimeout (fun () -> Panel.update event.textEditor event.selections) 500.) + + + let private documentParsedHandler (event : Errors.DocumentParsedEvent) = + if event.document = window.activeTextEditor.document && not Panel.locked then + clearTimer() + Panel.update window.activeTextEditor window.activeTextEditor.selections + () + + let tooltipRequested (pos: Position) = + let updateMode = "FSharp.infoPanelUpdate" |> Configuration.get "onCursorMove" + if updateMode = "onHover" || updateMode = "both" then + clearTimer() + timer <- Some (setTimeout (fun () ->Panel.update' window.activeTextEditor pos) 500.) + + + let lockPanel () = + Panel.locked <- true + Context.set "infoPanelLocked" true + () + + let unlockPanel () = + Panel.locked <- false + Context.set "infoPanelLocked" false + () + + + + let activate (context : ExtensionContext) = + let startLocked = "FSharp.infoPanelStartLocked" |> Configuration.get false + let show = "FSharp.infoPanelShowOnStartup" |> Configuration.get false + + context.subscriptions.Add(window.onDidChangeTextEditorSelection.Invoke(unbox selectionChanged)) + context.subscriptions.Add(Errors.onDocumentParsed.Invoke(unbox documentParsedHandler)) + context.subscriptions.Add(Tooltip.tooltipRequested.Invoke(!! tooltipRequested)) + + commands.registerCommand("fsharp.openInfoPanel", openPanel |> unbox>) |> context.subscriptions.Add + commands.registerCommand("fsharp.updateInfoPanel", updatePanel |> unbox>) |> context.subscriptions.Add + commands.registerCommand("fsharp.openInfoPanel.lock", lockPanel |> unbox>) |> context.subscriptions.Add + commands.registerCommand("fsharp.openInfoPanel.unlock", unlockPanel |> unbox>) |> context.subscriptions.Add + commands.registerCommand("fsharp.showDocumentation", showDocumentation |> unbox>) |> context.subscriptions.Add + + if startLocked then Panel.locked <- true + if show then openPanel () |> ignore \ No newline at end of file diff --git a/src/Components/Tooltip.fs b/src/Components/Tooltip.fs index 2e5d35bc..12b22dfc 100644 --- a/src/Components/Tooltip.fs +++ b/src/Components/Tooltip.fs @@ -8,59 +8,67 @@ open DTO open Ionide.VSCode.Helpers module Tooltip = + let private tooltipRequestedEmitter = EventEmitter() + let tooltipRequested = tooltipRequestedEmitter.event; - let private createProvider () = - let mapResult (doc : TextDocument) (pos : Position) o = - let range = doc.getWordRangeAtPosition pos - if isNotNull o then - let res = (o.Data |> Array.concat).[0] - if JS.isDefined res.Signature then - let markStr lang (value:string) : MarkedString = - createObj [ - "language" ==> lang - "value" ==> value.Trim() - ] |> U3.Case3 + let mapResult (doc : TextDocument) (pos : Position) o = + let range = doc.getWordRangeAtPosition pos + if isNotNull o then + let res = (o.Data |> Array.concat).[0] + if JS.isDefined res.Signature then + let markStr lang (value:string) : MarkedString = + createObj [ + "language" ==> lang + "value" ==> value.Trim() + ] |> U3.Case3 - let fsharpBlock (lines: string[]) : MarkedString = - lines |> String.concat "\n" |> markStr "fsharp" + let fsharpBlock (lines: string[]) : MarkedString = + lines |> String.concat "\n" |> markStr "fsharp" - let sigContent = - let lines = - res.Signature - |> String.split [|'\n'|] - |> Array.filter (not << String.IsNullOrWhiteSpace) + let sigContent = + let lines = + res.Signature + |> String.split [|'\n'|] + |> Array.filter (not << String.IsNullOrWhiteSpace) - match lines |> Array.splitAt (lines.Length - 1) with - | (h, [| StartsWith "Full name:" fullName |]) -> - [| yield fsharpBlock h - yield U3.Case1 (MarkdownString("*").appendText(fullName).appendMarkdown("*")) |] - | _ -> [| fsharpBlock lines |] + match lines |> Array.splitAt (lines.Length - 1) with + | (h, [| StartsWith "Full name:" fullName |]) -> + [| yield fsharpBlock h + yield U3.Case1 (MarkdownString("*").appendText(fullName).appendMarkdown("*")) |] + | _ -> [| fsharpBlock lines |] - let commentContent = - res.Comment - |> Markdown.createCommentBlock - |> U3.Case1 - let footerContent = - res.Footer - |> String.split [|'\n' |] - |> Array.filter (not << String.IsNullOrWhiteSpace) - |> Array.map (fun n -> U3.Case1 (MarkdownString("*").appendText(n).appendMarkdown("*"))) + let commentContent = + res.Comment + |> Markdown.createCommentBlock + |> U3.Case1 + let footerContent = + res.Footer + |> String.split [|'\n' |] + |> Array.filter (not << String.IsNullOrWhiteSpace) + |> Array.map (fun n -> U3.Case1 (MarkdownString("*").appendText(n).appendMarkdown("*"))) - let result = createEmpty - result.range <- range - result.contents <- Array.append sigContent [| yield commentContent; yield! footerContent |] |> ResizeArray - result - else - createEmpty + let result = createEmpty + result.range <- range + result.contents <- Array.append sigContent [| yield commentContent; yield! footerContent |] |> ResizeArray + result else createEmpty + else + createEmpty + + let private createProvider () = { new HoverProvider with member this.provideHover(doc, pos, _ ) = promise { - let! res = LanguageService.tooltip doc.fileName (int pos.line + 1) (int pos.character + 1) - return mapResult doc pos res + tooltipRequestedEmitter.fire pos + let infoPanel = "FSharp.infoPanelReplaceHover" |> Configuration.get false + if infoPanel then + return JS.undefined + else + let! res = LanguageService.tooltip doc.fileName (int pos.line + 1) (int pos.character + 1) + return mapResult doc pos res } |> U2.Case2 } diff --git a/src/Core/DTO.fs b/src/Core/DTO.fs index 323bb755..76fe7e9e 100644 --- a/src/Core/DTO.fs +++ b/src/Core/DTO.fs @@ -41,6 +41,8 @@ module DTO = type WorkspaceLoadRequest = { Files : string[]; DisableInMemoryProjectReferences: bool } + type DocumentationForSymbolReuqest = {XmlSig: string; Assembly: string} + type OverloadSignature = { Signature : string Comment : string } @@ -55,6 +57,8 @@ module DTO = Constructors : string list Fields : string list Functions : string list + Interfaces: string list + Attributes: string list Signature : string Comment : string Footer : string } diff --git a/src/Core/LanguageService.fs b/src/Core/LanguageService.fs index 443d1585..481894d6 100644 --- a/src/Core/LanguageService.fs +++ b/src/Core/LanguageService.fs @@ -334,6 +334,10 @@ module LanguageService = { PositionRequest.Line = line; FileName = handleUntitled fn; Column = col; Filter = "" } |> request "documentation" 0 (makeRequestId()) + let documentationForSymbol xmlSig assembly = + { DocumentationForSymbolReuqest.Assembly = assembly; XmlSig = xmlSig} + |> request "documentationForSymbol" 0 (makeRequestId()) + let toolbar fn line col = { PositionRequest.Line = line; FileName = handleUntitled fn; Column = col; Filter = "" } |> request "tooltip" 0 (makeRequestId()) diff --git a/src/Core/Utils.fs b/src/Core/Utils.fs index d5d5893d..0acbda0c 100644 --- a/src/Core/Utils.fs +++ b/src/Core/Utils.fs @@ -305,6 +305,11 @@ module Markdown = |> normalizeLeadingSpace |> (fun v -> MarkdownString v) + let createCommentString (comment : string) : string = + comment + |> replaceXml + |> normalizeLeadingSpace + module Promise = open Fable.Import.JS diff --git a/src/Ionide.FSharp.fsproj b/src/Ionide.FSharp.fsproj index e89e8bbf..37f80a87 100644 --- a/src/Ionide.FSharp.fsproj +++ b/src/Ionide.FSharp.fsproj @@ -4,6 +4,10 @@ netstandard2.0 + + True + paket-files/Fable.Import.Showdown.fs + True paket-files/Fable.Import.VSCode.fs @@ -65,6 +69,7 @@ + diff --git a/src/fsharp.fs b/src/fsharp.fs index 0b6f1896..6ff8bb5f 100644 --- a/src/fsharp.fs +++ b/src/fsharp.fs @@ -103,6 +103,7 @@ let activate (context : ExtensionContext) : Api = SignatureData.activate context Debugger.activate context Diagnostics.activate context + ) |> Promise.catch (fun error -> promise { () }) // prevent unhandled rejected promises |> ignore @@ -112,6 +113,7 @@ let activate (context : ExtensionContext) : Api = ScriptRunner.activate context LanguageConfiguration.activate context HtmlConverter.activate context + InfoPanel.activate context let buildProject project = promise { let! exit = MSBuild.buildProjectPath "Build" project diff --git a/src/paket.references b/src/paket.references index e09d5eba..55d70472 100644 --- a/src/paket.references +++ b/src/paket.references @@ -6,4 +6,5 @@ Fable.HtmlConverter File:Fable.Import.VSCode.fs File:Helpers.fs File:Fable.Import.Axios.fs -File:Fable.Import.ws.fs \ No newline at end of file +File:Fable.Import.ws.fs +File:Fable.Import.Showdown.fs \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index db460d7b..dff0c471 100644 --- a/yarn.lock +++ b/yarn.lock @@ -828,6 +828,14 @@ cliui@^3.2.0: strip-ansi "^3.0.1" wrap-ansi "^2.0.0" +cliui@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-4.1.0.tgz#348422dbe82d800b3022eef4f6ac10bf2e4d1b49" + dependencies: + string-width "^2.1.1" + strip-ansi "^4.0.0" + wrap-ansi "^2.0.0" + clone-buffer@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/clone-buffer/-/clone-buffer-1.0.0.tgz#e3e25b207ac4e701af721e2cb5a16792cac3dc58" @@ -2977,6 +2985,12 @@ shebang-regex@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" +showdown@^1.9.0: + version "1.9.0" + resolved "https://registry.yarnpkg.com/showdown/-/showdown-1.9.0.tgz#d49d2a0b6db21b7c2e96ef855f7b3b2a28ef46f4" + dependencies: + yargs "^10.0.3" + signal-exit@^3.0.0: version "3.0.2" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" @@ -3092,7 +3106,7 @@ string-width@^1.0.1, string-width@^1.0.2: is-fullwidth-code-point "^1.0.0" strip-ansi "^3.0.0" -string-width@^2.0.0: +string-width@^2.0.0, string-width@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" dependencies: @@ -3585,6 +3599,29 @@ yargs-parser@^7.0.0: dependencies: camelcase "^4.1.0" +yargs-parser@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-8.1.0.tgz#f1376a33b6629a5d063782944da732631e966950" + dependencies: + camelcase "^4.1.0" + +yargs@^10.0.3: + version "10.1.2" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-10.1.2.tgz#454d074c2b16a51a43e2fb7807e4f9de69ccb5c5" + dependencies: + cliui "^4.0.0" + decamelize "^1.1.1" + find-up "^2.1.0" + get-caller-file "^1.0.1" + os-locale "^2.0.0" + require-directory "^2.1.1" + require-main-filename "^1.0.1" + set-blocking "^2.0.0" + string-width "^2.0.0" + which-module "^2.0.0" + y18n "^3.2.1" + yargs-parser "^8.1.0" + yargs@^8.0.2: version "8.0.2" resolved "https://registry.yarnpkg.com/yargs/-/yargs-8.0.2.tgz#6299a9055b1cefc969ff7e79c1d918dceb22c360"