Author Archives: AKIYAMA

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。