@astryxdesign/lab 0.7.6-canary.021bfb5 → 0.7.6-canary.032c4a1
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/dist/CodeEditor/CodeEditor.d.ts.map +1 -1
- package/dist/CodeEditor/CodeEditor.js +6 -0
- package/dist/DiffViewer/DiffViewer.d.ts.map +1 -1
- package/dist/DiffViewer/DiffViewer.js +309 -61
- package/dist/DiffViewer/createDiffFile.d.ts +0 -5
- package/dist/DiffViewer/createDiffFile.d.ts.map +1 -1
- package/dist/DiffViewer/createDiffFile.js +230 -21
- package/dist/DiffViewer/diffTypes.d.ts +7 -0
- package/dist/DiffViewer/diffTypes.d.ts.map +1 -1
- package/dist/lab.css +3 -0
- package/package.json +5 -5
- package/src/CircularProgress/__tests__/CircularProgressMotion.a11y.chromium.spec.ts +100 -62
- package/src/CodeEditor/CodeEditor.tsx +6 -2
- package/src/DiffViewer/DiffViewer.doc.mjs +1 -1
- package/src/DiffViewer/DiffViewer.test.tsx +236 -1
- package/src/DiffViewer/DiffViewer.tsx +371 -66
- package/src/DiffViewer/createDiffFile.test.ts +155 -0
- package/src/DiffViewer/createDiffFile.ts +278 -21
- package/src/DiffViewer/diffTypes.ts +7 -0
- package/src/MobileTokenizer/MobileTokenizer.test.tsx +2 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CodeEditor.d.ts","sourceRoot":"","sources":["../../src/CodeEditor/CodeEditor.tsx"],"names":[],"mappings":"AAyBA,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,oBAAoB,CAAC;AAkBlD,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,8BAA8B,CAAC;
|
|
1
|
+
{"version":3,"file":"CodeEditor.d.ts","sourceRoot":"","sources":["../../src/CodeEditor/CodeEditor.tsx"],"names":[],"mappings":"AAyBA,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,oBAAoB,CAAC;AAkBlD,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,8BAA8B,CAAC;AA2F5D,MAAM,WAAW,eAAgB,SAAQ,IAAI,CAC3C,SAAS,CAAC,cAAc,CAAC,EACzB,UAAU,CACX;IACC,wCAAwC;IACxC,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;IAChC,uBAAuB;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,qBAAqB;IACrB,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,sDAAsD;IACtD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,sFAAsF;IACtF,KAAK,EAAE,MAAM,CAAC;IACd,wCAAwC;IACxC,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,qCAAqC;IACrC,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,6BAA6B;IAC7B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,kCAAkC;IAClC,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAC5B,+BAA+B;IAC/B,IAAI,CAAC,EAAE,IAAI,GAAG,IAAI,CAAC;IACnB,uBAAuB;IACvB,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,SAAS,EAAE,CAAC;IAC5D;;;;;OAKG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAsED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,UAAU,CAAC,EACzB,KAAK,EACL,QAAQ,EACR,QAAsB,EACtB,cAAsB,EACtB,UAAkB,EAClB,WAAW,EACX,SAAS,EACT,IAAW,EACX,SAAS,EAAE,eAAe,EAC1B,KAAK,EACL,UAAqE,EACrE,MAAM,EACN,SAAS,EACT,KAAK,EACL,GAAG,EACH,GAAG,KAAK,EACT,EAAE,eAAe,+BAsVjB;yBAvWe,UAAU"}
|
|
@@ -35,6 +35,9 @@ const styles = {
|
|
|
35
35
|
k1xSpc: "x78zum5",
|
|
36
36
|
kaIpWk: "xh6dtrn",
|
|
37
37
|
kWkggS: "xwmxj5m",
|
|
38
|
+
kMzoRj: "x1litavf",
|
|
39
|
+
ksu8eU: "x1y0btm7",
|
|
40
|
+
kVAM5u: "x14i3s5s",
|
|
38
41
|
kVQacm: "xb3r6kr",
|
|
39
42
|
$$css: true
|
|
40
43
|
},
|
|
@@ -51,6 +54,9 @@ const styles = {
|
|
|
51
54
|
k9WMMc: "xp4054r",
|
|
52
55
|
kfSwDN: "x87ps6o",
|
|
53
56
|
kMwMTN: "xnbbluu",
|
|
57
|
+
kWqL5O: "x1pcaw5z",
|
|
58
|
+
kSWEuD: "x32b0ac",
|
|
59
|
+
k26BEO: "xtgwc6q",
|
|
54
60
|
$$css: true
|
|
55
61
|
},
|
|
56
62
|
editor: {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DiffViewer.d.ts","sourceRoot":"","sources":["../../src/DiffViewer/DiffViewer.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"DiffViewer.d.ts","sourceRoot":"","sources":["../../src/DiffViewer/DiffViewer.tsx"],"names":[],"mappings":"AAwCA,OAAO,EAQL,KAAK,UAAU,EACf,KAAK,SAAS,EACd,KAAK,GAAG,EAET,MAAM,OAAO,CAAC;AAGf,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAGL,KAAK,YAAY,EAGlB,MAAM,8BAA8B,CAAC;AAQtC,OAAO,EAEL,KAAK,qBAAqB,EAC3B,MAAM,iCAAiC,CAAC;AAsBzC,OAAO,KAAK,EACV,cAAc,EAGd,iBAAiB,EAElB,MAAM,aAAa,CAAC;AAGrB,uEAAuE;AACvE,MAAM,MAAM,kBAAkB,GAAG,SAAS,GAAG,OAAO,CAAC;AAErD;;;;GAIG;AACH,MAAM,MAAM,yBAAyB,GAAG,SAAS,GAAG,MAAM,GAAG,MAAM,CAAC;AAEpE;;;GAGG;AACH,MAAM,MAAM,yBAAyB,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC;AAEjE,MAAM,WAAW,eAAgB,SAAQ,SAAS,CAAC,cAAc,CAAC;IAChE,yCAAyC;IACzC,GAAG,CAAC,EAAE,GAAG,CAAC,cAAc,CAAC,CAAC;IAC1B;;;OAGG;IACH,IAAI,EAAE,cAAc,CAAC;IACrB;;;;;;;;OAQG;IACH,QAAQ,CAAC,EAAE,kBAAkB,CAAC;IAC9B;;;;;;;OAOG;IACH,QAAQ,CAAC,EAAE,MAAM,GAAG,YAAY,CAAC;IACjC;;;;OAIG;IACH,aAAa,CAAC,EAAE,aAAa,CAAC,YAAY,CAAC,CAAC;IAC5C;;;OAGG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB;;;;;;;;OAQG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB;;;OAGG;IACH,eAAe,CAAC,EAAE,yBAAyB,CAAC;IAC5C;;;;;OAKG;IACH,eAAe,CAAC,EAAE,yBAAyB,CAAC;IAC5C;;;;OAIG;IACH,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B;;;;OAIG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,iEAAiE;IACjE,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB;;;OAGG;IACH,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,uEAAuE;IACvE,iBAAiB,CAAC,EAAE,CAAC,WAAW,EAAE,OAAO,KAAK,IAAI,CAAC;IACnD;;;OAGG;IACH,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB;;;;;OAKG;IACH,oBAAoB,CAAC,EAAE,CAAC,GAAG,EAAE,iBAAiB,KAAK,SAAS,CAAC;IAC7D;;;;;;OAMG;IACH,iBAAiB,CAAC,EAAE,CAClB,GAAG,EAAE,iBAAiB,EACtB,KAAK,EAAE,UAAU,CAAC,iBAAiB,CAAC,KACjC,IAAI,CAAC;IACV,yDAAyD;IACzD,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAC5B;;;OAGG;IACH,WAAW,CAAC,EAAE,qBAAqB,CAAC;CACrC;AAw/BD;;;;;;;;;;GAUG;AACH,wBAAgB,UAAU,CAAC,EACzB,GAAG,EACH,IAAI,EACJ,QAAoB,EACpB,QAAQ,EAAE,YAAY,EACtB,aAAa,EACb,cAAqB,EACrB,SAAiB,EACjB,eAA2B,EAC3B,eAAwB,EACxB,mBAA0B,EAC1B,aAAoB,EACpB,WAAW,EAAE,eAAe,EAC5B,kBAA0B,EAC1B,iBAAiB,EACjB,SAAS,EACT,oBAAoB,EACpB,iBAAiB,EACjB,SAAS,EACT,WAAW,EACX,MAAM,EACN,SAAS,EACT,KAAK,EACL,GAAG,KAAK,EACT,EAAE,eAAe,+BA6yBjB;yBAr0Be,UAAU"}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* @file DiffViewer.tsx
|
|
5
5
|
* @input Uses React, StyleX, theme tokens, the shared CodeBlock tokenizer and
|
|
6
6
|
* syntax-theme slots (incl. the diff row slots), useScrollableArea,
|
|
7
|
-
* Token, Icon, VisuallyHidden, and the i18n translator
|
|
7
|
+
* Token, Icon, Button, VisuallyHidden, and the i18n translator
|
|
8
8
|
* @output Exports DiffViewer component, DiffViewerProps, DiffViewerViewMode,
|
|
9
9
|
* DiffViewerChangeIndicator, DiffViewerInlineHighlight
|
|
10
10
|
* @position Lab implementation; read-only, syntax-highlighted file diff for
|
|
@@ -12,6 +12,9 @@
|
|
|
12
12
|
*
|
|
13
13
|
* Renders one DiffViewerFile: a file header (path, status, +/- counts, a
|
|
14
14
|
* caller slot, optional disclosure) over its hunks in unified or split view.
|
|
15
|
+
* A hunk marked `isReplacedBlock`, and a file with more than
|
|
16
|
+
* MAX_UNFOLDED_CHANGED_LINES other changed lines, show a labeled fold until
|
|
17
|
+
* the reader opens it.
|
|
15
18
|
* Unified view is one table. Split view is two side-by-side panes (each an
|
|
16
19
|
* ARIA table) whose rows share one grid's row tracks through `subgrid`, so a
|
|
17
20
|
* row is as tall as its taller side while each pane scrolls sideways on its
|
|
@@ -39,12 +42,13 @@ import { Fragment, useId, useInsertionEffect, useMemo, useRef, useState } from '
|
|
|
39
42
|
import * as stylex from '@stylexjs/stylex';
|
|
40
43
|
import { ensureHighlightStyles, tokenize } from '@astryxdesign/core/CodeBlock';
|
|
41
44
|
import { useScrollableArea } from '@astryxdesign/core/hooks';
|
|
45
|
+
import { Button } from '@astryxdesign/core/Button';
|
|
42
46
|
import { useTranslator } from '@astryxdesign/core/i18n';
|
|
43
47
|
import { Icon } from '@astryxdesign/core/Icon';
|
|
44
48
|
import { SyntaxTheme } from '@astryxdesign/core/theme/syntax';
|
|
45
49
|
import '@astryxdesign/core/theme/tokens.stylex';
|
|
46
50
|
import { borderVars, colorVars, durationVars, easeVars, fontWeightVars, radiusVars, spacingVars, typeScaleVars, typographyVars } from '@astryxdesign/core/theme/tokens.stylex';
|
|
47
|
-
import { focusOutlineProps, mergeProps, rtlStyles, themeProps } from '@astryxdesign/core/utils';
|
|
51
|
+
import { focusOutlineProps, isRenderable, mergeProps, rtlStyles, themeProps } from '@astryxdesign/core/utils';
|
|
48
52
|
import { Token } from '@astryxdesign/core/Token';
|
|
49
53
|
import { VisuallyHidden } from '@astryxdesign/core/VisuallyHidden';
|
|
50
54
|
import { diffInlineRanges } from "./inlineDiff.js";
|
|
@@ -134,6 +138,17 @@ function inferLanguage(path, codeLanguages) {
|
|
|
134
138
|
// Row model
|
|
135
139
|
// ---------------------------------------------------------------------------
|
|
136
140
|
|
|
141
|
+
/** The control that folds a replaced block's changed lines. */
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* The caller's annotation for a line, or null when it renders nothing. An
|
|
145
|
+
* annotation row is drawn only for a note that renders, so
|
|
146
|
+
* `hasThread && <Thread />` adds no empty row.
|
|
147
|
+
*/
|
|
148
|
+
function lineAnnotation(renderLineAnnotation, ref) {
|
|
149
|
+
const note = renderLineAnnotation(ref);
|
|
150
|
+
return isRenderable(note) ? note : null;
|
|
151
|
+
}
|
|
137
152
|
function lineRef(line, hunkIndex) {
|
|
138
153
|
if (line.kind === 'removed') {
|
|
139
154
|
return line.oldLineNumber == null ? null : {
|
|
@@ -181,44 +196,129 @@ function addInlineEmphasis(cells, granularity) {
|
|
|
181
196
|
}
|
|
182
197
|
|
|
183
198
|
/**
|
|
184
|
-
*
|
|
185
|
-
*
|
|
186
|
-
*
|
|
199
|
+
* Most changed lines a file shows before the reader asks for them. Rows are
|
|
200
|
+
* not virtualized, and each row costs render time. A file with more changed
|
|
201
|
+
* lines starts folded. Lines in replaced blocks do not count, because those
|
|
202
|
+
* blocks fold on their own.
|
|
187
203
|
*/
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
} else {
|
|
205
|
-
oldIndex++;
|
|
206
|
-
tokens = newTokens[newIndex++] ?? [];
|
|
204
|
+
const MAX_UNFOLDED_CHANGED_LINES = 2000;
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Hunks with an annotation on one of their lines. A fold that would hide such
|
|
208
|
+
* a line starts open, so a review comment is never hidden.
|
|
209
|
+
*/
|
|
210
|
+
function findAnnotatedHunks(file, renderLineAnnotation) {
|
|
211
|
+
const hunks = new Set();
|
|
212
|
+
if (renderLineAnnotation == null) {
|
|
213
|
+
return hunks;
|
|
214
|
+
}
|
|
215
|
+
file.hunks.forEach((hunk, hunkIndex) => {
|
|
216
|
+
const isAnnotated = hunk.lines.some(line => {
|
|
217
|
+
// A replaced block's fold keeps its context lines in view.
|
|
218
|
+
if (hunk.isReplacedBlock === true && line.kind === 'context') {
|
|
219
|
+
return false;
|
|
207
220
|
}
|
|
208
221
|
const ref = lineRef(line, hunkIndex);
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
222
|
+
return ref != null && lineAnnotation(renderLineAnnotation, ref) != null;
|
|
223
|
+
});
|
|
224
|
+
if (isAnnotated) {
|
|
225
|
+
hunks.add(hunkIndex);
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
return hunks;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Tokenize a run of hunk lines with its old and new side each as continuous
|
|
233
|
+
* text, so constructs that span lines (block comments, template strings) keep
|
|
234
|
+
* their paint within the run, and give each line the tokens of the side it
|
|
235
|
+
* renders from.
|
|
236
|
+
*/
|
|
237
|
+
function tokenizeRun(lines, language) {
|
|
238
|
+
if (language === 'plaintext') {
|
|
239
|
+
return lines.map(() => []);
|
|
240
|
+
}
|
|
241
|
+
const sideText = kind => lines.filter(line => line.kind !== kind).map(line => line.content).join('\n');
|
|
242
|
+
const oldTokens = tokenize(sideText('added'), language);
|
|
243
|
+
const newTokens = tokenize(sideText('removed'), language);
|
|
244
|
+
let oldIndex = 0;
|
|
245
|
+
let newIndex = 0;
|
|
246
|
+
return lines.map(line => {
|
|
247
|
+
if (line.kind === 'removed') {
|
|
248
|
+
return oldTokens[oldIndex++] ?? [];
|
|
249
|
+
}
|
|
250
|
+
if (line.kind === 'added') {
|
|
251
|
+
return newTokens[newIndex++] ?? [];
|
|
252
|
+
}
|
|
253
|
+
oldIndex++;
|
|
254
|
+
return newTokens[newIndex++] ?? [];
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Build each hunk's line cells. A folded replaced block keeps only the
|
|
260
|
+
* context around it, and its changed lines are not tokenized.
|
|
261
|
+
*/
|
|
262
|
+
function buildCells(file, language, inlineHighlight, expandedBlocks) {
|
|
263
|
+
return file.hunks.map((hunk, hunkIndex) => {
|
|
264
|
+
const cells = [];
|
|
265
|
+
const addRun = lines => {
|
|
266
|
+
const tokens = tokenizeRun(lines, language);
|
|
267
|
+
lines.forEach((line, i) => {
|
|
268
|
+
const ref = lineRef(line, hunkIndex);
|
|
269
|
+
if (ref != null) {
|
|
270
|
+
cells.push({
|
|
271
|
+
line,
|
|
272
|
+
ref,
|
|
273
|
+
tokens: tokens[i],
|
|
274
|
+
emphasis: []
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
};
|
|
279
|
+
const changeStart = hunk.isReplacedBlock === true ? hunk.lines.findIndex(line => line.kind !== 'context') : -1;
|
|
280
|
+
if (changeStart === -1) {
|
|
281
|
+
addRun(hunk.lines);
|
|
282
|
+
if (inlineHighlight !== 'none') {
|
|
283
|
+
addInlineEmphasis(cells, inlineHighlight);
|
|
216
284
|
}
|
|
285
|
+
return {
|
|
286
|
+
cells,
|
|
287
|
+
fold: null
|
|
288
|
+
};
|
|
217
289
|
}
|
|
218
|
-
|
|
219
|
-
|
|
290
|
+
|
|
291
|
+
// A replaced block's removed and added lines are not edits of each
|
|
292
|
+
// other, so they get no inline highlight.
|
|
293
|
+
const isExpanded = expandedBlocks.has(hunkIndex);
|
|
294
|
+
const changedLines = hunk.lines.filter(line => line.kind !== 'context').length;
|
|
295
|
+
if (isExpanded) {
|
|
296
|
+
addRun(hunk.lines);
|
|
297
|
+
const at = cells.findIndex(cell => cell.line.kind !== 'context');
|
|
298
|
+
return {
|
|
299
|
+
cells,
|
|
300
|
+
fold: {
|
|
301
|
+
hunkIndex,
|
|
302
|
+
at,
|
|
303
|
+
changedLines,
|
|
304
|
+
isExpanded
|
|
305
|
+
}
|
|
306
|
+
};
|
|
220
307
|
}
|
|
221
|
-
|
|
308
|
+
// Leading and trailing context are tokenized apart: the hidden lines
|
|
309
|
+
// between them can open or close a comment or a string.
|
|
310
|
+
addRun(hunk.lines.slice(0, changeStart));
|
|
311
|
+
const at = cells.length;
|
|
312
|
+
addRun(hunk.lines.slice(changeStart + changedLines));
|
|
313
|
+
return {
|
|
314
|
+
cells,
|
|
315
|
+
fold: {
|
|
316
|
+
hunkIndex,
|
|
317
|
+
at,
|
|
318
|
+
changedLines,
|
|
319
|
+
isExpanded
|
|
320
|
+
}
|
|
321
|
+
};
|
|
222
322
|
});
|
|
223
323
|
}
|
|
224
324
|
function buildUnifiedRows(file, cells) {
|
|
@@ -229,13 +329,31 @@ function buildUnifiedRows(file, cells) {
|
|
|
229
329
|
key: `h${h}`,
|
|
230
330
|
header: hunk.header
|
|
231
331
|
});
|
|
232
|
-
|
|
332
|
+
const {
|
|
333
|
+
cells: hunkCells,
|
|
334
|
+
fold
|
|
335
|
+
} = cells[h];
|
|
336
|
+
hunkCells.forEach((cell, i) => {
|
|
337
|
+
if (fold != null && fold.at === i) {
|
|
338
|
+
rows.push({
|
|
339
|
+
type: 'fold',
|
|
340
|
+
key: `h${h}f`,
|
|
341
|
+
fold
|
|
342
|
+
});
|
|
343
|
+
}
|
|
233
344
|
rows.push({
|
|
234
345
|
type: 'line',
|
|
235
346
|
key: `h${h}l${i}`,
|
|
236
347
|
cell
|
|
237
348
|
});
|
|
238
349
|
});
|
|
350
|
+
if (fold != null && fold.at === hunkCells.length) {
|
|
351
|
+
rows.push({
|
|
352
|
+
type: 'fold',
|
|
353
|
+
key: `h${h}f`,
|
|
354
|
+
fold
|
|
355
|
+
});
|
|
356
|
+
}
|
|
239
357
|
});
|
|
240
358
|
return rows;
|
|
241
359
|
}
|
|
@@ -252,9 +370,20 @@ function buildSplitRows(file, cells) {
|
|
|
252
370
|
key: `h${h}`,
|
|
253
371
|
header: hunk.header
|
|
254
372
|
});
|
|
255
|
-
const
|
|
373
|
+
const {
|
|
374
|
+
cells: hunkCells,
|
|
375
|
+
fold
|
|
376
|
+
} = cells[h];
|
|
256
377
|
let i = 0;
|
|
257
378
|
while (i < hunkCells.length) {
|
|
379
|
+
// The fold sits where a run of changes starts or ends, never inside one.
|
|
380
|
+
if (fold != null && fold.at === i) {
|
|
381
|
+
rows.push({
|
|
382
|
+
type: 'fold',
|
|
383
|
+
key: `h${h}f`,
|
|
384
|
+
fold
|
|
385
|
+
});
|
|
386
|
+
}
|
|
258
387
|
const cell = hunkCells[i];
|
|
259
388
|
if (cell.line.kind === 'context') {
|
|
260
389
|
rows.push({
|
|
@@ -284,6 +413,13 @@ function buildSplitRows(file, cells) {
|
|
|
284
413
|
});
|
|
285
414
|
}
|
|
286
415
|
}
|
|
416
|
+
if (fold != null && fold.at === hunkCells.length) {
|
|
417
|
+
rows.push({
|
|
418
|
+
type: 'fold',
|
|
419
|
+
key: `h${h}f`,
|
|
420
|
+
fold
|
|
421
|
+
});
|
|
422
|
+
}
|
|
287
423
|
});
|
|
288
424
|
return rows;
|
|
289
425
|
}
|
|
@@ -764,9 +900,53 @@ export function DiffViewer({
|
|
|
764
900
|
}
|
|
765
901
|
onCollapsedChange?.(next);
|
|
766
902
|
};
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
903
|
+
let unfoldedChangedLines = 0;
|
|
904
|
+
let hasReplacedBlock = false;
|
|
905
|
+
for (const hunk of file.hunks) {
|
|
906
|
+
if (hunk.isReplacedBlock === true) {
|
|
907
|
+
hasReplacedBlock = true;
|
|
908
|
+
continue;
|
|
909
|
+
}
|
|
910
|
+
for (const line of hunk.lines) {
|
|
911
|
+
if (line.kind !== 'context') {
|
|
912
|
+
unfoldedChangedLines++;
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
const isLargeFile = unfoldedChangedLines > MAX_UNFOLDED_CHANGED_LINES;
|
|
917
|
+
const annotatedHunks = isLargeFile || hasReplacedBlock ? findAnnotatedHunks(file, renderLineAnnotation) : null;
|
|
918
|
+
|
|
919
|
+
// A large file and a replaced block start folded unless a line they would
|
|
920
|
+
// hide is annotated. The reader's toggles are kept per file; a new file
|
|
921
|
+
// starts over.
|
|
922
|
+
const [fileToggle, setFileToggle] = useState(null);
|
|
923
|
+
const isFileFolded = isLargeFile && !(fileToggle?.file === file ? fileToggle.isExpanded : (annotatedHunks?.size ?? 0) > 0);
|
|
924
|
+
const toggleFile = () => {
|
|
925
|
+
setFileToggle({
|
|
926
|
+
file,
|
|
927
|
+
isExpanded: isFileFolded
|
|
928
|
+
});
|
|
929
|
+
};
|
|
930
|
+
const [blockToggles, setBlockToggles] = useState(null);
|
|
931
|
+
const toggles = blockToggles?.file === file ? blockToggles.isExpanded : null;
|
|
932
|
+
const isBlockExpanded = hunkIndex => toggles?.get(hunkIndex) ?? annotatedHunks?.has(hunkIndex) ?? false;
|
|
933
|
+
const toggleBlock = hunkIndex => {
|
|
934
|
+
const next = new Map(toggles);
|
|
935
|
+
next.set(hunkIndex, !isBlockExpanded(hunkIndex));
|
|
936
|
+
setBlockToggles({
|
|
937
|
+
file,
|
|
938
|
+
isExpanded: next
|
|
939
|
+
});
|
|
940
|
+
};
|
|
941
|
+
// A key of the open blocks keeps the set stable across renders, so the
|
|
942
|
+
// cells are built again only when a block opens or closes.
|
|
943
|
+
const expandedKey = file.hunks.flatMap((hunk, hunkIndex) => hunk.isReplacedBlock === true && isBlockExpanded(hunkIndex) ? [hunkIndex] : []).join(',');
|
|
944
|
+
const expandedBlocks = useMemo(() => new Set(expandedKey === '' ? [] : expandedKey.split(',').map(Number)), [expandedKey]);
|
|
945
|
+
|
|
946
|
+
// A folded file builds no cells, so its lines are not tokenized.
|
|
947
|
+
const cells = useMemo(() => isFileFolded ? [] : buildCells(file, language, inlineHighlight, expandedBlocks), [isFileFolded, file, language, inlineHighlight, expandedBlocks]);
|
|
948
|
+
const unifiedRows = useMemo(() => viewMode === 'unified' && !isFileFolded ? buildUnifiedRows(file, cells) : [], [viewMode, isFileFolded, file, cells]);
|
|
949
|
+
const splitRows = useMemo(() => viewMode === 'split' && !isFileFolded ? buildSplitRows(file, cells) : [], [viewMode, isFileFolded, file, cells]);
|
|
770
950
|
let addedCount = 0;
|
|
771
951
|
let removedCount = 0;
|
|
772
952
|
let maxLineNumber = 1;
|
|
@@ -875,6 +1055,28 @@ export function DiffViewer({
|
|
|
875
1055
|
children: cell != null && buildSpans(cell.line.content, cell.tokens, cell.emphasis, [styles.change, kind === 'added' ? styles.changeAdded : styles.changeRemoved])
|
|
876
1056
|
}, key);
|
|
877
1057
|
};
|
|
1058
|
+
const renderFoldControl = (changedLines, isExpanded, onToggle, isSticky) => /*#__PURE__*/_jsxs("span", {
|
|
1059
|
+
...{
|
|
1060
|
+
0: {
|
|
1061
|
+
className: "x3nfvp2 x6s0dn4 x1a02dak xjcht0a"
|
|
1062
|
+
},
|
|
1063
|
+
1: {
|
|
1064
|
+
className: "x3nfvp2 x6s0dn4 x1a02dak xjcht0a x7wzq59 x1ybfrjj"
|
|
1065
|
+
}
|
|
1066
|
+
}[!!isSticky << 0],
|
|
1067
|
+
children: [/*#__PURE__*/_jsx("span", {
|
|
1068
|
+
children: t('@astryx.diffViewer.largeChange', {
|
|
1069
|
+
count: changedLines
|
|
1070
|
+
})
|
|
1071
|
+
}), /*#__PURE__*/_jsx(Button, {
|
|
1072
|
+
size: "sm",
|
|
1073
|
+
variant: "secondary",
|
|
1074
|
+
label: isExpanded ? t('@astryx.diffViewer.hideFullDiff') : t('@astryx.diffViewer.showFullDiff'),
|
|
1075
|
+
"aria-expanded": isExpanded,
|
|
1076
|
+
onClick: onToggle
|
|
1077
|
+
})]
|
|
1078
|
+
});
|
|
1079
|
+
const renderFold = fold => renderFoldControl(fold.changedLines, fold.isExpanded, () => toggleBlock(fold.hunkIndex), isPinned);
|
|
878
1080
|
const renderHunkText = header => /*#__PURE__*/_jsx("span", {
|
|
879
1081
|
...{
|
|
880
1082
|
0: {
|
|
@@ -886,16 +1088,7 @@ export function DiffViewer({
|
|
|
886
1088
|
}[!!isPinned << 0],
|
|
887
1089
|
children: header
|
|
888
1090
|
});
|
|
889
|
-
|
|
890
|
-
// `null`, `undefined` and booleans render nothing in React, so they draw no
|
|
891
|
-
// row either; that keeps `hasThread && <Thread />` from adding empty rows.
|
|
892
|
-
const annotationFor = cell => {
|
|
893
|
-
if (cell == null || renderLineAnnotation == null) {
|
|
894
|
-
return null;
|
|
895
|
-
}
|
|
896
|
-
const note = renderLineAnnotation(cell.ref);
|
|
897
|
-
return typeof note === 'boolean' ? null : note;
|
|
898
|
-
};
|
|
1091
|
+
const annotationFor = cell => cell == null || renderLineAnnotation == null ? null : lineAnnotation(renderLineAnnotation, cell.ref);
|
|
899
1092
|
|
|
900
1093
|
// Name every column so a screen reader moving across a row hears which
|
|
901
1094
|
// number is which (and, in split view, which side's code it is in).
|
|
@@ -955,6 +1148,17 @@ export function DiffViewer({
|
|
|
955
1148
|
})
|
|
956
1149
|
}, row.key);
|
|
957
1150
|
}
|
|
1151
|
+
if (row.type === 'fold') {
|
|
1152
|
+
return /*#__PURE__*/_jsx("tr", {
|
|
1153
|
+
children: /*#__PURE__*/_jsx("td", {
|
|
1154
|
+
colSpan: columns,
|
|
1155
|
+
...{
|
|
1156
|
+
className: "xce4md1 xrrkdod x1yc453h x11ds9ch xv1l7n4 x9ynric x141an7d x1ltkj2j xszpfcc xykt701 xgtxpae"
|
|
1157
|
+
},
|
|
1158
|
+
children: renderFold(row.fold)
|
|
1159
|
+
})
|
|
1160
|
+
}, row.key);
|
|
1161
|
+
}
|
|
958
1162
|
const note = annotationFor(row.cell);
|
|
959
1163
|
return /*#__PURE__*/_jsxs(Fragment, {
|
|
960
1164
|
children: [/*#__PURE__*/_jsxs("tr", {
|
|
@@ -1021,7 +1225,7 @@ export function DiffViewer({
|
|
|
1021
1225
|
};
|
|
1022
1226
|
};
|
|
1023
1227
|
const renderSplit = () => {
|
|
1024
|
-
const items = splitRows.map(row => row.type
|
|
1228
|
+
const items = splitRows.map(row => row.type !== 'pair' ? {
|
|
1025
1229
|
row,
|
|
1026
1230
|
oldNote: null,
|
|
1027
1231
|
newNote: null
|
|
@@ -1033,7 +1237,7 @@ export function DiffViewer({
|
|
|
1033
1237
|
newNote: annotationFor(row.new)
|
|
1034
1238
|
});
|
|
1035
1239
|
const hasSizer = !isWrapped;
|
|
1036
|
-
const sizerText = hasSizer ? widestLine(cells.flatMap(
|
|
1240
|
+
const sizerText = hasSizer ? widestLine(cells.flatMap(hunk => hunk.cells.map(cell => cell.line.content))) : '';
|
|
1037
1241
|
const rowCount = 1 + (hasSizer ? 1 : 0) + items.reduce((count, item) => count + 1 + (item.oldNote != null || item.newNote != null ? 1 : 0), 0);
|
|
1038
1242
|
const columns = `${hasLineNumbers ? `${gutter} ` : ''}${markerWidth} ${isWrapped ? 'minmax(0, 1fr)' : '1fr'}`;
|
|
1039
1243
|
const markerPin = hasLineNumbers ? gutter : '0px';
|
|
@@ -1084,6 +1288,41 @@ export function DiffViewer({
|
|
|
1084
1288
|
}, row.key));
|
|
1085
1289
|
continue;
|
|
1086
1290
|
}
|
|
1291
|
+
if (row.type === 'fold') {
|
|
1292
|
+
// Only the old side holds the control; the new side keeps its row
|
|
1293
|
+
// as tall and gives its own table's readers the label.
|
|
1294
|
+
rows.push(/*#__PURE__*/_jsx("div", {
|
|
1295
|
+
role: "row",
|
|
1296
|
+
...{
|
|
1297
|
+
className: "xrvj5dj x1osaytk x4w6khx"
|
|
1298
|
+
},
|
|
1299
|
+
children: /*#__PURE__*/_jsx("div", {
|
|
1300
|
+
role: "cell",
|
|
1301
|
+
...{
|
|
1302
|
+
0: {
|
|
1303
|
+
className: "xce4md1 xrrkdod x1yc453h x11ds9ch xv1l7n4 x9ynric x141an7d x1ltkj2j xszpfcc xykt701 xgtxpae x1osaytk"
|
|
1304
|
+
},
|
|
1305
|
+
1: {
|
|
1306
|
+
className: "xce4md1 xrrkdod x1yc453h x11ds9ch xv1l7n4 x9ynric x141an7d x1ltkj2j xszpfcc xykt701 xgtxpae x1osaytk x126k92a x1lldw8n xj0a0fe x5ujdhh"
|
|
1307
|
+
}
|
|
1308
|
+
}[!!isWrapped << 0],
|
|
1309
|
+
children: isOld ? renderFold(row.fold) : /*#__PURE__*/_jsxs(_Fragment, {
|
|
1310
|
+
children: [/*#__PURE__*/_jsx("span", {
|
|
1311
|
+
"aria-hidden": "true",
|
|
1312
|
+
...{
|
|
1313
|
+
className: "xlshs6z"
|
|
1314
|
+
},
|
|
1315
|
+
children: renderFold(row.fold)
|
|
1316
|
+
}), /*#__PURE__*/_jsx(VisuallyHidden, {
|
|
1317
|
+
children: t('@astryx.diffViewer.largeChange', {
|
|
1318
|
+
count: row.fold.changedLines
|
|
1319
|
+
})
|
|
1320
|
+
})]
|
|
1321
|
+
})
|
|
1322
|
+
})
|
|
1323
|
+
}, row.key));
|
|
1324
|
+
continue;
|
|
1325
|
+
}
|
|
1087
1326
|
const cell = isOld ? row.old : row.new;
|
|
1088
1327
|
rows.push(/*#__PURE__*/_jsxs("div", {
|
|
1089
1328
|
role: "row"
|
|
@@ -1149,7 +1388,6 @@ export function DiffViewer({
|
|
|
1149
1388
|
children: [renderSide('old'), renderSide('new')]
|
|
1150
1389
|
});
|
|
1151
1390
|
};
|
|
1152
|
-
const table = viewMode === 'split' ? renderSplit() : renderUnified();
|
|
1153
1391
|
let body;
|
|
1154
1392
|
if (file.isBinary) {
|
|
1155
1393
|
body = /*#__PURE__*/_jsx("div", {
|
|
@@ -1166,17 +1404,27 @@ export function DiffViewer({
|
|
|
1166
1404
|
children: t('@astryx.diffViewer.noChanges')
|
|
1167
1405
|
});
|
|
1168
1406
|
} else {
|
|
1169
|
-
|
|
1170
|
-
|
|
1407
|
+
// The fold row stays first whether the file is folded or not, so its
|
|
1408
|
+
// button keeps focus when it toggles.
|
|
1409
|
+
body = /*#__PURE__*/_jsxs(_Fragment, {
|
|
1410
|
+
children: [isLargeFile && /*#__PURE__*/_jsx("div", {
|
|
1411
|
+
"data-diff-fold": "",
|
|
1171
1412
|
...{
|
|
1172
|
-
className: "
|
|
1413
|
+
className: "xce4md1 xrrkdod x1yc453h x11ds9ch xv1l7n4 x9ynric x141an7d x1ltkj2j xszpfcc xykt701 xgtxpae"
|
|
1173
1414
|
},
|
|
1174
|
-
|
|
1175
|
-
})
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1415
|
+
children: renderFoldControl(unfoldedChangedLines, !isFileFolded, toggleFile, false)
|
|
1416
|
+
}), !isFileFolded && /*#__PURE__*/_jsx("div", {
|
|
1417
|
+
...withoutInlineOverscroll(getViewportProps({
|
|
1418
|
+
...{
|
|
1419
|
+
className: "xeuugli xzt5al7"
|
|
1420
|
+
},
|
|
1421
|
+
style: scrollStyle
|
|
1422
|
+
})),
|
|
1423
|
+
children: /*#__PURE__*/_jsx("div", {
|
|
1424
|
+
...getContentProps({}),
|
|
1425
|
+
children: viewMode === 'split' ? renderSplit() : renderUnified()
|
|
1426
|
+
})
|
|
1427
|
+
})]
|
|
1180
1428
|
});
|
|
1181
1429
|
}
|
|
1182
1430
|
|
|
@@ -6,11 +6,6 @@
|
|
|
6
6
|
* @position Lab utility; computes a line diff (Myers) when the caller has two
|
|
7
7
|
* texts rather than a patch
|
|
8
8
|
*
|
|
9
|
-
* Shared leading and trailing lines are matched without a search. The minimal
|
|
10
|
-
* search over the rest stops after MAX_EDIT_DISTANCE changed lines, so memory
|
|
11
|
-
* stays bounded for any input; a middle that differs more than that is shown
|
|
12
|
-
* as one replaced block rather than a minimal diff.
|
|
13
|
-
*
|
|
14
9
|
* SYNC: When modified, update:
|
|
15
10
|
* - /packages/lab/src/DiffViewer/createDiffFile.test.ts
|
|
16
11
|
* - /packages/lab/src/DiffViewer/DiffViewer.doc.mjs (utilities section)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"createDiffFile.d.ts","sourceRoot":"","sources":["../../src/DiffViewer/createDiffFile.ts"],"names":[],"mappings":"AAEA
|
|
1
|
+
{"version":3,"file":"createDiffFile.d.ts","sourceRoot":"","sources":["../../src/DiffViewer/createDiffFile.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EACV,cAAc,EAIf,MAAM,aAAa,CAAC;AAErB,MAAM,WAAW,qBAAqB;IACpC,yBAAyB;IACzB,QAAQ,EAAE,MAAM,CAAC;IACjB,wBAAwB;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB;;;OAGG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,qEAAqE;IACrE,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,sEAAsE;IACtE,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAqZD;;;;;;;;;GASG;AACH,wBAAgB,cAAc,CAAC,EAC7B,QAAQ,EACR,QAAQ,EACR,IAAI,EACJ,OAAO,EAAE,aAAa,EACtB,OAAO,EAAE,aAAa,EACtB,YAAgB,GACjB,EAAE,qBAAqB,GAAG,cAAc,CA4GxC"}
|