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