vim-sim 1.0.4 → 1.0.6
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/dist/index.d.ts +57 -1
- package/dist/index.js +503 -24
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -811,6 +811,13 @@ interface VimConfig {
|
|
|
811
811
|
undolevels: number;
|
|
812
812
|
clipboard: 'unnamed' | 'unnamedplus' | '';
|
|
813
813
|
}
|
|
814
|
+
/**
|
|
815
|
+
* Shell filter hook — set this on the ConfigManager instance directly.
|
|
816
|
+
* Called for :%!cmd and !! operators.
|
|
817
|
+
* Receives the shell command string and the input text; returns the filtered output.
|
|
818
|
+
* If not set, shell filter commands are no-ops.
|
|
819
|
+
*/
|
|
820
|
+
type FilterCommandFn = (cmd: string, input: string) => string;
|
|
814
821
|
type VimConfigKey = keyof VimConfig;
|
|
815
822
|
/**
|
|
816
823
|
* Metadata about a config option
|
|
@@ -830,6 +837,11 @@ interface VimConfigMetadata {
|
|
|
830
837
|
declare class ConfigManager {
|
|
831
838
|
private config;
|
|
832
839
|
private listeners;
|
|
840
|
+
/**
|
|
841
|
+
* Shell filter callback for :%!cmd and !! operators.
|
|
842
|
+
* Set this directly: configManager.filterCommand = (cmd, input) => ...
|
|
843
|
+
*/
|
|
844
|
+
filterCommand: FilterCommandFn | undefined;
|
|
833
845
|
constructor(initialConfig?: Partial<VimConfig>);
|
|
834
846
|
/**
|
|
835
847
|
* Get a config value
|
|
@@ -906,6 +918,25 @@ interface LastCommand {
|
|
|
906
918
|
count?: number;
|
|
907
919
|
register?: string;
|
|
908
920
|
}
|
|
921
|
+
interface LastSubstitute {
|
|
922
|
+
pattern: string;
|
|
923
|
+
replacement: string;
|
|
924
|
+
global: boolean;
|
|
925
|
+
ignoreCase: boolean;
|
|
926
|
+
}
|
|
927
|
+
interface ConfirmMatch {
|
|
928
|
+
line: number;
|
|
929
|
+
startCol: number;
|
|
930
|
+
endCol: number;
|
|
931
|
+
matchText: string;
|
|
932
|
+
replacementText: string;
|
|
933
|
+
}
|
|
934
|
+
interface PendingConfirmSubstitute {
|
|
935
|
+
matches: ConfirmMatch[];
|
|
936
|
+
currentIndex: number;
|
|
937
|
+
appliedIndices: number[];
|
|
938
|
+
originalContent: string;
|
|
939
|
+
}
|
|
909
940
|
declare class State {
|
|
910
941
|
buffer: Buffer;
|
|
911
942
|
cursor: Cursor;
|
|
@@ -929,7 +960,9 @@ declare class State {
|
|
|
929
960
|
undoTree: UndoTree | null;
|
|
930
961
|
lastCommand: LastCommand | null;
|
|
931
962
|
configManager: ConfigManager | null;
|
|
932
|
-
|
|
963
|
+
lastSubstitute: LastSubstitute | null;
|
|
964
|
+
pendingConfirm: PendingConfirmSubstitute | null;
|
|
965
|
+
constructor(buffer: Buffer, cursor: Cursor, selection: Range | null, mode: Mode, commandLine?: string, desiredColumn?: number | null, viewport?: VimViewport, visualAnchor?: Cursor | null, lastVisualSelection?: LastVisualSelection | null, recordingRegister?: string | null, lastMacroRegister?: string | null, foldManager?: FoldManager, markManager?: MarkManager, jumpListManager?: JumpListManager, fileSystem?: FileSystemManager, windowManager?: WindowManager, lastSearch?: SearchState | null, spellChecker?: SpellChecker, completionManager?: CompletionManager, undoTree?: UndoTree | null, lastCommand?: LastCommand | null, configManager?: ConfigManager | null, lastSubstitute?: LastSubstitute | null, pendingConfirm?: PendingConfirmSubstitute | null);
|
|
933
966
|
}
|
|
934
967
|
|
|
935
968
|
declare abstract class TextObject {
|
|
@@ -1026,6 +1059,7 @@ interface CommandContext {
|
|
|
1026
1059
|
indentationManager?: IndentationManager;
|
|
1027
1060
|
args?: string;
|
|
1028
1061
|
range?: Range;
|
|
1062
|
+
lastSubstitute?: LastSubstitute | null;
|
|
1029
1063
|
}
|
|
1030
1064
|
declare abstract class Command {
|
|
1031
1065
|
abstract key: string;
|
|
@@ -1139,6 +1173,28 @@ declare class Session {
|
|
|
1139
1173
|
handleKey(key: string): void;
|
|
1140
1174
|
private handleInsertMode;
|
|
1141
1175
|
private handleCommandMode;
|
|
1176
|
+
private resolveAddress;
|
|
1177
|
+
/**
|
|
1178
|
+
* Resolve a vim range string to [startLine, endLine] (0-indexed, inclusive).
|
|
1179
|
+
* Returns null when the range string contains no range (use defaults for the command).
|
|
1180
|
+
*/
|
|
1181
|
+
private resolveRange;
|
|
1182
|
+
/**
|
|
1183
|
+
* Split a raw ex command string into [rangeStr, commandBody].
|
|
1184
|
+
* Handles: %, ., $, N, N,M, .,$ etc.
|
|
1185
|
+
*/
|
|
1186
|
+
private splitRangeAndCommand;
|
|
1187
|
+
private executeMoveLines;
|
|
1188
|
+
private executeSubstitute;
|
|
1189
|
+
private executeGlobal;
|
|
1190
|
+
/** Execute a single ex sub-command (used by :g/:v) with cursor on `currentLine`. */
|
|
1191
|
+
private executeSubCommand;
|
|
1192
|
+
private executeShellFilter;
|
|
1193
|
+
/**
|
|
1194
|
+
* Handle key input while a confirm-substitution is pending.
|
|
1195
|
+
* Keys: y (apply), n (skip), a (apply all), q (quit/cancel remaining), l (apply this + quit)
|
|
1196
|
+
*/
|
|
1197
|
+
private handleConfirmMode;
|
|
1142
1198
|
private executeCommand;
|
|
1143
1199
|
private match;
|
|
1144
1200
|
private tryMatch;
|
package/dist/index.js
CHANGED
|
@@ -1525,6 +1525,11 @@ var CompletionManager = class _CompletionManager {
|
|
|
1525
1525
|
var ConfigManager = class _ConfigManager {
|
|
1526
1526
|
config;
|
|
1527
1527
|
listeners = /* @__PURE__ */ new Map();
|
|
1528
|
+
/**
|
|
1529
|
+
* Shell filter callback for :%!cmd and !! operators.
|
|
1530
|
+
* Set this directly: configManager.filterCommand = (cmd, input) => ...
|
|
1531
|
+
*/
|
|
1532
|
+
filterCommand = void 0;
|
|
1528
1533
|
constructor(initialConfig) {
|
|
1529
1534
|
this.config = {
|
|
1530
1535
|
// Display
|
|
@@ -1856,7 +1861,7 @@ var CONFIG_METADATA = {
|
|
|
1856
1861
|
|
|
1857
1862
|
// src/core/state.ts
|
|
1858
1863
|
var State = class {
|
|
1859
|
-
constructor(buffer, cursor, selection, mode, commandLine = "", desiredColumn = null, viewport = new VimViewport(), visualAnchor = null, lastVisualSelection = null, recordingRegister = null, lastMacroRegister = null, foldManager = new FoldManager(), markManager = new MarkManager(), jumpListManager = new JumpListManager(), fileSystem = new FileSystemManager(), windowManager = new WindowManager(), lastSearch = null, spellChecker = new SpellChecker(), completionManager = new CompletionManager(), undoTree = null, lastCommand = null, configManager = null) {
|
|
1864
|
+
constructor(buffer, cursor, selection, mode, commandLine = "", desiredColumn = null, viewport = new VimViewport(), visualAnchor = null, lastVisualSelection = null, recordingRegister = null, lastMacroRegister = null, foldManager = new FoldManager(), markManager = new MarkManager(), jumpListManager = new JumpListManager(), fileSystem = new FileSystemManager(), windowManager = new WindowManager(), lastSearch = null, spellChecker = new SpellChecker(), completionManager = new CompletionManager(), undoTree = null, lastCommand = null, configManager = null, lastSubstitute = null, pendingConfirm = null) {
|
|
1860
1865
|
this.buffer = buffer;
|
|
1861
1866
|
this.cursor = cursor;
|
|
1862
1867
|
this.selection = selection;
|
|
@@ -1879,6 +1884,8 @@ var State = class {
|
|
|
1879
1884
|
this.undoTree = undoTree;
|
|
1880
1885
|
this.lastCommand = lastCommand;
|
|
1881
1886
|
this.configManager = configManager;
|
|
1887
|
+
this.lastSubstitute = lastSubstitute;
|
|
1888
|
+
this.pendingConfirm = pendingConfirm;
|
|
1882
1889
|
}
|
|
1883
1890
|
};
|
|
1884
1891
|
function createEmptyState() {
|
|
@@ -3929,24 +3936,38 @@ function pasteCharacterwise(state, content, position) {
|
|
|
3929
3936
|
const lines = state.buffer.content.split("\n");
|
|
3930
3937
|
const currentLineIndex = state.cursor.line;
|
|
3931
3938
|
const currentLine2 = lines[currentLineIndex] ?? "";
|
|
3932
|
-
|
|
3933
|
-
|
|
3934
|
-
|
|
3935
|
-
|
|
3936
|
-
|
|
3937
|
-
newLine = beforeCursor + content + afterCursor;
|
|
3938
|
-
newColumn = state.cursor.column + content.length;
|
|
3939
|
-
|
|
3940
|
-
|
|
3941
|
-
|
|
3942
|
-
|
|
3943
|
-
|
|
3944
|
-
|
|
3945
|
-
|
|
3939
|
+
const contentLines = content.split("\n");
|
|
3940
|
+
const splitPoint = position === "after" ? state.cursor.column + 1 : state.cursor.column;
|
|
3941
|
+
const beforeCursor = currentLine2.substring(0, splitPoint);
|
|
3942
|
+
const afterCursor = currentLine2.substring(splitPoint);
|
|
3943
|
+
if (contentLines.length === 1) {
|
|
3944
|
+
const newLine = beforeCursor + content + afterCursor;
|
|
3945
|
+
const newColumn = position === "after" ? state.cursor.column + content.length : state.cursor.column + content.length - 1;
|
|
3946
|
+
lines[currentLineIndex] = newLine;
|
|
3947
|
+
return {
|
|
3948
|
+
...state,
|
|
3949
|
+
buffer: new Buffer(lines.join("\n")),
|
|
3950
|
+
cursor: new Cursor(currentLineIndex, Math.max(0, newColumn))
|
|
3951
|
+
};
|
|
3952
|
+
}
|
|
3953
|
+
const firstContentLine = contentLines[0];
|
|
3954
|
+
const lastContentLine = contentLines[contentLines.length - 1];
|
|
3955
|
+
const middleContentLines = contentLines.slice(1, -1);
|
|
3956
|
+
const newFirstLine = beforeCursor + firstContentLine;
|
|
3957
|
+
const newLastLine = lastContentLine + afterCursor;
|
|
3958
|
+
const newLines = [
|
|
3959
|
+
...lines.slice(0, currentLineIndex),
|
|
3960
|
+
newFirstLine,
|
|
3961
|
+
...middleContentLines,
|
|
3962
|
+
newLastLine,
|
|
3963
|
+
...lines.slice(currentLineIndex + 1)
|
|
3964
|
+
];
|
|
3965
|
+
const newCursorLine = currentLineIndex + contentLines.length - 1;
|
|
3966
|
+
const newCursorColumn = Math.max(0, lastContentLine.length - 1);
|
|
3946
3967
|
return {
|
|
3947
3968
|
...state,
|
|
3948
|
-
buffer: new Buffer(
|
|
3949
|
-
cursor: new Cursor(
|
|
3969
|
+
buffer: new Buffer(newLines.join("\n")),
|
|
3970
|
+
cursor: new Cursor(newCursorLine, newCursorColumn)
|
|
3950
3971
|
};
|
|
3951
3972
|
}
|
|
3952
3973
|
var p = class extends Command {
|
|
@@ -5511,6 +5532,152 @@ var ParagraphTextObject = class extends TextObject {
|
|
|
5511
5532
|
}
|
|
5512
5533
|
};
|
|
5513
5534
|
|
|
5535
|
+
// src/utils/substitute.ts
|
|
5536
|
+
function substitute(content, startLine, endLine, options) {
|
|
5537
|
+
const lines = content.split("\n");
|
|
5538
|
+
const modifiedLines = [];
|
|
5539
|
+
let totalReplacements = 0;
|
|
5540
|
+
const flags = (options.global ? "g" : "") + (options.ignoreCase ? "i" : "");
|
|
5541
|
+
const jsPattern = vimPatternToRegex(options.pattern);
|
|
5542
|
+
const regex = new RegExp(jsPattern, flags);
|
|
5543
|
+
for (let i = startLine; i <= endLine && i < lines.length; i++) {
|
|
5544
|
+
const line = lines[i];
|
|
5545
|
+
if (line === void 0) continue;
|
|
5546
|
+
const newLine = line.replace(regex, (match, ...args) => {
|
|
5547
|
+
totalReplacements++;
|
|
5548
|
+
return expandReplacement(options.replacement, match, args);
|
|
5549
|
+
});
|
|
5550
|
+
if (newLine !== line) {
|
|
5551
|
+
lines[i] = newLine;
|
|
5552
|
+
modifiedLines.push(i);
|
|
5553
|
+
}
|
|
5554
|
+
}
|
|
5555
|
+
return {
|
|
5556
|
+
newContent: lines.join("\n"),
|
|
5557
|
+
replacements: totalReplacements,
|
|
5558
|
+
lines: modifiedLines
|
|
5559
|
+
};
|
|
5560
|
+
}
|
|
5561
|
+
function findSubstituteMatches(content, startLine, endLine, options) {
|
|
5562
|
+
const lines = content.split("\n");
|
|
5563
|
+
const matches = [];
|
|
5564
|
+
const flags = "g" + (options.ignoreCase ? "i" : "");
|
|
5565
|
+
const jsPattern = vimPatternToRegex(options.pattern);
|
|
5566
|
+
const regex = new RegExp(jsPattern, flags);
|
|
5567
|
+
for (let i = startLine; i <= endLine && i < lines.length; i++) {
|
|
5568
|
+
const line = lines[i];
|
|
5569
|
+
if (line === void 0) continue;
|
|
5570
|
+
regex.lastIndex = 0;
|
|
5571
|
+
let match;
|
|
5572
|
+
let foundOnLine = false;
|
|
5573
|
+
while ((match = regex.exec(line)) !== null) {
|
|
5574
|
+
if (foundOnLine && !options.global) break;
|
|
5575
|
+
const groups = match.slice(1);
|
|
5576
|
+
matches.push({
|
|
5577
|
+
line: i,
|
|
5578
|
+
startCol: match.index,
|
|
5579
|
+
endCol: match.index + match[0].length,
|
|
5580
|
+
matchText: match[0],
|
|
5581
|
+
replacementText: expandReplacement(options.replacement, match[0], groups)
|
|
5582
|
+
});
|
|
5583
|
+
foundOnLine = true;
|
|
5584
|
+
if (match[0].length === 0) regex.lastIndex++;
|
|
5585
|
+
}
|
|
5586
|
+
}
|
|
5587
|
+
return matches;
|
|
5588
|
+
}
|
|
5589
|
+
function applySubstituteMatches(content, matches, appliedIndices) {
|
|
5590
|
+
const lines = content.split("\n");
|
|
5591
|
+
const sorted = Array.from(appliedIndices).sort((a, b2) => {
|
|
5592
|
+
const ma = matches[a];
|
|
5593
|
+
const mb = matches[b2];
|
|
5594
|
+
if (ma.line !== mb.line) return mb.line - ma.line;
|
|
5595
|
+
return mb.startCol - ma.startCol;
|
|
5596
|
+
});
|
|
5597
|
+
for (const idx of sorted) {
|
|
5598
|
+
const m = matches[idx];
|
|
5599
|
+
const line = lines[m.line] ?? "";
|
|
5600
|
+
lines[m.line] = line.slice(0, m.startCol) + m.replacementText + line.slice(m.endCol);
|
|
5601
|
+
}
|
|
5602
|
+
return lines.join("\n");
|
|
5603
|
+
}
|
|
5604
|
+
function evaluateVimExpr(expr, match, groups) {
|
|
5605
|
+
expr = expr.trim();
|
|
5606
|
+
const submatchRe = /^submatch\((\d+)\)$/;
|
|
5607
|
+
const submatchM = submatchRe.exec(expr);
|
|
5608
|
+
if (submatchM) {
|
|
5609
|
+
const n2 = parseInt(submatchM[1], 10);
|
|
5610
|
+
if (n2 === 0) return match;
|
|
5611
|
+
const g = groups[n2 - 1];
|
|
5612
|
+
return typeof g === "string" ? g : "";
|
|
5613
|
+
}
|
|
5614
|
+
const toupperM = /^toupper\((.+)\)$/.exec(expr);
|
|
5615
|
+
if (toupperM) return evaluateVimExpr(toupperM[1], match, groups).toUpperCase();
|
|
5616
|
+
const tolowerM = /^tolower\((.+)\)$/.exec(expr);
|
|
5617
|
+
if (tolowerM) return evaluateVimExpr(tolowerM[1], match, groups).toLowerCase();
|
|
5618
|
+
const strM = /^['"](.*)['"]$/.exec(expr);
|
|
5619
|
+
if (strM) return strM[1];
|
|
5620
|
+
return "";
|
|
5621
|
+
}
|
|
5622
|
+
function expandReplacement(replacement, match, groups) {
|
|
5623
|
+
let result = replacement;
|
|
5624
|
+
result = result.replace(/\\=([^\\]+)/g, (_, expr) => evaluateVimExpr(expr, match, groups));
|
|
5625
|
+
result = result.replace(/\\0/g, match);
|
|
5626
|
+
for (let i = 1; i <= 9; i++) {
|
|
5627
|
+
const group = groups[i - 1];
|
|
5628
|
+
if (group !== void 0) {
|
|
5629
|
+
result = result.replace(new RegExp(`\\\\${i}`, "g"), String(group));
|
|
5630
|
+
}
|
|
5631
|
+
}
|
|
5632
|
+
result = result.replace(/\\u(.)/g, (_, char) => char.toUpperCase());
|
|
5633
|
+
result = result.replace(/\\U(.*?)(?:\\E|$)/g, (_, text) => text.toUpperCase());
|
|
5634
|
+
result = result.replace(/\\l(.)/g, (_, char) => char.toLowerCase());
|
|
5635
|
+
result = result.replace(/\\L(.*?)(?:\\E|$)/g, (_, text) => text.toLowerCase());
|
|
5636
|
+
result = result.replace(/\\n/g, "\n");
|
|
5637
|
+
result = result.replace(/\\t/g, " ");
|
|
5638
|
+
result = result.replace(/\\r/g, "\r");
|
|
5639
|
+
result = result.replace(/\\\\/g, "\\");
|
|
5640
|
+
return result;
|
|
5641
|
+
}
|
|
5642
|
+
function parseSubstituteCommand(command) {
|
|
5643
|
+
const match = command.match(/^s(.)(.*?)\1(.*?)\1?([gicGIC]*)$/);
|
|
5644
|
+
if (!match) {
|
|
5645
|
+
return null;
|
|
5646
|
+
}
|
|
5647
|
+
const [, , pattern, replacement, flags] = match;
|
|
5648
|
+
return {
|
|
5649
|
+
pattern: pattern ?? "",
|
|
5650
|
+
replacement: replacement ?? "",
|
|
5651
|
+
global: (flags ?? "").toLowerCase().includes("g"),
|
|
5652
|
+
ignoreCase: (flags ?? "").toLowerCase().includes("i"),
|
|
5653
|
+
confirmEach: (flags ?? "").toLowerCase().includes("c")
|
|
5654
|
+
};
|
|
5655
|
+
}
|
|
5656
|
+
function vimPatternToRegex(vimPattern) {
|
|
5657
|
+
let p2 = vimPattern;
|
|
5658
|
+
p2 = p2.replace(/\\\(/g, "(");
|
|
5659
|
+
p2 = p2.replace(/\\\)/g, ")");
|
|
5660
|
+
p2 = p2.replace(/\\\|/g, "|");
|
|
5661
|
+
p2 = p2.replace(/\\=/g, "?");
|
|
5662
|
+
p2 = p2.replace(/\\\+/g, "+");
|
|
5663
|
+
p2 = p2.replace(/\\\{/g, "{");
|
|
5664
|
+
p2 = p2.replace(/\\\}/g, "}");
|
|
5665
|
+
p2 = p2.replace(/\\</g, "\\b");
|
|
5666
|
+
p2 = p2.replace(/\\>/g, "\\b");
|
|
5667
|
+
p2 = p2.replace(/\\s/g, "\\s");
|
|
5668
|
+
p2 = p2.replace(/\\S/g, "\\S");
|
|
5669
|
+
p2 = p2.replace(/\\d/g, "\\d");
|
|
5670
|
+
p2 = p2.replace(/\\D/g, "\\D");
|
|
5671
|
+
p2 = p2.replace(/\\w/g, "\\w");
|
|
5672
|
+
p2 = p2.replace(/\\W/g, "\\W");
|
|
5673
|
+
p2 = p2.replace(/\\a/g, "[a-zA-Z]");
|
|
5674
|
+
p2 = p2.replace(/\\A/g, "[^a-zA-Z]");
|
|
5675
|
+
p2 = p2.replace(/\\l/g, "[a-z]");
|
|
5676
|
+
p2 = p2.replace(/\\u/g, "[A-Z]");
|
|
5677
|
+
p2 = p2.replace(/\\n/g, "\\n");
|
|
5678
|
+
return p2;
|
|
5679
|
+
}
|
|
5680
|
+
|
|
5514
5681
|
// src/commands/misc.ts
|
|
5515
5682
|
var JumpForward = class extends Command {
|
|
5516
5683
|
key = "<C-i>";
|
|
@@ -6520,6 +6687,10 @@ var Session = class _Session {
|
|
|
6520
6687
|
}
|
|
6521
6688
|
handleKey(key) {
|
|
6522
6689
|
key = _Session.normalizeKey(key);
|
|
6690
|
+
if (this.state.pendingConfirm !== null) {
|
|
6691
|
+
this.handleConfirmMode(key);
|
|
6692
|
+
return;
|
|
6693
|
+
}
|
|
6523
6694
|
if (this.state.mode === 1 /* INSERT */) {
|
|
6524
6695
|
this.handleInsertMode(key);
|
|
6525
6696
|
return;
|
|
@@ -6551,6 +6722,7 @@ var Session = class _Session {
|
|
|
6551
6722
|
const stateBefore = this.state;
|
|
6552
6723
|
result.context.registerManager = this.registerManager;
|
|
6553
6724
|
result.context.indentationManager = this.indentationManager;
|
|
6725
|
+
result.context.lastSubstitute = this.state.lastSubstitute;
|
|
6554
6726
|
this.state = result.command.execute(this.state, result.context);
|
|
6555
6727
|
if ((stateBefore.mode === 2 /* VISUAL */ || stateBefore.mode === 3 /* VISUAL_LINE */ || stateBefore.mode === 4 /* VISUAL_BLOCK */) && this.state.mode === stateBefore.mode && // Still in same visual mode
|
|
6556
6728
|
stateBefore.visualAnchor && (this.state.cursor.line !== stateBefore.cursor.line || this.state.cursor.column !== stateBefore.cursor.column)) {
|
|
@@ -6700,24 +6872,331 @@ var Session = class _Session {
|
|
|
6700
6872
|
};
|
|
6701
6873
|
}
|
|
6702
6874
|
}
|
|
6875
|
+
// ── Ex-command range helpers ──────────────────────────────────────────────
|
|
6876
|
+
resolveAddress(addr, lines, currentLine2) {
|
|
6877
|
+
addr = addr.trim();
|
|
6878
|
+
if (addr === "" || addr === ".") return currentLine2;
|
|
6879
|
+
if (addr === "$") return lines.length - 1;
|
|
6880
|
+
const n2 = parseInt(addr, 10);
|
|
6881
|
+
if (!isNaN(n2)) return Math.max(0, Math.min(n2 - 1, lines.length - 1));
|
|
6882
|
+
return currentLine2;
|
|
6883
|
+
}
|
|
6884
|
+
/**
|
|
6885
|
+
* Resolve a vim range string to [startLine, endLine] (0-indexed, inclusive).
|
|
6886
|
+
* Returns null when the range string contains no range (use defaults for the command).
|
|
6887
|
+
*/
|
|
6888
|
+
resolveRange(rangeStr, lines, currentLine2) {
|
|
6889
|
+
if (rangeStr === "") return null;
|
|
6890
|
+
if (rangeStr === "%") return [0, lines.length - 1];
|
|
6891
|
+
if (rangeStr === ".") return [currentLine2, currentLine2];
|
|
6892
|
+
if (rangeStr === "$") return [lines.length - 1, lines.length - 1];
|
|
6893
|
+
const commaIdx = rangeStr.indexOf(",");
|
|
6894
|
+
if (commaIdx !== -1) {
|
|
6895
|
+
const start = this.resolveAddress(rangeStr.slice(0, commaIdx), lines, currentLine2);
|
|
6896
|
+
const end = this.resolveAddress(rangeStr.slice(commaIdx + 1), lines, currentLine2);
|
|
6897
|
+
return [Math.min(start, end), Math.max(start, end)];
|
|
6898
|
+
}
|
|
6899
|
+
const single = this.resolveAddress(rangeStr, lines, currentLine2);
|
|
6900
|
+
return [single, single];
|
|
6901
|
+
}
|
|
6902
|
+
/**
|
|
6903
|
+
* Split a raw ex command string into [rangeStr, commandBody].
|
|
6904
|
+
* Handles: %, ., $, N, N,M, .,$ etc.
|
|
6905
|
+
*/
|
|
6906
|
+
splitRangeAndCommand(cmd) {
|
|
6907
|
+
const rangeRe = /^(%|\.|(?:\d+|\.|\.?\$?)(?:,(?:\d+|\.|\.?\$?))?)/;
|
|
6908
|
+
const m = rangeRe.exec(cmd);
|
|
6909
|
+
if (m) {
|
|
6910
|
+
const range = m[0];
|
|
6911
|
+
const rest = cmd.slice(range.length).trimStart();
|
|
6912
|
+
return [range, rest];
|
|
6913
|
+
}
|
|
6914
|
+
return ["", cmd];
|
|
6915
|
+
}
|
|
6916
|
+
// ── :m (move) ──────────────────────────────────────────────────────────────
|
|
6917
|
+
executeMoveLines(startLine, endLine, dest) {
|
|
6918
|
+
const lines = this.state.buffer.content.split("\n");
|
|
6919
|
+
const count = endLine - startLine + 1;
|
|
6920
|
+
const toMove = lines.splice(startLine, count);
|
|
6921
|
+
let adjustedDest = dest;
|
|
6922
|
+
if (dest >= startLine) adjustedDest -= count;
|
|
6923
|
+
adjustedDest = Math.max(-1, Math.min(adjustedDest, lines.length - 1));
|
|
6924
|
+
lines.splice(adjustedDest + 1, 0, ...toMove);
|
|
6925
|
+
const newCursorLine = Math.min(adjustedDest + count, lines.length - 1);
|
|
6926
|
+
this.state = {
|
|
6927
|
+
...this.state,
|
|
6928
|
+
buffer: new Buffer(lines.join("\n")),
|
|
6929
|
+
cursor: new Cursor(Math.max(0, newCursorLine), 0),
|
|
6930
|
+
desiredColumn: null
|
|
6931
|
+
};
|
|
6932
|
+
}
|
|
6933
|
+
// ── :s (substitute) ────────────────────────────────────────────────────────
|
|
6934
|
+
executeSubstitute(rangeStr, subcmd) {
|
|
6935
|
+
const parsed = parseSubstituteCommand(subcmd);
|
|
6936
|
+
if (!parsed) return;
|
|
6937
|
+
const lines = this.state.buffer.content.split("\n");
|
|
6938
|
+
const currentLine2 = this.state.cursor.line;
|
|
6939
|
+
let startLine = currentLine2;
|
|
6940
|
+
let endLine = currentLine2;
|
|
6941
|
+
if (rangeStr !== "") {
|
|
6942
|
+
const resolved = this.resolveRange(rangeStr, lines, currentLine2);
|
|
6943
|
+
if (resolved) {
|
|
6944
|
+
[startLine, endLine] = resolved;
|
|
6945
|
+
}
|
|
6946
|
+
}
|
|
6947
|
+
const lastSub = {
|
|
6948
|
+
pattern: parsed.pattern,
|
|
6949
|
+
replacement: parsed.replacement,
|
|
6950
|
+
global: parsed.global,
|
|
6951
|
+
ignoreCase: parsed.ignoreCase
|
|
6952
|
+
};
|
|
6953
|
+
if (parsed.confirmEach) {
|
|
6954
|
+
const matches = findSubstituteMatches(
|
|
6955
|
+
this.state.buffer.content,
|
|
6956
|
+
startLine,
|
|
6957
|
+
endLine,
|
|
6958
|
+
{ pattern: parsed.pattern, replacement: parsed.replacement, global: parsed.global, ignoreCase: parsed.ignoreCase }
|
|
6959
|
+
);
|
|
6960
|
+
if (matches.length === 0) return;
|
|
6961
|
+
const pendingConfirm = {
|
|
6962
|
+
matches,
|
|
6963
|
+
currentIndex: 0,
|
|
6964
|
+
appliedIndices: [],
|
|
6965
|
+
originalContent: this.state.buffer.content
|
|
6966
|
+
};
|
|
6967
|
+
this.state = { ...this.state, lastSubstitute: lastSub, pendingConfirm };
|
|
6968
|
+
return;
|
|
6969
|
+
}
|
|
6970
|
+
const result = substitute(
|
|
6971
|
+
this.state.buffer.content,
|
|
6972
|
+
startLine,
|
|
6973
|
+
endLine,
|
|
6974
|
+
{ pattern: parsed.pattern, replacement: parsed.replacement, global: parsed.global, ignoreCase: parsed.ignoreCase }
|
|
6975
|
+
);
|
|
6976
|
+
if (result.replacements === 0) {
|
|
6977
|
+
this.state = { ...this.state, lastSubstitute: lastSub };
|
|
6978
|
+
return;
|
|
6979
|
+
}
|
|
6980
|
+
const lastModifiedLine = result.lines[result.lines.length - 1] ?? currentLine2;
|
|
6981
|
+
this.state = {
|
|
6982
|
+
...this.state,
|
|
6983
|
+
buffer: new Buffer(result.newContent),
|
|
6984
|
+
cursor: new Cursor(lastModifiedLine, 0),
|
|
6985
|
+
desiredColumn: null,
|
|
6986
|
+
lastSubstitute: lastSub
|
|
6987
|
+
};
|
|
6988
|
+
}
|
|
6989
|
+
// ── :g / :v (global) ───────────────────────────────────────────────────────
|
|
6990
|
+
executeGlobal(rangeStr, subcmd, invert) {
|
|
6991
|
+
const firstChar = subcmd[0];
|
|
6992
|
+
if (!firstChar) return;
|
|
6993
|
+
const parts = subcmd.slice(1).split(firstChar);
|
|
6994
|
+
if (parts.length < 2) return;
|
|
6995
|
+
const [pattern, ...cmdParts] = parts;
|
|
6996
|
+
const cmd = cmdParts.join(firstChar);
|
|
6997
|
+
const lines = this.state.buffer.content.split("\n");
|
|
6998
|
+
const currentLine2 = this.state.cursor.line;
|
|
6999
|
+
const resolved = this.resolveRange(rangeStr, lines, currentLine2) ?? [0, lines.length - 1];
|
|
7000
|
+
const [startLine, endLine] = resolved;
|
|
7001
|
+
let regex;
|
|
7002
|
+
try {
|
|
7003
|
+
regex = new RegExp(pattern ?? "", "g");
|
|
7004
|
+
} catch {
|
|
7005
|
+
return;
|
|
7006
|
+
}
|
|
7007
|
+
const matchingLines = [];
|
|
7008
|
+
for (let i = startLine; i <= endLine; i++) {
|
|
7009
|
+
const line = lines[i] ?? "";
|
|
7010
|
+
regex.lastIndex = 0;
|
|
7011
|
+
const matches = regex.test(line);
|
|
7012
|
+
if (matches !== invert) {
|
|
7013
|
+
matchingLines.push(i);
|
|
7014
|
+
}
|
|
7015
|
+
}
|
|
7016
|
+
let lineOffset = 0;
|
|
7017
|
+
for (const origLine of matchingLines) {
|
|
7018
|
+
const adjustedLine = origLine + lineOffset;
|
|
7019
|
+
const prevLineCount = this.state.buffer.content.split("\n").length;
|
|
7020
|
+
const currentLines = this.state.buffer.content.split("\n");
|
|
7021
|
+
const safeAdjusted = Math.max(0, Math.min(adjustedLine, currentLines.length - 1));
|
|
7022
|
+
this.state = {
|
|
7023
|
+
...this.state,
|
|
7024
|
+
cursor: new Cursor(safeAdjusted, 0)
|
|
7025
|
+
};
|
|
7026
|
+
this.executeSubCommand(cmd, safeAdjusted);
|
|
7027
|
+
const newLineCount = this.state.buffer.content.split("\n").length;
|
|
7028
|
+
lineOffset += newLineCount - prevLineCount;
|
|
7029
|
+
}
|
|
7030
|
+
}
|
|
7031
|
+
/** Execute a single ex sub-command (used by :g/:v) with cursor on `currentLine`. */
|
|
7032
|
+
executeSubCommand(subcmd, currentLine2) {
|
|
7033
|
+
const trimmed = subcmd.trim();
|
|
7034
|
+
if (trimmed === "") return;
|
|
7035
|
+
if (trimmed === "d" || trimmed === "delete") {
|
|
7036
|
+
const lines = this.state.buffer.content.split("\n");
|
|
7037
|
+
if (currentLine2 >= lines.length) return;
|
|
7038
|
+
lines.splice(currentLine2, 1);
|
|
7039
|
+
const newLine = Math.min(currentLine2, lines.length - 1);
|
|
7040
|
+
this.state = {
|
|
7041
|
+
...this.state,
|
|
7042
|
+
buffer: new Buffer(lines.join("\n")),
|
|
7043
|
+
cursor: new Cursor(Math.max(0, newLine), 0),
|
|
7044
|
+
desiredColumn: null
|
|
7045
|
+
};
|
|
7046
|
+
return;
|
|
7047
|
+
}
|
|
7048
|
+
const moveM = /^m(?:ove)?\s*(.*)$/.exec(trimmed);
|
|
7049
|
+
if (moveM) {
|
|
7050
|
+
const addrStr = (moveM[1] ?? "").trim();
|
|
7051
|
+
const lines = this.state.buffer.content.split("\n");
|
|
7052
|
+
const dest = addrStr === "0" ? -1 : this.resolveAddress(addrStr, lines, currentLine2);
|
|
7053
|
+
this.executeMoveLines(currentLine2, currentLine2, dest === -1 ? -1 : dest);
|
|
7054
|
+
return;
|
|
7055
|
+
}
|
|
7056
|
+
const subsM = /^s(.)/.exec(trimmed);
|
|
7057
|
+
if (subsM) {
|
|
7058
|
+
this.executeSubstitute(".", trimmed);
|
|
7059
|
+
return;
|
|
7060
|
+
}
|
|
7061
|
+
if (trimmed === "p" || trimmed === "print") return;
|
|
7062
|
+
}
|
|
7063
|
+
// ── Shell filter (%!cmd) ───────────────────────────────────────────────────
|
|
7064
|
+
executeShellFilter(rangeStr, shellCmd) {
|
|
7065
|
+
const filterFn = this.configManager.filterCommand;
|
|
7066
|
+
if (!filterFn) return;
|
|
7067
|
+
const lines = this.state.buffer.content.split("\n");
|
|
7068
|
+
const currentLine2 = this.state.cursor.line;
|
|
7069
|
+
const [startLine, endLine] = this.resolveRange(rangeStr, lines, currentLine2) ?? [0, lines.length - 1];
|
|
7070
|
+
const input = lines.slice(startLine, endLine + 1).join("\n");
|
|
7071
|
+
let output;
|
|
7072
|
+
try {
|
|
7073
|
+
output = filterFn(shellCmd, input);
|
|
7074
|
+
} catch {
|
|
7075
|
+
return;
|
|
7076
|
+
}
|
|
7077
|
+
const outputLines = output.split("\n");
|
|
7078
|
+
const newLines = [
|
|
7079
|
+
...lines.slice(0, startLine),
|
|
7080
|
+
...outputLines,
|
|
7081
|
+
...lines.slice(endLine + 1)
|
|
7082
|
+
];
|
|
7083
|
+
this.state = {
|
|
7084
|
+
...this.state,
|
|
7085
|
+
buffer: new Buffer(newLines.join("\n")),
|
|
7086
|
+
cursor: new Cursor(Math.min(startLine + outputLines.length - 1, newLines.length - 1), 0),
|
|
7087
|
+
desiredColumn: null
|
|
7088
|
+
};
|
|
7089
|
+
}
|
|
7090
|
+
// ── Confirm-substitution mode ─────────────────────────────────────────────
|
|
7091
|
+
/**
|
|
7092
|
+
* Handle key input while a confirm-substitution is pending.
|
|
7093
|
+
* Keys: y (apply), n (skip), a (apply all), q (quit/cancel remaining), l (apply this + quit)
|
|
7094
|
+
*/
|
|
7095
|
+
handleConfirmMode(key) {
|
|
7096
|
+
const pending = this.state.pendingConfirm;
|
|
7097
|
+
const { matches, currentIndex, appliedIndices } = pending;
|
|
7098
|
+
const advance = (applied) => {
|
|
7099
|
+
const nextIndex = currentIndex + 1;
|
|
7100
|
+
if (nextIndex >= matches.length) {
|
|
7101
|
+
const newContent = applySubstituteMatches(
|
|
7102
|
+
pending.originalContent,
|
|
7103
|
+
matches,
|
|
7104
|
+
new Set(applied)
|
|
7105
|
+
);
|
|
7106
|
+
this.state = {
|
|
7107
|
+
...this.state,
|
|
7108
|
+
buffer: new Buffer(newContent),
|
|
7109
|
+
pendingConfirm: null,
|
|
7110
|
+
cursor: new Cursor(matches[applied[applied.length - 1] ?? 0]?.line ?? this.state.cursor.line, 0),
|
|
7111
|
+
desiredColumn: null
|
|
7112
|
+
};
|
|
7113
|
+
} else {
|
|
7114
|
+
this.state = {
|
|
7115
|
+
...this.state,
|
|
7116
|
+
pendingConfirm: { ...pending, currentIndex: nextIndex, appliedIndices: applied }
|
|
7117
|
+
};
|
|
7118
|
+
}
|
|
7119
|
+
};
|
|
7120
|
+
const finishNow = (applied) => {
|
|
7121
|
+
const newContent = applySubstituteMatches(
|
|
7122
|
+
pending.originalContent,
|
|
7123
|
+
matches,
|
|
7124
|
+
new Set(applied)
|
|
7125
|
+
);
|
|
7126
|
+
const lastApplied = applied[applied.length - 1];
|
|
7127
|
+
this.state = {
|
|
7128
|
+
...this.state,
|
|
7129
|
+
buffer: new Buffer(newContent),
|
|
7130
|
+
pendingConfirm: null,
|
|
7131
|
+
cursor: new Cursor(lastApplied !== void 0 ? matches[lastApplied]?.line ?? this.state.cursor.line : this.state.cursor.line, 0),
|
|
7132
|
+
desiredColumn: null
|
|
7133
|
+
};
|
|
7134
|
+
};
|
|
7135
|
+
if (key === "y") {
|
|
7136
|
+
advance([...appliedIndices, currentIndex]);
|
|
7137
|
+
} else if (key === "n") {
|
|
7138
|
+
advance([...appliedIndices]);
|
|
7139
|
+
} else if (key === "a") {
|
|
7140
|
+
const allApplied = [...appliedIndices];
|
|
7141
|
+
for (let i = currentIndex; i < matches.length; i++) allApplied.push(i);
|
|
7142
|
+
finishNow(allApplied);
|
|
7143
|
+
} else if (key === "l") {
|
|
7144
|
+
finishNow([...appliedIndices, currentIndex]);
|
|
7145
|
+
} else if (key === "q" || key === "<Esc>") {
|
|
7146
|
+
finishNow([...appliedIndices]);
|
|
7147
|
+
}
|
|
7148
|
+
}
|
|
7149
|
+
// ── Main ex-command dispatcher ─────────────────────────────────────────────
|
|
6703
7150
|
executeCommand(command) {
|
|
6704
|
-
const
|
|
7151
|
+
const raw = command.trim();
|
|
7152
|
+
const [rangeStr, cmd] = this.splitRangeAndCommand(raw);
|
|
6705
7153
|
const setMatch = cmd.match(/^set\s+(.+)$/);
|
|
6706
7154
|
if (setMatch) {
|
|
6707
|
-
|
|
6708
|
-
|
|
7155
|
+
this.configManager.parseSetCommand(setMatch[1].trim());
|
|
7156
|
+
return;
|
|
7157
|
+
}
|
|
7158
|
+
if (/^s[^a-zA-Z]/.test(cmd)) {
|
|
7159
|
+
this.executeSubstitute(rangeStr, cmd);
|
|
7160
|
+
return;
|
|
7161
|
+
}
|
|
7162
|
+
const globalM = /^(g|v)([^a-zA-Z].+)$/.exec(cmd);
|
|
7163
|
+
if (globalM) {
|
|
7164
|
+
const invert = globalM[1] === "v";
|
|
7165
|
+
this.executeGlobal(rangeStr, globalM[2], invert);
|
|
6709
7166
|
return;
|
|
6710
7167
|
}
|
|
6711
|
-
|
|
7168
|
+
const moveM = /^m(?:ove)?\s*(.*)$/.exec(cmd);
|
|
7169
|
+
if (moveM) {
|
|
7170
|
+
const addrStr = (moveM[1] ?? "").trim();
|
|
7171
|
+
const lines = this.state.buffer.content.split("\n");
|
|
7172
|
+
const currentLine2 = this.state.cursor.line;
|
|
7173
|
+
const [startLine, endLine] = this.resolveRange(rangeStr, lines, currentLine2) ?? [currentLine2, currentLine2];
|
|
7174
|
+
const dest = addrStr === "0" ? -1 : this.resolveAddress(addrStr, lines, currentLine2);
|
|
7175
|
+
this.executeMoveLines(startLine, endLine, dest);
|
|
6712
7176
|
return;
|
|
6713
7177
|
}
|
|
6714
|
-
if (cmd
|
|
7178
|
+
if (cmd.startsWith("!")) {
|
|
7179
|
+
this.executeShellFilter(rangeStr || "%", cmd.slice(1).trim());
|
|
6715
7180
|
return;
|
|
6716
7181
|
}
|
|
6717
|
-
if (cmd === "
|
|
7182
|
+
if (cmd === "d" || cmd === "delete") {
|
|
7183
|
+
const lines = this.state.buffer.content.split("\n");
|
|
7184
|
+
const currentLine2 = this.state.cursor.line;
|
|
7185
|
+
const [startLine, endLine] = this.resolveRange(rangeStr, lines, currentLine2) ?? [currentLine2, currentLine2];
|
|
7186
|
+
lines.splice(startLine, endLine - startLine + 1);
|
|
7187
|
+
this.state = {
|
|
7188
|
+
...this.state,
|
|
7189
|
+
buffer: new Buffer(lines.join("\n")),
|
|
7190
|
+
cursor: new Cursor(Math.max(0, Math.min(startLine, lines.length - 1)), 0),
|
|
7191
|
+
desiredColumn: null
|
|
7192
|
+
};
|
|
6718
7193
|
return;
|
|
6719
7194
|
}
|
|
6720
|
-
|
|
7195
|
+
if (cmd === "q" || cmd === "quit") return;
|
|
7196
|
+
if (cmd === "w" || cmd === "write") return;
|
|
7197
|
+
if (cmd === "wq" || cmd === "x") return;
|
|
7198
|
+
const lineNavStr = cmd !== "" ? cmd : rangeStr;
|
|
7199
|
+
const lineMatch = lineNavStr.match(/^(\d+)$/);
|
|
6721
7200
|
if (lineMatch) {
|
|
6722
7201
|
const targetLine = parseInt(lineMatch[1], 10) - 1;
|
|
6723
7202
|
const lines = this.state.buffer.content.split("\n");
|