テーマ
Vim scriptを使って簡易なプラグインを作ってみよう
前提知識
- ターミナルの操作方法
必要環境
- 以下のバージョンのVimとNeovimを用意
全体の流れ
- Vim scriptの基礎
- セッション管理のプラグインを作ってみよう
Vim scriptの基礎
- Vim scriptはVim上で実行できるスクリプト言語
- Exコマンド(
:
で始まるコマンド)の集合体 - vimrcに記述しているのもVim script
- Vimのプラグインの多くはVim scriptで書かれている
Vim scriptの実行
以下の手順通りに実施してみてください。コマンドラインにgorilla
が表示されれば成功です。
sample.vim
を作成
$ mkdir sample
$ cd sample
$ vim sample.vim
- Vimで以下のコードを記述して保存
echo 'gorilla'
:source
でVim scriptを実行
:source sample.vim
コメント
Vim scriptでは"
がコメント行として解釈され処理をスキップします。
" この行は処理されない
" echo 'gorilla'
データ型
主に以下のデータを使用できます。
データ型 | 例 |
---|---|
数値 | 5 |
小数 | 5.5 |
文字列 | 'gorilla'、"gorilla" |
リスト | [1, 2, 3] |
辞書 | {'name': 'gorilla'} |
文字列
"
と'
で囲ったものは文字列になります。
"
はタブを表す\t
といった特殊な文字をタブとして出力しますが、'
は囲った文字列をそのまま出力するといった違いがあります。
sample.vim
の先程まで記述したコードを削除して、以下のコードを記述して実行してみてください。
echo 'hello\tgorilla'
echo "hello\tgorilla"
結果は以下になります。
hello\tgorilla
hello gorilla
変数
let
を使っての宣言と値を代入する- 宣言済みの変数でも値を代入するときは
let
を使用しなければいけない
let name = 'gorilla'
" letがないのでエラーになる
name = 'cat'
変数名
- アルファベット、数字、アンダースコアを使用できる
- 数字で始まることはできない
" OK
let _a1 = 1
echo _a1
" NG
let 1a = 1
let a-b = 1
スコープ
- 変数や後述する関数にはスコープがある
- 接頭子によってスコープが変わる
- 関数内で
l:
を省略した場合は暗黙的にローカル変数にアクセスする
接頭子 | スコープ |
---|---|
g: |
グローバルスコープ、どこからも利用可能 |
s: |
スクリプトスコープ、スクリプトファイル内のみ使用可能 |
l: |
ローカルスコープ、関数内のみ使用可能 |
a: |
関数の引数、関数内のみ使用可能 |
v: |
グローバルスコープ、Vimが予め定義している変数 |
辞書
{}
で囲う- 1つ要素は
{key}: {value}
からなる {key}
は文字列でなければいけない- 要素は
,
で区切られる
let animal = {'name': 'gorilla', 'age': 27}
" 結果 => {'age': '27', 'name': 'gorilla'}
echo animal
辞書の要素取得
{dict}.{key}
{dict}[{key}]
get({dict}, {key}, {default})
let animal = {'name': 'gorilla', 'age': 27}
" 結果 => gorilla
echo animal.name
" 結果 => 27
echo animal['age']
" 結果 => banana
echo get(animal, 'name', 'banana')
辞書の要素追加
{dict}.{key} = {expr}
{dict}[{key}] = {expr}
let animal = {}
let animal.name = 'gorilla'
let animal['age'] = 27
" 結果 => {'age': 27, 'name': 'gorilla'}
echo animal
辞書の要素削除
- remove({dict}, {key})
call remove(animal, 'age')
" 結果 => {'name': 'gorilla'}
echo animal
リスト
[]
の中にカンマで区切って複数の要素を保持できるリストを作れる
let list = ['cat', 10, {'name': 'gorilla'}]
" 結果 => ['cat', 10, {'name': 'gorilla'}]
echo list
リストの要素取得
{list}[{idx}]
get({list}, {idx}, {default})
let list = ['cat', 10, {'name': 'gorilla'}]
" 結果 => cat
echo list[0]
" 結果 => 10
echo get(list, 1, 'NONE')
リストの結合
join({list}, {sep})
で{list}
を{sep}
で結合して1つの文字列を返す
let list = ['hello', 'my', 'name', 'is', 'gorilla']
" 結果 => hello my name is gorilla
echo join(list, ' ')
if文
- if文の基本形は
if {expr} | endif
{expr}
が1の場合はtrue、0の場合はfalse
if {expr}
" do something
elseif {expr}
" do something
else
" do something
endif
比較演算子
- Vim scriptで主な比較演算子は次の通り
ignorecase
の設定次第で動きが変わる演算子がある- 基本的に
#
がつく大文字小文字考慮の比較演算子を使うと良い
ignorecase 次第 |
大小文字考慮 | 大小文字無視 | 意味 |
---|---|---|---|
== |
==# |
==? |
等しい |
!= |
!=# |
!=? |
等しくない |
> |
># |
>? |
より大きい |
>= |
>=# |
>=? |
より大きいか等しい |
< |
<# |
<? |
より小さい |
<= |
<=# |
<=? |
より小さいか等しい |
is |
is# |
is? |
同一のインスタンス |
isnot |
isnot# |
isnot? |
異なるのインスタンス |
バッファについて
- メモリ上にロードされたファイルのこと
- バッファには名前と番号があり、名前はファイル名で、番号は作成された順で割り当てられる
- バッファは
:bwipeout
で明示的に削除するかVimを終了しなければメモリに残る
バッファの存在チェック
bufexists({expr})
で{expr}
のバッファがあるかを確認できる{expr}
が数値の場合はバッファ番号、文字列の場合はバッファ名とみなされる
バッファのタイプ
set buftype={type}
でバッファのタイプを設定できる- 一時的に使うバッファは
nofile
というタイプするのが一般的 - 詳細は
:h buftype
を参照
バッファのテキストを取得
- カレントバッファからテキストを取得するには
getline({lnum}, {end})
を使用する{end}
を指定しない場合は{lnum}
で指定した行だけを取得する
" 結果 => 1行目のテキストが出力される
echo getline(1)
" 結果 => 1~3行目のテキストがリストで取得できる
echo getline(1, 3)
バッファにテキストを挿入
- カレントバッファにテキストを挿入するには
setline({lnum}, {text})
を使用する {text}
はリストの場合は、{lnum}
行目とそれ以降の行に要素が挿入される
" 結果 => 1行目に my name is gorilla が挿入される
call setline(1, 'my name is gorilla')
" 結果 => 1行目がmy、2行目がnameが挿入される
call setline(1, ['my', 'name'])
ウィンドウについて
- ウィンドウはバッファを表示するための領域
- ウィンドウにはIDが割り当てられます。
- 複数のウィンドウで複数のバッファを表示できます。
:q
といったコマンドではウィンドウを閉じるだけなのでバッファは残る
ウィンドウIDを取得
winnr()
で現在のウィンドウIDを取得できる- 引数を受け取ることもできるので詳細は
:h winnr()
を参照
ウィンドウに移動
win_gotoid({expr})
で{expr}
のIDのウィンドウに移動
バッファが表示されているウィンドウのIDを取得
bufwinid({expr})
で{expr}
のバッファが表示されているウィンドウのIDを取得
関数
- 関数は
function
とendfunction
で囲い、処理はその間に記述
function! Echo(msg) abort
echo a:msg
endfunction
関数の存在チェック
exists({expr})
で{expr}
の関数があるかをチェックできる- 関数をチェックするとき関数名の前に
*
をつける
if exists('*readdir')
" do something
else
!
とabort
!
は同名の関数がある場合は上書きするabort
は関数内でエラーが発生した場合、そこで処理を終了する- Vim scriptはデフォルトでエラーがあっても処理が継続されるため基本的に
abort
をつける
引数
- 引数を使用するときは
a:
スコープ接頭子を付ける必要がある
戻り値
return {expr}
で{expr}
の評価結果を返すことができる
" 結果 => gorillaが返る
function! MyName() abort
return 'gorilla'
endfunction
Exコマンド実行
execute {expr} ..
で{expr}
の評価結果の文字列をExコマンドとして実行できる- 複数の引数がある場合、それらはスペースで結合される
" 結果 => godzilla
execute 'echo' '"godzilla"'
" 結果 => gorilla godzilla
execute 'echo' '"gorilla"' '"godzilla"'
外部コマンド実行
system({expr}, {input})
で{expr}
の評価結果の文字列を外部コマンドとして実行できる{input}
は省略可能で指定した場合はその文字列をそのままコマンドの標準入力として渡される
" 結果 => my name is gorilla
echo system('echo "my name is gorilla"')
" 結果 => my name is gorilla
echo system('cat', 'my name is gorilla')
Lambda
{ args -> expr }
という形でLambdaを書くことができる
let F = {a, b -> a - b}
" 結果 => [1, 2, 3, 4, 7]
echo sort([3, 7, 2, 1, 4], F)
セッション管理のプラグインを作ってみよう
今回作成するプラグインはVimのセッション機能を少し便利にするプラグインで、仕様は以下になります。
let g:session_path = {path}
でセッション保存先を設定できる(必須オプション):SessionCreate {name}
で{name}
の名前でセッションファイルを保存できる:SessionList
でセッション一覧をバッファに表示し、Enter
を押下するとカーソル上にあるセッションをロードできる
ディレクトリ構成
プラグインの基本的なディレクトリ構成は次のようになります。
*.vim
はスクリプトファイルと呼びます。
session.vim/
├── autoload
│ └── session.vim
├── doc
│ └── session.txt
└── plugin
└── session.vim
plugin
ディレクトリについて
plugin
配下はプラグインが提供するExコマンドやオプションを記述したスクリプトファイルを置きます。
メインの処理はここではなく後述するautoload
に記述します。
スクリプトファイル名はプラグイン名と同じにするのが一般的です。
autoload
ディレクトリについて
autoload
配下はメインの処理を記述したスクリプトファイルを置きます。
配下のスクリプトファイルはVim起動時ではなく、コマンド実行時に一度だけ読み込まれます。
また、スクリプトファイル名はプラグイン名にすることが一般的です。
plugin
配下から呼ぶことができる関数をautoload
配下に定義する時、ファイル名#関数名()
という命名規則に従う必要があります。
これはコマンドを実行する時にautoload
配下のどのファイルのどの関数を呼べば良いのかを知る必要があるからです。
そのため、プラグイン名が被るとautoload
配下のスクリプトファイル名も被り、最悪違うプラグインの関数で上書きされる可能性があります。
これがプラグイン名がかぶらないようにする必要がある理由です。
doc
ディレクトリについて
doc
配下はヘルプファイルを置きます。:h SessionList
というようにコマンドのヘルプを引けるようにするためです。
基本的にヘルプに書かれているものは公式、書かれていないものは非公式の機能になります。プラグインを公開する時はREADME.mdだけでなくヘルプを書きましょう。
session.vim
の作成
プラグインディレクトリ開発中のプラグインを動作確認をするために、プラグインをロードする必要があります。 今回ではVimにビルドインされているパッケージ機能を利用して、開発中のプラグインをロードします。 開発の準備としてパッケージ機能で使用するディレクトリと、今回開発するプラグインのディレクトリ構成を作成します。
# パッケージ機能で使用するディレクトを作成します。ここにプラグインのディレクトリを置くとVim起動時にruntimepathに追加され、プラグインがロードされます
$ mkdir -p ~/.vim/pack/plugins/start/
# Neovimの場合は以下のディレクトリになります。以下手順は適宜読み替えてください
$ mkdir -p ~/.config/nvim/pack/plugins/start/
# プラグインのディレクトリ構成を作成します
$ cd ~/.vim/pack/plugins/start/
# Neovimの場合は以下
$ cd ~/.config/nvim/pack/plugins/start/
$ mkdir session.vim
$ cd session.vim
$ mkdir autoload plugin
$ touch autoload/session.vim
$ touch plugin/session.vim
セッションファイルを保存するディレクトリの作成
# Vimの方は~/.vim/session
mkdir -p ~/.vim/session
# Neovimの方は~/.config/nvim/session
mkdir -p ~/.config/nvim/session
autoload/session.vim
の実装
1. セッションを保存する関数
まずはg:session_path
にセッションファイルを保存する関数を作ります。
let s:sep = fnamemodify('.', ':p')[-1:]
function! session#create_session(file) abort
execute 'mksession!' join([g:session_path, a:file], s:sep)
redraw
echo 'session.vim: created'
endfunction
関数を実装したら、so %
で一度スクリプトファイルをロードします。そうすると関数を実行できるようになります。
次にコマンドラインでg:session_path
を設定します。それぞれの環境に合わせて先ほど作成したパスを設定してください。
:let g:session_path = {path}
では実際関数を実行して、セッションファイルを作ってみましょう。正常に作成できたらsession.vim: created
メッセージが出力されます。
:call session#create_session('test')
2. セッションをロードする関数
以下の関数を作ります。
function! session#load_session(file) abort
execute 'source' join([g:session_path, a:file], s:sep)
endfunction
関数を作ったら、一度Vimを再起動して先程保存したセッションファイルを実際ロードしてみましょう。 ウィンドウの状態が戻ったらOKです。
call session#load_session('test')
3. エラーメッセージを出力する関数
処理中に何かしらエラーが発生した場合、エラーメッセージであることがわかるように、
echohl
を使ってコマンドラインに赤いメッセージを出力する関数を作ります。
function! s:echo_err(msg) abort
echohl ErrorMsg
echomsg 'session.vim:' a:msg
echohl None
endfunction
実際メッセージは赤くなるのかを確かめるため、グローバルな関数TestEcho()
を作ります。
function! TestEcho(msg) abort
call s:echo_err(a:msg)
endfunction
上記2つの関数を作ったらTestEecho
を実行して、赤いメッセージが出たらOKです。
:call TestEcho('I am gorilla')
これは動作確認の関数なので削除しておきましょう。:delfunc TestEcho
で削除するか、一度Vimを再起動するかしましょう。
g:session_path
からセッションファイル一覧を取得する関数を実装
4. s:readdir
関数を使ってg:session_path
配下にあるファイルのリストを取得します。
ここでのキモはexists()
でreaddir()
関数があるかを確認するところです。
readdir()
がなければglob()
関数を使ってファイルとディレクトリ一覧を取得する関数s:readdir()
を用意している部分です。Neovimではreaddir()
がないためglob()
を使う必要があります。
readdir()
がある場合はfunction()
で関数への参照を取得してs:readdir
変数に代入して、NeovimでもVimでも同じ変数名でファイル一覧を取得できるようにします。
ファイル、ディレクトリ一覧を取得したあとに、ファイルのみを抽出するためにFilter
Lambdaを用意しfilter()
関数を使って絞り込みます。
if exists('*readdir')
let s:readdir = function('readdir')
else
function! s:readdir(dir) abort
return map(glob(a:dir . s:sep . '*', 1, 1), 'fnamemodify(v:val, ":t")')
endfunction
endif
function! s:files() abort
let session_path = get(g:, 'session_path', '')
if session_path is# ''
call s:echo_err('session_path is empty')
return []
endif
let session_path = expand(session_path)
let Filter = { file -> !isdirectory(session_path . s:sep . file) }
return filter(s:readdir(session_path), Filter)
endfunction
では、実際ファイル一覧を取得できるかを確認してみましょう。グローバルな関数TestFiles()
を作ります。
function! TestFiles() abort
echo s:files()
endfunction
作った関数を実行して、先程作成したtest
ファイルが出力されればOKです。
:call TestFiles()
このテストのための関数も不要なので削除しておきましょう。
5. セッション一覧を表示する
セッションファイルをリストで取得できるようになったので、次に取得したセッション一覧をバッファに書き出します。
let s:session_list_buffer = 'SESSIONS'
function! session#sessions() abort
let files = s:files()
if empty(files)
return
endif
execute 'new' s:session_list_buffer
set buftype=nofile
call setline(1, files)
endfunction
これで:call session#sessions()
を実行するとSESSIONS
というバッファにセッションファイル一覧が表示されます。
しかし、このままでは関数を実行するたびに新しいウィンドウが作れてしまうので、以下のことを考慮して改善する必要があります。
- バッファがなければ新規作成
- バッファがあるがウィンドウに表示されていないならウィンドウに表示させる
- バッファがあってウィンドウに表示されているなら、バッファの中身をクリア
function! session#sessions() abort
let files = s:files()
if empty(files)
return
endif
+ " if buffer exists
+ if bufexists(s:session_list_buffer)
+ " if buffer display in window
+ let winid = bufwinid(s:session_list_buffer)
+ if winid isnot# -1
+ call win_gotoid(winid)
+ else
+ execute 'sbuffer' s:session_list_buffer
+ endif
+ else
execute 'new' s:session_list_buffer
set buftype=nofile
+ endif
+
+ " delete buffer contents
+ %delete _
call setline(1, files)
endfunction
diffの処理を追加したら:so %
で再度スクリプトをロードして関数を実行してみましょう。新たなウィンドは作れず既存バッファとウィンドウを使うようになっているはずです。
6. キーマッピングを追加
表示はできたので、最後に以下のキーマッピングを追加していきます。
Enter
でカーソル下にあるセッションファイルをロードq
でバッファを破棄
function! session#sessions() abort
let files = s:files()
if empty(files)
return
endif
" if buffer exists
if bufexists(s:session_list_buffer)
" if buffer display in window
let winid = bufwinid(s:session_list_buffer)
if winid isnot# -1
call win_gotoid(winid)
else
execute 'sbuffer' s:session_list_buffer
endif
else
execute 'new' s:session_list_buffer
set buftype=nofile
+ nnoremap <silent> <buffer>
+ \ <Plug>(session-close)
+ \ :<C-u>bwipeout!<CR>
+ nnoremap <silent> <buffer>
+ \ <Plug>(session-open)
+ \ :<C-u>call session#load_session(trim(getline('.')))<CR>
+
+ nmap <buffer> q <Plug>(session-close)
+ nmap <buffer> <CR> <Plug>(session-open)
endif
" delete buffer contents
%delete _
call setline(1, files)
endfunction
<Plug>
は特殊でどのキーともマッピングしないです。多くのプラグインではこの<Plug>(xxxx)
を提供して、ユーザが自由にキーマッピングできる仕組みを提供しています。
<buffer>
は現在のバッファだけにキーマッピングを適用します。今回のような他のバッファに影響しないキーマップを用意するときは付ける必要があります。
以上がautoload/session.vim
の実装になります。
plugin/session.vim
の実装
続けてplugin
配下を実装していきます。plugin/session.vim
でやることは2つです。
- プラグイン無効化、二重ロード防止
- コマンド定義
プラグイン無効化、二重ロード防止
Vim起動時にplugin
配下のスクリプトがロードされるので、そこでロード済みかどうかを判断するグローバル変数を用意します。変数名はプラグイン名にプレフィックスg:loaded_
をつけます。
この変数がすでに定義済みなら、finish
でロード処理を中止します。ユーザがプラグインを無効化したい場合はこの変数を予めvimrcに設定しておくことで、プラグインを無効化できます。
if exists('g:loaded_session')
finish
endif
let g:loaded_session = 1
コマンド定義
command
関数でExコマンドを定義します。-nargs
はコマンドに渡せる引数の数を設定できます。
今回はセッションの作成時にファイル名が必要なので-nargs=1
で1つ引数が必要の設定にします。
<q-args>
は引数を意味します。詳細は:h <q-args>
を参照して下さい。
command! SessionList call session#sessions()
command! -nargs=1 SessionCreate call session#create_session(<q-args>)
以上、plugin/session.vim
の実装は終わりです。これでコマンドでセッションの保存とセッション一覧表示とロードが出来るようになります。
実際にVimを再起動して:SessionCreate
と:SessionList
を実行してEnter
でロードできるか確認してみましょう。
ヘルプ
プラグインの実装は終わったのですが、プラグインを公開するにあたりヘルプを書く必要があります。ヘルプはユーザがプラグインで使用できる設定変数やコマンド、関数、キーマッピングの使い方を知るのに必要です。
今回はコマンド2つに設定変数が1つなので記述する量は少ないのですが、大きなプラグインとなると記述量も増えます。そこでLeafCage/vimhelpgeneratorを使ってある程度ヘルプのテンプレートを生成します。
プラグインを導入して:VimHelpGenerator
を実行するとdoc/session.txt
が作られます。それがヘルプファイルになります。
今回追記する部分は以下になります。
------------------------------------------------------------------------------
VARIABLES *session-variables*
ここにユーザが使用できる変数の説明を記述
------------------------------------------------------------------------------
COMMANDS *session-commands*
ここにユーザが使用できるコマンドの説明を記述
------------------------------------------------------------------------------
KEY-MAPPINGS *session-key-mappings*
ここにユーザが使用できるキーマップの説明を記述
一例ですが、以下の様に設定変数と説明を記述します。*
で囲っている部分は実際:h
で検索される部分なので、そこは必ず記述しましょう。
------------------------------------------------------------------------------
VARIABLES *session-variables*
g:session_path *g:session_path*
セッションを保存するファイルパスを設定します。
------------------------------------------------------------------------------
COMMANDS *session-commands*
ここにユーザが使用できるコマンドの説明を記述
:SessionList *:SessionList*
セッション一覧を開きます。
Enterでカーソル上にあるセッションをロードします。
:SessionCreate {name} *:SessionCreate*
セッションを{name}で保存します。
------------------------------------------------------------------------------
KEY-MAPPINGS *session-key-mappings*
<CR> *session-list-<cr>*
カーソル下のセッションをロードします。
q *session-list-q*
セッションリストのバッファを閉じます。
ヘルプを記述し終わったら、ちゃんとヘルプを引けるかどうか:helptags doc
でヘルプタグを生成して実際引いてみましょう。
最後に
これでハンズオンは終わりです。Vim scriptの基礎とプラグインの作り方について一通り解説しましたがわからないところもあるかと思います。不明点などあればいつでもゴリラまで質問してください。
このハンズオンでみなさんにプラグインの作り方について体験して頂くことで、なにかしらを持ち帰っていただけたらと思います。
お疲れさまでした。