8
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

EmacsAdvent Calendar 2024

Day 14

Emacsウィンドウ管理年代記

Last updated at Posted at 2024-12-13

これはKarthik Chikmagalurさんによって記述された記事を日本語に翻訳した記事であり、記事の所有権と著作権はKarthik Chikmagalurさんに帰属します。

元の記事: The Emacs Window Management Almanac | Karthinks


"ウィンドウ管理"ってどんな意味だろう


  • この記事は少なくともEmacsのチュートリアルを終えていること、Emacsの基本的な用語(バッファー、ウィンドウ、フレーム)、およびウィンドウの分割や削除、他のウィンドウの削除、フォーカス切り替えのようなウィンドウアクションに親しんでいることを前提としている。
  • タブはウィンドウ管理というより、主にワークスペースの管理用ツールなので少しだけ触れるに留めている。
  • 記事ではEmacsフレーム内でのウィンドウ/バッファー管理に焦点を当てている。この後述べるツールの多くはフレームを跨いでも同じように機能するが、フレーム間のサポートを有効にするためには、それに応じたスイッチを見つける必要があるだろう。
  • 最後にこれは記事というよりもわたしのウィンドウ管理における年代記のようなものなので、何年かに渡って個人的に探求したツールやアイデアは網羅しているが、役に立つかもしれないが試していないパッケージについては簡単に言及するに留めている。したがって何かを省略していたとしても、有用性に基づいて省略した訳ではない。有用な何かをわたしが見逃しているようなら、是非知らせいただきたい。

この記事の内容はよく知られているツールからヒント、ハッキング、そして最後には忌憚のない意見へと遷移するポイントがいくつかある。この記事には前半ほど多くの内容を詰め込んだ。記述の冒頭部分を読めば解決策の70%が得られるだろう。Emacs初心者なら30%の部分で読むのを止めても構わない。代替え手段についてもリストしておいた。同じことを行うための方法はいくつもあるが、1つだけ採用して他を無視 しても問題ない。後半になるにつれ、記事の内容は独善的になり特異度が増していくだろう。

あなたが読む頃には、この記事は古くなっていることだろう。Emacsのコア部分は非常に安定しているものの、パッケージの開発や放棄によってパッケージの生態系は揺さぶられ続ける傾向がある。組み込みソリューションは存続し続けるが、サードパーティのパッケージについてはその限りではないのだ! とは言えパッケージの存在期間が長くなるにつれて、機能的な存在としては生き残る可能性が高くなる(たとえEmacs Orphanageの凍結エントリーであったとしても)。

新たなアイデアの誕生によって、ここでカバーしていないようなウィンドウ管理への新たなアプローチが生まれることだろう。このような革新の発現がEmacsの領域に制限される必要はない。Emacsが1990年代に導入したアイデアを他のアプリケーションが再発見するのと同じくらい、他のどこかで生まれたアイデアをEmacsが 拝借 再発見するのはよくあることだ。そういった理由により、このトピックについては数年後に再検討する価値があるのかもしれない。

"ウィンドウ管理"ってどんな意味だろう

emacs-window-buffer-frame.png

Emacsではバッファー(連続したテキストの集合)とウィンドウ(フレーム内部にある"表示領域"あるいは"窓枠")を別の概念として分離している。IDEやテキストエディターでは普通はこれらの概念は融合されている。これによりアプリケーションを使う際の認知負荷は軽減されるが、それは同時により柔軟な動作や自由形式によるアレンジへの道を閉ざすことでもあるのだ。たとえば同じファイルに2つのビューをもつことは多くのエディターではできないが、Emacsでは簡単なことだ。Emacs以外のユーザーが接続されていないバッファー、すなわち(恐らくは編集する)ファイルの内容を表していないバッファーという概念にさえ不快感を覚えるのは珍しくない。Emacsのインダイレクトバッファー(indirect buffer: 間接バッファー)のように、具現化された概念は彼らにとってはまったく異質な概念なのである1

Emacsならもっと多くのことが可能になる。しかしこの認知コストはユーザーに対価の支払いを要求する。まったく初めてのユーザーは上述の分離がもたらす可能性を理解していないのでフレームの正しい位置へのウィンドウの配置、正しいウィンドウでのバッファーの表示、理解不足による機会逸失という3回払いでその代償を払うことになるのだ。この記事がこれらのうちの2つのコストに対処する道標となることを願っている。

以降で参照できるように以下に左側のウィンドウが選択された、まだ目茶苦茶になる前のフレーム図を示しておこう:

emacs-window-chart.png

  • 色付きのブロックはそれぞれウィンドウ、数字はそのウィンドウに表示されているバッファーを表す

  • 黒枠で囲んであるのがアクティブなウィンドウ

この記事で説明しないことは…

Emacsでのウィンドウにたいするアクション、あるいはウィンドウ上でのアクションは、Emacsのあらゆるレベルの使用において避けられないほど原始的かつ一般的な操作である。ウィンドウ関連のアクションは驚くほど微妙、広範かつ深いテーマであり15000語程度で探求できる範囲は限られている。そこでまずは曖昧さを取り除き、焦点を絞ることから始めようと思う。この記事は以下の事柄に関する記事ではない

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

Emacsが間違った場所にウィンドウをポップアップし続けたので、わたしのウィンドウ配置が台無しだ!

状況は… 芳しくない。正しいウィンドウにバッファーを自動的に表示するのは一般的には不可能ではないものの、これには複雑な設定が必要でありウィンドウパラメーター、スロット、専用ウィンドウ(dedicated window)といったEmacsのAPIの詳細に関する知識が要求される。APIのdisplay-bufferについてはElispマニュアル2でかなりの紙面を割いて説明されているが、それでも「とにかくやってみたまえ(just go with it)」で結ばれている(訳注: "Just Go with It"は邦題"ウソツキは結婚のはじまり"という映画のタイトルでもあります。未婚者から既婚者が結婚についてしつこく聞かれたら、まあこう答えるかもですね)。自動的なウィンドウ管理についてはこの記事の終わりで簡単に触れているものの、この記事はdisplay-bufferの手綱を握る術に関する記事ではない。このテーマについてはMickey Peterson氏のウィンドウマネージャーの神秘を暴くことに関する記事Protesilaos Stavrou氏によるビデオ、興味がそそられたらマニュアルを参照することをお勧めする。

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

タスク用にウィンドウ同士をグループ化して永続化して、セッションを跨いで使いたいんだ!

Emacsの使用に影響を及ぼす一般的な要因として以下の2つが挙げられる:

  • Emacsのセッションは長時間に及ぶ傾向があり、

  • ユーザーはセッション中ますます多くのタスクをもつ

その結果として幾百ものバッファーをもつことになり、それらをグループ化して分離、保存する術を探し始めることになる。

これはウィンドウ管理と関連はあるものの、あなたが保存したい状態の一部としてウィンドウ配置があるという意味でしかない。

これは好みの分かれる複雑なテーマであり、この記事の範囲を遥かに超えている。

好きなものを選んで欲しい: tab-bartabspaceseyebrowsetab-bookmarkdesktop.elpersp-mode.elperspectiveproject-tab-groupsbeframeactivities.elなど、助けとなるプロジェクトには事欠かない筈だ。

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

Emacsのウィンドウ配置は何故こんなに気まぐれなのか? タイル式ウィンドウマネージャーはこの問題をとうの昔に解決済みなのに!

ウィンドウの配置と管理にたいしてすべてを網羅する先進的なソリューションを提供するパッケージがある。これらのパッケージは本質的にEmacs用のウィンドウマネージャーと言えるだろう。

たとえばEdwinaはウィンドウ管理コマンドの完全な一式とともに、Emacsの手作業によるツリーベースの挙動をDWM-style auto-tiling layoutのマスター/スタックベースの挙動に変更するパッケージだ。

HyControlはウィンドウレイアウトアクション用のコントロールパネルを提供するとともに、フレーム上の均一なグリッド上にウィンドウを表示できる等の機能を提供する3

わたしの経験ではこれらの"完璧"なソリューションは使い始めこそ素晴らしいが、最終的には抵抗感が高揚感を上回った。これはEmacsをカスタマイズするほど当てはまるだろう。長期的に見るとこれらのパッケージがEmacsのAPI上に構築する抽象化によって、ウィンドウ管理から開放されるのではなく制限されてしまうからだ。

では他に何があるだろう? この記事でのウィンドウ管理とは、手作業で行う日常的な意味においてのウィンドウ管理を意味している。つまりウィンドウフォーカスの切り替え、ウィンドウ周りのバッファー移動、ウィンドウの分割やクローズ等々のことだ。これらの作業はたとえあなたがdisplay-bufferをすべて整理してウィンドウをワークスペースにグループ化したとしても、分単位の定期的な編集の過程において頻繁に行う必要がある作業だ。

では本格的に始める前に、一般的な考え方にたいする取捨選択を行っておこう。

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

この長い議論は無意味だ、必要なウィンドウは多くても2つなのだから。

異議あり: 一度に必要なウィンドウが多くて2つなのだ。Emacsではウィンドウを整理して並べても目茶苦茶になってしまうということも理由の1つだ。夢中で記述やコーディングを行っている場合を除外すれば参考資料、検索、検索やコンパイルの結果、ファイルアクセス、シェルやREPL、目次等々への簡単なアクセスを要する場合があるかもしれない。それらすべてを同時にスクリーン上に表示するのか、それとも必要に応じて簡単に表示できればよいかはスクリーンサイズと好みの問題だろうが、いずれのケースでも手作業によるウィンドウやバッファーとの対話が必要となる。どちらのアプローチを採用するにしてもこれは"ウィンドウ管理"の範疇であり、この記事で取り上げるべき対象だろう。

