このエントリーは、aratana Adventカレンダー20日目のエントリーです。
こんにちは!
Qiita aratana organizationsの投稿タイプにVimをねじ込もうと企んでいる田村です。
前日は、@seiyaanさんの「仮想通貨取引所(bitbank.cc)の公式APIを使って仮想通貨の取引をする方法」というエントリーでした。
仮想通貨と聞いて手を出しづらいイメージがありますが、うまくいけばガッポリいけるんですかね?
今回は、簡単なVimプラグインを作成しながら、プラグイン作成の理解度を深めていきましょう回です。
この記事読めば、Vimプラグインを作成できるようになります。
作るプラグイン
Vimに、現在のソースが、スペースなのかタブなのか判定する変数が無かったような気がするので、
スペースとタブ判定のプラグイン作ろうと思います。(既に何個かあるっぽいですけど)
入門プラグイン作成としては、ちょうどよい感じでしょう。
ちなみに、、、
タブよりスペースを使う方が高年収という謎結果が出ております(参考サイト)
私自身、昔はタブ派だったのですが、最近では、スペース派になりました。
Vimプラグイン基礎
Vimプラグインを作成するにあたり、基礎知識を確認していきましょう。
詳細は、:h write-plugin
。
- プラグイン名を決める
- ファイル構造の雛形を作り実装していく
- プラグイン作成で必要なこと
- プラグイン作成の注意点まとめ
プラグイン名を決める
プラグイン名は、とても大事なので慎重に決めていきましょう。
プラグインの名は(正確にいうとプラグインのファイル名かな?)、古いWindowsで問題起きないように、
できれば8文字以内が良いらしいです。(なぜ8文字以内?は、こちら)
今の時代、8文字以内は守る必要はなさそうですね。
__シンプル__にプラグインを表すことを、重要とした方が良いと思います。
私の場合、スペースとタブを検知する動作なので、space tab
をもじって、vim-spatab
と意味分からん感じにします。(__スパタブ__してる?)
※ 既に同じプラグイン名が無いかググって決めましょう。被ると正常に読み込まれません。
ファイル構造の雛形作る
Vimプラグインを作るにあたり、ファイル構造を確認します。
pluginディレクトリ
まずは、plugin
ディレクトリを作成します。
__このディレクトリは、Vimが立ち上がった時に、読み込まれるファイル__です。
基本的にここには、マッピングやコマンド定義ぐらいしか書きません。
つまり、必要最小限のことを記述します。
autoloadディレクトリ
次に、autoload
ディレクトリを作成します。
autoload
とは、簡単に説明すると、__必要になったら読み込んでくれる環境に優しいやつ__です。
詳しくはこちらでモテる男になってください。モテる男のVim Script短期集中講座
プラグインファイル構造
上記を考慮したファイル構造は以下のとおりです。
vim-spatab
├── LICENSE
├── README.md
├── autoload
│ └── spatab.vim
└── plugin
└── spatab.vim
こちらのファイルに、処理を書いていきます。
プラグイン作成で必要なこと
プラグイン作成でこれを守ってねルールがあります。
みんな幸せになる、お決まりみたいなやつです。
必要だと思われることだけ書いていきます。
行継続がある場合
行継続(line-continuation
)がある場合、
Vi互換を保つために、cpoptions
保護しておきましょう。
つまり、行継続使ってなかったら、cpoptions
保護不要ってことですね(ですよね?)。
例:
let s:save_cpo = &cpoptions
set cpoptions&vim
nnoremap <Plug>(test)
\ :call Test<CR>
let &cpoptions = s:save_cpo
unlet s:save_cpo
普通、行継続を表す\
は、行末尾に書きますが、
なぜ、Vim scriptの行継続を表す\
が、行の先頭にあるのでしょうか。
:h line-continuous
に書いてある例だと、
:map xx asdf\
バックスラッシュもコマンドの一部として認識してしまうので、
先頭にバックスラッシュってことなんでしょう。
再度読込防止
再度設定ファイルが読み込まれた場合に、無駄な処理やエラー表示されないように、
一回のみ読込されるようにしましょう。
if exists('g:loaded_spatab')
finish
endif
let g:loaded_spatab = 1
キーマッピング
利用者になにかしらキーマッピングを提供する場合のお決まりです。
" 例
noremap <Plug>(test_da) :<C-u>call testda()
マッピングの仕方は、<Plug>TestDa
だったり、<Plug>(test-da)
だったりと、
人によって異なりますが、私は、スネークケース派です。
<C-u>
は、実行する前に数字など入力されていたときに予期せぬ動作をさせないため。
例えば、
nnoremap <Space>ec :echo 'test dayo!'<CR>
を設定したあと、1<Space>ec
とか、コマンド入力前に、数字を入れてしまうと、
E481: 範囲指定は許可されていません
と表示されてしまいます。
これを防ぐために、先程の<C-u>
を追加します。
nnoremap <Space>ec :<C-u>echo 'test dayo!'<CR>
とすることで、範囲指定部分が取り除かれ、問題なく動作します。
注意点まとめ
- プラグイン名に気をつける(被らないように。シンプルに。)
- Vi互換性のことを考える
- 再度設定ファイルが読み込まれることを想定して作る
- マッピングの際のお決まりを守る
以上を守れば、Vimプラグイン作成なんて怖くないです。
spatabプラグイン作成
これからプラグインを作成していきます。
軽く設計
このプラグインにどのような機能をもたせるか考えます。
- 数行取得して先頭がスペースかタブかで判定(簡単に行きましょう)
- スペースだったら
space
、タブならtab
も文字列を返してくれる - それぞれの判定で実行されるフックポイント的なのを用意しておく
以上の実装だけで十分そうですね。
では、作成していきましょう。
必要そうな変数宣言
先に必要そうな、変数たちを宣言しておきます。
" 何行まで判定に利用するか
let s:max_line_num = get(g:, 'spatab_max_line_num', 300)
" スペース判定の場合に返す文字列
let s:space_name = get(g:, 'spatab_space_name', 'space')
" タブ判定の場合に返す文字列
let s:tab_name = get(g:, 'spatab_tab_name', 'tab')
" スペース判定の場合に実行される関数名
let s:space_func_name = get(g:, 'spatab_space_func_name', '')
" タブ判定の場合に実行される関数名
let s:tab_func_name = get(g:, 'spatab_tab_func_name', '')
" 判定時に、自動的にexpandtabを切り替えるかのフラグ
let s:auto_expandtab = get(g:, 'spatab_auto_expandtab', 1)
get関数
get
というのは、
- 第一引数に、辞書を。(この場合、グローバル変数のリストを指定)
- 第二引数に、キーを。
- 第三引数に、デフォルト値を。
- 返り値、見つからなかったらデフォルト値を、見つかったらその値を。
つまり、指定の変数がなかったら、デフォルト値が返ってくるが、
指定の変数があったらその値を取得する便利なやつ。
スペースとタブを判定
現在開いているファイルが、スペースかタブか判定して、
それぞれの文字列を返す関数を作ります。
function! spatab#GetDetectName() abort
let detect_name = get(b:, 'spatab_detect_name', '')
" 既にチェック済みか確認。済みなら飛ばす。
if detect_name ==# ''
" 現在ファイルの指定行数分の文字列を配列として取得
let buflines = getbufline(bufname('%'), 1, s:max_line_num)
" 各行の先頭がタブかどうか調べ、個数を調べる
let len_tab = len( filter(copy(buflines), "v:val =~# '^\\t'") )
" 各行の先頭がスペースかどうか調べ、個数を調べる
let len_space = len( filter(copy(buflines), "v:val =~# '^ '") )
" スペース数とタブ数を比較して、適切な文字列代入
if len_space > len_tab
" space
let detect_name = s:space_name
elseif len_space < len_tab
" tab
let detect_name = s:tab_name
endif
" 結果をバッファ変数に保持し、チャック済みとする
let b:spatab_detect_name = detect_name
endif
" 結果を返す
return detect_name
endfunction
#
の意味
#
区切りは、なんやってなりますが、
例えば、autoload/spatab/get.vim
に記述する場合は、
spatab#get#GetDetectName
という風に記述します。
つまり、階層区切りってことですね。
判定した後、それぞれの処理を実行
タブだった場合の処理、スペースだった場合の処理が、
必要になる人がいるかもしれないので、作成。
function! spatab#Execute() abort
let res = spatab#GetDetectName()
if res ==# s:space_name
" スペース判定の場合
if s:auto_expandtab | setlocal expandtab | endif
if s:space_func_name !=# '' | call {s:space_func_name}() | endif
elseif res ==# s:tab_name
" タブ判定の場合
if s:auto_expandtab | setlocal noexpandtab | endif
if s:tab_func_name !=# '' | call {s:tab_func_name}() | endif
endif
endfunction
pluginの設定
plugin
ディレクトリに記述していきます。
if exists('g:loaded_spatab')
finish
endif
let g:loaded_spatab = 1
command! -nargs=0 STDetect call spatab#Execute()
command! -nargs=0 STEcho echo spatab#GetDetectName()
noremap <unique> <Plug>(spatab_echo_detect_name) :<C-u>echo spatab#GetDetectName()<CR>
利用しましょ!
dein.vim
などのプラグインマネージャーを利用して、利用できるようにしましょう。
まずは、ローカルに作成したプラグインディレクトリを読み込みましょう。
[[plugins]]
repo = '~/vim-spatab'
動作問題なければ、GithubへプッシュしてVimプラグイン公開完了です!!
vim-spatab
このQiita記事では、Vimプラグイン作成指南用に、シンプル?に書いているので、
Githubソースとは差異があります。
実際に利用してみます。
augroup spatab
autocmd!
autocmd BufWinEnter * STDetect
augroup END
let g:spatab_space_func_name = 'SpaceMode'
function! SpaceMode()
echomsg 'space mode!!'
endfunction
ファイルがウィンドウに表示された後、STDetect
が発動して、スペースだった場合、
space mode!!
と表示されます。
他には、lightline
などで、spatab#GetDetectName
の関数を利用したりして、
ステータスバーに表示させることができるようになりました。
まとめ
これで、どのようにプラグイン作成するか分かったと思います。
docやtestのことも、書きたいです。
他にも、スペースとタブ判定時に実行される、いい感じにフックさせる方法とかないですかね?
明日(21日目)のaratana Advent Calendar 2017のエントリーは、
色々テクってる@mokamoto12さんのエントリーです!お楽しみに!