VSCodeで「フォルダ一覧取得」を実現するTypeScriptマクロ【ファイル管理が10倍速くなる】

Development
スポンサーリンク
スポンサーリンク

はじめに

プロジェクト管理や作業記録で、こんな悩みありませんか?

作業メモ.txt
---
■ タスク
プロジェクトフォルダのファイル構成をドキュメント化
→ C:\work\my-project のファイル一覧を取得したい

■ 課題
共有フォルダの棚卸し
→ \\server\share\old-projects のファイル数とサイズを確認したい

「フォルダ配下のファイル一覧が欲しい」
「サイズや更新日時も記録したい」
「再帰的に全サブフォルダも取得したい」

Windowsのエクスプローラーでは、ファイル一覧をテキストでコピーできません。コマンドプロンプトでdirコマンド…も面倒。

今回、カーソル行のフォルダパスから一覧を取得し、別ファイルに出力するTypeScriptマクロを実装しました。

シンプル版と詳細版の2モードで、実務でのファイル管理が劇的に効率化します。

なぜこのツールが必要なのか?

Windowsの標準機能では実現困難

エクスプローラーの限界:

  • ファイル一覧をテキストでコピーできない
  • ドラッグ&ドロップは画像のみ
  • パスのリストは手動でコピペが必要

コマンドプロンプトの課題:

dir /s /b C:\work\my-project > list.txt
  • パス指定が面倒
  • オプションを毎回調べる必要
  • 出力フォーマットの調整が困難

PowerShellでも:

Get-ChildItem -Recurse | Out-File list.txt
  • コマンドが長い
  • カスタマイズが複雑
  • 毎回コマンドを打つのが手間

このマクロで実現できること

項目エクスプローラーコマンドラインこのマクロ
ファイル一覧❌ コピー不可⚠️ コマンド必要✅ 1キー
サイズ・日時❌ 手動⚠️ フォーマット調整✅ 自動
再帰検索❌ 不可✅ 可能✅ モード選択
安全装置-❌ なし✅ タイムアウト等
操作数手動コピペコマンド入力1キー操作

実装した機能

主な機能

ショートカット:

  • Ctrl+F11: シンプル版(パスのみ)
  • Ctrl+Shift+F11: 詳細版(サイズ・日時付き)

2つの出力モード

  • シンプル版:パスのみをリスト化
  • 詳細版:ファイルサイズ+更新日時付き

4つの取得モード(ダイアログで選択)

  1. 直下のみ(フォルダ + ファイル)← 最も安全
  2. 直下のみ(ファイルのみ)
  3. 再帰的(フォルダ + ファイル)← 警告付き
  4. 再帰的(ファイルのみ)← 警告付き

充実の安全装置

  • 再帰検索時の確認ダイアログ
  • ネットワークドライブ検出&警告
  • タイムアウト:60秒
  • 最大件数:50,000件(超過時は切り詰め)
  • プログレス表示+キャンセル可能
  • node_modules, .git, dist等を自動除外

柔軟なパス指定

  • ファイルパス → 親フォルダを対象
  • フォルダパス → そのフォルダを対象
  • 絶対パス、相対パス、ネットワークパス対応

出力例

シンプル版(Ctrl+F11)

============================================================
フォルダ一覧取得結果
============================================================
実行日時: 2024-02-08 15:30:45
対象フォルダ: C:\work\my-project
モード: 直下のみ(フォルダ + ファイル)
取得件数: 25 件
============================================================

C:\work\my-project\src
C:\work\my-project\test
C:\work\my-project\package.json
C:\work\my-project\README.md
C:\work\my-project\tsconfig.json
...

詳細版(Ctrl+Shift+F11)

============================================================
フォルダ一覧取得結果
============================================================
実行日時: 2024-02-08 15:30:45
対象フォルダ: C:\work\my-project
モード: 再帰的(ファイルのみ)
取得件数: 234 件
============================================================

[DIR ] C:\work\my-project\src
[FILE] C:\work\my-project\src\main.ts          (12.5 KB) 2024-02-01
[FILE] C:\work\my-project\src\util.ts          (8.2 KB)  2024-02-05
[DIR ] C:\work\my-project\test
[FILE] C:\work\my-project\test\test.ts         (4.1 KB)  2024-02-03
[FILE] C:\work\my-project\package.json         (1.2 KB)  2024-01-30
...