位置選択のための処方箋

マウス使えば? ほとんどのソフトウェアにとってこれは話題にすらない問題だ。

確かにウィンドウを操作する手段として、マウスはもっとも自然な方法だろう。動作のエコ度、反復性ストレス障害(訳注: マウス肘、Emacs小指など)、個人的嗜好のような物議を醸す議論に踏み込まないかぎり、マウスによるアプローチの主な問題点は(キーボードと比較して)学習曲線がないことと、(キーボードと比較して)表現力の欠落の釣り合いがとれていることだろう。

たとえそうであったとしてもEmacsであれば他のほとんどのアプリケーションに比べて、より多くの表現力をマウスから絞り出すことができる筈だ4

わたしはウィンドウの管理にマウスを使うことがよくある。とはいえ特定のコンテキストにおいてのみではあるが。マウスであれこれを参照して欲しい。

ウォーミングアップ

前菜: もっとも有名で一般的に推奨されているウィンドウマネージャーの選択肢を一通り見ておこう。これらのウィンドウマネージャーのカバー範囲にはバッファー管理やお決まりのウィンドウアクション以外にも、フォーカスのあるウィンドウの変更や移動、間違いの取り消しが含まれている5

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

other-windowが提供すること: ウィンドウの選択

other-window (C-x o)はウィンドウ切り替えというエクスペリエンスにたいする基準線となるコマンドだ。これこそEmacsのチュートリアルがウィンドウを切り替えるコマンドとして教えるコマンドであり、ウィンドウの数が少ないうちは十分に機能するだろう:

other-window-chart.png

ウィンドウは(大雑把には)フレームを時計回りに循環して選択される。このアプローチの利点は単純さだ。コマンドとキーバインディングをそれぞれ1つしか使わない。お察しの通り(あるいは経験済みかもしれないが)、ウィンドウが蓄積されるほどに目的地到着までの呼び出しも徐々に増えていくので、3つ以上のウィンドウを一度に表示することが滅多にないケースで最適に機能する。

other-windowの基本的な小技とチューニング
  1. もっとも使われているEmacsコマンドの1つかもしれない。M-oのようなもっと便利なキーにバインドしよう。

  2. 前方あるいは後方、つまり反時計回りにスキップするウィンドウの数の指定には数引数を使用できる。M-3 M-oなら3個前、M-- M-2 M-oなら2つ後のウィンドウが選択されるといった具合だ。しかし残念ながらこのアプローチは、循環がどのような順序で発生するかについての視覚的な理解が要求される。ウィンドウのレイアウトが複雑になってくると、どのウィンドウが3つ先のウィンドウなのかが判りにくくなってくるだろう。

  3. repeat-mode (M-x repeat-mode)をオンにしよう。そうすればC-x o o o o...のようにoだけ、後方ならM-o o o o...のようにOだけでウィンドウを連続して切り替えられる。これならC-x o C-x o C-x o...より余程高速だ。

other-windowのハッキング

ウィンドウパラメーターno-other-windowをセットすれば、other-windowで切り替える先となるウィンドウからそのウィンドウを除外させることができる。ウィンドウパラメーターとはEmacsのデータ構造をもつプロパティであり、それらをセットするためのElisp関数が存在する。これは手動で切り替えるのではなく、特定のバッファークラスにたいして前もってあらかじめdisplay-buffer-alistにセットして使用するパラメーターだ。other-windowが何故(dired-sidebarやdirvish-sideのような)見た目も派手なファイルマネージャーのウィンドウを選択しないのか疑問に思ったことがないだろうか。つまりこれが答えだ。

あなたが今までEmacsで同時に2つまでしかウィンドウウィンドウ表示したことがないとか、多少キーを押す回数が増えても気にしない人ならここで読むのを終えても問題はない。なぜならこの記事の残りの部分は、あなたなら多分解決の必要を感じない(そして恐らく実際に必要ないのだろう)問題にたいする、さまざまな最適化レベルに関する話題だけだからだ!

"次のウィンドウ(next window)"とは何か

"次のウィンドウ"とはother-windowが通常選択するウィンドウ、つまりカレントウィンドウから時計回りで次のウィンドウのことだ。これはelispからnext-window関数を呼び出してアクセスできるウィンドウでもある。Emacsのフレームにおける時計回りのウィンドウ順は、日々の使用で自ずと身についていく。考えるというより、勘で判るという意味でだが。次のウィンドウというのは、ウィンドウ選択以外でも使用する概念なので身につけておけば役に立つだろう。ウィンドウを選択するためにはもっとよい方法も存在する。でなければ記事を書いた意味がない! 次のウィンドウ、他のウィンドウを操作する、scroll-other-windowのようなコマンドにとってはデフォルトとなるウィンドウでもある。ウィンドウを切り替える必要があるのか?を参照のこと。

windmove (built-in)

windmoveが提供すること: ウィンドウの選択、ウィンドウのバッファーの切り替え、それらの削除

Windmoveとは方向によってウィンドウを跨いでフォーカスを移動したり、バッファーを移動するためのEmacsのビルトイン関数だ。Vimユーザーにとっては期待通りに動作するだろう。evil-modeユーザーなら気づいていなかっただけで、あなたはすでにWindmoveのユーザーだ。

other-windowをEmacsのAlt-TABに例えるとしたら、Windmoveはタイル式のウィンドウマネージャーに匹敵するだろう。フレーム内のウィンドウの空間的な配置とウィンドウ選択を結びつけるのだ。これはマウスの使用を除けば、もっとも自然な方法のように思える。

Windmoveの使い方は簡単だ。windmove-left(および-right-up-down)を、修飾キーやプレフィックスキーと方向を表すWASDHJKL、もしかしたら矢印キーの組み合わせにバインドすればよい。

windmove-chart.png

別れ道: この図での右のウィンドウへの移動は、カーソルにたいして正確に右にあるのはどのウィンドウなのかに依存する。バッファー1の上部からwindmove-rightを呼び出すとバッファー2、下部からだとバッファー3にフォーカスが移動する。

Windmoveでウィンドウのバッファーを直接入れ替えることも可能だ。これを使えばフレーム上のウィンドウを手軽に再配置できる6。関連するコマンドはwindmove-swap-states-left-right-up-downだ。

windmove-swap-chart.png

これを行うことで、バッファーと一緒にフォーカスも移動することに注意。

Windmoveにはまだまだ機能がある。たとえばwindmove-delete-*を使えば任意の方向にある次のウィンドウを削除できるが、以下ではこれを行うもっとよい方法を説明しよう。

タイル式マネージャーとの統合

もしタイル式の環境下でEmacsを使用すれば、ネスト(入れ子)されたタイル式ウィンドウマネージャーという状況を手にすることになる。同じキーでEmacsのウィンドウとOSのウィンドウをシームレスに行き来できるように、2つを統合したほうが便利な場合もあるかもしれない(tmuxでVimを使っているユーザーにはお馴染みだろう)。これには多少の手間は要するものの実現は可能だ。Emacs+i3wm(おそらくSwayも可)の統合についてはPavel Korytovによるi3 integrationのポスト、ウィンドウマネージャーについてはわたしも1つqtile用の設定を記述したことがある。このプロジェクトについては詳細に述べることにしよう。

frames-only-mode

frames-only-modeが提供すること: Emacsのウィンドウ処理をOSに任せる

frames-only-mode.png

議論しているテーマはタイル方式についてだが、ネストされたウィンドウマネージャー、つまりタイル式ウィンドウマネージャー内部でのEmacsという状況には、ウィンドウ管理でEmacsの手を煩わせないという別次元の解も存在する。ウィンドウではなく新たなフレームですべてのバッファーをオープンして、それらのフレームの配置や整頓をウィンドウマネージャーの役目にしてしまうのだ。これによりEmacsのバッファーはOSのウィンドウと同格になり、両方を同じキーで管理できるようになる。

この記事で紹介する他のほとんどのコマンド(Avy、winum、ace-window、scroll-other-windowなど)は、フレームを跨いで使ってもウィンドウのときと同様に機能する。つまり2つのアプローチのいいとこ取りができるということだ。とはいえその他のEmacsコマンドの場合と同じように、例外は必ず存在する。そのようなコマンドの多くは、フレームを自由に分割できることを前提とするコマンドだ7

Linuxユーザーへ: コンポジット方式のWaylandでは、わたしはframes-only-modeを試したことがまだない。

winum-mode

winumが提供すること: ウィンドウの選択と削除

ウィンドウ間の切り替え向上はother-windowのO(n)からwindmoveのO(√n)、そしてその努力の自然な流れによってWinumのO(1)へと進化を遂げた。

winum-chart.png

他にも役に立つボーナス機能が2つある:

  • 負のプレフィックス引数とともにコマンドを呼び出すとウィンドウを削除

  • ミニバッファーがアクティブなら常に数字の0を割り当てる

シンプルで短く、Emacsのフレームも跨いで機能する。わたしがウィンドウ切り替えにもっとも使用する方法がwinum-modeだ。

winumによるウィンドウアクセスの高速化

わたしの好みからするとデフォルトのキーバインディング(ウィンドウnを選択する場合にはC-x w \<n>)は、他の2ステップのキーバインディングと同様冗長すぎる。M-0からM-9による数字引数へのアクセスを失うことを気にしないのであれば、かわりにウィンドウの選択に流用するのはどうだろう:

