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
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { undoIcon } from '@jupyterlab/ui-components';
|
|
3
|
+
import { FileDiff } from '@pierre/diffs/react';
|
|
4
|
+
import { diffAcceptRejectHunk, parseDiffFromFile } from '@pierre/diffs';
|
|
5
|
+
import { ImageDiffView, imageDataType } from './imageDiff';
|
|
6
|
+
import { buildNotebookDiff, NotebookDiffView } from './notebookDiff';
|
|
7
|
+
/**
|
|
8
|
+
* Shared diff rendering surface used by both diff hosts in this extension:
|
|
9
|
+
*
|
|
10
|
+
* - the launcher dashboard's own `xtralab:git:open-diff` command
|
|
11
|
+
* (via {@link DiffContentWidget} in `diffWidget.tsx`), and
|
|
12
|
+
* - the `jupyterlab-git` diff providers registered into the upstream git
|
|
13
|
+
* panel (via `XtralabDiffWidget` in `diffProvider.tsx`).
|
|
14
|
+
*
|
|
15
|
+
* The surface is deliberately content-source agnostic: callers resolve the
|
|
16
|
+
* old/new text however they like (xtralab's REST helper, or a
|
|
17
|
+
* `Git.Diff.IModel`'s `content()` getters) and hand the resolved strings in.
|
|
18
|
+
* Everything visual — the `@pierre/diffs` split view, the cell-by-cell
|
|
19
|
+
* notebook view, the draggable column resizer, and the per-hunk discard
|
|
20
|
+
* affordance — lives here so the two hosts stay pixel-identical.
|
|
21
|
+
*/
|
|
22
|
+
/**
|
|
23
|
+
* The CSS class added to the diff main-area widget. The selectors that
|
|
24
|
+
* style the embedded `@pierre/diffs` viewer hang off this class.
|
|
25
|
+
*/
|
|
26
|
+
export const DIFF_WIDGET_CSS_CLASS = 'jp-xtralab-DiffWidget';
|
|
27
|
+
/**
|
|
28
|
+
* `localStorage` key for the user's last-chosen split ratio, persisted so
|
|
29
|
+
* the preference survives reloads (the library has no built-in way to
|
|
30
|
+
* remember it).
|
|
31
|
+
*/
|
|
32
|
+
const SPLIT_RATIO_STORAGE_KEY = 'xtralab:diff-split-ratio';
|
|
33
|
+
/**
|
|
34
|
+
* Bounds on the left-pane fraction. Keeping a few percent on either side
|
|
35
|
+
* guarantees both panes remain visible — dragging to 0/100 would hide a
|
|
36
|
+
* column and is almost never what the user wants.
|
|
37
|
+
*/
|
|
38
|
+
const MIN_SPLIT_RATIO = 0.1;
|
|
39
|
+
const MAX_SPLIT_RATIO = 0.9;
|
|
40
|
+
const DEFAULT_SPLIT_RATIO = 0.5;
|
|
41
|
+
/**
|
|
42
|
+
* CSS injected via the `@pierre/diffs` `unsafeCSS` option. The library
|
|
43
|
+
* places it inside the shadow root under `@layer unsafe`, which beats
|
|
44
|
+
* the library's own `@layer base` rule that hardcodes
|
|
45
|
+
* `grid-template-columns: 1fr 1fr` on the split-mode container.
|
|
46
|
+
*
|
|
47
|
+
* The override reads from `--xtralab-split-cols`, a custom property we set
|
|
48
|
+
* on the host element via the `style` prop. Custom properties inherit
|
|
49
|
+
* through the shadow DOM boundary, so adjusting it on the host instantly
|
|
50
|
+
* resizes the columns without re-rendering the diff.
|
|
51
|
+
*/
|
|
52
|
+
const SPLIT_RESIZE_CSS = `pre[data-diff-type="split"][data-overflow="scroll"] {
|
|
53
|
+
grid-template-columns: var(--xtralab-split-cols, 1fr 1fr);
|
|
54
|
+
}`;
|
|
55
|
+
function readStoredSplitRatio() {
|
|
56
|
+
try {
|
|
57
|
+
const raw = window.localStorage.getItem(SPLIT_RATIO_STORAGE_KEY);
|
|
58
|
+
if (raw === null) {
|
|
59
|
+
return DEFAULT_SPLIT_RATIO;
|
|
60
|
+
}
|
|
61
|
+
const parsed = Number.parseFloat(raw);
|
|
62
|
+
if (Number.isFinite(parsed) &&
|
|
63
|
+
parsed >= MIN_SPLIT_RATIO &&
|
|
64
|
+
parsed <= MAX_SPLIT_RATIO) {
|
|
65
|
+
return parsed;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
catch (_a) {
|
|
69
|
+
// localStorage may throw in privacy mode or sandboxed contexts. Falling
|
|
70
|
+
// back to the default is fine — the user can resize again on this run.
|
|
71
|
+
}
|
|
72
|
+
return DEFAULT_SPLIT_RATIO;
|
|
73
|
+
}
|
|
74
|
+
function writeStoredSplitRatio(ratio) {
|
|
75
|
+
try {
|
|
76
|
+
window.localStorage.setItem(SPLIT_RATIO_STORAGE_KEY, ratio.toString());
|
|
77
|
+
}
|
|
78
|
+
catch (_a) {
|
|
79
|
+
// See readStoredSplitRatio — best-effort persistence.
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const NOTEBOOK_DIFF_VIEW_MODE_STORAGE_KEY = 'xtralab:notebook-diff-view-mode';
|
|
83
|
+
export function readStoredNotebookViewMode() {
|
|
84
|
+
try {
|
|
85
|
+
const raw = window.localStorage.getItem(NOTEBOOK_DIFF_VIEW_MODE_STORAGE_KEY);
|
|
86
|
+
if (raw === 'json' || raw === 'notebook') {
|
|
87
|
+
return raw;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
catch (_a) {
|
|
91
|
+
// See readStoredSplitRatio — privacy-mode / sandboxed contexts can
|
|
92
|
+
// throw on access. The notebook view is the better default anyway.
|
|
93
|
+
}
|
|
94
|
+
return 'notebook';
|
|
95
|
+
}
|
|
96
|
+
export function writeStoredNotebookViewMode(mode) {
|
|
97
|
+
try {
|
|
98
|
+
window.localStorage.setItem(NOTEBOOK_DIFF_VIEW_MODE_STORAGE_KEY, mode);
|
|
99
|
+
}
|
|
100
|
+
catch (_a) {
|
|
101
|
+
// Best-effort.
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Detect notebook files by extension. We only look at the suffix so the
|
|
106
|
+
* caller doesn't need to plumb a content-type field through; mistaking
|
|
107
|
+
* another `.ipynb`-named file for a notebook is harmless because
|
|
108
|
+
* {@link buildNotebookDiff} validates the JSON shape and we fall back to
|
|
109
|
+
* the file diff if parsing fails.
|
|
110
|
+
*/
|
|
111
|
+
function isNotebookPath(path) {
|
|
112
|
+
return path.toLowerCase().endsWith('.ipynb');
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Decide whether the active JupyterLab theme is a dark theme. Prefers the
|
|
116
|
+
* `IThemeManager` token when available, and falls back to sniffing the
|
|
117
|
+
* `data-jp-theme-light` attribute that JupyterLab sets on `<body>` so the
|
|
118
|
+
* component still works in stripped-down hosts where the token isn't
|
|
119
|
+
* provided (lite, certain notebook configurations, …).
|
|
120
|
+
*/
|
|
121
|
+
export function isDarkTheme(themeManager) {
|
|
122
|
+
if (themeManager !== null && themeManager.theme !== null) {
|
|
123
|
+
return !themeManager.isLight(themeManager.theme);
|
|
124
|
+
}
|
|
125
|
+
return document.body.dataset.jpThemeLight === 'false';
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* The shared, content-source-agnostic diff renderer. See the module
|
|
129
|
+
* docstring for the division of labor between this surface and its hosts.
|
|
130
|
+
*/
|
|
131
|
+
export function DiffSurface(props) {
|
|
132
|
+
const { loading, error, isBinary, oldText, newText, newName, oldName, dark, rendermime, notebookViewMode, onNotebookAvailabilityChange, onMetadataChange, hunkDiscard } = props;
|
|
133
|
+
const hasContent = !loading && error === null && !isBinary;
|
|
134
|
+
// Raster images take a dedicated `<img>`-based view instead of a
|
|
135
|
+
// text/notebook diff; the host passes the two sides as base64 strings in
|
|
136
|
+
// `oldText`/`newText` (the git server base64-encodes binary content).
|
|
137
|
+
const imageType = React.useMemo(() => imageDataType(newName), [newName]);
|
|
138
|
+
// Pre-compute the line-oriented diff metadata. Owning it here lets us
|
|
139
|
+
// pass it to {@link FileDiff} *and* thread the same instance through
|
|
140
|
+
// {@link diffAcceptRejectHunk} when the user discards a hunk — so hunk
|
|
141
|
+
// indexes line up between what the user clicked and what we mutate.
|
|
142
|
+
const metadata = React.useMemo(() => {
|
|
143
|
+
if (!hasContent || imageType !== null) {
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
const oldFile = {
|
|
147
|
+
name: oldName !== null && oldName !== void 0 ? oldName : newName,
|
|
148
|
+
contents: oldText
|
|
149
|
+
};
|
|
150
|
+
const newFile = { name: newName, contents: newText };
|
|
151
|
+
return parseDiffFromFile(oldFile, newFile);
|
|
152
|
+
}, [hasContent, imageType, oldText, newText, oldName, newName]);
|
|
153
|
+
// Cell-by-cell notebook diff for `.ipynb` files. When non-null the
|
|
154
|
+
// renderer uses a notebook-aware view instead of the line-oriented file
|
|
155
|
+
// diff — kept distinct from {@link metadata} so a notebook whose JSON we
|
|
156
|
+
// cannot parse can fall back to the file diff path.
|
|
157
|
+
const notebookDiff = React.useMemo(() => {
|
|
158
|
+
if (!hasContent || imageType !== null || !isNotebookPath(newName)) {
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
return buildNotebookDiff({ oldText, newText });
|
|
162
|
+
}, [hasContent, imageType, oldText, newText, newName]);
|
|
163
|
+
// Tell the host whether a rendered notebook view is currently available
|
|
164
|
+
// so it can show/hide its Notebook/JSON toggle.
|
|
165
|
+
React.useEffect(() => {
|
|
166
|
+
onNotebookAvailabilityChange === null || onNotebookAvailabilityChange === void 0 ? void 0 : onNotebookAvailabilityChange(notebookDiff !== null);
|
|
167
|
+
}, [onNotebookAvailabilityChange, notebookDiff]);
|
|
168
|
+
// Report the diff shape so hosts can react (e.g. auto-close on empty).
|
|
169
|
+
React.useEffect(() => {
|
|
170
|
+
onMetadataChange === null || onMetadataChange === void 0 ? void 0 : onMetadataChange({
|
|
171
|
+
hunkCount: metadata !== null ? metadata.hunks.length : null
|
|
172
|
+
});
|
|
173
|
+
}, [onMetadataChange, metadata]);
|
|
174
|
+
// Split ratio for the diff columns: fraction of width given to the
|
|
175
|
+
// deletions (left) pane. Persisted across sessions so the user only
|
|
176
|
+
// dials in their layout once.
|
|
177
|
+
const [leftRatio, setLeftRatio] = React.useState(() => readStoredSplitRatio());
|
|
178
|
+
const wrapperRef = React.useRef(null);
|
|
179
|
+
// The drag handler reads the live ratio from a ref so the listeners we
|
|
180
|
+
// attach on pointer-down don't capture a stale value across renders.
|
|
181
|
+
const leftRatioRef = React.useRef(leftRatio);
|
|
182
|
+
React.useEffect(() => {
|
|
183
|
+
leftRatioRef.current = leftRatio;
|
|
184
|
+
}, [leftRatio]);
|
|
185
|
+
const canDiscardHunk = (hunkDiscard === null || hunkDiscard === void 0 ? void 0 : hunkDiscard.enabled) === true;
|
|
186
|
+
const lineAnnotations = React.useMemo(() => {
|
|
187
|
+
if (!canDiscardHunk || metadata === null) {
|
|
188
|
+
return [];
|
|
189
|
+
}
|
|
190
|
+
return metadata.hunks.map((hunk, hunkIndex) => ({
|
|
191
|
+
side: 'additions',
|
|
192
|
+
// The first line of each hunk in the new file. We anchor the button
|
|
193
|
+
// there so it sits at the top of the hunk's body.
|
|
194
|
+
lineNumber: hunk.additionStart,
|
|
195
|
+
metadata: { hunkIndex }
|
|
196
|
+
}));
|
|
197
|
+
}, [canDiscardHunk, metadata]);
|
|
198
|
+
const handleDiscardHunk = React.useCallback(async (hunkIndex) => {
|
|
199
|
+
if (metadata === null || hunkDiscard === undefined) {
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
const updated = diffAcceptRejectHunk(metadata, hunkIndex, 'reject');
|
|
203
|
+
// `additionLines` after a 'reject' contains the full new file with
|
|
204
|
+
// the hunk reverted back to the old content. The library splits
|
|
205
|
+
// file contents with `/(?<=\n)/` (a lookbehind that keeps the
|
|
206
|
+
// trailing `\n` on each line), so the entries already carry their
|
|
207
|
+
// own line endings — joining with `''` rebuilds the original text
|
|
208
|
+
// verbatim, while joining with `'\n'` would double every newline.
|
|
209
|
+
const text = updated.additionLines.join('');
|
|
210
|
+
try {
|
|
211
|
+
await hunkDiscard.save(text);
|
|
212
|
+
hunkDiscard.onAfterSave();
|
|
213
|
+
}
|
|
214
|
+
catch (err) {
|
|
215
|
+
console.error('xtralab: failed to discard hunk', err);
|
|
216
|
+
}
|
|
217
|
+
}, [hunkDiscard, metadata]);
|
|
218
|
+
const renderAnnotation = React.useCallback((annotation) => {
|
|
219
|
+
if (annotation.metadata === undefined) {
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
const { hunkIndex } = annotation.metadata;
|
|
223
|
+
return (React.createElement("div", { className: "jp-xtralab-DiffWidget-hunkAnnotation" },
|
|
224
|
+
React.createElement("button", { type: "button", className: "jp-xtralab-DiffWidget-hunkButton", title: "Discard this hunk's changes", "aria-label": "Discard hunk", onClick: () => void handleDiscardHunk(hunkIndex) },
|
|
225
|
+
React.createElement(undoIcon.react, { tag: "span", className: "jp-xtralab-DiffWidget-hunkButton-icon", elementSize: "normal" }))));
|
|
226
|
+
}, [handleDiscardHunk]);
|
|
227
|
+
const handleResizerPointerDown = React.useCallback((event) => {
|
|
228
|
+
// Only react to primary-button drags. Right-click / middle-click on
|
|
229
|
+
// the handle should pass through to the browser default.
|
|
230
|
+
if (event.button !== 0) {
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
const wrapper = wrapperRef.current;
|
|
234
|
+
if (wrapper === null) {
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
event.preventDefault();
|
|
238
|
+
const handle = event.currentTarget;
|
|
239
|
+
handle.setPointerCapture(event.pointerId);
|
|
240
|
+
handle.dataset.dragging = 'true';
|
|
241
|
+
const rect = wrapper.getBoundingClientRect();
|
|
242
|
+
const startX = event.clientX;
|
|
243
|
+
const startRatio = leftRatioRef.current;
|
|
244
|
+
const onPointerMove = (ev) => {
|
|
245
|
+
if (rect.width <= 0) {
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
const next = startRatio + (ev.clientX - startX) / rect.width;
|
|
249
|
+
const clamped = Math.max(MIN_SPLIT_RATIO, Math.min(MAX_SPLIT_RATIO, next));
|
|
250
|
+
setLeftRatio(clamped);
|
|
251
|
+
};
|
|
252
|
+
const onPointerEnd = (ev) => {
|
|
253
|
+
handle.releasePointerCapture(ev.pointerId);
|
|
254
|
+
delete handle.dataset.dragging;
|
|
255
|
+
handle.removeEventListener('pointermove', onPointerMove);
|
|
256
|
+
handle.removeEventListener('pointerup', onPointerEnd);
|
|
257
|
+
handle.removeEventListener('pointercancel', onPointerEnd);
|
|
258
|
+
// Only persist on drag-end so a quick drag isn't followed by a
|
|
259
|
+
// burst of writes for every intermediate position.
|
|
260
|
+
writeStoredSplitRatio(leftRatioRef.current);
|
|
261
|
+
};
|
|
262
|
+
handle.addEventListener('pointermove', onPointerMove);
|
|
263
|
+
handle.addEventListener('pointerup', onPointerEnd);
|
|
264
|
+
handle.addEventListener('pointercancel', onPointerEnd);
|
|
265
|
+
}, []);
|
|
266
|
+
const handleResizerDoubleClick = React.useCallback(() => {
|
|
267
|
+
setLeftRatio(DEFAULT_SPLIT_RATIO);
|
|
268
|
+
writeStoredSplitRatio(DEFAULT_SPLIT_RATIO);
|
|
269
|
+
}, []);
|
|
270
|
+
if (loading) {
|
|
271
|
+
return React.createElement("div", { className: "jp-xtralab-DiffWidget-status" }, "Loading diff\u2026");
|
|
272
|
+
}
|
|
273
|
+
if (error !== null) {
|
|
274
|
+
return (React.createElement("div", { className: "jp-xtralab-DiffWidget-status", "data-error": "true" }, error));
|
|
275
|
+
}
|
|
276
|
+
if (isBinary) {
|
|
277
|
+
return (React.createElement("div", { className: "jp-xtralab-DiffWidget-status" }, "Binary file \u2014 diff not supported."));
|
|
278
|
+
}
|
|
279
|
+
if (imageType !== null) {
|
|
280
|
+
// `oldText`/`newText` carry the two revisions as base64 here. The
|
|
281
|
+
// image view owns its own layout and mode selector, so it does not
|
|
282
|
+
// use the split resizer or the notebook/JSON toggle.
|
|
283
|
+
return (React.createElement("div", { className: "jp-xtralab-DiffWidget-content" },
|
|
284
|
+
React.createElement(ImageDiffView, { reference: oldText, challenger: newText, fileType: imageType })));
|
|
285
|
+
}
|
|
286
|
+
// Decide which view to render. The notebook view requires a successful
|
|
287
|
+
// nbformat parse; if that failed, we only show the raw JSON file diff.
|
|
288
|
+
const hasNotebookView = notebookDiff !== null;
|
|
289
|
+
const showNotebookView = hasNotebookView && notebookViewMode === 'notebook';
|
|
290
|
+
const showFileDiff = !showNotebookView && metadata !== null;
|
|
291
|
+
if (!showNotebookView && !showFileDiff) {
|
|
292
|
+
return (React.createElement("div", { className: "jp-xtralab-DiffWidget-status" }, "No content to diff."));
|
|
293
|
+
}
|
|
294
|
+
// Custom property set on the diffs-container host. The library's
|
|
295
|
+
// shadow-root rule consumes it via `var(--xtralab-split-cols, …)` (see
|
|
296
|
+
// SPLIT_RESIZE_CSS) and recomputes the column tracks instantly. Only
|
|
297
|
+
// relevant in the file-diff path; the notebook view doesn't use it.
|
|
298
|
+
const leftPercent = leftRatio * 100;
|
|
299
|
+
const hostStyle = {
|
|
300
|
+
'--xtralab-split-cols': `${leftPercent}% ${100 - leftPercent}%`
|
|
301
|
+
};
|
|
302
|
+
return (React.createElement("div", { className: "jp-xtralab-DiffWidget-content" },
|
|
303
|
+
React.createElement("div", { ref: wrapperRef, className: "jp-xtralab-DiffWidget-body" },
|
|
304
|
+
React.createElement("div", { className: "jp-xtralab-DiffWidget-scroll" }, showNotebookView && notebookDiff !== null ? (React.createElement(NotebookDiffView, { diff: notebookDiff, dark: dark, rendermime: rendermime })) : showFileDiff && metadata !== null ? (React.createElement(FileDiff, { fileDiff: metadata, lineAnnotations: lineAnnotations, renderAnnotation: renderAnnotation, style: hostStyle,
|
|
305
|
+
// Disable the worker pool: JupyterLab's webpack federation
|
|
306
|
+
// pipeline doesn't ship the library's
|
|
307
|
+
// `@pierre/diffs/worker/worker.js` file at a URL the worker
|
|
308
|
+
// bootstrap can resolve from, so leaving the pool enabled
|
|
309
|
+
// crashes on instantiation. Running on the main thread is
|
|
310
|
+
// fine for the small files git diffs typically operate on.
|
|
311
|
+
disableWorkerPool: true, options: {
|
|
312
|
+
diffStyle: 'split',
|
|
313
|
+
// Drop the file header — the tab title already shows the
|
|
314
|
+
// file name and the panel header carries the change
|
|
315
|
+
// context.
|
|
316
|
+
disableFileHeader: true,
|
|
317
|
+
theme: dark ? 'pierre-dark' : 'pierre-light',
|
|
318
|
+
themeType: dark ? 'dark' : 'light',
|
|
319
|
+
// Inject the column-resize override into the shadow root
|
|
320
|
+
// via the library's `@layer unsafe` channel. Keeping the
|
|
321
|
+
// string constant lets the library short-circuit the
|
|
322
|
+
// re-render path it uses when this option actually
|
|
323
|
+
// changes.
|
|
324
|
+
unsafeCSS: SPLIT_RESIZE_CSS
|
|
325
|
+
} })) : null),
|
|
326
|
+
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)));
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Segmented Notebook/JSON selector. Hosts mount this into whatever toolbar
|
|
330
|
+
* they own (the launcher's `MainAreaWidget` toolbar, or the
|
|
331
|
+
* `jupyterlab-git`-provided diff toolbar) and drive its value/visibility
|
|
332
|
+
* from the same state they pass to {@link DiffSurface}.
|
|
333
|
+
*/
|
|
334
|
+
export function NotebookViewModeControl(props) {
|
|
335
|
+
const { mode, available, onChange } = props;
|
|
336
|
+
if (!available) {
|
|
337
|
+
return React.createElement(React.Fragment, null);
|
|
338
|
+
}
|
|
339
|
+
return (React.createElement("div", { className: "jp-xtralab-DiffWidget-segmented", role: "tablist", "aria-label": "Notebook diff view mode" },
|
|
340
|
+
React.createElement("button", { type: "button", role: "tab", "aria-selected": mode === 'notebook', "data-active": mode === 'notebook', className: "jp-xtralab-DiffWidget-segmentedButton", onClick: () => onChange('notebook') }, "Notebook"),
|
|
341
|
+
React.createElement("button", { type: "button", role: "tab", "aria-selected": mode === 'json', "data-active": mode === 'json', className: "jp-xtralab-DiffWidget-segmentedButton", onClick: () => onChange('json') }, "JSON")));
|
|
342
|
+
}
|
package/lib/git/diffWidget.d.ts
CHANGED
|
@@ -4,21 +4,11 @@ import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
|
|
|
4
4
|
import { Contents } from '@jupyterlab/services';
|
|
5
5
|
import { ReactWidget } from '@jupyterlab/ui-components';
|
|
6
6
|
import { ISignal } from '@lumino/signaling';
|
|
7
|
+
import { DIFF_WIDGET_CSS_CLASS, type NotebookDiffViewMode } from './diffSurface';
|
|
7
8
|
import { GitReference, IFileChange } from './tokens';
|
|
8
|
-
|
|
9
|
-
* The CSS class added to the diff main-area widget. The selectors that
|
|
10
|
-
* style the embedded `@pierre/diffs` viewer hang off this class.
|
|
11
|
-
*/
|
|
12
|
-
export declare const DIFF_WIDGET_CSS_CLASS = "jp-xtralab-DiffWidget";
|
|
9
|
+
export { DIFF_WIDGET_CSS_CLASS };
|
|
13
10
|
export declare const PREVIEW_DIFF_WIDGET_ID = "xtralab:diff:preview";
|
|
14
11
|
export declare function pinnedDiffWidgetId(change: IFileChange): string;
|
|
15
|
-
/**
|
|
16
|
-
* View modes available for `.ipynb` diffs. `notebook` is the cell-by-cell
|
|
17
|
-
* rendered view; `json` falls back to the raw nbformat JSON file diff so
|
|
18
|
-
* the user can inspect exactly which bytes changed (handy for nbformat
|
|
19
|
-
* spelunking, seeing execution_count drift, etc.).
|
|
20
|
-
*/
|
|
21
|
-
type NotebookDiffViewMode = 'notebook' | 'json';
|
|
22
12
|
/**
|
|
23
13
|
* Build the deterministic widget id used for a diff against a particular
|
|
24
14
|
* file/group pair. Reused across plugin and command code so reopening a
|
|
@@ -69,6 +59,12 @@ export interface IGitDiffWidgetOptions {
|
|
|
69
59
|
* usual document chrome — toolbar slot, focus tracker integration, and
|
|
70
60
|
* spinner-while-loading hooks — and matches the pattern used for other
|
|
71
61
|
* read-only main-area widgets in the JupyterLab ecosystem.
|
|
62
|
+
*
|
|
63
|
+
* This is the launcher dashboard's own diff path
|
|
64
|
+
* (`xtralab:git:open-diff`), kept independent of `jupyterlab-git`. The
|
|
65
|
+
* actual diff rendering is delegated to the shared {@link DiffSurface};
|
|
66
|
+
* `jupyterlab-git`'s panel reaches the same surface through the providers
|
|
67
|
+
* registered in `diffProvider.tsx`.
|
|
72
68
|
*/
|
|
73
69
|
export declare function createGitDiffWidget(options: IGitDiffWidgetOptions): MainAreaWidget<DiffContentWidget>;
|
|
74
70
|
export declare function updateGitDiffWidget(widget: MainAreaWidget<DiffContentWidget>, change: IFileChange): void;
|
|
@@ -116,4 +112,3 @@ export declare class DiffContentWidget extends ReactWidget {
|
|
|
116
112
|
private _pinned;
|
|
117
113
|
private _pinnedChanged;
|
|
118
114
|
}
|
|
119
|
-
export {};
|