xtralab 0.15.1 → 0.15.2

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 (39) hide show
  1. package/README.md +58 -33
  2. package/lib/fileBrowser/commands.d.ts +1 -0
  3. package/lib/fileBrowser/commands.js +46 -1
  4. package/lib/fileBrowser/fileBrowser.js +4 -8
  5. package/lib/fileBrowser/gitStatus.d.ts +16 -5
  6. package/lib/fileBrowser/gitStatus.js +28 -17
  7. package/lib/fileBrowser/widget.d.ts +15 -0
  8. package/lib/fileBrowser/widget.js +13 -0
  9. package/lib/git/commands.d.ts +5 -0
  10. package/lib/git/commands.js +4 -3
  11. package/lib/git/diffPreferences.d.ts +90 -0
  12. package/lib/git/diffPreferences.js +107 -0
  13. package/lib/git/diffProvider.js +10 -2
  14. package/lib/git/diffSurface.d.ts +34 -19
  15. package/lib/git/diffSurface.js +49 -115
  16. package/lib/git/diffTheme.d.ts +7 -0
  17. package/lib/git/diffTheme.js +23 -0
  18. package/lib/git/diffWidget.d.ts +7 -30
  19. package/lib/git/diffWidget.js +56 -100
  20. package/lib/git/imageDiff.d.ts +9 -0
  21. package/lib/git/imageDiff.js +2 -28
  22. package/lib/git/index.js +41 -2
  23. package/lib/git/notebookDiff.js +3 -2
  24. package/package.json +5 -3
  25. package/schema/git-diff-preferences.json +42 -0
  26. package/src/fileBrowser/commands.ts +64 -0
  27. package/src/fileBrowser/fileBrowser.tsx +4 -9
  28. package/src/fileBrowser/gitStatus.ts +33 -20
  29. package/src/fileBrowser/widget.tsx +22 -0
  30. package/src/git/commands.ts +13 -1
  31. package/src/git/diffPreferences.ts +192 -0
  32. package/src/git/diffProvider.tsx +11 -2
  33. package/src/git/diffSurface.tsx +113 -146
  34. package/src/git/diffTheme.ts +24 -0
  35. package/src/git/diffWidget.tsx +108 -138
  36. package/src/git/imageDiff.tsx +12 -34
  37. package/src/git/index.ts +51 -2
  38. package/src/git/notebookDiff.tsx +3 -2
  39. package/style/git.css +4 -2
@@ -5,7 +5,8 @@ import { ReactWidget } from '@jupyterlab/ui-components';
5
5
  import { PromiseDelegate } from '@lumino/coreutils';
6
6
  import { Signal } from '@lumino/signaling';
7
7
  import { buildDiffAskRequest } from './askRequest';
8
- import { DIFF_WIDGET_CSS_CLASS, DiffStyleControl, DiffSurface, NotebookViewModeControl, isDarkTheme, isPierreTheme, readStoredDiffStyle, readStoredNotebookViewMode, writeStoredDiffStyle, writeStoredNotebookViewMode } from './diffSurface';
8
+ import { useDiffPreferences } from './diffPreferences';
9
+ import { DIFF_WIDGET_CSS_CLASS, DiffStyleControl, DiffSurface, LineWrapControl, NotebookViewModeControl, isDarkTheme, isPierreTheme } from './diffSurface';
9
10
  import { imageDataType } from './imageDiff';
10
11
  export { DIFF_WIDGET_CSS_CLASS };
