5
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Emacs Window Management Almanacを要約しようとした何か

Last updated at Posted at 2024-06-26

前書き

Emacsへの不満としてよく目にするのがウィンドウ管理の訳わからなさです。

勝手に分割したり、閉じたり閉じなかったり、使っているウィンドウに別のバッファーを表示したり、と訳わかりません。

Emacs Lispリファレンスマニュアルとかを読めば、それを制御するウィンドウパラメーターなるものとか記述されてますが、読んでも「えーそこからですか?」という感じで面倒くさくてあきらめてしまいます。

そんな折、redditで高評価だったウィンドウ管理に関する長文のブログ記事を読んでとてもためになったので要約してみました。本当は進歩著しいAIで手っ取り早く和訳orgを作ったんですが(自分用に色々なとこからこつこつためてます; 最近はAIの使い方でサイトごと一気に取り込んで、とか色々あるようですがわたしはセンテンス単位でブラウザ上でカットアンドペースト; わたしはこーゆーやり方じゃないと頭に入らないんですよね)、最近のAIさんは本当に優秀なので元記事読みたい人は自分で簡単に読めるよなあ、と思い紹介用に要約を試みました(が結局後半は抜粋ばかりになってしまいました)。

以下、Emacsにおけるウィンドウ管理にたいして長年アプローチしてきたブログ著者Karthik Chikmagalurさんによる入門記事からリソース詳解やヒント部分をより好んでまとめた要約とか抜粋とかです。

章立ては元記事をそのまま拝借してますが、課題みたいな部分は省略してます。自分の関心が薄い部分も端折ってます。

以下は要約や抜粋みたいなものです

Emacsのウィンドウ管理が不評な理由

  • 非常に柔軟で詳細なレイアウトシステムと比較的粗雑なコントロールを組み合わせているから。
  • 反面、よいメタファや機能を提供するツールを作成する余地も生まれる。

ウィンドウ管理は大きなトピックなので範囲を絞る。

  • チュートリアル修了、Emacsの基本的な用語やウィンドウ操作に慣れてる前提で記述。
  • タブについての言及は少しだけ(主にワークスペース管理のツールとして使われてるから)。
  • フレーム内でのウィンドウやバッファの管理に絞る。
  • 個人的な経験が基なので試したツールやアイデアのみ対象。

ウィンドウ管理とは

Emacs: ウィンドウ(フレーム内のビューポートやペイン、覗き窓のようなもの)と連続したテキストの塊(ファイル内容とはかぎらず)であるバッファが分離されてる

通常のIDEやエディタ: これら2つは統合されているので使用時の認知負荷は小さい(反面、柔軟な動作や自由な配置は妨げられる)。

Emacsなら同じファイルの2つのビューを簡単にもてる(他のエディタはEmacsの間接バッファのように具体化された概念には全く馴染みがない)。

Emacsなら多くのことが可能になるというメリットもあるが認知的負担は増える。

認知負担の軽減を大きく妨げる不足しがちな3大知識:

  • ウィンドウの正しい場所への配置。
  • バッファの正しいウィンドウへの配置。
  • 分離されてれば何が可能になるのか。

以上の3つを理解していないと厳しい。この文書で3つのうち2つでも軽減できればなと思った。

除外対象

ウィンドウ関連操作は原始的、かつあらゆるレベルの操作において一般的で不可避、すなわち意外なほど微妙で広範囲かつ深いトピックなので、曖昧さの解消と焦点の絞り込みから。

以下の内容については触れない。

  • バッファー表示のルール

    Emacsが間違った場所にウィンドウを表示してウィンドウ配置を壊す!

    EmacsのAPI細部に関する知識を要し、(ユーザー向けマニュアルではなく)Emacs Lispでユーザー用モードなどを作成する開発者用のリファレンスマニュアルにさえ「あきらめろ」と書いてある。この問題についてはMickey Peterson氏の記事やマニュアル(原文/和訳)を読むことを勧める。

