feat: rename search_keyword => grep_search (#1584)

This commit is contained in:
yetone
2025-03-14 15:57:24 +08:00
committed by GitHub
parent e332d74c06
commit bafe156639
4 changed files with 120 additions and 25 deletions

View File

@@ -83,8 +83,8 @@ function M.search_files(opts, on_log)
return vim.json.encode(filepaths), nil return vim.json.encode(filepaths), nil
end end
---@type AvanteLLMToolFunc<{ rel_path: string, keyword: string }> ---@type AvanteLLMToolFunc<{ rel_path: string, query: string, case_sensitive?: boolean, include_pattern?: string, exclude_pattern?: string }>
function M.search_keyword(opts, on_log) function M.grep_search(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 not Path:new(abs_path):exists() then return "", "No such file or directory: " .. abs_path end if not Path:new(abs_path):exists() then return "", "No such file or directory: " .. abs_path end
@@ -99,15 +99,32 @@ function M.search_keyword(opts, on_log)
---execute the search command ---execute the search command
local cmd = "" local cmd = ""
if search_cmd:find("rg") then if search_cmd:find("rg") then
cmd = string.format("%s --files-with-matches --ignore-case --hidden --glob '!.git'", search_cmd) cmd = string.format("%s --files-with-matches --hidden", search_cmd)
cmd = string.format("%s '%s' %s", cmd, opts.keyword, abs_path) if opts.case_sensitive then
cmd = string.format("%s --case-sensitive", cmd)
else
cmd = string.format("%s --ignore-case", cmd)
end
if opts.include_pattern then cmd = string.format("%s --glob '%s'", cmd, opts.include_pattern) end
if opts.exclude_pattern then cmd = string.format("%s --glob '!%s'", cmd, opts.exclude_pattern) end
cmd = string.format("%s '%s' %s", cmd, opts.query, abs_path)
elseif search_cmd:find("ag") then elseif search_cmd:find("ag") then
cmd = string.format("%s '%s' --nocolor --nogroup --hidden --ignore .git %s", search_cmd, opts.keyword, abs_path) cmd = string.format("%s --nocolor --nogroup --hidden", search_cmd)
if opts.case_sensitive then cmd = string.format("%s --case-sensitive", cmd) end
if opts.include_pattern then cmd = string.format("%s --ignore '!%s'", cmd, opts.include_pattern) end
if opts.exclude_pattern then cmd = string.format("%s --ignore '%s'", cmd, opts.exclude_pattern) end
cmd = string.format("%s '%s' %s", cmd, opts.query, abs_path)
elseif search_cmd:find("ack") then elseif search_cmd:find("ack") then
cmd = string.format("%s --nocolor --nogroup --hidden --ignore-dir .git", search_cmd) cmd = string.format("%s --nocolor --nogroup --hidden", search_cmd)
cmd = string.format("%s '%s' %s", cmd, opts.keyword, abs_path) if opts.case_sensitive then cmd = string.format("%s --smart-case", cmd) end
if opts.exclude_pattern then cmd = string.format("%s --ignore-dir '%s'", cmd, opts.exclude_pattern) end
cmd = string.format("%s '%s' %s", cmd, opts.query, abs_path)
elseif search_cmd:find("grep") then elseif search_cmd:find("grep") then
cmd = string.format("%s -riH --exclude-dir=.git %s %s", search_cmd, opts.keyword, abs_path) cmd = string.format("cd %s && git ls-files -co --exclude-standard | xargs %s -rH", abs_path, search_cmd, abs_path)
if not opts.case_sensitive then cmd = string.format("%s -i", cmd) end
if opts.include_pattern then cmd = string.format("%s --include '%s'", cmd, opts.include_pattern) end
if opts.exclude_pattern then cmd = string.format("%s --exclude '%s'", cmd, opts.exclude_pattern) end
cmd = string.format("%s '%s'", cmd, opts.query)
end end
Utils.debug("cmd", cmd) Utils.debug("cmd", cmd)
@@ -842,8 +859,8 @@ M._tools = {
}, },
}, },
{ {
name = "search_keyword", name = "grep_search",
description = "Search for a keyword in a directory", description = "Search for a keyword in a directory using grep",
param = { param = {
type = "table", type = "table",
fields = { fields = {
@@ -853,10 +870,29 @@ M._tools = {
type = "string", type = "string",
}, },
{ {
name = "keyword", name = "query",
description = "Keyword to search for", description = "Query to search for",
type = "string", type = "string",
}, },
{
name = "case_sensitive",
description = "Whether to search case sensitively",
type = "boolean",
default = false,
optional = true,
},
{
name = "include_pattern",
description = "Glob pattern to include files",
type = "string",
optional = true,
},
{
name = "exclude_pattern",
description = "Glob pattern to exclude files",
type = "string",
optional = true,
},
}, },
}, },
returns = { returns = {

View File

@@ -354,7 +354,7 @@ vim.g.avante_login = vim.g.avante_login
---@class AvanteLLMToolParamField ---@class AvanteLLMToolParamField
---@field name string ---@field name string
---@field description string ---@field description string
---@field type 'string' | 'integer' ---@field type 'string' | 'integer' | 'boolean'
---@field optional? boolean ---@field optional? boolean
---@class AvanteLLMToolReturn ---@class AvanteLLMToolReturn

View File

@@ -767,10 +767,7 @@ function M.scan_directory(options)
cmd = { cmd = {
"bash", "bash",
"-c", "-c",
string.format( string.format("cd %s && git ls-files -co --exclude-standard", options.directory),
"cd %s && cat <(git ls-files --exclude-standard) <(git ls-files --exclude-standard --others)",
options.directory
),
} }
end end
cmd_supports_max_depth = false cmd_supports_max_depth = false

View File

@@ -124,7 +124,7 @@ describe("llm_tools", function()
end) end)
end) end)
describe("search_keyword", function() describe("grep_search", function()
local original_exepath = vim.fn.exepath local original_exepath = vim.fn.exepath
after_each(function() vim.fn.exepath = original_exepath end) after_each(function() vim.fn.exepath = original_exepath end)
@@ -147,10 +147,35 @@ describe("llm_tools", function()
file:write("this is nothing") file:write("this is nothing")
file:close() file:close()
local result, err = LlmTools.search_keyword({ rel_path = ".", keyword = "searchable" }) local result, err = LlmTools.grep_search({ rel_path = ".", query = "Searchable", case_sensitive = false })
assert.is_nil(err) assert.is_nil(err)
assert.truthy(result:find("searchable.txt")) assert.truthy(result:find("searchable.txt"))
assert.falsy(result:find("nothing.txt")) assert.falsy(result:find("nothing.txt"))
local result2, err2 = LlmTools.grep_search({ rel_path = ".", query = "searchable", case_sensitive = true })
assert.is_nil(err2)
assert.truthy(result2:find("searchable.txt"))
assert.falsy(result2:find("nothing.txt"))
local result3, err3 = LlmTools.grep_search({ rel_path = ".", query = "Searchable", case_sensitive = true })
assert.is_nil(err3)
assert.falsy(result3:find("searchable.txt"))
assert.falsy(result3:find("nothing.txt"))
local result4, err4 = LlmTools.grep_search({ rel_path = ".", query = "searchable", case_sensitive = false })
assert.is_nil(err4)
assert.truthy(result4:find("searchable.txt"))
assert.falsy(result4:find("nothing.txt"))
local result5, err5 = LlmTools.grep_search({
rel_path = ".",
query = "searchable",
case_sensitive = false,
exclude_pattern = "search*",
})
assert.is_nil(err5)
assert.falsy(result5:find("searchable.txt"))
assert.falsy(result5:find("nothing.txt"))
end) end)
it("should search using ag when rg is not available", function() it("should search using ag when rg is not available", function()
@@ -166,7 +191,7 @@ describe("llm_tools", function()
file:write("content for ag test") file:write("content for ag test")
file:close() file:close()
local result, err = LlmTools.search_keyword({ rel_path = ".", keyword = "ag test" }) local result, err = LlmTools.grep_search({ rel_path = ".", query = "ag test" })
assert.is_nil(err) assert.is_nil(err)
assert.is_string(result) assert.is_string(result)
assert.truthy(result:find("ag_test.txt")) assert.truthy(result:find("ag_test.txt"))
@@ -179,27 +204,64 @@ describe("llm_tools", function()
return "" return ""
end end
local result, err = LlmTools.search_keyword({ rel_path = ".", keyword = "test" }) -- 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()
local result, err = LlmTools.grep_search({ rel_path = ".", query = "Searchable", case_sensitive = false })
assert.is_nil(err) assert.is_nil(err)
assert.truthy(result:find("test.txt")) assert.truthy(result:find("searchable.txt"))
assert.falsy(result:find("nothing.txt"))
local result2, err2 = LlmTools.grep_search({ rel_path = ".", query = "searchable", case_sensitive = true })
assert.is_nil(err2)
assert.truthy(result2:find("searchable.txt"))
assert.falsy(result2:find("nothing.txt"))
local result3, err3 = LlmTools.grep_search({ rel_path = ".", query = "Searchable", case_sensitive = true })
assert.is_nil(err3)
assert.falsy(result3:find("searchable.txt"))
assert.falsy(result3:find("nothing.txt"))
local result4, err4 = LlmTools.grep_search({ rel_path = ".", query = "searchable", case_sensitive = false })
assert.is_nil(err4)
assert.truthy(result4:find("searchable.txt"))
assert.falsy(result4:find("nothing.txt"))
local result5, err5 = LlmTools.grep_search({
rel_path = ".",
query = "searchable",
case_sensitive = false,
exclude_pattern = "search*",
})
assert.is_nil(err5)
assert.falsy(result5:find("searchable.txt"))
assert.falsy(result5:find("nothing.txt"))
end) end)
it("should return error when no search tool is available", function() it("should return error when no search tool is available", function()
-- Mock exepath to return nothing -- Mock exepath to return nothing
vim.fn.exepath = function() return "" end vim.fn.exepath = function() return "" end
local result, err = LlmTools.search_keyword({ rel_path = ".", keyword = "test" }) local result, err = LlmTools.grep_search({ rel_path = ".", query = "test" })
assert.equals("", result) assert.equals("", result)
assert.equals("No search command found", err) assert.equals("No search command found", err)
end) end)
it("should respect path permissions", function() it("should respect path permissions", function()
local result, err = LlmTools.search_keyword({ rel_path = "../outside_project", keyword = "test" }) local result, err = LlmTools.grep_search({ rel_path = "../outside_project", query = "test" })
assert.truthy(err:find("No permission to access path")) assert.truthy(err:find("No permission to access path"))
end) end)
it("should handle non-existent paths", function() it("should handle non-existent paths", function()
local result, err = LlmTools.search_keyword({ rel_path = "non_existent_dir", keyword = "test" }) local result, err = LlmTools.grep_search({ rel_path = "non_existent_dir", query = "test" })
assert.equals("", result) assert.equals("", result)
assert.truthy(err) assert.truthy(err)
assert.truthy(err:find("No such file or directory")) assert.truthy(err:find("No such file or directory"))