11
12
  /**
@@ -16,17 +17,13 @@ export class XtralabDiffWidget extends ReactWidget {
16
17
  super();
17
18
  this._reloadNonce = 0;
18
19
  this._pendingRefresh = null;
19
- this._notebookViewModeChanged = new Signal(this);
20
20
  this._hasNotebookView = false;
21
21
  this._hasNotebookViewChanged = new Signal(this);
22
- this._diffStyleChanged = new Signal(this);
23
22
  this._fileDiffActive = false;
24
23
  this._fileDiffActiveChanged = new Signal(this);
25
24
  this._emptied = new Signal(this);
26
25
  this._model = model;
27
26
  this._context = context;
28
- this._notebookViewMode = readStoredNotebookViewMode();
29
- this._diffStyle = readStoredDiffStyle();
30
27
  // Reuse jupyterlab-git's host sizing while applying xtralab styling.
31
28
  this.addClass('jp-git-diff-root');
32
29
  this.addClass(DIFF_WIDGET_CSS_CLASS);
@@ -77,29 +74,6 @@ export class XtralabDiffWidget extends ReactWidget {
77
74
  this.update();
78
75
  await done.promise;
79
76
  }
80
- /**
81
- * The current rendered-vs-JSON choice for notebook diffs.
82
- */
83
- get notebookViewMode() {
84
- return this._notebookViewMode;
85
- }
86
- /**
87
- * Set the notebook view mode and persist it.
88
- */
89
- setNotebookViewMode(mode) {
90
- if (mode === this._notebookViewMode) {
91
- return;
92
- }
93
- this._notebookViewMode = mode;
94
- writeStoredNotebookViewMode(mode);
95
- this._notebookViewModeChanged.emit(mode);
96
- }
97
- /**
98
- * A signal emitted when the notebook view mode changes.
99
- */
100
- get notebookViewModeChanged() {
101
- return this._notebookViewModeChanged;
102
- }
103
77
  /**
104
78
  * Whether a rendered notebook view is currently available.
105
79
  */
@@ -123,30 +97,7 @@ export class XtralabDiffWidget extends ReactWidget {
123
97
  return this._hasNotebookViewChanged;
124
98
  }
125
99
  /**
126
- * The current split/unified layout for textual file diffs.
127
- */
128
- get diffStyle() {
129
- return this._diffStyle;
130
- }
131
- /**
132
- * Set the diff style and persist it.
133
- */
134
- setDiffStyle(style) {
135
- if (style === this._diffStyle) {
136
- return;
137
- }
138
- this._diffStyle = style;
139
- writeStoredDiffStyle(style);
140
- this._diffStyleChanged.emit(style);
141
- }
142
- /**
143
- * A signal emitted when the diff style changes.
144
- */
145
- get diffStyleChanged() {
146
- return this._diffStyleChanged;
147
- }
148
- /**
149
- * Whether the Split/Unified toolbar selector currently applies.
100
+ * Whether the Split/Unified and line-wrap toolbar controls currently apply.
150
101
  */
151
102
  get fileDiffActive() {
152
103
  return this._fileDiffActive;
@@ -219,7 +170,8 @@ function ModelDiffView(props) {
219
170
  var _a, _b;
220
171
  const { widget } = props;
221
172
  const model = widget.model;
222
- const { contentsManager, rendermime, themeManager, askAgent, trans } = widget.context;
173
+ const { contentsManager, rendermime, themeManager, askAgent, preferences, trans } = widget.context;
174
+ const values = useDiffPreferences(preferences);
223
175
  const nonce = widget.reloadNonce;
224
176
  const [state, setState] = React.useState({
225
177
  loading: true,
@@ -227,31 +179,15 @@ function ModelDiffView(props) {
227
179
  newText: '',
228
180
  error: null
229
181
  });
230
- const [notebookViewMode, setNotebookViewMode] = React.useState(() => widget.notebookViewMode);
231
- React.useEffect(() => {
232
- const handler = (sender, mode) => {
233
- setNotebookViewMode(mode);
234
- };
235
- widget.notebookViewModeChanged.connect(handler);
236
- setNotebookViewMode(widget.notebookViewMode);
237
- return () => {
238
- widget.notebookViewModeChanged.disconnect(handler);
239
- };
240
- }, [widget]);
241
182
  const handleNotebookAvailabilityChange = React.useCallback((available) => {
242
183
  widget.setHasNotebookView(available);
243
184
  }, [widget]);
244
- const [diffStyle, setDiffStyle] = React.useState(() => widget.diffStyle);
245
- React.useEffect(() => {
246
- const handler = (sender, style) => {
247
- setDiffStyle(style);
248
- };
249
- widget.diffStyleChanged.connect(handler);
250
- setDiffStyle(widget.diffStyle);
251
- return () => {
252
- widget.diffStyleChanged.disconnect(handler);
253
- };
254
- }, [widget]);
185
+ const handleSplitRatioChange = React.useCallback((splitRatio) => {
186
+ preferences.update({ splitRatio });
187
+ }, [preferences]);
188
+ const handleImageViewModeChange = React.useCallback((imageViewMode) => {
189
+ preferences.update({ imageViewMode });
190
+ }, [preferences]);
255
191
  const handleFileDiffActiveChange = React.useCallback((active) => {
256
192
  widget.setFileDiffActive(active);
257
193
  }, [widget]);
@@ -372,7 +308,7 @@ function ModelDiffView(props) {
372
308
  }));
373
309
  };
