Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
254 changes: 131 additions & 123 deletions doc/nvim-tree-lua.txt

Large diffs are not rendered by default.

6 changes: 5 additions & 1 deletion lua/nvim-tree/_meta/api/fs.lua
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ function nvim_tree.api.fs.copy.filename(node) end

---
---Copy to the nvim-tree clipboard.
---In visual mode, copies all nodes in the visual selection.
---
---@param node? nvim_tree.api.Node
function nvim_tree.api.fs.copy.node(node) end
Expand All @@ -52,6 +53,7 @@ function nvim_tree.api.fs.create(node) end

---
---Cut to the nvim-tree clipboard.
---In visual mode, cuts all nodes in the visual selection.
---
---@param node? nvim_tree.api.Node
function nvim_tree.api.fs.cut(node) end
Expand All @@ -71,6 +73,7 @@ function nvim_tree.api.fs.print_clipboard() end

---
---Delete from the file system.
---In visual mode, deletes all nodes in the visual selection with a single prompt.
---
---@param node? nvim_tree.api.Node
function nvim_tree.api.fs.remove(node) end
Expand Down Expand Up @@ -106,7 +109,8 @@ function nvim_tree.api.fs.rename_node(node) end
function nvim_tree.api.fs.rename_sub(node) end

---
---Trash as per |nvim_tree.config.trash|
---Trash as per |nvim_tree.config.trash|.
---In visual mode, trashes all nodes in the visual selection with a single prompt.
---
---@param node? nvim_tree.api.Node
function nvim_tree.api.fs.trash(node) end
Expand Down
2 changes: 1 addition & 1 deletion lua/nvim-tree/_meta/api/marks.lua
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ function nvim_tree.api.marks.get() end
function nvim_tree.api.marks.list() end

---
---Toggle mark.
---Toggle mark. In visual mode, toggles all nodes in the visual selection.
---
---@param node? nvim_tree.api.Node file or directory
function nvim_tree.api.marks.toggle(node) end
Expand Down
4 changes: 3 additions & 1 deletion lua/nvim-tree/api.lua
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@
---local api = require("nvim-tree.api")
---api.tree.reload()
---```
---Generally, functions accepting a [nvim_tree.api.Node] as their first argument will use the node under the cursor when that argument is not present or nil. e.g. the following are functionally identical:
---Generally, functions accepting a [nvim_tree.api.Node] as their first argument will use the node under the cursor when that argument is not present or nil. Some functions are mode-dependent: when invoked in visual mode they will operate on all nodes in the visual selection instead of a single node. See |nvim-tree-mappings-default| for which mappings support visual mode.
---
---e.g. the following are functionally identical:
---```lua
---
---api.node.open.edit(nil, { focus = true })
Expand Down
102 changes: 95 additions & 7 deletions lua/nvim-tree/api/impl/post.lua
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,90 @@ local function wrap_explorer_member(explorer_member, member_method)
end
end

---Check if the current mode is visual (v, V, or CTRL-V).
---@return boolean
local function is_visual_mode()
local mode = vim.api.nvim_get_mode().mode
return mode == "v" or mode == "V" or mode == "\22" -- \22 is CTRL-V
end

---Exit visual mode synchronously.
local function exit_visual_mode()
local esc = vim.api.nvim_replace_termcodes("<Esc>", true, false, true)
vim.api.nvim_feedkeys(esc, "nx", false)
end

---Get the visual selection range nodes, exiting visual mode.
---@return Node[]?
local function get_visual_nodes()
local explorer = require("nvim-tree.core").get_explorer()
if not explorer then
return nil
end
local start_line = vim.fn.line("v")
local end_line = vim.fn.line(".")
if start_line > end_line then
start_line, end_line = end_line, start_line
end
local nodes = explorer:get_nodes_in_range(start_line, end_line)
exit_visual_mode()
return nodes
end

