Λlisue's blog

つれづれなるままに更新されないブログ

Enhance Vim's Experience

ご無沙汰しております、有末です。 Vim Advent Calendar 2017 の 12/24 の記事となります。

皆様、本日は休日かつクリスマス・イブという最高の日ですね。 イブといえば、デコレーションが施された街に愛するパートナーと出かけ、楽しい時間を過ごすというのが一般的な日本人の過ごし方かと思います。

しかしながら、これはあくまでも「日本」という国での話です。 クリスマス・イブの「イブ」というワードは、英語圏では "EVE" と書かれます。 これは "Enhance Vim's Experience" の略語であり、本来クリスマス・イブとは Vim の生誕を祝福し、次年度以降に更に効率的に仕事をこなすために vimrc を見直す日でした。

ということで、今年のイブは僕が利用している vimrc から便利そうなやつをピックアップします。 皆様に置かれましても、本来のクリスマス・イブの意味を尊重し「あ、これ良いな」と思う設定があったら積極的に盗んで作業効率アップを目指しましょう。

なお、明日は平日です。この意味を十分に理解した上でいつ読み進めるかを判断しましょう。

最初の一歩

まずは最低限必要だと思われる設定を記載します。 以下を参考にしてください。

if has('vim_starting')
  set encoding=utf-8
  scriptencoding utf-8
endif

" -----------------------------------
" ここに追記していく
" -----------------------------------

" シンタックスハイライトを有効化
" ファイルタイプ別インデント・プラグインの有効化
syntax on
filetype indent plugin on

" 任意ディレクトリに存在する vimrc による悪意を持ったコマンド実行を防ぐ
" ~/.vimrc や ~/.config/nvim/init.vim の一番最後には必ず書くのがお薦め
set secure

" このファイルを開いた際のルールを以下で定義する
" 詳細は ':help modeline' を参照
" vim: expandtab softtabstop=2 shiftwidth=2 foldmethod=marker

上記~/.vimrc に記載後 Vim を再起動してください。

リロード設定

今後 ~/.vimrc はバリバリ修正を加えていきます。 その為以下のように簡単に編集・適用ができるようにマッピングしておきましょう。

" <F1> で ~/.vimrc を表示 (Neovim の場合は ~/.config/nvim/init.vim)
" <F1> はデフォルトで `:help` にマッピングされているが `:help` と打てば良いため潰す
if has('nvim')
    nnoremap <silent> <F1> :<C-u>e ~/.config/nvim/init.vim<CR>
else
    nnoremap <silent> <F1> :<C-u>e ~/.vimrc<CR>
endif

" <F10> で編集中の Vim script をソース
if !exists('*s:source_script')
  " ~/.vimrc をソースすると関数実行中に関数の上書きを行うことになりエラーとなるため
  " 'function!' による強制上書きではなく if によるガードを行っている
  function s:source_script(path) abort
    let path = expand(a:path)
    if !filereadable(path)
      return
    endif
    execute 'source' fnameescape(path)
    echomsg printf(
          \ '"%s" has sourced (%s)',
          \ simplify(fnamemodify(path, ':~:.')),
          \ strftime('%c'),
          \)
  endfunction
endif
nnoremap <silent> <F10> :<C-u>call <SID>source_script('%')<CR>

上記~/.vimrc に記載後 Vim を再起動してください(マッピング系の部分に追記すると良いでしょう)。 以後は <F1> を押すと ~/.vimrc が表示され、修正したあとは <F10> を押すだけでリロードが可能です。

True Color での表示

set termguicolors を設定すれば True Color での表示が可能ですが、ターミナル自体が True Color に対応している必要があります。 また set termguicolors は起動時に行う必要があるため vimrc の先頭 に記載する必要があります。

if has('vim_starting')
  " Vim のエンコーディングを utf-8 に
  set encoding=utf-8
  scriptencoding utf-8

  " 利用可能な場合は true color を有効化する
  if !has('gui_running')
        \ && exists('&termguicolors')
        \ && $COLORTERM ==# 'truecolor'
    " tmux 等でも強制的に termguicolors を有効化するための設定 (Neovim では不要)
    " https://medium.com/@dubistkomisch/how-to-actually-get-italics-and-true-colour-to-work-in-iterm-tmux-vim-9ebe55ebc2be
    if !has('nvim')
      let &t_8f = "\e[38;2;%lu;%lu;%lum"
      let &t_8b = "\e[48;2;%lu;%lu;%lum"
    endif
    set termguicolors       " use truecolor in term
  endif
