This commit is contained in:
Tomas Mirchev 2025-10-26 08:32:01 +02:00
parent 36817d1e18
commit 095c7b92a2
2 changed files with 92 additions and 92 deletions

View File

@ -1,6 +1,6 @@
require('plugins.filetree')
require('plugins.finder').setup({
exclude_patterns = { 'node_modules', 'dist', 'build', '.git', '.cache', '.turbo' },
exclude_patterns = { 'node_modules', 'dist', 'build', '.git', '.cache', '.turbo', '*-lock.json' },
use_disk_cache = true, -- optional
})

View File

@ -10,6 +10,8 @@ M.config = {
grep_cmd = 'rg', -- ripgrep binary
page_size = 60, -- soft cap; real viewport height is measured
debounce_ms = 80,
instant_items = 1500, -- <= this count + cached => 0 ms debounce
fast_items = 4000, -- <= this count => debounce_ms/2
cache_ttl_sec = 20,
max_items = 5000, -- safety cap for massive outputs
debug = false,
@ -53,6 +55,9 @@ local S = {
select = 1, -- absolute index in filtered (1-based)
scroll = 0, -- top index (0-based)
user_moved = false, -- has user navigated during this session?
-- meta
files_cached = false,
}
---------------------------------------------------------------------
@ -81,34 +86,28 @@ local function cmd_exists(bin)
return vim.fn.executable(bin) == 1
end
-- single reusable timer to avoid churn; caller controls ms
local function debounce(fn, ms)
if S.timer then
S.timer:stop()
S.timer:close()
S.timer = nil
end
if not S.timer then
S.timer = vim.loop.new_timer()
S.timer:start(ms, 0, function()
if S.timer then
S.timer:stop()
S.timer:close()
S.timer = nil
end
S.timer:stop()
S.timer:start(ms, 0, function()
S.timer:stop()
vim.schedule(fn)
end)
end
local function read_gitignore(root)
local p = root .. '/.gitignore'
local lines = {}
local f = io.open(p, 'r')
if not f then
return {}
end
local lines = {}
for line in f:lines() do
local l = vim.trim(line)
if #l > 0 and not l:match('^#') and not l:match('^!') then
-- normalize directory suffix to plain token for our excludes
l = l:gsub('/+$', '')
table.insert(lines, l)
end
@ -142,6 +141,7 @@ local function load_cache(root)
return nil
end
if now_sec() - (data.timestamp or 0) > M.config.cache_ttl_sec then
pcall(os.remove, file) -- proactively clear stale
return nil
end
return data.files
@ -173,19 +173,24 @@ local function project_root()
return vim.fn.getcwd(0, 0)
end
-- Robust relative-to-root
local function normalize_rel(root, p)
if not p or p == '' then
return ''
end
-- strip leading ./ and leading root if any
p = p:gsub('^%./', '')
local abs = vim.fs.normalize(vim.fn.fnamemodify(p, ':p'))
local root_norm = vim.fs.normalize(root)
local abs = vim.fs.normalize(p)
if abs:find(root_norm, 1, true) == 1 then
local rel = abs:sub(#root_norm + 2)
return rel ~= '' and rel or abs
if abs == root_norm then
return ''
end
return p
if abs:find(root_norm, 1, true) == 1 then
local ch = abs:sub(#root_norm + 1, #root_norm + 1)
if ch == '/' or ch == '\\' then
return abs:sub(#root_norm + 2)
end
end
-- strip leading ./ as last resort
return (p:gsub('^%./', ''))
end
local function to_abs_path(root, rel)
@ -195,6 +200,11 @@ local function to_abs_path(root, rel)
return vim.fs.normalize(root .. '/' .. rel)
end
local function to_display_path(abs)
-- relative to current working dir if possible, else absolute
return vim.fn.fnamemodify(abs, ':.')
end
local function page_rows()
if S.win_res and vim.api.nvim_win_is_valid(S.win_res) then
return math.max(1, vim.api.nvim_win_get_height(S.win_res))
@ -202,6 +212,18 @@ local function page_rows()
return math.max(1, math.min(M.config.page_size, vim.o.lines))
end
local function effective_debounce_ms()
if S.mode == 'files' then
local n = #S.items
if S.files_cached and n > 0 and n <= (M.config.instant_items or 1500) then
return 0
elseif n > 0 and n <= (M.config.fast_items or 4000) then
return math.max(0, math.floor((M.config.debounce_ms or 80) / 2))
end
end
return M.config.debounce_ms or 80
end
---------------------------------------------------------------------
-- Render helpers
---------------------------------------------------------------------
@ -237,7 +259,7 @@ local function render()
local end_idx = math.min(start_idx + page_rows() - 1, total)
for i = start_idx, end_idx do
local line = S.filtered[i]
view[#view + 1] = type(line) == 'string' and line or tostring(line or '')
view[#view + 1] = tostring(line or '')
end
end
@ -283,7 +305,6 @@ end
-- Querying / Filtering
---------------------------------------------------------------------
local function compute_positions_files(items, q)
-- matchfuzzypos returns {items, positions}
local ok, res = pcall(vim.fn.matchfuzzypos, items, q)
if not ok or type(res) ~= 'table' then
return {}, {}
@ -293,14 +314,26 @@ local function compute_positions_files(items, q)
local filtered, positions = {}, {}
for i, v in ipairs(out_items) do
filtered[i] = (type(v) == 'string') and v or tostring(v or '')
filtered[i] = tostring(v or '')
local cols = pos[i] or {}
table.sort(cols)
local spans = {}
local start_col, last_col = nil, nil
for _, c in ipairs(cols) do
local start0 = (c > 0) and (c - 1) or 0 -- to 0-based
spans[#spans + 1] = { start0, start0 + 1 }
local c0 = c
if not start_col then
start_col, last_col = c0, c0
elseif c0 == last_col + 1 then
last_col = c0
else
table.insert(spans, { start_col, last_col + 1 }) -- end exclusive
start_col, last_col = c0, c0
end
positions[i] = spans
end
if start_col then
table.insert(spans, { start_col, last_col + 1 })
end
positions[i] = (#spans > 0) and spans or nil
end
return filtered, positions
end
@ -312,15 +345,15 @@ local function compute_positions_grep(lines, q)
local qlow = q:lower()
local positions = {}
for i, line in ipairs(lines) do
local sidx = 1
local llow = tostring(line or ''):lower()
local spans = {}
local llow = (type(line) == 'string' and line or tostring(line or '')):lower()
local sidx = 1
while true do
local s, e = string.find(llow, qlow, sidx, true)
local s, e = llow:find(qlow, sidx, true)
if not s then
break
end
spans[#spans + 1] = { s - 1, e } -- 0-based start, exclusive end
table.insert(spans, { s - 1, e })
sidx = e + 1
if #spans > 64 then
break
@ -346,10 +379,8 @@ local function set_items(list)
-- sanitize to strings
for i, v in ipairs(list) do
if type(v) ~= 'string' then
list[i] = tostring(v or '')
end
end
S.items = list
S.filtered = list
@ -379,16 +410,7 @@ local function set_query(q)
end
end
-- sanitize filtered list
for i, v in ipairs(S.filtered) do
if type(v) ~= 'string' then
S.filtered[i] = tostring(v or '')
end
end
-- selection policy:
-- if user hasn't moved, always reset to first on query changes;
-- if user moved, preserve previous pick when present, else first.
if not S.user_moved then
S.select = 1
else
@ -432,12 +454,12 @@ local function move_up()
render()
end
-- Correct parser for rg --vimgrep (file:line:col:match)
local function parse_vimgrep(line)
-- Greedy file capture up to last ":<lnum>:"
if type(line) ~= 'string' then
return nil
end
local file, lnum = line:match('^(.*):(%d+):')
local file, lnum = line:match('^(.-):(%d+):%d+:')
if not file then
return nil
end
@ -450,9 +472,10 @@ local function accept_selection_files()
return
end
local abs = to_abs_path(S.root, pick)
local path = to_display_path(abs)
M.close()
vim.schedule(function()
vim.cmd.edit(vim.fn.fnameescape(abs))
vim.cmd.edit(vim.fn.fnameescape(path))
end)
end
@ -466,12 +489,12 @@ local function accept_selection_grep()
return
end
local abs = to_abs_path(S.root, file)
local path = to_display_path(abs)
local ln = tonumber(lnum) or 1
M.close()
vim.schedule(function()
vim.cmd.edit(vim.fn.fnameescape(abs))
if pcall(vim.api.nvim_win_set_cursor, 0, { ln, 0 }) then
end
vim.cmd.edit(vim.fn.fnameescape(path))
pcall(vim.api.nvim_win_set_cursor, 0, { ln, 0 })
end)
end
@ -493,9 +516,11 @@ local function collect_files_async(cb)
local disk = load_cache(root)
if disk and type(disk) == 'table' then
S.files_cached = true
cb(disk)
return
end
S.files_cached = false
local file_cmd = M.config.file_cmd
if not file_cmd then
@ -517,7 +542,6 @@ local function collect_files_async(cb)
if file_cmd then
local args = { file_cmd, '--type', 'f', '--hidden', '--color', 'never' }
-- best-effort: avoid leading ./ in output
if file_cmd == 'fd' or file_cmd == 'fdfind' then
table.insert(args, '--strip-cwd-prefix')
end
@ -531,7 +555,7 @@ local function collect_files_async(cb)
return
end
local list = {}
if obj.code == 0 and obj.stdout then
if obj.stdout then
local raw = vim.split(obj.stdout, '\n', { trimempty = true })
for _, p in ipairs(raw) do
list[#list + 1] = normalize_rel(root, p)
@ -565,7 +589,7 @@ local function collect_files_async(cb)
return
end
local list = {}
if o2.code == 0 and o2.stdout then
if o2.stdout then
for p in o2.stdout:gmatch('([^%z]+)') do
list[#list + 1] = normalize_rel(root, p)
end
@ -588,33 +612,8 @@ local function collect_files_async(cb)
return
end
-- Last resort: blocking glob with simple exclusion checks
local list = vim.fn.globpath(root, '**/*', false, true)
list = vim.tbl_filter(function(p)
if vim.fn.isdirectory(p) == 1 then
return false
end
for _, ex in ipairs(excludes) do
if p:find('/' .. ex .. '/', 1, true) or p:find('/' .. ex .. '$', 1, true) then
return false
end
end
return true
end, list)
local rel = {}
for i = 1, #list do
rel[i] = normalize_rel(root, list[i])
end
if #rel > M.config.max_items then
local tmp = {}
for i = 1, M.config.max_items do
tmp[i] = rel[i]
end
rel = tmp
end
save_cache(root, rel)
cb(rel)
-- Last resort omitted: blocking glob removed to keep async-only behavior
cb({})
end
local function grep_async(query, cb)
@ -645,17 +644,13 @@ local function grep_async(query, cb)
query,
}
-- Apply excludes as negative globs
local excludes = {}
for _, p in ipairs(M.config.exclude_patterns or {}) do
table.insert(excludes, p)
table.insert(args, 2, '--glob')
table.insert(args, 3, '!' .. p)
end
local gi = read_gitignore(root)
for _, p in ipairs(gi) do
table.insert(excludes, p)
end
for _, ex in ipairs(excludes) do
table.insert(args, 2, '--glob') -- insert before '--'
table.insert(args, 3, '!' .. ex)
for _, p in ipairs(read_gitignore(root)) do
table.insert(args, 2, '--glob')
table.insert(args, 3, '!' .. p)
end
cancel_job(S.job_rg)
@ -664,7 +659,8 @@ local function grep_async(query, cb)
return
end
local list = {}
if obj.code == 0 and obj.stdout then
-- Accept output even if exit code != 0 (no matches or partial errors)
if obj.stdout and #obj.stdout > 0 then
list = vim.split(obj.stdout, '\n', { trimempty = true })
end
if #list > M.config.max_items then
@ -837,6 +833,7 @@ local function attach_handlers()
group = S.aug,
buffer = S.buf_inp,
callback = function()
local delay = effective_debounce_ms()
debounce(function()
if not (S.active and S.buf_inp and vim.api.nvim_buf_is_valid(S.buf_inp)) then
return
@ -856,7 +853,7 @@ local function attach_handlers()
else
set_query(q)
end
end, M.config.debounce_ms)
end, delay)
end,
})
end
@ -882,7 +879,7 @@ function M.files()
if not S.active then
return
end
set_items(list) -- render initial list (relative paths)
set_items(list) -- render initial list (relative to root)
end)
end
@ -907,8 +904,10 @@ function M.close()
end
-- stop timers and jobs first
if S.timer then
pcall(function()
S.timer:stop()
S.timer:close()
end)
S.timer = nil
end
cancel_job(S.job_rg)
@ -922,6 +921,7 @@ function M.close()
S.items, S.filtered, S.positions = {}, {}, {}
S.query, S.select, S.scroll = '', 1, 0
S.user_moved = false
S.files_cached = false
L('session closed')
end