年末だし、プラグインの入れ替えをしました

この記事は Vim Advent Calendar 2021 11日目の記事です。

My (Neo)Vim RC is here!

github.com

背景

なんだかんだVimを使い初めて9年が経っておりました。

プラグインGitHubで管理し始めて6年です。

更にVimからNeoVimに変えて5年が経ちます。

これまでも色々なプラグインを追加したり取り除いたり乗り換えたりしてきました。最初は10個ほどだったプラグインも気付けば60前後になっています。

設定ファイルは、ずっとひとつのvimrcを加筆修正してきたわけではなく、たまには全体を見直して調整などをしておりました。 それでも積み重なったものはあるようで、喧嘩しているプラグインや、はたまた組み合わせて拡張したいプラグインなどがでてきました。

そこで、直近で全体的な見直しを行いましたので、その時の思いや変化を書き残したいと思います。

解決したいこと

今回見直すにあたって、解決したかった課題は以下のとおりです。

(1) dein を使いこなせてない

プラグインマネージャーは、最初 NeoBundle を使っており、 NeoVim 移行のタイミングで dein を使い始めました。

dein は、プラグインの読み込みタイミングの細かい制御や、依存関係の管理など、高度な機能を数多く提供しています。しかし、私は実際のところこれを使いこなせていません。

例えば、 g:hoge_auto_enable = 1 が設定してあるときに自動で有効になるプラグインがあるのですが、その設定を書いているにも関わらずファイルの開き方(起動時に開くかVim内から開くか)で動かなかったりしました。

また、プラグインAとBが両方読み込まれたときに有効にしたい設定をどこに記述するのかも悩ましいところでした。私は dein の設定をTOMLで管理していましたが、片側のhookに書くのは反対側から見えなくなるので違うな……と思っていました。

(2) LanguageServerが二重起動している

6年前はまだLanguageServerは影もありませんでした。また、静的解析系のプラグインもなしで、書いたコードを逐次コンパイル(または実行)して確認していました。 小さいコードのうちはそれで全然問題なかったのですが、コードが大きくなってくるとエラーメッセージの見やすさやコンパイル時間が問題になってきます。

最初に導入したのは Syntastic でした。これでJavaファイルの保存時にエラーを出すようにしていました。とはいえ、 Syntastic は同期的にチェックしますので、実行時間が問題になってきます。

そこでしばらく悩んでいたところ、 非同期でエラーチェックしてくれる ALE を見つけました。これはいろいろなLinter, compilerのエラー情報を非同期でチェックし、まとめて出してくれるプラグインです。 all-in-one 系のプラグインなので、設定もあまりせずに使えていました。

しばらくしてLanguageServerが(私のvimrcに)登場しました。 LanguageServerでは、静的解析や補完の提供、フォーマットなど、IDEの機能を一通り提供してくれます。 私は deoplete + LanguageClient-neovim を使って、こちらで補完のみを行い、静的解析は ALE で行っていました。 というのも、LanguageServerが提供されたばかりの頃は、あまり安定していなかったり、言語によってまだ「これ」というものがなかったりしており、「linterもLanguageServerも使っていい感じの解析結果になる」くらいの塩梅でした。 この頃から「 ALELanguageClient-neovim の両方でLanguageServerを起動している」問題がありました。これの何が問題かというと、たとえば rust ではビルド結果のディレクトリのロックを取り合うので、片方の解析結果が遅れたり、CPUへの負荷が無駄に高くなったりしていました。

それから時が経って、LanguageServerが成熟してきました。linterが全く必要ないとは言いませんが、私の環境ではLanguageServerメインでやっていってもいい頃になってきたなと思っています。

(3) ファイルビューアーが重い

長らく NERDTree を愛用してきました。これにプラグインを追加して、Gitのステータスを見られるようにしていました。

NERDTree は他のエディタから入ってきたときに、一番わかりやすく便利なプラグインかもしれません。編集しているファイルの横にファイルツリーを表示したいというのは普通の欲求です。また、 NERDTree にはデフォルトで(ヘルプ表示を含む)各種キーバインドがあります。覚えるのに少しかかりますが、入れるだけで無設定で使えるのは便利です。

