@singapore-editor/diff 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/README.md +158 -0
  2. package/dist/diffGutter.d.ts +37 -0
  3. package/dist/diffGutter.d.ts.map +1 -0
  4. package/dist/diffGutter.js +189 -0
  5. package/dist/diffGutter.js.map +1 -0
  6. package/dist/diffRows.d.ts +5 -0
  7. package/dist/diffRows.d.ts.map +1 -0
  8. package/dist/diffRows.js +40 -0
  9. package/dist/diffRows.js.map +1 -0
  10. package/dist/diffSyntax.d.ts +124 -0
  11. package/dist/diffSyntax.d.ts.map +1 -0
  12. package/dist/diffSyntax.js +491 -0
  13. package/dist/diffSyntax.js.map +1 -0
  14. package/dist/editorDiffPlugin.d.ts +74 -0
  15. package/dist/editorDiffPlugin.d.ts.map +1 -0
  16. package/dist/editorDiffPlugin.js +546 -0
  17. package/dist/editorDiffPlugin.js.map +1 -0
  18. package/dist/editorOptions.d.ts +15 -0
  19. package/dist/editorOptions.d.ts.map +1 -0
  20. package/dist/editorOptions.js +33 -0
  21. package/dist/editorOptions.js.map +1 -0
  22. package/dist/gutters.d.ts +53 -0
  23. package/dist/gutters.d.ts.map +1 -0
  24. package/dist/gutters.js +122 -0
  25. package/dist/gutters.js.map +1 -0
  26. package/dist/index.d.ts +18 -0
  27. package/dist/index.d.ts.map +1 -0
  28. package/dist/index.js +11 -0
  29. package/dist/inline.d.ts +3 -0
  30. package/dist/inline.d.ts.map +1 -0
  31. package/dist/inline.js +72 -0
  32. package/dist/inline.js.map +1 -0
  33. package/dist/lines.d.ts +8 -0
  34. package/dist/lines.d.ts.map +1 -0
  35. package/dist/lines.js +49 -0
  36. package/dist/lines.js.map +1 -0
  37. package/dist/liveProjection.d.ts +11 -0
  38. package/dist/liveProjection.d.ts.map +1 -0
  39. package/dist/liveProjection.js +152 -0
  40. package/dist/liveProjection.js.map +1 -0
  41. package/dist/model.d.ts +4 -0
  42. package/dist/model.d.ts.map +1 -0
  43. package/dist/model.js +247 -0
  44. package/dist/model.js.map +1 -0
  45. package/dist/projection.d.ts +18 -0
  46. package/dist/projection.d.ts.map +1 -0
  47. package/dist/projection.js +281 -0
  48. package/dist/projection.js.map +1 -0
  49. package/dist/regions.d.ts +28 -0
  50. package/dist/regions.d.ts.map +1 -0
  51. package/dist/regions.js +0 -0
  52. package/dist/regions.js.map +1 -0
  53. package/dist/style.css +145 -0
  54. package/dist/theme.d.ts +2 -0
  55. package/dist/theme.d.ts.map +1 -0
  56. package/dist/theme.js +43 -0
  57. package/dist/theme.js.map +1 -0
  58. package/dist/types.d.ts +78 -0
  59. package/dist/types.d.ts.map +1 -0
  60. package/package.json +44 -0
