-
-
Notifications
You must be signed in to change notification settings - Fork 282
/
InfoPanel.fs
339 lines (271 loc) · 11.9 KB
/
InfoPanel.fs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
namespace Ionide.VSCode.FSharp
open System
open Fable.Core
open Fable.Core.JsInterop
open Fable.Import.VSCode.Vscode
open Ionide.VSCode.Helpers
open DTO
module node = Node.Api
module InfoPanel =
let private isFsharpTextEditor (textEditor: TextEditor) =
if JS.isDefined textEditor && JS.isDefined textEditor.document then
let doc = textEditor.document
match doc with
| Document.FSharp
| Document.FSharpScript -> true
| _ -> false
else
false
module Panel =
let showdownOptions =
Fable.Import.Showdown.showdown.getDefaultOptions () :?> Fable.Import.Showdown.Showdown.ConverterOptions
showdownOptions.tables <- Some true
let showdown = Fable.Import.Showdown.showdown.Converter.Create(showdownOptions)
let mutable panel: WebviewPanel option = None
let mutable locked = false
let setContent str =
panel
|> Option.iter (fun p ->
let str = showdown.makeHtml str
let str =
sprintf
"""
<html>
<head>
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline';">
<style>
pre {
color: var(--vscode-editor-foreground);
font-family: var(--vscode-editor-font-family);
font-weight: var(--vscode-editor-font-weight);
font-size: var(--vscode-editor-font-size);
}
code {
font-family: var(--vscode-editor-font-family);
font-weight: var(--vscode-editor-font-weight);
font-size: var(--vscode-editor-font-size);
}
</style>
</head>
<body>
%s
</body>
</html>
"""
str
p.webview.html <- str)
let clear () =
panel |> Option.iter (fun p -> p.webview.html <- "")
let mapContent res =
if isNotNull res then
let res: DocumentationDescription = res.Data
let fsharpBlock lines =
let cnt = (lines |> String.concat "\n")
if String.IsNullOrWhiteSpace cnt then
""
else
sprintf "<pre>\n%s\n</pre>" cnt
let sigContent =
res.Signature
|> String.split [| '\n' |]
|> Array.filter (not << String.IsNullOrWhiteSpace)
|> fsharpBlock
let commentContent = res.Comment
let footerContent = res.FooterLines |> 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 types =
res.DeclaredTypes
|> 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 types) then
yield "---"
yield "#### Declared Types"
yield types
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<Selection>) =
promise {
if isFsharpTextEditor textEditor && selections.Count > 0 && panel.IsSome then
let doc = textEditor.document
let pos = selections.[0].active
let! res = LanguageService.documentation doc.uri (int pos.line) (int pos.character)
res |> Option.bind mapContent |> Option.iter setContent
else
return ()
}
|> ignore
let update' (textEditor: TextEditor) (pos: Position) =
promise {
if isFsharpTextEditor textEditor && panel.IsSome then
let doc = textEditor.document
let! res = LanguageService.documentation doc.uri (int pos.line) (int pos.character)
res |> Option.bind mapContent |> Option.iter setContent
else
return ()
}
|> ignore
let updateOnLink xmlSig assemblyName =
promise {
let! res = LanguageService.documentationForSymbol xmlSig assemblyName
res |> Option.bind mapContent |> Option.iter setContent
}
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.Value
let selection = textEditor.selections
do
if not Panel.locked then
Panel.update textEditor selection
}
let private updatePanel () =
match Panel.panel with
| Some _ ->
let textEditor = window.activeTextEditor.Value
let selection = textEditor.selections
Panel.update textEditor selection
| None -> openPanel () |> ignore
let private showDocumentation o =
// If the panel doesn't exist, open it
// This happens when using click on "Open documentation" from inside
// the tooltip
promise {
match Panel.panel with
| Some _ -> ()
| None -> do! openPanel ()
do! 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: Notifications.DocumentParsedEvent) =
if event.document = window.activeTextEditor.Value.document && not Panel.locked then
clearTimer ()
Panel.update window.activeTextEditor.Value window.activeTextEditor.Value.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.Value 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.Subscribe(window.onDidChangeTextEditorSelection.Invoke(selectionChanged >> box >> Some))
context.Subscribe(Notifications.onDocumentParsed.Invoke(documentParsedHandler >> box >> Some))
context.Subscribe(Notifications.tooltipRequested.Invoke(tooltipRequested >> box >> Some))
commands.registerCommand ("fsharp.openInfoPanel", openPanel |> objfy2)
|> context.Subscribe
commands.registerCommand ("fsharp.updateInfoPanel", updatePanel |> objfy2)
|> context.Subscribe
commands.registerCommand ("fsharp.openInfoPanel.lock", lockPanel |> objfy2)
|> context.Subscribe
commands.registerCommand ("fsharp.openInfoPanel.unlock", unlockPanel |> objfy2)
|> context.Subscribe
commands.registerCommand ("fsharp.showDocumentation", showDocumentation |> objfy2)
|> context.Subscribe
if startLocked then
Panel.locked <- true
if show && window.visibleTextEditors |> Seq.exists isFsharpTextEditor then
openPanel () |> ignore
else
()