この記事は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
というコマンドを置き換えます.
エイリアスの本体は,引数をダブルクオートしてカンマ区切りにするシェル関数になっています.
これらのエイリアスやシェル関数の定義は autocmd
の TerminalOpen
のタイミングで term_sendkeys()
を利用することで行います.
term_sendkeys()
を利用するので,起動時にエコーバックがあることや,cmd.exeに非対応であるのが難点ですが,徐々に改善していきたいと思います(最近考えた設定なので).
使い方は簡単で, :terminal
でbashもしくは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の面白いところですね.