概要
いつにもまして、タイトルが長いですね。
昨今の一般的な開発環境は、バックグラウンドで色々な処理を行なっており、そのひとつにバリデーションがあります。今回の記事では、これまでの一連の実装をベースに、VSCodeのワークスペース下のあるファイルが更新されたとき、その更新されたファイルを参照するファイルに対してバリデーションを行う、といったことを実現します。
なお分量を考慮して、検討中心と実装中心の2回の記事に分けることにします。
動機
DSL関連技術を使って簡易的な言語を定義する目的は、ドメインの知識を有する人を前提に
- ドメインに関する最低限のコードを書くだけで良い
- 入力補完に対応し、ユーザによる入力負荷を可能な限り減らす
- 既存のファイルを可能な限り流用する
だと考えます。
ドメイン~(略)は、DSLを定義する時点で追求する部分でまさにドメイン固有部分。入力補完はもはやあって党是園の機能な感じですね。既存の~(略)は、より本格的な開発環境を実現するにあたっては外せないことでありながら、その情報が少ないのが実状です。ここで、既存のファイルとはjson形式のファイルとします。
上記の情報が少しでも参考になれば幸いです。
開発環境
- Windows10 Pro 1909
- VSCode 1.47
- JDK 1.8
- Xtext 2.24
- LSP4J 0.10.0
課題
実現にあたっては
- VSCodeのワークスペース下にあるjsonファイルの更新通知をハンドルすること
- jsonファイルのフォーマットは、keyはnames、valueは文字列であること
- jsonファイルが更新された場合、サーバーはそれを参照するmydslファイルをバリデーションすること
- バリデーション時、サーバーは参照するjsonファイルが保持する情報を読み取り、その結果をメモリに保持すること
- ユーザーから入力補完が指定された場合、jsonファイルから読み取ったvalueを入力補完の候補として表示すること
- 参照するjsonファイルから情報を読み取りにある程度の時間を要する場合、サーバーは進捗状況を表示すること
- mydslファイルはjsonファイルを参照可能な文法になっていること
を満たすことを課題とします。
検討
課題上げた項目のうち、重要と考える項目を少し掘り下げます。
ワークスペース下にあるjsonファイルの更新をハンドルする
毎度のことですが、LSPの仕様を確認するとDidChangeWatchedFiles Notificationが使えそうです。
また、現状のLanguageServer実装のdidChangeWatchedFilesメソッドにログ出力を実装し、VSCodeで簡単なファイル操作を行い、得られたログとの対応関係を確認しました。
結果は
- VSCodeは、ワークスペースフォルダ以下のファイルに変化があると、その変化をLSP経由でサーバー側に通知する
- 通知してくる変化の種類には、Created、Deleted、Changedがある
- ファイルリネームの場合は、Created、Deletedの順に連続して通知される
- 変化したファイルには、git関連など直接の編集対象でないファイルを含め、すべてが含まれる
- 変化の対象はファイルだけで、フォルダ関連の変化は通知されない
- VSCode以外、例えばエクスプローラーなどによるファイル変更時にも、ワークスペースフォルダ以下のファイルである限り、変化が通知される
と分かりました。つまり、jsonファイルの更新も通知に含まれるので、それを拡張子でフィルタするなどによりハンドルするのは難しくはないでしょう。
jsonファイルのフォーマット
keyはnames、valueは文字列とすることは上述のとおりです。
具体例としては以下です。
{
"names" : [
"MPS",
"VSCode",
"VisualStudio",
"Eclipse"
]
}
jsonファイルを参照するmydslファイルをバリデーションする
これは言い換えると、参照される側のリソースに変化が生じたので、参照している側に変化があったとみなして、バリデーションを実施する、になります。先人たちも同じことを考えたに違いないと検索した結果、EclipseのForumでTriggering "re-validation" of non-dirty resourceという記事を見つけました。記事によると、DefaultResourceDescriptionManager.isAffectedメソッドをオーバーライドすれば良さそうなので、例に倣って、
public class MyDslRuntimeModule extends AbstractMyDslRuntimeModule {
public Class<? extends IResourceDescription.Manager> bindIResourceDescription$Manager() {
return MyDSLResourceDescriptionManager.class;
}
}
public class MyDSLResourceDescriptionManager extends DefaultResourceDescriptionManager {
@Override
public boolean isAffected(Collection<Delta> deltas, IResourceDescription candidate, IResourceDescriptions context) {
return super.isAffected(deltas, candidate, context);
}
}
と適切と思う箇所にコードを追加してみたものの、そもそもjsonファイル更新時にisAffectedメソッドが実行されません…。詳細な検討過程は省きますが、isAffectedメソッドより前段階の
public class Indexer {
(略)
public Indexer.IndexResult computeAndIndexAffected(BuildRequest request, BuildContext context) {
(略)
}
このメソッドでjsonファイルがフィルタされ、isAffectedメソッドによる判定に到達しません。関連する処理はbind~メソッドを駆使して独自クラス側に処理を誘導すればできそうなものの、現在の自分の技量を考えると、標準実装に適切に独自の変更を加えていくための作業量が読めません。
以上から、独自の処理で乗り切ることにします。つまり、mydslファイルの内容からdirtyと認識されたjsonファイルを参照しているものを探し、見つかったmydslファイルをjsonファイルの代わりにdirtyとして扱うようする。それをBuildManagerの
protected List<IResourceDescription.Delta> internalValidate(CancelIndicator cancelIndicator) {
List<URI> allDirty = new ArrayList<>(dirtyFiles);
Multimap<ProjectDescription, URI> project2dirty = HashMultimap.create();
for (URI dirty : allDirty) {
//ここの処理を変更
}
}
//ここの処理とコメントした箇所に変更を入れれば、バリデーションまでつながるはずです。なお、ファイルの内容が変化していなくてもバリデーションを実行するのは問題ないことは確認済みです。
入力補完が指定された場合、jsonファイルから読んだ文字列をその候補として表示する
こちらのまとめで一覧できるようにしていますが、入力補完の通知をハンドルする部分は対応済です。
今回取り組むのは、xtext文法の
Greeting:
'Hello' name=ID ('from' from=[Greeting])? '!';
のname=IDの部分で入力補完を指定された場合に、jsonファイルの中身を補完候補として表示することとします。それを受け持つのが、このクラス
package org.xtext.example.mydsl.ide.contentassist
class MyDslIdeContentProposalProvider extends IdeContentProposalProvider {
(略)
}
なのですが、実装言語はxtendです。Eclipse環境での実装を前提とすれば、Eclipseにプラグインを入れることでxtendの各種編集支援機能が使えるので生産性は落ちにくいのですが、VSCodeにはEclipseのプラグインに該当するものが実質ありません1。javaのコードがほぼそのまま書けるとはいえ、リファレンスを調べながら実装することが多い現状では、生産性が落ちることは否めません。この機にjavaに変換します。
mydslファイルはjsonファイルを参照可能にする
Helloとどことなく雰囲気が合うように、xtextの文法を次のフォーマットをサポートできるように見直します。あわせて、シンタックスハイライトの変更で、inviteにも色がつくよう取り組む予定です。
invite "ide.json"
(略)
Hello Xtext!
次回、これで実装をします。