Author Archives: AKIYAMA

2024-02-23 ,

org-link-completion.el

先日からorg-elisp-link.elに書いていた補完用のコードのうち、elispリンクに限らない一般的な枠組みの部分をorg-link-completion.elへ移動しました。

ブログリンクなどelispリンクと関係ないものを書いているのに org-elisp-link- で始まる関数やらマクロやらを使わないといけないのが何だか気持ち悪く感じてきたので。

基本的な考え方はorg-elisp-link.elにあった時とほとんど同じです。

より強くorg-modeのリンク部分をサポートするという意味で、リンクタイプ部分の補完機能も入れてあります。つまりorg-modeでの入力補完に書いた(pcomplete/org-mode/link関数を修正した)ような [[ の直後部分でリンクタイプを補完することもできるようになっています。ポイントの周辺を解析する関数もそれに合わせてリンクタイプ部分にいることを理解できるように修正しました。解析結果のリストの構造も少しだけ変わっています。

そのリストにアクセスするために以前はnthを連発していたのですが、それを改善するマクロも追加しました。

例えば以前次のようなコードを書きました。

(defun my-blog-link-capf-path ()
  "[[blog:2024-02-20-hello-emacs]]のようなリンクの補完候補を返します。
my-blog-dirにorgファイルがあるものとします。"
  (when-let ((pos (or org-elisp-link-capf-pos (org-elisp-link-capf-path-parse))))
    (let* ((type-beg (nth 0 pos))
           (type-end (nth 1 pos))
           (path-beg (nth 2 pos))
           (path-end (nth 3 pos))
           (type (buffer-substring-no-properties type-beg type-end)))
      (when (string= type "blog")
        (list
         path-beg path-end
         (cl-loop for file in (directory-files my-blog-dir)
                  when (string-match "\\`\\(.+\\)\\.org\\'" file)
                  collect (match-string 1 file))
         :company-kind (lambda (_) 'file))))))

これを次のようにアクセッサで書けるようにし……

(defun my-blog-link-capf-path ()
  "[[blog:2024-02-20-hello-emacs]]のようなリンクの補完候補を返します。
my-blog-dirにorgファイルがあるものとします。"
  (when-let ((pos (or org-link-completion-pos (org-link-completion-parse-at-point))))
    (let* ((path-beg (org-link-completion-pos-ref pos path-beg))
           (path-end (org-link-completion-pos-ref pos path-end))
           (type (org-link-completion-pos-ref pos type)))
      (when (string= type "blog")
        (list
         path-beg path-end
         (cl-loop for file in (directory-files my-blog-dir)
                  when (string-match "\\`\\(.+\\)\\.org\\'" file)
                  collect (match-string 1 file))
         :company-kind (lambda (_) 'file))))))

最終的に次のようになりました。

(defun my-blog-link-capf-path ()
  "[[blog:2024-02-20-hello-emacs]]のようなリンクの補完候補を返します。
my-blog-dirにorgファイルがあるものとします。"
  (org-link-completion-parse-let :path (type path-beg path-end)
    (when (string= type "blog")
      (list
       path-beg path-end
       (cl-loop for file in (directory-files my-blog-dir)
                when (string-match "\\`\\(.+\\)\\.org\\'" file)
                collect (match-string 1 file))
       :company-kind (lambda (_) 'file)))))

最初は分割束縛(この用語もどう日本語にするか悩ましいですが、今回はとりあえずこう書きます)で書こうと思ってpcasecl-destructuring-bindseq-letの三つを理解しやすさ、コード量、意味論、速度、letの入れ子の数など多角的に検討したのですが、実際に使って書いてみたときの全体的なコードが過剰に複雑に見える気がしてnth羅列の方がまだマシという気がしたので最終的に上のようなマクロに落ち着きました。

我々はコード中のマジックナンバーに不快な匂いを感じるよう訓練されているわけですが、それが分割束縛になったところで明示的なインデックス番号が非明示的な語順に変わっただけだということで汚いことには変わりがないんですよね。汚いものは汚いように見えるべきじゃないかと私はよく思います。上のようなマクロを作ってそれだけを使うようにしておけば汚い部分は大分局所化されますね。

で、三つの分割束縛の方法について。

pcaseは総じて優秀だなという印象。少ないコード量で複雑なマッチングと束縛をこなせます。マクロ展開の速度?がこれだけやけに速いみたいなのですが何だろう。チェックが過剰になりがちなのでバイトコンパイルすると最終的な速度はcl-destructuring-bindに追い越されますが。一番の問題点はコードが記号だらけで見づらくなるところですね。それと展開されたコードがかなりの数のletの入れ子になっていました。これ再帰で使ったらすぐにmax-lisp-eval-depthmax-specpdl-sizeの制限に引っかかるのでは? まぁバイトコンパイルすれば緩和されますが。値がnil、4要素のリスト、6要素のリストのいずれかという状況に対してパターンの指定は意味的にやや過剰という面があるでしょう。ちなみにpcase-letというのもありますが、パターンに一致しなかったときの動作は未定義と書かれています。まぁ、最初に全体をwhen-letで受けてから長さをチェックしてpcase-letを使えば良いのでしょうが、大人しくpcaseを使いました。

cl-destructuring-bindの引数リストってあのcl-defunと同じなんですね。ビックリしました。 (&whole pos &optional type-beg type-end path-beg path-end desc-beg (desc-end nil desc-p)) みたいに書いて遊んでました。やべぇなこれ(笑) 面白いから使いたかったのですが、結局どれも使わなかったのと、記述が長くなりがちなのもマイナスでしょうか。というかあまり遊んでると目的外使用感が出てきてしまいますね。あ、plistをこれで受けるなんて使い方もできるのか! 展開されたコードは一番美しかったです。順番にpopしていくだけですね。

seq-letはコードの見た目がシンプルでとても気持ちが良いです。でもシーケンスの長さが一致しないときの挙動が文書化されていません。docstringにもEmacs Lispのマニュアルにも書かれていません。中身はpcase-letを使っているみたいなのですが……。いや、最初の例で4要素のvectorを2つの変数で受けているから大丈夫ということなのかな? もし足りないのはnilになり過剰なのは単に捨てられることが保障されているなら積極的に使っていきたいです。リスト用ではなくシーケンス用なので速度は若干遅いです。まぁ、ほとんどの場合気にする差ではありません。基本的にseq--elt-safeの羅列が生成されるようです。

まぁ、細けぇことはいいんだよ、動けばいいんだ! ということで。

2024-02-20 ,

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

昨日の続き。

昨日の投稿の最後に blog: リンクのpath部分([[blog:<ここ>)を補完するコードを書きましたが、今度は説明(description)部分([[blog:2024-02-20-hello-emacs][<ここ>))を補完するコードを書いてみました。

(2024-02-23追記: 補完関数を定義する一般的な枠組み部分をorg-link-completion.elへ移動しました。それに伴い以下のコード等を書き直しました)

org-elisp-link.el org-link-completion.elの方に必要となる機能をすでに追加してあります。org-link-properties:completion-at-point プロパティはやめて :capf-path:capf-desc を使うようにしました。これによって、path部分と説明部分を補完する関数を別々に指定することが出来ます。

これを使うとブログリンクの説明部分を補完するコードを次のように書くことが出来ます。

(require 'org-link-completion)

(defun my-org-blog-link-capf-desc ()
  "ポイント上のblogリンクの説明部分を補完します。"
  (org-link-completion-parse-let :desc (type path desc-beg desc-end)
    (when-let ((blog (my-blog-from-link-type type)))
      (let* ((title (let* ((dir (plist-get blog :local-dir))
                           (file (expand-file-name (concat path ".org") dir)))
                      (my-org-blog-org-file-title file))))
        (list
         desc-beg desc-end
         (append
          (when title
            (list title
                  (concat title " | " (plist-get blog :title))))
          (list path)))))))

(defun my-org-blog-org-file-title (file)
  "org-modeで記述されているFILEからタイトルを取得します。"
  (when (file-regular-p file)
    (with-temp-buffer
      (insert-file-contents file nil nil 16384) ;; きっと先頭の方にあるでしょう。
      (goto-char (point-min))
      (let ((case-fold-search t))
        (when (re-search-forward
               "^#\\+TITLE: *\\(.*\\)$" nil t)
          (match-string-no-properties 1))))))

(org-link-set-parameters "blog"
                         :capf-desc 'my-org-blog-link-capf-desc)

実際に使うと次のようになります。

blogリンクのdescriptionを補完しているところ
図1: blogリンクのdescriptionを補完しているところ

ファイルの先頭部分にある「#+TITLE:」を見て自動的にタイトルを候補にしてくれます。

blog: リンクに留まらず、普通の file: リンクでも同じ事が出来そうです。.orgファイルへのリンクは同じ方法でタイトルが取得できますから、それを補完候補にすることができます。他にも何らかの方法でタイトルを取得できるファイルへのリンクはそれを補完候補にすることもできるでしょうね。といってもあまり思いつきませんが。pdfとか? http、httpsリンクタイプでhtmlからtitleを取ってくるのはやり過ぎでしょうか?

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)