作るもの
本記事について
何に使えるかわからないけどVSCode拡張しよう!
VSCodeのExtension Guide TreeView
があるが、手順になってないので以下の公式サンプルを参考にしつつTreeViewを作る手順について示す。
対象
TreeViewの作り方としてUnityEditorでのTreeViewの作り方を参考にしたのでそっちの知識があると良い。
なくても大丈夫。
前提
- VSCodeの拡張機能のプロジェクトの作り方がわかること。
筆者は以下の書籍で学んだ。
プログラマーのためのVisual Studio Codeの教科書 (Compass Booksシリーズ)
- TypeScript
Tree View API とは
VSCodeのサイドバーを拡張する機能。
既存の検索ツールとかの拡張も可能。
流れ
ざっくりこんな感じ
- アイコン素材の用意
- package.jsonに表示するための設定を書く
- TreeDataProvider(後述)を作る
- TreeDataProviderを使ってサイドバー表示
- 右クリックした時等のイベント作成
アイコン素材の用意
contributes.views Icon specifications
以下のようなアイコン素材を作る
画像サイズ: 24x24
色: 単色、背景は透過
拡張子: svg 推奨
サンプルみると、こんな画像使えばいいんだなあっていうのがとわかる。
筆者はSketchで雑に作ったが、サンプルの画像をそのまま使っちゃっうのもありだと思う。
package.json
TreeViewAPI package.json Contribution
package.jsonについて記述する内容について示す。
1 左のアイコンが並んでる部分を Activity Bar
2 アイコンをクリックした時に表示される2の部分を Side Bar と呼ぶ。
contributes.views
拡張機能の表示の構成を示す
"contributes": {
...
"views": {
"アクティビティバーの部分のID": [
{
"id": "サイドバーに表示する内容のID1",
"name": "サイドバーに表示する内容の名前1"
}
]
}
},
だいたいこんな認識
"contributes": {
...
"views": {
"quickstart-view": [
{
"id": "quickstartContainer1",
"name": "Container1"
}
]
}
},
今回はquictart-view
というviewのサイドバーの中の1要素としてquickstartContainer1を定義した。
(viewsContainersとは別であくまでも1要素って意味でのContainer。
別の名前にすればよかったと後悔してる。quickStartSidebarView1
とかが分かりやすかったかもしれない。
実際に拡張機能作る時は機能名つければ良いと思う)
2 contributes.viewsContainers
拡張機能の表示の内容を示す
"contributes": {
...
"viewsContainers": {
"activitybar": [
{
"id": "quickstart-view",
"title": "Quickstart",
"icon": "media/quickstart_icon.svg"
}
]
},
アクティビティバーのアイコンの指定
3. activationEvents
"activationEvents": [
"onView:quickstartContainer1"
],
これ書かないとアクティビティバーのアイコンクリックした時にサイドバー表示されない。
TreeDataProviderを作る
サイドバー描画の流れはざっくりこんな感じ
ここがポイント
const nodeDependenciesProvider = new DepNodeProvider(rootPath);
vscode.window.registerTreeDataProvider('nodeDependencies', nodeDependenciesProvider);
サンプルではこのようにregisterTreeDataProviderを使って表示する内容をvscodeと紐づけている
だから、まずその引数であるTreeDataProviderを作る。
コンセプト
TreeItem
TreeViewの各アイテムを描画するためのクラス。
主なプロパティーは 文字、開閉状態、アイコン
コンストラクタはこれ使っておけば良いと思ってる
new TreeItem(label: string | TreeItemLabel, collapsibleState?: TreeItemCollapsibleState): TreeItem
TreeItemCollapsibleState
折りたたみの状態。TreeItemに持たせる
Collapsed 閉じてる
Expand 開いてる
None 折りたたみ機能がない。
TreeDataProvider
ただのインタフェースで実装は持ってない。
/**
* 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
export type ProviderResult<T> = T | undefined | null | Thenable<T | undefined | null>;
このうちのどれかならどれでもいいよっていう型
実装
- elementのクラス作成
- TreeDataProviderのgetTreeItem
- TreeDataProviderのgetChildren
の順に実装していく
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の実装
import * as vscode from 'vscode';
export class QuickStartContainer1Provider implements vscode.TreeDataProvider<QuickStartContainer1TreeElement> {
}
まずここだけ書いて、vscodeのQuick Fixでインタフェース部分は自動生成
getTreeItem
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
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
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
ここはドキュメント通りでOK
import { QuickStartContainer1Provider, QuickStartContainer1TreeElement } from './quickstartContainer1Provider';
...
export function activate(context: vscode.ExtensionContext) {
const quickstartContainer1Provider = new QuickStartContainer1Provider();
vscode.window.registerTreeDataProvider('quickstartContainer1', quickstartContainer1Provider);
ここまでいったら拡張機能走らせたらTreeViewが表示されるのでやってみる。
右クリックした時等のイベント作成
ドキュメントから view/item/context に書けば良いってことがわかる。
あとこれもサンプルみると分かりやすい。
"contributes": {
"commands": [
{
"command": "quickstartContainer1.show",
"title": "Show"
}
],
"menus": {
"view/item/context": [
{
"command": "quickstartContainer1.show",
"when": "view == quickstartContainer1"
}
]
},
引数についてはここにさらっと書かれている
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きてるしそういうものなんだろう!
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>のようはハッシュを作っておいて
そこから取ってくるとかでも良いと思う。