(defvar-keymap winum-keymap
  :doc "winum-modeアクション用のキーマップ"
  "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)

ウィンドウにたいするウィンドウの切り替えや削除以外のアクションを追加するようにwinum-modeを拡張することもできるが、次に説明するパッケージのおかげでその必要はほとんどないだろう。

ace-window

提供すること: ウィンドウおよびバッファーの管理にたいするすべてのアクション

キーボード駆動によるEmacsのウィンドウ制御というゲームを終わらせてしまったのがace-windowだ。

ace-windowコマンドはウィンドウそれぞれの上端に"ヒント"を配置する。そのキーをタイプすればそれに応じたウィンドウにフォーカスが切り替わるのだ。

ace-window-chart.png

現在のところは2ステージ版のwinumより若干遅いが、ace-window-display-modeをオンにすればwinumのウィンドウ番号のようにヒントが常時表示されるようになるので、このプロセスも多少スピードアップするだろう:

ace-window-display-mode.png

Avyがスクリーンにたいして行うことを、ウィンドウにたいして行うのがace-windowだ8

しかしそのAvyにとっては、文字に応じてスクリーン上をジャンプするのはAvyでできることのうちのほんの一部に過ぎない。ace-windowもできることがウィンドウの切り替えだけだったら、それほど推奨はしなかったと思う。切り替えだけではなく、ace-windowは必要であればEmacsのすべてのフレームを跨いで、ウィンドウを"ピックアップ"する汎用的な方法を提供するのだ。そのウィンドウにたいして何を行うかは、あなたの選択に任されている。ace-windowはAvyのようにスクリーン上の任意のウィンドウに、アクションをディスパッチできるのだ。選択したウィンドウを離れることなくウィンドウの削除、移動や入れ替え、分割、バッファーの表示等々を行うことができる。これらはace-windowの一部として提供される、ただの組み込みアクションに過ぎない:

ace-dispatch-chart.png

ace-windowの使用時に?を押下すると、ディスパッチメニューが立ち上がる9

ビデオ実況
  1. ウィンドウを2つ以上オープンしてace-windowを呼び出す(ウィンドウが2つ以下なら変数aw-dispatch-alwaystすること)。

  2. ?を押下してディスパッチメニューを立ち上げる。

  3. ウィンドウを水平に分割するディスパッチキーを押下する(ビデオではv)。

  4. 分割したいバッファーに応じたace-windowキーを押下する(ビデオではe)。

  5. ステップ1とステップ2を繰り返す。

  6. ウィンドウを垂直に分割するディスパッチキーを押下する(ビデオではs)。

  7. 分割したいバッファーに応じたace-windowキーを押下する(ビデオではw)。

マウスであれこれ (built-in)

マウスが提供すること: 任意のウィンドウやバッファーへの管理アクション

さあ遂にマウスの出番だ。

ウィンドウ管理でのマウスの利点は即効性と明確さだろう。マウスの基本的な使い方を自然に延長したやり方でウィンドウを選択できるし、ウィンドウのリサイズも簡単だ。コンテキストメニュー(右クリック)とドラッグアンドドロップをサポートしている。Emacsが新しくリリースされる度にこのサポートは、非常に直感的なものへと進化してきている10。しかし残念なことにEmacsユーザーはマウスの使用に関して特に偏向した持論をもつ傾向があるので、欠点の緩和について議論する前に、部屋にいるこの齧歯類(訳注: マウスには英語で鼠という意味もある)について少し話しておくべきだろう。

わたしはEmacsでは絶対にマウスを使わない。ただし他の何かですでにマウスを使っていた場合は別だ。そのようなタイミングでEmacsをマウスで操るのは、正にもっとも抵抗感が少ない流れと言える。すでに手がキーボードから離れているのなら、マウスでEmacsを操作するのはとても容易だ:

ビデオ実況

このデモでは以下の操作を、マウスのジェスチャーを用いて行う方法を紹介する:

  • フレームを垂直および水平に分割

  • ウィンドウの削除

  • ウィンドウ内のバッファーを循環させる

  • 左右のウィンドウを入れ替え

  • ウィンドウに最後に表示された2つのバッファーを交互に表示

focus-follows-mouseの動作をオンにしたいと思ったら:

;; 負の値にセットするのもあり
(setq mouse-autoselect-window t)

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

transpose-frame が提供すること: ウィンドウレイアウトを簡単に変更できる

transpose-frameはフレーム上のウィンドウの回転(rotate)、反転(mirror)を行うためのコマンドを提供します、というのがこのパッケージの謳い文句だ。rotate-frameflip-frameflop-frameを適切なキーにバインドするほど、わたしはこれを頻繁に使用している。皮肉なことにtranspose-frameコマンド自体が役に立ったことはほとんどない。これはフレームの対角線を軸にウィンドウを転置するコマンドだ。

rotate-frame

rotate-frame-chart.png

flip-frame

flop-frame-chart.png

flop-frame

flip-frame-chart.png

window-prefix-map (built-in)

window-prefix-mapが提供すること: 特別誂えのウィンドウ管理コマンド

window-prefix-map(デフォルトではC-x wにバインドされている)には、役に立ついくつかのウィンドウ管理コマンドが集められている。

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

フレームのルートウィンドウを分割する。説明を聞くより見たほうが早い :

emacs-window-root-frame-split.png

これらはそれぞれC-x w 3C-x w 2にバインドされている。

ウィンドウツリー

いい機会なので説明しておこう。Emacsにおいてウィンドウはツリー構造にアレンジされている。このツリー構造では"現実のウィンドウ"がleafノード、すなわち葉ノードとなる。分割アクションを行う度に葉ノードが2つのウィンドウ(分割したウィンドウと新たに分割されたウィンドウ)の親ノードになるという訳だ。これはi3bspwmのような手動タイル式ウィンドウマネージャーによるウィンドウのアレンジによく似ているので、冗長性を改善するためにパッチを入力したほうがよいだろう。

わたしの知る限り、これらはただのEmacsの組み込みコマンドだ。これらのコマンドを使えば、(delete-other-windowsのように)単にツリー全体をクリアーするのではなく、葉ノード以外のレベルでツリー構造を変更できる。現実的側面から考えると、フレーム内でタスクを論理的に分割するために空きを作成するなど、役に立つことが多い(デフォルトの分割コマンドは既存のウィンドウをさらに細分化するだけだ)。

ツリーの配置を理解することでよりキメの細かい制御が可能になる筈だが、それ用のツールはまだ登場していない。 後述が提言を参照のこと。

tab-window-detachとtear-off-window

ウィンドウを新たなタブやフレームに手軽に移動できるコマンド。

emacs-window-tear-off.png

ルートウィンドウのときと同様、これらのコマンドは論理的なウィンドウ管理に非常に役に立つ。ウィンドウを取得したら新たなタブかフレームに移動して、そこで新しいタスクを開始するといった具合だ。

これらのコマンドは、C-x w ^ tC-x w ^ fという何ともやれやれなキーにバインドされている。かわりにace-windowのディスパッチアクションにすることもできる。ace-windowであれば何でも可能だ。デフォルトではバインドされていない、もう少しまともなC-x w tC-x w fにバインドしてもよい。わたしはウィンドウを切り離す必要がある場合には、マウスを好んで使っている:

;; わたしのマウスはmouse-9が"前進"のボタン
(keymap-global-set "M-\<mouse-9>" 'tear-off-window)

other-window-prefix (built-in)

other-window-prefixが提供するのはバッファーの表示からウィンドウ選択を切り離して、ウィンドウにまつわる3つのイライラの解決だ。

イライラその1

Emacsの多くのコマンドは、バッファーとウィンドウという主要なアクションを密接に結合している。たとえばfind-fileを実行するということにはファイルの選択、バッファーの作成、そのバッファーをカレントウィンドウに表示するというアクションが含まれている。ウィンドウの選択をコマンドから切り離したい場合にはfind-file-other-windowfind-file-other-tabfind-file-other-frame(それぞれ独自のキーバインドをもつ)といったいくつかの選択肢から1つをピックアップする必要がある。読み取り専用モードでファイルをオープンしたければ find-file-read-onlyfind-file-read-only-other-windowfind-file-read-only-other-tabfind-file-read-only-other-frameがあり、それらの分のキーバインドが増えた。

同様の選択をバッファーの選択にも適用したい? ならばswitch-to-buffer-⋆という別のコマンド群も加えよう。bookmark-jumpでブックマークをオープンする場合用には? いくつかあるbookmark-jump-*コマンドのうちから1つ選択しよう。これは狂気への道だ。

カップリング、つまり結合されていることが問題なのだ。バッファーを表示するウィンドウの選択は、コマンドの主機能、たとえばこの例ではファイルのオープンから分離可能であるべきなのだ。この問題にたいする解決策となるのが、other-window-prefixだ(C-x 4 4にバインドされている)。

このコマンドを使えば次のコマンド(バッファーをウィンドウに表示するコマンドなら何でも)が次のウィンドウに表示される(必要ならウィンドウを作成する)。これで必要なコマンドはfind-filefind-file-read-onlyswitch-to-bufferだけになった。このプレフィックスを使えば、ウィンドウでの表示を必要とするバッファーを、以下のように別のウィンドウにリダイレクトできる:

  1. other-window-prefix(C-x 4 4)を呼び出す

  2. find-filefind-file-read-onlyswitch-to-bufferbookmark-jump、あるいはバッファを表示する別のコマンドを呼び出す

  3. 結果: バッファーは次のウィンドウに表示されるだろう

この問題を解決する方法として、以前の記事でEmbarkに触れたことがある。

ビルトインのother-window-prefixよりもEmbarkの方が、この問題をエレガントに解決できるというのは嘘ではない。

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

イライラその2

上述の例では、少なくとも*command*のかわりに*command*-other-windowを呼び出すという選択肢が1つはあった。多すぎる。選択肢が1つでも存在するというのがラッキーなのだ。ほとんどの場合には選択肢など存在せず、望まぬ動作にただ翻弄されるがままだ。リンクのようなオブジェクトをアクティブにしたときなどが、典型的なケースだ。以下の例(Forgeパッケージより)ではissueのタイトルでRETを押下すると、そのissueがカレントバッファーでオープンされる。

ビデオ実況

これはForgeパッケージで表示しているコードレポジトリのissueリストだ。

  1. issueの上でRETを押下する

  2. カレントウィンドウでオープンされるのでリストとアイテムのパターン (完全なリストと選択したissueを同時に閲覧する)は拒絶される。

この記事の執筆時点では、Forgeは別のウィンドウで"リンクをオープン"する手段を何も提供していない。other-window-prefixに助けてもらおう:

ビデオ実況
  1. C-x 4 4other-window-prefixを呼び出す

  2. issueの上でRETを押下、"次のウィンドウ"でissueがオープンする(次のウィンドウがないので新たにウィンドウを作成してオープンする)

イライラその3

このコマンドが解決してくれる3つ目の問題は、上述の1つ目と2つ目が組み合わさったパターンだ。Forgeの兄弟分であるMagitは問題を解決する手段を用意している。考えてみよう。一般的にMagitはRETの前にユニバーサル引数(C-u)を使用することによって、"リンク"を次のウィンドウオープンする。Orgモード、Notmuch、Elfeed、EWWには次のウィンドウでオープンする手段がないか、別のウィンドウでリンクをオープンするために互いに別の手段を提供したりする。もしもForgeが別の方法を何か提供していたとしたらそれは実際のところ、ある意味においては状況を悪化させていたということになっていたのではないだろうか。ところがありがたいことにother-window-prefixなら、アクションがどのように動作すべきかというパッケージ作者それぞれの理念にカスタマイズを加えたりあるいは迎合する必要から開放されるのだ。other-window-prefixを実行して、それから"リンク"オブジェクトをアクティブにしよう。お好みによってはマウスでクリックしてもよい。リンクは一貫して次のウィンドウでオープンされるようになる。

他のコマンドも参照して欲しい: same-window-prefix (C-x 4 1)はカレントウィンドウに次のコマンドのバッファー(もしあれば)の表示を強制する。other-frame-prefix (C-x 5 5)とother-tab-prefix (C-x t t)はそれぞれ次のコマンドのバッファーを新たなフレーム、あるいはタブにオープンする。

これらのキーバインディングで何ができるか?

C-x 4 4C-x 4 1C-x 5 5のように狂気じみた見た目のキーバインディングにはそれなりの理由がある。

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

各マップにおいて"基本"となる最後のキーは、一貫したパターンにしたがっている。ファイルのオープンはf、何かを読み取り専用でオープンするならr、バッファーの切り替えはb等々といった具合だ。そこでC-x 4 4C-x 5 5C-x t tの最後の45tだが、これらはそれぞれ次のバッファーアクションを別のウィンドウ、新たなフレーム、新たなタブにリダイレクトするという動作を補強する意味合いであることが判るだろう。

さらに以下ではace-window(他に何があるというのだ)によってこのアイデアを極限まで推し進めて、次のコマンドのバッファーを任意のウィンドウ(ジャストインタイムで作成したウィンドウを含む)にリダイレクトする方法を議論しよう。

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

切れ味は鈍いもののwindow-configuration-to-registerは、赤い大きなリセットボタンとしてEmacs初心者にとっては完璧だろう。どんなタイミングでもこのコマンド(デフォルトではC-x r wにバインドされている)で、カレントのウィンドウ構成をレジスター11に保存できる。危惧した通りにフレーム上が目茶苦茶になった後でも、jump-to-register (C-x r j)で保存した構成をリストアできる。これだけだ。

セッション間でウィンドウ構成を永続化する

current-window-configuration関数はwindow-configuration-to-registerのelispバージョンであり、この関数のリターン値を変数にバインドして、set-window-configurationでそれをフレームに適用できる。lispオブジェクトのデータをディスクに永続化するprin1、あるいはpersistmultisessionのようなライブラリー組み合わせることによって、Emacsのセッション間に跨がる状態リストア機能の種とすることも可能だ。言うまでもないと思うがこれは原始的なアプローチであり、ウィンドウ構成の永続化に挙げた多くのパッケージリストから選んで使うほうがよいだろう。

この方法の問題の1つは、それぞれのウィンドウのカーソル位置までウィンドウ配置がリストアされることだ。これが期待通りにリストアされることは滅多にないだろう。

もう1つ別の問題として適切なタイミングでウィンドウ構成を忘れずに保存するためには、とてつもないレベルの先見性が要求される。ウィンドウ構成が変更される度に、Emacsが自動的に実行してくれれば…

"oops"オプション

…前節からの続きだが、Emacsによる自動的な保存はもちろん可能だ。Emacsに過去のウィンドウ配置用スタックの保守させて、バッファーの変更にたいしてundoやredoを行うのと同じように、スタックを循環すればよいのだ。

Emacsを使用する目的に応じて3つのマイナーモードがある。これらはそれぞれ独立してオンに切り替えられる。

  • winner-mode

タブを使用しない人向け。winner-undowinner-redoをを呼び出せばそれぞれウィンドウ構成の変更をundoまたはredoする。ウィンドウ構成のヒストリーはフレームごとに個別に保守される。

  • tab-bar-history-mode

タブを使う人向け。タブはそれぞれ独自のヒストリースタックをもつ。コマンドはtab-bar-history-backtab-bar-history-forward

  • undelete-frame-modeおよびtab-undo

しょっちゅうフレームやタブを作成、削除している人向け。間違えてフレームを閉じてしまったらundelete-frame (C-x 5 u)、タブならtab-undo(C-x t u)だ。

これらのオプションは前の配置に戻す前に選択されているウィンドウを一時的に最大化したい等の寄り道をしたい場合に役に立つだろう。

たとえばEmacsが間違った場所へのウィンドウのポップアップしたり分割したウィンドウを勝手にリサイズするなどして、手作業で慎重に配置したウィンドウが目茶苦茶にされた際の応急処置としても、winner-modeとその仲間たちが推奨されることがよくある。これはアンチパターンのように思う。winner-undo(または同等機能)を使ってEmacsの振る舞いを毎回修正している自分に気づいたのなら、そもそもEmacsが間違った場所にバッファーを表示するのが問題であって、つまりそれは苛立たしいデフォルトが原因なのだ。モグラ叩き問題を参照して欲しい。

深堀り

食欲が湧いてきたら、いよいよメインコースに取り掛かろう。ここまでで上手く機能すると判ったツールにたいする微調整、カスタマイズ、そしてバリエーションについてだ。

Emacsにイライラさせられるのには2つの段階があるように思える。最初は馴染みのないものばかり、キーバインディングと用語が判りにくい、他のソフトウェアと同じように機能するものが何もないのであなたはイライラする。パッケージのインストールによって欠点と判断した部分の修正を試みると、意味不明の暗号のようなエラーに見舞われる。シングルスレッド故に、処理速度をとんでもなく遅くするようなミスを犯すのはあまりに容易だ。ガベージコレクタは最悪のタイミングで発動する。上手くいくはずの事が上手くいかない。Emacsの欠点と思われる部分にイライラさせられる。ウィンドウ管理がこんなに複雑であっていい訳がない!

時を経て(数年? 数十年?)、あなたは内部で何が起こっているかについて、より改良されたメンタルモデルを育むかもしれない。Emacsのイベントループがどのように機能するか、バッファーやウィンドウ、キーマップ、テキストプロパティやオーバーレイ、すなわちEmacsを構築するデータ構造についてのメンタルモデルだ。ぎこちなく動く巨大な怪物(再表示のことだ)を密かに盗み見することさえあるかもしれない(訳注: 再表示、つまりredisplay機能はEmacsとともに配布されるデバッグ用の"etc/DEBUG"で個別に説明される程のトピックです)。 Elispの一般的なイディオムやマクロ、一般的な罠についての知見も深まった。そして遂にEmacsの実際の欠点であるAPIにイライラするのだ。ウィンドウ管理がこんなに複雑であっていい訳がない!

おっと、失礼。

…ということでわたしたちは今ここに集った訳だ。この記事の残りの部分は、上述した2段階のイライラの間のどこかにいる人を対象に記述した。ほとんどわたしが提案したアイデアではあるが、それらの多くは互いに排他であり、ウィンドウを処理する独自のアイデアをあなたに与えるかもしれない。これらのアイデアの実装には若干の調整が必要であり、コードをそのままコピペしてもあなたの期待する結果は得られないかもしれない。これらの理由により、Emacs初心者の人はもう少しEmacsを習得した後にここに戻ってくることをお勧めする。

back-and-forth手法

提案: 素早いウィンドウ選択

観察: スクリーン上に同時に何個のウィンドウがあり、何個必要なのかとは関係なく、必要なのはそれらのうちの2つの間を切り替えることだけという場合がほとんどだ。コードとREPLの組み合わせ、コードとgrep (検索結果)の組み合わせ、散文とノートの組み合わせも例に含まれる。リストとアイテムパターンはプログラミングや散文に属さない例だ。これには展開された入力ウィンドウを備えたカレンダーやアジェンダのウィンドウ、電子メールの受信ボックスとオープンされた電子メールが含まれる。

これら以外のスクリーン上のウィンドウは、通常は役に立つ情報(ドキュメント、デバッグ情報、メッセージ、ログ、コマンド出力、目次、ファイルマネージャー、ドキュメントのプレビュー)を表示していて、頻繁に見ることはあっても切り替えることは滅多にないだろう。

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

しかしこのアイデアに汎用性をもたせれば、任意のウィンドウペアー間を切り替えるコマンドを提供できるだろう:

window-back-and-forth.png

(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)

ウィンドウ間のback-and-forth、すなわち行き来において2つ目のウィンドウを選択する方法は重要ではない。マウスace-windowwinum、あるいは他の任意の手段を使ってもよい。その後はother-window-mruがサポートする。

other-windowの改善

other-windowの基本的なアイデア(同じ循環順でフレーム上のウィンドウ間を移動)を維持しつつ、よりDWIM13に合わせた順序に改善できる筈だ。

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

switchy-window-order.png

直感で判ったかもしれないが、もう1つは時計回りという空間的な順序ではなく、Alt-TABや一部ブラウザのタブ循環のように最後に使ったウィンドウ順で循環させるための変更だ。これは努力を払えば達成可能だがother-windowに代わるswitchy-windowを提供するswitchy-windowパッケージで行うことにしよう。

ウィンドウを循環する際に、switchy-windowはウィンドウが選択されている状態を数秒間待機してから、そのウィンドウを使用済みとマークして、もっとも最近使用したウィンドウ用の最新リストを更新する。実際にこれは非常にシームレスに機能する。switchy-windowを呼び出せば、移動する必要があったウィンドウに移動する場合がほとんどだ。

とは言ったものの、わたしが通常好んで使うのはback-and-forth手法に記した、よりシンプルなバージョンだが。

other-window-alternating

back-and-forthと言えばother-windowにはもう1つバリエーションが考えられる。最初聞いたときは混乱するかもしれないが、結果的にはDWIMの好事例だということが判るだろう。other-windowを連鎖させる場合を除き、呼び出しごとにウィンドウ切り替えの方向を反転させるのだ。ウィンドウが2つしかなければ違いは生じない。3つ以上ある場合には、たとえ循環順においてウィンドウが隣接していなくても、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-windowcompleting-readにおける文字列リスト、Avyにおいてはスクリーン上の文字に匹敵する。任意のウィンドウに任意のアクションを呼び出すための3ステッププロセス、その最初の2つのステップであるフィルター、選択としては理想的だ。

emacs-window-selection-pattern.png

Emacsウィンドウにとってのcompleting-readに匹敵するもの: aw-select

"ace-windowアクション"を定義して、aw-dispatch-alist14にバインディングを追加するという方法により、ace-windowは拡張されるべくデザインされた。これはウィンドウを受け取り、そのウィンドウを用いて何か有用なことを行う関数だ。そのエントリーポイントとして機能するのがace-windowコマンドである。

emacs-window-selection-via-ace.png

この制御フローは、Avyが機能する方法とほぼ同様。しかしcompleting-readの代替えとしては何か足りていない。コマンドではパターンを逆にして、ace-windowの選択方法を使いたいのだ。都合のいいことにaw-selectが行っているのが正にこれだ。

emacs-window-selection-ace-inside.png

基本的なパターンは極めてシンプル。(aw-select nil)15呼び出しにより選択したウィンドウがリターンされる。このウィンドウをタスク用に使用できるのだ。

scroll-other-windowがスクロールするウィンドウのセットは、このようなタスクの一例だ。他にもあるがまだ試さないで欲しい! 以下ではこのアイデアをさらに汎用化していこう。

tear-off-windowとtab-window-detach

Emacsのインタラクティブ(対話的)なウィンドウコマンドは、すべてカレントウィンドウに作用する。

ここではwindow-prefix-map (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))))

