はじめに
この記事は Vim Advent Calendar 2021 13日目の記事です。
vimの縦移動はrelative numberを使った行数指定移動や検索によるジャンプなどが一般的で、fコマンドのように直観的に素早く移動できるコマンドはデフォルトではないと思います。そこで調べたところ以下の記事で縦方向f移動という考えを知りました。是非一度ご覧ください。
この記事を参考に、今回は縦方向f移動を改良して作成しましたので紹介したいと思います。
また、今回作成したスクリプトはslackのvim-jpの皆様の知識をお借りすることで実現したものです。この場を借りてお礼申し上げます。
f移動とは?
vimのノーマルモードに標準で存在する強力な横移動コマンドです。 f{char} と入力するとカーソル位置より後ろに{char}が存在すればその文字まで移動します。また、 F{char} と入力するとカーソル位置より前の{char}へ移動できます。例を以下に示します。(例文は上の記事から引用しています)
2次方程式 $ax^2 + |bx + c = 0$ の解は以下で与えられる.
\begin{align}
x = \dfrac{
-b + \sqrt{b^2 - 4ac}
}{
2a
}.
\end{align}
|をカーソル位置として、この場所で fc と入力すると以下のようにcの位置までカーソルが移動します。
2次方程式 $ax^2 + bx + |c = 0$ の解は以下で与えられる.
\begin{align}
x = \dfrac{
-b + \sqrt{b^2 - 4ac}
}{
2a
}.
\end{align}
また、続けて Fa と入力すると今度はaの位置までカーソルが移動します。
2次方程式 $|ax^2 + bx + c = 0$ の解は以下で与えられる.
\begin{align}
x = \dfrac{
-b + \sqrt{b^2 - 4ac}
}{
2a
}.
\end{align}
余談ですが、quick-scopeというプラグインを入れるとf移動に相性のいいキーがハイライトされるようになります。f移動になれるのにとても良いプラグインなので活用してみるのもいいと思います。
縦方向f移動とは?
今回作成する縦方向f移動は、名前の通り上記のf移動を縦方向にも使えるように拡張したものになります。今回は本家の記事の通り <Space>f にキーを割り当てているので、 <Space>f{char} と入力すると先頭が{char}である文の先頭へジャンプします。こちらも例を示します。
2次方程式 $ax^2 + |bx + c = 0$ の解は以下で与えられる.
\begin{align}
x = \dfrac{
-b + \sqrt{b^2 - 4ac}
}{
2a
}.
\end{align}
この状態から<Space>f-と入力すると
2次方程式 $ax^2 + bx + c = 0$ の解は以下で与えられる.
\begin{align}
x = \dfrac{
|-b + \sqrt{b^2 - 4ac}
}{
2a
}.
\end{align}
のように先頭が-の行にジャンプできます。
改良型縦方向f移動について
本家の縦方向f移動に対して以下の改善および変更を加えました。
- visualモードで使用可能になった
- 入力を1文字に制限してf移動に近い操作感にした
- 代わりに\などの一部の文字を無視できるようにした
- ;によってリピートできるようにした(本家記事では<Space>;によってリピートする方法が紹介されている)
- 移動先を行頭ではなく文頭にした
これらの変更は部分的にスクリプトをいじればもとに戻せるため、使いやすいようにいじってみてください。
以下に実装を示します。
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移動にはもう一つ違う実装が考えられます。それは縦方向に検索文字が存在する場合、その場所へジャンプするというものです。以下に例を示します。
2次方|程式 $ax^2 + bx + c = 0$ の解は以下で与えられる.
\begin{align}
x = \dfrac{
-b + \sqrt{b^2 - 4ac}
}{
2a
}.
\end{align}
キーマップをfj{char}として、fjsと入力すると
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移動という風にマップすると直観的な気がするのでそういう風にマップしています。
以下に実装を示します。
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移動が少し使いやすくなると思います。
類似プロジェクト
似たような機能を持つプラグインを紹介します。
結構な人が使ってるイメージがある移動用プラグインです。コマンド一つでどんな場所にも移動できるすごいプラグインです。
f移動を拡張するプラグインです。本家記事のコメント欄で知りましたがいろんな機能があって面白そうですね。
おわりに
ここまでお付き合いいただき誠にありがとうございます。
本家記事から面白そうだと思っていろいろ試してみましたが、vimは思いついたことをいろいろできるのがいいですね。皆さんもいろいろやってみて、できればコメントなどで教えていただけると嬉しいです!