Diffs
Rendering is two independent signals merged in crates/core/src/render.rs:
- syntect colours every byte of both files. This is what delta does
internally — delta ships no library, and its highlighter is syntect, so
there is nothing to extract that using syntect directly does not give.
The syntax definitions come from
two-face(bat's set); syntect's own bundle has no TOML, Nix or Dockerfile. A theme is a page as well as a set of inks, so the diff is painted on the background the theme names and anything the highlighter leaves uncoloured takes the foreground it names. Using only the inks is what leaves Solarized code floating on a grey the theme never asked for. The red and green of the diff come from the theme too, where it has an opinion: most themes colourmarkup.insertedandmarkup.deleted, and each wash is the page with that colour stirred in — lightly for a changed line, strongly for the span difftastic marked. A theme with no diff colours of its own, which includes the default light one, keeps the window's own pair. - an engine supplies the alignment and the byte ranges that actually
changed. There are two, behind one trait in
crates/core/src/diff: difftastic, which compares syntax trees, and a line engine built onimara-diffthat runs in this process.
difftastic declares only a [[bin]] target, so there is no library to link;
the JSON is the API. Its format is explicitly unstable, which is why the
difft version is pinned in devenv.nix and parsing is confined to
crates/core/src/diff/difft.rs. Both sides go in on pipes rather than through
files: difftastic is called the way git calls it — a path to display, then
each side as a file, an object and a mode — and the language comes from the
path it is shown rather than from where the bytes are. A review writes
nothing to $TMPDIR — anywhere a descriptor can be named to a subprocess,
which is everywhere but Windows. There the two sides go into a temporary
directory that lasts for the one call; the argv is the same either way, so
what difftastic is asked and what it answers do not depend on which of the
two got the bytes to it.
The router in front of them answers four questions. A file difftastic has no
grammar for — it will say which, with --list-languages — goes straight to
the line engine, saving a subprocess. A file it does parse but sees no change
in is a reformat: the tree did not move but the lines did, so the line engine
answers that too, and the language stays the one that was detected. A file it
crashes on — it has assertions of its own, and a tree it did not expect trips
them — is drawn line by line as well, with the crash written to the --log
rather than put where the diff should be: a line diff can be read, and a
panic message cannot be reviewed. And where difftastic is not installed at
all, the line engine answers everything and each diff carries a note saying
so. The window works without it; it is better with it.
The result is a span stream where colour and changed-ness are independent — a smart diff rather than a red/green line diff — plus context collapsing, so you see changes and not whole files.
There are two layouts, switchable in the file header. Side by side sizes
both columns to the longest line in the file, so a row that exists on only
one side still holds the other column open and the two halves stay aligned
however far you scroll sideways. Unified puts one line per row with both
line numbers in the gutter. The pick is automatic until you make one: a file
with nothing to compare (a pure addition or deletion), or a panel too narrow
to give each side a readable column, opens unified. Both buttons carry the
key that does the same thing — w s and w w — the way the decision
buttons do: these are the two settings changed most often, and a reviewer
who came to them with a mouse should not have to find a layer to learn the
key.
A file the window can look at rather than read — .png, .jpg, .gif,
.webp, .bmp, .ico — is shown instead of being diffed. Both sides are
drawn: side by side, blended one over the other on a slider, or what
moved — the picture itself dimmed to a grey, with every pixel the two
sides disagree about marked over it in a colour a grey picture cannot hold,
brighter the further apart they are. The two sides subtracted is the obvious
thing to draw and the wrong thing to look at: a screenshot that gained a
word in its bottom strip subtracts to a black rectangle with a thin dim line
across it, which is neither where the eye goes nor bright enough to see once
the picture has been fitted into half a panel. What it costs is a picture
that was recompressed rather than edited, where every pixel is a shade off
and the whole frame is marked — which is what the caption over it already
says. Above them is what each side measures and what share of the
pixels differ, so "nothing moved" and "everything moved" are told apart before
you look. A picture is scaled to the room it is in, and magnified with its
nearest neighbour rather than smoothed, so a blown-up pixel is the colour it
really is. Over 16 MB a side it is named rather than decoded. Everything else
binary is still one word: binary file.
The window leader walks the three: w p is the same three buttons on one
key, because which of them answers the question is a fact about the pair
rather than about the reviewer. It stands still over a file that is read —
which way is in force outlives the file it was chosen on, and a key that
changed it with no picture on screen would decide how the next one opens
without showing what it was deciding.
A picture has no lines to hang a remark on, so the comment key over one writes about the whole file, and what has already been written about it is drawn above the picture. The check-off key does the same thing for the same reason: a picture is ticked off whole or not at all.
Wrap turns off the horizontal scroll and folds long lines instead. Rows are then no longer a uniform height, and the diff is virtualised, so each row's height has to be known before it is drawn: lines break at a whole column count rather than at a pixel width, which makes the height plain arithmetic on the column widths measured once when the diff arrives. Breaks land anywhere, not at word boundaries — code has no words worth keeping whole, and word breaks would not land where the arithmetic expects.
The row under the pointer lights up across both columns and both line numbers brighten with it — side by side, that is the only thing that says which old line became which new one. The code itself is selectable: drag across it to copy a line out. The gutter is not, so dragging there still picks a range of lines to comment on.
The sidebar toggles sit in the top corners of the window, which is the one strip that never moves: a control that hides a panel has to stay put, or hiding the panel moves the way back.
Every section of both sidebars is a panel with a draggable edge. Which one deserves the room depends on what you are doing: a branch of thirty commits wants the commit list, a commit with a long message wants the message, a wide-reaching change wants the files. The sections claim their space before the one between them is laid out, so a long commit message can never push the file list out of the window.
Opening a file is two VCS subprocesses, sometimes an engine subprocess, and a
syntect pass. make bench measures the last two over text held in memory and
says where the time goes — on this machine, for a two-thousand-line file:
comparing lines in process, 0.12 ms; asking difftastic, 133 ms, nearly all of
it the subprocess; highlighting, 399 ms; the same render with highlighting
capped, 0.76 ms; reading the whole thing back out of the cache, 2 ms. It runs
on a worker thread with a spinner in its place; the window never blocks, and a
result the reviewer has already clicked past is discarded rather than drawn.
The second look costs almost nothing. A rendered diff is kept under a key
taken from the file's own content key, the engine that drew it and the theme
it was drawn in, in $XDG_CACHE_HOME/slop-review/diffs.db — so switching
appearance, coming back to a file, and reopening the same branch tomorrow are
all lookups. Deleting that file costs nothing but time; it is a cache, and it
is deliberately not the notebook under $XDG_STATE_HOME, which is not. A
fourth worker renders the file after the one on screen while you are still
reading, so moving down a listing is a lookup too.
Above highlight_max_lines — four thousand by default — a file is drawn in
the theme's plain ink rather than highlighted, and the header says so.
Highlighting walks every character of both sides, and on a generated lockfile
that is most of what the diff costs, for colour nobody is reading.
Which syntax a file is drawn in is guessed from its name, against bat's set —
which knows most of what a review meets, and not the extension your build
system invented. [languages] is where you say so:
[languages]
do = "bash" # redo's build files
Justfile = "make"
".mjs" = "javascript"
The key is a file's whole name or its extension, with the dot or without; the
key is matched without regard to case, and a whole name is tried before an
extension, so a rule for .gitignore is about that file and one for do is
about every file that ends in it. The value is a language's name or any
extension it already answers to — bash, sh and Bourne Again Shell (bash)
all name the same one. What you write here beats the guess, which is the point
of writing it; a value naming no syntax at all is reported on stderr and left
out, rather than quietly drawing the file plain.
You need not write it by hand. The file's header carries a picker of every syntax there is, next to the layout and wrapping buttons: it says what the open file is being drawn as, picking one writes the line into your own configuration file under the file's extension — so it holds for every file like it — and while something is written down, the button beside it hands the file back to the guess. The settings window has the same table as a table, with a searchable list to add one from, in whichever of the three files you are editing.
Dependencies are built at opt-level = 3 even in the dev profile. Debug
syntect is roughly ten times slower, which is the difference between a review
window and one the desktop offers to force-quit.