もちろんこれを行うためにアクションそれぞれにたいして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)

以下のデモでは、ウィンドウ選択の前にアクションを実行している点を除けば、ace-windowと同じように見えるだろう。ここでのwinはアクションである。シンプルなコマンドであれば何でも機能する筈だ。aw-dispatch-alistにアクションを事前に設定する必要はない。セットアップや記憶しておく必要もないのだ。デモではace-window-run-commandを使って、C-x -(shrink-window-if-larger-than-bufferという何とも説明的な名前の関数)で選択されていないウィンドウを縮小している。

ビデオ実況
  1. 行を点滅させてどのウィンドウがアクティブなのか確認

  2. ace-window-one-actionを呼び出して左上隅のOccurバッファーを選択、何か1つコマンドを実行するまでEmacsは待機

  3. C-x -shrink-window-if-larger-than-bufferを実行してOccurバッファーを縮小、カーソル位置とウィンドウには変化なし

ace-window-one-commandは別のウィンドウで任意のコマンドを素早く実行するのに役に立つ。このアイデアについてはで検討しよう。

Embarkは?

Emacs(やace-window)のアクションして選択という通常のパラダイムの逆転こそEmbarkのハートであり、わたしもways to use Embarkという記事で触れたことがある。もちろん、この"オブジェクト·ファースト"とも言えるアプローチは1つの見え方に過ぎない。Embarkにはたくさんのハートがあるのだ。

