VSCode API - エクスプローラーのコンテキストメニューからコマンドが呼び出されたときの引数パターン

# 概要

VSCode の機能拡張でエクスプローラー上からコンテキストメニュー経由で実行するコマンドを実装した際、呼び出されるコマンドが受け取る引数の説明が公式ドキュメントに無く、またそのパターンもやや特殊であった。

[公式リファレンス](https://code.visualstudio.com/api/references/contribution-points)には `package.json` の `menu` に設定する値として `explorer/context` が使えることは書かれているが、そのコンテキストメニューからコマンドが呼び出されたときの引数についての説明がない。

ここではその調査結果をまとめる。


**調査時点での VSCode のバージョン**

Version: 1.96.2 (Universal)

## 引数について

基本的には第1引数に `vscode.Uri` 型の値、第2引数に `vscode.Uri[]` 型の値が渡される。

つまりコマンドを実行する関数の定義は以下の様な感じになる。

```ts
export function my_command_from_explorer(uri: vscode.Uri, selectedFiles?: vscode.Uri[])
{
  console.log( uri.fsPath );
}
```

`vscode.Uri` はクラス。詳細は[公式リファレンス](https://code.visualstudio.com/api/references/vscode-api#Uri)を参照。


## 引数のパターン

分かりにくい仕様として条件によって渡される引数のパターンが異なる問題があった。具体的には以下の条件が影響する。

- エクスプローラー上のどこをクリックしてコンテキストメニューを開いたか
- クリックしたファイル・ディレクトリの選択状態
- 他のファイル・ディレクトリの選択状態（無視されたり無視されなかったりする）

これらのパターンの調査結果は以下の様になる。


| コンテキストメニューを開いた場所 | 当該アイテムの選択状態 | 他の選択状態 | 第1引数         | 第2引数                             |
| ---------------- | ----------- | ------ | ------------ | -------------------------------- |
| 空白部分             | -           | 無視される  | ワークスペースフォルダー | `[]`                             |
| ファイル    | 選択状態        | 無し     | 当該ファイル       | `["当該ファイル"]`                     |
| 〃                | 非選択状態       | 無し     | 当該ファイル       | `["当該ファイル"]`                     |
| 〃                | 選択状態        | 有り     | 当該ファイル       | `["当該ファイル", ..."選択中のファイルやフォルダ"]` |
| 〃                | 非選択状態       | 有り     | 当該ファイル      | `["当該ファイル"]`                     |


_※表中の「ファイル」は「ディレクトリ」の場合も同様であるため「ファイルまたはディレクトリ」と読み替え可能_


表だと分かりにくいため、以下特徴的な仕様について説明する。


### 1. エクスプローラーの空白部分 == ワークスペースフォルダ、という判定

エクスプローラーの空白部分でコンテキストメニューを開いた場合、ワークスペースフォルダ自体が処理対象となる。

エクスプローラーの空白部分とは下図の赤点線で囲んだ部分のこと。

<img width="324" alt="エクスプローラーの空白部分とは" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36566/2d85475d-40f5-ed14-7d1d-82767cd79213.jpeg">


この時エクスプローラー上で**ファイルやフォルダが選択状態にあっても無視され、ルートフォルダのパスが渡される**。ユーザーが GUI の状態からこの内部仕様を予測するのは難しい。

もし実行するコマンドが破壊的な処理[^1]を行うならばユーザーの期待を裏切った上に、大きな損失を与える事になるだろう。

**破壊的[^1]なコマンドを提供するならばユーザーに事前に適切なダイアログを提示すべき**だろう。

#### エクスプローラーの空白部分でコンテキストメニューが開かれた事を判定する条件

調査の結果から以下の条件を満たすとき、エクスプローラーの空白部分からコンテキストメニューが起動されたと判定出来る。

1. 第1引数がワークスペースフォルダを指している
2. 第2引数が空の配列である

プログラム側はこの条件を元に警告を出すことが出来る。


#### 悩みどころ

- そもそも第2引数が空のリストになるため、第2引数だけを参照する様にすれば「処理対象無し」とすることも出来る
- しかし、ルートフォルダ全体にコマンドを適用したいケースもある場合、第2引数だけを見る方法は採用できない



### 2. 選択状態のファイルが存在してもクリックした位置で渡されるファイルが異なる


例えばエクスプローラーの選択状態が下図の状態を例とする。

<img width="324" alt="選択状態のあるエクスプローラー" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36566/e6d704cf-fe9e-57b4-2252-ea52a8dc12f0.jpeg">

この時 `memo.md` を右クリックしてコンテキストメニューからコマンドを実行した場合、第1引数にも第2引数にも `memo.md` しか渡されない。

つまり以下の様になる。

- **第1引数**: `memo.md`
- **第2引数**:
  - `memo.md`

しかし `edited-state.diff` の上でコンテキストメニューを開いた場合以下の様になる。

- **第1引数**: `edited-state.diff`
- **第2引数**:
  - `sub-dir/buzz.md`
  - `edited-state.diff`
  - `in-padding-lines.md`
  
_※ 読みやすさのために相対パスにしているが実際には絶対パスで渡される_  

この仕様は比較的理解できる仕様であり、また破壊的な処理[^1]であってもポインタが指し示すファイル・ディレクトリのみが対象となるためユーザーの意図に反することは無いだろう。

選択された他のファイル・ディレクトリが対象とならないことについての疑問も、再度の選択されたファイル・ディレクトリ上から実行すれば処理対象となるため、比較的短期間で経験的な理解を獲得すると思われる。


#### この仕様についてまとめると

| 右クリックしたファイル・ディレクトリの状態が: | 第2引数の値 |
|-----|------|
| 選択状態である | **他の選択状態のファイル・ディレクトリも含んだリスト**になる|
| 非選択状態である | **右クリックしたファイル・ディレクトリのみ**からなるリストになる | 

プログラムからはユーザーがいずれの状態で操作を行ったのかを判定することは出来無い[^2]。


# 結論

1. 第1引数がワークスペースフォルダを指している場合、コマンドの性質によってはユーザーに告知すべき（第2引数だけを参照して無視する手段もあり得る）
2. それ以外のケースでは通常第2引数を見れば対象とすべきファイル・ディレクトリのリストが得られ、それを対象に処理を行えば良い
3. そもそも単一のファイル・フォルダ向けのコマンドである場合は第1引数のみを参照すれば良い（ただしワークスペースフォルダの場合だけは注意）


[^1]: 完全に破壊的で無くとも Undo 出来ない、手動で戻すのも難しい・非現実的な処理などを含む
[^2]: もしかすると内部的にエクスプローラーの状態を取得する API はあるかもしれないが未調査