Get workspace module in and compiling

This commit is contained in:
Nathan Sobo 2021-03-18 15:52:46 -06:00
parent 171dd0c243
commit 9bab29c72f
12 changed files with 1696 additions and 39 deletions

103
Cargo.lock generated
View file

@ -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",
]

View file

@ -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();

View file

@ -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"

View file

@ -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 {

View file

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

View file

@ -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")
}
}

119
zed/src/workspace/mod.rs Normal file
View file

@ -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);
});
}
}

285
zed/src/workspace/pane.rs Normal file
View file

@ -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(),
// }
// }
}

View file

@ -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)
}

View file

@ -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(())
})
}
}

View file

@ -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(())
})
}
}

View file

@ -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};