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/lib/git/diffWidget.js
CHANGED
|
@@ -1,186 +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, DiffSurface, NotebookViewModeControl, isDarkTheme, readStoredNotebookViewMode, 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
|
-
/**
|
|
48
|
-
* Build a `MainAreaWidget`-hosted diff viewer for a single file change.
|
|
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`.
|
|
59
|
-
*/
|
|
60
|
-
export function createGitDiffWidget(options) {
|
|
61
|
-
var _a;
|
|
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
|
-
const syncToolbarVisibility = () => {
|
|
105
|
-
if (widget === null || widget.isDisposed) {
|
|
106
|
-
return;
|
|
107
|
-
}
|
|
108
|
-
if (!content.pinned || content.hasNotebookView) {
|
|
109
|
-
widget.toolbar.show();
|
|
110
|
-
}
|
|
111
|
-
else {
|
|
112
|
-
widget.toolbar.hide();
|
|
113
|
-
}
|
|
114
|
-
};
|
|
115
|
-
const onHasNotebookViewChanged = () => {
|
|
116
|
-
if (widget === null || widget.isDisposed) {
|
|
117
|
-
return;
|
|
118
|
-
}
|
|
119
|
-
syncToolbarVisibility();
|
|
120
|
-
};
|
|
121
|
-
const onPinnedChanged = (sender, value) => {
|
|
122
|
-
if (widget === null || widget.isDisposed) {
|
|
123
|
-
return;
|
|
124
|
-
}
|
|
125
|
-
void sender;
|
|
126
|
-
if (value) {
|
|
127
|
-
pinButton.hide();
|
|
128
|
-
// Keep toolbar state and title styling consistent for promoted tabs.
|
|
129
|
-
onPinned(widget);
|
|
130
|
-
if (widget.isDisposed) {
|
|
131
|
-
return;
|
|
132
|
-
}
|
|
133
|
-
widget.title.className = '';
|
|
134
|
-
}
|
|
135
|
-
syncToolbarVisibility();
|
|
136
|
-
};
|
|
137
|
-
content.hasNotebookViewChanged.connect(onHasNotebookViewChanged);
|
|
138
|
-
content.pinnedChanged.connect(onPinnedChanged);
|
|
139
|
-
syncToolbarVisibility();
|
|
140
|
-
widget.disposed.connect(() => {
|
|
141
|
-
content.hasNotebookViewChanged.disconnect(onHasNotebookViewChanged);
|
|
142
|
-
content.pinnedChanged.disconnect(onPinnedChanged);
|
|
143
|
-
});
|
|
144
|
-
return widget;
|
|
145
|
-
}
|
|
146
|
-
export function updateGitDiffWidget(widget, change) {
|
|
147
|
-
widget.content.setChange(change);
|
|
148
|
-
widget.title.label = formatTitle(change);
|
|
149
|
-
widget.title.caption = change.path;
|
|
150
|
-
widget.title.icon = getTreeIcon(change.path);
|
|
151
|
-
}
|
|
152
10
|
/**
|
|
153
|
-
*
|
|
154
|
-
* `ReactWidget` so the surrounding `MainAreaWidget` can manage chrome
|
|
155
|
-
* without React having to know about it.
|
|
156
|
-
*
|
|
157
|
-
* Owns the persistent UI state that the surrounding `MainAreaWidget`
|
|
158
|
-
* toolbar needs to read and write (`notebookViewMode` and whether a
|
|
159
|
-
* notebook view is even available for the current file). Storing this on
|
|
160
|
-
* the widget — and exposing change signals — lets the toolbar widget and
|
|
161
|
-
* the React content stay in sync without either side knowing about the
|
|
162
|
-
* other.
|
|
11
|
+
* Shared diff widget used by the launcher and by `jupyterlab-git`.
|
|
163
12
|
*/
|
|
164
|
-
export class
|
|
165
|
-
constructor(
|
|
13
|
+
export class XtralabDiffWidget extends ReactWidget {
|
|
14
|
+
constructor(model, context) {
|
|
166
15
|
super();
|
|
16
|
+
this._reloadNonce = 0;
|
|
17
|
+
this._pendingRefresh = null;
|
|
167
18
|
this._notebookViewModeChanged = new Signal(this);
|
|
168
19
|
this._hasNotebookView = false;
|
|
169
20
|
this._hasNotebookViewChanged = new Signal(this);
|
|
170
|
-
this.
|
|
171
|
-
this.
|
|
172
|
-
this.
|
|
21
|
+
this._diffStyleChanged = new Signal(this);
|
|
22
|
+
this._fileDiffActive = false;
|
|
23
|
+
this._fileDiffActiveChanged = new Signal(this);
|
|
24
|
+
this._emptied = new Signal(this);
|
|
25
|
+
this._model = model;
|
|
26
|
+
this._context = context;
|
|
173
27
|
this._notebookViewMode = readStoredNotebookViewMode();
|
|
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;
|
|
48
|
+
}
|
|
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
|
+
};
|
|
174
62
|
}
|
|
175
|
-
/**
|
|
176
|
-
|
|
177
|
-
|
|
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;
|
|
178
72
|
}
|
|
179
|
-
/** The user's current rendered-vs-JSON view choice for notebook diffs. */
|
|
180
73
|
get notebookViewMode() {
|
|
181
74
|
return this._notebookViewMode;
|
|
182
75
|
}
|
|
183
|
-
/** Update the notebook view mode and persist it across sessions. */
|
|
184
76
|
setNotebookViewMode(mode) {
|
|
185
77
|
if (mode === this._notebookViewMode) {
|
|
186
78
|
return;
|
|
@@ -189,15 +81,9 @@ export class DiffContentWidget extends ReactWidget {
|
|
|
189
81
|
writeStoredNotebookViewMode(mode);
|
|
190
82
|
this._notebookViewModeChanged.emit(mode);
|
|
191
83
|
}
|
|
192
|
-
/** Emits whenever {@link notebookViewMode} changes. */
|
|
193
84
|
get notebookViewModeChanged() {
|
|
194
85
|
return this._notebookViewModeChanged;
|
|
195
86
|
}
|
|
196
|
-
/**
|
|
197
|
-
* Whether a rendered notebook view is currently available. Set by the
|
|
198
|
-
* React content once it has parsed the diff; the toolbar reads this to
|
|
199
|
-
* decide whether to show the view-mode selector.
|
|
200
|
-
*/
|
|
201
87
|
get hasNotebookView() {
|
|
202
88
|
return this._hasNotebookView;
|
|
203
89
|
}
|
|
@@ -208,109 +94,119 @@ export class DiffContentWidget extends ReactWidget {
|
|
|
208
94
|
this._hasNotebookView = value;
|
|
209
95
|
this._hasNotebookViewChanged.emit(value);
|
|
210
96
|
}
|
|
211
|
-
/** Emits whenever {@link hasNotebookView} changes. */
|
|
212
97
|
get hasNotebookViewChanged() {
|
|
213
98
|
return this._hasNotebookViewChanged;
|
|
214
99
|
}
|
|
215
|
-
get
|
|
216
|
-
return this.
|
|
100
|
+
get diffStyle() {
|
|
101
|
+
return this._diffStyle;
|
|
217
102
|
}
|
|
218
|
-
|
|
219
|
-
if (this.
|
|
103
|
+
setDiffStyle(style) {
|
|
104
|
+
if (style === this._diffStyle) {
|
|
220
105
|
return;
|
|
221
106
|
}
|
|
222
|
-
this.
|
|
223
|
-
|
|
107
|
+
this._diffStyle = style;
|
|
108
|
+
writeStoredDiffStyle(style);
|
|
109
|
+
this._diffStyleChanged.emit(style);
|
|
224
110
|
}
|
|
225
|
-
get
|
|
226
|
-
return this.
|
|
111
|
+
get diffStyleChanged() {
|
|
112
|
+
return this._diffStyleChanged;
|
|
227
113
|
}
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
114
|
+
/**
|
|
115
|
+
* Whether the Split/Unified toolbar selector currently applies.
|
|
116
|
+
*/
|
|
117
|
+
get fileDiffActive() {
|
|
118
|
+
return this._fileDiffActive;
|
|
119
|
+
}
|
|
120
|
+
setFileDiffActive(value) {
|
|
121
|
+
if (value === this._fileDiffActive) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
this._fileDiffActive = value;
|
|
125
|
+
this._fileDiffActiveChanged.emit(value);
|
|
126
|
+
}
|
|
127
|
+
get fileDiffActiveChanged() {
|
|
128
|
+
return this._fileDiffActiveChanged;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Emitted when a post-discard reload leaves the diff with no hunks.
|
|
132
|
+
*/
|
|
133
|
+
get emptied() {
|
|
134
|
+
return this._emptied;
|
|
135
|
+
}
|
|
136
|
+
notifyEmptied() {
|
|
137
|
+
this._emptied.emit();
|
|
138
|
+
}
|
|
139
|
+
settleRefresh() {
|
|
140
|
+
const pending = this._pendingRefresh;
|
|
141
|
+
this._pendingRefresh = null;
|
|
142
|
+
pending === null || pending === void 0 ? void 0 : pending.resolve();
|
|
143
|
+
}
|
|
144
|
+
get reloadNonce() {
|
|
145
|
+
return this._reloadNonce;
|
|
146
|
+
}
|
|
147
|
+
get context() {
|
|
148
|
+
return this._context;
|
|
231
149
|
}
|
|
232
150
|
render() {
|
|
233
|
-
return React.createElement(
|
|
151
|
+
return React.createElement(ModelDiffView, { widget: this });
|
|
234
152
|
}
|
|
235
153
|
}
|
|
236
|
-
function
|
|
237
|
-
var _a;
|
|
238
|
-
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;
|
|
239
160
|
const [state, setState] = React.useState({
|
|
240
161
|
loading: true,
|
|
241
162
|
oldText: '',
|
|
242
163
|
newText: '',
|
|
243
|
-
serverPath: '',
|
|
244
|
-
isBinary: false,
|
|
245
164
|
error: null
|
|
246
165
|
});
|
|
247
|
-
//
|
|
248
|
-
|
|
249
|
-
// without the user having to close and reopen the tab.
|
|
250
|
-
const [reloadKey, setReloadKey] = React.useState(0);
|
|
251
|
-
// Mirror the widget's notebook view mode into local state so that
|
|
252
|
-
// toolbar-driven changes re-render the diff. The widget owns the
|
|
253
|
-
// canonical value (so the toolbar can read and write it without going
|
|
254
|
-
// through React); the React copy is just a cache for rendering.
|
|
255
|
-
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);
|
|
256
168
|
React.useEffect(() => {
|
|
257
169
|
const handler = (_sender, mode) => {
|
|
258
|
-
|
|
170
|
+
setNotebookViewMode(mode);
|
|
259
171
|
};
|
|
260
172
|
widget.notebookViewModeChanged.connect(handler);
|
|
261
|
-
|
|
262
|
-
setNotebookViewModeLocal(widget.notebookViewMode);
|
|
173
|
+
setNotebookViewMode(widget.notebookViewMode);
|
|
263
174
|
return () => {
|
|
264
175
|
widget.notebookViewModeChanged.disconnect(handler);
|
|
265
176
|
};
|
|
266
177
|
}, [widget]);
|
|
267
|
-
// Tell the host widget whether a rendered notebook view is currently
|
|
268
|
-
// available. The toolbar item that lives in the `MainAreaWidget`
|
|
269
|
-
// toolbar reads this flag to decide whether to show the
|
|
270
|
-
// Notebook/JSON selector — and the toolbar bar itself stays hidden
|
|
271
|
-
// when the file isn't a parseable notebook.
|
|
272
178
|
const handleNotebookAvailabilityChange = React.useCallback((available) => {
|
|
273
179
|
widget.setHasNotebookView(available);
|
|
274
180
|
}, [widget]);
|
|
275
|
-
//
|
|
276
|
-
|
|
277
|
-
// discard empties the diff.
|
|
278
|
-
const [hunkCount, setHunkCount] = React.useState(null);
|
|
279
|
-
const handleMetadataChange = React.useCallback((info) => {
|
|
280
|
-
setHunkCount(info.hunkCount);
|
|
281
|
-
}, []);
|
|
282
|
-
// Auto-close after a discard that emptied the diff. Gated on `reloadKey > 0`
|
|
283
|
-
// so a file that opens already-empty (uncommon but possible) is not closed
|
|
284
|
-
// before the user has had a chance to look at it.
|
|
181
|
+
// Mirror toolbar-driven diff-style changes into React state.
|
|
182
|
+
const [diffStyle, setDiffStyle] = React.useState(() => widget.diffStyle);
|
|
285
183
|
React.useEffect(() => {
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
reloadKey,
|
|
299
|
-
onEmpty
|
|
300
|
-
]);
|
|
184
|
+
const handler = (_sender, style) => {
|
|
185
|
+
setDiffStyle(style);
|
|
186
|
+
};
|
|
187
|
+
widget.diffStyleChanged.connect(handler);
|
|
188
|
+
setDiffStyle(widget.diffStyle);
|
|
189
|
+
return () => {
|
|
190
|
+
widget.diffStyleChanged.disconnect(handler);
|
|
191
|
+
};
|
|
192
|
+
}, [widget]);
|
|
193
|
+
const handleFileDiffActiveChange = React.useCallback((active) => {
|
|
194
|
+
widget.setFileDiffActive(active);
|
|
195
|
+
}, [widget]);
|
|
301
196
|
const [dark, setDark] = React.useState(() => isDarkTheme(themeManager));
|
|
302
|
-
|
|
303
|
-
// JupyterLab. We listen on `IThemeManager.themeChanged` when available
|
|
304
|
-
// and fall back to a `<body>` attribute observer otherwise.
|
|
197
|
+
const [pierre, setPierre] = React.useState(() => isPierreTheme(themeManager));
|
|
305
198
|
React.useEffect(() => {
|
|
199
|
+
const sync = () => {
|
|
200
|
+
setDark(isDarkTheme(themeManager));
|
|
201
|
+
setPierre(isPierreTheme(themeManager));
|
|
202
|
+
};
|
|
306
203
|
if (themeManager !== null) {
|
|
307
|
-
|
|
308
|
-
themeManager.themeChanged.connect(handler);
|
|
204
|
+
themeManager.themeChanged.connect(sync);
|
|
309
205
|
return () => {
|
|
310
|
-
themeManager.themeChanged.disconnect(
|
|
206
|
+
themeManager.themeChanged.disconnect(sync);
|
|
311
207
|
};
|
|
312
208
|
}
|
|
313
|
-
const observer = new MutationObserver(
|
|
209
|
+
const observer = new MutationObserver(sync);
|
|
314
210
|
observer.observe(document.body, {
|
|
315
211
|
attributes: true,
|
|
316
212
|
attributeFilter: ['data-jp-theme-light']
|
|
@@ -319,55 +215,40 @@ function DiffViewer(props) {
|
|
|
319
215
|
observer.disconnect();
|
|
320
216
|
};
|
|
321
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.
|
|
322
227
|
React.useEffect(() => {
|
|
323
228
|
let cancelled = false;
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
loading:
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
// Raster images are still binary, but the git server base64-encodes
|
|
335
|
-
// binary content, so we fetch them like text and let the surface render
|
|
336
|
-
// an image diff. Other binary files have no useful textual diff.
|
|
337
|
-
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 });
|
|
338
239
|
void (async () => {
|
|
339
|
-
var _a, _b, _c;
|
|
340
240
|
try {
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
setState({
|
|
346
|
-
loading: false,
|
|
347
|
-
oldText: '',
|
|
348
|
-
newText: '',
|
|
349
|
-
serverPath,
|
|
350
|
-
isBinary: true,
|
|
351
|
-
error: null
|
|
352
|
-
});
|
|
353
|
-
return;
|
|
354
|
-
}
|
|
355
|
-
const fetchOld = oldRef === null
|
|
356
|
-
? Promise.resolve({ content: '' })
|
|
357
|
-
: content(repoPath, (_a = change.from) !== null && _a !== void 0 ? _a : change.path, oldRef);
|
|
358
|
-
const fetchNew = newRef === null
|
|
359
|
-
? Promise.resolve({ content: '' })
|
|
360
|
-
: content(repoPath, change.path, newRef);
|
|
361
|
-
const [oldRes, newRes] = await Promise.all([fetchOld, fetchNew]);
|
|
241
|
+
const [oldText, newText] = await Promise.all([
|
|
242
|
+
model.reference.content(),
|
|
243
|
+
model.challenger.content()
|
|
244
|
+
]);
|
|
362
245
|
if (cancelled) {
|
|
363
246
|
return;
|
|
364
247
|
}
|
|
365
248
|
setState({
|
|
366
249
|
loading: false,
|
|
367
|
-
oldText:
|
|
368
|
-
newText:
|
|
369
|
-
serverPath,
|
|
370
|
-
isBinary: false,
|
|
250
|
+
oldText: oldText !== null && oldText !== void 0 ? oldText : '',
|
|
251
|
+
newText: newText !== null && newText !== void 0 ? newText : '',
|
|
371
252
|
error: null
|
|
372
253
|
});
|
|
373
254
|
}
|
|
@@ -379,60 +260,67 @@ function DiffViewer(props) {
|
|
|
379
260
|
loading: false,
|
|
380
261
|
oldText: '',
|
|
381
262
|
newText: '',
|
|
382
|
-
serverPath,
|
|
383
|
-
isBinary: false,
|
|
384
263
|
error: err instanceof Error ? err.message : String(err)
|
|
385
264
|
});
|
|
386
265
|
}
|
|
266
|
+
finally {
|
|
267
|
+
if (!cancelled) {
|
|
268
|
+
// Let `refresh()` settle once content has loaded.
|
|
269
|
+
widget.settleRefresh();
|
|
270
|
+
}
|
|
271
|
+
}
|
|
387
272
|
})();
|
|
388
273
|
return () => {
|
|
389
274
|
cancelled = true;
|
|
390
275
|
};
|
|
391
|
-
}, [
|
|
392
|
-
//
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
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]);
|
|
397
291
|
const hunkDiscard = React.useMemo(() => ({
|
|
398
292
|
enabled: canDiscardHunk,
|
|
399
293
|
save: async (text) => {
|
|
400
|
-
await contentsManager.save(
|
|
294
|
+
await contentsManager.save(serverPath, {
|
|
401
295
|
type: 'file',
|
|
402
296
|
format: 'text',
|
|
403
297
|
content: text
|
|
404
298
|
});
|
|
405
299
|
},
|
|
406
300
|
onAfterSave: () => {
|
|
407
|
-
|
|
408
|
-
|
|
301
|
+
// Re-pull so the diff reflects the reverted hunk.
|
|
302
|
+
void widget.refresh();
|
|
409
303
|
}
|
|
410
|
-
}), [canDiscardHunk, contentsManager,
|
|
411
|
-
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 }));
|
|
412
306
|
}
|
|
413
307
|
/**
|
|
414
|
-
*
|
|
415
|
-
* toolbar for `.ipynb` diffs (launcher path). The selector reads from and
|
|
416
|
-
* writes to the {@link DiffContentWidget} so React state changes (via the
|
|
417
|
-
* diff content) and toolbar interactions stay in sync without either side
|
|
418
|
-
* knowing about the other. The host toolbar widget's visibility is toggled
|
|
419
|
-
* by {@link createGitDiffWidget} based on the same `hasNotebookView`
|
|
420
|
-
* signal, so this control is always meaningful when it appears.
|
|
308
|
+
* Notebook/JSON toggle mounted into a host-owned toolbar.
|
|
421
309
|
*/
|
|
422
310
|
class NotebookViewModeToolbarItem extends ReactWidget {
|
|
423
|
-
constructor(
|
|
311
|
+
constructor(widget) {
|
|
424
312
|
super();
|
|
425
|
-
this.
|
|
313
|
+
this._widget = widget;
|
|
426
314
|
this.addClass('jp-xtralab-DiffWidget-viewModeToolbarItem');
|
|
427
315
|
}
|
|
428
316
|
render() {
|
|
429
|
-
return React.createElement(NotebookViewModeToolbarControl, {
|
|
317
|
+
return React.createElement(NotebookViewModeToolbarControl, { widget: this._widget });
|
|
430
318
|
}
|
|
431
319
|
}
|
|
432
320
|
function NotebookViewModeToolbarControl(props) {
|
|
433
|
-
const {
|
|
434
|
-
const [mode, setMode] = React.useState(() =>
|
|
435
|
-
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);
|
|
436
324
|
React.useEffect(() => {
|
|
437
325
|
const onMode = (_sender, next) => {
|
|
438
326
|
setMode(next);
|
|
@@ -440,20 +328,56 @@ function NotebookViewModeToolbarControl(props) {
|
|
|
440
328
|
const onAvailable = (_sender, next) => {
|
|
441
329
|
setAvailable(next);
|
|
442
330
|
};
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
setMode(
|
|
446
|
-
setAvailable(
|
|
331
|
+
widget.notebookViewModeChanged.connect(onMode);
|
|
332
|
+
widget.hasNotebookViewChanged.connect(onAvailable);
|
|
333
|
+
setMode(widget.notebookViewMode);
|
|
334
|
+
setAvailable(widget.hasNotebookView);
|
|
447
335
|
return () => {
|
|
448
|
-
|
|
449
|
-
|
|
336
|
+
widget.notebookViewModeChanged.disconnect(onMode);
|
|
337
|
+
widget.hasNotebookViewChanged.disconnect(onAvailable);
|
|
450
338
|
};
|
|
451
|
-
}, [
|
|
452
|
-
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) }));
|
|
453
341
|
}
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
342
|
+
/**
|
|
343
|
+
* Split/Unified toggle mounted into a host-owned toolbar.
|
|
344
|
+
*/
|
|
345
|
+
class DiffStyleToolbarItem extends ReactWidget {
|
|
346
|
+
constructor(widget) {
|
|
347
|
+
super();
|
|
348
|
+
this._widget = widget;
|
|
349
|
+
this.addClass('jp-xtralab-DiffWidget-diffStyleToolbarItem');
|
|
350
|
+
}
|
|
351
|
+
render() {
|
|
352
|
+
return React.createElement(DiffStyleToolbarControl, { widget: this._widget });
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
function DiffStyleToolbarControl(props) {
|
|
356
|
+
const { widget } = props;
|
|
357
|
+
const [style, setStyle] = React.useState(() => widget.diffStyle);
|
|
358
|
+
const [available, setAvailable] = React.useState(() => widget.fileDiffActive);
|
|
359
|
+
React.useEffect(() => {
|
|
360
|
+
const onStyle = (_sender, next) => {
|
|
361
|
+
setStyle(next);
|
|
362
|
+
};
|
|
363
|
+
const onAvailable = (_sender, next) => {
|
|
364
|
+
setAvailable(next);
|
|
365
|
+
};
|
|
366
|
+
widget.diffStyleChanged.connect(onStyle);
|
|
367
|
+
widget.fileDiffActiveChanged.connect(onAvailable);
|
|
368
|
+
setStyle(widget.diffStyle);
|
|
369
|
+
setAvailable(widget.fileDiffActive);
|
|
370
|
+
return () => {
|
|
371
|
+
widget.diffStyleChanged.disconnect(onStyle);
|
|
372
|
+
widget.fileDiffActiveChanged.disconnect(onAvailable);
|
|
373
|
+
};
|
|
374
|
+
}, [widget]);
|
|
375
|
+
return (React.createElement(DiffStyleControl, { diffStyle: style, available: available, onChange: next => widget.setDiffStyle(next) }));
|
|
376
|
+
}
|
|
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));
|
|
459
383
|
}
|