374
310
  }, [askAgent, model, state.oldText, state.newText, trans]);
375
- return (React.createElement(DiffSurface, { loading: state.loading, error: state.error, isBinary: isBinary, oldText: state.oldText, newText: state.newText, newName: model.filename, oldName: (_b = model.oldFilename) !== null && _b !== void 0 ? _b : model.filename, dark: dark, pierreTheme: pierre, rendermime: rendermime, notebookViewMode: notebookViewMode, diffStyle: diffStyle, onNotebookAvailabilityChange: handleNotebookAvailabilityChange, onFileDiffActiveChange: handleFileDiffActiveChange, onMetadataChange: handleMetadataChange, hunkDiscard: hunkDiscard, onLineAsk: handleLineAsk, trans: trans }));
311
+ return (React.createElement(DiffSurface, { loading: state.loading, error: state.error, isBinary: isBinary, oldText: state.oldText, newText: state.newText, newName: model.filename, oldName: (_b = model.oldFilename) !== null && _b !== void 0 ? _b : model.filename, dark: dark, pierreTheme: pierre, rendermime: rendermime, notebookViewMode: values.notebookViewMode, diffStyle: values.diffStyle, lineWrap: values.lineWrap, splitRatio: values.splitRatio, onSplitRatioChange: handleSplitRatioChange, imageViewMode: values.imageViewMode, onImageViewModeChange: handleImageViewModeChange, onNotebookAvailabilityChange: handleNotebookAvailabilityChange, onFileDiffActiveChange: handleFileDiffActiveChange, onMetadataChange: handleMetadataChange, hunkDiscard: hunkDiscard, onLineAsk: handleLineAsk, trans: trans }));
376
312
  }
