1
0
mirror of https://github.com/SpaceVim/SpaceVim.git synced 2025-01-24 09:40:06 +08:00
SpaceVim/bundle/plenary.nvim/lua/plenary/scandir.lua
2022-05-16 22:20:10 +08:00

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