🖊️

Vimを支える技術: Alacritty, AquaSKK, tmux, Language Server… 高速ウェブ開発の世界

2021/12/17に公開
2

ストックマーク Advent Calendar 2021

はじめに

これは、ストックマーク Advent Calendar 2021 17日目の記事です。こんにちは、ストックマークでAstrategyというビジネス向けSaaSについて、主にフロントエンドの開発を担当している@tsukkeeです。

Astrategyの技術構成については以前にAstrategyを支える技術: gRPC, Elasticsearch, Cloud TPU, Fargate... SaaS型AIサービスの内側の世界という弊社テックブログ記事で紹介したことがあるのですが、本記事ではその開発環境の一部を紹介したいと思います。

さて、開発環境と言えばテキストエディタですが、皆さん開発にはどのテキストエディタ(またはIDE)を使っていますでしょうか?本記事のタイトルにもあるとおり私はVimを使っています。ただ、Astrategyの開発チームでは使うテキストエディタに制限はなく、また多様性を重視する文化なので、Emacs、IntelliJ IDEA、Cloud9、Visual Studio Code、そしてVimと開発チームの5人が5人とも別のエディタを使って開発しています。

さらに、弊社では心理的安全性が担保されているため、俗に言うエディタ戦争のようなものも起こりません。その証拠に、このAdvent Calendarの9日目にもEmacsの記事が書かれていますし、Slackのカスタム絵文字は以下のように設定されています(私を採用してくれたEmacs使いの当時の上司が設定したものです)。素晴しいですね!

ということで、以降では私のVim環境 on macOSについて紹介したいと思います。なお、私のこのあたりの設定ファイルはだいたいGitHubで公開しているので興味のある方はあわせてご覧ください。

なぜVimを使うのか?

そもそもなぜVimを使っているのかは語り出すと色々あるのですが、おおまかにはプログラムのソースコード(MarkdownやLaTeXなどによるドキュメントも含む)を編集することに特化した機能が多く搭載されているとことと、全ての操作がキーボードで完結して素早く操作できるところかなと思います。

具体的な例としては、テキストオブジェクトという機能があり、様々な単位でテキストの固まりをオブジェクトとして扱って編集できる機能があります。例えば、以下のようなテキストがあり|にカーソルがあったときに、

