Skip to content

Commit 7df3a9d

Browse files
committed
feat: nested treesj
- use `vim.v.count1` to select parents based on nesting depth - use `flash.nvim` to select parents using labels - dot repeat remembers the nesting depth (configurable)
1 parent dff18bb commit 7df3a9d

File tree

7 files changed

+202
-6
lines changed

7 files changed

+202
-6
lines changed

lua/treesj/format.lua

+33-2
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ function M.get_node_at_cursor(root_lang_tree)
4545
)
4646
end
4747

48-
function M._format(mode, override)
48+
function M._format(mode, override, selector)
4949
-- Tree reparsing is required, otherwise the tree may not be updated
5050
-- and each node will be processed only once (until
5151
-- the tree is updated). See issue #118
@@ -67,7 +67,7 @@ function M._format(mode, override)
6767

6868
-- If the node is marked as "disabled", continue searching from its parent.
6969
while true do
70-
found, tsn_data = pcall(search.get_configured_node, start_node)
70+
found, tsn_data = pcall(selector or search.get_configured_node, start_node)
7171
if not found then
7272
notify.warn(tsn_data)
7373
return
@@ -169,4 +169,35 @@ function M._format(mode, override)
169169
pcall(vim.api.nvim_win_set_cursor, 0, new_cursor)
170170
end
171171

172+
M.last_selected = nil
173+
function M._nested(selector, mode, preset)
174+
local nodes
175+
M._format(mode, preset, function(start_node)
176+
if not nodes then
177+
nodes = search.get_configured_nodes(start_node)
178+
end
179+
local selected
180+
if settings.remember_selected and M.last_selected then
181+
-- TODO: basic indexing is not ideal if the sticky cursor doesn't work?
182+
selected = nodes[M.last_selected]
183+
else
184+
if type(selector) == 'string' then
185+
selector = require('treesj.selectors.' .. selector).selector
186+
end
187+
local sel, index = selector(nodes)
188+
if sel then
189+
M.last_selected = index
190+
selected = sel
191+
end
192+
end
193+
if selected then
194+
return selected
195+
else
196+
-- TODO: change the error messages
197+
error(msg.no_chosen_node, 0)
198+
return
199+
end
200+
end)
201+
end
202+
172203
return M

lua/treesj/init.lua

+21-4
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,14 @@ local function repeatable(fn)
1515
if settings.settings.dot_repeat then
1616
M.__repeat = fn
1717
vim.opt.operatorfunc = "v:lua.require'treesj'.__repeat"
18-
vim.api.nvim_feedkeys('g@l', 'n', true)
18+
vim.api.nvim_feedkeys(vim.v.count1 .. 'g@l', 'n', true)
1919
else
2020
fn()
2121
end
2222
end
2323

2424
M.format = function(mode, preset)
25-
repeatable(function()
26-
require('treesj.format')._format(mode, preset)
27-
end)
25+
M.nested_format('count', mode, preset)
2826
end
2927

3028
M.toggle = function(preset)
@@ -39,4 +37,23 @@ M.split = function(preset)
3937
M.format('split', preset)
4038
end
4139

40+
M.nested_format = function(selector, mode, preset)
41+
require('treesj.format').last_selected = nil
42+
repeatable(function()
43+
require('treesj.format')._nested(selector, mode, preset)
44+
end)
45+
end
46+
47+
M.nested_toggle = function(selector, preset)
48+
M.nested_format(selector, nil, preset)
49+
end
50+
51+
M.nested_join = function(selector, preset)
52+
M.nested_format(selector, 'join', preset)
53+
end
54+
55+
M.nested_split = function(selector, preset)
56+
M.nested_format(selector, 'split', preset)
57+
end
58+
4259
return M

lua/treesj/notify.lua

+1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ local M = {}
77

