承前 (重厚長大の時代)
1980年代の unix の設定ファイルってのは、長いのが恰好良かった。
if 文をがしがし使って、他人より早く取り入れたアプリの設定をぼこぼこ放り込んで、ひとより早く取り入れたアプリの設定が含まれた1ファイルのサンプルが誇らしげに公開され、それをお手本にしていたものだ。
BASIC で書かれたゲームの再現コードみたいな長い設定ファイルを良いと受け止め使いながら、「 Unix は C 言語で書かれた OS だから良い、関数があって階層化できる言語っていいよね」 「ひとつの関数は35行(一画面)まで」と言いあっていたのだから妙な話だよね。
……と、数年前。昔からずっと手直しして使っている .Xresources というファイルに #incldue 命令があるのに気づいて、環境ごとに読み込む設定を判断本体だけ残し、がりがり分離しながら考えていた。
$HOME/.vimrc から $HOME/.vim/vimrc へ
unix ターミナルの vim が $HOME/.vim/vimrc を読み込むようになり、Windows 環境の $HOME/vimfiles/vimrc とだいたい環境が揃えられるようになった。
たとえば作業している環境を汚さないように、しかしミスを犯したときの復旧が容易なように バックアップ, swapfile, undofile をホームディレクトリ以下に保存する設定をしていたとする。
設定ファイルをよそにもっていくたび、一々自分でディレクトリを掘るのが面倒で、「なかったら作る」処理を .vimrc に組み込んだとする。
unix で .vimrc , Windows で vimfiles/vimrc を使っているとこうなる
set backup
set backupext=.bak
" 作業ディレクトリを掘るために、.vimrc の本体があるディレクトリを取得
" 真面目にコード書いたらこんなんなった。決め打ちのほうがきれい
" substitute( resolve( expand( "<sfile>:p" ) ) , "[^\/]*$" , "" , "" )
if has( 'unix' )
let s:vimDir = expand( $HOME ) . "/.vim"
else
let s:vimDir = expand( "<sfile>:p:h" )
endif
let s:dirs = { "backup": "backupdir" , "swap": "directory" ,
\ "undo": "undodir" }
for item in keys( s:dirs )
let s:dir = s:vimDir . "/workfile/" . item
if ! filewritable( s:dir ) || ! isdirectory( s:dir )
call mkdir( s:dir , "p")
endif
exec( expand( "set " . s:dirs[item] . "=" . s:dir ) )
endfor
" set patchmode=.orig " 大事なファイルを編集する時には、こちらを使おう
これで workfile は ヴァージョン管理の設定で ignore 指定 (保存しない)指定をしておけば、どこにでも持っていける。
でも unix でも $HOME/.vim/vimrc の名前にすれば、上の(コメント込み)8行は要らない。条件なしで
let s:vimDir = expand( "<sfile>:p:h" )
と書けば終わる。
さて、ではほかの設定もすべて分割してしまえば本体は if 文だけにならないだろうか。
分割への道
最初は include_if_exist
function! s:load_if_exist(filename) abort
if filereadable(a:filename)
exec("source " . a:filename)
return v:true
endif
if g:setting['debug']
echo(a:filename . " is not exist!" )
endif
return v:false
endfunction
次にディレクトリで読み込む
たとえばWindows 独自の設定をするファイルなら
if !has('win32') | finish | endif
と先頭に書いておけば、vimrc ではなにも考えずに「サブディレクトリにあるすべての *.vim を読み込むと指定できないだろうか。
するとファイルを読み込む順序だけ指定すれば、とまずは Linux 風に番号をファイル名の先頭につけてみた。
失敗である。glob() 関数が返すファイル名のリストに順序が保証されているとは書かれていない(実装はみていない)ので、自分でリストをソートしていたら体感するほど遅い。
そこで vim の設定を機能ごとに分類して、どの機能からさきに設定したら良いかと考えてみた。手を付ける前に考えたのが以下だ。
この順序で読んでみよう (試案)
0. &rtp の設定
編集している vimrc のテストを -u オプションで指定したとき、plugin,syntax などは vimrc の位置に関係なくデフォルトから読み込んでしまうので簡単にハマる。
&rtp の先頭項目と :p:h の置換を &rtp 全体にかけておけば、 作業ディレクトリの vimrc で編集してテストできるから楽。
1. LANGUAGE を指定
charset が決まる。
ふだんは 内部処理 UTF-8 が良いが、環境変数 LANG がないような緊急事態には iconv を無効にしないといけない
(日本語コメントを含んだ設定ファイルを触ってなんど文字化けさせたことか)
2. FONT の設定
charset が決まれば FONT も指定できる。 (だろうという予測。後述)
3. プラグイン用の環境設定
グローバル変数を参照して挙動を決めるプラグインを読み込む前に、設定をしておきたい。好みというか心配性というか、そんなの。
4.プラグインマネージャの呼び出し
この作業をやった時点で、まだ何を採用するか決めていないが。なにかプラグインマネージャを入れて外のディレクトリから読み込むのは決めていた。
5. 公式配布の設定読み込み
example.vim を読み込むか default.vim だけ読み込むか。
6.個人設定
- GUI のときの枠組み
- 端末のとき二色表示なら syntax off
- 環境設定(上記のswap,backup先)
- vim だけにあって vi にはないエディタの設定
- .exrc の設定を最後に明示的に再読み込み
if has('gui_running') なんて要らなかったんや!
端末でテストした .vim を Windows 環境の vimfiles にもってきて、デバッグモードで動かしてみて気づいた。
「なんか二回読み込もうとしているな」
もともと、.gvimrc と .vimrc は同一で長大なファイルだったのだが、Windows で要求するファイル名が違って読み込めないため、$HOME/_gvimrc のなかで
source <sfile>:p:h/.vimrc
としていた。そのまま名前を $HOME/vimfiles/vimrc と $HOME/vimfiles/gvimrc となるようにしたのが二回読み込む理由。
デバッグモードで良く調べ、またヘルプを読めば gvim で起動したときにまず vimrc を読み、次いで gvimrc を読むことになっている。
なので has('gui_running') の条件を付けられた内容をすべて gvimrc にもってくれば、条件判定はなくなるのであった。
if v:version < 800
echohl ErrorMsg
echomsg 'You need Vim 8.0 ( need read ~/.vim/vimrc on unix tty and v:true,v:false)'
echohl None
finish
endif
let s:debug = v:false " if development, v:true.
"let s:debug = v:true
let s:self = expand("<sfile>:p")
let s:vimrc_dir = expand("<sfile>:p:h")
if ! exists( "s:loader" )
exec( "source " . s:vimrc_dir . "/rc/00.init/loader.vim")
let s:loader = Loader(&rtp, s:vimrc_dir, s:debug)
if s:debug
echo(s:loader)
endif
endif
if s:loader.start(s:self) && (! s:debug) " if already file loaded and not debug
finish
endif
call s:loader.rtpHere() " or you can use s:loader.rtpRestore()
if s:debug
:echo(&rtp)
endif
call s:loader.load(s:vimrc_dir . "/rc/01.lang/*.vim") " language is first
" 02. is moved to gvimrc
call s:loader.load(s:vimrc_dir . "/rc/03.conf/*.conf") " plugin's conf
call s:loader.load(s:vimrc_dir . "/rc/04.manager/*.vim") " plugin
call s:loader.load(s:vimrc_dir . "/rc/05.official/*.vim") " $VIMDIR
call s:loader.load(s:vimrc_dir . "/rc/06.personal/*.vim") " Mine
""" Garbage collection
""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
call s:loader.gc() "By reason of duplicate load stopper, cant destroy() but gc()
call s:loader.finish(s:self) "this file load finish
if s:debug
echo(s:loader)
endif
" call s:loader.load({"name": s:vimrc_dir . "/rc/10.gui_instance/*.vim"})
finish
" modeline:
" vim:ts=4:sw=4:et:tw=78:fenc=utf-8:ff=unix:ft=vim
" gvim auto load first vimrc, and load this file.
source $VIMRUNTIME/gvimrc_example.vim
if has('win32')
source <sfile>:p:h/rc/10.gui_instance/mswin.vim
elseif has( 'unix' )
source <sfile>:p:h/rc/10.gui_instance/unix_gvim.vim
endif
source <sfile>:p:h/rc/10.gui_instance/redraw_menu.vim
" vim:ts=4:sw=4:et:fileencoding=utf-8:ff=unix
余談
ところで、ファイルの二重読み込みを防ぐために g:filename_loaded を設定しておいて if exists( "g:filename_loaded") とするのは何故だろうね。
if exists( "s:read") | let s:read = 1 | endif
でも良いような気がするのだが、グローバル変数よりファイルスコープ変数のほうがよりメモリを喰うとか、そんな実装上の都合があるのだろうか。