EmacsからVimに引越ししてきた自分が初めてVimScript書いた話

この記事は、諸般の事情でVimを触り始めた結果、Emacsを投げ捨ててVimばかし使うようになった自分が、初めて自作VimScriptを書いた顛末を書いたものです。

最終的に自分が作ったものは、「ヤンクしたテキストを標準入力として外部コマンドを走らせる関数」です。また、そこに至るまでに得た知識である、VimのRanges機能とビジュアルモードの関係、VimのRegistersについて、外部コマンド実行のexecuteの落とし穴についても記載します。

各々独立しているので、目次機能にて全部読まずとも必要な部分だけお読みいただければ幸いです。:relaxed:

# きっかけ
まずはVimScriptをあえて書こうと思ったきっかけについてお話します。現在SQLのフォーマッタを書いてまして[^sqlfmt]、テストコードを書く際、SQL文中の要素の開始位置をカウントする必要がありました。
<img width="1067" alt="source code sample" src="https://qiita-image-store.s3.amazonaws.com/0/64292/77d1b042-267e-ff5d-8a56-d934ead979b2.png">

例えば"select col1 from tbl1;"のcol1は何文字目から始まるか？ですね。自分もVimを使い始めて暫く経つので、「ああ、こういうときはこうだな・・・」とおもむろに次のコマンドを打ちました。

```vim
" visual mode で"select c"までを選択後
:'<,'>w !wc
```
すると次のような結果が帰ってきました。

```vim
:'<,'>w !wc
    1    15    80
```
**えっ！？15単語80文字！？どう見てもそんな文字数多くねーよ！！！**:scream:


これをきっかけに、Vimの機能を色々調べることになりました。その結果、自分のやりたいことを満たす機能は自分で作るしかないのかな・・・という結論にいたり、VimScript自作しました。ともあれ、まずはなぜ`wc`での文字数カウントがこんなに多かったのか、説明します。

