#概要
前回の記事に続き、サーバー側でテキストの折りたたみ表示に対応しました。Xtext Visual Studio Code Exampleでは、テキストの折りたたみ機能は執筆現在で未対応です。であればということで取り組んだ結果、LSP関連について今までより多く把握する必要があり、調査から実装までをこなせるいい機会となりました。
これらが誰かのお役に立てば幸いです。
#開発環境
- Windows10 Pro 1909
- VSCode 1.47
- JDK 1.8
- Xtext 2.20
- LSP4J 0.9.0
#課題
折りたたみ機能が目指すところは、似た記述の行が連続する部分の表示を省略/展開可能とし、編集時の見通しを上げる。これに尽きるでしょう。
具体的には、Xtext Visual Studio Code Exampleのサンプル
Hello Xtext!
Hello VSCode from Xtext!
Hello ThisFile from Other!
Hello you!
のHello~の行が続く部分を
Hello Xtext! ...
のように折りたたみ表示を実現したいと思います。
#結果
課題に対応したソースコードをGitHubに置きました。興味のある方はクローンして動作を確認してください。
#詳細
まずは最低限の仕様だけは調査、理解の上、実装へ臨みます。
##ポイント
調査の結果、ポイントとなるのは次の通りです。
- LSPの折りたたみ仕様によれば、サーバー側がFoldingRangeClientCapabilitiesで折りたたみに対応していることをクライアント側に返送する
- サーバーはクライアントからの折りたたみ範囲の問い合わせtextDocument/foldingRangeに対して***FoldingRange[]***を返送すること
- FoldingRangeのプロパティには、startLineとendLineがあり、いずれも0始まりであると明記されているが、例えば、サーバー側がマイナスを返送した場合や、startLineよりendLineのほうが小さい値のセットを返送した場合に、クライアントがどう動くのかは明記されていない
- FoldingRange[]の配列要素同士が入れ子の関係にない場合、例えばサーバー側が1つ目の要素が(0,3)、2つめの要素が(1,4)とクライアントに返送した場合も、やはり、クライアントはどう動くのかは明記されていない
上記の不明なところは実装しつつ、動作を確認しながら、順次対応を進めていきます。
##折りたたみ機能対応を返送する
LSP4Jでは、このプロパティは、initializeメソッドで返送する、InitializeResultクラスのインスタンスが間接的に保持しています。つまり、InitializeResultクラスのインスタンスはServerCapabilitiesクラスのインスタンスを保持しており、それにセットすれば折りたたみ機能があることをクライアントに返送できます。ServerCapabilitiesクラスにはSetterとしてsetFoldingRangeProviderメソッドがあるので、trueを引数に呼び出すだけです。
##textDocument/foldingRangeへ応答する
折りたたみ機能があることをクライアントに返送すると、クライアントはfoldingRangeを問い合わせてきます。この問い合わせに応答するには、LSP4JではTextDocumentServiceにあるfoldingRangeメソッドをオーバーライドすれば良さそうです。
この段階では、要素の方がFoldingRangeであるArrayListを生成し、あえて空のままreturnとします。
目的は、クライアントがどんなタイミングで問い合わせを行ってくるのかを確認するためです。
##VSCodeがfoldingRangeを送ってくるタイミングを確認する
ここまでの実装でVSCodeを起動した結果、少なくとも次のタイミングでVSCodeはtextDocument/foldingRangeを送信してくるとわかりました。
- 起動直後
- マウスカーソルが行番号が書かれた右隣(折りたたみのマークが現れるエリア)に移動したとき
- 改行などでテキストの内容が変化したとき
サーバーからの応答内容によって、VSCodeの挙動が変わる可能性はあるものの、これくらいが把握できていれば、冒頭であげた動作確認には十分でしょう。
##サーバーがイレギュラーな応答を返したときのVSCodeの挙動を確認する
サーバーからの応答は、FoldingRangeクラスのインスタンスを生成し、0起点の開始行数と終了行数を返送内容に含めるだけだったので、難しくはありません。
続けて、サーバーによるイレギュラーな応答と、VSCodeの動きを確認しました。
確認の前提は、a.mydslを用いること、内容は4行、としています。
-
startLineまたはendLineのいずれかがマイナスの値の場合
カーソルを持っていっても折りたたみ矢印が出てこなかったので、VSCodeは無効な値として無視しているのでしょう。 -
startLineよりendLineのほうが小さい場合
マイナスの値の場合と同じ挙動でした。 -
FoldingRange[]の配列要素同士が入れ子の関係にない場合
サーバー側から1つ目のFoldingRangeインスタンスが(0,3)、2つめのFoldingRangeインスタンスが(1,4)とVSCodeに返送してみました。初期状態では、折りたたみ箇所が2つ確認できるので、サーバーの返送内容通りです。
2つ目たたむとこうなりました。全部折りたたまれて見えるのは1行目だけです。
一旦全て展開し、1つ目を先にたたむと2つ目の折りたたみ箇所が見えなくなりました。
サーバーからの返送内容が正しい入れ子でなくてもVSCodeはそれなりに動く、ということがわかりました。階層構造がきちっとした文法であれば、これを使用する場面が個人的には思いつかないのですが、できるから対応しておく、というVSCodeの設計方針なのかもしれません。
##textDocument/foldingRangeへ適切に応答する
以上を踏まえつつ、foldingRangeメソッドを実装します。Hello~の行が続く部分を折りたたむには、そのブロックが何行目から何行目までなのかを判定する必要があります。いくつかの案はあると思うものの、改行時などの変更を考慮すると、
- Hello~の行が続く部分を1つオブジェクトとして扱う
- NodeModelUtilでオブジェクトのINodeインスタンス取得する
- INodeインスタンスの開始行と終了行を取得する
が簡単だと思うので順に対応します。
Hello~の行が続く部分を1つオブジェクトとして扱うには、文法を見直す必要があります。mydsl言語の文法定義の現状は
Model:
greetings+=Greeting*;
Greeting:
'Hello' name=ID ('from' from=[Greeting])? '!';
となっており、これを
Model:
greetingBlock=GreetingBlock;
GreetingBlock:
greetings+=Greeting*;
Greeting:
'Hello' name=ID ('from' from=[Greeting])? '!';
のように、GreetingBlockルールを定義し、そのオブジェクトであるgreetingBlockをModel.getGreetingBlockメソッドで取得できるようにします。
取得したオブジェクトのままでは行番号がわからないので、NodeModelUtilのgetNodeメソッドを用いて、オブジェクトのINodeインスタンスを取得します。
INodeインスタンスのgetStartLineメソッドとgetEndLineメソッドを呼ぶことで、文書にあたる1起点の行数が取得できます。
得られた行数をこのまま使いたいところですが、上述したとおり、FoldingRangeインスタンスの開始と終了は0起点になるので、マイナス1する必要があります。
さて、実装を終えてVSCodeを起動すると、折りたたむ前は
でした。これを折りたたむと
意図通りに折りたたまることが確認できました。また、
2行目と3行目の間に改行を入れても、
改行後に合わせて折りたたまることが確認できました。