분석 대상: zed-industries/zed · ~200 crates · Cargo.toml 의존성 + 실제 소스 확인
Zed의 crate들은 6개 레이어로 나뉜다. 의존 방향은 항상 위 → 아래다. 상위 레이어는 하위 레이어를 알지만, 하위 레이어는 상위 레이어를 모른다. 덕분에 하위 레이어는 독립적으로 테스트 가능하고, 플랫폼 백엔드만 교체해서 macOS/Linux/Windows를 지원할 수 있다.
zed와 collab(서버) 두 바이너리.project는 파일시스템+LSP+Git을 총괄하는 핵심 컨트롤러. multi_buffer는 여러 파일을 하나의 편집 뷰로 합치고, client는 협업 서버 연결을 관리한다.text(CRDT 버퍼), language(파싱/하이라이팅), gpui(UI 렌더링). 이 세 개가 Zed의 핵심 엔진.sum_tree(B+ 트리), clock(Lamport 시계), collections(커스텀 해시맵 등).gpui의 trait들을 각 OS/GPU API로 구현. 이 레이어만 교체하면 macOS ↔ Linux ↔ Web 포팅이 가능한 구조다.아래는 실제 editor/Cargo.toml의 의존성 일부다. editor는 Layer 4이므로 Layer 2(gpui, text)를 알지만, Layer 5 바이너리(zed)를 알 수 없다. 역방향 import는 컴파일러가 막는다.
# crates/editor/Cargo.toml (실제 의존성 발췌)
[dependencies]
gpui = { workspace = true } # Layer 2: UI 프레임워크
text = { workspace = true } # Layer 2: CRDT 텍스트
language = { workspace = true } # Layer 2: 파싱/하이라이팅
project = { workspace = true } # Layer 3: 파일시스템/LSP
lsp = { workspace = true } # Layer 2: LSP 클라이언트
theme = { workspace = true } # Layer 3: 테마/색상
sum_tree = { workspace = true } # Layer 1: 기반 자료구조
# ❌ zed = ... 불가 — 상위 바이너리는 절대 참조할 수 없다
Layer 1 sum_tree는 반대로 Zed 자체 crate를 하나도 의존하지 않는다:
# crates/sum_tree/Cargo.toml
[dependencies]
heapless = "0.8" # 스택 배열
rayon = "1" # 병렬 이터레이터
# 👆 Zed crate 의존 없음 — 완전 독립 라이브러리
GPUI는 Zed 팀이 직접 만든 UI 프레임워크다. React처럼 컴포넌트 트리를 매 프레임 재구성하되, Rust의 소유권 모델 위에서 동작한다. 핵심 차이점은 동적 디스패치(dyn Trait) 대신 함수 포인터를 쓰고, 프레임당 아레나 할당으로 수천 번의 Box::new()를 없앴다는 것이다.
Entity<T>로 EntityMap에 저장된다. 컴포넌트는 핸들(Entity)만 갖고, 실제 데이터는 중앙에서 관리된다.AnyView.render에 fn 포인터를 저장해 vtable 없이 타입별 렌더 함수를 호출한다.clear() 한 번으로 전부 해제한다.crates/gpui/src/app/entity_map.rs에서 발췌. PhantomData<fn(T)→T>로 불변(invariant) 타입 파라미터를 만드는 Rust 고급 패턴이 쓰인다.
// gpui/src/app/entity_map.rs
slotmap::new_key_type! {
pub struct EntityId; // SlotMap 키 — 재사용 없는 고유 ID
}
/// 타입-지워진 동적 엔티티 핸들 (기반)
pub struct AnyEntity {
pub(crate) entity_id: EntityId,
pub(crate) entity_type: TypeId,
entity_map: Weak<RwLock<EntityRefCounts>>, // 약한 참조 → 해제 감지
}
/// 타입-안전 핸들. T의 실제 값은 EntityMap에 저장됨
#[derive(Deref, DerefMut)]
pub struct Entity<T> {
#[deref]
pub(crate) any_entity: AnyEntity,
pub(crate) entity_type: PhantomData<fn(T) -> T>,
// ^^^^^^^^^^^
// fn(T)->T 트릭: T에 대해 공변도 반변도 아닌 "불변" 분산 확보
// → Entity<Child>를 Entity<Parent>로 coerce 불가
}
impl Clone for AnyEntity {
fn clone(&self) -> Self {
// 해제된 엔티티 복제 시도 → 즉시 패닉
if let Some(map) = self.entity_map.upgrade() {
let count = map.read().counts.get(self.entity_id).unwrap();
let prev = count.fetch_add(1, SeqCst);
assert_ne!(prev, 0, "Detected over-release of a entity.");
}
Self { entity_id: self.entity_id, entity_type: self.entity_type,
entity_map: self.entity_map.clone(), .. }
}
}
crates/gpui/src/element.rs에서 발췌. 모든 GPUI UI 컴포넌트는 이 trait을 구현한다.
// gpui/src/element.rs
pub trait Element: 'static + IntoElement {
type RequestLayoutState: 'static; // request_layout이 반환하는 상태
type PrepaintState: 'static; // prepaint가 반환하는 상태
fn id(&self) -> Option<ElementId>;
/// 1단계: Taffy에 크기를 요청하고 초기 상태를 초기화
fn request_layout(
&mut self,
id: Option<&GlobalElementId>,
inspector_id: Option<&InspectorElementId>,
window: &mut Window,
cx: &mut App,
) -> (LayoutId, Self::RequestLayoutState);
/// 2단계: 레이아웃이 확정된 뒤 Hitbox·스크롤 등 등록
fn prepaint(
&mut self,
id: Option<&GlobalElementId>,
inspector_id: Option<&InspectorElementId>,
bounds: Bounds<Pixels>, // Taffy 계산 결과
request_layout: &mut Self::RequestLayoutState,
window: &mut Window,
cx: &mut App,
) -> Self::PrepaintState;
/// 3단계: Scene에 드로우 명령 추가 (GPU로 갈 명령)
fn paint(
&mut self,
id: Option<&GlobalElementId>,
inspector_id: Option<&InspectorElementId>,
bounds: Bounds<Pixels>,
request_layout: &mut Self::RequestLayoutState,
prepaint: &mut Self::PrepaintState,
window: &mut Window,
cx: &mut App,
);
}
// 사용 예: 한 프레임에서 element_arena를 clear()로 일괄 해제
// window.element_arena.clear(); ← 수천 번 Box::new() 비용 제거
crates/gpui/src/subscription.rs에서 발췌. Subscription이 스코프를 벗어나면 Drop이 자동으로 이벤트 리스너를 해제한다. #[must_use]로 의도치 않은 즉시 drop을 컴파일 경고로 잡는다.
// gpui/src/subscription.rs
#[must_use] // 반환값을 무시하면 즉시 drop → 경고
pub struct Subscription {
unsubscribe: Option<Box<dyn FnOnce() + 'static>>,
}
impl Subscription {
/// 영구 구독 — 직접 해제할 때까지 유지
pub fn detach(mut self) {
self.unsubscribe.take(); // drop 시 unsubscribe 호출 안 함
}
/// 두 구독을 하나로 묶기
pub fn join(mut a: Self, mut b: Self) -> Self {
let fa = a.unsubscribe.take();
let fb = b.unsubscribe.take();
Self {
unsubscribe: Some(Box::new(move || {
if let Some(f) = fa { f(); }
if let Some(f) = fb { f(); }
})),
}
}
}
impl Drop for Subscription {
fn drop(&mut self) {
// 스코프 종료 시 자동으로 리스너 해제
if let Some(unsubscribe) = self.unsubscribe.take() {
unsubscribe();
}
}
}
// 사용 예:
// let _sub = cx.observe(&model, |this, cx| { ... });
// _sub이 drop되면 → observer 자동 해제
Zed의 텍스트 엔진은 5개 레이어 스택으로 이루어진다. 맨 아래에 고성능 B+ 트리(SumTree)가 있고, 그 위에 Unicode 텍스트(Rope), CRDT 버퍼(Buffer), 다중 파일 뷰(MultiBuffer), 최상단에 편집 UI(Editor)가 쌓인다. 각 레이어는 명확한 책임을 갖는다.
crates/sum_tree/src/sum_tree.rs에서 발췌. 이 세 trait의 조합이 Zed 전체 자료구조의 핵심이다.
// sum_tree/src/sum_tree.rs
/// 트리에 저장되는 아이템. 자신의 Summary를 계산할 수 있어야 함
pub trait Item: Clone {
type Summary: Summary;
fn summary(&self, cx: <Self::Summary as Summary>::Context<'_>) -> Self::Summary;
}
/// 서브트리 전체의 집계값. add_summary로 누적, zero로 초기화
pub trait Summary: Clone {
type Context<'a>: Copy; // GAT — 집계에 필요한 컨텍스트 (빌림 가능)
fn zero<'a>(cx: Self::Context<'a>) -> Self;
fn add_summary<'a>(&mut self, summary: &Self, cx: Self::Context<'a>);
}
/// Summary에서 특정 차원(예: 바이트 수, 줄 수)을 추출해 탐색에 쓰는 인터페이스
pub trait Dimension<'a, S: Summary>: Clone {
fn zero(cx: S::Context<'_>) -> Self;
fn add_summary(&mut self, summary: &'a S, cx: S::Context<'_>);
}
/// 실제 B+ 트리 구조체 — Arc로 공유, 편집 시 CoW
#[derive(Clone)]
pub struct SumTree<T: Item>(Arc<Node<T>>);
// 구체적인 예: Rope의 Chunk
// impl Item for Chunk {
// type Summary = TextSummary;
// fn summary(&self, _: ()) -> TextSummary {
// TextSummary { bytes: self.0.len(), lines: self.0.matches('\n').count(), .. }
// }
// }
// → SumTree<Chunk>.summary().lines 로 전체 줄 수를 O(1)에 읽을 수 있다
crates/rope/src/rope.rs, crates/text/src/text.rs에서 발췌.
// rope/src/rope.rs
#[derive(Clone, Default)]
pub struct Rope {
chunks: SumTree<Chunk>, // Chunk = 최대 CHUNK_BASE 바이트의 UTF-8 슬라이스
}
// text/src/text.rs
pub struct Buffer {
snapshot: BufferSnapshot, // 현재 상태의 불변 스냅샷
history: History, // 편집 이력 + undo/redo 스택
deferred_ops: OperationQueue<Operation>, // 아직 처리 안 된 원격 op
pub lamport_clock: clock::Lamport, // 이 레플리카의 논리 시계
subscriptions: Topic<usize>, // 변경 알림 구독자 목록
}
#[derive(Clone)]
pub struct BufferSnapshot {
visible_text: Rope, // 현재 보이는 텍스트
deleted_text: Rope, // 삭제된 텍스트 (undo를 위해 보관)
fragments: SumTree<Fragment>, // CRDT의 핵심 — 삽입/삭제 이력
insertions: SumTree<InsertionFragment>,
undo_map: UndoMap,
pub version: clock::Global, // 이 스냅샷의 벡터 시계
replica_id: ReplicaId,
}
// Buffer 편집의 핵심:
// 1. Lamport 타임스탬프를 부여한 Operation 생성
// 2. Rope(SumTree) 수정
// 3. Fragment 목록 갱신 (삭제된 텍스트도 보관!)
// 4. cx.notify()로 dirty flag → 다음 tick에 재렌더링
키보드 하나를 눌렀을 때 화면이 바뀌기까지 어떤 경로를 거치는지 단계별로 따라가 본다. 크게 입력 처리 → 데이터 변경 → 재렌더링 → GPU 출력 네 구간으로 나뉜다.
cx.notify()는 즉시 화면을 그리지 않고 dirty 플래그만 세운다.
현재 이벤트 처리가 끝난 뒤 다음 event loop tick에서 dirty views를 한 번에 그린다.
이 배치 처리가 한 프레임에 여러 상태 변경이 있어도 화면을 한 번만 그리게 보장한다 — React의 setState batching과 같은 원리.
crates/gpui/src/window.rs에서 발췌. ③④⑤ 단계의 실제 구현이다.
// gpui/src/window.rs
/// ③ 키스트로크를 GPUI 이벤트로 변환해 디스패치
pub fn dispatch_keystroke(&mut self, keystroke: Keystroke, cx: &mut App) -> bool {
let keystroke = keystroke.with_simulated_ime();
let result = self.dispatch_event(
PlatformInput::KeyDown(KeyDownEvent {
keystroke: keystroke.clone(),
is_held: false,
prefer_character_input: false,
}),
cx,
);
if !result.propagate {
return true; // 누군가 처리 → 문자 입력 단계 건너뜀
}
// 키맵 매칭 실패 시 문자로 직접 IME에 전달
if let Some(input) = keystroke.key_char
&& let Some(mut handler) = self.platform_window.take_input_handler()
{
handler.dispatch_input(&input, self, cx); // ⑥ Editor::handle_input
self.platform_window.set_input_handler(handler);
return true;
}
false
}
/// ⑤ 포커스 경로를 따라 Action 핸들러를 capture → bubble 순으로 시도
fn dispatch_action_on_node_inner(&mut self, node_id: DispatchNodeId,
action: &dyn Action, cx: &mut App) {
let dispatch_path = self.rendered_frame.dispatch_tree.dispatch_path(node_id);
// 캡처 단계: 루트 → 포커스 노드 방향
cx.propagate_event = true;
for node_id in &dispatch_path {
let node = self.rendered_frame.dispatch_tree.node(*node_id);
for DispatchActionListener { action_type, listener } in node.action_listeners.clone() {
if action_type == action.as_any().type_id() {
listener(action.as_any(), DispatchPhase::Capture, self, cx);
if !cx.propagate_event { return; } // 처리 완료 → 버블링 중단
}
}
}
// 버블 단계: 포커스 노드 → 루트 방향
for node_id in dispatch_path.iter().rev() {
let node = self.rendered_frame.dispatch_tree.node(*node_id);
for DispatchActionListener { action_type, listener } in node.action_listeners.clone() {
if action_type == action.as_any().type_id() {
listener(action.as_any(), DispatchPhase::Bubble, self, cx);
if !cx.propagate_event { return; }
}
}
}
}
crate 이름 → struct 이름 → 한 줄 역할 순서로 읽으면 된다. 녹색 굵은 항목은 Zed 아키텍처에서 특히 핵심적인 타입이다.
| crate | 타입 | 역할 | 핵심 필드 / 특이사항 |
|---|---|---|---|
sum_tree |
SumTree<T> | Zed 전체의 데이터 기반. 함수형 B+ 트리. Arc로 공유, 편집 시 Copy-on-Write | Item trait · Summary trait · GAT Context |
rope |
Rope |
Unicode 텍스트 저장. 중간 삽입 O(log n). 바이트/줄/문자 다중 인덱싱 | chunks: SumTree<Chunk> |
clock |
Lamport, ReplicaId |
CRDT 연산에 전역 순서를 부여하는 논리 시계 | value: u64 · ReplicaId는 u16 newtype |
text |
Buffer | CRDT 텍스트 버퍼. 모든 편집이 Operation으로 기록됨. 실시간 협업 + 무한 undo 지원 | snapshot, history, replica_id, clock |
text |
BufferSnapshot |
Buffer의 특정 시점 불변 스냅샷. 스레드 간 공유 가능 | visible_text: Rope, fragments: SumTree<Fragment> |
multi_buffer |
MultiBuffer |
여러 Buffer를 하나의 가상 문서로 합침. 검색 결과, Diff 뷰, 다중 커서 편집에 사용 | buffers: HashMap<BufferId, Buffer> |
lsp |
LanguageServer |
LSP 서버 프로세스를 생성하고 JSON-RPC로 통신 | server_id, capabilities, stdin/stdout 채널 |
gpui |
App | GPUI 전역 싱글턴. 모든 Entity, Window, Action, Platform을 소유 | entities, windows, platform, executors |
gpui |
Entity<T> |
타입 안전 핸들. 실제 데이터는 EntityMap에, 핸들만 코드에서 전달됨 | AnyEntity + PhantomData<fn(T)→T> (불변 분산) |
gpui |
AnyView |
동적 뷰 핸들. render 함수를 fn 포인터로 저장 (vtable 없음) | entity: AnyEntity, render: fn ptr |
gpui |
Window |
네이티브 창 + CSS Flexbox 레이아웃(Taffy) + 프레임 버퍼 + 글리프 Atlas | platform_window, root, layout_engine, element_arena |
gpui |
Scene |
CPU→GPU 중간 표현. 한 프레임의 모든 draw 명령 목록. #[repr(C)]로 GPU 버퍼에 직접 복사 | quads, shadows, paths, sprites Vec 목록 |
project |
Project | 파일시스템 + LSP + Git + 빌드 태스크를 총괄하는 앱 로직 중추 | worktrees, lsp_store, buffer_store, git_store |
project |
Worktree |
하나의 루트 폴더. 파일 트리를 SumTree로 관리, 파일시스템 변경사항 감시 | entries: SumTree<Entry>, fs_watcher |
editor |
Editor |
텍스트 편집 UI 전체. 커서/선택/스크롤/자동완성/인레이힌트/문법 하이라이팅 | buffer, display_map, selections, project |
editor |
DisplayMap |
Buffer 내용을 화면 좌표로 변환. 소프트랩·폴딩·인레이힌트 반영 | Buffer 오프셋 ↔ 화면 행/열 변환 |
workspace |
Workspace |
에디터 창 전체의 레이아웃. Pane 분할, 탭 관리, 도크 패널 | panes: Vec<Entity<Pane>>, project |
client |
Client |
Zed 협업 서버와의 WebSocket 연결. RPC peer, HTTP 클라이언트 관리 | peer: Arc<Peer>, http_client |
위 표에서 가장 중요한 세 타입의 실제 소스 발췌. 주석은 역할과 설계 의도를 설명한다.
// ① sum_tree/src/sum_tree.rs
// Zed 전체에서 리스트형 자료구조로 쓰이는 함수형 B+ 트리
pub struct SumTree<T: Item>(Arc<Node<T>>);
// ^^^
// Arc 하나만 복사하면 스냅샷 완성 — O(1)
// Node는 Internal 또는 Leaf
// Leaf { summary, items: ArrayVec<T>, item_summaries: ArrayVec<S> }
// Internal { summary, child_summaries, child_trees: ArrayVec<SumTree<T>> }
// 편집(push/splice)은 항상 새 Arc를 만들어 CoW; 공유된 서브트리는 그대로 재사용
// ② text/src/text.rs
// Zed 실시간 협업의 핵심 — 모든 편집이 CRDT Operation으로 기록됨
pub struct Buffer {
snapshot: BufferSnapshot, // 현재 보이는 상태
history: History, // undo/redo 스택 + base_text
deferred_ops: OperationQueue<Operation>, // 아직 정렬 안 된 원격 편집
pub lamport_clock: clock::Lamport, // 전역 순서 부여용 논리 시계
subscriptions: Topic<usize>, // 변경 알림 구독자
edit_id_resolvers: HashMap<clock::Lamport, Vec<oneshot::Sender<()>>>,
wait_for_version_txs: Vec<(clock::Global, oneshot::Sender<()>)>,
}
#[derive(Clone)]
pub struct BufferSnapshot {
visible_text: Rope, // 삽입 텍스트만 (실제 보이는 내용)
deleted_text: Rope, // 삭제된 텍스트 (undo 복원용)
fragments: SumTree<Fragment>, // 삽입/삭제 이력 — CRDT 핵심
pub version: clock::Global, // 이 스냅샷의 벡터 시계 (협업 병합 기준)
replica_id: ReplicaId,
line_ending: LineEnding,
}
// ③ gpui/src/app/entity_map.rs
// EntityId는 SlotMap 키라 재발급 없음, Weak 참조로 해제 감지
pub struct EntityMap {
entities: SecondaryMap<EntityId, Box<dyn Any>>, // T를 type-erase
accessed_entities: RefCell<FxHashSet<EntityId>>, // 렌더 추적용
ref_counts: Arc<RwLock<EntityRefCounts>>,
}
pub struct EntityRefCounts {
counts: SlotMap<EntityId, AtomicUsize>, // 원자적 refcount
dropped_entity_ids: Vec<EntityId>, // 지연 해제 큐
}
// EntityMap::lease() — 업데이트 중 일시적으로 소유권을 "대출"
pub fn lease<T>(&mut self, pointer: &Entity<T>) -> Lease<T> {
// entities에서 꺼냄 → 업데이트 콜백에서 mutable 접근 가능
// end_lease() 호출로 반납 필수 (Drop이 패닉으로 강제)
}
pub struct Lease<T> {
entity: Option<Box<dyn Any>>,
pub id: EntityId,
entity_type: PhantomData<T>,
}
impl<T> Drop for Lease<T> {
fn drop(&mut self) {
// end_lease() 없이 drop되면 패닉 — 버그를 런타임에 즉시 포착
if self.entity.is_some() && !panicking() {
panic!("Leases must be ended with EntityMap::end_lease")
}
}
}