Skip to content

Commit c74f07b

Browse files
Tobias LarossTobias Laross
authored andcommitted
Updated based on comments
1 parent 490c7e9 commit c74f07b

File tree

3 files changed

+124
-19
lines changed

3 files changed

+124
-19
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ Vim's built-in marks are great, but they're global and get messy fast. Marksman
2020
- **Persistent storage** - Your marks survive Neovim restarts with automatic backup
2121
- **Smart naming** - Context-aware auto-generation using Treesitter and pattern matching
2222
- **Quick access** - Jump to marks with single keys or interactive UI
23-
- **Sequential navigation** — Jump to next/previous marks with wrap-around behavior
23+
- **Sequential navigation** — Jump to the closest mark relative to your cursor, fallback to jump from mark 1 when no marks are in the current file
2424
- **Enhanced search** - Find marks by name, file path, or content with real-time filtering
2525
- **Mark reordering** - Move marks up/down to organize them as needed
2626
- **Multiple integrations** - Works with Telescope, Snacks.nvim, and more

lua/marksman/init.lua

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -289,15 +289,17 @@ function M.goto_mark(name_or_index)
289289
end
290290
end
291291

292+
-- Finds the mark index closest to the current cursor position, using 1 as fallback.
293+
-- Returns:
294+
-- index (number | nil): the resolved mark index, or nil if no marks exist
295+
-- total (number | nil): total number of marks when successful, nil on failure
296+
-- error (string | nil): error message when no marks are available
292297
local function get_current_mark_index(storage_module)
293298
local mark_names = storage_module.get_mark_names()
294299
local count = #mark_names
295300

296301
if count == 0 then
297-
return nil, "No marks available"
298-
end
299-
if count == 1 then
300-
return nil, "Not enough marks to move"
302+
return nil, nil, "No marks available"
301303
end
302304

303305
local marks = storage_module.get_marks()
@@ -311,7 +313,7 @@ local function get_current_mark_index(storage_module)
311313
local mark = marks[mark_name]
312314
if mark.file == current_file then
313315
if mark.line == current_line then
314-
return index, nil
316+
return index, count, nil
315317
end
316318
local distance = math.abs(mark.line - current_line)
317319
if not best_distance or distance < best_distance then
@@ -321,42 +323,50 @@ local function get_current_mark_index(storage_module)
321323
end
322324
end
323325

324-
return best_index or 1, nil
326+
return best_index or 1, count, nil
325327
end
326328

327-
---Jump to next mark
328-
---@return table result Result with success and message
329+
---Jump to the next mark.
330+
---Navigation is context-aware:
331+
---• If the cursor is on a mark, jump relative to it.
332+
---• If the cursor is not on a mark, select the nearest mark in the same file before jumping.
333+
---• If the current file has no marks, fall back to first index before jumping.
334+
---Wraps when reaching the last mark.
335+
---@return table result Result with success and optional message
329336
function M.goto_next()
330337
local storage_module = get_storage()
331338
if not storage_module then
332339
return { success = false, message = "Failed to load storage module" }
333340
end
334341

335-
local current_index, err = get_current_mark_index(storage_module)
342+
local current_index, count, err = get_current_mark_index(storage_module)
336343
if not current_index then
337344
return { success = false, message = err }
338345
end
339346

