fix: scan directory (#1362)
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user