補完機能を持つ拡張機能の開発チュートリアル

## Abstract

本記事では拡張機能開発におけるregisterCompletionItemProviderの使い方を説明したいと思います。
つまり拡張機能で補完を追加する方法を解説します。(公式のチュートリアル)

## 想定読者

- VSCodeで拡張機能開発をしたい人
    - hello worldの次の段階を想定します。
- TypeScriptの文法をある程度知っている人
    - 筆者はTypeScript自体歴が浅いので、文法を解説できる自信はないです。非推奨な書き方をみつけたら指摘していただけると嬉しいです。
   
hello worldについては、[こちら](https://qiita.com/intertext543210/items/75e8dedab69ae3b1cff7)がチュートリアルになっていると思います。単に動かすだけではなく、コードを少し変更しているので理解も深まると思います。

## 環境

windows 10です。

```bash
$ code --version
1.54.2

$ node --version
v12.16.1

$ npm --version
7.6.1
```

## 動機

フレームワーク[deap](https://github.com/deap/deap)をVSCodeで使っていた時、補完が効かないことに気づいてしまいました。原因は簡単で、クラスを動的に生成しているからです。

```python
creator.create("FitnessMax", base.Fitness, weights=(1.0,))
creator.create("Individual", list, fitness=creator.FitnessMax)
```

実行時にクラスが生成されるので、当然コーディングでは補完されないというわけですね。
そこで補完機能を加える拡張機能を開発したいと考えたのですが、情報が少なく苦労したので備忘録を兼ねて筆を執りました。
完成品は[Deap-supporter](https://github.com/Take-Me1010/Deap-supporter)です。

## 公式チュートリアル

公式の補完を実装する例を解説します。
コードは[公式チュートリアルのリポジトリ](https://github.com/microsoft/vscode-extension-samples/tree/main/completions-sample)のものです。

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

export function activate(context: vscode.ExtensionContext) {

	const provider1 = vscode.languages.registerCompletionItemProvider('plaintext', {

		provideCompletionItems(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken, context: vscode.CompletionContext) {

			// a simple completion item which inserts `Hello World!`
			const simpleCompletion = new vscode.CompletionItem('Hello World!');

			// a completion item that inserts its text as snippet,
			// the `insertText`-property is a `SnippetString` which will be
			// honored by the editor.
			const snippetCompletion = new vscode.CompletionItem('Good part of the day');
			snippetCompletion.insertText = new vscode.SnippetString('Good ${1|morning,afternoon,evening|}. It is ${1}, right?');
			snippetCompletion.documentation = new vscode.MarkdownString("Inserts a snippet that lets you select the _appropriate_ part of the day for your greeting.");

			// a completion item that can be accepted by a commit character,
			// the `commitCharacters`-property is set which means that the completion will
			// be inserted and then the character will be typed.
			const commitCharacterCompletion = new vscode.CompletionItem('console');
			commitCharacterCompletion.commitCharacters = ['.'];
			commitCharacterCompletion.documentation = new vscode.MarkdownString('Press `.` to get `console.`');

			// a completion item that retriggers IntelliSense when being accepted,
			// the `command`-property is set which the editor will execute after 
			// completion has been inserted. Also, the `insertText` is set so that 
			// a space is inserted after `new`
			const commandCompletion = new vscode.CompletionItem('new');
			commandCompletion.kind = vscode.CompletionItemKind.Keyword;
			commandCompletion.insertText = 'new ';
			commandCompletion.command = { command: 'editor.action.triggerSuggest', title: 'Re-trigger completions...' };

			// return all completion items as array
			return [
				simpleCompletion,
				snippetCompletion,
				commitCharacterCompletion,
				commandCompletion
			];
		}
	});

	const provider2 = vscode.languages.registerCompletionItemProvider(
		'plaintext',
		{
			provideCompletionItems(document: vscode.TextDocument, position: vscode.Position) {

				// get all text until the `position` and check if it reads `console.`
				// and if so then complete if `log`, `warn`, and `error`
				const linePrefix = document.lineAt(position).text.substr(0, position.character);
				if (!linePrefix.endsWith('console.')) {
					return undefined;
				}

				return [
					new vscode.CompletionItem('log', vscode.CompletionItemKind.Method),
					new vscode.CompletionItem('warn', vscode.CompletionItemKind.Method),
					new vscode.CompletionItem('error', vscode.CompletionItemKind.Method),
				];
			}
		},
		'.' // triggered whenever a '.' is being typed
	);

	context.subscriptions.push(provider1, provider2);
}
```

基本的な流れは`vscode.languages.registerCompletionItemProvider`でproviderを登録し、その返り値を`context.subscriptions.push`で追加すると言った、シンプルな流れをとります。

以下では上記のコードを細かく見ていきます。

```extension.ts
const provider1 = vscode.languages.registerCompletionItemProvider('plaintext', {
```
まずは登録です。
`vscode.lanmguages.registerCompletionItemProvider('補完を有効にする言語モード', provider, '補完のトリガーになる単語')`
という形で使用します。ここで`provider`は`provideCompletionItems`をメソッドに持つオブジェクトです。補完のトリガーになる単語は、その単語が入力されたら起動するという意味です。例では'.'を指定しています。
例では第一引数に'plaintext'を指定しているので、このproviderは、プレーンテキスト編集時に有効になります。他にも例えば'python'と指定すれば、pythonを編集時に有効になります。

次の行では、`provider`を定義しています。`provideCompletionItems`を実装しなければならないので

```typescript
{
    provideCompletionItems(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken, context: vscode.CompletionContext){
        // 色々やって
        // vscode.CompletionItemのリストを返す
        return compItems
    }
}
```

大枠として、上記のようなオブジェクトを必要とします。
ここで返却したvscode.CompletionItemのリストが表示される補完を決めるものになります。
以下では`vscode.CompletionItem`の生成例を見ていきます。


```typescript
// a simple completion item which inserts `Hello World!`
const simpleCompletion = new vscode.CompletionItem('Hello World!');
```

newを使って生成します。コンストラクタ―は```CompletionItem('ラベル名', kind)```の形です。
ラベル名は、補完の際に表示される名前です。
kindは任意引数です。`vscode.CompletionItemKind`に存在する値しか受け取りません。これについては[VSCode拡張機能開発・Intellisenseのアイコンマークまとめ](https://qiita.com/k-wth/items/1e4f44771d34125d191f)に一覧があります。

さてその他にも補完が登録されていますが、主に使用されているアトリビュートをまとめました。

|アトリビュート|型|意味|
|------------|--------|--------|
|kind|vscode.CompletionItemKind|補完で端に表示されるアイコン|
|insertText|String / SnippetString|選択された際に挿入されるテキスト。未設定ではラベル名が使われる。|
|documentation| string / MarkdownString|補完を選択するときに表示されるドキュメンテーション。下の図：documentation参照。|
|detail|string|補完選択で表示される詳細。図：detail1, detail2を参照。|
|command|Command|補完で選択された時に実行するコマンド。イマイチ使い道がわからないが、{command: 'editor.action.triggerSuggest', title: '説明'}のようなオブジェクトを渡す。例では補完を連続で起動できるということであろうか。|

![detail1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/483769/bea1482f-5ef0-fc0c-1e3c-0a453da665b9.png)
図：detail1
![detail2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/483769/653d3492-a847-35d0-720e-0c401e80265c.png)
図：detail2

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/483769/e624bee2-7a61-fb35-66c3-3c5956c3a4c6.png)
図：documentation
右に出ている吹き出しで、上の薄い字がdetailで、下側がdocumentationです。

上記のアトリビュートを変えることで、補完を簡単に(?)登録できるというわけですね！

例では4つの`CompletionItem`を作り、最後に

```extension.ts
            // return all completion items as array
            return [
                simpleCompletion,
                snippetCompletion,
                commitCharacterCompletion,
                commandCompletion
            ];
```

のようにして返却しています。
以上で簡単な補完を実装するprovider1を登録できました。しかし実際には直前の単語によって提案する補完を変えたりする必要があります。その機能を盛り込んだものがprovider2です。
以下ではprovider2の実装を見ていきます。

`provideCompletionItems`の`document: vscode.TextDocument, position: vscode.Position`はざっくり言えばそれぞれ編集中のファイルと補完が起動している場所になっています。
この引数を利用して、まず初めに`console.`で終わっていないかを確認する処理を入れています。

```typescript
const linePrefix = document.lineAt(position).text.substr(0, position.character);
if (!linePrefix.endsWith('console.')) {
	return undefined;
}
```

細かな仕様を理解していないのですが、上記のようなメソッドチェーンで現在補完を起動している行(カーソルがあるところの手前まで)を取得できます。
また、`console.`で終わっていない場合には、`undefined`を返却することで補完を何も表示しないようにしています。
なお、`.`で補完が起動するように登録しているので、この関数が呼ばれている時点で100％行の最後の文字は`.`です。そこに注意しましょう。

```typescript
return [
	new vscode.CompletionItem('log', vscode.CompletionItemKind.Method),
	new vscode.CompletionItem('warn', vscode.CompletionItemKind.Method),
	new vscode.CompletionItem('error', vscode.CompletionItemKind.Method),
];
```

と返すことで、補完候補に`log`, `warn`, `error`を表示するようにしています。

## まとめ

`registerCompletionItemProvider`を使えば簡単に補完機能を追加する拡張機能を開発できることがわかったと思います。
その他の機能も比較的簡単に実装はできるのでしょうが、英語の情報が基本となるので少々大変です。
この記事がその足がかりになることを願います。

## 今後の展望

`implements`を使用した別の書き方の紹介