Meh I'll figure out submodules later
This commit is contained in:
parent
4ca9d44a90
commit
8cb281f436
352 changed files with 66107 additions and 0 deletions
|
|
@ -0,0 +1,146 @@
|
|||
local log = require("neo-tree.log")
|
||||
|
||||
---@class neotree.collections.ListNode
|
||||
---@field prev neotree.collections.ListNode?
|
||||
---@field next neotree.collections.ListNode?
|
||||
---@field value any
|
||||
|
||||
local Node = {}
|
||||
function Node:new(value)
|
||||
local props = { prev = nil, next = nil, value = value }
|
||||
setmetatable(props, self)
|
||||
self.__index = self
|
||||
return props
|
||||
end
|
||||
|
||||
---@class neotree.collections.LinkedList
|
||||
---@field head neotree.collections.ListNode?
|
||||
---@field tail neotree.collections.ListNode?
|
||||
---@field size integer
|
||||
local LinkedList = {}
|
||||
|
||||
---@return neotree.collections.LinkedList
|
||||
function LinkedList:new()
|
||||
local props = { head = nil, tail = nil, size = 0 }
|
||||
setmetatable(props, self)
|
||||
self.__index = self
|
||||
return props
|
||||
end
|
||||
|
||||
---@param node neotree.collections.ListNode
|
||||
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
|
||||
|
||||
---@param node neotree.collections.ListNode
|
||||
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
|
||||
---@class neotree.collections.Queue
|
||||
---@field _list neotree.collections.LinkedList
|
||||
local Queue = {}
|
||||
|
||||
---@return neotree.collections.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.
|
||||
---@return table? result
|
||||
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 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
|
||||
---@diagnostic disable-next-line: need-check-nil
|
||||
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,130 @@
|
|||
local parser = require("neo-tree.command.parser")
|
||||
local utils = require("neo-tree.utils")
|
||||
|
||||
local M = {
|
||||
show_key_value_completions = true,
|
||||
}
|
||||
|
||||
---@param key_prefix string?
|
||||
---@param base_path string
|
||||
---@return string paths_string
|
||||
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
|
||||
|
||||
---@param key_prefix string?
|
||||
---@return string references_string
|
||||
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
|
||||
|
||||
---@param argLead string
|
||||
---@param cmdLine string
|
||||
---@return string candidates_string
|
||||
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.argtype_lookup[key]
|
||||
if arg_type == parser.argtypes.PATH then
|
||||
return get_path_completions(key .. "=", value)
|
||||
elseif arg_type == parser.argtypes.REF then
|
||||
return get_ref_completions(key .. "=")
|
||||
elseif arg_type == parser.argtypes.LIST then
|
||||
local valid_values = parser.arguments[key].values
|
||||
if valid_values and not (parsed[key] and #parsed[key] > 0) then
|
||||
for _, vv in ipairs(valid_values) do
|
||||
if vv:find(value, 1, true) 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.argtype_lookup[key] == parser.argtypes.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,245 @@
|
|||
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
|
||||
---@type neotree.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
|
||||
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
|
||||
handle_reveal(args, state)
|
||||
return
|
||||
end
|
||||
if not args.dir then
|
||||
args.dir = state.path
|
||||
end
|
||||
do_show_or_focus(args, state, force_navigate)
|
||||
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)
|
||||
args.reveal_file = utils.normalize_path(args.reveal_file)
|
||||
-- Deal with cwd if we need to
|
||||
local cwd = args.dir or state.path or manager.get_cwd(state)
|
||||
if utils.is_subpath(cwd, args.reveal_file) then
|
||||
args.dir = cwd
|
||||
do_show_or_focus(args, state, true)
|
||||
return
|
||||
end
|
||||
|
||||
local reveal_file_parent, _ = utils.split_path(args.reveal_file) --[[@as string]]
|
||||
if args.reveal_force_cwd then
|
||||
args.dir = reveal_file_parent
|
||||
do_show_or_focus(args, state, true)
|
||||
return
|
||||
end
|
||||
|
||||
-- if dir doesn't have the reveal_file, ignore the reveal_file
|
||||
if args.dir then
|
||||
args.reveal_file = nil
|
||||
do_show_or_focus(args, state, true)
|
||||
return
|
||||
end
|
||||
|
||||
-- force was not specified and the file does not belong to cwd, so we need to ask the user
|
||||
inputs.confirm("File not in cwd. Change cwd to " .. reveal_file_parent .. "?", function(response)
|
||||
if response == true then
|
||||
args.dir = reveal_file_parent
|
||||
else
|
||||
args.reveal_file = nil
|
||||
end
|
||||
do_show_or_focus(args, state, true)
|
||||
end)
|
||||
end
|
||||
|
||||
return M
|
||||
|
|
@ -0,0 +1,208 @@
|
|||
local uv = vim.uv or vim.loop
|
||||
local utils = require("neo-tree.utils")
|
||||
local _compat = require("neo-tree.utils._compat")
|
||||
|
||||
---@enum neotree.command.ParserArgument.Type
|
||||
local argtype = {
|
||||
FLAG = "<FLAG>",
|
||||
LIST = "<LIST>",
|
||||
PATH = "<PATH>",
|
||||
REF = "<REF>",
|
||||
}
|
||||
|
||||
---@class neotree.command.Parser
|
||||
---@field argtypes table<string, neotree.command.ParserArgument.Type>
|
||||
local M = {
|
||||
argtypes = argtype,
|
||||
}
|
||||
|
||||
---@param all_source_names string[]
|
||||
M.setup = function(all_source_names)
|
||||
local source_names = vim.deepcopy(all_source_names, _compat.noref())
|
||||
table.insert(source_names, "migrations")
|
||||
|
||||
-- A special source referring to the last used source.
|
||||
table.insert(source_names, "last")
|
||||
|
||||
---@class neotree.command.ParserArgument
|
||||
---@field type neotree.command.ParserArgument.Type
|
||||
|
||||
-- For lists, the first value is the default value.
|
||||
---@class neotree.command.ParserArguments
|
||||
---@field [string] neotree.command.ParserArgument
|
||||
---@field values string[]
|
||||
local arguments = {
|
||||
action = {
|
||||
type = M.argtypes.LIST,
|
||||
values = {
|
||||
"close",
|
||||
"focus",
|
||||
"show",
|
||||
},
|
||||
},
|
||||
position = {
|
||||
type = M.argtypes.LIST,
|
||||
values = {
|
||||
"left",
|
||||
"right",
|
||||
"top",
|
||||
"bottom",
|
||||
"float",
|
||||
"current",
|
||||
},
|
||||
},
|
||||
source = {
|
||||
type = M.argtypes.LIST,
|
||||
values = source_names,
|
||||
},
|
||||
dir = { type = M.argtypes.PATH, stat_type = "directory" },
|
||||
reveal_file = { type = M.argtypes.PATH, stat_type = "file" },
|
||||
git_base = { type = M.argtypes.REF },
|
||||
toggle = { type = M.argtypes.FLAG },
|
||||
reveal = { type = M.argtypes.FLAG },
|
||||
reveal_force_cwd = { type = M.argtypes.FLAG },
|
||||
selector = { type = M.argtypes.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.argtypes.LIST then
|
||||
table.insert(list_args, name)
|
||||
for _, vv in ipairs(def.values) do
|
||||
reverse_lookup[tostring(vv)] = name
|
||||
end
|
||||
elseif def.type == M.argtypes.PATH then
|
||||
table.insert(path_args, name)
|
||||
elseif def.type == M.argtypes.FLAG then
|
||||
table.insert(flag_args, name)
|
||||
reverse_lookup[name] = M.argtypes.FLAG
|
||||
elseif def.type == M.argtypes.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.argtype_lookup = arg_type_lookup
|
||||
M.reverse_lookup = reverse_lookup
|
||||
end
|
||||
|
||||
---@param path string
|
||||
---@param validate_type string?
|
||||
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 = uv.fs_stat(abs_path)
|
||||
if not stat or stat.type ~= validate_type then
|
||||
error("Invalid path: " .. path .. " is not a " .. validate_type)
|
||||
end
|
||||
end
|
||||
return abs_path
|
||||
end
|
||||
|
||||
---@param ref string
|
||||
M.verify_git_ref = function(ref)
|
||||
local ok, _ = utils.execute_command("git rev-parse --verify " .. ref)
|
||||
return ok
|
||||
end
|
||||
|
||||
---@class neotree.command.Parser.Parsed
|
||||
---@field [string] string|boolean
|
||||
|
||||
---@param result neotree.command.Parser.Parsed
|
||||
---@param arg string
|
||||
local parse_arg = function(result, arg)
|
||||
if type(arg) ~= "string" then
|
||||
return
|
||||
end
|
||||
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.argtypes.PATH then
|
||||
result[key] = M.resolve_path(value, def.stat_type)
|
||||
elseif def.type == M.argtypes.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.argtypes.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 = uv.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.argtypes.FLAG then
|
||||
result[value] = true
|
||||
else
|
||||
result[key] = value
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---@param args string|string[]
|
||||
---@param strict_checking boolean
|
||||
---@return neotree.command.Parser.Parsed parsed_args
|
||||
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,745 @@
|
|||
---@type neotree.Config.Base
|
||||
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 ':lua require("neo-tree").show_logs()' 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
|
||||
open_files_using_relative_paths = false,
|
||||
-- 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", "rounded", "single", "solid", (or "" to use 'winborder' on Neovim v0.11+)
|
||||
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",
|
||||
provider = function(icon, node, state) -- default icon provider utilizes nvim-web-devicons if available
|
||||
if 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.text = devicon or icon.text
|
||||
icon.highlight = hl or icon.highlight
|
||||
end
|
||||
end
|
||||
end
|
||||
},
|
||||
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,
|
||||
width = 12, -- width of the column
|
||||
required_width = 64, -- min width of window required to show this column
|
||||
},
|
||||
type = {
|
||||
enabled = true,
|
||||
width = 10, -- width of the column
|
||||
required_width = 110, -- min width of window required to show this column
|
||||
},
|
||||
last_modified = {
|
||||
enabled = true,
|
||||
width = 20, -- width of the column
|
||||
required_width = 88, -- min width of window required to show this column
|
||||
format = "%Y-%m-%d %I:%M %p", -- format string for timestamp (see `:h os.date()`)
|
||||
-- or use a function that takes in the date in seconds and returns a string to display
|
||||
--format = require("neo-tree.utils").relative_date, -- enable relative timestamps
|
||||
},
|
||||
created = {
|
||||
enabled = false,
|
||||
width = 20, -- width of the column
|
||||
required_width = 120, -- min width of window required to show this column
|
||||
format = "%Y-%m-%d %I:%M %p", -- format string for timestamp (see `:h os.date()`)
|
||||
-- or use a function that takes in the date in seconds and returns a string to display
|
||||
--format = require("neo-tree.utils").relative_date, -- enable relative timestamps
|
||||
},
|
||||
symlink_target = {
|
||||
enabled = false,
|
||||
text_format = " ➛ %s", -- %s will be replaced with the symlink target's path.
|
||||
},
|
||||
},
|
||||
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
|
||||
title = function (state) -- format the text that appears at the top of a popup window
|
||||
return "Neo-tree " .. state.name:gsub("^%l", string.upper)
|
||||
end,
|
||||
-- you can also specify border here, if you want a different setting from
|
||||
-- the global popup_border_style.
|
||||
},
|
||||
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_snacks_image = true,
|
||||
use_image_nvim = true,
|
||||
-- title = "Neo-tree Preview", -- You can define a custom title for the preview floating window.
|
||||
}
|
||||
},
|
||||
["<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",
|
||||
--["C"] = "close_all_subnodes",
|
||||
["z"] = "close_all_nodes",
|
||||
--["Z"] = "expand_all_nodes",
|
||||
--["Z"] = "expand_all_subnodes",
|
||||
["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",
|
||||
--["/"] = {"fuzzy_finder", config = { keep_filter_on_submit = true }},
|
||||
--["/"] = "filter_as_you_type", -- this was the default until v1.28
|
||||
["D"] = "fuzzy_finder_directory",
|
||||
-- ["D"] = "fuzzy_sorter_directory",
|
||||
["#"] = "fuzzy_sorter", -- fuzzy sorting using the fzy algorithm
|
||||
["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", -- see `:h neo-tree-file-actions` for options to customize the window.
|
||||
["b"] = "rename_basename",
|
||||
["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",
|
||||
["<Esc>"] = "close",
|
||||
["<S-CR>"] = "close_keep_filter",
|
||||
["<C-CR>"] = "close_clear_filter",
|
||||
["<C-w>"] = { "<C-S-w>", raw = true },
|
||||
{
|
||||
-- normal mode mappings
|
||||
n = {
|
||||
["j"] = "move_cursor_down",
|
||||
["k"] = "move_cursor_up",
|
||||
["<S-CR>"] = "close_keep_filter",
|
||||
["<C-CR>"] = "close_clear_filter",
|
||||
["<esc>"] = "close",
|
||||
}
|
||||
}
|
||||
-- ["<esc>"] = "noop", -- if you want to use normal mode
|
||||
-- ["key"] = function(state, scroll_padding) ... end,
|
||||
},
|
||||
},
|
||||
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
|
||||
children_inherit_highlights = true, -- whether children of filtered parents should inherit their parent's highlight group
|
||||
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",
|
||||
},
|
||||
always_show_by_pattern = { -- uses glob style patterns
|
||||
--".env*",
|
||||
},
|
||||
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",
|
||||
["d"] = "buffer_delete",
|
||||
["bd"] = "buffer_delete",
|
||||
["i"] = "show_file_details", -- see `:h neo-tree-file-actions` for options to customize the window.
|
||||
["b"] = "rename_basename",
|
||||
["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",
|
||||
["gU"] = "git_undo_last_commit",
|
||||
["ga"] = "git_add_file",
|
||||
["gr"] = "git_revert_file",
|
||||
["gc"] = "git_commit",
|
||||
["gp"] = "git_push",
|
||||
["gg"] = "git_commit_and_push",
|
||||
["i"] = "show_file_details", -- see `:h neo-tree-file-actions` for options to customize the window.
|
||||
["b"] = "rename_basename",
|
||||
["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,110 @@
|
|||
local q = require("neo-tree.events.queue")
|
||||
local log = require("neo-tree.log")
|
||||
local utils = require("neo-tree.utils")
|
||||
|
||||
---@class neotree.event.Functions
|
||||
local M = {
|
||||
-- Well known event names, you can make up your own
|
||||
AFTER_RENDER = "after_render",
|
||||
BEFORE_FILE_ADD = "before_file_add",
|
||||
BEFORE_FILE_DELETE = "before_file_delete",
|
||||
BEFORE_FILE_MOVE = "before_file_move",
|
||||
BEFORE_FILE_RENAME = "before_file_rename",
|
||||
BEFORE_RENDER = "before_render",
|
||||
FILE_ADDED = "file_added",
|
||||
FILE_DELETED = "file_deleted",
|
||||
FILE_MOVED = "file_moved",
|
||||
FILE_OPENED = "file_opened",
|
||||
FILE_OPEN_REQUESTED = "file_open_requested",
|
||||
FILE_RENAMED = "file_renamed",
|
||||
FS_EVENT = "fs_event",
|
||||
GIT_EVENT = "git_event",
|
||||
GIT_STATUS_CHANGED = "git_status_changed",
|
||||
STATE_CREATED = "state_created",
|
||||
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",
|
||||
NEO_TREE_PREVIEW_BEFORE_RENDER = "neo_tree_preview_before_render",
|
||||
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",
|
||||
}
|
||||
|
||||
---@param autocmds string
|
||||
---@return string event
|
||||
---@return string? pattern
|
||||
local parse_autocmd_string = function(autocmds)
|
||||
local parsed = vim.split(autocmds, " ")
|
||||
return parsed[1], parsed[2]
|
||||
end
|
||||
|
||||
---@param event_name neotree.EventName|string
|
||||
---@param autocmds string[]
|
||||
---@param debounce_frequency integer?
|
||||
---@param seed_fn function?
|
||||
---@param nested boolean?
|
||||
M.define_autocmd_event = function(event_name, autocmds, debounce_frequency, seed_fn, nested)
|
||||
log.debug("Defining autocmd event: %s", event_name)
|
||||
local augroup_name = "NeoTreeEvent_" .. event_name
|
||||
q.define_event(event_name, {
|
||||
setup = function()
|
||||
local augroup = vim.api.nvim_create_augroup(augroup_name, { clear = false })
|
||||
for _, autocmd in ipairs(autocmds) do
|
||||
local event, pattern = parse_autocmd_string(autocmd)
|
||||
log.trace("Registering autocmds on %s %s", event, pattern or "")
|
||||
vim.api.nvim_create_autocmd({ event }, {
|
||||
pattern = pattern or "*",
|
||||
group = augroup,
|
||||
nested = nested,
|
||||
callback = function(args)
|
||||
---@class neotree.event.Autocmd.CallbackArgs : neotree._vim.api.keyset.create_autocmd.callback_args
|
||||
---@field afile string
|
||||
local event_args = args --[[@as neotree._vim.api.keyset.create_autocmd.callback_args]]
|
||||
event_args.afile = args.file or ""
|
||||
M.fire_event(event_name, event_args)
|
||||
end,
|
||||
})
|
||||
end
|
||||
end,
|
||||
seed = seed_fn,
|
||||
teardown = function()
|
||||
log.trace("Teardown autocmds for ", event_name)
|
||||
vim.api.nvim_create_augroup(augroup_name, { clear = true })
|
||||
end,
|
||||
debounce_frequency = debounce_frequency,
|
||||
debounce_strategy = utils.debounce_strategy.CALL_LAST_ONLY,
|
||||
})
|
||||
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,167 @@
|
|||
local utils = require("neo-tree.utils")
|
||||
local log = require("neo-tree.log")
|
||||
local Queue = require("neo-tree.collections").Queue
|
||||
|
||||
---@type table<string, neotree.collections.Queue?>
|
||||
local event_queues = {}
|
||||
---@type table <string, neotree.event.Definition?>
|
||||
local event_definitions = {}
|
||||
local M = {}
|
||||
|
||||
---@class neotree.event.Handler.Result
|
||||
---@field handled boolean?
|
||||
|
||||
---@class neotree.event.Handler
|
||||
---@field event neotree.EventName|string
|
||||
---@field handler fun(table?):(neotree.event.Handler.Result?)
|
||||
---@field id string?
|
||||
|
||||
local typecheck = require("neo-tree.health.typecheck")
|
||||
local validate = typecheck.validate
|
||||
---@param event_handler neotree.event.Handler
|
||||
local validate_event_handler = function(event_handler)
|
||||
return validate("event_handler", event_handler, function(eh)
|
||||
validate("event", eh.event, "string")
|
||||
validate("handler", eh.handler, "function")
|
||||
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
|
||||
|
||||
---@class neotree.event.Definition
|
||||
---@field teardown function?
|
||||
---@field setup function?
|
||||
---@field setup_was_run boolean?
|
||||
|
||||
---@param event_name neotree.EventName|string
|
||||
---@param opts neotree.event.Definition
|
||||
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
|
||||
|
||||
---@param event_name neotree.EventName|string
|
||||
---@return boolean existed_and_destroyed
|
||||
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
|
||||
|
||||
---@param event neotree.EventName|string
|
||||
---@param args table
|
||||
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
|
||||
|
||||
---@param event neotree.EventName|string
|
||||
---@param args any?
|
||||
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
|
||||
|
||||
---@param event_handler neotree.event.Handler
|
||||
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
|
||||
|
||||
---@param event_handler neotree.event.Handler
|
||||
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,183 @@
|
|||
local Job = require("plenary.job")
|
||||
local uv = vim.uv or vim.loop
|
||||
|
||||
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
|
||||
|
||||
---@param ignored string[]
|
||||
---@param path string
|
||||
---@param _type neotree.Filetype
|
||||
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
|
||||
|
||||
---@param state neotree.State
|
||||
---@param items neotree.FileItem[]
|
||||
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 = uv.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" }
|
||||
---@diagnostic disable-next-line: missing-fields
|
||||
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,350 @@
|
|||
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
|
||||
|
||||
---@class (exact) neotree.git.Context
|
||||
---@field git_status neotree.git.Status
|
||||
---@field git_root string
|
||||
---@field exclude_directories boolean
|
||||
---@field lines_parsed integer
|
||||
|
||||
---@alias neotree.git.Status table<string, string>
|
||||
|
||||
---@param context neotree.git.Context
|
||||
local parse_git_status_line = function(context, line)
|
||||
context.lines_parsed = context.lines_parsed + 1
|
||||
if type(line) ~= "string" then
|
||||
return
|
||||
end
|
||||
if #line < 3 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 neotree.git.Status, string? git_status the neotree.Git.Status of the given root
|
||||
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
|
||||
---@type neotree.git.Context
|
||||
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()
|
||||
---@diagnostic disable-next-line: missing-fields
|
||||
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 = 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,
|
||||
})
|
||||
|
||||
---@diagnostic disable-next-line: missing-fields
|
||||
local unstaged_job = Job:new({
|
||||
command = "git",
|
||||
args = { "-C", git_root, "diff", "--name-status" },
|
||||
enable_recording = false,
|
||||
maximium_results = context.max_lines,
|
||||
on_stdout = 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,
|
||||
})
|
||||
|
||||
---@diagnostic disable-next-line: missing-fields
|
||||
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 = 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,
|
||||
})
|
||||
|
||||
---@diagnostic disable-next-line: missing-fields
|
||||
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,66 @@
|
|||
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
|
||||
---@diagnostic disable-next-line: missing-fields
|
||||
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_output = utils.execute_command({ "git", unpack(args) })
|
||||
if not ok then
|
||||
log.trace("GIT ROOT ERROR ", git_output)
|
||||
return nil
|
||||
end
|
||||
local git_root = git_output[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,326 @@
|
|||
local typecheck = require("neo-tree.health.typecheck")
|
||||
local M = {}
|
||||
local health = vim.health
|
||||
|
||||
local function check_dependencies()
|
||||
local devicons_ok = pcall(require, "nvim-web-devicons")
|
||||
if devicons_ok then
|
||||
health.ok("nvim-web-devicons is installed")
|
||||
else
|
||||
health.info("nvim-web-devicons not installed")
|
||||
end
|
||||
|
||||
local plenary_ok = pcall(require, "plenary")
|
||||
if plenary_ok then
|
||||
health.ok("plenary.nvim is installed")
|
||||
else
|
||||
health.error("plenary.nvim is not installed")
|
||||
end
|
||||
|
||||
local nui_ok = pcall(require, "nui.tree")
|
||||
if nui_ok then
|
||||
health.ok("nui.nvim is installed")
|
||||
else
|
||||
health.error("nui.nvim not installed")
|
||||
end
|
||||
|
||||
health.info("Optional dependencies for preview image support (only need one):")
|
||||
-- optional
|
||||
local snacks_ok = pcall(require, "snacks.image")
|
||||
if snacks_ok then
|
||||
health.ok("snacks.image is installed")
|
||||
else
|
||||
health.info("nui.nvim not installed")
|
||||
end
|
||||
|
||||
local image_ok = pcall(require, "image")
|
||||
if image_ok then
|
||||
health.ok("image.nvim is installed")
|
||||
else
|
||||
health.info("nui.nvim not installed")
|
||||
end
|
||||
end
|
||||
|
||||
local validate = typecheck.validate
|
||||
|
||||
---@module "neo-tree.types.config"
|
||||
---@param config neotree.Config.Base
|
||||
function M.check_config(config)
|
||||
---@type [string, string?][]
|
||||
local errors = {}
|
||||
local start = vim.uv.hrtime()
|
||||
local verbose = vim.o.verbose > 0
|
||||
local matched, missed = validate(
|
||||
"config",
|
||||
config,
|
||||
function(cfg)
|
||||
---@class neotree.health.Validator.Generators
|
||||
local v = {
|
||||
array = function(validator)
|
||||
---@generic T
|
||||
---@param arr T[]
|
||||
return function(arr)
|
||||
for i, val in ipairs(arr) do
|
||||
validate(("[%d]"):format(i), val, validator)
|
||||
end
|
||||
end
|
||||
end,
|
||||
literal = function(literals)
|
||||
return function(value)
|
||||
return vim.tbl_contains(literals, value),
|
||||
("value %s did not match literals %s"):format(value, table.concat(literals, "|"))
|
||||
end
|
||||
end,
|
||||
}
|
||||
local schema = {
|
||||
Filesystem = {
|
||||
---@param follow_current_file neotree.Config.Filesystem.FollowCurrentFile
|
||||
FollowCurrentFile = function(follow_current_file)
|
||||
validate("enabled", follow_current_file.enabled, "boolean", true)
|
||||
validate("leave_dirs_open", follow_current_file.leave_dirs_open, "boolean", true)
|
||||
end,
|
||||
},
|
||||
|
||||
---@param window neotree.Config.Window
|
||||
Window = function(window)
|
||||
validate("mappings", window.mappings, "table") -- TODO: More specific validation for mappings table
|
||||
end,
|
||||
SourceSelector = {
|
||||
---@param item neotree.Config.SourceSelector.Item
|
||||
Item = function(item)
|
||||
validate("source", item.source, "string")
|
||||
validate("padding", item.padding, { "number", "table" }, true) -- TODO: More specific validation for padding table
|
||||
validate("separator", item.separator, { "string", "table" }, true) -- TODO: More specific validation for separator table
|
||||
end,
|
||||
---@param sep neotree.Config.SourceSelector.Separator
|
||||
Separator = function(sep)
|
||||
validate("left", sep.left, "string")
|
||||
validate("right", sep.right, "string")
|
||||
validate("override", sep.override, v.literal({ "right", "left", "active" }), true)
|
||||
end,
|
||||
},
|
||||
Renderers = v.array("table"),
|
||||
}
|
||||
|
||||
if not validate("config", cfg, "table") then
|
||||
health.error("Config does not exist")
|
||||
return
|
||||
end
|
||||
|
||||
validate("sources", cfg.sources, v.array("string"), false)
|
||||
validate("add_blank_line_at_top", cfg.add_blank_line_at_top, "boolean")
|
||||
validate("auto_clean_after_session_restore", cfg.auto_clean_after_session_restore, "boolean")
|
||||
validate("close_if_last_window", cfg.close_if_last_window, "boolean")
|
||||
validate("default_source", cfg.default_source, "string")
|
||||
validate("enable_diagnostics", cfg.enable_diagnostics, "boolean")
|
||||
validate("enable_git_status", cfg.enable_git_status, "boolean")
|
||||
validate("enable_modified_markers", cfg.enable_modified_markers, "boolean")
|
||||
validate("enable_opened_markers", cfg.enable_opened_markers, "boolean")
|
||||
validate("enable_refresh_on_write", cfg.enable_refresh_on_write, "boolean")
|
||||
validate("enable_cursor_hijack", cfg.enable_cursor_hijack, "boolean")
|
||||
validate("git_status_async", cfg.git_status_async, "boolean")
|
||||
validate("git_status_async_options", cfg.git_status_async_options, function(options)
|
||||
validate("batch_size", options.batch_size, "number")
|
||||
validate("batch_delay", options.batch_delay, "number")
|
||||
validate("max_lines", options.max_lines, "number")
|
||||
end)
|
||||
validate("hide_root_node", cfg.hide_root_node, "boolean")
|
||||
validate("retain_hidden_root_indent", cfg.retain_hidden_root_indent, "boolean")
|
||||
validate(
|
||||
"log_level",
|
||||
cfg.log_level,
|
||||
v.literal({ "trace", "debug", "info", "warn", "error", "fatal" }),
|
||||
true
|
||||
)
|
||||
validate("log_to_file", cfg.log_to_file, { "boolean", "string" })
|
||||
validate("open_files_in_last_window", cfg.open_files_in_last_window, "boolean")
|
||||
validate(
|
||||
"open_files_do_not_replace_types",
|
||||
cfg.open_files_do_not_replace_types,
|
||||
v.array("string")
|
||||
)
|
||||
validate("open_files_using_relative_paths", cfg.open_files_using_relative_paths, "boolean")
|
||||
validate(
|
||||
"popup_border_style",
|
||||
cfg.popup_border_style,
|
||||
v.literal({ "NC", "rounded", "single", "solid", "double", "" })
|
||||
)
|
||||
validate("resize_timer_interval", cfg.resize_timer_interval, "number")
|
||||
validate("sort_case_insensitive", cfg.sort_case_insensitive, "boolean")
|
||||
validate("sort_function", cfg.sort_function, "function", true)
|
||||
validate("use_popups_for_input", cfg.use_popups_for_input, "boolean")
|
||||
validate("use_default_mappings", cfg.use_default_mappings, "boolean")
|
||||
validate("source_selector", cfg.source_selector, function(ss)
|
||||
validate("winbar", ss.winbar, "boolean")
|
||||
validate("statusline", ss.statusline, "boolean")
|
||||
validate("show_scrolled_off_parent_node", ss.show_scrolled_off_parent_node, "boolean")
|
||||
validate("sources", ss.sources, v.array(schema.SourceSelector.Item))
|
||||
validate("content_layout", ss.content_layout, v.literal({ "start", "end", "center" }))
|
||||
validate(
|
||||
"tabs_layout",
|
||||
ss.tabs_layout,
|
||||
v.literal({ "equal", "start", "end", "center", "focus" })
|
||||
)
|
||||
validate("truncation_character", ss.truncation_character, "string", false)
|
||||
validate("tabs_min_width", ss.tabs_min_width, "number", true)
|
||||
validate("tabs_max_width", ss.tabs_max_width, "number", true)
|
||||
validate("padding", ss.padding, { "number", "table" }) -- TODO: More specific validation for padding table
|
||||
validate("separator", ss.separator, schema.SourceSelector.Separator)
|
||||
validate("separator_active", ss.separator_active, schema.SourceSelector.Separator, true)
|
||||
validate("show_separator_on_edge", ss.show_separator_on_edge, "boolean")
|
||||
validate("highlight_tab", ss.highlight_tab, "string")
|
||||
validate("highlight_tab_active", ss.highlight_tab_active, "string")
|
||||
validate("highlight_background", ss.highlight_background, "string")
|
||||
validate("highlight_separator", ss.highlight_separator, "string")
|
||||
validate("highlight_separator_active", ss.highlight_separator_active, "string")
|
||||
end)
|
||||
validate("event_handlers", cfg.event_handlers, v.array("table"), true) -- TODO: More specific validation for event handlers
|
||||
validate("default_component_configs", cfg.default_component_configs, function(defaults)
|
||||
validate("container", defaults.container, "table") -- TODO: More specific validation
|
||||
validate("indent", defaults.indent, "table") -- TODO: More specific validation
|
||||
validate("icon", defaults.icon, "table") -- TODO: More specific validation
|
||||
validate("modified", defaults.modified, "table") -- TODO: More specific validation
|
||||
validate("name", defaults.name, "table") -- TODO: More specific validation
|
||||
validate("git_status", defaults.git_status, "table") -- TODO: More specific validation
|
||||
validate("file_size", defaults.file_size, "table") -- TODO: More specific validation
|
||||
validate("type", defaults.type, "table") -- TODO: More specific validation
|
||||
validate("last_modified", defaults.last_modified, "table") -- TODO: More specific validation
|
||||
validate("created", defaults.created, "table") -- TODO: More specific validation
|
||||
validate("symlink_target", defaults.symlink_target, "table") -- TODO: More specific validation
|
||||
end)
|
||||
validate("renderers", cfg.renderers, schema.Renderers)
|
||||
validate("nesting_rules", cfg.nesting_rules, v.array("table"), true) -- TODO: More specific validation for nesting rules
|
||||
validate("commands", cfg.commands, "table", true) -- TODO: More specific validation for commands
|
||||
validate("window", cfg.window, function(window)
|
||||
validate("position", window.position, "string") -- TODO: More specific validation
|
||||
validate("width", window.width, "number")
|
||||
validate("height", window.height, "number")
|
||||
validate("auto_expand_width", window.auto_expand_width, "boolean")
|
||||
validate("popup", window.popup, function(popup)
|
||||
validate("title", popup.title, "function")
|
||||
validate("size", popup.size, function(size)
|
||||
validate("height", size.height, { "string", "number" })
|
||||
validate("width", size.width, { "string", "number" })
|
||||
end)
|
||||
validate(
|
||||
"border",
|
||||
popup.border,
|
||||
v.literal({ "NC", "rounded", "single", "solid", "double", "" }),
|
||||
true
|
||||
)
|
||||
end)
|
||||
validate("insert_as", window.insert_as, v.literal({ "child", "sibling" }), true)
|
||||
validate("mapping_options", window.mapping_options, "table") -- TODO: More specific validation
|
||||
validate("mappings", window.mappings, v.array("table")) -- TODO: More specific validation for mapping items
|
||||
end)
|
||||
|
||||
validate("filesystem", cfg.filesystem, function(fs)
|
||||
validate(
|
||||
"async_directory_scan",
|
||||
fs.async_directory_scan,
|
||||
v.literal({ "auto", "always", "never" })
|
||||
)
|
||||
validate("scan_mode", fs.scan_mode, v.literal({ "shallow", "deep" }))
|
||||
validate("bind_to_cwd", fs.bind_to_cwd, "boolean")
|
||||
validate("cwd_target", fs.cwd_target, function(cwd_target)
|
||||
validate("sidebar", cwd_target.sidebar, v.literal({ "tab", "window", "global" }))
|
||||
validate("current", cwd_target.current, v.literal({ "tab", "window", "global" }))
|
||||
end)
|
||||
validate("check_gitignore_in_search", fs.check_gitignore_in_search, "boolean")
|
||||
validate("filtered_items", fs.filtered_items, function(f)
|
||||
validate("visible", f.visible, "boolean")
|
||||
validate("force_visible_in_empty_folder", f.force_visible_in_empty_folder, "boolean")
|
||||
validate("children_inherit_highlights", f.children_inherit_highlights, "boolean")
|
||||
validate("show_hidden_count", f.show_hidden_count, "boolean")
|
||||
validate("hide_dotfiles", f.hide_dotfiles, "boolean")
|
||||
validate("hide_gitignored", f.hide_gitignored, "boolean")
|
||||
validate("hide_hidden", f.hide_hidden, "boolean")
|
||||
validate("hide_by_name", f.hide_by_name, v.array("string"))
|
||||
validate("hide_by_pattern", f.hide_by_pattern, v.array("string"))
|
||||
validate("always_show", f.always_show, v.array("string"))
|
||||
validate("always_show_by_pattern", f.always_show_by_pattern, v.array("string"))
|
||||
validate("never_show", f.never_show, v.array("string"))
|
||||
validate("never_show_by_pattern", f.never_show_by_pattern, v.array("string"))
|
||||
end)
|
||||
validate("find_by_full_path_words", fs.find_by_full_path_words, "boolean")
|
||||
validate("find_command", fs.find_command, "string", true)
|
||||
validate("find_args", fs.find_args, { "table", "function" }, true)
|
||||
validate("group_empty_dirs", fs.group_empty_dirs, "boolean")
|
||||
validate("search_limit", fs.search_limit, "number")
|
||||
validate("follow_current_file", fs.follow_current_file, schema.Filesystem.FollowCurrentFile)
|
||||
validate(
|
||||
"hijack_netrw_behavior",
|
||||
fs.hijack_netrw_behavior,
|
||||
v.literal({ "open_default", "open_current", "disabled" }),
|
||||
true
|
||||
)
|
||||
validate("use_libuv_file_watcher", fs.use_libuv_file_watcher, "boolean")
|
||||
validate("renderers", fs.renderers, schema.Renderers)
|
||||
validate("window", fs.window, function(window)
|
||||
validate("mappings", window.mappings, "table") -- TODO: More specific validation for mappings table
|
||||
validate("fuzzy_finder_mappings", window.fuzzy_finder_mappings, "table") -- TODO: More specific validation
|
||||
end)
|
||||
end)
|
||||
validate("buffers", cfg.buffers, function(buffers)
|
||||
validate("bind_to_cwd", buffers.bind_to_cwd, "boolean")
|
||||
validate(
|
||||
"follow_current_file",
|
||||
buffers.follow_current_file,
|
||||
schema.Filesystem.FollowCurrentFile
|
||||
)
|
||||
validate("group_empty_dirs", buffers.group_empty_dirs, "boolean")
|
||||
validate("show_unloaded", buffers.show_unloaded, "boolean")
|
||||
validate("terminals_first", buffers.terminals_first, "boolean")
|
||||
validate("renderers", buffers.renderers, schema.Renderers)
|
||||
validate("window", buffers.window, schema.Window)
|
||||
end)
|
||||
validate("git_status", cfg.git_status, function(git_status)
|
||||
validate("renderers", git_status.renderers, schema.Renderers)
|
||||
validate("window", git_status.window, schema.Window)
|
||||
end)
|
||||
validate("document_symbols", cfg.document_symbols, function(ds)
|
||||
validate("follow_cursor", ds.follow_cursor, "boolean")
|
||||
validate("client_filters", ds.client_filters, { "string", "table" }) -- TODO: More specific validation
|
||||
validate("custom_kinds", ds.custom_kinds, "table") -- TODO: More specific validation
|
||||
validate("kinds", ds.kinds, "table")
|
||||
validate("renderers", ds.renderers, schema.Renderers)
|
||||
validate("window", ds.window, schema.Window)
|
||||
end)
|
||||
end,
|
||||
false,
|
||||
nil,
|
||||
function(err)
|
||||
errors[#errors + 1] = { err }
|
||||
end,
|
||||
true
|
||||
)
|
||||
local _end = vim.uv.hrtime()
|
||||
|
||||
if #errors == 0 then
|
||||
health.ok("Configuration conforms to the neotree.Config.Base schema")
|
||||
else
|
||||
for _, err in ipairs(errors) do
|
||||
health.error(unpack(err))
|
||||
end
|
||||
end
|
||||
if verbose then
|
||||
health.info(
|
||||
"[verbose] Config schema checking is not comprehensive yet, unchecked keys listed below:"
|
||||
)
|
||||
if missed then
|
||||
for _, miss in ipairs(missed) do
|
||||
health.info(miss)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function M.check()
|
||||
health.start("Dependencies")
|
||||
check_dependencies()
|
||||
health.start("Configuration")
|
||||
local config = require("neo-tree").ensure_config()
|
||||
M.check_config(config)
|
||||
end
|
||||
|
||||
return M
|
||||
|
|
@ -0,0 +1,213 @@
|
|||
local M = {}
|
||||
|
||||
---Like type() but also supports "callable" like neovim does.
|
||||
---@see _G.type
|
||||
---@param obj any
|
||||
---@param expected neotree.LuaType
|
||||
function M.match(obj, expected)
|
||||
if type(obj) == expected then
|
||||
return true
|
||||
end
|
||||
if expected == "callable" and vim.is_callable(obj) then
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
---@alias neotree.LuaType type|"callable"
|
||||
---@alias neotree.health.ValidatorFunction<T> fun(value: T):boolean?,string?
|
||||
---@alias neotree.health.Validator<T> elem_or_list<neotree.LuaType>|neotree.health.ValidatorFunction<T>
|
||||
|
||||
---@type (fun(err:string))[]
|
||||
M.errfuncs = {}
|
||||
---@type string[]
|
||||
M.namestack = {}
|
||||
|
||||
---@generic T : table
|
||||
---@param path string
|
||||
---@param tbl T
|
||||
---@param accesses string[]
|
||||
---@param missed_paths table<string, true?>
|
||||
---@return T mocked_tbl
|
||||
local function mock_recursive(path, tbl, accesses, missed_paths, track_missed)
|
||||
local mock_table = {}
|
||||
|
||||
---@class neotree.health.Mock.Metatable<T> : metatable
|
||||
---@field accesses string[]
|
||||
local mt = {
|
||||
__original_table = tbl,
|
||||
accesses = accesses,
|
||||
}
|
||||
|
||||
---@return string[] missed_paths
|
||||
mt.get_missed_paths = function()
|
||||
---@type string[]
|
||||
local missed_list = {}
|
||||
if track_missed then
|
||||
for p, _ in pairs(missed_paths) do
|
||||
table.insert(missed_list, p)
|
||||
end
|
||||
end
|
||||
table.sort(missed_list)
|
||||
return missed_list
|
||||
end
|
||||
|
||||
mt.__index = function(_, key)
|
||||
local path_segment
|
||||
if type(key) == "number" then
|
||||
path_segment = ("[%02d]"):format(key)
|
||||
else
|
||||
path_segment = tostring(key)
|
||||
end
|
||||
|
||||
local full_path
|
||||
if path == "" then
|
||||
full_path = path_segment
|
||||
elseif type(key) == "number" then
|
||||
full_path = path .. path_segment
|
||||
else
|
||||
full_path = path .. "." .. path_segment
|
||||
end
|
||||
|
||||
-- Track accesses and missed accesses
|
||||
mt.accesses[#mt.accesses + 1] = full_path
|
||||
if track_missed then
|
||||
missed_paths[full_path] = nil
|
||||
end
|
||||
|
||||
local value = mt.__original_table[key]
|
||||
|
||||
if type(value) == "table" then
|
||||
return mock_recursive(full_path, value, mt.accesses, missed_paths, track_missed)
|
||||
end
|
||||
return value
|
||||
end
|
||||
|
||||
setmetatable(mock_table, mt)
|
||||
return mock_table
|
||||
end
|
||||
|
||||
--- Wraps a given table in a special mock table that tracks all accesses
|
||||
--- (reads) to its fields and sub-fields. Optionally tracks unaccessed fields.
|
||||
---
|
||||
---@generic T : table
|
||||
---@param name string The base name for the table, this forms the root of the access paths.
|
||||
---@param tbl T The table to be mocked.
|
||||
---@param track_missed boolean? Track which fields were NOT accessed.
|
||||
---@return T mocked
|
||||
function M.mock(name, tbl, track_missed)
|
||||
local accesses = {}
|
||||
local path_set = {}
|
||||
track_missed = track_missed or false
|
||||
|
||||
if track_missed then
|
||||
-- Generate another mock table and fully traverse that one first
|
||||
local root_mock = M.mock(name, tbl, false)
|
||||
|
||||
---@param current_table table
|
||||
local function deep_traverse_mock(current_table)
|
||||
---@type neotree.health.Mock.Metatable
|
||||
local mt = getmetatable(current_table)
|
||||
for k, v in pairs(mt.__original_table) do
|
||||
if type(v) == "table" then
|
||||
deep_traverse_mock(current_table[k])
|
||||
else
|
||||
mt.__index(nil, k)
|
||||
end
|
||||
end
|
||||
end
|
||||
deep_traverse_mock(root_mock)
|
||||
accesses = getmetatable(root_mock).accesses
|
||||
for _, path in ipairs(accesses) do
|
||||
path_set[path] = true
|
||||
end
|
||||
end
|
||||
|
||||
-- Start the recursive mocking process, passing all necessary shared tracking data.
|
||||
return mock_recursive(name, tbl, accesses, path_set, track_missed)
|
||||
end
|
||||
|
||||
---A comprehensive version of vim.validate that makes it easy to validate nested tables of various types
|
||||
---@generic T
|
||||
---@param name string
|
||||
---@param value T
|
||||
---@param validator neotree.health.Validator<T>
|
||||
---@param optional? boolean Whether value can be nil
|
||||
---@param message? string message when validation fails
|
||||
---@param on_invalid? fun(err: string, value: T):boolean? What to do when a (nested) validation fails, return true to throw error
|
||||
---@param track_missed? boolean Whether to return a second table that contains every non-checked field
|
||||
---@return boolean valid
|
||||
---@return string[]? missed
|
||||
function M.validate(name, value, validator, optional, message, on_invalid, track_missed)
|
||||
local matched, errmsg, errinfo
|
||||
M.namestack[#M.namestack + 1] = name
|
||||
if type(validator) == "string" then
|
||||
matched = M.match(value, validator)
|
||||
elseif type(validator) == "table" then
|
||||
for _, v in ipairs(validator) do
|
||||
matched = M.match(value, v)
|
||||
if matched then
|
||||
break
|
||||
end
|
||||
end
|
||||
elseif type(validator) == "function" and value ~= nil then
|
||||
local ok = false
|
||||
if on_invalid then
|
||||
M.errfuncs[#M.errfuncs + 1] = on_invalid
|
||||
end
|
||||
if track_missed and type(value) == "table" then
|
||||
value = M.mock(name, value, true)
|
||||
end
|
||||
ok, matched, errinfo = pcall(validator, value)
|
||||
if on_invalid then
|
||||
M.errfuncs[#M.errfuncs] = nil
|
||||
end
|
||||
if not ok then
|
||||
errinfo = matched
|
||||
matched = false
|
||||
elseif matched == nil then
|
||||
matched = true
|
||||
end
|
||||
end
|
||||
matched = matched or (optional and value == nil) or false
|
||||
|
||||
if not matched then
|
||||
---@type string
|
||||
local expected
|
||||
if vim.is_callable(validator) then
|
||||
expected = "?"
|
||||
else
|
||||
---@cast validator -function
|
||||
local expected_types = type(validator) == "string" and { validator } or validator
|
||||
---@cast expected_types -string
|
||||
if optional then
|
||||
expected_types[#expected_types + 1] = "nil"
|
||||
end
|
||||
expected = table.concat(expected_types, "|")
|
||||
end
|
||||
|
||||
errmsg = ("%s: %s, got %s"):format(
|
||||
table.concat(M.namestack, "."),
|
||||
message or ("expected " .. expected),
|
||||
message and value or type(value)
|
||||
)
|
||||
if errinfo then
|
||||
errmsg = errmsg .. ", Info: " .. errinfo
|
||||
end
|
||||
local errfunc = M.errfuncs[#M.errfuncs]
|
||||
local should_error = not errfunc or errfunc(errmsg)
|
||||
if should_error then
|
||||
M.namestack[#M.namestack] = nil
|
||||
error(errmsg, 2)
|
||||
end
|
||||
end
|
||||
M.namestack[#M.namestack] = nil
|
||||
|
||||
if track_missed then
|
||||
local missed = getmetatable(value).get_missed_paths()
|
||||
return matched, missed
|
||||
end
|
||||
return matched
|
||||
end
|
||||
|
||||
return M
|
||||
188
.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/log.lua
Normal file
188
.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/log.lua
Normal file
|
|
@ -0,0 +1,188 @@
|
|||
-- 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.
|
||||
|
||||
-- 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.DEBUG },
|
||||
{ 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
|
||||
|
||||
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
|
||||
-- Ignore 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,136 @@
|
|||
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.bo[buf].buftype = "nofile"
|
||||
vim.bo[buf].bufhidden = "wipe"
|
||||
vim.bo[buf].buflisted = false
|
||||
vim.bo[buf].swapfile = false
|
||||
vim.bo[buf].modifiable = false
|
||||
vim.bo[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
|
||||
|
||||
---@param config neotree.Config.Base
|
||||
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,715 @@
|
|||
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 = {}
|
||||
|
||||
---@param source_config { window: {mappings: neotree.Config.Window.Mappings} }
|
||||
local normalize_mappings = function(source_config)
|
||||
if source_config == nil then
|
||||
return
|
||||
end
|
||||
local mappings = vim.tbl_get(source_config, { "window", "mappings" })
|
||||
if mappings then
|
||||
local fixed = mapping_helper.normalize_mappings(mappings)
|
||||
source_config.window.mappings = fixed --[[@as neotree.Config.Window.Mappings]]
|
||||
end
|
||||
end
|
||||
|
||||
---@param source_config neotree.Config.Filesystem
|
||||
local normalize_fuzzy_mappings = function(source_config)
|
||||
if source_config == nil then
|
||||
return
|
||||
end
|
||||
local mappings = source_config.window and source_config.window.fuzzy_finder_mappings
|
||||
if mappings then
|
||||
local fixed = mapping_helper.normalize_mappings(mappings)
|
||||
source_config.window.fuzzy_finder_mappings = fixed --[[@as neotree.Config.FuzzyFinder.Mappings]]
|
||||
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
|
||||
|
||||
-- if vim is trying to open a dir, then we hijack it
|
||||
if netrw.hijack() then
|
||||
return
|
||||
end
|
||||
|
||||
-- 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.bo[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 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#")
|
||||
local win_width = vim.api.nvim_win_get_width(current_winid)
|
||||
-- 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.
|
||||
---@diagnostic disable-next-line: param-type-mismatch
|
||||
pcall(vim.cmd, "bdelete " .. bufname)
|
||||
local fake_state = {
|
||||
window = {
|
||||
position = position,
|
||||
width = win_width or M.config.window.width,
|
||||
},
|
||||
}
|
||||
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 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) --[[@as neotree.sources.filesystem.State]]
|
||||
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
|
||||
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
|
||||
|
||||
---@param user_config neotree.Config?
|
||||
---@return neotree.Config.Base full_config
|
||||
M.merge_config = function(user_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")
|
||||
|
||||
if events_setup then
|
||||
events.clear_all_events()
|
||||
end
|
||||
define_events()
|
||||
|
||||
-- Prevent netrw hijacking lazy-loading from conflicting with normal hijacking.
|
||||
vim.g.neotree_watching_bufenter = 1
|
||||
|
||||
-- Prevent accidentally opening another file in the neo-tree window.
|
||||
events.subscribe({
|
||||
event = events.VIM_BUFFER_ENTER,
|
||||
handler = M.buffer_enter_event,
|
||||
})
|
||||
events.subscribe({
|
||||
event = events.NEO_TREE_WINDOW_AFTER_OPEN,
|
||||
handler = function(args)
|
||||
if not vim.w[args.winid].neo_tree_settings_applied then
|
||||
-- TODO: should figure out a less disorganized way to set window options
|
||||
-- BufEnter doesn't trigger while vim is starting up so this will handle it instead.
|
||||
M.buffer_enter_event()
|
||||
end
|
||||
end,
|
||||
})
|
||||
|
||||
-- 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 or {}) 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)
|
||||
|
||||
normalize_fuzzy_mappings(default_config.filesystem)
|
||||
normalize_fuzzy_mappings(user_config.filesystem)
|
||||
if user_config.use_default_mappings == false then
|
||||
default_config.filesystem.window.fuzzy_finder_mappings = {}
|
||||
end
|
||||
-- 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
|
||||
|
||||
-- local 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
|
||||
|
||||
---@type neotree.Config.HijackNetrwBehavior[]
|
||||
local disable_netrw_values = { "open_default", "open_current" }
|
||||
local hijack_behavior = M.config.filesystem.hijack_netrw_behavior
|
||||
if vim.tbl_contains(disable_netrw_values, hijack_behavior) then
|
||||
-- Disable netrw autocmds
|
||||
vim.cmd("silent! autocmd! FileExplorer *")
|
||||
elseif hijack_behavior ~= "disabled" then
|
||||
require("neo-tree.log").error(
|
||||
"Invalid value for filesystem.hijack_netrw_behavior: '"
|
||||
.. hijack_behavior
|
||||
.. "', will default to 'disabled'"
|
||||
)
|
||||
M.config.filesystem.hijack_netrw_behavior = "disabled"
|
||||
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
|
||||
if vim.tbl_contains(M.config.sources, ss_source.source) 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] --[[@as table]], M.config, module)
|
||||
manager.redraw(source_name)
|
||||
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)
|
||||
if not winid then
|
||||
return
|
||||
end
|
||||
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,76 @@
|
|||
local utils = require("neo-tree.utils")
|
||||
|
||||
local M = {}
|
||||
|
||||
---@param key string
|
||||
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
|
||||
|
||||
---@class neotree.SimpleMappings
|
||||
---@field [string] string|function?
|
||||
|
||||
---@class neotree.SimpleMappingsByMode
|
||||
---@field [string] neotree.SimpleMappings?
|
||||
|
||||
---@class neotree.Mappings : neotree.SimpleMappings
|
||||
---@field [integer] neotree.SimpleMappingsByMode?
|
||||
|
||||
---@param map neotree.Mappings
|
||||
---@return neotree.Mappings new_map
|
||||
M.normalize_mappings = function(map)
|
||||
local new_map = M.normalize_simple_mappings(map)
|
||||
---@cast new_map neotree.Mappings
|
||||
for i, mappings_by_mode in ipairs(map) do
|
||||
new_map[i] = {}
|
||||
for mode, simple_mappings in pairs(mappings_by_mode) do
|
||||
---@cast simple_mappings neotree.SimpleMappings
|
||||
new_map[i][mode] = M.normalize_simple_mappings(simple_mappings)
|
||||
end
|
||||
end
|
||||
return new_map
|
||||
end
|
||||
|
||||
---@param map neotree.SimpleMappings
|
||||
---@return neotree.SimpleMappings new_map
|
||||
M.normalize_simple_mappings = function(map)
|
||||
local new_map = {}
|
||||
for key, value in pairs(map) do
|
||||
if type(key) == "string" then
|
||||
local normalized_key = M.normalize_map_key(key)
|
||||
if normalized_key ~= nil then
|
||||
new_map[normalized_key] = value
|
||||
end
|
||||
end
|
||||
end
|
||||
return new_map
|
||||
end
|
||||
|
||||
return M
|
||||
|
|
@ -0,0 +1,102 @@
|
|||
local uv = vim.uv or vim.loop
|
||||
local nt = require("neo-tree")
|
||||
local utils = require("neo-tree.utils")
|
||||
local M = {}
|
||||
|
||||
local get_position = function(source_name)
|
||||
local pos = utils.get_value(nt.config, source_name .. ".window.position", "left", true)
|
||||
return pos
|
||||
end
|
||||
|
||||
---@return neotree.Config.HijackNetrwBehavior
|
||||
M.get_hijack_behavior = function()
|
||||
nt.ensure_config()
|
||||
return nt.config.filesystem.hijack_netrw_behavior
|
||||
end
|
||||
|
||||
---@return boolean hijacked Whether the hijack was successful
|
||||
M.hijack = function()
|
||||
local hijack_behavior = M.get_hijack_behavior()
|
||||
if hijack_behavior == "disabled" then
|
||||
return false
|
||||
end
|
||||
|
||||
-- ensure this is a directory
|
||||
local dir_bufnr = vim.api.nvim_get_current_buf()
|
||||
local path_to_hijack = vim.api.nvim_buf_get_name(dir_bufnr)
|
||||
local stats = uv.fs_stat(path_to_hijack)
|
||||
if not stats or 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 dir_window = vim.api.nvim_get_current_win()
|
||||
|
||||
-- Now actually open the tree, with a very quick debounce because this may be
|
||||
-- called multiple times in quick succession.
|
||||
utils.debounce("hijack_netrw_" .. dir_window, function()
|
||||
local manager = require("neo-tree.sources.manager")
|
||||
local log = require("neo-tree.log")
|
||||
-- We will want to replace the "directory" buffer with either the "alternate"
|
||||
-- buffer or a new blank one.
|
||||
local replacement_buffer = vim.fn.bufnr("#")
|
||||
local is_currently_neo_tree = false
|
||||
if replacement_buffer > 0 then
|
||||
if vim.bo[replacement_buffer].filetype == "neo-tree" then
|
||||
-- don't hijack the current window if it's already a Neo-tree sidebar
|
||||
local position = vim.b[replacement_buffer].neo_tree_position
|
||||
if position == "current" then
|
||||
replacement_buffer = -1
|
||||
else
|
||||
is_currently_neo_tree = true
|
||||
end
|
||||
end
|
||||
end
|
||||
if not should_open_current then
|
||||
if replacement_buffer == dir_bufnr or replacement_buffer < 1 then
|
||||
replacement_buffer = vim.api.nvim_create_buf(true, false)
|
||||
log.trace("Created new buffer for netrw hijack", replacement_buffer)
|
||||
end
|
||||
end
|
||||
if replacement_buffer > 0 then
|
||||
log.trace("Replacing buffer in netrw hijack", replacement_buffer)
|
||||
pcall(vim.api.nvim_win_set_buf, dir_window, replacement_buffer)
|
||||
end
|
||||
|
||||
-- If a window takes focus (e.g. lazy.nvim installing plugins on startup) in the time between the method call and
|
||||
-- this debounced callback, we should focus that window over neo-tree.
|
||||
local current_window = vim.api.nvim_get_current_win()
|
||||
local should_restore_cursor = current_window ~= dir_window
|
||||
|
||||
local cleanup = vim.schedule_wrap(function()
|
||||
log.trace("Deleting buffer in netrw hijack", dir_bufnr)
|
||||
pcall(vim.api.nvim_buf_delete, dir_bufnr, { force = true })
|
||||
if should_restore_cursor then
|
||||
vim.api.nvim_set_current_win(current_window)
|
||||
end
|
||||
end)
|
||||
|
||||
---@type neotree.sources.filesystem.State
|
||||
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, dir_window) --[[@as neotree.sources.filesystem.State]]
|
||||
state.current_position = "current"
|
||||
elseif is_currently_neo_tree then
|
||||
log.debug("hijack_netrw: opening in existing Neo-tree")
|
||||
state = manager.get_state("filesystem") --[[@as neotree.sources.filesystem.State]]
|
||||
else
|
||||
log.debug("hijack_netrw: opening default")
|
||||
manager.close_all_except("filesystem")
|
||||
state = manager.get_state("filesystem") --[[@as neotree.sources.filesystem.State]]
|
||||
end
|
||||
|
||||
require("neo-tree.sources.filesystem")._navigate_internal(state, path_to_hijack, nil, cleanup)
|
||||
end, 10, utils.debounce_strategy.CALL_LAST_ONLY)
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
return M
|
||||
|
|
@ -0,0 +1,100 @@
|
|||
--This file should contain all commands meant to be used by mappings.
|
||||
|
||||
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")
|
||||
|
||||
---@class neotree.sources.Buffers.Commands : neotree.sources.Common.Commands
|
||||
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
|
||||
|
||||
---@type neotree.TreeCommandVisual
|
||||
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
|
||||
|
||||
---@type neotree.TreeCommandVisual
|
||||
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,58 @@
|
|||
-- 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")
|
||||
|
||||
---@alias neotree.Component.Buffers._Key
|
||||
---|"name"
|
||||
|
||||
---@class neotree.Component.Buffers
|
||||
---@field [1] neotree.Component.Buffers._Key|neotree.Component.Common._Key
|
||||
|
||||
---@type table<neotree.Component.Buffers._Key, neotree.Renderer>
|
||||
local M = {}
|
||||
|
||||
---@class (exact) neotree.Component.Buffers.Name : neotree.Component.Common.Name
|
||||
|
||||
---@param config neotree.Component.Buffers.Name
|
||||
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,215 @@
|
|||
--This file should have all functions that are in the public api and either set
|
||||
--or read the state of this source.
|
||||
|
||||
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")
|
||||
|
||||
---@class neotree.sources.Buffers : neotree.Source
|
||||
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 state neotree.State
|
||||
---@param path string? Path to navigate to. If empty, will navigate to the cwd.
|
||||
---@param path_to_reveal string?
|
||||
---@param callback function?
|
||||
---@param async boolean?
|
||||
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
|
||||
|
||||
---@class neotree.Config.Buffers.Renderers : neotree.Config.Renderers
|
||||
|
||||
---@class (exact) neotree.Config.Buffers : neotree.Config.Source
|
||||
---@field bind_to_cwd boolean?
|
||||
---@field follow_current_file neotree.Config.Filesystem.FollowCurrentFile?
|
||||
---@field group_empty_dirs boolean?
|
||||
---@field show_unloaded boolean?
|
||||
---@field terminals_first boolean?
|
||||
---@field renderers neotree.Config.Buffers.Renderers?
|
||||
|
||||
---Configures the plugin, should be called before the plugin is used.
|
||||
---@param config neotree.Config.Buffers Configuration table containing any keys that the user wants to change from the defaults. May be empty to accept default values.
|
||||
---@param global_config neotree.Config.Base
|
||||
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,110 @@
|
|||
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") --[[@as neotree.FileItem.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
|
||||
-- make sure this is within the root path
|
||||
if utils.is_subpath(state.path, 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,965 @@
|
|||
--This file should contain all commands meant to be used by mappings.
|
||||
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")
|
||||
|
||||
---@alias neotree.TreeCommandNormal fun(state: neotree.StateWithTree, ...: any)
|
||||
---@alias neotree.TreeCommandVisual fun(state: neotree.StateWithTree, selected_nodes: NuiTree.Node[], ...: any)
|
||||
---@alias neotree.TreeCommand neotree.TreeCommandNormal|neotree.TreeCommandVisual
|
||||
|
||||
---Gets the node parent folder
|
||||
---@param state neotree.StateWithTree
|
||||
---@return NuiTree.Node? node
|
||||
local function get_folder_node(state)
|
||||
local tree = state.tree
|
||||
local node = tree:get_node()
|
||||
local last_id = assert(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 neotree.StateWithTree
|
||||
---@return string root_path 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
|
||||
---TODO
|
||||
---@diagnostic disable-next-line: return-type-mismatch
|
||||
return using_root_directory
|
||||
end
|
||||
|
||||
---@class neotree.sources.Common.Commands
|
||||
---@field [string] neotree.TreeCommand
|
||||
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 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)
|
||||
if not node then
|
||||
return
|
||||
end
|
||||
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 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)
|
||||
if not node then
|
||||
return
|
||||
end
|
||||
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 node table? A single node to expand (defaults to all root nodes)
|
||||
---@param prefetcher table? an object with two methods `prefetch(state, node)` and `should_prefetch(node) => boolean`
|
||||
M.expand_all_nodes = function(state, node, prefetcher)
|
||||
local root_nodes = node and { node } or state.tree:get_nodes()
|
||||
|
||||
renderer.position.set(state, nil)
|
||||
|
||||
local task = function()
|
||||
for _, root in pairs(root_nodes) do
|
||||
log.debug("Expanding all nodes under " .. root:get_id())
|
||||
node_expander.expand_directory_recursively(state, root, prefetcher)
|
||||
end
|
||||
end
|
||||
async.run(task, function()
|
||||
log.debug("All nodes expanded - redrawing")
|
||||
renderer.redraw(state)
|
||||
end)
|
||||
end
|
||||
|
||||
---Expand all subnodes
|
||||
---@param node table? A single node to expand (defaults to node under the cursor)
|
||||
---@param prefetcher table? an object with two methods `prefetch(state, node)` and `should_prefetch(node) => boolean`
|
||||
M.expand_all_subnodes = function(state, node, prefetcher)
|
||||
M.expand_all_nodes(state, node or state.tree:get_node(), prefetcher)
|
||||
end
|
||||
|
||||
---@param callback function
|
||||
M.close_node = function(state, callback)
|
||||
local tree = state.tree
|
||||
local node = assert(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
|
||||
assert(target_node, "no node found to close")
|
||||
|
||||
local root = tree:get_nodes()[1]
|
||||
local is_root = target_node:get_id() == root:get_id()
|
||||
|
||||
if 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_nodes and state.explicitly_opened_nodes[target_node:get_id()] then
|
||||
state.explicitly_opened_nodes[target_node:get_id()] = false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
M.close_all_subnodes = function(state)
|
||||
local tree = state.tree
|
||||
local node = assert(tree:get_node())
|
||||
local parent_node = assert(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_nodes and state.explicitly_opened_nodes[target_node:get_id()] then
|
||||
state.explicitly_opened_nodes[target_node:get_id()] = false
|
||||
end
|
||||
end
|
||||
|
||||
---@param state neotree.State
|
||||
M.close_all_nodes = function(state)
|
||||
state.explicitly_opened_nodes = {}
|
||||
renderer.collapse_all_nodes(state.tree)
|
||||
renderer.redraw(state)
|
||||
end
|
||||
|
||||
---@param state neotree.State
|
||||
M.close_window = function(state)
|
||||
renderer.close(state)
|
||||
end
|
||||
|
||||
---@param state neotree.State
|
||||
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
|
||||
|
||||
---@param state neotree.State
|
||||
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.
|
||||
---@param state neotree.State
|
||||
M.copy_to_clipboard = function(state, callback)
|
||||
local node = assert(state.tree:get_node())
|
||||
if node.type == "message" then
|
||||
return
|
||||
end
|
||||
copy_node_to_clipboard(state, node)
|
||||
if callback then
|
||||
callback()
|
||||
end
|
||||
end
|
||||
|
||||
---@type neotree.TreeCommandVisual
|
||||
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
|
||||
|
||||
---@param state neotree.State
|
||||
---@param node NuiTree.Node
|
||||
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 = assert(state.tree:get_node())
|
||||
cut_node_to_clipboard(state, node)
|
||||
if callback then
|
||||
callback()
|
||||
end
|
||||
end
|
||||
|
||||
---@type neotree.TreeCommandVisual
|
||||
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
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
---@param state neotree.State
|
||||
M.git_add_file = function(state)
|
||||
local node = assert(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
|
||||
|
||||
---@param state neotree.State
|
||||
M.git_add_all = function(state)
|
||||
local cmd = { "git", "add", "-A" }
|
||||
vim.fn.system(cmd)
|
||||
events.fire_event(events.GIT_EVENT)
|
||||
end
|
||||
|
||||
---@param state neotree.State
|
||||
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 = assert(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_undo_last_commit = function(state)
|
||||
inputs.confirm("Are you sure you want to undo the last commit? (keeps changes)", function(yes)
|
||||
if yes then
|
||||
local cmd = { "git", "reset", "--soft", "HEAD~1" }
|
||||
local result = vim.fn.systemlist(cmd)
|
||||
if vim.v.shell_error ~= 0 then
|
||||
popups.alert("ERROR: git reset --soft HEAD~1", result)
|
||||
return
|
||||
end
|
||||
events.fire_event(events.GIT_EVENT)
|
||||
popups.alert(
|
||||
"git reset --soft HEAD~1",
|
||||
{ "Last commit undone successfully", "Changes kept in staging area" }
|
||||
)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
M.git_revert_file = function(state)
|
||||
local node = assert(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
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
local get_sources = function()
|
||||
local config = require("neo-tree").config
|
||||
return config.source_selector.sources or config.sources
|
||||
end
|
||||
|
||||
M.next_source = function(state)
|
||||
local sources = get_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 = get_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")
|
||||
local config = require("neo-tree").config
|
||||
if config.sort_case_insensitive then
|
||||
state.sort_field_provider = function(node)
|
||||
return node.path:lower()
|
||||
end
|
||||
else
|
||||
state.sort_field_provider = function(node)
|
||||
return node.path
|
||||
end
|
||||
end
|
||||
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
|
||||
|
||||
local default_filetime_format = "%Y-%m-%d %I:%M %p"
|
||||
M.show_file_details = function(state)
|
||||
local node = assert(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")
|
||||
local created_format = state.config.created_format or default_filetime_format
|
||||
table.insert(right, utils.date(created_format, stat.birthtime.sec))
|
||||
table.insert(left, "Modified")
|
||||
local modified_format = state.config.modified_format or default_filetime_format
|
||||
table.insert(right, utils.date(modified_format, 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 callback fun(node: NuiTree.Node?, destination: string) 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 callback fun(parent_node: NuiTree.Node)
|
||||
M.copy = function(state, callback)
|
||||
local node = assert(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 callback fun(parent_node: NuiTree.Node)
|
||||
M.move = function(state, callback)
|
||||
local node = assert(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 node = assert(state.tree:get_node())
|
||||
if node.type ~= "file" and node.type ~= "directory" then
|
||||
log.warn("The `delete` command can only be used on files and directories")
|
||||
return
|
||||
end
|
||||
if node:get_depth() == 1 then
|
||||
log.error(
|
||||
"Will not delete root node "
|
||||
.. node.path
|
||||
.. ", please back out of the current directory if you want to delete the root node."
|
||||
)
|
||||
return
|
||||
end
|
||||
fs_actions.delete_node(node.path, callback)
|
||||
end
|
||||
|
||||
---@param callback function
|
||||
---@type neotree.TreeCommandVisual
|
||||
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:get_depth() == 1 then
|
||||
log.error(
|
||||
"Will not delete root node "
|
||||
.. node_to_delete.path
|
||||
.. ", please back out of the current directory if you want to delete the root node."
|
||||
)
|
||||
return
|
||||
end
|
||||
|
||||
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(state)
|
||||
if Preview.is_active() then
|
||||
Preview.focus()
|
||||
else
|
||||
vim.api.nvim_win_call(state.winid, function()
|
||||
vim.api.nvim_feedkeys(state.fallback, "n", false)
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
---Expands or collapses the current node.
|
||||
M.toggle_node = function(state, toggle_directory)
|
||||
local tree = state.tree
|
||||
local node = assert(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 = assert(tree:get_node())
|
||||
if node.type ~= "directory" then
|
||||
return
|
||||
end
|
||||
M.toggle_node(state, toggle_directory)
|
||||
end
|
||||
|
||||
---Open file or expandable node
|
||||
---@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 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 == "file" 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
|
||||
|
||||
local should_expand_file = config.expand_nested_files and not node:is_expanded()
|
||||
if utils.is_expandable(node) and (node.type ~= "file" or should_expand_file) then
|
||||
M.toggle_node(state, toggle_directory)
|
||||
else
|
||||
open()
|
||||
end
|
||||
end
|
||||
|
||||
---Open file or directory in the closest window
|
||||
---@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 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 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 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 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 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 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 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 = assert(tree:get_node())
|
||||
if node.type == "message" then
|
||||
return
|
||||
end
|
||||
fs_actions.rename_node(node.path, callback)
|
||||
end
|
||||
|
||||
M.rename_basename = function(state, callback)
|
||||
local tree = state.tree
|
||||
local node = assert(tree:get_node())
|
||||
if node.type == "message" then
|
||||
return
|
||||
end
|
||||
fs_actions.rename_node_basename(node.path, callback)
|
||||
end
|
||||
|
||||
---Marks potential windows with letters and will open the give node in the picked window.
|
||||
---@param state neotree.State
|
||||
---@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)
|
||||
---@diagnostic disable-next-line: param-type-mismatch
|
||||
local result, err = pcall(vim.cmd, cmd .. " " .. vim.fn.fnameescape(path))
|
||||
if result or err == "Vim(edit):E325: ATTENTION" then
|
||||
-- fixes #321
|
||||
vim.bo[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,720 @@
|
|||
-- 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 nt = require("neo-tree")
|
||||
|
||||
---@alias neotree.Component.Common._Key
|
||||
---|"bufnr"
|
||||
---|"clipboard"
|
||||
---|"container"
|
||||
---|"current_filter"
|
||||
---|"diagnostics"
|
||||
---|"git_status"
|
||||
---|"filtered_by"
|
||||
---|"icon"
|
||||
---|"modified"
|
||||
---|"name"
|
||||
---|"indent"
|
||||
---|"file_size"
|
||||
---|"last_modified"
|
||||
---|"created"
|
||||
---|"symlink_target"
|
||||
---|"type"
|
||||
|
||||
---@class neotree.Component.Common Use the neotree.Component.Common.* types to get more specific types.
|
||||
---@field [1] neotree.Component.Common._Key
|
||||
|
||||
---@type table<neotree.Component.Common._Key, neotree.FileRenderer>
|
||||
local M = {}
|
||||
|
||||
local make_two_char = function(symbol)
|
||||
if vim.fn.strchars(symbol) == 1 then
|
||||
return symbol .. " "
|
||||
else
|
||||
return symbol
|
||||
end
|
||||
end
|
||||
|
||||
---@class (exact) neotree.Component.Common.Bufnr : neotree.Component
|
||||
---@field [1] "bufnr"?
|
||||
|
||||
-- Config fields below:
|
||||
-- only works in the buffers component, but it's here so we don't have to defined
|
||||
-- multple renderers.
|
||||
---@param config neotree.Component.Common.Bufnr
|
||||
M.bufnr = function(config, node, _)
|
||||
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
|
||||
|
||||
---@class (exact) neotree.Component.Common.Clipboard : neotree.Component
|
||||
---@field [1] "clipboard"?
|
||||
|
||||
---@param config neotree.Component.Common.Clipboard
|
||||
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
|
||||
|
||||
---@class (exact) neotree.Component.Common.Container : neotree.Component
|
||||
---@field [1] "container"?
|
||||
---@field left_padding integer?
|
||||
---@field right_padding integer?
|
||||
---@field enable_character_fade boolean?
|
||||
---@field content (neotree.Component|{zindex: number, align: "left"|"right"|nil})[]?
|
||||
|
||||
M.container = container.render
|
||||
|
||||
---@class (exact) neotree.Component.Common.CurrentFilter : neotree.Component
|
||||
---@field [1] "current_filter"
|
||||
|
||||
---@param config neotree.Component.Common.CurrentFilter
|
||||
M.current_filter = function(config, node, _)
|
||||
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 get_legacy_sign = function(severity)
|
||||
local sign = vim.fn.sign_getdefined("DiagnosticSign" .. severity)
|
||||
if vim.tbl_isempty(sign) then
|
||||
-- backwards compatibility...
|
||||
local old_severity = severity
|
||||
if severity == "Warning" then
|
||||
old_severity = "Warn"
|
||||
elseif severity == "Information" then
|
||||
old_severity = "Info"
|
||||
end
|
||||
sign = vim.fn.sign_getdefined("LspDiagnosticsSign" .. old_severity)
|
||||
end
|
||||
return sign and sign[1]
|
||||
end
|
||||
|
||||
local nvim_0_10 = vim.fn.has("nvim-0.10") > 0
|
||||
---Returns the sign corresponding to the given severity
|
||||
---@param severity string
|
||||
---@return vim.fn.sign_getdefined.ret.item
|
||||
local function get_diagnostic_sign(severity)
|
||||
local sign
|
||||
|
||||
if nvim_0_10 then
|
||||
local signs = vim.diagnostic.config().signs
|
||||
|
||||
if type(signs) == "function" then
|
||||
--TODO: Find a better way to get a namespace
|
||||
local namespaces = vim.diagnostic.get_namespaces()
|
||||
if not vim.tbl_isempty(namespaces) then
|
||||
local ns_id = next(namespaces)
|
||||
---@cast ns_id -nil
|
||||
signs = signs(ns_id, 0)
|
||||
end
|
||||
end
|
||||
|
||||
if type(signs) == "table" then
|
||||
local identifier = severity:sub(1, 1)
|
||||
if identifier == "H" then
|
||||
identifier = "N"
|
||||
end
|
||||
sign = {
|
||||
text = (signs.text or {})[vim.diagnostic.severity[identifier]],
|
||||
texthl = "DiagnosticSign" .. severity,
|
||||
}
|
||||
elseif signs == true then
|
||||
sign = get_legacy_sign(severity)
|
||||
end
|
||||
else -- before 0.10
|
||||
sign = get_legacy_sign(severity)
|
||||
end
|
||||
|
||||
if type(sign) ~= "table" then
|
||||
sign = {}
|
||||
end
|
||||
return sign
|
||||
end
|
||||
|
||||
---@class (exact) neotree.Component.Common.Diagnostics : neotree.Component
|
||||
---@field [1] "diagnostics"?
|
||||
---@field errors_only boolean?
|
||||
---@field hide_when_expanded boolean?
|
||||
---@field symbols table<string, string>?
|
||||
---@field highlights table<string, string>?
|
||||
|
||||
---@param config neotree.Component.Common.Diagnostics
|
||||
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
|
||||
---@type string
|
||||
local severity = diag_state.severity_string
|
||||
local sign = get_diagnostic_sign(severity)
|
||||
|
||||
-- check for overrides in the component config
|
||||
local severity_lower = severity:lower()
|
||||
if config.symbols and config.symbols[severity_lower] then
|
||||
sign.texthl = sign.texthl or ("Diagnostic" .. severity)
|
||||
sign.text = config.symbols[severity_lower]
|
||||
end
|
||||
if config.highlights and config.highlights[severity_lower] then
|
||||
sign.text = sign.text or severity:sub(1, 1)
|
||||
sign.texthl = config.highlights[severity_lower]
|
||||
end
|
||||
|
||||
if sign.text and sign.texthl then
|
||||
return {
|
||||
text = make_two_char(sign.text),
|
||||
highlight = sign.texthl,
|
||||
}
|
||||
else
|
||||
return {
|
||||
text = severity:sub(1, 1),
|
||||
highlight = "Diagnostic" .. severity,
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
---@class (exact) neotree.Component.Common.GitStatus : neotree.Component
|
||||
---@field [1] "git_status"?
|
||||
---@field hide_when_expanded boolean?
|
||||
---@field symbols table<string, string>?
|
||||
|
||||
---@param config neotree.Component.Common.GitStatus
|
||||
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
|
||||
---@type string?
|
||||
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
|
||||
|
||||
---@class neotree.Component.Common.FilteredBy
|
||||
---@field [1] "filtered_by"?
|
||||
M.filtered_by = function(_, node, state)
|
||||
local fby = node.filtered_by
|
||||
if not state.filtered_items or type(fby) ~= "table" then
|
||||
return {}
|
||||
end
|
||||
repeat
|
||||
if fby.name then
|
||||
return {
|
||||
text = "(hide by name)",
|
||||
highlight = highlights.HIDDEN_BY_NAME,
|
||||
}
|
||||
elseif fby.pattern then
|
||||
return {
|
||||
text = "(hide by pattern)",
|
||||
highlight = highlights.HIDDEN_BY_NAME,
|
||||
}
|
||||
elseif fby.gitignored then
|
||||
return {
|
||||
text = "(gitignored)",
|
||||
highlight = highlights.GIT_IGNORED,
|
||||
}
|
||||
elseif fby.dotfiles then
|
||||
return {
|
||||
text = "(dotfile)",
|
||||
highlight = highlights.DOTFILE,
|
||||
}
|
||||
elseif fby.hidden then
|
||||
return {
|
||||
text = "(hidden)",
|
||||
highlight = highlights.WINDOWS_HIDDEN,
|
||||
}
|
||||
end
|
||||
fby = fby.parent
|
||||
until not state.filtered_items.children_inherit_highlights or not fby
|
||||
return {}
|
||||
end
|
||||
|
||||
---@class (exact) neotree.Component.Common.Icon : neotree.Component
|
||||
---@field [1] "icon"?
|
||||
---@field default string The default icon for a node.
|
||||
---@field folder_empty string The string to display to represent an empty folder.
|
||||
---@field folder_empty_open string The icon to display to represent an empty but open folder.
|
||||
---@field folder_open string The icon to display for an open folder.
|
||||
---@field folder_closed string The icon to display for a closed folder.
|
||||
---@field provider neotree.IconProvider?
|
||||
|
||||
---@param config neotree.Component.Common.Icon
|
||||
M.icon = function(config, node, state)
|
||||
-- calculate default icon
|
||||
---@type neotree.Render.Node
|
||||
local icon =
|
||||
{ text = config.default or " ", highlight = config.highlight or highlights.FILE_ICON }
|
||||
if node.type == "directory" then
|
||||
icon.highlight = highlights.DIRECTORY_ICON
|
||||
if node.loaded and not node:has_children() then
|
||||
icon.text = not node.empty_expanded and config.folder_empty or config.folder_empty_open
|
||||
elseif node:is_expanded() then
|
||||
icon.text = config.folder_open or "-"
|
||||
else
|
||||
icon.text = config.folder_closed or "+"
|
||||
end
|
||||
end
|
||||
|
||||
-- use icon provider if available
|
||||
if config.provider then
|
||||
icon = config.provider(icon, node, state) or icon
|
||||
end
|
||||
|
||||
local filtered_by = M.filtered_by(config, node, state)
|
||||
|
||||
icon.text = icon.text .. " " -- add padding
|
||||
icon.highlight = filtered_by.highlight or icon.highlight -- prioritize filtered highlighting
|
||||
|
||||
return icon
|
||||
end
|
||||
|
||||
---@class (exact) neotree.Component.Common.Modified : neotree.Component
|
||||
---@field [1] "modified"?
|
||||
---@field symbol string?
|
||||
|
||||
---@param config neotree.Component.Common.Modified
|
||||
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
|
||||
|
||||
---@class (exact) neotree.Component.Common.Name : neotree.Component
|
||||
---@field [1] "name"?
|
||||
---@field trailing_slash boolean?
|
||||
---@field use_git_status_colors boolean?
|
||||
---@field highlight_opened_files boolean|"all"?
|
||||
---@field right_padding integer?
|
||||
|
||||
---@param config neotree.Component.Common.Name
|
||||
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
|
||||
if state.current_position == "current" and state.sort and state.sort.label == "Name" then
|
||||
local icon = state.sort.direction == 1 and "▲" or "▼"
|
||||
text = text .. " " .. icon
|
||||
end
|
||||
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
|
||||
|
||||
---@class (exact) neotree.Component.Common.Indent : neotree.Component
|
||||
---@field [1] "indent"?
|
||||
---@field expander_collapsed string?
|
||||
---@field expander_expanded string?
|
||||
---@field expander_highlight string?
|
||||
---@field indent_marker string?
|
||||
---@field indent_size integer?
|
||||
---@field last_indent_marker string?
|
||||
---@field padding integer?
|
||||
---@field with_expanders boolean?
|
||||
---@field with_markers boolean?
|
||||
|
||||
---@param config neotree.Component.Common.Indent
|
||||
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
|
||||
---@cast skip_marker -nil
|
||||
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 truncate_string = function(str, max_length)
|
||||
if #str <= max_length then
|
||||
return str
|
||||
end
|
||||
return str:sub(1, max_length - 1) .. "…"
|
||||
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
|
||||
---diagnostic here is wrong, printf has arbitrary args.
|
||||
---@diagnostic disable-next-line: redundant-parameter
|
||||
return vim.fn.printf("%" .. size .. "s %s ", truncate_string(label, size), icon)
|
||||
end
|
||||
return vim.fn.printf("%" .. size .. "s ", truncate_string(label, size))
|
||||
end
|
||||
|
||||
---@class (exact) neotree.Component.Common.FileSize : neotree.Component
|
||||
---@field [1] "file_size"?
|
||||
---@field width integer?
|
||||
|
||||
---@param config neotree.Component.Common.FileSize
|
||||
M.file_size = function(config, node, state)
|
||||
-- Root node gets column labels
|
||||
if node:get_depth() == 1 then
|
||||
return {
|
||||
text = get_header(state, "Size", config.width),
|
||||
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 = vim.fn.printf("%" .. config.width .. "s ", truncate_string(text, config.width)),
|
||||
highlight = config.highlight or highlights.FILE_STATS,
|
||||
}
|
||||
end
|
||||
|
||||
---@class (exact) neotree.Component.Common._Time : neotree.Component
|
||||
---@field format neotree.DateFormat
|
||||
---@field width integer?
|
||||
|
||||
---@param config neotree.Component.Common._Time
|
||||
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, config.width),
|
||||
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 utils.date(config.format, seconds) or "-"
|
||||
|
||||
return {
|
||||
text = vim.fn.printf("%" .. config.width .. "s ", truncate_string(display, config.width)),
|
||||
highlight = config.highlight or highlights.FILE_STATS,
|
||||
}
|
||||
end
|
||||
|
||||
---@class (exact) neotree.Component.Common.LastModified : neotree.Component.Common._Time
|
||||
---@field [1] "last_modified"?
|
||||
|
||||
---@param config neotree.Component.Common.LastModified
|
||||
M.last_modified = function(config, node, state)
|
||||
return file_time(config, node, state, "mtime")
|
||||
end
|
||||
|
||||
---@class (exact) neotree.Component.Common.Created : neotree.Component.Common._Time
|
||||
---@field [1] "created"?
|
||||
|
||||
---@param config neotree.Component.Common.Created
|
||||
M.created = function(config, node, state)
|
||||
return file_time(config, node, state, "birthtime")
|
||||
end
|
||||
|
||||
---@class (exact) neotree.Component.Common.SymlinkTarget : neotree.Component
|
||||
---@field [1] "symlink_target"?
|
||||
---@field text_format string?
|
||||
|
||||
---@param config neotree.Component.Common.SymlinkTarget
|
||||
M.symlink_target = function(config, node, _)
|
||||
if node.is_link then
|
||||
return {
|
||||
text = string.format(config.text_format or "-> %s", node.link_to),
|
||||
highlight = config.highlight or highlights.SYMBOLIC_LINK_TARGET,
|
||||
}
|
||||
else
|
||||
return {}
|
||||
end
|
||||
end
|
||||
|
||||
---@class (exact) neotree.Component.Common.Type : neotree.Component
|
||||
---@field [1] "type"?
|
||||
---@field width integer?
|
||||
|
||||
---@param config neotree.Component.Common.Type
|
||||
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", config.width),
|
||||
highlight = highlights.FILE_STATS_HEADER,
|
||||
}
|
||||
end
|
||||
|
||||
return {
|
||||
text = vim.fn.printf("%" .. config.width .. "s ", truncate_string(text, config.width)),
|
||||
highlight = highlights.FILE_STATS,
|
||||
}
|
||||
end
|
||||
|
||||
return M
|
||||
|
|
@ -0,0 +1,339 @@
|
|||
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 strwidth = vim.api.nvim_strwidth
|
||||
local calc_rendered_width = function(rendered_item)
|
||||
local width = 0
|
||||
|
||||
for _, item in ipairs(rendered_item) do
|
||||
if item.text then
|
||||
width = width + strwidth(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 > 0 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
|
||||
repeat
|
||||
if item.enabled == false then
|
||||
break
|
||||
end
|
||||
local required_width = item.required_width or 0
|
||||
if required_width > window_width then
|
||||
break
|
||||
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
|
||||
until true
|
||||
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
|
||||
|
||||
local truncate = utils.truncate_by_cell
|
||||
|
||||
---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_width number The maximum number of characters to return.
|
||||
local truncate_layer_keep_left = function(layer, skip_count, max_width)
|
||||
local result = {}
|
||||
local taken = 0
|
||||
local skipped = 0
|
||||
for _, item in ipairs(layer) do
|
||||
local remaining_to_skip = skip_count - skipped
|
||||
local text_width = strwidth(item.text)
|
||||
if remaining_to_skip > 0 then
|
||||
if text_width <= remaining_to_skip then
|
||||
skipped = skipped + text_width
|
||||
item.text = ""
|
||||
else
|
||||
item.text, text_width = truncate(item.text, text_width - remaining_to_skip, "right")
|
||||
if text_width > max_width - taken then
|
||||
item.text, text_width = truncate(item.text, max_width - taken)
|
||||
end
|
||||
table.insert(result, item)
|
||||
taken = taken + text_width
|
||||
skipped = skipped + remaining_to_skip
|
||||
end
|
||||
elseif taken <= max_width then
|
||||
item.text, text_width = truncate(item.text, max_width - taken)
|
||||
table.insert(result, item)
|
||||
taken = taken + text_width
|
||||
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_width number The maximum number of characters to return.
|
||||
local truncate_layer_keep_right = function(layer, skip_count, max_width)
|
||||
local result = {}
|
||||
local taken = 0
|
||||
local skipped = 0
|
||||
for i = #layer, 1, -1 do
|
||||
local item = layer[i]
|
||||
local text_width = strwidth(item.text)
|
||||
local remaining_to_skip = skip_count - skipped
|
||||
if remaining_to_skip > 0 then
|
||||
if text_width <= remaining_to_skip then
|
||||
skipped = skipped + text_width
|
||||
item.text = ""
|
||||
else
|
||||
item.text, text_width = truncate(item.text, text_width - remaining_to_skip)
|
||||
if text_width > max_width - taken then
|
||||
item.text, text_width = truncate(item.text, max_width - taken, "right")
|
||||
end
|
||||
table.insert(result, item)
|
||||
taken = taken + text_width
|
||||
skipped = skipped + remaining_to_skip
|
||||
end
|
||||
elseif taken <= max_width then
|
||||
if text_width > max_width - taken then
|
||||
item.text, text_width = truncate(item.text, max_width - taken, "right")
|
||||
end
|
||||
table.insert(result, item)
|
||||
taken = taken + text_width
|
||||
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
|
||||
|
||||
---@param config neotree.Component.Common.Container
|
||||
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,341 @@
|
|||
local file_nesting = require("neo-tree.sources.common.file-nesting")
|
||||
local utils = require("neo-tree.utils")
|
||||
local log = require("neo-tree.log")
|
||||
local uv = vim.uv or vim.loop
|
||||
|
||||
---@type neotree.Config.SortFunction
|
||||
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
|
||||
|
||||
---@type neotree.Config.SortFunction
|
||||
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 neotree.Internal.SortFieldProvider a function that takes an item and returns a value to sort by.
|
||||
---@param fallback_sort_function neotree.Config.SortFunction a sort function to use if the field provider returns the same value for both items.
|
||||
---@return neotree.Config.SortFunction
|
||||
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
|
||||
|
||||
---@param func neotree.Config.SortFunction?
|
||||
---@return boolean
|
||||
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
|
||||
|
||||
---@param tbl table
|
||||
---@param sort_func neotree.Config.SortFunction?
|
||||
---@param field_provider neotree.Internal.SortFieldProvider?
|
||||
---@param direction? 1|0
|
||||
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
|
||||
---@cast sort_func -nil
|
||||
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
|
||||
|
||||
---@param state neotree.State
|
||||
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
|
||||
|
||||
---@alias neotree.Filetype
|
||||
---|"file"
|
||||
---|"link"
|
||||
---|"directory"
|
||||
---|"unknown"
|
||||
|
||||
---@class neotree.FileItemFilters
|
||||
---@field never_show boolean?
|
||||
---@field always_show boolean?
|
||||
---@field name boolean?
|
||||
---@field pattern boolean?
|
||||
---@field dotfiles boolean?
|
||||
---@field hidden boolean?
|
||||
---@field gitignored boolean?
|
||||
---@field parent neotree.FileItemFilters?
|
||||
---@field show_gitignored boolean?
|
||||
|
||||
---@class (exact) neotree.FileItemExtra
|
||||
---@field status string? Git status
|
||||
|
||||
---@class (exact) neotree.FileItem
|
||||
---@field id string
|
||||
---@field name string
|
||||
---@field parent_path string?
|
||||
---@field path string
|
||||
---@field type neotree.Filetype|string
|
||||
---@field is_reveal_target boolean
|
||||
---@field contains_reveal_target boolean
|
||||
---@field filtered_by neotree.FileItemFilters?
|
||||
---@field extra neotree.FileItemExtra?
|
||||
---@field status string? Git status
|
||||
---@field is_nested boolean?
|
||||
|
||||
---@class (exact) neotree.FileItem.File : neotree.FileItem
|
||||
---@field children table<string, neotree.FileItem?>?
|
||||
---@field nesting_callback neotree.filenesting.Callback
|
||||
---@field base string
|
||||
---@field ext string
|
||||
---@field exts string
|
||||
---@field name_lcase string
|
||||
|
||||
---@class (exact) neotree.FileItem.Link : neotree.FileItem
|
||||
---@field is_link boolean
|
||||
---@field link_to string?
|
||||
|
||||
---@class (exact) neotree.FileItem.Directory : neotree.FileItem
|
||||
---@field children table<string, neotree.FileItem?>
|
||||
---@field loaded boolean
|
||||
---@field search_pattern string?
|
||||
|
||||
---@param context neotree.FileItemContext
|
||||
---@param path string
|
||||
---@param _type neotree.Filetype?
|
||||
---@param bufnr integer?
|
||||
---@return neotree.FileItem
|
||||
function create_item(context, path, _type, bufnr)
|
||||
local parent_path, name = utils.split_path(utils.normalize_path(path))
|
||||
name = name or ""
|
||||
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 = uv.fs_stat(path)
|
||||
_type = stat and stat.type or "unknown"
|
||||
end
|
||||
local is_reveal_target = (path == context.path_to_reveal)
|
||||
---@type neotree.FileItem
|
||||
local item = {
|
||||
id = id,
|
||||
name = name,
|
||||
parent_path = parent_path,
|
||||
path = path,
|
||||
type = _type,
|
||||
is_reveal_target = is_reveal_target,
|
||||
contains_reveal_target = is_reveal_target and utils.is_subpath(path, context.path_to_reveal),
|
||||
}
|
||||
if utils.is_windows then
|
||||
if vim.fn.getftype(path) == "link" then
|
||||
item.type = "link"
|
||||
end
|
||||
end
|
||||
if item.type == "link" then
|
||||
---@cast item neotree.FileItem.Link
|
||||
item.is_link = true
|
||||
item.link_to = uv.fs_realpath(path)
|
||||
if item.link_to ~= nil then
|
||||
item.type = uv.fs_stat(item.link_to).type
|
||||
end
|
||||
end
|
||||
if item.type == "directory" then
|
||||
---@cast item neotree.FileItem.Directory
|
||||
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
|
||||
---@cast item neotree.FileItem.File
|
||||
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
|
||||
|
||||
local state = assert(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
|
||||
else
|
||||
if utils.is_filtered_by_pattern(f.always_show_by_pattern, path, name) then
|
||||
item.filtered_by = item.filtered_by or {}
|
||||
item.filtered_by.always_show = true
|
||||
end
|
||||
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
|
||||
---@param context neotree.FileItemContext
|
||||
---@param item neotree.FileItem
|
||||
function set_parents(context, item)
|
||||
-- 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
|
||||
---@cast parent neotree.FileItem.Directory
|
||||
context.folders[parent.id] = parent
|
||||
set_parents(context, parent)
|
||||
end
|
||||
table.insert(parent.children, item)
|
||||
context.item_exists[item.id] = true
|
||||
|
||||
if not item.filtered_by and parent.filtered_by then
|
||||
item.filtered_by = {
|
||||
parent = parent.filtered_by,
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
---@class (exact) neotree.FileItemContext
|
||||
---@field state neotree.State?
|
||||
---@field folders table<string, neotree.FileItem.Directory|neotree.FileItem.Link?>
|
||||
---@field nesting neotree.FileItem[]
|
||||
---@field item_exists table<string, boolean?>
|
||||
---@field all_items table<string, neotree.FileItem?>
|
||||
---@field path_to_reveal string?
|
||||
|
||||
---Create context to be used in other file-items functions.
|
||||
---@param state neotree.State? The state of the file-items.
|
||||
---@return neotree.FileItemContext
|
||||
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,324 @@
|
|||
local utils = require("neo-tree.utils")
|
||||
local globtopattern = require("neo-tree.sources.filesystem.lib.globtopattern")
|
||||
local log = require("neo-tree.log")
|
||||
|
||||
-- File nesting a la JetBrains (#117).
|
||||
local M = {}
|
||||
|
||||
---@alias neotree.filenesting.Callback fun(item: table, siblings: table[], rule: neotree.filenesting.Rule): neotree.filenesting.Matches
|
||||
|
||||
---@class neotree.filenesting.Matcher
|
||||
---@field rules table<string, neotree.filenesting.Rule>|neotree.filenesting.Rule[]
|
||||
---@field get_children neotree.filenesting.Callback
|
||||
---@field get_nesting_callback fun(item: table): neotree.filenesting.Callback|nil A callback that returns all the files
|
||||
|
||||
local DEFAULT_PATTERN_PRIORITY = 100
|
||||
---@class neotree.filenesting.Rule
|
||||
---@field priority number? Default is 100. Higher is prioritized.
|
||||
---@field _priority number The internal priority, lower is prioritized. Determined through priority and the key for the rule at setup.
|
||||
|
||||
---@class neotree.filenesting.Rule.Pattern : neotree.filenesting.Rule
|
||||
---@field files string[]
|
||||
---@field files_exact string[]?
|
||||
---@field files_glob string[]?
|
||||
---@field ignore_case boolean? Default is false
|
||||
---@field pattern string
|
||||
|
||||
---@class neotree.filenesting.Matcher.Pattern : neotree.filenesting.Matcher
|
||||
---@field rules neotree.filenesting.Rule.Pattern[]
|
||||
local pattern_matcher = {
|
||||
rules = {},
|
||||
}
|
||||
|
||||
---@class neotree.filenesting.Rule.Extension : neotree.filenesting.Rule
|
||||
---@field [integer] string
|
||||
|
||||
---@class neotree.filenesting.Matcher.Extension : neotree.filenesting.Matcher
|
||||
---@field rules table<string, neotree.filenesting.Rule.Extension>
|
||||
local extension_matcher = {
|
||||
rules = {},
|
||||
}
|
||||
|
||||
local matchers = {
|
||||
pattern = pattern_matcher,
|
||||
exts = extension_matcher,
|
||||
}
|
||||
|
||||
---@class neotree.filenesting.Matches
|
||||
---@field priority number
|
||||
---@field parent table
|
||||
---@field children table[]
|
||||
|
||||
extension_matcher.get_nesting_callback = function(item)
|
||||
local rule = extension_matcher.rules[item.exts]
|
||||
if utils.truthy(rule) then
|
||||
return function(inner_item, siblings)
|
||||
return {
|
||||
parent = inner_item,
|
||||
children = extension_matcher.get_children(inner_item, siblings, rule),
|
||||
priority = rule._priority,
|
||||
}
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
---@type neotree.filenesting.Callback
|
||||
extension_matcher.get_children = function(item, siblings, rule)
|
||||
local matching_files = {}
|
||||
if siblings == nil then
|
||||
return matching_files
|
||||
end
|
||||
for _, ext in pairs(rule) do
|
||||
for _, sibling in pairs(siblings) do
|
||||
if
|
||||
sibling.id ~= item.id
|
||||
and sibling.exts == ext
|
||||
and item.base .. "." .. ext == sibling.name
|
||||
then
|
||||
table.insert(matching_files, sibling)
|
||||
end
|
||||
end
|
||||
end
|
||||
---@type neotree.filenesting.Matches
|
||||
return matching_files
|
||||
end
|
||||
|
||||
pattern_matcher.get_nesting_callback = function(item)
|
||||
---@type neotree.filenesting.Rule.Pattern[]
|
||||
local matching_rules = {}
|
||||
for _, rule in ipairs(pattern_matcher.rules) do
|
||||
if item.name:match(rule.pattern) then
|
||||
table.insert(matching_rules, rule)
|
||||
end
|
||||
end
|
||||
|
||||
if #matching_rules > 0 then
|
||||
return function(inner_item, siblings)
|
||||
local match_set = {}
|
||||
---@type neotree.filenesting.Matches[]
|
||||
local all_item_matches = {}
|
||||
for _, rule in ipairs(matching_rules) do
|
||||
---@type neotree.filenesting.Matches
|
||||
local item_matches = {
|
||||
priority = rule._priority,
|
||||
parent = inner_item,
|
||||
children = {},
|
||||
}
|
||||
local matched_siblings = pattern_matcher.get_children(inner_item, siblings, rule)
|
||||
for _, match in ipairs(matched_siblings) do
|
||||
-- Use file path as key to prevent duplicates
|
||||
if not match_set[match.id] then
|
||||
match_set[match.id] = true
|
||||
table.insert(item_matches.children, match)
|
||||
end
|
||||
end
|
||||
table.insert(all_item_matches, item_matches)
|
||||
end
|
||||
|
||||
return all_item_matches
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
local pattern_matcher_types = {
|
||||
files_glob = {
|
||||
get_pattern = function(pattern)
|
||||
return globtopattern.globtopattern(pattern)
|
||||
end,
|
||||
match = function(filename, pattern)
|
||||
return filename:match(pattern)
|
||||
end,
|
||||
},
|
||||
files_exact = {
|
||||
get_pattern = function(pattern)
|
||||
return pattern
|
||||
end,
|
||||
match = function(filename, pattern)
|
||||
return filename == pattern
|
||||
end,
|
||||
},
|
||||
}
|
||||
|
||||
---@type neotree.filenesting.Callback
|
||||
pattern_matcher.get_children = function(item, siblings, rule)
|
||||
local matching_files = {}
|
||||
if siblings == nil then
|
||||
return matching_files
|
||||
end
|
||||
|
||||
for type, type_functions in pairs(pattern_matcher_types) do
|
||||
for _, pattern in pairs(rule[type] or {}) do
|
||||
repeat
|
||||
---@cast rule neotree.filenesting.Rule.Pattern
|
||||
local item_name = rule.ignore_case and item.name:lower() or item.name
|
||||
|
||||
local success, replaced_pattern = pcall(string.gsub, item_name, rule.pattern, pattern)
|
||||
if not success then
|
||||
log.error("Error using file glob '" .. pattern .. "'; Error: " .. replaced_pattern)
|
||||
break
|
||||
end
|
||||
for _, sibling in pairs(siblings) do
|
||||
if sibling.id ~= item.id then
|
||||
local sibling_name = rule.ignore_case and sibling.name:lower() or sibling.name
|
||||
local glob_or_file = type_functions.get_pattern(replaced_pattern)
|
||||
if type_functions.match(sibling_name, glob_or_file) then
|
||||
table.insert(matching_files, sibling)
|
||||
end
|
||||
end
|
||||
end
|
||||
until true
|
||||
end
|
||||
end
|
||||
return matching_files
|
||||
end
|
||||
|
||||
---@type neotree.filenesting.Matcher[]
|
||||
local enabled_matchers = {}
|
||||
|
||||
function M.is_enabled()
|
||||
return not vim.tbl_isempty(enabled_matchers)
|
||||
end
|
||||
|
||||
function M.nest_items(context)
|
||||
if not M.is_enabled() or vim.tbl_isempty(context.nesting or {}) then
|
||||
return
|
||||
end
|
||||
|
||||
-- First collect all nesting relationships
|
||||
---@type neotree.filenesting.Matches[]
|
||||
local nesting_relationships = {}
|
||||
for _, parent in pairs(context.nesting) do
|
||||
local siblings = context.folders[parent.parent_path].children
|
||||
vim.list_extend(nesting_relationships, parent.nesting_callback(parent, siblings))
|
||||
end
|
||||
|
||||
table.sort(nesting_relationships, function(a, b)
|
||||
if a.priority == b.priority then
|
||||
return a.parent.id < b.parent.id
|
||||
end
|
||||
return a.priority < b.priority
|
||||
end)
|
||||
|
||||
-- Then apply them in order
|
||||
for _, relationship in ipairs(nesting_relationships) do
|
||||
local folder = context.folders[relationship.parent.parent_path]
|
||||
for _, sibling in ipairs(relationship.children) do
|
||||
if not sibling.is_nested then
|
||||
table.insert(relationship.parent.children, sibling)
|
||||
sibling.is_nested = true
|
||||
sibling.nesting_parent = relationship.parent
|
||||
|
||||
if folder ~= nil then
|
||||
for index, file_to_check in ipairs(folder.children) do
|
||||
if file_to_check.id == sibling.id then
|
||||
table.remove(folder.children, index)
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function M.get_nesting_callback(item)
|
||||
local cbs = {}
|
||||
for _, matcher in ipairs(enabled_matchers) do
|
||||
local callback = matcher.get_nesting_callback(item)
|
||||
if callback ~= nil then
|
||||
table.insert(cbs, callback)
|
||||
end
|
||||
end
|
||||
if #cbs <= 1 then
|
||||
return cbs[1]
|
||||
else
|
||||
return function(...)
|
||||
local res = {}
|
||||
for _, cb in ipairs(cbs) do
|
||||
vim.list_extend(res, cb(...))
|
||||
end
|
||||
return res
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function is_glob(str)
|
||||
local test = str:gsub("\\[%*%?%[%]]", "")
|
||||
local pos, _ = test:find("*")
|
||||
return pos ~= nil
|
||||
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
|
||||
|
||||
---Setup the module with the given config
|
||||
---@param config table<string, neotree.filenesting.Rule>
|
||||
function M.setup(config)
|
||||
config = config or {}
|
||||
enabled_matchers = {}
|
||||
local real_priority = 0
|
||||
for _, m in pairs(matchers) do
|
||||
m.rules = {}
|
||||
end
|
||||
|
||||
for key, rule in
|
||||
utils.spairs(config, function(a, b)
|
||||
-- Organize by priority (descending) or by key (ascending)
|
||||
local a_prio = config[a].priority or DEFAULT_PATTERN_PRIORITY
|
||||
local b_prio = config[b].priority or DEFAULT_PATTERN_PRIORITY
|
||||
if a_prio == b_prio then
|
||||
return a < b
|
||||
end
|
||||
return a_prio > b_prio
|
||||
end)
|
||||
do
|
||||
rule.priority = rule.priority or DEFAULT_PATTERN_PRIORITY
|
||||
rule._priority = real_priority
|
||||
real_priority = real_priority + 1
|
||||
if rule.pattern then
|
||||
---@cast rule neotree.filenesting.Rule.Pattern
|
||||
rule.ignore_case = rule.ignore_case or false
|
||||
if rule.ignore_case then
|
||||
rule.pattern = case_insensitive_pattern(rule.pattern)
|
||||
end
|
||||
rule.files_glob = {}
|
||||
rule.files_exact = {}
|
||||
for _, glob in pairs(rule.files) do
|
||||
if rule.ignore_case then
|
||||
glob = glob:lower()
|
||||
end
|
||||
local replaced = glob:gsub("%%%d+", "")
|
||||
if is_glob(replaced) then
|
||||
table.insert(rule.files_glob, glob)
|
||||
else
|
||||
table.insert(rule.files_exact, glob)
|
||||
end
|
||||
end
|
||||
-- priority does matter for pattern.rules
|
||||
table.insert(matchers.pattern.rules, rule)
|
||||
else
|
||||
---@cast rule neotree.filenesting.Rule.Extension
|
||||
matchers.exts.rules[key] = rule
|
||||
end
|
||||
end
|
||||
|
||||
enabled_matchers = vim.tbl_filter(function(m)
|
||||
return not vim.tbl_isempty(m.rules)
|
||||
end, matchers)
|
||||
end
|
||||
|
||||
return M
|
||||
|
|
@ -0,0 +1,249 @@
|
|||
-- 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
|
||||
|
||||
---@type integer?
|
||||
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,355 @@
|
|||
---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 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 compat = require("neo-tree.utils._compat")
|
||||
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 = {}
|
||||
|
||||
---Reset the current filter to the empty string.
|
||||
---@param state neotree.State
|
||||
---@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, compat.noref())
|
||||
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 neotree.State 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
|
||||
if state.config.title then
|
||||
popup_msg = state.config.title
|
||||
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)
|
||||
|
||||
---@alias neotree.FuzzyFinder.BuiltinCommandNames
|
||||
---|"move_cursor_down"
|
||||
---|"move_cursor_up"
|
||||
---|"close"
|
||||
---|"close_clear_filter"
|
||||
---|"close_keep_filter"
|
||||
---|neotree.FuzzyFinder.FalsyMappingNames
|
||||
|
||||
---@alias neotree.FuzzyFinder.CommandFunction fun(state: neotree.State, scroll_padding: integer):string?
|
||||
|
||||
---@class neotree.FuzzyFinder.BuiltinCommands
|
||||
---@field [string] neotree.FuzzyFinder.CommandFunction?
|
||||
local cmds
|
||||
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,
|
||||
|
||||
close = function(_state)
|
||||
vim.cmd("stopinsert")
|
||||
input:unmount()
|
||||
if utils.truthy(_state.search_pattern) then
|
||||
reset_filter(_state, true)
|
||||
end
|
||||
restore_height()
|
||||
end,
|
||||
|
||||
close_keep_filter = function(_state, _scroll_padding)
|
||||
log.info("Persisting the search filter")
|
||||
keep_filter_on_submit = true
|
||||
cmds.close(_state, _scroll_padding)
|
||||
end,
|
||||
close_clear_filter = function(_state, _scroll_padding)
|
||||
log.info("Clearing the search filter")
|
||||
keep_filter_on_submit = false
|
||||
cmds.close(_state, _scroll_padding)
|
||||
end,
|
||||
}
|
||||
|
||||
M.setup_hooks(input, cmds, state, scroll_padding)
|
||||
M.setup_mappings(input, cmds, state, scroll_padding)
|
||||
end
|
||||
|
||||
---@param input NuiInput
|
||||
---@param cmds neotree.FuzzyFinder.BuiltinCommands
|
||||
---@param state neotree.State
|
||||
---@param scroll_padding integer
|
||||
function M.setup_hooks(input, cmds, state, scroll_padding)
|
||||
input:on(
|
||||
{ event.BufLeave, event.BufDelete },
|
||||
utils.wrap(cmds.close, state, scroll_padding),
|
||||
{ once = true }
|
||||
)
|
||||
|
||||
-- hacky bugfix for quitting from the filter window
|
||||
input:on("QuitPre", function()
|
||||
if vim.api.nvim_get_current_win() ~= input.winid then
|
||||
return
|
||||
end
|
||||
---'confirm' can cause blocking user input on exit, so this hack disables it.
|
||||
local old_confirm = vim.o.confirm
|
||||
vim.o.confirm = false
|
||||
vim.schedule(function()
|
||||
vim.o.confirm = old_confirm
|
||||
end)
|
||||
end)
|
||||
end
|
||||
|
||||
---@enum neotree.FuzzyFinder.FalsyMappingNames
|
||||
M._falsy_mapping_names = { "noop", "none" }
|
||||
|
||||
---@alias neotree.FuzzyFinder.CommandOrName neotree.FuzzyFinder.CommandFunction|neotree.FuzzyFinder.BuiltinCommandNames
|
||||
|
||||
---@class neotree.FuzzyFinder.VerboseCommand
|
||||
---@field [1] neotree.FuzzyFinder.Command
|
||||
---@field [2] vim.keymap.set.Opts?
|
||||
---@field raw boolean?
|
||||
|
||||
---@alias neotree.FuzzyFinder.Command neotree.FuzzyFinder.CommandOrName|neotree.FuzzyFinder.VerboseCommand|string
|
||||
|
||||
---@class neotree.FuzzyFinder.SimpleMappings : neotree.SimpleMappings
|
||||
---@field [string] neotree.FuzzyFinder.Command?
|
||||
|
||||
---@class neotree.Config.FuzzyFinder.Mappings : neotree.FuzzyFinder.SimpleMappings, neotree.Mappings
|
||||
---@field [integer] table<string, neotree.FuzzyFinder.SimpleMappings>
|
||||
|
||||
---@param input NuiInput
|
||||
---@param cmds neotree.FuzzyFinder.BuiltinCommands
|
||||
---@param state neotree.State
|
||||
---@param scroll_padding integer
|
||||
---@param mappings neotree.FuzzyFinder.SimpleMappings
|
||||
---@param mode string
|
||||
local function apply_simple_mappings(input, cmds, state, scroll_padding, mode, mappings)
|
||||
---@param command neotree.FuzzyFinder.CommandFunction
|
||||
---@return function
|
||||
local function setup_command(command)
|
||||
return utils.wrap(command, state, scroll_padding)
|
||||
end
|
||||
for lhs, rhs in pairs(mappings) do
|
||||
if type(lhs) == "string" then
|
||||
---@cast rhs neotree.FuzzyFinder.Command
|
||||
local cmd, raw, opts
|
||||
if type(rhs) == "table" then
|
||||
---type doesn't narrow properly
|
||||
---@cast rhs -neotree.FuzzyFinder.FalsyMappingNames
|
||||
raw = rhs.raw
|
||||
opts = vim.deepcopy(rhs)
|
||||
opts[1] = nil
|
||||
opts.raw = nil
|
||||
cmd = rhs[1]
|
||||
else
|
||||
---type also doesn't narrow properly
|
||||
---@cast rhs -neotree.FuzzyFinder.VerboseCommand
|
||||
cmd = rhs
|
||||
end
|
||||
|
||||
local cmdtype = type(cmd)
|
||||
if cmdtype == "string" then
|
||||
if raw then
|
||||
input:map(mode, lhs, cmd, opts)
|
||||
else
|
||||
local command = cmds[cmd]
|
||||
if command then
|
||||
input:map(mode, lhs, setup_command(command), opts)
|
||||
elseif not vim.tbl_contains(M._falsy_mapping_names, cmd) then
|
||||
log.warn(
|
||||
string.format("Invalid command in fuzzy_finder_mappings: ['%s'] = '%s'", lhs, cmd)
|
||||
)
|
||||
end
|
||||
end
|
||||
elseif cmdtype == "function" then
|
||||
---@cast cmd -neotree.FuzzyFinder.VerboseCommand
|
||||
input:map(mode, lhs, setup_command(cmd), opts)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---@param input NuiInput
|
||||
---@param cmds neotree.FuzzyFinder.BuiltinCommands
|
||||
---@param state neotree.State
|
||||
---@param scroll_padding integer
|
||||
function M.setup_mappings(input, cmds, state, scroll_padding)
|
||||
local config = require("neo-tree").config
|
||||
|
||||
local ff_mappings = config.filesystem.window.fuzzy_finder_mappings or {}
|
||||
apply_simple_mappings(input, cmds, state, scroll_padding, "i", ff_mappings)
|
||||
|
||||
for _, mappings_by_mode in ipairs(ff_mappings) do
|
||||
for mode, mappings in pairs(mappings_by_mode) do
|
||||
apply_simple_mappings(input, cmds, state, scroll_padding, mode, mappings)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return M
|
||||
|
|
@ -0,0 +1,172 @@
|
|||
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 = {}
|
||||
|
||||
---@param text string
|
||||
---@param highlight string?
|
||||
local add_text = function(text, highlight)
|
||||
local line = NuiLine()
|
||||
line:append(text, highlight)
|
||||
return line
|
||||
end
|
||||
|
||||
---@param state neotree.State
|
||||
---@param prefix_key string?
|
||||
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
|
||||
|
||||
---@param key string
|
||||
---@param prefix string?
|
||||
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 neotree.State 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
|
||||
---@type neotree.State.ResolvedMapping
|
||||
local value = state.resolved_mappings[key]
|
||||
or { text = "<error mapping for key " .. key .. ">", handler = function() end }
|
||||
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)
|
||||
local col
|
||||
if state.current_position == "right" then
|
||||
col = vim.o.columns - tree_width - width - 1
|
||||
else
|
||||
col = tree_width - 1
|
||||
end
|
||||
|
||||
---@type nui_popup_options
|
||||
local options = {
|
||||
position = {
|
||||
row = 2,
|
||||
col = col,
|
||||
},
|
||||
size = {
|
||||
width = width,
|
||||
height = #keys + 5,
|
||||
},
|
||||
enter = true,
|
||||
focusable = true,
|
||||
zindex = 50,
|
||||
relative = "editor",
|
||||
win_options = {
|
||||
foldenable = false, -- Prevent folds from hiding lines
|
||||
},
|
||||
}
|
||||
|
||||
---@return integer lines The number of screen lines that the popup should occupy at most
|
||||
local popup_max_height = function()
|
||||
-- statusline
|
||||
local statusline_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
|
||||
statusline_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 vim.o.lines - vim.o.cmdheight - statusline_lines - tab_lines - 2
|
||||
end
|
||||
local max_height = popup_max_height()
|
||||
if options.size.height > max_height then
|
||||
options.size.height = max_height
|
||||
end
|
||||
|
||||
title = title or "Neotree Help"
|
||||
options = popups.popup_options(title, width, options)
|
||||
local popup = Popup(options)
|
||||
popup:mount()
|
||||
|
||||
local event = require("nui.utils.autocmd").event
|
||||
popup:on({ event.VimResized }, function()
|
||||
popup:update_layout({
|
||||
size = {
|
||||
height = math.min(options.size.height --[[@as integer]], popup_max_height()),
|
||||
width = math.min(options.size.width --[[@as integer]], vim.o.columns - 2),
|
||||
},
|
||||
})
|
||||
end)
|
||||
popup:on({ event.BufLeave, event.BufDelete }, function()
|
||||
popup:unmount()
|
||||
end, { once = true })
|
||||
|
||||
popup:map("n", "<esc>", function()
|
||||
popup:unmount()
|
||||
end, { noremap = 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]
|
||||
or { text = "<error mapping for key " .. key .. ">", handler = function() end }
|
||||
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,49 @@
|
|||
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 not state or not state.tree then
|
||||
return
|
||||
end
|
||||
local node = state.tree:get_node()
|
||||
if not node then
|
||||
return
|
||||
end
|
||||
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,85 @@
|
|||
local log = require("neo-tree.log")
|
||||
local utils = require("neo-tree.utils")
|
||||
|
||||
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 neotree.State 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_nodes[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 utils.is_expandable(child) then
|
||||
rec(child, to_load)
|
||||
else
|
||||
log.trace("Child: " .. (child.name or "") .. " is not expandable, 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 neotree.State 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 neotree.State 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())
|
||||
prefetcher = prefetcher or M.default_prefetcher
|
||||
if not utils.is_expandable(node) then
|
||||
return
|
||||
end
|
||||
|
||||
state.explicitly_opened_nodes = state.explicitly_opened_nodes or {}
|
||||
if prefetcher.should_prefetch(node) then
|
||||
local id = node:get_id()
|
||||
state.explicitly_opened_nodes[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,572 @@
|
|||
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 NuiPopup = require("nui.popup")
|
||||
|
||||
---@class neotree.Preview.Config
|
||||
---@field use_float boolean?
|
||||
---@field use_image_nvim boolean?
|
||||
---@field use_snacks_image boolean?
|
||||
|
||||
---@class neotree.Preview.Event
|
||||
---@field source string?
|
||||
---@field event neotree.event.Handler
|
||||
|
||||
---@class neotree.Preview
|
||||
---@field config neotree.Preview.Config?
|
||||
---@field active boolean Whether the preview is active.
|
||||
---@field winid integer The id of the window being used to preview.
|
||||
---@field is_neo_tree_window boolean Whether the preview window belongs to neo-tree.
|
||||
---@field bufnr number The buffer that is currently in the preview window.
|
||||
---@field start_pos integer[]? An array-like table specifying the (0-indexed) starting position of the previewed text.
|
||||
---@field end_pos integer[]? An array-like table specifying the (0-indexed) ending position of the preview text.
|
||||
---@field truth table A table containing information to be restored when the preview ends.
|
||||
---@field events neotree.Preview.Event[] A list of events the preview is subscribed to.
|
||||
local Preview = {}
|
||||
|
||||
---@type neotree.Preview?
|
||||
local instance = nil
|
||||
|
||||
local neo_tree_preview_namespace = vim.api.nvim_create_namespace("neo_tree_preview")
|
||||
|
||||
---@param state neotree.State
|
||||
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 title = state.config.title or "Neo-tree Preview"
|
||||
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]
|
||||
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
|
||||
|
||||
if height < 5 or width < 5 then
|
||||
log.error(
|
||||
"Preview cannot be used without any space, please resize the neo-tree split to allow for at least 5 cells of free space."
|
||||
)
|
||||
return
|
||||
end
|
||||
local popups = require("neo-tree.ui.popups")
|
||||
local options = popups.popup_options(title, 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 win = NuiPopup(options)
|
||||
win:mount()
|
||||
return win
|
||||
end
|
||||
|
||||
---Creates a new preview.
|
||||
---@param state neotree.State The state of the source.
|
||||
---@return neotree.Preview preview A new preview. A preview is a table consisting of the following keys:
|
||||
--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 integer? The number of the buffer to be previewed.
|
||||
---@param start_pos integer[]? The (0-indexed) starting position of the previewed text. May be absent.
|
||||
---@param end_pos integer[]? 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
|
||||
|
||||
self:setBuffer(bufnr)
|
||||
|
||||
self.start_pos = start_pos
|
||||
self.end_pos = end_pos
|
||||
|
||||
self:reveal()
|
||||
self:highlight_preview_range()
|
||||
end
|
||||
|
||||
---Reverts the preview and inactivates it, restoring the preview window to its previous state.
|
||||
function Preview:revert()
|
||||
self.active = false
|
||||
self:unsubscribe()
|
||||
|
||||
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.wo[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)
|
||||
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.bo[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 neotree.event.Handler 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 neotree.State 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.bufnr = vim.api.nvim_create_buf(false, true)
|
||||
self.truth = {}
|
||||
else
|
||||
self.truth = {
|
||||
bufnr = self.bufnr,
|
||||
view = vim.api.nvim_win_call(self.winid, vim.fn.winsaveview),
|
||||
options = {
|
||||
bufhidden = vim.bo[self.bufnr].bufhidden,
|
||||
foldenable = vim.wo[self.winid].foldenable,
|
||||
},
|
||||
}
|
||||
vim.bo[self.bufnr].bufhidden = "hide"
|
||||
vim.wo[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
|
||||
---@return boolean hijacked Whether the buffer was successfully hijacked.
|
||||
local function try_load_image_nvim_buf(winid, bufnr)
|
||||
-- notify only image.nvim to let it try and hijack
|
||||
local image_augroup = vim.api.nvim_create_augroup("image.nvim", { clear = false })
|
||||
if #vim.api.nvim_get_autocmds({ group = image_augroup }) == 0 then
|
||||
local image_available, image = pcall(require, "image")
|
||||
if not image_available then
|
||||
local image_nvim_url = "https://github.com/3rd/image.nvim"
|
||||
log.debug(
|
||||
"use_image_nvim was set but image.nvim was not found. Install from: " .. image_nvim_url
|
||||
)
|
||||
return false
|
||||
end
|
||||
log.warn("image.nvim was not setup. Calling require('image').setup().")
|
||||
image.setup()
|
||||
end
|
||||
|
||||
vim.opt.eventignore:remove("BufWinEnter")
|
||||
local ok = pcall(vim.api.nvim_win_call, winid, function()
|
||||
vim.api.nvim_exec_autocmds("BufWinEnter", { group = image_augroup, buffer = bufnr })
|
||||
end)
|
||||
vim.opt.eventignore:append("BufWinEnter")
|
||||
if not ok then
|
||||
log.debug("image.nvim doesn't have any file patterns to hijack.")
|
||||
return false
|
||||
end
|
||||
if vim.bo[bufnr].filetype ~= "image_nvim" then
|
||||
return false
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
---@param bufnr number The buffer number of the buffer to set.
|
||||
---@return number bytecount The number of bytes in the buffer
|
||||
local get_bufsize = function(bufnr)
|
||||
return vim.api.nvim_buf_call(bufnr, function()
|
||||
return vim.fn.line2byte(vim.fn.line("$") + 1)
|
||||
end)
|
||||
end
|
||||
|
||||
events.subscribe({
|
||||
event = events.NEO_TREE_PREVIEW_BEFORE_RENDER,
|
||||
---@param args neotree.event.args.PREVIEW_BEFORE_RENDER
|
||||
handler = function(args)
|
||||
local preview = args.preview
|
||||
local bufnr = args.bufnr
|
||||
|
||||
if not preview.config.use_snacks_image then
|
||||
return
|
||||
end
|
||||
-- check if snacks.image is available
|
||||
local snacks_image_ok, image = pcall(require, "snacks.image")
|
||||
if not snacks_image_ok then
|
||||
local snacks_nvim_url = "https://github.com/folke/snacks.nvim"
|
||||
log.debug(
|
||||
"use_snacks_image was set but snacks.nvim was not found. Install from: " .. snacks_nvim_url
|
||||
)
|
||||
return
|
||||
end
|
||||
local bufname = vim.api.nvim_buf_get_name(bufnr)
|
||||
-- try attaching it
|
||||
if image.supports(bufname) then
|
||||
image.placement.new(preview.bufnr, bufname)
|
||||
vim.bo[preview.bufnr].modifiable = true
|
||||
return { handled = true } -- let snacks.image handle the rest
|
||||
end
|
||||
end,
|
||||
})
|
||||
events.subscribe({
|
||||
event = events.NEO_TREE_PREVIEW_BEFORE_RENDER,
|
||||
---@param args neotree.event.args.PREVIEW_BEFORE_RENDER
|
||||
handler = function(args)
|
||||
local preview = args.preview
|
||||
local bufnr = args.bufnr
|
||||
|
||||
if preview.config.use_image_nvim and try_load_image_nvim_buf(preview.winid, bufnr) then
|
||||
-- calling the try method twice should be okay here, image.nvim should cache the image and displaying the image takes
|
||||
-- really long anyways
|
||||
vim.api.nvim_win_set_buf(preview.winid, bufnr)
|
||||
return { handled = try_load_image_nvim_buf(preview.winid, bufnr) }
|
||||
end
|
||||
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)
|
||||
self:clearHighlight()
|
||||
if bufnr == self.bufnr then
|
||||
return
|
||||
end
|
||||
local eventignore = vim.opt.eventignore
|
||||
vim.opt.eventignore:append("BufEnter,BufWinEnter")
|
||||
|
||||
repeat
|
||||
---@class neotree.event.args.PREVIEW_BEFORE_RENDER
|
||||
local args = {
|
||||
preview = self,
|
||||
bufnr = bufnr,
|
||||
}
|
||||
events.fire_event(events.NEO_TREE_PREVIEW_BEFORE_RENDER, args)
|
||||
|
||||
if self.config.use_float then
|
||||
-- Workaround until https://github.com/neovim/neovim/issues/24973 is resolved or maybe 'previewpopup' comes in?
|
||||
vim.fn.bufload(bufnr)
|
||||
local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
|
||||
vim.api.nvim_buf_set_lines(self.bufnr, 0, -1, false, lines)
|
||||
vim.api.nvim_win_set_buf(self.winid, self.bufnr)
|
||||
-- I'm not sure why float windows won't show numbers without this
|
||||
vim.wo[self.winid].number = true
|
||||
|
||||
-- code below is from mini.pick
|
||||
-- only starts treesitter parser if the filetype is matching
|
||||
local ft = vim.bo[bufnr].filetype
|
||||
local bufsize = get_bufsize(bufnr)
|
||||
if bufsize > 1024 * 1024 or bufsize > 1000 * #lines then
|
||||
break -- goto end
|
||||
end
|
||||
local has_lang, lang = pcall(vim.treesitter.language.get_lang, ft)
|
||||
lang = has_lang and lang or ft
|
||||
local has_parser, parser =
|
||||
pcall(vim.treesitter.get_parser, self.bufnr, lang, { error = false })
|
||||
has_parser = has_parser and parser ~= nil
|
||||
if has_parser then
|
||||
has_parser = pcall(vim.treesitter.start, self.bufnr, lang)
|
||||
end
|
||||
if not has_parser then
|
||||
vim.bo[self.bufnr].syntax = ft
|
||||
end
|
||||
else
|
||||
vim.api.nvim_win_set_buf(self.winid, bufnr)
|
||||
self.bufnr = bufnr
|
||||
end
|
||||
|
||||
until true
|
||||
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_preview_range()
|
||||
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
|
||||
end
|
||||
|
||||
if not start_pos then
|
||||
---@cast end_pos table
|
||||
start_pos = end_pos
|
||||
elseif not end_pos then
|
||||
---@cast start_pos table
|
||||
end_pos = start_pos
|
||||
end
|
||||
|
||||
local start_line, end_line = start_pos[1], end_pos[1]
|
||||
local start_col, end_col = start_pos[2], end_pos[2]
|
||||
vim.api.nvim_buf_set_extmark(self.bufnr, neo_tree_preview_namespace, start_line, start_col, {
|
||||
hl_group = highlights.PREVIEW,
|
||||
end_row = end_line,
|
||||
end_col = end_col,
|
||||
-- priority = priority,
|
||||
strict = false,
|
||||
})
|
||||
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
|
||||
|
||||
---@param state neotree.State
|
||||
Preview.show = function(state)
|
||||
local node = assert(state.tree:get_node())
|
||||
|
||||
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
|
||||
|
||||
---@param state neotree.State
|
||||
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
|
||||
---@cast instance table
|
||||
vim.fn.win_gotoid(instance.winid)
|
||||
end
|
||||
end
|
||||
|
||||
local CTRL_E = utils.keycode("<c-e>")
|
||||
local CTRL_Y = utils.keycode("<c-y>")
|
||||
---@param state neotree.State
|
||||
Preview.scroll = function(state)
|
||||
local direction = state.config.direction
|
||||
local input = direction < 0 and CTRL_E or CTRL_Y
|
||||
local count = math.abs(direction)
|
||||
|
||||
if Preview:is_active() then
|
||||
---@cast instance table
|
||||
vim.api.nvim_win_call(instance.winid, function()
|
||||
vim.cmd(("normal! %s%s"):format(count, input))
|
||||
end)
|
||||
else
|
||||
vim.api.nvim_win_call(state.winid, function()
|
||||
vim.api.nvim_feedkeys(state.fallback, "n", false)
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
return Preview
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
--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")
|
||||
|
||||
---@class neotree.sources.DocumentSymbols.Commands : neotree.sources.Common.Commands
|
||||
---@field [string] neotree.TreeCommand
|
||||
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
|
||||
|
||||
---@param node NuiTree.Node
|
||||
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 = assert(state.tree:get_node())
|
||||
if node:get_depth() == 1 then
|
||||
return
|
||||
end
|
||||
local old_name = node.name
|
||||
|
||||
---@param new_name string?
|
||||
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,66 @@
|
|||
-- 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")
|
||||
|
||||
---@alias neotree.Component.DocumentSymbols._Key
|
||||
---|"kind_icon"
|
||||
---|"kind_name"
|
||||
---|"name"
|
||||
|
||||
---@class neotree.Component.DocumentSymbols Use the neotree.Component.DocumentSymbols.* types to get more specific types.
|
||||
---@field [1] neotree.Component.DocumentSymbols._Key|neotree.Component.Common._Key
|
||||
|
||||
---@type table<neotree.Component.DocumentSymbols._Key, neotree.Renderer>
|
||||
local M = {}
|
||||
|
||||
---@class (exact) neotree.Component.DocumentSymbols.KindIcon : neotree.Component
|
||||
---@field [1] "kind_icon"?
|
||||
---@field provider neotree.IconProvider?
|
||||
|
||||
---@param config neotree.Component.DocumentSymbols.KindIcon
|
||||
M.kind_icon = function(config, node, state)
|
||||
local icon = {
|
||||
text = node:get_depth() == 1 and "" or node.extra.kind.icon,
|
||||
highlight = node.extra.kind.hl,
|
||||
}
|
||||
|
||||
if config.provider then
|
||||
icon = config.provider(icon, node, state) or icon
|
||||
end
|
||||
|
||||
return icon
|
||||
end
|
||||
|
||||
---@class (exact) neotree.Component.DocumentSymbols.KindName : neotree.Component
|
||||
---@field [1] "kind_name"?
|
||||
|
||||
---@param config neotree.Component.DocumentSymbols.KindName
|
||||
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
|
||||
|
||||
---@class (exact) neotree.Component.DocumentSymbols.Name : neotree.Component.Common.Name
|
||||
|
||||
---@param config neotree.Component.DocumentSymbols.Name
|
||||
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,129 @@
|
|||
--This file should have all functions that are in the public api and either set
|
||||
--or read the state of this source.
|
||||
|
||||
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")
|
||||
|
||||
---@class neotree.sources.DocumentSymbols : neotree.Source
|
||||
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
|
||||
|
||||
---@class neotree.sources.documentsymbols.DebounceArgs
|
||||
|
||||
---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
|
||||
|
||||
---@class neotree.Config.LspKindDisplay
|
||||
---@field icon string
|
||||
---@field hl string
|
||||
|
||||
---@class neotree.Config.DocumentSymbols.Renderers : neotree.Config.Renderers
|
||||
---@field root neotree.Component.DocumentSymbols[]?
|
||||
---@field symbol neotree.Component.DocumentSymbols[]?
|
||||
|
||||
---@class (exact) neotree.Config.DocumentSymbols : neotree.Config.Source
|
||||
---@field follow_cursor boolean?
|
||||
---@field client_filters neotree.lsp.ClientFilter?
|
||||
---@field custom_kinds table<integer, string>?
|
||||
---@field kinds table<string, neotree.Config.LspKindDisplay>?
|
||||
---@field renderers neotree.Config.DocumentSymbols.Renderers?
|
||||
|
||||
---Configures the plugin, should be called before the plugin is used.
|
||||
---@param config neotree.Config.DocumentSymbols
|
||||
---@param global_config neotree.Config.Base
|
||||
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,97 @@
|
|||
---Utilities function to filter the LSP servers
|
||||
local utils = require("neo-tree.utils")
|
||||
|
||||
---@class neotree.lsp.RespRaw
|
||||
---@field err lsp.ResponseError?
|
||||
---@field error lsp.ResponseError?
|
||||
---@field result any
|
||||
|
||||
local M = {}
|
||||
|
||||
---@alias neotree.lsp.Filter fun(client_name: string): boolean
|
||||
|
||||
---Filter clients
|
||||
---@param filter_type "first" | "all"
|
||||
---@param filter_fn neotree.lsp.Filter?
|
||||
---@param resp table<integer, neotree.lsp.RespRaw>
|
||||
---@return table<string, any>
|
||||
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 neotree.lsp.Filter
|
||||
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 neotree.lsp.Filter
|
||||
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 table<integer, neotree.lsp.RespRaw>
|
||||
---@return table<string, any>
|
||||
M.filter_resp = function(resp)
|
||||
return {}
|
||||
end
|
||||
|
||||
---@alias neotree.lsp.Filter.Type
|
||||
---|"first" # Allow the first that matches
|
||||
---|"all" # Allow all that match
|
||||
|
||||
---@alias neotree.lsp.ClientFilter neotree.lsp.Filter.Type | { type: neotree.lsp.Filter.Type, fn: neotree.lsp.Filter, allow_only: string[], ignore: string[] }
|
||||
---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 neotree.lsp.ClientFilter
|
||||
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,67 @@
|
|||
---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 = {}
|
||||
|
||||
---@class neotree.LspKindDisplay
|
||||
---@field name string Display name
|
||||
---@field icon string Icon to render
|
||||
---@field hl string Highlight for the node
|
||||
|
||||
---Get how the kind with kind_id should be rendered
|
||||
---@param kind_id integer the kind_id to be render
|
||||
---@return neotree.LspKindDisplay res
|
||||
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,211 @@
|
|||
---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 neotree.SymbolExtra
|
||||
---@field bufnr integer the buffer containing the symbols,
|
||||
---@field kind neotree.LspKindDisplay 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 neotree.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 neotree.SymbolNode[]
|
||||
---@field extra neotree.SymbolExtra additional info
|
||||
|
||||
---Parse the lsp.Range
|
||||
---@param range lsp.Range the lsp.Range 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 lsp.DocumentSymbol|lsp.SymbolInformation the LSP response node
|
||||
---@param id string the id of the current node
|
||||
---@return neotree.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
|
||||
|
||||
---@type neotree.SymbolNode
|
||||
local symbol_node = {
|
||||
id = id,
|
||||
name = resp_node.name,
|
||||
type = "symbol",
|
||||
path = state.path,
|
||||
children = children,
|
||||
---@diagnostic disable-next-line: missing-fields
|
||||
extra = {
|
||||
bufnr = state.lsp_bufnr,
|
||||
kind = kinds.get_kind(resp_node.kind),
|
||||
search_path = search_path,
|
||||
-- detail = resp_node.detail,
|
||||
},
|
||||
}
|
||||
local preview_range = resp_node.range
|
||||
if preview_range then
|
||||
---@cast resp_node lsp.DocumentSymbol
|
||||
symbol_node.extra.selection_range = parse_range(resp_node.selectionRange)
|
||||
else
|
||||
---@cast resp_node lsp.SymbolInformation
|
||||
preview_range = resp_node.location.range
|
||||
symbol_node.extra.selection_range = parse_range(preview_range)
|
||||
end
|
||||
|
||||
preview_range = parse_range(preview_range)
|
||||
symbol_node.extra.position = preview_range.start
|
||||
symbol_node.extra.end_position = preview_range["end"]
|
||||
return symbol_node
|
||||
end
|
||||
|
||||
---Callback function for lsp request
|
||||
---@param lsp_resp table<integer, neotree.lsp.RespRaw> the response of the lsp clients
|
||||
---@param state neotree.State 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 = assert(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
|
||||
|
||||
---latter is deprecated in neovim v0.11
|
||||
---@diagnostic disable-next-line: deprecated
|
||||
local get_clients = vim.lsp.get_clients or vim.lsp.get_active_clients
|
||||
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(get_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,284 @@
|
|||
--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 uv = vim.uv or vim.loop
|
||||
|
||||
---@class neotree.sources.Filesystem.Commands : neotree.sources.Common.Commands
|
||||
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
|
||||
|
||||
---@type neotree.TreeCommandVisual
|
||||
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
|
||||
|
||||
---@type neotree.TreeCommandVisual
|
||||
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
|
||||
|
||||
---@type neotree.TreeCommandVisual
|
||||
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)
|
||||
cc.expand_all_nodes(state, node, fs.prefetcher)
|
||||
end
|
||||
|
||||
M.expand_all_subnodes = function(state, node)
|
||||
cc.expand_all_subnodes(state, node, fs.prefetcher)
|
||||
end
|
||||
|
||||
---Shows the filter input, which will filter the tree.
|
||||
---@param state neotree.sources.filesystem.State
|
||||
M.filter_as_you_type = function(state)
|
||||
local config = state.config or {}
|
||||
filter.show_filter(state, true, false, false, config.keep_filter_on_submit or false)
|
||||
end
|
||||
|
||||
---Shows the filter input, which will filter the tree.
|
||||
---@param state neotree.sources.filesystem.State
|
||||
M.filter_on_submit = function(state)
|
||||
filter.show_filter(state, false, false, false, true)
|
||||
end
|
||||
|
||||
---Shows the filter input in fuzzy finder mode.
|
||||
---@param state neotree.sources.filesystem.State
|
||||
M.fuzzy_finder = function(state)
|
||||
local config = state.config or {}
|
||||
filter.show_filter(state, true, true, false, config.keep_filter_on_submit or false)
|
||||
end
|
||||
|
||||
---Shows the filter input in fuzzy finder mode.
|
||||
---@param state neotree.sources.filesystem.State
|
||||
M.fuzzy_finder_directory = function(state)
|
||||
local config = state.config or {}
|
||||
filter.show_filter(state, true, "directory", false, config.keep_filter_on_submit or false)
|
||||
end
|
||||
|
||||
---Shows the filter input in fuzzy sorter
|
||||
---@param state neotree.sources.filesystem.State
|
||||
M.fuzzy_sorter = function(state)
|
||||
local config = state.config or {}
|
||||
filter.show_filter(state, true, true, true, config.keep_filter_on_submit or false)
|
||||
end
|
||||
|
||||
---Shows the filter input in fuzzy sorter with only directories
|
||||
---@param state neotree.sources.filesystem.State
|
||||
M.fuzzy_sorter_directory = function(state)
|
||||
local config = state.config or {}
|
||||
filter.show_filter(state, true, "directory", true, config.keep_filter_on_submit or false)
|
||||
end
|
||||
|
||||
---Navigate up one level.
|
||||
---@param state neotree.sources.filesystem.State
|
||||
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(uv.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
|
||||
|
||||
---@param state neotree.sources.filesystem.State
|
||||
M.next_git_modified = function(state)
|
||||
focus_next_git_modified(state, false)
|
||||
end
|
||||
|
||||
---@param state neotree.sources.filesystem.State
|
||||
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
|
||||
|
||||
---@param state neotree.sources.filesystem.State
|
||||
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.
|
||||
---@param state neotree.sources.filesystem.State
|
||||
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.
|
||||
---@param state neotree.sources.filesystem.State
|
||||
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,49 @@
|
|||
-- 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")
|
||||
|
||||
---@alias neotree.Component.Filesystem._Key
|
||||
---|"current_filter"
|
||||
|
||||
---@class neotree.Component.Filesystem
|
||||
---@field [1] neotree.Component.Filesystem._Key|neotree.Component.Common._Key
|
||||
|
||||
---@type table<neotree.Component.Filesystem._Key, neotree.Renderer>
|
||||
local M = {}
|
||||
|
||||
---@class (exact) neotree.Component.Filesystem.CurrentFilter : neotree.Component.Common.CurrentFilter
|
||||
|
||||
---@param config neotree.Component.Filesystem.CurrentFilter
|
||||
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,536 @@
|
|||
--This file should have all functions that are in the public api and either set
|
||||
--or read the state of this source.
|
||||
|
||||
local utils = require("neo-tree.utils")
|
||||
local _compat = require("neo-tree.utils._compat")
|
||||
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")
|
||||
|
||||
---@class neotree.sources.filesystem : neotree.Source
|
||||
local M = {
|
||||
name = "filesystem",
|
||||
display_name = " Files ",
|
||||
}
|
||||
|
||||
local wrap = function(func)
|
||||
return utils.wrap(func, M.name)
|
||||
end
|
||||
|
||||
---@return neotree.sources.filesystem.State
|
||||
local get_state = function(tabid)
|
||||
return manager.get_state(M.name, tabid) --[[@as neotree.sources.filesystem.State]]
|
||||
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_nodes = state.explicitly_opened_nodes 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_nodes[id] then
|
||||
if path_to_reveal:sub(1, #id) == id then
|
||||
state.explicitly_opened_nodes[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
|
||||
|
||||
local fs_stat = (vim.uv or vim.loop).fs_stat
|
||||
|
||||
---@param state neotree.sources.filesystem.State
|
||||
---@param path string?
|
||||
---@param path_to_reveal string?
|
||||
---@param callback function?
|
||||
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 doesn't exist, navigate upwards until it does
|
||||
local orig_path = path
|
||||
local backed_out = false
|
||||
while not fs_stat(path) do
|
||||
log.debug(("navigate_internal: path %s didn't exist, going up a directory"):format(path))
|
||||
backed_out = true
|
||||
local parent, _ = utils.split_path(path)
|
||||
if not parent then
|
||||
break
|
||||
end
|
||||
path = parent
|
||||
end
|
||||
|
||||
if backed_out then
|
||||
log.warn(("Root path %s doesn't exist, backing out to %s"):format(orig_path, path))
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
---@param state neotree.State
|
||||
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.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, _compat.noref())
|
||||
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
|
||||
|
||||
---@alias neotree.Config.Cwd "tab"|"window"|"global"
|
||||
|
||||
---@class neotree.Config.Filesystem.CwdTarget
|
||||
---@field sidebar neotree.Config.Cwd?
|
||||
---@field current neotree.Config.Cwd?
|
||||
|
||||
---@class neotree.Config.Filesystem.FilteredItems
|
||||
---@field visible boolean?
|
||||
---@field force_visible_in_empty_folder boolean?
|
||||
---@field children_inherit_highlights boolean?
|
||||
---@field show_hidden_count boolean?
|
||||
---@field hide_dotfiles boolean?
|
||||
---@field hide_gitignored boolean?
|
||||
---@field hide_hidden boolean?
|
||||
---@field hide_by_name string[]?
|
||||
---@field hide_by_pattern string[]?
|
||||
---@field always_show string[]?
|
||||
---@field always_show_by_pattern string[]?
|
||||
---@field never_show string[]?
|
||||
---@field never_show_by_pattern string[]?
|
||||
|
||||
---@alias neotree.Config.Filesystem.FindArgsHandler fun(cmd:string, path:string, search_term:string, args:string[]):string[]
|
||||
|
||||
---@class neotree.Config.Filesystem.FollowCurrentFile
|
||||
---@field enabled boolean?
|
||||
---@field leave_dirs_open boolean?
|
||||
|
||||
---@alias neotree.Config.HijackNetrwBehavior
|
||||
---|"open_default" # opening a directory opens neo-tree with the default window.position.
|
||||
---|"open_current" # opening a directory opens neo-tree within the current window.
|
||||
---|"disabled" # opening a directory opens neo-tree within the current window.
|
||||
|
||||
---@class neotree.Config.Filesystem.Renderers : neotree.Config.Renderers
|
||||
|
||||
---@class neotree.Config.Filesystem.Window : neotree.Config.Window
|
||||
---@field fuzzy_finder_mappings neotree.Config.FuzzyFinder.Mappings?
|
||||
|
||||
---@alias neotree.Config.Filesystem.AsyncDirectoryScan
|
||||
---|"auto"
|
||||
---|"always"
|
||||
---|"never"
|
||||
|
||||
---@alias neotree.Config.Filesystem.ScanMode
|
||||
---|"shallow"
|
||||
---|"deep"
|
||||
|
||||
---@class (exact) neotree.Config.Filesystem : neotree.Config.Source
|
||||
---@field async_directory_scan neotree.Config.Filesystem.AsyncDirectoryScan?
|
||||
---@field scan_mode neotree.Config.Filesystem.ScanMode?
|
||||
---@field bind_to_cwd boolean?
|
||||
---@field cwd_target neotree.Config.Filesystem.CwdTarget?
|
||||
---@field check_gitignore_in_search boolean?
|
||||
---@field filtered_items neotree.Config.Filesystem.FilteredItems?
|
||||
---@field find_by_full_path_words boolean?
|
||||
---@field find_command string?
|
||||
---@field find_args table<string, string[]>|neotree.Config.Filesystem.FindArgsHandler|nil
|
||||
---@field group_empty_dirs boolean?
|
||||
---@field search_limit integer?
|
||||
---@field follow_current_file neotree.Config.Filesystem.FollowCurrentFile?
|
||||
---@field hijack_netrw_behavior neotree.Config.HijackNetrwBehavior?
|
||||
---@field use_libuv_file_watcher boolean?
|
||||
---@field renderers neotree.Config.Filesystem.Renderers?
|
||||
---@field window neotree.Config.Filesystem.Window?
|
||||
---@field enable_git_status boolean?
|
||||
|
||||
---Configures the plugin, should be called before the plugin is used.
|
||||
---@param config neotree.Config.Filesystem Configuration table containing any keys that the user wants to change from the defaults. May be empty to accept default values.
|
||||
---@param global_config neotree.Config.Base
|
||||
M.setup = function(config, global_config)
|
||||
config.filtered_items = config.filtered_items or {}
|
||||
config.enable_git_status = config.enable_git_status or global_config.enable_git_status
|
||||
|
||||
for _, key in ipairs({ "hide_by_pattern", "always_show_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.
|
||||
---@param state neotree.sources.filesystem.State
|
||||
---@param node NuiTree.Node
|
||||
---@param path_to_reveal string
|
||||
---@param skip_redraw boolean?
|
||||
---@param recursive boolean?
|
||||
---@param callback function?
|
||||
M.toggle_directory = function(state, node, path_to_reveal, skip_redraw, recursive, callback)
|
||||
local tree = state.tree
|
||||
if not node then
|
||||
node = assert(tree:get_node())
|
||||
end
|
||||
if node.type ~= "directory" then
|
||||
return
|
||||
end
|
||||
state.explicitly_opened_nodes = state.explicitly_opened_nodes or {}
|
||||
if node.loaded == false then
|
||||
local id = node:get_id()
|
||||
state.explicitly_opened_nodes[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_nodes[node:get_id()] = false
|
||||
else
|
||||
updated = node:expand()
|
||||
state.explicitly_opened_nodes[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 = {
|
||||
---@param state neotree.sources.filesystem.State
|
||||
---@param node NuiTree.Node
|
||||
prefetch = function(state, node)
|
||||
if node.type ~= "directory" then
|
||||
return
|
||||
end
|
||||
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,247 @@
|
|||
-- This file holds all code for the search function.
|
||||
|
||||
local Input = require("nui.input")
|
||||
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 compat = require("neo-tree.utils._compat")
|
||||
local common_filter = require("neo-tree.sources.common.filters")
|
||||
|
||||
local M = {}
|
||||
|
||||
---@param state neotree.sources.filesystem.State
|
||||
---@param search_as_you_type boolean?
|
||||
---@param fuzzy_finder_mode "directory"|boolean?
|
||||
---@param use_fzy boolean?
|
||||
---@param keep_filter_on_submit boolean?
|
||||
M.show_filter = function(
|
||||
state,
|
||||
search_as_you_type,
|
||||
fuzzy_finder_mode,
|
||||
use_fzy,
|
||||
keep_filter_on_submit
|
||||
)
|
||||
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.config.title then
|
||||
popup_msg = state.config.title
|
||||
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
|
||||
|
||||
---@type neotree.Config.SortFunction
|
||||
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")
|
||||
---@type table|nil
|
||||
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 and not keep_filter_on_submit 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, compat.noref())
|
||||
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
|
||||
---@type function|nil
|
||||
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)
|
||||
---@class neotree.sources.filesystem.FuzzyFinder.BuiltinCommands : neotree.FuzzyFinder.BuiltinCommands
|
||||
local cmds
|
||||
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,
|
||||
|
||||
close = function(_state, _scroll_padding)
|
||||
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)
|
||||
and not keep_filter_on_submit
|
||||
then
|
||||
fs.reset_search(state, true)
|
||||
end
|
||||
end, 100)
|
||||
restore_height()
|
||||
end,
|
||||
close_keep_filter = function(_state, _scroll_padding)
|
||||
log.info("Persisting the search filter")
|
||||
keep_filter_on_submit = true
|
||||
cmds.close(_state, _scroll_padding)
|
||||
end,
|
||||
close_clear_filter = function(_state, _scroll_padding)
|
||||
log.info("Clearing the search filter")
|
||||
keep_filter_on_submit = false
|
||||
cmds.close(_state, _scroll_padding)
|
||||
end,
|
||||
}
|
||||
|
||||
common_filter.setup_hooks(input, cmds, state, scroll_padding)
|
||||
|
||||
if not fuzzy_finder_mode then
|
||||
return
|
||||
end
|
||||
|
||||
common_filter.setup_mappings(input, cmds, state, scroll_padding)
|
||||
end
|
||||
|
||||
return M
|
||||
|
|
@ -0,0 +1,393 @@
|
|||
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 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 neotree.FileKind
|
||||
---@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 kind neotree.FileKind | 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: number): any Executed at the end.
|
||||
M.filter_files_external = function(
|
||||
cmd,
|
||||
path,
|
||||
glob,
|
||||
regex,
|
||||
full_path,
|
||||
kind,
|
||||
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 {}
|
||||
kind = kind or {}
|
||||
limit = limit or math.huge -- math.huge == no limit
|
||||
local file_kind_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(kind) do
|
||||
if v and file_kind_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_kinds = {}
|
||||
for k, v in pairs(kind) do
|
||||
if v and file_kind_map[k] ~= nil then
|
||||
file_kinds[#file_kinds + 1] = file_kind_map[k]
|
||||
end
|
||||
end
|
||||
if ignore.dotfiles then
|
||||
append("-name", ".*", "-prune", "-o")
|
||||
end
|
||||
if #file_kinds > 0 then
|
||||
append("-type", table.concat(file_kinds, ","))
|
||||
end
|
||||
if kind.empty then
|
||||
append("-empty")
|
||||
end
|
||||
if kind.executable then
|
||||
append("-executable")
|
||||
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("-print")
|
||||
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
|
||||
---@diagnostic disable-next-line: missing-fields
|
||||
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
|
||||
|
||||
---@param state neotree.State
|
||||
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,719 @@
|
|||
-- 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 api = vim.api
|
||||
local uv = vim.uv or 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 = {}
|
||||
|
||||
---@param a uv.fs_stat.result?
|
||||
---@param b uv.fs_stat.result?
|
||||
---@return boolean equal Whether a and b are stats of the same file
|
||||
local same_file = function(a, b)
|
||||
return a and b and a.dev == b.dev and a.ino == b.ino or false
|
||||
end
|
||||
|
||||
---Checks to see if a file can safely be renamed to its destination without data loss.
|
||||
---Also prevents renames from going through if the rename will not do anything.
|
||||
---Has an additional check for case-insensitive filesystems (e.g. for windows)
|
||||
---@param source string
|
||||
---@param destination string
|
||||
---@return boolean rename_is_safe
|
||||
local function rename_is_safe(source, destination)
|
||||
local destination_file = uv.fs_stat(destination)
|
||||
if not destination_file then
|
||||
return true
|
||||
end
|
||||
|
||||
local src = utils.normalize_path(source)
|
||||
local dest = utils.normalize_path(destination)
|
||||
local changing_casing = src ~= dest and src:lower() == dest:lower()
|
||||
if changing_casing then
|
||||
local src_file = uv.fs_stat(src)
|
||||
-- We check that the two paths resolve to the same canonical filename and file.
|
||||
return same_file(src_file, destination_file)
|
||||
and uv.fs_realpath(src) == uv.fs_realpath(destination)
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
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.bo[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.bo[new_buf].buflisted = true
|
||||
replace_buffer_in_windows(buf, new_buf)
|
||||
|
||||
if vim.bo[buf].buftype == "" then
|
||||
local modified = vim.bo[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 function create_all_as_folders(in_path)
|
||||
if not uv.fs_stat(in_path) then
|
||||
local parent, _ = utils.split_path(in_path)
|
||||
if parent then
|
||||
create_all_as_folders(parent)
|
||||
end
|
||||
uv.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.
|
||||
---@param source string
|
||||
---@param destination string
|
||||
---@param using_root_directory boolean
|
||||
---@param name_chosen_callback fun(string)
|
||||
---@param first_message string?
|
||||
local function get_unused_name(
|
||||
source,
|
||||
destination,
|
||||
using_root_directory,
|
||||
name_chosen_callback,
|
||||
first_message
|
||||
)
|
||||
if not rename_is_safe(source, 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(source, 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(source, destination or source, using_root_directory, function(dest)
|
||||
-- Resolve user-inputted relative paths out of the absolute paths
|
||||
dest = vim.fs.normalize(dest)
|
||||
if utils.is_windows then
|
||||
dest = utils.windowize_path(dest)
|
||||
end
|
||||
local function move_file()
|
||||
create_all_parents(dest)
|
||||
uv.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(source, _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 event_result = events.fire_event(events.BEFORE_FILE_ADD, destination) or {}
|
||||
if event_result.handled then
|
||||
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
|
||||
|
||||
local event_result = events.fire_event(events.BEFORE_FILE_ADD, destination) or {}
|
||||
if event_result.handled then
|
||||
return
|
||||
end
|
||||
|
||||
if uv.fs_stat(destination) then
|
||||
log.warn("Directory already exists")
|
||||
return
|
||||
end
|
||||
|
||||
create_all_parents(destination)
|
||||
uv.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
|
||||
|
||||
destination = utils.normalize_path(destination)
|
||||
if uv.fs_stat(destination) then
|
||||
log.warn("File already exists")
|
||||
return
|
||||
end
|
||||
|
||||
local complete = vim.schedule_wrap(function()
|
||||
events.fire_event(events.FILE_ADDED, destination)
|
||||
if callback then
|
||||
callback(destination)
|
||||
end
|
||||
end)
|
||||
local event_result = events.fire_event(events.BEFORE_FILE_ADD, destination) or {}
|
||||
if event_result.handled then
|
||||
complete()
|
||||
return
|
||||
end
|
||||
|
||||
create_all_parents(destination)
|
||||
if is_dir then
|
||||
uv.fs_mkdir(destination, 493)
|
||||
else
|
||||
local open_mode = uv.constants.O_CREAT + uv.constants.O_WRONLY + uv.constants.O_TRUNC
|
||||
local fd = uv.fs_open(destination, open_mode, 420)
|
||||
if not fd then
|
||||
if not uv.fs_stat(destination) then
|
||||
log.error("Could not create file " .. destination)
|
||||
return
|
||||
else
|
||||
log.warn("Failed to complete file creation of " .. destination)
|
||||
end
|
||||
else
|
||||
uv.fs_close(fd)
|
||||
end
|
||||
end
|
||||
complete()
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
---Recursively delete a directory and its children.
|
||||
---@param dir_path string Directory to delete.
|
||||
---@return boolean success Whether the directory was deleted.
|
||||
local function delete_dir(dir_path)
|
||||
local handle = uv.fs_scandir(dir_path)
|
||||
if type(handle) == "string" then
|
||||
log.error(handle)
|
||||
return false
|
||||
end
|
||||
|
||||
if not handle then
|
||||
log.error("could not scan dir " .. dir_path)
|
||||
return false
|
||||
end
|
||||
|
||||
while true do
|
||||
local child_name, t = uv.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 = uv.fs_unlink(child_path)
|
||||
if not success then
|
||||
return false
|
||||
end
|
||||
clear_buffer(child_path)
|
||||
end
|
||||
end
|
||||
return uv.fs_rmdir(dir_path) or false
|
||||
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 = uv.fs_stat(path)
|
||||
if stat then
|
||||
_type = stat.type
|
||||
if _type == "link" then
|
||||
local link_to = uv.fs_readlink(path)
|
||||
if not link_to then
|
||||
log.error("Could not read link")
|
||||
return
|
||||
end
|
||||
local target_file = uv.fs_stat(link_to)
|
||||
if target_file then
|
||||
_type = target_file.type
|
||||
end
|
||||
_type = uv.fs_stat(link_to).type
|
||||
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()
|
||||
local complete = vim.schedule_wrap(function()
|
||||
events.fire_event(events.FILE_DELETED, path)
|
||||
if callback then
|
||||
callback(path)
|
||||
end
|
||||
end)
|
||||
|
||||
local event_result = events.fire_event(events.BEFORE_FILE_DELETE, path) or {}
|
||||
if event_result.handled then
|
||||
complete()
|
||||
return
|
||||
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 log.error("Could not remove directory: " .. path)
|
||||
end
|
||||
end
|
||||
else
|
||||
local success = uv.fs_unlink(path)
|
||||
if not success then
|
||||
return log.error("Could not remove file: " .. path)
|
||||
end
|
||||
clear_buffer(path)
|
||||
end
|
||||
complete()
|
||||
end
|
||||
|
||||
if noconfirm then
|
||||
do_delete()
|
||||
else
|
||||
inputs.confirm(msg, function(confirmed)
|
||||
if confirmed then
|
||||
do_delete()
|
||||
end
|
||||
end)
|
||||
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
|
||||
|
||||
local rename_node = function(msg, name, get_destination, path, callback)
|
||||
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 = get_destination(new_name)
|
||||
|
||||
if not rename_is_safe(path, destination) then
|
||||
log.warn(destination, " already exists, canceling")
|
||||
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()
|
||||
uv.fs_rename(path, destination, function(err)
|
||||
if err then
|
||||
log.warn("Could not rename the files")
|
||||
return
|
||||
end
|
||||
complete()
|
||||
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
|
||||
complete()
|
||||
return
|
||||
end
|
||||
fs_rename()
|
||||
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)
|
||||
|
||||
local get_destination = function(new_name)
|
||||
return parent_path .. utils.path_separator .. new_name
|
||||
end
|
||||
|
||||
rename_node(msg, name, get_destination, path, callback)
|
||||
end
|
||||
|
||||
-- Rename Node Base Name
|
||||
M.rename_node_basename = function(path, callback)
|
||||
local parent_path, name = utils.split_path(path)
|
||||
local base_name = vim.fn.fnamemodify(path, ":t:r")
|
||||
local extension = vim.fn.fnamemodify(path, ":e")
|
||||
|
||||
local msg = string.format('Enter new base name for "%s":', name)
|
||||
|
||||
local get_destination = function(new_base_name)
|
||||
return parent_path
|
||||
.. utils.path_separator
|
||||
.. new_base_name
|
||||
.. (extension:len() == 0 and "" or "." .. extension)
|
||||
end
|
||||
|
||||
rename_node(msg, base_name, get_destination, path, callback)
|
||||
end
|
||||
|
||||
return M
|
||||
|
|
@ -0,0 +1,738 @@
|
|||
-- This files holds code for scanning the filesystem to build the tree.
|
||||
local uv = vim.uv or 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 = {}
|
||||
|
||||
--- how many entries to load per readdir
|
||||
local ENTRIES_BATCH_SIZE = 1000
|
||||
|
||||
---@param context neotree.sources.filesystem.Context
|
||||
---@param dir_path string
|
||||
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
|
||||
|
||||
---@param context neotree.sources.filesystem.Context
|
||||
---@param dir_path string
|
||||
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(uv.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
|
||||
|
||||
---@param context neotree.sources.filesystem.Context
|
||||
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
|
||||
---@diagnostic disable-next-line: cast-local-type
|
||||
context = nil
|
||||
end
|
||||
|
||||
---@param context neotree.sources.filesystem.Context
|
||||
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)
|
||||
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, ENTRIES_BATCH_SIZE)
|
||||
if not dir then
|
||||
---@cast err -nil
|
||||
if is_permission_error(err) then
|
||||
log.debug(err)
|
||||
else
|
||||
log.error(err)
|
||||
end
|
||||
return children
|
||||
end
|
||||
repeat
|
||||
local stats = uv.fs_readdir(dir)
|
||||
if not stats then
|
||||
break
|
||||
end
|
||||
local more = false
|
||||
for i, stat in ipairs(stats) do
|
||||
more = i == ENTRIES_BATCH_SIZE
|
||||
local child_path = utils.path_join(path, stat.name)
|
||||
table.insert(children, { path = child_path, type = stat.type })
|
||||
end
|
||||
until not more
|
||||
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
|
||||
local readdir_batch
|
||||
---@param _ string?
|
||||
---@param stats uv.fs_readdir.entry[]
|
||||
readdir_batch = function(_, stats)
|
||||
if stats then
|
||||
local more = false
|
||||
for i, stat in ipairs(stats) do
|
||||
more = i == ENTRIES_BATCH_SIZE
|
||||
local child_path = utils.path_join(path, stat.name)
|
||||
table.insert(children, { path = child_path, type = stat.type })
|
||||
end
|
||||
if more then
|
||||
return uv.fs_readdir(dir, readdir_batch)
|
||||
end
|
||||
end
|
||||
uv.fs_closedir(dir)
|
||||
callback(children)
|
||||
end
|
||||
|
||||
uv.fs_readdir(dir, readdir_batch)
|
||||
end, ENTRIES_BATCH_SIZE)
|
||||
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.
|
||||
---@param context neotree.sources.filesystem.Context
|
||||
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
|
||||
|
||||
---@param context neotree.sources.filesystem.Context
|
||||
---@param path_to_scan string
|
||||
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 dir, err = uv.fs_opendir(path_to_scan, nil, ENTRIES_BATCH_SIZE)
|
||||
if dir then
|
||||
repeat
|
||||
local stats = uv.fs_readdir(dir)
|
||||
if not stats then
|
||||
break
|
||||
end
|
||||
|
||||
local more = false
|
||||
for i, stat in ipairs(stats) do
|
||||
more = i == ENTRIES_BATCH_SIZE
|
||||
local path = utils.path_join(path_to_scan, stat.name)
|
||||
local success, _ = pcall(file_items.create_item, context, path, stat.type)
|
||||
if success 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
|
||||
until not more
|
||||
uv.fs_closedir(dir)
|
||||
else
|
||||
log.error("Error opening dir:", err)
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
---@param state neotree.sources.filesystem.State
|
||||
---@param parent_id string?
|
||||
---@param path_to_reveal string?
|
||||
---@param callback function
|
||||
M.get_items_sync = function(state, parent_id, path_to_reveal, callback)
|
||||
M.get_items(state, parent_id, path_to_reveal, callback, false)
|
||||
end
|
||||
|
||||
---@param state neotree.sources.filesystem.State
|
||||
---@param parent_id string?
|
||||
---@param path_to_reveal string?
|
||||
---@param callback function
|
||||
M.get_items_async = function(state, parent_id, path_to_reveal, callback)
|
||||
M.get_items(state, parent_id, path_to_reveal, callback, true)
|
||||
end
|
||||
|
||||
---@param context neotree.sources.filesystem.Context
|
||||
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
|
||||
|
||||
---@param context neotree.sources.filesystem.Context
|
||||
---@param async_dir_scan boolean
|
||||
local handle_refresh_or_up = function(context, async_dir_scan)
|
||||
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
|
||||
---@type table<string, boolean?>
|
||||
local seen = {}
|
||||
for _, p in ipairs(context.paths_to_load) do
|
||||
---@type string?
|
||||
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 = uv.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_dir_scan then
|
||||
async_scan(context, path)
|
||||
else
|
||||
sync_scan(context, path)
|
||||
end
|
||||
end
|
||||
|
||||
---@class neotree.sources.filesystem.Context : neotree.FileItemContext
|
||||
---@field state neotree.sources.filesystem.State
|
||||
---@field recursive boolean?
|
||||
---@field parent_id string?
|
||||
---@field callback function?
|
||||
---@field async boolean?
|
||||
---@field root neotree.FileItem.Directory|neotree.FileItem.Link
|
||||
---@field directories_scanned integer?
|
||||
---@field directories_to_scan integer?
|
||||
---@field on_exit function?
|
||||
---async
|
||||
---@field paths_to_load string[]
|
||||
---@field is_a_never_show_file fun(filename: string?):boolean
|
||||
|
||||
---@class neotree.sources.filesystem.State : neotree.StateWithTree, neotree.Config.Filesystem
|
||||
---@field path string
|
||||
|
||||
---@param state neotree.sources.filesystem.State
|
||||
---@param parent_id string?
|
||||
---@param callback function?
|
||||
---@param async_dir_scan boolean?
|
||||
---@param recursive boolean?
|
||||
M.get_items = function(state, parent_id, path_to_reveal, callback, async_dir_scan, recursive)
|
||||
renderer.acquire_window(state)
|
||||
if state.async_directory_scan == "always" then
|
||||
async_dir_scan = true
|
||||
elseif state.async_directory_scan == "never" then
|
||||
async_dir_scan = false
|
||||
elseif type(async_dir_scan) == "nil" then
|
||||
async_dir_scan = (state.async_directory_scan == "auto") or state.async_directory_scan ~= nil
|
||||
end
|
||||
|
||||
if not parent_id then
|
||||
M.stop_watchers(state)
|
||||
end
|
||||
---@type neotree.sources.filesystem.Context
|
||||
local context = file_items.create_context() --[[@as neotree.sources.filesystem.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") --[[@as neotree.FileItem.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_dir_scan)
|
||||
end
|
||||
end
|
||||
|
||||
---@param state neotree.sources.filesystem.State
|
||||
---@param parent_id string
|
||||
---@param recursive boolean?
|
||||
M.get_dir_items_async = function(state, parent_id, recursive)
|
||||
local context = file_items.create_context() --[[@as neotree.sources.filesystem.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") --[[@as neotree.FileItem.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
|
||||
|
||||
---@param state neotree.sources.filesystem.State
|
||||
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 events = require("neo-tree.events")
|
||||
local log = require("neo-tree.log")
|
||||
local git = require("neo-tree.git")
|
||||
local utils = require("neo-tree.utils")
|
||||
local uv = vim.uv or vim.loop
|
||||
|
||||
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 = uv.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 = uv.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 = uv.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,74 @@
|
|||
--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")
|
||||
|
||||
---@class neotree.sources.GitStatus.Commands : neotree.sources.Common.Commands
|
||||
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
|
||||
|
||||
---@type neotree.TreeCommandVisual
|
||||
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
|
||||
|
||||
---@type neotree.TreeCommandVisual
|
||||
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
|
||||
|
||||
---@type neotree.TreeCommandVisual
|
||||
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,56 @@
|
|||
-- 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")
|
||||
|
||||
---@alias neotree.Component.GitStatus._Key
|
||||
---|"name"
|
||||
|
||||
---@class neotree.Component.GitStatus
|
||||
---@field [1] neotree.Component.GitStatus._Key|neotree.Component.Common._Key
|
||||
|
||||
---@type table<neotree.Component.GitStatus._Key, neotree.Renderer>
|
||||
local M = {}
|
||||
|
||||
---@class (exact) neotree.Component.GitStatus.Name : neotree.Component.Common.Name
|
||||
---@field [1] "current_filter"?
|
||||
---@field use_git_status_colors boolean?
|
||||
|
||||
---@param config neotree.Component.GitStatus.Name
|
||||
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,111 @@
|
|||
--This file should have all functions that are in the public api and either set
|
||||
--or read the state of this source.
|
||||
|
||||
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")
|
||||
|
||||
---@class neotree.sources.GitStatus : neotree.Source
|
||||
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.path = path or state.path
|
||||
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
|
||||
|
||||
---@class neotree.Config.GitStatus.Renderers : neotree.Config.Renderers
|
||||
|
||||
---@class (exact) neotree.Config.GitStatus : neotree.Config.Source
|
||||
---@field bind_to_cwd boolean?
|
||||
---@field renderers neotree.Config.GitStatus.Renderers?
|
||||
|
||||
---Configures the plugin, should be called before the plugin is used.
|
||||
---@param config neotree.Config.GitStatus 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 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'.
|
||||
---@param state neotree.State
|
||||
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)
|
||||
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") --[[@as neotree.FileItem.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") --[[@as neotree.FileItem.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,772 @@
|
|||
--This file should have all functions that are in the public api and either set
|
||||
--or read the state of this source.
|
||||
local nt = require("neo-tree")
|
||||
local utils = require("neo-tree.utils")
|
||||
local compat = require("neo-tree.utils._compat")
|
||||
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 = {}
|
||||
---@type table<string, neotree.SourceData>
|
||||
local source_data = {}
|
||||
---@type neotree.State[]
|
||||
local all_states = {}
|
||||
---@type table<string, neotree.Config.Source?>
|
||||
local default_configs = {}
|
||||
|
||||
---@class neotree.SourceData
|
||||
---@field name string
|
||||
---@field state_by_tab table<integer, neotree.State>
|
||||
---@field state_by_win table<integer, neotree.State>
|
||||
---@field subscriptions table
|
||||
---@field module neotree.Source?
|
||||
|
||||
---@param source_name string
|
||||
---@return neotree.SourceData
|
||||
local get_source_data = function(source_name)
|
||||
assert(source_name, "get_source_data: source_name cannot be nil")
|
||||
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
|
||||
|
||||
---@class neotree.State.Window : neotree.Config.Window
|
||||
---@field win_width integer
|
||||
---@field last_user_width integer
|
||||
|
||||
---@alias neotree.State.Position "top"|"bottom"|"left"|"right"|"current"|"float"
|
||||
|
||||
---@alias neotree.Internal.SortFieldProvider fun(node: NuiTree.Node):any
|
||||
|
||||
---@class neotree.State : neotree.Config.Source
|
||||
---@field name string
|
||||
---@field tabid integer
|
||||
---@field id integer
|
||||
---@field bufnr integer?
|
||||
---@field dirty boolean
|
||||
---@field position table
|
||||
---@field git_base string
|
||||
---@field sort table
|
||||
---@field clipboard table
|
||||
---@field current_position neotree.State.Position?
|
||||
---@field disposed boolean?
|
||||
---@field winid integer?
|
||||
---@field path string?
|
||||
---@field tree NuiTree?
|
||||
---@field components table<string, neotree.Component>
|
||||
---private-ish
|
||||
---@field orig_tree NuiTree?
|
||||
---@field _ready boolean?
|
||||
---@field loading boolean?
|
||||
---window
|
||||
---@field window neotree.State.Window?
|
||||
---@field win_width integer?
|
||||
---@field longest_width_exact integer?
|
||||
---@field longest_node integer?
|
||||
---extras
|
||||
---@field bind_to_cwd boolean?
|
||||
---@field opened_buffers neotree.utils.OpenedBuffers?
|
||||
---@field diagnostics_lookup neotree.utils.DiagnosticLookup?
|
||||
---@field cwd_target neotree.Config.Filesystem.CwdTarget?
|
||||
---@field sort_field_provider fun(node: NuiTree.Node):any
|
||||
---@field explicitly_opened_nodes table<string, boolean?>?
|
||||
---@field filtered_items neotree.Config.Filesystem.FilteredItems?
|
||||
---@field skip_marker_at_level table<integer, boolean?>?
|
||||
---@field group_empty_dirs boolean?
|
||||
---git
|
||||
---@field git_status_lookup neotree.git.Status?
|
||||
---optional mapping args
|
||||
---@field fallback string?
|
||||
---@field config table?
|
||||
---internal
|
||||
---@field default_expanded_nodes NuiTree.Node[]?
|
||||
---@field force_open_folders string[]?
|
||||
---@field enable_source_selector boolean?
|
||||
---@field follow_current_file neotree.Config.Filesystem.FollowCurrentFile?
|
||||
---lsp
|
||||
---@field lsp_winid number?
|
||||
---@field lsp_bufnr number?
|
||||
---search
|
||||
---@field search_pattern string?
|
||||
---@field use_fzy boolean?
|
||||
---@field fzy_sort_result_scores table<string, integer?>?
|
||||
---@field fuzzy_finder_mode "directory"|boolean?
|
||||
---@field open_folders_before_search table?
|
||||
---sort
|
||||
---@field sort_function_override neotree.Config.SortFunction?
|
||||
---keymaps
|
||||
---@field resolved_mappings table<string, neotree.State.ResolvedMapping?>?
|
||||
---@field commands table<string, neotree.TreeCommand?>?
|
||||
|
||||
---@class (exact) neotree.StateWithTree : neotree.State
|
||||
---@field tree NuiTree
|
||||
|
||||
local a = {}
|
||||
|
||||
---@param tabid integer
|
||||
---@param sd table
|
||||
---@param winid integer?
|
||||
---@return neotree.State
|
||||
local function create_state(tabid, sd, winid)
|
||||
nt.ensure_config()
|
||||
local default_config = assert(default_configs[sd.name])
|
||||
local state = vim.deepcopy(default_config, compat.noref())
|
||||
---@cast state neotree.State
|
||||
state.tabid = tabid
|
||||
state.id = winid or tabid
|
||||
state.dirty = true
|
||||
state.position = {}
|
||||
state.git_base = "HEAD"
|
||||
state.sort = { label = "Name", direction = 1 }
|
||||
events.fire_event(events.STATE_CREATED, state)
|
||||
table.insert(all_states, state)
|
||||
return state
|
||||
end
|
||||
|
||||
M._get_all_states = function()
|
||||
return all_states
|
||||
end
|
||||
|
||||
---@param source_name string?
|
||||
---@param action fun(state: neotree.State)
|
||||
M._for_each_state = function(source_name, action)
|
||||
M.dispose_invalid_tabs()
|
||||
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
|
||||
|
||||
---@param source_name string
|
||||
---@param config neotree.Config.Source
|
||||
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?
|
||||
---@param source_name string
|
||||
---@param tabid integer?
|
||||
---@param winid integer?
|
||||
---@return neotree.State
|
||||
M.get_state = function(source_name, tabid, winid)
|
||||
assert(source_name, "get_state: source_name cannot be nil")
|
||||
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
|
||||
end
|
||||
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
|
||||
|
||||
---Returns the state for the current buffer, assuming it is a neo-tree buffer.
|
||||
---@param winid number? The window id to use, if nil, the current window is used.
|
||||
---@return neotree.State? state The state for the current buffer, if it's a neo-tree buffer.
|
||||
M.get_state_for_window = function(winid)
|
||||
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
|
||||
|
||||
---@param source_name string
|
||||
M.subscribe = function(source_name, event)
|
||||
assert(source_name, "subscribe: source_name cannot be nil")
|
||||
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
|
||||
|
||||
---@param source_name string
|
||||
M.unsubscribe = function(source_name, event)
|
||||
assert(source_name, "unsubscribe: source_name cannot be nil")
|
||||
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
|
||||
|
||||
---@param source_name string
|
||||
M.unsubscribe_all = function(source_name)
|
||||
assert(source_name, "unsubscribe_all: source_name cannot be nil")
|
||||
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
|
||||
|
||||
---@param source_name string
|
||||
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.
|
||||
---@param source_name string
|
||||
---@param args table<string, neotree.utils.DiagnosticCounts?>
|
||||
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.
|
||||
---@param source_name string
|
||||
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.
|
||||
---@param source_name string
|
||||
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
|
||||
|
||||
---@param state neotree.State
|
||||
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
|
||||
|
||||
---@param state neotree.State
|
||||
---@return string
|
||||
M.get_cwd = function(state)
|
||||
local winid, tabnr = get_params_for_cwd(state)
|
||||
if winid or tabnr then
|
||||
local success, cwd = pcall(vim.fn.getcwd, winid, tabnr)
|
||||
if success then
|
||||
return cwd
|
||||
end
|
||||
end
|
||||
|
||||
local success, cwd = pcall(vim.fn.getcwd)
|
||||
if success then
|
||||
return cwd
|
||||
end
|
||||
|
||||
local err = cwd
|
||||
log.debug(err)
|
||||
return state.path or ""
|
||||
end
|
||||
|
||||
---@param state neotree.State
|
||||
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
|
||||
|
||||
---@param state neotree.State
|
||||
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
|
||||
|
||||
---@param source_name string
|
||||
---@param tabid integer
|
||||
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
|
||||
|
||||
---@param tabid integer
|
||||
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
|
||||
|
||||
---@param winid number
|
||||
M.dispose_window = function(winid)
|
||||
assert(winid, "dispose_window: winid cannot be nil")
|
||||
-- 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
|
||||
|
||||
---@param source_name string
|
||||
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 neotree.State|string 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")
|
||||
return
|
||||
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:
|
||||
--- ```lua
|
||||
--- 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:
|
||||
--- ```lua
|
||||
--- 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
|
||||
|
||||
local validate = require("neo-tree.health.typecheck").validate
|
||||
---@param source_name string
|
||||
---@param module neotree.Source
|
||||
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
|
||||
validate(source_name, module, function(mod)
|
||||
validate("navigate", mod.navigate, "function")
|
||||
validate("setup", mod.setup, "function")
|
||||
end)
|
||||
end
|
||||
|
||||
---@class neotree.Source
|
||||
---@field setup fun(config: neotree.Config.Source, global_config: neotree.Config.Base)
|
||||
---@field navigate fun(state: neotree.State, path: string?, path_to_reveal: string?, callback: function?, async: boolean?)
|
||||
|
||||
---Configures the plugin, should be called before the plugin is used.
|
||||
---@param source_name string Name of the source.
|
||||
---@param config neotree.Config.Source Configuration table containing merged configuration for the source.
|
||||
---@param global_config neotree.Config.Base Global configuration table, shared between all sources.
|
||||
---@param module neotree.Source 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,16 @@
|
|||
---@meta
|
||||
|
||||
---@alias neotree.Renderer fun(config: table, node: NuiTree.Node, state: neotree.StateWithTree):(neotree.Render.Node|neotree.Render.Node[])
|
||||
|
||||
---@alias neotree.FileRenderer fun(config: table, node: neotree.FileNode, state: neotree.StateWithTree):(neotree.Render.Node|neotree.Render.Node[])
|
||||
|
||||
---@class (exact) neotree.Render.Node
|
||||
---@field text string The text to display.
|
||||
---@field highlight string The highlight for the text.
|
||||
|
||||
---@class (exact) neotree.Component
|
||||
---@field [1] string?
|
||||
---@field enabled boolean?
|
||||
---@field highlight string?
|
||||
|
||||
---@alias neotree.IconProvider fun(icon: neotree.Render.Node, node: NuiTree.Node, state: neotree.StateWithTree):(neotree.Render.Node|neotree.Render.Node[]|nil)
|
||||
|
|
@ -0,0 +1,151 @@
|
|||
---@meta
|
||||
|
||||
---@class neotree.Config.Mapping.Options
|
||||
---@field noremap boolean?
|
||||
---@field nowait boolean?
|
||||
---@field desc string?
|
||||
|
||||
---@class neotree.Config.Window.Command.Configured : neotree.Config.Mapping.Options
|
||||
---@field [1] string?
|
||||
---@field command string?
|
||||
---@field config table?
|
||||
|
||||
---@class neotree.Config.Source
|
||||
---@field window neotree.Config.Window?
|
||||
---@field renderers neotree.Config.Renderers?
|
||||
---@field commands table<string, neotree.Config.TreeCommand?>?
|
||||
---@field before_render fun(state: neotree.State)?
|
||||
|
||||
---@class neotree.Config.SourceSelector.Item
|
||||
---@field source string?
|
||||
---@field padding integer|{left:integer,right:integer}?
|
||||
---@field separator string|{left:string,right:string, override?:string}?
|
||||
|
||||
---@alias neotree.Config.SourceSelector.Separator.Override
|
||||
---|"right" # When right and left separators meet, only show the right one.
|
||||
---|"left" # When right and left separators meet, only show the left one.
|
||||
---|"active" # Only use the left separator on the left of the active tab, and only the right afterwards.
|
||||
---|nil # Show both separators.
|
||||
|
||||
---@class neotree.Config.SourceSelector.Separator
|
||||
---@field left string?
|
||||
---@field right string?
|
||||
---@field override neotree.Config.SourceSelector.Separator.Override?
|
||||
|
||||
---@class neotree.Config.SourceSelector
|
||||
---@field winbar boolean?
|
||||
---@field statusline boolean?
|
||||
---@field show_scrolled_off_parent_node boolean?
|
||||
---@field sources neotree.Config.SourceSelector.Item[]?
|
||||
---@field content_layout? "start"|"end"|"center"
|
||||
---@field tabs_layout? "equal"|"start"|"end"|"center"|"focus"
|
||||
---@field truncation_character string
|
||||
---@field tabs_min_width integer?
|
||||
---@field tabs_max_width integer?
|
||||
---@field padding integer|{left: integer, right:integer}?
|
||||
---@field separator neotree.Config.SourceSelector.Separator?
|
||||
---@field separator_active neotree.Config.SourceSelector.Separator?
|
||||
---@field show_separator_on_edge boolean?
|
||||
---@field highlight_tab string?
|
||||
---@field highlight_tab_active string?
|
||||
---@field highlight_background string?
|
||||
---@field highlight_separator string?
|
||||
---@field highlight_separator_active string?
|
||||
|
||||
---@class neotree.Config.GitStatusAsync
|
||||
---@field batch_size integer?
|
||||
---@field batch_delay integer?
|
||||
---@field max_lines integer?
|
||||
|
||||
---@class neotree.Config.Window.Size
|
||||
---@field height string|number?
|
||||
---@field width string|number?
|
||||
|
||||
---@class neotree.Config.Window.Popup
|
||||
---@field title (fun(state:table):string)?
|
||||
---@field size neotree.Config.Window.Size?
|
||||
---@field border neotree.Config.BorderStyle?
|
||||
|
||||
---@alias neotree.Config.TreeCommand string|neotree.TreeCommand|neotree.Config.Window.Command.Configured
|
||||
|
||||
---@class (exact) neotree.Config.Commands
|
||||
---@field [string] function
|
||||
|
||||
---@class (exact) neotree.Config.Window.Mappings
|
||||
---@field [string] neotree.Config.TreeCommand?
|
||||
|
||||
---@class neotree.Config.Window
|
||||
---@field position string?
|
||||
---@field width integer?
|
||||
---@field height integer?
|
||||
---@field auto_expand_width boolean?
|
||||
---@field popup neotree.Config.Window.Popup?
|
||||
---@field insert_as "child"|"sibling"|nil
|
||||
---@field mapping_options neotree.Config.Mapping.Options?
|
||||
---@field mappings neotree.Config.Window.Mappings?
|
||||
|
||||
---@class neotree.Config.Renderers
|
||||
---@field directory neotree.Component.Common[]?
|
||||
---@field file neotree.Component.Common[]?
|
||||
---@field message neotree.Component.Common[]?
|
||||
---@field terminal neotree.Component.Common[]?
|
||||
|
||||
---@class neotree.Config.ComponentDefaults
|
||||
---@field container neotree.Component.Common.Container?
|
||||
---@field indent neotree.Component.Common.Indent?
|
||||
---@field icon neotree.Component.Common.Icon?
|
||||
---@field modified neotree.Component.Common.Modified?
|
||||
---@field name neotree.Component.Common.Name?
|
||||
---@field git_status neotree.Component.Common.GitStatus?
|
||||
---@field file_size neotree.Component.Common.FileSize?
|
||||
---@field type neotree.Component.Common.Type?
|
||||
---@field last_modified neotree.Component.Common.LastModified?
|
||||
---@field created neotree.Component.Common.Created?
|
||||
---@field symlink_target neotree.Component.Common.SymlinkTarget?
|
||||
|
||||
---@alias neotree.Config.BorderStyle "NC"|"rounded"|"single"|"solid"|"double"|""
|
||||
|
||||
---@alias neotree.Config.SortFunction fun(a: NuiTree.Node, b: NuiTree.Node):boolean?
|
||||
|
||||
---@class (exact) neotree.Config.Base
|
||||
---@field sources string[]
|
||||
---@field add_blank_line_at_top boolean
|
||||
---@field auto_clean_after_session_restore boolean
|
||||
---@field close_if_last_window boolean
|
||||
---@field default_source string
|
||||
---@field enable_diagnostics boolean
|
||||
---@field enable_git_status boolean
|
||||
---@field enable_modified_markers boolean
|
||||
---@field enable_opened_markers boolean
|
||||
---@field enable_refresh_on_write boolean
|
||||
---@field enable_cursor_hijack boolean
|
||||
---@field git_status_async boolean
|
||||
---@field git_status_async_options neotree.Config.GitStatusAsync
|
||||
---@field hide_root_node boolean
|
||||
---@field retain_hidden_root_indent boolean
|
||||
---@field log_level "trace"|"debug"|"info"|"warn"|"error"|"fatal"|nil
|
||||
---@field log_to_file boolean|string
|
||||
---@field open_files_in_last_window boolean
|
||||
---@field open_files_do_not_replace_types string[]
|
||||
---@field open_files_using_relative_paths boolean
|
||||
---@field popup_border_style neotree.Config.BorderStyle
|
||||
---@field resize_timer_interval integer|-1
|
||||
---@field sort_case_insensitive boolean
|
||||
---@field sort_function? neotree.Config.SortFunction
|
||||
---@field use_popups_for_input boolean
|
||||
---@field use_default_mappings boolean
|
||||
---@field source_selector neotree.Config.SourceSelector
|
||||
---@field event_handlers? neotree.event.Handler[]
|
||||
---@field default_component_configs neotree.Config.ComponentDefaults
|
||||
---@field renderers neotree.Config.Renderers
|
||||
---@field nesting_rules neotree.filenesting.Rule[]
|
||||
---@field commands table<string, neotree.Config.TreeCommand?>
|
||||
---@field window neotree.Config.Window
|
||||
---
|
||||
---@field filesystem neotree.Config.Filesystem
|
||||
---@field buffers neotree.Config.Buffers
|
||||
---@field git_status neotree.Config.GitStatus
|
||||
---@field document_symbols neotree.Config.DocumentSymbols
|
||||
---@field bind_to_cwd boolean?
|
||||
|
||||
---@class (partial) neotree.Config : neotree.Config.Base
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
---@meta
|
||||
|
||||
---@enum neotree.EventName
|
||||
local _ = {
|
||||
AFTER_RENDER = "after_render",
|
||||
BEFORE_FILE_ADD = "before_file_add",
|
||||
BEFORE_FILE_DELETE = "before_file_delete",
|
||||
BEFORE_FILE_MOVE = "before_file_move",
|
||||
BEFORE_FILE_RENAME = "before_file_rename",
|
||||
BEFORE_RENDER = "before_render",
|
||||
FILE_ADDED = "file_added",
|
||||
FILE_DELETED = "file_deleted",
|
||||
FILE_MOVED = "file_moved",
|
||||
FILE_OPENED = "file_opened",
|
||||
FILE_OPEN_REQUESTED = "file_open_requested",
|
||||
FILE_RENAMED = "file_renamed",
|
||||
FS_EVENT = "fs_event",
|
||||
GIT_EVENT = "git_event",
|
||||
GIT_STATUS_CHANGED = "git_status_changed",
|
||||
STATE_CREATED = "state_created",
|
||||
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",
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
---@meta
|
||||
--- A backport from nightly for v0.10 type checking
|
||||
|
||||
--- @class neotree._vim.api.keyset.create_autocmd.callback_args
|
||||
--- @field id integer autocommand id
|
||||
--- @field event string name of the triggered event |autocmd-events|
|
||||
--- @field group? integer autocommand group id, if any
|
||||
--- @field match string expanded value of <amatch>
|
||||
--- @field buf integer expanded value of <abuf>
|
||||
--- @field file string expanded value of <afile>
|
||||
--- @field data? any arbitrary data passed from |nvim_exec_autocmds()| *event-data*
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
---@meta
|
||||
|
||||
---@class uv
|
||||
---@field constants {O_RDONLY: integer, O_WRONLY: integer, O_RDWR: integer, O_APPEND: integer, O_CREAT: integer, O_DSYNC: integer, O_EXCL: integer, O_NOCTTY: integer, O_NONBLOCK: integer, O_RSYNC: integer, O_SYNC: integer, O_TRUNC: integer, SOCK_STREAM: integer, SOCK_DGRAM: integer, SOCK_SEQPACKET: integer, SOCK_RAW: integer, SOCK_RDM: integer, AF_UNIX: integer, AF_INET: integer, AF_INET6: integer, AF_IPX: integer, AF_NETLINK: integer, AF_X25: integer, AF_AX25: integer, AF_ATMPVC: integer, AF_APPLETALK: integer, AF_PACKET: integer, AI_ADDRCONFIG: integer, AI_V4MAPPED: integer, AI_ALL: integer, AI_NUMERICHOST: integer, AI_PASSIVE: integer, AI_NUMERICSERV: integer, SIGHUP: integer, SIGINT: integer, SIGQUIT: integer, SIGILL: integer, SIGTRAP: integer, SIGABRT: integer, SIGIOT: integer, SIGBUS: integer, SIGFPE: integer, SIGKILL: integer, SIGUSR1: integer, SIGSEGV: integer, SIGUSR2: integer, SIGPIPE: integer, SIGALRM: integer, SIGTERM: integer, SIGCHLD: integer, SIGSTKFLT: integer, SIGCONT: integer, SIGSTOP: integer, SIGTSTP: integer, SIGTTIN: integer, SIGWINCH: integer, SIGIO: integer, SIGPOLL: integer, SIGXFSZ: integer, SIGVTALRM: integer, SIGPROF: integer, UDP_RECVMMSG: integer, UDP_MMSG_CHUNK: integer, UDP_REUSEADDR: integer, UDP_PARTIAL: integer, UDP_IPV6ONLY: integer, TCP_IPV6ONLY: integer, UDP_MMSG_FREE: integer, SIGSYS: integer, SIGPWR: integer, SIGTTOU: integer, SIGURG: integer, SIGXCPU: integer}
|
||||
local uv = {}
|
||||
|
||||
--- Opens path as a directory stream. Returns a handle that the user can pass to
|
||||
--- `uv.fs_readdir()`. The `entries` parameter defines the maximum number of entries
|
||||
--- that should be returned by each call to `uv.fs_readdir()`.
|
||||
---
|
||||
--- **Returns (sync version):** `luv_dir_t userdata` or `fail`
|
||||
---
|
||||
--- **Returns (async version):** `uv_fs_t userdata`
|
||||
---
|
||||
---@param path string
|
||||
---@param callback nil
|
||||
---@param entries integer?
|
||||
---@return uv.luv_dir_t|nil dir
|
||||
---@return uv.error.message|nil err
|
||||
---@return uv.error.name|nil err_name
|
||||
---
|
||||
---@overload fun(path: string, callback: uv.fs_opendir.callback, entries?: integer):uv.uv_fs_t
|
||||
function uv.fs_opendir(path, callback, entries) end
|
||||
|
|
@ -0,0 +1,324 @@
|
|||
local log = require("neo-tree.log")
|
||||
local utils = require("neo-tree.utils")
|
||||
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.EXPANDER = "NeoTreeExpander"
|
||||
M.WINDOWS_HIDDEN = "NeoTreeWindowsHidden"
|
||||
M.PREVIEW = "NeoTreePreview"
|
||||
|
||||
---@param n integer
|
||||
---@param chars integer?
|
||||
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
|
||||
|
||||
---@param name string
|
||||
local get_hl_by_name = function(name)
|
||||
if vim.api.nvim_get_hl then
|
||||
local hl = vim.api.nvim_get_hl(0, { name = name })
|
||||
---@diagnostic disable-next-line: inject-field
|
||||
hl.foreground = hl.fg
|
||||
---@diagnostic disable-next-line: inject-field
|
||||
hl.background = hl.bg
|
||||
return hl
|
||||
end
|
||||
---TODO: remove in 4.0
|
||||
---@diagnostic disable-next-line: deprecated
|
||||
return vim.api.nvim_get_hl_by_name(name, true)
|
||||
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 string[] A list of highlight groups to link to, in order of priority. The first one that exists will be used.
|
||||
---@param background string? 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? The foreground color to use, in hex, if the highlight group is not defined and it is not linked to another group.
|
||||
---@param gui string? The gui to use, if the highlight group is not defined and it is not linked to another group.
|
||||
---@return table hlgroups The highlight group values.
|
||||
M.create_highlight_group = function(hl_group_name, link_to_if_exists, background, foreground, gui)
|
||||
local success, hl_group = pcall(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(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
|
||||
|
||||
---@param hl_group_name string
|
||||
---@param fade_percentage number
|
||||
local calculate_faded_highlight_group = function(hl_group_name, fade_percentage)
|
||||
local normal = get_hl_by_name("Normal")
|
||||
if type(normal.foreground) ~= "number" then
|
||||
if vim.go.background == "dark" then
|
||||
normal.foreground = 0xffffff
|
||||
else
|
||||
normal.foreground = 0x000000
|
||||
end
|
||||
end
|
||||
if type(normal.background) ~= "number" then
|
||||
if vim.go.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 = get_hl_by_name(hl_group_name)
|
||||
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
|
||||
|
||||
local hl
|
||||
if #gui > 0 then
|
||||
hl = table.concat(gui, ",")
|
||||
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 = hl,
|
||||
}
|
||||
end
|
||||
|
||||
local faded_highlight_group_cache = {}
|
||||
---@param hl_group_name string
|
||||
---@param fade_percentage number
|
||||
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
|
||||
local nvim_0_10 = vim.fn.has("nvim-0.10")
|
||||
M.setup = function()
|
||||
local added_hl_name = nvim_0_10 and "Added" or "diffAdded"
|
||||
local changed_hl_name = nvim_0_10 and "Changed" or "diffChanged"
|
||||
local removed_hl_name = nvim_0_10 and "Removed" or "diffRemoved"
|
||||
-- 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", added_hl_name },
|
||||
nil,
|
||||
"5faf5f"
|
||||
)
|
||||
M.create_highlight_group(
|
||||
M.GIT_DELETED,
|
||||
{ "GitGutterDelete", "GitSignsDelete", removed_hl_name },
|
||||
nil,
|
||||
"ff5900"
|
||||
)
|
||||
M.create_highlight_group(
|
||||
M.GIT_MODIFIED,
|
||||
{ "GitGutterChange", "GitSignsChange", changed_hl_name },
|
||||
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,109 @@
|
|||
local NuiInput = require("nui.input")
|
||||
local nt = require("neo-tree")
|
||||
local popups = require("neo-tree.ui.popups")
|
||||
local events = require("neo-tree.events")
|
||||
|
||||
local M = {}
|
||||
|
||||
---@param input NuiInput
|
||||
---@param callback function?
|
||||
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
|
||||
|
||||
---@param message string
|
||||
---@param default_value string?
|
||||
---@param callback function
|
||||
---@param options nui_popup_options?
|
||||
---@param completion string?
|
||||
M.input = function(message, default_value, callback, options, completion)
|
||||
if nt.config.use_popups_for_input then
|
||||
local popup_options = popups.popup_options(message, 10, options)
|
||||
|
||||
local input = NuiInput(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 except 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
|
||||
|
||||
---Blocks if callback is omitted
|
||||
---@param message string
|
||||
---@param callback? fun(confirmed: boolean)
|
||||
---@return boolean? confirmed_if_no_callback
|
||||
M.confirm = function(message, callback)
|
||||
if callback then
|
||||
if nt.config.use_popups_for_input then
|
||||
local popup_options = popups.popup_options(message, 10)
|
||||
|
||||
---@class NuiInput
|
||||
local input = NuiInput(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
|
||||
else
|
||||
return vim.fn.confirm(message, "&Yes\n&No") == 1
|
||||
end
|
||||
end
|
||||
|
||||
return M
|
||||
|
|
@ -0,0 +1,147 @@
|
|||
local NuiText = require("nui.text")
|
||||
local NuiPopup = require("nui.popup")
|
||||
local nt = require("neo-tree")
|
||||
local highlights = require("neo-tree.ui.highlights")
|
||||
local log = require("neo-tree.log")
|
||||
|
||||
local M = {}
|
||||
|
||||
local winborder_option_exists = vim.fn.exists("&winborder") > 0
|
||||
-- These borders will cause errors when trying to display border text with them
|
||||
local invalid_borders = { "", "none", "shadow" }
|
||||
---@param title string
|
||||
---@param min_width integer?
|
||||
---@param override_options table?
|
||||
M.popup_options = function(title, min_width, override_options)
|
||||
if string.len(title) ~= 0 then
|
||||
title = " " .. title .. " "
|
||||
end
|
||||
min_width = min_width or 30
|
||||
local width = string.len(title) + 2
|
||||
|
||||
local popup_border_style = nt.config.popup_border_style
|
||||
if popup_border_style == "" then
|
||||
-- Try to use winborder
|
||||
if not winborder_option_exists or vim.tbl_contains(invalid_borders, vim.o.winborder) then
|
||||
popup_border_style = "single"
|
||||
else
|
||||
---@diagnostic disable-next-line: cast-local-type
|
||||
popup_border_style = vim.o.winborder
|
||||
end
|
||||
end
|
||||
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
|
||||
---@type nui_popup_options
|
||||
local popup_options = {
|
||||
ns_id = highlights.ns_id,
|
||||
relative = "cursor",
|
||||
position = {
|
||||
row = 1,
|
||||
col = col,
|
||||
},
|
||||
size = width,
|
||||
border = {
|
||||
text = {
|
||||
top = popup_border_text,
|
||||
},
|
||||
---@diagnostic disable-next-line: assign-type-mismatch
|
||||
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
|
||||
|
||||
---@param title string
|
||||
---@param message elem_or_list<string|integer>
|
||||
---@param size integer?
|
||||
M.alert = function(title, message, size)
|
||||
local lines = {}
|
||||
local max_line_width = title:len()
|
||||
---@param line any
|
||||
local add_line = function(line)
|
||||
line = tostring(line)
|
||||
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()
|
||||
win:unmount()
|
||||
end, { noremap = true })
|
||||
|
||||
win:map("n", "<enter>", function()
|
||||
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,405 @@
|
|||
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
|
||||
|
||||
---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 (utils.truncate_by_cell(text, output_width))
|
||||
else
|
||||
return (utils.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 = assert(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 neotree.State:
|
||||
---@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 = config.source_selector.padding
|
||||
local padding
|
||||
if type(padding_config) == "number" then
|
||||
padding = { left = padding_config, right = padding_config }
|
||||
else
|
||||
padding = padding_config or { left = 0, right = 0 }
|
||||
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 or {}
|
||||
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 or "equal"
|
||||
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 neotree.State: 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 or {}
|
||||
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 = {}
|
||||
|
||||
locations.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 locations
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
local compat = {}
|
||||
---@return boolean
|
||||
compat.noref = function()
|
||||
return vim.fn.has("nvim-0.10") == 1 and true or {} --[[@as boolean]]
|
||||
end
|
||||
|
||||
---source: https://github.com/Validark/Lua-table-functions/blob/master/table.lua
|
||||
---Moves elements [f, e] from array a1 into a2 starting at index t
|
||||
---table.move implementation
|
||||
---@generic T: table
|
||||
---@param a1 T from which to draw elements from range
|
||||
---@param f integer starting index for range
|
||||
---@param e integer ending index for range
|
||||
---@param t integer starting index to move elements from a1 within [f, e]
|
||||
---@param a2 T the second table to move these elements to
|
||||
---@default a2 = a1
|
||||
---@returns a2
|
||||
local table_move = function(a1, f, e, t, a2)
|
||||
a2 = a2 or a1
|
||||
t = t + e
|
||||
|
||||
for i = e, f, -1 do
|
||||
t = t - 1
|
||||
a2[t] = a1[i]
|
||||
end
|
||||
|
||||
return a2
|
||||
end
|
||||
---source:
|
||||
compat.table_move = table.move or table_move
|
||||
|
||||
---@vararg any
|
||||
local table_pack = function(...)
|
||||
-- Returns a new table with parameters stored into an array, with field "n" being the total number of parameters
|
||||
local t = { ... }
|
||||
---@diagnostic disable-next-line: inject-field
|
||||
t.n = #t
|
||||
return t
|
||||
end
|
||||
compat.table_pack = table.pack or table_pack
|
||||
|
||||
return compat
|
||||
|
|
@ -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,134 @@
|
|||
-- 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
Loading…
Add table
Add a link
Reference in a new issue