Skip to content

Commit f9bf2da

Browse files
authored
fix(ui): preserve mounted window position (#470)
1 parent 4e9321b commit f9bf2da

9 files changed

Lines changed: 126 additions & 68 deletions

File tree

docs/recipes/three-state-layout/demo.lua

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -26,32 +26,34 @@ local ACTIONS = {
2626
if not config then
2727
return
2828
end
29-
api.toggle(false)
30-
config.ui.position = 'right'
31-
api.toggle(false)
29+
return api.toggle(false):and_then(function()
30+
config.ui.position = 'right'
31+
return api.toggle(false)
32+
end)
3233
end,
3334
to_deep = function(api)
3435
local config = get_opencode_config()
3536
if not config then
3637
return
3738
end
38-
api.toggle(false)
39-
config.ui.position = 'current'
40-
api.toggle(false)
39+
return api.toggle(false):and_then(function()
40+
config.ui.position = 'current'
41+
return api.toggle(false)
42+
end)
4143
end,
4244
open_side_by_side = function(api)
4345
local config = get_opencode_config()
4446
if config then
4547
config.ui.position = 'right'
4648
end
47-
api.toggle(false)
49+
return api.toggle(false)
4850
end,
4951
open_deep = function(api)
5052
local config = get_opencode_config()
5153
if config then
5254
config.ui.position = 'current'
5355
end
54-
api.toggle(false)
56+
return api.toggle(false)
5557
end,
5658
}
5759

@@ -82,8 +84,7 @@ local function get_current_mode(api)
8284
return MODE.focused
8385
end
8486

85-
local config = get_opencode_config()
86-
if config and config.ui.position == 'current' then
87+
if window_state.position == 'current' then
8788
return MODE.deep
8889
end
8990

@@ -103,7 +104,12 @@ local function run_transition(trigger)
103104
if not action then
104105
return
105106
end
106-
action(api)
107+
local transition = action(api)
108+
if transition then
109+
transition:catch(function(err)
110+
vim.notify('Three-state layout transition failed: ' .. tostring(err), vim.log.levels.ERROR)
111+
end)
112+
end
107113
end
108114

109115
-- Set up keymaps

lua/opencode/state/ui.lua

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ local store = require('opencode.state.store')
2424
---@field input_buf integer|nil
2525
---@field output_buf integer|nil
2626
---@field output_was_at_bottom boolean|nil
27+
---@field position 'right'|'left'|'current'|'float'|nil
2728

2829
---@class OpencodeUiStateMutations
2930
local M = {}
@@ -449,10 +450,11 @@ function M.get_window_state()
449450

450451
local status = status_rule and status_rule.status or 'closed'
451452
local current_windows = status_rule and status_rule.get_windows() or nil
453+
local hidden = status == 'hidden' and read_hidden_buffers_snapshot(false) or nil
452454

453455
return {
454456
status = status,
455-
position = config.ui.position,
457+
position = current_windows and current_windows.position or hidden and hidden.position or config.ui.position,
456458
windows = current_windows and vim.deepcopy(current_windows) or nil,
457459
cursor_positions = {
458460
input = M.get_window_cursor(current_windows and current_windows.input_win) or M.get_cursor_position('input'),

lua/opencode/ui/input_window.lua

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ local function calculate_height(windows)
5656
end
5757

5858
local function apply_dimensions(windows, height)
59-
if config.ui.position == 'current' then
59+
if windows.position == 'current' then
6060
pcall(vim.api.nvim_win_set_height, windows.input_win, height)
6161
return
6262
end
@@ -88,7 +88,7 @@ function M._build_input_win_config()
8888
end
8989

9090
function M.create_window(windows)
91-
if config.ui.position == 'float' then
91+
if windows.position == 'float' then
9292
local _, input_config = float_layout.window_configs(windows, true)
9393
windows.input_win = float_layout.open_win(windows.input_buf, true, input_config)
9494
return
@@ -277,7 +277,7 @@ function M.setup(windows)
277277
window_options.set_buffer_option('buflisted', false, windows.input_buf)
278278
window_options.set_buffer_option('swapfile', false, windows.input_buf)
279279

280-
if config.ui.position ~= 'current' then
280+
if windows.position ~= 'current' then
281281
window_options.set_window_option('winfixbuf', true, windows.input_win)
282282
end
283283
window_options.set_window_option('winfixwidth', true, windows.input_win)
@@ -295,7 +295,7 @@ function M.update_dimensions(windows)
295295
return
296296
end
297297

298-
if config.ui.position == 'float' then
298+
if windows.position == 'float' then
299299
float_layout.update(windows, true)
300300
return
301301
end
@@ -638,7 +638,7 @@ function M._hide()
638638
pcall(vim.api.nvim_win_close, windows.input_win, false)
639639
windows.input_win = nil
640640

641-
if config.ui.position == 'float' then
641+
if windows.position == 'float' then
642642
float_layout.update(windows, false)
643643
end
644644

@@ -680,7 +680,7 @@ function M._show()
680680
return
681681
end
682682

683-
if config.ui.position == 'float' then
683+
if windows.position == 'float' then
684684
local output_config, input_config = float_layout.window_configs(windows, true)
685685
pcall(vim.api.nvim_win_set_config, output_win, output_config)
686686
windows.input_win = float_layout.open_win(windows.input_buf, true, input_config)

lua/opencode/ui/navigation.lua

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,7 @@ function M.navigate_to_location(path, line, col)
196196
end
197197
local target_win = best_target_win()
198198
local windows = state.windows
199-
if config.ui.position == 'current' and windows and target_win == windows.output_win then
199+
if windows and windows.position == 'current' and target_win == windows.output_win then
200200
require('opencode.ui.ui').hide_visible_windows(windows)
201201
end
202202
return open_at(target_win, resolved_path, line, col)

lua/opencode/ui/output_window.lua

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -287,7 +287,7 @@ function M.setup(windows)
287287
end)
288288
window_options.set_window_option('foldtext', 'v:lua.opencode_fold_text()', windows.output_win)
289289

290-
if config.ui.position ~= 'current' then
290+
if windows.position ~= 'current' then
291291
window_options.set_window_option('winfixbuf', true, windows.output_win, { save_original = true })
292292
end
293293
window_options.set_window_option('winfixheight', true, windows.output_win, { save_original = true })
@@ -305,15 +305,15 @@ end
305305

306306
---@param windows OpencodeWindowState?
307307
function M.update_dimensions(windows)
308-
if config.ui.position == 'current' then
308+
if windows and windows.position == 'current' then
309309
return
310310
end
311311

312312
if not windows or not windows.output_win or not vim.api.nvim_win_is_valid(windows.output_win) then
313313
return
314314
end
315315

316-
if config.ui.position == 'float' then
316+
if windows.position == 'float' then
317317
float_layout.update(windows, windows.input_win ~= nil and vim.api.nvim_win_is_valid(windows.input_win))
318318
return
319319
end

lua/opencode/ui/ui.lua

Lines changed: 35 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ local function capture_hidden_snapshot(windows)
7676
output_cursor = cursor_positions.output,
7777
output_view = ok and type(view) == 'table' and view or nil,
7878
focused_window = focused,
79-
position = config.ui.position,
79+
position = windows.position,
8080
owner_tab = state.ui.are_windows_in_current_tab() and vim.api.nvim_get_current_tabpage() or nil,
8181
}
8282
end
@@ -115,7 +115,7 @@ local function close_or_restore_output_window(windows)
115115

116116
output_window.restore_winfix_options(windows.output_win)
117117

118-
if config.ui.position == 'current' then
118+
if windows.position == 'current' then
119119
if state.current_code_buf and vim.api.nvim_buf_is_valid(state.current_code_buf) then
120120
pcall(vim.api.nvim_win_set_buf, windows.output_win, state.current_code_buf)
121121
end
@@ -143,7 +143,7 @@ function M.hide_visible_windows(windows)
143143
local snapshot = capture_hidden_snapshot(windows)
144144

145145
-- Only save width ratio for split modes (not dialog/current mode)
146-
if config.ui.position ~= 'current' then
146+
if windows.position ~= 'current' then
147147
local total_cols = vim.o.columns
148148
local current_width = vim.api.nvim_win_get_width(windows.output_win)
149149
state.ui.set_last_window_width_ratio(current_width / total_cols)
@@ -225,21 +225,22 @@ function M.restore_hidden_windows()
225225
footer_buf = footer.create_buf()
226226
end
227227

228-
local win_ids = M.create_split_windows(hidden.input_buf, hidden.output_buf)
229-
230-
state.ui.consume_hidden_buffers()
231-
232-
state.ui.set_windows({
228+
local windows = {
233229
input_buf = hidden.input_buf,
234230
output_buf = hidden.output_buf,
235231
footer_buf = footer_buf,
236-
input_win = win_ids.input_win,
237-
output_win = win_ids.output_win,
238-
footer_win = nil,
239-
output_was_at_bottom = hidden.output_was_at_bottom == true,
240-
saved_width_ratio = state.last_window_width_ratio,
241-
})
242-
local windows = state.windows
232+
position = config.ui.position,
233+
}
234+
local win_ids = M.create_split_windows(windows)
235+
236+
state.ui.consume_hidden_buffers()
237+
238+
windows.input_win = win_ids.input_win
239+
windows.output_win = win_ids.output_win
240+
windows.footer_win = nil
241+
windows.output_was_at_bottom = hidden.output_was_at_bottom == true
242+
windows.saved_width_ratio = state.last_window_width_ratio
243+
state.ui.set_windows(windows)
243244

244245
state.ui.set_cursor_position('input', hidden.input_cursor)
245246
state.ui.set_cursor_position('output', hidden.output_cursor)
@@ -318,48 +319,43 @@ local function open_split(direction, type)
318319
return vim.api.nvim_get_current_win()
319320
end
320321

321-
---@param input_buf integer
322-
---@param output_buf integer
322+
---@param windows OpencodeWindowState
323323
---@return { input_win: integer, output_win: integer }
324-
local function open_float(input_buf, output_buf)
325-
local output_config, input_config =
326-
float_layout.window_configs({ input_buf = input_buf, output_buf = output_buf }, true)
327-
local output_win = float_layout.open_win(output_buf, true, output_config)
328-
local input_win = float_layout.open_win(input_buf, true, input_config)
324+
local function open_float(windows)
325+
local output_config, input_config = float_layout.window_configs(windows, true)
326+
local output_win = float_layout.open_win(windows.output_buf, true, output_config)
327+
local input_win = float_layout.open_win(windows.input_buf, true, input_config)
329328

330329
return { input_win = input_win, output_win = output_win }
331330
end
332331

333-
---@param input_buf integer
334-
---@param output_buf integer
332+
---@param windows OpencodeWindowState
335333
---@return { input_win: integer, output_win: integer }
336-
function M.create_split_windows(input_buf, output_buf)
334+
function M.create_split_windows(windows)
337335
if input_window.mounted() or output_window.mounted() then
338336
M.close_windows(state.windows, false)
339337
end
340-
local ui_conf = config.ui
341-
342-
if ui_conf.position == 'float' then
343-
return open_float(input_buf, output_buf)
338+
if windows.position == 'float' then
339+
return open_float(windows)
344340
end
345341

346342
local main_win
347-
if ui_conf.position == 'current' then
343+
if windows.position == 'current' then
348344
main_win = vim.api.nvim_get_current_win()
349345
else
350-
main_win = open_split(ui_conf.position, 'vertical')
346+
main_win = open_split(windows.position, 'vertical')
351347
end
352348
vim.api.nvim_set_current_win(main_win)
353349

354-
local input_win = open_split(ui_conf.input_position, 'horizontal')
350+
local input_win = open_split(config.ui.input_position, 'horizontal')
355351
local output_win = main_win
356352

357-
if ui_conf.position == 'current' then
353+
if windows.position == 'current' then
358354
pcall(vim.api.nvim_set_option_value, 'winfixbuf', false, { win = output_win })
359355
end
360356

361-
vim.api.nvim_win_set_buf(input_win, input_buf)
362-
vim.api.nvim_win_set_buf(output_win, output_buf)
357+
vim.api.nvim_win_set_buf(input_win, windows.input_buf)
358+
vim.api.nvim_win_set_buf(output_win, windows.output_buf)
363359
return { input_win = input_win, output_win = output_win }
364360
end
365361

@@ -383,16 +379,16 @@ function M.create_windows()
383379
end
384380

385381
-- Create new windows from scratch
386-
local buffers = M.setup_buffers()
387-
local windows = buffers
388-
local win_ids = M.create_split_windows(buffers.input_buf, buffers.output_buf)
382+
local windows = M.setup_buffers()
383+
windows.position = config.ui.position
384+
local win_ids = M.create_split_windows(windows)
389385

390386
windows.input_win = win_ids.input_win
391387
windows.output_win = win_ids.output_win
392388

393389
local filetype = config.ui.output.filetype or 'opencode_output'
394390
vim.api.nvim_win_call(windows.output_win, function()
395-
vim.api.nvim_set_option_value('filetype', filetype, { buf = buffers.output_buf })
391+
vim.api.nvim_set_option_value('filetype', filetype, { buf = windows.output_buf })
396392
end)
397393

398394
windows.saved_width_ratio = state.last_window_width_ratio

tests/unit/navigation_spec.lua

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ describe('output token navigation', function()
3939
col = 0,
4040
})
4141