340-
local mark_names = storage_module.get_mark_names()
341-
local next_index = (current_index % #mark_names) + 1
347+
local next_index = (current_index % count) + 1
342348
return M.goto_mark(next_index)
343349
end
344350

345-
---Jump to previous mark
346-
---@return table result Result with success and message
351+
---Jump to the previous mark.
352+
---Navigation is context-aware:
353+
---• If the cursor is on a mark, jump relative to it.
354+
---• If the cursor is not on a mark, select the nearest mark in the same file before jumping.
355+
---• If the current file has no marks, fall back to first index before jumping.
356+
---Wraps when reaching the last mark.
357+
---@return table result Result with success and optional message
347358
function M.goto_previous()
348359
local storage_module = get_storage()
349360
if not storage_module then
350361
return { success = false, message = "Failed to load storage module" }
351362
end
352363

353-
local current_index, err = get_current_mark_index(storage_module)
364+
local current_index, count, err = get_current_mark_index(storage_module)
354365
if not current_index then
355366
return { success = false, message = err }
356367
end
357368

358-
local mark_names = storage_module.get_mark_names()
359-
local previous_index = ((current_index - 2) % #mark_names) + 1
369+
local previous_index = ((current_index - 2) % count) + 1
360370
return M.goto_mark(previous_index)
361371
end
362372

tests/marksman_spec.lua

Lines changed: 97 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,16 +55,26 @@ describe("marksman.nvim", function()
5555
describe("mark operations", function()
5656
local marksman = require("marksman")
5757
local test_file
58+
local test_file2
5859

5960
before_each(function()
6061
clear_marks()
6162
local test_dir = vim.env.MARKSMAN_TEST_DIR or vim.fn.tempname()
6263
test_file = test_dir .. "/test.lua"
63-
vim.fn.mkdir(vim.fn.fnamemodify(test_file, ":h"), "p")
64+
test_file2 = test_dir .. "/test2.lua"
6465

6566
setup_buffer_with_file(test_file, {
6667
"local function test()",
67-
" return true",
68+
" local value = false",
69+
" if value then",
70+
" return true",
71+
" end",
72+
" return false",
73+
})
74+
75+
setup_buffer_with_file(test_file2, {
76+
"local function test2()",
77+
" return false",
6878
"end",
6979
})
7080
end)
@@ -155,6 +165,91 @@ describe("marksman.nvim", function()
155165
assert.equals(1, vim.fn.line("."), "Should wrap from m3 to m1")
156166
end)
157167

168+
it("jumps to next mark when cursor is between marks", function()
169+
vim.cmd("edit " .. test_file)
170+
171+
vim.fn.cursor(1, 1)
172+
marksman.add_mark("m1")
173+
174+
vim.fn.cursor(4, 1)
175+
marksman.add_mark("m2")
176+
177+
vim.fn.cursor(5, 1)
178+
marksman.add_mark("m3")
179+
180+
-- cursor on line 3 → distance to m1 = 2, m2 = 1 → choose m2 as current index
181+
vim.fn.cursor(3, 1)
182+
183+
local result = marksman.goto_next()
184+
assert.is_true(result.success)
185+
assert.equals(5, vim.fn.line("."), "Should jump from m2 to m3")
186+
end)
187+
188+
it("jumps to next in another file", function()
189+
-- file A
190+
vim.cmd("edit " .. test_file)
191+
vim.fn.cursor(1, 1)
192+
marksman.add_mark("a1")
193+
194+
-- file B
195+
vim.cmd("edit " .. test_file2)
196+
vim.fn.cursor(1, 1)
197+
marksman.add_mark("b1")
198+
vim.fn.cursor(3, 1)
199+
marksman.add_mark("b2")
200+
201+
vim.cmd("edit " .. test_file)
202+
vim.fn.cursor(1, 1) -- at a1
203+
local result = marksman.goto_next()
204+
205+
assert.is_true(result.success)
206+
assert.equals(test_file2, vim.fn.expand("%:p"), "Should move to next mark in file2")
207+
assert.equals(1, vim.fn.line("."), "Should move to b1")
208+
end)
209+
210+
it("jumps to second mark when current file has no marks", function()
211+
-- file A with marks
212+
vim.cmd("edit " .. test_file)
213+
vim.fn.cursor(1, 1)
214+
marksman.add_mark("m1")
215+
vim.fn.cursor(2, 1)
216+
marksman.add_mark("m2")
217+
218+
-- file B with zero marks
219+
vim.cmd("edit " .. test_file2)
220+
vim.fn.cursor(1, 1)
221+
222+
local result = marksman.goto_next()
223+
assert.is_true(result.success)
224+
225+
-- Should jump to second mark because fallback picks first index
226+
assert.equals(test_file, vim.fn.expand("%:p"))
227+
assert.equals(2, vim.fn.line("."), "Should move to m2")
228+
end)
229+
230+
it("jumps to first mark when only 1 mark exists", function()
231+
-- file A with marks
232+
vim.cmd("edit " .. test_file)
233+
vim.fn.cursor(1, 1)
234+
marksman.add_mark("m1")
235+
236+
-- file B with zero marks
237+
vim.cmd("edit " .. test_file2)
238+
vim.fn.cursor(1, 1)
239+
240+
local result = marksman.goto_next()
241+
assert.is_true(result.success)
242+
243+
assert.equals(test_file, vim.fn.expand("%:p"))
244+
assert.equals(1, vim.fn.line("."), "Should move to m1")
245+
end)
246+
247+
it("returns error when no marks exist", function()
248+
local result = marksman.goto_next()
249+
assert.is_false(result.success)
250+
assert.is_string(result.message)
251+
end)
252+
158253
it("jumps to previous mark with wrap-around", function()
159254
vim.cmd("edit " .. test_file)
160255

0 commit comments

Comments
 (0)