背景
「現在行から任意の文字列を含む行まで一括選択・コピーする」VSCodeマクロを作る際、「よく使う検索対象(空行・EOFなど)はプリセットからすぐ選びたい」が「たまに任意の文字列も検索したい」という2つの要求を1つのダイアログで両立させる必要がありました。
1. showQuickPickではなくcreateQuickPickを使う
const quickPick = vscode.window.createQuickPick();
quickPick.items = PRESET_PATTERNS;
quickPick.placeholder = '終了位置を選択または直接入力してください';
quickPick.ignoreFocusOut = true;
vscode.window.showQuickPick()は選択肢からの選択専用で、自由入力を受け付けません。createQuickPick()で低レベルAPIを直接扱うことで、入力欄にテキストを打った状態でEnterを押すという操作も拾えるようになります。
2. 選択 or 入力をイベントハンドラで分岐
quickPick.onDidAccept(() => {
const selected = quickPick.selectedItems[0];
const inputValue = quickPick.value.trim();
if (selected) {
// プリセットの選択肢をハイライトした状態でEnter
resolve(pattern?.value || null);
} else if (inputValue) {
// プリセットを選ばずテキストを入力してEnter
resolve(inputValue);
} else {
resolve(null);
}
quickPick.hide();
});
onDidAcceptが発火した時点でselectedItems[0]があるかどうかで、「候補をハイライトして選んだ」のか「そのまま自由入力した」のかを判別できます。同じイベントの中で両方のケースを吸収できるため、ダイアログを分ける必要がありません。
3. Promiseでラップしてasync/awaitに乗せる
return new Promise((resolve) => {
quickPick.onDidAccept(() => { /* ... */ resolve(value); });
quickPick.onDidHide(() => { resolve(null); quickPick.dispose(); });
quickPick.show();
});
createQuickPickはイベント駆動のAPIなので、そのままでは呼び出し側でawaitできません。Promiseでラップし、onDidAcceptとonDidHide(Escでキャンセルした場合)の両方でresolveすることで、呼び出し元はawait showPatternDialog()と書くだけで済みます。onDidHide側でquickPick.dispose()を呼び、リソースの解放も忘れずに行います。
4. 改行を含む選択範囲の作り方
const endPos = endLine < document.lineCount - 1
? new vscode.Position(endLine + 1, 0)
: new vscode.Position(endLine, document.lineAt(endLine).text.length);
選択範囲の終端を「対象行の行末」ではなく「次の行の先頭(0文字目)」にすることで、コピー時に末尾の改行までまとめて含められます。最終行の場合は次の行が存在しないため、行末位置にフォールバックします。
まとめ
createQuickPickのイベントハンドラで選択と自由入力を1つのダイアログに統合するパターンは、この機能に限らず「プリセット+カスタム値」を扱うUIで汎用的に使えます。空行検出や文字列検索を含む完全なコードは元記事にまとめています。