Repository navigation
[FEATURE] Inline diff #82
Description
Activity
Until this feature has been added, is there a way to prevent the split diffs from appearing and only see the suggested changes in the claude code sidebar?
You can try creating a custom output style with something like this:
/output-style:new Claude only shows diffs for suggested changes in the Claude Code pane, but doesn't create diffs in the linked IDE.With this command, Claude created for me a custom style in~/.claude/output-styles/diff-review-only.mdand when I activated this output style with/output-style, Claude really reacted by just showing the diff in the Claude Code pane (at least for a simple refactor task). When I re-enabled the default output style, Claude started creating diffs in Nvim again.Until this feature has been added, is there a way to prevent the split diffs from appearing and only see the suggested changes in the claude code sidebar?
There's a setting in Claude Code for this. Once connected to the Neovim plugin, a "Diff Tool" option will appear under
/config. You can then select whether diffs are displayed in Neovim (auto) or within Claude Code (terminal).
Diffs will be shown in Neovim:
Diffs will be shown in Claude Code:
Reacted by Dan Nguyen, Luis A. Rodriguez, Nick Pupko, Daniel Imfeld, Kyle Awayan and AndrewReacted by Jakub F. Bortlík and Kyle AwayanNote
This triage report is AI-generated using Claude Code
Triage: inline diff
This issue is a feature request. Reading through the thread, two distinct asks are present, and separating them helps:
- (A) An inline / unified diff inside Neovim — a single, compact buffer with additions and deletions interleaved, instead of the current side-by-side split. This is the original request.
- (B) Suppressing the Neovim diff entirely — raised later in the thread — so proposed changes are shown only inside the Claude Code pane. This one is already solved: selecting
/config→ Diff Tool →terminalmakes Claude render diffs in its own TUI and never call the editor'sopenDiffat all (as covered earlier in this thread). The custom output-style suggestion mentioned above also works; the built-in setting is the durable answer.
The remainder of this report concerns ask (A).
Is an inline diff feasible and in scope?
Yes on both counts.
- The protocol does not mandate a layout. The
openDiffMCP tool is defined by four parameters and two text results (FILE_SAVED+ final contents on accept,DIFF_REJECTED+ tab name on reject) — there is no orientation / side-by-side field anywhere (PROTOCOL.md§2;lua/claudecode/tools/open_diff.lua). Diff presentation is therefore entirely client-side. This is corroborated by an independent client of the identical protocol (manzaltu/claude-code-ide.el), which renders its own way while honoring the same contract. - Relationship to the official VS Code experience. The official extension's default is actually a side-by-side, editable comparison; its inline / unified view is a VS Code editor toggle (
diffEditor.renderSideBySide), not a Claude setting. The current side-by-side layout here is therefore faithful to upstream, and an inline view would be an opt-in enhancement rather than a parity fix. - It can be done with zero dependencies. A single-buffer unified diff is achievable with built-ins only —
vim.diff()for hunk computation plus extmarks for rendering — on Neovim ≥ 0.9. (Verified againstapi.txtat the v0.9.0 tag:virt_lines,line_hl_group,sign_text, andsign_hl_groupare all present then;vim.diffitself dates to 0.6 and was renamedvim.text.diffin 0.12, with the old name kept as an alias.)
An implementation already exists: PR #195
PR #195 already implements this. It adds a self-contained
diff_inline.lua(purevim.diff()+ extmark rendering with+/-sign markers), dispatches accept / reject / cleanup onlayout == "inline", and ships 23 tests.The approach was reviewed and exercised during this triage:
- Correctness — confirmed. The diff computation and accept-time content extraction reconstruct exactly the proposed file across modify / new-file / pure-deletion cases (the read-only buffer keeps deleted lines for display, and accept strips them via the saved line-type array). A standalone run is in the first collapsible box below.
- Version gate — correct. The stated requirement of Neovim ≥ 0.9 is accurate, and even slightly conservative.
- Points worth addressing before merge:
- The branch is currently out of date and conflicts with
main(its base predates roughly 35 commits ofdiff.luarefactoring — including the reject-on-:q, only-window, new-tab, foreign-diff, and terminal-resize changes), so a rebase is required. vim.diff()is called directly; bindingvim.text.diff or vim.diffkeeps it forward-compatible with 0.12+.- The buffer is rendered read-only, which forfeits the ability to edit a proposal before accepting — see the next section.
- The branch is currently out of date and conflicts with
Design note: editability
The current side-by-side layout (and the official VS Code experience) allows a proposal to be edited before accepting, with the final buffer contents returned in
FILE_SAVED. The inline buffer in PR #195 is read-only, so that capability is lost in inline mode. The same trade-off appears in a sibling fork that also moved to an extmark overlay (douglasjordan2/claudecode.nvim).This trade-off is avoidable. If the proposed lines are rendered as real, editable buffer lines (highlighted, with a
+sign) and the deleted lines are shown as read-only virtual lines (virt_lines), the inline view stays compact and editable, and accept simply returns the live buffer contents. A prototype of this pattern is in the second collapsible box; a simulated edit to a proposed line is correctly captured on accept while the virtual deletions are excluded. This mirrors howcodecompanion.nvim's built-in inline engine andfolke/sidekick.nvimrender.On delegating to an external diff engine (relates to #169)
Delegating the diff view to
diffview.nvimorcodediff.nvimwas also evaluated, since #169 requests it. It is not recommended:diffview.nvimrequires a Git root and a heavier (GPL-3) dependency and still leaves the blocking accept / reject + final-content handshake to be built here;codediff.nvimcannot render an in-memory right-hand side at all. Notably,codecompanion.nvimremoved its pluggable diff providers (includingmini.diff) in favor of a single built-in engine — a useful precedent. A self-contained, pure-Lua engine is the better fit for this project's zero-dependency design. (A bonus of a single-buffer extmark view: it opens no&diffwindows, so it sidesteps the class of foreign-diff-window problems seen in #277 and the stale-diff accumulation in #205.)Workarounds available today
- For ask (B):
/config→ Diff Tool →terminal(built-in), or a custom output style. - For ask (A): there is no configuration-only workaround — the native
diffoptinline options only affect two-buffer diff mode, not a single-buffer unified view — so this needs code (i.e. PR feat(diff): add layout = "inline" for VS Code-style unified diff #195 or equivalent). The closest existing options arediff_opts.layout = "horizontal"(less width) or the terminal Diff Tool above.
Minor documentation / cleanup notes
- The README documents only
layout = "vertical" | "horizontal"; an inline option would need a docs entry. diff_opts.auto_close_on_acceptanddiff_opts.show_diff_statsare validated inconfig.luabut not implemented or documented anywhere (they appear to date back to an early PR) — candidates for either implementation or removal.
Suggested direction
Adopting PR #195 as the basis — after a rebase, the forward-compatible diff call, and a decision on the editability model (the editable proposed-lines pattern is recommended) — looks like the most sensible path, and #169 can be addressed by the same self-contained engine rather than an external dependency.
The two prototypes used to ground this report are included below for review.
proto_inline.lua — verification of PR #195's diff algorithm (run under headless Neovim 0.12.3)
-- Standalone prototype of PR #195's inline-diff core (pure functions copied verbatim), -- exercised in real headless nvim to verify correctness + probe version gates. local function split_lines(text) if text == "" then return {} end local lines = vim.split(text, "\n", { plain = true }) if #lines > 0 and lines[#lines] == "" then table.remove(lines) end return lines end local function compute_inline_diff(old_text, new_text) old_text = old_text or "" new_text = new_text or "" local hunks = vim.diff(old_text, new_text, { result_type = "indices" }) or {} local old_lines = split_lines(old_text) local new_lines = split_lines(new_text) local result_lines, result_types = {}, {} local old_pos, new_pos = 1, 1 for _, hunk in ipairs(hunks) do local start_a, count_a, _, count_b = hunk[1], hunk[2], hunk[3], hunk[4] local unchanged_count if count_a > 0 then unchanged_count = start_a - old_pos else unchanged_count = start_a - old_pos + 1 end for _ = 1, unchanged_count do result_lines[#result_lines + 1] = new_lines[new_pos] result_types[#result_types + 1] = "unchanged" old_pos = old_pos + 1; new_pos = new_pos + 1 end for _ = 1, count_a do result_lines[#result_lines + 1] = old_lines[old_pos] result_types[#result_types + 1] = "deleted" old_pos = old_pos + 1 end for _ = 1, count_b do result_lines[#result_lines + 1] = new_lines[new_pos] result_types[#result_types + 1] = "added" new_pos = new_pos + 1 end end while new_pos <= #new_lines do result_lines[#result_lines + 1] = new_lines[new_pos] result_types[#result_types + 1] = "unchanged" new_pos = new_pos + 1 end return result_lines, result_types end local function extract_new_content(lines, line_types) local out = {} for i, lt in ipairs(line_types) do if lt ~= "deleted" then out[#out + 1] = lines[i] end end return table.concat(out, "\n") end local function P(s) io.write(s .. "\n") end P("=== nvim version ===") P(tostring(vim.version())) P("vim.diff present: " .. tostring(vim.diff ~= nil)) -- Probe: does nvim_buf_set_extmark accept sign_text in THIS version? local probe_buf = vim.api.nvim_create_buf(false, true) vim.api.nvim_buf_set_lines(probe_buf, 0, -1, false, { "x" }) local ns = vim.api.nvim_create_namespace("proto") local ok_sign = pcall(vim.api.nvim_buf_set_extmark, probe_buf, ns, 0, 0, { sign_text = "+", sign_hl_group = "DiffAdd", line_hl_group = "DiffAdd", }) P("nvim_buf_set_extmark{sign_text=...} accepted: " .. tostring(ok_sign)) local function show_case(title, old_text, new_text) P("\n=== CASE: " .. title .. " ===") local lines, types = compute_inline_diff(old_text, new_text) for i = 1, #lines do local mark = ({ unchanged = " ", added = "+ ", deleted = "- " })[types[i]] P(string.format("%s%s", mark, lines[i])) end local extracted = extract_new_content(lines, types) -- The accepted content must equal new_text (modulo trailing newline handling) local norm = function(s) return (s:gsub("\n$", "")) end local match = norm(extracted) == norm(new_text) P(string.format("-> extract_new_content == new_text ? %s", tostring(match))) if not match then P(" !! MISMATCH") P(" extracted=[" .. extracted .. "]") P(" expected =[" .. norm(new_text) .. "]") end end local old1 = table.concat({ "local function greet(name)", " print('hi ' .. name)", " return true", "end", }, "\n") local new1 = table.concat({ "local function greet(name)", " -- be polite", " print('hello, ' .. name .. '!')", " return true", "end", }, "\n") show_case("modify a function", old1, new1) show_case("new file (old empty)", "", "line one\nline two\nline three") show_case("pure deletion", "keep\ndrop me\nkeep2", "keep\nkeep2") -- Demonstrate the read-only limitation: in inline mode the buffer holds deleted -- lines too, so accept must reconstruct from line_types (NOT from buffer text). -- If a user could edit the buffer, their edits to 'deleted' lines would be discarded, -- and edits to 'added' lines are only preserved if accept re-reads the buffer (it does NOT). P("\n=== editability note ===") P("PR #195 sets modifiable=false and reconstructs accepted content from the saved") P("line_types array, so user edits to the proposal are impossible (unlike vertical/horizontal,") P("where the proposed buffer is modifiable and accept re-reads live buffer lines).") vim.cmd("qa!")
Output (headless
nvim --headless -l):=== nvim version === 0.12.3+v0.12.3 vim.diff present: true nvim_buf_set_extmark{sign_text=...} accepted: true === CASE: modify a function === local function greet(name) - print('hi ' .. name) + -- be polite + print('hello, ' .. name .. '!') return true end -> extract_new_content == new_text ? true === CASE: new file (old empty) === + line one + line two + line three -> extract_new_content == new_text ? true === CASE: pure deletion === keep - drop me keep2 -> extract_new_content == new_text ? true === editability note === PR #195 sets modifiable=false and reconstructs accepted content from the saved line_types array, so user edits to the proposal are impossible (unlike vertical/horizontal, where the proposed buffer is modifiable and accept re-reads live buffer lines).proto_editable_inline.lua — prototype of the editability-preserving inline pattern (proposed lines real & editable, deletions as virtual lines)
-- Prototype: editability-PRESERVING inline unified diff (the pattern PR #195 does NOT use). -- Buffer holds the NEW/proposed content as REAL, EDITABLE lines (added lines highlighted + -- "+" sign). Deleted OLD lines are shown as read-only virtual lines (virt_lines) anchored in -- place. On accept, final content = the live buffer text (so user edits are captured), exactly -- like the current vertical/horizontal split and like VS Code. local diff_fn = vim.text and vim.text.diff or vim.diff -- forward-compatible (0.12 renamed it) local function split_lines(text) if text == "" then return {} end local l = vim.split(text, "\n", { plain = true }) if #l > 0 and l[#l] == "" then table.remove(l) end return l end local ns = vim.api.nvim_create_namespace("proto_editable") vim.api.nvim_set_hl(0, "PInlAdd", { link = "DiffAdd", default = true }) vim.api.nvim_set_hl(0, "PInlDel", { link = "DiffDelete", default = true }) --- Render new_text as real editable lines; show deletions as virt_lines. Returns buf. local function render(old_text, new_text) local buf = vim.api.nvim_create_buf(false, true) local new_lines = split_lines(new_text) local old_lines = split_lines(old_text) vim.api.nvim_buf_set_lines(buf, 0, -1, false, new_lines) -- REAL proposed content vim.bo[buf].modifiable = true -- editable! local hunks = diff_fn(old_text, new_text, { result_type = "indices" }) or {} local added, deleted = 0, 0 for _, h in ipairs(hunks) do local start_a, count_a, start_b, count_b = h[1], h[2], h[3], h[4] -- Added new lines -> real lines, highlight + "+" sign for r = start_b, start_b + count_b - 1 do added = added + 1 vim.api.nvim_buf_set_extmark(buf, ns, r - 1, 0, { line_hl_group = "PInlAdd", sign_text = "+", sign_hl_group = "PInlAdd", }) end -- Deleted old lines -> read-only virtual lines, anchored at the hunk local vlines = {} for r = start_a, start_a + count_a - 1 do deleted = deleted + 1 vlines[#vlines + 1] = { { "- " .. (old_lines[r] or ""), "PInlDel" } } end if #vlines > 0 then if count_b > 0 then -- anchor above the first added line vim.api.nvim_buf_set_extmark(buf, ns, start_b - 1, 0, { virt_lines = vlines, virt_lines_above = true }) else -- pure deletion: anchor below the preceding kept line (start_b is the kept line) local anchor = math.max(start_b - 1, 0) vim.api.nvim_buf_set_extmark(buf, ns, anchor, 0, { virt_lines = vlines, virt_lines_above = false }) end end end return buf, added, deleted end local function P(s) io.write(s .. "\n") end local function dump(buf) local lines = vim.api.nvim_buf_get_lines(buf, 0, -1, false) local marks = vim.api.nvim_buf_get_extmarks(buf, ns, 0, -1, { details = true }) -- index extmarks by row local add_at, del_at = {}, {} for _, m in ipairs(marks) do local row, det = m[2], m[4] if det.line_hl_group == "PInlAdd" then add_at[row] = true end if det.virt_lines then del_at[row] = det.virt_lines end end for i = 0, #lines - 1 do -- print any deletion virt_lines anchored ABOVE this row if del_at[i] then for _, vl in ipairs(del_at[i]) do P(" ¦ " .. vl[1][1]) end end P((add_at[i] and "+ " or " ") .. lines[i + 1]) end end local old1 = table.concat({ "function greet(name)", " print('hi ' .. name)", " return true", "end", }, "\n") local new1 = table.concat({ "function greet(name)", " -- be polite", " print('hello, ' .. name .. '!')", " return true", "end", }, "\n") P("=== editable inline render (proposed=real, deleted=virtual) ===") P("nvim " .. tostring(vim.version()) .. " diff_fn=" .. (vim.text and vim.text.diff and "vim.text.diff" or "vim.diff")) local buf, added, deleted = render(old1, new1) dump(buf) P(string.format("-> %d added (real,editable), %d deleted (virtual,read-only)", added, deleted)) -- Editability proof: user tweaks an added line, then accepts. local cur = vim.api.nvim_buf_get_lines(buf, 0, -1, false) for i, l in ipairs(cur) do if l:find("hello, ", 1, true) then cur[i] = " print('Hello there, ' .. name)" end end vim.api.nvim_buf_set_lines(buf, 0, -1, false, cur) -- simulate a user edit in the diff buffer local accepted = table.concat(vim.api.nvim_buf_get_lines(buf, 0, -1, false), "\n") P("\n=== accept after user edit -> FILE_SAVED content is the LIVE buffer (edits captured) ===") P(accepted) P("contains user's edited text? " .. tostring(accepted:find("Hello there", 1, true) ~= nil)) P("virtual deleted lines are NOT in accepted content? " .. tostring(accepted:find("print%('hi '") == nil)) vim.cmd("qa!")
Output (headless
nvim --headless -l):=== editable inline render (proposed=real, deleted=virtual) === nvim 0.12.3+v0.12.3 diff_fn=vim.text.diff function greet(name) ¦ - print('hi ' .. name) + -- be polite + print('hello, ' .. name .. '!') return true end -> 2 added (real,editable), 1 deleted (virtual,read-only) === accept after user edit -> FILE_SAVED content is the LIVE buffer (edits captured) === function greet(name) -- be polite print('Hello there, ' .. name) return true end contains user's edited text? true virtual deleted lines are NOT in accepted content? true- added a commit that references this issue
on Jun 23, 2026 - added a commit that references this issue
on Jun 24, 2026
Is it possible to use an inline diff version?