はじめに
テキスト編集で「複数の文字列を一度に置換したい」と思ったことはありませんか?
例えば、こんなシーン:
- 変数名を複数箇所で一括変更
- テーブル定義のデータ型を複数列で一括更新
- ドキュメント内の用語統一を複数ワードで一括実施
VSCodeの標準機能では「1つずつ置換」が限界。複数の置換ペアを一度に処理するには、拡張機能や外部ツールが必要でした。
そこで、サクラエディタで10年以上愛用してきた「選択行一括置換マクロ」をTypeScriptで再実装しました。
この記事で分かること
- 選択行一括置換マクロの機能と使い方
- リテラル置換と正規表現置換の使い分け
- TypeScript実装の詳細コード
- 実務での活用例
- トラブルシューティング
機能の概要
できること
選択した行から置換ペアを読み取り、指定範囲を一括置換
置換指示行(選択する):
19: aaa 111
20: bbb 222
21: ccc 333
↓ Ctrl+Alt+T で実行
置換対象範囲(21行目以降):
22: テスト aaa です
23: bbb もあります
24: ccc も置換されます
↓ 結果
22: テスト 111 です
23: 222 もあります
24: 333 も置換されます2つの置換モード
リテラル置換(Ctrl+Alt+T)
- 文字列をそのまま検索・置換
- 特殊文字もそのまま扱う
- 安全で確実
正規表現置換(Ctrl+Shift+Alt+T)
- パターンマッチングが可能
- 後方参照・グループ化対応
- 高度な置換に対応
終了位置の指定(改良版)
実行時にQuickPickダイアログが開き、以下の方法で終了位置を指定できます:
プリセット選択(↑↓キーで選択)
- 空行: 次の空行の1つ前まで
- EOF: ファイル末尾まで(デフォルト)
- ▲: 「▲」を含む行の1つ前まで
- ▲▲▲: 「▲▲▲」を含む行の1つ前まで
- ```: コードブロック終端の1つ前まで
- ---: 区切り線の1つ前まで
直接入力も可能
プリセットを選ばずに、そのまま文字列を入力してEnterでもOK。例えば「TODO」と入力すれば、TODOを含む行の1つ前まで置換されます。
実務での活用例
例1:SQL文のテーブル名一括変更
置換指示行:
old_users new_users
old_posts new_posts
old_comments new_comments置換対象:
SELECT * FROM old_users WHERE ...;
UPDATE old_posts SET ...;
DELETE FROM old_comments WHERE ...;結果:
SELECT * FROM new_users WHERE ...;
UPDATE new_posts SET ...;
DELETE FROM new_comments WHERE ...;例2:Markdown用語統一
置換指示行:
サーバー サーバ
データベース DB
ブラウザー ブラウザ1行に1つずつ置換ペアを定義するだけで、ドキュメント全体を一括修正!
例3:変数名リファクタリング(正規表現)
置換指示行:
user_(\w+) customer_$1
old_(\w+)_id new_$1_id置換対象:
const user_name = "John";
const user_age = 30;
const old_order_id = 123;結果:
const customer_name = "John";
const customer_age = 30;
const new_order_id = 123;正規表現の後方参照も完全対応!
例4:設定ファイルの値一括変更
置換指示行:
localhost production.example.com
:8080 :443
http:// https://開発環境から本番環境への設定変更が一瞬で完了。
例5:HTMLタグの一括変更
置換指示行:
<div <section
</div> </section>
<span <strong
</span> </strong>セマンティックHTMLへのリファクタリングが簡単に。
サクラエディタからの移植経緯
サクラエディタ時代の実績
この機能、実は10年以上前からサクラエディタで愛用していた自作マクロです。
元記事:【サクラエディタ】1ファイル内一括置換マクロ【画像サンプルあり】
当時の課題:
- VSCodeとサクラエディタを行き来するのが面倒
- Git統合やリモート開発ではVSCodeが必須
- でも一括置換はサクラエディタでないとできない
VSCode移植の決断
VSCodeで開発する機会が増えたため、「どうせなら全部VSCodeで完結させたい」と思い、TypeScriptで再実装を決意。
移植のメリット:
- エディタ統一による効率化
- TypeScriptの型安全性
- Git管理による履歴保持
- 正規表現モードの追加
- QuickPickによる柔軟な入力
TypeScript実装の詳細
終了位置選択の実装(重要な改良点)
従来のshowInputBoxからcreateQuickPickに変更したことで、プリセット選択と直接入力の両方に対応しました。
/**
* プリセット検索パターン
*/
const PRESET_PATTERNS = [
{ label: '空行', value: 'EMPTY_LINE', description: '次の空行まで' },
{ label: 'EOF', value: 'EOF', description: 'ファイル末尾まで' },
{ label: '▲', value: '▲', description: '▲が含まれる行の前まで' },
{ label: '▲▲▲', value: '▲▲▲', description: '▲▲▲が含まれる行の前まで' },
{ label: '```', value: '```', description: 'コードブロック終端の前まで' },
{ label: '---', value: '---', description: '区切り線の前まで' }
];
/**
* 終了位置の選択ダイアログ(直接入力も可能)
*/
async function promptForEndLine(
document: vscode.TextDocument,
startLine: number
): Promise {
const quickPick = vscode.window.createQuickPick();
quickPick.items = PRESET_PATTERNS;
quickPick.placeholder = '終了位置を選択または直接入力してください';
quickPick.ignoreFocusOut = true;
return new Promise((resolve) => {
quickPick.onDidAccept(async () => {
const selected = quickPick.selectedItems[0];
const inputValue = quickPick.value.trim();
let pattern: string | null = null;
// 選択肢を選んだ場合
if (selected) {
const presetPattern = PRESET_PATTERNS.find(p => p.label === selected.label);
pattern = presetPattern?.value || null;
}
// 直接入力した場合
else if (inputValue) {
pattern = inputValue;
}
quickPick.hide();
if (!pattern) {
resolve(null);
return;
}
// 終了行を検索
const endLine = await findEndLine(document, startLine, pattern);
resolve(endLine);
});
quickPick.onDidHide(() => {
resolve(null);
quickPick.dispose();
});
quickPick.show();
});
}
/**
* 終了行を検索
*/
async function findEndLine(
document: vscode.TextDocument,
startLine: number,
pattern: string
): Promise {
const lastLine = document.lineCount - 1;
// EOFの場合
if (pattern === 'EOF') {
return lastLine + 1; // 最終行の次(全体を対象)
}
// 空行の場合
if (pattern === 'EMPTY_LINE') {
for (let i = startLine + 1; i <= lastLine; i++) {
const line = document.lineAt(i);
if (line.isEmptyOrWhitespace) {
return i; // 空行の番号を返す(1行前まで置換)
}
}
// 空行が見つからない場合は最終行
vscode.window.showInformationMessage('空行が見つからないため、ファイル末尾まで置換します');
return lastLine + 1;
}
// 文字列検索(該当行の1行前まで)
for (let i = startLine + 1; i <= lastLine; i++) {
const lineText = document.lineAt(i).text;
if (lineText.includes(pattern)) {
return i; // 該当行の番号を返す(1行前まで置換)
}
}
// 見つからない場合はEOF
vscode.window.showInformationMessage(
`「${pattern}」が見つからないため、ファイル末尾まで置換します`
);
return lastLine + 1;
} その他の主要コード
置換ペアの抽出や実際の置換処理は従来通りです:
/**
* 選択範囲から「置換前\t置換後」ペアを抽出
*/
function collectReplacePairs(
document: vscode.TextDocument,
startLine: number,
endLine: number
): Array<[string, string]> {
const pairs: Array<[string, string]> = [];
for (let i = startLine; i <= endLine; i++) {
const lineText = document.lineAt(i).text;
const tabIndex = lineText.indexOf('\t');
if (tabIndex < 0) {
continue; // タブがない行は無視
}
const before = lineText.substring(0, tabIndex);
const after = lineText.substring(tabIndex + 1);
if (!before) {
continue; // 置換前が空の行は無視
}
pairs.push([before, after]);
}
return pairs;
}
/**
* 実際に置換を適用(修正版:行ごとに全ペアを適用)
*/
async function applyReplacements(
editor: vscode.TextEditor,
pairs: Array<[string, string]>,
startLine: number,
endLine: number,
mode: ReplaceMode
) {
const document = editor.document;
const lastLine = document.lineCount - 1;
const actualEndLine = Math.min(endLine - 1, lastLine);
await editor.edit(editBuilder => {
// 行ごとにループ(オーバーラップを防ぐため)
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 {
// 正規表現置換
try {
const regex = new RegExp(before, 'g');
lineText = lineText.replace(regex, after);
} catch (e) {
// 正規表現エラーの場合はスキップ
console.error(`正規表現エラー: ${before}`, e);
}
}
}
// 変更があった場合のみ置換(1行につき1回のreplaceでオーバーラップを回避)
if (lineText !== originalText) {
const range = new vscode.Range(
new vscode.Position(lineNum, 0),
new vscode.Position(lineNum, originalText.length)
);
editBuilder.replace(range, lineText);
}
}
});
}コードのポイント
1. QuickPickによる柔軟な入力
const quickPick = vscode.window.createQuickPick();
quickPick.items = PRESET_PATTERNS;
// プリセット選択 or 直接入力が可能2. プリセットと直接入力の両対応
if (selected) {
// プリセット選択
pattern = presetPattern?.value || null;
} else if (inputValue) {
// 直接入力
pattern = inputValue;
}3. タブ区切りのパース
const tabIndex = lineText.indexOf('\t');
const before = lineText.substring(0, tabIndex);
const after = lineText.substring(tabIndex + 1);「置換前[Tab]置換後」形式を確実に解析。
4. オーバーラップエラーの回避(重要)
// 行ごとにループ(オーバーラップを防ぐため)
for (let lineNum = startLine; lineNum <= actualEndLine; lineNum++) {
let lineText = line.text;
// 全ペアを順番に適用
for (const [before, after] of pairs) {
lineText = lineText.split(before).join(after);
}
// 変更があった場合のみ1回だけreplace
if (lineText !== originalText) {
editBuilder.replace(range, lineText);
}
}同じ行に複数の置換対象がある場合でも、行ごとに全ペアを適用してから1回だけreplaceすることでオーバーラップエラーを回避。
package.jsonの設定
{
"contributes": {
"commands": [
{
"command": "myMacros.replaceBySelectionLiteral",
"title": "Replace by Selection (Literal)",
"category": "My Macros"
},
{
"command": "myMacros.replaceBySelectionRegex",
"title": "Replace by Selection (Regex)",
"category": "My Macros"
}
],
"keybindings": [
{
"command": "myMacros.replaceBySelectionLiteral",
"key": "ctrl+alt+t",
"when": "editorTextFocus"
},
{
"command": "myMacros.replaceBySelectionRegex",
"key": "ctrl+shift+alt+t",
"when": "editorTextFocus"
}
]
}
}使い方
基本的な使い方
ステップ1:置換ペアを記述
old_name new_name
old_value new_value
old_key new_keyステップ2:選択する
置換ペアの行を選択(Shift+↓で複数行選択)
ステップ3:実行
- リテラル置換:
Ctrl+Alt+T - 正規表現置換:
Ctrl+Shift+Alt+T
ステップ4:終了位置を指定
QuickPickダイアログが開きます:
- ↑↓でプリセットを選択してEnter
- または直接文字列を入力してEnter
ステップ5:確認
置換内容を確認して「はい」をクリック
応用テクニック
範囲を限定する:
置換指示行(選択)
↓
置換対象範囲
↓
--- (ここで終了)
↓
置換しない範囲終了位置に「---」を選択またはダイアログで入力すれば、区切り線までのみ置換。
正規表現の後方参照:
(\w+)_old $1_new
old_(\d+) new_$1キャプチャグループを活用した高度な置換が可能。
トラブルシューティング
Q: 「置換指示行を選択してください」と表示される
A: 置換ペアの行を選択していません
- 置換ペアを記述した行を選択してから実行してください
- 選択範囲が空の場合はこのエラーが出ます
Q: 「タブ区切りの置換指定行がありません」と表示される
A: タブ文字が入っていません
- 「置換前[Tab]置換後」の形式で記述してください
- スペースではなく、タブ文字(\t)を使ってください
Q: 正規表現が動かない
A: 正規表現モードで実行していますか?
- リテラル:
Ctrl+Alt+T - 正規表現:
Ctrl+Shift+Alt+T - モードを確認してください
Q: 一部の置換がスキップされる
A: 正規表現エラーの可能性
- コンソールログ(Ctrl+Shift+U → Extension Host)を確認
- 正規表現の構文を見直してください
Q: 「Overlapping ranges are not allowed!」エラーが出る
A: 修正済みです
- 最新版では、同じ行に複数の置換対象がある場合でも正常に動作します
git pullして最新版を取得してください
他のマクロとの組み合わせ
選択範囲コピーマクロとの連携
- 置換ペアをテンプレート化
- 空行までコピーマクロで複製
- 値だけ書き換えて実行
Git連携
# 置換前
git diff
# 置換実行
Ctrl+Alt+T
# 結果確認
git diff
# 問題なければコミット
git commit -am "Refactor: update variable names"まとめ
VSCodeで複数の文字列を一括置換する機能、実装してみました。
このマクロの強み:
- シンプルな操作(選択して実行)
- タブ区切りの直感的な書式
- リテラルと正規表現の両対応
- QuickPickによる柔軟な終了位置指定
- プリセット選択と直接入力の両対応
- 確認ダイアログで安全
- オーバーラップエラーを回避
こんな人におすすめ:
- テキスト編集の効率化したい
- リファクタリングが多い
- ドキュメント整備をよくする
- サクラエディタから移行したい
サクラエディタで10年以上愛用してきた機能が、TypeScriptで現代的に蘇りました。
ぜひVSCodeでの快適なテキスト編集にお役立てください!
関連記事
- サクラエディタからの移行経緯

- TypeScriptマクロ開発環境の構築方法

- 空行までの一括選択・コピーマクロ

タグ: #VSCode #TypeScript #マクロ #テキスト編集 #正規表現 #生産性向上 #サクラエディタ
コメント