« ^ »

Emacsの好きなところと幾つかのTips

所要時間: 約 19分

Emacsは世にあるテキストエディタの中の1つで、以前はViに並んで一時代を築いていた。現在はVisutal Studio Codeに押され、NeoVimに押され、その他のIDEに押され、一時期の勢いはないように感じる。しかし、まだまだ根強い人気があり開発も活発だ。しばらく前からEmacsはWebkitを飲み込んだため、ブラウザとして完全なるWebブラウザとしても使用できるようになった。きっとこれからも、ますますいろんなものを飲み込んで進化していくと思う。少なくとも私が死ぬまでにEmacsが死ぬ1ことはなさそうだ。今回はそんなEmacsとその拡張言語であるEmacs LispのちょっとしたTipsを紹介しながら、Emacsについて考えてみる。

なお、目次から分かると思うけれど、Emacsについて思い付いた事をとりとめなく書いているので、内容的にも雑然としているしまとまってもいない。気がついた事があったら、加筆したりしている。そういう文章になっている。

キーバインドを覚える事を諦める

キーバインドとは、このキーを押すと何が起きるかという対応の事で、いわゆるショートカットやホットキーに似ている。「コントロールキーを押しながらcを押す」でコピー、「コントロールキーを押しながらvを押す」で貼り付けといったものはショートカットだ。厳密な定義があるのか知らないけれど、キーバインドと言う時にはショートカットやホットキーよりも広い意味を持っているように思う。例えばキーコード65を入力するとAが挿入されるというのは、ショートカットとは言わないけれど、キーバインドには含むように思う。

キーの入力の表現も少し独特で、「コントロールキーを押しながらcを押す」を他のシステムでは「Ctrl+c」と表現したりするが、Emacsの場合は「C-c」と表現する。ちなみに他のシステムでは「Ctrl+c」はコピーが行なわれる事が多いが、Emacsで「C-c」はコピーではなく、モードによって呼び出される処理が異なる。

Emacsのキーバインドは更に独特で、Emacsを始めたばかりの人はこのあたりに躓く事が多い。「C-c」はコピーではないし、「C-v」は貼り付けではないから、コピーアンドペーストすら、どうやったら良いのか分からず戸惑う。極めつけはEmacsを終了する方法も分からず、強制的に終了せざるを得なくなる。GUIでEmacsを使用している場合は、「Quit Emacs」メニューが表示されているからそこから終了はできるが、 emacs -nw などでCUIでEmacsを起動している場合、メニューは表示されない。Emacsを終了したければ「C-x C-c」だ。

このようなキーバインドが無数にあるため覚えられないと思うかもしれない。実際にキーバインドは、ある程度覚える必要はあるが、決して全て覚えなければ使い熟せないという訳ではない。

Emacsのキーバインドで覚えないといけないのは M-x だけだと思う。「M」というのはメタキーの事で、キーボードによっては無い事もある。メタキーがない場合、ESC(エスケープキー)やALTが、メタキーの代りとして割り当てられている事が多い。「M-x」というのは「メタキーを押しながらxを押す」という意味だ。 M-x すると、Emacsに用意されているコマンドを指定して実行できる。コマンドは様々なものがあり、これらのコマンドがキーバインドとして設定されている。文字を入力するのも、カーソルを移動するのも、Emacsを終了するのもコマンドとして提供されている。注意したいのはこのコマンドというのは、ターミナルエミュレータで呼び出す「ls」や「pwd」のようなコマンドとは、全く異なるという事だ。「ls」や「pwd」のようなコマンドは、それ自体が実行可能ファイルとなっているプログラムだ。Emacsの言うコマンドはEmacs Lispで呼び出せる関数として定義されている。

