編集を加速するVimのquickfix機能
この記事はVim Advent Calendar 2020の3日目の記事です。
昨日は@mira010さんのvim pluginsをインストールしてみましょうでした。
みなさんquickfixを使っていますか?
Vimのquickfix機能はgrepやmakeなどの結果を保持する専用のバッファと、それを扱うための各種コマンドからなります。
IDEには当たり前のようにあるような機能ですが、Vimの場合は他の機能と組み合わせることで編集操作を格段に効率化できます。
- 外部コマンドを指定した引数で実行し、
- ファイル名や行番号、メッセージなどの出力を解析し、
- ジャンプのために使えるリストを作ってくれる
のは共通です。
このリストはquickfixリストと呼ばれるもので、:copenで専用のウィンドウが開きます。
そして<Enter>
キーやダブルクリックで該当行にジャンプします。
ただデフォルトのgrepコマンドはまだしも、makeコマンドは滅多に使わないという人もいるかもしれません。
その際はgrepprg
やmakeprg
オプションで実行する外部コマンドを任意のコマンドに変更できます。
(出力される結果が解析できない形式の場合はerrorformat
なども変更する必要があります。)
grepコマンドはgit grep
、
" スペースはエスケープが必要 set grepprg=git\ grep\ -n\ --no-color
makeコマンドはlinterやタスクランナーなどにするとグッと使いやすくなります。
" 使用例 " :make ./... " :make --disable-all -E staticcheck set makeprg=golangci-lint\ run
これらの設定はvimrcに書いておいたり、簡単に切り替えられるコマンドやマッピングを用意しておいても良いでしょう。
しかし、quickfixリストは同時に複数の結果を表示することができません。
そのため、複数のquickfixリストを扱うには:colderや:cnewerや:chistoryを使って履歴を行き来する必要があります。
もしくはlocationリストを使います。
locationリストとはウィンドウローカルなquickfixリストのことで、コマンドのプレフィックスが
だけで、quickfixリストと同じように使えます。
そのため、別のウィンドウやタブページで個別にlocationリストを開くことで複数の結果を表示できます。
さて、ここまで紹介した機能だと便利なジャンプリストでしかありません。
quickfixリストを使ってさらに効率的な編集をするには、:cnextとマクロを組み合わせて使います。
例えばgrepで絞り込んだ行の特定の文字列を置換したければ、以下の操作を(qq
などで)マクロに記録し、
:s/Before/After/ :w :cnext
大きな数を指定して(100@q
や1000@q
で)実行すると、quickfixリストの最後まで自動的に繰り返してくれます。
リストが指定した数より少なければそこでマクロが止まってくれるので重複実行は気にしなくて大丈夫です。
この時、さらにquickfixリストを絞り込みたくなることがあるかもしれません。
:grep
の場合は正規表現を工夫しても良いですが、:packadd cfilter
で使えるようになる:Cfilterでquickfixリストを絞り込んでしまうのがとても楽です。
それでも絞り込むのが難しい場合、いったん別のバッファにコピーして編集し、:cbufferや:cgetbufferでquickfixリストを読み込み直しても構いません。
(quickfixリストを:setlocal modifiable
して書き換えるのはちょっと乱暴なので非推奨です。)
また、quickfixリスト自体をファイルとして保存しておき、:cfileや:cgetfileで読み込み直すことも可能です。
そうするとジャンプ可能なTODOリストとしても使えるので、リストが巨大だったら少しずつ進めたり、他のVimmerと作業を分担する、なんてこともできるかもしれません。
ということで、ぜひquickfix機能を活用してみてください。
本当はより実践的な例として、最近やったことを具体的なコマンド付きで紹介できれば良かったんですが、ある事情でそのヒストリーをロストして再現環境もなく...
覚えている範囲でフワッと紹介して終わります。
目的はterraformerで生成したmonitoring_alert_policy.tf
ファイルのdocumentation
に、フィルター付きでCloud LoggingのWeb画面に飛べるリンクを追加することでした。
そのフィルターの内容はlogging_metric.tf
ファイルに定義されているものを使います。
- monitoring_alert_policy.tf(のサンプル)
resource "google_monitoring_alert_policy" "alert_policy_error" { display_name = "My Alert Policy(Error)" combiner = "OR" conditions { display_name = "test condition" condition_threshold { filter = "resource.type=gae_app AND metric.type=logging.googleapis.com/user/my-error-metric" duration = "60s" comparison = "COMPARISON_GT" aggregations { alignment_period = "60s" per_series_aligner = "ALIGN_RATE" } threshold_value = 0.1 trigger { count = 1 } } } documentation = { mime_type = "text/markdown" # TODO: ここに↓の形式でリンクを入れたい # - [NAME](https://console.cloud.google.com/logs/query?project=PROJECT_ID&query=FILTER) content = "" } }
- logging_metric.tf(のサンプル)
resource "google_logging_metric" "logging_metric_error" { name = "my-error-metric" filter = "resource.type=gae_app AND severity>=ERROR" metric_descriptor { metric_kind = "DELTA" value_type = "INT64" } }
手順:
:grep
する:copen
して、不要な行があれば除外する- マクロを記録開始して、
getline()
とsubstitute()
で検索するmetricの名前を抽出するlogging_metric.tf
のバッファに移動するsearch()
とgetline()
とsubstitute()
を使ってfilterを取得する- 元バッファに戻る
- documentationのcontentがある行に移動する
- リンクを生成して追加する
- conditionsが複数ある場合はそれぞれのリンクを追加する
- フィルターはbase64化してクエリパラメータにする必要がある
- webapi-vimを使ったはず
:w
で保存する:cnext
する
- マクロの記録を終了する
- 1000回ほど繰り返す
実は初めの数回は関数などを使用せず、ノーマルモードコマンドで普通に編集をしていました。
ただ同じような操作を繰り返していることは薄々感じていたのと、残りの件数を見て即マクロに切り替えたという経緯があります。
そのまま続けていたら数時間はかかっていたと思いますが、ほぼ一瞬で終わらせることができました。
明日は@kaneshinさんです。
GoでDBのテストにgithub.com/cockroachdb/copyistを使う
久しぶりにMySQLを使ったシステムを触ることになったものの、現状のテストが遅すぎたので高速化に取り組むことにした。
遅い原因としては、
- テストでDBを使うパッケージが多いので接続時間がそれなりになってしまう
- テストケースごとにTRUNCATE→INSERTでデータを入れ直している
といったところ。
なので今回はDBにアクセスしないでテストができるようになるcopyistを試してみようと思う。
仕組みとしては実行されたSQLを記録しておき、以降はその時のデータを使うという
などと似たようなものとなる。
README.mdにはMySQLは非対応と書いてあったが、普通に使うことができた。
(もしかしたら一部正しく記録/再生できないクエリがあるのかもしれない)
MySQL版のサンプルコードはこちら。
mysql_test.go
package example import ( "database/sql" "log" "testing" "github.com/cockroachdb/copyist" _ "github.com/go-sql-driver/mysql" ) // GetName をテストする。 func GetName(db *sql.DB, id int64) (string, error) { var name string err := db.QueryRow("SELECT name FROM `user` WHERE id=?", id).Scan(&name) return name, err } // resetDBで使うためにグローバル変数にしておく。 var db *sql.DB // 記録時にcopyist.Open()の中で実行される。 func resetDB() { db.Exec("DROP TABLE `user`") db.Exec("CREATE TABLE `user` (`id` INT(11) unsigned NOT NULL AUTO_INCREMENT, `name` VARCHAR(20) NOT NULL, PRIMARY KEY (`id`))") db.Exec("INSERT INTO `user` (id, name) VALUES (?, ?)", 1, "Andy") } func TestMain(m *testing.M) { // ドライバ名は"copyist_mysql"になる。 copyist.Register("mysql", resetDB) // resetDBとテストで使うdbを作る。 var err error db, err = sql.Open("copyist_mysql", "admin:pass@/copyist") if err != nil { log.Fatal(err) } defer db.Close() m.Run() } func TestGetName(t *testing.T) { defer copyist.Open(t).Close() // found name, err := GetName(db, 1) if err != nil { t.Fatal(err) } if name != "Andy" { t.Error("failed test") } // not found if _, err := GetName(db, 100); err != sql.ErrNoRows { t.Error(err) } } // subtest版 func TestGetName_subtest(t *testing.T) { t.Run("found", func(t *testing.T) { defer copyist.Open(t).Close() name, err := GetName(db, 1) if err != nil { t.Fatal(err) } if name != "Andy" { t.Error("failed test") } }) t.Run("not found", func(t *testing.T) { defer copyist.Open(t).Close() if _, err := GetName(db, 100); err != sql.ErrNoRows { t.Error(err) } }) }
1回目は以下のように -record
をつけてテストを実行する。
$ go test -v -record === RUN TestGetName --- PASS: TestGetName (0.09s) === RUN TestGetName_subtest === RUN TestGetName_subtest/found === RUN TestGetName_subtest/not_found --- PASS: TestGetName_subtest (0.12s) --- PASS: TestGetName_subtest/found (0.06s) --- PASS: TestGetName_subtest/not_found (0.06s) PASS
そうするとtestdataディレクトに以下のようなmysql_test.copyistが生成される。
1=DriverOpen 1:nil 2=ConnPrepare 2:"SELECT name FROM `user` WHERE id=?" 1:nil 3=StmtNumInput 3:1 4=StmtQuery 1:nil 5=RowsColumns 9:["name"] 6=RowsNext 11:[10:QW5keQ] 1:nil 7=RowsNext 11:[] 7:EOF "TestGetName"=1,2,3,4,5,6,2,3,4,5,7 "TestGetName_subtest/found"=1,2,3,4,5,6 "TestGetName_subtest/not_found"=1,2,3,4,5,7
この状態で -record
をつけずに実行すると↑のデータが使われる。
$ go test -v === RUN TestGetName --- PASS: TestGetName (0.00s) === RUN TestGetName_subtest === RUN TestGetName_subtest/found === RUN TestGetName_subtest/not_found --- PASS: TestGetName_subtest (0.00s) --- PASS: TestGetName_subtest/found (0.00s) --- PASS: TestGetName_subtest/not_found (0.00s) PASS
なお、記録する前に -record
無しで実行するとpanicになる。
$ go test -v === RUN TestGetName --- FAIL: TestGetName (0.00s) panic: no recording exists with this name: TestGetName [recovered] panic: no recording exists with this name: TestGetName
また、以下のようにクエリを変えて、
--- a/mysql_test.go +++ b/mysql_test.go @@ -12,7 +12,7 @@ import ( // GetName をテストする。 func GetName(db *sql.DB, id int64) (string, error) { var name string - err := db.QueryRow("SELECT name FROM `user` WHERE id=?", id).Scan(&name) + err := db.QueryRow("SELECT name FROM `user` WHERE id=? AND TRUE", id).Scan(&name) return name, err }
記録し直さずにそのまま実行してもpanicになる。
$ go test -v === RUN TestGetName --- FAIL: TestGetName (0.00s) panic: mismatched argument to ConnPrepare, expected SELECT name FROM `user` WHERE id=? AND TRUE, got SELECT name FROM `user` WHERE id=? - regenerate recording [recovered] panic: mismatched argument to ConnPrepare, expected SELECT name FROM `user` WHERE id=? AND TRUE, got SELECT name FROM `user` WHERE id=? - regenerate recording
ただし、プレースホルダーの値が変わっても返ってくるレコードは変わらないのでその点は注意が必要。
例えば以下のようにidを逆にしてもテストはPassしてしまう。
--- a/mysql_test.go +++ b/mysql_test.go @@ -45,7 +45,7 @@ func TestGetName(t *testing.T) { defer copyist.Open(t).Close() // found - name, err := GetName(db, 1) + name, err := GetName(db, 100) if err != nil { t.Fatal(err) } @@ -54,7 +54,7 @@ func TestGetName(t *testing.T) { } // not found - if _, err := GetName(db, 100); err != sql.ErrNoRows { + if _, err := GetName(db, 1); err != sql.ErrNoRows { t.Error(err) } } @@ -63,7 +63,7 @@ func TestGetName_subtest(t *testing.T) { t.Run("found", func(t *testing.T) { defer copyist.Open(t).Close() - name, err := GetName(db, 1) + name, err := GetName(db, 100) if err != nil { t.Fatal(err) } @@ -75,7 +75,7 @@ func TestGetName_subtest(t *testing.T) { t.Run("not found", func(t *testing.T) { defer copyist.Open(t).Close() - if _, err := GetName(db, 100); err != sql.ErrNoRows { + if _, err := GetName(db, 1); err != sql.ErrNoRows { t.Error(err) } })
細かい部分はまだ何とも言えないが、とりあえず使えそうだし高速化にも期待ができそう。
さて、あとはこれをどうやって既存のテストに組み込んでいくか。
既存のテストではxormが使われているが、copyistに対応していないので少し特殊な初期化をしないといけないようだ。
engine, err := xorm.NewEngine("mysql", "") // エラーハンドリングやengineの設定など // ... // DBをsql.Open("copyist_mysql", ...)したものと差し替える engine.DB().DB = db
これを何とかするのが一番大変なのかもしれない...
ISUCON10に初参戦してみた
ゴリラさんと出てみようと話していたものの、申し込みのタイミングを逃してオワタと思っていたら@inductorさんに拾ってもらい、カンガルーと犬とゴリラ
としてなんとかISUCON10に参加することができました。
当日は開始ちょっと前*1にDiscordとGoogleドキュメントでコミュニケーションすることを決め、 開始直後は
- 環境を整備する
- アプリを見る
- ドキュメントを見る
といった感じで役割分担。
30分くらい経って、これからはベンチマークを走らせながらどこから手を付けるか考えようと思っていましたがトラブルのためなかなか実行できず、New Relicの設定をしたあとはしばらくWebアプリをポチポチしたりホストの中をウロウロしたりしていました。
ベンチマークが走ってからは、
- 検索系
- インデックス
- DB分割
- 最終的にはApp1台 + DB2台に
- nazotte
- LIMITでループを抜ける
- それ以上のチューニングは結局できず...
- low_priced
- 静的に返したり、キャッシュしたり
- botからのアクセス
- nginxで弾く
をなんとかすることに。
これでスコアが480→805になったのがだいたい19:00で、そこから都合により20:20くらいまで離席...
戻ってきたタイミングで検索系がさらに最適化されてスコアは1280まで伸びましたが、最終的には1244で競技終了の21:00になりました。
以前に参加した社内ISUCONでは個人でもチームでもスコアアップに繋がるようなことがほとんどできなかったので*2、それに比べれば多少はできるようになったかもしれませんが、まだまだ力不足*3なことを痛感しました。。。
goplsのSymbolMatcherとSymbolStyleオプション
以下の記事で、workspace/symbol
のオプションは補完と同じmatcher
と紹介したが、現在のv0.4.4ではsymbolMatcher
とsymbolStyle
になっている。
daisuzu.hatenablog.com
まずはv0.4.1でmatcher
がsymbolMatcher
に変わり、それまでと同様、
- fuzzy
- caseSensitive
- caseInsensitive(デフォルト)
の3つが補完と独立して設定できるようになった。
その後、v0.4.4でsymbolStyle
が追加され、
- package(デフォルト)
- full
- dynamic
を指定できるようになった。
packageの場合は今までと同様、パッケージ名をクエリに含める際にはコードで使う時のようにcontext.Context
といった形式で検索する。
新たに追加されたfullではgolang.org/x/net/context.Context
のようにすることで同じパッケージ名の別モジュールを区別することが可能になった。
dynamicはpackage→fullの順番でマッチさせるため、パッケージ名を含んだクエリはpackageと同じ結果になり、インポートパスから指定したクエリはfullと同じ結果になる。
Goでprotobufの定義をimportせずにフィールドの値を使う
以下のようなコードでreq
にあるフィールドを使いたい場合、
opt := grpc.WithUnaryInterceptor(func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error { // ここでreqから特定のフィールド値を取得したい return nil })
型が定義されているパッケージをインポートして、
if v, ok := req.(*pb.Req); ok { // v.Bodyでフィールドにアクセスできる }
のように変換することになる。
この時、インポートしたくない or インポートできないのであれば、protoc
が生成する定義にはGetterも同時に生成されるため、対象フィールドのGetterがあるinterfaceで型アサーションすると手軽にできる。
if v, ok := req.(interface{ GetBody() []byte }); ok { // v.GetBody()でフィールドの値を取得できる }
あまり使い所はないかもしれないが、google.golang.org/appengineは定義がinternal配下にあるため、例えばテストでWithAPICallFuncを使う際など、以下のようにしてtaskqueue.Addのリクエスト(TaskQueueAddRequest)からフィールドの値を取得できる。
var got []byte ctx = appengine.WithAPICallFunc(ctx, func(ctx context.Context, service, method string, in, out proto.Message) error { if service == "taskqueue" && method == "Add" { if v, ok := in.(interface{ GetBody() []byte }); ok { got = v.GetBody() } } return nil }) DoSomething(ctx) // 関数内でtaskqueue.Add()が呼ばれる if !reflect.DeepEqual(got, want) { t.Errorf("body = %q, want %q", got, want) }
なお、フィールドがインポートできない型になっているとinterfaceも作れないのでこの方法は使えない。
それでも必要な場合はencoding/json
パッケージでMarshal
してからmap[string]interface{}
や独自型にUnmarshal
すれば良い。
gopls(v0.4.0)の機能
goplsは更新頻度が高く、何ができるのかをちゃんと把握できていなかったので、LSPのメソッドベースで今の時点(v0.4.0)の機能をざっと調べてみることにしました。
補完(textDocument/completion)
入力中の変数や定数、関数といった各種定義などをbuiltinも含めて補完してくれます。
この機能のためにgopls
を使っているという人もけっこう多いはず。
基本的に文脈に合わせた候補を返したり、優先度が高くなるようになっていますが、
- おかしな候補が返ってくる
- 期待する候補が返ってこない
という場合はコントリビュート(issue報告や修正する)チャンスかもしれません。
例えば、
- スコープに存在しない定義は候補にならない
- スコープ外の変数など
- カーソル位置で使えないキーワードは候補にならない
iota
: const入力中のみrange
: for入力中のみbreak
: for、switch、select内continue
: for内- など
- 型が合わないものは候補から外されることがある
- 代入時
のような制御があります。
また、以下の場合は補完で入力される内容自体が変化します。
- 参照が要求される場合、値には
&
がつく
// vへの代入時、 var v *int i := 1 v = // iは&iになる // func f(i *int)を呼び出す際、 f( // iは&iに変換される
- 値が要求される場合、参照には
*
がつく
// iへの代入時、 var v *int var i int = // vは*vになる // func f(i int)を呼び出す際、 f( // vは*vに変換される
- 可変長引数の場合に
...
が展開される
func fn(v ...int) {} func do(v []int) { fn( // vはv...になる }
- 引数の型がわかる場合はその型が補完される
func(...[]int)
を呼び出す際は[]int{}
が候補になるvar _ []int = make()
の第1引数は[]int
が候補になる
ただし、プレースホルダー*1やスニペット*2が有効になっていないと変換されなかったり、候補として表示されません。
VS Codeはデフォルトで有効になっているはずですが、Vimの場合はプラグインによっては有効になっていないかもしれません。
そしてシグネチャを補完・展開する際にはこれらが必須となっているため、もしうまく動かない時は設定を確認してみましょう。
他にも、次の設定で補完の動作を変えることができます。
initializationOptions.matcher
- デフォルトは
"fuzzy"
なので多少違っている候補も返ってきますが、 "caseSensitive"
や"caseInsensitive"
に変更できます
- デフォルトは
initializationOptions.completeUnimported
- デフォルトは
true
なので未importのパッケージからも候補が返ってきますが、 false
で無効化できます
- デフォルトは
initializationOptions.deepCompletion
- デフォルトは
true
なので構造体のフィールドも候補として返ってきますが、 false
で無効化できます
- デフォルトは
// deepCompletion=trueだと type param struct{ i int } func fn(i int) {} func do(p param) { fn( // p.iが候補として表示される }
それ以外の特殊な動作としては、
if err != nil
のスニペット展開error
を返す関数内のみ
- 公開される
var
のgodoc補完- 次のバージョンでは
const
やfunc
、type
も対象になる
- 次のバージョンでは
があります。
ジャンプ系
自分の場合は補完よりこちらを多用しています。
ただ、definition
とtypeDefinition
は違いがわからないまま使っていました。
textDocument/definition
カーソル位置にあるシンボルの定義元にジャンプする時に使用します。
関数内の変数はその関数内で定義(宣言)された場所にジャンプします。
type myType struct{} func fn() { t := myType{} // ← t.method() // tが宣言されたのは1行上 }
コマンドラインからはgopls query definitions
で使えます。
※次のバージョンからquery
が不要になる
gopls query definition internal/lsp/definition.go:14:67
textDocument/typeDefinition
カーソル位置にあるシンボルの型定義にジャンプする時に使用します。
関数内の変数は実際の型のある場所にジャンプします。
type myType struct{} // ← func fn() { t := myType{} t.method() // tの型定義はfnの上にあるmyType }
textDocument/implementation
カーソル位置にあるシンボルの、
- インタフェースから型
- 型からインタフェース
にジャンプする時に使用します。
コマンドラインからはgopls implementation
で使えます。
gopls implementation internal/lsp/implementation.go:14:10
textDocument/references
カーソル位置にあるシンボルが参照されている(使われている)場所にジャンプする時に使用します。
コマンドラインからはgopls references
で使えます。
gopls references internal/lsp/references.go:14:18
textDocument/documentSymbol
現在開いているファイルのシンボル一覧を取得します。
結果はジャンプするためや、ファイルのアウトライン表示のためにも使われます。
コマンドラインからはgopls symbols
で使えます。
※CLIでうまく動かない? → https://go-review.googlesource.com/c/tools/+/232557
workspace/symbol
プロジェクト(リポジトリ本体と依存パッケージ)からシンボル一覧を検索します。
※LSPの仕様では依存パッケージは含まれないはずですが、その方が便利なのでそういう実装になっています
今のところ結果は100件までに制限されています。
※一度に全部返してしまうとクライアントが高負荷で固まってしまうことがあるため
また、検索方法は補完と同じでinitializationOptions.matcher
に従います。
コマンドラインからはgopls workspace_symbol
で使えます。
gopls workspace_symbol WorkspaceSymbols
Vimからはtagfunc
経由でctags
のように使うこともできます。
設定例は下記参照。
daisuzu.hatenablog.com
その他
Diagnostic
以外はほとんど使っていませんでした。。。
しかし、今回調べたことでrename
がgorenameの代わりになるくらい完成度が高くなっていたり、SuggestedFix
の種類がだんだん増えていることがわかったのは大きな収穫でした。
Diagnostic
go vet
(golang.org/x/tools/go/analysis/passes/...
)やgofmt -s
、staticcheckなどのチェックを行います。
実行するチェッカーは以下のオプションでカスタマイズできます。
initializationOptions.analyses
initializationOptions.staticcheck
結果にSuggestedFixが含まれている場合、codeAction
経由で修正可能です。
SuggestedFix
の例:
return
の返り値の数があっていない時に追加したり、削除したり- 2回目以降の
:=
を=
に変更したり - 未定義変数の
<変数名> :=
を挿入したり
コマンドラインからはgopls check
で使えます。
gopls check internal/lsp/testdata/lsp/primarymod/analyzer/bad_test.go
textDocument/codeAction
SuggestedFix
の実行やimportの追加・削除(go.mod
への追加も含む)を行います。
コマンドラインからはgopls fix
やgopls imports
で使えます。
textDocument/codeLens
調べてもイマイチよくわかっていない機能ですが、現状は//go:generate
コメントのある行でgo generate
が実行できるよ、という情報を返してくれるようです。
また、go.modを開いている場合は依存パッケージのアップデートができるかどうかを教えてくれるようです。
textDocument/hover
カーソル位置のシグネチャや型などの情報を返してくれます。
textDocument/signatureHelp
カーソル位置のシグネチャやヘルプを返してくれます。
コマンドラインからはgopls signature
で使えます。
※<position>
の指定方法がわからず...は関数呼び出しの()
内の位置を指定する必要がある
gopls signature internal/lsp/signature_help.go:21:53
textDocument/documentHighlight
カーソル位置のシンボルが使われている場所をハイライトします。
コマンドラインからはgopls highlight
で使えます。
gopls highlight internal/lsp/highlight.go:17:2
textDocument/documentLink
現在開いているファイルがimportしているパッケージの https://pkg.go.dev へのリンクや、
<org>/<repo>#<number>
形式のコメントからGitHub Issuesへのリンクを返してくれます。
コマンドラインからはgopls links
で使えます。
gopls links internal/lsp/link.go
textDocument/formatting
現在開いているファイルを整形します。
コマンドラインからはgopls format
で使えます。
# -dでdiffを表示する gopls format -d internal/lsp/testdata/lsp/primarymod/format/bad_format.go.in
textDocument/rename
カーソル位置のシンボルをリネームします。
コマンドラインからはgopls rename
で使えます。
# -dでdiffを表示する gopls rename -d internal/lsp/rename.go:14:18 doRename
リネーム可能かどうかを調べるためにはtextDocument/prepareRenameで確認できます。
コマンドラインからはgopls prepare_rename
で使えます。
# rangeが表示されればリネーム可能
gopls prepare_rename internal/lsp/rename.go:14:18
textDocument/foldingRange
コードを折り畳む範囲を返してくれます。
コマンドラインからはgopls folding_ranges
で使えます。
gopls folding_ranges internal/lsp/folding_range.go
workspace/executeCommand
codeLens
の結果からgo generate
やgo get
の実行をしてくれるようです。
また、diagnostic
の結果からgo mod tidy
の実行をしてくれるようです。
未実装
以下のメソッドはまだ実装されていません。
GoでREST APIを呼ぶテストにhttp.FileServerを使う
例えばGET /users/:id
のようなAPIの場合、
testdata └── users └── 1
のように、testdata配下のusersディレクトリにidをファイル名としたJSONファイルを配置しておきます。
そうすると、
http.FileServer(http.Dir("testdata"))
で、testdata配下のパスとリクエストのパスが一致するファイルが
- 存在すれば
200 OK
でそのファイルの中身 - 存在しなければ
404 Not Found
を返すサーバが作れます。
http.Get(os.Getenv("API_URL") + "/users/" + strconv.FormatInt(id, 10))
のような呼び出し方をしていればhttptest
を使って次のようにテストが書けます。
package api import ( "net/http" "net/http/httptest" "os" "reflect" "testing" ) func Test_getUser(t *testing.T) { ts := httptest.NewServer(http.FileServer(http.Dir("testdata"))) defer ts.Close() if err := os.Setenv("API_URL", ts.URL); err != nil { t.Fatal(err) } defer os.Unsetenv("API_URL") type args struct { id int64 } tests := []struct { name string args args want *user wantErr bool }{ { name: "OK", args: args{id: 1}, want: &user{ID: 1, Name: "Alice"}, wantErr: false, }, { name: "NotFound", args: args{id: 2}, want: nil, wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := getUser(tt.args.id) if (err != nil) != tt.wantErr { t.Errorf("getUser() error = %v, wantErr %v", err, tt.wantErr) return } if !reflect.DeepEqual(got, tt.want) { t.Errorf("getUser() = %v, want %v", got, tt.want) } }) } }
POSTメソッドの場合、
- エンドポイントが
POST /users
のような形式になるのと、 201 Created
や500 Internal Server Error
が返せないため、
http.FileServerよりはhttp.HandlerFuncを使った方が良いでしょう。
また、リクエストパスとファイルが1対1にならない時も同様です。