Skip to content

Commit 3146c28

Browse files
authored
feat: Added support for backend-specific layouts (#82)
1 parent e8c3366 commit 3146c28

4 files changed

Lines changed: 77 additions & 19 deletions

File tree

lua/code-preview/diff.lua

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,17 @@ local function active_count()
127127
return n
128128
end
129129

130+
local function layout_for_backend(cfg, backend)
131+
local diff_cfg = (cfg and cfg.diff) or {}
132+
local layouts = diff_cfg.layouts or {}
133+
134+
if backend and layouts[backend] then
135+
return layouts[backend]
136+
end
137+
138+
return diff_cfg.layout or "tab"
139+
end
140+
130141
function M.is_open(file_path)
131142
if file_path and file_path ~= "" then
132143
local entry = active_diffs[file_path]
@@ -416,12 +427,14 @@ local function show_inline_diff(original_path, proposed_path, real_file_path, cf
416427
return { tab = tab, bufs = { buf }, inline_win = win }
417428
end
418429

419-
function M.show_diff(original_path, proposed_path, real_file_path, abs_file_path, action)
430+
function M.show_diff(original_path, proposed_path, real_file_path, abs_file_path, action, backend)
420431
local file_key = abs_file_path or real_file_path
421432
local cfg = require("code-preview").config
422-
log.info(log.fmt("show_diff: file=%s layout=%s active=%d",
433+
local layout = layout_for_backend(cfg, backend)
434+
log.info(log.fmt("show_diff: file=%s layout=%s backend=%s active=%d",
423435
file_key or "nil",
424-
(cfg.diff and cfg.diff.layout) or "tab",
436+
layout,
437+
backend or "nil",
425438
active_count()))
426439

427440
-- If a diff for this SAME file is already open, close it first (re-edit)
@@ -434,7 +447,7 @@ function M.show_diff(original_path, proposed_path, real_file_path, abs_file_path
434447
mark_change_and_reveal(abs_file_path, action)
435448

436449
-- Inline layout
437-
if cfg.diff.layout == "inline" then
450+
if layout == "inline" then
438451
local result = show_inline_diff(original_path, proposed_path, real_file_path, cfg)
439452
active_diffs[file_key] = result
440453
-- Force terminal redraw so RPC-triggered tab creation is visible (see force_redraw).
@@ -449,7 +462,7 @@ function M.show_diff(original_path, proposed_path, real_file_path, abs_file_path
449462
local labels = cfg.diff.labels or { current = "CURRENT", proposed = "PROPOSED" }
450463
local ft = vim.filetype.match({ filename = real_file_path }) or ""
451464

452-
if cfg.diff.layout == "vsplit" then
465+
if layout == "vsplit" then
453466
vim.cmd("vsplit")
454467
else
455468
vim.cmd("tabnew")

lua/code-preview/init.lua

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ local default_config = {
77
debug = false, -- enable debug logging to stdpath("log")/code-preview.log
88
diff = {
99
layout = "tab", -- "tab", "vsplit", or "inline"
10+
layouts = {}, -- override layout per backend: { opencode = "tab", codex = "vsplit" }
1011
labels = { current = "CURRENT", proposed = "PROPOSED" },
1112
equalize = true,
1213
full_file = true,

lua/code-preview/pre_tool/init.lua

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ M.display_path = display_path -- exposed for testing
146146
-- to diff.show_diff (or skip when visible_only excludes the file). The only
147147
-- per-tool variation is how `proposed_content` is computed, so each handler
148148
-- below is a one-liner around this helper.
149-
local function present_single_file(file_path, proposed_content, input, cfg)
149+
local function present_single_file(file_path, proposed_content, input, cfg, backend)
150150
local id = next_id()
151151
local orig = tmpdir() .. "/code-preview-diff-original-" .. id
152152
local prop = tmpdir() .. "/code-preview-diff-proposed-" .. id
@@ -159,29 +159,29 @@ local function present_single_file(file_path, proposed_content, input, cfg)
159159
return
160160
end
161161

162-
diff.show_diff(orig, prop, display_path(file_path, input.cwd), file_path, nil)
162+
diff.show_diff(orig, prop, display_path(file_path, input.cwd), file_path, nil, backend)
163163
end
164164

165-
local function handle_edit(input, cfg)
165+
local function handle_edit(input, cfg, backend)
166166
local fp = input.tool_input.file_path
167167
local content = apply_edit.apply(
168168
fp,
169169
input.tool_input.old_string or "",
170170
input.tool_input.new_string or "",
171171
input.tool_input.replace_all == true
172172
)
173-
present_single_file(fp, content, input, cfg)
173+
present_single_file(fp, content, input, cfg, backend)
174174
end
175175

176-
local function handle_write(input, cfg)
176+
local function handle_write(input, cfg, backend)
177177
local fp = input.tool_input.file_path
178-
present_single_file(fp, input.tool_input.content or "", input, cfg)
178+
present_single_file(fp, input.tool_input.content or "", input, cfg, backend)
179179
end
180180

181-
local function handle_multi_edit(input, cfg)
181+
local function handle_multi_edit(input, cfg, backend)
182182
local fp = input.tool_input.file_path
183183
local content = apply_multi_edit.apply(fp, input.tool_input.edits or {})
184-
present_single_file(fp, content, input, cfg)
184+
present_single_file(fp, content, input, cfg, backend)
185185
end
186186

187187
local function handle_bash(input)
@@ -214,7 +214,7 @@ local function handle_bash(input)
214214
end
215215
end
216216

217-
local function handle_apply_patch(input, cfg)
217+
local function handle_apply_patch(input, cfg, backend)
218218
local patch_text = input.tool_input and input.tool_input.patch_text
219219
if not patch_text or patch_text == "" then
220220
log.info("pre_tool: ApplyPatch with empty patch_text")
@@ -250,7 +250,7 @@ local function handle_apply_patch(input, cfg)
250250
-- whatever the model wrote in the `*** Update File:` directive, and some
251251
-- codex models (e.g. GPT 5.3) write an absolute path there, which would
252252
-- render the tab as `D:\...` instead of a cwd-relative label.
253-
diff.show_diff(orig, prop, display_path(file.path, input.cwd), file.path, file.action)
253+
diff.show_diff(orig, prop, display_path(file.path, input.cwd), file.path, file.action, backend)
254254
else
255255
log.info(log.fmt("pre_tool: ApplyPatch skip %s (visible_only)", file.rel_path))
256256
end
@@ -261,7 +261,7 @@ local dispatchers = {
261261
Edit = handle_edit,
262262
Write = handle_write,
263263
MultiEdit = handle_multi_edit,
264-
Bash = function(input, _cfg) handle_bash(input) end,
264+
Bash = function(input, _cfg, _backend) handle_bash(input) end,
265265
ApplyPatch = handle_apply_patch,
266266
}
267267

@@ -280,7 +280,7 @@ function M.handle(raw, backend)
280280

281281
local fn = dispatchers[tool_name]
282282
if fn then
283-
local ok, err = pcall(fn, input, cfg)
283+
local ok, err = pcall(fn, input, cfg, backend)
284284
if not ok then
285285
log.error("pre_tool: dispatch failed: " .. tostring(err))
286286
end

tests/plugin/diff_lifecycle_spec.lua

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -213,12 +213,15 @@ describe("diff lifecycle", function()
213213
end)
214214

215215
describe("diff layouts", function()
216-
-- Temporarily override the diff layout for one test, restoring it afterwards.
217-
local function with_layout(layout, fn)
216+
-- Temporarily override diff layout config for one test, restoring it afterwards.
217+
local function with_layout(layout, fn, layouts)
218218
local saved = require("code-preview").config.diff.layout
219+
local saved_layouts = vim.deepcopy(require("code-preview").config.diff.layouts or {})
219220
require("code-preview").config.diff.layout = layout
221+
require("code-preview").config.diff.layouts = layouts or {}
220222
local ok, err = pcall(fn)
221223
require("code-preview").config.diff.layout = saved
224+
require("code-preview").config.diff.layouts = saved_layouts
222225
if not ok then error(err, 2) end
223226
end
224227

@@ -289,4 +292,45 @@ describe("diff layouts", function()
289292
os.remove(orig)
290293
os.remove(prop)
291294
end)
295+
296+
it("backend layout override wins over the default layout", function()
297+
local orig = tmp_file("backend_vs_orig.txt", "alpha\nbeta")
298+
local prop = tmp_file("backend_vs_prop.txt", "alpha\ngamma")
299+
300+
local tabs_before = #vim.api.nvim_list_tabpages()
301+
local tab = vim.api.nvim_get_current_tabpage()
302+
local wins_before = #vim.api.nvim_tabpage_list_wins(tab)
303+
304+
with_layout("tab", function()
305+
diff.show_diff(orig, prop, "layout_backend_vsplit.txt", nil, nil, "codex")
306+
end, { codex = "vsplit" })
307+
308+
assert.is_true(diff.is_open())
309+
assert.equals(tabs_before, #vim.api.nvim_list_tabpages())
310+
assert.equals(wins_before + 2, #vim.api.nvim_tabpage_list_wins(tab))
311+
312+
diff.close_diff()
313+
os.remove(orig)
314+
os.remove(prop)
315+
end)
316+
317+
it("missing backend falls back to the default layout", function()
318+
local orig = tmp_file("no_backend_orig.txt", "alpha\nbeta")
319+
local prop = tmp_file("no_backend_prop.txt", "alpha\ngamma")
320+
321+
local tabs_before = #vim.api.nvim_list_tabpages()
322+
323+
with_layout("tab", function()
324+
diff.show_diff(orig, prop, "layout_no_backend.txt")
325+
end, { codex = "vsplit" })
326+
327+
assert.is_true(diff.is_open())
328+
assert.equals(tabs_before + 1, #vim.api.nvim_list_tabpages())
329+
local diff_tabpage = vim.api.nvim_get_current_tabpage()
330+
assert.equals(2, #vim.api.nvim_tabpage_list_wins(diff_tabpage))
331+
332+
diff.close_diff()
333+
os.remove(orig)
334+
os.remove(prop)
335+
end)
292336
end)

0 commit comments

Comments
 (0)