377
313
  /**
378
314
  * Notebook/JSON toggle mounted into a host-owned toolbar.
@@ -392,26 +328,20 @@ class NotebookViewModeToolbarItem extends ReactWidget {
392
328
  }
393
329
  function NotebookViewModeToolbarControl(props) {
394
330
  const { widget } = props;
395
- const [mode, setMode] = React.useState(() => widget.notebookViewMode);
331
+ const { preferences, trans } = widget.context;
332
+ const { notebookViewMode } = useDiffPreferences(preferences);
396
333
  const [available, setAvailable] = React.useState(() => widget.hasNotebookView);
397
334
  React.useEffect(() => {
398
- const onMode = (sender, next) => {
399
- setMode(next);
400
- };
401
335
  const onAvailable = (sender, next) => {
402
336
  setAvailable(next);
403
337
  };
404
- widget.notebookViewModeChanged.connect(onMode);
405
338
  widget.hasNotebookViewChanged.connect(onAvailable);
406
- setMode(widget.notebookViewMode);
407
339
  setAvailable(widget.hasNotebookView);
408
340
  return () => {
409
- widget.notebookViewModeChanged.disconnect(onMode);
410
341
  widget.hasNotebookViewChanged.disconnect(onAvailable);
411
342
  };
412
343
  }, [widget]);
413
- const trans = widget.context.trans;
414
- return (React.createElement(NotebookViewModeControl, { mode: mode, available: available, onChange: next => widget.setNotebookViewMode(next), trans: trans }));
344
+ return (React.createElement(NotebookViewModeControl, { mode: notebookViewMode, available: available, onChange: next => preferences.update({ notebookViewMode: next }), trans: trans }));
415
345
  }
416
346
  /**
417
347
  * Split/Unified toggle mounted into a host-owned toolbar.
@@ -431,26 +361,51 @@ class DiffStyleToolbarItem extends ReactWidget {
431
361
  }
432
362
  function DiffStyleToolbarControl(props) {
433
363
  const { widget } = props;
434
- const [style, setStyle] = React.useState(() => widget.diffStyle);
435
- const [available, setAvailable] = React.useState(() => widget.fileDiffActive);
364
+ const { preferences, trans } = widget.context;
365
+ const { diffStyle, diffStyleControl } = useDiffPreferences(preferences);
366
+ const available = useFileDiffActive(widget);
367
+ return (React.createElement(DiffStyleControl, { diffStyle: diffStyle, control: diffStyleControl, available: available, onChange: next => preferences.update({ diffStyle: next }), trans: trans }));
368
+ }
369
+ /**
370
+ * Line-wrap toggle mounted into a host-owned toolbar.
371
+ */
372
+ class LineWrapToolbarItem extends ReactWidget {
373
+ constructor(widget) {
374
+ super();
375
+ this._widget = widget;
376
+ this.addClass('jp-xtralab-DiffWidget-lineWrapToolbarItem');
377
+ }
378
+ /**
379
+ * Render the line-wrap toggle.
380
+ */
381
+ render() {
382
+ return React.createElement(LineWrapToolbarControl, { widget: this._widget });
383
+ }
384
+ }
385
+ function LineWrapToolbarControl(props) {
386
+ const { widget } = props;
387
+ const { preferences, trans } = widget.context;
388
+ const { lineWrap } = useDiffPreferences(preferences);
389
+ const available = useFileDiffActive(widget);
390
+ return (React.createElement(LineWrapControl, { lineWrap: lineWrap, available: available, onChange: next => preferences.update({ lineWrap: next }), trans: trans }));
391
+ }
392
+ /**
393
+ * Track {@link XtralabDiffWidget.fileDiffActive} for the split/unified and
394
+ * line-wrap controls.
395
+ */
396
+ function useFileDiffActive(widget) {
397
+ const [active, setActive] = React.useState(() => widget.fileDiffActive);
436
398
  React.useEffect(() => {
437
- const onStyle = (sender, next) => {
438
- setStyle(next);
439
- };
440
- const onAvailable = (sender, next) => {
441
- setAvailable(next);
399
+ const onActive = (sender, next) => {
400
+ setActive(next);
442
401
  };
443
- widget.diffStyleChanged.connect(onStyle);
444
- widget.fileDiffActiveChanged.connect(onAvailable);
445
- setStyle(widget.diffStyle);
446
- setAvailable(widget.fileDiffActive);
402
+ widget.fileDiffActiveChanged.connect(onActive);
403
+ setActive(widget.fileDiffActive);
447
404
  return () => {
448
- widget.diffStyleChanged.disconnect(onStyle);
449
- widget.fileDiffActiveChanged.disconnect(onAvailable);
405
+ widget.fileDiffActiveChanged.disconnect(onActive);
450
406
  };
451
407
  }, [widget]);
452
- const trans = widget.context.trans;
453
- return (React.createElement(DiffStyleControl, { diffStyle: style, available: available, onChange: next => widget.setDiffStyle(next), trans: trans }));
408
+ return active;
454
409
  }
455
410
  /**
456
411
  * Mount the shared diff toolbar controls into a host-owned toolbar.
@@ -458,4 +413,5 @@ function DiffStyleToolbarControl(props) {
458
413
  export function addDiffToolbarItems(toolbar, widget) {
459
414
  toolbar.addItem('xtralab-notebook-view-mode', new NotebookViewModeToolbarItem(widget));
460
415
  toolbar.addItem('xtralab-diff-style', new DiffStyleToolbarItem(widget));
416
+ toolbar.addItem('xtralab-diff-line-wrap', new LineWrapToolbarItem(widget));
461
417
  }
@@ -1,5 +1,6 @@
1
1
  import * as React from 'react';
2
2
  import type { TranslationBundle } from '@jupyterlab/translation';
3
+ import type { ImageDiffViewMode } from './diffPreferences';
3
4
  export declare const IMAGE_DIFF_EXTENSIONS: string[];
4
5
  /**
5
6
  * The `data:` MIME subtype for a path xtralab renders as an image, or
@@ -22,6 +23,14 @@ interface IImageDiffViewProps {
22
23
  * MIME subtype from {@link imageDataType}.
23
24
  */
24
25
  fileType: string;
26
+ /**
27
+ * The selected comparison mode (host-controlled).
28
+ */
29
+ mode: ImageDiffViewMode;
30
+ /**
31
+ * Called when the user picks another comparison mode.
32
+ */
33
+ onModeChange: (mode: ImageDiffViewMode) => void;
25
34
  /**
26
35
  * Translation bundle for user-facing strings.
27
36
  */
@@ -33,27 +33,6 @@ export function imageDataType(path) {
33
33
  }
34
34
  return (_a = IMAGE_DATA_TYPES[lower.slice(dot)]) !== null && _a !== void 0 ? _a : null;
35
35
  }
