xtextによるVSCodeのExtensionでライブプレビュー

# 概要
前回の記事[xtextによるVSCodeのExtensionをビルドしてみた](https://qiita.com/HiroInTFLab/items/ac8ffc86b5b822aa68b1)では、GitHubからCloneしたソースコードからVSCodeのExtensionをビルドした際のメモをレポートしました。
今回は、出来上がったExtensionをライブプレビューの観点で掘り下げたいと思います。
掘り下げていく文章形式のため、結論めいたものやコードは用意してません。

# ライブプレビュー
ライブプレビュー（リアルタイムプレビュー、単にプレビューとも呼ばれるようです）といえば、編集した内容が即座に別の形式に変換され、さらみ変換結果が画面で確認できることを指すと考えています。
[VS CodeでMarkdownをプレビューするには？](https://www.atmarkit.co.jp/ait/articles/1804/20/news030.html)でも示されているとおり、VSCodeはデフォルトでMarkdownドキュメントをプレビューすることをサポートしているので、簡単にできます。
Qiitaの記事を編集する機能も同様ですね。
これが前回の記事でビルドしたExtensionでもできます。

## xtextによるExtensionでは
まずは、VScodeが起動している状態から始めます。
現状では残念ですが、このExtensionでは**ウィンドウ分割と同時にPreview表示（Ctrl+K+V）**をサポートしていないので、次に示すやり方とする必要があります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94018/e69f50fc-4699-e6d8-5e0f-2a7278ad81e6.png)
画面の右上に赤枠で囲ったアイコンがあります。このアイコンにマウスカーソルを合わせると、```Split Editor Right```とを表示されるのが確認できます。ちなみに、altキーを押しながらアイコンにマウスカーソルを合わせると、```Split Editor Down```とを表示されます。芸が細かい。
altキーを押しながらアイコンクリックし、下側のウィンドウをアクティブにして```src-gen```フォルダにある```aGreeter.java```ファイルを開いてください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94018/e6272fd6-1ee1-0a8e-0302-78b51296c295.png)
ここまでで、上下に分割されたエディタエリアにファイルが表示され、このような表示になったと思います。
a.mydslファイルのどの行でもいいのですが、ここでは最終行の```You```に続けて```u```を複数回入力すると、
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94018/0fa9bc9c-3c65-9743-213d-3f2a52482b82.png)
のように、a.mydslの編集結果が即座にaGreeter.javaに```System.out.println("Hello Youuuu !");```と反映されました。
ファイル生成を伴うのでMarkdownの編集時の雰囲気と異なりますが、ライブプレビューの要件が**変更内容が即座に所望のドキュメントまたは画面に反映されること**と考えると、これで十分に要件を満たしていると考えることができます。

## なぜライブプレビューのような動作になるのか？
一方で、なぜビルドしたExtensionはこのような動作になるのでしょうか。
org.xtext.example.mydsl.generator.MyDslGeneratorクラスのdoGenerateメソッドが呼び出されていることは明らかです。

<details><summary>スタックトレース</summary><div>

