fixes #673. Not sure why this check was added in the first place, but some testing made me realize maybe it wasn't useful.
589 lines
14 KiB
Lua
589 lines
14 KiB
Lua
local api = vim.api
|
|
local luv = vim.loop
|
|
|
|
local renderer = require'nvim-tree.renderer'
|
|
local config = require'nvim-tree.config'
|
|
local git = require'nvim-tree.git'
|
|
local diagnostics = require'nvim-tree.diagnostics'
|
|
local pops = require'nvim-tree.populate'
|
|
local utils = require'nvim-tree.utils'
|
|
local view = require'nvim-tree.view'
|
|
local events = require'nvim-tree.events'
|
|
local populate = pops.populate
|
|
local refresh_entries = pops.refresh_entries
|
|
|
|
local first_init_done = false
|
|
|
|
local M = {}
|
|
|
|
M.Tree = {
|
|
entries = {},
|
|
cwd = nil,
|
|
loaded = false,
|
|
target_winid = nil,
|
|
}
|
|
|
|
function M.init(with_open, with_reload)
|
|
M.Tree.entries = {}
|
|
if not M.Tree.cwd then
|
|
M.Tree.cwd = luv.cwd()
|
|
end
|
|
if config.use_git() then
|
|
git.git_root(M.Tree.cwd)
|
|
end
|
|
populate(M.Tree.entries, M.Tree.cwd)
|
|
|
|
local stat = luv.fs_stat(M.Tree.cwd)
|
|
M.Tree.last_modified = stat.mtime.sec
|
|
|
|
if with_open then
|
|
M.open()
|
|
elseif view.win_open() then
|
|
M.refresh_tree()
|
|
end
|
|
|
|
if with_reload then
|
|
renderer.draw(M.Tree, true)
|
|
M.Tree.loaded = true
|
|
end
|
|
|
|
if not first_init_done then
|
|
events._dispatch_ready()
|
|
first_init_done = true
|
|
end
|
|
end
|
|
|
|
function M.redraw()
|
|
renderer.draw(M.Tree, true)
|
|
end
|
|
|
|
local function get_node_at_line(line)
|
|
local index = 2
|
|
local function iter(entries)
|
|
for _, node in ipairs(entries) do
|
|
if index == line then
|
|
return node
|
|
end
|
|
index = index + 1
|
|
if node.open == true then
|
|
local child = iter(node.entries)
|
|
if child ~= nil then return child end
|
|
end
|
|
end
|
|
end
|
|
return iter
|
|
end
|
|
|
|
local function get_line_from_node(node, find_parent)
|
|
local node_path = node.absolute_path
|
|
|
|
if find_parent then
|
|
node_path = node.absolute_path:match("(.*)"..utils.path_separator)
|
|
end
|
|
|
|
local line = 2
|
|
local function iter(entries, recursive)
|
|
for _, entry in ipairs(entries) do
|
|
local n = M.get_last_group_node(entry)
|
|
if node_path:match('^'..n.match_path..'$') ~= nil then
|
|
return line, entry
|
|
end
|
|
|
|
line = line + 1
|
|
if entry.open == true and recursive then
|
|
local _, child = iter(entry.entries, recursive)
|
|
if child ~= nil then return line, child end
|
|
end
|
|
end
|
|
end
|
|
return iter
|
|
end
|
|
|
|
function M.get_node_at_cursor()
|
|
local winnr = view.get_winnr()
|
|
if not winnr then
|
|
return
|
|
end
|
|
local cursor = api.nvim_win_get_cursor(view.get_winnr())
|
|
local line = cursor[1]
|
|
if view.is_help_ui() then
|
|
local help_lines, _ = renderer.draw_help()
|
|
local help_text = get_node_at_line(line+1)(help_lines)
|
|
return {name = help_text}
|
|
else
|
|
if line == 1 and M.Tree.cwd ~= "/" then
|
|
return { name = ".." }
|
|
end
|
|
|
|
if M.Tree.cwd == "/" then
|
|
line = line + 1
|
|
end
|
|
return get_node_at_line(line)(M.Tree.entries)
|
|
end
|
|
end
|
|
|
|
-- If node is grouped, return the last node in the group. Otherwise, return the given node.
|
|
function M.get_last_group_node(node)
|
|
local next = node
|
|
while next.group_next do
|
|
next = next.group_next
|
|
end
|
|
return next
|
|
end
|
|
|
|
function M.unroll_dir(node)
|
|
node.open = not node.open
|
|
if node.has_children then node.has_children = false end
|
|
if #node.entries > 0 then
|
|
renderer.draw(M.Tree, true)
|
|
else
|
|
if config.use_git() then
|
|
git.git_root(node.absolute_path)
|
|
end
|
|
populate(node.entries, node.link_to or node.absolute_path, node)
|
|
|
|
renderer.draw(M.Tree, true)
|
|
end
|
|
|
|
diagnostics.update()
|
|
end
|
|
|
|
local function refresh_git(node)
|
|
if not node then node = M.Tree end
|
|
git.update_status(node.entries, node.absolute_path or node.cwd, node, false)
|
|
for _, entry in pairs(node.entries) do
|
|
if entry.entries and #entry.entries > 0 then
|
|
refresh_git(entry)
|
|
end
|
|
end
|
|
end
|
|
|
|
-- TODO update only entries where directory has changed
|
|
local function refresh_nodes(node)
|
|
refresh_entries(node.entries, node.absolute_path or node.cwd, node)
|
|
for _, entry in ipairs(node.entries) do
|
|
if entry.entries and entry.open then
|
|
refresh_nodes(entry)
|
|
end
|
|
end
|
|
end
|
|
|
|
-- this variable is used to bufferize the refresh actions
|
|
-- so only one happens every second at most
|
|
local refreshing = false
|
|
|
|
function M.refresh_tree(disable_clock)
|
|
if not M.Tree.cwd or (not disable_clock and refreshing) or vim.v.exiting ~= vim.NIL then
|
|
return
|
|
end
|
|
refreshing = true
|
|
|
|
refresh_nodes(M.Tree)
|
|
|
|
local use_git = config.use_git()
|
|
if use_git then
|
|
vim.schedule(function()
|
|
git.reload_roots()
|
|
refresh_git(M.Tree)
|
|
M.redraw()
|
|
end)
|
|
end
|
|
|
|
vim.schedule(diagnostics.update)
|
|
|
|
if view.win_open() then
|
|
renderer.draw(M.Tree, true)
|
|
else
|
|
M.Tree.loaded = false
|
|
end
|
|
|
|
if not disable_clock then
|
|
vim.defer_fn(function() refreshing = false end, vim.g.nvim_tree_refresh_wait or 1000)
|
|
end
|
|
end
|
|
|
|
function M.set_index_and_redraw(fname)
|
|
local i
|
|
if M.Tree.cwd == '/' then
|
|
i = 0
|
|
else
|
|
i = 1
|
|
end
|
|
local reload = false
|
|
|
|
local function iter(entries)
|
|
for _, entry in ipairs(entries) do
|
|
i = i + 1
|
|
if entry.absolute_path == fname then
|
|
return i
|
|
end
|
|
|
|
if fname:match(entry.match_path..utils.path_separator) ~= nil then
|
|
if #entry.entries == 0 then
|
|
reload = true
|
|
populate(entry.entries, entry.absolute_path, entry)
|
|
end
|
|
if entry.open == false then
|
|
reload = true
|
|
entry.open = true
|
|
end
|
|
if iter(entry.entries) ~= nil then
|
|
return i
|
|
end
|
|
elseif entry.open == true then
|
|
iter(entry.entries)
|
|
end
|
|
end
|
|
end
|
|
|
|
local index = iter(M.Tree.entries)
|
|
if not view.win_open() then
|
|
M.Tree.loaded = false
|
|
return
|
|
end
|
|
renderer.draw(M.Tree, reload)
|
|
if index then
|
|
view.set_cursor({index, 0})
|
|
end
|
|
end
|
|
|
|
---Get user to pick a window. Selectable windows are all windows in the current
|
|
---tabpage that aren't NvimTree.
|
|
---@return integer|nil -- If a valid window was picked, return its id. If an
|
|
--- invalid window was picked / user canceled, return nil. If there are
|
|
--- no selectable windows, return -1.
|
|
function M.pick_window()
|
|
local tabpage = api.nvim_get_current_tabpage()
|
|
local win_ids = api.nvim_tabpage_list_wins(tabpage)
|
|
local tree_winid = view.get_winnr(tabpage)
|
|
local exclude = config.window_picker_exclude()
|
|
|
|
local selectable = vim.tbl_filter(function (id)
|
|
local bufid = api.nvim_win_get_buf(id)
|
|
for option, v in pairs(exclude) do
|
|
local ok, option_value = pcall(api.nvim_buf_get_option, bufid, option)
|
|
if ok and vim.tbl_contains(v, option_value) then
|
|
return false
|
|
end
|
|
end
|
|
|
|
local win_config = api.nvim_win_get_config(id)
|
|
return id ~= tree_winid
|
|
and win_config.focusable
|
|
and not win_config.external
|
|
end, win_ids)
|
|
|
|
-- If there are no selectable windows: return. If there's only 1, return it without picking.
|
|
if #selectable == 0 then return -1 end
|
|
if #selectable == 1 then return selectable[1] end
|
|
|
|
local chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"
|
|
if vim.g.nvim_tree_window_picker_chars then
|
|
chars = tostring(vim.g.nvim_tree_window_picker_chars):upper()
|
|
end
|
|
|
|
local i = 1
|
|
local win_opts = {}
|
|
local win_map = {}
|
|
local laststatus = vim.o.laststatus
|
|
vim.o.laststatus = 2
|
|
|
|
-- Setup UI
|
|
for _, id in ipairs(selectable) do
|
|
local char = chars:sub(i, i)
|
|
local ok_status, statusline = pcall(api.nvim_win_get_option, id, "statusline")
|
|
local ok_hl, winhl = pcall(api.nvim_win_get_option, id, "winhl")
|
|
|
|
win_opts[id] = {
|
|
statusline = ok_status and statusline or "",
|
|
winhl = ok_hl and winhl or ""
|
|
}
|
|
win_map[char] = id
|
|
|
|
api.nvim_win_set_option(id, "statusline", "%=" .. char .. "%=")
|
|
api.nvim_win_set_option(
|
|
id, "winhl", "StatusLine:NvimTreeWindowPicker,StatusLineNC:NvimTreeWindowPicker"
|
|
)
|
|
|
|
i = i + 1
|
|
if i > #chars then break end
|
|
end
|
|
|
|
vim.cmd("redraw")
|
|
print("Pick window: ")
|
|
local _, resp = pcall(utils.get_user_input_char)
|
|
resp = (resp or ""):upper()
|
|
utils.clear_prompt()
|
|
|
|
-- Restore window options
|
|
for _, id in ipairs(selectable) do
|
|
for opt, value in pairs(win_opts[id]) do
|
|
api.nvim_win_set_option(id, opt, value)
|
|
end
|
|
end
|
|
|
|
vim.o.laststatus = laststatus
|
|
|
|
return win_map[resp]
|
|
end
|
|
|
|
function M.open_file(mode, filename)
|
|
if mode == "tabnew" then
|
|
M.open_file_in_tab(filename)
|
|
return
|
|
end
|
|
|
|
local tabpage = api.nvim_get_current_tabpage()
|
|
local win_ids = api.nvim_tabpage_list_wins(tabpage)
|
|
|
|
local target_winid
|
|
if vim.g.nvim_tree_disable_window_picker == 1 then
|
|
target_winid = M.Tree.target_winid
|
|
else
|
|
target_winid = M.pick_window()
|
|
end
|
|
|
|
if target_winid == -1 then
|
|
target_winid = M.Tree.target_winid
|
|
elseif target_winid == nil then
|
|
return
|
|
end
|
|
|
|
local do_split = mode == "split" or mode == "vsplit"
|
|
local vertical = mode ~= "split"
|
|
|
|
-- Check if filename is already open in a window
|
|
local found = false
|
|
for _, id in ipairs(win_ids) do
|
|
if filename == api.nvim_buf_get_name(api.nvim_win_get_buf(id)) then
|
|
if mode == "preview" then return end
|
|
found = true
|
|
api.nvim_set_current_win(id)
|
|
break
|
|
end
|
|
end
|
|
|
|
if not found then
|
|
if not target_winid or not vim.tbl_contains(win_ids, target_winid) then
|
|
-- Target is invalid, or window does not exist in current tabpage: create
|
|
-- new window
|
|
local window_opts = config.window_options()
|
|
local splitside = view.is_vertical() and "vsp" or "sp"
|
|
vim.cmd(window_opts.split_command .. " " .. splitside)
|
|
target_winid = api.nvim_get_current_win()
|
|
M.Tree.target_winid = target_winid
|
|
|
|
-- No need to split, as we created a new window.
|
|
do_split = false
|
|
elseif not vim.o.hidden then
|
|
-- If `hidden` is not enabled, check if buffer in target window is
|
|
-- modified, and create new split if it is.
|
|
local target_bufid = api.nvim_win_get_buf(target_winid)
|
|
if api.nvim_buf_get_option(target_bufid, "modified") then
|
|
do_split = true
|
|
end
|
|
end
|
|
|
|
local cmd
|
|
if do_split then
|
|
cmd = string.format("%ssplit ", vertical and "vertical " or "")
|
|
else
|
|
cmd = "edit "
|
|
end
|
|
|
|
cmd = cmd .. vim.fn.fnameescape(filename)
|
|
api.nvim_set_current_win(target_winid)
|
|
vim.cmd(cmd)
|
|
view.resize()
|
|
end
|
|
|
|
if mode == "preview" then
|
|
view.focus()
|
|
return
|
|
end
|
|
|
|
if vim.g.nvim_tree_quit_on_open == 1 then
|
|
view.close()
|
|
end
|
|
|
|
renderer.draw(M.Tree, true)
|
|
end
|
|
|
|
function M.open_file_in_tab(filename)
|
|
local close = vim.g.nvim_tree_quit_on_open == 1
|
|
if close then
|
|
view.close()
|
|
else
|
|
-- Switch window first to ensure new window doesn't inherit settings from
|
|
-- NvimTree
|
|
if M.Tree.target_winid > 0 and api.nvim_win_is_valid(M.Tree.target_winid) then
|
|
api.nvim_set_current_win(M.Tree.target_winid)
|
|
else
|
|
vim.cmd("wincmd p")
|
|
end
|
|
end
|
|
|
|
-- This sequence of commands are here to ensure a number of things: the new
|
|
-- buffer must be opened in the current tabpage first so that focus can be
|
|
-- brought back to the tree if it wasn't quit_on_open. It also ensures that
|
|
-- when we open the new tabpage with the file, its window doesn't inherit
|
|
-- settings from NvimTree, as it was already loaded.
|
|
|
|
vim.cmd("edit " .. vim.fn.fnameescape(filename))
|
|
|
|
local alt_bufid = vim.fn.bufnr("#")
|
|
if alt_bufid ~= -1 then
|
|
api.nvim_set_current_buf(alt_bufid)
|
|
end
|
|
|
|
if not close then
|
|
vim.cmd("wincmd p")
|
|
end
|
|
|
|
vim.cmd("tabe " .. vim.fn.fnameescape(filename))
|
|
end
|
|
|
|
function M.collapse_all()
|
|
local function iter(nodes)
|
|
for _, node in pairs(nodes) do
|
|
if node.open then
|
|
node.open = false
|
|
end
|
|
if node.entries then
|
|
iter(node.entries)
|
|
end
|
|
end
|
|
end
|
|
|
|
iter(M.Tree.entries)
|
|
M.redraw()
|
|
end
|
|
|
|
function M.change_dir(name)
|
|
local foldername = name == '..' and vim.fn.fnamemodify(M.Tree.cwd, ':h') or name
|
|
local no_cwd_change = vim.fn.expand(foldername) == M.Tree.cwd
|
|
if no_cwd_change then
|
|
return
|
|
end
|
|
|
|
vim.cmd('lcd '..foldername)
|
|
M.Tree.cwd = foldername
|
|
M.init(false, true)
|
|
end
|
|
|
|
function M.set_target_win()
|
|
local id = api.nvim_get_current_win()
|
|
local tree_id = view.get_winnr()
|
|
if tree_id and id == tree_id then
|
|
M.Tree.target_winid = 0
|
|
return
|
|
end
|
|
|
|
M.Tree.target_winid = id
|
|
end
|
|
|
|
function M.open()
|
|
M.set_target_win()
|
|
|
|
local cwd = vim.fn.getcwd()
|
|
view.open()
|
|
|
|
local respect_buf_cwd = vim.g.nvim_tree_respect_buf_cwd or 0
|
|
if M.Tree.loaded and (respect_buf_cwd == 1 and cwd ~= M.Tree.cwd) then
|
|
M.change_dir(cwd)
|
|
end
|
|
renderer.draw(M.Tree, not M.Tree.loaded)
|
|
M.Tree.loaded = true
|
|
end
|
|
|
|
function M.sibling(node, direction)
|
|
if not direction then return end
|
|
|
|
local iter = get_line_from_node(node, true)
|
|
local node_path = node.absolute_path
|
|
|
|
local line = 0
|
|
local parent, _
|
|
|
|
-- Check if current node is already at root entries
|
|
for index, entry in ipairs(M.Tree.entries) do
|
|
if node_path:match('^'..entry.match_path..'$') ~= nil then
|
|
line = index
|
|
end
|
|
end
|
|
|
|
if line > 0 then
|
|
parent = M.Tree
|
|
else
|
|
_, parent = iter(M.Tree.entries, true)
|
|
if parent ~= nil and #parent.entries > 1 then
|
|
line, _ = get_line_from_node(node)(parent.entries)
|
|
end
|
|
|
|
-- Ignore parent line count
|
|
line = line - 1
|
|
end
|
|
|
|
local index = line + direction
|
|
if index < 1 then
|
|
index = 1
|
|
elseif index > #parent.entries then
|
|
index = #parent.entries
|
|
end
|
|
local target_node = parent.entries[index]
|
|
|
|
line, _ = get_line_from_node(target_node)(M.Tree.entries, true)
|
|
view.set_cursor({line, 0})
|
|
renderer.draw(M.Tree, true)
|
|
end
|
|
|
|
function M.close_node(node)
|
|
M.parent_node(node, true)
|
|
end
|
|
|
|
function M.parent_node(node, should_close)
|
|
if node.name == '..' then return end
|
|
should_close = should_close or false
|
|
|
|
local iter = get_line_from_node(node, true)
|
|
if node.open == true and should_close then
|
|
node.open = false
|
|
else
|
|
local line, parent = iter(M.Tree.entries, true)
|
|
if parent == nil then
|
|
line = 1
|
|
elseif should_close then
|
|
parent.open = false
|
|
end
|
|
api.nvim_win_set_cursor(view.get_winnr(), {line, 0})
|
|
end
|
|
renderer.draw(M.Tree, true)
|
|
end
|
|
|
|
function M.toggle_ignored()
|
|
pops.show_ignored = not pops.show_ignored
|
|
return M.refresh_tree()
|
|
end
|
|
|
|
function M.toggle_dotfiles()
|
|
pops.show_dotfiles = not pops.show_dotfiles
|
|
return M.refresh_tree()
|
|
end
|
|
|
|
function M.toggle_help()
|
|
view.toggle_help()
|
|
return M.redraw()
|
|
end
|
|
|
|
function M.dir_up(node)
|
|
if not node or node.name == ".." then
|
|
return M.change_dir('..')
|
|
else
|
|
local newdir = vim.fn.fnamemodify(M.Tree.cwd, ':h')
|
|
M.change_dir(newdir)
|
|
return M.set_index_and_redraw(node.absolute_path)
|
|
end
|
|
end
|
|
|
|
return M
|