見出し画像

vimプラグインを作成

vimの勉強になると思い、プラグイン作成を初めて進めています。
参考にさせていただいたチュートリアルはこちらとなります。

まずvimプラグインを作成するディレクトリ構成で躓いたので、自分なりに分かる範囲で調べてnoteを作成しました。

プラグインのディレクトリ構成についてはこちらの記事を参照ください。


チュートリアルを進めるにあたって、分からなかったことなど調べて都度このnoteに追記や内容の修正をしていきます。

僕が分からないことってほとんど基礎的なことなので、vimプラグインを作成する過程で学んだことは、他にプラグインを作るときにも役立つ実用的な知識になるかも。

セッションのプラグインを作るけどそもそもvimのセッションとは何か?について知らないので調べました。ウェブブラウザでいっぱいタブを開きっぱなしでpcシャットダウン→pc起動してウェブブラウザを開いたら開きっぱなしだったタブやウィンドウの大きさなどがちゃんと保持されるやつと同じイメージなのがセッションのイメージ。

vimの日本語公式ドキュメント↓

https://vim-jp.org/vimdoc-ja/usr_21.html#21.4


vim公式の:mksessionというコマンドでセッション機能を利用でき、セッションを作成できる。

今回進めるチュートリアルはこのもともとあるセッション機能を利用して便利にするというもの。


まずコードの最初でいきなり分からなかったのが、

:h fnamemodifyを読んでて出てきた:p、:hといった表記です。

:p、:hといった表記の意味については:h filename-modifiersで分かります。

fnamemodifyの使い方

fnamemodify({fname}, {mods})

実際の使用方法

// :pはfull pathのディレクトリが得られる

fnamemodify('.', ':p')[-1:]

第一引数に渡しているドット「 . 」はカレントディレクトリ(現在のディレクトリ)の意味。

[-1:]で抽出方法を決めれる。[ ]はfnamemodifyで得られる結果のリストを表していて、-1は末尾を表す。:の左側に始点を書くみたいなので、末尾を始点にしているのでフルパスで得られたディレクトリの末尾のパスセパレータを取得できる。

fnamemodify('file名', ':p')としたら、/home/file名という文字列が得られる。

fnamemodify('.', ':p')とすると、カレントディレクトリのパスが得られる。

カレントディレクトリが/home/ だとすると、上記で/home/という文字列が得られる。

[-1:]と指定することで、/home/の文字列から末尾のパスセパレーター「/」が得られる。

説明を読んだだけでは動作の理解が難しいので、実際にvimのコマンドラインで:echo fnamemodify('.', ':p')[-1:]を試して動作確認しました。


.vimrcに記載するg:session_pathの書き方:

 g:session_pathをどうやって記載すればいいのか分からず、結構つまづきました。

セッションファイルの保存ディレクトリは任意のところを指定。僕の場合はホームディレクトリ内にしました。

let g:session_path = '/home/user'

すると、セッションファイルが上記のディレクトリ内に作成できるようになります。


echohlはvimのコマンドラインの表示をハイライト表示する。

" echohl でコマンドラインの文字列をハイライトできる。詳細は:h echohl参照
function! s:echo_err(msg) abort
    echohl ErrorMsg
    echomsg 'session.vim:' a:msg
    echohl None
endfunction 

コマンドラインモードで

:echohl ErrorMsg | echo "Don't Panic!" | echohl Noneと入力すると、コマンドラインにDon't Panic!と赤色で表示される。

echohl <メッセージの種類>
 echo <メッセージの種類の色で表示させたいメッセージ>
echo None
のように、1ブロックのひとかたまりがechohlの使い方となる。


ErrorMsgの色でハイライト表示

echohl ErrorMsg

ErrorMsgはコマンドラインでのエラーメッセージを表す色。

echomsg 'session.vim' a:msg

a:msgは関数内のみ有効な変数。a:が関数内のみ有効のスコープとなる。あとで他の処理でこの関数を使うときにmsgに任意のメッセージを入力すると、session.vim: という文字列のあとに任意のメッセージが連結されて表示させることができる。

例えば。msg にhelloという文字列を設定すると、session.vim:helloと表示される。

echomsgコマンドは、echomsg {expr1},...のように使い、引数に渡した{expr}をMessageが保存されている領域に保存させることができる。
:mesコマンドで、保存されているメッセージが確認できる。

