Skip to content

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

Merged
stefanhaller merged 7 commits into
masterfrom
support-osc-1717-diff-metadata
Oct 5, 2026
Merged

stefanhaller merged 7 commits into
masterfrom
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 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 2 times, most recently from ed08dd6 to debcd1f Compare September 21, 2026 13:46
@stefanhaller
stefanhaller force-pushed the support-osc-1717-diff-metadata branch 2 times, most recently from 0c60ec6 to 5b845d7 Compare September 25, 2026 10:14
@stefanhaller
stefanhaller force-pushed the support-osc-1717-diff-metadata branch 2 times, most recently from 1dbb705 to ea6d77c Compare October 1, 2026 15:28
@stefanhaller
stefanhaller force-pushed the support-osc-1717-diff-metadata branch from ea6d77c to aa36b7a Compare October 4, 2026 13:48
@stefanhaller
stefanhaller force-pushed the support-osc-1717-diff-metadata branch from aa36b7a to e09a4dc Compare October 4, 2026 15:01
@stefanhaller
stefanhaller force-pushed the support-osc-1717-diff-metadata branch from e09a4dc to 9fa207e Compare October 4, 2026 17:01
Base automatically changed from resolve-diff-lines-to-identities to master October 5, 2026 07:54
stefanhaller and others added 7 commits October 5, 2026 09:54
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 9fa207e to 3af5e79 Compare October 5, 2026 07:55
@stefanhaller
stefanhaller merged commit 9d4424b into master Oct 5, 2026
12 checks passed
@stefanhaller
stefanhaller deleted the support-osc-1717-diff-metadata branch October 5, 2026 08:03
stefanhaller added a commit that referenced this pull request Oct 5, 2026
Third PR in the stack of PRs towards staging hunks directly from the
main view. This one is stacked on #6029.

The focused main view is already reachable with `0` or by clicking it.
This PR gives it a real selection and line-level commands, so that a
diff is something you move around in rather than only scroll.

A diff shown in a focused main view now always carries a selection,
anchored at the first visible change line when you focus it. There is no
toggle and no config for it. Non-diff main views, such as a branch's
log, keep no selection.

Keys in the focused main view:

- `↑`/`↓` move the selection, `v` starts a range, `a` toggles between
hunk and line-by-line selection, `shift+↑`/`shift+↓` extend. These all
work the same as in the staging panel.
- `←`/`→` jump by hunk (change block), `n`/`N` by file. Both new keys
have config entries, `keybinding.main.nextFile` and
`keybinding.main.prevFile`, defaulting to `n` and `N`.
- `e` opens the selected line in your editor. Over a file header it
opens the file without a line.
- Clicking selects the clicked line, and dragging selects a range.

The selection is drawn as a narrow bar at the left edge, two columns
wide, not across the whole line. Now that a diff view always carries a
selection, a full-width highlight takes over the line's background, and
a selected hunk becomes one solid block with no boundary between what
was removed and what replaced it. Renderers that say which side of the
diff a line is on by colouring its background suffer most, but git's own
red and green text reads badly that way too. Since there is no rendering
of a diff that a full-width highlight doesn't degrade, there is nothing
here worth configuring.

This PR by itself is not very useful yet, so it shouldn't be released on
its own; you can't do anything with the selection except press `e` for
edit. It will take us several more PRs until you can actually stage the
selected hunk.
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