VSCodeでカーソル下のパスを開くマクロ【Office読み取り専用・関連付けアプリ・連続起動対応】

Development
スポンサーリンク
スポンサーリンク
  1. はじめに
  2. なぜOffice系ファイルを読み取り専用で開く必要があるのか?
    1. Windowsでは意外と面倒
    2. 読み取り専用で開きたいシーン(多い!)
    3. 今回のマクロの価値
  3. 実装した機能
    1. 1. パスオープン機能(通常モード)
    2. 2. パスオープン機能(ReadOnly / 関連付けアプリ)
    3. 3. 親フォルダを開く
    4. 4. 新規Officeファイル作成
    5. 5. 複数行選択時の一括起動(2026年9月追加)
    6. 6. タブ区切り階層テキストでの位置非依存検出(2026年9月追加)
    7. 対応ファイル形式
    8. パス記述の柔軟性
    9. 連続起動対応
  4. 技術解説:PowerShell + COMでバージョン非依存を実現
    1. なぜPowerShell + COMなのか?
    2. GetActiveObjectで既存インスタンスを再利用
    3. フォーカスを奪わない起動方法
    4. COM APIの読み取り専用パラメータ
    5. PowerShellのパスエスケープ
  5. TypeScriptコード解説
    1. メイン処理(openPath.ts)
    2. パス前処理
    3. ファイルオープン処理
    4. 拡張子関連付けアプリで開く(フォーカス維持)
    5. Office系ファイルの読み取り専用オープン
    6. 一括起動後のカーソル移動
    7. 新機能:親フォルダを開く
    8. 新機能:新規Officeファイル作成
  6. 使い方
    1. 1. 拡張機能のインストール
    2. 2. 基本的な使い方
    3. 3. 実践例
  7. package.jsonの設定
  8. メリット・デメリット
    1. メリット
    2. デメリット
  9. トラブルシューティング
    1. ファイルが開かない
    2. 読み取り専用で開かない
    3. 外部アプリ起動時にVSCodeのフォーカスが奪われる
  10. まとめ
  11. 関連記事

はじめに

VSCodeでコードやドキュメントを書いている時、テキスト内にパスが書かれていてそのファイルを開きたいこと、ありませんか?

作業メモ.txt
---
設計書: C:\work\documents\設計書_v1.0.docx
手順書: C:\work\manual\操作手順.xlsx
参考資料: \\server\share\資料\参考.pptx

こういった場面で、カーソル下のパスを一発で開けたら便利ですよね。

さらに、「参照するだけで編集はしたくない」というシーンも多いはず。特にOffice系ファイルは誤って編集して上書き保存してしまうリスクがあります。

また、複数ファイルを連続して開く作業では、いちいちフォーカスが奪われてストレスになることも。

今回、VSCodeのTypeScriptマクロでカーソル下のパスからファイル/フォルダを開く機能を実装しました。特に以下の点にこだわっています。

  • Office系ファイルを読み取り専用で開く
  • テキストファイルを拡張子関連付けアプリ(サクラエディタ等)で開く
  • 外部アプリ起動後もVSCodeのフォーカスを維持
  • 実行後に元のエディタへ自動復帰・次の行へ移動(連続起動対応)

本記事ではその実装方法と、なぜこの機能が実務で役立つのかを紹介します。

スポンサーリンク

なぜOffice系ファイルを読み取り専用で開く必要があるのか?

Windowsでは意外と面倒

実は、Windowsの標準機能ではOffice系ファイルを簡単に読み取り専用で開けません

標準的な方法:

  1. Excel/Word/PowerPointを起動
  2. ファイル → 開く
  3. ファイルを選択
  4. 「開く」ボタンの▼をクリック
  5. "読み取り専用で開く" を選択

面倒すぎる…!

読み取り専用で開きたいシーン(多い!)

実務でこんな経験ありませんか?

✅ 参照だけしたいファイル

  • マニュアル、手順書、テンプレート
  • 誤編集を絶対に避けたい重要ファイル

✅ 他人のファイルを確認

  • レビュー時に誤って保存してしまうのを防ぐ
  • 共有フォルダのファイルを安全に閲覧

✅ 古いバージョンを参照

  • バックアップファイルを見るだけ
  • 過去の仕様書を確認

✅ 複数のファイルを比較

  • 片方は読み取り専用で開いて参照
  • 編集版と元版を並べて確認

今回のマクロの価値

方法手順数手間
Office内から開く5手順以上★★★★★
ファイルプロパティ変更3手順+戻す作業★★★★☆
今回のマクロ1キー操作★☆☆☆☆
スポンサーリンク

実装した機能

1. パスオープン機能(通常モード)

ショートカット: Ctrl+F12

  • カーソル下のパスを通常モードで開く(編集可能)
  • フォルダ → Explorerで開く
  • テキストファイル → VSCodeで開く
  • Office系ファイル → COMで通常モード起動(フォーカス維持)

2. パスオープン機能(ReadOnly / 関連付けアプリ)

ショートカット: Ctrl+Shift+F12

  • テキスト系ファイル → 拡張子関連付けアプリで開く(サクラエディタ等)
  • Office系ファイル → 読み取り専用で開く(Excel/Word/PowerPoint)

