Skip to content

Commit 25e5ef2

Browse files
committed
Implement config option to specify movements should stay in scope
1 parent 6dcffe5 commit 25e5ef2

20 files changed

+374
-95
lines changed

lua/treewalker/init.lua

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ Treewalker.opts = {
1313
jumplist = true,
1414
select = false,
1515
notifications = true,
16+
scope_confined = false,
1617
}
1718

1819
-- This does not need to be called for Treewalker to work. The defaults are preinitialized and aim to be sane.

lua/treewalker/movement.lua

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,24 @@ local function add_jumplist_for_move(command)
2424
end
2525
end
2626

27+
---@param current_node TSNode
28+
---@param candidate TSNode
29+
---@return boolean
30+
local function should_confine_vertical_move(current_node, candidate)
31+
local opts = require('treewalker').opts
32+
if opts.scope_confined ~= true then
33+
return false
34+
end
35+
36+
local current_parent = nodes.scope_parent(current_node)
37+
if not current_parent then
38+
return false
39+
end
40+
41+
local candidate_anchor = nodes.get_highest_row_coincident(candidate)
42+
return not nodes.is_descendant_of(current_parent, candidate_anchor)
43+
end
44+
2745
---@return nil
2846
function M.move_out()
2947
-- Add to jumplist at original cursor position before normalizing
@@ -55,6 +73,10 @@ function M.move_up()
5573
local target, row = targets.up(current_node, current_row)
5674
if not target or not row then return end
5775

76+
if should_confine_vertical_move(current_node, target) then
77+
return
78+
end
79+
5880
local is_neighbor = nodes.have_neighbor_srow(current_node, target)
5981

6082
if not is_neighbor then
@@ -70,6 +92,10 @@ function M.move_down()
7092
local target, row = targets.down(current_node, current_row)
7193
if not target or not row then return end
7294

95+
if should_confine_vertical_move(current_node, target) then
96+
return
97+
end
98+
7399
local is_neighbor = nodes.have_neighbor_srow(current_node, target)
74100

75101
if not is_neighbor then

lua/treewalker/nodes.lua

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,27 @@ function M.get_descendants(node)
207207
return descendants
208208
end
209209

210+
---@param parent TSNode
211+
---@param node TSNode
212+
---@return boolean
213+
function M.is_descendant_of(parent, node)
214+
local iter = node
215+
while iter do
216+
if iter == parent then
217+
return true
218+
end
219+
iter = iter:parent()
220+
end
221+
return false
222+
end
223+
224+
---@param node TSNode
225+
---@return TSNode|nil
226+
function M.scope_parent(node)
227+
local anchor = M.get_highest_row_coincident(node)
228+
return anchor:parent()
229+
end
230+
210231
-- Take row, give next row / node with same indentation
211232
---@param current_row integer
212233
---@param dir "up" | "down"

lua/treewalker/options.lua

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ local M = {}
77
--- jumplist: boolean | 'left',
88
--- highlight_group: string,
99
--- select: boolean,
10-
--- notifications: boolean }
10+
--- notifications: boolean,
11+
--- scope_confined: boolean }
1112

1213
---@param opts Opts
1314
---@return boolean, table<string>
@@ -41,6 +42,10 @@ function M.validate_opts(opts)
4142
table.insert(errors, "`notifications` should be boolean or nil")
4243
end
4344

45+
if type(opts.scope_confined) ~= "boolean" and opts.scope_confined ~= nil then
46+
table.insert(errors, "`scope_confined` should be boolean or nil")
47+
end
48+
4449
if #errors == 0 then
4550
return true, {}
4651
else

tests/treewalker/c_sharp_spec.lua

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,4 +103,17 @@ describe("In a C Sharp file", function()
103103
assert.same(first_block, lines.get_lines(135, 138))
104104
h.assert_cursor_at(135, 13)
105105
end)
106+
describe("scope_confined", function()
107+
before_each(function()
108+
tw.setup({ scope_confined = true })
109+
end)
110+
111+
it("confines move_down", function()
112+
h.assert_confined_by_parent(22, 1, 'down')
113+
end)
114+
115+
it("confines move_up", function()
116+
h.assert_confined_by_parent(20, 1, 'up')
117+
end)
118+
end)
106119
end)

tests/treewalker/c_spec.lua

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,4 +70,17 @@ describe("In a c file:", function()
7070
tw.move_up()
7171
h.assert_highlighted(26, 1, 33, 1)
7272
end)
73+
describe("scope_confined", function()
74+
before_each(function()
75+
tw.setup({ scope_confined = true })
76+
end)
77+
78+
it("confines move_down", function()
79+
h.assert_confined_by_parent(23, 1, 'down')
80+
end)
81+
82+
it("confines move_up", function()
83+
h.assert_confined_by_parent(10, 1, 'up')
84+
end)
85+
end)
7386
end)

tests/treewalker/go_spec.lua

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,4 +47,18 @@ describe("Movement in Go file with tab indentation:", function()
4747
local row = vim.fn.line('.')
4848
assert.equals(19, row, "Should move from main to helper (line 19)")
4949
end)
50+
51+
describe("scope_confined", function()
52+
before_each(function()
53+
tw.setup({ scope_confined = true })
54+
end)
55+
56+
it("confines move_down", function()
57+
h.assert_confined_by_parent(17, 1, 'down')
58+
end)
59+
60+
it("confines move_up", function()
61+
h.assert_confined_by_parent(16, 1, 'up')
62+
end)
63+
end)
5064
end)

tests/treewalker/haskell_spec.lua

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,17 @@ describe("In a haskell file: ", function()
2828
-- tw.move_out()
2929
-- h.assert_cursor_at(19, 1)
3030
-- end)
31-
end)
31+
describe("scope_confined", function()
32+
before_each(function()
33+
tw.setup({ scope_confined = true })
34+
end)
3235

36+
it("confines move_down", function()
37+
h.assert_confined_by_parent(10, 1, 'down')
38+
end)
3339

40+
it("confines move_up", function()
41+
h.assert_confined_by_parent(11, 1, 'up')
42+
end)
43+
end)
44+
end)

0 commit comments

Comments
 (0)