xtralab 0.1.2 → 0.3.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 +129 -46
- package/lib/fileBrowser/commands.d.ts +2 -2
- package/lib/fileBrowser/commands.js +2 -2
- package/lib/fileBrowser/fileBrowser.js +72 -2
- package/lib/fileBrowser/gitignore.d.ts +29 -0
- package/lib/fileBrowser/gitignore.js +74 -0
- package/lib/fileBrowser/icons.d.ts +4 -4
- package/lib/fileBrowser/icons.js +4 -4
- package/lib/fileBrowser/widget.d.ts +2 -2
- package/lib/fileBrowser/widget.js +2 -2
- package/lib/git/api.d.ts +11 -0
- package/lib/git/api.js +19 -0
- package/lib/git/commands.d.ts +3 -1
- package/lib/git/commands.js +10 -4
- package/lib/git/diffWidget.d.ts +11 -0
- package/lib/git/diffWidget.js +90 -9
- package/lib/git/icons.d.ts +1 -1
- package/lib/git/icons.js +1 -1
- package/lib/git/index.js +22 -6
- package/lib/git/panelComponent.d.ts +3 -1
- package/lib/git/panelComponent.js +6 -3
- package/lib/index.js +9 -1
- package/lib/launcher/agents.d.ts +78 -0
- package/lib/launcher/agents.js +168 -0
- package/lib/launcher/availability.d.ts +12 -0
- package/lib/launcher/availability.js +53 -0
- package/lib/launcher/commands.d.ts +22 -0
- package/lib/launcher/commands.js +89 -0
- package/lib/launcher/dashboard.d.ts +46 -0
- package/lib/launcher/dashboard.js +214 -0
- package/lib/launcher/icons.d.ts +33 -0
- package/lib/launcher/icons.js +113 -0
- package/lib/launcher/index.d.ts +23 -0
- package/lib/launcher/index.js +191 -0
- package/lib/launcher/invocation.d.ts +29 -0
- package/lib/launcher/invocation.js +42 -0
- package/lib/menus/index.d.ts +14 -0
- package/lib/menus/index.js +102 -0
- package/lib/sidebar/index.d.ts +18 -0
- package/lib/sidebar/index.js +155 -0
- package/lib/statusBar/index.d.ts +14 -0
- package/lib/statusBar/index.js +89 -0
- package/lib/statusBar/widget.d.ts +121 -0
- package/lib/statusBar/widget.js +237 -0
- package/package.json +25 -3
- package/schema/launcher.json +70 -0
- package/schema/sidebar.json +57 -0
- package/src/fileBrowser/commands.ts +2 -2
- package/src/fileBrowser/fileBrowser.tsx +76 -2
- package/src/fileBrowser/gitignore.ts +86 -0
- package/src/fileBrowser/icons.ts +4 -4
- package/src/fileBrowser/widget.tsx +3 -3
- package/src/git/api.ts +20 -0
- package/src/git/commands.ts +19 -5
- package/src/git/diffWidget.tsx +109 -13
- package/src/git/icons.ts +1 -1
- package/src/git/index.ts +31 -5
- package/src/git/panelComponent.tsx +17 -5
- package/src/index.ts +9 -1
- package/src/launcher/agents.ts +248 -0
- package/src/launcher/availability.ts +73 -0
- package/src/launcher/commands.ts +103 -0
- package/src/launcher/dashboard.tsx +462 -0
- package/src/launcher/icons.ts +123 -0
- package/src/launcher/index.ts +228 -0
- package/src/launcher/invocation.ts +45 -0
- package/src/menus/index.ts +124 -0
- package/src/sidebar/index.ts +195 -0
- package/src/statusBar/index.ts +115 -0
- package/src/statusBar/widget.tsx +318 -0
- package/style/git.css +4 -8
- package/style/index.css +2 -0
- package/style/index.js +2 -0
- package/style/launcher.css +323 -0
- package/style/statusBar.css +95 -0
package/README.md
CHANGED
|
@@ -2,70 +2,153 @@
|
|
|
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
|
+
## Install
|
|
14
9
|
|
|
15
|
-
|
|
10
|
+
```bash
|
|
11
|
+
pip install xtralab
|
|
12
|
+
```
|
|
16
13
|
|
|
17
|
-
|
|
14
|
+
## Usage
|
|
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`)
|
|
16
|
+
### As a JupyterLab package
|
|
25
17
|
|
|
26
|
-
|
|
27
|
-
override versions and Jupyter configuration in the usual ways.
|
|
18
|
+
Run JupyterLab the usual way:
|
|
28
19
|
|
|
29
|
-
|
|
20
|
+
```bash
|
|
21
|
+
jupyter lab
|
|
22
|
+
```
|
|
30
23
|
|
|
31
|
-
|
|
24
|
+
### As a desktop app
|
|
32
25
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
26
|
+
A standalone Electron build (DMG on macOS, AppImage on Linux) ships with each
|
|
27
|
+
tagged release — grab the installer for your platform from the
|
|
28
|
+
[Releases page](https://github.com/jtpio/xtralab/releases/latest). Builds from
|
|
29
|
+
the current `main` branch are also produced on every push as workflow
|
|
30
|
+
artifacts under the repository's
|
|
31
|
+
[Actions tab](https://github.com/jtpio/xtralab/actions). See
|
|
32
|
+
[CONTRIBUTING.md](./CONTRIBUTING.md) for the architecture and local build
|
|
33
|
+
instructions.
|
|
38
34
|
|
|
39
|
-
##
|
|
35
|
+
## What's included
|
|
40
36
|
|
|
41
|
-
`
|
|
37
|
+
- [`ajlab`](https://github.com/jtpio/ajlab) — agent-ready JupyterLab base
|
|
38
|
+
- JupyterLab 4.6+
|
|
39
|
+
- `jupyterlab-git` — backs the bundled changes panel
|
|
40
|
+
- `jupyterlab-lsp` + `ty` — Python LSP via Astral's `ty` (bundled); also
|
|
41
|
+
detects `typescript-language-server` on `PATH` for JS/TS
|
|
42
|
+
- `jupyterlab-quickopen`
|
|
43
|
+
- `jupyterlab-cursor-light`, `jupyterlab-cursor-dark`
|
|
44
|
+
- `jupyterlab-day`, `jupyterlab-night` themes
|
|
45
|
+
|
|
46
|
+
The bundled labextension adds:
|
|
47
|
+
|
|
48
|
+
- A path-first file browser in the left sidebar.
|
|
49
|
+
- A "Source Control" panel powered by `jupyterlab-git` and `@pierre/diffs`.
|
|
50
|
+
- An agent launcher with a prompt textarea, a row of agent buttons (Claude,
|
|
51
|
+
Codex, Gemini, Copilot, Goose, OpenCode, Kiro, Mistral Vibe), and a
|
|
52
|
+
collapsible list of changed files. Buttons are filtered to agents installed
|
|
53
|
+
on the machine; a typed prompt is shell-quoted and spliced into the launch
|
|
54
|
+
command for agents that accept one.
|
|
55
|
+
|
|
56
|
+
## Language servers
|
|
57
|
+
|
|
58
|
+
xtralab ships with
|
|
59
|
+
[`jupyterlab-lsp`](https://github.com/jupyter-lsp/jupyterlab-lsp) and
|
|
60
|
+
pre-registers two language servers through
|
|
61
|
+
`jupyter_server_config.d/xtralab-lsp.json`:
|
|
62
|
+
|
|
63
|
+
- **Python — [`ty`](https://github.com/astral-sh/ty)** — installed as a
|
|
64
|
+
Python dependency. Works out of the box.
|
|
65
|
+
- **TypeScript / JavaScript — `typescript-language-server`** — install
|
|
66
|
+
yourself:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
npm install -g typescript-language-server typescript
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Restart JupyterLab (or the desktop app) after installing.
|
|
73
|
+
|
|
74
|
+
Specs use bare command names (`["ty", "server"]`), so binaries are resolved
|
|
75
|
+
from `PATH` at spawn time.
|
|
76
|
+
|
|
77
|
+
### Where binaries are discovered
|
|
78
|
+
|
|
79
|
+
- **`pip install xtralab`** — anything on the JupyterLab process's `PATH`.
|
|
80
|
+
- **Desktop app** — the supervisor augments `PATH` with common shim
|
|
81
|
+
locations (`~/.volta/bin`, `~/.npm-global/bin`, `~/.bun/bin`,
|
|
82
|
+
`~/.asdf/shims`, `~/.mise/shims`, `/opt/homebrew/bin`, `/usr/local/bin`,
|
|
83
|
+
…). Set `XTRALAB_EXTRA_PATH` (colon-separated) before launching the app
|
|
84
|
+
to add directories the defaults miss.
|
|
85
|
+
|
|
86
|
+
### Adding more servers
|
|
87
|
+
|
|
88
|
+
To enable another server (bash, yaml, json, dockerfile, pyright, …),
|
|
89
|
+
install the binary then drop a JSON file into a `jupyter_server_config.d/`
|
|
90
|
+
directory:
|
|
91
|
+
|
|
92
|
+
- **`pip install xtralab`** — run `jupyter --paths` and pick a config dir
|
|
93
|
+
(typically `~/.jupyter/jupyter_server_config.d/`).
|
|
94
|
+
- **Desktop app (macOS)** —
|
|
95
|
+
`~/Library/Application Support/xtralab/jupyter/config/jupyter_server_config.d/`
|
|
96
|
+
(or `xtralab dev` for local dev builds).
|
|
97
|
+
- **Desktop app (Linux AppImage)** —
|
|
98
|
+
`~/.config/xtralab/jupyter/config/jupyter_server_config.d/`.
|
|
99
|
+
|
|
100
|
+
Example (`bash-lsp.json`):
|
|
101
|
+
|
|
102
|
+
```json
|
|
103
|
+
{
|
|
104
|
+
"LanguageServerManager": {
|
|
105
|
+
"language_servers": {
|
|
106
|
+
"bash-language-server": {
|
|
107
|
+
"version": 2,
|
|
108
|
+
"argv": ["bash-language-server", "start"],
|
|
109
|
+
"languages": ["bash", "sh"],
|
|
110
|
+
"mime_types": ["text/x-sh", "application/x-sh"],
|
|
111
|
+
"display_name": "bash-language-server"
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
```
|
|
42
117
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
-
|
|
46
|
-
|
|
47
|
-
- `dockPanelPadding` is off, so the main dock area is flush with the
|
|
48
|
-
surrounding chrome.
|
|
118
|
+
Pair with `npm install -g bash-language-server`. Reuse a bundled `key` to
|
|
119
|
+
override it. See
|
|
120
|
+
[jupyterlab-lsp's docs](https://jupyterlab-lsp.readthedocs.io/en/latest/Configuring.html)
|
|
121
|
+
for the full spec schema.
|
|
49
122
|
|
|
50
|
-
|
|
51
|
-
fragments so downstream meta-packages can add their own Lab configuration
|
|
52
|
-
fragments without replacing `xtralab`'s files.
|
|
123
|
+
## Customizing the launcher
|
|
53
124
|
|
|
54
|
-
|
|
125
|
+
Open `Settings → Settings Editor → xtralab launcher` and edit the `agents`
|
|
126
|
+
array. Entries are merged with the defaults by `id`.
|
|
55
127
|
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
|
|
128
|
+
```jsonc
|
|
129
|
+
{
|
|
130
|
+
"agents": [
|
|
131
|
+
// Hide an agent
|
|
132
|
+
{ "id": "kiro", "enabled": false },
|
|
59
133
|
|
|
60
|
-
|
|
134
|
+
// Override an agent's command (e.g. point Claude at a shell alias)
|
|
135
|
+
{ "id": "claude", "command": "cl", "requireAvailable": false },
|
|
61
136
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
jlpm build
|
|
137
|
+
// Add a new agent; promptArgs: [] appends the prompt as a positional arg
|
|
138
|
+
{ "id": "aider", "label": "Aider", "command": "aider", "promptArgs": [] }
|
|
139
|
+
]
|
|
140
|
+
}
|
|
67
141
|
```
|
|
68
142
|
|
|
143
|
+
Fields: `id` (required), `label`, `caption`, `command`, `promptArgs` (how to
|
|
144
|
+
splice the prompt — `[]` for positional, `["--flag"]` for flagged, `null` to
|
|
145
|
+
opt out), `iconSvg`, `rank`, `enabled`, `requireAvailable`.
|
|
146
|
+
|
|
147
|
+
## Contributing
|
|
148
|
+
|
|
149
|
+
See [CONTRIBUTING.md](./CONTRIBUTING.md) for the development setup, the
|
|
150
|
+
Electron desktop app architecture, and the build pipeline.
|
|
151
|
+
|
|
69
152
|
## License
|
|
70
153
|
|
|
71
154
|
BSD-3-Clause
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { JupyterFrontEnd } from '@jupyterlab/application';
|
|
2
2
|
import { IXtralabFileBrowser } from './widget';
|
|
3
3
|
/**
|
|
4
|
-
* Command identifiers exposed by the
|
|
4
|
+
* Command identifiers exposed by the xtralab browser. We deliberately namespace
|
|
5
5
|
* these under `xtralab:` rather than reusing the `filebrowser:` ids
|
|
6
6
|
* because the core `filebrowser:*` commands look up a `FileBrowser` instance
|
|
7
7
|
* via `IFileBrowserFactory.tracker` — our widget is not a `FileBrowser`, so
|
|
@@ -31,7 +31,7 @@ export interface IRegisterCommandsOptions {
|
|
|
31
31
|
browser: IXtralabFileBrowser;
|
|
32
32
|
}
|
|
33
33
|
/**
|
|
34
|
-
* Register every
|
|
34
|
+
* Register every xtralab command on the application command registry, attach
|
|
35
35
|
* the items to the application context menu, and wire up the dynamic
|
|
36
36
|
* "Open With" submenu populator.
|
|
37
37
|
*
|
|
@@ -3,7 +3,7 @@ import { PageConfig, PathExt } from '@jupyterlab/coreutils';
|
|
|
3
3
|
import { addIcon, closeIcon, CommandToolbarButton, copyIcon, downloadIcon, editIcon, fileIcon, newFolderIcon, refreshIcon } from '@jupyterlab/ui-components';
|
|
4
4
|
import { toCanonicalPath, toServerPath } from './contents';
|
|
5
5
|
/**
|
|
6
|
-
* Command identifiers exposed by the
|
|
6
|
+
* Command identifiers exposed by the xtralab browser. We deliberately namespace
|
|
7
7
|
* these under `xtralab:` rather than reusing the `filebrowser:` ids
|
|
8
8
|
* because the core `filebrowser:*` commands look up a `FileBrowser` instance
|
|
9
9
|
* via `IFileBrowserFactory.tracker` — our widget is not a `FileBrowser`, so
|
|
@@ -136,7 +136,7 @@ function makeOpenWithUpdater(app, browser) {
|
|
|
136
136
|
};
|
|
137
137
|
}
|
|
138
138
|
/**
|
|
139
|
-
* Register every
|
|
139
|
+
* Register every xtralab command on the application command registry, attach
|
|
140
140
|
* the items to the application context menu, and wire up the dynamic
|
|
141
141
|
* "Open With" submenu populator.
|
|
142
142
|
*
|
|
@@ -4,6 +4,7 @@ import { MimeData, PromiseDelegate } from '@lumino/coreutils';
|
|
|
4
4
|
import { Drag } from '@lumino/dragdrop';
|
|
5
5
|
import { FileTree, useFileTree } from '@pierre/trees/react';
|
|
6
6
|
import { ROOT_LOAD_KEY, listDirectory, toServerPath } from './contents';
|
|
7
|
+
import { buildIgnoredEntries, loadGitignoreMatcher } from './gitignore';
|
|
7
8
|
import { FILE_BROWSER_ICONS } from './icons';
|
|
8
9
|
/**
|
|
9
10
|
* MIME types used by the dock panel and other JupyterLab drop targets.
|
|
@@ -11,7 +12,7 @@ import { FILE_BROWSER_ICONS } from './icons';
|
|
|
11
12
|
* synchronous function that returns a Widget. The contents MIME types
|
|
12
13
|
* mirror what the default file browser sends so other drop targets that
|
|
13
14
|
* understand them (the file browser itself, custom drop zones) keep
|
|
14
|
-
* working when the user drags out of
|
|
15
|
+
* working when the user drags out of xtralab.
|
|
15
16
|
*/
|
|
16
17
|
const FACTORY_MIME = 'application/vnd.lumino.widget-factory';
|
|
17
18
|
const CONTENTS_MIME = 'application/x-jupyter-icontents';
|
|
@@ -41,11 +42,54 @@ export function FileBrowserComponent(props) {
|
|
|
41
42
|
paths: [],
|
|
42
43
|
initialExpansion: 'closed',
|
|
43
44
|
search: true,
|
|
44
|
-
icons: FILE_BROWSER_ICONS
|
|
45
|
+
icons: FILE_BROWSER_ICONS,
|
|
46
|
+
itemHeight: 24
|
|
45
47
|
});
|
|
46
48
|
React.useEffect(() => {
|
|
47
49
|
const knownDirs = new Map();
|
|
50
|
+
// Mirror of the canonical paths currently loaded into the tree. Kept in
|
|
51
|
+
// sync with `model.resetPaths`/`model.batch`/`model.add` so we can
|
|
52
|
+
// re-test every loaded path against the gitignore matcher whenever
|
|
53
|
+
// either side changes — the model itself does not expose a
|
|
54
|
+
// path-iteration API.
|
|
55
|
+
const loadedPaths = new Set();
|
|
56
|
+
let gitignoreMatcher = null;
|
|
48
57
|
let cancelled = false;
|
|
58
|
+
/**
|
|
59
|
+
* Recompute the `ignored` git status entries from the current
|
|
60
|
+
* gitignore matcher and the set of loaded paths, and push them into
|
|
61
|
+
* the tree. Safe to call at any time: when the matcher is `null`
|
|
62
|
+
* (no `.gitignore` or it failed to load) we send an empty list, which
|
|
63
|
+
* also clears any statuses that may have been applied previously.
|
|
64
|
+
*/
|
|
65
|
+
const syncGitStatus = () => {
|
|
66
|
+
if (gitignoreMatcher === null) {
|
|
67
|
+
model.setGitStatus([]);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const entries = buildIgnoredEntries(gitignoreMatcher, loadedPaths);
|
|
71
|
+
model.setGitStatus(entries);
|
|
72
|
+
};
|
|
73
|
+
/**
|
|
74
|
+
* Reload the workspace `.gitignore`, then re-apply the resulting
|
|
75
|
+
* ignored statuses. Called on initial mount and whenever the user
|
|
76
|
+
* triggers a refresh — the file may have been edited or created in
|
|
77
|
+
* between.
|
|
78
|
+
*/
|
|
79
|
+
const refreshGitignoreMatcher = async () => {
|
|
80
|
+
let next = null;
|
|
81
|
+
try {
|
|
82
|
+
next = await loadGitignoreMatcher(contentsManager);
|
|
83
|
+
}
|
|
84
|
+
catch (err) {
|
|
85
|
+
console.error('xtralab: failed to load .gitignore', err);
|
|
86
|
+
}
|
|
87
|
+
if (cancelled) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
gitignoreMatcher = next;
|
|
91
|
+
syncGitStatus();
|
|
92
|
+
};
|
|
49
93
|
const fetchDirectory = async (canonicalPath) => {
|
|
50
94
|
const current = knownDirs.get(canonicalPath);
|
|
51
95
|
if (current === 'loading' || current === 'loaded') {
|
|
@@ -60,6 +104,10 @@ export function FileBrowserComponent(props) {
|
|
|
60
104
|
}
|
|
61
105
|
if (canonicalPath === ROOT_LOAD_KEY) {
|
|
62
106
|
model.resetPaths(paths);
|
|
107
|
+
loadedPaths.clear();
|
|
108
|
+
for (const path of paths) {
|
|
109
|
+
loadedPaths.add(path);
|
|
110
|
+
}
|
|
63
111
|
}
|
|
64
112
|
else {
|
|
65
113
|
// Filter out paths already in the model. The path-store throws
|
|
@@ -72,6 +120,14 @@ export function FileBrowserComponent(props) {
|
|
|
72
120
|
if (operations.length > 0) {
|
|
73
121
|
model.batch(operations);
|
|
74
122
|
}
|
|
123
|
+
// Mirror every directory child into `loadedPaths` regardless of
|
|
124
|
+
// whether it was just added or skipped above — paths skipped by
|
|
125
|
+
// the filter are already in the model from a prior add and so
|
|
126
|
+
// belong in the mirror too. `Set.add` is idempotent, so the
|
|
127
|
+
// duplicate adds are a no-op.
|
|
128
|
+
for (const path of paths) {
|
|
129
|
+
loadedPaths.add(path);
|
|
130
|
+
}
|
|
75
131
|
}
|
|
76
132
|
for (const subdir of subdirectories) {
|
|
77
133
|
if (!knownDirs.has(subdir)) {
|
|
@@ -79,6 +135,7 @@ export function FileBrowserComponent(props) {
|
|
|
79
135
|
}
|
|
80
136
|
}
|
|
81
137
|
knownDirs.set(canonicalPath, 'loaded');
|
|
138
|
+
syncGitStatus();
|
|
82
139
|
}
|
|
83
140
|
catch (err) {
|
|
84
141
|
console.error(`xtralab: failed to load directory "${canonicalPath}"`, err);
|
|
@@ -165,12 +222,22 @@ export function FileBrowserComponent(props) {
|
|
|
165
222
|
// Reset the tree contents and restore the load-state map so future
|
|
166
223
|
// expansions know which directories still need fetching.
|
|
167
224
|
model.resetPaths(allPaths);
|
|
225
|
+
loadedPaths.clear();
|
|
226
|
+
for (const path of allPaths) {
|
|
227
|
+
loadedPaths.add(path);
|
|
228
|
+
}
|
|
168
229
|
knownDirs.set(ROOT_LOAD_KEY, 'loaded');
|
|
169
230
|
subdirsByParent.forEach(subdirs => {
|
|
170
231
|
for (const subdir of subdirs) {
|
|
171
232
|
knownDirs.set(subdir, expandedPaths.has(subdir) ? 'loaded' : 'unloaded');
|
|
172
233
|
}
|
|
173
234
|
});
|
|
235
|
+
// Refresh the `.gitignore` matcher in case the file was edited
|
|
236
|
+
// since the last load, then re-apply the resulting statuses to the
|
|
237
|
+
// newly-loaded paths. The reload runs in parallel with the rest of
|
|
238
|
+
// the refresh — `refreshGitignoreMatcher` calls `syncGitStatus`
|
|
239
|
+
// itself when it completes.
|
|
240
|
+
void refreshGitignoreMatcher();
|
|
174
241
|
// Re-expand the directories that were expanded before the refresh.
|
|
175
242
|
// We have to do this after `resetPaths` because the reset starts
|
|
176
243
|
// every directory in its initial collapsed state.
|
|
@@ -196,12 +263,14 @@ export function FileBrowserComponent(props) {
|
|
|
196
263
|
return;
|
|
197
264
|
}
|
|
198
265
|
}
|
|
266
|
+
loadedPaths.add(canonicalPath);
|
|
199
267
|
if (canonicalPath.endsWith('/') && !knownDirs.has(canonicalPath)) {
|
|
200
268
|
// The new directory has no children yet, so mark it as already
|
|
201
269
|
// loaded — there's nothing to fetch and we don't want a stale
|
|
202
270
|
// "unloaded" entry to trigger a fetch on the next expand.
|
|
203
271
|
knownDirs.set(canonicalPath, 'loaded');
|
|
204
272
|
}
|
|
273
|
+
syncGitStatus();
|
|
205
274
|
const parent = parentOf(canonicalPath);
|
|
206
275
|
if (parent === ROOT_LOAD_KEY) {
|
|
207
276
|
return;
|
|
@@ -217,6 +286,7 @@ export function FileBrowserComponent(props) {
|
|
|
217
286
|
};
|
|
218
287
|
knownDirs.set(ROOT_LOAD_KEY, 'unloaded');
|
|
219
288
|
void fetchDirectory(ROOT_LOAD_KEY);
|
|
289
|
+
void refreshGitignoreMatcher();
|
|
220
290
|
const unsubscribe = model.subscribe(() => {
|
|
221
291
|
knownDirs.forEach((state, canonicalPath) => {
|
|
222
292
|
if (state !== 'unloaded') {
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { Ignore } from 'ignore';
|
|
2
|
+
import { Contents } from '@jupyterlab/services';
|
|
3
|
+
import type { GitStatusEntry } from '@pierre/trees';
|
|
4
|
+
/**
|
|
5
|
+
* Load the workspace `.gitignore` and return an `ignore` matcher built from
|
|
6
|
+
* its contents. Returns `null` when the file is missing, empty, or fails to
|
|
7
|
+
* load — callers should treat any of those as "no ignored entries".
|
|
8
|
+
*/
|
|
9
|
+
export declare function loadGitignoreMatcher(contentsManager: Contents.IManager): Promise<Ignore | null>;
|
|
10
|
+
/**
|
|
11
|
+
* Build `GitStatusEntry`s for the canonical paths that match the given
|
|
12
|
+
* gitignore matcher.
|
|
13
|
+
*
|
|
14
|
+
* `@pierre/trees` propagates the `ignored` style from a directory entry
|
|
15
|
+
* down to every descendant via its internal `ignoredDirectoryPaths` set, so
|
|
16
|
+
* we only emit one entry per ignored subtree root: emitting descendants
|
|
17
|
+
* would also pollute the library's `directoriesWithChanges` set, causing
|
|
18
|
+
* parent directories of nested ignored items to render a "contains git
|
|
19
|
+
* status items" dot.
|
|
20
|
+
*
|
|
21
|
+
* Paths are sorted lexicographically so a directory always precedes its
|
|
22
|
+
* descendants, allowing a simple ancestor-suppression check via
|
|
23
|
+
* `path.startsWith(dir)`. The trailing slash on canonical directory paths
|
|
24
|
+
* (required by `@pierre/trees`, also recognized by `ignore` as the
|
|
25
|
+
* "directory" indicator) keeps the prefix check from accidentally matching
|
|
26
|
+
* sibling paths that happen to share a name prefix (e.g. `node_modules_old`
|
|
27
|
+
* vs `node_modules/`).
|
|
28
|
+
*/
|
|
29
|
+
export declare function buildIgnoredEntries(matcher: Ignore, canonicalPaths: Iterable<string>): GitStatusEntry[];
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import ignore from 'ignore';
|
|
2
|
+
/**
|
|
3
|
+
* The path of the workspace `.gitignore` file. Loaded relative to the
|
|
4
|
+
* Jupyter contents root, which matches the directory the gitignore patterns
|
|
5
|
+
* are evaluated against — patterns and tested paths share the same base.
|
|
6
|
+
*/
|
|
7
|
+
const GITIGNORE_PATH = '.gitignore';
|
|
8
|
+
/**
|
|
9
|
+
* Load the workspace `.gitignore` and return an `ignore` matcher built from
|
|
10
|
+
* its contents. Returns `null` when the file is missing, empty, or fails to
|
|
11
|
+
* load — callers should treat any of those as "no ignored entries".
|
|
12
|
+
*/
|
|
13
|
+
export async function loadGitignoreMatcher(contentsManager) {
|
|
14
|
+
let model;
|
|
15
|
+
try {
|
|
16
|
+
model = await contentsManager.get(GITIGNORE_PATH, {
|
|
17
|
+
content: true,
|
|
18
|
+
format: 'text',
|
|
19
|
+
type: 'file'
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
catch (_a) {
|
|
23
|
+
// The most common failure here is `.gitignore` simply not existing.
|
|
24
|
+
// Other transient errors (permission denied, server unreachable) are
|
|
25
|
+
// also treated as "no matcher" so the file browser still works.
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
if (model.type !== 'file' || typeof model.content !== 'string') {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
if (model.content.length === 0) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
return ignore().add(model.content);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Build `GitStatusEntry`s for the canonical paths that match the given
|
|
38
|
+
* gitignore matcher.
|
|
39
|
+
*
|
|
40
|
+
* `@pierre/trees` propagates the `ignored` style from a directory entry
|
|
41
|
+
* down to every descendant via its internal `ignoredDirectoryPaths` set, so
|
|
42
|
+
* we only emit one entry per ignored subtree root: emitting descendants
|
|
43
|
+
* would also pollute the library's `directoriesWithChanges` set, causing
|
|
44
|
+
* parent directories of nested ignored items to render a "contains git
|
|
45
|
+
* status items" dot.
|
|
46
|
+
*
|
|
47
|
+
* Paths are sorted lexicographically so a directory always precedes its
|
|
48
|
+
* descendants, allowing a simple ancestor-suppression check via
|
|
49
|
+
* `path.startsWith(dir)`. The trailing slash on canonical directory paths
|
|
50
|
+
* (required by `@pierre/trees`, also recognized by `ignore` as the
|
|
51
|
+
* "directory" indicator) keeps the prefix check from accidentally matching
|
|
52
|
+
* sibling paths that happen to share a name prefix (e.g. `node_modules_old`
|
|
53
|
+
* vs `node_modules/`).
|
|
54
|
+
*/
|
|
55
|
+
export function buildIgnoredEntries(matcher, canonicalPaths) {
|
|
56
|
+
const sorted = Array.from(canonicalPaths).sort();
|
|
57
|
+
const emittedIgnoredDirs = [];
|
|
58
|
+
const entries = [];
|
|
59
|
+
for (const path of sorted) {
|
|
60
|
+
if (path.length === 0) {
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (emittedIgnoredDirs.some(dir => path.startsWith(dir))) {
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (matcher.ignores(path)) {
|
|
67
|
+
entries.push({ path, status: 'ignored' });
|
|
68
|
+
if (path.endsWith('/')) {
|
|
69
|
+
emittedIgnoredDirs.push(path);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return entries;
|
|
74
|
+
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { LabIcon } from '@jupyterlab/ui-components';
|
|
2
2
|
import type { FileTreeIconConfig } from '@pierre/trees';
|
|
3
3
|
/**
|
|
4
|
-
* The glyph used as the
|
|
4
|
+
* The glyph used as the xtralab file browser's sidebar tab icon. Defined as a
|
|
5
5
|
* `LabIcon` (rather than the default `folderIcon` from
|
|
6
|
-
* `@jupyterlab/ui-components`) so the
|
|
6
|
+
* `@jupyterlab/ui-components`) so the xtralab browser is visually distinguishable
|
|
7
7
|
* from JupyterLab's built-in file browser, which also lives in the left
|
|
8
8
|
* sidebar.
|
|
9
9
|
*
|
|
@@ -14,7 +14,7 @@ import type { FileTreeIconConfig } from '@pierre/trees';
|
|
|
14
14
|
*/
|
|
15
15
|
export declare const xtralabFileBrowserIcon: LabIcon;
|
|
16
16
|
/**
|
|
17
|
-
* Icon configuration for the
|
|
17
|
+
* Icon configuration for the xtralab file browser. Keeps the default `complete`
|
|
18
18
|
* built-in icon set (the colored language icons) and layers a Jupyter notebook
|
|
19
19
|
* symbol on top so `.ipynb` files render with the JupyterLab notebook glyph.
|
|
20
20
|
*
|
|
@@ -24,7 +24,7 @@ export declare const xtralabFileBrowserIcon: LabIcon;
|
|
|
24
24
|
*/
|
|
25
25
|
export declare const FILE_BROWSER_ICONS: FileTreeIconConfig;
|
|
26
26
|
/**
|
|
27
|
-
* Return a JupyterLab `LabIcon` matching the icon the
|
|
27
|
+
* Return a JupyterLab `LabIcon` matching the icon the xtralab file tree would
|
|
28
28
|
* render for the given file path. Used by other plugins (e.g. the git diff
|
|
29
29
|
* viewer) that want their main-area widget tabs to display the same per-file
|
|
30
30
|
* glyph the tree shows in the sidebar.
|
package/lib/fileBrowser/icons.js
CHANGED
|
@@ -2,9 +2,9 @@ var _a;
|
|
|
2
2
|
import { LabIcon, fileIcon } from '@jupyterlab/ui-components';
|
|
3
3
|
import { createFileTreeIconResolver, getBuiltInFileIconColor, getBuiltInSpriteSheet } from '@pierre/trees';
|
|
4
4
|
/**
|
|
5
|
-
* The glyph used as the
|
|
5
|
+
* The glyph used as the xtralab file browser's sidebar tab icon. Defined as a
|
|
6
6
|
* `LabIcon` (rather than the default `folderIcon` from
|
|
7
|
-
* `@jupyterlab/ui-components`) so the
|
|
7
|
+
* `@jupyterlab/ui-components`) so the xtralab browser is visually distinguishable
|
|
8
8
|
* from JupyterLab's built-in file browser, which also lives in the left
|
|
9
9
|
* sidebar.
|
|
10
10
|
*
|
|
@@ -42,7 +42,7 @@ const JUPYTER_NOTEBOOK_SPRITE_SHEET = `<svg data-icon-sprite aria-hidden="true"
|
|
|
42
42
|
</symbol>
|
|
43
43
|
</svg>`;
|
|
44
44
|
/**
|
|
45
|
-
* Icon configuration for the
|
|
45
|
+
* Icon configuration for the xtralab file browser. Keeps the default `complete`
|
|
46
46
|
* built-in icon set (the colored language icons) and layers a Jupyter notebook
|
|
47
47
|
* symbol on top so `.ipynb` files render with the JupyterLab notebook glyph.
|
|
48
48
|
*
|
|
@@ -105,7 +105,7 @@ function extractSymbol(symbolId) {
|
|
|
105
105
|
};
|
|
106
106
|
}
|
|
107
107
|
/**
|
|
108
|
-
* Return a JupyterLab `LabIcon` matching the icon the
|
|
108
|
+
* Return a JupyterLab `LabIcon` matching the icon the xtralab file tree would
|
|
109
109
|
* render for the given file path. Used by other plugins (e.g. the git diff
|
|
110
110
|
* viewer) that want their main-area widget tabs to display the same per-file
|
|
111
111
|
* glyph the tree shows in the sidebar.
|
|
@@ -5,7 +5,7 @@ import { ISignal } from '@lumino/signaling';
|
|
|
5
5
|
import { Widget } from '@lumino/widgets';
|
|
6
6
|
export declare const FILE_BROWSER_ID = "xtralab:file-browser";
|
|
7
7
|
/**
|
|
8
|
-
* The CSS class added to the
|
|
8
|
+
* The CSS class added to the xtralab file browser widget. The selectors that
|
|
9
9
|
* bind application context menu items hang off this class, so it must remain
|
|
10
10
|
* specific enough not to clash with other browsers (such as the default
|
|
11
11
|
* `jp-FileBrowser`).
|
|
@@ -17,7 +17,7 @@ export interface IXtralabFileBrowserOptions {
|
|
|
17
17
|
onOpenFile?: (serverPath: string) => void;
|
|
18
18
|
}
|
|
19
19
|
/**
|
|
20
|
-
* Public API of the
|
|
20
|
+
* Public API of the xtralab file browser used by command handlers and other
|
|
21
21
|
* collaborators. Kept narrow on purpose so the React component stays free to
|
|
22
22
|
* evolve the underlying tree integration.
|
|
23
23
|
*/
|
|
@@ -6,7 +6,7 @@ import { FileBrowserComponent } from './fileBrowser';
|
|
|
6
6
|
import { xtralabFileBrowserIcon } from './icons';
|
|
7
7
|
export const FILE_BROWSER_ID = 'xtralab:file-browser';
|
|
8
8
|
/**
|
|
9
|
-
* The CSS class added to the
|
|
9
|
+
* The CSS class added to the xtralab file browser widget. The selectors that
|
|
10
10
|
* bind application context menu items hang off this class, so it must remain
|
|
11
11
|
* specific enough not to clash with other browsers (such as the default
|
|
12
12
|
* `jp-FileBrowser`).
|
|
@@ -32,7 +32,7 @@ export class XtralabFileBrowser extends Widget {
|
|
|
32
32
|
this._onOpenFile = options.onOpenFile;
|
|
33
33
|
this.id = FILE_BROWSER_ID;
|
|
34
34
|
this.title.icon = xtralabFileBrowserIcon;
|
|
35
|
-
this.title.caption = '
|
|
35
|
+
this.title.caption = 'xtralab File Browser';
|
|
36
36
|
this.addClass(FILE_BROWSER_CSS_CLASS);
|
|
37
37
|
this._toolbar = new Toolbar();
|
|
38
38
|
this._toolbar.addClass(TOOLBAR_CSS_CLASS);
|
package/lib/git/api.d.ts
CHANGED
|
@@ -28,6 +28,17 @@ export declare function reset(repoPath: string, filename: string): Promise<void>
|
|
|
28
28
|
export declare function resetAll(repoPath: string): Promise<void>;
|
|
29
29
|
/** Restore a single file to the version in the index (i.e. discard worktree changes). */
|
|
30
30
|
export declare function checkout(repoPath: string, filename: string): Promise<void>;
|
|
31
|
+
/**
|
|
32
|
+
* Expand the porcelain `files` array into one entry per logical change.
|
|
33
|
+
* A single porcelain row may map to two `IFileChange` rows (a file that's
|
|
34
|
+
* both staged and modified again in the worktree); callers that just want
|
|
35
|
+
* a flat list of all changes — without any staged-vs-unstaged grouping —
|
|
36
|
+
* can use this helper.
|
|
37
|
+
*
|
|
38
|
+
* Provided as a manual concat rather than `Array.prototype.flatMap` so we
|
|
39
|
+
* don't pin our `tsconfig.lib` to ES2019.
|
|
40
|
+
*/
|
|
41
|
+
export declare function expandStatusFiles(files: IGitStatusFile[]): IFileChange[];
|
|
31
42
|
/**
|
|
32
43
|
* Translate one entry in the porcelain `files` array into the panel's
|
|
33
44
|
* preferred shape. A single porcelain entry may represent up to two changes
|
package/lib/git/api.js
CHANGED
|
@@ -103,6 +103,25 @@ export async function checkout(repoPath, filename) {
|
|
|
103
103
|
filename
|
|
104
104
|
});
|
|
105
105
|
}
|
|
106
|
+
/**
|
|
107
|
+
* Expand the porcelain `files` array into one entry per logical change.
|
|
108
|
+
* A single porcelain row may map to two `IFileChange` rows (a file that's
|
|
109
|
+
* both staged and modified again in the worktree); callers that just want
|
|
110
|
+
* a flat list of all changes — without any staged-vs-unstaged grouping —
|
|
111
|
+
* can use this helper.
|
|
112
|
+
*
|
|
113
|
+
* Provided as a manual concat rather than `Array.prototype.flatMap` so we
|
|
114
|
+
* don't pin our `tsconfig.lib` to ES2019.
|
|
115
|
+
*/
|
|
116
|
+
export function expandStatusFiles(files) {
|
|
117
|
+
const result = [];
|
|
118
|
+
for (const file of files) {
|
|
119
|
+
for (const change of porcelainToFileChanges(file)) {
|
|
120
|
+
result.push(change);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return result;
|
|
124
|
+
}
|
|
106
125
|
/**
|
|
107
126
|
* Translate one entry in the porcelain `files` array into the panel's
|
|
108
127
|
* preferred shape. A single porcelain entry may represent up to two changes
|
package/lib/git/commands.d.ts
CHANGED
|
@@ -24,6 +24,7 @@ export declare namespace CommandArguments {
|
|
|
24
24
|
interface IOpenDiff {
|
|
25
25
|
repoPath: string;
|
|
26
26
|
change: IFileChange;
|
|
27
|
+
pin?: boolean;
|
|
27
28
|
}
|
|
28
29
|
}
|
|
29
30
|
export interface IRegisterGitCommandsOptions {
|
|
@@ -58,7 +59,8 @@ export interface IRegisterGitCommandsOptions {
|
|
|
58
59
|
* Look up an already-open diff widget for a given file change. Returns
|
|
59
60
|
* `undefined` when none is open.
|
|
60
61
|
*/
|
|
61
|
-
findDiff(change: IFileChange): ReturnType<typeof createGitDiffWidget> | undefined;
|
|
62
|
+
findDiff(change: IFileChange, pin?: boolean): ReturnType<typeof createGitDiffWidget> | undefined;
|
|
63
|
+
onPinned(widget: ReturnType<typeof createGitDiffWidget>): void;
|
|
62
64
|
}
|
|
63
65
|
/**
|
|
64
66
|
* Register the git commands on the application command registry. Returns
|