はじめに:引用符や括弧内のテキスト選択、面倒じゃないですか?
VSCodeで文字列編集をしていて、こんな経験はありませんか?
const message = "このテキストだけコピーしたい";
const path = '/home/user/documents/report.pdf';
const config = { name: "設定値", value: 123 };ダブルクォーテーション内の文字列だけコピーしたいときに、以下のような操作が必要です:
- マウスで開始位置をクリック
- ドラッグして終了位置まで選択
- Ctrl+Cでコピー
あるいは:
- Ctrl+Shift+→を何度も押して単語単位で選択
- 微調整のためにShift+→/←
- Ctrl+Cでコピー
特に長い文字列や入れ子構造の場合、この作業は非常に煩雑です。
標準のVSCodeには「Ctrl+Shift+→」「Alt+Shift+→」などの単語単位選択機能がありますが、デリミタ(区切り文字)を意識した選択には対応していません。引用符や括弧の中身だけを1キー操作で選択・コピーできたら便利だと思いませんか?
そこで、カーソル位置から自動でデリミタを検索し、その間のテキストを1キーで選択・コピーするTypeScriptマクロを開発しました。
この記事で分かること
- デリミタ間テキスト選択コピーの機能と使い方
- 対応するデリミタの種類(18種類)
- コピーせず選択のみを行う
Ctrl+Alt+Sの活用法 - 選択方向による動作の違い(右方向/左方向)
- TypeScript実装の詳細コード
- 実務での活用例とパフォーマンス
実装した機能の概要
できること
2つのショートカットキーで使い分けられます:
Ctrl+Alt+D: デリミタ間を選択してクリップボードにコピーCtrl+Alt+S: デリミタ間を選択のみ(クリップボードは変更しない)
Ctrl+Alt+S は「コピー元でCtrl+Alt+D → 置換先でCtrl+Alt+S → Ctrl+V」というコピー&貼り付け置換の用途を想定しています。
Ctrl+Alt+D を押すだけで、カーソル位置の最も近いデリミタ間のテキストを自動選択・コピー
例:<"「あいうえお」、【かきくけこ】、{さしすせそ}">
↑ここにカーソル
Ctrl+Alt+D を押す
→「あいうえお」だけが選択・コピーされる(引用符は除外)対応デリミタ(18種類)
クォーテーション・スラッシュ系
"~"(ダブルクォート)'~'(シングルクォート)`~`(バッククォート)/~/(スラッシュ・半角)/~/(スラッシュ・全角)\~\(バックスラッシュ・半角)¥~¥(円記号・全角)|~|(パイプ・半角)|~|(パイプ・全角)
括弧系(半角・全角)
(~)/(~)(丸括弧)[~](角括弧){~}/{~}(波括弧)<~>/<~>(山括弧)【~】(墨付き括弧)「~」(鉤括弧)
3つの動作モード
1. 通常モード(カーソル位置から左に検索)
<"「あいうえお」、【かきくけこ】">
↑カーソル
→ カーソルの左にある最も近い開始デリミタ「"」を検出
→ 対応する終了デリミタ「"」を右に検索
→ 「あいうえお」、【かきくけこ】 が選択される2. 右方向モード(1文字選択 + カーソルが右側)
<"「あいうえお」、【かきくけこ】">
↑選択 ↑カーソル(左→右に選択)
→ 選択文字「"」が開始デリミタか判定
→ 右に終了デリミタ「"」を検索
→ 「あいうえお」、【かきくけこ】 が選択される3. 左方向モード(1文字選択 + カーソルが左側)
<"「あいうえお」、【かきくけこ】">
↑カーソル ↑選択(右→左に選択)
→ 選択文字「"」が終了デリミタか判定
→ 左に開始デリミタ「"」を検索
→ 「あいうえお」、【かきくけこ】 が選択されるフォールバック機能
デリミタが見つからない場合は、行全体をコピー(Ctrl+Alt+D)または行全体を選択(Ctrl+Alt+S)します。
具体的な使用場面とメリット
場面1:コード内の文字列リテラルを抽出
const errorMessage = "接続に失敗しました。しばらく経ってから再度お試しください。";文字列の中にカーソルを置いてCtrl+Alt+D → 文字列だけが即座にコピーされる。
従来の方法:
"の直後をクリック- Shift+Endで行末まで選択
- Shift+←で
"の手前まで調整 - Ctrl+C
このマクロ:
- 文字列内にカーソルを置く
Ctrl+Alt+D
たった1操作で完了!
場面2:ファイルパスを素早く抽出
const configPath = '/etc/nginx/sites-available/default';
const dataPath = '/var/www/html/data/uploads/2026/02/';パス内にカーソルを置いてCtrl+Alt+D → パス全体がコピーされる。
場面3:JSONのキーや値を抽出
{
"database": {
"host": "localhost",
"port": 5432,
"name": "production_db"
}
}各値にカーソルを置いてCtrl+Alt+D → 値だけが抽出される。
場面4:正規表現パターンを抽出
const pattern = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;パターン内にカーソルを置いてCtrl+Alt+D → 正規表現本体だけがコピーされる(/~/を除外)。
場面5:複雑な入れ子構造から特定部分を抽出
異なる種類のデリミタの入れ子
<"「あいうえお」、【かきくけこ】、{さしすせそ}">この複雑な構造でも:
「の後にカーソル →あいうえおが選択される【の後にカーソル →かきくけこが選択される<の後にカーソル → 最外側の"~"が選択される
最も近いデリミタを自動判別するため、入れ子構造でも正確に動作します。
同じ種類のデリミタのネスト構造
function outer(a, inner(b, c), d)この場合、カーソルがouterにあるとき:
- 左で最も近い
((outerの後)を検出 - ネストレベルを考慮して対応する
)を検索 - 内側の
inner(b, c)のネストをカウント a, inner(b, c), d全体が正確に選択される ✅
従来バージョンでは誤ってouter(a, inner(b, cが選択されていましたが、ネスト対応により正確に動作するようになりました。
場面6:Markdown内のコード抜き出し
インラインコードは`console.log('Hello')`のように書きます。バッククォート内にカーソルを置いてCtrl+Alt+D → console.log('Hello')だけがコピーされる。
場面7:コピー&貼り付け置換(Ctrl+Alt+Sの活用)
Ctrl+Alt+S(選択のみ)との組み合わせで、貼り付けによる置換が手軽にできます。
const oldValue = "置換前の文字列";
const newValue = "置換後の文字列";手順:
"置換前の文字列"内にカーソル →Ctrl+Alt+Dでコピー"置換後の文字列"内にカーソルを移動Ctrl+Alt+Sで置換対象を選択Ctrl+Vで貼り付け → 置換完了!
クリップボードを変更せずに置換先だけを選択できるので、途中でクリップボードが上書きされる心配がありません。
メリットまとめ
- マウス操作不要: カーソル位置だけで判定
- 高速: キーボード1回で完結
- 正確: デリミタを自動検出
- 柔軟: 選択方向で動作を変更可能、コピー/選択を使い分け可能
- 安全: デリミタが見つからなくても行全体をコピー/選択
改修履歴:ネスト構造への対応
改修前の問題点
初期バージョンでは、ネスト(入れ子)構造の括弧を正しく処理できない問題がありました。
問題の例:
function(b,c){...}この場合、カーソルがfunctionにあるとき:
- 左から最も近い開始デリミタ
(を検出 - その位置から右に最初に見つかった終了デリミタ
)をマッチング - しかし、最初の
)はb,c)の閉じ括弧なので、function(b,cが誤って選択される ❌
ネストレベルを考慮していないため、入れ子構造で誤動作していました。
改修内容:括弧のバランスを考慮したマッチング
VSCodeのCtrl+Shift+\(対応する括弧へジャンプ)と同じように、ネストレベルをカウントしながら対応する括弧を探すアルゴリズムを実装しました。
アルゴリズム:
- カーソル位置から左に開始デリミタを検索
- その位置から右に向かって:
- 同じ開始デリミタが出現 → カウンタ +1(ネストが深くなる)
- 対応する終了デリミタが出現 → カウンタ -1(ネストから抜ける)
- カウンタが 0 になった位置が対応する終了デリミタ ✅
追加した関数:
findMatchingEndDelimiter()- 順方向のネスト対応マッチングfindMatchingStartDelimiter()- 逆方向のネスト対応マッチング
これにより、複雑な入れ子構造でも正確にデリミタのペアを検出できるようになりました。
TypeScript実装の詳細
実際のコードを紹介します。copyTextBetweenDelimiters.tsファイルに実装しました。
import * as vscode from 'vscode';
/**
* デリミタペアの定義(開始文字と終了文字)
*/
const DELIMITER_PAIRS: Array<{ start: string; end: string }> = [
{ start: '"', end: '"' },
{ start: "'", end: "'" },
{ start: '`', end: '`' }, // バッククォート(テンプレートリテラル・Markdownインラインコード)
{ start: '/', end: '/' },
{ start: '/', end: '/' },
{ start: '\\', end: '\\' },
{ start: '¥', end: '¥' },
{ start: '|', end: '|' },
{ start: '|', end: '|' },
{ start: '「', end: '」' },
{ start: '[', end: ']' },
{ start: '【', end: '】' },
{ start: '{', end: '}' },
{ start: '{', end: '}' },
{ start: '(', end: ')' },
{ start: '(', end: ')' },
{ start: '<', end: '>' },
{ start: '<', end: '>' }
];
/**
* デリミタで囲まれたテキストを選択してコピー(デリミタは除外)
* - カーソル位置から左に開始デリミタを検索
* - 対応する終了デリミタを右に検索
* - 見つからない場合は行全体をコピー
* - 1文字選択時はカーソル位置(選択方向)で動作を変更
*/
export async function copyTextBetweenDelimiters() {
await processTextBetweenDelimiters(true); // 選択+コピー
}
/**
* デリミタで囲まれたテキストを選択するだけ(コピーしない)
* - copyTextBetweenDelimiters と同じ検索ロジック
* - 貼り付け先を選択してから Ctrl+V で置換する用途を想定
*/
export async function selectTextBetweenDelimiters() {
await processTextBetweenDelimiters(false); // 選択のみ
}
/**
* デリミタ間テキストの共通処理
* @param copyToClipboard true=選択+コピー、false=選択のみ
*/
async function processTextBetweenDelimiters(copyToClipboard: boolean) {
const editor = vscode.window.activeTextEditor;
if (!editor) {
vscode.window.showWarningMessage('アクティブなエディタがありません');
return;
}
const document = editor.document;
const selection = editor.selection;
const cursorPos = selection.active;
const line = document.lineAt(cursorPos.line);
const lineText = line.text;
// 1文字選択時の特殊処理(選択方向で動作を変更)
if (!selection.isEmpty && selection.end.character - selection.start.character === 1) {
const selectedChar = lineText[selection.start.character];
const charPos = selection.start.character;
// カーソルが選択文字の右側(左→右選択)
if (selection.active.character > selection.anchor.character) {
const result = handleRightDirection(lineText, charPos, selectedChar);
if (result) {
await applyResult(editor, cursorPos.line, result.start, result.end, copyToClipboard);
return;
}
}
// カーソルが選択文字の左側(右→左選択)
else if (selection.active.character < selection.anchor.character) {
const result = handleLeftDirection(lineText, charPos, selectedChar);
if (result) {
await applyResult(editor, cursorPos.line, result.start, result.end, copyToClipboard);
return;
}
}
}
// 通常処理:カーソル位置から左に開始デリミタを検索
const result = findDelimitedText(lineText, cursorPos.character);
if (result) {
await applyResult(editor, cursorPos.line, result.start, result.end, copyToClipboard);
} else {
// 見つからない場合は行全体を対象
if (copyToClipboard) {
copyEntireLine(editor, cursorPos.line);
} else {
selectEntireLine(editor, cursorPos.line);
}
}
}
/**
* 右方向処理:選択文字が開始デリミタかチェックし、右に終了デリミタを検索(ネスト考慮)
*/
function handleRightDirection(
lineText: string,
charPos: number,
selectedChar: string
): { start: number; end: number } | null {
for (const pair of DELIMITER_PAIRS) {
if (selectedChar === pair.start) {
// ネストレベルを考慮して対応する終了デリミタを検索
const endPos = findMatchingEndDelimiter(lineText, charPos, pair.start, pair.end);
if (endPos !== -1) {
// デリミタを除外: 開始の次の文字から終了の前まで
return { start: charPos + 1, end: endPos };
}
}
}
return null;
}
/**
* 左方向処理:選択文字が終了デリミタかチェックし、左に開始デリミタを検索(ネスト考慮)
*/
function handleLeftDirection(
lineText: string,
charPos: number,
selectedChar: string
): { start: number; end: number } | null {
for (const pair of DELIMITER_PAIRS) {
if (selectedChar === pair.end) {
// ネストレベルを考慮して対応する開始デリミタを検索
const startPos = findMatchingStartDelimiter(lineText, charPos, pair.start, pair.end);
if (startPos !== -1) {
// デリミタを除外: 開始の次の文字から終了の前まで
return { start: startPos + 1, end: charPos };
}
}
}
return null;
}
/**
* カーソル位置からデリミタで囲まれたテキストを検索(デリミタは除外)
* - 左に開始デリミタを検索
* - ネストレベルを考慮して対応する終了デリミタを右に検索
*/
function findDelimitedText(
lineText: string,
cursorPos: number
): { start: number; end: number } | null {
let bestMatch: { start: number; end: number; startPos: number } | null = null;
// 各デリミタペアについて検索
for (const pair of DELIMITER_PAIRS) {
// カーソル位置の左側で最後に出現する開始デリミタを検索
let startPos = -1;
for (let i = cursorPos - 1; i >= 0; i--) {
if (lineText[i] === pair.start) {
startPos = i;
break;
}
}
if (startPos === -1) {
continue; // 開始デリミタが見つからない
}
// 対応する終了デリミタを検索(ネストレベルを考慮)
const endPos = findMatchingEndDelimiter(lineText, startPos, pair.start, pair.end);
if (endPos !== -1) {
// カーソル位置が範囲内にあるかチェック
if (startPos < cursorPos && cursorPos <= endPos) {
// より近い開始デリミタを優先
if (!bestMatch || startPos > bestMatch.startPos) {
// デリミタを除外: 開始の次の文字から終了の前まで
bestMatch = {
start: startPos + 1,
end: endPos,
startPos: startPos
};
}
}
}
}
if (bestMatch) {
return { start: bestMatch.start, end: bestMatch.end };
}
return null;
}
/**
* ネストレベルを考慮して対応する終了デリミタを検索
*/
function findMatchingEndDelimiter(
lineText: string,
startPos: number,
startDelimiter: string,
endDelimiter: string
): number {
// 開始と終了が同じ文字の場合(" や ' など)は従来の方式
if (startDelimiter === endDelimiter) {
return lineText.indexOf(endDelimiter, startPos + 1);
}
// 異なる文字の場合はネストレベルを考慮
let nestLevel = 1; // 開始デリミタを見つけた状態から開始
for (let i = startPos + 1; i < lineText.length; i++) {
const char = lineText[i];
if (char === startDelimiter) {
nestLevel++; // 同じ開始デリミタが出現したらネストレベル+1
} else if (char === endDelimiter) {
nestLevel--; // 終了デリミタが出現したらネストレベル-1
if (nestLevel === 0) {
return i; // ネストレベルが0になったら対応する終了デリミタを発見
}
}
}
return -1; // 対応する終了デリミタが見つからない
}
/**
* ネストレベルを考慮して対応する開始デリミタを検索(逆方向)
*/
function findMatchingStartDelimiter(
lineText: string,
endPos: number,
startDelimiter: string,
endDelimiter: string
): number {
// 開始と終了が同じ文字の場合(" や ' など)は従来の方式
if (startDelimiter === endDelimiter) {
return lineText.lastIndexOf(startDelimiter, endPos - 1);
}
// 異なる文字の場合はネストレベルを考慮(逆方向)
let nestLevel = 1; // 終了デリミタを見つけた状態から開始
for (let i = endPos - 1; i >= 0; i--) {
const char = lineText[i];
if (char === endDelimiter) {
nestLevel++; // 同じ終了デリミタが出現したらネストレベル+1
} else if (char === startDelimiter) {
nestLevel--; // 開始デリミタが出現したらネストレベル-1
if (nestLevel === 0) {
return i; // ネストレベルが0になったら対応する開始デリミタを発見
}
}
}
return -1; // 対応する開始デリミタが見つからない
}
/**
* 指定範囲を選択し、copyToClipboard=true の場合はコピーも行う
*/
async function applyResult(
editor: vscode.TextEditor,
line: number,
startChar: number,
endChar: number,
copyToClipboard: boolean
) {
const startPos = new vscode.Position(line, startChar);
const endPos = new vscode.Position(line, endChar);
editor.selection = new vscode.Selection(startPos, endPos);
if (copyToClipboard) {
await vscode.commands.executeCommand('editor.action.clipboardCopyAction');
}
}
/**
* 行全体をクリップボードにコピー
*/
async function copyEntireLine(editor: vscode.TextEditor, lineNumber: number) {
const line = editor.document.lineAt(lineNumber);
editor.selection = new vscode.Selection(line.range.start, line.range.end);
await vscode.commands.executeCommand('editor.action.clipboardCopyAction');
vscode.window.showInformationMessage('デリミタが見つからないため、行全体をコピーしました');
}
/**
* 行全体を選択するだけ(コピーしない)
*/
function selectEntireLine(editor: vscode.TextEditor, lineNumber: number) {
const line = editor.document.lineAt(lineNumber);
editor.selection = new vscode.Selection(line.range.start, line.range.end);
vscode.window.showInformationMessage('デリミタが見つからないため、行全体を選択しました');
}コードのポイント解説
1. デリミタペアの定義
const DELIMITER_PAIRS: Array<{ start: string; end: string }> = [
{ start: '"', end: '"' },
{ start: "'", end: "'" },
{ start: '`', end: '`' }, // バッククォート(テンプレートリテラル・Markdownインラインコード)
{ start: '「', end: '」' },
// ... 18種類
];開始文字と終了文字のペアを配列で管理。拡張が容易な設計です。
2. コピーと選択の共通化
// コピーする場合(Ctrl+Alt+D)
export async function copyTextBetweenDelimiters() {
await processTextBetweenDelimiters(true);
}
// 選択のみの場合(Ctrl+Alt+S)
export async function selectTextBetweenDelimiters() {
await processTextBetweenDelimiters(false);
}
// 共通処理(copyToClipboard で動作を分岐)
async function processTextBetweenDelimiters(copyToClipboard: boolean) {
// ...
await applyResult(editor, line, start, end, copyToClipboard);
}copyToClipboardフラグ1つで2つのコマンドを共通化。検索ロジックの重複をなくし、保守性を高めています。
3. 選択方向の判定
// カーソルが選択文字の右側(左→右選択)
if (selection.active.character > selection.anchor.character) {
// 右方向処理
}
// カーソルが選択文字の左側(右→左選択)
else if (selection.active.character < selection.anchor.character) {
// 左方向処理
}VSCodeのselection.active(カーソル位置)とselection.anchor(選択開始位置)を比較して選択方向を判定。
4. 最も近いデリミタの優先
// より近い開始デリミタを優先
if (!bestMatch || startPos > bestMatch.startPos) {
bestMatch = {
start: startPos + 1,
end: endPos,
startPos: startPos
};
}複数のデリミタペアが見つかった場合、カーソルに最も近い開始デリミタを優先します。
5. デリミタの除外
// デリミタを除外: 開始の次の文字から終了の前まで
return { start: charPos + 1, end: endPos };開始デリミタの次の文字から終了デリミタの前までを選択範囲とすることで、デリミタ自体を除外します。
6. パフォーマンス最適化
// 行内のみの線形検索
for (let i = cursorPos - 1; i >= 0; i--) {
if (lineText[i] === pair.start) {
startPos = i;
break;
}
}行内のみを検索対象とすることで、長大なファイルでもパフォーマンスに影響しません。
7. ネストレベル考慮のアルゴリズム
function findMatchingEndDelimiter(lineText: string, startPos: number, startDelimiter: string, endDelimiter: string): number {
// 開始と終了が同じ文字(" や ' など)は従来の方式
if (startDelimiter === endDelimiter) {
return lineText.indexOf(endDelimiter, startPos + 1);
}
// 異なる文字の場合はネストレベルを考慮
let nestLevel = 1; // 開始デリミタを見つけた状態から開始
for (let i = startPos + 1; i < lineText.length; i++) {
const char = lineText[i];
if (char === startDelimiter) {
nestLevel++; // 同じ開始デリミタが出現 → ネスト +1
} else if (char === endDelimiter) {
nestLevel--; // 終了デリミタが出現 → ネスト -1
if (nestLevel === 0) {
return i; // ネストレベルが0 → 対応する括弧を発見
}
}
}
return -1; // 対応する終了デリミタが見つからない
}ポイント:
nestLevel = 1から開始(開始デリミタを見つけた状態)- 同じ開始デリミタが出現するたびに
nestLevel++(ネストが深くなる) - 終了デリミタが出現するたびに
nestLevel--(ネストから抜ける) nestLevel === 0になった位置が対応する終了デリミタ
具体例:
function(a, (b, c), d)
↑開始function(→nestLevel = 1(b→nestLevel = 2(ネストが深くなる)c)→nestLevel = 1(ネストから抜ける)d)→nestLevel = 0✅ ここが対応する閉じ括弧
これにより、複雑な入れ子構造でも正確にペアを検出できます。
package.jsonの設定
拡張機能のコマンドとキーバインディングを登録します。
{
"contributes": {
"commands": [
{
"command": "myMacros.copyTextBetweenDelimiters",
"title": "Copy Text Between Delimiters",
"category": "My Macros"
},
{
"command": "myMacros.selectTextBetweenDelimiters",
"title": "Select Text Between Delimiters",
"category": "My Macros"
}
],
"keybindings": [
{
"command": "myMacros.copyTextBetweenDelimiters",
"key": "ctrl+alt+d",
"when": "editorTextFocus"
},
{
"command": "myMacros.selectTextBetweenDelimiters",
"key": "ctrl+alt+s",
"when": "editorTextFocus"
}
]
}
}extension.tsでの登録
import { copyTextBetweenDelimiters, selectTextBetweenDelimiters } from './macros/copyTextBetweenDelimiters';
export function activate(context: vscode.ExtensionContext) {
const commands = [
vscode.commands.registerCommand(
'myMacros.copyTextBetweenDelimiters',
copyTextBetweenDelimiters
),
vscode.commands.registerCommand(
'myMacros.selectTextBetweenDelimiters',
selectTextBetweenDelimiters
),
// ... 他のコマンド
];
commands.forEach(command => context.subscriptions.push(command));
}使い方
基本的な使い方
ステップ1:カーソルを配置
デリミタで囲まれたテキスト内にカーソルを配置します。
const message = "ここにカーソル";
↑ステップ2:マクロ実行
Ctrl+Alt+D→ 選択してクリップボードにコピーCtrl+Alt+S→ 選択のみ(クリップボードは変更しない)
ステップ3:完了
デリミタ内のテキストが選択(または選択+コピー)されます。
コピー&貼り付け置換の使い方
Ctrl+Alt+DとCtrl+Alt+Sを組み合わせると、クリップボードを保持したまま置換できます。
const src = "コピー元のテキスト";
const dst = "ここに上書きしたい";"コピー元のテキスト"内にカーソル →Ctrl+Alt+Dでコピー"ここに上書きしたい"内にカーソルを移動Ctrl+Alt+Sで選択(クリップボードは変わらない)Ctrl+Vで貼り付け → 置換完了!
方向別の使い方
右方向検索
開始デリミタを1文字選択(左→右)してからCtrl+Alt+D
<"あいうえお">
↑選択 ↑カーソル→ 右に"を検索してあいうえおを選択
左方向検索
終了デリミタを1文字選択(右→左)してからCtrl+Alt+D
<"あいうえお">
↑カーソル ↑選択→ 左に"を検索してあいうえおを選択
トラブルシューティング
Q: デリミタが認識されない
A: 対応デリミタを確認してください
現在対応しているのは18種類のデリミタです。バッククォート(`)も対応済みです。対応していないデリミタはDELIMITER_PAIRS配列に追加すれば簡単に拡張できます。
Q: 入れ子構造で意図しない範囲が選択される
A: ネストレベルを考慮して正確にマッチングします
改修後のバージョンでは、括弧のネストレベルを考慮して対応する括弧を検出します。
function(a, (b, c), d)
↑ここにカーソルこの場合:
- カーソルの左で最も近い
((functionの後)を検出 - ネストレベルをカウントしながら対応する
)を検索 - 内側の
(b, c)のネストを考慮して、最外側の)を正確に検出
ただし、異なる種類のデリミタの場合は最も近いものが優先されます:
<"「あいうえお」、【かきくけこ】">
↑ここにカーソルこの場合、「~」が選択されます(最も近いため)。外側の"~"を選択したい場合は、"と「の間にカーソルを配置してください。
Q: 行全体がコピーされてしまう
A: デリミタが見つからない場合のフォールバック動作です
- カーソル位置の左に開始デリミタがない
- 対応する終了デリミタが右に見つからない
- カーソル位置がデリミタの外側
これらの場合、安全のため行全体をコピーします。
Q: 動作が遅い
A: 行内のみの検索なのでパフォーマンス問題はありません
1行あたりの検索は線形時間(O(n))で、通常の行(数百文字)なら瞬時に完了します。
パフォーマンスについて
計算量
時間計算量: O(n × m)
- n: 行の文字数
- m: デリミタペアの数(18個)
- 通常の行(100文字)なら約1800回の比較
空間計算量: O(1)
- 固定サイズのデータ構造のみ使用
ベンチマーク(参考値)
- 100文字の行: < 1ms
- 1000文字の行: < 5ms
- 10000文字の行: < 50ms
実用上、全く問題ないレベルです。
拡張アイデア
カスタムデリミタの設定
VSCodeの設定ファイル(settings.json)から読み込むようにすれば、ユーザー独自のデリミタにも対応可能です。
{
"myMacros.customDelimiters": [
{ "start": "/*", "end": "*/" },
{ "start": "<!--", "end": "-->" }
]
}複数行対応
現在は行内のみですが、開始・終了デリミタが別の行にある場合も対応できます(ただし、パフォーマンスへの配慮が必要)。
まとめ:キーボード1回でデリミタ内を抜き出す快適さ
VSCodeの標準機能では手が届かなかった「デリミタ間のテキスト選択」を、TypeScriptマクロで実現しました。
このマクロの強み:
- 1キー操作で完結(
Ctrl+Alt+Dコピー /Ctrl+Alt+S選択のみ) - 18種類のデリミタに対応(バッククォートも含む)
- 選択方向で動作を切り替え可能
- 入れ子構造でも正確に動作(ネストレベル考慮)
- パフォーマンス問題なし
- デリミタが見つからない場合も安全(行全体をフォールバック)
こんな人におすすめ:
- コードレビューでログやJSONを頻繁に扱う
- 文字列リテラルのコピペが多い
- マウス操作を減らしたい
- キーボードだけで完結させたい
標準のVSCodeでは「Ctrl+Shift+→」を何度も押す必要があった操作が、たった1回のキー操作で完了します。
「需要はないかもしれない」と思っていましたが、一度使うと手放せない便利機能です。特に大量のテキストを扱う作業では、作業効率が劇的に向上します。
ぜひVSCodeでの快適なテキスト編集にお役立てください!
関連記事
- VSCodeで「任意行までの一括選択・コピー」を実現するTypeScriptマクロ

- VSCodeで「選択行による一括置換」を実現するTypeScriptマクロ

- VSCodeで正規表現マッチを一括抽出するTypeScriptマクロ

- VSCodeでパスを読み取り専用で開くTypeScriptマクロ

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