mirror of
https://github.com/SpaceVim/SpaceVim.git
synced 2025-01-24 09:40:06 +08:00
603 lines
18 KiB
Lua
Vendored
603 lines
18 KiB
Lua
Vendored
local Path = require "plenary.path"
|
|
local os_sep = Path.path.sep
|
|
local F = require "plenary.functional"
|
|
|
|
local uv = vim.loop
|
|
|
|
local m = {}
|
|
|
|
local make_gitignore = function(basepath)
|
|
local patterns = {}
|
|
local valid = false
|
|
for _, v in ipairs(basepath) do
|
|
local p = Path:new(v .. os_sep .. ".gitignore")
|
|
if p:exists() then
|
|
valid = true
|
|
patterns[v] = { ignored = {}, negated = {} }
|
|
for l in p:iter() do
|
|
local prefix = l:sub(1, 1)
|
|
local negated = prefix == "!"
|
|
if negated then
|
|
l = l:sub(2)
|
|
prefix = l:sub(1, 1)
|
|
end
|
|
if prefix == "/" then
|
|
l = v .. l
|
|
end
|
|
if not (prefix == "" or prefix == "#") then
|
|
local el = vim.trim(l)
|
|
el = el:gsub("%-", "%%-")
|
|
el = el:gsub("%.", "%%.")
|
|
el = el:gsub("/%*%*/", "/%%w+/")
|
|
el = el:gsub("%*%*", "")
|
|
el = el:gsub("%*", "%%w+")
|
|
el = el:gsub("%?", "%%w")
|
|
if el ~= "" then
|
|
table.insert(negated and patterns[v].negated or patterns[v].ignored, el)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
if not valid then
|
|
return nil
|
|
end
|
|
return function(bp, entry)
|
|
for _, v in ipairs(bp) do
|
|
if entry:find(v, 1, true) then
|
|
local negated = false
|
|
for _, w in ipairs(patterns[v].ignored) do
|
|
if not negated and entry:match(w) then
|
|
for _, inverse in ipairs(patterns[v].negated) do
|
|
if not negated and entry:match(inverse) then
|
|
negated = true
|
|
end
|
|
end
|
|
if not negated then
|
|
return false
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
return true
|
|
end
|
|
end
|
|
-- exposed for testing
|
|
m.__make_gitignore = make_gitignore
|
|
|
|
local handle_depth = function(base_paths, entry, depth)
|
|
for _, v in ipairs(base_paths) do
|
|
if entry:find(v, 1, true) then
|
|
local cut = entry:sub(#v + 1, -1)
|
|
cut = cut:sub(1, 1) == os_sep and cut:sub(2, -1) or cut
|
|
local _, count = cut:gsub(os_sep, "")
|
|
if depth <= (count + 1) then
|
|
return nil
|
|
end
|
|
end
|
|
end
|
|
return entry
|
|
end
|
|
|
|
local gen_search_pat = function(pattern)
|
|
if type(pattern) == "string" then
|
|
return function(entry)
|
|
return entry:match(pattern)
|
|
end
|
|
elseif type(pattern) == "table" then
|
|
return function(entry)
|
|
for _, v in ipairs(pattern) do
|
|
if entry:match(v) then
|
|
return true
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
elseif type(pattern) == "function" then
|
|
return pattern
|
|
end
|
|
end
|
|
|
|
local process_item = function(opts, name, typ, current_dir, next_dir, bp, data, giti, msp)
|
|
if opts.hidden or name:sub(1, 1) ~= "." then
|
|
if typ == "directory" then
|
|
local entry = current_dir .. os_sep .. name
|
|
if opts.depth then
|
|
table.insert(next_dir, handle_depth(bp, entry, opts.depth))
|
|
else
|
|
table.insert(next_dir, entry)
|
|
end
|
|
if opts.add_dirs or opts.only_dirs then
|
|
if not giti or giti(bp, entry .. "/") then
|
|
if not msp or msp(entry) then
|
|
table.insert(data, entry)
|
|
if opts.on_insert then
|
|
opts.on_insert(entry, typ)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
elseif not opts.only_dirs then
|
|
local entry = current_dir .. os_sep .. name
|
|
if not giti or giti(bp, entry) then
|
|
if not msp or msp(entry) then
|
|
table.insert(data, entry)
|
|
if opts.on_insert then
|
|
opts.on_insert(entry, typ)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
--- m.scan_dir
|
|
-- Search directory recursive and syncronous
|
|
-- @param path: string or table
|
|
-- string has to be a valid path
|
|
-- table has to be a array of valid paths
|
|
-- @param opts: table to change behavior
|
|
-- opts.hidden (bool): if true hidden files will be added
|
|
-- opts.add_dirs (bool): if true dirs will also be added to the results
|
|
-- opts.only_dirs (bool): if true only dirs will be added to the results
|
|
-- opts.respect_gitignore (bool): if true will only add files that are not ignored by the git (uses each gitignore found in path table)
|
|
-- opts.depth (int): depth on how deep the search should go
|
|
-- opts.search_pattern (regex): regex for which files will be added, string, table of strings, or callback (should return bool)
|
|
-- opts.on_insert(entry): Will be called for each element
|
|
-- opts.silent (bool): if true will not echo messages that are not accessible
|
|
-- @return array with files
|
|
m.scan_dir = function(path, opts)
|
|
opts = opts or {}
|
|
|
|
local data = {}
|
|
local base_paths = vim.tbl_flatten { path }
|
|
local next_dir = vim.tbl_flatten { path }
|
|
|
|
local gitignore = opts.respect_gitignore and make_gitignore(base_paths) or nil
|
|
local match_search_pat = opts.search_pattern and gen_search_pat(opts.search_pattern) or nil
|
|
|
|
for i = table.getn(base_paths), 1, -1 do
|
|
if uv.fs_access(base_paths[i], "X") == false then
|
|
if not F.if_nil(opts.silent, false, opts.silent) then
|
|
print(string.format("%s is not accessible by the current user!", base_paths[i]))
|
|
end
|
|
table.remove(base_paths, i)
|
|
end
|
|
end
|
|
if table.getn(base_paths) == 0 then
|
|
return {}
|
|
end
|
|
|
|
repeat
|
|
local current_dir = table.remove(next_dir, 1)
|
|
local fd = uv.fs_scandir(current_dir)
|
|
if fd then
|
|
while true do
|
|
local name, typ = uv.fs_scandir_next(fd)
|
|
if name == nil then
|
|
break
|
|
end
|
|
process_item(opts, name, typ, current_dir, next_dir, base_paths, data, gitignore, match_search_pat)
|
|
end
|
|
end
|
|
until table.getn(next_dir) == 0
|
|
return data
|
|
end
|
|
|
|
--- m.scan_dir_async
|
|
-- Search directory recursive and asyncronous
|
|
-- @param path: string or table
|
|
-- string has to be a valid path
|
|
-- table has to be a array of valid paths
|
|
-- @param opts: table to change behavior
|
|
-- opts.hidden (bool): if true hidden files will be added
|
|
-- opts.add_dirs (bool): if true dirs will also be added to the results
|
|
-- opts.only_dirs (bool): if true only dirs will be added to the results
|
|
-- opts.respect_gitignore (bool): if true will only add files that are not ignored by git
|
|
-- opts.depth (int): depth on how deep the search should go
|
|
-- opts.search_pattern (regex): regex for which files will be added, string, table of strings, or callback (should return bool)
|
|
-- opts.on_insert function(entry): will be called for each element
|
|
-- opts.on_exit function(results): will be called at the end
|
|
-- opts.silent (bool): if true will not echo messages that are not accessible
|
|
m.scan_dir_async = function(path, opts)
|
|
opts = opts or {}
|
|
|
|
local data = {}
|
|
local base_paths = vim.tbl_flatten { path }
|
|
local next_dir = vim.tbl_flatten { path }
|
|
local current_dir = table.remove(next_dir, 1)
|
|
|
|
-- TODO(conni2461): get gitignore is not async
|
|
local gitignore = opts.respect_gitignore and make_gitignore(base_paths) or nil
|
|
local match_search_pat = opts.search_pattern and gen_search_pat(opts.search_pattern) or nil
|
|
|
|
-- TODO(conni2461): is not async. Shouldn't be that big of a problem but still
|
|
-- Maybe obers async pr can take me out of callback hell
|
|
for i = table.getn(base_paths), 1, -1 do
|
|
if uv.fs_access(base_paths[i], "X") == false then
|
|
if not F.if_nil(opts.silent, false, opts.silent) then
|
|
print(string.format("%s is not accessible by the current user!", base_paths[i]))
|
|
end
|
|
table.remove(base_paths, i)
|
|
end
|
|
end
|
|
if table.getn(base_paths) == 0 then
|
|
return {}
|
|
end
|
|
|
|
local read_dir
|
|
read_dir = function(err, fd)
|
|
if not err then
|
|
while true do
|
|
local name, typ = uv.fs_scandir_next(fd)
|
|
if name == nil then
|
|
break
|
|
end
|
|
process_item(opts, name, typ, current_dir, next_dir, base_paths, data, gitignore, match_search_pat)
|
|
end
|
|
if table.getn(next_dir) == 0 then
|
|
if opts.on_exit then
|
|
opts.on_exit(data)
|
|
end
|
|
else
|
|
current_dir = table.remove(next_dir, 1)
|
|
uv.fs_scandir(current_dir, read_dir)
|
|
end
|
|
end
|
|
end
|
|
uv.fs_scandir(current_dir, read_dir)
|
|
end
|
|
|
|
local gen_permissions = (function()
|
|
local conv_to_octal = function(nr)
|
|
local octal, i = 0, 1
|
|
|
|
while nr ~= 0 do
|
|
octal = octal + (nr % 8) * i
|
|
nr = math.floor(nr / 8)
|
|
i = i * 10
|
|
end
|
|
|
|
return octal
|
|
end
|
|
|
|
local type_tbl = { [1] = "p", [2] = "c", [4] = "d", [6] = "b", [10] = ".", [12] = "l", [14] = "s" }
|
|
local permissions_tbl = { [0] = "---", "--x", "-w-", "-wx", "r--", "r-x", "rw-", "rwx" }
|
|
local bit_tbl = { 4, 2, 1 }
|
|
|
|
return function(cache, mode)
|
|
if cache[mode] then
|
|
return cache[mode]
|
|
end
|
|
|
|
local octal = string.format("%6d", conv_to_octal(mode))
|
|
local l4 = octal:sub(#octal - 3, -1)
|
|
local bit = tonumber(l4:sub(1, 1))
|
|
|
|
local result = type_tbl[tonumber(octal:sub(1, 2))] or "-"
|
|
for i = 2, #l4 do
|
|
result = result .. permissions_tbl[tonumber(l4:sub(i, i))]
|
|
if bit - bit_tbl[i - 1] >= 0 then
|
|
result = result:sub(1, -2) .. (bit_tbl[i - 1] == 1 and "T" or "S")
|
|
bit = bit - bit_tbl[i - 1]
|
|
end
|
|
end
|
|
|
|
cache[mode] = result
|
|
return result
|
|
end
|
|
end)()
|
|
|
|
local gen_size = (function()
|
|
local size_types = { "", "K", "M", "G", "T", "P", "E", "Z" }
|
|
|
|
return function(size)
|
|
-- TODO(conni2461): If type directory we could just return 4.0K
|
|
for _, v in ipairs(size_types) do
|
|
if math.abs(size) < 1024.0 then
|
|
if math.abs(size) > 9 then
|
|
return string.format("%3d%s", size, v)
|
|
else
|
|
return string.format("%3.1f%s", size, v)
|
|
end
|
|
end
|
|
size = size / 1024.0
|
|
end
|
|
return string.format("%.1f%s", size, "Y")
|
|
end
|
|
end)()
|
|
|
|
local gen_date = (function()
|
|
local current_year = os.date "%Y"
|
|
return function(mtime)
|
|
if current_year ~= os.date("%Y", mtime) then
|
|
return os.date("%b %d %Y", mtime)
|
|
end
|
|
return os.date("%b %d %H:%M", mtime)
|
|
end
|
|
end)()
|
|
|
|
local get_username = (function()
|
|
if jit and os_sep ~= "\\" then
|
|
local ffi = require "ffi"
|
|
ffi.cdef [[
|
|
typedef unsigned int __uid_t;
|
|
typedef __uid_t uid_t;
|
|
typedef unsigned int __gid_t;
|
|
typedef __gid_t gid_t;
|
|
|
|
typedef struct {
|
|
char *pw_name;
|
|
char *pw_passwd;
|
|
__uid_t pw_uid;
|
|
__gid_t pw_gid;
|
|
char *pw_gecos;
|
|
char *pw_dir;
|
|
char *pw_shell;
|
|
} passwd;
|
|
|
|
passwd *getpwuid(uid_t uid);
|
|
]]
|
|
|
|
return function(tbl, id)
|
|
if tbl[id] then
|
|
return tbl[id]
|
|
end
|
|
local struct = ffi.C.getpwuid(id)
|
|
local name
|
|
if struct == nil then
|
|
name = tostring(id)
|
|
else
|
|
name = ffi.string(struct.pw_name)
|
|
end
|
|
tbl[id] = name
|
|
return name
|
|
end
|
|
else
|
|
return function(tbl, id)
|
|
if not tbl then
|
|
return id
|
|
end
|
|
if tbl[id] then
|
|
return tbl[id]
|
|
end
|
|
tbl[id] = tostring(id)
|
|
return id
|
|
end
|
|
end
|
|
end)()
|
|
|
|
local get_groupname = (function()
|
|
if jit and os_sep ~= "\\" then
|
|
local ffi = require "ffi"
|
|
ffi.cdef [[
|
|
typedef unsigned int __gid_t;
|
|
typedef __gid_t gid_t;
|
|
|
|
typedef struct {
|
|
char *gr_name;
|
|
char *gr_passwd;
|
|
__gid_t gr_gid;
|
|
char **gr_mem;
|
|
} group;
|
|
group *getgrgid(gid_t gid);
|
|
]]
|
|
|
|
return function(tbl, id)
|
|
if tbl[id] then
|
|
return tbl[id]
|
|
end
|
|
local struct = ffi.C.getgrgid(id)
|
|
local name
|
|
if struct == nil then
|
|
name = tostring(id)
|
|
else
|
|
name = ffi.string(struct.gr_name)
|
|
end
|
|
tbl[id] = name
|
|
return name
|
|
end
|
|
else
|
|
return function(tbl, id)
|
|
if not tbl then
|
|
return id
|
|
end
|
|
if tbl[id] then
|
|
return tbl[id]
|
|
end
|
|
tbl[id] = tostring(id)
|
|
return id
|
|
end
|
|
end
|
|
end)()
|
|
|
|
local get_max_len = function(tbl)
|
|
if not tbl then
|
|
return 0
|
|
end
|
|
local max_len = 0
|
|
for _, v in pairs(tbl) do
|
|
if #v > max_len then
|
|
max_len = #v
|
|
end
|
|
end
|
|
return max_len
|
|
end
|
|
|
|
local gen_ls = function(data, path, opts)
|
|
if not data or table.getn(data) == 0 then
|
|
return {}, {}
|
|
end
|
|
|
|
local check_link = function(per, file)
|
|
if per:sub(1, 1) == "l" then
|
|
local resolved = uv.fs_realpath(path .. os_sep .. file)
|
|
if not resolved then
|
|
return file
|
|
end
|
|
if resolved:sub(1, #path) == path then
|
|
resolved = resolved:sub(#path + 2, -1)
|
|
end
|
|
return string.format("%s -> %s", file, resolved)
|
|
end
|
|
return file
|
|
end
|
|
|
|
local results, sections = {}, {}
|
|
|
|
local users_tbl = os_sep ~= "\\" and {} or nil
|
|
local groups_tbl = os_sep ~= "\\" and {} or nil
|
|
|
|
local stats, permissions_cache = {}, {}
|
|
for _, v in ipairs(data) do
|
|
local stat = uv.fs_lstat(v)
|
|
if stat then
|
|
stats[v] = stat
|
|
get_username(users_tbl, stat.uid)
|
|
get_groupname(groups_tbl, stat.gid)
|
|
end
|
|
end
|
|
|
|
local insert_in_results = (function()
|
|
if not users_tbl and not groups_tbl then
|
|
local section_spacing_tbl = { [5] = 2, [6] = 0 }
|
|
|
|
return function(...)
|
|
local args = { ... }
|
|
local section = {
|
|
{ start_index = 01, end_index = 11 }, -- permissions, hardcoded indexes
|
|
{ start_index = 12, end_index = 17 }, -- size, hardcoded indexes
|
|
}
|
|
local cur_index = 19
|
|
for k = 5, 6 do
|
|
local v = section_spacing_tbl[k]
|
|
local end_index = cur_index + #args[k]
|
|
table.insert(section, { start_index = cur_index, end_index = end_index })
|
|
cur_index = end_index + v
|
|
end
|
|
table.insert(sections, section)
|
|
table.insert(
|
|
results,
|
|
string.format("%10s %5s %s %s", args[1], args[2], args[5], check_link(args[1], args[6]))
|
|
)
|
|
end
|
|
else
|
|
local max_user_len = get_max_len(users_tbl)
|
|
local max_group_len = get_max_len(groups_tbl)
|
|
|
|
local section_spacing_tbl = {
|
|
[3] = { max = max_user_len, add = 1 },
|
|
[4] = { max = max_group_len, add = 2 },
|
|
[5] = { add = 2 },
|
|
[6] = { add = 0 },
|
|
}
|
|
local fmt_str = "%10s %5s %-" .. max_user_len .. "s %-" .. max_group_len .. "s %s %s"
|
|
|
|
return function(...)
|
|
local args = { ... }
|
|
local section = {
|
|
{ start_index = 01, end_index = 11 }, -- permissions, hardcoded indexes
|
|
{ start_index = 12, end_index = 17 }, -- size, hardcoded indexes
|
|
}
|
|
local cur_index = 18
|
|
for k = 3, 6 do
|
|
local v = section_spacing_tbl[k]
|
|
local end_index = cur_index + #args[k]
|
|
table.insert(section, { start_index = cur_index, end_index = end_index })
|
|
if v.max then
|
|
cur_index = cur_index + v.max + v.add
|
|
else
|
|
cur_index = end_index + v.add
|
|
end
|
|
end
|
|
table.insert(sections, section)
|
|
table.insert(
|
|
results,
|
|
string.format(fmt_str, args[1], args[2], args[3], args[4], args[5], check_link(args[1], args[6]))
|
|
)
|
|
end
|
|
end
|
|
end)()
|
|
|
|
for name, stat in pairs(stats) do
|
|
insert_in_results(
|
|
gen_permissions(permissions_cache, stat.mode),
|
|
gen_size(stat.size),
|
|
get_username(users_tbl, stat.uid),
|
|
get_groupname(groups_tbl, stat.gid),
|
|
gen_date(stat.mtime.sec),
|
|
name:sub(#path + 2, -1)
|
|
)
|
|
end
|
|
|
|
if opts and opts.group_directories_first then
|
|
local sorted_results = {}
|
|
local sorted_sections = {}
|
|
for k, v in ipairs(results) do
|
|
if v:sub(1, 1) == "d" then
|
|
table.insert(sorted_results, v)
|
|
table.insert(sorted_sections, sections[k])
|
|
end
|
|
end
|
|
for k, v in ipairs(results) do
|
|
if v:sub(1, 1) ~= "d" then
|
|
table.insert(sorted_results, v)
|
|
table.insert(sorted_sections, sections[k])
|
|
end
|
|
end
|
|
return sorted_results, sorted_sections
|
|
else
|
|
return results, sections
|
|
end
|
|
end
|
|
|
|
--- m.ls
|
|
-- List directory contents. Will always apply --long option. Use scan_dir for without --long
|
|
-- @param path: string
|
|
-- string has to be a valid path
|
|
-- @param opts: table to change behavior
|
|
-- opts.hidden (bool): if true hidden files will be added
|
|
-- opts.add_dirs (bool): if true dirs will also be added to the results, default: true
|
|
-- opts.respect_gitignore (bool): if true will only add files that are not ignored by git
|
|
-- opts.depth (int): depth on how deep the search should go, default: 1
|
|
-- opts.group_directories_first (bool): same as real ls
|
|
-- @return array with formatted output
|
|
m.ls = function(path, opts)
|
|
opts = opts or {}
|
|
opts.depth = opts.depth or 1
|
|
opts.add_dirs = opts.add_dirs or true
|
|
local data = m.scan_dir(path, opts)
|
|
|
|
return gen_ls(data, path, opts)
|
|
end
|
|
|
|
--- m.ls_async
|
|
-- List directory contents. Will always apply --long option. Use scan_dir for without --long
|
|
-- @param path: string
|
|
-- string has to be a valid path
|
|
-- @param opts: table to change behavior
|
|
-- opts.hidden (bool): if true hidden files will be added
|
|
-- opts.add_dirs (bool): if true dirs will also be added to the results, default: true
|
|
-- opts.respect_gitignore (bool): if true will only add files that are not ignored by git
|
|
-- opts.depth (int): depth on how deep the search should go, default: 1
|
|
-- opts.group_directories_first (bool): same as real ls
|
|
-- opts.on_exit function(results): will be called at the end (required)
|
|
m.ls_async = function(path, opts)
|
|
opts = opts or {}
|
|
opts.depth = opts.depth or 1
|
|
opts.add_dirs = opts.add_dirs or true
|
|
|
|
local opts_copy = vim.deepcopy(opts)
|
|
|
|
opts_copy.on_exit = function(data)
|
|
if opts.on_exit then
|
|
opts.on_exit(gen_ls(data, path, opts_copy))
|
|
end
|
|
end
|
|
|
|
m.scan_dir_async(path, opts_copy)
|
|
end
|
|
|
|
return m
|