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