```
java.lang.Throwable
	at org.xtext.example.mydsl.generator.MyDslGenerator.doGenerate(MyDslGenerator.java:25)
	at org.eclipse.xtext.generator.GeneratorDelegate.doGenerate(GeneratorDelegate.java:43)
	at org.eclipse.xtext.generator.GeneratorDelegate.generate(GeneratorDelegate.java:34)
	at org.xtext.example.mydsl.ide.server.build.IncrementalBuilder$InternalStatefulIncrementalBuilder.generate(IncrementalBuilder.java:425)
	at org.xtext.example.mydsl.ide.server.build.IncrementalBuilder$InternalStatefulIncrementalBuilder.lambda$launch$2(IncrementalBuilder.java:284)
	at com.google.common.collect.Iterators$6.transform(Iterators.java:783)
	at com.google.common.collect.TransformedIterator.next(TransformedIterator.java:47)
	at com.google.common.collect.FluentIterable.copyInto(FluentIterable.java:791)
	at org.eclipse.xtext.build.ClusteringStorageAwareResourceLoader.executeClustered(ClusteringStorageAwareResourceLoader.java:68)
	at org.eclipse.xtext.build.BuildContext.executeClustered(BuildContext.java:54)
	at org.xtext.example.mydsl.ide.server.build.IncrementalBuilder$InternalStatefulIncrementalBuilder.launch(IncrementalBuilder.java:265)
	at org.xtext.example.mydsl.ide.server.build.IncrementalBuilder.build(IncrementalBuilder.java:493)
	at org.xtext.example.mydsl.ide.server.build.IncrementalBuilder.build(IncrementalBuilder.java:470)
	at org.xtext.example.mydsl.ide.server.ProjectManager.doBuild(ProjectManager.java:106)
	at org.xtext.example.mydsl.ide.server.ProjectManager.doInitialBuild(ProjectManager.java:97)
	at org.xtext.example.mydsl.ide.server.BuildManager.doInitialBuild(BuildManager.java:185)
	at org.xtext.example.mydsl.ide.server.WorkspaceManager.refreshWorkspaceConfig(WorkspaceManager.java:180)
	at org.xtext.example.mydsl.ide.server.WorkspaceManager.initialize(WorkspaceManager.java:153)
	at org.xtext.example.mydsl.ide.server.LanguageServerImpl.lambda$initialize$0(LanguageServerImpl.java:233)
	at org.xtext.example.mydsl.ide.server.concurrent.WriteRequest.run(WriteRequest.java:38)
	at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)
	at java.util.concurrent.FutureTask.run(FutureTask.java:266)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
	at java.lang.Thread.run(Thread.java:748)
java.lang.Throwable
	at org.xtext.example.mydsl.generator.MyDslGenerator.doGenerate(MyDslGenerator.java:25)
	at org.eclipse.xtext.generator.GeneratorDelegate.doGenerate(GeneratorDelegate.java:43)
	at org.eclipse.xtext.generator.GeneratorDelegate.generate(GeneratorDelegate.java:34)
	at org.xtext.example.mydsl.ide.server.build.IncrementalBuilder$InternalStatefulIncrementalBuilder.generate(IncrementalBuilder.java:425)
	at org.xtext.example.mydsl.ide.server.build.IncrementalBuilder$InternalStatefulIncrementalBuilder.lambda$launch$2(IncrementalBuilder.java:284)
	at com.google.common.collect.Iterators$6.transform(Iterators.java:783)
	at com.google.common.collect.TransformedIterator.next(TransformedIterator.java:47)
	at com.google.common.collect.FluentIterable.copyInto(FluentIterable.java:791)
	at org.eclipse.xtext.build.ClusteringStorageAwareResourceLoader.executeClustered(ClusteringStorageAwareResourceLoader.java:68)
	at org.eclipse.xtext.build.BuildContext.executeClustered(BuildContext.java:54)
	at org.xtext.example.mydsl.ide.server.build.IncrementalBuilder$InternalStatefulIncrementalBuilder.launch(IncrementalBuilder.java:265)
	at org.xtext.example.mydsl.ide.server.build.IncrementalBuilder.build(IncrementalBuilder.java:493)
	at org.xtext.example.mydsl.ide.server.build.IncrementalBuilder.build(IncrementalBuilder.java:470)
	at org.xtext.example.mydsl.ide.server.ProjectManager.doBuild(ProjectManager.java:106)
	at org.xtext.example.mydsl.ide.server.BuildManager.internalBuild(BuildManager.java:211)
	at org.xtext.example.mydsl.ide.server.WorkspaceManager.lambda$didChangeFiles$1(WorkspaceManager.java:226)
	at org.xtext.example.mydsl.ide.server.LanguageServerImpl.lambda$runBuildable$16(LanguageServerImpl.java:459)
	at org.xtext.example.mydsl.ide.server.concurrent.WriteRequest.run(WriteRequest.java:40)
	at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)
	at java.util.concurrent.FutureTask.run(FutureTask.java:266)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
	at java.lang.Thread.run(Thread.java:748)
```
</div></details>

