mirror of
https://github.com/SpaceVim/SpaceVim.git
synced 2025-02-04 15:50:03 +08:00
449 lines
12 KiB
Lua
449 lines
12 KiB
Lua
|
local uv = require('luv')
|
||
|
local Async = require('cmp_dictionary.kit.Async')
|
||
|
|
||
|
local is_windows = uv.os_uname().sysname:lower() == 'windows'
|
||
|
|
||
|
---@see https://github.com/luvit/luvit/blob/master/deps/fs.lua
|
||
|
local IO = {}
|
||
|
|
||
|
---@class cmp_dictionary.kit.IO.UV.Stat
|
||
|
---@field public dev integer
|
||
|
---@field public mode integer
|
||
|
---@field public nlink integer
|
||
|
---@field public uid integer
|
||
|
---@field public gid integer
|
||
|
---@field public rdev integer
|
||
|
---@field public ino integer
|
||
|
---@field public size integer
|
||
|
---@field public blksize integer
|
||
|
---@field public blocks integer
|
||
|
---@field public flags integer
|
||
|
---@field public gen integer
|
||
|
---@field public atime { sec: integer, nsec: integer }
|
||
|
---@field public mtime { sec: integer, nsec: integer }
|
||
|
---@field public ctime { sec: integer, nsec: integer }
|
||
|
---@field public birthtime { sec: integer, nsec: integer }
|
||
|
---@field public type string
|
||
|
|
||
|
---@enum cmp_dictionary.kit.IO.UV.AccessMode
|
||
|
IO.AccessMode = {
|
||
|
r = 'r',
|
||
|
rs = 'rs',
|
||
|
sr = 'sr',
|
||
|
['r+'] = 'r+',
|
||
|
['rs+'] = 'rs+',
|
||
|
['sr+'] = 'sr+',
|
||
|
w = 'w',
|
||
|
wx = 'wx',
|
||
|
xw = 'xw',
|
||
|
['w+'] = 'w+',
|
||
|
['wx+'] = 'wx+',
|
||
|
['xw+'] = 'xw+',
|
||
|
a = 'a',
|
||
|
ax = 'ax',
|
||
|
xa = 'xa',
|
||
|
['a+'] = 'a+',
|
||
|
['ax+'] = 'ax+',
|
||
|
['xa+'] = 'xa+',
|
||
|
}
|
||
|
|
||
|
---@enum cmp_dictionary.kit.IO.WalkStatus
|
||
|
IO.WalkStatus = {
|
||
|
SkipDir = 1,
|
||
|
Break = 2,
|
||
|
}
|
||
|
|
||
|
---@type fun(path: string): cmp_dictionary.kit.Async.AsyncTask
|
||
|
IO.fs_stat = Async.promisify(uv.fs_stat)
|
||
|
|
||
|
---@type fun(path: string): cmp_dictionary.kit.Async.AsyncTask
|
||
|
IO.fs_unlink = Async.promisify(uv.fs_unlink)
|
||
|
|
||
|
---@type fun(path: string): cmp_dictionary.kit.Async.AsyncTask
|
||
|
IO.fs_rmdir = Async.promisify(uv.fs_rmdir)
|
||
|
|
||
|
---@type fun(path: string, mode: integer): cmp_dictionary.kit.Async.AsyncTask
|
||
|
IO.fs_mkdir = Async.promisify(uv.fs_mkdir)
|
||
|
|
||
|
---@type fun(from: string, to: string, option?: { excl?: boolean, ficlone?: boolean, ficlone_force?: boolean }): cmp_dictionary.kit.Async.AsyncTask
|
||
|
IO.fs_copyfile = Async.promisify(uv.fs_copyfile)
|
||
|
|
||
|
---@type fun(path: string, flags: cmp_dictionary.kit.IO.UV.AccessMode, mode: integer): cmp_dictionary.kit.Async.AsyncTask
|
||
|
IO.fs_open = Async.promisify(uv.fs_open)
|
||
|
|
||
|
---@type fun(fd: userdata): cmp_dictionary.kit.Async.AsyncTask
|
||
|
IO.fs_close = Async.promisify(uv.fs_close)
|
||
|
|
||
|
---@type fun(fd: userdata, chunk_size: integer, offset?: integer): cmp_dictionary.kit.Async.AsyncTask
|
||
|
IO.fs_read = Async.promisify(uv.fs_read)
|
||
|
|
||
|
---@type fun(fd: userdata, content: string, offset?: integer): cmp_dictionary.kit.Async.AsyncTask
|
||
|
IO.fs_write = Async.promisify(uv.fs_write)
|
||
|
|
||
|
---@type fun(fd: userdata, offset: integer): cmp_dictionary.kit.Async.AsyncTask
|
||
|
IO.fs_ftruncate = Async.promisify(uv.fs_ftruncate)
|
||
|
|
||
|
---@type fun(path: string, chunk_size?: integer): cmp_dictionary.kit.Async.AsyncTask
|
||
|
IO.fs_opendir = Async.promisify(uv.fs_opendir, { callback = 2 })
|
||
|
|
||
|
---@type fun(fd: userdata): cmp_dictionary.kit.Async.AsyncTask
|
||
|
IO.fs_closedir = Async.promisify(uv.fs_closedir)
|
||
|
|
||
|
---@type fun(fd: userdata): cmp_dictionary.kit.Async.AsyncTask
|
||
|
IO.fs_readdir = Async.promisify(uv.fs_readdir)
|
||
|
|
||
|
---@type fun(path: string): cmp_dictionary.kit.Async.AsyncTask
|
||
|
IO.fs_scandir = Async.promisify(uv.fs_scandir)
|
||
|
|
||
|
---@type fun(path: string): cmp_dictionary.kit.Async.AsyncTask
|
||
|
IO.fs_realpath = Async.promisify(uv.fs_realpath)
|
||
|
|
||
|
---Return if the path is directory.
|
||
|
---@param path string
|
||
|
---@return cmp_dictionary.kit.Async.AsyncTask
|
||
|
function IO.is_directory(path)
|
||
|
path = IO.normalize(path)
|
||
|
return Async.run(function()
|
||
|
return IO.fs_stat(path):catch(function()
|
||
|
return {}
|
||
|
end):await().type == 'directory'
|
||
|
end)
|
||
|
end
|
||
|
|
||
|
---Read file.
|
||
|
---@param path string
|
||
|
---@param chunk_size? integer
|
||
|
---@return cmp_dictionary.kit.Async.AsyncTask
|
||
|
function IO.read_file(path, chunk_size)
|
||
|
chunk_size = chunk_size or 1024
|
||
|
return Async.run(function()
|
||
|
local stat = IO.fs_stat(path):await()
|
||
|
local fd = IO.fs_open(path, IO.AccessMode.r, tonumber('755', 8)):await()
|
||
|
local ok, res = pcall(function()
|
||
|
local chunks = {}
|
||
|
local offset = 0
|
||
|
while offset < stat.size do
|
||
|
local chunk = IO.fs_read(fd, math.min(chunk_size, stat.size - offset), offset):await()
|
||
|
if not chunk then
|
||
|
break
|
||
|
end
|
||
|
table.insert(chunks, chunk)
|
||
|
offset = offset + #chunk
|
||
|
end
|
||
|
return table.concat(chunks, ''):sub(1, stat.size - 1) -- remove EOF.
|
||
|
end)
|
||
|
IO.fs_close(fd):await()
|
||
|
if not ok then
|
||
|
error(res)
|
||
|
end
|
||
|
return res
|
||
|
end)
|
||
|
end
|
||
|
|
||
|
---Write file.
|
||
|
---@param path string
|
||
|
---@param content string
|
||
|
---@param chunk_size? integer
|
||
|
function IO.write_file(path, content, chunk_size)
|
||
|
chunk_size = chunk_size or 1024
|
||
|
content = content .. '\n' -- add EOF.
|
||
|
return Async.run(function()
|
||
|
local fd = IO.fs_open(path, IO.AccessMode.w, tonumber('755', 8)):await()
|
||
|
local ok, err = pcall(function()
|
||
|
local offset = 0
|
||
|
while offset < #content do
|
||
|
local chunk = content:sub(offset + 1, offset + chunk_size)
|
||
|
offset = offset + IO.fs_write(fd, chunk, offset):await()
|
||
|
end
|
||
|
IO.fs_ftruncate(fd, offset):await()
|
||
|
end)
|
||
|
IO.fs_close(fd):await()
|
||
|
if not ok then
|
||
|
error(err)
|
||
|
end
|
||
|
end)
|
||
|
end
|
||
|
|
||
|
---Create directory.
|
||
|
---@param path string
|
||
|
---@param mode integer
|
||
|
---@param option? { recursive?: boolean }
|
||
|
function IO.mkdir(path, mode, option)
|
||
|
path = IO.normalize(path)
|
||
|
option = option or {}
|
||
|
option.recursive = option.recursive or false
|
||
|
return Async.run(function()
|
||
|
if not option.recursive then
|
||
|
IO.fs_mkdir(path, mode):await()
|
||
|
else
|
||
|
local not_exists = {}
|
||
|
local current = path
|
||
|
while current ~= '/' do
|
||
|
local stat = IO.fs_stat(current):catch(function() end):await()
|
||
|
if stat then
|
||
|
break
|
||
|
end
|
||
|
table.insert(not_exists, 1, current)
|
||
|
current = IO.dirname(current)
|
||
|
end
|
||
|
for _, dir in ipairs(not_exists) do
|
||
|
IO.fs_mkdir(dir, mode):await()
|
||
|
end
|
||
|
end
|
||
|
end)
|
||
|
end
|
||
|
|
||
|
---Remove file or directory.
|
||
|
---@param start_path string
|
||
|
---@param option? { recursive?: boolean }
|
||
|
function IO.rm(start_path, option)
|
||
|
start_path = IO.normalize(start_path)
|
||
|
option = option or {}
|
||
|
option.recursive = option.recursive or false
|
||
|
return Async.run(function()
|
||
|
local stat = IO.fs_stat(start_path):await()
|
||
|
if stat.type == 'directory' then
|
||
|
local children = IO.scandir(start_path):await()
|
||
|
if not option.recursive and #children > 0 then
|
||
|
error(('IO.rm: `%s` is a directory and not empty.'):format(start_path))
|
||
|
end
|
||
|
IO.walk(start_path, function(err, entry)
|
||
|
if err then
|
||
|
error('IO.rm: ' .. tostring(err))
|
||
|
end
|
||
|
if entry.type == 'directory' then
|
||
|
IO.fs_rmdir(entry.path):await()
|
||
|
else
|
||
|
IO.fs_unlink(entry.path):await()
|
||
|
end
|
||
|
end, { postorder = true }):await()
|
||
|
else
|
||
|
IO.fs_unlink(start_path):await()
|
||
|
end
|
||
|
end)
|
||
|
end
|
||
|
|
||
|
---Copy file or directory.
|
||
|
---@param from any
|
||
|
---@param to any
|
||
|
---@param option? { recursive?: boolean }
|
||
|
---@return cmp_dictionary.kit.Async.AsyncTask
|
||
|
function IO.cp(from, to, option)
|
||
|
from = IO.normalize(from)
|
||
|
to = IO.normalize(to)
|
||
|
option = option or {}
|
||
|
option.recursive = option.recursive or false
|
||
|
return Async.run(function()
|
||
|
local stat = IO.fs_stat(from):await()
|
||
|
if stat.type == 'directory' then
|
||
|
if not option.recursive then
|
||
|
error(('IO.cp: `%s` is a directory.'):format(from))
|
||
|
end
|
||
|
IO.walk(from, function(err, entry)
|
||
|
if err then
|
||
|
error('IO.cp: ' .. tostring(err))
|
||
|
end
|
||
|
local new_path = entry.path:gsub(vim.pesc(from), to)
|
||
|
if entry.type == 'directory' then
|
||
|
IO.mkdir(new_path, tonumber(stat.mode, 10), { recursive = true }):await()
|
||
|
else
|
||
|
IO.fs_copyfile(entry.path, new_path):await()
|
||
|
end
|
||
|
end):await()
|
||
|
else
|
||
|
IO.fs_copyfile(from, to):await()
|
||
|
end
|
||
|
end)
|
||
|
end
|
||
|
|
||
|
---Walk directory entries recursively.
|
||
|
---@param start_path string
|
||
|
---@param callback fun(err: string|nil, entry: { path: string, type: string }): cmp_dictionary.kit.IO.WalkStatus?
|
||
|
---@param option? { postorder?: boolean }
|
||
|
function IO.walk(start_path, callback, option)
|
||
|
start_path = IO.normalize(start_path)
|
||
|
option = option or {}
|
||
|
option.postorder = option.postorder or false
|
||
|
return Async.run(function()
|
||
|
local function walk_pre(dir)
|
||
|
local ok, iter_entries = pcall(function()
|
||
|
return IO.iter_scandir(dir.path):await()
|
||
|
end)
|
||
|
if not ok then
|
||
|
return callback(iter_entries, dir)
|
||
|
end
|
||
|
local status = callback(nil, dir)
|
||
|
if status == IO.WalkStatus.SkipDir then
|
||
|
return
|
||
|
elseif status == IO.WalkStatus.Break then
|
||
|
return status
|
||
|
end
|
||
|
for entry in iter_entries do
|
||
|
if entry.type == 'directory' then
|
||
|
if walk_pre(entry) == IO.WalkStatus.Break then
|
||
|
return IO.WalkStatus.Break
|
||
|
end
|
||
|
else
|
||
|
if callback(nil, entry) == IO.WalkStatus.Break then
|
||
|
return IO.WalkStatus.Break
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
|
||
|
local function walk_post(dir)
|
||
|
local ok, iter_entries = pcall(function()
|
||
|
return IO.iter_scandir(dir.path):await()
|
||
|
end)
|
||
|
if not ok then
|
||
|
return callback(iter_entries, dir)
|
||
|
end
|
||
|
for entry in iter_entries do
|
||
|
if entry.type == 'directory' then
|
||
|
if walk_post(entry) == IO.WalkStatus.Break then
|
||
|
return IO.WalkStatus.Break
|
||
|
end
|
||
|
else
|
||
|
if callback(nil, entry) == IO.WalkStatus.Break then
|
||
|
return IO.WalkStatus.Break
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
return callback(nil, dir)
|
||
|
end
|
||
|
|
||
|
if not IO.is_directory(start_path) then
|
||
|
error(('IO.walk: `%s` is not a directory.'):format(start_path))
|
||
|
end
|
||
|
if option.postorder then
|
||
|
walk_post({ path = start_path, type = 'directory' })
|
||
|
else
|
||
|
walk_pre({ path = start_path, type = 'directory' })
|
||
|
end
|
||
|
end)
|
||
|
end
|
||
|
|
||
|
---Scan directory entries.
|
||
|
---@param path string
|
||
|
---@return cmp_dictionary.kit.Async.AsyncTask
|
||
|
function IO.scandir(path)
|
||
|
path = IO.normalize(path)
|
||
|
return Async.run(function()
|
||
|
local fd = IO.fs_scandir(path):await()
|
||
|
local entries = {}
|
||
|
while true do
|
||
|
local name, type = uv.fs_scandir_next(fd)
|
||
|
if not name then
|
||
|
break
|
||
|
end
|
||
|
table.insert(entries, {
|
||
|
type = type,
|
||
|
path = IO.join(path, name),
|
||
|
})
|
||
|
end
|
||
|
return entries
|
||
|
end)
|
||
|
end
|
||
|
|
||
|
---Scan directory entries.
|
||
|
---@param path any
|
||
|
---@return cmp_dictionary.kit.Async.AsyncTask
|
||
|
function IO.iter_scandir(path)
|
||
|
path = IO.normalize(path)
|
||
|
return Async.run(function()
|
||
|
local fd = IO.fs_scandir(path):await()
|
||
|
return function()
|
||
|
local name, type = uv.fs_scandir_next(fd)
|
||
|
if name then
|
||
|
return {
|
||
|
type = type,
|
||
|
path = IO.join(path, name),
|
||
|
}
|
||
|
end
|
||
|
end
|
||
|
end)
|
||
|
end
|
||
|
|
||
|
---Return normalized path.
|
||
|
---@param path string
|
||
|
---@return string
|
||
|
function IO.normalize(path)
|
||
|
if is_windows then
|
||
|
path = path:gsub('\\', '/')
|
||
|
end
|
||
|
|
||
|
-- remove trailing slash.
|
||
|
if path:sub(-1) == '/' then
|
||
|
path = path:sub(1, -2)
|
||
|
end
|
||
|
|
||
|
-- skip if the path already absolute.
|
||
|
if IO.is_absolute(path) then
|
||
|
return path
|
||
|
end
|
||
|
|
||
|
-- homedir.
|
||
|
if path:sub(1, 1) == '~' then
|
||
|
path = IO.join(uv.os_homedir(), path:sub(2))
|
||
|
end
|
||
|
|
||
|
-- absolute.
|
||
|
if path:sub(1, 1) == '/' then
|
||
|
return path:sub(-1) == '/' and path:sub(1, -2) or path
|
||
|
end
|
||
|
|
||
|
-- resolve relative path.
|
||
|
local up = uv.cwd()
|
||
|
up = up:sub(-1) == '/' and up:sub(1, -2) or up
|
||
|
while true do
|
||
|
if path:sub(1, 3) == '../' then
|
||
|
path = path:sub(4)
|
||
|
up = IO.dirname(up)
|
||
|
elseif path:sub(1, 2) == './' then
|
||
|
path = path:sub(3)
|
||
|
else
|
||
|
break
|
||
|
end
|
||
|
end
|
||
|
return IO.join(up, path)
|
||
|
end
|
||
|
|
||
|
---Join the paths.
|
||
|
---@param base string
|
||
|
---@param path string
|
||
|
---@return string
|
||
|
function IO.join(base, path)
|
||
|
if base:sub(-1) == '/' then
|
||
|
base = base:sub(1, -2)
|
||
|
end
|
||
|
return base .. '/' .. path
|
||
|
end
|
||
|
|
||
|
---Return the path of the current working directory.
|
||
|
---@param path string
|
||
|
---@return string
|
||
|
function IO.dirname(path)
|
||
|
if path:sub(-1) == '/' then
|
||
|
path = path:sub(1, -2)
|
||
|
end
|
||
|
return (path:gsub('/[^/]+$', ''))
|
||
|
end
|
||
|
|
||
|
if is_windows then
|
||
|
---Return the path is absolute or not.
|
||
|
---@param path string
|
||
|
---@return boolean
|
||
|
function IO.is_absolute(path)
|
||
|
return path:sub(1, 1) == '/' or path:match('^%a://')
|
||
|
end
|
||
|
else
|
||
|
---Return the path is absolute or not.
|
||
|
---@param path string
|
||
|
---@return boolean
|
||
|
function IO.is_absolute(path)
|
||
|
return path:sub(1, 1) == '/'
|
||
|
end
|
||
|
end
|
||
|
|
||
|
return IO
|