ace-window用のwindow-prefixコマンド

other-window-prefixシステムother-windowコマンドと同様に役に立つが、同じ問題点も抱えている:

選択するウィンドウに厳格な循環順を強制する。一貫して期待できるのは、アクティブなウィンドウが次のコマンドに渡されないことだけだ。もっと上手い方法がある筈だ。

より制御に優れた特別仕立ての解決策を与えてくれるのがaw-selectだ。次のコマンドがウィンドウへのバッファー表示を伴うのなら、わたしたちが選択するウィンドウを使う筈だ。以下の例ではmanページを表示するウィンドウを明示的に選択している。"次のウィンドウ"の位置が望ましくないからだ:

ビデオ実況
  1. 行を点滅させてどのウィンドウがアクティブなのか確認(左下)

  2. ace-window-dispatch (C-x 4 o)、その後にM-x manを実行してcurl(1)を選択、Emacsは何か1つウィンドウを選択するまで待機

  3. "e"で右のウィンドウを選択、manページがそのウィンドウで表示される

elispライブラリーのManは、実際にはどこに表示すべきかをカスタマイズするための一連のオプションを提供していることに注意。このEmacsのすべての物事に共通の扱いにくい作業全体を回避したことになる。

以下の例は、たくさんのウィンドウがひしめくフレームでForgeリンクを閲覧する上述の例を流用する。ランダムなウィンドウが選択されるother-window-prefix、特定のウィンドウを選択できるace-window-prefix、両者の違いを比較する。

ビデオ実況

このフレームでForgeのtopicウィンドウ(下のウィンドウ)からみて"次"のウィンドウは左上のウィンドウだ。

  1. 最後にリストされているトピックまで移動して行点滅(アクティブなウィンドウを確認)

  2. other-window-prefix (C-x 4 4) 呼び出して"リンク"の上でRETを押下、左上のウィンドウでオープンするがこのウィンドウは望ましくない

  3. ウィンドウ構成をリストアするためにtab-bar-history-backを呼び出す

  4. 今度はace-window-prefix (C-x 4 o)を呼び出してRETを押下、結果バッファーを表示するウィンドウが選択されるまでEmacsは待機

  5. "r"で右のウィンドウを選択、Forgeはそのウィンドウでリンクの内容を表示する

ace-windowは可視なフレームを跨いで使用できるので、スクリーン上の任意のEmacsウィンドウを選択できる。さらに素晴らしいのは、オンザフライでウィンドウを新たに作成して、そのウィンドウを使うこともできることだ。以下では次のコマンドが使用するウィンドウの作成にace-windowを使用している:

ビデオ実況

Orgモードのリンクをオープンすると、普通だとOrgの設定次第でカレントウィンドウか次のウィンドウでオープンされる。わたしたちは違う選択をしよう。

  1. リンク上でRETを押下してイメージを次のウィンドウでオープン

  2. qを押下して閉じてOrgバッファーに戻る

  3. ace-window-prefixを呼び出してリンク上でRETを押下、リンクされたファイルを表示するウィンドウを選択するまでEmacsは待機

  4. ウィンドウの分割にはace-windowアクションを使用、アクションが終了するとそのウィンドウでリンクされたイメージが表示される

実際のところace-window-prefixの実装はother-window-prefixよりもシンプルだ:

(defun ace-window-prefix ()
  "次のコマンドのバッファーの表示に`ace-window'を使用する。
次のバッファーとはこのコマンド(ミニバッファーの読み込みは除外)の
直後に呼び出される次のコマンドが表示するバッファのこと。
バッファー表示前に新たにウィンドウを作成する。
`switch-to-buffer-obey-display-actions'が非nilなら
`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 "Use `ace-window' to display next command buffer..."))

⋆-window-prefix用のキーバインディングパターンを踏襲して、C-x 4 oにバインドしよう。

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

ウィンドウを切り替える必要があるのか?

ちょっと立ち止まって基本的な質問に答えて欲しい: そもそも何でウィンドウを切り替える必要が? 少々単純化して考えてみると答えは2つ、ただ2つの可能性だけに行き着く:

  • 切り替えてそこに留まる: 切り替え先のウィンドウである程度の"作業"(あらゆる形式のテキスト編集が含まれる)を永続的に行うためだろう。このイベントでは切り替え先のウィンドウが主要な作業エリアになる。

  • 切り替えてその後に戻る: そのウィンドウやウィンドウの内容にたいして短時間やり取りをするためだ。もしかしたら戻る前にスクロールしたり、何かテキストをコピーしたり、あるいはそのウィンドウを削除するかもしれない。このイベントではウィンドウは副次的な目的用の一時的な切り替え先だろう。