[^sqlfmt]: [拙作（WIP）](https://github.com/Neetless/sqlfmt)です。

# VimのRanges機能
結論から言うと、`'<,'>`はVimのRanges機能で使われる特殊文字で、visualモードで選択した範囲の、先頭行から最終行を表します。ここで注意しなければならなかったのは、visualモードで選択した文字列ではなく、選択した文字列を含む全ての行全体を`'<,'>`は指定するということです[^helpmark]。これはVimのvisualモードとRanges機能の連携を行うためのものと思って間違いなさそうです。

また、**Ranges機能はBuffer中の行単位で範囲指定を行う機能なので、文字単位での処理は不可能です**[^ranges]。

その他、Ranges機能の基本的な使い方を紹介しますと:


```vim
" 10行目から20行目までを置換します。
:10,20s/before/after/g

" カーソル現在位置の行から５行目までを置換します。
:.,+5s/before/after/g

" NOTE: 先程の'<,'>は、'<から'>まで、という指定で、
"       '< はvisualモードで範囲指定した最初の行
"       '> はvisualモードで範囲指定した最後の行
"       という意味になります。なので次のような使い方もできますね。
:'<,+5s/before/after/g
```

[^helpmark]: Vim内で`:help :marks`を実行すれば、`'<,'>`の説明が見れます。
[^ranges]: [vim wikiの解説](http://vim.wikia.com/wiki/Ranges)です。Vim wikiは分かりやすいと思うので、Vim機能の調べ物する際"wiki"も加えて検索する事が多いです。

# 閑話休題：Vimの文字数カウント機能g ctrl-g
ちなみにですが、visualモードで指定した選択範囲の文字数をカウントする機能は、Vim標準であります。visualモード中に、`g ctrl-g`とコマンド打つだけです。  

<img width="1039" alt="g ctrl-gスクリーンショット" src="https://qiita-image-store.s3.amazonaws.com/0/64292/efb7943e-2b39-7f22-702b-eeac70a0acd3.png">

ただこの表示、**ほんの1,２秒で消えちゃうのでほんと見づらいです**[^angry]。
:rage:この表示時間どうにかなりませんかね？そういうこともあって、自作のVimScript作る気持ちは強まりました。標準入力でいろんなコマンドに渡せたほうが汎用的ですしね。

[^angry]: このスクリーンショット撮るのもほんと難儀しました。:rage:

# Vim版コピペ、YankとRegisters

次に説明するのはVimのRegistersです。Registersとは、VimでYank（コピー）したときにテキストが格納される領域です[^yank]。なぜこれについて調べたかといいますと、visualモード中に選択した範囲のテキストを取得するVimScriptが、いくら探しても見つからなかったためです。そこで**いっそ一旦ヤンクして、Regisitersにつっこんだテキストを渡してコマンド実行すればよいのでは？と考えました。**

Registersは色々ありますが、今回使うのはふつーにvisualモードで選択した範囲をYankしたときに使われる、`"`Registerのみ使います[^registers]。VimScript内では、`@"`として扱うことができ、例えば`:echo @"`とVim内でコマンド実行すれば、現在Yankされてるテキストが表示されます。

ちなみに`:reg`を実行すれば、現在Registersに保存されてるテキスト一覧を見ることができますので、挙動や状態を確認できます。VimScriptのテストしてるときにちょっと便利でした。

[^yank]: Vimは（Emacsも）コピペにOSのクリップボードを利用せず、コピーしたいテキストをYankという機能でエディタ内の領域に一時保存するのが基本的な仕組みとなっています。

[^registers]: [このstackoverflow記事](http://stackoverflow.com/questions/3997078/how-to-paste-yanked-text-into-vim-command-line)が詳しいです。

# executeの罠~system()だけでええやん…~

さて、ここまで来て書きたいVimScriptがようやく見えてきました。処理内容の手順はだいたいこんな感じです。

```vim
" 事前につっこみたいテキストをYankしておく
function! ProcessRegisteredText()
	" 実行したい外部コマンド文字列を取得（ex. wc -m）
	let input=ユーザに文字列入力を促す。

	" 作成文字列イメージはこんな感じ "!echo -n @" | wc -l"
	let executeCmd=@"とinputから、実行文字列を作成。

	" 外部コマンドを実行！めでたしめでたし。
	exe executeCmd
endfunction
```

まずはユーザ入力を受け取る関数ですね。ちょっと調べると出てきました[^input]。

```vim
call inputsave()
let userInput=input("Enter command: ")
call inputrestore()
```

`inputsave()`→`input("Explanation Text")`→`inputrestore()`と３ステップ必ず必要みたいです。ちょっとめんどくさいですが、まあいいでしょう。

次にVimの標準機能`execute`に渡す文字列の作成です[^exeshort]。`execute`はVimのコマンドラインで使えるコマンド文字列を評価し、実行します。具体的には次のように使います。

```vim
" Vimコマンドラインで外部コマンドを実行する際の記法
:!date

" executeを介して実行
:execute "!date"
```

そのため、次のVimScript関数で完成！終わり！と思ったのですが…

```vim
function! ProcessRegisteredText()
	" ユーザの入力を受け取る 
	call inputsave()
	let userInput=input("Enter command: ")
	call inputrestore()

	" 外部コマンド実行（ちなみに文字列連結は.です。ex: "a"."b"）
	let executeCmd="echo -n '" . @" . "' | " . userInput
	execute executeCmd
endfunction
```

ただ、**executeの仕様にはこの機能を実装するにあたり重大な問題が有りまして、`@"`に複数行にまたがるテキストが入ってた場合、想定通りに動きません。**`execute`はどうも、途中に改行がある場合、別コマンドとして実行してしまうようです。具体的には次のように処理されます。

```vim
:execute "!echo test\n!date"
" これは、次の２回の命令に分けられて実行してしまいます。
" execute !echo test
" execute !date
```

どうしたもんか...:worried:と引き続き調べてましたら、`system()`という関数でも外部コマンド実行できるという情報を見つけることができました。そちらでは、改行含む場合でもターミナルで実行したときと同様に外部実行してくれました。:relaxed:

[^input]: [vim wikiからinputの記事](http://vim.wikia.com/wiki/User_input_from_a_script)です。
[^exeshort]: ちなみにVimの標準機能は省略形を許すものが多く`execute`は`exe`でも大丈夫です。

# 最後に

最終的に、以下のVimScriptを.vimrcに追加しています。（シングルクオートがYankしたテキスト中に存在する場合、エスケープを加える処理も追加してます。）

```vim
" execute command with feeding registered(yanked) text
" registered text will be passed by pipe. 
" In short,'echo -n <@"> | <your desired command>' runs. 
function! ProcessRegisteredText()
	call inputsave()
	" enter your desiered external command	
	let userInput=input("Enter command: ")
	call inputrestore()
	echo "\n"
	" escape single quote
	let registeredText=shellescape(@")	
	let executeCmd="echo -n ".registeredText." | ".userInput
	echo executeCmd."\n"
	echo system(executeCmd)
endfunction

noremap <Plug>(process_registered) :<C-u>call ProcessRegisteredText()<CR>
nmap <leader>pr <Plug>(process_registered)
```

次の手順で外部コマンド実行結果を表示できます。
1. 対象の文字列をYank
2. \<Leader> p　r
3. 実行したい外部コマンドを入力（wc -m等）

<img width="851" alt="実行コマンドを入力" src="https://qiita-image-store.s3.amazonaws.com/0/64292/e0c03043-274b-4efd-d21e-66dfb1fb3484.png">

<img width="851" alt="コマンド実行結果出力" src="https://qiita-image-store.s3.amazonaws.com/0/64292/938db162-b1a6-7af4-75d8-6fe59fcbcb2c.png">

以上、お読みいただきありがとうございました。Vimについてまだまだ知らない機能が多いですし、VimScriptコーディングのお作法、keymappingのお作法などまで完璧にすることはできませんでしたが、おいおい学んでゆきたい所存です。