feat: streaming 'typing' reveal for the unified inline diff#301
Open
spoloxs wants to merge 6 commits into
Open
feat: streaming 'typing' reveal for the unified inline diff#301spoloxs wants to merge 6 commits into
spoloxs wants to merge 6 commits into
Conversation
Introduce a VS Code-style unified inline diff: a single persistent, read-only buffer (ClaudeCodeBuffer) showing deleted lines in red/ strikethrough and added lines in green, interleaved in one pane. - Pure-Lua interleave via vim.text.diff/vim.diff (zero external deps, coder#169) - Single reused buffer (bufhidden=hide) kept open showing the reviewed file after accept/reject, so it survives window/tab closes - Tears down any prior unified diff in the same buffer before rendering, preventing stale diffs from accumulating (fixes coder#205) - Wires ClaudeCodeDiffOpened/ClaudeCodeClosed User autocmds for the unified layout (noted in coder#294) - Tracks originating client_id so close_diffs_for_client can tear the diff down on disconnect (parity with the native path, coder#261) - Dispatch in diff.lua branches on layout == 'unified' Implements the unified inline diff layout described in coder#294.
There was a problem hiding this comment.
Pull request overview
Adds an opt-in streamed “typing” reveal for the unified diff renderer, while also refactoring unified diff UI to reuse a persistent buffer/window and improving diff error formatting/cleanup behavior.
Changes:
- Introduces a cancellable, incremental streamed renderer for unified diffs (
stream_reveal*options) and shares highlight logic between static/streamed paths. - Reuses a single persistent
ClaudeCodeBufferacross unified diffs and changes cleanup to leave the buffer/tab open showing the reviewed file. - Updates diff tab/window handling and improves error formatting/logging during diff setup failures.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 6 comments.
| File | Description |
|---|---|
| lua/claudecode/diff.lua | Adds editor-window reuse in a “diff tab”, tweaks new-tab terminal display behavior, improves setup error formatting/logging, and adjusts accept/deny cleanup calls. |
| lua/claudecode/diff_inline.lua | Implements the streamed reveal renderer + cancellation, introduces a persistent unified diff buffer, reworks setup/cleanup flow, and reuses diff windows where possible. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
1629
to
+1631
| if not setup_success then | ||
| local error_msg | ||
| if type(setup_error) == "table" and setup_error.message then | ||
| -- Handle structured error objects | ||
| error_msg = "Failed to setup diff operation: " .. setup_error.message | ||
| if setup_error.data then | ||
| error_msg = error_msg .. " (" .. setup_error.data .. ")" | ||
| end | ||
| error_msg = "Failed to setup diff operation: " .. format_error(setup_error) | ||
|
|
Comment on lines
+528
to
+532
| local function reuse_or_open_diff_window() | ||
| local diff_win = diff.get_or_create_editor_win_in_diff_tab() | ||
| if diff_win then | ||
| vim.api.nvim_set_current_win(diff_win) | ||
| end |
Comment on lines
+795
to
+817
| local content_lines = {} | ||
| local f = io.open(diff_data.old_file_path, "r") | ||
| if f then | ||
| local text = f:read("*a") or "" | ||
| f:close() | ||
| content_lines = vim.split(text, "\n", { plain = true }) | ||
| if #content_lines > 0 and content_lines[#content_lines] == "" then | ||
| table.remove(content_lines, #content_lines) | ||
| end | ||
| end | ||
| vim.api.nvim_buf_set_lines(b, 0, -1, false, content_lines) | ||
|
|
||
| -- Keep it as a persistent review buffer (no auto-wipe) showing the file. | ||
| vim.api.nvim_buf_set_option(b, "buftype", "nofile") | ||
| vim.api.nvim_buf_set_option(b, "bufhidden", "hide") | ||
| vim.api.nvim_buf_set_option(b, "modifiable", false) | ||
| vim.b[b].claudecode_inline_diff = nil | ||
| vim.b[b].claudecode_diff_tab_name = nil | ||
|
|
||
| local ft = diff._detect_filetype(diff_data.old_file_path) | ||
| if ft and ft ~= "" then | ||
| vim.api.nvim_set_option_value("filetype", ft, { buf = b }) | ||
| end |
Comment on lines
+399
to
+403
| local function get_or_create_diff_tab() | ||
| local tabs = vim.api.nvim_list_tabpages() | ||
| if #tabs >= 2 then | ||
| return tabs[2] | ||
| end |
Comment on lines
+1424
to
+1428
| if original_tab_number and vim.api.nvim_tabpage_is_valid(original_tab_number) then | ||
| vim.schedule(function() | ||
| vim.api.nvim_set_current_tabpage(original_tab_number) | ||
| end) | ||
| end |
Comment on lines
+202
to
+216
| --- Progressively reveal `lines` in `buf` with a natural "typing" cadence. | ||
| --- | ||
| --- The reveal is *incremental*: each tick advances by `chars_per_tick` characters, | ||
| --- appends only the new tail to the buffer (never rewriting earlier lines), and | ||
| --- adds extmarks only for lines that just became fully revealed. This keeps the | ||
| --- work O(n) in the number of lines instead of O(n²), and because earlier lines | ||
| --- and their marks are never touched, they stay correctly positioned. | ||
| ---@param buf number Buffer handle | ||
| ---@param lines string[] Full diff lines (source of truth) | ||
| ---@param line_types string[] Parallel type array | ||
| ---@param opts table|nil { delay_ms:number, chars_per_tick:number } | ||
| local function render_diff_buffer_streamed(buf, lines, line_types, opts) | ||
| opts = opts or {} | ||
| local chars_per_tick = math.max(1, opts.chars_per_tick or 1) | ||
| local delay_floor_ms = math.max(0, opts.delay_ms or 0) |
- Hoist format_error to module scope so the open_diff_blocking error path can use it (it was defined inside _setup_blocking_diff and raised a secondary error, obscuring the original failure). - Declare error_msg as local (was implicitly a global). - Stop reusing tabs[2] in get_or_create_diff_tab; always :tabnew so cleanup never closes an unrelated user tab (issue flagged by reviewer). - Rename the internal export get_or_create_editor_win_in_diff_tab -> _get_or_create_editor_win_in_diff_tab for API consistency. - Guard reuse_or_open_diff_window to only reuse a window in the current tabpage, avoiding hijacking/switching to an unrelated tab.
05f31fb to
e79a0b9
Compare
…ffer in current tab - The unified diff no longer auto-opens a window or creates tab 2. - prepares silently. - Added and for user-initiated buffer access (works in any tab, creates a vsplit if needed). - Cleanup closes any window showing and keeps the buffer alive showing the reviewed file. - Removed , , tab2 logic from this path; those remain for native diff. This eliminates the tab-3 confusion: the diff always lives in and the user decides where to view it.
e79a0b9 to
d08e7d0
Compare
Addresses review feedback and several lifecycle bugs in the unified inline diff (ClaudeCodeBuffer): - Never revert the buffer to the old file on resolve. cleanup now loads new_file_contents (the proposed change) so the buffer shows the updated file after both accept and reject, instead of stale old content (on accept Claude may not have written the file yet). Resolves the Copilot comment about reloading old_file_path for new files. - Do not close the buffer's window on resolve; the persistent buffer stays on screen in place (bufhidden="hide"). - Drop the WinClosed reject autocmd: with bufhidden="hide" closing/toggling the window must only hide it, never send DIFF_REJECTED. Rejection is explicit (ClaudeCodeDiffDeny) or via real buffer deletion (:bd/:bw). - On reject, resolve every queued diff as DIFF_REJECTED so their blocking openDiff MCP calls don't hang forever. - Reset current_diff_tab on setup failure so a failed setup can't wedge the queue behind a dead diff; set the pending marker only after a successful render, and guard rendering so failures run cleanup. - Cancel any in-flight streaming animation on resolve/cleanup so it can't clobber the reloaded file view. - Auto-scroll the diff window to the first changed line (centered) whenever the buffer is shown. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ntent - Restore auto-opening the ClaudeCodeBuffer in a split in the current tab at setup (reusing an open window if present), with a scratch fallback window for terminal-only tabs. This re-satisfies the maintainer's unified-layout lifecycle tests (ClaudeCodeDiffOpened carries a valid diff_window/ target_window; terminal-only fallback_window; setup-failure cleanup), which the earlier "simplify" refactor had broken by removing window opening. - On resolve, keep the diff window open (persistent buffer) but close the throwaway scratch fallback window. - cleanup now shows plain content: proposed on accept, ORIGINAL on reject/ cancel (previously it left the proposed change on screen after a reject). Original text is captured at setup as old_file_contents. - Jump to the first changed line in the diff window. Tests: - Update cleanup_inline_diff specs to the persistent-buffer behavior (window stays open; accept shows proposed, reject shows original). - Add nvim_buf_clear_namespace / nvim_buf_del_extmark to the vim mock so the cleanup and streaming reanchor paths are exercised under busted. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A Claude-side accept writes the file and then closes the still-pending diff; close_diff_by_tab_name treats a pending close as a reject, so status becomes "rejected" and the buffer showed the original — i.e. the accepted change did not appear. cleanup_inline_diff now reconciles from disk for the non-"saved" path: it shows the original immediately, then (after Claude's async write lands) reloads the file from disk. Disk is the source of truth — new content if the change was applied, original if it was genuinely rejected (file untouched). Explicit `:w` accepts (status "saved") still trust new_file_contents with no disk read, so they never show stale content. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds an opt-in animated "typing" reveal for the unified inline diff introduced in #300.
reveal_genstops animation on new diff or cleanup.<leader>dt/<leader>dcand stays open after accept/reject.Enable
Related
Stacks on #300.