一方で、比較的重いプラグインでもあります。 NERDTree 単体だとそこまでではないのですが、 nerdtree-git-plugin を入れると少し重くなり(バッファを開くのに若干時間がかかる程度)、 vim-devicons + vim-nerdtree-syntax-highlight の追加で激重になります(j連打でのカーソル移動が数秒遅延する程度)。 ファイルのアイコン表示自体はいいなと思ったのですが、流石にカーソル移動がままならないのでは使い続けるのは難しいので、より軽量な解決策が必要になりました。

今回の大きな変更点

上記の解決したいことを踏まえて、更新をしていきました。ここでは、大きな変更点を紹介します。

vim-plugへの移行

これは後述するプラグイン設定の管理方法のためにやった部分もありますが主に (1) のために、プラグイン管理を dein から vim-plug へと移行しました。

vim-plug は、長くメンテされているプラグインマネージャで、外部依存がありません。ファイルタイプとコマンド実行時の遅延読み込みはあるものの、他の高度な機能は無く、シンプルなものになっています。

今回その他プラグインマネージャーも見たのですが、これだけ機能があれば十分なことと、長く広く使われているためWebから情報を拾いやすい点でこちらにしました。

プラグイン設定の管理方法

プラグイン設定の管理方法は、ほとんど以下を参考にしました。

zenn.dev

ただし、上記はプラグインごとのconfigファイルの場所がruntimepathを前提にしているので、少し変えています(私はvimrcを書き替えるときにvimが使えないと困るので、 nvim -u path/to/init.vim でなるべくどこでも動くようにしています)。

現在、この方法で問題なく作業できています。特に難しいところはなく、プラグインマネージャーの設定ではなくプラグインや本体の設定改善に時間を使えています。

coc frameworkへの移行

お世話になった ALE, deoplete, LanguageClient-nvim, vim-lsp を外し、 coc を導入しました。

踏み切れたのは、LanguageServer関連の成熟が大きいと思います。 また、 cocREADMEWiki におすすめの設定やmappingが書かれていて、非常に導入しやすかったです。

cocになって、大きく不満がある箇所は特にないです。全体的に問題なく動いています。

今は、直前まで使っていた vim-lsp との使用感の違い(例えば、CodeActionの対象があったときに、範囲指定が必要かどうかなど)に慣れていっている段階です。

Fernへの移行

NERDTree から Fern へ移行しました。関連プラグインと、 Wiki記載の NERDTree に寄せるマッピング でおよそ違和感なく使えています。

NERDTree と Fern の差を埋める

以下設定で NERDTree 風のコマンドを定義しました。

私は :NERDTreeFind を多用していたのですが、これに相当するコマンドは Issue に記載されていました。

command! FERNTree       Fern . -drawer
command! FERNTreeToggle Fern . -drawer -toggle
command! FERNTreeFind   Fern . -drawer -reveal=%
command! FEF            FERNTreeFind

コマンド定義はしたものの、絶対に :NEF (:NERDTreeFindマッピングしている) を打ってしまうと思うので、 NERDTree が読み込まれていないときは NERDTree 系のコマンドを fern に寄せます。

まず、 UsePlugin コマンドを拡張して否定形を利用できるようにします。

" UsePlugin 'hoge' で利用チェック
" UsePlugin! 'foo' で不使用チェック
command! -bang -nargs=1 UsePlugin if <bang>!FindPlugin(<args>) | finish | endif

そして、 fern を利用していて NERDTree を入れていないときだけ :NERDTree:FERNTreeマッピングしました。

UsePlugin 'fern.vim'
UsePlugin! 'nerdtree'


" 絶対打ってしまうと思うのでmapping
command! NERDTree       FERNTree
command! NERDTreeToggle FERNTreeToggle
command! NERDTreeFind   FERNTreeFind
command! NEF            FERNTreeFind

プラグインと設定紹介

最後に、いくつかプラグインと設定を紹介します。

easymotion

カーソル移動系のプラグインです。 画面内の任意の位置に、おそらくほぼ最短で辿り着けるプラグインだと思います。

重要な特徴として、 視線を移動目標から動かさずに辿り着ける というのがあるかと思います。 README最初のGIFは、565行目の let s:s へ移動しようとしています。ぜひこれを認識した上でもう一度GIFをご覧いただきたいです。

プラグインの詳細は以下の記事をご覧ください。 haya14busa.com

私の設定ですが、以下のようにして、normalモードでの s, f (行内ジャンプ)をよく使います。

