1
0
mirror of https://github.com/SpaceVim/SpaceVim.git synced 2025-02-03 07:10:05 +08:00
SpaceVim/bundle/neo-tree.nvim/lua/neo-tree/sources/manager.lua
2023-06-15 13:03:33 +08:00

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