diff --git a/lua/avante/file_selector.lua b/lua/avante/file_selector.lua index 59d475b..cc64325 100644 --- a/lua/avante/file_selector.lua +++ b/lua/avante/file_selector.lua @@ -60,7 +60,7 @@ end local function get_project_filepaths() local project_root = Utils.get_project_root() - local files = Utils.scan_directory_respect_gitignore({ directory = project_root, add_dirs = true }) + local files = Utils.scan_directory({ directory = project_root, add_dirs = true }) files = vim.iter(files):map(function(filepath) return Path:new(filepath):make_relative(project_root) end):totable() return vim.tbl_map(function(path) diff --git a/lua/avante/llm_tools.lua b/lua/avante/llm_tools.lua index e6b183c..a8ca520 100644 --- a/lua/avante/llm_tools.lua +++ b/lua/avante/llm_tools.lua @@ -31,7 +31,7 @@ local function has_permission_to_access(abs_path) return not Utils.is_ignored(abs_path, gitignore_patterns, gitignore_negate_patterns) end ----@param opts { rel_path: string, depth?: integer } +---@param opts { rel_path: string, max_depth?: integer } ---@param on_log? fun(log: string): nil ---@return string files ---@return string|nil error @@ -39,11 +39,11 @@ function M.list_files(opts, on_log) local abs_path = get_abs_path(opts.rel_path) if not has_permission_to_access(abs_path) then return "", "No permission to access path: " .. abs_path end if on_log then on_log("path: " .. abs_path) end - if on_log then on_log("depth: " .. tostring(opts.depth)) end - local files = Utils.scan_directory_respect_gitignore({ + if on_log then on_log("max depth: " .. tostring(opts.max_depth)) end + local files = Utils.scan_directory({ directory = abs_path, add_dirs = true, - depth = opts.depth, + max_depth = opts.max_depth, }) local filepaths = {} for _, file in ipairs(files) do @@ -62,7 +62,7 @@ function M.search_files(opts, on_log) if not has_permission_to_access(abs_path) then return "", "No permission to access path: " .. abs_path end if on_log then on_log("path: " .. abs_path) end if on_log then on_log("keyword: " .. opts.keyword) end - local files = Utils.scan_directory_respect_gitignore({ + local files = Utils.scan_directory({ directory = abs_path, }) local filepaths = {} @@ -710,10 +710,9 @@ M._tools = { type = "string", }, { - name = "depth", - description = "Depth of the directory", + name = "max_depth", + description = "Maximum depth of the directory", type = "integer", - optional = true, }, }, }, diff --git a/lua/avante/repo_map.lua b/lua/avante/repo_map.lua index 42334d8..d6e7591 100644 --- a/lua/avante/repo_map.lua +++ b/lua/avante/repo_map.lua @@ -1,7 +1,6 @@ local Popup = require("nui.popup") local Utils = require("avante.utils") local event = require("nui.utils.autocmd").event -local Config = require("avante.config") local filetype_map = { ["javascriptreact"] = "javascript", @@ -35,15 +34,9 @@ end function RepoMap._build_repo_map(project_root, file_ext) local output = {} - local gitignore_path = project_root .. "/.gitignore" - local gitignore_patterns, gitignore_negate_patterns = Utils.parse_gitignore(gitignore_path) - local ignore_patterns = vim.list_extend(gitignore_patterns, Config.repo_map.ignore_patterns) - local negate_patterns = vim.list_extend(gitignore_negate_patterns, Config.repo_map.negate_patterns) local filepaths = Utils.scan_directory({ directory = project_root, - gitignore_patterns = ignore_patterns, - gitignore_negate_patterns = negate_patterns, }) if filepaths and not RepoMap._init_repo_map_lib() then -- or just throw an error if we don't want to execute request without codebase diff --git a/lua/avante/utils/init.lua b/lua/avante/utils/init.lua index a6aed48..8fac8fe 100644 --- a/lua/avante/utils/init.lua +++ b/lua/avante/utils/init.lua @@ -658,71 +658,85 @@ function M.is_ignored(file, ignore_patterns, negate_patterns) return false end ----@param options { directory: string, add_dirs?: boolean, depth?: integer } -function M.scan_directory_respect_gitignore(options) - local directory = options.directory - local gitignore_path = directory .. "/.gitignore" - local gitignore_patterns, gitignore_negate_patterns = M.parse_gitignore(gitignore_path) +---@param options { directory: string, add_dirs?: boolean, max_depth?: integer } +---@return string[] +function M.scan_directory(options) + local cmd_supports_max_depth = true + local cmd = (function() + if vim.fn.executable("rg") == 1 then + local cmd = { "rg", "--files", "--color", "never", "--no-require-git" } + if options.max_depth ~= nil then vim.list_extend(cmd, { "--max-depth", options.max_depth }) end + table.insert(cmd, options.directory) + return cmd + end + if vim.fn.executable("fd") == 1 then + local cmd = { "fd", "--type", "f", "--color", "never", "--no-require-git" } + if options.max_depth ~= nil then vim.list_extend(cmd, { "--max-depth", options.max_depth }) end + vim.list_extend(cmd, { "--base-directory", options.directory }) + return cmd + end + if vim.fn.executable("fdfind") == 1 then + local cmd = { "fdfind", "--type", "f", "--color", "never", "--no-require-git" } + if options.max_depth ~= nil then vim.list_extend(cmd, { "--max-depth", options.max_depth }) end + vim.list_extend(cmd, { "--base-directory", options.directory }) + return cmd + end + end)() - -- Convert relative paths in gitignore to absolute paths based on project root - local project_root = M.get_project_root() - local function to_absolute_path(pattern) - -- Skip if already absolute path - if pattern:sub(1, 1) == "/" then return pattern end - -- Convert relative path to absolute - return Path:new(project_root, pattern):absolute() + if not cmd then + local p = Path:new(options.directory) + if p:joinpath(".git"):exists() and vim.fn.executable("git") == 1 then + cmd = { + "bash", + "-c", + string.format( + "cd %s && cat <(git ls-files --exclude-standard) <(git ls-files --exclude-standard --others)", + options.directory + ), + } + cmd_supports_max_depth = false + else + M.error("No search command found") + return {} + end end - gitignore_patterns = vim.tbl_map(to_absolute_path, gitignore_patterns) - gitignore_negate_patterns = vim.tbl_map(to_absolute_path, gitignore_negate_patterns) + local files = vim.fn.systemlist(cmd) - return M.scan_directory({ - directory = directory, - gitignore_patterns = gitignore_patterns, - gitignore_negate_patterns = gitignore_negate_patterns, - add_dirs = options.add_dirs, - depth = options.depth, - }) -end + files = vim + .iter(files) + :map(function(file) + local p = Path:new(file) + if not p:is_absolute() then return tostring(Path:new(options.directory):joinpath(file):absolute()) end + return file + end) + :totable() ----@param options { directory: string, gitignore_patterns: string[], gitignore_negate_patterns: string[], add_dirs?: boolean, depth?: integer, current_depth?: integer } -function M.scan_directory(options) - local directory = options.directory - local ignore_patterns = options.gitignore_patterns - local negate_patterns = options.gitignore_negate_patterns - local add_dirs = options.add_dirs or false - local depth = options.depth or -1 - local current_depth = options.current_depth or 0 + if options.max_depth ~= nil and not cmd_supports_max_depth then + files = vim + .iter(files) + :filter(function(file) + local base_dir = options.directory + if base_dir:sub(-2) == "/." then base_dir = base_dir:sub(1, -3) end + local rel_path = tostring(Path:new(file):make_relative(base_dir)) + local pieces = vim.split(rel_path, "/") + return #pieces <= options.max_depth + end) + :totable() + end - local files = {} - local handle = vim.loop.fs_scandir(directory) - - if not handle then return files end - - while true do - if depth > 0 and current_depth >= depth then break end - - local name, type = vim.loop.fs_scandir_next(handle) - if not name then break end - - local full_path = directory .. "/" .. name - if type == "directory" then - if add_dirs and not M.is_ignored(full_path, ignore_patterns, negate_patterns) then - table.insert(files, full_path) + if options.add_dirs then + local dirs = {} + local dirs_seen = {} + for _, file in ipairs(files) do + local dir = tostring(Path:new(file):parent()) + dir = dir .. "/" + if not dirs_seen[dir] then + table.insert(dirs, dir) + dirs_seen[dir] = true end - vim.list_extend( - files, - M.scan_directory({ - directory = full_path, - gitignore_patterns = ignore_patterns, - gitignore_negate_patterns = negate_patterns, - add_dirs = add_dirs, - current_depth = current_depth + 1, - }) - ) - elseif type == "file" then - if not M.is_ignored(full_path, ignore_patterns, negate_patterns) then table.insert(files, full_path) end end + files = vim.list_extend(dirs, files) end return files @@ -890,9 +904,10 @@ function M.get_current_selection_diagnostics(bufnr, selection) end function M.uniform_path(path) + if type(path) ~= "string" then path = tostring(path) end if not M.file.is_in_cwd(path) then return path end local project_root = M.get_project_root() - local abs_path = Path:new(project_root):joinpath(path):absolute() + local abs_path = Path:new(path):is_absolute() and path or Path:new(project_root):joinpath(path):absolute() local relative_path = Path:new(abs_path):make_relative(project_root) return relative_path end diff --git a/tests/llm_tools_spec.lua b/tests/llm_tools_spec.lua index 372499c..95dacb7 100644 --- a/tests/llm_tools_spec.lua +++ b/tests/llm_tools_spec.lua @@ -1,4 +1,3 @@ -local mock = require("luassert.mock") local stub = require("luassert.stub") local LlmTools = require("avante.llm_tools") local Utils = require("avante.utils") @@ -12,9 +11,25 @@ describe("llm_tools", function() before_each(function() -- 创建测试目录和文件 os.execute("mkdir -p " .. test_dir) + os.execute(string.format("cd %s; git init", test_dir)) local file = io.open(test_file, "w") + if not file then error("Failed to create test file") end file:write("test content") file:close() + os.execute("mkdir -p " .. test_dir .. "/test_dir1") + file = io.open(test_dir .. "/test_dir1/test1.txt", "w") + if not file then error("Failed to create test file") end + file:write("test1 content") + file:close() + os.execute("mkdir -p " .. test_dir .. "/test_dir2") + file = io.open(test_dir .. "/test_dir2/test2.txt", "w") + if not file then error("Failed to create test file") end + file:write("test2 content") + file:close() + file = io.open(test_dir .. "/.gitignore", "w") + if not file then error("Failed to create test file") end + file:write("test_dir2/") + file:close() -- Mock get_project_root stub(Utils, "get_project_root", function() return test_dir end) @@ -29,9 +44,26 @@ describe("llm_tools", function() describe("list_files", function() it("should list files in directory", function() - local result, err = LlmTools.list_files({ rel_path = ".", depth = 1 }) + local result, err = LlmTools.list_files({ rel_path = ".", max_depth = 1 }) assert.is_nil(err) + assert.falsy(result:find("avante.nvim")) assert.truthy(result:find("test.txt")) + assert.falsy(result:find("test1.txt")) + end) + it("should list files in directory with depth", function() + local result, err = LlmTools.list_files({ rel_path = ".", max_depth = 2 }) + assert.is_nil(err) + assert.falsy(result:find("avante.nvim")) + assert.truthy(result:find("test.txt")) + assert.truthy(result:find("test1.txt")) + end) + it("should list files respecting gitignore", function() + local result, err = LlmTools.list_files({ rel_path = ".", max_depth = 2 }) + assert.is_nil(err) + assert.falsy(result:find("avante.nvim")) + assert.truthy(result:find("test.txt")) + assert.truthy(result:find("test1.txt")) + assert.falsy(result:find("test2.txt")) end) end) @@ -104,10 +136,12 @@ describe("llm_tools", function() -- Create a test file with searchable content local file = io.open(test_dir .. "/searchable.txt", "w") + if not file then error("Failed to create test file") end file:write("this is searchable content") file:close() file = io.open(test_dir .. "/nothing.txt", "w") + if not file then error("Failed to create test file") end file:write("this is nothing") file:close() @@ -126,6 +160,7 @@ describe("llm_tools", function() -- Create a test file specifically for ag local file = io.open(test_dir .. "/ag_test.txt", "w") + if not file then error("Failed to create test file") end file:write("content for ag test") file:close()