既に nyagos.d/category/subcomplete.lua
というフレームワークがあるけれども、今回はそれを使わず新規に作ってみた1 。jj (jujutsu) と nyagos 両方のユーザは自分だけだと思うので、当面、nyagos に添付はしない。
用いるAPI
API は nyagos.complete_for["xx"]=function(args) ... end
を使う。
この関数は xx
コマンドのパラメーターにおいて TAB が押下されたタイミングで、その時点でカーソルより左の単語全部が args として渡して呼び出される。この関数にて、補完候補をテーブルとして返してやればよい。
この戻り値は結構ざっくりでよく、"aaaa" の補完に対して "bbbb" という候補がまざっていても、自動的に排除するようになっている。
補完候補の作成
手作業でやると大変なので、半自動で作る。jj
は jj -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.lua
や assert(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
ファイル数の削減
ファイル数がちょっと多くなってしまった。
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 のサブコマンド補完ができるようになった。オプションの補完が未対応だが、いつかやりたいね
- 自分で作ってみたい && 人様のコードを読むのがたいへんだからという、あまりよくない理由↩