閑古鳥

オールドプログラマの日記。プログラミングとか病気(透析)の話とか。

vscode-journalで初回の「Open Today(今日のファイルを開く)」コマンドの実行が遅い / のでコマンドを作ってしまった

VSCodeを起動してjournal.todayコマンドを実行すると、数秒待たされる。原因はmdファイルを初めて開いた時にMarkdown関係の拡張が有効化されるが、それが終わるまでコマンドが完了しないため。
普通にmdファイルを開くと、ファイルを開いて編集可能になってから拡張の読み込みが始まるので待たされていると感じない。この快適さがほしい。

vscode-journalで使うファイルの拡張子をmd以外にするとか、Markdownの拡張を減らすという回避策もあるけど、それは残念感が強い。Emacsのごとく最初だけなんだから気にしない、というのがベターっぽいけども、暇なので少し調べてみた。

ソースを見るとどうやら:

const doc = await vscode.workspace.openTextDocument(path); // ファイルを開いて
vscode.window.showTextDocument(doc);                       // アクティブにする

みたいなコードを実行すると拡張の読み込みが終わるまで処理を返さないらしい。VSCodeにわかなので何もわからないのだけど、openTextDocumentを待たずにonDidOpenTextDocumentイベントを使うといいのだろうか?

いっちょvscode-journalのソース書き換えてPRでも送ってみようかと思ったけど、ソースを読むとopenTextDocumentの戻り値で得られるドキュメントにテンプレート書き込んだりしていて、案外変更量多くなりそう……と気付いたらvscode-powertoolsで自前のコマンドを作ってしまいました。VSCodeではファイルを開く方法がいくつかあるみたいで、上記の他に

vscode.commands.executeCommand("vscode.open", vscode.Uri.file(ファイル名));

でも開けて、こちらは拡張の読み込みを非同期で行ってくれるバージョンのようなので、こっちを使ったコマンドを作ってみました。

journal.js

Power Tools - Visual Studio Marketplace

まずPower Toolsをインストールして、 ~/.vscode-powertools/直下にjournal.jsファイルを作ります。

// ~/.vscode-powertools/journal.js
function getJournalFile(args, offset) {
    const vscode = args.require('vscode');
    const path = args.require('path');
    const moment = args.require('moment');
    const config = vscode.workspace.getConfiguration("journal");

    moment.locale(config.get('locale')); // journal.locale

    const base = config.get('base'); // journal.base
    const ext = config.get('ext'); // journal.ext
    const day = moment(Date.now()).add(offset, 'days');
    const parent = path.join(base, day.format('YYYY'), day.format('MM'));
    return [day, parent, path.join(parent, `${day.format('YYYYMMDD')}.${ext}`)];
}

async function openJournal(args, offset) {
    const vscode = args.require('vscode');
    const fs = args.require('fs');
    const fsExtra = args.require('fs-extra');

    const [day, parent, fileName] = getJournalFile(args, offset);

    if (!fs.existsSync(fileName)) {
        fsExtra.mkdirpSync(parent);

        const config = vscode.workspace.getConfiguration("journal");
        let template = config.get('tpl-entry');  // journal.tpl-entry
        template = template.replace('${localDate}', day.format('LL'));
        template = template.replace('${weekday}', day.format('dddd'));
        fs.writeFileSync(fileName, template, err => console.error(err));
    }

    await vscode.commands.executeCommand("vscode.open", vscode.Uri.file(fileName));
}

async function pickJournalOffset(args) {
    const disposables = [];
    try {
        return await new Promise((resolve, _) => {
            const vscode = args.require('vscode');
            const input = vscode.window.createQuickPick();
            input.items = [
                {label: 'Today', parsedInput: 0, alwaysShow: true},
                {label: 'Yesterday', parsedInput: -1, alwaysShow: true},
                {label: 'Tomorrow', parsedInput: +1, alwaysShow: true}
            ];

            input.onDidAccept(() => {
                const offset = input.value.length === 0 ? input.selectedItems[0].parsedInput : parseInt(input.value);
                resolve(offset);
            }, disposables);

            input.onDidHide(() => {
                input.dispose();
            }, disposables);

            input.show();
        });
    } finally {
        disposables.forEach(d => d.dispose());
    }
}

exports.getJournalFile = (args, offset) => {
    const arr = getJournalFile(args, offset);
    return arr[arr.length - 1];
}

exports.open = async (args, offset) => {
    openJournal(args, offset);
};

exports.execute = async args => {
    const offset = await pickJournalOffset(args);
    openJournal(args, offset);
};

settings.jsonにコマンドを追加。

// settings.json
"ego.power-tools.user": {
        "commands": {
            "myjournal.open": { // 名前は何でもいい
                "name": "Open Journal file",
                "script": "journal.js",
            },
        }
    }

keybindings.jsonにショートカットを追加。

{
    "key": "ctrl+shift+j",
    "command": "myjournal.open"
},

これでCtrl+Shift+Jを押すとメニューが表示され、選んだ日のファイルが開けます(なければ作る)。

journal-prev.js

ついでに「今開いているファイルの1日前のファイルを探して開く」コマンドも作りました。Ctrl+Shift+Alt+Pにショートカットを割り当てて、連打すると昨日のファイル、一昨日のファイル、一昨昨日のファイル…と辿って見られるようにしました。あれ何日前に書いたメモだっけ、みたいな捜し物をするときに使います。(こういうの、howmとかなら検索で解決するんだろうなぁ)

// ~/.vscode-powertools/journal-prev.js
exports.execute = async args => {
    const vscode = args.require('vscode');
    const fs = args.require('fs');
    const path = args.require('path');
    const journal = require('./journal.js');

    const currentFile = vscode.window.activeTextEditor.document.uri.path;
    let offsetDays = 0; // today
    if (currentFile) { 
        // YYYYMMDD -> YYYY-MM-DD (決め打ち)
        const fileName = path.basename(currentFile, path.extname(currentFile));
        const date = new Date(`${fileName.substr(0, 4)}-${fileName.substr(4, 2)}-${fileName.substr(6, 2)}`);
        offsetDays = Math.round((Date.now() - date.getTime()) / (1000*60*60*24));
    }

    // 直近10日前まで遡って、最初に見つかったファイルを開く
    // (getJournalFileの効率が悪いし、1ヶ月空いてもいいようにしたいし、もうちょっとうまくやりたい)
    for (let i = 1; i <= 10; ++i) {
        const offset = -(i + offsetDays);
        const fileName = journal.getJournalFile(args, offset);
        if (fs.existsSync(fileName)) {
            vscode.commands.executeCommand("vscode.open", vscode.Uri.file(fileName));
            return;
        }
    }
};
// settings.json
"ego.power-tools.user": {
    "commands": {
        "myjournal.previous": {
            "name": "Open Previous Journal file",
            "script": "journal-prev.js",
        }
    }
}
// keybindings.json
{
    // Ctrl+Shift+Alt+←(right) にしようとしたらなぜか動かない🤔
    "key": "ctrl+shift+alt+P",
    "command": "myjournal.previous",
}