この記事は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の面白いところですね.
参考文献