Nearly there!!

This commit is contained in:
2026-10-07 08:21:36 +02:00
parent aeb9df3e0e
commit ee1bb98515
3 changed files with 218 additions and 168 deletions
+139 -33
View File
@@ -1,12 +1,30 @@
--pickers: fuzzy file finder, live ripgrep, yazi file manager, gitu.
-- pickers: fuzzy file finder, live ripgrep, build errors, yazi file manager, gitu.
local M = {}
-- height of the drop-down fzf panel at the top ('100%' = full screen)
M.height = '100%'
-- ---------------------------------------------------------------------------
-- helper funcs
-- shared constants
-- ---------------------------------------------------------------------------
local BAT = 'bat --theme=custom_zen --color=always --style=numbers'
local PREVIEW_FILE = BAT .. ' --line-range=:500 {}' -- {} = path
local PREVIEW_AT = BAT .. ' --highlight-line {2} {1}' -- {1}=path {2}=line
local KEYS_NAV = 'ctrl-n:down,ctrl-p:up'
local KEYS_PREVIEW = 'ctrl-/:toggle-preview,shift-up:preview-up,shift-down:preview-down'
-- an Odin error line (-error-pos-style:unix): path.odin:line:col: message
local ERROR_PATTERN = '^(.-%.odin):(%d+):(%d+):'
-- ---------------------------------------------------------------------------
-- helpers
-- ---------------------------------------------------------------------------
-- single-quote a string for the shell
local function q(s)
return "'" .. s:gsub("'", "'\\''") .. "'"
end
-- write a raw escape sequence straight to the terminal
local function tty_write(seq)
@@ -23,6 +41,17 @@ local function restore_screen()
vis:redraw()
end
-- fzf command line: common flags + a list of extra options
local function fzf(prompt, opts)
return 'fzf --height=' .. M.height .. ' --layout=reverse --border'
.. ' --prompt ' .. q(prompt) .. ' ' .. table.concat(opts, ' ')
end
-- "--flag 'value'"
local function opt(flag, value)
return flag .. ' ' .. q(value)
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
@@ -66,18 +95,14 @@ end
-- 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 cmd = 'fd --type f --hidden --exclude .git | ' .. fzf('files> ', {
'--print-query --expect=alt-enter,ctrl-v', -- output: query, key, choice
opt('--header', 'enter: open · alt-enter: new file · ctrl-v: vsplit'),
opt('--preview', PREVIEW_FILE),
opt('--preview-window', 'down:75%:border-top'),
opt('--bind', KEYS_NAV),
opt('--bind', KEYS_PREVIEW),
})
local out, code = run_fzf(cmd)
if not out or code == 130 then return end -- failed, or Esc / Ctrl-C
@@ -100,28 +125,29 @@ end
-- ---------------------------------------------------------------------------
-- grep: live ripgrep search across the project, jump to the match
-- M.grep() start empty, results appear as you type
-- M.grep_selection() visual mode: start with the selected text
-- ---------------------------------------------------------------------------
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}'
local function grep_with(query)
-- 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 opts = {
'--ansi --disabled', -- let rg do the matching, not fzf
'--delimiter :', -- fields: path:line:col:text
opt('--bind', 'change:reload:' .. reload),
opt('--preview', PREVIEW_AT),
opt('--preview-window', 'down:90%:border-top:+{2}/2'),
opt('--bind', KEYS_PREVIEW),
}
if query then
table.insert(opts, opt('--query', query))
table.insert(opts, opt('--bind', 'start:reload:' .. reload))
end
local out = run_fzf(cmd)
-- empty stdin: results only appear once there is a query
local out = run_fzf(': | ' .. fzf('rg> ', opts))
local choice = out and out[1]
if not choice or choice == '' then return end
@@ -132,6 +158,84 @@ function M.grep()
vis.win.selection:to(tonumber(line), tonumber(col))
end
function M.grep()
grep_with(nil)
end
function M.grep_selection()
local win = vis.win
local text
if win and win.file and win.selection.range then
text = win.file:content(win.selection.range)
end
-- leave visual mode before fzf takes over the screen
vis.mode = vis.modes.NORMAL
-- first line of the selection only; nothing usable -> plain grep
text = text and text:match('^[^\n]*')
if not text or text:match('^%s*$') then
grep_with(nil)
else
grep_with(text)
end
end
-- ---------------------------------------------------------------------------
-- errors: browse the errors from the last build (the log build.lua writes)
-- enter jump to the error
-- ---------------------------------------------------------------------------
function M.errors()
local f = io.open(require('build').log, 'r')
if not f then
vis:info('No build log yet — run a build first')
return
end
-- keep only the path.odin:line:col: lines; the log comes from a pty, so
-- strip colour escapes and carriage returns first
local errors = {}
for line in f:lines() do
local clean = line:gsub('\27%[[%d;?]*%a', ''):gsub('\r', '')
if clean:match(ERROR_PATTERN) then table.insert(errors, clean) end
end
f:close()
if #errors == 0 then
vis:info('No errors in the last build')
return
end
local tmp = os.tmpname()
f = io.open(tmp, 'w')
if not f then
vis:info('errors: cannot write temp file')
return
end
f:write(table.concat(errors, '\n'), '\n')
f:close()
local cmd = fzf('errors> ', {
'--no-sort', -- keep build order: the first error is the one to fix first
'--delimiter :',
opt('--preview', PREVIEW_AT),
opt('--preview-window', 'down:75%:border-top:+{2}/2'),
opt('--bind', KEYS_NAV),
opt('--bind', KEYS_PREVIEW),
}) .. ' < ' .. q(tmp)
local out = run_fzf(cmd)
os.remove(tmp)
local choice = out and out[1]
if not choice or choice == '' then return end
local path, line, col = choice:match(ERROR_PATTERN)
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
-- ---------------------------------------------------------------------------
@@ -153,6 +257,7 @@ end
-- git: run gitu, then hand the screen back to vis
-- ---------------------------------------------------------------------------
function M.git()
tty_write('\27[?1049l')
os.execute('gitu </dev/tty >/dev/tty 2>&1')
restore_screen()
@@ -164,16 +269,17 @@ function M.git()
end
end
-- ---------------------------------------------------------------------------
-- events
-- ---------------------------------------------------------------------------
-- 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("'", "'\\''")) .. "'")
os.execute('mkdir -p ' .. q(dir))
end
return true
end)
return M
return M