koturnの日記

普通の人です.ブログ上のコードはコピペ自由です.

IQ1の人間が書いているVimの設定の一部

この記事はIQ1 Advent Calendar 2018の10日目の記事です.

特に書くことを決めてなかったのと,最近Vimconf2018やOsaka.vim等でVimに対するモチベーションが高まっているのでVimのことを書きたいと思います. Vimのモチベーションが高まっていなかったら,艦隊これくしょんの装備や編成について書いていたかもしれないです.

何を書くか

既に自分の .vimrc に書いてある設定のうちのいくつかを紹介していきたいと思います.

存在しないディレクトリのファイルを保存するときにディレクトリを作成する

例えば,hoge というディレクトリが存在しないときに,

$ vim hoge/fuga.txt

でファイル編集を開始したとき,ファイルを保存しようとしても hoge が存在しないため,ファイル保存ができません.

そこで以下のような autocmd を定義しておくことで,ファイル保存時にディレクトリを作成するかどうかを尋ねるようになります.

function! s:auto_mkdir(dir, force) abort " {{{
  if !isdirectory(a:dir) && (a:force || input(printf('"%s" does not exist. Create? [y/N]', a:dir)) =~? '^y\%[es]$')
    call mkdir(iconv(a:dir, &enc, &tenc), 'p')
  endif
endfunction " }}}
autocmd BufWritePre * call s:auto_mkdir(expand('<afile>:p:h'), v:cmdbang)

:w ではなく,:w! で保存した場合は,ディレクトリ作成するかどうかを尋ねることなく,ディレクトリを作成します.

新規作成ファイルにshebangがある場合,保存時に実行可能属性を与える

シェルスクリプトなどでファイルにshebangを記す場合,自動的に実行可能属性を付与する設定です.

if s:executable('chmod')
  function! s:add_permission_x() abort " {{{
    autocmd! Permission BufWritePost <buffer>
    if !stridx(getline(1), '#!')
      silent system('chmod u+x ' . shellescape(expand('%')))
    endif
  endfunction " }}}
  augroup Permission " {{{
    autocmd!
    autocmd BufNewFile * autocmd Permission BufWritePost <buffer>  call s:add_permission_x()
  augroup END " }}}
endif

Vim8のターミナル機能内でVimを起動したとき,親のVimでファイルを開くようにする

Vimのアドベントカレンダー(その2)の4日目の記事:terminal に関する小さい Tips - Qiitaにて,Vim8の :terminal にはTapiという機能があることを知りました.

タイトルにもあるやりたいこと=ターミナルから親のVimでファイルを開くことは既に上記の記事で行われているのですが,ファイルセレクタではなくデフォルトで存在しているであろうコマンドのみで,かつ渡した引数のファイルのみを開くようにしたいと考え,以下のような autocmd を定義してみました.

function! Tapi_Drop(bufnum, arglist) abort " {{{
  let [pwd, argv] = [a:arglist[0] . '/', a:arglist[1 :]]
  for arg in map(argv, 'pwd . v:val')
    execute 'drop ' . fnameescape(arg)
  endfor
endfunction " }}}

autocmd TerminalOpen *bash*,*zsh* call term_sendkeys(bufnr('%'), join([
      \ 'function vimterm_quote_args() { for a in "$@"; do echo ", \"$a\""; done; }',
      \ 'function vimterm_drop() { echo -e "\e]51;[\"call\", \"Tapi_Drop\", [\"$PWD\" `vimterm_quote_args "$@"`]]\x07"; }',
      \ 'alias vim=vimterm_drop'
      \], "\n") . "\n")

上記を .vimrc に記述した上でターミナルを起動すると,与えた引数をTapiのcallによってVim側に定義した関数に丸ごと引き渡すエイリアスを行い, vim というコマンドを置き換えます. エイリアスの本体は,引数をダブルクオートしてカンマ区切りにするシェル関数になっています. これらのエイリアスやシェル関数の定義は autocmdTerminalOpen のタイミングで term_sendkeys() を利用することで行います.

term_sendkeys() を利用するので,起動時にエコーバックがあることや,cmd.exeに非対応であるのが難点ですが,徐々に改善していきたいと思います(最近考えた設定なので).

使い方は簡単で, :terminalbashもしくはzshを起動し,そのシェル上で

$ vim hoge.txt

とするだけです. これで,親のVimでファイルを開くことができます.

他のディレクトリに移動したり,hoge/fuga.txt のようなファイルパスだったり,ho ge/fuga/txtのようなスペースを含むファイルパスでも問題はないようにしています(多分).

行末スペースを削除する

行末スペースを削除するコマンドです. jumplist や 検索履歴を汚すことなく,またカーソル位置を移動することなく置換するようにしてあります.

function! s:delete_match_pattern(pattern, line1, line2) abort " {{{
  let cursor = getcurpos()
  execute 'silent keepjumps keeppatterns' a:line1 ',' a:line2 's/' . a:pattern . '//ge'
  call setpos('.', cursor)
