e2wm の設計と実装

はじめに

e2wm.elのソース読んだり、自分流に拡張するための参考になるように、e2wmの設計と実装について簡単に説明します。

あらすじ
  • 全体の構成の説明
  • 各モジュールの説明
  • 各イベント時の動きの説明
  • 現在の実装と今後の予定

全体の構成

まず、全体構成の図を示します。



全体の構成図

全体的な流れとして、イベントや画面の変化などをパースペクティブ管理に集めてきて、そこでパースペクティブに応じた加工を行い、ウインドウレイアウト制御のライブラリで画面に描画するという形です。

モジュール間の依存関係は次の図のようです。矢印の先が依存先です。



モジュール間の大まかな依存関係

パースペクティブ管理や履歴の情報とウインドウ制御の機能を、各モジュールが参照して使っているというようなイメージになります。

e2wm.elのソースコード命名も、大体このような分類で並んでいます。

各モジュール説明

各モジュールについて大まかな役割を説明します。

ウインドウレイアウト制御

window-layout.el によって、実際のウインドウの制御やバッファの切り替えを行います。
どんな機能があるのかイメージがつかみやすいように、主なAPIを列挙してみます。

○レイアウト構築

  • wlf:layout / wlf:no-layout
    • ウインドウレイアウトのレシピに従って現在フレームのレイアウトを行い、レイアウト制御に必要なwm(Window set Manager)オブジェクトを返します。
    • ウインドウの構築に必要なレシピやパラメーターリストについては window-layout.elのソース を参照してください。(今後ここの機能を改めて紹介するかもしれません)
    • no-layoutの方は、実際のレイアウトをせずに、単純にwmオブジェクトを返します。 wlf:refresh などでレイアウトを変更したりするときに使います。

○ウインドウのコントロール

  • wlf:show / wlf:hide / wlf:toggle
    • 指定したウインドウを表示したり、非表示にしたり、トグル(表示・非表示の切り替え)を行います。
  • wlf:select
    • 指定したウインドウにフォーカスを移動します。
  • wlf:toggle-maximize
    • 指定したウインドウ以外を消して最大化したり、元に戻したりします。
  • wlf:refresh
    • ウインドウを再レイアウトします。

○アクセス

  • wlf:get-buffer / wlf:set-buffer
    • 指定したウインドウ内に表示されているバッファオブジェクトを取得、設定します。
  • wlf:get-window
    • 指定したウインドウのオブジェクトを返します。ただし、ウインドウのレイアウトが更新されたり、 set-window-configuration などでウインドウのレイアウトが変更されると、無効なオブジェクトになります。一時的に使うのみにして保持しないようにします。
  • wlf:wset-winfo-list (defstructで定義される)
    • wmオブジェクトの中のwinfo構造体のリストを返す。ウインドウでループしたいときに使います。(扱いとしては内部用なので将来変更されるかも。)

e2wmではこれ以外にも細かい関数を使っています。たぶん後でe2wm用のAPIを定義して整理するかもしれません。

バッファ履歴管理

編集対象のバッファの履歴を管理します。編集対象かどうかは e2wm:c-recordable-buffer-p の関数で判断していますので、この関数を変更することで履歴に載せたいバッファをカスタマイズできます。

また、バッファ履歴の最大数を e2wm:c-max-history-num で定義しています。デフォルトは20なので、もっと大量にバッファを開きたい人は、ここを1000ぐらいにしても良いかもしれません。

パースペクティブプラグインのなどのモジュールから履歴を扱う場合は、このモジュールの関数を使うことになります。

履歴の順序についてですが、基本的に LRU(Least Recently Used) で順序を決定しています。いろいろと試行錯誤して、自分的に使いやすく調整した結果、以下のようなルールでバッファ履歴の順序を決定しています。

  • switch-to-buffer, pop-to-buffer で表示しようとした編集対象バッファを先頭に持ってくる
    • →開いたバッファが上に来る。これは自然。
  • それ以外のバッファについては buffer-display-time の時間を見て最近順に並べる
    • →バッファを表示する方法がいろいろあるので、表示イベントよりは表示時間を使う方が確実そう。
  • history-back, history-forward 、またパースペクティブの中で表示した場合は、履歴を変更しない
    • →履歴を行ったり来たりして編集する場合は、順番が変わって欲しくない。

