VSCodeで複数ファイルの同じ単語をハイライトする拡張を作った

# 作ったもの
Atomの標準機能である、複数ウィンドウのハイライトをVSCodeの拡張で作成しました。
AtomからVSCodeに乗り換えたときから唯一の不満だったのでとても快適になりました。

![multiwindows-highlight.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/224692/0ecd1d95-d03b-97ab-d76b-878624eb66f6.gif)

`extension.js` に数行付け足しただけの非常に簡単な実装になったので、今後拡張を作る方の参考になるといいなと思いプログラムの解説を書いてみます。

[Github](https://github.com/aki12n/multiwindows-highlight/tree/0.0.1)
[VSCode MarketPlace](https://marketplace.visualstudio.com/items?itemName=aki12n.multiwindows-highlight&ssr=false#overview)

# やったことざっくり
* 環境構築
* テンプレートの生成
* 実装
    * 実装したい機能に近い拡張機能のコードを読む
    * 公式ドキュメント読む
    * デバッグしながら作る
* 公開

環境構築、テンプレート生成、公開については情報も多く、すんなり行うことができました。

# 使用した機能
* イベント検知
* ウィンドウの取得
* テキストへの装飾(decorator)

**カーソルの移動をイベント検知して、テキストへの装飾を変更する** というプログラムです。
コマンドも使用していません。起動時に自動的に実行されるプログラムにしました。

# コード概要

## `pachage.json`
プロジェクト自体の設定やイベントの登録などをするためのファイル。

```pachage.json
{
    ...
    "activationEvents": [
	"*"
    ],
    "main": "./extension.js",
    ...
}
```
`"main"` ：エントリポイントが存在するファイルを指定
`"activationEvents"` ：拡張機能を実行するイベントを指定(`onCommand` でコマンドから呼び出し可能になる等。今回は常に有効にするために`"*"`を指定しました。)

## `extension.js`
エントリポイント（activate）がある。
今回は短い処理なので全てこのファイル内に処理を記載しました。

```extension.js
const vscode = require('vscode');
var curDecorator;

// 起動時に呼ばれる(エントリポイント)
function activate(context) {
	decorateSameWords();
	exports.activate = activate;
	// カーソル移動イベントを検知
	const disposable = vscode.window.onDidChangeTextEditorSelection(event => {
		if (event.textEditor.selection.isEmpty) {
			// 選択されていない場合は前回のデコレーションを削除
			deleteDecorator(curDecorator);
			return;
		}
		decorateSameWords(event.textEditor.selection);
	});
	context.subscriptions.push(disposable);
}
```
エントリポイント関数内ではカーソル移動の検知を行っています。

`onDidChangeTextEditorSelection` でカーソル移動が検知できました。  
他にもアクティブなテキストエディタの変更や、ターミナルの開閉などのイベント検知ができる関数もあるようです。

イベントが検知された場合、既に存在するハイライト(decorationを使用)の削除関数や、ハイライトを新規作成する関数をコールさせました。



```extension.js
function decorateSameWords(curSelection) {
	const editor = vscode.window.activeTextEditor;
	if (!editor) {
		return;
	}
	// 既にあるデコレーションをクリア
	deleteDecorator(curDecorator);
	curDecorator = { // 変数にそのままcreateTextEditorDecorationTypeを入れてしまうとdispose()で変数自体が無効になってしまうため入れ子にする
		'decorator': vscode.window.createTextEditorDecorationType({
			'borderWidth': '1px',
			'borderStyle': 'solid',
			'light': {
				'overviewRulerColor': 'rgba(124,77, 255, 0.3)',
				'backgroundColor': 'rgba(124,77, 255, 0.3)',
				'borderColor': 'rgba(124,77, 255, 0.4)',
				'color': 'rgba(255, 0, 0, 1.0)'
			},
			'dark': {
				'overviewRulerColor': 'rgba(255, 255, 204, 0.3)',
				'backgroundColor': 'rgba(255, 255, 204, 0.3)',
				'borderColor': 'rgba(255, 255, 204, 0.4)',
				'color': 'rgba(255, 255, 0, 1.0)'
			}
		})
	};

	//選択している文字列を取得
	var searchWord = editor.document.getText(curSelection);
	var otheEditors = vscode.window.visibleTextEditors;

	otheEditors.forEach((eachEditor) => {
		let ranges = new Array();
		for (var i = 0; i < eachEditor.document.lineCount; ++i) {
			for (var j = 0; j < eachEditor.document.lineAt(i).text.length; ++j){
				var curPosition = eachEditor.document.lineAt(i).text.indexOf(searchWord, j);
				if (~curPosition) {
					const startPosition = new vscode.Position(i, curPosition);
					const endPosition = new vscode.Position(i, curPosition + searchWord.length);
					const range = new vscode.Range(startPosition, endPosition);
					ranges.push(range);
					j = curPosition + searchWord.length - 1;
				}
			}
		}
		eachEditor.setDecorations(curDecorator.decorator, ranges);
	});
}
```

`createTextEditorDecorationType` でハイライトを作成していますが、通常は以下の様なコードで使用されると思います。

<想定されている使用方法>

```js
curDecorator = vscode.window.createTextEditorDecorationType({...});
...
setDecorations(curDecorator, ranges);
```
しかし、この方法で装飾を作成すると、装飾を消す処理(`curDecorator.dispose()`)を行った後は、変数自体が無効(?)になってしまい、
`curDecorator` に再度`createTextEditorDecorationType`した結果を渡しても新しい装飾として使用することはできませんでした。

そのため、入れ子構造にすることでこれを変数名の使い回しを可能にしています。

※ こういった仕様が一般的なのか、この対処方法で正しいのかが分からないのでコメントで教えていただけると助かります。


ハイライトする箇所については、JavaScriptの標準の関数を使用して定番らしい文字列検索を行っています。
そしてそれを`setDecorations` に渡すのにふさわしい形におさめています。


```extension.js
function deleteDecorator(deleteDecorator) {
	if (curDecorator != undefined) {
		deleteDecorator.decorator.dispose();
	}
}
```
参考にさせて頂きました：https://qiita.com/kojisaiki/items/c20ccfd45c98fa3301d6


# デバッグ

VSCodeから直接デバッグできます。
Extension Runを選択してF5を押すと新しいウィンドウが開いて拡張機能が実行されます。
ブレークポイントなども普通に使用できました。

<img width="1168" alt="スクリーンショット 2019-11-24 1.10.17.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/224692/4a4a659c-a667-5511-28bf-84786215a418.png">

# 公開

このあたりを設定しました。
特に、keywordsやcategoriesは、公開コマンド(vsce)によって自動的にここの設定がマーケットプレイスへの登録情報になります。

```pachage.json
	"name": "multiwindows-highlight",
	"displayName": "multiWindows-highLight",
	"description": "highlight same words in multi windows",
	"version": "0.0.1",
	"publisher": "aki12n",
	"license": "SEE LICENSE IN LICENSE.txt",
	"icon": "images/multiwindows-highlight.png",
	"bugs": {
		"url": "https://github.com/aki12n/multiwindows-highlight/issues"
	},
	"homepage": "https://github.com/aki12n/multiwindows-highlight/blob/master/README.md",
	"repository": {
		"type": "git",
		"url": "https://github.com/aki12n/multiwindows-highlight.git"
	},
	"engines": {
		"vscode": "^1.40.0"
	},
	"categories": [
		"Other"
	],
	"keywords": [
		"match",
		"highlight",
		"search"
	],
```

# おわり

テストを作成していなかったりクラス分けしていなかったりするので、今後機能追加の際に修正していきたいです。

# 追記：公開した拡張のバージョンアップ

コードを更新し、`vsce publish patch` で https://marketplace.visualstudio.com/ に登録された情報をアップデートすることができました。
minor, patch 等の引数を与えることでバージョン情報はvsceが勝手にインクリメントをしてくれるようなのですが、pachage.jsonに記載されたversionは0.0.1のままで、マーケット上の表示は0.0.2になっていたので、pachage.json そのあたりの扱いがよくわからないままです・・・

他の方の記事ではpachage.jsonの記載が変わるという情報が多いので、私のやり方に何か問題があったのかもしれません。