Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feature request] Tooltip support #3465

Open
ImFstAsFckBoi opened this issue Sep 11, 2024 · 7 comments
Open

[Feature request] Tooltip support #3465

ImFstAsFckBoi opened this issue Sep 11, 2024 · 7 comments

Comments

@ImFstAsFckBoi
Copy link

Adding the ability to draw tooltips at the cursor for autocompletes, LSP documentation, etc. is really the only feature that I miss in micro compared to Emacs, Neovim, etc.

Its implementation should not be too disruptive to the codebase either, only an optional API that can be adopted picewise by plugins.

Has anyone worked on this previously? I've whipped up a small test implementation and modified the LSP hover command to use it here: https://github.com/ImFstAsFckBoi/micro/tree/feat/tooltip. If anyone already has a better implementation, then maybe that one should be used instead, but if no one want to work on it, I would be happy to do it.

image

@Andriamanitra
Copy link
Contributor

Ability for plugins to draw tooltips is something I brought up in the discussion about LSP clients (#3231). I think it would be a great addition to the editor.

@ImFstAsFckBoi
Copy link
Author

Great! What @glupi-borna showed in #3231 looked a lot more finished than what I've made so far, so I suppose his implementation would be the most sensible one to actually try and merge at some point.

@glupi-borna
Copy link
Contributor

Honestly, I'd love to work on getting something like this implemented.

I doubt merging my fork would work, cause it's based on a very outdated version of micro, and it has a bunch of other unrelated changes (and it's hacky as all hell, on top of that). I also didn't bother to design the API very well, cause I just needed it to work well enough for my own purposes.

It's a good amount of effort, though, so I'd like to know that the maintainers would even consider merging such a change before I start working :)

@JoeKar
Copy link
Collaborator

JoeKar commented Sep 11, 2024

It's a good amount of effort, though, so I'd like to know that the maintainers would even consider merging such a change before I start working :)

I don't see a problem in case you can provide the most of the logic as a plugin. Tell us which Lua or exposed API is currently insufficient for this feature and we can discuss what is possible.
But as all of you already realized there is already a lot to do maintaining micro, so don't expect to see it realized in the next couple of weeks. 😅

@ImFstAsFckBoi
Copy link
Author

Tell us which Lua or exposed API is currently insufficient for this feature and we can discuss what is possible.

As I implemented it, it would be a new API 'endpoint' similar to micro.InfoBar():Message(msg), for displaying messages to the user like micro.Tooltip():Message(msg).

I've not yet fully implemented a multiple choice dropdown for autocompletion, but I imagine it would be something akin to micro.Tooltip():Choices(choice1, choice2, ...).

This way, the lsp-plugin (or any other plugin for that matter), just need to forward its results to this new tooltip drawing API, implemented in the editor itself.

@glupi-borna
Copy link
Contributor

glupi-borna commented Sep 14, 2024

To start, I would suggest we implement something more primitive than specific components -- simple tooltips, autocompletes, etc., are neat, but they are also limiting in many ways, and implementing and maintaining different UI components for different use cases would increase the burden on the maintainers significantly.

Instead, I propose overlays. As the name suggests, overlays are UI elements that are drawn on top of the rest of the UI. This would be achieved by exposing a few simple rendering primitives (functions) that allow plugin authors to experiment with novel UI ideas and implement complex extension UIs with no additional burden for the maintainers.

These primitives would be thin wrappers around screen.SetContent() and a few other useful functions like config.StringToStyle and friends. Something like:

// DrawRect draws a colored rectangle to the screen
func DrawRect(x, y, w, h int, style tcell.Style)
// DrawText draws text at the given coordinates, clipped by the provided maximum
// width and height.
func DrawText(text string, x, y, w, h int, style tcell.Style)

Overlay positioning

I have discovered that you generally want to position overlays in one of three ways:

  1. relative to the entire screen
  2. relative to the bounds of a BufPane
  3. relative to a buffer Location(line,column)

