VSCodeのサイドメニューの拡張の作り方

# 作るもの
![result.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/113504/83044098-6612-3aa6-f037-ad8d88b8892a.gif)

[リポジトリ](https://github.com/teach310/vsce-quickstart)

# 本記事について
**何に使えるかわからないけど**VSCode拡張しよう！

[VSCodeのExtension Guide TreeView](https://code.visualstudio.com/api/extension-guides/tree-view)

があるが、手順になってないので以下の公式サンプルを参考にしつつTreeViewを作る手順について示す。

[VSCodeのTreeViewの公式サンプル](https://github.com/microsoft/vscode-extension-samples/tree/main/tree-view-sample)


# 対象
TreeViewの作り方としてUnityEditorでのTreeViewの作り方を参考にしたのでそっちの知識があると良い。
なくても大丈夫。

# 前提
- VSCodeの拡張機能のプロジェクトの作り方がわかること。

筆者は以下の書籍で学んだ。
[プログラマーのためのVisual Studio Codeの教科書 (Compass Booksシリーズ)](https://www.amazon.co.jp/dp/B087Q2P56J/ref=dp-kindle-redirect?_encoding=UTF8&btkr=1)

- TypeScript

# Tree View API とは
VSCodeのサイドバーを拡張する機能。
既存の検索ツールとかの拡張も可能。

# 流れ
ざっくりこんな感じ

1. アイコン素材の用意
2. package.jsonに表示するための設定を書く
3. TreeDataProvider(後述)を作る
4. TreeDataProviderを使ってサイドバー表示
5. 右クリックした時等のイベント作成

# アイコン素材の用意

[contributes.views Icon specifications](https://code.visualstudio.com/api/references/contribution-points#Icon-specifications)

![スクリーンショット_2022-02-12_10_34_55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/113504/c21a5214-9561-8d5d-e49f-182d5e2031ad.png)

以下のようなアイコン素材を作る

画像サイズ: 24x24
色: 単色、背景は透過
拡張子: svg 推奨


[サンプル](https://github.com/microsoft/vscode-extension-samples/tree/main/tree-view-sample/media)みると、こんな画像使えばいいんだなあっていうのがとわかる。

筆者はSketchで[雑に作った](https://github.com/teach310/vsce-quickstart/tree/treeview/media)が、サンプルの画像をそのまま使っちゃっうのもありだと思う。

# package.json

[TreeViewAPI package.json Contribution](https://code.visualstudio.com/api/extension-guides/tree-view#package.json-contribution)

package.jsonについて記述する内容について示す。

![スクリーンショット_2022-02-12_11_06_12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/113504/66f86ee1-0b80-b6ca-b716-798d3986bd98.png)


1 左のアイコンが並んでる部分を Activity Bar
2 アイコンをクリックした時に表示される2の部分を Side Bar と呼ぶ。

### contributes.views

[contributes.views](https://code.visualstudio.com/api/references/contribution-points#contributes.views)

拡張機能の表示の**構成**を示す

```.json
	"contributes": {
...
		"views": {
			"アクティビティバーの部分のID": [
				{
					"id": "サイドバーに表示する内容のID1",
					"name": "サイドバーに表示する内容の名前1"
				}
			]
		}
	},
```

だいたいこんな認識



```package.json
	"contributes": {
...
		"views": {
			"quickstart-view": [
				{
					"id": "quickstartContainer1",
					"name": "Container1"
				}
			]
		}
	},
```

今回は`quictart-view`というviewのサイドバーの中の1要素としてquickstartContainer1を定義した。

(viewsContainersとは別であくまでも1要素って意味でのContainer。
別の名前にすればよかったと後悔してる。`quickStartSidebarView1`とかが分かりやすかったかもしれない。
実際に拡張機能作る時は機能名つければ良いと思う)


### 2 contributes.viewsContainers
[contributes.viewsContainers](https://code.visualstudio.com/api/references/contribution-points#contributes.viewsContainers)

拡張機能の表示の**内容**を示す

```package.json
	"contributes": {
...
		"viewsContainers": {
			"activitybar": [
				{
					"id": "quickstart-view",
					"title": "Quickstart",
					"icon": "media/quickstart_icon.svg"
				}
			]
		},
```

アクティビティバーのアイコンの指定

### 3. activationEvents

[Activation](https://code.visualstudio.com/api/extension-guides/tree-view#activation)

https://code.visualstudio.com/api/references/activation-events#onView

```package.json
	"activationEvents": [
		"onView:quickstartContainer1"
	],
```

これ書かないとアクティビティバーのアイコンクリックした時にサイドバー表示されない。

# TreeDataProviderを作る

サイドバー描画の流れはざっくりこんな感じ

![スクリーンショット 2022-02-12 14.25.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/113504/d3628bd2-391d-e6f5-6454-13c334b73f79.png)

https://code.visualstudio.com/api/extension-guides/tree-view#tree-data-provider

ここがポイント

https://github.com/microsoft/vscode-extension-samples/blob/main/tree-view-sample/src/extension.ts#L17-L18

```extension.ts
	const nodeDependenciesProvider = new DepNodeProvider(rootPath);
	vscode.window.registerTreeDataProvider('nodeDependencies', nodeDependenciesProvider);
```

サンプルではこのようにregisterTreeDataProviderを使って表示する内容をvscodeと紐づけている

だから、まずその引数であるTreeDataProviderを作る。

## コンセプト
### TreeItem
[TreeItem](https://code.visualstudio.com/api/references/vscode-api#TreeItem)

TreeViewの各アイテムを描画するためのクラス。
主なプロパティーは　文字、開閉状態、アイコン

コンストラクタはこれ使っておけば良いと思ってる

```ts
new TreeItem(label: string | TreeItemLabel, collapsibleState?: TreeItemCollapsibleState): TreeItem
```

### TreeItemCollapsibleState
[TreeItemCollapsibleState](https://code.visualstudio.com/api/references/vscode-api#TreeItemCollapsibleState)

折りたたみの状態。TreeItemに持たせる
Collapsed 閉じてる
Expand 開いてる
None 折りたたみ機能がない。

### TreeDataProvider
[TreeDataProvider](https://code.visualstudio.com/api/references/vscode-api#TreeDataProvider)

ただのインタフェースで実装は持ってない。

```.ts
        /**
         * Get {@link TreeItem} representation of the `element`
         *
         * @param element The element for which {@link TreeItem} representation is asked for.
         * @return TreeItem representation of the element.
         */
        getTreeItem(element: T): TreeItem | Thenable<TreeItem>;

        /**
         * Get the children of `element` or root if no element is passed.
         *
         * @param element The element from which the provider gets children. Can be `undefined`.
         * @return Children of `element` or root if no element is passed.
         */
        getChildren(element?: T): ProviderResult<T[]>;
```

他はOptionalなので上記2つだけ最低実装しておけばいい。

#### element
TreeItemのモデルと考えるのが自然だが、サンプルではTreeItemを継承したクラスをelementとして扱っている。
たしかにhtmlだとelementはUI要素を示すのでそれでもおかしくないのだが、ViewとModelは分けた方がよいと思うので、
本記事ではelementはTreeItemを継承しないクラスにする。

### ProviderResult
[ProviderResult](https://code.visualstudio.com/api/references/vscode-api#ProviderResult)

```ts
export type ProviderResult<T> = T | undefined | null | Thenable<T | undefined | null>;
```

このうちのどれかならどれでもいいよっていう型

## 実装
1. elementのクラス作成
2. TreeDataProviderのgetTreeItem
3. TreeDataProviderのgetChildren

の順に実装していく

https://github.com/teach310/vsce-quickstart/blob/treeview/src/quickstartContainer1Provider.ts#L34-L66

```.ts
export class QuickStartContainer1TreeElement {

  private _children: QuickStartContainer1TreeElement[];
  private _parent: QuickStartContainer1TreeElement | undefined | null
  constructor(
    public name: string
  ) {
    this._children = [];
  }

  get parent(): QuickStartContainer1TreeElement | undefined | null {
    return this._parent;
  }

  get children(): QuickStartContainer1TreeElement[] {
    return this._children;
  }

  addChild(child: QuickStartContainer1TreeElement) {
    child.parent?.removeChild(child);
    this._children.push(child);
    child._parent = this;
  }

  removeChild(child: QuickStartContainer1TreeElement) {
    const childIndex = this._children.indexOf(child);
    if (childIndex >= 0) {
      this._children.splice(childIndex, 1);
      child._parent = null;
    }
  }
}
```

C#のLinkedListのような感じで、elementが自身の親子を把握するような設計にした。
本記事のTreeItemは表示する文字の情報しかないため、親子関係以外のプロパティーはnameのみ

参考
https://light11.hatenadiary.com/entry/2019/02/07/010146

次にTreeDataProviderの実装

```.ts
import * as vscode from 'vscode';

export class QuickStartContainer1Provider implements vscode.TreeDataProvider<QuickStartContainer1TreeElement> {
}
```

まずここだけ書いて、vscodeのQuick Fixでインタフェース部分は自動生成

getTreeItem



```.ts
export class QuickStartContainer1Provider implements vscode.TreeDataProvider<QuickStartContainer1ItemModel> {
  onDidChangeTreeData?: vscode.Event<void | QuickStartContainer1ItemModel | null | undefined> | undefined;
  getTreeItem(element: QuickStartContainer1ItemModel): vscode.TreeItem | Thenable<vscode.TreeItem> {
    throw new Error('Method not implemented.');
  }
  getChildren(element?: QuickStartContainer1ItemModel): vscode.ProviderResult<QuickStartContainer1ItemModel[]> {
    throw new Error('Method not implemented.');
  }
}
```

onDidChangeTreeData?はオプショナルで今回は使用しないので削除。

getTreeItem

```ts
  getTreeItem(element: QuickStartContainer1TreeElement): vscode.TreeItem | Thenable<vscode.TreeItem> {
    const collapsibleState = element.children.length > 0 ? vscode.TreeItemCollapsibleState.Collapsed : vscode.TreeItemCollapsibleState.None;
    return new vscode.TreeItem(element.name, collapsibleState);
  }
```

子がいたら開閉できるようにっていうのを書く。
ただ、関数名がgetなので、生成せずにキャッシュから返したほうがいいのかもしれない。(ここは正解わからない)

getChildren

```.ts
import * as vscode from 'vscode';

export class QuickStartContainer1Provider implements vscode.TreeDataProvider<QuickStartContainer1TreeElement> {

  private rootElements: QuickStartContainer1TreeElement[]
  constructor() {
    this.rootElements = this.createElements();
  }

  getTreeItem(element: QuickStartContainer1TreeElement): vscode.TreeItem | Thenable<vscode.TreeItem> {
    const collapsibleState = element.children.length > 0 ? vscode.TreeItemCollapsibleState.Collapsed : vscode.TreeItemCollapsibleState.None;
    return new vscode.TreeItem(element.name, collapsibleState);
  }

  getChildren(element?: QuickStartContainer1TreeElement): vscode.ProviderResult<QuickStartContainer1TreeElement[]> {
    return element ? element.children : this.rootElements;
  }

  /**
   * @return rootElements
   */
  private createElements(): QuickStartContainer1TreeElement[] {
    const parent1 = new QuickStartContainer1TreeElement('Item1');
    ['Item1_1', 'Item1_2'].forEach(name => {
      parent1.addChild(new QuickStartContainer1TreeElement(name));
    });

    const parent2 = new QuickStartContainer1TreeElement('Item2');
    parent2.addChild(new QuickStartContainer1TreeElement('Item2_1'));
    return [parent1, parent2];
  }
}
```

constructorでデータ作るのはあまり良い設計ではないが、今回は楽なのでやってしまう。
rootだけあればそこから子のインスタンスとってこれるのでrootだけキャッシュ。

getChildrenで渡される引数がなかったらrootなのでrootを返し、それ以外であれば子要素を返す。

# TreeDataProviderを使ってサイドバー表示

[Registering the TreeDataProvider](https://code.visualstudio.com/api/extension-guides/tree-view#registering-the-treedataprovider)

ここはドキュメント通りでOK

```extension.ts
import { QuickStartContainer1Provider, QuickStartContainer1TreeElement } from './quickstartContainer1Provider';

...

export function activate(context: vscode.ExtensionContext) {
	const quickstartContainer1Provider = new QuickStartContainer1Provider();
	vscode.window.registerTreeDataProvider('quickstartContainer1', quickstartContainer1Provider);
```

ここまでいったら拡張機能走らせたらTreeViewが表示されるのでやってみる。

![スクリーンショット 2022-02-12 19.56.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/113504/84b1e738-26c8-e158-c5e8-93d2401ccd34.png)


# 右クリックした時等のイベント作成
[View Actions](https://code.visualstudio.com/api/extension-guides/tree-view#view-actions)

ドキュメントから view/item/context に書けば良いってことがわかる。

あとこれもサンプルみると分かりやすい。


```package.json
"contributes": {
		"commands": [
			{
				"command": "quickstartContainer1.show",
				"title": "Show"
			}
		],
		"menus": {
			"view/item/context": [
				{
					"command": "quickstartContainer1.show",
					"when": "view == quickstartContainer1"
				}
			]
		},
	
```

引数については[ここ](https://code.visualstudio.com/api/references/contribution-points#contributes.menus)にさらっと書かれている

> Note: When a command is invoked from a (context) menu, VS Code tries to infer the currently selected resource and passes that as a parameter when invoking the command

よくわからないが、サンプル見た感じelementきてるしそういうものなんだろう！

[サンプル](https://github.com/microsoft/vscode-extension-samples/blob/main/tree-view-sample/src/extension.ts#L23)

```.ts
export function activate(context: vscode.ExtensionContext) {
...
	const showDisposable = vscode.commands.registerCommand('quickstartContainer1.show', (element: QuickStartContainer1TreeElement) => {
		if (element) {
			vscode.window.showInformationMessage(`This is ${element.name}`, { modal: true });
		}
	});

	context.subscriptions.push(
		showDisposable
	);
}
```

これで冒頭のgifのような拡張機能ができる。

# まとめ
階層構造の取得の仕方はあくまでもやり方の一つというだけで、例えば<親のID、子のID>のようはハッシュを作っておいて
そこから取ってくるとかでも良いと思う。