Skip to content

Commit 06047d8

Browse files
committed
Refactor hover tooltips
1 parent 2a109c3 commit 06047d8

File tree

8 files changed

+401
-283
lines changed

8 files changed

+401
-283
lines changed

docs/settings.gen.md

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,14 +32,13 @@ For examples and default values see [here](../config/settings.json)
3232
| `editor.watch-workspace-config` | bool | true | Watch the config files in the workspace directory and automatically reload them when they change. |
3333
| `formatter.command` | string[] | [] | Command to run. First entry is path to the formatter program, subsequent entries are passed as arguments to the formatter. |
3434
| `formatter.on-save` | bool | false | If true run the formatter when saving. |
35+
| `hover.command` | JsonNodeEx | null | Command to run when hovering something. |
36+
| `hover.delay` | int | 200 | How many milliseconds after hovering a word the lsp hover request is sent. |
3537
| `lsp-merge.timeout` | int | 10000 | Timeout for LSP requests in milliseconds |
3638
| `plugins.command-load-behaviour` | PluginCommandLoadBehaviour | "async-or-wait" | Defines if and how to run commands which trigger a plugin to load. "dont-run": Don't run the command after the plugin is loaded. You have to manually run the command again. "async-run": Asynchronously load the plugin and run the command afterwards. If the command returns something then the return value will not be available if the command is e.g. called from a plugin. "wait-and-run": Synchronously load the plugin and run the command afterwards. Return values work fine, but the editor will freeze while loading the plugin. "async-or-wait": Use "async-run" behaviour for commands with no return value and "wait-and-run" for commands with return values. |
3739
| `plugins.watch-plugin-directories` | bool | true | Whether to watch the plugin directories for changes and load new plugins |
3840
| `selector.base-mode` | string | "popup.selector" | |
3941
| `selector.min-score` | float | 0 | |
40-
| `terminal.base-mode` | string | "terminal" | Input mode which is always active while a terminal view is active. |
41-
| `terminal.default-mode` | string | "" | Input mode to activate when creating a new terminal, if no mode is specified otherwise. |
42-
| `terminal.idle-threshold` | int | 500 | After how many milliseconds of no data received from a terminal it is considered idle, and can be reused for running more commands. |
4342
| `text.auto-insert-close` | bool | true | Automatically insert closing parenthesis, braces, brackets and quotes. |
4443
| `text.auto-reload` | bool | false | If true then files will be automatically reloaded when the content on disk changes (except if you have unsaved changes). |
4544
| `text.choose-cursor-max` | int | 300 | Maximum number of locations to highlight choose cursor mode. |
@@ -68,9 +67,9 @@ For examples and default values see [here](../config/settings.json)
6867
| `text.highlight-matches.max-file-size` | int | 104857600 | Don't highlight matching text in files above this size (in bytes). |
6968
| `text.highlight-matches.max-selection-length` | int | 1024 | Don't highlight matching text if the selection spans more bytes than this. |
7069
| `text.highlight-matches.max-selection-lines` | int | 5 | Don't highlight matching text if the selection spans more lines than this. |
71-
| `text.hover-command` | JsonNodeEx | null | Arguments to the command which is run when triple clicking on some text. |
72-
| `text.hover-delay` | int | 200 | How many milliseconds after hovering a word the lsp hover request is sent. |
7370
| `text.hover-mode` | string | "editor.text.hover" | Mode to activate while hover window is open. |
71+
| `text.hover.command` | string \| null | null | Command to run when hovering something. |
72+
| `text.hover.delay` | int | 200 | How many milliseconds after hovering a word the lsp hover request is sent. |
7473
| `text.inclusive-selection` | bool | false | Specifies whether a selection includes the character after the end cursor. If true then a selection like (0:0...0:4) with the text "Hello world" would select "Hello". If false then the selected text would be "Hell". If you use Vim motions then the Vim plugin manages this setting. |
7574
| `text.indent` | "tabs" \| "spaces" | "spaces" | Whether to used spaces or tabs for indentation. When indent detection is enabled then this only specfies the default for new files and files where the indentation type can't be detected automatically. |
7675
| `text.indent-after` | string[] \| null | null | When you insert a new line, if the current line ends with one of these strings then the new line will be indented. |