endfunction " }}}
command! -bar -range=% DeleteTrailingWhitespace  call s:delete_match_pattern('\s\+$', <line1>, <line2>)

句読点をカンマ・ピリオドに置換する

句読点をカンマ・ピリオドに置換するコマンドです. ビジュアルモードで選択した行のみを置換することも可能になっています. 卒論,修論の執筆に必要になることもあるでしょう.

function! s:comma_period(line1, line2) abort range " {{{
  let cursor = getcurpos()
  execute 'silent keepjumps keeppatterns' a:line1 ',' a:line2 's/、/,/ge'
  execute 'silent keepjumps keeppatterns' a:line1 ',' a:line2 's/。/./ge'
  call setpos('.', cursor)
endfunction " }}}
command! -bar -range=% CommaPeriod  call s:comma_period(<line1>, <line2>)

カンマ・ピリオドを句読点に置換する

反対にカンマ・ピリオドを句読点に置換するコマンドです. 入力でデフォルトでカンマ・ピリオドが入力されるように設定している場合,このコマンドが必要になることがあります.

function! s:kutouten(line1, line2) abort range " {{{
  let cursor = getcurpos()
  execute 'silent keepjumps keeppatterns' a:line1 ',' a:line2 's/,/、/ge'
  execute 'silent keepjumps keeppatterns' a:line1 ',' a:line2 's/./。/ge'
  call setpos('.', cursor)
endfunction " }}}
command! -bar -range=% Kutouten  call s:kutouten(<line1>, <line2>)

行頭のみの retab コマンド

Vimには retab コマンドというコマンドがあります. これは,スペースとタブが混在している場合, expandtab の設定状況に応じて,スペースやタブに統一するコマンドです. ただし,行頭以外にも適用されてしまうため,それがあまり好ましくないと思える場面もあります.

そこで,行頭のみに適用できるように retab コマンドをエミュレーションするコマンドを定義しています.

function! s:retab_head(has_bang, width, line1, line2) abort " {{{
  if &l:tabstop != a:width
    let &l:tabstop = a:width
  endif
  let spaces = repeat(' ', a:width)
  let cursor = getcurpos()
  if &expandtab
    execute 'silent keepjumps keeppatterns' a:line1 ',' a:line2 . (a:has_bang ?
          \ 's/^\s\+/\=substitute(substitute(submatch(0), spaces, "\t", "g"), "\t", spaces, "g")/ge' :
          \ 's/^\(\s*\t\+ \+\|\s\+\t\+ *\)\ze[^ ]/\=substitute(submatch(0), "\t", spaces, "g")/ge')
  else
    execute 'silent keepjumps keeppatterns' a:line1 ',' a:line2 . (a:has_bang ?
          \ 's/^\s\+/\=substitute(substitute(submatch(0), "\t", spaces, "g"), spaces, "\t", "g")/ge' :
          \ 's#^\(\s*\t\+ \+\|\s\+\t\+ *\)\ze[^ ]#\=repeat("\t", len(substitute(submatch(0), "\t", spaces, "g")) / a:width)#ge')
  endif
  call setpos('.', cursor)
endfunction " }}}
command! -bar -bang -range=% -nargs=?  RetabHead  call s:retab_head(<bang>0, add([<f-args>], &tabstop)[0], <line1>, <line2>)

range 指定もしてあるので,ビジュアルモードで選択した範囲のみにコマンドを適用することもできます.

インデントをスペースかタブか切り替える

これも前述のものと似たものですが,インデントにスペースを用いるかタブを用いるか切り替えます. 切り替えにあたって, 'expandtab' オプションの設定状況もトグルするようになっています.

単純なスペース・タブ置換とは異なり,行頭以外には適用されないためベンリです(多分).

function! s:toggle_tab_space(has_bang, width, line1, line2) abort " {{{
  let [&l:shiftwidth, &l:tabstop, &l:softtabstop] = [a:width, a:width, a:width]
  let [spaces, cursor] = [repeat(' ', a:width), getcurpos()]
  if &expandtab
    setlocal noexpandtab
    execute 'silent keepjumps keeppatterns' a:line1 ',' a:line2 . (a:has_bang ?
          \ 's/^\s\+/\=substitute(substitute(submatch(0), "\t", spaces, "g"), spaces, "\t", "g")/ge' :
          \ 's#^ \+#\=repeat("\t", len(submatch(0)) / a:width) . repeat(" ", len(submatch(0)) % a:width)#ge')
  else
    setlocal expandtab
    execute 'silent keepjumps keeppatterns' a:line1 ',' a:line2 . (a:has_bang ?
          \ 's/^\s\+/\=substitute(submatch(0), "\t", spaces, "g")/ge' :
          \ 's/^\t\+/\=repeat(" ", len(submatch(0)) * a:width)/ge')
  endif
  call setpos('.', cursor)
endfunction " }}}
command! -bar -bang -range=% ToggleTabSpace  call s:toggle_tab_space(<bang>0, &l:tabstop, <line1>, <line2>)

