xtralab 0.2.0 → 0.4.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 +126 -125
- package/lib/editorBreadcrumbs/index.d.ts +11 -0
- package/lib/editorBreadcrumbs/index.js +41 -0
- package/lib/editorBreadcrumbs/widget.d.ts +34 -0
- package/lib/editorBreadcrumbs/widget.js +131 -0
- package/lib/editorIndent/index.d.ts +17 -0
- package/lib/editorIndent/index.js +142 -0
- package/lib/fileBrowser/commands.d.ts +5 -2
- package/lib/fileBrowser/commands.js +42 -3
- package/lib/fileBrowser/fileBrowser.js +394 -11
- package/lib/fileBrowser/gitStatus.d.ts +13 -0
- package/lib/fileBrowser/gitStatus.js +60 -0
- package/lib/fileBrowser/icons.d.ts +4 -4
- package/lib/fileBrowser/icons.js +4 -4
- package/lib/fileBrowser/widget.d.ts +50 -2
- package/lib/fileBrowser/widget.js +26 -2
- package/lib/git/icons.d.ts +1 -1
- package/lib/git/icons.js +1 -1
- package/lib/git/panelComponent.js +28 -11
- package/lib/index.js +4 -0
- package/lib/launcher/dashboard.js +78 -16
- package/package.json +22 -2
- package/src/editorBreadcrumbs/index.ts +70 -0
- package/src/editorBreadcrumbs/widget.ts +158 -0
- package/src/editorIndent/index.ts +177 -0
- package/src/fileBrowser/commands.ts +45 -3
- package/src/fileBrowser/fileBrowser.tsx +428 -12
- package/src/fileBrowser/gitStatus.ts +66 -0
- package/src/fileBrowser/icons.ts +4 -4
- package/src/fileBrowser/widget.tsx +80 -3
- package/src/git/icons.ts +1 -1
- package/src/git/panelComponent.tsx +29 -15
- package/src/index.ts +4 -0
- package/src/launcher/dashboard.tsx +135 -16
- package/style/editorBreadcrumbs.css +86 -0
- package/style/index.css +2 -0
- package/style/index.js +1 -0
- package/style/launcher.css +0 -5
- package/style/tabs.css +66 -0
package/README.md
CHANGED
|
@@ -2,153 +2,154 @@
|
|
|
2
2
|
|
|
3
3
|
An opinionated JupyterLab meta-package.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
changes panel, and applies a quieter default workspace configuration. The
|
|
8
|
-
defaults are shipped under `etc/jupyter/labconfig` and the bundled
|
|
9
|
-
labextension is shipped under `share/jupyter/labextensions/xtralab`.
|
|
5
|
+
Bundles a curated set of extensions, a path-first file browser, a VS Code-style
|
|
6
|
+
git changes panel, an agent-focused launcher, and a quieter default workspace.
|
|
10
7
|
|
|
11
|
-
|
|
12
|
-
upstream Jupyter packages, while `xtralab` defines the default environment we
|
|
13
|
-
want out of the box.
|
|
8
|
+

