Yearly Archives: 2024

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へ移行して使わなくなったので削除。
  )