Monthly Archives: 1月 2024

2024-01-31

Emacsでdiffの文字化けを回避する(様々な文字エンコーディングに対応する)

何だか時代錯誤感のあるタイトルで申し訳ないのですが、私は長年Emacsを使っていてもdiffやらgrepやら基本的なコマンドの使い方が分かっていない人間なのです。ご容赦ください。grepの方は最近はripgrepの登場で大分マシになりましたが。いや、そうじゃ無くて、2024年にもなって文字化けなどと書かねばならないというところですよ!

日常的に複数の文字エンコーディング(文字符号化方式、簡単に言えば文字コード、Emacs用語ではコーディングシステム)を使っている人はdiffをどうしているのでしょうか。まぁ、使う文字エンコーディングが一つに偏っているならそれに合わせて残りは場当たり的に対処すれば良いのでしょう。私もそうしていました。UTF-8以外使うな! などと過激なことを言う方も昨今いらっしゃいますが、私はそうは思いません。長年コンピュータを使ってきた人間にとって、過去に作った物を無かったことには出来ませんからね。

とは言えdiffを取ったときに文字化けしているバッファを見ると煩わしさを感じるのも事実です。

そういうときはdiffのバッファの中で M-x revert-buffer-with-coding-system (C-x RET r) の後、文字エンコーディングを選ぶのが簡単です。diffは取り直しになりますが。

他にもread onlyを解除して、バッファ全体をencode-coding-regionしてからdecode-coding-regionしてやると直せる場合もあります。diffの取り直しは回避できますが、常に直せるかはちょっと分からないです。

ediffで済むならそれを使うという手もあります。

いずれにせよ煩わしいことには変わりないので、ある程度自動的に対処するように次のようなコードを書きました。

