diff --git a/Cargo.lock b/Cargo.lock index 06ce5843d4..21a08332c5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1180,6 +1180,7 @@ dependencies = [ "etagere", "font-kit", "foreign-types", + "gpui_macros", "log", "metal", "num_cpus", @@ -1205,6 +1206,14 @@ dependencies = [ "usvg", ] +[[package]] +name = "gpui_macros" +version = "0.1.0" +dependencies = [ + "quote", + "syn", +] + [[package]] name = "hashbrown" version = "0.9.1" diff --git a/Cargo.toml b/Cargo.toml index bf2d5c8143..8109db121b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["zed", "gpui", "fsevent", "scoped_pool"] +members = ["zed", "gpui", "gpui_macros", "fsevent", "scoped_pool"] [patch.crates-io] async-task = {git = "https://github.com/zed-industries/async-task", rev = "341b57d6de98cdfd7b418567b8de2022ca993a6e"} diff --git a/gpui/Cargo.toml b/gpui/Cargo.toml index 8c7c3bf4cb..d421a5a2f3 100644 --- a/gpui/Cargo.toml +++ b/gpui/Cargo.toml @@ -8,6 +8,7 @@ version = "0.1.0" async-task = "4.0.3" ctor = "0.1" etagere = "0.2" +gpui_macros = {path = "../gpui_macros"} log = "0.4" num_cpus = "1.13" ordered-float = "2.1.1" diff --git a/gpui/src/app.rs b/gpui/src/app.rs index decc8096be..bc9b5ed57b 100644 --- a/gpui/src/app.rs +++ b/gpui/src/app.rs @@ -8,6 +8,7 @@ use crate::{ AssetCache, AssetSource, ClipboardItem, FontCache, PathPromptOptions, TextLayoutCache, }; use anyhow::{anyhow, Result}; +use async_task::Task; use keymap::MatchResult; use parking_lot::{Mutex, RwLock}; use pathfinder_geometry::{rect::RectF, vector::vec2f}; @@ -50,6 +51,14 @@ pub trait ReadModel { fn read_model(&self, handle: &ModelHandle) -> &T; } +pub trait ReadModelWith { + fn read_model_with T, T>( + &self, + handle: &ModelHandle, + read: F, + ) -> T; +} + pub trait UpdateModel { fn update_model(&mut self, handle: &ModelHandle, update: F) -> S where @@ -61,6 +70,13 @@ pub trait ReadView { fn read_view(&self, handle: &ViewHandle) -> &T; } +pub trait ReadViewWith { + fn read_view_with(&self, handle: &ViewHandle, read: F) -> T + where + V: View, + F: FnOnce(&V, &AppContext) -> T; +} + pub trait UpdateView { fn update_view(&mut self, handle: &ViewHandle, update: F) -> S where @@ -86,6 +102,8 @@ pub enum MenuItem<'a> { #[derive(Clone)] pub struct App(Rc>); +pub struct AsyncAppContext(Rc>); + #[derive(Clone)] pub struct TestAppContext(Rc>, Rc); @@ -362,6 +380,84 @@ impl TestAppContext { } } +impl AsyncAppContext { + pub fn read T>(&mut self, callback: F) -> T { + callback(self.0.borrow().as_ref()) + } + + pub fn update T>(&mut self, callback: F) -> T { + let mut state = self.0.borrow_mut(); + state.pending_flushes += 1; + let result = callback(&mut *state); + state.flush_effects(); + result + } + + pub fn add_model(&mut self, build_model: F) -> ModelHandle + where + T: Entity, + F: FnOnce(&mut ModelContext) -> T, + { + self.update(|ctx| ctx.add_model(build_model)) + } + + pub fn background_executor(&self) -> Arc { + self.0.borrow().ctx.background.clone() + } +} + +impl UpdateModel for AsyncAppContext { + fn update_model(&mut self, handle: &ModelHandle, update: F) -> S + where + T: Entity, + F: FnOnce(&mut T, &mut ModelContext) -> S, + { + let mut state = self.0.borrow_mut(); + state.pending_flushes += 1; + let result = state.update_model(handle, update); + state.flush_effects(); + result + } +} + +impl ReadModelWith for AsyncAppContext { + fn read_model_with T, T>( + &self, + handle: &ModelHandle, + read: F, + ) -> T { + let ctx = self.0.borrow(); + let ctx = ctx.as_ref(); + read(handle.read(ctx), ctx) + } +} + +impl UpdateView for AsyncAppContext { + fn update_view(&mut self, handle: &ViewHandle, update: F) -> S + where + T: View, + F: FnOnce(&mut T, &mut ViewContext) -> S, + { + let mut state = self.0.borrow_mut(); + state.pending_flushes += 1; + let result = state.update_view(handle, update); + state.flush_effects(); + result + } +} + +impl ReadViewWith for AsyncAppContext { + fn read_view_with(&self, handle: &ViewHandle, read: F) -> T + where + V: View, + F: FnOnce(&V, &AppContext) -> T, + { + let ctx = self.0.borrow(); + let ctx = ctx.as_ref(); + read(handle.read(ctx), ctx) + } +} + impl UpdateModel for TestAppContext { fn update_model(&mut self, handle: &ModelHandle, update: F) -> S where @@ -376,6 +472,18 @@ impl UpdateModel for TestAppContext { } } +impl ReadModelWith for TestAppContext { + fn read_model_with T, T>( + &self, + handle: &ModelHandle, + read: F, + ) -> T { + let ctx = self.0.borrow(); + let ctx = ctx.as_ref(); + read(handle.read(ctx), ctx) + } +} + impl UpdateView for TestAppContext { fn update_view(&mut self, handle: &ViewHandle, update: F) -> S where @@ -390,6 +498,18 @@ impl UpdateView for TestAppContext { } } +impl ReadViewWith for TestAppContext { + fn read_view_with(&self, handle: &ViewHandle, read: F) -> T + where + V: View, + F: FnOnce(&V, &AppContext) -> T, + { + let ctx = self.0.borrow(); + let ctx = ctx.as_ref(); + read(handle.read(ctx), ctx) + } +} + type ActionCallback = dyn FnMut(&mut dyn AnyView, &dyn Any, &mut MutableAppContext, usize, usize) -> bool; @@ -405,7 +525,6 @@ pub struct MutableAppContext { keystroke_matcher: keymap::Matcher, next_entity_id: usize, next_window_id: usize, - next_task_id: usize, subscriptions: HashMap>, model_observations: HashMap>, view_observations: HashMap>, @@ -413,8 +532,6 @@ pub struct MutableAppContext { HashMap>, Box)>, debug_elements_callbacks: HashMap crate::json::Value>>, foreground: Rc, - future_handlers: Rc>>, - stream_handlers: Rc>>, pending_effects: VecDeque, pending_flushes: usize, flushing_effects: bool, @@ -446,15 +563,12 @@ impl MutableAppContext { keystroke_matcher: keymap::Matcher::default(), next_entity_id: 0, next_window_id: 0, - next_task_id: 0, subscriptions: HashMap::new(), model_observations: HashMap::new(), view_observations: HashMap::new(), presenters_and_platform_windows: HashMap::new(), debug_elements_callbacks: HashMap::new(), foreground, - future_handlers: Default::default(), - stream_handlers: Default::default(), pending_effects: VecDeque::new(), pending_flushes: 0, flushing_effects: false, @@ -1201,96 +1315,14 @@ impl MutableAppContext { self.flush_effects(); } - fn spawn(&mut self, spawner: Spawner, future: F) -> EntityTask + pub fn spawn(&self, f: F) -> Task where - F: 'static + Future, + F: FnOnce(AsyncAppContext) -> Fut, + Fut: 'static + Future, T: 'static, { - let task_id = post_inc(&mut self.next_task_id); - let app = self.weak_self.as_ref().unwrap().upgrade().unwrap(); - let task = { - let app = app.clone(); - self.foreground.spawn(async move { - let output = future.await; - app.borrow_mut() - .handle_future_output(task_id, Box::new(output)) - .map(|output| *output.downcast::().unwrap()) - }) - }; - EntityTask::new( - task_id, - task, - spawner, - TaskHandlerMap::Future(self.future_handlers.clone()), - ) - } - - fn spawn_stream(&mut self, spawner: Spawner, mut stream: F) -> EntityTask - where - F: 'static + Stream + Unpin, - T: 'static, - { - let task_id = post_inc(&mut self.next_task_id); - let app = self.weak_self.as_ref().unwrap().upgrade().unwrap(); - let task = self.foreground.spawn(async move { - loop { - match stream.next().await { - Some(item) => { - let mut app = app.borrow_mut(); - if app.handle_stream_item(task_id, Box::new(item)) { - break; - } - } - None => { - break; - } - } - } - - app.borrow_mut() - .stream_completed(task_id) - .map(|output| *output.downcast::().unwrap()) - }); - - EntityTask::new( - task_id, - task, - spawner, - TaskHandlerMap::Stream(self.stream_handlers.clone()), - ) - } - - fn handle_future_output( - &mut self, - task_id: usize, - output: Box, - ) -> Option> { - self.pending_flushes += 1; - let future_callback = self.future_handlers.borrow_mut().remove(&task_id).unwrap(); - let result = future_callback(output, self); - self.flush_effects(); - result - } - - fn handle_stream_item(&mut self, task_id: usize, output: Box) -> bool { - self.pending_flushes += 1; - - let mut handler = self.stream_handlers.borrow_mut().remove(&task_id).unwrap(); - let halt = (handler.item_callback)(output, self); - self.stream_handlers.borrow_mut().insert(task_id, handler); - - self.flush_effects(); - halt - } - - fn stream_completed(&mut self, task_id: usize) -> Option> { - self.pending_flushes += 1; - - let handler = self.stream_handlers.borrow_mut().remove(&task_id).unwrap(); - let result = (handler.done_callback)(self); - - self.flush_effects(); - result + let ctx = AsyncAppContext(self.weak_self.as_ref().unwrap().upgrade().unwrap()); + self.foreground.spawn(f(ctx)) } pub fn write_to_clipboard(&self, item: ClipboardItem) { @@ -1666,76 +1698,14 @@ impl<'a, T: Entity> ModelContext<'a, T> { ModelHandle::new(self.model_id, &self.app.ctx.ref_counts) } - pub fn spawn(&mut self, future: S, callback: F) -> EntityTask + pub fn spawn(&self, f: F) -> Task where - S: 'static + Future, - F: 'static + FnOnce(&mut T, S::Output, &mut ModelContext) -> U, - U: 'static, + F: FnOnce(ModelHandle, AsyncAppContext) -> Fut, + Fut: 'static + Future, + S: 'static, { let handle = self.handle(); - let weak_handle = handle.downgrade(); - let task = self - .app - .spawn::(Spawner::Model(handle.into()), future); - - self.app.future_handlers.borrow_mut().insert( - task.id, - Box::new(move |output, ctx| { - weak_handle.upgrade(ctx.as_ref()).map(|handle| { - let output = *output.downcast().unwrap(); - handle.update(ctx, |model, ctx| { - Box::new(callback(model, output, ctx)) as Box - }) - }) - }), - ); - - task - } - - pub fn spawn_stream( - &mut self, - stream: S, - mut item_callback: F, - done_callback: G, - ) -> EntityTask - where - S: 'static + Stream + Unpin, - F: 'static + FnMut(&mut T, S::Item, &mut ModelContext), - G: 'static + FnOnce(&mut T, &mut ModelContext) -> U, - U: 'static + Any, - { - let handle = self.handle(); - let weak_handle = handle.downgrade(); - let task = self.app.spawn_stream(Spawner::Model(handle.into()), stream); - - self.app.stream_handlers.borrow_mut().insert( - task.id, - StreamHandler { - item_callback: { - let weak_handle = weak_handle.clone(); - Box::new(move |output, app| { - if let Some(handle) = weak_handle.upgrade(app.as_ref()) { - let output = *output.downcast().unwrap(); - handle.update(app, |model, ctx| { - item_callback(model, output, ctx); - ctx.halt_stream - }) - } else { - true - } - }) - }, - done_callback: Box::new(move |app| { - weak_handle.upgrade(app.as_ref()).map(|handle| { - handle.update(app, |model, ctx| Box::new(done_callback(model, ctx))) - as Box - }) - }), - }, - ); - - task + self.app.spawn(|ctx| f(handle, ctx)) } } @@ -1773,7 +1743,6 @@ pub struct ViewContext<'a, T: ?Sized> { view_id: usize, view_type: PhantomData, halt_action_dispatch: bool, - halt_stream: bool, } impl<'a, T: View> ViewContext<'a, T> { @@ -1784,7 +1753,6 @@ impl<'a, T: View> ViewContext<'a, T> { view_id, view_type: PhantomData, halt_action_dispatch: true, - halt_stream: false, } } @@ -1994,77 +1962,14 @@ impl<'a, T: View> ViewContext<'a, T> { self.halt_action_dispatch = false; } - pub fn halt_stream(&mut self) { - self.halt_stream = true; - } - - pub fn spawn(&mut self, future: S, callback: F) -> EntityTask + pub fn spawn(&self, f: F) -> Task where - S: 'static + Future, - F: 'static + FnOnce(&mut T, S::Output, &mut ViewContext) -> U, - U: 'static, + F: FnOnce(ViewHandle, AsyncAppContext) -> Fut, + Fut: 'static + Future, + S: 'static, { let handle = self.handle(); - let weak_handle = handle.downgrade(); - let task = self.app.spawn(Spawner::View(handle.into()), future); - - self.app.future_handlers.borrow_mut().insert( - task.id, - Box::new(move |output, app| { - weak_handle.upgrade(app.as_ref()).map(|handle| { - let output = *output.downcast().unwrap(); - handle.update(app, |view, ctx| { - Box::new(callback(view, output, ctx)) as Box - }) - }) - }), - ); - - task - } - - pub fn spawn_stream( - &mut self, - stream: S, - mut item_callback: F, - done_callback: G, - ) -> EntityTask - where - S: 'static + Stream + Unpin, - F: 'static + FnMut(&mut T, S::Item, &mut ViewContext), - G: 'static + FnOnce(&mut T, &mut ViewContext) -> U, - U: 'static + Any, - { - let handle = self.handle(); - let weak_handle = handle.downgrade(); - let task = self.app.spawn_stream(Spawner::View(handle.into()), stream); - self.app.stream_handlers.borrow_mut().insert( - task.id, - StreamHandler { - item_callback: { - let weak_handle = weak_handle.clone(); - Box::new(move |output, ctx| { - if let Some(handle) = weak_handle.upgrade(ctx.as_ref()) { - let output = *output.downcast().unwrap(); - handle.update(ctx, |view, ctx| { - item_callback(view, output, ctx); - ctx.halt_stream - }) - } else { - true - } - }) - }, - done_callback: Box::new(move |ctx| { - weak_handle.upgrade(ctx.as_ref()).map(|handle| { - handle.update(ctx, |view, ctx| { - Box::new(done_callback(view, ctx)) as Box - }) - }) - }), - }, - ); - task + self.app.spawn(|ctx| f(handle, ctx)) } } @@ -2157,6 +2062,14 @@ impl ModelHandle { app.read_model(self) } + pub fn read_with<'a, A, F, S>(&self, ctx: &A, read: F) -> S + where + A: ReadModelWith, + F: FnOnce(&T, &AppContext) -> S, + { + ctx.read_model_with(self, read) + } + pub fn update(&self, app: &mut A, update: F) -> S where A: UpdateModel, @@ -2299,9 +2212,10 @@ impl WeakModelHandle { } } - pub fn upgrade(&self, app: &AppContext) -> Option> { - if app.models.contains_key(&self.model_id) { - Some(ModelHandle::new(self.model_id, &app.ref_counts)) + pub fn upgrade(&self, ctx: impl AsRef) -> Option> { + let ctx = ctx.as_ref(); + if ctx.models.contains_key(&self.model_id) { + Some(ModelHandle::new(self.model_id, &ctx.ref_counts)) } else { None } @@ -2351,6 +2265,14 @@ impl ViewHandle { app.read_view(self) } + pub fn read_with(&self, ctx: &A, read: F) -> S + where + A: ReadViewWith, + F: FnOnce(&T, &AppContext) -> S, + { + ctx.read_view_with(self, read) + } + pub fn update(&self, app: &mut A, update: F) -> S where A: UpdateView, @@ -2761,94 +2683,14 @@ struct ViewObservation { callback: Box, } -type FutureHandler = Box, &mut MutableAppContext) -> Option>>; - -struct StreamHandler { - item_callback: Box, &mut MutableAppContext) -> bool>, - done_callback: Box Option>>, -} - -#[must_use] -pub struct EntityTask { - id: usize, - task: Option>>, - _spawner: Spawner, // Keeps the spawning entity alive for as long as the task exists - handler_map: TaskHandlerMap, -} - -pub enum Spawner { - Model(AnyModelHandle), - View(AnyViewHandle), -} - -enum TaskHandlerMap { - Detached, - Future(Rc>>), - Stream(Rc>>), -} - -impl EntityTask { - fn new( - id: usize, - task: executor::Task>, - spawner: Spawner, - handler_map: TaskHandlerMap, - ) -> Self { - Self { - id, - task: Some(task), - _spawner: spawner, - handler_map, - } - } - - pub fn detach(mut self) { - self.handler_map = TaskHandlerMap::Detached; - self.task.take().unwrap().detach(); - } - - pub async fn cancel(mut self) -> Option { - let task = self.task.take().unwrap(); - task.cancel().await.unwrap() - } -} - -impl Future for EntityTask { - type Output = T; - - fn poll( - self: std::pin::Pin<&mut Self>, - ctx: &mut std::task::Context<'_>, - ) -> std::task::Poll { - let task = unsafe { self.map_unchecked_mut(|task| task.task.as_mut().unwrap()) }; - task.poll(ctx).map(|output| output.unwrap()) - } -} - -impl Drop for EntityTask { - fn drop(self: &mut Self) { - match &self.handler_map { - TaskHandlerMap::Detached => { - return; - } - TaskHandlerMap::Future(map) => { - map.borrow_mut().remove(&self.id); - } - TaskHandlerMap::Stream(map) => { - map.borrow_mut().remove(&self.id); - } - } - } -} - #[cfg(test)] mod tests { use super::*; use crate::elements::*; use smol::future::poll_once; - #[test] - fn test_model_handles() { + #[crate::test(self)] + fn test_model_handles(app: &mut MutableAppContext) { struct Model { other: Option>, events: Vec, @@ -2876,40 +2718,38 @@ mod tests { } } - App::test((), |app| { - let handle_1 = app.add_model(|ctx| Model::new(None, ctx)); - let handle_2 = app.add_model(|ctx| Model::new(Some(handle_1.clone()), ctx)); - assert_eq!(app.ctx.models.len(), 2); + let handle_1 = app.add_model(|ctx| Model::new(None, ctx)); + let handle_2 = app.add_model(|ctx| Model::new(Some(handle_1.clone()), ctx)); + assert_eq!(app.ctx.models.len(), 2); - handle_1.update(app, |model, ctx| { - model.events.push("updated".into()); - ctx.emit(1); - ctx.notify(); - ctx.emit(2); - }); - assert_eq!(handle_1.read(app).events, vec!["updated".to_string()]); - assert_eq!( - handle_2.read(app).events, - vec![ - "observed event 1".to_string(), - "notified".to_string(), - "observed event 2".to_string(), - ] - ); - - handle_2.update(app, |model, _| { - drop(handle_1); - model.other.take(); - }); - - assert_eq!(app.ctx.models.len(), 1); - assert!(app.subscriptions.is_empty()); - assert!(app.model_observations.is_empty()); + handle_1.update(app, |model, ctx| { + model.events.push("updated".into()); + ctx.emit(1); + ctx.notify(); + ctx.emit(2); }); + assert_eq!(handle_1.read(app).events, vec!["updated".to_string()]); + assert_eq!( + handle_2.read(app).events, + vec![ + "observed event 1".to_string(), + "notified".to_string(), + "observed event 2".to_string(), + ] + ); + + handle_2.update(app, |model, _| { + drop(handle_1); + model.other.take(); + }); + + assert_eq!(app.ctx.models.len(), 1); + assert!(app.subscriptions.is_empty()); + assert!(app.model_observations.is_empty()); } - #[test] - fn test_subscribe_and_emit_from_model() { + #[crate::test(self)] + fn test_subscribe_and_emit_from_model(app: &mut MutableAppContext) { #[derive(Default)] struct Model { events: Vec, @@ -2919,31 +2759,29 @@ mod tests { type Event = usize; } - App::test((), |app| { - let handle_1 = app.add_model(|_| Model::default()); - let handle_2 = app.add_model(|_| Model::default()); - let handle_2b = handle_2.clone(); + let handle_1 = app.add_model(|_| Model::default()); + let handle_2 = app.add_model(|_| Model::default()); + let handle_2b = handle_2.clone(); - handle_1.update(app, |_, c| { - c.subscribe(&handle_2, move |model: &mut Model, event, c| { - model.events.push(*event); + handle_1.update(app, |_, c| { + c.subscribe(&handle_2, move |model: &mut Model, event, c| { + model.events.push(*event); - c.subscribe(&handle_2b, |model, event, _| { - model.events.push(*event * 2); - }); + c.subscribe(&handle_2b, |model, event, _| { + model.events.push(*event * 2); }); }); + }); - handle_2.update(app, |_, c| c.emit(7)); - assert_eq!(handle_1.read(app).events, vec![7]); + handle_2.update(app, |_, c| c.emit(7)); + assert_eq!(handle_1.read(app).events, vec![7]); - handle_2.update(app, |_, c| c.emit(5)); - assert_eq!(handle_1.read(app).events, vec![7, 10, 5]); - }) + handle_2.update(app, |_, c| c.emit(5)); + assert_eq!(handle_1.read(app).events, vec![7, 10, 5]); } - #[test] - fn test_observe_and_notify_from_model() { + #[crate::test(self)] + fn test_observe_and_notify_from_model(app: &mut MutableAppContext) { #[derive(Default)] struct Model { count: usize, @@ -2954,99 +2792,34 @@ mod tests { type Event = (); } - App::test((), |app| { - let handle_1 = app.add_model(|_| Model::default()); - let handle_2 = app.add_model(|_| Model::default()); - let handle_2b = handle_2.clone(); + let handle_1 = app.add_model(|_| Model::default()); + let handle_2 = app.add_model(|_| Model::default()); + let handle_2b = handle_2.clone(); - handle_1.update(app, |_, c| { - c.observe(&handle_2, move |model, observed, c| { - model.events.push(observed.read(c).count); - c.observe(&handle_2b, |model, observed, c| { - model.events.push(observed.read(c).count * 2); - }); + handle_1.update(app, |_, c| { + c.observe(&handle_2, move |model, observed, c| { + model.events.push(observed.read(c).count); + c.observe(&handle_2b, |model, observed, c| { + model.events.push(observed.read(c).count * 2); }); }); - - handle_2.update(app, |model, c| { - model.count = 7; - c.notify() - }); - assert_eq!(handle_1.read(app).events, vec![7]); - - handle_2.update(app, |model, c| { - model.count = 5; - c.notify() - }); - assert_eq!(handle_1.read(app).events, vec![7, 10, 5]) - }) - } - - #[test] - fn test_spawn_from_model() { - #[derive(Default)] - struct Model { - count: usize, - } - - impl Entity for Model { - type Event = (); - } - - App::test_async((), |mut app| async move { - let handle = app.add_model(|_| Model::default()); - handle - .update(&mut app, |_, c| { - c.spawn(async { 7 }, |model, output, _| { - model.count = output; - }) - }) - .await; - app.read(|ctx| assert_eq!(handle.read(ctx).count, 7)); - - handle - .update(&mut app, |_, c| { - c.spawn(async { 14 }, |model, output, _| { - model.count = output; - }) - }) - .await; - app.read(|ctx| assert_eq!(handle.read(ctx).count, 14)); }); + + handle_2.update(app, |model, c| { + model.count = 7; + c.notify() + }); + assert_eq!(handle_1.read(app).events, vec![7]); + + handle_2.update(app, |model, c| { + model.count = 5; + c.notify() + }); + assert_eq!(handle_1.read(app).events, vec![7, 10, 5]) } - #[test] - fn test_spawn_stream_local_from_model() { - #[derive(Default)] - struct Model { - events: Vec>, - } - - impl Entity for Model { - type Event = (); - } - - App::test_async((), |mut app| async move { - let handle = app.add_model(|_| Model::default()); - handle - .update(&mut app, |_, c| { - c.spawn_stream( - smol::stream::iter(vec![1, 2, 3]), - |model, output, _| { - model.events.push(Some(output)); - }, - |model, _| { - model.events.push(None); - }, - ) - }) - .await; - app.read(|ctx| assert_eq!(handle.read(ctx).events, [Some(1), Some(2), Some(3), None])); - }) - } - - #[test] - fn test_view_handles() { + #[crate::test(self)] + fn test_view_handles(app: &mut MutableAppContext) { struct View { other: Option>, events: Vec, @@ -3080,39 +2853,37 @@ mod tests { } } - App::test((), |app| { - let (window_id, _) = app.add_window(|ctx| View::new(None, ctx)); - let handle_1 = app.add_view(window_id, |ctx| View::new(None, ctx)); - let handle_2 = app.add_view(window_id, |ctx| View::new(Some(handle_1.clone()), ctx)); - assert_eq!(app.ctx.views.len(), 3); + let (window_id, _) = app.add_window(|ctx| View::new(None, ctx)); + let handle_1 = app.add_view(window_id, |ctx| View::new(None, ctx)); + let handle_2 = app.add_view(window_id, |ctx| View::new(Some(handle_1.clone()), ctx)); + assert_eq!(app.ctx.views.len(), 3); - handle_1.update(app, |view, ctx| { - view.events.push("updated".into()); - ctx.emit(1); - ctx.emit(2); - }); - assert_eq!(handle_1.read(app).events, vec!["updated".to_string()]); - assert_eq!( - handle_2.read(app).events, - vec![ - "observed event 1".to_string(), - "observed event 2".to_string(), - ] - ); + handle_1.update(app, |view, ctx| { + view.events.push("updated".into()); + ctx.emit(1); + ctx.emit(2); + }); + assert_eq!(handle_1.read(app).events, vec!["updated".to_string()]); + assert_eq!( + handle_2.read(app).events, + vec![ + "observed event 1".to_string(), + "observed event 2".to_string(), + ] + ); - handle_2.update(app, |view, _| { - drop(handle_1); - view.other.take(); - }); + handle_2.update(app, |view, _| { + drop(handle_1); + view.other.take(); + }); - assert_eq!(app.ctx.views.len(), 2); - assert!(app.subscriptions.is_empty()); - assert!(app.model_observations.is_empty()); - }) + assert_eq!(app.ctx.views.len(), 2); + assert!(app.subscriptions.is_empty()); + assert!(app.model_observations.is_empty()); } - #[test] - fn test_subscribe_and_emit_from_view() { + #[crate::test(self)] + fn test_subscribe_and_emit_from_view(app: &mut MutableAppContext) { #[derive(Default)] struct View { events: Vec, @@ -3138,39 +2909,37 @@ mod tests { type Event = usize; } - App::test((), |app| { - let (window_id, handle_1) = app.add_window(|_| View::default()); - let handle_2 = app.add_view(window_id, |_| View::default()); - let handle_2b = handle_2.clone(); - let handle_3 = app.add_model(|_| Model); + let (window_id, handle_1) = app.add_window(|_| View::default()); + let handle_2 = app.add_view(window_id, |_| View::default()); + let handle_2b = handle_2.clone(); + let handle_3 = app.add_model(|_| Model); - handle_1.update(app, |_, c| { - c.subscribe_to_view(&handle_2, move |me, _, event, c| { - me.events.push(*event); + handle_1.update(app, |_, c| { + c.subscribe_to_view(&handle_2, move |me, _, event, c| { + me.events.push(*event); - c.subscribe_to_view(&handle_2b, |me, _, event, _| { - me.events.push(*event * 2); - }); + c.subscribe_to_view(&handle_2b, |me, _, event, _| { + me.events.push(*event * 2); }); - - c.subscribe_to_model(&handle_3, |me, _, event, _| { - me.events.push(*event); - }) }); - handle_2.update(app, |_, c| c.emit(7)); - assert_eq!(handle_1.read(app).events, vec![7]); + c.subscribe_to_model(&handle_3, |me, _, event, _| { + me.events.push(*event); + }) + }); - handle_2.update(app, |_, c| c.emit(5)); - assert_eq!(handle_1.read(app).events, vec![7, 10, 5]); + handle_2.update(app, |_, c| c.emit(7)); + assert_eq!(handle_1.read(app).events, vec![7]); - handle_3.update(app, |_, c| c.emit(9)); - assert_eq!(handle_1.read(app).events, vec![7, 10, 5, 9]); - }) + handle_2.update(app, |_, c| c.emit(5)); + assert_eq!(handle_1.read(app).events, vec![7, 10, 5]); + + handle_3.update(app, |_, c| c.emit(9)); + assert_eq!(handle_1.read(app).events, vec![7, 10, 5, 9]); } - #[test] - fn test_dropping_subscribers() { + #[crate::test(self)] + fn test_dropping_subscribers(app: &mut MutableAppContext) { struct View; impl Entity for View { @@ -3193,33 +2962,31 @@ mod tests { type Event = (); } - App::test((), |app| { - let (window_id, _) = app.add_window(|_| View); - let observing_view = app.add_view(window_id, |_| View); - let emitting_view = app.add_view(window_id, |_| View); - let observing_model = app.add_model(|_| Model); - let observed_model = app.add_model(|_| Model); + let (window_id, _) = app.add_window(|_| View); + let observing_view = app.add_view(window_id, |_| View); + let emitting_view = app.add_view(window_id, |_| View); + let observing_model = app.add_model(|_| Model); + let observed_model = app.add_model(|_| Model); - observing_view.update(app, |_, ctx| { - ctx.subscribe_to_view(&emitting_view, |_, _, _, _| {}); - ctx.subscribe_to_model(&observed_model, |_, _, _, _| {}); - }); - observing_model.update(app, |_, ctx| { - ctx.subscribe(&observed_model, |_, _, _| {}); - }); + observing_view.update(app, |_, ctx| { + ctx.subscribe_to_view(&emitting_view, |_, _, _, _| {}); + ctx.subscribe_to_model(&observed_model, |_, _, _, _| {}); + }); + observing_model.update(app, |_, ctx| { + ctx.subscribe(&observed_model, |_, _, _| {}); + }); - app.update(|| { - drop(observing_view); - drop(observing_model); - }); + app.update(|| { + drop(observing_view); + drop(observing_model); + }); - emitting_view.update(app, |_, ctx| ctx.emit(())); - observed_model.update(app, |_, ctx| ctx.emit(())); - }) + emitting_view.update(app, |_, ctx| ctx.emit(())); + observed_model.update(app, |_, ctx| ctx.emit(())); } - #[test] - fn test_observe_and_notify_from_view() { + #[crate::test(self)] + fn test_observe_and_notify_from_view(app: &mut MutableAppContext) { #[derive(Default)] struct View { events: Vec, @@ -3248,26 +3015,24 @@ mod tests { type Event = (); } - App::test((), |app| { - let (_, view) = app.add_window(|_| View::default()); - let model = app.add_model(|_| Model::default()); + let (_, view) = app.add_window(|_| View::default()); + let model = app.add_model(|_| Model::default()); - view.update(app, |_, c| { - c.observe_model(&model, |me, observed, c| { - me.events.push(observed.read(c).count) - }); + view.update(app, |_, c| { + c.observe_model(&model, |me, observed, c| { + me.events.push(observed.read(c).count) }); + }); - model.update(app, |model, c| { - model.count = 11; - c.notify(); - }); - assert_eq!(view.read(app).events, vec![11]); - }) + model.update(app, |model, c| { + model.count = 11; + c.notify(); + }); + assert_eq!(view.read(app).events, vec![11]); } - #[test] - fn test_dropping_observers() { + #[crate::test(self)] + fn test_dropping_observers(app: &mut MutableAppContext) { struct View; impl Entity for View { @@ -3290,30 +3055,28 @@ mod tests { type Event = (); } - App::test((), |app| { - let (window_id, _) = app.add_window(|_| View); - let observing_view = app.add_view(window_id, |_| View); - let observing_model = app.add_model(|_| Model); - let observed_model = app.add_model(|_| Model); + let (window_id, _) = app.add_window(|_| View); + let observing_view = app.add_view(window_id, |_| View); + let observing_model = app.add_model(|_| Model); + let observed_model = app.add_model(|_| Model); - observing_view.update(app, |_, ctx| { - ctx.observe_model(&observed_model, |_, _, _| {}); - }); - observing_model.update(app, |_, ctx| { - ctx.observe(&observed_model, |_, _, _| {}); - }); + observing_view.update(app, |_, ctx| { + ctx.observe_model(&observed_model, |_, _, _| {}); + }); + observing_model.update(app, |_, ctx| { + ctx.observe(&observed_model, |_, _, _| {}); + }); - app.update(|| { - drop(observing_view); - drop(observing_model); - }); + app.update(|| { + drop(observing_view); + drop(observing_model); + }); - observed_model.update(app, |_, ctx| ctx.notify()); - }) + observed_model.update(app, |_, ctx| ctx.notify()); } - #[test] - fn test_focus() { + #[crate::test(self)] + fn test_focus(app: &mut MutableAppContext) { struct View { name: String, events: Arc>>, @@ -3341,123 +3104,38 @@ mod tests { } } - App::test((), |app| { - let events: Arc>> = Default::default(); - let (window_id, view_1) = app.add_window(|_| View { - events: events.clone(), - name: "view 1".to_string(), - }); - let view_2 = app.add_view(window_id, |_| View { - events: events.clone(), - name: "view 2".to_string(), - }); - - view_1.update(app, |_, ctx| ctx.focus(&view_2)); - view_1.update(app, |_, ctx| ctx.focus(&view_1)); - view_1.update(app, |_, ctx| ctx.focus(&view_2)); - view_1.update(app, |_, _| drop(view_2)); - - assert_eq!( - *events.lock(), - [ - "view 1 focused".to_string(), - "view 1 blurred".to_string(), - "view 2 focused".to_string(), - "view 2 blurred".to_string(), - "view 1 focused".to_string(), - "view 1 blurred".to_string(), - "view 2 focused".to_string(), - "view 1 focused".to_string(), - ], - ); - }) - } - - #[test] - fn test_spawn_from_view() { - #[derive(Default)] - struct View { - count: usize, - } - - impl Entity for View { - type Event = (); - } - - impl super::View for View { - fn render<'a>(&self, _: &AppContext) -> ElementBox { - Empty::new().boxed() - } - - fn ui_name() -> &'static str { - "View" - } - } - - App::test_async((), |mut app| async move { - let handle = app.add_window(|_| View::default()).1; - handle - .update(&mut app, |_, c| { - c.spawn(async { 7 }, |me, output, _| { - me.count = output; - }) - }) - .await; - app.read(|ctx| assert_eq!(handle.read(ctx).count, 7)); - handle - .update(&mut app, |_, c| { - c.spawn(async { 14 }, |me, output, _| { - me.count = output; - }) - }) - .await; - app.read(|ctx| assert_eq!(handle.read(ctx).count, 14)); + let events: Arc>> = Default::default(); + let (window_id, view_1) = app.add_window(|_| View { + events: events.clone(), + name: "view 1".to_string(), }); - } - - #[test] - fn test_spawn_stream_local_from_view() { - #[derive(Default)] - struct View { - events: Vec>, - } - - impl Entity for View { - type Event = (); - } - - impl super::View for View { - fn render<'a>(&self, _: &AppContext) -> ElementBox { - Empty::new().boxed() - } - - fn ui_name() -> &'static str { - "View" - } - } - - App::test_async((), |mut app| async move { - let (_, handle) = app.add_window(|_| View::default()); - handle - .update(&mut app, |_, c| { - c.spawn_stream( - smol::stream::iter(vec![1_usize, 2, 3]), - |me, output, _| { - me.events.push(Some(output)); - }, - |me, _| { - me.events.push(None); - }, - ) - }) - .await; - - app.read(|ctx| assert_eq!(handle.read(ctx).events, [Some(1), Some(2), Some(3), None])) + let view_2 = app.add_view(window_id, |_| View { + events: events.clone(), + name: "view 2".to_string(), }); + + view_1.update(app, |_, ctx| ctx.focus(&view_2)); + view_1.update(app, |_, ctx| ctx.focus(&view_1)); + view_1.update(app, |_, ctx| ctx.focus(&view_2)); + view_1.update(app, |_, _| drop(view_2)); + + assert_eq!( + *events.lock(), + [ + "view 1 focused".to_string(), + "view 1 blurred".to_string(), + "view 2 focused".to_string(), + "view 2 blurred".to_string(), + "view 1 focused".to_string(), + "view 1 blurred".to_string(), + "view 2 focused".to_string(), + "view 1 focused".to_string(), + ], + ); } - #[test] - fn test_dispatch_action() { + #[crate::test(self)] + fn test_dispatch_action(app: &mut MutableAppContext) { struct ViewA { id: usize, } @@ -3498,81 +3176,79 @@ mod tests { foo: String, } - App::test((), |app| { - let actions = Rc::new(RefCell::new(Vec::new())); + let actions = Rc::new(RefCell::new(Vec::new())); - let actions_clone = actions.clone(); - app.add_global_action("action", move |_: &ActionArg, _: &mut MutableAppContext| { - actions_clone.borrow_mut().push("global a".to_string()); - }); + let actions_clone = actions.clone(); + app.add_global_action("action", move |_: &ActionArg, _: &mut MutableAppContext| { + actions_clone.borrow_mut().push("global a".to_string()); + }); - let actions_clone = actions.clone(); - app.add_global_action("action", move |_: &ActionArg, _: &mut MutableAppContext| { - actions_clone.borrow_mut().push("global b".to_string()); - }); + let actions_clone = actions.clone(); + app.add_global_action("action", move |_: &ActionArg, _: &mut MutableAppContext| { + actions_clone.borrow_mut().push("global b".to_string()); + }); - let actions_clone = actions.clone(); - app.add_action("action", move |view: &mut ViewA, arg: &ActionArg, ctx| { - assert_eq!(arg.foo, "bar"); + let actions_clone = actions.clone(); + app.add_action("action", move |view: &mut ViewA, arg: &ActionArg, ctx| { + assert_eq!(arg.foo, "bar"); + ctx.propagate_action(); + actions_clone.borrow_mut().push(format!("{} a", view.id)); + }); + + let actions_clone = actions.clone(); + app.add_action("action", move |view: &mut ViewA, _: &ActionArg, ctx| { + if view.id != 1 { ctx.propagate_action(); - actions_clone.borrow_mut().push(format!("{} a", view.id)); - }); + } + actions_clone.borrow_mut().push(format!("{} b", view.id)); + }); - let actions_clone = actions.clone(); - app.add_action("action", move |view: &mut ViewA, _: &ActionArg, ctx| { - if view.id != 1 { - ctx.propagate_action(); - } - actions_clone.borrow_mut().push(format!("{} b", view.id)); - }); + let actions_clone = actions.clone(); + app.add_action("action", move |view: &mut ViewB, _: &ActionArg, ctx| { + ctx.propagate_action(); + actions_clone.borrow_mut().push(format!("{} c", view.id)); + }); - let actions_clone = actions.clone(); - app.add_action("action", move |view: &mut ViewB, _: &ActionArg, ctx| { - ctx.propagate_action(); - actions_clone.borrow_mut().push(format!("{} c", view.id)); - }); + let actions_clone = actions.clone(); + app.add_action("action", move |view: &mut ViewB, _: &ActionArg, ctx| { + ctx.propagate_action(); + actions_clone.borrow_mut().push(format!("{} d", view.id)); + }); - let actions_clone = actions.clone(); - app.add_action("action", move |view: &mut ViewB, _: &ActionArg, ctx| { - ctx.propagate_action(); - actions_clone.borrow_mut().push(format!("{} d", view.id)); - }); + let (window_id, view_1) = app.add_window(|_| ViewA { id: 1 }); + let view_2 = app.add_view(window_id, |_| ViewB { id: 2 }); + let view_3 = app.add_view(window_id, |_| ViewA { id: 3 }); + let view_4 = app.add_view(window_id, |_| ViewB { id: 4 }); - let (window_id, view_1) = app.add_window(|_| ViewA { id: 1 }); - let view_2 = app.add_view(window_id, |_| ViewB { id: 2 }); - let view_3 = app.add_view(window_id, |_| ViewA { id: 3 }); - let view_4 = app.add_view(window_id, |_| ViewB { id: 4 }); + app.dispatch_action( + window_id, + vec![view_1.id(), view_2.id(), view_3.id(), view_4.id()], + "action", + ActionArg { foo: "bar".into() }, + ); - app.dispatch_action( - window_id, - vec![view_1.id(), view_2.id(), view_3.id(), view_4.id()], - "action", - ActionArg { foo: "bar".into() }, - ); + assert_eq!( + *actions.borrow(), + vec!["4 d", "4 c", "3 b", "3 a", "2 d", "2 c", "1 b"] + ); - assert_eq!( - *actions.borrow(), - vec!["4 d", "4 c", "3 b", "3 a", "2 d", "2 c", "1 b"] - ); + // Remove view_1, which doesn't propagate the action + actions.borrow_mut().clear(); + app.dispatch_action( + window_id, + vec![view_2.id(), view_3.id(), view_4.id()], + "action", + ActionArg { foo: "bar".into() }, + ); - // Remove view_1, which doesn't propagate the action - actions.borrow_mut().clear(); - app.dispatch_action( - window_id, - vec![view_2.id(), view_3.id(), view_4.id()], - "action", - ActionArg { foo: "bar".into() }, - ); - - assert_eq!( - *actions.borrow(), - vec!["4 d", "4 c", "3 b", "3 a", "2 d", "2 c", "global b", "global a"] - ); - }) + assert_eq!( + *actions.borrow(), + vec!["4 d", "4 c", "3 b", "3 a", "2 d", "2 c", "global b", "global a"] + ); } - #[test] - fn test_dispatch_keystroke() { + #[crate::test(self)] + fn test_dispatch_keystroke(app: &mut MutableAppContext) { use std::cell::Cell; #[derive(Clone)] @@ -3612,45 +3288,43 @@ mod tests { } } - App::test((), |app| { - let mut view_1 = View::new(1); - let mut view_2 = View::new(2); - let mut view_3 = View::new(3); - view_1.keymap_context.set.insert("a".into()); - view_2.keymap_context.set.insert("b".into()); - view_3.keymap_context.set.insert("c".into()); + let mut view_1 = View::new(1); + let mut view_2 = View::new(2); + let mut view_3 = View::new(3); + view_1.keymap_context.set.insert("a".into()); + view_2.keymap_context.set.insert("b".into()); + view_3.keymap_context.set.insert("c".into()); - let (window_id, view_1) = app.add_window(|_| view_1); - let view_2 = app.add_view(window_id, |_| view_2); - let view_3 = app.add_view(window_id, |_| view_3); + let (window_id, view_1) = app.add_window(|_| view_1); + let view_2 = app.add_view(window_id, |_| view_2); + let view_3 = app.add_view(window_id, |_| view_3); - // This keymap's only binding dispatches an action on view 2 because that view will have - // "a" and "b" in its context, but not "c". - let binding = keymap::Binding::new("a", "action", Some("a && b && !c")) - .with_arg(ActionArg { key: "a".into() }); - app.add_bindings(vec![binding]); + // This keymap's only binding dispatches an action on view 2 because that view will have + // "a" and "b" in its context, but not "c". + let binding = keymap::Binding::new("a", "action", Some("a && b && !c")) + .with_arg(ActionArg { key: "a".into() }); + app.add_bindings(vec![binding]); - let handled_action = Rc::new(Cell::new(false)); - let handled_action_clone = handled_action.clone(); - app.add_action("action", move |view: &mut View, arg: &ActionArg, _ctx| { - handled_action_clone.set(true); - assert_eq!(view.id, 2); - assert_eq!(arg.key, "a"); - }); - - app.dispatch_keystroke( - window_id, - vec![view_1.id(), view_2.id(), view_3.id()], - &Keystroke::parse("a").unwrap(), - ) - .unwrap(); - - assert!(handled_action.get()); + let handled_action = Rc::new(Cell::new(false)); + let handled_action_clone = handled_action.clone(); + app.add_action("action", move |view: &mut View, arg: &ActionArg, _ctx| { + handled_action_clone.set(true); + assert_eq!(view.id, 2); + assert_eq!(arg.key, "a"); }); + + app.dispatch_keystroke( + window_id, + vec![view_1.id(), view_2.id(), view_3.id()], + &Keystroke::parse("a").unwrap(), + ) + .unwrap(); + + assert!(handled_action.get()); } - #[test] - fn test_model_condition() { + #[crate::test(self)] + async fn test_model_condition(mut app: TestAppContext) { struct Counter(usize); impl super::Entity for Counter { @@ -3664,62 +3338,56 @@ mod tests { } } - App::test_async((), |mut app| async move { - let model = app.add_model(|_| Counter(0)); + let model = app.add_model(|_| Counter(0)); - let condition1 = model.condition(&app, |model, _| model.0 == 2); - let condition2 = model.condition(&app, |model, _| model.0 == 3); - smol::pin!(condition1, condition2); + let condition1 = model.condition(&app, |model, _| model.0 == 2); + let condition2 = model.condition(&app, |model, _| model.0 == 3); + smol::pin!(condition1, condition2); - model.update(&mut app, |model, ctx| model.inc(ctx)); - assert_eq!(poll_once(&mut condition1).await, None); - assert_eq!(poll_once(&mut condition2).await, None); + model.update(&mut app, |model, ctx| model.inc(ctx)); + assert_eq!(poll_once(&mut condition1).await, None); + assert_eq!(poll_once(&mut condition2).await, None); - model.update(&mut app, |model, ctx| model.inc(ctx)); - assert_eq!(poll_once(&mut condition1).await, Some(())); - assert_eq!(poll_once(&mut condition2).await, None); + model.update(&mut app, |model, ctx| model.inc(ctx)); + assert_eq!(poll_once(&mut condition1).await, Some(())); + assert_eq!(poll_once(&mut condition2).await, None); - model.update(&mut app, |model, ctx| model.inc(ctx)); - assert_eq!(poll_once(&mut condition2).await, Some(())); + model.update(&mut app, |model, ctx| model.inc(ctx)); + assert_eq!(poll_once(&mut condition2).await, Some(())); - model.update(&mut app, |_, ctx| ctx.notify()); - }); + model.update(&mut app, |_, ctx| ctx.notify()); } - #[test] + #[crate::test(self)] #[should_panic] - fn test_model_condition_timeout() { + async fn test_model_condition_timeout(mut app: TestAppContext) { struct Model; impl super::Entity for Model { type Event = (); } - App::test_async((), |mut app| async move { - let model = app.add_model(|_| Model); - model.condition(&app, |_, _| false).await; - }); + let model = app.add_model(|_| Model); + model.condition(&app, |_, _| false).await; } - #[test] + #[crate::test(self)] #[should_panic(expected = "model dropped with pending condition")] - fn test_model_condition_panic_on_drop() { + async fn test_model_condition_panic_on_drop(mut app: TestAppContext) { struct Model; impl super::Entity for Model { type Event = (); } - App::test_async((), |mut app| async move { - let model = app.add_model(|_| Model); - let condition = model.condition(&app, |_, _| false); - app.update(|_| drop(model)); - condition.await; - }); + let model = app.add_model(|_| Model); + let condition = model.condition(&app, |_, _| false); + app.update(|_| drop(model)); + condition.await; } - #[test] - fn test_view_condition() { + #[crate::test(self)] + async fn test_view_condition(mut app: TestAppContext) { struct Counter(usize); impl super::Entity for Counter { @@ -3743,30 +3411,28 @@ mod tests { } } - App::test_async((), |mut app| async move { - let (_, view) = app.add_window(|_| Counter(0)); + let (_, view) = app.add_window(|_| Counter(0)); - let condition1 = view.condition(&app, |view, _| view.0 == 2); - let condition2 = view.condition(&app, |view, _| view.0 == 3); - smol::pin!(condition1, condition2); + let condition1 = view.condition(&app, |view, _| view.0 == 2); + let condition2 = view.condition(&app, |view, _| view.0 == 3); + smol::pin!(condition1, condition2); - view.update(&mut app, |view, ctx| view.inc(ctx)); - assert_eq!(poll_once(&mut condition1).await, None); - assert_eq!(poll_once(&mut condition2).await, None); + view.update(&mut app, |view, ctx| view.inc(ctx)); + assert_eq!(poll_once(&mut condition1).await, None); + assert_eq!(poll_once(&mut condition2).await, None); - view.update(&mut app, |view, ctx| view.inc(ctx)); - assert_eq!(poll_once(&mut condition1).await, Some(())); - assert_eq!(poll_once(&mut condition2).await, None); + view.update(&mut app, |view, ctx| view.inc(ctx)); + assert_eq!(poll_once(&mut condition1).await, Some(())); + assert_eq!(poll_once(&mut condition2).await, None); - view.update(&mut app, |view, ctx| view.inc(ctx)); - assert_eq!(poll_once(&mut condition2).await, Some(())); - view.update(&mut app, |_, ctx| ctx.notify()); - }); + view.update(&mut app, |view, ctx| view.inc(ctx)); + assert_eq!(poll_once(&mut condition2).await, Some(())); + view.update(&mut app, |_, ctx| ctx.notify()); } - #[test] + #[crate::test(self)] #[should_panic] - fn test_view_condition_timeout() { + async fn test_view_condition_timeout(mut app: TestAppContext) { struct View; impl super::Entity for View { @@ -3783,15 +3449,13 @@ mod tests { } } - App::test_async((), |mut app| async move { - let (_, view) = app.add_window(|_| View); - view.condition(&app, |_, _| false).await; - }); + let (_, view) = app.add_window(|_| View); + view.condition(&app, |_, _| false).await; } - #[test] + #[crate::test(self)] #[should_panic(expected = "view dropped with pending condition")] - fn test_view_condition_panic_on_drop() { + async fn test_view_condition_panic_on_drop(mut app: TestAppContext) { struct View; impl super::Entity for View { @@ -3808,17 +3472,15 @@ mod tests { } } - App::test_async((), |mut app| async move { - let window_id = app.add_window(|_| View).0; - let view = app.add_view(window_id, |_| View); + let window_id = app.add_window(|_| View).0; + let view = app.add_view(window_id, |_| View); - let condition = view.condition(&app, |_, _| false); - app.update(|_| drop(view)); - condition.await; - }); + let condition = view.condition(&app, |_, _| false); + app.update(|_| drop(view)); + condition.await; } - // #[test] + // #[crate::test(self)] // fn test_ui_and_window_updates() { // struct View { // count: usize, diff --git a/gpui/src/lib.rs b/gpui/src/lib.rs index 72e3c5a2f5..eef55742cb 100644 --- a/gpui/src/lib.rs +++ b/gpui/src/lib.rs @@ -24,6 +24,7 @@ pub mod color; pub mod json; pub mod keymap; mod platform; +pub use gpui_macros::test; pub use platform::{Event, PathPromptOptions, PromptLevel}; pub use presenter::{ AfterLayoutContext, Axis, DebugContext, EventContext, LayoutContext, PaintContext, diff --git a/gpui_macros/Cargo.toml b/gpui_macros/Cargo.toml new file mode 100644 index 0000000000..19a8e11eb6 --- /dev/null +++ b/gpui_macros/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "gpui_macros" +version = "0.1.0" +edition = "2018" + +[lib] +proc-macro = true + +[dependencies] +syn = "1.0" +quote = "1.0" diff --git a/gpui_macros/src/lib.rs b/gpui_macros/src/lib.rs new file mode 100644 index 0000000000..cdd4cacb80 --- /dev/null +++ b/gpui_macros/src/lib.rs @@ -0,0 +1,57 @@ +use std::mem; + +use proc_macro::TokenStream; +use quote::{format_ident, quote}; +use syn::{parse_macro_input, parse_quote, AttributeArgs, ItemFn, Meta, NestedMeta}; + +#[proc_macro_attribute] +pub fn test(args: TokenStream, function: TokenStream) -> TokenStream { + let mut namespace = format_ident!("gpui"); + + let args = syn::parse_macro_input!(args as AttributeArgs); + for arg in args { + match arg { + NestedMeta::Meta(Meta::Path(name)) + if name.get_ident().map_or(false, |n| n == "self") => + { + namespace = format_ident!("crate"); + } + other => { + return TokenStream::from( + syn::Error::new_spanned(other, "invalid argument").into_compile_error(), + ) + } + } + } + + let mut inner_fn = parse_macro_input!(function as ItemFn); + let inner_fn_attributes = mem::take(&mut inner_fn.attrs); + let inner_fn_name = format_ident!("_{}", inner_fn.sig.ident); + let outer_fn_name = mem::replace(&mut inner_fn.sig.ident, inner_fn_name.clone()); + let mut outer_fn: ItemFn = if inner_fn.sig.asyncness.is_some() { + parse_quote! { + #[test] + fn #outer_fn_name() { + #inner_fn + + #namespace::App::test_async((), move |ctx| async { + #inner_fn_name(ctx).await; + }); + } + } + } else { + parse_quote! { + #[test] + fn #outer_fn_name() { + #inner_fn + + #namespace::App::test((), |ctx| { + #inner_fn_name(ctx); + }); + } + } + }; + outer_fn.attrs.extend(inner_fn_attributes); + + TokenStream::from(quote!(#outer_fn)) +} diff --git a/zed/src/editor/buffer/mod.rs b/zed/src/editor/buffer/mod.rs index 1951a764b7..190564a4b4 100644 --- a/zed/src/editor/buffer/mod.rs +++ b/zed/src/editor/buffer/mod.rs @@ -4,12 +4,10 @@ mod selection; mod text; pub use anchor::*; -use futures_core::future::LocalBoxFuture; pub use point::*; use seahash::SeaHasher; pub use selection::*; use similar::{ChangeTag, TextDiff}; -use smol::future::FutureExt; pub use text::*; use crate::{ @@ -20,7 +18,7 @@ use crate::{ worktree::FileHandle, }; use anyhow::{anyhow, Result}; -use gpui::{Entity, EntityTask, ModelContext}; +use gpui::{Entity, ModelContext, Task}; use lazy_static::lazy_static; use rand::prelude::*; use std::{ @@ -385,23 +383,27 @@ impl Buffer { if file.is_deleted() { ctx.emit(Event::Dirtied); } else { - ctx.spawn( - file.load_history(ctx.as_ref()), - move |this, history, ctx| { - if let (Ok(history), true) = (history, this.version == version) { - let task = this.set_text_via_diff(history.base_text, ctx); - ctx.spawn(task, move |this, ops, ctx| { - if ops.is_some() { - this.saved_version = this.version.clone(); - this.saved_mtime = file.mtime(); - ctx.emit(Event::Reloaded); - } + ctx.spawn(|handle, mut ctx| async move { + let (current_version, history) = handle.read_with(&ctx, |this, ctx| { + (this.version.clone(), file.load_history(ctx.as_ref())) + }); + if let (Ok(history), true) = (history.await, current_version == version) + { + let operations = handle + .update(&mut ctx, |this, ctx| { + this.set_text_via_diff(history.base_text, ctx) }) - .detach(); + .await; + if operations.is_some() { + handle.update(&mut ctx, |this, ctx| { + this.saved_version = this.version.clone(); + this.saved_mtime = file.mtime(); + ctx.emit(Event::Reloaded); + }); } - }, - ) - .detach() + } + }) + .detach(); } } ctx.emit(Event::FileHandleChanged); @@ -499,21 +501,22 @@ impl Buffer { &mut self, new_file: Option, ctx: &mut ModelContext, - ) -> LocalBoxFuture<'static, Result<()>> { + ) -> Task> { let snapshot = self.snapshot(); let version = self.version.clone(); - if let Some(file) = new_file.as_ref().or(self.file.as_ref()) { - let save_task = file.save(snapshot, ctx.as_ref()); - ctx.spawn(save_task, |me, save_result, ctx| { - if save_result.is_ok() { - me.did_save(version, new_file, ctx); + let file = self.file.clone(); + + ctx.spawn(|handle, mut ctx| async move { + if let Some(file) = new_file.as_ref().or(file.as_ref()) { + let result = ctx.read(|ctx| file.save(snapshot, ctx.as_ref())).await; + if result.is_ok() { + handle.update(&mut ctx, |me, ctx| me.did_save(version, new_file, ctx)); } - save_result - }) - .boxed_local() - } else { - async { Ok(()) }.boxed_local() - } + result + } else { + Ok(()) + } + }) } fn did_save( @@ -536,20 +539,23 @@ impl Buffer { &mut self, new_text: Arc, ctx: &mut ModelContext, - ) -> EntityTask>> { + ) -> Task>> { let version = self.version.clone(); let old_text = self.text(); - ctx.spawn( - ctx.background_executor().spawn({ - let new_text = new_text.clone(); - async move { - TextDiff::from_lines(old_text.as_str(), new_text.as_ref()) - .iter_all_changes() - .map(|c| (c.tag(), c.value().len())) - .collect::>() - } - }), - move |this, diff, ctx| { + ctx.spawn(|handle, mut ctx| async move { + let diff = ctx + .background_executor() + .spawn({ + let new_text = new_text.clone(); + async move { + TextDiff::from_lines(old_text.as_str(), new_text.as_ref()) + .iter_all_changes() + .map(|c| (c.tag(), c.value().len())) + .collect::>() + } + }) + .await; + handle.update(&mut ctx, |this, ctx| { if this.version == version { this.start_transaction(None).unwrap(); let mut operations = Vec::new(); @@ -575,8 +581,8 @@ impl Buffer { } else { None } - }, - ) + }) + }) } pub fn is_dirty(&self) -> bool { @@ -2480,259 +2486,245 @@ mod tests { sync::atomic::{self, AtomicUsize}, }; - #[test] - fn test_edit() { - App::test((), |ctx| { - ctx.add_model(|ctx| { - let mut buffer = Buffer::new(0, "abc", ctx); - assert_eq!(buffer.text(), "abc"); - buffer.edit(vec![3..3], "def", None).unwrap(); - assert_eq!(buffer.text(), "abcdef"); - buffer.edit(vec![0..0], "ghi", None).unwrap(); - assert_eq!(buffer.text(), "ghiabcdef"); - buffer.edit(vec![5..5], "jkl", None).unwrap(); - assert_eq!(buffer.text(), "ghiabjklcdef"); - buffer.edit(vec![6..7], "", None).unwrap(); - assert_eq!(buffer.text(), "ghiabjlcdef"); - buffer.edit(vec![4..9], "mno", None).unwrap(); - assert_eq!(buffer.text(), "ghiamnoef"); - buffer - }); - }) - } - - #[test] - fn test_edit_events() { - App::test((), |app| { - let mut now = Instant::now(); - let buffer_1_events = Rc::new(RefCell::new(Vec::new())); - let buffer_2_events = Rc::new(RefCell::new(Vec::new())); - - let buffer1 = app.add_model(|ctx| Buffer::new(0, "abcdef", ctx)); - let buffer2 = app.add_model(|ctx| Buffer::new(1, "abcdef", ctx)); - let mut buffer_ops = Vec::new(); - buffer1.update(app, |buffer, ctx| { - let buffer_1_events = buffer_1_events.clone(); - ctx.subscribe(&buffer1, move |_, event, _| { - buffer_1_events.borrow_mut().push(event.clone()) - }); - let buffer_2_events = buffer_2_events.clone(); - ctx.subscribe(&buffer2, move |_, event, _| { - buffer_2_events.borrow_mut().push(event.clone()) - }); - - // An edit emits an edited event, followed by a dirtied event, - // since the buffer was previously in a clean state. - let ops = buffer.edit(Some(2..4), "XYZ", Some(ctx)).unwrap(); - buffer_ops.extend_from_slice(&ops); - - // An empty transaction does not emit any events. - buffer.start_transaction(None).unwrap(); - buffer.end_transaction(None, Some(ctx)).unwrap(); - - // A transaction containing two edits emits one edited event. - now += Duration::from_secs(1); - buffer.start_transaction_at(None, now).unwrap(); - let ops = buffer.edit(Some(5..5), "u", Some(ctx)).unwrap(); - buffer_ops.extend_from_slice(&ops); - let ops = buffer.edit(Some(6..6), "w", Some(ctx)).unwrap(); - buffer_ops.extend_from_slice(&ops); - buffer.end_transaction_at(None, now, Some(ctx)).unwrap(); - - // Undoing a transaction emits one edited event. - let ops = buffer.undo(Some(ctx)); - buffer_ops.extend_from_slice(&ops); - }); - - // Incorporating a set of remote ops emits a single edited event, - // followed by a dirtied event. - buffer2.update(app, |buffer, ctx| { - buffer.apply_ops(buffer_ops, Some(ctx)).unwrap(); - }); - - let buffer_1_events = buffer_1_events.borrow(); - assert_eq!( - *buffer_1_events, - vec![Event::Edited, Event::Dirtied, Event::Edited, Event::Edited] - ); - - let buffer_2_events = buffer_2_events.borrow(); - assert_eq!(*buffer_2_events, vec![Event::Edited, Event::Dirtied]); + #[gpui::test] + fn test_edit(ctx: &mut gpui::MutableAppContext) { + ctx.add_model(|ctx| { + let mut buffer = Buffer::new(0, "abc", ctx); + assert_eq!(buffer.text(), "abc"); + buffer.edit(vec![3..3], "def", None).unwrap(); + assert_eq!(buffer.text(), "abcdef"); + buffer.edit(vec![0..0], "ghi", None).unwrap(); + assert_eq!(buffer.text(), "ghiabcdef"); + buffer.edit(vec![5..5], "jkl", None).unwrap(); + assert_eq!(buffer.text(), "ghiabjklcdef"); + buffer.edit(vec![6..7], "", None).unwrap(); + assert_eq!(buffer.text(), "ghiabjlcdef"); + buffer.edit(vec![4..9], "mno", None).unwrap(); + assert_eq!(buffer.text(), "ghiamnoef"); + buffer }); } - #[test] - fn test_random_edits() { + #[gpui::test] + fn test_edit_events(app: &mut gpui::MutableAppContext) { + let mut now = Instant::now(); + let buffer_1_events = Rc::new(RefCell::new(Vec::new())); + let buffer_2_events = Rc::new(RefCell::new(Vec::new())); + + let buffer1 = app.add_model(|ctx| Buffer::new(0, "abcdef", ctx)); + let buffer2 = app.add_model(|ctx| Buffer::new(1, "abcdef", ctx)); + let mut buffer_ops = Vec::new(); + buffer1.update(app, |buffer, ctx| { + let buffer_1_events = buffer_1_events.clone(); + ctx.subscribe(&buffer1, move |_, event, _| { + buffer_1_events.borrow_mut().push(event.clone()) + }); + let buffer_2_events = buffer_2_events.clone(); + ctx.subscribe(&buffer2, move |_, event, _| { + buffer_2_events.borrow_mut().push(event.clone()) + }); + + // An edit emits an edited event, followed by a dirtied event, + // since the buffer was previously in a clean state. + let ops = buffer.edit(Some(2..4), "XYZ", Some(ctx)).unwrap(); + buffer_ops.extend_from_slice(&ops); + + // An empty transaction does not emit any events. + buffer.start_transaction(None).unwrap(); + buffer.end_transaction(None, Some(ctx)).unwrap(); + + // A transaction containing two edits emits one edited event. + now += Duration::from_secs(1); + buffer.start_transaction_at(None, now).unwrap(); + let ops = buffer.edit(Some(5..5), "u", Some(ctx)).unwrap(); + buffer_ops.extend_from_slice(&ops); + let ops = buffer.edit(Some(6..6), "w", Some(ctx)).unwrap(); + buffer_ops.extend_from_slice(&ops); + buffer.end_transaction_at(None, now, Some(ctx)).unwrap(); + + // Undoing a transaction emits one edited event. + let ops = buffer.undo(Some(ctx)); + buffer_ops.extend_from_slice(&ops); + }); + + // Incorporating a set of remote ops emits a single edited event, + // followed by a dirtied event. + buffer2.update(app, |buffer, ctx| { + buffer.apply_ops(buffer_ops, Some(ctx)).unwrap(); + }); + + let buffer_1_events = buffer_1_events.borrow(); + assert_eq!( + *buffer_1_events, + vec![Event::Edited, Event::Dirtied, Event::Edited, Event::Edited] + ); + + let buffer_2_events = buffer_2_events.borrow(); + assert_eq!(*buffer_2_events, vec![Event::Edited, Event::Dirtied]); + } + + #[gpui::test] + fn test_random_edits(ctx: &mut gpui::MutableAppContext) { for seed in 0..100 { - App::test((), |ctx| { - println!("{:?}", seed); - let mut rng = &mut StdRng::seed_from_u64(seed); + println!("{:?}", seed); + let mut rng = &mut StdRng::seed_from_u64(seed); - let reference_string_len = rng.gen_range(0..3); - let mut reference_string = RandomCharIter::new(&mut rng) - .take(reference_string_len) - .collect::(); - ctx.add_model(|ctx| { - let mut buffer = Buffer::new(0, reference_string.as_str(), ctx); - let mut buffer_versions = Vec::new(); - for _i in 0..10 { - let (old_ranges, new_text, _) = buffer.randomly_mutate(rng, None); - for old_range in old_ranges.iter().rev() { - reference_string = [ - &reference_string[0..old_range.start], - new_text.as_str(), - &reference_string[old_range.end..], - ] - .concat(); - } - assert_eq!(buffer.text(), reference_string); + let reference_string_len = rng.gen_range(0..3); + let mut reference_string = RandomCharIter::new(&mut rng) + .take(reference_string_len) + .collect::(); + ctx.add_model(|ctx| { + let mut buffer = Buffer::new(0, reference_string.as_str(), ctx); + let mut buffer_versions = Vec::new(); + for _i in 0..10 { + let (old_ranges, new_text, _) = buffer.randomly_mutate(rng, None); + for old_range in old_ranges.iter().rev() { + reference_string = [ + &reference_string[0..old_range.start], + new_text.as_str(), + &reference_string[old_range.end..], + ] + .concat(); + } + assert_eq!(buffer.text(), reference_string); - if rng.gen_bool(0.25) { - buffer.randomly_undo_redo(rng); - reference_string = buffer.text(); - } + if rng.gen_bool(0.25) { + buffer.randomly_undo_redo(rng); + reference_string = buffer.text(); + } - { - let line_lengths = line_lengths_in_range(&buffer, 0..buffer.len()); + { + let line_lengths = line_lengths_in_range(&buffer, 0..buffer.len()); - for (len, rows) in &line_lengths { - for row in rows { - assert_eq!(buffer.line_len(*row).unwrap(), *len); - } + for (len, rows) in &line_lengths { + for row in rows { + assert_eq!(buffer.line_len(*row).unwrap(), *len); } - - let (longest_column, longest_rows) = - line_lengths.iter().next_back().unwrap(); - let rightmost_point = buffer.rightmost_point(); - assert_eq!(rightmost_point.column, *longest_column); - assert!(longest_rows.contains(&rightmost_point.row)); } - for _ in 0..5 { - let end = rng.gen_range(0..buffer.len() + 1); - let start = rng.gen_range(0..end + 1); - - let line_lengths = line_lengths_in_range(&buffer, start..end); - let (longest_column, longest_rows) = - line_lengths.iter().next_back().unwrap(); - let range_sum = buffer.text_summary_for_range(start..end); - assert_eq!(range_sum.rightmost_point.column, *longest_column); - assert!(longest_rows.contains(&range_sum.rightmost_point.row)); - let range_text = &buffer.text()[start..end]; - assert_eq!(range_sum.chars, range_text.chars().count()); - assert_eq!(range_sum.bytes, range_text.len()); - } - - if rng.gen_bool(0.3) { - buffer_versions.push(buffer.clone()); - } + let (longest_column, longest_rows) = + line_lengths.iter().next_back().unwrap(); + let rightmost_point = buffer.rightmost_point(); + assert_eq!(rightmost_point.column, *longest_column); + assert!(longest_rows.contains(&rightmost_point.row)); } - for mut old_buffer in buffer_versions { - let mut delta = 0_isize; - for Edit { - old_range, - new_range, - } in buffer.edits_since(old_buffer.version.clone()) - { - let old_len = old_range.end - old_range.start; - let new_len = new_range.end - new_range.start; - let old_start = (old_range.start as isize + delta) as usize; - let new_text: String = - buffer.text_for_range(new_range).unwrap().collect(); - old_buffer - .edit(Some(old_start..old_start + old_len), new_text, None) - .unwrap(); + for _ in 0..5 { + let end = rng.gen_range(0..buffer.len() + 1); + let start = rng.gen_range(0..end + 1); - delta += new_len as isize - old_len as isize; - } - assert_eq!(old_buffer.text(), buffer.text()); + let line_lengths = line_lengths_in_range(&buffer, start..end); + let (longest_column, longest_rows) = + line_lengths.iter().next_back().unwrap(); + let range_sum = buffer.text_summary_for_range(start..end); + assert_eq!(range_sum.rightmost_point.column, *longest_column); + assert!(longest_rows.contains(&range_sum.rightmost_point.row)); + let range_text = &buffer.text()[start..end]; + assert_eq!(range_sum.chars, range_text.chars().count()); + assert_eq!(range_sum.bytes, range_text.len()); } - buffer - }) + if rng.gen_bool(0.3) { + buffer_versions.push(buffer.clone()); + } + } + + for mut old_buffer in buffer_versions { + let mut delta = 0_isize; + for Edit { + old_range, + new_range, + } in buffer.edits_since(old_buffer.version.clone()) + { + let old_len = old_range.end - old_range.start; + let new_len = new_range.end - new_range.start; + let old_start = (old_range.start as isize + delta) as usize; + let new_text: String = buffer.text_for_range(new_range).unwrap().collect(); + old_buffer + .edit(Some(old_start..old_start + old_len), new_text, None) + .unwrap(); + + delta += new_len as isize - old_len as isize; + } + assert_eq!(old_buffer.text(), buffer.text()); + } + + buffer }); } } - #[test] - fn test_line_len() { - App::test((), |ctx| { - ctx.add_model(|ctx| { - let mut buffer = Buffer::new(0, "", ctx); - buffer.edit(vec![0..0], "abcd\nefg\nhij", None).unwrap(); - buffer.edit(vec![12..12], "kl\nmno", None).unwrap(); - buffer.edit(vec![18..18], "\npqrs\n", None).unwrap(); - buffer.edit(vec![18..21], "\nPQ", None).unwrap(); + #[gpui::test] + fn test_line_len(ctx: &mut gpui::MutableAppContext) { + ctx.add_model(|ctx| { + let mut buffer = Buffer::new(0, "", ctx); + buffer.edit(vec![0..0], "abcd\nefg\nhij", None).unwrap(); + buffer.edit(vec![12..12], "kl\nmno", None).unwrap(); + buffer.edit(vec![18..18], "\npqrs\n", None).unwrap(); + buffer.edit(vec![18..21], "\nPQ", None).unwrap(); - assert_eq!(buffer.line_len(0).unwrap(), 4); - assert_eq!(buffer.line_len(1).unwrap(), 3); - assert_eq!(buffer.line_len(2).unwrap(), 5); - assert_eq!(buffer.line_len(3).unwrap(), 3); - assert_eq!(buffer.line_len(4).unwrap(), 4); - assert_eq!(buffer.line_len(5).unwrap(), 0); - assert!(buffer.line_len(6).is_err()); - buffer - }); + assert_eq!(buffer.line_len(0).unwrap(), 4); + assert_eq!(buffer.line_len(1).unwrap(), 3); + assert_eq!(buffer.line_len(2).unwrap(), 5); + assert_eq!(buffer.line_len(3).unwrap(), 3); + assert_eq!(buffer.line_len(4).unwrap(), 4); + assert_eq!(buffer.line_len(5).unwrap(), 0); + assert!(buffer.line_len(6).is_err()); + buffer }); } - #[test] - fn test_rightmost_point() { - App::test((), |ctx| { - ctx.add_model(|ctx| { - let mut buffer = Buffer::new(0, "", ctx); - assert_eq!(buffer.rightmost_point().row, 0); - buffer.edit(vec![0..0], "abcd\nefg\nhij", None).unwrap(); - assert_eq!(buffer.rightmost_point().row, 0); - buffer.edit(vec![12..12], "kl\nmno", None).unwrap(); - assert_eq!(buffer.rightmost_point().row, 2); - buffer.edit(vec![18..18], "\npqrs", None).unwrap(); - assert_eq!(buffer.rightmost_point().row, 2); - buffer.edit(vec![10..12], "", None).unwrap(); - assert_eq!(buffer.rightmost_point().row, 0); - buffer.edit(vec![24..24], "tuv", None).unwrap(); - assert_eq!(buffer.rightmost_point().row, 4); - buffer - }); + #[gpui::test] + fn test_rightmost_point(ctx: &mut gpui::MutableAppContext) { + ctx.add_model(|ctx| { + let mut buffer = Buffer::new(0, "", ctx); + assert_eq!(buffer.rightmost_point().row, 0); + buffer.edit(vec![0..0], "abcd\nefg\nhij", None).unwrap(); + assert_eq!(buffer.rightmost_point().row, 0); + buffer.edit(vec![12..12], "kl\nmno", None).unwrap(); + assert_eq!(buffer.rightmost_point().row, 2); + buffer.edit(vec![18..18], "\npqrs", None).unwrap(); + assert_eq!(buffer.rightmost_point().row, 2); + buffer.edit(vec![10..12], "", None).unwrap(); + assert_eq!(buffer.rightmost_point().row, 0); + buffer.edit(vec![24..24], "tuv", None).unwrap(); + assert_eq!(buffer.rightmost_point().row, 4); + buffer }); } - #[test] - fn test_text_summary_for_range() { - App::test((), |ctx| { - ctx.add_model(|ctx| { - let buffer = Buffer::new(0, "ab\nefg\nhklm\nnopqrs\ntuvwxyz", ctx); - let text = Text::from(buffer.text()); - assert_eq!( - buffer.text_summary_for_range(1..3), - text.slice(1..3).summary() - ); - assert_eq!( - buffer.text_summary_for_range(1..12), - text.slice(1..12).summary() - ); - assert_eq!( - buffer.text_summary_for_range(0..20), - text.slice(0..20).summary() - ); - assert_eq!( - buffer.text_summary_for_range(0..22), - text.slice(0..22).summary() - ); - assert_eq!( - buffer.text_summary_for_range(7..22), - text.slice(7..22).summary() - ); - buffer - }); + #[gpui::test] + fn test_text_summary_for_range(ctx: &mut gpui::MutableAppContext) { + ctx.add_model(|ctx| { + let buffer = Buffer::new(0, "ab\nefg\nhklm\nnopqrs\ntuvwxyz", ctx); + let text = Text::from(buffer.text()); + assert_eq!( + buffer.text_summary_for_range(1..3), + text.slice(1..3).summary() + ); + assert_eq!( + buffer.text_summary_for_range(1..12), + text.slice(1..12).summary() + ); + assert_eq!( + buffer.text_summary_for_range(0..20), + text.slice(0..20).summary() + ); + assert_eq!( + buffer.text_summary_for_range(0..22), + text.slice(0..22).summary() + ); + assert_eq!( + buffer.text_summary_for_range(7..22), + text.slice(7..22).summary() + ); + buffer }); } - #[test] - fn test_chars_at() { - App::test((), |ctx| { - ctx.add_model(|ctx| { + #[gpui::test] + fn test_chars_at(ctx: &mut gpui::MutableAppContext) { + ctx.add_model(|ctx| { let mut buffer = Buffer::new(0, "", ctx); buffer.edit(vec![0..0], "abcd\nefgh\nij", None).unwrap(); buffer.edit(vec![12..12], "kl\nmno", None).unwrap(); @@ -2764,7 +2756,6 @@ mod tests { buffer }); - }); } // #[test] @@ -2881,196 +2872,192 @@ mod tests { } } - #[test] - fn test_anchors() { - App::test((), |ctx| { - ctx.add_model(|ctx| { - let mut buffer = Buffer::new(0, "", ctx); - buffer.edit(vec![0..0], "abc", None).unwrap(); - let left_anchor = buffer.anchor_before(2).unwrap(); - let right_anchor = buffer.anchor_after(2).unwrap(); + #[gpui::test] + fn test_anchors(ctx: &mut gpui::MutableAppContext) { + ctx.add_model(|ctx| { + let mut buffer = Buffer::new(0, "", ctx); + buffer.edit(vec![0..0], "abc", None).unwrap(); + let left_anchor = buffer.anchor_before(2).unwrap(); + let right_anchor = buffer.anchor_after(2).unwrap(); - buffer.edit(vec![1..1], "def\n", None).unwrap(); - assert_eq!(buffer.text(), "adef\nbc"); - assert_eq!(left_anchor.to_offset(&buffer).unwrap(), 6); - assert_eq!(right_anchor.to_offset(&buffer).unwrap(), 6); - assert_eq!( - left_anchor.to_point(&buffer).unwrap(), - Point { row: 1, column: 1 } - ); - assert_eq!( - right_anchor.to_point(&buffer).unwrap(), - Point { row: 1, column: 1 } - ); + buffer.edit(vec![1..1], "def\n", None).unwrap(); + assert_eq!(buffer.text(), "adef\nbc"); + assert_eq!(left_anchor.to_offset(&buffer).unwrap(), 6); + assert_eq!(right_anchor.to_offset(&buffer).unwrap(), 6); + assert_eq!( + left_anchor.to_point(&buffer).unwrap(), + Point { row: 1, column: 1 } + ); + assert_eq!( + right_anchor.to_point(&buffer).unwrap(), + Point { row: 1, column: 1 } + ); - buffer.edit(vec![2..3], "", None).unwrap(); - assert_eq!(buffer.text(), "adf\nbc"); - assert_eq!(left_anchor.to_offset(&buffer).unwrap(), 5); - assert_eq!(right_anchor.to_offset(&buffer).unwrap(), 5); - assert_eq!( - left_anchor.to_point(&buffer).unwrap(), - Point { row: 1, column: 1 } - ); - assert_eq!( - right_anchor.to_point(&buffer).unwrap(), - Point { row: 1, column: 1 } - ); + buffer.edit(vec![2..3], "", None).unwrap(); + assert_eq!(buffer.text(), "adf\nbc"); + assert_eq!(left_anchor.to_offset(&buffer).unwrap(), 5); + assert_eq!(right_anchor.to_offset(&buffer).unwrap(), 5); + assert_eq!( + left_anchor.to_point(&buffer).unwrap(), + Point { row: 1, column: 1 } + ); + assert_eq!( + right_anchor.to_point(&buffer).unwrap(), + Point { row: 1, column: 1 } + ); - buffer.edit(vec![5..5], "ghi\n", None).unwrap(); - assert_eq!(buffer.text(), "adf\nbghi\nc"); - assert_eq!(left_anchor.to_offset(&buffer).unwrap(), 5); - assert_eq!(right_anchor.to_offset(&buffer).unwrap(), 9); - assert_eq!( - left_anchor.to_point(&buffer).unwrap(), - Point { row: 1, column: 1 } - ); - assert_eq!( - right_anchor.to_point(&buffer).unwrap(), - Point { row: 2, column: 0 } - ); + buffer.edit(vec![5..5], "ghi\n", None).unwrap(); + assert_eq!(buffer.text(), "adf\nbghi\nc"); + assert_eq!(left_anchor.to_offset(&buffer).unwrap(), 5); + assert_eq!(right_anchor.to_offset(&buffer).unwrap(), 9); + assert_eq!( + left_anchor.to_point(&buffer).unwrap(), + Point { row: 1, column: 1 } + ); + assert_eq!( + right_anchor.to_point(&buffer).unwrap(), + Point { row: 2, column: 0 } + ); - buffer.edit(vec![7..9], "", None).unwrap(); - assert_eq!(buffer.text(), "adf\nbghc"); - assert_eq!(left_anchor.to_offset(&buffer).unwrap(), 5); - assert_eq!(right_anchor.to_offset(&buffer).unwrap(), 7); - assert_eq!( - left_anchor.to_point(&buffer).unwrap(), - Point { row: 1, column: 1 }, - ); - assert_eq!( - right_anchor.to_point(&buffer).unwrap(), - Point { row: 1, column: 3 } - ); + buffer.edit(vec![7..9], "", None).unwrap(); + assert_eq!(buffer.text(), "adf\nbghc"); + assert_eq!(left_anchor.to_offset(&buffer).unwrap(), 5); + assert_eq!(right_anchor.to_offset(&buffer).unwrap(), 7); + assert_eq!( + left_anchor.to_point(&buffer).unwrap(), + Point { row: 1, column: 1 }, + ); + assert_eq!( + right_anchor.to_point(&buffer).unwrap(), + Point { row: 1, column: 3 } + ); - // Ensure anchoring to a point is equivalent to anchoring to an offset. - assert_eq!( - buffer.anchor_before(Point { row: 0, column: 0 }).unwrap(), - buffer.anchor_before(0).unwrap() - ); - assert_eq!( - buffer.anchor_before(Point { row: 0, column: 1 }).unwrap(), - buffer.anchor_before(1).unwrap() - ); - assert_eq!( - buffer.anchor_before(Point { row: 0, column: 2 }).unwrap(), - buffer.anchor_before(2).unwrap() - ); - assert_eq!( - buffer.anchor_before(Point { row: 0, column: 3 }).unwrap(), - buffer.anchor_before(3).unwrap() - ); - assert_eq!( - buffer.anchor_before(Point { row: 1, column: 0 }).unwrap(), - buffer.anchor_before(4).unwrap() - ); - assert_eq!( - buffer.anchor_before(Point { row: 1, column: 1 }).unwrap(), - buffer.anchor_before(5).unwrap() - ); - assert_eq!( - buffer.anchor_before(Point { row: 1, column: 2 }).unwrap(), - buffer.anchor_before(6).unwrap() - ); - assert_eq!( - buffer.anchor_before(Point { row: 1, column: 3 }).unwrap(), - buffer.anchor_before(7).unwrap() - ); - assert_eq!( - buffer.anchor_before(Point { row: 1, column: 4 }).unwrap(), - buffer.anchor_before(8).unwrap() - ); + // Ensure anchoring to a point is equivalent to anchoring to an offset. + assert_eq!( + buffer.anchor_before(Point { row: 0, column: 0 }).unwrap(), + buffer.anchor_before(0).unwrap() + ); + assert_eq!( + buffer.anchor_before(Point { row: 0, column: 1 }).unwrap(), + buffer.anchor_before(1).unwrap() + ); + assert_eq!( + buffer.anchor_before(Point { row: 0, column: 2 }).unwrap(), + buffer.anchor_before(2).unwrap() + ); + assert_eq!( + buffer.anchor_before(Point { row: 0, column: 3 }).unwrap(), + buffer.anchor_before(3).unwrap() + ); + assert_eq!( + buffer.anchor_before(Point { row: 1, column: 0 }).unwrap(), + buffer.anchor_before(4).unwrap() + ); + assert_eq!( + buffer.anchor_before(Point { row: 1, column: 1 }).unwrap(), + buffer.anchor_before(5).unwrap() + ); + assert_eq!( + buffer.anchor_before(Point { row: 1, column: 2 }).unwrap(), + buffer.anchor_before(6).unwrap() + ); + assert_eq!( + buffer.anchor_before(Point { row: 1, column: 3 }).unwrap(), + buffer.anchor_before(7).unwrap() + ); + assert_eq!( + buffer.anchor_before(Point { row: 1, column: 4 }).unwrap(), + buffer.anchor_before(8).unwrap() + ); - // Comparison between anchors. - let anchor_at_offset_0 = buffer.anchor_before(0).unwrap(); - let anchor_at_offset_1 = buffer.anchor_before(1).unwrap(); - let anchor_at_offset_2 = buffer.anchor_before(2).unwrap(); + // Comparison between anchors. + let anchor_at_offset_0 = buffer.anchor_before(0).unwrap(); + let anchor_at_offset_1 = buffer.anchor_before(1).unwrap(); + let anchor_at_offset_2 = buffer.anchor_before(2).unwrap(); - assert_eq!( - anchor_at_offset_0 - .cmp(&anchor_at_offset_0, &buffer) - .unwrap(), - Ordering::Equal - ); - assert_eq!( - anchor_at_offset_1 - .cmp(&anchor_at_offset_1, &buffer) - .unwrap(), - Ordering::Equal - ); - assert_eq!( - anchor_at_offset_2 - .cmp(&anchor_at_offset_2, &buffer) - .unwrap(), - Ordering::Equal - ); + assert_eq!( + anchor_at_offset_0 + .cmp(&anchor_at_offset_0, &buffer) + .unwrap(), + Ordering::Equal + ); + assert_eq!( + anchor_at_offset_1 + .cmp(&anchor_at_offset_1, &buffer) + .unwrap(), + Ordering::Equal + ); + assert_eq!( + anchor_at_offset_2 + .cmp(&anchor_at_offset_2, &buffer) + .unwrap(), + Ordering::Equal + ); - assert_eq!( - anchor_at_offset_0 - .cmp(&anchor_at_offset_1, &buffer) - .unwrap(), - Ordering::Less - ); - assert_eq!( - anchor_at_offset_1 - .cmp(&anchor_at_offset_2, &buffer) - .unwrap(), - Ordering::Less - ); - assert_eq!( - anchor_at_offset_0 - .cmp(&anchor_at_offset_2, &buffer) - .unwrap(), - Ordering::Less - ); + assert_eq!( + anchor_at_offset_0 + .cmp(&anchor_at_offset_1, &buffer) + .unwrap(), + Ordering::Less + ); + assert_eq!( + anchor_at_offset_1 + .cmp(&anchor_at_offset_2, &buffer) + .unwrap(), + Ordering::Less + ); + assert_eq!( + anchor_at_offset_0 + .cmp(&anchor_at_offset_2, &buffer) + .unwrap(), + Ordering::Less + ); - assert_eq!( - anchor_at_offset_1 - .cmp(&anchor_at_offset_0, &buffer) - .unwrap(), - Ordering::Greater - ); - assert_eq!( - anchor_at_offset_2 - .cmp(&anchor_at_offset_1, &buffer) - .unwrap(), - Ordering::Greater - ); - assert_eq!( - anchor_at_offset_2 - .cmp(&anchor_at_offset_0, &buffer) - .unwrap(), - Ordering::Greater - ); - buffer - }); + assert_eq!( + anchor_at_offset_1 + .cmp(&anchor_at_offset_0, &buffer) + .unwrap(), + Ordering::Greater + ); + assert_eq!( + anchor_at_offset_2 + .cmp(&anchor_at_offset_1, &buffer) + .unwrap(), + Ordering::Greater + ); + assert_eq!( + anchor_at_offset_2 + .cmp(&anchor_at_offset_0, &buffer) + .unwrap(), + Ordering::Greater + ); + buffer }); } - #[test] - fn test_anchors_at_start_and_end() { - App::test((), |ctx| { - ctx.add_model(|ctx| { - let mut buffer = Buffer::new(0, "", ctx); - let before_start_anchor = buffer.anchor_before(0).unwrap(); - let after_end_anchor = buffer.anchor_after(0).unwrap(); + #[gpui::test] + fn test_anchors_at_start_and_end(ctx: &mut gpui::MutableAppContext) { + ctx.add_model(|ctx| { + let mut buffer = Buffer::new(0, "", ctx); + let before_start_anchor = buffer.anchor_before(0).unwrap(); + let after_end_anchor = buffer.anchor_after(0).unwrap(); - buffer.edit(vec![0..0], "abc", None).unwrap(); - assert_eq!(buffer.text(), "abc"); - assert_eq!(before_start_anchor.to_offset(&buffer).unwrap(), 0); - assert_eq!(after_end_anchor.to_offset(&buffer).unwrap(), 3); + buffer.edit(vec![0..0], "abc", None).unwrap(); + assert_eq!(buffer.text(), "abc"); + assert_eq!(before_start_anchor.to_offset(&buffer).unwrap(), 0); + assert_eq!(after_end_anchor.to_offset(&buffer).unwrap(), 3); - let after_start_anchor = buffer.anchor_after(0).unwrap(); - let before_end_anchor = buffer.anchor_before(3).unwrap(); + let after_start_anchor = buffer.anchor_after(0).unwrap(); + let before_end_anchor = buffer.anchor_before(3).unwrap(); - buffer.edit(vec![3..3], "def", None).unwrap(); - buffer.edit(vec![0..0], "ghi", None).unwrap(); - assert_eq!(buffer.text(), "ghiabcdef"); - assert_eq!(before_start_anchor.to_offset(&buffer).unwrap(), 0); - assert_eq!(after_start_anchor.to_offset(&buffer).unwrap(), 3); - assert_eq!(before_end_anchor.to_offset(&buffer).unwrap(), 6); - assert_eq!(after_end_anchor.to_offset(&buffer).unwrap(), 9); - buffer - }); + buffer.edit(vec![3..3], "def", None).unwrap(); + buffer.edit(vec![0..0], "ghi", None).unwrap(); + assert_eq!(buffer.text(), "ghiabcdef"); + assert_eq!(before_start_anchor.to_offset(&buffer).unwrap(), 0); + assert_eq!(after_start_anchor.to_offset(&buffer).unwrap(), 3); + assert_eq!(before_end_anchor.to_offset(&buffer).unwrap(), 6); + assert_eq!(after_end_anchor.to_offset(&buffer).unwrap(), 9); + buffer }); } @@ -3190,227 +3177,219 @@ mod tests { }); } - #[test] - fn test_file_changes_on_disk() { - App::test_async((), |mut app| async move { - let initial_contents = "aaa\nbbbbb\nc\n"; - let dir = temp_tree(json!({ "the-file": initial_contents })); - let tree = app.add_model(|ctx| Worktree::new(dir.path(), ctx)); - app.read(|ctx| tree.read(ctx).scan_complete()).await; + #[gpui::test] + async fn test_file_changes_on_disk(mut app: gpui::TestAppContext) { + let initial_contents = "aaa\nbbbbb\nc\n"; + let dir = temp_tree(json!({ "the-file": initial_contents })); + let tree = app.add_model(|ctx| Worktree::new(dir.path(), ctx)); + app.read(|ctx| tree.read(ctx).scan_complete()).await; - let abs_path = dir.path().join("the-file"); - let file = app.read(|ctx| tree.file("the-file", ctx)); - let buffer = app.add_model(|ctx| { - Buffer::from_history(0, History::new(initial_contents.into()), Some(file), ctx) - }); + let abs_path = dir.path().join("the-file"); + let file = app.read(|ctx| tree.file("the-file", ctx)); + let buffer = app.add_model(|ctx| { + Buffer::from_history(0, History::new(initial_contents.into()), Some(file), ctx) + }); - // Add a cursor at the start of each row. - let (selection_set_id, _) = buffer.update(&mut app, |buffer, ctx| { - assert!(!buffer.is_dirty()); - buffer.add_selection_set( - (0..3) - .map(|row| { - let anchor = buffer - .anchor_at(Point::new(row, 0), AnchorBias::Right) - .unwrap(); - Selection { - id: row as usize, - start: anchor.clone(), - end: anchor, - reversed: false, - goal: SelectionGoal::None, - } - }) - .collect::>(), - Some(ctx), - ) - }); - - // Change the file on disk, adding two new lines of text, and removing - // one line. - buffer.update(&mut app, |buffer, _| { - assert!(!buffer.is_dirty()); - assert!(!buffer.has_conflict()); - }); - tree.flush_fs_events(&app).await; - let new_contents = "AAAA\naaa\nBB\nbbbbb\n"; - - fs::write(&abs_path, new_contents).unwrap(); - - // Because the buffer was not modified, it is reloaded from disk. Its - // contents are edited according to the diff between the old and new - // file contents. - buffer - .condition_with_duration(Duration::from_millis(500), &app, |buffer, _| { - buffer.text() != initial_contents - }) - .await; - - buffer.update(&mut app, |buffer, _| { - assert_eq!(buffer.text(), new_contents); - assert!(!buffer.is_dirty()); - assert!(!buffer.has_conflict()); - - let selections = buffer.selections(selection_set_id).unwrap(); - let cursor_positions = selections - .iter() - .map(|selection| { - assert_eq!(selection.start, selection.end); - selection.start.to_point(&buffer).unwrap() + // Add a cursor at the start of each row. + let (selection_set_id, _) = buffer.update(&mut app, |buffer, ctx| { + assert!(!buffer.is_dirty()); + buffer.add_selection_set( + (0..3) + .map(|row| { + let anchor = buffer + .anchor_at(Point::new(row, 0), AnchorBias::Right) + .unwrap(); + Selection { + id: row as usize, + start: anchor.clone(), + end: anchor, + reversed: false, + goal: SelectionGoal::None, + } }) - .collect::>(); - assert_eq!( - cursor_positions, - &[Point::new(1, 0), Point::new(3, 0), Point::new(4, 0),] - ); - }); + .collect::>(), + Some(ctx), + ) + }); - // Modify the buffer - buffer.update(&mut app, |buffer, ctx| { - buffer.edit(vec![0..0], " ", Some(ctx)).unwrap(); - assert!(buffer.is_dirty()); - }); + // Change the file on disk, adding two new lines of text, and removing + // one line. + buffer.update(&mut app, |buffer, _| { + assert!(!buffer.is_dirty()); + assert!(!buffer.has_conflict()); + }); + tree.flush_fs_events(&app).await; + let new_contents = "AAAA\naaa\nBB\nbbbbb\n"; - // Change the file on disk again, adding blank lines to the beginning. - fs::write(&abs_path, "\n\n\nAAAA\naaa\nBB\nbbbbb\n").unwrap(); + fs::write(&abs_path, new_contents).unwrap(); + + // Because the buffer was not modified, it is reloaded from disk. Its + // contents are edited according to the diff between the old and new + // file contents. + buffer + .condition_with_duration(Duration::from_millis(500), &app, |buffer, _| { + buffer.text() != initial_contents + }) + .await; + + buffer.update(&mut app, |buffer, _| { + assert_eq!(buffer.text(), new_contents); + assert!(!buffer.is_dirty()); + assert!(!buffer.has_conflict()); + + let selections = buffer.selections(selection_set_id).unwrap(); + let cursor_positions = selections + .iter() + .map(|selection| { + assert_eq!(selection.start, selection.end); + selection.start.to_point(&buffer).unwrap() + }) + .collect::>(); + assert_eq!( + cursor_positions, + &[Point::new(1, 0), Point::new(3, 0), Point::new(4, 0),] + ); + }); + + // Modify the buffer + buffer.update(&mut app, |buffer, ctx| { + buffer.edit(vec![0..0], " ", Some(ctx)).unwrap(); + assert!(buffer.is_dirty()); + }); + + // Change the file on disk again, adding blank lines to the beginning. + fs::write(&abs_path, "\n\n\nAAAA\naaa\nBB\nbbbbb\n").unwrap(); + + // Becaues the buffer is modified, it doesn't reload from disk, but is + // marked as having a conflict. + buffer + .condition(&app, |buffer, _| buffer.has_conflict()) + .await; + } + + #[gpui::test] + async fn test_set_text_via_diff(mut app: gpui::TestAppContext) { + let text = "a\nbb\nccc\ndddd\neeeee\nffffff\n"; + let buffer = app.add_model(|ctx| Buffer::new(0, text, ctx)); + + let text = "a\nccc\ndddd\nffffff\n"; + buffer + .update(&mut app, |b, ctx| b.set_text_via_diff(text.into(), ctx)) + .await; + app.read(|ctx| assert_eq!(buffer.read(ctx).text(), text)); + + let text = "a\n1\n\nccc\ndd2dd\nffffff\n"; + buffer + .update(&mut app, |b, ctx| b.set_text_via_diff(text.into(), ctx)) + .await; + app.read(|ctx| assert_eq!(buffer.read(ctx).text(), text)); + } + + #[gpui::test] + fn test_undo_redo(app: &mut gpui::MutableAppContext) { + app.add_model(|ctx| { + let mut buffer = Buffer::new(0, "1234", ctx); + + let edit1 = buffer.edit(vec![1..1], "abx", None).unwrap(); + let edit2 = buffer.edit(vec![3..4], "yzef", None).unwrap(); + let edit3 = buffer.edit(vec![3..5], "cd", None).unwrap(); + assert_eq!(buffer.text(), "1abcdef234"); + + buffer.undo_or_redo(edit1[0].edit_id().unwrap()).unwrap(); + assert_eq!(buffer.text(), "1cdef234"); + buffer.undo_or_redo(edit1[0].edit_id().unwrap()).unwrap(); + assert_eq!(buffer.text(), "1abcdef234"); + + buffer.undo_or_redo(edit2[0].edit_id().unwrap()).unwrap(); + assert_eq!(buffer.text(), "1abcdx234"); + buffer.undo_or_redo(edit3[0].edit_id().unwrap()).unwrap(); + assert_eq!(buffer.text(), "1abx234"); + buffer.undo_or_redo(edit2[0].edit_id().unwrap()).unwrap(); + assert_eq!(buffer.text(), "1abyzef234"); + buffer.undo_or_redo(edit3[0].edit_id().unwrap()).unwrap(); + assert_eq!(buffer.text(), "1abcdef234"); + + buffer.undo_or_redo(edit3[0].edit_id().unwrap()).unwrap(); + assert_eq!(buffer.text(), "1abyzef234"); + buffer.undo_or_redo(edit1[0].edit_id().unwrap()).unwrap(); + assert_eq!(buffer.text(), "1yzef234"); + buffer.undo_or_redo(edit2[0].edit_id().unwrap()).unwrap(); + assert_eq!(buffer.text(), "1234"); - // Becaues the buffer is modified, it doesn't reload from disk, but is - // marked as having a conflict. buffer - .condition(&app, |buffer, _| buffer.has_conflict()) - .await; }); } - #[test] - fn test_set_text_via_diff() { - App::test_async((), |mut app| async move { - let text = "a\nbb\nccc\ndddd\neeeee\nffffff\n"; - let buffer = app.add_model(|ctx| Buffer::new(0, text, ctx)); + #[gpui::test] + fn test_history(app: &mut gpui::MutableAppContext) { + app.add_model(|ctx| { + let mut now = Instant::now(); + let mut buffer = Buffer::new(0, "123456", ctx); - let text = "a\nccc\ndddd\nffffff\n"; + let (set_id, _) = + buffer.add_selection_set(buffer.selections_from_ranges(vec![4..4]).unwrap(), None); + buffer.start_transaction_at(Some(set_id), now).unwrap(); + buffer.edit(vec![2..4], "cd", None).unwrap(); + buffer.end_transaction_at(Some(set_id), now, None).unwrap(); + assert_eq!(buffer.text(), "12cd56"); + assert_eq!(buffer.selection_ranges(set_id).unwrap(), vec![4..4]); + + buffer.start_transaction_at(Some(set_id), now).unwrap(); buffer - .update(&mut app, |b, ctx| b.set_text_via_diff(text.into(), ctx)) - .await; - app.read(|ctx| assert_eq!(buffer.read(ctx).text(), text)); + .update_selection_set( + set_id, + buffer.selections_from_ranges(vec![1..3]).unwrap(), + None, + ) + .unwrap(); + buffer.edit(vec![4..5], "e", None).unwrap(); + buffer.end_transaction_at(Some(set_id), now, None).unwrap(); + assert_eq!(buffer.text(), "12cde6"); + assert_eq!(buffer.selection_ranges(set_id).unwrap(), vec![1..3]); + + now += UNDO_GROUP_INTERVAL + Duration::from_millis(1); + buffer.start_transaction_at(Some(set_id), now).unwrap(); + buffer + .update_selection_set( + set_id, + buffer.selections_from_ranges(vec![2..2]).unwrap(), + None, + ) + .unwrap(); + buffer.edit(vec![0..1], "a", None).unwrap(); + buffer.edit(vec![1..1], "b", None).unwrap(); + buffer.end_transaction_at(Some(set_id), now, None).unwrap(); + assert_eq!(buffer.text(), "ab2cde6"); + assert_eq!(buffer.selection_ranges(set_id).unwrap(), vec![3..3]); + + // Last transaction happened past the group interval, undo it on its + // own. + buffer.undo(None); + assert_eq!(buffer.text(), "12cde6"); + assert_eq!(buffer.selection_ranges(set_id).unwrap(), vec![1..3]); + + // First two transactions happened within the group interval, undo them + // together. + buffer.undo(None); + assert_eq!(buffer.text(), "123456"); + assert_eq!(buffer.selection_ranges(set_id).unwrap(), vec![4..4]); + + // Redo the first two transactions together. + buffer.redo(None); + assert_eq!(buffer.text(), "12cde6"); + assert_eq!(buffer.selection_ranges(set_id).unwrap(), vec![1..3]); + + // Redo the last transaction on its own. + buffer.redo(None); + assert_eq!(buffer.text(), "ab2cde6"); + assert_eq!(buffer.selection_ranges(set_id).unwrap(), vec![3..3]); - let text = "a\n1\n\nccc\ndd2dd\nffffff\n"; buffer - .update(&mut app, |b, ctx| b.set_text_via_diff(text.into(), ctx)) - .await; - app.read(|ctx| assert_eq!(buffer.read(ctx).text(), text)); }); } - #[test] - fn test_undo_redo() { - App::test((), |app| { - app.add_model(|ctx| { - let mut buffer = Buffer::new(0, "1234", ctx); - - let edit1 = buffer.edit(vec![1..1], "abx", None).unwrap(); - let edit2 = buffer.edit(vec![3..4], "yzef", None).unwrap(); - let edit3 = buffer.edit(vec![3..5], "cd", None).unwrap(); - assert_eq!(buffer.text(), "1abcdef234"); - - buffer.undo_or_redo(edit1[0].edit_id().unwrap()).unwrap(); - assert_eq!(buffer.text(), "1cdef234"); - buffer.undo_or_redo(edit1[0].edit_id().unwrap()).unwrap(); - assert_eq!(buffer.text(), "1abcdef234"); - - buffer.undo_or_redo(edit2[0].edit_id().unwrap()).unwrap(); - assert_eq!(buffer.text(), "1abcdx234"); - buffer.undo_or_redo(edit3[0].edit_id().unwrap()).unwrap(); - assert_eq!(buffer.text(), "1abx234"); - buffer.undo_or_redo(edit2[0].edit_id().unwrap()).unwrap(); - assert_eq!(buffer.text(), "1abyzef234"); - buffer.undo_or_redo(edit3[0].edit_id().unwrap()).unwrap(); - assert_eq!(buffer.text(), "1abcdef234"); - - buffer.undo_or_redo(edit3[0].edit_id().unwrap()).unwrap(); - assert_eq!(buffer.text(), "1abyzef234"); - buffer.undo_or_redo(edit1[0].edit_id().unwrap()).unwrap(); - assert_eq!(buffer.text(), "1yzef234"); - buffer.undo_or_redo(edit2[0].edit_id().unwrap()).unwrap(); - assert_eq!(buffer.text(), "1234"); - - buffer - }); - }); - } - - #[test] - fn test_history() { - App::test((), |app| { - app.add_model(|ctx| { - let mut now = Instant::now(); - let mut buffer = Buffer::new(0, "123456", ctx); - - let (set_id, _) = buffer - .add_selection_set(buffer.selections_from_ranges(vec![4..4]).unwrap(), None); - buffer.start_transaction_at(Some(set_id), now).unwrap(); - buffer.edit(vec![2..4], "cd", None).unwrap(); - buffer.end_transaction_at(Some(set_id), now, None).unwrap(); - assert_eq!(buffer.text(), "12cd56"); - assert_eq!(buffer.selection_ranges(set_id).unwrap(), vec![4..4]); - - buffer.start_transaction_at(Some(set_id), now).unwrap(); - buffer - .update_selection_set( - set_id, - buffer.selections_from_ranges(vec![1..3]).unwrap(), - None, - ) - .unwrap(); - buffer.edit(vec![4..5], "e", None).unwrap(); - buffer.end_transaction_at(Some(set_id), now, None).unwrap(); - assert_eq!(buffer.text(), "12cde6"); - assert_eq!(buffer.selection_ranges(set_id).unwrap(), vec![1..3]); - - now += UNDO_GROUP_INTERVAL + Duration::from_millis(1); - buffer.start_transaction_at(Some(set_id), now).unwrap(); - buffer - .update_selection_set( - set_id, - buffer.selections_from_ranges(vec![2..2]).unwrap(), - None, - ) - .unwrap(); - buffer.edit(vec![0..1], "a", None).unwrap(); - buffer.edit(vec![1..1], "b", None).unwrap(); - buffer.end_transaction_at(Some(set_id), now, None).unwrap(); - assert_eq!(buffer.text(), "ab2cde6"); - assert_eq!(buffer.selection_ranges(set_id).unwrap(), vec![3..3]); - - // Last transaction happened past the group interval, undo it on its - // own. - buffer.undo(None); - assert_eq!(buffer.text(), "12cde6"); - assert_eq!(buffer.selection_ranges(set_id).unwrap(), vec![1..3]); - - // First two transactions happened within the group interval, undo them - // together. - buffer.undo(None); - assert_eq!(buffer.text(), "123456"); - assert_eq!(buffer.selection_ranges(set_id).unwrap(), vec![4..4]); - - // Redo the first two transactions together. - buffer.redo(None); - assert_eq!(buffer.text(), "12cde6"); - assert_eq!(buffer.selection_ranges(set_id).unwrap(), vec![1..3]); - - // Redo the last transaction on its own. - buffer.redo(None); - assert_eq!(buffer.text(), "ab2cde6"); - assert_eq!(buffer.selection_ranges(set_id).unwrap(), vec![3..3]); - - buffer - }); - }); - } - - #[test] - fn test_random_concurrent_edits() { + #[gpui::test] + fn test_random_concurrent_edits(ctx: &mut gpui::MutableAppContext) { use crate::test::Network; const PEERS: usize = 5; @@ -3419,66 +3398,64 @@ mod tests { println!("{:?}", seed); let mut rng = &mut StdRng::seed_from_u64(seed); - App::test((), |ctx| { - let base_text_len = rng.gen_range(0..10); - let base_text = RandomCharIter::new(&mut rng) - .take(base_text_len) - .collect::(); - let mut replica_ids = Vec::new(); - let mut buffers = Vec::new(); - let mut network = Network::new(); - for i in 0..PEERS { - let buffer = - ctx.add_model(|ctx| Buffer::new(i as ReplicaId, base_text.as_str(), ctx)); - buffers.push(buffer); - replica_ids.push(i as u16); - network.add_peer(i as u16); - } + let base_text_len = rng.gen_range(0..10); + let base_text = RandomCharIter::new(&mut rng) + .take(base_text_len) + .collect::(); + let mut replica_ids = Vec::new(); + let mut buffers = Vec::new(); + let mut network = Network::new(); + for i in 0..PEERS { + let buffer = + ctx.add_model(|ctx| Buffer::new(i as ReplicaId, base_text.as_str(), ctx)); + buffers.push(buffer); + replica_ids.push(i as u16); + network.add_peer(i as u16); + } - let mut mutation_count = 10; - loop { - let replica_index = rng.gen_range(0..PEERS); - let replica_id = replica_ids[replica_index]; - buffers[replica_index].update(ctx, |buffer, _| match rng.gen_range(0..=100) { - 0..=50 if mutation_count != 0 => { - let (_, _, ops) = buffer.randomly_mutate(&mut rng, None); - network.broadcast(replica_id, ops, &mut rng); - mutation_count -= 1; - } - 51..=70 if mutation_count != 0 => { - let ops = buffer.randomly_undo_redo(&mut rng); - network.broadcast(replica_id, ops, &mut rng); - mutation_count -= 1; - } - 71..=100 if network.has_unreceived(replica_id) => { - buffer - .apply_ops(network.receive(replica_id, &mut rng), None) - .unwrap(); - } - _ => {} - }); - - if mutation_count == 0 && network.is_idle() { - break; + let mut mutation_count = 10; + loop { + let replica_index = rng.gen_range(0..PEERS); + let replica_id = replica_ids[replica_index]; + buffers[replica_index].update(ctx, |buffer, _| match rng.gen_range(0..=100) { + 0..=50 if mutation_count != 0 => { + let (_, _, ops) = buffer.randomly_mutate(&mut rng, None); + network.broadcast(replica_id, ops, &mut rng); + mutation_count -= 1; } - } + 51..=70 if mutation_count != 0 => { + let ops = buffer.randomly_undo_redo(&mut rng); + network.broadcast(replica_id, ops, &mut rng); + mutation_count -= 1; + } + 71..=100 if network.has_unreceived(replica_id) => { + buffer + .apply_ops(network.receive(replica_id, &mut rng), None) + .unwrap(); + } + _ => {} + }); - let first_buffer = buffers[0].read(ctx); - for buffer in &buffers[1..] { - let buffer = buffer.read(ctx); - assert_eq!(buffer.text(), first_buffer.text()); - assert_eq!( - buffer.all_selections().collect::>(), - first_buffer.all_selections().collect::>() - ); - assert_eq!( - buffer.all_selection_ranges().collect::>(), - first_buffer - .all_selection_ranges() - .collect::>() - ); + if mutation_count == 0 && network.is_idle() { + break; } - }); + } + + let first_buffer = buffers[0].read(ctx); + for buffer in &buffers[1..] { + let buffer = buffer.read(ctx); + assert_eq!(buffer.text(), first_buffer.text()); + assert_eq!( + buffer.all_selections().collect::>(), + first_buffer.all_selections().collect::>() + ); + assert_eq!( + buffer.all_selection_ranges().collect::>(), + first_buffer + .all_selection_ranges() + .collect::>() + ); + } } } diff --git a/zed/src/editor/buffer_view.rs b/zed/src/editor/buffer_view.rs index c2aef72ff1..3319e81f41 100644 --- a/zed/src/editor/buffer_view.rs +++ b/zed/src/editor/buffer_view.rs @@ -4,11 +4,10 @@ use super::{ }; use crate::{settings::Settings, util::post_inc, workspace, worktree::FileHandle}; use anyhow::Result; -use futures_core::future::LocalBoxFuture; use gpui::{ fonts::Properties as FontProperties, geometry::vector::Vector2F, keymap::Binding, text_layout, AppContext, ClipboardItem, Element, ElementBox, Entity, FontCache, ModelHandle, - MutableAppContext, TextLayoutCache, View, ViewContext, WeakViewHandle, + MutableAppContext, Task, TextLayoutCache, View, ViewContext, WeakViewHandle, }; use parking_lot::Mutex; use postage::watch; @@ -2348,13 +2347,12 @@ impl BufferView { ctx.notify(); let epoch = self.next_blink_epoch(); - ctx.spawn( - async move { - Timer::after(CURSOR_BLINK_INTERVAL).await; - epoch - }, - Self::resume_cursor_blinking, - ) + ctx.spawn(|this, mut ctx| async move { + Timer::after(CURSOR_BLINK_INTERVAL).await; + this.update(&mut ctx, |this, ctx| { + this.resume_cursor_blinking(epoch, ctx); + }) + }) .detach(); } @@ -2371,13 +2369,10 @@ impl BufferView { ctx.notify(); let epoch = self.next_blink_epoch(); - ctx.spawn( - async move { - Timer::after(CURSOR_BLINK_INTERVAL).await; - epoch - }, - Self::blink_cursors, - ) + ctx.spawn(|this, mut ctx| async move { + Timer::after(CURSOR_BLINK_INTERVAL).await; + this.update(&mut ctx, |this, ctx| this.blink_cursors(epoch, ctx)); + }) .detach(); } } @@ -2499,7 +2494,7 @@ impl workspace::ItemView for BufferView { &mut self, new_file: Option, ctx: &mut ViewContext, - ) -> LocalBoxFuture<'static, Result<()>> { + ) -> Task> { self.buffer.update(ctx, |b, ctx| b.save(new_file, ctx)) } @@ -2516,222 +2511,207 @@ impl workspace::ItemView for BufferView { mod tests { use super::*; use crate::{editor::Point, settings, test::sample_text}; - use gpui::App; use unindent::Unindent; - #[test] - fn test_selection_with_mouse() { - App::test((), |app| { - let buffer = - app.add_model(|ctx| Buffer::new(0, "aaaaaa\nbbbbbb\ncccccc\ndddddd\n", ctx)); - let settings = settings::channel(&app.font_cache()).unwrap().1; - let (_, buffer_view) = - app.add_window(|ctx| BufferView::for_buffer(buffer, settings, ctx)); + #[gpui::test] + fn test_selection_with_mouse(app: &mut gpui::MutableAppContext) { + let buffer = app.add_model(|ctx| Buffer::new(0, "aaaaaa\nbbbbbb\ncccccc\ndddddd\n", ctx)); + let settings = settings::channel(&app.font_cache()).unwrap().1; + let (_, buffer_view) = app.add_window(|ctx| BufferView::for_buffer(buffer, settings, ctx)); - buffer_view.update(app, |view, ctx| { - view.begin_selection(DisplayPoint::new(2, 2), false, ctx); - }); - - let view = buffer_view.read(app); - let selections = view - .selections_in_range( - DisplayPoint::zero()..view.max_point(app.as_ref()), - app.as_ref(), - ) - .collect::>(); - assert_eq!( - selections, - [DisplayPoint::new(2, 2)..DisplayPoint::new(2, 2)] - ); - - buffer_view.update(app, |view, ctx| { - view.update_selection(DisplayPoint::new(3, 3), Vector2F::zero(), ctx); - }); - - let view = buffer_view.read(app); - let selections = view - .selections_in_range( - DisplayPoint::zero()..view.max_point(app.as_ref()), - app.as_ref(), - ) - .collect::>(); - assert_eq!( - selections, - [DisplayPoint::new(2, 2)..DisplayPoint::new(3, 3)] - ); - - buffer_view.update(app, |view, ctx| { - view.update_selection(DisplayPoint::new(1, 1), Vector2F::zero(), ctx); - }); - - let view = buffer_view.read(app); - let selections = view - .selections_in_range( - DisplayPoint::zero()..view.max_point(app.as_ref()), - app.as_ref(), - ) - .collect::>(); - assert_eq!( - selections, - [DisplayPoint::new(2, 2)..DisplayPoint::new(1, 1)] - ); - - buffer_view.update(app, |view, ctx| { - view.end_selection(ctx); - view.update_selection(DisplayPoint::new(3, 3), Vector2F::zero(), ctx); - }); - - let view = buffer_view.read(app); - let selections = view - .selections_in_range( - DisplayPoint::zero()..view.max_point(app.as_ref()), - app.as_ref(), - ) - .collect::>(); - assert_eq!( - selections, - [DisplayPoint::new(2, 2)..DisplayPoint::new(1, 1)] - ); - - buffer_view.update(app, |view, ctx| { - view.begin_selection(DisplayPoint::new(3, 3), true, ctx); - view.update_selection(DisplayPoint::new(0, 0), Vector2F::zero(), ctx); - }); - - let view = buffer_view.read(app); - let selections = view - .selections_in_range( - DisplayPoint::zero()..view.max_point(app.as_ref()), - app.as_ref(), - ) - .collect::>(); - assert_eq!( - selections, - [ - DisplayPoint::new(2, 2)..DisplayPoint::new(1, 1), - DisplayPoint::new(3, 3)..DisplayPoint::new(0, 0) - ] - ); - - buffer_view.update(app, |view, ctx| { - view.end_selection(ctx); - }); - - let view = buffer_view.read(app); - let selections = view - .selections_in_range( - DisplayPoint::zero()..view.max_point(app.as_ref()), - app.as_ref(), - ) - .collect::>(); - assert_eq!( - selections, - [DisplayPoint::new(3, 3)..DisplayPoint::new(0, 0)] - ); + buffer_view.update(app, |view, ctx| { + view.begin_selection(DisplayPoint::new(2, 2), false, ctx); }); - } - #[test] - fn test_canceling_pending_selection() { - App::test((), |app| { - let buffer = - app.add_model(|ctx| Buffer::new(0, "aaaaaa\nbbbbbb\ncccccc\ndddddd\n", ctx)); - let settings = settings::channel(&app.font_cache()).unwrap().1; - let (_, view) = app.add_window(|ctx| BufferView::for_buffer(buffer, settings, ctx)); + let view = buffer_view.read(app); + let selections = view + .selections_in_range( + DisplayPoint::zero()..view.max_point(app.as_ref()), + app.as_ref(), + ) + .collect::>(); + assert_eq!( + selections, + [DisplayPoint::new(2, 2)..DisplayPoint::new(2, 2)] + ); - view.update(app, |view, ctx| { - view.begin_selection(DisplayPoint::new(2, 2), false, ctx); - }); - assert_eq!( - view.read(app).selection_ranges(app.as_ref()), - [DisplayPoint::new(2, 2)..DisplayPoint::new(2, 2)] - ); - - view.update(app, |view, ctx| { - view.update_selection(DisplayPoint::new(3, 3), Vector2F::zero(), ctx); - }); - assert_eq!( - view.read(app).selection_ranges(app.as_ref()), - [DisplayPoint::new(2, 2)..DisplayPoint::new(3, 3)] - ); - - view.update(app, |view, ctx| { - view.cancel(&(), ctx); - view.update_selection(DisplayPoint::new(1, 1), Vector2F::zero(), ctx); - }); - assert_eq!( - view.read(app).selection_ranges(app.as_ref()), - [DisplayPoint::new(2, 2)..DisplayPoint::new(3, 3)] - ); + buffer_view.update(app, |view, ctx| { + view.update_selection(DisplayPoint::new(3, 3), Vector2F::zero(), ctx); }); - } - #[test] - fn test_cancel() { - App::test((), |app| { - let buffer = - app.add_model(|ctx| Buffer::new(0, "aaaaaa\nbbbbbb\ncccccc\ndddddd\n", ctx)); - let settings = settings::channel(&app.font_cache()).unwrap().1; - let (_, view) = app.add_window(|ctx| BufferView::for_buffer(buffer, settings, ctx)); + let view = buffer_view.read(app); + let selections = view + .selections_in_range( + DisplayPoint::zero()..view.max_point(app.as_ref()), + app.as_ref(), + ) + .collect::>(); + assert_eq!( + selections, + [DisplayPoint::new(2, 2)..DisplayPoint::new(3, 3)] + ); - view.update(app, |view, ctx| { - view.begin_selection(DisplayPoint::new(3, 4), false, ctx); - view.update_selection(DisplayPoint::new(1, 1), Vector2F::zero(), ctx); - view.end_selection(ctx); - - view.begin_selection(DisplayPoint::new(0, 1), true, ctx); - view.update_selection(DisplayPoint::new(0, 3), Vector2F::zero(), ctx); - view.end_selection(ctx); - }); - assert_eq!( - view.read(app).selection_ranges(app.as_ref()), - [ - DisplayPoint::new(0, 1)..DisplayPoint::new(0, 3), - DisplayPoint::new(3, 4)..DisplayPoint::new(1, 1), - ] - ); - - view.update(app, |view, ctx| view.cancel(&(), ctx)); - assert_eq!( - view.read(app).selection_ranges(app.as_ref()), - [DisplayPoint::new(3, 4)..DisplayPoint::new(1, 1)] - ); - - view.update(app, |view, ctx| view.cancel(&(), ctx)); - assert_eq!( - view.read(app).selection_ranges(app.as_ref()), - [DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1)] - ); + buffer_view.update(app, |view, ctx| { + view.update_selection(DisplayPoint::new(1, 1), Vector2F::zero(), ctx); }); + + let view = buffer_view.read(app); + let selections = view + .selections_in_range( + DisplayPoint::zero()..view.max_point(app.as_ref()), + app.as_ref(), + ) + .collect::>(); + assert_eq!( + selections, + [DisplayPoint::new(2, 2)..DisplayPoint::new(1, 1)] + ); + + buffer_view.update(app, |view, ctx| { + view.end_selection(ctx); + view.update_selection(DisplayPoint::new(3, 3), Vector2F::zero(), ctx); + }); + + let view = buffer_view.read(app); + let selections = view + .selections_in_range( + DisplayPoint::zero()..view.max_point(app.as_ref()), + app.as_ref(), + ) + .collect::>(); + assert_eq!( + selections, + [DisplayPoint::new(2, 2)..DisplayPoint::new(1, 1)] + ); + + buffer_view.update(app, |view, ctx| { + view.begin_selection(DisplayPoint::new(3, 3), true, ctx); + view.update_selection(DisplayPoint::new(0, 0), Vector2F::zero(), ctx); + }); + + let view = buffer_view.read(app); + let selections = view + .selections_in_range( + DisplayPoint::zero()..view.max_point(app.as_ref()), + app.as_ref(), + ) + .collect::>(); + assert_eq!( + selections, + [ + DisplayPoint::new(2, 2)..DisplayPoint::new(1, 1), + DisplayPoint::new(3, 3)..DisplayPoint::new(0, 0) + ] + ); + + buffer_view.update(app, |view, ctx| { + view.end_selection(ctx); + }); + + let view = buffer_view.read(app); + let selections = view + .selections_in_range( + DisplayPoint::zero()..view.max_point(app.as_ref()), + app.as_ref(), + ) + .collect::>(); + assert_eq!( + selections, + [DisplayPoint::new(3, 3)..DisplayPoint::new(0, 0)] + ); } - #[test] - fn test_layout_line_numbers() { - App::test((), |app| { - let layout_cache = TextLayoutCache::new(app.platform().fonts()); - let font_cache = app.font_cache().clone(); + #[gpui::test] + fn test_canceling_pending_selection(app: &mut gpui::MutableAppContext) { + let buffer = app.add_model(|ctx| Buffer::new(0, "aaaaaa\nbbbbbb\ncccccc\ndddddd\n", ctx)); + let settings = settings::channel(&app.font_cache()).unwrap().1; + let (_, view) = app.add_window(|ctx| BufferView::for_buffer(buffer, settings, ctx)); - let buffer = app.add_model(|ctx| Buffer::new(0, sample_text(6, 6), ctx)); + view.update(app, |view, ctx| { + view.begin_selection(DisplayPoint::new(2, 2), false, ctx); + }); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + [DisplayPoint::new(2, 2)..DisplayPoint::new(2, 2)] + ); - let settings = settings::channel(&font_cache).unwrap().1; - let (_, view) = - app.add_window(|ctx| BufferView::for_buffer(buffer.clone(), settings, ctx)); + view.update(app, |view, ctx| { + view.update_selection(DisplayPoint::new(3, 3), Vector2F::zero(), ctx); + }); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + [DisplayPoint::new(2, 2)..DisplayPoint::new(3, 3)] + ); - let layouts = view - .read(app) - .layout_line_numbers(1000.0, &font_cache, &layout_cache, app.as_ref()) - .unwrap(); - assert_eq!(layouts.len(), 6); - }) + view.update(app, |view, ctx| { + view.cancel(&(), ctx); + view.update_selection(DisplayPoint::new(1, 1), Vector2F::zero(), ctx); + }); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + [DisplayPoint::new(2, 2)..DisplayPoint::new(3, 3)] + ); } - #[test] - fn test_fold() { - App::test((), |app| { - let buffer = app.add_model(|ctx| { - Buffer::new( - 0, - " + #[gpui::test] + fn test_cancel(app: &mut gpui::MutableAppContext) { + let buffer = app.add_model(|ctx| Buffer::new(0, "aaaaaa\nbbbbbb\ncccccc\ndddddd\n", ctx)); + let settings = settings::channel(&app.font_cache()).unwrap().1; + let (_, view) = app.add_window(|ctx| BufferView::for_buffer(buffer, settings, ctx)); + + view.update(app, |view, ctx| { + view.begin_selection(DisplayPoint::new(3, 4), false, ctx); + view.update_selection(DisplayPoint::new(1, 1), Vector2F::zero(), ctx); + view.end_selection(ctx); + + view.begin_selection(DisplayPoint::new(0, 1), true, ctx); + view.update_selection(DisplayPoint::new(0, 3), Vector2F::zero(), ctx); + view.end_selection(ctx); + }); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + [ + DisplayPoint::new(0, 1)..DisplayPoint::new(0, 3), + DisplayPoint::new(3, 4)..DisplayPoint::new(1, 1), + ] + ); + + view.update(app, |view, ctx| view.cancel(&(), ctx)); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + [DisplayPoint::new(3, 4)..DisplayPoint::new(1, 1)] + ); + + view.update(app, |view, ctx| view.cancel(&(), ctx)); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + [DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1)] + ); + } + + #[gpui::test] + fn test_layout_line_numbers(app: &mut gpui::MutableAppContext) { + let layout_cache = TextLayoutCache::new(app.platform().fonts()); + let font_cache = app.font_cache().clone(); + + let buffer = app.add_model(|ctx| Buffer::new(0, sample_text(6, 6), ctx)); + + let settings = settings::channel(&font_cache).unwrap().1; + let (_, view) = app.add_window(|ctx| BufferView::for_buffer(buffer.clone(), settings, ctx)); + + let layouts = view + .read(app) + .layout_line_numbers(1000.0, &font_cache, &layout_cache, app.as_ref()) + .unwrap(); + assert_eq!(layouts.len(), 6); + } + + #[gpui::test] + fn test_fold(app: &mut gpui::MutableAppContext) { + let buffer = app.add_model(|ctx| { + Buffer::new( + 0, + " impl Foo { // Hello! @@ -2748,24 +2728,20 @@ mod tests { } } " - .unindent(), - ctx, - ) - }); - let settings = settings::channel(&app.font_cache()).unwrap().1; - let (_, view) = - app.add_window(|ctx| BufferView::for_buffer(buffer.clone(), settings, ctx)); + .unindent(), + ctx, + ) + }); + let settings = settings::channel(&app.font_cache()).unwrap().1; + let (_, view) = app.add_window(|ctx| BufferView::for_buffer(buffer.clone(), settings, ctx)); - view.update(app, |view, ctx| { - view.select_display_ranges( - &[DisplayPoint::new(8, 0)..DisplayPoint::new(12, 0)], - ctx, - ) + view.update(app, |view, ctx| { + view.select_display_ranges(&[DisplayPoint::new(8, 0)..DisplayPoint::new(12, 0)], ctx) .unwrap(); - view.fold(&(), ctx); - assert_eq!( - view.text(ctx.as_ref()), - " + view.fold(&(), ctx); + assert_eq!( + view.text(ctx.as_ref()), + " impl Foo { // Hello! @@ -2780,23 +2756,23 @@ mod tests { } } " - .unindent(), - ); + .unindent(), + ); - view.fold(&(), ctx); - assert_eq!( - view.text(ctx.as_ref()), - " + view.fold(&(), ctx); + assert_eq!( + view.text(ctx.as_ref()), + " impl Foo {… } " - .unindent(), - ); + .unindent(), + ); - view.unfold(&(), ctx); - assert_eq!( - view.text(ctx.as_ref()), - " + view.unfold(&(), ctx); + assert_eq!( + view.text(ctx.as_ref()), + " impl Foo { // Hello! @@ -2811,1109 +2787,1055 @@ mod tests { } } " - .unindent(), - ); + .unindent(), + ); - view.unfold(&(), ctx); - assert_eq!(view.text(ctx.as_ref()), buffer.read(ctx).text()); - }); + view.unfold(&(), ctx); + assert_eq!(view.text(ctx.as_ref()), buffer.read(ctx).text()); }); } - #[test] - fn test_move_cursor() { - App::test((), |app| { - let buffer = app.add_model(|ctx| Buffer::new(0, sample_text(6, 6), ctx)); - let settings = settings::channel(&app.font_cache()).unwrap().1; - let (_, view) = - app.add_window(|ctx| BufferView::for_buffer(buffer.clone(), settings, ctx)); + #[gpui::test] + fn test_move_cursor(app: &mut gpui::MutableAppContext) { + let buffer = app.add_model(|ctx| Buffer::new(0, sample_text(6, 6), ctx)); + let settings = settings::channel(&app.font_cache()).unwrap().1; + let (_, view) = app.add_window(|ctx| BufferView::for_buffer(buffer.clone(), settings, ctx)); - buffer.update(app, |buffer, ctx| { - buffer - .edit( - vec![ - Point::new(1, 0)..Point::new(1, 0), - Point::new(1, 1)..Point::new(1, 1), - ], - "\t", - Some(ctx), - ) - .unwrap(); - }); - - view.update(app, |view, ctx| { - view.move_down(&(), ctx); - assert_eq!( - view.selection_ranges(ctx.as_ref()), - &[DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)] - ); - - view.move_right(&(), ctx); - assert_eq!( - view.selection_ranges(ctx.as_ref()), - &[DisplayPoint::new(1, 4)..DisplayPoint::new(1, 4)] - ); - - view.move_left(&(), ctx); - assert_eq!( - view.selection_ranges(ctx.as_ref()), - &[DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)] - ); - - view.move_up(&(), ctx); - assert_eq!( - view.selection_ranges(ctx.as_ref()), - &[DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)] - ); - - view.move_to_end(&(), ctx); - assert_eq!( - view.selection_ranges(ctx.as_ref()), - &[DisplayPoint::new(5, 6)..DisplayPoint::new(5, 6)] - ); - - view.move_to_beginning(&(), ctx); - assert_eq!( - view.selection_ranges(ctx.as_ref()), - &[DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)] - ); - - view.select_display_ranges( - &[DisplayPoint::new(0, 1)..DisplayPoint::new(0, 2)], - ctx, - ) - .unwrap(); - view.select_to_beginning(&(), ctx); - assert_eq!( - view.selection_ranges(ctx.as_ref()), - &[DisplayPoint::new(0, 1)..DisplayPoint::new(0, 0)] - ); - - view.select_to_end(&(), ctx); - assert_eq!( - view.selection_ranges(ctx.as_ref()), - &[DisplayPoint::new(0, 1)..DisplayPoint::new(5, 6)] - ); - }); - }); - } - - #[test] - fn test_beginning_end_of_line() { - App::test((), |app| { - let buffer = app.add_model(|ctx| Buffer::new(0, "abc\n def", ctx)); - let settings = settings::channel(&app.font_cache()).unwrap().1; - let (_, view) = app.add_window(|ctx| BufferView::for_buffer(buffer, settings, ctx)); - view.update(app, |view, ctx| { - view.select_display_ranges( - &[ - DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1), - DisplayPoint::new(1, 4)..DisplayPoint::new(1, 4), - ], - ctx, - ) - .unwrap(); - }); - - view.update(app, |view, ctx| view.move_to_beginning_of_line(&(), ctx)); - assert_eq!( - view.read(app).selection_ranges(app.as_ref()), - &[ - DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0), - DisplayPoint::new(1, 2)..DisplayPoint::new(1, 2), - ] - ); - - view.update(app, |view, ctx| view.move_to_beginning_of_line(&(), ctx)); - assert_eq!( - view.read(app).selection_ranges(app.as_ref()), - &[ - DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0), - DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0), - ] - ); - - view.update(app, |view, ctx| view.move_to_beginning_of_line(&(), ctx)); - assert_eq!( - view.read(app).selection_ranges(app.as_ref()), - &[ - DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0), - DisplayPoint::new(1, 2)..DisplayPoint::new(1, 2), - ] - ); - - view.update(app, |view, ctx| view.move_to_end_of_line(&(), ctx)); - assert_eq!( - view.read(app).selection_ranges(app.as_ref()), - &[ - DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3), - DisplayPoint::new(1, 5)..DisplayPoint::new(1, 5), - ] - ); - - // Moving to the end of line again is a no-op. - view.update(app, |view, ctx| view.move_to_end_of_line(&(), ctx)); - assert_eq!( - view.read(app).selection_ranges(app.as_ref()), - &[ - DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3), - DisplayPoint::new(1, 5)..DisplayPoint::new(1, 5), - ] - ); - - view.update(app, |view, ctx| { - view.move_left(&(), ctx); - view.select_to_beginning_of_line(&true, ctx); - }); - assert_eq!( - view.read(app).selection_ranges(app.as_ref()), - &[ - DisplayPoint::new(0, 2)..DisplayPoint::new(0, 0), - DisplayPoint::new(1, 4)..DisplayPoint::new(1, 2), - ] - ); - - view.update(app, |view, ctx| { - view.select_to_beginning_of_line(&true, ctx) - }); - assert_eq!( - view.read(app).selection_ranges(app.as_ref()), - &[ - DisplayPoint::new(0, 2)..DisplayPoint::new(0, 0), - DisplayPoint::new(1, 4)..DisplayPoint::new(1, 0), - ] - ); - - view.update(app, |view, ctx| { - view.select_to_beginning_of_line(&true, ctx) - }); - assert_eq!( - view.read(app).selection_ranges(app.as_ref()), - &[ - DisplayPoint::new(0, 2)..DisplayPoint::new(0, 0), - DisplayPoint::new(1, 4)..DisplayPoint::new(1, 2), - ] - ); - - view.update(app, |view, ctx| view.select_to_end_of_line(&(), ctx)); - assert_eq!( - view.read(app).selection_ranges(app.as_ref()), - &[ - DisplayPoint::new(0, 2)..DisplayPoint::new(0, 3), - DisplayPoint::new(1, 4)..DisplayPoint::new(1, 5), - ] - ); - - view.update(app, |view, ctx| view.delete_to_end_of_line(&(), ctx)); - assert_eq!(view.read(app).text(app.as_ref()), "ab\n de"); - assert_eq!( - view.read(app).selection_ranges(app.as_ref()), - &[ - DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2), - DisplayPoint::new(1, 4)..DisplayPoint::new(1, 4), - ] - ); - - view.update(app, |view, ctx| view.delete_to_beginning_of_line(&(), ctx)); - assert_eq!(view.read(app).text(app.as_ref()), "\n"); - assert_eq!( - view.read(app).selection_ranges(app.as_ref()), - &[ - DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0), - DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0), - ] - ); - }); - } - - #[test] - fn test_prev_next_word_boundary() { - App::test((), |app| { - let buffer = app - .add_model(|ctx| Buffer::new(0, "use std::str::{foo, bar}\n\n {baz.qux()}", ctx)); - let settings = settings::channel(&app.font_cache()).unwrap().1; - let (_, view) = app.add_window(|ctx| BufferView::for_buffer(buffer, settings, ctx)); - view.update(app, |view, ctx| { - view.select_display_ranges( - &[ - DisplayPoint::new(0, 11)..DisplayPoint::new(0, 11), - DisplayPoint::new(2, 4)..DisplayPoint::new(2, 4), - ], - ctx, - ) - .unwrap(); - }); - - view.update(app, |view, ctx| { - view.move_to_previous_word_boundary(&(), ctx) - }); - assert_eq!( - view.read(app).selection_ranges(app.as_ref()), - &[ - DisplayPoint::new(0, 9)..DisplayPoint::new(0, 9), - DisplayPoint::new(2, 3)..DisplayPoint::new(2, 3), - ] - ); - - view.update(app, |view, ctx| { - view.move_to_previous_word_boundary(&(), ctx) - }); - assert_eq!( - view.read(app).selection_ranges(app.as_ref()), - &[ - DisplayPoint::new(0, 7)..DisplayPoint::new(0, 7), - DisplayPoint::new(2, 2)..DisplayPoint::new(2, 2), - ] - ); - - view.update(app, |view, ctx| { - view.move_to_previous_word_boundary(&(), ctx) - }); - assert_eq!( - view.read(app).selection_ranges(app.as_ref()), - &[ - DisplayPoint::new(0, 4)..DisplayPoint::new(0, 4), - DisplayPoint::new(2, 0)..DisplayPoint::new(2, 0), - ] - ); - - view.update(app, |view, ctx| { - view.move_to_previous_word_boundary(&(), ctx) - }); - assert_eq!( - view.read(app).selection_ranges(app.as_ref()), - &[ - DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3), - DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0), - ] - ); - - view.update(app, |view, ctx| { - view.move_to_previous_word_boundary(&(), ctx) - }); - assert_eq!( - view.read(app).selection_ranges(app.as_ref()), - &[ - DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0), - DisplayPoint::new(0, 24)..DisplayPoint::new(0, 24), - ] - ); - - view.update(app, |view, ctx| { - view.move_to_previous_word_boundary(&(), ctx) - }); - assert_eq!( - view.read(app).selection_ranges(app.as_ref()), - &[ - DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0), - DisplayPoint::new(0, 23)..DisplayPoint::new(0, 23), - ] - ); - - view.update(app, |view, ctx| view.move_to_next_word_boundary(&(), ctx)); - assert_eq!( - view.read(app).selection_ranges(app.as_ref()), - &[ - DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3), - DisplayPoint::new(0, 24)..DisplayPoint::new(0, 24), - ] - ); - - view.update(app, |view, ctx| view.move_to_next_word_boundary(&(), ctx)); - assert_eq!( - view.read(app).selection_ranges(app.as_ref()), - &[ - DisplayPoint::new(0, 4)..DisplayPoint::new(0, 4), - DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0), - ] - ); - - view.update(app, |view, ctx| view.move_to_next_word_boundary(&(), ctx)); - assert_eq!( - view.read(app).selection_ranges(app.as_ref()), - &[ - DisplayPoint::new(0, 7)..DisplayPoint::new(0, 7), - DisplayPoint::new(2, 0)..DisplayPoint::new(2, 0), - ] - ); - - view.update(app, |view, ctx| view.move_to_next_word_boundary(&(), ctx)); - assert_eq!( - view.read(app).selection_ranges(app.as_ref()), - &[ - DisplayPoint::new(0, 9)..DisplayPoint::new(0, 9), - DisplayPoint::new(2, 2)..DisplayPoint::new(2, 2), - ] - ); - - view.update(app, |view, ctx| { - view.move_right(&(), ctx); - view.select_to_previous_word_boundary(&(), ctx); - }); - assert_eq!( - view.read(app).selection_ranges(app.as_ref()), - &[ - DisplayPoint::new(0, 10)..DisplayPoint::new(0, 9), - DisplayPoint::new(2, 3)..DisplayPoint::new(2, 2), - ] - ); - - view.update(app, |view, ctx| { - view.select_to_previous_word_boundary(&(), ctx) - }); - assert_eq!( - view.read(app).selection_ranges(app.as_ref()), - &[ - DisplayPoint::new(0, 10)..DisplayPoint::new(0, 7), - DisplayPoint::new(2, 3)..DisplayPoint::new(2, 0), - ] - ); - - view.update(app, |view, ctx| view.select_to_next_word_boundary(&(), ctx)); - assert_eq!( - view.read(app).selection_ranges(app.as_ref()), - &[ - DisplayPoint::new(0, 10)..DisplayPoint::new(0, 9), - DisplayPoint::new(2, 3)..DisplayPoint::new(2, 2), - ] - ); - - view.update(app, |view, ctx| view.delete_to_next_word_boundary(&(), ctx)); - assert_eq!( - view.read(app).text(app.as_ref()), - "use std::s::{foo, bar}\n\n {az.qux()}" - ); - assert_eq!( - view.read(app).selection_ranges(app.as_ref()), - &[ - DisplayPoint::new(0, 10)..DisplayPoint::new(0, 10), - DisplayPoint::new(2, 3)..DisplayPoint::new(2, 3), - ] - ); - - view.update(app, |view, ctx| { - view.delete_to_previous_word_boundary(&(), ctx) - }); - assert_eq!( - view.read(app).text(app.as_ref()), - "use std::::{foo, bar}\n\n az.qux()}" - ); - assert_eq!( - view.read(app).selection_ranges(app.as_ref()), - &[ - DisplayPoint::new(0, 9)..DisplayPoint::new(0, 9), - DisplayPoint::new(2, 2)..DisplayPoint::new(2, 2), - ] - ); - }); - } - - #[test] - fn test_backspace() { - App::test((), |app| { - let buffer = app.add_model(|ctx| { - Buffer::new( - 0, - "one two three\nfour five six\nseven eight nine\nten\n", - ctx, - ) - }); - let settings = settings::channel(&app.font_cache()).unwrap().1; - let (_, view) = - app.add_window(|ctx| BufferView::for_buffer(buffer.clone(), settings, ctx)); - - view.update(app, |view, ctx| { - view.select_display_ranges( - &[ - // an empty selection - the preceding character is deleted - DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2), - // one character selected - it is deleted - DisplayPoint::new(1, 4)..DisplayPoint::new(1, 3), - // a line suffix selected - it is deleted - DisplayPoint::new(2, 6)..DisplayPoint::new(3, 0), - ], - ctx, - ) - .unwrap(); - view.backspace(&(), ctx); - }); - - assert_eq!( - buffer.read(app).text(), - "oe two three\nfou five six\nseven ten\n" - ); - }) - } - - #[test] - fn test_delete() { - App::test((), |app| { - let buffer = app.add_model(|ctx| { - Buffer::new( - 0, - "one two three\nfour five six\nseven eight nine\nten\n", - ctx, - ) - }); - let settings = settings::channel(&app.font_cache()).unwrap().1; - let (_, view) = - app.add_window(|ctx| BufferView::for_buffer(buffer.clone(), settings, ctx)); - - view.update(app, |view, ctx| { - view.select_display_ranges( - &[ - // an empty selection - the following character is deleted - DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2), - // one character selected - it is deleted - DisplayPoint::new(1, 4)..DisplayPoint::new(1, 3), - // a line suffix selected - it is deleted - DisplayPoint::new(2, 6)..DisplayPoint::new(3, 0), - ], - ctx, - ) - .unwrap(); - view.delete(&(), ctx); - }); - - assert_eq!( - buffer.read(app).text(), - "on two three\nfou five six\nseven ten\n" - ); - }) - } - - #[test] - fn test_delete_line() { - App::test((), |app| { - let settings = settings::channel(&app.font_cache()).unwrap().1; - let buffer = app.add_model(|ctx| Buffer::new(0, "abc\ndef\nghi\n", ctx)); - let (_, view) = app.add_window(|ctx| BufferView::for_buffer(buffer, settings, ctx)); - view.update(app, |view, ctx| { - view.select_display_ranges( - &[ - DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1), - DisplayPoint::new(1, 0)..DisplayPoint::new(1, 1), - DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0), - ], - ctx, - ) - .unwrap(); - view.delete_line(&(), ctx); - }); - assert_eq!(view.read(app).text(app.as_ref()), "ghi"); - assert_eq!( - view.read(app).selection_ranges(app.as_ref()), - vec![ - DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0), - DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1) - ] - ); - - let settings = settings::channel(&app.font_cache()).unwrap().1; - let buffer = app.add_model(|ctx| Buffer::new(0, "abc\ndef\nghi\n", ctx)); - let (_, view) = app.add_window(|ctx| BufferView::for_buffer(buffer, settings, ctx)); - view.update(app, |view, ctx| { - view.select_display_ranges( - &[DisplayPoint::new(2, 0)..DisplayPoint::new(0, 1)], - ctx, - ) - .unwrap(); - view.delete_line(&(), ctx); - }); - assert_eq!(view.read(app).text(app.as_ref()), "ghi\n"); - assert_eq!( - view.read(app).selection_ranges(app.as_ref()), - vec![DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1)] - ); - }); - } - - #[test] - fn test_duplicate_line() { - App::test((), |app| { - let settings = settings::channel(&app.font_cache()).unwrap().1; - let buffer = app.add_model(|ctx| Buffer::new(0, "abc\ndef\nghi\n", ctx)); - let (_, view) = app.add_window(|ctx| BufferView::for_buffer(buffer, settings, ctx)); - view.update(app, |view, ctx| { - view.select_display_ranges( - &[ - DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1), - DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2), - DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0), - DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0), - ], - ctx, - ) - .unwrap(); - view.duplicate_line(&(), ctx); - }); - assert_eq!( - view.read(app).text(app.as_ref()), - "abc\nabc\ndef\ndef\nghi\n\n" - ); - assert_eq!( - view.read(app).selection_ranges(app.as_ref()), - vec![ - DisplayPoint::new(1, 0)..DisplayPoint::new(1, 1), - DisplayPoint::new(1, 2)..DisplayPoint::new(1, 2), - DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0), - DisplayPoint::new(6, 0)..DisplayPoint::new(6, 0), - ] - ); - - let settings = settings::channel(&app.font_cache()).unwrap().1; - let buffer = app.add_model(|ctx| Buffer::new(0, "abc\ndef\nghi\n", ctx)); - let (_, view) = app.add_window(|ctx| BufferView::for_buffer(buffer, settings, ctx)); - view.update(app, |view, ctx| { - view.select_display_ranges( - &[ - DisplayPoint::new(0, 1)..DisplayPoint::new(1, 1), - DisplayPoint::new(1, 2)..DisplayPoint::new(2, 1), - ], - ctx, - ) - .unwrap(); - view.duplicate_line(&(), ctx); - }); - assert_eq!( - view.read(app).text(app.as_ref()), - "abc\ndef\nghi\nabc\ndef\nghi\n" - ); - assert_eq!( - view.read(app).selection_ranges(app.as_ref()), - vec![ - DisplayPoint::new(3, 1)..DisplayPoint::new(4, 1), - DisplayPoint::new(4, 2)..DisplayPoint::new(5, 1), - ] - ); - }); - } - - #[test] - fn test_move_line_up_down() { - App::test((), |app| { - let settings = settings::channel(&app.font_cache()).unwrap().1; - let buffer = app.add_model(|ctx| Buffer::new(0, sample_text(10, 5), ctx)); - let (_, view) = app.add_window(|ctx| BufferView::for_buffer(buffer, settings, ctx)); - view.update(app, |view, ctx| { - view.fold_ranges( + buffer.update(app, |buffer, ctx| { + buffer + .edit( vec![ - Point::new(0, 2)..Point::new(1, 2), - Point::new(2, 3)..Point::new(4, 1), - Point::new(7, 0)..Point::new(8, 4), + Point::new(1, 0)..Point::new(1, 0), + Point::new(1, 1)..Point::new(1, 1), ], - ctx, - ); - view.select_display_ranges( - &[ - DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1), - DisplayPoint::new(3, 1)..DisplayPoint::new(3, 1), - DisplayPoint::new(3, 2)..DisplayPoint::new(4, 2), - DisplayPoint::new(5, 0)..DisplayPoint::new(5, 2), - ], - ctx, + "\t", + Some(ctx), ) .unwrap(); - }); + }); + + view.update(app, |view, ctx| { + view.move_down(&(), ctx); assert_eq!( - view.read(app).text(app.as_ref()), - "aa…bbb\nccc…eeee\nfffff\nggggg\n…i\njjjjj" + view.selection_ranges(ctx.as_ref()), + &[DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)] ); - view.update(app, |view, ctx| view.move_line_up(&(), ctx)); + view.move_right(&(), ctx); assert_eq!( - view.read(app).text(app.as_ref()), - "aa…bbb\nccc…eeee\nggggg\n…i\njjjjj\nfffff" - ); - assert_eq!( - view.read(app).selection_ranges(app.as_ref()), - vec![ - DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1), - DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1), - DisplayPoint::new(2, 2)..DisplayPoint::new(3, 2), - DisplayPoint::new(4, 0)..DisplayPoint::new(4, 2) - ] + view.selection_ranges(ctx.as_ref()), + &[DisplayPoint::new(1, 4)..DisplayPoint::new(1, 4)] ); - view.update(app, |view, ctx| view.move_line_down(&(), ctx)); + view.move_left(&(), ctx); assert_eq!( - view.read(app).text(app.as_ref()), - "ccc…eeee\naa…bbb\nfffff\nggggg\n…i\njjjjj" - ); - assert_eq!( - view.read(app).selection_ranges(app.as_ref()), - vec![ - DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1), - DisplayPoint::new(3, 1)..DisplayPoint::new(3, 1), - DisplayPoint::new(3, 2)..DisplayPoint::new(4, 2), - DisplayPoint::new(5, 0)..DisplayPoint::new(5, 2) - ] + view.selection_ranges(ctx.as_ref()), + &[DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)] ); - view.update(app, |view, ctx| view.move_line_down(&(), ctx)); + view.move_up(&(), ctx); assert_eq!( - view.read(app).text(app.as_ref()), - "ccc…eeee\nfffff\naa…bbb\nggggg\n…i\njjjjj" - ); - assert_eq!( - view.read(app).selection_ranges(app.as_ref()), - vec![ - DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1), - DisplayPoint::new(3, 1)..DisplayPoint::new(3, 1), - DisplayPoint::new(3, 2)..DisplayPoint::new(4, 2), - DisplayPoint::new(5, 0)..DisplayPoint::new(5, 2) - ] + view.selection_ranges(ctx.as_ref()), + &[DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)] ); - view.update(app, |view, ctx| view.move_line_up(&(), ctx)); + view.move_to_end(&(), ctx); assert_eq!( - view.read(app).text(app.as_ref()), - "ccc…eeee\naa…bbb\nggggg\n…i\njjjjj\nfffff" + view.selection_ranges(ctx.as_ref()), + &[DisplayPoint::new(5, 6)..DisplayPoint::new(5, 6)] ); + + view.move_to_beginning(&(), ctx); assert_eq!( - view.read(app).selection_ranges(app.as_ref()), - vec![ - DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1), - DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1), - DisplayPoint::new(2, 2)..DisplayPoint::new(3, 2), - DisplayPoint::new(4, 0)..DisplayPoint::new(4, 2) - ] + view.selection_ranges(ctx.as_ref()), + &[DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)] + ); + + view.select_display_ranges(&[DisplayPoint::new(0, 1)..DisplayPoint::new(0, 2)], ctx) + .unwrap(); + view.select_to_beginning(&(), ctx); + assert_eq!( + view.selection_ranges(ctx.as_ref()), + &[DisplayPoint::new(0, 1)..DisplayPoint::new(0, 0)] + ); + + view.select_to_end(&(), ctx); + assert_eq!( + view.selection_ranges(ctx.as_ref()), + &[DisplayPoint::new(0, 1)..DisplayPoint::new(5, 6)] ); }); } - #[test] - fn test_clipboard() { - App::test((), |app| { - let buffer = app.add_model(|ctx| Buffer::new(0, "one two three four five six ", ctx)); - let settings = settings::channel(&app.font_cache()).unwrap().1; - let view = app - .add_window(|ctx| BufferView::for_buffer(buffer.clone(), settings, ctx)) - .1; - - // Cut with three selections. Clipboard text is divided into three slices. - view.update(app, |view, ctx| { - view.select_ranges(vec![0..4, 8..14, 19..24], false, ctx); - view.cut(&(), ctx); - }); - assert_eq!(view.read(app).text(app.as_ref()), "two four six "); - - // Paste with three cursors. Each cursor pastes one slice of the clipboard text. - view.update(app, |view, ctx| { - view.select_ranges(vec![4..4, 9..9, 13..13], false, ctx); - view.paste(&(), ctx); - }); - assert_eq!( - view.read(app).text(app.as_ref()), - "two one four three six five " - ); - assert_eq!( - view.read(app).selection_ranges(app.as_ref()), + #[gpui::test] + fn test_beginning_end_of_line(app: &mut gpui::MutableAppContext) { + let buffer = app.add_model(|ctx| Buffer::new(0, "abc\n def", ctx)); + let settings = settings::channel(&app.font_cache()).unwrap().1; + let (_, view) = app.add_window(|ctx| BufferView::for_buffer(buffer, settings, ctx)); + view.update(app, |view, ctx| { + view.select_display_ranges( &[ - DisplayPoint::new(0, 8)..DisplayPoint::new(0, 8), - DisplayPoint::new(0, 19)..DisplayPoint::new(0, 19), - DisplayPoint::new(0, 28)..DisplayPoint::new(0, 28) - ] - ); + DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1), + DisplayPoint::new(1, 4)..DisplayPoint::new(1, 4), + ], + ctx, + ) + .unwrap(); + }); - // Paste again but with only two cursors. Since the number of cursors doesn't - // match the number of slices in the clipboard, the entire clipboard text - // is pasted at each cursor. - view.update(app, |view, ctx| { - view.select_ranges(vec![0..0, 28..28], false, ctx); - view.insert(&"( ".to_string(), ctx); - view.paste(&(), ctx); - view.insert(&") ".to_string(), ctx); - }); - assert_eq!( - view.read(app).text(app.as_ref()), - "( one three five ) two one four three six five ( one three five ) " - ); + view.update(app, |view, ctx| view.move_to_beginning_of_line(&(), ctx)); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + &[ + DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0), + DisplayPoint::new(1, 2)..DisplayPoint::new(1, 2), + ] + ); - view.update(app, |view, ctx| { - view.select_ranges(vec![0..0], false, ctx); - view.insert(&"123\n4567\n89\n".to_string(), ctx); - }); - assert_eq!( - view.read(app).text(app.as_ref()), - "123\n4567\n89\n( one three five ) two one four three six five ( one three five ) " - ); + view.update(app, |view, ctx| view.move_to_beginning_of_line(&(), ctx)); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + &[ + DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0), + DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0), + ] + ); - // Cut with three selections, one of which is full-line. - view.update(app, |view, ctx| { - view.select_display_ranges( - &[ - DisplayPoint::new(0, 1)..DisplayPoint::new(0, 2), - DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1), - DisplayPoint::new(2, 0)..DisplayPoint::new(2, 1), - ], - ctx, - ) - .unwrap(); - view.cut(&(), ctx); - }); - assert_eq!( - view.read(app).text(app.as_ref()), - "13\n9\n( one three five ) two one four three six five ( one three five ) " - ); + view.update(app, |view, ctx| view.move_to_beginning_of_line(&(), ctx)); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + &[ + DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0), + DisplayPoint::new(1, 2)..DisplayPoint::new(1, 2), + ] + ); - // Paste with three selections, noticing how the copied selection that was full-line - // gets inserted before the second cursor. - view.update(app, |view, ctx| { - view.select_display_ranges( - &[ - DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1), - DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1), - DisplayPoint::new(2, 2)..DisplayPoint::new(2, 3), - ], - ctx, - ) - .unwrap(); - view.paste(&(), ctx); - }); - assert_eq!( - view.read(app).text(app.as_ref()), - "123\n4567\n9\n( 8ne three five ) two one four three six five ( one three five ) " - ); - assert_eq!( - view.read(app).selection_ranges(app.as_ref()), + view.update(app, |view, ctx| view.move_to_end_of_line(&(), ctx)); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + &[ + DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3), + DisplayPoint::new(1, 5)..DisplayPoint::new(1, 5), + ] + ); + + // Moving to the end of line again is a no-op. + view.update(app, |view, ctx| view.move_to_end_of_line(&(), ctx)); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + &[ + DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3), + DisplayPoint::new(1, 5)..DisplayPoint::new(1, 5), + ] + ); + + view.update(app, |view, ctx| { + view.move_left(&(), ctx); + view.select_to_beginning_of_line(&true, ctx); + }); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + &[ + DisplayPoint::new(0, 2)..DisplayPoint::new(0, 0), + DisplayPoint::new(1, 4)..DisplayPoint::new(1, 2), + ] + ); + + view.update(app, |view, ctx| { + view.select_to_beginning_of_line(&true, ctx) + }); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + &[ + DisplayPoint::new(0, 2)..DisplayPoint::new(0, 0), + DisplayPoint::new(1, 4)..DisplayPoint::new(1, 0), + ] + ); + + view.update(app, |view, ctx| { + view.select_to_beginning_of_line(&true, ctx) + }); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + &[ + DisplayPoint::new(0, 2)..DisplayPoint::new(0, 0), + DisplayPoint::new(1, 4)..DisplayPoint::new(1, 2), + ] + ); + + view.update(app, |view, ctx| view.select_to_end_of_line(&(), ctx)); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + &[ + DisplayPoint::new(0, 2)..DisplayPoint::new(0, 3), + DisplayPoint::new(1, 4)..DisplayPoint::new(1, 5), + ] + ); + + view.update(app, |view, ctx| view.delete_to_end_of_line(&(), ctx)); + assert_eq!(view.read(app).text(app.as_ref()), "ab\n de"); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + &[ + DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2), + DisplayPoint::new(1, 4)..DisplayPoint::new(1, 4), + ] + ); + + view.update(app, |view, ctx| view.delete_to_beginning_of_line(&(), ctx)); + assert_eq!(view.read(app).text(app.as_ref()), "\n"); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + &[ + DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0), + DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0), + ] + ); + } + + #[gpui::test] + fn test_prev_next_word_boundary(app: &mut gpui::MutableAppContext) { + let buffer = + app.add_model(|ctx| Buffer::new(0, "use std::str::{foo, bar}\n\n {baz.qux()}", ctx)); + let settings = settings::channel(&app.font_cache()).unwrap().1; + let (_, view) = app.add_window(|ctx| BufferView::for_buffer(buffer, settings, ctx)); + view.update(app, |view, ctx| { + view.select_display_ranges( &[ + DisplayPoint::new(0, 11)..DisplayPoint::new(0, 11), + DisplayPoint::new(2, 4)..DisplayPoint::new(2, 4), + ], + ctx, + ) + .unwrap(); + }); + + view.update(app, |view, ctx| { + view.move_to_previous_word_boundary(&(), ctx) + }); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + &[ + DisplayPoint::new(0, 9)..DisplayPoint::new(0, 9), + DisplayPoint::new(2, 3)..DisplayPoint::new(2, 3), + ] + ); + + view.update(app, |view, ctx| { + view.move_to_previous_word_boundary(&(), ctx) + }); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + &[ + DisplayPoint::new(0, 7)..DisplayPoint::new(0, 7), + DisplayPoint::new(2, 2)..DisplayPoint::new(2, 2), + ] + ); + + view.update(app, |view, ctx| { + view.move_to_previous_word_boundary(&(), ctx) + }); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + &[ + DisplayPoint::new(0, 4)..DisplayPoint::new(0, 4), + DisplayPoint::new(2, 0)..DisplayPoint::new(2, 0), + ] + ); + + view.update(app, |view, ctx| { + view.move_to_previous_word_boundary(&(), ctx) + }); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + &[ + DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3), + DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0), + ] + ); + + view.update(app, |view, ctx| { + view.move_to_previous_word_boundary(&(), ctx) + }); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + &[ + DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0), + DisplayPoint::new(0, 24)..DisplayPoint::new(0, 24), + ] + ); + + view.update(app, |view, ctx| { + view.move_to_previous_word_boundary(&(), ctx) + }); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + &[ + DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0), + DisplayPoint::new(0, 23)..DisplayPoint::new(0, 23), + ] + ); + + view.update(app, |view, ctx| view.move_to_next_word_boundary(&(), ctx)); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + &[ + DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3), + DisplayPoint::new(0, 24)..DisplayPoint::new(0, 24), + ] + ); + + view.update(app, |view, ctx| view.move_to_next_word_boundary(&(), ctx)); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + &[ + DisplayPoint::new(0, 4)..DisplayPoint::new(0, 4), + DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0), + ] + ); + + view.update(app, |view, ctx| view.move_to_next_word_boundary(&(), ctx)); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + &[ + DisplayPoint::new(0, 7)..DisplayPoint::new(0, 7), + DisplayPoint::new(2, 0)..DisplayPoint::new(2, 0), + ] + ); + + view.update(app, |view, ctx| view.move_to_next_word_boundary(&(), ctx)); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + &[ + DisplayPoint::new(0, 9)..DisplayPoint::new(0, 9), + DisplayPoint::new(2, 2)..DisplayPoint::new(2, 2), + ] + ); + + view.update(app, |view, ctx| { + view.move_right(&(), ctx); + view.select_to_previous_word_boundary(&(), ctx); + }); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + &[ + DisplayPoint::new(0, 10)..DisplayPoint::new(0, 9), + DisplayPoint::new(2, 3)..DisplayPoint::new(2, 2), + ] + ); + + view.update(app, |view, ctx| { + view.select_to_previous_word_boundary(&(), ctx) + }); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + &[ + DisplayPoint::new(0, 10)..DisplayPoint::new(0, 7), + DisplayPoint::new(2, 3)..DisplayPoint::new(2, 0), + ] + ); + + view.update(app, |view, ctx| view.select_to_next_word_boundary(&(), ctx)); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + &[ + DisplayPoint::new(0, 10)..DisplayPoint::new(0, 9), + DisplayPoint::new(2, 3)..DisplayPoint::new(2, 2), + ] + ); + + view.update(app, |view, ctx| view.delete_to_next_word_boundary(&(), ctx)); + assert_eq!( + view.read(app).text(app.as_ref()), + "use std::s::{foo, bar}\n\n {az.qux()}" + ); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + &[ + DisplayPoint::new(0, 10)..DisplayPoint::new(0, 10), + DisplayPoint::new(2, 3)..DisplayPoint::new(2, 3), + ] + ); + + view.update(app, |view, ctx| { + view.delete_to_previous_word_boundary(&(), ctx) + }); + assert_eq!( + view.read(app).text(app.as_ref()), + "use std::::{foo, bar}\n\n az.qux()}" + ); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + &[ + DisplayPoint::new(0, 9)..DisplayPoint::new(0, 9), + DisplayPoint::new(2, 2)..DisplayPoint::new(2, 2), + ] + ); + } + + #[gpui::test] + fn test_backspace(app: &mut gpui::MutableAppContext) { + let buffer = app.add_model(|ctx| { + Buffer::new( + 0, + "one two three\nfour five six\nseven eight nine\nten\n", + ctx, + ) + }); + let settings = settings::channel(&app.font_cache()).unwrap().1; + let (_, view) = app.add_window(|ctx| BufferView::for_buffer(buffer.clone(), settings, ctx)); + + view.update(app, |view, ctx| { + view.select_display_ranges( + &[ + // an empty selection - the preceding character is deleted DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2), - DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1), - DisplayPoint::new(3, 3)..DisplayPoint::new(3, 3), - ] + // one character selected - it is deleted + DisplayPoint::new(1, 4)..DisplayPoint::new(1, 3), + // a line suffix selected - it is deleted + DisplayPoint::new(2, 6)..DisplayPoint::new(3, 0), + ], + ctx, + ) + .unwrap(); + view.backspace(&(), ctx); + }); + + assert_eq!( + buffer.read(app).text(), + "oe two three\nfou five six\nseven ten\n" + ); + } + + #[gpui::test] + fn test_delete(app: &mut gpui::MutableAppContext) { + let buffer = app.add_model(|ctx| { + Buffer::new( + 0, + "one two three\nfour five six\nseven eight nine\nten\n", + ctx, + ) + }); + let settings = settings::channel(&app.font_cache()).unwrap().1; + let (_, view) = app.add_window(|ctx| BufferView::for_buffer(buffer.clone(), settings, ctx)); + + view.update(app, |view, ctx| { + view.select_display_ranges( + &[ + // an empty selection - the following character is deleted + DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2), + // one character selected - it is deleted + DisplayPoint::new(1, 4)..DisplayPoint::new(1, 3), + // a line suffix selected - it is deleted + DisplayPoint::new(2, 6)..DisplayPoint::new(3, 0), + ], + ctx, + ) + .unwrap(); + view.delete(&(), ctx); + }); + + assert_eq!( + buffer.read(app).text(), + "on two three\nfou five six\nseven ten\n" + ); + } + + #[gpui::test] + fn test_delete_line(app: &mut gpui::MutableAppContext) { + let settings = settings::channel(&app.font_cache()).unwrap().1; + let buffer = app.add_model(|ctx| Buffer::new(0, "abc\ndef\nghi\n", ctx)); + let (_, view) = app.add_window(|ctx| BufferView::for_buffer(buffer, settings, ctx)); + view.update(app, |view, ctx| { + view.select_display_ranges( + &[ + DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1), + DisplayPoint::new(1, 0)..DisplayPoint::new(1, 1), + DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0), + ], + ctx, + ) + .unwrap(); + view.delete_line(&(), ctx); + }); + assert_eq!(view.read(app).text(app.as_ref()), "ghi"); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + vec![ + DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0), + DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1) + ] + ); + + let settings = settings::channel(&app.font_cache()).unwrap().1; + let buffer = app.add_model(|ctx| Buffer::new(0, "abc\ndef\nghi\n", ctx)); + let (_, view) = app.add_window(|ctx| BufferView::for_buffer(buffer, settings, ctx)); + view.update(app, |view, ctx| { + view.select_display_ranges(&[DisplayPoint::new(2, 0)..DisplayPoint::new(0, 1)], ctx) + .unwrap(); + view.delete_line(&(), ctx); + }); + assert_eq!(view.read(app).text(app.as_ref()), "ghi\n"); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + vec![DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1)] + ); + } + + #[gpui::test] + fn test_duplicate_line(app: &mut gpui::MutableAppContext) { + let settings = settings::channel(&app.font_cache()).unwrap().1; + let buffer = app.add_model(|ctx| Buffer::new(0, "abc\ndef\nghi\n", ctx)); + let (_, view) = app.add_window(|ctx| BufferView::for_buffer(buffer, settings, ctx)); + view.update(app, |view, ctx| { + view.select_display_ranges( + &[ + DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1), + DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2), + DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0), + DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0), + ], + ctx, + ) + .unwrap(); + view.duplicate_line(&(), ctx); + }); + assert_eq!( + view.read(app).text(app.as_ref()), + "abc\nabc\ndef\ndef\nghi\n\n" + ); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + vec![ + DisplayPoint::new(1, 0)..DisplayPoint::new(1, 1), + DisplayPoint::new(1, 2)..DisplayPoint::new(1, 2), + DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0), + DisplayPoint::new(6, 0)..DisplayPoint::new(6, 0), + ] + ); + + let settings = settings::channel(&app.font_cache()).unwrap().1; + let buffer = app.add_model(|ctx| Buffer::new(0, "abc\ndef\nghi\n", ctx)); + let (_, view) = app.add_window(|ctx| BufferView::for_buffer(buffer, settings, ctx)); + view.update(app, |view, ctx| { + view.select_display_ranges( + &[ + DisplayPoint::new(0, 1)..DisplayPoint::new(1, 1), + DisplayPoint::new(1, 2)..DisplayPoint::new(2, 1), + ], + ctx, + ) + .unwrap(); + view.duplicate_line(&(), ctx); + }); + assert_eq!( + view.read(app).text(app.as_ref()), + "abc\ndef\nghi\nabc\ndef\nghi\n" + ); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + vec![ + DisplayPoint::new(3, 1)..DisplayPoint::new(4, 1), + DisplayPoint::new(4, 2)..DisplayPoint::new(5, 1), + ] + ); + } + + #[gpui::test] + fn test_move_line_up_down(app: &mut gpui::MutableAppContext) { + let settings = settings::channel(&app.font_cache()).unwrap().1; + let buffer = app.add_model(|ctx| Buffer::new(0, sample_text(10, 5), ctx)); + let (_, view) = app.add_window(|ctx| BufferView::for_buffer(buffer, settings, ctx)); + view.update(app, |view, ctx| { + view.fold_ranges( + vec![ + Point::new(0, 2)..Point::new(1, 2), + Point::new(2, 3)..Point::new(4, 1), + Point::new(7, 0)..Point::new(8, 4), + ], + ctx, ); + view.select_display_ranges( + &[ + DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1), + DisplayPoint::new(3, 1)..DisplayPoint::new(3, 1), + DisplayPoint::new(3, 2)..DisplayPoint::new(4, 2), + DisplayPoint::new(5, 0)..DisplayPoint::new(5, 2), + ], + ctx, + ) + .unwrap(); + }); + assert_eq!( + view.read(app).text(app.as_ref()), + "aa…bbb\nccc…eeee\nfffff\nggggg\n…i\njjjjj" + ); - // Copy with a single cursor only, which writes the whole line into the clipboard. - view.update(app, |view, ctx| { - view.select_display_ranges( - &[DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1)], - ctx, - ) - .unwrap(); - view.copy(&(), ctx); - }); + view.update(app, |view, ctx| view.move_line_up(&(), ctx)); + assert_eq!( + view.read(app).text(app.as_ref()), + "aa…bbb\nccc…eeee\nggggg\n…i\njjjjj\nfffff" + ); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + vec![ + DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1), + DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1), + DisplayPoint::new(2, 2)..DisplayPoint::new(3, 2), + DisplayPoint::new(4, 0)..DisplayPoint::new(4, 2) + ] + ); - // Paste with three selections, noticing how the copied full-line selection is inserted - // before the empty selections but replaces the selection that is non-empty. - view.update(app, |view, ctx| { - view.select_display_ranges( - &[ - DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1), - DisplayPoint::new(1, 0)..DisplayPoint::new(1, 2), - DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1), - ], - ctx, - ) + view.update(app, |view, ctx| view.move_line_down(&(), ctx)); + assert_eq!( + view.read(app).text(app.as_ref()), + "ccc…eeee\naa…bbb\nfffff\nggggg\n…i\njjjjj" + ); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + vec![ + DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1), + DisplayPoint::new(3, 1)..DisplayPoint::new(3, 1), + DisplayPoint::new(3, 2)..DisplayPoint::new(4, 2), + DisplayPoint::new(5, 0)..DisplayPoint::new(5, 2) + ] + ); + + view.update(app, |view, ctx| view.move_line_down(&(), ctx)); + assert_eq!( + view.read(app).text(app.as_ref()), + "ccc…eeee\nfffff\naa…bbb\nggggg\n…i\njjjjj" + ); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + vec![ + DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1), + DisplayPoint::new(3, 1)..DisplayPoint::new(3, 1), + DisplayPoint::new(3, 2)..DisplayPoint::new(4, 2), + DisplayPoint::new(5, 0)..DisplayPoint::new(5, 2) + ] + ); + + view.update(app, |view, ctx| view.move_line_up(&(), ctx)); + assert_eq!( + view.read(app).text(app.as_ref()), + "ccc…eeee\naa…bbb\nggggg\n…i\njjjjj\nfffff" + ); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + vec![ + DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1), + DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1), + DisplayPoint::new(2, 2)..DisplayPoint::new(3, 2), + DisplayPoint::new(4, 0)..DisplayPoint::new(4, 2) + ] + ); + } + + #[gpui::test] + fn test_clipboard(app: &mut gpui::MutableAppContext) { + let buffer = app.add_model(|ctx| Buffer::new(0, "one two three four five six ", ctx)); + let settings = settings::channel(&app.font_cache()).unwrap().1; + let view = app + .add_window(|ctx| BufferView::for_buffer(buffer.clone(), settings, ctx)) + .1; + + // Cut with three selections. Clipboard text is divided into three slices. + view.update(app, |view, ctx| { + view.select_ranges(vec![0..4, 8..14, 19..24], false, ctx); + view.cut(&(), ctx); + }); + assert_eq!(view.read(app).text(app.as_ref()), "two four six "); + + // Paste with three cursors. Each cursor pastes one slice of the clipboard text. + view.update(app, |view, ctx| { + view.select_ranges(vec![4..4, 9..9, 13..13], false, ctx); + view.paste(&(), ctx); + }); + assert_eq!( + view.read(app).text(app.as_ref()), + "two one four three six five " + ); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + &[ + DisplayPoint::new(0, 8)..DisplayPoint::new(0, 8), + DisplayPoint::new(0, 19)..DisplayPoint::new(0, 19), + DisplayPoint::new(0, 28)..DisplayPoint::new(0, 28) + ] + ); + + // Paste again but with only two cursors. Since the number of cursors doesn't + // match the number of slices in the clipboard, the entire clipboard text + // is pasted at each cursor. + view.update(app, |view, ctx| { + view.select_ranges(vec![0..0, 28..28], false, ctx); + view.insert(&"( ".to_string(), ctx); + view.paste(&(), ctx); + view.insert(&") ".to_string(), ctx); + }); + assert_eq!( + view.read(app).text(app.as_ref()), + "( one three five ) two one four three six five ( one three five ) " + ); + + view.update(app, |view, ctx| { + view.select_ranges(vec![0..0], false, ctx); + view.insert(&"123\n4567\n89\n".to_string(), ctx); + }); + assert_eq!( + view.read(app).text(app.as_ref()), + "123\n4567\n89\n( one three five ) two one four three six five ( one three five ) " + ); + + // Cut with three selections, one of which is full-line. + view.update(app, |view, ctx| { + view.select_display_ranges( + &[ + DisplayPoint::new(0, 1)..DisplayPoint::new(0, 2), + DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1), + DisplayPoint::new(2, 0)..DisplayPoint::new(2, 1), + ], + ctx, + ) + .unwrap(); + view.cut(&(), ctx); + }); + assert_eq!( + view.read(app).text(app.as_ref()), + "13\n9\n( one three five ) two one four three six five ( one three five ) " + ); + + // Paste with three selections, noticing how the copied selection that was full-line + // gets inserted before the second cursor. + view.update(app, |view, ctx| { + view.select_display_ranges( + &[ + DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1), + DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1), + DisplayPoint::new(2, 2)..DisplayPoint::new(2, 3), + ], + ctx, + ) + .unwrap(); + view.paste(&(), ctx); + }); + assert_eq!( + view.read(app).text(app.as_ref()), + "123\n4567\n9\n( 8ne three five ) two one four three six five ( one three five ) " + ); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + &[ + DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2), + DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1), + DisplayPoint::new(3, 3)..DisplayPoint::new(3, 3), + ] + ); + + // Copy with a single cursor only, which writes the whole line into the clipboard. + view.update(app, |view, ctx| { + view.select_display_ranges(&[DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1)], ctx) .unwrap(); - view.paste(&(), ctx); - }); - assert_eq!( + view.copy(&(), ctx); + }); + + // Paste with three selections, noticing how the copied full-line selection is inserted + // before the empty selections but replaces the selection that is non-empty. + view.update(app, |view, ctx| { + view.select_display_ranges( + &[ + DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1), + DisplayPoint::new(1, 0)..DisplayPoint::new(1, 2), + DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1), + ], + ctx, + ) + .unwrap(); + view.paste(&(), ctx); + }); + assert_eq!( view.read(app).text(app.as_ref()), "123\n123\n123\n67\n123\n9\n( 8ne three five ) two one four three six five ( one three five ) " ); - assert_eq!( - view.read(app).selection_ranges(app.as_ref()), + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + &[ + DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1), + DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0), + DisplayPoint::new(5, 1)..DisplayPoint::new(5, 1), + ] + ); + } + + #[gpui::test] + fn test_select_all(app: &mut gpui::MutableAppContext) { + let buffer = app.add_model(|ctx| Buffer::new(0, "abc\nde\nfgh", ctx)); + let settings = settings::channel(&app.font_cache()).unwrap().1; + let (_, view) = app.add_window(|ctx| BufferView::for_buffer(buffer, settings, ctx)); + view.update(app, |b, ctx| b.select_all(&(), ctx)); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + &[DisplayPoint::new(0, 0)..DisplayPoint::new(2, 3)] + ); + } + + #[gpui::test] + fn test_select_line(app: &mut gpui::MutableAppContext) { + let settings = settings::channel(&app.font_cache()).unwrap().1; + let buffer = app.add_model(|ctx| Buffer::new(0, sample_text(6, 5), ctx)); + let (_, view) = app.add_window(|ctx| BufferView::for_buffer(buffer, settings, ctx)); + view.update(app, |view, ctx| { + view.select_display_ranges( &[ - DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1), - DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0), - DisplayPoint::new(5, 1)..DisplayPoint::new(5, 1), - ] - ); - }); - } - - #[test] - fn test_select_all() { - App::test((), |app| { - let buffer = app.add_model(|ctx| Buffer::new(0, "abc\nde\nfgh", ctx)); - let settings = settings::channel(&app.font_cache()).unwrap().1; - let (_, view) = app.add_window(|ctx| BufferView::for_buffer(buffer, settings, ctx)); - view.update(app, |b, ctx| b.select_all(&(), ctx)); - assert_eq!( - view.read(app).selection_ranges(app.as_ref()), - &[DisplayPoint::new(0, 0)..DisplayPoint::new(2, 3)] - ); - }); - } - - #[test] - fn test_select_line() { - App::test((), |app| { - let settings = settings::channel(&app.font_cache()).unwrap().1; - let buffer = app.add_model(|ctx| Buffer::new(0, sample_text(6, 5), ctx)); - let (_, view) = app.add_window(|ctx| BufferView::for_buffer(buffer, settings, ctx)); - view.update(app, |view, ctx| { - view.select_display_ranges( - &[ - DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1), - DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2), - DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0), - DisplayPoint::new(4, 2)..DisplayPoint::new(4, 2), - ], - ctx, - ) - .unwrap(); - view.select_line(&(), ctx); - }); - assert_eq!( - view.read(app).selection_ranges(app.as_ref()), - vec![ - DisplayPoint::new(0, 0)..DisplayPoint::new(2, 0), - DisplayPoint::new(4, 0)..DisplayPoint::new(5, 0), - ] - ); - - view.update(app, |view, ctx| view.select_line(&(), ctx)); - assert_eq!( - view.read(app).selection_ranges(app.as_ref()), - vec![ - DisplayPoint::new(0, 0)..DisplayPoint::new(3, 0), - DisplayPoint::new(4, 0)..DisplayPoint::new(5, 5), - ] - ); - - view.update(app, |view, ctx| view.select_line(&(), ctx)); - assert_eq!( - view.read(app).selection_ranges(app.as_ref()), - vec![DisplayPoint::new(0, 0)..DisplayPoint::new(5, 5)] - ); - }); - } - - #[test] - fn test_split_selection_into_lines() { - App::test((), |app| { - let settings = settings::channel(&app.font_cache()).unwrap().1; - let buffer = app.add_model(|ctx| Buffer::new(0, sample_text(9, 5), ctx)); - let (_, view) = app.add_window(|ctx| BufferView::for_buffer(buffer, settings, ctx)); - view.update(app, |view, ctx| { - view.fold_ranges( - vec![ - Point::new(0, 2)..Point::new(1, 2), - Point::new(2, 3)..Point::new(4, 1), - Point::new(7, 0)..Point::new(8, 4), - ], - ctx, - ); - view.select_display_ranges( - &[ - DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1), - DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2), - DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0), - DisplayPoint::new(4, 2)..DisplayPoint::new(4, 2), - ], - ctx, - ) - .unwrap(); - }); - assert_eq!( - view.read(app).text(app.as_ref()), - "aa…bbb\nccc…eeee\nfffff\nggggg\n…i" - ); - - view.update(app, |view, ctx| view.split_selection_into_lines(&(), ctx)); - assert_eq!( - view.read(app).text(app.as_ref()), - "aa…bbb\nccc…eeee\nfffff\nggggg\n…i" - ); - assert_eq!( - view.read(app).selection_ranges(app.as_ref()), - [ - DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1), + DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1), DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2), DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0), - DisplayPoint::new(4, 2)..DisplayPoint::new(4, 2) - ] - ); - - view.update(app, |view, ctx| { - view.select_display_ranges( - &[DisplayPoint::new(4, 0)..DisplayPoint::new(0, 1)], - ctx, - ) - .unwrap(); - view.split_selection_into_lines(&(), ctx); - }); - assert_eq!( - view.read(app).text(app.as_ref()), - "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\n…i" - ); - assert_eq!( - view.read(app).selection_ranges(app.as_ref()), - [ - DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1), - DisplayPoint::new(1, 5)..DisplayPoint::new(1, 5), - DisplayPoint::new(2, 5)..DisplayPoint::new(2, 5), - DisplayPoint::new(3, 5)..DisplayPoint::new(3, 5), - DisplayPoint::new(4, 5)..DisplayPoint::new(4, 5), - DisplayPoint::new(5, 5)..DisplayPoint::new(5, 5), - DisplayPoint::new(6, 5)..DisplayPoint::new(6, 5), - DisplayPoint::new(7, 0)..DisplayPoint::new(7, 0) - ] - ); + DisplayPoint::new(4, 2)..DisplayPoint::new(4, 2), + ], + ctx, + ) + .unwrap(); + view.select_line(&(), ctx); }); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + vec![ + DisplayPoint::new(0, 0)..DisplayPoint::new(2, 0), + DisplayPoint::new(4, 0)..DisplayPoint::new(5, 0), + ] + ); + + view.update(app, |view, ctx| view.select_line(&(), ctx)); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + vec![ + DisplayPoint::new(0, 0)..DisplayPoint::new(3, 0), + DisplayPoint::new(4, 0)..DisplayPoint::new(5, 5), + ] + ); + + view.update(app, |view, ctx| view.select_line(&(), ctx)); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + vec![DisplayPoint::new(0, 0)..DisplayPoint::new(5, 5)] + ); } - #[test] - fn test_add_selection_above_below() { - App::test((), |app| { - let settings = settings::channel(&app.font_cache()).unwrap().1; - let buffer = app.add_model(|ctx| Buffer::new(0, "abc\ndefghi\n\njk\nlmno\n", ctx)); - let (_, view) = app.add_window(|ctx| BufferView::for_buffer(buffer, settings, ctx)); - - view.update(app, |view, ctx| { - view.select_display_ranges( - &[DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3)], - ctx, - ) - .unwrap(); - }); - view.update(app, |view, ctx| view.add_selection_above(&(), ctx)); - assert_eq!( - view.read(app).selection_ranges(app.as_ref()), + #[gpui::test] + fn test_split_selection_into_lines(app: &mut gpui::MutableAppContext) { + let settings = settings::channel(&app.font_cache()).unwrap().1; + let buffer = app.add_model(|ctx| Buffer::new(0, sample_text(9, 5), ctx)); + let (_, view) = app.add_window(|ctx| BufferView::for_buffer(buffer, settings, ctx)); + view.update(app, |view, ctx| { + view.fold_ranges( vec![ - DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3), - DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3) - ] - ); - - view.update(app, |view, ctx| view.add_selection_above(&(), ctx)); - assert_eq!( - view.read(app).selection_ranges(app.as_ref()), - vec![ - DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3), - DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3) - ] - ); - - view.update(app, |view, ctx| view.add_selection_below(&(), ctx)); - assert_eq!( - view.read(app).selection_ranges(app.as_ref()), - vec![DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3)] - ); - - view.update(app, |view, ctx| view.add_selection_below(&(), ctx)); - assert_eq!( - view.read(app).selection_ranges(app.as_ref()), - vec![ - DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3), - DisplayPoint::new(4, 3)..DisplayPoint::new(4, 3) - ] - ); - - view.update(app, |view, ctx| view.add_selection_below(&(), ctx)); - assert_eq!( - view.read(app).selection_ranges(app.as_ref()), - vec![ - DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3), - DisplayPoint::new(4, 3)..DisplayPoint::new(4, 3) - ] - ); - - view.update(app, |view, ctx| { - view.select_display_ranges( - &[DisplayPoint::new(1, 4)..DisplayPoint::new(1, 3)], - ctx, - ) - .unwrap(); - }); - view.update(app, |view, ctx| view.add_selection_below(&(), ctx)); - assert_eq!( - view.read(app).selection_ranges(app.as_ref()), - vec![ - DisplayPoint::new(1, 4)..DisplayPoint::new(1, 3), - DisplayPoint::new(4, 4)..DisplayPoint::new(4, 3) - ] - ); - - view.update(app, |view, ctx| view.add_selection_below(&(), ctx)); - assert_eq!( - view.read(app).selection_ranges(app.as_ref()), - vec![ - DisplayPoint::new(1, 4)..DisplayPoint::new(1, 3), - DisplayPoint::new(4, 4)..DisplayPoint::new(4, 3) - ] - ); - - view.update(app, |view, ctx| view.add_selection_above(&(), ctx)); - assert_eq!( - view.read(app).selection_ranges(app.as_ref()), - vec![DisplayPoint::new(1, 4)..DisplayPoint::new(1, 3)] - ); - - view.update(app, |view, ctx| view.add_selection_above(&(), ctx)); - assert_eq!( - view.read(app).selection_ranges(app.as_ref()), - vec![DisplayPoint::new(1, 4)..DisplayPoint::new(1, 3)] - ); - - view.update(app, |view, ctx| { - view.select_display_ranges( - &[DisplayPoint::new(0, 1)..DisplayPoint::new(1, 4)], - ctx, - ) - .unwrap(); - }); - view.update(app, |view, ctx| view.add_selection_below(&(), ctx)); - assert_eq!( - view.read(app).selection_ranges(app.as_ref()), - vec![ - DisplayPoint::new(0, 1)..DisplayPoint::new(0, 3), - DisplayPoint::new(1, 1)..DisplayPoint::new(1, 4), - DisplayPoint::new(3, 1)..DisplayPoint::new(3, 2), - ] - ); - - view.update(app, |view, ctx| view.add_selection_below(&(), ctx)); - assert_eq!( - view.read(app).selection_ranges(app.as_ref()), - vec![ - DisplayPoint::new(0, 1)..DisplayPoint::new(0, 3), - DisplayPoint::new(1, 1)..DisplayPoint::new(1, 4), - DisplayPoint::new(3, 1)..DisplayPoint::new(3, 2), - DisplayPoint::new(4, 1)..DisplayPoint::new(4, 4), - ] - ); - - view.update(app, |view, ctx| view.add_selection_above(&(), ctx)); - assert_eq!( - view.read(app).selection_ranges(app.as_ref()), - vec![ - DisplayPoint::new(0, 1)..DisplayPoint::new(0, 3), - DisplayPoint::new(1, 1)..DisplayPoint::new(1, 4), - DisplayPoint::new(3, 1)..DisplayPoint::new(3, 2), - ] - ); - - view.update(app, |view, ctx| { - view.select_display_ranges( - &[DisplayPoint::new(4, 3)..DisplayPoint::new(1, 1)], - ctx, - ) - .unwrap(); - }); - view.update(app, |view, ctx| view.add_selection_above(&(), ctx)); - assert_eq!( - view.read(app).selection_ranges(app.as_ref()), - vec![ - DisplayPoint::new(0, 3)..DisplayPoint::new(0, 1), - DisplayPoint::new(1, 3)..DisplayPoint::new(1, 1), - DisplayPoint::new(3, 2)..DisplayPoint::new(3, 1), - DisplayPoint::new(4, 3)..DisplayPoint::new(4, 1), - ] - ); - - view.update(app, |view, ctx| view.add_selection_below(&(), ctx)); - assert_eq!( - view.read(app).selection_ranges(app.as_ref()), - vec![ - DisplayPoint::new(1, 3)..DisplayPoint::new(1, 1), - DisplayPoint::new(3, 2)..DisplayPoint::new(3, 1), - DisplayPoint::new(4, 3)..DisplayPoint::new(4, 1), - ] + Point::new(0, 2)..Point::new(1, 2), + Point::new(2, 3)..Point::new(4, 1), + Point::new(7, 0)..Point::new(8, 4), + ], + ctx, ); + view.select_display_ranges( + &[ + DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1), + DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2), + DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0), + DisplayPoint::new(4, 2)..DisplayPoint::new(4, 2), + ], + ctx, + ) + .unwrap(); }); + assert_eq!( + view.read(app).text(app.as_ref()), + "aa…bbb\nccc…eeee\nfffff\nggggg\n…i" + ); + + view.update(app, |view, ctx| view.split_selection_into_lines(&(), ctx)); + assert_eq!( + view.read(app).text(app.as_ref()), + "aa…bbb\nccc…eeee\nfffff\nggggg\n…i" + ); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + [ + DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1), + DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2), + DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0), + DisplayPoint::new(4, 2)..DisplayPoint::new(4, 2) + ] + ); + + view.update(app, |view, ctx| { + view.select_display_ranges(&[DisplayPoint::new(4, 0)..DisplayPoint::new(0, 1)], ctx) + .unwrap(); + view.split_selection_into_lines(&(), ctx); + }); + assert_eq!( + view.read(app).text(app.as_ref()), + "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\n…i" + ); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + [ + DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1), + DisplayPoint::new(1, 5)..DisplayPoint::new(1, 5), + DisplayPoint::new(2, 5)..DisplayPoint::new(2, 5), + DisplayPoint::new(3, 5)..DisplayPoint::new(3, 5), + DisplayPoint::new(4, 5)..DisplayPoint::new(4, 5), + DisplayPoint::new(5, 5)..DisplayPoint::new(5, 5), + DisplayPoint::new(6, 5)..DisplayPoint::new(6, 5), + DisplayPoint::new(7, 0)..DisplayPoint::new(7, 0) + ] + ); + } + + #[gpui::test] + fn test_add_selection_above_below(app: &mut gpui::MutableAppContext) { + let settings = settings::channel(&app.font_cache()).unwrap().1; + let buffer = app.add_model(|ctx| Buffer::new(0, "abc\ndefghi\n\njk\nlmno\n", ctx)); + let (_, view) = app.add_window(|ctx| BufferView::for_buffer(buffer, settings, ctx)); + + view.update(app, |view, ctx| { + view.select_display_ranges(&[DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3)], ctx) + .unwrap(); + }); + view.update(app, |view, ctx| view.add_selection_above(&(), ctx)); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + vec![ + DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3), + DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3) + ] + ); + + view.update(app, |view, ctx| view.add_selection_above(&(), ctx)); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + vec![ + DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3), + DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3) + ] + ); + + view.update(app, |view, ctx| view.add_selection_below(&(), ctx)); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + vec![DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3)] + ); + + view.update(app, |view, ctx| view.add_selection_below(&(), ctx)); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + vec![ + DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3), + DisplayPoint::new(4, 3)..DisplayPoint::new(4, 3) + ] + ); + + view.update(app, |view, ctx| view.add_selection_below(&(), ctx)); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + vec![ + DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3), + DisplayPoint::new(4, 3)..DisplayPoint::new(4, 3) + ] + ); + + view.update(app, |view, ctx| { + view.select_display_ranges(&[DisplayPoint::new(1, 4)..DisplayPoint::new(1, 3)], ctx) + .unwrap(); + }); + view.update(app, |view, ctx| view.add_selection_below(&(), ctx)); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + vec![ + DisplayPoint::new(1, 4)..DisplayPoint::new(1, 3), + DisplayPoint::new(4, 4)..DisplayPoint::new(4, 3) + ] + ); + + view.update(app, |view, ctx| view.add_selection_below(&(), ctx)); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + vec![ + DisplayPoint::new(1, 4)..DisplayPoint::new(1, 3), + DisplayPoint::new(4, 4)..DisplayPoint::new(4, 3) + ] + ); + + view.update(app, |view, ctx| view.add_selection_above(&(), ctx)); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + vec![DisplayPoint::new(1, 4)..DisplayPoint::new(1, 3)] + ); + + view.update(app, |view, ctx| view.add_selection_above(&(), ctx)); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + vec![DisplayPoint::new(1, 4)..DisplayPoint::new(1, 3)] + ); + + view.update(app, |view, ctx| { + view.select_display_ranges(&[DisplayPoint::new(0, 1)..DisplayPoint::new(1, 4)], ctx) + .unwrap(); + }); + view.update(app, |view, ctx| view.add_selection_below(&(), ctx)); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + vec![ + DisplayPoint::new(0, 1)..DisplayPoint::new(0, 3), + DisplayPoint::new(1, 1)..DisplayPoint::new(1, 4), + DisplayPoint::new(3, 1)..DisplayPoint::new(3, 2), + ] + ); + + view.update(app, |view, ctx| view.add_selection_below(&(), ctx)); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + vec![ + DisplayPoint::new(0, 1)..DisplayPoint::new(0, 3), + DisplayPoint::new(1, 1)..DisplayPoint::new(1, 4), + DisplayPoint::new(3, 1)..DisplayPoint::new(3, 2), + DisplayPoint::new(4, 1)..DisplayPoint::new(4, 4), + ] + ); + + view.update(app, |view, ctx| view.add_selection_above(&(), ctx)); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + vec![ + DisplayPoint::new(0, 1)..DisplayPoint::new(0, 3), + DisplayPoint::new(1, 1)..DisplayPoint::new(1, 4), + DisplayPoint::new(3, 1)..DisplayPoint::new(3, 2), + ] + ); + + view.update(app, |view, ctx| { + view.select_display_ranges(&[DisplayPoint::new(4, 3)..DisplayPoint::new(1, 1)], ctx) + .unwrap(); + }); + view.update(app, |view, ctx| view.add_selection_above(&(), ctx)); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + vec![ + DisplayPoint::new(0, 3)..DisplayPoint::new(0, 1), + DisplayPoint::new(1, 3)..DisplayPoint::new(1, 1), + DisplayPoint::new(3, 2)..DisplayPoint::new(3, 1), + DisplayPoint::new(4, 3)..DisplayPoint::new(4, 1), + ] + ); + + view.update(app, |view, ctx| view.add_selection_below(&(), ctx)); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + vec![ + DisplayPoint::new(1, 3)..DisplayPoint::new(1, 1), + DisplayPoint::new(3, 2)..DisplayPoint::new(3, 1), + DisplayPoint::new(4, 3)..DisplayPoint::new(4, 1), + ] + ); } impl BufferView { diff --git a/zed/src/editor/display_map/fold_map.rs b/zed/src/editor/display_map/fold_map.rs index e7f538c686..e33013565f 100644 --- a/zed/src/editor/display_map/fold_map.rs +++ b/zed/src/editor/display_map/fold_map.rs @@ -671,176 +671,163 @@ mod tests { use super::*; use crate::test::sample_text; use buffer::ToPoint; - use gpui::App; - #[test] - fn test_basic_folds() { - App::test((), |app| { - let buffer = app.add_model(|ctx| Buffer::new(0, sample_text(5, 6), ctx)); - let mut map = FoldMap::new(buffer.clone(), app.as_ref()); + #[gpui::test] + fn test_basic_folds(app: &mut gpui::MutableAppContext) { + let buffer = app.add_model(|ctx| Buffer::new(0, sample_text(5, 6), ctx)); + let mut map = FoldMap::new(buffer.clone(), app.as_ref()); - map.fold( - vec![ - Point::new(0, 2)..Point::new(2, 2), - Point::new(2, 4)..Point::new(4, 1), - ], - app.as_ref(), - ) - .unwrap(); - assert_eq!(map.text(app.as_ref()), "aa…cc…eeeee"); + map.fold( + vec![ + Point::new(0, 2)..Point::new(2, 2), + Point::new(2, 4)..Point::new(4, 1), + ], + app.as_ref(), + ) + .unwrap(); + assert_eq!(map.text(app.as_ref()), "aa…cc…eeeee"); - buffer.update(app, |buffer, ctx| { - buffer - .edit( - vec![ - Point::new(0, 0)..Point::new(0, 1), - Point::new(2, 3)..Point::new(2, 3), - ], - "123", - Some(ctx), - ) - .unwrap(); - }); - assert_eq!(map.text(app.as_ref()), "123a…c123c…eeeee"); - - buffer.update(app, |buffer, ctx| { - let start_version = buffer.version.clone(); - buffer - .edit(Some(Point::new(2, 6)..Point::new(4, 3)), "456", Some(ctx)) - .unwrap(); - buffer.edits_since(start_version).collect::>() - }); - assert_eq!(map.text(app.as_ref()), "123a…c123456eee"); - - map.unfold(Some(Point::new(0, 4)..Point::new(0, 5)), app.as_ref()) + buffer.update(app, |buffer, ctx| { + buffer + .edit( + vec![ + Point::new(0, 0)..Point::new(0, 1), + Point::new(2, 3)..Point::new(2, 3), + ], + "123", + Some(ctx), + ) .unwrap(); - assert_eq!(map.text(app.as_ref()), "123aaaaa\nbbbbbb\nccc123456eee"); }); - } + assert_eq!(map.text(app.as_ref()), "123a…c123c…eeeee"); - #[test] - fn test_adjacent_folds() { - App::test((), |app| { - let buffer = app.add_model(|ctx| Buffer::new(0, "abcdefghijkl", ctx)); - - { - let mut map = FoldMap::new(buffer.clone(), app.as_ref()); - - map.fold(vec![5..8], app.as_ref()).unwrap(); - map.check_invariants(app.as_ref()); - assert_eq!(map.text(app.as_ref()), "abcde…ijkl"); - - // Create an fold adjacent to the start of the first fold. - map.fold(vec![0..1, 2..5], app.as_ref()).unwrap(); - map.check_invariants(app.as_ref()); - assert_eq!(map.text(app.as_ref()), "…b…ijkl"); - - // Create an fold adjacent to the end of the first fold. - map.fold(vec![11..11, 8..10], app.as_ref()).unwrap(); - map.check_invariants(app.as_ref()); - assert_eq!(map.text(app.as_ref()), "…b…kl"); - } - - { - let mut map = FoldMap::new(buffer.clone(), app.as_ref()); - - // Create two adjacent folds. - map.fold(vec![0..2, 2..5], app.as_ref()).unwrap(); - map.check_invariants(app.as_ref()); - assert_eq!(map.text(app.as_ref()), "…fghijkl"); - - // Edit within one of the folds. - buffer.update(app, |buffer, ctx| { - let version = buffer.version(); - buffer.edit(vec![0..1], "12345", Some(ctx)).unwrap(); - buffer.edits_since(version).collect::>() - }); - map.check_invariants(app.as_ref()); - assert_eq!(map.text(app.as_ref()), "12345…fghijkl"); - } + buffer.update(app, |buffer, ctx| { + let start_version = buffer.version.clone(); + buffer + .edit(Some(Point::new(2, 6)..Point::new(4, 3)), "456", Some(ctx)) + .unwrap(); + buffer.edits_since(start_version).collect::>() }); - } + assert_eq!(map.text(app.as_ref()), "123a…c123456eee"); - #[test] - fn test_overlapping_folds() { - App::test((), |app| { - let buffer = app.add_model(|ctx| Buffer::new(0, sample_text(5, 6), ctx)); - let mut map = FoldMap::new(buffer.clone(), app.as_ref()); - map.fold( - vec![ - Point::new(0, 2)..Point::new(2, 2), - Point::new(0, 4)..Point::new(1, 0), - Point::new(1, 2)..Point::new(3, 2), - Point::new(3, 1)..Point::new(4, 1), - ], - app.as_ref(), - ) + map.unfold(Some(Point::new(0, 4)..Point::new(0, 5)), app.as_ref()) .unwrap(); - assert_eq!(map.text(app.as_ref()), "aa…eeeee"); - }) + assert_eq!(map.text(app.as_ref()), "123aaaaa\nbbbbbb\nccc123456eee"); } - #[test] - fn test_merging_folds_via_edit() { - App::test((), |app| { - let buffer = app.add_model(|ctx| Buffer::new(0, sample_text(5, 6), ctx)); + #[gpui::test] + fn test_adjacent_folds(app: &mut gpui::MutableAppContext) { + let buffer = app.add_model(|ctx| Buffer::new(0, "abcdefghijkl", ctx)); + + { let mut map = FoldMap::new(buffer.clone(), app.as_ref()); - map.fold( - vec![ - Point::new(0, 2)..Point::new(2, 2), - Point::new(3, 1)..Point::new(4, 1), - ], - app.as_ref(), - ) - .unwrap(); - assert_eq!(map.text(app.as_ref()), "aa…cccc\nd…eeeee"); + map.fold(vec![5..8], app.as_ref()).unwrap(); + map.check_invariants(app.as_ref()); + assert_eq!(map.text(app.as_ref()), "abcde…ijkl"); + // Create an fold adjacent to the start of the first fold. + map.fold(vec![0..1, 2..5], app.as_ref()).unwrap(); + map.check_invariants(app.as_ref()); + assert_eq!(map.text(app.as_ref()), "…b…ijkl"); + + // Create an fold adjacent to the end of the first fold. + map.fold(vec![11..11, 8..10], app.as_ref()).unwrap(); + map.check_invariants(app.as_ref()); + assert_eq!(map.text(app.as_ref()), "…b…kl"); + } + + { + let mut map = FoldMap::new(buffer.clone(), app.as_ref()); + + // Create two adjacent folds. + map.fold(vec![0..2, 2..5], app.as_ref()).unwrap(); + map.check_invariants(app.as_ref()); + assert_eq!(map.text(app.as_ref()), "…fghijkl"); + + // Edit within one of the folds. buffer.update(app, |buffer, ctx| { - buffer - .edit(Some(Point::new(2, 2)..Point::new(3, 1)), "", Some(ctx)) - .unwrap(); + let version = buffer.version(); + buffer.edit(vec![0..1], "12345", Some(ctx)).unwrap(); + buffer.edits_since(version).collect::>() }); - assert_eq!(map.text(app.as_ref()), "aa…eeeee"); - }); + map.check_invariants(app.as_ref()); + assert_eq!(map.text(app.as_ref()), "12345…fghijkl"); + } } - #[test] - fn test_folds_in_range() { - App::test((), |app| { - let buffer = app.add_model(|ctx| Buffer::new(0, sample_text(5, 6), ctx)); - let mut map = FoldMap::new(buffer.clone(), app.as_ref()); - let buffer = buffer.read(app); - - map.fold( - vec![ - Point::new(0, 2)..Point::new(2, 2), - Point::new(0, 4)..Point::new(1, 0), - Point::new(1, 2)..Point::new(3, 2), - Point::new(3, 1)..Point::new(4, 1), - ], - app.as_ref(), - ) - .unwrap(); - let fold_ranges = map - .folds_in_range(Point::new(1, 0)..Point::new(1, 3), app.as_ref()) - .unwrap() - .map(|fold| { - fold.start.to_point(buffer).unwrap()..fold.end.to_point(buffer).unwrap() - }) - .collect::>(); - assert_eq!( - fold_ranges, - vec![ - Point::new(0, 2)..Point::new(2, 2), - Point::new(1, 2)..Point::new(3, 2) - ] - ); - }); + #[gpui::test] + fn test_overlapping_folds(app: &mut gpui::MutableAppContext) { + let buffer = app.add_model(|ctx| Buffer::new(0, sample_text(5, 6), ctx)); + let mut map = FoldMap::new(buffer.clone(), app.as_ref()); + map.fold( + vec![ + Point::new(0, 2)..Point::new(2, 2), + Point::new(0, 4)..Point::new(1, 0), + Point::new(1, 2)..Point::new(3, 2), + Point::new(3, 1)..Point::new(4, 1), + ], + app.as_ref(), + ) + .unwrap(); + assert_eq!(map.text(app.as_ref()), "aa…eeeee"); } - #[test] - fn test_random_folds() { + #[gpui::test] + fn test_merging_folds_via_edit(app: &mut gpui::MutableAppContext) { + let buffer = app.add_model(|ctx| Buffer::new(0, sample_text(5, 6), ctx)); + let mut map = FoldMap::new(buffer.clone(), app.as_ref()); + + map.fold( + vec![ + Point::new(0, 2)..Point::new(2, 2), + Point::new(3, 1)..Point::new(4, 1), + ], + app.as_ref(), + ) + .unwrap(); + assert_eq!(map.text(app.as_ref()), "aa…cccc\nd…eeeee"); + + buffer.update(app, |buffer, ctx| { + buffer + .edit(Some(Point::new(2, 2)..Point::new(3, 1)), "", Some(ctx)) + .unwrap(); + }); + assert_eq!(map.text(app.as_ref()), "aa…eeeee"); + } + + #[gpui::test] + fn test_folds_in_range(app: &mut gpui::MutableAppContext) { + let buffer = app.add_model(|ctx| Buffer::new(0, sample_text(5, 6), ctx)); + let mut map = FoldMap::new(buffer.clone(), app.as_ref()); + let buffer = buffer.read(app); + + map.fold( + vec![ + Point::new(0, 2)..Point::new(2, 2), + Point::new(0, 4)..Point::new(1, 0), + Point::new(1, 2)..Point::new(3, 2), + Point::new(3, 1)..Point::new(4, 1), + ], + app.as_ref(), + ) + .unwrap(); + let fold_ranges = map + .folds_in_range(Point::new(1, 0)..Point::new(1, 3), app.as_ref()) + .unwrap() + .map(|fold| fold.start.to_point(buffer).unwrap()..fold.end.to_point(buffer).unwrap()) + .collect::>(); + assert_eq!( + fold_ranges, + vec![ + Point::new(0, 2)..Point::new(2, 2), + Point::new(1, 2)..Point::new(3, 2) + ] + ); + } + + #[gpui::test] + fn test_random_folds(app: &mut gpui::MutableAppContext) { use crate::editor::ToPoint; use crate::util::RandomCharIter; use rand::prelude::*; @@ -863,203 +850,197 @@ mod tests { dbg!(seed); let mut rng = StdRng::seed_from_u64(seed); - App::test((), |app| { - let buffer = app.add_model(|ctx| { - let len = rng.gen_range(0..10); - let text = RandomCharIter::new(&mut rng).take(len).collect::(); - Buffer::new(0, text, ctx) - }); - let mut map = FoldMap::new(buffer.clone(), app.as_ref()); + let buffer = app.add_model(|ctx| { + let len = rng.gen_range(0..10); + let text = RandomCharIter::new(&mut rng).take(len).collect::(); + Buffer::new(0, text, ctx) + }); + let mut map = FoldMap::new(buffer.clone(), app.as_ref()); - for _ in 0..operations { - log::info!("text: {:?}", buffer.read(app).text()); - match rng.gen_range(0..=100) { - 0..=34 => { - let buffer = buffer.read(app); - let mut to_fold = Vec::new(); - for _ in 0..rng.gen_range(1..=5) { - let end = rng.gen_range(0..=buffer.len()); - let start = rng.gen_range(0..=end); - to_fold.push(start..end); - } - log::info!("folding {:?}", to_fold); - map.fold(to_fold, app.as_ref()).unwrap(); + for _ in 0..operations { + log::info!("text: {:?}", buffer.read(app).text()); + match rng.gen_range(0..=100) { + 0..=34 => { + let buffer = buffer.read(app); + let mut to_fold = Vec::new(); + for _ in 0..rng.gen_range(1..=5) { + let end = rng.gen_range(0..=buffer.len()); + let start = rng.gen_range(0..=end); + to_fold.push(start..end); } - 35..=59 if !map.folds.is_empty() => { - let buffer = buffer.read(app); - let mut to_unfold = Vec::new(); - for _ in 0..rng.gen_range(1..=3) { - let end = rng.gen_range(0..=buffer.len()); - let start = rng.gen_range(0..=end); - to_unfold.push(start..end); - } - log::info!("unfolding {:?}", to_unfold); - map.unfold(to_unfold, app.as_ref()).unwrap(); - } - _ => { - let edits = buffer.update(app, |buffer, ctx| { - let start_version = buffer.version.clone(); - let edit_count = rng.gen_range(1..=5); - buffer.randomly_edit(&mut rng, edit_count, Some(ctx)); - buffer.edits_since(start_version).collect::>() - }); - log::info!("editing {:?}", edits); + log::info!("folding {:?}", to_fold); + map.fold(to_fold, app.as_ref()).unwrap(); + } + 35..=59 if !map.folds.is_empty() => { + let buffer = buffer.read(app); + let mut to_unfold = Vec::new(); + for _ in 0..rng.gen_range(1..=3) { + let end = rng.gen_range(0..=buffer.len()); + let start = rng.gen_range(0..=end); + to_unfold.push(start..end); } + log::info!("unfolding {:?}", to_unfold); + map.unfold(to_unfold, app.as_ref()).unwrap(); } - map.check_invariants(app.as_ref()); - - let buffer = map.buffer.read(app); - let mut expected_text = buffer.text(); - let mut expected_buffer_rows = Vec::new(); - let mut next_row = buffer.max_point().row; - for fold_range in map.merged_fold_ranges(app.as_ref()).into_iter().rev() { - let fold_start = buffer.point_for_offset(fold_range.start).unwrap(); - let fold_end = buffer.point_for_offset(fold_range.end).unwrap(); - expected_buffer_rows.extend((fold_end.row + 1..=next_row).rev()); - next_row = fold_start.row; - - expected_text.replace_range(fold_range.start..fold_range.end, "…"); - } - expected_buffer_rows.extend((0..=next_row).rev()); - expected_buffer_rows.reverse(); - - assert_eq!(map.text(app.as_ref()), expected_text); - - for (display_row, line) in expected_text.lines().enumerate() { - let line_len = map.line_len(display_row as u32, app.as_ref()).unwrap(); - assert_eq!(line_len, line.chars().count() as u32); - } - - let mut display_point = DisplayPoint::new(0, 0); - let mut display_offset = DisplayOffset(0); - for c in expected_text.chars() { - let buffer_point = map.to_buffer_point(display_point, app.as_ref()); - let buffer_offset = buffer_point.to_offset(buffer).unwrap(); - assert_eq!( - map.to_display_point(buffer_point, app.as_ref()), - display_point - ); - assert_eq!( - map.to_buffer_offset(display_point, app.as_ref()).unwrap(), - buffer_offset - ); - assert_eq!( - map.to_display_offset(display_point, app.as_ref()).unwrap(), - display_offset - ); - - if c == '\n' { - *display_point.row_mut() += 1; - *display_point.column_mut() = 0; - } else { - *display_point.column_mut() += 1; - } - display_offset.0 += 1; - } - - for _ in 0..5 { - let row = rng.gen_range(0..=map.max_point(app.as_ref()).row()); - let column = rng.gen_range(0..=map.line_len(row, app.as_ref()).unwrap()); - let point = DisplayPoint::new(row, column); - let offset = map.to_display_offset(point, app.as_ref()).unwrap().0; - let len = rng.gen_range(0..=map.len(app.as_ref()) - offset); - assert_eq!( - map.snapshot(app.as_ref()) - .chars_at(point, app.as_ref()) - .unwrap() - .take(len) - .collect::(), - expected_text - .chars() - .skip(offset) - .take(len) - .collect::() - ); - } - - for (idx, buffer_row) in expected_buffer_rows.iter().enumerate() { - let display_row = map - .to_display_point(Point::new(*buffer_row, 0), app.as_ref()) - .row(); - assert_eq!( - map.snapshot(app.as_ref()) - .buffer_rows(display_row) - .unwrap() - .collect::>(), - expected_buffer_rows[idx..], - ); - } - - for fold_range in map.merged_fold_ranges(app.as_ref()) { - let display_point = map.to_display_point( - fold_range.start.to_point(buffer).unwrap(), - app.as_ref(), - ); - assert!(map.is_line_folded(display_point.row(), app.as_ref())); - } - - for _ in 0..5 { - let end = rng.gen_range(0..=buffer.len()); - let start = rng.gen_range(0..=end); - let expected_folds = map - .folds - .items() - .into_iter() - .filter(|fold| { - let start = buffer.anchor_before(start).unwrap(); - let end = buffer.anchor_after(end).unwrap(); - start.cmp(&fold.0.end, buffer).unwrap() == Ordering::Less - && end.cmp(&fold.0.start, buffer).unwrap() == Ordering::Greater - }) - .map(|fold| fold.0) - .collect::>(); - - assert_eq!( - map.folds_in_range(start..end, app.as_ref()) - .unwrap() - .cloned() - .collect::>(), - expected_folds - ); + _ => { + let edits = buffer.update(app, |buffer, ctx| { + let start_version = buffer.version.clone(); + let edit_count = rng.gen_range(1..=5); + buffer.randomly_edit(&mut rng, edit_count, Some(ctx)); + buffer.edits_since(start_version).collect::>() + }); + log::info!("editing {:?}", edits); } } - }); + map.check_invariants(app.as_ref()); + + let buffer = map.buffer.read(app); + let mut expected_text = buffer.text(); + let mut expected_buffer_rows = Vec::new(); + let mut next_row = buffer.max_point().row; + for fold_range in map.merged_fold_ranges(app.as_ref()).into_iter().rev() { + let fold_start = buffer.point_for_offset(fold_range.start).unwrap(); + let fold_end = buffer.point_for_offset(fold_range.end).unwrap(); + expected_buffer_rows.extend((fold_end.row + 1..=next_row).rev()); + next_row = fold_start.row; + + expected_text.replace_range(fold_range.start..fold_range.end, "…"); + } + expected_buffer_rows.extend((0..=next_row).rev()); + expected_buffer_rows.reverse(); + + assert_eq!(map.text(app.as_ref()), expected_text); + + for (display_row, line) in expected_text.lines().enumerate() { + let line_len = map.line_len(display_row as u32, app.as_ref()).unwrap(); + assert_eq!(line_len, line.chars().count() as u32); + } + + let mut display_point = DisplayPoint::new(0, 0); + let mut display_offset = DisplayOffset(0); + for c in expected_text.chars() { + let buffer_point = map.to_buffer_point(display_point, app.as_ref()); + let buffer_offset = buffer_point.to_offset(buffer).unwrap(); + assert_eq!( + map.to_display_point(buffer_point, app.as_ref()), + display_point + ); + assert_eq!( + map.to_buffer_offset(display_point, app.as_ref()).unwrap(), + buffer_offset + ); + assert_eq!( + map.to_display_offset(display_point, app.as_ref()).unwrap(), + display_offset + ); + + if c == '\n' { + *display_point.row_mut() += 1; + *display_point.column_mut() = 0; + } else { + *display_point.column_mut() += 1; + } + display_offset.0 += 1; + } + + for _ in 0..5 { + let row = rng.gen_range(0..=map.max_point(app.as_ref()).row()); + let column = rng.gen_range(0..=map.line_len(row, app.as_ref()).unwrap()); + let point = DisplayPoint::new(row, column); + let offset = map.to_display_offset(point, app.as_ref()).unwrap().0; + let len = rng.gen_range(0..=map.len(app.as_ref()) - offset); + assert_eq!( + map.snapshot(app.as_ref()) + .chars_at(point, app.as_ref()) + .unwrap() + .take(len) + .collect::(), + expected_text + .chars() + .skip(offset) + .take(len) + .collect::() + ); + } + + for (idx, buffer_row) in expected_buffer_rows.iter().enumerate() { + let display_row = map + .to_display_point(Point::new(*buffer_row, 0), app.as_ref()) + .row(); + assert_eq!( + map.snapshot(app.as_ref()) + .buffer_rows(display_row) + .unwrap() + .collect::>(), + expected_buffer_rows[idx..], + ); + } + + for fold_range in map.merged_fold_ranges(app.as_ref()) { + let display_point = map + .to_display_point(fold_range.start.to_point(buffer).unwrap(), app.as_ref()); + assert!(map.is_line_folded(display_point.row(), app.as_ref())); + } + + for _ in 0..5 { + let end = rng.gen_range(0..=buffer.len()); + let start = rng.gen_range(0..=end); + let expected_folds = map + .folds + .items() + .into_iter() + .filter(|fold| { + let start = buffer.anchor_before(start).unwrap(); + let end = buffer.anchor_after(end).unwrap(); + start.cmp(&fold.0.end, buffer).unwrap() == Ordering::Less + && end.cmp(&fold.0.start, buffer).unwrap() == Ordering::Greater + }) + .map(|fold| fold.0) + .collect::>(); + + assert_eq!( + map.folds_in_range(start..end, app.as_ref()) + .unwrap() + .cloned() + .collect::>(), + expected_folds + ); + } + } } } - #[test] - fn test_buffer_rows() { - App::test((), |app| { - let text = sample_text(6, 6) + "\n"; - let buffer = app.add_model(|ctx| Buffer::new(0, text, ctx)); + #[gpui::test] + fn test_buffer_rows(app: &mut gpui::MutableAppContext) { + let text = sample_text(6, 6) + "\n"; + let buffer = app.add_model(|ctx| Buffer::new(0, text, ctx)); - let mut map = FoldMap::new(buffer.clone(), app.as_ref()); + let mut map = FoldMap::new(buffer.clone(), app.as_ref()); - map.fold( - vec![ - Point::new(0, 2)..Point::new(2, 2), - Point::new(3, 1)..Point::new(4, 1), - ], - app.as_ref(), - ) - .unwrap(); + map.fold( + vec![ + Point::new(0, 2)..Point::new(2, 2), + Point::new(3, 1)..Point::new(4, 1), + ], + app.as_ref(), + ) + .unwrap(); - assert_eq!(map.text(app.as_ref()), "aa…cccc\nd…eeeee\nffffff\n"); - assert_eq!( - map.snapshot(app.as_ref()) - .buffer_rows(0) - .unwrap() - .collect::>(), - vec![0, 3, 5, 6] - ); - assert_eq!( - map.snapshot(app.as_ref()) - .buffer_rows(3) - .unwrap() - .collect::>(), - vec![6] - ); - }); + assert_eq!(map.text(app.as_ref()), "aa…cccc\nd…eeeee\nffffff\n"); + assert_eq!( + map.snapshot(app.as_ref()) + .buffer_rows(0) + .unwrap() + .collect::>(), + vec![0, 3, 5, 6] + ); + assert_eq!( + map.snapshot(app.as_ref()) + .buffer_rows(3) + .unwrap() + .collect::>(), + vec![6] + ); } impl FoldMap { diff --git a/zed/src/editor/display_map/mod.rs b/zed/src/editor/display_map/mod.rs index 97c1c0891b..608d39dac8 100644 --- a/zed/src/editor/display_map/mod.rs +++ b/zed/src/editor/display_map/mod.rs @@ -339,53 +339,50 @@ pub fn collapse_tabs( mod tests { use super::*; use crate::test::*; - use gpui::App; - #[test] - fn test_chars_at() { - App::test((), |app| { - let text = sample_text(6, 6); - let buffer = app.add_model(|ctx| Buffer::new(0, text, ctx)); - let map = DisplayMap::new(buffer.clone(), 4, app.as_ref()); - buffer - .update(app, |buffer, ctx| { - buffer.edit( - vec![ - Point::new(1, 0)..Point::new(1, 0), - Point::new(1, 1)..Point::new(1, 1), - Point::new(2, 1)..Point::new(2, 1), - ], - "\t", - Some(ctx), - ) - }) - .unwrap(); + #[gpui::test] + fn test_chars_at(app: &mut gpui::MutableAppContext) { + let text = sample_text(6, 6); + let buffer = app.add_model(|ctx| Buffer::new(0, text, ctx)); + let map = DisplayMap::new(buffer.clone(), 4, app.as_ref()); + buffer + .update(app, |buffer, ctx| { + buffer.edit( + vec![ + Point::new(1, 0)..Point::new(1, 0), + Point::new(1, 1)..Point::new(1, 1), + Point::new(2, 1)..Point::new(2, 1), + ], + "\t", + Some(ctx), + ) + }) + .unwrap(); - assert_eq!( - map.snapshot(app.as_ref()) - .chars_at(DisplayPoint::new(1, 0), app.as_ref()) - .unwrap() - .take(10) - .collect::(), - " b bb" - ); - assert_eq!( - map.snapshot(app.as_ref()) - .chars_at(DisplayPoint::new(1, 2), app.as_ref()) - .unwrap() - .take(10) - .collect::(), - " b bbbb" - ); - assert_eq!( - map.snapshot(app.as_ref()) - .chars_at(DisplayPoint::new(1, 6), app.as_ref()) - .unwrap() - .take(13) - .collect::(), - " bbbbb\nc c" - ); - }); + assert_eq!( + map.snapshot(app.as_ref()) + .chars_at(DisplayPoint::new(1, 0), app.as_ref()) + .unwrap() + .take(10) + .collect::(), + " b bb" + ); + assert_eq!( + map.snapshot(app.as_ref()) + .chars_at(DisplayPoint::new(1, 2), app.as_ref()) + .unwrap() + .take(10) + .collect::(), + " b bbbb" + ); + assert_eq!( + map.snapshot(app.as_ref()) + .chars_at(DisplayPoint::new(1, 6), app.as_ref()) + .unwrap() + .take(13) + .collect::(), + " bbbbb\nc c" + ); } #[test] @@ -411,12 +408,10 @@ mod tests { assert_eq!(collapse_tabs("\ta".chars(), 5, Bias::Right, 4), (2, 0)); } - #[test] - fn test_max_point() { - App::test((), |app| { - let buffer = app.add_model(|ctx| Buffer::new(0, "aaa\n\t\tbbb", ctx)); - let map = DisplayMap::new(buffer.clone(), 4, app.as_ref()); - assert_eq!(map.max_point(app.as_ref()), DisplayPoint::new(1, 11)) - }); + #[gpui::test] + fn test_max_point(app: &mut gpui::MutableAppContext) { + let buffer = app.add_model(|ctx| Buffer::new(0, "aaa\n\t\tbbb", ctx)); + let map = DisplayMap::new(buffer.clone(), 4, app.as_ref()); + assert_eq!(map.max_point(app.as_ref()), DisplayPoint::new(1, 11)) } } diff --git a/zed/src/file_finder.rs b/zed/src/file_finder.rs index 0f966cc578..691bf4ae3f 100644 --- a/zed/src/file_finder.rs +++ b/zed/src/file_finder.rs @@ -399,7 +399,7 @@ impl FileFinder { self.cancel_flag.store(true, atomic::Ordering::Relaxed); self.cancel_flag = Arc::new(AtomicBool::new(false)); let cancel_flag = self.cancel_flag.clone(); - let task = ctx.background_executor().spawn(async move { + let background_task = ctx.background_executor().spawn(async move { let include_root_name = snapshots.len() > 1; let matches = match_paths( snapshots.iter(), @@ -415,7 +415,11 @@ impl FileFinder { (search_id, did_cancel, query, matches) }); - ctx.spawn(task, Self::update_matches).detach(); + ctx.spawn(|this, mut ctx| async move { + let matches = background_task.await; + this.update(&mut ctx, |this, ctx| this.update_matches(matches, ctx)); + }) + .detach(); Some(()) } @@ -453,220 +457,208 @@ impl FileFinder { mod tests { use super::*; use crate::{editor, settings, test::temp_tree, workspace::Workspace}; - use gpui::App; use serde_json::json; use std::fs; use tempdir::TempDir; - #[test] - fn test_matching_paths() { - App::test_async((), |mut app| async move { - let tmp_dir = TempDir::new("example").unwrap(); - fs::create_dir(tmp_dir.path().join("a")).unwrap(); - fs::write(tmp_dir.path().join("a/banana"), "banana").unwrap(); - fs::write(tmp_dir.path().join("a/bandana"), "bandana").unwrap(); - app.update(|ctx| { - super::init(ctx); - editor::init(ctx); - }); - - let settings = settings::channel(&app.font_cache()).unwrap().1; - let (window_id, workspace) = app.add_window(|ctx| { - let mut workspace = Workspace::new(0, settings, ctx); - workspace.add_worktree(tmp_dir.path(), ctx); - workspace - }); - app.read(|ctx| workspace.read(ctx).worktree_scans_complete(ctx)) - .await; - app.dispatch_action( - window_id, - vec![workspace.id()], - "file_finder:toggle".into(), - (), - ); - - let finder = app.read(|ctx| { - workspace - .read(ctx) - .modal() - .cloned() - .unwrap() - .downcast::() - .unwrap() - }); - let query_buffer = app.read(|ctx| finder.read(ctx).query_buffer.clone()); - - let chain = vec![finder.id(), query_buffer.id()]; - app.dispatch_action(window_id, chain.clone(), "buffer:insert", "b".to_string()); - app.dispatch_action(window_id, chain.clone(), "buffer:insert", "n".to_string()); - app.dispatch_action(window_id, chain.clone(), "buffer:insert", "a".to_string()); - finder - .condition(&app, |finder, _| finder.matches.len() == 2) - .await; - - let active_pane = app.read(|ctx| workspace.read(ctx).active_pane().clone()); - app.dispatch_action( - window_id, - vec![workspace.id(), finder.id()], - "menu:select_next", - (), - ); - app.dispatch_action( - window_id, - vec![workspace.id(), finder.id()], - "file_finder:confirm", - (), - ); - active_pane - .condition(&app, |pane, _| pane.active_item().is_some()) - .await; - app.read(|ctx| { - let active_item = active_pane.read(ctx).active_item().unwrap(); - assert_eq!(active_item.title(ctx), "bandana"); - }); + #[gpui::test] + async fn test_matching_paths(mut app: gpui::TestAppContext) { + let tmp_dir = TempDir::new("example").unwrap(); + fs::create_dir(tmp_dir.path().join("a")).unwrap(); + fs::write(tmp_dir.path().join("a/banana"), "banana").unwrap(); + fs::write(tmp_dir.path().join("a/bandana"), "bandana").unwrap(); + app.update(|ctx| { + super::init(ctx); + editor::init(ctx); }); - } - - #[test] - fn test_matching_cancellation() { - App::test_async((), |mut app| async move { - let tmp_dir = temp_tree(json!({ - "hello": "", - "goodbye": "", - "halogen-light": "", - "happiness": "", - "height": "", - "hi": "", - "hiccup": "", - })); - let settings = settings::channel(&app.font_cache()).unwrap().1; - let (_, workspace) = app.add_window(|ctx| { - let mut workspace = Workspace::new(0, settings.clone(), ctx); - workspace.add_worktree(tmp_dir.path(), ctx); - workspace - }); - app.read(|ctx| workspace.read(ctx).worktree_scans_complete(ctx)) - .await; - let (_, finder) = - app.add_window(|ctx| FileFinder::new(settings, workspace.clone(), ctx)); - - let query = "hi".to_string(); - finder.update(&mut app, |f, ctx| f.spawn_search(query.clone(), ctx)); - finder.condition(&app, |f, _| f.matches.len() == 5).await; - - finder.update(&mut app, |finder, ctx| { - let matches = finder.matches.clone(); - - // Simulate a search being cancelled after the time limit, - // returning only a subset of the matches that would have been found. - finder.spawn_search(query.clone(), ctx); - finder.update_matches( - ( - finder.latest_search_id, - true, // did-cancel - query.clone(), - vec![matches[1].clone(), matches[3].clone()], - ), - ctx, - ); - - // Simulate another cancellation. - finder.spawn_search(query.clone(), ctx); - finder.update_matches( - ( - finder.latest_search_id, - true, // did-cancel - query.clone(), - vec![matches[0].clone(), matches[2].clone(), matches[3].clone()], - ), - ctx, - ); - - assert_eq!(finder.matches, matches[0..4]) - }); - }); - } - - #[test] - fn test_single_file_worktrees() { - App::test_async((), |mut app| async move { - let temp_dir = TempDir::new("test-single-file-worktrees").unwrap(); - let dir_path = temp_dir.path().join("the-parent-dir"); - let file_path = dir_path.join("the-file"); - fs::create_dir(&dir_path).unwrap(); - fs::write(&file_path, "").unwrap(); - - let settings = settings::channel(&app.font_cache()).unwrap().1; - let (_, workspace) = app.add_window(|ctx| { - let mut workspace = Workspace::new(0, settings.clone(), ctx); - workspace.add_worktree(&file_path, ctx); - workspace - }); - app.read(|ctx| workspace.read(ctx).worktree_scans_complete(ctx)) - .await; - let (_, finder) = - app.add_window(|ctx| FileFinder::new(settings, workspace.clone(), ctx)); - - // Even though there is only one worktree, that worktree's filename - // is included in the matching, because the worktree is a single file. - finder.update(&mut app, |f, ctx| f.spawn_search("thf".into(), ctx)); - finder.condition(&app, |f, _| f.matches.len() == 1).await; - - app.read(|ctx| { - let finder = finder.read(ctx); - let (file_name, file_name_positions, full_path, full_path_positions) = - finder.labels_for_match(&finder.matches[0], ctx).unwrap(); - - assert_eq!(file_name, "the-file"); - assert_eq!(file_name_positions, &[0, 1, 4]); - assert_eq!(full_path, "the-file"); - assert_eq!(full_path_positions, &[0, 1, 4]); - }); - - // Since the worktree root is a file, searching for its name followed by a slash does - // not match anything. - finder.update(&mut app, |f, ctx| f.spawn_search("thf/".into(), ctx)); - finder.condition(&app, |f, _| f.matches.len() == 0).await; - }); - } - - #[test] - fn test_multiple_matches_with_same_relative_path() { - App::test_async((), |mut app| async move { - let tmp_dir = temp_tree(json!({ - "dir1": { "a.txt": "" }, - "dir2": { "a.txt": "" } - })); - let settings = settings::channel(&app.font_cache()).unwrap().1; - - let (_, workspace) = app.add_window(|ctx| Workspace::new(0, settings.clone(), ctx)); + let settings = settings::channel(&app.font_cache()).unwrap().1; + let (window_id, workspace) = app.add_window(|ctx| { + let mut workspace = Workspace::new(0, settings, ctx); + workspace.add_worktree(tmp_dir.path(), ctx); workspace - .update(&mut app, |workspace, ctx| { - workspace.open_paths( - &[tmp_dir.path().join("dir1"), tmp_dir.path().join("dir2")], - ctx, - ) - }) - .await; - app.read(|ctx| workspace.read(ctx).worktree_scans_complete(ctx)) - .await; + }); + app.read(|ctx| workspace.read(ctx).worktree_scans_complete(ctx)) + .await; + app.dispatch_action( + window_id, + vec![workspace.id()], + "file_finder:toggle".into(), + (), + ); - let (_, finder) = - app.add_window(|ctx| FileFinder::new(settings, workspace.clone(), ctx)); + let finder = app.read(|ctx| { + workspace + .read(ctx) + .modal() + .cloned() + .unwrap() + .downcast::() + .unwrap() + }); + let query_buffer = app.read(|ctx| finder.read(ctx).query_buffer.clone()); - // Run a search that matches two files with the same relative path. - finder.update(&mut app, |f, ctx| f.spawn_search("a.t".into(), ctx)); - finder.condition(&app, |f, _| f.matches.len() == 2).await; + let chain = vec![finder.id(), query_buffer.id()]; + app.dispatch_action(window_id, chain.clone(), "buffer:insert", "b".to_string()); + app.dispatch_action(window_id, chain.clone(), "buffer:insert", "n".to_string()); + app.dispatch_action(window_id, chain.clone(), "buffer:insert", "a".to_string()); + finder + .condition(&app, |finder, _| finder.matches.len() == 2) + .await; - // Can switch between different matches with the same relative path. - finder.update(&mut app, |f, ctx| { - assert_eq!(f.selected_index(), 0); - f.select_next(&(), ctx); - assert_eq!(f.selected_index(), 1); - f.select_prev(&(), ctx); - assert_eq!(f.selected_index(), 0); - }); + let active_pane = app.read(|ctx| workspace.read(ctx).active_pane().clone()); + app.dispatch_action( + window_id, + vec![workspace.id(), finder.id()], + "menu:select_next", + (), + ); + app.dispatch_action( + window_id, + vec![workspace.id(), finder.id()], + "file_finder:confirm", + (), + ); + active_pane + .condition(&app, |pane, _| pane.active_item().is_some()) + .await; + app.read(|ctx| { + let active_item = active_pane.read(ctx).active_item().unwrap(); + assert_eq!(active_item.title(ctx), "bandana"); + }); + } + + #[gpui::test] + async fn test_matching_cancellation(mut app: gpui::TestAppContext) { + let tmp_dir = temp_tree(json!({ + "hello": "", + "goodbye": "", + "halogen-light": "", + "happiness": "", + "height": "", + "hi": "", + "hiccup": "", + })); + let settings = settings::channel(&app.font_cache()).unwrap().1; + let (_, workspace) = app.add_window(|ctx| { + let mut workspace = Workspace::new(0, settings.clone(), ctx); + workspace.add_worktree(tmp_dir.path(), ctx); + workspace + }); + app.read(|ctx| workspace.read(ctx).worktree_scans_complete(ctx)) + .await; + let (_, finder) = app.add_window(|ctx| FileFinder::new(settings, workspace.clone(), ctx)); + + let query = "hi".to_string(); + finder.update(&mut app, |f, ctx| f.spawn_search(query.clone(), ctx)); + finder.condition(&app, |f, _| f.matches.len() == 5).await; + + finder.update(&mut app, |finder, ctx| { + let matches = finder.matches.clone(); + + // Simulate a search being cancelled after the time limit, + // returning only a subset of the matches that would have been found. + finder.spawn_search(query.clone(), ctx); + finder.update_matches( + ( + finder.latest_search_id, + true, // did-cancel + query.clone(), + vec![matches[1].clone(), matches[3].clone()], + ), + ctx, + ); + + // Simulate another cancellation. + finder.spawn_search(query.clone(), ctx); + finder.update_matches( + ( + finder.latest_search_id, + true, // did-cancel + query.clone(), + vec![matches[0].clone(), matches[2].clone(), matches[3].clone()], + ), + ctx, + ); + + assert_eq!(finder.matches, matches[0..4]) + }); + } + + #[gpui::test] + async fn test_single_file_worktrees(mut app: gpui::TestAppContext) { + let temp_dir = TempDir::new("test-single-file-worktrees").unwrap(); + let dir_path = temp_dir.path().join("the-parent-dir"); + let file_path = dir_path.join("the-file"); + fs::create_dir(&dir_path).unwrap(); + fs::write(&file_path, "").unwrap(); + + let settings = settings::channel(&app.font_cache()).unwrap().1; + let (_, workspace) = app.add_window(|ctx| { + let mut workspace = Workspace::new(0, settings.clone(), ctx); + workspace.add_worktree(&file_path, ctx); + workspace + }); + app.read(|ctx| workspace.read(ctx).worktree_scans_complete(ctx)) + .await; + let (_, finder) = app.add_window(|ctx| FileFinder::new(settings, workspace.clone(), ctx)); + + // Even though there is only one worktree, that worktree's filename + // is included in the matching, because the worktree is a single file. + finder.update(&mut app, |f, ctx| f.spawn_search("thf".into(), ctx)); + finder.condition(&app, |f, _| f.matches.len() == 1).await; + + app.read(|ctx| { + let finder = finder.read(ctx); + let (file_name, file_name_positions, full_path, full_path_positions) = + finder.labels_for_match(&finder.matches[0], ctx).unwrap(); + + assert_eq!(file_name, "the-file"); + assert_eq!(file_name_positions, &[0, 1, 4]); + assert_eq!(full_path, "the-file"); + assert_eq!(full_path_positions, &[0, 1, 4]); + }); + + // Since the worktree root is a file, searching for its name followed by a slash does + // not match anything. + finder.update(&mut app, |f, ctx| f.spawn_search("thf/".into(), ctx)); + finder.condition(&app, |f, _| f.matches.len() == 0).await; + } + + #[gpui::test] + async fn test_multiple_matches_with_same_relative_path(mut app: gpui::TestAppContext) { + let tmp_dir = temp_tree(json!({ + "dir1": { "a.txt": "" }, + "dir2": { "a.txt": "" } + })); + let settings = settings::channel(&app.font_cache()).unwrap().1; + + let (_, workspace) = app.add_window(|ctx| Workspace::new(0, settings.clone(), ctx)); + + workspace + .update(&mut app, |workspace, ctx| { + workspace.open_paths( + &[tmp_dir.path().join("dir1"), tmp_dir.path().join("dir2")], + ctx, + ) + }) + .await; + app.read(|ctx| workspace.read(ctx).worktree_scans_complete(ctx)) + .await; + + let (_, finder) = app.add_window(|ctx| FileFinder::new(settings, workspace.clone(), ctx)); + + // Run a search that matches two files with the same relative path. + finder.update(&mut app, |f, ctx| f.spawn_search("a.t".into(), ctx)); + finder.condition(&app, |f, _| f.matches.len() == 2).await; + + // Can switch between different matches with the same relative path. + finder.update(&mut app, |f, ctx| { + assert_eq!(f.selected_index(), 0); + f.select_next(&(), ctx); + assert_eq!(f.selected_index(), 1); + f.select_prev(&(), ctx); + assert_eq!(f.selected_index(), 0); }); } } diff --git a/zed/src/workspace.rs b/zed/src/workspace.rs index b737b0e99d..e82d7351e4 100644 --- a/zed/src/workspace.rs +++ b/zed/src/workspace.rs @@ -6,11 +6,11 @@ use crate::{ time::ReplicaId, worktree::{FileHandle, Worktree, WorktreeHandle}, }; -use futures_core::{future::LocalBoxFuture, Future}; +use futures_core::Future; use gpui::{ color::rgbu, elements::*, json::to_string_pretty, keymap::Binding, AnyViewHandle, AppContext, - ClipboardItem, Entity, EntityTask, ModelHandle, MutableAppContext, PathPromptOptions, - PromptLevel, View, ViewContext, ViewHandle, WeakModelHandle, + ClipboardItem, Entity, ModelHandle, MutableAppContext, PathPromptOptions, PromptLevel, Task, + View, ViewContext, ViewHandle, WeakModelHandle, }; use log::error; pub use pane::*; @@ -126,7 +126,7 @@ pub trait ItemView: View { &mut self, _: Option, _: &mut ViewContext, - ) -> LocalBoxFuture<'static, anyhow::Result<()>>; + ) -> Task>; fn should_activate_item_on_event(_: &Self::Event) -> bool { false } @@ -165,7 +165,7 @@ pub trait ItemViewHandle: Send + Sync { &self, file: Option, ctx: &mut MutableAppContext, - ) -> LocalBoxFuture<'static, anyhow::Result<()>>; + ) -> Task>; } impl ItemHandle for ModelHandle { @@ -243,7 +243,7 @@ impl ItemViewHandle for ViewHandle { &self, file: Option, ctx: &mut MutableAppContext, - ) -> LocalBoxFuture<'static, anyhow::Result<()>> { + ) -> Task> { self.update(ctx, |item, ctx| item.save(file, ctx)) } @@ -367,16 +367,17 @@ impl Workspace { .cloned() .zip(entries.into_iter()) .map(|(abs_path, file)| { - ctx.spawn( - bg.spawn(async move { abs_path.is_file() }), - move |me, is_file, ctx| { + let is_file = bg.spawn(async move { abs_path.is_file() }); + ctx.spawn(|this, mut ctx| async move { + let is_file = is_file.await; + this.update(&mut ctx, |this, ctx| { if is_file { - me.open_entry(file.entry_id(), ctx) + this.open_entry(file.entry_id(), ctx) } else { None } - }, - ) + }) + }) }) .collect::>(); async move { @@ -450,7 +451,7 @@ impl Workspace { &mut self, entry: (usize, Arc), ctx: &mut ViewContext, - ) -> Option> { + ) -> Option> { // If the active pane contains a view for this file, then activate // that item view. if self @@ -504,44 +505,46 @@ impl Workspace { let history = ctx .background_executor() .spawn(file.load_history(ctx.as_ref())); - ctx.spawn(history, move |_, history, ctx| { - *tx.borrow_mut() = Some(match history { - Ok(history) => Ok(Box::new(ctx.add_model(|ctx| { - Buffer::from_history(replica_id, history, Some(file), ctx) - }))), - Err(error) => Err(Arc::new(error)), + + ctx.as_mut() + .spawn(|mut ctx| async move { + *tx.borrow_mut() = Some(match history.await { + Ok(history) => Ok(Box::new(ctx.add_model(|ctx| { + Buffer::from_history(replica_id, history, Some(file), ctx) + }))), + Err(error) => Err(Arc::new(error)), + }) }) - }) - .detach() + .detach(); } let mut watch = self.loading_items.get(&entry).unwrap().clone(); - Some(ctx.spawn( - async move { - loop { - if let Some(load_result) = watch.borrow().as_ref() { - return load_result.clone(); - } - watch.next().await; + + Some(ctx.spawn(|this, mut ctx| async move { + let load_result = loop { + if let Some(load_result) = watch.borrow().as_ref() { + break load_result.clone(); } - }, - move |me, load_result, ctx| { - me.loading_items.remove(&entry); + watch.next().await; + }; + + this.update(&mut ctx, |this, ctx| { + this.loading_items.remove(&entry); match load_result { Ok(item) => { let weak_item = item.downgrade(); let view = weak_item .add_view(ctx.window_id(), settings, ctx.as_mut()) .unwrap(); - me.items.push(weak_item); - me.add_item_view(view, ctx); + this.items.push(weak_item); + this.add_item_view(view, ctx); } Err(error) => { log::error!("error opening item: {}", error); } } - }, - )) + }) + })) } pub fn active_item(&self, ctx: &ViewContext) -> Option> { @@ -550,8 +553,8 @@ impl Workspace { pub fn save_active_item(&mut self, _: &(), ctx: &mut ViewContext) { if let Some(item) = self.active_item(ctx) { + let handle = ctx.handle(); if item.entry_id(ctx.as_ref()).is_none() { - let handle = ctx.handle(); let start_path = self .worktrees .iter() @@ -560,46 +563,39 @@ impl Workspace { .to_path_buf(); ctx.prompt_for_new_path(&start_path, move |path, ctx| { if let Some(path) = path { - handle.update(ctx, move |this, ctx| { - let file = this.file_for_path(&path, ctx); - let task = item.save(Some(file), ctx.as_mut()); - ctx.spawn(task, move |_, result, _| { - if let Err(e) = result { - error!("failed to save item: {:?}, ", e); - } - }) - .detach() + ctx.spawn(|mut ctx| async move { + let file = + handle.update(&mut ctx, |me, ctx| me.file_for_path(&path, ctx)); + if let Err(error) = ctx.update(|ctx| item.save(Some(file), ctx)).await { + error!("failed to save item: {:?}, ", error); + } }) + .detach() } }); return; } else if item.has_conflict(ctx.as_ref()) { const CONFLICT_MESSAGE: &'static str = "This file has changed on disk since you started editing it. Do you want to overwrite it?"; - let handle = ctx.handle(); ctx.prompt( PromptLevel::Warning, CONFLICT_MESSAGE, &["Overwrite", "Cancel"], move |answer, ctx| { if answer == 0 { - handle.update(ctx, move |_, ctx| { - let task = item.save(None, ctx.as_mut()); - ctx.spawn(task, |_, result, _| { - if let Err(e) = result { - error!("failed to save item: {:?}, ", e); - } - }) - .detach(); - }); + ctx.spawn(|mut ctx| async move { + if let Err(error) = ctx.update(|ctx| item.save(None, ctx)).await { + error!("failed to save item: {:?}, ", error); + } + }) + .detach(); } }, ); } else { - let task = item.save(None, ctx.as_mut()); - ctx.spawn(task, |_, result, _| { - if let Err(e) = result { - error!("failed to save item: {:?}, ", e); + ctx.spawn(|_, mut ctx| async move { + if let Err(error) = ctx.update(|ctx| item.save(None, ctx)).await { + error!("failed to save item: {:?}, ", error); } }) .detach(); @@ -759,451 +755,436 @@ impl WorkspaceHandle for ViewHandle { mod tests { use super::*; use crate::{editor::BufferView, settings, test::temp_tree}; - use gpui::App; use serde_json::json; use std::{collections::HashSet, fs}; use tempdir::TempDir; - #[test] - fn test_open_paths_action() { - App::test((), |app| { - let settings = settings::channel(&app.font_cache()).unwrap().1; + #[gpui::test] + fn test_open_paths_action(app: &mut gpui::MutableAppContext) { + let settings = settings::channel(&app.font_cache()).unwrap().1; - init(app); + init(app); - let dir = temp_tree(json!({ - "a": { - "aa": null, - "ab": null, - }, - "b": { - "ba": null, - "bb": null, - }, - "c": { - "ca": null, - "cb": null, - }, - })); + 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().count(), 1); + 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().count(), 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().count(), 1); - let workspace_view_1 = app - .root_view::(app.window_ids().next().unwrap()) - .unwrap(); - assert_eq!(workspace_view_1.read(app).worktrees().len(), 2); + 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().count(), 1); + let workspace_view_1 = app + .root_view::(app.window_ids().next().unwrap()) + .unwrap(); + assert_eq!(workspace_view_1.read(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().count(), 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().count(), 2); } - #[test] - fn test_open_entry() { - App::test_async((), |mut app| async move { - let dir = temp_tree(json!({ - "a": { - "file1": "contents 1", - "file2": "contents 2", - "file3": "contents 3", - }, - })); + #[gpui::test] + async fn test_open_entry(mut app: gpui::TestAppContext) { + let dir = temp_tree(json!({ + "a": { + "file1": "contents 1", + "file2": "contents 2", + "file3": "contents 3", + }, + })); - let settings = settings::channel(&app.font_cache()).unwrap().1; + let settings = settings::channel(&app.font_cache()).unwrap().1; - let (_, workspace) = app.add_window(|ctx| { - let mut workspace = Workspace::new(0, settings, ctx); - workspace.add_worktree(dir.path(), ctx); - workspace - }); - - app.read(|ctx| workspace.read(ctx).worktree_scans_complete(ctx)) - .await; - let entries = app.read(|ctx| workspace.file_entries(ctx)); - let file1 = entries[0].clone(); - let file2 = entries[1].clone(); - let file3 = entries[2].clone(); - - // Open the first entry + let (_, workspace) = app.add_window(|ctx| { + let mut workspace = Workspace::new(0, settings, ctx); + workspace.add_worktree(dir.path(), ctx); workspace - .update(&mut app, |w, ctx| w.open_entry(file1.clone(), ctx)) - .unwrap() - .await; - app.read(|ctx| { - let pane = workspace.read(ctx).active_pane().read(ctx); - assert_eq!( - pane.active_item().unwrap().entry_id(ctx), - Some(file1.clone()) - ); - assert_eq!(pane.items().len(), 1); - }); - - // Open the second entry - workspace - .update(&mut app, |w, ctx| w.open_entry(file2.clone(), ctx)) - .unwrap() - .await; - app.read(|ctx| { - let pane = workspace.read(ctx).active_pane().read(ctx); - assert_eq!( - pane.active_item().unwrap().entry_id(ctx), - Some(file2.clone()) - ); - assert_eq!(pane.items().len(), 2); - }); - - // Open the first entry again. The existing pane item is activated. - workspace.update(&mut app, |w, ctx| { - assert!(w.open_entry(file1.clone(), ctx).is_none()) - }); - app.read(|ctx| { - let pane = workspace.read(ctx).active_pane().read(ctx); - assert_eq!( - pane.active_item().unwrap().entry_id(ctx), - Some(file1.clone()) - ); - assert_eq!(pane.items().len(), 2); - }); - - // Split the pane with the first entry, then open the second entry again. - workspace.update(&mut app, |w, ctx| { - w.split_pane(w.active_pane().clone(), SplitDirection::Right, ctx); - assert!(w.open_entry(file2.clone(), ctx).is_none()); - assert_eq!( - w.active_pane() - .read(ctx) - .active_item() - .unwrap() - .entry_id(ctx.as_ref()), - Some(file2.clone()) - ); - }); - - // Open the third entry twice concurrently. Two pane items - // are added. - let (t1, t2) = workspace.update(&mut app, |w, ctx| { - ( - w.open_entry(file3.clone(), ctx).unwrap(), - w.open_entry(file3.clone(), ctx).unwrap(), - ) - }); - t1.await; - t2.await; - app.read(|ctx| { - let pane = workspace.read(ctx).active_pane().read(ctx); - assert_eq!( - pane.active_item().unwrap().entry_id(ctx), - Some(file3.clone()) - ); - let pane_entries = pane - .items() - .iter() - .map(|i| i.entry_id(ctx).unwrap()) - .collect::>(); - assert_eq!(pane_entries, &[file1, file2, file3.clone(), file3]); - }); }); - } - #[test] - fn test_open_paths() { - App::test_async((), |mut app| async move { - let dir1 = temp_tree(json!({ - "a.txt": "", - })); - let dir2 = temp_tree(json!({ - "b.txt": "", - })); - - let settings = settings::channel(&app.font_cache()).unwrap().1; - let (_, workspace) = app.add_window(|ctx| { - let mut workspace = Workspace::new(0, settings, ctx); - workspace.add_worktree(dir1.path(), ctx); - workspace - }); - app.read(|ctx| workspace.read(ctx).worktree_scans_complete(ctx)) - .await; - - // Open a file within an existing worktree. - app.update(|ctx| { - workspace.update(ctx, |view, ctx| { - view.open_paths(&[dir1.path().join("a.txt")], ctx) - }) - }) + app.read(|ctx| workspace.read(ctx).worktree_scans_complete(ctx)) .await; - app.read(|ctx| { - assert_eq!( - workspace - .read(ctx) - .active_pane() - .read(ctx) - .active_item() - .unwrap() - .title(ctx), - "a.txt" - ); - }); + let entries = app.read(|ctx| workspace.file_entries(ctx)); + let file1 = entries[0].clone(); + let file2 = entries[1].clone(); + let file3 = entries[2].clone(); - // Open a file outside of any existing worktree. - app.update(|ctx| { - workspace.update(ctx, |view, ctx| { - view.open_paths(&[dir2.path().join("b.txt")], ctx) - }) - }) + // Open the first entry + workspace + .update(&mut app, |w, ctx| w.open_entry(file1.clone(), ctx)) + .unwrap() .await; - app.read(|ctx| { - let worktree_roots = workspace + app.read(|ctx| { + let pane = workspace.read(ctx).active_pane().read(ctx); + assert_eq!( + pane.active_item().unwrap().entry_id(ctx), + Some(file1.clone()) + ); + assert_eq!(pane.items().len(), 1); + }); + + // Open the second entry + workspace + .update(&mut app, |w, ctx| w.open_entry(file2.clone(), ctx)) + .unwrap() + .await; + app.read(|ctx| { + let pane = workspace.read(ctx).active_pane().read(ctx); + assert_eq!( + pane.active_item().unwrap().entry_id(ctx), + Some(file2.clone()) + ); + assert_eq!(pane.items().len(), 2); + }); + + // Open the first entry again. The existing pane item is activated. + workspace.update(&mut app, |w, ctx| { + assert!(w.open_entry(file1.clone(), ctx).is_none()) + }); + app.read(|ctx| { + let pane = workspace.read(ctx).active_pane().read(ctx); + assert_eq!( + pane.active_item().unwrap().entry_id(ctx), + Some(file1.clone()) + ); + assert_eq!(pane.items().len(), 2); + }); + + // Split the pane with the first entry, then open the second entry again. + workspace.update(&mut app, |w, ctx| { + w.split_pane(w.active_pane().clone(), SplitDirection::Right, ctx); + assert!(w.open_entry(file2.clone(), ctx).is_none()); + assert_eq!( + w.active_pane() .read(ctx) - .worktrees() - .iter() - .map(|w| w.read(ctx).abs_path()) - .collect::>(); - assert_eq!( - worktree_roots, - vec![dir1.path(), &dir2.path().join("b.txt")] - .into_iter() - .collect(), - ); - assert_eq!( - workspace - .read(ctx) - .active_pane() - .read(ctx) - .active_item() - .unwrap() - .title(ctx), - "b.txt" - ); - }); + .active_item() + .unwrap() + .entry_id(ctx.as_ref()), + Some(file2.clone()) + ); + }); + + // Open the third entry twice concurrently. Two pane items + // are added. + let (t1, t2) = workspace.update(&mut app, |w, ctx| { + ( + w.open_entry(file3.clone(), ctx).unwrap(), + w.open_entry(file3.clone(), ctx).unwrap(), + ) + }); + t1.await; + t2.await; + app.read(|ctx| { + let pane = workspace.read(ctx).active_pane().read(ctx); + assert_eq!( + pane.active_item().unwrap().entry_id(ctx), + Some(file3.clone()) + ); + let pane_entries = pane + .items() + .iter() + .map(|i| i.entry_id(ctx).unwrap()) + .collect::>(); + assert_eq!(pane_entries, &[file1, file2, file3.clone(), file3]); }); } - #[test] - fn test_open_and_save_new_file() { - App::test_async((), |mut app| async move { - let dir = TempDir::new("test-new-file").unwrap(); - let settings = settings::channel(&app.font_cache()).unwrap().1; - let (_, workspace) = app.add_window(|ctx| { - let mut workspace = Workspace::new(0, settings, ctx); - workspace.add_worktree(dir.path(), ctx); - workspace - }); - let tree = app.read(|ctx| { + #[gpui::test] + async fn test_open_paths(mut app: gpui::TestAppContext) { + let dir1 = temp_tree(json!({ + "a.txt": "", + })); + let dir2 = temp_tree(json!({ + "b.txt": "", + })); + + let settings = settings::channel(&app.font_cache()).unwrap().1; + let (_, workspace) = app.add_window(|ctx| { + let mut workspace = Workspace::new(0, settings, ctx); + workspace.add_worktree(dir1.path(), ctx); + workspace + }); + app.read(|ctx| workspace.read(ctx).worktree_scans_complete(ctx)) + .await; + + // Open a file within an existing worktree. + app.update(|ctx| { + workspace.update(ctx, |view, ctx| { + view.open_paths(&[dir1.path().join("a.txt")], ctx) + }) + }) + .await; + app.read(|ctx| { + assert_eq!( workspace .read(ctx) - .worktrees() - .iter() - .next() + .active_pane() + .read(ctx) + .active_item() .unwrap() - .clone() - }); - tree.flush_fs_events(&app).await; + .title(ctx), + "a.txt" + ); + }); - // Create a new untitled buffer - let editor = workspace.update(&mut app, |workspace, ctx| { - workspace.open_new_file(&(), ctx); - workspace - .active_item(ctx) - .unwrap() - .to_any() - .downcast::() - .unwrap() - }); - editor.update(&mut app, |editor, ctx| { - assert!(!editor.is_dirty(ctx.as_ref())); - assert_eq!(editor.title(ctx.as_ref()), "untitled"); - editor.insert(&"hi".to_string(), ctx); - assert!(editor.is_dirty(ctx.as_ref())); - }); - - // Save the buffer. This prompts for a filename. - workspace.update(&mut app, |workspace, ctx| { - workspace.save_active_item(&(), ctx) - }); - app.simulate_new_path_selection(|parent_dir| { - assert_eq!(parent_dir, dir.path()); - Some(parent_dir.join("the-new-name")) - }); - app.read(|ctx| { - assert!(editor.is_dirty(ctx)); - assert_eq!(editor.title(ctx), "untitled"); - }); - - // When the save completes, the buffer's title is updated. - tree.update(&mut app, |tree, ctx| tree.next_scan_complete(ctx)) - .await; - app.read(|ctx| { - assert!(!editor.is_dirty(ctx)); - assert_eq!(editor.title(ctx), "the-new-name"); - }); - - // Edit the file and save it again. This time, there is no filename prompt. - editor.update(&mut app, |editor, ctx| { - editor.insert(&" there".to_string(), ctx); - assert_eq!(editor.is_dirty(ctx.as_ref()), true); - }); - workspace.update(&mut app, |workspace, ctx| { - workspace.save_active_item(&(), ctx) - }); - assert!(!app.did_prompt_for_new_path()); - editor - .condition(&app, |editor, ctx| !editor.is_dirty(ctx)) - .await; - app.read(|ctx| assert_eq!(editor.title(ctx), "the-new-name")); - - // Open the same newly-created file in another pane item. The new editor should reuse - // the same buffer. - workspace.update(&mut app, |workspace, ctx| { - workspace.open_new_file(&(), ctx); - workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, ctx); - assert!(workspace - .open_entry((tree.id(), Path::new("the-new-name").into()), ctx) - .is_none()); - }); - let editor2 = workspace.update(&mut app, |workspace, ctx| { - workspace - .active_item(ctx) - .unwrap() - .to_any() - .downcast::() - .unwrap() - }); - app.read(|ctx| { - assert_eq!(editor2.read(ctx).buffer(), editor.read(ctx).buffer()); + // Open a file outside of any existing worktree. + app.update(|ctx| { + workspace.update(ctx, |view, ctx| { + view.open_paths(&[dir2.path().join("b.txt")], ctx) }) + }) + .await; + app.read(|ctx| { + let worktree_roots = workspace + .read(ctx) + .worktrees() + .iter() + .map(|w| w.read(ctx).abs_path()) + .collect::>(); + assert_eq!( + worktree_roots, + vec![dir1.path(), &dir2.path().join("b.txt")] + .into_iter() + .collect(), + ); + assert_eq!( + workspace + .read(ctx) + .active_pane() + .read(ctx) + .active_item() + .unwrap() + .title(ctx), + "b.txt" + ); }); } - #[test] - fn test_save_conflicting_item() { - App::test_async((), |mut app| async move { - let dir = temp_tree(json!({ - "a.txt": "", - })); - - let settings = settings::channel(&app.font_cache()).unwrap().1; - let (window_id, workspace) = app.add_window(|ctx| { - let mut workspace = Workspace::new(0, settings, ctx); - workspace.add_worktree(dir.path(), ctx); - workspace - }); - let tree = app.read(|ctx| { - let mut trees = workspace.read(ctx).worktrees().iter(); - trees.next().unwrap().clone() - }); - tree.flush_fs_events(&app).await; - - // Open a file within an existing worktree. - app.update(|ctx| { - workspace.update(ctx, |view, ctx| { - view.open_paths(&[dir.path().join("a.txt")], ctx) - }) - }) - .await; - let editor = app.read(|ctx| { - let pane = workspace.read(ctx).active_pane().read(ctx); - let item = pane.active_item().unwrap(); - item.to_any().downcast::().unwrap() - }); - - app.update(|ctx| { - editor.update(ctx, |editor, ctx| editor.insert(&"x".to_string(), ctx)) - }); - fs::write(dir.path().join("a.txt"), "changed").unwrap(); - tree.flush_fs_events(&app).await; - app.read(|ctx| { - assert!(editor.is_dirty(ctx)); - assert!(editor.has_conflict(ctx)); - }); - - app.update(|ctx| workspace.update(ctx, |w, ctx| w.save_active_item(&(), ctx))); - app.simulate_prompt_answer(window_id, 0); - tree.update(&mut app, |tree, ctx| tree.next_scan_complete(ctx)) - .await; - app.read(|ctx| { - assert!(!editor.is_dirty(ctx)); - assert!(!editor.has_conflict(ctx)); - }); - }); - } - - #[test] - fn test_pane_actions() { - App::test_async((), |mut app| async move { - app.update(|ctx| pane::init(ctx)); - - let dir = temp_tree(json!({ - "a": { - "file1": "contents 1", - "file2": "contents 2", - "file3": "contents 3", - }, - })); - - let settings = settings::channel(&app.font_cache()).unwrap().1; - let (window_id, workspace) = app.add_window(|ctx| { - let mut workspace = Workspace::new(0, settings, ctx); - workspace.add_worktree(dir.path(), ctx); - workspace - }); - app.read(|ctx| workspace.read(ctx).worktree_scans_complete(ctx)) - .await; - let entries = app.read(|ctx| workspace.file_entries(ctx)); - let file1 = entries[0].clone(); - - let pane_1 = app.read(|ctx| workspace.read(ctx).active_pane().clone()); + #[gpui::test] + async fn test_save_conflicting_item(mut app: gpui::TestAppContext) { + let dir = temp_tree(json!({ + "a.txt": "", + })); + let settings = settings::channel(&app.font_cache()).unwrap().1; + let (window_id, workspace) = app.add_window(|ctx| { + let mut workspace = Workspace::new(0, settings, ctx); + workspace.add_worktree(dir.path(), ctx); workspace - .update(&mut app, |w, ctx| w.open_entry(file1.clone(), ctx)) + }); + let tree = app.read(|ctx| { + let mut trees = workspace.read(ctx).worktrees().iter(); + trees.next().unwrap().clone() + }); + tree.flush_fs_events(&app).await; + + // Open a file within an existing worktree. + app.update(|ctx| { + workspace.update(ctx, |view, ctx| { + view.open_paths(&[dir.path().join("a.txt")], ctx) + }) + }) + .await; + let editor = app.read(|ctx| { + let pane = workspace.read(ctx).active_pane().read(ctx); + let item = pane.active_item().unwrap(); + item.to_any().downcast::().unwrap() + }); + + app.update(|ctx| editor.update(ctx, |editor, ctx| editor.insert(&"x".to_string(), ctx))); + fs::write(dir.path().join("a.txt"), "changed").unwrap(); + tree.flush_fs_events(&app).await; + app.read(|ctx| { + assert!(editor.is_dirty(ctx)); + assert!(editor.has_conflict(ctx)); + }); + + app.update(|ctx| workspace.update(ctx, |w, ctx| w.save_active_item(&(), ctx))); + app.simulate_prompt_answer(window_id, 0); + tree.update(&mut app, |tree, ctx| tree.next_scan_complete(ctx)) + .await; + app.read(|ctx| { + assert!(!editor.is_dirty(ctx)); + assert!(!editor.has_conflict(ctx)); + }); + } + + #[gpui::test] + async fn test_open_and_save_new_file(mut app: gpui::TestAppContext) { + let dir = TempDir::new("test-new-file").unwrap(); + let settings = settings::channel(&app.font_cache()).unwrap().1; + let (_, workspace) = app.add_window(|ctx| { + let mut workspace = Workspace::new(0, settings, ctx); + workspace.add_worktree(dir.path(), ctx); + workspace + }); + let tree = app.read(|ctx| { + workspace + .read(ctx) + .worktrees() + .iter() + .next() .unwrap() - .await; - app.read(|ctx| { - assert_eq!( - pane_1.read(ctx).active_item().unwrap().entry_id(ctx), - Some(file1.clone()) - ); - }); + .clone() + }); + tree.flush_fs_events(&app).await; - app.dispatch_action(window_id, vec![pane_1.id()], "pane:split_right", ()); - app.update(|ctx| { - let pane_2 = workspace.read(ctx).active_pane().clone(); - assert_ne!(pane_1, pane_2); + // Create a new untitled buffer + let editor = workspace.update(&mut app, |workspace, ctx| { + workspace.open_new_file(&(), ctx); + workspace + .active_item(ctx) + .unwrap() + .to_any() + .downcast::() + .unwrap() + }); + editor.update(&mut app, |editor, ctx| { + assert!(!editor.is_dirty(ctx.as_ref())); + assert_eq!(editor.title(ctx.as_ref()), "untitled"); + editor.insert(&"hi".to_string(), ctx); + assert!(editor.is_dirty(ctx.as_ref())); + }); - let pane2_item = pane_2.read(ctx).active_item().unwrap(); - assert_eq!(pane2_item.entry_id(ctx.as_ref()), Some(file1.clone())); + // Save the buffer. This prompts for a filename. + workspace.update(&mut app, |workspace, ctx| { + workspace.save_active_item(&(), ctx) + }); + app.simulate_new_path_selection(|parent_dir| { + assert_eq!(parent_dir, dir.path()); + Some(parent_dir.join("the-new-name")) + }); + app.read(|ctx| { + assert!(editor.is_dirty(ctx)); + assert_eq!(editor.title(ctx), "untitled"); + }); - ctx.dispatch_action(window_id, vec![pane_2.id()], "pane:close_active_item", ()); - let workspace_view = workspace.read(ctx); - assert_eq!(workspace_view.panes.len(), 1); - assert_eq!(workspace_view.active_pane(), &pane_1); - }); + // When the save completes, the buffer's title is updated. + tree.update(&mut app, |tree, ctx| tree.next_scan_complete(ctx)) + .await; + app.read(|ctx| { + assert!(!editor.is_dirty(ctx)); + assert_eq!(editor.title(ctx), "the-new-name"); + }); + + // Edit the file and save it again. This time, there is no filename prompt. + editor.update(&mut app, |editor, ctx| { + editor.insert(&" there".to_string(), ctx); + assert_eq!(editor.is_dirty(ctx.as_ref()), true); + }); + workspace.update(&mut app, |workspace, ctx| { + workspace.save_active_item(&(), ctx) + }); + assert!(!app.did_prompt_for_new_path()); + editor + .condition(&app, |editor, ctx| !editor.is_dirty(ctx)) + .await; + app.read(|ctx| assert_eq!(editor.title(ctx), "the-new-name")); + + // Open the same newly-created file in another pane item. The new editor should reuse + // the same buffer. + workspace.update(&mut app, |workspace, ctx| { + workspace.open_new_file(&(), ctx); + workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, ctx); + assert!(workspace + .open_entry((tree.id(), Path::new("the-new-name").into()), ctx) + .is_none()); + }); + let editor2 = workspace.update(&mut app, |workspace, ctx| { + workspace + .active_item(ctx) + .unwrap() + .to_any() + .downcast::() + .unwrap() + }); + app.read(|ctx| { + assert_eq!(editor2.read(ctx).buffer(), editor.read(ctx).buffer()); + }) + } + + #[gpui::test] + async fn test_pane_actions(mut app: gpui::TestAppContext) { + app.update(|ctx| pane::init(ctx)); + + let dir = temp_tree(json!({ + "a": { + "file1": "contents 1", + "file2": "contents 2", + "file3": "contents 3", + }, + })); + + let settings = settings::channel(&app.font_cache()).unwrap().1; + let (window_id, workspace) = app.add_window(|ctx| { + let mut workspace = Workspace::new(0, settings, ctx); + workspace.add_worktree(dir.path(), ctx); + workspace + }); + app.read(|ctx| workspace.read(ctx).worktree_scans_complete(ctx)) + .await; + let entries = app.read(|ctx| workspace.file_entries(ctx)); + let file1 = entries[0].clone(); + + let pane_1 = app.read(|ctx| workspace.read(ctx).active_pane().clone()); + + workspace + .update(&mut app, |w, ctx| w.open_entry(file1.clone(), ctx)) + .unwrap() + .await; + app.read(|ctx| { + assert_eq!( + pane_1.read(ctx).active_item().unwrap().entry_id(ctx), + Some(file1.clone()) + ); + }); + + app.dispatch_action(window_id, vec![pane_1.id()], "pane:split_right", ()); + app.update(|ctx| { + let pane_2 = workspace.read(ctx).active_pane().clone(); + assert_ne!(pane_1, pane_2); + + let pane2_item = pane_2.read(ctx).active_item().unwrap(); + assert_eq!(pane2_item.entry_id(ctx.as_ref()), Some(file1.clone())); + + ctx.dispatch_action(window_id, vec![pane_2.id()], "pane:close_active_item", ()); + let workspace_view = workspace.read(ctx); + assert_eq!(workspace_view.panes.len(), 1); + assert_eq!(workspace_view.active_pane(), &pane_1); }); } } diff --git a/zed/src/worktree.rs b/zed/src/worktree.rs index 62497ba168..62bb4ae008 100644 --- a/zed/src/worktree.rs +++ b/zed/src/worktree.rs @@ -16,7 +16,7 @@ use postage::{ prelude::{Sink, Stream}, watch, }; -use smol::{channel::Sender, Timer}; +use smol::channel::Sender; use std::{ cmp, collections::{HashMap, HashSet}, @@ -99,8 +99,27 @@ impl Worktree { scanner.run(event_stream) }); - ctx.spawn_stream(scan_state_rx, Self::observe_scan_state, |_, _| {}) - .detach(); + ctx.spawn(|this, mut ctx| { + let this = this.downgrade(); + async move { + while let Ok(scan_state) = scan_state_rx.recv().await { + let alive = ctx.update(|ctx| { + if let Some(handle) = this.upgrade(&ctx) { + handle + .update(ctx, |this, ctx| this.observe_scan_state(scan_state, ctx)); + true + } else { + false + } + }); + + if !alive { + break; + } + } + } + }) + .detach(); tree } @@ -117,15 +136,16 @@ impl Worktree { pub fn next_scan_complete(&self, ctx: &mut ModelContext) -> impl Future { let scan_id = self.snapshot.scan_id; - ctx.spawn_stream( - self.scan_state.1.clone(), - move |this, scan_state, ctx| { - if matches!(scan_state, ScanState::Idle) && this.snapshot.scan_id > scan_id { - ctx.halt_stream(); + let mut scan_state = self.scan_state.1.clone(); + ctx.spawn(|this, ctx| async move { + while let Some(scan_state) = scan_state.recv().await { + if this.read_with(&ctx, |this, _| { + matches!(scan_state, ScanState::Idle) && this.snapshot.scan_id > scan_id + }) { + break; } - }, - |_, _| {}, - ) + } + }) } fn observe_scan_state(&mut self, scan_state: ScanState, ctx: &mut ModelContext) { @@ -138,9 +158,11 @@ impl Worktree { ctx.notify(); if self.is_scanning() && !self.poll_scheduled { - ctx.spawn(Timer::after(Duration::from_millis(100)), |this, _, ctx| { - this.poll_scheduled = false; - this.poll_entries(ctx); + ctx.spawn(|this, mut ctx| async move { + this.update(&mut ctx, |this, ctx| { + this.poll_scheduled = false; + this.poll_entries(ctx); + }) }) .detach(); self.poll_scheduled = true; @@ -1394,7 +1416,6 @@ mod tests { use crate::editor::Buffer; use crate::test::*; use anyhow::Result; - use gpui::App; use rand::prelude::*; use serde_json::json; use std::env; @@ -1402,248 +1423,237 @@ mod tests { use std::os::unix; use std::time::{SystemTime, UNIX_EPOCH}; - #[test] - fn test_populate_and_search() { - App::test_async((), |mut app| async move { - let dir = temp_tree(json!({ - "root": { - "apple": "", - "banana": { - "carrot": { - "date": "", - "endive": "", - } - }, - "fennel": { - "grape": "", + #[gpui::test] + async fn test_populate_and_search(mut app: gpui::TestAppContext) { + let dir = temp_tree(json!({ + "root": { + "apple": "", + "banana": { + "carrot": { + "date": "", + "endive": "", } + }, + "fennel": { + "grape": "", } - })); + } + })); - let root_link_path = dir.path().join("root_link"); - unix::fs::symlink(&dir.path().join("root"), &root_link_path).unwrap(); - unix::fs::symlink( - &dir.path().join("root/fennel"), - &dir.path().join("root/finnochio"), + let root_link_path = dir.path().join("root_link"); + unix::fs::symlink(&dir.path().join("root"), &root_link_path).unwrap(); + unix::fs::symlink( + &dir.path().join("root/fennel"), + &dir.path().join("root/finnochio"), + ) + .unwrap(); + + let tree = app.add_model(|ctx| Worktree::new(root_link_path, ctx)); + + app.read(|ctx| tree.read(ctx).scan_complete()).await; + app.read(|ctx| { + let tree = tree.read(ctx); + assert_eq!(tree.file_count(), 5); + + assert_eq!( + tree.inode_for_path("fennel/grape"), + tree.inode_for_path("finnochio/grape") + ); + + let results = match_paths( + Some(tree.snapshot()).iter(), + "bna", + false, + false, + false, + 10, + Default::default(), + ctx.thread_pool().clone(), ) + .into_iter() + .map(|result| result.path) + .collect::>>(); + assert_eq!( + results, + vec![ + PathBuf::from("banana/carrot/date").into(), + PathBuf::from("banana/carrot/endive").into(), + ] + ); + }) + } + + #[gpui::test] + async fn test_save_file(mut app: gpui::TestAppContext) { + let dir = temp_tree(json!({ + "file1": "the old contents", + })); + + let tree = app.add_model(|ctx| Worktree::new(dir.path(), ctx)); + app.read(|ctx| tree.read(ctx).scan_complete()).await; + app.read(|ctx| assert_eq!(tree.read(ctx).file_count(), 1)); + + let buffer = + app.add_model(|ctx| Buffer::new(1, "a line of text.\n".repeat(10 * 1024), ctx)); + + let path = tree.update(&mut app, |tree, ctx| { + let path = tree.files(0).next().unwrap().path().clone(); + assert_eq!(path.file_name().unwrap(), "file1"); + smol::block_on(tree.save(&path, buffer.read(ctx).snapshot(), ctx.as_ref())).unwrap(); + path + }); + + let history = app + .read(|ctx| tree.read(ctx).load_history(&path, ctx)) + .await .unwrap(); - - let tree = app.add_model(|ctx| Worktree::new(root_link_path, ctx)); - - app.read(|ctx| tree.read(ctx).scan_complete()).await; - app.read(|ctx| { - let tree = tree.read(ctx); - assert_eq!(tree.file_count(), 5); - - assert_eq!( - tree.inode_for_path("fennel/grape"), - tree.inode_for_path("finnochio/grape") - ); - - let results = match_paths( - Some(tree.snapshot()).iter(), - "bna", - false, - false, - false, - 10, - Default::default(), - ctx.thread_pool().clone(), - ) - .into_iter() - .map(|result| result.path) - .collect::>>(); - assert_eq!( - results, - vec![ - PathBuf::from("banana/carrot/date").into(), - PathBuf::from("banana/carrot/endive").into(), - ] - ); - }) + app.read(|ctx| { + assert_eq!(history.base_text.as_ref(), buffer.read(ctx).text()); }); } - #[test] - fn test_save_file() { - App::test_async((), |mut app| async move { - let dir = temp_tree(json!({ - "file1": "the old contents", - })); + #[gpui::test] + async fn test_save_in_single_file_worktree(mut app: gpui::TestAppContext) { + let dir = temp_tree(json!({ + "file1": "the old contents", + })); - let tree = app.add_model(|ctx| Worktree::new(dir.path(), ctx)); - app.read(|ctx| tree.read(ctx).scan_complete()).await; - app.read(|ctx| assert_eq!(tree.read(ctx).file_count(), 1)); + let tree = app.add_model(|ctx| Worktree::new(dir.path().join("file1"), ctx)); + app.read(|ctx| tree.read(ctx).scan_complete()).await; + app.read(|ctx| assert_eq!(tree.read(ctx).file_count(), 1)); - let buffer = - app.add_model(|ctx| Buffer::new(1, "a line of text.\n".repeat(10 * 1024), ctx)); + let buffer = + app.add_model(|ctx| Buffer::new(1, "a line of text.\n".repeat(10 * 1024), ctx)); - let path = tree.update(&mut app, |tree, ctx| { - let path = tree.files(0).next().unwrap().path().clone(); - assert_eq!(path.file_name().unwrap(), "file1"); - smol::block_on(tree.save(&path, buffer.read(ctx).snapshot(), ctx.as_ref())) - .unwrap(); - path - }); - - let history = app - .read(|ctx| tree.read(ctx).load_history(&path, ctx)) - .await - .unwrap(); - app.read(|ctx| { - assert_eq!(history.base_text.as_ref(), buffer.read(ctx).text()); - }); + let file = app.read(|ctx| tree.file("", ctx)); + app.update(|ctx| { + assert_eq!(file.path().file_name(), None); + smol::block_on(file.save(buffer.read(ctx).snapshot(), ctx.as_ref())).unwrap(); }); + + let history = app.read(|ctx| file.load_history(ctx)).await.unwrap(); + app.read(|ctx| assert_eq!(history.base_text.as_ref(), buffer.read(ctx).text())); } - #[test] - fn test_save_in_single_file_worktree() { - App::test_async((), |mut app| async move { - let dir = temp_tree(json!({ - "file1": "the old contents", - })); - - let tree = app.add_model(|ctx| Worktree::new(dir.path().join("file1"), ctx)); - app.read(|ctx| tree.read(ctx).scan_complete()).await; - app.read(|ctx| assert_eq!(tree.read(ctx).file_count(), 1)); - - let buffer = - app.add_model(|ctx| Buffer::new(1, "a line of text.\n".repeat(10 * 1024), ctx)); - - let file = app.read(|ctx| tree.file("", ctx)); - app.update(|ctx| { - assert_eq!(file.path().file_name(), None); - smol::block_on(file.save(buffer.read(ctx).snapshot(), ctx.as_ref())).unwrap(); - }); - - let history = app.read(|ctx| file.load_history(ctx)).await.unwrap(); - app.read(|ctx| assert_eq!(history.base_text.as_ref(), buffer.read(ctx).text())); - }); - } - - #[test] - fn test_rescan_simple() { - App::test_async((), |mut app| async move { - let dir = temp_tree(json!({ - "a": { - "file1": "", - "file2": "", - "file3": "", - }, - "b": { - "c": { - "file4": "", - "file5": "", - } + #[gpui::test] + async fn test_rescan_simple(mut app: gpui::TestAppContext) { + let dir = temp_tree(json!({ + "a": { + "file1": "", + "file2": "", + "file3": "", + }, + "b": { + "c": { + "file4": "", + "file5": "", } - })); + } + })); - let tree = app.add_model(|ctx| Worktree::new(dir.path(), ctx)); - let (file2, file3, file4, file5, non_existent_file) = app.read(|ctx| { - ( - tree.file("a/file2", ctx), - tree.file("a/file3", ctx), - tree.file("b/c/file4", ctx), - tree.file("b/c/file5", ctx), - tree.file("a/filex", ctx), - ) - }); + let tree = app.add_model(|ctx| Worktree::new(dir.path(), ctx)); + let (file2, file3, file4, file5, non_existent_file) = app.read(|ctx| { + ( + tree.file("a/file2", ctx), + tree.file("a/file3", ctx), + tree.file("b/c/file4", ctx), + tree.file("b/c/file5", ctx), + tree.file("a/filex", ctx), + ) + }); - // The worktree hasn't scanned the directories containing these paths, - // so it can't determine that the paths are deleted. + // The worktree hasn't scanned the directories containing these paths, + // so it can't determine that the paths are deleted. + assert!(!file2.is_deleted()); + assert!(!file3.is_deleted()); + assert!(!file4.is_deleted()); + assert!(!file5.is_deleted()); + assert!(!non_existent_file.is_deleted()); + + // After scanning, the worktree knows which files exist and which don't. + app.read(|ctx| tree.read(ctx).scan_complete()).await; + assert!(!file2.is_deleted()); + assert!(!file3.is_deleted()); + assert!(!file4.is_deleted()); + assert!(!file5.is_deleted()); + assert!(non_existent_file.is_deleted()); + + tree.flush_fs_events(&app).await; + std::fs::rename(dir.path().join("a/file3"), dir.path().join("b/c/file3")).unwrap(); + std::fs::remove_file(dir.path().join("b/c/file5")).unwrap(); + std::fs::rename(dir.path().join("b/c"), dir.path().join("d")).unwrap(); + std::fs::rename(dir.path().join("a/file2"), dir.path().join("a/file2.new")).unwrap(); + tree.update(&mut app, |tree, ctx| tree.next_scan_complete(ctx)) + .await; + + app.read(|ctx| { + assert_eq!( + tree.read(ctx) + .paths() + .map(|p| p.to_str().unwrap()) + .collect::>(), + vec![ + "a", + "a/file1", + "a/file2.new", + "b", + "d", + "d/file3", + "d/file4" + ] + ); + + assert_eq!(file2.path().to_str().unwrap(), "a/file2.new"); + assert_eq!(file4.path().as_ref(), Path::new("d/file4")); + assert_eq!(file5.path().as_ref(), Path::new("d/file5")); assert!(!file2.is_deleted()); - assert!(!file3.is_deleted()); assert!(!file4.is_deleted()); - assert!(!file5.is_deleted()); - assert!(!non_existent_file.is_deleted()); + assert!(file5.is_deleted()); - // After scanning, the worktree knows which files exist and which don't. - app.read(|ctx| tree.read(ctx).scan_complete()).await; - assert!(!file2.is_deleted()); - assert!(!file3.is_deleted()); - assert!(!file4.is_deleted()); - assert!(!file5.is_deleted()); - assert!(non_existent_file.is_deleted()); - - tree.flush_fs_events(&app).await; - fs::rename(dir.path().join("a/file3"), dir.path().join("b/c/file3")).unwrap(); - fs::remove_file(dir.path().join("b/c/file5")).unwrap(); - fs::rename(dir.path().join("b/c"), dir.path().join("d")).unwrap(); - fs::rename(dir.path().join("a/file2"), dir.path().join("a/file2.new")).unwrap(); - tree.update(&mut app, |tree, ctx| tree.next_scan_complete(ctx)) - .await; - - app.read(|ctx| { - assert_eq!( - tree.read(ctx) - .paths() - .map(|p| p.to_str().unwrap()) - .collect::>(), - vec![ - "a", - "a/file1", - "a/file2.new", - "b", - "d", - "d/file3", - "d/file4" - ] - ); - - assert_eq!(file2.path().to_str().unwrap(), "a/file2.new"); - assert_eq!(file4.path().as_ref(), Path::new("d/file4")); - assert_eq!(file5.path().as_ref(), Path::new("d/file5")); - assert!(!file2.is_deleted()); - assert!(!file4.is_deleted()); - assert!(file5.is_deleted()); - - // Right now, this rename isn't detected because the target path - // no longer exists on the file system by the time we process the - // rename event. - assert_eq!(file3.path().as_ref(), Path::new("a/file3")); - assert!(file3.is_deleted()); - }); + // Right now, this rename isn't detected because the target path + // no longer exists on the file system by the time we process the + // rename event. + assert_eq!(file3.path().as_ref(), Path::new("a/file3")); + assert!(file3.is_deleted()); }); } - #[test] - fn test_rescan_with_gitignore() { - App::test_async((), |mut app| async move { - let dir = temp_tree(json!({ - ".git": {}, - ".gitignore": "ignored-dir\n", - "tracked-dir": { - "tracked-file1": "tracked contents", - }, - "ignored-dir": { - "ignored-file1": "ignored contents", - } - })); + #[gpui::test] + async fn test_rescan_with_gitignore(mut app: gpui::TestAppContext) { + let dir = temp_tree(json!({ + ".git": {}, + ".gitignore": "ignored-dir\n", + "tracked-dir": { + "tracked-file1": "tracked contents", + }, + "ignored-dir": { + "ignored-file1": "ignored contents", + } + })); - let tree = app.add_model(|ctx| Worktree::new(dir.path(), ctx)); - app.read(|ctx| tree.read(ctx).scan_complete()).await; - tree.flush_fs_events(&app).await; - app.read(|ctx| { - let tree = tree.read(ctx); - let tracked = tree.entry_for_path("tracked-dir/tracked-file1").unwrap(); - let ignored = tree.entry_for_path("ignored-dir/ignored-file1").unwrap(); - assert_eq!(tracked.is_ignored(), false); - assert_eq!(ignored.is_ignored(), true); - }); + let tree = app.add_model(|ctx| Worktree::new(dir.path(), ctx)); + app.read(|ctx| tree.read(ctx).scan_complete()).await; + tree.flush_fs_events(&app).await; + app.read(|ctx| { + let tree = tree.read(ctx); + let tracked = tree.entry_for_path("tracked-dir/tracked-file1").unwrap(); + let ignored = tree.entry_for_path("ignored-dir/ignored-file1").unwrap(); + assert_eq!(tracked.is_ignored(), false); + assert_eq!(ignored.is_ignored(), true); + }); - fs::write(dir.path().join("tracked-dir/tracked-file2"), "").unwrap(); - fs::write(dir.path().join("ignored-dir/ignored-file2"), "").unwrap(); - tree.update(&mut app, |tree, ctx| tree.next_scan_complete(ctx)) - .await; - app.read(|ctx| { - let tree = tree.read(ctx); - let dot_git = tree.entry_for_path(".git").unwrap(); - let tracked = tree.entry_for_path("tracked-dir/tracked-file2").unwrap(); - let ignored = tree.entry_for_path("ignored-dir/ignored-file2").unwrap(); - assert_eq!(tracked.is_ignored(), false); - assert_eq!(ignored.is_ignored(), true); - assert_eq!(dot_git.is_ignored(), true); - }); + fs::write(dir.path().join("tracked-dir/tracked-file2"), "").unwrap(); + fs::write(dir.path().join("ignored-dir/ignored-file2"), "").unwrap(); + tree.update(&mut app, |tree, ctx| tree.next_scan_complete(ctx)) + .await; + app.read(|ctx| { + let tree = tree.read(ctx); + let dot_git = tree.entry_for_path(".git").unwrap(); + let tracked = tree.entry_for_path("tracked-dir/tracked-file2").unwrap(); + let ignored = tree.entry_for_path("ignored-dir/ignored-file2").unwrap(); + assert_eq!(tracked.is_ignored(), false); + assert_eq!(ignored.is_ignored(), true); + assert_eq!(dot_git.is_ignored(), true); }); }