Vimを支える技術: Alacritty, AquaSKK, tmux, Language Server… 高速ウェブ開発の世界
はじめに
これは、ストックマーク 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も入るので見た目をかっこよくするのが簡単になります。
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
に設定します。
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はちゃんと波下線になったと思うので、そこはちょっと残念なところです。
let &t_Cs = "\e[4:3m"
let &t_Ce = "\e[4:0m"
カラーテーマ
次にカラーテーマですが、せっかくなのでAlacritty、tmux、Vimと同じカラーテーマを適用していきたいと思います。私が知っている範囲ではNordやDraculaあたりが、様々なアプリ向けに統一されたカラーテーマを提供していて、ここ最近はNordがお気に入りなのでそれを使います。
Alacritty
ココの内容を.alacritty.yml
にコピペします。
tmux
こちらを参考に、tpmを使ってインストールします。
set-option -g @plugin "arcticicestudio/nord-tmux"
Vim
こちらを参考にインストールします。ちなみに、Vimのプラグインマネージャーはいくつか実装があり、Nordのページではわりと広く使われているvim-plugの設定例が記載されていますが、私はminpacというシンプルなものを使っているので、設定は以下のとおりになります。
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のリネームや定義へのジャンプなど便利な機能をすぐに使えるようにしている感じです。
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-langserverとefm-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もすこーしだけ触ることがあるので、だいたい以下の設定でどの言語で対応できるようにしています。
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に興味を持ってもらえたら嬉しいです。
Discussion
初めまして、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つの選択肢として見ていただければ幸いです。
コメントありがとうございます!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で矛盾しているところがあるので修正予定…)。