Skip to content

Commit 574c536

Browse files
committed
fix: prevent duplicate windows by implementing buffer-to-window binding
This should fix #144
1 parent 0708c35 commit 574c536

6 files changed

Lines changed: 550 additions & 47 deletions

File tree

lua/opencode/ui/autocmds.lua

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -81,15 +81,12 @@ function M.setup_resize_handler(windows)
8181
vim.api.nvim_create_autocmd('VimResized', {
8282
group = resize_group,
8383
callback = function()
84-
require('opencode.ui.ui').reconcile_windows(windows, 'input')
85-
require('opencode.ui.ui').reconcile_windows(windows, 'output')
8684
require('opencode.ui.topbar').render()
8785
require('opencode.ui.footer').update_window(windows)
8886
input_window.update_dimensions(windows)
8987
output_window.update_dimensions(windows)
9088
end,
9189
})
92-
9390
vim.api.nvim_create_autocmd('WinResized', {
9491
group = resize_group,
9592
callback = function(args)

lua/opencode/ui/buf_fix_win.lua

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
--- Prevents a buffer from appearing in multiple windows (opposite of 'winfixbuf')
2+
---
3+
--- This module solves the problem where buffers can appear in multiple windows
4+
--- despite having 'winfixbuf' set. The 'winfixbuf' option prevents a window from
5+
--- changing buffers, but doesn't prevent a buffer from appearing in multiple windows.
6+
---
7+
local M = {}
8+
local buff_to_win_map = {}
9+
local global_autocmd_setup = false
10+
11+
local function close_duplicates(buf, get_win)
12+
local intended = get_win()
13+
if not intended or not vim.api.nvim_win_is_valid(intended) then
14+
return
15+
end
16+
17+
local wins = vim.fn.win_findbuf(buf)
18+
if #wins <= 1 then
19+
return
20+
end
21+
22+
for _, win in ipairs(wins) do
23+
if win ~= intended and vim.api.nvim_win_is_valid(win) then
24+
vim.schedule(function()
25+
pcall(vim.api.nvim_win_close, win, true)
26+
end)
27+
end
28+
end
29+
end
30+
31+
local check_all_buffers = vim.schedule_wrap(function()
32+
for buf, get_win in pairs(buff_to_win_map) do
33+
if vim.api.nvim_buf_is_valid(buf) then
34+
close_duplicates(buf, get_win)
35+
else
36+
buff_to_win_map[buf] = nil
37+
end
38+
end
39+
end)
40+
41+
local function setup()
42+
if global_autocmd_setup then
43+
return
44+
end
45+
global_autocmd_setup = true
46+
47+
vim.api.nvim_create_autocmd({ 'WinNew', 'VimResized' }, {
48+
callback = check_all_buffers,
49+
})
50+
end
51+
52+
--- Protect a buffer from appearing in multiple windows
53+
---@param buf integer Buffer number
54+
---@param get_intended_window fun(): integer? Function returning intended window ID
55+
function M.fix_to_win(buf, get_intended_window)
56+
setup()
57+
58+
buff_to_win_map[buf] = get_intended_window
59+
vim.api.nvim_create_autocmd('BufWinEnter', {
60+
buffer = buf,
61+
callback = function()
62+
close_duplicates(buf, get_intended_window)
63+
end,
64+
})
65+
end
66+
67+
return M

lua/opencode/ui/input_window.lua

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,13 @@ end
6262
function M.create_buf()
6363
local input_buf = vim.api.nvim_create_buf(false, true)
6464
vim.api.nvim_set_option_value('filetype', 'opencode', { buf = input_buf })
65+
66+
local buffixwin = require('opencode.ui.buf_fix_win')
67+
buffixwin.fix_to_win(input_buf, function()
68+
local state = require('opencode.state')
69+
return state.windows and state.windows.input_win
70+
end)
71+
6572
return input_buf
6673
end
6774

@@ -261,6 +268,8 @@ function M.setup(windows)
261268
set_win_option('number', false, windows)
262269
set_win_option('relativenumber', false, windows)
263270
set_buf_option('buftype', 'nofile', windows)
271+
set_buf_option('bufhidden', 'hide', windows)
272+
set_buf_option('buflisted', false, windows)
264273
set_buf_option('swapfile', false, windows)
265274

266275
if config.ui.position ~= 'current' then

lua/opencode/ui/output_window.lua

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,13 @@ M.viewport_at_bottom = true
88
function M.create_buf()
99
local output_buf = vim.api.nvim_create_buf(false, true)
1010
vim.api.nvim_set_option_value('filetype', 'opencode_output', { buf = output_buf })
11+
12+
local buffixwin = require('opencode.ui.buf_fix_win')
13+
buffixwin.fix_to_win(output_buf, function()
14+
local state = require('opencode.state')
15+
return state.windows and state.windows.output_win
16+
end)
17+
1118
return output_buf
1219
end
1320

@@ -97,7 +104,10 @@ function M.setup(windows)
97104
set_win_option('relativenumber', false, windows.output_win)
98105
set_buf_option('modifiable', false, windows.output_buf)
99106
set_buf_option('buftype', 'nofile', windows.output_buf)
107+
set_buf_option('bufhidden', 'hide', windows.output_buf)
108+
set_buf_option('buflisted', false, windows.output_buf)
100109
set_buf_option('swapfile', false, windows.output_buf)
110+
101111
if config.ui.position ~= 'current' then
102112
set_win_option('winfixbuf', true, windows.output_win)
103113
end

lua/opencode/ui/ui.lua

Lines changed: 0 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -296,48 +296,4 @@ function M.toggle_zoom()
296296
end
297297
end
298298

299-
---Reconcile window state when duplicate windows may have been created
300-
---This can happen when external commands like 'tabdo' manipulate window layouts
301-
---@param windows OpencodeWindowState?
302-
---@param key string
303-
function M.reconcile_windows(windows, key)
304-
local buf_key = key .. '_buf'
305-
if not windows or not windows[buf_key] then
306-
return
307-
end
308-
309-
local wins = vim.fn.win_findbuf(windows[buf_key])
310-
if type(wins) ~= 'table' or #wins == 0 then
311-
return
312-
end
313-
314-
local valid_wins = {}
315-
for _, win in ipairs(wins) do
316-
if vim.api.nvim_win_is_valid(win) then
317-
table.insert(valid_wins, win)
318-
end
319-
end
320-
if #valid_wins == 0 then
321-
return
322-
end
323-
324-
local current_tab = vim.api.nvim_get_current_tabpage()
325-
local preferred = nil
326-
for _, win in ipairs(valid_wins) do
327-
if vim.api.nvim_win_get_tabpage(win) == current_tab then
328-
preferred = win
329-
break
330-
end
331-
end
332-
preferred = preferred or valid_wins[1]
333-
334-
windows[key .. '_win'] = preferred
335-
336-
for _, win in ipairs(valid_wins) do
337-
if win ~= preferred and vim.api.nvim_win_get_tabpage(win) == current_tab then
338-
pcall(vim.api.nvim_win_close, win, false)
339-
end
340-
end
341-
end
342-
343299
return M

0 commit comments

Comments
 (0)