diff --git a/lua/code-preview/diff.lua b/lua/code-preview/diff.lua index db14932..8d19964 100644 --- a/lua/code-preview/diff.lua +++ b/lua/code-preview/diff.lua @@ -127,6 +127,17 @@ local function active_count() return n end +local function layout_for_backend(cfg, backend) + local diff_cfg = (cfg and cfg.diff) or {} + local layouts = diff_cfg.layouts or {} + + if backend and layouts[backend] then + return layouts[backend] + end + + return diff_cfg.layout or "tab" +end + function M.is_open(file_path) if file_path and file_path ~= "" then local entry = active_diffs[file_path] @@ -416,12 +427,14 @@ local function show_inline_diff(original_path, proposed_path, real_file_path, cf return { tab = tab, bufs = { buf }, inline_win = win } end -function M.show_diff(original_path, proposed_path, real_file_path, abs_file_path, action) +function M.show_diff(original_path, proposed_path, real_file_path, abs_file_path, action, backend) local file_key = abs_file_path or real_file_path local cfg = require("code-preview").config - log.info(log.fmt("show_diff: file=%s layout=%s active=%d", + local layout = layout_for_backend(cfg, backend) + log.info(log.fmt("show_diff: file=%s layout=%s backend=%s active=%d", file_key or "nil", - (cfg.diff and cfg.diff.layout) or "tab", + layout, + backend or "nil", active_count())) -- 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 mark_change_and_reveal(abs_file_path, action) -- Inline layout - if cfg.diff.layout == "inline" then + if layout == "inline" then local result = show_inline_diff(original_path, proposed_path, real_file_path, cfg) active_diffs[file_key] = result -- 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 local labels = cfg.diff.labels or { current = "CURRENT", proposed = "PROPOSED" } local ft = vim.filetype.match({ filename = real_file_path }) or "" - if cfg.diff.layout == "vsplit" then + if layout == "vsplit" then vim.cmd("vsplit") else vim.cmd("tabnew") diff --git a/lua/code-preview/init.lua b/lua/code-preview/init.lua index fdbcc6f..b586625 100644 --- a/lua/code-preview/init.lua +++ b/lua/code-preview/init.lua @@ -7,6 +7,7 @@ local default_config = { debug = false, -- enable debug logging to stdpath("log")/code-preview.log diff = { layout = "tab", -- "tab", "vsplit", or "inline" + layouts = {}, -- override layout per backend: { opencode = "tab", codex = "vsplit" } labels = { current = "CURRENT", proposed = "PROPOSED" }, equalize = true, full_file = true, diff --git a/lua/code-preview/pre_tool/init.lua b/lua/code-preview/pre_tool/init.lua index b6b7eab..ad7a130 100644 --- a/lua/code-preview/pre_tool/init.lua +++ b/lua/code-preview/pre_tool/init.lua @@ -146,7 +146,7 @@ M.display_path = display_path -- exposed for testing -- to diff.show_diff (or skip when visible_only excludes the file). The only -- per-tool variation is how `proposed_content` is computed, so each handler -- below is a one-liner around this helper. -local function present_single_file(file_path, proposed_content, input, cfg) +local function present_single_file(file_path, proposed_content, input, cfg, backend) local id = next_id() local orig = tmpdir() .. "/code-preview-diff-original-" .. id local prop = tmpdir() .. "/code-preview-diff-proposed-" .. id @@ -159,10 +159,10 @@ local function present_single_file(file_path, proposed_content, input, cfg) return end - diff.show_diff(orig, prop, display_path(file_path, input.cwd), file_path, nil) + diff.show_diff(orig, prop, display_path(file_path, input.cwd), file_path, nil, backend) end -local function handle_edit(input, cfg) +local function handle_edit(input, cfg, backend) local fp = input.tool_input.file_path local content = apply_edit.apply( fp, @@ -170,18 +170,18 @@ local function handle_edit(input, cfg) input.tool_input.new_string or "", input.tool_input.replace_all == true ) - present_single_file(fp, content, input, cfg) + present_single_file(fp, content, input, cfg, backend) end -local function handle_write(input, cfg) +local function handle_write(input, cfg, backend) local fp = input.tool_input.file_path - present_single_file(fp, input.tool_input.content or "", input, cfg) + present_single_file(fp, input.tool_input.content or "", input, cfg, backend) end -local function handle_multi_edit(input, cfg) +local function handle_multi_edit(input, cfg, backend) local fp = input.tool_input.file_path local content = apply_multi_edit.apply(fp, input.tool_input.edits or {}) - present_single_file(fp, content, input, cfg) + present_single_file(fp, content, input, cfg, backend) end local function handle_bash(input) @@ -214,7 +214,7 @@ local function handle_bash(input) end end -local function handle_apply_patch(input, cfg) +local function handle_apply_patch(input, cfg, backend) local patch_text = input.tool_input and input.tool_input.patch_text if not patch_text or patch_text == "" then log.info("pre_tool: ApplyPatch with empty patch_text") @@ -250,7 +250,7 @@ local function handle_apply_patch(input, cfg) -- whatever the model wrote in the `*** Update File:` directive, and some -- codex models (e.g. GPT 5.3) write an absolute path there, which would -- render the tab as `D:\...` instead of a cwd-relative label. - diff.show_diff(orig, prop, display_path(file.path, input.cwd), file.path, file.action) + diff.show_diff(orig, prop, display_path(file.path, input.cwd), file.path, file.action, backend) else log.info(log.fmt("pre_tool: ApplyPatch skip %s (visible_only)", file.rel_path)) end @@ -261,7 +261,7 @@ local dispatchers = { Edit = handle_edit, Write = handle_write, MultiEdit = handle_multi_edit, - Bash = function(input, _cfg) handle_bash(input) end, + Bash = function(input, _cfg, _backend) handle_bash(input) end, ApplyPatch = handle_apply_patch, } @@ -280,7 +280,7 @@ function M.handle(raw, backend) local fn = dispatchers[tool_name] if fn then - local ok, err = pcall(fn, input, cfg) + local ok, err = pcall(fn, input, cfg, backend) if not ok then log.error("pre_tool: dispatch failed: " .. tostring(err)) end diff --git a/tests/plugin/diff_lifecycle_spec.lua b/tests/plugin/diff_lifecycle_spec.lua index e50af6c..de805a0 100644 --- a/tests/plugin/diff_lifecycle_spec.lua +++ b/tests/plugin/diff_lifecycle_spec.lua @@ -213,12 +213,15 @@ describe("diff lifecycle", function() end) describe("diff layouts", function() - -- Temporarily override the diff layout for one test, restoring it afterwards. - local function with_layout(layout, fn) + -- Temporarily override diff layout config for one test, restoring it afterwards. + local function with_layout(layout, fn, layouts) local saved = require("code-preview").config.diff.layout + local saved_layouts = vim.deepcopy(require("code-preview").config.diff.layouts or {}) require("code-preview").config.diff.layout = layout + require("code-preview").config.diff.layouts = layouts or {} local ok, err = pcall(fn) require("code-preview").config.diff.layout = saved + require("code-preview").config.diff.layouts = saved_layouts if not ok then error(err, 2) end end @@ -289,4 +292,45 @@ describe("diff layouts", function() os.remove(orig) os.remove(prop) end) + + it("backend layout override wins over the default layout", function() + local orig = tmp_file("backend_vs_orig.txt", "alpha\nbeta") + local prop = tmp_file("backend_vs_prop.txt", "alpha\ngamma") + + local tabs_before = #vim.api.nvim_list_tabpages() + local tab = vim.api.nvim_get_current_tabpage() + local wins_before = #vim.api.nvim_tabpage_list_wins(tab) + + with_layout("tab", function() + diff.show_diff(orig, prop, "layout_backend_vsplit.txt", nil, nil, "codex") + end, { codex = "vsplit" }) + + assert.is_true(diff.is_open()) + assert.equals(tabs_before, #vim.api.nvim_list_tabpages()) + assert.equals(wins_before + 2, #vim.api.nvim_tabpage_list_wins(tab)) + + diff.close_diff() + os.remove(orig) + os.remove(prop) + end) + + it("missing backend falls back to the default layout", function() + local orig = tmp_file("no_backend_orig.txt", "alpha\nbeta") + local prop = tmp_file("no_backend_prop.txt", "alpha\ngamma") + + local tabs_before = #vim.api.nvim_list_tabpages() + + with_layout("tab", function() + diff.show_diff(orig, prop, "layout_no_backend.txt") + end, { codex = "vsplit" }) + + assert.is_true(diff.is_open()) + assert.equals(tabs_before + 1, #vim.api.nvim_list_tabpages()) + local diff_tabpage = vim.api.nvim_get_current_tabpage() + assert.equals(2, #vim.api.nvim_tabpage_list_wins(diff_tabpage)) + + diff.close_diff() + os.remove(orig) + os.remove(prop) + end) end)