pi-vim 0.1.8 → 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 +68 -13
- package/index.ts +31 -4
- 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,6 +113,7 @@ 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) |
|
|
66
119
|
| `{count}gg` | Go to line `{count}` (1-indexed, clamped) |
|
|
@@ -133,6 +186,7 @@ word, char-find, and linewise motions. Maximum total count: `9999`.
|
|
|
133
186
|
| `d{count}W/E/B` | Forward/backward `{count}` `WORD` motions |
|
|
134
187
|
| `d$` | To end of line |
|
|
135
188
|
| `d0` | To start of line |
|
|
189
|
+
| `d^` | To first non-whitespace char of line |
|
|
136
190
|
| `dd` | Current line (linewise) |
|
|
137
191
|
| `{count}dd` | `{count}` lines (linewise) |
|
|
138
192
|
| `d{count}j` | Current line + `{count}` lines below (linewise) |
|
|
@@ -162,7 +216,7 @@ Same motion and count set as `d`. Deletes text then enters Insert mode.
|
|
|
162
216
|
| `ciw` | Change inner word |
|
|
163
217
|
| `caw` | Change around word |
|
|
164
218
|
| `cc` | Delete line content + Insert |
|
|
165
|
-
| `c$`
|
|
219
|
+
| `c$` / `c0` / `c^` | Delete to EOL / BOL / first non-whitespace + Insert |
|
|
166
220
|
| … | All `d` motions apply |
|
|
167
221
|
|
|
168
222
|
#### Single-key edits
|
|
@@ -199,6 +253,7 @@ Same motion set as `d`. Writes to register, **no text mutation**.
|
|
|
199
253
|
| `yB` | Backward to `WORD` start |
|
|
200
254
|
| `y$` | To end of line |
|
|
201
255
|
| `y0` | To start of line |
|
|
256
|
+
| `y^` | To first non-whitespace char of line |
|
|
202
257
|
| `yf{c}` | To and including `char` |
|
|
203
258
|
| `yiw` | Inner word |
|
|
204
259
|
| `yaw` | Around word (includes spaces) |
|
|
@@ -295,6 +350,6 @@ These are **explicitly deferred** and not planned for this feature:
|
|
|
295
350
|
Run tests:
|
|
296
351
|
|
|
297
352
|
```
|
|
298
|
-
cd vim
|
|
353
|
+
cd pi-vim
|
|
299
354
|
npm test
|
|
300
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
|
|
|
@@ -758,6 +764,11 @@ export class ModalEditor extends CustomEditor {
|
|
|
758
764
|
return;
|
|
759
765
|
}
|
|
760
766
|
|
|
767
|
+
if (data === "^") {
|
|
768
|
+
this.moveCursorToFirstNonWhitespace();
|
|
769
|
+
return;
|
|
770
|
+
}
|
|
771
|
+
|
|
761
772
|
if (data === "w") {
|
|
762
773
|
const count = this.takeTotalCount(1);
|
|
763
774
|
return this.moveWord("forward", "start", count, "word");
|
|
@@ -920,6 +931,12 @@ export class ModalEditor extends CustomEditor {
|
|
|
920
931
|
super.handleInput(CTRL_A);
|
|
921
932
|
}
|
|
922
933
|
|
|
934
|
+
private moveCursorToFirstNonWhitespace(): void {
|
|
935
|
+
const { line, col } = this.getCurrentLineAndCol();
|
|
936
|
+
const targetCol = findFirstNonWhitespaceColumn(line);
|
|
937
|
+
this.moveCursorBy(targetCol - col);
|
|
938
|
+
}
|
|
939
|
+
|
|
923
940
|
private moveCursorToBufferStart(): void {
|
|
924
941
|
this.moveCursorToLineStart(0);
|
|
925
942
|
}
|
|
@@ -1388,6 +1405,11 @@ export class ModalEditor extends CustomEditor {
|
|
|
1388
1405
|
return true;
|
|
1389
1406
|
}
|
|
1390
1407
|
|
|
1408
|
+
if (motion === "^") {
|
|
1409
|
+
this.deleteRange(col, findFirstNonWhitespaceColumn(this.getLines()[cursor.line] ?? ""), false);
|
|
1410
|
+
return true;
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1391
1413
|
const wordMotion = this.resolveWordMotion(motion);
|
|
1392
1414
|
if (wordMotion) {
|
|
1393
1415
|
const lineLocalRange = this.tryWordMotionLineLocalRange(
|
|
@@ -1511,6 +1533,11 @@ export class ModalEditor extends CustomEditor {
|
|
|
1511
1533
|
return true;
|
|
1512
1534
|
}
|
|
1513
1535
|
|
|
1536
|
+
if (motion === "^") {
|
|
1537
|
+
this.yankRange(col, findFirstNonWhitespaceColumn(line), false);
|
|
1538
|
+
return true;
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1514
1541
|
const wordMotion = this.resolveWordMotion(motion);
|
|
1515
1542
|
if (wordMotion) {
|
|
1516
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
|
*/
|