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 · 12 comments
Open

[Feature request] Tooltip support #3465

ImFstAsFckBoi opened this issue Sep 11, 2024 · 12 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. 👍

@usfbih8u
Copy link
Contributor

I implemented a Lua module that creates tooltips and made a plugin to show autocompletions in a tooltip under the cursor.

You can check it here: https://github.com/usfbih8u/micro-autocomplete-tooltip

To make it look like a real tooltip, I had to fake the background with padded text. In this plugin, it's not a big deal, but I made another plugin to navigate and show the gutter messages in a tooltip (I will release it this week), and there, the formatting becomes a little too cumbersome.

Having a real background in micro, a colorscheme per buffer, or a full border will help with readability and make it easier to create tooltips for plugins. The use of a custom syntax and colorscheme is mandatory.

@JoeKar @glupi-borna @ImFstAsFckBoi

@glupi-borna
Copy link
Contributor

@usfbih8u wow, that's pretty cool! I didn't have time to take a detailed look at the code yet - are you achieving this by writing directly to the buffer?

BTW, I'm planning to take a stab at implementing what's been laid out in my earlier comment sometime soon, it's just that work has kept me very busy lately. Would an API as described above be helpful for you?

@usfbih8u
Copy link
Contributor

TLDR

Having almost full access to Go structs inside Lua is a blessing. However, closing that access with an overlay API and losing control over the buffer itself and the events associated with it may not be an improvement.

The API creates a rectangle with a color and text without syntax, similar to the image at the top(?). This is the idea I’m getting from your post. Please, correct me if I misunderstood.

I believe using a buffer is the best approach, but we need to prioritize addressing the readability issue and the padding situation (more on that later).

I'm not sure how easy it would be to implement this. Perhaps you could create a TooltipPane struct inside Micro instead of modifying BufPane directly. This new struct could have many similarities to BufPane (mostly 1:1), but with some tweaks to resolve the issues that arise from using a standard BufPane.

NOTE: I would like to refine these plugins a bit more, as I think the event handling can be simplified. I will work on this throughout the week, so if you’re busy, let me clean up the code a little, and then you can take a deep look at it.

Problems that I encounter during development

Readability issues and padding shenanigans

Lacking a solid background in Micro, a color scheme per buffer, or a full border forces you to pad the text with spaces and use custom syntax and color schemes to make it readable.

For instance, if you look at the autocomplete plugin, you'll see that it creates a list of suggestions and stores each length to later pad them with spaces until a "box" is formed. It looks something like this:

'for        '
'format     '
'formation  '
'formations '

To highlight the current selection, I added a non-visible Unicode character at the end of the suggestion so that the syntax can detect it as a different item and display it in another color.

Now, regarding the other plugin, gutter-message, which displays messages from the gutter: the main content of the buffer (from the linter) is one line of text. The challenge here is that you cannot allow it to wrap, as it looks terrible, and having no wrapping at all is not a solution. Therefore, you have to (you can check the code; this is the gist):

  1. Check the longest line (can be multiple after the merge).
  2. Calculate the best position to place the tooltip (e.g., top-left corner under the cursor, top-right corner under the cursor, bottom-left corner, etc.) and determine the available width.
  3. Split the lines into words and create the longest line without exceeding that width.
  4. Pad the lines with spaces to reach that width and create the "box."

All of these shenanigans arise from the lack of a proper background.

NOTE: In my color scheme, if the current line is a wrapped line, you can see what happens with the highlight and the background. The highlight ends after the last word of the wrapped line, and only the last line extends to the end. A similar issue occurs if you don't pad the text with spaces, resulting in a "stair-step" appearance.

Thus, avoiding all of this padding with spaces and having a proper background is, in my opinion, a top priority.

Resize and Split issues

  • Layout Modification: The creation of splits modifies the layout. I avoid this by taking snapshots of the splits in the tab and then recovering that snapshot, so there’s no significant issue, and the user of the module remains unaware.

  • Type of Split Matters: While this is not a major issue, it’s something to consider. You are forced to use HSplit, as VSplit leaves a vertical line. However, the module uses HSplit exclusively, so it’s not a significant concern.

  • Rendering Order: The order of execution can create problems, such as the status line being drawn after all the buffers. In my case, this does not create issues because:

    • The autocomplete is drawn within the bounds of the buffer.
    • In the case of the infobar, if the list is too long (scrollbar + full size), it does not allow you to draw it under the status line, which results in the last item being covered. This issue does not occur when the list is shorter.
    • For gutter-message, it is drawn on top of the status line, at least in my tests.
  • Bottom Border Issue: I cannot eliminate the last line that draws the bottom border in the tooltip's BufPane. This needs to be resolved within Micro, possibly as a 'no-border' option(?).

  • Mouse Scroll Issue: Mouse scrolling does not work inside the tooltip. This is likely due to the tree node, being the tooltip over a BufPane with a higher "priority" index, which is the one that receives the scroll events (this is just a guess).

Not detecting some events breaks the layout

