xtextによるvscodeのエクステンションを保存時にgeneratorを動かすように変えてみた

#概要
[前々回の記事](https://qiita.com/HiroInTFLab/items/ac8ffc86b5b822aa68b1)でビルドしたVSCode向けエクステンションを変更し、保存時にGeneratorを起動するよう変更した記事です。なお、動きを変えるのが目的なので動画で結果を伝えるのが適切なのでしょうけど、そこまでは行っていません…。繰り返しとなりますが、記事を書いた動機は、

* 同じ変更を再利用する際の覚書として
* xtextの日本語での情報が少ない

ことなどを加味してです。
これらが誰かのお役に立てば幸いです。

#課題
 [前回の記事](https://qiita.com/HiroInTFLab/items/5336764148e4e48d5c32)で述べた通り、Clone直後にビルドしたエクステンションではライブプレビュー的な動作です。これはEclipseのプラグインと異なる動作で、操作性の統一感や直感的ではない面から、あまり好ましくないと考えます。
また弊害として、文法(とGeneratorにて)にファイル出力のパスをルール定義する拡張を行うと、ファイル名を変更するたびにファイルが生成されてしまいます[^拡張の是非]。
いずれにしても、このままではちょっと不便です。
そこで、操作性の統一感を取り戻すべく保存時にファイルを生成することを実現し、かつ、既存のソースコードを出来るだけ無変更で活用すること、を課題として取り組んでみました。
[^拡張の是非]: この拡張の是非についてはここでは議論しないことにします

#開発環境
- Windows10 Pro 1909
- VSCode 1.47
- JDK 1.8
- Xtext 2.20
- Tortoise Git 2.9.0.0

#結果
どうにか課題を解決した[^課題は解決したが]ソースコードを[GitHub](https://github.com/tfLabs/xtext-lsp-validation-anyway)に置きました。興味のある方はクローンして確認してください。
[^課題は解決したが]: ある理由から、納得のいっていない方法ですが
次でその作業の詳細を述べます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94018/7dc0153b-e55c-e1ce-bd5b-94f91032133a.png)

#方針
課題で上げた指摘は[Stop VS Code from always generating code during input #50](https://github.com/itemis/xtext-languageserver-example/issues/50)や[Don't run the generator for open files #373](https://github.com/eclipse/xtext-core/issues/373)にもあり、Xtext2.20をベースにしたエクステンションでも発生する現象のようです。
解決法には、いくつかの選択肢があったものの、saveとvalidationまでのソースコードの作りを考慮して、オリジナルのxtextのクラスを複製し、複製側に必要な変更を加える方法を取ることにしました。
主な作業を次で説明します。

#作業
##LanguageServerImplを入れ替える
クライアント（VSCode）とやり取りする、いわば起点となるクラスです。
LanguageServerImpl[^LSP]を[ここ](https://github.com/eclipse/xtext-core/blob/v2.20.0/org.eclipse.xtext.ide/src/org/eclipse/xtext/ide/server/LanguageServerImpl.java)から複製し、名前空間を変更。さらに、didSaveメソッドとdidChangeメソッドを変更します。
didSaveメソッドの中身は複製時点でdo nothingです。これを利用して、didChangeメソッドが実行する内容をdidSaveメソッド呼び出すように処理を付け替えます。一方のdidSaveメソッドは、didChangeメソッドが行っていたgenerator呼び出しの手前まで、つまりvalidationまでを行う様に複製した処理を追加します。
[^LSP]: LanguageServerImplはLanguage Server Protocol(LSP)をJavaで実装したものです。LSPについてはマイクロソフトの公式サイトを参照してください。

##ServerLauncherを入れ替える
ServerLauncherクラスは、先のLanguageServerImplクラスのインスタンスを生成する役割です。
LanguageServerImplクラスの入れ替えに伴い、ServerLauncherのパスも入れ替えの必要があります。既存のServerLauncherクラスをそのまま利用すると、ビルドでエラーになるので、中身を[ここ](https://github.com/eclipse/xtext-core/blob/v2.20.0/org.eclipse.xtext.ide/src/org/eclipse/xtext/ide/server/ServerLauncher.xtend)から複製し、名前空間を変更します。
次に、importするLanguageServerを先のLanguageServerImplクラスのパスに変更します。

##build.gradleを変更する
org.xtext.example.mydsl.ide/build.gradleを変更します。
ここにmainメソッドが実装されたServerLauncherクラスのパスを指定する箇所があるので、それを既存のServerLauncherに入れ替えます。

##WorkspaceManagerを入れ替える
LanguageServerImplの裏に控え、クライアントが開いているフォルダのパスなどを管理するクラス。
[ここ](https://github.com/eclipse/xtext-core/blob/v2.20.0/org.eclipse.xtext.ide/src/org/eclipse/xtext/ide/server/WorkspaceManager.java)から複製し、名前空間を変更。
LanguageServerImplのdidSaveメソッド、didChangeメソッドの詳細はここに実装します。

##BuildManagerを入れ替える
WorkspaceManagerとProjectManagerの間に位置するクラスです。
[ここ](https://github.com/eclipse/xtext-core/blob/v2.20.0/org.eclipse.xtext.ide/src/org/eclipse/xtext/ide/server/BuildManager.java)から複製し、名前空間を変更。
WorkspaceManagerからのdidSaveTextDocumentContentメソッドの詳細はここに実装します。

##ProjectManagerを入れ替える
BuildManagerとIncrementalBuildの間に位置するクラスです。
[ここ](https://github.com/eclipse/xtext-core/blob/v2.20.0/org.eclipse.xtext.ide/src/org/eclipse/xtext/ide/server/ProjectManager.java)から複製し、名前空間を変更。
BuildManagerからのdoValidateメソッドの詳細はここに実装します。

##IncrementalBuilderを入れ替える
実際にbuildやvalidationの行う最終段階に位置するクラスです。
[ここ](https://github.com/eclipse/xtext-core/blob/v2.20.0/org.eclipse.xtext.ide/src/org/eclipse/xtext/ide/server/ProjectManager.java)から複製し、名前空間を変更。
BuildManagerからのvalidateメソッドの詳細はここに実装します。

##MultiProjectServerLauncherを削除する
MultiProjectServerLauncherクラスが参照するメソッドがどこにもありません。そもそも、未使用にもかかわらず、gradlewでビルドエラーになるためです。

#補足
簡単ですけど、本文中で述べたことの詳細や技術に関して、触れておこうと思います。

##いくつかの選択肢
課題の解決にあたっていくつもの検討を行いました。いずれも苦行だったのですが、一方で知識や教訓も得ることが出来ました。ここでは概要だけにとどめ、詳細は別記事で書きたいと思います。

###n4jsの一部を移植
GitHubのソースコードを隈なく読んだ結果、LanguageServerImplに相当するクラスとして、XLanguageServerImplがあることが分かりました。
これを移植しつつ、必要なクラスを持ってくればおしまいか、と思ったのも束の間、

- 芋づる式に移植が必要なクラスを持ってくる、ビルドする、エラーの解消のために別のクラスを移植する、の繰り返しで疲労蓄積
- @injectにも関わらず注入クラスを明示しなければならなかった箇所があり、DIの恩恵にあやかれないのは歯痒い
などから、断念しました。

###n4jsを丸ごと移植
芋づる式に耐えかねたので、ソースコード丸ごと持ってくれば依存関係の問題はないし簡単なんじゃないかと取り組んだものの断念。例えば、

- プロジェクトの依存関係の定義はgradleで解決する必要があり、groovyの記述に慣れないとならなかった
- ソースコード中にjava9や10で導入された記述があり、Java8ベースに書き換えるのが苦痛
- deprecatedなクラスが普通に使われたままになっており、実行時のクラスキャスト例外の回避が必要だった
- injectに対してあらかじめクラスを指定するbindメソッドの呼び出しで躓いた

など数々のトラップに引っかかり、心が折れました。

##教訓
オープンソースで中身が丸わかりだからといって、別のシステムに必要な箇所だけを移植するのも、丸ごと持っていくのも、一筋縄ではいかないことが分かりました。□□にある〇〇だけを持ってくれば簡単、などという安易な考えは捨てようと実感しました。