Skip to content

Commit e806a5b

Browse files
committed
refactor(actions): simplify and improve navigate_buf_mark
Rewrite navigate_buf_mark to collect all marks into a sorted list then do a single linear scan, eliminating duplicated closure logic and redundant getmarklist() calls. Additional behavioural improvements: - wrap now defaults to true (opt out with wrap = false) - Navigation is now column-aware: multiple marks on the same line are visited in column order instead of being skipped - Fix off-by-one: getmarklist() returns 1-indexed columns but nvim_win_set_cursor expects 0-indexed, so subtract 1 from pos[3] Tests updated to assert full {line, col} cursor positions, add a new case for same-line mark navigation, and reflect the new wrap default.
1 parent b57e6dd commit e806a5b

File tree

2 files changed

+79
-79
lines changed

2 files changed

+79
-79
lines changed

lua/guttermarks/actions.lua

Lines changed: 34 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -93,81 +93,61 @@ local function navigate_buf_mark(direction, opts)
9393
opts = opts or {}
9494
local local_mark = opts.local_mark ~= false
9595
local global_mark = opts.global_mark ~= false
96-
local wrap = opts.wrap == true
96+
local wrap = opts.wrap ~= false
9797

9898
local bufnr = vim.api.nvim_get_current_buf()
99-
local current_line = vim.api.nvim_win_get_cursor(0)[1]
99+
local cursor = vim.api.nvim_win_get_cursor(0)
100+
local current_line = cursor[1]
101+
local current_col = cursor[2]
100102

101-
local target_mark = nil
102-
local compare_fn = direction == "forward" and function(mark_line)
103-
return mark_line > current_line
104-
end or function(mark_line)
105-
return mark_line < current_line
106-
end
107-
108-
local select_fn = direction == "forward"
109-
and function(new_mark, current_best)
110-
return not current_best or new_mark.line < current_best.line
111-
end
112-
or function(new_mark, current_best)
113-
return not current_best or new_mark.line > current_best.line
114-
end
103+
-- Collect all relevant marks into one list
104+
local all_marks = {}
115105

116-
-- Check local marks
117106
if local_mark then
118107
for _, m in ipairs(vim.fn.getmarklist(bufnr)) do
119-
if m.mark:match("^'[a-z]") and compare_fn(m.pos[2]) then
120-
local mark_data = { line = m.pos[2], col = m.pos[3], mark = m.mark:sub(2) }
121-
if select_fn(mark_data, target_mark) then
122-
target_mark = mark_data
123-
end
108+
if m.mark:match("^'[a-z]") then
109+
table.insert(all_marks, { line = m.pos[2], col = m.pos[3] - 1, mark = m.mark:sub(2) })
124110
end
125111
end
126112
end
127113

128-
-- Check global marks
129114
if global_mark then
130115
for _, m in ipairs(vim.fn.getmarklist()) do
131-
if m.pos[1] == bufnr and m.mark:match("^'[A-Z]") and compare_fn(m.pos[2]) then
132-
local mark_data = { line = m.pos[2], col = m.pos[3], mark = m.mark:sub(2) }
133-
if select_fn(mark_data, target_mark) then
134-
target_mark = mark_data
135-
end
116+
if m.pos[1] == bufnr and m.mark:match("^'[A-Z]") then
117+
table.insert(all_marks, { line = m.pos[2], col = m.pos[3] - 1, mark = m.mark:sub(2) })
136118
end
137119
end
138120
end
139121

140-
-- Wrap around: find the first/last mark in the buffer when no mark in direction
141-
if not target_mark and wrap then
142-
local wrap_select_fn = direction == "forward"
143-
and function(new_mark, current_best)
144-
return not current_best or new_mark.line < current_best.line
145-
end
146-
or function(new_mark, current_best)
147-
return not current_best or new_mark.line > current_best.line
148-
end
122+
table.sort(all_marks, function(a, b)
123+
if a.line ~= b.line then
124+
return a.line < b.line
125+
end
126+
return a.col < b.col
127+
end)
149128

