mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-09-10 23:56:24 -04:00
Revise the OSC spec draft after the first review pass
Incorporate the round of feedback before circulating to pager developers. The substantive decisions: - Make f and h (file-header / hunk-header) mandatory v1 record types and drop the speculative o; a header carries new-line = its hunk's first line so e lands near what the user is looking at, and a multi-row header block (delta's box) re-emits the record on every row, mirroring the wrapped-row rule. This replaces the old reserved-and-unused framing and the host-side backUpOverHeader guessing. - A host must ignore an unrecognized type rather than reject the record, so the type set can grow without a version bump. - Drop sections that were redundant or over-stated: §5.4 (the side is already motivated in §1), §8.1 (c/a old-line is a host concern with no consumer), §8.4 (word-diff), and the at-most-two-records and trailing-fields paragraphs. - Tighten the difftastic token-vs-line note (now §8.1): the c-on-a-pure-addition old line only happens in difftastic's AST mode and the record faithfully matches what difftastic renders (dim context); its line/Text fallback emits d/a normally. - Document the rename rule for the file field, and reframe §7 as the host access model the carrier is designed for rather than a description of the prototype. Also record in the design notes the one user-visible bite of the token-vs-line model: cross-pager selection preservation (delta d vs difftastic-AST c) — a host concern kept out of the spec. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
a1eec2f3b6
commit
1eb032abe8
|
|
@ -682,6 +682,16 @@ on the left still opens new-file line 4 — but it is a genuine semantic gap, an
|
|||
exactly the kind of thing the prototype exists to find. **The faithful emission
|
||||
was chosen deliberately** (it surfaces the mismatch rather than hiding it).
|
||||
|
||||
> **Note (session 9): the one user-visible bite is cross-pager selection
|
||||
> preservation (#7).** Within difftastic this is invisible — it *renders* the old
|
||||
> line as dim context, matching the `c` record. But it only happens in difftastic's
|
||||
> **AST mode**; in its line/Text fallback (e.g. our test file with `let x=1;` in a
|
||||
> `.go`, which fails the Go parse) difftastic diffs by line and emits `d`/`a` like a
|
||||
> unified diff. The bite: select the old line in delta (where it is a red `d`), then
|
||||
> `|` to difftastic AST mode, where the same patch line is now `c` — the identity
|
||||
> restore (#7) can't match `d` against `c`, so the selection is dropped. Minor in
|
||||
> practice; a host concern, not a pager-spec one (kept out of the spec).
|
||||
|
||||
Options to flag for the spec / production, none taken now:
|
||||
- **Host-side:** treat an old-column cell that is aligned with a novel new line as
|
||||
the `-` side of a modification (the host knows it's the left column).
|
||||
|
|
|
|||
|
|
@ -41,13 +41,8 @@ the pager.
|
|||
But the moment a pager **restructures** the diff, the unified-diff structure the
|
||||
parse relies on is gone:
|
||||
|
||||
- `delta` in its default mode conveys the side (added/removed) purely by
|
||||
background color and prefixes a line-number gutter, so there is no `+`/`-`
|
||||
marker to read and no parseable `@@` body;
|
||||
- `delta --color-only --line-numbers` (the configuration people actually use)
|
||||
pushes the `+`/`-` marker off the start of the line behind the gutter, so a
|
||||
naive parse is *confidently wrong*;
|
||||
- `diff-so-fancy` rewrites the headers and strips the `+`/`-` markers entirely;
|
||||
- `delta` (default mode) and `diff-so-fancy` drop or hide the `+`/`-` markers and
|
||||
convey the side with color, leaving no parseable unified-diff structure;
|
||||
- `difftastic` is token-granular and side-by-side — there is no unified-diff line
|
||||
structure left in *either* of its modes.
|
||||
|
||||
|
|
@ -131,11 +126,15 @@ Raw bytes, for a context line at new-file line 10 of `src/foo.go`:
|
|||
| field | presence | meaning |
|
||||
|---|---|---|
|
||||
| `version` | always | decimal protocol version; `1` for v1. Carried in every record so attachments are self-describing. |
|
||||
| `type` | always | one character — see §5.1. v1 emits `c` (context), `a` (added), `d` (deleted). |
|
||||
| `new-line` | content lines | new-file line number, in the **diff's new-file space** (see §5.2). |
|
||||
| `type` | always | one character — see §5.1. v1 emits `c` (context), `a` (added), `d` (deleted), `f` (file header), `h` (hunk header). |
|
||||
| `new-line` | content & header lines | new-file line number, in the **diff's new-file space** (see §5.2). |
|
||||
| `old-line` | only `type=d` | old-file line number. **Empty** for `c` and `a`. |
|
||||
| `file` | always | the file path the line belongs to; absolute or repo-root-relative (the host normalizes — emit whichever is convenient). Carried on **every** record so a single record is a complete answer. |
|
||||
|
||||
For a **renamed** file, `file` is the **new** path (git's `+++ b/…` side); the old
|
||||
path is not carried. A pure rename with no content change emits no records at all
|
||||
(it has no content lines, only header rows).
|
||||
|
||||
### 4.3 Examples
|
||||
|
||||
| rendered line | emitted record (`;`-form) |
|
||||
|
|
@ -144,7 +143,9 @@ Raw bytes, for a context line at new-file line 10 of `src/foo.go`:
|
|||
| addition, new line 11 | `1717;1;a;11;;src/foo.go` |
|
||||
| deletion, old line 9, sits at new pos 11 | `1717;1;d;11;9;src/foo.go` |
|
||||
| two consecutive deletions | `…;d;11;9;…` then `…;d;11;10;…` (same `new-line`, different `old-line` — see §5.3) |
|
||||
| whole-file deletion | `1717;1;d;0;9;old/path` (`new-line` 0 — see §5.5) |
|
||||
| whole-file deletion | `1717;1;d;0;9;old/path` (`new-line` 0 — see §5.4) |
|
||||
| file header | `1717;1;f;10;;src/foo.go` (`new-line` = the file's first hunk line — §5.2) |
|
||||
| hunk header | `1717;1;h;10;;src/foo.go` |
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -152,24 +153,30 @@ Raw bytes, for a context line at new-file line 10 of `src/foo.go`:
|
|||
|
||||
### 5.1 Type
|
||||
|
||||
`type` is one character. v1 defines and emits three (the content-line types):
|
||||
`type` is one character. v1 defines five, and a conforming pager emits each
|
||||
wherever the corresponding row exists:
|
||||
|
||||
- `c` — context (unchanged) line
|
||||
- `a` — added line
|
||||
- `d` — deleted line
|
||||
- `f` — file header — the row a pager renders to announce a file
|
||||
- `h` — hunk header — the row announcing a hunk (e.g. a `@@ … @@` line)
|
||||
|
||||
Reserved for future use (v1 emitters do **not** emit these; header rows simply
|
||||
carry no record and the host falls back):
|
||||
`c`/`a`/`d` are the content-line types. `f`/`h` mark the structural rows so the
|
||||
host can place every file and hunk **exactly** — for navigation, and for acting on
|
||||
a header — instead of inferring structure from layout, the one thing it cannot do.
|
||||
They are not optional: a pager emits `f` on each file-header row and `h` on each
|
||||
hunk-header row it renders (a pager that renders no hunk-header rows, e.g.
|
||||
difftastic, simply has none to tag). See §6.4.
|
||||
|
||||
- `h` — hunk header (`@@ … @@`)
|
||||
- `f` — file header (`diff --git`, `---`, `+++`)
|
||||
- `o` — other
|
||||
A host **must ignore a `type` it does not recognize** (treat the row as
|
||||
non-actionable) rather than reject the record, so the set can grow later without a
|
||||
version bump.
|
||||
|
||||
**`type` is load-bearing and cannot be inferred from the other fields.** Under the
|
||||
coordinate rules, `added` and `context` both carry `{new-line present, old-line
|
||||
absent}`, so presence alone cannot distinguish them — yet the host must (scroll
|
||||
preservation anchors specifically on *change* lines, so it has to tell `added`
|
||||
from `context`). Hence an explicit type rather than an inferred one.
|
||||
**`type` is load-bearing and cannot be inferred from the other fields.** `added`
|
||||
and `context` both carry `{new-line present, old-line absent}`, so presence alone
|
||||
cannot distinguish them — yet the host must (scroll preservation anchors
|
||||
specifically on *change* lines). Hence an explicit type.
|
||||
|
||||
### 5.2 Line-number spaces
|
||||
|
||||
|
|
@ -179,6 +186,11 @@ from `context`). Hence an explicit type rather than an inferred one.
|
|||
expected to re-map this through its own diff↔worktree adjustment; the pager
|
||||
should emit the number as it appears in the diff it is rendering.
|
||||
- `old-line` is the old-file line number, present **only** for deletions.
|
||||
- On a **header** record (`f`/`h`), `new-line` is the new-file line of the first
|
||||
line of the hunk it heads — for `h` that hunk, for `f` the file's first hunk —
|
||||
and `old-line` is empty. This is what lets `e` ("open in editor") on a header
|
||||
land at the top of what the user is looking at. (A round-trip *through* staging
|
||||
from a header is therefore only approximate, which is an accepted trade.)
|
||||
|
||||
### 5.3 The deleted-line convention (both numbers)
|
||||
|
||||
|
|
@ -202,15 +214,7 @@ them) and are told apart only by `old-line`. Example — two deletions at old li
|
|||
A host uses `old-line` to find a deletion's patch line and its old-side
|
||||
(review/PR `L`) anchor, and `new-line` for the editor target.
|
||||
|
||||
### 5.4 Why the side must be carried at all
|
||||
|
||||
It is tempting to think the side (added vs deleted) is redundant with the rendered
|
||||
glyph. It is not: in `delta`'s default rendering there are **no `+`/`-` glyphs** —
|
||||
the side is conveyed purely by background color, which a host reading decolorized
|
||||
text cannot recover. The pager therefore has to state the side explicitly, which
|
||||
is what the `a`/`d` type does.
|
||||
|
||||
### 5.5 Whole-file add / delete
|
||||
### 5.4 Whole-file add / delete
|
||||
|
||||
A deleted file's lines carry `new-line` = `0` (mirroring git's `@@ -1,N +0,0 @@`);
|
||||
an added file's lines carry the new-file numbers normally and `type=a`.
|
||||
|
|
@ -242,12 +246,6 @@ one before each column:
|
|||
|
||||
The blank counterpart of a pure add/delete (the empty half) carries **no** record.
|
||||
|
||||
A side-by-side row carries **at most two** records — one per column. (An earlier
|
||||
draft worried token-level tools might need *N* records per row; they do not.
|
||||
Token-level novelty is *sub-cell coloring*, not separate identity: a column's cell
|
||||
is one source line however many tokens within it are highlighted. Two columns →
|
||||
at most two records.)
|
||||
|
||||
**v1 needs no column/side discriminator field for this.** `type` already implies
|
||||
the column — `a` is inherently the new/right side, `d` the old/left side — and a
|
||||
context line's two halves are the *same* logical line, so the host tells the two
|
||||
|
|
@ -279,22 +277,33 @@ record on its padding rows.
|
|||
|
||||
Getting this wrong is not theoretical: without per-row records, acting on a
|
||||
wrapped continuation row does nothing, and hunk/file navigation breaks because the
|
||||
untagged rows fragment a wrapped line into one block per visual row. Both delta and
|
||||
difftastic had this bug in their prototypes and were fixed to emit per-row.
|
||||
untagged rows fragment a wrapped line into one block per visual row.
|
||||
|
||||
### 6.4 Header rows
|
||||
|
||||
Hunk headers (`@@`), file headers (`diff --git`, `---`, `+++`), and any other
|
||||
non-content rows carry **no** record in v1 (the `h`/`f`/`o` types are reserved but
|
||||
unused). A host acting on a header row falls back to whatever non-metadata path it
|
||||
has, or to no action.
|
||||
File-header and hunk-header rows carry records too, and these are **mandatory**:
|
||||
`f` on each file-header row, `h` on each hunk-header row (§5.1). With them the host
|
||||
places every file and hunk exactly — file/hunk navigation lands on the header, and
|
||||
`e` on a header opens at the hunk's first line — with no layout guessing.
|
||||
|
||||
Where a header spans **several rows** — delta boxes a file name in divider/name/
|
||||
divider lines; a pager might draw a rule under a hunk header — the recommendation
|
||||
is that **every row of that block carry the same record** (all three box lines get
|
||||
the file's `f`; a hunk header and its rule get that hunk's `h`), exactly as a
|
||||
wrapped content line re-emits its record on every row (§6.3). That leaves no dead
|
||||
rows in a header block — the user can press `e` anywhere on it and land in the same
|
||||
place. The pager has the final say over what counts as its header block and which
|
||||
row "is" the heading; this is the recommended default, not a hard rule.
|
||||
|
||||
A pager with no hunk-header rows (e.g. difftastic) has no `h` to emit. A row a pager
|
||||
genuinely leaves un-recorded is treated by the host as non-actionable.
|
||||
|
||||
---
|
||||
|
||||
## 7. How the host consumes it (informative)
|
||||
|
||||
This section is not normative — it describes how lazygit's prototype consumes the
|
||||
protocol, to make the emit rules concrete.
|
||||
This section is not normative — it sketches the access model the per-cell carrier
|
||||
is designed for, to make the emit rules concrete.
|
||||
|
||||
- The host attaches each record to the following cell, like an OSC-8 hyperlink. If
|
||||
a record has no following cell (a genuinely empty rendered line), the host adds
|
||||
|
|
@ -302,8 +311,10 @@ protocol, to make the emit rules concrete.
|
|||
- **Row-granular action** (e.g. a keyboard "act on this line"): use the **first**
|
||||
record on the row. In side-by-side this is the left column — fine, since the two
|
||||
sides of a change are one hunk for staging purposes.
|
||||
- **Point-granular action** (a mouse click): use the **nearest record at or to the
|
||||
left of the click column** → lands in the column actually clicked.
|
||||
- **Point-granular action** (a mouse click): the per-cell attachment lets a host
|
||||
use the **nearest record at or to the left of the click column**, landing in the
|
||||
column actually clicked. (A host may equally resolve clicks at row granularity;
|
||||
the carrier supports either.)
|
||||
- The host normalizes `file` (resolving it relative to the repository working
|
||||
tree) and otherwise treats the record as opaque identity.
|
||||
|
||||
|
|
@ -311,63 +322,28 @@ protocol, to make the emit rules concrete.
|
|||
|
||||
## 8. Known limitations and v2 candidates — feedback wanted
|
||||
|
||||
These are surfaced honestly because the protocol is a draft. None blocks v1; each
|
||||
is a place where pager-author input would shape v2.
|
||||
None of these blocks v1; each is a place where input would shape a future version.
|
||||
|
||||
### 8.1 `c` and `a` records carry no old-file number
|
||||
### 8.1 The token-vs-line model mismatch (difftastic, AST mode)
|
||||
|
||||
For context and addition records, `old-line` is empty. This is invisible in a
|
||||
single-column rendering, but **side-by-side makes it visible**: the old/left
|
||||
column shows a real old-file line number on screen, yet the record for that cell
|
||||
reports only `new-line`. With difftastic it bites hardest — difftastic *always*
|
||||
shows old-file numbers, old ≠ new is the norm, and the offset is **not constant**
|
||||
(it follows structural alignment, not a per-hunk delta), so the old number is not
|
||||
even derivable from the new one.
|
||||
Our `c`/`a`/`d` set is git's **line-granular** shape: a modified line is a `-` plus
|
||||
a `+`. difftastic, when it parses the language (its **AST/token mode**), is finer —
|
||||
it aligns lines and marks novelty per token. A line changed *only by added tokens*
|
||||
(e.g. `println!("{}", x);` → `println!("{}", x + y);`) then has **no novelty on the
|
||||
old side**, so difftastic renders that old line as context (dimmed, not red) and the
|
||||
record faithfully says `c`, not `d`; only the new line gets `a`. (In difftastic's
|
||||
line/Text fallback — e.g. an unparseable file — it diffs by line and emits `d`/`a`
|
||||
as expected.)
|
||||
|
||||
Nothing in the current host consumers needs the old number for `c`/`a`, so v1
|
||||
leaves it empty. The clean v2 fix, if needed, is **carry both numbers on every
|
||||
record** (applies to both single-column and side-by-side) — *not* a side-only tag.
|
||||
Because the record matches what difftastic shows, this isn't visible within
|
||||
difftastic itself. The consequence is only for a host mapping cells back to git's
|
||||
*line* diff: that old cell reports context at the new line, so its old-side `-`
|
||||
identity isn't recoverable (impact small — `e` opens the right new-file line, and
|
||||
users act on the changed side). A `modified`/`m` type — "aligned, changed, present
|
||||
on both sides" — would name the case directly but splits the clean `c`/`a`/`d`
|
||||
mapping; recorded as a v2 candidate, not taken (§9).
|
||||
|
||||
### 8.2 The token-vs-line model mismatch (difftastic)
|
||||
|
||||
The unified-diff pagers (git, delta, diff-so-fancy) derive from git's
|
||||
**line-granular** patch, where a modified line is by construction a `-` line plus a
|
||||
`+` line. The `c`/`a`/`d` type set fits that model exactly.
|
||||
|
||||
difftastic is **token-granular**: it aligns an old line with a new line and marks
|
||||
novelty *per token*. That produces aligned rows the line model has no clean slot
|
||||
for. Real example — `let x=1; println!("{}", x);` changed to
|
||||
`let x=2; let y=3; println!("{}", x + y);`:
|
||||
|
||||
```
|
||||
…;c;4;;src/lib.rs 3 println!("{}", x); …;a;4;;src/lib.rs 4 println!("{}", x + y);
|
||||
```
|
||||
|
||||
The old line `println!("{}", x);` has no novel tokens (they all survive into the
|
||||
new line), while the new line added `+ y`. So difftastic's faithful per-cell
|
||||
verdict is **`c` on the left, `a` on the right** — the same aligned row carrying a
|
||||
context record and an addition record, with **no `d`**, because by difftastic's
|
||||
model nothing was deleted.
|
||||
|
||||
A host mapping cells to git's line-granular patch then resolves the right/added
|
||||
cell correctly, but the left/old cell resolves as *context at the new line*, not
|
||||
as git's `-` line — its old-side deletion identity is not recoverable from the
|
||||
record. Practical impact is small (users click the changed side; an editor target
|
||||
on the left still opens the right new-file line), but it is a genuine semantic gap.
|
||||
|
||||
Options, none taken in v1, all wanted-as-feedback:
|
||||
|
||||
- **Host-side:** treat an old-column cell aligned with a novel new line as the `-`
|
||||
side of a modification (the host knows it is the left column).
|
||||
- **Emitter-side, v1-compatible:** difftastic could emit `d` for the old side of
|
||||
*any* aligned changed row — but that re-imposes the very line-granular model
|
||||
difftastic exists to escape, discarding its more precise "this was not removed"
|
||||
judgement. Probably wrong to force.
|
||||
- **A `modified` / `m` type (v2):** names the case directly — "aligned, changed,
|
||||
present on both sides" — but splits the clean `c`/`a`/`d` staging mapping and
|
||||
needs every pager to agree when to use it. Recorded as a v2 candidate.
|
||||
|
||||
### 8.3 Pure-deletion `new-line` in token tools is synthesized
|
||||
### 8.2 Pure-deletion `new-line` in token tools is synthesized
|
||||
|
||||
A line-granular pager always knows the new-file position a deletion sits at. A
|
||||
token tool without a linear new-file counter (difftastic) computes it from the
|
||||
|
|
@ -376,11 +352,6 @@ can drift across hunk boundaries or with zero context lines. The deletion's
|
|||
*old*-line (its real identity for staging) is always exact; only the editor-target
|
||||
`new-line` is approximate. Pagers should emit the most precise `new-line` they can.
|
||||
|
||||
### 8.4 Out of scope for v1
|
||||
|
||||
`git --word-diff` (inline `[-…-]{+…+}` markup, no per-line `+`/`-`) has no clean
|
||||
per-line identity and is not addressed.
|
||||
|
||||
---
|
||||
|
||||
## 9. Where feedback is most wanted
|
||||
|
|
@ -392,17 +363,15 @@ per-line identity and is not addressed.
|
|||
central registry, so this is "verified unused across the terminals that matter,"
|
||||
not "allocated." If you know of a terminal that interprets `1717`, please say so.
|
||||
2. **The env-var name and grammar** (`EMIT_OSC1717_METADATA=V1,…`).
|
||||
3. **The both-numbers question** (§8.1) — is per-column-exact old/new numbering
|
||||
worth a v2, or is it never needed?
|
||||
4. **The token-vs-line mismatch** (§8.2) — should there be an `m` type, or is
|
||||
3. **The token-vs-line mismatch** (§8.1) — should there be an `m` type, or is
|
||||
host-side inference the right home for it?
|
||||
5. **Can your pager actually produce all four fields per region?** In particular
|
||||
4. **Can your pager actually produce all four fields per region?** In particular
|
||||
the side for deleted lines, and in side-by-side mode. (delta needed to track
|
||||
its own old/new counters because its line-number counters are dormant unless
|
||||
`--line-numbers` is on; difftastic had them natively. Your mileage may vary.)
|
||||
6. **Optional trailing fields within a version** — a cheap, additive escape valve
|
||||
short of a version bump: a consumer stops at the fields it knows. Is this worth
|
||||
blessing in v1, or should all growth go through the version field?
|
||||
5. **Is emitting `f`/`h` on header rows a burden?** v1 makes them mandatory (§5.1,
|
||||
§6.4); it should be cheap — the pager already knows the file and the hunk's
|
||||
start line — but tell us if your format makes it awkward.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue