Merge branch 'master' into file-changed-on-disk

Max Brunsfeld created

Change summary

Cargo.lock                             |    9 
Cargo.toml                             |    2 
gpui/Cargo.toml                        |    1 
gpui/src/app.rs                        |  807 +++------
gpui/src/lib.rs                        |    1 
gpui_macros/Cargo.toml                 |   11 
gpui_macros/src/lib.rs                 |   57 
zed/src/editor/buffer/mod.rs           | 1259 +++++++-------
zed/src/editor/buffer_view.rs          | 2330 +++++++++++++--------------
zed/src/editor/display_map/fold_map.rs |  639 +++---
zed/src/editor/display_map/mod.rs      |  101 
zed/src/file_finder.rs                 |  390 ++--
zed/src/workspace.rs                   |  911 +++++-----
zed/src/worktree.rs                    |  460 ++--
14 files changed, 3,342 insertions(+), 3,636 deletions(-)

Detailed changes

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"

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

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"

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<T: Entity>(&self, handle: &ModelHandle<T>) -> &T;
 }
 
+pub trait ReadModelWith {
+    fn read_model_with<E: Entity, F: FnOnce(&E, &AppContext) -> T, T>(
+        &self,
+        handle: &ModelHandle<E>,
+        read: F,
+    ) -> T;
+}
+
 pub trait UpdateModel {
     fn update_model<T, F, S>(&mut self, handle: &ModelHandle<T>, update: F) -> S
     where
@@ -61,6 +70,13 @@ pub trait ReadView {
     fn read_view<T: View>(&self, handle: &ViewHandle<T>) -> &T;
 }
 
+pub trait ReadViewWith {
+    fn read_view_with<V, F, T>(&self, handle: &ViewHandle<V>, read: F) -> T
+    where
+        V: View,
+        F: FnOnce(&V, &AppContext) -> T;
+}
+
 pub trait UpdateView {
     fn update_view<T, F, S>(&mut self, handle: &ViewHandle<T>, update: F) -> S
     where
@@ -86,6 +102,8 @@ pub enum MenuItem<'a> {
 #[derive(Clone)]
 pub struct App(Rc<RefCell<MutableAppContext>>);
 
+pub struct AsyncAppContext(Rc<RefCell<MutableAppContext>>);
+
 #[derive(Clone)]
 pub struct TestAppContext(Rc<RefCell<MutableAppContext>>, Rc<platform::test::Platform>);
 
@@ -362,6 +380,84 @@ impl TestAppContext {
     }
 }
 
+impl AsyncAppContext {
+    pub fn read<T, F: FnOnce(&AppContext) -> T>(&mut self, callback: F) -> T {
+        callback(self.0.borrow().as_ref())
+    }
+
+    pub fn update<T, F: FnOnce(&mut MutableAppContext) -> 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<T, F>(&mut self, build_model: F) -> ModelHandle<T>
+    where
+        T: Entity,
+        F: FnOnce(&mut ModelContext<T>) -> T,
+    {
+        self.update(|ctx| ctx.add_model(build_model))
+    }
+
+    pub fn background_executor(&self) -> Arc<executor::Background> {
+        self.0.borrow().ctx.background.clone()
+    }
+}
+
+impl UpdateModel for AsyncAppContext {
+    fn update_model<T, F, S>(&mut self, handle: &ModelHandle<T>, update: F) -> S
+    where
+        T: Entity,
+        F: FnOnce(&mut T, &mut ModelContext<T>) -> 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<E: Entity, F: FnOnce(&E, &AppContext) -> T, T>(
+        &self,
+        handle: &ModelHandle<E>,
+        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<T, F, S>(&mut self, handle: &ViewHandle<T>, update: F) -> S
+    where
+        T: View,
+        F: FnOnce(&mut T, &mut ViewContext<T>) -> 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<V, F, T>(&self, handle: &ViewHandle<V>, 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<T, F, S>(&mut self, handle: &ModelHandle<T>, update: F) -> S
     where
@@ -376,6 +472,18 @@ impl UpdateModel for TestAppContext {
     }
 }
 
+impl ReadModelWith for TestAppContext {
+    fn read_model_with<E: Entity, F: FnOnce(&E, &AppContext) -> T, T>(
+        &self,
+        handle: &ModelHandle<E>,
+        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<T, F, S>(&mut self, handle: &ViewHandle<T>, update: F) -> S
     where
@@ -390,6 +498,18 @@ impl UpdateView for TestAppContext {
     }
 }
 
+impl ReadViewWith for TestAppContext {
+    fn read_view_with<V, F, T>(&self, handle: &ViewHandle<V>, 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<usize, Vec<Subscription>>,
     model_observations: HashMap<usize, Vec<ModelObservation>>,
     view_observations: HashMap<usize, Vec<ViewObservation>>,
@@ -413,8 +532,6 @@ pub struct MutableAppContext {
         HashMap<usize, (Rc<RefCell<Presenter>>, Box<dyn platform::Window>)>,
     debug_elements_callbacks: HashMap<usize, Box<dyn Fn(&AppContext) -> crate::json::Value>>,
     foreground: Rc<executor::Foreground>,
-    future_handlers: Rc<RefCell<HashMap<usize, FutureHandler>>>,
-    stream_handlers: Rc<RefCell<HashMap<usize, StreamHandler>>>,
     pending_effects: VecDeque<Effect>,
     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<F, T>(&mut self, spawner: Spawner, future: F) -> EntityTask<T>
+    pub fn spawn<F, Fut, T>(&self, f: F) -> Task<T>
     where
-        F: 'static + Future,
+        F: FnOnce(AsyncAppContext) -> Fut,
+        Fut: 'static + Future<Output = T>,
         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::<T>().unwrap())
-            })
-        };
-        EntityTask::new(
-            task_id,
-            task,
-            spawner,
-            TaskHandlerMap::Future(self.future_handlers.clone()),
-        )
-    }
-
-    fn spawn_stream<F, T>(&mut self, spawner: Spawner, mut stream: F) -> EntityTask<T>
-    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::<T>().unwrap())
-        });
-
-        EntityTask::new(
-            task_id,
-            task,
-            spawner,
-            TaskHandlerMap::Stream(self.stream_handlers.clone()),
-        )
-    }
-
-    fn handle_future_output(
-        &mut self,
-        task_id: usize,
-        output: Box<dyn Any>,
-    ) -> Option<Box<dyn Any>> {
-        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<dyn Any>) -> 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<Box<dyn Any>> {
-        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<S, F, U>(&mut self, future: S, callback: F) -> EntityTask<U>
-    where
-        S: 'static + Future,
-        F: 'static + FnOnce(&mut T, S::Output, &mut ModelContext<T>) -> U,
-        U: 'static,
-    {
-        let handle = self.handle();
-        let weak_handle = handle.downgrade();
-        let task = self
-            .app
-            .spawn::<S, U>(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<dyn Any>
-                    })
-                })
-            }),
-        );
-
-        task
-    }
-
-    pub fn spawn_stream<S, F, G, U>(
-        &mut self,
-        stream: S,
-        mut item_callback: F,
-        done_callback: G,
-    ) -> EntityTask<U>
+    pub fn spawn<F, Fut, S>(&self, f: F) -> Task<S>
     where
-        S: 'static + Stream + Unpin,
-        F: 'static + FnMut(&mut T, S::Item, &mut ModelContext<T>),
-        G: 'static + FnOnce(&mut T, &mut ModelContext<T>) -> U,
-        U: 'static + Any,
+        F: FnOnce(ModelHandle<T>, AsyncAppContext) -> Fut,
+        Fut: 'static + Future<Output = S>,
+        S: 'static,
     {
         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<dyn Any>
-                    })
-                }),
-            },
-        );
-
-        task
+        self.app.spawn(|ctx| f(handle, ctx))
     }
 }
 
@@ -1773,7 +1743,6 @@ pub struct ViewContext<'a, T: ?Sized> {
     view_id: usize,
     view_type: PhantomData<T>,
     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<S, F, U>(&mut self, future: S, callback: F) -> EntityTask<U>
+    pub fn spawn<F, Fut, S>(&self, f: F) -> Task<S>
     where
-        S: 'static + Future,
-        F: 'static + FnOnce(&mut T, S::Output, &mut ViewContext<T>) -> U,
-        U: 'static,
+        F: FnOnce(ViewHandle<T>, AsyncAppContext) -> Fut,
+        Fut: 'static + Future<Output = S>,
+        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<dyn Any>
-                    })
-                })
-            }),
-        );
-
-        task
-    }
-
-    pub fn spawn_stream<S, F, G, U>(
-        &mut self,
-        stream: S,
-        mut item_callback: F,
-        done_callback: G,
-    ) -> EntityTask<U>
-    where
-        S: 'static + Stream + Unpin,
-        F: 'static + FnMut(&mut T, S::Item, &mut ViewContext<T>),
-        G: 'static + FnOnce(&mut T, &mut ViewContext<T>) -> 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<dyn Any>
-                        })
-                    })
-                }),
-            },
-        );
-        task
+        self.app.spawn(|ctx| f(handle, ctx))
     }
 }
 
@@ -2157,6 +2062,14 @@ impl<T: Entity> ModelHandle<T> {
         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<A, F, S>(&self, app: &mut A, update: F) -> S
     where
         A: UpdateModel,
@@ -2299,9 +2212,10 @@ impl<T: Entity> WeakModelHandle<T> {
         }
     }
 
-    pub fn upgrade(&self, app: &AppContext) -> Option<ModelHandle<T>> {
-        if app.models.contains_key(&self.model_id) {
-            Some(ModelHandle::new(self.model_id, &app.ref_counts))
+    pub fn upgrade(&self, ctx: impl AsRef<AppContext>) -> Option<ModelHandle<T>> {
+        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<T: View> ViewHandle<T> {
         app.read_view(self)
     }
 
+    pub fn read_with<A, F, S>(&self, ctx: &A, read: F) -> S
+    where
+        A: ReadViewWith,
+        F: FnOnce(&T, &AppContext) -> S,
+    {
+        ctx.read_view_with(self, read)
+    }
+
     pub fn update<A, F, S>(&self, app: &mut A, update: F) -> S
     where
         A: UpdateView,
@@ -2761,94 +2683,14 @@ struct ViewObservation {
     callback: Box<dyn FnMut(&mut dyn Any, usize, usize, &mut MutableAppContext, usize, usize)>,
 }
 
-type FutureHandler = Box<dyn FnOnce(Box<dyn Any>, &mut MutableAppContext) -> Option<Box<dyn Any>>>;
-
-struct StreamHandler {
-    item_callback: Box<dyn FnMut(Box<dyn Any>, &mut MutableAppContext) -> bool>,
-    done_callback: Box<dyn FnOnce(&mut MutableAppContext) -> Option<Box<dyn Any>>>,
-}
-
-#[must_use]
-pub struct EntityTask<T> {
-    id: usize,
-    task: Option<executor::Task<Option<T>>>,
-    _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<RefCell<HashMap<usize, FutureHandler>>>),
-    Stream(Rc<RefCell<HashMap<usize, StreamHandler>>>),
-}
-
-impl<T> EntityTask<T> {
-    fn new(
-        id: usize,
-        task: executor::Task<Option<T>>,
-        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<T> {
-        let task = self.task.take().unwrap();
-        task.cancel().await.unwrap()
-    }
-}
-
-impl<T> Future for EntityTask<T> {
-    type Output = T;
-
-    fn poll(
-        self: std::pin::Pin<&mut Self>,
-        ctx: &mut std::task::Context<'_>,
-    ) -> std::task::Poll<Self::Output> {
-        let task = unsafe { self.map_unchecked_mut(|task| task.task.as_mut().unwrap()) };
-        task.poll(ctx).map(|output| output.unwrap())
-    }
-}
-
-impl<T> Drop for EntityTask<T> {
-    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<ModelHandle<Model>>,
             events: Vec<String>,
@@ -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();
-            });
+        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(),
+            ]
+        );
 
-            assert_eq!(app.ctx.models.len(), 1);
-            assert!(app.subscriptions.is_empty());
-            assert!(app.model_observations.is_empty());
+        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<usize>,
@@ -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));
         });
-    }
 
-    #[test]
-    fn test_spawn_stream_local_from_model() {
-        #[derive(Default)]
-        struct Model {
-            events: Vec<Option<usize>>,
-        }
-
-        impl Entity for Model {
-            type Event = ();
-        }
+        handle_2.update(app, |model, c| {
+            model.count = 7;
+            c.notify()
+        });
+        assert_eq!(handle_1.read(app).events, vec![7]);
 
-        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]));
-        })
+        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_view_handles() {
+    #[crate::test(self)]
+    fn test_view_handles(app: &mut MutableAppContext) {
         struct View {
             other: Option<ViewHandle<View>>,
             events: Vec<String>,
@@ -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<usize>,
@@ -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(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]);
 
-            handle_3.update(app, |_, c| c.emit(9));
-            assert_eq!(handle_1.read(app).events, vec![7, 10, 5, 9]);
-        })
+        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<usize>,

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,

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"

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

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<FileHandle>,
         ctx: &mut ModelContext<Self>,
-    ) -> LocalBoxFuture<'static, Result<()>> {
+    ) -> Task<Result<()>> {
         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<str>,
         ctx: &mut ModelContext<Self>,
-    ) -> EntityTask<Option<Vec<Operation>>> {
+    ) -> Task<Option<Vec<Operation>>> {
         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::<Vec<_>>()
-                }
-            }),
-            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::<Vec<_>>()
+                    }
+                })
+                .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
-            });
-        })
+    #[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_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())
-                });
+    #[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()));
 
-                // 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);
+        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())
             });
-
-            // 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_2_events = buffer_2_events.clone();
+            ctx.subscribe(&buffer2, move |_, event, _| {
+                buffer_2_events.borrow_mut().push(event.clone())
             });
 
-            let buffer_1_events = buffer_1_events.borrow();
-            assert_eq!(
-                *buffer_1_events,
-                vec![Event::Edited, Event::Dirtied, Event::Edited, Event::Edited]
-            );
+            // 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);
+        });
 
-            let buffer_2_events = buffer_2_events.borrow();
-            assert_eq!(*buffer_2_events, vec![Event::Edited, Event::Dirtied]);
+        // 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]);
     }
 
-    #[test]
-    fn test_random_edits() {
+    #[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);
-
-                let reference_string_len = rng.gen_range(0..3);
-                let mut reference_string = RandomCharIter::new(&mut rng)
-                    .take(reference_string_len)
-                    .collect::<String>();
-                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);
+            println!("{:?}", seed);
+            let mut rng = &mut StdRng::seed_from_u64(seed);
 
-                        if rng.gen_bool(0.25) {
-                            buffer.randomly_undo_redo(rng);
-                            reference_string = buffer.text();
-                        }
+            let reference_string_len = rng.gen_range(0..3);
+            let mut reference_string = RandomCharIter::new(&mut rng)
+                .take(reference_string_len)
+                .collect::<String>();
+            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();
+                    }
 
-                        {
-                            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());
-                        }
+                        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));
+                    }
 
-                        if rng.gen_bool(0.3) {
-                            buffer_versions.push(buffer.clone());
-                        }
+                    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());
                     }
 
-                    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());
+                    if rng.gen_bool(0.3) {
+                        buffer_versions.push(buffer.clone());
                     }
+                }
 
-                    buffer
-                })
+                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();
-
-                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 }
-                );
+    #[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![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![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![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![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![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![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 }
+            );
 
-                // 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()
-                );
+            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 }
+            );
 
-                // 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();
+            // 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()
+            );
 
-                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
-                );
+            // 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_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_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_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_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
         });
     }
 
-    #[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();
-
-                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();
-
-                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
-            });
+    #[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);
+
+            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
         });
     }
 
@@ -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)
-            });
-
-            // 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::<Vec<_>>(),
-                    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";
+        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)
+        });
 
-            fs::write(&abs_path, new_contents).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::<Vec<_>>(),
+                Some(ctx),
+            )
+        });
 
-            // 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;
+        // 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";
 
-            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::<Vec<_>>();
-                assert_eq!(
-                    cursor_positions,
-                    &[Point::new(1, 0), Point::new(3, 0), Point::new(4, 0),]
-                );
-            });
+        fs::write(&abs_path, new_contents).unwrap();
 
-            // Modify the buffer
-            buffer.update(&mut app, |buffer, ctx| {
-                buffer.edit(vec![0..0], " ", Some(ctx)).unwrap();
-                assert!(buffer.is_dirty());
-            });
+        // 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;
 
-            // Change the file on disk again, adding blank lines to the beginning.
-            fs::write(&abs_path, "\n\n\nAAAA\naaa\nBB\nbbbbb\n").unwrap();
+        buffer.update(&mut app, |buffer, _| {
+            assert_eq!(buffer.text(), new_contents);
+            assert!(!buffer.is_dirty());
+            assert!(!buffer.has_conflict());
 
-            // 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;
+            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::<Vec<_>>();
+            assert_eq!(
+                cursor_positions,
+                &[Point::new(1, 0), Point::new(3, 0), Point::new(4, 0),]
+            );
         });
-    }
 
-    #[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));
+        // Modify the buffer
+        buffer.update(&mut app, |buffer, ctx| {
+            buffer.edit(vec![0..0], " ", Some(ctx)).unwrap();
+            assert!(buffer.is_dirty());
+        });
 
-            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));
+        // 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");
 
-            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");
+    #[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);
 
-                buffer
-            });
-        });
-    }
+            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]);
 
-    #[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]);
+            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]);
+            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
-            });
+            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;

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<FileHandle>,
         ctx: &mut ViewContext<Self>,
-    ) -> LocalBoxFuture<'static, Result<()>> {
+    ) -> Task<Result<()>> {
         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));
-
-            buffer_view.update(app, |view, ctx| {
-                view.begin_selection(DisplayPoint::new(2, 2), false, 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));
 
-            let view = buffer_view.read(app);
-            let selections = view
-                .selections_in_range(
-                    DisplayPoint::zero()..view.max_point(app.as_ref()),
-                    app.as_ref(),
-                )
-                .collect::<Vec<_>>();
-            assert_eq!(
-                selections,
-                [DisplayPoint::new(2, 2)..DisplayPoint::new(2, 2)]
-            );
+        buffer_view.update(app, |view, ctx| {
+            view.begin_selection(DisplayPoint::new(2, 2), false, ctx);
+        });
 
-            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::<Vec<_>>();
+        assert_eq!(
+            selections,
+            [DisplayPoint::new(2, 2)..DisplayPoint::new(2, 2)]
+        );
 
-            let view = buffer_view.read(app);
-            let selections = view
-                .selections_in_range(
-                    DisplayPoint::zero()..view.max_point(app.as_ref()),
-                    app.as_ref(),
-                )
-                .collect::<Vec<_>>();
-            assert_eq!(
-                selections,
-                [DisplayPoint::new(2, 2)..DisplayPoint::new(3, 3)]
-            );
+        buffer_view.update(app, |view, ctx| {
+            view.update_selection(DisplayPoint::new(3, 3), Vector2F::zero(), ctx);
+        });
 
-            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::<Vec<_>>();
+        assert_eq!(
+            selections,
+            [DisplayPoint::new(2, 2)..DisplayPoint::new(3, 3)]
+        );
 
-            let view = buffer_view.read(app);
-            let selections = view
-                .selections_in_range(
-                    DisplayPoint::zero()..view.max_point(app.as_ref()),
-                    app.as_ref(),
-                )
-                .collect::<Vec<_>>();
-            assert_eq!(
-                selections,
-                [DisplayPoint::new(2, 2)..DisplayPoint::new(1, 1)]
-            );
+        buffer_view.update(app, |view, ctx| {
+            view.update_selection(DisplayPoint::new(1, 1), Vector2F::zero(), ctx);
+        });
 
-            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::<Vec<_>>();
+        assert_eq!(
+            selections,
+            [DisplayPoint::new(2, 2)..DisplayPoint::new(1, 1)]
+        );
 
-            let view = buffer_view.read(app);
-            let selections = view
-                .selections_in_range(
-                    DisplayPoint::zero()..view.max_point(app.as_ref()),
-                    app.as_ref(),
-                )
-                .collect::<Vec<_>>();
-            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);
+        });
 
-            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::<Vec<_>>();
+        assert_eq!(
+            selections,
+            [DisplayPoint::new(2, 2)..DisplayPoint::new(1, 1)]
+        );
 
-            let view = buffer_view.read(app);
-            let selections = view
-                .selections_in_range(
-                    DisplayPoint::zero()..view.max_point(app.as_ref()),
-                    app.as_ref(),
-                )
-                .collect::<Vec<_>>();
-            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.begin_selection(DisplayPoint::new(3, 3), true, ctx);
+            view.update_selection(DisplayPoint::new(0, 0), Vector2F::zero(), ctx);
+        });
 
-            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::<Vec<_>>();
+        assert_eq!(
+            selections,
+            [
+                DisplayPoint::new(2, 2)..DisplayPoint::new(1, 1),
+                DisplayPoint::new(3, 3)..DisplayPoint::new(0, 0)
+            ]
+        );
 
-            let view = buffer_view.read(app);
-            let selections = view
-                .selections_in_range(
-                    DisplayPoint::zero()..view.max_point(app.as_ref()),
-                    app.as_ref(),
-                )
-                .collect::<Vec<_>>();
-            assert_eq!(
-                selections,
-                [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::<Vec<_>>();
+        assert_eq!(
+            selections,
+            [DisplayPoint::new(3, 3)..DisplayPoint::new(0, 0)]
+        );
     }
 
-    #[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));
+    #[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));
 
-            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.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.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)]
-            );
+        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_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));
+    #[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.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.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(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)]
-            );
-        });
+        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)]
+        );
     }
 
-    #[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_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 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 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);
-        })
+        let layouts = view
+            .read(app)
+            .layout_line_numbers(1000.0, &font_cache, &layout_cache, app.as_ref())
+            .unwrap();
+        assert_eq!(layouts.len(), 6);
     }
 
-    #[test]
-    fn test_fold() {
-        App::test((), |app| {
-            let buffer = app.add_model(|ctx| {
-                Buffer::new(
-                    0,
-                    "
+    #[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));
-
-            view.update(app, |view, ctx| {
-                view.select_display_ranges(
-                    &[DisplayPoint::new(8, 0)..DisplayPoint::new(12, 0)],
-                    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)
                 .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));
-
-            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)]
-                );
-            });
-        });
-    }
+    #[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));
 
-    #[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),
+        buffer.update(app, |buffer, ctx| {
+            buffer
+                .edit(
+                    vec![
+                        Point::new(1, 0)..Point::new(1, 0),
+                        Point::new(1, 1)..Point::new(1, 1),
                     ],
-                    ctx,
+                    "\t",
+                    Some(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));
+        view.update(app, |view, ctx| {
+            view.move_down(&(), 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.selection_ranges(ctx.as_ref()),
+                &[DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)]
             );
 
-            view.update(app, |view, ctx| view.move_to_beginning_of_line(&(), ctx));
+            view.move_right(&(), 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.selection_ranges(ctx.as_ref()),
+                &[DisplayPoint::new(1, 4)..DisplayPoint::new(1, 4)]
             );
 
-            view.update(app, |view, ctx| view.move_to_end_of_line(&(), ctx));
+            view.move_left(&(), 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.selection_ranges(ctx.as_ref()),
+                &[DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)]
             );
 
-            // Moving to the end of line again is a no-op.
-            view.update(app, |view, ctx| view.move_to_end_of_line(&(), ctx));
+            view.move_up(&(), 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.selection_ranges(ctx.as_ref()),
+                &[DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)]
             );
 
-            view.update(app, |view, ctx| {
-                view.move_left(&(), ctx);
-                view.select_to_beginning_of_line(&true, ctx);
-            });
+            view.move_to_end(&(), 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.selection_ranges(ctx.as_ref()),
+                &[DisplayPoint::new(5, 6)..DisplayPoint::new(5, 6)]
             );
 
-            view.update(app, |view, ctx| {
-                view.select_to_beginning_of_line(&true, ctx)
-            });
+            view.move_to_beginning(&(), 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.selection_ranges(ctx.as_ref()),
+                &[DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)]
             );
 
-            view.update(app, |view, ctx| {
-                view.select_to_beginning_of_line(&true, ctx)
-            });
+            view.select_display_ranges(&[DisplayPoint::new(0, 1)..DisplayPoint::new(0, 2)], ctx)
+                .unwrap();
+            view.select_to_beginning(&(), 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.selection_ranges(ctx.as_ref()),
+                &[DisplayPoint::new(0, 1)..DisplayPoint::new(0, 0)]
             );
 
-            view.update(app, |view, ctx| view.select_to_end_of_line(&(), ctx));
+            view.select_to_end(&(), 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.selection_ranges(ctx.as_ref()),
+                &[DisplayPoint::new(0, 1)..DisplayPoint::new(5, 6)]
             );
+        });
+    }
 
-            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()),
+    #[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, 2)..DisplayPoint::new(0, 2),
+                    DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1),
                     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),
-                ]
-            );
+                ],
+                ctx,
+            )
+            .unwrap();
         });
-    }
 
-    #[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_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_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_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_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_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_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_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_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),
-                ]
-            );
+        // 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_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_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.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.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.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.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.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.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.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.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.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.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),
+            ]
+        );
+    }
 
-            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()),
+    #[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, 10)..DisplayPoint::new(0, 9),
-                    DisplayPoint::new(2, 3)..DisplayPoint::new(2, 2),
-                ]
-            );
+                    DisplayPoint::new(0, 11)..DisplayPoint::new(0, 11),
+                    DisplayPoint::new(2, 4)..DisplayPoint::new(2, 4),
+                ],
+                ctx,
+            )
+            .unwrap();
+        });
 
-            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.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.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.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.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.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.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),
-                ]
-            );
+        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),
+            ]
+        );
 
-    #[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);
-            });
+        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),
+            ]
+        );
 
-            assert_eq!(
-                buffer.read(app).text(),
-                "oe two three\nfou five six\nseven ten\n"
-            );
-        })
+        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_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);
-            });
+    #[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));
 
-            assert_eq!(
-                buffer.read(app).text(),
-                "on two three\nfou five six\nseven ten\n"
-            );
-        })
+        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_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)
-                ]
-            );
+    #[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));
 
-            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)]
-            );
+        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_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![
+    #[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(1, 2)..DisplayPoint::new(1, 2),
                     DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0),
-                    DisplayPoint::new(6, 0)..DisplayPoint::new(6, 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(0, 1)..DisplayPoint::new(1, 1),
-                        DisplayPoint::new(1, 2)..DisplayPoint::new(2, 1),
-                    ],
-                    ctx,
-                )
+        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.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),
-                ]
-            );
+            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_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(
-                    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"
-            );
+    #[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),
+            ]
+        );
 
-            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)
-                ]
-            );
+        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),
+            ]
+        );
+    }
 
-            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()),
+    #[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![
-                    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)
-                ]
+                    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.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),
+            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)
-                ]
-            );
-
-            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)
-                ]
-            );
+                    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"
+        );
+
+        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)
+            ]
+        );
+
+        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)
+            ]
+        );
     }
 
-    #[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;
+    #[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 ");
+        // 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 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 ) "
-            );
+        // 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 ) "
-            );
+        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 ) "
-            );
+        // 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()),
+        // 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, 2)..DisplayPoint::new(0, 2),
-                    DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1),
-                    DisplayPoint::new(3, 3)..DisplayPoint::new(3, 3),
-                ]
-            );
+                    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,
-                )
+        // 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.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!(
+        // 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()),
-                &[
-                    DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1),
-                    DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0),
-                    DisplayPoint::new(5, 1)..DisplayPoint::new(5, 1),
-                ]
-            );
-        });
+        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),
+            ]
+        );
     }
 
-    #[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)]
-            );
-        });
+    #[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)]
+        );
     }
 
-    #[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),
-                ]
-            );
+    #[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(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(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)]
-            );
-        });
+        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"
+    #[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![
+                    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,
             );
-            assert_eq!(
-                view.read(app).selection_ranges(app.as_ref()),
-                [
-                    DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1),
+            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)
-                ]
-            );
+                    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.select_display_ranges(
-                    &[DisplayPoint::new(4, 0)..DisplayPoint::new(0, 1)],
-                    ctx,
-                )
+        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)
-                ]
-            );
+            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)
+            ]
+        );
     }
 
-    #[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));
+    #[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,
-                )
+        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_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)]
+        );
 
-            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.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,
-                )
+        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_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.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,
-                )
+        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),
+            ]
+        );
 
-            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_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.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,
-                )
+        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),
-                ]
-            );
         });
+        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 {

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());
-
-            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::<Vec<_>>()
-            });
-            assert_eq!(map.text(app.as_ref()), "123a…c123456eee");
+    #[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");
+
+        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");
 
-            map.unfold(Some(Point::new(0, 4)..Point::new(0, 5)), app.as_ref())
+        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();
-            assert_eq!(map.text(app.as_ref()), "123aaaaa\nbbbbbb\nccc123456eee");
+            buffer.edits_since(start_version).collect::<Vec<_>>()
         });
+        assert_eq!(map.text(app.as_ref()), "123a…c123456eee");
+
+        map.unfold(Some(Point::new(0, 4)..Point::new(0, 5)), app.as_ref())
+            .unwrap();
+        assert_eq!(map.text(app.as_ref()), "123aaaaa\nbbbbbb\nccc123456eee");
     }
 
-    #[test]
-    fn test_adjacent_folds() {
-        App::test((), |app| {
-            let buffer = app.add_model(|ctx| Buffer::new(0, "abcdefghijkl", 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());
+        {
+            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");
+            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 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");
-            }
+            // 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());
+        {
+            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");
+            // 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::<Vec<_>>()
-                });
-                map.check_invariants(app.as_ref());
-                assert_eq!(map.text(app.as_ref()), "12345…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::<Vec<_>>()
+            });
+            map.check_invariants(app.as_ref());
+            assert_eq!(map.text(app.as_ref()), "12345…fghijkl");
+        }
     }
 
-    #[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(),
-            )
-            .unwrap();
-            assert_eq!(map.text(app.as_ref()), "aa…eeeee");
-        })
+    #[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_merging_folds_via_edit() {
-        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(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_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");
     }
 
-    #[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::<Vec<_>>();
-            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_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::<Vec<_>>();
+        assert_eq!(
+            fold_ranges,
+            vec![
+                Point::new(0, 2)..Point::new(2, 2),
+                Point::new(1, 2)..Point::new(3, 2)
+            ]
+        );
     }
 
-    #[test]
-    fn test_random_folds() {
+    #[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::<String>();
-                    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();
-                        }
-                        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 buffer = app.add_model(|ctx| {
+                let len = rng.gen_range(0..10);
+                let text = RandomCharIter::new(&mut rng).take(len).collect::<String>();
+                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);
                         }
-                        _ => {
-                            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::<Vec<_>>()
-                            });
-                            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, "…");
+                    _ => {
+                        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::<Vec<_>>()
+                        });
+                        log::info!("editing {:?}", edits);
                     }
-                    expected_buffer_rows.extend((0..=next_row).rev());
-                    expected_buffer_rows.reverse();
-
-                    assert_eq!(map.text(app.as_ref()), expected_text);
+                }
+                map.check_invariants(app.as_ref());
 
-                    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 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();
 
-                    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;
-                    }
+                assert_eq!(map.text(app.as_ref()), expected_text);
 
-                    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::<String>(),
-                            expected_text
-                                .chars()
-                                .skip(offset)
-                                .take(len)
-                                .collect::<String>()
-                        );
-                    }
+                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);
+                }
 
-                    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::<Vec<_>>(),
-                            expected_buffer_rows[idx..],
-                        );
-                    }
+                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
+                    );
 
-                    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()));
+                    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 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::<Vec<_>>();
-
-                        assert_eq!(
-                            map.folds_in_range(start..end, app.as_ref())
-                                .unwrap()
-                                .cloned()
-                                .collect::<Vec<_>>(),
-                            expected_folds
-                        );
-                    }
+                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::<String>(),
+                        expected_text
+                            .chars()
+                            .skip(offset)
+                            .take(len)
+                            .collect::<String>()
+                    );
                 }
-            });
-        }
-    }
 
-    #[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));
+                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::<Vec<_>>(),
+                        expected_buffer_rows[idx..],
+                    );
+                }
 
-            let mut map = FoldMap::new(buffer.clone(), app.as_ref());
+                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()));
+                }
 
-            map.fold(
-                vec![
-                    Point::new(0, 2)..Point::new(2, 2),
-                    Point::new(3, 1)..Point::new(4, 1),
-                ],
-                app.as_ref(),
-            )
-            .unwrap();
+                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::<Vec<_>>();
+
+                    assert_eq!(
+                        map.folds_in_range(start..end, app.as_ref())
+                            .unwrap()
+                            .cloned()
+                            .collect::<Vec<_>>(),
+                        expected_folds
+                    );
+                }
+            }
+        }
+    }
 
-            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<_>>(),
-                vec![0, 3, 5, 6]
-            );
-            assert_eq!(
-                map.snapshot(app.as_ref())
-                    .buffer_rows(3)
-                    .unwrap()
-                    .collect::<Vec<_>>(),
-                vec![6]
-            );
-        });
+    #[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());
+
+        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<_>>(),
+            vec![0, 3, 5, 6]
+        );
+        assert_eq!(
+            map.snapshot(app.as_ref())
+                .buffer_rows(3)
+                .unwrap()
+                .collect::<Vec<_>>(),
+            vec![6]
+        );
     }
 
     impl FoldMap {

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();
-
-            assert_eq!(
-                map.snapshot(app.as_ref())
-                    .chars_at(DisplayPoint::new(1, 0), app.as_ref())
-                    .unwrap()
-                    .take(10)
-                    .collect::<String>(),
-                "    b   bb"
-            );
-            assert_eq!(
-                map.snapshot(app.as_ref())
-                    .chars_at(DisplayPoint::new(1, 2), app.as_ref())
-                    .unwrap()
-                    .take(10)
-                    .collect::<String>(),
-                "  b   bbbb"
-            );
-            assert_eq!(
-                map.snapshot(app.as_ref())
-                    .chars_at(DisplayPoint::new(1, 6), app.as_ref())
-                    .unwrap()
-                    .take(13)
-                    .collect::<String>(),
-                "  bbbbb\nc   c"
-            );
-        });
+    #[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::<String>(),
+            "    b   bb"
+        );
+        assert_eq!(
+            map.snapshot(app.as_ref())
+                .chars_at(DisplayPoint::new(1, 2), app.as_ref())
+                .unwrap()
+                .take(10)
+                .collect::<String>(),
+            "  b   bbbb"
+        );
+        assert_eq!(
+            map.snapshot(app.as_ref())
+                .chars_at(DisplayPoint::new(1, 6), app.as_ref())
+                .unwrap()
+                .take(13)
+                .collect::<String>(),
+            "  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))
     }
 }

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(),
-                (),
-            );
+    #[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);
+        });
 
-            let finder = app.read(|ctx| {
-                workspace
-                    .read(ctx)
-                    .modal()
-                    .cloned()
-                    .unwrap()
-                    .downcast::<FileFinder>()
-                    .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");
-            });
+        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::<FileFinder>()
+                .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");
         });
     }
 
-    #[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,
-                );
+    #[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])
-            });
+            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;
+    #[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;
     }
 
-    #[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;
+    #[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));
+        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);
-            });
+        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);
         });
     }
 }

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<FileHandle>,
         _: &mut ViewContext<Self>,
-    ) -> LocalBoxFuture<'static, anyhow::Result<()>>;
+    ) -> Task<anyhow::Result<()>>;
     fn should_activate_item_on_event(_: &Self::Event) -> bool {
         false
     }
@@ -165,7 +165,7 @@ pub trait ItemViewHandle: Send + Sync {
         &self,
         file: Option<FileHandle>,
         ctx: &mut MutableAppContext,
-    ) -> LocalBoxFuture<'static, anyhow::Result<()>>;
+    ) -> Task<anyhow::Result<()>>;
 }
 
 impl<T: Item> ItemHandle for ModelHandle<T> {
@@ -243,7 +243,7 @@ impl<T: ItemView> ItemViewHandle for ViewHandle<T> {
         &self,
         file: Option<FileHandle>,
         ctx: &mut MutableAppContext,
-    ) -> LocalBoxFuture<'static, anyhow::Result<()>> {
+    ) -> Task<anyhow::Result<()>> {
         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::<Vec<_>>();
         async move {
@@ -450,7 +451,7 @@ impl Workspace {
         &mut self,
         entry: (usize, Arc<Path>),
         ctx: &mut ViewContext<Self>,
-    ) -> Option<EntityTask<()>> {
+    ) -> Option<Task<()>> {
         // 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<Self>) -> Option<Box<dyn ItemViewHandle>> {
@@ -550,8 +553,8 @@ impl Workspace {
 
     pub fn save_active_item(&mut self, _: &(), ctx: &mut ViewContext<Self>) {
         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<Workspace> {
 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;
-
-            init(app);
-
-            let dir = temp_tree(json!({
-                "a": {
-                    "aa": null,
-                    "ab": null,
-                },
-                "b": {
-                    "ba": null,
-                    "bb": null,
-                },
-                "c": {
-                    "ca": null,
-                    "cb": null,
-                },
-            }));
-
-            app.dispatch_global_action(
-                "workspace:open_paths",
-                OpenParams {
-                    paths: vec![
-                        dir.path().join("a").to_path_buf(),
-                        dir.path().join("b").to_path_buf(),
-                    ],
-                    settings: settings.clone(),
-                },
-            );
-            assert_eq!(app.window_ids().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::<Workspace>(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);
-        });
-    }
+    #[gpui::test]
+    fn test_open_paths_action(app: &mut gpui::MutableAppContext) {
+        let settings = settings::channel(&app.font_cache()).unwrap().1;
 
-    #[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",
-                },
-            }));
-
-            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
-            });
+        init(app);
 
-            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();
+        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()],
+                settings: settings.clone(),
+            },
+        );
+        assert_eq!(app.window_ids().count(), 1);
+        let workspace_view_1 = app
+            .root_view::<Workspace>(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);
+    }
 
-            // Open the first entry
-            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);
-            });
+    #[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",
+            },
+        }));
 
-            // Open the second entry
+        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
-                .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::<Vec<_>>();
-                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 the first entry
+        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 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 second entry
+        workspace
+            .update(&mut app, |w, ctx| w.open_entry(file2.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(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::<HashSet<_>>();
-                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::<Vec<_>>();
+            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::<BufferView>()
-                    .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| {
+        // 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::<HashSet<_>>();
+            assert_eq!(
+                worktree_roots,
+                vec![dir1.path(), &dir2.path().join("b.txt")]
+                    .into_iter()
+                    .collect(),
+            );
+            assert_eq!(
                 workspace
-                    .active_item(ctx)
-                    .unwrap()
-                    .to_any()
-                    .downcast::<BufferView>()
+                    .read(ctx)
+                    .active_pane()
+                    .read(ctx)
+                    .active_item()
                     .unwrap()
-            });
-            app.read(|ctx| {
-                assert_eq!(editor2.read(ctx).buffer(), editor.read(ctx).buffer());
-            })
+                    .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)
-                })
+    #[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
+        });
+        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::<BufferView>().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;
-            let editor = app.read(|ctx| {
-                let pane = workspace.read(ctx).active_pane().read(ctx);
-                let item = pane.active_item().unwrap();
-                item.to_any().downcast::<BufferView>().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));
-            });
+        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();
+    #[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()
+                .clone()
+        });
+        tree.flush_fs_events(&app).await;
+
+        // 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::<BufferView>()
+                .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");
+        });
 
-            let pane_1 = app.read(|ctx| workspace.read(ctx).active_pane().clone());
+        // 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
-                .update(&mut app, |w, ctx| w.open_entry(file1.clone(), ctx))
+                .active_item(ctx)
                 .unwrap()
-                .await;
-            app.read(|ctx| {
-                assert_eq!(
-                    pane_1.read(ctx).active_item().unwrap().entry_id(ctx),
-                    Some(file1.clone())
-                );
-            });
+                .to_any()
+                .downcast::<BufferView>()
+                .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);
+        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()));
+            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);
-            });
+            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);
         });
     }
 }

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<Self>) -> impl Future<Output = ()> {
         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<Self>) {
@@ -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"),
-            )
-            .unwrap();
+        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));
+        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);
+        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")
-                );
+            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::<Vec<Arc<Path>>>();
-                assert_eq!(
-                    results,
-                    vec![
-                        PathBuf::from("banana/carrot/date").into(),
-                        PathBuf::from("banana/carrot/endive").into(),
-                    ]
-                );
-            })
-        });
+            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::<Vec<Arc<Path>>>();
+            assert_eq!(
+                results,
+                vec![
+                    PathBuf::from("banana/carrot/date").into(),
+                    PathBuf::from("banana/carrot/endive").into(),
+                ]
+            );
+        })
     }
 
-    #[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_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 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 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 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 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());
         });
     }
 
-    #[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));
+    #[gpui::test]
+    async fn test_save_in_single_file_worktree(mut app: gpui::TestAppContext) {
+        let dir = temp_tree(json!({
+            "file1": "the old contents",
+        }));
 
-            let buffer =
-                app.add_model(|ctx| Buffer::new(1, "a line of text.\n".repeat(10 * 1024), ctx));
+        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 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 buffer =
+            app.add_model(|ctx| Buffer::new(1, "a line of text.\n".repeat(10 * 1024), ctx));
 
-            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()));
+        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.
-            assert!(!file2.is_deleted());
-            assert!(!file3.is_deleted());
-            assert!(!file4.is_deleted());
-            assert!(!file5.is_deleted());
-            assert!(!non_existent_file.is_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;
 
-            // After scanning, the worktree knows which files exist and which don't.
-            app.read(|ctx| tree.read(ctx).scan_complete()).await;
+        app.read(|ctx| {
+            assert_eq!(
+                tree.read(ctx)
+                    .paths()
+                    .map(|p| p.to_str().unwrap())
+                    .collect::<Vec<_>>(),
+                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());
-
-            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<_>>(),
-                    vec![
-                        "a",
-                        "a/file1",
-                        "a/file2.new",
-                        "b",
-                        "d",
-                        "d/file3",
-                        "d/file4"
-                    ]
-                );
+            assert!(file5.is_deleted());
 
-                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",
-                }
-            }));
-
-            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);
-            });
+    #[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);
+        });
 
-            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);
         });
     }