xtralab 0.3.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.
Files changed (60) hide show
  1. package/README.md +11 -4
  2. package/lib/editorBreadcrumbs/index.d.ts +11 -0
  3. package/lib/editorBreadcrumbs/index.js +41 -0
  4. package/lib/editorBreadcrumbs/widget.d.ts +34 -0
  5. package/lib/editorBreadcrumbs/widget.js +131 -0
  6. package/lib/editorIndent/index.d.ts +17 -0
  7. package/lib/editorIndent/index.js +142 -0
  8. package/lib/fileBrowser/commands.d.ts +3 -0
  9. package/lib/fileBrowser/commands.js +40 -1
  10. package/lib/fileBrowser/fileBrowser.js +393 -10
  11. package/lib/fileBrowser/gitStatus.d.ts +13 -0
  12. package/lib/fileBrowser/gitStatus.js +60 -0
  13. package/lib/fileBrowser/widget.d.ts +48 -0
  14. package/lib/fileBrowser/widget.js +24 -0
  15. package/lib/git/commands.d.ts +12 -13
  16. package/lib/git/commands.js +7 -28
  17. package/lib/git/diffProvider.d.ts +8 -0
  18. package/lib/git/diffProvider.js +314 -0
  19. package/lib/git/diffSurface.d.ts +115 -0
  20. package/lib/git/diffSurface.js +342 -0
  21. package/lib/git/diffWidget.d.ts +8 -13
  22. package/lib/git/diffWidget.js +65 -311
  23. package/lib/git/imageDiff.d.ts +23 -0
  24. package/lib/git/imageDiff.js +184 -0
  25. package/lib/git/index.d.ts +10 -8
  26. package/lib/git/index.js +37 -52
  27. package/lib/index.d.ts +3 -2
  28. package/lib/index.js +9 -4
  29. package/lib/launcher/dashboard.js +27 -15
  30. package/package.json +24 -4
  31. package/src/editorBreadcrumbs/index.ts +70 -0
  32. package/src/editorBreadcrumbs/widget.ts +158 -0
  33. package/src/editorIndent/index.ts +177 -0
  34. package/src/fileBrowser/commands.ts +43 -1
  35. package/src/fileBrowser/fileBrowser.tsx +427 -11
  36. package/src/fileBrowser/gitStatus.ts +66 -0
  37. package/src/fileBrowser/widget.tsx +77 -0
  38. package/src/git/commands.ts +12 -38
  39. package/src/git/diffProvider.tsx +462 -0
  40. package/src/git/diffSurface.tsx +588 -0
  41. package/src/git/diffWidget.tsx +105 -465
  42. package/src/git/imageDiff.tsx +368 -0
  43. package/src/git/index.ts +38 -68
  44. package/src/index.ts +9 -4
  45. package/src/launcher/dashboard.tsx +28 -15
  46. package/style/editorBreadcrumbs.css +86 -0
  47. package/style/git.css +158 -266
  48. package/style/index.css +2 -0
  49. package/style/index.js +1 -0
  50. package/style/launcher.css +0 -5
  51. package/style/tabs.css +66 -0
  52. package/lib/git/icons.d.ts +0 -14
  53. package/lib/git/icons.js +0 -19
  54. package/lib/git/panel.d.ts +0 -29
  55. package/lib/git/panel.js +0 -42
  56. package/lib/git/panelComponent.d.ts +0 -28
  57. package/lib/git/panelComponent.js +0 -303
  58. package/src/git/icons.ts +0 -20
  59. package/src/git/panel.tsx +0 -56
  60. package/src/git/panelComponent.tsx +0 -561
@@ -1,8 +1,7 @@
1
1
  import { JupyterFrontEnd } from '@jupyterlab/application';
2
- import { ICommandPalette, IThemeManager } from '@jupyterlab/apputils';
2
+ import { IThemeManager } from '@jupyterlab/apputils';
3
3
  import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
4
4
  import { Contents } from '@jupyterlab/services';
5
- import { refreshIcon } from '@jupyterlab/ui-components';
6
5
  import { ReadonlyPartialJSONObject } from '@lumino/coreutils';
7
6
 