かなり独自なルールです。表示させたいバッファの好みや履歴に関しては、エディタの使い方のスタイルとも直結すると思いますので、これが万人に使いやすいとは言えないかもしれません。並び替えのルールはいくつかのパターンを定義したり、カスタマイズの仕組みを組み込んだり出来ると便利かなと考えています。

バッファ・ウインドウ表示制御

Emacsがもともと持っているウインドウ制御の仕組みを乗っ取る部分です。以下の関数やフックを乗っ取って、パースペクティブフレームワークに処理を任せるようにします。

○advice

  • buffer系
    • switch-to-buffer, pop-to-buffer
  • window-configuration系
    • current-window-configuration
    • window-configuration-frame
    • compare-window-configurations
    • set-window-configuration
    • window-configuration-p

○hook / 関数

    • kill-buffer-hook
    • window-configuration-change-hook
    • completion-setup-hook
    • special-display-function

かなり広範囲に乗っ取っていますので、相性の悪い拡張があると思います。

もともとe2wmが成立できるかどうかは、Emacsのウインドウ制御を完全に乗っ取れるかどうかにかかっていました。Emacsや拡張のドキュメントやソースを読みながら効率的な乗っ取り方法を調べていたのですが、結局手当たり次第に実験しながらその都度対応していってみないと分からないという結論に至りました。デバッガで追えない動きについては、 e2wm:debug 周辺のログの仕組みでトレースを取りながら調査しました。

この乗っ取りにより、switch-to-bufferやpop-to-bufferなどの高レベルな関数を使ってバッファを表示している場合は大体うまくいくようになりました。ただ、補完候補バッファなどのEmacsの実装が管理しているバッファの表示については、一部うまく乗っ取れないことがあります。このあたりの試行錯誤や現状の対応については、少しコードのコメントに残してあります。

一方で、低レベルなバッファ表示関数のset-window-bufferや、自前でウインドウのsplitやレイアウトの制御をしているような場合(例えばwanderlustやlookupなど)は、そのままではe2wmと合わないことが多いです。低レベル関数まで乗っ取るのは大変なので、何とかうまい対応方法を考えたいと思います。

ということで、導入している拡張や環境によっては動きが微妙なところがあると思います。機能的な競合が起きない限り、なるべく対応していきたいと思っていますので、状況を教えていただければ可能な限り調べてみます。

パースペクティブ管理

パースペクティブを切り替えたり、現在選択中のパースペクティブに処理を委譲するなど、パースペクティブに関する情報や処理を集中管理するモジュールです。

パースペクティブの情報の管理については、e2wm:$pst-class と e2wm:$pst の2つの構造体を使っています。e2wm:$pst-class 構造体がパースペクティブの「クラス」で、 e2wm:$pst 構造体がパースペクティブインスタンスのような関係になっています。パースペクティブを管理するのに必要な値はフレームのプロパティに保持します。この関係を簡単に図に表すと以下図のようです。



フレームとパースペクティブ関連オブジェクトの関係

中心になるAPIは以下のようです。

e2wm:managed-p
現在のフレームがe2wmの管理対象状態である場合は t
e2wm:pst-get-instance
現在のフレームを管理しているパースペクティブインスタンスオブジェクトを返す
e2wm:pst-get-wm
現在のフレームのレイアウトを管理しているwmオブジェクトを返す
e2wm:pst-change
パースペクティブを切り替える
e2wm:pst-update-windows
ウインドウのレイアウト、バッファの表示、プラグインの実行を行う

e2wm自体をいじる場合は、このあたりを中心に見ていく感じになるかなと思います。

パースペクティブの主な役割は以下の定義です。

e2wm:$pst-class 構造体の主なものを以下に示します。各イベント時の具体的な動きについては後ほど説明します。

name / title
このパースペクティブの名前(シンボル)とタイトル(人が読む用)
extend
このパースペクティブの継承元名。このクラスの以下スロットが nil だったら継承元の定義を使う。
init
このパースペクティブのコンストラクタ。
start / leave
このパースペクティブの開始・終了時に呼ばれる関数。レイアウトや必要なフックなどのセットアップ、後片付けを行う。
update
各windowの内容を更新する時に呼ばれる関数。ウインドウの構成の変更や履歴を戻ったりするたびに呼ばれる。
switch / popup
switch-to-buffer, pop-to-buffer, special-display-func を乗っ取る関数。ここで表示すべきウインドウを決定する。
keymap
このパースペクティブで有効にするキーマップ。
save
after-save-hook で呼ばれる。

