改良型縦方向f移動

# はじめに
この記事は [Vim Advent Calendar 2021](https://qiita.com/advent-calendar/2021/vim) 13日目の記事です。

vimの縦移動はrelative numberを使った行数指定移動や検索によるジャンプなどが一般的で、fコマンドのように直観的に素早く移動できるコマンドはデフォルトではないと思います。そこで調べたところ以下の記事で縦方向f移動という考えを知りました。是非一度ご覧ください。

https://qiita.com/monaqa/items/9764e957523ad0b56aec

この記事を参考に、今回は縦方向f移動を改良して作成しましたので紹介したいと思います。

また、今回作成したスクリプトはslackのvim-jpの皆様の知識をお借りすることで実現したものです。この場を借りてお礼申し上げます。
# f移動とは？
vimのノーマルモードに標準で存在する強力な横移動コマンドです。 f{char} と入力するとカーソル位置より後ろに{char}が存在すればその文字まで移動します。また、 F{char} と入力するとカーソル位置より前の{char}へ移動できます。例を以下に示します。（例文は上の記事から引用しています）

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

\|をカーソル位置として、この場所で fc と入力すると以下のようにcの位置までカーソルが移動します。

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

また、続けて Fa と入力すると今度はaの位置までカーソルが移動します。

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

余談ですが、quick-scopeというプラグインを入れるとf移動に相性のいいキーがハイライトされるようになります。f移動になれるのにとても良いプラグインなので活用してみるのもいいと思います。

https://github.com/unblevable/quick-scope

# 縦方向f移動とは？
今回作成する縦方向f移動は、名前の通り上記のf移動を縦方向にも使えるように拡張したものになります。今回は本家の記事の通り \<Space>f にキーを割り当てているので、 \<Space>f{char} と入力すると先頭が{char}である文の先頭へジャンプします。こちらも例を示します。

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

この状態から\<Space>f-と入力すると

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

のように先頭が-の行にジャンプできます。


# 改良型縦方向f移動について
本家の縦方向f移動に対して以下の改善および変更を加えました。

- visualモードで使用可能になった
- 入力を1文字に制限してf移動に近い操作感にした
- 代わりに\\などの一部の文字を無視できるようにした
- ;によってリピートできるようにした（本家記事では\<Space>;によってリピートする方法が紹介されている）
- 移動先を行頭ではなく文頭にした

これらの変更は部分的にスクリプトをいじればもとに戻せるため、使いやすいようにいじってみてください。

以下に実装を示します。

```vimscript
let g:my_line_search_arg = ''
let g:my_repeat_type = ''
let g:my_line_search_ignore_chars = '\\ / \* \s #'

function! MyLineSearch(arg) abort
  let g:my_line_search_arg = a:arg
  call search('^\s*[' . g:my_line_search_ignore_chars . ']*' . a:arg, 'W')
  normal! ^
endfunction

function! MyLineBackSearch(arg) abort
  let g:my_line_search_arg = a:arg
  normal! 0
  call search('^\s*[' . g:my_line_search_ignore_chars . ']*' . a:arg, 'bW')
  normal! ^
endfunction

function! s:Mygf(arg) abort
  let g:my_repeat_type = 'forword'
  call MyLineSearch(a:arg)
endfunction

function! s:MygF(arg) abort
  let g:my_repeat_type = 'back'
  call MyLineBackSearch(a:arg)
endfunction

function! s:MyRepeat() abort
  if g:my_repeat_type == 'forword'
	call MyLineSearch(g:my_line_search_arg)
  elseif g:my_repeat_type == 'back'
	call MyLineBackSearch(g:my_line_search_arg)
  elseif g:my_repeat_type == 'gitgutter_forword'
	GitGutterNextHunk
  elseif g:my_repeat_type == 'gitgutter_back'
	GitGutterPrevHunk
  else
	normal! ;
  endif
endfunction

function! s:MyAntiRepeat() abort
  if g:my_repeat_type == 'forword'
	call MyLineBackSearch(g:my_line_search_arg)
  elseif g:my_repeat_type == 'back'
	call MyLineSearch(g:my_line_search_arg)
  elseif g:my_repeat_type == 'gitgutter_forword'
	GitGutterPrevHunk
  elseif g:my_repeat_type == 'gitgutter_back'
	GitGutterNextHunk
  else
	normal! ,
  endif
endfunction

function! s:Myf() abort
  let g:my_repeat_type = ''
  return 'f'
endfunction
function! s:MyF() abort
  let g:my_repeat_type = ''
  return 'F'
endfunction
function! s:Myt() abort
  let g:my_repeat_type = ''
  return 't'
endfunction
function! s:MyT() abort
  let g:my_repeat_type = ''
  return 'T'
endfunction

nnoremap <silent> <Leader>p gf
vnoremap <silent> <Leader>p gf
onoremap <silent> <Leader>p gf
nnoremap <silent> gf <Cmd>call <SID>Mygf(nr2char(getchar()))<CR>
vnoremap <silent> gf <Cmd>call <SID>Mygf(nr2char(getchar()))<CR>
onoremap <silent> gf <Cmd>call <SID>Mygf(nr2char(getchar()))<CR>
nnoremap <silent> gF <Cmd>call <SID>MygF(nr2char(getchar()))<CR>
vnoremap <silent> gF <Cmd>call <SID>MygF(nr2char(getchar()))<CR>
onoremap <silent> gF <Cmd>call <SID>MygF(nr2char(getchar()))<CR>
nnoremap <silent> ; <Cmd>call <SID>MyRepeat()<CR>
vnoremap <silent> ; <Cmd>call <SID>MyRepeat()<CR>
xnoremap <silent> ; <Cmd>call <SID>MyRepeat()<CR>
nnoremap <silent> , <Cmd>call <SID>MyAntiRepeat()<CR>
vnoremap <silent> , <Cmd>call <SID>MyAntiRepeat()<CR>
xnoremap <silent> , <Cmd>call <SID>MyAntiRepeat()<CR>
nnoremap <silent><expr> f <SID>Myf()
vnoremap <silent><expr> f <SID>Myf()
onoremap <silent><expr> f <SID>Myf()
nnoremap <silent><expr> F <SID>MyF()
vnoremap <silent><expr> F <SID>MyF()
onoremap <silent><expr> F <SID>MyF()
nnoremap <silent><expr> t <SID>Myt()
vnoremap <silent><expr> t <SID>Myt()
onoremap <silent><expr> t <SID>Myt()
nnoremap <silent><expr> T <SID>MyT()
vnoremap <silent><expr> T <SID>MyT()
onoremap <silent><expr> T <SID>MyT()
```

g:my_line_search_ignore_charsに含まれる文字は飛ばして検索できるので、例えばtexファイルを編集する際には値を'\\\\'とすると\\beginなどの\\から始まる行も検索できます。

# ちょっとおもしろい縦方向f移動について
このスクリプトを作成しているうちに気付いたのですが、縦方向f移動にはもう一つ違う実装が考えられます。それは縦方向に検索文字が存在する場合、その場所へジャンプするというものです。以下に例を示します。

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

キーマップをfj{char}として、fjsと入力すると

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

のように同じ行のaの位置まで移動します。キーマップは上と同様に\<Space>fでも問題ありませんが、個人的にflをf移動、fhをF移動、fjを縦方向f移動、fkを縦方向F移動という風にマップすると直観的な気がするのでそういう風にマップしています。

以下に実装を示します。

```vimscript
set cursorcolumn
let g:my_line_search_arg = ''
let g:my_repeat_type = ''

function! GetSeemingIndex(str, index) abort
  let num_tab = count(a:str[:a:index], "\t")
  return a:index + (&tabstop - 1) * num_tab
endfunction

function! GetIndex(str, seeming_index) abort
  let splited_strs = split(' ' . a:str, "\t")
  let splited_strs[0] = splited_strs[0][1:]

  " calculate num of tab which is front of index
  let num_tab = 0
  let len_seeming_str = strlen(splited_strs[0])
  for splited_str in splited_strs[1:]
	if len_seeming_str > a:seeming_index
	  break
	endif
	let len_seeming_str += &tabstop + strlen(splited_str)
	let num_tab += 1
  endfor

  if len_seeming_str <= a:seeming_index
	return -1
  endif

  return a:seeming_index - (&tabstop - 1) * num_tab
endfunction

function! MyColumnSearch(arg) abort
  let g:my_line_search_arg = a:arg
  let pos = getpos('.')
  let seeming_index = GetSeemingIndex(getline('.'), pos[2] - 1)

  let i = 0
  for line in getline(pos[1] + 1, '$')
	let i += 1
	let index = GetIndex(line, seeming_index)
	if index == -1
	  continue
	endif
	" moves position that the argument was found
	if line[index] == a:arg
	  let pos[1] += i
	  let pos[2] = index + 1
	  call setpos('.', pos)
	  return
	endif
  endfor
endfunction

function! MyColumnBackSearch(arg) abort
  let g:my_line_search_arg = a:arg
  let pos = getpos('.')
  let seeming_index = GetSeemingIndex(getline('.'), pos[2] - 1)

  let i = 0
  for line in reverse(getline(1, pos[1] - 1))
	let i += 1
	let index = GetIndex(line, seeming_index)
	if index == -1
	  continue
	endif
	" moves position that the argument was found
	if line[index] == a:arg
	  let pos[1] -= i
	  let pos[2] = index + 1
	  call setpos('.', pos)
	  return
	endif
  endfor
endfunction

function! s:Myfl() abort
  let g:my_repeat_type = ''
  return 'f'
endfunction
function! s:Myfh() abort
  let g:my_repeat_type = ''
  return 'F'
endfunction

function! s:Myfj(arg) abort
  let g:my_repeat_type = 'forword'
  call MyColumnSearch(a:arg)
endfunction

function! s:Myfk(arg) abort
  let g:my_repeat_type = 'back'
  call MyColumnBackSearch(a:arg)
endfunction

function! s:MyRepeat() abort
  if g:my_repeat_type == 'forword'
	call MyColumnSearch(g:my_line_search_arg)
  elseif g:my_repeat_type == 'back'
	call MyColumnBackSearch(g:my_line_search_arg)
  else
	normal! ;
  endif
endfunction

function! s:MyAntiRepeat() abort
  if g:my_repeat_type == 'forword'
	call MyColumnBackSearch(g:my_line_search_arg)
  elseif g:my_repeat_type == 'back'
	call MyColumnSearch(g:my_line_search_arg)
  else
	normal! ,
  endif
endfunction

function! s:Myt() abort
  let g:my_repeat_type = ''
  return 't'
endfunction
function! s:MyT() abort
  let g:my_repeat_type = ''
  return 'T'
endfunction

nnoremap <silent>f <Nop>
vnoremap <silent>f <Nop>
onoremap <silent>f <Nop>
nnoremap <silent><expr> fl <SID>Myfl()
vnoremap <silent><expr> fl <SID>Myfl()
onoremap <silent><expr> fl <SID>Myfl()
nnoremap <silent><expr> fh <SID>Myfh()
vnoremap <silent><expr> fh <SID>Myfh()
onoremap <silent><expr> fh <SID>Myfh()
nnoremap <silent> fj <Cmd>call <SID>Myfj(nr2char(getchar()))<CR>
vnoremap <silent> fj <Cmd>call <SID>Myfj(nr2char(getchar()))<CR>
onoremap <silent> fj <Cmd>call <SID>Myfj(nr2char(getchar()))<CR>
nnoremap <silent> fk <Cmd>call <SID>Myfk(nr2char(getchar()))<CR>
vnoremap <silent> fk <Cmd>call <SID>Myfk(nr2char(getchar()))<CR>
onoremap <silent> fk <Cmd>call <SID>Myfk(nr2char(getchar()))<CR>
nnoremap <silent> ; <Cmd>call <SID>MyRepeat()<CR>
vnoremap <silent> ; <Cmd>call <SID>MyRepeat()<CR>
xnoremap <silent> ; <Cmd>call <SID>MyRepeat()<CR>
nnoremap <silent> , <Cmd>call <SID>MyAntiRepeat()<CR>
vnoremap <silent> , <Cmd>call <SID>MyAntiRepeat()<CR>
xnoremap <silent> , <Cmd>call <SID>MyAntiRepeat()<CR>
nnoremap <silent><expr> t <SID>Myt()
vnoremap <silent><expr> t <SID>Myt()
onoremap <silent><expr> t <SID>Myt()
nnoremap <silent><expr> T <SID>MyT()
vnoremap <silent><expr> T <SID>MyT()
onoremap <silent><expr> T <SID>MyT()
```

set cursorcolumnはなくてもよいですが、これがあると列がわかりやすくなるのでこの縦方向f移動が少し使いやすくなると思います。

# 類似プロジェクト
似たような機能を持つプラグインを紹介します。

https://github.com/easymotion/vim-easymotion

結構な人が使ってるイメージがある移動用プラグインです。コマンド一つでどんな場所にも移動できるすごいプラグインです。

https://github.com/justinmk/vim-sneak

f移動を拡張するプラグインです。本家記事のコメント欄で知りましたがいろんな機能があって面白そうですね。

# おわりに
ここまでお付き合いいただき誠にありがとうございます。

本家記事から面白そうだと思っていろいろ試してみましたが、vimは思いついたことをいろいろできるのがいいですね。皆さんもいろいろやってみて、できればコメントなどで教えていただけると嬉しいです！



# 参考資料
- https://qiita.com/monaqa/items/9764e957523ad0b56aec
- https://github.com/easymotion/vim-easymotion
- https://github.com/justinmk/vim-sneak