fix: scan directory (#1362)

This commit is contained in:
yetone
2025-02-23 18:26:52 +08:00
committed by GitHub
parent 284998a994
commit e93f2426e9
5 changed files with 118 additions and 76 deletions

View File

@@ -60,7 +60,7 @@ end
local function get_project_filepaths() local function get_project_filepaths()
local project_root = Utils.get_project_root() 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() files = vim.iter(files):map(function(filepath) return Path:new(filepath):make_relative(project_root) end):totable()
return vim.tbl_map(function(path) return vim.tbl_map(function(path)

View File

@@ -31,7 +31,7 @@ local function has_permission_to_access(abs_path)
return not Utils.is_ignored(abs_path, gitignore_patterns, gitignore_negate_patterns) return not Utils.is_ignored(abs_path, gitignore_patterns, gitignore_negate_patterns)
end end
---@param opts { rel_path: string, depth?: integer } ---@param opts { rel_path: string, max_depth?: integer }
---@param on_log? fun(log: string): nil ---@param on_log? fun(log: string): nil
---@return string files ---@return string files
---@return string|nil error ---@return string|nil error
@@ -39,11 +39,11 @@ function M.list_files(opts, on_log)
local abs_path = get_abs_path(opts.rel_path) 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 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("path: " .. abs_path) end
if on_log then on_log("depth: " .. tostring(opts.depth)) end if on_log then on_log("max depth: " .. tostring(opts.max_depth)) end
local files = Utils.scan_directory_respect_gitignore({ local files = Utils.scan_directory({
directory = abs_path, directory = abs_path,
add_dirs = true, add_dirs = true,
depth = opts.depth, max_depth = opts.max_depth,
}) })
local filepaths = {} local filepaths = {}
for _, file in ipairs(files) do 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 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("path: " .. abs_path) end
if on_log then on_log("keyword: " .. opts.keyword) 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, directory = abs_path,
}) })
local filepaths = {} local filepaths = {}
@@ -710,10 +710,9 @@ M._tools = {
type = "string", type = "string",
}, },
{ {
name = "depth", name = "max_depth",
description = "Depth of the directory", description = "Maximum depth of the directory",
type = "integer", type = "integer",
optional = true,
}, },
}, },
}, },

View File

