Refresh generated nvim config
This commit is contained in:
@ -0,0 +1,89 @@
|
||||
local vim = vim
|
||||
local utils = require("neo-tree.utils")
|
||||
local log = require("neo-tree.log")
|
||||
local M = {}
|
||||
|
||||
-- DEPRECATED: to be removed in a future release, use this instead:
|
||||
-- ```
|
||||
-- require("neo-tree.command").execute({ action = "close" })
|
||||
-- ```
|
||||
M.close_all = function()
|
||||
require("neo-tree.command").execute({ action = "close" })
|
||||
end
|
||||
|
||||
M.ensure_config = function()
|
||||
if not M.config then
|
||||
M.setup({ log_to_file = false }, true)
|
||||
end
|
||||
end
|
||||
|
||||
M.get_prior_window = function(ignore_filetypes, ignore_winfixbuf)
|
||||
ignore_filetypes = ignore_filetypes or {}
|
||||
local ignore = utils.list_to_dict(ignore_filetypes)
|
||||
ignore["neo-tree"] = true
|
||||
|
||||
local tabid = vim.api.nvim_get_current_tabpage()
|
||||
local wins = utils.get_value(M, "config.prior_windows", {}, true)[tabid]
|
||||
if wins == nil then
|
||||
return -1
|
||||
end
|
||||
local win_index = #wins
|
||||
while win_index > 0 do
|
||||
local last_win = wins[win_index]
|
||||
if type(last_win) == "number" then
|
||||
local success, is_valid = pcall(vim.api.nvim_win_is_valid, last_win)
|
||||
if success and is_valid and not (ignore_winfixbuf and utils.is_winfixbuf(last_win)) then
|
||||
local buf = vim.api.nvim_win_get_buf(last_win)
|
||||
local ft = vim.api.nvim_buf_get_option(buf, "filetype")
|
||||
local bt = vim.api.nvim_buf_get_option(buf, "buftype") or "normal"
|
||||
if ignore[ft] ~= true and ignore[bt] ~= true then
|
||||
return last_win
|
||||
end
|
||||
end
|
||||
end
|
||||
win_index = win_index - 1
|
||||
end
|
||||
return -1
|
||||
end
|
||||
|
||||
M.paste_default_config = function()
|
||||
local base_path = debug.getinfo(utils.truthy).source:match("@(.*)/utils/init.lua$")
|
||||
local config_path = base_path .. utils.path_separator .. "defaults.lua"
|
||||
local lines = vim.fn.readfile(config_path)
|
||||
if lines == nil then
|
||||
error("Could not read neo-tree.defaults")
|
||||
end
|
||||
|
||||
-- read up to the end of the config, jut to omit the final return
|
||||
local config = {}
|
||||
for _, line in ipairs(lines) do
|
||||
table.insert(config, line)
|
||||
if line == "}" then
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
vim.api.nvim_put(config, "l", true, false)
|
||||
vim.schedule(function()
|
||||
vim.cmd("normal! `[v`]=")
|
||||
end)
|
||||
end
|
||||
|
||||
M.set_log_level = function(level)
|
||||
log.set_level(level)
|
||||
end
|
||||
|
||||
M.setup = function(config, is_auto_config)
|
||||
M.config = require("neo-tree.setup").merge_config(config, is_auto_config)
|
||||
local netrw = require("neo-tree.setup.netrw")
|
||||
if not is_auto_config and netrw.get_hijack_netrw_behavior() ~= "disabled" then
|
||||
vim.cmd("silent! autocmd! FileExplorer *")
|
||||
netrw.hijack()
|
||||
end
|
||||
end
|
||||
|
||||
M.show_logs = function()
|
||||
vim.cmd("tabnew " .. log.outfile)
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,127 @@
|
||||
local log = require("neo-tree.log")
|
||||
|
||||
local Node = {}
|
||||
function Node:new(value)
|
||||
local props = { prev = nil, next = nil, value = value }
|
||||
setmetatable(props, self)
|
||||
self.__index = self
|
||||
return props
|
||||
end
|
||||
|
||||
local LinkedList = {}
|
||||
function LinkedList:new()
|
||||
local props = { head = nil, tail = nil, size = 0 }
|
||||
setmetatable(props, self)
|
||||
self.__index = self
|
||||
return props
|
||||
end
|
||||
|
||||
function LinkedList:add_node(node)
|
||||
if self.head == nil then
|
||||
self.head = node
|
||||
self.tail = node
|
||||
else
|
||||
self.tail.next = node
|
||||
node.prev = self.tail
|
||||
self.tail = node
|
||||
end
|
||||
self.size = self.size + 1
|
||||
return node
|
||||
end
|
||||
|
||||
function LinkedList:remove_node(node)
|
||||
if node.prev ~= nil then
|
||||
node.prev.next = node.next
|
||||
end
|
||||
if node.next ~= nil then
|
||||
node.next.prev = node.prev
|
||||
end
|
||||
if self.head == node then
|
||||
self.head = node.next
|
||||
end
|
||||
if self.tail == node then
|
||||
self.tail = node.prev
|
||||
end
|
||||
self.size = self.size - 1
|
||||
node.prev = nil
|
||||
node.next = nil
|
||||
node.value = nil
|
||||
end
|
||||
|
||||
-- First in Last Out
|
||||
local Queue = {}
|
||||
function Queue:new()
|
||||
local props = { _list = LinkedList:new() }
|
||||
setmetatable(props, self)
|
||||
self.__index = self
|
||||
return props
|
||||
end
|
||||
|
||||
---Add an element to the end of the queue.
|
||||
---@param value any The value to add.
|
||||
function Queue:add(value)
|
||||
self._list:add_node(Node:new(value))
|
||||
end
|
||||
|
||||
---Iterates over the entire list, running func(value) on each element.
|
||||
---If func returns true, the element is removed from the list.
|
||||
---@param func function The function to run on each element.
|
||||
function Queue:for_each(func)
|
||||
local node = self._list.head
|
||||
while node ~= nil do
|
||||
local result = func(node.value)
|
||||
local node_is_next = false
|
||||
if result then
|
||||
if type(result) == "boolean" then
|
||||
local node_to_remove = node
|
||||
node = node.next
|
||||
node_is_next = true
|
||||
self._list:remove_node(node_to_remove)
|
||||
elseif type(result) == "table" then
|
||||
if type(result.handled) == "boolean" and result.handled == true then
|
||||
log.trace(
|
||||
"Handler ",
|
||||
node.value.id,
|
||||
" for "
|
||||
.. node.value.event
|
||||
.. " returned handled = true, skipping the rest of the queue."
|
||||
)
|
||||
return result
|
||||
end
|
||||
end
|
||||
end
|
||||
if not node_is_next then
|
||||
node = node.next
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function Queue:is_empty()
|
||||
return self._list.size == 0
|
||||
end
|
||||
|
||||
function Queue:remove_by_id(id)
|
||||
local current = self._list.head
|
||||
while current ~= nil do
|
||||
local is_match = false
|
||||
local item = current.value
|
||||
if item ~= nil then
|
||||
local item_id = item.id or item
|
||||
if item_id == id then
|
||||
is_match = true
|
||||
end
|
||||
end
|
||||
if is_match then
|
||||
local next = current.next
|
||||
self._list:remove_node(current)
|
||||
current = next
|
||||
else
|
||||
current = current.next
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return {
|
||||
Queue = Queue,
|
||||
LinkedList = LinkedList,
|
||||
}
|
||||
@ -0,0 +1,122 @@
|
||||
local parser = require("neo-tree.command.parser")
|
||||
local utils = require("neo-tree.utils")
|
||||
|
||||
local M = {
|
||||
show_key_value_completions = true,
|
||||
}
|
||||
|
||||
local get_path_completions = function(key_prefix, base_path)
|
||||
key_prefix = key_prefix or ""
|
||||
local completions = {}
|
||||
local expanded = parser.resolve_path(base_path)
|
||||
local path_completions = vim.fn.glob(expanded .. "*", false, true)
|
||||
for _, completion in ipairs(path_completions) do
|
||||
if expanded ~= base_path then
|
||||
-- we need to recreate the relative path from the aboluste path
|
||||
-- first strip trailing slashes to normalize
|
||||
if expanded:sub(-1) == utils.path_separator then
|
||||
expanded = expanded:sub(1, -2)
|
||||
end
|
||||
if base_path:sub(-1) == utils.path_separator then
|
||||
base_path = base_path:sub(1, -2)
|
||||
end
|
||||
-- now put just the current completion onto the base_path being used
|
||||
completion = base_path .. string.sub(completion, #expanded + 1)
|
||||
end
|
||||
table.insert(completions, key_prefix .. completion)
|
||||
end
|
||||
|
||||
return table.concat(completions, "\n")
|
||||
end
|
||||
|
||||
local get_ref_completions = function(key_prefix)
|
||||
key_prefix = key_prefix or ""
|
||||
local completions = { key_prefix .. "HEAD" }
|
||||
local ok, refs = utils.execute_command("git show-ref")
|
||||
if not ok then
|
||||
return ""
|
||||
end
|
||||
for _, ref in ipairs(refs) do
|
||||
local _, i = ref:find("refs%/%a+%/")
|
||||
if i then
|
||||
table.insert(completions, key_prefix .. ref:sub(i + 1))
|
||||
end
|
||||
end
|
||||
|
||||
return table.concat(completions, "\n")
|
||||
end
|
||||
|
||||
M.complete_args = function(argLead, cmdLine)
|
||||
local candidates = {}
|
||||
local existing = utils.split(cmdLine, " ")
|
||||
local parsed = parser.parse(existing, false)
|
||||
|
||||
local eq = string.find(argLead, "=")
|
||||
if eq == nil then
|
||||
if M.show_key_value_completions then
|
||||
-- may be the start of a new key=value pair
|
||||
for _, key in ipairs(parser.list_args) do
|
||||
key = tostring(key)
|
||||
if key:find(argLead, 1, true) and not parsed[key] then
|
||||
table.insert(candidates, key .. "=")
|
||||
end
|
||||
end
|
||||
|
||||
for _, key in ipairs(parser.path_args) do
|
||||
key = tostring(key)
|
||||
if key:find(argLead, 1, true) and not parsed[key] then
|
||||
table.insert(candidates, key .. "=./")
|
||||
end
|
||||
end
|
||||
|
||||
for _, key in ipairs(parser.ref_args) do
|
||||
key = tostring(key)
|
||||
if key:find(argLead, 1, true) and not parsed[key] then
|
||||
table.insert(candidates, key .. "=")
|
||||
end
|
||||
end
|
||||
end
|
||||
else
|
||||
-- continuation of a key=value pair
|
||||
local key = string.sub(argLead, 1, eq - 1)
|
||||
local value = string.sub(argLead, eq + 1)
|
||||
local arg_type = parser.arg_type_lookup[key]
|
||||
if arg_type == parser.PATH then
|
||||
return get_path_completions(key .. "=", value)
|
||||
elseif arg_type == parser.REF then
|
||||
return get_ref_completions(key .. "=")
|
||||
elseif arg_type == parser.LIST then
|
||||
local valid_values = parser.arguments[key].values
|
||||
if valid_values and not parsed[key] then
|
||||
for _, vv in ipairs(valid_values) do
|
||||
if vv:find(value) then
|
||||
table.insert(candidates, key .. "=" .. vv)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- may be a value without a key
|
||||
for value, key in pairs(parser.reverse_lookup) do
|
||||
value = tostring(value)
|
||||
local key_already_used = false
|
||||
if parser.arg_type_lookup[key] == parser.LIST then
|
||||
key_already_used = type(parsed[key]) ~= "nil"
|
||||
else
|
||||
key_already_used = type(parsed[value]) ~= "nil"
|
||||
end
|
||||
|
||||
if not key_already_used and value:find(argLead, 1, true) then
|
||||
table.insert(candidates, value)
|
||||
end
|
||||
end
|
||||
|
||||
if #candidates == 0 then
|
||||
-- default to path completion
|
||||
return get_path_completions(nil, argLead) .. "\n" .. get_ref_completions(nil)
|
||||
end
|
||||
return table.concat(candidates, "\n")
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,236 @@
|
||||
local parser = require("neo-tree.command.parser")
|
||||
local log = require("neo-tree.log")
|
||||
local manager = require("neo-tree.sources.manager")
|
||||
local utils = require("neo-tree.utils")
|
||||
local renderer = require("neo-tree.ui.renderer")
|
||||
local inputs = require("neo-tree.ui.inputs")
|
||||
local completion = require("neo-tree.command.completion")
|
||||
local do_show_or_focus, handle_reveal
|
||||
|
||||
local M = {
|
||||
complete_args = completion.complete_args,
|
||||
}
|
||||
|
||||
-- Store the last source used for `M.execute`
|
||||
M._last = {
|
||||
source = nil,
|
||||
position = nil,
|
||||
}
|
||||
|
||||
---Executes a Neo-tree action from outside of a Neo-tree window,
|
||||
---such as show, hide, navigate, etc.
|
||||
---@param args table The action to execute. The table can have the following keys:
|
||||
--- action = string The action to execute, can be one of:
|
||||
--- "close",
|
||||
--- "focus", <-- default value
|
||||
--- "show",
|
||||
--- source = string The source to use for this action. This will default
|
||||
--- to the default_source specified in the user's config.
|
||||
--- Can be one of:
|
||||
--- "filesystem",
|
||||
--- "buffers",
|
||||
--- "git_status",
|
||||
-- "migrations"
|
||||
--- position = string The position this action will affect. This will default
|
||||
--- to the the last used position or the position specified
|
||||
--- in the user's config for the given source. Can be one of:
|
||||
--- "left",
|
||||
--- "right",
|
||||
--- "float",
|
||||
--- "current"
|
||||
--- toggle = boolean Whether to toggle the visibility of the Neo-tree window.
|
||||
--- reveal = boolean Whether to reveal the current file in the Neo-tree window.
|
||||
--- reveal_file = string The specific file to reveal.
|
||||
--- dir = string The root directory to set.
|
||||
--- git_base = string The git base used for diff
|
||||
M.execute = function(args)
|
||||
local nt = require("neo-tree")
|
||||
nt.ensure_config()
|
||||
|
||||
if args.source == "migrations" then
|
||||
require("neo-tree.setup.deprecations").show_migrations()
|
||||
return
|
||||
end
|
||||
|
||||
args.action = args.action or "focus"
|
||||
|
||||
-- handle close action, which can specify a source and/or position
|
||||
if args.action == "close" then
|
||||
if args.source then
|
||||
manager.close(args.source, args.position)
|
||||
else
|
||||
manager.close_all(args.position)
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
-- The rest of the actions require a source
|
||||
args.source = args.source or nt.config.default_source
|
||||
|
||||
-- Handle source=last
|
||||
if args.source == "last" then
|
||||
args.source = M._last.source or nt.config.default_source
|
||||
|
||||
-- Restore last position if it was not specified
|
||||
if args.position == nil then
|
||||
args.position = M._last.position
|
||||
end
|
||||
|
||||
-- Prevent the default source from being set to "last"
|
||||
if args.source == "last" then
|
||||
args.source = nt.config.sources[1]
|
||||
end
|
||||
end
|
||||
M._last.source = args.source
|
||||
M._last.position = args.position
|
||||
|
||||
-- If position=current was requested, but we are currently in a neo-tree window,
|
||||
-- then we need to override that.
|
||||
if args.position == "current" and vim.bo.filetype == "neo-tree" then
|
||||
local position = vim.api.nvim_buf_get_var(0, "neo_tree_position")
|
||||
if position then
|
||||
args.position = position
|
||||
end
|
||||
end
|
||||
|
||||
-- Now get the correct state
|
||||
local state
|
||||
local requested_position = args.position or nt.config[args.source].window.position
|
||||
if requested_position == "current" then
|
||||
local winid = vim.api.nvim_get_current_win()
|
||||
state = manager.get_state(args.source, nil, winid)
|
||||
else
|
||||
state = manager.get_state(args.source, nil, nil)
|
||||
end
|
||||
|
||||
-- Next handle toggle, the rest is irrelevant if there is a window to toggle
|
||||
if args.toggle then
|
||||
if renderer.close(state) then
|
||||
-- It was open, and now it's not.
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
-- Handle position override
|
||||
local default_position = nt.config[args.source].window.position
|
||||
local current_position = state.current_position or default_position
|
||||
local position_changed = false
|
||||
if args.position then
|
||||
state.current_position = args.position
|
||||
position_changed = args.position ~= current_position
|
||||
end
|
||||
|
||||
-- Handle setting directory if requested
|
||||
local path_changed = false
|
||||
if utils.truthy(args.dir) then
|
||||
-- Root paths on Windows have 3 characters ("C:\")
|
||||
local root_len = vim.fn.has("win32") == 1 and 3 or 1
|
||||
if #args.dir > root_len and args.dir:sub(-1) == utils.path_separator then
|
||||
args.dir = args.dir:sub(1, -2)
|
||||
end
|
||||
path_changed = state.path ~= args.dir
|
||||
else
|
||||
args.dir = state.path
|
||||
end
|
||||
|
||||
-- Handle setting git ref
|
||||
local git_base_changed = state.git_base ~= args.git_base
|
||||
if utils.truthy(args.git_base) then
|
||||
state.git_base = args.git_base
|
||||
end
|
||||
|
||||
-- Handle source selector option
|
||||
state.enable_source_selector = args.selector
|
||||
|
||||
-- Handle reveal logic
|
||||
args.reveal = args.reveal or args.reveal_force_cwd
|
||||
local do_reveal = utils.truthy(args.reveal_file)
|
||||
if args.reveal and not do_reveal then
|
||||
args.reveal_file = manager.get_path_to_reveal()
|
||||
do_reveal = utils.truthy(args.reveal_file)
|
||||
end
|
||||
|
||||
-- All set, now show or focus the window
|
||||
local force_navigate = path_changed or do_reveal or git_base_changed or state.dirty
|
||||
--if position_changed and args.position ~= "current" and current_position ~= "current" then
|
||||
-- manager.close(args.source)
|
||||
--end
|
||||
if do_reveal then
|
||||
args.reveal_file = utils.normalize_path(args.reveal_file)
|
||||
handle_reveal(args, state)
|
||||
else
|
||||
do_show_or_focus(args, state, force_navigate)
|
||||
end
|
||||
end
|
||||
|
||||
---Parses and executes the command line. Use execute(args) instead.
|
||||
---@param ... string Argument as strings.
|
||||
M._command = function(...)
|
||||
local args = parser.parse({ ... }, true)
|
||||
M.execute(args)
|
||||
end
|
||||
|
||||
do_show_or_focus = function(args, state, force_navigate)
|
||||
local window_exists = renderer.window_exists(state)
|
||||
local function close_other_sources()
|
||||
if not window_exists then
|
||||
-- Clear the space in case another source is already open
|
||||
local target_position = args.position or state.current_position or state.window.position
|
||||
if target_position ~= "current" then
|
||||
manager.close_all(target_position)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if args.action == "show" then
|
||||
-- "show" means show the window without focusing it
|
||||
if window_exists and not force_navigate then
|
||||
-- There's nothing to do here, we are already at the target state
|
||||
return
|
||||
end
|
||||
-- close_other_sources()
|
||||
local current_win = vim.api.nvim_get_current_win()
|
||||
manager.navigate(state, args.dir, args.reveal_file, function()
|
||||
-- navigate changes the window to neo-tree, so just quickly hop back to the original window
|
||||
vim.api.nvim_set_current_win(current_win)
|
||||
end, false)
|
||||
elseif args.action == "focus" then
|
||||
-- "focus" mean open and jump to the window if closed, and just focus it if already opened
|
||||
if window_exists then
|
||||
vim.api.nvim_set_current_win(state.winid)
|
||||
end
|
||||
if force_navigate or not window_exists then
|
||||
-- close_other_sources()
|
||||
manager.navigate(state, args.dir, args.reveal_file, nil, false)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
handle_reveal = function(args, state)
|
||||
-- Deal with cwd if we need to
|
||||
local cwd = state.path
|
||||
local path = args.reveal_file
|
||||
if cwd == nil then
|
||||
cwd = manager.get_cwd(state)
|
||||
end
|
||||
if args.reveal_force_cwd and not utils.is_subpath(cwd, path) then
|
||||
args.dir, _ = utils.split_path(path)
|
||||
do_show_or_focus(args, state, true)
|
||||
return
|
||||
elseif not utils.is_subpath(cwd, path) then
|
||||
-- force was not specified, so we need to ask the user
|
||||
cwd, _ = utils.split_path(path)
|
||||
inputs.confirm("File not in cwd. Change cwd to " .. cwd .. "?", function(response)
|
||||
if response == true then
|
||||
args.dir = cwd
|
||||
else
|
||||
args.reveal_file = nil
|
||||
end
|
||||
do_show_or_focus(args, state, true)
|
||||
end)
|
||||
return
|
||||
else
|
||||
do_show_or_focus(args, state, true)
|
||||
end
|
||||
end
|
||||
return M
|
||||
@ -0,0 +1,180 @@
|
||||
local utils = require("neo-tree.utils")
|
||||
|
||||
local M = {
|
||||
FLAG = "<FLAG>",
|
||||
LIST = "<LIST>",
|
||||
PATH = "<PATH>",
|
||||
REF = "<REF>",
|
||||
}
|
||||
|
||||
M.setup = function(all_source_names)
|
||||
local source_names = utils.table_copy(all_source_names)
|
||||
table.insert(source_names, "migrations")
|
||||
|
||||
-- A special source referring to the last used source.
|
||||
table.insert(source_names, "last")
|
||||
|
||||
-- For lists, the first value is the default value.
|
||||
local arguments = {
|
||||
action = {
|
||||
type = M.LIST,
|
||||
values = {
|
||||
"close",
|
||||
"focus",
|
||||
"show",
|
||||
},
|
||||
},
|
||||
position = {
|
||||
type = M.LIST,
|
||||
values = {
|
||||
"left",
|
||||
"right",
|
||||
"top",
|
||||
"bottom",
|
||||
"float",
|
||||
"current",
|
||||
},
|
||||
},
|
||||
source = {
|
||||
type = M.LIST,
|
||||
values = source_names,
|
||||
},
|
||||
dir = { type = M.PATH, stat_type = "directory" },
|
||||
reveal_file = { type = M.PATH, stat_type = "file" },
|
||||
git_base = { type = M.REF },
|
||||
toggle = { type = M.FLAG },
|
||||
reveal = { type = M.FLAG },
|
||||
reveal_force_cwd = { type = M.FLAG },
|
||||
selector = { type = M.FLAG },
|
||||
}
|
||||
|
||||
local arg_type_lookup = {}
|
||||
local list_args = {}
|
||||
local path_args = {}
|
||||
local ref_args = {}
|
||||
local flag_args = {}
|
||||
local reverse_lookup = {}
|
||||
for name, def in pairs(arguments) do
|
||||
arg_type_lookup[name] = def.type
|
||||
if def.type == M.LIST then
|
||||
table.insert(list_args, name)
|
||||
for _, vv in ipairs(def.values) do
|
||||
reverse_lookup[tostring(vv)] = name
|
||||
end
|
||||
elseif def.type == M.PATH then
|
||||
table.insert(path_args, name)
|
||||
elseif def.type == M.FLAG then
|
||||
table.insert(flag_args, name)
|
||||
reverse_lookup[name] = M.FLAG
|
||||
elseif def.type == M.REF then
|
||||
table.insert(ref_args, name)
|
||||
else
|
||||
error("Unknown type: " .. def.type)
|
||||
end
|
||||
end
|
||||
|
||||
M.arguments = arguments
|
||||
M.list_args = list_args
|
||||
M.path_args = path_args
|
||||
M.ref_args = ref_args
|
||||
M.flag_args = flag_args
|
||||
M.arg_type_lookup = arg_type_lookup
|
||||
M.reverse_lookup = reverse_lookup
|
||||
end
|
||||
|
||||
M.resolve_path = function(path, validate_type)
|
||||
path = vim.fs.normalize(path)
|
||||
local expanded = vim.fn.expand(path)
|
||||
local abs_path = vim.fn.fnamemodify(expanded, ":p")
|
||||
if validate_type then
|
||||
local stat = vim.loop.fs_stat(abs_path)
|
||||
if stat.type ~= validate_type then
|
||||
error("Invalid path: " .. path .. " is not a " .. validate_type)
|
||||
end
|
||||
end
|
||||
return abs_path
|
||||
end
|
||||
|
||||
M.verify_git_ref = function(ref)
|
||||
local ok, _ = utils.execute_command("git rev-parse --verify " .. ref)
|
||||
return ok
|
||||
end
|
||||
|
||||
local parse_arg = function(result, arg)
|
||||
if type(arg) == "string" then
|
||||
local eq = arg:find("=")
|
||||
if eq then
|
||||
local key = arg:sub(1, eq - 1)
|
||||
local value = arg:sub(eq + 1)
|
||||
local def = M.arguments[key]
|
||||
if not def.type then
|
||||
error("Invalid argument: " .. arg)
|
||||
end
|
||||
|
||||
if def.type == M.PATH then
|
||||
result[key] = M.resolve_path(value, def.stat_type)
|
||||
elseif def.type == M.FLAG then
|
||||
if value == "true" then
|
||||
result[key] = true
|
||||
elseif value == "false" then
|
||||
result[key] = false
|
||||
else
|
||||
error("Invalid value for " .. key .. ": " .. value)
|
||||
end
|
||||
elseif def.type == M.REF then
|
||||
if not M.verify_git_ref(value) then
|
||||
error("Invalid value for " .. key .. ": " .. value)
|
||||
end
|
||||
result[key] = value
|
||||
else
|
||||
result[key] = value
|
||||
end
|
||||
else
|
||||
local value = arg
|
||||
local key = M.reverse_lookup[value]
|
||||
if key == nil then
|
||||
-- maybe it's a git ref
|
||||
if M.verify_git_ref(value) then
|
||||
result["git_base"] = value
|
||||
return
|
||||
end
|
||||
-- maybe it's a path
|
||||
local path = M.resolve_path(value)
|
||||
local stat = vim.loop.fs_stat(path)
|
||||
if stat then
|
||||
if stat.type == "directory" then
|
||||
result["dir"] = path
|
||||
elseif stat.type == "file" then
|
||||
result["reveal_file"] = path
|
||||
end
|
||||
else
|
||||
error("Invalid argument: " .. arg)
|
||||
end
|
||||
elseif key == M.FLAG then
|
||||
result[value] = true
|
||||
else
|
||||
result[key] = value
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
M.parse = function(args, strict_checking)
|
||||
require("neo-tree").ensure_config()
|
||||
local result = {}
|
||||
|
||||
if type(args) == "string" then
|
||||
args = utils.split(args, " ")
|
||||
end
|
||||
-- read args from user
|
||||
for _, arg in ipairs(args) do
|
||||
local success, err = pcall(parse_arg, result, arg)
|
||||
if strict_checking and not success then
|
||||
error(err)
|
||||
end
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,683 @@
|
||||
local config = {
|
||||
-- If a user has a sources list it will replace this one.
|
||||
-- Only sources listed here will be loaded.
|
||||
-- You can also add an external source by adding it's name to this list.
|
||||
-- The name used here must be the same name you would use in a require() call.
|
||||
sources = {
|
||||
"filesystem",
|
||||
"buffers",
|
||||
"git_status",
|
||||
-- "document_symbols",
|
||||
},
|
||||
add_blank_line_at_top = false, -- Add a blank line at the top of the tree.
|
||||
auto_clean_after_session_restore = false, -- Automatically clean up broken neo-tree buffers saved in sessions
|
||||
close_if_last_window = false, -- Close Neo-tree if it is the last window left in the tab
|
||||
default_source = "filesystem", -- you can choose a specific source `last` here which indicates the last used source
|
||||
enable_diagnostics = true,
|
||||
enable_git_status = true,
|
||||
enable_modified_markers = true, -- Show markers for files with unsaved changes.
|
||||
enable_opened_markers = true, -- Enable tracking of opened files. Required for `components.name.highlight_opened_files`
|
||||
enable_refresh_on_write = true, -- Refresh the tree when a file is written. Only used if `use_libuv_file_watcher` is false.
|
||||
enable_cursor_hijack = false, -- If enabled neotree will keep the cursor on the first letter of the filename when moving in the tree.
|
||||
git_status_async = true,
|
||||
-- These options are for people with VERY large git repos
|
||||
git_status_async_options = {
|
||||
batch_size = 1000, -- how many lines of git status results to process at a time
|
||||
batch_delay = 10, -- delay in ms between batches. Spreads out the workload to let other processes run.
|
||||
max_lines = 10000, -- How many lines of git status results to process. Anything after this will be dropped.
|
||||
-- Anything before this will be used. The last items to be processed are the untracked files.
|
||||
},
|
||||
hide_root_node = false, -- Hide the root node.
|
||||
retain_hidden_root_indent = false, -- IF the root node is hidden, keep the indentation anyhow.
|
||||
-- This is needed if you use expanders because they render in the indent.
|
||||
log_level = "info", -- "trace", "debug", "info", "warn", "error", "fatal"
|
||||
log_to_file = false, -- true, false, "/path/to/file.log", use :NeoTreeLogs to show the file
|
||||
open_files_in_last_window = true, -- false = open files in top left window
|
||||
open_files_do_not_replace_types = { "terminal", "Trouble", "qf", "edgy" }, -- when opening files, do not use windows containing these filetypes or buftypes
|
||||
-- popup_border_style is for input and confirmation dialogs.
|
||||
-- Configurtaion of floating window is done in the individual source sections.
|
||||
-- "NC" is a special style that works well with NormalNC set
|
||||
popup_border_style = "NC", -- "double", "none", "rounded", "shadow", "single" or "solid"
|
||||
resize_timer_interval = 500, -- in ms, needed for containers to redraw right aligned and faded content
|
||||
-- set to -1 to disable the resize timer entirely
|
||||
-- -- NOTE: this will speed up to 50 ms for 1 second following a resize
|
||||
sort_case_insensitive = false, -- used when sorting files and directories in the tree
|
||||
sort_function = nil , -- uses a custom function for sorting files and directories in the tree
|
||||
use_popups_for_input = true, -- If false, inputs will use vim.ui.input() instead of custom floats.
|
||||
use_default_mappings = true,
|
||||
-- source_selector provides clickable tabs to switch between sources.
|
||||
source_selector = {
|
||||
winbar = false, -- toggle to show selector on winbar
|
||||
statusline = false, -- toggle to show selector on statusline
|
||||
show_scrolled_off_parent_node = false, -- this will replace the tabs with the parent path
|
||||
-- of the top visible node when scrolled down.
|
||||
sources = {
|
||||
{ source = "filesystem" },
|
||||
{ source = "buffers" },
|
||||
{ source = "git_status" },
|
||||
},
|
||||
content_layout = "start", -- only with `tabs_layout` = "equal", "focus"
|
||||
-- start : |/ bufname \/...
|
||||
-- end : |/ bufname \/...
|
||||
-- center : |/ bufname \/...
|
||||
tabs_layout = "equal", -- start, end, center, equal, focus
|
||||
-- start : |/ a \/ b \/ c \ |
|
||||
-- end : | / a \/ b \/ c \|
|
||||
-- center : | / a \/ b \/ c \ |
|
||||
-- equal : |/ a \/ b \/ c \|
|
||||
-- active : |/ focused tab \/ b \/ c \|
|
||||
truncation_character = "…", -- character to use when truncating the tab label
|
||||
tabs_min_width = nil, -- nil | int: if int padding is added based on `content_layout`
|
||||
tabs_max_width = nil, -- this will truncate text even if `text_trunc_to_fit = false`
|
||||
padding = 0, -- can be int or table
|
||||
-- padding = { left = 2, right = 0 },
|
||||
-- separator = "▕", -- can be string or table, see below
|
||||
separator = { left = "▏", right= "▕" },
|
||||
-- separator = { left = "/", right = "\\", override = nil }, -- |/ a \/ b \/ c \...
|
||||
-- separator = { left = "/", right = "\\", override = "right" }, -- |/ a \ b \ c \...
|
||||
-- separator = { left = "/", right = "\\", override = "left" }, -- |/ a / b / c /...
|
||||
-- separator = { left = "/", right = "\\", override = "active" },-- |/ a / b:active \ c \...
|
||||
-- separator = "|", -- || a | b | c |...
|
||||
separator_active = nil, -- set separators around the active tab. nil falls back to `source_selector.separator`
|
||||
show_separator_on_edge = false,
|
||||
-- true : |/ a \/ b \/ c \|
|
||||
-- false : | a \/ b \/ c |
|
||||
highlight_tab = "NeoTreeTabInactive",
|
||||
highlight_tab_active = "NeoTreeTabActive",
|
||||
highlight_background = "NeoTreeTabInactive",
|
||||
highlight_separator = "NeoTreeTabSeparatorInactive",
|
||||
highlight_separator_active = "NeoTreeTabSeparatorActive",
|
||||
},
|
||||
--
|
||||
--event_handlers = {
|
||||
-- {
|
||||
-- event = "before_render",
|
||||
-- handler = function (state)
|
||||
-- -- add something to the state that can be used by custom components
|
||||
-- end
|
||||
-- },
|
||||
-- {
|
||||
-- event = "file_opened",
|
||||
-- handler = function(file_path)
|
||||
-- --auto close
|
||||
-- require("neo-tree.command").execute({ action = "close" })
|
||||
-- end
|
||||
-- },
|
||||
-- {
|
||||
-- event = "file_opened",
|
||||
-- handler = function(file_path)
|
||||
-- --clear search after opening a file
|
||||
-- require("neo-tree.sources.filesystem").reset_search()
|
||||
-- end
|
||||
-- },
|
||||
-- {
|
||||
-- event = "file_renamed",
|
||||
-- handler = function(args)
|
||||
-- -- fix references to file
|
||||
-- print(args.source, " renamed to ", args.destination)
|
||||
-- end
|
||||
-- },
|
||||
-- {
|
||||
-- event = "file_moved",
|
||||
-- handler = function(args)
|
||||
-- -- fix references to file
|
||||
-- print(args.source, " moved to ", args.destination)
|
||||
-- end
|
||||
-- },
|
||||
-- {
|
||||
-- event = "neo_tree_buffer_enter",
|
||||
-- handler = function()
|
||||
-- vim.cmd 'highlight! Cursor blend=100'
|
||||
-- end
|
||||
-- },
|
||||
-- {
|
||||
-- event = "neo_tree_buffer_leave",
|
||||
-- handler = function()
|
||||
-- vim.cmd 'highlight! Cursor guibg=#5f87af blend=0'
|
||||
-- end
|
||||
-- },
|
||||
-- {
|
||||
-- event = "neo_tree_window_before_open",
|
||||
-- handler = function(args)
|
||||
-- print("neo_tree_window_before_open", vim.inspect(args))
|
||||
-- end
|
||||
-- },
|
||||
-- {
|
||||
-- event = "neo_tree_window_after_open",
|
||||
-- handler = function(args)
|
||||
-- vim.cmd("wincmd =")
|
||||
-- end
|
||||
-- },
|
||||
-- {
|
||||
-- event = "neo_tree_window_before_close",
|
||||
-- handler = function(args)
|
||||
-- print("neo_tree_window_before_close", vim.inspect(args))
|
||||
-- end
|
||||
-- },
|
||||
-- {
|
||||
-- event = "neo_tree_window_after_close",
|
||||
-- handler = function(args)
|
||||
-- vim.cmd("wincmd =")
|
||||
-- end
|
||||
-- }
|
||||
--},
|
||||
default_component_configs = {
|
||||
container = {
|
||||
enable_character_fade = true,
|
||||
width = "100%",
|
||||
right_padding = 0,
|
||||
},
|
||||
--diagnostics = {
|
||||
-- symbols = {
|
||||
-- hint = "H",
|
||||
-- info = "I",
|
||||
-- warn = "!",
|
||||
-- error = "X",
|
||||
-- },
|
||||
-- highlights = {
|
||||
-- hint = "DiagnosticSignHint",
|
||||
-- info = "DiagnosticSignInfo",
|
||||
-- warn = "DiagnosticSignWarn",
|
||||
-- error = "DiagnosticSignError",
|
||||
-- },
|
||||
--},
|
||||
indent = {
|
||||
indent_size = 2,
|
||||
padding = 1,
|
||||
-- indent guides
|
||||
with_markers = true,
|
||||
indent_marker = "│",
|
||||
last_indent_marker = "└",
|
||||
highlight = "NeoTreeIndentMarker",
|
||||
-- expander config, needed for nesting files
|
||||
with_expanders = nil, -- if nil and file nesting is enabled, will enable expanders
|
||||
expander_collapsed = "",
|
||||
expander_expanded = "",
|
||||
expander_highlight = "NeoTreeExpander",
|
||||
},
|
||||
icon = {
|
||||
folder_closed = "",
|
||||
folder_open = "",
|
||||
folder_empty = "",
|
||||
folder_empty_open = "",
|
||||
-- The next two settings are only a fallback, if you use nvim-web-devicons and configure default icons there
|
||||
-- then these will never be used.
|
||||
default = "*",
|
||||
highlight = "NeoTreeFileIcon"
|
||||
},
|
||||
modified = {
|
||||
symbol = "[+] ",
|
||||
highlight = "NeoTreeModified",
|
||||
},
|
||||
name = {
|
||||
trailing_slash = false,
|
||||
highlight_opened_files = false, -- Requires `enable_opened_markers = true`.
|
||||
-- Take values in { false (no highlight), true (only loaded),
|
||||
-- "all" (both loaded and unloaded)}. For more information,
|
||||
-- see the `show_unloaded` config of the `buffers` source.
|
||||
use_git_status_colors = true,
|
||||
highlight = "NeoTreeFileName",
|
||||
},
|
||||
git_status = {
|
||||
symbols = {
|
||||
-- Change type
|
||||
added = "✚", -- NOTE: you can set any of these to an empty string to not show them
|
||||
deleted = "✖",
|
||||
modified = "",
|
||||
renamed = "",
|
||||
-- Status type
|
||||
untracked = "",
|
||||
ignored = "",
|
||||
unstaged = "",
|
||||
staged = "",
|
||||
conflict = "",
|
||||
},
|
||||
align = "right",
|
||||
},
|
||||
-- If you don't want to use these columns, you can set `enabled = false` for each of them individually
|
||||
file_size = {
|
||||
enabled = true,
|
||||
required_width = 64, -- min width of window required to show this column
|
||||
},
|
||||
type = {
|
||||
enabled = true,
|
||||
required_width = 110, -- min width of window required to show this column
|
||||
},
|
||||
last_modified = {
|
||||
enabled = true,
|
||||
required_width = 88, -- min width of window required to show this column
|
||||
},
|
||||
created = {
|
||||
enabled = false,
|
||||
required_width = 120, -- min width of window required to show this column
|
||||
},
|
||||
symlink_target = {
|
||||
enabled = false,
|
||||
},
|
||||
},
|
||||
renderers = {
|
||||
directory = {
|
||||
{ "indent" },
|
||||
{ "icon" },
|
||||
{ "current_filter" },
|
||||
{
|
||||
"container",
|
||||
content = {
|
||||
{ "name", zindex = 10 },
|
||||
{
|
||||
"symlink_target",
|
||||
zindex = 10,
|
||||
highlight = "NeoTreeSymbolicLinkTarget",
|
||||
},
|
||||
{ "clipboard", zindex = 10 },
|
||||
{ "diagnostics", errors_only = true, zindex = 20, align = "right", hide_when_expanded = true },
|
||||
{ "git_status", zindex = 10, align = "right", hide_when_expanded = true },
|
||||
{ "file_size", zindex = 10, align = "right" },
|
||||
{ "type", zindex = 10, align = "right" },
|
||||
{ "last_modified", zindex = 10, align = "right" },
|
||||
{ "created", zindex = 10, align = "right" },
|
||||
},
|
||||
},
|
||||
},
|
||||
file = {
|
||||
{ "indent" },
|
||||
{ "icon" },
|
||||
{
|
||||
"container",
|
||||
content = {
|
||||
{
|
||||
"name",
|
||||
zindex = 10
|
||||
},
|
||||
{
|
||||
"symlink_target",
|
||||
zindex = 10,
|
||||
highlight = "NeoTreeSymbolicLinkTarget",
|
||||
},
|
||||
{ "clipboard", zindex = 10 },
|
||||
{ "bufnr", zindex = 10 },
|
||||
{ "modified", zindex = 20, align = "right" },
|
||||
{ "diagnostics", zindex = 20, align = "right" },
|
||||
{ "git_status", zindex = 10, align = "right" },
|
||||
{ "file_size", zindex = 10, align = "right" },
|
||||
{ "type", zindex = 10, align = "right" },
|
||||
{ "last_modified", zindex = 10, align = "right" },
|
||||
{ "created", zindex = 10, align = "right" },
|
||||
},
|
||||
},
|
||||
},
|
||||
message = {
|
||||
{ "indent", with_markers = false },
|
||||
{ "name", highlight = "NeoTreeMessage" },
|
||||
},
|
||||
terminal = {
|
||||
{ "indent" },
|
||||
{ "icon" },
|
||||
{ "name" },
|
||||
{ "bufnr" }
|
||||
}
|
||||
},
|
||||
nesting_rules = {},
|
||||
-- Global custom commands that will be available in all sources (if not overridden in `opts[source_name].commands`)
|
||||
--
|
||||
-- You can then reference the custom command by adding a mapping to it:
|
||||
-- globally -> `opts.window.mappings`
|
||||
-- locally -> `opt[source_name].window.mappings` to make it source specific.
|
||||
--
|
||||
-- commands = { | window { | filesystem {
|
||||
-- hello = function() | mappings = { | commands = {
|
||||
-- print("Hello world") | ["<C-c>"] = "hello" | hello = function()
|
||||
-- end | } | print("Hello world in filesystem")
|
||||
-- } | } | end
|
||||
--
|
||||
-- see `:h neo-tree-custom-commands-global`
|
||||
commands = {}, -- A list of functions
|
||||
|
||||
window = { -- see https://github.com/MunifTanjim/nui.nvim/tree/main/lua/nui/popup for
|
||||
-- possible options. These can also be functions that return these options.
|
||||
position = "left", -- left, right, top, bottom, float, current
|
||||
width = 40, -- applies to left and right positions
|
||||
height = 15, -- applies to top and bottom positions
|
||||
auto_expand_width = false, -- expand the window when file exceeds the window width. does not work with position = "float"
|
||||
popup = { -- settings that apply to float position only
|
||||
size = {
|
||||
height = "80%",
|
||||
width = "50%",
|
||||
},
|
||||
position = "50%", -- 50% means center it
|
||||
-- you can also specify border here, if you want a different setting from
|
||||
-- the global popup_border_style.
|
||||
},
|
||||
same_level = false, -- Create and paste/move files/directories on the same level as the directory under cursor (as opposed to within the directory under cursor).
|
||||
insert_as = "child", -- Affects how nodes get inserted into the tree during creation/pasting/moving of files if the node under the cursor is a directory:
|
||||
-- "child": Insert nodes as children of the directory under cursor.
|
||||
-- "sibling": Insert nodes as siblings of the directory under cursor.
|
||||
-- Mappings for tree window. See `:h neo-tree-mappings` for a list of built-in commands.
|
||||
-- You can also create your own commands by providing a function instead of a string.
|
||||
mapping_options = {
|
||||
noremap = true,
|
||||
nowait = true,
|
||||
},
|
||||
mappings = {
|
||||
["<space>"] = {
|
||||
"toggle_node",
|
||||
nowait = false, -- disable `nowait` if you have existing combos starting with this char that you want to use
|
||||
},
|
||||
["<2-LeftMouse>"] = "open",
|
||||
["<cr>"] = "open",
|
||||
-- ["<cr>"] = { "open", config = { expand_nested_files = true } }, -- expand nested file takes precedence
|
||||
["<esc>"] = "cancel", -- close preview or floating neo-tree window
|
||||
["P"] = { "toggle_preview", config = { use_float = true, use_image_nvim = false } },
|
||||
["<C-f>"] = { "scroll_preview", config = {direction = -10} },
|
||||
["<C-b>"] = { "scroll_preview", config = {direction = 10} },
|
||||
["l"] = "focus_preview",
|
||||
["S"] = "open_split",
|
||||
-- ["S"] = "split_with_window_picker",
|
||||
["s"] = "open_vsplit",
|
||||
-- ["sr"] = "open_rightbelow_vs",
|
||||
-- ["sl"] = "open_leftabove_vs",
|
||||
-- ["s"] = "vsplit_with_window_picker",
|
||||
["t"] = "open_tabnew",
|
||||
-- ["<cr>"] = "open_drop",
|
||||
-- ["t"] = "open_tab_drop",
|
||||
["w"] = "open_with_window_picker",
|
||||
["C"] = "close_node",
|
||||
["z"] = "close_all_nodes",
|
||||
--["Z"] = "expand_all_nodes",
|
||||
["R"] = "refresh",
|
||||
["a"] = {
|
||||
"add",
|
||||
-- some commands may take optional config options, see `:h neo-tree-mappings` for details
|
||||
config = {
|
||||
show_path = "none", -- "none", "relative", "absolute"
|
||||
}
|
||||
},
|
||||
["A"] = "add_directory", -- also accepts the config.show_path and config.insert_as options.
|
||||
["d"] = "delete",
|
||||
["r"] = "rename",
|
||||
["y"] = "copy_to_clipboard",
|
||||
["x"] = "cut_to_clipboard",
|
||||
["p"] = "paste_from_clipboard",
|
||||
["c"] = "copy", -- takes text input for destination, also accepts the config.show_path and config.insert_as options
|
||||
["m"] = "move", -- takes text input for destination, also accepts the config.show_path and config.insert_as options
|
||||
["e"] = "toggle_auto_expand_width",
|
||||
["q"] = "close_window",
|
||||
["?"] = "show_help",
|
||||
["<"] = "prev_source",
|
||||
[">"] = "next_source",
|
||||
},
|
||||
},
|
||||
filesystem = {
|
||||
window = {
|
||||
mappings = {
|
||||
["H"] = "toggle_hidden",
|
||||
["/"] = "fuzzy_finder",
|
||||
["D"] = "fuzzy_finder_directory",
|
||||
--["/"] = "filter_as_you_type", -- this was the default until v1.28
|
||||
["#"] = "fuzzy_sorter", -- fuzzy sorting using the fzy algorithm
|
||||
-- ["D"] = "fuzzy_sorter_directory",
|
||||
["f"] = "filter_on_submit",
|
||||
["<C-x>"] = "clear_filter",
|
||||
["<bs>"] = "navigate_up",
|
||||
["."] = "set_root",
|
||||
["[g"] = "prev_git_modified",
|
||||
["]g"] = "next_git_modified",
|
||||
["i"] = "show_file_details",
|
||||
["o"] = { "show_help", nowait=false, config = { title = "Order by", prefix_key = "o" }},
|
||||
["oc"] = { "order_by_created", nowait = false },
|
||||
["od"] = { "order_by_diagnostics", nowait = false },
|
||||
["og"] = { "order_by_git_status", nowait = false },
|
||||
["om"] = { "order_by_modified", nowait = false },
|
||||
["on"] = { "order_by_name", nowait = false },
|
||||
["os"] = { "order_by_size", nowait = false },
|
||||
["ot"] = { "order_by_type", nowait = false },
|
||||
},
|
||||
fuzzy_finder_mappings = { -- define keymaps for filter popup window in fuzzy_finder_mode
|
||||
["<down>"] = "move_cursor_down",
|
||||
["<C-n>"] = "move_cursor_down",
|
||||
["<up>"] = "move_cursor_up",
|
||||
["<C-p>"] = "move_cursor_up",
|
||||
},
|
||||
},
|
||||
async_directory_scan = "auto", -- "auto" means refreshes are async, but it's synchronous when called from the Neotree commands.
|
||||
-- "always" means directory scans are always async.
|
||||
-- "never" means directory scans are never async.
|
||||
scan_mode = "shallow", -- "shallow": Don't scan into directories to detect possible empty directory a priori
|
||||
-- "deep": Scan into directories to detect empty or grouped empty directories a priori.
|
||||
bind_to_cwd = true, -- true creates a 2-way binding between vim's cwd and neo-tree's root
|
||||
cwd_target = {
|
||||
sidebar = "tab", -- sidebar is when position = left or right
|
||||
current = "window" -- current is when position = current
|
||||
},
|
||||
check_gitignore_in_search = true, -- check gitignore status for files/directories when searching
|
||||
-- setting this to false will speed up searches, but gitignored
|
||||
-- items won't be marked if they are visible.
|
||||
-- The renderer section provides the renderers that will be used to render the tree.
|
||||
-- The first level is the node type.
|
||||
-- For each node type, you can specify a list of components to render.
|
||||
-- Components are rendered in the order they are specified.
|
||||
-- The first field in each component is the name of the function to call.
|
||||
-- The rest of the fields are passed to the function as the "config" argument.
|
||||
filtered_items = {
|
||||
visible = false, -- when true, they will just be displayed differently than normal items
|
||||
force_visible_in_empty_folder = false, -- when true, hidden files will be shown if the root folder is otherwise empty
|
||||
show_hidden_count = true, -- when true, the number of hidden items in each folder will be shown as the last entry
|
||||
hide_dotfiles = true,
|
||||
hide_gitignored = true,
|
||||
hide_hidden = true, -- only works on Windows for hidden files/directories
|
||||
hide_by_name = {
|
||||
".DS_Store",
|
||||
"thumbs.db"
|
||||
--"node_modules",
|
||||
},
|
||||
hide_by_pattern = { -- uses glob style patterns
|
||||
--"*.meta",
|
||||
--"*/src/*/tsconfig.json"
|
||||
},
|
||||
always_show = { -- remains visible even if other settings would normally hide it
|
||||
--".gitignored",
|
||||
},
|
||||
never_show = { -- remains hidden even if visible is toggled to true, this overrides always_show
|
||||
--".DS_Store",
|
||||
--"thumbs.db"
|
||||
},
|
||||
never_show_by_pattern = { -- uses glob style patterns
|
||||
--".null-ls_*",
|
||||
},
|
||||
},
|
||||
find_by_full_path_words = false, -- `false` means it only searches the tail of a path.
|
||||
-- `true` will change the filter into a full path
|
||||
-- search with space as an implicit ".*", so
|
||||
-- `fi init`
|
||||
-- will match: `./sources/filesystem/init.lua
|
||||
--find_command = "fd", -- this is determined automatically, you probably don't need to set it
|
||||
--find_args = { -- you can specify extra args to pass to the find command.
|
||||
-- fd = {
|
||||
-- "--exclude", ".git",
|
||||
-- "--exclude", "node_modules"
|
||||
-- }
|
||||
--},
|
||||
---- or use a function instead of list of strings
|
||||
--find_args = function(cmd, path, search_term, args)
|
||||
-- if cmd ~= "fd" then
|
||||
-- return args
|
||||
-- end
|
||||
-- --maybe you want to force the filter to always include hidden files:
|
||||
-- table.insert(args, "--hidden")
|
||||
-- -- but no one ever wants to see .git files
|
||||
-- table.insert(args, "--exclude")
|
||||
-- table.insert(args, ".git")
|
||||
-- -- or node_modules
|
||||
-- table.insert(args, "--exclude")
|
||||
-- table.insert(args, "node_modules")
|
||||
-- --here is where it pays to use the function, you can exclude more for
|
||||
-- --short search terms, or vary based on the directory
|
||||
-- if string.len(search_term) < 4 and path == "/home/cseickel" then
|
||||
-- table.insert(args, "--exclude")
|
||||
-- table.insert(args, "Library")
|
||||
-- end
|
||||
-- return args
|
||||
--end,
|
||||
group_empty_dirs = false, -- when true, empty folders will be grouped together
|
||||
search_limit = 50, -- max number of search results when using filters
|
||||
follow_current_file = {
|
||||
enabled = false, -- This will find and focus the file in the active buffer every time
|
||||
-- -- the current file is changed while the tree is open.
|
||||
leave_dirs_open = false, -- `false` closes auto expanded dirs, such as with `:Neotree reveal`
|
||||
},
|
||||
hijack_netrw_behavior = "open_default", -- netrw disabled, opening a directory opens neo-tree
|
||||
-- in whatever position is specified in window.position
|
||||
-- "open_current",-- netrw disabled, opening a directory opens within the
|
||||
-- window like netrw would, regardless of window.position
|
||||
-- "disabled", -- netrw left alone, neo-tree does not handle opening dirs
|
||||
use_libuv_file_watcher = false, -- This will use the OS level file watchers to detect changes
|
||||
-- instead of relying on nvim autocmd events.
|
||||
},
|
||||
buffers = {
|
||||
bind_to_cwd = true,
|
||||
follow_current_file = {
|
||||
enabled = true, -- This will find and focus the file in the active buffer every time
|
||||
-- -- the current file is changed while the tree is open.
|
||||
leave_dirs_open = false, -- `false` closes auto expanded dirs, such as with `:Neotree reveal`
|
||||
},
|
||||
group_empty_dirs = true, -- when true, empty directories will be grouped together
|
||||
show_unloaded = false, -- When working with sessions, for example, restored but unfocused buffers
|
||||
-- are mark as "unloaded". Turn this on to view these unloaded buffer.
|
||||
terminals_first = false, -- when true, terminals will be listed before file buffers
|
||||
window = {
|
||||
mappings = {
|
||||
["<bs>"] = "navigate_up",
|
||||
["."] = "set_root",
|
||||
["bd"] = "buffer_delete",
|
||||
["i"] = "show_file_details",
|
||||
["o"] = { "show_help", nowait=false, config = { title = "Order by", prefix_key = "o" }},
|
||||
["oc"] = { "order_by_created", nowait = false },
|
||||
["od"] = { "order_by_diagnostics", nowait = false },
|
||||
["om"] = { "order_by_modified", nowait = false },
|
||||
["on"] = { "order_by_name", nowait = false },
|
||||
["os"] = { "order_by_size", nowait = false },
|
||||
["ot"] = { "order_by_type", nowait = false },
|
||||
},
|
||||
},
|
||||
},
|
||||
git_status = {
|
||||
window = {
|
||||
mappings = {
|
||||
["A"] = "git_add_all",
|
||||
["gu"] = "git_unstage_file",
|
||||
["ga"] = "git_add_file",
|
||||
["gr"] = "git_revert_file",
|
||||
["gc"] = "git_commit",
|
||||
["gp"] = "git_push",
|
||||
["gg"] = "git_commit_and_push",
|
||||
["i"] = "show_file_details",
|
||||
["o"] = { "show_help", nowait=false, config = { title = "Order by", prefix_key = "o" }},
|
||||
["oc"] = { "order_by_created", nowait = false },
|
||||
["od"] = { "order_by_diagnostics", nowait = false },
|
||||
["om"] = { "order_by_modified", nowait = false },
|
||||
["on"] = { "order_by_name", nowait = false },
|
||||
["os"] = { "order_by_size", nowait = false },
|
||||
["ot"] = { "order_by_type", nowait = false },
|
||||
},
|
||||
},
|
||||
},
|
||||
document_symbols = {
|
||||
follow_cursor = false,
|
||||
client_filters = "first",
|
||||
renderers = {
|
||||
root = {
|
||||
{"indent"},
|
||||
{"icon", default="C" },
|
||||
{"name", zindex = 10},
|
||||
},
|
||||
symbol = {
|
||||
{"indent", with_expanders = true},
|
||||
{"kind_icon", default="?" },
|
||||
{"container",
|
||||
content = {
|
||||
{"name", zindex = 10},
|
||||
{"kind_name", zindex = 20, align = "right"},
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
window = {
|
||||
mappings = {
|
||||
["<cr>"] = "jump_to_symbol",
|
||||
["o"] = "jump_to_symbol",
|
||||
["A"] = "noop", -- also accepts the config.show_path and config.insert_as options.
|
||||
["d"] = "noop",
|
||||
["y"] = "noop",
|
||||
["x"] = "noop",
|
||||
["p"] = "noop",
|
||||
["c"] = "noop",
|
||||
["m"] = "noop",
|
||||
["a"] = "noop",
|
||||
["/"] = "filter",
|
||||
["f"] = "filter_on_submit",
|
||||
},
|
||||
},
|
||||
custom_kinds = {
|
||||
-- define custom kinds here (also remember to add icon and hl group to kinds)
|
||||
-- ccls
|
||||
-- [252] = 'TypeAlias',
|
||||
-- [253] = 'Parameter',
|
||||
-- [254] = 'StaticMethod',
|
||||
-- [255] = 'Macro',
|
||||
},
|
||||
kinds = {
|
||||
Unknown = { icon = "?", hl = "" },
|
||||
Root = { icon = "", hl = "NeoTreeRootName" },
|
||||
File = { icon = "", hl = "Tag" },
|
||||
Module = { icon = "", hl = "Exception" },
|
||||
Namespace = { icon = "", hl = "Include" },
|
||||
Package = { icon = "", hl = "Label" },
|
||||
Class = { icon = "", hl = "Include" },
|
||||
Method = { icon = "", hl = "Function" },
|
||||
Property = { icon = "", hl = "@property" },
|
||||
Field = { icon = "", hl = "@field" },
|
||||
Constructor = { icon = "", hl = "@constructor" },
|
||||
Enum = { icon = "", hl = "@number" },
|
||||
Interface = { icon = "", hl = "Type" },
|
||||
Function = { icon = "", hl = "Function" },
|
||||
Variable = { icon = "", hl = "@variable" },
|
||||
Constant = { icon = "", hl = "Constant" },
|
||||
String = { icon = "", hl = "String" },
|
||||
Number = { icon = "", hl = "Number" },
|
||||
Boolean = { icon = "", hl = "Boolean" },
|
||||
Array = { icon = "", hl = "Type" },
|
||||
Object = { icon = "", hl = "Type" },
|
||||
Key = { icon = "", hl = "" },
|
||||
Null = { icon = "", hl = "Constant" },
|
||||
EnumMember = { icon = "", hl = "Number" },
|
||||
Struct = { icon = "", hl = "Type" },
|
||||
Event = { icon = "", hl = "Constant" },
|
||||
Operator = { icon = "", hl = "Operator" },
|
||||
TypeParameter = { icon = "", hl = "Type" },
|
||||
|
||||
-- ccls
|
||||
-- TypeAlias = { icon = ' ', hl = 'Type' },
|
||||
-- Parameter = { icon = ' ', hl = '@parameter' },
|
||||
-- StaticMethod = { icon = ' ', hl = 'Function' },
|
||||
-- Macro = { icon = ' ', hl = 'Macro' },
|
||||
}
|
||||
},
|
||||
example = {
|
||||
renderers = {
|
||||
custom = {
|
||||
{"indent"},
|
||||
{"icon", default="C" },
|
||||
{"custom"},
|
||||
{"name"}
|
||||
}
|
||||
},
|
||||
window = {
|
||||
mappings = {
|
||||
["<cr>"] = "toggle_node",
|
||||
["<C-e>"] = "example_command",
|
||||
["d"] = "show_debug_info",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
return config
|
||||
@ -0,0 +1,95 @@
|
||||
local vim = vim
|
||||
local q = require("neo-tree.events.queue")
|
||||
local log = require("neo-tree.log")
|
||||
local utils = require("neo-tree.utils")
|
||||
|
||||
local M = {
|
||||
-- Well known event names, you can make up your own
|
||||
STATE_CREATED = "state_created",
|
||||
BEFORE_RENDER = "before_render",
|
||||
AFTER_RENDER = "after_render",
|
||||
FILE_ADDED = "file_added",
|
||||
FILE_DELETED = "file_deleted",
|
||||
BEFORE_FILE_MOVE = "before_file_move",
|
||||
FILE_MOVED = "file_moved",
|
||||
FILE_OPEN_REQUESTED = "file_open_requested",
|
||||
FILE_OPENED = "file_opened",
|
||||
BEFORE_FILE_RENAME = "before_file_rename",
|
||||
FILE_RENAMED = "file_renamed",
|
||||
FS_EVENT = "fs_event",
|
||||
GIT_EVENT = "git_event",
|
||||
GIT_STATUS_CHANGED = "git_status_changed",
|
||||
NEO_TREE_BUFFER_ENTER = "neo_tree_buffer_enter",
|
||||
NEO_TREE_BUFFER_LEAVE = "neo_tree_buffer_leave",
|
||||
NEO_TREE_LSP_UPDATE = "neo_tree_lsp_update",
|
||||
NEO_TREE_POPUP_BUFFER_ENTER = "neo_tree_popup_buffer_enter",
|
||||
NEO_TREE_POPUP_BUFFER_LEAVE = "neo_tree_popup_buffer_leave",
|
||||
NEO_TREE_POPUP_INPUT_READY = "neo_tree_popup_input_ready",
|
||||
NEO_TREE_WINDOW_AFTER_CLOSE = "neo_tree_window_after_close",
|
||||
NEO_TREE_WINDOW_AFTER_OPEN = "neo_tree_window_after_open",
|
||||
NEO_TREE_WINDOW_BEFORE_CLOSE = "neo_tree_window_before_close",
|
||||
NEO_TREE_WINDOW_BEFORE_OPEN = "neo_tree_window_before_open",
|
||||
VIM_AFTER_SESSION_LOAD = "vim_after_session_load",
|
||||
VIM_BUFFER_ADDED = "vim_buffer_added",
|
||||
VIM_BUFFER_CHANGED = "vim_buffer_changed",
|
||||
VIM_BUFFER_DELETED = "vim_buffer_deleted",
|
||||
VIM_BUFFER_ENTER = "vim_buffer_enter",
|
||||
VIM_BUFFER_MODIFIED_SET = "vim_buffer_modified_set",
|
||||
VIM_COLORSCHEME = "vim_colorscheme",
|
||||
VIM_CURSOR_MOVED = "vim_cursor_moved",
|
||||
VIM_DIAGNOSTIC_CHANGED = "vim_diagnostic_changed",
|
||||
VIM_DIR_CHANGED = "vim_dir_changed",
|
||||
VIM_INSERT_LEAVE = "vim_insert_leave",
|
||||
VIM_LEAVE = "vim_leave",
|
||||
VIM_LSP_REQUEST = "vim_lsp_request",
|
||||
VIM_RESIZED = "vim_resized",
|
||||
VIM_TAB_CLOSED = "vim_tab_closed",
|
||||
VIM_TERMINAL_ENTER = "vim_terminal_enter",
|
||||
VIM_TEXT_CHANGED_NORMAL = "vim_text_changed_normal",
|
||||
VIM_WIN_CLOSED = "vim_win_closed",
|
||||
VIM_WIN_ENTER = "vim_win_enter",
|
||||
}
|
||||
|
||||
M.define_autocmd_event = function(event_name, autocmds, debounce_frequency, seed_fn, nested)
|
||||
local opts = {
|
||||
setup = function()
|
||||
local tpl =
|
||||
":lua require('neo-tree.events').fire_event('%s', { afile = vim.F.npcall(vim.fn.expand, '<afile>') or '' })"
|
||||
local callback = string.format(tpl, event_name)
|
||||
if nested then
|
||||
callback = "++nested " .. callback
|
||||
end
|
||||
|
||||
local autocmd = table.concat(autocmds, ",")
|
||||
if not vim.startswith(autocmd, "User") then
|
||||
autocmd = autocmd .. " *"
|
||||
end
|
||||
local cmds = {
|
||||
"augroup NeoTreeEvent_" .. event_name,
|
||||
"autocmd " .. autocmd .. " " .. callback,
|
||||
"augroup END",
|
||||
}
|
||||
log.trace("Registering autocmds: %s", table.concat(cmds, "\n"))
|
||||
vim.cmd(table.concat(cmds, "\n"))
|
||||
end,
|
||||
seed = seed_fn,
|
||||
teardown = function()
|
||||
log.trace("Teardown autocmds for ", event_name)
|
||||
vim.cmd(string.format("autocmd! NeoTreeEvent_%s", event_name))
|
||||
end,
|
||||
debounce_frequency = debounce_frequency,
|
||||
debounce_strategy = utils.debounce_strategy.CALL_LAST_ONLY,
|
||||
}
|
||||
log.debug("Defining autocmd event: %s", event_name)
|
||||
q.define_event(event_name, opts)
|
||||
end
|
||||
|
||||
M.clear_all_events = q.clear_all_events
|
||||
M.define_event = q.define_event
|
||||
M.destroy_event = q.destroy_event
|
||||
M.fire_event = q.fire_event
|
||||
|
||||
M.subscribe = q.subscribe
|
||||
M.unsubscribe = q.unsubscribe
|
||||
|
||||
return M
|
||||
@ -0,0 +1,144 @@
|
||||
local utils = require("neo-tree.utils")
|
||||
local log = require("neo-tree.log")
|
||||
local Queue = require("neo-tree.collections").Queue
|
||||
|
||||
local event_queues = {}
|
||||
local event_definitions = {}
|
||||
local M = {}
|
||||
|
||||
local validate_event_handler = function(event_handler)
|
||||
if type(event_handler) ~= "table" then
|
||||
error("Event handler must be a table")
|
||||
end
|
||||
if type(event_handler.event) ~= "string" then
|
||||
error("Event handler must have an event")
|
||||
end
|
||||
if type(event_handler.handler) ~= "function" then
|
||||
error("Event handler must have a handler")
|
||||
end
|
||||
end
|
||||
|
||||
M.clear_all_events = function()
|
||||
for event_name, queue in pairs(event_queues) do
|
||||
M.destroy_event(event_name)
|
||||
end
|
||||
event_queues = {}
|
||||
end
|
||||
|
||||
M.define_event = function(event_name, opts)
|
||||
local existing = event_definitions[event_name]
|
||||
if existing ~= nil then
|
||||
error("Event already defined: " .. event_name)
|
||||
end
|
||||
event_definitions[event_name] = opts
|
||||
end
|
||||
|
||||
M.destroy_event = function(event_name)
|
||||
local existing = event_definitions[event_name]
|
||||
if existing == nil then
|
||||
return false
|
||||
end
|
||||
if existing.setup_was_run and type(existing.teardown) == "function" then
|
||||
local success, result = pcall(existing.teardown)
|
||||
if not success then
|
||||
error("Error in teardown for " .. event_name .. ": " .. result)
|
||||
end
|
||||
existing.setup_was_run = false
|
||||
end
|
||||
event_queues[event_name] = nil
|
||||
return true
|
||||
end
|
||||
|
||||
local fire_event_internal = function(event, args)
|
||||
local queue = event_queues[event]
|
||||
if queue == nil then
|
||||
return nil
|
||||
end
|
||||
--log.trace("Firing event: ", event, " with args: ", args)
|
||||
|
||||
if queue:is_empty() then
|
||||
--log.trace("Event queue is empty")
|
||||
return nil
|
||||
end
|
||||
local seed = utils.get_value(event_definitions, event .. ".seed")
|
||||
if seed ~= nil then
|
||||
local success, result = pcall(seed, args)
|
||||
if success and result then
|
||||
log.trace("Seed for " .. event .. " returned: " .. tostring(result))
|
||||
elseif success then
|
||||
log.trace("Seed for " .. event .. " returned falsy, cancelling event")
|
||||
else
|
||||
log.error("Error in seed function for " .. event .. ": " .. result)
|
||||
end
|
||||
end
|
||||
|
||||
return queue:for_each(function(event_handler)
|
||||
local remove_node = event_handler == nil or event_handler.cancelled
|
||||
if not remove_node then
|
||||
local success, result = pcall(event_handler.handler, args)
|
||||
local id = event_handler.id or event_handler
|
||||
if success then
|
||||
log.trace("Handler ", id, " for " .. event .. " called successfully.")
|
||||
else
|
||||
log.error(string.format("Error in event handler for event %s[%s]: %s", event, id, result))
|
||||
end
|
||||
if event_handler.once then
|
||||
event_handler.cancelled = true
|
||||
return true
|
||||
end
|
||||
return result
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
M.fire_event = function(event, args)
|
||||
local freq = utils.get_value(event_definitions, event .. ".debounce_frequency", 0, true)
|
||||
local strategy = utils.get_value(event_definitions, event .. ".debounce_strategy", 0, true)
|
||||
log.trace("Firing event: ", event, " with args: ", args)
|
||||
if freq > 0 then
|
||||
utils.debounce("EVENT_FIRED: " .. event, function()
|
||||
fire_event_internal(event, args or {})
|
||||
end, freq, strategy)
|
||||
else
|
||||
return fire_event_internal(event, args or {})
|
||||
end
|
||||
end
|
||||
|
||||
M.subscribe = function(event_handler)
|
||||
validate_event_handler(event_handler)
|
||||
|
||||
local queue = event_queues[event_handler.event]
|
||||
if queue == nil then
|
||||
log.debug("Creating queue for event: " .. event_handler.event)
|
||||
queue = Queue:new()
|
||||
local def = event_definitions[event_handler.event]
|
||||
if def and type(def.setup) == "function" then
|
||||
local success, result = pcall(def.setup)
|
||||
if success then
|
||||
def.setup_was_run = true
|
||||
log.debug("Setup for event " .. event_handler.event .. " was run")
|
||||
else
|
||||
log.error("Error in setup for " .. event_handler.event .. ": " .. result)
|
||||
end
|
||||
end
|
||||
event_queues[event_handler.event] = queue
|
||||
end
|
||||
log.debug("Adding event handler [", event_handler.id, "] for event: ", event_handler.event)
|
||||
queue:add(event_handler)
|
||||
end
|
||||
|
||||
M.unsubscribe = function(event_handler)
|
||||
local queue = event_queues[event_handler.event]
|
||||
if queue == nil then
|
||||
return nil
|
||||
end
|
||||
queue:remove_by_id(event_handler.id or event_handler)
|
||||
if queue:is_empty() then
|
||||
M.destroy_event(event_handler.event)
|
||||
event_queues[event_handler.event] = nil
|
||||
else
|
||||
event_queues[event_handler.event] = queue
|
||||
end
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,176 @@
|
||||
local Job = require("plenary.job")
|
||||
|
||||
local utils = require("neo-tree.utils")
|
||||
local log = require("neo-tree.log")
|
||||
local git_utils = require("neo-tree.git.utils")
|
||||
|
||||
local M = {}
|
||||
local sep = utils.path_separator
|
||||
|
||||
M.is_ignored = function(ignored, path, _type)
|
||||
if _type == "directory" and not utils.is_windows then
|
||||
path = path .. sep
|
||||
end
|
||||
|
||||
return vim.tbl_contains(ignored, path)
|
||||
end
|
||||
|
||||
local git_root_cache = {
|
||||
known_roots = {},
|
||||
dir_lookup = {},
|
||||
}
|
||||
local get_root_for_item = function(item)
|
||||
local dir = item.type == "directory" and item.path or item.parent_path
|
||||
if type(git_root_cache.dir_lookup[dir]) ~= "nil" then
|
||||
return git_root_cache.dir_lookup[dir]
|
||||
end
|
||||
--for _, root in ipairs(git_root_cache.known_roots) do
|
||||
-- if vim.startswith(dir, root) then
|
||||
-- git_root_cache.dir_lookup[dir] = root
|
||||
-- return root
|
||||
-- end
|
||||
--end
|
||||
local root = git_utils.get_repository_root(dir)
|
||||
if root then
|
||||
git_root_cache.dir_lookup[dir] = root
|
||||
table.insert(git_root_cache.known_roots, root)
|
||||
else
|
||||
git_root_cache.dir_lookup[dir] = false
|
||||
end
|
||||
return root
|
||||
end
|
||||
|
||||
M.mark_ignored = function(state, items, callback)
|
||||
local folders = {}
|
||||
log.trace("================================================================================")
|
||||
log.trace("IGNORED: mark_ignore BEGIN...")
|
||||
|
||||
for _, item in ipairs(items) do
|
||||
local folder = utils.split_path(item.path)
|
||||
if folder then
|
||||
if not folders[folder] then
|
||||
folders[folder] = {}
|
||||
end
|
||||
table.insert(folders[folder], item.path)
|
||||
end
|
||||
end
|
||||
|
||||
local function process_result(result)
|
||||
if utils.is_windows then
|
||||
--on Windows, git seems to return quotes and double backslash "path\\directory"
|
||||
result = vim.tbl_map(function(item)
|
||||
item = item:gsub("\\\\", "\\")
|
||||
return item
|
||||
end, result)
|
||||
else
|
||||
--check-ignore does not indicate directories the same as 'status' so we need to
|
||||
--add the trailing slash to the path manually if not on Windows.
|
||||
log.trace("IGNORED: Checking types of", #result, "items to see which ones are directories")
|
||||
for i, item in ipairs(result) do
|
||||
local stat = vim.loop.fs_stat(item)
|
||||
if stat and stat.type == "directory" then
|
||||
result[i] = item .. sep
|
||||
end
|
||||
end
|
||||
end
|
||||
result = vim.tbl_map(function(item)
|
||||
-- remove leading and trailing " from git output
|
||||
item = item:gsub('^"', ""):gsub('"$', "")
|
||||
-- convert octal encoded lines to utf-8
|
||||
item = git_utils.octal_to_utf8(item)
|
||||
return item
|
||||
end, result)
|
||||
return result
|
||||
end
|
||||
|
||||
local function finalize(all_results)
|
||||
local show_gitignored = state.filtered_items and state.filtered_items.hide_gitignored == false
|
||||
log.trace("IGNORED: Comparing results to mark items as ignored:", show_gitignored)
|
||||
local ignored, not_ignored = 0, 0
|
||||
for _, item in ipairs(items) do
|
||||
if M.is_ignored(all_results, item.path, item.type) then
|
||||
item.filtered_by = item.filtered_by or {}
|
||||
item.filtered_by.gitignored = true
|
||||
item.filtered_by.show_gitignored = show_gitignored
|
||||
ignored = ignored + 1
|
||||
else
|
||||
not_ignored = not_ignored + 1
|
||||
end
|
||||
end
|
||||
log.trace("IGNORED: mark_ignored is complete, ignored:", ignored, ", not ignored:", not_ignored)
|
||||
log.trace("================================================================================")
|
||||
end
|
||||
|
||||
local all_results = {}
|
||||
if type(callback) == "function" then
|
||||
local jobs = {}
|
||||
local running_jobs = 0
|
||||
local job_count = 0
|
||||
local completed_jobs = 0
|
||||
|
||||
-- This is called when a job completes, and starts the next job if there are any left
|
||||
-- or calls the callback if all jobs are complete.
|
||||
-- It is also called once at the start to start the first 50 jobs.
|
||||
--
|
||||
-- This is done to avoid running too many jobs at once, which can cause a crash from
|
||||
-- having too many open files.
|
||||
local run_more_jobs = function()
|
||||
while #jobs > 0 and running_jobs < 50 and job_count > completed_jobs do
|
||||
local next_job = table.remove(jobs, #jobs)
|
||||
next_job:start()
|
||||
running_jobs = running_jobs + 1
|
||||
end
|
||||
|
||||
if completed_jobs == job_count then
|
||||
finalize(all_results)
|
||||
callback(all_results)
|
||||
end
|
||||
end
|
||||
|
||||
for folder, folder_items in pairs(folders) do
|
||||
local args = { "-C", folder, "check-ignore", "--stdin" }
|
||||
local job = Job:new({
|
||||
command = "git",
|
||||
args = args,
|
||||
enabled_recording = true,
|
||||
writer = folder_items,
|
||||
on_start = function()
|
||||
log.trace("IGNORED: Running async git with args: ", args)
|
||||
end,
|
||||
on_exit = function(self, code, _)
|
||||
local result
|
||||
if code ~= 0 then
|
||||
log.debug("Failed to load ignored files for", folder, ":", self:stderr_result())
|
||||
result = {}
|
||||
else
|
||||
result = self:result()
|
||||
end
|
||||
vim.list_extend(all_results, process_result(result))
|
||||
|
||||
running_jobs = running_jobs - 1
|
||||
completed_jobs = completed_jobs + 1
|
||||
run_more_jobs()
|
||||
end,
|
||||
})
|
||||
table.insert(jobs, job)
|
||||
job_count = job_count + 1
|
||||
end
|
||||
|
||||
run_more_jobs()
|
||||
else
|
||||
for folder, folder_items in pairs(folders) do
|
||||
local cmd = { "git", "-C", folder, "check-ignore", unpack(folder_items) }
|
||||
log.trace("IGNORED: Running cmd: ", cmd)
|
||||
local result = vim.fn.systemlist(cmd)
|
||||
if vim.v.shell_error == 128 then
|
||||
log.debug("Failed to load ignored files for", state.path, ":", result)
|
||||
result = {}
|
||||
end
|
||||
vim.list_extend(all_results, process_result(result))
|
||||
end
|
||||
finalize(all_results)
|
||||
return all_results
|
||||
end
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,13 @@
|
||||
local status = require("neo-tree.git.status")
|
||||
local ignored = require("neo-tree.git.ignored")
|
||||
local git_utils = require("neo-tree.git.utils")
|
||||
|
||||
local M = {
|
||||
get_repository_root = git_utils.get_repository_root,
|
||||
is_ignored = ignored.is_ignored,
|
||||
mark_ignored = ignored.mark_ignored,
|
||||
status = status.status,
|
||||
status_async = status.status_async,
|
||||
}
|
||||
|
||||
return M
|
||||
@ -0,0 +1,338 @@
|
||||
local utils = require("neo-tree.utils")
|
||||
local events = require("neo-tree.events")
|
||||
local Job = require("plenary.job")
|
||||
local log = require("neo-tree.log")
|
||||
local git_utils = require("neo-tree.git.utils")
|
||||
|
||||
local M = {}
|
||||
|
||||
local function get_simple_git_status_code(status)
|
||||
-- Prioritze M then A over all others
|
||||
if status:match("U") or status == "AA" or status == "DD" then
|
||||
return "U"
|
||||
elseif status:match("M") then
|
||||
return "M"
|
||||
elseif status:match("[ACR]") then
|
||||
return "A"
|
||||
elseif status:match("!$") then
|
||||
return "!"
|
||||
elseif status:match("?$") then
|
||||
return "?"
|
||||
else
|
||||
local len = #status
|
||||
while len > 0 do
|
||||
local char = status:sub(len, len)
|
||||
if char ~= " " then
|
||||
return char
|
||||
end
|
||||
len = len - 1
|
||||
end
|
||||
return status
|
||||
end
|
||||
end
|
||||
|
||||
local function get_priority_git_status_code(status, other_status)
|
||||
if not status then
|
||||
return other_status
|
||||
elseif not other_status then
|
||||
return status
|
||||
elseif status == "U" or other_status == "U" then
|
||||
return "U"
|
||||
elseif status == "?" or other_status == "?" then
|
||||
return "?"
|
||||
elseif status == "M" or other_status == "M" then
|
||||
return "M"
|
||||
elseif status == "A" or other_status == "A" then
|
||||
return "A"
|
||||
else
|
||||
return status
|
||||
end
|
||||
end
|
||||
|
||||
local parse_git_status_line = function(context, line)
|
||||
context.lines_parsed = context.lines_parsed + 1
|
||||
if type(line) ~= "string" then
|
||||
return
|
||||
end
|
||||
if #line < 4 then
|
||||
return
|
||||
end
|
||||
local git_root = context.git_root
|
||||
local git_status = context.git_status
|
||||
local exclude_directories = context.exclude_directories
|
||||
|
||||
local line_parts = vim.split(line, " ")
|
||||
if #line_parts < 2 then
|
||||
return
|
||||
end
|
||||
local status = line_parts[1]
|
||||
local relative_path = line_parts[2]
|
||||
|
||||
-- rename output is `R000 from/filename to/filename`
|
||||
if status:match("^R") then
|
||||
relative_path = line_parts[3]
|
||||
end
|
||||
|
||||
-- remove any " due to whitespace or utf-8 in the path
|
||||
relative_path = relative_path:gsub('^"', ""):gsub('"$', "")
|
||||
-- convert octal encoded lines to utf-8
|
||||
relative_path = git_utils.octal_to_utf8(relative_path)
|
||||
|
||||
if utils.is_windows == true then
|
||||
relative_path = utils.windowize_path(relative_path)
|
||||
end
|
||||
local absolute_path = utils.path_join(git_root, relative_path)
|
||||
-- merge status result if there are results from multiple passes
|
||||
local existing_status = git_status[absolute_path]
|
||||
if existing_status then
|
||||
local merged = ""
|
||||
local i = 0
|
||||
while i < 2 do
|
||||
i = i + 1
|
||||
local existing_char = #existing_status >= i and existing_status:sub(i, i) or ""
|
||||
local new_char = #status >= i and status:sub(i, i) or ""
|
||||
local merged_char = get_priority_git_status_code(existing_char, new_char)
|
||||
merged = merged .. merged_char
|
||||
end
|
||||
status = merged
|
||||
end
|
||||
git_status[absolute_path] = status
|
||||
|
||||
if not exclude_directories then
|
||||
-- Now bubble this status up to the parent directories
|
||||
local parts = utils.split(absolute_path, utils.path_separator)
|
||||
table.remove(parts) -- pop the last part so we don't override the file's status
|
||||
utils.reduce(parts, "", function(acc, part)
|
||||
local path = acc .. utils.path_separator .. part
|
||||
if utils.is_windows == true then
|
||||
path = path:gsub("^" .. utils.path_separator, "")
|
||||
end
|
||||
local path_status = git_status[path]
|
||||
local file_status = get_simple_git_status_code(status)
|
||||
git_status[path] = get_priority_git_status_code(path_status, file_status)
|
||||
return path
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
---Parse "git status" output for the current working directory.
|
||||
---@base git ref base
|
||||
---@exclude_directories boolean Whether to skip bubling up status to directories
|
||||
---@path string Path to run the git status command in, defaults to cwd.
|
||||
---@return table table Table with the path as key and the status as value.
|
||||
---@return table, string|nil The git root for the specified path.
|
||||
M.status = function(base, exclude_directories, path)
|
||||
local git_root = git_utils.get_repository_root(path)
|
||||
if not utils.truthy(git_root) then
|
||||
return {}
|
||||
end
|
||||
|
||||
local C = git_root
|
||||
local staged_cmd = { "git", "-C", C, "diff", "--staged", "--name-status", base, "--" }
|
||||
local staged_ok, staged_result = utils.execute_command(staged_cmd)
|
||||
if not staged_ok then
|
||||
return {}
|
||||
end
|
||||
local unstaged_cmd = { "git", "-C", C, "diff", "--name-status" }
|
||||
local unstaged_ok, unstaged_result = utils.execute_command(unstaged_cmd)
|
||||
if not unstaged_ok then
|
||||
return {}
|
||||
end
|
||||
local untracked_cmd = { "git", "-C", C, "ls-files", "--exclude-standard", "--others" }
|
||||
local untracked_ok, untracked_result = utils.execute_command(untracked_cmd)
|
||||
if not untracked_ok then
|
||||
return {}
|
||||
end
|
||||
|
||||
local context = {
|
||||
git_root = git_root,
|
||||
git_status = {},
|
||||
exclude_directories = exclude_directories,
|
||||
lines_parsed = 0,
|
||||
}
|
||||
|
||||
for _, line in ipairs(staged_result) do
|
||||
parse_git_status_line(context, line)
|
||||
end
|
||||
for _, line in ipairs(unstaged_result) do
|
||||
if line then
|
||||
line = " " .. line
|
||||
end
|
||||
parse_git_status_line(context, line)
|
||||
end
|
||||
for _, line in ipairs(untracked_result) do
|
||||
if line then
|
||||
line = "? " .. line
|
||||
end
|
||||
parse_git_status_line(context, line)
|
||||
end
|
||||
|
||||
return context.git_status, git_root
|
||||
end
|
||||
|
||||
local function parse_lines_batch(context, job_complete_callback)
|
||||
local i, batch_size = 0, context.batch_size
|
||||
|
||||
if context.lines_total == nil then
|
||||
-- first time through, get the total number of lines
|
||||
context.lines_total = math.min(context.max_lines, #context.lines)
|
||||
context.lines_parsed = 0
|
||||
if context.lines_total == 0 then
|
||||
if type(job_complete_callback) == "function" then
|
||||
job_complete_callback()
|
||||
end
|
||||
return
|
||||
end
|
||||
end
|
||||
batch_size = math.min(context.batch_size, context.lines_total - context.lines_parsed)
|
||||
|
||||
while i < batch_size do
|
||||
i = i + 1
|
||||
parse_git_status_line(context, context.lines[context.lines_parsed + 1])
|
||||
end
|
||||
|
||||
if context.lines_parsed >= context.lines_total then
|
||||
if type(job_complete_callback) == "function" then
|
||||
job_complete_callback()
|
||||
end
|
||||
else
|
||||
-- add small delay so other work can happen
|
||||
vim.defer_fn(function()
|
||||
parse_lines_batch(context, job_complete_callback)
|
||||
end, context.batch_delay)
|
||||
end
|
||||
end
|
||||
|
||||
M.status_async = function(path, base, opts)
|
||||
git_utils.get_repository_root(path, function(git_root)
|
||||
if utils.truthy(git_root) then
|
||||
log.trace("git.status.status_async called")
|
||||
else
|
||||
log.trace("status_async: not a git folder: ", path)
|
||||
return false
|
||||
end
|
||||
|
||||
local event_id = "git_status_" .. git_root
|
||||
local context = {
|
||||
git_root = git_root,
|
||||
git_status = {},
|
||||
exclude_directories = false,
|
||||
lines = {},
|
||||
lines_parsed = 0,
|
||||
batch_size = opts.batch_size or 1000,
|
||||
batch_delay = opts.batch_delay or 10,
|
||||
max_lines = opts.max_lines or 100000,
|
||||
}
|
||||
|
||||
local should_process = function(err, line, job, err_msg)
|
||||
if vim.v.dying > 0 or vim.v.exiting ~= vim.NIL then
|
||||
job:shutdown()
|
||||
return false
|
||||
end
|
||||
if err and err > 0 then
|
||||
log.error(err_msg, err, line)
|
||||
return false
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
local job_complete_callback = function()
|
||||
vim.schedule(function()
|
||||
events.fire_event(events.GIT_STATUS_CHANGED, {
|
||||
git_root = context.git_root,
|
||||
git_status = context.git_status,
|
||||
})
|
||||
end)
|
||||
end
|
||||
|
||||
local parse_lines = vim.schedule_wrap(function()
|
||||
parse_lines_batch(context, job_complete_callback)
|
||||
end)
|
||||
|
||||
utils.debounce(event_id, function()
|
||||
local staged_job = Job:new({
|
||||
command = "git",
|
||||
args = { "-C", git_root, "diff", "--staged", "--name-status", base, "--" },
|
||||
enable_recording = false,
|
||||
maximium_results = context.max_lines,
|
||||
on_stdout = vim.schedule_wrap(function(err, line, job)
|
||||
if should_process(err, line, job, "status_async staged error:") then
|
||||
table.insert(context.lines, line)
|
||||
end
|
||||
end),
|
||||
on_stderr = function(err, line)
|
||||
if err and err > 0 then
|
||||
log.error("status_async staged error: ", err, line)
|
||||
end
|
||||
end,
|
||||
})
|
||||
|
||||
local unstaged_job = Job:new({
|
||||
command = "git",
|
||||
args = { "-C", git_root, "diff", "--name-status" },
|
||||
enable_recording = false,
|
||||
maximium_results = context.max_lines,
|
||||
on_stdout = vim.schedule_wrap(function(err, line, job)
|
||||
if should_process(err, line, job, "status_async unstaged error:") then
|
||||
if line then
|
||||
line = " " .. line
|
||||
end
|
||||
table.insert(context.lines, line)
|
||||
end
|
||||
end),
|
||||
on_stderr = function(err, line)
|
||||
if err and err > 0 then
|
||||
log.error("status_async unstaged error: ", err, line)
|
||||
end
|
||||
end,
|
||||
})
|
||||
|
||||
local untracked_job = Job:new({
|
||||
command = "git",
|
||||
args = { "-C", git_root, "ls-files", "--exclude-standard", "--others" },
|
||||
enable_recording = false,
|
||||
maximium_results = context.max_lines,
|
||||
on_stdout = vim.schedule_wrap(function(err, line, job)
|
||||
if should_process(err, line, job, "status_async untracked error:") then
|
||||
if line then
|
||||
line = "? " .. line
|
||||
end
|
||||
table.insert(context.lines, line)
|
||||
end
|
||||
end),
|
||||
on_stderr = function(err, line)
|
||||
if err and err > 0 then
|
||||
log.error("status_async untracked error: ", err, line)
|
||||
end
|
||||
end,
|
||||
})
|
||||
|
||||
Job:new({
|
||||
command = "git",
|
||||
args = {
|
||||
"-C",
|
||||
git_root,
|
||||
"config",
|
||||
"--get",
|
||||
"status.showUntrackedFiles",
|
||||
},
|
||||
enabled_recording = true,
|
||||
on_exit = function(self, _, _)
|
||||
local result = self:result()
|
||||
log.debug("git status.showUntrackedFiles =", result[1])
|
||||
if result[1] == "no" then
|
||||
unstaged_job:after(parse_lines)
|
||||
Job.chain(staged_job, unstaged_job)
|
||||
else
|
||||
untracked_job:after(parse_lines)
|
||||
Job.chain(staged_job, unstaged_job, untracked_job)
|
||||
end
|
||||
end,
|
||||
}):start()
|
||||
end, 1000, utils.debounce_strategy.CALL_FIRST_AND_LAST, utils.debounce_action.START_ASYNC_JOB)
|
||||
|
||||
return true
|
||||
end)
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,65 @@
|
||||
local Job = require("plenary.job")
|
||||
|
||||
local utils = require("neo-tree.utils")
|
||||
local log = require("neo-tree.log")
|
||||
|
||||
local M = {}
|
||||
|
||||
M.get_repository_root = function(path, callback)
|
||||
local args = { "rev-parse", "--show-toplevel" }
|
||||
if utils.truthy(path) then
|
||||
args = { "-C", path, "rev-parse", "--show-toplevel" }
|
||||
end
|
||||
if type(callback) == "function" then
|
||||
Job:new({
|
||||
command = "git",
|
||||
args = args,
|
||||
enabled_recording = true,
|
||||
on_exit = function(self, code, _)
|
||||
if code ~= 0 then
|
||||
log.trace("GIT ROOT ERROR ", self:stderr_result())
|
||||
callback(nil)
|
||||
return
|
||||
end
|
||||
local git_root = self:result()[1]
|
||||
|
||||
if utils.is_windows then
|
||||
git_root = utils.windowize_path(git_root)
|
||||
end
|
||||
|
||||
log.trace("GIT ROOT for '", path, "' is '", git_root, "'")
|
||||
callback(git_root)
|
||||
end,
|
||||
}):start()
|
||||
else
|
||||
local ok, git_root = utils.execute_command({ "git", unpack(args) })
|
||||
if not ok then
|
||||
log.trace("GIT ROOT ERROR ", git_root)
|
||||
return nil
|
||||
end
|
||||
git_root = git_root[1]
|
||||
|
||||
if utils.is_windows then
|
||||
git_root = utils.windowize_path(git_root)
|
||||
end
|
||||
|
||||
log.trace("GIT ROOT for '", path, "' is '", git_root, "'")
|
||||
return git_root
|
||||
end
|
||||
end
|
||||
|
||||
local convert_octal_char = function(octal)
|
||||
return string.char(tonumber(octal, 8))
|
||||
end
|
||||
|
||||
M.octal_to_utf8 = function(text)
|
||||
-- git uses octal encoding for utf-8 filepaths, convert octal back to utf-8
|
||||
local success, converted = pcall(string.gsub, text, "\\([0-7][0-7][0-7])", convert_octal_char)
|
||||
if success then
|
||||
return converted
|
||||
else
|
||||
return text
|
||||
end
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,189 @@
|
||||
-- log.lua
|
||||
--
|
||||
-- Inspired by rxi/log.lua
|
||||
-- Modified by tjdevries and can be found at github.com/tjdevries/vlog.nvim
|
||||
--
|
||||
-- This library is free software; you can redistribute it and/or modify it
|
||||
-- under the terms of the MIT license. See LICENSE for details.
|
||||
|
||||
local vim = vim
|
||||
-- User configuration section
|
||||
local default_config = {
|
||||
-- Name of the plugin. Prepended to log messages
|
||||
plugin = "neo-tree.nvim",
|
||||
|
||||
-- Should print the output to neovim while running
|
||||
use_console = true,
|
||||
|
||||
-- Should highlighting be used in console (using echohl)
|
||||
highlights = true,
|
||||
|
||||
-- Should write to a file
|
||||
use_file = false,
|
||||
|
||||
-- Any messages above this level will be logged.
|
||||
level = "info",
|
||||
|
||||
-- Level configuration
|
||||
modes = {
|
||||
{ name = "trace", hl = "None", level = vim.log.levels.TRACE },
|
||||
{ name = "debug", hl = "None", level = vim.log.levels.DEBGUG },
|
||||
{ name = "info", hl = "None", level = vim.log.levels.INFO },
|
||||
{ name = "warn", hl = "WarningMsg", level = vim.log.levels.WARN },
|
||||
{ name = "error", hl = "ErrorMsg", level = vim.log.levels.ERROR },
|
||||
{ name = "fatal", hl = "ErrorMsg", level = vim.log.levels.ERROR },
|
||||
},
|
||||
|
||||
-- Can limit the number of decimals displayed for floats
|
||||
float_precision = 0.01,
|
||||
}
|
||||
|
||||
-- {{{ NO NEED TO CHANGE
|
||||
local log = {}
|
||||
|
||||
local unpack = unpack or table.unpack
|
||||
|
||||
local notify = function(message, level_config)
|
||||
if type(vim.notify) == "table" then
|
||||
-- probably using nvim-notify
|
||||
vim.notify(message, level_config.level, { title = "Neo-tree" })
|
||||
else
|
||||
local nameupper = level_config.name:upper()
|
||||
local console_string = string.format("[Neo-tree %s] %s", nameupper, message)
|
||||
vim.notify(console_string, level_config.level)
|
||||
end
|
||||
end
|
||||
|
||||
log.new = function(config, standalone)
|
||||
config = vim.tbl_deep_extend("force", default_config, config)
|
||||
|
||||
local outfile =
|
||||
string.format("%s/%s.log", vim.api.nvim_call_function("stdpath", { "data" }), config.plugin)
|
||||
|
||||
local obj
|
||||
if standalone then
|
||||
obj = log
|
||||
else
|
||||
obj = {}
|
||||
end
|
||||
obj.outfile = outfile
|
||||
|
||||
obj.use_file = function(file, quiet)
|
||||
if file == false then
|
||||
if not quiet then
|
||||
obj.info("[neo-tree] Logging to file disabled")
|
||||
end
|
||||
config.use_file = false
|
||||
else
|
||||
if type(file) == "string" then
|
||||
obj.outfile = file
|
||||
else
|
||||
obj.outfile = outfile
|
||||
end
|
||||
config.use_file = true
|
||||
if not quiet then
|
||||
obj.info("[neo-tree] Logging to file: " .. obj.outfile)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local levels = {}
|
||||
for i, v in ipairs(config.modes) do
|
||||
levels[v.name] = i
|
||||
end
|
||||
|
||||
obj.set_level = function(level)
|
||||
if levels[level] then
|
||||
if config.level ~= level then
|
||||
config.level = level
|
||||
end
|
||||
else
|
||||
notify("Invalid log level: " .. level, config.modes[5])
|
||||
end
|
||||
end
|
||||
|
||||
local round = function(x, increment)
|
||||
increment = increment or 1
|
||||
x = x / increment
|
||||
return (x > 0 and math.floor(x + 0.5) or math.ceil(x - 0.5)) * increment
|
||||
end
|
||||
|
||||
local make_string = function(...)
|
||||
local t = {}
|
||||
for i = 1, select("#", ...) do
|
||||
local x = select(i, ...)
|
||||
|
||||
if type(x) == "number" and config.float_precision then
|
||||
x = tostring(round(x, config.float_precision))
|
||||
elseif type(x) == "table" then
|
||||
x = vim.inspect(x)
|
||||
if #x > 300 then
|
||||
x = x:sub(1, 300) .. "..."
|
||||
end
|
||||
else
|
||||
x = tostring(x)
|
||||
end
|
||||
|
||||
t[#t + 1] = x
|
||||
end
|
||||
return table.concat(t, " ")
|
||||
end
|
||||
|
||||
local log_at_level = function(level, level_config, message_maker, ...)
|
||||
-- Return early if we're below the config.level
|
||||
if level < levels[config.level] then
|
||||
return
|
||||
end
|
||||
-- Ignnore this if vim is exiting
|
||||
if vim.v.dying > 0 or vim.v.exiting ~= vim.NIL then
|
||||
return
|
||||
end
|
||||
local nameupper = level_config.name:upper()
|
||||
|
||||
local msg = message_maker(...)
|
||||
local info = debug.getinfo(2, "Sl")
|
||||
local lineinfo = info.short_src .. ":" .. info.currentline
|
||||
|
||||
-- Output to log file
|
||||
if config.use_file then
|
||||
local str = string.format("[%-6s%s] %s: %s\n", nameupper, os.date(), lineinfo, msg)
|
||||
local fp = io.open(obj.outfile, "a")
|
||||
if fp then
|
||||
fp:write(str)
|
||||
fp:close()
|
||||
else
|
||||
print("[neo-tree] Could not open log file: " .. obj.outfile)
|
||||
end
|
||||
end
|
||||
|
||||
-- Output to console
|
||||
if config.use_console and level > 2 then
|
||||
vim.schedule(function()
|
||||
notify(msg, level_config)
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
for i, x in ipairs(config.modes) do
|
||||
obj[x.name] = function(...)
|
||||
return log_at_level(i, x, make_string, ...)
|
||||
end
|
||||
|
||||
obj[("fmt_%s"):format(x.name)] = function()
|
||||
return log_at_level(i, x, function(...)
|
||||
local passed = { ... }
|
||||
local fmt = table.remove(passed, 1)
|
||||
local inspected = {}
|
||||
for _, v in ipairs(passed) do
|
||||
table.insert(inspected, vim.inspect(v))
|
||||
end
|
||||
return string.format(fmt, unpack(inspected))
|
||||
end)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
log.new(default_config, true)
|
||||
-- }}}
|
||||
|
||||
return log
|
||||
@ -0,0 +1,135 @@
|
||||
local utils = require("neo-tree.utils")
|
||||
|
||||
local M = {}
|
||||
|
||||
local migrations = {}
|
||||
|
||||
M.show_migrations = function()
|
||||
if #migrations > 0 then
|
||||
local content = {}
|
||||
for _, message in ipairs(migrations) do
|
||||
vim.list_extend(content, vim.split("\n## " .. message, "\n", { trimempty = false }))
|
||||
end
|
||||
local header = "# Neo-tree configuration has been updated. Please review the changes below."
|
||||
table.insert(content, 1, header)
|
||||
local buf = vim.api.nvim_create_buf(false, true)
|
||||
vim.api.nvim_buf_set_lines(buf, 0, -1, false, content)
|
||||
vim.api.nvim_buf_set_option(buf, "buftype", "nofile")
|
||||
vim.api.nvim_buf_set_option(buf, "bufhidden", "wipe")
|
||||
vim.api.nvim_buf_set_option(buf, "buflisted", false)
|
||||
vim.api.nvim_buf_set_option(buf, "swapfile", false)
|
||||
vim.api.nvim_buf_set_option(buf, "modifiable", false)
|
||||
vim.api.nvim_buf_set_option(buf, "filetype", "markdown")
|
||||
vim.api.nvim_buf_set_name(buf, "Neo-tree migrations")
|
||||
vim.defer_fn(function()
|
||||
vim.cmd(string.format("%ssplit", #content))
|
||||
vim.api.nvim_win_set_buf(0, buf)
|
||||
end, 100)
|
||||
end
|
||||
end
|
||||
|
||||
M.migrate = function(config)
|
||||
migrations = {}
|
||||
|
||||
local moved = function(old, new, converter)
|
||||
local existing = utils.get_value(config, old)
|
||||
if type(existing) ~= "nil" then
|
||||
if type(converter) == "function" then
|
||||
existing = converter(existing)
|
||||
end
|
||||
utils.set_value(config, old, nil)
|
||||
utils.set_value(config, new, existing)
|
||||
migrations[#migrations + 1] =
|
||||
string.format("The `%s` option has been deprecated, please use `%s` instead.", old, new)
|
||||
end
|
||||
end
|
||||
|
||||
local moved_inside = function(old, new_inside, converter)
|
||||
local existing = utils.get_value(config, old)
|
||||
if type(existing) ~= "nil" and type(existing) ~= "table" then
|
||||
if type(converter) == "function" then
|
||||
existing = converter(existing)
|
||||
end
|
||||
utils.set_value(config, old, {})
|
||||
local new = old .. "." .. new_inside
|
||||
utils.set_value(config, new, existing)
|
||||
migrations[#migrations + 1] =
|
||||
string.format("The `%s` option is replaced with a table, please move to `%s`.", old, new)
|
||||
end
|
||||
end
|
||||
|
||||
local removed = function(key, desc)
|
||||
local value = utils.get_value(config, key)
|
||||
if type(value) ~= "nil" then
|
||||
utils.set_value(config, key, nil)
|
||||
migrations[#migrations + 1] =
|
||||
string.format("The `%s` option has been removed.\n%s", key, desc or "")
|
||||
end
|
||||
end
|
||||
|
||||
local renamed_value = function(key, old_value, new_value)
|
||||
local value = utils.get_value(config, key)
|
||||
if value == old_value then
|
||||
utils.set_value(config, key, new_value)
|
||||
migrations[#migrations + 1] =
|
||||
string.format("The `%s=%s` option has been renamed to `%s`.", key, old_value, new_value)
|
||||
end
|
||||
end
|
||||
|
||||
local opposite = function(value)
|
||||
return not value
|
||||
end
|
||||
|
||||
local tab_to_source_migrator = function(labels)
|
||||
local converted_sources = {}
|
||||
for entry, label in pairs(labels) do
|
||||
table.insert(converted_sources, { source = entry, display_name = label })
|
||||
end
|
||||
return converted_sources
|
||||
end
|
||||
|
||||
moved("filesystem.filters", "filesystem.filtered_items")
|
||||
moved("filesystem.filters.show_hidden", "filesystem.filtered_items.hide_dotfiles", opposite)
|
||||
moved("filesystem.filters.respect_gitignore", "filesystem.filtered_items.hide_gitignored")
|
||||
moved("open_files_do_not_replace_filetypes", "open_files_do_not_replace_types")
|
||||
moved("source_selector.tab_labels", "source_selector.sources", tab_to_source_migrator)
|
||||
removed("filesystem.filters.gitignore_source")
|
||||
removed("filesystem.filter_items.gitignore_source")
|
||||
renamed_value("filesystem.hijack_netrw_behavior", "open_split", "open_current")
|
||||
for _, source in ipairs({ "filesystem", "buffers", "git_status" }) do
|
||||
renamed_value(source .. "window.position", "split", "current")
|
||||
end
|
||||
moved_inside("filesystem.follow_current_file", "enabled")
|
||||
moved_inside("buffers.follow_current_file", "enabled")
|
||||
|
||||
-- v3.x
|
||||
removed("close_floats_on_escape_key")
|
||||
|
||||
-- v4.x
|
||||
removed(
|
||||
"enable_normal_mode_for_inputs",
|
||||
[[
|
||||
Please use `neo_tree_popup_input_ready` event instead and call `stopinsert` inside the handler.
|
||||
<https://github.com/nvim-neo-tree/neo-tree.nvim/pull/1372>
|
||||
|
||||
See instructions in `:h neo-tree-events` for more details.
|
||||
|
||||
```lua
|
||||
event_handlers = {
|
||||
{
|
||||
event = "neo_tree_popup_input_ready",
|
||||
---@param args { bufnr: integer, winid: integer }
|
||||
handler = function(args)
|
||||
vim.cmd("stopinsert")
|
||||
vim.keymap.set("i", "<esc>", vim.cmd.stopinsert, { noremap = true, buffer = args.bufnr })
|
||||
end,
|
||||
}
|
||||
}
|
||||
```
|
||||
]]
|
||||
)
|
||||
|
||||
return migrations
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,746 @@
|
||||
local utils = require("neo-tree.utils")
|
||||
local defaults = require("neo-tree.defaults")
|
||||
local mapping_helper = require("neo-tree.setup.mapping-helper")
|
||||
local events = require("neo-tree.events")
|
||||
local log = require("neo-tree.log")
|
||||
local file_nesting = require("neo-tree.sources.common.file-nesting")
|
||||
local highlights = require("neo-tree.ui.highlights")
|
||||
local manager = require("neo-tree.sources.manager")
|
||||
local netrw = require("neo-tree.setup.netrw")
|
||||
local hijack_cursor = require("neo-tree.sources.common.hijack_cursor")
|
||||
|
||||
local M = {}
|
||||
|
||||
local normalize_mappings = function(config)
|
||||
if config == nil then
|
||||
return false
|
||||
end
|
||||
local mappings = utils.get_value(config, "window.mappings", nil)
|
||||
if mappings then
|
||||
local fixed = mapping_helper.normalize_map(mappings)
|
||||
config.window.mappings = fixed
|
||||
return true
|
||||
else
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
local events_setup = false
|
||||
local define_events = function()
|
||||
if events_setup then
|
||||
return
|
||||
end
|
||||
|
||||
events.define_event(events.FS_EVENT, {
|
||||
debounce_frequency = 100,
|
||||
debounce_strategy = utils.debounce_strategy.CALL_LAST_ONLY,
|
||||
})
|
||||
|
||||
local v = vim.version()
|
||||
local diag_autocmd = "DiagnosticChanged"
|
||||
if v.major < 1 and v.minor < 6 then
|
||||
diag_autocmd = "User LspDiagnosticsChanged"
|
||||
end
|
||||
events.define_autocmd_event(events.VIM_DIAGNOSTIC_CHANGED, { diag_autocmd }, 500, function(args)
|
||||
args.diagnostics_lookup = utils.get_diagnostic_counts()
|
||||
return args
|
||||
end)
|
||||
|
||||
|
||||
|
||||
local update_opened_buffers = function(args)
|
||||
args.opened_buffers = utils.get_opened_buffers()
|
||||
return args
|
||||
end
|
||||
|
||||
events.define_autocmd_event(events.VIM_AFTER_SESSION_LOAD, { "SessionLoadPost" }, 200)
|
||||
events.define_autocmd_event(events.VIM_BUFFER_ADDED, { "BufAdd" }, 200, update_opened_buffers)
|
||||
events.define_autocmd_event(events.VIM_BUFFER_CHANGED, { "BufWritePost" }, 200)
|
||||
events.define_autocmd_event(
|
||||
events.VIM_BUFFER_DELETED,
|
||||
{ "BufDelete" },
|
||||
200,
|
||||
update_opened_buffers
|
||||
)
|
||||
events.define_autocmd_event(events.VIM_BUFFER_ENTER, { "BufEnter", "BufWinEnter" }, 0)
|
||||
events.define_autocmd_event(
|
||||
events.VIM_BUFFER_MODIFIED_SET,
|
||||
{ "BufModifiedSet" },
|
||||
0,
|
||||
update_opened_buffers
|
||||
)
|
||||
events.define_autocmd_event(events.VIM_COLORSCHEME, { "ColorScheme" }, 0)
|
||||
events.define_autocmd_event(events.VIM_CURSOR_MOVED, { "CursorMoved" }, 100)
|
||||
events.define_autocmd_event(events.VIM_DIR_CHANGED, { "DirChanged" }, 200, nil, true)
|
||||
events.define_autocmd_event(events.VIM_INSERT_LEAVE, { "InsertLeave" }, 200)
|
||||
events.define_autocmd_event(events.VIM_LEAVE, { "VimLeavePre" })
|
||||
events.define_autocmd_event(events.VIM_RESIZED, { "VimResized" }, 100)
|
||||
events.define_autocmd_event(events.VIM_TAB_CLOSED, { "TabClosed" })
|
||||
events.define_autocmd_event(events.VIM_TERMINAL_ENTER, { "TermEnter" }, 0)
|
||||
events.define_autocmd_event(events.VIM_TEXT_CHANGED_NORMAL, { "TextChanged" }, 200)
|
||||
events.define_autocmd_event(events.VIM_WIN_CLOSED, { "WinClosed" })
|
||||
events.define_autocmd_event(events.VIM_WIN_ENTER, { "WinEnter" }, 0, nil, true)
|
||||
|
||||
events.define_autocmd_event(events.GIT_EVENT, { "User FugitiveChanged" }, 100)
|
||||
events.define_event(events.GIT_STATUS_CHANGED, { debounce_frequency = 0 })
|
||||
events_setup = true
|
||||
|
||||
events.subscribe({
|
||||
event = events.VIM_LEAVE,
|
||||
handler = function()
|
||||
events.clear_all_events()
|
||||
end,
|
||||
})
|
||||
|
||||
events.subscribe({
|
||||
event = events.VIM_RESIZED,
|
||||
handler = function()
|
||||
require("neo-tree.ui.renderer").update_floating_window_layouts()
|
||||
end,
|
||||
})
|
||||
end
|
||||
|
||||
local prior_window_options = {}
|
||||
|
||||
--- Store the current window options so we can restore them when we close the tree.
|
||||
--- @param winid number | nil The window id to store the options for, defaults to current window
|
||||
local store_local_window_settings = function(winid)
|
||||
winid = winid or vim.api.nvim_get_current_win()
|
||||
local neo_tree_settings_applied, _ =
|
||||
pcall(vim.api.nvim_win_get_var, winid, "neo_tree_settings_applied")
|
||||
if neo_tree_settings_applied then
|
||||
-- don't store our own window settings
|
||||
return
|
||||
end
|
||||
prior_window_options[tostring(winid)] = {
|
||||
cursorline = vim.wo.cursorline,
|
||||
cursorlineopt = vim.wo.cursorlineopt,
|
||||
foldcolumn = vim.wo.foldcolumn,
|
||||
wrap = vim.wo.wrap,
|
||||
list = vim.wo.list,
|
||||
spell = vim.wo.spell,
|
||||
number = vim.wo.number,
|
||||
relativenumber = vim.wo.relativenumber,
|
||||
winhighlight = vim.wo.winhighlight,
|
||||
}
|
||||
end
|
||||
|
||||
--- Restore the window options for the current window
|
||||
--- @param winid number | nil The window id to restore the options for, defaults to current window
|
||||
local restore_local_window_settings = function(winid)
|
||||
winid = winid or vim.api.nvim_get_current_win()
|
||||
-- return local window settings to their prior values
|
||||
local wo = prior_window_options[tostring(winid)]
|
||||
if wo then
|
||||
vim.wo.cursorline = wo.cursorline
|
||||
vim.wo.cursorlineopt = wo.cursorlineopt
|
||||
vim.wo.foldcolumn = wo.foldcolumn
|
||||
vim.wo.wrap = wo.wrap
|
||||
vim.wo.list = wo.list
|
||||
vim.wo.spell = wo.spell
|
||||
vim.wo.number = wo.number
|
||||
vim.wo.relativenumber = wo.relativenumber
|
||||
vim.wo.winhighlight = wo.winhighlight
|
||||
log.debug("Window settings restored")
|
||||
vim.api.nvim_win_set_var(0, "neo_tree_settings_applied", false)
|
||||
else
|
||||
log.debug("No window settings to restore")
|
||||
end
|
||||
end
|
||||
|
||||
local last_buffer_enter_filetype = nil
|
||||
M.buffer_enter_event = function()
|
||||
-- if it is a neo-tree window, just set local options
|
||||
if vim.bo.filetype == "neo-tree" then
|
||||
if last_buffer_enter_filetype == "neo-tree" then
|
||||
-- we've switched to another neo-tree window
|
||||
events.fire_event(events.NEO_TREE_BUFFER_LEAVE)
|
||||
else
|
||||
store_local_window_settings()
|
||||
end
|
||||
vim.cmd([[
|
||||
setlocal cursorline
|
||||
setlocal cursorlineopt=line
|
||||
setlocal nowrap
|
||||
setlocal nolist nospell nonumber norelativenumber
|
||||
]])
|
||||
|
||||
local winhighlight =
|
||||
"Normal:NeoTreeNormal,NormalNC:NeoTreeNormalNC,SignColumn:NeoTreeSignColumn,CursorLine:NeoTreeCursorLine,FloatBorder:NeoTreeFloatBorder,StatusLine:NeoTreeStatusLine,StatusLineNC:NeoTreeStatusLineNC,VertSplit:NeoTreeVertSplit,EndOfBuffer:NeoTreeEndOfBuffer"
|
||||
if vim.version().minor >= 7 then
|
||||
vim.cmd("setlocal winhighlight=" .. winhighlight .. ",WinSeparator:NeoTreeWinSeparator")
|
||||
else
|
||||
vim.cmd("setlocal winhighlight=" .. winhighlight)
|
||||
end
|
||||
|
||||
events.fire_event(events.NEO_TREE_BUFFER_ENTER)
|
||||
last_buffer_enter_filetype = vim.bo.filetype
|
||||
vim.api.nvim_win_set_var(0, "neo_tree_settings_applied", true)
|
||||
return
|
||||
end
|
||||
|
||||
if vim.bo.filetype == "neo-tree-popup" then
|
||||
vim.cmd([[
|
||||
setlocal winhighlight=Normal:NeoTreeFloatNormal,FloatBorder:NeoTreeFloatBorder
|
||||
setlocal nolist nospell nonumber norelativenumber
|
||||
]])
|
||||
events.fire_event(events.NEO_TREE_POPUP_BUFFER_ENTER)
|
||||
last_buffer_enter_filetype = vim.bo.filetype
|
||||
return
|
||||
end
|
||||
|
||||
if last_buffer_enter_filetype == "neo-tree" then
|
||||
events.fire_event(events.NEO_TREE_BUFFER_LEAVE)
|
||||
end
|
||||
if last_buffer_enter_filetype == "neo-tree-popup" then
|
||||
events.fire_event(events.NEO_TREE_POPUP_BUFFER_LEAVE)
|
||||
end
|
||||
last_buffer_enter_filetype = vim.bo.filetype
|
||||
|
||||
-- For all others, make sure another buffer is not hijacking our window
|
||||
-- ..but not if the position is "current"
|
||||
local prior_buf = vim.fn.bufnr("#")
|
||||
if prior_buf < 1 then
|
||||
return
|
||||
end
|
||||
local prior_type = vim.api.nvim_buf_get_option(prior_buf, "filetype")
|
||||
|
||||
-- there is nothing more we want to do with floating windows
|
||||
-- but when prior_type is neo-tree we might need to redirect buffer somewhere else.
|
||||
if utils.is_floating() and prior_type ~= "neo-tree" then
|
||||
return
|
||||
end
|
||||
|
||||
-- if vim is trying to open a dir, then we hijack it
|
||||
if netrw.hijack() then
|
||||
return
|
||||
end
|
||||
|
||||
if prior_type == "neo-tree" then
|
||||
local success, position = pcall(vim.api.nvim_buf_get_var, prior_buf, "neo_tree_position")
|
||||
if not success then
|
||||
-- just bail out now, the rest of these lookups will probably fail too.
|
||||
return
|
||||
end
|
||||
|
||||
if position == "current" then
|
||||
-- nothing to do here, files are supposed to open in same window
|
||||
return
|
||||
end
|
||||
|
||||
local current_tabid = vim.api.nvim_get_current_tabpage()
|
||||
local neo_tree_tabid = vim.api.nvim_buf_get_var(prior_buf, "neo_tree_tabid")
|
||||
if neo_tree_tabid ~= current_tabid then
|
||||
-- This a new tab, so the alternate being neo-tree doesn't matter.
|
||||
return
|
||||
end
|
||||
local neo_tree_winid = vim.api.nvim_buf_get_var(prior_buf, "neo_tree_winid")
|
||||
local current_winid = vim.api.nvim_get_current_win()
|
||||
if neo_tree_winid ~= current_winid then
|
||||
-- This is not the neo-tree window, so the alternate being neo-tree doesn't matter.
|
||||
return
|
||||
end
|
||||
|
||||
local bufname = vim.api.nvim_buf_get_name(0)
|
||||
log.debug("redirecting buffer " .. bufname .. " to new split")
|
||||
vim.cmd("b#")
|
||||
-- Using schedule at this point fixes problem with syntax
|
||||
-- highlighting in the buffer. I also prevents errors with diagnostics
|
||||
-- trying to work with the buffer as it's being closed.
|
||||
vim.schedule(function()
|
||||
-- try to delete the buffer, only because if it was new it would take
|
||||
-- on options from the neo-tree window that are undesirable.
|
||||
pcall(vim.cmd, "bdelete " .. bufname)
|
||||
local fake_state = {
|
||||
window = {
|
||||
position = position,
|
||||
},
|
||||
}
|
||||
utils.open_file(fake_state, bufname)
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
M.win_enter_event = function()
|
||||
local win_id = vim.api.nvim_get_current_win()
|
||||
if utils.is_floating(win_id) then
|
||||
return
|
||||
end
|
||||
|
||||
-- if the new win is not a floating window, make sure all neo-tree floats are closed
|
||||
manager.close_all("float")
|
||||
|
||||
if M.config.close_if_last_window then
|
||||
local tabid = vim.api.nvim_get_current_tabpage()
|
||||
local wins = utils.get_value(M, "config.prior_windows", {})[tabid]
|
||||
local prior_exists = utils.truthy(wins)
|
||||
local non_floating_wins = vim.tbl_filter(function(win)
|
||||
return not utils.is_floating(win)
|
||||
end, vim.api.nvim_tabpage_list_wins(tabid))
|
||||
local win_count = #non_floating_wins
|
||||
log.trace("checking if last window")
|
||||
log.trace("prior window exists = ", prior_exists)
|
||||
log.trace("win_count: ", win_count)
|
||||
if prior_exists and win_count == 1 and vim.o.filetype == "neo-tree" then
|
||||
local position = vim.api.nvim_buf_get_var(0, "neo_tree_position")
|
||||
local source = vim.api.nvim_buf_get_var(0, "neo_tree_source")
|
||||
if position ~= "current" then
|
||||
-- close_if_last_window just doesn't make sense for a split style
|
||||
log.trace("last window, closing")
|
||||
local state = require("neo-tree.sources.manager").get_state(source)
|
||||
if state == nil then
|
||||
return
|
||||
end
|
||||
local mod = utils.get_opened_buffers()
|
||||
log.debug("close_if_last_window, modified files found: ", vim.inspect(mod))
|
||||
for filename, buf_info in pairs(mod) do
|
||||
if buf_info.modified then
|
||||
local buf_name, message
|
||||
if vim.startswith(filename, "[No Name]#") then
|
||||
buf_name = string.sub(filename, 11)
|
||||
message = "Cannot close because an unnamed buffer is modified. Please save or discard this file."
|
||||
else
|
||||
buf_name = filename
|
||||
message = "Cannot close because one of the files is modified. Please save or discard changes."
|
||||
end
|
||||
log.trace("close_if_last_window, showing unnamed modified buffer: ", filename)
|
||||
vim.schedule(function()
|
||||
log.warn(message)
|
||||
vim.cmd("rightbelow vertical split")
|
||||
vim.api.nvim_win_set_width(win_id, state.window.width or 40)
|
||||
vim.cmd("b " .. buf_name)
|
||||
end)
|
||||
return
|
||||
end
|
||||
end
|
||||
vim.cmd("q!")
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if vim.o.filetype == "neo-tree" then
|
||||
local _, position = pcall(vim.api.nvim_buf_get_var, 0, "neo_tree_position")
|
||||
if position == "current" then
|
||||
-- make sure the buffer wasn't moved to a new window
|
||||
local neo_tree_winid = vim.api.nvim_buf_get_var(0, "neo_tree_winid")
|
||||
local current_winid = vim.api.nvim_get_current_win()
|
||||
local current_bufnr = vim.api.nvim_get_current_buf()
|
||||
if neo_tree_winid ~= current_winid then
|
||||
-- At this point we know that either the neo-tree window was split,
|
||||
-- or the neo-tree buffer is being shown in another window for some other reason.
|
||||
-- Sometime the split is just the first step in the process of opening somethig else,
|
||||
-- so instead of fixing this right away, we add a short delay and check back again to see
|
||||
-- if the buffer is still in this window.
|
||||
local old_state = manager.get_state("filesystem", nil, neo_tree_winid)
|
||||
vim.schedule(function()
|
||||
local bufnr = vim.api.nvim_get_current_buf()
|
||||
if bufnr ~= current_bufnr then
|
||||
-- The neo-tree buffer was replaced with something else, so we don't need to do anything.
|
||||
log.trace("neo-tree buffer replaced with something else - no further action required")
|
||||
return
|
||||
end
|
||||
-- create a new tree for this window
|
||||
local state = manager.get_state("filesystem", nil, current_winid)
|
||||
state.path = old_state.path
|
||||
state.current_position = "current"
|
||||
local renderer = require("neo-tree.ui.renderer")
|
||||
state.force_open_folders = renderer.get_expanded_nodes(old_state.tree)
|
||||
require("neo-tree.sources.filesystem")._navigate_internal(state, nil, nil, nil, false)
|
||||
end)
|
||||
return
|
||||
end
|
||||
end
|
||||
-- it's a neo-tree window, ignore
|
||||
return
|
||||
end
|
||||
|
||||
M.config.prior_windows = M.config.prior_windows or {}
|
||||
|
||||
local tabid = vim.api.nvim_get_current_tabpage()
|
||||
local tab_windows = M.config.prior_windows[tabid]
|
||||
if tab_windows == nil then
|
||||
tab_windows = {}
|
||||
M.config.prior_windows[tabid] = tab_windows
|
||||
end
|
||||
table.insert(tab_windows, win_id)
|
||||
|
||||
-- prune the history when it gets too big
|
||||
if #tab_windows > 100 then
|
||||
local new_array = {}
|
||||
local win_count = #tab_windows
|
||||
for i = 80, win_count do
|
||||
table.insert(new_array, tab_windows[i])
|
||||
end
|
||||
M.config.prior_windows[tabid] = new_array
|
||||
end
|
||||
end
|
||||
|
||||
M.set_log_level = function(level)
|
||||
log.set_level(level)
|
||||
end
|
||||
|
||||
local function merge_global_components_config(components, config)
|
||||
local indent_exists = false
|
||||
local merged_components = {}
|
||||
local do_merge
|
||||
|
||||
do_merge = function(component)
|
||||
local name = component[1]
|
||||
if type(name) == "string" then
|
||||
if name == "indent" then
|
||||
indent_exists = true
|
||||
end
|
||||
local merged = { name }
|
||||
local global_config = config.default_component_configs[name]
|
||||
if global_config then
|
||||
for k, v in pairs(global_config) do
|
||||
merged[k] = v
|
||||
end
|
||||
end
|
||||
for k, v in pairs(component) do
|
||||
merged[k] = v
|
||||
end
|
||||
if name == "container" then
|
||||
for i, child in ipairs(component.content) do
|
||||
merged.content[i] = do_merge(child)
|
||||
end
|
||||
end
|
||||
return merged
|
||||
else
|
||||
log.error("component name is the wrong type", component)
|
||||
end
|
||||
end
|
||||
|
||||
for _, component in ipairs(components) do
|
||||
local merged = do_merge(component)
|
||||
table.insert(merged_components, merged)
|
||||
end
|
||||
|
||||
-- If the indent component is not specified, then add it.
|
||||
-- We do this because it used to be implicitly added, so we don't want to
|
||||
-- break any existing configs.
|
||||
if not indent_exists then
|
||||
local indent = { "indent" }
|
||||
for k, v in pairs(config.default_component_configs.indent or {}) do
|
||||
indent[k] = v
|
||||
end
|
||||
table.insert(merged_components, 1, indent)
|
||||
end
|
||||
return merged_components
|
||||
end
|
||||
|
||||
local merge_renderers = function(default_config, source_default_config, user_config)
|
||||
-- This can't be a deep copy/merge. If a renderer is specified in the target it completely
|
||||
-- replaces the base renderer.
|
||||
|
||||
if source_default_config == nil then
|
||||
-- first override the default config global renderer with the user's global renderers
|
||||
for name, renderer in pairs(user_config.renderers or {}) do
|
||||
log.debug("overriding global renderer for " .. name)
|
||||
default_config.renderers[name] = renderer
|
||||
end
|
||||
else
|
||||
-- then override the global renderers with the source specific renderers
|
||||
source_default_config.renderers = source_default_config.renderers or {}
|
||||
for name, renderer in pairs(default_config.renderers or {}) do
|
||||
if source_default_config.renderers[name] == nil then
|
||||
log.debug("overriding source renderer for " .. name)
|
||||
local r = {}
|
||||
-- Only copy components that exist in the target source.
|
||||
-- This alllows us to specify global renderers that include components from all sources,
|
||||
-- even if some of those components are not universal
|
||||
for _, value in ipairs(renderer) do
|
||||
if value[1] and source_default_config.components[value[1]] ~= nil then
|
||||
table.insert(r, value)
|
||||
end
|
||||
end
|
||||
source_default_config.renderers[name] = r
|
||||
end
|
||||
end
|
||||
|
||||
-- if user sets renderers, completely wipe the default ones
|
||||
local source_name = source_default_config.name
|
||||
for name, _ in pairs(source_default_config.renderers) do
|
||||
local user = utils.get_value(user_config, source_name .. ".renderers." .. name)
|
||||
if user then
|
||||
source_default_config.renderers[name] = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
M.merge_config = function(user_config, is_auto_config)
|
||||
local default_config = vim.deepcopy(defaults)
|
||||
user_config = vim.deepcopy(user_config or {})
|
||||
|
||||
local migrations = require("neo-tree.setup.deprecations").migrate(user_config)
|
||||
if #migrations > 0 then
|
||||
-- defer to make sure it is the last message printed
|
||||
vim.defer_fn(function()
|
||||
vim.cmd(
|
||||
"echohl WarningMsg | echo 'Some options have changed, please run `:Neotree migrations` to see the changes' | echohl NONE"
|
||||
)
|
||||
end, 50)
|
||||
end
|
||||
|
||||
if user_config.log_level ~= nil then
|
||||
M.set_log_level(user_config.log_level)
|
||||
end
|
||||
log.use_file(user_config.log_to_file, true)
|
||||
log.debug("setup")
|
||||
|
||||
events.clear_all_events()
|
||||
define_events()
|
||||
|
||||
-- Prevent accidentally opening another file in the neo-tree window.
|
||||
events.subscribe({
|
||||
event = events.VIM_BUFFER_ENTER,
|
||||
handler = M.buffer_enter_event,
|
||||
})
|
||||
|
||||
-- Setup autocmd for neo-tree BufLeave, to restore window settings.
|
||||
-- This is set to happen just before leaving the window.
|
||||
-- The patterns used should ensure it only runs in neo-tree windows where position = "current"
|
||||
local augroup = vim.api.nvim_create_augroup("NeoTree_BufLeave", { clear = true })
|
||||
local bufleave = function(data)
|
||||
-- Vim patterns in autocmds are not quite precise enough
|
||||
-- so we are doing a second stage filter in lua
|
||||
local pattern = "neo%-tree [^ ]+ %[1%d%d%d%]"
|
||||
if string.match(data.file, pattern) then
|
||||
restore_local_window_settings()
|
||||
end
|
||||
end
|
||||
vim.api.nvim_create_autocmd({ "BufWinLeave" }, {
|
||||
group = augroup,
|
||||
pattern = "neo-tree *",
|
||||
callback = bufleave,
|
||||
})
|
||||
|
||||
if user_config.event_handlers ~= nil then
|
||||
for _, handler in ipairs(user_config.event_handlers) do
|
||||
events.subscribe(handler)
|
||||
end
|
||||
end
|
||||
|
||||
highlights.setup()
|
||||
|
||||
-- used to either limit the sources that or loaded, or add extra external sources
|
||||
local all_sources = {}
|
||||
local all_source_names = {}
|
||||
for _, source in ipairs(user_config.sources or default_config.sources) do
|
||||
local parts = utils.split(source, ".")
|
||||
local name = parts[#parts]
|
||||
local is_internal_ns, is_external_ns = false, false
|
||||
local module
|
||||
|
||||
if #parts == 1 then
|
||||
-- might be a module name in the internal namespace
|
||||
is_internal_ns, module = pcall(require, "neo-tree.sources." .. source)
|
||||
end
|
||||
if is_internal_ns then
|
||||
name = module.name or name
|
||||
all_sources[name] = "neo-tree.sources." .. name
|
||||
else
|
||||
-- fully qualified module name
|
||||
-- or just a root level module name
|
||||
is_external_ns, module = pcall(require, source)
|
||||
if is_external_ns then
|
||||
name = module.name or name
|
||||
all_sources[name] = source
|
||||
else
|
||||
log.error("Source module not found", source)
|
||||
name = nil
|
||||
end
|
||||
end
|
||||
if name then
|
||||
default_config[name] = module.default_config or default_config[name]
|
||||
table.insert(all_source_names, name)
|
||||
end
|
||||
end
|
||||
log.debug("Sources to load: ", vim.inspect(all_sources))
|
||||
require("neo-tree.command.parser").setup(all_source_names)
|
||||
|
||||
-- setup the default values for all sources
|
||||
normalize_mappings(default_config)
|
||||
normalize_mappings(user_config)
|
||||
merge_renderers(default_config, nil, user_config)
|
||||
|
||||
for source_name, mod_root in pairs(all_sources) do
|
||||
local module = require(mod_root)
|
||||
default_config[source_name] = default_config[source_name]
|
||||
or {
|
||||
renderers = {},
|
||||
components = {},
|
||||
}
|
||||
local source_default_config = default_config[source_name]
|
||||
source_default_config.components = module.components or require(mod_root .. ".components")
|
||||
source_default_config.commands = module.commands or require(mod_root .. ".commands")
|
||||
source_default_config.name = source_name
|
||||
source_default_config.display_name = module.display_name or source_default_config.name
|
||||
|
||||
if user_config.use_default_mappings == false then
|
||||
default_config.window.mappings = {}
|
||||
source_default_config.window.mappings = {}
|
||||
end
|
||||
-- Make sure all the mappings are normalized so they will merge properly.
|
||||
normalize_mappings(source_default_config)
|
||||
normalize_mappings(user_config[source_name])
|
||||
-- merge the global config with the source specific config
|
||||
source_default_config.window = vim.tbl_deep_extend(
|
||||
"force",
|
||||
default_config.window or {},
|
||||
source_default_config.window or {},
|
||||
user_config.window or {}
|
||||
)
|
||||
|
||||
merge_renderers(default_config, source_default_config, user_config)
|
||||
|
||||
--validate the window.position
|
||||
local pos_key = source_name .. ".window.position"
|
||||
local position = utils.get_value(user_config, pos_key, "left", true)
|
||||
local valid_positions = {
|
||||
left = true,
|
||||
right = true,
|
||||
top = true,
|
||||
bottom = true,
|
||||
float = true,
|
||||
current = true,
|
||||
}
|
||||
if not valid_positions[position] then
|
||||
log.error("Invalid value for ", pos_key, ": ", position)
|
||||
user_config[source_name].window.position = "left"
|
||||
end
|
||||
end
|
||||
--print(vim.inspect(default_config.filesystem))
|
||||
|
||||
-- Moving user_config.sources to user_config.orig_sources
|
||||
user_config.orig_sources = user_config.sources and user_config.sources or {}
|
||||
|
||||
-- apply the users config
|
||||
M.config = vim.tbl_deep_extend("force", default_config, user_config)
|
||||
|
||||
-- RE: 873, fixes issue with invalid source checking by overriding
|
||||
-- source table with name table
|
||||
-- Setting new "sources" to be the parsed names of the sources
|
||||
M.config.sources = all_source_names
|
||||
|
||||
if ( M.config.source_selector.winbar or M.config.source_selector.statusline )
|
||||
and M.config.source_selector.sources
|
||||
and not user_config.default_source then
|
||||
-- Set the default source to the head of these
|
||||
-- This resolves some weirdness with the source selector having
|
||||
-- a different "head" item than our current default.
|
||||
-- Removing this line makes Neo-tree show the "filesystem"
|
||||
-- source instead of whatever the first item in the config is.
|
||||
-- Probably don't remove this unless you have a better fix for that
|
||||
M.config.default_source = M.config.source_selector.sources[1].source
|
||||
end
|
||||
-- Check if the default source is not included in config.sources
|
||||
-- log a warning and then "pick" the first in the sources list
|
||||
local match = false
|
||||
for _, source in ipairs(M.config.sources) do
|
||||
if source == M.config.default_source then
|
||||
match = true
|
||||
break
|
||||
end
|
||||
end
|
||||
if not match and M.config.default_source ~= "last" then
|
||||
M.config.default_source = M.config.sources[1]
|
||||
log.warn(string.format("Invalid default source found in configuration. Using first available source: %s", M.config.default_source))
|
||||
end
|
||||
|
||||
if not M.config.enable_git_status then
|
||||
M.config.git_status_async = false
|
||||
end
|
||||
|
||||
-- Validate that the source_selector.sources are all available and if any
|
||||
-- aren't, remove them
|
||||
local source_selector_sources = {}
|
||||
for _, ss_source in ipairs(M.config.source_selector.sources or {}) do
|
||||
local source_match = false
|
||||
for _, source in ipairs(M.config.sources) do
|
||||
if ss_source.source == source then
|
||||
source_match = true
|
||||
break
|
||||
end
|
||||
end
|
||||
if source_match then
|
||||
table.insert(source_selector_sources, ss_source)
|
||||
else
|
||||
log.debug(string.format("Unable to locate Neo-tree extension %s", ss_source.source))
|
||||
end
|
||||
end
|
||||
M.config.source_selector.sources = source_selector_sources
|
||||
|
||||
file_nesting.setup(M.config.nesting_rules)
|
||||
|
||||
for source_name, mod_root in pairs(all_sources) do
|
||||
for name, rndr in pairs(M.config[source_name].renderers) do
|
||||
M.config[source_name].renderers[name] = merge_global_components_config(rndr, M.config)
|
||||
end
|
||||
local module = require(mod_root)
|
||||
if M.config.commands then
|
||||
M.config[source_name].commands =
|
||||
vim.tbl_extend("keep", M.config[source_name].commands or {}, M.config.commands)
|
||||
end
|
||||
manager.setup(source_name, M.config[source_name], M.config, module)
|
||||
manager.redraw(source_name)
|
||||
end
|
||||
|
||||
if M.config.auto_clean_after_session_restore then
|
||||
require("neo-tree.ui.renderer").clean_invalid_neotree_buffers(false)
|
||||
events.subscribe({
|
||||
event = events.VIM_AFTER_SESSION_LOAD,
|
||||
handler = function()
|
||||
require("neo-tree.ui.renderer").clean_invalid_neotree_buffers(true)
|
||||
end,
|
||||
})
|
||||
end
|
||||
|
||||
events.subscribe({
|
||||
event = events.VIM_COLORSCHEME,
|
||||
handler = highlights.setup,
|
||||
id = "neo-tree-highlight",
|
||||
})
|
||||
|
||||
events.subscribe({
|
||||
event = events.VIM_WIN_ENTER,
|
||||
handler = M.win_enter_event,
|
||||
id = "neo-tree-win-enter",
|
||||
})
|
||||
|
||||
--Dispose ourselves if the tab closes
|
||||
events.subscribe({
|
||||
event = events.VIM_TAB_CLOSED,
|
||||
handler = function(args)
|
||||
local tabnr = tonumber(args.afile)
|
||||
log.debug("VIM_TAB_CLOSED: disposing state for tabnr", tabnr)
|
||||
-- Internally we use tabids to track state but <afile> is tabnr of a tab that has already been
|
||||
-- closed so there is no way to get its tabid. Instead dispose all tabs that are no longer valid.
|
||||
-- Must be scheduled because nvim_tabpage_is_valid does not work inside TabClosed event callback.
|
||||
vim.schedule_wrap(manager.dispose_invalid_tabs)()
|
||||
end,
|
||||
})
|
||||
|
||||
--Dispose ourselves if the tab closes
|
||||
events.subscribe({
|
||||
event = events.VIM_WIN_CLOSED,
|
||||
handler = function(args)
|
||||
local winid = tonumber(args.afile)
|
||||
log.debug("VIM_WIN_CLOSED: disposing state for window", winid)
|
||||
manager.dispose_window(winid)
|
||||
end,
|
||||
})
|
||||
|
||||
local rt = utils.get_value(M.config, "resize_timer_interval", 50, true)
|
||||
require("neo-tree.ui.renderer").resize_timer_interval = rt
|
||||
|
||||
if M.config.enable_cursor_hijack then
|
||||
hijack_cursor.setup()
|
||||
end
|
||||
|
||||
return M.config
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,62 @@
|
||||
local utils = require("neo-tree.utils")
|
||||
|
||||
local M = {}
|
||||
|
||||
M.normalize_map_key = function(key)
|
||||
if key == nil then
|
||||
return nil
|
||||
end
|
||||
if key:match("^<[^>]+>$") then
|
||||
local parts = utils.split(key, "-")
|
||||
if #parts == 2 then
|
||||
local mod = parts[1]:lower()
|
||||
if mod == "<a" then
|
||||
mod = "<m"
|
||||
end
|
||||
local alpha = parts[2]
|
||||
if #alpha > 2 then
|
||||
alpha = alpha:lower()
|
||||
end
|
||||
key = string.format("%s-%s", mod, alpha)
|
||||
return key
|
||||
else
|
||||
key = key:lower()
|
||||
if key == "<backspace>" then
|
||||
return "<bs>"
|
||||
elseif key == "<enter>" then
|
||||
return "<cr>"
|
||||
elseif key == "<return>" then
|
||||
return "<cr>"
|
||||
end
|
||||
end
|
||||
end
|
||||
return key
|
||||
end
|
||||
|
||||
M.normalize_map = function(map)
|
||||
local new_map = {}
|
||||
for key, value in pairs(map) do
|
||||
local normalized_key = M.normalize_map_key(key)
|
||||
if normalized_key ~= nil then
|
||||
new_map[normalized_key] = value
|
||||
end
|
||||
end
|
||||
return new_map
|
||||
end
|
||||
|
||||
local tests = {
|
||||
{ "<BS>", "<bs>" },
|
||||
{ "<Backspace>", "<bs>" },
|
||||
{ "<Enter>", "<cr>" },
|
||||
{ "<C-W>", "<c-W>" },
|
||||
{ "<A-q>", "<m-q>" },
|
||||
{ "<C-Left>", "<c-left>" },
|
||||
{ "<C-Right>", "<c-right>" },
|
||||
{ "<C-Up>", "<c-up>" },
|
||||
}
|
||||
for _, test in ipairs(tests) do
|
||||
local key = M.normalize_map_key(test[1])
|
||||
assert(key == test[2], string.format("%s != %s", key, test[2]))
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,103 @@
|
||||
local utils = require("neo-tree.utils")
|
||||
local log = require("neo-tree.log")
|
||||
local manager = require("neo-tree.sources.manager")
|
||||
local command = require("neo-tree.command")
|
||||
local M = {}
|
||||
|
||||
local get_position = function(source_name)
|
||||
local nt = require("neo-tree")
|
||||
local pos = utils.get_value(nt.config, source_name .. ".window.position", "left", true)
|
||||
return pos
|
||||
end
|
||||
|
||||
M.get_hijack_netrw_behavior = function()
|
||||
local nt = require("neo-tree")
|
||||
local option = "filesystem.hijack_netrw_behavior"
|
||||
local hijack_behavior = utils.get_value(nt.config, option, "open_default", true)
|
||||
if hijack_behavior == "disabled" then
|
||||
return hijack_behavior
|
||||
elseif hijack_behavior == "open_default" then
|
||||
return hijack_behavior
|
||||
elseif hijack_behavior == "open_current" then
|
||||
return hijack_behavior
|
||||
else
|
||||
log.error("Invalid value for " .. option .. ": " .. hijack_behavior)
|
||||
return "disabled"
|
||||
end
|
||||
end
|
||||
|
||||
M.hijack = function()
|
||||
local hijack_behavior = M.get_hijack_netrw_behavior()
|
||||
if hijack_behavior == "disabled" then
|
||||
return false
|
||||
end
|
||||
|
||||
-- ensure this is a directory
|
||||
local bufname = vim.api.nvim_buf_get_name(0)
|
||||
local stats = vim.loop.fs_stat(bufname)
|
||||
if not stats then
|
||||
return false
|
||||
end
|
||||
if stats.type ~= "directory" then
|
||||
return false
|
||||
end
|
||||
|
||||
-- record where we are now
|
||||
local pos = get_position("filesystem")
|
||||
local should_open_current = hijack_behavior == "open_current" or pos == "current"
|
||||
local winid = vim.api.nvim_get_current_win()
|
||||
local dir_bufnr = vim.api.nvim_get_current_buf()
|
||||
|
||||
-- Now actually open the tree, with a very quick debounce because this may be
|
||||
-- called multiple times in quick succession.
|
||||
utils.debounce("hijack_netrw_" .. winid, function()
|
||||
-- We will want to replace the "directory" buffer with either the "alternate"
|
||||
-- buffer or a new blank one.
|
||||
local replace_with_bufnr = vim.fn.bufnr("#")
|
||||
local is_currently_neo_tree = false
|
||||
if replace_with_bufnr > 0 then
|
||||
if vim.api.nvim_buf_get_option(replace_with_bufnr, "filetype") == "neo-tree" then
|
||||
-- don't hijack the current window if it's already a Neo-tree sidebar
|
||||
local _, position = pcall(vim.api.nvim_buf_get_var, replace_with_bufnr, "neo_tree_position")
|
||||
if position ~= "current" then
|
||||
is_currently_neo_tree = true
|
||||
else
|
||||
replace_with_bufnr = -1
|
||||
end
|
||||
end
|
||||
end
|
||||
if not should_open_current then
|
||||
if replace_with_bufnr == dir_bufnr or replace_with_bufnr < 1 then
|
||||
replace_with_bufnr = vim.api.nvim_create_buf(true, false)
|
||||
log.trace("Created new buffer for netrw hijack", replace_with_bufnr)
|
||||
end
|
||||
end
|
||||
if replace_with_bufnr > 0 then
|
||||
log.trace("Replacing buffer in netrw hijack", replace_with_bufnr)
|
||||
pcall(vim.api.nvim_win_set_buf, winid, replace_with_bufnr)
|
||||
end
|
||||
local remove_dir_buf = vim.schedule_wrap(function()
|
||||
log.trace("Deleting buffer in netrw hijack", dir_bufnr)
|
||||
pcall(vim.api.nvim_buf_delete, dir_bufnr, { force = true })
|
||||
end)
|
||||
|
||||
local state
|
||||
if should_open_current and not is_currently_neo_tree then
|
||||
log.debug("hijack_netrw: opening current")
|
||||
state = manager.get_state("filesystem", nil, winid)
|
||||
state.current_position = "current"
|
||||
elseif is_currently_neo_tree then
|
||||
log.debug("hijack_netrw: opening in existing Neo-tree")
|
||||
state = manager.get_state("filesystem")
|
||||
else
|
||||
log.debug("hijack_netrw: opening default")
|
||||
manager.close_all_except("filesystem")
|
||||
state = manager.get_state("filesystem")
|
||||
end
|
||||
require("neo-tree.sources.filesystem")._navigate_internal(state, bufname, nil, remove_dir_buf)
|
||||
end, 10, utils.debounce_strategy.CALL_LAST_ONLY)
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,98 @@
|
||||
--This file should contain all commands meant to be used by mappings.
|
||||
|
||||
local vim = vim
|
||||
local cc = require("neo-tree.sources.common.commands")
|
||||
local buffers = require("neo-tree.sources.buffers")
|
||||
local utils = require("neo-tree.utils")
|
||||
local manager = require("neo-tree.sources.manager")
|
||||
|
||||
local M = {}
|
||||
|
||||
local refresh = utils.wrap(manager.refresh, "buffers")
|
||||
local redraw = utils.wrap(manager.redraw, "buffers")
|
||||
|
||||
M.add = function(state)
|
||||
cc.add(state, refresh)
|
||||
end
|
||||
|
||||
M.add_directory = function(state)
|
||||
cc.add_directory(state, refresh)
|
||||
end
|
||||
|
||||
M.buffer_delete = function(state)
|
||||
local node = state.tree:get_node()
|
||||
if node then
|
||||
if node.type == "message" then
|
||||
return
|
||||
end
|
||||
vim.api.nvim_buf_delete(node.extra.bufnr, { force = false, unload = false })
|
||||
refresh()
|
||||
end
|
||||
end
|
||||
|
||||
---Marks node as copied, so that it can be pasted somewhere else.
|
||||
M.copy_to_clipboard = function(state)
|
||||
cc.copy_to_clipboard(state, redraw)
|
||||
end
|
||||
|
||||
M.copy_to_clipboard_visual = function(state, selected_nodes)
|
||||
cc.copy_to_clipboard_visual(state, selected_nodes, redraw)
|
||||
end
|
||||
|
||||
---Marks node as cut, so that it can be pasted (moved) somewhere else.
|
||||
M.cut_to_clipboard = function(state)
|
||||
cc.cut_to_clipboard(state, redraw)
|
||||
end
|
||||
|
||||
M.cut_to_clipboard_visual = function(state, selected_nodes)
|
||||
cc.cut_to_clipboard_visual(state, selected_nodes, redraw)
|
||||
end
|
||||
|
||||
M.copy = function(state)
|
||||
cc.copy(state, redraw)
|
||||
end
|
||||
|
||||
M.move = function(state)
|
||||
cc.move(state, redraw)
|
||||
end
|
||||
|
||||
M.show_debug_info = cc.show_debug_info
|
||||
|
||||
---Pastes all items from the clipboard to the current directory.
|
||||
M.paste_from_clipboard = function(state)
|
||||
cc.paste_from_clipboard(state, refresh)
|
||||
end
|
||||
|
||||
M.delete = function(state)
|
||||
cc.delete(state, refresh)
|
||||
end
|
||||
|
||||
---Navigate up one level.
|
||||
M.navigate_up = function(state)
|
||||
local parent_path, _ = utils.split_path(state.path)
|
||||
buffers.navigate(state, parent_path)
|
||||
end
|
||||
|
||||
M.refresh = refresh
|
||||
|
||||
M.rename = function(state)
|
||||
cc.rename(state, refresh)
|
||||
end
|
||||
|
||||
M.set_root = function(state)
|
||||
local node = state.tree:get_node()
|
||||
while node and node.type ~= "directory" do
|
||||
local parent_id = node:get_parent_id()
|
||||
node = parent_id and state.tree:get_node(parent_id) or nil
|
||||
end
|
||||
|
||||
if not node then
|
||||
return
|
||||
end
|
||||
|
||||
buffers.navigate(state, node:get_id())
|
||||
end
|
||||
|
||||
cc._add_common_commands(M)
|
||||
|
||||
return M
|
||||
@ -0,0 +1,48 @@
|
||||
-- This file contains the built-in components. Each componment is a function
|
||||
-- that takes the following arguments:
|
||||
-- config: A table containing the configuration provided by the user
|
||||
-- when declaring this component in their renderer config.
|
||||
-- node: A NuiNode object for the currently focused node.
|
||||
-- state: The current state of the source providing the items.
|
||||
--
|
||||
-- The function should return either a table, or a list of tables, each of which
|
||||
-- contains the following keys:
|
||||
-- text: The text to display for this item.
|
||||
-- highlight: The highlight group to apply to this text.
|
||||
|
||||
local highlights = require("neo-tree.ui.highlights")
|
||||
local common = require("neo-tree.sources.common.components")
|
||||
local utils = require("neo-tree.utils")
|
||||
|
||||
local M = {}
|
||||
|
||||
M.name = function(config, node, state)
|
||||
local highlight = config.highlight or highlights.FILE_NAME_OPENED
|
||||
local name = node.name
|
||||
if node.type == "directory" then
|
||||
if node:get_depth() == 1 then
|
||||
highlight = highlights.ROOT_NAME
|
||||
name = "OPEN BUFFERS in " .. name
|
||||
else
|
||||
highlight = highlights.DIRECTORY_NAME
|
||||
end
|
||||
elseif node.type == "terminal" then
|
||||
if node:get_depth() == 1 then
|
||||
highlight = highlights.ROOT_NAME
|
||||
name = "TERMINALS"
|
||||
else
|
||||
highlight = highlights.FILE_NAME
|
||||
end
|
||||
elseif config.use_git_status_colors then
|
||||
local git_status = state.components.git_status({}, node, state)
|
||||
if git_status and git_status.highlight then
|
||||
highlight = git_status.highlight
|
||||
end
|
||||
end
|
||||
return {
|
||||
text = name,
|
||||
highlight = highlight,
|
||||
}
|
||||
end
|
||||
|
||||
return vim.tbl_deep_extend("force", common, M)
|
||||
@ -0,0 +1,201 @@
|
||||
--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 renderer = require("neo-tree.ui.renderer")
|
||||
local items = require("neo-tree.sources.buffers.lib.items")
|
||||
local events = require("neo-tree.events")
|
||||
local manager = require("neo-tree.sources.manager")
|
||||
local git = require("neo-tree.git")
|
||||
|
||||
local M = {
|
||||
name = "buffers",
|
||||
display_name = " Buffers "
|
||||
}
|
||||
|
||||
local wrap = function(func)
|
||||
return utils.wrap(func, M.name)
|
||||
end
|
||||
|
||||
local get_state = function()
|
||||
return manager.get_state(M.name)
|
||||
end
|
||||
|
||||
local follow_internal = function()
|
||||
if vim.bo.filetype == "neo-tree" or vim.bo.filetype == "neo-tree-popup" then
|
||||
return
|
||||
end
|
||||
local bufnr = vim.api.nvim_get_current_buf()
|
||||
local path_to_reveal = manager.get_path_to_reveal(true) or tostring(bufnr)
|
||||
|
||||
local state = get_state()
|
||||
if state.current_position == "float" then
|
||||
return false
|
||||
end
|
||||
if not state.path then
|
||||
return false
|
||||
end
|
||||
local window_exists = renderer.window_exists(state)
|
||||
if window_exists then
|
||||
local node = state.tree and state.tree:get_node()
|
||||
if node then
|
||||
if node:get_id() == path_to_reveal then
|
||||
-- already focused
|
||||
return false
|
||||
end
|
||||
end
|
||||
renderer.focus_node(state, path_to_reveal, true)
|
||||
end
|
||||
end
|
||||
|
||||
M.follow = function()
|
||||
if vim.fn.bufname(0) == "COMMIT_EDITMSG" then
|
||||
return false
|
||||
end
|
||||
utils.debounce("neo-tree-buffer-follow", function()
|
||||
return follow_internal()
|
||||
end, 100, utils.debounce_strategy.CALL_LAST_ONLY)
|
||||
end
|
||||
|
||||
local buffers_changed_internal = function()
|
||||
for _, tabid in ipairs(vim.api.nvim_list_tabpages()) do
|
||||
local state = manager.get_state(M.name, tabid)
|
||||
if state.path and renderer.window_exists(state) then
|
||||
items.get_opened_buffers(state)
|
||||
if state.follow_current_file.enabled then
|
||||
follow_internal()
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---Calld by autocmd when any buffer is open, closed, renamed, etc.
|
||||
M.buffers_changed = function()
|
||||
utils.debounce(
|
||||
"buffers_changed",
|
||||
buffers_changed_internal,
|
||||
100,
|
||||
utils.debounce_strategy.CALL_LAST_ONLY
|
||||
)
|
||||
end
|
||||
|
||||
---Navigate to the given path.
|
||||
---@param path string Path to navigate to. If empty, will navigate to the cwd.
|
||||
M.navigate = function(state, path, path_to_reveal, callback, async)
|
||||
state.dirty = false
|
||||
local path_changed = false
|
||||
if path == nil then
|
||||
path = vim.fn.getcwd()
|
||||
end
|
||||
if path ~= state.path then
|
||||
state.path = path
|
||||
path_changed = true
|
||||
end
|
||||
if path_to_reveal then
|
||||
renderer.position.set(state, path_to_reveal)
|
||||
end
|
||||
|
||||
items.get_opened_buffers(state)
|
||||
|
||||
if path_changed and state.bind_to_cwd then
|
||||
vim.api.nvim_command("tcd " .. path)
|
||||
end
|
||||
|
||||
if type(callback) == "function" then
|
||||
vim.schedule(callback)
|
||||
end
|
||||
end
|
||||
|
||||
---Configures the plugin, should be called before the plugin is used.
|
||||
---@param config table Configuration table containing any keys that the user
|
||||
--wants to change from the defaults. May be empty to accept default values.
|
||||
M.setup = function(config, global_config)
|
||||
--Configure events for before_render
|
||||
if config.before_render then
|
||||
--convert to new event system
|
||||
manager.subscribe(M.name, {
|
||||
event = events.BEFORE_RENDER,
|
||||
handler = function(state)
|
||||
local this_state = get_state()
|
||||
if state == this_state then
|
||||
config.before_render(this_state)
|
||||
end
|
||||
end,
|
||||
})
|
||||
elseif global_config.enable_git_status then
|
||||
manager.subscribe(M.name, {
|
||||
event = events.BEFORE_RENDER,
|
||||
handler = function(state)
|
||||
local this_state = get_state()
|
||||
if state == this_state then
|
||||
state.git_status_lookup = git.status(state.git_base)
|
||||
end
|
||||
end,
|
||||
})
|
||||
manager.subscribe(M.name, {
|
||||
event = events.GIT_EVENT,
|
||||
handler = M.buffers_changed,
|
||||
})
|
||||
end
|
||||
|
||||
local refresh_events = {
|
||||
events.VIM_BUFFER_ADDED,
|
||||
events.VIM_BUFFER_DELETED,
|
||||
}
|
||||
if global_config.enable_refresh_on_write then
|
||||
table.insert(refresh_events, events.VIM_BUFFER_CHANGED)
|
||||
end
|
||||
for _, e in ipairs(refresh_events) do
|
||||
manager.subscribe(M.name, {
|
||||
event = e,
|
||||
handler = function(args)
|
||||
if args.afile == "" or utils.is_real_file(args.afile) then
|
||||
M.buffers_changed()
|
||||
end
|
||||
end,
|
||||
})
|
||||
end
|
||||
|
||||
if config.bind_to_cwd then
|
||||
manager.subscribe(M.name, {
|
||||
event = events.VIM_DIR_CHANGED,
|
||||
handler = wrap(manager.dir_changed),
|
||||
})
|
||||
end
|
||||
|
||||
if global_config.enable_diagnostics then
|
||||
manager.subscribe(M.name, {
|
||||
event = events.STATE_CREATED,
|
||||
handler = function(state)
|
||||
state.diagnostics_lookup = utils.get_diagnostic_counts()
|
||||
end,
|
||||
})
|
||||
manager.subscribe(M.name, {
|
||||
event = events.VIM_DIAGNOSTIC_CHANGED,
|
||||
handler = wrap(manager.diagnostics_changed),
|
||||
})
|
||||
end
|
||||
|
||||
--Configure event handlers for modified files
|
||||
if global_config.enable_modified_markers then
|
||||
manager.subscribe(M.name, {
|
||||
event = events.VIM_BUFFER_MODIFIED_SET,
|
||||
handler = wrap(manager.opened_buffers_changed),
|
||||
})
|
||||
end
|
||||
|
||||
-- Configure event handler for follow_current_file option
|
||||
if config.follow_current_file.enabled then
|
||||
manager.subscribe(M.name, {
|
||||
event = events.VIM_BUFFER_ENTER,
|
||||
handler = M.follow,
|
||||
})
|
||||
manager.subscribe(M.name, {
|
||||
event = events.VIM_TERMINAL_ENTER,
|
||||
handler = M.follow,
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,112 @@
|
||||
local vim = vim
|
||||
local renderer = require("neo-tree.ui.renderer")
|
||||
local utils = require("neo-tree.utils")
|
||||
local file_items = require("neo-tree.sources.common.file-items")
|
||||
local log = require("neo-tree.log")
|
||||
|
||||
local M = {}
|
||||
|
||||
---Get a table of all open buffers, along with all parent paths of those buffers.
|
||||
---The paths are the keys of the table, and all the values are 'true'.
|
||||
M.get_opened_buffers = function(state)
|
||||
if state.loading then
|
||||
return
|
||||
end
|
||||
state.loading = true
|
||||
local context = file_items.create_context()
|
||||
context.state = state
|
||||
-- Create root folder
|
||||
local root = file_items.create_item(context, state.path, "directory")
|
||||
root.name = vim.fn.fnamemodify(root.path, ":~")
|
||||
root.loaded = true
|
||||
root.search_pattern = state.search_pattern
|
||||
context.folders[root.path] = root
|
||||
local terminals = {}
|
||||
|
||||
local function add_buffer(bufnr, path)
|
||||
local is_loaded = vim.api.nvim_buf_is_loaded(bufnr)
|
||||
if is_loaded or state.show_unloaded then
|
||||
local is_listed = vim.fn.buflisted(bufnr)
|
||||
if is_listed == 1 then
|
||||
if path == "" then
|
||||
path = "[No Name]"
|
||||
end
|
||||
local success, item = pcall(file_items.create_item, context, path, "file", bufnr)
|
||||
if success then
|
||||
item.extra = {
|
||||
bufnr = bufnr,
|
||||
is_listed = is_listed,
|
||||
}
|
||||
else
|
||||
log.error("Error creating item for " .. path .. ": " .. item)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local bufs = vim.api.nvim_list_bufs()
|
||||
for _, b in ipairs(bufs) do
|
||||
local path = vim.api.nvim_buf_get_name(b)
|
||||
if vim.startswith(path, "term://") then
|
||||
local name = path:match("term://(.*)//.*")
|
||||
local abs_path = vim.fn.fnamemodify(name, ":p")
|
||||
local has_title, title = pcall(vim.api.nvim_buf_get_var, b, "term_title")
|
||||
local item = {
|
||||
name = has_title and title or name,
|
||||
ext = "terminal",
|
||||
path = abs_path,
|
||||
id = path,
|
||||
type = "terminal",
|
||||
loaded = true,
|
||||
extra = {
|
||||
bufnr = b,
|
||||
is_listed = true,
|
||||
},
|
||||
}
|
||||
if utils.is_subpath(state.path, abs_path) then
|
||||
table.insert(terminals, item)
|
||||
end
|
||||
elseif path == "" then
|
||||
add_buffer(b, path)
|
||||
else
|
||||
if #state.path > 1 then
|
||||
local rootsub = path:sub(1, #state.path)
|
||||
-- make sure this is within the root path
|
||||
if rootsub == state.path then
|
||||
add_buffer(b, path)
|
||||
end
|
||||
else
|
||||
add_buffer(b, path)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local root_folders = { root }
|
||||
|
||||
if #terminals > 0 then
|
||||
local terminal_root = {
|
||||
name = "Terminals",
|
||||
id = "Terminals",
|
||||
ext = "terminal",
|
||||
type = "terminal",
|
||||
children = terminals,
|
||||
loaded = true,
|
||||
search_pattern = state.search_pattern,
|
||||
}
|
||||
context.folders["Terminals"] = terminal_root
|
||||
if state.terminals_first then
|
||||
table.insert(root_folders, 1, terminal_root)
|
||||
else
|
||||
table.insert(root_folders, terminal_root)
|
||||
end
|
||||
end
|
||||
state.default_expanded_nodes = {}
|
||||
for id, _ in pairs(context.folders) do
|
||||
table.insert(state.default_expanded_nodes, id)
|
||||
end
|
||||
file_items.advanced_sort(root.children, state)
|
||||
renderer.show_nodes(root_folders, state)
|
||||
state.loading = false
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,889 @@
|
||||
--This file should contain all commands meant to be used by mappings.
|
||||
|
||||
local vim = vim
|
||||
local fs_actions = require("neo-tree.sources.filesystem.lib.fs_actions")
|
||||
local utils = require("neo-tree.utils")
|
||||
local renderer = require("neo-tree.ui.renderer")
|
||||
local events = require("neo-tree.events")
|
||||
local inputs = require("neo-tree.ui.inputs")
|
||||
local popups = require("neo-tree.ui.popups")
|
||||
local log = require("neo-tree.log")
|
||||
local help = require("neo-tree.sources.common.help")
|
||||
local Preview = require("neo-tree.sources.common.preview")
|
||||
local async = require("plenary.async")
|
||||
local node_expander = require("neo-tree.sources.common.node_expander")
|
||||
|
||||
---Gets the node parent folder
|
||||
---@param state table to look for nodes
|
||||
---@return table? node
|
||||
local function get_folder_node(state)
|
||||
local tree = state.tree
|
||||
local node = tree:get_node()
|
||||
local last_id = node:get_id()
|
||||
|
||||
while node do
|
||||
local insert_as_local = state.config.insert_as
|
||||
local insert_as_global = require("neo-tree").config.window.insert_as
|
||||
local use_parent
|
||||
if insert_as_local then
|
||||
use_parent = insert_as_local == "sibling"
|
||||
else
|
||||
use_parent = insert_as_global == "sibling"
|
||||
end
|
||||
|
||||
local is_open_dir = node.type == "directory" and (node:is_expanded() or node.empty_expanded)
|
||||
if use_parent and not is_open_dir then
|
||||
return tree:get_node(node:get_parent_id())
|
||||
end
|
||||
|
||||
if node.type == "directory" then
|
||||
return node
|
||||
end
|
||||
|
||||
local parent_id = node:get_parent_id()
|
||||
if not parent_id or parent_id == last_id then
|
||||
return node
|
||||
else
|
||||
last_id = parent_id
|
||||
node = tree:get_node(parent_id)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---The using_root_directory is used to decide what part of the filename to show
|
||||
-- the user when asking for a new filename to e.g. create, copy to or move to.
|
||||
---@param state table The state of the source
|
||||
---@return string The root path from which the relative source path should be taken
|
||||
local function get_using_root_directory(state)
|
||||
-- default to showing only the basename of the path
|
||||
local using_root_directory = get_folder_node(state):get_id()
|
||||
local show_path = state.config.show_path
|
||||
if show_path == "absolute" then
|
||||
using_root_directory = ""
|
||||
elseif show_path == "relative" then
|
||||
using_root_directory = state.path
|
||||
elseif show_path ~= nil and show_path ~= "none" then
|
||||
log.warn(
|
||||
'A neo-tree mapping was setup with a config.show_path option with invalid value: "'
|
||||
.. show_path
|
||||
.. '", falling back to its default: nil/"none"'
|
||||
)
|
||||
end
|
||||
return using_root_directory
|
||||
end
|
||||
|
||||
local M = {}
|
||||
|
||||
---Adds all missing common commands to the given module
|
||||
---@param to_source_command_module table The commands module for a source
|
||||
---@param pattern string? A pattern specifying which commands to add, nil to add all
|
||||
M._add_common_commands = function(to_source_command_module, pattern)
|
||||
for name, func in pairs(M) do
|
||||
if
|
||||
type(name) == "string"
|
||||
and not to_source_command_module[name]
|
||||
and (not pattern or name:find(pattern))
|
||||
and not name:find("^_")
|
||||
then
|
||||
to_source_command_module[name] = func
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---Add a new file or dir at the current node
|
||||
---@param state table The state of the source
|
||||
---@param callback function The callback to call when the command is done. Called with the parent node as the argument.
|
||||
M.add = function(state, callback)
|
||||
local node = get_folder_node(state)
|
||||
local in_directory = node:get_id()
|
||||
local using_root_directory = get_using_root_directory(state)
|
||||
fs_actions.create_node(in_directory, callback, using_root_directory)
|
||||
end
|
||||
|
||||
---Add a new file or dir at the current node
|
||||
---@param state table The state of the source
|
||||
---@param callback function The callback to call when the command is done. Called with the parent node as the argument.
|
||||
M.add_directory = function(state, callback)
|
||||
local node = get_folder_node(state)
|
||||
local in_directory = node:get_id()
|
||||
local using_root_directory = get_using_root_directory(state)
|
||||
fs_actions.create_directory(in_directory, callback, using_root_directory)
|
||||
end
|
||||
|
||||
---Expand all nodes
|
||||
---@param state table The state of the source
|
||||
---@param node table A node to expand
|
||||
---@param prefetcher table an object with two methods `prefetch(state, node)` and `should_prefetch(node) => boolean`
|
||||
M.expand_all_nodes = function(state, node, prefetcher)
|
||||
log.debug("Expanding all nodes under " .. node:get_id())
|
||||
if prefetcher == nil then
|
||||
prefetcher = node_expander.default_prefetcher
|
||||
end
|
||||
|
||||
renderer.position.set(state, nil)
|
||||
|
||||
local task = function()
|
||||
node_expander.expand_directory_recursively(state, node, prefetcher)
|
||||
end
|
||||
async.run(task, function()
|
||||
log.debug("All nodes expanded - redrawing")
|
||||
renderer.redraw(state)
|
||||
end)
|
||||
end
|
||||
|
||||
M.close_node = function(state, callback)
|
||||
local tree = state.tree
|
||||
local node = tree:get_node()
|
||||
local parent_node = tree:get_node(node:get_parent_id())
|
||||
local target_node
|
||||
|
||||
if node:has_children() and node:is_expanded() then
|
||||
target_node = node
|
||||
else
|
||||
target_node = parent_node
|
||||
end
|
||||
|
||||
local root = tree:get_nodes()[1]
|
||||
local is_root = target_node:get_id() == root:get_id()
|
||||
|
||||
if target_node and target_node:has_children() and not is_root then
|
||||
target_node:collapse()
|
||||
renderer.redraw(state)
|
||||
renderer.focus_node(state, target_node:get_id())
|
||||
if
|
||||
state.explicitly_opened_directories
|
||||
and state.explicitly_opened_directories[target_node:get_id()]
|
||||
then
|
||||
state.explicitly_opened_directories[target_node:get_id()] = false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
M.close_all_subnodes = function(state)
|
||||
local tree = state.tree
|
||||
local node = tree:get_node()
|
||||
local parent_node = tree:get_node(node:get_parent_id())
|
||||
local target_node
|
||||
|
||||
if node:has_children() and node:is_expanded() then
|
||||
target_node = node
|
||||
else
|
||||
target_node = parent_node
|
||||
end
|
||||
|
||||
renderer.collapse_all_nodes(tree, target_node:get_id())
|
||||
renderer.redraw(state)
|
||||
renderer.focus_node(state, target_node:get_id())
|
||||
if
|
||||
state.explicitly_opened_directories
|
||||
and state.explicitly_opened_directories[target_node:get_id()]
|
||||
then
|
||||
state.explicitly_opened_directories[target_node:get_id()] = false
|
||||
end
|
||||
end
|
||||
|
||||
M.close_all_nodes = function(state)
|
||||
state.explicitly_opened_directories = {}
|
||||
renderer.collapse_all_nodes(state.tree)
|
||||
renderer.redraw(state)
|
||||
end
|
||||
|
||||
M.close_window = function(state)
|
||||
renderer.close(state)
|
||||
end
|
||||
|
||||
M.toggle_auto_expand_width = function(state)
|
||||
if state.window.position == "float" then
|
||||
return
|
||||
end
|
||||
state.window.auto_expand_width = state.window.auto_expand_width == false
|
||||
local width = utils.resolve_width(state.window.width)
|
||||
if not state.window.auto_expand_width then
|
||||
if (state.window.last_user_width or width) >= vim.api.nvim_win_get_width(0) then
|
||||
state.window.last_user_width = width
|
||||
end
|
||||
vim.api.nvim_win_set_width(0, state.window.last_user_width)
|
||||
state.win_width = state.window.last_user_width
|
||||
state.longest_width_exact = 0
|
||||
log.trace(string.format("Collapse auto_expand_width."))
|
||||
end
|
||||
renderer.redraw(state)
|
||||
end
|
||||
|
||||
local copy_node_to_clipboard = function(state, node)
|
||||
state.clipboard = state.clipboard or {}
|
||||
local existing = state.clipboard[node.id]
|
||||
if existing and existing.action == "copy" then
|
||||
state.clipboard[node.id] = nil
|
||||
else
|
||||
state.clipboard[node.id] = { action = "copy", node = node }
|
||||
log.info("Copied " .. node.name .. " to clipboard")
|
||||
end
|
||||
end
|
||||
|
||||
---Marks node as copied, so that it can be pasted somewhere else.
|
||||
M.copy_to_clipboard = function(state, callback)
|
||||
local node = state.tree:get_node()
|
||||
if node.type == "message" then
|
||||
return
|
||||
end
|
||||
copy_node_to_clipboard(state, node)
|
||||
if callback then
|
||||
callback()
|
||||
end
|
||||
end
|
||||
|
||||
M.copy_to_clipboard_visual = function(state, selected_nodes, callback)
|
||||
for _, node in ipairs(selected_nodes) do
|
||||
if node.type ~= "message" then
|
||||
copy_node_to_clipboard(state, node)
|
||||
end
|
||||
end
|
||||
if callback then
|
||||
callback()
|
||||
end
|
||||
end
|
||||
|
||||
local cut_node_to_clipboard = function(state, node)
|
||||
state.clipboard = state.clipboard or {}
|
||||
local existing = state.clipboard[node.id]
|
||||
if existing and existing.action == "cut" then
|
||||
state.clipboard[node.id] = nil
|
||||
else
|
||||
state.clipboard[node.id] = { action = "cut", node = node }
|
||||
log.info("Cut " .. node.name .. " to clipboard")
|
||||
end
|
||||
end
|
||||
|
||||
---Marks node as cut, so that it can be pasted (moved) somewhere else.
|
||||
M.cut_to_clipboard = function(state, callback)
|
||||
local node = state.tree:get_node()
|
||||
cut_node_to_clipboard(state, node)
|
||||
if callback then
|
||||
callback()
|
||||
end
|
||||
end
|
||||
|
||||
M.cut_to_clipboard_visual = function(state, selected_nodes, callback)
|
||||
for _, node in ipairs(selected_nodes) do
|
||||
if node.type ~= "message" then
|
||||
cut_node_to_clipboard(state, node)
|
||||
end
|
||||
end
|
||||
if callback then
|
||||
callback()
|
||||
end
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Git commands
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
M.git_add_file = function(state)
|
||||
local node = state.tree:get_node()
|
||||
if node.type == "message" then
|
||||
return
|
||||
end
|
||||
local path = node:get_id()
|
||||
local cmd = { "git", "add", path }
|
||||
vim.fn.system(cmd)
|
||||
events.fire_event(events.GIT_EVENT)
|
||||
end
|
||||
|
||||
M.git_add_all = function(state)
|
||||
local cmd = { "git", "add", "-A" }
|
||||
vim.fn.system(cmd)
|
||||
events.fire_event(events.GIT_EVENT)
|
||||
end
|
||||
|
||||
M.git_commit = function(state, and_push)
|
||||
local width = vim.fn.winwidth(0) - 2
|
||||
local row = vim.api.nvim_win_get_height(0) - 3
|
||||
local popup_options = {
|
||||
relative = "win",
|
||||
position = {
|
||||
row = row,
|
||||
col = 0,
|
||||
},
|
||||
size = width,
|
||||
}
|
||||
|
||||
inputs.input("Commit message: ", "", function(msg)
|
||||
local cmd = { "git", "commit", "-m", msg }
|
||||
local title = "git commit"
|
||||
local result = vim.fn.systemlist(cmd)
|
||||
if vim.v.shell_error ~= 0 or (#result > 0 and vim.startswith(result[1], "fatal:")) then
|
||||
popups.alert("ERROR: git commit", result)
|
||||
return
|
||||
end
|
||||
if and_push then
|
||||
title = "git commit && git push"
|
||||
cmd = { "git", "push" }
|
||||
local result2 = vim.fn.systemlist(cmd)
|
||||
table.insert(result, "")
|
||||
for i = 1, #result2 do
|
||||
table.insert(result, result2[i])
|
||||
end
|
||||
end
|
||||
events.fire_event(events.GIT_EVENT)
|
||||
popups.alert(title, result)
|
||||
end, popup_options)
|
||||
end
|
||||
|
||||
M.git_commit_and_push = function(state)
|
||||
M.git_commit(state, true)
|
||||
end
|
||||
|
||||
M.git_push = function(state)
|
||||
inputs.confirm("Are you sure you want to push your changes?", function(yes)
|
||||
if yes then
|
||||
local result = vim.fn.systemlist({ "git", "push" })
|
||||
events.fire_event(events.GIT_EVENT)
|
||||
popups.alert("git push", result)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
M.git_unstage_file = function(state)
|
||||
local node = state.tree:get_node()
|
||||
if node.type == "message" then
|
||||
return
|
||||
end
|
||||
local path = node:get_id()
|
||||
local cmd = { "git", "reset", "--", path }
|
||||
vim.fn.system(cmd)
|
||||
events.fire_event(events.GIT_EVENT)
|
||||
end
|
||||
|
||||
M.git_revert_file = function(state)
|
||||
local node = state.tree:get_node()
|
||||
if node.type == "message" then
|
||||
return
|
||||
end
|
||||
local path = node:get_id()
|
||||
local cmd = { "git", "checkout", "HEAD", "--", path }
|
||||
local msg = string.format("Are you sure you want to revert %s?", node.name)
|
||||
inputs.confirm(msg, function(yes)
|
||||
if yes then
|
||||
vim.fn.system(cmd)
|
||||
events.fire_event(events.GIT_EVENT)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- END Git commands
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
M.next_source = function(state)
|
||||
local sources = require("neo-tree").config.sources
|
||||
local sources = require("neo-tree").config.source_selector.sources
|
||||
local next_source = sources[1]
|
||||
for i, source_info in ipairs(sources) do
|
||||
if source_info.source == state.name then
|
||||
next_source = sources[i + 1]
|
||||
if not next_source then
|
||||
next_source = sources[1]
|
||||
end
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
require("neo-tree.command").execute({
|
||||
source = next_source.source,
|
||||
position = state.current_position,
|
||||
action = "focus",
|
||||
})
|
||||
end
|
||||
|
||||
M.prev_source = function(state)
|
||||
local sources = require("neo-tree").config.sources
|
||||
local sources = require("neo-tree").config.source_selector.sources
|
||||
local next_source = sources[#sources]
|
||||
for i, source_info in ipairs(sources) do
|
||||
if source_info.source == state.name then
|
||||
next_source = sources[i - 1]
|
||||
if not next_source then
|
||||
next_source = sources[#sources]
|
||||
end
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
require("neo-tree.command").execute({
|
||||
source = next_source.source,
|
||||
position = state.current_position,
|
||||
action = "focus",
|
||||
})
|
||||
end
|
||||
|
||||
local function set_sort(state, label)
|
||||
local sort = state.sort or { label = "Name", direction = -1 }
|
||||
if sort.label == label then
|
||||
sort.direction = sort.direction * -1
|
||||
else
|
||||
sort.label = label
|
||||
sort.direction = -1
|
||||
end
|
||||
state.sort = sort
|
||||
end
|
||||
|
||||
M.order_by_created = function(state)
|
||||
set_sort(state, "Created")
|
||||
state.sort_field_provider = function(node)
|
||||
local stat = utils.get_stat(node)
|
||||
return stat.birthtime and stat.birthtime.sec or 0
|
||||
end
|
||||
require("neo-tree.sources.manager").refresh(state.name)
|
||||
end
|
||||
|
||||
M.order_by_modified = function(state)
|
||||
set_sort(state, "Last Modified")
|
||||
state.sort_field_provider = function(node)
|
||||
local stat = utils.get_stat(node)
|
||||
return stat.mtime and stat.mtime.sec or 0
|
||||
end
|
||||
require("neo-tree.sources.manager").refresh(state.name)
|
||||
end
|
||||
|
||||
M.order_by_name = function(state)
|
||||
set_sort(state, "Name")
|
||||
state.sort_field_provider = nil
|
||||
require("neo-tree.sources.manager").refresh(state.name)
|
||||
end
|
||||
|
||||
M.order_by_size = function(state)
|
||||
set_sort(state, "Size")
|
||||
state.sort_field_provider = function(node)
|
||||
local stat = utils.get_stat(node)
|
||||
return stat.size or 0
|
||||
end
|
||||
require("neo-tree.sources.manager").refresh(state.name)
|
||||
end
|
||||
|
||||
M.order_by_type = function(state)
|
||||
set_sort(state, "Type")
|
||||
state.sort_field_provider = function(node)
|
||||
return node.ext or node.type
|
||||
end
|
||||
require("neo-tree.sources.manager").refresh(state.name)
|
||||
end
|
||||
|
||||
M.order_by_git_status = function(state)
|
||||
set_sort(state, "Git Status")
|
||||
|
||||
state.sort_field_provider = function(node)
|
||||
local git_status_lookup = state.git_status_lookup or {}
|
||||
local git_status = git_status_lookup[node.path]
|
||||
if git_status then
|
||||
return git_status
|
||||
end
|
||||
|
||||
if node.filtered_by and node.filtered_by.gitignored then
|
||||
return "!!"
|
||||
else
|
||||
return ""
|
||||
end
|
||||
end
|
||||
|
||||
require("neo-tree.sources.manager").refresh(state.name)
|
||||
end
|
||||
|
||||
M.order_by_diagnostics = function(state)
|
||||
set_sort(state, "Diagnostics")
|
||||
|
||||
state.sort_field_provider = function(node)
|
||||
local diag = state.diagnostics_lookup or {}
|
||||
local diagnostics = diag[node.path]
|
||||
if not diagnostics then
|
||||
return 0
|
||||
end
|
||||
if not diagnostics.severity_number then
|
||||
return 0
|
||||
end
|
||||
-- lower severity number means higher severity
|
||||
return 5 - diagnostics.severity_number
|
||||
end
|
||||
|
||||
require("neo-tree.sources.manager").refresh(state.name)
|
||||
end
|
||||
|
||||
M.show_debug_info = function(state)
|
||||
print(vim.inspect(state))
|
||||
end
|
||||
|
||||
M.show_file_details = function(state)
|
||||
local node = state.tree:get_node()
|
||||
if node.type == "message" then
|
||||
return
|
||||
end
|
||||
local stat = utils.get_stat(node)
|
||||
local left = {}
|
||||
local right = {}
|
||||
table.insert(left, "Name")
|
||||
table.insert(right, node.name)
|
||||
table.insert(left, "Path")
|
||||
table.insert(right, node:get_id())
|
||||
table.insert(left, "Type")
|
||||
table.insert(right, node.type)
|
||||
if stat.size then
|
||||
table.insert(left, "Size")
|
||||
table.insert(right, utils.human_size(stat.size))
|
||||
table.insert(left, "Created")
|
||||
table.insert(right, os.date("%Y-%m-%d %I:%M %p", stat.birthtime.sec))
|
||||
table.insert(left, "Modified")
|
||||
table.insert(right, os.date("%Y-%m-%d %I:%M %p", stat.mtime.sec))
|
||||
end
|
||||
|
||||
local lines = {}
|
||||
for i, v in ipairs(left) do
|
||||
local line = string.format("%9s: %s", v, right[i])
|
||||
table.insert(lines, line)
|
||||
end
|
||||
|
||||
popups.alert("File Details", lines)
|
||||
end
|
||||
|
||||
---Pastes all items from the clipboard to the current directory.
|
||||
---@param state table The state of the source
|
||||
---@param callback function The callback to call when the command is done. Called with the parent node as the argument.
|
||||
M.paste_from_clipboard = function(state, callback)
|
||||
if state.clipboard then
|
||||
local folder = get_folder_node(state):get_id()
|
||||
-- Convert to list so to make it easier to pop items from the stack.
|
||||
local clipboard_list = {}
|
||||
for _, item in pairs(state.clipboard) do
|
||||
table.insert(clipboard_list, item)
|
||||
end
|
||||
state.clipboard = nil
|
||||
local handle_next_paste, paste_complete
|
||||
|
||||
paste_complete = function(source, destination)
|
||||
if callback then
|
||||
local insert_as = require("neo-tree").config.window.insert_as
|
||||
-- open the folder so the user can see the new files
|
||||
local node = insert_as == "sibling" and state.tree:get_node() or state.tree:get_node(folder)
|
||||
if not node then
|
||||
log.warn("Could not find node for " .. folder)
|
||||
end
|
||||
callback(node, destination)
|
||||
end
|
||||
local next_item = table.remove(clipboard_list)
|
||||
if next_item then
|
||||
handle_next_paste(next_item)
|
||||
end
|
||||
end
|
||||
|
||||
handle_next_paste = function(item)
|
||||
if item.action == "copy" then
|
||||
fs_actions.copy_node(
|
||||
item.node.path,
|
||||
folder .. utils.path_separator .. item.node.name,
|
||||
paste_complete
|
||||
)
|
||||
elseif item.action == "cut" then
|
||||
fs_actions.move_node(
|
||||
item.node.path,
|
||||
folder .. utils.path_separator .. item.node.name,
|
||||
paste_complete
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
local next_item = table.remove(clipboard_list)
|
||||
if next_item then
|
||||
handle_next_paste(next_item)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---Copies a node to a new location, using typed input.
|
||||
---@param state table The state of the source
|
||||
---@param callback function The callback to call when the command is done. Called with the parent node as the argument.
|
||||
M.copy = function(state, callback)
|
||||
local node = state.tree:get_node()
|
||||
if node.type == "message" then
|
||||
return
|
||||
end
|
||||
local using_root_directory = get_using_root_directory(state)
|
||||
fs_actions.copy_node(node.path, nil, callback, using_root_directory)
|
||||
end
|
||||
|
||||
---Moves a node to a new location, using typed input.
|
||||
---@param state table The state of the source
|
||||
---@param callback function The callback to call when the command is done. Called with the parent node as the argument.
|
||||
M.move = function(state, callback)
|
||||
local node = state.tree:get_node()
|
||||
if node.type == "message" then
|
||||
return
|
||||
end
|
||||
local using_root_directory = get_using_root_directory(state)
|
||||
fs_actions.move_node(node.path, nil, callback, using_root_directory)
|
||||
end
|
||||
|
||||
M.delete = function(state, callback)
|
||||
local tree = state.tree
|
||||
local node = tree:get_node()
|
||||
if node.type == "file" or node.type == "directory" then
|
||||
fs_actions.delete_node(node.path, callback)
|
||||
else
|
||||
log.warn("The `delete` command can only be used on files and directories")
|
||||
end
|
||||
end
|
||||
|
||||
M.delete_visual = function(state, selected_nodes, callback)
|
||||
local paths_to_delete = {}
|
||||
for _, node_to_delete in pairs(selected_nodes) do
|
||||
if node_to_delete.type == "file" or node_to_delete.type == "directory" then
|
||||
table.insert(paths_to_delete, node_to_delete.path)
|
||||
end
|
||||
end
|
||||
fs_actions.delete_nodes(paths_to_delete, callback)
|
||||
end
|
||||
|
||||
M.preview = function(state)
|
||||
Preview.show(state)
|
||||
end
|
||||
|
||||
M.revert_preview = function()
|
||||
Preview.hide()
|
||||
end
|
||||
--
|
||||
-- Multi-purpose function to back out of whatever we are in
|
||||
M.cancel = function(state)
|
||||
if Preview.is_active() then
|
||||
Preview.hide()
|
||||
else
|
||||
if state.current_position == "float" then
|
||||
renderer.close_all_floating_windows()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
M.toggle_preview = function(state)
|
||||
Preview.toggle(state)
|
||||
end
|
||||
|
||||
M.scroll_preview = function(state)
|
||||
Preview.scroll(state)
|
||||
end
|
||||
|
||||
M.focus_preview = function()
|
||||
Preview.focus()
|
||||
end
|
||||
|
||||
---Expands or collapses the current node.
|
||||
M.toggle_node = function(state, toggle_directory)
|
||||
local tree = state.tree
|
||||
local node = tree:get_node()
|
||||
if not utils.is_expandable(node) then
|
||||
return
|
||||
end
|
||||
if node.type == "directory" and toggle_directory then
|
||||
toggle_directory(node)
|
||||
elseif node:has_children() then
|
||||
local updated = false
|
||||
if node:is_expanded() then
|
||||
updated = node:collapse()
|
||||
else
|
||||
updated = node:expand()
|
||||
end
|
||||
if updated then
|
||||
renderer.redraw(state)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---Expands or collapses the current node.
|
||||
M.toggle_directory = function(state, toggle_directory)
|
||||
local tree = state.tree
|
||||
local node = tree:get_node()
|
||||
if node.type ~= "directory" then
|
||||
return
|
||||
end
|
||||
M.toggle_node(state, toggle_directory)
|
||||
end
|
||||
|
||||
---Open file or directory
|
||||
---@param state table The state of the source
|
||||
---@param open_cmd string The vim command to use to open the file
|
||||
---@param toggle_directory function The function to call to toggle a directory
|
||||
---open/closed
|
||||
local open_with_cmd = function(state, open_cmd, toggle_directory, open_file)
|
||||
local tree = state.tree
|
||||
local success, node = pcall(tree.get_node, tree)
|
||||
if node.type == "message" then
|
||||
return
|
||||
end
|
||||
if not (success and node) then
|
||||
log.debug("Could not get node.")
|
||||
return
|
||||
end
|
||||
|
||||
local function open()
|
||||
M.revert_preview()
|
||||
local path = node.path or node:get_id()
|
||||
local bufnr = node.extra and node.extra.bufnr
|
||||
if node.type == "terminal" then
|
||||
path = node:get_id()
|
||||
end
|
||||
if type(open_file) == "function" then
|
||||
open_file(state, path, open_cmd, bufnr)
|
||||
else
|
||||
utils.open_file(state, path, open_cmd, bufnr)
|
||||
end
|
||||
local extra = node.extra or {}
|
||||
local pos = extra.position or extra.end_position
|
||||
if pos ~= nil then
|
||||
vim.api.nvim_win_set_cursor(0, { (pos[1] or 0) + 1, pos[2] or 0 })
|
||||
vim.api.nvim_win_call(0, function()
|
||||
vim.cmd("normal! zvzz") -- expand folds and center cursor
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
local config = state.config or {}
|
||||
if node.type ~= "directory" and config.no_expand_file ~= nil then
|
||||
log.warn("`no_expand_file` options is deprecated, move to `expand_nested_files` (OPPOSITE)")
|
||||
config.expand_nested_files = not config.no_expand_file
|
||||
end
|
||||
if node.type == "directory" then
|
||||
M.toggle_node(state, toggle_directory)
|
||||
elseif node:has_children() and config.expand_nested_files and not node:is_expanded() then
|
||||
M.toggle_node(state, toggle_directory)
|
||||
else
|
||||
open()
|
||||
end
|
||||
end
|
||||
|
||||
---Open file or directory in the closest window
|
||||
---@param state table The state of the source
|
||||
---@param toggle_directory function The function to call to toggle a directory
|
||||
---open/closed
|
||||
M.open = function(state, toggle_directory)
|
||||
open_with_cmd(state, "e", toggle_directory)
|
||||
end
|
||||
|
||||
---Open file or directory in a split of the closest window
|
||||
---@param state table The state of the source
|
||||
---@param toggle_directory function The function to call to toggle a directory
|
||||
---open/closed
|
||||
M.open_split = function(state, toggle_directory)
|
||||
open_with_cmd(state, "split", toggle_directory)
|
||||
end
|
||||
|
||||
---Open file or directory in a vertical split of the closest window
|
||||
---@param state table The state of the source
|
||||
---@param toggle_directory function The function to call to toggle a directory
|
||||
---open/closed
|
||||
M.open_vsplit = function(state, toggle_directory)
|
||||
open_with_cmd(state, "vsplit", toggle_directory)
|
||||
end
|
||||
|
||||
---Open file or directory in a right below vertical split of the closest window
|
||||
---@param state table The state of the source
|
||||
---@param toggle_directory function The function to call to toggle a directory
|
||||
---open/closed
|
||||
M.open_rightbelow_vs = function(state, toggle_directory)
|
||||
open_with_cmd(state, "rightbelow vs", toggle_directory)
|
||||
end
|
||||
|
||||
---Open file or directory in a left above vertical split of the closest window
|
||||
---@param state table The state of the source
|
||||
---@param toggle_directory function The function to call to toggle a directory
|
||||
---open/closed
|
||||
M.open_leftabove_vs = function(state, toggle_directory)
|
||||
open_with_cmd(state, "leftabove vs", toggle_directory)
|
||||
end
|
||||
|
||||
---Open file or directory in a new tab
|
||||
---@param state table The state of the source
|
||||
---@param toggle_directory function The function to call to toggle a directory
|
||||
---open/closed
|
||||
M.open_tabnew = function(state, toggle_directory)
|
||||
open_with_cmd(state, "tabnew", toggle_directory)
|
||||
end
|
||||
|
||||
---Open file or directory or focus it if a buffer already exists with it
|
||||
---@param state table The state of the source
|
||||
---@param toggle_directory function The function to call to toggle a directory
|
||||
---open/closed
|
||||
M.open_drop = function(state, toggle_directory)
|
||||
open_with_cmd(state, "drop", toggle_directory)
|
||||
end
|
||||
|
||||
---Open file or directory in new tab or focus it if a buffer already exists with it
|
||||
---@param state table The state of the source
|
||||
---@param toggle_directory function The function to call to toggle a directory
|
||||
---open/closed
|
||||
M.open_tab_drop = function(state, toggle_directory)
|
||||
open_with_cmd(state, "tab drop", toggle_directory)
|
||||
end
|
||||
|
||||
M.rename = function(state, callback)
|
||||
local tree = state.tree
|
||||
local node = tree:get_node()
|
||||
if node.type == "message" then
|
||||
return
|
||||
end
|
||||
fs_actions.rename_node(node.path, callback)
|
||||
end
|
||||
|
||||
---Marks potential windows with letters and will open the give node in the picked window.
|
||||
---@param state table The state of the source
|
||||
---@param path string The path to open
|
||||
---@param cmd string Command that is used to perform action on picked window
|
||||
local use_window_picker = function(state, path, cmd)
|
||||
local success, picker = pcall(require, "window-picker")
|
||||
if not success then
|
||||
print(
|
||||
"You'll need to install window-picker to use this command: https://github.com/s1n7ax/nvim-window-picker"
|
||||
)
|
||||
return
|
||||
end
|
||||
local events = require("neo-tree.events")
|
||||
local event_result = events.fire_event(events.FILE_OPEN_REQUESTED, {
|
||||
state = state,
|
||||
path = path,
|
||||
open_cmd = cmd,
|
||||
}) or {}
|
||||
if event_result.handled then
|
||||
events.fire_event(events.FILE_OPENED, path)
|
||||
return
|
||||
end
|
||||
local picked_window_id = picker.pick_window()
|
||||
if picked_window_id then
|
||||
vim.api.nvim_set_current_win(picked_window_id)
|
||||
local result, err = pcall(vim.cmd, cmd .. " " .. vim.fn.fnameescape(path))
|
||||
if result or err == "Vim(edit):E325: ATTENTION" then
|
||||
-- fixes #321
|
||||
vim.api.nvim_buf_set_option(0, "buflisted", true)
|
||||
events.fire_event(events.FILE_OPENED, path)
|
||||
else
|
||||
log.error("Error opening file:", err)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---Marks potential windows with letters and will open the give node in the picked window.
|
||||
M.open_with_window_picker = function(state, toggle_directory)
|
||||
open_with_cmd(state, "edit", toggle_directory, use_window_picker)
|
||||
end
|
||||
|
||||
---Marks potential windows with letters and will open the give node in a split next to the picked window.
|
||||
M.split_with_window_picker = function(state, toggle_directory)
|
||||
open_with_cmd(state, "split", toggle_directory, use_window_picker)
|
||||
end
|
||||
|
||||
---Marks potential windows with letters and will open the give node in a vertical split next to the picked window.
|
||||
M.vsplit_with_window_picker = function(state, toggle_directory)
|
||||
open_with_cmd(state, "vsplit", toggle_directory, use_window_picker)
|
||||
end
|
||||
|
||||
M.show_help = function(state)
|
||||
local title = state.config and state.config.title or nil
|
||||
local prefix_key = state.config and state.config.prefix_key or nil
|
||||
help.show(state, title, prefix_key)
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,558 @@
|
||||
-- This file contains the built-in components. Each componment is a function
|
||||
-- that takes the following arguments:
|
||||
-- config: A table containing the configuration provided by the user
|
||||
-- when declaring this component in their renderer config.
|
||||
-- node: A NuiNode object for the currently focused node.
|
||||
-- state: The current state of the source providing the items.
|
||||
--
|
||||
-- The function should return either a table, or a list of tables, each of which
|
||||
-- contains the following keys:
|
||||
-- text: The text to display for this item.
|
||||
-- highlight: The highlight group to apply to this text.
|
||||
|
||||
local highlights = require("neo-tree.ui.highlights")
|
||||
local utils = require("neo-tree.utils")
|
||||
local file_nesting = require("neo-tree.sources.common.file-nesting")
|
||||
local container = require("neo-tree.sources.common.container")
|
||||
local log = require("neo-tree.log")
|
||||
|
||||
local M = {}
|
||||
|
||||
local make_two_char = function(symbol)
|
||||
if vim.fn.strchars(symbol) == 1 then
|
||||
return symbol .. " "
|
||||
else
|
||||
return symbol
|
||||
end
|
||||
end
|
||||
-- only works in the buffers component, but it's here so we don't have to defined
|
||||
-- multple renderers.
|
||||
M.bufnr = function(config, node, state)
|
||||
local highlight = config.highlight or highlights.BUFFER_NUMBER
|
||||
local bufnr = node.extra and node.extra.bufnr
|
||||
if not bufnr then
|
||||
return {}
|
||||
end
|
||||
return {
|
||||
text = string.format("#%s", bufnr),
|
||||
highlight = highlight,
|
||||
}
|
||||
end
|
||||
|
||||
M.clipboard = function(config, node, state)
|
||||
local clipboard = state.clipboard or {}
|
||||
local clipboard_state = clipboard[node:get_id()]
|
||||
if not clipboard_state then
|
||||
return {}
|
||||
end
|
||||
return {
|
||||
text = " (" .. clipboard_state.action .. ")",
|
||||
highlight = config.highlight or highlights.DIM_TEXT,
|
||||
}
|
||||
end
|
||||
|
||||
M.container = container.render
|
||||
|
||||
M.current_filter = function(config, node, state)
|
||||
local filter = node.search_pattern or ""
|
||||
if filter == "" then
|
||||
return {}
|
||||
end
|
||||
return {
|
||||
{
|
||||
text = "Find",
|
||||
highlight = highlights.DIM_TEXT,
|
||||
},
|
||||
{
|
||||
text = string.format('"%s"', filter),
|
||||
highlight = config.highlight or highlights.FILTER_TERM,
|
||||
},
|
||||
{
|
||||
text = "in",
|
||||
highlight = highlights.DIM_TEXT,
|
||||
},
|
||||
}
|
||||
end
|
||||
|
||||
---`sign_getdefined` based wrapper with compatibility
|
||||
---@param severity string
|
||||
---@return vim.fn.sign_getdefined.ret.item
|
||||
local function get_defined_sign(severity)
|
||||
local defined
|
||||
|
||||
if vim.fn.has("nvim-0.10") > 0 then
|
||||
local signs_config = vim.diagnostic.config().signs
|
||||
if type(signs_config) == "table" then
|
||||
local identifier = severity:sub(1, 1)
|
||||
if identifier == "H" then
|
||||
identifier = "N"
|
||||
end
|
||||
defined = {
|
||||
text = (signs_config.text or {})[vim.diagnostic.severity[identifier]],
|
||||
texthl = "DiagnosticSign" .. severity,
|
||||
}
|
||||
end
|
||||
else -- before 0.10
|
||||
defined = vim.fn.sign_getdefined("DiagnosticSign" .. severity)
|
||||
if vim.tbl_isempty(defined) then
|
||||
-- backwards compatibility...
|
||||
local old_severity = severity
|
||||
if severity == "Warning" then
|
||||
old_severity = "Warn"
|
||||
elseif severity == "Information" then
|
||||
old_severity = "Info"
|
||||
end
|
||||
defined = vim.fn.sign_getdefined("LspDiagnosticsSign" .. old_severity)
|
||||
end
|
||||
defined = defined and defined[1]
|
||||
end
|
||||
|
||||
if type(defined) ~= "table" then
|
||||
defined = {}
|
||||
end
|
||||
return defined
|
||||
end
|
||||
|
||||
M.diagnostics = function(config, node, state)
|
||||
local diag = state.diagnostics_lookup or {}
|
||||
local diag_state = utils.index_by_path(diag, node:get_id())
|
||||
if config.hide_when_expanded and node.type == "directory" and node:is_expanded() then
|
||||
return {}
|
||||
end
|
||||
if not diag_state then
|
||||
return {}
|
||||
end
|
||||
if config.errors_only and diag_state.severity_number > 1 then
|
||||
return {}
|
||||
end
|
||||
local severity = diag_state.severity_string
|
||||
local defined = get_defined_sign(severity)
|
||||
|
||||
-- check for overrides in the component config
|
||||
local severity_lower = severity:lower()
|
||||
if config.symbols and config.symbols[severity_lower] then
|
||||
defined.texthl = defined.texthl or ("Diagnostic" .. severity)
|
||||
defined.text = config.symbols[severity_lower]
|
||||
end
|
||||
if config.highlights and config.highlights[severity_lower] then
|
||||
defined.text = defined.text or severity:sub(1, 1)
|
||||
defined.texthl = config.highlights[severity_lower]
|
||||
end
|
||||
|
||||
if defined.text and defined.texthl then
|
||||
return {
|
||||
text = make_two_char(defined.text),
|
||||
highlight = defined.texthl,
|
||||
}
|
||||
else
|
||||
return {
|
||||
text = severity:sub(1, 1),
|
||||
highlight = "Diagnostic" .. severity,
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
M.git_status = function(config, node, state)
|
||||
local git_status_lookup = state.git_status_lookup
|
||||
if config.hide_when_expanded and node.type == "directory" and node:is_expanded() then
|
||||
return {}
|
||||
end
|
||||
if not git_status_lookup then
|
||||
return {}
|
||||
end
|
||||
local git_status = git_status_lookup[node.path]
|
||||
if not git_status then
|
||||
if node.filtered_by and node.filtered_by.gitignored then
|
||||
git_status = "!!"
|
||||
else
|
||||
return {}
|
||||
end
|
||||
end
|
||||
|
||||
local symbols = config.symbols or {}
|
||||
local change_symbol
|
||||
local change_highlt = highlights.FILE_NAME
|
||||
local status_symbol = symbols.staged
|
||||
local status_highlt = highlights.GIT_STAGED
|
||||
if node.type == "directory" and git_status:len() == 1 then
|
||||
status_symbol = nil
|
||||
end
|
||||
|
||||
if git_status:sub(1, 1) == " " then
|
||||
status_symbol = symbols.unstaged
|
||||
status_highlt = highlights.GIT_UNSTAGED
|
||||
end
|
||||
|
||||
if git_status:match("?$") then
|
||||
status_symbol = nil
|
||||
status_highlt = highlights.GIT_UNTRACKED
|
||||
change_symbol = symbols.untracked
|
||||
change_highlt = highlights.GIT_UNTRACKED
|
||||
-- all variations of merge conflicts
|
||||
elseif git_status == "DD" then
|
||||
status_symbol = symbols.conflict
|
||||
status_highlt = highlights.GIT_CONFLICT
|
||||
change_symbol = symbols.deleted
|
||||
change_highlt = highlights.GIT_CONFLICT
|
||||
elseif git_status == "UU" then
|
||||
status_symbol = symbols.conflict
|
||||
status_highlt = highlights.GIT_CONFLICT
|
||||
change_symbol = symbols.modified
|
||||
change_highlt = highlights.GIT_CONFLICT
|
||||
elseif git_status == "AA" then
|
||||
status_symbol = symbols.conflict
|
||||
status_highlt = highlights.GIT_CONFLICT
|
||||
change_symbol = symbols.added
|
||||
change_highlt = highlights.GIT_CONFLICT
|
||||
elseif git_status:match("U") then
|
||||
status_symbol = symbols.conflict
|
||||
status_highlt = highlights.GIT_CONFLICT
|
||||
if git_status:match("A") then
|
||||
change_symbol = symbols.added
|
||||
elseif git_status:match("D") then
|
||||
change_symbol = symbols.deleted
|
||||
end
|
||||
change_highlt = highlights.GIT_CONFLICT
|
||||
-- end merge conflict section
|
||||
elseif git_status:match("M") then
|
||||
change_symbol = symbols.modified
|
||||
change_highlt = highlights.GIT_MODIFIED
|
||||
elseif git_status:match("R") then
|
||||
change_symbol = symbols.renamed
|
||||
change_highlt = highlights.GIT_RENAMED
|
||||
elseif git_status:match("[ACT]") then
|
||||
change_symbol = symbols.added
|
||||
change_highlt = highlights.GIT_ADDED
|
||||
elseif git_status:match("!") then
|
||||
status_symbol = nil
|
||||
change_symbol = symbols.ignored
|
||||
change_highlt = highlights.GIT_IGNORED
|
||||
elseif git_status:match("D") then
|
||||
change_symbol = symbols.deleted
|
||||
change_highlt = highlights.GIT_DELETED
|
||||
end
|
||||
|
||||
if change_symbol or status_symbol then
|
||||
local components = {}
|
||||
if type(change_symbol) == "string" and #change_symbol > 0 then
|
||||
table.insert(components, {
|
||||
text = make_two_char(change_symbol),
|
||||
highlight = change_highlt,
|
||||
})
|
||||
end
|
||||
if type(status_symbol) == "string" and #status_symbol > 0 then
|
||||
table.insert(components, {
|
||||
text = make_two_char(status_symbol),
|
||||
highlight = status_highlt,
|
||||
})
|
||||
end
|
||||
return components
|
||||
else
|
||||
return {
|
||||
text = "[" .. git_status .. "]",
|
||||
highlight = config.highlight or change_highlt,
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
M.filtered_by = function(config, node, state)
|
||||
local result = {}
|
||||
if type(node.filtered_by) == "table" then
|
||||
local fby = node.filtered_by
|
||||
if fby.name then
|
||||
result = {
|
||||
text = "(hide by name)",
|
||||
highlight = highlights.HIDDEN_BY_NAME,
|
||||
}
|
||||
elseif fby.pattern then
|
||||
result = {
|
||||
text = "(hide by pattern)",
|
||||
highlight = highlights.HIDDEN_BY_NAME,
|
||||
}
|
||||
elseif fby.gitignored then
|
||||
result = {
|
||||
text = "(gitignored)",
|
||||
highlight = highlights.GIT_IGNORED,
|
||||
}
|
||||
elseif fby.dotfiles then
|
||||
result = {
|
||||
text = "(dotfile)",
|
||||
highlight = highlights.DOTFILE,
|
||||
}
|
||||
elseif fby.hidden then
|
||||
result = {
|
||||
text = "(hidden)",
|
||||
highlight = highlights.WINDOWS_HIDDEN,
|
||||
}
|
||||
end
|
||||
fby = nil
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
M.icon = function(config, node, state)
|
||||
local icon = config.default or " "
|
||||
local highlight = config.highlight or highlights.FILE_ICON
|
||||
if node.type == "directory" then
|
||||
highlight = highlights.DIRECTORY_ICON
|
||||
if node.loaded and not node:has_children() then
|
||||
icon = not node.empty_expanded and config.folder_empty or config.folder_empty_open
|
||||
elseif node:is_expanded() then
|
||||
icon = config.folder_open or "-"
|
||||
else
|
||||
icon = config.folder_closed or "+"
|
||||
end
|
||||
elseif node.type == "file" or node.type == "terminal" then
|
||||
local success, web_devicons = pcall(require, "nvim-web-devicons")
|
||||
local name = node.type == "terminal" and "terminal" or node.name
|
||||
if success then
|
||||
local devicon, hl = web_devicons.get_icon(name)
|
||||
icon = devicon or icon
|
||||
highlight = hl or highlight
|
||||
end
|
||||
end
|
||||
|
||||
local filtered_by = M.filtered_by(config, node, state)
|
||||
|
||||
return {
|
||||
text = icon .. " ",
|
||||
highlight = filtered_by.highlight or highlight,
|
||||
}
|
||||
end
|
||||
|
||||
M.modified = function(config, node, state)
|
||||
local opened_buffers = state.opened_buffers or {}
|
||||
local buf_info = utils.index_by_path(opened_buffers, node.path)
|
||||
|
||||
if buf_info and buf_info.modified then
|
||||
return {
|
||||
text = (make_two_char(config.symbol) or "[+]"),
|
||||
highlight = config.highlight or highlights.MODIFIED,
|
||||
}
|
||||
else
|
||||
return {}
|
||||
end
|
||||
end
|
||||
|
||||
M.name = function(config, node, state)
|
||||
local highlight = config.highlight or highlights.FILE_NAME
|
||||
local text = node.name
|
||||
if node.type == "directory" then
|
||||
highlight = highlights.DIRECTORY_NAME
|
||||
if config.trailing_slash and text ~= "/" then
|
||||
text = text .. "/"
|
||||
end
|
||||
end
|
||||
|
||||
if node:get_depth() == 1 and node.type ~= "message" then
|
||||
highlight = highlights.ROOT_NAME
|
||||
else
|
||||
local filtered_by = M.filtered_by(config, node, state)
|
||||
highlight = filtered_by.highlight or highlight
|
||||
if config.use_git_status_colors then
|
||||
local git_status = state.components.git_status({}, node, state)
|
||||
if git_status and git_status.highlight then
|
||||
highlight = git_status.highlight
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local hl_opened = config.highlight_opened_files
|
||||
if hl_opened then
|
||||
local opened_buffers = state.opened_buffers or {}
|
||||
if
|
||||
(hl_opened == "all" and opened_buffers[node.path])
|
||||
or (opened_buffers[node.path] and opened_buffers[node.path].loaded)
|
||||
then
|
||||
highlight = highlights.FILE_NAME_OPENED
|
||||
end
|
||||
end
|
||||
|
||||
if type(config.right_padding) == "number" then
|
||||
if config.right_padding > 0 then
|
||||
text = text .. string.rep(" ", config.right_padding)
|
||||
end
|
||||
else
|
||||
text = text
|
||||
end
|
||||
|
||||
return {
|
||||
text = text,
|
||||
highlight = highlight,
|
||||
}
|
||||
end
|
||||
|
||||
M.indent = function(config, node, state)
|
||||
if not state.skip_marker_at_level then
|
||||
state.skip_marker_at_level = {}
|
||||
end
|
||||
|
||||
local strlen = vim.fn.strdisplaywidth
|
||||
local skip_marker = state.skip_marker_at_level
|
||||
local indent_size = config.indent_size or 2
|
||||
local padding = config.padding or 0
|
||||
local level = node.level
|
||||
local with_markers = config.with_markers
|
||||
local with_expanders = config.with_expanders == nil and file_nesting.is_enabled()
|
||||
or config.with_expanders
|
||||
local marker_highlight = config.highlight or highlights.INDENT_MARKER
|
||||
local expander_highlight = config.expander_highlight or config.highlight or highlights.EXPANDER
|
||||
|
||||
local function get_expander()
|
||||
if with_expanders and utils.is_expandable(node) then
|
||||
return node:is_expanded() and (config.expander_expanded or "")
|
||||
or (config.expander_collapsed or "")
|
||||
end
|
||||
end
|
||||
|
||||
if indent_size == 0 or level < 2 or not with_markers then
|
||||
local len = indent_size * level + padding
|
||||
local expander = get_expander()
|
||||
if level == 0 or not expander then
|
||||
return {
|
||||
text = string.rep(" ", len),
|
||||
}
|
||||
end
|
||||
return {
|
||||
text = string.rep(" ", len - strlen(expander) - 1) .. expander .. " ",
|
||||
highlight = expander_highlight,
|
||||
}
|
||||
end
|
||||
|
||||
local indent_marker = config.indent_marker or "│"
|
||||
local last_indent_marker = config.last_indent_marker or "└"
|
||||
|
||||
skip_marker[level] = node.is_last_child
|
||||
local indent = {}
|
||||
if padding > 0 then
|
||||
table.insert(indent, { text = string.rep(" ", padding) })
|
||||
end
|
||||
|
||||
for i = 1, level do
|
||||
local char = ""
|
||||
local spaces_count = indent_size
|
||||
local highlight = nil
|
||||
|
||||
if i > 1 and not skip_marker[i] or i == level then
|
||||
spaces_count = spaces_count - 1
|
||||
char = indent_marker
|
||||
highlight = marker_highlight
|
||||
if i == level then
|
||||
local expander = get_expander()
|
||||
if expander then
|
||||
char = expander
|
||||
highlight = expander_highlight
|
||||
elseif node.is_last_child then
|
||||
char = last_indent_marker
|
||||
spaces_count = spaces_count - (vim.api.nvim_strwidth(last_indent_marker) - 1)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
table.insert(indent, {
|
||||
text = char .. string.rep(" ", spaces_count),
|
||||
highlight = highlight,
|
||||
no_next_padding = true,
|
||||
})
|
||||
end
|
||||
|
||||
return indent
|
||||
end
|
||||
|
||||
local get_header = function (state, label, size)
|
||||
if state.sort and state.sort.label == label then
|
||||
local icon = state.sort.direction == 1 and "▲" or "▼"
|
||||
size = size - 2
|
||||
return string.format("%" .. size .. "s %s ", label, icon)
|
||||
end
|
||||
return string.format("%" .. size .. "s ", label)
|
||||
end
|
||||
|
||||
M.file_size = function (config, node, state)
|
||||
-- Root node gets column labels
|
||||
if node:get_depth() == 1 then
|
||||
return {
|
||||
text = get_header(state, "Size", 12),
|
||||
highlight = highlights.FILE_STATS_HEADER
|
||||
}
|
||||
end
|
||||
|
||||
local text = "-"
|
||||
if node.type == "file" then
|
||||
local stat = utils.get_stat(node)
|
||||
local size = stat and stat.size or nil
|
||||
if size then
|
||||
local success, human = pcall(utils.human_size, size)
|
||||
if success then
|
||||
text = human or text
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return {
|
||||
text = string.format("%12s ", text),
|
||||
highlight = config.highlight or highlights.FILE_STATS
|
||||
}
|
||||
end
|
||||
|
||||
local file_time = function(config, node, state, stat_field)
|
||||
-- Root node gets column labels
|
||||
if node:get_depth() == 1 then
|
||||
local label = stat_field
|
||||
if stat_field == "mtime" then
|
||||
label = "Last Modified"
|
||||
elseif stat_field == "birthtime" then
|
||||
label = "Created"
|
||||
end
|
||||
return {
|
||||
text = get_header(state, label, 20),
|
||||
highlight = highlights.FILE_STATS_HEADER
|
||||
}
|
||||
end
|
||||
|
||||
local stat = utils.get_stat(node)
|
||||
local value = stat and stat[stat_field]
|
||||
local seconds = value and value.sec or nil
|
||||
local display = seconds and os.date("%Y-%m-%d %I:%M %p", seconds) or "-"
|
||||
return {
|
||||
text = string.format("%20s ", display),
|
||||
highlight = config.highlight or highlights.FILE_STATS
|
||||
}
|
||||
end
|
||||
|
||||
M.last_modified = function(config, node, state)
|
||||
return file_time(config, node, state, "mtime")
|
||||
end
|
||||
|
||||
M.created = function(config, node, state)
|
||||
return file_time(config, node, state, "birthtime")
|
||||
end
|
||||
|
||||
M.symlink_target = function(config, node, state)
|
||||
if node.is_link then
|
||||
return {
|
||||
text = string.format(" ➛ %s", node.link_to),
|
||||
highlight = config.highlight or highlights.SYMBOLIC_LINK_TARGET,
|
||||
}
|
||||
else
|
||||
return {}
|
||||
end
|
||||
end
|
||||
|
||||
M.type = function (config, node, state)
|
||||
local text = node.ext or node.type
|
||||
-- Root node gets column labels
|
||||
if node:get_depth() == 1 then
|
||||
return {
|
||||
text = get_header(state, "Type", 10),
|
||||
highlight = highlights.FILE_STATS_HEADER
|
||||
}
|
||||
end
|
||||
|
||||
return {
|
||||
text = string.format("%10s ", text),
|
||||
highlight = highlights.FILE_STATS
|
||||
}
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,340 @@
|
||||
local utils = require("neo-tree.utils")
|
||||
local renderer = require("neo-tree.ui.renderer")
|
||||
local highlights = require("neo-tree.ui.highlights")
|
||||
local log = require("neo-tree.log")
|
||||
|
||||
local M = {}
|
||||
|
||||
local calc_rendered_width = function(rendered_item)
|
||||
local width = 0
|
||||
|
||||
for _, item in ipairs(rendered_item) do
|
||||
if item.text then
|
||||
width = width + vim.fn.strchars(item.text)
|
||||
end
|
||||
end
|
||||
|
||||
return width
|
||||
end
|
||||
|
||||
local calc_container_width = function(config, node, state, context)
|
||||
local container_width = 0
|
||||
if type(config.width) == "string" then
|
||||
if config.width == "fit_content" then
|
||||
container_width = context.max_width
|
||||
elseif config.width == "100%" then
|
||||
container_width = context.available_width
|
||||
elseif config.width:match("^%d+%%$") then
|
||||
local percent = tonumber(config.width:sub(1, -2)) / 100
|
||||
container_width = math.floor(percent * context.available_width)
|
||||
else
|
||||
error("Invalid container width: " .. config.width)
|
||||
end
|
||||
elseif type(config.width) == "number" then
|
||||
container_width = config.width
|
||||
elseif type(config.width) == "function" then
|
||||
container_width = config.width(node, state)
|
||||
else
|
||||
error("Invalid container width: " .. config.width)
|
||||
end
|
||||
|
||||
if config.min_width then
|
||||
container_width = math.max(container_width, config.min_width)
|
||||
end
|
||||
if config.max_width then
|
||||
container_width = math.min(container_width, config.max_width)
|
||||
end
|
||||
context.container_width = container_width
|
||||
return container_width
|
||||
end
|
||||
|
||||
local render_content = function(config, node, state, context)
|
||||
local window_width = vim.api.nvim_win_get_width(state.winid)
|
||||
local add_padding = function(rendered_item, should_pad)
|
||||
for _, data in ipairs(rendered_item) do
|
||||
if data.text then
|
||||
local padding = (should_pad and #data.text and data.text:sub(1, 1) ~= " ") and " " or ""
|
||||
data.text = padding .. data.text
|
||||
should_pad = data.text:sub(#data.text) ~= " "
|
||||
end
|
||||
end
|
||||
return should_pad
|
||||
end
|
||||
|
||||
local max_width = 0
|
||||
local grouped_by_zindex = utils.group_by(config.content, "zindex")
|
||||
|
||||
for zindex, items in pairs(grouped_by_zindex) do
|
||||
local should_pad = { left = false, right = false }
|
||||
local zindex_rendered = { left = {}, right = {} }
|
||||
local rendered_width = 0
|
||||
|
||||
for _, item in ipairs(items) do
|
||||
if item.enabled == false then
|
||||
goto continue
|
||||
end
|
||||
local required_width = item.required_width or 0
|
||||
if required_width > window_width then
|
||||
goto continue
|
||||
end
|
||||
local rendered_item = renderer.render_component(item, node, state, context.available_width)
|
||||
if rendered_item then
|
||||
local align = item.align or "left"
|
||||
should_pad[align] = add_padding(rendered_item, should_pad[align])
|
||||
|
||||
vim.list_extend(zindex_rendered[align], rendered_item)
|
||||
rendered_width = rendered_width + calc_rendered_width(rendered_item)
|
||||
end
|
||||
::continue::
|
||||
end
|
||||
|
||||
max_width = math.max(max_width, rendered_width)
|
||||
grouped_by_zindex[zindex] = zindex_rendered
|
||||
end
|
||||
|
||||
context.max_width = max_width
|
||||
context.grouped_by_zindex = grouped_by_zindex
|
||||
return context
|
||||
end
|
||||
|
||||
---Takes a list of rendered components and truncates them to fit the container width
|
||||
---@param layer table The list of rendered components.
|
||||
---@param skip_count number The number of characters to skip from the begining/left.
|
||||
---@param max_length number The maximum number of characters to return.
|
||||
local truncate_layer_keep_left = function(layer, skip_count, max_length)
|
||||
local result = {}
|
||||
local taken = 0
|
||||
local skipped = 0
|
||||
for _, item in ipairs(layer) do
|
||||
local remaining_to_skip = skip_count - skipped
|
||||
if remaining_to_skip > 0 then
|
||||
if #item.text <= remaining_to_skip then
|
||||
skipped = skipped + vim.fn.strchars(item.text)
|
||||
item.text = ""
|
||||
else
|
||||
item.text = item.text:sub(remaining_to_skip)
|
||||
if #item.text + taken > max_length then
|
||||
item.text = item.text:sub(1, max_length - taken)
|
||||
end
|
||||
table.insert(result, item)
|
||||
taken = taken + #item.text
|
||||
skipped = skipped + remaining_to_skip
|
||||
end
|
||||
elseif taken <= max_length then
|
||||
if #item.text + taken > max_length then
|
||||
item.text = item.text:sub(1, max_length - taken)
|
||||
end
|
||||
table.insert(result, item)
|
||||
taken = taken + vim.fn.strchars(item.text)
|
||||
end
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
---Takes a list of rendered components and truncates them to fit the container width
|
||||
---@param layer table The list of rendered components.
|
||||
---@param skip_count number The number of characters to skip from the end/right.
|
||||
---@param max_length number The maximum number of characters to return.
|
||||
local truncate_layer_keep_right = function(layer, skip_count, max_length)
|
||||
local result = {}
|
||||
local taken = 0
|
||||
local skipped = 0
|
||||
local i = #layer
|
||||
while i > 0 do
|
||||
local item = layer[i]
|
||||
i = i - 1
|
||||
local text_length = vim.fn.strchars(item.text)
|
||||
local remaining_to_skip = skip_count - skipped
|
||||
if remaining_to_skip > 0 then
|
||||
if text_length <= remaining_to_skip then
|
||||
skipped = skipped + text_length
|
||||
item.text = ""
|
||||
else
|
||||
item.text = vim.fn.strcharpart(item.text, 0, text_length - remaining_to_skip)
|
||||
text_length = vim.fn.strchars(item.text)
|
||||
if text_length + taken > max_length then
|
||||
item.text = vim.fn.strcharpart(item.text, text_length - (max_length - taken))
|
||||
text_length = vim.fn.strchars(item.text)
|
||||
end
|
||||
table.insert(result, item)
|
||||
taken = taken + text_length
|
||||
skipped = skipped + remaining_to_skip
|
||||
end
|
||||
elseif taken <= max_length then
|
||||
if text_length + taken > max_length then
|
||||
item.text = vim.fn.strcharpart(item.text, text_length - (max_length - taken))
|
||||
text_length = vim.fn.strchars(item.text)
|
||||
end
|
||||
table.insert(result, item)
|
||||
taken = taken + text_length
|
||||
end
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
local fade_content = function(layer, fade_char_count)
|
||||
local text = layer[#layer].text
|
||||
if not text or #text == 0 then
|
||||
return
|
||||
end
|
||||
local hl = layer[#layer].highlight or "Normal"
|
||||
local fade = {
|
||||
highlights.get_faded_highlight_group(hl, 0.68),
|
||||
highlights.get_faded_highlight_group(hl, 0.6),
|
||||
highlights.get_faded_highlight_group(hl, 0.35),
|
||||
}
|
||||
|
||||
for i = 3, 1, -1 do
|
||||
if #text >= i and fade_char_count >= i then
|
||||
layer[#layer].text = text:sub(1, -i - 1)
|
||||
for j = i, 1, -1 do
|
||||
-- force no padding for each faded character
|
||||
local entry = { text = text:sub(-j, -j), highlight = fade[i - j + 1], no_padding = true }
|
||||
table.insert(layer, entry)
|
||||
end
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local try_fade_content = function(layer, fade_char_count)
|
||||
local success, err = pcall(fade_content, layer, fade_char_count)
|
||||
if not success then
|
||||
log.debug("Error while trying to fade content: ", err)
|
||||
end
|
||||
end
|
||||
|
||||
local merge_content = function(context)
|
||||
-- Heres the idea:
|
||||
-- * Starting backwards from the layer with the highest zindex
|
||||
-- set the left and right tables to the content of the layer
|
||||
-- * If a layer has more content than will fit, the left side will be truncated.
|
||||
-- * If the available space is not used up, move on to the next layer
|
||||
-- * With each subsequent layer, if the length of that layer is greater then the existing
|
||||
-- length for that side (left or right), then clip that layer and append whatver portion is
|
||||
-- not covered up to the appropriate side.
|
||||
-- * Check again to see if we have used up the available width, short circuit if we have.
|
||||
-- * Repeat until all layers have been merged.
|
||||
-- * Join the left and right tables together and return.
|
||||
--
|
||||
local remaining_width = context.container_width
|
||||
local left, right = {}, {}
|
||||
local left_width, right_width = 0, 0
|
||||
local wanted_width = 0
|
||||
|
||||
if context.left_padding and context.left_padding > 0 then
|
||||
table.insert(left, { text = string.rep(" ", context.left_padding) })
|
||||
remaining_width = remaining_width - context.left_padding
|
||||
left_width = left_width + context.left_padding
|
||||
wanted_width = wanted_width + context.left_padding
|
||||
end
|
||||
|
||||
if context.right_padding and context.right_padding > 0 then
|
||||
remaining_width = remaining_width - context.right_padding
|
||||
wanted_width = wanted_width + context.right_padding
|
||||
end
|
||||
|
||||
local keys = utils.get_keys(context.grouped_by_zindex, true)
|
||||
if type(keys) ~= "table" then
|
||||
return {}
|
||||
end
|
||||
local i = #keys
|
||||
while i > 0 do
|
||||
local key = keys[i]
|
||||
local layer = context.grouped_by_zindex[key]
|
||||
i = i - 1
|
||||
|
||||
if utils.truthy(layer.right) then
|
||||
local width = calc_rendered_width(layer.right)
|
||||
wanted_width = wanted_width + width
|
||||
if remaining_width > 0 then
|
||||
context.has_right_content = true
|
||||
if width > remaining_width then
|
||||
local truncated = truncate_layer_keep_right(layer.right, right_width, remaining_width)
|
||||
vim.list_extend(right, truncated)
|
||||
remaining_width = 0
|
||||
else
|
||||
remaining_width = remaining_width - width
|
||||
vim.list_extend(right, layer.right)
|
||||
right_width = right_width + width
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if utils.truthy(layer.left) then
|
||||
local width = calc_rendered_width(layer.left)
|
||||
wanted_width = wanted_width + width
|
||||
if remaining_width > 0 then
|
||||
if width > remaining_width then
|
||||
local truncated = truncate_layer_keep_left(layer.left, left_width, remaining_width)
|
||||
if context.enable_character_fade then
|
||||
try_fade_content(truncated, 3)
|
||||
end
|
||||
vim.list_extend(left, truncated)
|
||||
remaining_width = 0
|
||||
else
|
||||
remaining_width = remaining_width - width
|
||||
if context.enable_character_fade and not context.auto_expand_width then
|
||||
local fade_chars = 3 - remaining_width
|
||||
if fade_chars > 0 then
|
||||
try_fade_content(layer.left, fade_chars)
|
||||
end
|
||||
end
|
||||
vim.list_extend(left, layer.left)
|
||||
left_width = left_width + width
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if remaining_width == 0 and not context.auto_expand_width then
|
||||
i = 0
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
if remaining_width > 0 and #right > 0 then
|
||||
table.insert(left, { text = string.rep(" ", remaining_width) })
|
||||
end
|
||||
|
||||
local result = {}
|
||||
vim.list_extend(result, left)
|
||||
|
||||
-- we do not pad between left and right side
|
||||
if #right >= 1 then
|
||||
right[1].no_padding = true
|
||||
end
|
||||
|
||||
vim.list_extend(result, right)
|
||||
context.merged_content = result
|
||||
log.trace("wanted width: ", wanted_width, " actual width: ", context.container_width)
|
||||
context.wanted_width = math.max(wanted_width, context.wanted_width)
|
||||
end
|
||||
|
||||
M.render = function(config, node, state, available_width)
|
||||
local context = {
|
||||
wanted_width = 0,
|
||||
max_width = 0,
|
||||
grouped_by_zindex = {},
|
||||
available_width = available_width,
|
||||
left_padding = config.left_padding,
|
||||
right_padding = config.right_padding,
|
||||
enable_character_fade = config.enable_character_fade,
|
||||
auto_expand_width = state.window.auto_expand_width and state.window.position ~= "float",
|
||||
}
|
||||
|
||||
render_content(config, node, state, context)
|
||||
calc_container_width(config, node, state, context)
|
||||
merge_content(context)
|
||||
|
||||
if context.has_right_content then
|
||||
state.has_right_content = true
|
||||
end
|
||||
|
||||
-- we still want padding between this container and the previous component
|
||||
if #context.merged_content > 0 then
|
||||
context.merged_content[1].no_padding = false
|
||||
end
|
||||
return context.merged_content, context.wanted_width
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,252 @@
|
||||
local vim = vim
|
||||
local file_nesting = require("neo-tree.sources.common.file-nesting")
|
||||
local utils = require("neo-tree.utils")
|
||||
local log = require("neo-tree.log")
|
||||
|
||||
local function sort_items(a, b)
|
||||
if a.type == b.type then
|
||||
return a.path < b.path
|
||||
else
|
||||
return a.type < b.type
|
||||
end
|
||||
end
|
||||
|
||||
local function sort_items_case_insensitive(a, b)
|
||||
if a.type == b.type then
|
||||
return a.path:lower() < b.path:lower()
|
||||
else
|
||||
return a.type < b.type
|
||||
end
|
||||
end
|
||||
|
||||
---Creates a sort function the will sort by the values returned by the field provider.
|
||||
---@param field_provider function a function that takes an item and returns a value to
|
||||
-- sort by.
|
||||
---@param fallback_sort_function function a sort function to use if the field provider
|
||||
-- returns the same value for both items.
|
||||
local function make_sort_function(field_provider, fallback_sort_function, direction)
|
||||
return function(a, b)
|
||||
if a.type == b.type then
|
||||
local a_field = field_provider(a)
|
||||
local b_field = field_provider(b)
|
||||
if a_field == b_field then
|
||||
return fallback_sort_function(a, b)
|
||||
else
|
||||
if direction < 0 then
|
||||
return a_field > b_field
|
||||
else
|
||||
return a_field < b_field
|
||||
end
|
||||
end
|
||||
else
|
||||
return a.type < b.type
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function sort_function_is_valid(func)
|
||||
if func == nil then
|
||||
return false
|
||||
end
|
||||
|
||||
local a = { type = "dir", path = "foo" }
|
||||
local b = { type = "dir", path = "baz" }
|
||||
|
||||
local success, result = pcall(func, a, b)
|
||||
if success and type(result) == "boolean" then
|
||||
return true
|
||||
end
|
||||
|
||||
log.error("sort function isn't valid ", result)
|
||||
return false
|
||||
end
|
||||
|
||||
local function deep_sort(tbl, sort_func, field_provider, direction)
|
||||
if sort_func == nil then
|
||||
local config = require("neo-tree").config
|
||||
if sort_function_is_valid(config.sort_function) then
|
||||
sort_func = config.sort_function
|
||||
elseif config.sort_case_insensitive then
|
||||
sort_func = sort_items_case_insensitive
|
||||
else
|
||||
sort_func = sort_items
|
||||
end
|
||||
if field_provider ~= nil then
|
||||
sort_func = make_sort_function(field_provider, sort_func, direction)
|
||||
end
|
||||
end
|
||||
table.sort(tbl, sort_func)
|
||||
for _, item in pairs(tbl) do
|
||||
if item.type == "directory" or item.children ~= nil then
|
||||
deep_sort(item.children, sort_func)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local advanced_sort = function(tbl, state)
|
||||
local sort_func = state.sort_function_override
|
||||
local field_provider = state.sort_field_provider
|
||||
local direction = state.sort and state.sort.direction or 1
|
||||
deep_sort(tbl, sort_func, field_provider, direction)
|
||||
end
|
||||
|
||||
local create_item, set_parents
|
||||
|
||||
function create_item(context, path, _type, bufnr)
|
||||
local parent_path, name = utils.split_path(path)
|
||||
local id = path
|
||||
if path == "[No Name]" and bufnr then
|
||||
parent_path = context.state.path
|
||||
name = "[No Name]"
|
||||
id = tostring(bufnr)
|
||||
else
|
||||
-- avoid creating duplicate items
|
||||
if context.folders[path] or context.nesting[path] or context.item_exists[path] then
|
||||
return context.folders[path] or context.nesting[path] or context.item_exists[path]
|
||||
end
|
||||
end
|
||||
|
||||
if _type == nil then
|
||||
local stat = vim.loop.fs_stat(path)
|
||||
_type = stat and stat.type or "unknown"
|
||||
end
|
||||
local item = {
|
||||
id = id,
|
||||
name = name,
|
||||
parent_path = parent_path,
|
||||
path = path,
|
||||
type = _type,
|
||||
}
|
||||
if utils.is_windows then
|
||||
if vim.fn.getftype(path) == "link" then
|
||||
item.type = "link"
|
||||
end
|
||||
end
|
||||
if item.type == "link" then
|
||||
item.is_link = true
|
||||
item.link_to = vim.loop.fs_realpath(path)
|
||||
if item.link_to ~= nil then
|
||||
item.type = vim.loop.fs_stat(item.link_to).type
|
||||
end
|
||||
end
|
||||
if item.type == "directory" then
|
||||
item.children = {}
|
||||
item.loaded = false
|
||||
context.folders[path] = item
|
||||
if context.state.search_pattern then
|
||||
table.insert(context.state.default_expanded_nodes, item.id)
|
||||
end
|
||||
else
|
||||
item.base = item.name:match("^([-_,()%s%w%i]+)%.")
|
||||
item.ext = item.name:match("%.([-_,()%s%w%i]+)$")
|
||||
item.exts = item.name:match("^[-_,()%s%w%i]+%.(.*)")
|
||||
item.name_lcase = item.name:lower()
|
||||
|
||||
local nesting_callback = file_nesting.get_nesting_callback(item)
|
||||
if nesting_callback ~= nil then
|
||||
item.children = {}
|
||||
item.nesting_callback = nesting_callback
|
||||
context.nesting[path] = item
|
||||
end
|
||||
end
|
||||
|
||||
item.is_reveal_target = (path == context.path_to_reveal)
|
||||
local state = context.state
|
||||
local f = state.filtered_items
|
||||
local is_not_root = not utils.is_subpath(path, context.state.path)
|
||||
if f and is_not_root then
|
||||
if f.never_show[name] then
|
||||
item.filtered_by = item.filtered_by or {}
|
||||
item.filtered_by.never_show = true
|
||||
else
|
||||
if utils.is_filtered_by_pattern(f.never_show_by_pattern, path, name) then
|
||||
item.filtered_by = item.filtered_by or {}
|
||||
item.filtered_by.never_show = true
|
||||
end
|
||||
end
|
||||
if f.always_show[name] then
|
||||
item.filtered_by = item.filtered_by or {}
|
||||
item.filtered_by.always_show = true
|
||||
end
|
||||
if f.hide_by_name[name] then
|
||||
item.filtered_by = item.filtered_by or {}
|
||||
item.filtered_by.name = true
|
||||
end
|
||||
if utils.is_filtered_by_pattern(f.hide_by_pattern, path, name) then
|
||||
item.filtered_by = item.filtered_by or {}
|
||||
item.filtered_by.pattern = true
|
||||
end
|
||||
if f.hide_dotfiles and string.sub(name, 1, 1) == "." then
|
||||
item.filtered_by = item.filtered_by or {}
|
||||
item.filtered_by.dotfiles = true
|
||||
end
|
||||
if f.hide_hidden and utils.is_hidden(path) then
|
||||
item.filtered_by = item.filtered_by or {}
|
||||
item.filtered_by.hidden = true
|
||||
end
|
||||
-- NOTE: git_ignored logic moved to job_complete
|
||||
end
|
||||
|
||||
set_parents(context, item)
|
||||
if context.all_items == nil then
|
||||
context.all_items = {}
|
||||
end
|
||||
if is_not_root then
|
||||
table.insert(context.all_items, item)
|
||||
end
|
||||
return item
|
||||
end
|
||||
|
||||
-- function to set (or create) parent folder
|
||||
function set_parents(context, item, siblings)
|
||||
-- we can get duplicate items if we navigate up with open folders
|
||||
-- this is probably hacky, but it works
|
||||
if context.item_exists[item.id] then
|
||||
return
|
||||
end
|
||||
if not item.parent_path then
|
||||
return
|
||||
end
|
||||
|
||||
local parent = context.folders[item.parent_path]
|
||||
if not utils.truthy(item.parent_path) then
|
||||
return
|
||||
end
|
||||
if parent == nil then
|
||||
local success
|
||||
success, parent = pcall(create_item, context, item.parent_path, "directory")
|
||||
if not success then
|
||||
log.error("error creating item for ", item.parent_path)
|
||||
end
|
||||
context.folders[parent.id] = parent
|
||||
set_parents(context, parent)
|
||||
end
|
||||
table.insert(parent.children, item)
|
||||
context.item_exists[item.id] = true
|
||||
|
||||
if item.filtered_by == nil and type(parent.filtered_by) == "table" then
|
||||
item.filtered_by = vim.deepcopy(parent.filtered_by)
|
||||
end
|
||||
end
|
||||
|
||||
---Create context to be used in other file-items functions.
|
||||
---@param state table|nil The state of the file-items.
|
||||
---@return table
|
||||
local create_context = function(state)
|
||||
local context = {}
|
||||
-- Make the context a weak table so that it can be garbage collected
|
||||
--setmetatable(context, { __mode = 'v' })
|
||||
context.state = state
|
||||
context.folders = {}
|
||||
context.nesting = {}
|
||||
context.item_exists = {}
|
||||
context.all_items = {}
|
||||
return context
|
||||
end
|
||||
|
||||
return {
|
||||
create_context = create_context,
|
||||
create_item = create_item,
|
||||
deep_sort = deep_sort,
|
||||
advanced_sort = advanced_sort,
|
||||
}
|
||||
@ -0,0 +1,248 @@
|
||||
local utils = require("neo-tree.utils")
|
||||
local Path = require("plenary.path")
|
||||
local globtopattern = require("neo-tree.sources.filesystem.lib.globtopattern")
|
||||
local log = require("neo-tree.log")
|
||||
|
||||
-- File nesting a la JetBrains (#117).
|
||||
local M = {}
|
||||
|
||||
local pattern_matcher = {
|
||||
enabled = false,
|
||||
config = {},
|
||||
}
|
||||
|
||||
local extension_matcher = {
|
||||
enabled = false,
|
||||
config = {},
|
||||
}
|
||||
|
||||
local matchers = {}
|
||||
matchers.pattern = pattern_matcher
|
||||
matchers.exts = extension_matcher
|
||||
|
||||
extension_matcher.get_nesting_callback = function(item)
|
||||
if utils.truthy(extension_matcher.config[item.exts]) then
|
||||
return extension_matcher.get_children
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
extension_matcher.get_children = function(item, siblings)
|
||||
local matching_files = {}
|
||||
if siblings == nil then
|
||||
return matching_files
|
||||
end
|
||||
for _, ext in pairs(extension_matcher.config[item.exts]) do
|
||||
for _, sibling in pairs(siblings) do
|
||||
if
|
||||
sibling.id ~= item.id
|
||||
and sibling.is_nested ~= true
|
||||
and item.parent_path == sibling.parent_path
|
||||
and sibling.exts == ext
|
||||
and item.base .. "." .. ext == sibling.name
|
||||
then
|
||||
table.insert(matching_files, sibling)
|
||||
end
|
||||
end
|
||||
end
|
||||
return matching_files
|
||||
end
|
||||
|
||||
pattern_matcher.get_nesting_callback = function(item)
|
||||
for _, rule_config in pairs(pattern_matcher.config) do
|
||||
if item.name:match(rule_config["pattern"]) then
|
||||
return function(inner_item, siblings)
|
||||
local rule_config_helper = rule_config
|
||||
return pattern_matcher.get_children(inner_item, siblings, rule_config_helper)
|
||||
end
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
pattern_matcher.pattern_types = {}
|
||||
pattern_matcher.pattern_types.files_glob = {}
|
||||
pattern_matcher.pattern_types.files_glob.get_pattern = function(pattern)
|
||||
return globtopattern.globtopattern(pattern)
|
||||
end
|
||||
pattern_matcher.pattern_types.files_glob.match = function(filename, pattern)
|
||||
return filename:match(pattern)
|
||||
end
|
||||
pattern_matcher.pattern_types.files_exact = {}
|
||||
pattern_matcher.pattern_types.files_exact.get_pattern = function(pattern)
|
||||
return pattern
|
||||
end
|
||||
pattern_matcher.pattern_types.files_exact.match = function(filename, pattern)
|
||||
return filename == pattern
|
||||
end
|
||||
|
||||
pattern_matcher.get_children = function(item, siblings, rule_config)
|
||||
local matching_files = {}
|
||||
if siblings == nil then
|
||||
return matching_files
|
||||
end
|
||||
for type, type_functions in pairs(pattern_matcher.pattern_types) do
|
||||
for _, pattern in pairs(rule_config[type]) do
|
||||
local item_name = item.name
|
||||
if rule_config["ignore_case"] ~= nil and item.name_lcase ~= nil then
|
||||
item_name = item.name_lcase
|
||||
end
|
||||
local success, replaced_pattern =
|
||||
pcall(string.gsub, item_name, rule_config["pattern"], pattern)
|
||||
if success then
|
||||
local glob_or_file = type_functions.get_pattern(replaced_pattern)
|
||||
for _, sibling in pairs(siblings) do
|
||||
if
|
||||
sibling.id ~= item.id
|
||||
and sibling.is_nested ~= true
|
||||
and item.parent_path == sibling.parent_path
|
||||
then
|
||||
local sibling_name = sibling.name
|
||||
if rule_config["ignore_case"] ~= nil and sibling.name_lcase ~= nil then
|
||||
sibling_name = sibling.name_lcase
|
||||
end
|
||||
if type_functions.match(sibling_name, glob_or_file) then
|
||||
table.insert(matching_files, sibling)
|
||||
end
|
||||
end
|
||||
end
|
||||
else
|
||||
log.error("Error using file glob '" .. pattern .. "'; Error: " .. replaced_pattern)
|
||||
end
|
||||
end
|
||||
end
|
||||
return matching_files
|
||||
end
|
||||
|
||||
--- Checks if file-nesting module is enabled by config
|
||||
---@return boolean
|
||||
function M.is_enabled()
|
||||
for _, matcher in pairs(matchers) do
|
||||
if matcher.enabled then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
local function is_glob(str)
|
||||
local test = str:gsub("\\[%*%?%[%]]", "")
|
||||
local pos, _ = test:find("*")
|
||||
if pos ~= nil then
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
local function case_insensitive_pattern(pattern)
|
||||
-- find an optional '%' (group 1) followed by any character (group 2)
|
||||
local p = pattern:gsub("(%%?)(.)", function(percent, letter)
|
||||
if percent ~= "" or not letter:match("%a") then
|
||||
-- if the '%' matched, or `letter` is not a letter, return "as is"
|
||||
return percent .. letter
|
||||
else
|
||||
-- else, return a case-insensitive character class of the matched letter
|
||||
return string.format("[%s%s]", letter:lower(), letter:upper())
|
||||
end
|
||||
end)
|
||||
|
||||
return p
|
||||
end
|
||||
|
||||
function table_is_empty(table_to_check)
|
||||
return table_to_check == nil or next(table_to_check) == nil
|
||||
end
|
||||
|
||||
function flatten_nesting(nesting_parents)
|
||||
for key, config in pairs(nesting_parents) do
|
||||
if config.is_nested ~= nil then
|
||||
local parent = config.nesting_parent
|
||||
-- count for emergency escape
|
||||
local count = 0
|
||||
while parent.nesting_parent ~= nil and count < 100 do
|
||||
parent = parent.nesting_parent
|
||||
count = count + 1
|
||||
end
|
||||
if parent ~= nil then
|
||||
for _, child in pairs(config.children) do
|
||||
child.nesting_parent = parent
|
||||
table.insert(parent.children, child)
|
||||
end
|
||||
config.children = nil
|
||||
end
|
||||
end
|
||||
nesting_parents[key] = nil
|
||||
end
|
||||
end
|
||||
|
||||
function M.nest_items(context)
|
||||
if M.is_enabled() == false or table_is_empty(context.nesting) then
|
||||
return
|
||||
end
|
||||
for _, config in pairs(context.nesting) do
|
||||
local files = config.nesting_callback(config, context.all_items)
|
||||
local folder = context.folders[config.parent_path]
|
||||
for _, to_be_nested in ipairs(files) do
|
||||
table.insert(config.children, to_be_nested)
|
||||
to_be_nested.is_nested = true
|
||||
to_be_nested.nesting_parent = config
|
||||
if folder ~= nil then
|
||||
for index, file_to_check in ipairs(folder.children) do
|
||||
if file_to_check.id == to_be_nested.id then
|
||||
table.remove(folder.children, index)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
flatten_nesting(context.nesting)
|
||||
end
|
||||
|
||||
function M.get_nesting_callback(item)
|
||||
for _, matcher in pairs(matchers) do
|
||||
if matcher.enabled then
|
||||
local callback = matcher.get_nesting_callback(item)
|
||||
if callback ~= nil then
|
||||
return callback
|
||||
end
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
---Setup the module with the given config
|
||||
---@param config table
|
||||
function M.setup(config)
|
||||
for key, value in pairs(config or {}) do
|
||||
local type = "exts"
|
||||
if value["pattern"] ~= nil then
|
||||
type = "pattern"
|
||||
if value["ignore_case"] == true then
|
||||
value["pattern"] = case_insensitive_pattern(value["pattern"])
|
||||
end
|
||||
value["files_glob"] = {}
|
||||
value["files_exact"] = {}
|
||||
for _, glob in pairs(value["files"]) do
|
||||
if value["ignore_case"] == true then
|
||||
glob = glob:lower()
|
||||
end
|
||||
local replaced = glob:gsub("%%%d+", "")
|
||||
if is_glob(replaced) then
|
||||
table.insert(value["files_glob"], glob)
|
||||
else
|
||||
table.insert(value["files_exact"], glob)
|
||||
end
|
||||
end
|
||||
end
|
||||
matchers[type]["config"][key] = value
|
||||
end
|
||||
local next = next
|
||||
for _, value in pairs(matchers) do
|
||||
if next(value.config) ~= nil then
|
||||
value.enabled = true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,246 @@
|
||||
-- The lua implementation of the fzy string matching algorithm
|
||||
-- credits to: https://github.com/swarn/fzy-lua
|
||||
--[[
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2020 Seth Warn
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
--]]
|
||||
-- modified by: @pysan3 (2023)
|
||||
|
||||
local SCORE_GAP_LEADING = -0.005
|
||||
local SCORE_GAP_TRAILING = -0.005
|
||||
local SCORE_GAP_INNER = -0.01
|
||||
local SCORE_MATCH_CONSECUTIVE = 1.0
|
||||
local SCORE_MATCH_SLASH = 0.9
|
||||
local SCORE_MATCH_WORD = 0.8
|
||||
local SCORE_MATCH_CAPITAL = 0.7
|
||||
local SCORE_MATCH_DOT = 0.6
|
||||
local SCORE_MAX = math.huge
|
||||
local SCORE_MIN = -math.huge
|
||||
local MATCH_MAX_LENGTH = 1024
|
||||
|
||||
local M = {}
|
||||
|
||||
-- Return `true` if `needle` is a subsequence of `haystack`.
|
||||
function M.has_match(needle, haystack, case_sensitive)
|
||||
if not case_sensitive then
|
||||
needle = string.lower(needle)
|
||||
haystack = string.lower(haystack)
|
||||
end
|
||||
|
||||
local j = 1
|
||||
for i = 1, string.len(needle) do
|
||||
j = string.find(haystack, needle:sub(i, i), j, true)
|
||||
if not j then
|
||||
return false
|
||||
else
|
||||
j = j + 1
|
||||
end
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
local function is_lower(c)
|
||||
return c:match('%l')
|
||||
end
|
||||
|
||||
local function is_upper(c)
|
||||
return c:match('%u')
|
||||
end
|
||||
|
||||
local function precompute_bonus(haystack)
|
||||
local match_bonus = {}
|
||||
|
||||
local last_char = '/'
|
||||
for i = 1, string.len(haystack) do
|
||||
local this_char = haystack:sub(i, i)
|
||||
if last_char == '/' or last_char == '\\' then
|
||||
match_bonus[i] = SCORE_MATCH_SLASH
|
||||
elseif last_char == '-' or last_char == '_' or last_char == ' ' then
|
||||
match_bonus[i] = SCORE_MATCH_WORD
|
||||
elseif last_char == '.' then
|
||||
match_bonus[i] = SCORE_MATCH_DOT
|
||||
elseif is_lower(last_char) and is_upper(this_char) then
|
||||
match_bonus[i] = SCORE_MATCH_CAPITAL
|
||||
else
|
||||
match_bonus[i] = 0
|
||||
end
|
||||
|
||||
last_char = this_char
|
||||
end
|
||||
|
||||
return match_bonus
|
||||
end
|
||||
|
||||
local function compute(needle, haystack, D, T, case_sensitive)
|
||||
-- Note that the match bonuses must be computed before the arguments are
|
||||
-- converted to lowercase, since there are bonuses for camelCase.
|
||||
local match_bonus = precompute_bonus(haystack)
|
||||
local n = string.len(needle)
|
||||
local m = string.len(haystack)
|
||||
|
||||
if not case_sensitive then
|
||||
needle = string.lower(needle)
|
||||
haystack = string.lower(haystack)
|
||||
end
|
||||
|
||||
-- Because lua only grants access to chars through substring extraction,
|
||||
-- get all the characters from the haystack once now, to reuse below.
|
||||
local haystack_chars = {}
|
||||
for i = 1, m do
|
||||
haystack_chars[i] = haystack:sub(i, i)
|
||||
end
|
||||
|
||||
for i = 1, n do
|
||||
D[i] = {}
|
||||
T[i] = {}
|
||||
|
||||
local prev_score = SCORE_MIN
|
||||
local gap_score = i == n and SCORE_GAP_TRAILING or SCORE_GAP_INNER
|
||||
local needle_char = needle:sub(i, i)
|
||||
|
||||
for j = 1, m do
|
||||
if needle_char == haystack_chars[j] then
|
||||
local score = SCORE_MIN
|
||||
if i == 1 then
|
||||
score = ((j - 1) * SCORE_GAP_LEADING) + match_bonus[j]
|
||||
elseif j > 1 then
|
||||
local a = T[i - 1][j - 1] + match_bonus[j]
|
||||
local b = D[i - 1][j - 1] + SCORE_MATCH_CONSECUTIVE
|
||||
score = math.max(a, b)
|
||||
end
|
||||
D[i][j] = score
|
||||
prev_score = math.max(score, prev_score + gap_score)
|
||||
T[i][j] = prev_score
|
||||
else
|
||||
D[i][j] = SCORE_MIN
|
||||
prev_score = prev_score + gap_score
|
||||
T[i][j] = prev_score
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Compute a matching score for two strings.
|
||||
--
|
||||
-- Where `needle` is a subsequence of `haystack`, this returns a score
|
||||
-- measuring the quality of their match. Better matches get higher scores.
|
||||
--
|
||||
-- `needle` must be a subsequence of `haystack`, the result is undefined
|
||||
-- otherwise. Call `has_match()` before calling `score`.
|
||||
--
|
||||
-- returns `get_score_min()` where a or b are longer than `get_max_length()`
|
||||
--
|
||||
-- returns `get_score_min()` when a or b are empty strings.
|
||||
--
|
||||
-- returns `get_score_max()` when a and b are the same string.
|
||||
--
|
||||
-- When the return value is not covered by the above rules, it is a number
|
||||
-- in the range (`get_score_floor()`, `get_score_ceiling()`)
|
||||
function M.score(needle, haystack, case_sensitive)
|
||||
local n = string.len(needle)
|
||||
local m = string.len(haystack)
|
||||
|
||||
if n == 0 or m == 0 or m > MATCH_MAX_LENGTH or n > MATCH_MAX_LENGTH then
|
||||
return SCORE_MIN
|
||||
elseif n == m then
|
||||
return SCORE_MAX
|
||||
else
|
||||
local D = {}
|
||||
local T = {}
|
||||
compute(needle, haystack, D, T, case_sensitive)
|
||||
return T[n][m]
|
||||
end
|
||||
end
|
||||
|
||||
-- Find the locations where fzy matched a string.
|
||||
--
|
||||
-- Returns {score, indices}, where indices is an array showing where each
|
||||
-- character of the needle matches the haystack in the best match.
|
||||
function M.score_and_positions(needle, haystack, case_sensitive)
|
||||
local n = string.len(needle)
|
||||
local m = string.len(haystack)
|
||||
|
||||
if n == 0 or m == 0 or m > MATCH_MAX_LENGTH or n > MATCH_MAX_LENGTH then
|
||||
return SCORE_MIN, {}
|
||||
elseif n == m then
|
||||
local consecutive = {}
|
||||
for i = 1, n do
|
||||
consecutive[i] = i
|
||||
end
|
||||
return SCORE_MAX, consecutive
|
||||
end
|
||||
|
||||
local D = {}
|
||||
local T = {}
|
||||
compute(needle, haystack, D, T, case_sensitive)
|
||||
|
||||
local positions = {}
|
||||
local match_required = false
|
||||
local j = m
|
||||
for i = n, 1, -1 do
|
||||
while j >= 1 do
|
||||
if D[i][j] ~= SCORE_MIN and (match_required or D[i][j] == T[i][j]) then
|
||||
match_required = (i ~= 1) and (j ~= 1) and (T[i][j] == D[i - 1][j - 1] + SCORE_MATCH_CONSECUTIVE)
|
||||
positions[i] = j
|
||||
j = j - 1
|
||||
break
|
||||
else
|
||||
j = j - 1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return T[n][m], positions
|
||||
end
|
||||
|
||||
-- Return only the positions of a match.
|
||||
function M.positions(needle, haystack, case_sensitive)
|
||||
local _, positions = M.score_and_positions(needle, haystack, case_sensitive)
|
||||
return positions
|
||||
end
|
||||
|
||||
function M.get_score_min()
|
||||
return SCORE_MIN
|
||||
end
|
||||
|
||||
function M.get_score_max()
|
||||
return SCORE_MAX
|
||||
end
|
||||
|
||||
function M.get_max_length()
|
||||
return MATCH_MAX_LENGTH
|
||||
end
|
||||
|
||||
function M.get_score_floor()
|
||||
return MATCH_MAX_LENGTH * SCORE_GAP_INNER
|
||||
end
|
||||
|
||||
function M.get_score_ceiling()
|
||||
return MATCH_MAX_LENGTH * SCORE_MATCH_CONSECUTIVE
|
||||
end
|
||||
|
||||
function M.get_implementation_name()
|
||||
return 'lua'
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,232 @@
|
||||
---A generalization of the filter functionality to directly filter the
|
||||
---source tree instead of relying on pre-filtered data, which is specific
|
||||
---to the filesystem source.
|
||||
local vim = vim
|
||||
local Input = require("nui.input")
|
||||
local event = require("nui.utils.autocmd").event
|
||||
local popups = require("neo-tree.ui.popups")
|
||||
local renderer = require("neo-tree.ui.renderer")
|
||||
local utils = require("neo-tree.utils")
|
||||
local log = require("neo-tree.log")
|
||||
local manager = require("neo-tree.sources.manager")
|
||||
local fzy = require("neo-tree.sources.common.filters.filter_fzy")
|
||||
|
||||
local M = {}
|
||||
|
||||
local cmds = {
|
||||
move_cursor_down = function(state, scroll_padding)
|
||||
renderer.focus_node(state, nil, true, 1, scroll_padding)
|
||||
end,
|
||||
|
||||
move_cursor_up = function(state, scroll_padding)
|
||||
renderer.focus_node(state, nil, true, -1, scroll_padding)
|
||||
vim.cmd("redraw!")
|
||||
end,
|
||||
}
|
||||
|
||||
---Reset the current filter to the empty string.
|
||||
---@param state any
|
||||
---@param refresh boolean? whether to refresh the source tree
|
||||
---@param open_current_node boolean? whether to open the current node
|
||||
local reset_filter = function(state, refresh, open_current_node)
|
||||
log.trace("reset_search")
|
||||
if refresh == nil then
|
||||
refresh = true
|
||||
end
|
||||
|
||||
-- Cancel any pending search
|
||||
require("neo-tree.sources.filesystem.lib.filter_external").cancel()
|
||||
|
||||
-- reset search state
|
||||
if state.open_folders_before_search then
|
||||
state.force_open_folders = vim.deepcopy(state.open_folders_before_search, { noref = 1 })
|
||||
else
|
||||
state.force_open_folders = nil
|
||||
end
|
||||
state.open_folders_before_search = nil
|
||||
state.search_pattern = nil
|
||||
|
||||
if open_current_node then
|
||||
local success, node = pcall(state.tree.get_node, state.tree)
|
||||
if success and node then
|
||||
local id = node:get_id()
|
||||
renderer.position.set(state, id)
|
||||
id = utils.remove_trailing_slash(id)
|
||||
manager.navigate(state, nil, id, utils.wrap(pcall, renderer.focus_node, state, id, false))
|
||||
end
|
||||
elseif refresh then
|
||||
manager.navigate(state)
|
||||
else
|
||||
state.tree = vim.deepcopy(state.orig_tree)
|
||||
end
|
||||
state.orig_tree = nil
|
||||
end
|
||||
|
||||
---Show the filtered tree
|
||||
---@param state any
|
||||
---@param do_not_focus_window boolean? whether to focus the window
|
||||
local show_filtered_tree = function(state, do_not_focus_window)
|
||||
state.tree = vim.deepcopy(state.orig_tree)
|
||||
state.tree:get_nodes()[1].search_pattern = state.search_pattern
|
||||
local max_score, max_id = fzy.get_score_min(), nil
|
||||
local function filter_tree(node_id)
|
||||
local node = state.tree:get_node(node_id)
|
||||
local path = node.extra.search_path or node.path
|
||||
|
||||
local should_keep = fzy.has_match(state.search_pattern, path)
|
||||
if should_keep then
|
||||
local score = fzy.score(state.search_pattern, path)
|
||||
node.extra.fzy_score = score
|
||||
if score > max_score then
|
||||
max_score = score
|
||||
max_id = node_id
|
||||
end
|
||||
end
|
||||
|
||||
if node:has_children() then
|
||||
for _, child_id in ipairs(node:get_child_ids()) do
|
||||
should_keep = filter_tree(child_id) or should_keep
|
||||
end
|
||||
end
|
||||
if not should_keep then
|
||||
state.tree:remove_node(node_id) -- TODO: this might not be efficient
|
||||
end
|
||||
return should_keep
|
||||
end
|
||||
if #state.search_pattern > 0 then
|
||||
for _, root in ipairs(state.tree:get_nodes()) do
|
||||
filter_tree(root:get_id())
|
||||
end
|
||||
end
|
||||
manager.redraw(state.name)
|
||||
if max_id then
|
||||
renderer.focus_node(state, max_id, do_not_focus_window)
|
||||
end
|
||||
end
|
||||
|
||||
---Main entry point for the filter functionality.
|
||||
---This will display a filter input popup and filter the source tree on change and on submit
|
||||
---@param state table the source state
|
||||
---@param search_as_you_type boolean? whether to filter as you type or only on submit
|
||||
---@param keep_filter_on_submit boolean? whether to keep the filter on <CR> or reset it
|
||||
M.show_filter = function(state, search_as_you_type, keep_filter_on_submit)
|
||||
local winid = vim.api.nvim_get_current_win()
|
||||
local height = vim.api.nvim_win_get_height(winid)
|
||||
local scroll_padding = 3
|
||||
|
||||
-- setup the input popup options
|
||||
local popup_msg = "Search:"
|
||||
if search_as_you_type then
|
||||
popup_msg = "Filter:"
|
||||
end
|
||||
|
||||
local width = vim.fn.winwidth(0) - 2
|
||||
local row = height - 3
|
||||
if state.current_position == "float" then
|
||||
scroll_padding = 0
|
||||
width = vim.fn.winwidth(winid)
|
||||
row = height - 2
|
||||
vim.api.nvim_win_set_height(winid, row)
|
||||
end
|
||||
|
||||
state.orig_tree = vim.deepcopy(state.tree)
|
||||
|
||||
local popup_options = popups.popup_options(popup_msg, width, {
|
||||
relative = "win",
|
||||
winid = winid,
|
||||
position = {
|
||||
row = row,
|
||||
col = 0,
|
||||
},
|
||||
size = width,
|
||||
})
|
||||
|
||||
local has_pre_search_folders = utils.truthy(state.open_folders_before_search)
|
||||
if not has_pre_search_folders then
|
||||
log.trace("No search or pre-search folders, recording pre-search folders now")
|
||||
state.open_folders_before_search = renderer.get_expanded_nodes(state.tree)
|
||||
end
|
||||
|
||||
local waiting_for_default_value = utils.truthy(state.search_pattern)
|
||||
local input = Input(popup_options, {
|
||||
prompt = " ",
|
||||
default_value = state.search_pattern,
|
||||
on_submit = function(value)
|
||||
if value == "" then
|
||||
reset_filter(state)
|
||||
return
|
||||
end
|
||||
if search_as_you_type and not keep_filter_on_submit then
|
||||
reset_filter(state, true, true)
|
||||
return
|
||||
end
|
||||
-- do the search
|
||||
state.search_pattern = value
|
||||
show_filtered_tree(state, false)
|
||||
end,
|
||||
--this can be bad in a deep folder structure
|
||||
on_change = function(value)
|
||||
if not search_as_you_type then
|
||||
return
|
||||
end
|
||||
-- apparently when a default value is set, on_change fires for every character
|
||||
if waiting_for_default_value then
|
||||
if #value < #state.search_pattern then
|
||||
return
|
||||
end
|
||||
waiting_for_default_value = false
|
||||
end
|
||||
if value == state.search_pattern or value == nil then
|
||||
return
|
||||
end
|
||||
|
||||
-- finally do the search
|
||||
log.trace("Setting search in on_change to: " .. value)
|
||||
state.search_pattern = value
|
||||
local len_to_delay = { [0] = 500, 500, 400, 200 }
|
||||
local delay = len_to_delay[#value] or 100
|
||||
|
||||
utils.debounce(state.name .. "_filter", function()
|
||||
show_filtered_tree(state, true)
|
||||
end, delay, utils.debounce_strategy.CALL_LAST_ONLY)
|
||||
end,
|
||||
})
|
||||
|
||||
input:mount()
|
||||
|
||||
local restore_height = vim.schedule_wrap(function()
|
||||
if vim.api.nvim_win_is_valid(winid) then
|
||||
vim.api.nvim_win_set_height(winid, height)
|
||||
end
|
||||
end)
|
||||
|
||||
-- create mappings and autocmd
|
||||
input:map("i", "<C-w>", "<C-S-w>", { noremap = true })
|
||||
input:map("i", "<esc>", function(bufnr)
|
||||
vim.cmd("stopinsert")
|
||||
input:unmount()
|
||||
if utils.truthy(state.search_pattern) then
|
||||
reset_filter(state, true)
|
||||
end
|
||||
restore_height()
|
||||
end, { noremap = true })
|
||||
|
||||
local config = require("neo-tree").config
|
||||
for lhs, cmd_name in pairs(config.filesystem.window.fuzzy_finder_mappings) do
|
||||
local t = type(cmd_name)
|
||||
if t == "string" then
|
||||
local cmd = cmds[cmd_name]
|
||||
if cmd then
|
||||
input:map("i", lhs, utils.wrap(cmd, state, scroll_padding), { noremap = true })
|
||||
else
|
||||
log.warn(string.format("Invalid command in fuzzy_finder_mappings: %s = %s", lhs, cmd_name))
|
||||
end
|
||||
elseif t == "function" then
|
||||
input:map("i", lhs, utils.wrap(cmd_name, state, scroll_padding), { noremap = true })
|
||||
else
|
||||
log.warn(string.format("Invalid command in fuzzy_finder_mappings: %s = %s", lhs, cmd_name))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,152 @@
|
||||
local Popup = require("nui.popup")
|
||||
local NuiLine = require("nui.line")
|
||||
local utils = require("neo-tree.utils")
|
||||
local popups = require("neo-tree.ui.popups")
|
||||
local highlights = require("neo-tree.ui.highlights")
|
||||
local M = {}
|
||||
|
||||
local add_text = function(text, highlight)
|
||||
local line = NuiLine()
|
||||
line:append(text, highlight)
|
||||
return line
|
||||
end
|
||||
|
||||
local get_sub_keys = function(state, prefix_key)
|
||||
local keys = utils.get_keys(state.resolved_mappings, true)
|
||||
if prefix_key then
|
||||
local len = prefix_key:len()
|
||||
local sub_keys = {}
|
||||
for _, key in ipairs(keys) do
|
||||
if #key > len and key:sub(1, len) == prefix_key then
|
||||
table.insert(sub_keys, key)
|
||||
end
|
||||
end
|
||||
return sub_keys
|
||||
else
|
||||
return keys
|
||||
end
|
||||
end
|
||||
|
||||
local function key_minus_prefix(key, prefix)
|
||||
if prefix then
|
||||
return key:sub(prefix:len() + 1)
|
||||
else
|
||||
return key
|
||||
end
|
||||
end
|
||||
|
||||
---Shows a help screen for the mapped commands when will execute those commands
|
||||
---when the corresponding key is pressed.
|
||||
---@param state table state of the source.
|
||||
---@param title string if this is a sub-menu for a multi-key mapping, the title for the window.
|
||||
---@param prefix_key string if this is a sub-menu, the start of tehe multi-key mapping
|
||||
M.show = function(state, title, prefix_key)
|
||||
local tree_width = vim.api.nvim_win_get_width(state.winid)
|
||||
local keys = get_sub_keys(state, prefix_key)
|
||||
|
||||
local lines = { add_text("") }
|
||||
lines[1] = add_text(" Press the corresponding key to execute the command.", "Comment")
|
||||
lines[2] = add_text(" Press <Esc> to cancel.", "Comment")
|
||||
lines[3] = add_text("")
|
||||
local header = NuiLine()
|
||||
header:append(string.format(" %14s", "KEY(S)"), highlights.ROOT_NAME)
|
||||
header:append(" ", highlights.DIM_TEXT)
|
||||
header:append("COMMAND", highlights.ROOT_NAME)
|
||||
lines[4] = header
|
||||
local max_width = #lines[1]:content()
|
||||
for _, key in ipairs(keys) do
|
||||
local value = state.resolved_mappings[key]
|
||||
local nline = NuiLine()
|
||||
nline:append(string.format(" %14s", key_minus_prefix(key, prefix_key)), highlights.FILTER_TERM)
|
||||
nline:append(" -> ", highlights.DIM_TEXT)
|
||||
nline:append(value.text, highlights.NORMAL)
|
||||
local line = nline:content()
|
||||
if #line > max_width then
|
||||
max_width = #line
|
||||
end
|
||||
table.insert(lines, nline)
|
||||
end
|
||||
|
||||
local width = math.min(60, max_width + 1)
|
||||
|
||||
if state.current_position == "right" then
|
||||
col = vim.o.columns - tree_width - width - 1
|
||||
else
|
||||
col = tree_width - 1
|
||||
end
|
||||
|
||||
local options = {
|
||||
position = {
|
||||
row = 2,
|
||||
col = col,
|
||||
},
|
||||
size = {
|
||||
width = width,
|
||||
height = #keys + 5,
|
||||
},
|
||||
enter = true,
|
||||
focusable = true,
|
||||
zindex = 50,
|
||||
relative = "editor",
|
||||
}
|
||||
|
||||
local popup_max_height = function()
|
||||
local lines = vim.o.lines
|
||||
local cmdheight = vim.o.cmdheight
|
||||
-- statuscolumn
|
||||
local statuscolumn_lines = 0
|
||||
local laststatus = vim.o.laststatus
|
||||
if laststatus ~= 0 then
|
||||
local windows = vim.api.nvim_tabpage_list_wins(0)
|
||||
if (laststatus == 1 and #windows > 1) or laststatus > 1 then
|
||||
statuscolumn_lines = 1
|
||||
end
|
||||
end
|
||||
-- tabs
|
||||
local tab_lines = 0
|
||||
local showtabline = vim.o.showtabline
|
||||
if showtabline ~= 0 then
|
||||
local tabs = vim.api.nvim_list_tabpages()
|
||||
if (showtabline == 1 and #tabs > 1) or showtabline == 2 then
|
||||
tab_lines = 1
|
||||
end
|
||||
end
|
||||
return lines - cmdheight - statuscolumn_lines - tab_lines - 1
|
||||
end
|
||||
local max_height = popup_max_height()
|
||||
if options.size.height > max_height then
|
||||
options.size.height = max_height
|
||||
end
|
||||
|
||||
local title = title or "Neotree Help"
|
||||
local options = popups.popup_options(title, width, options)
|
||||
local popup = Popup(options)
|
||||
popup:mount()
|
||||
|
||||
popup:map("n", "<esc>", function()
|
||||
popup:unmount()
|
||||
end, { noremap = true })
|
||||
|
||||
local event = require("nui.utils.autocmd").event
|
||||
popup:on({ event.BufLeave, event.BufDelete }, function()
|
||||
popup:unmount()
|
||||
end, { once = true })
|
||||
|
||||
for _, key in ipairs(keys) do
|
||||
-- map everything except for <escape>
|
||||
if string.match(key:lower(), "^<esc") == nil then
|
||||
local value = state.resolved_mappings[key]
|
||||
popup:map("n", key_minus_prefix(key, prefix_key), function()
|
||||
popup:unmount()
|
||||
vim.api.nvim_set_current_win(state.winid)
|
||||
value.handler()
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
for i, line in ipairs(lines) do
|
||||
line:render(popup.bufnr, -1, i)
|
||||
end
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,46 @@
|
||||
local events = require("neo-tree.events")
|
||||
local log = require("neo-tree.log")
|
||||
local manager = require("neo-tree.sources.manager")
|
||||
|
||||
local M = {}
|
||||
|
||||
local hijack_cursor_handler = function()
|
||||
if vim.o.filetype ~= "neo-tree" then
|
||||
return
|
||||
end
|
||||
local success, source = pcall(vim.api.nvim_buf_get_var, 0, "neo_tree_source")
|
||||
if not success then
|
||||
log.debug("Cursor hijack failure: " .. vim.inspect(source))
|
||||
return
|
||||
end
|
||||
local winid = nil
|
||||
local _, position = pcall(vim.api.nvim_buf_get_var, 0, "neo_tree_position")
|
||||
if position == "current" then
|
||||
winid = vim.api.nvim_get_current_win()
|
||||
end
|
||||
|
||||
local state = manager.get_state(source, nil, winid)
|
||||
if state == nil then
|
||||
return
|
||||
end
|
||||
local node = state.tree:get_node()
|
||||
log.debug("Cursor moved in tree window, hijacking cursor position")
|
||||
local cursor = vim.api.nvim_win_get_cursor(0)
|
||||
local row = cursor[1]
|
||||
local current_line = vim.api.nvim_get_current_line()
|
||||
local startIndex, _ = string.find(current_line, node.name, nil, true)
|
||||
if startIndex then
|
||||
vim.api.nvim_win_set_cursor(0, { row, startIndex - 1 })
|
||||
end
|
||||
end
|
||||
|
||||
--Enables cursor hijack behavior for all sources
|
||||
M.setup = function()
|
||||
events.subscribe({
|
||||
event = events.VIM_CURSOR_MOVED,
|
||||
handler = hijack_cursor_handler,
|
||||
id = "neo-tree-hijack-cursor",
|
||||
})
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,82 @@
|
||||
local log = require("neo-tree.log")
|
||||
|
||||
local M = {}
|
||||
|
||||
--- Recursively expand all loaded nodes under the given node
|
||||
--- returns table with all discovered nodes that need to be loaded
|
||||
---@param node table a node to expand
|
||||
---@param state table current state of the source
|
||||
---@return table discovered nodes that need to be loaded
|
||||
local function expand_loaded(node, state, prefetcher)
|
||||
local function rec(current_node, to_load)
|
||||
if prefetcher.should_prefetch(current_node) then
|
||||
log.trace("Node " .. current_node:get_id() .. "not loaded, saving for later")
|
||||
table.insert(to_load, current_node)
|
||||
else
|
||||
if not current_node:is_expanded() then
|
||||
current_node:expand()
|
||||
state.explicitly_opened_directories[current_node:get_id()] = true
|
||||
end
|
||||
local children = state.tree:get_nodes(current_node:get_id())
|
||||
log.debug("Expanding childrens of " .. current_node:get_id())
|
||||
for _, child in ipairs(children) do
|
||||
if child.type == "directory" then
|
||||
rec(child, to_load)
|
||||
else
|
||||
log.trace("Child: " .. child.name .. " is not a directory, skipping")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local to_load = {}
|
||||
rec(node, to_load)
|
||||
return to_load
|
||||
end
|
||||
|
||||
--- Recursively expands all nodes under the given node collecting all unloaded nodes
|
||||
--- Then run prefetcher on all unloaded nodes. Finally, expand loded nodes.
|
||||
--- async method
|
||||
---@param node table a node to expand
|
||||
---@param state table current state of the source
|
||||
local function expand_and_load(node, state, prefetcher)
|
||||
local to_load = expand_loaded(node, state, prefetcher)
|
||||
for _, _node in ipairs(to_load) do
|
||||
prefetcher.prefetch(state, _node)
|
||||
-- no need to handle results as prefetch is recursive
|
||||
expand_loaded(_node, state, prefetcher)
|
||||
end
|
||||
end
|
||||
|
||||
--- Expands given node recursively loading all descendant nodes if needed
|
||||
--- Nodes will be loaded using given prefetcher
|
||||
--- async method
|
||||
---@param state table current state of the source
|
||||
---@param node table a node to expand
|
||||
---@param prefetcher table an object with two methods `prefetch(state, node)` and `should_prefetch(node) => boolean`
|
||||
M.expand_directory_recursively = function(state, node, prefetcher)
|
||||
log.debug("Expanding directory " .. node:get_id())
|
||||
if node.type ~= "directory" then
|
||||
return
|
||||
end
|
||||
state.explicitly_opened_directories = state.explicitly_opened_directories or {}
|
||||
if prefetcher.should_prefetch(node) then
|
||||
local id = node:get_id()
|
||||
state.explicitly_opened_directories[id] = true
|
||||
prefetcher.prefetch(state, node)
|
||||
expand_loaded(node, state, prefetcher)
|
||||
else
|
||||
expand_and_load(node, state, prefetcher)
|
||||
end
|
||||
end
|
||||
|
||||
M.default_prefetcher = {
|
||||
prefetch = function (state, node)
|
||||
log.debug("Default expander prefetch does nothing")
|
||||
end,
|
||||
should_prefetch = function (node)
|
||||
return false
|
||||
end
|
||||
}
|
||||
|
||||
return M
|
||||
@ -0,0 +1,452 @@
|
||||
local vim = vim
|
||||
local utils = require("neo-tree.utils")
|
||||
local highlights = require("neo-tree.ui.highlights")
|
||||
local events = require("neo-tree.events")
|
||||
local manager = require("neo-tree.sources.manager")
|
||||
local log = require("neo-tree.log")
|
||||
local renderer = require("neo-tree.ui.renderer")
|
||||
|
||||
local neo_tree_preview_namespace = vim.api.nvim_create_namespace("neo_tree_preview")
|
||||
|
||||
local function create_floating_preview_window(state)
|
||||
local default_position = utils.resolve_config_option(state, "window.position", "left")
|
||||
state.current_position = state.current_position or default_position
|
||||
|
||||
local winwidth = vim.api.nvim_win_get_width(state.winid)
|
||||
local winheight = vim.api.nvim_win_get_height(state.winid)
|
||||
local height = vim.o.lines - 4
|
||||
local width = 120
|
||||
local row, col = 0, 0
|
||||
|
||||
if state.current_position == "left" then
|
||||
col = winwidth + 1
|
||||
width = math.min(vim.o.columns - col, 120)
|
||||
elseif state.current_position == "top" or state.current_position == "bottom" then
|
||||
height = height - winheight
|
||||
width = winwidth - 2
|
||||
if state.current_position == "top" then
|
||||
row = vim.api.nvim_win_get_height(state.winid) + 1
|
||||
end
|
||||
elseif state.current_position == "right" then
|
||||
width = math.min(vim.o.columns - winwidth - 4, 120)
|
||||
col = vim.o.columns - winwidth - width - 3
|
||||
elseif state.current_position == "float" then
|
||||
local pos = vim.api.nvim_win_get_position(state.winid)
|
||||
-- preview will be same height and top as tree
|
||||
row = pos[1] - 1
|
||||
height = winheight
|
||||
|
||||
-- tree and preview window will be side by side and centered in the editor
|
||||
width = math.min(vim.o.columns - winwidth - 4, 120)
|
||||
local total_width = winwidth + width + 4
|
||||
local margin = math.floor((vim.o.columns - total_width) / 2)
|
||||
col = margin + winwidth + 2
|
||||
|
||||
-- move the tree window to make the combined layout centered
|
||||
local popup = renderer.get_nui_popup(state.winid)
|
||||
popup:update_layout({
|
||||
relative = "editor",
|
||||
position = {
|
||||
row = row,
|
||||
col = margin,
|
||||
},
|
||||
})
|
||||
else
|
||||
local cur_pos = state.current_position or "unknown"
|
||||
log.error('Preview cannot be used when position = "' .. cur_pos .. '"')
|
||||
return
|
||||
end
|
||||
|
||||
local popups = require("neo-tree.ui.popups")
|
||||
local options = popups.popup_options("Neo-tree Preview", width, {
|
||||
ns_id = highlights.ns_id,
|
||||
size = { height = height, width = width },
|
||||
relative = "editor",
|
||||
position = {
|
||||
row = row,
|
||||
col = col,
|
||||
},
|
||||
win_options = {
|
||||
number = true,
|
||||
winhighlight = "Normal:"
|
||||
.. highlights.FLOAT_NORMAL
|
||||
.. ",FloatBorder:"
|
||||
.. highlights.FLOAT_BORDER,
|
||||
},
|
||||
})
|
||||
options.zindex = 40
|
||||
options.buf_options.filetype = "neo-tree-preview"
|
||||
|
||||
local NuiPopup = require("nui.popup")
|
||||
local win = NuiPopup(options)
|
||||
win:mount()
|
||||
return win
|
||||
end
|
||||
|
||||
local Preview = {}
|
||||
local instance = nil
|
||||
|
||||
---Creates a new preview.
|
||||
---@param state table The state of the source.
|
||||
---@return table preview A new preview. A preview is a table consisting of the following keys:
|
||||
-- active = boolean Whether the preview is active.
|
||||
-- winid = number The id of the window being used to preview.
|
||||
-- is_neo_tree_window boolean Whether the preview window belongs to neo-tree.
|
||||
-- bufnr = number The buffer that is currently in the preview window.
|
||||
-- start_pos = array or nil An array-like table specifying the (0-indexed) starting position of the previewed text.
|
||||
-- end_pos = array or nil An array-like table specifying the (0-indexed) ending position of the preview text.
|
||||
-- truth = table A table containing information to be restored when the preview ends.
|
||||
-- events = array A list of events the preview is subscribed to.
|
||||
--These keys should not be altered directly. Note that the keys `start_pos`, `end_pos` and `truth`
|
||||
--may be inaccurate if `active` is false.
|
||||
function Preview:new(state)
|
||||
local preview = {}
|
||||
preview.active = false
|
||||
preview.config = vim.deepcopy(state.config)
|
||||
setmetatable(preview, { __index = self })
|
||||
preview:findWindow(state)
|
||||
return preview
|
||||
end
|
||||
|
||||
---Preview a buffer in the preview window and optionally reveal and highlight the previewed text.
|
||||
---@param bufnr number? The number of the buffer to be previewed.
|
||||
---@param start_pos table? The (0-indexed) starting position of the previewed text. May be absent.
|
||||
---@param end_pos table? The (0-indexed) ending position of the previewed text. May be absent
|
||||
function Preview:preview(bufnr, start_pos, end_pos)
|
||||
if self.is_neo_tree_window then
|
||||
log.warn("Could not find appropriate window for preview")
|
||||
return
|
||||
end
|
||||
|
||||
bufnr = bufnr or self.bufnr
|
||||
if not self.active then
|
||||
self:activate()
|
||||
end
|
||||
|
||||
if not self.active then
|
||||
return
|
||||
end
|
||||
|
||||
if bufnr ~= self.bufnr then
|
||||
self:setBuffer(bufnr)
|
||||
end
|
||||
|
||||
self:clearHighlight()
|
||||
|
||||
self.bufnr = bufnr
|
||||
self.start_pos = start_pos
|
||||
self.end_pos = end_pos
|
||||
|
||||
self:reveal()
|
||||
self:highlight()
|
||||
end
|
||||
|
||||
---Reverts the preview and inactivates it, restoring the preview window to its previous state.
|
||||
function Preview:revert()
|
||||
self.active = false
|
||||
self:unsubscribe()
|
||||
self:clearHighlight()
|
||||
|
||||
if not renderer.is_window_valid(self.winid) then
|
||||
self.winid = nil
|
||||
return
|
||||
end
|
||||
|
||||
if self.config.use_float then
|
||||
vim.api.nvim_win_close(self.winid, true)
|
||||
self.winid = nil
|
||||
return
|
||||
else
|
||||
local foldenable = utils.get_value(self.truth, "options.foldenable", nil, false)
|
||||
if foldenable ~= nil then
|
||||
vim.api.nvim_win_set_option(self.winid, "foldenable", self.truth.options.foldenable)
|
||||
end
|
||||
vim.api.nvim_win_set_var(self.winid, "neo_tree_preview", 0)
|
||||
end
|
||||
|
||||
local bufnr = self.truth.bufnr
|
||||
if type(bufnr) ~= "number" then
|
||||
return
|
||||
end
|
||||
if not vim.api.nvim_buf_is_valid(bufnr) then
|
||||
return
|
||||
end
|
||||
self:setBuffer(bufnr)
|
||||
self.bufnr = bufnr
|
||||
if vim.api.nvim_win_is_valid(self.winid) then
|
||||
vim.api.nvim_win_call(self.winid, function()
|
||||
vim.fn.winrestview(self.truth.view)
|
||||
end)
|
||||
end
|
||||
vim.api.nvim_buf_set_option(self.bufnr, "bufhidden", self.truth.options.bufhidden)
|
||||
end
|
||||
|
||||
---Subscribe to event and add it to the preview event list.
|
||||
--@param source string? Name of the source to add the event to. Will use `events.subscribe` if nil.
|
||||
--@param event table Event to subscribe to.
|
||||
function Preview:subscribe(source, event)
|
||||
if source == nil then
|
||||
events.subscribe(event)
|
||||
else
|
||||
manager.subscribe(source, event)
|
||||
end
|
||||
self.events = self.events or {}
|
||||
table.insert(self.events, { source = source, event = event })
|
||||
end
|
||||
|
||||
---Unsubscribe to all events in the preview event list.
|
||||
function Preview:unsubscribe()
|
||||
if self.events == nil then
|
||||
return
|
||||
end
|
||||
for _, event in ipairs(self.events) do
|
||||
if event.source == nil then
|
||||
events.unsubscribe(event.event)
|
||||
else
|
||||
manager.unsubscribe(event.source, event.event)
|
||||
end
|
||||
end
|
||||
self.events = {}
|
||||
end
|
||||
|
||||
---Finds the appropriate window and updates the preview accordingly.
|
||||
---@param state table The state of the source.
|
||||
function Preview:findWindow(state)
|
||||
local winid, is_neo_tree_window
|
||||
if self.config.use_float then
|
||||
if
|
||||
type(self.winid) == "number"
|
||||
and vim.api.nvim_win_is_valid(self.winid)
|
||||
and utils.is_floating(self.winid)
|
||||
then
|
||||
return
|
||||
end
|
||||
local win = create_floating_preview_window(state)
|
||||
if not win then
|
||||
self.active = false
|
||||
return
|
||||
end
|
||||
winid = win.winid
|
||||
is_neo_tree_window = false
|
||||
else
|
||||
winid, is_neo_tree_window = utils.get_appropriate_window(state)
|
||||
self.bufnr = vim.api.nvim_win_get_buf(winid)
|
||||
end
|
||||
|
||||
if winid == self.winid then
|
||||
return
|
||||
end
|
||||
self.winid, self.is_neo_tree_window = winid, is_neo_tree_window
|
||||
|
||||
if self.active then
|
||||
self:revert()
|
||||
self:preview()
|
||||
end
|
||||
end
|
||||
|
||||
---Activates the preview, but does not populate the preview window,
|
||||
function Preview:activate()
|
||||
if self.active then
|
||||
return
|
||||
end
|
||||
if not renderer.is_window_valid(self.winid) then
|
||||
return
|
||||
end
|
||||
if self.config.use_float then
|
||||
self.truth = {}
|
||||
else
|
||||
self.truth = {
|
||||
bufnr = self.bufnr,
|
||||
view = vim.api.nvim_win_call(self.winid, vim.fn.winsaveview),
|
||||
options = {
|
||||
bufhidden = vim.api.nvim_buf_get_option(self.bufnr, "bufhidden"),
|
||||
foldenable = vim.api.nvim_win_get_option(self.winid, "foldenable"),
|
||||
},
|
||||
}
|
||||
vim.api.nvim_buf_set_option(self.bufnr, "bufhidden", "hide")
|
||||
vim.api.nvim_win_set_option(self.winid, "foldenable", false)
|
||||
end
|
||||
self.active = true
|
||||
vim.api.nvim_win_set_var(self.winid, "neo_tree_preview", 1)
|
||||
end
|
||||
|
||||
---@param winid number
|
||||
---@param bufnr number
|
||||
local function try_load_image_nvim_buf(winid, bufnr)
|
||||
if vim.bo[bufnr].filetype ~= "image_nvim" then
|
||||
return false
|
||||
end
|
||||
local success, mod = pcall(require, "image")
|
||||
if not success or not mod.hijack_buffer then
|
||||
local image_nvim_url = "https://github.com/3rd/image.nvim"
|
||||
log.debug("You'll need to install image.nvim to use this command: " .. image_nvim_url)
|
||||
return false
|
||||
end
|
||||
return mod.hijack_buffer(vim.api.nvim_buf_get_name(bufnr), winid, bufnr)
|
||||
end
|
||||
|
||||
---Set the buffer in the preview window without executing BufEnter or BufWinEnter autocommands.
|
||||
--@param bufnr number The buffer number of the buffer to set.
|
||||
function Preview:setBuffer(bufnr)
|
||||
local eventignore = vim.opt.eventignore
|
||||
vim.opt.eventignore:append("BufEnter,BufWinEnter")
|
||||
vim.api.nvim_win_set_buf(self.winid, bufnr)
|
||||
if self.config.use_image_nvim then
|
||||
try_load_image_nvim_buf(self.winid, bufnr)
|
||||
end
|
||||
if self.config.use_float then
|
||||
-- I'm not sufe why float windows won;t show numbers without this
|
||||
vim.api.nvim_win_set_option(self.winid, "number", true)
|
||||
end
|
||||
vim.opt.eventignore = eventignore
|
||||
end
|
||||
|
||||
---Move the cursor to the previewed position and center the screen.
|
||||
function Preview:reveal()
|
||||
local pos = self.start_pos or self.end_pos
|
||||
if not self.active or not self.winid or not pos then
|
||||
return
|
||||
end
|
||||
vim.api.nvim_win_set_cursor(self.winid, { (pos[1] or 0) + 1, pos[2] or 0 })
|
||||
vim.api.nvim_win_call(self.winid, function()
|
||||
vim.cmd("normal! zz")
|
||||
end)
|
||||
end
|
||||
|
||||
---Highlight the previewed range
|
||||
function Preview:highlight()
|
||||
if not self.active or not self.bufnr then
|
||||
return
|
||||
end
|
||||
local start_pos, end_pos = self.start_pos, self.end_pos
|
||||
if not start_pos and not end_pos then
|
||||
return
|
||||
elseif not start_pos then
|
||||
start_pos = end_pos
|
||||
elseif not end_pos then
|
||||
end_pos = start_pos
|
||||
end
|
||||
|
||||
local highlight = function(line, col_start, col_end)
|
||||
vim.api.nvim_buf_add_highlight(
|
||||
self.bufnr,
|
||||
neo_tree_preview_namespace,
|
||||
highlights.PREVIEW,
|
||||
line,
|
||||
col_start,
|
||||
col_end
|
||||
)
|
||||
end
|
||||
|
||||
local start_line, end_line = start_pos[1], end_pos[1]
|
||||
local start_col, end_col = start_pos[2], end_pos[2]
|
||||
if start_line == end_line then
|
||||
highlight(start_line, start_col, end_col)
|
||||
else
|
||||
highlight(start_line, start_col, -1)
|
||||
for line = start_line + 1, end_line - 1 do
|
||||
highlight(line, 0, -1)
|
||||
end
|
||||
highlight(end_line, 0, end_col)
|
||||
end
|
||||
end
|
||||
|
||||
---Clear the preview highlight in the buffer currently in the preview window.
|
||||
function Preview:clearHighlight()
|
||||
if type(self.bufnr) == "number" and vim.api.nvim_buf_is_valid(self.bufnr) then
|
||||
vim.api.nvim_buf_clear_namespace(self.bufnr, neo_tree_preview_namespace, 0, -1)
|
||||
end
|
||||
end
|
||||
|
||||
local toggle_state = false
|
||||
|
||||
Preview.hide = function()
|
||||
toggle_state = false
|
||||
if instance then
|
||||
instance:revert()
|
||||
end
|
||||
instance = nil
|
||||
end
|
||||
|
||||
Preview.is_active = function()
|
||||
return instance and instance.active
|
||||
end
|
||||
|
||||
Preview.show = function(state)
|
||||
local node = state.tree:get_node()
|
||||
if node.type == "directory" then
|
||||
return
|
||||
end
|
||||
|
||||
if instance then
|
||||
instance:findWindow(state)
|
||||
else
|
||||
instance = Preview:new(state)
|
||||
end
|
||||
|
||||
local extra = node.extra or {}
|
||||
local position = extra.position
|
||||
local end_position = extra.end_position
|
||||
local path = node.path or node:get_id()
|
||||
local bufnr = extra.bufnr or vim.fn.bufadd(path)
|
||||
|
||||
if bufnr and bufnr > 0 and instance then
|
||||
instance:preview(bufnr, position, end_position)
|
||||
end
|
||||
end
|
||||
|
||||
Preview.toggle = function(state)
|
||||
if toggle_state then
|
||||
Preview.hide()
|
||||
else
|
||||
Preview.show(state)
|
||||
if instance and instance.active then
|
||||
toggle_state = true
|
||||
else
|
||||
Preview.hide()
|
||||
return
|
||||
end
|
||||
local winid = state.winid
|
||||
local source_name = state.name
|
||||
local preview_event = {
|
||||
event = events.VIM_CURSOR_MOVED,
|
||||
handler = function()
|
||||
local did_enter_preview = vim.api.nvim_get_current_win() == instance.winid
|
||||
if not toggle_state or (did_enter_preview and instance.config.use_float) then
|
||||
return
|
||||
end
|
||||
if vim.api.nvim_get_current_win() == winid then
|
||||
log.debug("Cursor moved in tree window, updating preview")
|
||||
Preview.show(state)
|
||||
else
|
||||
log.debug("Neo-tree window lost focus, disposing preview")
|
||||
Preview.hide()
|
||||
end
|
||||
end,
|
||||
id = "preview-event",
|
||||
}
|
||||
instance:subscribe(source_name, preview_event)
|
||||
end
|
||||
end
|
||||
|
||||
Preview.focus = function()
|
||||
if Preview.is_active() then
|
||||
vim.fn.win_gotoid(instance.winid)
|
||||
end
|
||||
end
|
||||
|
||||
Preview.scroll = function(state)
|
||||
local direction = state.config.direction
|
||||
-- NOTE: Chars below are raw escape codes for <Ctrl-E>/<Ctrl-Y>
|
||||
local input = direction < 0 and [[]] or [[]]
|
||||
local count = math.abs(direction)
|
||||
|
||||
if Preview:is_active() then
|
||||
vim.api.nvim_win_call(instance.winid, function()
|
||||
vim.cmd([[normal! ]] .. count .. input)
|
||||
end)
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
return Preview
|
||||
@ -0,0 +1,69 @@
|
||||
--This file should contain all commands meant to be used by mappings.
|
||||
local cc = require("neo-tree.sources.common.commands")
|
||||
local utils = require("neo-tree.utils")
|
||||
local manager = require("neo-tree.sources.manager")
|
||||
local inputs = require("neo-tree.ui.inputs")
|
||||
local filters = require("neo-tree.sources.common.filters")
|
||||
|
||||
local vim = vim
|
||||
|
||||
local M = {}
|
||||
local SOURCE_NAME = "document_symbols"
|
||||
M.refresh = utils.wrap(manager.refresh, SOURCE_NAME)
|
||||
M.redraw = utils.wrap(manager.redraw, SOURCE_NAME)
|
||||
|
||||
M.show_debug_info = function(state)
|
||||
print(vim.inspect(state))
|
||||
end
|
||||
|
||||
M.jump_to_symbol = function(state, node)
|
||||
node = node or state.tree:get_node()
|
||||
if node:get_depth() == 1 then
|
||||
return
|
||||
end
|
||||
vim.api.nvim_set_current_win(state.lsp_winid)
|
||||
vim.api.nvim_set_current_buf(state.lsp_bufnr)
|
||||
local symbol_loc = node.extra.selection_range.start
|
||||
vim.api.nvim_win_set_cursor(state.lsp_winid, { symbol_loc[1] + 1, symbol_loc[2] })
|
||||
end
|
||||
|
||||
M.rename = function(state)
|
||||
local node = state.tree:get_node()
|
||||
if node:get_depth() == 1 then
|
||||
return
|
||||
end
|
||||
local old_name = node.name
|
||||
|
||||
local callback = function(new_name)
|
||||
if not new_name or new_name == "" or new_name == old_name then
|
||||
return
|
||||
end
|
||||
M.jump_to_symbol(state, node)
|
||||
vim.lsp.buf.rename(new_name)
|
||||
M.refresh(state)
|
||||
end
|
||||
local msg = string.format('Enter new name for "%s":', old_name)
|
||||
inputs.input(msg, old_name, callback)
|
||||
end
|
||||
|
||||
M.open = M.jump_to_symbol
|
||||
|
||||
M.filter_on_submit = function(state)
|
||||
filters.show_filter(state, true, true)
|
||||
end
|
||||
|
||||
M.filter = function(state)
|
||||
filters.show_filter(state, true)
|
||||
end
|
||||
|
||||
cc._add_common_commands(M, "node") -- common tree commands
|
||||
cc._add_common_commands(M, "^open") -- open commands
|
||||
cc._add_common_commands(M, "^close_window$")
|
||||
cc._add_common_commands(M, "source$") -- source navigation
|
||||
cc._add_common_commands(M, "preview") -- preview
|
||||
cc._add_common_commands(M, "^cancel$") -- cancel
|
||||
cc._add_common_commands(M, "help") -- help commands
|
||||
cc._add_common_commands(M, "with_window_picker$") -- open using window picker
|
||||
cc._add_common_commands(M, "^toggle_auto_expand_width$")
|
||||
|
||||
return M
|
||||
@ -0,0 +1,41 @@
|
||||
-- This file contains the built-in components. Each componment is a function
|
||||
-- that takes the following arguments:
|
||||
-- config: A table containing the configuration provided by the user
|
||||
-- when declaring this component in their renderer config.
|
||||
-- node: A NuiNode object for the currently focused node.
|
||||
-- state: The current state of the source providing the items.
|
||||
--
|
||||
-- The function should return either a table, or a list of tables, each of which
|
||||
-- contains the following keys:
|
||||
-- text: The text to display for this item.
|
||||
-- highlight: The highlight group to apply to this text.
|
||||
|
||||
local highlights = require("neo-tree.ui.highlights")
|
||||
local common = require("neo-tree.sources.common.components")
|
||||
|
||||
local M = {}
|
||||
|
||||
M.icon = function(config, node, state)
|
||||
return {
|
||||
text = node:get_depth() == 1 and "" or node.extra.kind.icon,
|
||||
highlight = node.extra and node.extra.kind.hl or highlights.FILE_NAME,
|
||||
}
|
||||
end
|
||||
|
||||
M.kind_icon = M.icon
|
||||
|
||||
M.kind_name = function(config, node, state)
|
||||
return {
|
||||
text = node:get_depth() == 1 and "" or node.extra.kind.name,
|
||||
highlight = node.extra and node.extra.kind.hl or highlights.FILE_NAME,
|
||||
}
|
||||
end
|
||||
|
||||
M.name = function(config, node, state)
|
||||
return {
|
||||
text = node.name,
|
||||
highlight = node.extra and node.extra.kind.hl or highlights.FILE_NAME,
|
||||
}
|
||||
end
|
||||
|
||||
return vim.tbl_deep_extend("force", common, M)
|
||||
@ -0,0 +1,112 @@
|
||||
--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 manager = require("neo-tree.sources.manager")
|
||||
local events = require("neo-tree.events")
|
||||
local utils = require("neo-tree.utils")
|
||||
local symbols = require("neo-tree.sources.document_symbols.lib.symbols_utils")
|
||||
local renderer = require("neo-tree.ui.renderer")
|
||||
|
||||
local M = {
|
||||
name = "document_symbols",
|
||||
display_name = " Symbols ",
|
||||
}
|
||||
|
||||
local get_state = function()
|
||||
return manager.get_state(M.name)
|
||||
end
|
||||
|
||||
---Refresh the source with debouncing
|
||||
---@param args { afile: string }
|
||||
local refresh_debounced = function(args)
|
||||
if utils.is_real_file(args.afile) == false then
|
||||
return
|
||||
end
|
||||
utils.debounce(
|
||||
"document_symbols_refresh",
|
||||
utils.wrap(manager.refresh, M.name),
|
||||
100,
|
||||
utils.debounce_strategy.CALL_LAST_ONLY
|
||||
)
|
||||
end
|
||||
|
||||
---Internal function to follow the cursor
|
||||
local follow_symbol = function()
|
||||
local state = get_state()
|
||||
if state.lsp_bufnr ~= vim.api.nvim_get_current_buf() then
|
||||
return
|
||||
end
|
||||
local cursor = vim.api.nvim_win_get_cursor(state.lsp_winid)
|
||||
local node_id = symbols.get_symbol_by_loc(state.tree, { cursor[1] - 1, cursor[2] })
|
||||
if #node_id > 0 then
|
||||
renderer.focus_node(state, node_id, true)
|
||||
end
|
||||
end
|
||||
|
||||
---Follow the cursor with debouncing
|
||||
---@param args { afile: string }
|
||||
local follow_debounced = function(args)
|
||||
if utils.is_real_file(args.afile) == false then
|
||||
return
|
||||
end
|
||||
utils.debounce(
|
||||
"document_symbols_follow",
|
||||
utils.wrap(follow_symbol, args.afile),
|
||||
100,
|
||||
utils.debounce_strategy.CALL_LAST_ONLY
|
||||
)
|
||||
end
|
||||
|
||||
---Navigate to the given path.
|
||||
M.navigate = function(state, path, path_to_reveal, callback, async)
|
||||
state.lsp_winid, _ = utils.get_appropriate_window(state)
|
||||
state.lsp_bufnr = vim.api.nvim_win_get_buf(state.lsp_winid)
|
||||
state.path = vim.api.nvim_buf_get_name(state.lsp_bufnr)
|
||||
|
||||
symbols.render_symbols(state)
|
||||
|
||||
if type(callback) == "function" then
|
||||
vim.schedule(callback)
|
||||
end
|
||||
end
|
||||
|
||||
---Configures the plugin, should be called before the plugin is used.
|
||||
---@param config table Configuration table containing any keys that the user
|
||||
---wants to change from the defaults. May be empty to accept default values.
|
||||
M.setup = function(config, global_config)
|
||||
symbols.setup(config)
|
||||
|
||||
if config.before_render then
|
||||
manager.subscribe(M.name, {
|
||||
event = events.BEFORE_RENDER,
|
||||
handler = function(state)
|
||||
local this_state = get_state()
|
||||
if state == this_state then
|
||||
config.before_render(this_state)
|
||||
end
|
||||
end,
|
||||
})
|
||||
end
|
||||
|
||||
local refresh_events = {
|
||||
events.VIM_BUFFER_ENTER,
|
||||
events.VIM_INSERT_LEAVE,
|
||||
events.VIM_TEXT_CHANGED_NORMAL,
|
||||
}
|
||||
for _, event in ipairs(refresh_events) do
|
||||
manager.subscribe(M.name, {
|
||||
event = event,
|
||||
handler = refresh_debounced,
|
||||
})
|
||||
end
|
||||
|
||||
if config.follow_cursor then
|
||||
manager.subscribe(M.name, {
|
||||
event = events.VIM_CURSOR_MOVED,
|
||||
handler = follow_debounced,
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,88 @@
|
||||
---Utilities function to filter the LSP servers
|
||||
local utils = require("neo-tree.utils")
|
||||
|
||||
---@alias LspRespRaw table<integer, { result: LspRespNode }>
|
||||
local M = {}
|
||||
|
||||
---@alias FilterFn fun(client_name: string): boolean
|
||||
|
||||
---Filter clients
|
||||
---@param filter_type "first" | "all"
|
||||
---@param filter_fn FilterFn
|
||||
---@param resp LspRespRaw
|
||||
---@return table<string, LspRespNode>
|
||||
local filter_clients = function(filter_type, filter_fn, resp)
|
||||
if resp == nil or type(resp) ~= "table" then
|
||||
return {}
|
||||
end
|
||||
filter_fn = filter_fn or function(client_name)
|
||||
return true
|
||||
end
|
||||
|
||||
local result = {}
|
||||
for client_id, client_resp in pairs(resp) do
|
||||
local client_name = vim.lsp.get_client_by_id(client_id).name
|
||||
if filter_fn(client_name) and client_resp.result ~= nil then
|
||||
result[client_name] = client_resp.result
|
||||
if filter_type ~= "all" then
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
---Filter only allowed clients
|
||||
---@param allow_only string[] the list of clients to keep
|
||||
---@return FilterFn
|
||||
local allow_only = function(allow_only)
|
||||
return function(client_name)
|
||||
return vim.tbl_contains(allow_only, client_name)
|
||||
end
|
||||
end
|
||||
|
||||
---Ignore clients
|
||||
---@param ignore string[] the list of clients to remove
|
||||
---@return FilterFn
|
||||
local ignore = function(ignore)
|
||||
return function(client_name)
|
||||
return not vim.tbl_contains(ignore, client_name)
|
||||
end
|
||||
end
|
||||
|
||||
---Main entry point for the filter
|
||||
---@param resp LspRespRaw
|
||||
---@return table<string, LspRespNode>
|
||||
M.filter_resp = function(resp)
|
||||
return {}
|
||||
end
|
||||
|
||||
---Setup the filter accordingly to the config
|
||||
---@see neo-tree-document-symbols-source for more details on options that the filter accepts
|
||||
---@param cfg_flt "first" | "all" | { type: "first" | "all", fn: FilterFn, allow_only: string[], ignore: string[] }
|
||||
M.setup = function(cfg_flt)
|
||||
local filter_type = "first"
|
||||
local filter_fn = nil
|
||||
|
||||
if type(cfg_flt) == "table" then
|
||||
if cfg_flt.type == "all" then
|
||||
filter_type = "all"
|
||||
end
|
||||
|
||||
if cfg_flt.fn ~= nil then
|
||||
filter_fn = cfg_flt.fn
|
||||
elseif cfg_flt.allow_only then
|
||||
filter_fn = allow_only(cfg_flt.allow_only)
|
||||
elseif cfg_flt.ignore then
|
||||
filter_fn = ignore(cfg_flt.ignore)
|
||||
end
|
||||
elseif cfg_flt == "all" then
|
||||
filter_type = "all"
|
||||
end
|
||||
|
||||
M.filter_resp = function(resp)
|
||||
return filter_clients(filter_type, filter_fn, resp)
|
||||
end
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,62 @@
|
||||
---Helper module to render symbols' kinds
|
||||
---Need to be initialized by calling M.setup()
|
||||
local M = {}
|
||||
|
||||
local kinds_id_to_name = {
|
||||
[0] = "Root",
|
||||
[1] = "File",
|
||||
[2] = "Module",
|
||||
[3] = "Namespace",
|
||||
[4] = "Package",
|
||||
[5] = "Class",
|
||||
[6] = "Method",
|
||||
[7] = "Property",
|
||||
[8] = "Field",
|
||||
[9] = "Constructor",
|
||||
[10] = "Enum",
|
||||
[11] = "Interface",
|
||||
[12] = "Function",
|
||||
[13] = "Variable",
|
||||
[14] = "Constant",
|
||||
[15] = "String",
|
||||
[16] = "Number",
|
||||
[17] = "Boolean",
|
||||
[18] = "Array",
|
||||
[19] = "Object",
|
||||
[20] = "Key",
|
||||
[21] = "Null",
|
||||
[22] = "EnumMember",
|
||||
[23] = "Struct",
|
||||
[24] = "Event",
|
||||
[25] = "Operator",
|
||||
[26] = "TypeParameter",
|
||||
}
|
||||
|
||||
local kinds_map = {}
|
||||
|
||||
---Get how the kind with kind_id should be rendered
|
||||
---@param kind_id integer the kind_id to be render
|
||||
---@return table res of the form { name = kind_display_name, icon = kind_icon, hl = kind_hl }
|
||||
M.get_kind = function(kind_id)
|
||||
local kind_name = kinds_id_to_name[kind_id]
|
||||
return vim.tbl_extend(
|
||||
"force",
|
||||
{ name = kind_name or ("Unknown: " .. kind_id), icon = "?", hl = "" },
|
||||
kind_name and (kinds_map[kind_name] or {}) or kinds_map["Unknown"]
|
||||
)
|
||||
end
|
||||
|
||||
---Setup the module with custom kinds
|
||||
---@param custom_kinds table additional kinds, should be of the form { [kind_id] = kind_name }
|
||||
---@param kinds_display table mapping of kind_name to corresponding display name, icon and hl group
|
||||
--- { [kind_name] = {
|
||||
--- name = kind_display_name,
|
||||
--- icon = kind_icon,
|
||||
--- hl = kind_hl
|
||||
--- }, }
|
||||
M.setup = function(custom_kinds, kinds_display)
|
||||
kinds_id_to_name = vim.tbl_deep_extend("force", kinds_id_to_name, custom_kinds or {})
|
||||
kinds_map = kinds_display
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,210 @@
|
||||
---Utilities functions for the document_symbols source
|
||||
local renderer = require("neo-tree.ui.renderer")
|
||||
local filters = require("neo-tree.sources.document_symbols.lib.client_filters")
|
||||
local kinds = require("neo-tree.sources.document_symbols.lib.kinds")
|
||||
|
||||
local M = {}
|
||||
|
||||
---@alias Loc integer[] a location in a buffer {row, col}, 0-indexed
|
||||
---@alias LocRange { start: Loc, ["end"]: Loc } a range consisting of two loc
|
||||
|
||||
---@class SymbolExtra
|
||||
---@field bufnr integer the buffer containing the symbols,
|
||||
---@field kind string the kind of each symbol
|
||||
---@field selection_range LocRange the symbol's location
|
||||
---@field position Loc start of symbol's definition
|
||||
---@field end_position Loc start of symbol's definition
|
||||
|
||||
---@class SymbolNode see
|
||||
---@field id string
|
||||
---@field name string name of symbol
|
||||
---@field path string buffer path - should all be the same
|
||||
---@field type "root"|"symbol"
|
||||
---@field children SymbolNode[]
|
||||
---@field extra SymbolExtra additional info
|
||||
|
||||
---@alias LspLoc { line: integer, character: integer}
|
||||
---@alias LspRange { start : LspLoc, ["end"]: LspLoc }
|
||||
---@class LspRespNode
|
||||
---@field name string
|
||||
---@field detail string?
|
||||
---@field kind integer
|
||||
---@field tags any
|
||||
---@field deprecated boolean?
|
||||
---@field range LspRange
|
||||
---@field selectionRange LspRange
|
||||
---@field children LspRespNode[]
|
||||
|
||||
---Parse the LspRange
|
||||
---@param range LspRange the LspRange object to parse
|
||||
---@return LocRange range the parsed range
|
||||
local parse_range = function(range)
|
||||
return {
|
||||
start = { range.start.line, range.start.character },
|
||||
["end"] = { range["end"].line, range["end"].character },
|
||||
}
|
||||
end
|
||||
|
||||
---Compare two tuples of length 2 by first - second elements
|
||||
---@param a Loc
|
||||
---@param b Loc
|
||||
---@return boolean
|
||||
local loc_less_than = function(a, b)
|
||||
if a[1] < b[1] then
|
||||
return true
|
||||
elseif a[1] == b[1] then
|
||||
return a[2] <= b[2]
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
---Check whether loc is contained in range, i.e range[1] <= loc <= range[2]
|
||||
---@param loc Loc
|
||||
---@param range LocRange
|
||||
---@return boolean
|
||||
M.is_loc_in_range = function(loc, range)
|
||||
return loc_less_than(range[1], loc) and loc_less_than(loc, range[2])
|
||||
end
|
||||
|
||||
---Get the the current symbol under the cursor
|
||||
---@param tree any the Nui symbol tree
|
||||
---@param loc Loc the cursor location {row, col} (0-index)
|
||||
---@return string node_id
|
||||
M.get_symbol_by_loc = function(tree, loc)
|
||||
local function dfs(node)
|
||||
local node_id = node:get_id()
|
||||
if node:has_children() then
|
||||
for _, child in ipairs(tree:get_nodes(node_id)) do
|
||||
if M.is_loc_in_range(loc, { child.extra.position, child.extra.end_position }) then
|
||||
return dfs(child)
|
||||
end
|
||||
end
|
||||
end
|
||||
return node_id
|
||||
end
|
||||
|
||||
for _, root in ipairs(tree:get_nodes()) do
|
||||
local node_id = dfs(root)
|
||||
if node_id ~= root:get_id() then
|
||||
return node_id
|
||||
end
|
||||
end
|
||||
return ""
|
||||
end
|
||||
|
||||
---Parse the LSP response into a tree. Each node on the tree follows
|
||||
---the same structure as a NuiTree node, with the extra field
|
||||
---containing additional information.
|
||||
---@param resp_node LspRespNode the LSP response node
|
||||
---@param id string the id of the current node
|
||||
---@return SymbolNode symb_node the parsed tree
|
||||
local function parse_resp(resp_node, id, state, parent_search_path)
|
||||
-- parse all children
|
||||
local children = {}
|
||||
local search_path = parent_search_path .. "/" .. resp_node.name
|
||||
for i, child in ipairs(resp_node.children or {}) do
|
||||
local child_node = parse_resp(child, id .. "." .. i, state, search_path)
|
||||
table.insert(children, child_node)
|
||||
end
|
||||
|
||||
-- parse current node
|
||||
local preview_range = parse_range(resp_node.range)
|
||||
local symb_node = {
|
||||
id = id,
|
||||
name = resp_node.name,
|
||||
type = "symbol",
|
||||
path = state.path,
|
||||
children = children,
|
||||
extra = {
|
||||
bufnr = state.lsp_bufnr,
|
||||
kind = kinds.get_kind(resp_node.kind),
|
||||
selection_range = parse_range(resp_node.selectionRange),
|
||||
search_path = search_path,
|
||||
-- detail = resp_node.detail,
|
||||
position = preview_range.start,
|
||||
end_position = preview_range["end"],
|
||||
},
|
||||
}
|
||||
return symb_node
|
||||
end
|
||||
|
||||
---Callback function for lsp request
|
||||
---@param lsp_resp LspRespRaw the response of the lsp client
|
||||
---@param state table the state of the source
|
||||
local on_lsp_resp = function(lsp_resp, state)
|
||||
if lsp_resp == nil or type(lsp_resp) ~= "table" then
|
||||
return
|
||||
end
|
||||
|
||||
-- filter the response to get only the desired LSP
|
||||
local resp = filters.filter_resp(lsp_resp)
|
||||
|
||||
local bufname = state.path
|
||||
local items = {}
|
||||
|
||||
-- parse each client's response
|
||||
for client_name, client_result in pairs(resp) do
|
||||
local symbol_list = {}
|
||||
for i, resp_node in ipairs(client_result) do
|
||||
table.insert(symbol_list, parse_resp(resp_node, #items .. "." .. i, state, "/"))
|
||||
end
|
||||
|
||||
-- add the parsed response to the tree
|
||||
local splits = vim.split(bufname, "/")
|
||||
local filename = splits[#splits]
|
||||
table.insert(items, {
|
||||
id = "" .. #items,
|
||||
name = string.format("SYMBOLS (%s) in %s", client_name, filename),
|
||||
path = bufname,
|
||||
type = "root",
|
||||
children = symbol_list,
|
||||
extra = { kind = kinds.get_kind(0), search_path = "/" },
|
||||
})
|
||||
end
|
||||
renderer.show_nodes(items, state)
|
||||
end
|
||||
|
||||
M.render_symbols = function(state)
|
||||
local bufnr = state.lsp_bufnr
|
||||
local bufname = state.path
|
||||
|
||||
-- if no client found, terminate
|
||||
local client_found = false
|
||||
for _, client in pairs(vim.lsp.get_active_clients({ bufnr = bufnr })) do
|
||||
if client.server_capabilities.documentSymbolProvider then
|
||||
client_found = true
|
||||
break
|
||||
end
|
||||
end
|
||||
if not client_found then
|
||||
local splits = vim.split(bufname, "/")
|
||||
renderer.show_nodes({
|
||||
{
|
||||
id = "0",
|
||||
name = "No client found for " .. splits[#splits],
|
||||
path = bufname,
|
||||
type = "root",
|
||||
children = {},
|
||||
extra = { kind = kinds.get_kind(0), search_path = "/" },
|
||||
},
|
||||
}, state)
|
||||
return
|
||||
end
|
||||
|
||||
-- client found
|
||||
vim.lsp.buf_request_all(
|
||||
bufnr,
|
||||
"textDocument/documentSymbol",
|
||||
{ textDocument = vim.lsp.util.make_text_document_params(bufnr) },
|
||||
function(resp)
|
||||
on_lsp_resp(resp, state)
|
||||
end
|
||||
)
|
||||
end
|
||||
|
||||
M.setup = function(config)
|
||||
filters.setup(config.client_filters)
|
||||
kinds.setup(config.custom_kinds, config.kinds)
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,261 @@
|
||||
--This file should contain all commands meant to be used by mappings.
|
||||
|
||||
local cc = require("neo-tree.sources.common.commands")
|
||||
local fs = require("neo-tree.sources.filesystem")
|
||||
local utils = require("neo-tree.utils")
|
||||
local filter = require("neo-tree.sources.filesystem.lib.filter")
|
||||
local renderer = require("neo-tree.ui.renderer")
|
||||
local log = require("neo-tree.log")
|
||||
|
||||
local M = {}
|
||||
local refresh = function(state)
|
||||
fs._navigate_internal(state, nil, nil, nil, false)
|
||||
end
|
||||
|
||||
local redraw = function(state)
|
||||
renderer.redraw(state)
|
||||
end
|
||||
|
||||
M.add = function(state)
|
||||
cc.add(state, utils.wrap(fs.show_new_children, state))
|
||||
end
|
||||
|
||||
M.add_directory = function(state)
|
||||
cc.add_directory(state, utils.wrap(fs.show_new_children, state))
|
||||
end
|
||||
|
||||
M.clear_filter = function(state)
|
||||
fs.reset_search(state, true)
|
||||
end
|
||||
|
||||
M.copy = function(state)
|
||||
cc.copy(state, utils.wrap(fs.focus_destination_children, state))
|
||||
end
|
||||
|
||||
---Marks node as copied, so that it can be pasted somewhere else.
|
||||
M.copy_to_clipboard = function(state)
|
||||
cc.copy_to_clipboard(state, utils.wrap(redraw, state))
|
||||
end
|
||||
|
||||
M.copy_to_clipboard_visual = function(state, selected_nodes)
|
||||
cc.copy_to_clipboard_visual(state, selected_nodes, utils.wrap(redraw, state))
|
||||
end
|
||||
|
||||
---Marks node as cut, so that it can be pasted (moved) somewhere else.
|
||||
M.cut_to_clipboard = function(state)
|
||||
cc.cut_to_clipboard(state, utils.wrap(redraw, state))
|
||||
end
|
||||
|
||||
M.cut_to_clipboard_visual = function(state, selected_nodes)
|
||||
cc.cut_to_clipboard_visual(state, selected_nodes, utils.wrap(redraw, state))
|
||||
end
|
||||
|
||||
M.move = function(state)
|
||||
cc.move(state, utils.wrap(fs.focus_destination_children, state))
|
||||
end
|
||||
|
||||
---Pastes all items from the clipboard to the current directory.
|
||||
M.paste_from_clipboard = function(state)
|
||||
cc.paste_from_clipboard(state, utils.wrap(fs.show_new_children, state))
|
||||
end
|
||||
|
||||
M.delete = function(state)
|
||||
cc.delete(state, utils.wrap(refresh, state))
|
||||
end
|
||||
|
||||
M.delete_visual = function(state, selected_nodes)
|
||||
cc.delete_visual(state, selected_nodes, utils.wrap(refresh, state))
|
||||
end
|
||||
|
||||
M.expand_all_nodes = function(state, node)
|
||||
if node == nil then
|
||||
node = state.tree:get_node(state.path)
|
||||
end
|
||||
cc.expand_all_nodes(state, node, fs.prefetcher)
|
||||
end
|
||||
|
||||
---Shows the filter input, which will filter the tree.
|
||||
M.filter_as_you_type = function(state)
|
||||
filter.show_filter(state, true)
|
||||
end
|
||||
|
||||
---Shows the filter input, which will filter the tree.
|
||||
M.filter_on_submit = function(state)
|
||||
filter.show_filter(state, false)
|
||||
end
|
||||
|
||||
---Shows the filter input in fuzzy finder mode.
|
||||
M.fuzzy_finder = function(state)
|
||||
filter.show_filter(state, true, true)
|
||||
end
|
||||
|
||||
---Shows the filter input in fuzzy finder mode.
|
||||
M.fuzzy_finder_directory = function(state)
|
||||
filter.show_filter(state, true, "directory")
|
||||
end
|
||||
|
||||
---Shows the filter input in fuzzy sorter
|
||||
M.fuzzy_sorter = function(state)
|
||||
filter.show_filter(state, true, true, true)
|
||||
end
|
||||
|
||||
---Shows the filter input in fuzzy sorter with only directories
|
||||
M.fuzzy_sorter_directory = function(state)
|
||||
filter.show_filter(state, true, "directory", true)
|
||||
end
|
||||
|
||||
---Navigate up one level.
|
||||
M.navigate_up = function(state)
|
||||
local parent_path, _ = utils.split_path(state.path)
|
||||
if not utils.truthy(parent_path) then
|
||||
return
|
||||
end
|
||||
local path_to_reveal = nil
|
||||
local node = state.tree:get_node()
|
||||
if node then
|
||||
path_to_reveal = node:get_id()
|
||||
end
|
||||
if state.search_pattern then
|
||||
fs.reset_search(state, false)
|
||||
end
|
||||
log.debug("Changing directory to:", parent_path)
|
||||
fs._navigate_internal(state, parent_path, path_to_reveal, nil, false)
|
||||
end
|
||||
|
||||
local focus_next_git_modified = function(state, reverse)
|
||||
local node = state.tree:get_node()
|
||||
local current_path = node:get_id()
|
||||
local g = state.git_status_lookup
|
||||
if not utils.truthy(g) then
|
||||
return
|
||||
end
|
||||
local paths = { current_path }
|
||||
for path, status in pairs(g) do
|
||||
if path ~= current_path and status and status ~= "!!" then
|
||||
--don't include files not in the current working directory
|
||||
if utils.is_subpath(state.path, path) then
|
||||
table.insert(paths, path)
|
||||
end
|
||||
end
|
||||
end
|
||||
local sorted_paths = utils.sort_by_tree_display(paths)
|
||||
if reverse then
|
||||
sorted_paths = utils.reverse_list(sorted_paths)
|
||||
end
|
||||
|
||||
local is_file = function(path)
|
||||
local success, stats = pcall(vim.loop.fs_stat, path)
|
||||
return (success and stats and stats.type ~= "directory")
|
||||
end
|
||||
|
||||
local passed = false
|
||||
local target = nil
|
||||
for _, path in ipairs(sorted_paths) do
|
||||
if target == nil and is_file(path) then
|
||||
target = path
|
||||
end
|
||||
if passed then
|
||||
if is_file(path) then
|
||||
target = path
|
||||
break
|
||||
end
|
||||
elseif path == current_path then
|
||||
passed = true
|
||||
end
|
||||
end
|
||||
|
||||
local existing = state.tree:get_node(target)
|
||||
if existing then
|
||||
renderer.focus_node(state, target)
|
||||
else
|
||||
fs.navigate(state, state.path, target, nil, false)
|
||||
end
|
||||
end
|
||||
|
||||
M.next_git_modified = function(state)
|
||||
focus_next_git_modified(state, false)
|
||||
end
|
||||
|
||||
M.prev_git_modified = function(state)
|
||||
focus_next_git_modified(state, true)
|
||||
end
|
||||
|
||||
M.open = function(state)
|
||||
cc.open(state, utils.wrap(fs.toggle_directory, state))
|
||||
end
|
||||
M.open_split = function(state)
|
||||
cc.open_split(state, utils.wrap(fs.toggle_directory, state))
|
||||
end
|
||||
M.open_rightbelow_vs = function(state)
|
||||
cc.open_rightbelow_vs(state, utils.wrap(fs.toggle_directory, state))
|
||||
end
|
||||
M.open_leftabove_vs = function(state)
|
||||
cc.open_leftabove_vs(state, utils.wrap(fs.toggle_directory, state))
|
||||
end
|
||||
M.open_vsplit = function(state)
|
||||
cc.open_vsplit(state, utils.wrap(fs.toggle_directory, state))
|
||||
end
|
||||
M.open_tabnew = function(state)
|
||||
cc.open_tabnew(state, utils.wrap(fs.toggle_directory, state))
|
||||
end
|
||||
M.open_drop = function(state)
|
||||
cc.open_drop(state, utils.wrap(fs.toggle_directory, state))
|
||||
end
|
||||
M.open_tab_drop = function(state)
|
||||
cc.open_tab_drop(state, utils.wrap(fs.toggle_directory, state))
|
||||
end
|
||||
|
||||
M.open_with_window_picker = function(state)
|
||||
cc.open_with_window_picker(state, utils.wrap(fs.toggle_directory, state))
|
||||
end
|
||||
M.split_with_window_picker = function(state)
|
||||
cc.split_with_window_picker(state, utils.wrap(fs.toggle_directory, state))
|
||||
end
|
||||
M.vsplit_with_window_picker = function(state)
|
||||
cc.vsplit_with_window_picker(state, utils.wrap(fs.toggle_directory, state))
|
||||
end
|
||||
|
||||
M.refresh = refresh
|
||||
|
||||
M.rename = function(state)
|
||||
cc.rename(state, utils.wrap(refresh, state))
|
||||
end
|
||||
|
||||
M.set_root = function(state)
|
||||
if state.search_pattern then
|
||||
fs.reset_search(state, false)
|
||||
end
|
||||
|
||||
local node = state.tree:get_node()
|
||||
while node and node.type ~= "directory" do
|
||||
local parent_id = node:get_parent_id()
|
||||
node = parent_id and state.tree:get_node(parent_id) or nil
|
||||
end
|
||||
|
||||
if not node then
|
||||
return
|
||||
end
|
||||
|
||||
fs._navigate_internal(state, node:get_id(), nil, nil, false)
|
||||
end
|
||||
|
||||
---Toggles whether hidden files are shown or not.
|
||||
M.toggle_hidden = function(state)
|
||||
state.filtered_items.visible = not state.filtered_items.visible
|
||||
log.info("Toggling hidden files: " .. tostring(state.filtered_items.visible))
|
||||
refresh(state)
|
||||
end
|
||||
|
||||
---Toggles whether the tree is filtered by gitignore or not.
|
||||
M.toggle_gitignore = function(state)
|
||||
log.warn("`toggle_gitignore` has been removed, running toggle_hidden instead.")
|
||||
M.toggle_hidden(state)
|
||||
end
|
||||
|
||||
M.toggle_node = function(state)
|
||||
cc.toggle_node(state, utils.wrap(fs.toggle_directory, state))
|
||||
end
|
||||
|
||||
cc._add_common_commands(M)
|
||||
|
||||
return M
|
||||
@ -0,0 +1,40 @@
|
||||
-- This file contains the built-in components. Each componment is a function
|
||||
-- that takes the following arguments:
|
||||
-- config: A table containing the configuration provided by the user
|
||||
-- when declaring this component in their renderer config.
|
||||
-- node: A NuiNode object for the currently focused node.
|
||||
-- state: The current state of the source providing the items.
|
||||
--
|
||||
-- The function should return either a table, or a list of tables, each of which
|
||||
-- contains the following keys:
|
||||
-- text: The text to display for this item.
|
||||
-- highlight: The highlight group to apply to this text.
|
||||
|
||||
local highlights = require("neo-tree.ui.highlights")
|
||||
local common = require("neo-tree.sources.common.components")
|
||||
local utils = require("neo-tree.utils")
|
||||
|
||||
local M = {}
|
||||
|
||||
M.current_filter = function(config, node, state)
|
||||
local filter = node.search_pattern or ""
|
||||
if filter == "" then
|
||||
return {}
|
||||
end
|
||||
return {
|
||||
{
|
||||
text = "Find",
|
||||
highlight = highlights.DIM_TEXT,
|
||||
},
|
||||
{
|
||||
text = string.format('"%s"', filter),
|
||||
highlight = config.highlight or highlights.FILTER_TERM,
|
||||
},
|
||||
{
|
||||
text = "in",
|
||||
highlight = highlights.DIM_TEXT,
|
||||
},
|
||||
}
|
||||
end
|
||||
|
||||
return vim.tbl_deep_extend("force", common, M)
|
||||
@ -0,0 +1,434 @@
|
||||
--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 events = require("neo-tree.events")
|
||||
local log = require("neo-tree.log")
|
||||
local manager = require("neo-tree.sources.manager")
|
||||
local git = require("neo-tree.git")
|
||||
local glob = require("neo-tree.sources.filesystem.lib.globtopattern")
|
||||
|
||||
local M = {
|
||||
name = "filesystem",
|
||||
display_name = " Files ",
|
||||
}
|
||||
|
||||
local wrap = function(func)
|
||||
return utils.wrap(func, M.name)
|
||||
end
|
||||
|
||||
local get_state = function(tabid)
|
||||
return manager.get_state(M.name, tabid)
|
||||
end
|
||||
|
||||
local follow_internal = function(callback, force_show, async)
|
||||
log.trace("follow called")
|
||||
local state = get_state()
|
||||
if vim.bo.filetype == "neo-tree" or vim.bo.filetype == "neo-tree-popup" then
|
||||
return false
|
||||
end
|
||||
local path_to_reveal = utils.normalize_path(manager.get_path_to_reveal() or "")
|
||||
if not utils.truthy(path_to_reveal) then
|
||||
return false
|
||||
end
|
||||
---@cast path_to_reveal string
|
||||
|
||||
if state.current_position == "float" then
|
||||
return false
|
||||
end
|
||||
if not state.path then
|
||||
return false
|
||||
end
|
||||
local window_exists = renderer.window_exists(state)
|
||||
if window_exists then
|
||||
local node = state.tree and state.tree:get_node()
|
||||
if node then
|
||||
if node:get_id() == path_to_reveal then
|
||||
-- already focused
|
||||
return false
|
||||
end
|
||||
end
|
||||
else
|
||||
if not force_show then
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
local is_in_path = path_to_reveal:sub(1, #state.path) == state.path
|
||||
if not is_in_path then
|
||||
return false
|
||||
end
|
||||
|
||||
log.debug("follow file: ", path_to_reveal)
|
||||
local show_only_explicitly_opened = function()
|
||||
state.explicitly_opened_directories = state.explicitly_opened_directories or {}
|
||||
local expanded_nodes = renderer.get_expanded_nodes(state.tree)
|
||||
local state_changed = false
|
||||
for _, id in ipairs(expanded_nodes) do
|
||||
if not state.explicitly_opened_directories[id] then
|
||||
if path_to_reveal:sub(1, #id) == id then
|
||||
state.explicitly_opened_directories[id] = state.follow_current_file.leave_dirs_open
|
||||
else
|
||||
local node = state.tree:get_node(id)
|
||||
if node then
|
||||
node:collapse()
|
||||
state_changed = true
|
||||
end
|
||||
end
|
||||
end
|
||||
if state_changed then
|
||||
renderer.redraw(state)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
fs_scan.get_items(state, nil, path_to_reveal, function()
|
||||
show_only_explicitly_opened()
|
||||
renderer.focus_node(state, path_to_reveal, true)
|
||||
if type(callback) == "function" then
|
||||
callback()
|
||||
end
|
||||
end, async)
|
||||
return true
|
||||
end
|
||||
|
||||
M.follow = function(callback, force_show)
|
||||
if vim.fn.bufname(0) == "COMMIT_EDITMSG" then
|
||||
return false
|
||||
end
|
||||
if utils.is_floating() then
|
||||
return false
|
||||
end
|
||||
utils.debounce("neo-tree-follow", function()
|
||||
return follow_internal(callback, force_show)
|
||||
end, 100, utils.debounce_strategy.CALL_LAST_ONLY)
|
||||
end
|
||||
|
||||
M._navigate_internal = function(state, path, path_to_reveal, callback, async)
|
||||
log.trace("navigate_internal", state.current_position, path, path_to_reveal)
|
||||
state.dirty = false
|
||||
local is_search = utils.truthy(state.search_pattern)
|
||||
local path_changed = false
|
||||
if not path and not state.bind_to_cwd then
|
||||
path = state.path
|
||||
end
|
||||
if path == nil then
|
||||
log.debug("navigate_internal: path is nil, using cwd")
|
||||
path = manager.get_cwd(state)
|
||||
end
|
||||
path = utils.normalize_path(path)
|
||||
if path ~= state.path then
|
||||
log.debug("navigate_internal: path changed from ", state.path, " to ", path)
|
||||
state.path = path
|
||||
path_changed = true
|
||||
end
|
||||
|
||||
if path_to_reveal then
|
||||
renderer.position.set(state, path_to_reveal)
|
||||
log.debug("navigate_internal: in path_to_reveal, state.position=", state.position.node_id)
|
||||
fs_scan.get_items(state, nil, path_to_reveal, callback)
|
||||
else
|
||||
local is_current = state.current_position == "current"
|
||||
local follow_file = state.follow_current_file.enabled
|
||||
and not is_search
|
||||
and not is_current
|
||||
and manager.get_path_to_reveal()
|
||||
local handled = false
|
||||
if utils.truthy(follow_file) then
|
||||
handled = follow_internal(callback, true, async)
|
||||
end
|
||||
if not handled then
|
||||
local success, msg = pcall(renderer.position.save, state)
|
||||
if success then
|
||||
log.trace("navigate_internal: position saved")
|
||||
else
|
||||
log.trace("navigate_internal: FAILED to save position: ", msg)
|
||||
end
|
||||
fs_scan.get_items(state, nil, nil, callback, async)
|
||||
end
|
||||
end
|
||||
|
||||
if path_changed and state.bind_to_cwd then
|
||||
manager.set_cwd(state)
|
||||
end
|
||||
local config = require("neo-tree").config
|
||||
if config.enable_git_status and not is_search and config.git_status_async then
|
||||
git.status_async(state.path, state.git_base, config.git_status_async_options)
|
||||
end
|
||||
end
|
||||
|
||||
---Navigate to the given path.
|
||||
---@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.
|
||||
M.navigate = function(state, path, path_to_reveal, callback, async)
|
||||
state._ready = false
|
||||
log.trace("navigate", path, path_to_reveal, async)
|
||||
utils.debounce("filesystem_navigate", function()
|
||||
M._navigate_internal(state, path, path_to_reveal, callback, async)
|
||||
end, 100, utils.debounce_strategy.CALL_FIRST_AND_LAST)
|
||||
end
|
||||
|
||||
M.reset_search = function(state, refresh, open_current_node)
|
||||
log.trace("reset_search")
|
||||
-- Cancel any pending search
|
||||
require("neo-tree.sources.filesystem.lib.filter_external").cancel()
|
||||
-- reset search state
|
||||
state.fuzzy_finder_mode = nil
|
||||
state.use_fzy = nil
|
||||
state.fzy_sort_result_scores = nil
|
||||
state.fzy_sort_file_list_cache = nil
|
||||
state.sort_function_override = nil
|
||||
|
||||
if refresh == nil then
|
||||
refresh = true
|
||||
end
|
||||
if state.open_folders_before_search then
|
||||
state.force_open_folders = vim.deepcopy(state.open_folders_before_search, { noref = 1 })
|
||||
else
|
||||
state.force_open_folders = nil
|
||||
end
|
||||
state.search_pattern = nil
|
||||
state.open_folders_before_search = nil
|
||||
if open_current_node then
|
||||
local success, node = pcall(state.tree.get_node, state.tree)
|
||||
if success and node then
|
||||
local path = node:get_id()
|
||||
renderer.position.set(state, path)
|
||||
if node.type == "directory" then
|
||||
path = utils.remove_trailing_slash(path)
|
||||
log.trace("opening directory from search: ", path)
|
||||
M.navigate(state, nil, path, function()
|
||||
pcall(renderer.focus_node, state, path, false)
|
||||
end)
|
||||
else
|
||||
utils.open_file(state, path)
|
||||
if
|
||||
refresh
|
||||
and state.current_position ~= "current"
|
||||
and state.current_position ~= "float"
|
||||
then
|
||||
M.navigate(state, nil, path)
|
||||
end
|
||||
end
|
||||
end
|
||||
else
|
||||
if refresh then
|
||||
M.navigate(state)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
M.show_new_children = function(state, node_or_path)
|
||||
local node = node_or_path
|
||||
if node_or_path == nil then
|
||||
node = state.tree:get_node()
|
||||
node_or_path = node:get_id()
|
||||
elseif type(node_or_path) == "string" then
|
||||
node = state.tree:get_node(node_or_path)
|
||||
if node == nil then
|
||||
local parent_path, _ = utils.split_path(node_or_path)
|
||||
node = state.tree:get_node(parent_path)
|
||||
if node == nil then
|
||||
M.navigate(state, nil, node_or_path)
|
||||
return
|
||||
end
|
||||
end
|
||||
else
|
||||
node = node_or_path
|
||||
node_or_path = node:get_id()
|
||||
end
|
||||
|
||||
if node.type ~= "directory" then
|
||||
return
|
||||
end
|
||||
|
||||
M.navigate(state, nil, node_or_path)
|
||||
end
|
||||
|
||||
M.focus_destination_children = function(state, move_from, destination)
|
||||
return M.show_new_children(state, destination)
|
||||
end
|
||||
|
||||
---Configures the plugin, should be called before the plugin is used.
|
||||
---@param config table Configuration table containing any keys that the user
|
||||
--wants to change from the defaults. May be empty to accept default values.
|
||||
M.setup = function(config, global_config)
|
||||
config.filtered_items = config.filtered_items or {}
|
||||
config.enable_git_status = global_config.enable_git_status
|
||||
|
||||
for _, key in ipairs({ "hide_by_pattern", "never_show_by_pattern" }) do
|
||||
local list = config.filtered_items[key]
|
||||
if type(list) == "table" then
|
||||
for i, pattern in ipairs(list) do
|
||||
list[i] = glob.globtopattern(pattern)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
for _, key in ipairs({ "hide_by_name", "always_show", "never_show" }) do
|
||||
local list = config.filtered_items[key]
|
||||
if type(list) == "table" then
|
||||
config.filtered_items[key] = utils.list_to_dict(list)
|
||||
end
|
||||
end
|
||||
|
||||
--Configure events for before_render
|
||||
if config.before_render then
|
||||
--convert to new event system
|
||||
manager.subscribe(M.name, {
|
||||
event = events.BEFORE_RENDER,
|
||||
handler = function(state)
|
||||
local this_state = get_state()
|
||||
if state == this_state then
|
||||
config.before_render(this_state)
|
||||
end
|
||||
end,
|
||||
})
|
||||
elseif global_config.enable_git_status and global_config.git_status_async then
|
||||
manager.subscribe(M.name, {
|
||||
event = events.GIT_STATUS_CHANGED,
|
||||
handler = wrap(manager.git_status_changed),
|
||||
})
|
||||
elseif global_config.enable_git_status then
|
||||
manager.subscribe(M.name, {
|
||||
event = events.BEFORE_RENDER,
|
||||
handler = function(state)
|
||||
local this_state = get_state()
|
||||
if state == this_state then
|
||||
state.git_status_lookup = git.status(state.git_base)
|
||||
end
|
||||
end,
|
||||
})
|
||||
end
|
||||
|
||||
-- Respond to git events from git_status source or Fugitive
|
||||
if global_config.enable_git_status then
|
||||
manager.subscribe(M.name, {
|
||||
event = events.GIT_EVENT,
|
||||
handler = function()
|
||||
manager.refresh(M.name)
|
||||
end,
|
||||
})
|
||||
end
|
||||
|
||||
--Configure event handlers for file changes
|
||||
if config.use_libuv_file_watcher then
|
||||
manager.subscribe(M.name, {
|
||||
event = events.FS_EVENT,
|
||||
handler = wrap(manager.refresh),
|
||||
})
|
||||
else
|
||||
require("neo-tree.sources.filesystem.lib.fs_watch").unwatch_all()
|
||||
if global_config.enable_refresh_on_write then
|
||||
manager.subscribe(M.name, {
|
||||
event = events.VIM_BUFFER_CHANGED,
|
||||
handler = function(arg)
|
||||
local afile = arg.afile or ""
|
||||
if utils.is_real_file(afile) then
|
||||
log.trace("refreshing due to vim_buffer_changed event: ", afile)
|
||||
manager.refresh("filesystem")
|
||||
else
|
||||
log.trace("Ignoring vim_buffer_changed event for non-file: ", afile)
|
||||
end
|
||||
end,
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
--Configure event handlers for cwd changes
|
||||
if config.bind_to_cwd then
|
||||
manager.subscribe(M.name, {
|
||||
event = events.VIM_DIR_CHANGED,
|
||||
handler = wrap(manager.dir_changed),
|
||||
})
|
||||
end
|
||||
|
||||
--Configure event handlers for lsp diagnostic updates
|
||||
if global_config.enable_diagnostics then
|
||||
manager.subscribe(M.name, {
|
||||
event = events.VIM_DIAGNOSTIC_CHANGED,
|
||||
handler = wrap(manager.diagnostics_changed),
|
||||
})
|
||||
end
|
||||
|
||||
--Configure event handlers for modified files
|
||||
if global_config.enable_modified_markers then
|
||||
manager.subscribe(M.name, {
|
||||
event = events.VIM_BUFFER_MODIFIED_SET,
|
||||
handler = wrap(manager.opened_buffers_changed),
|
||||
})
|
||||
end
|
||||
|
||||
if global_config.enable_opened_markers then
|
||||
for _, event in ipairs({ events.VIM_BUFFER_ADDED, events.VIM_BUFFER_DELETED }) do
|
||||
manager.subscribe(M.name, {
|
||||
event = event,
|
||||
handler = wrap(manager.opened_buffers_changed),
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
-- Configure event handler for follow_current_file option
|
||||
if config.follow_current_file.enabled then
|
||||
manager.subscribe(M.name, {
|
||||
event = events.VIM_BUFFER_ENTER,
|
||||
handler = function(args)
|
||||
if utils.is_real_file(args.afile) then
|
||||
M.follow()
|
||||
end
|
||||
end,
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
---Expands or collapses the current node.
|
||||
M.toggle_directory = function(state, node, path_to_reveal, skip_redraw, recursive, callback)
|
||||
local tree = state.tree
|
||||
if not node then
|
||||
node = tree:get_node()
|
||||
end
|
||||
if node.type ~= "directory" then
|
||||
return
|
||||
end
|
||||
state.explicitly_opened_directories = state.explicitly_opened_directories or {}
|
||||
if node.loaded == false then
|
||||
local id = node:get_id()
|
||||
state.explicitly_opened_directories[id] = true
|
||||
renderer.position.set(state, nil)
|
||||
fs_scan.get_items(state, id, path_to_reveal, callback, false, recursive)
|
||||
elseif node:has_children() then
|
||||
local updated = false
|
||||
if node:is_expanded() then
|
||||
updated = node:collapse()
|
||||
state.explicitly_opened_directories[node:get_id()] = false
|
||||
else
|
||||
updated = node:expand()
|
||||
state.explicitly_opened_directories[node:get_id()] = true
|
||||
end
|
||||
if updated and not skip_redraw then
|
||||
renderer.redraw(state)
|
||||
end
|
||||
if path_to_reveal then
|
||||
renderer.focus_node(state, path_to_reveal)
|
||||
end
|
||||
elseif require("neo-tree").config.filesystem.scan_mode == "deep" then
|
||||
node.empty_expanded = not node.empty_expanded
|
||||
renderer.redraw(state)
|
||||
end
|
||||
end
|
||||
|
||||
M.prefetcher = {
|
||||
prefetch = function(state, node)
|
||||
log.debug("Running fs prefetch for: " .. node:get_id())
|
||||
fs_scan.get_dir_items_async(state, node:get_id(), true)
|
||||
end,
|
||||
should_prefetch = function(node)
|
||||
return not node.loaded
|
||||
end,
|
||||
}
|
||||
|
||||
return M
|
||||
@ -0,0 +1,241 @@
|
||||
-- This file holds all code for the search function.
|
||||
|
||||
local vim = vim
|
||||
local Input = require("nui.input")
|
||||
local event = require("nui.utils.autocmd").event
|
||||
local fs = require("neo-tree.sources.filesystem")
|
||||
local popups = require("neo-tree.ui.popups")
|
||||
local renderer = require("neo-tree.ui.renderer")
|
||||
local utils = require("neo-tree.utils")
|
||||
local log = require("neo-tree.log")
|
||||
local manager = require("neo-tree.sources.manager")
|
||||
|
||||
local M = {}
|
||||
|
||||
local cmds = {
|
||||
move_cursor_down = function(state, scroll_padding)
|
||||
renderer.focus_node(state, nil, true, 1, scroll_padding)
|
||||
end,
|
||||
|
||||
move_cursor_up = function(state, scroll_padding)
|
||||
renderer.focus_node(state, nil, true, -1, scroll_padding)
|
||||
vim.cmd("redraw!")
|
||||
end,
|
||||
}
|
||||
|
||||
local function create_input_mapping_handle(cmd, state, scroll_padding)
|
||||
return function()
|
||||
cmd(state, scroll_padding)
|
||||
end
|
||||
end
|
||||
|
||||
M.show_filter = function(state, search_as_you_type, fuzzy_finder_mode, use_fzy)
|
||||
local popup_options
|
||||
local winid = vim.api.nvim_get_current_win()
|
||||
local height = vim.api.nvim_win_get_height(winid)
|
||||
local scroll_padding = 3
|
||||
local popup_msg = "Search:"
|
||||
|
||||
if search_as_you_type then
|
||||
if fuzzy_finder_mode == "directory" then
|
||||
popup_msg = "Filter Directories:"
|
||||
else
|
||||
popup_msg = "Filter:"
|
||||
end
|
||||
end
|
||||
if state.current_position == "float" then
|
||||
scroll_padding = 0
|
||||
local width = vim.fn.winwidth(winid)
|
||||
local row = height - 2
|
||||
vim.api.nvim_win_set_height(winid, row)
|
||||
popup_options = popups.popup_options(popup_msg, width, {
|
||||
relative = "win",
|
||||
winid = winid,
|
||||
position = {
|
||||
row = row,
|
||||
col = 0,
|
||||
},
|
||||
size = width,
|
||||
})
|
||||
else
|
||||
local width = vim.fn.winwidth(0) - 2
|
||||
local row = height - 3
|
||||
popup_options = popups.popup_options(popup_msg, width, {
|
||||
relative = "win",
|
||||
winid = winid,
|
||||
position = {
|
||||
row = row,
|
||||
col = 0,
|
||||
},
|
||||
size = width,
|
||||
})
|
||||
end
|
||||
|
||||
local sort_by_score = function(a, b)
|
||||
-- `state.fzy_sort_result_scores` should be defined in
|
||||
-- `sources.filesystem.lib.filter_external.fzy_sort_files`
|
||||
local result_scores = state.fzy_sort_result_scores or { foo = 0, baz = 0 }
|
||||
local a_score = result_scores[a.path]
|
||||
local b_score = result_scores[b.path]
|
||||
if a_score == nil or b_score == nil then
|
||||
log.debug(string.format([[Fzy: failed to compare %s: %s, %s: %s]], a.path, a_score, b.path, b_score))
|
||||
local config = require("neo-tree").config
|
||||
if config.sort_function ~= nil then
|
||||
return config.sort_function(a, b)
|
||||
end
|
||||
return nil
|
||||
end
|
||||
return a_score > b_score
|
||||
end
|
||||
|
||||
local select_first_file = function()
|
||||
local is_file = function(node)
|
||||
return node.type == "file"
|
||||
end
|
||||
local files = renderer.select_nodes(state.tree, is_file, 1)
|
||||
if #files > 0 then
|
||||
renderer.focus_node(state, files[1]:get_id(), true)
|
||||
end
|
||||
end
|
||||
|
||||
local has_pre_search_folders = utils.truthy(state.open_folders_before_search)
|
||||
if not has_pre_search_folders then
|
||||
log.trace("No search or pre-search folders, recording pre-search folders now")
|
||||
state.open_folders_before_search = renderer.get_expanded_nodes(state.tree)
|
||||
end
|
||||
|
||||
local waiting_for_default_value = utils.truthy(state.search_pattern)
|
||||
local input = Input(popup_options, {
|
||||
prompt = " ",
|
||||
default_value = state.search_pattern,
|
||||
on_submit = function(value)
|
||||
if value == "" then
|
||||
fs.reset_search(state)
|
||||
else
|
||||
if search_as_you_type and fuzzy_finder_mode then
|
||||
fs.reset_search(state, true, true)
|
||||
return
|
||||
end
|
||||
state.search_pattern = value
|
||||
manager.refresh("filesystem", function()
|
||||
-- focus first file
|
||||
local nodes = renderer.get_all_visible_nodes(state.tree)
|
||||
for _, node in ipairs(nodes) do
|
||||
if node.type == "file" then
|
||||
renderer.focus_node(state, node:get_id(), false)
|
||||
break
|
||||
end
|
||||
end
|
||||
end)
|
||||
end
|
||||
end,
|
||||
--this can be bad in a deep folder structure
|
||||
on_change = function(value)
|
||||
if not search_as_you_type then
|
||||
return
|
||||
end
|
||||
-- apparently when a default value is set, on_change fires for every character
|
||||
if waiting_for_default_value then
|
||||
if #value < #state.search_pattern then
|
||||
return
|
||||
else
|
||||
waiting_for_default_value = false
|
||||
end
|
||||
end
|
||||
if value == state.search_pattern then
|
||||
return
|
||||
elseif value == nil then
|
||||
return
|
||||
elseif value == "" then
|
||||
if state.search_pattern == nil then
|
||||
return
|
||||
end
|
||||
log.trace("Resetting search in on_change")
|
||||
local original_open_folders = nil
|
||||
if type(state.open_folders_before_search) == "table" then
|
||||
original_open_folders = vim.deepcopy(state.open_folders_before_search, { noref = 1 })
|
||||
end
|
||||
fs.reset_search(state)
|
||||
state.open_folders_before_search = original_open_folders
|
||||
else
|
||||
log.trace("Setting search in on_change to: " .. value)
|
||||
state.search_pattern = value
|
||||
state.fuzzy_finder_mode = fuzzy_finder_mode
|
||||
if use_fzy then
|
||||
state.sort_function_override = sort_by_score
|
||||
state.use_fzy = true
|
||||
end
|
||||
local callback = select_first_file
|
||||
if fuzzy_finder_mode == "directory" then
|
||||
callback = nil
|
||||
end
|
||||
|
||||
local len = #value
|
||||
local delay = 500
|
||||
if len > 3 then
|
||||
delay = 100
|
||||
elseif len > 2 then
|
||||
delay = 200
|
||||
elseif len > 1 then
|
||||
delay = 400
|
||||
end
|
||||
|
||||
utils.debounce("filesystem_filter", function()
|
||||
fs._navigate_internal(state, nil, nil, callback)
|
||||
end, delay, utils.debounce_strategy.CALL_LAST_ONLY)
|
||||
end
|
||||
end,
|
||||
})
|
||||
|
||||
input:mount()
|
||||
|
||||
local restore_height = vim.schedule_wrap(function()
|
||||
if vim.api.nvim_win_is_valid(winid) then
|
||||
vim.api.nvim_win_set_height(winid, height)
|
||||
end
|
||||
end)
|
||||
input:map("i", "<esc>", function(bufnr)
|
||||
vim.cmd("stopinsert")
|
||||
input:unmount()
|
||||
if fuzzy_finder_mode and utils.truthy(state.search_pattern) then
|
||||
fs.reset_search(state, true)
|
||||
end
|
||||
restore_height()
|
||||
end, { noremap = true })
|
||||
|
||||
input:map("i", "<C-w>", "<C-S-w>", { noremap = true })
|
||||
|
||||
input:on({ event.BufLeave, event.BufDelete }, function()
|
||||
vim.cmd("stopinsert")
|
||||
input:unmount()
|
||||
-- If this was closed due to submit, that function will handle the reset_search
|
||||
vim.defer_fn(function()
|
||||
if fuzzy_finder_mode and utils.truthy(state.search_pattern) then
|
||||
fs.reset_search(state, true)
|
||||
end
|
||||
end, 100)
|
||||
restore_height()
|
||||
end, { once = true })
|
||||
|
||||
if fuzzy_finder_mode then
|
||||
local config = require("neo-tree").config
|
||||
for lhs, cmd_name in pairs(config.filesystem.window.fuzzy_finder_mappings) do
|
||||
local t = type(cmd_name)
|
||||
if t == "string" then
|
||||
local cmd = cmds[cmd_name]
|
||||
if cmd then
|
||||
input:map("i", lhs, create_input_mapping_handle(cmd, state, scroll_padding), { noremap = true })
|
||||
else
|
||||
log.warn(string.format("Invalid command in fuzzy_finder_mappings: %s = %s", lhs, cmd_name))
|
||||
end
|
||||
elseif t == "function" then
|
||||
input:map("i", lhs, create_input_mapping_handle(cmd_name, state, scroll_padding),
|
||||
{ noremap = true })
|
||||
else
|
||||
log.warn(string.format("Invalid command in fuzzy_finder_mappings: %s = %s", lhs, cmd_name))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,392 @@
|
||||
local vim = vim
|
||||
local log = require("neo-tree.log")
|
||||
local Job = require("plenary.job")
|
||||
local utils = require("neo-tree.utils")
|
||||
local Queue = require("neo-tree.collections").Queue
|
||||
|
||||
local M = {}
|
||||
local fd_supports_max_results = nil
|
||||
local unpack = unpack or table.unpack
|
||||
|
||||
local test_for_max_results = function(cmd)
|
||||
if fd_supports_max_results == nil then
|
||||
if cmd == "fd" or cmd == "fdfind" then
|
||||
--test if it supports the max-results option
|
||||
local test = vim.fn.system(cmd .. " this_is_only_a_test --max-depth=1 --max-results=1")
|
||||
if test:match("^error:") then
|
||||
fd_supports_max_results = false
|
||||
log.debug(cmd, "does NOT support max-results")
|
||||
else
|
||||
fd_supports_max_results = true
|
||||
log.debug(cmd, "supports max-results")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local get_find_command = function(state)
|
||||
if state.find_command then
|
||||
test_for_max_results(state.find_command)
|
||||
return state.find_command
|
||||
end
|
||||
|
||||
if 1 == vim.fn.executable("fdfind") then
|
||||
state.find_command = "fdfind"
|
||||
elseif 1 == vim.fn.executable("fd") then
|
||||
state.find_command = "fd"
|
||||
elseif 1 == vim.fn.executable("find") and vim.fn.has("win32") == 0 then
|
||||
state.find_command = "find"
|
||||
elseif 1 == vim.fn.executable("where") then
|
||||
state.find_command = "where"
|
||||
end
|
||||
|
||||
test_for_max_results(state.find_command)
|
||||
return state.find_command
|
||||
end
|
||||
|
||||
local running_jobs = Queue:new()
|
||||
local kill_job = function(job)
|
||||
local pid = job.pid
|
||||
job:shutdown()
|
||||
if pid ~= nil and pid > 0 then
|
||||
if utils.is_windows then
|
||||
vim.fn.system("taskkill /F /T /PID " .. pid)
|
||||
else
|
||||
vim.fn.system("kill -9 " .. pid)
|
||||
end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
M.cancel = function()
|
||||
if running_jobs:is_empty() then
|
||||
return
|
||||
end
|
||||
running_jobs:for_each(kill_job)
|
||||
end
|
||||
|
||||
---@class FileTypes
|
||||
---@field file boolean
|
||||
---@field directory boolean
|
||||
---@field symlink boolean
|
||||
---@field socket boolean
|
||||
---@field pipe boolean
|
||||
---@field executable boolean
|
||||
---@field empty boolean
|
||||
---@field block boolean Only for `find`
|
||||
---@field character boolean Only for `find`
|
||||
|
||||
---filter_files_external
|
||||
-- Spawns a filter command based on `cmd`
|
||||
---@param cmd string Command to execute. Use `get_find_command` most times.
|
||||
---@param path string Base directory to start the search.
|
||||
---@param glob string | nil If not nil, do glob search. Take precedence on `regex`
|
||||
---@param regex string | nil If not nil, do regex search if command supports. if glob ~= nil, ignored
|
||||
---@param full_path boolean If true, search agaist the absolute path
|
||||
---@param types FileTypes | nil Return only true filetypes. If nil, all are returned.
|
||||
---@param ignore { dotfiles: boolean?, gitignore: boolean? } If true, ignored from result. Default: false
|
||||
---@param limit? integer | nil Maximim number of results. nil will return everything.
|
||||
---@param find_args? string[] | table<string, string[]> Any additional options passed to command if any.
|
||||
---@param on_insert? fun(err: string, line: string): any Executed for each line of stdout and stderr.
|
||||
---@param on_exit? fun(return_val: table): any Executed at the end.
|
||||
M.filter_files_external = function(
|
||||
cmd,
|
||||
path,
|
||||
glob,
|
||||
regex,
|
||||
full_path,
|
||||
types,
|
||||
ignore,
|
||||
limit,
|
||||
find_args,
|
||||
on_insert,
|
||||
on_exit
|
||||
)
|
||||
if glob ~= nil and regex ~= nil then
|
||||
local log_msg = string.format([[glob: %s, regex: %s]], glob, regex)
|
||||
log.warn("both glob and regex are set. glob will take precedence. " .. log_msg)
|
||||
end
|
||||
ignore = ignore or {}
|
||||
types = types or {}
|
||||
limit = limit or math.huge -- math.huge == no limit
|
||||
local file_type_map = {
|
||||
file = "f",
|
||||
directory = "d",
|
||||
symlink = "l",
|
||||
socket = "s",
|
||||
pipe = "p",
|
||||
executable = "x", -- only for `fd`
|
||||
empty = "e", -- only for `fd`
|
||||
block = "b", -- only for `find`
|
||||
character = "c", -- only for `find`
|
||||
}
|
||||
|
||||
local args = {}
|
||||
local function append(...)
|
||||
for _, v in pairs({ ... }) do
|
||||
if v ~= nil then
|
||||
args[#args + 1] = v
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function append_find_args()
|
||||
if find_args then
|
||||
if type(find_args) == "string" then
|
||||
append(find_args)
|
||||
elseif type(find_args) == "table" then
|
||||
if find_args[1] then
|
||||
append(unpack(find_args))
|
||||
elseif find_args[cmd] then
|
||||
append(unpack(find_args[cmd])) ---@diagnostic disable-line
|
||||
end
|
||||
elseif type(find_args) == "function" then
|
||||
args = find_args(cmd, path, glob, args)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if cmd == "fd" or cmd == "fdfind" then
|
||||
if not ignore.dotfiles then
|
||||
append("--hidden")
|
||||
end
|
||||
if not ignore.gitignore then
|
||||
append("--no-ignore")
|
||||
end
|
||||
append("--color", "never")
|
||||
if fd_supports_max_results and 0 < limit and limit < math.huge then
|
||||
append("--max-results", limit)
|
||||
end
|
||||
for k, v in pairs(types) do
|
||||
if v and file_type_map[k] ~= nil then
|
||||
append("--type", k)
|
||||
end
|
||||
end
|
||||
if full_path then
|
||||
append("--full-path")
|
||||
if glob ~= nil then
|
||||
local words = utils.split(glob, " ")
|
||||
regex = ".*" .. table.concat(words, ".*") .. ".*"
|
||||
glob = nil
|
||||
end
|
||||
end
|
||||
if glob ~= nil then
|
||||
append("--glob")
|
||||
end
|
||||
append_find_args()
|
||||
append("--", glob or regex or "")
|
||||
append(path)
|
||||
elseif cmd == "find" then
|
||||
append(path)
|
||||
local file_types = {}
|
||||
for k, v in pairs(types) do
|
||||
if v and file_type_map[k] ~= nil then
|
||||
file_types[#file_types + 1] = file_type_map[k]
|
||||
end
|
||||
end
|
||||
if #file_types > 0 then
|
||||
append("-type", table.concat(file_types, ","))
|
||||
end
|
||||
if types.empty then
|
||||
append("-empty")
|
||||
end
|
||||
if types.executable then
|
||||
append("-executable")
|
||||
end
|
||||
if not ignore.dotfiles then
|
||||
append("-not", "-path", "*/.*")
|
||||
end
|
||||
if glob ~= nil and not full_path then
|
||||
append("-iname", glob)
|
||||
elseif glob ~= nil and full_path then
|
||||
local words = utils.split(glob, " ")
|
||||
regex = ".*" .. table.concat(words, ".*") .. ".*"
|
||||
append("-regextype", "sed", "-regex", regex)
|
||||
elseif regex ~= nil then
|
||||
append("-regextype", "sed", "-regex", regex)
|
||||
end
|
||||
append_find_args()
|
||||
elseif cmd == "fzf" then
|
||||
-- This does not work yet, there's some kind of issue with how fzf uses stdout
|
||||
error("fzf is not a supported find_command")
|
||||
append_find_args()
|
||||
append("--no-sort", "--no-expect", "--filter", glob or regex) -- using the raw term without glob patterns
|
||||
elseif cmd == "where" then
|
||||
append_find_args()
|
||||
append("/r", path, glob or regex)
|
||||
else
|
||||
return { "No search command found!" }
|
||||
end
|
||||
|
||||
if fd_supports_max_results then
|
||||
limit = math.huge -- `fd` manages limit on its own
|
||||
end
|
||||
local item_count = 0
|
||||
local job = Job:new({
|
||||
command = cmd,
|
||||
cwd = path,
|
||||
args = args,
|
||||
enable_recording = false,
|
||||
on_stdout = function(err, line)
|
||||
if item_count < limit and on_insert then
|
||||
on_insert(err, line)
|
||||
item_count = item_count + 1
|
||||
end
|
||||
end,
|
||||
on_stderr = function(err, line)
|
||||
if item_count < limit and on_insert then
|
||||
on_insert(err or line, line)
|
||||
-- item_count = item_count + 1
|
||||
end
|
||||
end,
|
||||
on_exit = function(_, return_val)
|
||||
if on_exit then
|
||||
on_exit(return_val)
|
||||
end
|
||||
end,
|
||||
})
|
||||
|
||||
-- This ensures that only one job is running at a time
|
||||
running_jobs:for_each(kill_job)
|
||||
running_jobs:add(job)
|
||||
job:start()
|
||||
end
|
||||
|
||||
local function fzy_sort_get_total_score(terms, path)
|
||||
local fzy = require("neo-tree.sources.common.filters.filter_fzy")
|
||||
local total_score = 0
|
||||
for _, term in ipairs(terms) do -- spaces in `opts.term` are treated as `and`
|
||||
local score = fzy.score(term, path)
|
||||
if score == fzy.get_score_min() then -- if any not found, end searching
|
||||
return 0
|
||||
end
|
||||
total_score = total_score + score
|
||||
end
|
||||
return total_score
|
||||
end
|
||||
|
||||
local function modify_parent_scores(result_scores, path, score)
|
||||
local parent, _ = utils.split_path(path)
|
||||
while parent ~= nil do -- back propagate the score to its ancesters
|
||||
if score > (result_scores[parent] or 0) then
|
||||
result_scores[parent] = score
|
||||
parent, _ = utils.split_path(parent)
|
||||
else
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
M.fzy_sort_files = function(opts, state)
|
||||
state = state or {}
|
||||
local filters = opts.filtered_items
|
||||
local limit = opts.limit or 100
|
||||
local full_path_words = opts.find_by_full_path_words
|
||||
local fuzzy_finder_mode = opts.fuzzy_finder_mode
|
||||
local pwd = opts.path
|
||||
if pwd:sub(-1) ~= "/" then
|
||||
pwd = pwd .. "/"
|
||||
end
|
||||
local pwd_length = #pwd
|
||||
local terms = {}
|
||||
for term in string.gmatch(opts.term, "[^%s]+") do -- space split opts.term
|
||||
terms[#terms + 1] = term
|
||||
end
|
||||
|
||||
-- The base search is anything that contains the characters in the term
|
||||
-- The fzy score is then used to sort the results
|
||||
local chars = {}
|
||||
local regex = ".*"
|
||||
local chars_to_escape =
|
||||
{ "%", "+", "-", "?", "[", "^", "$", "(", ")", "{", "}", "=", "!", "<", ">", "|", ":", "#" }
|
||||
for _, term in ipairs(terms) do
|
||||
for c in term:gmatch(".") do
|
||||
if not chars[c] then
|
||||
chars[c] = true
|
||||
if chars_to_escape[c] then
|
||||
c = [[\]] .. c
|
||||
end
|
||||
regex = regex .. c .. ".*"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local result_counter = 0
|
||||
|
||||
local index = 1
|
||||
state.fzy_sort_result_scores = {}
|
||||
local function on_insert(err, path)
|
||||
if not err then
|
||||
local relative_path = path
|
||||
if not full_path_words and #path > pwd_length and path:sub(1, pwd_length) == pwd then
|
||||
relative_path = "./" .. path:sub(pwd_length + 1)
|
||||
end
|
||||
index = index + 1
|
||||
if state.fzy_sort_result_scores == nil then
|
||||
state.fzy_sort_result_scores = {}
|
||||
end
|
||||
state.fzy_sort_result_scores[path] = 0
|
||||
local score = fzy_sort_get_total_score(terms, relative_path)
|
||||
if score > 0 then
|
||||
state.fzy_sort_result_scores[path] = score
|
||||
result_counter = result_counter + 1
|
||||
modify_parent_scores(state.fzy_sort_result_scores, path, score)
|
||||
opts.on_insert(nil, path)
|
||||
if result_counter >= limit then
|
||||
vim.schedule(M.cancel)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
M.filter_files_external(
|
||||
get_find_command(state),
|
||||
pwd,
|
||||
nil,
|
||||
regex,
|
||||
true,
|
||||
{ directory = fuzzy_finder_mode == "directory", file = fuzzy_finder_mode ~= "directory" },
|
||||
{
|
||||
dotfiles = not filters.visible and filters.hide_dotfiles,
|
||||
gitignore = not filters.visible and filters.hide_gitignored,
|
||||
},
|
||||
nil,
|
||||
opts.find_args,
|
||||
on_insert,
|
||||
opts.on_exit
|
||||
)
|
||||
end
|
||||
|
||||
M.find_files = function(opts)
|
||||
local filters = opts.filtered_items
|
||||
local full_path_words = opts.find_by_full_path_words
|
||||
local regex, glob = nil, nil
|
||||
local fuzzy_finder_mode = opts.fuzzy_finder_mode
|
||||
|
||||
glob = opts.term
|
||||
if glob:sub(1) ~= "*" then
|
||||
glob = "*" .. glob
|
||||
end
|
||||
if glob:sub(-1) ~= "*" then
|
||||
glob = glob .. "*"
|
||||
end
|
||||
|
||||
M.filter_files_external(
|
||||
get_find_command(opts),
|
||||
opts.path,
|
||||
glob,
|
||||
regex,
|
||||
full_path_words,
|
||||
{ directory = fuzzy_finder_mode == "directory" },
|
||||
{
|
||||
dotfiles = not filters.visible and filters.hide_dotfiles,
|
||||
gitignore = not filters.visible and filters.hide_gitignored,
|
||||
},
|
||||
opts.limit or 200,
|
||||
opts.find_args,
|
||||
opts.on_insert,
|
||||
opts.on_exit
|
||||
)
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,620 @@
|
||||
-- This file is for functions that mutate the filesystem.
|
||||
|
||||
-- This code started out as a copy from:
|
||||
-- https://github.com/mhartington/dotfiles
|
||||
-- and modified to fit neo-tree's api.
|
||||
-- Permalink: https://github.com/mhartington/dotfiles/blob/7560986378753e0c047d940452cb03a3b6439b11/config/nvim/lua/mh/filetree/init.lua
|
||||
local vim = vim
|
||||
local api = vim.api
|
||||
local loop = vim.loop
|
||||
local scan = require("plenary.scandir")
|
||||
local utils = require("neo-tree.utils")
|
||||
local inputs = require("neo-tree.ui.inputs")
|
||||
local events = require("neo-tree.events")
|
||||
local log = require("neo-tree.log")
|
||||
local Path = require("plenary").path
|
||||
|
||||
local M = {}
|
||||
|
||||
local function find_replacement_buffer(for_buf)
|
||||
local bufs = vim.api.nvim_list_bufs()
|
||||
|
||||
-- make sure the alternate buffer is at the top of the list
|
||||
local alt = vim.fn.bufnr("#")
|
||||
if alt ~= -1 and alt ~= for_buf then
|
||||
table.insert(bufs, 1, alt)
|
||||
end
|
||||
|
||||
-- find the first valid real file buffer
|
||||
for _, buf in ipairs(bufs) do
|
||||
if buf ~= for_buf then
|
||||
local is_valid = vim.api.nvim_buf_is_valid(buf)
|
||||
if is_valid then
|
||||
local buftype = vim.api.nvim_buf_get_option(buf, "buftype")
|
||||
if buftype == "" then
|
||||
return buf
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return -1
|
||||
end
|
||||
|
||||
local function clear_buffer(path)
|
||||
local buf = utils.find_buffer_by_name(path)
|
||||
if buf < 1 then
|
||||
return
|
||||
end
|
||||
local alt = find_replacement_buffer(buf)
|
||||
-- Check all windows to see if they are using the buffer
|
||||
for _, win in ipairs(vim.api.nvim_list_wins()) do
|
||||
if vim.api.nvim_win_is_valid(win) and vim.api.nvim_win_get_buf(win) == buf then
|
||||
-- if there is no alternate buffer yet, create a blank one now
|
||||
if alt < 1 or alt == buf then
|
||||
alt = vim.api.nvim_create_buf(true, false)
|
||||
end
|
||||
-- replace the buffer displayed in this window with the alternate buffer
|
||||
vim.api.nvim_win_set_buf(win, alt)
|
||||
end
|
||||
end
|
||||
local success, msg = pcall(vim.api.nvim_buf_delete, buf, { force = true })
|
||||
if not success then
|
||||
log.error("Could not clear buffer: ", msg)
|
||||
end
|
||||
end
|
||||
|
||||
---Opens new_buf in each window that has old_buf currently open.
|
||||
---Useful during file rename.
|
||||
---@param old_buf number
|
||||
---@param new_buf number
|
||||
local function replace_buffer_in_windows(old_buf, new_buf)
|
||||
for _, win in ipairs(vim.api.nvim_list_wins()) do
|
||||
if vim.api.nvim_win_is_valid(win) and vim.api.nvim_win_get_buf(win) == old_buf then
|
||||
vim.api.nvim_win_set_buf(win, new_buf)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function rename_buffer(old_path, new_path)
|
||||
local force_save = function()
|
||||
vim.cmd("silent! write!")
|
||||
end
|
||||
|
||||
for _, buf in pairs(vim.api.nvim_list_bufs()) do
|
||||
if vim.api.nvim_buf_is_loaded(buf) then
|
||||
local buf_name = vim.api.nvim_buf_get_name(buf)
|
||||
local new_buf_name = nil
|
||||
if old_path == buf_name then
|
||||
new_buf_name = new_path
|
||||
elseif utils.is_subpath(old_path, buf_name) then
|
||||
new_buf_name = new_path .. buf_name:sub(#old_path + 1)
|
||||
end
|
||||
if utils.truthy(new_buf_name) then
|
||||
local new_buf = vim.fn.bufadd(new_buf_name)
|
||||
vim.fn.bufload(new_buf)
|
||||
vim.api.nvim_buf_set_option(new_buf, "buflisted", true)
|
||||
replace_buffer_in_windows(buf, new_buf)
|
||||
|
||||
if vim.api.nvim_buf_get_option(buf, "buftype") == "" then
|
||||
local modified = vim.api.nvim_buf_get_option(buf, "modified")
|
||||
if modified then
|
||||
local old_buffer_lines = vim.api.nvim_buf_get_lines(buf, 0, -1, false)
|
||||
vim.api.nvim_buf_set_lines(new_buf, 0, -1, false, old_buffer_lines)
|
||||
|
||||
local msg = buf_name .. " has been modified. Save under new name? (y/n) "
|
||||
inputs.confirm(msg, function(confirmed)
|
||||
if confirmed then
|
||||
vim.api.nvim_buf_call(new_buf, force_save)
|
||||
log.trace("Force saving renamed buffer with changes")
|
||||
else
|
||||
vim.cmd("echohl WarningMsg")
|
||||
vim.cmd(
|
||||
[[echo "Skipping force save. You'll need to save it with `:w!` when you are ready to force writing with the new name."]]
|
||||
)
|
||||
vim.cmd("echohl NONE")
|
||||
end
|
||||
end)
|
||||
end
|
||||
end
|
||||
vim.api.nvim_buf_delete(buf, { force = true })
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function create_all_parents(path)
|
||||
local create_all_as_folders
|
||||
function create_all_as_folders(in_path)
|
||||
if not loop.fs_stat(in_path) then
|
||||
local parent, _ = utils.split_path(in_path)
|
||||
if parent then
|
||||
create_all_as_folders(parent)
|
||||
end
|
||||
loop.fs_mkdir(in_path, 493)
|
||||
end
|
||||
end
|
||||
|
||||
local parent_path, _ = utils.split_path(path)
|
||||
create_all_as_folders(parent_path)
|
||||
end
|
||||
|
||||
-- Gets a non-existing filename from the user and executes the callback with it.
|
||||
local function get_unused_name(
|
||||
destination,
|
||||
using_root_directory,
|
||||
name_chosen_callback,
|
||||
first_message
|
||||
)
|
||||
if loop.fs_stat(destination) then
|
||||
local parent_path, name
|
||||
if not using_root_directory then
|
||||
parent_path, name = utils.split_path(destination)
|
||||
elseif #using_root_directory > 0 then
|
||||
parent_path = destination:sub(1, #using_root_directory)
|
||||
name = destination:sub(#using_root_directory + 2)
|
||||
else
|
||||
parent_path = nil
|
||||
name = destination
|
||||
end
|
||||
|
||||
local message = first_message or name .. " already exists. Please enter a new name: "
|
||||
inputs.input(message, name, function(new_name)
|
||||
if new_name and string.len(new_name) > 0 then
|
||||
local new_path = parent_path and parent_path .. utils.path_separator .. new_name or new_name
|
||||
get_unused_name(new_path, using_root_directory, name_chosen_callback)
|
||||
end
|
||||
end)
|
||||
else
|
||||
name_chosen_callback(destination)
|
||||
end
|
||||
end
|
||||
|
||||
-- Move Node
|
||||
M.move_node = function(source, destination, callback, using_root_directory)
|
||||
log.trace(
|
||||
"Moving node: ",
|
||||
source,
|
||||
" to ",
|
||||
destination,
|
||||
", using root directory: ",
|
||||
using_root_directory
|
||||
)
|
||||
local _, name = utils.split_path(source)
|
||||
get_unused_name(destination or source, using_root_directory, function(dest)
|
||||
local function move_file()
|
||||
create_all_parents(dest)
|
||||
loop.fs_rename(source, dest, function(err)
|
||||
if err then
|
||||
log.error("Could not move the files from", source, "to", dest, ":", err)
|
||||
return
|
||||
end
|
||||
vim.schedule(function()
|
||||
rename_buffer(source, dest)
|
||||
end)
|
||||
vim.schedule(function()
|
||||
events.fire_event(events.FILE_MOVED, {
|
||||
source = source,
|
||||
destination = dest,
|
||||
})
|
||||
if callback then
|
||||
callback(source, dest)
|
||||
end
|
||||
end)
|
||||
end)
|
||||
end
|
||||
local event_result = events.fire_event(events.BEFORE_FILE_MOVE, {
|
||||
source = source,
|
||||
destination = dest,
|
||||
callback = move_file,
|
||||
}) or {}
|
||||
if event_result.handled then
|
||||
return
|
||||
end
|
||||
move_file()
|
||||
end, 'Move "' .. name .. '" to:')
|
||||
end
|
||||
|
||||
---Plenary path.copy() when used to copy a recursive structure, can return a nested
|
||||
-- table with for each file a Path instance and the success result.
|
||||
---@param copy_result table The output of Path.copy()
|
||||
---@param flat_result table Return value containing the flattened results
|
||||
local function flatten_path_copy_result(flat_result, copy_result)
|
||||
if not copy_result then
|
||||
return
|
||||
end
|
||||
for k, v in pairs(copy_result) do
|
||||
if type(v) == "table" then
|
||||
flatten_path_copy_result(flat_result, v)
|
||||
else
|
||||
table.insert(flat_result, { destination = k.filename, success = v })
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Check if all files were copied successfully, using the flattened copy result
|
||||
local function check_path_copy_result(flat_result)
|
||||
if not flat_result then
|
||||
return
|
||||
end
|
||||
for _, file_result in ipairs(flat_result) do
|
||||
if not file_result.success then
|
||||
return false
|
||||
end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
-- Copy Node
|
||||
M.copy_node = function(source, _destination, callback, using_root_directory)
|
||||
local _, name = utils.split_path(source)
|
||||
get_unused_name(_destination or source, using_root_directory, function(destination)
|
||||
local parent_path, _ = utils.split_path(destination)
|
||||
if source == parent_path then
|
||||
log.warn("Cannot copy a file/folder to itself")
|
||||
return
|
||||
end
|
||||
local source_path = Path:new(source)
|
||||
if source_path:is_file() then
|
||||
-- When the source is a file, then Path.copy() currently doesn't create
|
||||
-- the potential non-existing parent directories of the destination.
|
||||
create_all_parents(destination)
|
||||
end
|
||||
local success, result = pcall(source_path.copy, source_path, {
|
||||
destination = destination,
|
||||
recursive = true,
|
||||
parents = true,
|
||||
})
|
||||
if not success then
|
||||
log.error("Could not copy the file(s) from", source, "to", destination, ":", result)
|
||||
return
|
||||
end
|
||||
|
||||
-- It can happen that the Path.copy() function returns successfully but
|
||||
-- the copy action still failed. In this case the copy() result contains
|
||||
-- a nested table of Path instances for each file copied, and the success
|
||||
-- result.
|
||||
local flat_result = {}
|
||||
flatten_path_copy_result(flat_result, result)
|
||||
if not check_path_copy_result(flat_result) then
|
||||
log.error("Could not copy the file(s) from", source, "to", destination, ":", flat_result)
|
||||
return
|
||||
end
|
||||
|
||||
vim.schedule(function()
|
||||
events.fire_event(events.FILE_ADDED, destination)
|
||||
if callback then
|
||||
callback(source, destination)
|
||||
end
|
||||
end)
|
||||
end, 'Copy "' .. name .. '" to:')
|
||||
end
|
||||
|
||||
--- Create a new directory
|
||||
M.create_directory = function(in_directory, callback, using_root_directory)
|
||||
local base
|
||||
if type(using_root_directory) == "string" then
|
||||
if in_directory == using_root_directory then
|
||||
base = ""
|
||||
elseif #using_root_directory > 0 then
|
||||
base = in_directory:sub(#using_root_directory + 2) .. utils.path_separator
|
||||
else
|
||||
base = in_directory .. utils.path_separator
|
||||
end
|
||||
else
|
||||
base = vim.fn.fnamemodify(in_directory .. utils.path_separator, ":~")
|
||||
using_root_directory = false
|
||||
end
|
||||
|
||||
inputs.input("Enter name for new directory:", base, function(destinations)
|
||||
if not destinations then
|
||||
return
|
||||
end
|
||||
|
||||
for _, destination in ipairs(utils.brace_expand(destinations)) do
|
||||
if not destination or destination == base then
|
||||
return
|
||||
end
|
||||
|
||||
if using_root_directory then
|
||||
destination = utils.path_join(using_root_directory, destination)
|
||||
else
|
||||
destination = vim.fn.fnamemodify(destination, ":p")
|
||||
end
|
||||
|
||||
if loop.fs_stat(destination) then
|
||||
log.warn("Directory already exists")
|
||||
return
|
||||
end
|
||||
|
||||
create_all_parents(destination)
|
||||
loop.fs_mkdir(destination, 493)
|
||||
|
||||
vim.schedule(function()
|
||||
events.fire_event(events.FILE_ADDED, destination)
|
||||
if callback then
|
||||
callback(destination)
|
||||
end
|
||||
end)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
--- Create Node
|
||||
M.create_node = function(in_directory, callback, using_root_directory)
|
||||
local base
|
||||
if type(using_root_directory) == "string" then
|
||||
if in_directory == using_root_directory then
|
||||
base = ""
|
||||
elseif #using_root_directory > 0 then
|
||||
base = in_directory:sub(#using_root_directory + 2) .. utils.path_separator
|
||||
else
|
||||
base = in_directory .. utils.path_separator
|
||||
end
|
||||
else
|
||||
base = vim.fn.fnamemodify(in_directory .. utils.path_separator, ":~")
|
||||
using_root_directory = false
|
||||
end
|
||||
|
||||
local dir_ending = '"/"'
|
||||
if utils.path_separator ~= '/' then
|
||||
dir_ending = dir_ending .. string.format(' or "%s"', utils.path_separator)
|
||||
end
|
||||
local msg = 'Enter name for new file or directory (dirs end with a ' .. dir_ending .. '):'
|
||||
inputs.input(
|
||||
msg,
|
||||
base,
|
||||
function(destinations)
|
||||
if not destinations then
|
||||
return
|
||||
end
|
||||
|
||||
for _, destination in ipairs(utils.brace_expand(destinations)) do
|
||||
if not destination or destination == base then
|
||||
return
|
||||
end
|
||||
local is_dir = vim.endswith(destination, "/") or vim.endswith(destination, utils.path_separator)
|
||||
|
||||
if using_root_directory then
|
||||
destination = utils.path_join(using_root_directory, destination)
|
||||
else
|
||||
destination = vim.fn.fnamemodify(destination, ":p")
|
||||
end
|
||||
|
||||
if utils.is_windows then destination = utils.windowize_path(destination) end
|
||||
if loop.fs_stat(destination) then
|
||||
log.warn("File already exists")
|
||||
return
|
||||
end
|
||||
|
||||
create_all_parents(destination)
|
||||
if is_dir then
|
||||
loop.fs_mkdir(destination, 493)
|
||||
else
|
||||
local open_mode = loop.constants.O_CREAT
|
||||
+ loop.constants.O_WRONLY
|
||||
+ loop.constants.O_TRUNC
|
||||
local fd = loop.fs_open(destination, open_mode, 420)
|
||||
if not fd then
|
||||
if not loop.fs_stat(destination) then
|
||||
api.nvim_err_writeln("Could not create file " .. destination)
|
||||
return
|
||||
else
|
||||
log.warn("Failed to complete file creation of " .. destination)
|
||||
end
|
||||
else
|
||||
loop.fs_close(fd)
|
||||
end
|
||||
end
|
||||
|
||||
vim.schedule(function()
|
||||
events.fire_event(events.FILE_ADDED, destination)
|
||||
if callback then
|
||||
callback(destination)
|
||||
end
|
||||
end)
|
||||
end
|
||||
end
|
||||
)
|
||||
end
|
||||
|
||||
-- Delete Node
|
||||
M.delete_node = function(path, callback, noconfirm)
|
||||
local _, name = utils.split_path(path)
|
||||
local msg = string.format("Are you sure you want to delete '%s'?", name)
|
||||
|
||||
log.trace("Deleting node: ", path)
|
||||
local _type = "unknown"
|
||||
local stat = loop.fs_stat(path)
|
||||
if stat then
|
||||
_type = stat.type
|
||||
if _type == "link" then
|
||||
local link_to = loop.fs_readlink(path)
|
||||
if not link_to then
|
||||
log.error("Could not read link")
|
||||
return
|
||||
end
|
||||
_type = loop.fs_stat(link_to)
|
||||
end
|
||||
if _type == "directory" then
|
||||
local children = scan.scan_dir(path, {
|
||||
hidden = true,
|
||||
respect_gitignore = false,
|
||||
add_dirs = true,
|
||||
depth = 1,
|
||||
})
|
||||
if #children > 0 then
|
||||
msg = "WARNING: Dir not empty! " .. msg
|
||||
end
|
||||
end
|
||||
else
|
||||
log.warn("Could not read file/dir:", path, stat, ", attempting to delete anyway...")
|
||||
-- Guess the type by whether it appears to have an extension
|
||||
if path:match("%.(.+)$") then
|
||||
_type = "file"
|
||||
else
|
||||
_type = "directory"
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
local do_delete = function(confirmed)
|
||||
if not confirmed then
|
||||
return
|
||||
end
|
||||
|
||||
local function delete_dir(dir_path)
|
||||
local handle = loop.fs_scandir(dir_path)
|
||||
if type(handle) == "string" then
|
||||
return api.nvim_err_writeln(handle)
|
||||
end
|
||||
|
||||
while true do
|
||||
local child_name, t = loop.fs_scandir_next(handle)
|
||||
if not child_name then
|
||||
break
|
||||
end
|
||||
|
||||
local child_path = dir_path .. "/" .. child_name
|
||||
if t == "directory" then
|
||||
local success = delete_dir(child_path)
|
||||
if not success then
|
||||
log.error("failed to delete ", child_path)
|
||||
return false
|
||||
end
|
||||
else
|
||||
local success = loop.fs_unlink(child_path)
|
||||
if not success then
|
||||
return false
|
||||
end
|
||||
clear_buffer(child_path)
|
||||
end
|
||||
end
|
||||
return loop.fs_rmdir(dir_path)
|
||||
end
|
||||
|
||||
if _type == "directory" then
|
||||
-- first try using native system commands, which are recursive
|
||||
local success = false
|
||||
if utils.is_windows then
|
||||
local result = vim.fn.system({ "cmd.exe", "/c", "rmdir", "/s", "/q", vim.fn.shellescape(path) })
|
||||
local error = vim.v.shell_error
|
||||
if error ~= 0 then
|
||||
log.debug("Could not delete directory '", path, "' with rmdir: ", result)
|
||||
else
|
||||
log.info("Deleted directory ", path)
|
||||
success = true
|
||||
end
|
||||
else
|
||||
local result = vim.fn.system({ "rm", "-Rf", path })
|
||||
local error = vim.v.shell_error
|
||||
if error ~= 0 then
|
||||
log.debug("Could not delete directory '", path, "' with rm: ", result)
|
||||
else
|
||||
log.info("Deleted directory ", path)
|
||||
success = true
|
||||
end
|
||||
end
|
||||
-- Fallback to using libuv if native commands fail
|
||||
if not success then
|
||||
success = delete_dir(path)
|
||||
if not success then
|
||||
return api.nvim_err_writeln("Could not remove directory: " .. path)
|
||||
end
|
||||
end
|
||||
else
|
||||
local success = loop.fs_unlink(path)
|
||||
if not success then
|
||||
return api.nvim_err_writeln("Could not remove file: " .. path)
|
||||
end
|
||||
clear_buffer(path)
|
||||
end
|
||||
|
||||
vim.schedule(function()
|
||||
events.fire_event(events.FILE_DELETED, path)
|
||||
if callback then
|
||||
callback(path)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
if noconfirm then
|
||||
do_delete(true)
|
||||
else
|
||||
inputs.confirm(msg, do_delete)
|
||||
end
|
||||
end
|
||||
|
||||
M.delete_nodes = function(paths_to_delete, callback)
|
||||
local msg = "Are you sure you want to delete " .. #paths_to_delete .. " items?"
|
||||
inputs.confirm(msg, function(confirmed)
|
||||
if not confirmed then
|
||||
return
|
||||
end
|
||||
|
||||
for _, path in ipairs(paths_to_delete) do
|
||||
M.delete_node(path, nil, true)
|
||||
end
|
||||
|
||||
if callback then
|
||||
vim.schedule(function()
|
||||
callback(paths_to_delete[#paths_to_delete])
|
||||
end)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
-- Rename Node
|
||||
M.rename_node = function(path, callback)
|
||||
local parent_path, name = utils.split_path(path)
|
||||
local msg = string.format('Enter new name for "%s":', name)
|
||||
|
||||
inputs.input(msg, name, function(new_name)
|
||||
-- If cancelled
|
||||
if not new_name or new_name == "" then
|
||||
log.info("Operation canceled")
|
||||
return
|
||||
end
|
||||
|
||||
local destination = parent_path .. utils.path_separator .. new_name
|
||||
-- If aleady exists
|
||||
if loop.fs_stat(destination) then
|
||||
log.warn(destination, " already exists")
|
||||
return
|
||||
end
|
||||
|
||||
local complete = vim.schedule_wrap(function()
|
||||
rename_buffer(path, destination)
|
||||
events.fire_event(events.FILE_RENAMED, {
|
||||
source = path,
|
||||
destination = destination,
|
||||
})
|
||||
if callback then
|
||||
callback(path, destination)
|
||||
end
|
||||
log.info("Renamed " .. new_name .. " successfully")
|
||||
end)
|
||||
|
||||
local function fs_rename()
|
||||
loop.fs_rename(path, destination, function(err)
|
||||
if err then
|
||||
log.warn("Could not rename the files")
|
||||
return
|
||||
else
|
||||
complete()
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
local event_result = events.fire_event(events.BEFORE_FILE_RENAME, {
|
||||
source = path,
|
||||
destination = destination,
|
||||
callback = fs_rename,
|
||||
}) or {}
|
||||
if event_result.handled then
|
||||
return
|
||||
end
|
||||
fs_rename()
|
||||
end)
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,664 @@
|
||||
-- This files holds code for scanning the filesystem to build the tree.
|
||||
local uv = vim.loop
|
||||
|
||||
local renderer = require("neo-tree.ui.renderer")
|
||||
local utils = require("neo-tree.utils")
|
||||
local filter_external = require("neo-tree.sources.filesystem.lib.filter_external")
|
||||
local file_items = require("neo-tree.sources.common.file-items")
|
||||
local file_nesting = require("neo-tree.sources.common.file-nesting")
|
||||
local log = require("neo-tree.log")
|
||||
local fs_watch = require("neo-tree.sources.filesystem.lib.fs_watch")
|
||||
local git = require("neo-tree.git")
|
||||
local events = require("neo-tree.events")
|
||||
local async = require("plenary.async")
|
||||
|
||||
local M = {}
|
||||
|
||||
local on_directory_loaded = function(context, dir_path)
|
||||
local state = context.state
|
||||
local scanned_folder = context.folders[dir_path]
|
||||
if scanned_folder then
|
||||
scanned_folder.loaded = true
|
||||
end
|
||||
if state.use_libuv_file_watcher then
|
||||
local root = context.folders[dir_path]
|
||||
if root then
|
||||
local target_path = root.is_link and root.link_to or root.path
|
||||
local fs_watch_callback = vim.schedule_wrap(function(err, fname)
|
||||
if err then
|
||||
log.error("file_event_callback: ", err)
|
||||
return
|
||||
end
|
||||
if context.is_a_never_show_file(fname) then
|
||||
-- don't fire events for nodes that are designated as "never show"
|
||||
return
|
||||
else
|
||||
events.fire_event(events.FS_EVENT, { afile = target_path })
|
||||
end
|
||||
end)
|
||||
|
||||
log.trace("Adding fs watcher for ", target_path)
|
||||
fs_watch.watch_folder(target_path, fs_watch_callback)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local dir_complete = function(context, dir_path)
|
||||
local paths_to_load = context.paths_to_load
|
||||
local folders = context.folders
|
||||
|
||||
on_directory_loaded(context, dir_path)
|
||||
|
||||
-- check to see if there are more folders to load
|
||||
local next_path = nil
|
||||
while #paths_to_load > 0 and not next_path do
|
||||
next_path = table.remove(paths_to_load)
|
||||
-- ensure that the path is still valid
|
||||
local success, result = pcall(vim.loop.fs_stat, next_path)
|
||||
-- ensure that the result is a directory
|
||||
if success and result and result.type == "directory" then
|
||||
-- ensure that it is not already loaded
|
||||
local existing = folders[next_path]
|
||||
if existing and existing.loaded then
|
||||
next_path = nil
|
||||
end
|
||||
else
|
||||
-- if the path doesn't exist, skip it
|
||||
next_path = nil
|
||||
end
|
||||
end
|
||||
return next_path
|
||||
end
|
||||
|
||||
local render_context = function(context)
|
||||
local state = context.state
|
||||
local root = context.root
|
||||
local parent_id = context.parent_id
|
||||
|
||||
if not parent_id and state.use_libuv_file_watcher and state.enable_git_status then
|
||||
log.trace("Starting .git folder watcher")
|
||||
local path = root.path
|
||||
if root.is_link then
|
||||
path = root.link_to
|
||||
end
|
||||
fs_watch.watch_git_index(path, require("neo-tree").config.git_status_async)
|
||||
end
|
||||
fs_watch.updated_watched()
|
||||
|
||||
if root and root.children then
|
||||
file_items.advanced_sort(root.children, state)
|
||||
end
|
||||
if parent_id then
|
||||
-- lazy loading a child folder
|
||||
renderer.show_nodes(root.children, state, parent_id, context.callback)
|
||||
else
|
||||
-- full render of the tree
|
||||
renderer.show_nodes({ root }, state, nil, context.callback)
|
||||
end
|
||||
|
||||
context.state = nil
|
||||
context.callback = nil
|
||||
context.all_items = nil
|
||||
context.root = nil
|
||||
context.parent_id = nil
|
||||
context = nil
|
||||
end
|
||||
|
||||
local should_check_gitignore = function (context)
|
||||
local state = context.state
|
||||
if #context.all_items == 0 then
|
||||
log.info("No items, skipping git ignored/status lookups")
|
||||
return false
|
||||
end
|
||||
if state.search_pattern and state.check_gitignore_in_search == false then
|
||||
return false
|
||||
end
|
||||
if state.filtered_items.hide_gitignored then
|
||||
return true
|
||||
end
|
||||
if state.enable_git_status == false then
|
||||
return false
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
local job_complete_async = function(context)
|
||||
local state = context.state
|
||||
local parent_id = context.parent_id
|
||||
|
||||
file_nesting.nest_items(context)
|
||||
|
||||
-- if state.search_pattern and #context.all_items > 50 then
|
||||
-- -- don't do git ignored/status lookups when searching unless we are down to a reasonable number of items
|
||||
-- return context
|
||||
-- end
|
||||
if should_check_gitignore(context) then
|
||||
local mark_ignored_async = async.wrap(function (_state, _all_items, _callback)
|
||||
git.mark_ignored(_state, _all_items, _callback)
|
||||
end, 3)
|
||||
local all_items = mark_ignored_async(state, context.all_items)
|
||||
|
||||
if parent_id then
|
||||
vim.list_extend(state.git_ignored, all_items)
|
||||
else
|
||||
state.git_ignored = all_items
|
||||
end
|
||||
end
|
||||
return context
|
||||
end
|
||||
|
||||
local job_complete = function(context)
|
||||
local state = context.state
|
||||
local parent_id = context.parent_id
|
||||
|
||||
file_nesting.nest_items(context)
|
||||
|
||||
if should_check_gitignore(context) then
|
||||
if require("neo-tree").config.git_status_async then
|
||||
git.mark_ignored(state, context.all_items, function(all_items)
|
||||
if parent_id then
|
||||
vim.list_extend(state.git_ignored, all_items)
|
||||
else
|
||||
state.git_ignored = all_items
|
||||
end
|
||||
vim.schedule(function()
|
||||
render_context(context)
|
||||
end)
|
||||
end)
|
||||
return
|
||||
else
|
||||
local all_items = git.mark_ignored(state, context.all_items)
|
||||
if parent_id then
|
||||
vim.list_extend(state.git_ignored, all_items)
|
||||
else
|
||||
state.git_ignored = all_items
|
||||
end
|
||||
end
|
||||
render_context(context)
|
||||
else
|
||||
render_context(context)
|
||||
end
|
||||
end
|
||||
|
||||
local function create_node(context, node)
|
||||
local success3, item = pcall(file_items.create_item, context, node.path, node.type)
|
||||
end
|
||||
|
||||
local function process_node(context, path)
|
||||
on_directory_loaded(context, path)
|
||||
end
|
||||
|
||||
---@param err string libuv error
|
||||
---@return boolean is_permission_error
|
||||
local function is_permission_error(err)
|
||||
-- Permission errors may be common when scanning over lots of folders;
|
||||
-- this is used to check for them and log to `debug` instead of `error`.
|
||||
return vim.startswith(err, "EPERM") or vim.startswith(err, "EACCES")
|
||||
end
|
||||
|
||||
local function get_children_sync(path)
|
||||
local children = {}
|
||||
local dir, err = uv.fs_opendir(path, nil, 1000)
|
||||
if err then
|
||||
if is_permission_error(err) then
|
||||
log.debug(err)
|
||||
else
|
||||
log.error(err)
|
||||
end
|
||||
return children
|
||||
end
|
||||
local stats = uv.fs_readdir(dir)
|
||||
if stats then
|
||||
for _, stat in ipairs(stats) do
|
||||
local child_path = utils.path_join(path, stat.name)
|
||||
table.insert(children, { path = child_path, type = stat.type })
|
||||
end
|
||||
end
|
||||
uv.fs_closedir(dir)
|
||||
return children
|
||||
end
|
||||
|
||||
local function get_children_async(path, callback)
|
||||
local children = {}
|
||||
uv.fs_opendir(path, function(err, dir)
|
||||
if err then
|
||||
if is_permission_error(err) then
|
||||
log.debug(err)
|
||||
else
|
||||
log.error(err)
|
||||
end
|
||||
callback(children)
|
||||
return
|
||||
end
|
||||
uv.fs_readdir(dir, function(_, stats)
|
||||
if stats then
|
||||
for _, stat in ipairs(stats) do
|
||||
local child_path = utils.path_join(path, stat.name)
|
||||
table.insert(children, { path = child_path, type = stat.type })
|
||||
end
|
||||
end
|
||||
uv.fs_closedir(dir)
|
||||
callback(children)
|
||||
end)
|
||||
end, 1000)
|
||||
end
|
||||
|
||||
local function scan_dir_sync(context, path)
|
||||
process_node(context, path)
|
||||
local children = get_children_sync(path)
|
||||
for _, child in ipairs(children) do
|
||||
create_node(context, child)
|
||||
if child.type == "directory" then
|
||||
local grandchild_nodes = get_children_sync(child.path)
|
||||
if
|
||||
grandchild_nodes == nil
|
||||
or #grandchild_nodes == 0
|
||||
or (#grandchild_nodes == 1 and grandchild_nodes[1].type == "directory")
|
||||
or context.recursive
|
||||
then
|
||||
scan_dir_sync(context, child.path)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- async method
|
||||
local function scan_dir_async(context, path)
|
||||
log.debug("scan_dir_async - start " .. path)
|
||||
|
||||
local get_children = async.wrap(function (_path, callback)
|
||||
return get_children_async(_path, callback)
|
||||
end, 2)
|
||||
|
||||
local children = get_children(path)
|
||||
for _, child in ipairs(children) do
|
||||
create_node(context, child)
|
||||
if child.type == "directory" then
|
||||
local grandchild_nodes = get_children(child.path)
|
||||
if
|
||||
grandchild_nodes == nil
|
||||
or #grandchild_nodes == 0
|
||||
or (#grandchild_nodes == 1 and grandchild_nodes[1].type == "directory")
|
||||
or context.recursive
|
||||
then
|
||||
scan_dir_async(context, child.path)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
process_node(context, path)
|
||||
log.debug("scan_dir_async - finish " .. path)
|
||||
return path
|
||||
end
|
||||
|
||||
|
||||
-- async_scan scans all the directories in context.paths_to_load
|
||||
-- and adds them as items to render in the UI.
|
||||
local function async_scan(context, path)
|
||||
log.trace("async_scan: ", path)
|
||||
local scan_mode = require("neo-tree").config.filesystem.scan_mode
|
||||
|
||||
if scan_mode == "deep" then
|
||||
local scan_tasks = {}
|
||||
for _, p in ipairs(context.paths_to_load) do
|
||||
local scan_task = function ()
|
||||
scan_dir_async(context, p)
|
||||
end
|
||||
table.insert(scan_tasks, scan_task)
|
||||
end
|
||||
|
||||
async.util.run_all(
|
||||
scan_tasks,
|
||||
vim.schedule_wrap(function()
|
||||
job_complete(context)
|
||||
end)
|
||||
)
|
||||
return
|
||||
end
|
||||
|
||||
-- scan_mode == "shallow"
|
||||
context.directories_scanned = 0
|
||||
context.directories_to_scan = #context.paths_to_load
|
||||
|
||||
context.on_exit = vim.schedule_wrap(function()
|
||||
job_complete(context)
|
||||
end)
|
||||
|
||||
-- from https://github.com/nvim-lua/plenary.nvim/blob/master/lua/plenary/scandir.lua
|
||||
local function read_dir(current_dir, ctx)
|
||||
uv.fs_opendir(current_dir, function(err, dir)
|
||||
if err then
|
||||
log.error(current_dir, ": ", err)
|
||||
return
|
||||
end
|
||||
local function on_fs_readdir(err, entries)
|
||||
if err then
|
||||
log.error(current_dir, ": ", err)
|
||||
return
|
||||
end
|
||||
if entries then
|
||||
for _, entry in ipairs(entries) do
|
||||
local success, item = pcall(
|
||||
file_items.create_item,
|
||||
ctx,
|
||||
utils.path_join(current_dir, entry.name),
|
||||
entry.type
|
||||
)
|
||||
if success then
|
||||
if ctx.recursive and item.type == "directory" then
|
||||
ctx.directories_to_scan = ctx.directories_to_scan + 1
|
||||
table.insert(ctx.paths_to_load, item.path)
|
||||
end
|
||||
else
|
||||
log.error("error creating item for ", path)
|
||||
end
|
||||
end
|
||||
|
||||
uv.fs_readdir(dir, on_fs_readdir)
|
||||
return
|
||||
end
|
||||
uv.fs_closedir(dir)
|
||||
on_directory_loaded(ctx, current_dir)
|
||||
ctx.directories_scanned = ctx.directories_scanned + 1
|
||||
if ctx.directories_scanned == #ctx.paths_to_load then
|
||||
ctx.on_exit()
|
||||
end
|
||||
|
||||
--local next_path = dir_complete(ctx, current_dir)
|
||||
--if next_path then
|
||||
-- local success, error = pcall(read_dir, next_path)
|
||||
-- if not success then
|
||||
-- log.error(next_path, ": ", error)
|
||||
-- end
|
||||
--else
|
||||
-- on_exit()
|
||||
--end
|
||||
end
|
||||
|
||||
uv.fs_readdir(dir, on_fs_readdir)
|
||||
end)
|
||||
end
|
||||
|
||||
--local first = table.remove(context.paths_to_load)
|
||||
--local success, err = pcall(read_dir, first)
|
||||
--if not success then
|
||||
-- log.error(first, ": ", err)
|
||||
--end
|
||||
for i = 1, context.directories_to_scan do
|
||||
read_dir(context.paths_to_load[i], context)
|
||||
end
|
||||
end
|
||||
|
||||
local function sync_scan(context, path_to_scan)
|
||||
log.trace("sync_scan: ", path_to_scan)
|
||||
local scan_mode = require("neo-tree").config.filesystem.scan_mode
|
||||
if scan_mode == "deep" then
|
||||
for _, path in ipairs(context.paths_to_load) do
|
||||
scan_dir_sync(context, path)
|
||||
-- scan_dir(context, path)
|
||||
end
|
||||
job_complete(context)
|
||||
else -- scan_mode == "shallow"
|
||||
local success, dir = pcall(vim.loop.fs_opendir, path_to_scan, nil, 1000)
|
||||
if not success then
|
||||
log.error("Error opening dir:", dir)
|
||||
end
|
||||
local success2, stats = pcall(vim.loop.fs_readdir, dir)
|
||||
if success2 and stats then
|
||||
for _, stat in ipairs(stats) do
|
||||
local path = utils.path_join(path_to_scan, stat.name)
|
||||
local success3, item = pcall(file_items.create_item, context, path, stat.type)
|
||||
if success3 then
|
||||
if context.recursive and stat.type == "directory" then
|
||||
table.insert(context.paths_to_load, path)
|
||||
end
|
||||
else
|
||||
log.error("error creating item for ", path)
|
||||
end
|
||||
end
|
||||
end
|
||||
vim.loop.fs_closedir(dir)
|
||||
|
||||
local next_path = dir_complete(context, path_to_scan)
|
||||
if next_path then
|
||||
sync_scan(context, next_path)
|
||||
else
|
||||
job_complete(context)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
M.get_items_sync = function(state, parent_id, path_to_reveal, callback)
|
||||
return M.get_items(state, parent_id, path_to_reveal, callback, false)
|
||||
end
|
||||
|
||||
M.get_items_async = function(state, parent_id, path_to_reveal, callback)
|
||||
M.get_items(state, parent_id, path_to_reveal, callback, true)
|
||||
end
|
||||
|
||||
local handle_search_pattern = function (context)
|
||||
local state = context.state
|
||||
local root = context.root
|
||||
local search_opts = {
|
||||
filtered_items = state.filtered_items,
|
||||
find_command = state.find_command,
|
||||
limit = state.search_limit or 50,
|
||||
path = root.path,
|
||||
term = state.search_pattern,
|
||||
find_args = state.find_args,
|
||||
find_by_full_path_words = state.find_by_full_path_words,
|
||||
fuzzy_finder_mode = state.fuzzy_finder_mode,
|
||||
on_insert = function(err, path)
|
||||
if err then
|
||||
log.debug(err)
|
||||
else
|
||||
file_items.create_item(context, path)
|
||||
end
|
||||
end,
|
||||
on_exit = vim.schedule_wrap(function()
|
||||
job_complete(context)
|
||||
end),
|
||||
}
|
||||
if state.use_fzy then
|
||||
filter_external.fzy_sort_files(search_opts, state)
|
||||
else
|
||||
-- Use the external command because the plenary search is slow
|
||||
filter_external.find_files(search_opts)
|
||||
end
|
||||
end
|
||||
|
||||
local handle_refresh_or_up = function (context, async)
|
||||
local parent_id = context.parent_id
|
||||
local path_to_reveal = context.path_to_reveal
|
||||
local state = context.state
|
||||
local path = parent_id or state.path
|
||||
context.paths_to_load = {}
|
||||
if parent_id == nil then
|
||||
if utils.truthy(state.force_open_folders) then
|
||||
for _, f in ipairs(state.force_open_folders) do
|
||||
table.insert(context.paths_to_load, f)
|
||||
end
|
||||
elseif state.tree then
|
||||
context.paths_to_load = renderer.get_expanded_nodes(state.tree, state.path)
|
||||
end
|
||||
-- Ensure parents of all expanded nodes are also scanned
|
||||
if #context.paths_to_load > 0 and state.tree then
|
||||
local seen = {}
|
||||
for _, p in ipairs(context.paths_to_load) do
|
||||
local current = p
|
||||
while current do
|
||||
if seen[current] then
|
||||
break
|
||||
end
|
||||
seen[current] = true
|
||||
local current_node = state.tree:get_node(current)
|
||||
current = current_node and current_node:get_parent_id()
|
||||
end
|
||||
end
|
||||
context.paths_to_load = vim.tbl_keys(seen)
|
||||
end
|
||||
-- Ensure that there are no nested files in the list of folders to load
|
||||
context.paths_to_load = vim.tbl_filter(function(p)
|
||||
local stats = vim.loop.fs_stat(p)
|
||||
return stats and stats.type == "directory" or false
|
||||
end, context.paths_to_load)
|
||||
if path_to_reveal then
|
||||
-- be sure to load all of the folders leading up to the path to reveal
|
||||
local path_to_reveal_parts = utils.split(path_to_reveal, utils.path_separator)
|
||||
table.remove(path_to_reveal_parts) -- remove the file name
|
||||
-- add all parent folders to the list of paths to load
|
||||
utils.reduce(path_to_reveal_parts, "", function(acc, part)
|
||||
local current_path = utils.path_join(acc, part)
|
||||
if #current_path > #path then -- within current root
|
||||
table.insert(context.paths_to_load, current_path)
|
||||
table.insert(state.default_expanded_nodes, current_path)
|
||||
end
|
||||
return current_path
|
||||
end)
|
||||
context.paths_to_load = utils.unique(context.paths_to_load)
|
||||
end
|
||||
end
|
||||
|
||||
local filtered_items = state.filtered_items or {}
|
||||
context.is_a_never_show_file = function(fname)
|
||||
if fname then
|
||||
local _, name = utils.split_path(fname)
|
||||
if name then
|
||||
if filtered_items.never_show and filtered_items.never_show[name] then
|
||||
return true
|
||||
end
|
||||
if utils.is_filtered_by_pattern(filtered_items.never_show_by_pattern, fname, name) then
|
||||
return true
|
||||
end
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
table.insert(context.paths_to_load, path)
|
||||
if async then
|
||||
async_scan(context, path)
|
||||
else
|
||||
sync_scan(context, path)
|
||||
end
|
||||
end
|
||||
|
||||
M.get_items = function(state, parent_id, path_to_reveal, callback, async, recursive)
|
||||
renderer.acquire_window(state)
|
||||
if state.async_directory_scan == "always" then
|
||||
async = true
|
||||
elseif state.async_directory_scan == "never" then
|
||||
async = false
|
||||
elseif type(async) == "nil" then
|
||||
async = (state.async_directory_scan == "auto") or state.async_directory_scan
|
||||
end
|
||||
|
||||
if not parent_id then
|
||||
M.stop_watchers(state)
|
||||
end
|
||||
local context = file_items.create_context()
|
||||
context.state = state
|
||||
context.parent_id = parent_id
|
||||
context.path_to_reveal = path_to_reveal
|
||||
context.recursive = recursive
|
||||
context.callback = callback
|
||||
-- Create root folder
|
||||
local root = file_items.create_item(context, parent_id or state.path, "directory")
|
||||
root.name = vim.fn.fnamemodify(root.path, ":~")
|
||||
root.loaded = true
|
||||
root.search_pattern = state.search_pattern
|
||||
context.root = root
|
||||
context.folders[root.path] = root
|
||||
state.default_expanded_nodes = state.force_open_folders or { state.path }
|
||||
|
||||
if state.search_pattern then
|
||||
handle_search_pattern(context)
|
||||
else
|
||||
-- In the case of a refresh or navigating up, we need to make sure that all
|
||||
-- open folders are loaded.
|
||||
handle_refresh_or_up(context, async)
|
||||
end
|
||||
end
|
||||
|
||||
-- async method
|
||||
M.get_dir_items_async = function(state, parent_id, recursive)
|
||||
local context = file_items.create_context()
|
||||
context.state = state
|
||||
context.parent_id = parent_id
|
||||
context.path_to_reveal = nil
|
||||
context.recursive = recursive
|
||||
context.callback = nil
|
||||
context.paths_to_load = {}
|
||||
|
||||
-- Create root folder
|
||||
local root = file_items.create_item(context, parent_id or state.path, "directory")
|
||||
root.name = vim.fn.fnamemodify(root.path, ":~")
|
||||
root.loaded = true
|
||||
root.search_pattern = state.search_pattern
|
||||
context.root = root
|
||||
context.folders[root.path] = root
|
||||
state.default_expanded_nodes = state.force_open_folders or { state.path }
|
||||
|
||||
local filtered_items = state.filtered_items or {}
|
||||
context.is_a_never_show_file = function(fname)
|
||||
if fname then
|
||||
local _, name = utils.split_path(fname)
|
||||
if name then
|
||||
if filtered_items.never_show and filtered_items.never_show[name] then
|
||||
return true
|
||||
end
|
||||
if utils.is_filtered_by_pattern(filtered_items.never_show_by_pattern, fname, name) then
|
||||
return true
|
||||
end
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
table.insert(context.paths_to_load, parent_id)
|
||||
|
||||
local scan_tasks = {}
|
||||
for _, p in ipairs(context.paths_to_load) do
|
||||
local scan_task = function ()
|
||||
scan_dir_async(context, p)
|
||||
end
|
||||
table.insert(scan_tasks, scan_task)
|
||||
end
|
||||
async.util.join(scan_tasks)
|
||||
|
||||
job_complete_async(context)
|
||||
|
||||
local finalize = async.wrap(function (_context, _callback)
|
||||
vim.schedule(function ()
|
||||
render_context(_context)
|
||||
_callback()
|
||||
end)
|
||||
end, 2)
|
||||
finalize(context)
|
||||
end
|
||||
|
||||
M.stop_watchers = function(state)
|
||||
if state.use_libuv_file_watcher and state.tree then
|
||||
-- We are loaded a new root or refreshing, unwatch any folders that were
|
||||
-- previously being watched.
|
||||
local loaded_folders = renderer.select_nodes(state.tree, function(node)
|
||||
return node.type == "directory" and node.loaded
|
||||
end)
|
||||
fs_watch.unwatch_git_index(state.path, require("neo-tree").config.git_status_async)
|
||||
for _, folder in ipairs(loaded_folders) do
|
||||
log.trace("Unwatching folder ", folder.path)
|
||||
if folder.is_link then
|
||||
fs_watch.unwatch_folder(folder.link_to)
|
||||
else
|
||||
fs_watch.unwatch_folder(folder:get_id())
|
||||
end
|
||||
end
|
||||
else
|
||||
log.debug(
|
||||
"Not unwatching folders... use_libuv_file_watcher is ",
|
||||
state.use_libuv_file_watcher,
|
||||
" and state.tree is ",
|
||||
utils.truthy(state.tree)
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,177 @@
|
||||
local vim = vim
|
||||
local events = require("neo-tree.events")
|
||||
local log = require("neo-tree.log")
|
||||
local git = require("neo-tree.git")
|
||||
local utils = require("neo-tree.utils")
|
||||
|
||||
local M = {}
|
||||
|
||||
local flags = {
|
||||
watch_entry = false,
|
||||
stat = false,
|
||||
recursive = false,
|
||||
}
|
||||
|
||||
local watched = {}
|
||||
|
||||
local get_dot_git_folder = function(path, callback)
|
||||
if type(callback) == "function" then
|
||||
git.get_repository_root(path, function(git_root)
|
||||
if git_root then
|
||||
local git_folder = utils.path_join(git_root, ".git")
|
||||
local stat = vim.loop.fs_stat(git_folder)
|
||||
if stat and stat.type == "directory" then
|
||||
callback(git_folder, git_root)
|
||||
end
|
||||
else
|
||||
callback(nil, nil)
|
||||
end
|
||||
end)
|
||||
else
|
||||
local git_root = git.get_repository_root(path)
|
||||
if git_root then
|
||||
local git_folder = utils.path_join(git_root, ".git")
|
||||
local stat = vim.loop.fs_stat(git_folder)
|
||||
if stat and stat.type == "directory" then
|
||||
return git_folder, git_root
|
||||
end
|
||||
end
|
||||
return nil, nil
|
||||
end
|
||||
end
|
||||
|
||||
M.show_watched = function()
|
||||
local items = {}
|
||||
for _, handle in pairs(watched) do
|
||||
items[handle.path] = handle.references
|
||||
end
|
||||
log.info("Watched Folders: ", vim.inspect(items))
|
||||
end
|
||||
|
||||
---Watch a directory for changes to it's children. Not recursive.
|
||||
---@param path string The directory to watch.
|
||||
---@param custom_callback? function The callback to call when a change is detected.
|
||||
---@param allow_git_watch? boolean Allow watching of git folders.
|
||||
M.watch_folder = function(path, custom_callback, allow_git_watch)
|
||||
if not allow_git_watch then
|
||||
if path:find("/%.git$") or path:find("/%.git/") then
|
||||
-- git folders seem to throw off fs events constantly.
|
||||
log.debug("watch_folder(path): Skipping git folder: ", path)
|
||||
return
|
||||
end
|
||||
end
|
||||
local h = watched[path]
|
||||
if h == nil then
|
||||
log.trace("Starting new fs watch on: ", path)
|
||||
local callback = custom_callback
|
||||
or vim.schedule_wrap(function(err, fname)
|
||||
if fname and fname:match("^%.null[-]ls_.+") then
|
||||
-- null-ls temp file: https://github.com/jose-elias-alvarez/null-ls.nvim/pull/1075
|
||||
return
|
||||
end
|
||||
if err then
|
||||
log.error("file_event_callback: ", err)
|
||||
return
|
||||
end
|
||||
events.fire_event(events.FS_EVENT, { afile = path })
|
||||
end)
|
||||
h = {
|
||||
handle = vim.loop.new_fs_event(),
|
||||
path = path,
|
||||
references = 0,
|
||||
active = false,
|
||||
callback = callback,
|
||||
}
|
||||
watched[path] = h
|
||||
--w:start(path, flags, callback)
|
||||
else
|
||||
log.trace("Incrementing references for fs watch on: ", path)
|
||||
end
|
||||
h.references = h.references + 1
|
||||
end
|
||||
|
||||
M.watch_git_index = function(path, async)
|
||||
local function watch_git_folder(git_folder, git_root)
|
||||
if git_folder then
|
||||
local git_event_callback = vim.schedule_wrap(function(err, fname)
|
||||
if fname and fname:match("^.+%.lock$") then
|
||||
return
|
||||
end
|
||||
if fname and fname:match("^%._null-ls_.+") then
|
||||
-- null-ls temp file: https://github.com/jose-elias-alvarez/null-ls.nvim/pull/1075
|
||||
return
|
||||
end
|
||||
if err then
|
||||
log.error("git_event_callback: ", err)
|
||||
return
|
||||
end
|
||||
events.fire_event(events.GIT_EVENT, { path = fname, repository = git_root })
|
||||
end)
|
||||
|
||||
M.watch_folder(git_folder, git_event_callback, true)
|
||||
end
|
||||
end
|
||||
|
||||
if async then
|
||||
get_dot_git_folder(path, watch_git_folder)
|
||||
else
|
||||
watch_git_folder(get_dot_git_folder(path))
|
||||
end
|
||||
end
|
||||
|
||||
M.updated_watched = function()
|
||||
for path, w in pairs(watched) do
|
||||
if w.references > 0 then
|
||||
if not w.active then
|
||||
log.trace("References added for fs watch on: ", path, ", starting.")
|
||||
w.handle:start(path, flags, w.callback)
|
||||
w.active = true
|
||||
end
|
||||
else
|
||||
if w.active then
|
||||
log.trace("No more references for fs watch on: ", path, ", stopping.")
|
||||
w.handle:stop()
|
||||
w.active = false
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---Stop watching a directory. If there are no more references to the handle,
|
||||
---it will be destroyed. Otherwise, the reference count will be decremented.
|
||||
---@param path string The directory to stop watching.
|
||||
M.unwatch_folder = function(path, callback_id)
|
||||
local h = watched[path]
|
||||
if h then
|
||||
log.trace("Decrementing references for fs watch on: ", path, callback_id)
|
||||
h.references = h.references - 1
|
||||
else
|
||||
log.trace("(unwatch_folder) No fs watch found for: ", path)
|
||||
end
|
||||
end
|
||||
|
||||
M.unwatch_git_index = function(path, async)
|
||||
local function unwatch_git_folder(git_folder, _)
|
||||
if git_folder then
|
||||
M.unwatch_folder(git_folder)
|
||||
end
|
||||
end
|
||||
|
||||
if async then
|
||||
get_dot_git_folder(path, unwatch_git_folder)
|
||||
else
|
||||
unwatch_git_folder(get_dot_git_folder(path))
|
||||
end
|
||||
end
|
||||
|
||||
---Stop watching all directories. This is the nuclear option and it affects all
|
||||
---sources.
|
||||
M.unwatch_all = function()
|
||||
for _, h in pairs(watched) do
|
||||
h.handle:stop()
|
||||
h.handle = nil
|
||||
end
|
||||
watched = {}
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,157 @@
|
||||
--(c) 2008-2011 David Manura. Licensed under the same terms as Lua (MIT).
|
||||
|
||||
--Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
--of this software and associated documentation files (the "Software"), to deal
|
||||
--in the Software without restriction, including without limitation the rights
|
||||
--to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
--copies of the Software, and to permit persons to whom the Software is
|
||||
--furnished to do so, subject to the following conditions:
|
||||
|
||||
--The above copyright notice and this permission notice shall be included in
|
||||
--all copies or substantial portions of the Software.
|
||||
|
||||
--THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
--IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
--FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
--AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
--LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
--OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
--THE SOFTWARE.
|
||||
--(end license)
|
||||
|
||||
local M = { _TYPE = "module", _NAME = "globtopattern", _VERSION = "0.2.1.20120406" }
|
||||
|
||||
function M.globtopattern(g)
|
||||
-- Some useful references:
|
||||
-- - apr_fnmatch in Apache APR. For example,
|
||||
-- http://apr.apache.org/docs/apr/1.3/group__apr__fnmatch.html
|
||||
-- which cites POSIX 1003.2-1992, section B.6.
|
||||
|
||||
local p = "^" -- pattern being built
|
||||
local i = 0 -- index in g
|
||||
local c -- char at index i in g.
|
||||
|
||||
-- unescape glob char
|
||||
local function unescape()
|
||||
if c == "\\" then
|
||||
i = i + 1
|
||||
c = g:sub(i, i)
|
||||
if c == "" then
|
||||
p = "[^]"
|
||||
return false
|
||||
end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
-- escape pattern char
|
||||
local function escape(c)
|
||||
return c:match("^%w$") and c or "%" .. c
|
||||
end
|
||||
|
||||
-- Convert tokens at end of charset.
|
||||
local function charset_end()
|
||||
while 1 do
|
||||
if c == "" then
|
||||
p = "[^]"
|
||||
return false
|
||||
elseif c == "]" then
|
||||
p = p .. "]"
|
||||
break
|
||||
else
|
||||
if not unescape() then
|
||||
break
|
||||
end
|
||||
local c1 = c
|
||||
i = i + 1
|
||||
c = g:sub(i, i)
|
||||
if c == "" then
|
||||
p = "[^]"
|
||||
return false
|
||||
elseif c == "-" then
|
||||
i = i + 1
|
||||
c = g:sub(i, i)
|
||||
if c == "" then
|
||||
p = "[^]"
|
||||
return false
|
||||
elseif c == "]" then
|
||||
p = p .. escape(c1) .. "%-]"
|
||||
break
|
||||
else
|
||||
if not unescape() then
|
||||
break
|
||||
end
|
||||
p = p .. escape(c1) .. "-" .. escape(c)
|
||||
end
|
||||
elseif c == "]" then
|
||||
p = p .. escape(c1) .. "]"
|
||||
break
|
||||
else
|
||||
p = p .. escape(c1)
|
||||
i = i - 1 -- put back
|
||||
end
|
||||
end
|
||||
i = i + 1
|
||||
c = g:sub(i, i)
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
-- Convert tokens in charset.
|
||||
local function charset()
|
||||
i = i + 1
|
||||
c = g:sub(i, i)
|
||||
if c == "" or c == "]" then
|
||||
p = "[^]"
|
||||
return false
|
||||
elseif c == "^" or c == "!" then
|
||||
i = i + 1
|
||||
c = g:sub(i, i)
|
||||
if c == "]" then
|
||||
-- ignored
|
||||
else
|
||||
p = p .. "[^"
|
||||
if not charset_end() then
|
||||
return false
|
||||
end
|
||||
end
|
||||
else
|
||||
p = p .. "["
|
||||
if not charset_end() then
|
||||
return false
|
||||
end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
-- Convert tokens.
|
||||
while 1 do
|
||||
i = i + 1
|
||||
c = g:sub(i, i)
|
||||
if c == "" then
|
||||
p = p .. "$"
|
||||
break
|
||||
elseif c == "?" then
|
||||
p = p .. "."
|
||||
elseif c == "*" then
|
||||
p = p .. ".*"
|
||||
elseif c == "[" then
|
||||
if not charset() then
|
||||
break
|
||||
end
|
||||
elseif c == "\\" then
|
||||
i = i + 1
|
||||
c = g:sub(i, i)
|
||||
if c == "" then
|
||||
p = p .. "\\$"
|
||||
break
|
||||
end
|
||||
p = p .. escape(c)
|
||||
else
|
||||
p = p .. escape(c)
|
||||
end
|
||||
end
|
||||
return p
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,71 @@
|
||||
--This file should contain all commands meant to be used by mappings.
|
||||
|
||||
local vim = vim
|
||||
local cc = require("neo-tree.sources.common.commands")
|
||||
local utils = require("neo-tree.utils")
|
||||
local manager = require("neo-tree.sources.manager")
|
||||
|
||||
local M = {}
|
||||
|
||||
local refresh = utils.wrap(manager.refresh, "git_status")
|
||||
local redraw = utils.wrap(manager.redraw, "git_status")
|
||||
|
||||
-- ----------------------------------------------------------------------------
|
||||
-- Common commands
|
||||
-- ----------------------------------------------------------------------------
|
||||
M.add = function(state)
|
||||
cc.add(state, refresh)
|
||||
end
|
||||
|
||||
M.add_directory = function(state)
|
||||
cc.add_directory(state, refresh)
|
||||
end
|
||||
|
||||
---Marks node as copied, so that it can be pasted somewhere else.
|
||||
M.copy_to_clipboard = function(state)
|
||||
cc.copy_to_clipboard(state, redraw)
|
||||
end
|
||||
|
||||
M.copy_to_clipboard_visual = function(state, selected_nodes)
|
||||
cc.copy_to_clipboard_visual(state, selected_nodes, redraw)
|
||||
end
|
||||
|
||||
---Marks node as cut, so that it can be pasted (moved) somewhere else.
|
||||
M.cut_to_clipboard = function(state)
|
||||
cc.cut_to_clipboard(state, redraw)
|
||||
end
|
||||
|
||||
M.cut_to_clipboard_visual = function(state, selected_nodes)
|
||||
cc.cut_to_clipboard_visual(state, selected_nodes, redraw)
|
||||
end
|
||||
|
||||
M.copy = function(state)
|
||||
cc.copy(state, redraw)
|
||||
end
|
||||
|
||||
M.move = function(state)
|
||||
cc.move(state, redraw)
|
||||
end
|
||||
|
||||
---Pastes all items from the clipboard to the current directory.
|
||||
M.paste_from_clipboard = function(state)
|
||||
cc.paste_from_clipboard(state, refresh)
|
||||
end
|
||||
|
||||
M.delete = function(state)
|
||||
cc.delete(state, refresh)
|
||||
end
|
||||
|
||||
M.delete_visual = function(state, selected_nodes)
|
||||
cc.delete_visual(state, selected_nodes, refresh)
|
||||
end
|
||||
|
||||
M.refresh = refresh
|
||||
|
||||
M.rename = function(state)
|
||||
cc.rename(state, refresh)
|
||||
end
|
||||
|
||||
cc._add_common_commands(M)
|
||||
|
||||
return M
|
||||
@ -0,0 +1,44 @@
|
||||
-- This file contains the built-in components. Each componment is a function
|
||||
-- that takes the following arguments:
|
||||
-- config: A table containing the configuration provided by the user
|
||||
-- when declaring this component in their renderer config.
|
||||
-- node: A NuiNode object for the currently focused node.
|
||||
-- state: The current state of the source providing the items.
|
||||
--
|
||||
-- The function should return either a table, or a list of tables, each of which
|
||||
-- contains the following keys:
|
||||
-- text: The text to display for this item.
|
||||
-- highlight: The highlight group to apply to this text.
|
||||
|
||||
local highlights = require("neo-tree.ui.highlights")
|
||||
local common = require("neo-tree.sources.common.components")
|
||||
|
||||
local M = {}
|
||||
|
||||
M.name = function(config, node, state)
|
||||
local highlight = config.highlight or highlights.FILE_NAME_OPENED
|
||||
local name = node.name
|
||||
if node.type == "directory" then
|
||||
if node:get_depth() == 1 then
|
||||
highlight = highlights.ROOT_NAME
|
||||
if node:has_children() then
|
||||
name = "GIT STATUS for " .. name
|
||||
else
|
||||
name = "GIT STATUS (working tree clean) for " .. name
|
||||
end
|
||||
else
|
||||
highlight = highlights.DIRECTORY_NAME
|
||||
end
|
||||
elseif config.use_git_status_colors then
|
||||
local git_status = state.components.git_status({}, node, state)
|
||||
if git_status and git_status.highlight then
|
||||
highlight = git_status.highlight
|
||||
end
|
||||
end
|
||||
return {
|
||||
text = name,
|
||||
highlight = highlight,
|
||||
}
|
||||
end
|
||||
|
||||
return vim.tbl_deep_extend("force", common, M)
|
||||
@ -0,0 +1,104 @@
|
||||
--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 renderer = require("neo-tree.ui.renderer")
|
||||
local items = require("neo-tree.sources.git_status.lib.items")
|
||||
local events = require("neo-tree.events")
|
||||
local manager = require("neo-tree.sources.manager")
|
||||
|
||||
local M = {
|
||||
name = "git_status",
|
||||
display_name = " Git "
|
||||
}
|
||||
|
||||
local wrap = function(func)
|
||||
return utils.wrap(func, M.name)
|
||||
end
|
||||
|
||||
local get_state = function()
|
||||
return manager.get_state(M.name)
|
||||
end
|
||||
|
||||
---Navigate to the given path.
|
||||
---@param path string Path to navigate to. If empty, will navigate to the cwd.
|
||||
M.navigate = function(state, path, path_to_reveal, callback, async)
|
||||
state.dirty = false
|
||||
if path_to_reveal then
|
||||
renderer.position.set(state, path_to_reveal)
|
||||
end
|
||||
items.get_git_status(state)
|
||||
|
||||
if type(callback) == "function" then
|
||||
vim.schedule(callback)
|
||||
end
|
||||
end
|
||||
|
||||
M.refresh = function()
|
||||
manager.refresh(M.name)
|
||||
end
|
||||
|
||||
---Configures the plugin, should be called before the plugin is used.
|
||||
---@param config table Configuration table containing any keys that the user
|
||||
--wants to change from the defaults. May be empty to accept default values.
|
||||
M.setup = function(config, global_config)
|
||||
if config.before_render then
|
||||
--convert to new event system
|
||||
manager.subscribe(M.name, {
|
||||
event = events.BEFORE_RENDER,
|
||||
handler = function(state)
|
||||
local this_state = get_state()
|
||||
if state == this_state then
|
||||
config.before_render(this_state)
|
||||
end
|
||||
end,
|
||||
})
|
||||
end
|
||||
|
||||
if global_config.enable_refresh_on_write then
|
||||
manager.subscribe(M.name, {
|
||||
event = events.VIM_BUFFER_CHANGED,
|
||||
handler = function(args)
|
||||
if utils.is_real_file(args.afile) then
|
||||
M.refresh()
|
||||
end
|
||||
end,
|
||||
})
|
||||
end
|
||||
|
||||
if config.bind_to_cwd then
|
||||
manager.subscribe(M.name, {
|
||||
event = events.VIM_DIR_CHANGED,
|
||||
handler = M.refresh,
|
||||
})
|
||||
end
|
||||
|
||||
if global_config.enable_diagnostics then
|
||||
manager.subscribe(M.name, {
|
||||
event = events.STATE_CREATED,
|
||||
handler = function(state)
|
||||
state.diagnostics_lookup = utils.get_diagnostic_counts()
|
||||
end,
|
||||
})
|
||||
manager.subscribe(M.name, {
|
||||
event = events.VIM_DIAGNOSTIC_CHANGED,
|
||||
handler = wrap(manager.diagnostics_changed),
|
||||
})
|
||||
end
|
||||
|
||||
--Configure event handlers for modified files
|
||||
if global_config.enable_modified_markers then
|
||||
manager.subscribe(M.name, {
|
||||
event = events.VIM_BUFFER_MODIFIED_SET,
|
||||
handler = wrap(manager.opened_buffers_changed),
|
||||
})
|
||||
end
|
||||
|
||||
manager.subscribe(M.name, {
|
||||
event = events.GIT_EVENT,
|
||||
handler = M.refresh,
|
||||
})
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,49 @@
|
||||
local vim = vim
|
||||
local renderer = require("neo-tree.ui.renderer")
|
||||
local file_items = require("neo-tree.sources.common.file-items")
|
||||
local log = require("neo-tree.log")
|
||||
local git = require("neo-tree.git")
|
||||
|
||||
local M = {}
|
||||
|
||||
---Get a table of all open buffers, along with all parent paths of those buffers.
|
||||
---The paths are the keys of the table, and all the values are 'true'.
|
||||
M.get_git_status = function(state)
|
||||
if state.loading then
|
||||
return
|
||||
end
|
||||
state.loading = true
|
||||
local status_lookup, project_root = git.status(state.git_base, true)
|
||||
state.path = project_root or state.path or vim.fn.getcwd()
|
||||
local context = file_items.create_context()
|
||||
context.state = state
|
||||
-- Create root folder
|
||||
local root = file_items.create_item(context, state.path, "directory")
|
||||
root.name = vim.fn.fnamemodify(root.path, ":~")
|
||||
root.loaded = true
|
||||
root.search_pattern = state.search_pattern
|
||||
context.folders[root.path] = root
|
||||
|
||||
for path, status in pairs(status_lookup) do
|
||||
local success, item = pcall(file_items.create_item, context, path, "file")
|
||||
item.status = status
|
||||
if success then
|
||||
item.extra = {
|
||||
git_status = status,
|
||||
}
|
||||
else
|
||||
log.error("Error creating item for " .. path .. ": " .. item)
|
||||
end
|
||||
end
|
||||
|
||||
state.git_status_lookup = status_lookup
|
||||
state.default_expanded_nodes = {}
|
||||
for id, _ in pairs(context.folders) do
|
||||
table.insert(state.default_expanded_nodes, id)
|
||||
end
|
||||
file_items.advanced_sort(root.children, state)
|
||||
renderer.show_nodes({ root }, state)
|
||||
state.loading = false
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,652 @@
|
||||
--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 = {}
|
||||
state.git_base = "HEAD"
|
||||
events.fire_event(events.STATE_CREATED, state)
|
||||
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
|
||||
local path = utils.escape_path_for_cmd(state.path)
|
||||
if winid > 0 then
|
||||
vim.cmd("lcd " .. path)
|
||||
elseif tabnr > 0 then
|
||||
vim.cmd("tcd " .. path)
|
||||
else
|
||||
vim.cmd("cd " .. 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)
|
||||
-- Iterate in reverse because we are removing items during loop
|
||||
for i = #all_states, 1, -1 do
|
||||
local state = all_states[i]
|
||||
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
|
||||
-- Iterate in reverse because we are removing items during loop
|
||||
for i = #all_states, 1, -1 do
|
||||
local state = all_states[i]
|
||||
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
|
||||
-- Iterate in reverse because we are removing items during loop
|
||||
for i = #all_states, 1, -1 do
|
||||
local state = all_states[i]
|
||||
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
|
||||
|
||||
--- DEPRECATED: To be removed in 4.0
|
||||
--- use `require("neo-tree.command").execute({ source_name = source_name, action = "focus", reveal = true })` instead
|
||||
M.reveal_current_file = function(source_name, callback, force_cwd)
|
||||
log.warn([[DEPRECATED: use `require("neo-tree.command").execute({ source_name = source_name, action = "focus", reveal = true })` instead]])
|
||||
|
||||
log.trace("Revealing current file")
|
||||
local state = M.get_state(source_name)
|
||||
state.current_position = nil
|
||||
|
||||
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)
|
||||
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)
|
||||
return
|
||||
end
|
||||
if path then
|
||||
if not renderer.focus_node(state, path) then
|
||||
M.focus(source_name, path, callback)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- DEPRECATED: To be removed in 4.0
|
||||
--- use `require("neo-tree.command").execute({ source_name = source_name, action = "focus", reveal = true, position = "current" })` instead
|
||||
M.reveal_in_split = function(source_name, callback)
|
||||
log.warn([[DEPRECATED: use `require("neo-tree.command").execute({ source_name = source_name, action = "focus", reveal = true, position = "current" })` instead]])
|
||||
|
||||
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
|
||||
@ -0,0 +1,292 @@
|
||||
local log = require("neo-tree.log")
|
||||
local utils = require("neo-tree.utils")
|
||||
local vim = vim
|
||||
local M = {}
|
||||
|
||||
---@type integer
|
||||
M.ns_id = vim.api.nvim_create_namespace("neo-tree.nvim")
|
||||
|
||||
M.BUFFER_NUMBER = "NeoTreeBufferNumber"
|
||||
M.CURSOR_LINE = "NeoTreeCursorLine"
|
||||
M.DIM_TEXT = "NeoTreeDimText"
|
||||
M.DIRECTORY_ICON = "NeoTreeDirectoryIcon"
|
||||
M.DIRECTORY_NAME = "NeoTreeDirectoryName"
|
||||
M.DOTFILE = "NeoTreeDotfile"
|
||||
M.FADE_TEXT_1 = "NeoTreeFadeText1"
|
||||
M.FADE_TEXT_2 = "NeoTreeFadeText2"
|
||||
M.FILE_ICON = "NeoTreeFileIcon"
|
||||
M.FILE_NAME = "NeoTreeFileName"
|
||||
M.FILE_NAME_OPENED = "NeoTreeFileNameOpened"
|
||||
M.FILE_STATS = "NeoTreeFileStats"
|
||||
M.FILE_STATS_HEADER = "NeoTreeFileStatsHeader"
|
||||
M.FILTER_TERM = "NeoTreeFilterTerm"
|
||||
M.FLOAT_BORDER = "NeoTreeFloatBorder"
|
||||
M.FLOAT_NORMAL = "NeoTreeFloatNormal"
|
||||
M.FLOAT_TITLE = "NeoTreeFloatTitle"
|
||||
M.GIT_ADDED = "NeoTreeGitAdded"
|
||||
M.GIT_CONFLICT = "NeoTreeGitConflict"
|
||||
M.GIT_DELETED = "NeoTreeGitDeleted"
|
||||
M.GIT_IGNORED = "NeoTreeGitIgnored"
|
||||
M.GIT_MODIFIED = "NeoTreeGitModified"
|
||||
M.GIT_RENAMED = "NeoTreeGitRenamed"
|
||||
M.GIT_STAGED = "NeoTreeGitStaged"
|
||||
M.GIT_UNTRACKED = "NeoTreeGitUntracked"
|
||||
M.GIT_UNSTAGED = "NeoTreeGitUnstaged"
|
||||
M.HIDDEN_BY_NAME = "NeoTreeHiddenByName"
|
||||
M.INDENT_MARKER = "NeoTreeIndentMarker"
|
||||
M.MESSAGE = "NeoTreeMessage"
|
||||
M.MODIFIED = "NeoTreeModified"
|
||||
M.NORMAL = "NeoTreeNormal"
|
||||
M.NORMALNC = "NeoTreeNormalNC"
|
||||
M.SIGNCOLUMN = "NeoTreeSignColumn"
|
||||
M.STATUS_LINE = "NeoTreeStatusLine"
|
||||
M.STATUS_LINE_NC = "NeoTreeStatusLineNC"
|
||||
M.TAB_ACTIVE = "NeoTreeTabActive"
|
||||
M.TAB_INACTIVE = "NeoTreeTabInactive"
|
||||
M.TAB_SEPARATOR_ACTIVE = "NeoTreeTabSeparatorActive"
|
||||
M.TAB_SEPARATOR_INACTIVE = "NeoTreeTabSeparatorInactive"
|
||||
M.VERTSPLIT = "NeoTreeVertSplit"
|
||||
M.WINSEPARATOR = "NeoTreeWinSeparator"
|
||||
M.END_OF_BUFFER = "NeoTreeEndOfBuffer"
|
||||
M.ROOT_NAME = "NeoTreeRootName"
|
||||
M.SYMBOLIC_LINK_TARGET = "NeoTreeSymbolicLinkTarget"
|
||||
M.TITLE_BAR = "NeoTreeTitleBar"
|
||||
M.INDENT_MARKER = "NeoTreeIndentMarker"
|
||||
M.EXPANDER = "NeoTreeExpander"
|
||||
M.WINDOWS_HIDDEN = "NeoTreeWindowsHidden"
|
||||
M.PREVIEW = "NeoTreePreview"
|
||||
|
||||
local function dec_to_hex(n, chars)
|
||||
chars = chars or 6
|
||||
local hex = string.format("%0" .. chars .. "x", n)
|
||||
while #hex < chars do
|
||||
hex = "0" .. hex
|
||||
end
|
||||
return hex
|
||||
end
|
||||
|
||||
---If the given highlight group is not defined, define it.
|
||||
---@param hl_group_name string The name of the highlight group.
|
||||
---@param link_to_if_exists table A list of highlight groups to link to, in
|
||||
--order of priority. The first one that exists will be used.
|
||||
---@param background string|nil The background color to use, in hex, if the highlight group
|
||||
--is not defined and it is not linked to another group.
|
||||
---@param foreground string|nil The foreground color to use, in hex, if the highlight group
|
||||
--is not defined and it is not linked to another group.
|
||||
---@gui string|nil The gui to use, if the highlight group is not defined and it is not linked
|
||||
--to another group.
|
||||
---@return table table The highlight group values.
|
||||
M.create_highlight_group = function(hl_group_name, link_to_if_exists, background, foreground, gui)
|
||||
local success, hl_group = pcall(vim.api.nvim_get_hl_by_name, hl_group_name, true)
|
||||
if not success or not hl_group.foreground or not hl_group.background then
|
||||
for _, link_to in ipairs(link_to_if_exists) do
|
||||
success, hl_group = pcall(vim.api.nvim_get_hl_by_name, link_to, true)
|
||||
if success then
|
||||
local new_group_has_settings = background or foreground or gui
|
||||
local link_to_has_settings = hl_group.foreground or hl_group.background
|
||||
if link_to_has_settings or not new_group_has_settings then
|
||||
vim.cmd("highlight default link " .. hl_group_name .. " " .. link_to)
|
||||
return hl_group
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if type(background) == "number" then
|
||||
background = dec_to_hex(background)
|
||||
end
|
||||
if type(foreground) == "number" then
|
||||
foreground = dec_to_hex(foreground)
|
||||
end
|
||||
|
||||
local cmd = "highlight default " .. hl_group_name
|
||||
if background then
|
||||
cmd = cmd .. " guibg=#" .. background
|
||||
end
|
||||
if foreground then
|
||||
cmd = cmd .. " guifg=#" .. foreground
|
||||
else
|
||||
cmd = cmd .. " guifg=NONE"
|
||||
end
|
||||
if gui then
|
||||
cmd = cmd .. " gui=" .. gui
|
||||
end
|
||||
vim.cmd(cmd)
|
||||
|
||||
return {
|
||||
background = background and tonumber(background, 16) or nil,
|
||||
foreground = foreground and tonumber(foreground, 16) or nil,
|
||||
}
|
||||
end
|
||||
return hl_group
|
||||
end
|
||||
|
||||
local calculate_faded_highlight_group = function (hl_group_name, fade_percentage)
|
||||
local normal = vim.api.nvim_get_hl_by_name("Normal", true)
|
||||
if type(normal.foreground) ~= "number" then
|
||||
if vim.api.nvim_get_option("background") == "dark" then
|
||||
normal.foreground = 0xffffff
|
||||
else
|
||||
normal.foreground = 0x000000
|
||||
end
|
||||
end
|
||||
if type(normal.background) ~= "number" then
|
||||
if vim.api.nvim_get_option("background") == "dark" then
|
||||
normal.background = 0x000000
|
||||
else
|
||||
normal.background = 0xffffff
|
||||
end
|
||||
end
|
||||
local foreground = dec_to_hex(normal.foreground)
|
||||
local background = dec_to_hex(normal.background)
|
||||
|
||||
local hl_group = vim.api.nvim_get_hl_by_name(hl_group_name, true)
|
||||
if type(hl_group.foreground) == "number" then
|
||||
foreground = dec_to_hex(hl_group.foreground)
|
||||
end
|
||||
if type(hl_group.background) == "number" then
|
||||
background = dec_to_hex(hl_group.background)
|
||||
end
|
||||
|
||||
local gui = {}
|
||||
if hl_group.bold then
|
||||
table.insert(gui, "bold")
|
||||
end
|
||||
if hl_group.italic then
|
||||
table.insert(gui, "italic")
|
||||
end
|
||||
if hl_group.underline then
|
||||
table.insert(gui, "underline")
|
||||
end
|
||||
if hl_group.undercurl then
|
||||
table.insert(gui, "undercurl")
|
||||
end
|
||||
if #gui > 0 then
|
||||
gui = table.concat(gui, ",")
|
||||
else
|
||||
gui = nil
|
||||
end
|
||||
|
||||
local f_red = tonumber(foreground:sub(1, 2), 16)
|
||||
local f_green = tonumber(foreground:sub(3, 4), 16)
|
||||
local f_blue = tonumber(foreground:sub(5, 6), 16)
|
||||
|
||||
local b_red = tonumber(background:sub(1, 2), 16)
|
||||
local b_green = tonumber(background:sub(3, 4), 16)
|
||||
local b_blue = tonumber(background:sub(5, 6), 16)
|
||||
|
||||
local red = (f_red * fade_percentage) + (b_red * (1 - fade_percentage))
|
||||
local green = (f_green * fade_percentage) + (b_green * (1 - fade_percentage))
|
||||
local blue = (f_blue * fade_percentage) + (b_blue * (1 - fade_percentage))
|
||||
|
||||
local new_foreground =
|
||||
string.format("%s%s%s", dec_to_hex(red, 2), dec_to_hex(green, 2), dec_to_hex(blue, 2))
|
||||
|
||||
return {
|
||||
background = hl_group.background,
|
||||
foreground = new_foreground,
|
||||
gui = gui,
|
||||
}
|
||||
end
|
||||
|
||||
local faded_highlight_group_cache = {}
|
||||
M.get_faded_highlight_group = function(hl_group_name, fade_percentage)
|
||||
if type(hl_group_name) ~= "string" then
|
||||
error("hl_group_name must be a string")
|
||||
end
|
||||
if type(fade_percentage) ~= "number" then
|
||||
error("hl_group_name must be a number")
|
||||
end
|
||||
if fade_percentage < 0 or fade_percentage > 1 then
|
||||
error("fade_percentage must be between 0 and 1")
|
||||
end
|
||||
|
||||
local key = hl_group_name .. "_" .. tostring(math.floor(fade_percentage * 100))
|
||||
if faded_highlight_group_cache[key] then
|
||||
return faded_highlight_group_cache[key]
|
||||
end
|
||||
|
||||
local faded = calculate_faded_highlight_group(hl_group_name, fade_percentage)
|
||||
|
||||
M.create_highlight_group(key, {}, faded.background, faded.foreground, faded.gui)
|
||||
faded_highlight_group_cache[key] = key
|
||||
return key
|
||||
end
|
||||
|
||||
M.setup = function()
|
||||
-- Reset this here in case of color scheme change
|
||||
faded_highlight_group_cache = {}
|
||||
|
||||
local normal_hl = M.create_highlight_group(M.NORMAL, { "Normal" })
|
||||
local normalnc_hl = M.create_highlight_group(M.NORMALNC, { "NormalNC", M.NORMAL })
|
||||
|
||||
M.create_highlight_group(M.SIGNCOLUMN, { "SignColumn", M.NORMAL })
|
||||
|
||||
M.create_highlight_group(M.STATUS_LINE, { "StatusLine" })
|
||||
M.create_highlight_group(M.STATUS_LINE_NC, { "StatusLineNC" })
|
||||
|
||||
M.create_highlight_group(M.VERTSPLIT, { "VertSplit" })
|
||||
M.create_highlight_group(M.WINSEPARATOR, { "WinSeparator" })
|
||||
|
||||
M.create_highlight_group(M.END_OF_BUFFER, { "EndOfBuffer" })
|
||||
|
||||
local float_border_hl =
|
||||
M.create_highlight_group(M.FLOAT_BORDER, { "FloatBorder" }, normalnc_hl.background, "444444")
|
||||
|
||||
M.create_highlight_group(M.FLOAT_NORMAL, { "NormalFloat", M.NORMAL })
|
||||
|
||||
M.create_highlight_group(M.FLOAT_TITLE, {}, float_border_hl.background, normal_hl.foreground)
|
||||
|
||||
local title_fg = normal_hl.background
|
||||
if title_fg == float_border_hl.foreground then
|
||||
title_fg = normal_hl.foreground
|
||||
end
|
||||
M.create_highlight_group(M.TITLE_BAR, {}, float_border_hl.foreground, title_fg)
|
||||
|
||||
local dim_text = calculate_faded_highlight_group("NeoTreeNormal", 0.3)
|
||||
|
||||
M.create_highlight_group(M.BUFFER_NUMBER, { "SpecialChar" })
|
||||
--M.create_highlight_group(M.DIM_TEXT, {}, nil, "505050")
|
||||
M.create_highlight_group(M.MESSAGE, {}, nil, dim_text.foreground, "italic")
|
||||
M.create_highlight_group(M.FADE_TEXT_1, {}, nil, "626262")
|
||||
M.create_highlight_group(M.FADE_TEXT_2, {}, nil, "444444")
|
||||
M.create_highlight_group(M.DOTFILE, {}, nil, "626262")
|
||||
M.create_highlight_group(M.HIDDEN_BY_NAME, { M.DOTFILE }, nil, nil)
|
||||
M.create_highlight_group(M.CURSOR_LINE, { "CursorLine" }, nil, nil, "bold")
|
||||
M.create_highlight_group(M.DIM_TEXT, {}, nil, dim_text.foreground)
|
||||
M.create_highlight_group(M.DIRECTORY_NAME, { "Directory" }, "NONE", "NONE")
|
||||
M.create_highlight_group(M.DIRECTORY_ICON, { "Directory" }, nil, "73cef4")
|
||||
M.create_highlight_group(M.FILE_ICON, { M.DIRECTORY_ICON })
|
||||
M.create_highlight_group(M.FILE_NAME, {}, "NONE", "NONE")
|
||||
M.create_highlight_group(M.FILE_NAME_OPENED, {}, nil, nil, "bold")
|
||||
M.create_highlight_group(M.SYMBOLIC_LINK_TARGET, { M.FILE_NAME })
|
||||
M.create_highlight_group(M.FILTER_TERM, { "SpecialChar", "Normal" })
|
||||
M.create_highlight_group(M.ROOT_NAME, {}, nil, nil, "bold,italic")
|
||||
M.create_highlight_group(M.INDENT_MARKER, { M.DIM_TEXT })
|
||||
M.create_highlight_group(M.EXPANDER, { M.DIM_TEXT })
|
||||
M.create_highlight_group(M.MODIFIED, {}, nil, "d7d787")
|
||||
M.create_highlight_group(M.WINDOWS_HIDDEN, { M.DOTFILE }, nil, nil)
|
||||
M.create_highlight_group(M.PREVIEW, { "Search" }, nil, nil)
|
||||
|
||||
M.create_highlight_group(M.GIT_ADDED, { "GitGutterAdd", "GitSignsAdd" }, nil, "5faf5f")
|
||||
M.create_highlight_group(M.GIT_DELETED, { "GitGutterDelete", "GitSignsDelete" }, nil, "ff5900")
|
||||
M.create_highlight_group(M.GIT_MODIFIED, { "GitGutterChange", "GitSignsChange" }, nil, "d7af5f")
|
||||
local conflict = M.create_highlight_group(M.GIT_CONFLICT, {}, nil, "ff8700", "italic,bold")
|
||||
M.create_highlight_group(M.GIT_IGNORED, { M.DOTFILE }, nil, nil)
|
||||
M.create_highlight_group(M.GIT_RENAMED, { M.GIT_MODIFIED }, nil, nil)
|
||||
M.create_highlight_group(M.GIT_STAGED, { M.GIT_ADDED }, nil, nil)
|
||||
M.create_highlight_group(M.GIT_UNSTAGED, { M.GIT_CONFLICT }, nil, nil)
|
||||
M.create_highlight_group(M.GIT_UNTRACKED, {}, nil, conflict.foreground, "italic")
|
||||
|
||||
M.create_highlight_group(M.TAB_ACTIVE, {}, nil, nil, "bold")
|
||||
M.create_highlight_group(M.TAB_INACTIVE, {}, "141414", "777777")
|
||||
M.create_highlight_group(M.TAB_SEPARATOR_ACTIVE, {}, nil, "0a0a0a")
|
||||
M.create_highlight_group(M.TAB_SEPARATOR_INACTIVE, {}, "141414", "101010")
|
||||
|
||||
local faded_normal = calculate_faded_highlight_group("NeoTreeNormal", 0.4)
|
||||
M.create_highlight_group(M.FILE_STATS, {}, nil, faded_normal.foreground)
|
||||
|
||||
local faded_root = calculate_faded_highlight_group("NeoTreeRootName", 0.5)
|
||||
M.create_highlight_group(M.FILE_STATS_HEADER, {}, nil, faded_root.foreground, faded_root.gui)
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,99 @@
|
||||
local vim = vim
|
||||
local Input = require("nui.input")
|
||||
local popups = require("neo-tree.ui.popups")
|
||||
local utils = require("neo-tree.utils")
|
||||
local events = require("neo-tree.events")
|
||||
|
||||
local M = {}
|
||||
|
||||
local should_use_popup_input = function()
|
||||
local nt = require("neo-tree")
|
||||
return utils.get_value(nt.config, "use_popups_for_input", true, false)
|
||||
end
|
||||
|
||||
M.show_input = function(input, callback)
|
||||
input:mount()
|
||||
|
||||
input:map("i", "<esc>", function()
|
||||
vim.cmd("stopinsert")
|
||||
input:unmount()
|
||||
end, { noremap = true })
|
||||
|
||||
input:map("n", "<esc>", function()
|
||||
input:unmount()
|
||||
end, { noremap = true })
|
||||
|
||||
input:map("n", "q", function()
|
||||
input:unmount()
|
||||
end, { noremap = true })
|
||||
|
||||
input:map("i", "<C-w>", "<C-S-w>", { noremap = true })
|
||||
|
||||
local event = require("nui.utils.autocmd").event
|
||||
input:on({ event.BufLeave, event.BufDelete }, function()
|
||||
input:unmount()
|
||||
if callback then
|
||||
callback()
|
||||
end
|
||||
end, { once = true })
|
||||
|
||||
if input.prompt_type ~= "confirm" then
|
||||
vim.schedule(function()
|
||||
events.fire_event(events.NEO_TREE_POPUP_INPUT_READY, {
|
||||
bufnr = input.bufnr,
|
||||
winid = input.winid,
|
||||
})
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
M.input = function(message, default_value, callback, options, completion)
|
||||
if should_use_popup_input() then
|
||||
local popup_options = popups.popup_options(message, 10, options)
|
||||
|
||||
local input = Input(popup_options, {
|
||||
prompt = " ",
|
||||
default_value = default_value,
|
||||
on_submit = callback,
|
||||
})
|
||||
|
||||
M.show_input(input)
|
||||
else
|
||||
local opts = {
|
||||
prompt = message .. "\n",
|
||||
default = default_value,
|
||||
}
|
||||
if vim.opt.cmdheight:get() == 0 then
|
||||
-- NOTE: I really don't know why but letters before the first '\n' is not rendered execpt in noice.nvim
|
||||
-- when vim.opt.cmdheight = 0 <2023-10-24, pysan3>
|
||||
opts.prompt = "Neo-tree Popup\n" .. opts.prompt
|
||||
end
|
||||
if completion then
|
||||
opts.completion = completion
|
||||
end
|
||||
vim.ui.input(opts, callback)
|
||||
end
|
||||
end
|
||||
|
||||
M.confirm = function(message, callback)
|
||||
if should_use_popup_input() then
|
||||
local popup_options = popups.popup_options(message, 10)
|
||||
|
||||
local input = Input(popup_options, {
|
||||
prompt = " y/n: ",
|
||||
on_close = function()
|
||||
callback(false)
|
||||
end,
|
||||
on_submit = function(value)
|
||||
callback(value == "y" or value == "Y")
|
||||
end,
|
||||
})
|
||||
|
||||
input.prompt_type = "confirm"
|
||||
M.show_input(input)
|
||||
else
|
||||
callback(vim.fn.confirm(message, "&Yes\n&No") == 1)
|
||||
end
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,126 @@
|
||||
local vim = vim
|
||||
local NuiText = require("nui.text")
|
||||
local NuiPopup = require("nui.popup")
|
||||
local highlights = require("neo-tree.ui.highlights")
|
||||
local log = require("neo-tree.log")
|
||||
|
||||
local M = {}
|
||||
|
||||
M.popup_options = function(title, min_width, override_options)
|
||||
min_width = min_width or 30
|
||||
local width = string.len(title) + 2
|
||||
|
||||
local nt = require("neo-tree")
|
||||
local popup_border_style = nt.config.popup_border_style
|
||||
local popup_border_text = NuiText(" " .. title .. " ", highlights.FLOAT_TITLE)
|
||||
local col = 0
|
||||
-- fix popup position when using multigrid
|
||||
local popup_last_col = vim.api.nvim_win_get_position(0)[2] + width + 2
|
||||
if popup_last_col >= vim.o.columns then
|
||||
col = vim.o.columns - popup_last_col
|
||||
end
|
||||
local popup_options = {
|
||||
ns_id = highlights.ns_id,
|
||||
relative = "cursor",
|
||||
position = {
|
||||
row = 1,
|
||||
col = col,
|
||||
},
|
||||
size = width,
|
||||
border = {
|
||||
text = {
|
||||
top = popup_border_text,
|
||||
},
|
||||
style = popup_border_style,
|
||||
highlight = highlights.FLOAT_BORDER,
|
||||
},
|
||||
win_options = {
|
||||
winhighlight = "Normal:"
|
||||
.. highlights.FLOAT_NORMAL
|
||||
.. ",FloatBorder:"
|
||||
.. highlights.FLOAT_BORDER,
|
||||
},
|
||||
buf_options = {
|
||||
bufhidden = "delete",
|
||||
buflisted = false,
|
||||
filetype = "neo-tree-popup",
|
||||
},
|
||||
}
|
||||
|
||||
if popup_border_style == "NC" then
|
||||
local blank = NuiText(" ", highlights.TITLE_BAR)
|
||||
popup_border_text = NuiText(" " .. title .. " ", highlights.TITLE_BAR)
|
||||
popup_options.border = {
|
||||
style = { "▕", blank, "▏", "▏", " ", "▔", " ", "▕" },
|
||||
highlight = highlights.FLOAT_BORDER,
|
||||
text = {
|
||||
top = popup_border_text,
|
||||
top_align = "left",
|
||||
},
|
||||
}
|
||||
end
|
||||
|
||||
if override_options then
|
||||
return vim.tbl_extend("force", popup_options, override_options)
|
||||
else
|
||||
return popup_options
|
||||
end
|
||||
end
|
||||
|
||||
M.alert = function(title, message, size)
|
||||
local lines = {}
|
||||
local max_line_width = title:len()
|
||||
local add_line = function(line)
|
||||
if not type(line) == "string" then
|
||||
line = tostring(line)
|
||||
end
|
||||
if line:len() > max_line_width then
|
||||
max_line_width = line:len()
|
||||
end
|
||||
table.insert(lines, line)
|
||||
end
|
||||
|
||||
if type(message) == "table" then
|
||||
for _, v in ipairs(message) do
|
||||
add_line(v)
|
||||
end
|
||||
else
|
||||
add_line(message)
|
||||
end
|
||||
|
||||
add_line("")
|
||||
add_line(" Press <Escape> or <Enter> to close")
|
||||
|
||||
local win_options = M.popup_options(title, 80)
|
||||
win_options.zindex = 60
|
||||
win_options.size = {
|
||||
width = max_line_width + 4,
|
||||
height = #lines + 1,
|
||||
}
|
||||
local win = NuiPopup(win_options)
|
||||
win:mount()
|
||||
|
||||
local success, msg = pcall(vim.api.nvim_buf_set_lines, win.bufnr, 0, 0, false, lines)
|
||||
if success then
|
||||
win:map("n", "<esc>", function(bufnr)
|
||||
win:unmount()
|
||||
end, { noremap = true })
|
||||
|
||||
win:map("n", "<enter>", function(bufnr)
|
||||
win:unmount()
|
||||
end, { noremap = true })
|
||||
|
||||
local event = require("nui.utils.autocmd").event
|
||||
win:on({ event.BufLeave, event.BufDelete }, function()
|
||||
win:unmount()
|
||||
end, { once = true })
|
||||
|
||||
-- why is this necessary?
|
||||
vim.api.nvim_set_current_win(win.winid)
|
||||
else
|
||||
log.error(msg)
|
||||
win:unmount()
|
||||
end
|
||||
end
|
||||
|
||||
return M
|
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,424 @@
|
||||
local vim = vim
|
||||
local utils = require("neo-tree.utils")
|
||||
local log = require("neo-tree.log")
|
||||
local manager = require("neo-tree.sources.manager")
|
||||
|
||||
local M = {}
|
||||
|
||||
---calc_click_id_from_source:
|
||||
-- Calculates click_id that stores information of the source and window id
|
||||
-- DANGER: Do not change this function unless you know what you are doing
|
||||
---@param winid integer: window id of the window source_selector is placed
|
||||
---@param source_index integer: index of the source
|
||||
---@return integer
|
||||
local calc_click_id_from_source = function(winid, source_index)
|
||||
local base_number = #require("neo-tree").config.source_selector.sources + 1
|
||||
return base_number * winid + source_index
|
||||
end
|
||||
|
||||
---calc_source_from_click_id:
|
||||
-- Calculates source index and window id from click_id. Paired with `M.calc_click_id_from_source`
|
||||
-- DANGER: Do not change this function unless you know what you are doing
|
||||
---@param click_id integer: click_id
|
||||
---@return integer, integer
|
||||
local calc_source_from_click_id = function(click_id)
|
||||
local base_number = #require("neo-tree").config.source_selector.sources + 1
|
||||
return math.floor(click_id / base_number), click_id % base_number
|
||||
end
|
||||
---sep_tbl:
|
||||
-- Returns table expression of separator.
|
||||
-- Converts to table expression if sep is string.
|
||||
---@param sep string | table:
|
||||
---@return table: `{ left = .., right = .., override = .. }`
|
||||
local sep_tbl = function(sep)
|
||||
if type(sep) == "nil" then
|
||||
return {}
|
||||
elseif type(sep) ~= "table" then
|
||||
return { left = sep, right = sep, override = "active" }
|
||||
end
|
||||
return sep
|
||||
end
|
||||
|
||||
-- Function below provided by @akinsho
|
||||
-- https://github.com/nvim-neo-tree/neo-tree.nvim/pull/427#discussion_r924947766
|
||||
|
||||
-- truncate a string based on number of display columns/cells it occupies
|
||||
-- so that multibyte characters are not broken up mid-character
|
||||
---@param str string
|
||||
---@param col_limit number
|
||||
---@return string
|
||||
local function truncate_by_cell(str, col_limit)
|
||||
local api = vim.api
|
||||
local fn = vim.fn
|
||||
if str and str:len() == api.nvim_strwidth(str) then
|
||||
return fn.strcharpart(str, 0, col_limit)
|
||||
end
|
||||
local short = fn.strcharpart(str, 0, col_limit)
|
||||
if api.nvim_strwidth(short) > col_limit then
|
||||
while api.nvim_strwidth(short) > col_limit do
|
||||
short = fn.strcharpart(short, 0, fn.strchars(short) - 1)
|
||||
end
|
||||
end
|
||||
return short
|
||||
end
|
||||
|
||||
---get_separators
|
||||
-- Returns information about separator on each tab.
|
||||
---@param source_index integer: index of source
|
||||
---@param active_index integer: index of active source. used to check if source is active and when `override = "active"`
|
||||
---@param force_ignore_left boolean: overwrites calculated results with "" if set to true
|
||||
---@param force_ignore_right boolean: overwrites calculated results with "" if set to true
|
||||
---@return table: something like `{ left = "|", right = "|" }`
|
||||
local get_separators = function(source_index, active_index, force_ignore_left, force_ignore_right)
|
||||
local config = require("neo-tree").config
|
||||
local is_active = source_index == active_index
|
||||
local sep = sep_tbl(config.source_selector.separator)
|
||||
if is_active then
|
||||
sep = vim.tbl_deep_extend("force", sep, sep_tbl(config.source_selector.separator_active))
|
||||
end
|
||||
local show_left = sep.override == "left"
|
||||
or (sep.override == "active" and source_index <= active_index)
|
||||
or sep.override == nil
|
||||
local show_right = sep.override == "right"
|
||||
or (sep.override == "active" and source_index >= active_index)
|
||||
or sep.override == nil
|
||||
return {
|
||||
left = (show_left and not force_ignore_left) and sep.left or "",
|
||||
right = (show_right and not force_ignore_right) and sep.right or "",
|
||||
}
|
||||
end
|
||||
|
||||
---get_selector_tab_info:
|
||||
-- Returns information to create a tab
|
||||
---@param source_name string: name of source. should be same as names in `config.sources`
|
||||
---@param source_index integer: index of source_name
|
||||
---@param is_active boolean: whether this source is currently focused
|
||||
---@param separator table: `{ left = .., right = .. }`: output from `get_separators()`
|
||||
---@return table (see code): Note: `length`: length of whole tab (including seps), `text_length`: length of tab excluding seps
|
||||
local get_selector_tab_info = function(source_name, source_index, is_active, separator)
|
||||
local config = require("neo-tree").config
|
||||
local separator_config = utils.resolve_config_option(config, "source_selector", nil)
|
||||
if separator_config == nil then
|
||||
log.warn("Cannot find source_selector config. `get_selector` abort.")
|
||||
return {}
|
||||
end
|
||||
local source_config = config[source_name] or {}
|
||||
local get_strlen = vim.api.nvim_strwidth
|
||||
local text = separator_config.sources[source_index].display_name or source_config.display_name or source_name
|
||||
local text_length = get_strlen(text)
|
||||
if separator_config.tabs_min_width ~= nil and text_length < separator_config.tabs_min_width then
|
||||
text = M.text_layout(text, separator_config.content_layout, separator_config.tabs_min_width)
|
||||
text_length = separator_config.tabs_min_width
|
||||
end
|
||||
if separator_config.tabs_max_width ~= nil and text_length > separator_config.tabs_max_width then
|
||||
text = M.text_layout(text, separator_config.content_layout, separator_config.tabs_max_width)
|
||||
text_length = separator_config.tabs_max_width
|
||||
end
|
||||
local tab_hl = is_active and separator_config.highlight_tab_active
|
||||
or separator_config.highlight_tab
|
||||
local sep_hl = is_active and separator_config.highlight_separator_active
|
||||
or separator_config.highlight_separator
|
||||
return {
|
||||
index = source_index,
|
||||
is_active = is_active,
|
||||
left = separator.left,
|
||||
right = separator.right,
|
||||
text = text,
|
||||
tab_hl = tab_hl,
|
||||
sep_hl = sep_hl,
|
||||
length = text_length + get_strlen(separator.left) + get_strlen(separator.right),
|
||||
text_length = text_length,
|
||||
}
|
||||
end
|
||||
|
||||
---text_with_hl:
|
||||
-- Returns text with highlight syntax for winbar / statusline
|
||||
---@param text string: text to highlight
|
||||
---@param tab_hl string | nil: if nil, does nothing
|
||||
---@return string: e.g. "%#HiName#text"
|
||||
local text_with_hl = function(text, tab_hl)
|
||||
if tab_hl == nil then
|
||||
return text
|
||||
end
|
||||
return string.format("%%#%s#%s", tab_hl, text)
|
||||
end
|
||||
|
||||
---add_padding:
|
||||
-- Use for creating padding with highlight
|
||||
---@param padding_legth number: number of padding. if float, value is rounded with `math.floor`
|
||||
---@param padchar string | nil: if nil, " " (space) is used
|
||||
---@return string
|
||||
local add_padding = function(padding_legth, padchar)
|
||||
if padchar == nil then
|
||||
padchar = " "
|
||||
end
|
||||
return string.rep(padchar, math.floor(padding_legth))
|
||||
end
|
||||
|
||||
---text_layout:
|
||||
-- Add padding to fill `output_width`.
|
||||
-- If `output_width` is less than `text_length`, text is truncated to fit `output_width`.
|
||||
---@param text string:
|
||||
---@param content_layout string: `"start", "center", "end"`: see `config.source_selector.tabs_layout` for more details
|
||||
---@param output_width integer: exact `strdisplaywidth` of the output string
|
||||
---@param trunc_char string | nil: Character used to indicate truncation. If nil, "…" (ellipsis) is used.
|
||||
---@return string
|
||||
local text_layout = function(text, content_layout, output_width, trunc_char)
|
||||
if output_width < 1 then
|
||||
return ""
|
||||
end
|
||||
local text_length = vim.fn.strdisplaywidth(text)
|
||||
local pad_length = output_width - text_length
|
||||
local left_pad, right_pad = 0, 0
|
||||
if pad_length < 0 then
|
||||
if output_width < 4 then
|
||||
return truncate_by_cell(text, output_width)
|
||||
else
|
||||
return truncate_by_cell(text, output_width - 1) .. trunc_char
|
||||
end
|
||||
elseif content_layout == "start" then
|
||||
left_pad, right_pad = 0, pad_length
|
||||
elseif content_layout == "end" then
|
||||
left_pad, right_pad = pad_length, 0
|
||||
elseif content_layout == "center" then
|
||||
left_pad, right_pad = pad_length / 2, math.ceil(pad_length / 2)
|
||||
end
|
||||
return add_padding(left_pad) .. text .. add_padding(right_pad)
|
||||
end
|
||||
|
||||
---render_tab:
|
||||
-- Renders string to express one tab for winbar / statusline.
|
||||
---@param left_sep string: left separator
|
||||
---@param right_sep string: right separator
|
||||
---@param sep_hl string: highlight of separators
|
||||
---@param text string: text, mostly name of source in this case
|
||||
---@param tab_hl string: highlight of text
|
||||
---@param click_id integer: id passed to `___neotree_selector_click`, should be calculated with `M.calc_click_id_from_source`
|
||||
---@return string: complete string to render one tab
|
||||
local render_tab = function(left_sep, right_sep, sep_hl, text, tab_hl, click_id)
|
||||
local res = "%" .. click_id .. "@v:lua.___neotree_selector_click@"
|
||||
if left_sep ~= nil then
|
||||
res = res .. text_with_hl(left_sep, sep_hl)
|
||||
end
|
||||
res = res .. text_with_hl(text, tab_hl)
|
||||
if right_sep ~= nil then
|
||||
res = res .. text_with_hl(right_sep, sep_hl)
|
||||
end
|
||||
return res
|
||||
end
|
||||
|
||||
M.get_scrolled_off_node_text = function(state)
|
||||
if state == nil then
|
||||
state = require("neo-tree.sources.manager").get_state_for_window()
|
||||
if state == nil then
|
||||
return
|
||||
end
|
||||
end
|
||||
local win_top_line = vim.fn.line("w0")
|
||||
if win_top_line == nil or win_top_line == 1 then
|
||||
return
|
||||
end
|
||||
local node = state.tree:get_node(win_top_line)
|
||||
return " " .. vim.fn.fnamemodify(node.path, ":~:h")
|
||||
end
|
||||
|
||||
M.get = function()
|
||||
local state = require("neo-tree.sources.manager").get_state_for_window()
|
||||
if state == nil then
|
||||
return
|
||||
else
|
||||
local config = require("neo-tree").config
|
||||
local scrolled_off =
|
||||
utils.resolve_config_option(config, "source_selector.show_scrolled_off_parent_node", false)
|
||||
if scrolled_off then
|
||||
local node_text = M.get_scrolled_off_node_text(state)
|
||||
if node_text ~= nil then
|
||||
return node_text
|
||||
end
|
||||
end
|
||||
return M.get_selector(state, vim.api.nvim_win_get_width(0))
|
||||
end
|
||||
end
|
||||
|
||||
---get_selector:
|
||||
-- Does everything to generate the string for source_selector in winbar / statusline.
|
||||
---@param state table:
|
||||
---@param width integer: width of the entire window where the source_selector is displayed
|
||||
---@return string | nil
|
||||
M.get_selector = function(state, width)
|
||||
local config = require("neo-tree").config
|
||||
if config == nil then
|
||||
log.warn("Cannot find config. `get_selector` abort.")
|
||||
return nil
|
||||
end
|
||||
local winid = state.winid or vim.api.nvim_get_current_win()
|
||||
|
||||
-- load padding from config
|
||||
local padding = config.source_selector.padding
|
||||
if type(padding) == "number" then
|
||||
padding = { left = padding, right = padding }
|
||||
end
|
||||
width = math.floor(width - padding.left - padding.right)
|
||||
|
||||
-- generate information of each tab (look `get_selector_tab_info` for type hint)
|
||||
local tabs = {}
|
||||
local sources = config.source_selector.sources
|
||||
local active_index = #sources
|
||||
local length_sum, length_active, length_separators = 0, 0, 0
|
||||
for i, source_info in ipairs(sources) do
|
||||
local is_active = source_info.source == state.name
|
||||
if is_active then
|
||||
active_index = i
|
||||
end
|
||||
local separator = get_separators(
|
||||
i,
|
||||
active_index,
|
||||
config.source_selector.show_separator_on_edge == false and i == 1,
|
||||
config.source_selector.show_separator_on_edge == false and i == #sources
|
||||
)
|
||||
local element = get_selector_tab_info(source_info.source, i, is_active, separator)
|
||||
length_sum = length_sum + element.length
|
||||
length_separators = length_separators + element.length - element.text_length
|
||||
if is_active then
|
||||
length_active = element.length
|
||||
end
|
||||
table.insert(tabs, element)
|
||||
end
|
||||
|
||||
-- start creating string to display
|
||||
local tabs_layout = config.source_selector.tabs_layout
|
||||
local content_layout = config.source_selector.content_layout or "center"
|
||||
local hl_background = config.source_selector.highlight_background
|
||||
local trunc_char = config.source_selector.truncation_character or "…"
|
||||
local remaining_width = width - length_separators
|
||||
local return_string = text_with_hl(add_padding(padding.left), hl_background)
|
||||
if width < length_sum then -- not enough width
|
||||
tabs_layout = "equal" -- other methods cannot handle this
|
||||
end
|
||||
if tabs_layout == "active" then
|
||||
local active_tab_length = width - length_sum + length_active - 1
|
||||
for _, tab in ipairs(tabs) do
|
||||
return_string = return_string
|
||||
.. render_tab(
|
||||
tab.left,
|
||||
tab.right,
|
||||
tab.sep_hl,
|
||||
text_layout(
|
||||
tab.text,
|
||||
tab.is_active and content_layout or nil,
|
||||
active_tab_length,
|
||||
trunc_char
|
||||
),
|
||||
tab.tab_hl,
|
||||
calc_click_id_from_source(winid, tab.index)
|
||||
)
|
||||
.. text_with_hl("", hl_background)
|
||||
end
|
||||
elseif tabs_layout == "equal" then
|
||||
for _, tab in ipairs(tabs) do
|
||||
return_string = return_string
|
||||
.. render_tab(
|
||||
tab.left,
|
||||
tab.right,
|
||||
tab.sep_hl,
|
||||
text_layout(tab.text, content_layout, math.floor(remaining_width / #tabs), trunc_char),
|
||||
tab.tab_hl,
|
||||
calc_click_id_from_source(winid, tab.index)
|
||||
)
|
||||
.. text_with_hl("", hl_background)
|
||||
end
|
||||
else -- config.source_selector.tab_labels == "start", "end", "center"
|
||||
-- calculate padding based on tabs_layout
|
||||
local pad_length = width - length_sum
|
||||
local left_pad, right_pad = 0, 0
|
||||
if pad_length > 0 then
|
||||
if tabs_layout == "start" then
|
||||
left_pad, right_pad = 0, pad_length
|
||||
elseif tabs_layout == "end" then
|
||||
left_pad, right_pad = pad_length, 0
|
||||
elseif tabs_layout == "center" then
|
||||
left_pad, right_pad = pad_length / 2, math.ceil(pad_length / 2)
|
||||
end
|
||||
end
|
||||
|
||||
for i, tab in ipairs(tabs) do
|
||||
if width == 0 then
|
||||
break
|
||||
end
|
||||
|
||||
-- only render trunc_char if there is no space for the tab
|
||||
local sep_length = tab.length - tab.text_length
|
||||
if width <= sep_length + 1 then
|
||||
return_string = return_string
|
||||
.. text_with_hl(trunc_char .. add_padding(width - 1), hl_background)
|
||||
width = 0
|
||||
break
|
||||
end
|
||||
|
||||
-- tab_length should not exceed width
|
||||
local tab_length = width < tab.length and width or tab.length
|
||||
width = width - tab_length
|
||||
|
||||
-- add padding for first and last tab
|
||||
local tab_text = tab.text
|
||||
if i == 1 then
|
||||
tab_text = add_padding(left_pad) .. tab_text
|
||||
tab_length = tab_length + left_pad
|
||||
end
|
||||
if i == #tabs then
|
||||
tab_text = tab_text .. add_padding(right_pad)
|
||||
tab_length = tab_length + right_pad
|
||||
end
|
||||
|
||||
return_string = return_string
|
||||
.. render_tab(
|
||||
tab.left,
|
||||
tab.right,
|
||||
tab.sep_hl,
|
||||
text_layout(tab_text, tabs_layout, tab_length - sep_length, trunc_char),
|
||||
tab.tab_hl,
|
||||
calc_click_id_from_source(winid, tab.index)
|
||||
)
|
||||
end
|
||||
end
|
||||
return return_string .. "%<%0@v:lua.___neotree_selector_click@"
|
||||
end
|
||||
|
||||
---set_source_selector:
|
||||
-- (public): Directly set source_selector to current window's winbar / statusline
|
||||
---@param state table: state
|
||||
---@return nil
|
||||
M.set_source_selector = function(state)
|
||||
if state.enable_source_selector == false then
|
||||
return
|
||||
end
|
||||
local sel_config = utils.resolve_config_option(require("neo-tree").config, "source_selector", {})
|
||||
if sel_config and sel_config.winbar then
|
||||
vim.wo[state.winid].winbar = "%{%v:lua.require'neo-tree.ui.selector'.get()%}"
|
||||
end
|
||||
if sel_config and sel_config.statusline then
|
||||
vim.wo[state.winid].statusline = "%{%v:lua.require'neo-tree.ui.selector'.get()%}"
|
||||
end
|
||||
end
|
||||
|
||||
-- @v:lua@ in the tabline only supports global functions, so this is
|
||||
-- the only way to add click handlers without autoloaded vimscript functions
|
||||
_G.___neotree_selector_click = function(id, _, _, _)
|
||||
if id < 1 then
|
||||
return
|
||||
end
|
||||
local sources = require("neo-tree").config.source_selector.sources
|
||||
local winid, source_index = calc_source_from_click_id(id)
|
||||
local state = manager.get_state_for_window(winid)
|
||||
if state == nil then
|
||||
log.warn("state not found for window ", winid, "; ignoring click")
|
||||
return
|
||||
end
|
||||
require("neo-tree.command").execute({
|
||||
source = sources[source_index].source,
|
||||
position = state.current_position,
|
||||
action = "focus",
|
||||
})
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,27 @@
|
||||
local locations = {}
|
||||
|
||||
local get_location = function(location)
|
||||
local tab = vim.api.nvim_get_current_tabpage()
|
||||
if not locations[tab] then
|
||||
locations[tab] = {}
|
||||
end
|
||||
local loc = locations[tab][location]
|
||||
if loc then
|
||||
if loc.winid ~= 0 then
|
||||
-- verify the window before we return it
|
||||
if not vim.api.nvim_win_is_valid(loc.winid) then
|
||||
loc.winid = 0
|
||||
end
|
||||
end
|
||||
return loc
|
||||
end
|
||||
loc = {
|
||||
source = nil,
|
||||
name = location,
|
||||
winid = 0,
|
||||
}
|
||||
locations[tab][location] = loc
|
||||
return loc
|
||||
end
|
||||
|
||||
return { get_location = get_location }
|
||||
@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2016 Boris Nagaev
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@ -0,0 +1,137 @@
|
||||
-- lua-filesize, generate a human readable string describing the file size
|
||||
-- Copyright (c) 2016 Boris Nagaev
|
||||
-- See the LICENSE file for terms of use.
|
||||
|
||||
local si = {
|
||||
bits = {"b", "Kb", "Mb", "Gb", "Tb", "Pb", "Eb", "Zb", "Yb"},
|
||||
bytes = {"B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"},
|
||||
}
|
||||
|
||||
local function isNan(num)
|
||||
-- http://lua-users.org/wiki/InfAndNanComparisons
|
||||
-- NaN is the only value that doesn't equal itself
|
||||
return num ~= num
|
||||
end
|
||||
|
||||
local function roundNumber(num, digits)
|
||||
local fmt = "%." .. digits .. "f"
|
||||
return tonumber(fmt:format(num))
|
||||
end
|
||||
|
||||
local function filesize(size, options)
|
||||
|
||||
-- copy options to o
|
||||
local o = {}
|
||||
for key, value in pairs(options or {}) do
|
||||
o[key] = value
|
||||
end
|
||||
|
||||
local function setDefault(name, default)
|
||||
if o[name] == nil then
|
||||
o[name] = default
|
||||
end
|
||||
end
|
||||
setDefault("bits", false)
|
||||
setDefault("unix", false)
|
||||
setDefault("base", 2)
|
||||
setDefault("round", o.unix and 1 or 2)
|
||||
setDefault("spacer", o.unix and "" or " ")
|
||||
setDefault("suffixes", {})
|
||||
setDefault("output", "string")
|
||||
setDefault("exponent", -1)
|
||||
|
||||
assert(not isNan(size), "Invalid arguments")
|
||||
|
||||
local ceil = (o.base > 2) and 1000 or 1024
|
||||
local negative = (size < 0)
|
||||
if negative then
|
||||
-- Flipping a negative number to determine the size
|
||||
size = -size
|
||||
end
|
||||
|
||||
local result
|
||||
|
||||
-- Zero is now a special case because bytes divide by 1
|
||||
if size == 0 then
|
||||
result = {
|
||||
0,
|
||||
o.unix and "" or (o.bits and "b" or "B"),
|
||||
}
|
||||
else
|
||||
-- Determining the exponent
|
||||
if o.exponent == -1 or isNan(o.exponent) then
|
||||
o.exponent = math.floor(math.log(size) / math.log(ceil))
|
||||
end
|
||||
|
||||
-- Exceeding supported length, time to reduce & multiply
|
||||
if o.exponent > 8 then
|
||||
o.exponent = 8
|
||||
end
|
||||
|
||||
local val
|
||||
if o.base == 2 then
|
||||
val = size / math.pow(2, o.exponent * 10)
|
||||
else
|
||||
val = size / math.pow(1000, o.exponent)
|
||||
end
|
||||
|
||||
if o.bits then
|
||||
val = val * 8
|
||||
if val > ceil then
|
||||
val = val / ceil
|
||||
o.exponent = o.exponent + 1
|
||||
end
|
||||
end
|
||||
|
||||
result = {
|
||||
roundNumber(val, o.exponent > 0 and o.round or 0),
|
||||
(o.base == 10 and o.exponent == 1) and
|
||||
(o.bits and "kb" or "kB") or
|
||||
(si[o.bits and "bits" or "bytes"][o.exponent + 1]),
|
||||
}
|
||||
|
||||
if o.unix then
|
||||
result[2] = result[2]:sub(1, 1)
|
||||
|
||||
if result[2] == "b" or result[2] == "B" then
|
||||
result ={
|
||||
math.floor(result[1]),
|
||||
"",
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
assert(result)
|
||||
|
||||
-- Decorating a 'diff'
|
||||
if negative then
|
||||
result[1] = -result[1]
|
||||
end
|
||||
|
||||
-- Applying custom suffix
|
||||
result[2] = o.suffixes[result[2]] or result[2]
|
||||
|
||||
-- Applying custom suffix
|
||||
result[2] = o.suffixes[result[2]] or result[2]
|
||||
|
||||
-- Returning Array, Object, or String (default)
|
||||
if o.output == "array" then
|
||||
return result
|
||||
elseif o.output == "exponent" then
|
||||
return o.exponent
|
||||
elseif o.output == "object" then
|
||||
return {
|
||||
value = result[1],
|
||||
suffix = result[2],
|
||||
}
|
||||
elseif o.output == "string" then
|
||||
local value = tostring(result[1])
|
||||
value = value:gsub('%.0$', '')
|
||||
local suffix = result[2]
|
||||
return value .. o.spacer .. suffix
|
||||
end
|
||||
end
|
||||
|
||||
return filesize
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user