(defun my-diff-detect-coding-system (file)
  "FILEのcoding systemを返す。分からなかったらnilを返す。"
  (let ((cs
         (when (file-regular-p file) ;;ディレクトリは除外する
           (with-temp-buffer
             (insert-file-contents file nil nil 1000000) ;;1MBくらい読んでおく?
             ;; これが一番簡単で確実っぽい
             last-coding-system-used))))
    (message "Detected coding system: %s" cs)
    (unless (memq cs '(nil undecided no-conversion)) ;;変なのは返さない
      cs)))

(defun my-diff-around (orig-fun old new &rest args)
  "diffにひっかけるaroundアドバイス。"
  ;; NEWのcoding systemに合わせてdiffを取る
  (let ((coding-system-for-read (or coding-system-for-read ;;すでに指定されている場合はそれを使う
                                    (my-diff-detect-coding-system new))))
    (apply orig-fun old new args)))

(advice-add 'diff :around 'my-diff-around)

要するにファイル(NEW側のみ)の文字エンコーディングを判別して、それをcoding-system-for-readに設定してからdiffを実行するだけです。

my-diffという関数を作ろうか迷いましたが、diffはいろんな場所から呼び出されているような気がしたので全てに適用させるためにdiffに対するadviceにしてみました。

文字エンコーディングを判別しているところですが、insert-file-contentsの後にlast-coding-system-usedを参照するのが見つけた方法の中では一番簡単でした。最初はdetect-coding-regionを使ったのですが、UTF-16が判別できないこととファイルローカル変数の指定が効かないことが問題になりました。UTF-16はどのみち別の問題があるので諦めるとして、 -*- coding:cp932 -*- のような指定は効いてほしいところ。半角カナでCP932(SJIS)で「ミエ」と書いたらUTF-8の「д」と区別が付かないんですよ(どんなシチュエーションだよ)。そんなときにcoding:の指定を入れれば解決できるわけです。set-auto-coding関数を使えばUTF-16(auto-coding-regexp-alist)やファイルローカル変数の判別が可能になるのですが、今度は行末タイプ(unix、dos、mac)が判別できません。行末タイプだけを判別するような関数を探したのですが見当たりませんでした。自分で \r\n を検索すれば良いのでしょうが、そんな面倒なことをするよりもlast-coding-system-usedを参照するだけで済むようでした。それらの判別処理は全てinsert-file-contentsの中で行われていますので。

UTF-16はどうしましょうね。こればっかりはUTF-8にでも変換してからdiffを取るくらいしか思いつきません。--textを指定するとして、diff自身が出力するヘッダーの文字エンコーディングと合いませんからね。

ディレクトリ単位の比較は相変わらず化けるので必要に応じて C-x RET r するということで。

あ、diff自体が出力する日本語メッセージが化けますね。「のみに存在」とかいうやつ。実行前に環境変数も変えようかな……。

こうして今日も一つ直すと何個も直すところが増えていくのでした。

まだまだdiffの事はよく分かりません。

2024-01-28 ,

org-modeでインライン画像化する画像形式を限定する

以前Emacsが扱える画像形式をちゃんと設定して多種多様な画像を表示できるようにしたのですが(「画像形式とimage-converterの設定」のあたり)、その副作用でorg-mode内で余計なファイルリンクまでインライン画像表示されるようになってしまいました。

例えばmp3や動画ファイル、pdfに至るまでorg-modeの中でインライン画像表示されるようになってしまったのです。例えばTODOリスト内にローカルにあるメディアファイルへのリンクを書いてそれを読む(もしくは聞く)ようにメモを書いたとして、そのリンクがインライン画像表示されてしまうわけです。「image-diredでmp3カバー画像を表示する」のようにImage Dired内でサムネイルとして表示される分には全く構わないわけですが、org-mode内でいちいち全てのリンクが画像として表示されてはたまりません。

原因

インライン画像化される画像形式は、org-display-inline-images関数から呼び出されるimage-file-name-regexp関数が返す正規表現によって決まっています。現在私の所でこの関数を呼び出すと……

(image-file-name-regexp)
\.\(3\(?:G[2P]\|g[2p]\)\|A\(?:I\|PNG\|RT\|VIF?\)\|BMP\|C\(?:R[23]\|UR\)\|D\(?:C[MR]\|DS\|NG\|PX\|XT[15]\)\|E\(?:P\(?:DF\|S[FI]\|T[23]\|[IST]\)\|RF\)\|F\(?:ITS\|L\(?:32\|IF\|V\)\|TS\)\|GIF\|H\(?:DR\|EI[CF]\|RZ\)\|I\(?:C\(?:ON\|[BO]\)\|IQ\|PL\)\|J\(?:2[CK]\|B\(?:I?G\)\|N[GX]\|P\(?:EG\|[2CEGMST]\)\)\|K\(?:25\|DC\)\|M\(?:2V\|4[AV]\|EF\|IFF\|KV\|NG\|O\(?:NO\|V\)\|P\(?:EG\|[34CGO]\)\|RW\|TV\|VG\)\|N\(?:EF\|RW\)\|O\(?:RF\|T[BF]\)\|P\(?:AM\|BM\|C\(?:DS\|[DLTX]\)\|DFA?\|EF\|F[ABM]\|G[MX]\|HM\|I\(?:C\(?:ON\|T\)\|X\)\|JPEG\|N[GM]\|PM\|S[BD]?\|TIF\|WP\)\|QOI\|R\(?:A[FS]\|GF\|L[AE]\|MF\|W2\)\|S\(?:FW\|VGZ?\)\|T\(?:GA\|I\(?:FF\(?:64\)?\|[FM]\)\|M2\|T[CF]\)\|V\(?:DA\|I\(?:CAR\|FF\|PS\)\|ST\)\|W\(?:BMP\|EB[MP]\|MV\|PG\)\|X\(?:3F\|BM\|CF\|P[MS]\|V\)\|a\(?:i\|png\|rt\|vif?\)\|bmp\|c\(?:r[23]\|ur\)\|d\(?:c[mr]\|ds\|ng\|px\|xt[15]\)\|e\(?:p\(?:df\|s[fi]\|t[23]\|[ist]\)\|rf\)\|f\(?:its\|l\(?:32\|if\|v\)\|ts\)\|gif\|h\(?:dr\|ei[cf]\|rz\)\|i\(?:c\(?:on\|[bo]\)\|iq\|pl\)\|j\(?:2[ck]\|b\(?:i?g\)\|n[gx]\|p\(?:eg\|[2cegmst]\)\)\|k\(?:25\|dc\)\|m\(?:2v\|4[av]\|ef\|iff\|kv\|ng\|o\(?:no\|v\)\|p\(?:eg\|[34cgo]\)\|rw\|tv\|vg\)\|n\(?:ef\|rw\)\|o\(?:rf\|t[bf]\)\|p\(?:am\|bm\|c\(?:ds\|[dltx]\)\|dfa?\|ef\|f[abm]\|g[mx]\|hm\|i\(?:c\(?:on\|t\)\|x\)\|jpeg\|n[gm]\|pm\|s[bd]?\|tif\|wp\)\|qoi\|r\(?:a[fs]\|gf\|l[ae]\|mf\|w2\)\|s\(?:fw\|vgz?\)\|t\(?:ga\|i\(?:ff\(?:64\)?\|[fm]\)\|m2\|t[cf]\)\|v\(?:da\|i\(?:car\|ff\|ps\)\|st\)\|w\(?:bmp\|eb[mp]\|mv\|pg\)\|x\(?:3f\|bm\|cf\|p[ms]\|v\)\)\'

といった具合なので、そりゃ沢山の形式がインライン画像化されてしまうわけです。

手動でインライン画像表示をしていたらあまり気にならないのかもしれませんが、私はorg-flyimageで自動的にインライン画像表示をさせているので意図しないものまで全て即事に表示されてしまうわけです。

修正方法

これを修正するとして、image-file-name-regexp関数が返す内容を修正すべきでしょうか。それともorg-mode側を修正すべきでしょうか。

image-file-name-regexp関数を修正してしまうと他の部分で画像が表示されなくなってしまうことが予想されます。また、そもそもインライン画像化はエクスポートしたときに画像化される形式に限定すべきでしょう。

org-flyimageの自動表示対象を変更できるようにするという手もありますが(必要なら手動で表示する余地を残す)、そこまでは必要ないでしょう。

というわけでorg-display-inline-images関数の挙動を書き替えれば良いのですが、私の場合以前「org-inline-image-fixのEmacs 29対応」に書いたような経緯でこの関数を完全に置き換えてしまっているので、そちらを修正することになります。org-display-inline-images関数は外から手を加えるのが難しい構造をしていて、色々強引な手を使った挙げ句Emacs29になったタイミングでより良い関数に置き換えたのでした。

Add ability to customize displayed image file names · misohena/org-inline-image-fix@07856aa

上のコミットでorg-better-inline-images-image-file-name-regexpというカスタマイズ変数を追加し、画像化するか判定するための正規表現を変更できるようにしました。設定できる値は、nil(従来通りimage-file-name-regexp関数を使う)、文字列(正規表現)、関数(image-file-name-regexp関数の代わりに正規表現を返す)、拡張子のリストに対応しています。

本当は画像としてエクスポートするファイル名かどうか(org-export-default-inline-image-ruleorg-html-inline-image-rules)を基準にしようとも思ったのですが、tifやxpm等微妙な形式もありますし、ox.elやox-html.el等を必ずロードしなければならないのでやめておきました。数も少ないですし、拡張子のリストが指定出来ればそれで十分でしょう。

これで私はインライン画像表示する形式を、gif、jpg、jpeg、png、svg、webpに限定しました。必要な形式があったらその都度追加するということで。

org-better-inline-images-image-file-name-pというカスタマイズ変数も追加しておきましたが不要でした。

Org 9.6から現在までのインライン画像表示機能に対する変更点の確認

ついでに最近のインライン画像表示機能に対する変更点も確認しておきました。関数を置き換えた以上、本家の方に加えられた変更に目を光らせていなければなりません。

これらはおそらく次のリリース(9.7?)に含まれることになるのでしょう。

注目はインライン画像の幅を制限する機能(org-image-max-width変数)でしょう。待ちわびていた人もいるのではないでしょうか。今のところ高さの制限(org-image-max-height?)は無いように見えます。なので私の改良はまだ意義があるということで。

インライン画像のalign(右寄せ、中央寄せ)も実装されたようです。 #+ATTR_HTML: :align center 等の指定やグローバルオプション(org-image-align)の指定が反映されるようです。個人的には使う予定はありません。

org-elementにいくつか便利な関数が追加されたり、引数の指定方法が改善されたりしたので、それに伴う修正がいくつか入っていました。

環境変数の展開は、そもそもそんなことができること自体知りませんでした。試しに [[file:$APPDATA/Microsoft/Windows/Start Menu/Programs]] と書いたらちゃんとスタートメニューにアクセスできました。私はCorfuでファイル名の補完を有効にしているのですが、 file:$ と打った瞬間に全環境変数が補完候補として出てきます。環境変数を入れた後も、ちゃんとそれを展開した後のディレクトリにあるファイルを補完候補として出してきます!

一部のものは私の改造版にも反映しておきました。残りは9.7が出てからにします。

2024-01-27

複数行にわたるコメントの中のS式を評価する

Emacs Lispで次のようなコードを書いたとします。

;; 使用例:
;; (my-hogehoge-function
;;   1
;;   2
;;   3)

(defun my-hogehoge-function (a b c)
  (+ a b c))

複数行あるコメントの最後、 ) の直後でeval-last-sexp (C-x C-e)を実行すると……

