Vim で縦方向 f 移動を実現した

## はじめに

最近，コーディングをしたり学会の原稿を書いたりするときに vim を使おうと試みています．
今日は自分が実装した（ちょっと）便利なコマンドを紹介します．

※筆者は vim 歴 2 ヶ月ほどです．
vim ベテランの皆様はどうぞ，おかしい点の指摘などよろしくお願いします．

## f 移動とは？

vim にはカーソル移動のためのコマンドが豊富に用意されていますが，中でも私が好きなのは `f` を用いた移動です．以下の例を考えてみましょう．

```latex
|2次方程式 $ax^2 + bx + c = 0$ の解は以下で与えられる．
\begin{align}
  x = \dfrac{
    -b + \sqrt{b^2 - 4ac}
    }{
    2a
  }.
\end{align}
```

たとえばこんな感じのスクリプトがあったとしましょう [^1]．`|`は現在のカーソル位置を表します．上の例だと1行目の1文字目，`2` の上にカーソルがあることになります．

[^1]: なぜ LaTeX なのかというと，私が現在一番書くことが多いのが LaTeX だからです．

さて，ここであなたは，`ax^2 + bx + c = 0` の直後に ` (a\neq 0)` を挿入したくなったとします．カーソルを挿入位置に移動させる必要がありますが，あなたはどうやって移動するでしょうか．現在位置と目的地の間に数式やたくさんの空白が挟まっているため，`w` で移動するのは大変です．行の末尾に近いわけでもない，微妙な位置です．

`f` コマンドはこんなときに有用です．
`f0` と打つと，「その行のそのカーソル以降にある最初の0」を探して移動してくれます．先程の例では

```latex
2次方程式 $ax^2 + bx + c = |0$ の解は以下で与えられる．
\begin{align}
  x = \dfrac{
    -b + \sqrt{b^2 - 4ac}
    }{
    2a
  }.
\end{align}
```

となります．あとは `a` を使うなりして自由に挿入すれば OK．別解として，`f$`とすれば1つ目の `$` に飛び，続いて `;` と打てば次の `$` に飛ぶことができます．そこで `i` を使って挿入モードに入っても同じ結果になります．
`f` 移動，便利ですね！

`f`移動のコツは

- レア文字
- どこに出現しうるか予測しやすい文字
- 何かの区切りになりやすい文字

を狙って飛ぶことです．チュートリアルではしばしば `fa` といった例が出てきますが，むしろアルファベットより記号を指定したほうが真価を発揮しやすいと私は思います[^2]．記号は比較的場所を予測しやすいからです．たとえば，Python のコード中で`f[`と打てば，カーソル位置の後にある最初のリストまたはスライスの頭に飛ぶでしょう．アルファベットの `a` よりは，どこに `[` があるのか予測しやすいはずです．

[^2]: ただしもちろん例外もあり，特にアルファベットの大文字は結構役立つ場合が多いです．多くのコードや欧文において，大文字は文頭や CamelCase など限られた場所にしか出てこない傾向がありますから，候補を絞りやすいといえます．


## 縦方向 f 移動がほしい！

数あるエディタの中から vim を選択した人にはきっと「感動した vim のキーストローク」が1つや2つほどあるのだろうと思いますが，`f` は間違いなく私の感動キーストロークの1つです．`d` や `c` などと組み合わせたりしたらさらに凶悪なコマンドに化けてしまう，というポテンシャルも魅力の一つですね[^3]．

[^3]: 実際，`df` `cf` `yf` は「指定文字が最初に出てくる箇所までを削除する[置き換える/ヤンクする]」というコマンドになります．例えば `df<Space>` は「その行で次に出現する空白文字までを削除する」，いわば `dW` に似た働きをします．これは `F` `t` `T` などの移動コマンドでも同様で，例えば`|hogehoge-fugafuga@gmail.com` というテキストで `ct@` と押すと，`|@gmail.com` のように`@`の直前までが削除された状態で挿入モードに入ることができます．

しかし，`f` 移動はあくまで「横方向（＝同一行内）の移動」のためのコマンドであり，縦方向の移動には無力です．縦方向の移動手段は

- jk 連打
- `<C-f>` や `<C-b>` でガサッと移動
- `{}`などブロック単位で移動

などがありますが，いずれも自由な行にピンポイントで移動できる類のものではなく，`f` コマンドほどの利便性はありません．特定の言語に限れば「関数の頭にジャンプする」などの機能を備えたプラグインで改善することもありますが，`f` のように全種類のテキストファイルに通用する方法はあまり見たことがありません．
縦方向も同じように汎用的に移動の自由度を上げたい，**縦方向 f 移動**が欲しい，と考えるのは自然ではないでしょうか．