これもビジュアルモード指定している範囲のみに適用することができます.

手動補完のヒント表示

これは以前 VimのCtrl-X補完を使えるようになりたい - koturnの日記にも書いた内容です.

Vimにはプラグインに頼らなくても補完する機能があるのですが, <C-x> から始まる補完は12種類あるので覚えるのが大変です(特にIQ1には). なので,<C-x> を押下した時点でヒントを echo で表示するようにしてみました.

let s:compl_key_dict = {
      \ char2nr("\<C-l>"): "\<C-x>\<C-l>",
      \ char2nr("\<C-n>"): "\<C-x>\<C-n>",
      \ char2nr("\<C-p>"): "\<C-x>\<C-p>",
      \ char2nr("\<C-k>"): "\<C-x>\<C-k>",
      \ char2nr("\<C-t>"): "\<C-x>\<C-t>",
      \ char2nr("\<C-i>"): "\<C-x>\<C-i>",
      \ char2nr("\<C-]>"): "\<C-x>\<C-]>",
      \ char2nr("\<C-f>"): "\<C-x>\<C-f>",
      \ char2nr("\<C-d>"): "\<C-x>\<C-d>",
      \ char2nr("\<C-v>"): "\<C-x>\<C-v>",
      \ char2nr("\<C-u>"): "\<C-x>\<C-u>",
      \ char2nr("\<C-o>"): "\<C-x>\<C-o>",
      \ char2nr('s'): "\<C-x>s",
      \ char2nr("\<C-s>"): "\<C-x>s"
      \}
let s:hint_i_ctrl_x_msg = join([
      \ '<C-l>: While lines',
      \ '<C-n>: keywords in the current file',
      \ "<C-k>: keywords in 'dictionary'",
      \ "<C-t>: keywords in 'thesaurus'",
      \ '<C-i>: keywords in the current and included files',
      \ '<C-]>: tags',
      \ '<C-f>: file names',
      \ '<C-d>: definitions or macros',
      \ '<C-v>: Vim command-line',
      \ "<C-u>: User defined completion ('completefunc')",
      \ "<C-o>: omni completion ('omnifunc')",
      \ "s: Spelling suggestions ('spell')"
      \], "\n")
function! s:hint_i_ctrl_x() abort " {{{
  let more_old = &more
  set nomore
  echo s:hint_i_ctrl_x_msg
  let &more = more_old
  let c = getchar()
  return get(s:compl_key_dict, c, nr2char(c))
endfunction " }}}

inoremap <expr> <C-x>  <SID>hint_i_ctrl_x()

レジスタのヒント表示

これも VimのCtrl-X補完を使えるようになりたい - koturnの日記のおまけに書いた内容です.

IQ1にとっては,Vimレジスタに何が入っているかを覚えるのは困難です. そこで,レジスタを参照するキーを押下したときにヒント表示するようにしてみました.

function! s:hint_cmd_output(prefix, cmd) abort " {{{
  redir => str
    execute a:cmd
  redir END
  let more_old = &more
  set nomore
  echo str
  let &more = more_old
  return a:prefix . nr2char(getchar())
endfunction " }}}
nnoremap <expr> m  <SID>hint_cmd_output('m', 'marks')
nnoremap <expr> `  <SID>hint_cmd_output('`', 'marks') . 'zz'
nnoremap <expr> '  <SID>hint_cmd_output("'", 'marks') . 'zz'
nnoremap <expr> "  <SID>hint_cmd_output('"', 'registers')
if exists('*reg_recording')
  nnoremap <expr> q  reg_recording() ==# '' ? <SID>hint_cmd_output('q', 'registers') : 'q'
else
  nnoremap <expr> q  <SID>hint_cmd_output('q', 'registers')
endif
nnoremap <expr> @  <SID>hint_cmd_output('@', 'registers')

カーソル操作停止時のみカーソル位置をハイライトする

Vimには現在行をハイライトする 'cursorline' というオプションと現在列をハイライトする 'cursorcolumn' というオプションがあるのですが,これをカーソルの動きをとめた場合のみ有効にする設定です.

command! -bar ToggleCursorHighlight
      \   if !&cursorline || !&cursorcolumn || &colorcolumn ==# ''
      \ |   set   cursorline   cursorcolumn
      \ | else
      \ |   set nocursorline nocursorcolumn
      \ | endif
autocmd CursorHold,CursorHoldI,WinEnter *  set cursorline cursorcolumn
autocmd CursorMoved,CursorMovedI,WinLeave *  set nocursorline nocursorcolumn

最後に

この記事では僕が .vimrc に記述している設定を紹介しました. 割とありきたりなコマンドも紹介しましたが,カーソル位置をそのままにしておくことや,ビジュアルモードで選択した範囲のみに適用可能にしたり,検索履歴等を汚さないようにしているようにこだわってもいます. Vim力が上がるにつれて,少しずつ設定の挙動を改善できるようになるのもVimの面白いところですね.

参考文献