Overlays, example overlay plugins

This commit is contained in:
Borna Lang 2025-05-03 12:50:26 +02:00
parent 06fe85c8c9
commit 051d715457
10 changed files with 723 additions and 0 deletions

View File

@ -4,6 +4,7 @@ import (
"log"
"time"
"github.com/micro-editor/tcell/v2"
lua "github.com/yuin/gopher-lua"
luar "layeh.com/gopher-luar"
@ -12,6 +13,7 @@ import (
"github.com/zyedidia/micro/v2/internal/config"
"github.com/zyedidia/micro/v2/internal/display"
ulua "github.com/zyedidia/micro/v2/internal/lua"
"github.com/zyedidia/micro/v2/internal/overlay"
"github.com/zyedidia/micro/v2/internal/screen"
"github.com/zyedidia/micro/v2/internal/shell"
"github.com/zyedidia/micro/v2/internal/util"
@ -35,6 +37,8 @@ func LuaImport(pkg string) *lua.LTable {
return luaImportMicroConfig()
case "micro/util":
return luaImportMicroUtil()
case "micro/overlay":
return luaImportMicroOverlay()
default:
return ulua.Import(pkg)
}
@ -163,3 +167,20 @@ func luaImportMicroUtil() *lua.LTable {
return pkg
}
func luaImportMicroOverlay() *lua.LTable {
pkg := ulua.L.NewTable()
ulua.L.SetField(pkg, "CreateOverlay", luar.New(ulua.L, overlay.CreateOverlay))
ulua.L.SetField(pkg, "DestroyOverlay", luar.New(ulua.L, overlay.DestroyOverlay))
ulua.L.SetField(pkg, "DrawText", luar.New(ulua.L, overlay.DrawText))
ulua.L.SetField(pkg, "DrawRect", luar.New(ulua.L, overlay.DrawRect))
ulua.L.SetField(pkg, "BufPaneScreenRect", luar.New(ulua.L, overlay.BufPaneScreenRect))
ulua.L.SetField(pkg, "BufPaneScreenLoc", luar.New(ulua.L, overlay.BufPaneScreenLoc))
ulua.L.SetField(pkg, "Style", luar.New(ulua.L, func() tcell.Style { return tcell.Style{} }))
ulua.L.SetField(pkg, "GetColor", luar.New(ulua.L, config.GetColor))
ulua.L.SetField(pkg, "StringToStyle", luar.New(ulua.L, config.StringToStyle))
ulua.L.SetField(pkg, "Redraw", luar.New(ulua.L, screen.Redraw))
return pkg
}

View File

@ -24,6 +24,7 @@ import (
"github.com/zyedidia/micro/v2/internal/buffer"
"github.com/zyedidia/micro/v2/internal/clipboard"
"github.com/zyedidia/micro/v2/internal/config"
"github.com/zyedidia/micro/v2/internal/overlay"
"github.com/zyedidia/micro/v2/internal/screen"
"github.com/zyedidia/micro/v2/internal/shell"
"github.com/zyedidia/micro/v2/internal/util"
@ -469,6 +470,7 @@ func DoEvent() {
}
action.MainTab().Display()
action.InfoBar.Display()
overlay.DisplayOverlays()
screen.Screen.Show()
// Check for new events

View File

@ -109,6 +109,10 @@ func (w *BufWindow) BufView() View {
}
}
func (w *BufWindow) GutterOffset() int {
return w.gutterOffset
}
func (w *BufWindow) updateDisplayInfo() {
b := w.Buf

View File

@ -455,6 +455,7 @@ func importStrings() *lua.LTable {
L.SetField(pkg, "ContainsAny", luar.New(L, strings.ContainsAny))
L.SetField(pkg, "ContainsRune", luar.New(L, strings.ContainsRune))
L.SetField(pkg, "Count", luar.New(L, strings.Count))
L.SetField(pkg, "Cut", luar.New(L, strings.Cut))
L.SetField(pkg, "EqualFold", luar.New(L, strings.EqualFold))
L.SetField(pkg, "Fields", luar.New(L, strings.Fields))
L.SetField(pkg, "FieldsFunc", luar.New(L, strings.FieldsFunc))

130
internal/overlay/overlay.go Normal file
View File

@ -0,0 +1,130 @@
package overlay
import (
"github.com/mattn/go-runewidth"
"github.com/micro-editor/tcell/v2"
"github.com/zyedidia/micro/v2/internal/action"
"github.com/zyedidia/micro/v2/internal/buffer"
"github.com/zyedidia/micro/v2/internal/config"
"github.com/zyedidia/micro/v2/internal/display"
"github.com/zyedidia/micro/v2/internal/screen"
"github.com/zyedidia/micro/v2/internal/util"
)
type OverlayHandle int
type OverlayFunction func()
type Rect struct {
X, Y, W, H int
}
var overlay_handle = OverlayHandle(0)
var overlays = make(map[OverlayHandle]OverlayFunction)
func DisplayOverlays() {
// Should an OverlayFunction create or destroy an overlay, that would modify
// the overlays map while we are iterating through it.
// For this reason, we copy the overlays map into temp_overlays.
temp_overlays := make(map[OverlayHandle]OverlayFunction, len(overlays))
for h, o := range overlays {
temp_overlays[h] = o
}
for _, draw_fn := range temp_overlays {
draw_fn()
}
}
// CreateOverlay creates and registers a new overlay, and returns
// the OverlayHandle associated with it.
func CreateOverlay(draw OverlayFunction) OverlayHandle {
overlay_handle++
overlays[overlay_handle] = draw
return overlay_handle
}
// DestroyOverlay destroys/deregisters an existing overlay via its handle.
func DestroyOverlay(overlay OverlayHandle) {
delete(overlays, overlay)
}
// DrawRect draws a flat styled rectangle to the provided screen coordinates.
func DrawRect(x, y, w, h int, style tcell.Style) {
for yy := 0; yy < h; yy++ {
for xx := 0; xx < w; xx++ {
screen.SetContent(x+xx, y+yy, ' ', nil, style)
}
}
}
// DrawText draws styled clipped text to the provided screen coordinates.
func DrawText(text string, x, y, w, h int, style tcell.Style) {
DrawRect(x, y, w, h, style)
tabsize := util.IntOpt(config.GlobalSettings["tabsize"])
text_bytes := []byte(text)
xx := 0
yy := 0
for len(text_bytes) > 0 {
r, combc, size := util.DecodeCharacter(text_bytes)
text_bytes = text_bytes[size:]
width := 0
switch r {
case '\t':
width = tabsize - (xx % tabsize)
case '\n':
xx = 0
yy++
continue
default:
width = runewidth.RuneWidth(r)
}
if yy > h {
break
}
if xx+width <= w {
screen.SetContent(x+xx, y+yy, r, combc, style)
}
xx += width
}
}
// BufPaneScreenRect returns the bounds of a BufPane in screen coordinates.
func BufPaneScreenRect(bp *action.BufPane) Rect {
// NOTE: This function is a very thin wrapper around bp.GetView(). As such,
// it is maybe a candidate for removal?
v := bp.GetView()
return Rect{
X: v.X,
Y: v.Y,
W: v.Width,
H: v.Height,
}
}
// BufPaneScreenLoc converts a Loc in the buffer displayed in
// a bufpane to screen coordinates.
func BufPaneScreenLoc(bp *action.BufPane, loc buffer.Loc) buffer.Loc {
gutter := 0
bw, ok := bp.BWindow.(*display.BufWindow)
if ok {
gutter = bw.GutterOffset()
}
v := bp.GetView()
vloc := bp.VLocFromLoc(loc)
top := v.StartLine
yoff := bp.Diff(top, vloc.SLoc)
return buffer.Loc{
X: v.X + gutter + vloc.VisualX,
Y: v.Y + yoff,
}
}

View File

@ -364,6 +364,32 @@ The packages and their contents are listed below (in Go type signatures):
Relevant links:
[Rune](https://pkg.go.dev/builtin#rune)
* `micro/overlay`
- `CreateOverlay(draw func()) OverlayHandle`: creates and registers a new
overlay, and returns the OverlayHandle associated with it.
- `DestroyOverlay(handle OverlayHandle)`: deregisters an existing overlay
via its handle.
- `DrawText(text string, x, y, w, h int, style tcell.Style)`: draws styled
text clipped to the bounds of the provided screen rectangle.
- `DrawRect(x, y, w, h int, style tcell.Style)`: draws a rectangle to the
provided screen coordinates.
- `BufPaneScreenRect(bp BufPane) overlay.Rect`: returns the bounds of a
BufPane in screen coordinates.
- `BufPaneScreenLoc(bp BufPane, l Loc) Loc`: converts from line/column
coordinates to screen coordinates.
- `Style() tcell.Style`: returns a default (empty) tcell.Style.
- `GetColor(name string) tcell.Style`: takes in a syntax group and returns
the colorscheme's style for that group.
- `StringToStyle(str string) tcell.Style`: returns a style from a string.
The string must be in the format "extra foregroundcolor,backgroundcolor".
The "extra" can be bold, reverse, italic or underline.
- `Redraw()`: schedules a redraw of the entire screen.
Relevant links:
[BufPane](https://pkg.go.dev/github.com/zyedidia/micro/v2/internal/action#BufPane)
[tcell.Style](https://pkg.go.dev/github.com/micro-editor/tcell/v2#Style)
This may seem like a small list of available functions, but some of the objects
returned by the functions have many methods. The Lua plugin may access any
public methods of an object returned by any of the functions above.

View File

@ -0,0 +1,144 @@
VERSION = "1.0.0"
local micro = import("micro")
local config = import("micro/config")
local buffer = import("micro/buffer")
local overlay = import("micro/overlay")
-- Immediate-mode event handling
local overlay_handle = nil
local event_count = 0
local events = {}
local tracked_events = {}
function track_event(name, block)
-- Registers a global handler for an event
-- If "no_block" is passed as the second argument,
-- the event will not be prevented.
local full_name = "pre" .. name
if block=="no_block" then
full_name = "on"..name
end
if not tracked_events[full_name] then
tracked_events[full_name] = true
if block~="no_block" then
_G[full_name] = function()
if overlay_handle then
events[name] = true
event_count = event_count + 1
end
end
else
_G[full_name] = function()
if overlay_handle then
events[name] = true
event_count = event_count + 1
return false
end
end
end
end
end
function untrack_events()
-- Removes all global event handlers
for e, _ in pairs(tracked_events) do
_G[e] = nil
end
tracked_events = {}
end
function reset_events()
-- Resets tracked events between redraws
events = {}
event_count = 0
end
function event(event_name, block)
-- Returns true if the event has occured.
track_event(event_name, block)
return events[event_name] or false
end
function close_overlay()
-- Closes the overlay and untracks all events.
untrack_events()
overlay.DestroyOverlay(overlay_handle)
overlay_handle = nil
end
function max_len(iter)
-- Returns the length of the longest string in iterable
local max = 0
for _, item in iter do
max = math.max(max, #item)
end
return max
end
function draw_autocomplete_overlay()
local bp = micro.CurPane()
local buf = bp.Buf
if not buf.HasSuggestions then
-- If there are no suggestions, we close the overlay.
close_overlay()
return
end
-- These events should not close the menu, so we track them, but
-- we do not block them, because we want autocomplete cycling to work.
event("CycleAutocomplete", "no_block")
event("CycleAutocompleteBack", "no_block")
-- Positioning adjustment - show the menu below where the cursor
-- was when autocomplete was initiated by subtracting the length
-- of the currently applied completion.
local compl_len = #buf.Completions[buf.CurSuggestion+1] + 1
-- Note: The minus dereferences the Loc pointer
local l = -buf:GetActiveCursor().Loc
l = overlay.BufPaneScreenLoc(bp, l)
local x = l.X-compl_len
local y = l.Y+1
-- Calculate the maximum text width of the options,
-- add 2 cells of padding
local w = max_len(buf.Suggestions())+2
-- Draw each option, highlight the current option
local yoff = 0
local style = overlay.GetColor("cursor-line")
for i, option in buf.Suggestions() do
local style = overlay.Style()
if i == buf.CurSuggestion+1 then
style = overlay.GetColor("statusline")
end
overlay.DrawText(" "..option, x, y+yoff, w, 1, style)
yoff = yoff+1
end
reset_events()
end
function init()
config.AddRuntimeFile("completebox", config.RTHelp, "help/completebox.md")
end
function deinit()
close_overlay()
untrack_events()
end
function onAutocomplete()
if overlay_handle then return end
reset_events()
overlay_handle = overlay.CreateOverlay(draw_autocomplete_overlay)
end

View File

@ -0,0 +1,5 @@
# CompleteBox Plugin
The completebox plugin demonstrates a simple way to hook
into micro's autocomplete mechanism to display the list of
available completions as an overlay at the cursor.

View File

@ -0,0 +1,21 @@
# QuickMenu Plugin
The quickmenu plugin is a slightly more involved example of what micro's new
overlay system can do.
The plugin exposes a palette-like quickmenu that can be used to quickly find
files by name (via 'find') or by content (via 'grep').
It exposes two new commands, and a single global option.
Commands:
* `quicksearch`: Opens the find-by-name menu.
* `quickopen`: Opens the find-by-contents menu.
By default, quicksearch will be bound to `Alt-f`, and quickopen to `Alt-o`
Options:
* `quickmenu.newtab`: when a file is opened via the quickmenu, it will be opened
in a new tab.
default value: `true`

View File

@ -0,0 +1,369 @@
local micro = import("micro")
local config = import("micro/config")
local buffer = import("micro/buffer")
local overlay = import("micro/overlay")
local shell = import("micro/shell")
local strings = import("strings")
local pathlib = import("path")
local last_job = nil
local results = {}
function wrap_int(val, min, max)
if min==max then return min end
local range = max - min + 1
return min + (val - min) % range
end
function clamp(val, min, max)
if val < min then return min end
if val > max then return max end
return val
end
function array_get(arr, idx)
if idx <= #arr then
return arr[idx]
else
return nil
end
end
function cancel_job(job)
if job and not job.ProcessState then
shell.JobStop(job)
end
end
function find(query)
cancel_job(last_job)
local job = nil
results = {}
local parts = strings.Fields(query)
local args = {".", "-type", "f"}
for i, part in parts() do
if i>1 then
args[#args+1] = "-and"
end
args[#args+1] = "-ipath"
args[#args+1] = "*"..part.."*"
end
function on_stdout(data)
if job~=last_job then
cancel_job(job)
return
end
local new_results = strings.Split(data, "\n")
for _, path in new_results() do
if #path>0 then
results[#results+1] = {type="file",path=path}
end
end
overlay.Redraw()
if #results>20 then
cancel_job()
end
end
function on_stderr()
cancel_job(job)
end
job = shell.JobSpawn(
"find", args, on_stdout, on_stderr, nil
)
last_job = job
end
function grep(query)
cancel_job(last_job)
local job = nil
results = {}
function on_stdout(data)
if job~=last_job then
cancel_job(job)
return
end
local new_results = strings.Split(data, "\n")
for _, res in new_results() do
local path, line, content, ok
path, res, ok = strings.Cut(res, ":")
if ok then
line, content, ok = strings.Cut(res, ":")
if ok then
results[#results+1] = {
type="line",
path=path,
line=line,
content=content
}
end
end
end
overlay.Redraw()
if #results>10 then
cancel_job(job)
end
end
function on_stderr()
cancel_job(job)
end
job = shell.JobSpawn(
"grep", {"-rn", query, "."},
on_stdout, on_stderr, nil
)
last_job = job
end
-- Immediate-mode event handling
local overlay_handle = nil
local event_count = 0
local events = {}
local tracked_events = {}
function track_event(name, block)
-- Registers a global handler for an event
-- If "no_block" is passed as the second argument,
-- the event will not be prevented.
local full_name = "pre" .. name
if block=="no_block" then
full_name = "on"..name
end
if not tracked_events[full_name] then
tracked_events[full_name] = true
if block~="no_block" then
_G[full_name] = function(...)
if overlay_handle then
events[name] = {...}
event_count = event_count + 1
return false
end
end
else
_G[full_name] = function(...)
if overlay_handle then
events[name] = {...}
event_count = event_count + 1
end
end
end
end
end
function untrack_events()
-- Removes all global event handlers
for e, _ in pairs(tracked_events) do
_G[e] = nil
end
tracked_events = {}
end
function reset_events()
-- Resets tracked events between redraws
events = {}
event_count = 0
end
function dispatch(event_name, ...)
-- Lets us dispatch our own custom events
local pre_event = _G["pre"..event_name]
local on_event = _G["on"..event_name]
if pre_event then
local res = pre_event(...)
if not res then
return false
end
end
if on_event then
on_event(...)
end
end
function event(event_name, block)
-- Returns event arguments if the event has occurred, or nil otherwise.
track_event(event_name, block)
return events[event_name]
end
function close_finder()
-- Closes the overlay and untracks all events.
untrack_events()
overlay.DestroyOverlay(overlay_handle)
overlay_handle = nil
end
local mode = "quicksearch"
local query = ""
local current_result = 1
function rerun_query(query)
if mode == "quicksearch" then
grep(query)
elseif mode == "quickopen" then
find(query)
end
end
function preRune(_, r)
-- Note: We handle rune events like this because we could
-- get more than one rune event per render (for example,
-- if the redraw is slow for whatever reason and the
-- user is typing fast).
if overlay_handle then
query = query .. r
current_result = 1
rerun_query(query)
return false
end
end
function draw_finder()
local bp = micro.CurPane()
if event("Escape") then
close_finder()
return
end
if event("Backspace") then
query = query:sub(1, -2)
current_result = 1
rerun_query(query)
end
if event("InsertNewline") then
local result = results[current_result]
if result then
local buf_path = pathlib.Clean(bp.Buf.Path)
result.path = pathlib.Clean(result.path)
if config.GetGlobalOption("quickmenu.newtab") and buf_path~=result.path then
bp:NewTabCmd{result.path}
bp = micro.CurPane()
else
bp:OpenCmd{result.path}
end
if result.type == "line" then
bp:GotoLoc{X=0, Y=tonumber(result.line)-1}
end
end
close_finder()
return
end
-- TODO: Make the Left and Right arrow keys work too!
if event("CursorUp") then current_result = current_result-1 end
if event("CursorDown") then current_result = current_result+1 end
local result_count = clamp(#results, 1, 10)
current_result = wrap_int(current_result, 1, result_count+1)
local r = overlay.BufPaneScreenRect(bp)
local x = math.floor(r.X + r.W*0.15)
local w = math.ceil(r.W*0.7)
local y = r.Y + 2
-- Draw the input box
local input_style = overlay.GetColor("line-number")
overlay.DrawRect(x-1, y, w+2, 1, input_style)
overlay.DrawText(query, x, y, w, 1, input_style)
if query=="" then
if mode=="quicksearch" then
overlay.DrawText("Search code...", x, y, w, 1, input_style:Dim(true))
elseif mode=="quickopen" then
overlay.DrawText("Find file...", x, y, w, 1, input_style:Dim(true))
end
end
-- Draw the results
local normal = overlay.GetColor("line-number")
local highlight = overlay.GetColor("selection")
for i, result in pairs(results) do
local style = normal
if i==current_result then
style = highlight
end
if result.type=="line" then
y = y+1
overlay.DrawText(result.path..":"..result.line, x-1, y, w+2, 1, style:Bold(true))
y = y+1
overlay.DrawRect(x-1, y, w+2, 1, style)
overlay.DrawText(" " .. result.content, x, y, w, 1, style)
elseif result.type=="file" then
y = y+1
overlay.DrawText(result.path, x, y, w, 1, style:Bold(true))
end
if i>10 then break end
end
reset_events()
end
function open_finder(q)
if overlay_handle then return end
reset_events()
if q then
query = q
else
query = ""
end
results = {}
current_result = 1
overlay_handle = overlay.CreateOverlay(draw_finder)
end
function open_quickopen(_, args)
mode = "quickopen"
open_finder(array_get(args, 1))
end
function open_quicksearch(_, args)
mode = "quicksearch"
open_finder(array_get(args, 1))
end
function init()
config.AddRuntimeFile("quickmenu", config.RTHelp, "help/quickmenu.md")
config.RegisterGlobalOption("quickmenu", "newtab", true)
config.MakeCommand("quicksearch", open_quicksearch, config.NoComplete)
config.MakeCommand("quickopen", open_quickopen, config.NoComplete)
config.TryBindKey("Alt-f", "command:quicksearch", false)
config.TryBindKey("Alt-o", "command:quickopen", false)
end
function deinit()
close_finder()
untrack_events()
end