Author Archives: AKIYAMA

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)