Debugger entered--Lisp error: (scan-error "Unbalanced parentheses" 313 1)

などと出てコメント内のS式を評価できません。

いちいちuncommentしてから評価して元に戻すのも面倒です。

Googleで検索して見ると kensanata/eval-sexp-in-comments: eval sexp in comments, for Emacs というのを見つけました。ソースコードを見るとwith-temp-bufferで別バッファへ移してからコメントを外し、その後eval-last-sexpを実行していました。それだと現在のバッファの中で評価したい場合に困ります。

eval-last-sexpの中身を見てみると、elisp--preceding-sexpという関数でポイントの前にあるS式テキストをlispオブジェクトの形で取り出してから、評価しているようでした。

なので、このelisp--preceding-sexpに細工をしてコメントの中にいるときは別バッファにコピーしてコメントを外し、そこでelisp--preceding-sexpを呼び出してS式を返せば良いと考えました。

;; my-elisp.el

(defun my-elisp-beginning-of-continuous-comments ()
  "現在の連続コメントの先頭を返す。

連続コメントとは、連続改行(空行)を除く空白文字のみで区切られた複
数のコメントのまとまりを指す。そのまとまりの最初の;の位置を返す。

例:
123 ;; line-1
    ;; line-2

    ;; line-3

「line-3」の末尾の場合「;; line-3」の先頭、「line-2」の末尾の場合
「;; line-1」の先頭の位置を返す。

各行;;の先頭はコメントに含まれない。

現在のポイントがコメント内ではない場合nilを返す。

文字列の中の;;には反応しない。
例:
\"
;; line-1 ←ここで実行してもnilを返す。コメントでは無く文字列の中なので。
\"

(以下追記)
同じコメントスタートに限定するかは迷うところ。
 ;; (+
;;     2
;;;    3)
みたいなのも現状では受け入れる。

    ;; line-1
123 ;; line-2
    ;; line-3
みたいなのは無理(;; line-2が先頭になる)。
対応できないことは無いだろうけど
そもそもコメントの前に何かある場合も対応する必要があるかは疑問。

理想的にはコメント開始の水平位置と;の数が揃っている連続行を
抽出すべきなのだと思う。"
  (cond
   ((derived-mode-p 'emacs-lisp-mode)
    (save-excursion
      (let (beginning-of-comment)
        (while (and (comment-beginning)
                    (progn
                      (skip-chars-backward " \t")
                      (skip-chars-backward ";")
                      (setq beginning-of-comment (point))
                      (skip-chars-backward " \t")
                      (bolp))
                    (not (bobp)))
          (backward-char))
        beginning-of-comment)))
   (t
    (save-excursion
      ;; TODO: 現在のメジャーモードでのコメントが先頭にある場合はそれも無視すべき?
      ;; TODO: 「123 ;; (if」から複数行に続く形式に対応していない。123の部分にコメント以外のセミコロン(文字列等)があることを考慮しなければならない。
      (let (beginning-of-comment)
        (forward-line 0)
        (while (looking-at "[ \t]*;+[ \t]*")
          (setq beginning-of-comment (match-end 0))
          (forward-line -1))
        beginning-of-comment)))))

(defun my-elisp-sexp-in-comment (beg end)
  "BEGからENDの中にあるコメントの中にあるS式を返す。"
  (when (and beg end (< beg end))
    (let ((original-buf (current-buffer)))
      (with-temp-buffer
        (emacs-lisp-mode)
        (insert-buffer-substring original-buf beg end)
        (goto-char (point-min))
        ;; ↓これだと最終行のコメントが空でかつEOBの時になぜか「Beginning of buffer」のエラーが出る。
        ;; (uncomment-region (point-min) (point-max))
        (while (re-search-forward "^\\s-*;+" nil t)
          (replace-match "")
          ;; 次の行へ(現在の行にある残りのコメント中コメントは残す)。
          (forward-line 1))
        (goto-char (point-max))
        ;; 1段階コメントを外した後の状態からS式を取り出す。
        ;; コメントの中にコメントがある場合は、
        ;; 再帰的にこの関数が呼び出されることもある。
        ;; ;; ;; (+
        ;; ;; ;;  ;; コメント
        ;; ;; ;;  1 2)
        ;; みたいなのも正しく処理する。
        (elisp--preceding-sexp)))))

(defun my-elisp-preceding-sexp-in-comment ()
  "ポイントがコメント内にあるとき、複数行にわたるコメントを考慮して
ポイントの直前にあるS式を読み取る。"
  (my-elisp-sexp-in-comment (my-elisp-beginning-of-continuous-comments) (point)))

(defun my-elisp-preceding-sexp-around (orig-fun &rest args)
  "elisp--preceding-sexpの:around advice。"
  (let ((end (point))
        (beg (my-elisp-beginning-of-continuous-comments)))
    (if (and beg (< beg end))
        (my-elisp-sexp-in-comment beg end)
      (apply orig-fun args))))

(provide 'my-elisp)

(2024-01-30修正: コメントの中のコメントをうまく処理できるようにしました)

設定方法:

(when (version<= "25.1" emacs-version) ;; Require #'elisp--preceding-sexp
  (autoload 'my-elisp-preceding-sexp-around "my-elisp")
  (advice-add 'elisp--preceding-sexp :around 'my-elisp-preceding-sexp-around))

これで無事複数行にわたるコメント内のS式を評価できるようになりました。

ただ、メジャーモードがemacs-lisp-modeではない場合のことも考えると、少々煮え切らないコードになってしまいました。例えばorg-modeのソースコードブロックの中にコメントがあって、その中のS式を評価したい場合など。C言語やシェルスクリプトのコメント(//や#)の中に複数行にわたってS式を書いてそれを評価したい、なんてケースはあるでしょうか?(実際、別言語のソースコードのコメントの中にelispのコードを書いて、その別言語のコードを生成したことは度々あった気がします) 考え出すと切りがないです。文字列などクォートされたセミコロンを考慮しなければならないので単純な正規表現で処理するのもためらわれたりと色々面倒なところもありました。