実務での活用例

例1:プロジェクトのファイル構成をドキュメント化

シーン:READMEにファイル構成を記載

README.md作成タスク
---
プロジェクト: C:\work\api-server

手順:

  1. カーソルをパスの行に置く
  2. Ctrl+F11 → 直下のみ(全て)
  3. 結果をREADMEにコピペ

成果:

## ファイル構成
src/
  main.ts       - エントリーポイント
  routes/       - APIルート定義
  models/       - データモデル
test/           - テストコード
package.json    - 依存関係

例2:共有フォルダの棚卸し

シーン:不要ファイルの洗い出し

棚卸しタスク
---
対象: \\server\share\old-projects

手順:

  1. Ctrl+Shift+F11 → 再帰的(ファイルのみ)
  2. 警告確認 → 続行
  3. サイズと日時で古いファイルを特定

成果:

[FILE] \\server\share\old-projects\backup_2020.zip  (500 MB) 2020-01-15
[FILE] \\server\share\old-projects\temp.dat         (1.2 GB) 2019-08-22
→ 不要ファイルを削除して1.7GB削減

例3:納品物のファイルリスト作成

シーン:納品前のファイルリスト作成

納品準備
---
成果物フォルダ: C:\delivery\client-system-v1.0

手順:

  1. Ctrl+F11 → 再帰的(全て)
  2. ファイルリストを納品書に添付

成果:

【納品物一覧】
src/           - ソースコード(50ファイル)
docs/          - 設計書・マニュアル(12ファイル)
installer/     - インストーラー(3ファイル)
合計:65ファイル

例4:バックアップ前の記録

シーン:バックアップ前のスナップショット

バックアップ計画
---
対象: C:\important-data

手順:

  1. Ctrl+Shift+F11 → 再帰的(詳細版)
  2. ファイル一覧をバックアップと一緒に保存

成果:

  • バックアップ時のファイル構成を記録
  • 復旧時に何があったか確認可能
  • サイズと日時で変更箇所を追跡

例5:プロジェクトの進捗報告

シーン:週次報告でのファイル数増加報告

先週: 150ファイル
今週: 234ファイル
→ 84ファイル増加(機能追加により)

手順:

  1. 毎週同じフォルダでCtrl+F11実行
  2. ファイル数の推移を記録
  3. 報告書に添付

TypeScriptコード解説

メイン処理(listFolderContents.ts)

import * as vscode from 'vscode';
import * as path from 'path';
import * as fs from 'fs';

/**
 * 出力モード
 */
enum OutputMode {
    Simple,   // シンプル(パスのみ)
    Detailed  // 詳細(サイズ・日時付き)
}

/**
 * 一覧取得オプション
 */
interface ListOptions {
    recursive: boolean;    // 再帰的に取得するか
    filesOnly: boolean;    // ファイルのみか
}

/**
 * フォルダ一覧取得(シンプル版)
 */
export async function listFolderContentsSimple() {
    await listFolderContents(OutputMode.Simple);
}

/**
 * フォルダ一覧取得(詳細版)
 */
export async function listFolderContentsDetailed() {
    await listFolderContents(OutputMode.Detailed);
}

/**
 * メイン処理
 */
async function listFolderContents(mode: OutputMode) {
    const editor = vscode.window.activeTextEditor;
    if (!editor) {
        vscode.window.showWarningMessage('アクティブなエディタがありません');
        return;
    }

    // カーソル行からパスを取得
    const line = editor.document.lineAt(editor.selection.active.line);
    let targetPath = line.text.trim();

    // パス前処理
    targetPath = preprocessPath(targetPath);
    targetPath = resolveAbsolutePath(targetPath, editor.document.uri.fsPath);

    // 存在チェック
    if (!fs.existsSync(targetPath)) {
        vscode.window.showErrorMessage(`パスが見つかりません: ${targetPath}`);
        return;
    }

    // フォルダパスに変換(ファイルの場合は親フォルダ)
    const folderPath = getFolderPath(targetPath);

    // オプション選択
    const options = await showOptionsDialog();
    if (!options) return;

    // 再帰的検索の場合は警告
    if (options.recursive) {
        const confirmed = await showWarningDialog(folderPath);
        if (!confirmed) return;
    }

    // 実行
    await executeWithProgress(folderPath, options, mode);
}

