Vim scriptを使って簡易なプラグインを作ってみよう
- ターミナルの操作方法
- 以下のバージョンのVimかNeovimを用意
エディタ | バージョン |
---|---|
Vim | v8.1.1120 ~ |
Neovim | v0.4.0 ~ |
- Vim scriptの基礎
- セッション管理のプラグインを作ってみよう
- Vim scriptはVim上で実行できるスクリプト言語
- Exコマンド(
:
で始まるコマンド)の集合体 - vimrcに記述しているのもVim script
- Vimのプラグインの多くは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 {expr} | endif
{expr}
が0以外の場合は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
といったコマンドではウィンドウを閉じるだけなのでバッファは残る
win_getid()
で現在のウィンドウIDを取得できる- 引数を受け取ることもできるので詳細は
:h win_getid()
を参照
win_gotoid({expr})
で{expr}
の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
は関数内でエラーが発生した場合、そこで処理を終了する- Vim scriptはデフォルトでエラーがあっても処理が継続されるため基本的に
abort
をつける
- 引数を使用するときは
a:
スコープ接頭子を付ける必要がある
return {expr}
で{expr}
の評価結果を返すことができる
" 結果 => gorillaが返る
function! MyName() abort
return 'gorilla'
endfunction
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')
{ 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
配下はプラグインが提供するExコマンドやオプションを記述したスクリプトファイルを置きます。
メインの処理はここではなく後述するautoload
に記述します。
スクリプトファイル名はプラグイン名と同じにするのが一般的です。
autoload
配下はメインの処理を記述したスクリプトファイルを置きます。
配下のスクリプトファイルはVim起動時ではなく、コマンド実行時に一度だけ読み込まれます。
また、スクリプトファイル名はプラグイン名にすることが一般的です。
plugin
配下から呼ぶことができる関数をautoload
配下に定義する時、ファイル名#関数名()
という命名規則に従う必要があります。
これはコマンドを実行する時にautoload
配下のどのファイルのどの関数を呼べば良いのかを知る必要があるからです。
そのため、プラグイン名が被るとautoload
配下のスクリプトファイル名も被り、最悪違うプラグインの関数で上書きされる可能性があります。
これがプラグイン名がかぶらないようにする必要がある理由です。
doc
配下はヘルプファイルを置きます。:h SessionList
というようにコマンドのヘルプを引けるようにするためです。
基本的にヘルプに書かれているものは公式、書かれていないものは非公式の機能になります。プラグインを公開する時はREADME.mdだけでなくヘルプを書きましょう。
開発中のプラグインを動作確認をするために、プラグインをロードする必要があります。 今回では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
まずは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')
以下の関数を作ります。
function! session#load_session(file) abort
execute 'source' join([g:session_path, a:file], s:sep)
endfunction
関数を作ったら、一度Vimを再起動して先程保存したセッションファイルを実際ロードしてみましょう。 ウィンドウの状態が戻ったらOKです。
call session#load_session('test')
処理中に何かしらエラーが発生した場合、エラーメッセージであることがわかるように、
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を再起動するかしましょう。
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()
このテストのための関数も不要なので削除しておきましょう。
セッションファイルをリストで取得できるようになったので、次に取得したセッション一覧をバッファに書き出します。
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 %
で再度スクリプトをロードして関数を実行してみましょう。新たなウィンドは作れず既存バッファとウィンドウを使うようになっているはずです。
表示はできたので、最後に以下のキーマッピングを追加していきます。
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
配下を実装していきます。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の基礎とプラグインの作り方について一通り解説しましたがわからないところもあるかと思います。不明点などあればいつでもゴリラまで質問してください。
このハンズオンでみなさんにプラグインの作り方について体験して頂くことで、なにかしらを持ち帰っていただけたらと思います。
お疲れさまでした。