modules/debugger/debugger.nim

Lines changed: 41 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
#use hover_component
12
import platform/[tui]
23
import nimsumtree/[arc]
34
import misc/[id]
@@ -25,7 +26,7 @@ when implModule:
2526
import workspaces/workspace, vfs, vfs_service, language_server_dynamic
2627
import ui/node
2728
import nimsumtree/[rope, buffer]
28-
import text_component, text_editor_component, language_server_component, decoration_component, inlay_hint_component, treesitter_component
29+
import text_component, text_editor_component, language_server_component, decoration_component, inlay_hint_component, treesitter_component, hover_component, move_component
2930
import types_impl
3031

3132
import scripting_api except DocumentEditor, TextDocumentEditor, AstDocumentEditor
@@ -981,35 +982,45 @@ when implModule:
981982
if frame.isNone:
982983
return
983984

984-
# todo
985-
# if self.layout.tryGetCurrentView().getSome(view) and view of EditorView:
986-
# let editor = view.EditorView.editor
987-
# let timestamp = self.timestamp
988-
# if self.client.getSome(client):
989-
# var range = if useMouseHover:
990-
# editor.mouseHoverLocation.toSelection
991-
# else:
992-
# editor.selection
993-
# if range.isEmpty:
994-
# let move = self.config.runtime.get("debugger.hover.move", "(word)")
995-
# range = editor.getSelectionForMove(range.last, move, 1, true)
996-
# let expression = editor.getText(range)
997-
# var evaluation = await client.evaluate(expression, editor.document.localizedPath, range.first.line, range.first.column, frame.get.id)
998-
# if evaluation.isError:
999-
# log lvlWarn, &"Failed to evaluate {expression}: {evaluation}"
1000-
# return
1001-
1002-
# if timestamp != self.timestamp:
1003-
# return
1004-
1005-
# let view = self.createVariablesView()
1006-
# view.renderHeader = false
1007-
# view.evaluation = evaluation.result
1008-
# view.evaluationName = expression
1009-
# view.variablesCursor.scope = -1
1010-
# editor.showHover(view, range.first)
1011-
# if evaluation.result.variablesReference != 0.VariablesReference:
1012-
# asyncSpawn self.updateVariables(evaluation.result.variablesReference, 0)
985+
if self.layout.tryGetCurrentView().getSome(view) and view of EditorView:
986+
let editor = view.EditorView.editor
987+
let hover = editor.getHoverComponent().getOr:
988+
return
989+
990+
let te = editor.getTextEditorComponent().getOr:
991+
return
992+
993+
let moves = editor.currentDocument.getMoveComponent().getOr:
994+
return
995+
let text = editor.currentDocument.getTextComponent().getOr:
996+
return
997+
998+
let timestamp = self.timestamp
999+
if self.client.getSome(client):
1000+
var range = if useMouseHover:
1001+
hover.mouseHoverLocation...hover.mouseHoverLocation
1002+
else:
1003+
te.selection
1004+
if range.a == range.b:
1005+
let move = self.config.runtime.get("debugger.hover.move", "(word)")
1006+
range = moves.applyMove(range, move, 1)
1007+
let expression = text.content(range)
1008+
var evaluation = await client.evaluate(expression, editor.document.localizedPath, range.a.row.int, range.a.column.int, frame.get.id)
1009+
if evaluation.isError:
1010+
log lvlWarn, &"Failed to evaluate {expression}: {evaluation}"
1011+
return
1012+
1013+
if timestamp != self.timestamp:
1014+
return
1015+
1016+
let view = self.createVariablesView()
1017+
view.renderHeader = false
1018+
view.evaluation = evaluation.result
1019+
view.evaluationName = expression
1020+
view.variablesCursor.scope = -1
1021+
hover.showHoverView(view, range.a)
1022+
if evaluation.result.variablesReference != 0.VariablesReference:
1023+
asyncSpawn self.updateVariables(evaluation.result.variablesReference, 0)
10131024

10141025
proc evaluateHover*(self: Debugger) =
10151026
asyncSpawn self.evaluateHoverAsync(useMouseHover = false)

