Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
15 changes: 15 additions & 0 deletions lua/opencode/ui/navigation.lua
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@ local state = require('opencode.state')
local config = require('opencode.config')
local renderer = require('opencode.ui.renderer')

---@param win integer
local function mark_jump_position(win)
pcall(vim.api.nvim_win_call, win, function()
vim.cmd([[noau normal! m']])
end)
end

function M.goto_message_by_id(message_id)
require('opencode.ui.ui').focus_output()
local windows = state.windows or {}
Expand All @@ -18,6 +25,7 @@ function M.goto_message_by_id(message_id)
if not rendered_msg or not rendered_msg.line_start then
return
end
mark_jump_position(win)
vim.api.nvim_win_set_cursor(win, { rendered_msg.line_start + 1, 0 })
end

Expand All @@ -34,11 +42,13 @@ function M.goto_next_message()
local current_line = vim.api.nvim_win_get_cursor(win)[1]
local next_message = renderer.get_next_rendered_message(current_line)
if next_message and next_message.line_start then
mark_jump_position(win)
vim.api.nvim_win_set_cursor(win, { next_message.line_start + 1, 0 })
return
end

local line_count = vim.api.nvim_buf_line_count(buf)
mark_jump_position(win)
vim.api.nvim_win_set_cursor(win, { line_count, 0 })
end

Expand All @@ -55,10 +65,12 @@ function M.goto_prev_message()
local current_line = vim.api.nvim_win_get_cursor(win)[1]
local previous_message = renderer.get_prev_rendered_message(current_line)
if previous_message and previous_message.line_start then
mark_jump_position(win)
vim.api.nvim_win_set_cursor(win, { previous_message.line_start + 1, 0 })
return
end

mark_jump_position(win)
vim.api.nvim_win_set_cursor(win, { 1, 0 })
end

Expand All @@ -79,6 +91,7 @@ function M.goto_next_user_message()
local current_line = vim.api.nvim_win_get_cursor(win)[1]
local next_message = renderer.get_next_user_message(current_line)
if next_message and next_message.line_start then
mark_jump_position(win)
vim.api.nvim_win_set_cursor(win, { next_message.line_start + 1, 0 })
return
end
Expand All @@ -101,6 +114,7 @@ function M.goto_prev_user_message()
local current_line = vim.api.nvim_win_get_cursor(win)[1]
local previous_message = renderer.get_prev_user_message(current_line)
if previous_message and previous_message.line_start then
mark_jump_position(win)
vim.api.nvim_win_set_cursor(win, { previous_message.line_start + 1, 0 })
return
end
Expand Down Expand Up @@ -153,6 +167,7 @@ local function open_at(win, path, line, col)
local line_text = target_lines[1] or ''
target_col = math.max(0, math.min(col - 1, math.max(#line_text - 1, 0)))
end
mark_jump_position(win)
pcall(vim.api.nvim_win_set_cursor, win, { target_line, target_col })
vim.cmd('normal! zz')
end
Expand Down
1 change: 1 addition & 0 deletions lua/opencode/ui/output_window.lua
Original file line number Diff line number Diff line change
Expand Up @@ -677,6 +677,7 @@ function M.setup_keymaps(windows)
vim.keymap.set('n', 'gg', function()
local renderer = require('opencode.ui.renderer')
renderer.load_all_messages()
pcall(vim.cmd, [[noau normal! m']])
vim.api.nvim_win_set_cursor(0, { 1, 0 })
end, { buffer = windows.output_buf })
end
Expand Down
117 changes: 117 additions & 0 deletions tests/unit/navigation_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -539,3 +539,120 @@ describe('output token navigation', function()
assert.same({ 2, 3 }, vim.api.nvim_win_get_cursor(state.windows.output_win))
end)
end)

describe('navigation jumplist preservation', function()
local output_buf, output_win, input_buf, input_win, code_buf, code_win
local original_windows, original_code_win, original_code_buf, original_config

before_each(function()
original_windows = state.store.get('windows')
original_code_win = state.store.get('last_code_win_before_opencode')
original_code_buf = state.store.get('current_code_buf')
original_config = vim.deepcopy(config.values)
state.ui.clear_hidden_window_state()

code_buf = vim.api.nvim_create_buf(false, true)
vim.api.nvim_buf_set_lines(code_buf, 0, -1, false, { 'alpha', 'beta', 'gamma' })
code_win = vim.api.nvim_open_win(code_buf, true, {
relative = 'editor',
width = 40,
height = 5,
row = 0,
col = 0,
})

output_buf = vim.api.nvim_create_buf(false, true)
output_win = vim.api.nvim_open_win(output_buf, true, {
relative = 'editor',
width = 80,
height = 8,
row = 6,
col = 0,
})

state.ui.set_windows({ output_buf = output_buf, output_win = output_win })
state.ui.set_last_code_window(code_win)
end)

after_each(function()
state.ui.clear_hidden_window_state()
pcall(vim.api.nvim_win_close, output_win, true)
pcall(vim.api.nvim_win_close, input_win, true)
pcall(vim.api.nvim_win_close, code_win, true)
pcall(vim.api.nvim_buf_delete, output_buf, { force = true })
pcall(vim.api.nvim_buf_delete, input_buf, { force = true })
pcall(vim.api.nvim_buf_delete, code_buf, { force = true })

if original_windows ~= nil then
state.ui.set_windows(original_windows)
else
state.ui.clear_windows()
end
state.ui.set_last_code_window(original_code_win)
state.ui.set_current_code_buf(original_code_buf)
config.values = original_config
end)

it('marks the output cursor before goto_next_message moves', function()
local renderer = require('opencode.ui.renderer')
local ctx = require('opencode.ui.renderer.ctx')
state.renderer.set_messages({
{ info = { id = 'm1', role = 'user' } },
{ info = { id = 'm2', role = 'assistant' } },
})
ctx.render_state:set_message({ info = { id = 'm1', role = 'user' } }, 1, 1)
ctx.render_state:set_message({ info = { id = 'm2', role = 'assistant' } }, 20, 20)
vim.api.nvim_buf_set_lines(output_buf, 0, -1, false, vim.fn['repeat']({ 'line' }, 40))
vim.api.nvim_win_set_cursor(output_win, { 5, 0 })
vim.api.nvim_buf_set_mark(output_buf, "'", 1, 0, {})

navigation.goto_next_message()

local mark = vim.api.nvim_buf_get_mark(output_buf, "'")
assert.equals(5, mark[1])
assert.equals(0, mark[2])
end)

it('marks the output cursor before goto_prev_message moves', function()
local renderer = require('opencode.ui.renderer')
local ctx = require('opencode.ui.renderer.ctx')
state.renderer.set_messages({
{ info = { id = 'm1', role = 'user' } },
{ info = { id = 'm2', role = 'assistant' } },
})
ctx.render_state:set_message({ info = { id = 'm1', role = 'user' } }, 1, 1)
ctx.render_state:set_message({ info = { id = 'm2', role = 'assistant' } }, 20, 20)
vim.api.nvim_buf_set_lines(output_buf, 0, -1, false, vim.fn['repeat']({ 'line' }, 40))
vim.api.nvim_win_set_cursor(output_win, { 31, 0 })
vim.api.nvim_buf_set_mark(output_buf, "'", 1, 0, {})

navigation.goto_prev_message()

local mark = vim.api.nvim_buf_get_mark(output_buf, "'")
assert.equals(31, mark[1])
assert.equals(0, mark[2])
end)

it('marks the output cursor before jumping to a rendered file target', function()
vim.api.nvim_buf_set_lines(output_buf, 0, -1, false, { existing_path })
vim.api.nvim_win_set_cursor(output_win, { 1, 0 })
vim.api.nvim_buf_set_mark(output_buf, "'", 1, 0, {})
local original_navigate = navigation.navigate_to_location
navigation.navigate_to_location = function()
return true
end
stub(renderer, 'get_target_at_position').returns({
kind = 'file',
path = existing_path,
line = 1,
col = 1,
})

navigation.jump_to_target_at_cursor()

navigation.navigate_to_location = original_navigate
local mark = vim.api.nvim_buf_get_mark(output_buf, "'")
assert.equals(1, mark[1])
assert.equals(0, mark[2])
end)
end)
42 changes: 42 additions & 0 deletions tests/unit/navigation_user_message_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -333,4 +333,46 @@ describe('navigation user message jumps', function()
assert.equals(2, cursor[1])
end)
end)

describe('jumplist preservation', function()
it('marks the previous position before jumping to the next user message', function()
seed({
{ info = { id = 'u1', role = 'user' } },
{ info = { id = 'a1', role = 'assistant' } },
{ info = { id = 'u2', role = 'user' } },
}, {
{ id = 'u1', role = 'user', line_start = 1 },
{ id = 'a1', role = 'assistant', line_start = 20 },
{ id = 'u2', role = 'user', line_start = 40 },
})

vim.api.nvim_win_set_cursor(output_win, { 5, 0 })
vim.api.nvim_buf_set_mark(output_buf, "'", 1, 0, {})
navigation.goto_next_user_message()

local mark = vim.api.nvim_buf_get_mark(output_buf, "'")
assert.equals(5, mark[1])
assert.equals(0, mark[2])
end)

it('marks the previous position before jumping to the previous user message', function()
seed({
{ info = { id = 'u1', role = 'user' } },
{ info = { id = 'a1', role = 'assistant' } },
{ info = { id = 'u2', role = 'user' } },
}, {
{ id = 'u1', role = 'user', line_start = 1 },
{ id = 'a1', role = 'assistant', line_start = 20 },
{ id = 'u2', role = 'user', line_start = 40 },
})

vim.api.nvim_win_set_cursor(output_win, { 81, 0 })
vim.api.nvim_buf_set_mark(output_buf, "'", 1, 0, {})
navigation.goto_prev_user_message()

local mark = vim.api.nvim_buf_get_mark(output_buf, "'")
assert.equals(81, mark[1])
assert.equals(0, mark[2])
end)
end)
end)
Loading