概要
前回の記事xtextによるVSCodeのExtensionをビルドしてみたでは、GitHubからCloneしたソースコードからVSCodeのExtensionをビルドした際のメモをレポートしました。
今回は、出来上がったExtensionをライブプレビューの観点で掘り下げたいと思います。
掘り下げていく文章形式のため、結論めいたものやコードは用意してません。
ライブプレビュー
ライブプレビュー(リアルタイムプレビュー、単にプレビューとも呼ばれるようです)といえば、編集した内容が即座に別の形式に変換され、さらみ変換結果が画面で確認できることを指すと考えています。
VS CodeでMarkdownをプレビューするには?でも示されているとおり、VSCodeはデフォルトでMarkdownドキュメントをプレビューすることをサポートしているので、簡単にできます。
Qiitaの記事を編集する機能も同様ですね。
これが前回の記事でビルドしたExtensionでもできます。
xtextによるExtensionでは
まずは、VScodeが起動している状態から始めます。
現状では残念ですが、このExtensionではウィンドウ分割と同時にPreview表示(Ctrl+K+V)をサポートしていないので、次に示すやり方とする必要があります。
画面の右上に赤枠で囲ったアイコンがあります。このアイコンにマウスカーソルを合わせると、Split Editor Right
とを表示されるのが確認できます。ちなみに、altキーを押しながらアイコンにマウスカーソルを合わせると、Split Editor Down
とを表示されます。芸が細かい。
altキーを押しながらアイコンクリックし、下側のウィンドウをアクティブにしてsrc-gen
フォルダにあるaGreeter.java
ファイルを開いてください。
ここまでで、上下に分割されたエディタエリアにファイルが表示され、このような表示になったと思います。
a.mydslファイルのどの行でもいいのですが、ここでは最終行のYou
に続けてu
を複数回入力すると、
のように、a.mydslの編集結果が即座にaGreeter.javaにSystem.out.println("Hello Youuuu !");
と反映されました。
ファイル生成を伴うのでMarkdownの編集時の雰囲気と異なりますが、ライブプレビューの要件が変更内容が即座に所望のドキュメントまたは画面に反映されることと考えると、これで十分に要件を満たしていると考えることができます。
なぜライブプレビューのような動作になるのか?
一方で、なぜビルドしたExtensionはこのような動作になるのでしょうか。
org.xtext.example.mydsl.generator.MyDslGeneratorクラスのdoGenerateメソッドが呼び出されていることは明らかです。
スタックトレース
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)
Eclipse環境で生成したxtextプラグイン内にあるGeneratorは、DSLファイルの保存に同期してdoGenerateメソッドが実行されるようになっており、キー押下に同期してdoGenerateメソッドは実行しません。だからこそ、以前書いた最近よく見るライブプレビューなアプリをEclipseで作ってみた・準備編一連の記事で、ライブプレビューができる仕組みを作ってみたのですが…。
さて、その流れをdoGenerateメソッドでのコールスタックから見てみると、org.xtext.example.mydsl.ide.server.LanguageServerImplクラスのdidChangeメソッドから続く一連の処理において、org.xtext.example.mydsl.ide.server.WorkspaceManagerクラスのdidChangeFilesメソッド
didChangeFilesメソッド
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;
};
}
を経由し、さらに、buildable.buildメソッドの呼び出しの先に、org.xtext.example.mydsl.generator.MyDslGeneratorクラスのdoGenerateメソッドが呼ばれるようになっていました。
なお、LanguageServerImplクラスのdidChangeメソッドは、キー押下によってドキュメントの内容が変更された際にLanguage Server Protocol(LSP)が呼び出すことになっています。LSPのdidChangeメソッドのReferenceはこちら。
よって、キー押下に同期してファイルが保存される、ということがわかりました。
なお、doGenerateメソッドについてのデバッグの詳細はデバッグ環境に書いたのでそちらを参照してください。
保存時に同期するためのヒント
GitHubの変更履歴にあるcdietrichさんはxtextのDeveloperの1人で、しかも相当にxtextに詳しいであろう人がなぜEclipseのxtextプラグインとは違う動きを実装してしまったのか。この記事でも同様の事象を止められないか質問されているのですが、cdietrichさんによる直接の解決策は提示されていません。しかし、同記事にあるリンク先にヒントがありました。
問題提起のタイトルこそ違うものの、内容を読むと同一原因と思われる現象が、このGitHub上ですでに解決していそうです。そこで、この記事およびGitHubの変更を確認して、キー押下の同期から保存時の同期への変更に着手したいと思います。
詳細は別の記事に書く予定です。
補足
簡単ですけど、本文中で述べたことの詳細や技術に関して、触れておこうと思います。
デバッグ環境
ソフトを開発する上で、コードを読んで意味を理解する、動きを予想する、動かした結果と過程から理解へフィードバックする、は基本サイクルのひとつですが、VSCodeでExtensionを動作させている間は結果だけが明確にわかるものの、過程についてはわかりません。
Xtext serverにログが出る
にも関わらず、なぜdoGenerateメソッドが呼ばれているタイミングが分かったのでしょうか。
答えは偶然です。
どこかにヒントがないか、とVSCodeを舐め回すようにクリックしていたところ、赤枠で示したプルダウンにXtext Serverがあり、それを選択したところ、javaの例外スロー時のスタックトレースがそこに表示されていました。
printfデバッグ
であれば、ということで、こちらのExceptionなくStack trace出力
を参考に、doGenerateメソッドの呼び出し時に意図的にスタックトレースを表示するようにしたところ、予想通りコンソールにスタックトレースが得られました。
なお、俗にいうprintfデバッグを行うには、標準エラー出力すなわち、System.err.printlnメソッドの呼び出しで可能なことも棚ぼたでわかり、最低限のデバッグ環境は整いました。
0から1になったことでだいぶ改善されたものの、いつまでも古典的なデバッグ方法に頼るのも心もとないし、効率もよくありません。やはりインスタンスレベルでの確認やブレークポイントを張りたいのです。さらに言えば、VSCode用のExtensionを作っているのだからVSCode上でデバッグが可能にならないか、少しずつ検討中です。
LSP
公式サイトはこちらです。
Language Server Protocolの略で、マイクロソフトが開発した技術です。
エディタの入力補完を含む機能実装において、実装言語と実現機能を切り離して自由度を上げる、再利用を可能とする、が目的の様です。
正直すごいなと思っていますが、現状はVScode用の様ですね。