vim-sim 1.0.5 → 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 +473 -8
- 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() {
|
|
@@ -5525,6 +5532,152 @@ var ParagraphTextObject = class extends TextObject {
|
|
|
5525
5532
|
}
|
|
5526
5533
|
};
|
|
5527
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
|
+
|
|
5528
5681
|
// src/commands/misc.ts
|
|
5529
5682
|
var JumpForward = class extends Command {
|
|
5530
5683
|
key = "<C-i>";
|
|
@@ -6534,6 +6687,10 @@ var Session = class _Session {
|
|
|
6534
6687
|
}
|
|
6535
6688
|
handleKey(key) {
|
|
6536
6689
|
key = _Session.normalizeKey(key);
|
|
6690
|
+
if (this.state.pendingConfirm !== null) {
|
|
6691
|
+
this.handleConfirmMode(key);
|
|
6692
|
+
return;
|
|
6693
|
+
}
|
|
6537
6694
|
if (this.state.mode === 1 /* INSERT */) {
|
|
6538
6695
|
this.handleInsertMode(key);
|
|
6539
6696
|
return;
|
|
@@ -6565,6 +6722,7 @@ var Session = class _Session {
|
|
|
6565
6722
|
const stateBefore = this.state;
|
|
6566
6723
|
result.context.registerManager = this.registerManager;
|
|
6567
6724
|
result.context.indentationManager = this.indentationManager;
|
|
6725
|
+
result.context.lastSubstitute = this.state.lastSubstitute;
|
|
6568
6726
|
this.state = result.command.execute(this.state, result.context);
|
|
6569
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
|
|
6570
6728
|
stateBefore.visualAnchor && (this.state.cursor.line !== stateBefore.cursor.line || this.state.cursor.column !== stateBefore.cursor.column)) {
|
|
@@ -6714,24 +6872,331 @@ var Session = class _Session {
|
|
|
6714
6872
|
};
|
|
6715
6873
|
}
|
|
6716
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 ─────────────────────────────────────────────
|
|
6717
7150
|
executeCommand(command) {
|
|
6718
|
-
const
|
|
7151
|
+
const raw = command.trim();
|
|
7152
|
+
const [rangeStr, cmd] = this.splitRangeAndCommand(raw);
|
|
6719
7153
|
const setMatch = cmd.match(/^set\s+(.+)$/);
|
|
6720
7154
|
if (setMatch) {
|
|
6721
|
-
|
|
6722
|
-
this.configManager.parseSetCommand(setCommand);
|
|
7155
|
+
this.configManager.parseSetCommand(setMatch[1].trim());
|
|
6723
7156
|
return;
|
|
6724
7157
|
}
|
|
6725
|
-
if (cmd
|
|
7158
|
+
if (/^s[^a-zA-Z]/.test(cmd)) {
|
|
7159
|
+
this.executeSubstitute(rangeStr, cmd);
|
|
6726
7160
|
return;
|
|
6727
7161
|
}
|
|
6728
|
-
|
|
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);
|
|
6729
7166
|
return;
|
|
6730
7167
|
}
|
|
6731
|
-
|
|
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);
|
|
7176
|
+
return;
|
|
7177
|
+
}
|
|
7178
|
+
if (cmd.startsWith("!")) {
|
|
7179
|
+
this.executeShellFilter(rangeStr || "%", cmd.slice(1).trim());
|
|
7180
|
+
return;
|
|
7181
|
+
}
|
|
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
|
+
};
|
|
6732
7193
|
return;
|
|
6733
7194
|
}
|
|
6734
|
-
|
|
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+)$/);
|
|
6735
7200
|
if (lineMatch) {
|
|
6736
7201
|
const targetLine = parseInt(lineMatch[1], 10) - 1;
|
|
6737
7202
|
const lines = this.state.buffer.content.split("\n");
|