S-JIS[2022-09-09] 変更履歴

JLine3 Completer

JLine3のCompleterのメモ。


概要

JLineは、タブキーを押すと入力候補を表示・選択できる機能がある。
デフォルトでは入力候補が空なので、何も補完できないが。

LineReaderにCompleterを指定すると入力補完されるようになる。
Completerは、JLineで入力補完の候補を管理するクラス(インターフェース)。
具象クラスとして、文字列を補完するStringsCompleterやファイル名を補完するFileNameCompleter等がある。


import org.jline.reader.Completer;
import org.jline.reader.impl.completer.StringsCompleter;
    Completer completer = new StringsCompleter("aaa", "abc", "bbb");

    LineReader lineReader = LineReaderBuilder.builder() //
        .completer(completer) //
        .build();

実行例

タブキーを押すと、StringsCompleterで指定した文字列が入力候補として表示される。

jline> ここでタブキーを押下
aaa   abc   bbb	←補完候補の一覧が表示される
jline> aaa	←さらにタブキーを押すと、候補の中から順番に選択できる
aaa   abc   bbb
jline> aここでタブキーを押下
aaa   abc	←先頭が一致する候補だけが表示される
jline> aaa 空白の後でタブキーを押すと、そこでも補完候補が表示される
aaa   abc   bbb

独自Completerの例

独自のCompleterを作る例として、SQL文の入力補完を考える。
まず、select・insert・update・deleteを候補として表示してみる。

import org.jline.reader.Candidate;
import org.jline.reader.Completer;
import org.jline.reader.LineReader;
import org.jline.reader.ParsedLine;
public class MyCompleter implements Completer {

    private List<String> list = List.of("insert", "select", "update", "delete");
    @Override
    public void complete(LineReader reader, ParsedLine commandLine, List<Candidate> candidates) {
        for (String word : list) {
            candidates.add(new Candidate(word, word, null, null, null, null, true));
        }
    }
}
    LineReader lineReader = LineReaderBuilder.builder() //
        .completer(new MyCompleter) //
        .build();

入力候補は、completeメソッドのcandidatesに追加する。
Candidateコンストラクターの第1引数が候補の文字列。
第2引数は一覧表示されるときの文字列。色を変えたりすることが出来る模様。
第7引数(最後の引数)は、後続があるかどうか。後続がある場合、補完後にコマンドライン上で空白が追加される。


ただし、これだけだと、一度補完した後にさらにタブキーを押すと、そこでも同じ候補が出てきてしまう。

jline> select 空白の後でタブキーを押すと、そこでも補完候補が表示される
delete   insert   select   update

一度単語を入力した後にさらに補完されないためには、カーソル位置に応じて補完候補を用意する必要がある。

    @Override
    public void complete(LineReader reader, ParsedLine commandLine, List<Candidate> candidates) {
        String buffer = commandLine.line().substring(0, commandLine.cursor());
        for (String word : list) {
            if (word.startsWith(buffer)) {
                candidates.add(new Candidate(word, word, null, null, null, null, true));
            }
        }
    }

commandLine.line()で、入力されている文字列全体が取れる。
commandLine.cursor()で、現在のカーソル位置が取れる。
カーソル位置までの文字列の先頭が入力候補の文字列と同じであれば、候補として表示しないようにすれば良さそう。


さらに、insertの場合は直後の単語はinto、deleteの直後はfromと決まっているので、それを補完したい。

駄目な例

入力候補に2つ目の単語を入れてみる。

    private List<String> list = List.of("insert into", "select", "update", "delete from");

これでも補完候補として扱われるのだが、空白を含む文字列は、補完されたときにエスケープされてしまう。
(エスケープしないように設定している場合は、シングルクォーテーションで囲まれてしまう)

jline> ここでタブキーを押下
delete from   insert into   select   update

↓補完後

jline> insert\ into

力技の例

自分で、入力中の文字列がinsertやdeleteであることを判別してみる。

    @Override
    public void complete(LineReader reader, ParsedLine commandLine, List<Candidate> candidates) {
        String buffer = commandLine.line().substring(0, commandLine.cursor());

        if (buffer.startsWith("insert")) {
            if (buffer.startsWith("insert into")) {
                return;
            }
            String word = "into";
            candidates.add(new Candidate(word, word, null, null, null, null, true));
            return;
        }
        if (buffer.startsWith("delete")) {
            if (buffer.startsWith("delete from")) {
                return;
            }
            String word = "from";
            candidates.add(new Candidate(word, word, null, null, null, null, true));
            return;
        }

        for (String word : list) {
            if (word.startsWith(buffer)) {
                candidates.add(new Candidate(word, word, null, null, null, null, true));
            }
        }
    }

こうして見るとなんとなく分かるが、本来はパーサー(構文解析器)を用意して、カーソル位置に応じた単語を補完候補にするのが妥当だろう。


JLine3へ戻る / JLineへ戻る / Javaへ戻る / 技術メモへ戻る
メールの送信先:ひしだま