背景
「選択した行から置換ペアを読み取り、指定範囲を一括置換する」VSCodeマクロを作る際、同じ行に複数の置換対象が含まれるケースでOverlapping ranges are not allowed!という実行時エラーに遭遇しました。原因と対処を整理します。
1. 素朴な実装が失敗するパターン
// NG: 置換ペアごとに個別のeditBuilder.replaceを呼ぶ
for (const [before, after] of pairs) {
const range = findRange(lineText, before);
editBuilder.replace(range, after);
}
editor.edit()のコールバック内で複数回editBuilder.replace()を呼ぶ場合、それぞれのRangeがドキュメント上で重なっているとOverlapping ranges are not allowed!エラーになります。同じ行に複数の置換対象があると、文字位置がずれて範囲が重複しやすくなります。
2. 解決策:行ごとに全ペアを文字列レベルで適用してから1回だけreplace
for (let lineNum = startLine; lineNum <= actualEndLine; lineNum++) {
const line = document.lineAt(lineNum);
let lineText = line.text;
const originalText = lineText;
for (const [before, after] of pairs) {
if (!before || before === after) continue;
if (mode === ReplaceMode.Literal) {
lineText = lineText.split(before).join(after);
} else {
const regex = new RegExp(before, 'g');
lineText = lineText.replace(regex, after);
}
}
if (lineText !== originalText) {
const range = new vscode.Range(
new vscode.Position(lineNum, 0),
new vscode.Position(lineNum, originalText.length)
);
editBuilder.replace(range, lineText);
}
}
VSCode APIのRangeを都度計算するのではなく、まずJavaScriptの文字列操作だけで全置換ペアを行テキストに適用し、最終的な結果と元のテキストが変わっていた場合にだけ「行全体」を1回のeditBuilder.replace()で置き換えます。Range同士の重複という問題自体を、VSCode APIに触れる前の文字列処理の段階で解消しているのがポイントです。
3. リテラル置換はsplit().join()、正規表現はreplace(regex, 'g')
if (mode === ReplaceMode.Literal) {
lineText = lineText.split(before).join(after);
} else {
const regex = new RegExp(before, 'g');
lineText = lineText.replace(regex, after);
}
リテラル置換でreplace()を使わずsplit().join()にしているのは、beforeに正規表現の特殊文字(.や(など)が含まれていても、意図せず正規表現として解釈されるのを防ぐためです。正規表現モードでは明示的にnew RegExp()を生成し、gフラグで行内の全マッチを対象にします。
4. before === afterや空文字列のペアは早期スキップ
if (!before || before === after) {
continue;
}
置換前後が同じ値のペアや、置換前が空文字列のペア(タブの位置がおかしい行など)を事前にスキップすることで、無意味な処理と予期しない全文字挿入(空文字列に対するsplit().join()は1文字ごとに挿入されてしまう)を防いでいます。
まとめ
複数の置換を1つのeditor.edit()呼び出しで安全に行うコツは、VSCodeのRangeを意識する前に、まず行テキストを純粋な文字列操作で完成させてしまうことでした。QuickPickによる終了位置指定の仕組みは、姉妹記事の「任意行までの一括選択・コピー」マクロと共通の実装です。タブ区切りペアの抽出を含む完全なコードは元記事にまとめています。