---Wrap a single-node function to be mode-dependent: in visual mode, operate
---on all nodes in the visual range; in normal mode, operate on a single node.
---@param fn fun(node: Node, ...): any
---@param filter_descendants boolean? filter out descendant nodes in visual mode
---@return fun(node: Node?, ...): any
local function wrap_node_or_visual(fn, filter_descendants)
return function(node, ...)
if is_visual_mode() then
local nodes = get_visual_nodes()
if nodes then
if filter_descendants then
local explorer = require("nvim-tree.core").get_explorer()
if explorer then
nodes = explorer.marks:filter_descendant_nodes(nodes)
end
end
for _, n in ipairs(nodes) do
fn(n, ...)
end
end
else
node = node or wrap_explorer("get_node_at_cursor")()
if node then
return fn(node, ...)
end
end
end
end

---Wrap a destructive operation to be mode-dependent: in visual mode, collect
---nodes and call a bulk method; in normal mode, call the single-node function.
---@param normal_fn fun(node: Node): any
---@param bulk_member string explorer member name for bulk op
---@param bulk_method string method name on member for bulk op
---@return fun(node: Node?): any
local function wrap_node_or_visual_bulk(normal_fn, bulk_member, bulk_method)
return function(node)
if is_visual_mode() then
local nodes = get_visual_nodes()
if nodes then
local explorer = require("nvim-tree.core").get_explorer()
if explorer then
explorer[bulk_member][bulk_method](explorer[bulk_member], nodes)
end
end
else
node = node or wrap_explorer("get_node_at_cursor")()
if node then
return normal_fn(node)
end
end
end
end

---@class NodeEditOpts
---@field quit_on_open boolean|nil default false
---@field focus boolean|nil default true
Expand Down Expand Up @@ -172,18 +256,18 @@ function M.hydrate(api)
api.tree.winid = view.winid

api.fs.create = wrap_node_or_nil(actions.fs.create_file.fn)
api.fs.remove = wrap_node(actions.fs.remove_file.fn)
api.fs.trash = wrap_node(actions.fs.trash.fn)
api.fs.remove = wrap_node_or_visual_bulk(actions.fs.remove_file.fn, "marks", "bulk_delete_nodes")
api.fs.trash = wrap_node_or_visual_bulk(actions.fs.trash.fn, "marks", "bulk_trash_nodes")
api.fs.rename_node = wrap_node(actions.fs.rename_file.fn(":t"))
api.fs.rename = wrap_node(actions.fs.rename_file.fn(":t"))
api.fs.rename_sub = wrap_node(actions.fs.rename_file.fn(":p:h"))
api.fs.rename_basename = wrap_node(actions.fs.rename_file.fn(":t:r"))
api.fs.rename_full = wrap_node(actions.fs.rename_file.fn(":p"))
api.fs.cut = wrap_node(wrap_explorer_member("clipboard", "cut"))
api.fs.cut = wrap_node_or_visual(wrap_explorer_member("clipboard", "cut"), true)
api.fs.paste = wrap_node(wrap_explorer_member("clipboard", "paste"))
api.fs.clear_clipboard = wrap_explorer_member("clipboard", "clear_clipboard")
api.fs.print_clipboard = wrap_explorer_member("clipboard", "print_clipboard")
api.fs.copy.node = wrap_node(wrap_explorer_member("clipboard", "copy"))
api.fs.copy.node = wrap_node_or_visual(wrap_explorer_member("clipboard", "copy"), true)
api.fs.copy.absolute_path = wrap_node(wrap_explorer_member("clipboard", "copy_absolute_path"))
api.fs.copy.filename = wrap_node(wrap_explorer_member("clipboard", "copy_filename"))
api.fs.copy.basename = wrap_node(wrap_explorer_member("clipboard", "copy_basename"))
Expand Down Expand Up @@ -229,8 +313,12 @@ function M.hydrate(api)
api.node.expand = wrap_node(wrap_explorer("expand_node"))
api.node.collapse = wrap_node(actions.tree.collapse.node)

api.node.buffer.delete = wrap_node(function(node, opts) actions.node.buffer.delete(node, opts) end)
api.node.buffer.wipe = wrap_node(function(node, opts) actions.node.buffer.wipe(node, opts) end)
api.node.buffer.delete = wrap_node(function(node, opts)
actions.node.buffer.delete(node, opts)
end)
api.node.buffer.wipe = wrap_node(function(node, opts)
actions.node.buffer.wipe(node, opts)
end)

api.tree.reload_git = wrap_explorer("reload_git")

Expand All @@ -246,7 +334,7 @@ function M.hydrate(api)

