既に nyagos.d/category/subcomplete.lua
というフレームワークがあるけれども、今回はそれを使わず新規に作ってみた1 。jj (jujutsu) と nyagos 両方のユーザは自分だけだと思うので、当面、nyagos に添付はしない。
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
ファイル数の削減
ファイル数がちょっと多くなってしまった。
- テーブルを作成する Lua コード (2. を出力する)
- テーブルを定義する Lua コード (1. が作成する)
- 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 のサブコマンド補完ができるようになった。オプションの補完が未対応だが、いつかやりたいね