pi-vim 0.1.7 → 0.1.9
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 +71 -14
- package/index.ts +39 -6
- package/motions.ts +8 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,35 +1,87 @@
|
|
|
1
|
-
# vim
|
|
1
|
+
# pi-vim — Vim Mode for Pi REPL
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
high-frequency
|
|
3
|
+
Modal vim-like editing for Pi's REPL prompt.
|
|
4
|
+
Focus: the high-frequency 90% command surface, not full Vim.
|
|
5
5
|
|
|
6
|
-
##
|
|
6
|
+
## TL;DR
|
|
7
7
|
|
|
8
|
+
- Problem: REPL editing is slow with only linear cursor movement.
|
|
9
|
+
- Solution: modal editing (`INSERT`/`NORMAL`) with Vim-style motions,
|
|
10
|
+
operators, counts, and repeatable workflows.
|
|
11
|
+
- Install: `pi install npm:pi-vim`
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pi install npm:pi-vim
|
|
8
17
|
```
|
|
9
|
-
|
|
18
|
+
|
|
19
|
+
Restart Pi after install.
|
|
20
|
+
|
|
21
|
+
### Local loading (dev)
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pi --extension /path/to/pi-vim/index.ts
|
|
10
25
|
```
|
|
11
26
|
|
|
12
27
|
Or add to `.pi/settings.json`:
|
|
13
28
|
|
|
14
29
|
```json
|
|
15
30
|
{
|
|
16
|
-
"extensions": ["./pi-extensions/vim
|
|
31
|
+
"extensions": ["./pi-extensions/pi-vim/index.ts"]
|
|
17
32
|
}
|
|
18
33
|
```
|
|
19
34
|
|
|
20
|
-
|
|
21
|
-
|
|
35
|
+
## 30-second quickstart
|
|
36
|
+
|
|
37
|
+
Try on multi-line input:
|
|
38
|
+
|
|
39
|
+
```text
|
|
40
|
+
Esc # NORMAL mode
|
|
41
|
+
3gg # jump to absolute line 3
|
|
42
|
+
2dw # delete two words
|
|
43
|
+
u # undo
|
|
44
|
+
2} # jump two paragraphs forward
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Mode indicator (`INSERT` / `NORMAL`) appears at bottom-right.
|
|
48
|
+
|
|
49
|
+
## Why pi-vim
|
|
50
|
+
|
|
51
|
+
- Fast modal editing without leaving Pi.
|
|
52
|
+
- Count-aware motions/operators (`2dw`, `3G`, `d2j`, `2}`).
|
|
53
|
+
- Strong REPL-focused defaults; safe out-of-scope boundaries documented.
|
|
54
|
+
- Clipboard/register behavior is explicit and tested.
|
|
55
|
+
|
|
56
|
+
## For you / not for you
|
|
57
|
+
|
|
58
|
+
Use pi-vim if you want fast Vim muscle-memory in Pi prompts.
|
|
59
|
+
Skip it if you need full Vim feature parity (visual mode, macros, search,
|
|
60
|
+
ex-commands, etc.).
|
|
61
|
+
|
|
62
|
+
## Common recipes
|
|
63
|
+
|
|
64
|
+
| Goal | Keys |
|
|
65
|
+
|------|------|
|
|
66
|
+
| Jump to exact line 25 | `25gg` (or `25G`) |
|
|
67
|
+
| Delete two words | `2dw` |
|
|
68
|
+
| Change to end of line | `C` |
|
|
69
|
+
| Delete current + 2 lines below | `d2j` |
|
|
70
|
+
| Yank 3 lines | `3yy` |
|
|
71
|
+
| Join 3 lines with spacing | `3J` |
|
|
72
|
+
| Jump 2 paragraphs forward | `2}` |
|
|
73
|
+
| Undo last edit | `u` |
|
|
22
74
|
|
|
23
75
|
---
|
|
24
76
|
|
|
25
|
-
##
|
|
77
|
+
## Full reference
|
|
26
78
|
|
|
27
79
|
### Mode switching
|
|
28
80
|
|
|
29
81
|
| Key | Action |
|
|
30
82
|
|----------|----------------------------------------|
|
|
31
|
-
| `Esc`
|
|
32
|
-
| `Esc`
|
|
83
|
+
| `Esc` / `Ctrl+[` | Insert → Normal mode |
|
|
84
|
+
| `Esc` / `Ctrl+[` | Normal mode → pass to Pi (abort agent) |
|
|
33
85
|
| `i` | Normal → Insert at cursor |
|
|
34
86
|
| `a` | Normal → Insert after cursor |
|
|
35
87
|
| `I` | Normal → Insert at line start |
|
|
@@ -61,9 +113,12 @@ A `{count}` prefix can be prepended to any navigation key (max: `9999`).
|
|
|
61
113
|
| `{count}h/l` | Move left/right `{count}` cols |
|
|
62
114
|
| `{count}j/k` | Move down/up `{count}` lines (clamped to buffer size) |
|
|
63
115
|
| `0` | Line start |
|
|
116
|
+
| `^` | First non-whitespace char of line |
|
|
64
117
|
| `$` | Line end |
|
|
65
118
|
| `gg` | Buffer start (line 1) |
|
|
119
|
+
| `{count}gg` | Go to line `{count}` (1-indexed, clamped) |
|
|
66
120
|
| `G` | Buffer end (last line) |
|
|
121
|
+
| `{count}G` | Go to line `{count}` (1-indexed, clamped) |
|
|
67
122
|
| `w` | Next `word` start (keyword/punctuation aware) |
|
|
68
123
|
| `b` | Previous `word` start |
|
|
69
124
|
| `e` | `word` end (inclusive) |
|
|
@@ -131,6 +186,7 @@ word, char-find, and linewise motions. Maximum total count: `9999`.
|
|
|
131
186
|
| `d{count}W/E/B` | Forward/backward `{count}` `WORD` motions |
|
|
132
187
|
| `d$` | To end of line |
|
|
133
188
|
| `d0` | To start of line |
|
|
189
|
+
| `d^` | To first non-whitespace char of line |
|
|
134
190
|
| `dd` | Current line (linewise) |
|
|
135
191
|
| `{count}dd` | `{count}` lines (linewise) |
|
|
136
192
|
| `d{count}j` | Current line + `{count}` lines below (linewise) |
|
|
@@ -160,7 +216,7 @@ Same motion and count set as `d`. Deletes text then enters Insert mode.
|
|
|
160
216
|
| `ciw` | Change inner word |
|
|
161
217
|
| `caw` | Change around word |
|
|
162
218
|
| `cc` | Delete line content + Insert |
|
|
163
|
-
| `c$`
|
|
219
|
+
| `c$` / `c0` / `c^` | Delete to EOL / BOL / first non-whitespace + Insert |
|
|
164
220
|
| … | All `d` motions apply |
|
|
165
221
|
|
|
166
222
|
#### Single-key edits
|
|
@@ -197,6 +253,7 @@ Same motion set as `d`. Writes to register, **no text mutation**.
|
|
|
197
253
|
| `yB` | Backward to `WORD` start |
|
|
198
254
|
| `y$` | To end of line |
|
|
199
255
|
| `y0` | To start of line |
|
|
256
|
+
| `y^` | To first non-whitespace char of line |
|
|
200
257
|
| `yf{c}` | To and including `char` |
|
|
201
258
|
| `yiw` | Inner word |
|
|
202
259
|
| `yaw` | Around word (includes spaces) |
|
|
@@ -274,7 +331,7 @@ These are **explicitly deferred** and not planned for this feature:
|
|
|
274
331
|
- Ex command surface (`:s`, `:g`, `:r`, …)
|
|
275
332
|
- Search mode (`/`, `?`, `n`, `N`)
|
|
276
333
|
- Repeat (`.`)
|
|
277
|
-
- Extended count prefix beyond currently supported motions (e.g.
|
|
334
|
+
- Extended count prefix beyond currently supported motions (e.g. `:`, global operator counts)
|
|
278
335
|
- Redo (`<C-r>`) — no native redo primitive in the underlying readline editor;
|
|
279
336
|
deferred until a suitable hook is available.
|
|
280
337
|
- Window / tab / buffer management
|
|
@@ -293,6 +350,6 @@ These are **explicitly deferred** and not planned for this feature:
|
|
|
293
350
|
Run tests:
|
|
294
351
|
|
|
295
352
|
```
|
|
296
|
-
cd vim
|
|
353
|
+
cd pi-vim
|
|
297
354
|
npm test
|
|
298
355
|
```
|
package/index.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Usage: pi --extension ./index.ts
|
|
5
5
|
*
|
|
6
|
-
* - Escape: insert → normal mode (in normal mode, aborts agent)
|
|
6
|
+
* - Escape / ctrl+[: insert → normal mode (in normal mode, aborts agent)
|
|
7
7
|
* - i: normal → insert mode (at cursor)
|
|
8
8
|
* - a: insert after cursor
|
|
9
9
|
* - A: insert at end of line
|
|
@@ -12,11 +12,12 @@
|
|
|
12
12
|
* - O: open new line above (insert mode)
|
|
13
13
|
* - hjkl: navigation in normal mode
|
|
14
14
|
* - 0/$: line start/end
|
|
15
|
+
* - ^: first non-whitespace char of line
|
|
15
16
|
* - x: delete char under cursor
|
|
16
17
|
* - D: delete to end of line
|
|
17
18
|
* - S: substitute line (delete line content + insert mode)
|
|
18
19
|
* - s: substitute char (delete char + insert mode)
|
|
19
|
-
* - d{motion}: delete with motion (`w/b/e` + `W/B/E`, `$`, `0`, `dd`, `f/t/F/T{char}`)
|
|
20
|
+
* - d{motion}: delete with motion (`w/b/e` + `W/B/E`, `$`, `0`, `^`, `dd`, `f/t/F/T{char}`)
|
|
20
21
|
* - c{motion}: change with same motion set as `d` (then enter insert mode)
|
|
21
22
|
* - y{motion}: yank with same motion set as `d` (no text mutation)
|
|
22
23
|
* - f{char}: jump to next {char} on line
|
|
@@ -84,6 +85,7 @@ import {
|
|
|
84
85
|
reverseCharMotion,
|
|
85
86
|
findCharMotionTarget,
|
|
86
87
|
findParagraphMotionTarget,
|
|
88
|
+
findFirstNonWhitespaceColumn,
|
|
87
89
|
type WordMotionClass,
|
|
88
90
|
} from "./motions.js";
|
|
89
91
|
import {
|
|
@@ -134,6 +136,10 @@ export class ModalEditor extends CustomEditor {
|
|
|
134
136
|
this.pendingGCount = "";
|
|
135
137
|
}
|
|
136
138
|
|
|
139
|
+
private isEscapeLikeInput(data: string): boolean {
|
|
140
|
+
return matchesKey(data, "escape") || matchesKey(data, "ctrl+[");
|
|
141
|
+
}
|
|
142
|
+
|
|
137
143
|
private stripBracketedPasteInNormalMode(data: string): { filtered: string | null; stripped: boolean } {
|
|
138
144
|
let chunk = data;
|
|
139
145
|
let stripped = false;
|
|
@@ -172,7 +178,7 @@ export class ModalEditor extends CustomEditor {
|
|
|
172
178
|
handleInput(data: string): void {
|
|
173
179
|
if (this.mode !== "insert") {
|
|
174
180
|
if (this.discardingBracketedPasteInNormalMode) {
|
|
175
|
-
if (data
|
|
181
|
+
if (this.isEscapeLikeInput(data)) {
|
|
176
182
|
if (this.pendingEscWhileDiscardingBracketedPasteInNormalMode) {
|
|
177
183
|
this.pendingEscWhileDiscardingBracketedPasteInNormalMode = false;
|
|
178
184
|
this.discardingBracketedPasteInNormalMode = false;
|
|
@@ -206,7 +212,7 @@ export class ModalEditor extends CustomEditor {
|
|
|
206
212
|
data = filtered;
|
|
207
213
|
}
|
|
208
214
|
|
|
209
|
-
if (
|
|
215
|
+
if (this.isEscapeLikeInput(data)) {
|
|
210
216
|
return this.handleEscape();
|
|
211
217
|
}
|
|
212
218
|
|
|
@@ -573,8 +579,8 @@ export class ModalEditor extends CustomEditor {
|
|
|
573
579
|
|
|
574
580
|
if (!hadGCount) {
|
|
575
581
|
if (data === "g") {
|
|
576
|
-
this.takeTotalCount(1);
|
|
577
|
-
this.
|
|
582
|
+
const count = this.takeTotalCount(1);
|
|
583
|
+
this.moveCursorToLineStart(count - 1);
|
|
578
584
|
return;
|
|
579
585
|
}
|
|
580
586
|
|
|
@@ -610,6 +616,12 @@ export class ModalEditor extends CustomEditor {
|
|
|
610
616
|
return;
|
|
611
617
|
}
|
|
612
618
|
|
|
619
|
+
if (data === "G") {
|
|
620
|
+
const count = this.takeTotalCount(1);
|
|
621
|
+
this.moveCursorToLineStart(count - 1);
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
|
|
613
625
|
const supportsCountedStandaloneEdit = (
|
|
614
626
|
data === "x"
|
|
615
627
|
|| data === "s"
|
|
@@ -752,6 +764,11 @@ export class ModalEditor extends CustomEditor {
|
|
|
752
764
|
return;
|
|
753
765
|
}
|
|
754
766
|
|
|
767
|
+
if (data === "^") {
|
|
768
|
+
this.moveCursorToFirstNonWhitespace();
|
|
769
|
+
return;
|
|
770
|
+
}
|
|
771
|
+
|
|
755
772
|
if (data === "w") {
|
|
756
773
|
const count = this.takeTotalCount(1);
|
|
757
774
|
return this.moveWord("forward", "start", count, "word");
|
|
@@ -914,6 +931,12 @@ export class ModalEditor extends CustomEditor {
|
|
|
914
931
|
super.handleInput(CTRL_A);
|
|
915
932
|
}
|
|
916
933
|
|
|
934
|
+
private moveCursorToFirstNonWhitespace(): void {
|
|
935
|
+
const { line, col } = this.getCurrentLineAndCol();
|
|
936
|
+
const targetCol = findFirstNonWhitespaceColumn(line);
|
|
937
|
+
this.moveCursorBy(targetCol - col);
|
|
938
|
+
}
|
|
939
|
+
|
|
917
940
|
private moveCursorToBufferStart(): void {
|
|
918
941
|
this.moveCursorToLineStart(0);
|
|
919
942
|
}
|
|
@@ -1382,6 +1405,11 @@ export class ModalEditor extends CustomEditor {
|
|
|
1382
1405
|
return true;
|
|
1383
1406
|
}
|
|
1384
1407
|
|
|
1408
|
+
if (motion === "^") {
|
|
1409
|
+
this.deleteRange(col, findFirstNonWhitespaceColumn(this.getLines()[cursor.line] ?? ""), false);
|
|
1410
|
+
return true;
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1385
1413
|
const wordMotion = this.resolveWordMotion(motion);
|
|
1386
1414
|
if (wordMotion) {
|
|
1387
1415
|
const lineLocalRange = this.tryWordMotionLineLocalRange(
|
|
@@ -1505,6 +1533,11 @@ export class ModalEditor extends CustomEditor {
|
|
|
1505
1533
|
return true;
|
|
1506
1534
|
}
|
|
1507
1535
|
|
|
1536
|
+
if (motion === "^") {
|
|
1537
|
+
this.yankRange(col, findFirstNonWhitespaceColumn(line), false);
|
|
1538
|
+
return true;
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1508
1541
|
const wordMotion = this.resolveWordMotion(motion);
|
|
1509
1542
|
if (wordMotion) {
|
|
1510
1543
|
const lineLocalRange = this.tryWordMotionLineLocalRange(
|
package/motions.ts
CHANGED
|
@@ -30,6 +30,14 @@ function clampLineIndex(lines: readonly string[], lineIndex: number): number {
|
|
|
30
30
|
return Math.max(0, Math.min(normalized, lines.length - 1));
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Column of first non-whitespace char, or 0 for blank lines.
|
|
35
|
+
*/
|
|
36
|
+
export function findFirstNonWhitespaceColumn(line: string): number {
|
|
37
|
+
const match = line.search(/\S/);
|
|
38
|
+
return match === -1 ? 0 : match;
|
|
39
|
+
}
|
|
40
|
+
|
|
33
41
|
/**
|
|
34
42
|
* True when line matches ^\s*$.
|
|
35
43
|
*/
|