VSCode の Vim 拡張 (VSCodeVim) をより Vim っぽくする PR を送った
随時追加中。送った PR はこちら。
Pull Requests · VSCodeVim/Vim · GitHub
あんまり一度に送りすぎてもアレなので温存してるブランチもまだあります…
送った PR の内容
注:まだマージされてない PR もあります。
全てマージされました。次のリリースが楽しみですね。
- Implement gn command (#2589)
- これが一番個人的に嬉しい。
gn
コマンドがなかったので実装した gn
コマンドの挙動は「実践Vim 思考のスピードで編集しよう!」参照か、この記事参照- ちなみにまだ翻訳されてませんが、原著の Practical Vim は第2版が出てます
- これが一番個人的に嬉しい。
- [Fix] * command highlights extra content (#2611)
- これも気になってた。なぜか
*
で検索した時に次の候補が4文字分右に多くハイライトされてしまうってやつ。該当 issue のスクリーンショット見ると分かるけどかなりうっとうしい。
- これも気になってた。なぜか
- Add missing window keys (<C-w><C-[hjklovq]>) (#2600)
- いくつかのウィンドウ関連のキーがなかったので追加。
- 実装はあったけど Ctrl 無しのキーだけでしか提供されていなかった。例えば
<C-w>l
はあったけど<C-w><C-l>
はなかった、みたいな。 - キーバインド追加して既存のコマンド呼んだだけ
- みんなよく Ctrl 押したり離したりできるなーと思った (こなみ)
- [Fix] aW doesn't work at the end of lines (#2591)
daW
を行末で実行した場合の挙動が Vim と異なっていたため修正
- [Fix] p in visual-mode should save last selection (#2588)
p
をビジュアルモードで実行した後、gv
で選択される範囲が挿入されたテキストではなく最後にビジュアルモードで選択した範囲になっていたため修正:help gv
にAfter using "p" or "P" in Visual mode the text that was put will be selected.
と書いてある- つまりビジュアルモードの
p
は最後の選択範囲を更新しなければならない
- [Fix] Transition between v,V,<C-v> is different with original Vim behavior (#2581)
v
,V
,<C-v>
をそれぞれビジュアルモードで実行した場合、同じビジュアルモードの種類だったらノーマルモード (例:vv
)、そうでなかったら押したビジュアルモードの種類に変更 (例:vV
→ linewise visual-mode)
- [Fix] Don't add beginning newline of linewise put in visual-mode (#2579)
- linewise なレジスタの内容を linewise visual-mode で
p
した場合に不必要な改行が挿入されるのを修正 (例:yy
してVp
した場合の挙動)
- linewise なレジスタの内容を linewise visual-mode で
- [Fix] p won't work in linewise visual-mode at the end of document (#2601)
p
がファイルの最後で使われた時の挙動が間違ってたので修正。上の PR (#2579) で考慮が漏れていた。
- [Fix] p in visual block appends unnecessary newline (#2609)
- linewise visual mode の時、
p
コマンドで余計な改行が入ってしまうのを修正 - 色々送りすぎて最初 (#2601) に戻ってる、と見せかけてちょっとだけ違うケースの別のバグ (ファイル先頭と末尾のみで起きる)
- ここらへんリファクタリングしたい…つらい…
- linewise visual mode の時、
- [Fix] <C-h> should work as same as
in search mode (#2593) /
コマンドで検索中に<BS>
は効くのに<C-h>
が効かなかったので修正
- [Fix] p in visual-mode should update register content (#2602)
- ビジュアルモードで選択中に
p
を押した時、テキストが置換された後にレジスタの内容が置換前のテキストに変わらなかったので修正
- ビジュアルモードで選択中に
- Add o command in visual block mode (#2604)
o
コマンドが visual block mode で動かなかったので追加
Go で struct をそのまま公開するのはあんまり良くないんじゃないか
と思った。理由は以下の通り。
ずっと地味に困ってた事があって、ある struct をコンストラクタ的な関数 (例えば Foo
って struct だったら NewFoo
って名前の関数) 経由で生成したい、と思っても、
Go の言語仕様上普通にこんな感じで struct を作れてしまう。
package main import "fmt" type Editor struct { name string } // ほんとはこっちを使ってほしい func NewEditor(name string) *editor { return &editor{name: name} } func (e *Editor) Hello() string { return "こんにちは、" + e.name + "です" } func main() { // NewEditor() 使わずに普通に作れてしまう editor := &Editor{name: "ビム"} fmt.Println(editor.Hello()) // => "こんにちは、ビムです" // こんなのも作れてしまう editor = &Editor{} fmt.Println(editor.Hello()) // => "こんにちは、です" }
見ての通り、自分的には必須のつもりが指定してないフィールドがあっても勝手に zero value になってしまってコンパイルエラーとはならない点。 運が悪いと例えば struct に新しいフィールドを追加してもコンパイルエラーにはならずに気付かずに runtime error とかで落ちる事になったりする。 この場合 string の zero value は "" なので runtime error にはならないものの、期待した結果と異なる可能性がある。
これは自分だけのプロジェクトでやってる分には問題ないんだけど、ライブラリを提供する場合、
何か機能を付け足して struct にフィールドを足したくなった時、
すべてのフィールドに対して zero value で指定される可能性を考慮する必要がある。
つまりカジュアルに &Editor{}
とかで生成させたくない、できれば struct は unexported にしてコンストラクタ的な関数で生成したいという場合がままある。このように。
package main import "fmt" type editor struct { name string } func NewEditor(name string) *editor { return &editor{name: name} } func (e *editor) Hello() string { return "こんにちは、" + e.name + "です" } func main() { // 違うパッケージの想定なので直接 editor 構造体を触ることはできない // editor := editor{name: "ビム"} editor := NewEditor("ビム") fmt.Println(editor.Hello()) // => "こんにちは、ビムです" // これは引数の数が違うためコンパイルエラーになる (YATTA) // editor = NewEditor() }
けどそれをすると golint に以下のように怒られてしまう (コンパイルは通る)。
exported func NewEditor returns unexported type *main.editor, which can be annoying to use
Vim で ALE を入れてマーカーに表示させてる様子 (うっとうしい)。
Lint の警告が出るのは精神衛生上よくないのでなんとかしたい。
というわけでもやもやしながらも struct を export するという選択をしてたのですが、
これを回避する方法があります。といっても Gopher には当然だろと怒られるかもしれませんが、
interface を定義して、 NewEditor()
でそれを返すことです。
type Editor interface { Hello() string } func NewEditor(name string) Editor { return &editor{name: name} }
NewEditor()
の返り値の型を定義した interface のものに変えました。
すると golint も unexported な型を返してるとは言わないですし、副作用として GoDoc を付ける必要のある箇所が減ります。
(本来 export する必要のない struct を export してたんだから当然ですが)
golint は GoDoc が付いてない export されてる interface、struct、メソッドに警告を出します。
そのため1つの interface に対して複数の struct (実装) がある場合、 以前は golint を黙らせるために各 Hello() メソッドに GoDoc が必要になりました。
package main import "fmt" // NewVim is ... func NewVim() *Vim { return &Vim{} } // NewNeoVim is ... func NewNeoVim() *NeoVim { return &NeoVim{} } // Vim is ... type Vim struct{} // Hello is ... func (e *Vim) Hello() string { return "こんにちは、ビムです" } // NeoVim is ... type NeoVim struct{} // Hello is ... func (e *NeoVim) Hello() string { return "こんにちは、ネオビムです" } func main() { // export してるからこれもできてしまう // editor := &Vim{} fmt.Println(NewVim().Hello()) // => "こんにちは、ビムです" fmt.Println(NewNeoVim().Hello()) // => "こんにちは、ネオビムです" }
しかし interface を定義すれば各 struct とそのメソッドに GoDoc を付ける必要がなくなります。 以下で struct へのコメントがなくなったのが分かると思います。 大量の struct がある場合にこれがかなり苦痛だったのが改善されました。
package main import "fmt" // Editor is ... type Editor interface { Hello() string } // NewVim is ... func NewVim() Editor { return &vim{} } // NewNeoVim is ... func NewNeoVim() Editor { return &neoVim{} } type vim struct{} func (e *vim) Hello() string { return "こんにちは、ビムです" } type neoVim struct{} func (e *neoVim) Hello() string { return "こんにちは、ネオビムです" } func main() { fmt.Println(NewVim().Hello()) // => "こんにちは、ビムです" fmt.Println(NewNeoVim().Hello()) // => "こんにちは、ネオビムです" }
結論
というわけで
- まず interface とコンストラクタ関数を定義することを考える
ことにしました。
なんか Java みたい (ファクトリ関数定義しよう、全てのプロパティに getter を生やそう) ですが、 API の互換性とか考えるならやっぱ interface が一番ですね。 もちろんもっとカジュアルな用途 (個人プロジェクトとかチーム間でノウハウ共有できてるとか) を想定するなら struct をそのまま export してもいいかもしれませんが、個人的には (リファクタリングした時にうっかり必須フィールド付け忘れとか) ミスを減らすために export する API は基本これで行こうかなーと考えてます (そもそも Go の struct 生成の仕様がゆるふわすぎる)。
ちなみに (ALE の Vim 設定)
ALE はデフォルトだとかなり頻繁な間隔で golint とかぶん回してくれてもたついてウザいので、
こんな感じに設定すると :w
のタイミングでのみ golint ぶん回すようになってくれて便利。
let g:ale_lint_on_enter = 1 let g:ale_lint_on_filetype_changed = 1 let g:ale_lint_on_save = 1 let g:ale_lint_on_text_changed = 'never' let g:ale_lint_on_insert_leave = 0
neosnippet でプレースホルダがある場合は展開よりジャンプを優先させる
今週の Vim の細道見てて neosnippet で困った挙動があるのを思い出して、重い腰上げて help 見て設定したら解消した。
<Tab>
にこんな感じで割り当ててたけど
imap <expr> <Tab> neosnippet#expandable_or_jumpable() ? \ "\<Plug>(neosnippet_expand_or_jump)" : "\<Tab>" smap <expr> <Tab> neosnippet#expandable_or_jumpable() ? \ "\<Plug>(neosnippet_expand_or_jump)" : "\<Tab>"
これだとプレースホルダーがある時に <Tab>
を押すとジャンプよりも展開を優先してしまう。
func<Tab>
で展開した後、以下で <Tab>
を押すと path
が現在のファイルのパスに展開されてしまう (まだプレースホルダがある時は次のプレースホルダに移ってほしい)。
function! s:func(path<Tab>) <`0:TARGET`> endfunction
そこで <Plug>(neosnippet_expand_or_jump)
の代わりに <Plug>(neosnippet_jump_or_expand)
使うようにしたら次のプレースホルダへのジャンプを優先してくれるようになって便利。
imap <expr> <Tab> neosnippet#expandable_or_jumpable() ? \ "\<Plug>(neosnippet_jump_or_expand)" : "\<Tab>" smap <expr> <Tab> neosnippet#expandable_or_jumpable() ? \ "\<Plug>(neosnippet_jump_or_expand)" : "\<Tab>"
Go とかでも同じように困ってて、 ok
が定義されてるので以下で <Tab>
を押すと
if v, ok := m["key"]; ok<Tab> { <`0:TARGET`> }
以下のように展開されてしまっていた。
if v, ok := m["key"]; if !ok { } { <`0`> }
これは ok<Tab>
が以下に展開されるため。
if !ok { <カーソル位置> }
けどこれもちゃんと以下のようにジャンプを優先してくれるようになった。
if v, ok := m["key"]; ok { <カーソル位置> }