yoi/docs/ref/zed-workspace.md
2026-04-21 17:39:43 +09:00

10 KiB
Raw Blame History

Zed モノレポのワークスペース・クレート規則

zed-industries/zed のソースコードを読み解くための、ワークスペース構成とクレート分割に関する規則のまとめ。


1. ワークスペース構成

単一ワークスペース

  • リポジトリ直下の Cargo.toml[workspace] を持ち、crates/ 以下のすべてのクレート200個超を members として束ねる単一ワークスペース。
  • default-members = ["crates/zed"] が指定されており、ルートで cargo run するとエディタ本体が起動する。
  • Cargo.lock はルートに1つだけ。すべてのクレートで共有される。
  • ルートに rust-toolchain.toml / clippy.toml / rustfmt.toml / .cargo/ を置き、ワークスペース全体に共通設定を効かせる。

内部依存は [workspace.dependencies] に集約

ルート Cargo.tomlすべての内部クレートを path 指定で列挙 する:

[workspace.dependencies]
acp_thread       = { path = "crates/acp_thread" }
action_log       = { path = "crates/action_log" }
agent            = { path = "crates/agent" }
agent_ui         = { path = "crates/agent_ui" }
anthropic        = { path = "crates/anthropic" }
# ... 200個以上続く

外部クレートserde, tokio, フォーク版 calloop など)も同じ場所に集約され、バージョンや git rev を一元管理する。

各クレートは .workspace = true で参照

個々の crates/<name>/Cargo.toml ではバージョン番号もパスも書かない:

[dependencies]
gpui.workspace = true
project.workspace = true
serde.workspace = true

これにより:

  • バージョン更新やフォーク差し替えがルート1ファイルで完結
  • 同じ依存が複数バージョンに分裂する事故が起きない
  • 新しいクレートの追加は「フォルダ作成 → ルートに1行追加 → 使う側で name.workspace = true」だけ

publish = false

ワークスペース全体で publish = false。crates.io には公開されないため、editorprojectlanguage のような一般名詞を平気で使える。


2. クレート命名規則

表記ルール

  • snake_case で統一。ハイフンや CamelCase は使わない。
  • 全部小文字、略語も小文字(lsp, rpc, acp, aws, ui)。
  • ディレクトリ名 = package.name = [workspace.dependencies] のキー で完全一致。grep しやすさを優先。

命名パターン

1. コア基盤は1語の名詞

zed / gpui / editor / project / workspace / language / theme / ui / lsp / rpc / audio / assets / askpass

2. 機能ファミリーは「共通プレフィックス + サフィックス」

agent / agent_ui / agent_ui_v2 / agent_settings / agent_servers
auto_update / auto_update_ui / auto_update_helper
assistant_text_thread / assistant_slash_command / assistant_slash_commands
acp_thread / acp_tools

サフィックスの慣習:

サフィックス 役割
_ui 機能のビュー/ウィジェット層(コアロジックとは分離)
_settings 設定スキーマと読み込み
_servers 外部プロセス連携アダプタ
_helper 補助バイナリ
_v2 既存版と並行する新実装の隔離
_tools デバッグ/開発者向けユーティリティ

3. 外部プロトコル/ベンダー連携はその名前そのまま

anthropic / bedrock / aws_http_client のように、相手のサービスや仕様の名前をそのまま使う。acp_* (Agent Client Protocol) のようにプロトコルの略号を頭に付けて系列化するのも同様。


3. クレート分割の方針

zed クレートは薄いシェル

crates/zed/ の中身は最小限:

  • main.rs — エントリポイント、CLI 引数、シングルインスタンス処理、クラッシュハンドラ、パス初期化
  • zed.rs — グローバル action ハンドラ登録、initialize_workspace でのパネル組み立て
  • build.rs / RELEASE_CHANNEL

app.run(...) の中身は 200個以上のクレートの init(cx) を正しい順序で呼ぶだけ:

settings::init(cx);
theme::init(cx);
client::init(&client, cx);
workspace::init(app_state.clone(), cx);
editor::init(cx);
project_panel::init(cx);
agent_ui::init(fs, client, prompt_builder, languages, cx);
git_ui::init(cx);
// ...

zed は依存グラフの頂点に立つ唯一のクレートで、機能の置き場所ではなく 配線盤 (wiring) として存在する。「どこに置くか迷ったら zed 以外のどこかに置く」 が鉄則。

境界を切る基準

① レイヤー(下から上への単方向依存)

gpui                                       ← UI フレームワーク
  ↓
text / language / fs / rpc / settings /    ← ドメインの基本型
theme / ui
  ↓
project / lsp / git / terminal /           ← サービス層
multi_buffer
  ↓
editor / workspace                         ← 中核機能
  ↓
