Visual Studio Code はじめての拡張機能開発 2


# Visual Studio Code はじめての拡張機能開発 2

前回、[Visual Studio Code はじめての拡張機能開発](http://qiita.com/rma/items/8c53077d1355ab8fa4c6)として、HelloWorldについて、書きました。

次は、WordCountになります。
単語数をカウントして、ステータスバーに表示する拡張機能です。

もとは、こちらになります。
[公式サイト Example - Word Count](https://code.visualstudio.com/docs/extensions/example-word-count)

## ひな形の作成
まずは、ひな形を作成します。

` > yo code`

```
? What type of extension do you want to create? New Extension (TypeScript)
? What's the name of your extension? WordCount
? What's the identifier of your extension? wordcount
? What's the description of your extension?
? What's your publisher name? helloworld
? Initialize a git repository? Yes
```

wordcountをVS Codeで開きます。

`> cd wordcount  `
`> code .  `

## 開発
wordcountは、2ステップで開発をします。

- ステップ１ コマンドパレット編
	- コマンドパレットのコマンドを使用したタイミングで、ステータスバーに単語カウントを表示
- ステップ2 エディタのイベントによる、単語カウントの表示

### ステップ１ コマンドパレット編

コマンドパレットのコマンドを使用したタイミングで、ステータスバーに単語カウントを表示するようにします。  
ひな形を作成すると、コマンドパレットのコマンドを使用して、メッセージを表示するコードが作成されます。  
まずは、それを使用して、コマンド実行時にステータスバーに表示する拡張機能をステップ１として、作成します。

#### コード修正
「**src/extension.ts**」を修正します。以降、同じファイルを修正していると思って下さい。

修正方針：

1. import文の変更。使用するモジュールを詳細に指定します。
1. WordCounterクラスを作成して、単語のカウントとステータスバーの表示制御を作ります。この時、markdownファイルのみ、制御が入るようにします。
1. activate関数を修正

まずは、import文を変更してみましょう。
```
import {window, commands, Disposable, ExtensionContext, StatusBarAlignment, StatusBarItem, TextDocument} from 'vscode';  
```
このように変更すると、デフォルトで使用されていた、「vscode」が使えなくなりますので、使用している箇所「vscode.」を削除します。

> ４行目  
> import {window, commands, Disposable, ExtensionContext, StatusBarAlignment, StatusBarItem, TextDocument} from 'vscode';  
> ８行目 vscode.ExtensionContext  
> export function activate(context: ExtensionContext) {  
> 17行目 vscode.commands  
> let disposable = commands.registerCommand('extension.sayHello', () => {  
> 21行目 vscode.window  
> window.showInformationMessage('Hello World!');  

次に、WordCounterクラスを作成します。
「src/extension.ts」の最後に記述します。
コードに説明を入れておきます。

```ts
// WordCounterクラス宣言
class WordCounter {
    // ステータスバー保存用のprivate変数定義
    private _statusBarItem: StatusBarItem;

    public updateWordCounter() {
        if (!this._statusBarItem) {
            // ステータスバー(左揃え)のリソースを取得。
            this._statusBarItem = window.createStatusBarItem(StatusBarAlignment.Left);
        }

        // アクティブなエディタを取得
        let editor = window.activeTextEditor;
        if (!editor) {
            // 見つからない場合は、ステータスバーを非表示にして、何もしないでreturn。
            this._statusBarItem.hide();
            return;
        }

        // エディタ内のドキュメントを取得
        let doc = editor.document;

        if (doc.languageId == "markdown") {
            // 「.md」のmarkdownの場合に、単語をカウントする(_getWordCount)呼び出し。
            let wordCount = this._getWordCount(doc);

            // 単語の数をステータスバーに設定して、ステータスバーを表示する。
            this._statusBarItem.text = wordCount !== 1 ? `${wordCount} Words` : '1 Word';
            this._statusBarItem.show();

        } else {
            // markdown以外は、ステータスバーを非表示にする
            this._statusBarItem.hide();
        }
    }

    // 単語の数を取得する。
    public _getWordCount(doc: TextDocument): number {
        // ドキュメント内のテキストを取得
        let docContext = doc.getText();

        // テキストの先頭と最後の２文字以上の空白を削除
        docContext = docContext.replace(/^\s\s*/, '').replace(/\s\s*$/, '');

        let wordCount = 0;
        if (docContext != "") {
            // スペースで分割して、分割された数を取得
            wordCount = docContext.split(" ").length;
        }

        return wordCount;
    }

    // リソース開放用の関数を追加
    dispose() {
        // ステータスバーのリソースを開放。
        this._statusBarItem.dispose();
    }
}
```

次に、activateで、WordCounterを使用できるようにします。
コマンド実行時に、updateWordCounterを実行します。
リソースの開放を設定します。

```ts
export function activate(context: ExtensionContext) {

    console.log('Congratulations, your extension "wordcount" is now active!');

    // WordCounterインスタンス生成
    let wordCounter = new WordCounter();

    let disposable = commands.registerCommand('extension.sayHello', () => {
        // コマンド実行時に、updateWordCount
        wordCounter.updateWordCounter();
    });

    // リソース開放設定
    context.subscriptions.push(wordCounter);
    context.subscriptions.push(disposable);
}
```

#### デバッグ

F5を押して、拡張機能のデバッグができます。  
新しいウィンドウが開くので、markdownの新しいファイルを作成して、拡張子「.md」で、保存してみてください。  
ショートカットは、新規ファイル作成「ctrl + n」、保存「ctrl + s」です。

```txt
aaaa aaaa
bbbb bbbb
```

「ctrl + shift + p」で、コマンドパレットを開き、「>」がついた状態で、HelloWorldを選択してみてください。
ステータスバーが表示されて、単語の数が表示されているはずです。

> すでに、デバッグ状態の「\[拡張機能開発ホスト\]」ウィンドウがある場合、そのウィンドウをアクティブにして、「ctrl + r」で、拡張機能のリロードが可能です。

#### ちょっと回り道1/2 (型定義ファイル)

TypeScriptは、「静的型付け」のため、型がコンパイル時に必要となっています。
そこで、他のJavaScriptライブラリを使用するためには、型定義ファイルが必要になっています。
型定義ファイルは、「d.ts」の拡張子で、記述されて、実装がない宣言のみのファイルとなっています。

vscodeの型定義ファイルの「**node_modules/vscode/vscode.d.ts**」を見てみます。
その中から、vscodeとExtensionContextをピックアップして見ました。

```
declare namespace vscode {
	・・・
	export interface ExtensionContext {
		subscriptions: { dispose(): any }[];
		workspaceState: Memento;
		globalState: Memento;
		extensionPath: string;
		asAbsolutePath(relativePath: string): string;
	}
	・・・
}
```

vscodeは、declareが指定されているnamespace宣言になります。  
また、「ExtensionContext」は、「export interface」宣言されていることで、外部から呼び出せるように、なっています。  

変更前と、変更後の違いは、モジュール全体か、それぞれ個別かになります。
>変更前のimportでは、モジュール全体をimportして、使用しています。
>   >import * as vscode from 'vscode';
>
>変更後のimportでは、export宣言されているExtensionContextをimportしています。
>   >import {window, commands, Disposable, ExtensionContext, StatusBarAlignment, StatusBarItem, TextDocument} from 'vscode';

#### ちょっと回り道2/2 (ExtensionContext.subscriptions)

**ExtensionContext.subscriptions**をもう少し掘り下げてみましょう。
「vscode.d.ts」を見ると、dispose関数が入る配列になっています。
「src/extension.ts」の、registerCommand関数の戻り値は、Disposableとなっています。
「vscode.d.ts」にある、Disposableをピックアップしてみました。

```ts
export class Disposable {
		static from(...disposableLikes: { dispose: () => any }[]): Disposable;
		constructor(callOnDispose: Function);
		dispose(): any;
}
```

定義を見てみると、クラス宣言されていて、dispose関数があります。
そのため、指定が可能となっています。
このように、定義を見ると、いろいろできることがありそうです。


### ステップ2 エディタのイベントによる、単語カウントの表示

イベントによる、処理を追加していきます。

- onDidChangeTextEditorSelection
    - カーソルのポジションが変更された時
- onDidChangeActiveTextEditor
    - アクティブエディタが変更された時

まずは、extension.tsにコントロールクラスを追加します。
コードは、以下になります。

```ts
class WordCounterController {

    private _wordCounter: WordCounter;
    private _disposable: Disposable;

    constructor(wordCounter: WordCounter) {
        this._wordCounter = wordCounter;
        this._wordCounter.updateWordCount();

        let subscriptions: Disposable[] = [];
        window.onDidChangeTextEditorSelection(this._onEvent, this, subscriptions);
        window.onDidChangeActiveTextEditor(this._onEvent, this, subscriptions);

        // update the counter for the current file
        // コメントは、上のようになっているが、この部分は、よくわからない。初めに呼んでいるが、なぜ、二回呼ぶのか。
        this._wordCounter.updateWordCount();

        this._disposable = Disposable.from(...subscriptions);
    }

    dispose() {
        this._disposable.dispose();
    }

    private _onEvent() {
        this._wordCounter.updateWordCount();
    }
}
```

次に、コマンドパレットのコマンドによるロードと別の方法で実行されるようにします。
activate関数の修正を行います。

```ts
export function activate(context: ExtensionContext) {

    console.log('Congratulations, your extension "wordcount" is now active!');

    // WordCounterインスタンス生成
    let wordCounter = new WordCounter();
    // WordCounterControllerインスタンス作成
    let controller = new WordCounterController(wordCounter);

    // WordCounterとWordCounterControllerのリソース開放
    context.subscriptions.push(controller);
    context.subscriptions.push(wordCounter);
}
```

package.jsonの修正

コマンド設定の削除を行います。また、
contributes自体を削除するようにします。

```
    "contributes": {
        "commands": [{
            "command": "extension.sayHello",
            "title": "Hello World"
        }]
    },
```

コマンドイベントから、Languageイベントに修正します。

```
    "activationEvents": [
        "onLanguage:markdown"
    ],
```

こうすることで、markdownファイルでの、ロードとイベントの実行が可能になります。
ファイルの拡張子で判断をしているので、ファイルは、保存していないとステータスバーに表示はされません。

#### ステータスバーをカスタマイズ

ステータスバーをもう少し変更します。
updateWordCount関数にある、ステータスバーに表示する文字列に、$(pencil)を記載します。

```ts
this._statusBarItem.text = wordCount !== 1 ? `$(pencil) ${wordCount} Words` : '$(pencil) 1 Word';
```

こうすることで、鉛筆のアイコンが表示されます。

アイコンは、[GitHub Ocicon](https://octicons.github.com/)で見ることができます。