88
M.msg = {
99
no_detect_node = 'No detected node at cursor',
10+
no_chosen_node = 'Node choice aborted',
1011
no_configured_lang = 'Language "%s" is not configured',
1112
contains_error = 'The node "%s" or its descendants contain a syntax error and cannot be %s',
1213
no_configured_node = 'Node "%s" for lang "%s" is not configured',

lua/treesj/search.lua

+40
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,46 @@ function M.get_configured_node(node)
173173

174174
return data
175175
end
176+
---Return the all configured nodes
177+
---@param node TSNode|nil TSNode instance
178+
---@return table
179+
function M.get_configured_nodes(node)
180+
if not node then
181+
error(msg.node_not_received, 0)
182+
end
183+
184+
local lang = get_node_lang(node)
185+
if not langs[lang] then
186+
error(msg.no_configured_lang:format(lang), 0)
187+
end
188+
local start_node_type = node:type()
189+
190+
local nodes = {}
191+
local done = {}
192+
while node do
193+
local data = search_node(node, lang)
194+
195+
if not data or not data.tsnode then
196+
error(msg.no_configured_node:format(start_node_type, lang), 0)
197+
break
198+
end
199+
200+
local id = table.concat({ data.tsnode:range() }, '-')
201+
if done[id] then
202+
break
203+
else
204+
done[id] = true
205+
end
206+
207+
nodes[#nodes + 1] = data
208+
node = data.tsnode:parent()
209+
end
210+
211+
if #nodes == 0 then
212+
error(msg.no_configured_node:format(start_node_type, lang), 0)
213+
end
214+
return nodes
215+
end
176216

177217
---Return the preset for current node if it no contains field 'target_nodes'
178218
---@param tsn_type string TSNode type

lua/treesj/selectors/count.lua

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
local M = {}
2+
local meta_M = {}
3+
M = setmetatable(M, meta_M)
4+
function M.selector(nodes)
5+
local c = vim.v.count1
6+
return nodes[c], c
7+
end
8+
return M

lua/treesj/selectors/flash.lua

+97
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
local M = {}
2+
local flash = require('flash')
3+
local Pos = require('flash.search.pos')
4+
5+
local opts = { mode = 'treesj' }
6+
function M.setup(config)
7+
opts = config
8+
end
9+
10+
function M.selector(nodes)
11+
local currwin = vim.api.nvim_get_current_win()
12+
local buf = vim.api.nvim_get_current_buf()
13+
local line_count = vim.api.nvim_buf_line_count(buf)
14+
local state = flash.jump(vim.tbl_deep_extend('force', {
15+
matcher = function(win, _)
16+
if win ~= currwin then
17+
return {}
18+
end
19+
local ret = {}
20+
local done = {}
21+
-- https://github.com/folke/flash.nvim/blob/967117690bd677cb7b6a87f0bc0077d2c0be3a27/lua/flash/plugins/treesitter.lua#L52
22+
for i, node in ipairs(nodes) do
23+
local tsn = node.tsnode
24+
local range = { tsn:range() }
25+
local match = {
26+
win = win,
27+
node = node,
28+
pos = { range[1] + 1, range[2] },
29+
end_pos = { range[3] + 1, range[4] - 1 },
30+
index = i,
31+
}
32+
33+
-- If the match is at the end of the buffer,
34+
-- then move it to the last character of the last line.
35+
if match.end_pos[1] > line_count then
36+
match.end_pos[1] = line_count
37+
match.end_pos[2] = #vim.api.nvim_buf_get_lines(
38+
buf,
39+
match.end_pos[1] - 1,
40+
match.end_pos[1],
41+
false
42+
)[1]
43+
elseif match.end_pos[2] == -1 then
44+
-- If the end points to the start of the next line, move it to the
45+
-- end of the previous line.
46+
-- Otherwise operations include the first character of the next line
47+
local line = vim.api.nvim_buf_get_lines(
48+
buf,
49+
match.end_pos[1] - 2,
50+
match.end_pos[1] - 1,
51+
false
52+
)[1]
53+
match.end_pos[1] = match.end_pos[1] - 1
54+
match.end_pos[2] = #line
55+
end
56+
local id =
57+
table.concat(vim.tbl_flatten({ match.pos, match.end_pos }), '.')
58+
if not done[id] then
59+
done[id] = true
60+
ret[#ret + 1] = match
61+
end
62+
end
63+
for m, match in ipairs(ret) do
64+
match.pos = Pos(match.pos)
65+
match.end_pos = Pos(match.end_pos)
66+
match.depth = #ret - m
67+
end
68+
return ret
69+
end,
70+
action = function(match, state)
71+
state.final_match = match
72+
end,
73+
search = {
74+
multi_window = false,
75+
wrap = true,
76+
incremental = false,
77+
max_length = 0,
78+
},
79+
label = {
80+
before = true,
81+
after = true,
82+
},
83+
highlight = {
84+
matches = false,
85+
},
86+
actions = {
87+
-- TODO: incremental preview/operations
88+
},
89+
jump = { autojump = true },
90+
}, opts or {}))
91+
local m = state.final_match
92+
if m then
93+
return m.node, m.index
94+
end
95+
end
96+
97+
return M

lua/treesj/settings.lua

+2
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ local DEFAULT_SETTINGS = {
2525
dot_repeat = true,
2626
---@type nil|function Callback for treesj error handler. func (err_text, level, ...)
2727
on_error = nil,
28+
---@type boolean Whether dot repeat on nested operations should remember the selected level of nesting
29+
remember_selected = true,
2830
}
2931

3032
local commands = {

0 commit comments

Comments
 (0)