Skip to content

Commit 8c89efb

Browse files
authored
feat(document_symbol): added filtering (#932)
1 parent e5594d5 commit 8c89efb

File tree

15 files changed

+480
-137
lines changed

15 files changed

+480
-137
lines changed

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -624,7 +624,7 @@ following features:
624624
- [ ] Call hierarchy
625625
- [x] LSP
626626
- [x] LSP Support
627-
- [x] LSP server selection (blacklist, use first, use all, etc.)
627+
- [x] LSP server selection (ignore, allow_only, use first, use all, etc.)
628628
- [ ] CoC Support
629629

630630
See #879 for the tracking issue of these features.

doc/neo-tree.txt

+6-6
Original file line numberDiff line numberDiff line change
@@ -1723,18 +1723,18 @@ symbols. This accepts one of the following values
17231723

17241724
`"first"`: use the first LSP server that provides the feature
17251725
`"all"`: use all LSP server that provides the feature
1726-
`{ fn = function(name), white_list = table, black_list = table }` where
1726+
`{ fn = function(name), allow_only = table, ignore = table }` where
17271727
`fn`: a function that returns `true` if the server `name` should be used
1728-
`white_list`: use only servers from this list
1729-
`black_list`: exclude all servers from this list
1730-
NOTE: `fn` preceeds `white_list` preceeds `black_list`
1728+
`allow_only`: use only servers from this list
1729+
`ignore`: exclude all servers from this list
1730+
NOTE: `fn` preceeds `allow_only` preceeds `ignore`
17311731

17321732
For example: (NOTE: here only `fn` will be taken into account)
17331733
>lua
17341734
{
17351735
fn = function(name) return name ~= "null-ls" end,
1736-
white_list = { "clangd", "lua_ls" },
1737-
black_list = { "pyright" },
1736+
allow_only = { "clangd", "lua_ls" },
1737+
ignore = { "pyright" },
17381738
}
17391739
<
17401740
Currently, this source supports the following commands:

lua/neo-tree/defaults.lua

+2
Original file line numberDiff line numberDiff line change
@@ -545,6 +545,8 @@ local config = {
545545
["c"] = "noop",
546546
["m"] = "noop",
547547
["a"] = "noop",
548+
["/"] = "filter",
549+
["f"] = "filter_on_submit",
548550
},
549551
},
550552
custom_kinds = {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
---A generalization of the filter functionality to directly filter the
2+
---source tree instead of relying on pre-filtered data, which is specific
3+
---to the filesystem source.
4+
local vim = vim
5+
local Input = require("nui.input")
6+
local event = require("nui.utils.autocmd").event
7+
local popups = require("neo-tree.ui.popups")
8+
local renderer = require("neo-tree.ui.renderer")
9+
local utils = require("neo-tree.utils")
10+
local log = require("neo-tree.log")
11+
local manager = require("neo-tree.sources.manager")
12+
local fzy = require("neo-tree.sources.common.filters.filter_fzy")
13+
14+
local M = {}
15+
16+
local cmds = {
17+
move_cursor_down = function(state, scroll_padding)
18+
renderer.focus_node(state, nil, true, 1, scroll_padding)
19+
end,
20+
21+
move_cursor_up = function(state, scroll_padding)
22+
renderer.focus_node(state, nil, true, -1, scroll_padding)
23+
vim.cmd("redraw!")
24+
end,
25+
}
26+
27+
---Reset the current filter to the empty string.
28+
---@param state any
29+
---@param refresh boolean? whether to refresh the source tree
30+
---@param open_current_node boolean? whether to open the current node
31+
local reset_filter = function(state, refresh, open_current_node)
32+
log.trace("reset_search")
33+
if refresh == nil then
34+
refresh = true
35+
end
36+
37+
-- Cancel any pending search
38+
require("neo-tree.sources.filesystem.lib.filter_external").cancel()
39+
40+
-- reset search state
41+
if state.open_folders_before_search then
42+
state.force_open_folders = vim.deepcopy(state.open_folders_before_search, { noref = 1 })
43+
else
44+
state.force_open_folders = nil
45+
end
46+
state.open_folders_before_search = nil
47+
state.search_pattern = nil
48+
49+
if open_current_node then
50+
local success, node = pcall(state.tree.get_node, state.tree)
51+
if success and node then
52+
local id = node:get_id()
53+
renderer.position.set(state, id)
54+
id = utils.remove_trailing_slash(id)
55+
manager.navigate(state, nil, id, utils.wrap(pcall, renderer.focus_node, state, id, false))
56+
end
57+
elseif refresh then
58+
manager.navigate(state)
59+
else
60+
state.tree = vim.deepcopy(state.orig_tree)
61+
end
62+
state.orig_tree = nil
63+
end
64+
65+
---Show the filtered tree
66+
---@param state any
67+
---@param do_not_focus_window boolean? whether to focus the window
68+
local show_filtered_tree = function(state, do_not_focus_window)
69+
state.tree = vim.deepcopy(state.orig_tree)
70+
state.tree:get_nodes()[1].search_pattern = state.search_pattern
71+
local max_score, max_id = fzy.get_score_min(), nil
72+
local function filter_tree(node_id)
73+
local node = state.tree:get_node(node_id)
74+
local path = node.extra.search_path or node.path
75+
76+
local should_keep = fzy.has_match(state.search_pattern, path)
77+
if should_keep then
78+
local score = fzy.score(state.search_pattern, path)
79+
node.extra.fzy_score = score
80+
if score > max_score then
81+
max_score = score
82+
max_id = node_id
83+
end
84+
end
85+
86+
if node:has_children() then
87+
for _, child_id in ipairs(node:get_child_ids()) do
88+
should_keep = filter_tree(child_id) or should_keep
89+
end
90+
end
91+
if not should_keep then
92+
state.tree:remove_node(node_id) -- TODO: this might not be efficient
93+
end
94+
return should_keep
95+
end
96+
if #state.search_pattern > 0 then
97+
for _, root in ipairs(state.tree:get_nodes()) do
98+
filter_tree(root:get_id())
99+
end
100+
end
101+
manager.redraw(state.name)
102+
if max_id then
103+
renderer.focus_node(state, max_id, do_not_focus_window)
104+
end
105+
end
106+
107+
---Main entry point for the filter functionality.
108+
---This will display a filter input popup and filter the source tree on change and on submit
109+
---@param state table the source state
110+
---@param search_as_you_type boolean? whether to filter as you type or only on submit
111+
---@param keep_filter_on_submit boolean? whether to keep the filter on <CR> or reset it
112+
M.show_filter = function(state, search_as_you_type, keep_filter_on_submit)
113+
local winid = vim.api.nvim_get_current_win()
114+
local height = vim.api.nvim_win_get_height(winid)
115+
local scroll_padding = 3
116+
117+
-- setup the input popup options
118+
local popup_msg = "Search:"
119+
if search_as_you_type then
120+
popup_msg = "Filter:"
121+
end
122+
123+
local width = vim.fn.winwidth(0) - 2
124+
local row = height - 3
125+
if state.current_position == "float" then
126+
scroll_padding = 0
127+
width = vim.fn.winwidth(winid)
128+
row = height - 2
129+
vim.api.nvim_win_set_height(winid, row)
130+
end
131+
132+
state.orig_tree = vim.deepcopy(state.tree)
133+
134+
local popup_options = popups.popup_options(popup_msg, width, {
135+
relative = "win",
136+
winid = winid,
137+
position = {
138+
row = row,
139+
col = 0,
140+
},
141+
size = width,
142+
})
143+
144+
local has_pre_search_folders = utils.truthy(state.open_folders_before_search)
145+
if not has_pre_search_folders then
146+
log.trace("No search or pre-search folders, recording pre-search folders now")
147+
state.open_folders_before_search = renderer.get_expanded_nodes(state.tree)
148+
end
149+
150+
local waiting_for_default_value = utils.truthy(state.search_pattern)
151+
local input = Input(popup_options, {
152+
prompt = " ",
153+
default_value = state.search_pattern,
154+
on_submit = function(value)
155+
if value == "" then
156+
reset_filter(state)
157+
return
158+
end
159+
if search_as_you_type and not keep_filter_on_submit then
160+
reset_filter(state, true, true)
161+
return
162+
end
163+
-- do the search
164+
state.search_pattern = value
165+
show_filtered_tree(state, false)
166+
end,
167+
--this can be bad in a deep folder structure
168+
on_change = function(value)
169+
if not search_as_you_type then
170+
return
171+
end
172+
-- apparently when a default value is set, on_change fires for every character
173+
if waiting_for_default_value then
174+
if #value < #state.search_pattern then
175+
return
176+
end
177+
waiting_for_default_value = false
178+
end
179+
if value == state.search_pattern or value == nil then
180+
return
181+
end
182+
183+
-- finally do the search
184+
log.trace("Setting search in on_change to: " .. value)
185+
state.search_pattern = value
186+
local len_to_delay = { [0] = 500, 500, 400, 200 }
187+
local delay = len_to_delay[#value] or 100
188+
189+
utils.debounce(state.name .. "_filter", function()
190+
show_filtered_tree(state, true)
191+
end, delay, utils.debounce_strategy.CALL_LAST_ONLY)
192+
end,
193+
})
194+
195+
input:mount()
196+
197+
local restore_height = vim.schedule_wrap(function()
198+
if vim.api.nvim_win_is_valid(winid) then
199+
vim.api.nvim_win_set_height(winid, height)
200+
end
201+
end)
202+
203+
-- create mappings and autocmd
204+
input:map("i", "<C-w>", "<C-S-w>", { noremap = true })
205+
input:map("i", "<esc>", function(bufnr)
206+
vim.cmd("stopinsert")
207+
input:unmount()
208+
if utils.truthy(state.search_pattern) then
209+
reset_filter(state, true)
210+
end
211+
restore_height()
212+
end, { noremap = true })
213+
214+
local config = require("neo-tree").config
215+
for lhs, cmd_name in pairs(config.filesystem.window.fuzzy_finder_mappings) do
216+
local cmd = cmds[cmd_name]
217+
if cmd then
218+
input:map("i", lhs, utils.wrap(cmd, state, scroll_padding), { noremap = true })
219+
else
220+
log.warn(string.format("Invalid command in fuzzy_finder_mappings: %s = %s", lhs, cmd_name))
221+
end
222+
end
223+
end
224+
225+
return M

lua/neo-tree/sources/document_symbols/commands.lua

+9
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ local cc = require("neo-tree.sources.common.commands")
33
local utils = require("neo-tree.utils")
44
local manager = require("neo-tree.sources.manager")
55
local inputs = require("neo-tree.ui.inputs")
6+
local filters = require("neo-tree.sources.common.filters")
67

78
local vim = vim
89

@@ -47,6 +48,14 @@ end
4748

4849
M.open = M.jump_to_symbol
4950

51+
M.filter_on_submit = function(state)
52+
filters.show_filter(state, true, true)
53+
end
54+
55+
M.filter = function(state)
56+
filters.show_filter(state, true)
57+
end
58+
5059
cc._add_common_commands(M, "node") -- common tree commands
5160
cc._add_common_commands(M, "^open") -- open commands
5261
cc._add_common_commands(M, "^close_window$")

lua/neo-tree/sources/document_symbols/init.lua

+6-1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ local get_state = function()
1717
return manager.get_state(M.name)
1818
end
1919

20+
---Refresh the source with debouncing
21+
---@param args { afile: string }
2022
local refresh_debounced = function(args)
2123
if utils.is_real_file(args.afile) == false then
2224
return
@@ -29,18 +31,21 @@ local refresh_debounced = function(args)
2931
)
3032
end
3133

34+
---Internal function to follow the cursor
3235
local follow_symbol = function()
3336
local state = get_state()
3437
if state.lsp_bufnr ~= vim.api.nvim_get_current_buf() then
3538
return
3639
end
3740
local cursor = vim.api.nvim_win_get_cursor(state.lsp_winid)
38-
local node_id = symbols.get_symbol_by_range(state.tree, { cursor[1] - 1, cursor[2] })
41+
local node_id = symbols.get_symbol_by_loc(state.tree, { cursor[1] - 1, cursor[2] })
3942
if #node_id > 0 then
4043
renderer.focus_node(state, node_id, true)
4144
end
4245
end
4346

47+
---Follow the cursor with debouncing
48+
---@param args { afile: string }
4449
local follow_debounced = function(args)
4550
if utils.is_real_file(args.afile) == false then
4651
return

0 commit comments

Comments
 (0)