pi-vim 0.2.1 → 0.3.1

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.
Files changed (3) hide show
  1. package/README.md +7 -2
  2. package/index.ts +51 -15
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -34,6 +34,8 @@ u # undo
34
34
  ```
35
35
 
36
36
  Mode indicator (`INSERT` / `NORMAL`) appears at bottom-right.
37
+ Its label is theme-colored: reverse-video `borderMuted` for
38
+ INSERT, `borderAccent` for NORMAL.
37
39
 
38
40
  ## why pi-vim
39
41
 
@@ -74,7 +76,7 @@ ex-commands, etc.).
74
76
  | `Esc` / `Ctrl+[` | Normal mode → pass to Pi (abort agent) |
75
77
  | `i` | Normal → Insert at cursor |
76
78
  | `a` | Normal → Insert after cursor |
77
- | `I` | Normal → Insert at line start |
79
+ | `I` | Normal → Insert at first non-whitespace |
78
80
  | `A` | Normal → Insert at line end |
79
81
  | `o` | Normal → open line below + Insert |
80
82
  | `O` | Normal → open line above + Insert |
@@ -238,7 +240,9 @@ Same motion set as `d`. Writes to register, **no text mutation**.
238
240
  | command | yanks |
239
241
  |---------|---------------------------------|
240
242
  | `yy` | Whole line + trailing `\n` |
243
+ | `Y` | Whole line + trailing `\n` (same as `yy`) |
241
244
  | `{count}yy` | `{count}` whole lines + trailing `\n` |
245
+ | `{count}Y` | `{count}` whole lines + trailing `\n` (same as `{count}yy`) |
242
246
  | `y{count}j` | Current line + `{count}` lines below (linewise) |
243
247
  | `y{count}k` | Current line + `{count}` lines above (linewise) |
244
248
  | `yG` | Current line to end of buffer (linewise) |
@@ -281,7 +285,8 @@ Line-wise detection: register content ending in `\n` is treated as line-wise.
281
285
 
282
286
  | key | action |
283
287
  |-----|--------|
284
- | `u` | Undo in normal mode |
288
+ | `u` | Undo one change in normal mode |
289
+ | `{count}u` | Undo up to `{count}` changes in normal mode; clamps at available history |
285
290
  | `Ctrl+_` | Undo in normal mode (alias for `u`) |
286
291
  | `<C-r>` | Redo one undone change in normal mode; safe no-op when redo history is empty |
287
292
  | `{count}<C-r>` | Redo up to `{count}` undone changes in order; clamps at available history and consumes count state (no leak to the next command) |
package/index.ts CHANGED
@@ -136,6 +136,7 @@ export class ModalEditor extends CustomEditor {
136
136
  private readonly redoStack: EditorSnapshot[] = [];
137
137
  private currentTransition: TransitionState = "none";
138
138
  private onChangeHooked: boolean = false;
139
+ private readonly labelColorizers: { insert: (s: string) => string; normal: (s: string) => string } | null;
139
140
 
140
141
  // Unnamed register
141
142
  private unnamedRegister: string = "";
@@ -143,6 +144,16 @@ export class ModalEditor extends CustomEditor {
143
144
  try { copyToClipboard(text); } catch { /* best effort */ }
144
145
  };
145
146
 
147
+ constructor(
148
+ tui: any,
149
+ theme: any,
150
+ kb: any,
151
+ labelColorizers?: { insert: (s: string) => string; normal: (s: string) => string } | null,
152
+ ) {
153
+ super(tui, theme, kb);
154
+ this.labelColorizers = labelColorizers ?? null;
155
+ }
156
+
146
157
  // Test seams
147
158
  setClipboardFn(fn: (text: string) => void): void { this.clipboardFn = fn; }
148
159
  getRegister(): string { return this.unnamedRegister; }
@@ -220,16 +231,22 @@ export class ModalEditor extends CustomEditor {
220
231
  }
221
232
  }
222
233
 
223
- private performUndo(): void {
224
- this.withTransition("undo", () => {
225
- const beforeUndo = this.captureSnapshot();
226
- super.handleInput(CTRL_UNDERSCORE);
227
- const afterUndo = this.captureSnapshot();
228
-
229
- if (this.snapshotChanged(beforeUndo, afterUndo)) {
230
- this.redoStack.push(beforeUndo);
231
- }
232
- });
234
+ private performUndo(count: number = this.takeTotalCount(1)): void {
235
+ const maxSteps = Math.max(1, Math.min(MAX_COUNT, count));
236
+ for (let i = 0; i < maxSteps; i++) {
237
+ let changed = false;
238
+ this.withTransition("undo", () => {
239
+ const beforeUndo = this.captureSnapshot();
240
+ super.handleInput(CTRL_UNDERSCORE);
241
+ const afterUndo = this.captureSnapshot();
242
+
243
+ if (this.snapshotChanged(beforeUndo, afterUndo)) {
244
+ this.redoStack.push(beforeUndo);
245
+ changed = true;
246
+ }
247
+ });
248
+ if (!changed) break;
249
+ }
233
250
  }
234
251
 
235
252
  private performRedo(count: number = this.takeTotalCount(1)): void {
@@ -889,7 +906,11 @@ export class ModalEditor extends CustomEditor {
889
906
  || data === "C"
890
907
  || data === "p"
891
908
  || data === "P"
909
+ || data === "Y"
892
910
  || data === "J"
911
+ || data === "u"
912
+ || data === CTRL_UNDERSCORE
913
+ || matchesKey(data, "ctrl+_")
893
914
  || data === CTRL_R
894
915
  || matchesKey(data, "ctrl+r")
895
916
  );
@@ -996,6 +1017,12 @@ export class ModalEditor extends CustomEditor {
996
1017
  return;
997
1018
  }
998
1019
 
1020
+ if (data === "Y") {
1021
+ const count = this.takeTotalCount(1);
1022
+ this.yankLinewiseByDelta(count - 1);
1023
+ return;
1024
+ }
1025
+
999
1026
  if (CHAR_MOTION_KEYS.has(data)) {
1000
1027
  this.pendingMotion = data as PendingMotion;
1001
1028
  return;
@@ -1091,7 +1118,7 @@ export class ModalEditor extends CustomEditor {
1091
1118
  break;
1092
1119
  case "I":
1093
1120
  this.mode = "insert";
1094
- super.handleInput(CTRL_A);
1121
+ this.moveCursorToFirstNonWhitespace();
1095
1122
  break;
1096
1123
  case "o":
1097
1124
  this.openLineBelow();
@@ -2187,10 +2214,14 @@ export class ModalEditor extends CustomEditor {
2187
2214
  const lines = super.render(width);
2188
2215
  if (lines.length === 0) return lines;
2189
2216
 
2190
- const label = this.getModeLabel();
2217
+ const rawLabel = this.getModeLabel();
2218
+ const colorize = this.labelColorizers
2219
+ ? (this.mode === "insert" ? this.labelColorizers.insert : this.labelColorizers.normal)
2220
+ : null;
2221
+ const label = colorize ? colorize(rawLabel) : rawLabel;
2191
2222
  const last = lines.length - 1;
2192
- if (visibleWidth(lines[last]!) >= label.length) {
2193
- lines[last] = truncateToWidth(lines[last]!, width - label.length, "") + label;
2223
+ if (visibleWidth(lines[last]!) >= visibleWidth(rawLabel)) {
2224
+ lines[last] = truncateToWidth(lines[last]!, width - visibleWidth(rawLabel), "") + label;
2194
2225
  }
2195
2226
  return lines;
2196
2227
  }
@@ -2225,6 +2256,11 @@ export class ModalEditor extends CustomEditor {
2225
2256
 
2226
2257
  export default function (pi: ExtensionAPI) {
2227
2258
  pi.on("session_start", (_event, ctx) => {
2228
- ctx.ui.setEditorComponent((tui, theme, kb) => new ModalEditor(tui, theme, kb));
2259
+ const t = ctx.ui.theme;
2260
+ const colorizers = t ? {
2261
+ insert: (s: string) => t.fg("borderMuted", `\x1b[7m${s}\x1b[27m`),
2262
+ normal: (s: string) => t.fg("borderAccent", `\x1b[7m${s}\x1b[27m`),
2263
+ } : null;
2264
+ ctx.ui.setEditorComponent((tui, theme, kb) => new ModalEditor(tui, theme, kb, colorizers));
2229
2265
  });
2230
2266
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-vim",
3
- "version": "0.2.1",
3
+ "version": "0.3.1",
4
4
  "description": "Vim-style modal editing for Pi's TUI editor",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -12,7 +12,7 @@
12
12
  "license": "MIT",
13
13
  "repository": {
14
14
  "type": "git",
15
- "url": "git+https://github.com/lajarre/pi-vim.git"
15
+ "url": "https://github.com/lajarre/pi-vim.git"
16
16
  },
17
17
  "files": [
18
18
  "*.ts",