xtralab 0.4.0 → 0.5.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 +9 -4
- package/lib/git/commands.d.ts +12 -13
- package/lib/git/commands.js +7 -28
- package/lib/git/diffProvider.d.ts +8 -0
- package/lib/git/diffProvider.js +314 -0
- package/lib/git/diffSurface.d.ts +115 -0
- package/lib/git/diffSurface.js +342 -0
- package/lib/git/diffWidget.d.ts +8 -13
- package/lib/git/diffWidget.js +65 -311
- package/lib/git/imageDiff.d.ts +23 -0
- package/lib/git/imageDiff.js +184 -0
- package/lib/git/index.d.ts +10 -8
- package/lib/git/index.js +37 -52
- package/lib/index.d.ts +3 -2
- package/lib/index.js +5 -4
- package/package.json +18 -4
- package/src/git/commands.ts +12 -38
- package/src/git/diffProvider.tsx +462 -0
- package/src/git/diffSurface.tsx +588 -0
- package/src/git/diffWidget.tsx +105 -465
- package/src/git/imageDiff.tsx +368 -0
- package/src/git/index.ts +38 -68
- package/src/index.ts +5 -4
- package/style/git.css +158 -266
- package/lib/git/icons.d.ts +0 -14
- package/lib/git/icons.js +0 -19
- package/lib/git/panel.d.ts +0 -29
- package/lib/git/panel.js +0 -42
- package/lib/git/panelComponent.d.ts +0 -28
- package/lib/git/panelComponent.js +0 -320
- package/src/git/icons.ts +0 -20
- package/src/git/panel.tsx +0 -56
- package/src/git/panelComponent.tsx +0 -575
package/lib/git/diffWidget.js
CHANGED
|
@@ -1,100 +1,17 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
2
|
import { MainAreaWidget } from '@jupyterlab/apputils';
|
|
3
3
|
import { PathExt } from '@jupyterlab/coreutils';
|
|
4
|
-
import { ReactWidget, ToolbarButton, launchIcon
|
|
4
|
+
import { ReactWidget, ToolbarButton, launchIcon } from '@jupyterlab/ui-components';
|
|
5
5
|
import { Signal } from '@lumino/signaling';
|
|
6
|
-
import { FileDiff } from '@pierre/diffs/react';
|
|
7
|
-
import { diffAcceptRejectHunk, parseDiffFromFile } from '@pierre/diffs';
|
|
8
6
|
import { getTreeIcon } from '../fileBrowser/icons';
|
|
9
7
|
import { content } from './api';
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
* style the embedded `@pierre/diffs` viewer hang off this class.
|
|
14
|
-
*/
|
|
15
|
-
export const DIFF_WIDGET_CSS_CLASS = 'jp-xtralab-DiffWidget';
|
|
8
|
+
import { imageDataType } from './imageDiff';
|
|
9
|
+
import { DIFF_WIDGET_CSS_CLASS, DiffSurface, NotebookViewModeControl, isDarkTheme, readStoredNotebookViewMode, writeStoredNotebookViewMode } from './diffSurface';
|
|
10
|
+
export { DIFF_WIDGET_CSS_CLASS };
|
|
16
11
|
export const PREVIEW_DIFF_WIDGET_ID = 'xtralab:diff:preview';
|
|
17
12
|
export function pinnedDiffWidgetId(change) {
|
|
18
13
|
return `xtralab:diff:pinned:${change.group}:${change.path}`;
|
|
19
14
|
}
|
|
20
|
-
/**
|
|
21
|
-
* `localStorage` key for the user's last-chosen split ratio. Persisted so
|
|
22
|
-
* the preference survives reloads — the library has no built-in way to
|
|
23
|
-
* remember per-host UI state, and reopening every diff at 50/50 is
|
|
24
|
-
* annoying once the user has dialed in a layout they like.
|
|
25
|
-
*/
|
|
26
|
-
const SPLIT_RATIO_STORAGE_KEY = 'xtralab:diff-split-ratio';
|
|
27
|
-
/**
|
|
28
|
-
* Bounds on the left-pane fraction. Keeping a few percent on either side
|
|
29
|
-
* guarantees both panes remain visible — dragging to 0/100 would hide a
|
|
30
|
-
* column and is almost never what the user wants.
|
|
31
|
-
*/
|
|
32
|
-
const MIN_SPLIT_RATIO = 0.1;
|
|
33
|
-
const MAX_SPLIT_RATIO = 0.9;
|
|
34
|
-
const DEFAULT_SPLIT_RATIO = 0.5;
|
|
35
|
-
/**
|
|
36
|
-
* CSS injected via the `@pierre/diffs` `unsafeCSS` option. The library
|
|
37
|
-
* places it inside the shadow root under `@layer unsafe`, which beats
|
|
38
|
-
* the library's own `@layer base` rule that hardcodes
|
|
39
|
-
* `grid-template-columns: 1fr 1fr` on the split-mode container.
|
|
40
|
-
*
|
|
41
|
-
* The override reads from `--xtralab-split-cols`, a custom property we set
|
|
42
|
-
* on the host element via the `style` prop. Custom properties inherit
|
|
43
|
-
* through the shadow DOM boundary, so adjusting it on the host instantly
|
|
44
|
-
* resizes the columns without re-rendering the diff.
|
|
45
|
-
*/
|
|
46
|
-
const SPLIT_RESIZE_CSS = `pre[data-diff-type="split"][data-overflow="scroll"] {
|
|
47
|
-
grid-template-columns: var(--xtralab-split-cols, 1fr 1fr);
|
|
48
|
-
}`;
|
|
49
|
-
function readStoredSplitRatio() {
|
|
50
|
-
try {
|
|
51
|
-
const raw = window.localStorage.getItem(SPLIT_RATIO_STORAGE_KEY);
|
|
52
|
-
if (raw === null) {
|
|
53
|
-
return DEFAULT_SPLIT_RATIO;
|
|
54
|
-
}
|
|
55
|
-
const parsed = Number.parseFloat(raw);
|
|
56
|
-
if (Number.isFinite(parsed) &&
|
|
57
|
-
parsed >= MIN_SPLIT_RATIO &&
|
|
58
|
-
parsed <= MAX_SPLIT_RATIO) {
|
|
59
|
-
return parsed;
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
catch (_a) {
|
|
63
|
-
// localStorage may throw in privacy mode or sandboxed contexts. Falling
|
|
64
|
-
// back to the default is fine — the user can resize again on this run.
|
|
65
|
-
}
|
|
66
|
-
return DEFAULT_SPLIT_RATIO;
|
|
67
|
-
}
|
|
68
|
-
function writeStoredSplitRatio(ratio) {
|
|
69
|
-
try {
|
|
70
|
-
window.localStorage.setItem(SPLIT_RATIO_STORAGE_KEY, ratio.toString());
|
|
71
|
-
}
|
|
72
|
-
catch (_a) {
|
|
73
|
-
// See readStoredSplitRatio — best-effort persistence.
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
const NOTEBOOK_DIFF_VIEW_MODE_STORAGE_KEY = 'xtralab:notebook-diff-view-mode';
|
|
77
|
-
function readStoredNotebookViewMode() {
|
|
78
|
-
try {
|
|
79
|
-
const raw = window.localStorage.getItem(NOTEBOOK_DIFF_VIEW_MODE_STORAGE_KEY);
|
|
80
|
-
if (raw === 'json' || raw === 'notebook') {
|
|
81
|
-
return raw;
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
catch (_a) {
|
|
85
|
-
// See readStoredSplitRatio — privacy-mode / sandboxed contexts can
|
|
86
|
-
// throw on access. The notebook view is the better default anyway.
|
|
87
|
-
}
|
|
88
|
-
return 'notebook';
|
|
89
|
-
}
|
|
90
|
-
function writeStoredNotebookViewMode(mode) {
|
|
91
|
-
try {
|
|
92
|
-
window.localStorage.setItem(NOTEBOOK_DIFF_VIEW_MODE_STORAGE_KEY, mode);
|
|
93
|
-
}
|
|
94
|
-
catch (_a) {
|
|
95
|
-
// Best-effort.
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
15
|
/**
|
|
99
16
|
* Build the deterministic widget id used for a diff against a particular
|
|
100
17
|
* file/group pair. Reused across plugin and command code so reopening a
|
|
@@ -133,6 +50,12 @@ export function resolveReferences(change) {
|
|
|
133
50
|
* usual document chrome — toolbar slot, focus tracker integration, and
|
|
134
51
|
* spinner-while-loading hooks — and matches the pattern used for other
|
|
135
52
|
* read-only main-area widgets in the JupyterLab ecosystem.
|
|
53
|
+
*
|
|
54
|
+
* This is the launcher dashboard's own diff path
|
|
55
|
+
* (`xtralab:git:open-diff`), kept independent of `jupyterlab-git`. The
|
|
56
|
+
* actual diff rendering is delegated to the shared {@link DiffSurface};
|
|
57
|
+
* `jupyterlab-git`'s panel reaches the same surface through the providers
|
|
58
|
+
* registered in `diffProvider.tsx`.
|
|
136
59
|
*/
|
|
137
60
|
export function createGitDiffWidget(options) {
|
|
138
61
|
var _a;
|
|
@@ -310,50 +233,17 @@ export class DiffContentWidget extends ReactWidget {
|
|
|
310
233
|
return React.createElement(DiffViewer, { ...this._options, widget: this });
|
|
311
234
|
}
|
|
312
235
|
}
|
|
313
|
-
/**
|
|
314
|
-
* Detect notebook files by extension. We only look at the suffix so the
|
|
315
|
-
* caller (the file-change tracker) doesn't need to plumb a content-type
|
|
316
|
-
* field through; mistaking another `.ipynb`-named file for a notebook is
|
|
317
|
-
* harmless because {@link buildNotebookDiff} validates the JSON shape and
|
|
318
|
-
* we fall back to the file diff if parsing fails.
|
|
319
|
-
*/
|
|
320
|
-
function isNotebookPath(path) {
|
|
321
|
-
return path.toLowerCase().endsWith('.ipynb');
|
|
322
|
-
}
|
|
323
|
-
/**
|
|
324
|
-
* Decide whether the active JupyterLab theme is a dark theme. Prefers the
|
|
325
|
-
* `IThemeManager` token when available, and falls back to sniffing the
|
|
326
|
-
* `data-jp-theme-light` attribute that JupyterLab sets on `<body>` so the
|
|
327
|
-
* component still works in stripped-down hosts where the token isn't
|
|
328
|
-
* provided (lite, certain notebook configurations, …).
|
|
329
|
-
*/
|
|
330
|
-
function isDarkTheme(themeManager) {
|
|
331
|
-
if (themeManager !== null && themeManager.theme !== null) {
|
|
332
|
-
return !themeManager.isLight(themeManager.theme);
|
|
333
|
-
}
|
|
334
|
-
return document.body.dataset.jpThemeLight === 'false';
|
|
335
|
-
}
|
|
336
236
|
function DiffViewer(props) {
|
|
237
|
+
var _a;
|
|
337
238
|
const { repoPath, change, themeManager, contentsManager, rendermime, onChanged, onEmpty, widget } = props;
|
|
338
239
|
const [state, setState] = React.useState({
|
|
339
240
|
loading: true,
|
|
340
|
-
|
|
341
|
-
|
|
241
|
+
oldText: '',
|
|
242
|
+
newText: '',
|
|
342
243
|
serverPath: '',
|
|
343
244
|
isBinary: false,
|
|
344
245
|
error: null
|
|
345
246
|
});
|
|
346
|
-
// Split ratio for the diff columns: fraction of width given to the
|
|
347
|
-
// deletions (left) pane. Persisted across sessions so the user only
|
|
348
|
-
// dials in their layout once.
|
|
349
|
-
const [leftRatio, setLeftRatio] = React.useState(() => readStoredSplitRatio());
|
|
350
|
-
const wrapperRef = React.useRef(null);
|
|
351
|
-
// The drag handler reads the live ratio from a ref so the listeners we
|
|
352
|
-
// attach on pointer-down don't capture a stale value across renders.
|
|
353
|
-
const leftRatioRef = React.useRef(leftRatio);
|
|
354
|
-
React.useEffect(() => {
|
|
355
|
-
leftRatioRef.current = leftRatio;
|
|
356
|
-
}, [leftRatio]);
|
|
357
247
|
// Bumping `reloadKey` re-enters the fetch effect. Used after a successful
|
|
358
248
|
// hunk discard, so the diff reflects the new working-tree contents
|
|
359
249
|
// without the user having to close and reopen the tab.
|
|
@@ -379,9 +269,16 @@ function DiffViewer(props) {
|
|
|
379
269
|
// toolbar reads this flag to decide whether to show the
|
|
380
270
|
// Notebook/JSON selector — and the toolbar bar itself stays hidden
|
|
381
271
|
// when the file isn't a parseable notebook.
|
|
382
|
-
React.
|
|
383
|
-
widget.setHasNotebookView(
|
|
384
|
-
}, [widget
|
|
272
|
+
const handleNotebookAvailabilityChange = React.useCallback((available) => {
|
|
273
|
+
widget.setHasNotebookView(available);
|
|
274
|
+
}, [widget]);
|
|
275
|
+
// The shared surface reports its hunk count after every (re)computation;
|
|
276
|
+
// we keep the latest so the auto-close-on-empty effect can fire after a
|
|
277
|
+
// discard empties the diff.
|
|
278
|
+
const [hunkCount, setHunkCount] = React.useState(null);
|
|
279
|
+
const handleMetadataChange = React.useCallback((info) => {
|
|
280
|
+
setHunkCount(info.hunkCount);
|
|
281
|
+
}, []);
|
|
385
282
|
// Auto-close after a discard that emptied the diff. Gated on `reloadKey > 0`
|
|
386
283
|
// so a file that opens already-empty (uncommon but possible) is not closed
|
|
387
284
|
// before the user has had a chance to look at it.
|
|
@@ -389,8 +286,7 @@ function DiffViewer(props) {
|
|
|
389
286
|
if (!state.loading &&
|
|
390
287
|
state.error === null &&
|
|
391
288
|
!state.isBinary &&
|
|
392
|
-
|
|
393
|
-
state.metadata.hunks.length === 0 &&
|
|
289
|
+
hunkCount === 0 &&
|
|
394
290
|
reloadKey > 0) {
|
|
395
291
|
onEmpty === null || onEmpty === void 0 ? void 0 : onEmpty();
|
|
396
292
|
}
|
|
@@ -398,7 +294,7 @@ function DiffViewer(props) {
|
|
|
398
294
|
state.loading,
|
|
399
295
|
state.error,
|
|
400
296
|
state.isBinary,
|
|
401
|
-
|
|
297
|
+
hunkCount,
|
|
402
298
|
reloadKey,
|
|
403
299
|
onEmpty
|
|
404
300
|
]);
|
|
@@ -428,24 +324,28 @@ function DiffViewer(props) {
|
|
|
428
324
|
const serverPath = toServerPath(repoPath, change.path);
|
|
429
325
|
setState({
|
|
430
326
|
loading: true,
|
|
431
|
-
|
|
432
|
-
|
|
327
|
+
oldText: '',
|
|
328
|
+
newText: '',
|
|
433
329
|
serverPath,
|
|
434
330
|
isBinary: false,
|
|
435
331
|
error: null
|
|
436
332
|
});
|
|
437
333
|
const { oldRef, newRef } = resolveReferences(change);
|
|
334
|
+
// Raster images are still binary, but the git server base64-encodes
|
|
335
|
+
// binary content, so we fetch them like text and let the surface render
|
|
336
|
+
// an image diff. Other binary files have no useful textual diff.
|
|
337
|
+
const isImage = imageDataType(change.path) !== null;
|
|
438
338
|
void (async () => {
|
|
439
|
-
var _a, _b, _c
|
|
339
|
+
var _a, _b, _c;
|
|
440
340
|
try {
|
|
441
|
-
if (change.isBinary === true) {
|
|
341
|
+
if (change.isBinary === true && !isImage) {
|
|
442
342
|
if (cancelled) {
|
|
443
343
|
return;
|
|
444
344
|
}
|
|
445
345
|
setState({
|
|
446
346
|
loading: false,
|
|
447
|
-
|
|
448
|
-
|
|
347
|
+
oldText: '',
|
|
348
|
+
newText: '',
|
|
449
349
|
serverPath,
|
|
450
350
|
isBinary: true,
|
|
451
351
|
error: null
|
|
@@ -462,31 +362,10 @@ function DiffViewer(props) {
|
|
|
462
362
|
if (cancelled) {
|
|
463
363
|
return;
|
|
464
364
|
}
|
|
465
|
-
const oldText = (_b = oldRes.content) !== null && _b !== void 0 ? _b : '';
|
|
466
|
-
const newText = (_c = newRes.content) !== null && _c !== void 0 ? _c : '';
|
|
467
|
-
// For .ipynb files we precompute *both* the cell-by-cell
|
|
468
|
-
// notebook diff and the raw JSON file diff up front. The user
|
|
469
|
-
// toggles between them via the in-widget toolbar, so keeping
|
|
470
|
-
// both around (instead of re-fetching on switch) makes the
|
|
471
|
-
// toggle feel instant. If parsing fails, `notebookDiff` stays
|
|
472
|
-
// null and we silently fall back to the file diff path.
|
|
473
|
-
let notebookDiff = null;
|
|
474
|
-
if (isNotebookPath(change.path)) {
|
|
475
|
-
notebookDiff = buildNotebookDiff({ oldText, newText });
|
|
476
|
-
}
|
|
477
|
-
const oldFile = {
|
|
478
|
-
name: (_d = change.from) !== null && _d !== void 0 ? _d : change.path,
|
|
479
|
-
contents: oldText
|
|
480
|
-
};
|
|
481
|
-
const newFile = {
|
|
482
|
-
name: change.path,
|
|
483
|
-
contents: newText
|
|
484
|
-
};
|
|
485
|
-
const metadata = parseDiffFromFile(oldFile, newFile);
|
|
486
365
|
setState({
|
|
487
366
|
loading: false,
|
|
488
|
-
|
|
489
|
-
|
|
367
|
+
oldText: (_b = oldRes.content) !== null && _b !== void 0 ? _b : '',
|
|
368
|
+
newText: (_c = newRes.content) !== null && _c !== void 0 ? _c : '',
|
|
490
369
|
serverPath,
|
|
491
370
|
isBinary: false,
|
|
492
371
|
error: null
|
|
@@ -498,8 +377,8 @@ function DiffViewer(props) {
|
|
|
498
377
|
}
|
|
499
378
|
setState({
|
|
500
379
|
loading: false,
|
|
501
|
-
|
|
502
|
-
|
|
380
|
+
oldText: '',
|
|
381
|
+
newText: '',
|
|
503
382
|
serverPath,
|
|
504
383
|
isBinary: false,
|
|
505
384
|
error: err instanceof Error ? err.message : String(err)
|
|
@@ -513,158 +392,31 @@ function DiffViewer(props) {
|
|
|
513
392
|
// Per-hunk discard buttons are only meaningful for unstaged modifications:
|
|
514
393
|
// staged-vs-HEAD diffs would need an index manipulation we can't do
|
|
515
394
|
// through the available endpoints, and untracked files have no previous
|
|
516
|
-
// version to revert to.
|
|
517
|
-
// cases and the diff renders without action buttons.
|
|
395
|
+
// version to revert to.
|
|
518
396
|
const canDiscardHunk = change.group === 'unstaged' && change.status !== 'untracked';
|
|
519
|
-
const
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
}
|
|
523
|
-
return state.metadata.hunks.map((hunk, hunkIndex) => ({
|
|
524
|
-
side: 'additions',
|
|
525
|
-
// The first line of each hunk in the new file. We anchor the button
|
|
526
|
-
// there so it sits at the top of the hunk's body.
|
|
527
|
-
lineNumber: hunk.additionStart,
|
|
528
|
-
metadata: { hunkIndex }
|
|
529
|
-
}));
|
|
530
|
-
}, [canDiscardHunk, state.metadata]);
|
|
531
|
-
const handleDiscardHunk = React.useCallback(async (hunkIndex) => {
|
|
532
|
-
if (state.metadata === null) {
|
|
533
|
-
return;
|
|
534
|
-
}
|
|
535
|
-
const updated = diffAcceptRejectHunk(state.metadata, hunkIndex, 'reject');
|
|
536
|
-
// `additionLines` after a 'reject' contains the full new file with
|
|
537
|
-
// the hunk reverted back to the old content. The library splits
|
|
538
|
-
// file contents with `/(?<=\n)/` (a lookbehind that keeps the
|
|
539
|
-
// trailing `\n` on each line), so the entries already carry their
|
|
540
|
-
// own line endings — joining with `''` rebuilds the original text
|
|
541
|
-
// verbatim, while joining with `'\n'` would double every newline.
|
|
542
|
-
const text = updated.additionLines.join('');
|
|
543
|
-
try {
|
|
397
|
+
const hunkDiscard = React.useMemo(() => ({
|
|
398
|
+
enabled: canDiscardHunk,
|
|
399
|
+
save: async (text) => {
|
|
544
400
|
await contentsManager.save(state.serverPath, {
|
|
545
401
|
type: 'file',
|
|
546
402
|
format: 'text',
|
|
547
403
|
content: text
|
|
548
404
|
});
|
|
405
|
+
},
|
|
406
|
+
onAfterSave: () => {
|
|
549
407
|
onChanged();
|
|
550
408
|
setReloadKey(key => key + 1);
|
|
551
409
|
}
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
}
|
|
555
|
-
}, [contentsManager, onChanged, state.metadata, state.serverPath]);
|
|
556
|
-
const renderAnnotation = React.useCallback((annotation) => {
|
|
557
|
-
if (annotation.metadata === undefined) {
|
|
558
|
-
return null;
|
|
559
|
-
}
|
|
560
|
-
const { hunkIndex } = annotation.metadata;
|
|
561
|
-
return (React.createElement("div", { className: "jp-xtralab-DiffWidget-hunkAnnotation" },
|
|
562
|
-
React.createElement("button", { type: "button", className: "jp-xtralab-DiffWidget-hunkButton", title: "Discard this hunk's changes", "aria-label": "Discard hunk", onClick: () => void handleDiscardHunk(hunkIndex) },
|
|
563
|
-
React.createElement(undoIcon.react, { tag: "span", className: "jp-xtralab-DiffWidget-hunkButton-icon", elementSize: "normal" }))));
|
|
564
|
-
}, [handleDiscardHunk]);
|
|
565
|
-
const handleResizerPointerDown = React.useCallback((event) => {
|
|
566
|
-
// Only react to primary-button drags. Right-click / middle-click on
|
|
567
|
-
// the handle should pass through to the browser default.
|
|
568
|
-
if (event.button !== 0) {
|
|
569
|
-
return;
|
|
570
|
-
}
|
|
571
|
-
const wrapper = wrapperRef.current;
|
|
572
|
-
if (wrapper === null) {
|
|
573
|
-
return;
|
|
574
|
-
}
|
|
575
|
-
event.preventDefault();
|
|
576
|
-
const handle = event.currentTarget;
|
|
577
|
-
handle.setPointerCapture(event.pointerId);
|
|
578
|
-
handle.dataset.dragging = 'true';
|
|
579
|
-
const rect = wrapper.getBoundingClientRect();
|
|
580
|
-
const startX = event.clientX;
|
|
581
|
-
const startRatio = leftRatioRef.current;
|
|
582
|
-
const onPointerMove = (ev) => {
|
|
583
|
-
if (rect.width <= 0) {
|
|
584
|
-
return;
|
|
585
|
-
}
|
|
586
|
-
const next = startRatio + (ev.clientX - startX) / rect.width;
|
|
587
|
-
const clamped = Math.max(MIN_SPLIT_RATIO, Math.min(MAX_SPLIT_RATIO, next));
|
|
588
|
-
setLeftRatio(clamped);
|
|
589
|
-
};
|
|
590
|
-
const onPointerEnd = (ev) => {
|
|
591
|
-
handle.releasePointerCapture(ev.pointerId);
|
|
592
|
-
delete handle.dataset.dragging;
|
|
593
|
-
handle.removeEventListener('pointermove', onPointerMove);
|
|
594
|
-
handle.removeEventListener('pointerup', onPointerEnd);
|
|
595
|
-
handle.removeEventListener('pointercancel', onPointerEnd);
|
|
596
|
-
// Only persist on drag-end so a quick drag isn't followed by a
|
|
597
|
-
// burst of writes for every intermediate position.
|
|
598
|
-
writeStoredSplitRatio(leftRatioRef.current);
|
|
599
|
-
};
|
|
600
|
-
handle.addEventListener('pointermove', onPointerMove);
|
|
601
|
-
handle.addEventListener('pointerup', onPointerEnd);
|
|
602
|
-
handle.addEventListener('pointercancel', onPointerEnd);
|
|
603
|
-
}, []);
|
|
604
|
-
const handleResizerDoubleClick = React.useCallback(() => {
|
|
605
|
-
setLeftRatio(DEFAULT_SPLIT_RATIO);
|
|
606
|
-
writeStoredSplitRatio(DEFAULT_SPLIT_RATIO);
|
|
607
|
-
}, []);
|
|
608
|
-
if (state.loading) {
|
|
609
|
-
return React.createElement("div", { className: "jp-xtralab-DiffWidget-status" }, "Loading diff\u2026");
|
|
610
|
-
}
|
|
611
|
-
if (state.error !== null) {
|
|
612
|
-
return (React.createElement("div", { className: "jp-xtralab-DiffWidget-status", "data-error": "true" }, state.error));
|
|
613
|
-
}
|
|
614
|
-
if (state.isBinary) {
|
|
615
|
-
return (React.createElement("div", { className: "jp-xtralab-DiffWidget-status" }, "Binary file \u2014 diff not supported."));
|
|
616
|
-
}
|
|
617
|
-
// Decide which view to render. The notebook view requires a successful
|
|
618
|
-
// nbformat parse; if that failed, the toolbar is hidden and we only
|
|
619
|
-
// show the raw JSON file diff. The toolbar itself is sticky at the top
|
|
620
|
-
// of the content area, sitting above the scroll viewport.
|
|
621
|
-
const hasNotebookView = state.notebookDiff !== null;
|
|
622
|
-
const showNotebookView = hasNotebookView && notebookViewMode === 'notebook';
|
|
623
|
-
const showFileDiff = !showNotebookView && state.metadata !== null;
|
|
624
|
-
if (!showNotebookView && !showFileDiff) {
|
|
625
|
-
return (React.createElement("div", { className: "jp-xtralab-DiffWidget-status" }, "No content to diff."));
|
|
626
|
-
}
|
|
627
|
-
// Custom property set on the diffs-container host. The library's
|
|
628
|
-
// shadow-root rule consumes it via `var(--xtralab-split-cols, …)` (see
|
|
629
|
-
// SPLIT_RESIZE_CSS) and recomputes the column tracks instantly. Only
|
|
630
|
-
// relevant in the file-diff path; the notebook view doesn't use it.
|
|
631
|
-
const leftPercent = leftRatio * 100;
|
|
632
|
-
const hostStyle = {
|
|
633
|
-
'--xtralab-split-cols': `${leftPercent}% ${100 - leftPercent}%`
|
|
634
|
-
};
|
|
635
|
-
return (React.createElement("div", { className: "jp-xtralab-DiffWidget-content" },
|
|
636
|
-
React.createElement("div", { ref: wrapperRef, className: "jp-xtralab-DiffWidget-body" },
|
|
637
|
-
React.createElement("div", { className: "jp-xtralab-DiffWidget-scroll" }, showNotebookView && state.notebookDiff !== null ? (React.createElement(NotebookDiffView, { diff: state.notebookDiff, dark: dark, rendermime: rendermime })) : showFileDiff && state.metadata !== null ? (React.createElement(FileDiff, { fileDiff: state.metadata, lineAnnotations: lineAnnotations, renderAnnotation: renderAnnotation, style: hostStyle,
|
|
638
|
-
// Disable the worker pool: JupyterLab's webpack federation
|
|
639
|
-
// pipeline doesn't ship the library's
|
|
640
|
-
// `@pierre/diffs/worker/worker.js` file at a URL the worker
|
|
641
|
-
// bootstrap can resolve from, so leaving the pool enabled
|
|
642
|
-
// crashes on instantiation. Running on the main thread is
|
|
643
|
-
// fine for the small files git diffs typically operate on.
|
|
644
|
-
disableWorkerPool: true, options: {
|
|
645
|
-
diffStyle: 'split',
|
|
646
|
-
// Drop the file header — the tab title already shows the
|
|
647
|
-
// file name and the panel header carries the change
|
|
648
|
-
// context.
|
|
649
|
-
disableFileHeader: true,
|
|
650
|
-
theme: dark ? 'pierre-dark' : 'pierre-light',
|
|
651
|
-
themeType: dark ? 'dark' : 'light',
|
|
652
|
-
// Inject the column-resize override into the shadow root
|
|
653
|
-
// via the library's `@layer unsafe` channel. Keeping the
|
|
654
|
-
// string constant lets the library short-circuit the
|
|
655
|
-
// re-render path it uses when this option actually
|
|
656
|
-
// changes.
|
|
657
|
-
unsafeCSS: SPLIT_RESIZE_CSS
|
|
658
|
-
} })) : null),
|
|
659
|
-
showFileDiff ? (React.createElement("div", { className: "jp-xtralab-DiffWidget-resizer", style: { left: `${leftPercent}%` }, role: "separator", "aria-orientation": "vertical", "aria-valuenow": Math.round(leftPercent), "aria-valuemin": Math.round(MIN_SPLIT_RATIO * 100), "aria-valuemax": Math.round(MAX_SPLIT_RATIO * 100), title: "Drag to resize the diff panes (double-click to reset)", onPointerDown: handleResizerPointerDown, onDoubleClick: handleResizerDoubleClick })) : null)));
|
|
410
|
+
}), [canDiscardHunk, contentsManager, onChanged, state.serverPath]);
|
|
411
|
+
return (React.createElement(DiffSurface, { loading: state.loading, error: state.error, isBinary: state.isBinary, oldText: state.oldText, newText: state.newText, newName: change.path, oldName: (_a = change.from) !== null && _a !== void 0 ? _a : change.path, dark: dark, rendermime: rendermime, notebookViewMode: notebookViewMode, onNotebookAvailabilityChange: handleNotebookAvailabilityChange, onMetadataChange: handleMetadataChange, hunkDiscard: hunkDiscard }));
|
|
660
412
|
}
|
|
661
413
|
/**
|
|
662
414
|
* Segmented Notebook/JSON selector mounted into the `MainAreaWidget`
|
|
663
|
-
* toolbar for `.ipynb` diffs. The selector reads from and
|
|
664
|
-
*
|
|
665
|
-
* toolbar interactions stay in sync without either side
|
|
666
|
-
* the other. The host toolbar widget's visibility is toggled
|
|
667
|
-
* {@link createGitDiffWidget} based on the same `hasNotebookView`
|
|
415
|
+
* toolbar for `.ipynb` diffs (launcher path). The selector reads from and
|
|
416
|
+
* writes to the {@link DiffContentWidget} so React state changes (via the
|
|
417
|
+
* diff content) and toolbar interactions stay in sync without either side
|
|
418
|
+
* knowing about the other. The host toolbar widget's visibility is toggled
|
|
419
|
+
* by {@link createGitDiffWidget} based on the same `hasNotebookView`
|
|
668
420
|
* signal, so this control is always meaningful when it appears.
|
|
669
421
|
*/
|
|
670
422
|
class NotebookViewModeToolbarItem extends ReactWidget {
|
|
@@ -674,28 +426,30 @@ class NotebookViewModeToolbarItem extends ReactWidget {
|
|
|
674
426
|
this.addClass('jp-xtralab-DiffWidget-viewModeToolbarItem');
|
|
675
427
|
}
|
|
676
428
|
render() {
|
|
677
|
-
return React.createElement(
|
|
429
|
+
return React.createElement(NotebookViewModeToolbarControl, { content: this._content });
|
|
678
430
|
}
|
|
679
431
|
}
|
|
680
|
-
function
|
|
432
|
+
function NotebookViewModeToolbarControl(props) {
|
|
681
433
|
const { content } = props;
|
|
682
434
|
const [mode, setMode] = React.useState(() => content.notebookViewMode);
|
|
435
|
+
const [available, setAvailable] = React.useState(() => content.hasNotebookView);
|
|
683
436
|
React.useEffect(() => {
|
|
684
|
-
const
|
|
437
|
+
const onMode = (_sender, next) => {
|
|
685
438
|
setMode(next);
|
|
686
439
|
};
|
|
687
|
-
|
|
440
|
+
const onAvailable = (_sender, next) => {
|
|
441
|
+
setAvailable(next);
|
|
442
|
+
};
|
|
443
|
+
content.notebookViewModeChanged.connect(onMode);
|
|
444
|
+
content.hasNotebookViewChanged.connect(onAvailable);
|
|
688
445
|
setMode(content.notebookViewMode);
|
|
446
|
+
setAvailable(content.hasNotebookView);
|
|
689
447
|
return () => {
|
|
690
|
-
content.notebookViewModeChanged.disconnect(
|
|
448
|
+
content.notebookViewModeChanged.disconnect(onMode);
|
|
449
|
+
content.hasNotebookViewChanged.disconnect(onAvailable);
|
|
691
450
|
};
|
|
692
451
|
}, [content]);
|
|
693
|
-
|
|
694
|
-
return React.createElement(React.Fragment, null);
|
|
695
|
-
}
|
|
696
|
-
return (React.createElement("div", { className: "jp-xtralab-DiffWidget-segmented", role: "tablist", "aria-label": "Notebook diff view mode" },
|
|
697
|
-
React.createElement("button", { type: "button", role: "tab", "aria-selected": mode === 'notebook', "data-active": mode === 'notebook', className: "jp-xtralab-DiffWidget-segmentedButton", onClick: () => content.setNotebookViewMode('notebook') }, "Notebook"),
|
|
698
|
-
React.createElement("button", { type: "button", role: "tab", "aria-selected": mode === 'json', "data-active": mode === 'json', className: "jp-xtralab-DiffWidget-segmentedButton", onClick: () => content.setNotebookViewMode('json') }, "JSON")));
|
|
452
|
+
return (React.createElement(NotebookViewModeControl, { mode: mode, available: available, onChange: next => content.setNotebookViewMode(next) }));
|
|
699
453
|
}
|
|
700
454
|
function formatTitle(change) {
|
|
701
455
|
var _a;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
/** The file extensions registered as the xtralab image diff provider. */
|
|
3
|
+
export declare const IMAGE_DIFF_EXTENSIONS: string[];
|
|
4
|
+
/**
|
|
5
|
+
* The `data:` MIME subtype for a path xtralab renders as an image, or
|
|
6
|
+
* `null` when the path is not a supported raster image.
|
|
7
|
+
*/
|
|
8
|
+
export declare function imageDataType(path: string): string | null;
|
|
9
|
+
interface IImageDiffViewProps {
|
|
10
|
+
/** Base64 of the reference (old) revision; empty when added. */
|
|
11
|
+
reference: string;
|
|
12
|
+
/** Base64 of the challenger (new) revision; empty when deleted. */
|
|
13
|
+
challenger: string;
|
|
14
|
+
/** MIME subtype from {@link imageDataType}. */
|
|
15
|
+
fileType: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* The image comparison surface: a segmented mode selector (reusing the
|
|
19
|
+
* diff segmented-control styling so it matches the Notebook/JSON toggle)
|
|
20
|
+
* plus the selected 2-up / swipe / onion-skin view.
|
|
21
|
+
*/
|
|
22
|
+
export declare function ImageDiffView(props: IImageDiffViewProps): React.ReactElement;
|
|
23
|
+
export {};
|