nixos/lua-lsp/script/fs-utility.lua

664 lines
16 KiB
Lua

local fs = require 'bee.filesystem'
local platform = require 'bee.platform'
local type = type
local ioOpen = io.open
local pcall = pcall
local pairs = pairs
local setmetatable = setmetatable
local next = next
local ipairs = ipairs
local tostring = tostring
local tableSort = table.sort
_ENV = nil
---@class fs-utility
local m = {}
--- 读取文件
---@param path string|fs.path
function m.loadFile(path, keepBom)
if type(path) ~= 'string' then
---@diagnostic disable-next-line: undefined-field
path = path:string()
end
local f, e = ioOpen(path, 'rb')
if not f then
return nil, e
end
local text = f:read 'a'
f:close()
if not keepBom then
if text:sub(1, 3) == '\xEF\xBB\xBF' then
return text:sub(4)
end
if text:sub(1, 2) == '\xFF\xFE'
or text:sub(1, 2) == '\xFE\xFF' then
return text:sub(3)
end
end
return text
end
--- 写入文件
---@param path any
---@param content string
function m.saveFile(path, content)
if type(path) ~= 'string' then
---@diagnostic disable-next-line: undefined-field
path = path:string()
end
local f, e = ioOpen(path, "wb")
if f then
f:write(content)
f:close()
return true
else
return false, e
end
end
function m.relative(path, base)
local sPath = fs.absolute(path):string()
local sBase = fs.absolute(base):string()
--TODO 先只支持最简单的情况
if sPath:sub(1, #sBase) == sBase
and sPath:sub(#sBase + 1, #sBase + 1):match '^[/\\]' then
return fs.path(sPath:sub(#sBase + 1):gsub('^[/\\]+', ''))
end
return nil
end
local function buildOption(option)
option = option or {}
option.add = option.add or {}
option.del = option.del or {}
option.mod = option.mod or {}
option.err = option.err or {}
return option
end
local function split(str, sep)
local t = {}
local current = 1
while current <= #str do
local s, e = str:find(sep, current)
if not s then
t[#t+1] = str:sub(current)
break
end
if s > 1 then
t[#t+1] = str:sub(current, s - 1)
end
current = e + 1
end
return t
end
---@class dummyfs
---@operator div(string|fs.path|dummyfs): dummyfs
---@field files table
local dfs = {}
dfs.__index = dfs
dfs.type = 'dummy'
dfs.path = ''
---@return dummyfs
function m.dummyFS(t)
return setmetatable({
files = t or {},
}, dfs)
end
function dfs:__tostring()
return 'dummy:' .. tostring(self.path)
end
function dfs:__div(filename)
if type(filename) ~= 'string' then
filename = filename:string()
end
local new = m.dummyFS(self.files)
if self.path:sub(-1):match '[^/\\]' then
new.path = self.path .. '\\' .. filename
else
new.path = self.path .. filename
end
return new
end
function dfs:_open(index)
local paths = split(self.path, '[/\\]')
local current = self.files
if not index then
index = #paths
elseif index < 0 then
index = #paths + index + 1
end
for i = 1, index do
local path = paths[i]
if current[path] then
current = current[path]
else
return nil
end
end
return current
end
function dfs:_filename()
return self.path:match '[^/\\]+$'
end
function dfs:parent_path()
local new = m.dummyFS(self.files)
if self.path:find('[/\\]') then
new.path = self.path:gsub('[/\\]+[^/\\]*$', '')
else
new.path = ''
end
return new
end
function dfs:filename()
local new = m.dummyFS(self.files)
new.path = self:_filename()
return new
end
function dfs:string()
return self.path
end
---@return fun(): dummyfs?
function dfs:listDirectory()
local dir = self:_open()
if type(dir) ~= 'table' then
return function () end
end
local keys = {}
for k in pairs(dir) do
keys[#keys+1] = k
end
tableSort(keys)
local i = 0
return function ()
i = i + 1
local k = keys[i]
if not k then
return nil
end
return self / k
end
end
function dfs:isDirectory()
local target = self:_open()
if type(target) == 'table' then
return true
end
return false
end
function dfs:remove()
local dir = self:_open(-2)
local filename = self:_filename()
if not filename then
return
end
dir[filename] = nil
end
function dfs:exists()
local target = self:_open()
return target ~= nil
end
function dfs:createDirectories(path)
if not path then
return false
end
if type(path) ~= 'string' then
path = path:string()
end
local paths = split(path, '[/\\]+')
local current = self.files
for i = 1, #paths do
local sub = paths[i]
if current[sub] then
if type(current[sub]) ~= 'table' then
return false
end
else
current[sub] = {}
end
current = current[sub]
end
return true
end
function dfs:saveFile(path, text)
if not path then
return false, 'no path'
end
if type(path) ~= 'string' then
path = path:string()
end
local temp = m.dummyFS(self.files)
temp.path = path
local dir = temp:_open(-2)
if not dir then
return false, '无法打开:' .. path
end
local filename = temp:_filename()
if not filename then
return false, '无法打开:' .. path
end
if type(dir[filename]) == 'table' then
return false, '无法打开:' .. path
end
dir[filename] = text
return true
end
---@param path string|fs.path|dummyfs
---@param option table
---@return fs.path|dummyfs?
local function fsAbsolute(path, option)
if type(path) == 'string' then
local suc, res = pcall(fs.path, path)
if not suc then
option.err[#option.err+1] = res
return nil
end
path = res
elseif type(path) == 'table' then
return path
end
local suc, res = pcall(fs.absolute, path)
if not suc then
option.err[#option.err+1] = res
return nil
end
return res
end
local function fsIsDirectory(path, option)
if not path then
return false
end
if path.type == 'dummy' then
return path:isDirectory()
end
local status = fs.symlink_status(path):type()
return status == 'directory'
end
---@param path fs.path|dummyfs|nil
---@param option table
---@return fun(): fs.path|dummyfs|nil
local function fsPairs(path, option)
if not path then
return function () end
end
if path.type == 'dummy' then
return path:listDirectory()
end
local suc, res = pcall(fs.pairs, path)
if not suc then
option.err[#option.err+1] = res
return function () end
end
return res
end
local function fsRemove(path, option)
if not path then
return false
end
if path.type == 'dummy' then
return path:remove()
end
local suc, res = pcall(fs.remove, path)
if not suc then
option.err[#option.err+1] = res
end
option.del[#option.del+1] = path:string()
end
local function fsExists(path, option)
if not path then
return false
end
if path.type == 'dummy' then
return path:exists()
end
local suc, res = pcall(fs.exists, path)
if not suc then
option.err[#option.err+1] = res
return false
end
return res
end
local function fsSave(path, text, option)
if not path then
return false
end
if path.type == 'dummy' then
local dir = path:_open(-2)
if not dir then
option.err[#option.err+1] = '无法打开:' .. path:string()
return false
end
local filename = path:_filename()
if not filename then
option.err[#option.err+1] = '无法打开:' .. path:string()
return false
end
if type(dir[filename]) == 'table' then
option.err[#option.err+1] = '无法打开:' .. path:string()
return false
end
dir[filename] = text
else
local suc, err = m.saveFile(path, text)
if suc then
return true
end
option.err[#option.err+1] = err
return false
end
end
local function fsLoad(path, option)
if not path then
return nil
end
if path.type == 'dummy' then
local text = path:_open()
if type(text) == 'string' then
return text
else
option.err[#option.err+1] = '无法打开:' .. path:string()
return nil
end
else
local text, err = m.loadFile(path)
if text then
return text
else
option.err[#option.err+1] = err
return nil
end
end
end
local function fsCopy(source, target, option)
if not source or not target then
return
end
if source.type == 'dummy' then
local sourceText = source:_open()
if not sourceText then
option.err[#option.err+1] = '无法打开:' .. source:string()
return false
end
return fsSave(target, sourceText, option)
else
if target.type == 'dummy' then
local sourceText, err = m.loadFile(source)
if not sourceText then
option.err[#option.err+1] = err
return false
end
return fsSave(target, sourceText, option)
else
local suc, res = pcall(fs.copy_file, source, target, fs.copy_options.overwrite_existing)
if not suc then
option.err[#option.err+1] = res
return false
end
end
end
return true
end
---@param path dummyfs|fs.path
---@param option table
local function fsCreateDirectories(path, option)
if not path then
return
end
if path.type == 'dummy' then
return path:createDirectories()
end
local suc, res = pcall(fs.create_directories, path)
if not suc then
option.err[#option.err+1] = res
return false
end
return true
end
local function fileRemove(path, option)
if not path then
return
end
if option.onRemove and option.onRemove(path) == false then
return
end
if fsIsDirectory(path, option) then
for child in fsPairs(path, option) do
fileRemove(child, option)
end
end
if fsRemove(path, option) then
option.del[#option.del+1] = path:string()
end
end
---@param source fs.path|dummyfs?
---@param target fs.path|dummyfs?
---@param option table
local function fileCopy(source, target, option)
if not source or not target then
return
end
local isDir1 = fsIsDirectory(source, option)
local isDir2 = fsIsDirectory(target, option)
local isExists = fsExists(target, option)
if isDir1 then
if isDir2 or fsCreateDirectories(target, option) then
for filePath in fsPairs(source, option) do
local name = filePath:filename():string()
fileCopy(filePath, target / name, option)
end
end
else
if isExists and not isDir2 then
local buf1 = fsLoad(source, option)
local buf2 = fsLoad(target, option)
if buf1 and buf2 then
if buf1 ~= buf2 then
if fsCopy(source, target, option) then
option.mod[#option.mod+1] = target:string()
end
end
end
else
if fsCopy(source, target, option) then
option.add[#option.add+1] = target:string()
end
end
end
end
---@param source fs.path|dummyfs?
---@param target fs.path|dummyfs?
---@param option table
local function fileSync(source, target, option)
if not source or not target then
return
end
local isDir1 = fsIsDirectory(source, option)
local isDir2 = fsIsDirectory(target, option)
local isExists = fsExists(target, option)
if isDir1 then
if isDir2 then
local fileList = m.fileList()
if type(target) == 'table' then
---@cast target dummyfs
for filePath in target:listDirectory() do
fileList[filePath] = true
end
else
---@cast target fs.path
for filePath in fs.pairs(target) do
fileList[filePath] = true
end
end
for filePath in fsPairs(source, option) do
local name = filePath:filename():string()
local targetPath = target / name
fileSync(filePath, targetPath, option)
fileList[targetPath] = nil
end
for path in pairs(fileList) do
fileRemove(path, option)
end
else
if isExists then
fileRemove(target, option)
end
if fsCreateDirectories(target, option) then
for filePath in fsPairs(source, option) do
local name = filePath:filename():string()
fileCopy(filePath, target / name, option)
end
end
end
else
if isDir2 then
fileRemove(target, option)
end
if isExists then
local buf1 = fsLoad(source, option)
local buf2 = fsLoad(target, option)
if buf1 and buf2 then
if buf1 ~= buf2 then
if fsCopy(source, target, option) then
option.mod[#option.mod+1] = target:string()
end
end
end
else
if fsCopy(source, target, option) then
option.add[#option.add+1] = target:string()
end
end
end
end
--- 文件列表
function m.fileList(option)
option = option or buildOption(option)
local os = platform.OS
local keyMap = {}
local fileList = {}
local function computeKey(path)
path = fsAbsolute(path, option)
if not path then
return nil
end
return path:string()
end
return setmetatable({}, {
__index = function (_, path)
local key = computeKey(path)
return fileList[key]
end,
__newindex = function (_, path, value)
local key = computeKey(path)
if not key then
return
end
if value == nil then
keyMap[key] = nil
else
keyMap[key] = path
fileList[key] = value
end
end,
__pairs = function ()
local key, path
return function ()
key, path = next(keyMap, key)
return path, fileList[key]
end
end,
})
end
--- 删除文件(夹)
function m.fileRemove(path, option)
option = buildOption(option)
path = fsAbsolute(path, option)
fileRemove(path, option)
return option
end
--- 复制文件(夹)
---@param source string|fs.path|dummyfs
---@param target string|fs.path|dummyfs
---@return table
function m.fileCopy(source, target, option)
option = buildOption(option)
local fsSource = fsAbsolute(source, option)
local fsTarget = fsAbsolute(target, option)
fileCopy(fsSource, fsTarget, option)
return option
end
--- 同步文件(夹)
---@param source string|fs.path|dummyfs
---@param target string|fs.path|dummyfs
---@return table
function m.fileSync(source, target, option)
option = buildOption(option)
local fsSource = fsAbsolute(source, option)
local fsTarget = fsAbsolute(target, option)
fileSync(fsSource, fsTarget, option)
return option
end
---@param dir fs.path
---@param callback fun(fullPath: fs.path)
function m.scanDirectory(dir, callback)
for fullpath in fs.pairs(dir) do
local status = fs.symlink_status(fullpath):type()
if status == 'directory' then
m.scanDirectory(fullpath, callback)
elseif status == 'regular' then
callback(fullpath)
end
end
end
function m.listDirectory(dir)
if dir.type == 'dummy' then
return dir:listDirectory()
else
return fs.pairs(dir)
end
end
return m