modules/hover_component.nim

Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
1+
import std/[options, tables]
2+
import nimsumtree/[arc, rope]
3+
import misc/[event, custom_async, delayed_task, jsonex]
4+
import component, view, config_provider, input_api
5+
6+
export component
7+
8+
const currentSourcePath2 = currentSourcePath()
9+
include module_base
10+
11+
declareSettings HoverSettings, "hover":
12+
## How many milliseconds after hovering a word the lsp hover request is sent.
13+
declare delay, int, 200
14+
15+
## Command to run when hovering something.
16+
declare command, JsonNodeEx, nil
17+
18+
type
19+
HoverComponent* = ref object of Component
20+
settings*: HoverSettings
21+
hoverView*: View
22+
showHoverTask: DelayedTask # for showing hover info after a delay
23+
hideHoverTask: DelayedTask # for hiding hover info after a delay
24+
currentHoverLocation: Point # the location of the mouse hover
25+
showHover*: bool # whether to show hover info in ui
26+
hoverText*: string # the text to show in the hover info
27+
hoverLocation*: Point # where to show the hover info
28+
hoverScrollOffset*: float # the scroll offset inside the hover window
29+
onHoverViewMarkedDirtyHandle: Id
30+
hoverViewCallbackHandles: Id
31+
overlayViews*: seq[View]
32+
mouseHoverLocation*: Point
33+
mouseHoverMods*: Modifiers
34+
onModsChangedHandle: Id
35+
isHovered*: bool
36+
37+
# DLL API
38+
{.push rtl, gcsafe, raises: [].}
39+
proc getHoverComponent*(self: ComponentOwner): Option[HoverComponent]
40+
proc newHoverComponent*(settings: HoverSettings): HoverComponent
41+
42+
proc hoverComponentClearOverlayViews(self: HoverComponent)
43+
proc hoverComponentClearHoverView(self: HoverComponent)
44+
proc hoverComponentShowHoverForAsync(self: HoverComponent, cursor: Point): Future[void] {.async.}
45+
proc hoverComponentHideHover(self: HoverComponent)
46+
proc hoverComponentShowHoverDelayed(self: HoverComponent)
47+
proc hoverComponentCancelHover(self: HoverComponent)
48+
proc hoverComponentShowHoverView(self: HoverComponent, view: View, location: Point)
49+
{.pop.}
50+
51+
# Nice wrappers
52+
{.push inline.}
53+
proc clearHoverView*(self: HoverComponent) = hoverComponentClearHoverView(self)
54+
proc clearOverlayViews*(self: HoverComponent) = hoverComponentClearOverlayViews(self)
55+
proc showHoverForAsync*(self: HoverComponent, cursor: Point): Future[void] {.async.} = hoverComponentShowHoverForAsync(self, cursor).await
56+
proc hideHover*(self: HoverComponent) = hoverComponentHideHover(self)
57+
proc showHoverDelayed*(self: HoverComponent) = hoverComponentShowHoverDelayed(self)
58+
proc cancelHover*(self: HoverComponent) = hoverComponentCancelHover(self)
59+
proc showHoverView*(self: HoverComponent, view: View, location: Point) = hoverComponentShowHoverView(self, view, location)
60+
{.pop.}
61+
62+
# Implementation
63+
when implModule:
64+
import std/[sequtils, streams, strformat]
65+
import misc/[util, custom_logger, rope_utils, async_process]
66+
import nimsumtree/[rope, buffer, clock]
67+
import document, document_editor, text_component, language_server_component
68+
import scripting_api except DocumentEditor, HoverComponent, AstDocumentEditor
69+
import command_service, service, platform_service, platform/platform
70+
71+
logCategory "hover-component"
72+
73+
proc handleModsChanged(self: HoverComponent, oldMods: Modifiers, newMods: Modifiers) {.gcsafe, raises: [].}
74+
75+
var HoverComponentId: ComponentTypeId = componentGenerateTypeId()
76+
77+
proc markDirty(self: HoverComponent) =
78+
self.owner.DocumentEditor.markDirty()
79+
80+
proc getHoverComponent*(self: ComponentOwner): Option[HoverComponent] {.gcsafe, raises: [].} =
81+
return self.getComponent(HoverComponentId).mapIt(it.HoverComponent)
82+
83+
proc newHoverComponent*(settings: HoverSettings): HoverComponent =
84+
return HoverComponent(
85+
typeId: HoverComponentId,
86+
settings: settings,
87+
initializeImpl: (proc(self: Component, owner: ComponentOwner) =
88+
let self = self.HoverComponent
89+
let platform = getServices().getService(PlatformService).get.platform
90+
self.onModsChangedHandle = platform.onModifiersChanged.subscribe proc(change: tuple[old: Modifiers, new: Modifiers]) {.gcsafe.} =
91+
self.handleModsChanged(change.old, change.new)
92+
),
93+
deinitializeImpl: (proc(self: Component) =
94+
let self = self.HoverComponent
95+
if self.showHoverTask.isNotNil:
96+
self.showHoverTask.pause()
97+
if self.hideHoverTask.isNotNil:
98+
self.hideHoverTask.pause()
99+
let platform = getServices().getService(PlatformService).get.platform
100+
platform.onModifiersChanged.unsubscribe self.onModsChangedHandle
101+
),
102+
)
103+
104+
proc handleModsChanged(self: HoverComponent, oldMods: Modifiers, newMods: Modifiers) =
105+
if not self.isHovered:
106+
return
107+
self.mouseHoverMods = newMods
108+
if self.showHover:
109+
self.showHoverDelayed()
110+
111+
proc hoverComponentClearOverlayViews(self: HoverComponent) =
112+
for overlay in self.overlayViews:
113+
overlay.onMarkedDirty.unsubscribe(self.onHoverViewMarkedDirtyHandle)
114+
overlay.onDetached.unsubscribe(self.hoverViewCallbackHandles)
115+
self.overlayViews.setLen(0)
116+
117+
proc hoverComponentClearHoverView(self: HoverComponent) =
118+
if self.hoverView != nil:
119+
self.hoverView.onMarkedDirty.unsubscribe(self.onHoverViewMarkedDirtyHandle)
120+
self.hoverView.onDetached.unsubscribe(self.hoverViewCallbackHandles)
121+
self.hoverView = nil
122+
123+
proc detachHoverView(self: HoverComponent) =
124+
if self.hoverView != nil:
125+
self.overlayViews.add(self.hoverView)
126+
self.hoverView = nil
127+
self.showHover = false
128+
129+
proc hoverComponentShowHoverForAsync(self: HoverComponent, cursor: Point): Future[void] {.async.} =
130+
if self.hideHoverTask.isNotNil:
131+
self.hideHoverTask.pause()
132+
133+
let document = self.owner.DocumentEditor.currentDocument
134+
135+
let ls = document.getLanguageServerComponent().getOr:
136+
log lvlWarn, &"Failed to show hover for '{cursor}': No language server"
137+
return
138+
139+
let hoverInfo = await ls.getHover(document.filename, cursor.toCursor)
140+
if self.owner.isNil:
141+
return
142+
143+
if hoverInfo.getSome(hoverInfo):
144+
self.showHover = true
145+
# self.showSignatureHelp = false # todo
146+
self.hoverScrollOffset = 0
147+
self.hoverText = hoverInfo
148+
self.clearHoverView()
149+
self.hoverLocation = cursor
150+
else:
151+
self.clearHoverView()
152+
self.showHover = false
153+
154+
self.markDirty()
155+
156+
# proc showHover*(self: HoverComponent, message: string, location: Cursor) =
157+
# if self.hideHoverTask.isNotNil:
158+
# self.hideHoverTask.pause()
159+
160+
# self.showHover = true
161+
# # self.showSignatureHelp = false # todo
162+
# self.hoverScrollOffset = 0
163+
# self.hoverText = message
164+
# self.clearHoverView()
165+
# self.hoverLocation = location
166+
167+
# self.markDirty()
168+
169+
proc hoverComponentShowHoverView(self: HoverComponent, view: View, location: Point) =
170+
if self.hideHoverTask.isNotNil:
171+
self.hideHoverTask.pause()
172+
173+
self.clearHoverView()
174+
self.showHover = true
175+
self.hoverScrollOffset = 0
176+
self.hoverView = view
177+
self.hoverLocation = location
178+
self.onHoverViewMarkedDirtyHandle = view.onMarkedDirty.subscribe proc() = self.markDirty()
179+
view.onDetached.subscribe self.hoverViewCallbackHandles, proc() = self.detachHoverView()
180+
181+
self.markDirty()
182+
183+
proc hoverComponentCancelHover(self: HoverComponent) =
184+
if self.showHoverTask.isNotNil:
185+
self.showHoverTask.pause()
186+
187+
proc hoverComponentHideHover(self: HoverComponent) =
188+
## Hides the hover information.
189+
self.clearHoverView()
190+
self.showHover = false
191+
self.markDirty()
192+
193+
proc hideHoverDelayed*(self: HoverComponent) =
194+
## Hides the hover information after a delay.
195+
if self.showHoverTask.isNotNil:
196+
self.showHoverTask.pause()
197+
198+
let hoverDelayMs = self.settings.delay.get()
199+
if self.hideHoverTask.isNil:
200+
self.hideHoverTask = startDelayed(hoverDelayMs, repeat=false):
201+
self.hideHover()
202+
else:
203+
self.hideHoverTask.interval = hoverDelayMs
204+
self.hideHoverTask.reschedule()
205+
206+
proc runHoverCommand*(self: HoverComponent) =
207+
try:
208+
let commands = getServices().getService(CommandService).get
209+
var command = ".show-hover-for-current"
210+
var configCommand = self.settings.command.get()
211+
if configCommand != nil:
212+
let modsKey = $self.mouseHoverMods
213+
if configCommand.kind == jsonex.JObject and configCommand.hasKey(modsKey):
214+
configCommand = configCommand[modsKey]
215+
216+
let (name, args, ok) = configCommand.parseCommand()
217+
if name == "":
218+
return
219+
if ok:
220+
discard commands.executeCommand(name & " " & args, record = false)
221+
return
222+
223+
discard commands.executeCommand(command, record = false)
224+
except CatchableError as e:
225+
log lvlError, &"Failed to execute hover command: {e.msg}"
226+
227+
proc hoverComponentShowHoverDelayed(self: HoverComponent) =
228+
## Show hover information after a delay.
229+
230+
if self.hideHoverTask.isNotNil:
231+
self.hideHoverTask.pause()
232+
233+
let hoverDelayMs = self.settings.delay.get()
234+
if self.showHoverTask.isNil:
235+
self.showHoverTask = startDelayed(hoverDelayMs, repeat=false):
236+
self.runHoverCommand()
237+
else:
238+
self.showHoverTask.interval = hoverDelayMs
239+
self.showHoverTask.reschedule()
240+
241+
proc init_module_hover_component*() {.cdecl, exportc, dynlib.} =
242+
discard