nmap s <Plug>(easymotion-s2)
xmap s <Plug>(easymotion-s2)
map f <Plug>(easymotion-fl)
map t <Plug>(easymotion-tl)
map F <Plug>(easymotion-Fl)
map T <Plug>(easymotion-Tl)

gina

GIt操作系プラグインです。

:Gina statusgit status と同様のバッファを開き、そこで add, reset を行い、 commit していきます。

それほど多くを覚えずとも使えるのと、複数のファイルをまとめてaddできる点が便利です。

設定としては、 lightline に情報を表示するのに直接関数を呼ぶのではなく、適当に状態管理して呼び出しを削減し、変数にキャッシュしています。これは、statuslineに重くなり得るコンポーネントを置くとカーソル移動に影響する(遅くなる)事があるからです。

augroup gina-status-lazy
  autocmd!
  autocmd CursorMoved,CursorMovedI * call s:f_gina_status('CursorMoved')
  autocmd CursorHold,CursorHoldI * call s:f_gina_status('CursorHold')
  autocmd WinEnter * call s:f_gina_status('WinEnter')
  autocmd WinLeave * call s:f_gina_status('WinLeave')

  let s:gina_status = 0
  let g:gina_status_str = MyGinaRepoStatus()
  function! s:f_gina_status(event)
    if a:event ==# 'WinEnter'
      let g:gina_status_str = MyGinaRepoStatus()
      let s:gina_status = 2
    elseif a:event ==# 'WinLeave'
      " Do nothing
    elseif a:event ==# 'CursorMoved'
      if s:gina_status
        if 1 < s:gina_status
          let s:gina_status = 1
        else
          " Do nothing
          let s:gina_status = 0
        endif
      endif
    elseif a:event ==# 'CursorHold'
      let g:gina_status_str = MyGinaRepoStatus()
      let s:gina_status = 1
    endif
  endfunction
augroup END

(今見れば読めたものではないですが、とりあえず呼び出しを削減したかった、ということだけお伝えできればと思います)

vista

LSPなどの情報をもとにファイルの構造情報(アウトラインや関数変数の一覧など)を出せます。 設定することはあまりなく、 :Vista で構造を表示できます。それだけなのですが、IDEライクになり便利です。

その他個別の設定

現在編集しているファイルの相対パスを取得する

チームで作業しているとき、リポジトリpath/to/Hoge.java についてコミュニケーションをとりたいときがあるかと思います。 ではそのファイル名をどのように伝えたら良いのでしょうか。

  1. Hoge.java などファイル名だけtypeする
  2. GitHubで同じファイルを開いてURLで共有する
  3. Vimにコマンドを定義してクリップボードにコピーする

1はシンプルに大変です。

2はファイル名が十分短く一意ならば有効です。一方でファイル名が一意でないときは曖昧な補足情報(「infra の~」とか)を付け加えたりする必要があります。

3はブラウザを開き、同じファイルにたどり着くだけ手間がかかります。

そういうわけで私は4の方法を取りました。 私は基本的にrepository rootでVimを開いているので、以下のような定義で :CopyFilePath を定義しました。

function! s:lineNumberWithSeparator(line1, line2)
  if a:line1 != a:line2
    return ": " . a:line1 . "-" . a:line2
  endif
  return ""
endfunction

"""現在のファイルの相対パスを取得
function! RelativeFilePath(line1, line2)
    return expand("%"). s:lineNumberWithSeparator(a:line1, a:line2)
endfunction

command -range CopyFilePath let @+ = call('RelativeFilePath', [<line1>, <line2>])

このコマンドは範囲選択にも対応しており、その場合は path/to/Hoge.java: 12-15 のように行番号付きでコピーできます。 また、私の環境では :Cop で一意になるのでこのコマンドが実行できます。

セッションファイルの保存先設定

vim-session を使っているのですが、セッションファイルの保存先を以下の優先順位で決めています。

  1. 現在のディレクトリ直下の .vimsessions フォルダ
  2. gitリポジトリ配下ならリポジトリroot直下の .vimsessionsフォルダ
  3. ~/.config/nvim/default-sessions

また、Gitのブランチごとにセッションを簡単に分けられるよう、 :SaveBranchSession, :LoadBranchSession を定義しています。 このあたりの設定は長くなるので 設定ファイル をご覧ください。

おわりに

久しぶりにvimrcの大きな見直しができました。軽量化できたもの、まだ改善したいものなどもありますが、いいデジタル大掃除ができたと思っています。