## 縦方向 f 移動

というわけで，縦方向 f 移動を実装してみました．
実装はシンプルなので，お手持ちの`.vimrc`に数行書くだけで再現できるはずです．

### 実装

```vimscript:.vimrc
command -nargs=1 MyLineSearch let @m=<q-args> | call search('^\s*'. @m)
command -nargs=1 MyLineBackSearch let @m=<q-args> | call search('^\s*'. @m, 'b')
nnoremap <Space>f :MyLineSearch<Space>
nnoremap <Space>F :MyLineBackSearch<Space>
```

最後のキーリマップはお好みに合わせて自由に変えられます．私は [この記事](http://deris.hatenablog.jp/entry/2013/05/02/192415) などを参考に， `<Space>` を積極的に潰しています．

### 使用時の様子

それでは，上のコードが反映された状態で，先程の例をもう一度考えてみましょう．

```latex
2次方程式 $ax^2 + bx + c = 0 (a\neq 0|)$ の解は以下で与えられる．
\begin{align}
  x = \dfrac{
    -b + \sqrt{b^2 - 4ac}
    }{
    2a
  }.
\end{align}
```

皆さんもすでにお気付きと思いますが，この解の公式はオカシイですね．2次方程式の解の公式なのに，解が1つしかありません．正しい解の公式にするためには，4行目のプラス（`+`）をプラスマイナス（`\pm`）に変える必要があります．いつもなら `jjj` とするか，行数を数えてから `3j` と押すところですが，ここは縦方向 f 移動を使ってみましょう．

使い方は簡単で，`<Space>f-<CR>`と押すだけです（`<Space>` はスペースキー，`<CR>`はエンターキーです）．
すると，「現在いる行以降にあり，空白文字を除いて `-` から始まる最初の行」の先頭にジャンプします．今回の例では4行目がその行に相当しますから，直ちに4行目の先頭にジャンプします．つまり以下のようになります．

```latex
2次方程式 $ax^2 + bx + c = 0 (a\neq 0)$ の解は以下で与えられる．
\begin{align}
  x = \dfrac{
|    -b + \sqrt{b^2 - 4ac}
    }{
    2a
  }.
\end{align}
```

あとは煮るなり焼くなり `f+s\pm<ESC>` と打つなりすれば OK．

### 特徴

縦方向 f 移動は本家 `f` 移動と異なり，エンターキーを押すまで入力を受け付けるため，2文字以上を受け付けられるという特徴があります．これは一長一短です [^4]．

[^4]: もともとは本家 `f` 移動のような機能を実装する知識が私になかったために生じた違いでした．

- 使うたびに最後にエンターキーを押す必要があるため，本家 `f` 移動ほどの利便性はありません．
- 一方で，候補を絞りやすいというメリットもあります．先程の例で `<Space>f-b<CR>` と打てば，目標がもっと遠い場合でもより正確に狙い撃ちできますね．また，Markdown の箇条書きのように，最初の数文字が被りがちなテキストを扱うときも便利です．

### おすすめの使い方

縦方向 f 移動は，以下のようなときにおすすめです．

- コードをぼんやり眺めながら，この行に飛びたいな，と感じたとき
- スクロールしないと見えないほど遠い行に飛びたい，でも行頭の何文字かは覚えている，というとき

縦方向 f 移動は「行頭だけ注意すれば移動できる」という点がミソです．
先程の LaTeX コードの例で「`/+<CR>` でいいじゃん」と思った方もいるかもしれません．それはごもっともですが，もっと複雑な例では「+」はたくさん出現するかもしれませんから，更に限定されたキーワードを選ばないと `n` を連打するハメになるリスクがあります．しかも，そのキーワードはコード全体に潜んでいる可能性があります．つまり，`/` を使って検索するときは，全体を見てキーワードを決める必要があるのです．
一方で縦方向 f 移動では「行頭に指定文字があるもの」のみマッチするため，候補を絞るときも行頭のみ気をつけていればよいという利点があります．

## 応用

縦方向 `f` 移動をさらに便利にするために，いくつか機能を追加しました．

### 縦方向 f 移動を繰り返す（`;` や `,` のように）

```vimscript:.vimrc
command MyLineSameSearch call search('^\s*'. @m)
command MyLineBackSameSearch call search('^\s*'. @m, 'b')
nnoremap <Space>; :MyLineSameSearch<CR>
nnoremap <Space>, :MyLineBackSameSearch<CR>
```

`<Space>;` と打つことで一つ前に行われた縦方向 f 移動を繰り返すことができます [^5]．本家 `;` の踏襲です．
ただし逆方向サーチ `<Space>F` を用いるときは，本家とは進行方向が（おそらく）逆になることに注意してください．

[^5]: この繰り返し機能を実装するためにわざわざ `m` レジスタを使っています．逆に言えば，繰り返し機能を使うつもりがないならば，`MyLineSearch` コマンドにおいて `m` レジスタを使う必要は特にありません．

### submode を用いて複数回の繰り返しを楽に行う

上の繰り返しですが，マッチするパターンが所望の行までにいくつもある場合，`<Space>;<Space>;<Space>;`... などと打つのは面倒ですよね．`<Space>;;;` で同じ機能が実現できたら便利です．[submode プラグイン](https://github.com/kana/vim-submode) を用いれば楽に実現できます．

```vimscript:.vimrc
call submode#enter_with('vertjmp', 'n', '', '<Space>;', ':MyLineSameSearch<CR>')
call submode#enter_with('vertjmp', 'n', '', '<Space>,', ':MyLineBackSameSearch<CR>')
call submode#map('vertjmp', 'n', '', ';', ':MyLineSameSearch<CR>')
call submode#map('vertjmp', 'n', '', ',', ':MyLineBackSameSearch<CR>')
call submode#leave_with('vertjmp', 'n', '', '<Space>')
```

一旦 `<Space>;` と打つと `vertjmp` というサブモードに入り，サブモードに入っている間は `;` キーが `<Space>;` と同じ役割を果たすようになります．目標点に達したら一定時間待つか， `<Space>` を打つことでサブモードを抜けることができます．

### `d` コマンドや `y` コマンドなどと組み合わせる
先程，`f` コマンドは `d` `c` `y` 系のコマンドと組み合わせられることが魅力だ，と述べました．
以下のように書けば，縦方向 `f` 移動でもそれに似た機能を実装することができます．

```vimscript:.vimrc
nnoremap d<Space>f d:MyLineSearch<Space>
nnoremap d<Space>F d:MyLineBackSearch<Space>
nnoremap c<Space>f c:MyLineSearch<Space>
nnoremap c<Space>F c:MyLineBackSearch<Space>
nnoremap y<Space>f y:MyLineSearch<Space>
nnoremap y<Space>F y:MyLineBackSearch<Space>
```

ただし，小文字の `f` の場合，上の定義で実装される `d<Space>f` などは「最初に指定文字が行頭に出現する行の *1行前* までを削除する[置き換える/ヤンクする]」というコマンドとなります．つまり，どちらかというと `dt` コマンドなどの挙動に近いといえます．
大文字の方はそんなことはなく，ちゃんとオリジナルの `F` コマンドに近い挙動になります．ここらへん，まだ改良の余地がありそうですね．

#### 追記（2019/1/17）

上記のリマッピングはあまり能率的ではありませんでした．
`d` や `c` などの後は「オペレータ待機モード」となり，オペレータ待機モードのときの動作は `omap` のように頭文字に `o` がついたコマンドを用いることでマッピングできます．
筆者は現在，そもそも最初の実装で `nnoremap` を用いるのではなくて

```vimscript:.vimrc
noremap <Space>f :MyLineSearch<Space>
noremap <Space>F :MyLineBackSearch<Space>
```

としてしまっています（`n`が1回）．
こうすれば normal モードだけでなく，オペレータ待機モードにも反映させることができます．
ただし，コマンド内部で `:` を用いている都合上，Visual モードではうまく動きません．これはいつか解消したいと思っています．
モードとマッピングに関しては[公式のヘルプ](https://vim-jp.org/vimdoc-ja/map.html#mapmode-o)を読むのが手っ取り早いと思うので，ぜひ参照してみてください．

## 迷った点

現在は検索結果を `m` レジスタに格納しています．
これは `/` レジスタで代用できます．その場合，`MyLineSearch` で最後に検索結果のハイライトを有効にすれば，検索結果をハイライトすることもできます．
検索結果がハイライトされるのは便利ですが，それ以前に何を検索していたかという情報は消えてしまうため，一長一短だと思います．
「移動」以外の副作用を極力抑えるために現在は別のレジスタを使用して実装しています．

## おわりに

ここまで長々と読んでくださりありがとうございました．
もしかすると既に誰かが実装しているかもしれないと感じるぐらいには，実装の手軽さのわりに便利なコマンドだと思いました．是非活用してみてください．

※お試しの際は自己責任でお願いします．特に，既存の有用なキーマッピングを潰さないようご注意ください．