xtralab 0.15.0 → 0.15.2
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 +65 -30
- package/lib/about/icons.d.ts +0 -5
- package/lib/about/icons.js +0 -5
- package/lib/about/index.d.ts +4 -8
- package/lib/about/index.js +6 -17
- package/lib/agentSessions.d.ts +9 -29
- package/lib/agentSessions.js +2 -7
- package/lib/askAgent/editorSelection.d.ts +20 -26
- package/lib/askAgent/editorSelection.js +11 -26
- package/lib/askAgent/icons.d.ts +2 -3
- package/lib/askAgent/icons.js +2 -3
- package/lib/askAgent/index.d.ts +4 -26
- package/lib/askAgent/index.js +54 -158
- package/lib/askAgent/popup.d.ts +12 -16
- package/lib/askAgent/popup.js +18 -37
- package/lib/askAgent/prompt.d.ts +3 -5
- package/lib/askAgent/prompt.js +6 -19
- package/lib/askAgent/queue.d.ts +11 -21
- package/lib/askAgent/queue.js +9 -19
- package/lib/askAgent/queuePanel.d.ts +13 -18
- package/lib/askAgent/queuePanel.js +15 -37
- package/lib/askAgent/targetPicker.d.ts +2 -9
- package/lib/askAgent/targetPicker.js +6 -11
- package/lib/askAgent/tokens.d.ts +32 -46
- package/lib/askAgent/tokens.js +4 -6
- package/lib/commandBar/index.d.ts +4 -13
- package/lib/commandBar/index.js +21 -39
- package/lib/customPanel/index.js +15 -0
- package/lib/editorBreadcrumbs/index.d.ts +3 -6
- package/lib/editorBreadcrumbs/index.js +10 -6
- package/lib/editorBreadcrumbs/widget.d.ts +17 -4
- package/lib/editorBreadcrumbs/widget.js +11 -17
- package/lib/editorIndent/index.d.ts +4 -12
- package/lib/editorIndent/index.js +8 -23
- package/lib/fileBrowser/commands.d.ts +29 -19
- package/lib/fileBrowser/commands.js +83 -97
- package/lib/fileBrowser/contents.d.ts +3 -0
- package/lib/fileBrowser/dragAndDrop.d.ts +17 -4
- package/lib/fileBrowser/dragAndDrop.js +2 -7
- package/lib/fileBrowser/fileBrowser.d.ts +19 -9
- package/lib/fileBrowser/fileBrowser.js +70 -222
- package/lib/fileBrowser/gitStatus.d.ts +16 -10
- package/lib/fileBrowser/gitStatus.js +31 -30
- package/lib/fileBrowser/gitignore.d.ts +4 -17
- package/lib/fileBrowser/gitignore.js +4 -25
- package/lib/fileBrowser/icons.d.ts +7 -17
- package/lib/fileBrowser/icons.js +25 -80
- package/lib/fileBrowser/index.js +2 -4
- package/lib/fileBrowser/widget.d.ts +141 -48
- package/lib/fileBrowser/widget.js +102 -4
- package/lib/fileTypeIcons/index.js +0 -10
- package/lib/git/api.d.ts +5 -13
- package/lib/git/api.js +10 -36
- package/lib/git/askRequest.d.ts +4 -8
- package/lib/git/askRequest.js +6 -20
- package/lib/git/commands.d.ts +45 -5
- package/lib/git/commands.js +20 -13
- package/lib/git/diffModel.js +4 -0
- package/lib/git/diffPreferences.d.ts +90 -0
- package/lib/git/diffPreferences.js +107 -0
- package/lib/git/diffProvider.js +10 -2
- package/lib/git/diffSurface.d.ts +64 -63
- package/lib/git/diffSurface.js +72 -213
- package/lib/git/diffTheme.d.ts +7 -0
- package/lib/git/diffTheme.js +23 -0
- package/lib/git/diffWidget.d.ts +70 -14
- package/lib/git/diffWidget.js +100 -94
- package/lib/git/imageDiff.d.ts +14 -6
- package/lib/git/imageDiff.js +19 -75
- package/lib/git/index.d.ts +0 -3
- package/lib/git/index.js +44 -10
- package/lib/git/notebookDiff.d.ts +110 -33
- package/lib/git/notebookDiff.js +55 -197
- package/lib/git/tokens.d.ts +73 -31
- package/lib/git/tokens.js +2 -3
- package/lib/highlight/index.d.ts +4 -10
- package/lib/highlight/index.js +10 -41
- package/lib/index.d.ts +0 -9
- package/lib/index.js +0 -9
- package/lib/launcher/agents.d.ts +55 -51
- package/lib/launcher/agents.js +15 -57
- package/lib/launcher/availability.d.ts +3 -9
- package/lib/launcher/availability.js +3 -9
- package/lib/launcher/commands.d.ts +7 -32
- package/lib/launcher/commands.js +14 -45
- package/lib/launcher/dashboard.d.ts +19 -24
- package/lib/launcher/dashboard.js +21 -67
- package/lib/launcher/editorRegistry.d.ts +10 -22
- package/lib/launcher/editorRegistry.js +14 -18
- package/lib/launcher/editors.d.ts +39 -45
- package/lib/launcher/editors.js +8 -34
- package/lib/launcher/icons.d.ts +0 -12
- package/lib/launcher/icons.js +5 -39
- package/lib/launcher/index.js +12 -55
- package/lib/launcher/invocation.d.ts +4 -10
- package/lib/launcher/invocation.js +7 -22
- package/lib/launcher/registry.d.ts +9 -7
- package/lib/launcher/registry.js +9 -7
- package/lib/launcher/tokens.d.ts +10 -22
- package/lib/launcher/tokens.js +5 -8
- package/lib/menuBar/index.d.ts +3 -10
- package/lib/menuBar/index.js +11 -28
- package/lib/menus/index.d.ts +3 -9
- package/lib/menus/index.js +14 -30
- package/lib/omnibox/files.d.ts +2 -4
- package/lib/omnibox/files.js +5 -11
- package/lib/omnibox/index.d.ts +4 -14
- package/lib/omnibox/index.js +9 -27
- package/lib/omnibox/model.d.ts +38 -10
- package/lib/omnibox/model.js +15 -33
- package/lib/omnibox/recents.d.ts +15 -17
- package/lib/omnibox/recents.js +12 -17
- package/lib/omnibox/tokens.d.ts +5 -10
- package/lib/omnibox/tokens.js +2 -6
- package/lib/omnibox/widget.d.ts +11 -7
- package/lib/omnibox/widget.js +8 -16
- package/lib/searchReplace/index.d.ts +4 -20
- package/lib/searchReplace/index.js +7 -27
- package/lib/showOutput/index.d.ts +3 -12
- package/lib/showOutput/index.js +6 -22
- package/lib/sidebar/index.d.ts +4 -7
- package/lib/sidebar/index.js +5 -12
- package/lib/terminalNotifications/index.d.ts +4 -10
- package/lib/terminalNotifications/index.js +6 -20
- package/lib/terminals/agentTerminals.d.ts +12 -5
- package/lib/terminals/agentTerminals.js +25 -44
- package/lib/terminals/detection.d.ts +4 -8
- package/lib/terminals/detection.js +4 -8
- package/lib/terminals/index.d.ts +0 -35
- package/lib/terminals/index.js +12 -101
- package/lib/terminals/model.d.ts +64 -139
- package/lib/terminals/model.js +80 -220
- package/lib/terminals/tokens.d.ts +11 -22
- package/lib/terminals/tokens.js +2 -2
- package/lib/terminals/widget.d.ts +43 -13
- package/lib/terminals/widget.js +33 -18
- package/lib/topBar/icons.d.ts +3 -8
- package/lib/topBar/icons.js +3 -8
- package/lib/topBar/index.d.ts +4 -11
- package/lib/topBar/index.js +10 -38
- package/lib/walkthrough/index.d.ts +4 -9
- package/lib/walkthrough/index.js +6 -20
- package/lib/walkthrough/panel.d.ts +40 -9
- package/lib/walkthrough/panel.js +4 -10
- package/package.json +5 -3
- package/schema/git-diff-preferences.json +42 -0
- package/src/about/icons.ts +0 -5
- package/src/about/index.tsx +6 -17
- package/src/agentSessions.ts +9 -29
- package/src/askAgent/editorSelection.ts +22 -29
- package/src/askAgent/icons.ts +2 -3
- package/src/askAgent/index.ts +54 -158
- package/src/askAgent/popup.tsx +23 -45
- package/src/askAgent/prompt.ts +6 -19
- package/src/askAgent/queue.ts +13 -24
- package/src/askAgent/queuePanel.tsx +21 -46
- package/src/askAgent/targetPicker.tsx +5 -15
- package/src/askAgent/tokens.ts +32 -46
- package/src/commandBar/index.ts +24 -39
- package/src/customPanel/index.ts +15 -0
- package/src/editorBreadcrumbs/index.ts +10 -6
- package/src/editorBreadcrumbs/widget.ts +23 -17
- package/src/editorIndent/index.ts +17 -23
- package/src/fileBrowser/commands.ts +119 -96
- package/src/fileBrowser/contents.ts +3 -0
- package/src/fileBrowser/dragAndDrop.ts +17 -7
- package/src/fileBrowser/fileBrowser.tsx +86 -230
- package/src/fileBrowser/gitStatus.ts +36 -33
- package/src/fileBrowser/gitignore.ts +4 -25
- package/src/fileBrowser/icons.ts +25 -80
- package/src/fileBrowser/index.ts +2 -4
- package/src/fileBrowser/widget.tsx +171 -50
- package/src/fileTypeIcons/index.ts +0 -10
- package/src/git/api.ts +10 -36
- package/src/git/askRequest.ts +6 -20
- package/src/git/commands.ts +83 -11
- package/src/git/diffModel.ts +25 -0
- package/src/git/diffPreferences.ts +192 -0
- package/src/git/diffProvider.tsx +11 -2
- package/src/git/diffSurface.tsx +165 -286
- package/src/git/diffTheme.ts +24 -0
- package/src/git/diffWidget.tsx +194 -134
- package/src/git/imageDiff.tsx +56 -84
- package/src/git/index.ts +54 -10
- package/src/git/notebookDiff.tsx +214 -240
- package/src/git/tokens.ts +73 -31
- package/src/highlight/index.ts +10 -41
- package/src/index.ts +0 -9
- package/src/launcher/agents.ts +64 -95
- package/src/launcher/availability.ts +3 -9
- package/src/launcher/commands.ts +14 -51
- package/src/launcher/dashboard.tsx +49 -88
- package/src/launcher/editorRegistry.ts +21 -29
- package/src/launcher/editors.ts +39 -58
- package/src/launcher/icons.ts +5 -39
- package/src/launcher/index.ts +12 -55
- package/src/launcher/invocation.ts +7 -22
- package/src/launcher/registry.ts +9 -7
- package/src/launcher/tokens.ts +10 -22
- package/src/menuBar/index.ts +11 -28
- package/src/menus/index.ts +14 -30
- package/src/omnibox/files.ts +10 -14
- package/src/omnibox/index.ts +9 -27
- package/src/omnibox/model.ts +50 -38
- package/src/omnibox/recents.ts +17 -22
- package/src/omnibox/tokens.ts +5 -10
- package/src/omnibox/widget.tsx +14 -19
- package/src/searchReplace/index.ts +7 -27
- package/src/showOutput/index.ts +6 -22
- package/src/sidebar/index.ts +20 -12
- package/src/terminalNotifications/index.ts +42 -25
- package/src/terminals/agentTerminals.ts +25 -49
- package/src/terminals/detection.ts +4 -8
- package/src/terminals/index.ts +12 -101
- package/src/terminals/model.ts +130 -246
- package/src/terminals/tokens.ts +11 -22
- package/src/terminals/widget.tsx +48 -31
- package/src/topBar/icons.ts +3 -8
- package/src/topBar/index.ts +21 -44
- package/src/walkthrough/index.ts +6 -20
- package/src/walkthrough/panel.ts +40 -10
- package/style/about.css +2 -7
- package/style/askAgent.css +8 -40
- package/style/base.css +9 -45
- package/style/chrome.css +108 -308
- package/style/commandBar.css +4 -20
- package/style/customPanel.css +1 -4
- package/style/git.css +26 -186
- package/style/highlight.css +1 -3
- package/style/launcher.css +6 -70
- package/style/omnibox.css +2 -14
- package/style/showOutput.css +0 -6
- package/style/sidebar.css +29 -9
- package/style/tabs.css +5 -13
- package/style/terminals.css +6 -53
- package/style/topBar.css +2 -22
- package/style/walkthrough.css +0 -8
|
@@ -21,6 +21,10 @@ const PLUGIN_ID = 'xtralab:editor-breadcrumbs';
|
|
|
21
21
|
const EDITOR_FACTORY = 'Editor';
|
|
22
22
|
const TOOLBAR_ITEM_NAME = 'xtralab-editor-breadcrumbs';
|
|
23
23
|
|
|
24
|
+
/**
|
|
25
|
+
* A widget extension that adds an `EditorBreadcrumbs` item to the toolbar of
|
|
26
|
+
* each new editor widget.
|
|
27
|
+
*/
|
|
24
28
|
class EditorBreadcrumbsExtension implements DocumentRegistry.IWidgetExtension<
|
|
25
29
|
IDocumentWidget<Widget, DocumentRegistry.IModel>,
|
|
26
30
|
DocumentRegistry.IModel
|
|
@@ -30,6 +34,9 @@ class EditorBreadcrumbsExtension implements DocumentRegistry.IWidgetExtension<
|
|
|
30
34
|
this._trans = trans;
|
|
31
35
|
}
|
|
32
36
|
|
|
37
|
+
/**
|
|
38
|
+
* Create the breadcrumbs toolbar item for a new editor widget.
|
|
39
|
+
*/
|
|
33
40
|
createNew(
|
|
34
41
|
widget: IDocumentWidget<Widget, DocumentRegistry.IModel>,
|
|
35
42
|
context: DocumentRegistry.IContext<DocumentRegistry.IModel>
|
|
@@ -56,12 +63,9 @@ class EditorBreadcrumbsExtension implements DocumentRegistry.IWidgetExtension<
|
|
|
56
63
|
}
|
|
57
64
|
|
|
58
65
|
/**
|
|
59
|
-
* Adds a
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
* underlying folder or file. The breadcrumbs plugin therefore does not
|
|
63
|
-
* import the file browser at all — the JupyterLab command registry is
|
|
64
|
-
* the only seam between them.
|
|
66
|
+
* Adds a clickable path breadcrumb to text editor toolbars. Clicks dispatch
|
|
67
|
+
* `xtralab:reveal-path`; the command registry is the only seam with the file
|
|
68
|
+
* browser.
|
|
65
69
|
*/
|
|
66
70
|
const plugin: JupyterFrontEndPlugin<void> = {
|
|
67
71
|
id: PLUGIN_ID,
|
|
@@ -15,15 +15,25 @@ const CURRENT_ITEM_CLASS = 'jp-mod-current';
|
|
|
15
15
|
const SEPARATOR_CLASS = 'jp-BreadCrumbs-separator';
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
|
-
*
|
|
19
|
-
* Registered by the file browser plugin; the breadcrumbs only know
|
|
20
|
-
* the name so they stay decoupled from the file browser internals.
|
|
18
|
+
* Registered by the file browser plugin; only the name is shared here.
|
|
21
19
|
*/
|
|
22
20
|
const REVEAL_COMMAND = 'xtralab:reveal-path';
|
|
23
21
|
|
|
22
|
+
/**
|
|
23
|
+
* The options used to create an `EditorBreadcrumbs` widget.
|
|
24
|
+
*/
|
|
24
25
|
interface IEditorBreadcrumbsOptions {
|
|
26
|
+
/**
|
|
27
|
+
* The document context whose path is rendered.
|
|
28
|
+
*/
|
|
25
29
|
context: DocumentRegistry.IContext<DocumentRegistry.IModel>;
|
|
30
|
+
/**
|
|
31
|
+
* The command registry used to dispatch the reveal command.
|
|
32
|
+
*/
|
|
26
33
|
commands: CommandRegistry;
|
|
34
|
+
/**
|
|
35
|
+
* The application translation bundle.
|
|
36
|
+
*/
|
|
27
37
|
trans: TranslationBundle;
|
|
28
38
|
}
|
|
29
39
|
|
|
@@ -38,9 +48,8 @@ export class EditorBreadcrumbs extends Widget {
|
|
|
38
48
|
this._commands = options.commands;
|
|
39
49
|
this._trans = options.trans;
|
|
40
50
|
this.addClass(EDITOR_BREADCRUMBS_CLASS);
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
// stretched clientWidth they would evict every item into the popup.
|
|
51
|
+
// ReactiveToolbar must count the stretched breadcrumbs as a spacer (2px);
|
|
52
|
+
// measured at clientWidth they would evict every item into the popup.
|
|
44
53
|
this.addClass(TOOLBAR_SPACER_CLASS);
|
|
45
54
|
this.node.setAttribute('aria-label', this._trans.__('Editor file path'));
|
|
46
55
|
|
|
@@ -55,6 +64,9 @@ export class EditorBreadcrumbs extends Widget {
|
|
|
55
64
|
this._render();
|
|
56
65
|
}
|
|
57
66
|
|
|
67
|
+
/**
|
|
68
|
+
* Dispose of the resources held by the widget.
|
|
69
|
+
*/
|
|
58
70
|
dispose(): void {
|
|
59
71
|
if (this.isDisposed) {
|
|
60
72
|
return;
|
|
@@ -81,9 +93,7 @@ export class EditorBreadcrumbs extends Widget {
|
|
|
81
93
|
parts.forEach((part, index) => {
|
|
82
94
|
const isLast = index === parts.length - 1;
|
|
83
95
|
const subPath = parts.slice(0, index + 1).join('/');
|
|
84
|
-
// Canonical
|
|
85
|
-
// slash, files do not. Every part except the last names a folder
|
|
86
|
-
// along the way to the open file.
|
|
96
|
+
// Canonical @pierre/trees paths: directories carry a trailing slash.
|
|
87
97
|
const canonical = isLast ? subPath : `${subPath}/`;
|
|
88
98
|
|
|
89
99
|
const item = document.createElement('span');
|
|
@@ -117,19 +127,15 @@ export class EditorBreadcrumbs extends Widget {
|
|
|
117
127
|
item.dataset.path = '/';
|
|
118
128
|
item.tabIndex = 0;
|
|
119
129
|
item.setAttribute('role', 'button');
|
|
120
|
-
// Empty
|
|
121
|
-
//
|
|
122
|
-
// selection, and scroll back to the top. The tree has no row for
|
|
123
|
-
// the workspace root itself.
|
|
130
|
+
// Empty path is the workspace-root gesture; the tree has no row for
|
|
131
|
+
// the root itself.
|
|
124
132
|
this._attachReveal(item, '');
|
|
125
133
|
return item;
|
|
126
134
|
}
|
|
127
135
|
|
|
128
136
|
/**
|
|
129
|
-
* Bind click and keyboard activation on a crumb
|
|
130
|
-
*
|
|
131
|
-
* resolve or execute the command is logged but never thrown into
|
|
132
|
-
* the editor's toolbar.
|
|
137
|
+
* Bind click and keyboard activation on a crumb to the reveal command;
|
|
138
|
+
* dispatch failures are logged, never thrown into the toolbar.
|
|
133
139
|
*/
|
|
134
140
|
private _attachReveal(element: HTMLElement, canonicalPath: string): void {
|
|
135
141
|
const fire = (event: Event): void => {
|
|
@@ -14,10 +14,8 @@ import detectIndent from 'detect-indent';
|
|
|
14
14
|
const PLUGIN_ID = 'xtralab:editor-indent';
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* types not listed here inherit whatever the user has set in JupyterLab.
|
|
20
|
-
* Covers .ts/.tsx/.jsx/.js variants registered by `@jupyterlab/codemirror`.
|
|
17
|
+
* 2-space fallback for new or undetectable files, matching Prettier's
|
|
18
|
+
* web-language defaults; unlisted mime types keep the user's setting.
|
|
21
19
|
*/
|
|
22
20
|
const FALLBACK_INDENT_BY_MIME: Record<string, number> = {
|
|
23
21
|
'application/ecmascript': 2,
|
|
@@ -38,15 +36,20 @@ const FALLBACK_INDENT_BY_MIME: Record<string, number> = {
|
|
|
38
36
|
'text/yaml': 2
|
|
39
37
|
};
|
|
40
38
|
|
|
39
|
+
/**
|
|
40
|
+
* The indentation resolved for a document.
|
|
41
|
+
*/
|
|
41
42
|
interface IResolvedIndent {
|
|
43
|
+
/**
|
|
44
|
+
* The string inserted per indent level (spaces or a tab), for `indentUnit`.
|
|
45
|
+
*/
|
|
42
46
|
unit: string;
|
|
47
|
+
/**
|
|
48
|
+
* The indent width in columns, for `EditorState.tabSize`.
|
|
49
|
+
*/
|
|
43
50
|
width: number;
|
|
44
51
|
}
|
|
45
52
|
|
|
46
|
-
/**
|
|
47
|
-
* Run `detect-indent` against the file's content and convert its result into
|
|
48
|
-
* something we can hand to CodeMirror.
|
|
49
|
-
*/
|
|
50
53
|
function detectFromContent(text: string): IResolvedIndent | null {
|
|
51
54
|
if (text.length === 0) {
|
|
52
55
|
return null;
|
|
@@ -76,9 +79,8 @@ function resolveIndent(text: string, mimeType: string): IResolvedIndent | null {
|
|
|
76
79
|
}
|
|
77
80
|
|
|
78
81
|
function buildExtension(resolved: IResolvedIndent): Extension {
|
|
79
|
-
// `Prec.highest`
|
|
80
|
-
//
|
|
81
|
-
// both of which JupyterLab registers at default precedence.
|
|
82
|
+
// `Prec.highest` beats both `defaultConfig` and per-editor
|
|
83
|
+
// `editorConfig.indentUnit`, which JupyterLab registers at default precedence.
|
|
82
84
|
return Prec.highest([
|
|
83
85
|
indentUnit.of(resolved.unit),
|
|
84
86
|
EditorState.tabSize.of(resolved.width)
|
|
@@ -86,18 +88,10 @@ function buildExtension(resolved: IResolvedIndent): Extension {
|
|
|
86
88
|
}
|
|
87
89
|
|
|
88
90
|
/**
|
|
89
|
-
* Sniff each
|
|
90
|
-
* `
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
* detecting from the file's own content sidesteps that without forcing every
|
|
94
|
-
* project to share the same convention.
|
|
95
|
-
*
|
|
96
|
-
* Detection runs at editor-construction time and again on the first content
|
|
97
|
-
* change — that second pass exists because JupyterLab's collaborative document
|
|
98
|
-
* loader can settle the file body into the shared model after the editor view
|
|
99
|
-
* already exists, in which case the factory would otherwise have only seen an
|
|
100
|
-
* empty string.
|
|
91
|
+
* Sniff each document's indentation and configure CodeMirror's `indentUnit` /
|
|
92
|
+
* `tabSize` to match, since JupyterLab only has one global indent setting.
|
|
93
|
+
* Detection reruns on the first content change: the collaborative loader can
|
|
94
|
+
* settle the file body after the editor view exists, so the factory may see ''.
|
|
101
95
|
*/
|
|
102
96
|
const plugin: JupyterFrontEndPlugin<void> = {
|
|
103
97
|
id: PLUGIN_ID,
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
fileIcon,
|
|
22
22
|
filterIcon,
|
|
23
23
|
IDisposableMenuItem,
|
|
24
|
+
LabIcon,
|
|
24
25
|
newFolderIcon,
|
|
25
26
|
RankedMenu,
|
|
26
27
|
refreshIcon
|
|
@@ -28,19 +29,24 @@ import {
|
|
|
28
29
|
import { ReadonlyPartialJSONObject } from '@lumino/coreutils';
|
|
29
30
|
import { ContextMenu, Widget } from '@lumino/widgets';
|
|
30
31
|
|
|
32
|
+
import {
|
|
33
|
+
CommandArguments as GitCommandArguments,
|
|
34
|
+
CommandIDs as GitCommandIDs
|
|
35
|
+
} from '../git/commands';
|
|
36
|
+
import type { IFileChange } from '../git/tokens';
|
|
31
37
|
import { toCanonicalPath, toServerPath } from './contents';
|
|
38
|
+
import { GIT_REPO_PATH } from './gitStatus';
|
|
32
39
|
import { FILE_BROWSER_ID, IXtralabFileBrowser } from './widget';
|
|
33
40
|
|
|
34
41
|
/**
|
|
35
|
-
* Command
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
* via `IFileBrowserFactory.tracker` — our widget is not a `FileBrowser`, so
|
|
39
|
-
* those handlers would never see it.
|
|
42
|
+
* Command ids for the xtralab browser. Namespaced `xtralab:` rather than
|
|
43
|
+
* reusing the `filebrowser:` ids: the core handlers resolve their target via
|
|
44
|
+
* `IFileBrowserFactory.tracker`, which never sees this widget.
|
|
40
45
|
*/
|
|
41
46
|
export namespace CommandIDs {
|
|
42
47
|
export const open = 'xtralab:open';
|
|
43
48
|
export const openBrowserTab = 'xtralab:open-browser-tab';
|
|
49
|
+
export const openGitDiff = 'xtralab:open-git-diff';
|
|
44
50
|
export const rename = 'xtralab:rename';
|
|
45
51
|
export const del = 'xtralab:delete';
|
|
46
52
|
export const duplicate = 'xtralab:duplicate';
|
|
@@ -56,27 +62,40 @@ export namespace CommandIDs {
|
|
|
56
62
|
}
|
|
57
63
|
|
|
58
64
|
/**
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
* core's populator does not try to fill ours with items derived from the
|
|
62
|
-
* default file browser's selection.
|
|
65
|
+
* "Open With" submenu id. Distinct from the core `jp-contextmenu-open-with`
|
|
66
|
+
* so the core's populator does not fill ours from its own selection.
|
|
63
67
|
*/
|
|
64
68
|
const OPEN_WITH_SUBMENU_ID = 'jp-contextmenu-xtralab-open-with';
|
|
65
69
|
|
|
70
|
+
/**
|
|
71
|
+
* Options for registering the xtralab file browser commands.
|
|
72
|
+
*/
|
|
66
73
|
interface IRegisterCommandsOptions {
|
|
74
|
+
/**
|
|
75
|
+
* The JupyterLab application the commands are registered on.
|
|
76
|
+
*/
|
|
67
77
|
app: JupyterFrontEnd;
|
|
78
|
+
/**
|
|
79
|
+
* The file browser widget the commands act on.
|
|
80
|
+
*/
|
|
68
81
|
browser: IXtralabFileBrowser;
|
|
82
|
+
/**
|
|
83
|
+
* The document manager used to resolve the current widget's file path.
|
|
84
|
+
*/
|
|
69
85
|
docManager: IDocumentManager;
|
|
86
|
+
/**
|
|
87
|
+
* The command palette the commands are added to, or `null` when unavailable.
|
|
88
|
+
*/
|
|
70
89
|
palette: ICommandPalette | null;
|
|
90
|
+
/**
|
|
91
|
+
* The translator for user-facing strings; `null` falls back to English.
|
|
92
|
+
*/
|
|
71
93
|
translator: ITranslator | null;
|
|
72
94
|
}
|
|
73
95
|
|
|
74
96
|
/**
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
* items match a selector — and fall back to the first item in the tree's
|
|
78
|
-
* selection if the command was invoked without an active contextmenu event
|
|
79
|
-
* (e.g. from the command palette).
|
|
97
|
+
* Path of the right-clicked item, from the contextmenu event's target;
|
|
98
|
+
* falls back to the tree selection when invoked without one (the palette).
|
|
80
99
|
*/
|
|
81
100
|
function getTargetPath(
|
|
82
101
|
app: JupyterFrontEnd,
|
|
@@ -92,11 +111,6 @@ function getTargetPath(
|
|
|
92
111
|
return browser.selectedPaths[0];
|
|
93
112
|
}
|
|
94
113
|
|
|
95
|
-
/**
|
|
96
|
-
* Resolve the kind of the item that was right-clicked, based on the data
|
|
97
|
-
* attributes emitted by `@pierre/trees` rows. Returns `undefined` if no
|
|
98
|
-
* contextmenu event is active or the target is not a tree item.
|
|
99
|
-
*/
|
|
100
114
|
function getTargetKind(app: JupyterFrontEnd): 'file' | 'folder' | undefined {
|
|
101
115
|
const node = app.contextMenuHitTest(
|
|
102
116
|
n => n.dataset !== undefined && n.dataset.type === 'item'
|
|
@@ -106,14 +120,9 @@ function getTargetKind(app: JupyterFrontEnd): 'file' | 'folder' | undefined {
|
|
|
106
120
|
}
|
|
107
121
|
|
|
108
122
|
/**
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
* "Open" on any file in a multi-selection opens them all. If the
|
|
113
|
-
* right-clicked row is *not* in the selection, only that row is opened.
|
|
114
|
-
*
|
|
115
|
-
* Folders are always filtered out: there is no "current directory" in
|
|
116
|
-
* this tree, so opening a folder via "Open" is a no-op.
|
|
123
|
+
* Canonical paths "Open" acts on: the whole selection when the right-clicked
|
|
124
|
+
* row is part of it (matching the default browser), else just that row.
|
|
125
|
+
* Folders are filtered out — this tree has no "current directory".
|
|
117
126
|
*/
|
|
118
127
|
function getOpenPaths(
|
|
119
128
|
app: JupyterFrontEnd,
|
|
@@ -135,8 +144,22 @@ function getOpenPaths(
|
|
|
135
144
|
}
|
|
136
145
|
|
|
137
146
|
/**
|
|
138
|
-
*
|
|
147
|
+
* Git changes of the target file, at most one staged and one unstaged.
|
|
148
|
+
* Status paths are repo-relative; like the tree's git badges, this assumes
|
|
149
|
+
* the repo root is the server root.
|
|
139
150
|
*/
|
|
151
|
+
function getTargetChanges(
|
|
152
|
+
app: JupyterFrontEnd,
|
|
153
|
+
browser: IXtralabFileBrowser
|
|
154
|
+
): IFileChange[] {
|
|
155
|
+
const targetPath = getTargetPath(app, browser);
|
|
156
|
+
if (targetPath === undefined) {
|
|
157
|
+
return [];
|
|
158
|
+
}
|
|
159
|
+
const serverPath = toServerPath(targetPath);
|
|
160
|
+
return browser.gitChanges.filter(change => change.path === serverPath);
|
|
161
|
+
}
|
|
162
|
+
|
|
140
163
|
function hasTarget(
|
|
141
164
|
app: JupyterFrontEnd,
|
|
142
165
|
browser: IXtralabFileBrowser
|
|
@@ -145,11 +168,9 @@ function hasTarget(
|
|
|
145
168
|
}
|
|
146
169
|
|
|
147
170
|
/**
|
|
148
|
-
*
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
* when the tree has no selection — that matches the contents API's
|
|
152
|
-
* convention for "the root directory".
|
|
171
|
+
* Working directory for create actions: a selected folder is its own cwd, a
|
|
172
|
+
* file falls back to its parent; empty string (the contents-API root) when
|
|
173
|
+
* nothing is selected.
|
|
153
174
|
*/
|
|
154
175
|
function getWorkingDirectory(browser: IXtralabFileBrowser): string {
|
|
155
176
|
const first = browser.selectedPaths[0];
|
|
@@ -164,14 +185,10 @@ function getWorkingDirectory(browser: IXtralabFileBrowser): string {
|
|
|
164
185
|
}
|
|
165
186
|
|
|
166
187
|
/**
|
|
167
|
-
*
|
|
168
|
-
*
|
|
169
|
-
*
|
|
170
|
-
*
|
|
171
|
-
*
|
|
172
|
-
* Valid only during a context-menu invocation: `contextMenuHitTest` reads the
|
|
173
|
-
* last context-menu event, which is not cleared when the menu closes, so
|
|
174
|
-
* {@link documentPathToReveal} gates this to avoid resolving a stale tab.
|
|
188
|
+
* Main-area widget whose tab is the target of the current context-menu
|
|
189
|
+
* event, or `null`. `contextMenuHitTest` reads the last context-menu event,
|
|
190
|
+
* which is not cleared when the menu closes, so callers must gate against
|
|
191
|
+
* resolving a stale tab.
|
|
175
192
|
*/
|
|
176
193
|
function contextMenuTabWidget(app: JupyterFrontEnd): Widget | null {
|
|
177
194
|
const node = app.contextMenuHitTest(
|
|
@@ -190,10 +207,9 @@ function contextMenuTabWidget(app: JupyterFrontEnd): Widget | null {
|
|
|
190
207
|
}
|
|
191
208
|
|
|
192
209
|
/**
|
|
193
|
-
*
|
|
194
|
-
*
|
|
195
|
-
*
|
|
196
|
-
* `path` is already canonical. Returns `undefined` when nothing resolves.
|
|
210
|
+
* Canonical path of the document to reveal: the right-clicked tab when
|
|
211
|
+
* `fromContextMenu` is true (the file-tab menu), else the active main-area
|
|
212
|
+
* widget (the palette).
|
|
197
213
|
*/
|
|
198
214
|
function documentPathToReveal(
|
|
199
215
|
app: JupyterFrontEnd,
|
|
@@ -211,14 +227,10 @@ function documentPathToReveal(
|
|
|
211
227
|
}
|
|
212
228
|
|
|
213
229
|
/**
|
|
214
|
-
*
|
|
215
|
-
*
|
|
216
|
-
* `@jupyterlab/filebrowser-extension:open-with` pattern: the schema declares
|
|
217
|
-
* an empty submenu by id, and we look it up on the open context menu and
|
|
218
|
-
* fill it before the user can hover.
|
|
219
|
-
*
|
|
230
|
+
* Build the populator that fills the "Open With" submenu on every
|
|
231
|
+
* context-menu open, mirroring `filebrowser-extension:open-with`.
|
|
220
232
|
* `preferredWidgetFactories(path)` is path-only, so the populator stays
|
|
221
|
-
* synchronous and the submenu is ready before the user can hover
|
|
233
|
+
* synchronous and the submenu is ready before the user can hover.
|
|
222
234
|
*/
|
|
223
235
|
function makeOpenWithUpdater(
|
|
224
236
|
app: JupyterFrontEnd,
|
|
@@ -255,17 +267,26 @@ function makeOpenWithUpdater(
|
|
|
255
267
|
command: CommandIDs.open
|
|
256
268
|
})
|
|
257
269
|
);
|
|
270
|
+
|
|
271
|
+
const changes = getTargetChanges(app, browser);
|
|
272
|
+
if (changes.length > 0) {
|
|
273
|
+
items.push(submenu.addItem({ type: 'separator' }));
|
|
274
|
+
for (const change of changes) {
|
|
275
|
+
items.push(
|
|
276
|
+
submenu.addItem({
|
|
277
|
+
args: { group: change.group },
|
|
278
|
+
command: CommandIDs.openGitDiff
|
|
279
|
+
})
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
258
283
|
};
|
|
259
284
|
}
|
|
260
285
|
|
|
261
286
|
/**
|
|
262
|
-
* Register
|
|
263
|
-
*
|
|
264
|
-
*
|
|
265
|
-
*
|
|
266
|
-
* @returns A function that detaches the menu items and signal listener.
|
|
267
|
-
* The commands themselves are owned by the registry for the lifetime of
|
|
268
|
-
* the plugin.
|
|
287
|
+
* Register the xtralab commands, context-menu wiring, and the dynamic
|
|
288
|
+
* "Open With" populator. Returns a detach function; the commands themselves
|
|
289
|
+
* stay owned by the registry.
|
|
269
290
|
*/
|
|
270
291
|
export function registerCommands(opts: IRegisterCommandsOptions): () => void {
|
|
271
292
|
const { app, browser, docManager, palette, translator } = opts;
|
|
@@ -320,6 +341,32 @@ export function registerCommands(opts: IRegisterCommandsOptions): () => void {
|
|
|
320
341
|
}
|
|
321
342
|
});
|
|
322
343
|
|
|
344
|
+
commands.addCommand(CommandIDs.openGitDiff, {
|
|
345
|
+
label: args =>
|
|
346
|
+
args.group === 'staged'
|
|
347
|
+
? trans.__('Git Diff (Staged)')
|
|
348
|
+
: trans.__('Git Diff (Working)'),
|
|
349
|
+
icon: LabIcon.resolve({ icon: 'git:diff' }).bindprops({
|
|
350
|
+
stylesheet: 'menuItem'
|
|
351
|
+
}),
|
|
352
|
+
execute: async (args: ReadonlyPartialJSONObject) => {
|
|
353
|
+
const change = getTargetChanges(app, browser).find(
|
|
354
|
+
candidate => candidate.group === args.group
|
|
355
|
+
);
|
|
356
|
+
if (change === undefined) {
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
const diffArgs: GitCommandArguments.IOpenDiff = {
|
|
360
|
+
repoPath: GIT_REPO_PATH,
|
|
361
|
+
change
|
|
362
|
+
};
|
|
363
|
+
return commands.execute(
|
|
364
|
+
GitCommandIDs.openDiff,
|
|
365
|
+
diffArgs as unknown as ReadonlyPartialJSONObject
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
});
|
|
369
|
+
|
|
323
370
|
commands.addCommand(CommandIDs.rename, {
|
|
324
371
|
label: trans.__('Rename…'),
|
|
325
372
|
icon: editIcon.bindprops({ stylesheet: 'menuItem' }),
|
|
@@ -496,23 +543,16 @@ export function registerCommands(opts: IRegisterCommandsOptions): () => void {
|
|
|
496
543
|
}
|
|
497
544
|
});
|
|
498
545
|
|
|
499
|
-
//
|
|
500
|
-
//
|
|
501
|
-
// the tree focused opens a search session.
|
|
546
|
+
// The filter can also auto-show when typing with the tree focused, so
|
|
547
|
+
// track visibility changes rather than the command's own executions.
|
|
502
548
|
const onFilterVisibleChanged = (): void => {
|
|
503
549
|
commands.notifyCommandChanged(CommandIDs.toggleFileFilter);
|
|
504
550
|
};
|
|
505
551
|
browser.fileFilterVisibleChanged.connect(onFilterVisibleChanged);
|
|
506
552
|
|
|
507
|
-
// Public reveal seam
|
|
508
|
-
//
|
|
509
|
-
//
|
|
510
|
-
// of the contract: the tree is hidden behind a tab and a reveal that
|
|
511
|
-
// does not surface the panel would silently no-op from the user's
|
|
512
|
-
// point of view. An empty path is the "workspace root" gesture —
|
|
513
|
-
// there is no tree row for the root itself, so the browser is asked
|
|
514
|
-
// to scroll back to the top and clear its selection instead.
|
|
515
|
-
// The tree is movable, so surface whichever sidebar widget holds it.
|
|
553
|
+
// Public reveal seam for other plugins; an empty path is the workspace-root
|
|
554
|
+
// gesture (the root has no tree row of its own). The tree is movable, so
|
|
555
|
+
// surface whichever sidebar widget holds it.
|
|
516
556
|
const activateTreeHost = (): void => {
|
|
517
557
|
for (const area of ['left', 'right']) {
|
|
518
558
|
for (const widget of app.shell.widgets(area)) {
|
|
@@ -538,8 +578,6 @@ export function registerCommands(opts: IRegisterCommandsOptions): () => void {
|
|
|
538
578
|
}
|
|
539
579
|
});
|
|
540
580
|
|
|
541
|
-
// Reveal the resolved document in the tree browser via the reveal-path
|
|
542
|
-
// command, which surfaces the sidebar and scrolls to the target.
|
|
543
581
|
commands.addCommand(CommandIDs.revealInFileTree, {
|
|
544
582
|
label: trans.__('Show in File Tree'),
|
|
545
583
|
caption: trans.__('Show this file in the xtralab file browser'),
|
|
@@ -579,10 +617,8 @@ export function registerCommands(opts: IRegisterCommandsOptions): () => void {
|
|
|
579
617
|
commands.addCommand(CommandIDs.newLauncher, {
|
|
580
618
|
label: trans.__('New Launcher'),
|
|
581
619
|
caption: trans.__('Open a new launcher'),
|
|
582
|
-
//
|
|
583
|
-
//
|
|
584
|
-
// styling expects the raw `jp-icon3` paths from `addIcon` so it can
|
|
585
|
-
// recolor them against the brand background.
|
|
620
|
+
// Raw `addIcon`, no `menuItem` bindprops: the launcher-extension's blue
|
|
621
|
+
// toolbar-button styling recolors the raw `jp-icon3` paths.
|
|
586
622
|
icon: addIcon,
|
|
587
623
|
execute: () => {
|
|
588
624
|
const cwd = getWorkingDirectory(browser);
|
|
@@ -590,34 +626,24 @@ export function registerCommands(opts: IRegisterCommandsOptions): () => void {
|
|
|
590
626
|
}
|
|
591
627
|
});
|
|
592
628
|
|
|
593
|
-
// Re-evaluate enabled/visible state when the user changes the tree
|
|
594
|
-
// selection, in case a command's predicate depends on the selection.
|
|
595
629
|
browser.selectionChanged.connect(() => {
|
|
596
630
|
for (const id of Object.values(CommandIDs)) {
|
|
597
631
|
commands.notifyCommandChanged(id);
|
|
598
632
|
}
|
|
599
633
|
});
|
|
600
634
|
|
|
601
|
-
// This command's enabled state tracks the active widget, not the tree
|
|
602
|
-
// selection, so re-evaluate it on current-widget changes. `currentChanged`
|
|
603
|
-
// is optional on the shell interface.
|
|
604
635
|
const onCurrentChanged = (): void => {
|
|
605
636
|
commands.notifyCommandChanged(CommandIDs.revealInFileTree);
|
|
606
637
|
};
|
|
607
638
|
app.shell.currentChanged?.connect(onCurrentChanged);
|
|
608
639
|
|
|
609
|
-
// Also surface it in the palette (acts on the active editor); the file-tab
|
|
610
|
-
// context-menu entry is declared in `schema/plugin.json`.
|
|
611
640
|
const paletteItem = palette?.addItem({
|
|
612
641
|
command: CommandIDs.revealInFileTree,
|
|
613
642
|
category: trans.__('File Browser')
|
|
614
643
|
});
|
|
615
644
|
|
|
616
|
-
//
|
|
617
|
-
//
|
|
618
|
-
// schema also declares an empty submenu placeholder with id
|
|
619
|
-
// `OPEN_WITH_SUBMENU_ID`; we fill it on every open from the document
|
|
620
|
-
// factories that can open the right-clicked file.
|
|
645
|
+
// Static context-menu items live in `schema/plugin.json` so users can
|
|
646
|
+
// override them; the schema declares the empty "Open With" placeholder.
|
|
621
647
|
const updateOpenWithMenu = makeOpenWithUpdater(app, browser);
|
|
622
648
|
app.contextMenu.opened.connect(updateOpenWithMenu);
|
|
623
649
|
|
|
@@ -630,9 +656,8 @@ export function registerCommands(opts: IRegisterCommandsOptions): () => void {
|
|
|
630
656
|
}
|
|
631
657
|
|
|
632
658
|
/**
|
|
633
|
-
*
|
|
634
|
-
*
|
|
635
|
-
* than commands.
|
|
659
|
+
* Toolbar item names, distinct from {@link CommandIDs}: the toolbar API
|
|
660
|
+
* uses opaque names rather than commands.
|
|
636
661
|
*/
|
|
637
662
|
export namespace ToolbarNames {
|
|
638
663
|
export const newLauncher = 'new-launcher';
|
|
@@ -643,10 +668,8 @@ export namespace ToolbarNames {
|
|
|
643
668
|
}
|
|
644
669
|
|
|
645
670
|
/**
|
|
646
|
-
* Populate the
|
|
647
|
-
*
|
|
648
|
-
* button, a refresh button, a collapse-all button, and the file filter
|
|
649
|
-
* toggle.
|
|
671
|
+
* Populate the toolbar with the buttons that mirror the default JupyterLab
|
|
672
|
+
* file browser.
|
|
650
673
|
*/
|
|
651
674
|
export function populateToolbar(opts: {
|
|
652
675
|
app: JupyterFrontEnd;
|
|
@@ -60,6 +60,9 @@ export function toServerPath(canonicalPath: string): string {
|
|
|
60
60
|
: canonicalPath;
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
/**
|
|
64
|
+
* The immediate children of a directory, as canonical `@pierre/trees` paths.
|
|
65
|
+
*/
|
|
63
66
|
interface IListedDirectory {
|
|
64
67
|
/**
|
|
65
68
|
* Canonical paths for every immediate child of the requested directory.
|
|
@@ -14,7 +14,13 @@ import { ROOT_LOAD_KEY, canonicalBasename, parentOf } from './contents';
|
|
|
14
14
|
* One tree-model move a drop performs, as canonical paths.
|
|
15
15
|
*/
|
|
16
16
|
export interface IDropMove {
|
|
17
|
+
/**
|
|
18
|
+
* The canonical path of the dragged item.
|
|
19
|
+
*/
|
|
17
20
|
from: string;
|
|
21
|
+
/**
|
|
22
|
+
* The canonical destination path: its basename under the target directory.
|
|
23
|
+
*/
|
|
18
24
|
to: string;
|
|
19
25
|
}
|
|
20
26
|
|
|
@@ -55,9 +61,6 @@ export function computeDropMoves(context: FileTreeDropContext): IDropMove[] {
|
|
|
55
61
|
.map(path => ({ from: path, to: `${dir}${canonicalBasename(path)}` }));
|
|
56
62
|
}
|
|
57
63
|
|
|
58
|
-
/**
|
|
59
|
-
* The drop target the tree reports for a move to the workspace root.
|
|
60
|
-
*/
|
|
61
64
|
const ROOT_DROP_TARGET: FileTreeDropTarget = {
|
|
62
65
|
directoryPath: null,
|
|
63
66
|
flattenedSegmentPath: null,
|
|
@@ -92,8 +95,17 @@ export function createTreeDragAndDropConfig(
|
|
|
92
95
|
};
|
|
93
96
|
}
|
|
94
97
|
|
|
98
|
+
/**
|
|
99
|
+
* Options for {@link useRootDropZone}.
|
|
100
|
+
*/
|
|
95
101
|
interface IRootDropZoneOptions {
|
|
102
|
+
/**
|
|
103
|
+
* The tree model, queried for the selection at drag start.
|
|
104
|
+
*/
|
|
96
105
|
model: FileTree;
|
|
106
|
+
/**
|
|
107
|
+
* The drop handler ref, filled by the contents-sync effect.
|
|
108
|
+
*/
|
|
97
109
|
handlerRef: React.RefObject<ITreeDropHandler | null>;
|
|
98
110
|
/**
|
|
99
111
|
* The light-DOM wrapper around the tree host; composed drag events
|
|
@@ -104,10 +116,8 @@ interface IRootDropZoneOptions {
|
|
|
104
116
|
|
|
105
117
|
/**
|
|
106
118
|
* Accept drops on the empty space below the last row as moves to the
|
|
107
|
-
* workspace root — the tree resolves a drop target only while the
|
|
108
|
-
*
|
|
109
|
-
* records the dragged paths; a drop whose composed path contains no
|
|
110
|
-
* row hands them over as a root move.
|
|
119
|
+
* workspace root — the tree resolves a drop target only while the cursor
|
|
120
|
+
* is over a row and silently discards such drops.
|
|
111
121
|
*/
|
|
112
122
|
export function useRootDropZone(options: IRootDropZoneOptions): void {
|
|
113
123
|
const { model, handlerRef, wrapperRef } = options;
|