Mickey PetersonさんはMastering Emacsの著者さんです。この記事と比較すると自分用にEmacs Lispコードを結構記述するプログラマー向けに、リファレンスマニュアルに記載された情報を説明しつつ実際のコーディングノウハウについて記述されています。なぜこちらを要約しなかったかというと、冒頭の「えーそこからですか?」と同じ理由です。

  • ウィンドウ構成の永続化、ワークスペースやバッファーの分離

    特定のタスク用にウィンドウをグループ、セッションをまたいで保持してほしい!

    1. Emacsセッションは長く続く傾向にある。
    2. 長いとますます多くのタスクにEmacsが使用される。

    そしてバッファが増えすぎてグループ化、分離保存を求めるようになる。確かにウィンドウ管理に関連するものの、保存したい状態の一部にウィンドウの配置が含まれるというだけの話し。これは厄介で複雑な問題で記事の範囲を超える。(tab-bartabspaceseyebrowsetab-bookmarkdesktop.elpersp-mode.elperspectiveproject-tab-groupsbeframeactivities.elなどのプロジェクトが助けになるかもしれない。

  • Emacsにおけるウィンドウの挙動にたいするパラダイム的な変更

    なぜEmacsのウィンドウ配置はこんなに気まぐれなのか?タイル型ウィンドウマネージャーならずっと前に解決されているのに!

  • ウィンドウ配置と管理に包括的かつ抜本的な解決策を提供する一部パッケージはEmacsにおける実質的なウィンドウマネージャーといえる。
  • このような完全なソリューションは最初は素晴らしい。
  • そして最終的には摩擦を引き起こす。
  • Emacsをカスタマイズすればするほど、これらのソリューションによるEmacs API上に構築された抽象化が長期的に足枷となる。

これらを除外した結果、手作業での日常的なウィンドウ管理、すなわちウィンドウのフォーカス切り替え、バッファ移動、ウィンドウ分割やクローズなどが対象。

本題に入る前に一般的な懸念や否定的な意見についても述べておく

  • 2つのウィンドウという視点

    この議論は無意味。せいぜい2つウィンドウがあればよい。

    ちょっと違う。必要なのは一度に最大で2つのウィンドウ。Emacsでウィンドウを整理するのが面倒なのだ。書き込みやコーディングの最中以外は資料や検索、コンパイル結果、ファイルアクセス、シェルやREPL、目次など複数のバッファに簡単にアクセスする必要があり、常に表示しておくか、必要に応じて簡単に表示するか、これは画面サイズと好みの問題だが、いずれもウィンドウやバッファとの手動のやり取りを含み、どちらもウィンドウ管理の範疇なので取り上げる。

  • マウス処方

    マウスを使えば? ほとんどのソフトウェアでは問題にならないけど。

    動作コストや流行、個人の好みについての論争に踏みいらずにウィンドウを操作する最も自然な方法だが、キーボードと比較すると学習曲線と表現力が反比例する。

    にも関わらず、Emacsでは他のほとんどのアプリケーションよりもURLマウスからより多くの表現力を引き出すことができるだろう。

著者さん曰く、

残念なことにEmacsにはマウスの使用について非常に主観的な意見をもつユーザーが多い。私もEmacsでマウスは使わないが、他の何かでマウスをすでに使っていれば話しは別。それならマウスを使ってEmacsを操作することが実際には最も簡単な方法となる。キーボードから手が離れていれば、Emacsでも簡単にマウスを操作できるから。

ウォーミングアップ

人気があり一般的に推奨されているウィンドウ管理オプションmに関する簡単な紹介。フォーカス変更、ウィンドウの移動、アンドゥ操作、他にもバッファ管理やオーダーメイド版のウィンドウ操作が含まれる。

other(他)のウィンドウとnext(次)のウィンドウ(built-in)

other-window(C-x o)はウィンドウ切り替えの基本的なコマンド。ウィンドウが少なければ十分に機能する。

  • ウィンドウ選択はフレームを(おおよそ)時計回りに巡回する。
  • コマンドとキーバインディングが1つずつだけという単純さが売り。
  • ウィンドウが増えると呼び出し回数が増えるのが欠点。
  • 一度に多くのウィンドウを表示しないのなら効果的なコマンド。
  • other-windowの基本的なヒントと調整

    • おそらく最も使われているEmacsコマンドの1つなのでもっと便利な、たとえばM-oに割り当て可。
    • 数引数を使用してウィンドウをスキップしたり逆に巡回できる。M-3 M-oは3つ先のウィンドウ、M-- M-2 M-oは2つ前のウィンドウを選択する。
    • リピートモード(M-x repeat-mode)でoと(逆方向)Oを使ってウィンドウを繰り返し切り替えできる。C-x o o o o...やM-o o o o...は、C-x o C-x o C-x o...より速い。
  • other-windowのハック

    • ウィンドウパラメータno-other-windowを設定すればウィンドウをother-windowの対象外にできる。
    • 通常は特定のバッファクラス用にdisplay-buffer-alistで事前指定する(つまり手動ではセットしない)。
    • 洗練されたファイルマネージャ(dired-sidebarやdirvish-side)のウィンドウがother-windowで選択されないのはこれが理由。
  • 常に2つのウィンドウしか表示しない
  • oを追加で数回押すことが気にならない
    ならばここまで読めばOK。

ここから先は、(おそらく現実的には)解決が不要と思われる問題にたいするさまざまなレベルの最適化に過ぎない(らしい

  • "次(next)"のウィンドウとは
    • 通常はカレントウィンドウから時計回りに選択されるウィンドウのこと。
    • Elispではnext-window関数呼び出しでリターンされるウィンドウのこと。
    • 日常使用していればフレーム内の時計回りで次のウィンドウにたいする直感が自然に身につく(というか身につけよう!)。
    • 次のウィンドウという概念は、他のウィンドウで操作するコマンドにとっても有用だから。
    • 次のウィンドウはscroll-other-windowのような別ウィンドウで動作するコマンドのデフォルトのウィンドウ(なので身につけよう!)。

windmove (built-in)

  • ウィンドウ選択、ウィンドウ内のバッファ交換、ウィンドウ削除が可能。
  • 方向に応じたウィンドウ間でのフォーカス移動やバッファ移動を行うための組み込みのライブラリ。
  • Vimユーザーが期待する通りの動作を行う(evil-modeユーザーならすでにWindmoveを使用しているが気づいてないかもね)。
  • other-windowがEmacsのalt-tabとすれば、Windmoveはタイリング式ウィンドウマネージャのようなもの。
  • ウィンドウの空間的な配置を選択に関連付けるための最も自然な方法(マウスを使用する方法を除く)。
  • 使用は簡単:windmove-left(-right、-up、-downに対応)を修飾キーまたはプレフィクスキーに割り当てて、方向キー(WASD、HJKL、矢印キーなど)と組み合わせる。
  • 2つウィンドウがある場合にはカーソルの正確に右にあるウィンドウとなる。
  • ウィンドウのバッファを方向別に交換もできる。
  • フレーム内のウィンドウの再配置に役に立つ(evil-modeでも使用)
  • フォーカスはバッファとともに移動する。

他にもwindmove-delete-*などのコマンドは隣接する任意方向のウィンドウを削除できるが、もっと良い方法がある(後述)。

frames-only-mode

Emacsのウィンドウ処理をOSに任せてしまうという考え方。

  • タイリング式ウィンドウマネージャ内でEmacsを特別扱いせずに扱うという解決策。
  • すべてのバッファをウィンドウではなく新たなフレームで開いて、ウィンドウマネージャに管理を任せる。
  • EmacsバッファはOSのウィンドウと同等になり、同じキーで両方を管理できる。

この記事で説明するほとんどのコマンド(Avy、winum、ace-window、scroll-other-windowなど)は、ウィンドウと同様にフレームをまたいで動作できるので、両方のアプローチの利点が得られる。ただし他のEmacsコマンドには境界条件があるかもしれない(フレームを自由に分割できることを前提とするコマンドが多い; 特にorg-modeのコマンド! ただしOrgの状況も改善されつつあるらしいです)。

winum-mode

n個のウィンドウ間を切り替える取り組みの自然な進化形:other-windowのO(n)からwindmoveのO(√n)、さらにO(1)への移行。モードラインにウィンドウ番号を表示して番号でウィンドウを選択できる。

便利なボーナス機能が2つ:

  • ネガティブなプレフィックス引数でウィンドウを削除
  • ミニバッファがアクティブなら常に番号は0

シンプルで短くEmacsのフレーム全体で動作する。ウィンドウ切り替えに著者さんがもっとも使用するのがこれらしい。

  • winumでウィンドウへのアクセス高速化
    ウィンドウnを選択するためのデフォルトのキーバインディングはC-x w だけど、M-0からM-9までの数引数がいらなければ、それらをウィンドウの選択に使用できる。
(defvar-keymap winum-keymap
  :doc "Keymap for winum-mode actions."
  "M-0" 'winum-select-window-0-or-10
  "M-1" 'winum-select-window-1
  "M-2" 'winum-select-window-2
  "M-3" 'winum-select-window-3
  "M-4" 'winum-select-window-4
  "M-5" 'winum-select-window-5
  "M-6" 'winum-select-window-6
  "M-7" 'winum-select-window-7
  "M-8" 'winum-select-window-8
  "M-9" 'winum-select-window-9)
(require 'winum)
(winum-mode)

他のアクションやそれらにのバッファ表示への拡張も可能だが多分不要。なぜなら…

ace-window

答え: これがあるから。

提供するもの:ウィンドウやバッファの管理アクションなら何でもOK。

  • キーボード操作によるEmacsウィンドウ制御の最終形態。
  • コマンドは各ウィンドウ上部にヒントを配置して、対応するウィンドウにフォーカスを切り替える。
  • 現時点では速度が微妙に違う2バージョンがある。
  • ace-window-display-modeがオンだと、winumのように常にモードラインにヒントを表示(少し高速化)。

ace-windowはAvy(入力された文字の補完でその文字位置にジャンプできたりする)のウィンドウ版(両方とも作者はOleh Krehel)。

  • 画面上の文字へのジャンプはAvyでもっとも便利な操作という訳ではない。それはace-windowも同様。
  • ace-windowは必要に応じて表示されているフレームすべてを対象にウィンドウを選択する汎用的な方法を提供する。
  • Avyと同様にace-windowは画面上の任意のウィンドウにアクションをディスパッチできる。選択したウィンドウから離れずにウィンドウの削除、移動、入れ替え、分割、バッファの表示など、さまざまなことができる。これらはace-windowの一部として提供される組み込みのアクションである。
  • ?を押すとディスパッチメニューが表示される(Embarkの15の使用方法ではこのアイデアをさらに掘り下げている)。

マウスを使う (built-in)

マウスでも任意のウィンドウやバッファの管理アクション実行できる。

  • 利点は基本的な使い方の自然な延長によってウィンドウを選択できること。
  • ウィンドウのサイズ変更も簡単。
  • 右クリックメニューやドラッグアンドドロップのサポートなど、Emacsの新しいリリースごとに改善されるコンテキストメニューやドラッグアンドドロップのサポートは非常に直感的(context-menu-modeを参照; ウィンドウ管理にかぎった話しではないが、Emacsのメニューバーを介した発見性は驚くほど優秀)。

マウスによるフォーカスの移動をオンにしたければ

  ;; 負の数値のセットも考慮しよう
  (setq mouse-autoselect-window t)

transpose-frame (回転やフリップフロップ)

ウィンドウのレイアウトを簡単に変換する機能を提供する。

具体的には、フレーム上のウィンドウのレイアウトを回転または反転させるコマンドを提供する。

window-prefix-map (built-in)

ウィンドウ管理用のカスタムコマンドを提供する。デフォルトではC-x wにバインドされており、便利なウィンドウ管理コマンドがいくつかまとめられている。

split-root-window-rightとsplit-root-window-below

フレームのルートウィンドウを分割する。横方向と縦方向それぞれC-x w 3とC-x w 2にバインドされている。

  • window tree
    Emacsのウィンドウはツリー状に配置されている。"実際"のウィンドウはすべて葉(leaf)として配置される。どちらの分割アクションも葉ノードを2つのウィンドウ(分割されるウィンドウと新たなウィンドウ)の親に変更する。

実際にフレーム内で論理的に別タスクのためのスペースを作成するのに役に立つ。

デフォルトの分割コマンドは既存のウィンドウをさらに細かく分割するだけだが、これらのコマンドは葉レベルではなくツリー構造を変更できる唯一の組み込みコマンドである(delete-other-windowsのようにツリー全体をクリアするものを除く)。

ツリー配置に慣れればより細かい制御も可能になるが、それ用のツールはまだ整っていない。

tab-window-detachとtear-off-window

ウィンドウを新しいタブや新しいフレームに移動するコマンド。

  • ルートウィンドウの分割のように論理的なウィンドウ管理に役に立つ。
  • ウィンドウを新しいタブやフレームに移動して新しいタスクを開始できる。

それぞれC-x w ^ tとC-x w ^ fにバインドされているが、ace-windowのディスパッチアクションとして使用したり、使っていないC-x w tやC-x w fにリバインドも可能。

  ;; mouse-9 is the "forward" button on my mousee
  (keymap-global-set "M-<mouse-9>" 'tear-off-window)

other-window-prefix (built-in)

ウィンドウ選択とバッファ表示を分離する方法を提供することで、ウィンドウにまつわる3つの煩わしさを解決する。

煩わしさその1

  1. 多くのEmacsコマンドは主要なアクションとバッファやウィンドウを密接に結びつけている。たとえばfind-fileの実行にはファイル選択、バッファ作成、現在のウィンドウへの表示が含まれている。ウィンドウの選択をコマンドから分離したければfind-file-other-window、find-file-other-tab、find-file-other-frameなどの別コマンドを使う必要があり、それぞれに異なるキーが割り当てられている。
  2. に読み取り専用モードでファイルを開きたければfind-file-read-only、find-file-read-only-other-window、find-file-read-only-other-tab、find-file-read-only-other-frameといったコマンドがあるが、さらに多くのキー割り当てが必要になる。

バッファ選これほど多くの選択肢は必要か? switch-to-buffer-*というコマンドがあるし、bookmark-jumpでブックマークを開くときもbookmark-jump-*の中から選ぶ必要がでてくる。混乱の元。

問題は結合にある。

  • バッファを表示するウィンドウを選択するコマンド
  • 注目している機能をもつコマンド(ここではファイルを開くコマンド)

これらを分離すべきであり、解決策となるのがother-window-prefix(C-x 4 4にバインド)だ。これを使えば次のコマンドは(必要に応じて新しいウィンドウを作成してから)次のウィンドウにバッファ表示する。これで必要なのはfind-file、find-file-read-only、switch-to-bufferだけとなった。プレフィックスとこれらのコマンドを使用してバッファを別のウィンドウに表示できる。

  1. other-window-prefix(C-x 4 4)を呼び出す。
  2. find-file、find-file-read-only、switch-to-buffer、bookmark-jump、またはバッファを表示する任意のコマンドを呼び出す。
  3. 結果: バッファが次のウィンドウに表示される。

しかしコマンド増殖の回避はother-window-prefixが解決する3つの問題のうちの1つに過ぎない。

煩わしさその2

上記の例では最低でもcommand-other-window分の呼び出し選択肢があるのでまだまし。逆に他の多くの場合では選択肢がなく、固定された望まある動作にしたがわざるを得ない。

ここでother-window-prefixが役に立つ。たとえばForgeパッケージではissueタイトルでRETを押すとカレントバッファでissueがオープンする)。現在のところ"リンクを別のウィンドウで開く"選択肢がないForgeでもother-window-prefixは役に立つ。

煩わしさその3

3番目の問題となるのが前の2つの問題の解決の組み合わせ方。

  • たとえばForgeの姉妹パッケージのMagitは、一般的にRETの前にユニバーサル引数(C-u)を使用すると、リンクを次のウィンドウでオープンする。
  • 一方でOrgモード、Notmuch、Elfeed、EWWでは異なるウィンドウでリンクを開く方法が提供されていないか、違う方法を提供している。
  • たとえForgeが何らかの方法を提供しても状況が悪化するだけかもしれない。

しかしother-window-prefixを実行してからリンクオブジェクトをアクティブ(マウスクリックでも可)にすれば、一貫して次のウィンドウでオープンされるだろう。

  • このキーバインドは何?
    一見戸惑うようなC-x 4 4、C-x 4 1、C-x 5 5などの奇妙なキーバインディングには一貫したルールがある。

    特定のウィンドウ操作に関連するキーバインディングは、メニューのようにプレフィックスにグループ化されている。たとえばctl-x-4-mapには他のウィンドウを使用するコマンド、C-x 4 . はポイント位置のオブジェクトの定義(デフォルトのM-.と同様)にジャンプするが、オープンには他のウィンドウを用いる。ctl-x-5-mapのほとんどは新しいフレームを作成するコマンド、タブバーアクションはC-x tにグループ化されている。

    各マップの最後の基本となるキーは、一貫したパターンにしたがう。fはファイルのオープン、rは読み取り専用モードでオープン、bはバッファに切り替える。C-x 4 4、C-x 5 5、C-x t tの4、5、tは、次のバッファアクションがそれぞれ別のウィンドウ、新しいフレーム、タブにリダイレクトされることを強調している。

さらに以下ではace-windowを使用して(著者さん: これ以外に何があるだろう?)、次のコマンドのバッファを任意のウィンドウ(必要に応じて作成したウィンドウを含む)にリダイレクトする方法を極限まで追求しよう。

ウィンドウ構成の保存とリストア

やや荒っぽい手段だがwindow-configuration-to-registerは、特にEmacs初心者にとって大きなリセットボタンとして、完璧に機能する。このコマンド(C-x r w)で現在のウィンドウ構成をいつでもレジスタに保存できる。そしていつも通りフレームがちらかってしまったらjump-to-register(C-x r j)で保存した構成を復元できる。おしまい。

  • 再起動をまたいだウィンドウ構成の永続化
    • window-configuration-to-registerのelispバージョンがcurrent-window-configuration関数。
    • 戻り値を変数にバインドしておけばset-window-configurationでフレームに適用できる。

prin1などの方法でこのlispオブジェクトデータをディスクに永続化する手段と組み合わせれば、persistやmultisessionなどのライブラリを使用してEmacsセッション間で機能する状態復元機能の素ができる。

問題は各ウィンドウのカーソル位置までウィンドウ配置を復元してしまう点(これはほとんどの場合望ましくないはず)。
他の問題として、適切なタイミングにおけるウィンドウ構成の保存を身につけるには、並外れたレベルの先見性を要するという点がある。Emacsが自動的に保存してくれれば...

"やっちった!"とき用のオプション

もちろんEmacsなら自動的に保存できる。

過去のウィンドウ配置のスタックをEmacsに保持させて、バッファの変更をundo(元に戻す)/redo(やり直す)するのと同じ感覚でそれらを巡回するよう要求できる。Emacsの用途に応じて3つのマイナーモードがあり、それぞれ独立にオンにすることができる。

  • winner-mode ::
    タブを使用していなければ、winner-undoとwinner-redoを呼び出してウィンドウ構成の変更のundo/redoができる。これはフレームそれぞれにたいしてウィンドウ構成履歴を独自に維持する。

  • tab-bar-history-mode ::
    タブを使用していればタブはそれぞれ独自に履歴スタックをもっているので、その履歴を巡回すればよい。関連するコマンドはtab-bar-history-backとtab-bar-history-forward。

  • undelete-frame-modeとtab-undo ::
    フレームやタブの作成や削除を頻繁に行う場合はこれ。フレームを間違って閉じてしまったらC-x 5 uでundelete-frame、C-x t uでtab-undoを呼び出す。

これらのオプションは以前の配置に戻す前に選択したウィンドウを一時的に最大化したいなど、いろいろ行いたいときに役に立つ。

ここからは抜粋がメインです

ただし慎重に手作業で配置したウィンドウをEmacsが台無しにするとき、たとえばウィンドウを間違った位置にポップアップしたり、ウィンドウ分割をリサイズしたりするときにはwinner-modeなどが推奨されることも多い。これはアンチパターンではないだろうか? Emacsの振る舞いを修正するために常にwinner-undo(や類似物)を使用しているのなら、真の問題はそもそも最初にEmacsがバッファを間違ったウィンドウに表示することだろう。これはすなわち苛立たしいデフォルトという原因によってもたらされた結果なのだ。

後述する。

深堀り

ここまで読んで興味が沸いたらメインコースへ: ツールの調整、カスタマイズ、バリエーションと見てきた中で、より効果的だと感じるものを堀り下げる。

Emacsの苛立たしさには2段階ある。

  • 第1段階
    使い方がわからず、キーバインディングや用語がわかりにくく、他のソフトウェアとはやりかたが違って何もうまくいかない。パッケージをインストールして欠点を軽減しようとすると謎めいた暗号のようなエラーが発生する。シングルスレッドでは少し間違いを犯すだけで簡単に処理速度が極端に低下するし、ガベージコレクターは最悪のタイミングで発火する。うまく動作するはずのものが動作しないのには苛々させられる: ウィンドウ管理がこんなに複雑であってはいかんだろう!?

  • 第2段階
    長い年月(数年、数十年?)を経て、Emacsの内部動作についての理解は深まった。イベントループの仕組み、バッファ、ウィンドウ、キーマップ、テキストプロパティ、オーバーレイといったデータ構造についても学んだ。一般的なElispのイディオムやマクロ、よくある落とし穴にも慣れた。すると今度はEmacsのAPIの実際の欠点が苛立ちの種になるのだ。 ウィンドウ管理がこんなに複雑であってはいかんだろう!?

さて、ここまでおつかれさまでした。

ここからの内容はこれら2段階の苛立ちの中間にいる人を対象とする。ウィンドウ操作のためのアイデアを提供するために、相反する提案も多く含まれている。これらのアイデアの実装するには若干の調整を要するかもしれないし、そのままコピーしても期待通りの結果が得られないかもしれない。したがってEmacsに不慣れな人はもう少し経験を積んでから戻ってくることをお勧めする。

往復手法

提案: クイックウィンドウ選択

今まで観察したかぎり、

  • 同時に表示するウィンドウがいくつあっても、ほとんどの場合に必要なのは2つのウィンドウ間での切り替え。たとえばコードとREPL、コードと検索結果、文章とノートなどや、プログラミングや文章以外では、レンダーやアジェンダウィンドウと拡張されたエントリウィンドウ、メールの受信トレイウィンドウと開かれたメールなど。

  • 上記2つ以外のウィンドウはドキュメント、デバッグ情報、メッセージ、ログやコマンド出力、目次、ファイルエクスプローラー、ドキュメントプレビューなど、頻繁に目は通すものの切り替えることはあまりないウィンドウで有用な情報を表示している。

通常メジャーモードは関連する2つのウィンドウ間を行き来するために一貫したキーバインディングを提供する。一般的な例としてはEmacsのいくつかのプログラミングモードで使用されるC-c C-zがある。これはコードウィンドウと関連するREPL間を切り替えるために使用される。

このアイデアを一般化すれば、任意のウィンドウペア間を切り替えるコマンドを提供できる。

  (defun other-window-mru ()
    "フレームでもっとも最近使用したウィンドウを選択"
    (interactive)
    (when-let ((mru-window
                (get-mru-window
                 nil nil 'not-this-one-dummy)))
      (select-window mru-window)))

  (keymap-global-set "M-o" 'other-window-mru)

2つ目のウィンドウを選択する方法は自由(マウス、ace-window、winumなど)。一度選択すればその後はother-window-mruが対応する。

other-windowの改善

other-windowの基本的なアイデア(フレーム内のウィンドウ間を循環的に移動する)を維持しつつ、その順序を改良すればよりDWIM(Do What I Mean)的にできる。other-windowは単純なアイデアだが、ウィンドウの選択順序を工夫すれば作業スタイルにより適した形に調整できるはず。

倍加

ウィンドウが1つしかない場合にother-windowがフレームを分割するようにすれば、コマンドに新たな用途を持たせることができる。

  (advice-add 'other-window :before
              (defun other-window-split-if-single (&rest _)
                "ウィンドウが1つだけならフレームを分割"
                (when (one-window-p) (split-window-sensibly))))

switchy-window

直感的な変更をもう1つ。ウィンドウを時計回りの空間的な順序ではなく、最後に使用した順にサイクルさせてみる。これは、alt-tabや一部のWebブラウザがタブをサイクルさせる方法と似ていえう。多少手間がかかるがswitchy-windowパッケージが代替コマンドを提供しており、other-window用にもswitchy-windowが代替コマンドを提供している。

ウィンドウをサイクルさせる際、switchy-windowはウィンドウが選択されたまま数秒待機してから、そのウィンドウを使用済みとして記録し、最近使用したリストを更新します。これは実際にはかなりスムーズに動作するので、switchy-windowを呼び出せば、ほとんどの場合には移動したいウィンドウに移動する(とはいえback-and-forthメソッドで説明したシンプルなバリアントのほうが好み。)。

other-window-alternating

back-and-forthといえば、もう1つ変種がある。最初は混乱するがDWIM的に気に入るかもしれない。other-windowが連続して呼び出された場合には、ウィンドウ切り替えの巡回方向を逆にする。ウィンドウが2つだけなら違いはないが、それ以上だと、2つのウィンドウ間を交互に切り替えることが自然になるだろう。ウィンドウが循環順序では隣接していなくても問題ない。

  (defalias 'other-window-alternating
    (let ((direction 1))
      (lambda (&optional arg)
        "毎回巡回方向を前回と逆にして`other-window'を呼び出す"
        (interactive)
        (if (equal last-command 'other-window-alternating)
            (other-window (* direction (or arg 1)))
          (setq direction (- direction))
          (other-window (* direction (or arg 1)))))))

  (keymap-global-set "M-o" 'other-window-alternating)

  ;; repeat-modeに対応
  (put 'other-window-alternating 'repeat-map 'other-window-repeat-map)
  (keymap-set other-window-repeat-map "o" 'other-window-alternating)

ace-windowのディスパッチによるウィンドウ魔術

ace-windowは入力にしたがって候補の中からウィンドウを絞り込んで処理を行う。任意のウィンドウで任意のアクションを呼び出すための3段階プロセスのうちの最初の2つのステップである抽出と選択を理想的に行うことができる。

window_magic.png

no-ace-wm.png

with-ace-wm.png

実際の拡張は"ace-windowアクション"を定義することにより行う。

  • "ace-windowアクション"を定義する。
  • aw-dispatch-alistにそのバインディングを追加することによって拡張を行う(事前定義されたアクションもある)。
    アクション関数はウィンドウを受け取り、そのウィンドウに処理を行う。
  • ace-windowコマンドはエントリーポイントとして機能する。

ace-entry.png

制御フローは一般的にAvyの動作と似ているが、completing-readの代替としては少々力不足なので、パターンを反転させてace-windowの選択メソッドをace-windowコマンド内で使用したい(まさにその役割を果たすのがaw-select)。

reversed.png

基本的なパターン: (aw-select nil)

  • このシンプルな呼び出しは選択したウィンドウをリターンする。
  • そのウィンドウでタスクを実行できる。
  • ちなみにaw-selectの引数は選択のプロセス中にモードラインにメッセージを追加するためのものだが、それには関心がないので指定していない。

たとえばscroll-other-windowがスクロールするウィンドウを設定することができるが、以下でこのアイデアを一般化する。

tear-off-windowとtab-window-detach

Emacsのインタラクティブなウィンドウ関連コマンドは、すべてカレントウィンドウに機能する。ここでは、ウィンドウ用プレフィックスマップ(C-x w)で任意のウィンドウにインタラクティブに適用可能なコマンドをいくつか作成してみる。

  (defun ace-tear-off-window ()
    "ace-windowでウィンドウを選択してフレームから外す

  外したウィンドウは新たなフレームに表示する; `tear-off-window'を参照"
    (interactive)
    (when-let ((win (aw-select " ACE"))
               (buf (window-buffer win))
               (frame (make-frame)))
      (select-frame frame)
      (pop-to-buffer-same-window buf)
      (delete-window win)))

  (defun ace-tab-window-detach ()
    "ace-windowでウィンドウを選択して新たなタブに移動する"
    (interactive)
    (when-let ((win (aw-select " ACE")))
      (with-selected-window win
        (tab-window-detach))))

アクションそれぞれにたいして1つのace-windowベースのコマンドを定義するのは、拡張性や有用性の面で望ましくない。ウィンドウ選択手順をアクション手順から分離して後者を一般化する方が望ましい。以下ではこれを行うための2つの異なるアプローチを探求していく。

ace-window-one-command: ace-windowですべてのコマンドでを実行する

上記例を一般化することで、反転したace-windowパターンの概念が明確になる。以下はもっとも汎用的で構築が容易なバージョン:

  1. aw-selectを呼び出してウィンドウを選択(completing-readステップに該当)
  2. このウィンドウで任意のアクションを実行
  3. 元のウィンドウに戻る

選択したウィンドウでEmacsのイベントループをシミュレートしてこれを行う
ウィンドウを切り替えて、任意のキー列を読み取り、切り戻す前に実行する。

  (defun ace-window-one-command ()
    (interactive)
    (let ((win (aw-select " ACE")))
      (when (windowp win)
        (with-selected-window win
          (let* ((command (key-binding
                           (read-key-sequence
                            (format "Run in %s..." (buffer-name)))))
                 (this-command command))
            (call-interactively command))))))

  (keymap-global-set "C-x O" 'ace-window-one-command)

このアプローチはアクションの面で利点がある:

  • どんなシンプルなコマンドでも機能する
  • aw-dispatch-alistによるアクショの事前設定が不要
  • 設定するものや覚えることが何もない

ace-window-one-commandは、異なるウィンドウで任意のコマンドを素早く実行するために役に立つ。以下ではこのアイデアを堀り下げる。

  • Embarkとの比較
    Emacs(やace-window)において通常のパラダイムであるアクション->選択の逆転がEmbarkの核心であり、わたしも記事を 書いたことがある。もちろこのオブジェクトファーストというアプローチだけではなく、Embarkには多くの核心がある。

ace-windowにたいするプレフィックスコマンド

  • 便利なother-window-prefixシステムだが他のウィンドウコマンドと同じように、ウィンドウを選択する際には循環的に厳格な順序を強制する。唯一一貫性をもって期待できるのは次のコマンドがアクティブなウィンドウに適用されないことだけ。
  • aw-selectは次のコマンドがウィンドウにバッファを表示する場合に使用すべきウィンドウを選択するという、より制御しやすく特化した解決策を提供する。
  • Emacs LispによるManライブラリは、表示場所をカスタマイズするために一連のオプションを提供しているが、この解決策を用いればEmacsにつきものの手間のかかるジャスタマイズ作業を回避できる。
  • ace-windowは表示されているフレーム全体で機能するので、画面上の任意のEmacsウィンドウを選択できる。
    ace-windowアクションを使用して、必要に応じて新しいウィンドウを作成して使用できる。
  • 実際のところace-window-prefixの実装はother-window-prefixよりも簡単
  (defun ace-window-prefix ()
    "次コマンドのバッファー表示に`ace-window'を使用する。
  次バッファーはこのコマンド(ミニバッファーの入力は無視)の直後に呼び出す
  次コマンドが表示するバッファー。
  バッファー表示前に新たなウィンドウを作成する。
  `switch-to-buffer-obey-display-actions'が非nuiなら`switch-to-buffer'コマンドもサポートする。"
    (interactive)
    (display-buffer-override-next-command
     (lambda (buffer _)
       (let (window type)
         (setq
          window (aw-select (propertize " ACE" 'face 'mode-line-highlight))
          type 'reuse)
         (cons window type)))
     nil "[ace-window]")
    (message "`ace-window'が次コマンドのバッファー表示に使用するウィンドウは..."))

*-window-prefixコマンドのキーバインディングに合わせてC-x 4 oにバインドするには

  (keymap-global-set "C-x 4 o" 'ace-window-prefix)

ウィンドウの切り替えは必要か?

簡素化された考え方による基本的な質問: ウィンドウを切り替える必要があるのは なぜ か?

  • 答えその1
    切り替えて留まるパターン。切り替え先のウィンドウで永続的に作業を行うために切り替える。あらゆる形式のテキスト編集がカバーされる。切り替え先のウィンドウが主要な作業領域となる。

  • 答えその2
    切り替えて戻るパターン。ウィンドウやウィンドウの内容と一時的にやり取りするために切り替える。おそらく戻る前にテキストのスクロールやコピー、ウィンドウ削除が必要。切り替え先のウィンドウは補助的かつ一時的な目的地。

いずれもウィンドウの切り替えは目的ではなくコストである。編集プロセスの一部として自動的に行われのが理想。このちょっとした作業を、主要な編集アクションに単に組み込んでしまわなのはなぜか?

切り替えて留まる: ウィンドウ切り替えコマンドとしてのAvy

Emacsにおけるあらゆるナビゲーションは最終的にAvyに帰結する(著者さん談)。テキストの編集(や選択)でウィンドウの切り替えが発生する場合、そこには画面上の特定の場所に移動するという意図がある。

そこまでカーソル移動するに必要なのは

  1. ウィンドウを切り替えてから
  2. 適切な場所にカーソルを移動する

Avyはフレームをジャンプ先の場所の単一のプールとして扱うことにより、

  • このプロセスを単一のアクションにショートカットする。
  • 画面上の任意の文字へのジャンプを支援して、ウィンドウをシームレスに移動する。
  • 少し考え方を変えれば、ウィンドウを完全に独立したオブジェクトとして扱わなくてもよくなる(ナビゲーションという目的においては)。
  • 表示されているウィンドウとフレーム全体を通じて、数回のキーストロークで任意の文字に到達できる。
  • これがウィンドウを横断して移動するための唯一の手段という訳ではなく、pop-global-markなどを使って出発点に戻れる。
  • ウィンドウを感知しないAvy
    ウィンドウやフレームを横断してAvyがしない場合は、おそらくavy-all-windowsをカスタマイズする必要があるだろう。ついでにavy-styleをカスタマイズすることも考慮しよう。Avyでジャンプする方法は1つではない!

これはAvyでできることの表面にすぎないが、よく開拓された領域でもある。

切り替えて戻る: 他のウィンドウでのアクション

論理的に1つのアクションを実行するという理由でウィンドウの切り替えを行うことはよくある。

たとえば、メインバッファに戻る前にisearchなどでビューを絞り込んでから戻る、といった複合アクションを実行することがある。これは、

  1. 切り替え
  2. 主要アクション
  3. 追加アクション
  4. 切り戻し

というステップからなる1つのダンスとみなせる。このダンスを

  1. 具体的で明白な解決策
  2. 再現可能で一般的な解決策
  3. 抽象的で汎用的な解決策

のように段階的に自動化していけばよい(上記のace-window-one-commandが例)。

scroll-other-window (built-in)

  • scroll-other-windowとscroll-other-window-downの歴史は長い。
  • 2つのウィンドウからなるパラダイム(Emacsのデフォルト設定がとても合う)にきれいに収まるから。
  • 使い方と特徴は
    • 一方のウィンドウは編集用、もう一方は参照用として使用する。
    • 編集用ウィンドウを離れずに、参照用ウィンドウを上下にスクロールできる。
    • 任意の数のウィンドウで動作する。
    • スクロールされるウィンドウは、現在のウィンドウから時計回りに次のウィンドウ。

この模式図では、境界があるウィンドウが選択されたウィンドウであり、scroll-other-windowがスクロールするウィンドウは矢印があるウィンドウです:

scroll-other-window.png

3つ以上のウィンドウでは配置を考慮する必要がある。たとえば3つの横並びのバッファ(上記の1-3)があり、2と3で作業する際に1を参照として使用することはできない。2のscroll-other-windowは3をスクロールするので。幸いにもスクロールするウィンドウを選択するルールを指定できる。

  (setq other-window-scroll-default #'get-lru-window)

これにより常にもっとも最近使用されていないウィンドウがスクロールされる。参照用のバッファ1には頻繁にアクセスしないので使用に耐える。他にもバッファ2と3でscroll-other-windowに互いをスクロールさせたいかもしれない。その場合にはもっとも最近使用されたウィンドウを使用すればよい。

  (setq other-window-scroll-default
        (lambda ()
          (or (get-mru-window nil nil 'not-this-one-dummy)
              (next-window)               ; 次のウィンドウにフォールバック
              (next-window nil nil 'visible))))

行ったり来たりする場合にはうまく機能するだろう

  • スクロールするウィンドウを設定する
    変数(other-window-scroll-buffer)を設定すれば、次のウィンドウの代わりにスクロールするバッファを指定できる。ただしこれは主にパッケージの作者を意図したオプションなので、すぐ使うには、別のelispコマンドを書く必要がある。
  (defun ace-set-other-window ()
    "ace-windowでウィンドウを選択して、カレントウィンドウを
  \"他のウィンドウ\"としてセットする"
    (when-let* ((win (aw-select " ACE"))
                (buf (window-buffer buf)))
      (setq-local other-window-scroll-buffer buf)))

これが有用なのは、この関連付けを永続的にしたい場合のみ。そうでなければLRU/MRUメソッドがほとんどの場合はうまく機能するだろう。以下のmaster-modeも参照のこと。

  • 他のウィンドウのスクロール(詳解)
    1. scroll-other-windowのデフォルトのバインディング(C-M-vとC-M-S-v)の有効性は、修飾キーへの許容度次第。モーダル入力方法を使っていれば、再マッピングのよい候補だ。C-M-vはESC C-vでも呼び出せるので、もう一方をESC M-vにバインドすればよい。

    2. scroll-other-windowはミニバッファからでも機能する。通常だとスクロールされるウィンドウはミニバッファを使用するコマンドを呼び出したウィンドウだが、minibuffer-scroll-windowで明示的に設定も可。

    3. Emacs 29以降、PDF等の非テキストバッファにたいするscroll-other-windowの処理が向上した。スクロールは特別な関数によって処理され、現在は標準のスクロールコマンド(scroll-up-commandおよびscroll-down-command)にバインドされている関数を呼び出す。たとえばpdf-toolsパッケージで管理されているPDFバッファを"次のウィンドウ"の位置でスクロールするには:

 (with-eval-after-load 'pdf-tools
   (keymap-set pdf-view-mode-map "<remap> <scroll-up-command>"
               #'pdf-view-scroll-up-or-next-page)
   (keymap-set pdf-view-mode-map "<remap> <scroll-down-command>"
               #'pdf-view-scroll-down-or-previous-page))

isearch-other-window

他のウィンドウのバッファを参考にするというアイデアを発展させると、scroll-other-window の単純な拡張として"次のウィンドウ"を検索することになる。

上記のscroll-other-windowで設定と同じウィンドウで検索する。

  (defun isearch-other-window (regexp-p)
    "次のウィンドウでisearch-forwardを実行する関数。

  プレフィクス引数REGEXP-Pを与えると正規表現検索を行う。"
    (interactive "P")
    (unless (one-window-p)
      (with-selected-window (other-window-for-scrolling)
        (isearch-forward regexp-p))))

  (keymap-global-set "C-M-s" #'isearch-other-window)

other-window-for-scrolling関数は、上記のother-window-scroll-defaultの選択にしたがい適切なウィンドウをリターンする。

キーバインドC-M-sは既にisearch-forward-regexpに割り当て済みだが、たとえばisearch-forwardにプレフィックス引数を与える(C-u C-s)、isearch中にM-rで正規表現検索に切り替えるのように、他の方法でもコマンドを呼び出せる。

  • 他のウィンドウでのアクション実行
    elispで一時的に他のウィンドウに切り替えるには(save-window-excursion (select-window somewin) ...)と(with-selected-window somewin ...)の2つの方法がある。

    前者は実行時のウィンドウ構成を復元(バッファの位置や(point)の値も含まれる)、後者はフレーム全体で変更を保持(通常はこちらが望ましい; 変更が保持されなければ操作に意味がない)する。

次のウィンドウのバッファーを切り替える

  • Emacsでは数百のバッファを持つことができる
  • ウィンドウはそういう訳にもいかない

これがウィンドウ管理問題の原因となっているので、包括的な解決策には既存のウィンドウに表示されるバッファの変更も含まれる。

解決策の1つとしてace-windowのディスパッチシステムがあるが、他にも組み込みのnext-bufferとprevious-bufferコマンドも簡単な解決策を提供する。

  • このコマンドを使えばウィンドウの切り替えを自動化でき、
  • 専用のnext-buffer-other-windowコマンドは不要になり、
  • next-bufferを新しい関数に置き換えるだけで解決する。
  (defun my/next-buffer (&optional arg)
    "次のARG番目のバッファーに切り替える。

  プレフィクスとしてユニバーサル引数を指定すると次のウィンドウで実行する。"
    (interactive "P")
    (if-let (((equal arg '(4)))
             (win (other-window-for-scrolling)))
        (with-selected-window win
          (next-buffer)
          (setq prefix-arg current-prefix-arg))
      (next-buffer arg)))

  (defun my/previous-buffer (&optional arg)
    "前のARG番目のバッファーに切り替える。

  プレフィクスとしてユニバーサル引数を指定すると次のウィンドウで実行する。"
    (interactive "P")
    (if-let (((equal arg '(4)))
             (win (other-window-for-scrolling)))
        (with-selected-window win
          (previous-buffer)
          (setq prefix-arg current-prefix-arg))
      (previous-buffer arg)))

そしてnext-bufferとprevious-bufferの代役を引き継がせる。

  (keymap-global-set "<remap> <next-buffer>"     'my/next-buffer)
  (keymap-global-set "<remap> <previous-buffer>" 'my/previous-buffer)

最後にswitch-to-bufferのフォールバックバージョンを定義、これらをrepeat-mapにまとめてn、p、bで連続して呼び出せるようにする。

  ;; (もしかしたら次のウィンドウで)switch-to-bufferする
  (defun my/switch-buffer (&optional arg)
    (interactive "P")
    (run-at-time
     0 nil
     (lambda (&optional arg)
       (if-let (((equal arg '(4)))
                (win (other-window-for-scrolling)))
           (with-selected-window win
             (switch-to-buffer
              (read-buffer-to-switch
               (format "Switch to buffer (%S)" win))))
         (call-interactively #'switch-to-buffer)))
     arg))

  (defvar-keymap buffer-cycle-map
    :doc "`repeat-mode'を意図したバッファー巡回用キーマップ"
    :repeat t
    "n" 'my/next-buffer
    "p" 'my/previous-buffer
    "b" 'my/switch-buffer)

右上に表示されるキー説明はキーマップ操作の結果を示す。

master-modeとscroll-all-mode

メモ: 他のウィンドウでアクションを実行するためのカスタムソリューションとして、Emacsはmaster-modeが提供している。

  • カレントバッファ(master)にたいしてスレーブバッファ(slave)を指定できる。
  • カレントバッファを離れずにスレーブバッファのスクロール用のキーマップがオープンする。

単独では前述のother-window-scroll-defaulの透明で即時的な解決策に比べて劣るが、master-saysコマンドを使ってキーマップにキーを追加することで、定義されたアクションをスレーブバッファで実行するキーを設定できる。たとえばスレーブバッファを再センタリングする組み込みアクションは:

  (defun master-says-recenter (&optional arg)
    "スレーブバッファを再センタリングする。
  `recenter'を参照"
    (interactive)
    (master-says 'recenter arg))

これはどんなアクションでも可能。たとえばプロジェクトのバッファでシェルやコンパイル用のバッファをスレーブとして、master-modeを使ってページ送り、最新出力のコピー、コマンド送信などができる。

スクロールだけに絞るなら、scroll-all-modeでフレーム内のすべてのウィンドウのスクロールアクションを連動できる。複数のウィンドウビューを同期したい場合など、アクティブウィンドウと他のウィンドウを別々にスクロールするより便利。

** with-other-window: elispヘルパー

切り替え→アクション→切り戻しという一連の操作を自動化する汎用コマンドを書くよりも優れた方法は? 汎用コマンドを書くための汎用マクロが答え。。このマクロを使えばアクションと切り替えを分離できる。

  (defmacro with-other-window (&rest body)
    "other-windowでBODYのフォームを実行する"
    `(unless (one-window-p)
       (with-selected-window (other-window-for-scrolling)
         ,@body)))

上記の例は、以下のマクロの単純な応用例。

  (defun isearch-other-window (regexp-p)
    (interactive "P")
    (with-other-window (isearch-forward regexp-p)))

  (defun isearch-other-window-backwards (regexp-p)
    (interactive "P")
    (with-other-window (isearch-backward regexp-p)))

これは対話的なace-window-one-commandにたいする、elisp側のカウンターパートに該当する。

たくさんウィンドウが/必要/ですか?

エディターのための単一のUIが世界的に受け入れられつつあるようだ。メインウィンドウがあり、上部にタブバー(タブごとに1つのウィンドウがあり)、左側にディレクトリやコンテンツのサイドバーがあり、右側にオプションのアイテムがあり、下にはターミナルエミュレータがあるUIのことだ。

ほとんどのエディターが通達を受け取ったようだが、Emacsは違ったらしい。このウィンドウレイアウトとワークフローはEmacsでも再現できるし、他のエディターのUIでも同様。しかし、これらすべての熱烈なウィンドウ管理から、より基本的な問題を考える必要があるだろう。なぜ複数のウィンドウを持つ必要があるのだろうか?

いくつかの理由がある:

  • 1つのバッファ画面をに専念して、ウィンドの切り替えのかわりにバッファを切り替える。
  • ウィンドウのサイズ変更は不要
  • 検討や編集の間に現れるウィンドウ(ドキュメントウィンドウなど)は、通常はqキーで解除できる。
  • ファイルブラウザなどの特別なバッファは、dired-jumpのような専用のコマンドを使ってアクセスすればよい。

要件を2つのウィンドウに緩和すると、手間が少ない振る舞いのほとんどを維持しつつ、2つ目のウィンドウをライブ参照として使用する利点が追加される。デフォルトではEmacsはこれをうまく行うように設定されており、scroll-other-windowなどのコマンドがそれを証明している。上からの命令による厳格なレイアウトはないが、混沌とした構造のない、雑草のごとく突然現れるウィンドウも存在しない。

結局のところわたしたちはThe Zen of Buffer Displayに戻った訳だ。Emacsが提供するウィンドウ管理の自由がわたしたちの拒絶反応を引き起こすことになるのは皮肉だが、画面に同時に表示するウィンドウ数に関係なく、ウィンドウを全く扱わないように問題を回避することもできる。

ウィンドウの取り扱いを最小限に抑える2つのウィンドウ管理戦略を紹介しよう。1つ目は言葉の意味における"ウィンドウ管理"。

ウィンドウがつくられたから、無視しようぜ

Avyを使ったウィンドウに依存しないジャンプは、一般的な考え方の特殊ケースである。Emacsを使う場合に主に関心があるのはテキストであって、テキストのコンテナとしてウィンドウは不要な抽象化といえる。これはジャンプ先が画面外にある場合、例えばxref-find-definitionsで定義にジャンプする場合に自然な考え方だろう。

ウィンドウに依存しない別のアプローチとしてマークリングとグローバルマークリングがある。これらはジャンプ元の位置を追跡して、pop-to-mark-command(C-u C-SPC)とpop-global-mark(C-x C-SPC)でジャンプバックできる。後者は必要に応じてウィンドウをまたぐことができる。より細かい制御と追跡のステップに改善されたUIを提供する、dogearsのようなパッケージもある。

  • pop-to-bufferにウィンドウをまたいでジャンプさせる
    pop-global-markはデフォルトでは常にカレントウィンドウでバッファを切り替る。ウィンドウ切り替え機能としても使うには少し工夫が必要だ。
  (define-advice pop-global-mark (:around (pgm) use-display-buffer)
    "`display-buffer'経由で`pop-to-buffer'にバッファーをジャンプさせる"
    (cl-letf (((symbol-function 'switch-to-buffer)
               #'pop-to-buffer))
      (funcall pgm)))

point-to-register(C-x r SPC)とjump-to-register(C-x r j)を使えば後で戻る位置を手作業で固定できる。再び、これも副作用としてウィンドウを切り替える。

より永続的をもたせるにはbookmark-set(C-x r m)とbookmark-jump(C-x r b)でブックマークを作成して移動すればよい。

EmacsにはEmacsやユーザーが確認した場所に、ウィンドウをまたいでナビゲートするための多くのオプションがある。これらは21世紀の標準的なIDEウィンドウレイアウトでも、フレーム内の単一のウィンドウで使う場合と同じように機能するだろう。

ウィンドウ問題に対処すれば対処する必要がなくなるよxs

つまり次々とウィンドウが現れる問題を解決するということ。

この記事の主題はEmacsのウィンドウに関する手動操作についてだが、ここではdisplay-buffer-alistと自動ウィンドウ動作に言及せざるを得ない。

基本的な考え方はシンプル:

  • elispコードがバッファを表示しようとするたびに、この変数のルールと表示するバッファを照合します。
  • 一致したエントリが表示方法を指定する。

日常的に使うEmacsのバッファそれぞれにたいしてウィンドウのサイズ、位置、役割、フォーカスを指定するルールを設定してしまえば、ウィンドウ管理問題のほとんどは解決するのではないだろうか?

その通りではあるがdisplay-buffer-alistの問題は機能しないことではなく、設定が非常に手間がかかるという点なのだ。バッファを表示するためのルール作成にはEmacsのAPIの多くの側面(たとえばバッファとモードの判定、ウィンドウの種類とスロット、display-bufferアクション関数、ウィンドウパラメータなど)を理解する必要がある。そして設定を終えた後でも、"ウィンドウ配置を崩さない"というシンプルな意図を表現する簡単な方法がない(display-bufferを過度に設定して上書きはできるが、多くの例外や意図しない動作を引き起こすだろう)。そのためにdisplay-buffer-alistは主にパッケージ作者が自パッケージの自動ウィンドウ動作を指定するための、より親しみやすいインターフェースを提供するものとして主に使用されている。

しかしアルマナックの精神に則り、この話題を手ぶらで終わらせないように努力しよう。

  • Shackleパッケージはdisplay-buffer-alistの問題を解消して、ウィンドウルールを指定するための簡単なelispインターフェースを提供する。変なバッファタイプがウィンドウ配置を乱すのを防ぎたければ、これが最良の選択だろう。

  • Emacsのディストリビューションでは、通常はこれらの設定を指定するための簡単なインターフェースが提供されているので、それを使えば問題ないだろう。

  • さらに細かく調整したければMickey Petersonのウィンドウマネージャーの解説記事、Protesilaos Stavrouのビデオ、elispマニュアルが参考になる。

** Popper, Popwin, shell-pop, vterm-toggle

ウィンドウで散らかっていない最低限の作業スペースを理想として目指すなら、ポップアップマネージャーは役に立つツールだ。

PopwinとPopperは、すべてのバッファが等しく作られているわけではないという観察に基づいたEmacsパッケージである。主に使用するバッファ(プライマリ)と、一時的にアクセスしたいバッファ(ポップアップ)があるとき。display-buffer-alistなどの方法を使えば、これらのバッファを小さな補助ウィンドウで表示して、表示時にカーソルを奪われないようにできる。しかしアクセスの問題は解決しない。ポップアップバッファを呼び出すためのキーアクセスや、ウィンドウを簡単に閉じたり、サイクルしたり、終了させたりする方法が必要だ。

Popperは、ポップアップとして指定したさまざまなバッファに対応しており、1つまたは2つのウィンドウのパラダイムを維持しながら、必要に応じて補助ウィンドウを表示したり閉じたりすることができる。

Popwinはより古くて包括的な実装だが、迅速なキーアクセスと独自のdisplay-buffer設定が含まれていて、必ずしも望んだものではないかもしれない。シェルバッファの呼び出しと閉じるためのキーアクセスだけが必要なら、shell-popやvterm-toggleで十分かもしれない。

欠けている部分

現時点では存在しないが本来は存在すべきウィンドウ管理オプションについて。

「さあ君たちの出番だ!」みたいに結ばれていました。割愛

ここからの展望

冒頭部より抜粋

信じられないかもしれませんが、これが短縮版(のさらに要約、抜粋した版)。この内容の範囲を抑えるために、tab-line-mode、アトミックウィンドウや専用ウィンドウ、サイドウィンドウのようなウィンドウの種類やプロパティに関わるもの、そしてdisplay-bufferの問題など、いくつかのウィンドウ管理戦略を除外しました。

以下割愛

要約おわりのあと書き

冒頭では要約しようと思っていたんですが文章が面白いし、Emacsも29.4がリリースされたみたいなのでそちらもチェックしたいしで後半は抜粋(しかもテンプレは主にAIさんの出力)ばかりになってしまいました。でも読み物として面白かったし、他にものAvyとかEmbarkとか記事はあるみたいだし、読んでみたいですね。

おしまい。

5
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?