Detailed changes
@@ -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",
]
@@ -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();
@@ -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"
@@ -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 {
@@ -8,4 +8,5 @@ mod time;
mod timer;
mod util;
mod watch;
+mod workspace;
mod worktree;
@@ -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")
+ }
+}
@@ -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(¶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::<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);
+ });
+ }
+}
@@ -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(),
+ // }
+ // }
+}
@@ -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)
+}
@@ -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(())
+ })
+ }
+}
@@ -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(())
+ })
+ }
+}
@@ -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};