Skip to content

Explicit repository, diff titles, window bars, single panes and downloads - #4

Merged
jrosiek merged 17 commits into
mainfrom
explicit-repo-dir
Oct 9, 2026
Merged

jrosiek merged 17 commits into
mainfrom
explicit-repo-dir

Conversation

@jrosiek

@jrosiek jrosiek commented Oct 9, 2026 •

Copy link
Copy Markdown
Owner

A diff now runs in an explicit repository, is titled after what it shows, and has bars that say which file is shown and how it changed. Added and deleted files take a single pane, changed text stands out more, and a file difft fails on no longer fails the whole diff. The plugin downloads a library matching its version and a difft with word-level JSON output.

Explicit repository directory

  • open(revset, opts) takes opts.cwd, a directory inside the repository to diff, so a caller (for example another process talking to Neovim over its socket) can name the repository instead of depending on the current window. Without it, the current window's working directory is used.
  • :Difft takes the directory when it is run, before the view opens, so :lcd / :tcd of the window it was run from are honoured even when the view opens after startup settles.
  • Every git, jj and difft command runs in that repository's root. Previously they inherited the process's current directory, which follows the current window: a diff could pick up the directory of the new diff tab, or of a window switched to while it was still loading.
  • jj diffs from a subdirectory read the right files. jj diff now runs from the repository root, so the paths it reports match the paths the file contents are read from.
  • The diff tab works in the diffed directory (:tcd), leaving the tab it was opened from alone.
  • With multiple_diffs, the same revset in a different directory opens its own tab instead of switching to the other repository's.
  • The library's diff functions take the directory as an optional last argument (cwd in the asynchronous spec); without it, the process's directory at the time of the call is used.

Diff title

  • The side panel's header starts with a title and a subtitle, both derived by the library from the diff:
    • a commit diff: the title of its oldest commit, and +N more for the commits after it;
    • a staged or unstaged git diff, or the jj working copy: the kind of diff, and when its files were read (as of 9 Oct 16:30:04).
  • The title wraps to the panel width; when a resize changes its height, the tree is rebuilt below it, keeping collapsed directories and the cursor row.
  • Highlight groups DifftDiffTitle (links to Title) and DifftDiffSubtitle (the text colour blended into the background).

Bars on top of the panel and the panes

  • The side panel's bar shows the shown file's status (● modified, + added, - deleted, ➜ renamed), right-aligned next to the panes.
  • Each diff pane's bar shows the file on its side: the file-type icon, the file name, its directory dimmed, and that side's line count on the right (removed lines in the base pane, added lines in the head pane). A renamed file's base pane shows its old path.
  • Narrow panes give up the directory first (cut from its left), then the count, then cut the file name; every cut uses …, and the bars never get wider than their window.
  • The bars share the panel's background and are underlined; the unfocused windows' bars are dimmed. Highlight groups DifftBar, DifftBarNC and DifftBarMuted.
  • The view's buffers are named difftastic://<tab>/panel, difftastic://<tab>/base/<path> and difftastic://<tab>/head/<path>, so status lines show what a window holds instead of [Scratch].

One pane for added and deleted files

  • A file that exists on one side only is shown in one pane that takes the width of both. The other pane comes back, on its side and in the panes' last ratio (also one the user dragged), for the next file with both sides.
  • Focus moves to the remaining pane when the focused one closes; <Tab> from the tree and remembered positions go to the pane that is shown.

