From 3741460541ee87dd46fe51b27b20e26c99886c7c Mon Sep 17 00:00:00 2001 From: yetone Date: Sat, 8 Mar 2025 21:15:07 +0800 Subject: [PATCH] optimize: make relative (#1529) --- lua/avante/file_selector.lua | 7 ++- lua/avante/llm_tools.lua | 2 +- lua/avante/utils/init.lua | 75 +++++++++++++++++++++-- tests/utils/get_parent_path_spec.lua | 81 +++++++++++++++++++++++++ tests/utils/join_paths_spec.lua | 54 +++++++++++++++++ tests/utils/make_relative_path_spec.lua | 58 ++++++++++++++++++ 6 files changed, 268 insertions(+), 9 deletions(-) create mode 100644 tests/utils/get_parent_path_spec.lua create mode 100644 tests/utils/join_paths_spec.lua create mode 100644 tests/utils/make_relative_path_spec.lua diff --git a/lua/avante/file_selector.lua b/lua/avante/file_selector.lua index cd49ac9..1cbc2cf 100644 --- a/lua/avante/file_selector.lua +++ b/lua/avante/file_selector.lua @@ -18,6 +18,7 @@ local FileSelector = {} local function has_scheme(path) return path:find("^%w+://") ~= nil end function FileSelector:process_directory(absolute_path, project_root) + if absolute_path:sub(-1) == Utils.path_sep then absolute_path = absolute_path:sub(1, -2) end local files = scan.scan_dir(absolute_path, { hidden = false, depth = math.huge, @@ -26,7 +27,7 @@ function FileSelector:process_directory(absolute_path, project_root) }) for _, file in ipairs(files) do - local rel_path = Path:new(file):make_relative(project_root) + local rel_path = Utils.make_relative_path(file, project_root) if not vim.tbl_contains(self.selected_filepaths, rel_path) then table.insert(self.selected_filepaths, rel_path) end end self:emit("update") @@ -61,10 +62,10 @@ end local function get_project_filepaths() local project_root = Utils.get_project_root() 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 Utils.make_relative_path(filepath, project_root) end):totable() return vim.tbl_map(function(path) - local rel_path = Path:new(path):make_relative(project_root) + local rel_path = Utils.make_relative_path(path, project_root) local stat = vim.loop.fs_stat(path) if stat and stat.type == "directory" then rel_path = rel_path .. "/" end return rel_path diff --git a/lua/avante/llm_tools.lua b/lua/avante/llm_tools.lua index 6b07df1..6163716 100644 --- a/lua/avante/llm_tools.lua +++ b/lua/avante/llm_tools.lua @@ -34,7 +34,7 @@ local function has_permission_to_access(abs_path) -- Specifically, it should not check the project root itself -- Otherwise if the binary is named the same as the project root (such as Go binary), any paths -- insde the project root will be ignored - local rel_path = Path:new(abs_path):make_relative(project_root) + local rel_path = Utils.make_relative_path(abs_path, project_root) return not Utils.is_ignored(rel_path, gitignore_patterns, gitignore_negate_patterns) end diff --git a/lua/avante/utils/init.lua b/lua/avante/utils/init.lua index 0d93c01..d3aeaf1 100644 --- a/lua/avante/utils/init.lua +++ b/lua/avante/utils/init.lua @@ -35,6 +35,14 @@ end function M.is_win() return jit.os:find("Windows") ~= nil end +M.path_sep = (function() + if M.is_win() then + return "\\" + else + return "/" + end +end)() + ---@return "linux" | "darwin" | "windows" function M.get_os_name() local os_name = vim.uv.os_uname().sysname @@ -765,7 +773,7 @@ function M.scan_directory(options) :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 rel_path = M.make_relative_path(file, base_dir) local pieces = vim.split(rel_path, "/") return #pieces <= options.max_depth end) @@ -776,8 +784,7 @@ function M.scan_directory(options) local dirs = {} local dirs_seen = {} for _, file in ipairs(files) do - local dir = tostring(Path:new(file):parent()) - dir = dir .. "/" + local dir = M.get_parent_path(file) if not dirs_seen[dir] then table.insert(dirs, dir) dirs_seen[dir] = true @@ -789,6 +796,64 @@ function M.scan_directory(options) return files end +function M.get_parent_path(filepath) + if filepath == nil then error("filepath cannot be nil") end + if filepath == "" then return "" end + local is_abs = M.is_absolute_path(filepath) + if filepath:sub(-1) == M.path_sep then filepath = filepath:sub(1, -2) end + if filepath == "" then return "" end + local parts = vim.split(filepath, M.path_sep) + local parent_parts = vim.list_slice(parts, 1, #parts - 1) + local res = table.concat(parent_parts, M.path_sep) + if res == "" then + if is_abs then return M.path_sep end + return "." + end + return res +end + +function M.make_relative_path(filepath, base_dir) + if filepath:sub(-2) == M.path_sep .. "." then filepath = filepath:sub(1, -3) end + if base_dir:sub(-2) == M.path_sep .. "." then base_dir = base_dir:sub(1, -3) end + if filepath == base_dir then return "." end + if filepath:sub(1, #base_dir) == base_dir then + filepath = filepath:sub(#base_dir + 1) + if filepath:sub(1, 2) == "." .. M.path_sep then + filepath = filepath:sub(3) + elseif filepath:sub(1, 1) == M.path_sep then + filepath = filepath:sub(2) + end + end + return filepath +end + +function M.is_absolute_path(path) + if not path then return false end + if M.is_win() then return path:match("^%a:[/\\]") ~= nil end + return path:match("^/") ~= nil +end + +function M.join_paths(...) + local paths = { ... } + local result = paths[1] or "" + for i = 2, #paths do + local path = paths[i] + if path == nil or path == "" then goto continue end + + if M.is_absolute_path(path) then + result = path + goto continue + end + + if path:sub(1, 2) == "." .. M.path_sep then path = path:sub(3) end + + if result ~= "" and result:sub(-1) ~= M.path_sep then result = result .. M.path_sep end + result = result .. path + ::continue:: + end + return result +end + function M.is_first_letter_uppercase(str) return string.match(str, "^[A-Z]") ~= nil end ---@param content string @@ -954,8 +1019,8 @@ 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(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 abs_path = M.is_absolute_path(path) and path or M.join_paths(project_root, path) + local relative_path = M.make_relative_path(abs_path, project_root) return relative_path end diff --git a/tests/utils/get_parent_path_spec.lua b/tests/utils/get_parent_path_spec.lua new file mode 100644 index 0000000..f6359ff --- /dev/null +++ b/tests/utils/get_parent_path_spec.lua @@ -0,0 +1,81 @@ +local utils = require("avante.utils") + +describe("get_parent_path", function() + -- Define path separator for our tests, using the same logic as in the utils module + local path_sep = jit.os:find("Windows") ~= nil and "\\" or "/" + + it("should return the parent directory of a file path", function() + local filepath = "foo" .. path_sep .. "bar" .. path_sep .. "baz.txt" + local expected = "foo" .. path_sep .. "bar" + assert.are.equal(expected, utils.get_parent_path(filepath)) + end) + + it("should return the parent directory of a directory path", function() + local dirpath = "foo" .. path_sep .. "bar" .. path_sep .. "baz" + local expected = "foo" .. path_sep .. "bar" + assert.are.equal(expected, utils.get_parent_path(dirpath)) + end) + + it("should handle trailing separators", function() + local dirpath = "foo" .. path_sep .. "bar" .. path_sep .. "baz" .. path_sep + local expected = "foo" .. path_sep .. "bar" + assert.are.equal(expected, utils.get_parent_path(dirpath)) + end) + + it("should return '.' for a single file or directory", function() + assert.are.equal(".", utils.get_parent_path("foo.txt")) + assert.are.equal(".", utils.get_parent_path("dir")) + end) + + it("should handle paths with multiple levels", function() + local filepath = "a" .. path_sep .. "b" .. path_sep .. "c" .. path_sep .. "d" .. path_sep .. "file.txt" + local expected = "a" .. path_sep .. "b" .. path_sep .. "c" .. path_sep .. "d" + assert.are.equal(expected, utils.get_parent_path(filepath)) + end) + + it("should return empty string for root directory", function() + -- Root directory on Unix-like systems + if path_sep == "/" then + assert.are.equal("/", utils.get_parent_path("/foo")) + else + -- Windows uses drive letters, so parent of "C:\foo" is "C:" + local winpath = "C:" .. path_sep .. "foo" + assert.are.equal("C:", utils.get_parent_path(winpath)) + end + end) + + it("should return empty string for an empty string", function() assert.are.equal("", utils.get_parent_path("")) end) + + it("should throw an error for nil input", function() + assert.has_error(function() utils.get_parent_path(nil) end, "filepath cannot be nil") + end) + + it("should handle paths with spaces", function() + local filepath = "path with spaces" .. path_sep .. "file name.txt" + local expected = "path with spaces" + assert.are.equal(expected, utils.get_parent_path(filepath)) + end) + + it("should handle special characters in paths", function() + local filepath = "folder-name!" .. path_sep .. "file_#$%&.txt" + local expected = "folder-name!" + assert.are.equal(expected, utils.get_parent_path(filepath)) + end) + + it("should handle absolute paths", function() + if path_sep == "/" then + -- Unix-like paths + local filepath = path_sep .. "home" .. path_sep .. "user" .. path_sep .. "file.txt" + local expected = path_sep .. "home" .. path_sep .. "user" + assert.are.equal(expected, utils.get_parent_path(filepath)) + + -- Root directory edge case + assert.are.equal("", utils.get_parent_path(path_sep)) + else + -- Windows paths + local filepath = "C:" .. path_sep .. "Users" .. path_sep .. "user" .. path_sep .. "file.txt" + local expected = "C:" .. path_sep .. "Users" .. path_sep .. "user" + assert.are.equal(expected, utils.get_parent_path(filepath)) + end + end) +end) diff --git a/tests/utils/join_paths_spec.lua b/tests/utils/join_paths_spec.lua new file mode 100644 index 0000000..b367d86 --- /dev/null +++ b/tests/utils/join_paths_spec.lua @@ -0,0 +1,54 @@ +local assert = require("luassert") +local utils = require("avante.utils") + +describe("join_paths", function() + it("should join multiple path segments with proper separator", function() + local result = utils.join_paths("path", "to", "file.lua") + assert.equals("path" .. utils.path_sep .. "to" .. utils.path_sep .. "file.lua", result) + end) + + it("should handle empty path segments", function() + local result = utils.join_paths("", "to", "file.lua") + assert.equals("to" .. utils.path_sep .. "file.lua", result) + end) + + it("should handle nil path segments", function() + local result = utils.join_paths(nil, "to", "file.lua") + assert.equals("to" .. utils.path_sep .. "file.lua", result) + end) + + it("should handle empty path segments", function() + local result = utils.join_paths("path", "", "file.lua") + assert.equals("path" .. utils.path_sep .. "file.lua", result) + end) + + it("should use absolute path when encountered", function() + local absolute_path = utils.is_win() and "C:\\absolute\\path" or "/absolute/path" + local result = utils.join_paths("relative", "path", absolute_path) + assert.equals(absolute_path, result) + end) + + it("should handle paths with trailing separators", function() + local path_with_sep = "path" .. utils.path_sep + local result = utils.join_paths(path_with_sep, "file.lua") + assert.equals("path" .. utils.path_sep .. "file.lua", result) + end) + + it("should return empty string when no paths provided", function() + local result = utils.join_paths() + assert.equals("", result) + end) + + it("should return first path when only one path provided", function() + local result = utils.join_paths("path") + assert.equals("path", result) + end) + + it("should handle path with mixed separators", function() + -- This test is more relevant on Windows where both / and \ are valid separators + local mixed_path = utils.is_win() and "path\\to/file" or "path/to/file" + local result = utils.join_paths("base", mixed_path) + -- The function should use utils.path_sep for joining + assert.equals("base" .. utils.path_sep .. mixed_path, result) + end) +end) diff --git a/tests/utils/make_relative_path_spec.lua b/tests/utils/make_relative_path_spec.lua new file mode 100644 index 0000000..4aa4a67 --- /dev/null +++ b/tests/utils/make_relative_path_spec.lua @@ -0,0 +1,58 @@ +local assert = require("luassert") +local utils = require("avante.utils") + +describe("make_relative_path", function() + it("should remove base directory from filepath", function() + local test_filepath = "/path/to/project/src/file.lua" + local test_base_dir = "/path/to/project" + local result = utils.make_relative_path(test_filepath, test_base_dir) + assert.equals("src/file.lua", result) + end) + + it("should handle trailing dot-slash in base_dir", function() + local test_filepath = "/path/to/project/src/file.lua" + local test_base_dir = "/path/to/project/." + local result = utils.make_relative_path(test_filepath, test_base_dir) + assert.equals("src/file.lua", result) + end) + + it("should handle trailing dot-slash in filepath", function() + local test_filepath = "/path/to/project/src/." + local test_base_dir = "/path/to/project" + local result = utils.make_relative_path(test_filepath, test_base_dir) + assert.equals("src", result) + end) + + it("should handle both having trailing dot-slash", function() + local test_filepath = "/path/to/project/src/." + local test_base_dir = "/path/to/project/." + local result = utils.make_relative_path(test_filepath, test_base_dir) + assert.equals("src", result) + end) + + it("should return the filepath when base_dir is not a prefix", function() + local test_filepath = "/path/to/project/src/file.lua" + local test_base_dir = "/different/path" + local result = utils.make_relative_path(test_filepath, test_base_dir) + assert.equals("/path/to/project/src/file.lua", result) + end) + + it("should handle identical paths", function() + local test_filepath = "/path/to/project" + local test_base_dir = "/path/to/project" + local result = utils.make_relative_path(test_filepath, test_base_dir) + assert.equals(".", result) + end) + + it("should handle empty strings", function() + local result = utils.make_relative_path("", "") + assert.equals(".", result) + end) + + it("should preserve trailing slash in filepath", function() + local test_filepath = "/path/to/project/src/" + local test_base_dir = "/path/to/project" + local result = utils.make_relative_path(test_filepath, test_base_dir) + assert.equals("src/", result) + end) +end)