From 436a02c3556cb9d584b640c68c9163fcff12bbd6 Mon Sep 17 00:00:00 2001 From: guanghechen <42513619+guanghechen@users.noreply.github.com> Date: Tue, 17 Jun 2025 16:55:19 +0800 Subject: [PATCH] fix(#2094): fix the path resolving in windows (#2248) * fix(#2094): fix the path resolving in windows * fix test case * tweak test case --- lua/avante/utils/init.lua | 46 ++------- lua/avante/utils/path.lua | 174 ++++++++++++++++++++++++++++++++ tests/utils/join_paths_spec.lua | 4 +- 3 files changed, 184 insertions(+), 40 deletions(-) create mode 100644 lua/avante/utils/path.lua diff --git a/lua/avante/utils/init.lua b/lua/avante/utils/init.lua index ab0a683..d92b793 100644 --- a/lua/avante/utils/init.lua +++ b/lua/avante/utils/init.lua @@ -6,6 +6,7 @@ local lsp = vim.lsp ---@field tokens avante.utils.tokens ---@field root avante.utils.root ---@field file avante.utils.file +---@field path avante.utils.path ---@field environment avante.utils.environment ---@field lsp avante.utils.lsp local M = {} @@ -32,20 +33,9 @@ function M.has(plugin) return res end -local _is_win = nil +function M.is_win() return M.path.is_win() end -function M.is_win() - if _is_win == nil then _is_win = jit.os:find("Windows") ~= nil end - return _is_win -end - -M.path_sep = (function() - if M.is_win() then - return "\\" - else - return "/" - end -end)() +M.path_sep = M.path.SEP ---@return "linux" | "darwin" | "windows" function M.get_os_name() @@ -340,7 +330,7 @@ end ---@param path string ---@return string -function M.norm(path) return vim.fs.normalize(path) end +function M.norm(path) return M.path.normalize(path) end ---@param msg string|string[] ---@param opts? LazyNotifyOpts @@ -900,26 +890,9 @@ function M.get_parent_path(filepath) 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.make_relative_path(filepath, base_dir) return M.path.relative(base_dir, filepath, false) 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.is_absolute_path(path) return M.path.is_absolute(path) end function M.to_absolute_path(path) if not path or path == "" then return path end @@ -939,16 +912,13 @@ function M.join_paths(...) 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 + result = result == "" and path or M.path.join(result, path) ::continue:: end return M.norm(result) end -function M.path_exists(path) return vim.loop.fs_stat(path) ~= nil end +function M.path_exists(path) return M.path.is_exist(path) end function M.is_first_letter_uppercase(str) return string.match(str, "^[A-Z]") ~= nil end diff --git a/lua/avante/utils/path.lua b/lua/avante/utils/path.lua new file mode 100644 index 0000000..fa1c6ff --- /dev/null +++ b/lua/avante/utils/path.lua @@ -0,0 +1,174 @@ +local OS_NAME = vim.uv.os_uname().sysname ---@type string|nil +local IS_WIN = OS_NAME == "Windows_NT" ---@type boolean + +local SEP = IS_WIN and "\\" or "/" ---@type string + +local BYTE_SLASH = 0x2f ---@type integer '/' +local BYTE_BACKSLASH = 0x5c ---@type integer '\\' +local BYTE_COLON = 0x3a ---@type integer ':' +local BYTE_PATHSEP = string.byte(SEP) ---@type integer + +---@class avante.utils.path +local M = {} + +M.SEP = SEP + +---@return boolean +function M.is_win() return IS_WIN end + +---@param filepath string +---@return string +function M.basename(filepath) + if filepath == "" then return "" end + + local pos_invalid = #filepath + 1 ---@type integer + local pos_sep = 0 ---@type integer + + for i = #filepath, 1, -1 do + local byte = string.byte(filepath, i, i) ---@type integer + if byte == BYTE_SLASH or byte == BYTE_BACKSLASH then + if i + 1 == pos_invalid then + pos_invalid = i + else + pos_sep = i + break + end + end + end + + if pos_sep == 0 and pos_invalid == #filepath + 1 then return filepath end + return string.sub(filepath, pos_sep + 1, pos_invalid - 1) +end + +---@param filepath string +---@return string +function M.dirname(filepath) + local pieces = M.split(filepath) + if #pieces == 1 then + local piece = pieces[1] ---@type string + return piece == "" and string.byte(filepath, 1, 1) == BYTE_SLASH and "/" or piece + end + local dirpath = #pieces > 0 and table.concat(pieces, SEP, 1, #pieces - 1) or "" ---@type string + return dirpath == "" and string.byte(filepath, 1, 1) == BYTE_SLASH and "/" or dirpath +end + +---@param filename string +---@return string +function M.extname(filename) return filename:match("%.[^.]+$") or "" end + +---@param filepath string +---@return boolean +function M.is_absolute(filepath) + if IS_WIN then return #filepath > 1 and string.byte(filepath, 2, 2) == BYTE_COLON end + return string.byte(filepath, 1, 1) == BYTE_PATHSEP +end + +---@param filepath string +---@return boolean +function M.is_exist(filepath) + local stat = vim.uv.fs_stat(filepath) + return stat ~= nil and not vim.tbl_isempty(stat) +end + +---@param dirpath string +---@return boolean +function M.is_exist_dirpath(dirpath) + local stat = vim.uv.fs_stat(dirpath) + return stat ~= nil and stat.type == "directory" +end + +---@param filepath string +---@return boolean +function M.is_exist_filepath(filepath) + local stat = vim.uv.fs_stat(filepath) + return stat ~= nil and stat.type == "file" +end + +---@param from string +---@param to string +---@return string +function M.join(from, to) return M.normalize(from .. SEP .. to) end + +function M.mkdir_if_nonexist(dirpath) + if not M.is_exist(dirpath) then vim.fn.mkdir(dirpath, "p") end +end + +---@param filepath string +---@return string +function M.normalize(filepath) + if filepath == "/" and not IS_WIN then return "/" end + + if filepath == "" then return "." end + + filepath = filepath:gsub("%%(%x%x)", function(hex) return string.char(tonumber(hex, 16)) end) + return table.concat(M.split(filepath), SEP) +end + +---@param from string +---@param to string +---@param prefer_slash boolean +---@return string +function M.relative(from, to, prefer_slash) + local is_from_absolute = M.is_absolute(from) ---@type boolean + local is_to_absolute = M.is_absolute(to) ---@type boolean + + if is_from_absolute and not is_to_absolute then return M.normalize(to) end + + if is_to_absolute and not is_from_absolute then return M.normalize(to) end + + local from_pieces = M.split(from) ---@type string[] + local to_pieces = M.split(to) ---@type string[] + local L = #from_pieces < #to_pieces and #from_pieces or #to_pieces + + local i = 1 + while i <= L do + if from_pieces[i] ~= to_pieces[i] then break end + i = i + 1 + end + + if i == 2 and is_to_absolute then return M.normalize(to) end + + local sep = prefer_slash and "/" or SEP + local p = "" ---@type string + for _ = i, #from_pieces do + p = p .. sep .. ".." ---@type string + end + for j = i, #to_pieces do + p = p .. sep .. to_pieces[j] ---@type string + end + + if p == "" then return "." end + return #p > 1 and string.sub(p, 2) or p +end + +---@param cwd string +---@param to string +function M.resolve(cwd, to) return M.is_absolute(to) and M.normalize(to) or M.normalize(cwd .. SEP .. to) end + +---@param filepath string +---@return string[] +function M.split(filepath) + local pieces = {} ---@type string[] + local pattern = "([^/\\]+)" ---@type string + local has_sep_prefix = SEP == "/" and string.byte(filepath, 1, 1) == BYTE_PATHSEP ---@type boolean + local has_sep_suffix = #filepath > 1 and string.byte(filepath, #filepath, #filepath) == BYTE_PATHSEP ---@type boolean + + if has_sep_prefix then pieces[1] = "" end + + for piece in string.gmatch(filepath, pattern) do + if piece ~= "" and piece ~= "." then + if piece == ".." and (has_sep_prefix or #pieces > 0) then + pieces[#pieces] = nil + else + pieces[#pieces + 1] = piece + end + end + end + + if has_sep_suffix then pieces[#pieces + 1] = "" end + + if IS_WIN and #filepath > 1 and string.byte(filepath, 2, 2) == BYTE_COLON then pieces[1] = pieces[1]:upper() end + return pieces +end + +return M diff --git a/tests/utils/join_paths_spec.lua b/tests/utils/join_paths_spec.lua index b367d86..737fa57 100644 --- a/tests/utils/join_paths_spec.lua +++ b/tests/utils/join_paths_spec.lua @@ -34,9 +34,9 @@ describe("join_paths", function() assert.equals("path" .. utils.path_sep .. "file.lua", result) end) - it("should return empty string when no paths provided", function() + it("should handle no paths provided", function() local result = utils.join_paths() - assert.equals("", result) + assert.equals(".", result) end) it("should return first path when only one path provided", function()