ネットワーク機器用のVS Code Extensionを作りたい　第２回：エラー表示

#はじめに
ネットワーク機器のコンフィグをオシャレに書くために、VS Codeの拡張機能を作っています。

前回の記事
[ネットワーク機器用のVS CodeのExtensionを作りたい　第一回:コマンド一覧の取得](https://qiita.com/odenkun/items/01db4f3a21a25de9121b)

#この記事でやること
Diagnostic(エラー表示)を作ります。
`vs code diagnostic`などで調べると幾つか資料が見つかります。

#動作のサンプル
![diags33ample.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154094/b3e0be10-35ed-f2c6-dd92-bf6a1c3622cb.gif)




#Extensionプロジェクトの生成
Yeomanを使って`yo code`するとかんたんに生成出来ます。
しかしながら、DiagnosticやCompletionなどの各機能に特化した雛形はありません。
そういった特定の機能を**すぐに**使いたい方や、生成につまずいた方などは
https://github.com/microsoft/vscode-extension-samples
にてMSが各機能の軽い例を公開しているので、git cloneするのがおすすめです。

#Diagnosticについて
Extensionの動作の大雑把な流れはこんな感じです。

1.`package.json`の`main`に指定したファイルの`activate`関数が呼ばれる
2.`activate`関数でイベントリスナを登録
3.イベントリスナが文字入力のタイミングなどでテキストを元に
　`DiagnosticCollection`(エラー情報のリスト)を更新

エラー情報の1つを表す`Diagnostic`は
**エラーレベル**と、赤の波線を引く**範囲**(A文字目～B文字目)、**エラーメッセージ**で生成できます。
生成したインスタンスを上記の`DiagnosticCollection`に追加、あるいは削除することで、リストを更新していきます。

#書いたコードについて

イベントリスナ内では、テキストの各行について
スペースで区切って前回生成したモデルと順番に照合しています。
照合については、単純なコマンドの他にも
**省略形**、**パラメータ**、**コメント**、空の行などがあるので注意が必要です。

このコードでは入力されるたびに全行をチェックしていますが
毎回やっていると重いので**差分の行から下だけチェック**するほうが良いはずです。
加えて、エラーの表示範囲や半角スペースの扱い等に幾つかバグがあります。
**のでリファクタリングお待ちしてます**


```ts:extension.ts
'use strict';
import * as vscode from 'vscode';
import * as path from 'path';
import patterns from './patterns';
import * as fs from 'fs';

//モードごとのコマンドのJSONを読み込み
const model_commands: { [key: string]: any } = {};
for (const mode_file_name of fs.readdirSync(path.join(__dirname, 'modes'))) {
	const command_file = fs.readFileSync(
		path.join(__dirname, 'modes', mode_file_name),
		'utf8'
	);
	const no_ext = path.basename(mode_file_name, '.json');
	model_commands[no_ext] = JSON.parse(command_file);
	console.log('loaded mode file:' + mode_file_name);
}

export function activate(context: vscode.ExtensionContext) {
	//エラー情報のリスト
	const collection = vscode.languages.createDiagnosticCollection('test');
	if (vscode.window.activeTextEditor) {
		updateDiagnostics(vscode.window.activeTextEditor.document, collection);
	}
	//テキストの変更イベント
	context.subscriptions.push(
		vscode.workspace.onDidChangeTextDocument(editor =>
			updateDiagnostics(editor.document, collection)
		)
	);
}

//エラー表示用オブジェクトを取得
function getDiag(
	lineOfText: vscode.TextLine,
	message: string,
	beginIndex: number
): vscode.Diagnostic {
	const range = new vscode.Range(
		lineOfText.lineNumber,
		// beginIndex,
		0,
		lineOfText.lineNumber,
		lineOfText.text.length
	);
	return new vscode.Diagnostic(range, message, vscode.DiagnosticSeverity.Error);
}

function getVariable(
	current_commands: { [key: string]: any },
	token: string
): { [key: string]: any } | undefined {
	for (const variable_pattern of patterns) {
		//現在使用可能なコマンドの中から、パラメータになりうるものを探す
		const matched_variable = current_commands[variable_pattern['help_string']];
		if (matched_variable) {
			//入力されている値がパラメータの入力規則とマッチするなら
			if (token.match(RegExp(variable_pattern['config_patttrn']))) {
				return matched_variable;
			}
		}
	}
}

// 先頭から一致する文字数
function getMatchCount(a: string, b: string): number {
	let i = 0;
	while (i < a.length && i < b.length && a.charAt(i) === b.charAt(i)) {
		i++;
	}
	return i;
}

//メインの関数　エディタのエラー情報を更新する
function updateDiagnostics(
	document: vscode.TextDocument,
	commandDiagnostics: vscode.DiagnosticCollection
): void {
	if (!document) {
		return;
	}

	//エラーの一覧
	let diagnostics: vscode.Diagnostic[][] = new Array(document.lineCount).fill([]);
	//現在のコンフィグのモード　modesディレクトリ中のファイル名と合わせる
	let current_mode = 'user';
	//数値を入力する箇所用の正規表現　例：<1-4094>など
	const range_pattern = /<(\d+)-(\d+)>/;
	//ダブルクオーテーションに含まれていないスペース
	const division_pattern = /(?:\s+)(?=(?:[^"]*\"[^"]*\")*[^"]*$)/;

	//各行を走査する
	for (let lineIndex = 0; lineIndex < document.lineCount; lineIndex++) {
		//このモードで使用可能なコマンド
		const commands = model_commands[current_mode];
		const lineOfText = document.lineAt(lineIndex);
		//空行やコメントなら
		if (lineOfText.isEmptyOrWhitespace || lineOfText.text.startsWith('#')) {
			continue;
		}
		const offset = lineOfText.firstNonWhitespaceCharacterIndex;
		let current_commands = commands;
		for (const token of lineOfText.text.trim().split(division_pattern)) {
			//空白なら
			if (token.match(/^\s+/)) {
				continue;
			}
			//コマンドなら
			if (current_commands[token]) {
				current_commands = current_commands[token];
				continue;
			}
			//パラメータなら
			const matched_variable = getVariable(current_commands, token);
			if (matched_variable) {
				current_commands = matched_variable;
				continue;
			}
			//コマンド補完候補
			let candidates: string[] = [];
			//現在の最長一致文字数
			let longest_match = -1;
			const token_int = parseInt(token);
			for (const usable_command of Object.keys(current_commands)) {
				if (token_int) {
					const range_matched = range_pattern.exec(usable_command);
					//パラメータの範囲が示されているとき
					if (range_matched) {
						const range_min = parseInt(range_matched[1]);
						const range_max = parseInt(range_matched[2]);
						if (range_min <= token_int && range_max >= token_int) {
							candidates.push(usable_command);
							break;
						} else {
							diagnostics[lineIndex] = [
								getDiag(lineOfText, 'This number is out of bound:' + usable_command, 0)
							];
							break;
						}
					}
				}
				if (token.length > usable_command.length) {
					continue;
				}
				//このコマンドと入力されているトークンの先頭から一致する文字数
				const current_match = getMatchCount(usable_command, token);
				//必要以上に文字がある場合
				if (current_match < token.length) {
					continue;
				}
				if (current_match > longest_match) {
					longest_match = current_match;
					candidates = [usable_command];
				} else if (current_match === longest_match) {
					candidates.push(usable_command);
				}
			}
			if (diagnostics[lineIndex].length === 0) {

				//候補のコマンドがない
				if (candidates.length === 0) {
					diagnostics[lineIndex] = [
						getDiag(lineOfText, 'This command does not exist.', 0)
					];
					break;
				}
				//候補のコマンドが2つ以上の、曖昧なコマンド
				if (candidates.length > 1) {
					diagnostics[lineIndex] = [
						getDiag(
							lineOfText,
							'This command is ambigous:' + candidates.join(', '),
							0
						)
					];
					break;
				}
				//一意に特定可能な省略形
				current_commands = current_commands[candidates[0]];
			}
		}
		if (diagnostics[lineIndex].length === 0) {
			if (!current_commands['CR']) {
				//ここで終端は出来ない、次に続くコマンドがある
				const last_char_length =
					lineOfText.text.trimRight().length === lineOfText.text.length
						? lineOfText.text.length - 1
						: lineOfText.text.trimRight().length;
				diagnostics[lineIndex] = [
					getDiag(lineOfText, "You should append a command.", 0)
				];
			} else {
				//この時点で正しい行
				//モードを移動するコマンドの場合
				if (current_commands['into']) {
					current_mode = current_commands['into'];
				}
			}
		}
	}
	commandDiagnostics.set(document.uri, Array.prototype.concat.apply([], diagnostics));
}

// this method is called when your extension is deactivated
export function deactivate() {}



```

コード中で参照している`patterns.ts`には、以下のようにパラメータ用の正規表現がつらつらと書いてあります。
出来は良くないです。適宜差し替えて下さい。

```ts:patterns.ts

export default [
	{
		help_string: 'A.B.C.D',
		config_patttrn: /^(?:(?:^|\.)(?:2(?:5[0-5]|[0-4]\d)|1?\d?\d)){4}$/
	},
	{
		help_string: 'A.B.C.D/M',
		config_patttrn: /^(?:(?:^|\.)(?:2(?:5[0-5]|[0-4]\d)|1?\d?\d)){4}\/(?:[1-2]?\d|3[0-2])$/
	},
	{
		help_string: 'X:X::X:X',
		config_patttrn: /^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$/
	},
	{
		help_string: 'X:X::X:X/M',
		config_patttrn: /^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))\/(?:[0-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8])$/
	},
	{
		help_string: 'XX:XX:XX:XX:XX:XX',
		config_patttrn: /^[0-9a-f]{2}(:[0-9a-f]{2}){5}$/
	},
	{
		help_string: 'PORTRANGE',
		config_patttrn: /^[-,/\d]+$/
	},
	{
		help_string: 'LAGNO',
		config_patttrn: /^\d|[1-5]\d|6[0-4]+$/
	},
	{
		help_string: 'MLAGNO',
		config_patttrn: /^[\w_\/]+$/
	},
	{
		help_string: 'MLAGRANGE<1-64>',
		config_patttrn: /^\w+\/[-\d,]+$/
	},
	{
		help_string: 'PORTNO',
		config_patttrn: /^1\/(?:\d|[1-4]\d|5[0-2])$/
	},
	{
		help_string: 'YYYYMMDD',
		config_patttrn: /^(20\d{2})(\d{2})(\d{2})$/
	},
	{
		help_string: 'HH:MM:SS',
		config_patttrn: /^[0-9]{2}:[0-9]{2}:[0-9]{2}$/
	},
	{
		help_string: 'OFFSET',
		config_patttrn: /^(?:\+|-)[0-9]{2}:[0-9]{2}:[0-9]{2}$/
	},
	{
		help_string: 'SECOND',
		config_patttrn: /^0|[1-9][0-9]{1,5}|1000000$/
	},
	{
		help_string: 'GROUPRANGE',
		config_patttrn: /^[1-9]|[1-9][0-9]|[1-4][0-9]{2}|50[0-9]|51[0-2]$/
	},
	{
		help_string: 'AUTH_PASSWORD',
		config_patttrn: /^(?:[\w]+|"[\w]+")$/
	},
	{
		help_string: 'PRIV_PASSWORD',
		config_patttrn: /^(?:[\w]+|"[\w]+")$/
	},
	{
		help_string: 'POLICY-NAME',
		config_patttrn: /^(?:[\w]+|"[\w]+")$/
	},
	{
		help_string: 'WORD',
		config_patttrn: /^(?:[\w]+|"[\w]+")$/
	},
	{
		help_string: 'USERNAME',
		config_patttrn: /^(?:[\w]+|"[\w]+")$/
	},
	{
		help_string: 'LINE',
		config_patttrn: /^(?:[\w]+|"[\w ]+")$/
	},
	{
		help_string: 'FILENAME',
		config_patttrn: /^(?:[\w.]+|"[\w.]+")$/
	},
	{
		help_string: 'NAME',
		config_patttrn: /^(?:[\w]+|"[\w]+")$/
	},
	{
		help_string: 'ENGINEID',
		config_patttrn: /^(?:[\w]+|"[\w]+")$/
	},
	{
		help_string: 'OID',
		config_patttrn: /^(?:\d+\.)*[\d]$/
	}
];

```

#困っていること
ご存知の方居たら教えて下さい。
・ビルドしている筈なのにoutディレクトリに最新のソースが反映されないことがある


#つづく

今日の日曜大工はこれでおしまいです。
また進捗があったら続きを書きます。