M-x はこのEmacsの言うコマンドを呼び出す事ができる。そして呼び出す際にはコマンド名を M-x の後に指定する。コマンド名は意味のある言葉として関数名が付けられているため、やりたい事が分かればだいたい想像が付く。例えばEmacsを終了したい場合、 M-x kill-emacs とする事でEmacsを終了できる。関数名は補完できるため、 M-x kill まで入力しタブを押せば、コマンドの候補が表示される。そのコマンド名から挙動を予想できる。つまり、大切な事は自分がなにをしたいかという事をはっきり認識する事であり、覚えるべきはキーバインドではなくコマンド名という事になる。

ではキーバインドはどうかと言うと、キーバインドは自分自身の使い勝手に合わせて常に変化させる、どちらかとうと育てるようなものだと思う。そして、そういった設定がとてもやりやすいようになっている。人間をEmacsに合わせるのでなく、人間に合わせてEmacsを変化させる。更に言うと「あなた」という人間に合わせてEmacsを変化させる。人それぞれに何が良いかという基準は大きく異なる。それに合わせてEmacsを変化させる。だから、私のEmacsとあなたのEmacsは全く違うEmacsかもしれない。そういう事を大切にしているという所が、他のツールとは異なる点のように思うし、Emacsの好きな所だ。

私の好きな古いアニメの登場人物のセリフで気に入っている言葉があるので合わせて掲載する。

機械に使われたいのか、機械を使いたいのか。どっちだ?

「カウボーイ・ビバップ Session#19 ワイルドホーセス」より引用

この文章を書く以前にWeb上のEmacsに関する記事を読んでいて、それらに影響を受けていると思う。全てを覚えていないので分かる範囲で参考にした記事のリンクを掲載する。

キーバインドを設定する

Emacsにはメジャーモードやマイナーモードというものがあり、それぞれにキーマップを持っている。このキーマップは、どのキーが押されたら、どの関数を呼び出すのかという対応を持っている。全体に適応される特別なキーマップもあり、それがglobal-mapだ。まずはglobal-mapのカスタマイズから始めると良い。

キーマップをカスタマイズするには、以前は define-key 関数を使用してきたのだが、ソースコードを見てみると define-key は非推奨の古い関数となり、今は keymap-set を使用する事が推奨されるようだ。

