Skip to content

Commit 387ea3a

Browse files
committed
refactor(ui): route output window gg through keymap config
Replace hardcoded gg handling in output_window.setup_keymaps with a config-driven first_message command. gg now loads full history and jumps to line 1 while preserving custom or window-specific mappings, and can be disabled with output_window.gg = false.
1 parent 5ec2abc commit 387ea3a

7 files changed

Lines changed: 68 additions & 33 deletions

File tree

‎lua/opencode/commands/handlers/workflow.lua‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -388,6 +388,10 @@ M.actions.run_user_command = Promise.async(function(name, args)
388388
end) --[[@as Promise<void> ]]
389389
end)
390390

391+
function M.actions.first_message()
392+
require('opencode.ui.navigation').goto_first_message()
393+
end
394+
391395
function M.actions.next_message()
392396
require('opencode.ui.navigation').goto_next_message()
393397
end
@@ -603,6 +607,10 @@ M.command_defs = {
603607
desc = 'Open context items picker in input window',
604608
execute = M.actions.context_items,
605609
},
610+
first_message = {
611+
desc = 'Load history and go to the first message',
612+
execute = M.actions.first_message,
613+
},
606614
next_message = {
607615
desc = 'Navigate to next message in output window',
608616
execute = M.actions.next_message,

‎lua/opencode/config.lua‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ M.defaults = {
8282

8383
},
8484
output_window = {
85+
['gg'] = { 'first_message', desc = 'Load history and go to the first message' },
8586
['<esc>'] = { 'close', desc = 'Close Opencode windows' },
8687
['<C-c>'] = { 'cancel', desc = 'Cancel running request' },
8788
[']]'] = { 'next_message', desc = 'Go to next message' },

‎lua/opencode/ui/navigation.lua‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,12 @@ local function mark_jump_position(win)
1111
end)
1212
end
1313

14+
function M.goto_first_message()
15+
renderer.load_all_messages()
16+
mark_jump_position(vim.api.nvim_get_current_win())
17+
vim.api.nvim_win_set_cursor(0, { 1, 0 })
18+
end
19+
1420
function M.goto_message_by_id(message_id)
1521
require('opencode.ui.ui').focus_output()
1622
local windows = state.windows or {}
@@ -84,7 +90,7 @@ function M.goto_next_user_message()
8490
return
8591
end
8692

87-
-- Mirror `gg` in output_window.setup_keymaps: under lazy render the target
93+
-- Like `gg`, under lazy render the target
8894
-- message may not yet have a line_start, so force a full render first.
8995
renderer.load_all_messages()
9096

‎lua/opencode/ui/output_window.lua‎

Lines changed: 0 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -707,30 +707,6 @@ function M.close()
707707
pcall(vim.api.nvim_buf_delete, state.windows.output_buf, { force = true })
708708
end
709709

710-
---@param windows OpencodeWindowState
711-
---@param preserve_existing? boolean
712-
function M.setup_keymaps(windows, preserve_existing)
713-
-- When lazy-render is active, gg only reaches the top of rendered content.
714-
-- Load all messages first so gg reaches the true start of history.
715-
local has_gg = false
716-
if preserve_existing then
717-
for _, mapping in ipairs(vim.api.nvim_buf_get_keymap(windows.output_buf, 'n')) do
718-
if mapping.lhs == 'gg' then
719-
has_gg = true
720-
break
721-
end
722-
end
723-
end
724-
if not has_gg then
725-
vim.keymap.set('n', 'gg', function()
726-
local renderer = require('opencode.ui.renderer')
727-
renderer.load_all_messages()
728-
pcall(vim.cmd, [[noau normal! m']])
729-
vim.api.nvim_win_set_cursor(0, { 1, 0 })
730-
end, { buffer = windows.output_buf })
731-
end
732-
end
733-
734710
---Clear the output buffer and all namespaces.
735711
function M.clear()
736712
if M.mounted() then
@@ -749,10 +725,4 @@ function M.get_buf()
749725
return state.windows and state.windows.output_buf
750726
end
751727

752-
---Trigger a re-render by calling the renderer
753-
function M.render()
754-
local renderer = require('opencode.ui.renderer')
755-
renderer._render_all_messages()
756-
end
757-
758728
return M

‎lua/opencode/ui/ui.lua‎

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -276,7 +276,6 @@ function M.restore_hidden_windows()
276276

277277
input_window.setup(windows)
278278
output_window.setup(windows)
279-
output_window.setup_keymaps(windows, true)
280279
footer.setup(windows)
281280
session_tab_strip.setup(windows)
282281
topbar.setup()
@@ -486,7 +485,6 @@ function M.create_windows()
486485

487486
input_window.setup(windows)
488487
output_window.setup(windows)
489-
output_window.setup_keymaps(windows)
490488
footer.setup(windows)
491489
session_tab_strip.setup(windows)
492490
topbar.setup()

‎tests/unit/keymap_spec.lua‎

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
local assert = require('luassert')
22
local store = require('opencode.state.store')
3+
local default_gg = require('opencode.config').defaults.keymap.output_window.gg
34

45
describe('opencode.keymap', function()
56
local set_keymaps = {}
@@ -65,6 +66,7 @@ describe('opencode.keymap', function()
6566
mock_commands = {
6667
get_commands = function()
6768
return {
69+
first_message = { desc = 'Load history and go to the first message', execute = function() end },
6870
open_input = { desc = 'Open input window', execute = function() end },
6971
toggle = { desc = 'Toggle opencode windows', execute = function() end },
7072
submit_input_prompt = { desc = 'Submit input prompt', execute = function() end },
@@ -155,6 +157,40 @@ describe('opencode.keymap', function()
155157
end
156158
end
157159

160+
it('installs the default gg action and restores it without replacing a custom mapping', function()
161+
vim.keymap.set = original_keymap_set
162+
keymap.setup({ output_window = { gg = default_gg } })
163+
local windows = panel()
164+
store.set('windows', windows)
165+
assert.is_true(vim.wait(200, function()
166+
return mapping(windows.output_buf, 'gg') ~= nil
167+
end))
168+
mapping(windows.output_buf, 'gg').callback()
169+
assert.equals('first_message', executed_parsed[1].intent.name)
170+
171+
vim.keymap.del('n', 'gg', { buffer = windows.output_buf })
172+
store.set('windows', nil)
173+
store.set('windows', windows)
174+
assert.is_true(vim.wait(200, function()
175+
return mapping(windows.output_buf, 'gg') ~= nil
176+
end))
177+
original_keymap_set('n', 'gg', function() end, { buffer = windows.output_buf, desc = 'Custom gg' })
178+
store.set('windows', nil)
179+
store.set('windows', windows)
180+
local drained = false
181+
vim.schedule(function() drained = true end)
182+
assert.is_true(vim.wait(200, function() return drained end))
183+
assert.equals('Custom gg', mapping(windows.output_buf, 'gg').desc)
184+
end)
185+
186+
it('does not install gg when disabled', function()
187+
vim.keymap.set = original_keymap_set
188+
local windows = panel()
189+
store.set_raw('windows', windows)
190+
keymap.setup({ output_window = { gg = false } })
191+
assert.is_nil(mapping(windows.output_buf, 'gg'))
192+
end)
193+
158194
it('binds new panels and restores missing mappings without replacing custom or window mappings', function()
159195
vim.keymap.set = original_keymap_set
160196
keymap.setup({

‎tests/unit/navigation_user_message_spec.lua‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,22 @@ describe('navigation user message jumps', function()
5757
end
5858
end)
5959

60+
it('loads history before jumping to the first line', function()
61+
vim.api.nvim_win_set_cursor(output_win, { 100, 0 })
62+
local load_history = stub(renderer, 'load_all_messages').invokes(function()
63+
assert.equals(100, vim.api.nvim_win_get_cursor(output_win)[1])
64+
end)
65+
local ok, err = pcall(function()
66+
navigation.goto_first_message()
67+
assert.stub(load_history).was_called(1)
68+
assert.same({ 1, 0 }, vim.api.nvim_win_get_cursor(output_win))
69+
end)
70+
load_history:revert()
71+
if not ok then
72+
error(err)
73+
end
74+
end)
75+
6076
describe('renderer.get_prev_user_message', function()
6177
it('skips assistant messages and returns previous user message before cursor', function()
6278
seed({

0 commit comments

Comments
 (0)