@narumitw/pi-jupyter 0.1.4 → 0.33.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/LICENSE +21 -0
- package/README.md +129 -118
- package/package.json +44 -34
- package/src/index.ts +1 -0
- package/src/jupyter-command.ts +138 -0
- package/src/jupyter-menu.ts +310 -0
- package/src/jupyter-preview.ts +579 -0
- package/src/notebook-panel.ts +204 -0
- package/src/notebook.ts +237 -0
- package/src/png-thumbnail.ts +237 -0
- package/extensions/index.ts +0 -882
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 narumiruna
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,143 +1,154 @@
|
|
|
1
|
-
# pi-jupyter
|
|
1
|
+
# 📓 pi-jupyter
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/@narumitw/pi-jupyter)
|
|
4
|
+
[](https://github.com/earendil-works/pi)
|
|
5
|
+
[](LICENSE)
|
|
4
6
|
|
|
5
|
-
|
|
7
|
+
> [!WARNING]
|
|
8
|
+
> This extension is experimental. Its preview behavior, interaction model, and shortcuts may change between releases.
|
|
6
9
|
|
|
7
|
-
|
|
10
|
+
Preview a Jupyter notebook (`.ipynb`) in a terminal-native panel beside Pi's editor. The panel renders the notebook saved on disk; it does not run a Jupyter kernel or open a browser webview.
|
|
8
11
|
|
|
9
|
-
|
|
10
|
-
- Auto-updates when Pi reads, writes, or edits an `.ipynb` file.
|
|
11
|
-
- Watches the selected notebook on disk and refreshes after external saves.
|
|
12
|
-
- Displays markdown cells, code cells, execution counts, common text outputs/errors, and inline image outputs when the terminal supports images.
|
|
13
|
-
- Non-capturing by default, so you can keep typing in Pi while the panel stays visible.
|
|
14
|
-
- Mouse-resizable right-side panel: drag the preview's left border to change its width.
|
|
15
|
-
- Focus mode for scrolling the preview.
|
|
12
|
+
## ✨ Features
|
|
16
13
|
|
|
17
|
-
|
|
14
|
+
- Exposes one `/jupyter` current-state menu for choosing, opening, focusing, refreshing, switching, or closing a preview.
|
|
15
|
+
- Opens a non-capturing right-side preview while the normal Pi editor remains usable.
|
|
16
|
+
- Detects notebooks used by successful Pi file tools and refreshes after matching tool results.
|
|
17
|
+
- Watches the selected notebook's parent directory so ordinary and atomic external saves refresh the panel.
|
|
18
|
+
- Loads notebook changes atomically and preserves the last valid preview after cancellation, partial saves, or parse failures.
|
|
19
|
+
- Renders markdown and code cells, execution counts, bounded text/error output, and inline image output.
|
|
20
|
+
- Displays PNG output as truecolor ANSI half-block thumbnails; other image formats use Pi TUI terminal-image support when available.
|
|
21
|
+
- Supports keyboard scrolling, focused scrolling, and mouse resizing from the panel's left border.
|
|
22
|
+
- Auto-hides the overlay when the terminal is narrower than 90 columns.
|
|
18
23
|
|
|
19
|
-
|
|
24
|
+
## 📦 Install
|
|
20
25
|
|
|
21
|
-
|
|
22
|
-
pi install npm:@narumitw/pi-jupyter
|
|
23
|
-
```
|
|
24
|
-
|
|
25
|
-
Or pin a version:
|
|
26
|
-
|
|
27
|
-
```bash
|
|
28
|
-
pi install npm:@narumitw/pi-jupyter@0.1.3
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
Install for the current project only:
|
|
32
|
-
|
|
33
|
-
```bash
|
|
34
|
-
pi install npm:@narumitw/pi-jupyter -l
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
Install from GitHub/tag instead of npm:
|
|
26
|
+
Install persistently from npm:
|
|
38
27
|
|
|
39
28
|
```bash
|
|
40
|
-
pi install git:github.com/narumiruna/pi-jupyter@v0.1.3
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
If you previously installed the unscoped package, remove it before installing the scoped package:
|
|
44
|
-
|
|
45
|
-
```bash
|
|
46
|
-
pi remove npm:pi-jupyter
|
|
47
29
|
pi install npm:@narumitw/pi-jupyter
|
|
48
30
|
```
|
|
49
31
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
Use one source at a time. If `npm:@narumitw/pi-jupyter` is installed globally and this repo is also installed locally with `-l`, Pi will load both and report shortcut conflicts.
|
|
53
|
-
|
|
54
|
-
For temporary local testing, prefer:
|
|
55
|
-
|
|
56
|
-
```bash
|
|
57
|
-
pi -e .
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
For a persistent project-local install:
|
|
61
|
-
|
|
62
|
-
```bash
|
|
63
|
-
pi install . -l
|
|
64
|
-
```
|
|
65
|
-
|
|
66
|
-
If you also have an npm package installed globally, remove one source before starting Pi:
|
|
67
|
-
|
|
68
|
-
```bash
|
|
69
|
-
# Keep the npm package; remove the project-local package from this repo
|
|
70
|
-
pi remove . -l
|
|
71
|
-
|
|
72
|
-
# Or keep the local package; remove the global npm package
|
|
73
|
-
pi remove npm:@narumitw/pi-jupyter
|
|
74
|
-
|
|
75
|
-
# If you installed the older unscoped package, remove that too
|
|
76
|
-
pi remove npm:pi-jupyter
|
|
77
|
-
```
|
|
78
|
-
|
|
79
|
-
## Development
|
|
80
|
-
|
|
81
|
-
```bash
|
|
82
|
-
npm install
|
|
83
|
-
just check
|
|
84
|
-
just format
|
|
85
|
-
pre-commit install
|
|
86
|
-
```
|
|
87
|
-
|
|
88
|
-
Publish to npm. This runs `biome check .` first via the `justfile`, then `npm publish --access public`:
|
|
32
|
+
Try from npm without installing permanently:
|
|
89
33
|
|
|
90
34
|
```bash
|
|
91
|
-
npm
|
|
92
|
-
just publish
|
|
35
|
+
pi -e npm:@narumitw/pi-jupyter
|
|
93
36
|
```
|
|
94
37
|
|
|
95
|
-
|
|
38
|
+
Try the local working tree from this repository checkout:
|
|
96
39
|
|
|
97
40
|
```bash
|
|
98
|
-
|
|
41
|
+
pi -e ./experimental/pi-jupyter
|
|
42
|
+
# or
|
|
43
|
+
just try jupyter
|
|
99
44
|
```
|
|
100
45
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
46
|
+
Avoid loading both a global npm installation and the local workspace at the same time; duplicate instances register the same shortcuts.
|
|
47
|
+
|
|
48
|
+
## 🚀 Quick start
|
|
49
|
+
|
|
50
|
+
1. Start Pi in a directory containing a notebook.
|
|
51
|
+
2. Run `/jupyter` and choose a notebook. The picker lists top-level `.ipynb` files and also accepts an explicit path.
|
|
52
|
+
3. Keep editing normally while the preview follows valid saved changes.
|
|
53
|
+
4. Press `Shift+F8` to focus the panel for scrolling, then `Escape` or `F8` to return to the editor.
|
|
54
|
+
5. Press `F8` or choose **Close preview** from `/jupyter` when finished.
|
|
55
|
+
|
|
56
|
+
## 💬 Command
|
|
57
|
+
|
|
58
|
+
Pi-jupyter registers only `/jupyter`, and it requires Pi's interactive TUI mode. With no arguments it opens a shallow current-state menu. The menu shows the selected filename, open/closed state, cell count, load time, stale errors, and whether an open preview is hidden below 90 terminal columns.
|
|
59
|
+
|
|
60
|
+
Primary menu actions depend on the current state:
|
|
61
|
+
|
|
62
|
+
- **Choose a notebook…** lists top-level notebooks or accepts an explicit path; Escape returns without changing the preview.
|
|
63
|
+
- **Open / Focus preview** performs the most relevant next viewing action.
|
|
64
|
+
- **Refresh from disk** keeps the last valid version when a save is incomplete or invalid.
|
|
65
|
+
- **Switch notebook…** commits the new path and watcher only after the candidate loads successfully.
|
|
66
|
+
- **Close preview** closes the panel and watcher but retains the selection for a later reopen.
|
|
67
|
+
- **Controls and shortcuts** shows keyboard help; Escape goes back and Ctrl+C closes the flow.
|
|
68
|
+
|
|
69
|
+
Loading from the menu is cancellable with Escape. A cancelled or failed load leaves the previous path, content, watcher, scroll position, and panel state unchanged.
|
|
70
|
+
|
|
71
|
+
### Advanced direct routes
|
|
72
|
+
|
|
73
|
+
Known actions and nested scroll controls complete after `/jupyter `. Unknown actions, invalid counts, and trailing arguments are rejected.
|
|
74
|
+
|
|
75
|
+
| Route | Effect |
|
|
76
|
+
| --- | --- |
|
|
77
|
+
| `/jupyter open [path]` | Open a path, the current selection, or the first discovered top-level notebook. |
|
|
78
|
+
| `/jupyter toggle [path]` | Toggle the current preview, or open a supplied path. |
|
|
79
|
+
| `/jupyter focus` | Focus the open panel for keyboard scrolling. |
|
|
80
|
+
| `/jupyter refresh` | Reload the current selection while preserving the last valid version on failure. |
|
|
81
|
+
| `/jupyter close` | Close the panel and watcher. |
|
|
82
|
+
| `/jupyter scroll up [lines]` / `down [lines]` | Scroll by a positive line count; defaults to 3. |
|
|
83
|
+
| `/jupyter scroll page-up` / `page-down` | Scroll by 12 lines. |
|
|
84
|
+
| `/jupyter scroll top` | Return to the first rendered line. |
|
|
85
|
+
|
|
86
|
+
### Migration from preview commands
|
|
87
|
+
|
|
88
|
+
The experimental package no longer registers the old slash names. Equivalent routes are:
|
|
89
|
+
|
|
90
|
+
| Previous command | Replacement |
|
|
91
|
+
| --- | --- |
|
|
92
|
+
| `/jupyter-preview [path]` | `/jupyter open [path]` |
|
|
93
|
+
| `/jupyter-preview-toggle [path]` | `/jupyter toggle [path]` |
|
|
94
|
+
| `/jupyter-preview-focus` | `/jupyter focus` |
|
|
95
|
+
| `/jupyter-preview-refresh` | `/jupyter refresh` |
|
|
96
|
+
| `/jupyter-preview-close` | `/jupyter close` |
|
|
97
|
+
| `/jupyter-preview-up [lines]` / `-down [lines]` | `/jupyter scroll up [lines]` / `down [lines]` |
|
|
98
|
+
| `/jupyter-preview-page-up` / `-page-down` | `/jupyter scroll page-up` / `page-down` |
|
|
99
|
+
| `/jupyter-preview-top` | `/jupyter scroll top` |
|
|
100
|
+
|
|
101
|
+
## ⌨️ Shortcuts
|
|
102
|
+
|
|
103
|
+
| Shortcut | Action |
|
|
104
|
+
| --- | --- |
|
|
105
|
+
| `F8` | Toggle the preview. |
|
|
106
|
+
| `Shift+F8` | Focus the preview. |
|
|
107
|
+
| `Ctrl+Alt+J` / `Ctrl+Alt+K` | Scroll down/up without focusing. |
|
|
108
|
+
| `Ctrl+Alt+D` / `Ctrl+Alt+U` | Page down/up without focusing. |
|
|
109
|
+
| Drag the left border | Resize the panel. |
|
|
110
|
+
|
|
111
|
+
While focused, use `Up`, `Down`, `PgUp`, `PgDn`, `Home`, or `j`, `k`, `u`, `d`, `g`. Use `Escape` or `F8` to release focus.
|
|
112
|
+
|
|
113
|
+
## 🔒 Security and limits
|
|
114
|
+
|
|
115
|
+
- Pi extensions run with your full user permissions; install only trusted code.
|
|
116
|
+
- An explicitly supplied notebook path may point outside the current project. The extension only reads and watches the selected file; review the path before opening it.
|
|
117
|
+
- Notebook previews are limited to regular files no larger than 10 MB.
|
|
118
|
+
- Source display is limited to 12 lines per cell and rendered output to 24 lines per code cell.
|
|
119
|
+
- PNG decoding accepts non-interlaced 8-bit images and rejects decoded images above 16 million pixels.
|
|
120
|
+
- Notebook text, paths, errors, and output are escaped before terminal rendering to prevent embedded terminal controls.
|
|
121
|
+
- The extension does not execute notebook code, contact a Jupyter server, or send notebook contents to a separate service.
|
|
122
|
+
|
|
123
|
+
## 🧪 Experimental limitations
|
|
124
|
+
|
|
125
|
+
- Preview rendering is intentionally static and does not provide notebook editing or kernel execution.
|
|
126
|
+
- Notebook discovery scans only files directly inside Pi's current working directory.
|
|
127
|
+
- Rich HTML, JavaScript, widgets, LaTeX, and most MIME-specific output are not rendered.
|
|
128
|
+
- Long cells and outputs are summarized rather than interactively expanded.
|
|
129
|
+
- An open overlay is hidden below 90 terminal columns and reappears when the terminal is widened; `/jupyter` reports this hidden state.
|
|
130
|
+
- Mouse resizing depends on terminal SGR mouse reporting and may require the terminal's selection modifier for text selection.
|
|
131
|
+
|
|
132
|
+
## 🗂️ Package layout
|
|
133
|
+
|
|
134
|
+
```text
|
|
135
|
+
src/index.ts Thin Pi entrypoint
|
|
136
|
+
src/jupyter-command.ts Single-command parser, completions, and direct-route dispatch
|
|
137
|
+
src/jupyter-menu.ts Current-state menu, picker, help, and responsive state summaries
|
|
138
|
+
src/jupyter-preview.ts Shortcuts, tool hooks, atomic transitions, watcher, and lifecycle
|
|
139
|
+
src/notebook-panel.ts Overlay component, adaptive hints, scrolling, and mouse resizing
|
|
140
|
+
src/notebook.ts Cancellable loading, validation, and cell/output rendering
|
|
141
|
+
src/png-thumbnail.ts Bounded PNG decoding and ANSI thumbnail rendering
|
|
142
|
+
|
|
143
|
+
test/jupyter-command.test.ts Command, completion, menu, navigation, and help behavior
|
|
144
|
+
test/jupyter-preview.test.ts Paths, validation, modes, rendering, hooks, and lifecycle
|
|
145
|
+
test/jupyter-transitions.test.ts Cancellation, failure preservation, races, and reopen behavior
|
|
111
146
|
```
|
|
112
147
|
|
|
113
|
-
##
|
|
114
|
-
|
|
115
|
-
- `/jupyter-preview [path]` — open or refresh the right-side notebook preview.
|
|
116
|
-
- `/jupyter-preview-toggle [path]` — toggle the preview.
|
|
117
|
-
- `/jupyter-preview-focus` — focus the panel so arrow keys can scroll it.
|
|
118
|
-
- `/jupyter-preview-refresh` — reload the current notebook from disk.
|
|
119
|
-
- `/jupyter-preview-close` — close the preview.
|
|
120
|
-
|
|
121
|
-
## Shortcuts
|
|
122
|
-
|
|
123
|
-
- `F8` — toggle preview.
|
|
124
|
-
- `Shift+F8` — focus preview for scrolling.
|
|
125
|
-
- `Ctrl+Alt+J` / `Ctrl+Alt+K` — scroll preview down/up without focusing it.
|
|
126
|
-
- `Ctrl+Alt+D` / `Ctrl+Alt+U` — page down/up without focusing it.
|
|
127
|
-
- Drag the preview panel's left border with the mouse to resize it.
|
|
128
|
-
- In focused preview: `↑`, `↓`, `PgUp`, `PgDn`, `Home` or `j`, `k`, `u`, `d`, `g` scroll; `Esc` or `F8` returns focus to the editor.
|
|
129
|
-
|
|
130
|
-
## Notes
|
|
131
|
-
|
|
132
|
-
PNG outputs are rendered as truecolor ANSI thumbnails, so matplotlib-style `image/png` output is visible in Ghostty even inside the right-side overlay. Other image formats use `@mariozechner/pi-tui` terminal image support when available, otherwise they fall back to an image placeholder.
|
|
148
|
+
## 🔎 Keywords
|
|
133
149
|
|
|
134
|
-
|
|
135
|
-
Mouse resizing uses standard terminal mouse reporting; if your terminal reserves mouse drag for selection, use Shift-drag (or your terminal's selection modifier) to select text instead.
|
|
150
|
+
Pi extension, Jupyter notebook, ipynb preview, terminal UI, notebook side panel.
|
|
136
151
|
|
|
137
|
-
|
|
152
|
+
## 📄 License
|
|
138
153
|
|
|
139
|
-
|
|
140
|
-
- `/jupyter-preview-up [lines]`
|
|
141
|
-
- `/jupyter-preview-page-down`
|
|
142
|
-
- `/jupyter-preview-page-up`
|
|
143
|
-
- `/jupyter-preview-top`
|
|
154
|
+
[MIT](LICENSE)
|
package/package.json
CHANGED
|
@@ -1,36 +1,46 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
2
|
+
"name": "@narumitw/pi-jupyter",
|
|
3
|
+
"version": "0.33.0",
|
|
4
|
+
"description": "Experimental Pi extension for previewing Jupyter notebooks in a terminal side panel.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"private": false,
|
|
8
|
+
"keywords": [
|
|
9
|
+
"pi-package",
|
|
10
|
+
"pi-extension",
|
|
11
|
+
"pi",
|
|
12
|
+
"jupyter",
|
|
13
|
+
"notebook",
|
|
14
|
+
"preview"
|
|
15
|
+
],
|
|
16
|
+
"files": [
|
|
17
|
+
"src",
|
|
18
|
+
"README.md",
|
|
19
|
+
"LICENSE"
|
|
20
|
+
],
|
|
21
|
+
"pi": {
|
|
22
|
+
"extensions": [
|
|
23
|
+
"./src/index.ts"
|
|
24
|
+
]
|
|
25
|
+
},
|
|
26
|
+
"scripts": {
|
|
27
|
+
"check": "biome check . && npm run typecheck",
|
|
28
|
+
"format": "biome check --write .",
|
|
29
|
+
"typecheck": "tsc --noEmit"
|
|
30
|
+
},
|
|
31
|
+
"peerDependencies": {
|
|
32
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
33
|
+
"@earendil-works/pi-tui": "*"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@biomejs/biome": "2.5.5",
|
|
37
|
+
"@earendil-works/pi-coding-agent": "0.82.1",
|
|
38
|
+
"@earendil-works/pi-tui": "0.82.1",
|
|
39
|
+
"typescript": "7.0.2"
|
|
40
|
+
},
|
|
41
|
+
"repository": {
|
|
42
|
+
"type": "git",
|
|
43
|
+
"url": "https://github.com/narumiruna/pi-extensions",
|
|
44
|
+
"directory": "experimental/pi-jupyter"
|
|
45
|
+
}
|
|
36
46
|
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from "./jupyter-preview.js";
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { AutocompleteItem } from "@earendil-works/pi-tui";
|
|
3
|
+
|
|
4
|
+
export type JupyterScrollDirection = "up" | "down" | "page-up" | "page-down" | "top";
|
|
5
|
+
|
|
6
|
+
export type JupyterCommand =
|
|
7
|
+
| { action: "menu" }
|
|
8
|
+
| { action: "open"; path?: string }
|
|
9
|
+
| { action: "toggle"; path?: string }
|
|
10
|
+
| { action: "focus" | "refresh" | "close" }
|
|
11
|
+
| { action: "scroll"; direction: JupyterScrollDirection; lines?: number };
|
|
12
|
+
|
|
13
|
+
export type JupyterCommandActions = {
|
|
14
|
+
showMenu(ctx: ExtensionCommandContext): Promise<void>;
|
|
15
|
+
open(ctx: ExtensionCommandContext, path?: string): Promise<void>;
|
|
16
|
+
toggle(ctx: ExtensionCommandContext, path?: string): Promise<void>;
|
|
17
|
+
focus(ctx: ExtensionCommandContext): void;
|
|
18
|
+
refresh(ctx: ExtensionCommandContext): Promise<void>;
|
|
19
|
+
close(ctx: ExtensionCommandContext): void | Promise<void>;
|
|
20
|
+
scroll(
|
|
21
|
+
direction: JupyterScrollDirection,
|
|
22
|
+
lines: number | undefined,
|
|
23
|
+
ctx: ExtensionCommandContext,
|
|
24
|
+
): void;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const ROOT_COMPLETIONS: AutocompleteItem[] = [
|
|
28
|
+
{ value: "open ", label: "open", description: "Open a notebook preview" },
|
|
29
|
+
{ value: "focus", label: "focus", description: "Focus the open preview" },
|
|
30
|
+
{ value: "refresh", label: "refresh", description: "Reload the selected notebook" },
|
|
31
|
+
{ value: "close", label: "close", description: "Close the preview" },
|
|
32
|
+
{ value: "toggle ", label: "toggle", description: "Toggle the preview" },
|
|
33
|
+
{ value: "scroll ", label: "scroll", description: "Advanced scrolling controls" },
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
const SCROLL_COMPLETIONS: AutocompleteItem[] = [
|
|
37
|
+
{ value: "scroll up ", label: "scroll up", description: "Scroll up by lines" },
|
|
38
|
+
{ value: "scroll down ", label: "scroll down", description: "Scroll down by lines" },
|
|
39
|
+
{ value: "scroll page-up", label: "scroll page-up", description: "Scroll up one page" },
|
|
40
|
+
{ value: "scroll page-down", label: "scroll page-down", description: "Scroll down one page" },
|
|
41
|
+
{ value: "scroll top", label: "scroll top", description: "Return to the top" },
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
export function registerJupyterCommand(pi: ExtensionAPI, actions: JupyterCommandActions): void {
|
|
45
|
+
pi.registerCommand("jupyter", {
|
|
46
|
+
description: "Manage the current Jupyter notebook preview",
|
|
47
|
+
getArgumentCompletions: completeJupyterArguments,
|
|
48
|
+
handler: async (args, ctx) => {
|
|
49
|
+
const command = parseJupyterCommand(args);
|
|
50
|
+
switch (command.action) {
|
|
51
|
+
case "menu":
|
|
52
|
+
await actions.showMenu(ctx);
|
|
53
|
+
return;
|
|
54
|
+
case "open":
|
|
55
|
+
await actions.open(ctx, command.path);
|
|
56
|
+
return;
|
|
57
|
+
case "toggle":
|
|
58
|
+
await actions.toggle(ctx, command.path);
|
|
59
|
+
return;
|
|
60
|
+
case "focus":
|
|
61
|
+
actions.focus(ctx);
|
|
62
|
+
return;
|
|
63
|
+
case "refresh":
|
|
64
|
+
await actions.refresh(ctx);
|
|
65
|
+
return;
|
|
66
|
+
case "close":
|
|
67
|
+
await actions.close(ctx);
|
|
68
|
+
return;
|
|
69
|
+
case "scroll":
|
|
70
|
+
actions.scroll(command.direction, command.lines, ctx);
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function completeJupyterArguments(prefix: string): AutocompleteItem[] | null {
|
|
77
|
+
if (
|
|
78
|
+
prefix.startsWith("open ") ||
|
|
79
|
+
prefix.startsWith("toggle ") ||
|
|
80
|
+
prefix.startsWith("scroll up ") ||
|
|
81
|
+
prefix.startsWith("scroll down ")
|
|
82
|
+
) {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
const completions = prefix.startsWith("scroll ") ? SCROLL_COMPLETIONS : ROOT_COMPLETIONS;
|
|
86
|
+
const filtered = completions.filter((item) => item.value.startsWith(prefix));
|
|
87
|
+
return filtered.length > 0 ? filtered : null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function parseJupyterCommand(rawArgs: string): JupyterCommand {
|
|
91
|
+
const args = rawArgs.trim();
|
|
92
|
+
if (!args) return { action: "menu" };
|
|
93
|
+
const [action = "", ...rest] = args.split(/\s+/);
|
|
94
|
+
const remainder = args.slice(action.length).trim();
|
|
95
|
+
switch (action) {
|
|
96
|
+
case "open":
|
|
97
|
+
case "toggle":
|
|
98
|
+
return remainder ? { action, path: remainder } : { action };
|
|
99
|
+
case "focus":
|
|
100
|
+
case "refresh":
|
|
101
|
+
case "close":
|
|
102
|
+
assertNoArguments(`/jupyter ${action}`, remainder);
|
|
103
|
+
return { action };
|
|
104
|
+
case "scroll":
|
|
105
|
+
return parseScrollCommand(rest);
|
|
106
|
+
default:
|
|
107
|
+
throw new Error(`Unknown /jupyter action: ${action}. Run /jupyter to see available actions.`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function parseScrollCommand(parts: string[]): JupyterCommand {
|
|
112
|
+
const [direction, ...rest] = parts;
|
|
113
|
+
if (!direction || !["up", "down", "page-up", "page-down", "top"].includes(direction)) {
|
|
114
|
+
throw new Error(`Unknown /jupyter scroll action: ${direction ?? "(missing)"}.`);
|
|
115
|
+
}
|
|
116
|
+
if (direction === "up" || direction === "down") {
|
|
117
|
+
if (rest.length > 1)
|
|
118
|
+
throw new Error(`/jupyter scroll ${direction} accepts at most one line count.`);
|
|
119
|
+
return {
|
|
120
|
+
action: "scroll",
|
|
121
|
+
direction,
|
|
122
|
+
lines: rest[0] === undefined ? undefined : parsePositiveLineCount(rest[0]),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
assertNoArguments(`/jupyter scroll ${direction}`, rest.join(" "));
|
|
126
|
+
return { action: "scroll", direction: direction as JupyterScrollDirection };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function parsePositiveLineCount(value: string): number {
|
|
130
|
+
if (!/^[1-9]\d*$/.test(value)) throw new Error("Scroll amount must be one positive integer.");
|
|
131
|
+
const parsed = Number(value);
|
|
132
|
+
if (!Number.isSafeInteger(parsed)) throw new Error("Scroll amount must be one positive integer.");
|
|
133
|
+
return parsed;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function assertNoArguments(command: string, args: string): void {
|
|
137
|
+
if (args) throw new Error(`${command} does not accept arguments.`);
|
|
138
|
+
}
|