« ^ »

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

2023/3/31 更新
約 13分 で読める

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

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の仕様によって支えられているように思う。

interactiveの使い方

Emacsのコマンドを実装するにはinteractiveスペシャルフォームを使う。このスペシャルフォームを関数定義内で呼び出しておくと M-x を使って、定義した関数を呼び出すことができる。例えば次のような関数を定義したとする。

(defun testing ()
   (interactive)
   (print "Ok"))

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

この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))))

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

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

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

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して欲しくなる。アイドルタイマーとフック、一時バッファとしてサードパーティパッケージであるedit-indirectを利用し、SQLのEXPLAINを確認できるようにした。ソースを置いておく。パッケージで配布する程度のものではないのでMELPAには上げていない。使用する時は、このEmacs Lispをダウンロードして、loadするか、ファイルを開いてeva-bufferなどする。

https://github.com/TakesxiSximada/emacs.d/blob/main/lisp/sql-focus.el

こういった処理は、それぞれのエディタで用意されているが、何がどうなって欲しいかは、その時々で結構異なる事が多い。「こうしたいな」というイメージが浮んだ時に、すぐにエディタを拡張し、やりたいことを実現できる所がEmacsの良い所だと思っている。

脚注


1

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


©TakesxiSximada
しむどん三度無視 により 2020/10/11 に投稿、2023/3/31 に最終更新
« ^ »