Changed text

  • Changed text within a line has a stronger background and an underline in the added or removed colour half blended into the background (in the text's own colour where terminals draw no underline colours); the text keeps its syntax colour.
  • Secondary text (the subtitle, the directory in the bars) is the text colour blended into the background (DifftDiffSubtitle, DifftBarMuted), readable with themes that give NonText the background colour.

Header and loading window

  • The header box is replaced by text with thin rules, drawn as underlined blank rows like the bars' underlines (DifftTreeRule).
  • A long range in the header (e.g. tags/v5.8.2-stable~227 → tags/v5.8.2-stable) continues after its arrow on a second row; a side still too long for its row is cut at its start, keeping its end.
  • The loading window loses its border the same way: a bar on top with the share done (DifftLoadingDone), or a spinner while the number of files is unknown, then the range, the last message and a rule.
  • DifftTreeTitle and DifftTreeDivider are removed, as nothing uses them any more.

Side panel width

  • A side panel narrowed by Neovim, for example in a window that opened small, grows back to its width when Neovim grows. A width the user gives the panel is kept through resizes.

Robustness

  • A file difft fails on (e.g. crashes on its syntax) no longer fails the whole diff: for git it is compared again in difft's text mode, alone; for jj, which runs difft once over all files, all files are compared again as text. jj only warns when its diff tool fails and still exits successfully; that warning now counts as a failure. Such a file shows ≡ line by line in the side panel's bar.
  • The panes' split gives an odd extra column to the base pane everywhere, so evening panes that are already even no longer moves the split.

Downloads

  • The prebuilt library comes from the GitHub repository the plugin was cloned from (its origin), so a fork gets its own releases, else from clabby/difftastic.nvim: the newest release at or before the installed plugin commit, or the latest release when the plugin is not a git clone. A library from another repository, or one that no longer matches the installed plugin, is downloaded again at startup; :DifftUpdate downloads the matching release again instead of the latest.
  • difft is downloaded with download = true from the latest release of jrosiek/difftastic, whose JSON output lists only the words that changed in comments, strings and text (JSON: list only changed words of changed comments, strings and text lines Wilfred/difftastic#1075), into the library's directory. The library runs the downloaded difft, for jj too, and difft from PATH until it is there or when it cannot be downloaded.
  • Without any difft, a diff stops with a message saying how to get one; a diff opened while a download it needs is still running waits for it in the loading window.
  • :checkhealth difftastic-nvim shows the library and the difft in use, where they come from and their versions, and whether nui.nvim is installed.

Testing

  • End-to-end tests against scratch git and jj repositories: the given repository is diffed rather than the current one, an asynchronous diff keeps its directory when the current one changes while it runs, a directory outside a repository is reported, a jj diff from a subdirectory reads both sides, and the library's titles for single commits, ranges (.. and ...), staged, unstaged and jj diffs.
  • View tests for the title and subtitle (wrapping, rebuilding the tree on resize), the three bars (statuses, renames, counts, the narrowing order down to one column, colours), one pane for added and deleted files (also straight from an added to a deleted file), the ruled header and the loading window (spinner, percentage, re-centring).
  • Tests with a child Neovim and a real screen for the pane ratio through a one-pane file and the side panel width after resizes.
  • Download tests with GitHub stubbed (the matching release, a library from another repository, difft unpacked, made executable and recorded), the library running a given difft with none on PATH (git and jj), waiting for a download before a diff, and the health report. Tests use a data directory of their own.
  • Tests for the buffer names, the text fallback (a stand-in difft that fails unless asked for text mode, for git and jj), the colours, the range rows, and the split after a real mouse drag of the side panel's border.
  • The Rust tests, clippy, rustfmt and the Lua suite pass.

🤖 Generated with Claude Code

The repository was found from the process's current directory whenever
a git or jj command started. That directory follows the current window
(`:lcd`, `:tcd`), so a diff could use the directory of the new diff tab,
or of a window switched to while it loaded; jj diffs run from a
subdirectory also read file contents from the wrong paths.

`open(revset, opts)` now takes the directory to diff as `opts.cwd`,
falling back to the current window's working directory (`:Difft` takes
it when run, before the view opens). Every git, jj and difft command
runs in that directory's repository root, the diff tab works in that
directory (`:tcd`), and with `multiple_diffs` the same revset in another
directory opens its own tab. The library's diff functions take the
directory as an optional last argument (`cwd` in the asynchronous spec).
The header box of the side panel now starts with a title and a less
prominent subtitle, both derived from the diff:
- a commit diff: the title of its oldest commit, and "+N more" for the
  commits after it;
- a staged or unstaged git diff, or the jj working copy: the kind of diff,
  and when its files were read ("as of 9 Oct 16:30:04").
The library returns them as title, subtitle and snapshot_time. The title
wraps to the panel width, and when a resize changes its height the tree is
rebuilt below the box, keeping collapsed directories and the cursor row.
The box no longer shows the plugin name. The new highlight groups
DifftDiffTitle and DifftDiffSubtitle color the two parts.
A file that exists on one side only, added or deleted, is shown in one
pane that takes the width of both. The other pane's window is closed and
opened again, on its side, for the next file with both sides. The panes
come back in the ratio they had, also one the user dragged, and resizes
while one pane is shown leave that ratio alone. Focus moves to the
remaining pane when the focused one closes, and <Tab> from the tree and
remembered positions go to the pane that is shown.
The side panel gets a bar with the shown file's status (modified, added,
deleted, renamed), right-aligned next to the panes. Each diff pane gets a
bar with the file on its side: the file-type icon, the file name, its
directory dimmed, and that side's line count (removed lines in the base
pane, added lines in the head pane). A narrow pane gives up the directory
first, then the count, then cuts the name; every cut uses an ellipsis.
The bars share the panel's background and are underlined. The new
highlight groups DifftBar, DifftBarNC and DifftBarMuted color them.
The side panel's header and the loading window drop their rounded boxes
for the look of the bars: text with a one-cell margin and thin rules,
drawn as underlined blank rows (highlight group DifftTreeRule). The
loading window gets a bar on top with the share done, or a spinner while
the number of files is unknown (DifftLoadingDone), above the range and
the last message. DifftTreeTitle and DifftTreeDivider are removed, as
nothing uses them any more.
A side panel narrowed by Neovim, for example in a window that opened
small, grows back to its width when Neovim grows. A width the user gives
the panel, narrower or wider, is kept through resizes; a double click on
its border still resets it to the configured width.
@jrosiek jrosiek changed the title Diff an explicit repository directory Explicit repository, diff titles, window bars and single panes Oct 9, 2026
The prebuilt library is downloaded from the GitHub repository the plugin
was cloned from (its origin), so a fork gets its own releases, else from
clabby/difftastic.nvim: the newest release at or before the installed
plugin commit, or the latest release when the plugin is not a git clone.
The stored version names the repository and the tag. A library from
another repository, or one that no longer matches the installed plugin
(e.g. after a plugin update), is downloaded again at startup.
:DifftUpdate downloads the matching release again instead of the latest.
With download = true, difft is also downloaded from the latest release
of jrosiek/difftastic, whose JSON output lists only the words that
changed in comments, strings and text, into the library's directory.
The library runs the downloaded difft, for jj too, and difft from PATH
until it is there or when it cannot be downloaded. Without any difft, a
diff stops with a message saying how to get one. A diff opened while a
download it needs is still running waits for it in the loading window.
:checkhealth difftastic-nvim shows the library and the difft in use,
where they come from and their versions. Tests use a data directory of
their own.
@jrosiek jrosiek changed the title Explicit repository, diff titles, window bars and single panes Explicit repository, diff titles, window bars, single panes and downloads Oct 9, 2026
The side panel's buffer is named difftastic://<tab>/panel and the panes'
difftastic://<tab>/base/<path> and difftastic://<tab>/head/<path>,
following the shown file (the base pane names a renamed file's old
path), so status lines show what a window holds instead of [Scratch].
The tab keeps two diff tabs showing the same file apart; renaming leaves
no buffers behind, and the buffers stay unsaved scratch buffers.
A file difft fails on (e.g. crashes on its syntax) no longer fails the
whole diff: for git it is compared again in difft's text mode, alone;
jj runs difft once over all files, so there all files are compared
again as text. jj only warns when its diff tool fails and still exits
successfully; that warning now counts as a failure. A file compared as
text shows "≡ line by line" on the left of the side panel's bar.
The subtitle of the diff's title and the secondary text in the bars
(DifftDiffSubtitle, DifftBarMuted) are the text colour blended into the
background instead of links to NonText, which some themes give the
background colour itself, leaving that text invisible.
The background of changed text within a line is a fifth stronger, and
the text is underlined in the added or removed colour half blended into
the background (in the text's own colour where terminals draw no
underline colours). The text keeps its syntax colour.
With an odd number of columns to share, the panes' split gave the extra
column to the base pane after resizes and resets, but to the head pane
on opening the view and on a double click on the split. Evening panes
that were already even moved the split by a column. Every split now
gives it to the base pane.
A range too long for its row in the side panel's header continues after
its arrow on a second row, each side right-aligned; a side still too
long for its row is cut at its start, keeping its end (e.g. ~227).
Before, the range was cut at its end, often losing the head side.
The README's fork summary lists the diff's title and bars, single panes
for added and deleted files, the downloads of a matching library and of
a difft that marks only changed words in comments and text, and the
line-by-line fallback for files difft fails on. The highlight tables
describe DifftAdded and DifftRemoved as changed text within a line and
DifftTreeRange as derived from Changed.
Neovim reports a window without a border as nothing, "none" or empty
border parts, depending on its version; the loading window test now
accepts each.
@jrosiek
jrosiek merged commit 1a55918 into main Oct 9, 2026
4 checks passed
@jrosiek
jrosiek deleted the explicit-repo-dir branch October 9, 2026 22:20
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.

1 participant