@thazhemadam/vim-state 0.1.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.
@@ -0,0 +1,621 @@
1
+ import { ARROW_DOWN, ARROW_LEFT, ARROW_RIGHT, ARROW_UP, DELETE_BACKWARD, DELETE_FORWARD, LINE_END, LINE_START, NEWLINE, } from "./constants.js";
2
+ import { countedWordPosition, deleteDistance, normalizedCharRange, registerForRange, samePosition, } from "./operators.js";
3
+ import { firstNonBlankColumn, normalMaxColumn, toggleCase } from "./utils.js";
4
+ export { nounForKey } from "./utils.js";
5
+ /** Reusable Vim operations composed around a host editor. */
6
+ class VimEditorCore {
7
+ host;
8
+ options = {};
9
+ constructor(host) {
10
+ this.host = host;
11
+ }
12
+ setOptions(options) {
13
+ this.options = options;
14
+ }
15
+ getCursor() {
16
+ return this.cursor;
17
+ }
18
+ /** Apply a Vim motion, or move directly to an absolute cursor position. */
19
+ move(target) {
20
+ if (isVimPosition(target)) {
21
+ this.moveCursorToPosition(target);
22
+ return;
23
+ }
24
+ const result = this.resolveMotion(target);
25
+ if (!result) {
26
+ return;
27
+ }
28
+ this.moveCursorToPosition(result.destination);
29
+ }
30
+ /** Insert an empty line below the current line and leave the caret on it. */
31
+ insertLineBelow() {
32
+ this.host.sendInputToEditor(LINE_END);
33
+ this.host.sendInputToEditor(NEWLINE);
34
+ }
35
+ /** Insert an empty line above the current line and leave the caret on it. */
36
+ insertLineAbove() {
37
+ this.host.sendInputToEditor(LINE_START);
38
+ this.host.sendInputToEditor(NEWLINE);
39
+ this.host.sendInputToEditor(ARROW_UP);
40
+ }
41
+ /** Join the current line with following lines, inserting one separator space where needed. */
42
+ joinLines(count = 2) {
43
+ for (let i = 1; i < Math.max(count, 2); ++i) {
44
+ this.joinNextLine();
45
+ }
46
+ }
47
+ /** Join the lines covered by a target range, like Visual `J`. */
48
+ join(target) {
49
+ const range = this.resolveOperatorRange(target);
50
+ if (!range) {
51
+ return;
52
+ }
53
+ const startLine = range.type === "linewise" ? range.startLine : range.start.line;
54
+ const endLine = range.type === "linewise" ? range.endLine : range.end.line;
55
+ this.moveCursorToPosition({ line: startLine, col: 0 });
56
+ this.joinLines(endLine - startLine + 1);
57
+ }
58
+ /** Move to a target line, using 1-based line numbers for counted Vim commands. */
59
+ goToLine(line) {
60
+ const targetLine = line === "first" ? 0 : line === "last" ? this.lines.length - 1 : line - 1;
61
+ const clampedLine = Math.min(Math.max(targetLine, 0), Math.max(this.lines.length - 1, 0));
62
+ this.moveCursorToPosition({
63
+ line: clampedLine,
64
+ col: firstNonBlankColumn(this.lines[clampedLine] ?? ""),
65
+ });
66
+ }
67
+ /** Move to or before a target character on the current line. */
68
+ moveToChar(operation, direction, char, count = 1) {
69
+ const result = this.resolveFindMotion(operation, direction, char, count);
70
+ if (!result) {
71
+ return;
72
+ }
73
+ this.moveCursorToPosition(result.destination);
74
+ }
75
+ /** Place the Insert caret at the start of the current line. */
76
+ placeCaretAtLineStart() {
77
+ this.host.sendInputToEditor(LINE_START);
78
+ }
79
+ /** Place the Insert caret after the current Normal-mode character. */
80
+ placeCaretAfterCursor() {
81
+ if (this.cursor.col < this.currentLine.length) {
82
+ this.host.sendInputToEditor(ARROW_RIGHT);
83
+ }
84
+ }
85
+ /** Place the Insert caret at the end of the current line. */
86
+ placeCaretAtLineEnd() {
87
+ this.host.sendInputToEditor(LINE_END);
88
+ }
89
+ /**
90
+ * Apply a supported operator noun as a delete and return the deleted text.
91
+ *
92
+ * Operator nouns include real motions (`dw`) plus linewise nouns from doubled
93
+ * operators (`dd`), so range resolution is separate from cursor movement.
94
+ */
95
+ delete(target, count = 1) {
96
+ return this.applyOperator(target, count, (range) => this.applyDeleteRange(range));
97
+ }
98
+ /** Apply a supported operator noun as a change and return the changed text. */
99
+ change(target, count = 1) {
100
+ return this.applyOperator(target, count, (range) => this.applyChangeRange(range), { emitRegisterWrite: true, clampCursor: false });
101
+ }
102
+ /** Store a supported operator noun in the unnamed register. */
103
+ yank(target, count = 1) {
104
+ return this.applyOperator(target, count, (range) => this.registerForRange(range));
105
+ }
106
+ /** Replace a resolved target with register text and return the replaced text. */
107
+ replace(target, replacement, emitRegisterWrite = true) {
108
+ return this.applyOperator(target, 1, (range) => {
109
+ const replaced = this.applyDeleteRange(range);
110
+ this.insertText(replacement.text);
111
+ if (replacement.type === "charwise") {
112
+ this.move("left");
113
+ }
114
+ else {
115
+ this.clampCursorColumn();
116
+ }
117
+ return replaced;
118
+ }, { emitRegisterWrite, clampCursor: true });
119
+ }
120
+ /** Normal `~`: toggle characters under the cursor, then advance like Vim. */
121
+ toggleCase(count = 1) {
122
+ for (let i = 0; i < Math.max(count, 1); ++i) {
123
+ const char = this.currentLine[this.cursor.col];
124
+ if (!char) {
125
+ return;
126
+ }
127
+ this.deleteForward(1);
128
+ this.host.sendInputToEditor(toggleCase(char));
129
+ }
130
+ this.clampCursorColumn();
131
+ }
132
+ /** Visual `~`/`u`/`U`: transform a range, then return to the selection start. */
133
+ transformCase(target, transform) {
134
+ const range = this.resolveOperatorRange(target);
135
+ if (!range) {
136
+ return;
137
+ }
138
+ const charRange = range.type === "charwise"
139
+ ? range
140
+ : {
141
+ type: "charwise",
142
+ start: { line: range.startLine, col: 0 },
143
+ end: {
144
+ line: range.endLine,
145
+ col: this.lines[range.endLine]?.length ?? 0,
146
+ },
147
+ };
148
+ const text = this.registerForRange(charRange).text;
149
+ this.applyDeleteRange(charRange);
150
+ this.insertText(transformCaseText(text, transform));
151
+ this.moveCursorToPosition(charRange.start);
152
+ }
153
+ /** Put unnamed-register text before/after the cursor, or above/below the current line. */
154
+ put(register, placement) {
155
+ if (register.type === "linewise") {
156
+ if (placement === "before") {
157
+ this.placeCaretAtLineStart();
158
+ this.insertText(register.text);
159
+ }
160
+ else {
161
+ this.placeCaretAtLineEnd();
162
+ this.insertText(NEWLINE + register.text.replace(/\n$/, ""));
163
+ }
164
+ this.clampCursorColumn();
165
+ return;
166
+ }
167
+ if (placement === "after") {
168
+ this.placeCaretAfterCursor();
169
+ }
170
+ this.insertText(register.text);
171
+ this.move("left");
172
+ }
173
+ /** Replace the Normal-mode character under the cursor and keep the cursor on the replacement. */
174
+ replaceCharUnderCursor(char) {
175
+ if (this.cursor.col >= this.currentLine.length) {
176
+ return;
177
+ }
178
+ this.replace("right", { text: char, type: "charwise" }, false);
179
+ }
180
+ /** Restore the latest host-provided undo point. */
181
+ undo() {
182
+ this.host.undoEditor?.();
183
+ this.clampCursorColumn();
184
+ }
185
+ /** Restore the latest host-provided redo point. */
186
+ redo() {
187
+ this.host.redoEditor?.();
188
+ this.clampCursorColumn();
189
+ }
190
+ /** Move left until the Normal-mode cursor sits on a character, or column 0 for an empty line. */
191
+ clampCursorColumn() {
192
+ while (this.cursor.col > normalMaxColumn(this.currentLine)) {
193
+ this.host.sendInputToEditor(ARROW_LEFT);
194
+ }
195
+ }
196
+ get cursor() {
197
+ return this.host.getCursor();
198
+ }
199
+ get lines() {
200
+ return this.host.getLines();
201
+ }
202
+ get currentLine() {
203
+ return this.lines[this.cursor.line] ?? "";
204
+ }
205
+ /**
206
+ * Resolve a real cursor motion into both meanings Vim assigns to motions:
207
+ * where plain movement lands, and what range an operator using that motion covers.
208
+ */
209
+ resolveMotion(noun, count = 1) {
210
+ const start = this.cursor;
211
+ const steps = Math.max(count, 1);
212
+ switch (noun) {
213
+ case "left": {
214
+ if (start.col === 0) {
215
+ return undefined;
216
+ }
217
+ const destination = {
218
+ line: start.line,
219
+ col: Math.max(start.col - steps, 0),
220
+ };
221
+ return {
222
+ range: { type: "charwise", start: destination, end: start },
223
+ destination,
224
+ };
225
+ }
226
+ case "right": {
227
+ const line = this.lines[start.line] ?? "";
228
+ if (line.length === 0) {
229
+ return undefined;
230
+ }
231
+ const end = {
232
+ line: start.line,
233
+ col: Math.min(start.col + steps, line.length),
234
+ };
235
+ return {
236
+ range: { type: "charwise", start, end },
237
+ destination: {
238
+ line: start.line,
239
+ col: Math.min(end.col, normalMaxColumn(line)),
240
+ },
241
+ };
242
+ }
243
+ case "down": {
244
+ const line = Math.min(start.line + steps, this.lines.length - 1);
245
+ if (line === start.line) {
246
+ return undefined;
247
+ }
248
+ return {
249
+ range: { type: "linewise", startLine: start.line, endLine: line },
250
+ destination: this.clampedPosition({ line, col: start.col }),
251
+ };
252
+ }
253
+ case "up": {
254
+ const line = Math.max(start.line - steps, 0);
255
+ if (line === start.line) {
256
+ return undefined;
257
+ }
258
+ return {
259
+ range: { type: "linewise", startLine: line, endLine: start.line },
260
+ destination: this.clampedPosition({ line, col: start.col }),
261
+ };
262
+ }
263
+ case "lineStart": {
264
+ const destination = { line: start.line, col: 0 };
265
+ return {
266
+ range: normalizedCharRange(start, destination),
267
+ destination,
268
+ };
269
+ }
270
+ case "lineEnd": {
271
+ const line = Math.min(start.line + steps - 1, this.lines.length - 1);
272
+ const text = this.lines[line] ?? "";
273
+ return {
274
+ range: {
275
+ type: "charwise",
276
+ start,
277
+ end: { line, col: text.length },
278
+ },
279
+ destination: { line, col: normalMaxColumn(text) },
280
+ };
281
+ }
282
+ case "firstNonBlank": {
283
+ const line = this.lines[start.line] ?? "";
284
+ const destination = {
285
+ line: start.line,
286
+ col: firstNonBlankColumn(line),
287
+ };
288
+ return {
289
+ range: normalizedCharRange(start, destination),
290
+ destination,
291
+ };
292
+ }
293
+ case "nextWord":
294
+ case "nextBigWord": {
295
+ const destination = countedWordPosition(this.lines, start, noun, steps);
296
+ return {
297
+ range: { type: "charwise", start, end: destination },
298
+ destination,
299
+ };
300
+ }
301
+ case "previousWord":
302
+ case "previousBigWord": {
303
+ const destination = countedWordPosition(this.lines, start, noun, steps);
304
+ return {
305
+ range: normalizedCharRange(start, destination),
306
+ destination,
307
+ };
308
+ }
309
+ case "endOfWord":
310
+ case "endOfBigWord": {
311
+ const destination = countedWordPosition(this.lines, start, noun, steps);
312
+ // At the end of the final word, `e`/`E` has no motion. Operators like
313
+ // `de` should therefore leave the buffer untouched.
314
+ if (samePosition(start, destination)) {
315
+ return undefined;
316
+ }
317
+ return {
318
+ range: {
319
+ type: "charwise",
320
+ start,
321
+ end: { line: destination.line, col: destination.col + 1 },
322
+ },
323
+ destination,
324
+ };
325
+ }
326
+ }
327
+ }
328
+ /** Resolve an f/F/t/T motion on the current line. */
329
+ resolveFindMotion(operation, direction, char, count = 1) {
330
+ const start = this.cursor;
331
+ const destination = findCharPosition(this.currentLine, start.col, operation, direction, char, count);
332
+ if (!destination) {
333
+ return undefined;
334
+ }
335
+ const end = direction === "forward"
336
+ ? Math.min(destination + 1, this.currentLine.length)
337
+ : destination;
338
+ return {
339
+ range: normalizedCharRange(start, { line: start.line, col: end }),
340
+ destination: { line: start.line, col: destination },
341
+ };
342
+ }
343
+ /**
344
+ * Resolve an operator noun into the buffer range it covers.
345
+ *
346
+ * Motions reuse their motion range. `line` is not a cursor motion; it names the
347
+ * current-line range for doubled operators such as `dd` and `cc`.
348
+ */
349
+ resolveOperatorRange(target, count = 1) {
350
+ if (isVisualSelection(target)) {
351
+ return this.resolveVisualRange(target);
352
+ }
353
+ const noun = target;
354
+ if (noun === "line") {
355
+ const start = this.cursor;
356
+ return {
357
+ type: "linewise",
358
+ startLine: start.line,
359
+ endLine: Math.min(start.line + Math.max(count, 1) - 1, this.lines.length - 1),
360
+ };
361
+ }
362
+ if (typeof noun === "object") {
363
+ return noun.type === "textObject"
364
+ ? this.resolveTextObjectRange(noun)
365
+ : this.resolveFindMotion(noun.operation, noun.direction, noun.char, count)?.range;
366
+ }
367
+ return this.resolveMotion(noun, count)?.range;
368
+ }
369
+ resolveTextObjectRange(object) {
370
+ if (object.object !== "word") {
371
+ return undefined;
372
+ }
373
+ const line = this.currentLine;
374
+ let start = this.cursor.col;
375
+ while (start < line.length && /\s/.test(line[start])) {
376
+ start += 1;
377
+ }
378
+ if (start >= line.length) {
379
+ return undefined;
380
+ }
381
+ const word = /[A-Za-z0-9_]/.test(line[start])
382
+ ? /[A-Za-z0-9_]/
383
+ : /[^\sA-Za-z0-9_]/;
384
+ while (start > 0 && word.test(line[start - 1])) {
385
+ start -= 1;
386
+ }
387
+ let end = start;
388
+ while (end < line.length && word.test(line[end])) {
389
+ end += 1;
390
+ }
391
+ if (object.kind === "around") {
392
+ const after = end;
393
+ while (end < line.length && /\s/.test(line[end])) {
394
+ end += 1;
395
+ }
396
+ if (end === after) {
397
+ while (start > 0 && /\s/.test(line[start - 1])) {
398
+ start -= 1;
399
+ }
400
+ }
401
+ }
402
+ return {
403
+ type: "charwise",
404
+ start: { line: this.cursor.line, col: start },
405
+ end: { line: this.cursor.line, col: end },
406
+ };
407
+ }
408
+ resolveVisualRange(selection) {
409
+ const active = this.cursor;
410
+ if (selection.mode === "linewise") {
411
+ return {
412
+ type: "linewise",
413
+ startLine: Math.min(selection.anchor.line, active.line),
414
+ endLine: Math.max(selection.anchor.line, active.line),
415
+ };
416
+ }
417
+ const anchorEnd = {
418
+ line: selection.anchor.line,
419
+ col: Math.min(selection.anchor.col + 1, this.lines[selection.anchor.line]?.length ?? 0),
420
+ };
421
+ const activeEnd = {
422
+ line: active.line,
423
+ col: Math.min(active.col + 1, this.lines[active.line]?.length ?? 0),
424
+ };
425
+ if (selection.anchor.line < active.line ||
426
+ (selection.anchor.line === active.line &&
427
+ selection.anchor.col <= active.col)) {
428
+ return { type: "charwise", start: selection.anchor, end: activeEnd };
429
+ }
430
+ return { type: "charwise", start: active, end: anchorEnd };
431
+ }
432
+ /** Resolve a counted operator noun once, then apply the resulting range once. */
433
+ applyOperator(target, count, applyRange, options = {
434
+ emitRegisterWrite: true,
435
+ clampCursor: true,
436
+ }) {
437
+ const range = this.resolveOperatorRange(target, count);
438
+ if (!range) {
439
+ return undefined;
440
+ }
441
+ const register = applyRange(range);
442
+ if (options.clampCursor && target !== "left") {
443
+ this.clampCursorColumn();
444
+ }
445
+ if (options.emitRegisterWrite) {
446
+ this.emitUnnamedRegisterWrite(register);
447
+ }
448
+ return register;
449
+ }
450
+ /** Apply a resolved operator range as a delete and return the removed register text. */
451
+ applyDeleteRange(range) {
452
+ const register = this.registerForRange(range);
453
+ switch (range.type) {
454
+ case "charwise":
455
+ this.moveCursorToPosition(range.start);
456
+ this.deleteForward(deleteDistance(this.lines, range.start, range.end));
457
+ return register;
458
+ case "linewise":
459
+ return this.applyLineDelete(range, register);
460
+ }
461
+ }
462
+ /** Apply a resolved operator range as a change and return the removed register text. */
463
+ applyChangeRange(range) {
464
+ const register = this.registerForRange(range);
465
+ switch (range.type) {
466
+ case "charwise":
467
+ this.moveCursorToPosition(range.start);
468
+ this.deleteForward(deleteDistance(this.lines, range.start, range.end));
469
+ return register;
470
+ case "linewise":
471
+ return this.applyLineChange(range, register);
472
+ }
473
+ }
474
+ /**
475
+ * Delete whole rows for a linewise range.
476
+ *
477
+ * The host editor only exposes character deletion, so deleting the original EOF
478
+ * row needs one backward delete to remove the leftover empty line. After rows are
479
+ * removed, Vim keeps the old column where possible and clamps on shorter lines.
480
+ */
481
+ applyLineDelete(range, register) {
482
+ const currentCol = this.cursor.col;
483
+ const lastLine = this.lines.length - 1;
484
+ const deletesLastLine = range.endLine >= lastLine;
485
+ const lineCount = range.endLine - range.startLine + 1;
486
+ // Start at column 0 because linewise delete removes rows, not a span from the
487
+ // current cursor column.
488
+ this.moveCursorToPosition({ line: range.startLine, col: 0 });
489
+ for (let i = 0; i < lineCount; ++i) {
490
+ this.deleteForward(this.currentLine.length);
491
+ if (this.cursor.line < this.lines.length - 1) {
492
+ this.host.sendInputToEditor(DELETE_FORWARD);
493
+ }
494
+ }
495
+ if (deletesLastLine && range.startLine > 0) {
496
+ // Deleting the final row leaves an empty last line; backspace removes that
497
+ // row by joining it into the previous surviving line.
498
+ this.host.sendInputToEditor(DELETE_BACKWARD);
499
+ }
500
+ // Land on the next surviving row, unless the deleted range reached EOF; then
501
+ // land on the previous row. Keep the original column where possible.
502
+ const targetLine = deletesLastLine ? range.startLine - 1 : range.startLine;
503
+ const line = Math.max(targetLine, 0);
504
+ this.moveCursorToPosition(this.clampedPosition({ line, col: currentCol }));
505
+ return register;
506
+ }
507
+ /** Clear a linewise range to one empty row, which becomes the Insert target. */
508
+ applyLineChange(range, register) {
509
+ this.moveCursorToPosition({ line: range.startLine, col: 0 });
510
+ this.deleteForward(this.currentLine.length);
511
+ for (let line = range.startLine; line < range.endLine; ++line) {
512
+ if (this.cursor.line < this.lines.length - 1) {
513
+ this.host.sendInputToEditor(DELETE_FORWARD);
514
+ }
515
+ this.deleteForward(this.currentLine.length);
516
+ }
517
+ return register;
518
+ }
519
+ /** Join the next line into the current line. */
520
+ joinNextLine() {
521
+ if (this.cursor.line >= this.lines.length - 1) {
522
+ return;
523
+ }
524
+ const line = this.currentLine;
525
+ const nextLine = this.lines[this.cursor.line + 1] ?? "";
526
+ const indent = /^\s*/.exec(nextLine)?.[0].length ?? 0;
527
+ const needsSpace = line.length > 0 && nextLine.trimStart().length > 0 && !/\s$/.test(line);
528
+ this.moveCursorToPosition({ line: this.cursor.line, col: line.length });
529
+ this.host.sendInputToEditor(DELETE_FORWARD);
530
+ this.deleteForward(indent);
531
+ if (needsSpace) {
532
+ this.host.sendInputToEditor(" ");
533
+ this.host.sendInputToEditor(ARROW_LEFT);
534
+ }
535
+ }
536
+ /** Move to a zero-based position using host editor cursor primitives. */
537
+ moveCursorToPosition(position) {
538
+ while (this.cursor.line < position.line) {
539
+ this.host.sendInputToEditor(ARROW_DOWN);
540
+ }
541
+ while (this.cursor.line > position.line) {
542
+ this.host.sendInputToEditor(ARROW_UP);
543
+ }
544
+ this.moveCaretToColumn(position.col);
545
+ }
546
+ /** Move to a zero-based column using host editor cursor primitives. */
547
+ moveCaretToColumn(column) {
548
+ this.host.sendInputToEditor(LINE_START);
549
+ for (let i = 0; i < column; i += 1) {
550
+ this.host.sendInputToEditor(ARROW_RIGHT);
551
+ }
552
+ }
553
+ /** Clamp a requested destination to a valid Normal-mode cursor column. */
554
+ clampedPosition(position) {
555
+ return {
556
+ line: position.line,
557
+ col: Math.min(position.col, normalMaxColumn(this.lines[position.line] ?? "")),
558
+ };
559
+ }
560
+ /** Delete `count` characters using the host editor's forward-delete primitive. */
561
+ deleteForward(count) {
562
+ for (let i = 0; i < count; ++i) {
563
+ this.host.sendInputToEditor(DELETE_FORWARD);
564
+ }
565
+ }
566
+ /** Send plain inserted text one character at a time; host editors parse keys, not strings. */
567
+ insertText(text) {
568
+ for (const char of text) {
569
+ this.host.sendInputToEditor(char);
570
+ }
571
+ }
572
+ /** Build the register metadata for a range without mutating editor state. */
573
+ registerForRange(range) {
574
+ return registerForRange(this.lines, range);
575
+ }
576
+ /** Emit successful unnamed-register writes to the configured host hook. */
577
+ emitUnnamedRegisterWrite(register) {
578
+ if (register) {
579
+ this.options.onUnnamedRegisterWrite?.(register);
580
+ }
581
+ }
582
+ }
583
+ function isVisualSelection(target) {
584
+ return typeof target === "object" && "mode" in target && "anchor" in target;
585
+ }
586
+ function isVimPosition(target) {
587
+ return typeof target === "object" && "line" in target && "col" in target;
588
+ }
589
+ /** Apply the requested case transform to plain text. */
590
+ function transformCaseText(text, transform) {
591
+ switch (transform) {
592
+ case "toggle":
593
+ return Array.from(text, toggleCase).join("");
594
+ case "lower":
595
+ return text.toLocaleLowerCase();
596
+ case "upper":
597
+ return text.toLocaleUpperCase();
598
+ }
599
+ }
600
+ /** Return a host editor subclass with reusable Vim editing operations. */
601
+ export function VimEditor(Base) {
602
+ return class VimEditor extends Base {
603
+ vimEditor = new VimEditorCore(this);
604
+ };
605
+ }
606
+ /** Return the target column for an f/F/t/T search, or undefined when not found. */
607
+ function findCharPosition(line, column, operation, direction, char, count) {
608
+ const step = direction === "backward" ? -1 : 1;
609
+ let matches = Math.max(count, 1);
610
+ for (let col = column + step; col >= 0 && col < line.length; col += step) {
611
+ if (line[col] !== char) {
612
+ continue;
613
+ }
614
+ matches -= 1;
615
+ if (matches === 0) {
616
+ const offset = operation === "till" ? -step : 0;
617
+ return Math.min(Math.max(col + offset, 0), normalMaxColumn(line));
618
+ }
619
+ }
620
+ return undefined;
621
+ }
@@ -0,0 +1,17 @@
1
+ import type { VimPosition, VimRange, VimRegister } from "./types.js";
2
+ /** Build the register metadata for a range without mutating editor state. */
3
+ export declare function registerForRange(lines: string[], range: VimRange): VimRegister;
4
+ /** Resolve repeated word-ish motions without mutating the host editor. */
5
+ export declare function countedWordPosition(lines: string[], start: VimPosition, noun: WordMotion, count: number): VimPosition;
6
+ export declare function samePosition(left: VimPosition, right: VimPosition): boolean;
7
+ /** Return a forward charwise range even when the motion destination is before the cursor. */
8
+ export declare function normalizedCharRange(start: VimPosition, end: VimPosition): VimRange;
9
+ /**
10
+ * Return how many forward deletes move text from `start` up to `end`.
11
+ *
12
+ * Crossing a line counts the newline separator as one deleted character, matching
13
+ * the host editor's repeated forward-delete behavior.
14
+ */
15
+ export declare function deleteDistance(lines: string[], start: VimPosition, end: VimPosition): number;
16
+ type WordMotion = "nextWord" | "previousWord" | "endOfWord" | "nextBigWord" | "previousBigWord" | "endOfBigWord";
17
+ export {};