【Emacs】
C-n が表示行でなく論理行で動くように
なってしまった
(visual-line-mode)

関連記事

1. C-nが飛ぶ?

Markdown を書いていたら、突然 C-n の動きがおかしくなりました。

長い段落の途中にカーソルがあって C-n を押すと、画面上の次の行ではなく、遠く離れた行まで飛んでしまいます1
短い行を書いているときには起きないのに、折り返しが多い長い段落でだけ発生する。

直前まで init.el を触っていたので、何かを変えてしまったらしい。

1.1. 論理行と表示行

調べると最初に出てくるのが「論理行」という概念です。

Emacs の C-nnext-line というコマンドで、デフォルトでは表示行を単位に移動します。
画面の幅に合わせて折り返されて見えている行は「表示行」と呼びます。

一方、論理行とは、改行文字 \n で区切られた行のことです。
Markdown で長い段落を書くとき、意味のある区切りまで改行を入れないスタイルだと、1段落が1論理行になります。
たとえば、画面上では5行に見えていても、改行が1つしかなければ論理行としては1行です。

論理行基準になると、C-n はその1行を一気に飛び越えてしまいます2
これが「飛ぶ」感覚の原因です。

2. global-visual-line-mode

表示行単位で移動したいなら visual-line-mode を使います。

設定を変更するには、

(global-visual-line-mode 1)Code language: Lisp (lisp)

これを init.el に書いて再読み込みすると、C-nC-p が画面上の折り返し行を基準に動くようになり、長い段落の中でも1行ずつ移動できます3

もしかすると、C-n が突然おかしくなったのは、おそらくeval 操作の副作用で (global-visual-line-mode) が引数なしで呼ばれ、トグルが発動したためだと思われます4

2.1. モードに限定することもできる

プログラムを書くバッファでは、論理行移動の方が都合がいい場面も多くあります。

コードの1行は概ね短く、折り返しが問題になりにくいからです5

Markdown など文章を書くモードだけに限定したい場合はこちらでもよいです。

(add-hook 'markdown-mode-hook #'visual-line-mode)
(add-hook 'text-mode-hook #'visual-line-mode)Code language: Lisp (lisp)

3. 1 の意味

設定を書きながら、ふと気になりました。

なぜ 1 なのか。
Lispでは、通常 真偽値として tnil を使います。

Emacs のマイナーモード関数は、数値の引数で動作を決めます。

(global-visual-line-mode 1)   ; 有効化
(global-visual-line-mode -1)  ; 無効化
(global-visual-line-mode 0)   ; 無効化
(global-visual-line-mode)     ; 現在の状態を反転(トグル)Code language: Lisp (lisp)

正の数を渡すと有効、0または負の数を渡すと無効になります。
引数なしだとトグル、つまり現在の状態を反転させます6

3.1. なぜ t / nil ではないのか

マイナーモード関数が数値の符号を使うのは、対話的な操作との整合性のためです。

Emacs では C-u による「前置引数」でコマンドの動作を変えられます。
前置引数とは、コマンドを実行する前に数値を渡す仕組みのことです。

C-u 1 M-x global-visual-line-mode   ; オン
C-u -1 M-x global-visual-line-mode  ; オフCode language: PHP (php)

この仕組みと同じインターフェースをコードからも使えるよう、マイナーモード関数は数値引数を受け取る設計になっています7

t を渡しても多くの場合オンとして動きますが、数値を使う方が「反転ではなく明示的な有効化」として読みやすくなります。

  1. 厳密には、Emacs の C-nC-p はデフォルトから表示行を単位に移動する特例的なコマンドです。ただし visual-line-mode が無効な状態では、折り返しのない短い行と論理行が一致するため、違和感なく使えます。 – Continuation Lines (GNU Emacs Manual)
  2. グラフィック表示では、論理行が折り返されている箇所にウィンドウの左右フリンジに小さな折れ曲がり矢印が表示されます。テキスト端末では右端に \ が表示されます。 – Continuation Lines (GNU Emacs Manual)
  3. visual-line-mode を有効にすると、C-n/C-p だけでなく C-a(行頭)、C-e(行末)、C-k(行末まで削除)も表示行を基準に動くよう再定義されます。 – Visual Line Mode (GNU Emacs Manual)
  4. eval-regioneval-buffer で init.el の一部を再評価すると、すでにオンになっているモードに対してトグルが当たり、意図せずオフになる場合があります。これは Emacs のマイナーモードが対話的呼び出しでは引数なしをトグルとして扱う仕様によるものです。 – Minor Modes (GNU Emacs Manual)
  5. visual-line-mode が有効な状態でも、M-x next-logical-lineM-x previous-logical-line で論理行単位の移動ができます。頻繁に使うなら独自のキーバインドを割り当てることも可能です。 – Visual Line Mode (GNU Emacs Manual)
  6. Emacs Lisp から引数なしで呼び出した場合は nil として扱われ、トグルではなく無条件にオンになります。トグルになるのは対話的に(M-x やキーバインドから)引数なしで呼んだときだけです。 – Minor Mode Conventions (GNU Emacs Lisp Reference Manual)
  7. この挙動は define-minor-mode マクロが自動生成するコードで実装されています。マクロが生成する関数の内部では (> (prefix-numeric-value arg) 0) という判定で引数の正負を確認しています。 – Defining Minor Modes (GNU Emacs Lisp Reference Manual)