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/src/fileBrowser/icons.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { LabIcon, fileIcon } from '@jupyterlab/ui-components';
|
|
2
2
|
import {
|
|
3
3
|
createFileTreeIconResolver,
|
|
4
|
-
getBuiltInFileIconColor,
|
|
5
4
|
getBuiltInSpriteSheet
|
|
6
5
|
} from '@pierre/trees';
|
|
7
6
|
import type { FileTreeIconConfig } from '@pierre/trees';
|
|
@@ -121,28 +120,104 @@ function extractSymbol(
|
|
|
121
120
|
}
|
|
122
121
|
|
|
123
122
|
/**
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
* viewer) that want their main-area widget tabs to display the same per-file
|
|
127
|
-
* glyph the tree shows in the sidebar.
|
|
123
|
+
* Fallback colors for the built-in file icons, keyed by the icon token
|
|
124
|
+
* `@pierre/trees` resolves for a path (e.g. `python`, `typescript`).
|
|
128
125
|
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
126
|
+
* The tree only exposes these as `--trees-file-icon-color-<token>` CSS
|
|
127
|
+
* variables scoped to its shadow host, so they resolve to nothing for the
|
|
128
|
+
* standalone {@link LabIcon}s {@link getTreeIcon} builds for main-area tabs.
|
|
129
|
+
* Mirroring them here lets {@link getBuiltInFileIconColor} bottom out in a
|
|
130
|
+
* real color. Each is a `light-dark(…)` pair for dark themes.
|
|
133
131
|
*/
|
|
134
|
-
|
|
132
|
+
const BUILT_IN_FILE_ICON_COLOR_FALLBACKS: Record<string, string> = {
|
|
133
|
+
astro: 'light-dark(#a631be, #d568ea)',
|
|
134
|
+
babel: 'light-dark(#d5a910, #ffd452)',
|
|
135
|
+
bash: 'light-dark(#199f43, #5ecc71)',
|
|
136
|
+
biome: 'light-dark(#1a85d4, #69b1ff)',
|
|
137
|
+
bootstrap: 'light-dark(#693acf, #9d6afb)',
|
|
138
|
+
browserslist: 'light-dark(#d5a910, #ffd452)',
|
|
139
|
+
bun: 'light-dark(#594c5b, #79697b)',
|
|
140
|
+
c: 'light-dark(#1a85d4, #69b1ff)',
|
|
141
|
+
claude: 'light-dark(#d47628, #ffa359)',
|
|
142
|
+
cpp: 'light-dark(#1a85d4, #69b1ff)',
|
|
143
|
+
css: 'light-dark(#693acf, #9d6afb)',
|
|
144
|
+
database: 'light-dark(#a631be, #d568ea)',
|
|
145
|
+
default: 'light-dark(#84848a, #adadb1)',
|
|
146
|
+
docker: 'light-dark(#1a85d4, #69b1ff)',
|
|
147
|
+
eslint: 'light-dark(#693acf, #9d6afb)',
|
|
148
|
+
git: 'light-dark(#ff8c5b, #d5512f)',
|
|
149
|
+
go: 'light-dark(#1ca1c7, #68cdf2)',
|
|
150
|
+
graphql: 'light-dark(#d32a61, #ff678d)',
|
|
151
|
+
html: 'light-dark(#d47628, #ffa359)',
|
|
152
|
+
image: 'light-dark(#d32a61, #ff678d)',
|
|
153
|
+
javascript: 'light-dark(#d5a910, #ffd452)',
|
|
154
|
+
json: 'light-dark(#d47628, #ffa359)',
|
|
155
|
+
markdown: 'light-dark(#199f43, #5ecc71)',
|
|
156
|
+
mcp: 'light-dark(#17a5af, #64d1db)',
|
|
157
|
+
npm: 'light-dark(#d52c36, #ff6762)',
|
|
158
|
+
oxc: 'light-dark(#1ca1c7, #68cdf2)',
|
|
159
|
+
postcss: 'light-dark(#d52c36, #ff6762)',
|
|
160
|
+
prettier: 'light-dark(#17a5af, #64d1db)',
|
|
161
|
+
python: 'light-dark(#1a85d4, #69b1ff)',
|
|
162
|
+
react: 'light-dark(#1ca1c7, #68cdf2)',
|
|
163
|
+
ruby: 'light-dark(#d52c36, #ff6762)',
|
|
164
|
+
rust: 'light-dark(#d47628, #ffa359)',
|
|
165
|
+
sass: 'light-dark(#d32a61, #ff678d)',
|
|
166
|
+
svelte: 'light-dark(#d52c36, #ff6762)',
|
|
167
|
+
svg: 'light-dark(#d47628, #ffa359)',
|
|
168
|
+
svgo: 'light-dark(#199f43, #5ecc71)',
|
|
169
|
+
swift: 'light-dark(#d47628, #ffa359)',
|
|
170
|
+
table: 'light-dark(#17a5af, #64d1db)',
|
|
171
|
+
tailwind: 'light-dark(#1ca1c7, #68cdf2)',
|
|
172
|
+
terraform: 'light-dark(#693acf, #9d6afb)',
|
|
173
|
+
text: 'light-dark(#84848a, #adadb1)',
|
|
174
|
+
typescript: 'light-dark(#1a85d4, #69b1ff)',
|
|
175
|
+
vite: 'light-dark(#a631be, #d568ea)',
|
|
176
|
+
vscode: 'light-dark(#1a85d4, #69b1ff)',
|
|
177
|
+
vue: 'light-dark(#199f43, #5ecc71)',
|
|
178
|
+
wasm: 'light-dark(#693acf, #9d6afb)',
|
|
179
|
+
webpack: 'light-dark(#1a85d4, #69b1ff)',
|
|
180
|
+
yml: 'light-dark(#d52c36, #ff6762)',
|
|
181
|
+
zig: 'light-dark(#d47628, #ffa359)',
|
|
182
|
+
zip: 'light-dark(#d47628, #ffa359)'
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Color the file tree would paint for the built-in icon `token`, as a
|
|
187
|
+
* self-contained CSS value usable outside the tree's shadow DOM: the tree's
|
|
188
|
+
* `--trees-file-icon-color-*` properties when in scope, else the literal from
|
|
189
|
+
* {@link BUILT_IN_FILE_ICON_COLOR_FALLBACKS}. Returns `undefined` for unknown
|
|
190
|
+
* tokens.
|
|
191
|
+
*/
|
|
192
|
+
function getBuiltInFileIconColor(token: string): string | undefined {
|
|
193
|
+
const fallback = BUILT_IN_FILE_ICON_COLOR_FALLBACKS[token];
|
|
194
|
+
if (fallback === undefined) {
|
|
195
|
+
return undefined;
|
|
196
|
+
}
|
|
197
|
+
return `var(--trees-file-icon-color-${token}, var(--trees-file-icon-color, ${fallback}))`;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Resolve a tree glyph into a standalone `LabIcon`.
|
|
202
|
+
*/
|
|
203
|
+
function resolveTreeIcon(filePath: string): {
|
|
204
|
+
icon: LabIcon | null;
|
|
205
|
+
specific: boolean;
|
|
206
|
+
} {
|
|
135
207
|
const resolved = treeIconResolver.resolveIcon(
|
|
136
208
|
'file-tree-icon-file',
|
|
137
209
|
filePath
|
|
138
210
|
);
|
|
211
|
+
// Anything other than the catch-all glyph is specific to this file.
|
|
212
|
+
const specific =
|
|
213
|
+
resolved.token !== 'default' && resolved.name !== 'file-tree-icon-file';
|
|
139
214
|
const cached = TREE_ICON_CACHE.get(resolved.name);
|
|
140
215
|
if (cached !== undefined) {
|
|
141
|
-
return cached;
|
|
216
|
+
return { icon: cached, specific };
|
|
142
217
|
}
|
|
143
218
|
const symbol = extractSymbol(resolved.name);
|
|
144
219
|
if (symbol === null) {
|
|
145
|
-
return
|
|
220
|
+
return { icon: null, specific };
|
|
146
221
|
}
|
|
147
222
|
// Built-in language icons paint with `fill="currentColor"` so the colored
|
|
148
223
|
// tier of the tree can recolor them by setting `color` on the host
|
|
@@ -168,5 +243,20 @@ export function getTreeIcon(filePath: string): LabIcon {
|
|
|
168
243
|
svgstr
|
|
169
244
|
});
|
|
170
245
|
TREE_ICON_CACHE.set(resolved.name, icon);
|
|
171
|
-
return icon;
|
|
246
|
+
return { icon, specific };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Return the icon the xtralab file tree would render for `filePath`.
|
|
251
|
+
*/
|
|
252
|
+
export function getTreeIcon(filePath: string): LabIcon {
|
|
253
|
+
return resolveTreeIcon(filePath).icon ?? fileIcon;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Like {@link getTreeIcon}, but returns `null` for the generic file glyph.
|
|
258
|
+
*/
|
|
259
|
+
export function getSpecificTreeIcon(filePath: string): LabIcon | null {
|
|
260
|
+
const { icon, specific } = resolveTreeIcon(filePath);
|
|
261
|
+
return specific ? icon : null;
|
|
172
262
|
}
|
package/src/fileBrowser/index.ts
CHANGED
|
@@ -4,10 +4,14 @@ import {
|
|
|
4
4
|
JupyterFrontEndPlugin
|
|
5
5
|
} from '@jupyterlab/application';
|
|
6
6
|
|
|
7
|
+
import { ICommandPalette } from '@jupyterlab/apputils';
|
|
8
|
+
|
|
7
9
|
import { IDocumentManager } from '@jupyterlab/docmanager';
|
|
8
10
|
|
|
9
11
|
import { ISettingRegistry } from '@jupyterlab/settingregistry';
|
|
10
12
|
|
|
13
|
+
import { ITranslator } from '@jupyterlab/translation';
|
|
14
|
+
|
|
11
15
|
import { populateToolbar, registerCommands } from './commands';
|
|
12
16
|
import { XtralabFileBrowser } from './widget';
|
|
13
17
|
|
|
@@ -23,12 +27,14 @@ const plugin: JupyterFrontEndPlugin<void> = {
|
|
|
23
27
|
'A path-first file browser for JupyterLab built on @pierre/trees.',
|
|
24
28
|
autoStart: true,
|
|
25
29
|
requires: [IDocumentManager],
|
|
26
|
-
optional: [ILayoutRestorer, ISettingRegistry],
|
|
30
|
+
optional: [ILayoutRestorer, ISettingRegistry, ICommandPalette, ITranslator],
|
|
27
31
|
activate: (
|
|
28
32
|
app: JupyterFrontEnd,
|
|
29
33
|
docManager: IDocumentManager,
|
|
30
34
|
restorer: ILayoutRestorer | null,
|
|
31
|
-
settingRegistry: ISettingRegistry | null
|
|
35
|
+
settingRegistry: ISettingRegistry | null,
|
|
36
|
+
palette: ICommandPalette | null,
|
|
37
|
+
translator: ITranslator | null
|
|
32
38
|
): void => {
|
|
33
39
|
const browser = new XtralabFileBrowser({
|
|
34
40
|
contentsManager: app.serviceManager.contents,
|
|
@@ -46,7 +52,7 @@ const plugin: JupyterFrontEndPlugin<void> = {
|
|
|
46
52
|
restorer.add(browser, browser.id);
|
|
47
53
|
}
|
|
48
54
|
|
|
49
|
-
registerCommands({ app, browser });
|
|
55
|
+
registerCommands({ app, browser, docManager, palette, translator });
|
|
50
56
|
populateToolbar({ app, browser });
|
|
51
57
|
|
|
52
58
|
if (settingRegistry) {
|
|
@@ -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 };
|