Support OSC-1717 metadata#1014
Draft
stefanhaller wants to merge 4 commits into
Wilfred:masterWilfred/difftastic:masterfrom
stefanhaller:osc-1717-metadatastefanhaller/difftastic:osc-1717-metadataCopy head branch name to clipboard
Draft
Support OSC-1717 metadata#1014stefanhaller wants to merge 4 commits intoWilfred:masterWilfred/difftastic:masterfrom stefanhaller:osc-1717-metadatastefanhaller/difftastic:osc-1717-metadataCopy head branch name to clipboard
stefanhaller wants to merge 4 commits into
Wilfred:masterWilfred/difftastic:masterfrom
stefanhaller:osc-1717-metadatastefanhaller/difftastic:osc-1717-metadataCopy head branch name to clipboard
Conversation
A host that renders difftastic's output (e.g. lazygit) needs to map a rendered diff cell back to its patch-space identity -- file, line type, new/old line numbers -- to act on the line the user points at. difftastic produces a structural, non-unified rendering (side-by-side columns, no @@/+/- markers, syntax-aware alignment), so that identity cannot be recovered from the painted text at all; the pager, which has it at render time, has to state it. Gate the emission on the OSC1717 environment variable so output is byte-for-byte unchanged outside such a host -- harmless in a raw terminal, less, or tmux. The host advertises the protocol versions it understands and difftastic emits the highest mutually-understood one (currently always v1 because that's the only version that exists, but the mechanism is extensible). The metadata is attached per cell, since side-by-side is difftastic's default and signature mode: the old-file (left) column carries the deleted/old line's identity, the new-file (right) column the added/new line's. A modification -- one aligned row, old on the left and new on the right -- thus emits two records (d left, a right), exactly as a unified diff splits a change into a - and a + line; context lines, shown on both sides, emit the same c record before each column. When a hunk's changes are all one-sided, difftastic collapses it to a single column; a modified row is then rendered once but still represents both patch lines, so it carries its d and its a back-to-back at the row's start (the d's region is zero-width -- spec 6.2), letting a host stage both halves from the one row it can see. difftastic wraps a long line into several output rows itself, and a host sees each as a distinct line, so every one carries the same record (so the host can act on a continuation row and treat the wrapped line as one block when navigating); only a side's padding rows, once its wrapped content is exhausted, and the blank counterpart half of a pure add/delete carry none. The line type is the row's patch-space type, from comparing the aligned lines' contents -- not the display's per-token novelty. A line changed only by added tokens has no novel old-side tokens, and a blank line has no tokens at all; classifying by novelty would tag such old lines c, and a host staging by the records would drop the deletion half of the modification (spec 5.1, 8). Genuinely identical lines that merely fall inside a hunk still compare equal and stay c. Unlike a unified-diff pager, difftastic already has the new/old line numbers natively (it always renders them as gutters) and receives the file path as a parameter, so no hunk-header counter tracking is needed: each record falls out of the row's aligned (Option<LineNumber>, Option<LineNumber>) and the content comparison. The one synthesized field is a pure deletion's new-file line, which difftastic (having no linear new-file counter) derives from the previous aligned new line. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Inline mode renders the diff grouped: all old-side content (before-context, then deletions) is printed before all new-side content (additions, then after-context), rather than interleaved like a unified diff. Attach the same per-line OSC records here -- the old-side passes emit the left-column record, the new-side passes the right-column one. Because the records carry the file and new/old line numbers explicitly, a host can reconstruct each line's identity despite the grouping: a modification's d (in the deletions group) and a (in the additions group) are emitted far apart in the output yet both reference the same new-file line, with the d also carrying the old-file line. A positional or structural re-parse of inline output could not pair them; the metadata makes each line self-describing. The line type is the row's patch-space type, from comparing the aligned lines' contents (as in side-by-side) rather than the all-or-nothing novelty the display colors hunk rows with -- so a context line that happens to fall inside a hunk is tagged c, not a/d, while a line changed only by added tokens (nothing highlighted on its old side) is still the d half of its modification. In the context passes, an old line with no new-side counterpart is a deletion even though this display files it under context (a deleted blank line has no tokens to be novel), and the mirrored new-side line is an addition; an aligned context row stays c even if its sides differ in whitespace, since only one side of it is ever rendered here and a d would hand the host a deletion whose addition it can never show. A pure deletion's new-file line is derived from the previous new-file line carried through the old-side passes, as in side-by-side. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A host can already learn difftastic's per-line diff metadata, but only from the records emitted before content lines — so a diff with no content (a binary file, or the empty diff a host would use to probe) emits nothing, and "speaks the protocol" is indistinguishable from "unsupported pager". Emit a version-only OSC 1717 record (no further fields) once, as difftastic's first output, whenever a version is negotiated. It's content-independent, so a host can probe difftastic on an empty diff and get a conclusive answer. Flushed explicitly because difftastic exits via process::exit, which skips the implicit stdout flush — without it the lone handshake of an empty probe could be lost. See the diff-line-metadata OSC spec, §4.4. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
difftastic renders no `@@`/`diff --git` rows; its only structural row is one banner per hunk (`path --- N/total --- Format`) announcing both the file and the hunk. Annotate it with the header records so a host can anchor file and hunk navigation on the banners themselves and can see files with no content lines: every banner carries its hunk's `h` record with the hunk's first new-file line, and the first hunk's banner -- the only row announcing the file -- additionally carries the file's `f` record before the `h`, per the spec's combined-header rule (§5.5). `f` never carries a line number; the spec fixes each header type's payload rather than leaving it to the renderer (a streaming renderer like delta cannot know the first-hunk line at its file header). Unlike delta, difftastic builds the whole diff before rendering, so the hunk's first new-file line is in hand when the banner is printed: the first aligned row with a new-file line provides it, falling back to the position the next new line would occupy for a hunk with none (a pure deletion), mirroring the deletion convention. In inline mode the banner print moves below the context calculation for the same reason. A whole-file add/delete renders through the single-column path as one hunk, so its banner carries `f` plus an `h` at line 1 (addition) or 0 (deletion, matching its `d` records). A rename's two-row banner repeats the records on each row (spec §6.4). With OSC1717 unset, output remains byte-for-byte identical to stock difftastic.
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.
This adds support for the OSC-1717 spec described in jesseduffield/lazygit#5731; see also jesseduffield/lazygit#5732 for more context.
Closes #1004.