ep_vim 0.6.1 → 0.7.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.
- package/.claude/settings.local.json +8 -0
- package/package.json +1 -1
- package/static/js/index.js +131 -7
- package/static/js/vim-core.js +129 -0
package/package.json
CHANGED
package/static/js/index.js
CHANGED
|
@@ -18,6 +18,9 @@ const {
|
|
|
18
18
|
paragraphForward,
|
|
19
19
|
paragraphBackward,
|
|
20
20
|
getTextInRange,
|
|
21
|
+
matchingBracketPos,
|
|
22
|
+
paragraphTextRange,
|
|
23
|
+
sentenceTextRange,
|
|
21
24
|
} = require("./vim-core");
|
|
22
25
|
|
|
23
26
|
// --- State variables ---
|
|
@@ -48,6 +51,73 @@ const textObjectRange = (key, lineText, char, type) => {
|
|
|
48
51
|
return textBracketRange(lineText, char, key, type);
|
|
49
52
|
};
|
|
50
53
|
|
|
54
|
+
const resolveTextObject = (key, type, line, lineText, char, rep) => {
|
|
55
|
+
if (key === "p") {
|
|
56
|
+
return paragraphTextRange(rep, line, type);
|
|
57
|
+
}
|
|
58
|
+
if (key === "s") {
|
|
59
|
+
const r = sentenceTextRange(lineText, char, type);
|
|
60
|
+
if (!r) return null;
|
|
61
|
+
return {
|
|
62
|
+
startLine: line,
|
|
63
|
+
startChar: r.start,
|
|
64
|
+
endLine: line,
|
|
65
|
+
endChar: r.end,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
const r = textObjectRange(key, lineText, char, type);
|
|
69
|
+
if (!r) return null;
|
|
70
|
+
return { startLine: line, startChar: r.start, endLine: line, endChar: r.end };
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const getVisibleLineRange = (rep) => {
|
|
74
|
+
const totalLines = rep.lines.length();
|
|
75
|
+
if (!editorDoc) return { top: 0, bottom: totalLines - 1 };
|
|
76
|
+
const lineDivs = editorDoc.body.querySelectorAll("div");
|
|
77
|
+
const lineCount = Math.min(lineDivs.length, totalLines);
|
|
78
|
+
|
|
79
|
+
// The iframe doesn't scroll — the outer page does. getBoundingClientRect()
|
|
80
|
+
// inside the iframe is relative to the iframe document top (not the outer
|
|
81
|
+
// viewport). We need the iframe's own position in the outer viewport to
|
|
82
|
+
// know which lines are actually visible.
|
|
83
|
+
const frameEl = editorDoc.defaultView.frameElement;
|
|
84
|
+
const iframeTop = frameEl ? frameEl.getBoundingClientRect().top : 0;
|
|
85
|
+
const outerViewportHeight = window.parent ? window.parent.innerHeight : 600;
|
|
86
|
+
|
|
87
|
+
let top = 0;
|
|
88
|
+
let bottom = lineCount - 1;
|
|
89
|
+
for (let i = 0; i < lineCount; i++) {
|
|
90
|
+
const rect = lineDivs[i].getBoundingClientRect();
|
|
91
|
+
if (iframeTop + rect.bottom > 0) {
|
|
92
|
+
top = i;
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
for (let i = lineCount - 1; i >= 0; i--) {
|
|
97
|
+
const rect = lineDivs[i].getBoundingClientRect();
|
|
98
|
+
if (iframeTop + rect.top < outerViewportHeight) {
|
|
99
|
+
bottom = i;
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Lines can wrap, so find the middle by pixel position rather than index.
|
|
105
|
+
const visibleTop = iframeTop + lineDivs[top].getBoundingClientRect().top;
|
|
106
|
+
const visibleBottom =
|
|
107
|
+
iframeTop + lineDivs[bottom].getBoundingClientRect().bottom;
|
|
108
|
+
const pixelMidpoint = (visibleTop + visibleBottom) / 2;
|
|
109
|
+
let mid = top;
|
|
110
|
+
for (let i = top; i <= bottom; i++) {
|
|
111
|
+
const rect = lineDivs[i].getBoundingClientRect();
|
|
112
|
+
if (iframeTop + (rect.top + rect.bottom) / 2 >= pixelMidpoint) {
|
|
113
|
+
mid = i;
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return { top, mid, bottom };
|
|
119
|
+
};
|
|
120
|
+
|
|
51
121
|
// --- Count helpers ---
|
|
52
122
|
|
|
53
123
|
const consumeCount = () => {
|
|
@@ -66,7 +136,9 @@ const getCount = () => pendingCount || 1;
|
|
|
66
136
|
const setRegister = (value) => {
|
|
67
137
|
register = value;
|
|
68
138
|
const text = Array.isArray(value) ? value.join("\n") + "\n" : value;
|
|
69
|
-
navigator.clipboard
|
|
139
|
+
if (navigator.clipboard) {
|
|
140
|
+
navigator.clipboard.writeText(text).catch(() => {});
|
|
141
|
+
}
|
|
70
142
|
};
|
|
71
143
|
|
|
72
144
|
const moveCursor = (editorInfo, line, char) => {
|
|
@@ -315,6 +387,38 @@ const resolveMotion = (key, line, char, lineText, rep, count) => {
|
|
|
315
387
|
return "pending";
|
|
316
388
|
}
|
|
317
389
|
|
|
390
|
+
if (key === "%") {
|
|
391
|
+
const pos = matchingBracketPos(rep, line, char);
|
|
392
|
+
if (pos) {
|
|
393
|
+
desiredColumn = null;
|
|
394
|
+
return { line: pos.line, char: pos.char };
|
|
395
|
+
}
|
|
396
|
+
return { line, char };
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
if (key === "H") {
|
|
400
|
+
desiredColumn = null;
|
|
401
|
+
const { top } = getVisibleLineRange(rep);
|
|
402
|
+
const targetLine = clampLine(top + count - 1, rep);
|
|
403
|
+
const targetText = getLineText(rep, targetLine);
|
|
404
|
+
return { line: targetLine, char: firstNonBlank(targetText) };
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
if (key === "M") {
|
|
408
|
+
desiredColumn = null;
|
|
409
|
+
const { mid } = getVisibleLineRange(rep);
|
|
410
|
+
const targetText = getLineText(rep, mid);
|
|
411
|
+
return { line: mid, char: firstNonBlank(targetText) };
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
if (key === "L") {
|
|
415
|
+
desiredColumn = null;
|
|
416
|
+
const { bottom } = getVisibleLineRange(rep);
|
|
417
|
+
const targetLine = clampLine(bottom - count + 1, rep);
|
|
418
|
+
const targetText = getLineText(rep, targetLine);
|
|
419
|
+
return { line: targetLine, char: firstNonBlank(targetText) };
|
|
420
|
+
}
|
|
421
|
+
|
|
318
422
|
return null;
|
|
319
423
|
};
|
|
320
424
|
|
|
@@ -472,12 +576,12 @@ const handleKey = (rep, editorInfo, key) => {
|
|
|
472
576
|
const type = pendingKey;
|
|
473
577
|
pendingKey = null;
|
|
474
578
|
pendingOperator = null;
|
|
475
|
-
const range =
|
|
579
|
+
const range = resolveTextObject(key, type, line, lineText, char, rep);
|
|
476
580
|
if (range) {
|
|
477
581
|
applyCharOperator(
|
|
478
582
|
op,
|
|
479
|
-
[
|
|
480
|
-
[
|
|
583
|
+
[range.startLine, range.startChar],
|
|
584
|
+
[range.endLine, range.endChar],
|
|
481
585
|
editorInfo,
|
|
482
586
|
rep,
|
|
483
587
|
);
|
|
@@ -521,6 +625,26 @@ const handleKey = (rep, editorInfo, key) => {
|
|
|
521
625
|
return true;
|
|
522
626
|
}
|
|
523
627
|
|
|
628
|
+
if (key === "%") {
|
|
629
|
+
pendingOperator = null;
|
|
630
|
+
const matchPos = matchingBracketPos(rep, line, char);
|
|
631
|
+
if (matchPos) {
|
|
632
|
+
let start, end;
|
|
633
|
+
if (
|
|
634
|
+
matchPos.line > line ||
|
|
635
|
+
(matchPos.line === line && matchPos.char > char)
|
|
636
|
+
) {
|
|
637
|
+
start = [line, char];
|
|
638
|
+
end = [matchPos.line, matchPos.char + 1];
|
|
639
|
+
} else {
|
|
640
|
+
start = [matchPos.line, matchPos.char];
|
|
641
|
+
end = [line, char + 1];
|
|
642
|
+
}
|
|
643
|
+
applyCharOperator(op, start, end, editorInfo, rep);
|
|
644
|
+
}
|
|
645
|
+
return true;
|
|
646
|
+
}
|
|
647
|
+
|
|
524
648
|
pendingOperator = null;
|
|
525
649
|
const range = motionRange(key, char, lineText, count);
|
|
526
650
|
if (range && range.end > range.start) {
|
|
@@ -540,10 +664,10 @@ const handleKey = (rep, editorInfo, key) => {
|
|
|
540
664
|
if (inVisual && (pendingKey === "i" || pendingKey === "a")) {
|
|
541
665
|
const type = pendingKey;
|
|
542
666
|
pendingKey = null;
|
|
543
|
-
const range =
|
|
667
|
+
const range = resolveTextObject(key, type, line, lineText, char, rep);
|
|
544
668
|
if (range) {
|
|
545
|
-
visualAnchor = [
|
|
546
|
-
visualCursor = [
|
|
669
|
+
visualAnchor = [range.startLine, range.startChar];
|
|
670
|
+
visualCursor = [range.endLine, range.endChar];
|
|
547
671
|
setVisualMode("char");
|
|
548
672
|
updateVisualSelection(editorInfo, rep);
|
|
549
673
|
}
|
package/static/js/vim-core.js
CHANGED
|
@@ -301,6 +301,132 @@ const textBracketRange = (lineText, char, bracket, type) => {
|
|
|
301
301
|
return null;
|
|
302
302
|
};
|
|
303
303
|
|
|
304
|
+
const offsetToPos = (rep, offset) => {
|
|
305
|
+
const totalLines = rep.lines.length();
|
|
306
|
+
for (let i = 0; i < totalLines; i++) {
|
|
307
|
+
const lineStart = rep.lines.offsetOfIndex(i);
|
|
308
|
+
const lineLen = getLineText(rep, i).length;
|
|
309
|
+
if (offset >= lineStart && offset < lineStart + lineLen) {
|
|
310
|
+
return { line: i, char: offset - lineStart };
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return null;
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
const matchingBracketPos = (rep, line, char) => {
|
|
317
|
+
const lineText = getLineText(rep, line);
|
|
318
|
+
let bracketChar = -1;
|
|
319
|
+
let bracket = null;
|
|
320
|
+
for (let i = char; i < lineText.length; i++) {
|
|
321
|
+
if (lineText[i] in BRACKET_PAIRS) {
|
|
322
|
+
bracketChar = i;
|
|
323
|
+
bracket = lineText[i];
|
|
324
|
+
break;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
if (bracketChar === -1) return null;
|
|
328
|
+
const isOpen = OPEN_BRACKETS.has(bracket);
|
|
329
|
+
const match = BRACKET_PAIRS[bracket];
|
|
330
|
+
const startOffset = rep.lines.offsetOfIndex(line) + bracketChar;
|
|
331
|
+
const alltext = rep.alltext;
|
|
332
|
+
let depth = 0;
|
|
333
|
+
if (isOpen) {
|
|
334
|
+
for (let i = startOffset; i < alltext.length; i++) {
|
|
335
|
+
if (alltext[i] === bracket) depth++;
|
|
336
|
+
else if (alltext[i] === match) {
|
|
337
|
+
depth--;
|
|
338
|
+
if (depth === 0) return offsetToPos(rep, i);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
} else {
|
|
342
|
+
for (let i = startOffset; i >= 0; i--) {
|
|
343
|
+
if (alltext[i] === bracket) depth++;
|
|
344
|
+
else if (alltext[i] === match) {
|
|
345
|
+
depth--;
|
|
346
|
+
if (depth === 0) return offsetToPos(rep, i);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
return null;
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
const paragraphTextRange = (rep, line, type) => {
|
|
354
|
+
const totalLines = rep.lines.length();
|
|
355
|
+
const lineIsBlank = (l) => getLineText(rep, l).length === 0;
|
|
356
|
+
const onBlank = lineIsBlank(line);
|
|
357
|
+
let start = line;
|
|
358
|
+
while (start > 0 && lineIsBlank(start - 1) === onBlank) start--;
|
|
359
|
+
let end = line;
|
|
360
|
+
while (end < totalLines - 1 && lineIsBlank(end + 1) === onBlank) end++;
|
|
361
|
+
if (type === "i") {
|
|
362
|
+
return {
|
|
363
|
+
startLine: start,
|
|
364
|
+
startChar: 0,
|
|
365
|
+
endLine: end,
|
|
366
|
+
endChar: getLineText(rep, end).length,
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
if (!onBlank) {
|
|
370
|
+
let trailingEnd = end;
|
|
371
|
+
while (trailingEnd < totalLines - 1 && lineIsBlank(trailingEnd + 1))
|
|
372
|
+
trailingEnd++;
|
|
373
|
+
if (trailingEnd > end) {
|
|
374
|
+
return {
|
|
375
|
+
startLine: start,
|
|
376
|
+
startChar: 0,
|
|
377
|
+
endLine: trailingEnd,
|
|
378
|
+
endChar: getLineText(rep, trailingEnd).length,
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
let leadingStart = start;
|
|
382
|
+
while (leadingStart > 0 && lineIsBlank(leadingStart - 1)) leadingStart--;
|
|
383
|
+
return {
|
|
384
|
+
startLine: leadingStart,
|
|
385
|
+
startChar: 0,
|
|
386
|
+
endLine: end,
|
|
387
|
+
endChar: getLineText(rep, end).length,
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
let paraEnd = end;
|
|
391
|
+
while (paraEnd < totalLines - 1 && !lineIsBlank(paraEnd + 1)) paraEnd++;
|
|
392
|
+
return {
|
|
393
|
+
startLine: start,
|
|
394
|
+
startChar: 0,
|
|
395
|
+
endLine: paraEnd,
|
|
396
|
+
endChar: getLineText(rep, paraEnd).length,
|
|
397
|
+
};
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
const sentenceTextRange = (lineText, char, type) => {
|
|
401
|
+
const isTerminator = (ch) => ch === "." || ch === "!" || ch === "?";
|
|
402
|
+
let start = 0;
|
|
403
|
+
for (let i = char - 1; i >= 0; i--) {
|
|
404
|
+
if (isTerminator(lineText[i])) {
|
|
405
|
+
let j = i + 1;
|
|
406
|
+
while (j < lineText.length && lineText[j] === " ") j++;
|
|
407
|
+
start = j;
|
|
408
|
+
break;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
let end = lineText.length;
|
|
412
|
+
for (let i = char; i < lineText.length; i++) {
|
|
413
|
+
if (isTerminator(lineText[i])) {
|
|
414
|
+
end = i + 1;
|
|
415
|
+
break;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
if (type === "i") {
|
|
419
|
+
let s = start;
|
|
420
|
+
let e = end;
|
|
421
|
+
while (s < e && lineText[s] === " ") s++;
|
|
422
|
+
while (e > s && lineText[e - 1] === " ") e--;
|
|
423
|
+
return { start: s, end: e };
|
|
424
|
+
}
|
|
425
|
+
let e = end;
|
|
426
|
+
while (e < lineText.length && lineText[e] === " ") e++;
|
|
427
|
+
return { start, end: e };
|
|
428
|
+
};
|
|
429
|
+
|
|
304
430
|
const getTextInRange = (rep, start, end, type) => {
|
|
305
431
|
if (start[0] === end[0]) {
|
|
306
432
|
return getLineText(rep, start[0]).slice(start[1], end[1]);
|
|
@@ -336,4 +462,7 @@ module.exports = {
|
|
|
336
462
|
paragraphForward,
|
|
337
463
|
paragraphBackward,
|
|
338
464
|
getTextInRange,
|
|
465
|
+
matchingBracketPos,
|
|
466
|
+
paragraphTextRange,
|
|
467
|
+
sentenceTextRange,
|
|
339
468
|
};
|