2024-02-19 ,

org-modeリンクのpath部分でcompletion-at-pointできるようにする

(2024-02-23追記: 以下でorg-elisp-link.elに追加したリンク補完機能の内、一般的な枠組みの部分をorg-link-completion.elへ移動しました。考え方はほとんど以下と同じですが細部は多少変わっています。関数名等もorg-link-completion-で始まるように変わっています)

この間作った org-elisp-link.elで、リンクのpath部分をcompletion-at-point (C-M-i)で補完できるようにしました。

misohena/org-elisp-link: Org-mode Link Types for Emacs Lisp Elements

例えば [[elisp-function:save- と書いた後に C-M-i を押すと、save-で始まる関数名が補完候補としてずらっと出るというわけです。

関数名を補完させるところ
図1: 関数名を補完させるところ

いや、分かってるんですよ。C-c C-lを押せばミニバッファに補完付きでファイルタイプを入力できて、そのファイルタイプにorg-link-parameters:complete プロパティが設定されていれば、ミニバッファで補完付きで関数名ぐらい入力させられるということは。

でもやっぱりorg-modeバッファ内で、at pointでやりたいじゃないですか。せっかく最近Corfuの補完設定を改善したところですし。

どう実現したかというと、org-link-parametersに非標準のプロパティ :completion-at-point :capf-path を追加しました。(追記:description部分を補完するために :capf-path へ改名しました)

そしてcompletion-at-point-functionsへ登録するための関数(capf)を新しく作成しました。そう、org-pcompleteを拡張するのは止めました。pcompleteには上図のようなアイコン(kind)情報を伝える機能がありません。なのでorg-pcompleteの部分(pcomplete-completions-at-point)はそのままに、それと併存してもう一つ新しい補完関数を追加します。これはリンクのパス部分([[ の後の : 以降)だけに反応します。org-pcompleteはリンクのパス部分には反応しないので問題はありません。

新たに作成した補完関数は次の通りです。

(defvar org-elisp-link-capf-pos nil
  "Temporarily hold the result of `org-elisp-link-capf-path-parse' function.")

(defun org-elisp-link-completion-at-point ()
  "Complete path part of link in org-mode.

[[<link-type>:(complete at this point)

For completion, refer to `:capf-path property of
`org-link-parameters'.

To use this, do the following in org-mode buffer:
(add-hook \\='completion-at-point-functions
          #\\='org-elisp-link-completion-at-point nil t)"
  (when-let ((org-elisp-link-capf-pos (org-elisp-link-capf-path-parse)))
    (let* ((type-beg (nth 0 org-elisp-link-capf-pos))
           (type-end (nth 1 org-elisp-link-capf-pos))
           (type (buffer-substring-no-properties type-beg type-end))
           (capf (org-link-get-parameter type :capf-path)))
      (when capf
        (funcall capf)))))

(defun org-elisp-link-capf-path-parse ()
  "Return (type-beg type-end path-beg path-end) of link at point.

( [[<type>:<path>(point is in <path>) )"
  (save-excursion
    (let ((origin (point))
          path-beg path-end
          type-beg type-end)
      (when (and (skip-chars-backward "^:\n \t[") ;; TODO: Skip escape sequence
                 (eq (char-before) ?:)
                 (setq path-beg (point))
                 (goto-char (1- (point)))
                 (setq type-end (point))
                 (skip-chars-backward "-A-Za-z0-9_+")
                 (eq (char-before) ?\[)
                 (eq (char-before (1- (point))) ?\[)
                 (setq type-beg (point)))
        (goto-char origin)
        (skip-chars-forward "^]\n \t")
        (setq path-end (point))
        (list type-beg type-end path-beg path-end)))))

org-elisp-link-capf-path-parse関数はリンクの範囲を特定する関数です。ポイントの位置にあるリンクが [[<type>:<path> の形をしているとして、現在ポイントが<path>の中を指しているものと仮定します。前後の文字を調べて実際にそうならば、<type>と<path>それぞれの先頭と末尾の位置をリストで返します。

org-elisp-link-completion-at-point関数はcompletion-at-point-functions変数に登録される補完関数です。

しかし見てもらえれば分かりますが非常にシンプルです。やっている事と言えば:

  1. org-elisp-link-capf-path-parseで現在ポイントが指しているリンクの情報を取得して
  2. リンクタイプに対応するorg-link-parameters内の :capf-path プロパティ(補完関数)を取り出し
  3. そこに処理を丸投げする

だけです。

なので原理的にはelispリンクに限った話ではありません。他のリンクタイプでも、同様に補完関数を作ってorg-link-parameters:capf-path プロパティを設定すれば補完が出来るようになります。

丸投げされる側、例えば elisp-function: タイプの補完関数org-elisp-link-capf-path-functionは次のようになっています。

(defun org-elisp-link-capf-path-function ()
  "Complete <function> of [[<link-type>:<function> at point."
  (org-elisp-link-capf-path--symbol
   (lambda (sym)
     (when-let ((sym (intern-soft (symbol-name sym))))
       (or (fboundp sym)
           (get sym 'function-documentation))))
   #'elisp--company-kind))

(defun org-elisp-link-capf-path--symbol (predicate kind)
  "Complete <symbol> of [[<link-type>:<symbol> at point."
  (when-let ((pos (or org-elisp-link-capf-pos ;; 解析済みの情報があればそれを使う
                      (org-elisp-link-capf-path-parse))))
    (let ((path-beg (nth 2 pos))
          (path-end (nth 3 pos)))
      (list
       path-beg path-end
       (elisp--completion-local-symbols)
       :predicate
       predicate
       :company-kind kind
       :company-doc-buffer #'elisp--company-doc-buffer
       :company-docsig #'elisp--company-doc-string
       :company-location #'elisp--company-location
       :company-deprecated #'elisp--company-deprecated))))

この辺りはelisp-mode.elelisp-completion-at-point関数を大いに参考にさせてもらっています。というか内部的な関数も利用させてもらっています。まぁ、cape.elなんかも参照している部分がありますし、とりあえず良いんじゃないでしょうか。

こうしてみると結構簡単に作れるものですよね。org-modeでpcompleteなんか使う必要は無いような……。

他のリンクタイプ、 elisp-variable: elisp-face: elisp-library: にもそれぞれ補完関数があり、それらもorg-link-parameters:capf-path プロパティに設定すれば補完が出来るようになります。

おまけとして file: タイプに対する補完関数org-elisp-link-capf-path-fileも入れておきました。

(defun org-elisp-link-capf-path-file ()
  "Complete <filename> of [[<link-type>:<filename> at point."
  (when-let ((pos (or org-elisp-link-capf-pos
                      (org-elisp-link-capf-path-parse))))
    (let ((path-beg (nth 2 pos))
          (path-end (nth 3 pos)))
      (list
       path-beg path-end
       #'read-file-name-internal
       :annotation-function
       (lambda (str) (if (string-suffix-p "/" str) " Dir" " File"))
       :company-kind
       (lambda (str) (if (string-suffix-p "/" str) 'folder 'file))
       :exclusive 'no))))

これもread-file-name-internalなどという関数を使っていますが、やっぱりcape(cape-file)で使っていたのでそれに倣いました。

もはやelispリンクとは関係ありませんが、org-modeにリンクパスのcompletion-at-point機能が無いのが悪いんです。せっかく汎用的な仕組みを実装した以上、最低限 file: についても書いておくべきかなと思いました。

ちなみに file: だけでなく file+sys:file+emacs: にも同じ関数が使えます。 file+sys:file+emacs: って知ってました?

最終的に org-elisp-link.el の初期化は次のようになりました:

(with-eval-after-load "org"
  (require 'org-elisp-link)
  (org-elisp-link-initialize)

  (org-link-set-parameters "file"
                           :capf-path 'org-elisp-link-capf-path-file)
  (org-link-set-parameters "file+sys"
                           :capf-path 'org-elisp-link-capf-path-file)
  (org-link-set-parameters "file+emacs"
                           :capf-path 'org-elisp-link-capf-path-file)

  (add-hook 'org-mode-hook
            (lambda ()
              ;; 追加の順番よっては正しく動かないかも?
              (add-hook 'completion-at-point-functions
                        'org-elisp-link-completion-at-point nil t)))
  )

手元にはブログ専用リンクタイプ(blog:)なんてものもあるので、それも補完できると楽が出来そうです。補完関数はブログのディレクトリをdirectory-filesして少し加工すればいいだけなので簡単ですね。

例えば次のような感じでしょうか。(2024-02-23追記: org-link-completion.elを使うように書き替えました)

(require 'org-link-completion)

(defun my-org-blog-link-capf-path ()
  "ポイント上のリンクのパス部分を補完します。
[[blog:2024-02-20-hello-emacs]]のようなリンクの補完候補を返します。

ブログの元orgファイルは(plist-get blog :local-dir)で得られるディ
レクトリにあるものとします。"
  (org-link-completion-parse-let :path (type path-beg path-end)
    (when-let ((blog (my-blog-from-link-type type)))
      (list
       path-beg path-end
       (cl-loop for file in (directory-files (plist-get blog :local-dir))
                when (string-match "\\`\\(.+\\)\\.org\\'" file)
                collect (match-string 1 file))
       :company-kind (lambda (_) 'file)))))

(org-link-set-parameters "blog"
                         :capf-path 'my-org-blog-link-capf-path)
2024-02-18 ,

org-modeの補完を直す

最近補完まわりを色々改善したので気を良くしてあちこちで無駄に補完をさせて楽しんでいるのですが、org-modeの補完がイマイチうまく動かないことが多いです。色々調べた結果org-pcomplete.elに問題があることが分かってきました。なので少し直してみました。

見つけた問題はだいたい次の通りです:

  • リンクタイプが補完できない (→「org-modeでの入力補完」で解決済み)
  • #+ATTR_HTML等一部のキーワードが補完できない (→「org-modeでの入力補完」で解決済み)
  • 行の途中に区切り文字(スペースや[)があると、その行の末尾でリンクが補完できない
  • 行の途中にあるリンクが補完できない
  • 行の途中にある見出し検索リンクが補完できない (→最近1/7のコミット「Fix [[* completion when there is text after point」で解決済み)
  • 行の途中にあるTeX表記が補完できない
  • 右に既に優先度や見出しが入っている場合にTODOキーワードが補完できない

一番気になったのがリンクの補完です。例えば

[[
hoge [[
あれは[[

の末尾では補完できますが

hoge hoge [[
あれは[[file:hoge.txt]]または[[

の末尾では補完できません。日本語は空白で区切らないので最初は空白が入っていると同じ問題が起きることには気が付きませんでした。日本語でも二つ目以降のリンクが補完されません。

また、行の途中に戻って補完するのも無理なようです。基本的に行末で無ければ補完できません。

というわけで、それらを直すコードを作ってみました。

(with-eval-after-load "org-pcomplete"
  ;; 「#+」の部分を補完する関数。
  ;; ATTR_HTMLやBEGIN_FIGURES-COL2等を追加。
  ;; `pcomplete/org-mode/file-option'を置き換える。
  (defun my-pcomplete/org-mode/file-option ()
    "Complete against all valid file options."
    ;; org-pcomplete.elの`pcomplete/org-mode/file-option'よりコピーして改変
    (require 'org-element)
    (pcomplete-here
     (org-pcomplete-case-double
      (append (mapcar (lambda (keyword) (concat keyword " "))
                      org-options-keywords)
              (mapcar (lambda (keyword) (concat keyword ": "))
                      org-element-affiliated-keywords)
              ;; ★[変更]: 追加。
              (mapcar (lambda (keyword) (concat keyword ": "))
                      '("ATTR_HTML" "ATTR_ORG"))
              (let (block-names)
                (dolist (name
                         '("CENTER" "COMMENT" "EXAMPLE" "EXPORT" "QUOTE" "SRC"
                           "VERSE"
                           ;; ★[変更]: 追加。
                           "FIGURES-FLOW" "FIGURES-COL2" "FIGURES-COL3")
                         block-names)
                  (push (format "END_%s" name) block-names)
                  (push (concat "BEGIN_"
                                name
                                ;; Since language is compulsory in
                                ;; export blocks source blocks, add
                                ;; a space.
                                (and (member name '("EXPORT" "SRC")) " "))
                        block-names)
                  ;; ★[変更]: 削除。ATTR_CENTERって何だよ……
                  ;;(push (format "ATTR_%s: " name) block-names)
                  ))
              (mapcar (lambda (keyword) (concat keyword ": "))
                      (org-get-export-keywords))))
     (substring pcomplete-stub 2)))
  (advice-add 'pcomplete/org-mode/file-option :override
              'my-pcomplete/org-mode/file-option)

  ;; リンクを補完する関数。
  ;; link-abbrevだけでなくリンクタイプも補完させる。
  ;; `pcomplete/org-mode/link'を置き換える。
  (defun my-pcomplete/org-mode/link ()
    "Complete against defined #+LINK patterns."
    ;; org-pcomplete.elの`pcomplete/org-mode/link'よりコピーして改変
    (while ;;★[変更]: ←追加(ただし`my-org-parse-arguments'の修正で不要)
        (pcomplete-here
         (pcomplete-uniquify-list
          (copy-sequence
           (mapcar (lambda (e) (concat (car e) ":"))
                   (append org-link-abbrev-alist-local
                           org-link-abbrev-alist
                           ;; ★[変更]: 追加
                           org-link-parameters)))))))
  (advice-add 'pcomplete/org-mode/link :override 'my-pcomplete/org-mode/link)

  ;; 見出しへの検索リンク([[*)を補完する関数。
  ;; エラーが出るので最新のコミットでの修正を取りこむ。
  ;; `pcomplete/org-mode/searchhead'を置き換える。
  (defun my-pcomplete/org-mode/searchhead ()
    "Complete against all headings.
This needs more work, to handle headings with lots of spaces in them."
    (while (pcomplete-here
            (save-excursion
              (goto-char (point-min))
              (let (tbl)
                (while (re-search-forward org-outline-regexp nil t)
                  ;; Remove the leading asterisk from
                  ;; `org-link-heading-search-string' result.
                  (push (substring (org-link-heading-search-string) 1) tbl))
                (pcomplete-uniquify-list tbl)))
            ;; ★[変更]: この部分を削除。substringでpcomplete-stubが空の時にargs-out-of-rangeが出る。最新版では削除されてる。
            ;; ;; When completing a bracketed link, i.e., "[[*", argument
            ;; ;; starts at the star, so remove this character.
            ;; ;; Also, if the completion is done inside [[*head<point>]],
            ;; ;; drop the closing parentheses.
            ;; (replace-regexp-in-string
            ;;  "\\]+$" ""
            ;;  (substring pcomplete-stub 1))
            )))
  (advice-add 'pcomplete/org-mode/searchhead :override
              'my-pcomplete/org-mode/searchhead)

  ;; ★引数列の抽出部分を根本的に直す。
  ;; `org-parse-arguments'を置き換える。
  (defun my-org-parse-arguments ()
    "Parse whitespace separated arguments in the current region."
    (pcase (org-thing-at-point)
      ;; 2024-01-07のコミットで追加された部分
      ;; Fix [[* completion when there is text after point
      ;; https://git.savannah.gnu.org/cgit/emacs/org-mode.git/commit/lisp/org-pcomplete.el?id=97951352bb4a32b06f0dede37cf5f796ad3f14c2
      (`("searchhead" . nil)
       ;; [[* foo<point> bar link::search option.
       ;; Arguments are not simply space-separated.
       (save-excursion
         (let ((origin (point)))
           (skip-chars-backward "^*" (line-beginning-position))
           (cons (list (buffer-substring-no-properties (point) origin))
                 (list (point))))))
      ;; 行指向の文法要素のみ行頭から引数を集める
      ((or `("file-option" . ,thing)
           `("block-option" . ,_))
       (let ((begin (line-beginning-position))
             ;; #+STARTUPはpcomplete-argsを参照しているので行末まで集める
             ;; (ただし最後の引数より前で補完が出来ないバグはそのままになる)
             (end (if (equal thing "startup") (line-end-position) (point)))
             begins args)
         (save-excursion
           (goto-char begin)
           (while (< (point) end)
             (skip-chars-forward " \t[")
             (push (point) begins)
             (skip-chars-forward "^ \t\n[")
             (push (buffer-substring-no-properties (car begins) (point))
                   args)))
         (cons (reverse args) (reverse begins))))
      ;; それ以外は基本的に直前のみを返す
      ;; リンクは[の後からリンクタイプの後(:があればそれも含む)まで
      (`("link")
       (save-excursion
         (skip-chars-backward "^[" (line-beginning-position))
         (let ((beg (point))
               (end (progn
                      (skip-chars-forward "-A-Za-z0-9_")
                      (if (eq (char-after) ?:) (1+ (point)) (point)))))
           (cons (list (buffer-substring-no-properties beg end))
                 (list beg)))))
      (_
       (save-excursion
         (let ((end (point)))
           (skip-chars-backward "^ \t\n[")
           ;; TODO: 現在の引数の末尾まで含めるか迷う
           (cons (list (buffer-substring-no-properties (point) end))
                 (list (point))))))))
  (advice-add 'org-parse-arguments :override 'my-org-parse-arguments)

  ;; 現在いる場所の文法要素を調べる関数。
  ;; ★`org-thing-at-point'で検出できなかったものを補う。
  (defun my-org-thing-at-point:after-until ()
    ;; 元の`org-thing-at-point'がnilを返したとき、
    (cond
     ;; [[type:のようにコロンの後でもlinkにする。
     ;; 補完するのはコロンまでなのだから、コロンまで含めるべき。
     ((save-excursion
        (skip-chars-backward ":" (1- (point)))
        (skip-chars-backward "-A-Za-z0-9_")
        (and (eq ?\[ (char-before))
             (eq ?\[ (char-before (1- (point))))))
      (cons "link" nil))
     ;; 既に右に見出しが入っているときのTODOキーワード
     ((save-excursion
        (let ((pos (point)))
          (goto-char (line-beginning-position))
          (save-match-data
            (and (looking-at "^\\*+ +\\([^ \t\\[]*\\)")
                 (<= (match-beginning 1) pos)
                 (<= pos (match-end 1))))))
      (cons "todo" nil))))
  (advice-add 'org-thing-at-point :after-until
              'my-org-thing-at-point:after-until)

  ) ;; End of with-eval-after-load "org-pcomplete"

org-modeでの入力補完」ですでに書いたコードも含めているので少々長くなっています。

一番大きな原因はorg-parse-arguments関数の動作にあるようです。他の同種の関数(pcomplete-parse-arguments-function変数に設定される関数)であるpcomplete-parse-comint-arguments(shellのコマンドラインの補完に使われる)と比べてみるとよく分かるのですが、org-parse-argumentsは現在の行の頭から末尾まで全てを解析してしまっている一方pcomplete-parse-comint-argumentsは現在ポイントが指している引数までしか解析しません。pcompleteはそれらが返した引数列の最後の要素しか補完しないので、org-modeでは行末部分でしか補完されません。

org-parse-argumentsがなぜ行の末尾まで解析しているのかは不明ですが、pcomplete/org-mode/file-option/startupにはpcomplete-argsを参照して排他的なオプションを候補から外す処理をしているので、その時に末尾まで解析するように修正したのかもしれません。あくまで想像ですが。

いずれにせよ、基本的にはorg-parse-argumentsが返すのは現在のポイントが指している引数(字句)までに限るべきです。そうでないと行の途中で補完できなくなってしまいます。

そもそも「#+」のような行指向の文法要素なら別ですが、リンクやTeX表記などはポイントが指している字句のみを返せば十分であり、行頭から解析する必要は全くありません。最近(1月7日に) [[* で始まるリンク(見出し検索リンク)に対する修正が入りましたが(Fix [[* completion when there is text after point)これもその点は分かっているようで、ポイントが指している部分しか返していません。

上に書いたmy-org-parse-arguments関数の部分はその辺りを修正しました。

二つ目以降のリンクが補完されない原因はpcomplete/org-mode/link関数内のpcomplete-hereの前にwhileが無いからなのですが、org-parse-argumentsがポイントが指している部分しか返さないようにすればwhileが無くても問題ありません。

TODOキーワードが空の見出しで無ければ補完されないのはマニュアルにも書いてある(Completion (The Org Manual))ので仕様なのですが、一応修正しました。

単語の途中にポイントを置いて補完したときにどうなるべきなのかは微妙な問題な気がします。基本的にはその単語を置き換えるように動作すべきだと思うのですが、現状では必ずしもそうはなっていません。Corfuが有効だとエラーが発生するケースもありました。Corfuを無効化するとエラーは出なくなるのですが、かといって正しく補完されるわけでも無く単に空白が一文字挿入されるだけだったりします。よく分からないので未解決です。

pcompleteのことは詳しくないのですが、元々コマンドラインの補完をするためのライブラリなのでしょうか? org-modeの補完に利用するのが適切なのかは正直疑問だと思います。org-thing-at-pointで調べたものをorg-parse-argumentsで再度調べ直さなければならないような状況に陥っていますし、使わずに書いた方がスッキリしそうな気がします。まぁ、補完関数に必要な処理を全て知らないので分かりませんが。

2024-02-17

Emacsのコンテキストメニューをキーから開く

Emacs28から(?)context-menu-modeというグローバルマイナーモードがあって有効にしてどこかを右クリックするとコンテキストメニューが出ます。

diredでファイルを右クリックした画面
図1: diredでファイルを右クリックした画面

常々「何が出来るのかわかんねーよC-h mは見づらいし、マニュアル読め? あんなの全部読めるわけ無いだろ、というか覚えておけるわけ無いだろ」と思っている人間にはとても素晴らしい機能だと思います。

左手でポテチをつまみながらリラックスして右手でマウスを握って操作をしたい人にも良いですね。

といっても普通にキーボードで操作しているときには逆に使いづらいのです。このメニューはMS-Windowsだとアプリケーションキー(またはShift+F10)でも開けるのですが右クリックと同じメニュー(x-popup-menuによる)が出てきてしまいます。もちろん矢印キーで操作できるのですが、C-n等は使えません。私はいまだにXKeymacsを使っているので一般的なWindowsアプリケーションはたいていEmacsキーバインドで使えるのですが、Emacsには適用されないように設定しています。Emacsはそんなの無くてもEmacsキーバインドで使えますし、XKeymacsの不完全なエミュレーションを使う必要は無いですからね。でもx-popup-menuで表示されるWindows標準のメニューUIにも効果が無くなってしまうわけです。困った困った。

メニューバーの方も同じ問題があってF10を押すとメニューバーにキーフォーカスが移るのですが、こちらも矢印キーしか受け付けません。(ちなみに私は機能の存在を発見しやすくするためにメニューバーは表示したままにしています。使うことはそれほど多くはありません)

ただし、メニューバーを非表示にしているときはF10を押すとCUIでメニューが出てきます。menu-bar-openのコードを呼んでみるとtmm-menubartmm-promptという流れになっています。試しに M-: (tmm-prompt (context-menu-map)) を実行してみるとCUIでコンテキストメニューが表示されました。

diredで(tmm-prompt (context-menu-map))を評価した画面
図2: diredで(tmm-prompt (context-menu-map))を評価した画面

見た目が少しイモっぽいですがちゃんとメニューとして機能します。同じ物が二つ表示されてるのは vertico-mode のせいですね。後でどちらかを非表示にしないと。

とりあえず次のようにしておけばアプリケーションキーやShift+F10でtmm-promptを使ったコンテキストメニューが表示できます。

(with-eval-after-load "mouse"
  (defun my-context-menu-open-tmm ()
    (interactive)
    (tmm-prompt (context-menu-map)))

  (define-key global-map (kbd "S-<f10>") 'my-context-menu-open-tmm)
  (define-key context-menu-mode-map (kbd "<apps>") 'my-context-menu-open-tmm)
  (define-key context-menu-mode-map (kbd "<menu>") 'my-context-menu-open-tmm))

というかcontext-menu-openのdocstringには「Start key navigation of the context menu.」って書かれてるんですけどね。うーん……。context-menu-open関数自体を置き換えてしまってもいいかもしれませんね。もしくはremapする?

まぁそもそも始めから M-` を押してtmm-menubarを開くのでもたいていの場合は間に合ってしまいますが。今のところ内容はほとんど被っているようですし。もう少し現在の位置に応じて色々変えてくれるともっと使いやすくなりそうですね。org-modeなんてTableの位置でもないのにTableなんてメニュー項目を出してくるなと言いたい。

(追記)

Verticoとの兼ね合いをどうするかについて。とりあえずtmm-prompt関数ではVerticoを一時的に無効にしてみました。Verticoを使うと1文字入力だけでメニュー項目を選択出来なかったので。ヘルプを消したり"==>"を":"にしたりして見た目もちょっとスッキリに。

(defun my-tmm-prompt:around (oldfun &rest args)
  (let ((tmm-completion-prompt "")
        (tmm-mid-prompt ":")
        (completion-show-help nil))
    (if (and (boundp 'vertico-mode) vertico-mode)
        ;; verticoがある場合
        (if t
            ;; verticoを無効にする場合
            ;; 一時的にverticoを無効にして実行
            (unwind-protect
                (progn
                  (vertico-mode -1)
                  (apply oldfun args))
              (vertico-mode 1))
          ;; verticoを使う場合 (いまいち)
          (cl-letf (((symbol-function 'tmm-add-prompt) #'ignore)
                    (vertico-count 20))
            (apply oldfun args)))
      ;; そのまま実行
      (apply oldfun args))))
(advice-add 'tmm-prompt :around 'my-tmm-prompt:around)
2024-02-16 ,

org-modeでの入力補完

org-modeでの補完候補はorg-pcomplete.elで設定しているようで、特に#+で始まるオプションについてはpcomplete/org-mode/file-option関数で候補を生成しています。そのソースコードは次の通りです。

;; org-pcomplete.elより引用
(defun pcomplete/org-mode/file-option ()
  "Complete against all valid file options."
  (require 'org-element)
  (pcomplete-here
   (org-pcomplete-case-double
    (append (mapcar (lambda (keyword) (concat keyword " "))
                    org-options-keywords)
            (mapcar (lambda (keyword) (concat keyword ": "))
                    org-element-affiliated-keywords)
            (let (block-names)
              (dolist (name
                       '("CENTER" "COMMENT" "EXAMPLE" "EXPORT" "QUOTE" "SRC"
                         "VERSE")
                       block-names)
                (push (format "END_%s" name) block-names)
                (push (concat "BEGIN_"
                              name
                              ;; Since language is compulsory in
                              ;; export blocks source blocks, add
                              ;; a space.
                              (and (member name '("EXPORT" "SRC")) " "))
                      block-names)
                (push (format "ATTR_%s: " name) block-names)))
            (mapcar (lambda (keyword) (concat keyword ": "))
                    (org-get-export-keywords))))
   (substring pcomplete-stub 2)))

この関数で次のような文字列を補完候補として生成しています。

なぜこれを調べたかというと、 #+ATTR_HTML が補完候補に現れないことに気が付いたからです。

理由は上のコードを見れば分かるとおり、ATTR_で始まるものはCENTER、COMMENT、EXAMPLE、EXPORT、QUOTE、SRC、VERSEしか登録していないからです。

え、ちょっと待って? #+ATTR_CENTER: #+ATTR_COMMENT: #+ATTR_EXAMPLE: #+ATTR_EXPORT: #+ATTR_QUOTE: #+ATTR_SRC: #+ATTR_VERSE: なんてありましたっけ? 聞いたこともありませんし使ったこともありません。ちょっと検索しても分かりませんでした。どういうこと??

ATTR_HTMLはエクスポートキーワードの中にあるのかなとも思ったのですが、そういうわけでも無さそうです。

ATTR_HTMLを追加しようにもこの関数の途中に処理を挟むのは難しいので、諦めて全部上書きしてしまうことにしました。ついでに独自のspecial blocks(org-special-blocks.el ― turn blocks into LaTeX envs and HTML divs)も補完候補に加えます(#+begin_figures-col2<div class="figures-col2> にしてくれます)。ATTR_CENTER等は削除してしまいましょう。よく分かりませんし、使いませんし。

(with-eval-after-load "org-pcomplete"
  ;; org-pcomplete.elの`pcomplete/org-mode/file-option'よりコピーして改変
  (defun pcomplete/org-mode/file-option ()
    "Complete against all valid file options."
    (require 'org-element)
    (pcomplete-here
     (org-pcomplete-case-double
      (append (mapcar (lambda (keyword) (concat keyword " "))
                      org-options-keywords)
              (mapcar (lambda (keyword) (concat keyword ": "))
                      org-element-affiliated-keywords)
              ;; ★[変更]: 追加
              (mapcar (lambda (keyword) (concat keyword ": "))
                      '("ATTR_HTML" "ATTR_ORG"))
              (let (block-names)
                (dolist (name
                         '("CENTER" "COMMENT" "EXAMPLE" "EXPORT" "QUOTE" "SRC"
                           "VERSE"
                           ;; ★[変更]: 追加。CSSで画像を並べるのに使っています
                           "FIGURES-FLOW" "FIGURES-COL2" "FIGURES-COL3")
                         block-names)
                  (push (format "END_%s" name) block-names)
                  (push (concat "BEGIN_"
                                name
                                ;; Since language is compulsory in
                                ;; export blocks source blocks, add
                                ;; a space.
                                (and (member name '("EXPORT" "SRC")) " "))
                        block-names)
                  ;; ★[変更]: 削除。ATTR_CENTERって何……?
                  ;;(push (format "ATTR_%s: " name) block-names)
                  ))
              (mapcar (lambda (keyword) (concat keyword ": "))
                      (org-get-export-keywords))))
     (substring pcomplete-stub 2))))

この所Corfuの自動補完をいじっていましたが、もちろんこれらの設定も自動補完に影響します。 #+beg くらいまで打てば自動で補完候補が出現します。corfu-auto-prefixが3なので#+の後に3文字入力したら表示されるのでしょう。

本当は #+ と入力しただけで補完候補が現れてくれればいいのですが。

一応次のようにすれば実現出来ます(corfu--auto-complete-deferredにいったい幾つadviceを仕掛けるつもりだ)。

(defun my-corfu--auto-complete-deferred:around:for-org (oldfun &rest args)
  (let (;; org-modeで#+が出たら即自動補完する
        (corfu-auto-prefix
         (if (and (derived-mode-p 'org-mode) ;;org-modeで……
                  ;; <bol><spaces>#+<identifier>
                  (save-excursion
                    (and (skip-chars-backward "-A-Za-z0-9_+")
                         (eq (char-before) ?+)
                         (eq (char-before (1- (point))) ?#)
                         (goto-char (- (point) 2))
                         (skip-chars-backward " \t")
                         (bolp))))
             0
           corfu-auto-prefix)))
    (apply oldfun args)))
(advice-add 'corfu--auto-complete-deferred :around
            #'my-corfu--auto-complete-deferred:around:for-org)

[[ を入力したらリンクタイプも補完してほしいんですよね。先日書いたやつ[[elisp-function: を入力するのが大変なので(よくfucntionやらfunctoinやら打ち間違えます。やっぱり [[elfun: にでもしておけば良かったかな)。 まぁ、そのうち。

(追記:リンクタイプも補完するようにしました)

ああよく見たら、同じくorg-pcomplete.elpcomplete/org-mode/linkというのがあるんですね。

;; org-pcomplete.elより引用
(defun pcomplete/org-mode/link ()
  "Complete against defined #+LINK patterns."
  (pcomplete-here
   (pcomplete-uniquify-list
    (copy-sequence
     (mapcar (lambda (e) (concat (car e) ":"))
             (append org-link-abbrev-alist-local
                     org-link-abbrev-alist))))))

リンクタイプを差し置いてlink abbrevだけあるのかよ!

まぁ、これもそのまま修正しちゃいましょう。(2024-02-23追記:org-link-completion.elでリンクタイプを補完できるようにしたので、それを使えば次の変更は不要です)

(with-eval-after-load "org-pcomplete"
  ;; org-pcomplete.elの`pcomplete/org-mode/link'よりコピーして改変
  (defun pcomplete/org-mode/link ()
    "Complete against defined #+LINK patterns."
    (pcomplete-here
     (pcomplete-uniquify-list
      (copy-sequence
       (mapcar (lambda (e) (concat (car e) ":"))
               (append org-link-abbrev-alist-local
                       org-link-abbrev-alist
                       ;; ★[変更]: 追加
                       org-link-parameters)))))))

上で書いたコードもちょっと修正して [[ で自動補完が開始されるようにしましょう。あ、ソースコードブロック中の [[ にも反応しちゃうかな。まあいいや。

(defun my-corfu--auto-complete-deferred:around:for-org (oldfun &rest args)
  (let (;; org-modeで#+や[[が出たら即自動補完する
        (corfu-auto-prefix
         (if (and (derived-mode-p 'org-mode) ;;org-modeで……
                  (or
                   ;; <bol><spaces>#+<identifier>
                   (save-excursion
                     (and (skip-chars-backward "-A-Za-z0-9_")
                          (eq (char-before) ?+)
                          (eq (char-before (1- (point))) ?#)
                          (goto-char (- (point) 2))
                          (skip-chars-backward " \t")
                          (bolp)))
                   ;; [[<link-type>
                   (save-excursion
                     (and (skip-chars-backward "-A-Za-z0-9_+") ;;file+sysがある
                          (eq (char-before) ?\[)
                          (eq (char-before (1- (point))) ?\[)))
                   ;; (2024-02-18追記:見出しの補完を追加)
                   ;; [[*<heading>
                   (save-excursion
                     (and (skip-chars-backward "^*\t\n[")
                          (eq (char-before) ?*)
                          (eq (char-before (1- (point))) ?\[)
                          (eq (char-before (- (point) 2)) ?\[)))))
             0
           corfu-auto-prefix)))
    (apply oldfun args)))
(advice-add 'corfu--auto-complete-deferred :around
            #'my-corfu--auto-complete-deferred:around:for-org)
2024-02-15

Corfuの自動補完で候補の存在を伝える事と候補を選べるようにする事を分離する

前回の続き。

私が自動補完が煩わしいなと思う所は、補完候補が表示されると同時ににいくらかのキー割り当てが(候補選択のために)変更されてしまい、それが誤操作を誘発する点です。それはorg-modeの表で何かを入力した後にTABや矢印キーを押して別のセルへ移動しようとしたときかもしれません。はたまた普通の場所でどこかの行を修正して、下の行に移動し、RETを押して改行を追加しようとしたときかもしれません。そういったときに、突然出現した自動補完にキーを奪われ意図せず補完候補を選んでしまうわけです。

一方でそれでも自動補完が欲しいと思う所は、補完候補の存在に気が付かせてくれる点です。C-M-iで手動で補完候補を出すことも出来ますが、それはそこで補完できることを知っていないとできません。

自動補完は、候補の自動的な表示とユーザーの選択によって機能します。前者は欲しいのに後者は鬱陶しい。となればそれらを分離すれば良いと考えるのは自然でしょう。

つまり次のようになってくれれば良いわけです。

  1. 補完候補を見つけたらそれを自動でポップアップ表示する(しかし一切のキーを奪わず、候補の選択操作は出来ない)
  2. その状態で明示的にC-M-iを押すと候補の選択が出来るようになる(同時に最初の候補が選択されるもしくは唯一ある候補が入力される)

次のコードで出来ます。

(defvar-keymap my-corfu-auto-map
  :doc "Keymap used when popup is shown automatically."
  "C-g" #'corfu-quit)

(defvar my-corfu-in-auto-complete nil)

(defun my-corfu--auto-complete-deferred:around (oldfun &rest args)
  ;; 自動補完を試みるときに呼び出される
  (let ((my-corfu-in-auto-complete t))
    ;; 元の関数を呼び出す
    ;; 補完候補があるなら続けてsetup等が呼ばれる
    (apply oldfun args)))

(advice-add 'corfu--auto-complete-deferred :around #'my-corfu--auto-complete-deferred:around)

(defun my-corfu--setup:around (oldfun &rest args)
  (if my-corfu-in-auto-complete
      ;; 自動補完の時
      (progn
        (setf
         ;; 子フレームを半透明にする
         (alist-get 'alpha corfu--frame-parameters) 90
         ;; C-M-iを押せと表示する (2024-02-16追記) (2024-02-17修正: やっぱりtabではなくheaderを使う)(2024-02-18修正: やっぱりtabを使う。色が目立つから)
         (alist-get 'tab-line-format corfu--buffer-parameters) "   C-M-i:補完"
         ;; 最初の候補を選ばない
         corfu-preselect 'prompt)
        (let (;; キー割り当てを極力無くす
              (corfu-map my-corfu-auto-map))
          (apply oldfun args)))
    ;; 手動補完の時
    (setf
     ;; 子フレームを完全不透明にする
     (alist-get 'alpha corfu--frame-parameters) 100
     ;; C-M-iを押せと表示しない (2024-02-16追記) (2024-02-17修正: やっぱりtabではなくheaderを使う)(2024-02-18修正: やっぱりtabを使う。色が目立つから)
     (alist-get 'tab-line-format corfu--buffer-parameters) nil
     ;; 最初の候補を選ぶ
     corfu-preselect 'first)
    (apply oldfun args)))

(advice-add 'corfu--setup :around #'my-corfu--setup:around)

;; tab-line-heightを考慮して高さを増やす(2024-02-17追記)
(defun my-corfu--make-frame:around (oldfun frame x y width height buffer)
  (when (alist-get 'tab-line-format corfu--buffer-parameters)
    (cl-incf height (window-tab-line-height (frame-root-window frame))))
  (funcall oldfun frame x y width height buffer))

(advice-add 'corfu--make-frame :around #'my-corfu--make-frame:around)

補完候補が出ているときのキーマップは通常corfu-mapですが、自動補完の時にだけ使われるmy-corfu-auto-mapを定義しました。ポップアップを消すためのC-gだけ残して他のキー割り当てを全て取り除いています。これでもはや移動キーやTAB、RETを奪われることはありません。

Corfuのポップアップは基本的にcorfu--setupで始まりcorfu--teardownで終わるようです。自動補完の場合はcorfu--auto-complete-deferred経由でcorfu--setupが呼ばれるので、そのタイミングで各種変数を書き替えています。

選択操作ができない状態の時はポップアップを半透明で表示するようにしてみましたが、選択状態で分かるので特段必要では無かったかもしれません。「候補選択:C-M-i」などと一覧の一番下(上?)に表示すればより親切かもしれません(2024-02-16追記:そうしました)。

実際に使ってみると、最初は少し戸惑いますが慣れれば悪くないのかなとも思います。候補が出てるとこれまでの癖でついC-nを押してしまうというのはあるのですが……。しばらく使ってみないとよく分かりません。

(自動補完時に)候補を減らすのはもはや必要ないかもしれません。頻繁に候補を出されたところでキーが奪われるわけではありませんし。まぁ、ポップアップの表示自体が邪魔(視覚が奪われる)ということはあるかもしれませんが。その辺りも今後の調整ということで。

自動的に出現した補完候補(この時点でC-g以外のキーは通常通りの動作)
図1: 自動的に出現した補完候補(この時点でC-g以外のキーは通常通りの動作)
C-M-iを押して補完を開始したところ(この時点から候補選択等のキー操作ができる)
図2: C-M-iを押して補完を開始したところ(この時点から候補選択等のキー操作ができる)

(追記: 以前書いた設定corfu-mapのRETキーに独自の my-corfu-insert-or-newline という名前のコマンドを割り当てていましたが、正しくは corfu-my-insert-or-newline とすべきでした。C-M-iで手動補完を実行した後に素早くRETを押すと最初の補完候補が選ばれず単に改行されてしまっていました。なぜかというと、コマンド名がcorfu-で始まるかでupdateするかを判断している部分があったからです。なんてこった!)

(2024-02-18追記: 以前書いた設定はcorfu-mapを自動補完の時にも使うことが前提でしたが、今回の改良でcorfu-mapは手動補完専用になったので、設定を次のように書き替えました)

(with-eval-after-load "corfu"
  ;; 上の自動補完時にキーを奪わない設定が前提。
  ;; https://misohena.jp/blog/2024-02-15-separate-notification-and-selection-with-corfu-auto.html
  ;; 他にもorg-mode時に #+ や [[ で自動補完を開始する設定もしている。
  ;; https://misohena.jp/blog/2024-02-16-completion-in-org-mode.html

  ;; 候補リストの最初と最後を行き来できるようにする。
  (setq corfu-cycle t)
  ;; 自動的に補完候補を出す。
  (setq corfu-auto t)

  ;; 特殊な文字は決定と同時に挿入する。
  ;; https://github.com/minad/corfu/wiki#tab-and-go-completion が近い。
  ;; 特殊な文字はモードによって変わってくる。C++なら:や(も同様に処理したいだろう。
  ;; lispなら関数名の後にスペースや)で決定したい。他にも;とかも?
  (defun corfu-my-insert-RET () ;; corfu-で始まるかで動作が変わるところがある。 (savex C-M-i RETと素早く押したときに、RETがcorfu-で始まるコマンドだとupdateが走り正常に動作するが、my-corfu-だとそうならない。
    (interactive)
    (corfu-insert)
    (call-interactively 'newline));; インタラクティブじゃないとインデントされなかったりする。
  (defun corfu-my-insert-self ()
    (interactive)
    (let ((ch last-command-event))
      (when (characterp ch)
        ;; (2024-03-03追記:その文字を含んだ候補があるなら補完を終了しないようにした)
        (when (and (>= corfu--index 0)
                   (not (seq-some (lambda (str) (seq-contains-p str ch))
                                  corfu--candidates)))
          (corfu-insert))
        (insert-char ch))))
  (define-key corfu-map (kbd "RET") 'corfu-my-insert-RET)
  (define-key corfu-map (kbd "SPC") #'corfu-my-insert-self)
  (define-key corfu-map (kbd ")") #'corfu-my-insert-self)
  (define-key corfu-map (kbd "]") #'corfu-my-insert-self)
  (define-key corfu-map (kbd "}") #'corfu-my-insert-self)

  ;; その他手動補完時のキー設定
  (define-key corfu-map (kbd "M-TAB") #'corfu-complete) ;;M-TABをTABと同じにすることでM-TAB二回(C-M-i二回)で一つ目の候補を選択出来るようになる。C-M-i C-iと押すより楽
  )

  ;; Corfuの候補リストにアイコンを表示する。
  (when (locate-library "kind-icon")
    (setq kind-icon-default-face 'corfu-default)
    (add-to-list 'corfu-margin-formatters #'kind-icon-margin-formatter))

  ;; lsp-modeの設定はeglotへ移行して使わなくなったので削除。
  )
2024-02-14

Corfuの自動補完と手動補完で補完スタイルを変える方法

そもそも補完スタイルとは何かについてはマニュアルを参照するのが手っ取り早いでしょう(Completion Styles (GNU Emacs Manual)(日本語訳)。簡単に言えば先頭一致とか部分一致とか曖昧一致とかそういうのです。入力したテキストと補完候補が一致していることをどのように判定するかのルールです。例えば basic はほぼ先頭一致ですがカーソル(ポイント)を左に移動したときの挙動が追加されています(純粋な先頭一致は emacs21emacs22)。 substring はほぼ部分一致です。 flex は含まれている文字が順番に登場すれば一致と見なします。このようなルールを変数completion-stylesで指定します。複数指定出来るのは、マッチする候補があるスタイルを順に探していく仕組みになっているからです。

それで以前Corfuを導入したときに、私は自動補完と手動補完で補完候補のソースを変える設定をしました。

companyからcorfuへ移行~自動と手動で補完候補を変える | Misohena Blog

これは自動補完の時に確度の低い候補が出てきてキー入力を奪ってしまうことを回避するのが目的でした。自動補完の時は補完候補の大本を確度の高い物だけに限定してしまおうということです。

しかしそれだけでは不十分で、補完スタイル、つまり補完候補と入力テキストとのマッチング方法によっても実際に出現する候補は変わってきます。例えば補完スタイルにflexなどを指定してしまうと、非常に多くの補完候補とマッチングしてしまい、自動補完のポップアップが頻繁に出現することになりかねません。かといって、手動で補完するときはより沢山候補を出してほしい場合もあるでしょう。自動と手動で補完スタイルを切り替えたいのは当然ではないでしょうか。

というわけで、それを行うコードは次のようになります。

(defun my-corfu--auto-complete-deferred--change-completion-styles (old-fun &rest args)
  ;; corfu-autoの作用で補完候補を出すときに呼び出される
  (let (;; 自動補完の時に使う補完スタイル
        (completion-styles '(basic)))
    ;; 元の処理
    (apply old-fun args)))

(advice-add 'corfu--auto-complete-deferred :around #'my-corfu--auto-complete-deferred--change-completion-styles)

corfu--auto-complete-deferredは自動補完時にのみ呼び出される関数です。そこで一時的にcompletion-stylesをbasicのみにしてしまうと、入力したテキストと先頭一致する候補しか出てこなくなります。

これでひとまず目的は達成できたのですが、結局不意に現れた自動補完ポップアップによって誤操作してしまうという問題は相変わらず完全には解決できていません。自動補完で出す候補を少なくすることは確かに誤操作をする機会を減らしますが、一方で補完できる機会も減らしてしまいます。

そもそも自動補完というのは何が良いのでしょうか。別に補完がしたければ明示的にC-M-iと押せば補完できるのですからそれで良いはずです。しかしそれは補完できることをあらかじめ知っていなければできません。自動補完の良い所は、ユーザーが「こんな補完ができるのか」と気がつけるところにあるのです。そう考えたときに、別の方法を思いつきました。

(続く)

2024-02-05 ,

org-modeにEmacs Lisp要素へのリンクタイプを追加する(org-elisp-link.el)

以前「Emacs Lisp要素へのorg-modeリンクをエクスポートする」や「Emacs Lisp要素へのリンクをorg-modeに追加する」という記事を書きましたが、そこで書いた物を org-elisp-link.el として一つのEmacs Lispにまとめました。

misohena/org-elisp-link: Org-mode Link Types for Emacs Lisp Elements

同様の事をやるEmacs Lispはいくつか見かけましたが、エクスポートまでするのは見つかりませんでした。org-modeのリンクタイプはエクスポートを実装していないものが多い気がします。もちろんEmacs内での作業に役に立てばほとんどの場合それで十分なのですが、たまにエクスポートすると「あれ?」と思うことがあります。

Emacs Lispの言語要素(関数、変数、フェイス、ライブラリ)へのリンクをエクスポートしたいなんて人はそう多くは無いでしょう。誰得? オレだよオレ、俺得。私はこのブログで関数名や変数名を書くことがありますし、自分で見返したときにいちいちEmacsで定義を見に行くよりもブラウザでソースコードへ飛べた方が便利なケースもあります(常にとは言わない)。

READMEにも書きましたが、このEmacs Lispを使うと次のような記述が可能になります。

[[elisp-function:track-mouse]]関数は[[elisp-library:subr;line=4530][subr.elの4530行目]]に定義されています。[[elisp-variable:track-mouse]]という変数も別に定義されています。[[elisp-function:track-mouse]]関数は例えば[[elisp-function:artist-mouse-draw-continously;library=artist]]で使われています。

もちろんC-c C-o (org-open-at-point)で飛べますし、C-c l (org-store-link)でのリンクストア操作にも対応しています。

エクスポートについては以前「Emacs Lisp要素へのorg-modeリンクをエクスポートする」に書いたとおり、現在インストールされているソースコードの中からファイル名と行番号を探し、それに対応するWeb上のコードブラウザへのURLを作成しています。実際に上をエクスポートすると下のようになります。

<p>
<code><a href="https://git.savannah.gnu.org/cgit/emacs.git/tree/lisp/subr.el?h=emacs-29.2#n4530">track-mouse</a></code>関数は<a href="https://git.savannah.gnu.org/cgit/emacs.git/tree/lisp/subr.el?h=emacs-29.2#n4530">subr.elの4530行目</a>に定義されています。<code><a href="https://git.savannah.gnu.org/cgit/emacs.git/tree/src/keyboard.c?h=emacs-29.2#n12850">track-mouse</a></code>という変数も別に定義されています。<code><a href="https://git.savannah.gnu.org/cgit/emacs.git/tree/lisp/subr.el?h=emacs-29.2#n4530">track-mouse</a></code>関数は例えば<code><a href="https://git.savannah.gnu.org/cgit/emacs.git/tree/lisp/textmodes/artist.el?h=emacs-29.2#n4899">artist-mouse-draw-continously</a></code>で使われています。
</p>

以前書いたEmacsをアップグレードしたときにエクスポート結果が変わってしまう問題に対処するため、いくつかオプションを指定出来るようにしました。

[[elisp-function:tetris-start-game;line=600;library=tetris;emacs-version=29.2][Emacs 29.2におけるtetris.el内の600行目にあるtetris-start-game関数]]

このように書けばEmacs 29.2におけるtetris.el内の600行目を指すURLが必ずエクスポートされます。まぁ、常にこのような記述をすべきだとは思いませんけど。

リンクのdescription部分を書いていないときに見た目が酷いことになるので、 :activate-function を使って、シンボル名以外の部分を隠す機能も用意しておきました。前から [[elisp-function:track-mouse]] と書くと elisp-function: の部分が邪魔だなぁと思っていたのでした。もちろんdescription部分も含めて [[elisp-function:track-mouse][track-mouse]] と書けば良いのですが、情報が重複してて嫌だなぁと思っていたのでした。

その他README.org(日本語)に色々説明を書いたので詳しくはそちらで。

以下メモ:

;や&を含む関数名は存在する(c-forward-to-nth-EOF-;-or-}c-semi&comma-inside-parenlist)。もちろん<は>はある(string<とか)。\を含むものは見当たらない。ちゃんとエスケープできるようにした。

org-link-parameters:activate-func は使い方が難しいのだけど(特に効果を打ち消す方。変更フックでは検出できないケースもあるので)、すでに隠している部分を少し広げるくらいなら問題ないと思う。

find-funcライブラリの中身を見てEmacsが各種定義場所を探すときに何をしているのか色々勉強になった。もうちょっと直交性がある感じで綺麗に書いて欲しい。関数がnilで変数がdefvarでフェイスがdeffaceとか終始そんな感じ。いや、そもそもライブラリ名がfind-funcだった。

find-function-regexp-alistが興味深い。その正規表現(find-function-regexpとか)を見ると、思っていたより色々関数や変数等を定義する書式があることが分かる。ただ、この正規表現は%s部分に名前を入れて関数や変数等の定義を探すためのものなので、今回の用途に直接使うのは難しい。

結構頑張ってdefcustomを沢山用意した。

先日Emacs Widget Libraryの勉強をしたのでdefcustomの:type部分を書くのがとても楽になった。

バッファ内オプション( #+HTML_LINK_???? みたいの)の処理とテンプレート文字列( <a href="{{{URL}}}">{{{CONTENTS}}}</a> みたいなの)の処理は以前org-geolinkを作ったときのものがわりとよく出来ていたのでそのまま持ってきた。バッファ内オプションを増やす方法はもうちょっとマシな方法がないのだろうか。それほどちゃんと調べていないのでよく分からない。

ELPAのURL変換はもうちょっと何とかならないだろうか。それと私はEmacs設定ディレクトリ(Gitで管理している)のsubmoduleにしているものも多いので(自分の作ったものは特に)、それを検出してGitHubへのURLを生成したい。

elisp-functionとelisp-funのどちらがいいか。elfunというのもあり? elvar、elface、ellib。

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のコードを書いて、その別言語のコードを生成したことは度々あった気がします) 考え出すと切りがないです。文字列などクォートされたセミコロンを考慮しなければならないので単純な正規表現で処理するのもためらわれたりと色々面倒なところもありました。