36
- const IMAGE_DIFF_VIEW_MODE_STORAGE_KEY = 'xtralab:image-diff-view-mode';
37
- function readStoredImageViewMode() {
38
- try {
39
- const raw = window.localStorage.getItem(IMAGE_DIFF_VIEW_MODE_STORAGE_KEY);
40
- if (raw === '2-up' || raw === 'swipe' || raw === 'onion') {
41
- return raw;
42
- }
43
- }
44
- catch (_a) {
45
- // localStorage can throw in privacy/sandboxed contexts.
46
- }
47
- return '2-up';
48
- }
49
- function writeStoredImageViewMode(mode) {
50
- try {
51
- window.localStorage.setItem(IMAGE_DIFF_VIEW_MODE_STORAGE_KEY, mode);
52
- }
53
- catch (_a) {
54
- // Best-effort.
55
- }
56
- }
57
36
  /**
58
37
  * Strip whitespace from a base64 payload: the server's `base64.encodebytes`
59
38
  * inserts newlines some browsers reject in `data:` URLs, and the cleaned
@@ -97,21 +76,16 @@ function toSide(raw, fileType) {
97
76
  * segmented-control styling) plus the selected 2-up / swipe / onion view.
98
77
  */
99
78
  export function ImageDiffView(props) {
100
- const { reference, challenger, fileType, trans } = props;
79
+ const { reference, challenger, fileType, mode, onModeChange, trans } = props;
101
80
  const ref = React.useMemo(() => toSide(reference, fileType), [reference, fileType]);
102
81
  const chall = React.useMemo(() => toSide(challenger, fileType), [challenger, fileType]);
103
- const [mode, setMode] = React.useState(() => readStoredImageViewMode());
104
- const changeMode = React.useCallback((next) => {
105
- setMode(next);
106
- writeStoredImageViewMode(next);
107
- }, []);
108
82
  return (React.createElement("div", { className: "jp-xtralab-ImageDiff" },
109
83
  React.createElement("div", { className: "jp-xtralab-ImageDiff-header" },
110
84
  React.createElement("div", { className: "jp-xtralab-DiffWidget-segmented", role: "tablist", "aria-label": trans.__('Image diff view mode') }, [
111
85
  ['2-up', trans.__('2-up')],
112
86
  ['swipe', trans.__('Swipe')],
113
87
  ['onion', trans.__('Onion Skin')]
114
- ].map(([value, label]) => (React.createElement("button", { key: value, type: "button", role: "tab", "aria-selected": mode === value, "data-active": mode === value, className: "jp-xtralab-DiffWidget-segmentedButton", onClick: () => changeMode(value) }, label))))),
88
+ ].map(([value, label]) => (React.createElement("button", { key: value, type: "button", role: "tab", "aria-selected": mode === value, "data-active": mode === value, className: "jp-xtralab-DiffWidget-segmentedButton", onClick: () => onModeChange(value) }, label))))),
115
89
  React.createElement("div", { className: "jp-xtralab-ImageDiff-body" }, mode === '2-up' ? (React.createElement(TwoUp, { reference: ref, challenger: chall, trans: trans })) : mode === 'swipe' ? (React.createElement(Swipe, { reference: ref, challenger: chall, trans: trans })) : (React.createElement(OnionSkin, { reference: ref, challenger: chall, trans: trans })))));
116
90
  }
