VimのtagfuncでLSPを使う
この記事はVim Advent Calendar 2019の6日目の記事です。
今年の4月にv8.1.1228でtagfuncという機能が追加されました。
こちらは:tag
や:tselect
などのタグ系コマンド*1を実行した時、tagsファイルを検索する代わりに呼ばれる関数を設定するためのオプションです。
設定する関数の形式としては次のようなものです。
" pattern: タグ検索中に使用されたタグ識別子 " flags: 関数の挙動を制御するためのフラグのリスト " 'c' -> ノーマルモードのコマンドで呼び出された " 'i' -> インサートモードのタグ補完で呼び出された " info: 以下の情報を持つ辞書 " { " 'buf_ffname': 'フルファイル名', " 'user_data': 'カスタムデータ文字列', " } function! MyTagFunc(pattern, flags, info) " タグ情報のリストを返す " もしくはv:nullを返すとtagsファイルが使われる return [ \ { \ 'name': 'タグ名', \ 'filename': 'タグが定義されているファイル名', \ 'cmd': 'ファイル内のタグを見つけるためのExコマンド', \ 'kind': 'タグの種類(Optional)', \ 'user_data': 'カスタムデータ文字列(Optional)', \ }, \] endfunction
ヘルプにはtaglist()
の結果を並べ替える関数が例として記載されていますが、LSPを使ってタグ情報のリストを生成できればタグジャンプがすごく便利になりそうです。
VimConf 2019ではsettagstack()
を使ってタグスタックを直接操作する方法を紹介しましたが、こちらは、
- タグ系のコマンドを使うために、
- キーマップの変更が必須
- Exコマンドはユーザ定義コマンドを定義する必要がある
- Vim scriptでタグスタックの管理をしなければならない
といったものでした。
これに対し、tagfunc
はオプションを設定するだけで、
- 標準のキーマップ、Exコマンドがそのまま使える
- タグ補完の時にも使える
- タグスタックの管理をVim本体に任せられる
と、すごく良さそうです。
試しに任意の文字列で検索するworkspace/symbolをvim-lspから呼び出してみます。
function! MyTagFunc(pattern, flags, info) abort let l:servers = filter(lsp#get_whitelisted_servers(), \ 'lsp#capabilities#has_workspace_symbol_provider(v:val)') if len(l:servers) == 0 echoerr 'not supported: workspace/symbol' return [] endif " タグ補完の場合はa:patternの先頭に \< がつくので削除する " (サーバ側が正規表現に対応していないかもしれないので...) let l:query = a:flags =~# 'i' ? \ substitute(a:pattern, '^\\<', '', '') : a:pattern let l:ctx = {'result': []} for l:server in l:servers call lsp#send_request(l:server, { \ 'method': 'workspace/symbol', \ 'params': { \ 'query': l:query, \ }, \ 'sync': 1, \ 'on_notification': function('s:make_taglist', [l:ctx]), \ }) endfor return l:ctx.result endfunction " ctx.resultにタグ情報を追加する func s:make_taglist(ctx, data) abort for result in a:data.response.result call add(a:ctx.result, { \ 'name': result.name, \ 'filename': lsp#utils#uri_to_path(result.location.uri), \ 'cmd': string(result.location.range.start.line + 1), \ }) endfor endfunction
上記関数をset tagfunc=MyTagFunc
で設定すると、ctagsとほぼ同じ使い勝手のタグジャンプができるようになります。
ただ、workspace/symbol
はtextDocument/definitionなどのようなカーソル位置を起点に対象を探すメソッドとは異なり、100%目当ての場所にジャンプできるとは限りません。
確実にジャンプしたい時はtextDocument/definition
を呼べるような実装にできれば良いのですが、MyTagFunc
の引数からだと、それがExコマンドの引数に指定したタグ名なのか、カーソル位置のキーワードなのかを判別できないので、メソッドの呼び分けをするのは非常に困難です。
そのため、tagfunc
に設定する関数で全部をやるのではなく、
CTRL-]
とCTRL-W ]
はtextDocument/definition
*2を呼ぶ- 定義元にジャンプしたい
- それ以外は
tagfunc
からworkspace/symbol
を呼ぶ- 同名の別メソッド/関数やインタフェースの実体などを探したい
というように、用途に応じて使い分けるのがオススメです!
結局settagstack()
とtagfunc
を両方使うことに。。。
[2019/12/06 17:00追記]
flags
で判別可能でした。
@presukuさん、ありがとうございます!
MyTagFuncのflagsで呼び分けできるような?
— presuku (@presuku) 2019年12月6日
C-]とかだと c で :tag aaa みたいなコマンドだと空だった。そういう事じゃないのかな… tagfuncでなるべく完結したいなー。
tagfunc
だけで完結させるにはMyTagFunc
を次のようにします。
function! MyTagFunc(pattern, flags, info) abort let l:ctx = {'result': []} if a:flags ==# 'c' " ノーマルモード(CTRL-]など)の場合はカーソル位置の定義に飛ぶ let l:method = 'textDocument/definition' let l:servers = filter(lsp#get_whitelisted_servers(), \ 'lsp#capabilities#has_definition_provider(v:val)') let l:ctx.pattern = a:pattern let l:params = { \ 'textDocument': lsp#get_text_document_identifier(), \ 'position': lsp#get_position(), \ } else " Exコマンド(:tag {name}など)やタグ補完の場合はa:patternで探す let l:method= 'workspace/symbol' let l:servers = filter(lsp#get_whitelisted_servers(), \ 'lsp#capabilities#has_workspace_symbol_provider(v:val)') " タグ補完の場合はa:patternの先頭に \< がつくので削除する " (サーバ側が正規表現に対応していないかもしれないので...) let l:query = a:flags =~# 'i' ? \ substitute(a:pattern, '^\\<', '', '') : a:pattern let l:params = { \ 'query': l:query, \ } endif if len(l:servers) == 0 echoerr 'not supported: ' . l:method return [] endif for l:server in l:servers call lsp#send_request(l:server, { \ 'method': l:method, \ 'params': l:params, \ 'sync': 1, \ 'on_notification': function('s:make_taglist', [l:ctx, l:method]), \ }) endfor return l:ctx.result endfunction " ctx.resultにタグ情報を追加する func s:make_taglist(ctx, method, data) abort for result in a:data.response.result if a:method ==# 'workspace/symbol' let l:name = result.name let l:location = result.location else let l:name = a:ctx.pattern let l:location = result endif call add(a:ctx.result, { \ 'name': l:name, \ 'filename': lsp#utils#uri_to_path(l:location.uri), \ 'cmd': string(l:location.range.start.line + 1), \ }) endfor endfunction
ただ一番利用頻度の高いgoplsがworkspace/symbol
に対応していないのでまだ常用できていません。。。