前回までに、「)」を押したときに、自分の求める場所へカーソルをジャンプさせる機能を作り上げた。設定は次の通りだ。後方パターン検索を駆使して自分が行きたい場所へカーソルがジャンプするようになっている。

nnoremap ) />[^\n]\\|.[^ \t>]<\\|[:。、?!]\\|[.,?!][ \t\n]<CR>l

動作させると次のようになる。

「)」を押してジャンプさせた例

この設定は、ノーマルモードのみならずビジュアル選択モードでも機能してほしい。と言っても、特に動作の内容を変える必要はないので、次のように同じ設定を行うだけだ。

nnoremap ) />[^\n]\\|.[^ \t>]<\\|[:。、?!]\\|[.,?!][ \t\n]<CR>l
vnoremap ) />[^\n]\\|.[^ \t>]<\\|[:。、?!]\\|[.,?!][ \t\n]<CR>l

「)」の設定をしたということは、当然逆方向に移動する「(」もある。移動する方向が逆になっただけで、止まる場所は同じだ。つまり、ほとんど「)」の設定内容を使い回すことができる。今回は「(」の設定を完成させ、「(」と「)」によるカスタムジャンプのサンプルを完成させる。

「(」の設定

まず、何も考えずに「)」の設定を逆方向に適用するだけでいってみよう。先ほどの設定は以下のようなものだった。

nnoremap ) />[^\n]\\|.[^ \t>]<\\|[:。、?!]\\|[.,?!][ \t\n]<CR>l

