diff --git a/lua/avante/llm_tools.lua b/lua/avante/llm_tools.lua index fed30f5..6b07df1 100644 --- a/lua/avante/llm_tools.lua +++ b/lua/avante/llm_tools.lua @@ -38,6 +38,16 @@ local function has_permission_to_access(abs_path) return not Utils.is_ignored(rel_path, gitignore_patterns, gitignore_negate_patterns) end +---@type AvanteLLMToolFunc<{ rel_path: string, pattern: string }> +function M.glob(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("pattern: " .. opts.pattern) end + local files = vim.fn.glob(abs_path .. "/" .. opts.pattern, true, true) + return vim.json.encode(files), nil +end + ---@type AvanteLLMToolFunc<{ rel_path: string, max_depth?: integer }> function M.list_files(opts, on_log) local abs_path = get_abs_path(opts.rel_path) @@ -613,6 +623,38 @@ end ---@type AvanteLLMTool[] M._tools = { + { + name = "glob", + description = 'Fast file pattern matching using glob patterns like "**/*.js"', + param = { + type = "table", + fields = { + { + name = "pattern", + description = "Glob pattern", + type = "string", + }, + { + name = "rel_path", + description = "Relative path to the directory, as cwd", + type = "string", + }, + }, + }, + returns = { + { + name = "matches", + description = "List of matched files", + type = "string", + }, + { + name = "err", + description = "Error message", + type = "string", + optional = true, + }, + }, + }, { name = "rag_search", enabled = function() return Config.rag_service.enabled and RagService.is_ready() end, @@ -643,7 +685,7 @@ M._tools = { }, { name = "python", - description = "Run python code", + description = "Run python code. Can't use it to read files or modify files.", param = { type = "table", fields = { @@ -858,7 +900,7 @@ M._tools = { }, { name = "read_file", - description = "Read the contents of a file", + description = "Read the contents of a file. If the file content is already in the context, do not use this tool.", param = { type = "table", fields = { @@ -1057,7 +1099,7 @@ M._tools = { }, { name = "bash", - description = "Run a bash command in a directory", + description = "Run a bash command in a directory. Can't use search commands like find/grep or read tools like cat/ls.", param = { type = "table", fields = { diff --git a/tests/llm_tools_spec.lua b/tests/llm_tools_spec.lua index a7145d5..f75792d 100644 --- a/tests/llm_tools_spec.lua +++ b/tests/llm_tools_spec.lua @@ -272,4 +272,76 @@ describe("llm_tools", function() assert.equals("Hello from custom container\n", result) end) end) + + describe("glob", function() + it("should find files matching the pattern", function() + -- Create some additional test files with different extensions for glob testing + os.execute("touch " .. test_dir .. "/file1.lua") + os.execute("touch " .. test_dir .. "/file2.lua") + os.execute("touch " .. test_dir .. "/file3.js") + os.execute("mkdir -p " .. test_dir .. "/nested") + os.execute("touch " .. test_dir .. "/nested/file4.lua") + + -- Test for lua files in the root + local result, err = LlmTools.glob({ rel_path = ".", pattern = "*.lua" }) + assert.is_nil(err) + local files = vim.json.decode(result) + assert.equals(2, #files) + assert.truthy(vim.tbl_contains(files, test_dir .. "/file1.lua")) + assert.truthy(vim.tbl_contains(files, test_dir .. "/file2.lua")) + assert.falsy(vim.tbl_contains(files, test_dir .. "/file3.js")) + assert.falsy(vim.tbl_contains(files, test_dir .. "/nested/file4.lua")) + + -- Test with recursive pattern + local result2, err2 = LlmTools.glob({ rel_path = ".", pattern = "**/*.lua" }) + assert.is_nil(err2) + local files2 = vim.json.decode(result2) + assert.equals(3, #files2) + assert.truthy(vim.tbl_contains(files2, test_dir .. "/file1.lua")) + assert.truthy(vim.tbl_contains(files2, test_dir .. "/file2.lua")) + assert.truthy(vim.tbl_contains(files2, test_dir .. "/nested/file4.lua")) + end) + + it("should respect path permissions", function() + local result, err = LlmTools.glob({ rel_path = "../outside_project", pattern = "*.txt" }) + assert.equals("", result) + assert.truthy(err:find("No permission to access path")) + end) + + it("should handle patterns without matches", function() + local result, err = LlmTools.glob({ rel_path = ".", pattern = "*.nonexistent" }) + assert.is_nil(err) + local files = vim.json.decode(result) + assert.equals(0, #files) + end) + + it("should handle files in gitignored directories", function() + -- Create test files in ignored directory + os.execute("touch " .. test_dir .. "/test_dir2/ignored1.lua") + os.execute("touch " .. test_dir .. "/test_dir2/ignored2.lua") + + -- Create test files in non-ignored directory + os.execute("touch " .. test_dir .. "/test_dir1/notignored1.lua") + os.execute("touch " .. test_dir .. "/test_dir1/notignored2.lua") + + local result, err = LlmTools.glob({ rel_path = ".", pattern = "**/*.lua" }) + assert.is_nil(err) + local files = vim.json.decode(result) + + -- Check that files from non-ignored directory are found + local found_notignored = false + for _, file in ipairs(files) do + if file:find("test_dir1/notignored") then + found_notignored = true + break + end + end + assert.is_true(found_notignored) + + -- Note: By default, vim.fn.glob does not respect gitignore files + -- This test simply verifies the glob function works as expected + -- If in the future, the function is modified to respect gitignore, + -- this test can be updated + end) + end) end)