Skip to content

JSON: list only changed words of changed comments, strings and text lines - #1075

Open
jrosiek wants to merge 1 commit into
Wilfred:masterfrom
jrosiek:json-omit-unchanged-words
Open

jrosiek wants to merge 1 commit into
Wilfred:masterfrom
jrosiek:json-omit-unchanged-words

Conversation

@jrosiek

@jrosiek jrosiek commented Oct 9, 2026

Copy link
Copy Markdown

In the JSON output, a changed line of code lists only the tokens that changed, but a changed comment, string or text line lists every word on it, the unchanged ones included. difft already knows which words really changed (NovelWord vs UnchangedPartOfNovelItem) and uses that in its own display, emphasising only the changed words; the JSON drops the distinction, because every novel match kind is serialised the same way.

This leaves those unchanged words out of changes, as unchanged code tokens already are. JSON consumers can then show intraline changes for comments, strings and text the same way difft displays them, and the meaning of changes is the same for code and text: what changed on the line.

It covers the line-oriented fallback too (unsupported languages, DFT_BYTE_LIMIT, DFT_GRAPH_LIMIT, DFT_PARSE_ERROR_LIMIT), since that path produces the same match kinds.

Example, a changed Rust comment:

// Compute the total price of the order including tax.
// Compute the total cost of the order excluding tax.

changes contents before (each side lists all 21 tokens of the line):

lhs: "/", "/", " ", "Compute", " ", "the", " ", "total", " ", "price", " ", "of", ... "including", " ", "tax", "."
rhs: "/", "/", " ", "Compute", " ", "the", " ", "total", " ", "cost", " ", "of", ... "excluding", " ", "tax", "."

after:

lhs: "price", "including"
rhs: "cost", "excluding"

A line inside a changed comment block that is the same on both sides now has an empty changes list, like an unchanged code line within a hunk.

Addresses #658.

🤖 Generated with Claude Code

Words that are the same on both sides of a changed comment, string or
text line were listed as changes. Leave them out, as unchanged code
tokens are.
jrosiek added a commit to jrosiek/difftastic.nvim that referenced this pull request Oct 9, 2026
…oads (#4)

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](https://github.com/jrosiek/difftastic), whose
JSON output lists only the words that changed in comments, strings and
text (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](https://claude.com/claude-code)

This branch has not been deployed

No deployments
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