オプション選択ダイアログ

/**
 * オプション選択ダイアログ
 */
async function showOptionsDialog(): Promise<ListOptions | null> {
    const items = [
        {
            label: '$(file-directory) 直下のみ(フォルダ + ファイル)',
            description: '最も安全。1階層のみ取得',
            recursive: false,
            filesOnly: false
        },
        {
            label: '$(file) 直下のみ(ファイルのみ)',
            description: '最も安全。1階層のファイルのみ',
            recursive: false,
            filesOnly: true
        },
        {
            label: '$(repo) 再帰的(フォルダ + ファイル)',
            description: '⚠️ 時間がかかる可能性あり',
            recursive: true,
            filesOnly: false
        },
        {
            label: '$(files) 再帰的(ファイルのみ)',
            description: '⚠️ 時間がかかる可能性あり',
            recursive: true,
            filesOnly: true
        }
    ];

    const selected = await vscode.window.showQuickPick(items, {
        placeHolder: '一覧取得のモードを選択してください',
        ignoreFocusOut: true
    });

    if (!selected) return null;

    return {
        recursive: selected.recursive,
        filesOnly: selected.filesOnly
    };
}

警告ダイアログ(再帰検索時)

/**
 * 警告ダイアログ
 */
async function showWarningDialog(folderPath: string): Promise<boolean> {
    let warningMessage = '⚠️ 再帰的検索を実行します。\n';

    // ネットワークドライブ警告
    if (isNetworkPath(folderPath)) {
        warningMessage += '\n⚠️ ネットワークドライブが検出されました。\n' +
                         '処理に非常に時間がかかる可能性があります。\n';
    }

    warningMessage += '\n続行しますか?';

    const answer = await vscode.window.showWarningMessage(
        warningMessage,
        { modal: true },
        '続行', 'キャンセル'
    );

    return answer === '続行';
}

/**
 * ネットワークパスかどうか判定
 */
function isNetworkPath(p: string): boolean {
    return p.startsWith('\\\\') || p.startsWith('//');
}

プログレス表示とタイムアウト

/**
 * プログレス表示付きで実行
 */
async function executeWithProgress(
    folderPath: string,
    options: ListOptions,
    mode: OutputMode
) {
    await vscode.window.withProgress({
        location: vscode.ProgressLocation.Notification,
        title: "フォルダ一覧を取得中...",
        cancellable: true
    }, async (progress, token) => {
        try {
            let isCancelled = false;

            // キャンセルトークン
            token.onCancellationRequested(() => {
                isCancelled = true;
            });

            // タイムアウトPromise(60秒)
            const timeoutPromise = new Promise<never>((_, reject) => {
                setTimeout(() => reject(new Error('タイムアウト(60秒)')), 60000);
            });

            // ファイル一覧取得Promise
            const listPromise = listFiles(folderPath, options, mode, (count) => {
                progress.report({
                    message: `${count}件取得済み...`,
                    increment: 1
                });
                return isCancelled;
            });

            // タイムアウトとレース
            const result = await Promise.race([listPromise, timeoutPromise]);

            if (isCancelled) {
                vscode.window.showInformationMessage('処理をキャンセルしました');
                return;
            }

            // 結果を出力
            await createResultFile(folderPath, options, result.files, result.truncated, mode);

            let message = `取得完了: ${result.files.length}件`;
            if (result.truncated) {
                message += '(上限50,000件に達したため切り詰め)';
            }
            vscode.window.showInformationMessage(message);

        } catch (error: any) {
            if (error.message.includes('タイムアウト')) {
                vscode.window.showErrorMessage('処理がタイムアウトしました(60秒)');
            } else {
                vscode.window.showErrorMessage(`エラー: ${error.message}`);
            }
        }
    });
}

再帰的ファイル取得

/**
 * 除外するフォルダ名
 */
const EXCLUDE_PATTERNS = [
    'node_modules',
    '.git',
    '.vscode',
    'target',
    'dist',
    'build',
    'out',
    'bin',
    '.idea'
];

/**
 * 最大件数
 */
const MAX_FILES = 50000;

/**
 * 再帰的にファイル一覧を取得
 */
