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/diffWidget.tsx
CHANGED
|
@@ -1,25 +1,22 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
2
|
|
|
3
|
-
import { IThemeManager
|
|
3
|
+
import { IThemeManager } from '@jupyterlab/apputils';
|
|
4
4
|
import { PathExt } from '@jupyterlab/coreutils';
|
|
5
|
+
import { Git } from '@jupyterlab/git';
|
|
5
6
|
import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
|
|
6
7
|
import { Contents } from '@jupyterlab/services';
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
ToolbarButton,
|
|
10
|
-
launchIcon
|
|
11
|
-
} from '@jupyterlab/ui-components';
|
|
8
|
+
import { ReactWidget } from '@jupyterlab/ui-components';
|
|
9
|
+
import { PromiseDelegate } from '@lumino/coreutils';
|
|
12
10
|
import { ISignal, Signal } from '@lumino/signaling';
|
|
11
|
+
import { Widget } from '@lumino/widgets';
|
|
13
12
|
|
|
14
|
-
import { getTreeIcon } from '../fileBrowser/icons';
|
|
15
|
-
import { content } from './api';
|
|
16
|
-
import { imageDataType } from './imageDiff';
|
|
17
13
|
import {
|
|
18
14
|
DIFF_WIDGET_CSS_CLASS,
|
|
19
15
|
DiffStyleControl,
|
|
20
16
|
DiffSurface,
|
|
21
17
|
NotebookViewModeControl,
|
|
22
18
|
isDarkTheme,
|
|
19
|
+
isPierreTheme,
|
|
23
20
|
readStoredDiffStyle,
|
|
24
21
|
readStoredNotebookViewMode,
|
|
25
22
|
writeStoredDiffStyle,
|
|
@@ -27,228 +24,96 @@ import {
|
|
|
27
24
|
type DiffStyle,
|
|
28
25
|
type NotebookDiffViewMode
|
|
29
26
|
} from './diffSurface';
|
|
30
|
-
import {
|
|
27
|
+
import { imageDataType } from './imageDiff';
|
|
31
28
|
|
|
32
29
|
export { DIFF_WIDGET_CSS_CLASS };
|
|
33
30
|
|
|
34
|
-
export const PREVIEW_DIFF_WIDGET_ID = 'xtralab:diff:preview';
|
|
35
|
-
|
|
36
|
-
export function pinnedDiffWidgetId(change: IFileChange): string {
|
|
37
|
-
return `xtralab:diff:pinned:${change.group}:${change.path}`;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
31
|
/**
|
|
41
|
-
*
|
|
42
|
-
* file/group pair. Reused across plugin and command code so reopening a
|
|
43
|
-
* diff for the same file reveals the existing tab instead of opening a
|
|
44
|
-
* second one.
|
|
32
|
+
* `Git.Diff.IModel` plus optional xtralab-only metadata.
|
|
45
33
|
*/
|
|
46
|
-
export
|
|
47
|
-
|
|
34
|
+
export interface IXtralabDiffModel extends Git.Diff.IModel {
|
|
35
|
+
isBinary?: boolean;
|
|
36
|
+
canDiscard?: boolean;
|
|
37
|
+
oldFilename?: string;
|
|
48
38
|
}
|
|
49
39
|
|
|
50
|
-
|
|
51
|
-
|
|
40
|
+
/**
|
|
41
|
+
* Application-level services the diff model does not carry.
|
|
42
|
+
*/
|
|
43
|
+
export interface IXtralabDiffContext {
|
|
44
|
+
contentsManager: Contents.IManager;
|
|
45
|
+
rendermime: IRenderMimeRegistry | null;
|
|
46
|
+
themeManager: IThemeManager | null;
|
|
52
47
|
}
|
|
53
48
|
|
|
54
49
|
/**
|
|
55
|
-
*
|
|
56
|
-
* left and right side of the diff for a given file change. Mirrors VS
|
|
57
|
-
* Code's source control diff: staged-side compares INDEX vs HEAD,
|
|
58
|
-
* unstaged-side compares WORKING vs INDEX, and untracked files have no
|
|
59
|
-
* "previous" version to compare against.
|
|
50
|
+
* Shared diff widget used by the launcher and by `jupyterlab-git`.
|
|
60
51
|
*/
|
|
61
|
-
export
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
52
|
+
export class XtralabDiffWidget
|
|
53
|
+
extends ReactWidget
|
|
54
|
+
implements Git.Diff.IDiffWidget
|
|
55
|
+
{
|
|
56
|
+
constructor(model: Git.Diff.IModel, context: IXtralabDiffContext) {
|
|
57
|
+
super();
|
|
58
|
+
this._model = model;
|
|
59
|
+
this._context = context;
|
|
60
|
+
this._notebookViewMode = readStoredNotebookViewMode();
|
|
61
|
+
this._diffStyle = readStoredDiffStyle();
|
|
62
|
+
// Reuse jupyterlab-git's host sizing while applying xtralab styling.
|
|
63
|
+
this.addClass('jp-git-diff-root');
|
|
64
|
+
this.addClass(DIFF_WIDGET_CSS_CLASS);
|
|
67
65
|
}
|
|
68
|
-
|
|
69
|
-
|
|
66
|
+
|
|
67
|
+
get model(): Git.Diff.IModel {
|
|
68
|
+
return this._model;
|
|
70
69
|
}
|
|
71
|
-
// Unstaged: WORKING vs INDEX. If the file isn't in the index yet (e.g.
|
|
72
|
-
// a freshly-`git add`-ed file that has been modified again) the server
|
|
73
|
-
// returns the HEAD blob for INDEX instead, which is still the right
|
|
74
|
-
// baseline for the diff.
|
|
75
|
-
return { oldRef: { special: 'INDEX' }, newRef: { special: 'WORKING' } };
|
|
76
|
-
}
|
|
77
70
|
|
|
78
|
-
export interface IGitDiffWidgetOptions {
|
|
79
|
-
repoPath: string;
|
|
80
|
-
change: IFileChange;
|
|
81
|
-
themeManager: IThemeManager | null;
|
|
82
|
-
contentsManager: Contents.IManager;
|
|
83
71
|
/**
|
|
84
|
-
*
|
|
85
|
-
* and markdown cells. May be `null` in stripped-down hosts; the notebook
|
|
86
|
-
* diff falls back to a textual representation in that case.
|
|
72
|
+
* Swap the rendered model when the launcher reuses its preview tab.
|
|
87
73
|
*/
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
onChanged: () => void;
|
|
74
|
+
setModel(model: IXtralabDiffModel): void {
|
|
75
|
+
this._model = model;
|
|
76
|
+
this.update();
|
|
77
|
+
}
|
|
78
|
+
|
|
94
79
|
/**
|
|
95
|
-
*
|
|
96
|
-
* Wired by {@link createGitDiffWidget} to close the host
|
|
97
|
-
* `MainAreaWidget` — there is nothing left to view, so the empty pane
|
|
98
|
-
* would just be visual noise.
|
|
80
|
+
* This renderer has no merge editor, so it never blocks mark-as-resolved.
|
|
99
81
|
*/
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
pinned?: boolean;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* Build a `MainAreaWidget`-hosted diff viewer for a single file change.
|
|
107
|
-
* Wrapping the `ReactWidget` in a `MainAreaWidget` is what gives it the
|
|
108
|
-
* usual document chrome — toolbar slot, focus tracker integration, and
|
|
109
|
-
* spinner-while-loading hooks — and matches the pattern used for other
|
|
110
|
-
* read-only main-area widgets in the JupyterLab ecosystem.
|
|
111
|
-
*
|
|
112
|
-
* This is the launcher dashboard's own diff path
|
|
113
|
-
* (`xtralab:git:open-diff`), kept independent of `jupyterlab-git`. The
|
|
114
|
-
* actual diff rendering is delegated to the shared {@link DiffSurface};
|
|
115
|
-
* `jupyterlab-git`'s panel reaches the same surface through the providers
|
|
116
|
-
* registered in `diffProvider.tsx`.
|
|
117
|
-
*/
|
|
118
|
-
export function createGitDiffWidget(
|
|
119
|
-
options: IGitDiffWidgetOptions
|
|
120
|
-
): MainAreaWidget<DiffContentWidget> {
|
|
121
|
-
// `widget` is captured by the `onEmpty` closure we pass into the content,
|
|
122
|
-
// but doesn't exist yet at construction time — declare it up front so the
|
|
123
|
-
// closure can read it once the widget is wired.
|
|
124
|
-
let widget: MainAreaWidget<DiffContentWidget> | null = null;
|
|
125
|
-
const content = new DiffContentWidget({
|
|
126
|
-
...options,
|
|
127
|
-
onEmpty: () => {
|
|
128
|
-
if (widget !== null && !widget.isDisposed) {
|
|
129
|
-
widget.close();
|
|
130
|
-
}
|
|
131
|
-
options.onEmpty?.();
|
|
132
|
-
}
|
|
133
|
-
});
|
|
134
|
-
widget = new MainAreaWidget<DiffContentWidget>({ content });
|
|
135
|
-
widget.id =
|
|
136
|
-
options.pinned === true
|
|
137
|
-
? pinnedDiffWidgetId(options.change)
|
|
138
|
-
: PREVIEW_DIFF_WIDGET_ID;
|
|
139
|
-
widget.title.label = formatTitle(options.change);
|
|
140
|
-
widget.title.caption = options.change.path;
|
|
141
|
-
widget.title.closable = true;
|
|
142
|
-
widget.title.icon = getTreeIcon(options.change.path);
|
|
143
|
-
widget.title.className = options.pinned === true ? '' : 'jp-mod-preview';
|
|
144
|
-
const onPinned = options.onPinned ?? (() => {});
|
|
145
|
-
widget.addClass(DIFF_WIDGET_CSS_CLASS);
|
|
146
|
-
// Wire the notebook view-mode selector into the `MainAreaWidget` toolbar
|
|
147
|
-
// — the proper home for document chrome controls. The toolbar starts
|
|
148
|
-
// hidden and becomes visible once the React content reports a parsed
|
|
149
|
-
// notebook diff via `setHasNotebookView`. Plain text / code diffs never
|
|
150
|
-
// flip that flag, and pin state can hide the toolbar again, so the host
|
|
151
|
-
// chrome tracks the active mode.
|
|
152
|
-
const pinButton = new ToolbarButton({
|
|
153
|
-
icon: launchIcon,
|
|
154
|
-
tooltip: 'Pin tab',
|
|
155
|
-
onClick: () => content.pin()
|
|
156
|
-
});
|
|
157
|
-
if (content.pinned) {
|
|
158
|
-
pinButton.hide();
|
|
82
|
+
get isFileResolved(): boolean {
|
|
83
|
+
return true;
|
|
159
84
|
}
|
|
160
|
-
widget.toolbar.addItem('pin', pinButton);
|
|
161
|
-
widget.toolbar.addItem(
|
|
162
|
-
'notebook-view-mode',
|
|
163
|
-
new NotebookViewModeToolbarItem(content)
|
|
164
|
-
);
|
|
165
|
-
widget.toolbar.addItem('diff-style', new DiffStyleToolbarItem(content));
|
|
166
|
-
const syncToolbarVisibility = (): void => {
|
|
167
|
-
if (widget === null || widget.isDisposed) {
|
|
168
|
-
return;
|
|
169
|
-
}
|
|
170
|
-
if (!content.pinned || content.hasNotebookView || content.fileDiffActive) {
|
|
171
|
-
widget.toolbar.show();
|
|
172
|
-
} else {
|
|
173
|
-
widget.toolbar.hide();
|
|
174
|
-
}
|
|
175
|
-
};
|
|
176
|
-
const onToolbarStateChanged = (): void => {
|
|
177
|
-
if (widget === null || widget.isDisposed) {
|
|
178
|
-
return;
|
|
179
|
-
}
|
|
180
|
-
syncToolbarVisibility();
|
|
181
|
-
};
|
|
182
|
-
const onPinnedChanged = (sender: DiffContentWidget, value: boolean): void => {
|
|
183
|
-
if (widget === null || widget.isDisposed) {
|
|
184
|
-
return;
|
|
185
|
-
}
|
|
186
|
-
void sender;
|
|
187
|
-
if (value) {
|
|
188
|
-
pinButton.hide();
|
|
189
|
-
// Keep toolbar state and title styling consistent for promoted tabs.
|
|
190
|
-
onPinned(widget);
|
|
191
|
-
if (widget.isDisposed) {
|
|
192
|
-
return;
|
|
193
|
-
}
|
|
194
|
-
widget.title.className = '';
|
|
195
|
-
}
|
|
196
|
-
syncToolbarVisibility();
|
|
197
|
-
};
|
|
198
|
-
content.hasNotebookViewChanged.connect(onToolbarStateChanged);
|
|
199
|
-
content.fileDiffActiveChanged.connect(onToolbarStateChanged);
|
|
200
|
-
content.pinnedChanged.connect(onPinnedChanged);
|
|
201
|
-
syncToolbarVisibility();
|
|
202
|
-
widget.disposed.connect(() => {
|
|
203
|
-
content.hasNotebookViewChanged.disconnect(onToolbarStateChanged);
|
|
204
|
-
content.fileDiffActiveChanged.disconnect(onToolbarStateChanged);
|
|
205
|
-
content.pinnedChanged.disconnect(onPinnedChanged);
|
|
206
|
-
});
|
|
207
|
-
return widget;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
export function updateGitDiffWidget(
|
|
211
|
-
widget: MainAreaWidget<DiffContentWidget>,
|
|
212
|
-
change: IFileChange
|
|
213
|
-
): void {
|
|
214
|
-
widget.content.setChange(change);
|
|
215
|
-
widget.title.label = formatTitle(change);
|
|
216
|
-
widget.title.caption = change.path;
|
|
217
|
-
widget.title.icon = getTreeIcon(change.path);
|
|
218
|
-
}
|
|
219
85
|
|
|
220
|
-
/**
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
super();
|
|
235
|
-
this._options = options;
|
|
236
|
-
this._pinned = options.pinned === true;
|
|
237
|
-
this._notebookViewMode = readStoredNotebookViewMode();
|
|
238
|
-
this._diffStyle = readStoredDiffStyle();
|
|
86
|
+
/**
|
|
87
|
+
* Return the content jupyterlab-git should save for mark-as-resolved.
|
|
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
|
+
};
|
|
239
100
|
}
|
|
240
101
|
|
|
241
|
-
/**
|
|
242
|
-
|
|
243
|
-
|
|
102
|
+
/**
|
|
103
|
+
* Re-pull both sides of the model and re-render.
|
|
104
|
+
*/
|
|
105
|
+
async refresh(): Promise<void> {
|
|
106
|
+
const done = new PromiseDelegate<void>();
|
|
107
|
+
this._pendingRefresh = done;
|
|
108
|
+
this._reloadNonce += 1;
|
|
109
|
+
this.update();
|
|
110
|
+
await done.promise;
|
|
244
111
|
}
|
|
245
112
|
|
|
246
|
-
/** The user's current rendered-vs-JSON view choice for notebook diffs. */
|
|
247
113
|
get notebookViewMode(): NotebookDiffViewMode {
|
|
248
114
|
return this._notebookViewMode;
|
|
249
115
|
}
|
|
250
116
|
|
|
251
|
-
/** Update the notebook view mode and persist it across sessions. */
|
|
252
117
|
setNotebookViewMode(mode: NotebookDiffViewMode): void {
|
|
253
118
|
if (mode === this._notebookViewMode) {
|
|
254
119
|
return;
|
|
@@ -258,16 +123,10 @@ export class DiffContentWidget extends ReactWidget {
|
|
|
258
123
|
this._notebookViewModeChanged.emit(mode);
|
|
259
124
|
}
|
|
260
125
|
|
|
261
|
-
/** Emits whenever {@link notebookViewMode} changes. */
|
|
262
126
|
get notebookViewModeChanged(): ISignal<this, NotebookDiffViewMode> {
|
|
263
127
|
return this._notebookViewModeChanged;
|
|
264
128
|
}
|
|
265
129
|
|
|
266
|
-
/**
|
|
267
|
-
* Whether a rendered notebook view is currently available. Set by the
|
|
268
|
-
* React content once it has parsed the diff; the toolbar reads this to
|
|
269
|
-
* decide whether to show the view-mode selector.
|
|
270
|
-
*/
|
|
271
130
|
get hasNotebookView(): boolean {
|
|
272
131
|
return this._hasNotebookView;
|
|
273
132
|
}
|
|
@@ -280,17 +139,14 @@ export class DiffContentWidget extends ReactWidget {
|
|
|
280
139
|
this._hasNotebookViewChanged.emit(value);
|
|
281
140
|
}
|
|
282
141
|
|
|
283
|
-
/** Emits whenever {@link hasNotebookView} changes. */
|
|
284
142
|
get hasNotebookViewChanged(): ISignal<this, boolean> {
|
|
285
143
|
return this._hasNotebookViewChanged;
|
|
286
144
|
}
|
|
287
145
|
|
|
288
|
-
/** The user's current split-vs-unified layout choice for file diffs. */
|
|
289
146
|
get diffStyle(): DiffStyle {
|
|
290
147
|
return this._diffStyle;
|
|
291
148
|
}
|
|
292
149
|
|
|
293
|
-
/** Update the diff layout and persist it across sessions. */
|
|
294
150
|
setDiffStyle(style: DiffStyle): void {
|
|
295
151
|
if (style === this._diffStyle) {
|
|
296
152
|
return;
|
|
@@ -300,15 +156,12 @@ export class DiffContentWidget extends ReactWidget {
|
|
|
300
156
|
this._diffStyleChanged.emit(style);
|
|
301
157
|
}
|
|
302
158
|
|
|
303
|
-
/** Emits whenever {@link diffStyle} changes. */
|
|
304
159
|
get diffStyleChanged(): ISignal<this, DiffStyle> {
|
|
305
160
|
return this._diffStyleChanged;
|
|
306
161
|
}
|
|
307
162
|
|
|
308
163
|
/**
|
|
309
|
-
* Whether the
|
|
310
|
-
* choice affects) is currently active. Set by the React content; the
|
|
311
|
-
* toolbar reads it to decide whether to show the Split/Unified selector.
|
|
164
|
+
* Whether the Split/Unified toolbar selector currently applies.
|
|
312
165
|
*/
|
|
313
166
|
get fileDiffActive(): boolean {
|
|
314
167
|
return this._fileDiffActive;
|
|
@@ -322,37 +175,43 @@ export class DiffContentWidget extends ReactWidget {
|
|
|
322
175
|
this._fileDiffActiveChanged.emit(value);
|
|
323
176
|
}
|
|
324
177
|
|
|
325
|
-
/** Emits whenever {@link fileDiffActive} changes. */
|
|
326
178
|
get fileDiffActiveChanged(): ISignal<this, boolean> {
|
|
327
179
|
return this._fileDiffActiveChanged;
|
|
328
180
|
}
|
|
329
181
|
|
|
330
|
-
|
|
331
|
-
|
|
182
|
+
/**
|
|
183
|
+
* Emitted when a post-discard reload leaves the diff with no hunks.
|
|
184
|
+
*/
|
|
185
|
+
get emptied(): ISignal<this, void> {
|
|
186
|
+
return this._emptied;
|
|
332
187
|
}
|
|
333
188
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
return;
|
|
337
|
-
}
|
|
338
|
-
this._pinned = true;
|
|
339
|
-
this._pinnedChanged.emit(true);
|
|
189
|
+
notifyEmptied(): void {
|
|
190
|
+
this._emptied.emit();
|
|
340
191
|
}
|
|
341
192
|
|
|
342
|
-
|
|
343
|
-
|
|
193
|
+
settleRefresh(): void {
|
|
194
|
+
const pending = this._pendingRefresh;
|
|
195
|
+
this._pendingRefresh = null;
|
|
196
|
+
pending?.resolve();
|
|
344
197
|
}
|
|
345
198
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
199
|
+
get reloadNonce(): number {
|
|
200
|
+
return this._reloadNonce;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
get context(): IXtralabDiffContext {
|
|
204
|
+
return this._context;
|
|
349
205
|
}
|
|
350
206
|
|
|
351
207
|
protected render(): React.ReactElement {
|
|
352
|
-
return <
|
|
208
|
+
return <ModelDiffView widget={this} />;
|
|
353
209
|
}
|
|
354
210
|
|
|
355
|
-
private
|
|
211
|
+
private _model: IXtralabDiffModel;
|
|
212
|
+
private _context: IXtralabDiffContext;
|
|
213
|
+
private _reloadNonce = 0;
|
|
214
|
+
private _pendingRefresh: PromiseDelegate<void> | null = null;
|
|
356
215
|
private _notebookViewMode: NotebookDiffViewMode;
|
|
357
216
|
private _notebookViewModeChanged = new Signal<this, NotebookDiffViewMode>(
|
|
358
217
|
this
|
|
@@ -363,95 +222,70 @@ export class DiffContentWidget extends ReactWidget {
|
|
|
363
222
|
private _diffStyleChanged = new Signal<this, DiffStyle>(this);
|
|
364
223
|
private _fileDiffActive = false;
|
|
365
224
|
private _fileDiffActiveChanged = new Signal<this, boolean>(this);
|
|
366
|
-
private
|
|
367
|
-
private _pinnedChanged = new Signal<this, boolean>(this);
|
|
225
|
+
private _emptied = new Signal<this, void>(this);
|
|
368
226
|
}
|
|
369
227
|
|
|
370
|
-
interface
|
|
228
|
+
interface IModelDiffState {
|
|
371
229
|
loading: boolean;
|
|
372
|
-
/**
|
|
373
|
-
* Resolved old/reference and new/challenger text. The shared
|
|
374
|
-
* {@link DiffSurface} turns these into the line/notebook diff; owning
|
|
375
|
-
* only the raw strings here keeps the fetch path decoupled from how the
|
|
376
|
-
* diff is rendered.
|
|
377
|
-
*/
|
|
378
230
|
oldText: string;
|
|
379
231
|
newText: string;
|
|
380
|
-
/**
|
|
381
|
-
* Server-relative path of the file on disk. Computed from the repo path
|
|
382
|
-
* + the file's git-root-relative path. We need it to write the file back
|
|
383
|
-
* via the contents manager when discarding a hunk.
|
|
384
|
-
*/
|
|
385
|
-
serverPath: string;
|
|
386
|
-
isBinary: boolean;
|
|
387
232
|
error: string | null;
|
|
388
233
|
}
|
|
389
234
|
|
|
390
|
-
function
|
|
391
|
-
|
|
392
|
-
): React.ReactElement {
|
|
393
|
-
const {
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
onChanged,
|
|
400
|
-
onEmpty,
|
|
401
|
-
widget
|
|
402
|
-
} = props;
|
|
403
|
-
const [state, setState] = React.useState<IDiffState>({
|
|
235
|
+
function ModelDiffView(props: {
|
|
236
|
+
widget: XtralabDiffWidget;
|
|
237
|
+
}): React.ReactElement {
|
|
238
|
+
const { widget } = props;
|
|
239
|
+
const model = widget.model as IXtralabDiffModel;
|
|
240
|
+
const { contentsManager, rendermime, themeManager } = widget.context;
|
|
241
|
+
const nonce = widget.reloadNonce;
|
|
242
|
+
|
|
243
|
+
const [state, setState] = React.useState<IModelDiffState>({
|
|
404
244
|
loading: true,
|
|
405
245
|
oldText: '',
|
|
406
246
|
newText: '',
|
|
407
|
-
serverPath: '',
|
|
408
|
-
isBinary: false,
|
|
409
247
|
error: null
|
|
410
248
|
});
|
|
411
|
-
|
|
412
|
-
//
|
|
413
|
-
|
|
414
|
-
const [reloadKey, setReloadKey] = React.useState(0);
|
|
415
|
-
// Mirror the widget's notebook view mode into local state so that
|
|
416
|
-
// toolbar-driven changes re-render the diff. The widget owns the
|
|
417
|
-
// canonical value (so the toolbar can read and write it without going
|
|
418
|
-
// through React); the React copy is just a cache for rendering.
|
|
419
|
-
const [notebookViewMode, setNotebookViewModeLocal] =
|
|
249
|
+
|
|
250
|
+
// Mirror toolbar-driven notebook view changes into React state.
|
|
251
|
+
const [notebookViewMode, setNotebookViewMode] =
|
|
420
252
|
React.useState<NotebookDiffViewMode>(() => widget.notebookViewMode);
|
|
421
253
|
React.useEffect(() => {
|
|
422
254
|
const handler = (
|
|
423
|
-
_sender:
|
|
255
|
+
_sender: XtralabDiffWidget,
|
|
424
256
|
mode: NotebookDiffViewMode
|
|
425
257
|
): void => {
|
|
426
|
-
|
|
258
|
+
setNotebookViewMode(mode);
|
|
427
259
|
};
|
|
428
260
|
widget.notebookViewModeChanged.connect(handler);
|
|
429
|
-
|
|
430
|
-
setNotebookViewModeLocal(widget.notebookViewMode);
|
|
261
|
+
setNotebookViewMode(widget.notebookViewMode);
|
|
431
262
|
return () => {
|
|
432
263
|
widget.notebookViewModeChanged.disconnect(handler);
|
|
433
264
|
};
|
|
434
265
|
}, [widget]);
|
|
435
266
|
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
267
|
+
const handleNotebookAvailabilityChange = React.useCallback(
|
|
268
|
+
(available: boolean) => {
|
|
269
|
+
widget.setHasNotebookView(available);
|
|
270
|
+
},
|
|
271
|
+
[widget]
|
|
272
|
+
);
|
|
273
|
+
|
|
274
|
+
// Mirror toolbar-driven diff-style changes into React state.
|
|
275
|
+
const [diffStyle, setDiffStyle] = React.useState<DiffStyle>(
|
|
440
276
|
() => widget.diffStyle
|
|
441
277
|
);
|
|
442
278
|
React.useEffect(() => {
|
|
443
|
-
const handler = (_sender:
|
|
444
|
-
|
|
279
|
+
const handler = (_sender: XtralabDiffWidget, style: DiffStyle): void => {
|
|
280
|
+
setDiffStyle(style);
|
|
445
281
|
};
|
|
446
282
|
widget.diffStyleChanged.connect(handler);
|
|
447
|
-
|
|
283
|
+
setDiffStyle(widget.diffStyle);
|
|
448
284
|
return () => {
|
|
449
285
|
widget.diffStyleChanged.disconnect(handler);
|
|
450
286
|
};
|
|
451
287
|
}, [widget]);
|
|
452
288
|
|
|
453
|
-
// Tell the host widget whether the textual/code file diff is the active
|
|
454
|
-
// view, so the toolbar's Split/Unified selector only shows when it applies.
|
|
455
289
|
const handleFileDiffActiveChange = React.useCallback(
|
|
456
290
|
(active: boolean) => {
|
|
457
291
|
widget.setFileDiffActive(active);
|
|
@@ -459,68 +293,24 @@ function DiffViewer(
|
|
|
459
293
|
[widget]
|
|
460
294
|
);
|
|
461
295
|
|
|
462
|
-
// Tell the host widget whether a rendered notebook view is currently
|
|
463
|
-
// available. The toolbar item that lives in the `MainAreaWidget`
|
|
464
|
-
// toolbar reads this flag to decide whether to show the
|
|
465
|
-
// Notebook/JSON selector — and the toolbar bar itself stays hidden
|
|
466
|
-
// when the file isn't a parseable notebook.
|
|
467
|
-
const handleNotebookAvailabilityChange = React.useCallback(
|
|
468
|
-
(available: boolean) => {
|
|
469
|
-
widget.setHasNotebookView(available);
|
|
470
|
-
},
|
|
471
|
-
[widget]
|
|
472
|
-
);
|
|
473
|
-
|
|
474
|
-
// The shared surface reports its hunk count after every (re)computation;
|
|
475
|
-
// we keep the latest so the auto-close-on-empty effect can fire after a
|
|
476
|
-
// discard empties the diff.
|
|
477
|
-
const [hunkCount, setHunkCount] = React.useState<number | null>(null);
|
|
478
|
-
const handleMetadataChange = React.useCallback(
|
|
479
|
-
(info: { hunkCount: number | null }) => {
|
|
480
|
-
setHunkCount(info.hunkCount);
|
|
481
|
-
},
|
|
482
|
-
[]
|
|
483
|
-
);
|
|
484
|
-
|
|
485
|
-
// Auto-close after a discard that emptied the diff. Gated on `reloadKey > 0`
|
|
486
|
-
// so a file that opens already-empty (uncommon but possible) is not closed
|
|
487
|
-
// before the user has had a chance to look at it.
|
|
488
|
-
React.useEffect(() => {
|
|
489
|
-
if (
|
|
490
|
-
!state.loading &&
|
|
491
|
-
state.error === null &&
|
|
492
|
-
!state.isBinary &&
|
|
493
|
-
hunkCount === 0 &&
|
|
494
|
-
reloadKey > 0
|
|
495
|
-
) {
|
|
496
|
-
onEmpty?.();
|
|
497
|
-
}
|
|
498
|
-
}, [
|
|
499
|
-
state.loading,
|
|
500
|
-
state.error,
|
|
501
|
-
state.isBinary,
|
|
502
|
-
hunkCount,
|
|
503
|
-
reloadKey,
|
|
504
|
-
onEmpty
|
|
505
|
-
]);
|
|
506
296
|
const [dark, setDark] = React.useState<boolean>(() =>
|
|
507
297
|
isDarkTheme(themeManager)
|
|
508
298
|
);
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
// and fall back to a `<body>` attribute observer otherwise.
|
|
299
|
+
const [pierre, setPierre] = React.useState<boolean>(() =>
|
|
300
|
+
isPierreTheme(themeManager)
|
|
301
|
+
);
|
|
513
302
|
React.useEffect(() => {
|
|
303
|
+
const sync = (): void => {
|
|
304
|
+
setDark(isDarkTheme(themeManager));
|
|
305
|
+
setPierre(isPierreTheme(themeManager));
|
|
306
|
+
};
|
|
514
307
|
if (themeManager !== null) {
|
|
515
|
-
|
|
516
|
-
themeManager.themeChanged.connect(handler);
|
|
308
|
+
themeManager.themeChanged.connect(sync);
|
|
517
309
|
return () => {
|
|
518
|
-
themeManager.themeChanged.disconnect(
|
|
310
|
+
themeManager.themeChanged.disconnect(sync);
|
|
519
311
|
};
|
|
520
312
|
}
|
|
521
|
-
const observer = new MutationObserver(
|
|
522
|
-
setDark(isDarkTheme(themeManager))
|
|
523
|
-
);
|
|
313
|
+
const observer = new MutationObserver(sync);
|
|
524
314
|
observer.observe(document.body, {
|
|
525
315
|
attributes: true,
|
|
526
316
|
attributeFilter: ['data-jp-theme-light']
|
|
@@ -530,56 +320,48 @@ function DiffViewer(
|
|
|
530
320
|
};
|
|
531
321
|
}, [themeManager]);
|
|
532
322
|
|
|
323
|
+
// Image diffs use the server's base64 payloads; other binaries show a placeholder.
|
|
324
|
+
const isImage = React.useMemo(
|
|
325
|
+
() => imageDataType(model.filename) !== null,
|
|
326
|
+
[model.filename]
|
|
327
|
+
);
|
|
328
|
+
const isBinary = model.isBinary === true && !isImage;
|
|
329
|
+
|
|
330
|
+
// Used by the launcher to auto-close an emptied diff after discard.
|
|
331
|
+
const [hunkCount, setHunkCount] = React.useState<number | null>(null);
|
|
332
|
+
const handleMetadataChange = React.useCallback(
|
|
333
|
+
(info: { hunkCount: number | null }) => {
|
|
334
|
+
setHunkCount(info.hunkCount);
|
|
335
|
+
},
|
|
336
|
+
[]
|
|
337
|
+
);
|
|
338
|
+
|
|
339
|
+
// Fetch both sides whenever the model or reload nonce changes.
|
|
533
340
|
React.useEffect(() => {
|
|
534
341
|
let cancelled = false;
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
loading:
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
// Raster images are still binary, but the git server base64-encodes
|
|
546
|
-
// binary content, so we fetch them like text and let the surface render
|
|
547
|
-
// an image diff. Other binary files have no useful textual diff.
|
|
548
|
-
const isImage = imageDataType(change.path) !== null;
|
|
342
|
+
// Non-image binaries do not need content fetches.
|
|
343
|
+
if (isBinary) {
|
|
344
|
+
setState({ loading: false, oldText: '', newText: '', error: null });
|
|
345
|
+
// An in-flight `refresh()` still has to resolve.
|
|
346
|
+
widget.settleRefresh();
|
|
347
|
+
return () => {
|
|
348
|
+
cancelled = true;
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
setState({ loading: true, oldText: '', newText: '', error: null });
|
|
549
352
|
void (async () => {
|
|
550
353
|
try {
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
setState({
|
|
556
|
-
loading: false,
|
|
557
|
-
oldText: '',
|
|
558
|
-
newText: '',
|
|
559
|
-
serverPath,
|
|
560
|
-
isBinary: true,
|
|
561
|
-
error: null
|
|
562
|
-
});
|
|
563
|
-
return;
|
|
564
|
-
}
|
|
565
|
-
const fetchOld =
|
|
566
|
-
oldRef === null
|
|
567
|
-
? Promise.resolve({ content: '' })
|
|
568
|
-
: content(repoPath, change.from ?? change.path, oldRef);
|
|
569
|
-
const fetchNew =
|
|
570
|
-
newRef === null
|
|
571
|
-
? Promise.resolve({ content: '' })
|
|
572
|
-
: content(repoPath, change.path, newRef);
|
|
573
|
-
const [oldRes, newRes] = await Promise.all([fetchOld, fetchNew]);
|
|
354
|
+
const [oldText, newText] = await Promise.all([
|
|
355
|
+
model.reference.content(),
|
|
356
|
+
model.challenger.content()
|
|
357
|
+
]);
|
|
574
358
|
if (cancelled) {
|
|
575
359
|
return;
|
|
576
360
|
}
|
|
577
361
|
setState({
|
|
578
362
|
loading: false,
|
|
579
|
-
oldText:
|
|
580
|
-
newText:
|
|
581
|
-
serverPath,
|
|
582
|
-
isBinary: false,
|
|
363
|
+
oldText: oldText ?? '',
|
|
364
|
+
newText: newText ?? '',
|
|
583
365
|
error: null
|
|
584
366
|
});
|
|
585
367
|
} catch (err) {
|
|
@@ -590,52 +372,73 @@ function DiffViewer(
|
|
|
590
372
|
loading: false,
|
|
591
373
|
oldText: '',
|
|
592
374
|
newText: '',
|
|
593
|
-
serverPath,
|
|
594
|
-
isBinary: false,
|
|
595
375
|
error: err instanceof Error ? err.message : String(err)
|
|
596
376
|
});
|
|
377
|
+
} finally {
|
|
378
|
+
if (!cancelled) {
|
|
379
|
+
// Let `refresh()` settle once content has loaded.
|
|
380
|
+
widget.settleRefresh();
|
|
381
|
+
}
|
|
597
382
|
}
|
|
598
383
|
})();
|
|
599
384
|
return () => {
|
|
600
385
|
cancelled = true;
|
|
601
386
|
};
|
|
602
|
-
}, [
|
|
387
|
+
}, [widget, model, nonce, isBinary]);
|
|
603
388
|
|
|
604
|
-
//
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
389
|
+
// Only close after a reload, not for a file that opens already empty.
|
|
390
|
+
React.useEffect(() => {
|
|
391
|
+
if (
|
|
392
|
+
!state.loading &&
|
|
393
|
+
state.error === null &&
|
|
394
|
+
!isBinary &&
|
|
395
|
+
hunkCount === 0 &&
|
|
396
|
+
nonce > 0
|
|
397
|
+
) {
|
|
398
|
+
widget.notifyEmptied();
|
|
399
|
+
}
|
|
400
|
+
}, [state.loading, state.error, isBinary, hunkCount, nonce, widget]);
|
|
401
|
+
|
|
402
|
+
// The launcher sets `canDiscard`; jupyterlab-git models fall back to source.
|
|
608
403
|
const canDiscardHunk =
|
|
609
|
-
|
|
404
|
+
model.canDiscard ??
|
|
405
|
+
(model.challenger.source === Git.Diff.SpecialRef.WORKING &&
|
|
406
|
+
model.hasConflict !== true);
|
|
407
|
+
|
|
408
|
+
const serverPath = React.useMemo(
|
|
409
|
+
() => PathExt.join(model.repositoryPath ?? '', model.filename),
|
|
410
|
+
[model.repositoryPath, model.filename]
|
|
411
|
+
);
|
|
610
412
|
|
|
611
413
|
const hunkDiscard = React.useMemo(
|
|
612
414
|
() => ({
|
|
613
415
|
enabled: canDiscardHunk,
|
|
614
416
|
save: async (text: string) => {
|
|
615
|
-
await contentsManager.save(
|
|
417
|
+
await contentsManager.save(serverPath, {
|
|
616
418
|
type: 'file',
|
|
617
419
|
format: 'text',
|
|
618
420
|
content: text
|
|
619
421
|
});
|
|
620
422
|
},
|
|
621
423
|
onAfterSave: () => {
|
|
622
|
-
|
|
623
|
-
|
|
424
|
+
// Re-pull so the diff reflects the reverted hunk.
|
|
425
|
+
void widget.refresh();
|
|
624
426
|
}
|
|
625
427
|
}),
|
|
626
|
-
[canDiscardHunk, contentsManager,
|
|
428
|
+
[canDiscardHunk, contentsManager, serverPath, widget]
|
|
627
429
|
);
|
|
628
430
|
|
|
629
431
|
return (
|
|
630
432
|
<DiffSurface
|
|
631
433
|
loading={state.loading}
|
|
632
434
|
error={state.error}
|
|
633
|
-
isBinary={
|
|
435
|
+
isBinary={isBinary}
|
|
634
436
|
oldText={state.oldText}
|
|
635
437
|
newText={state.newText}
|
|
636
|
-
newName={
|
|
637
|
-
oldName={
|
|
438
|
+
newName={model.filename}
|
|
439
|
+
oldName={model.oldFilename ?? model.filename}
|
|
638
440
|
dark={dark}
|
|
441
|
+
pierreTheme={pierre}
|
|
639
442
|
rendermime={rendermime}
|
|
640
443
|
notebookViewMode={notebookViewMode}
|
|
641
444
|
diffStyle={diffStyle}
|
|
@@ -648,124 +451,129 @@ function DiffViewer(
|
|
|
648
451
|
}
|
|
649
452
|
|
|
650
453
|
/**
|
|
651
|
-
*
|
|
652
|
-
* toolbar for `.ipynb` diffs (launcher path). The selector reads from and
|
|
653
|
-
* writes to the {@link DiffContentWidget} so React state changes (via the
|
|
654
|
-
* diff content) and toolbar interactions stay in sync without either side
|
|
655
|
-
* knowing about the other. The host toolbar widget's visibility is toggled
|
|
656
|
-
* by {@link createGitDiffWidget} based on the same `hasNotebookView`
|
|
657
|
-
* signal, so this control is always meaningful when it appears.
|
|
454
|
+
* Notebook/JSON toggle mounted into a host-owned toolbar.
|
|
658
455
|
*/
|
|
659
456
|
class NotebookViewModeToolbarItem extends ReactWidget {
|
|
660
|
-
constructor(
|
|
457
|
+
constructor(widget: XtralabDiffWidget) {
|
|
661
458
|
super();
|
|
662
|
-
this.
|
|
459
|
+
this._widget = widget;
|
|
663
460
|
this.addClass('jp-xtralab-DiffWidget-viewModeToolbarItem');
|
|
664
461
|
}
|
|
665
462
|
|
|
666
463
|
protected render(): React.ReactElement {
|
|
667
|
-
return <NotebookViewModeToolbarControl
|
|
464
|
+
return <NotebookViewModeToolbarControl widget={this._widget} />;
|
|
668
465
|
}
|
|
669
466
|
|
|
670
|
-
private
|
|
467
|
+
private _widget: XtralabDiffWidget;
|
|
671
468
|
}
|
|
672
469
|
|
|
673
470
|
function NotebookViewModeToolbarControl(props: {
|
|
674
|
-
|
|
471
|
+
widget: XtralabDiffWidget;
|
|
675
472
|
}): React.ReactElement {
|
|
676
|
-
const {
|
|
473
|
+
const { widget } = props;
|
|
677
474
|
const [mode, setMode] = React.useState<NotebookDiffViewMode>(
|
|
678
|
-
() =>
|
|
475
|
+
() => widget.notebookViewMode
|
|
679
476
|
);
|
|
680
477
|
const [available, setAvailable] = React.useState<boolean>(
|
|
681
|
-
() =>
|
|
478
|
+
() => widget.hasNotebookView
|
|
682
479
|
);
|
|
683
480
|
React.useEffect(() => {
|
|
684
481
|
const onMode = (
|
|
685
|
-
_sender:
|
|
482
|
+
_sender: XtralabDiffWidget,
|
|
686
483
|
next: NotebookDiffViewMode
|
|
687
484
|
): void => {
|
|
688
485
|
setMode(next);
|
|
689
486
|
};
|
|
690
|
-
const onAvailable = (_sender:
|
|
487
|
+
const onAvailable = (_sender: XtralabDiffWidget, next: boolean): void => {
|
|
691
488
|
setAvailable(next);
|
|
692
489
|
};
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
setMode(
|
|
696
|
-
setAvailable(
|
|
490
|
+
widget.notebookViewModeChanged.connect(onMode);
|
|
491
|
+
widget.hasNotebookViewChanged.connect(onAvailable);
|
|
492
|
+
setMode(widget.notebookViewMode);
|
|
493
|
+
setAvailable(widget.hasNotebookView);
|
|
697
494
|
return () => {
|
|
698
|
-
|
|
699
|
-
|
|
495
|
+
widget.notebookViewModeChanged.disconnect(onMode);
|
|
496
|
+
widget.hasNotebookViewChanged.disconnect(onAvailable);
|
|
700
497
|
};
|
|
701
|
-
}, [
|
|
498
|
+
}, [widget]);
|
|
702
499
|
|
|
703
500
|
return (
|
|
704
501
|
<NotebookViewModeControl
|
|
705
502
|
mode={mode}
|
|
706
503
|
available={available}
|
|
707
|
-
onChange={next =>
|
|
504
|
+
onChange={next => widget.setNotebookViewMode(next)}
|
|
708
505
|
/>
|
|
709
506
|
);
|
|
710
507
|
}
|
|
711
508
|
|
|
712
509
|
/**
|
|
713
|
-
* Split/Unified
|
|
714
|
-
* {@link NotebookViewModeToolbarItem}: it reads from and writes to the
|
|
715
|
-
* {@link DiffContentWidget} so toolbar interactions and React state stay in
|
|
716
|
-
* sync, and only appears while the textual/code file diff is the active view
|
|
717
|
-
* (driven by the same `fileDiffActive` signal that gates toolbar visibility).
|
|
510
|
+
* Split/Unified toggle mounted into a host-owned toolbar.
|
|
718
511
|
*/
|
|
719
512
|
class DiffStyleToolbarItem extends ReactWidget {
|
|
720
|
-
constructor(
|
|
513
|
+
constructor(widget: XtralabDiffWidget) {
|
|
721
514
|
super();
|
|
722
|
-
this.
|
|
515
|
+
this._widget = widget;
|
|
723
516
|
this.addClass('jp-xtralab-DiffWidget-diffStyleToolbarItem');
|
|
724
517
|
}
|
|
725
518
|
|
|
726
519
|
protected render(): React.ReactElement {
|
|
727
|
-
return <DiffStyleToolbarControl
|
|
520
|
+
return <DiffStyleToolbarControl widget={this._widget} />;
|
|
728
521
|
}
|
|
729
522
|
|
|
730
|
-
private
|
|
523
|
+
private _widget: XtralabDiffWidget;
|
|
731
524
|
}
|
|
732
525
|
|
|
733
526
|
function DiffStyleToolbarControl(props: {
|
|
734
|
-
|
|
527
|
+
widget: XtralabDiffWidget;
|
|
735
528
|
}): React.ReactElement {
|
|
736
|
-
const {
|
|
737
|
-
const [style, setStyle] = React.useState<DiffStyle>(() =>
|
|
529
|
+
const { widget } = props;
|
|
530
|
+
const [style, setStyle] = React.useState<DiffStyle>(() => widget.diffStyle);
|
|
738
531
|
const [available, setAvailable] = React.useState<boolean>(
|
|
739
|
-
() =>
|
|
532
|
+
() => widget.fileDiffActive
|
|
740
533
|
);
|
|
741
534
|
React.useEffect(() => {
|
|
742
|
-
const onStyle = (_sender:
|
|
535
|
+
const onStyle = (_sender: XtralabDiffWidget, next: DiffStyle): void => {
|
|
743
536
|
setStyle(next);
|
|
744
537
|
};
|
|
745
|
-
const onAvailable = (_sender:
|
|
538
|
+
const onAvailable = (_sender: XtralabDiffWidget, next: boolean): void => {
|
|
746
539
|
setAvailable(next);
|
|
747
540
|
};
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
setStyle(
|
|
751
|
-
setAvailable(
|
|
541
|
+
widget.diffStyleChanged.connect(onStyle);
|
|
542
|
+
widget.fileDiffActiveChanged.connect(onAvailable);
|
|
543
|
+
setStyle(widget.diffStyle);
|
|
544
|
+
setAvailable(widget.fileDiffActive);
|
|
752
545
|
return () => {
|
|
753
|
-
|
|
754
|
-
|
|
546
|
+
widget.diffStyleChanged.disconnect(onStyle);
|
|
547
|
+
widget.fileDiffActiveChanged.disconnect(onAvailable);
|
|
755
548
|
};
|
|
756
|
-
}, [
|
|
549
|
+
}, [widget]);
|
|
757
550
|
|
|
758
551
|
return (
|
|
759
552
|
<DiffStyleControl
|
|
760
553
|
diffStyle={style}
|
|
761
554
|
available={available}
|
|
762
|
-
onChange={next =>
|
|
555
|
+
onChange={next => widget.setDiffStyle(next)}
|
|
763
556
|
/>
|
|
764
557
|
);
|
|
765
558
|
}
|
|
766
559
|
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
560
|
+
/**
|
|
561
|
+
* Structural toolbar type shared across host package boundaries.
|
|
562
|
+
*/
|
|
563
|
+
interface IDiffToolbar {
|
|
564
|
+
addItem(name: string, widget: Widget): boolean;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* Mount the shared diff toolbar controls into a host-owned toolbar.
|
|
569
|
+
*/
|
|
570
|
+
export function addDiffToolbarItems(
|
|
571
|
+
toolbar: IDiffToolbar,
|
|
572
|
+
widget: XtralabDiffWidget
|
|
573
|
+
): void {
|
|
574
|
+
toolbar.addItem(
|
|
575
|
+
'xtralab-notebook-view-mode',
|
|
576
|
+
new NotebookViewModeToolbarItem(widget)
|
|
577
|
+
);
|
|
578
|
+
toolbar.addItem('xtralab-diff-style', new DiffStyleToolbarItem(widget));
|
|
771
579
|
}
|