3. 親フォルダを開く

ショートカット: Ctrl+Alt+F12

  • カーソル下のパスの親フォルダをExplorerで開く
  • ファイルの格納場所をすぐに確認したい時に便利

4. 新規Officeファイル作成

ショートカット: Ctrl+Shift+Alt+F12

  • カーソル下のパスにOffice系ファイルを新規作成して開く
  • Excel/Word/PowerPointの各フォーマットに対応
  • 親フォルダが存在しない場合は自動作成

5. 複数行選択時の一括起動(2026年9月追加)

パスが並んだ複数行を選択した状態でCtrl+F12またはCtrl+Shift+F12を実行すると、選択範囲内のパスをまとめて起動できます。

  • 選択範囲の全行が有効なパスなら一括で起動
  • パスが見つからない行(コメント行・区切り線など)は無視してスキップ
  • 10件以上選択している場合は確認ダイアログを表示(誤操作で大量起動するのを防止)
  • Office系ファイルが混在していてもレースコンディションを避けるため1件ずつ順番に開く

6. タブ区切り階層テキストでの位置非依存検出(2026年9月追加)

以下のように、呼び出し元・呼び出し先のスクリプトをタブ区切りで階層的に並べたメモから、パスが行のどの位置にあっても検出して起動できます。

親    C:\work\test1.sh    親シェル
子1        C:\work\test1-1.sh    10    〇〇処理
子2        C:\work\test1-2.sh    20    △△処理

タブで区切った各フィールドを左から順に走査し、最初に実在するパスを採用します。採用したパスの直後のフィールドが数字であれば、従来通り行番号ジャンプとしても扱います(123のような単純な数字だけでなく、1,234のようなカンマ区切りにも対応)。

なお、区切り文字は意図的にタブのみに限定しています。半角スペース区切り(サクラエディタのF12ジャンプのように、ファイル名にスペースが混ざっていても対応する方式)も検討しましたが、スペースはファイル名の一部になり得るため区切り位置が一意に決まらず、実装するなら「先頭から連結して実在確認する」という曖昧さの残るヒューリスティックが必要になります。タブは100%確実に区切り文字として機能するため、確実性を優先してタブ区切りのみをサポートしています。

対応ファイル形式

Office系(読み取り専用対応):

  • Excel: .xls, .xlsx, .xlsm, .xlsb
  • Word: .doc, .docx, .docm
  • PowerPoint: .ppt, .pptx, .pptm

テキスト系(Ctrl+F12はVSCodeで開く / Ctrl+Shift+F12は関連付けアプリで開く):

  • .txt, .log, .sql, .js, .ts, .java, .sh
  • .css, .html, .htm, .md, .json, .jsonl, .xml, .yml, .yaml, .csv, .tsv
  • .c, .cpp, .h, .py, .rb, .go, .rs, .php, .jsx, .tsx, .ps1
  • .cbl, .pco(COBOLソース)

その他の拡張子:

  • 「テキストで開く / そのまま開く / キャンセル」の警告ダイアログを表示(複数行一括起動時はこの確認をスキップし、関連付けアプリで開きます)

フォルダ:

  • Explorerで開く

パス記述の柔軟性

✅ 絶対パス
C:\work\documents\設計書.docx

✅ 相対パス(現在ファイルのディレクトリまたはワークスペース基準)
documents\設計書.docx
./documents/設計書.docx

✅ ネットワークパス
\\server\share\資料\手順書.xlsx

✅ 先頭の@を除去
@C:\work\test.txt

✅ ダブルクォーテーションを除去
"C:\work\test.txt"

✅ タブ区切り(先頭のみ対象)
C:\work\test.txt	更新日: 2025/02/06

✅ 選択テキスト優先
テキストを選択した状態で実行すると、選択内容をパスとして使用

連続起動対応

単一行を開いた場合は、開いたファイル/フォルダがそのままアクティブになります(元のエディタには戻りません)。以前は「次の行もパスらしければ自動で元のエディタに戻る」という挙動でしたが、実際に使ってみると連続オープンよりも単発で素早く開きたい場面の方が圧倒的に多く、逆に使いづらかったため撤回しました。

連続して複数のファイルを開きたい場合は、前述の「複数行選択時の一括起動」を使うのがおすすめです。この場合は全ファイルを開いた後、元のエディタに自動で戻り、選択範囲の次の行へカーソルが移動します。

タスク一覧.txt
---
C:\work\設計書.docx   ← 複数行まとめて選択してCtrl+Shift+F12
C:\work\手順書.xlsx   ← 一括で開き、完了後に元のエディタへ自動復帰
C:\work\参考.pptx

技術解説:PowerShell + COMでバージョン非依存を実現

なぜPowerShell + COMなのか?

問題点:Office実行ファイルのパスがバージョンで異なる

# Office 2016
C:\Program Files\Microsoft Office\Office16\EXCEL.EXE

# Office 2019
C:\Program Files\Microsoft Office\Office19\EXCEL.EXE

# Microsoft 365
C:\Program Files\Microsoft Office\root\Office16\EXCEL.EXE

