goplsでworkspace/symbolが使いたい
現状の最新版(gopls/v0.2.2-pre2)では未実装。
を書いていたら欲しくなったので第1回 ゴリラ合宿で実装したのがコチラ。
symbolの探索はServerのsessionのViews()から
Snapshotを取ってきて、
KnownPackages()のCompiledGoFiles()をParse()
して*ast.Fileを引っ張り出して、
あとは地道にWorkspaceSymbolParamsにマッチするものを探していくようにした。
ただ、WorkspaceSymbolParams
の仕様*1がよくわかっておらず、実装の仕方も上記のやり方で良かったのか全然わかっていない。
また、最初は素直にスライスを作るように実装してみたところ、Vimから:tselect
などを実行すると
- タグが重複したり、
- 実行するたびに順番が変わったり、
するので、
重複を排除した上で、Vimで指定した文字列に近く、現在のパッケージに近いものを優先するようにしてみた。
とりあえず自分用としては使えそうなので色々と試してみることにする。
*1:特にQuery
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
に対応していないのでまだ常用できていません。。。
VimConf 2019に行ってきた
今年もVimConf 2019に行ってきました。
今回はテーマが “how to be more productive with Vim?”
ということもあったのか、比較的Vim暦が浅い人の登壇が多かったような気もしますが、その割には発表内容が素晴らしいものばかりだったので、やっぱりVimを使う人は(良い意味で)ヤバい人ばっかりだなと思いました。
毎年のことですが、運営のみなさま大変お疲れ様でした。本当にありがとうございます。
おかげでとても楽しく過ごすことができました。
発表メモ
Vim Renaissance by Prabir Shrestha
愛用しているvim-lspの作者によるキーノート。
色々なエディタ・IDE・プログラミング言語があった状況からLSPが登場し、そして未来への革命へ、的な。
乱立していると辛いとか、選択肢を広げるようにしているところとか、わかる部分もあれば自分の英語力だとちゃんと理解できているのか不安になるところもあったり...
多くの人が使えるようにするのは大事なことですけど、そこにもトレードオフがあったりするので難しいところですよね。
We can have nice things by Justin M. Keyes
2つ目のセッションはneovim作者によるキーノート。
neovimはあんまり使っていないんですが、その思想とか実装を直接聞くことができてすごく興味深かったです。
UI周りに関する作り込みとかもそうですけど、特に 「Legacy paradox」 の「表面化にある価値のあるモノ」についてはなるほどなーと思いました。
Your Vim is Only for You by mopp
午後の最初のセッション。
自分のVimを改善していくフローは今までそうしてきたことも多々あるし、今後もそうありたいと思っていることなのでとても共感できました。
Grown up from Vim User to Vim plugin developer side by IK
Vimを使い始めてからvim-jpに参加するまでの流れ。
課題を自分が直すしかない!みたいなところは思い当たることがあったり...
最後の「The next OSS contributer is YOU!!!」はとてもやる気が湧き起こる言葉ですね。
Usage and manipulation of the tag stack by daisuzu
こちらを発表してきました。 speakerdeck.com
make test by m-nishi
Vim本体のテストについて。
ターミナルが小さくてfailするのはやったことありますw
自分が以前blogに書いたことを見てくれていたりして、そういったことがこの発表に繋がっているのかなと思うとなんだか嬉しいです。
My Vim life by gorilla0513
1年前にVimを使い初めてやってきたことの軌跡。
ゴリラ.vimとか本とか、近くで関わっていて良く知ってる事もありますけど、ゴリラさんの情熱と行動力は本当にスゴいし尊敬しています!
Using Vim at Work! by Danish Prakash
心理学 + Vimといったとても興味深い発表でした。
UIについては(自分が思うように使えれば良いや、とか思ったりして)あんまり深く考えなかったりもしたけど、
- キーボードよりマウスの方が効率的な事がある
- テキストよりもビジュアル情報の方が6万倍速い
と言われると確かになーとなりました。
Let's Play with Vanilla Vim by Hezby Muhammad
プラグインを使わないVanilla Vimの話。
Vimを使いこなすと基本機能だけでもなんとかなってしまう事も多々あるんですよね。
:find
とか補完とかタグジャンプとか。
1つ前の発表でも少し出てきましたが、まさか他の発表でタグジャンプがあるとは思っていませんでした。
そして :normal
で I
とか A
とか使うやり方はやった事なかったです。
13 Vim plugins I use every day by Tatsuhiro Ujihisa
毎日使っているプラグインの紹介。
ライブコーディングでサーバをパパッと作っていましたが、流れるようなVim捌きが本当に凄かったです!
My dark plugins development history ~ over 10 years ~ by Shougo
shougo wareの開発の歴史。
最近は使わなくなってしまいましたが...第二世代くらいまではすごくお世話になっていました!
今回で最後の登壇になるかもしれないというのがちょっと信じられません。
来年でなくてもまたいつか発表していただければなと思います。
Cloud Datastore用キャッシュパッケージの検討
現在App Engine(Standard Environment) + Go1.11環境*1でgoonというパッケージを利用し、Datastoreのデータをキャッシュしています。
goon
はバックエンドとしてApp Engineのmemcache*2を使用しており、こちらがGo1.12から使えなくなってしまうので他のパッケージに移行しなければなりません。
App Engineのmemcacheに依存しない既存パッケージとしては、
などがありますが、どちらも独自のクライアントを作成するものとなっています。
goon
を使っていて気になったのが、独自クライアント方式はGoogleが提供するパッケージには無い便利な機能があるというメリットがあるものの、本家の機能が全て使えなかったり、そもそもパッケージとしての使い方が異なっているため、Go1.11以降で推奨されているcloud.google.com/go/datastoreへの移行はもちろん、元になっているgoogle.golang.org/appengine/datastoreに戻すことすら手間がかかってしまうという点です。
機能面で困ることなく、ずっと使い続けることができるのであれば特に気にするところではないのかもしれませんが、goon
の場合は更新があまり活発ではなく、移行時に大幅にコードを書き直さないといけないことがわかっているため、次のようなパッケージを検討することにしました。
cloud.google.com/go/datastore
のClientをそのまま使うことができる- 使い方が大きく変わるような付加機能は提供しない
- キャッシュのバックエンドを自由に選択できる
上記を実現するのに使用するのがgRPCのUnaryClientInterceptorというAPIです。
UnaryClientInterceptor
はリモート呼び出しの代わりに呼ばれる関数となっています。
DatastoreはgRPCで各種オペレーションを実行するため、NewClientのopts
にUnaryClientInterceptor
を渡すことでキャッシュ処理を差し込むことが可能です。
使用例:
// optを渡さなければキャッシュを無効にできる opt := option.WithGRPCDialOption( grpc.WithUnaryInterceptor(MemoryInterceptor), // grpc.WithUnaryInterceptor(MemcacheInterceptor), // grpc.WithUnaryInterceptor(RedisInterceptor), )
ただし、UnaryClientInterceptorに渡されるmethod
やreq
、reply
といった引数はDatastoreのClient
の各メソッドと1:1で対応しているわけではないため、どのメソッドがどのgRPC呼び出しになるのかを把握した上でキャッシュ戦略に応じた実装をしていく必要があります。
goon
に合わせたキャッシュ戦略とする場合、
- 非トランザクションでキー指定のデータ取得時にキャッシュの参照や保存を行い、
- Get
- GetMulti
- データ変更時にキャッシュの削除を行う
- Put/Transaction.Put
- PutMulti/Transaction.PutMulti
- Delete/Transaction.Delete
- DeleteMulti/Transaction.DeleteMulti
- Mutation/Transaction.Mutation
ので、Lookup
とCommit
の2つのRPCをインターセプトすることになります。
func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error { switch method { case "/google.datastore.v1.Datastore/Lookup": // キャッシュの参照や保存 return nil case "/google.datastore.v1.Datastore/Commit": // キャッシュの削除 return nil } return invoker(ctx, method, req, reply, cc, opts...) }
/google.datastore.v1.Datastore/Lookup (キャッシュの参照や保存)
Lookup
はreq
としてLookupRequestを受け取り、結果はreply
にLookupResponseを設定します。
実装する処理としては次のような流れです。
1. reqのread_optionsをチェックし、トランザクション内だった場合はinvokerを呼び出してreturnする 2. reqのkeysを使ってキャッシュを参照する * 全てのデータが取得できればreplyのfoundに設定してreturnする 3. reqのkeysを2で取得できなかったキーのみにしてinvokerを呼び出す 4. replyのfoundに2で取得したデータを追加する 5. 3で取得したデータをキャッシュに書き込む
データが見つからなかった場合のハンドリングはClient
のGet
やGetMulti
内で行われるため、UnaryClientInterceptor
の中で特殊な処理をする必要はありません。
/google.datastore.v1.Datastore/Commit (キャッシュの削除)
Commit
はreq
としてCommitRequestを受け取り、結果はreply
にCommitResponseを設定します。
実装する処理としては次のような流れです。
1. invokerを呼び出す * エラーだった場合はreturnする 2. reqのmutationsにupdate、upsert、deleteがあれば対応するキーのキャッシュデータを削除する * 削除に失敗した場合はエラーを返す
キャッシュの削除に失敗すると古いデータが残り続けてしまうため、
のいずれかを行わないとデータに不整合が生じてしまいます。
これについて、特に困っているのがRunInTransaction
の場合、キャッシュ削除の失敗はロールバックの対象にならず、個別にロールバックを呼び出すこともできないというところです。
そのため、
- キャッシュのバックエンドにリトライ処理を入れる
- 無限にリトライしてしまう可能性もある...
- キャッシュに有効期限を持たせる
- 一定時間は不整合を許容できるのであれば...
- キャッシュを削除してからDatastoreを更新する
- キャッシュ削除〜Datastore更新の間に別の処理が入ると結局不整合になる
- それならキャッシュ戦略を変更した方が...
- いっそのこと
RunInTransaction
を使わないようにする
などなど、このあたりはもう少し検討をしなければいけない状況です。
ただ実際に動かしてみないとわからないところもあると思うので、近いうちに実装して色々と試してみようと思っています。
Stackdriverの設定をterraformで管理する
今までアラートポリシーやカスタム指標などのStackdriverの設定をyamlやテキストで保存し、自作ツールで適用するようにしていましたが、自作ツールのメンテナンスが面倒になってきたのでterraformに移行することにしました。
移行にあたって、既存の設定ファイルは破棄し、terraformerを使って以下のように再作成しました。
1. 既存設定のインポート
PROJECT_ID="プロジェクトID" BUCKET="gs://バケット名" OUT="出力先ディレクトリ" terraformer import google --resources=monitoring -o $OUT -p '{output}' --state=bucket --bucket=$BUCKET --projects=$PROJECT_ID
※terraformer v0.7.8の場合、terraform v0.12への対応ができていない部分があるので以下を行う必要があります
- bucket.tfを修正
terraform 0.12upgrade
を実行
※slack通知はauth_token
がマスクされた状態だったので設定を削除し、terraform state rm
で管理から外しました
2. カスタム指標の作成
terraformer v0.7.8はカスタム指標に対応していないため、gcloudコマンドで取得した内容を .tf
に変換しました。
gcloud --project=$PROJECT_ID logging metrics list --format=json
このままだとterraform apply
時に新規作成しようとしてエラーになってしまうため、terraform import
でterraformの管理下に追加します。
3. 設定の適用
手元に一通りの設定が揃ったら適用します。
terraform fmt terraform validate terraform plan terraform apply
※必須フィールドが無い場合はterraform apply
時にエラーになるので個別に対応します
Datastore Emulatorのエンドポイント
Cloud Datastore エミュレータは起動した後、gcloud
コマンドではなくREST APIを使って操作するらしい。
が、そのあたりの情報が全然まとまっていなかったので本体の中をチラ見して必要そうなところを調べてみた。
cloud-datastore-emulator 2.1.0
Method | Path | Description |
---|---|---|
GET | / | OKを返すだけ |
POST | /reset | データをリセットする*1 |
POST | /persist | データを永続化する*2 |
POST | /shutdown | datastore emulatorを落とす |
POST | /_ah/admin/datastore | /reset と同じ |
POST | /_ah/admin/quit | /shutdownと同じ |
ユニットテストで使う際は--no-store-on-disk
をつけて起動し、
- テストケースごとに
POST /reset
- テストが終わったら
POST /shutdown
すると良さそう。
素のVimでGoを書く時のテクニック
にて、
外部ネットワークに繋がらない環境で
vim
とgo
しか使えない
という縛りでライブコーディングをしてきました。
実際にそういう環境があるのか?という話はさておき、そのような状況でも以下を駆使してそこそこコードが書けます。
:%!gofmt
でコードを整形できる:r! go doc
の結果を適当なバッファに出力する- Vim内でドキュメントが読める
- ドキュメントの内容を
CTRL-N
やCTRL-P
で補完できる
:set path+=$GOROOT/src
で標準パッケージを検索できるようにする
作ったものはGET /fizzbuzz/:number
にアクセスするとFizzBuzzを返すというHTTPサーバです。
package main import ( "net/http" "strconv" "strings" ) func fizzbuzz(w http.ResponseWriter, r *http.Request) { p := strings.TrimPrefix(r.URL.Path, "/fizzbuzz/") if p == "" { http.NotFound(w, r) return } n, _ := strconv.Atoi(p) if n < 1 { http.NotFound(w, r) return } switch { case n%5 == 0 && n%3 == 0: w.Write([]byte("FizzBuzz")) case n%5 == 0: w.Write([]byte("Buzz")) case n%3 == 0: w.Write([]byte("Fizz")) default: w.Write([]byte(p)) } w.Write([]byte("\n")) } func main() { http.HandleFunc("/fizzbuzz/", fizzbuzz) http.ListenAndServe(":8080", nil) }
ただ当日は1.のgofmt
しか使いませんでした。
というのもスパルタンなVimmerに憧れており、元から自動補完は使っておらず、オムニ補完にもそこまで依存しないようにしていたので、このくらいのものであれば無くても特に困らなかったからです。
まあ常に役立つものではありませんが、覚えておくと何かの時に便利かもしれません。