summaryrefslogtreecommitdiff
path: root/lua
diff options
context:
space:
mode:
authorThePrimeagen <the.primeagen@gmail.com>2026-02-28 14:20:33 -0700
committerGitHub <noreply@github.com>2026-02-28 14:20:33 -0700
commitd485196b0a7956e9efb9f73eccfb126b95d113b3 (patch)
tree17a463a60db88a2027950257f5ace294c01e936e /lua
parentd3e0a45d8ab67ab618a07dd40d7b15fa421ef0aa (diff)
parent86a2e9c723945b3531f899deb2b69278457c4a2e (diff)
downloada4-d485196b0a7956e9efb9f73eccfb126b95d113b3.tar.xz
a4-d485196b0a7956e9efb9f73eccfb126b95d113b3.zip
Merge pull request #132 from codegirl-007/just-gitignore-me
Exclude files/paths in .gitingore during completion
Diffstat (limited to 'lua')
-rw-r--r--lua/99/extensions/files/init.lua68
-rw-r--r--lua/99/test/files_spec.lua323
2 files changed, 388 insertions, 3 deletions
diff --git a/lua/99/extensions/files/init.lua b/lua/99/extensions/files/init.lua
index 9aa1780..b3c7c78 100644
--- a/lua/99/extensions/files/init.lua
+++ b/lua/99/extensions/files/init.lua
@@ -71,6 +71,59 @@ function M.set_project_root(root)
cache.files = {}
end
+--- @param root string
+--- @return boolean
+local function is_git_repo(root)
+ local git_dir = vim.fs.joinpath(root, ".git")
+ local stat = vim.uv.fs_stat(git_dir)
+ -- Check if .git exists (can be directory OR file for worktrees/submodules)
+ return stat ~= nil
+end
+
+--- @param root string
+--- @return _99.Files.File[]
+local function scan_with_git_sync(root)
+ local cmd = string.format(
+ "git -C %s ls-files --cached --others --exclude-standard --deduplicate",
+ vim.fn.shellescape(root)
+ )
+ local output = vim.fn.system(cmd)
+
+ if vim.v.shell_error ~= 0 then
+ return nil
+ end
+
+ if output == "" then
+ return {}
+ end
+
+ local files = {}
+ local count = 0
+
+ for line in output:gmatch("[^\n]+") do
+ if count >= config.max_files then
+ break
+ end
+
+ line = vim.trim(line)
+ if line ~= "" then
+ local name = line:match("([^/]+)$") or line
+ if
+ not matches_exclude_pattern(line) and not matches_exclude_pattern(name)
+ then
+ table.insert(files, {
+ path = line,
+ name = name,
+ absolute_path = vim.fs.joinpath(root, line),
+ })
+ count = count + 1
+ end
+ end
+ end
+
+ return files
+end
+
--- @return string
function M.get_project_root()
return cache.root
@@ -83,6 +136,19 @@ function M.discover_files()
return {}
end
+ -- Try git-based discovery first if in a git repo
+ if is_git_repo(root) then
+ local git_files = scan_with_git_sync(root)
+ if git_files then
+ table.sort(git_files, function(a, b)
+ return a.path < b.path
+ end)
+ cache.files = git_files
+ return git_files
+ end
+ end
+
+ -- Fallback to filesystem scanning
local files = {}
local count = 0
@@ -140,7 +206,7 @@ end
--- @return _99.Files.File[]
function M.get_files()
- if #cache.files == 0 and cache.root ~= "" then
+ if #cache.files == 0 then
return M.discover_files()
end
return cache.files
diff --git a/lua/99/test/files_spec.lua b/lua/99/test/files_spec.lua
index e033abe..53d9458 100644
--- a/lua/99/test/files_spec.lua
+++ b/lua/99/test/files_spec.lua
@@ -33,13 +33,11 @@ describe("files", function()
paths[f.path] = f
end
- -- known fixture files must be present
assert.is_not_nil(paths["scratch/refresh.lua"])
assert.is_not_nil(paths["scratch/test.ts"])
eq("refresh.lua", paths["scratch/refresh.lua"].name)
eq("test.ts", paths["scratch/test.ts"].name)
- -- .git must be excluded
for path, _ in pairs(paths) do
assert.is_nil(
path:match("^%.git/"),
@@ -199,3 +197,324 @@ describe("files", function()
)
end)
end)
+
+describe("files git integration", function()
+ -- Mock storage
+ local _mocks = {
+ system_output = "",
+ system_exit = 0,
+ stat_type = nil,
+ stat_exists = false,
+ orig_system = nil,
+ orig_stat = nil,
+ orig_shell_error = nil,
+ system_calls = {},
+ }
+
+ before_each(function()
+ _mocks.orig_system = vim.fn.system
+ _mocks.orig_stat = vim.uv.fs_stat
+
+ _mocks.system_output = ""
+ _mocks.system_exit = 0
+ _mocks.stat_type = nil
+ _mocks.stat_exists = false
+ _mocks.system_calls = {}
+
+ vim.fn.system = function(cmd)
+ table.insert(_mocks.system_calls, cmd)
+
+ pcall(function()
+ rawset(vim.v, "shell_error", _mocks.system_exit)
+ end)
+ return _mocks.system_output
+ end
+
+ vim.uv.fs_stat = function(_path)
+ if _mocks.stat_exists then
+ return { type = _mocks.stat_type }
+ end
+ return nil
+ end
+
+ Files.set_project_root("/test/repo")
+ end)
+
+ after_each(function()
+ vim.fn.system = _mocks.orig_system
+ vim.uv.fs_stat = _mocks.orig_stat
+
+ pcall(function()
+ rawset(vim.v, "shell_error", 0)
+ end)
+ Files.set_project_root("")
+ end)
+
+ it(
+ "detects git repo with .git directory and uses git-based discovery",
+ function()
+ _mocks.stat_exists = true
+ _mocks.stat_type = "directory"
+ _mocks.system_output = "README.md\n"
+ _mocks.system_exit = 0
+
+ local files = Files.discover_files()
+
+ assert.is_true(
+ #_mocks.system_calls > 0,
+ "git command should have been called"
+ )
+ local git_called = false
+ for _, cmd in ipairs(_mocks.system_calls) do
+ if cmd:match("git.*ls%-files") then
+ git_called = true
+ break
+ end
+ end
+ assert.is_true(git_called, "git ls-files should have been executed")
+
+ assert.is_true(#files > 0, "should return files from git")
+ eq("README.md", files[1].path)
+ end
+ )
+
+ it(
+ "detects git repo with .git file (worktree) and uses git-based discovery",
+ function()
+ _mocks.stat_exists = true
+ _mocks.stat_type = "file"
+ _mocks.system_output = "src/main.lua\ntest/file.lua\n"
+ _mocks.system_exit = 0
+
+ local files = Files.discover_files()
+
+ assert.is_true(
+ #_mocks.system_calls > 0,
+ "git command should have been called for worktree"
+ )
+ local git_called = false
+ for _, cmd in ipairs(_mocks.system_calls) do
+ if cmd:match("git.*ls%-files") then
+ git_called = true
+ break
+ end
+ end
+ assert.is_true(git_called, "should use git for worktree .git file")
+
+ assert.are.equal(2, #files)
+ eq("src/main.lua", files[1].path)
+ eq("test/file.lua", files[2].path)
+ end
+ )
+
+ it("returns files when git command succeeds", function()
+ _mocks.stat_exists = true
+ _mocks.stat_type = "directory"
+ _mocks.system_output = "README.md\nsrc/init.lua\nsrc/utils.lua\n"
+ _mocks.system_exit = 0
+
+ local files = Files.discover_files()
+
+ assert.are.equal(3, #files)
+ eq("README.md", files[1].path)
+ eq("src/init.lua", files[2].path)
+ eq("src/utils.lua", files[3].path)
+
+ for _, f in ipairs(files) do
+ assert.is_not_nil(f.path, "file should have path")
+ assert.is_not_nil(f.name, "file should have name")
+ assert.is_not_nil(f.absolute_path, "file should have absolute_path")
+ end
+ end)
+
+ it("returns empty table (not nil) for empty repo", function()
+ _mocks.stat_exists = true
+ _mocks.stat_type = "directory"
+ _mocks.system_output = ""
+ _mocks.system_exit = 0
+
+ local files = Files.discover_files()
+
+ assert.is_not_nil(files, "should return table, not nil")
+ assert.are.equal(0, #files, "should return empty table for empty repo")
+
+ for _, cmd in ipairs(_mocks.system_calls) do
+ assert.is_true(
+ cmd:match("git") ~= nil,
+ "should only call git, not fs commands"
+ )
+ end
+ end)
+
+ it("returns nil on git command failure", function()
+ _mocks.stat_exists = true
+ _mocks.stat_type = "directory"
+ _mocks.system_output = "fatal: not a git repository"
+ _mocks.system_exit = 128 -- Git failure
+
+ local orig_scandir = vim.uv.fs_scandir
+ vim.uv.fs_scandir = function(_dir)
+ return nil -- Empty directory
+ end
+
+ local files = Files.discover_files()
+
+ vim.uv.fs_scandir = orig_scandir
+
+ local git_failed = false
+ for _, cmd in ipairs(_mocks.system_calls) do
+ if cmd:match("git") and vim.v.shell_error ~= 0 then
+ git_failed = true
+ break
+ end
+ end
+ assert.is_true(git_failed, "git should have been called and failed")
+ end)
+
+ it("applies manual excludes on top of git output", function()
+ _mocks.stat_exists = true
+ _mocks.stat_type = "directory"
+ _mocks.system_output =
+ "README.md\n.env\n.env.local\nnode_modules/package.json\nsrc/main.lua\n"
+ _mocks.system_exit = 0
+
+ Files.setup({
+ enabled = true,
+ exclude = { ".env", ".env.*", "node_modules" },
+ }, {})
+
+ local files = Files.discover_files()
+
+ assert.are.equal(2, #files)
+ eq("README.md", files[1].path)
+ eq("src/main.lua", files[2].path)
+
+ for _, f in ipairs(files) do
+ assert.is_nil(f.path:match("%.env"), ".env files should be excluded")
+ assert.is_nil(
+ f.path:match("node_modules"),
+ "node_modules should be excluded"
+ )
+ end
+ end)
+
+ it(
+ "excludes files by filename (not just path) consistent with fs scanner",
+ function()
+ _mocks.stat_exists = true
+ _mocks.stat_type = "directory"
+ _mocks.system_output = "README.md\nsrc/builder.js\nsrc/build/config.lua\n"
+ _mocks.system_exit = 0
+
+ Files.setup({
+ enabled = true,
+ exclude = { "build" },
+ }, {})
+
+ local files = Files.discover_files()
+
+ assert.are.equal(1, #files)
+ eq("README.md", files[1].path)
+
+ for _, f in ipairs(files) do
+ assert.is_nil(
+ f.name:match("^build"),
+ "files starting with 'build' should be excluded"
+ )
+ end
+ end
+ )
+
+ it("uses --deduplicate flag to handle merge conflict duplicates", function()
+ _mocks.stat_exists = true
+ _mocks.stat_type = "directory"
+
+ _mocks.system_output = "README.md\nREADME.md\nREADME.md\n"
+ _mocks.system_exit = 0
+
+ local files = Files.discover_files()
+
+ local has_deduplicate = false
+ for _, cmd in ipairs(_mocks.system_calls) do
+ if cmd:match("%-%-deduplicate") then
+ has_deduplicate = true
+ break
+ end
+ end
+ assert.is_true(
+ has_deduplicate,
+ "git command should include --deduplicate flag"
+ )
+ end)
+
+ it("uses git-based discovery in git repo", function()
+ _mocks.stat_exists = true
+ _mocks.stat_type = "directory"
+ _mocks.system_output = "tracked.txt\n"
+ _mocks.system_exit = 0
+
+ Files.discover_files()
+
+ local git_called = false
+ for _, cmd in ipairs(_mocks.system_calls) do
+ if cmd:match("git") then
+ git_called = true
+ break
+ end
+ end
+ assert.is_true(git_called, "should use git ls-files in git repo")
+ end)
+
+ it("falls back to filesystem when not in git repo", function()
+ _mocks.stat_exists = false
+ local orig_scandir = vim.uv.fs_scandir
+ local fs_called = false
+ vim.uv.fs_scandir = function(_dir)
+ fs_called = true
+ return nil -- Empty
+ end
+
+ Files.discover_files()
+
+ vim.uv.fs_scandir = orig_scandir
+
+ for _, cmd in ipairs(_mocks.system_calls) do
+ assert.is_nil(
+ cmd:match("git"),
+ "git should not be called in non-git repo"
+ )
+ end
+
+ assert.is_true(fs_called, "filesystem fallback should be used")
+ end)
+
+ it("returns cached files on subsequent calls", function()
+ _mocks.stat_exists = true
+ _mocks.stat_type = "directory"
+ _mocks.system_output = "file.txt\n"
+ _mocks.system_exit = 0
+
+ local first = Files.get_files()
+ local first_call_count = #_mocks.system_calls
+
+ local second = Files.get_files()
+ local second_call_count = #_mocks.system_calls
+
+ eq(first, second)
+
+ assert.are.equal(
+ first_call_count,
+ second_call_count,
+ "should not re-scan, use cache"
+ )
+ end)
+
+ it("handles empty root gracefully", function()
+ Files.set_project_root("")
+
+ local files = Files.discover_files()
+
+ assert.is_not_nil(files, "should return table")
+ assert.are.equal(0, #files, "should return empty for empty root")
+ end)
+end)