元になるパースペクティブから一部をカスタマイズしたパースペクティブを効率よく作れるように、パースペクティブクラスに継承の仕組みがあります。

e2wmのキーバインドは、すべてのパースペクティブ全体で共通なキーマップと、パースペクティブ固有のキーマップをあわせたものになっています。具体的には、パースペクティブ共通の e2wm:pst-minor-mode-keymap をクローンして、パースペクティブ固有のキーマップをそこに上書きしたものを、毎回 minor-mode-map-alist の e2wm の項目に上書きしています。

プラグイン管理

画面アップデートの際に、各ウインドウに設定されているプラグイン関数を実行して回ります。主にパースペクティブフレームワークの e2wm:pst-update-windows から呼ばれます。

プラグイン関数は、指定されたウインドウに適当なバッファを設定するように定義します。wmオブジェクトやウインドウオブジェクトが渡ってきますので、履歴などにアクセスしながら、バッファを更新したり、切り替えたりします。また、このタイミング以外でもバッファを更新してもかまいませんので、バッファ内のイベントで更新したり、非同期で何かを行って後で更新するような動きも可能です。

プラグインの定義に必要なのは、名前(シンボル、人が読む用)とプラグイン関数ひとつだけです。簡単なものなら数行で書けますし、本格的に major-mode を定義するようなものを作っても良いと思います。

今後、プラグインを増やしていくに従って、プラグイン同士の通信みたいなものが必要になるかもしれないと思っています。

パースペクティブ / プラグイン定義

パースペクティブプラグインの各実装です。現在、以下の実装があります。

  • パースペクティブ
    • code (ひとつのバッファに集中、典型的IDEの形)
    • two (バッファを左右に並べて比較)
    • htwo (バッファを上下に並べて比較、継承の例)
    • doc (全バッファをfollow-modeにする)
    • dashboardプラグインを並べる)
    • array (バッファの選択、サマリー表示)
  • プラグイン
    • history-list / history-list2 (履歴アクセス)
    • files / dired (メインバッファ参照、簡単なプラグイン実装)
    • imenu (アウトライン、アイドルタイマーで更新)
    • top (topコマンド表示、一定時間タイマーで更新)
    • history-nth / main-prev (履歴アクセス、簡単なプラグイン実装)
    • clock (一定時間タイマーで更新、非同期更新)
    • open (プラグインへの引数を渡すデモ)

これらの説明については、これまでのエントリー(機能紹介要件定義)に記したとおりです。

e2wm全体

e2wmの起動と終了を管理する関数があります。ここでは、アドバイスの追加や現状復帰に必要なバックアップを行います。e2wmと相性の悪い拡張については、このタイミングで無効にするなどの対策を取るといいのではないかと考えています。

また起動時(e2wm:start-management)にパースペクティブセットの定義を行うことが出来ます。必要のないパースペクティブを外したり、フレームごとに目的別のパースペクティブのセットを作る感じで考えています。まだパースペクティブの個数が少なかったり、多フレームの対応が出来てないので、今後徐々に機能追加していく予定です。

各イベント時の動き

主なイベント時において、各モジュールやオブジェクトがどのように連携するのかを簡単に説明します。

パースペクティブ切り替え

e2wm:pst-change 関数が呼ばれた時の動きです。パースペクティブオブジェクトのライフサイクルのほとんどが入っています。



パースペクティブ切り替え時の動き

前のパースペクティブの後片付けを行って、次のパースペクティブオブジェクトを生成して画面の構築を開始するという流れです。

startに対応して必ずleaveが呼ばれます。また、startとleaveは管理状態を途中一時中断するために、(切り替え以外のタイミングで)何度か呼ばれることがありますので、leaveで片付ける必要のある初期化処理はstartでするようにして、initではなるべく副作用のある処理を行わないようにします。

画面更新

次に、画面のレイアウトや更新を実施する e2wm:pst-update-windows の動きです。



画面更新時の動き

ウインドウのレイアウトを行った上で、パースペクティブ固有の更新処理とプラグインの実行が続く形です。プラグインのバッファは、基本的にこの更新処理のタイミングで内容が更新されますが、プラグインの中には一定時間おきのタイマー(top,clock)やアイドルタイマー(imenu)で自動的に更新するものがあります。プラグインの性質によって、更新のタイミングを考えておく必要があると思います。

1番のウインドウの再レイアウトは重い処理(一旦全ウインドウを消して、レイアウト指示に従ってsplitし直す為)なので、レイアウト変更などの必要なときのみ実行されるようになっています。

バッファ表示、ポップアップ

最後に、switch-to-bufferやpop-to-bufferなどで、バッファ表示が行われようとしたときに、どのように処理を乗っ取るかについての動きです。



バッファ表示・ポップアップ時の動き

まず、バッファ表示の履歴に記録します。次に、パースペクティブ側の関数でバッファをどこに表示させるかを決めます。もし、パースペクティブ側の関数で何もしなかった場合は、元々の関数の動きに任せます。

注意点として、この流れの中では e2wm:pst-update-windows は呼ばれません。プラグインを更新したり画面をレイアウトし直したい場合は、パースペクティブの switch / popup の中で e2wm:pst-update-windows を呼ぶ必要があります。その際、パフォーマンスや、処理が行き違いになって意図しない動きにならないように気をつける必要があります。

現状と今後

以上が現状の設計と実装についての説明でした。
ここでは、現状の説明や今後どのようにしていこうと考えているかについて、少し書いてみます。

現在の実装について

現状はまたコンセプト実装の段階というか、まだまだ完成していないという認識です。ただ、手元での動作は安定していて一通りの機能はそろっているため、個人的には常用していて、実際に便利になりました。

e2wmの基本的な設計や実装などについてはある程度固まったかなと思っています。今後、まだ足りていないパースペクティブプラグインを追加したり、もう少し使っていただける方々のご意見なども集めながら拡張の仕組みなどについても改善して、e2wmというEmacs拡張として成長させていきたいと思っています。

機能が足りていない以外に、現時点の問題点としては、Emacsのデフォルト挙動の乗っ取り精度や、既存のEmacs拡張との相性の問題があります。乗っ取りについては自分の環境ではある程度うまくいくようになったのですが、Emacsの補完バッファのポップアップなどの動きが自分の中ではまだうまく追いきれてないため、set-window-configuration周辺のコードについてはまだまだ修正をしていく必要があるかと思っています。また、Elscreenのような画面制御系の拡張とうまく組み合わせられるようにしたいと思っています。

今後の開発について

報告していただいた問題の解決と、ビューやパースペクティブの充実を行いたいと思っています。

今後用意したいと思っている機能などは以下のようです。

予定ですので、変わるかもしれません。

パースペクティブユースケース

要件定義のエントリーでも書きましたが、e2wmはあくまでウインドウの管理であって、IDE的な機能を提供しようとしているわけではありません。あくまでウインドウ分割・バッファ表示の制御がメインであって、そこに付加的なプラグインが付いているだけです。

そのため、他のIDEのような使い方を強制されるものと言うよりは、自分の使い方にあわせたウインドウ分割をカスタマイズして、そこに便利なプラグインを追加するという形で活用していくイメージになります。もちろん、Emacs自身がIDE的な機能を持っていますので、他のIDEと同じような画面にして使うことも出来ます。実際に、EclipseのようなIDEはよく考えられていますので、画面構成は大変参考になると思います。

ひとつのパースペクティブを何でも使えるようにカスタマイズしても良いですし、基本となるパースペクティブを用意して、細かく分けてしまうという方法もあると思います。現在用意しているパースペクティブは、基本となるパースペクティブになるように調整していますので、これを継承(e2wm:$pst-extendスロットを使う)して自分用にあわせていくと良いかと思います。

パースペクティブをカスタマイズしたり新しく作ったりする場合には、「どういう場面を想定して使うのか」というユースケースを考えることが大事だと思っています。現在のe2wmは、コーディング作業を想定したユースケースからパースペクティブを考えていますので、例えばメールやorg-modeなど、コーディング以外の拡張とはうまく合わないと思っています。

さらに、新しくユースケースから考える場合は、他のアプリなども参考にして、Emacsの機能が生きるようなパースペクティブを考えていくとおもしろいと思います。自分のところでも、今後Wanderlusthowmなどとの連携を今後考えていきたいと思っています。

次の予定

最終章:導入方法やカスタマイズについて。かなり疲れてきたので、無くなるかもしれません。

はてなでimgタグを直接書けることに今気がつきました。ということで、cacoo.elをちょっと直しています。。。