xtralab 0.7.0 → 0.9.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 (54) hide show
  1. package/README.md +18 -10
  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 -284
  16. package/lib/git/diffSurface.d.ts +64 -25
  17. package/lib/git/diffSurface.js +84 -27
  18. package/lib/git/diffTheme.d.ts +9 -0
  19. package/lib/git/diffTheme.js +31 -0
  20. package/lib/git/diffWidget.d.ts +67 -85
  21. package/lib/git/diffWidget.js +242 -318
  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 +2 -0
  27. package/lib/terminals/model.d.ts +1 -1
  28. package/lib/terminals/model.js +1 -1
  29. package/lib/terminals/widget.d.ts +1 -1
  30. package/lib/terminals/widget.js +1 -1
  31. package/package.json +3 -3
  32. package/schema/plugin.json +6 -0
  33. package/src/fileBrowser/commands.ts +91 -2
  34. package/src/fileBrowser/fileBrowser.tsx +4 -1
  35. package/src/fileBrowser/icons.ts +103 -13
  36. package/src/fileBrowser/index.ts +9 -3
  37. package/src/fileTypeIcons/index.ts +174 -0
  38. package/src/git/commands.ts +177 -43
  39. package/src/git/diffModel.ts +102 -0
  40. package/src/git/diffProvider.tsx +8 -409
  41. package/src/git/diffSurface.tsx +187 -35
  42. package/src/git/diffTheme.ts +40 -0
  43. package/src/git/diffWidget.tsx +338 -393
  44. package/src/git/index.ts +14 -28
  45. package/src/git/notebookDiff.tsx +10 -3
  46. package/src/index.ts +2 -0
  47. package/src/terminals/model.ts +1 -1
  48. package/src/terminals/widget.tsx +1 -1
  49. package/style/base.css +1 -0
  50. package/style/git.css +40 -12
  51. package/style/index.css +1 -0
  52. package/style/index.js +1 -0
  53. package/style/launcher.css +46 -18
  54. package/style/sidebar.css +14 -0
@@ -1,411 +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
- DiffSurface,
19
- NotebookViewModeControl,
20
- isDarkTheme,
21
- readStoredNotebookViewMode,
22
- writeStoredNotebookViewMode,
23
- type NotebookDiffViewMode
24
- } from './diffSurface';
25
- import { IMAGE_DIFF_EXTENSIONS, imageDataType } from './imageDiff';
10
+ IXtralabDiffContext,
11
+ XtralabDiffWidget,
12
+ addDiffToolbarItems
13
+ } from './diffWidget';
14
+ import { IMAGE_DIFF_EXTENSIONS } from './imageDiff';
26
15
 
27
16
  /**
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.
17
+ * Build the `Git.Diff.Factory` registered with jupyterlab-git.
409
18
  */
410
19
  function makeXtralabDiffFactory(
411
20
  context: IXtralabDiffContext
@@ -413,20 +22,12 @@ function makeXtralabDiffFactory(
413
22
  return async (options: Git.Diff.IFactoryOptions) => {
414
23
  const widget = new XtralabDiffWidget(options.model, context);
415
24
  if (options.toolbar) {
416
- options.toolbar.addItem(
417
- 'xtralab-notebook-view-mode',
418
- new NotebookViewModeToolbarItem(widget)
419
- );
25
+ addDiffToolbarItems(options.toolbar, widget);
420
26
  }
421
27
  return widget;
422
28
  };
423
29
  }
424
30
 
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
31
  const diffProviderPlugin: JupyterFrontEndPlugin<void> = {
431
32
  id: 'xtralab:git-diff-providers',
432
33
  description:
@@ -446,9 +47,7 @@ const diffProviderPlugin: JupyterFrontEndPlugin<void> = {
446
47
  themeManager
447
48
  });
448
49
 
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.
50
+ // Use one shared factory; the surface detects notebooks/images by filename.
452
51
  gitExtension.registerDiffProvider('XtralabNotebook', ['.ipynb'], factory);
453
52
  gitExtension.registerDiffProvider(
454
53
  'XtralabImage',