pi-vim 0.3.2 → 0.9.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 CHANGED
@@ -2,10 +2,6 @@
2
2
 
3
3
  Modal vim-like editing for Pi's input prompt. Covers the high-frequency 90% command surface.
4
4
 
5
- ## why
6
-
7
- You love Pi, you love Vim, you'll love pi-vim.
8
-
9
5
  ## install
10
6
 
11
7
  ```bash
@@ -14,9 +10,52 @@ pi install npm:pi-vim
14
10
 
15
11
  Restart Pi after install.
16
12
 
13
+ ## configure
14
+
15
+ pi-vim reads persistent Pi settings from `~/.pi/agent/settings.json` and project `.pi/settings.json`.
16
+
17
+ Clipboard write mirroring is controlled by `piVim.clipboardMirror`:
18
+
19
+ ```json
20
+ {
21
+ "piVim": {
22
+ "clipboardMirror": "all"
23
+ }
24
+ }
25
+ ```
26
+
27
+ | value | behavior |
28
+ |-------|----------|
29
+ | `all` | Mirror every unnamed-register write (default/current behavior) |
30
+ | `yank` | Mirror yanks only; deletes/changes update only pi-vim's internal register |
31
+ | `never` | Never mirror register writes to the OS clipboard |
32
+
33
+ The setting controls which local register writes cross the OS clipboard boundary. `p` / `P` keep non-mirrored writes local.
34
+
35
+ ## wrapping pi-vim
36
+
37
+ Supported: `pi-vim` first, `@jordyvd/pi-image-attachments` second. pi-vim does not wrap previous editors; wrappers decorate in place or forward the CustomEditor surface: lifecycle (`handleInput`, `render`, `invalidate`), text (`getText`, `setText`, `insertTextAtCursor`, `getExpandedText`), callbacks, `actionHandlers`, flags, reads (`getLines`, `getCursor`, `getMode()`). Inverse order, insert delegates, and generic composition are unsupported.
38
+
39
+ Smoke:
40
+
41
+ ```bash
42
+ pi -e ./index.ts -e ../pi-image-attachments/index.ts
43
+ pi -e ./index.ts -e ../../../pi-image-attachments/index.ts
44
+ ```
45
+
46
+ Check: insert text; add/paste image path; see `[Image #1]`; submit text+image stripped; switch INSERT/NORMAL.
47
+
48
+ ## contributor setup
49
+
50
+ Hooks install with `npm install` after cloning. To wire them explicitly:
51
+
52
+ ```bash
53
+ npm run hooks:install
54
+ ```
55
+
17
56
  ## stats
18
57
 
19
- - **112 commands**: motions, operators, counts, text objects, undo/redo
58
+ - **188 commands**: motions, operators, counts, text objects, undo/redo, ex quit
20
59
  - **sub-µs word motions** via precomputed boundary cache (~4ms startup, ~150KB memory)
21
60
  - **0 dependencies**
22
61
 
@@ -33,22 +72,19 @@ u # undo
33
72
  2} # jump two paragraphs forward
34
73
  ```
35
74
 
36
- Mode indicator (`INSERT` / `NORMAL`) appears at bottom-right.
37
- Its label is theme-colored: reverse-video `borderMuted` for
38
- INSERT, `borderAccent` for NORMAL.
75
+ Mode indicator (`INSERT` / `NORMAL` / `EX`) appears bottom-right, theme-colored.
76
+
77
+ Requires `@mariozechner/pi-tui >= 0.47.0`. With `pi-tui >= 0.49.3` and DECSCUSR support, cursor shape follows mode; otherwise software cursor remains.
39
78
 
40
79
  ## why pi-vim
41
80
 
42
81
  - Fast modal editing without leaving Pi.
43
82
  - Count-aware motions/operators (`2dw`, `3G`, `d2j`, `2}`).
44
- - Strong REPL-focused defaults; safe out-of-scope boundaries documented.
83
+ - REPL-focused defaults; out-of-scope boundaries documented.
45
84
  - Clipboard/register behavior is explicit and tested.
46
85
 
47
- ## for you / not for you
48
-
49
- Use pi-vim if you want fast Vim muscle-memory in Pi prompts.
50
- Skip it if you need full Vim feature parity (visual mode, macros, search,
51
- ex-commands, etc.).
86
+ Use pi-vim for fast Vim muscle-memory in Pi prompts. Skip it if you need
87
+ full Vim parity (visual mode, macros, search, extended ex-commands, …).
52
88
 
53
89
  ## common recipes
54
90
 
@@ -56,6 +92,11 @@ ex-commands, etc.).
56
92
  |------|------|
57
93
  | Jump to exact line 25 | `25gg` (or `25G`) |
58
94
  | Delete two words | `2dw` |
95
+ | Change current whitespace-delimited WORD | `ciW` |
96
+ | Delete WORD plus adjacent whitespace | `daW` |
97
+ | Change inside double quotes | `ci"` |
98
+ | Delete inside parentheses | `di(` |
99
+ | Yank braces with contents | `ya{` |
59
100
  | Change to end of line | `C` |