async function listFilesRecursive(
    folderPath: string,
    files: FileInfo[],
    options: ListOptions,
    mode: OutputMode,
    onProgress: (count: number) => boolean
) {
    // キャンセルチェック
    if (onProgress(files.length)) return;

    // 最大件数チェック
    if (files.length >= MAX_FILES) return;

    try {
        const entries = fs.readdirSync(folderPath, { withFileTypes: true });

        for (const entry of entries) {
            const fullPath = path.join(folderPath, entry.name);

            if (entry.isDirectory()) {
                // 除外パターンチェック
                if (EXCLUDE_PATTERNS.includes(entry.name)) {
                    continue;
                }

                if (!options.filesOnly) {
                    files.push(createFileInfo(fullPath, true, mode));
                }

                // 再帰
                await listFilesRecursive(fullPath, files, options, mode, onProgress);
            } else {
                files.push(createFileInfo(fullPath, false, mode));
            }

            // 最大件数チェック
            if (files.length >= MAX_FILES) return;
        }
    } catch (error: any) {
        // アクセス権限エラーなどは無視
        console.error(`Error accessing ${folderPath}:`, error.message);
    }
}

出力フォーマット生成

/**
 * 出力内容を生成
 */
function generateOutput(
    folderPath: string,
    options: ListOptions,
    files: FileInfo[],
    truncated: boolean,
    mode: OutputMode
): string {
    const lines: string[] = [];

    // ヘッダー
    lines.push('='.repeat(60));
    lines.push('フォルダ一覧取得結果');
    lines.push('='.repeat(60));
    lines.push(`実行日時: ${formatDateTime(new Date())}`);
    lines.push(`対象フォルダ: ${folderPath}`);
    lines.push(`モード: ${getModeText(options)}`);
    lines.push(`取得件数: ${files.length.toLocaleString()} 件`);

    if (truncated) {
        lines.push('⚠️ 上限50,000件に達したため切り詰めました');
    }

    lines.push('='.repeat(60));
    lines.push('');

    // ファイル一覧
    if (mode === OutputMode.Simple) {
        // シンプル版:パスのみ
        files.forEach(file => {
            lines.push(file.path);
        });
    } else {
        // 詳細版:サイズ・日時付き
        files.forEach(file => {
            const type = file.isDirectory ? '[DIR ]' : '[FILE]';
            const sizeStr = file.size !== undefined ? formatSize(file.size) : '';
            const dateStr = file.modifiedDate ? formatDate(file.modifiedDate) : '';

            let line = `${type} ${file.path}`;
            if (sizeStr) line += `  (${sizeStr})`;
            if (dateStr) line += ` ${dateStr}`;

            lines.push(line);
        });
    }

    return lines.join('\n');
}

/**
 * ファイルサイズをフォーマット
 */
function formatSize(bytes: number): string {
    if (bytes < 1024) return `${bytes} B`;
    if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
    if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
    return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
}

使い方

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.6.vsix

2. 基本的な使い方

作業メモ.txt
---
プロジェクト: C:\work\my-project
共有フォルダ: \\server\share\documents

シンプル版(パスのみ):

  1. パスの行にカーソルを置く
  2. Ctrl+F11
  3. モード選択(直下のみ or 再帰的)
  4. 結果が右側に表示
  5. 保存ダイアログで保存

詳細版(サイズ・日時付き):

  1. パスの行にカーソルを置く
  2. Ctrl+Shift+F11
  3. モード選択(直下のみ or 再帰的)
  4. 再帰の場合は警告確認
  5. 結果が右側に表示
  6. 保存ダイアログで保存

3. 実践例

シーン1: プロジェクトの構成確認

メモ.txt
---
C:\work\api-server

手順:

  1. カーソルをパスに置く
  2. Ctrl+F11 → 直下のみ(全て)
  3. フォルダ構成を確認

シーン2: 大量ファイルの棚卸し

棚卸し.txt
---
\\server\share\old-projects

手順:

  1. Ctrl+Shift+F11 → 再帰的(ファイルのみ)
  2. 警告確認 → 続行
  3. サイズと日時で古いファイルを特定

シーン3: 納品物リスト作成

納品準備.txt
---
C:\delivery\system-v1.0

手順:

  1. Ctrl+F11 → 再帰的(全て)
  2. ファイルリストを納品書に添付

package.jsonの設定