117
91
  function useNaturalSize() {
package/lib/git/index.js CHANGED
@@ -1,11 +1,42 @@
1
1
  import { IThemeManager, WidgetTracker } from '@jupyterlab/apputils';
2
2
  import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
3
+ import { ISettingRegistry } from '@jupyterlab/settingregistry';
4
+ import { IStateDB } from '@jupyterlab/statedb';
3
5
  import { ITranslator, nullTranslator } from '@jupyterlab/translation';
4
6
  import { IAskAgent } from '../askAgent/tokens';
5
7
  import { CommandIDs, PREVIEW_DIFF_WIDGET_ID, pinnedDiffWidgetId, registerGitCommands } from './commands';
8
+ import { DiffPreferences, IDiffPreferences } from './diffPreferences';
6
9
  import diffProviderPlugin from './diffProvider';
7
10
  const GIT_DIFF_COMMAND_PLUGIN_ID = 'xtralab:git-diff-command';
11
+ const GIT_DIFF_PREFERENCES_PLUGIN_ID = 'xtralab:git-diff-preferences';
8
12
  const GIT_DIFF_TRACKER_NAMESPACE = 'xtralab-git-diff';
13
+ /**
14
+ * The display choices shared by the launcher diff and the Git panel diff.
15
+ */
16
+ const diffPreferencesPlugin = {
17
+ id: GIT_DIFF_PREFERENCES_PLUGIN_ID,
18
+ description: 'Keeps the git diff display choices in the settings and the state database.',
19
+ autoStart: true,
20
+ provides: IDiffPreferences,
21
+ optional: [ISettingRegistry, IStateDB],
22
+ activate: (app, settingRegistry, state) => {
23
+ const preferences = new DiffPreferences();
24
+ if (settingRegistry !== null) {
25
+ settingRegistry
26
+ .load(GIT_DIFF_PREFERENCES_PLUGIN_ID)
27
+ .then(settings => preferences.connectSettings(settings))
28
+ .catch(reason => {
29
+ console.error(`xtralab: failed to load settings for ${GIT_DIFF_PREFERENCES_PLUGIN_ID}`, reason);
30
+ });
31
+ }
32
+ if (state !== null) {
33
+ preferences.connectState(state).catch(reason => {
34
+ console.error('xtralab: failed to restore the diff split', reason);
35
+ });
36
+ }
37
+ return preferences;
38
+ }
39
+ };
9
40
  /**
10
41
  * The launcher's git diff command plugin. The upstream git panel stays
11
42
  * enabled; this only adds `xtralab:git:open-diff` and preview/pinned-tab
@@ -15,8 +46,14 @@ const diffCommandPlugin = {
15
46
  id: GIT_DIFF_COMMAND_PLUGIN_ID,
16
47
  description: "The launcher dashboard's side-by-side git diff command, powered by @pierre/diffs.",
17
48
  autoStart: true,
18
- optional: [IThemeManager, IRenderMimeRegistry, ITranslator, IAskAgent],
19
- activate: (app, themeManager, rendermime, translator, askAgent) => {
49
+ optional: [
50
+ IThemeManager,
51
+ IRenderMimeRegistry,
52
+ ITranslator,
53
+ IAskAgent,
54
+ IDiffPreferences
55
+ ],
56
+ activate: (app, themeManager, rendermime, translator, askAgent, preferences) => {
20
57
  const trans = (translator !== null && translator !== void 0 ? translator : nullTranslator).load('jupyterlab');
21
58
  const tracker = new WidgetTracker({
22
59
  namespace: GIT_DIFF_TRACKER_NAMESPACE
@@ -32,6 +69,7 @@ const diffCommandPlugin = {
32
69
  contentsManager: app.serviceManager.contents,
33
70
  rendermime,
34
71
  askAgent,
72
+ preferences: preferences !== null && preferences !== void 0 ? preferences : new DiffPreferences(),
35
73
  trans,
36
74
  trackDiff: widget => tracker.add(widget),
37
75
  onPinned: current => {
@@ -52,6 +90,7 @@ const diffCommandPlugin = {
52
90
  }
53
91
  };
54
92
  const plugins = [
93
+ diffPreferencesPlugin,
55
94
  diffCommandPlugin,
56
95
  diffProviderPlugin
57
96
  ];
@@ -5,7 +5,7 @@ import { MessageLoop } from '@lumino/messaging';
5
5
  import { Widget } from '@lumino/widgets';
6
6
  import { FileDiff } from '@pierre/diffs/react';
7
7
  import { parseDiffFromFile } from '@pierre/diffs';
8
- import { resolveDiffTheme } from './diffTheme';
8
+ import { DIFF_SCROLLBAR_CSS, resolveDiffTheme } from './diffTheme';
9
9
  const NOTEBOOK_DIFF_CSS_CLASS = 'jp-xtralab-NotebookDiff';
10
10
  /**
11
11
  * Parse notebook JSON. Returns `null` when the text is not a notebook so
@@ -317,7 +317,8 @@ function diffLibraryOptions(theme, dark) {
317
317
  diffStyle: 'split',
318
318
  disableFileHeader: true,
319
319
  theme,
320
- themeType: dark ? 'dark' : 'light'
320
+ themeType: dark ? 'dark' : 'light',
321
+ unsafeCSS: DIFF_SCROLLBAR_CSS
321
322
  };
322
323
  }
323
324
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xtralab",
3
- "version": "0.15.1",
3
+ "version": "0.15.2",
4
4
  "description": "An opinionated JupyterLab meta-package that bundles a curated set of extensions, ships a path-first file browser, renders jupyterlab-git's text, notebook and image diffs with its own viewers, and applies a quieter default workspace configuration.",
5
5
  "keywords": [
6
6
  "jupyter",
@@ -90,7 +90,8 @@
90
90
  "@lumino/polling": "^2.1.5",
91
91
  "@lumino/signaling": "^2.1.5",
92
92
  "@lumino/widgets": "^2.8.0",
93
- "@pierre/diffs": "^1.4.1",
93
+ "@pierre/diffs": "^1.5.1",
94
+ "@pierre/icons": "^0.9.0",
94
95
  "@pierre/trees": "1.0.0-beta.4",
95
96
  "detect-indent": "^7.0.2",
96
97
  "ignore": "^5.3.2",
@@ -136,7 +137,8 @@
136
137
  "@jupyterlab/git": {
137
138
  "bundled": false,
138
139
  "singleton": true
139
- }
140
+ },
141
+ "@pierre/icons": false
140
142
  },
141
143
  "disabledExtensions": [
142
144
  "@jupyterlab/git:notebook-diff",
@@ -0,0 +1,42 @@
1
+ {
2
+ "title": "xtralab Git Diff",
3
+ "description": "Display options for the git diff views in the launcher and the Git panel.",
4
+ "type": "object",
5
+ "properties": {
6
+ "diffStyle": {
7
+ "type": "string",
8
+ "enum": ["split", "unified"],
9
+ "title": "Layout",
10
+ "description": "Show text diffs side by side (split) or in one column (unified).",
11
+ "default": "split"
12
+ },
13
+ "diffStyleControl": {
14
+ "type": "string",
15
+ "enum": ["buttons", "toggle"],
16
+ "title": "Layout buttons",
17
+ "description": "Show two toolbar buttons for the split and unified layouts (buttons), or one button that switches between them (toggle).",
18
+ "default": "buttons"
19
+ },
20
+ "lineWrap": {
21
+ "type": "boolean",
22
+ "title": "Wrap long lines",
23
+ "description": "When true, long lines in text diffs wrap to the width of the view. When false, they scroll horizontally.",
24
+ "default": false
25
+ },
26
+ "notebookViewMode": {
27
+ "type": "string",
28
+ "enum": ["notebook", "json"],
29
+ "title": "Notebook diff view",
30
+ "description": "Show notebook diffs cell by cell (notebook) or as the diff of the notebook JSON (json).",
31
+ "default": "notebook"
32
+ },
33
+ "imageViewMode": {
34
+ "type": "string",
35
+ "enum": ["2-up", "swipe", "onion"],
36
+ "title": "Image diff view",
37
+ "description": "Compare images side by side (2-up), with a slider (swipe), or overlaid (onion).",
38
+ "default": "2-up"
39
+ }
40
+ },
41
+ "additionalProperties": false
42
+ }
@@ -21,6 +21,7 @@ import {
21
21
  fileIcon,
22
22
  filterIcon,
23
23
  IDisposableMenuItem,
24
+ LabIcon,
24
25
  newFolderIcon,
25
26
  RankedMenu,
26
27
  refreshIcon
@@ -28,7 +29,13 @@ import {
28
29
  import { ReadonlyPartialJSONObject } from '@lumino/coreutils';
29
30
  import { ContextMenu, Widget } from '@lumino/widgets';
30
31
 
32
+ import {
33
+ CommandArguments as GitCommandArguments,
34
+ CommandIDs as GitCommandIDs
35
+ } from '../git/commands';
36
+ import type { IFileChange } from '../git/tokens';
31
37
  import { toCanonicalPath, toServerPath } from './contents';
38
+ import { GIT_REPO_PATH } from './gitStatus';
32
39
  import { FILE_BROWSER_ID, IXtralabFileBrowser } from './widget';
33
40
 
34
41
  /**
@@ -39,6 +46,7 @@ import { FILE_BROWSER_ID, IXtralabFileBrowser } from './widget';
39
46
  export namespace CommandIDs {
40
47
  export const open = 'xtralab:open';
41
48
  export const openBrowserTab = 'xtralab:open-browser-tab';
49
+ export const openGitDiff = 'xtralab:open-git-diff';
42
50
  export const rename = 'xtralab:rename';
43
51
  export const del = 'xtralab:delete';
44
52
  export const duplicate = 'xtralab:duplicate';
@@ -135,6 +143,23 @@ function getOpenPaths(
135
143
  return candidates.filter(path => !path.endsWith('/'));
136
144
  }
137
145
 
146
+ /**
147
+ * Git changes of the target file, at most one staged and one unstaged.
148
+ * Status paths are repo-relative; like the tree's git badges, this assumes
149
+ * the repo root is the server root.
150
+ */
151
+ function getTargetChanges(
152
+ app: JupyterFrontEnd,
153
+ browser: IXtralabFileBrowser
154
+ ): IFileChange[] {
155
+ const targetPath = getTargetPath(app, browser);
156
+ if (targetPath === undefined) {
157
+ return [];
158
+ }
159
+ const serverPath = toServerPath(targetPath);
160
+ return browser.gitChanges.filter(change => change.path === serverPath);
161
+ }
162
+
138
163
  function hasTarget(
139
164
  app: JupyterFrontEnd,
140
165
  browser: IXtralabFileBrowser
@@ -242,6 +267,19 @@ function makeOpenWithUpdater(
242
267
  command: CommandIDs.open
243
268
  })
244
269
  );