解決策:COMオブジェクト経由でバージョン非依存に

# Officeのバージョンに関係なく動作
$excel = New-Object -ComObject Excel.Application
$excel.Workbooks.Open('C:\path\to\file.xlsx', 0, $true)
                                                   # ↑ ReadOnly = $true

GetActiveObjectで既存インスタンスを再利用

New-Object単独だと、Excelが既に起動中でも新しいインスタンスを作ってしまい、PERSONAL.XLSBのロック競合やセキュリティ通知が発生します。

GetActiveObjectで既存インスタンスを優先取得することで、この問題を回避します。

# 既存インスタンスを優先取得し、なければ新規作成
try {
    $excel = [System.Runtime.InteropServices.Marshal]::GetActiveObject('Excel.Application')
} catch {
    $excel = New-Object -ComObject Excel.Application
}
$excel.Visible = $true
$excel.Workbooks.Open('C:\path\to\file.xlsx', 0, $true)

フォーカスを奪わない起動方法

外部アプリを起動する際、通常の方法ではVSCodeのフォーカスが奪われます。これを回避するため、用途に応じて異なる方法を使います。

テキスト関連付けアプリ(サクラエディタ等): ShellExecuteのSW_SHOWNOACTIVATE

# SW_SHOWNOACTIVATE(4): ウィンドウを表示するがVSCodeのフォーカスを奪わない
(New-Object -ComObject Shell.Application).ShellExecute('C:\path\to\file.txt', '', '', 'open', 4)

Office系ファイル: COMで開く

COMで開く場合、ShellExecuteと異なりフォーカス権が付与されないため、VSCodeのフォーカスが維持されます。

COM APIの読み取り専用パラメータ

Excel:

$excel.Workbooks.Open(FileName, UpdateLinks, ReadOnly)
# 第3引数: ReadOnly = $true

Word:

$word.Documents.Open(FileName, ConfirmConversions, ReadOnly, AddToRecentFiles)
# 第3引数: ReadOnly = -1 (True)
# ※Wordは$trueではなく-1を指定

PowerPoint:

$ppt.Presentations.Open(FileName, ReadOnly, Untitled, WithWindow)
# 第2引数: ReadOnly = 1 (msoTrue)
# ※PowerPointはMsoTriState型で1を指定

PowerShellのパスエスケープ

シングルクォーテーション文字列内ではバックスラッシュはエスケープ不要です。シングルクォーテーションのみ''にエスケープします。

// バックスラッシュは不要、シングルクォートのみエスケープ
const escapedPath = filePath.replace(/'/g, "''");

TypeScriptコード解説

メイン処理(openPath.ts)

2026年9月の改修で、単一行実行時は開いたファイルをそのままアクティブにする(元エディタへは自動で戻らない)方式に変更し、複数行選択時は一括起動モードに分岐するようにしました。

import * as vscode from 'vscode';
import * as path from 'path';
import * as fs from 'fs';
import { exec } from 'child_process';

/**
 * カーソル下のパスを開く(VSCodeで開く)
 */
export async function openPath() {
    await openFileOrFolder(false);
}

/**
 * カーソル下のパスを開く(ReadOnly / 関連付けアプリ)
 */
export async function openPathReadOnly() {
    await openFileOrFolder(true);
}

const BATCH_WARNING_THRESHOLD = 10;

async function openFileOrFolder(readOnly: boolean) {
    const editor = vscode.window.activeTextEditor;
    if (!editor) {
        vscode.window.showWarningMessage('アクティブなエディタがありません');
        return;
    }

    const originalDocument = editor.document;
    const currentFilePath = editor.document.uri.fsPath;
    const selection = editor.selection;

    // 複数行選択かどうか判定
    // 行末で選択を終えた場合、selection.end は次行の0文字目になるため、その場合は前行までとみなす
    let selectionEndLine = selection.end.line;
    if (!selection.isEmpty && selection.end.character === 0 && selectionEndLine > selection.start.line) {
        selectionEndLine--;
    }

    if (!selection.isEmpty && selectionEndLine > selection.start.line) {
        await openMultiplePaths(originalDocument, currentFilePath, selection.start.line, selectionEndLine, readOnly);
        return;
    }

    // パスの取得: 選択テキストがあれば優先、なければ現在行から取得
    let rawText: string;
    if (!selection.isEmpty) {
        const selectedText = editor.document.getText(selection).trim();
        rawText = selectedText || editor.document.lineAt(selection.active.line).text;
    } else {
        rawText = editor.document.lineAt(selection.active.line).text;
    }

    // タブ区切りの各フィールドを左から順に走査し、最初に実在するパスを採用する
    // (階層表示のように、パスがどのフィールド位置にあっても対応するため)
    const found = findPathInLine(rawText, currentFilePath);
    if (!found) {
        vscode.window.showErrorMessage(`パスが見つかりません: ${rawText.trim()}`);
        return;
    }

    const stat = fs.statSync(found.resolvedPath);

    if (stat.isDirectory()) {
        // フォルダをExplorerで開く
        await openInExplorer(found.resolvedPath);
    } else {
        // ファイルを開く(開いたファイル/フォルダをそのままアクティブにし、元エディタへは戻らない)
        await openFile(found.resolvedPath, readOnly, found.lineNumber);
    }
}

/**
 * 複数行選択時: 選択範囲内の各行をパスとして一括起動する
 * - パスが見つからない行(コメント行・区切り線など)は無視する(エラーにしない)
 * - 有効件数が BATCH_WARNING_THRESHOLD 件以上の場合は確認ダイアログを表示
 * - Office系ファイルのCOMレースコンディションを避けるため直列(1件ずつ完了を待つ)で開く
 * - 全ファイルを開いた後、元のエディタに戻り選択範囲の次の行へカーソル移動する
 */
async function openMultiplePaths(
    originalDocument: vscode.TextDocument,
    currentFilePath: string,
    startLine: number,
    endLine: number,
    readOnly: boolean
) {
    type PathEntry = { resolvedPath: string; lineNumber: number | null };
    const entries: PathEntry[] = [];

    for (let i = startLine; i <= endLine; i++) {
        const rawLine = originalDocument.lineAt(i).text;
        if (rawLine.trim() === '') {
            continue;
        }
        const found = findPathInLine(rawLine, currentFilePath);
        if (!found) {
            continue;
        }
        entries.push(found);
    }

    if (entries.length === 0) {
        vscode.window.showWarningMessage('選択範囲内に開けるパスが見つかりません');
        return;
    }

    if (entries.length >= BATCH_WARNING_THRESHOLD) {
        const answer = await vscode.window.showWarningMessage(
            `${entries.length}件のファイル/フォルダを一括で開きます。よろしいですか?`,
            'OK',
            'キャンセル'
        );
        if (answer !== 'OK') {
            return;
        }
    }

    for (const entry of entries) {
        const stat = fs.statSync(entry.resolvedPath);
        if (stat.isDirectory()) {
            await openInExplorer(entry.resolvedPath);
        } else {
            // 一括起動時は未定義拡張子の確認ダイアログを省略
            await openFile(entry.resolvedPath, readOnly, entry.lineNumber, true);
        }
    }

    // 元のエディタに戻り、選択範囲の次の行へ移動
    await moveCursorToNextLine(originalDocument, endLine);
}

/**
 * 行内のタブ区切りフィールドを左から順に走査し、最初に実在するパスを探す。
 * パスの位置は固定せず、どのフィールドにあっても検出する
 * (例: "親\tC:\foo.sh\t親シェル" のように1列目がラベル、2列目がパスという階層表示に対応)。
 * 採用したパスの直後のフィールドが数字であれば行番号として扱う。
 */
function findPathInLine(rawLine: string, currentFilePath: string): { resolvedPath: string; lineNumber: number | null } | null {
    const tokens = rawLine.split('\t');
    for (let i = 0; i < tokens.length; i++) {
        const token = preprocessPath(tokens[i].trim());
        if (token === '') {
            continue;
        }
        const resolvedPath = resolveAbsolutePath(token, currentFilePath);
        if (fs.existsSync(resolvedPath)) {
            const lineNumber = extractLineNumberFromToken(tokens[i + 1]);
            return { resolvedPath, lineNumber };
        }
    }
    return null;
}

パス前処理

/**
 * パスの前処理(@、引用符、タブ対応)
 */
function preprocessPath(text: string): string {
    // 先頭の@を除去
    text = text.replace(/^@/, '');

    // 先頭と末尾のダブルクォーテーション除去
    text = text.replace(/^"|"$/g, '');

    // タブ区切りの場合、先頭のみ対象
    if (text.includes('\t')) {
        text = text.split('\t')[0];
    }

    // 先頭の空白・全角スペースを除去
    text = text.replace(/^[\s\u3000]+/, '');

    return text;
}

ファイルオープン処理

/**
 * ファイルを開く
 * @param lineNumber タブ区切りの隣接フィールドから検出した行番号(無ければnull)
 * @param skipUnknownExtDialog 複数行一括起動時はtrue(未定義拡張子の確認ダイアログを省略)
 * @returns 開く操作が実行されたかどうか(キャンセル時はfalse)
 */
async function openFile(
    filePath: string,
    readOnly: boolean,
    lineNumber: number | null = null,
    skipUnknownExtDialog: boolean = false
): Promise<boolean> {
    const ext = path.extname(filePath).toLowerCase();

    // テキスト系ファイルの拡張子定義
    const textExtensions = [
        '.txt', '.log', '.sql', '.js', '.ts', '.java', '.sh',
        '.css', '.html', '.htm', '.md', '.json', '.jsonl', '.xml',
        '.yml', '.yaml', '.csv', '.tsv', '.c', '.cpp', '.h', '.py',
        '.rb', '.go', '.rs', '.php', '.jsx', '.tsx', '.ps1',
        '.cbl', '.pco'  // COBOLソース
    ];

    if (textExtensions.includes(ext)) {
        if (readOnly) {
            // Ctrl+Shift+F12: 拡張子関連付けアプリで開く(サクラエディタ等)
            await openWithAssociatedApp(filePath);
        } else {
            // Ctrl+F12: VSCodeで開く(行番号があればジャンプ)
            try {
                const document = await vscode.workspace.openTextDocument(filePath);
                const editor = await vscode.window.showTextDocument(document, {
                    preview: false,
                    viewColumn: vscode.ViewColumn.Active
                });
                if (lineNumber !== null) {
                    const line = Math.min(lineNumber - 1, document.lineCount - 1);
                    const pos = new vscode.Position(line, 0);
                    editor.selection = new vscode.Selection(pos, pos);
                    editor.revealRange(new vscode.Range(pos, pos), vscode.TextEditorRevealType.InCenter);
                }
            } catch (error) {
                vscode.window.showErrorMessage(`ファイルを開けませんでした: ${error}`);
            }
        }
        return true;
    } else if (isOfficeFile(ext)) {
        // Office系ファイル: ReadOnly対応
        await openWithDefaultApp(filePath, ext, readOnly);
        return true;
    } else if (skipUnknownExtDialog) {
        // 一括起動時: 確認ダイアログを出さず関連付けアプリで開く
        await openWithAssociatedApp(filePath);
        return true;
    } else {
        // テキスト定義外の拡張子: 警告ダイアログ
        const answer = await vscode.window.showWarningMessage(
            `「${ext || '(拡張子なし)'}」はテキストとして定義されていない拡張子です。どのように開きますか?`,
            'テキストで開く',
            'そのまま開く',
            'キャンセル'
        );
        if (answer === 'テキストで開く') {
            try {
                const document = await vscode.workspace.openTextDocument(filePath);
                await vscode.window.showTextDocument(document, {
                    preview: false,
                    viewColumn: vscode.ViewColumn.Active
                });
            } catch (error) {
                vscode.window.showErrorMessage(`ファイルを開けませんでした: ${error}`);
            }
            return true;
        } else if (answer === 'そのまま開く') {
            await openWithAssociatedApp(filePath);
            return true;
        }
        return false;  // キャンセルまたは通知を閉じた
    }
}

拡張子関連付けアプリで開く(フォーカス維持)

複数行一括起動でOffice系ファイルが混在する場合に直列実行できるよう、execのコールバックをPromiseでラップしています。

/**
 * OSの拡張子関連付けアプリでファイルを開く(VSCodeのフォーカスを維持)
 */
function openWithAssociatedApp(filePath: string): Promise<void> {
    return new Promise((resolve) => {
        const platform = process.platform;

        if (platform === 'win32') {
            // SW_SHOWNOACTIVATE(4): ウィンドウを表示するがVSCodeのフォーカスを奪わない
            const escapedPath = filePath.replace(/'/g, "''");
            exec(
                `powershell -NoProfile -ExecutionPolicy Bypass -Command "(New-Object -ComObject Shell.Application).ShellExecute('${escapedPath}', '', '', 'open', 4)"`,
                (error) => {
                    if (error) {
                        vscode.window.showErrorMessage(`ファイルを開けませんでした: ${error.message}`);
                    }
                    resolve();
                }
            );
        } else if (platform === 'darwin') {
            exec(`open "${filePath}"`, (error) => {
                if (error) {
                    vscode.window.showErrorMessage(`ファイルを開けませんでした: ${error.message}`);
                }
                resolve();
            });
        } else {
            exec(`xdg-open "${filePath}"`, (error) => {
                if (error) {
                    vscode.window.showErrorMessage(`ファイルを開けませんでした: ${error.message}`);
                }
                resolve();
            });
        }
    });
}

Office系ファイルの読み取り専用オープン

/**
 * Office系ファイルかどうか判定
 */
function isOfficeFile(ext: string): boolean {
    const officeExtensions = [
        '.xls', '.xlsx', '.xlsm', '.xlsb',  // Excel
        '.doc', '.docx', '.docm',            // Word
        '.ppt', '.pptx', '.pptm'             // PowerPoint
    ];
    return officeExtensions.includes(ext);
}

/**
 * デフォルトアプリでファイルを開く(Office系COM対応)
 * 一括起動時の直列実行のためPromiseで完了を通知する
 */
function openWithDefaultApp(filePath: string, ext: string, readOnly: boolean): Promise<void> {
    const platform = process.platform;

    if (platform === 'win32') {
        // Windows: Office系ファイルはCOMで開く(ShellExecuteと異なりフォーカス権が付与されない)
        if (readOnly) {
            return openOfficeFileReadOnly(filePath, ext);
        } else {
            return openOfficeFileNormal(filePath, ext);
        }
    }

    return new Promise((resolve) => {
        if (platform === 'darwin') {
            exec(`open "${filePath}"`, (error) => {
                if (error) {
                    vscode.window.showErrorMessage(`ファイルを開けませんでした: ${error.message}`);
                }
                resolve();
            });
        } else {
            exec(`xdg-open "${filePath}"`, (error) => {
                if (error) {
                    vscode.window.showErrorMessage(`ファイルを開けませんでした: ${error.message}`);
                }
                resolve();
            });
        }
    });
}

/**
 * Office系ファイルを通常モードで開く(PowerShell + COM)
 * GetActiveObjectで既存インスタンスに接続することでPERSONAL.XLSBのロック競合とセキュリティ通知を回避する
 */
function openOfficeFileNormal(filePath: string, ext: string): Promise<void> {
    return new Promise((resolve) => {
        const escapedPath = filePath.replace(/'/g, "''");
        let psScript = '';

        if (['.xls', '.xlsx', '.xlsm', '.xlsb'].includes(ext)) {
            psScript = `try { $excel = [System.Runtime.InteropServices.Marshal]::GetActiveObject('Excel.Application') } catch { $excel = New-Object -ComObject Excel.Application }; $excel.Visible = $true; $excel.Workbooks.Open('${escapedPath}')`;
        } else if (['.doc', '.docx', '.docm'].includes(ext)) {
            psScript = `try { $word = [System.Runtime.InteropServices.Marshal]::GetActiveObject('Word.Application') } catch { $word = New-Object -ComObject Word.Application }; $word.Visible = $true; $word.Documents.Open('${escapedPath}')`;
        } else if (['.ppt', '.pptx', '.pptm'].includes(ext)) {
            psScript = `try { $ppt = [System.Runtime.InteropServices.Marshal]::GetActiveObject('PowerPoint.Application') } catch { $ppt = New-Object -ComObject PowerPoint.Application }; $ppt.Visible = 1; $ppt.Presentations.Open('${escapedPath}')`;
        }

        if (!psScript) {
            resolve();
            return;
        }

        const command = `powershell -NoProfile -ExecutionPolicy Bypass -Command "${psScript}"`;
        exec(command, (error, _stdout, stderr) => {
            if (error) {
                vscode.window.showErrorMessage(`ファイルを開けませんでした: ${error.message}`);
                console.error('Error:', error);
                console.error('stderr:', stderr);
            }
            resolve();
        });
    });
}

/**
 * Office系ファイルを読み取り専用で開く(PowerShell + COM)
 */
function openOfficeFileReadOnly(filePath: string, ext: string): Promise<void> {
    return new Promise((resolve) => {
        // パスのエスケープ処理(PowerShell用)
        // シングルクォーテーション文字列内ではバックスラッシュはエスケープ不要
        const escapedPath = filePath.replace(/'/g, "''");

        let psScript = '';

        if (['.xls', '.xlsx', '.xlsm', '.xlsb'].includes(ext)) {
            // Excel: Workbooks.Open(FileName, UpdateLinks, ReadOnly)
            psScript = `try { $excel = [System.Runtime.InteropServices.Marshal]::GetActiveObject('Excel.Application') } catch { $excel = New-Object -ComObject Excel.Application }; $excel.Visible = $true; $excel.Workbooks.Open('${escapedPath}', 0, $true)`;
        } else if (['.doc', '.docx', '.docm'].includes(ext)) {
            // Word: Documents.Open(FileName, ConfirmConversions, ReadOnly, AddToRecentFiles)
            // $true/$falseではなく数値で指定(0=false, -1=true)
            psScript = `try { $word = [System.Runtime.InteropServices.Marshal]::GetActiveObject('Word.Application') } catch { $word = New-Object -ComObject Word.Application }; $word.Visible = $true; $word.Documents.Open('${escapedPath}', 0, -1, 0)`;
        } else if (['.ppt', '.pptx', '.pptm'].includes(ext)) {
            // PowerPoint: Presentations.Open(FileName, ReadOnly, Untitled, WithWindow)
            // MsoTriState: msoTrue=1, msoFalse=0
            psScript = `try { $ppt = [System.Runtime.InteropServices.Marshal]::GetActiveObject('PowerPoint.Application') } catch { $ppt = New-Object -ComObject PowerPoint.Application }; $ppt.Visible = 1; $ppt.Presentations.Open('${escapedPath}', 1, 0, 1)`;
        }

        if (!psScript) {
            resolve();
            return;
        }

        const command = `powershell -NoProfile -ExecutionPolicy Bypass -Command "${psScript}"`;
        exec(command, (error, _stdout, stderr) => {
            if (error) {
                vscode.window.showErrorMessage(`ファイルを開けませんでした: ${error.message}`);
                console.error('Error:', error);
                console.error('stderr:', stderr);
            }
            resolve();
        });
    });
}

一括起動後のカーソル移動

複数行一括起動が完了した後、元のエディタに戻って次の行へカーソルを移動する処理です(単一行実行時はこの関数は呼ばれません)。

/**
 * 元のエディタをアクティブにして1行下の行頭にカーソルを移動する
 */
async function moveCursorToNextLine(document: vscode.TextDocument, currentLine: number) {
    try {
        const editor = await vscode.window.showTextDocument(document, {
            preview: false,
            preserveFocus: false
        });
        const nextLine = Math.min(currentLine + 1, document.lineCount - 1);
        const newPosition = new vscode.Position(nextLine, 0);
        editor.selection = new vscode.Selection(newPosition, newPosition);
        editor.revealRange(new vscode.Range(newPosition, newPosition));
    } catch (_error) {
        // 元のエディタが既に閉じている等の場合は無視
    }
}

新機能:親フォルダを開く

/**
 * カーソル下のパスの親フォルダをExplorerで開く
 */
export async function openParentFolder() {
    const editor = vscode.window.activeTextEditor;
    if (!editor) {
        vscode.window.showWarningMessage('アクティブなエディタがありません');
        return;
    }

    const line = editor.document.lineAt(editor.selection.active.line);
    let text = line.text.trim();
    text = preprocessPath(text);
    text = resolveAbsolutePath(text, editor.document.uri.fsPath);

    if (!fs.existsSync(text)) {
        vscode.window.showErrorMessage(`パスが見つかりません: ${text}`);
        return;
    }

    const parentFolder = path.dirname(text);
    openInExplorer(parentFolder);
    vscode.window.showInformationMessage(`親フォルダを開きました: ${parentFolder}`);
}

新機能:新規Officeファイル作成

/**
 * カーソル下のパスにOffice系ファイルを新規作成
 */
export async function createNewOfficeFile() {
    const editor = vscode.window.activeTextEditor;
    if (!editor) {
        vscode.window.showWarningMessage('アクティブなエディタがありません');
        return;
    }

    const line = editor.document.lineAt(editor.selection.active.line);
    let text = line.text.trim();
    text = preprocessPath(text);
    text = resolveAbsolutePath(text, editor.document.uri.fsPath);

    const ext = path.extname(text).toLowerCase();
    if (!isOfficeFile(ext)) {
        vscode.window.showErrorMessage('Office系ファイル(Excel/Word/PowerPoint)の拡張子を指定してください');
        return;
    }

    // ファイルが既に存在する場合は確認
    if (fs.existsSync(text)) {
        const answer = await vscode.window.showWarningMessage(
            `ファイルが既に存在します: ${path.basename(text)}\n上書きしますか?`,
            'はい',
            'いいえ'
        );
        if (answer !== 'はい') {
            return;
        }
    }

    // 親フォルダが存在しない場合は作成
    const parentFolder = path.dirname(text);
    if (!fs.existsSync(parentFolder)) {
        fs.mkdirSync(parentFolder, { recursive: true });
    }

    await createOfficeFile(text, ext);
}

使い方

1. 拡張機能のインストール

# GitHubからクローン
git clone https://github.com/xxxxx-sys/my-macros.git
cd my-macros

# 依存関係インストール
npm install --legacy-peer-deps

# パッケージ化
npm run compile
npx vsce package

# インストール
code --install-extension my-macros-0.0.12.vsix

2. 基本的な使い方

作業メモ.txt
---
設計書: C:\work\documents\設計書_v1.0.docx
手順書: C:\work\manual\操作手順.xlsx

通常モードで開く(テキスト→VSCode / Office→通常起動):

  1. パスの行にカーソルを置く
  2. Ctrl+F12

ReadOnly / 関連付けアプリで開く:

  1. パスの行にカーソルを置く
  2. Ctrl+Shift+F12

親フォルダをExplorerで開く:

  1. パスの行にカーソルを置く
  2. Ctrl+Alt+F12

選択テキストをパスとして使う:

  1. パス文字列を選択
  2. Ctrl+F12 または Ctrl+Shift+F12

3. 実践例

シーン1: 複数ファイルを一括で参照(複数行選択+一括起動)

タスク一覧.txt
---
■ 今日のタスク
1. 設計書レビュー: C:\work\設計書_v2.0.docx
2. データ分析: C:\work\分析結果.xlsx
3. 報告書作成: C:\work\報告書_draft.docx

↑ 1〜3行をまとめて選択してCtrl+Shift+F12 → 3ファイルとも読み取り専用で一括起動

シーン2: 共有フォルダのファイルを安全に閲覧

共有資料.txt
---
■ 参考資料(編集禁止)
\\server\share\templates\提案書テンプレート.pptx ← Ctrl+Shift+F12(読み取り専用)
\\server\share\manual\操作手順_最新版.docx       ← Ctrl+Shift+F12(読み取り専用)

シーン3: テキストファイルをサクラエディタで開く

ソース一覧.txt
---
C:\work\src\main.cbl    ← Ctrl+Shift+F12(サクラエディタで開く)
C:\work\src\sub.pco     ← Ctrl+Shift+F12(サクラエディタで開く)

シーン4: バックアップファイルを確認

バージョン管理.txt
---
■ 過去バージョン
現行: C:\work\契約書_v3.0.docx    ← Ctrl+F12(編集可能)
前回: C:\backup\契約書_v2.0.docx  ← Ctrl+Shift+F12(読み取り専用)

package.jsonの設定

{
  "contributes": {
    "commands": [
      {
        "command": "myMacros.openPath",
        "title": "Open Path Under Cursor",
        "category": "My Macros"
      },
      {
        "command": "myMacros.openPathReadOnly",
        "title": "Open Path Under Cursor (ReadOnly / Associated App)",
        "category": "My Macros"
      },
      {
        "command": "myMacros.openParentFolder",
        "title": "Open Parent Folder",
        "category": "My Macros"
      },
      {
        "command": "myMacros.createNewOfficeFile",
        "title": "Create New Office File",
        "category": "My Macros"
      }
    ],
    "keybindings": [
      {
        "command": "myMacros.openPath",
        "key": "ctrl+f12",
        "when": "editorTextFocus"
      },
      {
        "command": "myMacros.openPathReadOnly",
        "key": "ctrl+shift+f12",
        "when": "editorTextFocus"
      },
      {
        "command": "myMacros.openParentFolder",
        "key": "ctrl+alt+f12",
        "when": "editorTextFocus"
      },
      {
        "command": "myMacros.createNewOfficeFile",
        "key": "ctrl+shift+alt+f12",
        "when": "editorTextFocus"
      }
    ]
  }
}

メリット・デメリット

メリット

✅ 圧倒的な効率化

  • 1キー操作でファイルを開ける
  • エクスプローラーでパスをコピペする手間が不要

✅ 誤編集の防止

  • 読み取り専用モードで安全に閲覧
  • 共有フォルダのファイルも安心

✅ サクラエディタ代替の素早い起動

  • COBOLソースやテキストを関連付けアプリで1キー起動
  • 起動中もVSCodeのフォーカスを維持

✅ バージョン非依存

  • COMオブジェクト経由でOfficeのバージョンに関係なく動作
  • Office 2016/2019/Microsoft 365すべて対応

✅ PERSONAL.XLSBのロック競合を回避

  • GetActiveObjectで既存Excelインスタンスを再利用
  • セキュリティ通知も発生しない

✅ 連続起動に最適

  • 外部アプリはバックグラウンド起動でVSCodeのフォーカスを維持
  • 実行後は自動的に次の行へ移動
  • リスト形式のパスを上から順番に一気に開ける

✅ 柔軟なパス記述

  • 絶対パス、相対パス、ネットワークパス対応
  • タブ区切り、引用符付きも自動処理
  • 選択テキストをパスとして使用可能

デメリット

❌ Windows専用(一部機能)

  • PowerShell + COM APIを使用しているためWindows限定
  • Mac/Linuxでは読み取り専用機能は動作しない(通常オープンは可能)

❌ Office系以外の読み取り専用は非対応

  • PDFはデフォルトアプリで開くのみ

トラブルシューティング

ファイルが開かない

原因1: パスが認識されない

解決策: パスの前後に余分な文字がないか確認
NG: 設計書: C:\work\設計書.docx(余計な文字あり)
OK: C:\work\設計書.docx

原因2: 相対パスの基準が異なる

解決策: 絶対パスで記述するか、ワークスペースを開く
優先順位: 1.現在ファイルのディレクトリ 2.ワークスペースフォルダ

原因3: Officeがインストールされていない

解決策: Office系ファイルを開くにはOfficeが必要

読み取り専用で開かない

原因: PowerShellの実行ポリシー

# 確認
Get-ExecutionPolicy

# 設定(管理者権限で実行)
Set-ExecutionPolicy RemoteSigned

外部アプリ起動時にVSCodeのフォーカスが奪われる

openWithAssociatedAppSW_SHOWNOACTIVATEが正しく動作していない可能性があります。PowerShellのバージョンや環境によっては挙動が異なる場合があります。

まとめ

VSCodeでカーソル下のパスからファイル/フォルダを一発で開く機能を実装しました。

主な機能:

ショートカット動作
Ctrl+F12テキスト→VSCodeで開く、Office→通常モード起動
Ctrl+Shift+F12テキスト→関連付けアプリ(サクラエディタ等)、Office→読み取り専用
Ctrl+Alt+F12親フォルダをExplorerで開く
Ctrl+Shift+Alt+F12新規Officeファイル作成
Ctrl+F12 / Ctrl+Shift+F12(複数行選択時)選択範囲のパスを一括起動(10件以上は確認あり)

タブ区切りで階層的に並んだテキスト(呼び出し元・呼び出し先のスクリプト一覧など)からも、パスの位置に関わらず検出して起動できます。

この機能が役立つ人:

  • VSCodeでドキュメント管理をしている
  • 作業メモにファイルパスを書いている
  • 共有フォルダのファイルを頻繁に参照する
  • 誤編集を防ぎたい
  • サクラエディタから移行したい
  • 複数ファイルを連続して開く作業が多い

ぜひ試してみてください!

関連記事

移行の経緯と環境構築

サクラエディタからVSCodeへマクロ移行!快適開発環境の構築記録
はじめに長年愛用してきたサクラエディタのマクロ機能。便利なJavaScript/VBSマクロを多数作成して日常業務で活用してきましたが、最近のAWS開発やブログ執筆でVSCodeを使う機会が増えてきました。「VSCodeでもサクラエディタの...

詳細なセットアップ手順

VSCode TypeScriptマクロ開発環境の完全ガイド【セットアップから運用まで】
はじめにVSCodeでカスタムマクロを作成したいけど、どうやって開発環境を構築すればいいか分からない。そんな悩みを持つ方に向けて、TypeScriptでVSCode拡張機能を開発する環境の構築から、実際にマクロを作成して使えるようにするまで...

タグ: #VSCode #TypeScript #マクロ #Excel #Word #PowerPoint #生産性向上 #Office #読み取り専用

コメント