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,20 +1,144 @@
1
- import { createGitDiffWidget, diffWidgetId, updateGitDiffWidget } from './diffWidget';
1
+ import { MainAreaWidget } from '@jupyterlab/apputils';
2
+ import { ToolbarButton, launchIcon } from '@jupyterlab/ui-components';
3
+ import { Signal } from '@lumino/signaling';
4
+ import { getTreeIcon } from '../fileBrowser/icons';
5
+ import { fileChangeToDiffModel } from './diffModel';
6
+ import { DIFF_WIDGET_CSS_CLASS, XtralabDiffWidget, addDiffToolbarItems } from './diffWidget';
2
7
  /**
3
- * Command IDs exposed by the launcher's git diff path. The launcher
4
- * dashboard's "Changes" section drives {@link CommandIDs.openDiff} to open
5
- * its own diff tab, independent of `jupyterlab-git` (whose panel reaches
6
- * the same rendering via the providers in `diffProvider.tsx`).
8
+ * Command IDs exposed by the launcher's git diff path.
7
9
  */
8
10
  export var CommandIDs;
9
11
  (function (CommandIDs) {
10
12
  CommandIDs.openDiff = 'xtralab:git:open-diff';
11
13
  })(CommandIDs || (CommandIDs = {}));
14
+ /**
15
+ * Widget id of the single, reused preview diff tab.
16
+ */
17
+ export const PREVIEW_DIFF_WIDGET_ID = 'xtralab:diff:preview';
18
+ /**
19
+ * Deterministic widget id for a pinned file/group pair.
20
+ */
21
+ export function pinnedDiffWidgetId(change) {
22
+ return `xtralab:diff:pinned:${change.group}:${change.path}`;
23
+ }
24
+ function formatTitle(change) {
25
+ var _a;
26
+ const name = (_a = change.path.split('/').pop()) !== null && _a !== void 0 ? _a : change.path;
27
+ const groupLabel = change.group === 'staged' ? 'Staged' : 'Working';
28
+ return `${name} (${groupLabel})`;
29
+ }
30
+ /**
31
+ * Launcher-owned host for a shared diff widget.
32
+ */
33
+ export class DiffMainAreaWidget extends MainAreaWidget {
34
+ constructor(options, change, pinned) {
35
+ super(options);
36
+ this._pinnedChanged = new Signal(this);
37
+ this._change = change;
38
+ this._pinned = pinned;
39
+ }
40
+ get change() {
41
+ return this._change;
42
+ }
43
+ get pinned() {
44
+ return this._pinned;
45
+ }
46
+ pin() {
47
+ if (this._pinned) {
48
+ return;
49
+ }
50
+ this._pinned = true;
51
+ this._pinnedChanged.emit(true);
52
+ }
53
+ get pinnedChanged() {
54
+ return this._pinnedChanged;
55
+ }
56
+ setChange(repoPath, change) {
57
+ this._change = change;
58
+ this.content.setModel(fileChangeToDiffModel(repoPath, change));
59
+ this.title.label = formatTitle(change);
60
+ this.title.caption = change.path;
61
+ this.title.icon = getTreeIcon(change.path);
62
+ }
63
+ }
64
+ /**
65
+ * Build the launcher's diff tab around the shared diff widget.
66
+ */
67
+ function createDiffWidget(options) {
68
+ var _a;
69
+ const { repoPath, change, themeManager, contentsManager, rendermime } = options;
70
+ const pinned = options.pinned === true;
71
+ const content = new XtralabDiffWidget(fileChangeToDiffModel(repoPath, change), { contentsManager, rendermime, themeManager });
72
+ const widget = new DiffMainAreaWidget({ content }, change, pinned);
73
+ widget.id = pinned ? pinnedDiffWidgetId(change) : PREVIEW_DIFF_WIDGET_ID;
74
+ widget.title.label = formatTitle(change);
75
+ widget.title.caption = change.path;
76
+ widget.title.closable = true;
77
+ widget.title.icon = getTreeIcon(change.path);
78
+ widget.title.className = pinned ? '' : 'jp-mod-preview';
79
+ widget.addClass(DIFF_WIDGET_CSS_CLASS);
80
+ const onPinned = (_a = options.onPinned) !== null && _a !== void 0 ? _a : (() => undefined);
81
+ const pinButton = new ToolbarButton({
82
+ icon: launchIcon,
83
+ tooltip: 'Pin tab',
84
+ onClick: () => widget.pin()
85
+ });
86
+ if (widget.pinned) {
87
+ pinButton.hide();
88
+ }
89
+ widget.toolbar.addItem('pin', pinButton);
90
+ addDiffToolbarItems(widget.toolbar, content);
91
+ // Keep chrome visible only while there is a relevant toolbar control.
92
+ const syncToolbarVisibility = () => {
93
+ if (widget.isDisposed) {
94
+ return;
95
+ }
96
+ if (!widget.pinned || content.hasNotebookView || content.fileDiffActive) {
97
+ widget.toolbar.show();
98
+ }
99
+ else {
100
+ widget.toolbar.hide();
101
+ }
102
+ };
103
+ const onPinnedChanged = (_sender, value) => {
104
+ if (widget.isDisposed) {
105
+ return;
106
+ }
107
+ if (value) {
108
+ pinButton.hide();
109
+ onPinned(widget);
110
+ if (widget.isDisposed) {
111
+ return;
112
+ }
113
+ widget.title.className = '';
114
+ }
115
+ syncToolbarVisibility();
116
+ };
117
+ // Auto-close once a discard leaves nothing to show.
118
+ const onEmptied = () => {
119
+ if (!widget.isDisposed) {
120
+ widget.close();
121
+ }
122
+ };
123
+ content.hasNotebookViewChanged.connect(syncToolbarVisibility);
124
+ content.fileDiffActiveChanged.connect(syncToolbarVisibility);
125
+ content.emptied.connect(onEmptied);
126
+ widget.pinnedChanged.connect(onPinnedChanged);
127
+ syncToolbarVisibility();
128
+ widget.disposed.connect(() => {
129
+ content.hasNotebookViewChanged.disconnect(syncToolbarVisibility);
130
+ content.fileDiffActiveChanged.disconnect(syncToolbarVisibility);
131
+ content.emptied.disconnect(onEmptied);
132
+ widget.pinnedChanged.disconnect(onPinnedChanged);
133
+ });
134
+ return widget;
135
+ }
12
136
  /**
13
137
  * Register the launcher's git diff command on the application command
14
138
  * registry.
15
139
  */