60
101
  | Delete current + 2 lines below | `d2j` |
61
102
  | Yank 3 lines | `3yy` |
@@ -73,7 +114,8 @@ ex-commands, etc.).
73
114
  | key | action |
74
115
  |----------|----------------------------------------|
75
116
  | `Esc` / `Ctrl+[` | Insert → Normal mode |
76
- | `Esc` / `Ctrl+[` | Normal mode → pass to Pi (abort agent) |
117
+ | `Esc` / `Ctrl+[` | Normal mode → pass to Pi (aborts the agent under default Pi keybindings) |
118
+ | `:` | Normal → EX mini-mode |
77
119
  | `i` | Normal → Insert at cursor |
78
120
  | `a` | Normal → Insert after cursor |
79
121
  | `I` | Normal → Insert at first non-whitespace |
@@ -81,6 +123,24 @@ ex-commands, etc.).
81
123
  | `o` | Normal → open line below + Insert |
82
124
  | `O` | Normal → open line above + Insert |
83
125
 
126
+ Optional: heavy users may want to move Pi's `app.interrupt` off bare `escape` in `~/.pi/agent/keybindings.json` since it overlaps with Insert→Normal. Pick your own replacement; user config overrides defaults.
127
+
128
+ #### ex mini-mode
129
+
130
+ Quit-only ex flows.
131
+
132
+ | key / command | action |
133
+ |---------------|--------|
134
+ | `:` | Enter EX mini-mode |
135
+ | `Enter` | Execute pending ex command |
136
+ | `Esc` | Cancel EX mini-mode |
137
+ | `Backspace` / `Ctrl+h` | Delete one ex-command character; on bare `:` exits EX mode |
138
+ | `:q` | Quit the current Pi session only when the prompt is empty or whitespace-only; otherwise show a warning |
139
+ | `:q!` | Force quit the current Pi session even when the prompt has text |
140
+ | `:qa` | Same safe quit policy as `:q` |
141
+ | `:qa!` | Same force quit policy as `:q!` |
142
+ | unsupported `:{cmd}` | Show warning notification; no quit |
143
+
84
144
  Insert-mode shortcuts (stay in Insert mode):
85
145
 
86
146
  | key | action |
@@ -94,48 +154,18 @@ Insert-mode shortcuts (stay in Insert mode):
94
154
 
95
155
  ### navigation (normal mode)
96
156
 