150-
if local_mark then
151-
for _, m in ipairs(vim.fn.getmarklist(bufnr)) do
152-
if m.mark:match("^'[a-z]") then
153-
local mark_data = { line = m.pos[2], col = m.pos[3], mark = m.mark:sub(2) }
154-
if wrap_select_fn(mark_data, target_mark) then
155-
target_mark = mark_data
156-
end
157-
end
129+
-- Scan sorted list for the nearest mark in the requested direction
130+
local target_mark = nil
131+
if direction == "forward" then
132+
for _, m in ipairs(all_marks) do
133+
if m.line > current_line or (m.line == current_line and m.col > current_col) then
134+
target_mark = m
135+
break
158136
end
159137
end
160-
161-
if global_mark then
162-
for _, m in ipairs(vim.fn.getmarklist()) do
163-
if m.pos[1] == bufnr and m.mark:match("^'[A-Z]") then
164-
local mark_data = { line = m.pos[2], col = m.pos[3], mark = m.mark:sub(2) }
165-
if wrap_select_fn(mark_data, target_mark) then
166-
target_mark = mark_data
167-
end
168-
end
138+
if not target_mark and wrap then
139+
target_mark = all_marks[1]
140+
end
141+
else
142+
for i = #all_marks, 1, -1 do
143+
if all_marks[i].line < current_line or (all_marks[i].line == current_line and all_marks[i].col < current_col) then
144+
target_mark = all_marks[i]
145+
break
169146
end
170147
end
148+
if not target_mark and wrap then
149+
target_mark = all_marks[#all_marks]
150+
end
171151
end
172152

173153
if target_mark then

test/test_action_navigation.lua

Lines changed: 45 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -24,20 +24,21 @@ T["prev_buf_mark"] = MiniTest.new_set()
2424

2525
T["next_buf_mark"]["navigates forward"] = function()
2626
new_buf()
27-
child.type_keys({ "gg", "ma", "j", "mb", "j", "mc", "gg" })
27+
-- marks on lines 2 and 3; cursor starts at line 1 so no same-line ambiguity
28+
child.type_keys({ "gg", "0", "j", "0", "llll", "ma", "j", "0", "l", "mb", "gg", "0" })
2829

2930
child.lua([[ M.next_buf_mark() ]])
30-
eq(child.api.nvim_win_get_cursor(0)[1], 2)
31+
eq(child.api.nvim_win_get_cursor(0), { 2, 4 })
3132

3233
child.lua([[ M.next_buf_mark() ]])
33-
eq(child.api.nvim_win_get_cursor(0)[1], 3)
34+
eq(child.api.nvim_win_get_cursor(0), { 3, 1 })
3435
end
3536

3637
T["next_buf_mark"]["no marks ahead"] = function()
3738
new_buf()
3839
child.type_keys({ "gg", "ma", "j" })
3940

40-
child.lua([[ M.next_buf_mark() ]])
41+
child.lua([[ M.next_buf_mark({ wrap = false }) ]])
4142
eq(child.api.nvim_win_get_cursor(0)[1], 2)
4243
end
4344

@@ -51,24 +52,25 @@ end
5152

5253
T["next_buf_mark"]["skips global marks with opts"] = function()
5354
new_buf()
54-
child.type_keys({ "gg", "ma", "j", "mb", "j", "mC", "j", "md", "gg" })
55+
-- marks on lines 2-4; cursor starts at line 1 so no same-line ambiguity
56+
child.type_keys({ "gg", "0", "j", "0", "lll", "ma", "j", "0", "mC", "j", "0", "l", "mb", "gg", "0" })
5557

5658
child.lua([[ M.next_buf_mark({ global_mark = false }) ]])
57-
eq(child.api.nvim_win_get_cursor(0)[1], 2)
59+
eq(child.api.nvim_win_get_cursor(0), { 2, 3 })
5860

5961
child.lua([[ M.next_buf_mark({ global_mark = false }) ]])
60-
eq(child.api.nvim_win_get_cursor(0)[1], 4)
62+
eq(child.api.nvim_win_get_cursor(0), { 4, 1 })
6163
end
6264

6365
T["prev_buf_mark"]["navigates backward"] = function()
6466
new_buf()
65-
child.type_keys({ "gg", "ma", "j", "mb", "j", "mc", "G" })
67+
child.type_keys({ "gg", "ll", "ma", "j", "0", "llll", "mb", "j", "0", "l", "mc", "G" })
6668

6769
child.lua([[ M.prev_buf_mark() ]])
68-
eq(child.api.nvim_win_get_cursor(0)[1], 3)
70+
eq(child.api.nvim_win_get_cursor(0), { 3, 1 })
6971

7072
child.lua([[ M.prev_buf_mark() ]])
71-
eq(child.api.nvim_win_get_cursor(0)[1], 2)
73+
eq(child.api.nvim_win_get_cursor(0), { 2, 4 })
7274
end
7375

7476
T["prev_buf_mark"]["no marks behind"] = function()
@@ -89,40 +91,58 @@ end
8991

9092
T["prev_buf_mark"]["skips local marks with opts"] = function()
9193
new_buf()
92-
child.type_keys({ "gg", "ma", "jj", "mB", "jj", "mc", "G" })
94+
child.type_keys({ "gg", "ll", "ma", "jj", "0", "lll", "mB", "jj", "0", "l", "mc", "G" })
9395

9496
child.lua([[ M.prev_buf_mark({ local_mark = false }) ]])
95-
eq(child.api.nvim_win_get_cursor(0)[1], 3)
97+
eq(child.api.nvim_win_get_cursor(0), { 3, 3 })
98+
end
99+
100+
T["Multiple marks on the same line"] = function()
101+
new_buf()
102+
-- mark a at line 2 col 1, mark b at line 2 col 3
103+
child.type_keys({ "gg", "0", "j", "0", "l", "ma", "ll", "mb", "gg", "0" })
104+
105+
child.lua([[ M.next_buf_mark() ]])
106+
eq(child.api.nvim_win_get_cursor(0), { 2, 1 })
107+
108+
child.lua([[ M.next_buf_mark() ]])
109+
eq(child.api.nvim_win_get_cursor(0), { 2, 3 })
110+
111+
-- navigate backward through same-line marks
112+
child.lua([[ M.prev_buf_mark() ]])
113+
eq(child.api.nvim_win_get_cursor(0), { 2, 1 })
96114
end
97115

98116
T["Mixed local and global marks"] = function()
99117
new_buf()
100-
child.type_keys({ "gg", "ma", "j", "mB", "j", "mc", "gg" })
118+
-- mark a at cursor start position so it is not "ahead"; B and c are on later lines
119+
child.type_keys({ "gg", "0", "ma", "j", "0", "lll", "mB", "j", "0", "l", "mc", "gg", "0" })
101120

102121
child.lua([[ M.next_buf_mark() ]])
103-
eq(child.api.nvim_win_get_cursor(0)[1], 2)
122+
eq(child.api.nvim_win_get_cursor(0), { 2, 3 })
104123

105124
child.lua([[ M.next_buf_mark() ]])
106-
eq(child.api.nvim_win_get_cursor(0)[1], 3)
125+
eq(child.api.nvim_win_get_cursor(0), { 3, 1 })
107126
end
108127

109128
T["next_buf_mark wrap"] = MiniTest.new_set()
110129
T["prev_buf_mark wrap"] = MiniTest.new_set()
111130

112131
T["next_buf_mark wrap"]["wraps from last mark to first"] = function()
113132
new_buf()
114-
child.type_keys({ "gg", "ma", "j", "mb", "j", "mc", "G" })
133+
child.type_keys({ "gg", "0", "ll", "ma", "j", "0", "mb", "j", "0", "mc", "jj" })
115134

116135
child.lua([[ M.next_buf_mark({ wrap = true }) ]])
117-
eq(child.api.nvim_win_get_cursor(0)[1], 1)
136+
eq(child.api.nvim_win_get_cursor(0), { 1, 2 })
118137
end
119138

120139
T["next_buf_mark wrap"]["does not wrap when mark found ahead"] = function()
121140
new_buf()
122-
child.type_keys({ "gg", "ma", "j", "mb", "gg" })
141+
-- mark a at col 0, cursor ends at col 3 after gg (preserved), so a is behind
142+
child.type_keys({ "gg", "0", "ma", "j", "0", "lll", "mb", "gg" })
123143

124144
child.lua([[ M.next_buf_mark({ wrap = true }) ]])
125-
eq(child.api.nvim_win_get_cursor(0)[1], 2)
145+
eq(child.api.nvim_win_get_cursor(0), { 2, 3 })
126146
end
127147

128148
T["next_buf_mark wrap"]["no marks present stays put"] = function()
@@ -137,24 +157,24 @@ T["next_buf_mark wrap"]["no wrap without opt"] = function()
137157
new_buf()
138158
child.type_keys({ "gg", "ma", "j", "mb", "G" })
139159

140-
child.lua([[ M.next_buf_mark() ]])
160+
child.lua([[ M.next_buf_mark({ wrap = false }) ]])
141161
eq(child.api.nvim_win_get_cursor(0)[1], 5)
142162
end
143163

144164
T["prev_buf_mark wrap"]["wraps from first mark to last"] = function()
145165
new_buf()
146-
child.type_keys({ "gg", "ma", "j", "mb", "j", "mc", "gg" })
166+
child.type_keys({ "gg", "ma", "j", "0", "mb", "j", "0", "l", "mc", "gg" })
147167

148168
child.lua([[ M.prev_buf_mark({ wrap = true }) ]])
149-
eq(child.api.nvim_win_get_cursor(0)[1], 3)
169+
eq(child.api.nvim_win_get_cursor(0), { 3, 1 })
150170
end
151171

152172
T["prev_buf_mark wrap"]["does not wrap when mark found behind"] = function()
153173
new_buf()
154-
child.type_keys({ "gg", "ma", "j", "mb", "G" })
174+
child.type_keys({ "gg", "ma", "j", "0", "lll", "mb", "G" })
155175

156176
child.lua([[ M.prev_buf_mark({ wrap = true }) ]])
157-
eq(child.api.nvim_win_get_cursor(0)[1], 2)
177+
eq(child.api.nvim_win_get_cursor(0), { 2, 3 })
158178
end
159179

160180
T["prev_buf_mark wrap"]["no marks present stays put"] = function()
@@ -169,7 +189,7 @@ T["prev_buf_mark wrap"]["no wrap without opt"] = function()
169189
new_buf()
170190
child.type_keys({ "gg", "ma", "j", "mb", "gg" })
171191

172-
child.lua([[ M.prev_buf_mark() ]])
192+
child.lua([[ M.prev_buf_mark({ wrap = false }) ]])
173193
eq(child.api.nvim_win_get_cursor(0)[1], 1)
174194
end
175195

0 commit comments

Comments
 (0)