pi-vim 0.1.9 → 0.2.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 (5) hide show
  1. package/README.md +62 -60
  2. package/index.ts +545 -129
  3. package/motions.ts +55 -13
  4. package/package.json +1 -1
  5. package/types.ts +1 -1
package/motions.ts CHANGED
@@ -125,6 +125,27 @@ export function reverseCharMotion(motion: CharMotion): CharMotion {
125
125
  return reverseMap[motion];
126
126
  }
127
127
 
128
+ const GRAPHEME_SEGMENTER = typeof Intl !== "undefined"
129
+ && typeof Intl.Segmenter === "function"
130
+ ? new Intl.Segmenter(undefined, { granularity: "grapheme" })
131
+ : null;
132
+
133
+ export function getLineGraphemes(line: string): Array<{ start: number; end: number }> {
134
+ const segments: Array<{ start: number; end: number }> = [];
135
+ if (GRAPHEME_SEGMENTER) {
136
+ for (const part of GRAPHEME_SEGMENTER.segment(line)) {
137
+ segments.push({ start: part.index, end: part.index + part.segment.length });
138
+ }
139
+ return segments;
140
+ }
141
+ let start = 0;
142
+ for (const text of Array.from(line)) {
143
+ segments.push({ start, end: start + text.length });
144
+ start += text.length;
145
+ }
146
+ return segments;
147
+ }
148
+
128
149
  /**
129
150
  * Find target column for a character motion (f/F/t/T).
130
151
  * @returns target column or null if not found
@@ -141,7 +162,9 @@ export function findCharMotionTarget(
141
162
  const isTill = motion === "t" || motion === "T";
142
163
  const steps = Number.isFinite(count) && count > 0 ? Math.floor(count) : 1;
143
164
 
144
- let currentPos = col;
165
+ const graphemes = getLineGraphemes(line);
166
+ let currentIndex = graphemes.findIndex(g => col < g.end);
167
+ if (currentIndex === -1) currentIndex = graphemes.length;
145
168
 
146
169
  for (let i = 0; i < steps; i++) {
147
170
  const isFirst = i === 0;
@@ -149,19 +172,38 @@ export function findCharMotionTarget(
149
172
  const tillRepeatOffset = isFirst && isTill && isRepeat ? 1 : 0;
150
173
 
151
174
  if (isForward) {
152
- const searchStart = currentPos + 1 + tillRepeatOffset;
153
- const idx = line.indexOf(targetChar, searchStart);
154
- if (idx === -1) return null;
155
- if (isFinal) return isTill ? idx - 1 : idx;
156
- currentPos = idx;
157
- continue;
175
+ let nextIndex = currentIndex + 1 + tillRepeatOffset;
176
+ let found = -1;
177
+ for (let j = nextIndex; j < graphemes.length; j++) {
178
+ const g = graphemes[j]!;
179
+ // Use startsWith to allow matching base chars if targetChar lacks combining marks,
180
+ // or just exact match since targetChar is typically a full grapheme.
181
+ if (line.slice(g.start, g.end) === targetChar || line.slice(g.start, g.end).startsWith(targetChar)) {
182
+ found = j;
183
+ break;
184
+ }
185
+ }
186
+ if (found === -1) return null;
187
+ if (isFinal) return isTill ? graphemes[found - 1]!.start : graphemes[found]!.start;
188
+ currentIndex = found;
189
+ } else {
190
+ let nextIndex = currentIndex - 1 - tillRepeatOffset;
191
+ let found = -1;
192
+ for (let j = nextIndex; j >= 0; j--) {
193
+ const g = graphemes[j]!;
194
+ if (line.slice(g.start, g.end) === targetChar || line.slice(g.start, g.end).startsWith(targetChar)) {
195
+ found = j;
196
+ break;
197
+ }
198
+ }
199
+ if (found === -1) return null;
200
+ if (isFinal) {
201
+ if (!isTill) return graphemes[found]!.start;
202
+ const afterTarget = graphemes[found + 1];
203
+ return afterTarget ? afterTarget.start : line.length;
204
+ }
205
+ currentIndex = found;
158
206
  }
159
-
160
- const searchStart = currentPos - 1 - tillRepeatOffset;
161
- const idx = line.lastIndexOf(targetChar, searchStart);
162
- if (idx === -1) return null;
163
- if (isFinal) return isTill ? idx + 1 : idx;
164
- currentPos = idx;
165
207
  }
166
208
 
167
209
  return null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-vim",
3
- "version": "0.1.9",
3
+ "version": "0.2.1",
4
4
  "description": "Vim-style modal editing for Pi's TUI editor",
5
5
  "type": "module",
6
6
  "keywords": [
package/types.ts CHANGED
@@ -39,10 +39,10 @@ export const CHAR_MOTION_KEYS = new Set<string>(["f", "F", "t", "T"]);
39
39
  // Escape sequences
40
40
  export const ESC_LEFT = "\x1b[D";
41
41
  export const ESC_RIGHT = "\x1b[C";
42
- export const ESC_DELETE = "\x1b[3~";
43
42
  export const CTRL_A = "\x01"; // line start
44
43
  export const CTRL_E = "\x05"; // line end
45
44
  export const CTRL_K = "\x0b"; // kill to end of line
45
+ export const CTRL_R = "\x12"; // ctrl+r — readline redo trigger in vim layer
46
46
  export const CTRL_UNDERSCORE = "\x1f"; // ctrl+_ — readline undo
47
47
  export const NEWLINE = "\n"; // newline character
48
48
  export const ESC_UP = "\x1b[A"; // cursor up