mirror of
https://github.com/SpaceVim/SpaceVim.git
synced 2025-02-03 07:10:05 +08:00
651 lines
19 KiB
Lua
Vendored
651 lines
19 KiB
Lua
Vendored
--This file should have all functions that are in the public api and either set
|
|
--or read the state of this source.
|
|
|
|
local vim = vim
|
|
local utils = require("neo-tree.utils")
|
|
local fs_scan = require("neo-tree.sources.filesystem.lib.fs_scan")
|
|
local renderer = require("neo-tree.ui.renderer")
|
|
local inputs = require("neo-tree.ui.inputs")
|
|
local events = require("neo-tree.events")
|
|
local log = require("neo-tree.log")
|
|
local fs_watch = require("neo-tree.sources.filesystem.lib.fs_watch")
|
|
|
|
local M = {}
|
|
local source_data = {}
|
|
local all_states = {}
|
|
local default_configs = {}
|
|
|
|
local get_source_data = function(source_name)
|
|
if source_name == nil then
|
|
error("get_source_data: source_name cannot be nil")
|
|
end
|
|
local sd = source_data[source_name]
|
|
if sd then
|
|
return sd
|
|
end
|
|
sd = {
|
|
name = source_name,
|
|
state_by_tab = {},
|
|
state_by_win = {},
|
|
subscriptions = {},
|
|
}
|
|
source_data[source_name] = sd
|
|
return sd
|
|
end
|
|
|
|
local function create_state(tabid, sd, winid)
|
|
local default_config = default_configs[sd.name]
|
|
local state = vim.deepcopy(default_config, { noref = 1 })
|
|
state.tabid = tabid
|
|
state.id = winid or tabid
|
|
state.dirty = true
|
|
state.position = {
|
|
is = { restorable = false },
|
|
}
|
|
state.git_base = "HEAD"
|
|
table.insert(all_states, state)
|
|
return state
|
|
end
|
|
|
|
M._get_all_states = function()
|
|
return all_states
|
|
end
|
|
|
|
M._for_each_state = function(source_name, action)
|
|
for _, state in ipairs(all_states) do
|
|
if source_name == nil or state.name == source_name then
|
|
action(state)
|
|
end
|
|
end
|
|
end
|
|
|
|
---For use in tests only, completely resets the state of all sources.
|
|
---This closes all windows as well since they would be broken by this action.
|
|
M._clear_state = function()
|
|
fs_watch.unwatch_all()
|
|
renderer.close_all_floating_windows()
|
|
for _, data in pairs(source_data) do
|
|
for _, state in pairs(data.state_by_tab) do
|
|
renderer.close(state)
|
|
end
|
|
for _, state in pairs(data.state_by_win) do
|
|
renderer.close(state)
|
|
end
|
|
end
|
|
source_data = {}
|
|
end
|
|
|
|
M.set_default_config = function(source_name, config)
|
|
if source_name == nil then
|
|
error("set_default_config: source_name cannot be nil")
|
|
end
|
|
default_configs[source_name] = config
|
|
local sd = get_source_data(source_name)
|
|
for tabid, tab_config in pairs(sd.state_by_tab) do
|
|
sd.state_by_tab[tabid] = vim.tbl_deep_extend("force", tab_config, config)
|
|
end
|
|
end
|
|
|
|
--TODO: we need to track state per window when working with netwrw style "current"
|
|
--position. How do we know which one to return when this is called?
|
|
M.get_state = function(source_name, tabid, winid)
|
|
if source_name == nil then
|
|
error("get_state: source_name cannot be nil")
|
|
end
|
|
tabid = tabid or vim.api.nvim_get_current_tabpage()
|
|
local sd = get_source_data(source_name)
|
|
if type(winid) == "number" then
|
|
local win_state = sd.state_by_win[winid]
|
|
if not win_state then
|
|
win_state = create_state(tabid, sd, winid)
|
|
sd.state_by_win[winid] = win_state
|
|
end
|
|
return win_state
|
|
else
|
|
local tab_state = sd.state_by_tab[tabid]
|
|
if tab_state and tab_state.winid then
|
|
-- just in case tab and window get tangled up, tab state replaces window
|
|
sd.state_by_win[tab_state.winid] = nil
|
|
end
|
|
if not tab_state then
|
|
tab_state = create_state(tabid, sd)
|
|
sd.state_by_tab[tabid] = tab_state
|
|
end
|
|
return tab_state
|
|
end
|
|
end
|
|
|
|
---Returns the state for the current buffer, assuming it is a neo-tree buffer.
|
|
---@param winid number|nil The window id to use, if nil, the current window is used.
|
|
---@return table|nil The state for the current buffer, or nil if it is not a
|
|
---neo-tree buffer.
|
|
M.get_state_for_window = function(winid)
|
|
local winid = winid or vim.api.nvim_get_current_win()
|
|
local bufnr = vim.api.nvim_win_get_buf(winid)
|
|
local source_status, source_name = pcall(vim.api.nvim_buf_get_var, bufnr, "neo_tree_source")
|
|
local position_status, position = pcall(vim.api.nvim_buf_get_var, bufnr, "neo_tree_position")
|
|
if not source_status or not position_status then
|
|
return nil
|
|
end
|
|
|
|
local tabid = vim.api.nvim_get_current_tabpage()
|
|
if position == "current" then
|
|
return M.get_state(source_name, tabid, winid)
|
|
else
|
|
return M.get_state(source_name, tabid, nil)
|
|
end
|
|
end
|
|
|
|
M.get_path_to_reveal = function(include_terminals)
|
|
local win_id = vim.api.nvim_get_current_win()
|
|
local cfg = vim.api.nvim_win_get_config(win_id)
|
|
if cfg.relative > "" or cfg.external then
|
|
-- floating window, ignore
|
|
return nil
|
|
end
|
|
if vim.bo.filetype == "neo-tree" then
|
|
return nil
|
|
end
|
|
local path = vim.fn.expand("%:p")
|
|
if not utils.truthy(path) then
|
|
return nil
|
|
end
|
|
if not include_terminals and path:match("term://") then
|
|
return nil
|
|
end
|
|
return path
|
|
end
|
|
|
|
M.subscribe = function(source_name, event)
|
|
if source_name == nil then
|
|
error("subscribe: source_name cannot be nil")
|
|
end
|
|
local sd = get_source_data(source_name)
|
|
if not sd.subscriptions then
|
|
sd.subscriptions = {}
|
|
end
|
|
if not utils.truthy(event.id) then
|
|
event.id = sd.name .. "." .. event.event
|
|
end
|
|
log.trace("subscribing to event: " .. event.id)
|
|
sd.subscriptions[event] = true
|
|
events.subscribe(event)
|
|
end
|
|
|
|
M.unsubscribe = function(source_name, event)
|
|
if source_name == nil then
|
|
error("unsubscribe: source_name cannot be nil")
|
|
end
|
|
local sd = get_source_data(source_name)
|
|
log.trace("unsubscribing to event: " .. event.id or event.event)
|
|
if sd.subscriptions then
|
|
for sub, _ in pairs(sd.subscriptions) do
|
|
if sub.event == event.event and sub.id == event.id then
|
|
sd.subscriptions[sub] = false
|
|
events.unsubscribe(sub)
|
|
end
|
|
end
|
|
end
|
|
events.unsubscribe(event)
|
|
end
|
|
|
|
M.unsubscribe_all = function(source_name)
|
|
if source_name == nil then
|
|
error("unsubscribe_all: source_name cannot be nil")
|
|
end
|
|
local sd = get_source_data(source_name)
|
|
if sd.subscriptions then
|
|
for event, subscribed in pairs(sd.subscriptions) do
|
|
if subscribed then
|
|
events.unsubscribe(event)
|
|
end
|
|
end
|
|
end
|
|
sd.subscriptions = {}
|
|
end
|
|
|
|
M.close = function(source_name, at_position)
|
|
local state = M.get_state(source_name)
|
|
if at_position then
|
|
if state.current_position == at_position then
|
|
return renderer.close(state)
|
|
else
|
|
return false
|
|
end
|
|
else
|
|
return renderer.close(state)
|
|
end
|
|
end
|
|
|
|
M.close_all = function(at_position)
|
|
local tabid = vim.api.nvim_get_current_tabpage()
|
|
for source_name, _ in pairs(source_data) do
|
|
M._for_each_state(source_name, function(state)
|
|
if state.tabid == tabid then
|
|
if at_position then
|
|
if state.current_position == at_position then
|
|
log.trace("Closing " .. source_name .. " at position " .. at_position)
|
|
pcall(renderer.close, state)
|
|
end
|
|
else
|
|
log.trace("Closing " .. source_name)
|
|
pcall(renderer.close, state)
|
|
end
|
|
end
|
|
end)
|
|
end
|
|
end
|
|
|
|
M.close_all_except = function(except_source_name)
|
|
local tabid = vim.api.nvim_get_current_tabpage()
|
|
for source_name, _ in pairs(source_data) do
|
|
M._for_each_state(source_name, function(state)
|
|
if state.tabid == tabid and source_name ~= except_source_name then
|
|
log.trace("Closing " .. source_name)
|
|
pcall(renderer.close, state)
|
|
end
|
|
end)
|
|
end
|
|
end
|
|
|
|
---Redraws the tree with updated diagnostics without scanning the filesystem again.
|
|
M.diagnostics_changed = function(source_name, args)
|
|
if not type(args) == "table" then
|
|
error("diagnostics_changed: args must be a table")
|
|
end
|
|
M._for_each_state(source_name, function(state)
|
|
state.diagnostics_lookup = args.diagnostics_lookup
|
|
renderer.redraw(state)
|
|
end)
|
|
end
|
|
|
|
---Called by autocmds when the cwd dir is changed. This will change the root.
|
|
M.dir_changed = function(source_name)
|
|
M._for_each_state(source_name, function(state)
|
|
local cwd = M.get_cwd(state)
|
|
if state.path and cwd == state.path then
|
|
return
|
|
end
|
|
if renderer.window_exists(state) then
|
|
M.navigate(state, cwd)
|
|
else
|
|
state.path = nil
|
|
state.dirty = true
|
|
end
|
|
end)
|
|
end
|
|
--
|
|
---Redraws the tree with updated git_status without scanning the filesystem again.
|
|
M.git_status_changed = function(source_name, args)
|
|
if not type(args) == "table" then
|
|
error("git_status_changed: args must be a table")
|
|
end
|
|
M._for_each_state(source_name, function(state)
|
|
if utils.is_subpath(args.git_root, state.path) then
|
|
state.git_status_lookup = args.git_status
|
|
renderer.redraw(state)
|
|
end
|
|
end)
|
|
end
|
|
|
|
-- Vimscript functions like vim.fn.getcwd take tabpage number (tab position counting from left)
|
|
-- but API functions operate on tabpage id (as returned by nvim_tabpage_get_number). These values
|
|
-- get out of sync when tabs are being moved and we want to track state according to tabpage id.
|
|
local to_tabnr = function(tabid)
|
|
return tabid > 0 and vim.api.nvim_tabpage_get_number(tabid) or tabid
|
|
end
|
|
|
|
local get_params_for_cwd = function(state)
|
|
local tabid = state.tabid
|
|
-- the id is either the tabid for sidebars or the winid for splits
|
|
local winid = state.id == tabid and -1 or state.id
|
|
|
|
if state.cwd_target then
|
|
local target = state.cwd_target.sidebar
|
|
if state.current_position == "current" then
|
|
target = state.cwd_target.current
|
|
end
|
|
if target == "window" then
|
|
return winid, to_tabnr(tabid)
|
|
elseif target == "global" then
|
|
return -1, -1
|
|
elseif target == "none" then
|
|
return nil, nil
|
|
else -- default to tab
|
|
return -1, to_tabnr(tabid)
|
|
end
|
|
else
|
|
return winid, to_tabnr(tabid)
|
|
end
|
|
end
|
|
|
|
M.get_cwd = function(state)
|
|
local winid, tabnr = get_params_for_cwd(state)
|
|
local success, cwd = false, ""
|
|
if winid or tabnr then
|
|
success, cwd = pcall(vim.fn.getcwd, winid, tabnr)
|
|
end
|
|
if success then
|
|
return cwd
|
|
else
|
|
success, cwd = pcall(vim.fn.getcwd)
|
|
if success then
|
|
return cwd
|
|
else
|
|
return state.path
|
|
end
|
|
end
|
|
end
|
|
|
|
M.set_cwd = function(state)
|
|
if not state.path then
|
|
return
|
|
end
|
|
|
|
local winid, tabnr = get_params_for_cwd(state)
|
|
|
|
if winid == nil and tabnr == nil then
|
|
return
|
|
end
|
|
|
|
local _, cwd = pcall(vim.fn.getcwd, winid, tabnr)
|
|
if state.path ~= cwd then
|
|
if winid > 0 then
|
|
vim.cmd("lcd " .. state.path)
|
|
elseif tabnr > 0 then
|
|
vim.cmd("tcd " .. state.path)
|
|
else
|
|
vim.cmd("cd " .. state.path)
|
|
end
|
|
end
|
|
end
|
|
|
|
local dispose_state = function(state)
|
|
pcall(fs_scan.stop_watchers, state)
|
|
pcall(renderer.close, state)
|
|
source_data[state.name].state_by_tab[state.id] = nil
|
|
source_data[state.name].state_by_win[state.id] = nil
|
|
state.disposed = true
|
|
end
|
|
|
|
M.dispose = function(source_name, tabid)
|
|
for i, state in ipairs(all_states) do
|
|
if source_name == nil or state.name == source_name then
|
|
if not tabid or tabid == state.tabid then
|
|
log.trace(state.name, " disposing of tab: ", tabid)
|
|
dispose_state(state)
|
|
table.remove(all_states, i)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
M.dispose_tab = function(tabid)
|
|
if not tabid then
|
|
error("dispose_tab: tabid cannot be nil")
|
|
end
|
|
for i, state in ipairs(all_states) do
|
|
if tabid == state.tabid then
|
|
log.trace(state.name, " disposing of tab: ", tabid, state.name)
|
|
dispose_state(state)
|
|
table.remove(all_states, i)
|
|
end
|
|
end
|
|
end
|
|
|
|
M.dispose_invalid_tabs = function()
|
|
-- Iterate in reverse because we are removing items during loop
|
|
for i = #all_states, 1, -1 do
|
|
local state = all_states[i]
|
|
-- if not valid_tabs[state.tabid] then
|
|
if not vim.api.nvim_tabpage_is_valid(state.tabid) then
|
|
log.trace(state.name, " disposing of tab: ", state.tabid, state.name)
|
|
dispose_state(state)
|
|
table.remove(all_states, i)
|
|
end
|
|
end
|
|
end
|
|
|
|
M.dispose_window = function(winid)
|
|
if not winid then
|
|
error("dispose_window: winid cannot be nil")
|
|
end
|
|
for i, state in ipairs(all_states) do
|
|
if state.id == winid then
|
|
log.trace(state.name, " disposing of window: ", winid, state.name)
|
|
dispose_state(state)
|
|
table.remove(all_states, i)
|
|
end
|
|
end
|
|
end
|
|
|
|
M.float = function(source_name)
|
|
local state = M.get_state(source_name)
|
|
state.current_position = "float"
|
|
local path_to_reveal = M.get_path_to_reveal()
|
|
M.navigate(source_name, state.path, path_to_reveal)
|
|
end
|
|
|
|
---Focus the window, opening it if it is not already open.
|
|
---@param source_name string Source name.
|
|
---@param path_to_reveal string|nil Node to focus after the items are loaded.
|
|
---@param callback function|nil Callback to call after the items are loaded.
|
|
M.focus = function(source_name, path_to_reveal, callback)
|
|
local state = M.get_state(source_name)
|
|
state.current_position = nil
|
|
if path_to_reveal then
|
|
M.navigate(source_name, state.path, path_to_reveal, callback)
|
|
else
|
|
if not state.dirty and renderer.window_exists(state) then
|
|
vim.api.nvim_set_current_win(state.winid)
|
|
else
|
|
M.navigate(source_name, state.path, nil, callback)
|
|
end
|
|
end
|
|
end
|
|
|
|
---Redraws the tree with updated modified markers without scanning the filesystem again.
|
|
M.opened_buffers_changed = function(source_name, args)
|
|
if not type(args) == "table" then
|
|
error("opened_buffers_changed: args must be a table")
|
|
end
|
|
if type(args.opened_buffers) == "table" then
|
|
M._for_each_state(source_name, function(state)
|
|
if utils.tbl_equals(args.opened_buffers, state.opened_buffers) then
|
|
-- no changes, no need to redraw
|
|
return
|
|
end
|
|
state.opened_buffers = args.opened_buffers
|
|
renderer.redraw(state)
|
|
end)
|
|
end
|
|
end
|
|
|
|
---Navigate to the given path.
|
|
---@param state_or_source_name string|table The state or source name to navigate.
|
|
---@param path string? Path to navigate to. If empty, will navigate to the cwd.
|
|
---@param path_to_reveal string? Node to focus after the items are loaded.
|
|
---@param callback function? Callback to call after the items are loaded.
|
|
---@param async boolean? Whether to load the items asynchronously, may not be respected by all sources.
|
|
M.navigate = function(state_or_source_name, path, path_to_reveal, callback, async)
|
|
require("neo-tree").ensure_config()
|
|
local state, source_name
|
|
if type(state_or_source_name) == "string" then
|
|
state = M.get_state(state_or_source_name)
|
|
source_name = state_or_source_name
|
|
elseif type(state_or_source_name) == "table" then
|
|
state = state_or_source_name
|
|
source_name = state.name
|
|
else
|
|
log.error("navigate: state_or_source_name must be a string or a table")
|
|
end
|
|
log.trace("navigate", source_name, path, path_to_reveal)
|
|
local mod = get_source_data(source_name).module
|
|
if not mod then
|
|
mod = require("neo-tree.sources." .. source_name)
|
|
end
|
|
mod.navigate(state, path, path_to_reveal, callback, async)
|
|
end
|
|
|
|
---Redraws the tree without scanning the filesystem again. Use this after
|
|
-- making changes to the nodes that would affect how their components are
|
|
-- rendered.
|
|
M.redraw = function(source_name)
|
|
M._for_each_state(source_name, function(state)
|
|
renderer.redraw(state)
|
|
end)
|
|
end
|
|
|
|
---Refreshes the tree by scanning the filesystem again.
|
|
M.refresh = function(source_name, callback)
|
|
if type(callback) ~= "function" then
|
|
callback = nil
|
|
end
|
|
local current_tabid = vim.api.nvim_get_current_tabpage()
|
|
log.trace(source_name, "refresh")
|
|
for i = 1, #all_states, 1 do
|
|
local state = all_states[i]
|
|
if state.tabid == current_tabid and state.path and renderer.window_exists(state) then
|
|
local success, err = pcall(M.navigate, state, state.path, nil, callback)
|
|
if not success then
|
|
log.error(err)
|
|
end
|
|
else
|
|
state.dirty = true
|
|
end
|
|
end
|
|
end
|
|
|
|
M.reveal_current_file = function(source_name, callback, force_cwd)
|
|
log.trace("Revealing current file")
|
|
local state = M.get_state(source_name)
|
|
state.current_position = nil
|
|
|
|
-- When events trigger that try to restore the position of the cursor in the tree window,
|
|
-- we want them to ignore this "iteration" as the user is trying to explicitly focus a
|
|
-- (potentially) different position/node
|
|
state.position.is.restorable = false
|
|
|
|
require("neo-tree").close_all_except(source_name)
|
|
local path = M.get_path_to_reveal()
|
|
if not path then
|
|
M.focus(source_name)
|
|
return
|
|
end
|
|
local cwd = state.path
|
|
if cwd == nil then
|
|
cwd = M.get_cwd(state)
|
|
end
|
|
if force_cwd then
|
|
if not utils.is_subpath(cwd, path) then
|
|
state.path, _ = utils.split_path(path)
|
|
end
|
|
elseif not utils.is_subpath(cwd, path) then
|
|
cwd, _ = utils.split_path(path)
|
|
local nt = require("neo-tree")
|
|
if nt.config.force_change_cwd then
|
|
state.path = cwd
|
|
M.focus(source_name, path, callback)
|
|
else
|
|
inputs.confirm("File not in cwd. Change cwd to " .. cwd .. "?", function(response)
|
|
if response == true then
|
|
state.path = cwd
|
|
M.focus(source_name, path, callback)
|
|
else
|
|
M.focus(source_name, nil, callback)
|
|
end
|
|
end)
|
|
end
|
|
return
|
|
end
|
|
if path then
|
|
if not renderer.focus_node(state, path) then
|
|
M.focus(source_name, path, callback)
|
|
end
|
|
end
|
|
end
|
|
|
|
M.reveal_in_split = function(source_name, callback)
|
|
local state = M.get_state(source_name, nil, vim.api.nvim_get_current_win())
|
|
state.current_position = "current"
|
|
local path_to_reveal = M.get_path_to_reveal()
|
|
if not path_to_reveal then
|
|
M.navigate(state, nil, nil, callback)
|
|
return
|
|
end
|
|
local cwd = state.path
|
|
if cwd == nil then
|
|
cwd = M.get_cwd(state)
|
|
end
|
|
if cwd and not utils.is_subpath(cwd, path_to_reveal) then
|
|
state.path, _ = utils.split_path(path_to_reveal)
|
|
end
|
|
M.navigate(state, state.path, path_to_reveal, callback)
|
|
end
|
|
|
|
---Opens the tree and displays the current path or cwd, without focusing it.
|
|
M.show = function(source_name)
|
|
local state = M.get_state(source_name)
|
|
state.current_position = nil
|
|
if not renderer.window_exists(state) then
|
|
local current_win = vim.api.nvim_get_current_win()
|
|
M.navigate(source_name, state.path, nil, function()
|
|
vim.api.nvim_set_current_win(current_win)
|
|
end)
|
|
end
|
|
end
|
|
|
|
M.show_in_split = function(source_name, callback)
|
|
local state = M.get_state(source_name, nil, vim.api.nvim_get_current_win())
|
|
state.current_position = "current"
|
|
M.navigate(state, state.path, nil, callback)
|
|
end
|
|
|
|
M.validate_source = function(source_name, module)
|
|
if source_name == nil then
|
|
error("register_source: source_name cannot be nil")
|
|
end
|
|
if module == nil then
|
|
error("register_source: module cannot be nil")
|
|
end
|
|
if type(module) ~= "table" then
|
|
error("register_source: module must be a table")
|
|
end
|
|
local required_functions = {
|
|
"navigate",
|
|
"setup",
|
|
}
|
|
for _, name in ipairs(required_functions) do
|
|
if type(module[name]) ~= "function" then
|
|
error("Source " .. source_name .. " must have a " .. name .. " function")
|
|
end
|
|
end
|
|
end
|
|
|
|
---Configures the plugin, should be called before the plugin is used.
|
|
---@param source_name string Name of the source.
|
|
---@param config table Configuration table containing merged configuration for the source.
|
|
---@param global_config table Global configuration table, shared between all sources.
|
|
---@param module table Module containing the source's code.
|
|
M.setup = function(source_name, config, global_config, module)
|
|
log.debug(source_name, " setup ", config)
|
|
M.unsubscribe_all(source_name)
|
|
M.set_default_config(source_name, config)
|
|
if module == nil then
|
|
module = require("neo-tree.sources." .. source_name)
|
|
end
|
|
local success, err = pcall(M.validate_source, source_name, module)
|
|
if success then
|
|
success, err = pcall(module.setup, config, global_config)
|
|
if success then
|
|
get_source_data(source_name).module = module
|
|
else
|
|
log.error("Source " .. source_name .. " setup failed: " .. err)
|
|
end
|
|
else
|
|
log.error("Source " .. source_name .. " is invalid: " .. err)
|
|
end
|
|
end
|
|
|
|
return M
|