97
- A `{count}` prefix can be prepended to any navigation key (max: `9999`).
98
-
99
- | key | action |
100
- |---------------|-------------------------------|
101
- | `h` | Left |
102
- | `l` | Right |
103
- | `j` | Down |
104
- | `k` | Up |
105
- | `{count}h/l` | Move left/right `{count}` cols |
106
- | `{count}j/k` | Move down/up `{count}` lines (clamped to buffer size) |
107
- | `0` | Line start |
108
- | `^` | First non-whitespace char of line |
109
- | `_` | First non-whitespace char; with `{count}`, move down `count - 1` lines first |
110
- | `$` | Line end |
111
- | `gg` | Buffer start (line 1) |
112
- | `{count}gg` | Go to line `{count}` (1-indexed, clamped) |
113
- | `G` | Buffer end (last line) |
114
- | `{count}G` | Go to line `{count}` (1-indexed, clamped) |
115
- | `w` | Next `word` start (keyword/punctuation aware) |
116
- | `b` | Previous `word` start |
117
- | `e` | `word` end (inclusive) |
118
- | `W` | Next `WORD` start (whitespace-delimited token) |
119
- | `B` | Previous `WORD` start |
120
- | `E` | `WORD` end (inclusive) |
121
- | `{count}w/b/e`| Move `{count}` `word` motions |
122
- | `{count}W/B/E`| Move `{count}` `WORD` motions |
123
- | `}` | Move to next paragraph start (line start col `0`) |
124
- | `{` | Move to previous paragraph start (line start col `0`) |
125
- | `{count}}` | Repeat `}` `{count}` times |
126
- | `{count}{` | Repeat `{` `{count}` times |
127
-
128
- `word` (`w/b/e`) splits punctuation from keyword chars. `WORD` (`W/B/E`)
129
- treats any non-whitespace run as one token (`foo-bar`, `path/to`, `x.y`).
130
-
131
- Paragraph boundary definition (this extension wave):
132
- - blank line: matches `^\s*$`
133
- - paragraph start: non-blank line at BOF, or non-blank line immediately after a blank line
134
-
135
- Standalone `{` / `}` motions are navigation-only (no text/register mutation).
136
- Counted forms (`{count}{`, `{count}}`) step paragraph-by-paragraph.
137
- If no further paragraph boundary exists, motions clamp at BOF/EOF.
138
- Operator forms with braces (`d{`, `d}`, `c{`, `c}`, `y{`, `y}`) are out of scope for this wave.
157
+ A `{count}` prefix can be prepended to navigation keys (max: `9999`).
158
+
159
+ | key | action |
160
+ |-----|--------|
161
+ | `h` / `l` / `j` / `k`; `{count}h/l/j/k` | Move left/right/down/up; line moves clamp to the buffer |
162
+ | `0` / `^` / `_` / `$` | Line start / first non-whitespace / counted first non-whitespace / line end |
163
+ | `gg` / `G`; `{count}gg` / `{count}G` | Buffer start/end or absolute 1-indexed line |
164
+ | `w` / `b` / `e`; `{count}w/b/e` | `word` start/back/end motions |
165
+ | `W` / `B` / `E`; `{count}W/B/E` | whitespace-delimited `WORD` motions |
166
+ | `{` / `}`; `{count}{` / `{count}}` | Previous/next paragraph start |
167
+
168
+ `word` splits punctuation from keyword chars; `WORD` treats any non-whitespace run as one token (`foo-bar`, `path/to`). Paragraph starts are non-blank lines at BOF or after blank lines (`^\s*$`). `{` / `}` are navigation-only; brace operator forms (`d{`, `c}`, `y{`, …) are out of scope.
139
169
 
140
170
  ---
141
171
 
@@ -159,62 +189,60 @@ Char-find motions compose with operators: `df{char}`, `ct{char}`, `d{count}t{cha
159
189
 
160
190
  ### edit operators (normal mode)
161
191
 
162
- All operators write to the unnamed register and mirror to the system clipboard
163
- (best-effort; clipboard failure never breaks editing).
192
+ Register-writing edits write to the unnamed register. With the default clipboard mirror policy, they also mirror to the system clipboard best-effort (clipboard failure never breaks editing).
193
+
194
+ #### text objects
195
+
196
+ Text objects compose as `d`/`c`/`y` + `i`/`a` + object. `i` means inner; `a` means around.
197
+
198
+ | object | keys | range |
199
+ |--------|------|-------|
200
+ | word | `iw` / `aw` | Keyword word; `aw` includes spaces |
201
+ | WORD | `iW` / `aW` | Line-local whitespace-delimited WORD; `aW` includes adjacent whitespace |
202
+ | quotes | `i"` / `a"`, `i'` / `a'`, <code>i`</code> / <code>a`</code> | Smallest containing quote pair on the line |
203
+ | parentheses | `i(` / `a(`; aliases `i)` / `a)`, `ib` / `ab` | Smallest containing pair |
204
+ | brackets | `i[` / `a[`; aliases `i]` / `a]` | Smallest containing pair |
205
+ | braces | `i{` / `a{`; aliases `i}` / `a}`, `iB` / `aB` | Smallest containing pair |
206
+
207
+ Semantics:
208
+ - WORD objects are line-local and whitespace-delimited.
209
+ - Quote objects are line-local; odd-backslash escapes are ignored; `a` includes delimiters only, not surrounding whitespace.
210
+ - Bracket objects are buffer-aware, nested, lexical, and not parser-aware; brackets inside strings/comments still count.
211
+ - Empty inner delimiter objects no-op for delete/yank; change enters Insert at the inner start without writing the register.
212
+ - Delimited counts cancel (`d2i"`, `2ci(`, `y2a{`). Counted word/WORD text objects work for delete/change only; counted yank text objects cancel.
164
213
 
