@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.
- package/README.md +158 -0
- package/dist/diffGutter.d.ts +37 -0
- package/dist/diffGutter.d.ts.map +1 -0
- package/dist/diffGutter.js +189 -0
- package/dist/diffGutter.js.map +1 -0
- package/dist/diffRows.d.ts +5 -0
- package/dist/diffRows.d.ts.map +1 -0
- package/dist/diffRows.js +40 -0
- package/dist/diffRows.js.map +1 -0
- package/dist/diffSyntax.d.ts +124 -0
- package/dist/diffSyntax.d.ts.map +1 -0
- package/dist/diffSyntax.js +491 -0
- package/dist/diffSyntax.js.map +1 -0
- package/dist/editorDiffPlugin.d.ts +74 -0
- package/dist/editorDiffPlugin.d.ts.map +1 -0
- package/dist/editorDiffPlugin.js +546 -0
- package/dist/editorDiffPlugin.js.map +1 -0
- package/dist/editorOptions.d.ts +15 -0
- package/dist/editorOptions.d.ts.map +1 -0
- package/dist/editorOptions.js +33 -0
- package/dist/editorOptions.js.map +1 -0
- package/dist/gutters.d.ts +53 -0
- package/dist/gutters.d.ts.map +1 -0
- package/dist/gutters.js +122 -0
- package/dist/gutters.js.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/inline.d.ts +3 -0
- package/dist/inline.d.ts.map +1 -0
- package/dist/inline.js +72 -0
- package/dist/inline.js.map +1 -0
- package/dist/lines.d.ts +8 -0
- package/dist/lines.d.ts.map +1 -0
- package/dist/lines.js +49 -0
- package/dist/lines.js.map +1 -0
- package/dist/liveProjection.d.ts +11 -0
- package/dist/liveProjection.d.ts.map +1 -0
- package/dist/liveProjection.js +152 -0
- package/dist/liveProjection.js.map +1 -0
- package/dist/model.d.ts +4 -0
- package/dist/model.d.ts.map +1 -0
- package/dist/model.js +247 -0
- package/dist/model.js.map +1 -0
- package/dist/projection.d.ts +18 -0
- package/dist/projection.d.ts.map +1 -0
- package/dist/projection.js +281 -0
- package/dist/projection.js.map +1 -0
- package/dist/regions.d.ts +28 -0
- package/dist/regions.d.ts.map +1 -0
- package/dist/regions.js +0 -0
- package/dist/regions.js.map +1 -0
- package/dist/style.css +145 -0
- package/dist/theme.d.ts +2 -0
- package/dist/theme.d.ts.map +1 -0
- package/dist/theme.js +43 -0
- package/dist/theme.js.map +1 -0
- package/dist/types.d.ts +78 -0
- package/dist/types.d.ts.map +1 -0
- package/package.json +44 -0
|
@@ -0,0 +1,546 @@
|
|
|
1
|
+
import { registerDiffColors } from "./theme.js";
|
|
2
|
+
import { diffGutterDigits } from "./gutters.js";
|
|
3
|
+
import { createDiffGutterContribution } from "./diffGutter.js";
|
|
4
|
+
import { diffInlineHighlightRanges, diffRowDecorations } from "./diffRows.js";
|
|
5
|
+
import { DiffSyntaxController } from "./diffSyntax.js";
|
|
6
|
+
import { createLiveDiffProjection } from "./liveProjection.js";
|
|
7
|
+
import { createTextDiff } from "./model.js";
|
|
8
|
+
import { createSplitProjection, createStackedProjection } from "./projection.js";
|
|
9
|
+
import { createDiffRegionStore } from "./regions.js";
|
|
10
|
+
import { EDITOR_SNIPPET_TOKENS_FEATURE } from "@singapore-editor/core/extensions";
|
|
11
|
+
//#region src/editorDiffPlugin.ts
|
|
12
|
+
var viewsByScrollElement = /* @__PURE__ */ new WeakMap();
|
|
13
|
+
/** `null` outside a `document`-mode diff pane, or over something that is not a diff row. */
|
|
14
|
+
function diffRowAtEvent(event) {
|
|
15
|
+
const target = event.target;
|
|
16
|
+
if (!(target instanceof Element)) return null;
|
|
17
|
+
const scrollElement = target.closest("[data-editor-diff-side]");
|
|
18
|
+
if (!scrollElement) return null;
|
|
19
|
+
return viewsByScrollElement.get(scrollElement)?.rowHitAt(event) ?? null;
|
|
20
|
+
}
|
|
21
|
+
var EMPTY_PROJECTION = {
|
|
22
|
+
injectedRows: [],
|
|
23
|
+
rowDecorations: /* @__PURE__ */ new Map(),
|
|
24
|
+
rows: [],
|
|
25
|
+
rowsByBufferRow: /* @__PURE__ */ new Map()
|
|
26
|
+
};
|
|
27
|
+
var ROW_DECORATION_SOURCE = "editor.diff";
|
|
28
|
+
var INLINE_HIGHLIGHT_STYLE = { backgroundColor: "rgba(255, 255, 255, 0.18)" };
|
|
29
|
+
var nextDiffPluginId = 0;
|
|
30
|
+
/**
|
|
31
|
+
* The one diff plugin (§C1), carrying two internal row-delivery modes.
|
|
32
|
+
*
|
|
33
|
+
* `document` — the editor holds a synthetic buffer of the projected rows and the plugin publishes
|
|
34
|
+
* them for the host to push in. Everything works because the rows are real document lines.
|
|
35
|
+
*
|
|
36
|
+
* `overlay` — the editor holds the host's live, editable buffer and deletions arrive as injected
|
|
37
|
+
* rows. Non-parity by construction; see §C2 and test/overlayModeLimits.test.ts before reaching for
|
|
38
|
+
* it.
|
|
39
|
+
*/
|
|
40
|
+
function createDiffPlugin(options) {
|
|
41
|
+
registerDiffColors();
|
|
42
|
+
const runtime = new DiffPluginRuntime(options);
|
|
43
|
+
return {
|
|
44
|
+
name: `editor-diff-${options.mode}`,
|
|
45
|
+
activate(context) {
|
|
46
|
+
return runtime.activate(context);
|
|
47
|
+
},
|
|
48
|
+
setFile: (file, prepared) => runtime.setFile(file, prepared),
|
|
49
|
+
releasePreparedSyntax: () => runtime.releasePreparedSyntax(),
|
|
50
|
+
getRows: () => runtime.getRows(),
|
|
51
|
+
getStackedRows: () => runtime.getStackedRows(),
|
|
52
|
+
getTokens: () => runtime.getTokens(),
|
|
53
|
+
isSyntaxReady: () => runtime.isSyntaxReady(),
|
|
54
|
+
onDidChangeRows: (listener) => runtime.onDidChangeRows(listener),
|
|
55
|
+
onDidChangeTokens: (listener) => runtime.onDidChangeTokens(listener),
|
|
56
|
+
getExpandedRegions: () => runtime.getExpandedRegions(),
|
|
57
|
+
toggleRegion: (key) => runtime.toggleRegion(key),
|
|
58
|
+
setBaseFile: (file) => runtime.setBaseFile(file),
|
|
59
|
+
setEnabled: (enabled) => runtime.setEnabled(enabled)
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
var DiffPluginRuntime = class {
|
|
63
|
+
options;
|
|
64
|
+
id = nextDiffPluginId++;
|
|
65
|
+
mode;
|
|
66
|
+
side;
|
|
67
|
+
syntax;
|
|
68
|
+
rowListeners = /* @__PURE__ */ new Set();
|
|
69
|
+
tokenListeners = /* @__PURE__ */ new Set();
|
|
70
|
+
regions;
|
|
71
|
+
file = null;
|
|
72
|
+
rows = [];
|
|
73
|
+
stackedRows = null;
|
|
74
|
+
digits = {
|
|
75
|
+
old: 0,
|
|
76
|
+
new: 0
|
|
77
|
+
};
|
|
78
|
+
hunkRows = false;
|
|
79
|
+
expandableRows = false;
|
|
80
|
+
baseFile = null;
|
|
81
|
+
enabled;
|
|
82
|
+
liveProjection = EMPTY_PROJECTION;
|
|
83
|
+
decoration = null;
|
|
84
|
+
view = null;
|
|
85
|
+
injectedRowsListener = null;
|
|
86
|
+
lastGutterLayout = null;
|
|
87
|
+
regionSubscription;
|
|
88
|
+
constructor(options) {
|
|
89
|
+
this.options = options;
|
|
90
|
+
this.mode = options.mode;
|
|
91
|
+
this.side = options.side ?? "stacked";
|
|
92
|
+
this.regions = options.regions ?? createDiffRegionStore();
|
|
93
|
+
this.regionSubscription = this.regions.onDidChange(() => this.handleRegionsChanged());
|
|
94
|
+
this.enabled = this.mode === "document";
|
|
95
|
+
this.syntax = this.createSyntaxController();
|
|
96
|
+
}
|
|
97
|
+
activate(context) {
|
|
98
|
+
if (this.syntax.isDisposed()) {
|
|
99
|
+
this.syntax = this.createSyntaxController();
|
|
100
|
+
if (this.file) this.syntax.setFile(this.file, this.rows);
|
|
101
|
+
}
|
|
102
|
+
const disposables = [
|
|
103
|
+
context.registerGutterContribution(createDiffGutterContribution({
|
|
104
|
+
side: this.side,
|
|
105
|
+
tint: this.mode === "document" ? "row" : "cell",
|
|
106
|
+
getDigits: () => this.gutterDigits(),
|
|
107
|
+
resolveRow: (row) => this.resolveGutterRow(row),
|
|
108
|
+
isEnabled: () => this.enabled,
|
|
109
|
+
onLayout: (layout) => this.publishGutterLayout(layout)
|
|
110
|
+
})),
|
|
111
|
+
context.registerDecorationContribution({ createContribution: (featureContext) => this.createDecorationContribution(featureContext) }),
|
|
112
|
+
context.registerViewContribution({ createContribution: (viewContext) => this.createViewContribution(viewContext) }),
|
|
113
|
+
{ dispose: () => this.syntax.dispose() }
|
|
114
|
+
];
|
|
115
|
+
if (this.mode === "overlay") disposables.push(context.registerInjectedTextRowProvider({
|
|
116
|
+
getInjectedTextRows: () => this.injectedRows(),
|
|
117
|
+
onDidChangeInjectedTextRows: (listener) => this.setInjectedRowsListener(listener)
|
|
118
|
+
}));
|
|
119
|
+
return disposables;
|
|
120
|
+
}
|
|
121
|
+
setFile(file, prepared = []) {
|
|
122
|
+
if (this.mode !== "document") {
|
|
123
|
+
Promise.resolve(prepared).then((sources) => {
|
|
124
|
+
for (const source of sources) source.dispose();
|
|
125
|
+
}, () => void 0);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
this.file = file;
|
|
129
|
+
this.regions.setFile(file);
|
|
130
|
+
this.rebuildRows();
|
|
131
|
+
this.syntax.setFile(file, this.rows, prepared);
|
|
132
|
+
this.notifyRows();
|
|
133
|
+
}
|
|
134
|
+
releasePreparedSyntax() {
|
|
135
|
+
return this.syntax.release();
|
|
136
|
+
}
|
|
137
|
+
getRows() {
|
|
138
|
+
if (this.mode === "overlay") return this.liveProjection.rows;
|
|
139
|
+
return this.rows;
|
|
140
|
+
}
|
|
141
|
+
getStackedRows() {
|
|
142
|
+
if (this.mode === "overlay") return this.liveProjection.rows;
|
|
143
|
+
if (this.side === "stacked") return this.rows;
|
|
144
|
+
this.stackedRows ??= projectRows(this.file, "stacked", this.regions.getExpandedRegions());
|
|
145
|
+
return this.stackedRows;
|
|
146
|
+
}
|
|
147
|
+
isSyntaxReady() {
|
|
148
|
+
return this.syntax.isReady();
|
|
149
|
+
}
|
|
150
|
+
getTokens() {
|
|
151
|
+
return this.syntax.getTokens();
|
|
152
|
+
}
|
|
153
|
+
getExpandedRegions() {
|
|
154
|
+
return this.regions.getExpandedRegions();
|
|
155
|
+
}
|
|
156
|
+
toggleRegion(key) {
|
|
157
|
+
if (this.mode !== "document") return;
|
|
158
|
+
if (!this.file) return;
|
|
159
|
+
this.regions.toggleRegion(key);
|
|
160
|
+
}
|
|
161
|
+
handleRegionsChanged() {
|
|
162
|
+
if (this.mode !== "document") return;
|
|
163
|
+
if (!this.file) return;
|
|
164
|
+
this.rebuildRows();
|
|
165
|
+
this.syntax.setRows(this.rows);
|
|
166
|
+
this.notifyRows();
|
|
167
|
+
}
|
|
168
|
+
onDidChangeRows(listener) {
|
|
169
|
+
this.rowListeners.add(listener);
|
|
170
|
+
return { dispose: () => this.rowListeners.delete(listener) };
|
|
171
|
+
}
|
|
172
|
+
onDidChangeTokens(listener) {
|
|
173
|
+
this.tokenListeners.add(listener);
|
|
174
|
+
return { dispose: () => this.tokenListeners.delete(listener) };
|
|
175
|
+
}
|
|
176
|
+
rebuildRows() {
|
|
177
|
+
this.stackedRows = null;
|
|
178
|
+
this.rows = projectRows(this.file, this.side, this.regions.getExpandedRegions());
|
|
179
|
+
this.digits = diffGutterDigits(this.rows);
|
|
180
|
+
this.hunkRows = this.rows.some((row) => row.type === "hunk");
|
|
181
|
+
this.expandableRows = this.rows.some((row) => row.type === "hunk" && row.expandable === true);
|
|
182
|
+
}
|
|
183
|
+
notifyRows() {
|
|
184
|
+
for (const listener of this.rowListeners) listener();
|
|
185
|
+
this.decoration?.refresh();
|
|
186
|
+
this.view?.refreshRows();
|
|
187
|
+
}
|
|
188
|
+
notifyTokens() {
|
|
189
|
+
for (const listener of this.tokenListeners) listener();
|
|
190
|
+
}
|
|
191
|
+
setBaseFile(file) {
|
|
192
|
+
if (this.mode !== "overlay") return;
|
|
193
|
+
this.baseFile = file;
|
|
194
|
+
this.decoration?.refresh();
|
|
195
|
+
}
|
|
196
|
+
setEnabled(enabled) {
|
|
197
|
+
if (this.mode !== "overlay") return;
|
|
198
|
+
this.enabled = enabled;
|
|
199
|
+
this.decoration?.refresh();
|
|
200
|
+
}
|
|
201
|
+
injectedRows() {
|
|
202
|
+
if (!this.enabled) return [];
|
|
203
|
+
return this.liveProjection.injectedRows;
|
|
204
|
+
}
|
|
205
|
+
setInjectedRowsListener(listener) {
|
|
206
|
+
this.injectedRowsListener = listener;
|
|
207
|
+
return { dispose: () => {
|
|
208
|
+
if (this.injectedRowsListener === listener) this.injectedRowsListener = null;
|
|
209
|
+
} };
|
|
210
|
+
}
|
|
211
|
+
gutterDigits() {
|
|
212
|
+
if (this.mode === "overlay") return diffGutterDigits(this.liveProjection.rows);
|
|
213
|
+
return this.digits;
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Which diff row a rendered gutter row is showing. The two modes cannot share an answer.
|
|
217
|
+
*
|
|
218
|
+
* `document` — §C4 holds, so the projection index is the buffer row and a positional lookup is
|
|
219
|
+
* correct.
|
|
220
|
+
*
|
|
221
|
+
* `overlay` — the projection array interleaves injected deletion rows, so position and buffer
|
|
222
|
+
* row diverge from the first deletion onwards; the mapping the projection publishes for exactly
|
|
223
|
+
* this purpose is `rowsByBufferRow`, keyed `newLineNumber - 1`. An injected row is not in that
|
|
224
|
+
* map at all and carries its own row as metadata instead.
|
|
225
|
+
*/
|
|
226
|
+
resolveGutterRow(row) {
|
|
227
|
+
if (this.mode === "overlay") {
|
|
228
|
+
if (row.source === "injected") return injectedDiffRow(row);
|
|
229
|
+
if (row.source !== "document") return null;
|
|
230
|
+
return this.liveProjection.rowsByBufferRow.get(row.bufferRow) ?? null;
|
|
231
|
+
}
|
|
232
|
+
if (row.source !== "document") return null;
|
|
233
|
+
return this.rows[row.bufferRow] ?? null;
|
|
234
|
+
}
|
|
235
|
+
createDecorationContribution(context) {
|
|
236
|
+
const contribution = new DiffDecorationContribution(context, {
|
|
237
|
+
mode: this.mode,
|
|
238
|
+
getRows: () => this.rows,
|
|
239
|
+
buildLiveProjection: () => this.buildLiveProjection(context),
|
|
240
|
+
setLiveProjection: (projection) => {
|
|
241
|
+
this.liveProjection = projection;
|
|
242
|
+
},
|
|
243
|
+
notifyInjectedRowsChanged: () => this.injectedRowsListener?.()
|
|
244
|
+
});
|
|
245
|
+
this.decoration = contribution;
|
|
246
|
+
contribution.refresh();
|
|
247
|
+
return {
|
|
248
|
+
handleEditorChange: (change) => contribution.handleEditorChange(change),
|
|
249
|
+
dispose: () => {
|
|
250
|
+
if (this.decoration === contribution) this.decoration = null;
|
|
251
|
+
contribution.dispose();
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
buildLiveProjection(context) {
|
|
256
|
+
if (!this.enabled) return EMPTY_PROJECTION;
|
|
257
|
+
if (!this.baseFile) return EMPTY_PROJECTION;
|
|
258
|
+
if (!context.hasDocument()) return EMPTY_PROJECTION;
|
|
259
|
+
const file = createTextDiff({
|
|
260
|
+
oldFile: this.baseFile,
|
|
261
|
+
newFile: {
|
|
262
|
+
path: this.baseFile.path,
|
|
263
|
+
text: liveDiffDocumentText(context),
|
|
264
|
+
languageId: this.baseFile.languageId
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
return createLiveDiffProjection(file);
|
|
268
|
+
}
|
|
269
|
+
createViewContribution(context) {
|
|
270
|
+
const contribution = new DiffViewContribution(context, {
|
|
271
|
+
highlightName: `editor-diff-${this.id}-${this.side}-inline`,
|
|
272
|
+
mode: this.mode,
|
|
273
|
+
side: this.side,
|
|
274
|
+
getRows: () => this.getRows(),
|
|
275
|
+
hasHunkRows: () => this.hunkRows,
|
|
276
|
+
hasExpandableRows: () => this.expandableRows,
|
|
277
|
+
toggleRegion: (key) => this.toggleRegion(key),
|
|
278
|
+
detach: (disposed) => {
|
|
279
|
+
lentSyntax?.dispose();
|
|
280
|
+
if (this.view === disposed) this.view = null;
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
const lentSyntax = context.getFeature(EDITOR_SNIPPET_TOKENS_FEATURE)?.addSource(snippetTokenSource(this.options.syntaxBackend));
|
|
284
|
+
this.view = contribution;
|
|
285
|
+
if (this.lastGutterLayout) contribution.applyGutterLayout(this.lastGutterLayout);
|
|
286
|
+
return contribution;
|
|
287
|
+
}
|
|
288
|
+
publishGutterLayout(layout) {
|
|
289
|
+
this.lastGutterLayout = layout;
|
|
290
|
+
this.view?.applyGutterLayout(layout);
|
|
291
|
+
}
|
|
292
|
+
createSyntaxController() {
|
|
293
|
+
return new DiffSyntaxController({
|
|
294
|
+
side: this.side,
|
|
295
|
+
backend: this.options.syntaxBackend,
|
|
296
|
+
enabled: this.options.syntaxHighlight !== false,
|
|
297
|
+
onDidChangeTokens: () => this.notifyTokens()
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
};
|
|
301
|
+
function liveDiffDocumentText(context) {
|
|
302
|
+
return context.materializeFullText();
|
|
303
|
+
}
|
|
304
|
+
function projectRows(file, side, expandedRegions) {
|
|
305
|
+
if (!file) return [];
|
|
306
|
+
if (side === "stacked") return createStackedProjection(file, { expandedRegions }).rows;
|
|
307
|
+
const projection = createSplitProjection(file, { expandedRegions });
|
|
308
|
+
return side === "old" ? projection.leftRows : projection.rightRows;
|
|
309
|
+
}
|
|
310
|
+
var DiffDecorationContribution = class {
|
|
311
|
+
context;
|
|
312
|
+
options;
|
|
313
|
+
lastTextSnapshot = void 0;
|
|
314
|
+
constructor(context, options) {
|
|
315
|
+
this.context = context;
|
|
316
|
+
this.options = options;
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* The editor's notification, which is not the same thing as "the text changed".
|
|
320
|
+
*
|
|
321
|
+
* Rebuilding an overlay projection materializes the whole document and runs `structuredPatch`
|
|
322
|
+
* over it, synchronously. `handleEditorChange` also fires for selection-only changes —
|
|
323
|
+
* `EditorContributionChange.kind` is one of edit/selection/undo/redo/none — and a caret move cannot
|
|
324
|
+
* alter the diff, so paying for one is pure waste on the typing path. Text-snapshot identity is
|
|
325
|
+
* the second guard, for the notification paths that carry no change object at all.
|
|
326
|
+
*
|
|
327
|
+
* Plugin-state changes (`setBaseFile`, `setEnabled`) call `refresh()` directly and are never
|
|
328
|
+
* skipped: the text is the same but the diff it produces is not.
|
|
329
|
+
*/
|
|
330
|
+
handleEditorChange(change) {
|
|
331
|
+
if (this.options.mode === "overlay") {
|
|
332
|
+
if (change?.kind === "selection") return;
|
|
333
|
+
const snapshot = this.context.getTextSnapshot();
|
|
334
|
+
if (snapshot !== null && snapshot === this.lastTextSnapshot) return;
|
|
335
|
+
}
|
|
336
|
+
this.refresh();
|
|
337
|
+
}
|
|
338
|
+
refresh() {
|
|
339
|
+
this.lastTextSnapshot = this.context.getTextSnapshot();
|
|
340
|
+
if (this.options.mode === "document") {
|
|
341
|
+
this.context.setRowDecorations(ROW_DECORATION_SOURCE, diffRowDecorations(this.options.getRows()));
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
this.applyLiveProjection(this.options.buildLiveProjection());
|
|
345
|
+
}
|
|
346
|
+
dispose() {
|
|
347
|
+
if (this.options.mode === "overlay") this.applyLiveProjection(EMPTY_PROJECTION);
|
|
348
|
+
this.context.clearRowDecorations(ROW_DECORATION_SOURCE);
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* These three steps are ordered, and the order is load-bearing.
|
|
352
|
+
*
|
|
353
|
+
* Announcing the injected-row change first would rebuild the row set before the cells are
|
|
354
|
+
* repainted, and a row that the rebuild unmounts keeps whatever its cell last rendered — a
|
|
355
|
+
* disabled live diff would leave a stale `4+` behind on a detached cell. Repainting first, while
|
|
356
|
+
* that row is still mounted, is what clears it.
|
|
357
|
+
*
|
|
358
|
+
* The stored projection has to be current before the repaint, because the gutter reads its rows
|
|
359
|
+
* from it.
|
|
360
|
+
*/
|
|
361
|
+
applyLiveProjection(projection) {
|
|
362
|
+
this.options.setLiveProjection(projection);
|
|
363
|
+
this.context.setRowDecorations(ROW_DECORATION_SOURCE, projection.rowDecorations);
|
|
364
|
+
this.options.notifyInjectedRowsChanged();
|
|
365
|
+
}
|
|
366
|
+
};
|
|
367
|
+
/** Pointer handling and inline word-diff highlights. */
|
|
368
|
+
var DiffViewContribution = class {
|
|
369
|
+
context;
|
|
370
|
+
options;
|
|
371
|
+
snapshotKey = "diff-inline-v1";
|
|
372
|
+
pressParticipant;
|
|
373
|
+
separatorRows;
|
|
374
|
+
snapshot = null;
|
|
375
|
+
lastHighlightRows = null;
|
|
376
|
+
lastHighlightTextVersion = -1;
|
|
377
|
+
constructor(context, options) {
|
|
378
|
+
this.context = context;
|
|
379
|
+
this.options = options;
|
|
380
|
+
this.context.scrollElement.dataset.editorDiffSide = options.side;
|
|
381
|
+
viewsByScrollElement.set(this.context.scrollElement, this);
|
|
382
|
+
this.pressParticipant = this.context.registerPressParticipant(this.claimSeparatorPress);
|
|
383
|
+
this.separatorRows = this.context.registerNonCaretRows(this.isSeparatorRow);
|
|
384
|
+
this.context.scrollElement.addEventListener("click", this.handleClick);
|
|
385
|
+
this.context.scrollElement.addEventListener("mousemove", this.handleMouseMove);
|
|
386
|
+
this.context.scrollElement.addEventListener("mouseleave", this.handleMouseLeave);
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* The snapshot is kept for every update kind — the pointer hit-test reads it — but the two
|
|
390
|
+
* derived jobs below are not per-frame work.
|
|
391
|
+
*
|
|
392
|
+
* `update` is called for `'viewport'` too, which `Editor.handleViewportChange` fires on every
|
|
393
|
+
* scroll frame, and the highlights can only change when the rows or the text do.
|
|
394
|
+
*/
|
|
395
|
+
update(snapshot, kind) {
|
|
396
|
+
this.snapshot = snapshot;
|
|
397
|
+
if (this.options.mode !== "document") return;
|
|
398
|
+
if (kind === "viewport" || kind === "selection") return;
|
|
399
|
+
this.applyInlineHighlights();
|
|
400
|
+
}
|
|
401
|
+
captureVisiblePaint(snapshot) {
|
|
402
|
+
const viewport = this.context.scrollElement.getBoundingClientRect();
|
|
403
|
+
const rows = this.options.getRows();
|
|
404
|
+
const ranges = snapshot.visibleRows.flatMap((visible) => {
|
|
405
|
+
const row = rows[visible.bufferRow];
|
|
406
|
+
if (!row) return [];
|
|
407
|
+
const start = snapshot.lineStartsView.at(visible.bufferRow) ?? visible.startOffset;
|
|
408
|
+
return diffInlineHighlightRanges([row]).map((range) => ({
|
|
409
|
+
start: range.start + start,
|
|
410
|
+
end: range.end + start
|
|
411
|
+
}));
|
|
412
|
+
});
|
|
413
|
+
const rectangles = [];
|
|
414
|
+
for (const range of ranges) {
|
|
415
|
+
const rect = this.context.getRangeClientRect(range.start, range.end);
|
|
416
|
+
if (!rect || rect.width <= 0 || rect.height <= 0) continue;
|
|
417
|
+
rectangles.push({
|
|
418
|
+
left: rect.left - viewport.left + snapshot.viewport.scrollLeft,
|
|
419
|
+
top: rect.top - viewport.top + snapshot.viewport.scrollTop,
|
|
420
|
+
width: rect.width,
|
|
421
|
+
height: rect.height,
|
|
422
|
+
backgroundColor: INLINE_HIGHLIGHT_STYLE.backgroundColor
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
return {
|
|
426
|
+
id: `diff-inline-${this.options.side}`,
|
|
427
|
+
status: "ready",
|
|
428
|
+
rectangles
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
refreshRows() {
|
|
432
|
+
this.lastHighlightRows = null;
|
|
433
|
+
this.applyInlineHighlights();
|
|
434
|
+
}
|
|
435
|
+
applyGutterLayout(layout) {
|
|
436
|
+
const style = this.context.scrollElement.style;
|
|
437
|
+
const columns = layout.lanes.map((lane) => `${lane.width}px`).join(" ");
|
|
438
|
+
style.setProperty("--editor-diff-gutter-columns", columns);
|
|
439
|
+
style.setProperty("--editor-diff-gutter-total-width", `${layout.width}px`);
|
|
440
|
+
}
|
|
441
|
+
dispose() {
|
|
442
|
+
this.pressParticipant.dispose();
|
|
443
|
+
this.separatorRows.dispose();
|
|
444
|
+
this.context.scrollElement.removeEventListener("click", this.handleClick);
|
|
445
|
+
this.context.scrollElement.removeEventListener("mousemove", this.handleMouseMove);
|
|
446
|
+
this.context.scrollElement.removeEventListener("mouseleave", this.handleMouseLeave);
|
|
447
|
+
this.context.scrollElement.style.cursor = "";
|
|
448
|
+
if (viewsByScrollElement.get(this.context.scrollElement) === this) viewsByScrollElement.delete(this.context.scrollElement);
|
|
449
|
+
this.context.clearRangeHighlight(this.options.highlightName);
|
|
450
|
+
this.options.detach(this);
|
|
451
|
+
}
|
|
452
|
+
applyInlineHighlights() {
|
|
453
|
+
const rows = this.options.getRows();
|
|
454
|
+
const textVersion = this.snapshot?.textVersion ?? -1;
|
|
455
|
+
if (rows === this.lastHighlightRows && textVersion === this.lastHighlightTextVersion) return;
|
|
456
|
+
this.lastHighlightRows = rows;
|
|
457
|
+
this.lastHighlightTextVersion = textVersion;
|
|
458
|
+
try {
|
|
459
|
+
this.context.setRangeHighlight(this.options.highlightName, diffInlineHighlightRanges(rows), INLINE_HIGHLIGHT_STYLE);
|
|
460
|
+
} catch (error) {
|
|
461
|
+
this.context.log({
|
|
462
|
+
action: "editor.diff.inline_highlight_failed",
|
|
463
|
+
level: "warn",
|
|
464
|
+
error: { message: error instanceof Error ? error.message : String(error) }
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
469
|
+
* Keeps the caret off a separator row, expandable or not.
|
|
470
|
+
*
|
|
471
|
+
* A separator is chrome, not content — but its label is real buffer text, because the host's
|
|
472
|
+
* document is `joinRenderLines(rows)` and there is nowhere else for the label to live. So the
|
|
473
|
+
* editor will happily rest a caret in the middle of `Show 12 unmodified lines`, and a collapsed
|
|
474
|
+
* caret copies its own line: Cmd+C would put that sentence on the clipboard. The mousedown is
|
|
475
|
+
* refused here, and arrow keys step over the row (`isSeparatorRow`). A drag that *crosses* a separator still
|
|
476
|
+
* covers it, and always will while the label occupies offsets — giving these rows an offset
|
|
477
|
+
* space of their own is the parallel coordinate system §C2 rules out.
|
|
478
|
+
*
|
|
479
|
+
* Every click count, not just the first: a second press would select a word of the label, a
|
|
480
|
+
* third its line and a fourth the whole document.
|
|
481
|
+
*/
|
|
482
|
+
claimSeparatorPress = (event) => event.button === 0 && this.hunkRowAt(event) !== null;
|
|
483
|
+
/** The keyboard half of the same rule: arrow keys step over a separator. */
|
|
484
|
+
isSeparatorRow = (bufferRow) => {
|
|
485
|
+
if (this.options.mode !== "document" || !this.options.hasHunkRows()) return false;
|
|
486
|
+
return this.options.getRows()[bufferRow]?.type === "hunk";
|
|
487
|
+
};
|
|
488
|
+
handleClick = (event) => {
|
|
489
|
+
if (event.button !== 0) return;
|
|
490
|
+
const row = this.expandableRowAt(event);
|
|
491
|
+
if (!row?.expandKey) return;
|
|
492
|
+
event.preventDefault();
|
|
493
|
+
this.options.toggleRegion(row.expandKey);
|
|
494
|
+
};
|
|
495
|
+
handleMouseMove = (event) => {
|
|
496
|
+
const cursor = this.expandableRowAt(event) ? "pointer" : "";
|
|
497
|
+
if (this.context.scrollElement.style.cursor === cursor) return;
|
|
498
|
+
this.context.scrollElement.style.cursor = cursor;
|
|
499
|
+
};
|
|
500
|
+
handleMouseLeave = () => {
|
|
501
|
+
if (!this.context.scrollElement.style.cursor) return;
|
|
502
|
+
this.context.scrollElement.style.cursor = "";
|
|
503
|
+
};
|
|
504
|
+
expandableRowAt(event) {
|
|
505
|
+
if (!this.options.hasExpandableRows()) return null;
|
|
506
|
+
const row = this.hunkRowAt(event);
|
|
507
|
+
return row?.expandable ? row : null;
|
|
508
|
+
}
|
|
509
|
+
/**
|
|
510
|
+
* The separator under the pointer, whether or not it can be expanded. A partial diff — a plain
|
|
511
|
+
* patch with no file text behind it — has separators that say how much was skipped and cannot
|
|
512
|
+
* open them, and those are still not somewhere a caret belongs.
|
|
513
|
+
*/
|
|
514
|
+
hunkRowAt(event) {
|
|
515
|
+
if (!this.options.hasHunkRows()) return null;
|
|
516
|
+
const hit = this.rowHitAt(event);
|
|
517
|
+
const row = hit?.rows[hit.rowIndex];
|
|
518
|
+
return row?.type === "hunk" ? row : null;
|
|
519
|
+
}
|
|
520
|
+
/** Only `document` mode has an answer: there a buffer row is an index into `getRows()`. */
|
|
521
|
+
rowHitAt(event) {
|
|
522
|
+
if (this.options.mode !== "document") return null;
|
|
523
|
+
const hit = this.context.rowAtPoint(event.clientX, event.clientY);
|
|
524
|
+
if (hit?.source !== "document") return null;
|
|
525
|
+
const rowIndex = hit.bufferRow;
|
|
526
|
+
const rows = this.options.getRows();
|
|
527
|
+
return rows[rowIndex] ? {
|
|
528
|
+
side: this.options.side,
|
|
529
|
+
rowIndex,
|
|
530
|
+
rows
|
|
531
|
+
} : null;
|
|
532
|
+
}
|
|
533
|
+
};
|
|
534
|
+
function injectedDiffRow(row) {
|
|
535
|
+
const metadata = row.metadata;
|
|
536
|
+
if (!metadata || typeof metadata !== "object") return null;
|
|
537
|
+
return "type" in metadata && "text" in metadata ? metadata : null;
|
|
538
|
+
}
|
|
539
|
+
function snippetTokenSource(backend) {
|
|
540
|
+
if (backend?.kind === "highlighter") return { highlighter: backend.provider };
|
|
541
|
+
return { syntax: backend?.provider };
|
|
542
|
+
}
|
|
543
|
+
//#endregion
|
|
544
|
+
export { createDiffPlugin, diffRowAtEvent };
|
|
545
|
+
|
|
546
|
+
//# sourceMappingURL=editorDiffPlugin.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"editorDiffPlugin.js","names":[],"sources":["../src/editorDiffPlugin.ts"],"sourcesContent":["import { registerDiffColors } from './theme'\nimport type {\n EditorContributionChange,\n EditorDecorationContribution,\n EditorDecorationContributionContext,\n EditorDisposable,\n EditorGutterRowContext,\n EditorInjectedTextRow,\n EditorPlugin,\n EditorPluginContext,\n EditorSnippetTokenSource,\n EditorViewContribution,\n EditorViewContributionContext,\n EditorViewContributionUpdateKind,\n EditorViewSnapshot,\n} from '@singapore-editor/core/extensions'\nimport { EDITOR_SNIPPET_TOKENS_FEATURE } from '@singapore-editor/core/extensions'\nimport type { EditorToken } from '@singapore-editor/core/syntax'\nimport { createDiffGutterContribution } from './diffGutter'\nimport { diffInlineHighlightRanges, diffRowDecorations } from './diffRows'\nimport {\n DiffSyntaxController,\n type PreparedDiffSyntaxInput,\n type PreparedDiffSyntaxSource,\n} from './diffSyntax'\nimport {\n diffGutterDigits,\n type DiffGutterDigits,\n type DiffGutterLayout,\n type DiffGutterSide,\n} from './gutters'\nimport { createLiveDiffProjection, type LiveDiffProjection } from './liveProjection'\nimport { createTextDiff } from './model'\nimport { createSplitProjection, createStackedProjection } from './projection'\nimport { createDiffRegionStore, type DiffRegionStore } from './regions'\nimport type { DiffFile, DiffRenderRow, DiffSyntaxBackend, DiffTextFile } from './types'\n\nexport type DiffPluginMode = 'document' | 'overlay'\n\nexport type DiffPluginOptions = {\n /**\n * §C1. `document` is the parity path; `overlay` is explicitly non-parity — see §C2.\n *\n * Required, with no default. The factory this replaced took no arguments and was always overlay,\n * so the natural migration — rename the call, keep the empty argument list — would otherwise\n * produce a plugin that mounts, registers everything, renders nothing and reports no error,\n * because `setBaseFile` and `setEnabled` both early-return outside overlay mode.\n */\n readonly mode: DiffPluginMode\n readonly side?: DiffGutterSide\n /**\n * Expansion state, shared when a host drives two sides of one diff. Omit it and the plugin keeps\n * a private store; pass one store to both split plugins and a toggle on either side moves both.\n * See regions.ts — this is what holds §C7 together.\n */\n readonly regions?: DiffRegionStore\n readonly syntaxBackend?: DiffSyntaxBackend\n readonly syntaxHighlight?: boolean\n}\n\nexport type DiffPlugin = EditorPlugin & {\n /**\n * `document` mode: the file to project. The host owns the editor's text — §C3. `prepared`\n * streams from `prepareDiffSyntax` for this file paint with the first rows, and a preparation\n * still running is awaited in place of a second parse; the plugin owns them.\n */\n setFile(file: DiffFile | null, prepared?: PreparedDiffSyntaxInput): void\n /** The current file's parsed streams, handed to the caller; empty while a parse is running. */\n releasePreparedSyntax(): readonly PreparedDiffSyntaxSource[]\n getRows(): readonly DiffRenderRow[]\n /** Both sides in row order. Cached until the file or expansion state changes. */\n getStackedRows(): readonly DiffRenderRow[]\n /**\n * Projected syntax tokens for the current rows. The host passes them with the rows' text,\n * `setText(text, { tokens })`, or an expansion toggle repaints uncoloured (§C10).\n */\n isSyntaxReady(): boolean\n getTokens(): readonly EditorToken[]\n onDidChangeRows(listener: () => void): EditorDisposable\n onDidChangeTokens(listener: () => void): EditorDisposable\n /** §C5. Hosts must not mirror this. */\n getExpandedRegions(): ReadonlySet<string>\n toggleRegion(key: string): void\n\n /** `overlay` mode: the buffer the live editor text is diffed against. */\n setBaseFile(file: DiffTextFile | null): void\n setEnabled(enabled: boolean): void\n}\n\n/** The diff row under a pointer: which pane, that pane's rows, and the index into them. */\nexport type DiffRowHit = {\n readonly side: DiffGutterSide\n readonly rowIndex: number\n readonly rows: readonly DiffRenderRow[]\n}\n\n// How a pointer event finds the plugin that owns the pane under it, so a host never reads row\n// attributes or pane classes to work out where a press landed.\nconst viewsByScrollElement = new WeakMap<Element, DiffViewContribution>()\n\n/** `null` outside a `document`-mode diff pane, or over something that is not a diff row. */\nexport function diffRowAtEvent(event: MouseEvent): DiffRowHit | null {\n const target = event.target\n if (!(target instanceof Element)) return null\n\n const scrollElement = target.closest('[data-editor-diff-side]')\n if (!scrollElement) return null\n\n return viewsByScrollElement.get(scrollElement)?.rowHitAt(event) ?? null\n}\n\nconst EMPTY_PROJECTION: LiveDiffProjection = {\n injectedRows: [],\n rowDecorations: new Map(),\n rows: [],\n rowsByBufferRow: new Map(),\n}\nconst ROW_DECORATION_SOURCE = 'editor.diff'\nconst INLINE_HIGHLIGHT_STYLE = { backgroundColor: 'rgba(255, 255, 255, 0.18)' } as const\n\nlet nextDiffPluginId = 0\n\n/**\n * The one diff plugin (§C1), carrying two internal row-delivery modes.\n *\n * `document` — the editor holds a synthetic buffer of the projected rows and the plugin publishes\n * them for the host to push in. Everything works because the rows are real document lines.\n *\n * `overlay` — the editor holds the host's live, editable buffer and deletions arrive as injected\n * rows. Non-parity by construction; see §C2 and test/overlayModeLimits.test.ts before reaching for\n * it.\n */\nexport function createDiffPlugin(options: DiffPluginOptions): DiffPlugin {\n registerDiffColors()\n const runtime = new DiffPluginRuntime(options)\n\n return {\n name: `editor-diff-${options.mode}`,\n activate(context) {\n return runtime.activate(context)\n },\n setFile: (file, prepared) => runtime.setFile(file, prepared),\n releasePreparedSyntax: () => runtime.releasePreparedSyntax(),\n getRows: () => runtime.getRows(),\n getStackedRows: () => runtime.getStackedRows(),\n getTokens: () => runtime.getTokens(),\n isSyntaxReady: () => runtime.isSyntaxReady(),\n onDidChangeRows: (listener) => runtime.onDidChangeRows(listener),\n onDidChangeTokens: (listener) => runtime.onDidChangeTokens(listener),\n getExpandedRegions: () => runtime.getExpandedRegions(),\n toggleRegion: (key) => runtime.toggleRegion(key),\n setBaseFile: (file) => runtime.setBaseFile(file),\n setEnabled: (enabled) => runtime.setEnabled(enabled),\n }\n}\n\nclass DiffPluginRuntime {\n private readonly id = nextDiffPluginId++\n private readonly mode: DiffPluginMode\n private readonly side: DiffGutterSide\n private syntax: DiffSyntaxController\n private readonly rowListeners = new Set<() => void>()\n private readonly tokenListeners = new Set<() => void>()\n\n private readonly regions: DiffRegionStore\n private file: DiffFile | null = null\n private rows: readonly DiffRenderRow[] = []\n private stackedRows: readonly DiffRenderRow[] | null = null\n private digits: DiffGutterDigits = { old: 0, new: 0 }\n private hunkRows = false\n private expandableRows = false\n\n private baseFile: DiffTextFile | null = null\n private enabled: boolean\n private liveProjection: LiveDiffProjection = EMPTY_PROJECTION\n\n private decoration: DiffDecorationContribution | null = null\n private view: DiffViewContribution | null = null\n private injectedRowsListener: (() => void) | null = null\n private lastGutterLayout: DiffGutterLayout | null = null\n private readonly regionSubscription: EditorDisposable\n\n constructor(private readonly options: DiffPluginOptions) {\n this.mode = options.mode\n this.side = options.side ?? 'stacked'\n this.regions = options.regions ?? createDiffRegionStore()\n // A shared store means the sibling side's toggle lands here too.\n //\n // Held for the plugin's whole life, and never released. Every narrower lifetime is wrong:\n //\n // - torn down with the ACTIVATION, next to the syntax controller, which is the obvious place:\n // the first deactivation is then terminal. The plugin is re-activated against a fresh editor,\n // renders correctly, and is silently deaf to every expansion toggle after that. React\n // StrictMode makes mount -> unmount -> mount the normal development path, so a host that gets\n // this wrong sees gutter clicks do nothing at all.\n // - created only on activation: a plugin driven headlessly as a projection source, with no\n // editor behind it, never sees its own `toggleRegion`.\n // - released from `EditorPlugin.dispose`: that fires when an editor tears down, and a React\n // host reuses the same plugin instance across the next editor — so this is the first case\n // again with an extra step.\n //\n // Nothing leaks by holding it. The listener lives in the store, the store is the host's, and a\n // host drops both together; a plugin the host builds and never mounts — StrictMode\n // double-invokes the `useMemo` that builds one — is collected with it.\n this.regionSubscription = this.regions.onDidChange(() => this.handleRegionsChanged())\n // Overlay mode is switched on by the host once it has a base file; document mode is live from\n // the moment it has one.\n this.enabled = this.mode === 'document'\n this.syntax = this.createSyntaxController()\n }\n\n activate(context: EditorPluginContext): EditorDisposable[] {\n // A controller disposed by a previous deactivation is dead for good — its scheduler is\n // terminal, so `schedule()` returns an inactive handle forever and the diff would render\n // permanently uncoloured after a remove-then-re-add. The core supports that cycle\n // (`Editor.setPlugins`), so the controller's lifetime is the activation's, not the plugin's.\n //\n // Only a *replacement* controller needs the file pushed into it. Doing it unconditionally\n // re-parses a file the surviving controller is already holding — and when the host sets the\n // file before constructing the editor, which is the natural order, that means cancelling an\n // in-flight parse to start an identical one. Under shiki that is two worker owners for one\n // file.\n if (this.syntax.isDisposed()) {\n this.syntax = this.createSyntaxController()\n if (this.file) this.syntax.setFile(this.file, this.rows)\n }\n\n const disposables: EditorDisposable[] = [\n context.registerGutterContribution(\n createDiffGutterContribution({\n side: this.side,\n tint: this.mode === 'document' ? 'row' : 'cell',\n getDigits: () => this.gutterDigits(),\n resolveRow: (row) => this.resolveGutterRow(row),\n isEnabled: () => this.enabled,\n onLayout: (layout) => this.publishGutterLayout(layout),\n }),\n ),\n context.registerDecorationContribution({\n createContribution: (featureContext) => this.createDecorationContribution(featureContext),\n }),\n context.registerViewContribution({\n createContribution: (viewContext) => this.createViewContribution(viewContext),\n }),\n { dispose: () => this.syntax.dispose() },\n ]\n\n if (this.mode === 'overlay') {\n disposables.push(\n context.registerInjectedTextRowProvider({\n getInjectedTextRows: () => this.injectedRows(),\n onDidChangeInjectedTextRows: (listener) => this.setInjectedRowsListener(listener),\n }),\n )\n }\n\n return disposables\n }\n\n // ---------------------------------------------------------------- document mode (§C3, §C5)\n\n setFile(file: DiffFile | null, prepared: PreparedDiffSyntaxInput = []): void {\n if (this.mode !== 'document') {\n void Promise.resolve(prepared).then(\n (sources) => {\n for (const source of sources) source.dispose()\n },\n () => undefined,\n )\n return\n }\n\n this.file = file\n // The store decides whether this counts as the same diff, and drops expansion if not. Both\n // sides of a split share it, so both are told once.\n this.regions.setFile(file)\n this.rebuildRows()\n this.syntax.setFile(file, this.rows, prepared)\n this.notifyRows()\n }\n\n releasePreparedSyntax(): readonly PreparedDiffSyntaxSource[] {\n return this.syntax.release()\n }\n\n getRows(): readonly DiffRenderRow[] {\n if (this.mode === 'overlay') return this.liveProjection.rows\n return this.rows\n }\n\n getStackedRows(): readonly DiffRenderRow[] {\n if (this.mode === 'overlay') return this.liveProjection.rows\n if (this.side === 'stacked') return this.rows\n this.stackedRows ??= projectRows(this.file, 'stacked', this.regions.getExpandedRegions())\n return this.stackedRows\n }\n\n isSyntaxReady(): boolean {\n return this.syntax.isReady()\n }\n\n getTokens(): readonly EditorToken[] {\n return this.syntax.getTokens()\n }\n\n getExpandedRegions(): ReadonlySet<string> {\n return this.regions.getExpandedRegions()\n }\n\n toggleRegion(key: string): void {\n if (this.mode !== 'document') return\n if (!this.file) return\n\n // The store fans the change back to every plugin reading it, this one included, so the rebuild\n // happens in `handleRegionsChanged` rather than here.\n this.regions.toggleRegion(key)\n }\n\n private handleRegionsChanged(): void {\n if (this.mode !== 'document') return\n if (!this.file) return\n\n this.rebuildRows()\n // Same file, different rows: the parsed token streams still apply, so this re-projects\n // synchronously and `getTokens()` is correct before the host's `setText` returns (§C10).\n this.syntax.setRows(this.rows)\n this.notifyRows()\n }\n\n onDidChangeRows(listener: () => void): EditorDisposable {\n this.rowListeners.add(listener)\n return { dispose: () => this.rowListeners.delete(listener) }\n }\n\n onDidChangeTokens(listener: () => void): EditorDisposable {\n this.tokenListeners.add(listener)\n return { dispose: () => this.tokenListeners.delete(listener) }\n }\n\n private rebuildRows(): void {\n this.stackedRows = null\n this.rows = projectRows(this.file, this.side, this.regions.getExpandedRegions())\n // Both derived once here rather than per gutter-width recompute and per mousemove.\n this.digits = diffGutterDigits(this.rows)\n this.hunkRows = this.rows.some((row) => row.type === 'hunk')\n this.expandableRows = this.rows.some((row) => row.type === 'hunk' && row.expandable === true)\n }\n\n private notifyRows(): void {\n for (const listener of this.rowListeners) listener()\n this.decoration?.refresh()\n this.view?.refreshRows()\n }\n\n private notifyTokens(): void {\n for (const listener of this.tokenListeners) listener()\n }\n\n // ---------------------------------------------------------------- overlay mode\n\n setBaseFile(file: DiffTextFile | null): void {\n if (this.mode !== 'overlay') return\n\n this.baseFile = file\n this.decoration?.refresh()\n }\n\n setEnabled(enabled: boolean): void {\n if (this.mode !== 'overlay') return\n\n this.enabled = enabled\n this.decoration?.refresh()\n }\n\n private injectedRows(): readonly EditorInjectedTextRow[] {\n if (!this.enabled) return []\n return this.liveProjection.injectedRows\n }\n\n private setInjectedRowsListener(listener: () => void): EditorDisposable {\n this.injectedRowsListener = listener\n return {\n dispose: () => {\n if (this.injectedRowsListener === listener) this.injectedRowsListener = null\n },\n }\n }\n\n private gutterDigits(): DiffGutterDigits {\n if (this.mode === 'overlay') return diffGutterDigits(this.liveProjection.rows)\n return this.digits\n }\n\n /**\n * Which diff row a rendered gutter row is showing. The two modes cannot share an answer.\n *\n * `document` — §C4 holds, so the projection index is the buffer row and a positional lookup is\n * correct.\n *\n * `overlay` — the projection array interleaves injected deletion rows, so position and buffer\n * row diverge from the first deletion onwards; the mapping the projection publishes for exactly\n * this purpose is `rowsByBufferRow`, keyed `newLineNumber - 1`. An injected row is not in that\n * map at all and carries its own row as metadata instead.\n */\n private resolveGutterRow(row: EditorGutterRowContext): DiffRenderRow | null {\n if (this.mode === 'overlay') {\n if (row.source === 'injected') return injectedDiffRow(row)\n if (row.source !== 'document') return null\n return this.liveProjection.rowsByBufferRow.get(row.bufferRow) ?? null\n }\n\n if (row.source !== 'document') return null\n return this.rows[row.bufferRow] ?? null\n }\n\n // ---------------------------------------------------------------- contributions\n\n private createDecorationContribution(\n context: EditorDecorationContributionContext,\n ): EditorDecorationContribution {\n const contribution = new DiffDecorationContribution(context, {\n mode: this.mode,\n getRows: () => this.rows,\n buildLiveProjection: () => this.buildLiveProjection(context),\n setLiveProjection: (projection) => {\n this.liveProjection = projection\n },\n notifyInjectedRowsChanged: () => this.injectedRowsListener?.(),\n })\n this.decoration = contribution\n contribution.refresh()\n return {\n handleEditorChange: (change) => contribution.handleEditorChange(change),\n dispose: () => {\n if (this.decoration === contribution) this.decoration = null\n contribution.dispose()\n },\n }\n }\n\n private buildLiveProjection(context: EditorDecorationContributionContext): LiveDiffProjection {\n if (!this.enabled) return EMPTY_PROJECTION\n if (!this.baseFile) return EMPTY_PROJECTION\n if (!context.hasDocument()) return EMPTY_PROJECTION\n\n const file = createTextDiff({\n oldFile: this.baseFile,\n newFile: {\n path: this.baseFile.path,\n text: liveDiffDocumentText(context),\n languageId: this.baseFile.languageId,\n },\n })\n return createLiveDiffProjection(file)\n }\n\n private createViewContribution(\n context: EditorViewContributionContext,\n ): EditorViewContribution | null {\n const contribution = new DiffViewContribution(context, {\n highlightName: `editor-diff-${this.id}-${this.side}-inline`,\n mode: this.mode,\n side: this.side,\n getRows: () => this.getRows(),\n hasHunkRows: () => this.hunkRows,\n hasExpandableRows: () => this.expandableRows,\n toggleRegion: (key) => this.toggleRegion(key),\n detach: (disposed) => {\n lentSyntax?.dispose()\n if (this.view === disposed) this.view = null\n },\n })\n const lentSyntax = context\n .getFeature(EDITOR_SNIPPET_TOKENS_FEATURE)\n ?.addSource(snippetTokenSource(this.options.syntaxBackend))\n this.view = contribution\n if (this.lastGutterLayout) contribution.applyGutterLayout(this.lastGutterLayout)\n return contribution\n }\n\n private publishGutterLayout(layout: DiffGutterLayout): void {\n // Retained, never consumed. `width()` can run before a view contribution exists, and a\n // contribution can be torn down and rebuilt later on a fresh scroll element carrying none of\n // these custom properties. The gutter memoizes on the layout, so it will not republish an\n // unchanged one — leaving a replacement contribution with no other way to learn the geometry,\n // and `.editor-diff-gutter` falling back to `1fr 1fr 12px`, which is the drift the whole\n // custom-property dance exists to avoid.\n this.lastGutterLayout = layout\n this.view?.applyGutterLayout(layout)\n }\n\n private createSyntaxController(): DiffSyntaxController {\n return new DiffSyntaxController({\n side: this.side,\n backend: this.options.syntaxBackend,\n enabled: this.options.syntaxHighlight !== false,\n onDidChangeTokens: () => this.notifyTokens(),\n })\n }\n}\n\n// A live diff compares whole documents, and the diff library takes strings: an explicit extraction.\nfunction liveDiffDocumentText(context: EditorDecorationContributionContext): string {\n return context.materializeFullText()\n}\n\nfunction projectRows(\n file: DiffFile | null,\n side: DiffGutterSide,\n expandedRegions: ReadonlySet<string>,\n): readonly DiffRenderRow[] {\n if (!file) return []\n if (side === 'stacked') return createStackedProjection(file, { expandedRegions }).rows\n\n const projection = createSplitProjection(file, { expandedRegions })\n return side === 'old' ? projection.leftRows : projection.rightRows\n}\n\ntype DiffDecorationOptions = {\n readonly mode: DiffPluginMode\n readonly getRows: () => readonly DiffRenderRow[]\n readonly buildLiveProjection: () => LiveDiffProjection\n readonly setLiveProjection: (projection: LiveDiffProjection) => void\n readonly notifyInjectedRowsChanged: () => void\n}\n\nclass DiffDecorationContribution {\n private lastTextSnapshot: unknown = undefined\n\n constructor(\n private readonly context: EditorDecorationContributionContext,\n private readonly options: DiffDecorationOptions,\n ) {}\n\n /**\n * The editor's notification, which is not the same thing as \"the text changed\".\n *\n * Rebuilding an overlay projection materializes the whole document and runs `structuredPatch`\n * over it, synchronously. `handleEditorChange` also fires for selection-only changes —\n * `EditorContributionChange.kind` is one of edit/selection/undo/redo/none — and a caret move cannot\n * alter the diff, so paying for one is pure waste on the typing path. Text-snapshot identity is\n * the second guard, for the notification paths that carry no change object at all.\n *\n * Plugin-state changes (`setBaseFile`, `setEnabled`) call `refresh()` directly and are never\n * skipped: the text is the same but the diff it produces is not.\n */\n handleEditorChange(change: EditorContributionChange | null): void {\n if (this.options.mode === 'overlay') {\n if (change?.kind === 'selection') return\n\n const snapshot = this.context.getTextSnapshot()\n if (snapshot !== null && snapshot === this.lastTextSnapshot) return\n }\n\n this.refresh()\n }\n\n refresh(): void {\n this.lastTextSnapshot = this.context.getTextSnapshot()\n if (this.options.mode === 'document') {\n this.context.setRowDecorations(\n ROW_DECORATION_SOURCE,\n diffRowDecorations(this.options.getRows()),\n )\n return\n }\n\n this.applyLiveProjection(this.options.buildLiveProjection())\n }\n\n dispose(): void {\n if (this.options.mode === 'overlay') this.applyLiveProjection(EMPTY_PROJECTION)\n this.context.clearRowDecorations(ROW_DECORATION_SOURCE)\n }\n\n /**\n * These three steps are ordered, and the order is load-bearing.\n *\n * Announcing the injected-row change first would rebuild the row set before the cells are\n * repainted, and a row that the rebuild unmounts keeps whatever its cell last rendered — a\n * disabled live diff would leave a stale `4+` behind on a detached cell. Repainting first, while\n * that row is still mounted, is what clears it.\n *\n * The stored projection has to be current before the repaint, because the gutter reads its rows\n * from it.\n */\n private applyLiveProjection(projection: LiveDiffProjection): void {\n this.options.setLiveProjection(projection)\n this.context.setRowDecorations(ROW_DECORATION_SOURCE, projection.rowDecorations)\n this.options.notifyInjectedRowsChanged()\n }\n}\n\ntype DiffViewOptions = {\n readonly highlightName: string\n readonly mode: DiffPluginMode\n readonly side: DiffGutterSide\n readonly getRows: () => readonly DiffRenderRow[]\n readonly hasHunkRows: () => boolean\n readonly hasExpandableRows: () => boolean\n readonly toggleRegion: (key: string) => void\n readonly detach: (contribution: DiffViewContribution) => void\n}\n\n/** Pointer handling and inline word-diff highlights. */\nclass DiffViewContribution implements EditorViewContribution {\n readonly snapshotKey = 'diff-inline-v1'\n private readonly pressParticipant: EditorDisposable\n private readonly separatorRows: EditorDisposable\n private snapshot: EditorViewSnapshot | null = null\n private lastHighlightRows: readonly DiffRenderRow[] | null = null\n private lastHighlightTextVersion = -1\n\n constructor(\n private readonly context: EditorViewContributionContext,\n private readonly options: DiffViewOptions,\n ) {\n this.context.scrollElement.dataset.editorDiffSide = options.side\n viewsByScrollElement.set(this.context.scrollElement, this)\n this.pressParticipant = this.context.registerPressParticipant(this.claimSeparatorPress)\n this.separatorRows = this.context.registerNonCaretRows(this.isSeparatorRow)\n this.context.scrollElement.addEventListener('click', this.handleClick)\n this.context.scrollElement.addEventListener('mousemove', this.handleMouseMove)\n this.context.scrollElement.addEventListener('mouseleave', this.handleMouseLeave)\n }\n\n /**\n * The snapshot is kept for every update kind — the pointer hit-test reads it — but the two\n * derived jobs below are not per-frame work.\n *\n * `update` is called for `'viewport'` too, which `Editor.handleViewportChange` fires on every\n * scroll frame, and the highlights can only change when the rows or the text do.\n */\n update(snapshot: EditorViewSnapshot, kind: EditorViewContributionUpdateKind): void {\n this.snapshot = snapshot\n if (this.options.mode !== 'document') return\n if (kind === 'viewport' || kind === 'selection') return\n\n this.applyInlineHighlights()\n }\n\n captureVisiblePaint(snapshot: EditorViewSnapshot) {\n const viewport = this.context.scrollElement.getBoundingClientRect()\n const rows = this.options.getRows()\n const ranges = snapshot.visibleRows.flatMap((visible) => {\n const row = rows[visible.bufferRow]\n if (!row) return []\n const start = snapshot.lineStartsView.at(visible.bufferRow) ?? visible.startOffset\n return diffInlineHighlightRanges([row]).map((range) => ({\n start: range.start + start,\n end: range.end + start,\n }))\n })\n const rectangles = []\n for (const range of ranges) {\n const rect = this.context.getRangeClientRect(range.start, range.end)\n if (!rect || rect.width <= 0 || rect.height <= 0) continue\n rectangles.push({\n left: rect.left - viewport.left + snapshot.viewport.scrollLeft,\n top: rect.top - viewport.top + snapshot.viewport.scrollTop,\n width: rect.width,\n height: rect.height,\n backgroundColor: INLINE_HIGHLIGHT_STYLE.backgroundColor,\n })\n }\n return { id: `diff-inline-${this.options.side}`, status: 'ready' as const, rectangles }\n }\n\n refreshRows(): void {\n this.lastHighlightRows = null\n this.applyInlineHighlights()\n }\n\n applyGutterLayout(layout: DiffGutterLayout): void {\n // §3.3 trap 1: the canvas derived lane geometry arithmetically, so a DOM gutter has to hand CSS\n // the same numbers rather than approximate them with `1fr 1fr 12px`.\n const style = this.context.scrollElement.style\n const columns = layout.lanes.map((lane) => `${lane.width}px`).join(' ')\n style.setProperty('--editor-diff-gutter-columns', columns)\n style.setProperty('--editor-diff-gutter-total-width', `${layout.width}px`)\n }\n\n dispose(): void {\n this.pressParticipant.dispose()\n this.separatorRows.dispose()\n this.context.scrollElement.removeEventListener('click', this.handleClick)\n this.context.scrollElement.removeEventListener('mousemove', this.handleMouseMove)\n this.context.scrollElement.removeEventListener('mouseleave', this.handleMouseLeave)\n this.context.scrollElement.style.cursor = ''\n // A replacement view on the same element may already have registered itself.\n if (viewsByScrollElement.get(this.context.scrollElement) === this) {\n viewsByScrollElement.delete(this.context.scrollElement)\n }\n this.context.clearRangeHighlight(this.options.highlightName)\n // Or the runtime keeps writing highlights and gutter geometry through a torn-down context,\n // and holds its scroll element alive for the plugin's lifetime.\n this.options.detach(this)\n }\n\n // A highlight failure must not dispose the contribution and disable gutter expansion.\n private applyInlineHighlights(): void {\n const rows = this.options.getRows()\n const textVersion = this.snapshot?.textVersion ?? -1\n // Rows alone cannot key this: the view clamps ranges to the text it holds when they are set,\n // and the host pushes the new text only after the rows notification that first lands here.\n if (rows === this.lastHighlightRows && textVersion === this.lastHighlightTextVersion) return\n\n this.lastHighlightRows = rows\n this.lastHighlightTextVersion = textVersion\n try {\n this.context.setRangeHighlight(\n this.options.highlightName,\n diffInlineHighlightRanges(rows),\n INLINE_HIGHLIGHT_STYLE,\n )\n } catch (error) {\n this.context.log({\n action: 'editor.diff.inline_highlight_failed',\n level: 'warn',\n error: { message: error instanceof Error ? error.message : String(error) },\n })\n }\n }\n\n /**\n * Keeps the caret off a separator row, expandable or not.\n *\n * A separator is chrome, not content — but its label is real buffer text, because the host's\n * document is `joinRenderLines(rows)` and there is nowhere else for the label to live. So the\n * editor will happily rest a caret in the middle of `Show 12 unmodified lines`, and a collapsed\n * caret copies its own line: Cmd+C would put that sentence on the clipboard. The mousedown is\n * refused here, and arrow keys step over the row (`isSeparatorRow`). A drag that *crosses* a separator still\n * covers it, and always will while the label occupies offsets — giving these rows an offset\n * space of their own is the parallel coordinate system §C2 rules out.\n *\n * Every click count, not just the first: a second press would select a word of the label, a\n * third its line and a fourth the whole document.\n */\n private readonly claimSeparatorPress = (event: MouseEvent): boolean =>\n event.button === 0 && this.hunkRowAt(event) !== null\n\n /** The keyboard half of the same rule: arrow keys step over a separator. */\n private readonly isSeparatorRow = (bufferRow: number): boolean => {\n if (this.options.mode !== 'document' || !this.options.hasHunkRows()) return false\n return this.options.getRows()[bufferRow]?.type === 'hunk'\n }\n\n private readonly handleClick = (event: MouseEvent): void => {\n if (event.button !== 0) return\n\n const row = this.expandableRowAt(event)\n if (!row?.expandKey) return\n\n event.preventDefault()\n this.options.toggleRegion(row.expandKey)\n }\n\n private readonly handleMouseMove = (event: MouseEvent): void => {\n const cursor = this.expandableRowAt(event) ? 'pointer' : ''\n if (this.context.scrollElement.style.cursor === cursor) return\n\n this.context.scrollElement.style.cursor = cursor\n }\n\n private readonly handleMouseLeave = (): void => {\n if (!this.context.scrollElement.style.cursor) return\n\n this.context.scrollElement.style.cursor = ''\n }\n\n private expandableRowAt(event: MouseEvent): DiffRenderRow | null {\n // Avoid a point query on every mousemove when no separator can expand.\n if (!this.options.hasExpandableRows()) return null\n\n const row = this.hunkRowAt(event)\n return row?.expandable ? row : null\n }\n\n /**\n * The separator under the pointer, whether or not it can be expanded. A partial diff — a plain\n * patch with no file text behind it — has separators that say how much was skipped and cannot\n * open them, and those are still not somewhere a caret belongs.\n */\n private hunkRowAt(event: MouseEvent): DiffRenderRow | null {\n if (!this.options.hasHunkRows()) return null\n\n const hit = this.rowHitAt(event)\n const row = hit?.rows[hit.rowIndex]\n return row?.type === 'hunk' ? row : null\n }\n\n /** Only `document` mode has an answer: there a buffer row is an index into `getRows()`. */\n rowHitAt(event: MouseEvent): DiffRowHit | null {\n if (this.options.mode !== 'document') return null\n\n const hit = this.context.rowAtPoint(event.clientX, event.clientY)\n if (hit?.source !== 'document') return null\n const rowIndex = hit.bufferRow\n\n const rows = this.options.getRows()\n return rows[rowIndex] ? { side: this.options.side, rowIndex, rows } : null\n }\n}\n\nfunction injectedDiffRow(row: EditorGutterRowContext): DiffRenderRow | null {\n const metadata = row.metadata\n if (!metadata || typeof metadata !== 'object') return null\n return 'type' in metadata && 'text' in metadata ? (metadata as DiffRenderRow) : null\n}\n\nfunction snippetTokenSource(backend: DiffSyntaxBackend | undefined): EditorSnippetTokenSource {\n if (backend?.kind === 'highlighter') return { highlighter: backend.provider }\n return { syntax: backend?.provider }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { EditorOptions } from '@singapore-editor/core/editor';
|
|
2
|
+
import { type EditorKeymapOptions } from '@singapore-editor/core/keymap';
|
|
3
|
+
import type { EditorCursorLineHighlightOptions } from '@singapore-editor/core/rendering';
|
|
4
|
+
export type DiffEditorOptions = Required<Pick<EditorOptions, 'detectIndentation' | 'documentMode' | 'editability' | 'folding'>> & {
|
|
5
|
+
readonly cursorLineHighlight: Required<EditorCursorLineHighlightOptions>;
|
|
6
|
+
readonly keymap: DiffKeymap;
|
|
7
|
+
};
|
|
8
|
+
type DiffKeymap = Readonly<Required<Pick<EditorKeymapOptions, 'defaultBindings' | 'layers'>>>;
|
|
9
|
+
/**
|
|
10
|
+
* The options an editor holding a `mode: 'document'` diff needs; an overlay diff keeps its host's
|
|
11
|
+
* editable options. Spread them and add the host's own, such as plugins, typography and theme.
|
|
12
|
+
*/
|
|
13
|
+
export declare function createDiffEditorOptions(): DiffEditorOptions;
|
|
14
|
+
export {};
|
|
15
|
+
//# sourceMappingURL=editorOptions.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"editorOptions.d.ts","sourceRoot":"","sources":["../src/editorOptions.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAA;AAClE,OAAO,EAGL,KAAK,mBAAmB,EACzB,MAAM,+BAA+B,CAAA;AACtC,OAAO,KAAK,EAAE,gCAAgC,EAAE,MAAM,kCAAkC,CAAA;AAGxF,MAAM,MAAM,iBAAiB,GAAG,QAAQ,CACtC,IAAI,CAAC,aAAa,EAAE,mBAAmB,GAAG,cAAc,GAAG,aAAa,GAAG,SAAS,CAAC,CACtF,GAAG;IACF,QAAQ,CAAC,mBAAmB,EAAE,QAAQ,CAAC,gCAAgC,CAAC,CAAA;IACxE,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAA;CAC5B,CAAA;AAED,KAAK,UAAU,GAAG,QAAQ,CAAC,QAAQ,CAAC,IAAI,CAAC,mBAAmB,EAAE,iBAAiB,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAA;AAY7F;;;GAGG;AACH,wBAAgB,uBAAuB,IAAI,iBAAiB,CAc3D"}
|