Vis Editor updates.

This commit is contained in:
2026-10-06 09:14:01 +02:00
parent bc408c412b
commit aeb9df3e0e
7 changed files with 382 additions and 286 deletions
+179
View File
@@ -0,0 +1,179 @@
--pickers: fuzzy file finder, live ripgrep, yazi file manager, gitu.
local M = {}
-- height of the drop-down fzf panel at the top ('100%' = full screen)
M.height = '100%'
-- ---------------------------------------------------------------------------
-- helper funcs
-- ---------------------------------------------------------------------------
-- write a raw escape sequence straight to the terminal
local function tty_write(seq)
local tty = io.open('/dev/tty', 'w')
if tty then
tty:write(seq)
tty:close()
end
end
-- hand the terminal back to vis: re-enter the alt screen and repaint
local function restore_screen()
tty_write('\27[?1049h')
vis:redraw()
end
-- run an fzf pipeline; returns its output lines and exit code
local function run_fzf(cmd)
-- --height mode draws from the cursor down, so start at the top-left
tty_write('\27[H')
local handle = io.popen(cmd)
if not handle then
vis:info('fzf: failed to start')
return nil
end
local lines = {}
for line in handle:lines() do table.insert(lines, line) end
local _, _, code = handle:close()
restore_screen()
return lines, code
end
-- open a path in the current window, unless it's already the current file.
-- returns false if the buffer has unsaved changes and nothing was opened
local function open_path(path)
local win = vis.win
local current = win and win.file and win.file.path
if current and (current == path or current:sub(-#path - 1) == '/' .. path) then
return true
end
if win and win.file and win.file.modified then
vis:info('Buffer has unsaved changes — :w first (or use :e! manually)')
return false
end
vis:command(string.format("e '%s'", path))
return true
end
-- ---------------------------------------------------------------------------
-- files: fuzzy-find a file, or create one from the query
-- enter open in the current window
-- ctrl-v open in a vertical split
-- alt-enter create a file named after the query
-- ctrl-n/p move down/up
-- ---------------------------------------------------------------------------
function M.files()
local preview = 'bat --theme=custom_zen --color=always --style=numbers --line-range=:500 {}'
local cmd = 'fd --type f --hidden --exclude .git | fzf'
.. ' --height=' .. M.height .. ' --layout=reverse --border'
.. " --prompt 'files> '"
.. ' --print-query --expect=alt-enter,ctrl-v' -- output: query, key, choice
.. " --header 'enter: open · alt-enter: new file · ctrl-v: vsplit'"
.. " --preview '" .. preview .. "'"
.. " --preview-window 'down:75%:border-top'"
.. " --bind 'ctrl-n:down,ctrl-p:up'"
.. " --bind 'ctrl-/:toggle-preview'"
.. " --bind 'shift-up:preview-up,shift-down:preview-down'"
local out, code = run_fzf(cmd)
if not out or code == 130 then return end -- failed, or Esc / Ctrl-C
local query, key, choice = out[1], out[2], out[3]
local is_new = key == 'alt-enter' or not choice or choice == ''
local path = is_new and query or choice
if not path or path == '' then return end
if key == 'ctrl-v' then
vis:command(string.format("vsplit '%s'", path))
elseif not open_path(path) then
return
end
if is_new then
vis:info('New file: ' .. path .. ' (written to disk on :w)')
end
end
-- ---------------------------------------------------------------------------
-- grep: live ripgrep search across the project, jump to the match
-- ---------------------------------------------------------------------------
function M.grep()
-- {1} = path, {2} = line number (fields split on ':')
local preview = 'bat --theme=custom_zen --color=always --style=numbers --highlight-line {2} {1}'
-- don't run rg on an empty query (it would match every line)
local reload = '[ -n {q} ] && rg --column --line-number --no-heading'
.. ' --color=always --smart-case --hidden --glob "!.git" -- {q} || true'
-- empty stdin: results only appear once you start typing
local cmd = ': | fzf'
.. ' --height=' .. M.height .. ' --layout=reverse --border'
.. " --prompt 'rg> '"
.. ' --ansi --disabled' -- let rg do the matching, not fzf
.. ' --delimiter :'
.. " --bind 'change:reload:" .. reload .. "'"
.. " --preview '" .. preview .. "'"
.. " --preview-window 'down:90%:border-top:+{2}/2'"
.. " --bind 'ctrl-/:toggle-preview'"
.. " --bind 'shift-up:preview-up,shift-down:preview-down'"
local out = run_fzf(cmd)
local choice = out and out[1]
if not choice or choice == '' then return end
choice = choice:gsub('\27%[[%d;]*m', '') -- strip colour codes
local path, line, col = choice:match('^(.-):(%d+):(%d+):')
if not path or not open_path(path) then return end
vis.win.selection:to(tonumber(line), tonumber(col))
end
-- ---------------------------------------------------------------------------
-- fm: browse/create/rename/delete in yazi, then open the file you pick
-- ---------------------------------------------------------------------------
function M.fm()
local tmp = os.tmpname()
os.execute(string.format("yazi --chooser-file='%s' </dev/tty >/dev/tty 2>&1", tmp))
restore_screen()
local f = io.open(tmp, 'r')
local path = f and f:read('*l')
if f then f:close() end
os.remove(tmp)
if path and path ~= '' then open_path(path) end
end
-- ---------------------------------------------------------------------------
-- git: run gitu, then hand the screen back to vis
-- ---------------------------------------------------------------------------
function M.git()
os.execute('gitu </dev/tty >/dev/tty 2>&1')
restore_screen()
-- gitu may have changed files on disk (checkout, discard, stash, ...):
-- reload the current buffer if there's nothing unsaved to lose
local win = vis.win
if win and win.file and win.file.path and not win.file.modified then
vis:command('e!')
end
end
-- ---------------------------------------------------------------------------
-- create missing parent directories on save, so a new file like
-- src/util/new.odin can be written even if src/util/ doesn't exist yet
-- ---------------------------------------------------------------------------
vis.events.subscribe(vis.events.FILE_SAVE_PRE, function(file, path) -- luacheck: no unused args
local dir = path and path:match('^(.*)/[^/]+$')
if dir and dir ~= '' then
os.execute("mkdir -p '" .. (dir:gsub("'", "'\\''")) .. "'")
end
return true
end)
return M