Skip to content
Merged
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
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,16 @@ require("neo-tree").setup({
nowait = true,
},
mappings = {
["<C-s>"] = {
"quick_jump",
config = {
-- This will automaticly open / toggle the target node after jumping.
-- You can set it to `nil` to perform only the jump action,
-- or write your own callback (---@type fun(state, node)).
on_jump = "open_or_toggle",
jump_labels = "jfkdlsahgnuvrbytmiceoxwpqz",
}
},
["<space>"] = {
"toggle_node",
nowait = false, -- disable `nowait` if you have existing combos starting with this char that you want to use
Expand Down
11 changes: 11 additions & 0 deletions doc/neo-tree.txt
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,17 @@ m = move: Move the selected file or directory.
Also accepts the optional `config.show_path` option
like the add file action.

<C-s> = quick_jump Enters a temporary "jump" mode where icons are
replaced with jump labels. Inputting the labels
will jump to the corresponding node, inputting
otherwise will exit the mode.

`config.jump_labels` sets what characters are used
for jump labels (prioritized in the order listed),
`config.on_jump` can either be "open_or_toggle"
(default) or `fun(state, target_node)`, where
`target_node` is the node that was jumped to.


VIEW CHANGES *neo-tree-view-changes*
H = toggle_hidden: Toggle whether hidden (filtered items) are shown or not.
Expand Down
12 changes: 11 additions & 1 deletion lua/neo-tree/defaults.lua
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,16 @@ local config = {
nowait = true,
},
mappings = {
["<C-s>"] = {
"quick_jump",
config = {
-- This will automaticly open / toggle the target node after jumping.
-- You can set it to `nil` to perform only the jump action,
-- or write your own callback function.
on_jump = "open_or_toggle",
jump_labels = "jfkdlsahgnuvrbytmiceoxwpqz",
}
},
["<space>"] = {
"toggle_node",
nowait = false, -- disable `nowait` if you have existing combos starting with this char that you want to use
Expand Down Expand Up @@ -766,7 +776,7 @@ local config = {
-- Parameter = { icon = ' ', hl = '@parameter' },
-- StaticMethod = { icon = '󰠄 ', hl = 'Function' },
-- Macro = { icon = ' ', hl = 'Macro' },
}
},
},
example = {
renderers = {
Expand Down
147 changes: 116 additions & 31 deletions lua/neo-tree/sources/common/commands.lua
Original file line number Diff line number Diff line change
Expand Up @@ -874,9 +874,10 @@ M.focus_preview = function(state)
end

---Expands or collapses the current node.
M.toggle_node = function(state, toggle_directory)
---@param toggle_directory fun(node: NuiTree.Node)?
M.toggle_node = function(state, toggle_directory, node)
local tree = state.tree
local node = assert(tree:get_node())
node = node or assert(tree:get_node())
if not utils.is_expandable(node) then
return
end
Expand Down Expand Up @@ -905,10 +906,9 @@ M.toggle_directory = function(state, toggle_directory)
M.toggle_node(state, toggle_directory)
end

---Open file or expandable node
---Open node under cursor
---@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
---@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)
Expand All @@ -917,28 +917,6 @@ local open_with_cmd = function(state, open_cmd, toggle_directory, open_file)
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)")
Expand All @@ -947,9 +925,25 @@ local open_with_cmd = function(state, open_cmd, toggle_directory, open_file)

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()
M.toggle_node(state, toggle_directory, node)
return
end

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
open_file = type(open_file) == "function" and open_file or utils.open_file
open_file(state, path, open_cmd, bufnr)
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

Expand Down Expand Up @@ -1039,7 +1033,6 @@ local use_window_picker = function(state, path, cmd)
)
return
end
local events = require("neo-tree.events")
local event_result = events.fire_event(events.FILE_OPEN_REQUESTED, {
state = state,
path = path,
Expand Down Expand Up @@ -1079,6 +1072,98 @@ M.vsplit_with_window_picker = function(state, toggle_directory)
open_with_cmd(state, "vsplit", toggle_directory, use_window_picker)
end

-- Jump to any node by two characters.
---@param state neotree.sources.filesystem.State
M.quick_jump = function(state, toggle_directory)
local nodes = renderer.get_all_visible_nodes(state.tree)
local quick_jump_utils = require("neo-tree.sources.common.utils.quick-jump")

local jump_labels = state.config.jump_labels
local node2key = quick_jump_utils.assign_hotkeys(nodes, jump_labels)
local is_document_symbols = state.name == "document_symbols"
local icon = is_document_symbols and state.components.kind_icon or state.components.icon

-- Recover all icons.
local recover = function()
if is_document_symbols then
state.components.kind_icon = icon
else
state.components.icon = icon
end
renderer.redraw(state)
end

-- render hotkeys
local icon_from_map = function(map)
return function(config, n, s)
local key = map[n]
if key ~= nil then
return {
text = key,
highlight = "NeoTreeFileIcon",
}
end
return icon(config, n, s)
end
end

local redraw_jump_icons = function()
if is_document_symbols then
---@diagnostic disable-next-line: assign-type-mismatch
state.components.kind_icon = icon_from_map(node2key)
else
---@diagnostic disable-next-line: assign-type-mismatch
state.components.icon = icon_from_map(node2key)
end
renderer.redraw(state)
vim.cmd("redraw")
end

redraw_jump_icons()

local depth = 1
while true do
local ok, key = pcall(vim.fn.getchar)
if not ok or type(key) ~= "number" then
break
end

local ch = vim.fn.nr2char(key)
if not ch:match("[A-Za-z]") then
break
end

local candidate = quick_jump_utils.get_candidate(node2key, ch, depth)
local n = vim.tbl_count(candidate)
if n == 0 then
log.info("Exiting jump mode as no candidates were found for the given key")
break
end

if n > 1 or depth == 1 then
node2key = candidate
depth = depth + 1
redraw_jump_icons()
else
local target_node = assert(next(candidate), "expected 1 candidate but got none")

assert(
renderer.focus_node(state, target_node:get_id()),
("Could not focus node %s for quick_jump"):format(target_node.id)
)
local on_jump = state.config.on_jump
if type(on_jump) == "function" then
on_jump(state, target_node)
elseif on_jump == "open_or_toggle" then
open_with_cmd(state, "e", toggle_directory)
end
break
end
end

recover()
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
Expand Down
115 changes: 115 additions & 0 deletions lua/neo-tree/sources/common/utils/quick-jump.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
local M = {}

---@param first_char string
---@param count integer
---@param jump_labels string
---@return string hotkey
local compute_hotkey = function(first_char, count, jump_labels)
local labels = first_char .. string.gsub(jump_labels, first_char, "")
local labels_len = #labels

-- compute length
local length = 1
local prev = 0
local sum = labels_len ^ length
while sum < count do
length = length + 1
prev = sum
sum = sum + labels_len ^ length
end

local rest = count - prev - 1

-- generate hotkey
local hotkey = first_char
local q
while length > 0 do
q = math.floor(rest / (labels_len ^ (length - 1)) + 1)
hotkey = hotkey .. string.sub(labels, q, q)
rest = rest % (labels_len ^ (length - 1))
length = length - 1
end

return hotkey
end

---@param name string
---@return string c
local first_char_in_filename = function(name)
if type(name) ~= "string" then
return "j"
end

local c = string.match(name, "[A-Za-z]")
if c then
return string.lower(c)
end

return "j"
end

-- Return key - node pairs whose key starts by ch.
---@param node2key table<neotree.FileNode, string>
---@param ch string
---@param depth integer
---@return table<neotree.FileNode, string> candidate
M.get_candidate = function(node2key, ch, depth)
local candidate = {}
for node, key in pairs(node2key) do
if #key >= depth then
local fst = string.sub(key, depth, depth)
if fst == ch then
candidate[node] = key
end
end
end
return candidate
end

local byte_to_index_offset = string.byte("a") - 1
---@param node neotree.FileNode
local assign_hotkey = function(node, first_charbyte_counters, jump_labels)
local first_char = first_char_in_filename(node.name)
local first_char_byte = first_char:byte()
local count = first_charbyte_counters[first_char_byte - byte_to_index_offset]
local hotkey = compute_hotkey(first_char, count, jump_labels)
first_charbyte_counters[first_char_byte - byte_to_index_offset] = count + 1
return hotkey
end

-- Generate hotkeys map.
-- Hotkeys will take the first letter of the node name to be the leader,
-- and assign the rest according to the priority of the jump labels in the config.
-- The length is computed dynamiclly.
-- It will be like {leader}{label_1}{label_2}{label_3}......
---@param nodes neotree.FileNode[]
---@param jump_labels string
---@return table<neotree.FileNode, string> node2key
M.assign_hotkeys = function(nodes, jump_labels)
local node2key = {}

local first_charbyte_counters = {}
for c = string.byte("a"), string.byte("z") do
first_charbyte_counters[c - byte_to_index_offset] = 1
end

-- Assign opened buffers more convenient keys.
local opened_buffers = require("neo-tree.utils").get_opened_buffers()
local other_nodes = {}
for _, node in ipairs(nodes) do
if opened_buffers[node.name] ~= nil then
node2key[node] = assign_hotkey(node, first_charbyte_counters, jump_labels)
else
other_nodes[#other_nodes + 1] = node
end
end

-- Handle the rest.
for _, node in ipairs(other_nodes) do
node2key[node] = assign_hotkey(node, first_charbyte_counters, jump_labels)
end

return node2key
end

return M
5 changes: 5 additions & 0 deletions lua/neo-tree/sources/document_symbols/commands.lua
Original file line number Diff line number Diff line change
Expand Up @@ -96,5 +96,10 @@ 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$")
cc._add_common_commands(M, "quick_jump")

M.quick_jump = function(state)
cc.quick_jump(state)
end

return M
4 changes: 4 additions & 0 deletions lua/neo-tree/sources/filesystem/commands.lua
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,10 @@ M.vsplit_with_window_picker = function(state)
cc.vsplit_with_window_picker(state, utils.wrap(fs.toggle_directory, state))
end

M.quick_jump = function(state)
cc.quick_jump(state, utils.wrap(fs.toggle_directory, state))
end

M.refresh = refresh

M.rename = function(state)
Expand Down
2 changes: 1 addition & 1 deletion lua/neo-tree/sources/filesystem/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -492,7 +492,7 @@ end
---Expands or collapses the current node.
---@param state neotree.sources.filesystem.State
---@param node NuiTree.Node
---@param path_to_reveal string
---@param path_to_reveal string?
---@param skip_redraw boolean?
---@param recursive boolean?
---@param callback function?
Expand Down
Loading