42-
state.ui.set_windows({ output_buf = output_buf, output_win = output_win })
42+
state.ui.set_windows({ output_buf = output_buf, output_win = output_win, position = 'right' })
4343
state.ui.set_last_code_window(code_win)
4444
end)
4545

@@ -518,6 +518,7 @@ describe('output token navigation', function()
518518
input_win = input_win,
519519
output_buf = output_buf,
520520
output_win = code_win,
521+
position = 'current',
521522
})
522523
state.ui.set_last_code_window(code_win)
523524
state.ui.set_current_code_buf(code_buf)
@@ -570,7 +571,7 @@ describe('navigation jumplist preservation', function()
570571
col = 0,
571572
})
572573

573-
state.ui.set_windows({ output_buf = output_buf, output_win = output_win })
574+
state.ui.set_windows({ output_buf = output_buf, output_win = output_win, position = 'right' })
574575
state.ui.set_last_code_window(code_win)
575576
end)
576577

@@ -673,7 +674,7 @@ describe('navigation hidden-messages-notice handling', function()
673674
row = 0,
674675
col = 0,
675676
})
676-
state.ui.set_windows({ output_buf = output_buf, output_win = output_win })
677+
state.ui.set_windows({ output_buf = output_buf, output_win = output_win, position = 'right' })
677678
end)
678679

679680
after_each(function()

0 commit comments

Comments
 (0)