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
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import {
|
|
2
|
+
JupyterFrontEnd,
|
|
3
|
+
JupyterFrontEndPlugin
|
|
4
|
+
} from '@jupyterlab/application';
|
|
5
|
+
|
|
6
|
+
import { IDocumentWidgetOpener } from '@jupyterlab/docmanager';
|
|
7
|
+
|
|
8
|
+
import { DocumentRegistry } from '@jupyterlab/docregistry';
|
|
9
|
+
|
|
10
|
+
import { IGitExtension } from '@jupyterlab/git';
|
|
11
|
+
|
|
12
|
+
import { LabIcon } from '@jupyterlab/ui-components';
|
|
13
|
+
|
|
14
|
+
import { getSpecificTreeIcon } from '../fileBrowser/icons';
|
|
15
|
+
|
|
16
|
+
const PLUGIN_ID = 'xtralab:file-type-icons';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Synthetic path used to resolve a file type's tree glyph.
|
|
20
|
+
*/
|
|
21
|
+
function representativePath(
|
|
22
|
+
fileType: DocumentRegistry.IFileType
|
|
23
|
+
): string | null {
|
|
24
|
+
const extension = fileType.extensions?.[0];
|
|
25
|
+
if (extension === undefined || extension === '') {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
// Extensions are stored with a leading dot (e.g. `.py`, `.tar.gz`).
|
|
29
|
+
return `file${extension.startsWith('.') ? extension : `.${extension}`}`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Point a file type's icon at the tree glyph for its extension.
|
|
34
|
+
*/
|
|
35
|
+
function applyTreeIcon(fileType: DocumentRegistry.IFileType): void {
|
|
36
|
+
const path = representativePath(fileType);
|
|
37
|
+
if (path === null) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const icon = getSpecificTreeIcon(path);
|
|
41
|
+
if (icon === null) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
// The registry owns these objects; `readonly` only blocks the type shape.
|
|
45
|
+
(fileType as { icon?: LabIcon }).icon = icon;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Source extensions with tree glyphs but no default JupyterLab file type.
|
|
50
|
+
*/
|
|
51
|
+
const EXTRA_FILE_TYPE_EXTENSIONS = [
|
|
52
|
+
'.ts',
|
|
53
|
+
'.tsx',
|
|
54
|
+
'.mts',
|
|
55
|
+
'.cts',
|
|
56
|
+
'.js',
|
|
57
|
+
'.jsx',
|
|
58
|
+
'.mjs',
|
|
59
|
+
'.cjs',
|
|
60
|
+
'.go',
|
|
61
|
+
'.rs',
|
|
62
|
+
'.rb',
|
|
63
|
+
'.c',
|
|
64
|
+
'.h',
|
|
65
|
+
'.cpp',
|
|
66
|
+
'.cc',
|
|
67
|
+
'.hpp',
|
|
68
|
+
'.swift',
|
|
69
|
+
'.zig',
|
|
70
|
+
'.vue',
|
|
71
|
+
'.svelte',
|
|
72
|
+
'.astro',
|
|
73
|
+
'.scss',
|
|
74
|
+
'.sass',
|
|
75
|
+
'.sh',
|
|
76
|
+
'.bash',
|
|
77
|
+
'.zsh',
|
|
78
|
+
'.graphql',
|
|
79
|
+
'.wasm'
|
|
80
|
+
];
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Register icon-only file types for unclaimed source extensions.
|
|
84
|
+
*/
|
|
85
|
+
function registerExtraFileTypes(docRegistry: DocumentRegistry): void {
|
|
86
|
+
for (const extension of EXTRA_FILE_TYPE_EXTENSIONS) {
|
|
87
|
+
const icon = getSpecificTreeIcon(`file${extension}`);
|
|
88
|
+
if (icon === null) {
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
const claimed = docRegistry
|
|
92
|
+
.getFileTypesForPath(`file${extension}`)
|
|
93
|
+
.some(fileType =>
|
|
94
|
+
fileType.extensions?.some(e => e.toLowerCase() === extension)
|
|
95
|
+
);
|
|
96
|
+
if (claimed) {
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
docRegistry.addFileType({
|
|
100
|
+
name: `xtralab:file-type:${extension.slice(1)}`,
|
|
101
|
+
extensions: [extension],
|
|
102
|
+
icon,
|
|
103
|
+
contentType: 'file',
|
|
104
|
+
fileFormat: 'text'
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Share the file browser's tree glyphs with tabs and git file lists.
|
|
111
|
+
*/
|
|
112
|
+
const plugin: JupyterFrontEndPlugin<void> = {
|
|
113
|
+
id: PLUGIN_ID,
|
|
114
|
+
description:
|
|
115
|
+
"Gives the file browser's @pierre/trees glyphs to document tabs, the default file browser, and the git status panel.",
|
|
116
|
+
autoStart: true,
|
|
117
|
+
optional: [IDocumentWidgetOpener, IGitExtension],
|
|
118
|
+
activate: (
|
|
119
|
+
app: JupyterFrontEnd,
|
|
120
|
+
opener: IDocumentWidgetOpener | null,
|
|
121
|
+
gitExtension: IGitExtension | null
|
|
122
|
+
): void => {
|
|
123
|
+
const { docRegistry } = app;
|
|
124
|
+
for (const fileType of docRegistry.fileTypes()) {
|
|
125
|
+
applyTreeIcon(fileType);
|
|
126
|
+
}
|
|
127
|
+
registerExtraFileTypes(docRegistry);
|
|
128
|
+
// Pick up file types registered after this plugin activates.
|
|
129
|
+
docRegistry.changed.connect((_, args) => {
|
|
130
|
+
if (
|
|
131
|
+
args.type === 'fileType' &&
|
|
132
|
+
args.change === 'added' &&
|
|
133
|
+
args.name !== undefined
|
|
134
|
+
) {
|
|
135
|
+
const fileType = docRegistry.getFileType(args.name);
|
|
136
|
+
if (fileType !== undefined) {
|
|
137
|
+
applyTreeIcon(fileType);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
if (opener !== null) {
|
|
143
|
+
opener.opened.connect((_, widget) => {
|
|
144
|
+
const setIcon = (): void => {
|
|
145
|
+
const icon = getSpecificTreeIcon(widget.context.path);
|
|
146
|
+
if (icon !== null) {
|
|
147
|
+
widget.title.icon = icon;
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
setIcon();
|
|
151
|
+
// Keep the tab icon right after a rename.
|
|
152
|
+
widget.context.pathChanged.connect(setIcon);
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (gitExtension !== null) {
|
|
157
|
+
gitExtension.statusChanged.connect((_, status) => {
|
|
158
|
+
for (const file of status.files) {
|
|
159
|
+
if (file.type === undefined) {
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
const icon = getSpecificTreeIcon(file.to);
|
|
163
|
+
if (icon === null) {
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
// Cross jupyterlab-git's nested `LabIcon` package boundary.
|
|
167
|
+
file.type = { ...file.type, icon } as unknown as typeof file.type;
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
export default plugin;
|
package/src/git/commands.ts
CHANGED
|
@@ -1,31 +1,29 @@
|
|
|
1
1
|
import { JupyterFrontEnd } from '@jupyterlab/application';
|
|
2
|
-
import { IThemeManager } from '@jupyterlab/apputils';
|
|
2
|
+
import { IThemeManager, MainAreaWidget } from '@jupyterlab/apputils';
|
|
3
3
|
import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
|
|
4
4
|
import { Contents } from '@jupyterlab/services';
|
|
5
|
+
import { ToolbarButton, launchIcon } from '@jupyterlab/ui-components';
|
|
5
6
|
import { ReadonlyPartialJSONObject } from '@lumino/coreutils';
|
|
7
|
+
import { ISignal, Signal } from '@lumino/signaling';
|
|
6
8
|
|
|
9
|
+
import { getTreeIcon } from '../fileBrowser/icons';
|
|
10
|
+
import { fileChangeToDiffModel } from './diffModel';
|
|
7
11
|
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
12
|
+
DIFF_WIDGET_CSS_CLASS,
|
|
13
|
+
XtralabDiffWidget,
|
|
14
|
+
addDiffToolbarItems
|
|
11
15
|
} from './diffWidget';
|
|
12
16
|
import { IFileChange } from './tokens';
|
|
13
17
|
|
|
14
18
|
/**
|
|
15
|
-
* Command IDs exposed by the launcher's git diff path.
|
|
16
|
-
* dashboard's "Changes" section drives {@link CommandIDs.openDiff} to open
|
|
17
|
-
* its own diff tab, independent of `jupyterlab-git` (whose panel reaches
|
|
18
|
-
* the same rendering via the providers in `diffProvider.tsx`).
|
|
19
|
+
* Command IDs exposed by the launcher's git diff path.
|
|
19
20
|
*/
|
|
20
21
|
export namespace CommandIDs {
|
|
21
22
|
export const openDiff = 'xtralab:git:open-diff';
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
/**
|
|
25
|
-
* Argument shapes used by {@link CommandIDs.openDiff}.
|
|
26
|
-
* inside the execute callback for type safety; the registry itself takes
|
|
27
|
-
* `ReadonlyPartialJSONObject` because Lumino does not know about our
|
|
28
|
-
* extension-specific shapes.
|
|
26
|
+
* Argument shapes used by {@link CommandIDs.openDiff}.
|
|
29
27
|
*/
|
|
30
28
|
export namespace CommandArguments {
|
|
31
29
|
export interface IOpenDiff {
|
|
@@ -35,44 +33,184 @@ export namespace CommandArguments {
|
|
|
35
33
|
}
|
|
36
34
|
}
|
|
37
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Widget id of the single, reused preview diff tab.
|
|
38
|
+
*/
|
|
39
|
+
export const PREVIEW_DIFF_WIDGET_ID = 'xtralab:diff:preview';
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Deterministic widget id for a pinned file/group pair.
|
|
43
|
+
*/
|
|
44
|
+
export function pinnedDiffWidgetId(change: IFileChange): string {
|
|
45
|
+
return `xtralab:diff:pinned:${change.group}:${change.path}`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function formatTitle(change: IFileChange): string {
|
|
49
|
+
const name = change.path.split('/').pop() ?? change.path;
|
|
50
|
+
const groupLabel = change.group === 'staged' ? 'Staged' : 'Working';
|
|
51
|
+
return `${name} (${groupLabel})`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Launcher-owned host for a shared diff widget.
|
|
56
|
+
*/
|
|
57
|
+
export class DiffMainAreaWidget extends MainAreaWidget<XtralabDiffWidget> {
|
|
58
|
+
constructor(
|
|
59
|
+
options: MainAreaWidget.IOptions<XtralabDiffWidget>,
|
|
60
|
+
change: IFileChange,
|
|
61
|
+
pinned: boolean
|
|
62
|
+
) {
|
|
63
|
+
super(options);
|
|
64
|
+
this._change = change;
|
|
65
|
+
this._pinned = pinned;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
get change(): IFileChange {
|
|
69
|
+
return this._change;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
get pinned(): boolean {
|
|
73
|
+
return this._pinned;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
pin(): void {
|
|
77
|
+
if (this._pinned) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
this._pinned = true;
|
|
81
|
+
this._pinnedChanged.emit(true);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
get pinnedChanged(): ISignal<this, boolean> {
|
|
85
|
+
return this._pinnedChanged;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
setChange(repoPath: string, change: IFileChange): void {
|
|
89
|
+
this._change = change;
|
|
90
|
+
this.content.setModel(fileChangeToDiffModel(repoPath, change));
|
|
91
|
+
this.title.label = formatTitle(change);
|
|
92
|
+
this.title.caption = change.path;
|
|
93
|
+
this.title.icon = getTreeIcon(change.path);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
private _change: IFileChange;
|
|
97
|
+
private _pinned: boolean;
|
|
98
|
+
private _pinnedChanged = new Signal<this, boolean>(this);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
interface ICreateDiffWidgetOptions {
|
|
102
|
+
repoPath: string;
|
|
103
|
+
change: IFileChange;
|
|
104
|
+
themeManager: IThemeManager | null;
|
|
105
|
+
contentsManager: Contents.IManager;
|
|
106
|
+
rendermime: IRenderMimeRegistry | null;
|
|
107
|
+
pinned?: boolean;
|
|
108
|
+
onPinned?: (widget: DiffMainAreaWidget) => void;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Build the launcher's diff tab around the shared diff widget.
|
|
113
|
+
*/
|
|
114
|
+
function createDiffWidget(
|
|
115
|
+
options: ICreateDiffWidgetOptions
|
|
116
|
+
): DiffMainAreaWidget {
|
|
117
|
+
const { repoPath, change, themeManager, contentsManager, rendermime } =
|
|
118
|
+
options;
|
|
119
|
+
const pinned = options.pinned === true;
|
|
120
|
+
const content = new XtralabDiffWidget(
|
|
121
|
+
fileChangeToDiffModel(repoPath, change),
|
|
122
|
+
{ contentsManager, rendermime, themeManager }
|
|
123
|
+
);
|
|
124
|
+
const widget = new DiffMainAreaWidget({ content }, change, pinned);
|
|
125
|
+
widget.id = pinned ? pinnedDiffWidgetId(change) : PREVIEW_DIFF_WIDGET_ID;
|
|
126
|
+
widget.title.label = formatTitle(change);
|
|
127
|
+
widget.title.caption = change.path;
|
|
128
|
+
widget.title.closable = true;
|
|
129
|
+
widget.title.icon = getTreeIcon(change.path);
|
|
130
|
+
widget.title.className = pinned ? '' : 'jp-mod-preview';
|
|
131
|
+
widget.addClass(DIFF_WIDGET_CSS_CLASS);
|
|
132
|
+
|
|
133
|
+
const onPinned = options.onPinned ?? ((): void => undefined);
|
|
134
|
+
|
|
135
|
+
const pinButton = new ToolbarButton({
|
|
136
|
+
icon: launchIcon,
|
|
137
|
+
tooltip: 'Pin tab',
|
|
138
|
+
onClick: () => widget.pin()
|
|
139
|
+
});
|
|
140
|
+
if (widget.pinned) {
|
|
141
|
+
pinButton.hide();
|
|
142
|
+
}
|
|
143
|
+
widget.toolbar.addItem('pin', pinButton);
|
|
144
|
+
addDiffToolbarItems(widget.toolbar, content);
|
|
145
|
+
|
|
146
|
+
// Keep chrome visible only while there is a relevant toolbar control.
|
|
147
|
+
const syncToolbarVisibility = (): void => {
|
|
148
|
+
if (widget.isDisposed) {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
if (!widget.pinned || content.hasNotebookView || content.fileDiffActive) {
|
|
152
|
+
widget.toolbar.show();
|
|
153
|
+
} else {
|
|
154
|
+
widget.toolbar.hide();
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
const onPinnedChanged = (
|
|
158
|
+
_sender: DiffMainAreaWidget,
|
|
159
|
+
value: boolean
|
|
160
|
+
): void => {
|
|
161
|
+
if (widget.isDisposed) {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
if (value) {
|
|
165
|
+
pinButton.hide();
|
|
166
|
+
onPinned(widget);
|
|
167
|
+
if (widget.isDisposed) {
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
widget.title.className = '';
|
|
171
|
+
}
|
|
172
|
+
syncToolbarVisibility();
|
|
173
|
+
};
|
|
174
|
+
// Auto-close once a discard leaves nothing to show.
|
|
175
|
+
const onEmptied = (): void => {
|
|
176
|
+
if (!widget.isDisposed) {
|
|
177
|
+
widget.close();
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
content.hasNotebookViewChanged.connect(syncToolbarVisibility);
|
|
181
|
+
content.fileDiffActiveChanged.connect(syncToolbarVisibility);
|
|
182
|
+
content.emptied.connect(onEmptied);
|
|
183
|
+
widget.pinnedChanged.connect(onPinnedChanged);
|
|
184
|
+
syncToolbarVisibility();
|
|
185
|
+
widget.disposed.connect(() => {
|
|
186
|
+
content.hasNotebookViewChanged.disconnect(syncToolbarVisibility);
|
|
187
|
+
content.fileDiffActiveChanged.disconnect(syncToolbarVisibility);
|
|
188
|
+
content.emptied.disconnect(onEmptied);
|
|
189
|
+
widget.pinnedChanged.disconnect(onPinnedChanged);
|
|
190
|
+
});
|
|
191
|
+
return widget;
|
|
192
|
+
}
|
|
193
|
+
|
|
38
194
|
export interface IRegisterGitCommandsOptions {
|
|
39
195
|
app: JupyterFrontEnd;
|
|
40
196
|
themeManager: IThemeManager | null;
|
|
41
197
|
/**
|
|
42
|
-
* Contents manager used
|
|
43
|
-
* results back to the working tree.
|
|
198
|
+
* Contents manager used to write hunk-discard results.
|
|
44
199
|
*/
|
|
45
200
|
contentsManager: Contents.IManager;
|
|
46
201
|
/**
|
|
47
|
-
* Rendermime registry used by
|
|
48
|
-
* markdown cells with their actual mime-type renderers (images as
|
|
49
|
-
* images, HTML as HTML, etc.). May be `null` in stripped-down hosts —
|
|
50
|
-
* the notebook diff falls back to a text representation in that case.
|
|
202
|
+
* Rendermime registry used by notebook diffs; may be `null`.
|
|
51
203
|
*/
|
|
52
204
|
rendermime: IRenderMimeRegistry | null;
|
|
53
205
|
/**
|
|
54
|
-
*
|
|
55
|
-
* a hunk discard). The launcher dashboard polls git status on its own
|
|
56
|
-
* cadence and `jupyterlab-git`'s panel auto-refreshes on the contents
|
|
57
|
-
* `fileChanged` signal the save emits, so this is a hook for any extra
|
|
58
|
-
* bookkeeping rather than a required refresh.
|
|
59
|
-
*/
|
|
60
|
-
onChanged: () => void;
|
|
61
|
-
/**
|
|
62
|
-
* Called for every diff widget created via the {@link CommandIDs.openDiff}
|
|
63
|
-
* command, before the widget is added to the shell. Lets the plugin track
|
|
64
|
-
* the widget for layout restoration and reveal-on-reuse.
|
|
206
|
+
* Track newly created diff widgets before they are added to the shell.
|
|
65
207
|
*/
|
|
66
|
-
trackDiff(widget:
|
|
208
|
+
trackDiff(widget: DiffMainAreaWidget): Promise<void>;
|
|
67
209
|
/**
|
|
68
|
-
* Look up an already-open diff widget for a
|
|
69
|
-
* `undefined` when none is open.
|
|
210
|
+
* Look up an already-open diff widget for a file change.
|
|
70
211
|
*/
|
|
71
|
-
findDiff(
|
|
72
|
-
|
|
73
|
-
pin?: boolean
|
|
74
|
-
): ReturnType<typeof createGitDiffWidget> | undefined;
|
|
75
|
-
onPinned(widget: ReturnType<typeof createGitDiffWidget>): void;
|
|
212
|
+
findDiff(change: IFileChange, pin?: boolean): DiffMainAreaWidget | undefined;
|
|
213
|
+
onPinned(widget: DiffMainAreaWidget): void;
|
|
76
214
|
}
|
|
77
215
|
|
|
78
216
|
/**
|
|
@@ -87,7 +225,6 @@ export function registerGitCommands(
|
|
|
87
225
|
themeManager,
|
|
88
226
|
contentsManager,
|
|
89
227
|
rendermime,
|
|
90
|
-
onChanged,
|
|
91
228
|
trackDiff,
|
|
92
229
|
findDiff,
|
|
93
230
|
onPinned
|
|
@@ -106,18 +243,17 @@ export function registerGitCommands(
|
|
|
106
243
|
const existing = findDiff(typed.change, pin);
|
|
107
244
|
if (existing !== undefined && !existing.isDisposed) {
|
|
108
245
|
if (!pin) {
|
|
109
|
-
|
|
246
|
+
existing.setChange(typed.repoPath, typed.change);
|
|
110
247
|
}
|
|
111
248
|
app.shell.activateById(existing.id);
|
|
112
249
|
return;
|
|
113
250
|
}
|
|
114
|
-
const widget =
|
|
251
|
+
const widget = createDiffWidget({
|
|
115
252
|
repoPath: typed.repoPath,
|
|
116
253
|
change: typed.change,
|
|
117
254
|
themeManager,
|
|
118
255
|
contentsManager,
|
|
119
256
|
rendermime,
|
|
120
|
-
onChanged,
|
|
121
257
|
pinned: pin,
|
|
122
258
|
onPinned
|
|
123
259
|
});
|
|
@@ -127,5 +263,3 @@ export function registerGitCommands(
|
|
|
127
263
|
}
|
|
128
264
|
});
|
|
129
265
|
}
|
|
130
|
-
|
|
131
|
-
export { diffWidgetId };
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { Git } from '@jupyterlab/git';
|
|
2
|
+
import { ISignal, Signal } from '@lumino/signaling';
|
|
3
|
+
|
|
4
|
+
import { content } from './api';
|
|
5
|
+
import type { IXtralabDiffModel } from './diffWidget';
|
|
6
|
+
import type { GitReference, IFileChange } from './tokens';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Resolve the old/new git refs for a launcher file change. Staged diffs are
|
|
10
|
+
* INDEX vs HEAD; unstaged diffs are WORKING vs INDEX.
|
|
11
|
+
*/
|
|
12
|
+
export function resolveReferences(change: IFileChange): {
|
|
13
|
+
oldRef: GitReference | null;
|
|
14
|
+
newRef: GitReference | null;
|
|
15
|
+
} {
|
|
16
|
+
if (change.status === 'untracked') {
|
|
17
|
+
return { oldRef: null, newRef: { special: 'WORKING' } };
|
|
18
|
+
}
|
|
19
|
+
if (change.group === 'staged') {
|
|
20
|
+
return { oldRef: { git: 'HEAD' }, newRef: { special: 'INDEX' } };
|
|
21
|
+
}
|
|
22
|
+
// If INDEX has no blob, the server falls back to HEAD as the baseline.
|
|
23
|
+
return { oldRef: { special: 'INDEX' }, newRef: { special: 'WORKING' } };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Map a launcher git ref onto `Git.Diff.IContent.source`; keep JupyterLab's
|
|
28
|
+
* enum values where downstream diff code checks for them.
|
|
29
|
+
*/
|
|
30
|
+
function referenceSource(ref: GitReference | null): unknown {
|
|
31
|
+
if (ref === null) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
if ('git' in ref) {
|
|
35
|
+
return ref.git;
|
|
36
|
+
}
|
|
37
|
+
if (ref.special === 'WORKING') {
|
|
38
|
+
return Git.Diff.SpecialRef.WORKING;
|
|
39
|
+
}
|
|
40
|
+
if (ref.special === 'INDEX') {
|
|
41
|
+
return Git.Diff.SpecialRef.INDEX;
|
|
42
|
+
}
|
|
43
|
+
return Git.Diff.SpecialRef.BASE;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* `Git.Diff.IModel` adapter for launcher file changes.
|
|
48
|
+
*/
|
|
49
|
+
class FileChangeDiffModel implements IXtralabDiffModel {
|
|
50
|
+
constructor(repoPath: string, change: IFileChange) {
|
|
51
|
+
const { oldRef, newRef } = resolveReferences(change);
|
|
52
|
+
// Renames compare the new path against the old path's previous content.
|
|
53
|
+
const oldName = change.from ?? change.path;
|
|
54
|
+
this.filename = change.path;
|
|
55
|
+
this.oldFilename = change.from;
|
|
56
|
+
this.repositoryPath = repoPath;
|
|
57
|
+
this.isBinary = change.isBinary === true;
|
|
58
|
+
// Only unstaged tracked files can be reverted through a working-tree save.
|
|
59
|
+
this.canDiscard =
|
|
60
|
+
change.group === 'unstaged' && change.status !== 'untracked';
|
|
61
|
+
this.reference = {
|
|
62
|
+
label: oldName,
|
|
63
|
+
source: referenceSource(oldRef),
|
|
64
|
+
content: () =>
|
|
65
|
+
oldRef === null
|
|
66
|
+
? Promise.resolve('')
|
|
67
|
+
: content(repoPath, oldName, oldRef).then(
|
|
68
|
+
result => result.content ?? ''
|
|
69
|
+
)
|
|
70
|
+
};
|
|
71
|
+
this.challenger = {
|
|
72
|
+
label: change.path,
|
|
73
|
+
source: referenceSource(newRef),
|
|
74
|
+
content: () =>
|
|
75
|
+
newRef === null
|
|
76
|
+
? Promise.resolve('')
|
|
77
|
+
: content(repoPath, change.path, newRef).then(
|
|
78
|
+
result => result.content ?? ''
|
|
79
|
+
)
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
readonly changed: ISignal<Git.Diff.IModel, Git.Diff.IModelChange> =
|
|
84
|
+
new Signal<Git.Diff.IModel, Git.Diff.IModelChange>(this);
|
|
85
|
+
reference: Git.Diff.IContent;
|
|
86
|
+
challenger: Git.Diff.IContent;
|
|
87
|
+
readonly filename: string;
|
|
88
|
+
readonly oldFilename: string | undefined;
|
|
89
|
+
readonly repositoryPath: string;
|
|
90
|
+
readonly isBinary: boolean;
|
|
91
|
+
readonly canDiscard: boolean;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Build a diff model for a single {@link IFileChange}.
|
|
96
|
+
*/
|
|
97
|
+
export function fileChangeToDiffModel(
|
|
98
|
+
repoPath: string,
|
|
99
|
+
change: IFileChange
|
|
100
|
+
): IXtralabDiffModel {
|
|
101
|
+
return new FileChangeDiffModel(repoPath, change);
|
|
102
|
+
}
|