231 lines
7.8 KiB
Lua
231 lines
7.8 KiB
Lua
-- ~/.config/vis/fzf.lua
|
|
-- Pickers: fuzzy file finder, live ripgrep, terminal file manager.
|
|
-- Loaded from visrc.lua. Each function is self-contained on purpose.
|
|
|
|
local M = {}
|
|
|
|
-- height of the drop-down fzf panel at the top ('100%' = full screen)
|
|
M.height = '50%'
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- files: fuzzy-find a file, or create one from the query
|
|
-- enter open the highlighted file (or create the query if nothing matches)
|
|
-- ctrl-n new file named after the query, e.g. src/util/new.odin
|
|
-- ---------------------------------------------------------------------------
|
|
function M.files()
|
|
-- file list: fd, fdfind (Debian/Ubuntu), or plain find
|
|
local list = "find . -type f -not -path '*/.git/*' | sed 's|^\\./||'"
|
|
for _, name in ipairs({ 'fd', 'fdfind' }) do
|
|
local ok = os.execute('command -v ' .. name .. ' >/dev/null 2>&1')
|
|
if ok == true or ok == 0 then
|
|
list = name .. ' --type f --hidden --exclude .git'
|
|
break
|
|
end
|
|
end
|
|
|
|
-- preview: bat, batcat (Debian/Ubuntu), or head
|
|
local preview = 'head -n 500 {}'
|
|
for _, name in ipairs({ 'bat', 'batcat' }) do
|
|
local ok = os.execute('command -v ' .. name .. ' >/dev/null 2>&1')
|
|
if ok == true or ok == 0 then
|
|
preview = name .. ' --color=always --style=numbers --line-range=:500 {}'
|
|
break
|
|
end
|
|
end
|
|
|
|
local cmd = list .. ' | fzf'
|
|
.. ' --height=' .. M.height .. ' --layout=reverse --border'
|
|
.. " --prompt 'files> '"
|
|
.. ' --print-query --expect=ctrl-n' -- line 1: query, line 2: key
|
|
.. " --header 'enter: open · ctrl-n: new file from query'"
|
|
.. " --preview '" .. preview .. "'"
|
|
.. " --preview-window 'right:60%:border-left'"
|
|
.. " --bind 'ctrl-/:toggle-preview'"
|
|
.. " --bind 'shift-up:preview-up,shift-down:preview-down'"
|
|
|
|
-- fzf's --height mode draws from the cursor down: start at the top-left
|
|
local tty = io.open('/dev/tty', 'w')
|
|
if tty then tty:write('\27[H') tty:close() end
|
|
|
|
local handle = io.popen(cmd)
|
|
if not handle then
|
|
vis:info('fzf: failed to start')
|
|
return
|
|
end
|
|
local out = {}
|
|
for line in handle:lines() do table.insert(out, line) end
|
|
local _, _, code = handle:close()
|
|
|
|
-- back onto the alternate screen (so quitting vis restores the
|
|
-- terminal), then repaint vis
|
|
tty = io.open('/dev/tty', 'w')
|
|
if tty then tty:write('\27[?1049h') tty:close() end
|
|
vis:redraw()
|
|
|
|
if code == 130 then return end -- Esc / Ctrl-C
|
|
|
|
local query, key, choice = out[1], out[2], out[3]
|
|
local is_new = key == 'ctrl-n' or not choice or choice == ''
|
|
local path = is_new and query or choice
|
|
if not path or path == '' then return end
|
|
|
|
-- open in the current window, unless it's already the current file
|
|
local win = vis.win
|
|
local current = win and win.file and win.file.path
|
|
local same = current ~= nil
|
|
and (current == path or current:sub(-#path - 1) == '/' .. path)
|
|
if not same then
|
|
if win and win.file and win.file.modified then
|
|
vis:info('Buffer has unsaved changes — :w first (or use :e! manually)')
|
|
return
|
|
end
|
|
vis:command(string.format("e '%s'", path))
|
|
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()
|
|
local ok = os.execute('command -v rg >/dev/null 2>&1')
|
|
if not (ok == true or ok == 0) then
|
|
vis:info('ripgrep (rg) not found on PATH')
|
|
return
|
|
end
|
|
|
|
-- preview: bat/batcat with the match highlighted, or cat -n
|
|
-- {1} = path, {2} = line number (fields split on ':')
|
|
local preview = 'cat -n {1}'
|
|
for _, name in ipairs({ 'bat', 'batcat' }) do
|
|
local found = os.execute('command -v ' .. name .. ' >/dev/null 2>&1')
|
|
if found == true or found == 0 then
|
|
preview = name .. ' --color=always --style=numbers --highlight-line {2} {1}'
|
|
break
|
|
end
|
|
end
|
|
|
|
-- 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 'right:60%:border-left:+{2}/2'" -- centre match
|
|
.. " --bind 'ctrl-/:toggle-preview'"
|
|
.. " --bind 'shift-up:preview-up,shift-down:preview-down'"
|
|
|
|
-- fzf's --height mode draws from the cursor down: start at the top-left
|
|
local tty = io.open('/dev/tty', 'w')
|
|
if tty then tty:write('\27[H') tty:close() end
|
|
|
|
local handle = io.popen(cmd)
|
|
if not handle then
|
|
vis:info('fzf: failed to start')
|
|
return
|
|
end
|
|
local choice = handle:read('*l')
|
|
handle:close()
|
|
|
|
-- back onto the alternate screen, then repaint vis
|
|
tty = io.open('/dev/tty', 'w')
|
|
if tty then tty:write('\27[?1049h') tty:close() end
|
|
vis:redraw()
|
|
|
|
if not choice or choice == '' then return end
|
|
|
|
choice = choice:gsub('\27%[[%d;]*m', '') -- strip any colour codes
|
|
local path, line, col = choice:match('^(.-):(%d+):(%d+):')
|
|
if not path then return end
|
|
|
|
-- open in the current window, unless it's already the current file
|
|
local win = vis.win
|
|
local current = win and win.file and win.file.path
|
|
local same = current ~= nil
|
|
and (current == path or current:sub(-#path - 1) == '/' .. path)
|
|
if not same then
|
|
if win and win.file and win.file.modified then
|
|
vis:info('Buffer has unsaved changes — :w first (or use :e! manually)')
|
|
return
|
|
end
|
|
vis:command(string.format("e '%s'", path))
|
|
end
|
|
|
|
vis.win.selection:to(tonumber(line), tonumber(col))
|
|
end
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- fm: browse/create/rename/delete in a terminal file manager, then open the
|
|
-- file you pick. Uses the first one found: yazi, lf, nnn, ranger
|
|
-- ---------------------------------------------------------------------------
|
|
function M.fm()
|
|
local tmp = os.tmpname()
|
|
local managers = {
|
|
{ 'yazi', "yazi --chooser-file='%s'" },
|
|
{ 'lf', "lf -selection-path '%s'" },
|
|
{ 'nnn', "nnn -p '%s'" },
|
|
{ 'ranger', "ranger --choosefile='%s'" },
|
|
}
|
|
|
|
local cmd
|
|
for _, m in ipairs(managers) do
|
|
local ok = os.execute('command -v ' .. m[1] .. ' >/dev/null 2>&1')
|
|
if ok == true or ok == 0 then
|
|
cmd = string.format(m[2], tmp)
|
|
break
|
|
end
|
|
end
|
|
if not cmd then
|
|
os.remove(tmp)
|
|
vis:info('No file manager found (install yazi, lf, nnn or ranger)')
|
|
return
|
|
end
|
|
|
|
os.execute(cmd .. ' </dev/tty >/dev/tty 2>&1')
|
|
|
|
-- back onto the alternate screen, then repaint vis
|
|
local tty = io.open('/dev/tty', 'w')
|
|
if tty then tty:write('\27[?1049h') tty:close() end
|
|
vis:redraw()
|
|
|
|
local f = io.open(tmp, 'r')
|
|
local path = f and f:read('*l')
|
|
if f then f:close() end
|
|
os.remove(tmp)
|
|
if not path or path == '' then return end
|
|
|
|
-- open in the current window, unless it's already the current file
|
|
local win = vis.win
|
|
local current = win and win.file and win.file.path
|
|
if current ~= path then
|
|
if win and win.file and win.file.modified then
|
|
vis:info('Buffer has unsaved changes — :w first (or use :e! manually)')
|
|
return
|
|
end
|
|
vis:command(string.format("e '%s'", path))
|
|
end
|
|
end
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- create missing parent directories on save, so a new file from ctrl-n 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
|