標準愚痴出力

個人的なIT作業ログです。もしかしたら一般的に参考になることが書いているかもしれません(弱気

nyagos 向けに jj のサブコマンドの補完関数を書いてみる

既に nyagos.d/category/subcomplete.lua というフレームワークがあるけれども、今回はそれを使わず新規に作ってみた1jj (jujutsu) と nyagos 両方のユーザは自分だけだと思うので、当面、nyagos に添付はしない。

用いるAPI

APInyagos.complete_for["xx"]=function(args) ... end を使う。

この関数は xx コマンドのパラメーターにおいて TAB が押下されたタイミングで、その時点でカーソルより左の単語全部が args として渡して呼び出される。この関数にて、補完候補をテーブルとして返してやればよい。

この戻り値は結構ざっくりでよく、"aaaa" の補完に対して "bbbb" という候補がまざっていても、自動的に排除するようになっている。

補完候補の作成

手作業でやると大変なので、半自動で作る。jjjj -h もしくは jj サブコマンド -h を実行すると、ヘルプが出る。この時、行頭が空白二つだけで終わる行に、サブコマンドが書いてある。

例:

$ jj -h
Jujutsu (An experimental VCS)

Usage: jj.exe [OPTIONS] <COMMAND>

Commands:
  abandon    Abandon a revision
  backout    Apply the reverse of a revision on top of another revision
  branch     Manage branches
  :(以下略)

これを読み取る。

function getUsage(command)
    print("$ " .. command)
    local subcommand = {}
    local fd = assert(io.popen(command))
    for line in fd:lines() do
        local m = string.match(line,"^  ([a-z][-a-z]+)")
        if m then
            subcommand[m] = {}
        end
    end
    fd:close()
    return subcommand
end

local jj = getUsage("jj -h")
for name,_ in pairs(jj) do
    if name ~= "help" then
        if string.sub(name,1,1) ~= "-" then
            jj[name] = getUsage("jj ".. name .. " -h")
        end
    end
end

これを実行すると、jj のサブコマンドと、サブコマンドのサブコマンドがテーブル jj が次のような感じに格納される。

jj={
    ["abandon"]={},
    ["backout"]={},
    ["branch"]={
        ["create"]={},
        ["delete"]={},
:以下略

補完候補のセーブ

ヘルプの呼び出しを毎回やっていては、すごく時間がかかるので、作成は手元で一回だけ行い、利用時は作成済みのものを使用するものとする。

テーブルの保存は JSON などで行えれば一番よいが、Lua からの読み書きは大変なので、簡単な汎用シリアライズ関数を作った。

function dump(fd,obj,indent)
    local t = type(obj)
    if t == "string" then
        fd:write('"'..obj..'"')
    elseif t == "number" then
        fd:write(obj)
    elseif t == "table" then
        fd:write("{")
        for key,val in pairs(obj) do
            fd:write("\n"..string.rep("    ",indent+1).."[")
            dump(fd,key,indent+1)
            fd:write("]=")
            dump(fd,val,indent+1)
            fd:write(",")
        end
        if next(obj) then
            fd:write("\n"..string.rep("    ",indent).."}")
        else
            fd:write("}")
        end
    elseif t == "boolean" then
        if obj then
            fd:write("true")
        else
            fd:write("false")
        end
    else
        fd:write("nil")
    end
end

この関数を dump(fd,obj,0) などと呼び出すと、obj の内容を「ロード可能なLuaソースコード」形式で、ファイルハンドル fd に対して出力する。本来であれば、テーブルの値やキーに二重引用符や改行などが含まれていても問題ないようにエスケープ処理が必要だが、今回は文字列は確実に英単語なので、そこまでやっていない。

これをこのように呼び出せば、share.jj に補完テーブルを設定する Lua コードが出力される。

local fd = assert(io.open("complete-jj.lua","w+"))
fd:write("share.jj=")
dump(fd,jj,0)

これでセーブされたコードは lua_f complete-jj.luaassert(loadfile("complete-jj.lua"))() などでロードできる。

補完関数本体

今回は手を抜いて、オプション文字列はスキップするようにした。

nyagos.complete_for["jj"] = function(args)
    if not string.match(args[#args],"^[-a-z]+") then
        return nil
    end

    local j = share.jj
    local last = nil
    while true do
        repeat
            table.remove(args,1)
            if #args <= 0 then
                return last
            end
            last = args[1]
        until string.sub(last,1,1) ~= "-"

        local nextj = j[ last ]
        if not nextj then
            local result = {}
            for key,val in pairs(j) do
                result[#result+1] = key
            end
            if next(result) then
                return result
            else
                return nil
            end
        end
        j = nextj
    end
end

ファイル数の削減

ファイル数がちょっと多くなってしまった。

  1. テーブルを作成する Lua コード (2. を出力する)
  2. テーブルを定義する Lua コード (1. が作成する)
  3. 2 をロードして、補完関数を定義する Lua コード

3つはちょっと多い。ということで、2 と 3 を統合するよう 1 を改造しよう。1 の中で 2 を出力する際に、3 の関数も含めてしまえばよい。そうするとロード処理が省略できる。

local fd = assert(io.open("complete-jj.lua","w+"))
fd:write("share.jj=")
dump(fd,jj,0)

fd:write([[

nyagos.complete_for["jj"] = function(args)
    if not string.match(args[#args],"^[-a-z]+") then
        return nil
    end
    :
    : 中略
    :
]])

fd:close()

これでファイルは二つになった。テーブルを作るソースは make-complete-jj.lua、作られたテーブルを使って補完関数を定義するソースを complete-jj.lua としよう

一応、最終成果も gist に張り付けときます

https://gist.github.com/hymkor/3eafc73125c5b5306c35771842c39f4a

~/.nyagos からロード

こんなコードを追記した。

for _,fname in pairs{"gmnlisp_.lua","complete-jj.lua"} do
    local fullpath = nyagos.pathjoin(nyagos.env.userprofile,"Share\\etc\\" .. fname)
    local fd=io.open(fullpath)
    if fd then
        fd:close()
        print("loadfile " .. fullpath)
        assert(loadfile(fullpath))()
    end
end

もともと ~/Share/etc/gmnlisp_.lua というファイルがあったらロードするというコードだが、それを ~/Share/etc/complete-jj.lua というファイルも見るようにしただけ。

これで jj のサブコマンド補完ができるようになった。オプションの補完が未対応だが、いつかやりたいね


  1. 自分で作ってみたい && 人様のコードを読むのがたいへんだからという、あまりよくない理由