|
|
14
9
|
|
|
15
|
-
##
|
|
10
|
+
## Install
|
|
16
11
|
|
|
17
|
-
|
|
12
|
+
```bash
|
|
13
|
+
pip install xtralab
|
|
14
|
+
```
|
|
18
15
|
|
|
19
|
-
|
|
20
|
-
- JupyterLab 4.6+
|
|
21
|
-
- Git server integration (`jupyterlab-git` — backs the bundled changes panel)
|
|
22
|
-
- Quick file opening (`jupyterlab-quickopen`)
|
|
23
|
-
- Cursor styling helpers (`jupyterlab-cursor-light`, `jupyterlab-cursor-theme`)
|
|
24
|
-
- Day and night themes (`jupyterlab-day`, `jupyterlab-night`)
|
|
25
|
-
|
|
26
|
-
These are installed as normal Python dependencies, so deployments can still
|
|
27
|
-
override versions and Jupyter configuration in the usual ways.
|
|
28
|
-
|
|
29
|
-
## Bundled labextension
|
|
30
|
-
|
|
31
|
-
`xtralab` ships its own prebuilt labextension that contributes:
|
|
32
|
-
|
|
33
|
-
- A path-first file browser in the left sidebar, built on `@pierre/trees`.
|
|
34
|
-
- A VS Code-style "Source Control" panel in the left sidebar, powered by the
|
|
35
|
-
`jupyterlab-git` server REST API and `@pierre/diffs`. The bundled
|
|
36
|
-
`@jupyterlab/git` frontend is disabled automatically so the two panels do
|
|
37
|
-
not coexist.
|
|
38
|
-
- An agent-focused launcher that replaces the stock JupyterLab launcher.
|
|
39
|
-
The launcher tab shows an optional initial-prompt textarea, a row of
|
|
40
|
-
agent buttons (Claude, Codex, Gemini, Copilot, Goose, OpenCode, Kiro,
|
|
41
|
-
and Mistral Vibe), and a collapsible list of changed files (each
|
|
42
|
-
clickable into the diff viewer) below them. Clicking an agent opens a
|
|
43
|
-
fresh terminal and runs the agent's CLI; if the prompt textarea is
|
|
44
|
-
non-empty, the prompt is shell-quoted and spliced into the launch line
|
|
45
|
-
for agents that accept one. Agents are filtered by a `which`-based
|
|
46
|
-
availability check at activation, so only the ones actually installed
|
|
47
|
-
on the machine show up.
|
|
48
|
-
|
|
49
|
-
### Customizing the launcher
|
|
50
|
-
|
|
51
|
-
The launcher reads `xtralab:launcher` settings from the JupyterLab settings
|
|
52
|
-
registry. Open the Settings Editor (`Settings → Settings Editor → xtralab
|
|
53
|
-
launcher`) and edit the `agents` array; xtralab merges your entries with
|
|
54
|
-
the defaults by `id`.
|
|
16
|
+
## Usage
|
|
55
17
|
|
|
56
|
-
|
|
57
|
-
{
|
|
58
|
-
"agents": [
|
|
59
|
-
// Hide an agent you never want to see, even if it's installed:
|
|
60
|
-
{ "id": "kiro", "enabled": false },
|
|
18
|
+
### As a JupyterLab package
|
|
61
19
|
|
|
62
|
-
|
|
63
|
-
// `requireAvailable: false` keeps the card visible even though
|
|
64
|
-
// `which cl` returns nothing (interactive shells expand the alias).
|
|
65
|
-
{ "id": "claude", "command": "cl", "requireAvailable": false },
|
|
20
|
+
Run JupyterLab the usual way:
|
|
66
21
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
// launcher's prompt textarea is appended as a positional argument.
|
|
70
|
-
{
|
|
71
|
-
"id": "aider",
|
|
72
|
-
"label": "Aider",
|
|
73
|
-
"command": "aider",
|
|
74
|
-
"promptArgs": []
|
|
75
|
-
}
|
|
76
|
-
]
|
|
77
|
-
}
|
|
22
|
+
```bash
|
|
23
|
+
jupyter lab
|
|
78
24
|
```
|
|
79
25
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
| Field | Effect |
|
|
83
|
-
| ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
84
|
-
| `id` | Built-in id to override, or a new id to define a new card. |
|
|
85
|
-
| `label` | Card title. |
|
|
86
|
-
| `caption` | Subtitle shown beneath the title. |
|
|
87
|
-
| `command` | Literal text typed into the new terminal. |
|
|
88
|
-
| `iconSvg` | Inline SVG for the card icon (only needed for new agents). |
|
|
89
|
-
| `rank` | Sort order on the launcher; lower comes first. |
|
|
90
|
-
| `enabled` | `false` hides the card from the launcher and palette. |
|
|
91
|
-
| `requireAvailable` | `false` skips the `which` check (use for shell aliases that aren't on PATH but expand at runtime). |
|
|
92
|
-
| `promptArgs` | How to splice the launcher's optional prompt into the command. `[]` appends it as a positional argument (`<command> 'PROMPT'`); `["-i"]` or `["--prompt"]` puts it after a flag. Set to `null` to opt out — the agent button is dimmed when the prompt textarea is non-empty so a typed prompt is never silently dropped. Defaults vary per agent. |
|
|
93
|
-
|
|
94
|
-
## Default settings
|
|
95
|
-
|
|
96
|
-
`xtralab` ships the following JupyterLab defaults:
|
|
97
|
-
|
|
98
|
-
- The announcements plugin is disabled, so JupyterLab does not prompt to fetch
|
|
99
|
-
news from the Jupyter news feed and does not check for application updates.
|
|
100
|
-
- The Table of Contents, Debugger, and Notebook Tools panels are disabled so
|
|
101
|
-
they do not show up in the right sidebar by default.
|
|
102
|
-
- `dockPanelPadding` is off, so the main dock area is flush with the
|
|
103
|
-
surrounding chrome.
|
|
104
|
-
- The terminal `fontFamily` defaults to `MesloLGS NF, ui-monospace, monospace`
|
|
105
|
-
so Powerline / Nerd Font glyphs (used by Oh My Zsh themes such as
|
|
106
|
-
`powerlevel10k`) render correctly when the font is installed; the
|
|
107
|
-
`ui-monospace, monospace` fallback keeps things readable when it is not.
|
|
108
|
-
|
|
109
|
-
The JupyterLab frontend defaults are shipped as `labconfig/*.d/00-xtralab.json`
|
|
110
|
-
fragments so downstream meta-packages can add their own Lab configuration
|
|
111
|
-
fragments without replacing `xtralab`'s files.
|
|
26
|
+
### As a desktop app
|
|
112
27
|
|
|
113
|
-
|
|
28
|
+
A standalone Electron build (DMG on macOS, AppImage on Linux) ships with each
|
|
29
|
+
tagged release — grab the installer for your platform from the
|
|
30
|
+
[Releases page](https://github.com/jtpio/xtralab/releases/latest). Builds from
|
|
31
|
+
the current `main` branch are also produced on every push as workflow
|
|
32
|
+
artifacts under the repository's
|
|
33
|
+
[Actions tab](https://github.com/jtpio/xtralab/actions). See
|
|
34
|
+
[CONTRIBUTING.md](./CONTRIBUTING.md) for the architecture and local build
|
|
35
|
+
instructions.
|
|
114
36
|
|
|
115
|
-
|
|
116
|
-
pip install xtralab
|
|
117
|
-
```
|
|
37
|
+
## What's included
|
|
118
38
|
|
|
119
|
-
|
|
39
|
+
- [`ajlab`](https://github.com/jtpio/ajlab) — agent-ready JupyterLab base
|
|
40
|
+
- JupyterLab 4.6+
|
|
41
|
+
- `jupyterlab-git` — backs the bundled changes panel
|
|
42
|
+
- `jupyterlab-lsp` + `ty` — Python LSP via Astral's `ty` (bundled); also
|
|
43
|
+
detects `typescript-language-server` on `PATH` for JS/TS
|
|
44
|
+
- `jupyterlab-quickopen`
|
|
45
|
+
- `jupyterlab-cursor-light`, `jupyterlab-cursor-dark`
|
|
46
|
+
- `jupyterlab-day`, `jupyterlab-night` themes
|
|
47
|
+
|
|
48
|
+
The bundled labextension adds:
|
|
49
|
+
|
|
50
|
+
- A path-first file browser in the left sidebar.
|
|
51
|
+
- A "Source Control" panel powered by `jupyterlab-git` and `@pierre/diffs`.
|
|
52
|
+
- An agent launcher with a prompt textarea, a row of agent buttons (Claude,
|
|
53
|
+
Codex, Gemini, Copilot, Goose, OpenCode, Kiro, Mistral Vibe), and a
|
|
54
|
+
collapsible list of changed files. Buttons are filtered to agents installed
|
|
55
|
+
on the machine; a typed prompt is shell-quoted and spliced into the launch
|
|
56
|
+
command for agents that accept one.
|
|
57
|
+
|
|
58
|
+
## Language servers
|
|
59
|
+
|
|
60
|
+
xtralab ships with
|
|
61
|
+
[`jupyterlab-lsp`](https://github.com/jupyter-lsp/jupyterlab-lsp) and
|
|
62
|
+
pre-registers two language servers through
|
|
63
|
+
`jupyter_server_config.d/xtralab-lsp.json`:
|
|
64
|
+
|
|
65
|
+
- **Python — [`ty`](https://github.com/astral-sh/ty)** — installed as a
|
|
66
|
+
Python dependency. Works out of the box.
|
|
67
|
+
- **TypeScript / JavaScript — `typescript-language-server`** — install
|
|
68
|
+
yourself:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
npm install -g typescript-language-server typescript
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Restart JupyterLab (or the desktop app) after installing.
|
|
75
|
+
|
|
76
|
+
Specs use bare command names (`["ty", "server"]`), so binaries are resolved
|
|
77
|
+
from `PATH` at spawn time.
|
|
78
|
+
|
|
79
|
+
### Where binaries are discovered
|
|
80
|
+
|
|
81
|
+
- **`pip install xtralab`** — anything on the JupyterLab process's `PATH`.
|
|
82
|
+
- **Desktop app** — the supervisor augments `PATH` with common shim
|
|
83
|
+
locations (`~/.volta/bin`, `~/.npm-global/bin`, `~/.bun/bin`,
|
|
84
|
+
`~/.asdf/shims`, `~/.mise/shims`, `/opt/homebrew/bin`, `/usr/local/bin`,
|
|
85
|
+
…). Set `XTRALAB_EXTRA_PATH` (colon-separated) before launching the app
|
|
86
|
+
to add directories the defaults miss.
|
|
87
|
+
|
|
88
|
+
### Adding more servers
|
|
89
|
+
|
|
90
|
+
To enable another server (bash, yaml, json, dockerfile, pyright, …),
|
|
91
|
+
install the binary then drop a JSON file into a `jupyter_server_config.d/`
|
|
92
|
+
directory:
|
|
93
|
+
|
|
94
|
+
- **`pip install xtralab`** — run `jupyter --paths` and pick a config dir
|
|
95
|
+
(typically `~/.jupyter/jupyter_server_config.d/`).
|
|
96
|
+
- **Desktop app (macOS)** —
|
|
97
|
+
`~/Library/Application Support/xtralab/jupyter/config/jupyter_server_config.d/`
|
|
98
|
+
(or `xtralab dev` for local dev builds).
|
|
99
|
+
- **Desktop app (Linux AppImage)** —
|
|
100
|
+
`~/.config/xtralab/jupyter/config/jupyter_server_config.d/`.
|
|
101
|
+
|
|
102
|
+
Example (`bash-lsp.json`):
|
|
103
|
+
|
|
104
|
+
```json
|
|
105
|
+
{
|
|
106
|
+
"LanguageServerManager": {
|
|
107
|
+
"language_servers": {
|
|
108
|
+
"bash-language-server": {
|
|
109
|
+
"version": 2,
|
|
110
|
+
"argv": ["bash-language-server", "start"],
|
|
111
|
+
"languages": ["bash", "sh"],
|
|
112
|
+
"mime_types": ["text/x-sh", "application/x-sh"],
|
|
113
|
+
"display_name": "bash-language-server"
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
```
|
|
120
119
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
120
|
+
Pair with `npm install -g bash-language-server`. Reuse a bundled `key` to
|
|
121
|
+
override it. See
|
|
122
|
+
[jupyterlab-lsp's docs](https://jupyterlab-lsp.readthedocs.io/en/latest/Configuring.html)
|
|
123
|
+
for the full spec schema.
|
|
124
124
|
|
|
125
|
-
|
|
125
|
+
## Customizing the launcher
|
|
126
126
|
|
|
127
|
-
|
|
127
|
+
Open `Settings → Settings Editor → xtralab launcher` and edit the `agents`
|
|
128
|
+
array. Entries are merged with the defaults by `id`.
|
|
128
129
|
|
|
129
|
-
```
|
|
130
|
-
|
|
131
|
-
|
|
130
|
+
```jsonc
|
|
131
|
+
{
|
|
132
|
+
"agents": [
|
|
133
|
+
// Hide an agent
|
|
134
|
+
{ "id": "kiro", "enabled": false },
|
|
132
135
|
|
|
133
|
-
|
|
136
|
+
// Override an agent's command (e.g. point Claude at a shell alias)
|
|
137
|
+
{ "id": "claude", "command": "cl", "requireAvailable": false },
|
|
134
138
|
|
|
135
|
-
|
|
136
|
-
|
|
139
|
+
// Add a new agent; promptArgs: [] appends the prompt as a positional arg
|
|
140
|
+
{ "id": "aider", "label": "Aider", "command": "aider", "promptArgs": [] }
|
|
141
|
+
]
|
|
142
|
+
}
|
|
137
143
|
```
|
|
138
144
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
shuts everything down cleanly.
|
|
145
|
+
Fields: `id` (required), `label`, `caption`, `command`, `promptArgs` (how to
|
|
146
|
+
splice the prompt — `[]` for positional, `["--flag"]` for flagged, `null` to
|
|
147
|
+
opt out), `iconSvg`, `rank`, `enabled`, `requireAvailable`.
|
|
143
148
|
|
|
144
|
-
##
|
|
149
|
+
## Contributing
|
|
145
150
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
uv pip install -e ".[dev]"
|
|
149
|
-
jlpm
|
|
150
|
-
jlpm build
|
|
151
|
-
```
|
|
151
|
+
See [CONTRIBUTING.md](./CONTRIBUTING.md) for the development setup, the
|
|
152
|
+
Electron desktop app architecture, and the build pipeline.
|
|
152
153
|
|
|
153
154
|
## License
|
|
154
155
|
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { JupyterFrontEndPlugin } from '@jupyterlab/application';
|
|
2
|
+
/**
|
|
3
|
+
* Adds a VS Code-style path breadcrumb to text editor toolbars. Each
|
|
4
|
+
* segment is clickable: clicking dispatches `xtralab:reveal-path` so
|
|
5
|
+
* any plugin that listens (today, the file browser) can surface the
|
|
6
|
+
* underlying folder or file. The breadcrumbs plugin therefore does not
|
|
7
|
+
* import the file browser at all — the JupyterLab command registry is
|
|
8
|
+
* the only seam between them.
|
|
9
|
+
*/
|
|
10
|
+
declare const plugin: JupyterFrontEndPlugin<void>;
|
|
11
|
+
export default plugin;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { DisposableDelegate } from '@lumino/disposable';
|
|
2
|
+
import { EditorBreadcrumbs } from './widget';
|
|
3
|
+
const PLUGIN_ID = 'xtralab:editor-breadcrumbs';
|
|
4
|
+
const EDITOR_FACTORY = 'Editor';
|
|
5
|
+
const TOOLBAR_ITEM_NAME = 'xtralab-editor-breadcrumbs';
|
|
6
|
+
class EditorBreadcrumbsExtension {
|
|
7
|
+
constructor(commands) {
|
|
8
|
+
this._commands = commands;
|
|
9
|
+
}
|
|
10
|
+
createNew(widget, context) {
|
|
11
|
+
const breadcrumbs = new EditorBreadcrumbs({
|
|
12
|
+
context,
|
|
13
|
+
commands: this._commands
|
|
14
|
+
});
|
|
15
|
+
const added = widget.toolbar.insertItem(0, TOOLBAR_ITEM_NAME, breadcrumbs);
|
|
16
|
+
if (!added) {
|
|
17
|
+
breadcrumbs.dispose();
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
return new DisposableDelegate(() => {
|
|
21
|
+
breadcrumbs.dispose();
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Adds a VS Code-style path breadcrumb to text editor toolbars. Each
|
|
27
|
+
* segment is clickable: clicking dispatches `xtralab:reveal-path` so
|
|
28
|
+
* any plugin that listens (today, the file browser) can surface the
|
|
29
|
+
* underlying folder or file. The breadcrumbs plugin therefore does not
|
|
30
|
+
* import the file browser at all — the JupyterLab command registry is
|
|
31
|
+
* the only seam between them.
|
|
32
|
+
*/
|
|
33
|
+
const plugin = {
|
|
34
|
+
id: PLUGIN_ID,
|
|
35
|
+
description: 'Display the active file path in text editor toolbars.',
|
|
36
|
+
autoStart: true,
|
|
37
|
+
activate: (app) => {
|
|
38
|
+
app.docRegistry.addWidgetExtension(EDITOR_FACTORY, new EditorBreadcrumbsExtension(app.commands));
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
export default plugin;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { DocumentRegistry } from '@jupyterlab/docregistry';
|
|
2
|
+
import type { CommandRegistry } from '@lumino/commands';
|
|
3
|
+
import { Widget } from '@lumino/widgets';
|
|
4
|
+
export interface IEditorBreadcrumbsOptions {
|
|
5
|
+
context: DocumentRegistry.IContext<DocumentRegistry.IModel>;
|
|
6
|
+
commands: CommandRegistry;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Read-only breadcrumb path for file editor toolbars.
|
|
10
|
+
*
|
|
11
|
+
* The upstream `@jupyterlab/filebrowser` BreadCrumbs widget is coupled to
|
|
12
|
+
* FileBrowserModel navigation, drag/drop, and path editing. If that code
|
|
13
|
+
* becomes reusable upstream, replace this local display-only renderer with
|
|
14
|
+
* the shared implementation.
|
|
15
|
+
*/
|
|
16
|
+
export declare class EditorBreadcrumbs extends Widget {
|
|
17
|
+
constructor(options: IEditorBreadcrumbsOptions);
|
|
18
|
+
dispose(): void;
|
|
19
|
+
private _onPathChanged;
|
|
20
|
+
private _render;
|
|
21
|
+
private _createRootCrumb;
|
|
22
|
+
/**
|
|
23
|
+
* Bind click and keyboard activation on a crumb element to the
|
|
24
|
+
* shared reveal command. Dispatch is best-effort: a failure to
|
|
25
|
+
* resolve or execute the command is logged but never thrown into
|
|
26
|
+
* the editor's toolbar.
|
|
27
|
+
*/
|
|
28
|
+
private _attachReveal;
|
|
29
|
+
private _createSeparator;
|
|
30
|
+
private _context;
|
|
31
|
+
private _commands;
|
|
32
|
+
private _container;
|
|
33
|
+
private _content;
|
|
34
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { PageConfig } from '@jupyterlab/coreutils';
|
|
2
|
+
import { homeIcon as rootIcon } from '@jupyterlab/ui-components';
|
|
3
|
+
import { Widget } from '@lumino/widgets';
|
|
4
|
+
const EDITOR_BREADCRUMBS_CLASS = 'jp-xtralab-EditorBreadcrumbs';
|
|
5
|
+
const BREADCRUMBS_CONTAINER_CLASS = 'jp-BreadCrumbs-container';
|
|
6
|
+
const BREADCRUMBS_CONTENT_CLASS = 'jp-BreadCrumbs-content';
|
|
7
|
+
const ROOT_CLASS = 'jp-BreadCrumbs-home';
|
|
8
|
+
const ITEM_CLASS = 'jp-BreadCrumbs-item';
|
|
9
|
+
const CURRENT_ITEM_CLASS = 'jp-mod-current';
|
|
10
|
+
const SEPARATOR_CLASS = 'jp-BreadCrumbs-separator';
|
|
11
|
+
/**
|
|
12
|
+
* Command dispatched when the user clicks a breadcrumb segment.
|
|
13
|
+
* Registered by the file browser plugin; the breadcrumbs only know
|
|
14
|
+
* the name so they stay decoupled from the file browser internals.
|
|
15
|
+
*/
|
|
16
|
+
const REVEAL_COMMAND = 'xtralab:reveal-path';
|
|
17
|
+
/**
|
|
18
|
+
* Read-only breadcrumb path for file editor toolbars.
|
|
19
|
+
*
|
|
20
|
+
* The upstream `@jupyterlab/filebrowser` BreadCrumbs widget is coupled to
|
|
21
|
+
* FileBrowserModel navigation, drag/drop, and path editing. If that code
|
|
22
|
+
* becomes reusable upstream, replace this local display-only renderer with
|
|
23
|
+
* the shared implementation.
|
|
24
|
+
*/
|
|
25
|
+
export class EditorBreadcrumbs extends Widget {
|
|
26
|
+
constructor(options) {
|
|
27
|
+
super({ node: document.createElement('nav') });
|
|
28
|
+
this._context = options.context;
|
|
29
|
+
this._commands = options.commands;
|
|
30
|
+
this.addClass(EDITOR_BREADCRUMBS_CLASS);
|
|
31
|
+
this.node.setAttribute('aria-label', 'Editor file path');
|
|
32
|
+
this._container = document.createElement('span');
|
|
33
|
+
this._container.className = BREADCRUMBS_CONTAINER_CLASS;
|
|
34
|
+
this._content = document.createElement('span');
|
|
35
|
+
this._content.className = BREADCRUMBS_CONTENT_CLASS;
|
|
36
|
+
this._container.appendChild(this._content);
|
|
37
|
+
this.node.appendChild(this._container);
|
|
38
|
+
this._context.pathChanged.connect(this._onPathChanged, this);
|
|
39
|
+
this._render();
|
|
40
|
+
}
|
|
41
|
+
dispose() {
|
|
42
|
+
if (this.isDisposed) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
this._context.pathChanged.disconnect(this._onPathChanged, this);
|
|
46
|
+
super.dispose();
|
|
47
|
+
}
|
|
48
|
+
_onPathChanged() {
|
|
49
|
+
this._render();
|
|
50
|
+
}
|
|
51
|
+
_render() {
|
|
52
|
+
const path = this._context.localPath || this._context.path;
|
|
53
|
+
const parts = path.split('/').filter(part => part.length > 0);
|
|
54
|
+
this.node.title = this._context.path;
|
|
55
|
+
this._content.textContent = '';
|
|
56
|
+
this._content.appendChild(this._createRootCrumb());
|
|
57
|
+
if (parts.length > 0) {
|
|
58
|
+
this._content.appendChild(this._createSeparator());
|
|
59
|
+
}
|
|
60
|
+
parts.forEach((part, index) => {
|
|
61
|
+
const isLast = index === parts.length - 1;
|
|
62
|
+
const subPath = parts.slice(0, index + 1).join('/');
|
|
63
|
+
// Canonical `@pierre/trees` paths: directories carry a trailing
|
|
64
|
+
// slash, files do not. Every part except the last names a folder
|
|
65
|
+
// along the way to the open file.
|
|
66
|
+
const canonical = isLast ? subPath : `${subPath}/`;
|
|
67
|
+
const item = document.createElement('span');
|
|
68
|
+
item.className = ITEM_CLASS;
|
|
69
|
+
item.textContent = part;
|
|
70
|
+
item.title = subPath;
|
|
71
|
+
item.tabIndex = 0;
|
|
72
|
+
item.setAttribute('role', 'button');
|
|
73
|
+
if (isLast) {
|
|
74
|
+
item.classList.add(CURRENT_ITEM_CLASS);
|
|
75
|
+
item.setAttribute('aria-current', 'page');
|
|
76
|
+
}
|
|
77
|
+
this._attachReveal(item, canonical);
|
|
78
|
+
this._content.appendChild(item);
|
|
79
|
+
if (!isLast) {
|
|
80
|
+
this._content.appendChild(this._createSeparator());
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
_createRootCrumb() {
|
|
85
|
+
const item = rootIcon.element({
|
|
86
|
+
className: ROOT_CLASS,
|
|
87
|
+
tag: 'span',
|
|
88
|
+
title: PageConfig.getOption('serverRoot') || 'Jupyter Server Root',
|
|
89
|
+
stylesheet: 'breadCrumb'
|
|
90
|
+
});
|
|
91
|
+
item.dataset.path = '/';
|
|
92
|
+
item.tabIndex = 0;
|
|
93
|
+
item.setAttribute('role', 'button');
|
|
94
|
+
// Empty canonical path: the reveal command treats this as the
|
|
95
|
+
// workspace-root gesture — surface the file browser, clear its
|
|
96
|
+
// selection, and scroll back to the top. The tree has no row for
|
|
97
|
+
// the workspace root itself.
|
|
98
|
+
this._attachReveal(item, '');
|
|
99
|
+
return item;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Bind click and keyboard activation on a crumb element to the
|
|
103
|
+
* shared reveal command. Dispatch is best-effort: a failure to
|
|
104
|
+
* resolve or execute the command is logged but never thrown into
|
|
105
|
+
* the editor's toolbar.
|
|
106
|
+
*/
|
|
107
|
+
_attachReveal(element, canonicalPath) {
|
|
108
|
+
const fire = (event) => {
|
|
109
|
+
event.preventDefault();
|
|
110
|
+
event.stopPropagation();
|
|
111
|
+
void this._commands
|
|
112
|
+
.execute(REVEAL_COMMAND, { path: canonicalPath })
|
|
113
|
+
.catch((err) => {
|
|
114
|
+
console.error(`xtralab: failed to reveal "${canonicalPath}"`, err);
|
|
115
|
+
});
|
|
116
|
+
};
|
|
117
|
+
element.addEventListener('click', fire);
|
|
118
|
+
element.addEventListener('keydown', event => {
|
|
119
|
+
if (event.key === 'Enter' || event.key === ' ') {
|
|
120
|
+
fire(event);
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
_createSeparator() {
|
|
125
|
+
const item = document.createElement('span');
|
|
126
|
+
item.className = SEPARATOR_CLASS;
|
|
127
|
+
item.setAttribute('aria-hidden', 'true');
|
|
128
|
+
item.textContent = '/';
|
|
129
|
+
return item;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { JupyterFrontEndPlugin } from '@jupyterlab/application';
|
|
2
|
+
/**
|
|
3
|
+
* Sniff each opened document's leading whitespace and configure CodeMirror's
|
|
4
|
+
* `indentUnit` / `tabSize` to match. JupyterLab only exposes a single global
|
|
5
|
+
* indent setting, which means TypeScript files written with 4-space indent and
|
|
6
|
+
* Python files written with 2-space indent both look wrong in the same editor;
|
|
7
|
+
* detecting from the file's own content sidesteps that without forcing every
|
|
8
|
+
* project to share the same convention.
|
|
9
|
+
*
|
|
10
|
+
* Detection runs at editor-construction time and again on the first content
|
|
11
|
+
* change — that second pass exists because JupyterLab's collaborative document
|
|
12
|
+
* loader can settle the file body into the shared model after the editor view
|
|
13
|
+
* already exists, in which case the factory would otherwise have only seen an
|
|
14
|
+
* empty string.
|
|
15
|
+
*/
|
|
16
|
+
declare const plugin: JupyterFrontEndPlugin<void>;
|
|
17
|
+
export default plugin;
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { EditorExtensionRegistry, IEditorExtensionRegistry } from '@jupyterlab/codemirror';
|
|
2
|
+
import { indentUnit } from '@codemirror/language';
|
|
3
|
+
import { Compartment, EditorState, Prec } from '@codemirror/state';
|
|
4
|
+
import { ViewPlugin } from '@codemirror/view';
|
|
5
|
+
import detectIndent from 'detect-indent';
|
|
6
|
+
const PLUGIN_ID = 'xtralab:editor-indent';
|
|
7
|
+
/**
|
|
8
|
+
* Mime types whose newly-created (or too-short-to-detect) files should default
|
|
9
|
+
* to a 2-space indent. Matches Prettier's defaults for web languages; mime
|
|
10
|
+
* types not listed here inherit whatever the user has set in JupyterLab.
|
|
11
|
+
* Covers .ts/.tsx/.jsx/.js variants registered by `@jupyterlab/codemirror`.
|
|
12
|
+
*/
|
|
13
|
+
const FALLBACK_INDENT_BY_MIME = {
|
|
14
|
+
'application/ecmascript': 2,
|
|
15
|
+
'application/javascript': 2,
|
|
16
|
+
'application/json': 2,
|
|
17
|
+
'application/typescript': 2,
|
|
18
|
+
'application/x-javascript': 2,
|
|
19
|
+
'application/x-json': 2,
|
|
20
|
+
'application/x-yaml': 2,
|
|
21
|
+
'text/css': 2,
|
|
22
|
+
'text/ecmascript': 2,
|
|
23
|
+
'text/html': 2,
|
|
24
|
+
'text/javascript': 2,
|
|
25
|
+
'text/jsx': 2,
|
|
26
|
+
'text/typescript-jsx': 2,
|
|
27
|
+
'text/x-markdown': 2,
|
|
28
|
+
'text/x-yaml': 2,
|
|
29
|
+
'text/yaml': 2
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Run `detect-indent` against the file's content and convert its result into
|
|
33
|
+
* something we can hand to CodeMirror.
|
|
34
|
+
*/
|
|
35
|
+
function detectFromContent(text) {
|
|
36
|
+
if (text.length === 0) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
const result = detectIndent(text);
|
|
40
|
+
if (!result.type || result.amount <= 0) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
if (result.type === 'tab') {
|
|
44
|
+
// detect-indent returns `\t` for `indent` in tab mode; use a 4-column tab
|
|
45
|
+
// as the visual width, which is what most language servers assume.
|
|
46
|
+
return { unit: '\t', width: 4 };
|
|
47
|
+
}
|
|
48
|
+
return { unit: result.indent, width: result.amount };
|
|
49
|
+
}
|
|
50
|
+
function resolveIndent(text, mimeType) {
|
|
51
|
+
const detected = detectFromContent(text);
|
|
52
|
+
if (detected) {
|
|
53
|
+
return detected;
|
|
54
|
+
}
|
|
55
|
+
const fallback = FALLBACK_INDENT_BY_MIME[mimeType];
|
|
56
|
+
if (fallback === undefined) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
return { unit: ' '.repeat(fallback), width: fallback };
|
|
60
|
+
}
|
|
61
|
+
function buildExtension(resolved) {
|
|
62
|
+
// `Prec.highest` keeps these values first in their respective facets so
|
|
63
|
+
// they beat both `defaultConfig` and per-editor `editorConfig.indentUnit`,
|
|
64
|
+
// both of which JupyterLab registers at default precedence.
|
|
65
|
+
return Prec.highest([
|
|
66
|
+
indentUnit.of(resolved.unit),
|
|
67
|
+
EditorState.tabSize.of(resolved.width)
|
|
68
|
+
]);
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Sniff each opened document's leading whitespace and configure CodeMirror's
|
|
72
|
+
* `indentUnit` / `tabSize` to match. JupyterLab only exposes a single global
|
|
73
|
+
* indent setting, which means TypeScript files written with 4-space indent and
|
|
74
|
+
* Python files written with 2-space indent both look wrong in the same editor;
|
|
75
|
+
* detecting from the file's own content sidesteps that without forcing every
|
|
76
|
+
* project to share the same convention.
|
|
77
|
+
*
|
|
78
|
+
* Detection runs at editor-construction time and again on the first content
|
|
79
|
+
* change — that second pass exists because JupyterLab's collaborative document
|
|
80
|
+
* loader can settle the file body into the shared model after the editor view
|
|
81
|
+
* already exists, in which case the factory would otherwise have only seen an
|
|
82
|
+
* empty string.
|
|
83
|
+
*/
|
|
84
|
+
const plugin = {
|
|
85
|
+
id: PLUGIN_ID,
|
|
86
|
+
description: 'Detect each file’s indentation style from its contents and configure CodeMirror to match; new files fall back to a language-based default.',
|
|
87
|
+
autoStart: true,
|
|
88
|
+
requires: [IEditorExtensionRegistry],
|
|
89
|
+
activate: (app, registry) => {
|
|
90
|
+
registry.addExtension({
|
|
91
|
+
name: 'xtralab:per-language-indent',
|
|
92
|
+
factory: options => {
|
|
93
|
+
const { model } = options;
|
|
94
|
+
const slot = new Compartment();
|
|
95
|
+
const initialResolved = resolveIndent(model.sharedModel.getSource(), model.mimeType);
|
|
96
|
+
const initial = initialResolved ? buildExtension(initialResolved) : [];
|
|
97
|
+
let settled = initialResolved !== null;
|
|
98
|
+
const refreshPlugin = ViewPlugin.fromClass(class {
|
|
99
|
+
constructor(view) {
|
|
100
|
+
this.view = view;
|
|
101
|
+
this.disconnect = null;
|
|
102
|
+
if (settled) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
const handler = () => {
|
|
106
|
+
if (settled) {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
const resolved = resolveIndent(model.sharedModel.getSource(), model.mimeType);
|
|
110
|
+
if (!resolved) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
settled = true;
|
|
114
|
+
view.dispatch({
|
|
115
|
+
effects: slot.reconfigure(buildExtension(resolved))
|
|
116
|
+
});
|
|
117
|
+
if (this.disconnect) {
|
|
118
|
+
this.disconnect();
|
|
119
|
+
this.disconnect = null;
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
model.sharedModel.changed.connect(handler);
|
|
123
|
+
this.disconnect = () => {
|
|
124
|
+
model.sharedModel.changed.disconnect(handler);
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
destroy() {
|
|
128
|
+
if (this.disconnect) {
|
|
129
|
+
this.disconnect();
|
|
130
|
+
this.disconnect = null;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
return EditorExtensionRegistry.createImmutableExtension([
|
|
135
|
+
slot.of(initial),
|
|
136
|
+
refreshPlugin
|
|
137
|
+
]);
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
export default plugin;
|