api.marks.get = wrap_node(wrap_explorer_member("marks", "get"))
api.marks.list = wrap_explorer_member("marks", "list")
api.marks.toggle = wrap_node(wrap_explorer_member("marks", "toggle"))
api.marks.toggle = wrap_node_or_visual(wrap_explorer_member("marks", "toggle"))
api.marks.clear = wrap_explorer_member("marks", "clear")
api.marks.bulk.delete = wrap_explorer_member("marks", "bulk_delete")
api.marks.bulk.trash = wrap_explorer_member("marks", "bulk_trash")
Expand Down
16 changes: 16 additions & 0 deletions lua/nvim-tree/explorer/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -642,6 +642,22 @@ function Explorer:find_node(fn)
return node, i
end

---Get all nodes in a line range (inclusive), for visual selection operations.
---@param start_line integer
---@param end_line integer
---@return Node[]
function Explorer:get_nodes_in_range(start_line, end_line)
local nodes_by_line = self:get_nodes_by_line(core.get_nodes_starting_line())
local nodes = {}
for line = start_line, end_line do
local node = nodes_by_line[line]
if node and node.absolute_path then
table.insert(nodes, node)
end
end
return nodes
end

--- Return visible nodes indexed by line
---@param line_start number
---@return table
Expand Down
35 changes: 24 additions & 11 deletions lua/nvim-tree/help.lua
Original file line number Diff line number Diff line change
Expand Up @@ -92,10 +92,21 @@ local function compute(map)
local head_rhs1 = "exit: q"
local head_rhs2 = string.format("sort by %s: s", M.config.sort_by == "key" and "description" or "keymap")

-- formatted lhs and desc from active keymap
local mappings = vim.tbl_map(function(m)
return { lhs = tidy_lhs(m.lhs), desc = tidy_desc(m.desc) }
end, map)
-- merge modes for duplicate lhs+desc entries e.g. "n" + "x" -> "nx"
local merged = {}
local mappings = {}
for _, m in ipairs(map) do
local lhs = tidy_lhs(m.lhs)
local desc = tidy_desc(m.desc)
local key = lhs .. "\0" .. desc
if merged[key] then
merged[key].mode = merged[key].mode .. m.mode
else
local entry = { lhs = lhs, desc = desc, mode = m.mode or "n" }
merged[key] = entry
table.insert(mappings, entry)
end
end

-- sorter function for mappings
local sort_fn
Expand All @@ -113,21 +124,23 @@ local function compute(map)

table.sort(mappings, sort_fn)

-- longest lhs and description
-- longest lhs, mode and description
local max_lhs = 0
local max_mode = 0
local max_desc = 0
for _, l in pairs(mappings) do
for _, l in ipairs(mappings) do
max_lhs = math.max(#l.lhs, max_lhs)
max_mode = math.max(#l.mode, max_mode)
max_desc = math.max(#l.desc, max_desc)
end

-- increase desc if lines are shorter than the header
max_desc = math.max(max_desc, #head_lhs + #head_rhs1 - max_lhs)
max_desc = math.max(max_desc, #head_lhs + #head_rhs1 - max_lhs - max_mode)

-- header text, not padded
local lines = {
head_lhs .. string.rep(" ", max_desc + max_lhs - #head_lhs - #head_rhs1 + 2) .. head_rhs1,
string.rep(" ", max_desc + max_lhs - #head_rhs2 + 2) .. head_rhs2,
head_lhs .. string.rep(" ", max_lhs + max_mode + max_desc - #head_lhs - #head_rhs1 + 3) .. head_rhs1,
string.rep(" ", max_lhs + max_mode + max_desc - #head_rhs2 + 3) .. head_rhs2,
}
local width = #lines[1]

Expand All @@ -139,10 +152,10 @@ local function compute(map)
}

-- mappings, left padded 1
local fmt = string.format(" %%-%ds %%-%ds", max_lhs, max_desc)
local fmt = string.format(" %%-%ds %%-%ds %%-%ds", max_lhs, max_mode, max_desc)
for i, l in ipairs(mappings) do
-- format in left aligned columns
local line = string.format(fmt, l.lhs, l.desc)
local line = string.format(fmt, l.lhs, l.mode, l.desc)
table.insert(lines, line)
width = math.max(#line, width)

Expand Down
Loading