#はじめに
ネットワーク機器のコンフィグをオシャレに書くために、VS Codeの拡張機能を作っています。
前回の記事
ネットワーク機器用のVS CodeのExtensionを作りたい 第一回:コマンド一覧の取得
#この記事でやること
Diagnostic(エラー表示)を作ります。
vs code diagnostic
などで調べると幾つか資料が見つかります。
#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
に追加、あるいは削除することで、リストを更新していきます。
#書いたコードについて
イベントリスナ内では、テキストの各行について
スペースで区切って前回生成したモデルと順番に照合しています。
照合については、単純なコマンドの他にも
省略形、パラメータ、コメント、空の行などがあるので注意が必要です。
このコードでは入力されるたびに全行をチェックしていますが
毎回やっていると重いので差分の行から下だけチェックするほうが良いはずです。
加えて、エラーの表示範囲や半角スペースの扱い等に幾つかバグがあります。
のでリファクタリングお待ちしてます
'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
には、以下のようにパラメータ用の正規表現がつらつらと書いてあります。
出来は良くないです。適宜差し替えて下さい。
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ディレクトリに最新のソースが反映されないことがある
#つづく
今日の日曜大工はこれでおしまいです。
また進捗があったら続きを書きます。