In the gutter-message plugin, I encountered a problem due to the inability to detect when an interactive shell is run (the Screen is "shutdown"). Once the execution ends, the layout becomes broken because I could not capture that event. My solution was to detect when a command or command-edit is executed (indicating the start of the interactive shell).

To achieve this, I used the pre() callback, as these actions utilize an empty string as their name. This allows the tooltip to close before the screen is closed. This forces me to create a new tooltip each time the plugin is called, but this does not seem to significantly affect performance.

Things I need in Micro

  • Main Priority: Gaining access to style tcell.Style from Lua or finding another solution to address the readability issues. A custom background for the buffer, similar to what you do with the rectangle, would be ideal, but I suspect that the color scheme and syntax might cause complications.

  • Another High Priority: Filling the gaps in handling certain special events is crucial. For instance, detecting when the screen is closed (as RunInteractiveShell() does). Using the pre() trick is too early, and onSetActive() does not capture it (if I recall correctly).

  • Border Customization: If we pursue that route, border customization could be a valuable feature.

  • Moving Splits Between Tabs: Having the ability to move BufPanes/TooltipPanes between tabs would be useful. I created a testing plugin for a floating terminal pane (similar to a tmux pop-up); however, the issue is that it is attached to only one tab, which looks cool but is not particularly useful. To maintain the console's state I moved the pane in and out of the Screen.

As you can see, the major problems are general Micro issues and not directly related to tooltips.

I think @dmaluka and @JoeKar, being the main developers, should provide their insights on all of this.

@glupi-borna
Copy link
Contributor

The API creates a rectangle with a color and text without syntax, similar to the image at the top(?). This is the idea I’m getting from your post. Please, correct me if I misunderstood.

Not really. Let me explain:

The Overlay API I proposed is supposed to be a very low-level, immediate mode API that would allow plugins to draw anything to the terminal character grid (aka the screen), on top of micro. The reasoning for providing such a low-level API is to lessen the maintenance burden, while supporting a wide number of use cases that would be hard to predict, implement and maintain otherwise.

I intended the drawing commands (DrawRect, DrawText) to be low-level primitives for manipulating the character grid -- DrawRect is supposed to clear any characters in the area designated by the (x, y, w, h) arguments and set the background color to the provided tcell.Style. Then, DrawText could be used to draw colored text on top of that.

To help with positioning:

  • BufPaneScreenRect would tell you the position and extents of a BufPane in screen coordinates
  • BufPaneScreenLoc would allow you to supply any Loc (line and character offset in the buffer) and convert that to a position in screen coordinates

The CreateOverlay function (or specifically, the draw callback) is supposed to mitigate the problem you're having with knowing when to update the view. You simply supply a callback (which renders your custom UI in an immediate-mode manner) that micro can invoke whenever the screen is redrawn. Using the BufPaneScreen* functions in the callback guarantees that your UI will always be properly positioned.

Finally, supporting functions from the config package, like StringToStyle and GetColor (and possibly other functions) can be used to create a tcell.Style or to get tcell.Style values from the user's theme (i.e. GetColor("statusline")), so that plugin UIs can seamlessly integrate and look consistent with the rest of micro.

So, in terms of your main concerns:

  • access to style tcell.Style
    Should be covered by GetColor and StringToStyle, but we can also expose other functions if deemed necessary. I'd err on the conservative side here though - plugins should be encouraged to respect the user's theme as opposed to using their own colors.
  • Filling the gaps in handling certain special events
    I believe that micro calling the draw callback whenever it redraws would be enough to address this, no? Then the plugin does not need to know about these events (and does not need to be updated/become broken whenever new events are added).
  • Border Customization
    Drawing borders is not a part of the proposed API, as I would consider it way too high-level. Plugins should be able to implement such functionality easily on top of this API, if necessary.
  • Moving Splits Between Tabs
    Any plugin UI would by default care about Splits and Tabs unless specifically programmed to do so. So, if you wanted to implement a floating terminal, you just clear a rect (via DrawRect) whereever and draw the terminal contents (via DrawText) inside of the cleared area. Colored text within the terminal can easily be achieved by calling DrawText for each character (although it would probably be more easier and more performant if we exposed a simple command for modifying a single cell in the character grid, maybe DrawCell(x, y, style)?).

Does that make sense?

@usfbih8u
Copy link
Contributor

usfbih8u commented Feb 13, 2025

I think we are proposing different approaches. You want a low-level API with access to tcells, and I want floating BufPanes and all its functionalities.

Losing access to the BufPane functionalities (you cannot write on it, select text, you don't have syntaxes, terminal, etc.) feels like a wrong decision. I gain too much control in areas that I don't care about and lose all the functionality that I could use.

What would be useful for my use case is having floating BufPanes. Having a real background and being able to change it to make contrast with the user color scheme, and not much more (some events1 and other UI things2) would be a great improvement.

Footnotes

  1. When the screen is closed as in RunInteractiveShell() or other similar actions.

  2. The border line (last line that it's shown in my plugins' tooltips) will always be displayed even with the horizontal divchar being " ", and statusline and infobar deactivated. But maybe it can be solved inside Micro.

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

5 participants