Eclipse環境で生成したxtextプラグイン内にあるGeneratorは、DSLファイルの保存に同期してdoGenerateメソッドが実行されるようになっており、キー押下に同期してdoGenerateメソッドは実行しません。だからこそ、以前書いた[最近よく見るライブプレビューなアプリをEclipseで作ってみた・準備編](https://qiita.com/HiroInTFLab/items/e40bef536e8f15211615)一連の記事で、ライブプレビューができる仕組みを作ってみたのですが…。
さて、その流れをdoGenerateメソッドでのコールスタックから見てみると、org.xtext.example.mydsl.ide.server.LanguageServerImplクラスのdidChangeメソッドから続く一連の処理において、org.xtext.example.mydsl.ide.server.WorkspaceManagerクラスのdidChangeFilesメソッド

<details><summary>didChangeFilesメソッド</summary><div>

```java
	public Buildable didChangeFiles(List<URI> dirtyFiles, List<URI> deletedFiles) {
		BuildManager.Buildable buildable = buildManager.submit(dirtyFiles, deletedFiles);
		return (cancelIndicator) -> {
			List<IResourceDescription.Delta> deltas = buildable.build(cancelIndicator);
			afterBuild(deltas);
			return deltas;
		};
	}
```
</div></details>

を経由し、さらに、buildable.buildメソッドの呼び出しの先に、org.xtext.example.mydsl.generator.MyDslGeneratorクラスのdoGenerateメソッドが呼ばれるようになっていました。
なお、LanguageServerImplクラスのdidChangeメソッドは、キー押下によってドキュメントの内容が変更された際にLanguage Server Protocol(LSP)が呼び出すことになっています。LSPのdidChangeメソッドのReferenceは[こちら](https://microsoft.github.io/language-server-protocol/specification#textDocument_didChange)。
よって、キー押下に同期してファイルが保存される、ということがわかりました。
なお、doGenerateメソッドについてのデバッグの詳細は[デバッグ環境](#デバッグ環境)に書いたのでそちらを参照してください。

# 保存時に同期するためのヒント
[GitHub](https://github.com/itemis/xtext-languageserver-example)の変更履歴にあるcdietrichさんはxtextのDeveloperの1人で、しかも相当にxtextに詳しいであろう人がなぜEclipseのxtextプラグインとは違う動きを実装してしまったのか。[この記事](https://github.com/itemis/xtext-languageserver-example/issues/50)でも同様の事象を止められないか質問されているのですが、cdietrichさんによる直接の解決策は提示されていません。しかし、同記事にある[リンク先](https://github.com/eclipse/n4js/pull/1612)にヒントがありました。
問題提起のタイトルこそ違うものの、内容を読むと同一原因と思われる現象が、[このGitHub](https://github.com/eclipse/n4js)上ですでに解決していそうです。そこで、この記事およびGitHubの変更を確認して、キー押下の同期から保存時の同期への変更に着手したいと思います。
詳細は別の記事に書く予定です。

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

## デバッグ環境
ソフトを開発する上で、コードを読んで意味を理解する、動きを予想する、動かした結果と過程から理解へフィードバックする、は基本サイクルのひとつですが、VSCodeでExtensionを動作させている間は結果だけが明確にわかるものの、過程についてはわかりません。

### Xtext serverにログが出る
にも関わらず、なぜdoGenerateメソッドが呼ばれているタイミングが分かったのでしょうか。
答えは偶然です。
どこかにヒントがないか、とVSCodeを舐め回すようにクリックしていたところ、赤枠で示したプルダウンにXtext Serverがあり、それを選択したところ、javaの例外スロー時のスタックトレースがそこに表示されていました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94018/af7314fe-8088-b421-619d-d3a5ef042315.png)

### printfデバッグ
であれば、ということで、[こちら](https://codechacha.com/ja/java-print-stack-trace/)の```ExceptionなくStack trace出力```を参考に、doGenerateメソッドの呼び出し時に意図的にスタックトレースを表示するようにしたところ、予想通りコンソールにスタックトレースが得られました。
なお、俗にいうprintfデバッグを行うには、標準エラー出力すなわち、System.err.printlnメソッドの呼び出しで可能なことも棚ぼたでわかり、最低限のデバッグ環境は整いました。
0から1になったことでだいぶ改善されたものの、いつまでも古典的なデバッグ方法に頼るのも心もとないし、効率もよくありません。やはりインスタンスレベルでの確認やブレークポイントを張りたいのです。さらに言えば、VSCode用のExtensionを作っているのだからVSCode上でデバッグが可能にならないか、少しずつ検討中です。

## LSP
公式サイトは[こちら](https://docs.microsoft.com/ja-jp/visualstudio/extensibility/language-server-protocol?view=vs-2019)です。
Language Server Protocolの略で、マイクロソフトが開発した技術です。
エディタの入力補完を含む機能実装において、実装言語と実現機能を切り離して自由度を上げる、再利用を可能とする、が目的の様です。
正直すごいなと思っていますが、現状はVScode用の様ですね。