VSCodeで「デリミタ間テキスト選択コピー」を実現するTypeScriptマクロ【括弧・引用符を1キーで抜き出し】

Development
スポンサーリンク
スポンサーリンク
  1. はじめに:引用符や括弧内のテキスト選択、面倒じゃないですか?
  2. この記事で分かること
  3. 実装した機能の概要
    1. できること
    2. 対応デリミタ(18種類)
    3. 3つの動作モード
    4. フォールバック機能
  4. 具体的な使用場面とメリット
    1. 場面1:コード内の文字列リテラルを抽出
    2. 場面2:ファイルパスを素早く抽出
    3. 場面3:JSONのキーや値を抽出
    4. 場面4:正規表現パターンを抽出
    5. 場面5:複雑な入れ子構造から特定部分を抽出
    6. 場面6:Markdown内のコード抜き出し
    7. 場面7:コピー&貼り付け置換(Ctrl+Alt+Sの活用)
    8. メリットまとめ
  5. 改修履歴:ネスト構造への対応
    1. 改修前の問題点
    2. 改修内容:括弧のバランスを考慮したマッチング
  6. TypeScript実装の詳細
  7. コードのポイント解説
    1. 1. デリミタペアの定義
    2. 2. コピーと選択の共通化
    3. 3. 選択方向の判定
    4. 4. 最も近いデリミタの優先
    5. 5. デリミタの除外
    6. 6. パフォーマンス最適化
    7. 7. ネストレベル考慮のアルゴリズム
  8. package.jsonの設定
    1. extension.tsでの登録
  9. 使い方
    1. 基本的な使い方
    2. コピー&貼り付け置換の使い方
    3. 方向別の使い方
  10. トラブルシューティング
    1. Q: デリミタが認識されない
    2. Q: 入れ子構造で意図しない範囲が選択される
    3. Q: 行全体がコピーされてしまう
    4. Q: 動作が遅い
  11. パフォーマンスについて
    1. 計算量
    2. ベンチマーク(参考値)
  12. 拡張アイデア
    1. カスタムデリミタの設定
    2. 複数行対応
  13. まとめ:キーボード1回でデリミタ内を抜き出す快適さ
  14. 関連記事

はじめに:引用符や括弧内のテキスト選択、面倒じゃないですか?

VSCodeで文字列編集をしていて、こんな経験はありませんか?

const message = "このテキストだけコピーしたい";
const path = '/home/user/documents/report.pdf';
const config = { name: "設定値", value: 123 };

ダブルクォーテーション内の文字列だけコピーしたいときに、以下のような操作が必要です:

  1. マウスで開始位置をクリック
  2. ドラッグして終了位置まで選択
  3. Ctrl+Cでコピー

あるいは:

  1. Ctrl+Shift+→を何度も押して単語単位で選択
  2. 微調整のためにShift+→/←
  3. 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 → 文字列だけが即座にコピーされる。

従来の方法:

  1. "の直後をクリック
  2. Shift+Endで行末まで選択
  3. Shift+←で"の手前まで調整
  4. Ctrl+C

このマクロ:

  1. 文字列内にカーソルを置く
  2. 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+Dconsole.log('Hello')だけがコピーされる。

場面7:コピー&貼り付け置換(Ctrl+Alt+Sの活用)

Ctrl+Alt+S(選択のみ)との組み合わせで、貼り付けによる置換が手軽にできます。

const oldValue = "置換前の文字列";
const newValue = "置換後の文字列";

手順:

  1. "置換前の文字列" 内にカーソル → Ctrl+Alt+D でコピー
  2. "置換後の文字列" 内にカーソルを移動
  3. Ctrl+Alt+S で置換対象を選択
  4. Ctrl+V で貼り付け → 置換完了!

クリップボードを変更せずに置換先だけを選択できるので、途中でクリップボードが上書きされる心配がありません。

メリットまとめ

  • マウス操作不要: カーソル位置だけで判定
  • 高速: キーボード1回で完結
  • 正確: デリミタを自動検出
  • 柔軟: 選択方向で動作を変更可能、コピー/選択を使い分け可能
  • 安全: デリミタが見つからなくても行全体をコピー/選択

改修履歴:ネスト構造への対応

改修前の問題点

初期バージョンでは、ネスト(入れ子)構造の括弧を正しく処理できない問題がありました。

問題の例:

function(b,c){...}

この場合、カーソルがfunctionにあるとき:

  1. 左から最も近い開始デリミタ(を検出
  2. その位置から右に最初に見つかった終了デリミタ)をマッチング
  3. しかし、最初の)b,c)の閉じ括弧なので、function(b,cが誤って選択される

ネストレベルを考慮していないため、入れ子構造で誤動作していました。

改修内容:括弧のバランスを考慮したマッチング

VSCodeのCtrl+Shift+\(対応する括弧へジャンプ)と同じように、ネストレベルをカウントしながら対応する括弧を探すアルゴリズムを実装しました。

アルゴリズム:

  1. カーソル位置から左に開始デリミタを検索
  2. その位置から右に向かって:
    • 同じ開始デリミタが出現 → カウンタ +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)
        ↑開始
  1. function(nestLevel = 1
  2. (bnestLevel = 2(ネストが深くなる)
  3. c)nestLevel = 1(ネストから抜ける)
  4. 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+DCtrl+Alt+Sを組み合わせると、クリップボードを保持したまま置換できます。

const src = "コピー元のテキスト";
const dst = "ここに上書きしたい";
  1. "コピー元のテキスト" 内にカーソル → Ctrl+Alt+D でコピー
  2. "ここに上書きしたい" 内にカーソルを移動
  3. Ctrl+Alt+S で選択(クリップボードは変わらない)
  4. 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で長いログファイルやドキュメントを編集していて、「この行から特定の文字列まで一気にコピーしたい」と思ったことはありませんか?通常のVSCodeでは、以下のような手順が必要です:開...
  • VSCodeで「選択行による一括置換」を実現するTypeScriptマクロ
VSCodeで「選択行による一括置換」を実現するTypeScriptマクロ【テキスト編集が10倍速くなる】
はじめにテキスト編集で「複数の文字列を一度に置換したい」と思ったことはありませんか?例えば、こんなシーン:変数名を複数箇所で一括変更テーブル定義のデータ型を複数列で一括更新ドキュメント内の用語統一を複数ワードで一括実施VSCodeの標準機能...
  • VSCodeで正規表現マッチを一括抽出するTypeScriptマクロ
VSCodeで「正規表現マッチ抽出」を実現するTypeScriptマクロ【ログ解析が10倍速くなる】
はじめにサーバーログやアプリケーションログを解析する時、こんな悩みありませんか?2024-02-08 10:00:00 INFO: Server started2024-02-08 10:01:00 ERROR: Connection fa...
  • VSCodeでパスを読み取り専用で開くTypeScriptマクロ
VSCodeでカーソル下のパスを開くマクロ【Office読み取り専用・関連付けアプリ・連続起動対応】
はじめにVSCodeでコードやドキュメントを書いている時、テキスト内にパスが書かれていてそのファイルを開きたいこと、ありませんか?作業メモ.txt---設計書: C:\work\documents\設計書_v1.0.docx手順書: C:\...

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

コメント