試しに:echomsg "Hello" を実行→Messageに文字列"Hello"が保存される→:mesコマンドでMessageを確認→Helloという文字列が保存されていることが確認できる。


get( )の用法
get({dict}, {key}, [, {default}])

get(g:, 'session_path', '')        

g:(グローバル変数の辞書)からsession_pathというキーで保存されている値を取得する。第3引数の' 'は、もし取得できなかったときの戻り値。'default'とすると0をリターンする。

g:session_pathが設定されていないときはエラーメッセージを出し空のリストを返す。

if session_path is# ''
    call s:echo_err('g:session_path is empty')
    return []
endif

is#は、match caseでの同じインスタンスかを判定する。

isだとuse ignorecaseを使ってcaseを無視した上で判定、

is?だと?を付与することによってcaseを無視して判定

vimコマンドモードのヘルプで確認できました。

:h expr-is#


file という引数を受けとって、そのファイルがディレクトリでなければ1を返すLambda

let Filter = { file -> !isdirectory(session_path . s:sep . file) }

Lambda(ラムダ)式とは、無名関数のことで、{ 引数 -> 関数}の形で定義され、別の関数の引数にラムダ式を渡すことができる。

lambdaについて最初ググったりしてましたが、vimのヘルプで説明を見れました。

isdirectoryはディレクトリが存在するかどうかを確認するための関数。引数に渡した文字列がディレクトリならTrueを意味する数字の1で返す。ディレクトリが存在しないか、ディレクトリではない場合はFalseを数字の0で返す。

今回は!isdirectoryの引数にファイルのフルパスを渡しているので、もしディレクトリにファイルが存在している場合は、渡した引数はファイルのフルパスになるので、ディレクトリじゃないと判定される。

!isdirectoryの値が1、つまりディレクトリではない場合はFilterの値が1となることを利用して、値が1ならsession_pathにファイルが存在し、0ならディレクトリは空なことを確認するためにこの関数を使ってる。

!isdirectory(session_path . s:sep . file)でディレクトリにファイルがなかったら、session_path . s:sep . fileを文字連結した結果は、session_pathのディレクトリなので!isdirectory(session_path)は0を返す。

readdir の第2引数に Filter を使用することでファイルだけが入ったリストを取得。

上記の結果からFilterの値が1ならsession_pathにファイルが存在するので、ファイル名がリストに追加される。Filterの値が0ならディレクトリにはファイルが入ってないことを意味するので、リストに何も追加されない。

return readdir(session_path, Filter)


readdirという関数をvimのヘルプでみても出てこない→vim日本語ドキュメントでは出てきたので、僕のインストールしたバージョンのvimにはなく、あとでvim本体に追加された関数です。→vimを最新版にビルドしなおしたら関数が使えるようになりヘルプも表示されました。

readdirは引数のディレクトリからファイル名のリストを取得する関数。

第2引数は省略できる。もし省略すると、渡した第1引数であるディレクトリ内のディファイル名とディレクトリ名がリストに追加される。

readdirの第2引数の値で処理の内容を変えることができる。

-1のとき、それ以上のエントリは処理されない。

0のとき、このエントリはリストに追加されない。

1のとき、このエントリはリストに追加される。

下記のドキュメント内に使い方が載ってました。



セッションファイルのリストを表示するバッファを作成

セッションファイルを入れるバッファ名を定義する

let s:session_list_buffer = 'SESSIONS'

ここで疑問に思ったのが、左辺も右辺も自分で定義したものなので、どうやったらこれがvimにバッファとして認識されるのかな?という疑問。

→バッファ操作の関数の引数にはバッファ名を渡すので、バッファ操作の関数の引数にs:session_list_bufferを渡すことで、結果としてlet s:session_list_buffer = 'SESSIONS'がバッファとして認識されている。これで疑問は解決です。この疑問で少しハマりました。

if bufexists(s:session_list_buffer)
  let winid = bufwinid(s:session_list_buffer)
  if winid isnot# -1
    call win_gotoid(winid)

bufexists( )はバッファの有無を判定する関数で、:h bufexists()でヘルプを確認できる。
引数で渡したexprを評価しTrueならexprに該当する数字を返す。

bufwinid( )はバッファのウィンドウidを得るための関数で、:h bufwinid()でヘルプを確認できる。
引数で渡したexprを評価しwindow-idを数字で返す。もしwindowが存在しなかったら、「-1」が返される。

if winid isnot# -1のisnot#は「#」を付けることで評価対象をmatch caseで評価する。

