Skip to content

Support diff renderers that emit OSC 1717 diff line metadata - #6029

Open
stefanhaller wants to merge 7 commits into
resolve-diff-lines-to-identitiesfrom
support-osc-1717-diff-metadata
Open

stefanhaller wants to merge 7 commits into
resolve-diff-lines-to-identitiesfrom
support-osc-1717-diff-metadata

Conversation

@stefanhaller

Copy link
Copy Markdown
Collaborator

Second PR in the stack of PRs towards staging hunks directly from the main view. This one is stacked on #6026.

Parsing the rendered diff back (#6026) only works for renderings that stay faithful to a unified diff. If a renderer puts the line numbers in a gutter, or lays the diff out side by side, the +/- marker moves off the start of each body line and the parse refuses the result. Those are exactly the renderers people like using.

OSC 1717 is a small terminal protocol that lets a diff renderer state, inline in its own output, which file and line each row of its rendering stands for. The spec is at https://github.com/stefanhaller/diff-line-metadata-spec. This PR implements lazygit's side of it. Nothing changes for users yet; the consumers arrive in the following PRs.

What it does:

  • Advertises the protocol by setting OSC1717=V1 in the environment of every renderer invocation, and of git itself. If a renderer understands the variable, it answers with a handshake record and then emits records. If it doesn't, nothing changes and lazygit works exactly as before.
  • Reads the records in gocui and attaches each payload to the cells that follow it, clearing at line boundaries. A row carries every distinct payload on it, so a side-by-side row reports both of its sides.
  • Swallows the version-only handshake record, so it doesn't show up as a stray blank line.
  • Keeps records whose region covers no cell at all. Back-to-back records with nothing between them get their payloads drained into carrier cells, so a modification pair collapsed into one column stays discoverable.
  • Slots the metadata backend ahead of the buffer parser, so a conforming renderer's own word is taken where it is given, and the parse is the fallback.

There is also a preparatory refactor in the escape-sequence parser. It dispatched on a single character, so only the single-digit OSC 8 could be recognized; it now accumulates the number.

Reference emitters exist as open draft PRs against delta (#2181), difftastic (#1014) and diff-so-fancy (#538), and as an unproposed patch to git for its word-diff formats. None of them is merged.

@stefanhaller
stefanhaller added this pull request to stack #6027 September 19, 2026 20:00
@stefanhaller stefanhaller added the maintenance For refactorings, CI changes, tests, version bumping, etc label Sep 19, 2026
@stefanhaller
stefanhaller force-pushed the support-osc-1717-diff-metadata branch from 661842c to 49b0c3f Compare September 20, 2026 17:51
@stefanhaller
stefanhaller force-pushed the support-osc-1717-diff-metadata branch from 49b0c3f to ed08dd6 Compare September 21, 2026 12:44
stefanhaller and others added 7 commits September 21, 2026 15:44
The OSC parser dispatched on a single character, so only the
single-digit OSC 8 could ever be recognized; the diff-line metadata
protocol we are about to read uses OSC 1717.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
A diff renderer that restructures the diff — into columns, or with the
+/- markers replaced by colour — leaves us no way to tell which line of
which file a rendered row came from, which is what acting on the row
requires. The OSC 1717 protocol has the renderer say so directly: it
prefixes each line it renders with a record naming the file and the
line's position in the old and new versions of it.

Attach each record to the cells it precedes, so that a row's records
survive wrapping and the columns of a side-by-side rendering, and hand
them to readers together with the row's text: the two have to describe
the same buffer, and a re-render can rebuild it between two reads.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Before the diff, a conforming renderer emits one OSC 1717 record that
carries only the version — its way of announcing that it speaks the
protocol at all, without a host having to inspect what it renders. It
describes no line, so keeping it would give the first line of the diff a
record that says nothing about it.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Wherever two diff lines end up on one rendered line, a renderer emits
their records back to back: the deletion and the addition of a
modification collapsed into a single column, or a banner announcing a
file and its first hunk at once. A changed line that is empty is
rendered as its record alone. Attaching a record only to the cells it
precedes loses all of these — the last record of a run wins, and an
empty changed line becomes a line we can say nothing about at all.

Give such a record a cell of its own instead. It renders nothing, so the
diff looks the same, but the line keeps every record it was given.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
A diff line's identity is about to become recoverable from a second
source, a diff renderer's own records, and a renderer states the path
however it likes — absolute paths included. The field can't promise
repo-relative any more.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Where a renderer states which line of which file it is rendering, take
that as the line's identity. The renderer knows, and for a rendering
that no longer looks like a diff nothing else does. Reading the rendered
text stays the way for the renderings that keep a diff's structure, and
for the renderers that say nothing at all.

Which of the two is used is settled for the rendering as a whole, not
row by row. A rendering with records is not parsed at all, not even for
the rows the renderer says nothing about. Its text is no unified diff,
and a row of it can read like one. Under delta, a commit that adds a
test whose input is a diff shows the test's "diff --git a/img.png
b/img.png" line as a row of its own; parsed, that row opens a file
section that runs to the end of the rendering, and every row delta puts
between hunks and files comes out as a header of a file called img.png.
So the untagged rows of such a rendering stay unresolved, as the
protocol has it (spec section 6.4).

Header records are accepted too. This lets a renderer point at a file
with no content lines, such as a pure rename, a mode change or a binary
file: for those files the header is the only row in the diff.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A renderer that speaks the protocol emits nothing unless it is asked to,
so that its output stays plain wherever it is used outside lazygit. The
variable names the versions we understand.

git is one of the renderers we ask. It has no pager to spawn, and so no
terminal to spawn one in, but it still renders the diff itself — for the
word-diff formats, whose markup nothing else could resolve — so the
request has to be made before we decide a pty isn't needed.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
@stefanhaller
stefanhaller force-pushed the support-osc-1717-diff-metadata branch from ed08dd6 to debcd1f Compare September 21, 2026 13:46

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

maintenance For refactorings, CI changes, tests, version bumping, etc

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant