xtralab 0.8.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/README.md +30 -6
  2. package/lib/fileBrowser/commands.d.ts +7 -0
  3. package/lib/fileBrowser/commands.js +74 -1
  4. package/lib/fileBrowser/fileBrowser.js +4 -1
  5. package/lib/fileBrowser/icons.d.ts +5 -9
  6. package/lib/fileBrowser/icons.js +97 -13
  7. package/lib/fileBrowser/index.js +5 -3
  8. package/lib/fileTypeIcons/index.d.ts +6 -0
  9. package/lib/fileTypeIcons/index.js +144 -0
  10. package/lib/git/commands.d.ts +34 -33
  11. package/lib/git/commands.js +132 -10
  12. package/lib/git/diffModel.d.ts +14 -0
  13. package/lib/git/diffModel.js +75 -0
  14. package/lib/git/diffProvider.d.ts +0 -5
  15. package/lib/git/diffProvider.js +6 -376
  16. package/lib/git/diffSurface.d.ts +36 -26
  17. package/lib/git/diffSurface.js +13 -20
  18. package/lib/git/diffTheme.d.ts +9 -0
  19. package/lib/git/diffTheme.js +31 -0
  20. package/lib/git/diffWidget.d.ts +54 -91
  21. package/lib/git/diffWidget.js +184 -361
  22. package/lib/git/index.d.ts +1 -6
  23. package/lib/git/index.js +7 -23
  24. package/lib/git/notebookDiff.d.ts +6 -1
  25. package/lib/git/notebookDiff.js +4 -3
  26. package/lib/index.js +4 -0
  27. package/lib/terminalNotifications/index.d.ts +15 -0
  28. package/lib/terminalNotifications/index.js +213 -0
  29. package/lib/terminals/model.d.ts +1 -1
  30. package/lib/terminals/model.js +1 -1
  31. package/lib/terminals/widget.d.ts +1 -1
  32. package/lib/terminals/widget.js +1 -1
  33. package/package.json +3 -3
  34. package/schema/plugin.json +6 -0
  35. package/schema/terminal-notifications.json +21 -0
  36. package/src/fileBrowser/commands.ts +91 -2
  37. package/src/fileBrowser/fileBrowser.tsx +4 -1
  38. package/src/fileBrowser/icons.ts +103 -13
  39. package/src/fileBrowser/index.ts +9 -3
  40. package/src/fileTypeIcons/index.ts +174 -0
  41. package/src/git/commands.ts +177 -43
  42. package/src/git/diffModel.ts +102 -0
  43. package/src/git/diffProvider.tsx +8 -540
  44. package/src/git/diffSurface.tsx +44 -28
  45. package/src/git/diffTheme.ts +40 -0
  46. package/src/git/diffWidget.tsx +256 -448
  47. package/src/git/index.ts +14 -28
  48. package/src/git/notebookDiff.tsx +10 -3
  49. package/src/index.ts +4 -0
  50. package/src/terminalNotifications/index.ts +295 -0
  51. package/src/terminals/model.ts +1 -1
  52. package/src/terminals/widget.tsx +1 -1
  53. package/style/git.css +12 -9
  54. package/style/index.css +1 -0
  55. package/style/index.js +1 -0
  56. package/style/launcher.css +46 -18
  57. package/style/sidebar.css +14 -0
@@ -0,0 +1,102 @@
1
+ import { Git } from '@jupyterlab/git';
2
+ import { ISignal, Signal } from '@lumino/signaling';
3
+
4
+ import { content } from './api';
5
+ import type { IXtralabDiffModel } from './diffWidget';
6
+ import type { GitReference, IFileChange } from './tokens';
7
+
8
+ /**
9
+ * Resolve the old/new git refs for a launcher file change. Staged diffs are
10
+ * INDEX vs HEAD; unstaged diffs are WORKING vs INDEX.
11
+ */
12
+ export function resolveReferences(change: IFileChange): {
13
+ oldRef: GitReference | null;
14
+ newRef: GitReference | null;
15
+ } {
16
+ if (change.status === 'untracked') {
17
+ return { oldRef: null, newRef: { special: 'WORKING' } };
18
+ }
19
+ if (change.group === 'staged') {
20
+ return { oldRef: { git: 'HEAD' }, newRef: { special: 'INDEX' } };
21
+ }
22
+ // If INDEX has no blob, the server falls back to HEAD as the baseline.
23
+ return { oldRef: { special: 'INDEX' }, newRef: { special: 'WORKING' } };
24
+ }
25
+
26
+ /**
27
+ * Map a launcher git ref onto `Git.Diff.IContent.source`; keep JupyterLab's
28
+ * enum values where downstream diff code checks for them.
29
+ */
30
+ function referenceSource(ref: GitReference | null): unknown {
31
+ if (ref === null) {
32
+ return null;
33
+ }
34
+ if ('git' in ref) {
35
+ return ref.git;
36
+ }
37
+ if (ref.special === 'WORKING') {
38
+ return Git.Diff.SpecialRef.WORKING;
39
+ }
40
+ if (ref.special === 'INDEX') {
41
+ return Git.Diff.SpecialRef.INDEX;
42
+ }
43
+ return Git.Diff.SpecialRef.BASE;
44
+ }
45
+
46
+ /**
47
+ * `Git.Diff.IModel` adapter for launcher file changes.
48
+ */
49
+ class FileChangeDiffModel implements IXtralabDiffModel {
50
+ constructor(repoPath: string, change: IFileChange) {
51
+ const { oldRef, newRef } = resolveReferences(change);
52
+ // Renames compare the new path against the old path's previous content.
53
+ const oldName = change.from ?? change.path;
54
+ this.filename = change.path;
55
+ this.oldFilename = change.from;
56
+ this.repositoryPath = repoPath;
57
+ this.isBinary = change.isBinary === true;
58
+ // Only unstaged tracked files can be reverted through a working-tree save.
59
+ this.canDiscard =
60
+ change.group === 'unstaged' && change.status !== 'untracked';
61
+ this.reference = {
62
+ label: oldName,
63
+ source: referenceSource(oldRef),
64
+ content: () =>
65
+ oldRef === null
66
+ ? Promise.resolve('')
67
+ : content(repoPath, oldName, oldRef).then(
68
+ result => result.content ?? ''
69
+ )
70
+ };
71
+ this.challenger = {
72
+ label: change.path,
73
+ source: referenceSource(newRef),
74
+ content: () =>
75
+ newRef === null
76
+ ? Promise.resolve('')
77
+ : content(repoPath, change.path, newRef).then(
78
+ result => result.content ?? ''
79
+ )
80
+ };
81
+ }
82
+
83
+ readonly changed: ISignal<Git.Diff.IModel, Git.Diff.IModelChange> =
84
+ new Signal<Git.Diff.IModel, Git.Diff.IModelChange>(this);
85
+ reference: Git.Diff.IContent;
86
+ challenger: Git.Diff.IContent;
87
+ readonly filename: string;
88
+ readonly oldFilename: string | undefined;
89
+ readonly repositoryPath: string;
90
+ readonly isBinary: boolean;
91
+ readonly canDiscard: boolean;
92
+ }
93
+
94
+ /**
95
+ * Build a diff model for a single {@link IFileChange}.
96
+ */
97
+ export function fileChangeToDiffModel(
98
+ repoPath: string,
99
+ change: IFileChange
100
+ ): IXtralabDiffModel {
101
+ return new FileChangeDiffModel(repoPath, change);
102
+ }
@@ -1,538 +1,20 @@
1
- import * as React from 'react';
2
-
3
1
  import {
4
2
  JupyterFrontEnd,
5
3
  JupyterFrontEndPlugin
6
4
  } from '@jupyterlab/application';
7
5
  import { IThemeManager } from '@jupyterlab/apputils';
8
- import { PathExt } from '@jupyterlab/coreutils';
9
6
  import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
10
- import { Contents } from '@jupyterlab/services';
11
- import { ReactWidget } from '@jupyterlab/ui-components';
12
7
  import { Git, IGitExtension } from '@jupyterlab/git';
13
- import { PromiseDelegate } from '@lumino/coreutils';
14
- import { ISignal, Signal } from '@lumino/signaling';
15
8
 
16
9
  import {
17
- DIFF_WIDGET_CSS_CLASS,
18
- DiffStyleControl,
19
- DiffSurface,
20
- NotebookViewModeControl,
21
- isDarkTheme,
22
- readStoredDiffStyle,
23
- readStoredNotebookViewMode,
24
- writeStoredDiffStyle,
25
- writeStoredNotebookViewMode,
26
- type DiffStyle,
27
- type NotebookDiffViewMode
28
- } from './diffSurface';
29
- import { IMAGE_DIFF_EXTENSIONS, imageDataType } from './imageDiff';
30
-
31
- /**
32
- * `jupyterlab-git` diff providers backed by xtralab's `@pierre/diffs`
33
- * rendering.
34
- *
35
- * jupyterlab-git's notebook, plain-text and image diff plugins are disabled
36
- * (see `package.json`); this module registers a diff factory in their place
37
- * via `registerDiffProvider` / `registerFallbackDiffProvider`. The upstream
38
- * git panel, commands, status bar and context menus keep working unchanged;
39
- * only the rendered diff is xtralab's.
40
- *
41
- * The factory adapts a `Git.Diff.IModel` (reference vs. challenger content
42
- * getters) onto the shared {@link DiffSurface}, so a diff opened from the
43
- * git panel looks identical to one opened from the launcher dashboard.
44
- */
45
-
46
- interface IXtralabDiffContext {
47
- contentsManager: Contents.IManager;
48
- rendermime: IRenderMimeRegistry | null;
49
- themeManager: IThemeManager | null;
50
- }
51
-
52
- /**
53
- * A `Git.Diff.IDiffWidget` that renders through {@link DiffSurface}.
54
- *
55
- * `jupyterlab-git` constructs this via the registered factory, hosts it in
56
- * its own `PreviewMainAreaWidget`, and reads `model` / calls `refresh()` /
57
- * `getResolvedFile()` on it. It owns the notebook view-mode state (mounted
58
- * into the diff toolbar `jupyterlab-git` hands the factory) and a reload
59
- * nonce so `refresh()` re-pulls the model's content.
60
- */
61
- class XtralabDiffWidget extends ReactWidget implements Git.Diff.IDiffWidget {
62
- constructor(model: Git.Diff.IModel, context: IXtralabDiffContext) {
63
- super();
64
- this._model = model;
65
- this._context = context;
66
- this._notebookViewMode = readStoredNotebookViewMode();
67
- this._diffStyle = readStoredDiffStyle();
68
- // `jp-git-diff-root` borrows jupyterlab-git's host sizing (height:100%
69
- // flex column inside its PreviewMainAreaWidget); `jp-xtralab-DiffWidget`
70
- // brings our diff theming + the `.jp-xtralab-DiffWidget-*` styles.
71
- this.addClass('jp-git-diff-root');
72
- this.addClass(DIFF_WIDGET_CSS_CLASS);
73
- }
74
-
75
- /** The diff model, as required by `Git.Diff.IDiffWidget`. */
76
- get model(): Git.Diff.IModel {
77
- return this._model;
78
- }
79
-
80
- /**
81
- * We render a two-way (reference vs. challenger) view and do not provide
82
- * a merge editor, so there is nothing to leave unresolved — report
83
- * resolved so jupyterlab-git's merge-conflict flow is not blocked.
84
- */
85
- get isFileResolved(): boolean {
86
- return true;
87
- }
88
-
89
- /**
90
- * Used by jupyterlab-git's "Mark as resolved" button. For merge
91
- * conflicts, jupyterlab-git passes the working-tree result as `base`;
92
- * keep that content rather than overwriting it with either compared side.
93
- */
94
- async getResolvedFile(): Promise<Partial<Contents.IModel>> {
95
- const source =
96
- this._model.hasConflict === true && this._model.base !== undefined
97
- ? this._model.base
98
- : this._model.challenger;
99
- const content = await source.content();
100
- return {
101
- type: 'file',
102
- format: imageDataType(this._model.filename) !== null ? 'base64' : 'text',
103
- content
104
- };
105
- }
106
-
107
- /**
108
- * Re-pull both sides of the model and re-render. jupyterlab-git wires a
109
- * "Refresh" toolbar button to this whenever `model.changed` fires (e.g.
110
- * the compared `HEAD` moved).
111
- */
112
- async refresh(): Promise<void> {
113
- const done = new PromiseDelegate<void>();
114
- this._pendingRefresh = done;
115
- this._reloadNonce += 1;
116
- this.update();
117
- await done.promise;
118
- }
119
-
120
- /** The notebook rendered-vs-JSON choice; mirrored into the toolbar. */
121
- get notebookViewMode(): NotebookDiffViewMode {
122
- return this._notebookViewMode;
123
- }
124
-
125
- setNotebookViewMode(mode: NotebookDiffViewMode): void {
126
- if (mode === this._notebookViewMode) {
127
- return;
128
- }
129
- this._notebookViewMode = mode;
130
- writeStoredNotebookViewMode(mode);
131
- this._notebookViewModeChanged.emit(mode);
132
- }
133
-
134
- get notebookViewModeChanged(): ISignal<this, NotebookDiffViewMode> {
135
- return this._notebookViewModeChanged;
136
- }
137
-
138
- get hasNotebookView(): boolean {
139
- return this._hasNotebookView;
140
- }
141
-
142
- setHasNotebookView(value: boolean): void {
143
- if (value === this._hasNotebookView) {
144
- return;
145
- }
146
- this._hasNotebookView = value;
147
- this._hasNotebookViewChanged.emit(value);
148
- }
149
-
150
- get hasNotebookViewChanged(): ISignal<this, boolean> {
151
- return this._hasNotebookViewChanged;
152
- }
153
-
154
- /** The user's current split-vs-unified layout choice for file diffs. */
155
- get diffStyle(): DiffStyle {
156
- return this._diffStyle;
157
- }
158
-
159
- setDiffStyle(style: DiffStyle): void {
160
- if (style === this._diffStyle) {
161
- return;
162
- }
163
- this._diffStyle = style;
164
- writeStoredDiffStyle(style);
165
- this._diffStyleChanged.emit(style);
166
- }
167
-
168
- get diffStyleChanged(): ISignal<this, DiffStyle> {
169
- return this._diffStyleChanged;
170
- }
171
-
172
- /**
173
- * Whether the textual/code file diff (the only view the split-vs-unified
174
- * choice affects) is currently active; mirrored into the toolbar so the
175
- * Split/Unified selector only shows when it applies.
176
- */
177
- get fileDiffActive(): boolean {
178
- return this._fileDiffActive;
179
- }
180
-
181
- setFileDiffActive(value: boolean): void {
182
- if (value === this._fileDiffActive) {
183
- return;
184
- }
185
- this._fileDiffActive = value;
186
- this._fileDiffActiveChanged.emit(value);
187
- }
188
-
189
- get fileDiffActiveChanged(): ISignal<this, boolean> {
190
- return this._fileDiffActiveChanged;
191
- }
192
-
193
- /** Resolve the in-flight {@link refresh} promise, if any. */
194
- settleRefresh(): void {
195
- const pending = this._pendingRefresh;
196
- this._pendingRefresh = null;
197
- pending?.resolve();
198
- }
199
-
200
- get reloadNonce(): number {
201
- return this._reloadNonce;
202
- }
203
-
204
- get context(): IXtralabDiffContext {
205
- return this._context;
206
- }
207
-
208
- protected render(): React.ReactElement {
209
- return <ModelDiffView widget={this} />;
210
- }
211
-
212
- private _model: Git.Diff.IModel;
213
- private _context: IXtralabDiffContext;
214
- private _reloadNonce = 0;
215
- private _pendingRefresh: PromiseDelegate<void> | null = null;
216
- private _notebookViewMode: NotebookDiffViewMode;
217
- private _notebookViewModeChanged = new Signal<this, NotebookDiffViewMode>(
218
- this
219
- );
220
- private _hasNotebookView = false;
221
- private _hasNotebookViewChanged = new Signal<this, boolean>(this);
222
- private _diffStyle: DiffStyle;
223
- private _diffStyleChanged = new Signal<this, DiffStyle>(this);
224
- private _fileDiffActive = false;
225
- private _fileDiffActiveChanged = new Signal<this, boolean>(this);
226
- }
227
-
228
- interface IModelDiffState {
229
- loading: boolean;
230
- oldText: string;
231
- newText: string;
232
- error: string | null;
233
- }
234
-
235
- function ModelDiffView(props: {
236
- widget: XtralabDiffWidget;
237
- }): React.ReactElement {
238
- const { widget } = props;
239
- const model = widget.model;
240
- const { contentsManager, rendermime, themeManager } = widget.context;
241
- const nonce = widget.reloadNonce;
242
-
243
- const [state, setState] = React.useState<IModelDiffState>({
244
- loading: true,
245
- oldText: '',
246
- newText: '',
247
- error: null
248
- });
249
-
250
- // Mirror the widget's notebook view mode so toolbar-driven changes
251
- // re-render the diff. The widget owns the canonical value (so the
252
- // toolbar can read/write it without going through React).
253
- const [notebookViewMode, setNotebookViewMode] =
254
- React.useState<NotebookDiffViewMode>(() => widget.notebookViewMode);
255
- React.useEffect(() => {
256
- const handler = (
257
- _sender: XtralabDiffWidget,
258
- mode: NotebookDiffViewMode
259
- ): void => {
260
- setNotebookViewMode(mode);
261
- };
262
- widget.notebookViewModeChanged.connect(handler);
263
- setNotebookViewMode(widget.notebookViewMode);
264
- return () => {
265
- widget.notebookViewModeChanged.disconnect(handler);
266
- };
267
- }, [widget]);
268
-
269
- const handleNotebookAvailabilityChange = React.useCallback(
270
- (available: boolean) => {
271
- widget.setHasNotebookView(available);
272
- },
273
- [widget]
274
- );
275
-
276
- // Mirror the widget's diff-style choice so toolbar-driven changes
277
- // re-render the diff. The widget owns the canonical value.
278
- const [diffStyle, setDiffStyle] = React.useState<DiffStyle>(
279
- () => widget.diffStyle
280
- );
281
- React.useEffect(() => {
282
- const handler = (_sender: XtralabDiffWidget, style: DiffStyle): void => {
283
- setDiffStyle(style);
284
- };
285
- widget.diffStyleChanged.connect(handler);
286
- setDiffStyle(widget.diffStyle);
287
- return () => {
288
- widget.diffStyleChanged.disconnect(handler);
289
- };
290
- }, [widget]);
291
-
292
- const handleFileDiffActiveChange = React.useCallback(
293
- (active: boolean) => {
294
- widget.setFileDiffActive(active);
295
- },
296
- [widget]
297
- );
298
-
299
- const [dark, setDark] = React.useState<boolean>(() =>
300
- isDarkTheme(themeManager)
301
- );
302
- React.useEffect(() => {
303
- if (themeManager !== null) {
304
- const handler = (): void => setDark(isDarkTheme(themeManager));
305
- themeManager.themeChanged.connect(handler);
306
- return () => {
307
- themeManager.themeChanged.disconnect(handler);
308
- };
309
- }
310
- const observer = new MutationObserver(() =>
311
- setDark(isDarkTheme(themeManager))
312
- );
313
- observer.observe(document.body, {
314
- attributes: true,
315
- attributeFilter: ['data-jp-theme-light']
316
- });
317
- return () => {
318
- observer.disconnect();
319
- };
320
- }, [themeManager]);
321
-
322
- // Fetch both sides whenever the model or the reload nonce changes. The
323
- // model's `content()` getters target the right git refs internally
324
- // (working tree, index, a commit, …) so we stay content-source-agnostic.
325
- React.useEffect(() => {
326
- let cancelled = false;
327
- setState({ loading: true, oldText: '', newText: '', error: null });
328
- void (async () => {
329
- try {
330
- const [oldText, newText] = await Promise.all([
331
- model.reference.content(),
332
- model.challenger.content()
333
- ]);
334
- if (cancelled) {
335
- return;
336
- }
337
- setState({
338
- loading: false,
339
- oldText: oldText ?? '',
340
- newText: newText ?? '',
341
- error: null
342
- });
343
- } catch (err) {
344
- if (cancelled) {
345
- return;
346
- }
347
- setState({
348
- loading: false,
349
- oldText: '',
350
- newText: '',
351
- error: err instanceof Error ? err.message : String(err)
352
- });
353
- } finally {
354
- if (!cancelled) {
355
- // Let an in-flight `widget.refresh()` resolve once the reload
356
- // has settled (jupyterlab-git only awaits it to hide its
357
- // refresh button).
358
- widget.settleRefresh();
359
- }
360
- }
361
- })();
362
- return () => {
363
- cancelled = true;
364
- };
365
- }, [widget, model, nonce]);
366
-
367
- // Per-hunk discard is only meaningful when the challenger is the working
368
- // tree (an unstaged change) and we are not in a three-way merge view.
369
- const canDiscardHunk =
370
- model.challenger.source === Git.Diff.SpecialRef.WORKING &&
371
- model.hasConflict !== true;
372
-
373
- const serverPath = React.useMemo(
374
- () => PathExt.join(model.repositoryPath ?? '', model.filename),
375
- [model.repositoryPath, model.filename]
376
- );
377
-
378
- const hunkDiscard = React.useMemo(
379
- () => ({
380
- enabled: canDiscardHunk,
381
- save: async (text: string) => {
382
- await contentsManager.save(serverPath, {
383
- type: 'file',
384
- format: 'text',
385
- content: text
386
- });
387
- },
388
- onAfterSave: () => {
389
- // jupyterlab-git's panel auto-refreshes off the contents
390
- // `fileChanged` signal the save above emits; we just need to
391
- // re-pull so the diff reflects the reverted hunk. (We do not
392
- // auto-close: jupyterlab-git owns the diff tab.)
393
- void widget.refresh();
394
- }
395
- }),
396
- [canDiscardHunk, contentsManager, serverPath, widget]
397
- );
398
-
399
- return (
400
- <DiffSurface
401
- loading={state.loading}
402
- error={state.error}
403
- isBinary={false}
404
- oldText={state.oldText}
405
- newText={state.newText}
406
- newName={model.filename}
407
- oldName={model.filename}
408
- dark={dark}
409
- rendermime={rendermime}
410
- notebookViewMode={notebookViewMode}
411
- diffStyle={diffStyle}
412
- onNotebookAvailabilityChange={handleNotebookAvailabilityChange}
413
- onFileDiffActiveChange={handleFileDiffActiveChange}
414
- hunkDiscard={hunkDiscard}
415
- />
416
- );
417
- }
10
+ IXtralabDiffContext,
11
+ XtralabDiffWidget,
12
+ addDiffToolbarItems
13
+ } from './diffWidget';
14
+ import { IMAGE_DIFF_EXTENSIONS } from './imageDiff';
418
15
 
419
16
  /**
420
- * Notebook/JSON toggle mounted into the diff toolbar that jupyterlab-git
421
- * passes to the factory. Bound to the widget's signals so it stays in sync
422
- * with the rendered surface and only appears for parseable notebooks.
423
- */
424
- class NotebookViewModeToolbarItem extends ReactWidget {
425
- constructor(widget: XtralabDiffWidget) {
426
- super();
427
- this._widget = widget;
428
- this.addClass('jp-xtralab-DiffWidget-viewModeToolbarItem');
429
- }
430
-
431
- protected render(): React.ReactElement {
432
- return <ToolbarControl widget={this._widget} />;
433
- }
434
-
435
- private _widget: XtralabDiffWidget;
436
- }
437
-
438
- function ToolbarControl(props: {
439
- widget: XtralabDiffWidget;
440
- }): React.ReactElement {
441
- const { widget } = props;
442
- const [mode, setMode] = React.useState<NotebookDiffViewMode>(
443
- () => widget.notebookViewMode
444
- );
445
- const [available, setAvailable] = React.useState<boolean>(
446
- () => widget.hasNotebookView
447
- );
448
- React.useEffect(() => {
449
- const onMode = (
450
- _sender: XtralabDiffWidget,
451
- next: NotebookDiffViewMode
452
- ): void => {
453
- setMode(next);
454
- };
455
- const onAvailable = (_sender: XtralabDiffWidget, next: boolean): void => {
456
- setAvailable(next);
457
- };
458
- widget.notebookViewModeChanged.connect(onMode);
459
- widget.hasNotebookViewChanged.connect(onAvailable);
460
- setMode(widget.notebookViewMode);
461
- setAvailable(widget.hasNotebookView);
462
- return () => {
463
- widget.notebookViewModeChanged.disconnect(onMode);
464
- widget.hasNotebookViewChanged.disconnect(onAvailable);
465
- };
466
- }, [widget]);
467
-
468
- return (
469
- <NotebookViewModeControl
470
- mode={mode}
471
- available={available}
472
- onChange={next => widget.setNotebookViewMode(next)}
473
- />
474
- );
475
- }
476
-
477
- /**
478
- * Split/Unified toggle mounted into the diff toolbar jupyterlab-git passes
479
- * to the factory. Bound to the widget's signals so it stays in sync with the
480
- * rendered surface and only appears while the textual/code file diff is the
481
- * active view.
482
- */
483
- class DiffStyleToolbarItem extends ReactWidget {
484
- constructor(widget: XtralabDiffWidget) {
485
- super();
486
- this._widget = widget;
487
- this.addClass('jp-xtralab-DiffWidget-diffStyleToolbarItem');
488
- }
489
-
490
- protected render(): React.ReactElement {
491
- return <DiffStyleControlBound widget={this._widget} />;
492
- }
493
-
494
- private _widget: XtralabDiffWidget;
495
- }
496
-
497
- function DiffStyleControlBound(props: {
498
- widget: XtralabDiffWidget;
499
- }): React.ReactElement {
500
- const { widget } = props;
501
- const [style, setStyle] = React.useState<DiffStyle>(() => widget.diffStyle);
502
- const [available, setAvailable] = React.useState<boolean>(
503
- () => widget.fileDiffActive
504
- );
505
- React.useEffect(() => {
506
- const onStyle = (_sender: XtralabDiffWidget, next: DiffStyle): void => {
507
- setStyle(next);
508
- };
509
- const onAvailable = (_sender: XtralabDiffWidget, next: boolean): void => {
510
- setAvailable(next);
511
- };
512
- widget.diffStyleChanged.connect(onStyle);
513
- widget.fileDiffActiveChanged.connect(onAvailable);
514
- setStyle(widget.diffStyle);
515
- setAvailable(widget.fileDiffActive);
516
- return () => {
517
- widget.diffStyleChanged.disconnect(onStyle);
518
- widget.fileDiffActiveChanged.disconnect(onAvailable);
519
- };
520
- }, [widget]);
521
-
522
- return (
523
- <DiffStyleControl
524
- diffStyle={style}
525
- available={available}
526
- onChange={next => widget.setDiffStyle(next)}
527
- />
528
- );
529
- }
530
-
531
- /**
532
- * Build the `Git.Diff.Factory` xtralab registers with jupyterlab-git. The
533
- * factory closes over the application-level context (contents manager for
534
- * hunk discard, rendermime for notebook outputs, theme manager) that the
535
- * `Git.Diff.IFactoryOptions` does not carry.
17
+ * Build the `Git.Diff.Factory` registered with jupyterlab-git.
536
18
  */
