xtextによるVSCodeのエクステンションに進捗表示を追加した その2


概要

前回の記事では進捗表示の機能について理解したことを書きました。その続きとしてこの記事では、VSCodeからコマンドを受信した場合に、サーバー側はコマンドの処理に時間がかかっている体で、VSCodeに進捗状況の表示を依頼する状況を実現したいと思います。
これらが誰かのお役に立てば幸いです。

開発環境

  • Windows10 Pro 1909
  • VSCode 1.47
  • JDK 1.8
  • Xtext 2.24
  • LSP4J 0.10.0

課題

サーバーがVSCodeへ通知する進捗状況は

  • VSCodeから何らかの通知を受信すると、サーバーは進捗状況の通知を行う
  • VSCodeから複数の通知を受け付けるものの、サーバーは同時並行で進捗状況を更新する最大数は3個までとし、以降は、進行中の進捗状況の更新が終了次第、次の進捗状況の通知を開始する
  • ビジー表示は3秒間とする
  • プログレスバー表示では0.1秒間で5を増分とし、100で更新を終了とする
  • 更新終了後、2秒間終了の旨を表示する
  • NOTIFICATIONSに出るタイトルにはコマンド受信時刻と処理開始時刻を併記する

を満たすものとしつつ、ある程度使い勝手を考慮した実装とすることを課題とします。

詳細

ポイントと思うところだけ書きます。ソースコード全体を確認したい方はGitHubを見てください。

進捗呼び出し方法の検討

進捗状況を呼び出すトリガを何にするか、ですが、拡張機能の説明画面

を見てみると、Commandsmydsl.a.proxymydsl.bが書かれています。
VSCodeのコマンドパレットを呼び出し(ctrl + shit + p)たところ、

コマンドが指定できそうなことは確認できました。

コマンド受信処理に問題?

ただし、コマンドを選択すると画面右下にエラーの通知が…。

Go to outputを押して確認すると、

Caused by: java.lang.ClassCastException: com.google.gson.JsonPrimitive cannot be cast to java.lang.String
    at org.xtext.example.mydsl.ide.CommandService.execute(CommandService.java:34)

と例外報知されています。実装箇所を見てみると、

public class CommandService implements IExecutableCommandService {
//略
    @Override
    public Object execute(ExecuteCommandParams params, ILanguageServerAccess access, CancelIndicator cancelIndicator) {
        if ("mydsl.a".equals(params.getCommand())) {
            String uri = (String) Iterables.getFirst(params.getArguments(), null); // java.lang.ClassCastException
            if (uri != null) {
                try {
                    return access.doRead(uri, (ILanguageServerAccess.Context it) -> "Command A").get();
                } catch (InterruptedException | ExecutionException e) {
                    return e.getMessage();
                }
            } else {
                return "Param Uri Missing";
            }
        } else if ("mydsl.b".equals(params.getCommand())) {
            return "Command B";
        }
        return "Bad Command";
    }
}

// java.lang.ClassCastExceptionとコメントを追加した行で報知されています。

String uri = (String) Iterables.getFirst(params.getArguments(), null);

デバッグしたところ、Iterables.getFirst(params.getArguments(), null)の戻り値の型はcom.google.gson.JsonPrimitiveとわかりました。幸い、getAsStringメソッドがあるので、それを使うことで、1例外報知は回避できました。

コマンド受信処理を間借りする

さて、あらたてCommandAの処理を眺めると、メソッドを呼び出すなどコマンド固有の処理を実行している様子がありません。ちょっと強引ではあるものの、

return access.doRead(uri, (ILanguageServerAccess.Context it) -> "Command A").get();

return access.doRead(uri, (ILanguageServerAccess.Context it) -> commandA(params, access)).get();

    private String commandA(ExecuteCommandParams params, ILanguageServerAccess access) {
        // 進捗状況の表示の開始を呼び出す
        return "Command A";
    }

として、進捗状況の表示のトリガとして使うことにします。
-> "Command A"commandA(params, access))と置き換えられるのはラムダ式による記述の省略を利用したものだからです。これについては【Java】ラムダ式の省略方法まとめが参考になりました。

さて次は、VSCodeから複数の通知を受け付けつつ、進捗状況を更新する最大数は3個までとする方法です。いくつか方法があると思いますが、進捗更新は個別のスレッドに任せる方針で話を進めます。

進捗状況の同時表示数を制限する

スレッドに任せる方針とすると、同時表示数を制限はjava.util.concurrent.ExecutorsのnewFixedThreadPoolメソッドを使えば難しくありません。今回は適当に3を指定しました。

通知と進捗状況を紐付ける

特定の進捗状況を更新するには、LSP3.15の仕様のProgressTokenに固有の数値または文字列を与える必要がある。LSP4JのWorkDoneProgressCreateParamsを見ると、Either<String, Number> tokenとなっており、文字列または数値で指定することになっています。固有の数値または文字列、すなわち重複を生じさせない案として

  • Numberを使って連番の数値で管理する
  • ランダムに生成された文字列を使って管理する

が考えられます。簡単なのはNumberを使うことだと思うものの、それでは情報として面白くないと考えたので、ここはランダム文字列を使ってみることにしました。

ランダム文字列を生成する

といってもイチから作るのは流石に効率が悪いので、ネット検索で出てきたものを紹介するにとどめます。
Java – Generate Random Stringで紹介されている、4. Generate Random Alphabetic String With Java 8を流用することとしました。Java8の機能を使った簡単なものという基準で選びました。数値も混ぜたい場合は5. Generate Random Alphanumeric String With Java 8もいいですね。

2021/10/12 更新
今更ですが、ランダム文字列を生成する機能がjavaの標準としてあることに気づいたので、UUIDによるランダム文字列生成に入れ替えました。使い方は

import java.util.UUID;

String ramd = UUID.randomUUID().toString();

たったこれだけです。
当初使ったものはコンパクトでシンプルと気に入っているのですが、特段の理由がない限りは、標準で用意されたほうを使うのが大抵の場合は良い選択だと思います。

結果

コマンドを何度も呼び出した際のスクリーンショットです。

タイトルに受信時刻と開始時刻に差があることも踏まえ、並行で進捗状況を更新する数が3までに制限することが実現できていると思います。


  1. 開発者がテストをしていないのか、Json関連のライブラリのバージョンが開発者が想定したものと違ってしまったのか、理由は定かではありませんが、先に進むことにします