いずれのケースにおいてもウィンドウ切り替えはコストであって、目的ではない。理想的にはウィンドウ切り替えは編集プロセスの一環として自動的に発生するべきだろう。では何故この些細な雑用を主要なアクションである編集に"折り込んで"しまわないのだろうか?

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

Emacsにおいてナビゲーションに関するすべての操作は最終的にはAvyへと行きつく。テキストの編集(や選択)のためにウィンドウを切り替える場合には、スクリーン上の特定のポイントに移動することになる。そこにカーソルを移動するためにはウィンドウの切り替え、カーソルの正しい位置への移動という、2つのステップからなるプロセスが必要になる。このプロセスを単一のアクションに短縮するのがAvyだ。Avyはフレームをジャンプする位置からなる単一のプールとみなす。スクリーン上の任意の位置にジャンプする際には、ウィンドウを横断してシームレスな移動を行うのだ:

ビデオ実況
  1. avy-goto-char-timerを呼び出す

  2. "se"をタイプすると、"se"のマッチすべてにヒントが表示される(その中には"sentence"も含まれる)

  3. "sentence"に相当するgをタイプする。

少し考え方を切り替えれば、少なくともナビゲーションという目的においては、ウィンドウを別個のオブジェクトと考えることを完全に止めることができるだろう。(Emacsの可視なウィンドウとフレームを飛び越えて)すべての目的に、任意の文字を数回のキー入力すれば到達できるのだ。これがウィンドウを跨いでジャンプする唯一の方法という訳ではない。たとえばpop-global-markで(処理中に切り替えたウィンドウを横断して)スタート位置に戻ることができる。

ビデオ実況
  1. avy-goto-char-timerを呼び出す

  2. "demo"とタイプするとその文字列の候補は1つだけなので、Avyは別のウィンドウのその場所にジャンプする

  3. "jumpとタイプすると、"jump"にたいするすべてのマッチにヒントが表示される。

  4. マッチの1つを選択すると再びAvyはジャンプする(3つ目のウィンドウ)

  5. pop-global-mark (C-x C-SPC)を呼び出して前の場所にジャンプして戻る(詳細は以下参照)

  6. pop-global-mark (C-x C-SPC)をもう一度呼び出して前の場所にジャンプして戻る。

Avyがウィンドウを認識しない

Avyがウィンドウやフレームを跨いで移動しない場合には、多分avy-all-windowsをカスタマイズする必要がある。ついでにavy-styleのカスタマイズも検討して欲しい。Avyでジャンプする方法は1つではないのだから!

もちろんこれはAvyでできることの表面を少し引っ掻いただけだが、この時点における舗装としては十分ではないだろうか。

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

では別のケースだ。単一の論理的アクションを行うという理由でウィンドウを切り替えるのは珍しいことではない。メインバッファーに戻る前に、何かを見るためにisearchのようなアクションでフォーカスの移動する等の複合アクションかもしれない。これこそ切り替え→アクション→切り替えて戻るパターンのダンスだ。

わたしたちはこのダンスを自明かつ具体的な解決策から、反復可能で一般的な解決策、さらに最終的には抽象化された汎用性のある解決策へと段階的に自動化していこう。

まず最初は明白な点から: このダンスを繰り返し行っている自分に気づいたなら、キーボードマクロで自動化することが可能だ(読者の練習用に詳細は省く)。そのアクションがいつもあなたが行っているアクションである場合には、一歩進めて汎用目的のコマンドを記述できるだろう。上述したace-window-one-commandはこれを行う1つの方法だ。Emacsがわたしたちのために切り拓いてくれた道は…

scroll-other-window (built-in)

scroll-other-windowscroll-other-window-downが古くからEmacsの一部である理由は、Emacsのデフォルトセッティングである2-ウィンドウパラダイムに正しく準拠しているからだろう。あるウィンドウで編集する際にもう一方のウィンドウの内容をリファレンスとして使用するパラダイムのことだ。編集を行うウィンドウを離れずに、もう一方を上下にスクロールできる。これは任意の個数のウィンドウで機能することに注意。スクロールされるウィンドウは"次のウィンドウ"、すなわちカレントウィンドウから時計回りで次のウィンドウだ。以下の図では内枠線のあるウィンドウが選択されたウィンドウ、scroll-other-windowがスクロールするウィンドウには矢印がついている:

scroll-other-window.png

3つ以上ウィンドウがある場合に期待通り動作させるためのウィンドウ配置には、配慮が求められる。たとえば横並びの3つのバッファー(上図の1か3)では、2でscroll-other-windowすると3がスクロールされるので、1を参照しながら2と3で作業することはできない。しかしありがたいことに、わたしたちはスクロールするウィンドウを選択するルールを指定できるのだ。1つ目のオプションとしては

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

これは常に最近もっとも使用されていないウィンドウをスクロールする。リファレンスとして使用する1に頻繁に移動することはないだろうというのが理由だ。バッファー2と3を切り替えて使っているときにはバッファー1を無視して、scroll-other-windowでもう一方をスクロールしたいと思うかもしれない:

