2024-03-23 ,

org-elisp-linkでcl-defmethodへのリンクを書けるようにする

以前書いたorg-elisp-linkで、cl-defmethodで定義したメソッドへのリンクを書けるようにしました。

misohena/org-elisp-link: Org-mode Link Types for Emacs Lisp Elements

例えばlistを引数に取るseq-takeへのリンクは [[elisp-function:seq-take;method-args=(nil list t)]] のように書きます。

method-args= オプションの書式は ( qualifiers . specializers ) になります。 qualifiers はあまり指定されないのでnilの場合が多いかと思います。 specializers はcl-defmethodで指定する引数列の引数名を除いたものと考えれば良いでしょう。

qualifiers を使用する例としては、例えばelp.elの中にあるloadhist-unload-elementへのリンクなんてどうでしょう(lispディレクトリでgrepして探しました)。

(cl-defmethod loadhist-unload-element :extra "elp" :before ((x (head defun)))
  "Un-instrument before unloading a function."
  (elp-restore-function (cdr x)))

elp.elの中に上のような定義があるのですが、そこへのリンクは次のように書くことになります。

[[elisp-function:loadhist-unload-element;method-args=((:extra "elp" :before) (head defun));library=elp]]

実装はelisp-mode.elのxrefバックエンド、特にelisp--xref-find-definitionsあたりを利用しています。

xrefを使ってelispの関数やら変数やらの定義へジャンプするには、次のようにすればできます。

(let ((xref-backend-functions '(elisp--xref-backend))) ;; 強制的にelispバックエンドを使うようにする
  (xref-find-definitions "find-file"))

cl-defmethodによって複数の選択肢がある場合はxrefのメニューが出ます。

(let ((xref-backend-functions '(elisp--xref-backend)))
  (xref-find-definitions "seq-take"))

今回リンクの上でC-c C-o(org-open-at-point)したときはこの方法で定義位置へジャンプするようにしてみました。従来はこのような場合に必ずcl-defgenericの方へジャンプしてしまったり、それが無ければジャンプできなかったりしていました。

エクスポートの時はジャンプせずに定義の場所を取得する必要があります。

ジャンプせずに定義の候補を取得するには次のようにすればできます。

(elisp--xref-find-definitions 'seq-take)
(#s(xref-item
    #("(cl-defgeneric seq-take)" 1 14 (face font-lock-keyword-face) 15 23 (face font-lock-function-name-face))
    #s(xref-elisp-location
       seq-take
       cl-defgeneric "c:/...path-to-emacs.../share/emacs/29.2/lisp/emacs-lisp/seq.el"))
 #s(xref-item
    #("(cl-defmethod seq-take ((list list) n))" 1 13 (face font-lock-keyword-face) 14 22 (face font-lock-function-name-face))
    #s(xref-elisp-location
       (seq-take nil list t)
       cl-defmethod "c:/...path-to-emacs.../share/emacs/29.2/lisp/emacs-lisp/seq.el")))

結果はxref-itemというレコードになります。xref-itemはsummaryとlocationという二つの要素から成ります。

summaryの方は単なる文字列(テキストプロパティ付き)です。

locationの方は場所を特定するための情報で、elisp--xref-find-definitionsが返す場合はxref-elisp-locationというレコードになります。xref-elisp-locationはsymbol、type、fileから成ります。

試しにseq-takeの2番目の候補の場所を取得してみましょう。

(let ((loc (xref-item-location (nth 1 (elisp--xref-find-definitions 'seq-take)))))
  (list
   (xref-elisp-location-type loc)
   (xref-elisp-location-symbol loc)
   (xref-elisp-location-file loc)))
(cl-defmethod ; type
 (seq-take nil list t) ; symbol
 "c:/...path-to-emacs.../share/emacs/29.2/lisp/emacs-lisp/seq.el" ; file
 )

定義位置へジャンプするとき、これらの情報はそのままfind-function-search-for-symbolに引き渡されます。

typeの cl-defmethodfind-function-regexp-alistのキーです。このalistからcl--generic-search-method関数が求められ、symbolがそのcl--generic-search-method関数に引き渡されて実際の検索が行われます。symbolは (seq-take nil list t) なので、メソッド名が seq-take 、qualifier無し、引数の型がlistであるようなcl-defmethodが(re-search-forwardで)検索されます。

今回追加したmethod-args=オプションの形式は、このsymbolのメソッド名を除いた残りの部分と一致するようになっています。