8
7
  import {
@@ -10,16 +9,15 @@ import {
10
9
  diffWidgetId,
11
10
  updateGitDiffWidget
12
11
  } from './diffWidget';
13
- import { GitPanel } from './panel';
14
12
  import { IFileChange } from './tokens';
15
13
 
16
14
  /**
17
- * Command IDs exposed by the git plugin. Both are registered in
18
- * {@link registerGitCommands} so other extensions, the command palette and
19
- * keybindings can drive the panel without going through the React internals.
15
+ * Command IDs exposed by the launcher's git diff path. The launcher
16
+ * dashboard's "Changes" section drives {@link CommandIDs.openDiff} to open
17
+ * its own diff tab, independent of `jupyterlab-git` (whose panel reaches
18
+ * the same rendering via the providers in `diffProvider.tsx`).
20
19
  */
21
20
  export namespace CommandIDs {
22
- export const refresh = 'xtralab:git:refresh';
23
21
  export const openDiff = 'xtralab:git:open-diff';
24
22
  }
25
23
 
@@ -39,9 +37,7 @@ export namespace CommandArguments {
39
37
 
40
38
  export interface IRegisterGitCommandsOptions {
41
39
  app: JupyterFrontEnd;
42
- panel: GitPanel;
43
40
  themeManager: IThemeManager | null;
44
- commandPalette: ICommandPalette | null;
45
41
  /**
46
42
  * Contents manager used by the diff widget to write hunk-discard
47
43
  * results back to the working tree.
@@ -56,7 +52,10 @@ export interface IRegisterGitCommandsOptions {
56
52
  rendermime: IRenderMimeRegistry | null;
57
53
  /**
58
54
  * Called whenever the diff widget mutates the working tree (e.g. after
59
- * a hunk discard) so the plugin can refresh the changes panel.
55
+ * a hunk discard). The launcher dashboard polls git status on its own
56
+ * cadence and `jupyterlab-git`'s panel auto-refreshes on the contents
57
+ * `fileChanged` signal the save emits, so this is a hook for any extra
58
+ * bookkeeping rather than a required refresh.
60
59
  */
61
60
  onChanged: () => void;
62
61
  /**
@@ -77,18 +76,15 @@ export interface IRegisterGitCommandsOptions {
77
76
  }
78
77
 
79
78
  /**
80
- * Register the git commands on the application command registry. Returns
81
- * a disposer that clears the command palette items the registration created
82
- * (the commands themselves stay registered for the lifetime of the plugin).
79
+ * Register the launcher's git diff command on the application command
80
+ * registry.
83
81
  */
84
82
  export function registerGitCommands(
85
83
  options: IRegisterGitCommandsOptions
86
- ): () => void {
84
+ ): void {
87
85
  const {
88
86
  app,
89
- panel,
90
87
  themeManager,
91
- commandPalette,
92
88
  contentsManager,
93
89
  rendermime,
94
90
  onChanged,
@@ -98,15 +94,6 @@ export function registerGitCommands(
98
94
  } = options;
99
95
  const { commands } = app;
100
96
 
101
- commands.addCommand(CommandIDs.refresh, {
102
- label: 'Refresh Git Changes',
103
- caption: 'Refresh the git changes panel',
104
- icon: refreshIcon,
105
- execute: () => {
106
- panel.refresh();
107
- }
108
- });
109
-
110
97
  commands.addCommand(CommandIDs.openDiff, {
111
98
  label: 'Open Git Diff',
112
99
  caption: 'Open a side-by-side diff for a changed file',
@@ -139,19 +126,6 @@ export function registerGitCommands(
139
126
  app.shell.activateById(widget.id);
140
127
  }
141
128
  });
142
-
143
- const paletteItems = commandPalette
144
- ? [
145
- commandPalette.addItem({
146
- command: CommandIDs.refresh,
147
- category: 'Git'
148
- })
149
- ]
150
- : [];
151
-
152
- return () => {
153
- paletteItems.forEach(item => item.dispose());
154
- };
155
129
  }
156
130
 
157
131
  export { diffWidgetId };
@@ -0,0 +1,462 @@
1
+ import * as React from 'react';
2
+
3
+ import {
4
+ JupyterFrontEnd,
5
+ JupyterFrontEndPlugin
6
+ } from '@jupyterlab/application';
7
+ import { IThemeManager } from '@jupyterlab/apputils';
8
+ import { PathExt } from '@jupyterlab/coreutils';
9
+ import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
10
+ import { Contents } from '@jupyterlab/services';
11
+ import { ReactWidget } from '@jupyterlab/ui-components';
12
+ import { Git, IGitExtension } from '@jupyterlab/git';
13
+ import { PromiseDelegate } from '@lumino/coreutils';
14
+ import { ISignal, Signal } from '@lumino/signaling';
15
+
16
+ import {
17
+ DIFF_WIDGET_CSS_CLASS,
18
+ DiffSurface,
19
+ NotebookViewModeControl,
20
+ isDarkTheme,
21
+ readStoredNotebookViewMode,
22
+ writeStoredNotebookViewMode,
23
+ type NotebookDiffViewMode
24
+ } from './diffSurface';
25
+ import { IMAGE_DIFF_EXTENSIONS, imageDataType } from './imageDiff';
26
+
27
+ /**
28
+ * `jupyterlab-git` diff providers backed by xtralab's `@pierre/diffs`
29
+ * rendering.
30
+ *
31
+ * jupyterlab-git's notebook, plain-text and image diff plugins are disabled
32
+ * (see `package.json`); this module registers a diff factory in their place
33
+ * via `registerDiffProvider` / `registerFallbackDiffProvider`. The upstream
34
+ * git panel, commands, status bar and context menus keep working unchanged;
35
+ * only the rendered diff is xtralab's.
36
+ *
37
+ * The factory adapts a `Git.Diff.IModel` (reference vs. challenger content
38
+ * getters) onto the shared {@link DiffSurface}, so a diff opened from the
39
+ * git panel looks identical to one opened from the launcher dashboard.
40
+ */
41
+
42
+ interface IXtralabDiffContext {
43
+ contentsManager: Contents.IManager;
44
+ rendermime: IRenderMimeRegistry | null;
45
+ themeManager: IThemeManager | null;
46
+ }
47
+
48
+ /**
49
+ * A `Git.Diff.IDiffWidget` that renders through {@link DiffSurface}.
50
+ *
51
+ * `jupyterlab-git` constructs this via the registered factory, hosts it in
52
+ * its own `PreviewMainAreaWidget`, and reads `model` / calls `refresh()` /
53
+ * `getResolvedFile()` on it. It owns the notebook view-mode state (mounted
54
+ * into the diff toolbar `jupyterlab-git` hands the factory) and a reload
55
+ * nonce so `refresh()` re-pulls the model's content.
56
+ */
57
+ class XtralabDiffWidget extends ReactWidget implements Git.Diff.IDiffWidget {
58
+ constructor(model: Git.Diff.IModel, context: IXtralabDiffContext) {
59
+ super();
60
+ this._model = model;
61
+ this._context = context;
62
+ this._notebookViewMode = readStoredNotebookViewMode();
63
+ // `jp-git-diff-root` borrows jupyterlab-git's host sizing (height:100%
64
+ // flex column inside its PreviewMainAreaWidget); `jp-xtralab-DiffWidget`
65
+ // brings our diff theming + the `.jp-xtralab-DiffWidget-*` styles.
66
+ this.addClass('jp-git-diff-root');
67
+ this.addClass(DIFF_WIDGET_CSS_CLASS);
68
+ }
69
+
70
+ /** The diff model, as required by `Git.Diff.IDiffWidget`. */
71
+ get model(): Git.Diff.IModel {
72
+ return this._model;
73
+ }
74
+
75
+ /**
76
+ * We render a two-way (reference vs. challenger) view and do not provide
77
+ * a merge editor, so there is nothing to leave unresolved — report
78
+ * resolved so jupyterlab-git's merge-conflict flow is not blocked.
79
+ */
80
+ get isFileResolved(): boolean {
81
+ return true;
82
+ }
83
+
84
+ /**
85
+ * Used by jupyterlab-git's "Mark as resolved" button. For merge
86
+ * conflicts, jupyterlab-git passes the working-tree result as `base`;
87
+ * keep that content rather than overwriting it with either compared side.
88
+ */
89
+ async getResolvedFile(): Promise<Partial<Contents.IModel>> {
90
+ const source =
91
+ this._model.hasConflict === true && this._model.base !== undefined
92
+ ? this._model.base
93
+ : this._model.challenger;
94
+ const content = await source.content();
95
+ return {
96
+ type: 'file',
97
+ format: imageDataType(this._model.filename) !== null ? 'base64' : 'text',
98
+ content
99
+ };
100
+ }
101
+
102
+ /**
103
+ * Re-pull both sides of the model and re-render. jupyterlab-git wires a
104
+ * "Refresh" toolbar button to this whenever `model.changed` fires (e.g.
105
+ * the compared `HEAD` moved).
106
+ */
107
+ async refresh(): Promise<void> {
108
+ const done = new PromiseDelegate<void>();
109
+ this._pendingRefresh = done;
110
+ this._reloadNonce += 1;
111
+ this.update();
112
+ await done.promise;
113
+ }
114
+
115
+ /** The notebook rendered-vs-JSON choice; mirrored into the toolbar. */
116
+ get notebookViewMode(): NotebookDiffViewMode {
117
+ return this._notebookViewMode;
118
+ }
119
+
120
+ setNotebookViewMode(mode: NotebookDiffViewMode): void {
121
+ if (mode === this._notebookViewMode) {
122
+ return;
123
+ }
124
+ this._notebookViewMode = mode;
125
+ writeStoredNotebookViewMode(mode);
126
+ this._notebookViewModeChanged.emit(mode);
127
+ }
128
+
129
+ get notebookViewModeChanged(): ISignal<this, NotebookDiffViewMode> {
130
+ return this._notebookViewModeChanged;
131
+ }
132
+
133
+ get hasNotebookView(): boolean {
134
+ return this._hasNotebookView;
135
+ }
136
+
137
+ setHasNotebookView(value: boolean): void {
138
+ if (value === this._hasNotebookView) {
139
+ return;
140
+ }
141
+ this._hasNotebookView = value;
142
+ this._hasNotebookViewChanged.emit(value);
143
+ }
144
+
145
+ get hasNotebookViewChanged(): ISignal<this, boolean> {
146
+ return this._hasNotebookViewChanged;
147
+ }
148
+
149
+ /** Resolve the in-flight {@link refresh} promise, if any. */
150
+ settleRefresh(): void {
151
+ const pending = this._pendingRefresh;
152
+ this._pendingRefresh = null;
153
+ pending?.resolve();
154
+ }
155
+
156
+ get reloadNonce(): number {
157
+ return this._reloadNonce;
158
+ }
159
+
160
+ get context(): IXtralabDiffContext {
161
+ return this._context;
162
+ }
163
+
164
+ protected render(): React.ReactElement {
165
+ return <ModelDiffView widget={this} />;
166
+ }
167
+
168
+ private _model: Git.Diff.IModel;
169
+ private _context: IXtralabDiffContext;
170
+ private _reloadNonce = 0;
171
+ private _pendingRefresh: PromiseDelegate<void> | null = null;
172
+ private _notebookViewMode: NotebookDiffViewMode;
173
+ private _notebookViewModeChanged = new Signal<this, NotebookDiffViewMode>(
174
+ this
175
+ );
176
+ private _hasNotebookView = false;
177
+ private _hasNotebookViewChanged = new Signal<this, boolean>(this);
178
+ }
179
+
180
+ interface IModelDiffState {
181
+ loading: boolean;
182
+ oldText: string;
183
+ newText: string;
184
+ error: string | null;
185
+ }
186
+
187
+ function ModelDiffView(props: {
188
+ widget: XtralabDiffWidget;
189
+ }): React.ReactElement {
190
+ const { widget } = props;
191
+ const model = widget.model;
192
+ const { contentsManager, rendermime, themeManager } = widget.context;
193
+ const nonce = widget.reloadNonce;
194
+
195
+ const [state, setState] = React.useState<IModelDiffState>({
196
+ loading: true,
197
+ oldText: '',
198
+ newText: '',
199
+ error: null
200
+ });
201
+
202
+ // Mirror the widget's notebook view mode so toolbar-driven changes
203
+ // re-render the diff. The widget owns the canonical value (so the
204
+ // toolbar can read/write it without going through React).
205
+ const [notebookViewMode, setNotebookViewMode] =
206
+ React.useState<NotebookDiffViewMode>(() => widget.notebookViewMode);
207
+ React.useEffect(() => {
208
+ const handler = (
209
+ _sender: XtralabDiffWidget,
210
+ mode: NotebookDiffViewMode
211
+ ): void => {
212
+ setNotebookViewMode(mode);
213
+ };
214
+ widget.notebookViewModeChanged.connect(handler);
215
+ setNotebookViewMode(widget.notebookViewMode);
216
+ return () => {
217
+ widget.notebookViewModeChanged.disconnect(handler);
218
+ };
219
+ }, [widget]);
220
+
221
+ const handleNotebookAvailabilityChange = React.useCallback(
222
+ (available: boolean) => {
223
+ widget.setHasNotebookView(available);
224
+ },
225
+ [widget]
226
+ );
227
+
228
+ const [dark, setDark] = React.useState<boolean>(() =>
229
+ isDarkTheme(themeManager)
230
+ );
231
+ React.useEffect(() => {
232
+ if (themeManager !== null) {
233
+ const handler = (): void => setDark(isDarkTheme(themeManager));
234
+ themeManager.themeChanged.connect(handler);
235
+ return () => {
236
+ themeManager.themeChanged.disconnect(handler);
237
+ };
238
+ }
239
+ const observer = new MutationObserver(() =>
240
+ setDark(isDarkTheme(themeManager))
241
+ );
242
+ observer.observe(document.body, {
243
+ attributes: true,
244
+ attributeFilter: ['data-jp-theme-light']
245
+ });
246
+ return () => {
247
+ observer.disconnect();
248
+ };
249
+ }, [themeManager]);
250
+
251
+ // Fetch both sides whenever the model or the reload nonce changes. The
252
+ // model's `content()` getters target the right git refs internally
253
+ // (working tree, index, a commit, …) so we stay content-source-agnostic.
254
+ React.useEffect(() => {
255
+ let cancelled = false;
256
+ setState({ loading: true, oldText: '', newText: '', error: null });
257
+ void (async () => {
258
+ try {
259
+ const [oldText, newText] = await Promise.all([
260
+ model.reference.content(),
261
+ model.challenger.content()
262
+ ]);
263
+ if (cancelled) {
264
+ return;
265
+ }
266
+ setState({
267
+ loading: false,
268
+ oldText: oldText ?? '',
269
+ newText: newText ?? '',
270
+ error: null
271
+ });
272
+ } catch (err) {
273
+ if (cancelled) {
274
+ return;
275
+ }
276
+ setState({
277
+ loading: false,
278
+ oldText: '',
279
+ newText: '',
280
+ error: err instanceof Error ? err.message : String(err)
281
+ });
282
+ } finally {
283
+ if (!cancelled) {
284
+ // Let an in-flight `widget.refresh()` resolve once the reload
285
+ // has settled (jupyterlab-git only awaits it to hide its
286
+ // refresh button).
287
+ widget.settleRefresh();
288
+ }
289
+ }
290
+ })();
291
+ return () => {
292
+ cancelled = true;
293
+ };
294
+ }, [widget, model, nonce]);
295
+
296
+ // Per-hunk discard is only meaningful when the challenger is the working
297
+ // tree (an unstaged change) and we are not in a three-way merge view.
298
+ const canDiscardHunk =
299
+ model.challenger.source === Git.Diff.SpecialRef.WORKING &&
300
+ model.hasConflict !== true;
301
+
302
+ const serverPath = React.useMemo(
303
+ () => PathExt.join(model.repositoryPath ?? '', model.filename),
304
+ [model.repositoryPath, model.filename]
305
+ );
306
+
307
+ const hunkDiscard = React.useMemo(
308
+ () => ({
309
+ enabled: canDiscardHunk,
310
+ save: async (text: string) => {
311
+ await contentsManager.save(serverPath, {
312
+ type: 'file',
313
+ format: 'text',
314
+ content: text
315
+ });
316
+ },
317
+ onAfterSave: () => {
318
+ // jupyterlab-git's panel auto-refreshes off the contents
319
+ // `fileChanged` signal the save above emits; we just need to
320
+ // re-pull so the diff reflects the reverted hunk. (We do not
321
+ // auto-close: jupyterlab-git owns the diff tab.)
322
+ void widget.refresh();
323
+ }
324
+ }),
325
+ [canDiscardHunk, contentsManager, serverPath, widget]
326
+ );
327
+
328
+ return (
329
+ <DiffSurface
330
+ loading={state.loading}
331
+ error={state.error}
332
+ isBinary={false}
333
+ oldText={state.oldText}
334
+ newText={state.newText}
335
+ newName={model.filename}
336
+ oldName={model.filename}
337
+ dark={dark}
338
+ rendermime={rendermime}
339
+ notebookViewMode={notebookViewMode}
340
+ onNotebookAvailabilityChange={handleNotebookAvailabilityChange}
341
+ hunkDiscard={hunkDiscard}
342
+ />
343
+ );
344
+ }
345
+
346
+ /**
347
+ * Notebook/JSON toggle mounted into the diff toolbar that jupyterlab-git
348
+ * passes to the factory. Bound to the widget's signals so it stays in sync
349
+ * with the rendered surface and only appears for parseable notebooks.
350
+ */
351
+ class NotebookViewModeToolbarItem extends ReactWidget {
352
+ constructor(widget: XtralabDiffWidget) {
353
+ super();
354
+ this._widget = widget;
355
+ this.addClass('jp-xtralab-DiffWidget-viewModeToolbarItem');
356
+ }
357
+
358
+ protected render(): React.ReactElement {
359
+ return <ToolbarControl widget={this._widget} />;
360
+ }
361
+
362
+ private _widget: XtralabDiffWidget;
363
+ }
364
+
365
+ function ToolbarControl(props: {
366
+ widget: XtralabDiffWidget;
367
+ }): React.ReactElement {
368
+ const { widget } = props;
369
+ const [mode, setMode] = React.useState<NotebookDiffViewMode>(
370
+ () => widget.notebookViewMode
371
+ );
372
+ const [available, setAvailable] = React.useState<boolean>(
373
+ () => widget.hasNotebookView
374
+ );
375
+ React.useEffect(() => {
376
+ const onMode = (
377
+ _sender: XtralabDiffWidget,
378
+ next: NotebookDiffViewMode
379
+ ): void => {
380
+ setMode(next);
381
+ };
382
+ const onAvailable = (_sender: XtralabDiffWidget, next: boolean): void => {
383
+ setAvailable(next);
384
+ };
385
+ widget.notebookViewModeChanged.connect(onMode);
386
+ widget.hasNotebookViewChanged.connect(onAvailable);
387
+ setMode(widget.notebookViewMode);
388
+ setAvailable(widget.hasNotebookView);
389
+ return () => {
390
+ widget.notebookViewModeChanged.disconnect(onMode);
391
+ widget.hasNotebookViewChanged.disconnect(onAvailable);
392
+ };
393
+ }, [widget]);
394
+
395
+ return (
396
+ <NotebookViewModeControl
397
+ mode={mode}
398
+ available={available}
399
+ onChange={next => widget.setNotebookViewMode(next)}
400
+ />
401
+ );
402
+ }
403
+
404
+ /**
405
+ * Build the `Git.Diff.Factory` xtralab registers with jupyterlab-git. The
406
+ * factory closes over the application-level context (contents manager for
407
+ * hunk discard, rendermime for notebook outputs, theme manager) that the
408
+ * `Git.Diff.IFactoryOptions` does not carry.
409
+ */
410
+ function makeXtralabDiffFactory(
411
+ context: IXtralabDiffContext
412
+ ): Git.Diff.Factory {
413
+ return async (options: Git.Diff.IFactoryOptions) => {
414
+ const widget = new XtralabDiffWidget(options.model, context);
415
+ if (options.toolbar) {
416
+ options.toolbar.addItem(
417
+ 'xtralab-notebook-view-mode',
418
+ new NotebookViewModeToolbarItem(widget)
419
+ );
420
+ }
421
+ return widget;
422
+ };
423
+ }
424
+
425
+ /**
426
+ * Registers xtralab's diff rendering as jupyterlab-git's diff providers:
427
+ * `@pierre/diffs` for text and notebooks, the bundled `<img>`-based view
428
+ * for raster images.
429
+ */
430
+ const diffProviderPlugin: JupyterFrontEndPlugin<void> = {
431
+ id: 'xtralab:git-diff-providers',
432
+ description:
433
+ "Replaces jupyterlab-git's notebook, text and image diff plugins with xtralab's renderer.",
434
+ autoStart: true,
435
+ requires: [IGitExtension],
436
+ optional: [IRenderMimeRegistry, IThemeManager],
437
+ activate: (
438
+ app: JupyterFrontEnd,
439
+ gitExtension: IGitExtension,
440
+ rendermime: IRenderMimeRegistry | null,
441
+ themeManager: IThemeManager | null
442
+ ): void => {
443
+ const factory = makeXtralabDiffFactory({
444
+ contentsManager: app.serviceManager.contents,
445
+ rendermime,
446
+ themeManager
447
+ });
448
+
449
+ // Notebooks and raster images get extension-specific providers; every
450
+ // other text file falls back to the same factory. One factory serves
451
+ // all three — the surface auto-detects notebooks/images by filename.
452
+ gitExtension.registerDiffProvider('XtralabNotebook', ['.ipynb'], factory);
453
+ gitExtension.registerDiffProvider(
454
+ 'XtralabImage',
455
+ IMAGE_DIFF_EXTENSIONS,
456
+ factory
457
+ );
458
+ gitExtension.registerFallbackDiffProvider(factory);
459
+ }
460
+ };
461
+
462
+ export default diffProviderPlugin;