Repository navigation
feat(diff): add layout = "inline" for VS Code-style unified diff - #195
Conversation
|
Bumping ➕ , this is a feature I'd love as well. |
|
+1 need this! |
|
+1 |
Add a new inline diff layout that shows deleted (red/strikethrough) and
added (green) lines interleaved in a single read-only buffer, similar to
VS Code's inline diff view. This complements the existing "vertical" and
"horizontal" two-panel diff layouts.
Key changes:
- New `diff_inline.lua` module with pure diff computation, rendering via
extmarks, and resolution/cleanup functions
- Dispatch logic in `diff.lua` routes to inline module when configured
- Config/types updated to accept `layout = "inline"`
- `close_all_diff_tabs` detects inline diff buffers
- Customizable highlight groups: ClaudeCodeInlineDiff{Add,Delete,*Sign}
- Requires Neovim >= 0.9.0 (for vim.diff); base plugin remains 0.8.0+
- 23 new tests covering computation, rendering, MCP responses, cleanup
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Thomas Kosiewski <tk@coder.com>
- closeAllDiffTabs: call _resolve_diff_as_rejected before cleanup so pending coroutines are properly resumed instead of hanging on yield - closeAllDiffTabs: include resolved diffs in CLOSED_N_DIFF_TABS count - diff_inline: in open_in_new_tab mode, pick editor window from current tab via nvim_tabpage_list_wins instead of global search which could return a window from the original tab - Add tests for new-tab window selection and cleanup - Add nvim_tabpage_list_wins to vim mock Issues found by Codex 5.3 code review. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Thomas Kosiewski <tk@coder.com>
Two small refinements to the inline diff layout while rebasing onto main: - Resolve the diff function at call time as `vim.text.diff or vim.diff`. Neovim 0.12 renamed `vim.diff` to `vim.text.diff` (old name kept as a deprecated alias), so prefer the new name and have the version guard check the function actually used. Keeps `layout = "inline"` working across the supported range and forward-compatible. - Store `client_id` in the inline diff state so `close_diffs_for_client` tears the diff down if its MCP client disconnects, matching the native path (coder#261). Without it, orphaned inline diffs would linger. Change-Id: I59e3d29729e9b7d7838da25c53c23dc335f784c5 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Thomas Kosiewski <tk@coder.com>
|
Nice feature! However it still opens a new Also let's beware of potential breaking changes in the layout options, as this sounds more like "inline_vertical" (or "vertical_inline") whereas the true overlay+extmarks mode (once we implement that) should deserve "inline". |
|
Thanks @wookayin — you're right on both counts. The shipped Since it hasn't gone out in a tagged release yet, we're renaming it now: I opened #294 to track that real overlay — the prototype direction is proposed lines as real, editable text + deleted lines as |
) ## Summary Renames the `diff_opts.layout = "inline"` option (added in coder#195, queued for v0.4.0 via release-please coder#279) to **`"unified"`**, before it ships in a tagged release. Tracked in coder#293. [@wookayin pointed out](coder#195 (comment)) that the shipped `"inline"` mode isn't a true "inline" diff in the VS Code sense — it renders a unified diff in a separate `rightbelow vsplit` pane, not an in-place virtual-text overlay in the same window. Naming it `"inline"` now would force a breaking rename later, once we add the real in-place overlay (the mode that most deserves the name `"inline"`). Renaming is free now (not yet released) and a breaking change after coder#279 merges. ## What changed - **Config value** `"inline"` → `"unified"`: validation + error message (`config.lua`), the `ClaudeCodeDiffLayout` type alias (`types.lua`), all four dispatch sites (`diff.lua`), and the stored `layout` value (`diff_inline.lua`). - **User-facing buffer name**: `<file> (inline diff)` → `<file> (unified diff)`. - **Docs**: README layout enum + label; a `CHANGELOG.md` `[Unreleased]` entry for `layout = "unified"`. - **Tests**: the accept test now expects `"unified"`; a new test asserts the former `"inline"` is rejected. ## Decisions - **No deprecated alias.** `"inline"` was never in a tagged release, so there's no backward-compat obligation, and rejecting it cleanly reserves the name for the future in-place overlay (coder#294). The validation error names `'unified'`, so anyone tracking `main` is pointed at the new value. - **Internal names kept.** The `diff_inline.lua` module, its functions, the `claudecode_inline_diff` buffer variable, and the `ClaudeCodeInlineDiff*` highlight groups are unchanged — they describe the inline-*style* rendering, not the layout value (coder#293 noted the buffer var can stay). This keeps the diff small and low-risk. - **coder#279 changelog** regenerates from commits via Communique on the next push to `main`, so the v0.4.0 entry reflects "unified" automatically; I did not hand-edit the bot branch. ## Validation - `mise run all`: treefmt clean, luacheck **0/0**, **703/703** tests pass (baseline 702 + the new rejection test). - Headless Neovim smoke test: `layout = "unified"` accepted, `"inline"` rejected (error names `'unified'`), `"vertical"` still works. The true in-place overlay (the future `"inline"`) is tracked in coder#294. Refs coder#293, coder#195, coder#82 🤖 Generated with [Claude Code](https://claude.com/claude-code) Signed-off-by: Thomas Kosiewski <tk@coder.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The next claudecode.nvim release. Merging this PR creates the release
tag and GitHub Release, then starts Communique GitHub Release note
generation.
---
## [0.4.0] - 2026-11-09
### Added
- `:ClaudeCodeSendText {text}` command and
`require("claudecode.terminal").send_to_terminal(text, opts)` to send
arbitrary text to the open Claude terminal as if typed at the prompt
(submitting by default; `:ClaudeCodeSendText!` inserts without
submitting). Multi-line text is sent via bracketed paste. Works with the
in-editor `native`/`snacks` providers only.
([coder#197](coder#197),
[coder#272](coder#272))
- `:checkhealth claudecode` health check that verifies Neovim version,
`setup()`, the Claude CLI, terminal provider, WebSocket server, lock
file, and live connection state.
([coder#275](coder#275))
- `:ClaudeCodeCloseAllDiffs` command to close pending Claude diffs at
once. A diff accepted with `:w` closes once the file holds the accepted
text; otherwise the accepted text is put into the buffer as unsaved
changes for `:w`/`:e!` so edits are never discarded.
([coder#248](coder#248),
[coder#261](coder#261))
- `diff_opts.layout = "unified"`: a VS Code-style unified diff rendered
in a single buffer with interleaved deleted/added lines, as a compact
alternative to `"vertical"`/`"horizontal"`. Documented to require Neovim
>= 0.9.0. Customizable via the
`ClaudeCodeInlineDiffAdd`/`Delete`/`AddSign`/`DeleteSign` highlight
groups. ([coder#195](coder#195),
[coder#295](coder#295),
[coder#297](coder#297))
- `User ClaudeCodeSendComplete` autocmd, fired once per file when a send
is accepted while Claude is connected, with `data = { file_path,
start_line, end_line, context }` (lines 0-indexed). Enables post-send
logic such as focusing a Claude session running outside Neovim.
([coder#228](coder#228),
[coder#265](coder#265))
- `User ClaudeCodeDiffOpened` / `ClaudeCodeDiffClosed` autocmds carrying
diff metadata, plus `diff_opts.auto_resize_terminal` (default `true`) to
opt out of plugin-managed terminal width during diffs and
`terminal.diff_split_width_percentage` to set a distinct terminal width
while a diff is open.
([coder#270](coder#270))
- `terminal.auto_insert` option (default `true`): set to `false` to stay
in Normal mode when refocusing the Claude terminal and preserve your
scroll position instead of jumping to the prompt.
([coder#233](coder#233))
- `ClaudeCodeTreeAdd` / `<leader>as` support in snacks.nvim pickers
(`snacks_picker_list`), covering `Snacks.explorer()` and modal
`Snacks.picker.files()`/`grep()`.
([coder#192](coder#192),
[coder#269](coder#269))
- netrw file selection for `<leader>as` / `:ClaudeCodeTreeAdd`,
supporting marked files and the file under the cursor.
([coder#62](coder#62))
### Changed
- Model picker labels are now version-free evergreen aliases (`Claude
Opus (Latest)`, `Claude Sonnet (Latest)`, `Claude Haiku (Latest)`), with
new 1M-context variants (`opus[1m]`, `sonnet[1m]`) and a `Default
(account recommended)` entry.
([coder#146](coder#146),
[coder#256](coder#256))
- `:checkhealth claudecode` and the MCP `serverInfo.version` now report
the real plugin version from the `VERSION` file instead of a hard-coded
value. ([coder#437](coder#437))
### Fixed
- The Snacks provider no longer fails to launch Claude when a model
alias contains shell-glob characters (e.g. `opus[1m]`); the command is
now passed as an argv list with no shell interpretation.
([coder#256](coder#256))
- The native terminal now adopts only terminal buffers it started
(marked with `b:claudecode_native_terminal`), instead of any visible
terminal whose name contains `claude`. Upgrade note: a terminal that an
older plugin version started (for example after `:Lazy reload` following
an upgrade), or that a session file restored, carries no mark and is no
longer adopted. The next open starts a second Claude session, and the
old terminal keeps running until you close its buffer or quit Claude in
it. ([coder#349](coder#349),
[coder#352](coder#352),
[coder#364](coder#364))
- `closeAllDiffTabs` is now scoped to claudecode's own tracked diffs and
no longer wipes out an open diffview.nvim, fugitive, or native
`:diffsplit` review; `openFile`/`openDiff` also never reuse a window
that is in diff mode.
([coder#277](coder#277),
[coder#290](coder#290))
- `close_tab` now closes only the diff with that tab name and never
deletes a user buffer.
([coder#373](coder#373))
- Closing the native terminal, for example with `:ClaudeCodeClose`, now
stops the Claude job instead of leaving it running.
([coder#336](coder#336))
- The Claude terminal now adds loopback hosts to `no_proxy`/`NO_PROXY`
so a configured proxy no longer tunnels Claude's `ws://127.0.0.1` IDE
connection and times out queued @ mentions.
([coder#70](coder#70),
[coder#268](coder#268))
- `focus_after_send = true` with `provider = "none"`/`"external"` now
emits a one-time warning pointing to the `User ClaudeCodeSendComplete`
autocmd instead of failing silently.
([coder#228](coder#228),
[coder#265](coder#265))
- Rejecting a Claude diff with `:q` / `:close` / `<C-w>c` (or closing
the tab) now resolves it as rejected via a `WinClosed` autocmd.
([coder#266](coder#266))
- Quickly-made and single-line visual selections are now pushed to
Claude reliably; selections are flushed synchronously on visual-mode
exit and persist until the cursor moves, including in the external
`/ide` flow and Select mode.
([coder#246](coder#246),
[coder#267](coder#267),
[coder#367](coder#367),
[coder#381](coder#381),
[coder#388](coder#388))
- Pending diffs are now automatically closed when the client that opened
them disconnects or the integration stops, so orphaned proposals no
longer linger.
([coder#248](coder#248),
[coder#261](coder#261))
- `openDiff` now works when the Claude terminal is the only window,
creating a split to host the diff instead of failing with "No suitable
editor window found".
([coder#260](coder#260))
- Fixed the "climbing cursor" in the Snacks terminal, where the cursor
sat one row above Claude's prompt after hide and show. On Neovim >= 0.10
a floating terminal is now hidden instead of closed. A split is still
closed on hide and created again on show, which keeps the cursor in
place. Floating terminals on Neovim < 0.10 keep the old behavior.
([coder#240](coder#240),
[coder#183](coder#183),
[coder#271](coder#271))
- Worked around a Neovim core bug (< 0.12.2) that fragmented large
bracketed pastes into the terminal; controlled by
`terminal.fix_streamed_paste` (`"auto"` by default).
([coder#252](coder#252))
- `openFile` now selects exactly the requested line range or matched
text, keeps the user's Visual mode, unsaved changes, focus and preview
window intact, never prompts W11/W12, and never picks the preview window
as the main editor window.
([coder#353](coder#353),
[coder#365](coder#365),
[coder#370](coder#370),
[coder#375](coder#375),
[coder#382](coder#382),
[coder#384](coder#384),
[coder#387](coder#387),
[coder#396](coder#396),
[coder#408](coder#408))
- `getDiagnostics` now handles plain file paths without a `file://` URI
scheme, and IDE diagnostics return grouped URI-based payloads with
editor-native ranges and severity names.
([coder#163](coder#163),
[coder#274](coder#274))
- `:ClaudeCodeAdd` and `openFile` now handle `$` in file paths.
([coder#286](coder#286))
- Prevented a segfault when accepting a new-file diff with
`render-markdown.nvim` installed, by turning off diff mode before the
post-write redraw.
([coder#224](coder#224))
- The auth token is now generated from a CSPRNG and the lock file is
written atomically with mode `0600` in a `0700` directory.
([coder#259](coder#259))
- Detect system sleep/wake and reset client keepalive timestamps to
prevent false disconnections.
([coder#141](coder#141))
- The WebSocket server now retries port binding on `EADDRINUSE`
(parallel instances), closes connections on malformed/oversized or
binary frames with the correct status, joins fragmented messages,
validates UTF-8 per message, and never repeats a client id within a
process. ([coder#258](coder#258),
[coder#283](coder#283),
[coder#378](coder#378),
[coder#383](coder#383),
[coder#390](coder#390),
[coder#395](coder#395),
[coder#405](coder#405),
[coder#422](coder#422),
[coder#431](coder#431))
- Legacy `diff_opts` (e.g. `vertical_split`, `open_in_current_tab`) now
take effect again after the config merge-order regression.
([coder#142](coder#142))
- `:q` on a diff window no longer hangs; floating diff terminals keep
focus; empty buffers are wiped when the terminal provider is `none`; and
accepted diffs are handed over in the buffer's own line-ending format.
([coder#178](coder#178),
[coder#223](coder#223),
[coder#266](coder#266),
[coder#392](coder#392))
### Compatibility
- Integration tests now run on Neovim v0.8.0, and a new-tab diff no
longer fails there because Neovim 0.8 has no `statuscolumn`. The unified
layout is documented to require Neovim >= 0.9.0, and the v0.8.0
integration tests skip it.
([coder#369](coder#369),
[coder#394](coder#394))
---
Maintained by the release-please workflow. CHANGELOG.md and the release
PR body are regenerated by Communique on every push to main, so manual
edits to this PR are overwritten.
---------
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Thomas Kosiewski <tk@coder.com>
Summary
Add a new
layout = "inline"option fordiff_optsthat renders a VS Code-style unified inline diff - a single read-only buffer with deleted (red/strikethrough) and added (green) lines interleaved. This complements the existing"vertical"and"horizontal"two-panel diff layouts.Closes #82
Motivation
The existing diff layouts use Neovim's built-in
diffthisto show old and new content side by side. While effective, this uses significant screen real estate with two panels. An inline unified diff provides a more compact view that's familiar to VSCode users, showing changes in context within a single buffer.
Changes
lua/claudecode/diff_inline.lua— self-contained inline diff implementation with:vim.diff()(indices mode)+/-)diff.lua— routes to inline module whenlayout = "inline"is configured; exposes shared utilities asM._members following existing patterns"inline"as a valid layout valueclose_all_diff_tabs— detects inline diff buffers viaclaudecode_inline_diffbuffer variablevim.diffmock with LCS-based hunk computation totests/mocks/vim.luaConfiguration
Highlight groups are customizable:
Requirements
Design Decisions
Test Plan