165
214
  #### delete `d{motion}` / `dd`
166
215
 
167
- A `{count}` or dual-count prefix (`{pfx}d{op}{motion}`) is supported for
168
- word, char-find, and linewise motions. Maximum total count: `9999`.
169
-
170
- | command | deletes |
171
- |-------------------|-----------------------------------------------------------|
172
- | `dw` | Forward to next `word` start (exclusive, can cross lines) |
173
- | `de` | Forward to `word` end (inclusive, can cross lines) |
174
- | `db` | Backward to `word` start (exclusive, can cross lines) |
175
- | `dW` | Forward to next `WORD` start (exclusive, can cross lines) |
176
- | `dE` | Forward to `WORD` end (inclusive, can cross lines) |
177
- | `dB` | Backward to `WORD` start (exclusive, can cross lines) |
178
- | `d{count}w/e/b` | Forward/backward `{count}` `word` motions |
179
- | `d{count}W/E/B` | Forward/backward `{count}` `WORD` motions |
180
- | `d$` | To end of line |
181
- | `d0` | To start of line |
182
- | `d^` | To first non-whitespace char of line |
183
- | `d_` | Current line (linewise, same as `dd`) |
184
- | `d{count}_` | `{count}` lines (linewise, same as `{count}dd`) |
185
- | `dd` | Current line (linewise) |
186
- | `{count}dd` | `{count}` lines (linewise) |
187
- | `d{count}j` | Current line + `{count}` lines below (linewise) |
188
- | `d{count}k` | Current line + `{count}` lines above (linewise) |
189
- | `dG` | Current line to end of buffer (linewise) |
190
- | `df{char}` | To and including `char` |
191
- | `d{count}f{char}` | To and including Nth `char` |
192
- | `dt{char}` | Up to (not including) `char` |
193
- | `dF{char}` | Backward to and including `char` |
194
- | `dT{char}` | Backward to one after `char` |
195
- | `diw` | Inner word |
196
- | `daw` | Around word (includes surrounding spaces) |
197
- | `d{count}aw` | Around `{count}` words |
216
+ A `{count}` or dual-count prefix (`{pfx}d{op}{motion}`) is supported for word,
217
+ WORD, char-find, and linewise motions. Maximum total count: `9999`.
218
+
219
+ | command | deletes |
220
+ |---------|---------|
221
+ | `dw` / `de` / `db`; `dW` / `dE` / `dB` | word/WORD motion ranges; `{count}` repeats |
222
+ | `d$` / `d0` / `d^` | To EOL / BOL / first non-whitespace |
223
+ | `d_` / `dd`; `d{count}_` / `{count}dd` | Current or counted whole lines |
224
+ | `d{count}j` / `d{count}k` / `dG` | Linewise down/up/to EOF |
225
+ | `df{c}` / `dt{c}` / `dF{c}` / `dT{c}`; `d{count}f{c}` | Char-find ranges |
226
+ | `diw` / `daw`; `diW` / `daW` | Inner/around word or WORD |
227
+ | `d{count}iw` / `d{count}iW`; `d{count}aw` / `d{count}aW` | Counted word/WORD text objects |
228
+ | `di"` / `da"` (`'`, <code>`</code>) | Inside/around quotes |
229
+ | `di(` / `da(`, `di[` / `da[`, `di{` / `da{` | Inside/around brackets; aliases `)`, `]`, `}`, `b`, `B` |
198
230
 
199
231
  #### change `c{motion}` / `cc`
200
232
 
201
233
  Same motion and count set as `d`. Deletes text then enters Insert mode.
202
234
 
203
- | command | action |
204
- |-----------------|------------------------------------|
205
- | `cw` | Change `word` + Insert |
206
- | `ce` / `cb` | Change to `word` end / previous `word` start |
207
- | `cW` | Change `WORD` + Insert (`cW` on non-space behaves like `cE`) |
208
- | `cE` / `cB` | Change to `WORD` end / previous `WORD` start |
209
- | `c{count}w/e/b` | Change `{count}` `word` motions + Insert |
210
- | `c{count}W/E/B` | Change `{count}` `WORD` motions + Insert |
211
- | `ciw` | Change inner word |
212
- | `caw` | Change around word |
213
- | `cc` | Delete line content + Insert |
214
- | `c_` | Change line (linewise, same as `cc`) |
215
- | `c{count}_` | Change `{count}` lines (linewise) |
235
+ | command | action |
236
+ |---------|--------|
237
+ | `cw` / `ce` / `cb`; `cW` / `cE` / `cB` | Change word/WORD motion ranges + Insert |
238
+ | `c{count}w/e/b`; `c{count}W/E/B` | Change counted word/WORD motions + Insert |
239
+ | `ciw` / `caw`; `ciW` / `caW` | Change word/WORD text objects + Insert |
240
+ | `c{count}iw` / `c{count}iW`; `c{count}aw` / `c{count}aW` | Change counted word/WORD text objects + Insert |
241
+ | `ci"` / `ca"` (`'`, <code>`</code>) | Change inside/around quotes + Insert |
242
+ | `ci(` / `ca(`, `ci[` / `ca[`, `ci{` / `ca{` | Change inside/around brackets + Insert |
243
+ | `cc` / `c_`; `c{count}_` | Change current or counted whole lines + Insert |
216
244
  | `c$` / `c0` / `c^` | Delete to EOL / BOL / first non-whitespace + Insert |
217
- | … | All `d` motions apply |
245
+ | … | All `d` motions apply |
218
246
 
219
247
  #### single-key edits
220
248
 
@@ -237,33 +265,20 @@ A `{count}` prefix is supported for `x`, `p`, `P`. Maximum: `9999`.
237
265
 
238
266
  Same motion set as `d`. Writes to register, **no text mutation**.
239
267
 
240
- | command | yanks |
241
- |---------|---------------------------------|
242
- | `yy` | Whole line + trailing `\n` |
243
- | `Y` | Whole line + trailing `\n` (same as `yy`) |
244
- | `{count}yy` | `{count}` whole lines + trailing `\n` |
245
- | `{count}Y` | `{count}` whole lines + trailing `\n` (same as `{count}yy`) |
246
- | `y{count}j` | Current line + `{count}` lines below (linewise) |
247
- | `y{count}k` | Current line + `{count}` lines above (linewise) |
248
- | `yG` | Current line to end of buffer (linewise) |
249
- | `yw` | Forward to next `word` start |
250
- | `ye` | To `word` end (inclusive) |
251
- | `yb` | Backward to `word` start |
252
- | `yW` | Forward to next `WORD` start |
253
- | `yE` | To `WORD` end (inclusive) |
254
- | `yB` | Backward to `WORD` start |
255
- | `y$` | To end of line |
256
- | `y0` | To start of line |
257
- | `y^` | To first non-whitespace char of line |
258
- | `y_` | Whole line (linewise, same as `yy`) |
259
- | `y{count}_` | `{count}` whole lines (linewise) |
260
- | `yf{c}` | To and including `char` |
261
- | `yiw` | Inner word |
262
- | `yaw` | Around word (includes spaces) |
263
-
264
- Counted yank caveat: counted `word`/`WORD` yank motions are intentionally not
265
- implemented (`y2w`, `2yw`, `y2W`, `2yW`, etc. cancel the pending operator).
266
- Linewise counted yank (`{count}yy`, `y{count}j/k`) remains supported.
268
+ | command | yanks |
269
+ |---------|-------|
270
+ | `yy` / `Y`; `{count}yy` / `{count}Y` | Whole line(s) + trailing `\n` |
271
+ | `y{count}j` / `y{count}k` / `yG`; `y_` / `y{count}_` | Linewise ranges |
272
+ | `yw` / `ye` / `yb`; `yW` / `yE` / `yB` | word/WORD motion ranges |
273
+ | `y$` / `y0` / `y^`; `yf{c}` | EOL / BOL / first non-whitespace / char-find |
274
+ | `yiw` / `yaw`; `yiW` / `yaW` | Inner/around word or WORD |
275
+ | `yi"` / `ya"` (`'`, <code>`</code>) | Inside/around quotes |
276
+ | `yi(` / `ya(`, `yi[` / `ya[`, `yi{` / `ya{` | Inside/around brackets; aliases `)`, `]`, `}`, `b`, `B` |
277
+
278
+ Counted `word`/`WORD` yank motions and counted yank text objects (`y2w`,
279
+ `2yw`, `y2W`, `2yW`, `y2aw`, `2yaw`, `y2aW`, `y2a{`, …) are intentionally not
280
+ implemented and cancel the pending operator. Linewise counted yank (`{count}yy`,
281
+ `y{count}j/k`) is supported.
267
282
 
268
283
  ---
269
284
 
@@ -276,8 +291,7 @@ Linewise counted yank (`{count}yy`, `y{count}j/k`) remains supported.
276
291
  | `{count}p` | Put `{count}` times after cursor |
277
292
  | `{count}P` | Put `{count}` times before cursor |
278
293
 
279
- Put reads from the **unnamed register** (not OS clipboard).
280
- Line-wise detection: register content ending in `\n` is treated as line-wise.
294
+ Put reads the OS clipboard first unless the last local register write was not mirrored. Paste text ending in `\n` is line-wise.
281
295
 
282
296
  ---
283
297
 
@@ -295,54 +309,53 @@ Line-wise detection: register content ending in `\n` is treated as line-wise.
295
309
 
296
310
  ## register and clipboard policy
297
311
 
298
- - One unnamed register (like Vim's `""` register).
299
- - Every `d`, `c`, `x`, `s`, `S`, `D`, `C`, `y` operator form
300
- (including `dd`/`d_`, `{count}dd`, `d{count}j/k`, `dG`, `yy`/`y_`, `{count}yy`,
301
- `y{count}j/k`, `yG`) writes to the register and mirrors to the OS clipboard
302
- (via `copyToClipboard`, best-effort).
303
- - `p` / `P` read from the unnamed register only (not the OS clipboard).
304
- - This gives stable behaviour across local terminals and SSH / OSC52 setups.
312
+ - `piVim.clipboardMirror = "all"` is the default: every unnamed-register write mirrors to the OS clipboard best-effort.
313
+ - `piVim.clipboardMirror = "yank"` mirrors yanks only; deletes and changes update only pi-vim's internal shadow.
314
+ - `piVim.clipboardMirror = "never"` disables write mirroring while keeping internal register writes synchronous.
315
+ - Rapid mirrored writes coalesce: only the latest pending value is guaranteed to be mirrored.
316
+ - `p` / `P` read the OS clipboard first when no local write was skipped by policy, falling back to the shadow on read failure/timeout.
317
+ - If policy skipped the last local write, `p` / `P` use the shadow so delete/yank put works without touching the OS clipboard.
318
+ - While a mirror is in flight, `p` / `P` use the shadow so immediate yank/delete put stays ordered.
319
+ - Pi owns the terminal clipboard backends; on Wayland external state may lag while the shadow stays authoritative for immediate puts.
305
320
 
306
321
  ---
307
322
 
308
323
  ## known differences from full Vim
309
324
 
310
- | area | this extension | full Vim |
311
- |-----------------------|----------------------------------------|-------------------------------|
312
- | `$` motion | Moves past last char (readline CTRL+E) | Moves to last char |
313
- | `w` / `e` / `b` + `W` / `E` / `B` | Cross-line for `word` + `WORD` motions | Cross-line |
314
- | `0` / `$` operators | Exclusive of anchor col | `0` inclusive of col 0 |
315
- | Undo depth | Delegates to underlying readline undo | Full per-change undo tree |
316
- | Redo | Normal-mode `<C-r>` supported (safe no-op when empty; counted redo is stepwise, clamps to available history, and preserves single-step undo granularity) | `<C-r>` |
317
- | Visual mode | Not implemented | `v`, `V`, `<C-v>` |
318
- | Text objects | Supports `iw`/`aw` only | Full text-object set |
319
- | Count prefix | Supported for operators, word/char motions, navigation, and edits (`x`, `r`, `p`/`P`); capped at `MAX_COUNT=9999` to prevent abuse | Full support |
320
- | Named registers | Not implemented (`"a`, etc.) | Supported |
321
- | Macros | Not implemented (`q`, `@`) | Supported |
322
- | Search | Not implemented (`/`, `?`, `n`, `N`) | Supported |
323
- | Ex commands | Not implemented (`:s`, `:g`, etc.) | Supported |
324
- | Multi-line operators | Supports `d/c/y` with `w/e/b` and `W/E/B`, plus `j/k` counts and `G`; not full Vim motion matrix | Rich cross-line semantics |
325
+ | area | this extension | full Vim |
326
+ |------|----------------|----------|
327
+ | `$` motion | Moves past the last char (readline `Ctrl+E`) | Moves to the last char |
328
+ | `w` / `e` / `b` + `W` / `E` / `B` | Cross-line for both `word` and `WORD` motions | Cross-line |
329
+ | `0` / `$` operators | Exclusive of the anchor col | `0` is inclusive of col 0 |
330
+ | Undo / redo | Delegates undo to readline; normal-mode `<C-r>` redo is supported | Full per-change undo tree |
331
+ | Visual mode | Not implemented | `v`, `V`, `<C-v>` |
332
+ | Text objects | `iw` / `aw`, `iW` / `aW`, quote objects, and paren/bracket/brace objects; delimited counts cancel | Full text-object set |
333
+ | Count prefix | Operators, motions, navigation, `x`, `r`, `p`, `P`; capped at `MAX_COUNT=9999` | Full support |
334
+ | Registers / macros / search | Not implemented | Supported |
335
+ | Ex commands | Quit-only EX mini-mode (`:q`, `:q!`, `:qa`, `:qa!`) | Full ex command-line surface |
336
+ | Multi-line operators | `d/c/y` with `w/e/b`, `W/E/B`, `j/k`, and `G`; not the full Vim motion matrix | Rich cross-line semantics |
325
337
 
326
338
  ---
327
339
 
328
340
  ## out of scope
329
341
 
330
- These are **explicitly deferred** and not planned for this feature:
342
+ Explicitly deferred:
331
343
 
332
344
  - Visual modes (`v`, `V`, block visual)
333
- - Extended text objects beyond word (`ip`, `i"`, `i(`, etc.)
334
- - Named registers (`"a`, `"b`, )
335
- - Macros (`q{char}`, `@{char}`)
336
- - Ex command surface (`:s`, `:g`, `:r`, …)
337
- - Search mode (`/`, `?`, `n`, `N`)
338
- - Repeat (`.`)
339
- - Replace mode (`R`) only single-char `r{char}` is supported
340
- - Extended count prefix beyond currently supported motions (e.g. `:`, global operator counts)
341
- - No insert-mode `<C-r>` feature expansion beyond current underlying-editor behavior.
342
- - No cross-session redo persistence.
343
- - No upstream `pi-tui` redo prerequisite in this wave.
344
- - Window / tab / buffer management
345
- - Plugin / runtime ecosystem compatibility
345
+ - Tag text objects (`it`, `at`)
346
+ - Paragraph/sentence text objects (`ip`, `ap`, `is`, `as`)
347
+ - Angle bracket text objects (`i<`, `a<`)
348
+ - Visual-mode text-object selection
349
+ - Parser-aware delimiter matching
350
+ - Delimited-object counts (`d2i"`, `2ci(`, `y2a{`)
351
+ - Named registers (`"a`, `"b`, …), macros (`q{char}`, `@{char}`)
352
+ - Ex surface beyond quit (`:s`, `:g`, `:w`, `:r`, )
353
+ - Search (`/`, `?`, `n`, `N`), repeat (`.`)
354
+ - Replace mode (`R`) — only `r{char}` is supported
355
+ - Count prefix beyond currently supported motions
356
+ - No insert-mode `<C-r>` expansion, no cross-session redo persistence
357
+ - No upstream `pi-tui` redo prerequisite
358
+ - Window / tab / buffer management, plugin ecosystem compatibility
346
359
 
347
360
  ---
348
361
 
@@ -0,0 +1,73 @@
1
+ import { SettingsManager } from "@mariozechner/pi-coding-agent";
2
+
3
+ export type ClipboardMirrorPolicy = "all" | "yank" | "never";
4
+ export type RegisterWriteSource = "mutation" | "yank";
5
+
6
+ export const DEFAULT_CLIPBOARD_MIRROR_POLICY: ClipboardMirrorPolicy = "all";
7
+
8
+ export type PiVimSettings = { clipboardMirror?: unknown };
9
+
10
+ type UnknownRecord = Record<string, unknown>;
11
+
12
+ const missing = Symbol();
13
+
14
+ function formatInvalid(value: unknown) {
15
+ const type = value === null ? "null" : Array.isArray(value) ? "array" : typeof value;
16
+ try {
17
+ return `${JSON.stringify(value) ?? type} (type ${type})`;
18
+ } catch {
19
+ return `(type ${type})`;
20
+ }
21
+ }
22
+
23
+ function readSetting(settings: unknown): unknown {
24
+ if (typeof settings !== "object" || settings === null || !Object.hasOwn(settings, "piVim")) return missing;
25
+ const piVim = (settings as UnknownRecord).piVim;
26
+ if (typeof piVim !== "object" || piVim === null || Array.isArray(piVim)) return piVim;
27
+ return Object.hasOwn(piVim, "clipboardMirror") ? (piVim as UnknownRecord).clipboardMirror : missing;
28
+ }
29
+
30
+ export function resolveClipboardMirrorPolicy(value: unknown) {
31
+ if (value === undefined) return { policy: DEFAULT_CLIPBOARD_MIRROR_POLICY };
32
+
33
+ if (typeof value === "string") {
34
+ const policy = value.trim().toLowerCase();
35
+ if (policy === "all" || policy === "yank" || policy === "never") {
36
+ return { policy: policy as ClipboardMirrorPolicy };
37
+ }
38
+ }
39
+
40
+ return {
41
+ policy: DEFAULT_CLIPBOARD_MIRROR_POLICY,
42
+ warning: `Invalid piVim.clipboardMirror ${formatInvalid(value)}; expected all, yank, never. Using all.`,
43
+ };
44
+ }
45
+
46
+ export function readPiVimClipboardMirrorSetting(globalSettings: unknown, projectSettings: unknown): unknown | undefined {
47
+ const project = readSetting(projectSettings);
48
+ if (project !== missing) return project;
49
+ const global = readSetting(globalSettings);
50
+ return global === missing ? undefined : global;
51
+ }
52
+
53
+ function readPiVimSettingsFromDisk(cwd: string): PiVimSettings {
54
+ const settings = SettingsManager.create(cwd);
55
+ return {
56
+ clipboardMirror: readPiVimClipboardMirrorSetting(settings.getGlobalSettings(), settings.getProjectSettings()),
57
+ };
58
+ }
59
+
60
+ let piVimSettingsReader = readPiVimSettingsFromDisk;
61
+
62
+ export function readPiVimSettings(cwd: string) {
63
+ return piVimSettingsReader(cwd);
64
+ }
65
+
66
+ export function setPiVimSettingsReaderForTests(reader: typeof readPiVimSettingsFromDisk) {
67
+ const prev = piVimSettingsReader;
68
+ piVimSettingsReader = reader;
69
+
70
+ return () => {
71
+ piVimSettingsReader = prev;
72
+ };
73
+ }