isnotのように#も?も付けない場合は、use ignorecaseで評価、isnot?のように「?」を付けると、ignore caseで評価する。

今回は、#付けているので、match caseで-1でないかどうかを評価している。

isnotはTrueだと1が返され、falseだと0が返される。

if winid isnot# -1がTrueの場合、winidが-1ではなくwinidが存在することになるので、call win_gotoid(winid)で存在することが確かなことが分かったwinidのwindowに移動する(開く)。

:h win_gotoid( )でヘルプが見れる。実行に成功したら1が返され、存在しないなどの理由で失敗したら0が返される。


execute 'sbuffer' s:session_list_buffer

executeはExコマンドを実行するための命令
Exコマンドは、vimのコマンドラインモードで実行できるコマンドのこと。
:sbuffer s:session_list_bufferをexecuteすることで、vim script内でExコマンドを実行していることになります。

実際にコマンドラインモードで:sbufferを実行してみると、現在開いているvimファイルのウィンドウがsplitされバッファに存在するvimのファイルが開くので、動作のイメージがやってみるとつきます。

もしバッファーにファイルが存在しない場合は、現在開いているファイルのバッファが編集されることになる。

execute 'new' s:session_list_buffer

もしバッファ自体が存在しない場合、新しくバッファを作成する。
Exコマンドのnewコマンドは、新しいウィンドウで(既にvimファイルを開いている場合はウインドウがsplitされる)何も書かれていないvimファイルを開く。

:h :newでヘルプが見れる。


バッファの種類を指定

set buftype=nofile

buftypeでバッファの種類を指定できる。ユーザーが書き込むことはないバッファなので、値をnofileに設定。

※buftypeに何も設定しない<empty>の場合はnormal bufferとなる。

nofileは書き込みされないファイル、バッファとファイルの関連がないファイル

詳細はヘルプの:h buftypeで確認できる。


キーマッピング

キーマッピングと関係ないけど、下記コードの「\」は改行を意味する。1行にすると長くなる場合に、改行して書きたいときに使える。

nnoremap <silent> <buffer>
     \ <Plug>(session-close)
     \ :<C-u>bwipeout!<CR>

nnoremapはノーマルモードのキーマッピングを設定するコマンド。

キーマッピングを設定するコマンドは、vimのノーマルモード、ビジュアルモードなどモード毎に設定できるようになっている。

noremapは再帰的なキーマッピングを行わない。

再帰的なキーマッピングを行うnmapの動作はこんな動作になる。→同じ名前のキーマッピングが存在すると、違うとこでキーマッピングした同じ名前のキーマッピングが呼び出されること。

再帰的なキーマッピングの動作(弊害)

再帰的にキーマッピングを行うnmapコマンドを下記のように使用すると、

1回目:nmap j gj
2回目:nmap gj j

2回目の定義を実行した時点で、
→1回目のキーマッピングで定義したj = gjのgjの部分は2回目で定義したgj = jと評価し2回目のgj = jを呼び出す
→2回目のgj = j のjは1回目のキーマッピングで定義したj = gjだと評価し1回目のj = gjを呼び出す。
上記の無限ループが発生しエラーとなる。

<silent>とは

vimのヘルプで確認できる。

:h :map-silent

<silent>はコマンドラインへの表示を行わないマッピングで、<silent>はマッピング定義するときに最初に付ける。

:map-<silent>* *:map-silent*
To define a mapping which will not be echoed on the command line, add
"<silent>" as the first argument. Example: >
:map <silent> ,h /Header<CR>

<buffer>とは

<buffer>を使うと現在のバッファにのみ有効なキーマッピングを設定できる。

ヘルプは :h map-<buffer>

