Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 18 additions & 5 deletions lua/code-preview/diff.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment on lines +130 to +139

function M.is_open(file_path)
if file_path and file_path ~= "" then
local entry = active_diffs[file_path]
Expand Down Expand Up @@ -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)
Expand All @@ -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).
Expand All @@ -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")
Expand Down
1 change: 1 addition & 0 deletions lua/code-preview/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
24 changes: 12 additions & 12 deletions lua/code-preview/pre_tool/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -159,29 +159,29 @@ 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,
input.tool_input.old_string or "",
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)
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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
Expand All @@ -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,
}

Expand All @@ -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
Expand Down
48 changes: 46 additions & 2 deletions tests/plugin/diff_lifecycle_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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)
Comment on lines +296 to +315

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)