(setq other-window-scroll-default
      (lambda ()
        (or (get-mru-window nil nil 'not-this-one-dummy)
            (next-window)               ;fall back to next window
            (next-window nil nil 'visible))))

これはback-and-forth手法において極めて良好に機能する。

スクロールするウィンドウを設定する

別の方法でもスクロールするウィンドウを変更することができる。変数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-vC-M-S-v)の実行頻度は、あなたの修飾キーにたいする寛容さにもよるだろう。特にモーダルな入力メソッドを使用している場合には、リマップ候補として有力だと思われる。元々C-M-vESC C-vでも呼び出せるので、わたしはもう1つをESC M-vにバインドしている。

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

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

(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))

別の例としてpixel-scroll-precision-modeを通じて通常のページングコマンドをリバインドすれば、scroll-other-windowに別のウィンドウをスムーズにスクロールさせることができる:

isearch-other-window

リファレンスとして別のウィンドウでバッファーを表示するというアイデアを推し進めれば、scroll-other-windowを直接拡張して"次のウィンドウ"を検索させるという考えに行き着くだろう16。上述の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にしたがい、適切なウィンドウをリターンする関数だ。

以下はシェルとドキュメントでのisearch-other-windowの動作例だ:

ビデオ実況
  1. Curlコマンドの一部をタイプする

  2. Manバッファーを検索するためにisearch-other-window (ここではC-M-s)を呼び出す

  3. わたしたちが調べたいオプション--ssl revokeを検索する(このマッチングの特殊な振る舞いはisearch-whitespace-regexpの設定によるもの)

  4. RETを押下するとisearchは終了してシェルに戻る

  5. scroll-other-windowで別のウィンドウをスクロールしたら、hippie-expandを使って入力したい引数をタイプ入力する

C-M-sというキーはすでにisearch-forward-regexpにバインド済みだが、このコマンドを呼び出す方法は他にもたくさんある。isearch-forwardにプレフィックス引数を与える(C-u C-s)、あるいはisearchの途中でM-rでregexpに切り替える等だ。

他のウィンドウでのアクションの実行

elispから別のウィンドウに一時的に切り替える簡単な方法は2つある。(save-window-excursion (select-window somewin) ...)(with-selected-window somewin ...)だ。

わたしたちの目的に照らして比較しよう。前者は実行時のウィンドウ構成をリストアすることだ。これにはウィンドウからの相対的なバッファー位置やそのバッファーの(point)の値が含まれる。一方後者はフレーム全体にわたって変更を維持する。こちらがわたしたちの求めていたものだ。変更が永続的でなければ、この演習の意味がないではないか!

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

Emacsではバッファーなら幾らでももてるが、ウィンドウの方は一握りだけだ。実際のところ、これがウィンドウ管理における問題の元なのである。したがって包括的な解決策にはすべて、既存のウィンドウに表示されているバッファーを変更する必要がついてまわるのだ。ace-windowのディスパッチシステムは1つの解決策だろう。しかし別のウィンドウに表示されているバッファーの変更という、より容易な80点の別回答を提案するのが、ビルトインのコマンドnext-bufferprevious-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-bufferprevious-bufferを置き換えればよい。

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

そして最後はswitch-to-bufferのフォールバック版を定義して、これらすべてをrepeat-mapに放り込んでしまえばnpbで連続して呼び出せるだろう:

;; 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)

キーマップを頑張って、右上にキー説明も表示されるようにした。

ビデオ実況
  • my/next-buffermy/previous-bufferを呼び出す(わたしはnext-bufferのデフォルトのバインディングC-x \<right>はリマップせずに、C-x C-nC-x C-pにバインドした)

  • repeat-mapのbuffer-cycle-mapがアクティブになるのでnpでバッファーを循環させ続けられる

  • 他のキーを押下してrepeat-mapから抜け出す

  • プレフィックス引数とともにmy/next-buffer(C-u C-x C-n)を呼び出すと別ウィンドウでbuffer-cycle-mapがアクティブになり、npで別ウィンドウのバッファーを循環できる

  • repeat-mapアクティブ時にbを押下すると選択されたウィンドウでswitch-to-bufferが呼び出されるが、これは必要なウィンドウがバッファー履歴の1、2個先になかったときのフォールバックだ

別ウィンドウでのバッファー表示にbを用いているのは、ace-windowのディスパッチ版の動作と整合をとるためだ。

master-modeとscroll-all-mode

少し脱線 : Emacsはmaster-modeを提供している。ウィンドウを離れずに別のウィンドウでアクション行うための特別誂えの解決策である。あるバッファーをカレントバッファー("master")の"slave"バッファーに指定できるのだ。このモードはカレントウィンドウを離れずにslaveバッファーをスクロールするためのキーマップをオープンする。上述したother-window-scroll-defaultの方が透明性に勝り、即効性のある解決策なので、このモード自体は候補としては劣っている。しかしコマンドmaster-saysでこのキーマップを追加できる。これは伝声管のようなコマンドで、slaveバッファーで事前定義したアクションを行う助けになるだろう。たとえば以下はslaveバッファーを再センタリングするビルトインアクションだ:

(defun master-says-recenter (&optional arg)
  "slaveバッファーを再センタリングする
`recenter'を参照のこと"
  (interactive)
  (master-says 'recenter arg))

しかしこれはどんなアクションでも構わないのだ。すべてのプロジェクトにたいしてシェルバッファーやコンパイルバッファーをslaveバッファーにしてmaster-modeを使ってページ操作や最後の出力のコピー、コマンド送信等々を行うことができる。

スクロールに焦点を当てて考えると、scroll-all-modeならフレーム上のすべてのウィンドウのスクロールアクションをで結びつけることができる。ときには2つ以上のウィンドウのビューの同期を保たせていたい場合もあるだろう。アクティブなウィンドウをスクロールしてから別のウィンドウをスクロールするより、こちらのモードの方が手軽ではないだろうか。

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つ、上端にタブバー(そしてタブごとにウィンドウ)、左側のサイドバーにはディレクトリーやコンテンツ、右側にはオプションの安っぽいお飾り、下端には端末エミュレータ。

modern-editor-layouts.jpeg

すべてのエディターが回覧を受け取ったな… だがEmacs…お前は別だ、みたいだね。このウィンドウレイアウトとワークフローをEmacsで再現することは可能だ。ところで他のものでも何でも構わないが、怒り狂うがごとくウィンドウ管理に励む我々にこそ、より基本的な疑問を問う必要がある: 何で2つ以上のウィンドウが必要なの?

この考え方には幾らかのメリットがある。ウィンドウ切り替えではなくバッファーを切り替えれば、1つのバッファーにスクリーンを使用できる。ウィンドウのリサイズ、調べものや通常の編集でポップアップする(ドキュメントウィンドウのような)すべてのものは、通常はqを押下すれば閉じることができるだろう。ファイルブラウザのような特別なバッファーには、dired-jumpのような専用コマンド経由でアクセスできる。

同時に2つのウィンドウという要件を緩和して、2つ目のウィンドウを生きたリファレンスとして運用するのだ。そうすれば手間のかからない操作のほとんどはそのまま残したまま、増えた利点だけを享受できるだろう。その証拠がscroll-other-windowや他のコマンドだ。Emacsはデフォルトでこのような使い方をするように元々セットアップされているのである。Emacsには上からのお達しで押し付けられた厳格なレイアウトは存在しない。そちらであれば無秩序なカオス、雑草のごとくポップアップするウィンドウも存在しないだろうが。

そして実のところ、はわたしたちは1周回ってThe Zen of Buffer Displayに戻ったことになる。たしたちが拒絶する原因は、Emacsがわたしたちにウィンドウ管理の自由を与えたせいだというのは何とも皮肉な話しだ。スクリーン上に同時に何個のウィンドウをもちたいとか気にせず、ウィンドウを何とかしようとする試みすべてを止めて問題を回避する手もある。

そこでウィンドウ管理についてさらに2つのストラテジーを紹介しよう。どちらもウィンドウの取り扱いが最小限のストラテジーだ。1つ目はもっとも緩い基準としての"ウィンドウ管理"を行う方法について説明しよう:

ウィンドウが作られたら無視する

Window-agnostic jumping with Avyは汎用的なアイデアにおける特殊ケースだ。Emacsを使う際にわたしたちが主に関心をもつのはテキストである。テキストコンテナとして考えたとき、ウィンドウは不要な抽象化なのかもしれない。これがxref-find-definitionsによる定義へのジャンプのように、目標がスクリーンコンテンツ外部にあれば自然な枠組みと言えるだろう。

しかし他にもこのウィンドウ不価値論を適用する方法がいくつか存在する。ジャンプ前の場所を追跡するmark-ringglobal-mark-ringでは、それぞれpop-to-mark-command (C-u C-SPC)とpop-global-mark (C-x C-SPC)で元の場所にジャンプして戻ることができる。後者の方は必要に応じてウィンドウを跨いでジャンプしてくれるだろう。dogearsのようなパッケージなら、優れたUIでステップの再トレースをよりきめ細かく制御してくれる筈だ。

pop-to-bufferでウィンドウを跨いでジャンプする

pop-global-markはデフォルトでは常にカレントウィンドウのバッファーを切り替える。これをウィンドウ切り替えにも使いたいものだが、それには若干のadviceが必要だ:

(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)がある。前に触れたと思うが、これにはウィンドウが切り替えるという副次的な効果がある。

これらの方法の中間にも、ユーザーやEmacsにとって意味をもつ場所にウィンドウを跨いで移動するためのオプションがEmacsにはたくさん存在する。これらのオプションはEmacsウィンドウの単一ウィンドウにおいても、21世紀的なレイアウトをもつ標準的なIDEと同じことを行うことができるのだ。

ウィンドウの世話をしなくてよいようにウィンドウを取り扱う

: モグラ叩き問題を何とかする

これはEmacsウィンドウにまつわる手作業のアクションに関する記事なので通り過ごす訳にはいかない。display-buffer-alistとウィンドウの自動的な振る舞いについては記事のどこかで触れておく必要があるだろう。アイデアはシンプルだ。elispのコードがバッファーを表示しようとする度に、この変数のルールリスト照らしてバッファーが表示される。このリストに一致したバッファーのエントリーには、バッファーが如何に表示されるべきかが指定されている。

日常的なEmacsの使用において目にするすべての種類のバッファーにたいしてウィンドウのサイズ、位置、役割り、フォーカスといったルールをセットアップすればウィンドウ管理のほとんどは片付くだろうが…おわかり頂けただろうか?

そう正に今、わたしたちの強いあこがれに現実が忍び寄ってきた。display-buffer-alistの問題は機能しないことではないのだ。あまりに多くの作業を要するのが問題なのだ。バッファーを表示するルールを作成するには、ほとんどのユーザーにとっては十分な理解のレベルを遥かに超えるバッファー、さらに多くの述語、ウィンドウタイプやスロット、display-bufferアクション関数、ウィンドウパラメーター、ちんぷんかんぷんなたくさんの用語といったEmacs APIに大量に存在するAPIの側面にたいする理解が要求されるだろう。そしてelispマニュアルの探窟を終えた今となっては、"わたしのウィンドウ配置を乱さないでください"のようにシンプルな意図を容易に表す術はもはや残されていない17。これはパッケージの作者がウィンドウの自動的な振る舞いを指定することで、より親しみやすいインターフェイスを自分のパッケージに提供することを主目的としたツールなのである。

しかし年代記の精神に則れば、このトピックを手ぶらで去る訳にはいかない。

  • Shackledisplay-buffer-alistの奇妙な点を克服して、シンプルなウィンドウルールを指定するためのelispインターフェイスを提供するパッケージだ。いつもウィンドウ配置を台無しにして、あなたがwinner-undoに手を伸ばす原因となっている厄介なバッファータイプは、柵で囲ってしまうのが最善手だ

  • Emacsのディストリビューションには通常は、これらの設定を指定するためのインターフェイスを提供している。いずれかを使っているならサポートされているだろう18

Popper、Popwin、shell-pop、vterm-toggle

ウィンドウが散らかっていない最小限のワークスペースという理想を目指す我々ではあるが、他にも役に立つツールとしてポップアップマネージャーがある。

すべてのバッファーが同じように作成される訳ではないという観察に基づくEmacsパッケージがPopwinPopperだ。バッファーにはわたしたちが時間のほとんどを過ごすバッファー(プライマリーバッファー)があり、リファレンスとして用いたりドキュメントの参照、シェルコマンドの実行、タスクやコンパイルの状態チェック、検索結果へのアクセス、メッセージの読み取り等々で一時的にアクセスしたいバッファー(ポップアップバッファー)が存在するのだ。display-buffer-alistや同等の手法を使うことで、これらのバッファーにもっと小さく補助的なウィンドウを使わせて、表示時にカーソルを奪わないようにすることができる。しかしアクセス問題の解決にはならない。わたしたちが求めているのはこれらのポップアップバッファーをキー1つで召喚して閉じたり、循環させたり、強制的に閉じる簡単な方法なのだ。

Popperはどんな種類のバッファーにたいしても、あなたがポップアップさせたいバッファーを選択(事前に選ぶことも可能)すると、必要に応じてこれらの補助的なウィンドウのオープンやクローズを行うことで、1つ(あるいは2つ)のウィンドウというパラダイムにたいするあなたの強迫観念を支援してくれる。以下のイメージは、その時点で利用可能なポップアップをタブラインとして表示しているところ。これらのポップアップへのアクセスや循環は1つのキーで行うことができる:

popper-tab-line-demo.png

このアイデアより古く、より包括的に実装されているのがPopwinだ。アクセス用のクイックキー、独自にカスタムメイドされたdisplay-buffer構成(あなたの好みにはそぐわないかもしれない)が付属している。キーアクセス1回でシェルバッファーの召喚や終了を行いたい場合には、必要なのはshell-popvterm-toggleかもしれない。

未解決事項

最後は存在して然るべきウィンドウ管理用のオプション話題で締めよう… 存在はしないが。

window-tree

Emacsによるウィンドウの表現方法と、わたしたちがこれまで議論してきたアプローチによるウィンドウ操作の間には、根本的とも言える断絶が存在する。

Emacsのフレームにおけるウィンドウはツリーとして表されている。leafノード、すなわち葉ノードは"生きた(実際の)"ウィンドウ、残りは"内部的(仮想的)"なinternalノードだ19

emacs-window-tree-illustration.png

ウィンドウ操作においてほとんどのユーザーが直面するother-windowやWindmoveのような機能によるウィンドウ間の移動は、ウィンドウの空間的な位置を調べることによって機能するので、このツリー構造は無視される。これはウィンドウの分割や削除の際に予期せぬ直感に反する振る舞いを引き起こしたり、作成可能な分割にたいして意味不明な制約が課せられることが多々ある。たとえば以下のような変換を行う手段は存在しない

emacs-window-tree-deficit.png

ここで分割する必要があるウィンドウはフレームのルートではないし、leafウィンドウでもない。ツリーの中のinternalノードのうちのどれかだ。

window-treeの操作用にコマンドを追加することによって、新たな可能性に通じるたくさんのドアが開くだろう。分割、入れ替え、反転等々のフレーム変換は、window-tree上の枝にたいする初歩的な操作と言える。複数ウィンドウの選択はinternalウィンドウを"選択"すれば可能だ。そうすればウィンドウ構成の部分的な操作によって他のタブやフレームへの引き渡し、複製や永続化も可能になるだろう。ツリーの枝がdisplay-bufferや同類の関数20によって目茶苦茶にされることが防がれるし、柔軟で制約が緩い動作を許容することで、フレームの一部を1つのタスク専用にすることもできるのだ。

この仮説に過ぎないwintree、あなたが記述してみるというのはどうだろう?

  1. Elispはウィンドウツリーを問い合わせる関数をすでに提供済みだ: ツリー自体をリターンするのはwindow-treeframe-root-windowはツリーのルート、window-parentwindow-childwindow-*-siblingはあなたが期待する通りの動作をするだろう

  2. walk-window-treewalk-windowsを通じたツリーの横断がサポートされている

  3. 通常の方法で生きたウィンドウの分割や削除を行う方法以外は、ツリーを変更するための初歩的な関数が存在しない。

  4. internalウィンドウの"選択"という概念がないので、おそらくはサブツリー(部分木)のウィンドウそれぞれの内側に、UIを通じてボーダーを追加してシミュレートする必要があるだろう。

こうして必要な要素は提示された。あと不足している要素は"window.el"に足を踏み入れて自らの手を汚してやろうという、やる気に満ちたEmacsユーザー(あなたかも)だけだ!

タイル式ウィンドウマネージャー統合

Emacsのウィンドウツリーモデルは、i3やbspwmのような手動のタイル式ウィンドウマネージャーとほぼ等しいが、i3のタブ付きウィンドウ環境が及ぼす力のようなものに欠けている。これは次のような自然な疑問へとつながる。何故にタイル式ウィンドウマネージャーの中でタイル式マネージャーを使うのか?21 あなたがi3やbspwm、あるいはEmacs内部でtmuxを使っているなら、同じキーバインディングで両者をシームレスに操作したいと思うのは自然だ。これを行うためのEmacsパッケージはいくつか存在する。Pavel Korytovのi3-integrationがあるし、わたしも qtile用にハックしたことがある。しかしこれを行うためにより明解で統一されたインターフェイスをEmacsが提供できれば、すべてのウィンドウマネージャーの統合が遥かに容易になり得る。繰り返そう。必要な要素は既に提示されている:

  1. ウィンドウマネージャーはアクティブなウィルスクラスを識別する何らかの方法、ウィンドウ間を跨いだ移動したりウィンドウを扱うプログラム的な手段をを提供する必要がある。これはシェルコマンド、socketまたはサーバーベースのIPC、(Linuxなら)D-Busメソッドを通じて行うことができるだろう。ほとんどのウィンドウマネージャー、端末多重化アプリを網羅できる筈だ

  2. Emacs側では通信手法に依存しない、ほとんどのウィンドウマネージャーのウィンドウ操作を模倣した操作の共通サブセット、あるいは(より野心的には)それらを統合した操作をサポートするインターフェイスが必要だ。

  3. ウィンドウマネージャーでウィンドウが切り替わったらアクティブなウィンドウがEmacsかチェックしいぇ、必要に応じてウィンドウマネージャーに制御を渡す。

これも前と同じ。不足している要素はあなただ!

ここからの眺め

信じる信じないは自由だが、これが短いバージョンだ。記事の範囲を抑えるためにtab-line-mode関連すべて、atomicウィンドウや専用ウィンドウ(dedicated window)、サイドウィンドウといったいくつかのウィンドウストラテジーは除外せざるを得なかった。そしてわたしたち全員にとって、display-bufferにまつわる問題は避けておく方が無難だ。

わたしたちは何処に辿り着いたのだろう? 手に残されたのはウィンドウの切り替え、移動、飛び回り方、作成、削除、さもなくばウィンドウの取り扱いやウィンドウ構成に関する数十の方法、コマンド呼び出しでオンザフライでウィンドウ表現を制御する多くの手法、ウィンドウを跨いで作業する半ダースの方法、さらにはウィンドウ管理について完全に思考を停止することについてだ。繰り返しておくがこれはわたしがEmacsでウィンドウに悪戦苦闘してきた経験によって脚色、制限されたコレクションである。もっとシンプルだったり役に立つものを見逃していたら、是非教えて欲しい!

Emacsにおけるウィンドウ管理は良くも悪しくも、それほど複雑ではなく制限もない。素材といくつかの手順はEmacsによって提供されており、その素材自体も基本的な食事として耐え得るものだ。

でも少し調理すれば美味しい何かができるかもしれない。Bon appétit(さあ、たんとお食べ)

更新と修正

修正、提案をしてくださった以下の方々に感謝いたします:

  • winner-modeがEmacs フレームごとにウィンドウ構成履歴を個別に保守しているので、複数フレームの使用時にも値が保たれることを指摘してくれたJD Smith氏に。

  • pop-global-markがデフォルトではEmacsウィンドウを跨いで機能しないので少しadviceする必要があることを思い出させてくれたGrant Rosson氏に。

  • winum-keymapの間違いを指摘してくれたu/simplex5d氏に。

  • スクリーン上のウィンドウが2つ以下の際には
    ace-windowアクションが利用可能になるようにaw-dispatch-alwaysをセットする必要があることを指摘してくれたkotatsuyaki氏に。

  1. Emacsにとっての不幸は他のエディターが実装済みの優れたアイデアが、現在の設計指針の埒外にあることだろう。例として4corderのyeetsheetDion systemのviewが挙げられる。これは他の複数のバッファーの"生きた"部分文字列を内容としてもつバッファーという概念であり、バッファーを組み合わせたり一致させることができる。一方、Emacsでもつことができるのはインダイレクトバッファー、すなわちバッファー全体の"生きた"コピーである。

  2. Emacsのユーザーマニュアルの話しではない。elispを使いこなす開発者向けのマニュアルでさえこの記述なのだ!

  3. 簡単で恐らくは不正確な説明をお詫びする。これらを使った経験は少ししかないのだ。

  4. ACME editorは特筆すべき例外かもしれない。

  5. これらのウィンドウマネージャーを何個か試した経験があれば、ここは飛ばしてしまっても大丈夫だ。深堀りへ進もう。

  6. もう一度言おう。evil-modeはWindmoveを使ってこれを行っている。

  7. これは特にorg-modeのコマンドが該当する。ありがたいことにゆっくりとではOrgの状況も改善されつつある。

  8. デザインが似ているのは偶然ではない。どちらも作者はOleh Krehelだ。

  9. このアイデアの詳細については、Fifteen ways to use Embarkを参照して欲しい。

  10. context-menu-modeを調べて欲しい。話しをウィンドウ管理に限らなければ、Emacsのメニューバーを介した発見性は驚くほど良好である。

  11. レジスターとは多くの種類を保持できる名前付きのバケツのことだ。レジスターはそれぞれ(aからzのような)文字が割り振られていて、プレフィックスC-x rでレジスターの操作ができる。

  12. これはorg-babel-switch-to-sessionを介してOrg-babelブロックでも機能する。org-babel-mapではC-c C-v C-zという若干異なるキーにバインドされる。

  13. Do-What-I-Mean

  14. 上記の図のように事前定義されたいくつかのアクションとともに配布されている。

  15. aw-selectの引数は選択プロセス中にモードラインにメッセージを追加するためだが、わたしたちにとっては十分ではない。

  16. isearchは素晴らしいナビゲーションツールだ。

  17. display-bufferを過度に設定したりオーバーライドすることは可能だが、数十に及ぶ境界条件や予期せぬ挙動がもたらされるだろう。

  18. Doom Emacsはこれを行うためにset-popup-rule!という便利なコマンドを提供している。

  19. 技術的にはミニバッファーはこのツリーに属さないものの、このツリーを辿って到達することは可能だ(window-tree関数を参照のこと)。

  20. この全か無かの如きウィンドウ動作は、現在のところElispのatomicウィンドウAPI経由で有効になってはいるものの非常に制約のあるアプローチだ。

  21. よう、タイル式が好きらしいな…

8
5
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
8
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?