16
140
  export function registerGitCommands(options) {
17
- const { app, themeManager, contentsManager, rendermime, onChanged, trackDiff, findDiff, onPinned } = options;
141
+ const { app, themeManager, contentsManager, rendermime, trackDiff, findDiff, onPinned } = options;
18
142
  const { commands } = app;
19
143
  commands.addCommand(CommandIDs.openDiff, {
20
144
  label: 'Open Git Diff',
@@ -28,18 +152,17 @@ export function registerGitCommands(options) {
28
152
  const existing = findDiff(typed.change, pin);
29
153
  if (existing !== undefined && !existing.isDisposed) {
30
154
  if (!pin) {
31
- updateGitDiffWidget(existing, typed.change);
155
+ existing.setChange(typed.repoPath, typed.change);
32
156
  }
33
157
  app.shell.activateById(existing.id);
34
158
  return;
35
159
  }
36
- const widget = createGitDiffWidget({
160
+ const widget = createDiffWidget({
37
161
  repoPath: typed.repoPath,
38
162
  change: typed.change,
39
163
  themeManager,
40
164
  contentsManager,
41
165
  rendermime,
42
- onChanged,
43
166
  pinned: pin,
44
167
  onPinned
45
168
  });
@@ -49,4 +172,3 @@ export function registerGitCommands(options) {
49
172
  }
50
173
  });
51
174
  }
52
- export { diffWidgetId };
@@ -0,0 +1,14 @@
1
+ import type { IXtralabDiffModel } from './diffWidget';
2
+ import type { GitReference, IFileChange } from './tokens';
3
+ /**
4
+ * Resolve the old/new git refs for a launcher file change. Staged diffs are
5
+ * INDEX vs HEAD; unstaged diffs are WORKING vs INDEX.
6
+ */
7
+ export declare function resolveReferences(change: IFileChange): {
8
+ oldRef: GitReference | null;
9
+ newRef: GitReference | null;
10
+ };
11
+ /**
12
+ * Build a diff model for a single {@link IFileChange}.
13
+ */
14
+ export declare function fileChangeToDiffModel(repoPath: string, change: IFileChange): IXtralabDiffModel;
@@ -0,0 +1,75 @@
1
+ import { Git } from '@jupyterlab/git';
2
+ import { Signal } from '@lumino/signaling';
3
+ import { content } from './api';
4
+ /**
5
+ * Resolve the old/new git refs for a launcher file change. Staged diffs are
6
+ * INDEX vs HEAD; unstaged diffs are WORKING vs INDEX.
7
+ */
8
+ export function resolveReferences(change) {
9
+ if (change.status === 'untracked') {
10
+ return { oldRef: null, newRef: { special: 'WORKING' } };
11
+ }
12
+ if (change.group === 'staged') {
13
+ return { oldRef: { git: 'HEAD' }, newRef: { special: 'INDEX' } };
14
+ }
15
+ // If INDEX has no blob, the server falls back to HEAD as the baseline.
16
+ return { oldRef: { special: 'INDEX' }, newRef: { special: 'WORKING' } };
17
+ }
18
+ /**
19
+ * Map a launcher git ref onto `Git.Diff.IContent.source`; keep JupyterLab's
20
+ * enum values where downstream diff code checks for them.
21
+ */
22
+ function referenceSource(ref) {
23
+ if (ref === null) {
24
+ return null;
25
+ }
26
+ if ('git' in ref) {
27
+ return ref.git;
28
+ }
29
+ if (ref.special === 'WORKING') {
30
+ return Git.Diff.SpecialRef.WORKING;
31
+ }
32
+ if (ref.special === 'INDEX') {
33
+ return Git.Diff.SpecialRef.INDEX;
34
+ }
35
+ return Git.Diff.SpecialRef.BASE;
36
+ }
37
+ /**
38
+ * `Git.Diff.IModel` adapter for launcher file changes.
39
+ */
40
+ class FileChangeDiffModel {
41
+ constructor(repoPath, change) {
42
+ var _a;
43
+ this.changed = new Signal(this);
44
+ const { oldRef, newRef } = resolveReferences(change);
45
+ // Renames compare the new path against the old path's previous content.
46
+ const oldName = (_a = change.from) !== null && _a !== void 0 ? _a : change.path;
47
+ this.filename = change.path;
48
+ this.oldFilename = change.from;
49
+ this.repositoryPath = repoPath;
50
+ this.isBinary = change.isBinary === true;
51
+ // Only unstaged tracked files can be reverted through a working-tree save.
52
+ this.canDiscard =
53
+ change.group === 'unstaged' && change.status !== 'untracked';
54
+ this.reference = {
55
+ label: oldName,
56
+ source: referenceSource(oldRef),
57
+ content: () => oldRef === null
58
+ ? Promise.resolve('')
59
+ : content(repoPath, oldName, oldRef).then(result => { var _a; return (_a = result.content) !== null && _a !== void 0 ? _a : ''; })
60
+ };
61
+ this.challenger = {
62
+ label: change.path,
63
+ source: referenceSource(newRef),
64
+ content: () => newRef === null
65
+ ? Promise.resolve('')
66
+ : content(repoPath, change.path, newRef).then(result => { var _a; return (_a = result.content) !== null && _a !== void 0 ? _a : ''; })
67
+ };
68
+ }
69
+ }
70
+ /**
71
+ * Build a diff model for a single {@link IFileChange}.
72
+ */
73
+ export function fileChangeToDiffModel(repoPath, change) {
74
+ return new FileChangeDiffModel(repoPath, change);
75
+ }
@@ -1,8 +1,3 @@
1
1
  import { JupyterFrontEndPlugin } from '@jupyterlab/application';
2
- /**
3
- * Registers xtralab's diff rendering as jupyterlab-git's diff providers:
4
- * `@pierre/diffs` for text and notebooks, the bundled `<img>`-based view
5
- * for raster images.
6
- */
7
2
  declare const diffProviderPlugin: JupyterFrontEndPlugin<void>;
8
3
  export default diffProviderPlugin;
@@ -1,296 +1,20 @@
1
- import * as React from 'react';
2
1
  import { IThemeManager } from '@jupyterlab/apputils';
3
- import { PathExt } from '@jupyterlab/coreutils';
4
2
  import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
5
- import { ReactWidget } from '@jupyterlab/ui-components';
6
- import { Git, IGitExtension } from '@jupyterlab/git';
7
- import { PromiseDelegate } from '@lumino/coreutils';
8
- import { Signal } from '@lumino/signaling';
9
- import { DIFF_WIDGET_CSS_CLASS, DiffSurface, NotebookViewModeControl, isDarkTheme, readStoredNotebookViewMode, writeStoredNotebookViewMode } from './diffSurface';
10
- import { IMAGE_DIFF_EXTENSIONS, imageDataType } from './imageDiff';
3
+ import { IGitExtension } from '@jupyterlab/git';
4
+ import { XtralabDiffWidget, addDiffToolbarItems } from './diffWidget';
5
+ import { IMAGE_DIFF_EXTENSIONS } from './imageDiff';
11
6
  /**
12
- * A `Git.Diff.IDiffWidget` that renders through {@link DiffSurface}.
13
- *
14
- * `jupyterlab-git` constructs this via the registered factory, hosts it in
15
- * its own `PreviewMainAreaWidget`, and reads `model` / calls `refresh()` /
16
- * `getResolvedFile()` on it. It owns the notebook view-mode state (mounted
17
- * into the diff toolbar `jupyterlab-git` hands the factory) and a reload
18
- * nonce so `refresh()` re-pulls the model's content.
19
- */
20
- class XtralabDiffWidget extends ReactWidget {
21
- constructor(model, context) {
22
- super();
23
- this._reloadNonce = 0;
24
- this._pendingRefresh = null;
25
- this._notebookViewModeChanged = new Signal(this);
26
- this._hasNotebookView = false;
27
- this._hasNotebookViewChanged = new Signal(this);
28
- this._model = model;
29
- this._context = context;
30
- this._notebookViewMode = readStoredNotebookViewMode();
31
- // `jp-git-diff-root` borrows jupyterlab-git's host sizing (height:100%
32
- // flex column inside its PreviewMainAreaWidget); `jp-xtralab-DiffWidget`
33
- // brings our diff theming + the `.jp-xtralab-DiffWidget-*` styles.
34
- this.addClass('jp-git-diff-root');
35
- this.addClass(DIFF_WIDGET_CSS_CLASS);
36
- }
37
- /** The diff model, as required by `Git.Diff.IDiffWidget`. */
38
- get model() {
39
- return this._model;
40
- }
41
- /**
42
- * We render a two-way (reference vs. challenger) view and do not provide
43
- * a merge editor, so there is nothing to leave unresolved — report
44
- * resolved so jupyterlab-git's merge-conflict flow is not blocked.
45
- */
46
- get isFileResolved() {
47
- return true;
48
- }
49
- /**
50
- * Used by jupyterlab-git's "Mark as resolved" button. For merge
51
- * conflicts, jupyterlab-git passes the working-tree result as `base`;
52
- * keep that content rather than overwriting it with either compared side.
53
- */
54
- async getResolvedFile() {
55
- const source = this._model.hasConflict === true && this._model.base !== undefined
56
- ? this._model.base
57
- : this._model.challenger;
58
- const content = await source.content();
59
- return {
60
- type: 'file',
61
- format: imageDataType(this._model.filename) !== null ? 'base64' : 'text',
62
- content
63
- };
64
- }
65
- /**
66
- * Re-pull both sides of the model and re-render. jupyterlab-git wires a
67
- * "Refresh" toolbar button to this whenever `model.changed` fires (e.g.
68
- * the compared `HEAD` moved).
69
- */
70
- async refresh() {
71
- const done = new PromiseDelegate();
72
- this._pendingRefresh = done;
73
- this._reloadNonce += 1;
74
- this.update();
75
- await done.promise;
76
- }
77
- /** The notebook rendered-vs-JSON choice; mirrored into the toolbar. */
78
- get notebookViewMode() {
79
- return this._notebookViewMode;
80
- }
81
- setNotebookViewMode(mode) {
82
- if (mode === this._notebookViewMode) {
83
- return;
84
- }
85
- this._notebookViewMode = mode;
86
- writeStoredNotebookViewMode(mode);
87
- this._notebookViewModeChanged.emit(mode);
88
- }
89
- get notebookViewModeChanged() {
90
- return this._notebookViewModeChanged;
91
- }
92
- get hasNotebookView() {
93
- return this._hasNotebookView;
94
- }
95
- setHasNotebookView(value) {
96
- if (value === this._hasNotebookView) {
97
- return;
98
- }
99
- this._hasNotebookView = value;
100
- this._hasNotebookViewChanged.emit(value);
101
- }
102
- get hasNotebookViewChanged() {
103
- return this._hasNotebookViewChanged;
104
- }
105
- /** Resolve the in-flight {@link refresh} promise, if any. */
106
- settleRefresh() {
107
- const pending = this._pendingRefresh;
108
- this._pendingRefresh = null;
109
- pending === null || pending === void 0 ? void 0 : pending.resolve();
110
- }
111
- get reloadNonce() {
112
- return this._reloadNonce;
113
- }
114
- get context() {
115
- return this._context;
116
- }
117
- render() {
118
- return React.createElement(ModelDiffView, { widget: this });
119
- }
120
- }
121
- function ModelDiffView(props) {
122
- const { widget } = props;
123
- const model = widget.model;
124
- const { contentsManager, rendermime, themeManager } = widget.context;
125
- const nonce = widget.reloadNonce;
126
- const [state, setState] = React.useState({
127
- loading: true,
128
- oldText: '',
129
- newText: '',
130
- error: null
131
- });
132
- // Mirror the widget's notebook view mode so toolbar-driven changes
133
- // re-render the diff. The widget owns the canonical value (so the
134
- // toolbar can read/write it without going through React).
135
- const [notebookViewMode, setNotebookViewMode] = React.useState(() => widget.notebookViewMode);
136
- React.useEffect(() => {
137
- const handler = (_sender, mode) => {
138
- setNotebookViewMode(mode);
139
- };
140
- widget.notebookViewModeChanged.connect(handler);
141
- setNotebookViewMode(widget.notebookViewMode);
142
- return () => {
143
- widget.notebookViewModeChanged.disconnect(handler);
144
- };
145
- }, [widget]);
146
- const handleNotebookAvailabilityChange = React.useCallback((available) => {
147
- widget.setHasNotebookView(available);
148
- }, [widget]);
149
- const [dark, setDark] = React.useState(() => isDarkTheme(themeManager));
150
- React.useEffect(() => {
151
- if (themeManager !== null) {
152
- const handler = () => setDark(isDarkTheme(themeManager));
153
- themeManager.themeChanged.connect(handler);
154
- return () => {
155
- themeManager.themeChanged.disconnect(handler);
156
- };
157
- }
158
- const observer = new MutationObserver(() => setDark(isDarkTheme(themeManager)));
159
- observer.observe(document.body, {
160
- attributes: true,
161
- attributeFilter: ['data-jp-theme-light']
162
- });
163
- return () => {
164
- observer.disconnect();
165
- };
166
- }, [themeManager]);
167
- // Fetch both sides whenever the model or the reload nonce changes. The
168
- // model's `content()` getters target the right git refs internally
169
- // (working tree, index, a commit, …) so we stay content-source-agnostic.
170
- React.useEffect(() => {
171
- let cancelled = false;
172
- setState({ loading: true, oldText: '', newText: '', error: null });
173
- void (async () => {
174
- try {
175
- const [oldText, newText] = await Promise.all([
176
- model.reference.content(),
177
- model.challenger.content()
178
- ]);
179
- if (cancelled) {
180
- return;
181
- }
182
- setState({
183
- loading: false,
184
- oldText: oldText !== null && oldText !== void 0 ? oldText : '',
185
- newText: newText !== null && newText !== void 0 ? newText : '',
186
- error: null
187
- });
188
- }
189
- catch (err) {
190
- if (cancelled) {
191
- return;
192
- }
193
- setState({
194
- loading: false,
195
- oldText: '',
196
- newText: '',
197
- error: err instanceof Error ? err.message : String(err)
198
- });
199
- }
200
- finally {
201
- if (!cancelled) {
202
- // Let an in-flight `widget.refresh()` resolve once the reload
203
- // has settled (jupyterlab-git only awaits it to hide its
204
- // refresh button).
205
- widget.settleRefresh();
206
- }
207
- }
208
- })();
209
- return () => {
210
- cancelled = true;
211
- };
212
- }, [widget, model, nonce]);
213
- // Per-hunk discard is only meaningful when the challenger is the working
214
- // tree (an unstaged change) and we are not in a three-way merge view.
215
- const canDiscardHunk = model.challenger.source === Git.Diff.SpecialRef.WORKING &&
216
- model.hasConflict !== true;
217
- const serverPath = React.useMemo(() => { var _a; return PathExt.join((_a = model.repositoryPath) !== null && _a !== void 0 ? _a : '', model.filename); }, [model.repositoryPath, model.filename]);
218
- const hunkDiscard = React.useMemo(() => ({
219
- enabled: canDiscardHunk,
220
- save: async (text) => {
221
- await contentsManager.save(serverPath, {
222
- type: 'file',
223
- format: 'text',
224
- content: text
225
- });
226
- },
227
- onAfterSave: () => {
228
- // jupyterlab-git's panel auto-refreshes off the contents
229
- // `fileChanged` signal the save above emits; we just need to
230
- // re-pull so the diff reflects the reverted hunk. (We do not
231
- // auto-close: jupyterlab-git owns the diff tab.)
232
- void widget.refresh();
233
- }
234
- }), [canDiscardHunk, contentsManager, serverPath, widget]);
235
- return (React.createElement(DiffSurface, { loading: state.loading, error: state.error, isBinary: false, oldText: state.oldText, newText: state.newText, newName: model.filename, oldName: model.filename, dark: dark, rendermime: rendermime, notebookViewMode: notebookViewMode, onNotebookAvailabilityChange: handleNotebookAvailabilityChange, hunkDiscard: hunkDiscard }));
236
- }
237
- /**
238
- * Notebook/JSON toggle mounted into the diff toolbar that jupyterlab-git
239
- * passes to the factory. Bound to the widget's signals so it stays in sync
240
- * with the rendered surface and only appears for parseable notebooks.
241
- */
242
- class NotebookViewModeToolbarItem extends ReactWidget {
243
- constructor(widget) {
244
- super();
245
- this._widget = widget;
246
- this.addClass('jp-xtralab-DiffWidget-viewModeToolbarItem');
247
- }
248
- render() {
249
- return React.createElement(ToolbarControl, { widget: this._widget });
250
- }
251
- }
252
- function ToolbarControl(props) {
253
- const { widget } = props;
254
- const [mode, setMode] = React.useState(() => widget.notebookViewMode);
255
- const [available, setAvailable] = React.useState(() => widget.hasNotebookView);
256
- React.useEffect(() => {
257
- const onMode = (_sender, next) => {
258
- setMode(next);
259
- };
260
- const onAvailable = (_sender, next) => {
261
- setAvailable(next);
262
- };
263
- widget.notebookViewModeChanged.connect(onMode);
264
- widget.hasNotebookViewChanged.connect(onAvailable);
265
- setMode(widget.notebookViewMode);
266
- setAvailable(widget.hasNotebookView);
267
- return () => {
268
- widget.notebookViewModeChanged.disconnect(onMode);
269
- widget.hasNotebookViewChanged.disconnect(onAvailable);
270
- };
271
- }, [widget]);
272
- return (React.createElement(NotebookViewModeControl, { mode: mode, available: available, onChange: next => widget.setNotebookViewMode(next) }));
273
- }
274
- /**
275
- * Build the `Git.Diff.Factory` xtralab registers with jupyterlab-git. The
276
- * factory closes over the application-level context (contents manager for
277
- * hunk discard, rendermime for notebook outputs, theme manager) that the
278
- * `Git.Diff.IFactoryOptions` does not carry.
7
+ * Build the `Git.Diff.Factory` registered with jupyterlab-git.
279
8
  */
280
9
  function makeXtralabDiffFactory(context) {
281
10
  return async (options) => {
282
11
  const widget = new XtralabDiffWidget(options.model, context);
283
12
  if (options.toolbar) {
284
- options.toolbar.addItem('xtralab-notebook-view-mode', new NotebookViewModeToolbarItem(widget));
13
+ addDiffToolbarItems(options.toolbar, widget);
285
14
  }
286
15
  return widget;
287
16
  };
288
17
  }
289
- /**
290
- * Registers xtralab's diff rendering as jupyterlab-git's diff providers:
291
- * `@pierre/diffs` for text and notebooks, the bundled `<img>`-based view
292
- * for raster images.
293
- */
294
18
  const diffProviderPlugin = {
295
19
  id: 'xtralab:git-diff-providers',
296
20
  description: "Replaces jupyterlab-git's notebook, text and image diff plugins with xtralab's renderer.",
@@ -303,9 +27,7 @@ const diffProviderPlugin = {
303
27
  rendermime,
304
28
  themeManager
305
29
  });
306
- // Notebooks and raster images get extension-specific providers; every
307
- // other text file falls back to the same factory. One factory serves
308
- // all three — the surface auto-detects notebooks/images by filename.
30
+ // Use one shared factory; the surface detects notebooks/images by filename.
309
31
  gitExtension.registerDiffProvider('XtralabNotebook', ['.ipynb'], factory);
310
32
  gitExtension.registerDiffProvider('XtralabImage', IMAGE_DIFF_EXTENSIONS, factory);
311
33
  gitExtension.registerFallbackDiffProvider(factory);