Of these, 1. is easiest to implement - the overlay is simply drawn on top of everything else, at fixed coordinates. These could be used to, for example, supplant the infobar with a stack of hovering notifications, or to implement a hovering command palette (like in VSCode, SublimeText, etc.), or for some sort of "guided tutorial" (you can imagine small explanation blurbs popping up in the appropriate place when the user interacts with certain features of the editor, etc). Overlays with this positioning only need to be repositioned when the whole viewport (most commonly, the terminal window) is resized.

Next is 2., which is very similar to 1., but is drawn on top of a specific BufPane. This means that the overlay must be repositioned when the BufPane is moved, resized, hidden (tabbed away from), etc. Care needs to be taken that these overlays can not spill outside of the BufPane and into other bufpanes, the infobar, the statusbar area, etc.

The last (3.) type of positioning is also drawn on top of a specific BufPane, but is bound to a specific buffer Location. This means that everything from 2. still applies, but these also need to be repositioned when the view is scrolled or modified.

Interestingly, if we provide a couple of helper functions (getting the screen-space bounds of a BufPane, the screen-space location of a specific Location in a buffer, and perhaps one or two more that I'm not thinking of right now), we can only implement positioning 1. and let plugin authors do the rest. The only drawback is that we would probably be redrawing parts of the UI too often, but maybe that's not a big deal?

Overlay rendering

Depending on how positioning is implemented, overlay rendering can be more or less involved. If we go with implementing each of 1., 2. and 3. in micro, then we're on the hook for ensuring that all rendering operations that an overlay attempts are clipped to the area defined by the positioning.

However, if we go with the simpler method, then overlay rendering is as simple as calling a Draw() function provided by the plugin after everything else in micro is rendered.

Overlay event handling

This is not as important for informational overlays, but more advanced interactive overlays will want to do some level of event handling. For example, an autocomplete overlay will want to intercept and filter keyboard events before they are passed to the underlying bufpane.

The simplest way to enable this behavior (in my opinion): overlays can optionally supply a HandleEvent(tcell.Event) bool function. If the function returns true, the overlay consumes the event, and we don't pass it on to the BufPane. Otherwise, event handling proceeds as normal.

Obviously, this means that we need to expose some way of binding an overlay to a Buffer/BufPane.

On the other hand, event handling can also be left up to plugin maintainers - the onAction/preAction hooks should be enough to do all of this, albeit in a perhaps less ergonomic way.

TL;DR

We implement overlays - small bundles of stuff (at the minimum, a Draw() function that micro would call every frame) that enable plugins to draw to the screen on top of micro. Depending on how hands-on we would want to be, we can implement more or less features.

At the bare minimum, a few new plugin API functions are needed (creating/destroying an overlay, getting the screen-space positions of a BufPanes and buffer locations, a thin wrapper or two around screen.SetContent and a few other utility functions for creating tcell styles).

To have something more concrete to discuss, here is a minimal set of extensions to the plugin API:

// Note: OverlayHandle is just an opaque handle. Could be as simple as an integer
// that gets incremented every time CreateOverlay is called. All that matters is
// that it uniquely identifies the overlay, so that we can properly call
// DestroyOverlay later.
func CreateOverlay(draw func()) OverlayHandle
func DestroyOverlay(overlay OverlayHandle)
func DrawRect(x, y, w, h int, style tcell.Style)
func DrawText(text string, x, y, w, h int, style tcell.Style)
// Note: Rect is just a struct with X,Y,W,H
func BufPaneScreenRect(bp *BufPane) Rect
func BufPaneScreenLoc(bp *BufPane, loc Loc) Loc
func StringToStyle(str string) tcell.Style

Thoughts?

@JoeKar
Copy link
Collaborator

JoeKar commented Sep 22, 2024

Doesn't sound bad so far. 👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants