diff --git a/Cargo.lock b/Cargo.lock index d798682be9..5b013463fd 100644 --- a/Cargo.lock +++ b/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", ] diff --git a/gpui/src/app.rs b/gpui/src/app.rs index 75fef19094..798e152e68 100644 --- a/gpui/src/app.rs +++ b/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 { self.0.borrow().finish_pending_tasks() } @@ -1036,7 +1035,6 @@ impl MutableAppContext { .detach() } - #[cfg(test)] pub fn finish_pending_tasks(&self) -> impl Future { let mut pending_tasks = self.task_callbacks.keys().cloned().collect::>(); let task_done = self.task_done.1.clone(); diff --git a/zed/Cargo.toml b/zed/Cargo.toml index c473c263af..1dadf4c9ca 100644 --- a/zed/Cargo.toml +++ b/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" diff --git a/zed/src/editor/buffer_view.rs b/zed/src/editor/buffer_view.rs index 5fd6c0bf3c..28513f70d7 100644 --- a/zed/src/editor/buffer_view.rs +++ b/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, + settings: watch::Receiver, + ctx: &mut ViewContext, + ) -> 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) -> Option -// 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) -> Option + 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 { diff --git a/zed/src/lib.rs b/zed/src/lib.rs index 2c9a4ca283..7cc43687ec 100644 --- a/zed/src/lib.rs +++ b/zed/src/lib.rs @@ -8,4 +8,5 @@ mod time; mod timer; mod util; mod watch; +mod workspace; mod worktree; diff --git a/zed/src/test.rs b/zed/src/test.rs index 59248bfdd5..7e2d80efe0 100644 --- a/zed/src/test.rs +++ b/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") + } +} diff --git a/zed/src/workspace/mod.rs b/zed/src/workspace/mod.rs new file mode 100644 index 0000000000..d8dd0d3575 --- /dev/null +++ b/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, + pub settings: watch::Receiver, +} + +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::>() { + if let Some(handle) = app.root_view::(window_id) { + if handle.update(app, |view, ctx| { + if view.contains_paths(¶ms.paths, ctx.app()) { + view.open_paths(¶ms.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::(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); + }); + } +} diff --git a/zed/src/workspace/pane.rs b/zed/src/workspace/pane.rs new file mode 100644 index 0000000000..5857ef6cfc --- /dev/null +++ b/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, +} + +#[derive(Debug, Eq, PartialEq)] +pub struct TabState { + pub title: String, + pub active: bool, +} + +pub struct Pane { + items: Vec>, + active_item: usize, + settings: watch::Receiver, +} + +impl Pane { + pub fn new(settings: watch::Receiver) -> Self { + Self { + items: Vec::new(), + active_item: 0, + settings, + } + } + + pub fn activate(&self, ctx: &mut ViewContext) { + ctx.emit(Event::Activate); + } + + pub fn add_item( + &mut self, + item: Box, + ctx: &mut ViewContext, + ) -> 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] { + &self.items + } + + pub fn active_item(&self) -> Option> { + self.items.get(self.active_item).cloned() + } + + pub fn activate_entry( + &mut self, + entry_id: (usize, usize), + ctx: &mut ViewContext, + ) -> 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 { + self.items.iter().position(|i| i.id() == item.id()) + } + + pub fn activate_item(&mut self, index: usize, ctx: &mut ViewContext) { + 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) { + 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) { + 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) { + 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) { + if let Some(active_item) = self.active_item() { + ctx.focus(active_item.to_any()); + } + } + + pub fn split(&mut self, direction: SplitDirection, ctx: &mut ViewContext) { + ctx.emit(Event::Split(direction)); + } + + fn render_tabs<'a>(&self, app: &AppContext) -> Box { + 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 { + 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.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(), + // } + // } +} diff --git a/zed/src/workspace/pane_group.rs b/zed/src/workspace/pane_group.rs new file mode 100644 index 0000000000..5a3e3df09f --- /dev/null +++ b/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 { + 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 { + 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 { + 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, +} + +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> { + 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 { + 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) +} diff --git a/zed/src/workspace/workspace.rs b/zed/src/workspace/workspace.rs new file mode 100644 index 0000000000..2cbb39d61f --- /dev/null +++ b/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, + settings: watch::Receiver, + ctx: &mut ViewContext, + ) -> Self::View; +} + +pub trait ItemHandle: Debug + Send + Sync { + fn add_view( + &self, + window_id: usize, + settings: watch::Receiver, + app: &mut MutableAppContext, + ) -> Box; + fn id(&self) -> usize; + fn boxed_clone(&self) -> Box; +} + +impl ItemHandle for ModelHandle { + fn add_view( + &self, + window_id: usize, + settings: watch::Receiver, + app: &mut MutableAppContext, + ) -> Box { + 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 { + Box::new(self.clone()) + } +} + +impl Clone for Box { + fn clone(&self) -> Self { + self.boxed_clone() + } +} + +pub type OpenResult = Result, Arc>; + +#[derive(Clone)] +enum OpenedItem { + Loading(watch::Receiver>), + Loaded(Box), +} + +pub struct Workspace { + replica_id: ReplicaId, + worktrees: HashSet>, + items: HashMap<(usize, usize), OpenedItem>, +} + +impl Workspace { + pub fn new(paths: Vec, ctx: &mut ModelContext) -> 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> { + &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) { + for path in paths.iter().cloned() { + self.open_path(path, ctx); + } + } + + pub fn open_path<'a>(&'a mut self, path: PathBuf, ctx: &mut ModelContext) { + 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 + 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, ctx| match buffer { + Ok(buffer) => { + let handle = Box::new(ctx.add_model(|_| buffer)) as Box; + 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, ctx: &mut ModelContext) { + 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 { + 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::>() + }) + } +} + +#[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> { + 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(()) + }) + } +} diff --git a/zed/src/workspace/workspace_view.rs b/zed/src/workspace/workspace_view.rs new file mode 100644 index 0000000000..170b4eb1a7 --- /dev/null +++ b/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) -> Option + 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; + fn clone_on_split(&self, app: &mut MutableAppContext) -> Option>; + fn set_parent_pane(&self, pane: &ViewHandle, app: &mut MutableAppContext); + fn id(&self) -> usize; + fn to_any(&self) -> AnyViewHandle; +} + +impl ItemViewHandle for ViewHandle { + 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 { + Box::new(self.clone()) + } + + fn clone_on_split(&self, app: &mut MutableAppContext) -> Option> { + self.update(app, |item, ctx| { + ctx.add_option_view(|ctx| item.clone_on_split(ctx)) + }) + .map(|handle| Box::new(handle) as Box) + } + + fn set_parent_pane(&self, pane: &ViewHandle, 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 { + fn clone(&self) -> Box { + self.boxed_clone() + } +} + +#[derive(Debug)] +pub struct State { + pub modal: Option, + pub center: PaneGroup, +} + +pub struct WorkspaceView { + pub workspace: ModelHandle, + pub settings: watch::Receiver, + modal: Option, + center: PaneGroup, + panes: Vec>, + active_pane: ViewHandle, + loading_entries: HashSet<(usize, usize)>, +} + +impl WorkspaceView { + pub fn new( + workspace: ModelHandle, + settings: watch::Receiver, + ctx: &mut ViewContext, + ) -> 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(&mut self, ctx: &mut ViewContext, add_view: F) + where + V: 'static + View, + F: FnOnce(&mut ViewContext, &mut Self) -> ViewHandle, + { + if self.modal.as_ref().map_or(false, |modal| modal.is::()) { + 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) { + 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) { + 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) { + 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, ctx: &mut ViewContext) { + ctx.notify(); + } + + fn add_pane(&mut self, ctx: &mut ViewContext) -> ViewHandle { + 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, ctx: &mut ViewContext) { + 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, + ) { + 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, + direction: SplitDirection, + ctx: &mut ViewContext, + ) -> ViewHandle { + 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, ctx: &mut ViewContext) { + 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> { + self.panes.iter().find(|pane| pane.id() == pane_id).cloned() + } + + pub fn active_pane(&self) -> &ViewHandle { + &self.active_pane + } + + fn add_item(&self, item: Box, ctx: &mut ViewContext) { + 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 { + 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) { + 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(()) + }) + } +} diff --git a/zed/src/worktree/mod.rs b/zed/src/worktree/mod.rs index 3aa3ada945..3ece82b454 100644 --- a/zed/src/worktree/mod.rs +++ b/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};