Get workspace module in and compiling

Nathan Sobo created

Change summary

Cargo.lock                          | 103 ++++++
gpui/src/app.rs                     |   2 
zed/Cargo.toml                      |   2 
zed/src/editor/buffer_view.rs       |  72 ++--
zed/src/lib.rs                      |   1 
zed/src/test.rs                     |  41 ++
zed/src/workspace/mod.rs            | 119 ++++++++
zed/src/workspace/pane.rs           | 285 +++++++++++++++++++
zed/src/workspace/pane_group.rs     | 393 +++++++++++++++++++++++++++
zed/src/workspace/workspace.rs      | 271 ++++++++++++++++++
zed/src/workspace/workspace_view.rs | 444 +++++++++++++++++++++++++++++++
zed/src/worktree/mod.rs             |   2 
12 files changed, 1,696 insertions(+), 39 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -673,6 +673,12 @@ dependencies = [
  "pkg-config",
 ]
 
+[[package]]
+name = "fuchsia-cprng"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba"
+
 [[package]]
 name = "futures-core"
 version = "0.3.12"
@@ -776,7 +782,7 @@ dependencies = [
  "parking_lot",
  "pathfinder_color",
  "pathfinder_geometry",
- "rand",
+ "rand 0.8.3",
  "smallvec",
  "smol",
  "tree-sitter",
@@ -824,6 +830,12 @@ dependencies = [
  "cfg-if 1.0.0",
 ]
 
+[[package]]
+name = "itoa"
+version = "0.4.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736"
+
 [[package]]
 name = "lazy_static"
 version = "1.4.0"
@@ -1113,6 +1125,19 @@ dependencies = [
  "proc-macro2",
 ]
 
+[[package]]
+name = "rand"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293"
+dependencies = [
+ "fuchsia-cprng",
+ "libc",
+ "rand_core 0.3.1",
+ "rdrand",
+ "winapi",
+]
+
 [[package]]
 name = "rand"
 version = "0.8.3"
@@ -1121,7 +1146,7 @@ checksum = "0ef9e7e66b4468674bfcb0c81af8b7fa0bb154fa9f28eb840da5c447baeb8d7e"
 dependencies = [
  "libc",
  "rand_chacha",
- "rand_core",
+ "rand_core 0.6.2",
  "rand_hc",
 ]
 
@@ -1132,9 +1157,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e12735cf05c9e10bf21534da50a147b924d555dc7a547c42e6bb2d5b6017ae0d"
 dependencies = [
  "ppv-lite86",
- "rand_core",
+ "rand_core 0.6.2",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b"
+dependencies = [
+ "rand_core 0.4.2",
 ]
 
+[[package]]
+name = "rand_core"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc"
+
 [[package]]
 name = "rand_core"
 version = "0.6.2"
@@ -1150,7 +1190,16 @@ version = "0.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "3190ef7066a446f2e7f42e239d161e905420ccab01eb967c9eb27d21b2322a73"
 dependencies = [
- "rand_core",
+ "rand_core 0.6.2",
+]
+
+[[package]]
+name = "rdrand"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2"
+dependencies = [
+ "rand_core 0.3.1",
 ]
 
 [[package]]
@@ -1207,6 +1256,15 @@ version = "0.6.22"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b5eb417147ba9860a96cfe72a0b93bf88fee1744b5636ec99ab20c1aa9376581"
 
+[[package]]
+name = "remove_dir_all"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7"
+dependencies = [
+ "winapi",
+]
+
 [[package]]
 name = "rust-argon2"
 version = "0.8.3"
@@ -1234,6 +1292,12 @@ dependencies = [
  "semver",
 ]
 
+[[package]]
+name = "ryu"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e"
+
 [[package]]
 name = "same-file"
 version = "1.0.6"
@@ -1270,6 +1334,23 @@ version = "0.7.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3"
 
+[[package]]
+name = "serde"
+version = "1.0.124"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bd761ff957cb2a45fbb9ab3da6512de9de55872866160b23c25f1a841e99d29f"
+
+[[package]]
+name = "serde_json"
+version = "1.0.64"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "799e97dc9fdae36a5c8b8f2cae9ce2ee9fdce2058c57a93e6099d919fd982f79"
+dependencies = [
+ "itoa",
+ "ryu",
+ "serde",
+]
+
 [[package]]
 name = "servo-fontconfig"
 version = "0.5.1"
@@ -1379,6 +1460,16 @@ dependencies = [
  "unicode-xid",
 ]
 
+[[package]]
+name = "tempdir"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "15f2b5fb00ccdf689e0149d1b1b3c03fead81c2b37735d812fa8bddbbf41b6d8"
+dependencies = [
+ "rand 0.4.6",
+ "remove_dir_all",
+]
+
 [[package]]
 name = "termcolor"
 version = "1.1.2"
@@ -1566,9 +1657,11 @@ dependencies = [
  "log",
  "num_cpus",
  "parking_lot",
- "rand",
+ "rand 0.8.3",
+ "serde_json",
  "simplelog",
  "smallvec",
  "smol",
+ "tempdir",
  "unindent",
 ]

gpui/src/app.rs 🔗

@@ -230,7 +230,6 @@ impl App {
         read(state.view(handle), state.ctx())
     }
 
-    #[cfg(test)]
     pub fn finish_pending_tasks(&self) -> impl Future<Output = ()> {
         self.0.borrow().finish_pending_tasks()
     }
@@ -1036,7 +1035,6 @@ impl MutableAppContext {
             .detach()
     }
 
-    #[cfg(test)]
     pub fn finish_pending_tasks(&self) -> impl Future<Output = ()> {
         let mut pending_tasks = self.task_callbacks.keys().cloned().collect::<HashSet<_>>();
         let task_done = self.task_done.1.clone();

zed/Cargo.toml 🔗

@@ -31,4 +31,6 @@ smallvec = "1.6.1"
 smol = "1.2.5"
 
 [dev-dependencies]
+serde_json = "1.0.64"
+tempdir = "0.3.7"
 unindent = "0.1.7"

zed/src/editor/buffer_view.rs 🔗

@@ -2,7 +2,7 @@ use super::{
     buffer, movement, Anchor, Bias, Buffer, BufferElement, DisplayMap, DisplayPoint, Point,
     ToOffset, ToPoint,
 };
-use crate::{settings::Settings, watch};
+use crate::{settings::Settings, watch, workspace};
 use anyhow::Result;
 use easy_parallel::Parallel;
 use gpui::{
@@ -1161,38 +1161,50 @@ impl View for BufferView {
     }
 }
 
-// impl workspace::ItemView for BufferView {
-//     fn is_activate_event(event: &Self::Event) -> bool {
-//         match event {
-//             Event::Activate => true,
-//             _ => false,
-//         }
-//     }
+impl workspace::Item for Buffer {
+    type View = BufferView;
 
-//     fn title(&self, app: &AppContext) -> std::string::String {
-//         if let Some(path) = self.buffer.as_ref(app).path(app) {
-//             path.file_name()
-//                 .expect("buffer's path is always to a file")
-//                 .to_string_lossy()
-//                 .into()
-//         } else {
-//             "untitled".into()
-//         }
-//     }
+    fn build_view(
+        buffer: ModelHandle<Self>,
+        settings: watch::Receiver<Settings>,
+        ctx: &mut ViewContext<Self::View>,
+    ) -> Self::View {
+        BufferView::for_buffer(buffer, settings, ctx)
+    }
+}
 
-//     fn entry_id(&self, app: &AppContext) -> Option<(usize, usize)> {
-//         self.buffer.as_ref(app).entry_id()
-//     }
+impl workspace::ItemView for BufferView {
+    fn is_activate_event(event: &Self::Event) -> bool {
+        match event {
+            Event::Activate => true,
+            _ => false,
+        }
+    }
 
-//     fn clone_on_split(&self, ctx: &mut ViewContext<Self>) -> Option<Self>
-//     where
-//         Self: Sized,
-//     {
-//         let clone = BufferView::for_buffer(self.buffer.clone(), self.settings.clone(), ctx);
-//         *clone.scroll_position.lock() = *self.scroll_position.lock();
-//         Some(clone)
-//     }
-// }
+    fn title(&self, app: &AppContext) -> std::string::String {
+        if let Some(path) = self.buffer.as_ref(app).path(app) {
+            path.file_name()
+                .expect("buffer's path is always to a file")
+                .to_string_lossy()
+                .into()
+        } else {
+            "untitled".into()
+        }
+    }
+
+    fn entry_id(&self, app: &AppContext) -> Option<(usize, usize)> {
+        self.buffer.as_ref(app).entry_id()
+    }
+
+    fn clone_on_split(&self, ctx: &mut ViewContext<Self>) -> Option<Self>
+    where
+        Self: Sized,
+    {
+        let clone = BufferView::for_buffer(self.buffer.clone(), self.settings.clone(), ctx);
+        *clone.scroll_position.lock() = *self.scroll_position.lock();
+        Some(clone)
+    }
+}
 
 impl Selection {
     fn head(&self) -> &Anchor {

zed/src/lib.rs 🔗

@@ -8,4 +8,5 @@ mod time;
 mod timer;
 mod util;
 mod watch;
+mod workspace;
 mod worktree;

zed/src/test.rs 🔗

@@ -1,5 +1,9 @@
 use rand::Rng;
-use std::collections::BTreeMap;
+use std::{
+    collections::BTreeMap,
+    path::{Path, PathBuf},
+};
+use tempdir::TempDir;
 
 use crate::time::ReplicaId;
 
@@ -97,3 +101,38 @@ pub fn sample_text(rows: usize, cols: usize) -> String {
     }
     text
 }
+
+pub fn temp_tree(tree: serde_json::Value) -> TempDir {
+    let dir = TempDir::new("").unwrap();
+    write_tree(dir.path(), tree);
+    dir
+}
+
+fn write_tree(path: &Path, tree: serde_json::Value) {
+    use serde_json::Value;
+    use std::fs;
+
+    if let Value::Object(map) = tree {
+        for (name, contents) in map {
+            let mut path = PathBuf::from(path);
+            path.push(name);
+            match contents {
+                Value::Object(_) => {
+                    fs::create_dir(&path).unwrap();
+                    write_tree(&path, contents);
+                }
+                Value::Null => {
+                    fs::create_dir(&path).unwrap();
+                }
+                Value::String(contents) => {
+                    fs::write(&path, contents).unwrap();
+                }
+                _ => {
+                    panic!("JSON object must contain only objects, strings, or null");
+                }
+            }
+        }
+    } else {
+        panic!("You must pass a JSON object to this helper")
+    }
+}

zed/src/workspace/mod.rs 🔗

@@ -0,0 +1,119 @@
+pub mod pane;
+pub mod pane_group;
+pub mod workspace;
+pub mod workspace_view;
+
+pub use pane::*;
+pub use pane_group::*;
+pub use workspace::*;
+pub use workspace_view::*;
+
+use crate::{settings::Settings, watch};
+use gpui::{App, MutableAppContext};
+use std::path::PathBuf;
+
+pub fn init(app: &mut App) {
+    app.add_global_action("workspace:open_paths", open_paths);
+    pane::init(app);
+}
+
+pub struct OpenParams {
+    pub paths: Vec<PathBuf>,
+    pub settings: watch::Receiver<Settings>,
+}
+
+fn open_paths(params: &OpenParams, app: &mut MutableAppContext) {
+    log::info!("open paths {:?}", params.paths);
+
+    // Open paths in existing workspace if possible
+    for window_id in app.window_ids().collect::<Vec<_>>() {
+        if let Some(handle) = app.root_view::<WorkspaceView>(window_id) {
+            if handle.update(app, |view, ctx| {
+                if view.contains_paths(&params.paths, ctx.app()) {
+                    view.open_paths(&params.paths, ctx.app_mut());
+                    log::info!("open paths on existing workspace");
+                    true
+                } else {
+                    false
+                }
+            }) {
+                return;
+            }
+        }
+    }
+
+    log::info!("open new workspace");
+
+    // Add a new workspace if necessary
+    let workspace = app.add_model(|ctx| Workspace::new(params.paths.clone(), ctx));
+    app.add_window(|ctx| WorkspaceView::new(workspace, params.settings.clone(), ctx));
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::{settings, test::*};
+    use gpui::{App, FontCache};
+    use serde_json::json;
+
+    #[test]
+    fn test_open_paths_action() {
+        App::test(|mut app| async move {
+            let settings = settings::channel(&FontCache::new()).unwrap().1;
+
+            init(&mut app);
+
+            let dir = temp_tree(json!({
+                "a": {
+                    "aa": null,
+                    "ab": null,
+                },
+                "b": {
+                    "ba": null,
+                    "bb": null,
+                },
+                "c": {
+                    "ca": null,
+                    "cb": null,
+                },
+            }));
+
+            app.dispatch_global_action(
+                "workspace:open_paths",
+                OpenParams {
+                    paths: vec![
+                        dir.path().join("a").to_path_buf(),
+                        dir.path().join("b").to_path_buf(),
+                    ],
+                    settings: settings.clone(),
+                },
+            );
+            assert_eq!(app.window_ids().len(), 1);
+
+            app.dispatch_global_action(
+                "workspace:open_paths",
+                OpenParams {
+                    paths: vec![dir.path().join("a").to_path_buf()],
+                    settings: settings.clone(),
+                },
+            );
+            assert_eq!(app.window_ids().len(), 1);
+            let workspace_view_1 = app.root_view::<WorkspaceView>(app.window_ids()[0]).unwrap();
+            workspace_view_1.read(&app, |view, app| {
+                assert_eq!(view.workspace.as_ref(app).worktrees().len(), 2);
+            });
+
+            app.dispatch_global_action(
+                "workspace:open_paths",
+                OpenParams {
+                    paths: vec![
+                        dir.path().join("b").to_path_buf(),
+                        dir.path().join("c").to_path_buf(),
+                    ],
+                    settings: settings.clone(),
+                },
+            );
+            assert_eq!(app.window_ids().len(), 2);
+        });
+    }
+}

zed/src/workspace/pane.rs 🔗

@@ -0,0 +1,285 @@
+use super::{ItemViewHandle, SplitDirection};
+use crate::{settings::Settings, watch};
+use gpui::{
+    color::ColorU, elements::*, keymap::Binding, App, AppContext, ChildView, Entity, View,
+    ViewContext,
+};
+use std::cmp;
+
+pub fn init(app: &mut App) {
+    app.add_action(
+        "pane:activate_item",
+        |pane: &mut Pane, index: &usize, ctx| {
+            pane.activate_item(*index, ctx);
+        },
+    );
+    app.add_action("pane:activate_prev_item", |pane: &mut Pane, _: &(), ctx| {
+        pane.activate_prev_item(ctx);
+    });
+    app.add_action("pane:activate_next_item", |pane: &mut Pane, _: &(), ctx| {
+        pane.activate_next_item(ctx);
+    });
+    app.add_action("pane:close_active_item", |pane: &mut Pane, _: &(), ctx| {
+        pane.close_active_item(ctx);
+    });
+    app.add_action("pane:split_up", |pane: &mut Pane, _: &(), ctx| {
+        pane.split(SplitDirection::Up, ctx);
+    });
+    app.add_action("pane:split_down", |pane: &mut Pane, _: &(), ctx| {
+        pane.split(SplitDirection::Down, ctx);
+    });
+    app.add_action("pane:split_left", |pane: &mut Pane, _: &(), ctx| {
+        pane.split(SplitDirection::Left, ctx);
+    });
+    app.add_action("pane:split_right", |pane: &mut Pane, _: &(), ctx| {
+        pane.split(SplitDirection::Right, ctx);
+    });
+
+    app.add_bindings(vec![
+        Binding::new("shift-cmd-{", "pane:activate_prev_item", Some("Pane")),
+        Binding::new("shift-cmd-}", "pane:activate_next_item", Some("Pane")),
+        Binding::new("cmd-w", "pane:close_active_item", Some("Pane")),
+        Binding::new("cmd-k up", "pane:split_up", Some("Pane")),
+        Binding::new("cmd-k down", "pane:split_down", Some("Pane")),
+        Binding::new("cmd-k left", "pane:split_left", Some("Pane")),
+        Binding::new("cmd-k right", "pane:split_right", Some("Pane")),
+    ]);
+}
+
+pub enum Event {
+    Activate,
+    Remove,
+    Split(SplitDirection),
+}
+
+#[derive(Debug, Eq, PartialEq)]
+pub struct State {
+    pub tabs: Vec<TabState>,
+}
+
+#[derive(Debug, Eq, PartialEq)]
+pub struct TabState {
+    pub title: String,
+    pub active: bool,
+}
+
+pub struct Pane {
+    items: Vec<Box<dyn ItemViewHandle>>,
+    active_item: usize,
+    settings: watch::Receiver<Settings>,
+}
+
+impl Pane {
+    pub fn new(settings: watch::Receiver<Settings>) -> Self {
+        Self {
+            items: Vec::new(),
+            active_item: 0,
+            settings,
+        }
+    }
+
+    pub fn activate(&self, ctx: &mut ViewContext<Self>) {
+        ctx.emit(Event::Activate);
+    }
+
+    pub fn add_item(
+        &mut self,
+        item: Box<dyn ItemViewHandle>,
+        ctx: &mut ViewContext<Self>,
+    ) -> usize {
+        let item_idx = cmp::min(self.active_item + 1, self.items.len());
+        self.items.insert(item_idx, item);
+        ctx.notify();
+        item_idx
+    }
+
+    #[cfg(test)]
+    pub fn items(&self) -> &[Box<dyn ItemViewHandle>] {
+        &self.items
+    }
+
+    pub fn active_item(&self) -> Option<Box<dyn ItemViewHandle>> {
+        self.items.get(self.active_item).cloned()
+    }
+
+    pub fn activate_entry(
+        &mut self,
+        entry_id: (usize, usize),
+        ctx: &mut ViewContext<Self>,
+    ) -> bool {
+        if let Some(index) = self
+            .items
+            .iter()
+            .position(|item| item.entry_id(ctx.app()).map_or(false, |id| id == entry_id))
+        {
+            self.activate_item(index, ctx);
+            true
+        } else {
+            false
+        }
+    }
+
+    pub fn item_index(&self, item: &dyn ItemViewHandle) -> Option<usize> {
+        self.items.iter().position(|i| i.id() == item.id())
+    }
+
+    pub fn activate_item(&mut self, index: usize, ctx: &mut ViewContext<Self>) {
+        if index < self.items.len() {
+            self.active_item = index;
+            self.focus_active_item(ctx);
+            ctx.notify();
+        }
+    }
+
+    pub fn activate_prev_item(&mut self, ctx: &mut ViewContext<Self>) {
+        if self.active_item > 0 {
+            self.active_item -= 1;
+        } else {
+            self.active_item = self.items.len() - 1;
+        }
+        self.focus_active_item(ctx);
+        ctx.notify();
+    }
+
+    pub fn activate_next_item(&mut self, ctx: &mut ViewContext<Self>) {
+        if self.active_item + 1 < self.items.len() {
+            self.active_item += 1;
+        } else {
+            self.active_item = 0;
+        }
+        self.focus_active_item(ctx);
+        ctx.notify();
+    }
+
+    pub fn close_active_item(&mut self, ctx: &mut ViewContext<Self>) {
+        if !self.items.is_empty() {
+            self.items.remove(self.active_item);
+            if self.active_item >= self.items.len() {
+                self.active_item = self.items.len().saturating_sub(1);
+            }
+            ctx.notify();
+        }
+        if self.items.is_empty() {
+            ctx.emit(Event::Remove);
+        }
+    }
+
+    fn focus_active_item(&mut self, ctx: &mut ViewContext<Self>) {
+        if let Some(active_item) = self.active_item() {
+            ctx.focus(active_item.to_any());
+        }
+    }
+
+    pub fn split(&mut self, direction: SplitDirection, ctx: &mut ViewContext<Self>) {
+        ctx.emit(Event::Split(direction));
+    }
+
+    fn render_tabs<'a>(&self, app: &AppContext) -> Box<dyn Element> {
+        let settings = smol::block_on(self.settings.read());
+        let border_color = ColorU::new(0xdb, 0xdb, 0xdc, 0xff);
+
+        let mut row = Flex::row();
+        let last_item_ix = self.items.len() - 1;
+        for (ix, item) in self.items.iter().enumerate() {
+            let title = item.title(app);
+
+            let mut border = Border::new(1.0, border_color);
+            border.left = ix > 0;
+            border.right = ix == last_item_ix;
+            border.bottom = ix != self.active_item;
+
+            let mut container = Container::new(
+                Align::new(
+                    Label::new(title, settings.ui_font_family, settings.ui_font_size).boxed(),
+                )
+                .boxed(),
+            )
+            .with_uniform_padding(6.0)
+            .with_border(border);
+
+            if ix == self.active_item {
+                container = container
+                    .with_background_color(ColorU::white())
+                    .with_overdraw_bottom(1.5);
+            } else {
+                container = container.with_background_color(ColorU::new(0xea, 0xea, 0xeb, 0xff));
+            }
+
+            row.add_child(
+                Expanded::new(
+                    1.0,
+                    ConstrainedBox::new(
+                        EventHandler::new(container.boxed())
+                            .on_mouse_down(move |ctx, _| {
+                                ctx.dispatch_action("pane:activate_item", ix);
+                                true
+                            })
+                            .boxed(),
+                    )
+                    .with_max_width(264.0)
+                    .boxed(),
+                )
+                .boxed(),
+            );
+        }
+
+        row.add_child(
+            Expanded::new(
+                1.0,
+                Container::new(
+                    LineBox::new(
+                        settings.ui_font_family,
+                        settings.ui_font_size,
+                        Empty::new().boxed(),
+                    )
+                    .boxed(),
+                )
+                .with_uniform_padding(6.0)
+                .with_border(Border::bottom(1.0, border_color))
+                .boxed(),
+            )
+            .boxed(),
+        );
+
+        row.boxed()
+    }
+}
+
+impl Entity for Pane {
+    type Event = Event;
+}
+
+impl View for Pane {
+    fn ui_name() -> &'static str {
+        "Pane"
+    }
+
+    fn render<'a>(&self, app: &AppContext) -> Box<dyn Element> {
+        if let Some(active_item) = self.active_item() {
+            Flex::column()
+                .with_child(self.render_tabs(app))
+                .with_child(Expanded::new(1.0, ChildView::new(active_item.id()).boxed()).boxed())
+                .boxed()
+        } else {
+            Empty::new().boxed()
+        }
+    }
+
+    fn on_focus(&mut self, ctx: &mut ViewContext<Self>) {
+        self.focus_active_item(ctx);
+    }
+
+    // fn state(&self, app: &AppContext) -> Self::State {
+    //     State {
+    //         tabs: self
+    //             .items
+    //             .iter()
+    //             .enumerate()
+    //             .map(|(idx, item)| TabState {
+    //                 title: item.title(app),
+    //                 active: idx == self.active_item,
+    //             })
+    //             .collect(),
+    //     }
+    // }
+}

zed/src/workspace/pane_group.rs 🔗

@@ -0,0 +1,393 @@
+use anyhow::{anyhow, Result};
+use gpui::{
+    color::{rgbu, ColorU},
+    elements::*,
+    Axis, ChildView,
+};
+
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct PaneGroup {
+    root: Member,
+}
+
+impl PaneGroup {
+    pub fn new(pane_id: usize) -> Self {
+        Self {
+            root: Member::Pane(pane_id),
+        }
+    }
+
+    pub fn split(
+        &mut self,
+        old_pane_id: usize,
+        new_pane_id: usize,
+        direction: SplitDirection,
+    ) -> Result<()> {
+        match &mut self.root {
+            Member::Pane(pane_id) => {
+                if *pane_id == old_pane_id {
+                    self.root = Member::new_axis(old_pane_id, new_pane_id, direction);
+                    Ok(())
+                } else {
+                    Err(anyhow!("Pane not found"))
+                }
+            }
+            Member::Axis(axis) => axis.split(old_pane_id, new_pane_id, direction),
+        }
+    }
+
+    pub fn remove(&mut self, pane_id: usize) -> Result<bool> {
+        match &mut self.root {
+            Member::Pane(_) => Ok(false),
+            Member::Axis(axis) => {
+                if let Some(last_pane) = axis.remove(pane_id)? {
+                    self.root = last_pane;
+                }
+                Ok(true)
+            }
+        }
+    }
+
+    pub fn render<'a>(&self) -> Box<dyn Element> {
+        self.root.render()
+    }
+}
+
+#[derive(Clone, Debug, Eq, PartialEq)]
+enum Member {
+    Axis(PaneAxis),
+    Pane(usize),
+}
+
+impl Member {
+    fn new_axis(old_pane_id: usize, new_pane_id: usize, direction: SplitDirection) -> Self {
+        use Axis::*;
+        use SplitDirection::*;
+
+        let axis = match direction {
+            Up | Down => Vertical,
+            Left | Right => Horizontal,
+        };
+
+        let members = match direction {
+            Up | Left => vec![Member::Pane(new_pane_id), Member::Pane(old_pane_id)],
+            Down | Right => vec![Member::Pane(old_pane_id), Member::Pane(new_pane_id)],
+        };
+
+        Member::Axis(PaneAxis { axis, members })
+    }
+
+    pub fn render<'a>(&self) -> Box<dyn Element> {
+        match self {
+            Member::Pane(view_id) => ChildView::new(*view_id).boxed(),
+            Member::Axis(axis) => axis.render(),
+        }
+    }
+}
+
+#[derive(Clone, Debug, Eq, PartialEq)]
+struct PaneAxis {
+    axis: Axis,
+    members: Vec<Member>,
+}
+
+impl PaneAxis {
+    fn split(
+        &mut self,
+        old_pane_id: usize,
+        new_pane_id: usize,
+        direction: SplitDirection,
+    ) -> Result<()> {
+        use SplitDirection::*;
+
+        for (idx, member) in self.members.iter_mut().enumerate() {
+            match member {
+                Member::Axis(axis) => {
+                    if axis.split(old_pane_id, new_pane_id, direction).is_ok() {
+                        return Ok(());
+                    }
+                }
+                Member::Pane(pane_id) => {
+                    if *pane_id == old_pane_id {
+                        if direction.matches_axis(self.axis) {
+                            match direction {
+                                Up | Left => {
+                                    self.members.insert(idx, Member::Pane(new_pane_id));
+                                }
+                                Down | Right => {
+                                    self.members.insert(idx + 1, Member::Pane(new_pane_id));
+                                }
+                            }
+                        } else {
+                            *member = Member::new_axis(old_pane_id, new_pane_id, direction);
+                        }
+                        return Ok(());
+                    }
+                }
+            }
+        }
+        Err(anyhow!("Pane not found"))
+    }
+
+    fn remove(&mut self, pane_id_to_remove: usize) -> Result<Option<Member>> {
+        let mut found_pane = false;
+        let mut remove_member = None;
+        for (idx, member) in self.members.iter_mut().enumerate() {
+            match member {
+                Member::Axis(axis) => {
+                    if let Ok(last_pane) = axis.remove(pane_id_to_remove) {
+                        if let Some(last_pane) = last_pane {
+                            *member = last_pane;
+                        }
+                        found_pane = true;
+                        break;
+                    }
+                }
+                Member::Pane(pane_id) => {
+                    if *pane_id == pane_id_to_remove {
+                        found_pane = true;
+                        remove_member = Some(idx);
+                        break;
+                    }
+                }
+            }
+        }
+
+        if found_pane {
+            if let Some(idx) = remove_member {
+                self.members.remove(idx);
+            }
+
+            if self.members.len() == 1 {
+                Ok(self.members.pop())
+            } else {
+                Ok(None)
+            }
+        } else {
+            Err(anyhow!("Pane not found"))
+        }
+    }
+
+    fn render<'a>(&self) -> Box<dyn Element> {
+        let last_member_ix = self.members.len() - 1;
+        Flex::new(self.axis)
+            .with_children(self.members.iter().enumerate().map(|(ix, member)| {
+                let mut member = member.render();
+                if ix < last_member_ix {
+                    let mut border = Border::new(border_width(), border_color());
+                    match self.axis {
+                        Axis::Vertical => border.bottom = true,
+                        Axis::Horizontal => border.right = true,
+                    }
+                    member = Container::new(member).with_border(border).boxed();
+                }
+
+                Expanded::new(1.0, member).boxed()
+            }))
+            .boxed()
+    }
+}
+
+#[derive(Clone, Copy)]
+pub enum SplitDirection {
+    Up,
+    Down,
+    Left,
+    Right,
+}
+
+impl SplitDirection {
+    fn matches_axis(self, orientation: Axis) -> bool {
+        use Axis::*;
+        use SplitDirection::*;
+
+        match self {
+            Up | Down => match orientation {
+                Vertical => true,
+                Horizontal => false,
+            },
+            Left | Right => match orientation {
+                Vertical => false,
+                Horizontal => true,
+            },
+        }
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    // use super::*;
+    // use serde_json::json;
+
+    // #[test]
+    // fn test_split_and_remove() -> Result<()> {
+    //     let mut group = PaneGroup::new(1);
+    //     assert_eq!(
+    //         serde_json::to_value(&group)?,
+    //         json!({
+    //             "type": "pane",
+    //             "paneId": 1,
+    //         })
+    //     );
+
+    //     group.split(1, 2, SplitDirection::Right)?;
+    //     assert_eq!(
+    //         serde_json::to_value(&group)?,
+    //         json!({
+    //             "type": "axis",
+    //             "orientation": "horizontal",
+    //             "members": [
+    //                 {"type": "pane", "paneId": 1},
+    //                 {"type": "pane", "paneId": 2},
+    //             ]
+    //         })
+    //     );
+
+    //     group.split(2, 3, SplitDirection::Up)?;
+    //     assert_eq!(
+    //         serde_json::to_value(&group)?,
+    //         json!({
+    //             "type": "axis",
+    //             "orientation": "horizontal",
+    //             "members": [
+    //                 {"type": "pane", "paneId": 1},
+    //                 {
+    //                     "type": "axis",
+    //                     "orientation": "vertical",
+    //                     "members": [
+    //                         {"type": "pane", "paneId": 3},
+    //                         {"type": "pane", "paneId": 2},
+    //                     ]
+    //                 },
+    //             ]
+    //         })
+    //     );
+
+    //     group.split(1, 4, SplitDirection::Right)?;
+    //     assert_eq!(
+    //         serde_json::to_value(&group)?,
+    //         json!({
+    //             "type": "axis",
+    //             "orientation": "horizontal",
+    //             "members": [
+    //                 {"type": "pane", "paneId": 1},
+    //                 {"type": "pane", "paneId": 4},
+    //                 {
+    //                     "type": "axis",
+    //                     "orientation": "vertical",
+    //                     "members": [
+    //                         {"type": "pane", "paneId": 3},
+    //                         {"type": "pane", "paneId": 2},
+    //                     ]
+    //                 },
+    //             ]
+    //         })
+    //     );
+
+    //     group.split(2, 5, SplitDirection::Up)?;
+    //     assert_eq!(
+    //         serde_json::to_value(&group)?,
+    //         json!({
+    //             "type": "axis",
+    //             "orientation": "horizontal",
+    //             "members": [
+    //                 {"type": "pane", "paneId": 1},
+    //                 {"type": "pane", "paneId": 4},
+    //                 {
+    //                     "type": "axis",
+    //                     "orientation": "vertical",
+    //                     "members": [
+    //                         {"type": "pane", "paneId": 3},
+    //                         {"type": "pane", "paneId": 5},
+    //                         {"type": "pane", "paneId": 2},
+    //                     ]
+    //                 },
+    //             ]
+    //         })
+    //     );
+
+    //     assert_eq!(true, group.remove(5)?);
+    //     assert_eq!(
+    //         serde_json::to_value(&group)?,
+    //         json!({
+    //             "type": "axis",
+    //             "orientation": "horizontal",
+    //             "members": [
+    //                 {"type": "pane", "paneId": 1},
+    //                 {"type": "pane", "paneId": 4},
+    //                 {
+    //                     "type": "axis",
+    //                     "orientation": "vertical",
+    //                     "members": [
+    //                         {"type": "pane", "paneId": 3},
+    //                         {"type": "pane", "paneId": 2},
+    //                     ]
+    //                 },
+    //             ]
+    //         })
+    //     );
+
+    //     assert_eq!(true, group.remove(4)?);
+    //     assert_eq!(
+    //         serde_json::to_value(&group)?,
+    //         json!({
+    //             "type": "axis",
+    //             "orientation": "horizontal",
+    //             "members": [
+    //                 {"type": "pane", "paneId": 1},
+    //                 {
+    //                     "type": "axis",
+    //                     "orientation": "vertical",
+    //                     "members": [
+    //                         {"type": "pane", "paneId": 3},
+    //                         {"type": "pane", "paneId": 2},
+    //                     ]
+    //                 },
+    //             ]
+    //         })
+    //     );
+
+    //     assert_eq!(true, group.remove(3)?);
+    //     assert_eq!(
+    //         serde_json::to_value(&group)?,
+    //         json!({
+    //             "type": "axis",
+    //             "orientation": "horizontal",
+    //             "members": [
+    //                 {"type": "pane", "paneId": 1},
+    //                 {"type": "pane", "paneId": 2},
+    //             ]
+    //         })
+    //     );
+
+    //     assert_eq!(true, group.remove(2)?);
+    //     assert_eq!(
+    //         serde_json::to_value(&group)?,
+    //         json!({
+    //             "type": "pane",
+    //             "paneId": 1,
+    //         })
+    //     );
+
+    //     assert_eq!(false, group.remove(1)?);
+    //     assert_eq!(
+    //         serde_json::to_value(&group)?,
+    //         json!({
+    //             "type": "pane",
+    //             "paneId": 1,
+    //         })
+    //     );
+
+    //     Ok(())
+    // }
+}
+
+#[inline(always)]
+fn border_width() -> f32 {
+    2.0
+}
+
+#[inline(always)]
+fn border_color() -> ColorU {
+    rgbu(0xdb, 0xdb, 0xdc)
+}

zed/src/workspace/workspace.rs 🔗

@@ -0,0 +1,271 @@
+use super::{ItemView, ItemViewHandle};
+use crate::{
+    editor::Buffer,
+    settings::Settings,
+    time::ReplicaId,
+    watch,
+    worktree::{Worktree, WorktreeHandle as _},
+};
+use anyhow::anyhow;
+use gpui::{
+    App, AppContext, Entity, Handle, ModelContext, ModelHandle, MutableAppContext, ViewContext,
+};
+use smol::prelude::*;
+use std::{
+    collections::{HashMap, HashSet},
+    fmt::Debug,
+    path::{Path, PathBuf},
+    pin::Pin,
+    sync::Arc,
+};
+
+pub trait Item
+where
+    Self: Sized,
+{
+    type View: ItemView;
+    fn build_view(
+        handle: ModelHandle<Self>,
+        settings: watch::Receiver<Settings>,
+        ctx: &mut ViewContext<Self::View>,
+    ) -> Self::View;
+}
+
+pub trait ItemHandle: Debug + Send + Sync {
+    fn add_view(
+        &self,
+        window_id: usize,
+        settings: watch::Receiver<Settings>,
+        app: &mut MutableAppContext,
+    ) -> Box<dyn ItemViewHandle>;
+    fn id(&self) -> usize;
+    fn boxed_clone(&self) -> Box<dyn ItemHandle>;
+}
+
+impl<T: 'static + Item> ItemHandle for ModelHandle<T> {
+    fn add_view(
+        &self,
+        window_id: usize,
+        settings: watch::Receiver<Settings>,
+        app: &mut MutableAppContext,
+    ) -> Box<dyn ItemViewHandle> {
+        Box::new(app.add_view(window_id, |ctx| T::build_view(self.clone(), settings, ctx)))
+    }
+
+    fn id(&self) -> usize {
+        Handle::id(self)
+    }
+
+    fn boxed_clone(&self) -> Box<dyn ItemHandle> {
+        Box::new(self.clone())
+    }
+}
+
+impl Clone for Box<dyn ItemHandle> {
+    fn clone(&self) -> Self {
+        self.boxed_clone()
+    }
+}
+
+pub type OpenResult = Result<Box<dyn ItemHandle>, Arc<anyhow::Error>>;
+
+#[derive(Clone)]
+enum OpenedItem {
+    Loading(watch::Receiver<Option<OpenResult>>),
+    Loaded(Box<dyn ItemHandle>),
+}
+
+pub struct Workspace {
+    replica_id: ReplicaId,
+    worktrees: HashSet<ModelHandle<Worktree>>,
+    items: HashMap<(usize, usize), OpenedItem>,
+}
+
+impl Workspace {
+    pub fn new(paths: Vec<PathBuf>, ctx: &mut ModelContext<Self>) -> Self {
+        let mut workspace = Self {
+            replica_id: 0,
+            worktrees: HashSet::new(),
+            items: HashMap::new(),
+        };
+        workspace.open_paths(&paths, ctx);
+        workspace
+    }
+
+    pub fn worktrees(&self) -> &HashSet<ModelHandle<Worktree>> {
+        &self.worktrees
+    }
+
+    pub fn contains_paths(&self, paths: &[PathBuf], app: &AppContext) -> bool {
+        paths.iter().all(|path| self.contains_path(&path, app))
+    }
+
+    pub fn contains_path(&self, path: &Path, app: &AppContext) -> bool {
+        self.worktrees
+            .iter()
+            .any(|worktree| worktree.as_ref(app).contains_path(path))
+    }
+
+    pub fn open_paths(&mut self, paths: &[PathBuf], ctx: &mut ModelContext<Self>) {
+        for path in paths.iter().cloned() {
+            self.open_path(path, ctx);
+        }
+    }
+
+    pub fn open_path<'a>(&'a mut self, path: PathBuf, ctx: &mut ModelContext<Self>) {
+        for tree in self.worktrees.iter() {
+            if tree.as_ref(ctx).contains_path(&path) {
+                return;
+            }
+        }
+
+        let worktree = ctx.add_model(|ctx| Worktree::new(ctx.model_id(), path, Some(ctx)));
+        ctx.observe(&worktree, Self::on_worktree_updated);
+        self.worktrees.insert(worktree);
+        ctx.notify();
+    }
+
+    pub fn open_entry(
+        &mut self,
+        entry: (usize, usize),
+        ctx: &mut ModelContext<'_, Self>,
+    ) -> anyhow::Result<Pin<Box<dyn Future<Output = OpenResult> + Send>>> {
+        if let Some(item) = self.items.get(&entry).cloned() {
+            return Ok(async move {
+                match item {
+                    OpenedItem::Loaded(handle) => {
+                        return Ok(handle);
+                    }
+                    OpenedItem::Loading(rx) => loop {
+                        rx.updated().await;
+
+                        if let Some(result) = smol::block_on(rx.read()).clone() {
+                            return result;
+                        }
+                    },
+                }
+            }
+            .boxed());
+        }
+
+        let worktree = self
+            .worktrees
+            .get(&entry.0)
+            .cloned()
+            .ok_or(anyhow!("worktree {} does not exist", entry.0,))?;
+
+        let replica_id = self.replica_id;
+        let file = worktree.file(entry.1, ctx.app())?;
+        let history = file.load_history(ctx.app());
+        let buffer = async move { Ok(Buffer::from_history(replica_id, file, history.await?)) };
+
+        let (mut tx, rx) = watch::channel(None);
+        self.items.insert(entry, OpenedItem::Loading(rx));
+        let _ = ctx.spawn(
+            buffer,
+            move |me, buffer: anyhow::Result<Buffer>, ctx| match buffer {
+                Ok(buffer) => {
+                    let handle = Box::new(ctx.add_model(|_| buffer)) as Box<dyn ItemHandle>;
+                    me.items.insert(entry, OpenedItem::Loaded(handle.clone()));
+                    let _ = ctx.spawn(
+                        async move {
+                            tx.update(|value| *value = Some(Ok(handle))).await;
+                        },
+                        |_, _, _| {},
+                    );
+                }
+                Err(error) => {
+                    let _ = ctx.spawn(
+                        async move {
+                            tx.update(|value| *value = Some(Err(Arc::new(error)))).await;
+                        },
+                        |_, _, _| {},
+                    );
+                }
+            },
+        );
+
+        self.open_entry(entry, ctx)
+    }
+
+    fn on_worktree_updated(&mut self, _: ModelHandle<Worktree>, ctx: &mut ModelContext<Self>) {
+        ctx.notify();
+    }
+}
+
+impl Entity for Workspace {
+    type Event = ();
+}
+
+#[cfg(test)]
+pub trait WorkspaceHandle {
+    fn file_entries(&self, app: &App) -> Vec<(usize, usize)>;
+}
+
+#[cfg(test)]
+impl WorkspaceHandle for ModelHandle<Workspace> {
+    fn file_entries(&self, app: &App) -> Vec<(usize, usize)> {
+        self.read(&app, |w, app| {
+            w.worktrees()
+                .iter()
+                .flat_map(|tree| {
+                    let tree_id = tree.id();
+                    tree.as_ref(app)
+                        .files()
+                        .map(move |file| (tree_id, file.entry_id))
+                })
+                .collect::<Vec<_>>()
+        })
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::test::temp_tree;
+    use gpui::App;
+    use serde_json::json;
+
+    #[test]
+    fn test_open_entry() -> Result<(), Arc<anyhow::Error>> {
+        App::test(|mut app| async move {
+            let dir = temp_tree(json!({
+                "a": {
+                    "aa": "aa contents",
+                    "ab": "ab contents",
+                },
+            }));
+
+            let workspace = app.add_model(|ctx| Workspace::new(vec![dir.path().into()], ctx));
+            app.finish_pending_tasks().await; // Open and populate worktree.
+
+            // Get the first file entry.
+            let entry = workspace.read(&app, |w, app| {
+                let tree = w.worktrees.iter().next().unwrap();
+                let entry_id = tree.as_ref(app).files().next().unwrap().entry_id;
+                (tree.id(), entry_id)
+            });
+
+            // Open the same entry twice before it finishes loading.
+            let (future_1, future_2) = workspace.update(&mut app, |w, app| {
+                (
+                    w.open_entry(entry, app).unwrap(),
+                    w.open_entry(entry, app).unwrap(),
+                )
+            });
+
+            let handle_1 = future_1.await?;
+            let handle_2 = future_2.await?;
+            assert_eq!(handle_1.id(), handle_2.id());
+
+            // Open the same entry again now that it has loaded
+            let handle_3 = workspace
+                .update(&mut app, |w, app| w.open_entry(entry, app).unwrap())
+                .await?;
+
+            assert_eq!(handle_3.id(), handle_1.id());
+
+            Ok(())
+        })
+    }
+}

zed/src/workspace/workspace_view.rs 🔗

@@ -0,0 +1,444 @@
+use super::{pane, Pane, PaneGroup, SplitDirection, Workspace};
+use crate::{settings::Settings, watch};
+use gpui::{color::rgbu, ChildView};
+use gpui::{
+    elements::*, AnyViewHandle, AppContext, Entity, ModelHandle, MutableAppContext, View,
+    ViewContext, ViewHandle,
+};
+use log::{error, info};
+use std::{collections::HashSet, path::PathBuf};
+
+pub trait ItemView: View {
+    fn is_activate_event(event: &Self::Event) -> bool;
+    fn title(&self, app: &AppContext) -> String;
+    fn entry_id(&self, app: &AppContext) -> Option<(usize, usize)>;
+    fn clone_on_split(&self, _: &mut ViewContext<Self>) -> Option<Self>
+    where
+        Self: Sized,
+    {
+        None
+    }
+}
+
+pub trait ItemViewHandle: Send + Sync {
+    fn title(&self, app: &AppContext) -> String;
+    fn entry_id(&self, app: &AppContext) -> Option<(usize, usize)>;
+    fn boxed_clone(&self) -> Box<dyn ItemViewHandle>;
+    fn clone_on_split(&self, app: &mut MutableAppContext) -> Option<Box<dyn ItemViewHandle>>;
+    fn set_parent_pane(&self, pane: &ViewHandle<Pane>, app: &mut MutableAppContext);
+    fn id(&self) -> usize;
+    fn to_any(&self) -> AnyViewHandle;
+}
+
+impl<T: ItemView> ItemViewHandle for ViewHandle<T> {
+    fn title(&self, app: &AppContext) -> String {
+        self.as_ref(app).title(app)
+    }
+
+    fn entry_id(&self, app: &AppContext) -> Option<(usize, usize)> {
+        self.as_ref(app).entry_id(app)
+    }
+
+    fn boxed_clone(&self) -> Box<dyn ItemViewHandle> {
+        Box::new(self.clone())
+    }
+
+    fn clone_on_split(&self, app: &mut MutableAppContext) -> Option<Box<dyn ItemViewHandle>> {
+        self.update(app, |item, ctx| {
+            ctx.add_option_view(|ctx| item.clone_on_split(ctx))
+        })
+        .map(|handle| Box::new(handle) as Box<dyn ItemViewHandle>)
+    }
+
+    fn set_parent_pane(&self, pane: &ViewHandle<Pane>, app: &mut MutableAppContext) {
+        pane.update(app, |_, ctx| {
+            ctx.subscribe_to_view(self, |pane, item, event, ctx| {
+                if T::is_activate_event(event) {
+                    if let Some(ix) = pane.item_index(&item) {
+                        pane.activate_item(ix, ctx);
+                        pane.activate(ctx);
+                    }
+                }
+            })
+        })
+    }
+
+    fn id(&self) -> usize {
+        self.id()
+    }
+
+    fn to_any(&self) -> AnyViewHandle {
+        self.into()
+    }
+}
+
+impl Clone for Box<dyn ItemViewHandle> {
+    fn clone(&self) -> Box<dyn ItemViewHandle> {
+        self.boxed_clone()
+    }
+}
+
+#[derive(Debug)]
+pub struct State {
+    pub modal: Option<usize>,
+    pub center: PaneGroup,
+}
+
+pub struct WorkspaceView {
+    pub workspace: ModelHandle<Workspace>,
+    pub settings: watch::Receiver<Settings>,
+    modal: Option<AnyViewHandle>,
+    center: PaneGroup,
+    panes: Vec<ViewHandle<Pane>>,
+    active_pane: ViewHandle<Pane>,
+    loading_entries: HashSet<(usize, usize)>,
+}
+
+impl WorkspaceView {
+    pub fn new(
+        workspace: ModelHandle<Workspace>,
+        settings: watch::Receiver<Settings>,
+        ctx: &mut ViewContext<Self>,
+    ) -> Self {
+        ctx.observe(&workspace, Self::workspace_updated);
+
+        let pane = ctx.add_view(|_| Pane::new(settings.clone()));
+        let pane_id = pane.id();
+        ctx.subscribe_to_view(&pane, move |me, _, event, ctx| {
+            me.handle_pane_event(pane_id, event, ctx)
+        });
+        ctx.focus(&pane);
+
+        WorkspaceView {
+            workspace,
+            modal: None,
+            center: PaneGroup::new(pane.id()),
+            panes: vec![pane.clone()],
+            active_pane: pane.clone(),
+            loading_entries: HashSet::new(),
+            settings,
+        }
+    }
+
+    pub fn contains_paths(&self, paths: &[PathBuf], app: &AppContext) -> bool {
+        self.workspace.as_ref(app).contains_paths(paths, app)
+    }
+
+    pub fn open_paths(&self, paths: &[PathBuf], app: &mut MutableAppContext) {
+        self.workspace
+            .update(app, |workspace, ctx| workspace.open_paths(paths, ctx));
+    }
+
+    pub fn toggle_modal<V, F>(&mut self, ctx: &mut ViewContext<Self>, add_view: F)
+    where
+        V: 'static + View,
+        F: FnOnce(&mut ViewContext<Self>, &mut Self) -> ViewHandle<V>,
+    {
+        if self.modal.as_ref().map_or(false, |modal| modal.is::<V>()) {
+            self.modal.take();
+            ctx.focus_self();
+        } else {
+            let modal = add_view(ctx, self);
+            ctx.focus(&modal);
+            self.modal = Some(modal.into());
+        }
+        ctx.notify();
+    }
+
+    pub fn modal(&self) -> Option<&AnyViewHandle> {
+        self.modal.as_ref()
+    }
+
+    pub fn dismiss_modal(&mut self, ctx: &mut ViewContext<Self>) {
+        if self.modal.take().is_some() {
+            ctx.focus(&self.active_pane);
+            ctx.notify();
+        }
+    }
+
+    pub fn open_entry(&mut self, entry: (usize, usize), ctx: &mut ViewContext<Self>) {
+        if self.loading_entries.contains(&entry) {
+            return;
+        }
+
+        if self
+            .active_pane()
+            .update(ctx, |pane, ctx| pane.activate_entry(entry, ctx))
+        {
+            return;
+        }
+
+        self.loading_entries.insert(entry);
+
+        match self
+            .workspace
+            .update(ctx, |workspace, ctx| workspace.open_entry(entry, ctx))
+        {
+            Err(error) => error!("{}", error),
+            Ok(item) => {
+                let settings = self.settings.clone();
+                let _ = ctx.spawn(item, move |me, item, ctx| {
+                    me.loading_entries.remove(&entry);
+                    match item {
+                        Ok(item) => {
+                            let item_view = item.add_view(ctx.window_id(), settings, ctx.app_mut());
+                            me.add_item(item_view, ctx);
+                        }
+                        Err(error) => {
+                            error!("{}", error);
+                        }
+                    }
+                });
+            }
+        }
+    }
+
+    pub fn open_example_entry(&mut self, ctx: &mut ViewContext<Self>) {
+        if let Some(tree) = self.workspace.as_ref(ctx).worktrees().iter().next() {
+            if let Some(file) = tree.as_ref(ctx).files().next() {
+                info!("open_entry ({}, {})", tree.id(), file.entry_id);
+                self.open_entry((tree.id(), file.entry_id), ctx);
+            } else {
+                error!("No example file found for worktree {}", tree.id());
+            }
+        } else {
+            error!("No worktree found while opening example entry");
+        }
+    }
+
+    fn workspace_updated(&mut self, _: ModelHandle<Workspace>, ctx: &mut ViewContext<Self>) {
+        ctx.notify();
+    }
+
+    fn add_pane(&mut self, ctx: &mut ViewContext<Self>) -> ViewHandle<Pane> {
+        let pane = ctx.add_view(|_| Pane::new(self.settings.clone()));
+        let pane_id = pane.id();
+        ctx.subscribe_to_view(&pane, move |me, _, event, ctx| {
+            me.handle_pane_event(pane_id, event, ctx)
+        });
+        self.panes.push(pane.clone());
+        self.activate_pane(pane.clone(), ctx);
+        pane
+    }
+
+    fn activate_pane(&mut self, pane: ViewHandle<Pane>, ctx: &mut ViewContext<Self>) {
+        self.active_pane = pane;
+        ctx.focus(&self.active_pane);
+        ctx.notify();
+    }
+
+    fn handle_pane_event(
+        &mut self,
+        pane_id: usize,
+        event: &pane::Event,
+        ctx: &mut ViewContext<Self>,
+    ) {
+        if let Some(pane) = self.pane(pane_id) {
+            match event {
+                pane::Event::Split(direction) => {
+                    self.split_pane(pane, *direction, ctx);
+                }
+                pane::Event::Remove => {
+                    self.remove_pane(pane, ctx);
+                }
+                pane::Event::Activate => {
+                    self.activate_pane(pane, ctx);
+                }
+            }
+        } else {
+            error!("pane {} not found", pane_id);
+        }
+    }
+
+    fn split_pane(
+        &mut self,
+        pane: ViewHandle<Pane>,
+        direction: SplitDirection,
+        ctx: &mut ViewContext<Self>,
+    ) -> ViewHandle<Pane> {
+        let new_pane = self.add_pane(ctx);
+        self.activate_pane(new_pane.clone(), ctx);
+        if let Some(item) = pane.as_ref(ctx).active_item() {
+            if let Some(clone) = item.clone_on_split(ctx.app_mut()) {
+                self.add_item(clone, ctx);
+            }
+        }
+        self.center
+            .split(pane.id(), new_pane.id(), direction)
+            .unwrap();
+        ctx.notify();
+        new_pane
+    }
+
+    fn remove_pane(&mut self, pane: ViewHandle<Pane>, ctx: &mut ViewContext<Self>) {
+        if self.center.remove(pane.id()).unwrap() {
+            self.panes.retain(|p| p != &pane);
+            self.activate_pane(self.panes.last().unwrap().clone(), ctx);
+        }
+    }
+
+    fn pane(&self, pane_id: usize) -> Option<ViewHandle<Pane>> {
+        self.panes.iter().find(|pane| pane.id() == pane_id).cloned()
+    }
+
+    pub fn active_pane(&self) -> &ViewHandle<Pane> {
+        &self.active_pane
+    }
+
+    fn add_item(&self, item: Box<dyn ItemViewHandle>, ctx: &mut ViewContext<Self>) {
+        let active_pane = self.active_pane();
+        item.set_parent_pane(&active_pane, ctx.app_mut());
+        active_pane.update(ctx, |pane, ctx| {
+            let item_idx = pane.add_item(item, ctx);
+            pane.activate_item(item_idx, ctx);
+        });
+    }
+}
+
+impl Entity for WorkspaceView {
+    type Event = ();
+}
+
+impl View for WorkspaceView {
+    fn ui_name() -> &'static str {
+        "Workspace"
+    }
+
+    fn render(&self, _: &AppContext) -> Box<dyn Element> {
+        Container::new(
+            // self.center.render(bump)
+            Stack::new()
+                .with_child(self.center.render())
+                .with_children(self.modal.as_ref().map(|m| ChildView::new(m.id()).boxed()))
+                .boxed(),
+        )
+        .with_background_color(rgbu(0xea, 0xea, 0xeb))
+        .boxed()
+    }
+
+    fn on_focus(&mut self, ctx: &mut ViewContext<Self>) {
+        ctx.focus(&self.active_pane);
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::{pane, Workspace, WorkspaceView};
+    use crate::{settings, test::temp_tree, workspace::WorkspaceHandle as _};
+    use anyhow::Result;
+    use gpui::{App, FontCache};
+    use serde_json::json;
+
+    #[test]
+    fn test_open_entry() -> Result<()> {
+        App::test(|mut app| async move {
+            let dir = temp_tree(json!({
+                "a": {
+                    "aa": "aa contents",
+                    "ab": "ab contents",
+                    "ac": "ab contents",
+                },
+            }));
+
+            let settings = settings::channel(&FontCache::new()).unwrap().1;
+            let workspace = app.add_model(|ctx| Workspace::new(vec![dir.path().into()], ctx));
+            app.finish_pending_tasks().await; // Open and populate worktree.
+            let entries = workspace.file_entries(&app);
+
+            let (_, workspace_view) =
+                app.add_window(|ctx| WorkspaceView::new(workspace.clone(), settings, ctx));
+
+            // Open the first entry
+            workspace_view.update(&mut app, |w, ctx| w.open_entry(entries[0], ctx));
+            app.finish_pending_tasks().await;
+
+            workspace_view.read(&app, |w, app| {
+                assert_eq!(w.active_pane().as_ref(app).items().len(), 1);
+            });
+
+            // Open the second entry
+            workspace_view.update(&mut app, |w, ctx| w.open_entry(entries[1], ctx));
+            app.finish_pending_tasks().await;
+
+            workspace_view.read(&app, |w, app| {
+                let active_pane = w.active_pane().as_ref(app);
+                assert_eq!(active_pane.items().len(), 2);
+                assert_eq!(
+                    active_pane.active_item().unwrap().entry_id(app),
+                    Some(entries[1])
+                );
+            });
+
+            // Open the first entry again
+            workspace_view.update(&mut app, |w, ctx| w.open_entry(entries[0], ctx));
+            app.finish_pending_tasks().await;
+
+            workspace_view.read(&app, |w, app| {
+                let active_pane = w.active_pane().as_ref(app);
+                assert_eq!(active_pane.items().len(), 2);
+                assert_eq!(
+                    active_pane.active_item().unwrap().entry_id(app),
+                    Some(entries[0])
+                );
+            });
+
+            // Open the third entry twice concurrently
+            workspace_view.update(&mut app, |w, ctx| {
+                w.open_entry(entries[2], ctx);
+                w.open_entry(entries[2], ctx);
+            });
+            app.finish_pending_tasks().await;
+
+            workspace_view.read(&app, |w, app| {
+                assert_eq!(w.active_pane().as_ref(app).items().len(), 3);
+            });
+
+            Ok(())
+        })
+    }
+
+    #[test]
+    fn test_pane_actions() -> Result<()> {
+        App::test(|mut app| async move {
+            pane::init(&mut app);
+
+            let dir = temp_tree(json!({
+                "a": {
+                    "aa": "aa contents",
+                    "ab": "ab contents",
+                    "ac": "ab contents",
+                },
+            }));
+
+            let settings = settings::channel(&FontCache::new()).unwrap().1;
+            let workspace = app.add_model(|ctx| Workspace::new(vec![dir.path().into()], ctx));
+            app.finish_pending_tasks().await; // Open and populate worktree.
+            let entries = workspace.file_entries(&app);
+
+            let (window_id, workspace_view) =
+                app.add_window(|ctx| WorkspaceView::new(workspace.clone(), settings, ctx));
+
+            workspace_view.update(&mut app, |w, ctx| w.open_entry(entries[0], ctx));
+            app.finish_pending_tasks().await;
+
+            let pane_1 = workspace_view.read(&app, |w, _| w.active_pane().clone());
+
+            app.dispatch_action(window_id, vec![pane_1.id()], "pane:split_right", ());
+            let pane_2 = workspace_view.read(&app, |w, _| w.active_pane().clone());
+            assert_ne!(pane_1, pane_2);
+
+            pane_2.read(&app, |p, app| {
+                assert_eq!(p.active_item().unwrap().entry_id(app), Some(entries[0]));
+            });
+
+            app.dispatch_action(window_id, vec![pane_2.id()], "pane:close_active_item", ());
+
+            workspace_view.read(&app, |w, _| {
+                assert_eq!(w.panes.len(), 1);
+                assert_eq!(w.active_pane(), &pane_1)
+            });
+
+            Ok(())
+        })
+    }
+}

zed/src/worktree/mod.rs 🔗

@@ -2,4 +2,4 @@ mod char_bag;
 mod fuzzy;
 mod worktree;
 
-pub use worktree::{match_paths, FileHandle, PathMatch, Worktree};
+pub use worktree::{match_paths, FileHandle, PathMatch, Worktree, WorktreeHandle};