Skip to content

Commit 30dec32

Browse files
authored
fix(input): prevent cursor jumps on context completion trigger (#274)
Avoid restoring saved input cursor position when triggering # context completion
1 parent cb09a6f commit 30dec32

4 files changed

Lines changed: 84 additions & 2 deletions

File tree

lua/opencode/api.lua

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -364,7 +364,7 @@ end
364364

365365
function M.context_items()
366366
local char = config.get_key_for_function('input_window', 'context_items')
367-
ui.focus_input({ restore_position = true, start_insert = true })
367+
ui.focus_input({ restore_position = false, start_insert = true })
368368
require('opencode.ui.completion').trigger_completion(char)()
369369
end
370370

lua/opencode/ui/ui.lua

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,8 +136,11 @@ function M.focus_input(opts)
136136
return
137137
end
138138

139+
local was_input_focused = vim.api.nvim_get_current_win() == windows.input_win
140+
139141
if input_window.is_hidden() then
140142
input_window._show()
143+
was_input_focused = false
141144
end
142145

143146
if not windows.input_win then
@@ -146,7 +149,7 @@ function M.focus_input(opts)
146149

147150
vim.api.nvim_set_current_win(windows.input_win)
148151

149-
if opts.restore_position and state.last_input_window_position then
152+
if opts.restore_position and not was_input_focused and state.last_input_window_position then
150153
pcall(vim.api.nvim_win_set_cursor, 0, state.last_input_window_position)
151154
end
152155
if vim.api.nvim_get_current_win() == windows.input_win and opts.start_insert then

tests/unit/api_spec.lua

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,40 @@ describe('opencode.api', function()
146146
end)
147147
end)
148148

149+
describe('completion triggers', function()
150+
it('does not restore cursor position for context_items', function()
151+
local config = require('opencode.config')
152+
local original_get_key_for_function = config.get_key_for_function
153+
local original_completion = package.loaded['opencode.ui.completion']
154+
155+
config.get_key_for_function = function(scope, function_name)
156+
if scope == 'input_window' and function_name == 'context_items' then
157+
return '#'
158+
end
159+
return original_get_key_for_function(scope, function_name)
160+
end
161+
162+
local trigger_char = nil
163+
package.loaded['opencode.ui.completion'] = {
164+
trigger_completion = function(char)
165+
return function()
166+
trigger_char = char
167+
end
168+
end,
169+
}
170+
171+
stub(ui, 'focus_input')
172+
173+
api.context_items()
174+
175+
config.get_key_for_function = original_get_key_for_function
176+
package.loaded['opencode.ui.completion'] = original_completion
177+
178+
assert.stub(ui.focus_input).was_called_with({ restore_position = false, start_insert = true })
179+
assert.equal('#', trigger_char)
180+
end)
181+
end)
182+
149183
describe('run command argument parsing', function()
150184
it('parses agent prefix and passes to send_message', function()
151185
api.commands.run.fn({ 'agent=plan', 'analyze', 'this', 'code' }):wait()

tests/unit/cursor_tracking_spec.lua

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
local state = require('opencode.state')
22
local config = require('opencode.config')
3+
local ui = require('opencode.ui.ui')
34

45
describe('cursor persistence (state)', function()
56
before_each(function()
@@ -239,3 +240,47 @@ describe('renderer.scroll_to_bottom', function()
239240
config.values.ui.output.always_scroll_to_bottom = false
240241
end)
241242
end)
243+
244+
describe('ui.focus_input', function()
245+
local input_buf, output_buf, input_win, output_win
246+
247+
before_each(function()
248+
input_buf = vim.api.nvim_create_buf(false, true)
249+
output_buf = vim.api.nvim_create_buf(false, true)
250+
vim.api.nvim_buf_set_lines(input_buf, 0, -1, false, { 'abcde' })
251+
vim.api.nvim_buf_set_lines(output_buf, 0, -1, false, { 'output' })
252+
253+
output_win = vim.api.nvim_open_win(output_buf, true, {
254+
relative = 'editor', width = 40, height = 5, row = 0, col = 0,
255+
})
256+
input_win = vim.api.nvim_open_win(input_buf, true, {
257+
relative = 'editor', width = 40, height = 5, row = 6, col = 0,
258+
})
259+
260+
state.windows = {
261+
input_win = input_win,
262+
output_win = output_win,
263+
input_buf = input_buf,
264+
output_buf = output_buf,
265+
}
266+
state.last_input_window_position = { 1, 4 }
267+
end)
268+
269+
after_each(function()
270+
pcall(vim.api.nvim_win_close, input_win, true)
271+
pcall(vim.api.nvim_win_close, output_win, true)
272+
pcall(vim.api.nvim_buf_delete, input_buf, { force = true })
273+
pcall(vim.api.nvim_buf_delete, output_buf, { force = true })
274+
state.windows = nil
275+
state.last_input_window_position = nil
276+
end)
277+
278+
it('does not restore cursor when already focused in input window', function()
279+
vim.api.nvim_set_current_win(input_win)
280+
vim.api.nvim_win_set_cursor(input_win, { 1, 2 })
281+
282+
ui.focus_input({ restore_position = true, start_insert = false })
283+
284+
assert.same({ 1, 2 }, vim.api.nvim_win_get_cursor(input_win))
285+
end)
286+
end)

0 commit comments

Comments
 (0)