Vim
vim-plugins
aratanaDay 20

Vim一年生による、Vimプラグイン作成指南

ad_banner4.gif
このエントリーは、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も文字列を返してくれる
  • それぞれの判定で実行されるフックポイント的なのを用意しておく

以上の実装だけで十分そうですね。
では、作成していきましょう。

必要そうな変数宣言

先に必要そうな、変数たちを宣言しておきます。

vim-spatab/autoload/spatab.vim
" 何行まで判定に利用するか
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というのは、

  • 第一引数に、辞書を。(この場合、グローバル変数のリストを指定)
  • 第二引数に、キーを。
  • 第三引数に、デフォルト値を。
  • 返り値、見つからなかったらデフォルト値を、見つかったらその値を。

つまり、指定の変数がなかったら、デフォルト値が返ってくるが、
指定の変数があったらその値を取得する便利なやつ。

スペースとタブを判定

現在開いているファイルが、スペースかタブか判定して、
それぞれの文字列を返す関数を作ります。

vim-spatab/autoload/spatab.vim
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という風に記述します。
つまり、階層区切りってことですね。

判定した後、それぞれの処理を実行

タブだった場合の処理、スペースだった場合の処理が、
必要になる人がいるかもしれないので、作成。

vim-spatab/autoload/spatab.vim
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ディレクトリに記述していきます。

vim-spatab/plugin/spatab.vim
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などのプラグインマネージャーを利用して、利用できるようにしましょう。
まずは、ローカルに作成したプラグインディレクトリを読み込みましょう。

dein.toml
[[plugins]]
repo = '~/vim-spatab'

動作問題なければ、GithubへプッシュしてVimプラグイン公開完了です!!
vim-spatab

このQiita記事では、Vimプラグイン作成指南用に、シンプル?に書いているので、
Githubソースとは差異があります。

実際に利用してみます。

.vimrc
  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の関数を利用したりして、
ステータスバーに表示させることができるようになりました。

space.png

まとめ

これで、どのようにプラグイン作成するか分かったと思います。
docやtestのことも、書きたいです。
他にも、スペースとタブ判定時に実行される、いい感じにフックさせる方法とかないですかね?

明日(21日目)のaratana Advent Calendar 2017のエントリーは、
色々テクってる@mokamoto12さんのエントリーです!お楽しみに!