project_panel / git_ui / agent_ui /        ← 個別機能 (パネル/ビュー)
diagnostics / search / ...
  ↓
zed                                        ← 配線だけ

下のレイヤーは上のレイヤーを知らない。逆方向の拡張点は traitworkspace::Itemworkspace::Panel など)として下層が公開し、上層が実装する。

② ロジックと UI の分離

GPUI 依存を上層に閉じ込めるため、ロジックと UI を別クレートに切る:

ロジック UI
agent agent_ui
auto_update auto_update_ui
git git_ui
project project_panel

UI 側はロジック側を知るが、ロジック側は UI 側を知らない。

ProjectWorkspace の二分

Zed の設計で最も象徴的な分割:

  • project — worktree、LSP、ファイルシステム、Git といったサービスを束ねる。ヘッドレスでも動く側。
  • workspace — ペイン分割、ドック、パネル、ステータスバーといったウィンドウ表示を束ねる。GPUI に強く依存する側。

これにより Project をリモートマシンで走らせ Workspace をローカルで走らせる、というリモート開発が素直に成立する。

④ trait 境界 + Fake 実装

下層の重要な抽象は trait で公開し、テストでは Fake を差し込む:

  • fs::FsFakeFs
  • git::GitRepository ↔ Fake 実装
  • language::LanguageRegistry
  • gpui::Elementworkspace::Item/Panel

trait が置かれているクレートがそのまま境界になる。「どこまでがプラグイン可能か」がクレート一覧から読み取れる。

⑤ 外部世界との接点ごとにクレートを切る

新規追加で既存クレートを編集しなくて済むように、外部依存ごとに独立させる:

  • lsp / dap / rpc (プロトコル層)
  • anthropic / bedrock / open_ai (LLM プロバイダごと)
  • aws_http_client (特定の HTTP クライアント実装)
  • acp_thread / acp_tools (Agent Client Protocol)
  • extension_host + extension_api (拡張機能 WASM サンドボックス境界)

⑥ コンパイル時間最適化

頻繁に編集される editor のような大きいクレートを切り離すことで、並列ビルドとインクリメンタルコンパイルの効率を上げる。ルート Cargo.toml には単一ファイルクレートに codegen-units = 1 を効かせる調整も含まれる。


4. 「core」クレートを作らない

Zed には core / common / shared / zed_core といったクレートが意図的に存在しない。「コア」になりそうなものは機能軸で水平に分割される:

ありがちな "core" の中身 Zed での置き場所
基本データ型 (Rope, Point, Anchor) text
ファイルシステム抽象 fs (Fs trait + FakeFs)
設定の読み書き settings
テーマ・色 theme
共通 UI コンポーネント ui
汎用ユーティリティ関数 util
RPC/シリアライズ rpc / proto
ロギング zlog
言語抽象 language

core を作らない理由

  1. ビルドグラフの直列化を避けるcore を作るとほぼ全クレートがそこに依存し、1行触るたびに数百クレートが再コンパイルされる。並列ビルドの利点が消える。
  2. "ゴミ捨て場" 化の防止corecommon は置き場所に迷ったコードが流れ込む磁石になり、責務が肥大化して循環依存の温床になる。Zed は 「迷ったら新しいクレートを作る」 方向に振る。
  3. 依存方向のドキュメント化 — クレートを細かく分けると Cargo.toml を見るだけで何に依存し何に依存していないかが一覧できる。core があるとこの情報量がゼロになる。

例外的な util

crates/util は雑多な汎用ヘルパ(ResultExt, paths, debouncer 等)が入る "ややコアっぽい" クレートだが:

  • 名前は core ではなく util (補助関数の入れ物の慣習)
  • GPUI にも editor にも依存しない、完全な下層
  • trait や中核データ型は置かない (それは textfs の役目)

責務を「補助関数集」に限定することでゴミ捨て場化を防いでいる。


5. まとめ

Zed のクレート分割の優先順位:

  1. zed には何も入れない — 機能は必ず別クレートに押し出し、zed は init 呼び出しと CLI と main だけを持つ
  2. 下から上への単方向依存 — 上層が必要な拡張点は trait で下層に公開する
  3. ロジック / UI を分けるxxxxxx_ui のペア、GPUI 依存を上層に閉じ込める
  4. ProjectWorkspace を分ける — ヘッドレス/リモート実行可能性のため
  5. 外部世界 (プロトコル・ベンダー・拡張) ごとにクレートを切る — 新規追加を「ファイル追加だけ」で済ませる
  6. Fake が置けるところに trait を置く — テスト境界 = アーキテクチャ境界
  7. core を作らない — 万能箱の代わりに責務を限定した小さな箱を用意する

クレート数の多さは管理コストではなく、境界を守るためのコスト払いである。