endif

なお上記で利用している $COLORTERM 環境変数は一般的に TrueColor に対応したターミナルが自動的に設定する環境変数です(True Colour (16 million colours))。 そのため $COLORTERM が存在していなくても TrueColor に対応しているターミナルがあるかもしれません。

Vim から見える PATH を正確に

Vim から見える $PATHVim を起動した環境に依存します。 そのためターミナルから起動した Vim だと使えるけど GVim からは使えない等の現象が発生します。 これを治すために、利用する $PATH は以下のように明示的に宣言してしまいましょう。

let s:is_windows = has('win32') || has('win64')
function! s:configure_path(name, pathlist) abort
  let path_separator = s:is_windows ? ';' : ':'
  let pathlist = split(expand(a:name), path_separator)
  for path in map(filter(a:pathlist, '!empty(v:val)'), 'expand(v:val)')
    if isdirectory(path) && index(pathlist, path) == -1
      call insert(pathlist, path, 0)
    endif
  endfor
  execute printf('let %s = join(pathlist, ''%s'')', a:name, path_separator)
endfunction

" 以下 macOS の自分が使っているやつ
call s:configure_path('$PATH', [
    \ '~/.cache/dein/repos/github.com/thinca/vim-themis/bin',
    \ '~/.cabal/bin',
    \ '~/.zplug/bin',
    \ '~/.anyenv/envs/pyenv/bin',
    \ '~/.anyenv/envs/plenv/bin',
    \ '~/.anyenv/envs/rbenv/bin',
    \ '~/.anyenv/envs/ndenv/bin',
    \ '~/.anyenv/envs/pyenv/shims',
    \ '~/.anyenv/envs/plenv/shims',
    \ '~/.anyenv/envs/rbenv/shims',
    \ '~/.anyenv/envs/ndenv/shims',
    \ '/usr/local/bin',
    \ '/usr/local/texlive/2017basic/bin/x86_64-darwin',
    \])
call s:configure_path('$MANPATH', [
    \ '/usr/local/share/man/',
    \ '/usr/share/man/',
    \ '/Applications/Xcode.app/Contents/Developer/usr/share/man',
    \ '/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/share/man',
    \])

上記のように関数を噛ませることで、すでに登録されているパスは重複登録しないようにできます。

利用する Python を固定する(Neovim 限定)

Python の補完に davidhalter/jedi-vimzchee/deoplete-jedi を利用している場合は以下のように Neovim が参照する Python を明治指定することで、開発に用いている Python それぞれに neovim パッケージや jedi パッケージを入れなくても良くなり便利です。

function! s:pick_executable(pathspecs) abort
  for pathspec in filter(a:pathspecs, '!empty(v:val)')
    for path in reverse(glob(pathspec, 0, 1))
      if executable(path)
        return path
      endif
    endfor
  endfor
  return ''
endfunction

if has('nvim')
  let g:python_host_prog = s:pick_executable([
        \ '/usr/local/bin/python2',
        \ '/usr/bin/python2',
        \ '/bin/python2',
        \])
  let g:python3_host_prog = s:pick_executable([
        \ '/usr/local/bin/python3',
        \ '/usr/bin/python3',
        \ '/bin/python3',
        \])
endif

上記では関数をかませることで、幾つかの候補の中から最初に見つかった Python を利用するようにしています。

クリップボード連携をデフォルトに

Vim はデフォルトではシステムのクリップボードと連携しません。 システムのクリップボードと連携するには

"*y     システムのクリップボードにコピー (Linux の場合は "+y)
"*p     システムのクリップボードから貼り付け(Linux の場合は "+p)

と打つ必要があり、若干面倒です。 以下の設定を追記することで yp がデフォルトでシステムのクリップボードと連携します。

" システムのクリップボードを利用する
" OS 種により利用するべき値が違うため分岐させている
" - unnamed     : 'selection' in X11; clipboard in Mac OS X and Windows
" - unnamedplus : 'clipboard' in X11, Mac OS X, and Windows (but yank)
if has('win32') || has('win64') || has('mac')
  set clipboard=unnamed
else
  set clipboard=unnamed,unnamedplus
endif

スペースを有効活用する

