xtralab 0.12.5 → 0.13.1
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/lib/editorBreadcrumbs/widget.js +5 -0
- package/lib/fileBrowser/commands.d.ts +4 -2
- package/lib/fileBrowser/commands.js +27 -3
- package/lib/fileBrowser/fileBrowser.js +65 -0
- package/lib/fileBrowser/widget.d.ts +27 -0
- package/lib/fileBrowser/widget.js +18 -0
- package/package.json +1 -1
- package/src/editorBreadcrumbs/widget.ts +5 -0
- package/src/fileBrowser/commands.ts +32 -2
- package/src/fileBrowser/fileBrowser.tsx +64 -0
- package/src/fileBrowser/widget.tsx +47 -0
- package/style/base.css +1 -1
- package/style/chrome.css +1167 -0
- package/style/index.css +1 -0
- package/style/index.js +1 -0
- package/style/launcher.css +5 -2
- package/style/omnibox.css +17 -7
- package/style/tabs.css +9 -0
- package/style/walkthrough.css +3 -1
|
@@ -2,6 +2,7 @@ import { PageConfig } from '@jupyterlab/coreutils';
|
|
|
2
2
|
import { homeIcon as rootIcon } from '@jupyterlab/ui-components';
|
|
3
3
|
import { Widget } from '@lumino/widgets';
|
|
4
4
|
const EDITOR_BREADCRUMBS_CLASS = 'jp-xtralab-EditorBreadcrumbs';
|
|
5
|
+
const TOOLBAR_SPACER_CLASS = 'jp-Toolbar-spacer';
|
|
5
6
|
const BREADCRUMBS_CONTAINER_CLASS = 'jp-BreadCrumbs-container';
|
|
6
7
|
const BREADCRUMBS_CONTENT_CLASS = 'jp-BreadCrumbs-content';
|
|
7
8
|
const ROOT_CLASS = 'jp-BreadCrumbs-home';
|
|
@@ -25,6 +26,10 @@ export class EditorBreadcrumbs extends Widget {
|
|
|
25
26
|
this._commands = options.commands;
|
|
26
27
|
this._trans = options.trans;
|
|
27
28
|
this.addClass(EDITOR_BREADCRUMBS_CLASS);
|
|
29
|
+
// The breadcrumbs stretch to fill the toolbar, so ReactiveToolbar's
|
|
30
|
+
// overflow math must count them as a spacer (2px) — measured at their
|
|
31
|
+
// stretched clientWidth they would evict every item into the popup.
|
|
32
|
+
this.addClass(TOOLBAR_SPACER_CLASS);
|
|
28
33
|
this.node.setAttribute('aria-label', this._trans.__('Editor file path'));
|
|
29
34
|
this._container = document.createElement('span');
|
|
30
35
|
this._container.className = BREADCRUMBS_CONTAINER_CLASS;
|
|
@@ -20,6 +20,7 @@ export declare namespace CommandIDs {
|
|
|
20
20
|
const download = "xtralab:download";
|
|
21
21
|
const refresh = "xtralab:refresh";
|
|
22
22
|
const collapseAll = "xtralab:collapse-all";
|
|
23
|
+
const toggleFileFilter = "xtralab:toggle-file-filter";
|
|
23
24
|
const createNewDirectory = "xtralab:create-new-directory";
|
|
24
25
|
const newLauncher = "xtralab:new-launcher";
|
|
25
26
|
const revealPath = "xtralab:reveal-path";
|
|
@@ -52,12 +53,13 @@ export declare namespace ToolbarNames {
|
|
|
52
53
|
const newDirectory = "new-directory";
|
|
53
54
|
const refresh = "refresh";
|
|
54
55
|
const collapseAll = "collapse-all";
|
|
56
|
+
const toggleFileFilter = "toggle-file-filter";
|
|
55
57
|
}
|
|
56
58
|
/**
|
|
57
59
|
* Populate the file browser's toolbar with the buttons that mirror the
|
|
58
60
|
* default JupyterLab file browser: a "+" launcher button, a new-folder
|
|
59
|
-
* button,
|
|
60
|
-
*
|
|
61
|
+
* button, a refresh button, a collapse-all button, and the file filter
|
|
62
|
+
* toggle.
|
|
61
63
|
*/
|
|
62
64
|
export declare function populateToolbar(opts: {
|
|
63
65
|
app: JupyterFrontEnd;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Clipboard, Dialog, InputDialog, showDialog, showErrorMessage } from '@jupyterlab/apputils';
|
|
2
2
|
import { PageConfig, PathExt } from '@jupyterlab/coreutils';
|
|
3
3
|
import { nullTranslator } from '@jupyterlab/translation';
|
|
4
|
-
import { addIcon, closeIcon, collapseAllIcon, CommandToolbarButton, copyIcon, downloadIcon, editIcon, fileIcon, newFolderIcon, refreshIcon } from '@jupyterlab/ui-components';
|
|
4
|
+
import { addIcon, closeIcon, collapseAllIcon, CommandToolbarButton, copyIcon, downloadIcon, editIcon, fileIcon, filterIcon, newFolderIcon, refreshIcon } from '@jupyterlab/ui-components';
|
|
5
5
|
import { toCanonicalPath, toServerPath } from './contents';
|
|
6
6
|
import { FILE_BROWSER_ID } from './widget';
|
|
7
7
|
/**
|
|
@@ -22,6 +22,7 @@ export var CommandIDs;
|
|
|
22
22
|
CommandIDs.download = 'xtralab:download';
|
|
23
23
|
CommandIDs.refresh = 'xtralab:refresh';
|
|
24
24
|
CommandIDs.collapseAll = 'xtralab:collapse-all';
|
|
25
|
+
CommandIDs.toggleFileFilter = 'xtralab:toggle-file-filter';
|
|
25
26
|
CommandIDs.createNewDirectory = 'xtralab:create-new-directory';
|
|
26
27
|
CommandIDs.newLauncher = 'xtralab:new-launcher';
|
|
27
28
|
CommandIDs.revealPath = 'xtralab:reveal-path';
|
|
@@ -393,6 +394,22 @@ export function registerCommands(opts) {
|
|
|
393
394
|
browser.collapseAll();
|
|
394
395
|
}
|
|
395
396
|
});
|
|
397
|
+
commands.addCommand(CommandIDs.toggleFileFilter, {
|
|
398
|
+
label: trans.__('Toggle File Filter'),
|
|
399
|
+
caption: trans.__('Show or hide the file filter'),
|
|
400
|
+
icon: filterIcon.bindprops({ stylesheet: 'menuItem' }),
|
|
401
|
+
isToggled: () => browser.fileFilterVisible,
|
|
402
|
+
execute: () => {
|
|
403
|
+
browser.toggleFileFilter();
|
|
404
|
+
}
|
|
405
|
+
});
|
|
406
|
+
// Refresh the toggle's state however the filter is shown or hidden —
|
|
407
|
+
// the toolbar button, or the auto-show that kicks in when typing with
|
|
408
|
+
// the tree focused opens a search session.
|
|
409
|
+
const onFilterVisibleChanged = () => {
|
|
410
|
+
commands.notifyCommandChanged(CommandIDs.toggleFileFilter);
|
|
411
|
+
};
|
|
412
|
+
browser.fileFilterVisibleChanged.connect(onFilterVisibleChanged);
|
|
396
413
|
// Public reveal seam. Other plugins (editor breadcrumbs, future
|
|
397
414
|
// "reveal in tree" actions) call this command rather than depending
|
|
398
415
|
// on the file browser widget directly. Activating the sidebar is part
|
|
@@ -492,6 +509,7 @@ export function registerCommands(opts) {
|
|
|
492
509
|
var _a;
|
|
493
510
|
app.contextMenu.opened.disconnect(updateOpenWithMenu);
|
|
494
511
|
(_a = app.shell.currentChanged) === null || _a === void 0 ? void 0 : _a.disconnect(onCurrentChanged);
|
|
512
|
+
browser.fileFilterVisibleChanged.disconnect(onFilterVisibleChanged);
|
|
495
513
|
paletteItem === null || paletteItem === void 0 ? void 0 : paletteItem.dispose();
|
|
496
514
|
};
|
|
497
515
|
}
|
|
@@ -506,12 +524,13 @@ export var ToolbarNames;
|
|
|
506
524
|
ToolbarNames.newDirectory = 'new-directory';
|
|
507
525
|
ToolbarNames.refresh = 'refresh';
|
|
508
526
|
ToolbarNames.collapseAll = 'collapse-all';
|
|
527
|
+
ToolbarNames.toggleFileFilter = 'toggle-file-filter';
|
|
509
528
|
})(ToolbarNames || (ToolbarNames = {}));
|
|
510
529
|
/**
|
|
511
530
|
* Populate the file browser's toolbar with the buttons that mirror the
|
|
512
531
|
* default JupyterLab file browser: a "+" launcher button, a new-folder
|
|
513
|
-
* button,
|
|
514
|
-
*
|
|
532
|
+
* button, a refresh button, a collapse-all button, and the file filter
|
|
533
|
+
* toggle.
|
|
515
534
|
*/
|
|
516
535
|
export function populateToolbar(opts) {
|
|
517
536
|
const { app, browser } = opts;
|
|
@@ -536,4 +555,9 @@ export function populateToolbar(opts) {
|
|
|
536
555
|
id: CommandIDs.collapseAll,
|
|
537
556
|
label: ''
|
|
538
557
|
}));
|
|
558
|
+
browser.toolbar.addItem(ToolbarNames.toggleFileFilter, new CommandToolbarButton({
|
|
559
|
+
commands,
|
|
560
|
+
id: CommandIDs.toggleFileFilter,
|
|
561
|
+
label: ''
|
|
562
|
+
}));
|
|
539
563
|
}
|
|
@@ -64,9 +64,20 @@ const GIT_REPO_PATH = '';
|
|
|
64
64
|
* resolution cost just for one string.
|
|
65
65
|
*/
|
|
66
66
|
const FILE_TREE_TAG = 'file-tree-container';
|
|
67
|
+
/**
|
|
68
|
+
* Injected into the tree's shadow root. The second rule hides the search
|
|
69
|
+
* box unless the host carries the visibility marker set by the filter
|
|
70
|
+
* bridge effect below — `@pierre/trees` always renders the box when
|
|
71
|
+
* `search` is enabled, and its stylesheet lives in the shadow root where
|
|
72
|
+
* outside CSS cannot reach.
|
|
73
|
+
*/
|
|
67
74
|
const FILE_TREE_UNSAFE_CSS = '[data-type="item"][data-item-selected="true"] ' +
|
|
68
75
|
'[data-item-section="spacing-item"] {' +
|
|
69
76
|
'border-left-color: transparent;' +
|
|
77
|
+
'}' +
|
|
78
|
+
':host(:not([data-xtralab-filter-visible])) ' +
|
|
79
|
+
'[data-file-tree-search-container] {' +
|
|
80
|
+
'display: none;' +
|
|
70
81
|
'}';
|
|
71
82
|
/**
|
|
72
83
|
* Renders a `@pierre/trees` file tree backed by the Jupyter contents API.
|
|
@@ -652,6 +663,17 @@ export function FileBrowserComponent(props) {
|
|
|
652
663
|
};
|
|
653
664
|
contentsManager.fileChanged.connect(onContentsFileChanged);
|
|
654
665
|
const unsubscribe = model.subscribe(() => {
|
|
666
|
+
// Search-driven expansion must not trigger fetches: the tree
|
|
667
|
+
// auto-expands every directory whose path matches the query, and
|
|
668
|
+
// treating those as user expansions would recursively fetch every
|
|
669
|
+
// matching subtree — a single common letter can walk the whole
|
|
670
|
+
// workspace, node_modules included. Closing the search restores
|
|
671
|
+
// the pre-search expansion state, and a directory clicked in the
|
|
672
|
+
// results is toggled after the session closes, so real expansions
|
|
673
|
+
// are still fetched the moment the session ends.
|
|
674
|
+
if (model.isSearchOpen()) {
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
655
677
|
knownDirs.forEach((state, canonicalPath) => {
|
|
656
678
|
if (state !== 'unloaded') {
|
|
657
679
|
return;
|
|
@@ -744,6 +766,49 @@ export function FileBrowserComponent(props) {
|
|
|
744
766
|
unsubscribe();
|
|
745
767
|
};
|
|
746
768
|
}, [model, widget]);
|
|
769
|
+
// Bridge the filter-box visibility between the widget and the tree. The
|
|
770
|
+
// widget owns the flag; applying it stamps the marker attribute the
|
|
771
|
+
// unsafeCSS rule keys on and opens or closes the model's search session
|
|
772
|
+
// (opening focuses the input, closing clears any active filter). The
|
|
773
|
+
// model subscription covers the reverse direction: typing a printable
|
|
774
|
+
// character while the tree has focus opens a session on the tree's own
|
|
775
|
+
// initiative, and the box must surface for that session to be usable.
|
|
776
|
+
React.useEffect(() => {
|
|
777
|
+
if (widget === undefined) {
|
|
778
|
+
return;
|
|
779
|
+
}
|
|
780
|
+
const apply = (visible) => {
|
|
781
|
+
const host = model.getFileTreeContainer();
|
|
782
|
+
if (host !== undefined) {
|
|
783
|
+
if (visible) {
|
|
784
|
+
host.dataset.xtralabFilterVisible = 'true';
|
|
785
|
+
}
|
|
786
|
+
else {
|
|
787
|
+
delete host.dataset.xtralabFilterVisible;
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
if (visible && !model.isSearchOpen()) {
|
|
791
|
+
model.openSearch();
|
|
792
|
+
}
|
|
793
|
+
else if (!visible && model.isSearchOpen()) {
|
|
794
|
+
model.closeSearch();
|
|
795
|
+
}
|
|
796
|
+
};
|
|
797
|
+
apply(widget.fileFilterVisible);
|
|
798
|
+
const visibleSlot = (sender, visible) => {
|
|
799
|
+
apply(visible);
|
|
800
|
+
};
|
|
801
|
+
widget.fileFilterVisibleChanged.connect(visibleSlot);
|
|
802
|
+
const unsubscribe = model.subscribe(() => {
|
|
803
|
+
if (model.isSearchOpen() && !widget.fileFilterVisible) {
|
|
804
|
+
widget.setFileFilterVisible(true);
|
|
805
|
+
}
|
|
806
|
+
});
|
|
807
|
+
return () => {
|
|
808
|
+
widget.fileFilterVisibleChanged.disconnect(visibleSlot);
|
|
809
|
+
unsubscribe();
|
|
810
|
+
};
|
|
811
|
+
}, [model, widget]);
|
|
747
812
|
const wrapperRef = React.useRef(null);
|
|
748
813
|
React.useEffect(() => {
|
|
749
814
|
const wrapper = wrapperRef.current;
|
|
@@ -71,6 +71,19 @@ export interface IXtralabFileBrowser {
|
|
|
71
71
|
* directories, calling `.collapse()` on each expanded one.
|
|
72
72
|
*/
|
|
73
73
|
readonly collapseAllRequested: ISignal<IXtralabFileBrowser, void>;
|
|
74
|
+
/**
|
|
75
|
+
* Whether the filter box above the tree is shown. Mirrors the default
|
|
76
|
+
* file browser's `showFileFilter`: hidden on startup and flipped by the
|
|
77
|
+
* toolbar toggle. Also forced to `true` when the tree opens a search
|
|
78
|
+
* session on its own — typing a letter while the tree has focus.
|
|
79
|
+
*/
|
|
80
|
+
readonly fileFilterVisible: boolean;
|
|
81
|
+
/**
|
|
82
|
+
* Emits when {@link fileFilterVisible} changes. The React component
|
|
83
|
+
* listens to show or hide the filter box; the toggle command listens
|
|
84
|
+
* to refresh its toggled state.
|
|
85
|
+
*/
|
|
86
|
+
readonly fileFilterVisibleChanged: ISignal<IXtralabFileBrowser, boolean>;
|
|
74
87
|
/**
|
|
75
88
|
* Trigger a refresh of every loaded directory in the tree.
|
|
76
89
|
*/
|
|
@@ -98,6 +111,14 @@ export interface IXtralabFileBrowser {
|
|
|
98
111
|
* Ask the tree to collapse every currently expanded folder.
|
|
99
112
|
*/
|
|
100
113
|
collapseAll(): void;
|
|
114
|
+
/**
|
|
115
|
+
* Show or hide the filter box above the tree.
|
|
116
|
+
*/
|
|
117
|
+
setFileFilterVisible(visible: boolean): void;
|
|
118
|
+
/**
|
|
119
|
+
* Toggle the filter box above the tree.
|
|
120
|
+
*/
|
|
121
|
+
toggleFileFilter(): void;
|
|
101
122
|
}
|
|
102
123
|
/**
|
|
103
124
|
* Lumino widget that hosts the React-based `@pierre/trees` file browser.
|
|
@@ -115,6 +136,8 @@ export declare class XtralabFileBrowser extends Widget implements IXtralabFileBr
|
|
|
115
136
|
get revealRequested(): ISignal<this, string>;
|
|
116
137
|
get rootRequested(): ISignal<this, void>;
|
|
117
138
|
get collapseAllRequested(): ISignal<this, void>;
|
|
139
|
+
get fileFilterVisible(): boolean;
|
|
140
|
+
get fileFilterVisibleChanged(): ISignal<this, boolean>;
|
|
118
141
|
/**
|
|
119
142
|
* Update the cached selection. Called from the React tree when the
|
|
120
143
|
* underlying `@pierre/trees` model emits a selection change.
|
|
@@ -125,6 +148,8 @@ export declare class XtralabFileBrowser extends Widget implements IXtralabFileBr
|
|
|
125
148
|
reveal(canonicalPath: string): void;
|
|
126
149
|
scrollToRoot(): void;
|
|
127
150
|
collapseAll(): void;
|
|
151
|
+
setFileFilterVisible(visible: boolean): void;
|
|
152
|
+
toggleFileFilter(): void;
|
|
128
153
|
private _contentsManager;
|
|
129
154
|
private _docManager;
|
|
130
155
|
private _onOpenFile;
|
|
@@ -135,6 +160,8 @@ export declare class XtralabFileBrowser extends Widget implements IXtralabFileBr
|
|
|
135
160
|
private _revealRequested;
|
|
136
161
|
private _rootRequested;
|
|
137
162
|
private _collapseAllRequested;
|
|
163
|
+
private _fileFilterVisible;
|
|
164
|
+
private _fileFilterVisibleChanged;
|
|
138
165
|
private _toolbar;
|
|
139
166
|
private _content;
|
|
140
167
|
}
|
|
@@ -29,6 +29,8 @@ export class XtralabFileBrowser extends Widget {
|
|
|
29
29
|
this._revealRequested = new Signal(this);
|
|
30
30
|
this._rootRequested = new Signal(this);
|
|
31
31
|
this._collapseAllRequested = new Signal(this);
|
|
32
|
+
this._fileFilterVisible = false;
|
|
33
|
+
this._fileFilterVisibleChanged = new Signal(this);
|
|
32
34
|
const layout = (this.layout = new PanelLayout());
|
|
33
35
|
this._contentsManager = options.contentsManager;
|
|
34
36
|
this._docManager = options.docManager;
|
|
@@ -77,6 +79,12 @@ export class XtralabFileBrowser extends Widget {
|
|
|
77
79
|
get collapseAllRequested() {
|
|
78
80
|
return this._collapseAllRequested;
|
|
79
81
|
}
|
|
82
|
+
get fileFilterVisible() {
|
|
83
|
+
return this._fileFilterVisible;
|
|
84
|
+
}
|
|
85
|
+
get fileFilterVisibleChanged() {
|
|
86
|
+
return this._fileFilterVisibleChanged;
|
|
87
|
+
}
|
|
80
88
|
/**
|
|
81
89
|
* Update the cached selection. Called from the React tree when the
|
|
82
90
|
* underlying `@pierre/trees` model emits a selection change.
|
|
@@ -103,6 +111,16 @@ export class XtralabFileBrowser extends Widget {
|
|
|
103
111
|
collapseAll() {
|
|
104
112
|
this._collapseAllRequested.emit();
|
|
105
113
|
}
|
|
114
|
+
setFileFilterVisible(visible) {
|
|
115
|
+
if (this._fileFilterVisible === visible) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
this._fileFilterVisible = visible;
|
|
119
|
+
this._fileFilterVisibleChanged.emit(visible);
|
|
120
|
+
}
|
|
121
|
+
toggleFileFilter() {
|
|
122
|
+
this.setFileFilterVisible(!this._fileFilterVisible);
|
|
123
|
+
}
|
|
106
124
|
}
|
|
107
125
|
/**
|
|
108
126
|
* The React-rendered region of the file browser. Lives inside the host
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "xtralab",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.1",
|
|
4
4
|
"description": "An opinionated JupyterLab meta-package that bundles a curated set of extensions, ships a path-first file browser, renders jupyterlab-git's text, notebook and image diffs with its own viewers, and applies a quieter default workspace configuration.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"jupyter",
|
|
@@ -6,6 +6,7 @@ import type { CommandRegistry } from '@lumino/commands';
|
|
|
6
6
|
import { Widget } from '@lumino/widgets';
|
|
7
7
|
|
|
8
8
|
const EDITOR_BREADCRUMBS_CLASS = 'jp-xtralab-EditorBreadcrumbs';
|
|
9
|
+
const TOOLBAR_SPACER_CLASS = 'jp-Toolbar-spacer';
|
|
9
10
|
const BREADCRUMBS_CONTAINER_CLASS = 'jp-BreadCrumbs-container';
|
|
10
11
|
const BREADCRUMBS_CONTENT_CLASS = 'jp-BreadCrumbs-content';
|
|
11
12
|
const ROOT_CLASS = 'jp-BreadCrumbs-home';
|
|
@@ -37,6 +38,10 @@ export class EditorBreadcrumbs extends Widget {
|
|
|
37
38
|
this._commands = options.commands;
|
|
38
39
|
this._trans = options.trans;
|
|
39
40
|
this.addClass(EDITOR_BREADCRUMBS_CLASS);
|
|
41
|
+
// The breadcrumbs stretch to fill the toolbar, so ReactiveToolbar's
|
|
42
|
+
// overflow math must count them as a spacer (2px) — measured at their
|
|
43
|
+
// stretched clientWidth they would evict every item into the popup.
|
|
44
|
+
this.addClass(TOOLBAR_SPACER_CLASS);
|
|
40
45
|
this.node.setAttribute('aria-label', this._trans.__('Editor file path'));
|
|
41
46
|
|
|
42
47
|
this._container = document.createElement('span');
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
downloadIcon,
|
|
20
20
|
editIcon,
|
|
21
21
|
fileIcon,
|
|
22
|
+
filterIcon,
|
|
22
23
|
IDisposableMenuItem,
|
|
23
24
|
newFolderIcon,
|
|
24
25
|
RankedMenu,
|
|
@@ -47,6 +48,7 @@ export namespace CommandIDs {
|
|
|
47
48
|
export const download = 'xtralab:download';
|
|
48
49
|
export const refresh = 'xtralab:refresh';
|
|
49
50
|
export const collapseAll = 'xtralab:collapse-all';
|
|
51
|
+
export const toggleFileFilter = 'xtralab:toggle-file-filter';
|
|
50
52
|
export const createNewDirectory = 'xtralab:create-new-directory';
|
|
51
53
|
export const newLauncher = 'xtralab:new-launcher';
|
|
52
54
|
export const revealPath = 'xtralab:reveal-path';
|
|
@@ -484,6 +486,24 @@ export function registerCommands(opts: IRegisterCommandsOptions): () => void {
|
|
|
484
486
|
}
|
|
485
487
|
});
|
|
486
488
|
|
|
489
|
+
commands.addCommand(CommandIDs.toggleFileFilter, {
|
|
490
|
+
label: trans.__('Toggle File Filter'),
|
|
491
|
+
caption: trans.__('Show or hide the file filter'),
|
|
492
|
+
icon: filterIcon.bindprops({ stylesheet: 'menuItem' }),
|
|
493
|
+
isToggled: () => browser.fileFilterVisible,
|
|
494
|
+
execute: () => {
|
|
495
|
+
browser.toggleFileFilter();
|
|
496
|
+
}
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
// Refresh the toggle's state however the filter is shown or hidden —
|
|
500
|
+
// the toolbar button, or the auto-show that kicks in when typing with
|
|
501
|
+
// the tree focused opens a search session.
|
|
502
|
+
const onFilterVisibleChanged = (): void => {
|
|
503
|
+
commands.notifyCommandChanged(CommandIDs.toggleFileFilter);
|
|
504
|
+
};
|
|
505
|
+
browser.fileFilterVisibleChanged.connect(onFilterVisibleChanged);
|
|
506
|
+
|
|
487
507
|
// Public reveal seam. Other plugins (editor breadcrumbs, future
|
|
488
508
|
// "reveal in tree" actions) call this command rather than depending
|
|
489
509
|
// on the file browser widget directly. Activating the sidebar is part
|
|
@@ -591,6 +611,7 @@ export function registerCommands(opts: IRegisterCommandsOptions): () => void {
|
|
|
591
611
|
return () => {
|
|
592
612
|
app.contextMenu.opened.disconnect(updateOpenWithMenu);
|
|
593
613
|
app.shell.currentChanged?.disconnect(onCurrentChanged);
|
|
614
|
+
browser.fileFilterVisibleChanged.disconnect(onFilterVisibleChanged);
|
|
594
615
|
paletteItem?.dispose();
|
|
595
616
|
};
|
|
596
617
|
}
|
|
@@ -605,13 +626,14 @@ export namespace ToolbarNames {
|
|
|
605
626
|
export const newDirectory = 'new-directory';
|
|
606
627
|
export const refresh = 'refresh';
|
|
607
628
|
export const collapseAll = 'collapse-all';
|
|
629
|
+
export const toggleFileFilter = 'toggle-file-filter';
|
|
608
630
|
}
|
|
609
631
|
|
|
610
632
|
/**
|
|
611
633
|
* Populate the file browser's toolbar with the buttons that mirror the
|
|
612
634
|
* default JupyterLab file browser: a "+" launcher button, a new-folder
|
|
613
|
-
* button,
|
|
614
|
-
*
|
|
635
|
+
* button, a refresh button, a collapse-all button, and the file filter
|
|
636
|
+
* toggle.
|
|
615
637
|
*/
|
|
616
638
|
export function populateToolbar(opts: {
|
|
617
639
|
app: JupyterFrontEnd;
|
|
@@ -652,4 +674,12 @@ export function populateToolbar(opts: {
|
|
|
652
674
|
label: ''
|
|
653
675
|
})
|
|
654
676
|
);
|
|
677
|
+
browser.toolbar.addItem(
|
|
678
|
+
ToolbarNames.toggleFileFilter,
|
|
679
|
+
new CommandToolbarButton({
|
|
680
|
+
commands,
|
|
681
|
+
id: CommandIDs.toggleFileFilter,
|
|
682
|
+
label: ''
|
|
683
|
+
})
|
|
684
|
+
);
|
|
655
685
|
}
|
|
@@ -93,10 +93,21 @@ const GIT_REPO_PATH = '';
|
|
|
93
93
|
*/
|
|
94
94
|
const FILE_TREE_TAG = 'file-tree-container';
|
|
95
95
|
|
|
96
|
+
/**
|
|
97
|
+
* Injected into the tree's shadow root. The second rule hides the search
|
|
98
|
+
* box unless the host carries the visibility marker set by the filter
|
|
99
|
+
* bridge effect below — `@pierre/trees` always renders the box when
|
|
100
|
+
* `search` is enabled, and its stylesheet lives in the shadow root where
|
|
101
|
+
* outside CSS cannot reach.
|
|
102
|
+
*/
|
|
96
103
|
const FILE_TREE_UNSAFE_CSS =
|
|
97
104
|
'[data-type="item"][data-item-selected="true"] ' +
|
|
98
105
|
'[data-item-section="spacing-item"] {' +
|
|
99
106
|
'border-left-color: transparent;' +
|
|
107
|
+
'}' +
|
|
108
|
+
':host(:not([data-xtralab-filter-visible])) ' +
|
|
109
|
+
'[data-file-tree-search-container] {' +
|
|
110
|
+
'display: none;' +
|
|
100
111
|
'}';
|
|
101
112
|
|
|
102
113
|
interface IFileBrowserProps {
|
|
@@ -748,6 +759,17 @@ export function FileBrowserComponent(
|
|
|
748
759
|
contentsManager.fileChanged.connect(onContentsFileChanged);
|
|
749
760
|
|
|
750
761
|
const unsubscribe = model.subscribe(() => {
|
|
762
|
+
// Search-driven expansion must not trigger fetches: the tree
|
|
763
|
+
// auto-expands every directory whose path matches the query, and
|
|
764
|
+
// treating those as user expansions would recursively fetch every
|
|
765
|
+
// matching subtree — a single common letter can walk the whole
|
|
766
|
+
// workspace, node_modules included. Closing the search restores
|
|
767
|
+
// the pre-search expansion state, and a directory clicked in the
|
|
768
|
+
// results is toggled after the session closes, so real expansions
|
|
769
|
+
// are still fetched the moment the session ends.
|
|
770
|
+
if (model.isSearchOpen()) {
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
751
773
|
knownDirs.forEach((state, canonicalPath) => {
|
|
752
774
|
if (state !== 'unloaded') {
|
|
753
775
|
return;
|
|
@@ -848,6 +870,48 @@ export function FileBrowserComponent(
|
|
|
848
870
|
};
|
|
849
871
|
}, [model, widget]);
|
|
850
872
|
|
|
873
|
+
// Bridge the filter-box visibility between the widget and the tree. The
|
|
874
|
+
// widget owns the flag; applying it stamps the marker attribute the
|
|
875
|
+
// unsafeCSS rule keys on and opens or closes the model's search session
|
|
876
|
+
// (opening focuses the input, closing clears any active filter). The
|
|
877
|
+
// model subscription covers the reverse direction: typing a printable
|
|
878
|
+
// character while the tree has focus opens a session on the tree's own
|
|
879
|
+
// initiative, and the box must surface for that session to be usable.
|
|
880
|
+
React.useEffect(() => {
|
|
881
|
+
if (widget === undefined) {
|
|
882
|
+
return;
|
|
883
|
+
}
|
|
884
|
+
const apply = (visible: boolean): void => {
|
|
885
|
+
const host = model.getFileTreeContainer();
|
|
886
|
+
if (host !== undefined) {
|
|
887
|
+
if (visible) {
|
|
888
|
+
host.dataset.xtralabFilterVisible = 'true';
|
|
889
|
+
} else {
|
|
890
|
+
delete host.dataset.xtralabFilterVisible;
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
if (visible && !model.isSearchOpen()) {
|
|
894
|
+
model.openSearch();
|
|
895
|
+
} else if (!visible && model.isSearchOpen()) {
|
|
896
|
+
model.closeSearch();
|
|
897
|
+
}
|
|
898
|
+
};
|
|
899
|
+
apply(widget.fileFilterVisible);
|
|
900
|
+
const visibleSlot = (sender: unknown, visible: boolean): void => {
|
|
901
|
+
apply(visible);
|
|
902
|
+
};
|
|
903
|
+
widget.fileFilterVisibleChanged.connect(visibleSlot);
|
|
904
|
+
const unsubscribe = model.subscribe(() => {
|
|
905
|
+
if (model.isSearchOpen() && !widget.fileFilterVisible) {
|
|
906
|
+
widget.setFileFilterVisible(true);
|
|
907
|
+
}
|
|
908
|
+
});
|
|
909
|
+
return () => {
|
|
910
|
+
widget.fileFilterVisibleChanged.disconnect(visibleSlot);
|
|
911
|
+
unsubscribe();
|
|
912
|
+
};
|
|
913
|
+
}, [model, widget]);
|
|
914
|
+
|
|
851
915
|
const wrapperRef = React.useRef<HTMLDivElement | null>(null);
|
|
852
916
|
|
|
853
917
|
React.useEffect(() => {
|
|
@@ -99,6 +99,21 @@ export interface IXtralabFileBrowser {
|
|
|
99
99
|
*/
|
|
100
100
|
readonly collapseAllRequested: ISignal<IXtralabFileBrowser, void>;
|
|
101
101
|
|
|
102
|
+
/**
|
|
103
|
+
* Whether the filter box above the tree is shown. Mirrors the default
|
|
104
|
+
* file browser's `showFileFilter`: hidden on startup and flipped by the
|
|
105
|
+
* toolbar toggle. Also forced to `true` when the tree opens a search
|
|
106
|
+
* session on its own — typing a letter while the tree has focus.
|
|
107
|
+
*/
|
|
108
|
+
readonly fileFilterVisible: boolean;
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Emits when {@link fileFilterVisible} changes. The React component
|
|
112
|
+
* listens to show or hide the filter box; the toggle command listens
|
|
113
|
+
* to refresh its toggled state.
|
|
114
|
+
*/
|
|
115
|
+
readonly fileFilterVisibleChanged: ISignal<IXtralabFileBrowser, boolean>;
|
|
116
|
+
|
|
102
117
|
/**
|
|
103
118
|
* Trigger a refresh of every loaded directory in the tree.
|
|
104
119
|
*/
|
|
@@ -130,6 +145,16 @@ export interface IXtralabFileBrowser {
|
|
|
130
145
|
* Ask the tree to collapse every currently expanded folder.
|
|
131
146
|
*/
|
|
132
147
|
collapseAll(): void;
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Show or hide the filter box above the tree.
|
|
151
|
+
*/
|
|
152
|
+
setFileFilterVisible(visible: boolean): void;
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Toggle the filter box above the tree.
|
|
156
|
+
*/
|
|
157
|
+
toggleFileFilter(): void;
|
|
133
158
|
}
|
|
134
159
|
|
|
135
160
|
/**
|
|
@@ -201,6 +226,14 @@ export class XtralabFileBrowser extends Widget implements IXtralabFileBrowser {
|
|
|
201
226
|
return this._collapseAllRequested;
|
|
202
227
|
}
|
|
203
228
|
|
|
229
|
+
get fileFilterVisible(): boolean {
|
|
230
|
+
return this._fileFilterVisible;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
get fileFilterVisibleChanged(): ISignal<this, boolean> {
|
|
234
|
+
return this._fileFilterVisibleChanged;
|
|
235
|
+
}
|
|
236
|
+
|
|
204
237
|
/**
|
|
205
238
|
* Update the cached selection. Called from the React tree when the
|
|
206
239
|
* underlying `@pierre/trees` model emits a selection change.
|
|
@@ -235,6 +268,18 @@ export class XtralabFileBrowser extends Widget implements IXtralabFileBrowser {
|
|
|
235
268
|
this._collapseAllRequested.emit();
|
|
236
269
|
}
|
|
237
270
|
|
|
271
|
+
setFileFilterVisible(visible: boolean): void {
|
|
272
|
+
if (this._fileFilterVisible === visible) {
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
this._fileFilterVisible = visible;
|
|
276
|
+
this._fileFilterVisibleChanged.emit(visible);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
toggleFileFilter(): void {
|
|
280
|
+
this.setFileFilterVisible(!this._fileFilterVisible);
|
|
281
|
+
}
|
|
282
|
+
|
|
238
283
|
private _contentsManager: Contents.IManager;
|
|
239
284
|
private _docManager: IDocumentManager;
|
|
240
285
|
private _onOpenFile: ((serverPath: string) => void) | undefined;
|
|
@@ -245,6 +290,8 @@ export class XtralabFileBrowser extends Widget implements IXtralabFileBrowser {
|
|
|
245
290
|
private _revealRequested = new Signal<this, string>(this);
|
|
246
291
|
private _rootRequested = new Signal<this, void>(this);
|
|
247
292
|
private _collapseAllRequested = new Signal<this, void>(this);
|
|
293
|
+
private _fileFilterVisible = false;
|
|
294
|
+
private _fileFilterVisibleChanged = new Signal<this, boolean>(this);
|
|
248
295
|
private _toolbar: Toolbar;
|
|
249
296
|
private _content: XtralabFileTreeContent;
|
|
250
297
|
}
|
package/style/base.css
CHANGED
|
@@ -147,7 +147,7 @@
|
|
|
147
147
|
padding: 2px 6px;
|
|
148
148
|
background-color: var(--jp-layout-color1);
|
|
149
149
|
border: 1px solid var(--jp-border-color2);
|
|
150
|
-
border-radius:
|
|
150
|
+
border-radius: 6px;
|
|
151
151
|
color: var(--jp-ui-font-color1);
|
|
152
152
|
font-family: var(--jp-ui-font-family);
|
|
153
153
|
font-size: var(--jp-ui-font-size1);
|