Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@ All notable changes to Recall will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.3.0](https://github.com/fnune/recall.nvim/releases/tag/1.3.0) - 2026-03-04

### Added

- New `reuse_opened_windows` option (default: `false`). When enabled, navigation
reuses existing windows displaying the target buffer instead of switching buffers
in the current window. Useful for split-window workflows.

## [1.2.0](https://github.com/fnune/recall.nvim/releases/tag/1.2.0) - 2025-05-28

### Added
Expand Down
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,21 @@ require("recall").setup({
})
```

### Reusing opened windows

When navigating marks in a split-window layout, `recall` can reuse existing windows
instead of switching buffers in the current window.

```lua
require("recall").setup({
reuse_opened_windows = true -- default: false
})
```

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I appreciate the intention of keeping backward compatibility, but I feel like this is more of a bug fix than an optional enhancement. I would probably not make this an option but make reuse_opened_windows = true the default, without an option to configure that. If that breaks someone's workflow, they can open an issue.

Thoughts?

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we make it a bug fix this can become 1.2.1 instead.

When enabled, `recall` first checks if the target mark's buffer is already open in
another window. If so, it focuses that window and moves the cursor there. Otherwise,
it falls back to the default behavior of switching buffers in the current window.

### Project-specific global marks

Neovim stores global mark information in the [`shada` file][shada-docs]. The
Expand Down Expand Up @@ -269,3 +284,4 @@ To format the code in this repository, run [`./format.sh`](./format.sh).
## Licensing

[The MIT license](./LICENSE.md).

3 changes: 3 additions & 0 deletions lua/recall/config.lua
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ local M = {}
M.opts = {
sign = "",
sign_highlight = "@comment.note",
reuse_opened_windows = false,

telescope = {
autoload = true,
Expand Down Expand Up @@ -48,6 +49,8 @@ function M.setup(opts)
require("recall.telescope").autoload()
end)
end

require("recall.navigation").opts.reuse_opened_windows = M.opts.reuse_opened_windows
end

return M
15 changes: 13 additions & 2 deletions lua/recall/navigation.lua
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ local utils = require("recall.utils")

local M = {}

M.opts = {
reuse_opened_windows = false,
}

M.latest_mark_from_jumplist = function(marks)
local jumplist = vim.fn.getjumplist()
for i = #jumplist[1], 1, -1 do
Expand Down Expand Up @@ -61,8 +65,15 @@ end
M.goto_mark = function(direction)
local mark = M.find_mark(direction)
if mark then
vim.cmd("silent buffer " .. mark.file)
vim.api.nvim_win_set_cursor(0, { mark.pos[2], mark.pos[3] })
if M.opts.reuse_opened_windows then
local bufnr = vim.fn.bufnr(mark.file)
local win_id_to_reuse = vim.fn.bufwinid(bufnr)
if win_id_to_reuse ~= -1 then
utils.set_cursor_for_mark_in_window(win_id_to_reuse, mark)
return
end
end
utils.set_cursor_for_mark_in_current_window(mark)
else
print("No global marks set")
end
Expand Down
12 changes: 12 additions & 0 deletions lua/recall/utils.lua
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,16 @@ M.sorted_global_marks = function()
return marks
end

M.set_cursor_for_mark_in_window = function(window_id, mark)
if window_id ~= 0 then
vim.api.nvim_set_current_win(window_id)
end
vim.api.nvim_win_set_cursor(window_id, { mark.pos[2], mark.pos[3] })
Comment thread
bvdmitri marked this conversation as resolved.
Resolved
end

M.set_cursor_for_mark_in_current_window = function(mark)
vim.cmd("silent buffer " .. mark.file)

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit:

I'm actually no longer sure why silent buffer is necessary here. I know I added it but it's been a while. It seems to be forcing some indirection because you are trying to only run it in the current_window case.

I think I would rather inline this logic into the only call-site because you already run a check on window_id there: win_id_to_reuse ~= -1, so it would seem natural to use that location to make decisions.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it was necessary in case if the mark.file wasn't loaded in any buffer (which also fallbacks to the current_window case)

M.set_cursor_for_mark_in_window(0, mark)
end

return M
78 changes: 68 additions & 10 deletions tests/plenary/recall_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -131,20 +131,27 @@ describe("Recall", function()
place_cursor(unpack(c_pos))
recall.toggle()

recall.goto_prev()
assert.are.same(b_pos, vim.api.nvim_win_get_cursor(0))
for _, reuse_opened_windows in ipairs({ true, false }) do
require('recall.navigation').opts.reuse_opened_windows = reuse_opened_windows

recall.goto_prev()
assert.are.same(a_pos, vim.api.nvim_win_get_cursor(0))
recall.goto_prev()
assert.are.same(b_pos, vim.api.nvim_win_get_cursor(0))

recall.goto_next()
assert.are.same(b_pos, vim.api.nvim_win_get_cursor(0))
recall.goto_prev()
assert.are.same(a_pos, vim.api.nvim_win_get_cursor(0))

recall.goto_next()
assert.are.same(c_pos, vim.api.nvim_win_get_cursor(0))
recall.goto_next()
assert.are.same(b_pos, vim.api.nvim_win_get_cursor(0))

recall.goto_next()
assert.are.same(c_pos, vim.api.nvim_win_get_cursor(0))

recall.goto_prev()
assert.are.same(b_pos, vim.api.nvim_win_get_cursor(0))
recall.goto_prev()
assert.are.same(b_pos, vim.api.nvim_win_get_cursor(0))

recall.goto_next()
assert.are.same(c_pos, vim.api.nvim_win_get_cursor(0))
end
end)

it("can clear all marks", function()
Expand Down Expand Up @@ -204,4 +211,55 @@ describe("Recall", function()
assert.are.equal(results[3].lnum, 20)
assert.are.equal(results[3].col, 0)
end)

it("reuses opened windows when reuse_opened_windows is enabled", function()
-- Create a second buffer for a second window
local bufnr2 = vim.api.nvim_create_buf(true, false)
set_lines(bufnr2, line_count)
local temp_path2 = luv.os_tmpdir() .. "/nvim-recall-test2-" .. luv.hrtime() .. ".txt"
if jit and jit.os == "OSX" then
temp_path2 = "/private" .. temp_path2
end
table.insert(temp_paths, temp_path2)
vim.api.nvim_buf_set_name(bufnr2, temp_path2)
vim.api.nvim_buf_set_option(bufnr2, "modified", false)
vim.cmd("w")

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit:

I find the readability of this test setup a bit rough. The comment -- Create a second buffer... is a "what" comment, but as-is, I have some "why" questions:

  • Why does the test need a proper file in tmp? The other tests seem to get by with just nvim_create_buf
  • Why does OSX need to be handled in a special way here?

Suggestion: make a function that returns just what you need for this test, and add "why" comments there.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This piece of code is pretty much a copy-paste of the before_each block here. The code in the before_each does create a temp file for each test and also handles OSX specially. I wasn't sure why and when looking at that I was having the same questions as you do, but since it was the first time I ever test in Lua I simply assumed this is how it must be done. If that is actually unnecessary I can remove it yes.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, let's maybe put it into a function so that it's not repeated in multiple places. I can try to understand it at some other point but it should at least not be repeated multiple times.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it's already repeated in multiple places then ignore this 😅

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed, I will make it a simple function!


recall.setup({ reuse_opened_windows = true })

vim.api.nvim_set_current_buf(bufnr)
local win1 = vim.api.nvim_get_current_win()
local win2 = vim.api.nvim_open_win(bufnr2, true, { split = 'right' })
assert.are.not_equal(win1, win2)

vim.api.nvim_set_current_win(win1)
place_cursor(10, 1)
recall.mark()

vim.api.nvim_set_current_win(win2)
place_cursor(20, 1)
recall.mark()

vim.api.nvim_set_current_win(win2)
place_cursor(20, 1)
recall.goto_next()

local current_win = vim.api.nvim_get_current_win()
local current_buf = vim.api.nvim_get_current_buf()
local cursor_pos = vim.api.nvim_win_get_cursor(current_win)

assert.are.equal(current_win, win1, "Should reuse the window already displaying the target buffer")
assert.are.equal(current_buf, bufnr)
assert.are.same(cursor_pos, { 10, 1 })

recall.goto_next()

current_win = vim.api.nvim_get_current_win()
current_buf = vim.api.nvim_get_current_buf()
cursor_pos = vim.api.nvim_win_get_cursor(current_win)

assert.are.equal(current_win, win2, "Should reuse the window already displaying the target buffer")
assert.are.equal(current_buf, bufnr2)
assert.are.same(cursor_pos, { 20, 1 })
end)
end)