This is a legacy function; see `keymap-set' for the recommended function to use instead.

emacs/src/keymap.cに定義されているdefine-keyのdocstring

私は keymap-set についてよく知らないので、ここでは define-key を使ってカスタマイズしていく。

例として C-h に1文字前の文字を消すというコマンドをglobal-mapに設定する事にする。多くのシステムでは、C-hを入力すると直前の1文字を削除する、所謂バックスペースキーが押されたような挙動をする事が多い。しかし、Emacsの初期のキーバインドでは C-h にヘルプを表示するコマンドが設定されているため、1文字けそうと C-h を押すたびにヘルプが表示される。私はそれほどヘルプを見たい訳ではないので、 C-h には直前の一文字を削除する backward-delete-char-untabify を割り当てる事にする。

(define-key global-map (kbd "C-h") #'backward-delete-char-untabify)
C-hで直前の一文字を削除できるようにする

このEmacs Lispを評価すると、C-hで直前の一文字を削除できるようになる。 define-key の第1引数は変更するキーマップである global-map 、第2引数は割り当てるキー (kbd "C-h") 、第3引数は呼び出すコマンドのシンボル #'backward-delete-char-untabify を指定する。

基本的にはこれだけだ。後はどのキーマップを変更するのか、どのキーに割り当てるのか、どのコマンドを割り当てるのかを変更していけばよい。このEmacs Lispを評価すると、設定は即座に反映される。ただし、それはそのEmacsが起動している間だけだ。毎回この設定を維持したいのであれば、Emacsの起動時に読み込まれるように .emacs.d/init.el にでも記述すると良いだろう。

キーバインドは、自分の成長に合わせてEmacsを変化させるものだと思う。Emacsはその自分の成長の速度に合わせてくれる。

コマンドを作る

キーバインドの変更方法は分かったが、割り当てたいコマンドが必ずあるとは限らない。むしろ、いや、きっと割り当てたいコマンドはない。本当に割り当てたいコマンドというのは心の中にある。だから心の声を聞いて自分で実装する。心の声を聞くのは難しいけれど、実装は簡単だ。コマンドを定義するにはinteractiveスペシャルフォームを使う。このスペシャルフォームを関数定義内で呼び出しておくと M-x を使って、定義した関数を呼び出すことができる。例えば次のような関数を定義したとする。

(defun testing ()
   (interactive)  ;; <- これを記述している関数は、コマンドとして扱われる。
   (print "Ok"))
testingコマンドを定義する。

この関数は内部で interactive を呼び出しているため M-x testing と入力することで関数を実行できる。Emacsではこのような関数の事をコマンドと呼んでいる。

もしこのコマンドを C-h に割り当てたいと思ったら、当然次のようなEmacs Lispになる。

(define-key global-map (kbd "C-h") #'testing)
ただただC-hでtestingを呼び出す無意味な設定

このinteractiveスペシャルフォームだが引数を渡すことができる。これが結構種類があって複雑で分かりにくい。実は説明はコード内にドキュメンテーション文字列として記載されているのだが、ここではそれを元に自分用にまとめることにした。もし元の情報を見たければEmacsのsrc/callint.cに記載がある。

引数に渡す文字説明
a関数名: 関数を定義したシンボル
bバッファ名
Bバッファ名、ただし存在しないバッファ名も指定可能
c文字、インプットメソッドを使用しないためマルチバイト文字の入力はできない
Cコマンド名、インタラクティブな関数定義を持つシンボル
d数値としてのポイントの値、入力はできない。
Dディレクトリ名
eこのコマンドを呼び出したパラメーター化されたイベント
fファイル名
Fファイル名、ただし存在しないファイル名も指定可能
Gファイル名、ただし存在しないファイル名も指定可能であり、デフォルトはディレクトリ名
i無視。つまり、常にnilであり、入力はできない
kキーシーケンス、定義を取得する必要がある場合は、最後のイベントを小文字にする
K再定義するキーシーケンス 最後のイベントを小文字にしない
m数値としてのマークの値。 入力はできない
M任意の文字列。現在の入力方法を継承する
nミニバッファを使用して数値を入力する
N数値プレフィックス arg、またはない場合はコード `n' と同様
p数値に変換されたプレフィックス arg、 入力はしない
P生の形式で接頭辞 arg を付けます。 入力はしない
rリージョン: 2 つの数値引数としてポイントおよびマークを付けます。小さい方が先になります。 入力はしない
s任意の文字列。現在の入力方法を継承しない
S任意のシンボル
U前の k または K 引数によって破棄されたマウスアップ イベント
v変数名: 「custom-variable-p」であるシンボル
x式を読み取る、ただし評価はしない
X式を読み取り評価する
zコーディングシステム
Zコーディングシステム、接頭辞 arg がない場合は nil

マークしたリージョンの文字列になんらかの処理を行う

(interactive "r") を用いるとリージョンに指定したポジションをコマンド実行時に取得できる。選択した領域の文字列に対して、何らかの処理を行いたい状況にしばしば出会う。そんな時にこの機能を使って即席のコマンドを定義すると、自由度が増す。

以下では例として選択した領域の文字列をprintする。

(defun testing-region (&optional beg end)
  (interactive "r")
  (print (buffer-substring-no-properties beg end)))

この文字列をbase64でエンコードしたければ次のようになる。