537
19
  function makeXtralabDiffFactory(
538
20
  context: IXtralabDiffContext
@@ -540,24 +22,12 @@ function makeXtralabDiffFactory(
540
22
  return async (options: Git.Diff.IFactoryOptions) => {
541
23
  const widget = new XtralabDiffWidget(options.model, context);
542
24
  if (options.toolbar) {
543
- options.toolbar.addItem(
544
- 'xtralab-notebook-view-mode',
545
- new NotebookViewModeToolbarItem(widget)
546
- );
547
- options.toolbar.addItem(
548
- 'xtralab-diff-style',
549
- new DiffStyleToolbarItem(widget)
550
- );
25
+ addDiffToolbarItems(options.toolbar, widget);
551
26
  }
552
27
  return widget;
553
28
  };
554
29
  }
555
30
 
556
- /**
557
- * Registers xtralab's diff rendering as jupyterlab-git's diff providers:
558
- * `@pierre/diffs` for text and notebooks, the bundled `<img>`-based view
559
- * for raster images.
560
- */
561
31
  const diffProviderPlugin: JupyterFrontEndPlugin<void> = {
562
32
  id: 'xtralab:git-diff-providers',
563
33
  description:
@@ -577,9 +47,7 @@ const diffProviderPlugin: JupyterFrontEndPlugin<void> = {
577
47
  themeManager
578
48
  });
579
49
 
580
- // Notebooks and raster images get extension-specific providers; every
581
- // other text file falls back to the same factory. One factory serves
582
- // all three — the surface auto-detects notebooks/images by filename.
50
+ // Use one shared factory; the surface detects notebooks/images by filename.
583
51
  gitExtension.registerDiffProvider('XtralabNotebook', ['.ipynb'], factory);
584
52
  gitExtension.registerDiffProvider(
585
53
  'XtralabImage',