Vim のノーマルモードにおいてスペースキーは l と同じ機能を持つだけなのでとても無駄です。 親指という超強力な指をもっと有効活用するために <Leader> をスペースキーにマッピングしてしまいましょう。 またファイルタイププラグイン等で利用される <LocalLeader> には余った \ を割り当てると良さそうです。

noremap <Leader>      <Nop>
noremap <LocalLeader> <Nop>
let g:mapleader = "\<Space>"
let g:maplocalleader = '\'

上記により、以下のようにプラグインマッピングを定義できます。

" gina.vim
nnoremap <Leader>aa :<C-u>Gina status<CR>
nnoremap <Leader>ac :<C-u>Gina commit<CR>

" denite.nvim
nnoremap <Leader>df :<C-u>Denite file<CR>
nnoremap <Leader>db :<C-u>Denite buffer<CR>

" jedi.vim
function! s:configure_python() abort
  nnoremap <buffer> <LocalLeader>r :<C-u>call jedi#rename()<CR>
endfunction
autocmd MyAutoCmd FileType python call s:configure_python()

上記<Leader>g:mapleader で指定されているスペースに置き換わるので、利用時は <Space>aa<Space>df のように利用できます。 また <LocalLeader>g:maplocalleader で指定されている \ に置き換わるので、ファイルタイプが Python なバッファでは \r のように利用できます。

Quickfix/LocationList を簡単に表示・非表示する

以下の設定により Quickfix や LocationList の表示を QL でトグル出来ます。

function! s:toggle_qf() abort
  let nwin = winnr('$')
  cclose
  if nwin == winnr('$')
    botright copen
  endif
endfunction
nnoremap <silent> <Plug>(my-toggle-quickfix)
      \ :<C-u>call <SID>toggle_qf()<CR>
nmap Q <Plug>(my-toggle-quickfix)

function! s:toggle_ll() abort
  try
    let nwin = winnr('$')
    lclose
    if nwin == winnr('$')
      botright lopen
    endif
  catch /^Vim\%((\a\+)\)\=:E776/
    echohl WarningMsg
    redraw | echo 'No location list'
    echohl None
  endtry
endfunction
nnoremap <silent> <Plug>(my-toggle-locationlist)
      \ :<C-u>call <SID>toggle_ll()<CR>
nmap L <Plug>(my-toggle-locationlist)

参考: http://d.hatena.ne.jp/kuhukuhun/20090119/1232343733

一時的に現在のバッファを最大化する

以下により <C-w>z を押すことで現在のバッファを一時的に最大化します。 再度 <C-w>z を押すともとに戻ります。 多数のバッファを分割で開いている際に、一時的に現在のバッファにフォーカスしたい場合に利用できます。

function! s:toggle_window_zoom() abort
    if exists('t:zoom_winrestcmd')
        execute t:zoom_winrestcmd
        unlet t:zoom_winrestcmd
    else
        let t:zoom_winrestcmd = winrestcmd()
        resize
        vertical resize
    endif
endfunction
nnoremap <silent> <Plug>(my-zoom-window)
      \ :<C-u>call <SID>toggle_window_zoom()<CR>
nmap <C-w>z <Plug>(my-zoom-window)
nmap <C-w><C-z> <Plug>(my-zoom-window)

なお vim-scripts/ZoomWin というのがあるようです。

ファイルタイプの再アサイ

空のファイルを新規に作成したときなど、ファイルタイプの自動推定に失敗することはよくあります。 以下を記載することで、ファイルタイプの自動推定に失敗した場合でも保存時に再度ファイル推定を行うようになります。

" 一括リセットが可能なように MyAutoCmd グループで定義する
autocmd MyAutoCmd BufWritePost *
      \ if &filetype ==# '' && exists('b:ftdetect') |
      \  unlet! b:ftdetect |
      \  filetype detect |
      \ endif

なお autocmd は宣言により追加されるため vimrc をリロードした際に同じ autocmd が何個も登録されてしまいます。 これを防ぐために vimrc の最初の方で以下のようにして MyAutoCmd に属する autocmd をすべてリセットしましょう。

" 利用しているオートコマンドはすべて MyAutoCmd に所属させているためここですべて削除する。
" これは vimrc をリロードした際にオートコマンドが重複して登録されるのを防ぐために必要。
augroup MyAutoCmd
  autocmd! *
augroup END

おわりに

今回紹介していない色々な設定が lambdalisue/rook/blob/master/home/.config/nvim/init.vim に記載されているので、さらに盗みたい方は参照してください。