2021-12-30 ,

org-mode で現在の構文要素に応じて適切なメニューを出す

org-cmenuというのを作りました。

misohena/org-cmenu: Context Sensitive Menu for Emacs Org Mode

org-modeって機能は沢山あるしキー割り当てももの凄く沢山あって大変です。とても全部は覚えられないし覚えたとしてもすぐに忘れます。それに何か新しい機能を作ったとして、それをどのキーに割り当てるのかを決めるのも一苦労です。そして自分で作った機能だとしても結局たまにしか使わなければ忘れます。私はorg-modeを使っていると、人間というのは何かを覚え続けてはいられない存在なのだと言うことを痛感させられるんです。

それはともかく、最近また作業を効率化すべく新しい機能を追加したいと思ったのですが、それを実行するキー割り当てを考えていくうちに、やはりこれは文脈に応じて切り替わるメニューのようなものが必要だろうと思うに至ったのでした。

コンテキストメニュー。一般的には対象を右クリックするとそれに対する操作がポップアップで表示されるあれです。まぁ、Emacsなのでマウス用のポップアップメニューにしても仕方が無いので画面下にテキストで表示するのですが、いずれにせよ対象を選びそれに対する操作の中から選ぶというインタフェースは記憶すべきことを大幅に減らしてくれる素晴らしい仕組みです。覚えておくべきなのはそのメニューを開く操作のみです。対象以外で使う操作はメニューに出てこないのでメニューの中から操作を探す手間も最小限です。

Emacsでキー操作用のメニューを作るパッケージとしてはHydraが有名ですが、Hydraは複数行にわたる巨大な文字列でレイアウトを指定するので固定のメニューを作るのには直感的で良いのですが、変化の大きいメニューを作るには向かない気がします(気がするだけで工夫次第で何とかなるのかもしれませんが)。なので最近良く耳にするようになったtransient.elを試してみたのが前回前々回のお話しでした。

transientを使ったとしても色々問題はあったのですが、それはひとまず置いておいて、まずはorg-modeで現在のポイントがある地点の構文要素を割り出す方法についてご紹介したいと思います。

org-elementの使い方

org-modeには標準でorg-elementというのが入っていて、現在位置の構文要素を簡単に割り出せるようになっています。

バッファ(文書)全体を解析する org-element-parse-buffer という関数もありますが、部分的な解析には org-element-at-point と org-element-context という関数が使いやすいです。この二つの関数は共にセクション(見出しと見出しで区切られた範囲)以上の解析は行わず、現在のポイントの近くにある要素を返す関数です。 org-element-at-point は行内要素を含まない大きな構造のみを返し、org-element-context は内部で org-element-at-point を使用しつつ、行内の細かい要素も解析して一番限定された狭い要素を返します。

なので結論から言えば、現在のポイントにある構文要素に関する情報を得たければ (org-element-context) だけでおしまいです。

org-element-context の結果は、例えば次ような リスト です。

(bold
 (:begin 1739
  :end 1745
  :contents-begin 1740
  :contents-end 1743
  :post-blank 1
  :parent (paragraph
           (:begin 1728
            :end 1750
            :contents-begin 1728
            :contents-end 1749
            :post-blank 1
            :post-affiliated 1728
            :parent nil))))

これは実際に上の太字になっているところで M-: (org-element-context) を実行した結果です。boldで始まるノードの親(:parent)としてparagraphで始まるノードが続いているのが見て取れます。その上はsection、headlineと続くはずなのですが、org-element-context(というかorg-element-at-point)はそこまでは解析しません。

ノードから情報を取得するには org-element-type や org-element-property 関数(アクセッサ)を使用します。次のように。

(let ((datum (org-element-context)))
  (list
    (org-element-type datum) ;; => bold
    (org-element-property :begin datum) ;; => 1739
    (org-element-property :end datum))) ;; => 1745

要素の開始点と終了点は必ず取得できるので、これだけで全ての要素に適用出来るコマンドを記述できます。例えば要素全体のマーク(選択)やカット、コピーを実装するのは簡単です。

(defun my-org-kill-element (datum)
  (interactive (list (org-element-context)))
  (kill-region (org-element-property :begin datum)
               (org-element-property :end datum))) ;;本当は色々細かい問題があるのだけどまぁいいや……

org-element-type関数が返すboldやparagraphといったシンボルは type と呼ばれていて構文要素の種類を表します。

構文要素の種類

ところで皆さんはorg-modeにどのくらいの種類の構文要素があるかご存じですか?

org-elements.elの中には次のように書かれている部分があります。

(defconst org-element-all-elements
  '(babel-call center-block clock comment comment-block diary-sexp drawer
               dynamic-block example-block export-block fixed-width
               footnote-definition headline horizontal-rule inlinetask item
               keyword latex-environment node-property paragraph plain-list
               planning property-drawer quote-block section
               special-block src-block table table-row verse-block)
  "Complete list of element types.")

(defconst org-element-all-objects
  '(bold citation citation-reference code entity export-snippet
         footnote-reference inline-babel-call inline-src-block italic line-break
         latex-fragment link macro radio-target statistics-cookie strike-through
         subscript superscript table-cell target timestamp underline verbatim)
  "Complete list of object types.")

リストの中のシンボルがtypeの種類を表しています。org-element-all-elementsの中にあるのが30、org-element-all-objectsの中にあるのが24で計54種類にもなります(org-version 9.5.1 時点)。

org-element.elでは、bold等の行内に現れる要素のことを object と呼んでいて(HTMLで言えばインライン要素?)、それ以上の大きな要素のことを element と呼んでいるようです(HTMLで言えばブロックレベル要素?)。両者を合わせたもの(+α)はdatumと呼んでいる箇所が多いですが、普通にelementと呼んでいる場所もありややこしいです。

シンボルの名前だけだと何を意味しているのか分かりにくいので、全てのtypeを使用したorg-mode文書を作成して理解を深めました。

https://raw.githubusercontent.com/misohena/org-cmenu/main/examples/all-types.org

Emacsの中で見ないと構文がハイライトされないので見づらいですね……。参考までにtypeとそれに関連したマニュアルへのリンクも載せておきます。

all-elements
keyword headline section planning drawer property-drawer node-property clock center-block quote-block verse-block example-block comment-block dynamic-block export-block special-block src-block babel-call paragraph footnote-definition fixed-width comment plain-list item table table-row diary-sexp horizontal-rule inlinetask latex-environment
all-objects
bold underline italic code strike-through verbatim subscript superscript entity inline-babel-call inline-src-block line-break link target radio-target statistics-cookie footnote-reference table-cell timestamp macro export-snippet citation citation-reference latex-fragment

沢山あって大変ですが、やりたいことはこの全要素を網羅したorg-mode文書の各地点で適切なメニューを表示するということになります。

org-elementとtransientをつなぐ

transientには条件毎にメニューの項目を無効化する仕組みがあり、それを使ってメニューの内容を現在の文脈に合わせることもできます。つまり一つのメニュー(transient prefix)を定義して、その中に全部のコマンドを追加して所々述語で無効化していくわけです。

しかしそれよりは、まずtypeとそれに対して使えるコマンドの一覧表があって、そこからtypeごとのメニューを生成してしまった方がずっと楽な気がします。雑然とコマンドが並んでも分かりづらいので、カテゴリー分けするための情報はある程度持たせる必要はありますが。

なので次のような構造を考えました。

type -> group -> ( command[key desc func] | string | group(入れ子) )

まず事前にtypeとgroupに対してcommandを登録していきます。commandが使用できるtypeに対してのみ登録していきます。例えばリンクを開くコマンドはlinkというidを持つtype内の適当なグループに追加します。上のmy-org-kill-elementのように沢山の種類の要素に適用出来るコマンドもあるので、追加先はリストやallのような別名でも指定できるようにしておきます。

そしてメニューを開くときに、org-elementで現在のtypeを割り出し、そのtypeに応じたメニューをgroupから組み立ててtransient-define-prefixで定義して実行します。

org-cmenu

そんな感じのやり方で作ったのがorg-cmenuです。

misohena/org-cmenu: Context Sensitive Menu for Emacs Org Mode

org-cmenu-setup.elというファイルが前述の「typeとgroupに対してcommandを登録していく」処理になり、ここがメニューの内容を決めています。既存のorg-modeのコマンドを分類して追加しているほか、新たに欲しい機能はorg-cmenu-tools.elの方に書いてから追加しています。

メニューの構造を管理したり実際にメニューを表示する部分はorg-cmenu.elに入っています。コアの部分ですがこれだけでは何も表示されません。

これらを使うための設定は次のようになります。

;; メニューを開くorg-cmenuコマンド実行時にorg-cmenu.elを読み込む
(autoload 'org-cmenu "org-cmenu")
;; org-mode内にorg-cmenuコマンドを起動するキーを割り当てる
(add-hook 'org-mode-hook
          (lambda ()
            ;; お好きなキーを設定してください
            (define-key org-mode-map (kbd "C-^") #'org-cmenu)))

;; org-cmenu.elが読み込まれたらorg-cmenuコマンドが起動する前にメニュー内容を登録する
(with-eval-after-load "org-cmenu"
  (require 'org-cmenu-setup) ;;自分専用のsetupファイルに差し替えることもできる
  ;; ここで自分専用のコマンドを追加することもできる
)

リストに対するメニューを開く例

実際にall-types.orgのplain-listがあるところでメニューを起動すると次のようになります。

リストの段落の中でメニューを起動した様子
図1: リストの段落の中でメニューを起動した様子

現在の構文要素はparagraphです。段落に対してできることはまだそれほど多くはありません。何かobjectを挿入したり、全体をカット・コピーしたりするくらいでしょうか。

^キーを押すと一つ上の親要素が選択されます。2回押してplain-listを選択したところが次です。

親要素を選択したところ
図2: 親要素を選択したところ

リスト全体に対しては段落よりももう少し色々な操作ができるようになっています。 リスト全体をチェックボックス化したりLispのリストにしてコピーしたり。

リンクに対するメニュー

次は画像リンクに対して #+CAPTION#+ATTR_HTML を追加する例。

画像リンクに対して属性を追加する例
図3: 画像リンクに対して属性を追加する例

実は今回のメニューはこれがやりたいが為に作ったものでした。何か専用のキーを割り当てるのもバカバカしいちょっとしたことでした。でもCAPTIONやHTML属性(主にwidthやclass)の設定はこの記事を書くためだけでも既に何度も使用しています。

ちなみに #+CAPTION:#+ATTR_HTML:#+NAME: といった部分はorg-elementではaffiliated keywordsと呼ばれていて、原則的には全てのelement(非object)に付加できるようになっています(現実的には例外あり。table-cell等には付けようがない)。org-elementが返すノードの:beginプロパティはこのaffiliated keywordを含む要素全体の先頭になります。affiliated keywordを除いた先頭は:post-affiliatedプロパティで取得できます。

その他、リンクに対してはパス・説明部分の編集、リンク先を開く、パス・ファイル名のコピー、ファイル情報表示等を行えるようにしました。

画像リンクに対する操作一覧
図4: 画像リンクに対する操作一覧

これを作成しているときに初めて知ったのですが、 C-c C-o (org-open-at-point)によるリンクのオープンは、C-uを一回付けるとEmacs優先で、C-uを二回付けると外部アプリ優先(Windowsだとw32-shell-execute)で開くようになっていたんですね。知りませんでした……。

私は画像リンクに対してはプライベートな設定で撮影位置の地図表示やコピー、撮影日時のコピーなんかも加えています。この間やっていたことの続きですね。

表に対するメニュー

次はtable、table-row、table-cellに対する操作の例。

表に対して色々な操作をする例
図5: 表に対して色々な操作をする例

通常のカーソル移動の操作(C-f, C-b, C-p, C-n, C-a, C-e, M-<, M->)だけでセル単位で移動できるようにしてみました。

そのままC-SPCでリージョンを作成すれば複数のセルを矩形で選択、カット・コピー、ペーストできます。

TABやS-TABによるカラム幅の伸縮も非常に直感的になりました。

tableやtable-rowに対する操作をtable-cell選択時にも表示するかは迷ったのですが、あると便利なものは表示しておくことにしました。現在はファイルエクスポートやS式コピーといった頻繁には使わない機能は^でtableを選択しないと出てこないようにしてあります。

Insertメニュー

sectionやparagraph等で表示されるInsertメニュー。まだ発展途上ですが、objectは一通り挿入できるようになっています。elementはまだまだです。

Insertメニュー
図6: Insertメニュー

特に注目したいのがentityの挿入。entityについては以前org-modeで文字をエスケープする方法でも触れましたが、狙った文字を探すのが案外面倒なんですよね。なので、正引き・逆引きの両方に対応した補完入力を付けることで簡単に狙った文字をentityとして追加できるようにしました。C-^ i e & RETで \amp{} と入力されます。

Export Snippetなんかも(普段使わないので)地味に書き方を忘れたりするので便利です。もう書き方を検索する必要はありません!

上付き文字、下付き文字、entityに対するメニュー

superscript、subscript、entityに対しては org-toggle-pretty-entities が候補に出るようになっています。

prettyを有効にしたところ
図7: prettyを有効にしたところ

使っているところでC-^ pを押せば切り替えられるので多少は便利かもしれません。こういう地味な機能を盛り込むのも躊躇無くできるのがorg-cmenuの良い所です。

マニュアルを開く

ところで全てのメニューに ? キーとして Manual というのが書いてあるのにお気づきでしょうか。?キーを押すとその構文要素について書かれているorg-modeマニュアルの該当部分がブラウザで開きます。

これで覚えておかなければならない事がさらに減りますね。

最後に

まだまだ手つかずの構文要素が残っていますが全て埋める必要は無いでしょう。

考えていくといくらでもアイデアが出てきてキリがありません。必要になったときにちょくちょく追加していくことにします。

スニペットの挿入のような個人的なものも後から簡単に追加できるのが良いですね。

Pingback / Trackback