はじめに
テキスト編集で「複数の文字列を一度に置換したい」と思ったことはありませんか?
例えば、こんなシーン:
- 変数名を複数箇所で一括変更
- テーブル定義のデータ型を複数列で一括更新
- ドキュメント内の用語統一を複数ワードで一括実施
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)
- パターンマッチングが可能
- 後方参照・グループ化対応
- 高度な置換に対応
終了位置の指定
デフォルトは「EOF」(ファイル末尾まで)ですが、以下も指定可能:
- 文字列指定:その文字列を含む行の1行前まで
- EOF:ファイル末尾まで
- ---:区切り線まで
- ▲:三角マークまで
実務での活用例
例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管理による履歴保持
- 正規表現モードの追加
TypeScript実装の詳細
コード全体
import * as vscode from 'vscode';
/**
* 終了位置のデフォルト値を決定するタイプ
*/
enum EndMarkerType {
EOF = 1, // "EOF" (ファイル末尾)
Separator = 2, // "---" (区切り線)
Triangle = 3 // "▲" (三角マーク)
}
/**
* 置換モード
*/
enum ReplaceMode {
Literal, // リテラル一致
Regex // 正規表現
}
/**
* 選択行の「置換前\t置換後」ペアを使って、現在行から指定位置まで置換(リテラル)
*/
export async function replaceBySelectionLiteral() {
await replaceBySelection(ReplaceMode.Literal, EndMarkerType.EOF);
}
/**
* 選択行の「置換前\t置換後」ペアを使って、現在行から指定位置まで置換(正規表現)
*/
export async function replaceBySelectionRegex() {
await replaceBySelection(ReplaceMode.Regex, EndMarkerType.EOF);
}
/**
* メイン処理:選択行から置換ペアを抽出し、指定範囲を置換
*/
async function replaceBySelection(mode: ReplaceMode, defaultMarkerType: EndMarkerType) {
const editor = vscode.window.activeTextEditor;
if (!editor) {
vscode.window.showWarningMessage('アクティブなエディタがありません');
return;
}
const document = editor.document;
const selection = editor.selection;
// 選択範囲がない場合はエラー
if (selection.isEmpty) {
vscode.window.showWarningMessage('置換指示行を選択してください');
return;
}
// 選択範囲の開始行・終了行を取得(0-based)
const selectionStartLine = selection.start.line;
const selectionEndLine = selection.end.line;
// 置換開始行 = 選択範囲の次の行
const replaceStartLine = selectionEndLine + 1;
// 選択範囲の下に置換対象行がない場合はエラー
if (replaceStartLine >= document.lineCount) {
vscode.window.showInformationMessage('置換対象行がありません');
return;
}
// 選択行から置換ペアを抽出
const pairs = collectReplacePairs(document, selectionStartLine, selectionEndLine);
if (pairs.length === 0) {
vscode.window.showWarningMessage('タブ区切りの置換指定行がありません');
return;
}
// 終了位置を決定
const endLine = await promptForEndLine(document, replaceStartLine, defaultMarkerType);
if (endLine === null) {
vscode.window.showInformationMessage('操作がキャンセルされました');
return;
}
if (endLine <= replaceStartLine) {
vscode.window.showInformationMessage('終了位置が開始行と同じか手前のため、置換は行いません');
return;
}
// 確認ダイアログ
const modeText = mode === ReplaceMode.Literal ? 'リテラル' : '正規表現';
const answer = await vscode.window.showInformationMessage(
`${pairs.length}行のタブ区切り置換指定(${modeText})で\n` +
`「${replaceStartLine + 1}行目 ~ ${endLine}行目」を置換します。\n` +
`よろしいですか?`,
{ modal: true },
'はい', 'いいえ'
);
if (answer !== 'はい') {
return;
}
// 置換実行
await applyReplacements(editor, pairs, replaceStartLine, endLine, mode);
vscode.window.showInformationMessage(
`置換完了: ${pairs.length}種類の文字列を置換しました`
);
}
/**
* 選択範囲から「置換前\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 promptForEndLine(
document: vscode.TextDocument,
startLine: number,
markerType: EndMarkerType
): Promise {
const lastLine = document.lineCount - 1;
// デフォルト値を決定
let defaultValue: string;
switch (markerType) {
case EndMarkerType.EOF:
defaultValue = 'EOF';
break;
case EndMarkerType.Separator:
defaultValue = '---';
break;
case EndMarkerType.Triangle:
defaultValue = '▲';
break;
default:
defaultValue = 'EOF';
}
const input = await vscode.window.showInputBox({
prompt: '終了位置を指定してください:',
placeHolder: '文字列を指定: その文字列を含む行の1行上まで / 「EOF」: ファイル末尾まで / キャンセル: 処理中止',
value: defaultValue
});
if (!input) {
return null; // キャンセル
}
// EOFの場合
if (input.toUpperCase() === 'EOF') {
return lastLine + 1; // 最終行の次(全体を対象)
}
// 文字列検索(該当行の1行前まで)
for (let i = startLine + 1; i <= lastLine; i++) {
const lineText = document.lineAt(i).text;
if (lineText.includes(input)) {
return i; // 該当行の番号を返す(1行前まで置換)
}
}
// 見つからない場合はEOF
vscode.window.showInformationMessage(
`「${input}」が見つからないため、ファイル末尾まで置換します`
);
return lastLine + 1;
}
/**
* 実際に置換を適用
*/
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 (const [before, after] of pairs) {
if (!before || before === after) {
continue; // 空文字列や同じ文字列はスキップ
}
// 対象範囲を行ごとに処理
for (let lineNum = startLine; lineNum <= actualEndLine; lineNum++) {
const line = document.lineAt(lineNum);
const lineText = line.text;
let newText: string;
if (mode === ReplaceMode.Literal) {
// リテラル置換
newText = lineText.split(before).join(after);
} else {
// 正規表現置換
try {
const regex = new RegExp(before, 'g');
newText = lineText.replace(regex, after);
} catch (e) {
// 正規表現エラーの場合はスキップ
console.error(`正規表現エラー: ${before}`, e);
continue;
}
}
// 変更があった場合のみ置換
if (newText !== lineText) {
const range = new vscode.Range(
new vscode.Position(lineNum, 0),
new vscode.Position(lineNum, lineText.length)
);
editBuilder.replace(range, newText);
}
}
}
});
} コードのポイント
1. 選択範囲の活用
const selection = editor.selection;
const selectionStartLine = selection.start.line;
const selectionEndLine = selection.end.line;
const replaceStartLine = selectionEndLine + 1;選択範囲を置換指示として使い、その次の行から置換を開始。
2. タブ区切りのパース
const tabIndex = lineText.indexOf('\t');
const before = lineText.substring(0, tabIndex);
const after = lineText.substring(tabIndex + 1);「置換前[Tab]置換後」形式を確実に解析。
3. モード分岐
if (mode === ReplaceMode.Literal) {
newText = lineText.split(before).join(after);
} else {
const regex = new RegExp(before, 'g');
newText = lineText.replace(regex, after);
}リテラルと正規表現を明確に分離。
4. 安全な正規表現処理
try {
const regex = new RegExp(before, 'g');
newText = lineText.replace(regex, after);
} catch (e) {
console.error(`正規表現エラー: ${before}`, e);
continue;
}不正な正規表現でもスキップして続行。
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:終了位置を指定
デフォルトの「EOF」のまま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)を確認
- 正規表現の構文を見直してください
他のマクロとの組み合わせ
選択範囲コピーマクロとの連携
- 置換ペアをテンプレート化
- 空行までコピーマクロで複製
- 値だけ書き換えて実行
Git連携
# 置換前
git diff
# 置換実行
Ctrl+Alt+T
# 結果確認
git diff
# 問題なければコミット
git commit -am "Refactor: update variable names"まとめ
VSCodeで複数の文字列を一括置換する機能、実装してみました。
このマクロの強み:
- シンプルな操作(選択して実行)
- タブ区切りの直感的な書式
- リテラルと正規表現の両対応
- 範囲指定の柔軟性
- 確認ダイアログで安全
こんな人におすすめ:
- テキスト編集の効率化したい
- リファクタリングが多い
- ドキュメント整備をよくする
- サクラエディタから移行したい
サクラエディタで10年以上愛用してきた機能が、TypeScriptで現代的に蘇りました。
ぜひVSCodeでの快適なテキスト編集にお役立てください!
関連記事
- サクラエディタからの移行経緯

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

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

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