funciton hoge(arg1, |arg2) {

di(と入力するとdが消す、iが内側、(で丸カッコということで、()内のどこにカーソルがあっても一気に引数部分を消して以下のような状態にすることができます。

funciton hoge(|) {

ソースコードを書いていると、このようにテキストを固まりで扱うことが多いのでこれは大変役に立つ機能です。Vimを学ぶでもっともオススメの書籍であるPractical Vim(日本語版: 実践Vim)にも「Edit Text at the Speed of Thought(思考のスピードで編集しよう!)」というキャッチフレーズが付いていますが、正にこの感覚を得られるところが素晴しいところだと感じています。

環境構築のポイント

このように素晴しいVimをCUIで使おうと思うのですが、CUIと言えばなんだか真っ黒で味気ない画面を想像する方もいるかも知れません。しかし、昨今のターミナルはTrue color(RGBの24bit color)表示ができるので、以下で詳細を述べますが、Alacritty + tmux + VimでTrue color表示できる環境を構築します。

また、VimでもLanguage Serverを活用することでVisual Studio Codeなど他のテキストエディタと遜色ないコーディング環境を作ることができるので、ここではvim-lspというLanguage Serverクライアントプラグインを軸に、Formatter, Linter, Fuzzy Finderが連携できるように設定します。

最終的には以下のような感じになります。

ターミナル

Vimを使う上では、CUI上のVimを使うのか、GUI版のgVim(macOSなのでMacVim)を使うのかという選択肢があるのですが、私は他のCUIツールとの連携を重視してCUI版のVimを使っています。なお、Neovimという選択肢もありますが、私はなんやかんやで15年以上はVimを使っており、今のところ積極的に移行する理由がなく、Neovimと同様に現在も活発に開発が続いているVimを使っています。

その上で、私は日本語入力にAquaSKKを使っています。ところが、この記事を執筆している段階でAquaSKKで良い感じに日本語入力できるターミナルはAlacrittyのmasterブランチを自力でビルドしたもの(と、iTerm2も次点でギリいける)しか存在しません(筆者調べ)。適当に最新のRustをインストールしておいてから、GitHubからcloneしたディレクトリで

$ make app

するとビルドできるので、それを使います。そして、AquaSKKの環境設定 → 互換性でio.alacrittyに対して「露払後確定」を設定すれば完璧です!この記事自体もこの設定で書いていますが、特に問題は発生していません。

ちなみに、私が試した範囲では、他のターミナルの状況は以下のとおりです。

  • Terminal.app: まずTrue color表示に対応していないので却下。

  • iTerm2: この記事この記事に記載の内容をがんばって設定するとだいたいいけるが、稀に入力切り替えができなくなることがある。

  • Kitty: AquaSKKの入力切替は概ね問題ないが、Vimで日本語を書いているとどんどんその行の表示が壊れてくる。入力自体はできていて、他の行にカーソルを移動すると正常な表示に戻りはするが結構辛い。
    [2022/1/5 追記] おそらく https://github.com/kovidgoyal/kitty/issues/4219 が取り込まれた影響で、Kittyでも0.24.0から日本語入力の問題がかなり解消されています!場合によってはAlacrittyではなくKittyを使うという選択肢もありそうです。

  • Hyper: AquaSKKを使っていると「あ」「い」「う」「え」「お」が正しく入力できない(「a」「i」「u」「e」「o」とアルファベットになってしまう)。

  • リリース版のAlacritty: こちらのIssueで言及されている依存ライブラリの日本語入力対応が入っていないので、AquaSKKに限らず日本語が入力できない。

Alacrittyは~/.config/alacritty/alacritty.ymlを編集することで設定できます。Alacrittyリポジトリに雛形があるのでこれをコピーしてきて、必要なところだけコメントを外すのが簡単です。私はあまり多くは設定しておらずだいたい以下のとおりです。フォントはPremolJPを利用しており、PremolJP Console NFの方を利用すればNerd Fontsも入るので見た目をかっこよくするのが簡単になります。

~/.config/alacritty/alacritty.yml
window:
  # バグ回避: https://github.com/alacritty/alacritty/issues/4474
  opacity: 0.99999

font:
  normal:
    family: PlemolJP Console NF
  size: 19.0

live_config_reload: true

key_bindings:
  # 日本語キーボードでバックスラッシュを入力する
  - { key: Yen,                                         chars: "\x5C"          }
  - { key: Yen,        mods: Alt,                       chars: "\xA5"          }

  # USキーボードでControl-^を認識させる
  - { key: Key6,       mods: Control,                   chars: "\x1e"          }

ターミナルマルチプレクサ

Alacrittyはウィンドウ分割などの機能は搭載しない方針らしいので、tmuxを使います。ちなみに、私はAlacrittyを使わないときでもだいたいターミナルのウィンドウ分割やタブの機能は使わずtmuxに任せています。

さて、AlacrittyはTrue color表示に対応していますが、tmuxにもそれを認識させる必要があります。まずは、Alacritty用のTerminfoをインストールします。AlacrittyのINSTALL.mdに書かれているとおり、Alacrittyをcloneしてきたところで、

$ sudo tic -xe alacritty,alacritty-direct extra/alacritty.info

を実行すればOKです。さらに、tmux-256colorのTerminfoも必要なのでtmuxのIssueのコメントにもあるとおり、

$ brew install ncurses
$ /usr/local/opt/ncurses/bin/infocmp tmux-256color > ~/tmux-256color.info
$ tic -xe tmux-256color tmux-256color.info

でインストールします。その上で、.tmux.confに以下を追記します。

set-option -g default-terminal 'tmux-256color'
set-option -ga terminal-overrides ',$TERM:Tc'
set-option -ga terminal-overrides ',alacritty:RGB'

これによって、tmux側でもTrue colorであることを認識できるようになります。tmuxのその他の設定についてはここでは詳細に述べませんが、tpmというtmux用のプラグインマネージャは入れておくと便利です。

Vim

ようやくVimまで来ました。Alacritty、tmuxとTruc color設定をしてきましたが、さらにVimにも設定が必要です。Vimの:h xterm-true-colorにだいたい説明がありますが、以下を.vimrcに設定します。

~/.vimrc
set termguicolors
let &t_8f = "\<Esc>[38;2;%lu;%lu;%lum"
let &t_8b = "\<Esc>[48;2;%lu;%lu;%lum"

ちなみに、undercurl(下波線)対応のため:h undercurlを参考に以下の設定も追加していますが、私の環境ではAlacrittyではただの下線になってしまうようです。KittyやiTerm2はちゃんと波下線になったと思うので、そこはちょっと残念なところです。

~/.vimrc
let &t_Cs = "\e[4:3m"
let &t_Ce = "\e[4:0m"

カラーテーマ

次にカラーテーマですが、せっかくなのでAlacritty、tmux、Vimと同じカラーテーマを適用していきたいと思います。私が知っている範囲ではNordDraculaあたりが、様々なアプリ向けに統一されたカラーテーマを提供していて、ここ最近はNordがお気に入りなのでそれを使います。

Alacritty

ココの内容を.alacritty.ymlにコピペします。

tmux

こちらを参考に、tpmを使ってインストールします。

~/.tmux.conf
set-option -g @plugin "arcticicestudio/nord-tmux"

Vim

こちらを参考にインストールします。ちなみに、Vimのプラグインマネージャーはいくつか実装があり、Nordのページではわりと広く使われているvim-plugの設定例が記載されていますが、私はminpacというシンプルなものを使っているので、設定は以下のとおりになります。

~/.vimrc
call minpac#init()
call minpac#add('arcticicestudio/nord-vim')

let g:nord_bold_vertical_split_line = 1
let g:nord_italic = 1
let g:nord_italic_comments = 1

colorscheme nord

ここまでで、以下のような雰囲気で良い感じの表示になります。なお、Vimのstatuslineやtablineまわりはlightline.vimを使うと簡単にかっこよく設定することができるので試してみてください。

Language Server, Formatter, Linter

ここからは、編集機能の部分について簡単に紹介します。

最近のウェブ開発においては、エディタと開発をサポートするツール群(Language Server、Formatter、Linterなど)は分離されていることがほとんで、そのおかげで開発メンバーは好きなエディタを使いつつ、同じFormatterやLinterを適用することで統制の取れたコードを書くことができます。例えば、AstrategyのフロントエンドはVueで作られているのですが、Language ServerとしてVolarを使おう、FormatterはPrettier、LinterはESLintといったところの合意が取れていれば、あとはeslintrcやprettierrcをリポジトリに入れておけば、どのエディタを使っても問題ない状態にすることができます。

まず、Language ServerのクライアントはVim向けにいくつか実装がありますが、ここではVimでは広く使われていると思われるvim-lspを使います。さらに、自動補完用にasyncomplete.vim、そしてスニペット補完用にvim-vsnip及びvim-vsnip-integを導入します。ちょっと長いですが、私の設定の肝はだいたい以下のとおりになっています。だいたいgが頭に付いたキーバインドでLanguage Serverのリネームや定義へのジャンプなど便利な機能をすぐに使えるようにしている感じです。

~/.vimrc
set completeopt& completeopt+=menuone,popup,noinsert,noselect
set completepopup=height:10,width:60,highlight:InfoPopup

call minpac#add('prabirshrestha/vim-lsp')
call minpac#add('prabirshrestha/asyncomplete.vim')
call minpac#add('prabirshrestha/asyncomplete-lsp.vim')
call minpac#add('mattn/vim-lsp-settings')
call minpac#add('hrsh7th/vim-vsnip')
call minpac#add('hrsh7th/vim-vsnip-integ')

function! s:on_lsp_buffer_enabled() abort
    setlocal omnifunc=lsp#complete
    setlocal tagfunc=lsp#tagfunc
    setlocal signcolumn=yes
    nmap <buffer> gd <plug>(lsp-definition)
    nmap <buffer> gr <plug>(lsp-references)
    nmap <buffer> gi <plug>(lsp-implementation)
    nmap <buffer> ge <plug>(lsp-type-definition)
    nmap <buffer> gR <plug>(lsp-rename)
    nmap <buffer> gA <Plug>(lsp-code-action)
    nmap <buffer> gs <Plug>(lsp-document-symbol-search)

    if &filetype !=# 'vim'
        nmap <buffer> K <plug>(lsp-hover)
    endif
endfunction

augroup vimrc
  autocmd User lsp_buffer_enabled call s:on_lsp_buffer_enabled()
augroup END

let g:asyncomplete_auto_popup = 1
let g:asyncomplete_auto_completeopt = 0
let g:asyncomplete_popup_delay = 200
let g:lsp_text_edit_enabled = 1

imap <expr> <C-l>   vsnip#available(1)  ? '<Plug>(vsnip-expand-or-jump)' : '<C-l>'
smap <expr> <C-l>   vsnip#available(1)  ? '<Plug>(vsnip-expand-or-jump)' : '<C-l>'
imap <expr> <Tab>   vsnip#jumpable(1)   ? '<Plug>(vsnip-jump-next)'      : '<Tab>'
smap <expr> <Tab>   vsnip#jumpable(1)   ? '<Plug>(vsnip-jump-next)'      : '<Tab>'
imap <expr> <S-Tab> vsnip#jumpable(-1)  ? '<Plug>(vsnip-jump-prev)'      : '<S-Tab>'
smap <expr> <S-Tab> vsnip#jumpable(-1)  ? '<Plug>(vsnip-jump-prev)'      : '<S-Tab>'

また、vim-lsp-settingsがインストールされているので:LspInstallServerとするだけで、そのとき開いているファイルのfiletypeに応じたLanguage Serverがインストールされ、すぐに使いはじめることができます。Vueについては、デフォルトがVeturの中身のvlsになっていますが、最近だとVolarの方が便利なので:LspInstallServer volar-serverとすると良いです。なお、vim-lsp-settingsのVolarの設定は私がPRを出していることが多い(これとかこれとか…PR作成にあたっていつもサポート頂いているVimコミュニティの方々に感謝!)ので、気付いたところがあれば教えていただくか、PRを出していただくと良いと思います。

ところで、Visual Studio Code中心に発展してきているLanguage Server界隈ですが、その出自はVimだったりします。Language Server ProtocolのWikiにも記載がありますが、C#向けに開発されていたOmniSharpというVimプラグインが使っていた方式を発展させたのがLanguage Server Protocolになっているようです。

話を戻して、FormatterとLinterの適用については、ALEを使うのが簡単です。さらに、vim-lsp-aleと一緒に使うことで、vim-lspと良い感じに連携が取れるようになります。
[2021/12/20 追記] コメント頂いてますが、vim-lspとFormatterやLinterの連携については、efm-langserverefm-langserver-settingsを使う方法もあります。私は以前からALEを使っていたのでそのままvim-lspとALEの連携で使っていますが、これから環境構築される方は両方試してみても良いかも知れません。また、vim-lsp-aleはvim-lspとALEの機能が衝突しないように設定を調整してくれるので(詳細は:h vim-lsp-aleにて)そこで問題が発生することはありません。

私のAstrategy開発だと、Vue + TypeScript + SCSSによるフロントエンド開発をメインに、webpackなどの設定でJavaScriptを書くこともあり、バックエンドまわりでPython(Poetry利用)とGoをちょろりと触り、あと実験的なところでRustもすこーしだけ触ることがあるので、だいたい以下の設定でどの言語で対応できるようにしています。

~/.vimrc
call minpac#add('dense-analysis/ale')
call minpac#add('rhysd/vim-lsp-ale')

let g:ale_floating_preview = 1
let g:ale_linters_explicit = 1
let g:ale_fix_on_save = 1
let g:ale_fixers = {
\   'javascript': ['prettier'],
\   'typescript': ['prettier'],
\   'vue': ['prettier', 'eslint', 'stylelint'],
\   'scss': ['prettier', 'eslint', 'stylelint'],
\   'rust': ['rustfmt'],
\   'go': ['gofmt'],
\   'python': ['black', 'isort']
\}
let g:ale_linters = {
\   'typescript': ['eslint', 'vim-lsp'],
\   'vue': ['eslint', 'stylelint', 'vim-lsp'],
\   'scss': ['stylelint'],
\   'rust': ['clippy'],
\}
let g:ale_linter_aliases = {'vue': ['vue', 'typescript', 'scss']}
let g:ale_python_auto_poetry = 1

nmap <C-K> <Plug>(ale_detail)
nmap g] <Plug>(ale_next)
nmap g[ <Plug>(ale_previous)

Fuzzy Finder

インタラクティブにファイルや行、シンボルなどを検索できるFuzzy Finderと呼ばれる種類のプラグインも入れます。Vim向けの実装はなんかたくさんあるのですが、私はvim-lspとも連携できるvim-clapを使っています。vim-lspとの連携にはvista.vimも必要なので合わせてインストールします。

call minpac#add('liuchengxu/vim-clap')
call minpac#add('liuchengxu/vista.vim')

let g:clap_layout = {
\   'relative': 'editor',
\   'width': '70%', 'col': '15%',
\   'height': '40%', 'row': 3
\}
let g:clap_popup_move_manager = {
\ "\<C-N>": "\<Down>",
\ "\<C-P>": "\<Up>",
\ }
let g:clap_preview_direction = 'UD'

この状態で、:Clap tags vim_lspとすると、vim-lspが認識するシンボル情報をClap上で絞り込んでジャンプすることができます。

おわりに

かなり駆け足でしたが、私が普段開発で使っているVim環境について紹介しました。Vimの環境構築に関する記事は世の中にたくさんありますが、Alacritty, AquaSKK, tmux, Vimの組み合わせを網羅しつつ必要十分な設定を紹介している記事はあまりないと思いますので、参考になるところがあればと思います。また、これまでVimを使ったことがないよという方(がこの記事を最後まで読んでくださっているか分かりませんが…)も、この機会にVimに興味を持ってもらえたら嬉しいです。

GitHubで編集を提案

Discussion

Yuki YanoYuki Yano

初めまして、Yanoといいます。
丁度自分も先日cocを使った開発環境についての記事を書かせて貰ったのですが、この記事ではvim-lspを用いた開発環境について書かれていて非常に参考になりました。

個人的な意見として一点気になった点があるのですが、ALEは近年の開発環境においてtoo muchなプラグインかと思っています。
ALEはLSPが普及する以前から開発されていて機能が過剰になりつつあり、LSPクライアントと併用することで全体的な動作の把握の難易度がどうしても高くなってしまいます。それに伴ってALEを使うことでパフォーマンスの問題が発生している方を見かけることもあります。
あとはvim-lsp-aleがうまくやってくれているのだと思うのですが、signやquickfixを扱うプラグインは競合することが多いので自分はあまり併用したくないなどもあります。

自分はlintはLSPが提供するdiagnosticsを基本的に使い、対応していないものについてはcliをLanguage Serverとして扱えるefm-langserverを併用するのがいいかと思っています。
シンプルにLSPクライアントレベルで処理が閉じるのと、lintとformatの設定については多くのものがefm-langserver-settingsで提供されているため使うのも難しくないです。

正直この辺りのツールに何を採用するかは好みも大きいのでALEを使うこと自体を否定するつもりはないのですが、1つの選択肢として見ていただければ幸いです。

tsukkeetsukkee

コメントありがとうございます!Yanoさんの記事も読ませていただきました。
私の記事の方がプラグインの選定基準や設定の説明が不足しておりすみません(このあたり後ほど追記しておきたいと思いますー)。efm-langserverも良い選択肢だと思っているのですが、ALEでそこまで困っていなくて積極的に移行する理由がなく試せていないのが実情なので、今後さらに設定を見直していく中で検討できればと思っています。
ちなみに、vim-lspとALEで機能がオーバーラップする部分はvim-lsp-aleがそれぞれ良い感じに設定してくれるので(参考: https://github.com/rhysd/vim-lsp-ale/blob/master/doc/vim-lsp-ale.txt )、少なくとも私が使っている範囲では変な動きになったりはしておらず、そこは大丈夫そうかなと思っています(と言いつつ、あらためて見比べるとvim-lsp-aleがしてくれる設定と自分のvimrcで矛盾しているところがあるので修正予定…)。