refer-for で定義元のソースにジャンプ

refer-for で、
[File ]: なんちゃら.l
の行で enter 押すとそのファイルを開いて、定義の箇所を見れるようにした。

今のところ、対応しているのは下記だけ。

  • 関数名
  • 変数名
  • マクロ名
(defvar *refer-for-jump-dir* `(,(merge-pathnames "lisp" (si:system-root))
                               ,(merge-pathnames "site-lisp" (si:system-root)))
  "reference の `File' の欄から *.l を検索するとき、検索対象のディレクトリを指定する。")

(defvar *refer-for-jump-file-read-only* t
  "reference の `File' の欄から *.l を開くとき、read-only にするかどうかを指定する。
nil なら普通に開く。non-nil なら read-only で開く。")

(defvar refer-for-jump-file-alist
          '(("Accessor"     . 'ignore)
            ("BufferLocal"  . 'ignore)
            ("Keyword"      . 'ignore)
            ("Macro"        . "^[ \t]*\([ \t]*defmacro[ \t]+XXNAMEXX[ \t]+\(") ; 改行も
            ("Misc"         . 'ignore)
            ("Special Form" . 'ignore)
            ("Struct"       . "^[ \t]*\([ \t]*defstruct[ \t]+\([ \t]*XXNAMEXX[ \t]+")
            ("Tips"         . 'ignore)
            ("Variable"     . "^[ \t]*\([ \t]*defvar[ \t]+XXNAMEXX[ \t]+")
            ("Function (interactive)" . "[ \t]*\([ \t]*defun[ \t]+XXNAMEXX[ \t]+")
            ("Function"     . "[ \t]*\([ \t]*defun[ \t]+XXNAMEXX[ \t]+"))
  "`Type'欄と、それをソースファイルから検索するときの正規表現テンプレートの alist。'ignore は「今のところ無視する」という印。")

(defvar refer-for-jump-content-alist '((type    . "^\\[Type       \\]: \\(.+\\)$")
                                       (name    . "^■\\(.+\\)$"))
  "*Reference*バッファの欄のシンボルとその検索に使う正規表現の alist")


(defun refer-for-jump-get-content ()
  "ポイントが *Reference*バッファの`File'欄や`Type'欄の行にある場合、
欄のシンボルとその後ろの文字列を取得し、多値で返す。見つけられなかった場合は nil を返す。
例: 見つけた場合こんなのを返す 'seealso と \"buffer-read-only\""
  (let ((lim (save-excursion (progn (goto-eol) (point)))))
    (save-excursion
      (goto-bol)
      (if (scan-buffer "^\\[\\(.+\\) *\\]: *\\(.*\\)$" :regexp t :limit lim)
          (let ((desc (string-trim " \t" (match-string 2)))
                (header (intern (nstring-downcase (substitute-string (match-string 1) "[ \t]+" "")))))
            (values header desc))))))

(defun refer-for-jump-get-desc-at-point (sym)
  "現在参照中のリファレンス項目の 文字列を取得する。
例: 'type --> \"Function (interactive)\" を返す。"
  (let ((re (cdr (assoc sym refer-for-jump-content-alist))))
    (save-excursion
      (when (scan-buffer re :regexp t :reverse t)
        (string-trim " \t" (match-string 1))))))

(defun refer-for-jump-file (file)
  "reference の `File'欄から定義元のソースファイルを開く。"
  (flet ((get-file-path (name)
           (find-path-from-top-directory name *refer-for-jump-dir*)))
    (let* ((type (refer-for-jump-get-desc-at-point 'type))
           (name (refer-for-jump-get-desc-at-point 'name))
           (re-template (cdr (assoc type refer-for-jump-file-alist :test #'string=))))
      (cond ((null file))
            ((string= file "builtin.l") (error "ビルトイン関数なので開きません。"))
            ((null name) (error "reference の 項目名が見つかりません。"))
            ((null type) (error "reference の Type が見つかりません。"))
            ((null re-template) (error "Type: ~A には未対応です。" type))
            ((eq re-template 'ignore))
            (t
             (let* ((path (get-file-path file))
                    (buf-new (ed::find-file-internal path)))
               (set-buffer buf-new)
               (when *refer-for-jump-file-read-only*
                 (setq buffer-read-only t))
               (let ((re (substitute-string re-template "XXNAMEXX" name)))
                 (unless (scan-buffer re :regexp t)
                   (delete-buffer buf-new)
                   (error "定義元が見つかりませんでした。")))))))))

(defun refer-for-jump-seealso (name)
  "オリジナルの refer-for-search-seealso とだいたい同機能 (ただし、re は無効)"
  (let ((str (format nil "^~A$" (regexp-quote name))))
    (refer-for::output (refer-for::search str :by-title t))
    (refer-for::set-history str t)))

;;; command
(defun refer-for-jump ()
  "ポイントがある行によって、いろんなところへ飛ぶ。
`File'欄にあるときは、ソースファイルを検索する (refer-for-jump-file)
`See also'欄にあるときは、そのリファレンス項目に移動する (refer-for-jump-seealso)"
  (interactive)
  (multiple-value-bind (header desc) (refer-for-jump-get-content)
    (case header
      ('seealso (refer-for-jump-seealso desc))
      ('file (refer-for-jump-file desc)))))

(define-key refer-for::*refer-for-mode-map* #\RET 'refer-for-jump)

ほんのすこし doc-string を書く努力をしようと思った。