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