(require 'base64)

(defun testing-region (&optional beg end)
  (interactive "r")
  (print (base64-encode-string (buffer-substring-no-properties beg end))))

他にもさまざまな使い方ができるだろう。

HTTPクライアント

EmacsにはHTTPクライアントとして動作する機能は幾つかある。例えば、 url-retrieverequest.elplz.el や、趣向を変えて http.elrestclient.el といったものもある。これらについてEmacsのHTTPクライアント事情について考えるにまとめた。

マシンをスリープ状態にする

Emacs上からマシンをスリープ状態にする。現状ではmacOS10.9以降のみ対応している。

(defun sleep-machine-system-command ()
  (pcase system-type
    ('darwin '("pmset" "sleepnow"))
    (t nil)))

(defun sleep-machine ()
  (interactive)
  (if-let ((cmds (sleep-machine-system-command)))
      (apply #'call-process (car cmds) nil nil nil (cdr cmds))
    (error "Failed to sleep machine: Not support sysmte type")))

Emacsでtimestampを取得する

(float-time)
1641478725.882611

少数を切り捨てる

(truncate (float-time))
1641478716

日付のフォーマットをUNIXタイムスタンプに変換する

(truncate
 (float-time (date-to-time
	      "2020-10-11T21:36:21+0900")))
1602419781

連想リストをキーでソートする

(sort
 '((foo . 3)
   (bar . 1)
   (baz . 2))
 (lambda (one two)
   (string< (car one)
	    (car two))))
((bar . 1) (baz . 2) (foo . 3))

特定の領域を編集不可にする

編集不可にしたいバッファ上の文字列に'read-only属性を付加する。

(set-text-properties 1 10 '(read-only t))

カレントバッファの1文字目から10文字目までは、これでもう編集できない。

何かしらの文字を入力後、入力を確定した後でその部分を編集不可にしてみる。編集不可の部分は背景色を変更する。

(put-text-property (point-min) (point) 'read-only t)

(goto-char (point-max))

ここは入力ができる。

(get-text-property (point-max) 'read-only) (set-text-properties (point-min) (+ 10 (point-min)) '(background-color red)) (remove-text-properties (point-min) (+ 10 (point-min)) '(read-only t)) (remove-text-properties (point-min) (+ 10 (point-min)) '(read-only t) (current-buffer))

package-selected-packagesへの要素の追加と削除

package-selected-packagesはpackage.elで管理している値で、package-installなどでパッケージをインストールするとパッケージのシンボルが追加される。通常その値はcustom-set-variablesなどに自動で設定される。以下のように直接操作することは基本的にはない。

(package--save-selected-packages
 (cons 'testing package-selected-packages))
追加
(package--save-selected-packages
 (remove 'testing package-selected-packages))
削除

オーバーレイ

例えばファイルに関連付いているバッファには、そのファイルのデータが表示されている。そのバッファ上で様々な作業を行なっていると、ファイルには保存したくないが表示させておきたい情報があったりする。例えば、注釈や変換候補や構文エラー箇所の表示などがそれに当たる。こういった情報を表示させる機構の一つがオーバーレイだ。オーバーレイはバッファの任意の位置に一時的に表示したり、削除したりできる。

オーバーレイを使う

ここでは左側フリンジにマークを表示している。

(overlay-put
 (make-overlay (point) (point))
 'after-string
 (propertize "X" 'display `(left-fringe right-arrow info)))

オーバーレイを消す

カレントバッファに表示されているオーバーレイをけす。

(remove-overlays)

flymake

最近ではVSCodeが流行っていたりするし、LSPが広まったことによって、コードのエラーチェックやフォーマットといったものもLSP任せにすることが多くなってきているように思う。例えばPythonでMyPyを使ったチェックを行う場合、pylspとpylsp-mypyを用いて行う。pylsp-mypyはpylsp用のプラグインとして実装されている。そのためこれを使うためにはpylspとpylsp-mypyとmypyを知っていないと使えないことになる。これは大変だ。楽になるように進化したはずなのに全然楽になっていない。むしろ覚えることが増えて、間に存在するものが増えたため、もう何がなんなのかわからない。この手のツールはできるだけシンプルな構成で実装されていることが望ましい。コマンドを実行し、標準出力にエラーが表示される。その標準出力をパースし、エラーをマークする。これで十分なはずだ。Emacsにはこの手の作業をするために、flymakeというものがある。flycheckというサードパーティのものもあるが、flycheckはflymakeを進化させようとした結果、独自路線を歩んだように思う。僕はEmacsに標準でインストールされているflymakeを使って、一番基本的なエラー通知だけを利用したいと思った。ここでは、とても簡単なflymakeのバックエンドを実装する。何もチェックせず、バッファに色を付け、メッセージを表示するだけだ。

flymakeにはflymake-modeというマイナーモードが定義されている。これが有効な時はflymakeが実行される。実行されるタイミングも制御できるが、ここでは触れないことにする。flymake-modeが有効になっていると、呼び出しのタイミングに従い flymake-diagnostic-functions に登録されている関数を呼び出す。つまり flymake-diagnostic-functions にチェック用の関数を登録すればよい。各種言語のモードのフックとして関数を登録する方法が良く用いられる。

ここでは testing-flymake という関数を定義し、この関数を flymake-diagnostic-functions に登録することにする。

testing-flymake はflymakeによって呼び出される。その時いくつかの引数が渡される。第1引数はREPORT-FNで、この関数によってバッファに対する色付けなどが行なわれる。この引数は flymake--diag 構造体のリストを受けとる。 flymake-make-diagnostic を用いることで、この構造体を簡単に作成できる。対象のバッファ、エラーの開始位置、終了位置、エラー種別、メッセージなどを引数に渡すことができる。

testing-flymake は特にチェックする対処はないので、カレントバッファの10の位置から20の位置に種別をエラーとして「yay!!」というメッセージを表示させることにする。

(defun testing-flymake (report-fn &optional _args)
  (let ((diag (flymake-make-diagnostic (current-buffer) 10 20 'ERROR "yay!!")))
    (funcall report-fn (list diag))))

ではこの関数をflymakeに登録してみる。

(setq flymake-diagnostic-functions '(testing-flymake))

カレントバッファの先頭10の位置から20の位置まで、エラーがマークされただろう。

実際に利用する際には、何かしらの処理を行い、flymake–diagのリストを生成すればよい。何かしらの処理というのは、例えばリント用のコマンドを実行し、その出力をパースしたりする。

ディレクトリツリーの上方向にファイルを探索する

.gitがあるか、READMEがあるかなど、ディレクトリツリーの上方向に特定のファイルを探索して、そのプロジェクトのルートとなるディレクトリを特定する。projectileには projectile-acquire-root という関数が用意されており、それで特定できる。それを使用してもよかったが、よく考えてみると、そもそもそんなに特別なことを行わないはずであるので、そのためだけに projectile に依存するのは好ましくない。また何をもってルートディレクトリとするかは、文脈によって変わる。例えば、モノレポ構成をしている複数のサブシス化されたリポジトリがあるとする。この時のルートディレクトリはサブプロジェクトのルートディレクトリなのか、それとも最上位のディレクトリなのか、どちらが良いかは状況により異なるだろう。そのため、どのような判定のロジックにするかは、外部から与えられたほうがよい。Emacs Lispであるので、外部から一時的に関数の挙動を変更することも可能ではあるが、適切に判定部分と探索部分は分けたほうがよいと考え、車輪を再発明することにした。

(defun traverse-directory-to-up (target-directory predicate-fn-list)
  (let ((cwd (file-name-directory (expand-file-name target-directory))))
    (if (seq-some (lambda (predicate-fn)
		    (funcall predicate-fn cwd))
		  predicate-fn-list)
	cwd
      (traverse-directory-to-up
       (string-remove-suffix "/" cwd) predicate-fn-list))))

(defun traverse-directory-exist-readme-md-p (cwd)
  (file-exists-p (file-name-concat cwd "README.md")))

(defun traverse-directory-super-root-p (cwd)
  (string-equal "/" cwd ))

探索の最初のディレクトリとルート判定関数のリストを引数に渡す。

(traverse-directory-to-up
 "/foo/bar/baz/"
 '(traverse-directory-exist-readme-md-p
   traverse-directory-super-root-p))

このコードは各所で使用したいからrequireできるようにした。

PDF

EmacsはPDFも扱える。ただ表示するだけであれば、特に気にすることもなく普通にファイルを開けば、PDFが表示される。ただ、おそらくそれだけでは、きっと不満を感じる状態だと思う。なかなか癖があり、不満を解消していくためには通常以上にEmacsに "水やり" をする必要がある。もしかしたら、その不満を解消できず、ADOBE Acrobat Readerのような高機能なPDFビューアを使うことも選択する人もいるかもしれない。それはそれで良いと思うし、それぞれの選択を尊重したい。それでもなお、EmacsでPDFを操作する事に価値を感じる人には、手前味噌ではあるが「EmacsでPDFを扱う」が参考になるかもしれない。内容は主にpdf-toolsを使用してPDFの操作性を改善しようと四苦八苦している様子だ。ここに記述しても良かったのだが、内容があまりに長くなるため分割した。

DoctorまたはELIZA

Emacsには対話形式のセラピーを提供するdoctorが梱包されている。心理カウンセラーのような対話をしてくれる。ELIZAと呼ばれる機能なのだが、ChatGPTに対応させた。

Emacsの対話セラピー機能doctorをChatGPTに対応させる

SQLを編集しながらEXPLAINする

SQLを編集しながらEXPLAINするためのプラグインを書いた。

SQLを編集しながらEXPLAINする

Emacsの柔軟さ

Emacsは他のエディタとは比較にならない程、柔軟で拡張性の高いエディタだ。その性質をもたらしている理由の1つは、なんと言ってもEmacs Lispの柔軟性だろう。なかなか上手くこの自由さや開放感を文章で伝えることは難しいが、さまざまな利用方法を記述することでその一端でも感じとってもらえると嬉しい。

もちろん多くのエディタが同様の機能を実装できる。ただその機能を実装するまでに辿る過程を考えると、ファイルに記述する必要もなく、ただ単純に関数を書いてそれを評価すれば挙動が変更されるというのは他のエディタにもなかなかないだろう。Emacs Lispには厳密な名前空間もない。それは一見デメリットのようにも感じられる。似たようなものとして挙げるとするとCSSもそうだろう。きちんとした名前空間がないと、パッケージの仕組みが上手く機能せず、グローバルな領域に様々なシンボルがごちゃまぜの状態で定義されるようにも思える。CSSはそのためにBEMやSMACSSといった命名規則でその問題を解消しようとした。Emacs Lispもそれと良くにている。Emacs Lispには requireprovidedefgroup といった関数やマクロが用意され、命名規則のみではないパッケージと名前空間の機構を導入している。ただし定義された関数や多くの変数はあらゆるところから参照でき、また様々な形で上書きできる。この一見カオスなように感じる状況も、Emacsの挙動を柔軟に変更したい場合にはとても重宝する。気に入らない関数があれば、その関数をスクラッチバッファにコピーし書き換えて評価することで、上書きしてしまえばよい。ダイナミックスコープなのかレキシカルスコープなのかも選択できる。意図せずバグを踏むこともあるが、その代わりに処理の外から柔軟に挙動を変更していける。Emacs Lispよりも名前空間をはっきりさせているPythonのような言語でEmacsが記述されていて、拡張のためにPythonを用いるようなことがあるとすると、きっとこのようにはいかない。名前空間の下に存在する関数を書き換えるのは結構手間だ。Emacsの柔軟さはこのEmacs Lispの仕様によって支えられているように思う。


1

Emacsはこれまで何度も死んだという内容のWeb記事が投稿されている。人気のある(もしくはあった)技術トピックはそういうものから逃げられないのかもしれない。あくまでネタであり、真に受けたり、目くじら立てて怒ったりせず、エンターテインメントとして楽しむと良い。夏の風物詩のようなものだ。私もそのようなと記事がとっても好きだ。くだらなくて、面白くて、少しノスタルジーを感じ、そしてちょっと考えさせられる。そんな文章がもっとたくさん生産される世の中になれば良いと思っている。