src/dynamic_view.nim

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ method checkDirty*(view: DynamicView) =
5656
method markDirty*(view: DynamicView, notify: bool = true) =
5757
if view.markDirtyImpl != nil:
5858
view.markDirtyImpl(view, notify)
59+
else:
60+
view.markDirtyBase(notify)
5961

6062
method getEventHandlers*(view: DynamicView, inject: Table[string, EventHandler]): seq[EventHandler] =
6163
if view.getEventHandlersImpl != nil:

src/module_imports.nim

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import "../modules/debugger/debugger.nim"
2+
import "../modules/hover_component.nim"
23
import "../modules/language_server_ctags.nim"
34
import "../modules/language_server_regex.nim"
45
import "../modules/workspace_edit.nim"
@@ -10,6 +11,7 @@ import "../modules/angelscript_formatter.nim"
1011

1112
proc initModules*() =
1213
init_module_debugger()
14+
init_module_hover_component()
1315
init_module_language_server_ctags()
1416
init_module_language_server_regex()
1517
init_module_workspace_edit()
@@ -21,6 +23,7 @@ proc initModules*() =
2123

2224
proc shutdownModules*() =
2325
when declared(shutdown_module_debugger): shutdown_module_debugger()
26+
when declared(shutdown_module_hover_component): shutdown_module_hover_component()
2427
when declared(shutdown_module_language_server_ctags): shutdown_module_language_server_ctags()
2528
when declared(shutdown_module_language_server_regex): shutdown_module_language_server_regex()
2629
when declared(shutdown_module_workspace_edit): shutdown_module_workspace_edit()

0 commit comments

Comments
 (0)