diff --git a/.gitignore b/.gitignore index 08aef44..4a1e809 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,5 @@ cli/.*.bun-build cli/node_modules/ .DS_Store *.log + +node_modules/ diff --git a/lua/tau/context_picker.lua b/lua/tau/context_picker.lua index c6d78d4..c049cd5 100644 --- a/lua/tau/context_picker.lua +++ b/lua/tau/context_picker.lua @@ -9,6 +9,30 @@ local _open = false vim.api.nvim_set_hl(0, "TauContextCursorLine", { link = "CursorLine" }) +local function add_candidate(candidates, seen, path) + local abs = vim.fn.fnamemodify(path, ":p") + if seen[abs] or vim.fn.isdirectory(abs) ~= 0 or vim.fn.filereadable(abs) == 0 then return end + + seen[abs] = true + candidates[#candidates + 1] = { abs = abs, rel = vim.fn.fnamemodify(abs, ":~:.") } +end + +--- List cwd files, respecting git ignore rules when cwd is inside a Git repo. +--- @return string[] +local function list_cwd_files() + local cwd = vim.loop.cwd() or vim.fn.getcwd() + + if vim.fn.executable("git") == 1 then + local files = vim.fn.systemlist({ "git", "-C", cwd, "ls-files", "--cached", "--others", "--exclude-standard", "--", "." }) + if vim.v.shell_error == 0 then + return files + end + end + + -- Fallback for non-Git directories. This does not understand .gitignore. + return vim.fn.glob("**/*", false, true) +end + --- Collect candidate file paths, deduplicated and sorted. --- @return {abs: string, rel: string}[] local function get_candidates() @@ -20,23 +44,16 @@ local function get_candidates() if vim.bo[buf].buflisted and vim.bo[buf].buftype == "" then local name = vim.api.nvim_buf_get_name(buf) if name ~= "" then - local abs = vim.fn.fnamemodify(name, ":p") - if not seen[abs] then - seen[abs] = true - candidates[#candidates + 1] = { abs = abs, rel = vim.fn.fnamemodify(abs, ":~:.") } - end + add_candidate(candidates, seen, name) end end end - -- All files in the working directory - for _, rel_path in ipairs(vim.fn.glob("**/*", false, true)) do - if vim.fn.isdirectory(rel_path) == 0 then - local abs = vim.fn.fnamemodify(rel_path, ":p") - if not seen[abs] then - seen[abs] = true - candidates[#candidates + 1] = { abs = abs, rel = vim.fn.fnamemodify(abs, ":~:.") } - end + -- Files in cwd. In Git repos this uses `git ls-files --exclude-standard`, + -- so .gitignore/.git/info/exclude/global excludes are honored. + for _, rel_path in ipairs(list_cwd_files()) do + if rel_path ~= "" then + add_candidate(candidates, seen, rel_path) end end diff --git a/tests/context_picker_spec.lua b/tests/context_picker_spec.lua index e2cec05..b60942a 100644 --- a/tests/context_picker_spec.lua +++ b/tests/context_picker_spec.lua @@ -37,6 +37,14 @@ local function find_map(buf, lhs) return nil end +local function assert_no_line_contains(lines, needle, message) + for _, line in ipairs(lines) do + if line:find(needle, 1, true) then + fail((message or "unexpected line") .. ": " .. line) + end + end +end + local start_dir = vim.loop.cwd() local temp_dir = vim.fn.tempname() vim.fn.mkdir(temp_dir, "p") @@ -83,6 +91,30 @@ local ok, err = xpcall(function() assert_truthy(not context_files.contains_abs(current_file), "expected locked current file not to be toggled into context") assert_equal(vim.api.nvim_buf_get_lines(buf, 1, 2, false)[1], " ◎ b.txt", "expected locked current-file row to remain unchanged") + + local q_map = find_map(buf, "q") + assert_truthy(q_map, "expected buffer-local q mapping") + q_map.callback() + + local git_dir = vim.fn.tempname() + vim.fn.mkdir(git_dir .. "/node_modules/pkg", "p") + vim.fn.writefile({ "node_modules/" }, git_dir .. "/.gitignore") + vim.fn.writefile({ "visible" }, git_dir .. "/visible.txt") + vim.fn.writefile({ "ignored" }, git_dir .. "/node_modules/pkg/index.js") + + vim.cmd("cd " .. vim.fn.fnameescape(git_dir)) + vim.fn.system({ "git", "init", "-q" }) + assert_equal(vim.v.shell_error, 0, "expected git init to succeed") + + require("tau.context_picker").open() + local git_buf = vim.api.nvim_get_current_buf() + local lines = vim.api.nvim_buf_get_lines(git_buf, 0, -1, false) + + assert_truthy(vim.tbl_contains(lines, " .gitignore"), "expected .gitignore to be listed") + assert_truthy(vim.tbl_contains(lines, " visible.txt"), "expected visible untracked file to be listed") + assert_no_line_contains(lines, "node_modules", "expected .gitignore'd node_modules to be excluded") + + vim.fn.delete(git_dir, "rf") end, debug.traceback) vim.cmd("cd " .. vim.fn.fnameescape(start_dir))