package/README.md ADDED
@@ -0,0 +1,158 @@
1
+ # @singapore-editor/diff
2
+
3
+ Diff rendering and editor-diff helpers for Singapore.
4
+
5
+ ## Install
6
+
7
+ ```sh
8
+ npm install @singapore-editor/core @singapore-editor/diff
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ A diff is a real `Editor` with one plugin. The plugin owns the diff model, the projected rows, the
14
+ expansion state and the gutter; the **host owns the editor's document**, because no plugin context
15
+ can mutate document text. So the host pushes the plugin's rows in and re-applies its tokens:
16
+
17
+ ```ts
18
+ import {
19
+ createDiffEditorOptions,
20
+ createDiffPlugin,
21
+ joinRenderLines,
22
+ parseGitPatch,
23
+ } from '@singapore-editor/diff'
24
+ import { Editor } from '@singapore-editor/core/editor'
25
+ import '@singapore-editor/core/style.css'
26
+ import '@singapore-editor/diff/style.css'
27
+
28
+ const plugin = createDiffPlugin({ mode: 'document', side: 'stacked' })
29
+ const editor = new Editor(host, {
30
+ ...createDiffEditorOptions(),
31
+ plugins: [plugin],
32
+ tabSize: 4,
33
+ })
34
+
35
+ const push = () => {
36
+ // The tokens travel with the text: `setText` replaces the document, and a toggle without them
37
+ // would repaint uncoloured until the next parse.
38
+ //
39
+ // `Editor.syncText` is the cheaper alternative — it computes the minimal prefix/suffix edit
40
+ // rather than tearing the document down, and for an expansion that edit is exactly the inserted
41
+ // lines. It is not the default here because it also preserves caret and selection across rows
42
+ // that have moved, and an expansion moves every row below the region: a reader holding a
43
+ // selection would find it pointing at different text. Worth taking if your host has no selection
44
+ // to lose.
45
+ editor.setText(joinRenderLines(plugin.getRows()), { tokens: plugin.getTokens() })
46
+ }
47
+ plugin.onDidChangeRows(push)
48
+ plugin.onDidChangeTokens(() => editor.setTokens(plugin.getTokens()))
49
+
50
+ plugin.setFile(parseGitPatch(patchText)[0])
51
+ ```
52
+
53
+ `createDiffEditorOptions()` returns what the diff's editor needs: a static read-only document, the
54
+ configured `tabSize` with no indentation guess, no cursor-line paint over the row tint, `folding:
55
+ false` so no fold command can hide projected rows, and the navigation, selection and find keys.
56
+ Spread it and add your own options: plugins, typography, theme. The pushed text carries no
57
+ `languageId`, because the language belongs to the plugin's per-side syntax documents.
58
+
59
+ `keymap` and `cursorLineHighlight` are whole objects, and a field an override leaves out takes the
60
+ editor default: every key pack and a painted cursor line. Extend them:
61
+
62
+ ```ts
63
+ const preset = createDiffEditorOptions()
64
+ const editor = new Editor(host, {
65
+ ...preset,
66
+ cursorLineHighlight: { ...preset.cursorLineHighlight, gutterNumber: true },
67
+ keymap: { ...preset.keymap, layers: [...preset.keymap.layers, hostLayer] },
68
+ plugins: [plugin],
69
+ })
70
+ ```
71
+
72
+ Split mode is two editors, `side: 'old'` and `side: 'new'`, laid out and scroll-synced by the host.
73
+ **Give both plugins the same region store**, or expanding a collapsed region on one side leaves the
74
+ other where it was and every row below it misaligns:
75
+
76
+ ```ts
77
+ const regions = createDiffRegionStore()
78
+ const left = createDiffPlugin({ mode: 'document', side: 'old', regions })
79
+ const right = createDiffPlugin({ mode: 'document', side: 'new', regions })
80
+ ```
81
+
82
+ This is not the mirroring the design forbids — there is one store, and both sides read it, rather
83
+ than two sets kept in step. The panes stay aligned only while word wrap is off and no fold map is
84
+ set.
85
+
86
+ A host that needs the diff row under a pointer calls `diffRowAtEvent(event)`. It answers with the
87
+ pane's side, its rows and the index into them, resolved through the editor's own row geometry,
88
+ so a host never reads `data-editor-virtual-row` or a pane class to find out where a press landed.
89
+
90
+ Expansion is per _diff_, not per path: pushing the same path with different content resets it,
91
+ because region keys are absolute line numbers and any edit above a region renumbers it. Pushing an
92
+ identical file again keeps it.
93
+
94
+ ## Theme
95
+
96
+ Pass an `EditorTheme` to each diff editor. Base colors use the same fields as a plain editor:
97
+
98
+ | Removed CSS hook | EditorTheme field |
99
+ | --------------------------------- | ----------------------- |
100
+ | `--editor-diff-background` | `backgroundColor` |
101
+ | `--editor-diff-foreground` | `foregroundColor` |
102
+ | `--editor-diff-gutter-background` | `gutterBackgroundColor` |
103
+
104
+ The removed hooks have no aliases. Set these fields on the diff editor itself. Its rows and gutter
105
+ inherit the values, and `editor.setTheme(theme)` updates them without remounting.
106
+
107
+ ```ts
108
+ editor.setTheme({
109
+ type: 'light',
110
+ backgroundColor: '#ffffff',
111
+ foregroundColor: '#18181b',
112
+ gutterBackgroundColor: '#f4f4f5',
113
+ colors: { 'diff.added.bg': '#dcfce7' },
114
+ })
115
+ ```
116
+
117
+ `createDiffPlugin` registers the following `EditorTheme.colors` ids. Defaults follow the theme's
118
+ `type`; explicit colors override those defaults. A light theme should also supply its base colors.
119
+
120
+ | Color id | Paint |
121
+ | -------------------------------------- | ----------------------------------------------- |
122
+ | `diff.added`, `diff.deleted` | Added and deleted gutter numbers and indicators |
123
+ | `diff.modified` | Input to the default hunk background mix |
124
+ | `diff.added.bg`, `diff.deleted.bg` | Changed rows and their gutter rows |
125
+ | `diff.hunk.bg`, `diff.hunk.foreground` | Hunk separators and their gutter rows |
126
+ | `diff.placeholder.bg` | Empty cells on one side of a split diff |
127
+ | `diff.muted` | Empty diff message |
128
+ | `diff.border`, `diff.split.handle` | Host diff borders and split handles |
129
+
130
+ ## Stacked rows
131
+
132
+ `plugin.getStackedRows()` returns both sides in row order, including the plugin's current region
133
+ expansions. For a stacked plugin it returns the same readonly array as `getRows()`. For a split
134
+ plugin the result is cached until its file or expansion state changes. Private region stores keep
135
+ views independent; plugins given the same store observe each other's toggles. Overlay mode returns
136
+ its live projection rows, as `getRows()` does.
137
+
138
+ ## Modes
139
+
140
+ - **`document`** (default) — the editor holds a synthetic buffer of the projected rows. Deletion rows
141
+ are real document lines, so they highlight, select and copy like any other text. This is the
142
+ parity path.
143
+ - **`overlay`** — the editor holds the host's live, editable buffer and deletions arrive as injected
144
+ rows. Injected rows have no offset space (`startOffset === endOffset`), which means no selection,
145
+ no copy, no inline word-diff, and syntax colouring borrowed from whatever document text sits at
146
+ the anchor offset. Non-parity by construction — see `test/overlayModeLimits.test.ts`. Use it for a
147
+ live dirty-diff against an editable document, not for a diff view.
148
+
149
+ ## Exports
150
+
151
+ - `createDiffPlugin` — the one plugin factory, carrying both modes. `mode` is required.
152
+ - `createDiffEditorOptions` — the editor options a document-mode diff needs.
153
+ - `createDiffRegionStore` — shared expansion state for the two sides of a split view.
154
+ - `parseGitPatch` and `createTextDiff` build diff models.
155
+ - `createSplitProjection`, `createStackedProjection`, and `createLiveDiffProjection` expose render
156
+ projections.
157
+ - `joinRenderLines` turns projected rows into the buffer text the host pushes in.
158
+ - `projectDiffSyntaxTokens` maps full-file token streams onto projected rows.
@@ -0,0 +1,37 @@
1
+ import type { EditorGutterContribution, EditorGutterRowContext } from '@singapore-editor/core/extensions';
2
+ import { type DiffGutterDigits, type DiffGutterLayout, type DiffGutterSide } from './gutters';
3
+ import type { DiffRenderRow } from './types';
4
+ export type DiffGutterOptions = {
5
+ readonly side: DiffGutterSide;
6
+ /**
7
+ * Where a changed row's gutter tint is painted. `row`: this gutter is the pane's whole gutter
8
+ * (document mode), so the row decoration tints the gutter row, leading inset included, and the
9
+ * cell stays clear. `cell`: the gutter is shared with the host's lanes (overlay mode), so only
10
+ * this cell is tinted.
11
+ */
12
+ readonly tint: 'cell' | 'row';
13
+ /**
14
+ * The widest line number each lane must fit, computed once when the projection is built. Passing
15
+ * rows here instead would make `width()` re-scan every projected row on every width
16
+ * invalidation — which in overlay mode is every line of the file.
17
+ */
18
+ readonly getDigits: () => DiffGutterDigits;
19
+ /**
20
+ * Which diff row a rendered gutter row is showing. The two modes answer this differently and the
21
+ * gutter must not guess: in `document` mode the projection array index *is* the buffer row (§C4),
22
+ * but in `overlay` mode the array interleaves injected deletion rows, so position and buffer row
23
+ * diverge after the first deletion and the lookup has to go through `rowsByBufferRow`.
24
+ *
25
+ * Indexing a projection array positionally here is what broke overlay numbering once already.
26
+ */
27
+ readonly resolveRow: (row: EditorGutterRowContext) => DiffRenderRow | null;
28
+ readonly isEnabled?: () => boolean;
29
+ /**
30
+ * Called whenever the lane geometry is recomputed, so the host can publish it as CSS custom
31
+ * properties. The canvas gutter derived lane widths arithmetically; a DOM gutter has to hand the
32
+ * same numbers to CSS or the columns drift (§3.3, trap 1).
33
+ */
34
+ readonly onLayout?: (layout: DiffGutterLayout) => void;
35
+ };
36
+ export declare function createDiffGutterContribution(options: DiffGutterOptions): EditorGutterContribution;
37
+ //# sourceMappingURL=diffGutter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"diffGutter.d.ts","sourceRoot":"","sources":["../src/diffGutter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,wBAAwB,EACxB,sBAAsB,EACvB,MAAM,mCAAmC,CAAA;AAC1C,OAAO,EAML,KAAK,gBAAgB,EAErB,KAAK,gBAAgB,EACrB,KAAK,cAAc,EACpB,MAAM,WAAW,CAAA;AAClB,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,SAAS,CAAA;AAE5C,MAAM,MAAM,iBAAiB,GAAG;IAC9B,QAAQ,CAAC,IAAI,EAAE,cAAc,CAAA;IAC7B;;;;;OAKG;IACH,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,KAAK,CAAA;IAC7B;;;;OAIG;IACH,QAAQ,CAAC,SAAS,EAAE,MAAM,gBAAgB,CAAA;IAC1C;;;;;;;OAOG;IACH,QAAQ,CAAC,UAAU,EAAE,CAAC,GAAG,EAAE,sBAAsB,KAAK,aAAa,GAAG,IAAI,CAAA;IAC1E,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,OAAO,CAAA;IAClC;;;;OAIG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,gBAAgB,KAAK,IAAI,CAAA;CACvD,CAAA;AAoBD,wBAAgB,4BAA4B,CAAC,OAAO,EAAE,iBAAiB,GAAG,wBAAwB,CAgDjG"}
@@ -0,0 +1,189 @@
1
+ import { diffGutterIndicatorText, diffGutterLaneTone, diffGutterLayout, diffGutterNumberText, diffGutterRowTone } from "./gutters.js";
2
+ //#region src/diffGutter.ts
3
+ /**
4
+ * Lane elements are resolved once, when the cell is created, and never looked up again.
5
+ *
6
+ * The previous live-diff gutter ran three `querySelector` calls per row per update
7
+ * (editorDiffPlugin.ts:246-255 before this change) — ~132 lookups per repaint at 44 mounted rows,
8
+ * on the exact path the Aug-2026 scroll work fixed. Cells are created once and recycled by the
9
+ * core (virtualizedTextViewRows.ts:308-330), so caching here costs one WeakMap entry per mounted
10
+ * row and removes the lookups entirely (§3.3, trap 3).
11
+ */
12
+ var lanesByCell = /* @__PURE__ */ new WeakMap();
13
+ var NUMBER_LANES = ["old", "new"];
14
+ function createDiffGutterContribution(options) {
15
+ const laneKinds = gutterLaneKinds(options.side);
16
+ let lastLayoutKey = null;
17
+ let laneWidths = [];
18
+ return {
19
+ id: `editor-diff-gutter-${options.side}`,
20
+ className: "editor-diff-gutter-cell",
21
+ snapshotRenderer: {
22
+ key: `diff-gutter-v1:${options.side}`,
23
+ capture: (cell) => captureGutter(cell, laneKinds, laneWidths),
24
+ restore: (cell, paint) => restoreGutter(cell, paint, laneKinds)
25
+ },
26
+ createCell(document) {
27
+ const cell = createDiffGutterCell(document, laneKinds);
28
+ if (options.tint === "cell") cell.classList.add("editor-diff-gutter-tinted");
29
+ return cell;
30
+ },
31
+ width(context) {
32
+ if (options.isEnabled && !options.isEnabled()) return 0;
33
+ const layout = diffGutterLayout(options.side, options.getDigits(), context.lineCount, context.metrics.characterWidth);
34
+ laneWidths = layout.lanes.map((lane) => lane.width);
35
+ const layoutKey = gutterLayoutKey(layout);
36
+ if (layoutKey !== lastLayoutKey) {
37
+ lastLayoutKey = layoutKey;
38
+ options.onLayout?.(layout);
39
+ }
40
+ return layout.width;
41
+ },
42
+ updateCell(element, row) {
43
+ updateDiffGutterCell(element, row, options);
44
+ }
45
+ };
46
+ }
47
+ function gutterLaneKinds(side) {
48
+ if (side === "stacked") return [...NUMBER_LANES, "indicator"];
49
+ return [side, "indicator"];
50
+ }
51
+ /** Everything `onLayout` publishes, so the memo cannot miss a change the payload carries. */
52
+ function gutterLayoutKey(layout) {
53
+ return `${layout.width}:${layout.lanes.map((lane) => `${lane.kind}=${lane.width}`).join(",")}`;
54
+ }
55
+ function createDiffGutterCell(document, laneKinds) {
56
+ const cell = document.createElement("span");
57
+ cell.className = "editor-diff-gutter";
58
+ cell.setAttribute("aria-hidden", "true");
59
+ const lanes = /* @__PURE__ */ new Map();
60
+ for (const kind of laneKinds) {
61
+ const lane = document.createElement("span");
62
+ lane.className = `editor-diff-gutter-lane editor-diff-gutter-lane-${kind}`;
63
+ cell.appendChild(lane);
64
+ lanes.set(kind, lane);
65
+ }
66
+ lanesByCell.set(cell, {
67
+ cell,
68
+ lanes
69
+ });
70
+ return cell;
71
+ }
72
+ function updateDiffGutterCell(element, row, options) {
73
+ const cached = lanesByCell.get(element);
74
+ if (!cached) return;
75
+ element.style.removeProperty("grid-template-columns");
76
+ const diffRow = diffRowForGutterRow(row, options);
77
+ element.hidden = !diffRow;
78
+ if (!diffRow) {
79
+ for (const lane of cached.lanes.values()) clearLane(lane);
80
+ if (element.dataset.diffRowType !== void 0) delete element.dataset.diffRowType;
81
+ if (element.dataset.diffRowTone !== void 0) delete element.dataset.diffRowTone;
82
+ return;
83
+ }
84
+ for (const [kind, lane] of cached.lanes) {
85
+ setLaneText(lane, laneText(diffRow, kind));
86
+ setLaneTone(lane, diffGutterLaneTone(diffRow, kind));
87
+ }
88
+ setDataset(element, "diffRowType", diffRow.type);
89
+ setDataset(element, "diffRowTone", diffGutterRowTone(diffRow, options.side));
90
+ }
91
+ function diffRowForGutterRow(row, options) {
92
+ if (options.isEnabled && !options.isEnabled()) return null;
93
+ return options.resolveRow(row);
94
+ }
95
+ function laneText(row, kind) {
96
+ if (kind === "indicator") return diffGutterIndicatorText(row);
97
+ return diffGutterNumberText(row, kind);
98
+ }
99
+ function clearLane(lane) {
100
+ setLaneText(lane, "");
101
+ setLaneTone(lane, "default");
102
+ }
103
+ function setLaneText(lane, text) {
104
+ if (lane.textContent === text) return;
105
+ lane.textContent = text;
106
+ }
107
+ function setLaneTone(lane, tone) {
108
+ setDataset(lane, "diffTone", tone);
109
+ }
110
+ function setDataset(element, key, value) {
111
+ if (element.dataset[key] === value) return;
112
+ element.dataset[key] = value;
113
+ }
114
+ function captureGutter(cell, kinds, widths) {
115
+ const cached = lanesByCell.get(cell);
116
+ if (!cached || widths.length !== kinds.length) return null;
117
+ const lanes = kinds.map((kind, index) => {
118
+ const lane = cached.lanes.get(kind);
119
+ return {
120
+ text: lane?.textContent ?? "",
121
+ tone: lane?.dataset.diffTone ?? "default",
122
+ width: widths[index]
123
+ };
124
+ });
125
+ return JSON.stringify({
126
+ hidden: cell.hidden,
127
+ type: cell.dataset.diffRowType ?? "",
128
+ tone: cell.dataset.diffRowTone ?? "default",
129
+ lanes
130
+ });
131
+ }
132
+ function restoreGutter(cell, paint, kinds) {
133
+ if (paint.length > 4096) return false;
134
+ let value;
135
+ try {
136
+ value = JSON.parse(paint);
137
+ } catch {
138
+ return false;
139
+ }
140
+ if (!validGutterPaint(value, kinds.length)) return false;
141
+ const cached = lanesByCell.get(cell);
142
+ if (!cached) return false;
143
+ cell.hidden = value.hidden;
144
+ setDataset(cell, "diffRowType", value.type);
145
+ setDataset(cell, "diffRowTone", value.tone);
146
+ cell.style.gridTemplateColumns = value.lanes.map((lane) => `${lane.width}px`).join(" ");
147
+ for (let index = 0; index < kinds.length; index += 1) {
148
+ const lane = cached.lanes.get(kinds[index]);
149
+ const saved = value.lanes[index];
150
+ if (!lane || !saved) return false;
151
+ setLaneText(lane, saved.text);
152
+ setLaneTone(lane, saved.tone);
153
+ }
154
+ return true;
155
+ }
156
+ function validGutterPaint(value, length) {
157
+ if (!value || typeof value !== "object") return false;
158
+ if (!("hidden" in value) || typeof value.hidden !== "boolean") return false;
159
+ if (!("type" in value) || typeof value.type !== "string" || ![
160
+ "",
161
+ "context",
162
+ "addition",
163
+ "deletion",
164
+ "placeholder",
165
+ "hunk",
166
+ "empty"
167
+ ].includes(value.type)) return false;
168
+ if (!("tone" in value) || !validTone(value.tone)) return false;
169
+ if (!("lanes" in value) || !Array.isArray(value.lanes) || value.lanes.length !== length) return false;
170
+ return value.lanes.every(validLane);
171
+ }
172
+ function validTone(value) {
173
+ return typeof value === "string" && [
174
+ "default",
175
+ "added",
176
+ "deleted",
177
+ "hunk"
178
+ ].includes(value);
179
+ }
180
+ function validLane(value) {
181
+ if (!value || typeof value !== "object") return false;
182
+ if (!("text" in value) || typeof value.text !== "string" || value.text.length > 24) return false;
183
+ if (!("tone" in value) || !validTone(value.tone)) return false;
184
+ return "width" in value && typeof value.width === "number" && Number.isFinite(value.width) && value.width >= 0 && value.width <= 1e4;
185
+ }
186
+ //#endregion
187
+ export { createDiffGutterContribution };
188
+
189
+ //# sourceMappingURL=diffGutter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"diffGutter.js","names":[],"sources":["../src/diffGutter.ts"],"sourcesContent":["import type {\n EditorGutterContribution,\n EditorGutterRowContext,\n} from '@singapore-editor/core/extensions'\nimport {\n diffGutterIndicatorText,\n diffGutterLaneTone,\n diffGutterLayout,\n diffGutterNumberText,\n diffGutterRowTone,\n type DiffGutterDigits,\n type DiffGutterLaneKind,\n type DiffGutterLayout,\n type DiffGutterSide,\n} from './gutters'\nimport type { DiffRenderRow } from './types'\n\nexport type DiffGutterOptions = {\n readonly side: DiffGutterSide\n /**\n * Where a changed row's gutter tint is painted. `row`: this gutter is the pane's whole gutter\n * (document mode), so the row decoration tints the gutter row, leading inset included, and the\n * cell stays clear. `cell`: the gutter is shared with the host's lanes (overlay mode), so only\n * this cell is tinted.\n */\n readonly tint: 'cell' | 'row'\n /**\n * The widest line number each lane must fit, computed once when the projection is built. Passing\n * rows here instead would make `width()` re-scan every projected row on every width\n * invalidation — which in overlay mode is every line of the file.\n */\n readonly getDigits: () => DiffGutterDigits\n /**\n * Which diff row a rendered gutter row is showing. The two modes answer this differently and the\n * gutter must not guess: in `document` mode the projection array index *is* the buffer row (§C4),\n * but in `overlay` mode the array interleaves injected deletion rows, so position and buffer row\n * diverge after the first deletion and the lookup has to go through `rowsByBufferRow`.\n *\n * Indexing a projection array positionally here is what broke overlay numbering once already.\n */\n readonly resolveRow: (row: EditorGutterRowContext) => DiffRenderRow | null\n readonly isEnabled?: () => boolean\n /**\n * Called whenever the lane geometry is recomputed, so the host can publish it as CSS custom\n * properties. The canvas gutter derived lane widths arithmetically; a DOM gutter has to hand the\n * same numbers to CSS or the columns drift (§3.3, trap 1).\n */\n readonly onLayout?: (layout: DiffGutterLayout) => void\n}\n\ntype DiffGutterLanes = {\n readonly cell: HTMLElement\n readonly lanes: ReadonlyMap<DiffGutterLaneKind, HTMLElement>\n}\n\n/**\n * Lane elements are resolved once, when the cell is created, and never looked up again.\n *\n * The previous live-diff gutter ran three `querySelector` calls per row per update\n * (editorDiffPlugin.ts:246-255 before this change) — ~132 lookups per repaint at 44 mounted rows,\n * on the exact path the Aug-2026 scroll work fixed. Cells are created once and recycled by the\n * core (virtualizedTextViewRows.ts:308-330), so caching here costs one WeakMap entry per mounted\n * row and removes the lookups entirely (§3.3, trap 3).\n */\nconst lanesByCell = new WeakMap<HTMLElement, DiffGutterLanes>()\n\nconst NUMBER_LANES: readonly DiffGutterLaneKind[] = ['old', 'new']\n\nexport function createDiffGutterContribution(options: DiffGutterOptions): EditorGutterContribution {\n const laneKinds = gutterLaneKinds(options.side)\n let lastLayoutKey: string | null = null\n let laneWidths: readonly number[] = []\n\n return {\n id: `editor-diff-gutter-${options.side}`,\n className: 'editor-diff-gutter-cell',\n snapshotRenderer: {\n key: `diff-gutter-v1:${options.side}`,\n capture: (cell) => captureGutter(cell, laneKinds, laneWidths),\n restore: (cell, paint) => restoreGutter(cell, paint, laneKinds),\n },\n createCell(document) {\n const cell = createDiffGutterCell(document, laneKinds)\n if (options.tint === 'cell') cell.classList.add('editor-diff-gutter-tinted')\n return cell\n },\n width(context) {\n if (options.isEnabled && !options.isEnabled()) return 0\n\n const layout = diffGutterLayout(\n options.side,\n options.getDigits(),\n context.lineCount,\n context.metrics.characterWidth,\n )\n // Only on an actual change: `width` runs inside the core's layout pass, and writing custom\n // properties unconditionally would dirty style for the whole subtree on every recompute.\n //\n // Keyed on the whole lane split, not on the total. The stacked total is\n // `ceil(old*cw + 6) + ceil(new*cw + 6) + 12`, which is symmetric in the two lane character\n // counts — so swapping to a file whose old/new digit widths are transposed leaves the total\n // identical while the split needs to change, and a total-keyed memo would silently keep\n // publishing the previous columns. Lanes are `overflow: hidden`, so the under-sized one\n // clips its leading digit.\n laneWidths = layout.lanes.map((lane) => lane.width)\n const layoutKey = gutterLayoutKey(layout)\n if (layoutKey !== lastLayoutKey) {\n lastLayoutKey = layoutKey\n options.onLayout?.(layout)\n }\n return layout.width\n },\n updateCell(element, row) {\n updateDiffGutterCell(element, row, options)\n },\n }\n}\n\nfunction gutterLaneKinds(side: DiffGutterSide): readonly DiffGutterLaneKind[] {\n if (side === 'stacked') return [...NUMBER_LANES, 'indicator']\n return [side, 'indicator']\n}\n\n/** Everything `onLayout` publishes, so the memo cannot miss a change the payload carries. */\nfunction gutterLayoutKey(layout: DiffGutterLayout): string {\n return `${layout.width}:${layout.lanes.map((lane) => `${lane.kind}=${lane.width}`).join(',')}`\n}\n\nfunction createDiffGutterCell(\n document: Document,\n laneKinds: readonly DiffGutterLaneKind[],\n): HTMLElement {\n const cell = document.createElement('span')\n cell.className = 'editor-diff-gutter'\n cell.setAttribute('aria-hidden', 'true')\n\n const lanes = new Map<DiffGutterLaneKind, HTMLElement>()\n for (const kind of laneKinds) {\n const lane = document.createElement('span')\n lane.className = `editor-diff-gutter-lane editor-diff-gutter-lane-${kind}`\n cell.appendChild(lane)\n lanes.set(kind, lane)\n }\n\n lanesByCell.set(cell, { cell, lanes })\n return cell\n}\n\nfunction updateDiffGutterCell(\n element: HTMLElement,\n row: EditorGutterRowContext,\n options: DiffGutterOptions,\n): void {\n const cached = lanesByCell.get(element)\n if (!cached) return\n element.style.removeProperty('grid-template-columns')\n\n const diffRow = diffRowForGutterRow(row, options)\n element.hidden = !diffRow\n if (!diffRow) {\n for (const lane of cached.lanes.values()) clearLane(lane)\n if (element.dataset.diffRowType !== undefined) delete element.dataset.diffRowType\n if (element.dataset.diffRowTone !== undefined) delete element.dataset.diffRowTone\n return\n }\n\n for (const [kind, lane] of cached.lanes) {\n setLaneText(lane, laneText(diffRow, kind))\n setLaneTone(lane, diffGutterLaneTone(diffRow, kind))\n }\n setDataset(element, 'diffRowType', diffRow.type)\n setDataset(element, 'diffRowTone', diffGutterRowTone(diffRow, options.side))\n}\n\nfunction diffRowForGutterRow(\n row: EditorGutterRowContext,\n options: DiffGutterOptions,\n): DiffRenderRow | null {\n if (options.isEnabled && !options.isEnabled()) return null\n return options.resolveRow(row)\n}\n\nfunction laneText(row: DiffRenderRow, kind: DiffGutterLaneKind): string {\n if (kind === 'indicator') return diffGutterIndicatorText(row)\n return diffGutterNumberText(row, kind)\n}\n\nfunction clearLane(lane: HTMLElement): void {\n setLaneText(lane, '')\n setLaneTone(lane, 'default')\n}\n\nfunction setLaneText(lane: HTMLElement, text: string): void {\n if (lane.textContent === text) return\n lane.textContent = text\n}\n\nfunction setLaneTone(lane: HTMLElement, tone: string): void {\n setDataset(lane, 'diffTone', tone)\n}\n\nfunction setDataset(element: HTMLElement, key: string, value: string): void {\n if (element.dataset[key] === value) return\n element.dataset[key] = value\n}\n\ntype GutterPaint = {\n hidden: boolean\n type: string\n tone: string\n lanes: { text: string; tone: string; width: number }[]\n}\n\nfunction captureGutter(\n cell: HTMLElement,\n kinds: readonly DiffGutterLaneKind[],\n widths: readonly number[],\n): string | null {\n const cached = lanesByCell.get(cell)\n if (!cached || widths.length !== kinds.length) return null\n const lanes = kinds.map((kind, index) => {\n const lane = cached.lanes.get(kind)\n return {\n text: lane?.textContent ?? '',\n tone: lane?.dataset.diffTone ?? 'default',\n width: widths[index]!,\n }\n })\n return JSON.stringify({\n hidden: cell.hidden,\n type: cell.dataset.diffRowType ?? '',\n tone: cell.dataset.diffRowTone ?? 'default',\n lanes,\n })\n}\n\nfunction restoreGutter(\n cell: HTMLElement,\n paint: string,\n kinds: readonly DiffGutterLaneKind[],\n): boolean {\n if (paint.length > 4096) return false\n let value: unknown\n try {\n value = JSON.parse(paint)\n } catch {\n return false\n }\n if (!validGutterPaint(value, kinds.length)) return false\n const cached = lanesByCell.get(cell)\n if (!cached) return false\n cell.hidden = value.hidden\n setDataset(cell, 'diffRowType', value.type)\n setDataset(cell, 'diffRowTone', value.tone)\n cell.style.gridTemplateColumns = value.lanes.map((lane) => `${lane.width}px`).join(' ')\n for (let index = 0; index < kinds.length; index += 1) {\n const lane = cached.lanes.get(kinds[index]!)\n const saved = value.lanes[index]\n if (!lane || !saved) return false\n setLaneText(lane, saved.text)\n setLaneTone(lane, saved.tone)\n }\n return true\n}\n\nfunction validGutterPaint(value: unknown, length: number): value is GutterPaint {\n if (!value || typeof value !== 'object') return false\n if (!('hidden' in value) || typeof value.hidden !== 'boolean') return false\n if (\n !('type' in value) ||\n typeof value.type !== 'string' ||\n !['', 'context', 'addition', 'deletion', 'placeholder', 'hunk', 'empty'].includes(value.type)\n )\n return false\n if (!('tone' in value) || !validTone(value.tone)) return false\n if (!('lanes' in value) || !Array.isArray(value.lanes) || value.lanes.length !== length)\n return false\n return value.lanes.every(validLane)\n}\n\nfunction validTone(value: unknown): value is string {\n return typeof value === 'string' && ['default', 'added', 'deleted', 'hunk'].includes(value)\n}\n\nfunction validLane(value: unknown): boolean {\n if (!value || typeof value !== 'object') return false\n if (!('text' in value) || typeof value.text !== 'string' || value.text.length > 24) return false\n if (!('tone' in value) || !validTone(value.tone)) return false\n return (\n 'width' in value &&\n typeof value.width === 'number' &&\n Number.isFinite(value.width) &&\n value.width >= 0 &&\n value.width <= 10_000\n )\n}\n"],"mappings":"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"}
@@ -0,0 +1,5 @@
1
+ import type { VirtualizedTextHighlightRange, VirtualizedTextRowDecoration } from '@singapore-editor/core/rendering';
2
+ import type { DiffRenderRow } from './types';
3
+ export declare function diffRowDecorations(rows: readonly DiffRenderRow[]): ReadonlyMap<number, VirtualizedTextRowDecoration>;
4
+ export declare function diffInlineHighlightRanges(rows: readonly DiffRenderRow[]): readonly VirtualizedTextHighlightRange[];
5
+ //# sourceMappingURL=diffRows.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"diffRows.d.ts","sourceRoot":"","sources":["../src/diffRows.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,6BAA6B,EAC7B,4BAA4B,EAC7B,MAAM,kCAAkC,CAAA;AACzC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,SAAS,CAAA;AAE5C,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,SAAS,aAAa,EAAE,GAC7B,WAAW,CAAC,MAAM,EAAE,4BAA4B,CAAC,CAOnD;AAED,wBAAgB,yBAAyB,CACvC,IAAI,EAAE,SAAS,aAAa,EAAE,GAC7B,SAAS,6BAA6B,EAAE,CAU1C"}
@@ -0,0 +1,40 @@
1
+ //#region src/diffRows.ts
2
+ function diffRowDecorations(rows) {
3
+ const decorations = /* @__PURE__ */ new Map();
4
+ for (const [index, row] of rows.entries()) {
5
+ const decoration = decorationForRow(row);
6
+ if (decoration) decorations.set(index, decoration);
7
+ }
8
+ return decorations;
9
+ }
10
+ function diffInlineHighlightRanges(rows) {
11
+ const ranges = [];
12
+ let offset = 0;
13
+ for (const row of rows) {
14
+ appendInlineRanges(ranges, row, offset);
15
+ offset += row.text.length + 1;
16
+ }
17
+ return ranges;
18
+ }
19
+ function appendInlineRanges(ranges, row, rowOffset) {
20
+ for (const range of row.inlineRanges ?? []) {
21
+ if (range.end <= range.start) continue;
22
+ ranges.push({
23
+ start: rowOffset + range.start,
24
+ end: rowOffset + range.end
25
+ });
26
+ }
27
+ }
28
+ function decorationForRow(row) {
29
+ if (row.type === "context") return null;
30
+ const expandable = row.expandable ? " editor-diff-row-expandable" : "";
31
+ return {
32
+ snapshotStyle: "colors",
33
+ className: `editor-diff-row editor-diff-row-${row.type}${expandable}`,
34
+ gutterClassName: `editor-diff-gutter-row editor-diff-gutter-band-${row.type}`
35
+ };
36
+ }
37
+ //#endregion
38
+ export { diffInlineHighlightRanges, diffRowDecorations };
39
+
40
+ //# sourceMappingURL=diffRows.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"diffRows.js","names":[],"sources":["../src/diffRows.ts"],"sourcesContent":["import type {\n VirtualizedTextHighlightRange,\n VirtualizedTextRowDecoration,\n} from '@singapore-editor/core/rendering'\nimport type { DiffRenderRow } from './types'\n\nexport function diffRowDecorations(\n rows: readonly DiffRenderRow[],\n): ReadonlyMap<number, VirtualizedTextRowDecoration> {\n const decorations = new Map<number, VirtualizedTextRowDecoration>()\n for (const [index, row] of rows.entries()) {\n const decoration = decorationForRow(row)\n if (decoration) decorations.set(index, decoration)\n }\n return decorations\n}\n\nexport function diffInlineHighlightRanges(\n rows: readonly DiffRenderRow[],\n): readonly VirtualizedTextHighlightRange[] {\n const ranges: VirtualizedTextHighlightRange[] = []\n let offset = 0\n\n for (const row of rows) {\n appendInlineRanges(ranges, row, offset)\n offset += row.text.length + 1\n }\n\n return ranges\n}\n\nfunction appendInlineRanges(\n ranges: VirtualizedTextHighlightRange[],\n row: DiffRenderRow,\n rowOffset: number,\n): void {\n for (const range of row.inlineRanges ?? []) {\n if (range.end <= range.start) continue\n ranges.push({ start: rowOffset + range.start, end: rowOffset + range.end })\n }\n}\n\nfunction decorationForRow(row: DiffRenderRow): VirtualizedTextRowDecoration | null {\n if (row.type === 'context') return null\n\n const expandable = row.expandable ? ' editor-diff-row-expandable' : ''\n return {\n snapshotStyle: 'colors',\n className: `editor-diff-row editor-diff-row-${row.type}${expandable}`,\n // The whole gutter row, so the tint also covers the gutter's leading inset.\n gutterClassName: `editor-diff-gutter-row editor-diff-gutter-band-${row.type}`,\n }\n}\n"],"mappings":";AAMA,SAAgB,mBACd,MACmD;CACnD,MAAM,8BAAc,IAAI,IAA0C;CAClE,KAAK,MAAM,CAAC,OAAO,QAAQ,KAAK,QAAQ,GAAG;EACzC,MAAM,aAAa,iBAAiB,GAAG;EACvC,IAAI,YAAY,YAAY,IAAI,OAAO,UAAU;CACnD;CACA,OAAO;AACT;AAEA,SAAgB,0BACd,MAC0C;CAC1C,MAAM,SAA0C,CAAC;CACjD,IAAI,SAAS;CAEb,KAAK,MAAM,OAAO,MAAM;EACtB,mBAAmB,QAAQ,KAAK,MAAM;EACtC,UAAU,IAAI,KAAK,SAAS;CAC9B;CAEA,OAAO;AACT;AAEA,SAAS,mBACP,QACA,KACA,WACM;CACN,KAAK,MAAM,SAAS,IAAI,gBAAgB,CAAC,GAAG;EAC1C,IAAI,MAAM,OAAO,MAAM,OAAO;EAC9B,OAAO,KAAK;GAAE,OAAO,YAAY,MAAM;GAAO,KAAK,YAAY,MAAM;EAAI,CAAC;CAC5E;AACF;AAEA,SAAS,iBAAiB,KAAyD;CACjF,IAAI,IAAI,SAAS,WAAW,OAAO;CAEnC,MAAM,aAAa,IAAI,aAAa,gCAAgC;CACpE,OAAO;EACL,eAAe;EACf,WAAW,mCAAmC,IAAI,OAAO;EAEzD,iBAAiB,kDAAkD,IAAI;CACzE;AACF"}
@@ -0,0 +1,124 @@
1
+ import type { EditorHighlighterSession } from '@singapore-editor/core/extensions';
2
+ import { type EditorSyntaxResult, type EditorToken, type EditorTokenInput, type EditorTokenStore } from '@singapore-editor/core/syntax';
3
+ import type { DiffFile, DiffRenderRow, DiffSyntaxBackend } from './types';
4
+ type DiffSyntaxSide = 'old' | 'new' | 'stacked';
5
+ type DiffSyntaxSourceSide = 'old' | 'new';
6
+ export type DiffSyntaxTokenSource = {
7
+ readonly lineStarts: readonly number[];
8
+ readonly side: DiffSyntaxSourceSide;
9
+ readonly tokens: EditorTokenInput;
10
+ };
11
+ export type ProjectDiffSyntaxTokensOptions = {
12
+ readonly rows: readonly DiffRenderRow[];
13
+ readonly side: DiffSyntaxSide;
14
+ readonly sources: readonly DiffSyntaxTokenSource[];
15
+ };
16
+ export type DiffSyntaxControllerOptions = {
17
+ readonly side: DiffSyntaxSide;
18
+ readonly backend?: DiffSyntaxBackend;
19
+ readonly enabled?: boolean;
20
+ /** Fires when a *newly parsed* token stream lands; row-driven re-projection is synchronous. */
21
+ readonly onDidChangeTokens: () => void;
22
+ };
23
+ /** Streams from `prepareDiffSyntax`, or a preparation of them still running. */
24
+ export type PreparedDiffSyntaxInput = readonly PreparedDiffSyntaxSource[] | Promise<readonly PreparedDiffSyntaxSource[]>;
25
+ export type PrepareDiffSyntaxOptions = {
26
+ readonly backend?: DiffSyntaxBackend;
27
+ /** Which pane the sources are for. `stacked` prepares both source sides. */
28
+ readonly side?: DiffSyntaxSide;
29
+ readonly signal?: AbortSignal;
30
+ };
31
+ /**
32
+ * One source side's parsed token stream and the session that recolours it on a theme change.
33
+ * Exactly one holder owns it at a time — a host's store, or the controller that adopted it — and
34
+ * that holder disposes it.
35
+ */
36
+ export declare class PreparedDiffSyntaxSource {
37
+ readonly side: DiffSyntaxSourceSide;
38
+ readonly lineStarts: readonly number[];
39
+ private readonly listeners;
40
+ private readonly disposables;
41
+ private current;
42
+ private disposed;
43
+ constructor(side: DiffSyntaxSourceSide, lineStarts: readonly number[]);
44
+ get tokens(): EditorTokenStore;
45
+ /** Fires when a theme change recolours the stream. */
46
+ onDidChangeTokens(listener: () => void): () => void;
47
+ dispose(): void;
48
+ /** @internal */
49
+ own(disposable: {
50
+ dispose(): void;
51
+ }): void;
52
+ /** @internal */
53
+ setTokens(tokens: EditorTokenInput): void;
54
+ /** @internal */
55
+ recolor(session: DiffSyntaxServiceSession): Promise<void>;
56
+ }
57
+ /**
58
+ * Parses a diff's sources ahead of the view that will show them, so `setFile(file, rows, prepared)`
59
+ * paints coloured on its first frame. The caller owns the result; an aborted preparation resolves
60
+ * empty.
61
+ */
62
+ export declare function prepareDiffSyntax(file: DiffFile, options?: PrepareDiffSyntaxOptions): Promise<readonly PreparedDiffSyntaxSource[]>;
63
+ /**
64
+ * The syntax half of the old `DiffView`, with one behavioural change.
65
+ *
66
+ * `DiffView` re-ran the whole pipeline — new sessions, new parse — on every row change, including
67
+ * every expansion toggle (`updatePaneRows` → `refreshSyntaxHighlighting`, DiffView.ts:564). The
68
+ * parse result does not depend on which regions are expanded: the sources are the two *full files*,
69
+ * and expansion only changes which of their lines the projection shows. So the per-side token
70
+ * streams are cached against the file and re-projected synchronously when rows change, which is
71
+ * what lets a toggle repaint without an uncoloured frame (§C10).
72
+ */
73
+ export declare class DiffSyntaxController {
74
+ private readonly options;
75
+ private readonly scheduler;
76
+ private readonly key;
77
+ private sources;
78
+ private sourceSubscriptions;
79
+ private indexed;
80
+ private sourcesFile;
81
+ private file;
82
+ private rows;
83
+ private tokens;
84
+ private disposed;
85
+ private ready;
86
+ constructor(options: DiffSyntaxControllerOptions);
87
+ /** A disposed controller is terminal — its scheduler never runs another task. */
88
+ isDisposed(): boolean;
89
+ isReady(): boolean;
90
+ getTokens(): readonly EditorToken[];
91
+ /**
92
+ * New file: drop the cached streams, then adopt `prepared` when it covers this side, or reparse.
93
+ * A preparation still running is awaited instead of parsing the same file twice. Takes ownership
94
+ * of `prepared` either way.
95
+ */
96
+ setFile(file: DiffFile | null, rows: readonly DiffRenderRow[], prepared?: PreparedDiffSyntaxInput): void;
97
+ /** Whether `prepared` was adopted; otherwise it is disposed, and a current file reparses. */
98
+ private settlePrepared;
99
+ /** Same file, different rows — an expansion toggle. Re-project what is already parsed. */
100
+ setRows(rows: readonly DiffRenderRow[]): void;
101
+ /**
102
+ * Hands the parsed streams of the current file to the caller, which then owns them; empty while
103
+ * a parse is still running. The controller keeps projecting their current tokens, and stops
104
+ * following their recolouring.
105
+ */
106
+ release(): readonly PreparedDiffSyntaxSource[];
107
+ dispose(): void;
108
+ private reproject;
109
+ private load;
110
+ private applySources;
111
+ private adoptSources;
112
+ private recolor;
113
+ private unsubscribeSources;
114
+ private disposeSources;
115
+ }
116
+ export declare function diffSyntaxBackend(backend: DiffSyntaxBackend | undefined): DiffSyntaxBackend;
117
+ export declare function projectDiffSyntaxTokens({ rows, side, sources, }: ProjectDiffSyntaxTokensOptions): readonly EditorToken[];
118
+ type DiffSyntaxServiceSession = {
119
+ onDidChangeTheme?: EditorHighlighterSession['onDidChangeTheme'];
120
+ refresh(): Promise<EditorSyntaxResult>;
121
+ dispose(): void;
122
+ };
123
+ export {};
124
+ //# sourceMappingURL=diffSyntax.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"diffSyntax.d.ts","sourceRoot":"","sources":["../src/diffSyntax.ts"],"names":[],"mappings":"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"}