VimのCtrl-oで前の位置に戻る(ジャンプリストと:jumps / g;)
長いファイルでGや検索を使って飛んだあと、さっきまで見ていた場所に戻りたくなることはよくあります。そのときに押すのがCtrl-oです。Vimは「ジャンプ」と呼ばれる種類の移動をするたびに、飛ぶ前の位置をウィンドウごとのリストに積んでいて、Ctrl-oはそれを1つずつさかのぼります。このリストがジャンプリストです。
ただ、どの移動がジャンプとして記録されるかは見た目では分かりません。:42で42行目に行ってからCtrl-oを押しても、42行目に来る前の位置には戻りません。この記事では、移動キーを1つずつ打ってジャンプリストに載るかを実機で確かめ、戻ってから別の場所へ飛んだときの履歴の扱い(jumpoptions)、ファイルをまたぐ場合、編集した場所をたどる変更リストまでを並べます。
検証環境:Vim 9.1.1752(macOS付属)とNVIM v0.12.4(macOS)。tmuxの中で設定ファイルを読まずに起動し(Vimはdefaults.vimだけ、Neovimは--clean)、101行のCファイルで:clearjumpsしてから移動キーを1つ打ち、getjumplist()と:jumpsで記録の有無を見ました。掲載している一覧はすべて実行結果です。
目次(9項目)
| キー | これだけ覚える |
|---|---|
| Ctrl-o | ジャンプリストを1つ前(古い側)へ戻る |
| Ctrl-i | 1つ先(新しい側)へ進む。ターミナルではTabと同じキー |
:jumps | ジャンプリストの一覧と現在位置を見る |
| g; / g, | 編集した場所を古い順・新しい順にたどる |
:changes | 変更リストの一覧を見る |
Ctrl-oで戻る、Ctrl-iで進む
1行目にいる状態から21G、41G、61G、81Gと4回飛ぶと、ジャンプリストには1・21・41・61行目の4つが入ります。81行目は「いまいる場所」なのでまだ載っていません。ここでCtrl-oを押すと、Vimはまず現在位置の81行目をリストの末尾に足してから61行目へ戻ります。2回押せば41行目です。この状態でCtrl-iを押すと61行目へ、もう一度押すと81行目へ進みます。ブラウザの戻る・進むと同じ形だと考えて差し支えありません。
どちらもカウントを取ります。3Ctrl-oなら3つ前まで一気に戻ります。何個前に戻ればよいかは、次の:jumpsの一覧を見れば数えずに済みます。
似た操作に''(シングルクォート2つ)があります。こちらは直前のジャンプ元との間を行き来するだけで、リストをさかのぼることはできません。1行目からGで101行目へ飛び、''を押すと1行目へ戻ります。''そのものもジャンプとして記録され、このときリストには1行目と101行目の2つが入りました。マークを使った戻り方はVimのマーク機能で好きな位置にジャンプする方法で詳しく扱っています。
:jumpsの一覧の読み方
先ほどの4回のジャンプのあとCtrl-oを2回押した状態で:jumpsを打つと、Vim 9.1では次のように出ます。
jump line col file/text
2 1 0 #include <stdio.h>
1 21 4 count = count + 21; /* line 21 (paren) */
> 0 41 4 count = count + 41; /* line 41 (paren) */
1 61 4 count = count + 61; /* line 61 (paren) */
2 81 4 count = count + 81; /* line 81 (paren) */
左端の>がいまの位置です。jump列の数字は「そこへ行くのに付けるカウント」で、>より上の行はCtrl-oの、下の行はCtrl-iのカウントになっています。上の例で1行目へ戻りたければ2Ctrl-o、81行目へ進みたければ2Ctrl-iです。>が一覧の末尾に単独で出ているときは、まだ一度もCtrl-oを押していない状態を表します。
lineは行番号、colは0から数えた桁です。同じ手順でもNeovim 0.12.4ではcolがすべて0になりました。Gで行の先頭の空白を飛ばすかどうかを決めるstartoflineの既定値が、Vimでは1、Neovimでは0だったためです。最後のfile/textは、同じファイルならその行の中身、別のファイルならファイル名が出ます。
記録される移動と記録されない移動
移動キーを1つ打つたびに:clearjumpsでリストを空にし、打ったあとリストに1件増えたかどうかを見ました。結果は次のとおりです。
| 移動 | Vim 9.1 | Neovim 0.12.4 | 補足 |
|---|---|---|---|
| G / gg / 42G | 記録 | 記録 | 行番号を指定するならこちら |
:42 | されない | されない | Exコマンドでの行移動はジャンプではない |
| 50% | 記録 | 記録 | ファイルの半分の位置へ |
| / / n / N | 記録 | 記録 | |
| * / # | 記録 | 別の行へ動いたときだけ記録 | 同じ行の中の移動で差が出る |
| % | 記録 | 別の行へ動いたときだけ記録 | 同上 |
| { / } / ( | 記録 | 記録 | 段落・文の単位の移動 |
| gd | 記録 | 記録 | 宣言へ飛んで戻るのに使える |
| H / M / L | 記録 | 記録 | 画面内の移動でも記録される |
| '' | 記録 | 記録 | |
| j / 5j / w / f( | されない | されない | カウントを付けても同じ |
| Ctrl-d / Ctrl-f / Ctrl-e | されない | されない | スクロールはジャンプではない |
いちばん引っかかりやすいのは:42です。:help jump-motionsに並んでいるジャンプのコマンドに:42のような行番号だけのExコマンドは入っておらず、実際に2つのエディタとも記録しませんでした。エラーメッセージの行番号へ:42で飛び、直したあとCtrl-oで戻ろうとすると、その前のジャンプ元まで戻ってしまいます。戻る前提なら42Gを使うか、飛ぶ前にm'を打っておきます。m'のあとに:42で飛ぶと、リストに元の1行目が入り、Ctrl-oで戻れました。逆に記録したくない移動は:keepjumps normal! 80Gのように:keepjumpsを付けると、リストは空のままでした。
一方、H・M・Lは画面の中で動くだけなのに記録されます。5jとLで同じくらい動いても、Ctrl-oで戻れるのはLのほうだけです。Ctrl-dやCtrl-fでページ単位に送った場合も記録されないので、スクロールで読み進めた先からCtrl-oを押すと、スクロールを始める前ではなく、その前のジャンプ元へ戻ります。スクロールの各キーはVimの画面スクロールとジャンプ操作の記事にまとめています。gdで宣言へ飛んでCtrl-oで呼び出し側へ戻る使い方は、変数の宣言位置へジャンプする(gd/gD)の続きとしてそのまま効きます。
VimとNeovimで結果が分かれたのは、*や%で同じ行の中を動いたときです。count = count + 51;の先頭のcountで*を押すと、同じ行の2つ目のcountへ移ります。Vimはこれを記録し、Ctrl-oで元の桁へ戻りました。Neovimは記録せず、Ctrl-oを押しても桁は動きませんでした。(paren)の括弧で%を押した場合も同じです。Neovimのヘルプ:help vim_diffには、Vimとの違いとして次の1行があります。
The jumplist avoids useless/phantom jumps.
同じ行の中での移動は、Neovimでは「意味の無いジャンプ」として扱われるということです。行をまたぐ*(51行目から52行目へ)は両方とも記録しました。
戻ってから別の場所へ飛ぶと、先の履歴はどうなるか
Ctrl-oで途中まで戻ったところから新しくジャンプすると、戻った先より新しい履歴の扱いがオプションjumpoptionsで変わります。上の:jumpsの状態(41行目まで戻った位置)から91Gで飛んだ結果を並べます。まずVim 9.1の既定(jumpoptionsは空)です。
jump line col file/text
5 1 0 #include <stdio.h>
4 21 4 count = count + 21; /* line 21 (paren) */
3 61 4 count = count + 61; /* line 61 (paren) */
2 81 4 count = count + 81; /* line 81 (paren) */
1 41 4 count = count + 41; /* line 41 (paren) */
>
61行目と81行目は残り、ジャンプ元の41行目がいったんリストから外れて末尾に付け直されています。同じ行は1つしか持たないので、何度Ctrl-oを押しても同じ場所を二度通ることはありません。この状態で3Ctrl-oを押すと、一覧の3の行である61行目へ戻りました。
次に:set jumpoptions=stackにして同じ手順を踏んだ結果です。
jump line col file/text
3 1 0 #include <stdio.h>
2 21 4 count = count + 21; /* line 21 (paren) */
1 41 4 count = count + 41; /* line 41 (paren) */
>
41行目より先にあった61行目と81行目が捨てられました。タグジャンプのスタックと同じ動きで、「戻ったところから別の枝へ進んだら、元の枝の先は要らない」という考え方です。3Ctrl-oは1行目へ戻ります。
Neovim 0.12.4の既定値はcleanですが、この手順の結果はVimの既定と1行も違いませんでした。cleanは「読み込まれていないバッファをリストから外す」設定で、ファイルを1つしか開いていない場面では効きません。stackにしたNeovimの結果もVimのstackと同じでした。Neovimではview(戻ったときに画面上の表示位置も戻す)も指定でき、:set jumpoptions=view,stackのように並べられます。Vim 9.1.1752で:set jumpoptions=viewを打つとE474: Invalid argument: jop=viewになり、Vimで使えるのはstackだけでした。
ファイルをまたいで戻る
ジャンプリストはファイルをまたぎます。c.txtの2行目にあるb.txtという文字列の上でgfを押してそのファイルを開き、3Gで3行目へ行ってから:e a.cで別のファイルを開くと、:jumpsは次のようになりました(VimとNeovimで同じ)。
jump line col file/text
5 1 0 c.txt
4 2 0 c.txt
3 1 0 b.txt
2 3 0 b.txt
1 1 0 #include <stdio.h>
>
gfで開いたときも:eで開いたときも、開く前の位置が記録されています。ここからCtrl-oを押していくと、a.cの1行目、b.txtの3行目、b.txtの1行目と、ファイルを切り替えながら戻りました。gfで設定ファイルを渡り歩いたあと、最初のファイルに戻る用途にはこれで足ります。gfそのものの使い方はカーソル下のパスをそのまま開くgfの記事にあります。
VimとNeovimで差が出たのは、開いたファイルを閉じたあとです。a.cにいる状態で:bdelete b.txtを実行すると、Vimではb.txtの行がリストに残り、Ctrl-oを押すと閉じたはずのb.txtがもう一度開きました。Neovimではb.txtの行がリストから消え、Ctrl-oはa.cの中で戻りました。これがjumpoptionsのcleanの効き目です。:bwipeout b.txtでバッファごと消した場合は、VimでもNeovimでもb.txtの行が消えました。Vimで「閉じたファイルがCtrl-oで勝手に開く」のを避けたいなら、:bdeleteではなく:bwipeoutを使うことになります。
編集した場所へ戻る変更リスト
ジャンプリストとは別に、Vimは編集した位置も記録しています。これが変更リストで、g;で古い側へ、g,で新しい側へたどります。11行目・31行目・71行目の行末に文字を足してから3行目へ移り、:changesを打った結果です。
change line col text
3 11 45 count = count + 11; /* line 11 (paren) */ X
2 31 45 count = count + 31; /* line 31 (paren) */ Y
1 71 45 count = count + 71; /* line 71 (paren) */ Z
>
読み方は:jumpsと同じで、change列がg;に付けるカウントです。g;を1回押すと71行目、もう1回で31行目、g,で71行目へ戻りました。古い側の端を越えてg;を押すとE662: At start of changelistが出ます。リストの中の位置は覚えられていて、g,で71行目に戻った状態から3Gでよそへ移り、2g;を押すと、71行目から2つ古い11行目へ移りました。
最後に変更した位置だけでよければ、マークの`.で一発で戻れます。上の例では`.で71行目の変更した桁へ移りました。違いはジャンプリストへの影響で、Vimで試すと`.はジャンプとして記録され(リストが4件から5件に増えた)、g;とg,は何度押しても増えませんでした。変更リストを行き来してもCtrl-oの履歴は散らかりません。
Ctrl-iが効かないときはTabのマッピングを疑う
Ctrl-oは効くのにCtrl-iで進めない場合、ノーマルモードのTabに何かを割り当てていないかを確かめます。ターミナルの中では、Ctrl-iを押してもTabを押しても同じ文字コードが届くためです。
VimとNeovimでnnoremap <Tab> :echo 'TAB-MAP'<CR>を設定し、ジャンプリストを作ってCtrl-oで戻ってからCtrl-iを押すと、どちらも前へ進まずにTAB-MAPと表示しただけでした。Vimではここにnnoremap <C-i> <C-i>を足すと、maparg('<Tab>', 'n')の中身も<Tab>に置き換わりました。TabとCtrl-iは1つのキーとして扱われ、後から定義したほうが前のマッピングを上書きします。その結果Ctrl-iは51行目へ進むようになり、代わりにTabの割り当ては消えました。
Neovimは2つを別のキーとして登録できます。同じ順で定義しても<Tab>のマッピングは残り、maparg()で見ると2つが別々に入っていました。ただし、tmuxの中でCtrl-iを押したときに実際に動いたのはTabのほうのマッピングで、ジャンプリストは進みませんでした。キーを区別できるのは、ターミナルがCtrl-iをTabとは別の符号で送ってくる場合に限られます。そうした拡張キー入力に対応したターミナルでは試していません。手元のターミナルでTabをバッファ切り替えなどに使いたいなら、Ctrl-iを諦めるか、Tabの割り当てを別のキーへ移すかのどちらかになります。:nmap <Tab>で何も出なければ、この問題ではありません。
表を作る途中で、Neovimだけ*の結果が0件になり、しばらくはキーの送り方を間違えたのだと思って何度も打ち直していました。行を変えて試したら記録されたので、そこで初めて「同じ行の中だから」だと気づきました。もう1つ混乱したのは:changesで、Vimで試したときに打った覚えのない19行目や39行目の変更が並び、前の試行で付けた変更が.viminfoに保存されて次の起動で読み込まれていたのでした。変更リストを使っていて覚えのない位置に飛ばされたときは、前回の起動の履歴を疑うと早いです。
まとめ
Ctrl-oで戻れるのは、G・検索・%・{}・gd・HMLのような「ジャンプ」で飛んだときだけです。:42、hjkl、w、f、スクロールは記録されないので、戻る前提で行番号へ飛ぶなら42Gを使います。どこまで戻ればよいか迷ったら:jumpsを開き、jump列の数字をカウントとしてCtrl-oかCtrl-iに付けます。
途中まで戻ってから別の場所へ飛んだとき、既定では先の履歴が残り、jumpoptions=stackにすると捨てられます。VimとNeovimの差は、同じ行の中のジャンプを記録するかどうかと、閉じたバッファをリストに残すかどうかの2点でした。編集した場所へ戻るなら変更リストのg;とg,を使い、Ctrl-iが効かないときはまずTabのマッピングを確かめます。