Monthly Archives: 8月 2022

2022-08-29 ,

折りたたみ状態によって見出しのマークを切り替える(org-mode)

org-modernを入れたのでこれまで使っていたorg-bulletsをお払い箱にして見出しの表示設定を調整しました。

これまで私が使っていた全角■●▲(←全角で表示されていますか?)等はどうにも野暮ったかったので、半角で表示される右三角にしてみたところ結構いい感じになりました。しかし右三角を使うと、開いたときに下向き三角になって欲しい気がしてしまいます。というわけでやってみました。

結果:

TABキーによって見出しのマークが切り替わる様子
図1: TABキーによって見出しのマークが切り替わる様子

コード:

;; org-modern.el (2022-12-22) に対する変更

;; まずは深さ毎の見出しマーク文字列(展開時、折りたたみ時の両方)をあらかじめ組み立てます。
;; org-modernではorg-modern-modeを起動したときにできるだけpropertizeした文字列を
;; 変数にキャッシュしておくようになっているので、それに倣いました。

(defvar-local org-modern--open-star-cache nil)
(defvar-local org-modern--folded-star-cache nil)

(defun my-org-modern--cache-star ()
  ;; 状態によって次の記号を使う。
  ;;  open(unfolded): BLACK RIGHT POINTING TRIANGLE (U+25B6)
  ;;  folded: BLACK DOWN POINTING SMALL TRIANGLE (U+25BE)
  ;; (SMALLを使ったのは手元の環境できっちり半角で表示される下向き黒三角がこれだけだったので)
  ;; (2022-12-22削除:深さに応じて先頭に空白を入れる。)
  ;; (2022-12-22追加:深さに応じて先頭に空白を入れるには org-modern-hide-stars に空白文字を指定すること。org-modern 0.6以降の機能)
  ;; この辺は好みで。
  (setq
   org-modern--open-star-cache
   (vconcat (cl-loop for level from 1 to 10
                     ;; (2022-12-22修正:本家でpropertizeを使うコードがorg-modern--symbolに変わったので追従。また、levelに応じて空白を入れるのを止めた)
                     collect (org-modern--symbol "▾")))
   org-modern--folded-star-cache
   (vconcat (cl-loop for level from 1 to 10
                     ;; (2022-12-22修正:本家でpropertizeを使うコードがorg-modern--symbolに変わったので追従。また、levelに応じて空白を入れるのを止めた)
                     collect (org-modern--symbol "▶")))))
