xtextによるVSCodeのエクステンションに、被参照ファイル更新時に参照ファイル側をバリデーションする変更を加えてみた その2

#概要
[前回](https://qiita.com/HiroInTFLab/items/b841dd448880a5668092)の記事は、タイトルに書いたことを実現するための検討がメインの内容でした。今回は検討結果に基づく実装中心の内容となります。これらの情報が少しでもお役に立てば幸いです。

#開発環境
- Windows10 Pro 1909
- VSCode 1.47
- JDK 1.8
- Xtext 2.24
- LSP4J 0.10.0

#課題
[前回](https://qiita.com/HiroInTFLab/items/b841dd448880a5668092)からの再掲ですが課題は

- VSCodeのワークスペース下にあるjsonファイルの更新通知をハンドルすること
- jsonファイルのフォーマットは、keyはnames、valueは文字列であること
- jsonファイルが更新された場合、サーバーはそれを参照するmydslファイルをバリデーションすること
- バリデーション時、サーバーは参照するjsonファイルが保持する情報を読み取り、その結果をメモリに保持すること
- ユーザーから入力補完が指定された場合、jsonファイルから読み取ったvalueを入力補完の候補として表示すること
- 参照するjsonファイルから情報を読み取りにある程度の時間を要する場合、サーバーは進捗状況を表示すること
- mydslファイルはjsonファイルを参照可能な文法になっていること

です。

#結果
VSCodeから入力補完を行った結果…
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94018/6d3b42c5-8571-a97a-7236-a30b0be4df76.png)
となりました。
```name```という標準の選択肢が出ている点はあるものの、入力補完の候補にjson内で定義されたものが表示されています。
inviteキーワードにも色が付きました。

#詳細
ポイントだけに絞って書きます。
ソースコード全体とすべての変更点を確認したい方は[GitHub](https://github.com/tfLabs/xtext-languageserver-example)を見てください。

##mydslファイルはjsonファイルを参照可能な文法にする
折りたたみ表示に対応したときと同じように、BlockとBlock内の要素の2段階で定義します。
具体的には、invite1行に当たるInvitationルールと、Invitationが1以上からなるInvitationBlockルールと、InvitationBlockルールは未定義でもエラーとならないよう最上位ルールのModelには0以上、を追加します。
Invitationルールは、キーワード```invite```に続いて文字列を受け付けるように定義します。文字列をファイルパスとして扱う部分はバリデーション時に行うこととします。
なお、InvitationBlockは今回の例では定義することは必須ではないのですが、今後inviteの折りたたみ表示をサポートする場合を考慮して、予め定義しておきます。

```
Model:
	(invitationBlock=InvitationBlock)?
	greetingBlock=GreetingBlock
;

InvitationBlock:
	(invitaions+=Invitation)+;
（略）
Invitation:
	'invite' list=STRING;

```

##シンタックスハイライトでinviteに色を付ける
この拡張機能でのシンタックスハイライトの定義は、vscode-extension-self-contained\syntaxesフォルダの、mydsl.tmLanguage.jsonにあります。抜粋すると現状は

```json
{
（略）
        "keyword": {
            "name": "keyword.control.mydsl",
            "match": "\\b(Hello|from)\\b|!"

        }
（略）
}
```

な感じになっています。
```Hello```と```from```がキーワード指定されていることから、ここに|inviteを足すことで色付けは完了します。
なお、シンタックスハイライトは、[xtendのVSCode拡張機能のページ](https://marketplace.visualstudio.com/items?itemName=Grammarcraft.xtend-lang)にもあるとおり、さまざまなツールを駆使すればできるようです。1回は自分で書いてみたほうが学びがありそうなので取り組んでみたいものの、色付けの上書きは意外と大変そうでなので、次のテーマとしたいと思います。

##ワークスペース下にあるjsonファイルの更新をハンドルする
jsonファイルの更新時に、LanguageServerImplクラスのdidChangeWatchedFilesメソッドが呼ばれるのは調査済みです。didChangeWatchedFilesメソッドの先をバリデーションにつなげたいので、

```java
	@Override
	public void didChangeWatchedFiles(DidChangeWatchedFilesParams params) {
		runValidatable(() -> toBuildable(params));
	}
```

のように、runBuidableメソッドから、バリデーションまでを呼ぶように変更したrunValidatableメソッドの呼び出しに変えます。あわせてDidChangeWatchedFilesParamsを引数に受けるtoBuildableメソッドも

```java
	/**
	 * Evaluate the params and deduce the respective build command.
	 */
	protected Buildable toBuildable(DidChangeWatchedFilesParams params) {
（略）
		return workspaceManager.didChangingFiles(dirtyFiles, deletedFiles);
	}
```

のように、didChangedメソッドからFiledidChangingFilesメソッドの呼び出しに変更します。

## jsonファイルを参照するmydslファイルをバリデーションする
didChangingFilesメソッドの呼び出し後、変更のあったファイルに対してバリデーション実行を指示する役割を担うのは、BuildManagerです。さらに変更のあったファイルを実際に処理しているのは、internalValidateメソッドです。

```java
    protected List<IResourceDescription.Delta> internalValidate(CancelIndicator cancelIndicator) {
        List<URI> allDirty = new ArrayList<>(dirtyFiles);
Multimap<ProjectDescription, URI> project2dirty = HashMultimap.create();
        for (URI dirty : allDirty) {
            //ここの処理を変更
        }
    }

```

[前回](https://qiita.com/HiroInTFLab/items/b841dd448880a5668092)の記事で、```ここの処理を変更```すれば良いとしたとおり、jsonファイルを参照しているすべてのmydslファイルを抽出する処理が必要です。それをgetAffectedメソッドとしてWorkspaceManagerクラスに追加します。

```java
			List<URI> affedted = workspaceManager.getAffected(dirty);
			for (URI ref : affedted) {
				project2dirty.put(workspaceManager.getProjectManager(ref).getProjectDescription(), ref);
			}
```

なお、getAffectedメソッドではmydslファイルを直接参照する処理をWorkspaceManagerに実装するのは避けました。WorkspaceManagerという名前以上の責務を担うことになると考えたためです。そこで、Xtextファイルを読み取ることに特化した新規のExternalContentクラスに任せることにします。

```java
	public List<URI> getAffected(URI dirty) {
		ExternalContent ec = new ExternalContent();
		List<URI> affedted = ec.getAffected(dirty, getXtextResource());
		return affedted;
	}
```

ExternalContentクラスについてはGitHubのコードを確認してください。

## バリデーション時、サーバーは参照するjsonファイルを読み取る
バリデーションを担っているのは、MyDslValidatorクラスです。Xtextのバリデーションは、文法で定義したルール毎にチェック用のメソッド（@Checkをメソッドに付与する）を定義可能なようになっています。
ここでは、inviteでファイルが1行以上指定された場合と、inviteが1行も指定されない2つのケースを考慮した実装とします。ルールの定義次第なところはありますが、2つのケースを考慮する理由は、ルールに合致する記述をユーザーが入力していない場合、チェック用のメソッドが呼ばれないためです。
inviteルールに合致する記述がない場合に備え、inviteルールを包含するModelルールのチェック用のメソッドを追加して記述がない場合を扱えるようにします。

```java
	@Check
	public void checkModel(Model model) {
		InvitationBlock invitations = model.getInvitationBlock();
		if (invitations == null) {
			Map<String, Person> persons = new HashMap<String, Person>();
			InvitationHolder.INSTANCE.set(persons);
		}
	}

	@Check
	public void checkInvitationBlock(InvitationBlock invitations) {
		URI xrUri = invitations.eResource().getURI();
		String xPath = toAbsolutePath(xrUri);
		Path p = Paths.get(xPath);
		Path parent = p.getParent();

		List<String> list = getInvitationList(invitations.getInvitaions());
		List<String> paths = getInvitationListPath(list, parent);

		for (String path : paths) {
			Map<String, Person> persons = new HashMap<String, Person>();
			try {
				ObjectMapper mapper = new ObjectMapper();
				Person i = mapper.readValue(new File(path), Person.class);
				persons.put(path, i);
			} catch (Exception e) {
				e.printStackTrace();
			}
			InvitationHolder.INSTANCE.set(persons);
		}
	}
```

なお、jsonファイルの読み込みは、jacksonを使うことにします。
標準では使えないので、gradleで取得できるように記述を追加します。執筆時点の最新である2.13.0を使うことにします。

```gradle
dependencies {
（略）
	// https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind
	compile group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.13.0'
	// https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-core
	compile group: 'com.fasterxml.jackson.core', name: 'jackson-core', version: '2.13.0'
	// https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-annotations
	compile group: 'com.fasterxml.jackson.core', name: 'jackson-annotations', version: '2.13.0'	
}
```

## 入力補完が指定された場合、jsonファイルから読んだ文字列をその候補として表示する
入力補完が指定された場合に呼び出されるクラスは、MyDslIdeContentProposalProviderクラスです。これの実体はMyDslIdeContentProposalProvider.xtendに実装されています。xtendの文法が頭に入っている人であれば問題ないと思うのですが、xtendはVSCodeの編集支援が得られる現状にないので、この機にjava化します。

###下準備
java化は簡単です。
この時点でxtend-gen\org\xtext\example\mydsl\ide\contentassistフォルダ内に、MyDslIdeContentProposalProvider.javaファイルが生成されています。そのファイルをxtendファイルがある、src\org\xtext\example\mydsl\ide\contentassistフォルダにコピーします。
その後、MyDslIdeContentProposalProvider.xtendファイルに定義されたクラス名を

```java
package org.xtext.example.mydsl.ide.contentassist
class UnusedMyDslIdeContentProposalProvider extends IdeContentProposalProvider {
（略）
}
```

のように元のクラス名とは異なるものに変更します（ここではクラス名の頭にUnusedを加えています）。これにより次にビルドの際、xtend-genフォルダにUnusedMyDslIdeContentProposalProvider.javaファイルが生成されますが、どの処理からも参照されないこともありエラーも出ません。変更量を少なくするためにこの方法としましたが、気になる場合はxtendファイル自体を削除するのがいいでしょう。

###_createProposalsメソッドの変更
上でコピーした、MyDslIdeContentProposalProvider.javaファイルを変更します。コピー直後は次のように

```java
    @Override
    protected void _createProposals(RuleCall ruleCall, ContentAssistContext context,
            IIdeContentProposalAcceptor acceptor) {
		if ((Objects.equal(this._myDslGrammarAccess.getGreetingRule(), ruleCall.getRule()) && (context.getCurrentModel() != null))) {
（略）
		}
        super._createProposals(ruleCall, context, acceptor);
    }
```
_createProposalsメソッドが実装された状態です。
省略しているので記事上では分かりらないのですが、実際のコードでは**いかにも機械的に生成されたな**という印象を受けますが、それはさておき、最初のif文のブロックの下に、IDの位置で入力補完が指定された場合を条件に新規のif文のブロックを足すことにします。
ルール名とコンテキストで区別するのが基本で、次のようなコードになります。

```java
		if ((ruleCall.getRule().getName().equals("ID")) && (context.getCurrentModel() instanceof Greeting)) {
			IdeContentProposalCreator _proposalCreator = this.getProposalCreator();
			for (String s : InvitationHolder.INSTANCE.get().keySet()) {
				Person p = InvitationHolder.INSTANCE.get().get(s);
				for (String name : p.names) {
					final ContentAssistEntry entry = _proposalCreator.createProposal(name, context);
					acceptor.accept(entry, 0);
				}
			}
		}
```

#終わりに

- 要点さえわかれば独自実装でもそこそこ早く実現できたので良かったですが、isAffectedメソッドで対処する方法を追求したかったですね
- バリデーションと入力補完で共通で参照する結果を保持するものとして、シングルトンを意識した実装で実現してみたものの、もっといい方法がありそうな気がします
- tmLanguage.jsonの編集はツールもあるようだし、時間をかけて学んでみたいです