fix(#2094): fix the path resolving in windows (#2248)

* fix(#2094): fix the path resolving in windows

* fix test case

* tweak test case
This commit is contained in:
guanghechen
2025-06-17 16:55:19 +08:00
committed by GitHub
parent cf87220dff
commit 436a02c355
3 changed files with 184 additions and 40 deletions

View File

@@ -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

174
lua/avante/utils/path.lua Normal file
View File

@@ -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

View File

@@ -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()