*:map-<buffer>* *E224* *E225*
If the first argument to one of these commands is "<buffer>" the mapping will
be effective in the current buffer only. Example: >
:map <buffer> ,w /[.,;]<CR>
Then you can map ",w" to something else in another buffer: >
:map <buffer> ,w /[#&!]<CR>
The local buffer mappings are used before the global ones. See <nowait> below
to make a short local mapping not taking effect when a longer global one
exists.
The "<buffer>" argument can also be used to clear mappings: >
:unmap <buffer> ,w
:mapclear <buffer>
Local mappings are also cleared when a buffer is deleted, but not when it is
unloaded. Just like local option values.

<Plug>とは

<Plug>
特別な文字列 "<Plug>" を使ってスクリプトの内部作業用のマップを定義できます。こ
れはどのキー入力にもマッチしません。プラグインを作成するときに便利です
using-<Plug>。 -vim日本語公式ドキュメントから引用

例えば、A(foo)という関数をjにキーマッピングするには、
j A(foo)のように書く。関数A(foo)はjのキーにマッピングしていることになるので、他のところでA(foo)を使いたいとき、すでにjにマッピングしているのでA(foo)を他のところでマッピングしようとするとできない。

<Plug> j A(foo) のように<Plug>を付けると、jのキーにA(foo)がマッピングされていると他のところに認識されないので、他のところでも<Plug> k A(foo)のようにしてA(foo)を使える。

少し理解が難しいけど、<Plug>はとりあえずこんなイメージだといまのところ認識しています。

キーマッピングで使う<CR>や<C-u>の特殊キーの意味

<CR>はキャリッジリターンといい、エンターキーを押下して改行する動作のことを表している。

<C-u>はCtrlキー + uを押す動作のこと。

aキーなどだとそのままaとして使えるけど、Ctrlキーとの組み合わせなどは特殊キーとして処理することで、このような特殊なキーもキーマッピングに使用できるようになっている。

改めて同じコードを確認してみる。

nnoremap <silent> <buffer>
     \ <Plug>(session-close)
     \ :<C-u>bwipeout!<CR>

nnoremap<silent><buffer><Plug>(session-close)は、
実行したコマンドをコマンドラインに表示せず、現在のバッファのみ有効で、
<Plug>(session-close)によってsession-closeと名づけた関数を使うという意味。
そして、そのsession-closeというPlugの実装は、
:<C-u>bwipeout!<CR>というマッピングですよという意味。

:<C-u>bwipeout!<CR>は、コマンドラインモードでCtrl + uを押し、bwipeout! + Enterキーを押下するという意味。「!」によって強制的に実行。

<C-u>の意味: コマンドラインモードでCtrl + uを押すとコマンドラインモードに入力されていた何らかの文字が削除されカーソルが先頭に戻る。

なので、:<C-u>bwipeout!<CR>は下記の動作となる。

「:」でコマンドラインモードに入る→<C-u>でなんらかの文字がコマンドライン上にある場合は文字をクリア(削除)しカーソルを先頭に移動→強制的にバッファを削除するコマンドの文字列「bwipeout!」を入力→<CR>でエンターが押されbwipeout!が実行される。

上記をsession-closeという文字列で実行できる。

bwipeoutはバッファ削除の命令。


nnoremap <silent> <buffer>
 \ <Plug>(session-open)
 \ :<C-u>call session#load_session(trim(getline('.')))<CR>

getline( ) はcurrent bufferから引数に渡したlnum( line number)のラインを文字列で返す。

使用例:

getline(1) →ファイルの1行目の文字列を取得。

試しにコマンドラインモードで:echo getline(1)と入力すると、開いているファイルの1行めの文字列が表示される。

getlineに渡した引数が数字で始まらない文字列の場合、文字を数字に変換するためにline( )が呼ばれる。

line( expr)は数字が返される。exprを評価し、fileの中のexprに該当する行の番号を数字で返す。

引数に「.」を渡すと、cursorがある行番号を返す。

line('.')

getline('.')で、カーソルのある行の文字列を返す。

→つまり:<C-u>call session#load_session(trim(getline('.')))<CR>の処理としては、ファイル一覧を表示する一時バッファのsession_list_bufferに表示されたセッションファイルの一覧から特定のファイルを選択すると、そこにカーソルが当たっている状態なので、そのカーソルが当たっているセッションファイルのファイル名を読み込む処理となる。


%delete _
 call setline(1, files)
endfunction

%delete_でバッファに表示されているテキストを全て削除。
%で全ての範囲を指定、deleteで削除、underscoreモーションは1行下の非空白文字の先頭に移動

setline(1, files)でテキストを削除したバッファに取得したファイルの一覧を挿入する。


以上から、ここまでのざっくりな流れとしては、session_pathのディレクトリにセッションファイルを保存できるようにし、保存したセッションファイルの一覧を取得して表示できるようにし、一覧から開きたいセッションファイルを選択してバッファに読み込んで表示できるようにしている。

plugin/session.vimの作成

このディレクトリのファイルでコマンドを定義する。

先に作ったautoloadの中のファイルはメインの処理。

・プラグインを無効化するコマンドを定義
グローバル変数を.vimrcファイルに書くことで、プラグインを簡単に無効化できるようにする。

if exists('g:loaded_session')
    finish
endif
let g:loaded_session = 1

exists({expr})
The result is a Number, which is |TRUE| if {expr} is defined, zero otherwise.
引数は文字列。引数を評価し、引数がもし定義されていたらtrueを返す。引数が定義されていない、つまり存在してなかったら、0を返すのでif exists()はfalseとなり処理を抜ける。

グローバル変数のg:loaded_sessionを探し、.vimrcに定義されていたら

if exists(g:loaded_session)はtrueとなるので中身が実行、つまりfinishが実行される。

なので、g:loaded_session = 1と.vimrcファイルに定義していたら、finishが実行されてプラグインが無効化される。

g:loaded_session = 1をこのplugins/session.vimの中であらかじめ定義しておく。
一見すると別にこのファイルで定義しとかなくてもいけそうに見えるけど、あらかじめ定義しておくことで.vimrcの中でこのグローバル変数を使えると認識しとけば良さそう(定義してないとエラーになるのかも)。

finishコマンドはvim scriptの中でだけで使える、scriptの読み込みを止めるコマンド。


commandでユーザー定義のExコマンドを作成

command! SessionList call session#sessions()

上記のSessionListというユーザ定義のコマンド名のように、ユーザー定義のコマンド名は、ビルトインコマンドと区別するために大文字で始めないといけないルールになっている。

上記が原則だけど例外もあるので、ヘルプを確認したほうがよい。

あと、ユーザ定義コマンド名の後ろに数字を使わない方がいいみたい。コマンド名なのか、コマンドに付ける引数なのか曖昧になってしまうため。

command [ユーザ定義の関数名] [実行したい既存のコマンド]

SessionListという名前でcall session#sessions()を使うことができるようになる。

commandの後ろに付けた「!」によって、既にユーザ定義関数が存在したときにエラーを出さず再定義することができる。


もう一つユーザ定義関数を作成

command! -nargs=1 SessionCreate call session#create_session(<q-args>)

command {args} {ユーザ定義cmd名} {既存の関数}

argsはいろんな形式がある。
詳細は:h command に載っている。

デフォルトでは引数の数は-nargs=0で0となっているので、-nargs=1でひとつ引数を渡せるようにする。

<q-args>は引数のことで、q-をつけることで引数にabcを渡した場合は"abc"のようにquoteで囲って変換してくれる。引数がなしの場合は空文字に置き換えられる。

以上で、pluginディレクトリのsession.vimにはメインのファイルを更に利用したりしているユーザー定義関数などを書くことが分かった。

ヘルプの作成

ここまででセッションのプラグインができあがったので、次はヘルプを作成。

ヘルプのテキストを自動で生成できるプラグインを利用します。

・dein.tomlファイルにプラグインを追加

・ヘルプを作成したいファイルを開く。今回はメインの処理を行っているファイルを開く。今回はautoload/session.vim

・開いたファイルで:VimHelpGeneratorコマンドを実行→eを押してenterキーを押下

→doc/session.jaxというヘルプのテンプレートファイルが生成される。

→ヘルプを記載して保存

上記でヘルプのファイルが完成です。

ヘルプのためのtagファイルを生成

:helptags <docディレクトリのパス>

ヘルプのファイルsession.jaxを基にしてtags-jaというファイルが生成される。

以上でヘルプの検索対象の文字色が変わり、コマンドモードからも、ヘルプファイルからもちゃんとヘルプが使えるようになります。

コマンドモードで:h SessionListと入力すると、今回作成した関数のヘルプファイルが開かれ、該当の箇所が表示されます。ヘルプファイルの中では、Ctrl + ]の操作などvimのヘルプ機能が有効になります。

作成したプラグインをgithubに公開する

作成したプラグインのディレクトリをgitの対象にして、

$ git add .
$ git commit -m "変更内容の説明"
$ git push

以上でgithubのリモートリポジトリ上にプラグインのディレクトリが反映されます。

僕も使い慣れていないですが、gitでpushする方法について作成した記事はこちら。

https://note.com/noabou/n/n0bb3af3f1d38

gitをはじめて設定するなど初期設定はこちら。

https://note.com/noabou/n/n3ba65990e2b1






























続きはまた内容を追記します。

この記事が気に入ったらサポートをしてみませんか?