@@ -1,7 +1,6 @@
local Popup = require("nui.popup") local Popup = require("nui.popup")
local Utils = require("avante.utils") local Utils = require("avante.utils")
local event = require("nui.utils.autocmd").event local event = require("nui.utils.autocmd").event
local Config = require("avante.config")
local filetype_map = { local filetype_map = {
["javascriptreact"] = "javascript", ["javascriptreact"] = "javascript",
@@ -35,15 +34,9 @@ end
function RepoMap._build_repo_map(project_root, file_ext) function RepoMap._build_repo_map(project_root, file_ext)
local output = {} 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({ local filepaths = Utils.scan_directory({
directory = project_root, directory = project_root,
gitignore_patterns = ignore_patterns,
gitignore_negate_patterns = negate_patterns,
}) })
if filepaths and not RepoMap._init_repo_map_lib() then 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 -- or just throw an error if we don't want to execute request without codebase

View File

@@ -658,71 +658,85 @@ function M.is_ignored(file, ignore_patterns, negate_patterns)
return false return false
end end
---@param options { directory: string, add_dirs?: boolean, depth?: integer } ---@param options { directory: string, add_dirs?: boolean, max_depth?: integer }
function M.scan_directory_respect_gitignore(options) ---@return string[]
local directory = options.directory function M.scan_directory(options)
local gitignore_path = directory .. "/.gitignore" local cmd_supports_max_depth = true
local gitignore_patterns, gitignore_negate_patterns = M.parse_gitignore(gitignore_path) 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 if not cmd then
local project_root = M.get_project_root() local p = Path:new(options.directory)
local function to_absolute_path(pattern) if p:joinpath(".git"):exists() and vim.fn.executable("git") == 1 then
-- Skip if already absolute path cmd = {
if pattern:sub(1, 1) == "/" then return pattern end "bash",
-- Convert relative path to absolute "-c",
return Path:new(project_root, pattern):absolute() 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 end
gitignore_patterns = vim.tbl_map(to_absolute_path, gitignore_patterns) local files = vim.fn.systemlist(cmd)
gitignore_negate_patterns = vim.tbl_map(to_absolute_path, gitignore_negate_patterns)
return M.scan_directory({ files = vim
directory = directory, .iter(files)
gitignore_patterns = gitignore_patterns, :map(function(file)
gitignore_negate_patterns = gitignore_negate_patterns, local p = Path:new(file)
add_dirs = options.add_dirs, if not p:is_absolute() then return tostring(Path:new(options.directory):joinpath(file):absolute()) end
depth = options.depth, return file
}) end)
end :totable()
---@param options { directory: string, gitignore_patterns: string[], gitignore_negate_patterns: string[], add_dirs?: boolean, depth?: integer, current_depth?: integer } if options.max_depth ~= nil and not cmd_supports_max_depth then
function M.scan_directory(options) files = vim
local directory = options.directory .iter(files)
local ignore_patterns = options.gitignore_patterns :filter(function(file)
local negate_patterns = options.gitignore_negate_patterns local base_dir = options.directory
local add_dirs = options.add_dirs or false if base_dir:sub(-2) == "/." then base_dir = base_dir:sub(1, -3) end
local depth = options.depth or -1 local rel_path = tostring(Path:new(file):make_relative(base_dir))
local current_depth = options.current_depth or 0 local pieces = vim.split(rel_path, "/")
return #pieces <= options.max_depth
end)
:totable()
end
local files = {} if options.add_dirs then
local handle = vim.loop.fs_scandir(directory) local dirs = {}
local dirs_seen = {}
if not handle then return files end for _, file in ipairs(files) do
local dir = tostring(Path:new(file):parent())
while true do dir = dir .. "/"
if depth > 0 and current_depth >= depth then break end if not dirs_seen[dir] then
table.insert(dirs, dir)
local name, type = vim.loop.fs_scandir_next(handle) dirs_seen[dir] = true
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)
end 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 end
files = vim.list_extend(dirs, files)
end end
return files return files
@@ -890,9 +904,10 @@ function M.get_current_selection_diagnostics(bufnr, selection)
end end
function M.uniform_path(path) 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 if not M.file.is_in_cwd(path) then return path end
local project_root = M.get_project_root() 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) local relative_path = Path:new(abs_path):make_relative(project_root)
return relative_path return relative_path
end end

View File

@@ -1,4 +1,3 @@
local mock = require("luassert.mock")
local stub = require("luassert.stub") local stub = require("luassert.stub")
local LlmTools = require("avante.llm_tools") local LlmTools = require("avante.llm_tools")
local Utils = require("avante.utils") local Utils = require("avante.utils")
@@ -12,9 +11,25 @@ describe("llm_tools", function()
before_each(function() before_each(function()
-- 创建测试目录和文件 -- 创建测试目录和文件
os.execute("mkdir -p " .. test_dir) os.execute("mkdir -p " .. test_dir)
os.execute(string.format("cd %s; git init", test_dir))
local file = io.open(test_file, "w") local file = io.open(test_file, "w")
if not file then error("Failed to create test file") end
file:write("test content") file:write("test content")
file:close() 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 -- Mock get_project_root
stub(Utils, "get_project_root", function() return test_dir end) stub(Utils, "get_project_root", function() return test_dir end)
@@ -29,9 +44,26 @@ describe("llm_tools", function()
describe("list_files", function() describe("list_files", function()
it("should list files in directory", 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.is_nil(err)
assert.falsy(result:find("avante.nvim"))
assert.truthy(result:find("test.txt")) 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)
end) end)
@@ -104,10 +136,12 @@ describe("llm_tools", function()
-- Create a test file with searchable content -- Create a test file with searchable content
local file = io.open(test_dir .. "/searchable.txt", "w") 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:write("this is searchable content")
file:close() file:close()
file = io.open(test_dir .. "/nothing.txt", "w") 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:write("this is nothing")
file:close() file:close()
@@ -126,6 +160,7 @@ describe("llm_tools", function()
-- Create a test file specifically for ag -- Create a test file specifically for ag
local file = io.open(test_dir .. "/ag_test.txt", "w") 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:write("content for ag test")
file:close() file:close()