Author Archives: AKIYAMA

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のメソッド名を除いた残りの部分と一致するようになっています。

2024-03-03

shell-modeでgitを使うたびにブラウザが開きまくるのを直す

先日Corfuの自動補完の設定を変えてからだと思うのですが、M-x shellでgitのコマンドを打つたびにブラウザでヘルプが開いて困っていたので重い腰を上げて直しました。

原因は pcomplete/git 関数内で git help コマンドを実行しているところにあります。

;; pcmpl-git.elより
(defun pcomplete/git ()
  "Completion for the `git' command."
  (let ((subcommands (pcomplete-from-help `(,vc-git-program "help" "-a")
                                          :margin "^\\( +\\)[a-z]"
                                          :argument "[[:alnum:]-]+")))
    (while (not (member (pcomplete-arg 1) subcommands))
      (if (string-prefix-p "-" (pcomplete-arg))
          (pcomplete-here (pcomplete-from-help `(,vc-git-program "help")
                                               :margin "\\(\\[\\)-"
                                               :separator " | "
                                               :description "\\`"))
        (pcomplete-here (completion-table-merge
                         subcommands
                         (when (string-prefix-p "-" (pcomplete-arg 1))
                           (pcomplete-entries))))))
    (let ((subcmd (pcomplete-arg 1)))
      (while (pcase subcmd
               ((guard (string-prefix-p "-" (pcomplete-arg)))
                (pcomplete-here
                 (pcmpl-git--expand-flags
                  (pcomplete-from-help `(,vc-git-program "help" ,subcmd) ;; ★ここ★
                                       :argument
                                       "-+\\(?:\\[no-\\]\\)?[a-z-]+=?"))))

これはgitコマンドの引数を補完するためのコードです。 git help コマンドは指定可能なコマンドやオプションを列挙するために呼び出しているようです。

git help コマンドの呼び出しは三つ存在するのですが、 git help -agit help はコンソールから試してみても標準出力にテキストが表示されるので問題ありません。しかし git help status などとサブコマンド名を入れてみるとブラウザが開きます。

現在私が使っているGitはMSYS2やCygwinのものではなく、Git for Windowsなのでそれが原因なのかもしれません。Git Bashから git help --man status などと打っても No manual entry for git-status などと出るだけです。manやinfoが入っていないのでブラウザでhtmlを開くのでしょう。

試しに `(,vc-git-program "help" ,subcmd) の部分を `(,vc-git-program ,subcmd "-h") に直したら問題は解消しました。

といっても直接ファイル(pcmpl-git.el)を書き替えるのも何ですし、どうしましょうね。pcomplete/git関数は少々内容が込み入っていて、全体をコピーして一部を書き替えて置き換えるのも気が引けます。

問題のリスト `(,vc-git-program "help" ,subcmd)pcomplete-from-help関数に引き渡されています。なので、pcomplete-from-help関数の第一引数(command)に `(,vc-git-program "help" ,subcmd) が渡されたときに `(,vc-git-program ,subcmd "-h") へ差し替えてしまいましょう。pcomplete-from-help関数はあちこちで利用されているでしょうから、安全のためにpcomplete/gitの中から呼び出される時だけ、この変換処理をすることにしてみます。

;; shell-modeでgitコマンドの引数を補完させるとブラウザが開くのを抑制する。
;; 次の三つのパターンがあるが、git help <subcmd>のときだけブラウザが開くので
;; それをgit <subcmd> -hに置き換える。他はそのまま。
;;  git help -a        => そのまま
;;  git help           => そのまま
;;  git help <subcmd>  => git <subcmd> -h
(with-eval-after-load "pcmpl-git"
  (defun my-pcomplete/git:around (oldfun)
    (cl-letf* ((old-pcomplete-from-help
                (symbol-function 'pcomplete-from-help))
               ((symbol-function 'pcomplete-from-help)
                (lambda (command &rest args)
                  (apply old-pcomplete-from-help
                         ;; Replace git help <subcmd>
                         (if (and (listp command)
                                  (equal (car command) vc-git-program)
                                  (equal (cadr command) "help")
                                  (cddr command)
                                  (not (string-prefix-p "-" (caddr command))))
                             ;; => git <subcmd> -h
                             `(,(car command) ,(caddr command) "-h")
                           command)
                         args))))
      (funcall oldfun)))
  (advice-add 'pcomplete/git :around 'my-pcomplete/git:around))

cl-letfでsymbol-functionを書き替えるコードはやっぱり少し鬱陶しいですね。

pcomplete-from-helpgit help <subcmd> というコマンドを渡すところは他に無いでしょうし、もしあったとしても意図的にブラウザを開くために使うことは無いでしょうから、pcomplete/gitの中だけに限定する必要は無かったかもしれません。それなら:

(with-eval-after-load "pcmpl-git"
  (defun my-pcomplete-from-help:filter-args (args)
    ;; Replace git help <subcmd>
    (let ((command (car args)))
      (if (and (listp command)
               (equal (car command) vc-git-program)
               (equal (cadr command) "help")
               (cddr command)
               (not (string-prefix-p "-" (caddr command))))
          ;; => git <subcmd> -h
          `((,(car command) ,(caddr command) "-h") ,@(cdr args))
        args)))
  (advice-add 'pcomplete-from-help :filter-args 'my-pcomplete-from-help:filter-args))

程度でも良いかもしれません。

2024-02-25 ,

org-link-completion.elによるリンクの補完とorg-modeのリンク構文

先日から作っているorg-link-completion.elですが、一応org-modeのリンク表記の内部のうち、ほとんど全ての場所で補完ができるようになりました。

misohena/org-link-completion: Complete the link type, path and description part of links at point in org-mode buffer.

次の場所で補完できるはずです。

  • [[ link-type:
  • [[ searchtarget
  • [[# custom-id
  • [[# custom-id ][ description
  • [[* heading
  • [[* heading ][ description
  • [[( coderef)
  • [[(coderef)][ description
  • [[ Search Target
  • [[ Search Target ][ description
  • [[ /dir/file
  • [[ ./dir/file
  • [[ /dir/file
  • [[ c:/dir/file
  • [[ /dir/file ][ description (上記 / ./ / c:/ を含む)
  • [[file: file
  • [[file+sys: file
  • [[file+emacs: file
  • [[file: file ][ description (上記 file+sys: file+emacs: を含む)
  • [[ unknown-type: path
  • [[ unknown-type: path ][ description

補完できないところは[と[、]と[、]と]の間くらいじゃないでしょうか。

そもそもorg-modeでどんなリンクが書けるのかというのがあやふやだったんですよね。仕方ないのでちゃんとおさらいしました。

org-modeのリンクで一番重要なコンセプトは、URLや(ディレクトリを含む)ファイルパスのように見えるもの以外は内部リンクだということでしょう。つまり、 file: とか https: とか付いているものや ./screenshot.png のようなもの以外は現在のorg-modeバッファ内へのリンクです。

ただ、私は解析の都合上 file: とか https: のようなリンクタイプが付いているかそうでないかによって大きく二通りに分けました。その上で、付いていないもののうち、ファイルパスは外部リンク、そうでないものは内部リンクということになります。

それを踏まえて全ての形式を列挙すると次のようなります。

  • リンクタイプ無し
    • 内部リンク
      • [[ # カスタムID ]
      • [[ * 見出し ]
      • [[ ( コード行参照 ) ]
      • [[ 色々検索 ]
    • 外部リンク
      • ディレクトリ始まりファイルパス

        • 相対パス
          • [[ ./ ファイルパス
          • [[ ../ ファイルパス (追記:漏れていたので追加)
        • 絶対パス

          • [[ / ファイルパス
          • [[ ~/ ファイルパス
          • [[ ~ ユーザ名 ファイルパス (追記:漏れていたので追加)
          • [[ \ ファイルパス (MS-Win) (追記:漏れていたので追加)
          • [[ ドライブレター : /または\ ファイルパス (MS-Win) (追記:コロンの後には/か\が必要)

          (追記:絶対パスはfile-name-absolute-pがtを返す形式でなければならない。つまり ~ユーザ名 なんてのも許容される(!)し c:\ とバックスラッシュが続くのも許容される。c:の後にパス区切りが無いドライブ相対指定は許容されない。当然プラットフォームによって異なる。面白いのが相対パスにバックスラッシュを使った .\file は許容されないが絶対パス \file は許容される点)

        (注: file: と同じように 後ろに :: を付けられるが省略)

  • リンクタイプ付き
    • 省略記法(org-link-abbrev-alist, org-link-abbrev-alist-local)
    • リンクタイプ(org-link-parameters)
      • file: (file+sys:file+emacs: も形式は同じ。 ファイルパス は空でも良い)
        • [[file: ファイルパス ]
        • [[file: ファイルパス :: 行番号 ]
        • [[file: ファイルパス :: 色々検索 ]
        • [[file: ファイルパス :: * 見出し ]
        • [[file: ファイルパス :: # カスタムID ]
        • [[file: ファイルパス :: ( コード行参照 ) ]
        • [[file: ファイルパス :: / 正規表現 / ]
      • その他沢山(標準的なもの: attachment:, bbdb:, docview:, doi:, elisp:, gnus:, rmail:, mhe:, help:, http:, https:, id:, info:, irc:, mailto:, news:, shell:)

……漏れがあったらすみません。

いろんなリンクの例としてexamples/links.orgというのも作っておきました。

# の後で補完すれば全カスタムIDが候補として出ますし、 ( の後なら全(ref:)表記、 * の後なら全見出しが出ます(見出しだけはorg-modeの標準でも補完してくれます)。

色々検索 の部分ですが、概ね次のように検索されるようです。

  1. dedicated target (<< と >> で囲んだ文字列がリンクターゲットになります。文字列はエクスポートされません)
  2. 要素(ブロック)の名前 (#+NAME:で指定する)
  3. 見出し
  4. 全文検索 (org-link-search-must-match-exact-headlineがnilの時またはorg以外のファイルの時のみ)

概ね <<My Target>>#+NAME: table1 のようなものへのリンクと考えた方が良さそうなので、それだけを補完候補として出しています。

ファイルの :: 以降はまだ未実装です。現時点では補完されません。

file以外のリンクタイプについては手を付けていません。 org-link-parametersから :capf-path:capf-desc という非標準のプロパティを取得してそれを呼び出すようにしてあるので自分でいくらでも追加できます。org-elisp-link.elもその方法で補完関数を追加しているので、関数名や変数名をorg-modeバッファ内で補完できます。

いずれの種類においても有効なのが他の同種のリンクから候補を集めるという方法です。リンクの説明部分を補完するときに、既に書かれている同じタイプとパスを持つリンクから説明部分を拝借してくることが出来ます。パスについても同じタイプを持つリンクから候補を集めます。

色々やると大きなファイルで処理が遅くなる場合もあるかもしれません。全ての補完関数は個別に取り除く(無効化する)ことが出来るようになっています。

というわけで大枠は出来たかと思います。細かいところがまだ残っていますが、まぁ、そのうちやったりやらなかったりするでしょう。

ここまでやっておいてこの投稿を書いている間に何回C-c C-lでミニバッファにパスや説明部を入力してリンクを作成したことか。まぁ、今後は両方使えるということで(笑)

参考資料: