Skip to content

feat: streaming 'typing' reveal for the unified inline diff#301

Open
spoloxs wants to merge 3 commits into
coder:mainfrom
spoloxs:feat/inline-diff-streaming
Open

feat: streaming 'typing' reveal for the unified inline diff#301
spoloxs wants to merge 3 commits into
coder:mainfrom
spoloxs:feat/inline-diff-streaming

Conversation

@spoloxs

@spoloxs spoloxs commented Jul 6, 2026

Copy link
Copy Markdown

Summary

Adds an opt-in animated "typing" reveal for the unified inline diff introduced in #300.

  • Incremental & O(n): each tick appends only the newly revealed tail; extmarks added only for newly complete lines.
  • Cancellable: reveal_gen stops animation on new diff or cleanup.
  • Same toggle-anytime behavior: buffer opens/closes via <leader>dt/<leader>dc and stays open after accept/reject.

Enable

require('claudecode').setup({
  diff_opts = {
    stream_reveal = true,
    stream_reveal_chars_per_tick = 3,
  }
})

Related

Stacks on #300.

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.
Copilot AI review requested due to automatic review settings July 6, 2026 21:46

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 ClaudeCodeBuffer across 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 thread lua/claudecode/diff.lua
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 thread lua/claudecode/diff_inline.lua Outdated
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 thread lua/claudecode/diff.lua Outdated
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 thread lua/claudecode/diff.lua
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.
@spoloxs spoloxs force-pushed the feat/inline-diff-streaming branch from 05f31fb to e79a0b9 Compare July 6, 2026 22:00
…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.
@spoloxs spoloxs force-pushed the feat/inline-diff-streaming branch from e79a0b9 to d08e7d0 Compare July 6, 2026 22:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants