v4
This commit is contained in:
parent
36817d1e18
commit
095c7b92a2
@ -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
|
||||
})
|
||||
|
||||
|
||||
@ -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
|
||||
if not S.timer then
|
||||
S.timer = vim.loop.new_timer()
|
||||
end
|
||||
S.timer = vim.loop.new_timer()
|
||||
S.timer:stop()
|
||||
S.timer:start(ms, 0, function()
|
||||
if S.timer then
|
||||
S.timer:stop()
|
||||
S.timer:close()
|
||||
S.timer = nil
|
||||
end
|
||||
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
|
||||
end
|
||||
positions[i] = spans
|
||||
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,9 +379,7 @@ 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
|
||||
list[i] = tostring(v or '')
|
||||
end
|
||||
|
||||
S.items = 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
|
||||
S.timer:stop()
|
||||
S.timer:close()
|
||||
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
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user