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 CHANGED
@@ -1,35 +1,87 @@
1
- # vim-bindings — Pi REPL Vim Mode
1
+ # pi-vim — Vim Mode for Pi REPL
2
2
 
3
- A modal vim-like editing extension for Pi's REPL prompt, covering the
4
- high-frequency ("90%") command surface without trying to clone full Vim.
3
+ Modal vim-like editing for Pi's REPL prompt.
4
+ Focus: the high-frequency 90% command surface, not full Vim.
5
5
 
6
- ## Loading
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
- pi --extension /path/to/vim-bindings/index.ts
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-bindings/index.ts"]
31
+ "extensions": ["./pi-extensions/pi-vim/index.ts"]
17
32
  }
18
33
  ```
19
34
 
20
- The mode indicator (`INSERT` / `NORMAL`) is shown in the bottom-right corner
21
- of the prompt.
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
- ## Supported command surface
77
+ ## Full reference
26
78
 
27
79
  ### Mode switching
28
80
 
29
81
  | Key | Action |
30
82
  |----------|----------------------------------------|
31
- | `Esc` | Insert → Normal mode |
32
- | `Esc` | Normal mode → pass to Pi (abort agent) |
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$` | Delete to EOL + Insert |
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-bindings
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 === "\x1b") {
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 (matchesKey(data, "escape")) {
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
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-vim",
3
- "version": "0.1.8",
3
+ "version": "0.1.9",
4
4
  "description": "Vim-style modal editing for Pi's TUI editor",
5
5
  "type": "module",
6
6
  "keywords": [