単純に「)」を「(」に書き換え、後方検索の「/」を前方検索の「?」へ書き換えてみよう。設定は次のようになる。

nnoremap ( ?>[^\n]\\|.[^ \t>]<\\|[:。、?!]\\|[.,?!][ \t\n]<CR>l

この設定で「(」を動作させると次のようになる。

「(」の設定を実行したサンプル

最初に移動した場所でループすることが確認できる。これだと使い物にならない。

なぜループしているのか、分析してみよう。最初のジャンプで「。」へ移動していることを考えると、パターンとしては「.[^ \t>]<」に一致していることになる。先ほどの設定ではジャンプした後で1つ右へカーソルが移動(l)しているのだから、「[:。、?!]」に一致したわけではない。「[:。、?!]」に一致しているのであれば、移動先は「<」の上になるはずだ。「。」へ移動しているということは、移動後の右移動を加味して「.[^ \t>]<」に一致していることになる。

「.[^ \t>]<」に一致した場所から1つ右へ移動した場所から「.[^ \t>]<」でパターン検索をかければ、当然同じ場所に一致することになる。検索によって位置も1つ左へ移動するものの、検索後に「l」で右へ移動しているのだから、移動先は同じだ。つまり、ここで無限ループが発生することになる。

しかし、原因がわかれば対策もできる。前方検索を開始する前に1つ左へ移動すればよいのだ。パターン検索した後で「l」によって右に移動しているのだから、次は検索前に「h」で1つ左へ移動してから検索を開始すればよい。こうすれば同じ場所に一致し続けるという状況から抜け出すことができる。

設定としては次のようになる。「?」で前方検索を開始する前に「h」で1つ左へ移動させている。

nnoremap ( h?>[^\n]\\|.[^ \t>]<\\|[:。、?!]\\|[.,?!][ \t\n]<CR>l

この設定で実行すると次のようになる。

「(」の実行サンプル

これで意図通りに動作している。止まる場所は「)」の場合と同じなのでこれで問題ない。

「)」と同様に、このジャンプ設定もビジュアル選択モードの状態でも機能してほしい。設定は、次のようになる。

nnoremap ( h?>[^\n]\\|.[^ \t>]<\\|[:。、?!]\\|[.,?!][ \t\n]<CR>l
vnoremap ( h?>[^\n]\\|.[^ \t>]<\\|[:。、?!]\\|[.,?!][ \t\n]<CR>l

これで「(」の設定は完了だ。

「(」と「)」の設定ファイナル

これまでの取り組みをまとめると、「(」と「)」の設定は次のようになる。これで、ノーマルモード/ビジュアル選択モードにおいて「(」と「)」で自分の期待する場所へカーソルがジャンプするようになった。

nnoremap ( h?>[^\n]\\|.[^ \t>]<\\|[:。、?!]\\|[.,?!][ \t\n]<CR>l
vnoremap ( h?>[^\n]\\|.[^ \t>]<\\|[:。、?!]\\|[.,?!][ \t\n]<CR>l
nnoremap ) />[^\n]\\|.[^ \t>]<\\|[:。、?!]\\|[.,?!][ \t\n]<CR>l
vnoremap ) />[^\n]\\|.[^ \t>]<\\|[:。、?!]\\|[.,?!][ \t\n]<CR>l

これまで、「Tab」キーである程度カーソルをまとめて移動させる方法や単語単位でカーソルを移動させる方法などを取り上げてきた。「Tab」キーによる移動は比較的、活用できるのだが、分かち書きをしない日本語では単語単位での移動はなじみにくい。結局、「l」キーや「h」キーを連発してカーソルを移動していないだろうか。

そこで日本語にフォーカスしたカスタマイズ版の「(」と「)」だ。この設定ならば、意味のある場所へ確実にジャンプしてくれるので、慣れるととても便利に活用できるだろう。ここではXMLやHTMLファイルを編集する場合を想定してジャンプ先を設定したが、似たような手順で設定すれば、自分がカーソル移動したい場所へジャンプさせることができるはずだ。この設定は時間をかけるだけの見返りが見込める部分だと思うので、ぜひとも自分にマッチした設定を模索していただきたい。

ただし、パターン検索を使ってカーソルを移動させる方法は設定にも限界があることには留意が必要だ。OR検索で指定するパターンを増やせば条件を満たすのは難しくなっていくし、一致するパターンを複雑にすると指定が難解になる。可能な限りシンプルな状態にしておかないと、後で条件を変更しようとする自分が苦労することになるので要注意だ。

カーソル移動の高速化は、そのまま作業の高速化を意味している。この部分にはじっくりと時間をかけてこだわってほしい。

付録: 使っている設定ファイルとセットアップ方法

プラグインを使うためにDeinをセットアップする方法

mkdir -p ~/.cache/dein
cd ~/.cache/dein/
curl https://raw.githubusercontent.com/Shougo/dein.vim/master/bin/installer.sh > installer.sh
sh ./installer.sh .
rm ./installer.sh

本連載で使っている~/.vimrcファイル

"dein Scripts=============================
if &compatible
  set nocompatible               " Be iMproved
endif

" Required:
set runtimepath+=~/.cache/dein/./repos/github.com/Shougo/dein.vim

" Required:
if dein#load_state('~/.cache/dein/.')
  call dein#begin('~/.cache/dein/.')

  " Let dein manage dein
  " Required:
  call dein#add('~/.cache/dein/./repos/github.com/Shougo/dein.vim')

  " Add or remove your plugins here
  call dein#add('junegunn/seoul256.vim')
  call dein#add('vim-airline/vim-airline')
  call dein#add('vim-airline/vim-airline-themes')
  call dein#add('preservim/nerdtree')
  call dein#add('tpope/vim-commentary')
  call dein#add('tpope/vim-fugitive')
  call dein#add('fholgado/minibufexpl.vim')
  call dein#add('dense-analysis/ale')
  call dein#add('junegunn/fzf', {'build': './install --all'})
  call dein#add('junegunn/fzf.vim')
  call dein#add('sheerun/vim-polyglot')
  call dein#add('junegunn/vim-easy-align')

  " Required:
  call dein#end()
  call dein#save_state()
endif

" Required:
filetype plugin indent on
syntax enable

" If you want to install not installed plugins on startup.
if dein#check_install()
  call dein#install()
endif

" seoul256
let g:seoul256_background = 233
colo seoul256

" vim-airline
let g:airline_powerline_fonts = 1
let g:airline_theme = 'molokai'

" NERDTree
"  <C-o> open NERDTree
nnoremap <silent> <C-o> :NERDTreeToggle<CR>

" minibufexpl
nnoremap <silent> bn :<C-u>:bnext<CR>
nnoremap <silent> b1 :<C-u>:b1<CR>
nnoremap <silent> b2 :<C-u>:b2<CR>
nnoremap <silent> b3 :<C-u>:b3<CR>
nnoremap <silent> b4 :<C-u>:b4<CR>
nnoremap <silent> b5 :<C-u>:b5<CR>
nnoremap <silent> b6 :<C-u>:b6<CR>
nnoremap <silent> b7 :<C-u>:b7<CR>
nnoremap <silent> b8 :<C-u>:b8<CR>
nnoremap <silent> b9 :<C-u>:b9<CR>

" fzf
nnoremap <silent> fzf :Files<CR>
nnoremap <silent> ls :Buffers<CR>

" vim-easy-align
xmap ga <Plug>(EasyAlign)
nmap ga <Plug>(EasyAlign)

"End dein Scripts=========================

set number
syntax on
set whichwrap=b,s,[,],<,>,~,h,l
set cursorline
set incsearch
set hlsearch
set ignorecase
nnoremap k gk
nnoremap gk k
nnoremap j gj
nnoremap gj j
nnoremap q b
vnoremap q b
nnoremap <Tab> 15<Right>
nnoremap <S-Tab> 15<Left>
nnoremap vis h?>[^\n]\\|.[^ \t>]<\\|[。?!]\\|[.?!][ \t\n]<CR>lv/>[^\n]\\|.[^ \t>。?!]<\\|[。?!]\\|[.?!][ \t\n]<CR>
nnoremap >< /<\([^>/]\+\)><\/\1><CR>/<<CR>
nnoremap <> ?<<CR>h?<\([^>/]\+\)><\/\1><CR>/<<CR>
nnoremap ( h?>[^\n]\\|.[^ \t>]<\\|[:。、?!]\\|[.,?!][ \t\n]<CR>l
vnoremap ( h?>[^\n]\\|.[^ \t>]<\\|[:。、?!]\\|[.,?!][ \t\n]<CR>l
nnoremap ) />[^\n]\\|.[^ \t>]<\\|[:。、?!]\\|[.,?!][ \t\n]<CR>l
vnoremap ) />[^\n]\\|.[^ \t>]<\\|[:。、?!]\\|[.,?!][ \t\n]<CR>l
nnoremap "" /""<CR>l
nnoremap '' /''<CR>l
nnoremap :: hh?""<CR>l
nnoremap ;; hh?''<CR>l
nnoremap <C-a> <Home>
inoremap <C-a> <Home>
cnoremap <C-a> <Home>
vnoremap <C-a> <Home>
nnoremap <C-e> <End>
inoremap <C-e> <End>
cnoremap <C-e> <End>
vnoremap <C-e> <End>