Monthly Archives: 2月 2024

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でミニバッファにパスや説明部を入力してリンクを作成したことか。まぁ、今後は両方使えるということで(笑)

参考資料:

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を取ってくるのはやり過ぎでしょうか?