{
  "contributes": {
    "commands": [
      {
        "command": "myMacros.listFolderContentsSimple",
        "title": "List Folder Contents (Simple)",
        "category": "My Macros"
      },
      {
        "command": "myMacros.listFolderContentsDetailed",
        "title": "List Folder Contents (Detailed)",
        "category": "My Macros"
      }
    ],
    "keybindings": [
      {
        "command": "myMacros.listFolderContentsSimple",
        "key": "ctrl+f11",
        "when": "editorTextFocus"
      },
      {
        "command": "myMacros.listFolderContentsDetailed",
        "key": "ctrl+shift+f11",
        "when": "editorTextFocus"
      }
    ]
  }
}

メリット・デメリット

メリット

✅ 圧倒的な効率化

  • 1キー操作でファイル一覧取得
  • エクスプローラーでの手作業が不要
  • コマンドを覚える必要なし

✅ 充実の安全装置

  • タイムアウト(60秒)で暴走防止
  • 最大50,000件で自動停止
  • ネットワークドライブ警告
  • キャンセル可能なプログレス表示

✅ 柔軟な出力モード

  • シンプル版:パスのみ
  • 詳細版:サイズ・日時付き
  • 用途に応じて使い分け

✅ 自動除外機能

  • node_modules, .git等を自動スキップ
  • 不要なフォルダを除外して高速化

✅ 実行履歴の保存

  • 実行日時を記録
  • ファイル名に日時を自動付与
  • 定期的な記録に最適

デメリット

❌ 巨大フォルダは時間がかかる

  • 数万ファイルは数十秒待つ
  • ネットワークドライブは特に遅い
  • タイムアウト(60秒)で自動停止

❌ リアルタイム監視は非対応

  • スナップショット取得のみ
  • ファイル変更の自動検知なし

❌ アクセス権限エラー

  • 権限のないフォルダはスキップ
  • エラーは通知されない

トラブルシューティング

Q: パスが見つからない

A: パスの記述を確認

NG: プロジェクト: C:\work\project(余計な文字)
OK: C:\work\project

NG: "C:\work\project"(引用符付き)→ 自動除去されるが、余計な文字があると失敗

解決策: パスのみの行にする

Q: 処理が途中で止まる

A: タイムアウトまたは最大件数に達した

タイムアウト: 60秒経過で自動停止
最大件数: 50,000件で自動切り詰め

解決策:
- 直下のみで試す
- 対象フォルダを絞る
- 除外パターンを活用

Q: ネットワークドライブが遅い

A: 再帰検索を避ける

推奨: 直下のみで取得
非推奨: 再帰的(数分かかる可能性)

解決策:
- まず直下のみで確認
- サブフォルダを個別に処理

Q: node_modulesが除外される

A: 除外パターンを変更

// 除外したくない場合はコードを修正
const EXCLUDE_PATTERNS = [
    // 'node_modules', ← コメントアウト
    '.git',
    '.vscode'
];

Q: ファイルが保存できない

A: パスに書き込み権限があるか確認

NG: C:\Program Files\...(権限不足)
OK: C:\Users\[ユーザー名]\Documents\...

解決策: 書き込み可能なフォルダに保存

まとめ

VSCodeでカーソル行のフォルダパスから一覧を取得し、別ファイルに出力する機能を実装しました。

この機能が役立つ人:

  • プロジェクトのファイル構成をドキュメント化したい
  • 共有フォルダの棚卸しをしたい
  • 納品物のファイルリストを作成したい
  • バックアップ前の記録を残したい
  • ファイル管理の効率を上げたい

特にエクスプローラーでは不可能なファイル一覧のテキスト化を1キーで実現できるため、実務での生産性向上に大きく貢献します。

プロジェクト管理、作業記録、納品準備、棚卸しなど、様々なシーンで活用できる実用的なツールです。

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

関連記事

サクラエディタからの移行経緯

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

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

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

正規表現マッチ抽出ツール

VSCodeで「正規表現マッチ抽出」を実現するTypeScriptマクロ【ログ解析が10倍速くなる】
はじめにサーバーログやアプリケーションログを解析する時、こんな悩みありませんか?2024-02-08 10:00:00 INFO: Server started2024-02-08 10:01:00 ERROR: Connection fa...

タグ: #VSCode #TypeScript #マクロ #ファイル管理 #生産性向上 #フォルダ一覧

コメント