xtralab 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -6
- package/lib/fileBrowser/commands.d.ts +7 -0
- package/lib/fileBrowser/commands.js +74 -1
- package/lib/fileBrowser/fileBrowser.js +4 -1
- package/lib/fileBrowser/icons.d.ts +5 -9
- package/lib/fileBrowser/icons.js +97 -13
- package/lib/fileBrowser/index.js +5 -3
- package/lib/fileTypeIcons/index.d.ts +6 -0
- package/lib/fileTypeIcons/index.js +144 -0
- package/lib/git/commands.d.ts +34 -33
- package/lib/git/commands.js +132 -10
- package/lib/git/diffModel.d.ts +14 -0
- package/lib/git/diffModel.js +75 -0
- package/lib/git/diffProvider.d.ts +0 -5
- package/lib/git/diffProvider.js +6 -376
- package/lib/git/diffSurface.d.ts +36 -26
- package/lib/git/diffSurface.js +13 -20
- package/lib/git/diffTheme.d.ts +9 -0
- package/lib/git/diffTheme.js +31 -0
- package/lib/git/diffWidget.d.ts +54 -91
- package/lib/git/diffWidget.js +184 -361
- package/lib/git/index.d.ts +1 -6
- package/lib/git/index.js +7 -23
- package/lib/git/notebookDiff.d.ts +6 -1
- package/lib/git/notebookDiff.js +4 -3
- package/lib/index.js +2 -0
- package/lib/terminals/model.d.ts +1 -1
- package/lib/terminals/model.js +1 -1
- package/lib/terminals/widget.d.ts +1 -1
- package/lib/terminals/widget.js +1 -1
- package/package.json +3 -3
- package/schema/plugin.json +6 -0
- package/src/fileBrowser/commands.ts +91 -2
- package/src/fileBrowser/fileBrowser.tsx +4 -1
- package/src/fileBrowser/icons.ts +103 -13
- package/src/fileBrowser/index.ts +9 -3
- package/src/fileTypeIcons/index.ts +174 -0
- package/src/git/commands.ts +177 -43
- package/src/git/diffModel.ts +102 -0
- package/src/git/diffProvider.tsx +8 -540
- package/src/git/diffSurface.tsx +44 -28
- package/src/git/diffTheme.ts +40 -0
- package/src/git/diffWidget.tsx +256 -448
- package/src/git/index.ts +14 -28
- package/src/git/notebookDiff.tsx +10 -3
- package/src/index.ts +2 -0
- package/src/terminals/model.ts +1 -1
- package/src/terminals/widget.tsx +1 -1
- package/style/git.css +12 -9
- package/style/index.css +1 -0
- package/style/index.js +1 -0
- package/style/launcher.css +46 -18
- package/style/sidebar.css +14 -0
package/lib/git/commands.js
CHANGED
|
@@ -1,20 +1,144 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { MainAreaWidget } from '@jupyterlab/apputils';
|
|
2
|
+
import { ToolbarButton, launchIcon } from '@jupyterlab/ui-components';
|
|
3
|
+
import { Signal } from '@lumino/signaling';
|
|
4
|
+
import { getTreeIcon } from '../fileBrowser/icons';
|
|
5
|
+
import { fileChangeToDiffModel } from './diffModel';
|
|
6
|
+
import { DIFF_WIDGET_CSS_CLASS, XtralabDiffWidget, addDiffToolbarItems } from './diffWidget';
|
|
2
7
|
/**
|
|
3
|
-
* Command IDs exposed by the launcher's git diff path.
|
|
4
|
-
* dashboard's "Changes" section drives {@link CommandIDs.openDiff} to open
|
|
5
|
-
* its own diff tab, independent of `jupyterlab-git` (whose panel reaches
|
|
6
|
-
* the same rendering via the providers in `diffProvider.tsx`).
|
|
8
|
+
* Command IDs exposed by the launcher's git diff path.
|
|
7
9
|
*/
|
|
8
10
|
export var CommandIDs;
|
|
9
11
|
(function (CommandIDs) {
|
|
10
12
|
CommandIDs.openDiff = 'xtralab:git:open-diff';
|
|
11
13
|
})(CommandIDs || (CommandIDs = {}));
|
|
14
|
+
/**
|
|
15
|
+
* Widget id of the single, reused preview diff tab.
|
|
16
|
+
*/
|
|
17
|
+
export const PREVIEW_DIFF_WIDGET_ID = 'xtralab:diff:preview';
|
|
18
|
+
/**
|
|
19
|
+
* Deterministic widget id for a pinned file/group pair.
|
|
20
|
+
*/
|
|
21
|
+
export function pinnedDiffWidgetId(change) {
|
|
22
|
+
return `xtralab:diff:pinned:${change.group}:${change.path}`;
|
|
23
|
+
}
|
|
24
|
+
function formatTitle(change) {
|
|
25
|
+
var _a;
|
|
26
|
+
const name = (_a = change.path.split('/').pop()) !== null && _a !== void 0 ? _a : change.path;
|
|
27
|
+
const groupLabel = change.group === 'staged' ? 'Staged' : 'Working';
|
|
28
|
+
return `${name} (${groupLabel})`;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Launcher-owned host for a shared diff widget.
|
|
32
|
+
*/
|
|
33
|
+
export class DiffMainAreaWidget extends MainAreaWidget {
|
|
34
|
+
constructor(options, change, pinned) {
|
|
35
|
+
super(options);
|
|
36
|
+
this._pinnedChanged = new Signal(this);
|
|
37
|
+
this._change = change;
|
|
38
|
+
this._pinned = pinned;
|
|
39
|
+
}
|
|
40
|
+
get change() {
|
|
41
|
+
return this._change;
|
|
42
|
+
}
|
|
43
|
+
get pinned() {
|
|
44
|
+
return this._pinned;
|
|
45
|
+
}
|
|
46
|
+
pin() {
|
|
47
|
+
if (this._pinned) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
this._pinned = true;
|
|
51
|
+
this._pinnedChanged.emit(true);
|
|
52
|
+
}
|
|
53
|
+
get pinnedChanged() {
|
|
54
|
+
return this._pinnedChanged;
|
|
55
|
+
}
|
|
56
|
+
setChange(repoPath, change) {
|
|
57
|
+
this._change = change;
|
|
58
|
+
this.content.setModel(fileChangeToDiffModel(repoPath, change));
|
|
59
|
+
this.title.label = formatTitle(change);
|
|
60
|
+
this.title.caption = change.path;
|
|
61
|
+
this.title.icon = getTreeIcon(change.path);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Build the launcher's diff tab around the shared diff widget.
|
|
66
|
+
*/
|
|
67
|
+
function createDiffWidget(options) {
|
|
68
|
+
var _a;
|
|
69
|
+
const { repoPath, change, themeManager, contentsManager, rendermime } = options;
|
|
70
|
+
const pinned = options.pinned === true;
|
|
71
|
+
const content = new XtralabDiffWidget(fileChangeToDiffModel(repoPath, change), { contentsManager, rendermime, themeManager });
|
|
72
|
+
const widget = new DiffMainAreaWidget({ content }, change, pinned);
|
|
73
|
+
widget.id = pinned ? pinnedDiffWidgetId(change) : PREVIEW_DIFF_WIDGET_ID;
|
|
74
|
+
widget.title.label = formatTitle(change);
|
|
75
|
+
widget.title.caption = change.path;
|
|
76
|
+
widget.title.closable = true;
|
|
77
|
+
widget.title.icon = getTreeIcon(change.path);
|
|
78
|
+
widget.title.className = pinned ? '' : 'jp-mod-preview';
|
|
79
|
+
widget.addClass(DIFF_WIDGET_CSS_CLASS);
|
|
80
|
+
const onPinned = (_a = options.onPinned) !== null && _a !== void 0 ? _a : (() => undefined);
|
|
81
|
+
const pinButton = new ToolbarButton({
|
|
82
|
+
icon: launchIcon,
|
|
83
|
+
tooltip: 'Pin tab',
|
|
84
|
+
onClick: () => widget.pin()
|
|
85
|
+
});
|
|
86
|
+
if (widget.pinned) {
|
|
87
|
+
pinButton.hide();
|
|
88
|
+
}
|
|
89
|
+
widget.toolbar.addItem('pin', pinButton);
|
|
90
|
+
addDiffToolbarItems(widget.toolbar, content);
|
|
91
|
+
// Keep chrome visible only while there is a relevant toolbar control.
|
|
92
|
+
const syncToolbarVisibility = () => {
|
|
93
|
+
if (widget.isDisposed) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (!widget.pinned || content.hasNotebookView || content.fileDiffActive) {
|
|
97
|
+
widget.toolbar.show();
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
widget.toolbar.hide();
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
const onPinnedChanged = (_sender, value) => {
|
|
104
|
+
if (widget.isDisposed) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
if (value) {
|
|
108
|
+
pinButton.hide();
|
|
109
|
+
onPinned(widget);
|
|
110
|
+
if (widget.isDisposed) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
widget.title.className = '';
|
|
114
|
+
}
|
|
115
|
+
syncToolbarVisibility();
|
|
116
|
+
};
|
|
117
|
+
// Auto-close once a discard leaves nothing to show.
|
|
118
|
+
const onEmptied = () => {
|
|
119
|
+
if (!widget.isDisposed) {
|
|
120
|
+
widget.close();
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
content.hasNotebookViewChanged.connect(syncToolbarVisibility);
|
|
124
|
+
content.fileDiffActiveChanged.connect(syncToolbarVisibility);
|
|
125
|
+
content.emptied.connect(onEmptied);
|
|
126
|
+
widget.pinnedChanged.connect(onPinnedChanged);
|
|
127
|
+
syncToolbarVisibility();
|
|
128
|
+
widget.disposed.connect(() => {
|
|
129
|
+
content.hasNotebookViewChanged.disconnect(syncToolbarVisibility);
|
|
130
|
+
content.fileDiffActiveChanged.disconnect(syncToolbarVisibility);
|
|
131
|
+
content.emptied.disconnect(onEmptied);
|
|
132
|
+
widget.pinnedChanged.disconnect(onPinnedChanged);
|
|
133
|
+
});
|
|
134
|
+
return widget;
|
|
135
|
+
}
|
|
12
136
|
/**
|
|
13
137
|
* Register the launcher's git diff command on the application command
|
|
14
138
|
* registry.
|
|
15
139
|
*/
|
|
16
140
|
export function registerGitCommands(options) {
|
|
17
|
-
const { app, themeManager, contentsManager, rendermime,
|
|
141
|
+
const { app, themeManager, contentsManager, rendermime, trackDiff, findDiff, onPinned } = options;
|
|
18
142
|
const { commands } = app;
|
|
19
143
|
commands.addCommand(CommandIDs.openDiff, {
|
|
20
144
|
label: 'Open Git Diff',
|
|
@@ -28,18 +152,17 @@ export function registerGitCommands(options) {
|
|
|
28
152
|
const existing = findDiff(typed.change, pin);
|
|
29
153
|
if (existing !== undefined && !existing.isDisposed) {
|
|
30
154
|
if (!pin) {
|
|
31
|
-
|
|
155
|
+
existing.setChange(typed.repoPath, typed.change);
|
|
32
156
|
}
|
|
33
157
|
app.shell.activateById(existing.id);
|
|
34
158
|
return;
|
|
35
159
|
}
|
|
36
|
-
const widget =
|
|
160
|
+
const widget = createDiffWidget({
|
|
37
161
|
repoPath: typed.repoPath,
|
|
38
162
|
change: typed.change,
|
|
39
163
|
themeManager,
|
|
40
164
|
contentsManager,
|
|
41
165
|
rendermime,
|
|
42
|
-
onChanged,
|
|
43
166
|
pinned: pin,
|
|
44
167
|
onPinned
|
|
45
168
|
});
|
|
@@ -49,4 +172,3 @@ export function registerGitCommands(options) {
|
|
|
49
172
|
}
|
|
50
173
|
});
|
|
51
174
|
}
|
|
52
|
-
export { diffWidgetId };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { IXtralabDiffModel } from './diffWidget';
|
|
2
|
+
import type { GitReference, IFileChange } from './tokens';
|
|
3
|
+
/**
|
|
4
|
+
* Resolve the old/new git refs for a launcher file change. Staged diffs are
|
|
5
|
+
* INDEX vs HEAD; unstaged diffs are WORKING vs INDEX.
|
|
6
|
+
*/
|
|
7
|
+
export declare function resolveReferences(change: IFileChange): {
|
|
8
|
+
oldRef: GitReference | null;
|
|
9
|
+
newRef: GitReference | null;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Build a diff model for a single {@link IFileChange}.
|
|
13
|
+
*/
|
|
14
|
+
export declare function fileChangeToDiffModel(repoPath: string, change: IFileChange): IXtralabDiffModel;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { Git } from '@jupyterlab/git';
|
|
2
|
+
import { Signal } from '@lumino/signaling';
|
|
3
|
+
import { content } from './api';
|
|
4
|
+
/**
|
|
5
|
+
* Resolve the old/new git refs for a launcher file change. Staged diffs are
|
|
6
|
+
* INDEX vs HEAD; unstaged diffs are WORKING vs INDEX.
|
|
7
|
+
*/
|
|
8
|
+
export function resolveReferences(change) {
|
|
9
|
+
if (change.status === 'untracked') {
|
|
10
|
+
return { oldRef: null, newRef: { special: 'WORKING' } };
|
|
11
|
+
}
|
|
12
|
+
if (change.group === 'staged') {
|
|
13
|
+
return { oldRef: { git: 'HEAD' }, newRef: { special: 'INDEX' } };
|
|
14
|
+
}
|
|
15
|
+
// If INDEX has no blob, the server falls back to HEAD as the baseline.
|
|
16
|
+
return { oldRef: { special: 'INDEX' }, newRef: { special: 'WORKING' } };
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Map a launcher git ref onto `Git.Diff.IContent.source`; keep JupyterLab's
|
|
20
|
+
* enum values where downstream diff code checks for them.
|
|
21
|
+
*/
|
|
22
|
+
function referenceSource(ref) {
|
|
23
|
+
if (ref === null) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
if ('git' in ref) {
|
|
27
|
+
return ref.git;
|
|
28
|
+
}
|
|
29
|
+
if (ref.special === 'WORKING') {
|
|
30
|
+
return Git.Diff.SpecialRef.WORKING;
|
|
31
|
+
}
|
|
32
|
+
if (ref.special === 'INDEX') {
|
|
33
|
+
return Git.Diff.SpecialRef.INDEX;
|
|
34
|
+
}
|
|
35
|
+
return Git.Diff.SpecialRef.BASE;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* `Git.Diff.IModel` adapter for launcher file changes.
|
|
39
|
+
*/
|
|
40
|
+
class FileChangeDiffModel {
|
|
41
|
+
constructor(repoPath, change) {
|
|
42
|
+
var _a;
|
|
43
|
+
this.changed = new Signal(this);
|
|
44
|
+
const { oldRef, newRef } = resolveReferences(change);
|
|
45
|
+
// Renames compare the new path against the old path's previous content.
|
|
46
|
+
const oldName = (_a = change.from) !== null && _a !== void 0 ? _a : change.path;
|
|
47
|
+
this.filename = change.path;
|
|
48
|
+
this.oldFilename = change.from;
|
|
49
|
+
this.repositoryPath = repoPath;
|
|
50
|
+
this.isBinary = change.isBinary === true;
|
|
51
|
+
// Only unstaged tracked files can be reverted through a working-tree save.
|
|
52
|
+
this.canDiscard =
|
|
53
|
+
change.group === 'unstaged' && change.status !== 'untracked';
|
|
54
|
+
this.reference = {
|
|
55
|
+
label: oldName,
|
|
56
|
+
source: referenceSource(oldRef),
|
|
57
|
+
content: () => oldRef === null
|
|
58
|
+
? Promise.resolve('')
|
|
59
|
+
: content(repoPath, oldName, oldRef).then(result => { var _a; return (_a = result.content) !== null && _a !== void 0 ? _a : ''; })
|
|
60
|
+
};
|
|
61
|
+
this.challenger = {
|
|
62
|
+
label: change.path,
|
|
63
|
+
source: referenceSource(newRef),
|
|
64
|
+
content: () => newRef === null
|
|
65
|
+
? Promise.resolve('')
|
|
66
|
+
: content(repoPath, change.path, newRef).then(result => { var _a; return (_a = result.content) !== null && _a !== void 0 ? _a : ''; })
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Build a diff model for a single {@link IFileChange}.
|
|
72
|
+
*/
|
|
73
|
+
export function fileChangeToDiffModel(repoPath, change) {
|
|
74
|
+
return new FileChangeDiffModel(repoPath, change);
|
|
75
|
+
}
|
|
@@ -1,8 +1,3 @@
|
|
|
1
1
|
import { JupyterFrontEndPlugin } from '@jupyterlab/application';
|
|
2
|
-
/**
|
|
3
|
-
* Registers xtralab's diff rendering as jupyterlab-git's diff providers:
|
|
4
|
-
* `@pierre/diffs` for text and notebooks, the bundled `<img>`-based view
|
|
5
|
-
* for raster images.
|
|
6
|
-
*/
|
|
7
2
|
declare const diffProviderPlugin: JupyterFrontEndPlugin<void>;
|
|
8
3
|
export default diffProviderPlugin;
|
package/lib/git/diffProvider.js
CHANGED
|
@@ -1,388 +1,20 @@
|
|
|
1
|
-
import * as React from 'react';
|
|
2
1
|
import { IThemeManager } from '@jupyterlab/apputils';
|
|
3
|
-
import { PathExt } from '@jupyterlab/coreutils';
|
|
4
2
|
import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import { Signal } from '@lumino/signaling';
|
|
9
|
-
import { DIFF_WIDGET_CSS_CLASS, DiffStyleControl, DiffSurface, NotebookViewModeControl, isDarkTheme, readStoredDiffStyle, readStoredNotebookViewMode, writeStoredDiffStyle, writeStoredNotebookViewMode } from './diffSurface';
|
|
10
|
-
import { IMAGE_DIFF_EXTENSIONS, imageDataType } from './imageDiff';
|
|
3
|
+
import { IGitExtension } from '@jupyterlab/git';
|
|
4
|
+
import { XtralabDiffWidget, addDiffToolbarItems } from './diffWidget';
|
|
5
|
+
import { IMAGE_DIFF_EXTENSIONS } from './imageDiff';
|
|
11
6
|
/**
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* `jupyterlab-git` constructs this via the registered factory, hosts it in
|
|
15
|
-
* its own `PreviewMainAreaWidget`, and reads `model` / calls `refresh()` /
|
|
16
|
-
* `getResolvedFile()` on it. It owns the notebook view-mode state (mounted
|
|
17
|
-
* into the diff toolbar `jupyterlab-git` hands the factory) and a reload
|
|
18
|
-
* nonce so `refresh()` re-pulls the model's content.
|
|
19
|
-
*/
|
|
20
|
-
class XtralabDiffWidget extends ReactWidget {
|
|
21
|
-
constructor(model, context) {
|
|
22
|
-
super();
|
|
23
|
-
this._reloadNonce = 0;
|
|
24
|
-
this._pendingRefresh = null;
|
|
25
|
-
this._notebookViewModeChanged = new Signal(this);
|
|
26
|
-
this._hasNotebookView = false;
|
|
27
|
-
this._hasNotebookViewChanged = new Signal(this);
|
|
28
|
-
this._diffStyleChanged = new Signal(this);
|
|
29
|
-
this._fileDiffActive = false;
|
|
30
|
-
this._fileDiffActiveChanged = new Signal(this);
|
|
31
|
-
this._model = model;
|
|
32
|
-
this._context = context;
|
|
33
|
-
this._notebookViewMode = readStoredNotebookViewMode();
|
|
34
|
-
this._diffStyle = readStoredDiffStyle();
|
|
35
|
-
// `jp-git-diff-root` borrows jupyterlab-git's host sizing (height:100%
|
|
36
|
-
// flex column inside its PreviewMainAreaWidget); `jp-xtralab-DiffWidget`
|
|
37
|
-
// brings our diff theming + the `.jp-xtralab-DiffWidget-*` styles.
|
|
38
|
-
this.addClass('jp-git-diff-root');
|
|
39
|
-
this.addClass(DIFF_WIDGET_CSS_CLASS);
|
|
40
|
-
}
|
|
41
|
-
/** The diff model, as required by `Git.Diff.IDiffWidget`. */
|
|
42
|
-
get model() {
|
|
43
|
-
return this._model;
|
|
44
|
-
}
|
|
45
|
-
/**
|
|
46
|
-
* We render a two-way (reference vs. challenger) view and do not provide
|
|
47
|
-
* a merge editor, so there is nothing to leave unresolved — report
|
|
48
|
-
* resolved so jupyterlab-git's merge-conflict flow is not blocked.
|
|
49
|
-
*/
|
|
50
|
-
get isFileResolved() {
|
|
51
|
-
return true;
|
|
52
|
-
}
|
|
53
|
-
/**
|
|
54
|
-
* Used by jupyterlab-git's "Mark as resolved" button. For merge
|
|
55
|
-
* conflicts, jupyterlab-git passes the working-tree result as `base`;
|
|
56
|
-
* keep that content rather than overwriting it with either compared side.
|
|
57
|
-
*/
|
|
58
|
-
async getResolvedFile() {
|
|
59
|
-
const source = this._model.hasConflict === true && this._model.base !== undefined
|
|
60
|
-
? this._model.base
|
|
61
|
-
: this._model.challenger;
|
|
62
|
-
const content = await source.content();
|
|
63
|
-
return {
|
|
64
|
-
type: 'file',
|
|
65
|
-
format: imageDataType(this._model.filename) !== null ? 'base64' : 'text',
|
|
66
|
-
content
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
/**
|
|
70
|
-
* Re-pull both sides of the model and re-render. jupyterlab-git wires a
|
|
71
|
-
* "Refresh" toolbar button to this whenever `model.changed` fires (e.g.
|
|
72
|
-
* the compared `HEAD` moved).
|
|
73
|
-
*/
|
|
74
|
-
async refresh() {
|
|
75
|
-
const done = new PromiseDelegate();
|
|
76
|
-
this._pendingRefresh = done;
|
|
77
|
-
this._reloadNonce += 1;
|
|
78
|
-
this.update();
|
|
79
|
-
await done.promise;
|
|
80
|
-
}
|
|
81
|
-
/** The notebook rendered-vs-JSON choice; mirrored into the toolbar. */
|
|
82
|
-
get notebookViewMode() {
|
|
83
|
-
return this._notebookViewMode;
|
|
84
|
-
}
|
|
85
|
-
setNotebookViewMode(mode) {
|
|
86
|
-
if (mode === this._notebookViewMode) {
|
|
87
|
-
return;
|
|
88
|
-
}
|
|
89
|
-
this._notebookViewMode = mode;
|
|
90
|
-
writeStoredNotebookViewMode(mode);
|
|
91
|
-
this._notebookViewModeChanged.emit(mode);
|
|
92
|
-
}
|
|
93
|
-
get notebookViewModeChanged() {
|
|
94
|
-
return this._notebookViewModeChanged;
|
|
95
|
-
}
|
|
96
|
-
get hasNotebookView() {
|
|
97
|
-
return this._hasNotebookView;
|
|
98
|
-
}
|
|
99
|
-
setHasNotebookView(value) {
|
|
100
|
-
if (value === this._hasNotebookView) {
|
|
101
|
-
return;
|
|
102
|
-
}
|
|
103
|
-
this._hasNotebookView = value;
|
|
104
|
-
this._hasNotebookViewChanged.emit(value);
|
|
105
|
-
}
|
|
106
|
-
get hasNotebookViewChanged() {
|
|
107
|
-
return this._hasNotebookViewChanged;
|
|
108
|
-
}
|
|
109
|
-
/** The user's current split-vs-unified layout choice for file diffs. */
|
|
110
|
-
get diffStyle() {
|
|
111
|
-
return this._diffStyle;
|
|
112
|
-
}
|
|
113
|
-
setDiffStyle(style) {
|
|
114
|
-
if (style === this._diffStyle) {
|
|
115
|
-
return;
|
|
116
|
-
}
|
|
117
|
-
this._diffStyle = style;
|
|
118
|
-
writeStoredDiffStyle(style);
|
|
119
|
-
this._diffStyleChanged.emit(style);
|
|
120
|
-
}
|
|
121
|
-
get diffStyleChanged() {
|
|
122
|
-
return this._diffStyleChanged;
|
|
123
|
-
}
|
|
124
|
-
/**
|
|
125
|
-
* Whether the textual/code file diff (the only view the split-vs-unified
|
|
126
|
-
* choice affects) is currently active; mirrored into the toolbar so the
|
|
127
|
-
* Split/Unified selector only shows when it applies.
|
|
128
|
-
*/
|
|
129
|
-
get fileDiffActive() {
|
|
130
|
-
return this._fileDiffActive;
|
|
131
|
-
}
|
|
132
|
-
setFileDiffActive(value) {
|
|
133
|
-
if (value === this._fileDiffActive) {
|
|
134
|
-
return;
|
|
135
|
-
}
|
|
136
|
-
this._fileDiffActive = value;
|
|
137
|
-
this._fileDiffActiveChanged.emit(value);
|
|
138
|
-
}
|
|
139
|
-
get fileDiffActiveChanged() {
|
|
140
|
-
return this._fileDiffActiveChanged;
|
|
141
|
-
}
|
|
142
|
-
/** Resolve the in-flight {@link refresh} promise, if any. */
|
|
143
|
-
settleRefresh() {
|
|
144
|
-
const pending = this._pendingRefresh;
|
|
145
|
-
this._pendingRefresh = null;
|
|
146
|
-
pending === null || pending === void 0 ? void 0 : pending.resolve();
|
|
147
|
-
}
|
|
148
|
-
get reloadNonce() {
|
|
149
|
-
return this._reloadNonce;
|
|
150
|
-
}
|
|
151
|
-
get context() {
|
|
152
|
-
return this._context;
|
|
153
|
-
}
|
|
154
|
-
render() {
|
|
155
|
-
return React.createElement(ModelDiffView, { widget: this });
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
function ModelDiffView(props) {
|
|
159
|
-
const { widget } = props;
|
|
160
|
-
const model = widget.model;
|
|
161
|
-
const { contentsManager, rendermime, themeManager } = widget.context;
|
|
162
|
-
const nonce = widget.reloadNonce;
|
|
163
|
-
const [state, setState] = React.useState({
|
|
164
|
-
loading: true,
|
|
165
|
-
oldText: '',
|
|
166
|
-
newText: '',
|
|
167
|
-
error: null
|
|
168
|
-
});
|
|
169
|
-
// Mirror the widget's notebook view mode so toolbar-driven changes
|
|
170
|
-
// re-render the diff. The widget owns the canonical value (so the
|
|
171
|
-
// toolbar can read/write it without going through React).
|
|
172
|
-
const [notebookViewMode, setNotebookViewMode] = React.useState(() => widget.notebookViewMode);
|
|
173
|
-
React.useEffect(() => {
|
|
174
|
-
const handler = (_sender, mode) => {
|
|
175
|
-
setNotebookViewMode(mode);
|
|
176
|
-
};
|
|
177
|
-
widget.notebookViewModeChanged.connect(handler);
|
|
178
|
-
setNotebookViewMode(widget.notebookViewMode);
|
|
179
|
-
return () => {
|
|
180
|
-
widget.notebookViewModeChanged.disconnect(handler);
|
|
181
|
-
};
|
|
182
|
-
}, [widget]);
|
|
183
|
-
const handleNotebookAvailabilityChange = React.useCallback((available) => {
|
|
184
|
-
widget.setHasNotebookView(available);
|
|
185
|
-
}, [widget]);
|
|
186
|
-
// Mirror the widget's diff-style choice so toolbar-driven changes
|
|
187
|
-
// re-render the diff. The widget owns the canonical value.
|
|
188
|
-
const [diffStyle, setDiffStyle] = React.useState(() => widget.diffStyle);
|
|
189
|
-
React.useEffect(() => {
|
|
190
|
-
const handler = (_sender, style) => {
|
|
191
|
-
setDiffStyle(style);
|
|
192
|
-
};
|
|
193
|
-
widget.diffStyleChanged.connect(handler);
|
|
194
|
-
setDiffStyle(widget.diffStyle);
|
|
195
|
-
return () => {
|
|
196
|
-
widget.diffStyleChanged.disconnect(handler);
|
|
197
|
-
};
|
|
198
|
-
}, [widget]);
|
|
199
|
-
const handleFileDiffActiveChange = React.useCallback((active) => {
|
|
200
|
-
widget.setFileDiffActive(active);
|
|
201
|
-
}, [widget]);
|
|
202
|
-
const [dark, setDark] = React.useState(() => isDarkTheme(themeManager));
|
|
203
|
-
React.useEffect(() => {
|
|
204
|
-
if (themeManager !== null) {
|
|
205
|
-
const handler = () => setDark(isDarkTheme(themeManager));
|
|
206
|
-
themeManager.themeChanged.connect(handler);
|
|
207
|
-
return () => {
|
|
208
|
-
themeManager.themeChanged.disconnect(handler);
|
|
209
|
-
};
|
|
210
|
-
}
|
|
211
|
-
const observer = new MutationObserver(() => setDark(isDarkTheme(themeManager)));
|
|
212
|
-
observer.observe(document.body, {
|
|
213
|
-
attributes: true,
|
|
214
|
-
attributeFilter: ['data-jp-theme-light']
|
|
215
|
-
});
|
|
216
|
-
return () => {
|
|
217
|
-
observer.disconnect();
|
|
218
|
-
};
|
|
219
|
-
}, [themeManager]);
|
|
220
|
-
// Fetch both sides whenever the model or the reload nonce changes. The
|
|
221
|
-
// model's `content()` getters target the right git refs internally
|
|
222
|
-
// (working tree, index, a commit, …) so we stay content-source-agnostic.
|
|
223
|
-
React.useEffect(() => {
|
|
224
|
-
let cancelled = false;
|
|
225
|
-
setState({ loading: true, oldText: '', newText: '', error: null });
|
|
226
|
-
void (async () => {
|
|
227
|
-
try {
|
|
228
|
-
const [oldText, newText] = await Promise.all([
|
|
229
|
-
model.reference.content(),
|
|
230
|
-
model.challenger.content()
|
|
231
|
-
]);
|
|
232
|
-
if (cancelled) {
|
|
233
|
-
return;
|
|
234
|
-
}
|
|
235
|
-
setState({
|
|
236
|
-
loading: false,
|
|
237
|
-
oldText: oldText !== null && oldText !== void 0 ? oldText : '',
|
|
238
|
-
newText: newText !== null && newText !== void 0 ? newText : '',
|
|
239
|
-
error: null
|
|
240
|
-
});
|
|
241
|
-
}
|
|
242
|
-
catch (err) {
|
|
243
|
-
if (cancelled) {
|
|
244
|
-
return;
|
|
245
|
-
}
|
|
246
|
-
setState({
|
|
247
|
-
loading: false,
|
|
248
|
-
oldText: '',
|
|
249
|
-
newText: '',
|
|
250
|
-
error: err instanceof Error ? err.message : String(err)
|
|
251
|
-
});
|
|
252
|
-
}
|
|
253
|
-
finally {
|
|
254
|
-
if (!cancelled) {
|
|
255
|
-
// Let an in-flight `widget.refresh()` resolve once the reload
|
|
256
|
-
// has settled (jupyterlab-git only awaits it to hide its
|
|
257
|
-
// refresh button).
|
|
258
|
-
widget.settleRefresh();
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
})();
|
|
262
|
-
return () => {
|
|
263
|
-
cancelled = true;
|
|
264
|
-
};
|
|
265
|
-
}, [widget, model, nonce]);
|
|
266
|
-
// Per-hunk discard is only meaningful when the challenger is the working
|
|
267
|
-
// tree (an unstaged change) and we are not in a three-way merge view.
|
|
268
|
-
const canDiscardHunk = model.challenger.source === Git.Diff.SpecialRef.WORKING &&
|
|
269
|
-
model.hasConflict !== true;
|
|
270
|
-
const serverPath = React.useMemo(() => { var _a; return PathExt.join((_a = model.repositoryPath) !== null && _a !== void 0 ? _a : '', model.filename); }, [model.repositoryPath, model.filename]);
|
|
271
|
-
const hunkDiscard = React.useMemo(() => ({
|
|
272
|
-
enabled: canDiscardHunk,
|
|
273
|
-
save: async (text) => {
|
|
274
|
-
await contentsManager.save(serverPath, {
|
|
275
|
-
type: 'file',
|
|
276
|
-
format: 'text',
|
|
277
|
-
content: text
|
|
278
|
-
});
|
|
279
|
-
},
|
|
280
|
-
onAfterSave: () => {
|
|
281
|
-
// jupyterlab-git's panel auto-refreshes off the contents
|
|
282
|
-
// `fileChanged` signal the save above emits; we just need to
|
|
283
|
-
// re-pull so the diff reflects the reverted hunk. (We do not
|
|
284
|
-
// auto-close: jupyterlab-git owns the diff tab.)
|
|
285
|
-
void widget.refresh();
|
|
286
|
-
}
|
|
287
|
-
}), [canDiscardHunk, contentsManager, serverPath, widget]);
|
|
288
|
-
return (React.createElement(DiffSurface, { loading: state.loading, error: state.error, isBinary: false, oldText: state.oldText, newText: state.newText, newName: model.filename, oldName: model.filename, dark: dark, rendermime: rendermime, notebookViewMode: notebookViewMode, diffStyle: diffStyle, onNotebookAvailabilityChange: handleNotebookAvailabilityChange, onFileDiffActiveChange: handleFileDiffActiveChange, hunkDiscard: hunkDiscard }));
|
|
289
|
-
}
|
|
290
|
-
/**
|
|
291
|
-
* Notebook/JSON toggle mounted into the diff toolbar that jupyterlab-git
|
|
292
|
-
* passes to the factory. Bound to the widget's signals so it stays in sync
|
|
293
|
-
* with the rendered surface and only appears for parseable notebooks.
|
|
294
|
-
*/
|
|
295
|
-
class NotebookViewModeToolbarItem extends ReactWidget {
|
|
296
|
-
constructor(widget) {
|
|
297
|
-
super();
|
|
298
|
-
this._widget = widget;
|
|
299
|
-
this.addClass('jp-xtralab-DiffWidget-viewModeToolbarItem');
|
|
300
|
-
}
|
|
301
|
-
render() {
|
|
302
|
-
return React.createElement(ToolbarControl, { widget: this._widget });
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
function ToolbarControl(props) {
|
|
306
|
-
const { widget } = props;
|
|
307
|
-
const [mode, setMode] = React.useState(() => widget.notebookViewMode);
|
|
308
|
-
const [available, setAvailable] = React.useState(() => widget.hasNotebookView);
|
|
309
|
-
React.useEffect(() => {
|
|
310
|
-
const onMode = (_sender, next) => {
|
|
311
|
-
setMode(next);
|
|
312
|
-
};
|
|
313
|
-
const onAvailable = (_sender, next) => {
|
|
314
|
-
setAvailable(next);
|
|
315
|
-
};
|
|
316
|
-
widget.notebookViewModeChanged.connect(onMode);
|
|
317
|
-
widget.hasNotebookViewChanged.connect(onAvailable);
|
|
318
|
-
setMode(widget.notebookViewMode);
|
|
319
|
-
setAvailable(widget.hasNotebookView);
|
|
320
|
-
return () => {
|
|
321
|
-
widget.notebookViewModeChanged.disconnect(onMode);
|
|
322
|
-
widget.hasNotebookViewChanged.disconnect(onAvailable);
|
|
323
|
-
};
|
|
324
|
-
}, [widget]);
|
|
325
|
-
return (React.createElement(NotebookViewModeControl, { mode: mode, available: available, onChange: next => widget.setNotebookViewMode(next) }));
|
|
326
|
-
}
|
|
327
|
-
/**
|
|
328
|
-
* Split/Unified toggle mounted into the diff toolbar jupyterlab-git passes
|
|
329
|
-
* to the factory. Bound to the widget's signals so it stays in sync with the
|
|
330
|
-
* rendered surface and only appears while the textual/code file diff is the
|
|
331
|
-
* active view.
|
|
332
|
-
*/
|
|
333
|
-
class DiffStyleToolbarItem extends ReactWidget {
|
|
334
|
-
constructor(widget) {
|
|
335
|
-
super();
|
|
336
|
-
this._widget = widget;
|
|
337
|
-
this.addClass('jp-xtralab-DiffWidget-diffStyleToolbarItem');
|
|
338
|
-
}
|
|
339
|
-
render() {
|
|
340
|
-
return React.createElement(DiffStyleControlBound, { widget: this._widget });
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
function DiffStyleControlBound(props) {
|
|
344
|
-
const { widget } = props;
|
|
345
|
-
const [style, setStyle] = React.useState(() => widget.diffStyle);
|
|
346
|
-
const [available, setAvailable] = React.useState(() => widget.fileDiffActive);
|
|
347
|
-
React.useEffect(() => {
|
|
348
|
-
const onStyle = (_sender, next) => {
|
|
349
|
-
setStyle(next);
|
|
350
|
-
};
|
|
351
|
-
const onAvailable = (_sender, next) => {
|
|
352
|
-
setAvailable(next);
|
|
353
|
-
};
|
|
354
|
-
widget.diffStyleChanged.connect(onStyle);
|
|
355
|
-
widget.fileDiffActiveChanged.connect(onAvailable);
|
|
356
|
-
setStyle(widget.diffStyle);
|
|
357
|
-
setAvailable(widget.fileDiffActive);
|
|
358
|
-
return () => {
|
|
359
|
-
widget.diffStyleChanged.disconnect(onStyle);
|
|
360
|
-
widget.fileDiffActiveChanged.disconnect(onAvailable);
|
|
361
|
-
};
|
|
362
|
-
}, [widget]);
|
|
363
|
-
return (React.createElement(DiffStyleControl, { diffStyle: style, available: available, onChange: next => widget.setDiffStyle(next) }));
|
|
364
|
-
}
|
|
365
|
-
/**
|
|
366
|
-
* Build the `Git.Diff.Factory` xtralab registers with jupyterlab-git. The
|
|
367
|
-
* factory closes over the application-level context (contents manager for
|
|
368
|
-
* hunk discard, rendermime for notebook outputs, theme manager) that the
|
|
369
|
-
* `Git.Diff.IFactoryOptions` does not carry.
|
|
7
|
+
* Build the `Git.Diff.Factory` registered with jupyterlab-git.
|
|
370
8
|
*/
|
|
371
9
|
function makeXtralabDiffFactory(context) {
|
|
372
10
|
return async (options) => {
|
|
373
11
|
const widget = new XtralabDiffWidget(options.model, context);
|
|
374
12
|
if (options.toolbar) {
|
|
375
|
-
options.toolbar
|
|
376
|
-
options.toolbar.addItem('xtralab-diff-style', new DiffStyleToolbarItem(widget));
|
|
13
|
+
addDiffToolbarItems(options.toolbar, widget);
|
|
377
14
|
}
|
|
378
15
|
return widget;
|
|
379
16
|
};
|
|
380
17
|
}
|
|
381
|
-
/**
|
|
382
|
-
* Registers xtralab's diff rendering as jupyterlab-git's diff providers:
|
|
383
|
-
* `@pierre/diffs` for text and notebooks, the bundled `<img>`-based view
|
|
384
|
-
* for raster images.
|
|
385
|
-
*/
|
|
386
18
|
const diffProviderPlugin = {
|
|
387
19
|
id: 'xtralab:git-diff-providers',
|
|
388
20
|
description: "Replaces jupyterlab-git's notebook, text and image diff plugins with xtralab's renderer.",
|
|
@@ -395,9 +27,7 @@ const diffProviderPlugin = {
|
|
|
395
27
|
rendermime,
|
|
396
28
|
themeManager
|
|
397
29
|
});
|
|
398
|
-
//
|
|
399
|
-
// other text file falls back to the same factory. One factory serves
|
|
400
|
-
// all three — the surface auto-detects notebooks/images by filename.
|
|
30
|
+
// Use one shared factory; the surface detects notebooks/images by filename.
|
|
401
31
|
gitExtension.registerDiffProvider('XtralabNotebook', ['.ipynb'], factory);
|
|
402
32
|
gitExtension.registerDiffProvider('XtralabImage', IMAGE_DIFF_EXTENSIONS, factory);
|
|
403
33
|
gitExtension.registerFallbackDiffProvider(factory);
|