(advice-add #'org-modern-mode :before #'my-org-modern--cache-star)

;; 次に折りたたみ状態に(開閉状態)によってfontify時に使うキャッシュを切り替えます。
;; 折りたたみ状態は見出し行の直後が不可視状態になっているかで判断しています。

(defun my-org-modern--star-around (original-fun &rest args)
  "Prettify headline stars."
  ;; 開閉状況によって org-modern--star-cache を切り替える。
  (let* ((folded (invisible-p (line-end-position)))
         (org-modern--star-cache (if folded
                                     org-modern--folded-star-cache
                                   org-modern--open-star-cache)))
    (apply original-fun args)))
(advice-add #'org-modern--star :around #'my-org-modern--star-around)

;; 最後に折りたたみ状態が切り替わったときに見出し行をfontifyし直します。
;; org-modeがセクションを表示したり非表示にしたりするとき、必ず
;; org-flag-regionやoutline-flag-regionが呼ばれます。
;; 表示/非表示する範囲の一行前くらいから見出し行を抽出してfont-lock-flushで
;; 再fontifyを促します。

(defun my-org-modern-flush-headings (from to flag)
  (save-match-data
    (save-excursion
      (goto-char from)

      ;; 1行前から更新する。更新すべき見出しが先行しているかもしれないので。
      (forward-line -1)

      ;; 閉じるときは一行前からFROMまでを処理すれば十分。
      ;; FROM以降は隠されて見えないし、開くときはflag=nilでここが呼ばれる。
      (when flag ;;hide region FROM..TO
        (setq to from))

      (while (re-search-forward (concat "^" org-outline-regexp) to t)
        (font-lock-flush (line-beginning-position)
                         (min (1+ (line-end-position)) (point-max)))))))

(defun my-org-modern-flag-region-advice (original-fun from to flag &rest args)
  (apply original-fun from to flag args)
  ;; org-modeやoutline-modeでFROMからTOまでを表示したり隠したりしたときに、
  ;; その中にある見出し行をfont-lockし直す。
  ;; font-lock側では現在の開閉状況によって見出し行を変化させる。
  (my-org-modern-flush-headings from to flag))

(advice-add #'outline-flag-region :around #'my-org-modern-flag-region-advice)

;; (2022-12-22修正:Org 9.6からorg-flag-regionはobsoleteになってorg-fold-core-regionが使われるようになったので修正。)
(if (version<= "9.6" (org-version))
    (when (fboundp 'org-fold-core-region)
      (advice-add #'org-fold-core-region :around #'my-org-modern-flag-region-advice))
  (when (fboundp 'org-flag-region)
    (advice-add #'org-flag-region :around #'my-org-modern-flag-region-advice)))

今回のことでorg-mode(やoutline-mode)が領域を表示/非表示にする流れについて理解が深まりました。

以前、折りたたみ状態によって見出し行の大きさや行間スペースを変えたいと思ったこともあるので、今回の応用でそういったことも可能になるかもしれません。

2022-08-27 ,

org-modernとorg-indentを併用したときの表の乱れを直す

org-modern

先日org-modernを試してみました。org-modeの各部の見た目を綺麗に(モダンに)してくれます。

そんな中で私が最も気に入ったのは、表(テーブル)の線を綺麗にしてくれるという点です。

org-modernで見た目を改善したorg-modeの表
図1: org-modernで見た目を改善したorg-modeの表

もはやorg-modeと言われなければ分からないと思います。

いくつか問題もあって、 org-table-toggle-coordinate-overlays による座標表示は機能しなくなります。まぁ、仕方ないですね。(2022-08-27追記: org-table-toggle-column-width あたりも問題がありますね。これはちょっと気になるかなぁ)

その辺りは許容するとして、私が使っていて最も気になったのはorg-indentと併用した際の問題です。org-indentは階層に応じて左にインデントをつけてくれるモードです。org-modernはこのorg-indentと相性が悪いです。例えばブロック(#+begin_???から#+end_???までの間)の左に装飾を入れる機能があるのですが、これはorg-indentが有効になっているときは機能しません。

org-indentの併用で表に発生する問題

表の線についてはorg-indent使用時でも一見正しく機能するように見えましたが、よく見ると二つほど問題がありました。

  • 横線の下に大きく空白が空く
  • ウィンドウの先頭にある行がインデントされない
修正前
図2: 修正前

原因

横線の下に大きく空白が空く原因は、org-indentが挿入する空白文字が高さを持ってしまっているところにあります。高さを持っている以上、その行がそれ以上縮まることはありません。

ウィンドウの先頭がインデントされない原因は、先日書いたEmacsのバグにあります。

原因
図3: 原因

修正

org-indentで挿入する空白の高さを1ピクセルにする

org-indentが挿入する文字列が高さを持ってしまっていることが原因なので、それを無くせば良いのです。文字列では無くdisplayプロパティのspace指定を使うことで高さ1ピクセルの空白を作ります。

;; org-indentを使っていると表の水平線の高さが狭まらない問題を修正する。
;; インデントの空白文字列の高さよりも小さくならないのが原因。
;; インデントの空白文字列をdisplayプロパティで高さ1pxのspaceに置き換える。
;; @todo wrap-indentも修正すべき?
(defun my-org-indent--compute-prefixes-after ()
  ;; org-indent--text-line-prefixesはレベル毎のline-prefix。
  ;; org-indent--compute-prefixesがそれを計算した後ここが呼ばれる。
  (let ((prefixes org-indent--text-line-prefixes))
    ;; 各レベルのprefixを修正する。
    (dotimes (i (length prefixes))
      (let* ((space-str (aref prefixes i))
             (space-length (length space-str)))
        (when (> space-length 0)
          (aset prefixes i
                ;; テキストプロパティ
                ;; display (space :width i :height (1))
                ;; を追加する。
                ;; つまり、line-prefixとして空白文字ではなく高さ1pxのspace
                ;; が表示されるようになる。
                ;; これによって、(prefix以外の)行の高さが正しく反映される
                ;; ようになる。これまでは行が小さくなってもprefixの空白文
                ;; 字の高さより小さくならなかった。
                (org-add-props
                    space-str
                    nil
                  'display (cons 'space
                                 (list :width space-length
                                              ;; (list (* space-length
                                              ;;          (frame-char-width)))
                                       :height '(1))))))))))
(advice-add #'org-indent--compute-prefixes :after
            #'my-org-indent--compute-prefixes-after)

元々このorg-indentの空白文字を入れるという挙動は、org-modernに限らず他でも同様の問題を引き起こす可能性があるはずです。例えばfaceをカスタマイズして特定の行の文字サイズを小さくしたとき、文字は小さくなったのに行が一緒に小さくならなず無駄に空白が空くという問題が生じる可能性があります。

表の縦線部分をdisplay (space …)ではなくdisplay " "にする

先日書いた通りこの問題はdisplayプロパティにspaceではなく文字列を指定すれば発生しません。文字列にすると幅と高さを自由に指定できなくなってしまいますが、face属性に:height 0.1を設定することで極小文字にして回避します。

;; org-modern--tableを差し替える。
(defun org-modern--table ()
  "Prettify vertical table lines."
  (save-excursion
    (let* ((beg (match-beginning 0))
           (end (match-end 0))
           (tbeg (match-beginning 1))
           (tend (match-end 1))
           ;; Unique objects
           (sp1 (list 'space :width 1))
           (sp2 (list 'space :width 1))
           (color (face-attribute 'org-table :foreground nil t))
           (inner (progn
                    (goto-char beg)
                    (forward-line)
                    (re-search-forward "^[ \t]*|" (line-end-position) t)))
           (separator (progn
                        (goto-char beg)
                        (re-search-forward "^[ \t]*|-" end 'noerror))))

      ;; 横線を引く
      (goto-char beg)
      (when separator
        ;; overlineを引いて高さを縮める
        (when (numberp org-modern-table-horizontal)
          (add-face-text-property tbeg tend `(:overline ,color) 'append)
          (add-face-text-property beg (1+ end) `(:height ,org-modern-table-horizontal) 'append))
        ;; 横幅を1文字分確保する(縦線部分以外)
        (while (re-search-forward "[^|+]+" tend 'noerror)
          (let ((a (match-beginning 0))
                (b (match-end 0)))
            ;; TODO Text scaling breaks the table formatting since the space is not scaled accordingly
            (cl-loop for i from a below b do
                     (put-text-property i (1+ i) 'display
                                        (if (= 0 (mod i 2)) sp1 sp2))))))

      ;; 縦線を引く
      (goto-char beg)
      (while (re-search-forward
              "-+\\(?1:+\\)-\\|\\(?:^\\|[- ]\\)\\(?1:|\\)\\(?:$\\|[- ]\\)"
              end 'noerror)
        (let ((a (match-beginning 1))
              (b (match-end 1)))
          (cond
           ((and org-modern-table-vertical (or (not separator) inner))
            (add-text-properties
             a b
             `(;; vertical lineにspaceを使うとウィンドウ先頭でline-prefixが効かなくなる。
               ;;display (space :width (,org-modern-table-vertical))
               display
               " "
               face
               (:inherit org-table :inverse-video t)
               ))
            (add-face-text-property a b`(:height 0.1) 'append) ;;0.1の部分は,org-modern-table-verticalとしたいところだけどピクセル数で指定されるので無理。
            )
           ((and org-modern-table-horizontal separator)
            (put-text-property
             a b
             ;; vertical lineにspaceを使うとウィンドウ先頭でline-prefixが効かなくなる。
             ;;'display `(space :width (,org-modern-table-vertical))
             'display " "))
           (t (put-text-property a b 'face 'org-hide)))))
      )))

結果

修正後
図4: 修正後
2022-08-18

display spaceを併用するとウィンドウの先頭でline-prefixが効かなくなる件(Emacs Bug?)

org-modernを試したときに表の先頭が(org-indentによって)インデントされていないことに気がついた。(Emacs 28.1で確認)

ウィンドウの先頭部分が崩れている表
図1: ウィンドウの先頭部分が崩れている表

原因を調べたところ、org-modernに限らず次の条件で問題が起きることが分かった。

  • 行の先頭に対してline-prefixテキストプロパティとdisplayテキストプロパティの両方が指定されている
  • displayプロパティに(space …)を指定している
  • その行がウィンドウの先頭にある

この条件を満たすとき、なぜかline-prefixの効果が消えてしまう。

再現するコードは次の通り。

(progn
  ;; ウィンドウの先頭へ移動
  (goto-char (window-start))
  ;; line-prefixとdisplayの両方のテキストプロパティを持つテキストを挿入
  (insert (propertize "TEXT" ;;←この文字列はdisplayプロパティで置換される
                      'line-prefix "[PREFIX-STR]" ;;←行の前にこれが表示されるはず
                      'display '(space :width 1) ;;NG
                      ;;'display (svg-image (svg-create 10 10)) ;;OK
                      ;;'display "[DISPLAY-STR]" ;;OK
                      )))

displayテキストプロパティの値が(image …)の場合や単なる文字列の場合この現象は起きない。例えば単なる文字列の場合は[PREFIX-STR][DISPLAY-STR]と表示されるが、(space …)の場合は[PREFIX-STR]が表示されず1文字分の空白が表示されるだけとなる。

回避方法はちょっと思いつかない。いっそ表の部分はインデントを全部無効化するとか?

最初phscrollのせいかと思ったがそういうわけでは無さそうだ。

以前にもline-prefixでマウス入力の座標がずれる問題に遭遇したことがある。Emacsのソースコードを確認していないが、この辺りの処理には何らかの構造的な問題があるのかもしれない。私の経験的にもレイアウト処理というのはちゃんと設計しないと複雑怪奇なものになりがちだ。

それにしてもorg-modernの罫線の引き方、displayテキストプロパティの(space :width (1)) で幅1pxの空白を作って、faceに:inverse-video tを指定することで実現してるんだ! だからline-spacingがあっても隙間無く線が表示される。そんな方法考えもしなかった。

(insert
 ;; 1行目 赤い縦線にABC 行間は10
 (propertize "X"
             'display '(space :width (1))
             'font-lock-face '(:inverse-video t :foreground "red"))
 (propertize "ABC\n" 'line-spacing 10)
 ;; 2行目 赤い縦線にDEF
 (propertize "X"
             'display '(space :width (1))
             'font-lock-face '(:inverse-video t :foreground "red"))
 "DEF")

あー、透明な画像で1pxの空白を作れば回避可能かもしれない。でも全行に画像を挿入しまくるのもなぁ……。

(2022-08-27追記: org-modernでorg-indentを使っていると表のウィンドウ先頭部分がインデントされない問題は、displayテキストプロパティを" "にしてfaceの:heightを0.1にすることで回避した。org-modern–tableを色々といじると直せる。ちなみにこれとは別の話だが、org-indentを使っていると表の水平線の下に空白が空いてしまう問題は、org-indentが挿入する空白文字列の高さを小さくすることで回避できる。詳しくはorg-modernとorg-indentを併用したときの表の乱れを直すに書いた)

2022-08-16 ,

org-modeの起動時間を短縮する(org-babel-load-languages編)

org-modeの読み込みは非常に遅い。

原因は色々あるが、その一つが org-babel-load-languages の読み込みである。 org-babel-do-load-languages 関数によって、 org-babel-load-languages で指定されている全ての言語バックエンド(ob-???.el)を起動時に読み込んでしまい、その結果数秒も待たされることがある。

解決策はいくつか考えられる。一つはorg-mode起動後にタイマーによって少しずつ読み込んでいく方法。もう一つは必要になってから必要な言語だけ読み込む方法。

今回は後者を実現する。

ただし、この方法はorg-modeのバージョンアップによって機能しなくなる可能性が前者の方法より高い。org-modeは起動時に全ての言語バックエンドが読み込まれていることが前提で書かれており、言語バックエンドが必要なところで必ず呼び出す関数などは存在しない。強いて言えば、 (intern (concat "org-babel-???:" lang)) のようなコードによって org-babel-???:??? のようなシンボルを生成している箇所があちこちに存在する(grep '"org-babel-.*:'等で検索すると良い)。今回はそのような場所を詳しく調査することで半ば場当たり的に言語バックエンドが必要な箇所に処理を挟んで遅延読み込み処理を追加した。従ってorg-modeのバージョンアップに弱くなっている。

しかしながら調査した結果、大半は一つのパターンで対処が可能であることが分かった。ほとんどの場合、言語バックエンドを必要とする処理の前にはソースブロックの言語名を取得する処理が入っており、それは (org-element-property :language element) のような形になっている。このコードはorg-elementで解析した構文要素オブジェクトから言語名プロパティを取得するものだ。このコードで返すのは#+begin_srcの後の言語名なので、 org-element-property にadviceをかけて戻り値の言語名を元に言語バックエンドを読み込んでしまえば良い。 org-element-property は頻繁に呼ばれる関数なのであまり気は進まないが、少なくとも改変の影響はorg-modeの範囲に留まる(intern等に引っかけるよりはマシである)。

org-babel-get-src-block-infoorg-babel-lob-get-info のようなソースブロックの情報を返す関数も、結局はorg-elementで解析を行い要素の:languageプロパティを取得している。

まれにそれ以外の方法で言語名を生成している場合がある。例えばob-table.elには"emacs-lisp"のように言語名をハードコードしている箇所がある。また、 org-babel-enter-header-arg-w-completion 関数に対して (match-string) の値を言語名として引き渡している箇所もある。このようなケースには個別に対処する必要がある。

;; 使い方:
;; 1. org-babel-load-languagesの値はCustomizeの方ではnilにしておくこと。
;; 2. (with-eval-after-load "org"
;;      (load "このコードを含むファイル")) などとする。

;; 使用する言語名とそれを提供するelファイル名の一覧。
(defvar my-org-babel-languages
  ;;(言語名 . ob-ファイル名.el)
  '((elisp . emacs-lisp)
    (emacs-lisp . emacs-lisp)
    (makefile . makefile)
    (ditaa . ditaa)
    (dot . dot)
    (plantuml . plantuml)
    (perl . perl)
    (cpp . C)
    (C++ . C)
    (D . C)
    (C . C)
    (js . js)
    (java . java)
    (org . org)
    (R . R)
    (python . python)
    (shell . shell)
    (sh . shell)
    (bash . shell)
    (zsh . shell)
    (fish . shell)
    (csh . shell)
    (ash . shell)
    (dash . shell)
    (ksh . shell)
    (mksh . shell)
    (posh . shell)))

(defun my-org-babel-language-files ()
  "重複しない全ての言語バックエンドファイル名を返す。"
  (seq-uniq (mapcar #'cdr my-org-babel-languages)))

;; my-org-babel-languagesからorg-babel-load-languagesを設定する。
;; org-lintやorg-pcompleteにorg-babel-load-languagesを使った処理がある
;; ようなので。
;; このときcustom-set-variablesを使わないようにすること。
;; org-babel-do-load-languagesが呼ばれて全部読み込まれてしまうので。
(setq org-babel-load-languages
      (mapcar (lambda (lang) (cons lang t)) ;;(emacs-lisp . t)のような形式
              (my-org-babel-language-files)))

(defun my-org-require-lang-file (lang-file-name)
  "ob-LANG-FILE-NAME.elを読み込む。"
  (when lang-file-name
    (require (intern (format "ob-%s" lang-file-name)) nil t)))

(defun my-org-require-lang (lang)
  "LANGを読み込む。"
  (my-org-require-lang-file
   (alist-get
    (if (stringp lang) (intern lang) lang)
    my-org-babel-languages)))

(defun my-org-require-lang-all ()
  "全ての言語を読み込む。"
  (mapc #'my-org-require-lang-file
        (my-org-babel-language-files)))

;; org-elementで言語名を返す時、その言語をロードする。
(advice-add #'org-element-property :around #'my-org-element-property)
(defun my-org-element-property (original-fun property element)
  (let ((value (funcall original-fun property element)))
    (when (eq property :language)
      (my-org-require-lang value))
    value))

;; ob-table.elに(org-babel-execute-src-block nil (list "emacs-lisp" "results" params))のような呼び出し方をする所があるので。
(advice-add #'org-babel-execute-src-block :around
            #'my-org-babel-execute-src-block)
(defun my-org-babel-execute-src-block (original-fun
                                       &optional arg info params)
  (my-org-require-lang (nth 0 info))
  (funcall original-fun arg info params))

;; (match-string)の値を直接langとして渡しているので。
(advice-add #'org-babel-enter-header-arg-w-completion :around
            #'my-org-babel-enter-header-arg-w-completion)
(defun my-org-babel-enter-header-arg-w-completion (original-fun
                                                   lang)
  (my-org-require-lang lang)
  (funcall original-fun lang))

;; org-lint(org-lint-wrong-header-argument, org-lint-wrong-header-value)内で参照しているので。
;; 面倒なので全部読み込んでしまう。
(advice-add #'org-lint :around #'my-org-lint)
(defun my-org-lint (original-fun &rest args)
  (my-org-require-lang-all)
  (apply original-fun args))

;; 他にもinfoやlangを引数に取るような関数がある。
;; my-org-element-propertyやorg-babel-get-src-block-info等を使ってlangや
;; infoを取得していれば問題ないが、予期していない方法でlangやinfoを取得し
;; ている場合は対処する必要がある。

と、書いた後にorg-modeのメーリングリストに次のような投稿を見つけた。

Load Org Babel Languages on Demand

見たところ org-src--get-lang-modeorg-babel-confirm-evaluate にadviceを追加して似たようなことをしている。 org-src--get-lang-mode 関数は現在見当たらないが何か変更があったのだろうか( org-src-get-lang-mode はあるので改名された?)。

2020年の投稿だがその後どうなったのかは不明。

ちゃんと修正するのであれば、まずはあちこちに散らばっている org-babel-*:* シンボルを組み立てる処理を関数にまとめるのが良さそう。そしてその関数から言語名に対応するob-ファイルを読み込むようにすれば良い。有効言語の設定変数をどうするかは迷い所だが org-babel-load-languages の形式を拡張できるかもしれないし、あるいは別に変数を用意しても良さそうだ。

2022-08-15

Ivy/CounselからVertico/Consultへ移行~補完候補以外を選びづらい問題

通常バッファ内での補完をcompanyからcorfuに変えたので、ミニバッファ補完もivyからverticoへ変えてみた。

設定:

(vertico-mode)
(setq vertico-cycle t) ;;最初と最後の候補を行き来できるようにする
(setq completion-styles '(basic substring partial-completion flex)) ;;適当

;; 大文字小文字の区別をしない
(setq read-file-name-completion-ignore-case t
      read-buffer-completion-ignore-case t
      completion-ignore-case t)

;; 候補更新時に最初の候補を選択しない (2023-03-19追記: verticoにカスタマイズ変数が追加された! https://github.com/minad/vertico/commit/bedd146c3ffc236d746d088a94c3858eca0618d9 (Add vertico-preselect option (Fix #306) · minad/vertico@bedd146))
(setq vertico-preselect 'prompt)

;; (2023-04-18追記)
;; ただし、require-matchがt(やそれに類するもの)で入力が空ではなくマッ
;; チする候補がある場合は、その候補の先頭を選択する。
(defun my-vertico--recompute (orig-fun pt content &rest args)
  (let ((result (apply orig-fun pt content args)))
    (if (and (not (equal content "")) ;;入力が空の時は(require-matchであっても)defaultまたはnilを返すことになっている。
             (> (alist-get 'vertico--total result) 0)
             ;; completing-readの説明によれば
             ;; nil,confirm,confirm-after-completion以外はtのように
             ;; 振る舞うべき。
             (not (memq minibuffer--require-match
                        '(nil confirm confirm-after-completion))))
        (setf (alist-get 'vertico--index result) 0))
    result))
(advice-add #'vertico--recompute :around #'my-vertico--recompute)

;; 以下は過去のもの
;; ;; 候補更新時に最初の候補を選択しない (2022-12-01追記: vertico--allow-prompt-p関数が無くなって代わりに戻り値にvertico--allow-promptが増えたので修正)
;; (defun my-vertico--recompute (original-fun &rest args)
;;   ;; vertico--update-candidatesの最後の処理を置き換える。
;;   (let ((result (apply original-fun args)))
;;     (when result
;;       (let ((lock         (alist-get 'vertico--lock-candidate result))
;;             (allow-prompt (alist-get 'vertico--allow-prompt result))
;;             (index        (alist-get 'vertico--index result)))
;;         (when (and (not lock)
;;                    allow-prompt)
;;           ;; lockされておらず, require-matchじゃない場合は現在入力中の文字列を選択する。
;;           (setf (alist-get 'vertico--index result) -1))))
;;     result))
;; (advice-add #'vertico--recompute :around #'my-vertico--recompute)

;; ;; 候補更新時に最初の候補を選択しない (2022-10-24追記: 関数名や戻り値が変わったので修正)
;; (defun my-vertico--recompute (original-fun &rest args)
;;   ;; vertico--update-candidatesの最後の処理を置き換える。
;;   (let ((result (apply original-fun args)))
;;     (when result
;;       (let ((lock        (alist-get 'vertico--lock-candidate result))
;;             (def-missing (alist-get 'vertico--default-missing result))
;;             (index       (alist-get 'vertico--index result)))
;;         (when (and (not lock)
;;                    (let ((vertico--default-missing def-missing)) (vertico--allow-prompt-p)))
;;           ;; lockされておらず, require-matchじゃない場合は現在入力中の文字列を選択する。
;;           (setf (alist-get 'vertico--index result) -1))))
;;     result))
;; (advice-add #'vertico--recompute :around #'my-vertico--recompute)

;; ;; 候補更新時に最初の候補を選択しない (旧バージョン用)
;; (defun my-vertico--recompute-candidates (original-fun &rest args)
;;   ;; vertico--update-candidatesの最後の処理を置き換える
;;   (let ((result (apply original-fun args)))
;;     (when result
;;       (unless (nth 3 result) ;;3=index
;;         (setq vertico--lock-candidate nil)
;;         (setf (nth 3 result) ;;3=index
;;               (if (vertico--allow-prompt-selection-p)
;;                   ;; require-matchじゃない場合は現在入力中の文字列を選択する
;;                   -1
;;                 ;; require-matchの場合は最初の候補を選択する
;;                 0))))
;;     result))
;; (advice-add #'vertico--recompute-candidates :around #'my-vertico--recompute-candidates)

;; より簡単な方法としてRETをvertico-exit-inputにするという方法もある。
;; (define-key vertico-map [remap exit-minibuffer] #'vertico-exit-input)
;; しかし補完候補がハイライトされるので気持ち悪い。
;; プロンプトがハイライトされているべき。

移行して一番気になったのが、何かを入力するとそれにマッチする候補の中から最初の物を選択してしまうという挙動だ。これはhelmでもivyでもどうしてもなじめなかった。

(2023-03-19追記: 最初の物を選択しないようにできるカスタマイズ変数 vertico-preselect が追加されたので設定でこの挙動は抑制できるようになった)

よく起きる問題としては、find-fileで新規ファイルを作るときに間違って既存のファイルを開いてしまうというものだ。ファイル名を入力してRETを押したとき、その入力した文字列と部分的にマッチする候補が開いてしまうのだ。

helmにせよivyにせよverticoにせよそれに対する標準の解決策は一応あって、何か特殊なキーで確定させるというものだ。ivyではC-M-j、verticoではM-RETで現在の入力をそのまま確定できる。helmは覚えていないが、何かしらあったと思う。

私はこの挙動にとても強い違和感を覚える。

find-fileで出てくるミニバッファ入力は、「任意の」ファイル名を入力するためのものだ。決して限られた候補の中から選ぶというものでは無い。Emacsの標準的な動作でも、任意の文字列を入力して明示的にTABを押したときだけ補完されるというものだ。どんなOSのファイル選択ダイアログだって、ファイル名を入力してEnterを押したら別のファイル名が入力されるなんてバカなことは起きない。それをverticoでは新規のファイルを作る時だけM-RETを押せと言うのである。Emacsを使う人たちは学習能力が高いからどんな特殊な操作でもすぐに慣れてしまうのだろうけど私はそんなものには慣れたくない。そんなことだから初心者に逃げられるのでは無いか。

Emacs Lispにおいて補完入力を行う関数はcompleting-readだ。Verticoはここにも作用する。

(completing-read "商品: " '("ringo-ame" "ringo-cake" "ringo-juice"))

上のような式を評価したとして、ringo RETと入力したらringoと入力されるべきでringo-ame等が入力されるのは納得いかない。ringo-ameやらは例えば開発者が気を利かせて用意しただけの何か優先度の低い候補に過ぎないかもしれない。ringoよりringo-ameを優先すべき理由はない。

(completing-read "商品: " '("ringo-ame" "ringo-cake" "ringo-juice") nil t)

completing-readの第4引数(REQUIRE-MATCH)がtならば分かる。この場合、任意の文字列は入力できず、必ず候補の中から選ばなければならないからだ(ただし何も入力せずRETを押したときは第7引数のデフォルト値またはnilとなる)。

というわけで、この辺りを修正したのが上の設定だ。文字列を入力すると候補は絞り込まれるが選択はされない。C-n、C-pで明示的に選択したときだけそれが使われる。ただし、REQUIRE-MATCHがtのときだけは最初の候補を自動的に選択する。

このやり方の欠点は大半は候補の中から選べば済むときでも必ず明示的に選択操作をしなければならない点だ。例えばfind-fileは既存のファイルを選択することの方が多いはずだ。また、switch-to-bufferも既存のバッファを選択することの方が多い(任意のバッファ名を入力して新しく作れるということを覚えていない人もいるのではないか)。こういったときに、一つに絞り込んだのにいちいち選択操作をしなければならないのは面倒に感じることもあるかもしれない。

しかしそれはfind-fileやswitch-to-bufferといった用途毎に特有な事情であって、ミニバッファ補完付き入力全般に適用出来る問題ではないはずだ。用途毎にどちらが多いかは異なるのだから。後は好みで、find-fileやswitch-to-bufferに設定があればよい。個人的にはswitch-to-bufferで新しいバッファを作る機能は使っていないので、REQUIRE-MATCH=t、つまり既存のバッファのみ選択出来るようになっていて構わない。find-fileはいちいち選択するのに苦は感じない。どちらにせよ一つに絞り込んだらTABを押してRETすれば素のEmacsと同じなのだから分かりやすい。

ミニバッファ補完を活用する応用コマンドについては、ivyに依存するcounselではなくより幅広いミニバッファ補完に対応したconsultへ移行した。比較してみるとcounselの方が若干使いやすいと感じることもあるが、両者そう大きくは違わない。それほど使い込んでいるわけではないというのもあるけれど。Embarkと組み合わせると絞り込んだ候補に対して色々アクションを適用出来たりするようだ。拙作のel-winsearchにはconsult版を追加した。

2022-08-15

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

corfucompanyよりは幾分素直で扱いやすい印象。

corfuの設定:

(2024-02-18追記: Corfuの自動補完で候補の存在を伝える事と候補を選べるようにする事を分離するで設定を書き直したので以下のコードは古い)

(setq corfu-cycle t) ;; 候補の最初と最後を行き来出来るようにする。
(setq corfu-auto t) ;; 自動的に補完候補を出す。
(setq corfu-preselect 'prompt) ;; 最初の候補を選択しない。誤入力が多すぎるので。

;; 無選択時のRETはquitだけでなく改行もする。
;; (2024-02-15修正:my-corfu-だと素早くC-M-iの後素早くRETを押したときに正しく補完されない。コマンド名がcorfu-で始まっているときだけupdateしている場所があるので)
(defun corfu-my-insert-or-newline ()
  (interactive)
  (if (>= corfu--index 0)
      (corfu--insert 'finished)
    (corfu-quit)
    ;; (2024-02-15修正:インタラクティブじゃないとインデントされなかったりする)
    (call-interactively 'newline)))
(with-eval-after-load "corfu"
  (define-key corfu-map (kbd "RET") 'corfu-my-insert-or-newline))

(global-corfu-mode)

;; lsp-modeでcorfuを使う。
;; (see: https://github.com/minad/corfu/wiki#example-configuration-with-flex)
(setq lsp-completion-provider :none)
(defun my-lsp-mode-setup-completion ()
  (setf (alist-get 'styles
                   (alist-get 'lsp-capf completion-category-defaults))
        '(flex)))
(add-hook 'lsp-completion-mode-hook #'my-lsp-mode-setup-completion)

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

(corfu-auto=tで)自動的に補完候補を出しつつ自動的に最初の候補を選択する(corfu-preselectが'firstや'valid)というのは誤操作を引き起こす可能性がある。 何かを入力してRETやTABを押したら自動的に出てきた補完候補の方を意図せず選んでしまったというようなことが起こりえる。そのためにcorfu-preselectを'promptにして明示的に選ばなければ補完しないように設定した。

それでも何かを入力してC-n RETを押すようなシチュエーションでは下に移動して改行するつもりが最初の候補を選択して確定してしまうので、結局誤入力は完全には回避できなかった。なので自動的に出す時の候補をできるだけ少なくする(自動ではあまり候補を出さない)ようにしてみた。補完候補(バックエンド)は completion-at-point-functions (capf)という元々Emacsに備わっている仕組みに一本化されているので、それを調整する。

completion-at-point-functionsの設定:

;; 補完候補を出すときの文脈を特定

(defvar my-capf-context nil)

;; (2022-10-26修正: corfu--auto-complete => corfu--auto-complete-deferred)
(defun my-capf--corfu--auto-complete-deferred (old-fun &rest args)
  ;; corfu-autoの作用で補完候補を出すときに呼び出される。
  (let (;; 自動で補完候補を出す文脈だということを変数に記録する。
        (my-capf-context 'in-corfu--auto-complete-deferred)
        ;; 2024-02-14追記:
        ;; 自動で補完候補を出すときは必ずbasicスタイルのみを使うべし!
        ;; 先頭すら一致していない候補をバリバリ出されたら鬱陶しい!
        (completion-styles '(basic)))
    ;; 元の処理
    (apply old-fun args)))
(advice-add 'corfu--auto-complete-deferred :around #'my-capf--corfu--auto-complete-deferred)

;; 追加の補完関数

(defun my-capf-additional ()
  (pcase my-capf-context
    ('in-corfu--auto-complete-deferred
     ;; 自動補完の場合は確度の高い候補しか出さない。
     nil)
    (_
     ;; 手動補完の場合は積極的にいろんな候補を出す。
     (my-capf-manual))))
(add-hook 'completion-at-point-functions #'my-capf-additional 100)

;; 手動補完時の補完関数

(defvar my-capf-manual nil)
(defun my-capf-manual ()
  ;; capeパッケージの読み込みを遅延させる。
  (unless my-capf-manual
    (setq my-capf-manual
          ;; いろんな補完候補を合成する。
          (cape-super-capf
           #'cape-file #'cape-dabbrev #'cape-abbrev #'cape-line)))
  (funcall my-capf-manual))

自動的に補完候補を出す場合は元々モードに備わっているような補完候補しか出さないようにした。プログラミング言語用のモードの場合は文法に則した候補が出るのでそれほど邪魔にならないと思われる。

逆にC-M-iで手動で補完候補を出す場合はcapeパッケージの多種多様な補完候補を利用する。手動で出しているのだから多少確度の低い候補が出てきても構わない。

cape-super-capfで複数のバックエンドをマージしている。

capeは他にもcompany用バックエンドをcapfに変換するアダプタも持っている。手元にはorg-modeの「#+」行をより良く補完する自作のCompanyバックエンドがあるので、このアダプタでcorfu用に変換した。

今のところM-/はdabbrev-expandのままにしてある。

補完候補については不満なところがまだまだ沢山あるので逐一直していくつもりだ。completion-stylesも過剰な補完生成に一役買っているように見える。verticoにせよcorfuにせよ、どうも補完することばかりを優先して補完させたくないケースを軽視しているように見える。

(2024-02-15追記: Corfuの自動補完で候補の存在を伝える事と候補を選べるようにする事を分離するで設定を追加した)

2022-08-14

2022年夏の新番組

タイトル 開始 時刻 配信
スプリガン -     Netflix
BASTARD!!(バスタード)-暗黒の破壊神- -     Netflix
× 神クズ☆アイドル 07/02 12:00 dアニメ
彼女、お借りします 第2期 -      
iiiあいすくりん2 07/02 08:00 dアニメ
むさしの! 07/02 23:30 dアニメ
リコリス・リコイル 07/02 23:30 ABEMA
シュート!Goal to the Future 07/08 22:00 dアニメ
× てっぺんっ!!!!!!!!!!!!!!! 07/02 22:00 ABEMA
Engage Kiss 07/02 25:00 dアニメ
うたわれるもの 二人の白皇 -      
× 連盟空軍航空魔法音楽隊ルミナスウィッチーズ 07/03 23:00 dアニメ
RWBY 氷雪帝国 07/03 23:00 dアニメ
× 森のくまさん、冬眠中。 07/03 25:00 dアニメ
ユーレイデコ 07/03 23:30 dアニメ
ようこそ実力至上主義の教室へ 2nd Season -      
カードファイト!! ヴァンガード will+Dress (Season3) -      
転生賢者の異世界ライフ ~第二の職業を得て、世界最強になりました~ 07/04 24:00 ABEMA
邪神ちゃんドロップキックX(第3期) -      
オーバーロードⅣ(第4期) -      
× 東京ミュウミュウ にゅ~♡ 07/09 00:00 dアニメ
金装のヴェルメイユ 07/10 00:00 dアニメ
メイドインアビス -烈日の黄金郷-        
新テニスの王子様 U-17 WORLD CUP        
異世界迷宮でハーレムを 07/13 00:30 dアニメ
異世界おじさん 07/06 23:30 dアニメ
× 継母の連れ子が元カノだった -      
× 咲う アルスノトリア すんっ! 07/11 00:00 dアニメ
ちみも 07/09 00:00 dアニメ
組長娘と世話係 07/07 22:30 dアニメ
それでも歩は寄せてくる 07/07 02:28 Amazon
よふかしのうた 07/08 12:00 dアニメ
惑星のさみだれ 07/08 12:00 dアニメ
× プリマドール 07/09 01:00 dアニメ
シャドーハウス 2nd Season -      
× ブッチギレ! 07/09? 22:30 Amazon
メガトン級ムサシ シーズン1特別篇 -      
Extreme Hearts 07/10 02:00 dアニメ
Extreme Hearts SxSxS 07/10 02:00 dアニメ
黒の召喚士 07/09 22:00 dアニメ
Dr.STONE 龍水(TVスペシャル)        
ハナビちゃんは遅れがち 07/10 22:00 dアニメ
異世界薬局 07/10 23:00 dアニメ
× KJファイル 07/11 12:00 dアニメ
オリエント-淡路島激闘編- 07/15 00:00 dアニメ
× シャインポスト        
5億年ボタン~菅原そうたのショートショート~ 07/15 00:30 dアニメ
はたらく魔王さま!!(第2期)        
ラブライブ!スーパースター!! 第2期        
ダンジョンに出会いを求めるのは間違っているだろうかIV        
× 最近雇ったメイドが怪しい 07/26 02:30 dアニメ
BanG Dream! Morfonication        
うたの☆プリンスさまっ♪ マジLOVEスターリッシュツアーズ~旅の始まり~        
風都探偵(仮面ライダーW続編)       U-NEXT
夜は猫といっしょ 08/03   YouTube
賭ケグルイ双(ツイン) 08/04     Netflix
リラックマと遊園地(第2期)       Netflix
D4DJ Doubel Mix        
僕のヒーローアカデミア 【アニメ新作オリジナルエピソード×2話】        
ROLY POLY PEOPLES        
トニカクカワイイ ~制服~(新作エピソード)        
× 世界の終わりに柴犬と(※漫画動画)        
アイドルランドプリパラ        

ちみもの三姉妹の目の描き方が三人とも違うのが好き。