339 lines
8.4 KiB
Lua
339 lines
8.4 KiB
Lua
local has_notify, notify = pcall(require, "notify")
|
|
|
|
local a = vim.api
|
|
local uv = vim.loop
|
|
|
|
local Iterator = require "nvim-tree.iterators.node-iterator"
|
|
|
|
local M = {
|
|
debouncers = {},
|
|
}
|
|
|
|
M.is_windows = vim.fn.has "win32" == 1 or vim.fn.has "win32unix" == 1
|
|
|
|
function M.path_to_matching_str(path)
|
|
return path:gsub("(%-)", "(%%-)"):gsub("(%.)", "(%%.)"):gsub("(%_)", "(%%_)")
|
|
end
|
|
|
|
function M.warn(msg)
|
|
vim.schedule(function()
|
|
if has_notify then
|
|
notify(msg, vim.log.levels.WARN, { title = "NvimTree" })
|
|
else
|
|
vim.notify("[NvimTree] " .. msg, vim.log.levels.WARN)
|
|
end
|
|
end)
|
|
end
|
|
|
|
function M.str_find(haystack, needle)
|
|
return vim.fn.stridx(haystack, needle) ~= -1
|
|
end
|
|
|
|
function M.read_file(path)
|
|
local fd = uv.fs_open(path, "r", 438)
|
|
if not fd then
|
|
return ""
|
|
end
|
|
local stat = uv.fs_fstat(fd)
|
|
if not stat then
|
|
return ""
|
|
end
|
|
local data = uv.fs_read(fd, stat.size, 0)
|
|
uv.fs_close(fd)
|
|
return data or ""
|
|
end
|
|
|
|
local path_separator = package.config:sub(1, 1)
|
|
function M.path_join(paths)
|
|
return table.concat(vim.tbl_map(M.path_remove_trailing, paths), path_separator)
|
|
end
|
|
|
|
function M.path_split(path)
|
|
return path:gmatch("[^" .. path_separator .. "]+" .. path_separator .. "?")
|
|
end
|
|
|
|
---Get the basename of the given path.
|
|
---@param path string
|
|
---@return string
|
|
function M.path_basename(path)
|
|
path = M.path_remove_trailing(path)
|
|
local i = path:match("^.*()" .. path_separator)
|
|
if not i then
|
|
return path
|
|
end
|
|
return path:sub(i + 1, #path)
|
|
end
|
|
|
|
---Get a path relative to another path.
|
|
---@param path string
|
|
---@param relative_to string
|
|
---@return string
|
|
function M.path_relative(path, relative_to)
|
|
local p, _ = path:gsub("^" .. M.path_to_matching_str(M.path_add_trailing(relative_to)), "")
|
|
return p
|
|
end
|
|
|
|
function M.path_add_trailing(path)
|
|
if path:sub(-1) == path_separator then
|
|
return path
|
|
end
|
|
|
|
return path .. path_separator
|
|
end
|
|
|
|
function M.path_remove_trailing(path)
|
|
local p, _ = path:gsub(path_separator .. "$", "")
|
|
return p
|
|
end
|
|
|
|
M.path_separator = path_separator
|
|
|
|
function M.clear_prompt()
|
|
vim.api.nvim_command "normal! :"
|
|
end
|
|
|
|
function M.get_user_input_char()
|
|
local c = vim.fn.getchar()
|
|
while type(c) ~= "number" do
|
|
c = vim.fn.getchar()
|
|
end
|
|
return vim.fn.nr2char(c)
|
|
end
|
|
|
|
-- get the node and index of the node from the tree that matches the predicate.
|
|
-- The explored nodes are those displayed on the view.
|
|
-- @param nodes list of node
|
|
-- @param fn function(node): boolean
|
|
function M.find_node(nodes, fn)
|
|
local node, i = Iterator.builder(nodes)
|
|
:matcher(fn)
|
|
:recursor(function(node)
|
|
return node.open and #node.nodes > 0 and node.nodes
|
|
end)
|
|
:iterate()
|
|
i = require("nvim-tree.view").is_root_folder_visible() and i or i - 1
|
|
i = require("nvim-tree.live-filter").filter and i + 1 or i
|
|
return node, i
|
|
end
|
|
|
|
-- get the node in the tree state depending on the absolute path of the node
|
|
-- (grouped or hidden too)
|
|
function M.get_node_from_path(path)
|
|
local explorer = require("nvim-tree.core").get_explorer()
|
|
if explorer.absolute_path == path then
|
|
return explorer
|
|
end
|
|
|
|
return Iterator.builder(explorer.nodes)
|
|
:hidden()
|
|
:matcher(function(node)
|
|
return node.absolute_path == path or node.link_to == path
|
|
end)
|
|
:recursor(function(node)
|
|
if node.group_next then
|
|
return { node.group_next }
|
|
end
|
|
if node.nodes then
|
|
return node.nodes
|
|
end
|
|
end)
|
|
:iterate()
|
|
end
|
|
|
|
-- get the highest parent of grouped nodes
|
|
function M.get_parent_of_group(node_)
|
|
local node = node_
|
|
while node.parent and node.parent.group_next do
|
|
node = node.parent
|
|
end
|
|
return node
|
|
end
|
|
|
|
-- return visible nodes indexed by line
|
|
-- @param nodes_all list of node
|
|
-- @param line_start first index
|
|
---@return table
|
|
function M.get_nodes_by_line(nodes_all, line_start)
|
|
local nodes_by_line = {}
|
|
local line = line_start
|
|
|
|
Iterator.builder(nodes_all)
|
|
:applier(function(node)
|
|
nodes_by_line[line] = node
|
|
line = line + 1
|
|
end)
|
|
:recursor(function(node)
|
|
return node.open == true and node.nodes
|
|
end)
|
|
:iterate()
|
|
|
|
return nodes_by_line
|
|
end
|
|
|
|
---Matching executable files in Windows.
|
|
---@param ext string
|
|
---@return boolean
|
|
local PATHEXT = vim.env.PATHEXT or ""
|
|
local wexe = vim.split(PATHEXT:gsub("%.", ""), ";")
|
|
local pathexts = {}
|
|
for _, v in pairs(wexe) do
|
|
pathexts[v] = true
|
|
end
|
|
|
|
function M.is_windows_exe(ext)
|
|
return pathexts[ext:upper()]
|
|
end
|
|
|
|
function M.rename_loaded_buffers(old_path, new_path)
|
|
for _, buf in pairs(a.nvim_list_bufs()) do
|
|
if a.nvim_buf_is_loaded(buf) then
|
|
local buf_name = a.nvim_buf_get_name(buf)
|
|
local exact_match = buf_name == old_path
|
|
local child_match = (
|
|
buf_name:sub(1, #old_path) == old_path and buf_name:sub(#old_path + 1, #old_path + 1) == path_separator
|
|
)
|
|
if exact_match or child_match then
|
|
a.nvim_buf_set_name(buf, new_path .. buf_name:sub(#old_path + 1))
|
|
-- to avoid the 'overwrite existing file' error message on write for
|
|
-- normal files
|
|
if a.nvim_buf_get_option(buf, "buftype") == "" then
|
|
a.nvim_buf_call(buf, function()
|
|
vim.cmd "silent! write!"
|
|
vim.cmd "edit"
|
|
end)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
--- @param path string path to file or directory
|
|
--- @return boolean
|
|
function M.file_exists(path)
|
|
local _, error = vim.loop.fs_stat(path)
|
|
return error == nil
|
|
end
|
|
|
|
--- @param path string
|
|
--- @return string
|
|
function M.canonical_path(path)
|
|
if M.is_windows and path:match "^%a:" then
|
|
return path:sub(1, 1):upper() .. path:sub(2)
|
|
end
|
|
return path
|
|
end
|
|
|
|
-- Create empty sub-tables if not present
|
|
-- @param tbl to create empty inside of
|
|
-- @param path dot separated string of sub-tables
|
|
-- @return deepest sub-table
|
|
function M.table_create_missing(tbl, path)
|
|
if tbl == nil then
|
|
return nil
|
|
end
|
|
|
|
local t = tbl
|
|
for s in string.gmatch(path, "([^%.]+)%.*") do
|
|
if t[s] == nil then
|
|
t[s] = {}
|
|
end
|
|
t = t[s]
|
|
end
|
|
|
|
return t
|
|
end
|
|
|
|
-- Move a value from src to dst if value is nil on dst
|
|
-- @param src to copy from
|
|
-- @param src_path dot separated string of sub-tables
|
|
-- @param src_pos value pos
|
|
-- @param dst to copy to
|
|
-- @param dst_path dot separated string of sub-tables, created when missing
|
|
-- @param dst_pos value pos
|
|
function M.move_missing_val(src, src_path, src_pos, dst, dst_path, dst_pos)
|
|
local ok, err = pcall(vim.validate, {
|
|
src = { src, "table" },
|
|
src_path = { src_path, "string" },
|
|
src_pos = { src_pos, "string" },
|
|
dst = { dst, "table" },
|
|
dst_path = { dst_path, "string" },
|
|
dst_pos = { dst_pos, "string" },
|
|
})
|
|
if not ok then
|
|
M.warn("move_missing_val: " .. (err or "invalid arguments"))
|
|
end
|
|
|
|
for pos in string.gmatch(src_path, "([^%.]+)%.*") do
|
|
if src[pos] and type(src[pos]) == "table" then
|
|
src = src[pos]
|
|
else
|
|
src = nil
|
|
break
|
|
end
|
|
end
|
|
local src_val = src and src[src_pos]
|
|
if src_val == nil then
|
|
return
|
|
end
|
|
|
|
dst = M.table_create_missing(dst, dst_path)
|
|
if dst[dst_pos] == nil then
|
|
dst[dst_pos] = src_val
|
|
end
|
|
|
|
src[src_pos] = nil
|
|
end
|
|
|
|
function M.format_bytes(bytes)
|
|
local units = { "B", "K", "M", "G", "T" }
|
|
|
|
bytes = math.max(bytes, 0)
|
|
local pow = math.floor((bytes and math.log(bytes) or 0) / math.log(1024))
|
|
pow = math.min(pow, #units)
|
|
|
|
local value = bytes / (1024 ^ pow)
|
|
value = math.floor((value * 10) + 0.5) / 10
|
|
|
|
pow = pow + 1
|
|
|
|
return (units[pow] == nil) and (bytes .. "B") or (value .. units[pow])
|
|
end
|
|
|
|
function M.key_by(tbl, key)
|
|
local keyed = {}
|
|
for _, val in ipairs(tbl) do
|
|
keyed[val[key]] = val
|
|
end
|
|
return keyed
|
|
end
|
|
|
|
---Execute callback timeout ms after the lastest invocation with context. Waiting invocations for that context will be discarded. Caller should this ensure that callback performs the same or functionally equivalent actions.
|
|
---@param context string identifies the callback to debounce
|
|
---@param timeout number ms to wait
|
|
---@param callback function to execute on completion
|
|
function M.debounce(context, timeout, callback)
|
|
if M.debouncers[context] then
|
|
pcall(uv.close, M.debouncers[context])
|
|
end
|
|
|
|
M.debouncers[context] = uv.new_timer()
|
|
M.debouncers[context]:start(
|
|
timeout,
|
|
0,
|
|
vim.schedule_wrap(function()
|
|
M.debouncers[context]:close()
|
|
M.debouncers[context] = nil
|
|
callback()
|
|
end)
|
|
)
|
|
end
|
|
|
|
function M.focus_file(path)
|
|
local _, i = M.find_node(require("nvim-tree.core").get_explorer().nodes, function(node)
|
|
return node.absolute_path == path
|
|
end)
|
|
require("nvim-tree.view").set_cursor { i + 1, 1 }
|
|
end
|
|
|
|
return M
|