4
2

More than 1 year has passed since last update.

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

Last updated at Posted at 2022-02-12

作るもの

result.gif

リポジトリ

本記事について

何に使えるかわからないけどVSCode拡張しよう!

VSCodeのExtension Guide TreeView

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

VSCodeのTreeViewの公式サンプル

対象

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

前提

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

筆者は以下の書籍で学んだ。
プログラマーのためのVisual Studio Codeの教科書 (Compass Booksシリーズ)

  • TypeScript

Tree View API とは

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

流れ

ざっくりこんな感じ

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

アイコン素材の用意

contributes.views Icon specifications

スクリーンショット_2022-02-12_10_34_55.png

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

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

サンプルみると、こんな画像使えばいいんだなあっていうのがとわかる。

筆者はSketchで雑に作ったが、サンプルの画像をそのまま使っちゃっうのもありだと思う。

package.json

TreeViewAPI package.json Contribution

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

スクリーンショット_2022-02-12_11_06_12.png

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

contributes.views

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

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

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

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

3. activationEvents

Activation

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

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

TreeDataProviderを作る

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

スクリーンショット 2022-02-12 14.25.24.png

ここがポイント

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

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

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

コンセプト

TreeItem

TreeItem

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

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

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

TreeItemCollapsibleState

TreeItemCollapsibleState

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

TreeDataProvider

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

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

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

実装

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

の順に実装していく

.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

  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

ここはドキュメント通りで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

右クリックした時等のイベント作成

View Actions

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

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

package.json
"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きてるしそういうものなんだろう!

サンプル

.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>のようはハッシュを作っておいて
そこから取ってくるとかでも良いと思う。

4
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
2