270
+
271
+ const changes = getTargetChanges(app, browser);
272
+ if (changes.length > 0) {
273
+ items.push(submenu.addItem({ type: 'separator' }));
274
+ for (const change of changes) {
275
+ items.push(
276
+ submenu.addItem({
277
+ args: { group: change.group },
278
+ command: CommandIDs.openGitDiff
279
+ })
280
+ );
281
+ }
282
+ }
245
283
  };
246
284
  }
247
285
 
@@ -303,6 +341,32 @@ export function registerCommands(opts: IRegisterCommandsOptions): () => void {
303
341
  }
304
342
  });
305
343
 
344
+ commands.addCommand(CommandIDs.openGitDiff, {
345
+ label: args =>
346
+ args.group === 'staged'
347
+ ? trans.__('Git Diff (Staged)')
348
+ : trans.__('Git Diff (Working)'),
349
+ icon: LabIcon.resolve({ icon: 'git:diff' }).bindprops({
350
+ stylesheet: 'menuItem'
351
+ }),
352
+ execute: async (args: ReadonlyPartialJSONObject) => {
353
+ const change = getTargetChanges(app, browser).find(
354
+ candidate => candidate.group === args.group
355
+ );
356
+ if (change === undefined) {
357
+ return;
358
+ }
359
+ const diffArgs: GitCommandArguments.IOpenDiff = {
360
+ repoPath: GIT_REPO_PATH,
361
+ change
362
+ };
363
+ return commands.execute(
364
+ GitCommandIDs.openDiff,
365
+ diffArgs as unknown as ReadonlyPartialJSONObject
366
+ );
367
+ }
368
+ });
369
+
306
370
  commands.addCommand(CommandIDs.rename, {
307
371
  label: trans.__('Rename…'),
308
372
  icon: editIcon.bindprops({ stylesheet: 'menuItem' }),
@@ -32,7 +32,7 @@ import {
32
32
  useRootDropZone
33
33
  } from './dragAndDrop';
34
34
  import { buildIgnoredEntries, loadGitignoreMatcher } from './gitignore';
35
- import { loadGitStatusEntries } from './gitStatus';
35
+ import { GIT_REPO_PATH, loadGitChanges, toGitStatusEntries } from './gitStatus';
36
36
  import { FILE_BROWSER_ICONS } from './icons';
37
37
  import type { XtralabFileBrowser } from './widget';
38
38
 
@@ -54,12 +54,6 @@ const FILE_LISTING_REFRESH_INTERVAL_MS = 10000;
54
54
 
55
55
  const FILE_LISTING_REFRESH_MAX_MS = 300_000;
56
56
 
57
- /**
58
- * Repo path for `/git/*` calls; empty means the server root, letting git
59
- * resolve the enclosing repo (same convention as the git panel).
60
- */
61
- const GIT_REPO_PATH = '';
62
-
63
57
  const FILE_TREE_TAG = 'file-tree-container';
64
58
 
65
59
  /**
@@ -180,11 +174,12 @@ export function FileBrowserComponent(
180
174
  };
181
175
 
182
176
  const refreshGitStatus = async (): Promise<void> => {
183
- const next = await loadGitStatusEntries(GIT_REPO_PATH);
177
+ const changes = await loadGitChanges(GIT_REPO_PATH);
184
178
  if (cancelled) {
185
179
  return;
186
180
  }
187
- gitStatusEntries = next;
181
+ gitStatusEntries = toGitStatusEntries(changes);
182
+ widget?.updateGitChanges(changes);
188
183
  syncGitStatus();
189
184
  };
190
185