Files
avante.nvim/lua/avante/utils/path.lua
guanghechen 436a02c355 fix(#2094): fix the path resolving in windows (#2248)
* fix(#2094): fix the path resolving in windows

* fix test case

* tweak test case
2025-06-17 16:55:19 +08:00

175 lines
5.3 KiB
Lua

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