Merge branch 'main' into breadcrumbs

Antonio Scandurra created

Change summary

Cargo.lock                                                       |  35 
crates/chat_panel/src/chat_panel.rs                              |  21 
crates/client/src/channel.rs                                     |   2 
crates/client/src/client.rs                                      | 274 
crates/client/src/test.rs                                        |   2 
crates/contacts_panel/src/contacts_panel.rs                      |   4 
crates/diagnostics/src/diagnostics.rs                            | 121 
crates/diagnostics/src/items.rs                                  |   4 
crates/editor/Cargo.toml                                         |   2 
crates/editor/src/display_map.rs                                 | 261 
crates/editor/src/display_map/block_map.rs                       |   2 
crates/editor/src/display_map/fold_map.rs                        |  43 
crates/editor/src/editor.rs                                      | 627 
crates/editor/src/element.rs                                     |  90 
crates/editor/src/items.rs                                       | 413 
crates/editor/src/movement.rs                                    | 499 
crates/editor/src/multi_buffer.rs                                |  87 
crates/editor/src/multi_buffer/anchor.rs                         |  23 
crates/editor/src/test.rs                                        |  34 
crates/file_finder/Cargo.toml                                    |   2 
crates/file_finder/src/file_finder.rs                            |  21 
crates/go_to_line/src/go_to_line.rs                              |   7 
crates/gpui/src/app.rs                                           | 592 +
crates/gpui/src/executor.rs                                      |   2 
crates/gpui/src/keymap.rs                                        |  14 
crates/gpui/src/platform/mac/renderer.rs                         |   7 
crates/gpui/src/presenter.rs                                     |  43 
crates/language/src/buffer.rs                                    |  35 
crates/language/src/diagnostic_set.rs                            |  24 
crates/language/src/proto.rs                                     |  39 
crates/language/src/tests.rs                                     |  40 
crates/lsp/src/lsp.rs                                            |  57 
crates/outline/src/outline.rs                                    |  17 
crates/project/src/project.rs                                    | 447 
crates/project/src/worktree.rs                                   |  57 
crates/project_panel/src/project_panel.rs                        | 110 
crates/project_symbols/src/project_symbols.rs                    |  18 
crates/rpc/proto/zed.proto                                       | 103 
crates/rpc/src/peer.rs                                           |   2 
crates/rpc/src/proto.rs                                          |  19 
crates/rpc/src/rpc.rs                                            |   2 
crates/search/src/buffer_search.rs                               |  38 
crates/search/src/project_search.rs                              | 109 
crates/search/src/search.rs                                      |   8 
crates/server/src/rpc.rs                                         | 906 +
crates/text/src/anchor.rs                                        |  50 
crates/text/src/patch.rs                                         |  66 
crates/text/src/selection.rs                                     |  33 
crates/text/src/tests.rs                                         |  36 
crates/text/src/text.rs                                          |  26 
crates/theme/src/theme.rs                                        |   2 
crates/theme_selector/src/theme_selector.rs                      |  16 
crates/util/src/test.rs                                          |  47 
crates/vim/Cargo.toml                                            |  25 
crates/vim/src/editor_events.rs                                  |  53 
crates/vim/src/insert.rs                                         |  30 
crates/vim/src/mode.rs                                           |  36 
crates/vim/src/normal.rs                                         |  66 
crates/vim/src/vim.rs                                            |  97 
crates/vim/src/vim_tests.rs                                      | 253 
crates/workspace/src/lsp_status.rs                               |   8 
crates/workspace/src/pane.rs                                     | 332 
crates/workspace/src/pane_group.rs                               |  78 
crates/workspace/src/settings.rs                                 |   6 
crates/workspace/src/status_bar.rs                               |  10 
crates/workspace/src/workspace.rs                                | 753 +
crates/zed/Cargo.toml                                            |   4 
crates/zed/assets/fonts/zed-mono/zed-mono-extended.ttf           |   0 
crates/zed/assets/fonts/zed-mono/zed-mono-extendedbold.ttf       |   0 
crates/zed/assets/fonts/zed-mono/zed-mono-extendedbolditalic.ttf |   0 
crates/zed/assets/fonts/zed-mono/zed-mono-extendeditalic.ttf     |   0 
crates/zed/assets/fonts/zed-sans/zed-sans-extended.ttf           |   0 
crates/zed/assets/fonts/zed-sans/zed-sans-extendedbold.ttf       |   0 
crates/zed/assets/fonts/zed-sans/zed-sans-extendedbolditalic.ttf |   0 
crates/zed/assets/fonts/zed-sans/zed-sans-extendeditalic.ttf     |   0 
crates/zed/assets/themes/_base.toml                              |   2 
crates/zed/src/main.rs                                           |  27 
crates/zed/src/test.rs                                           |   6 
crates/zed/src/zed.rs                                            |  61 
79 files changed, 5,193 insertions(+), 2,193 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -1617,6 +1617,7 @@ dependencies = [
  "collections",
  "ctor",
  "env_logger",
+ "futures",
  "fuzzy",
  "gpui",
  "itertools",
@@ -1629,6 +1630,7 @@ dependencies = [
  "postage",
  "project",
  "rand 0.8.3",
+ "rpc",
  "serde",
  "smallvec",
  "smol",
@@ -1782,7 +1784,9 @@ dependencies = [
 name = "file_finder"
 version = "0.1.0"
 dependencies = [
+ "ctor",
  "editor",
+ "env_logger",
  "fuzzy",
  "gpui",
  "postage",
@@ -2524,6 +2528,15 @@ dependencies = [
  "hashbrown 0.9.1",
 ]
 
+[[package]]
+name = "indoc"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7906a9fababaeacb774f72410e497a1d18de916322e33797bb2cd29baa23c9e"
+dependencies = [
+ "unindent",
+]
+
 [[package]]
 name = "infer"
 version = "0.2.3"
@@ -5550,9 +5563,9 @@ checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e"
 
 [[package]]
 name = "unindent"
-version = "0.1.7"
+version = "0.1.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f14ee04d9415b52b3aeab06258a3f07093182b88ba0f9b8d203f211a7a7d41c7"
+checksum = "514672a55d7380da379785a4d70ca8386c8883ff7eaae877be4d2081cebe73d8"
 
 [[package]]
 name = "universal-hash"
@@ -5670,6 +5683,21 @@ version = "0.9.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed"
 
+[[package]]
+name = "vim"
+version = "0.1.0"
+dependencies = [
+ "collections",
+ "editor",
+ "gpui",
+ "indoc",
+ "language",
+ "log",
+ "project",
+ "util",
+ "workspace",
+]
+
 [[package]]
 name = "waker-fn"
 version = "1.1.0"
@@ -5929,7 +5957,7 @@ checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9"
 
 [[package]]
 name = "zed"
-version = "0.21.0"
+version = "0.23.0"
 dependencies = [
  "anyhow",
  "async-compression",
@@ -6000,6 +6028,7 @@ dependencies = [
  "unindent",
  "url",
  "util",
+ "vim",
  "workspace",
 ]
 

crates/chat_panel/src/chat_panel.rs 🔗

@@ -64,13 +64,13 @@ impl ChatPanel {
                         ix,
                         item_type,
                         is_hovered,
-                        &cx.app_state::<Settings>().theme.chat_panel.channel_select,
+                        &cx.global::<Settings>().theme.chat_panel.channel_select,
                         cx,
                     )
                 }
             })
             .with_style(move |cx| {
-                let theme = &cx.app_state::<Settings>().theme.chat_panel.channel_select;
+                let theme = &cx.global::<Settings>().theme.chat_panel.channel_select;
                 SelectStyle {
                     header: theme.header.container.clone(),
                     menu: theme.menu.clone(),
@@ -200,7 +200,7 @@ impl ChatPanel {
     }
 
     fn render_channel(&self, cx: &mut RenderContext<Self>) -> ElementBox {
-        let theme = &cx.app_state::<Settings>().theme;
+        let theme = &cx.global::<Settings>().theme;
         Flex::column()
             .with_child(
                 Container::new(ChildView::new(&self.channel_select).boxed())
@@ -224,7 +224,7 @@ impl ChatPanel {
 
     fn render_message(&self, message: &ChannelMessage, cx: &AppContext) -> ElementBox {
         let now = OffsetDateTime::now_utc();
-        let settings = cx.app_state::<Settings>();
+        let settings = cx.global::<Settings>();
         let theme = if message.is_pending() {
             &settings.theme.chat_panel.pending_message
         } else {
@@ -267,7 +267,7 @@ impl ChatPanel {
     }
 
     fn render_input_box(&self, cx: &AppContext) -> ElementBox {
-        let theme = &cx.app_state::<Settings>().theme;
+        let theme = &cx.global::<Settings>().theme;
         Container::new(ChildView::new(&self.input_editor).boxed())
             .with_style(theme.chat_panel.input_editor.container)
             .boxed()
@@ -304,7 +304,7 @@ impl ChatPanel {
     }
 
     fn render_sign_in_prompt(&self, cx: &mut RenderContext<Self>) -> ElementBox {
-        let theme = cx.app_state::<Settings>().theme.clone();
+        let theme = cx.global::<Settings>().theme.clone();
         let rpc = self.rpc.clone();
         let this = cx.handle();
 
@@ -327,7 +327,12 @@ impl ChatPanel {
                 let rpc = rpc.clone();
                 let this = this.clone();
                 cx.spawn(|mut cx| async move {
-                    if rpc.authenticate_and_connect(&cx).log_err().await.is_some() {
+                    if rpc
+                        .authenticate_and_connect(true, &cx)
+                        .log_err()
+                        .await
+                        .is_some()
+                    {
                         cx.update(|cx| {
                             if let Some(this) = this.upgrade(cx) {
                                 if this.is_focused(cx) {
@@ -385,7 +390,7 @@ impl View for ChatPanel {
         } else {
             self.render_sign_in_prompt(cx)
         };
-        let theme = &cx.app_state::<Settings>().theme;
+        let theme = &cx.global::<Settings>().theme;
         ConstrainedBox::new(
             Container::new(element)
                 .with_style(theme.chat_panel.container)

crates/client/src/channel.rs 🔗

@@ -181,7 +181,7 @@ impl Entity for Channel {
 
 impl Channel {
     pub fn init(rpc: &Arc<Client>) {
-        rpc.add_entity_message_handler(Self::handle_message_sent);
+        rpc.add_model_message_handler(Self::handle_message_sent);
     }
 
     pub fn new(

crates/client/src/client.rs 🔗

@@ -13,8 +13,8 @@ use async_tungstenite::tungstenite::{
 };
 use futures::{future::LocalBoxFuture, FutureExt, StreamExt};
 use gpui::{
-    action, AnyModelHandle, AnyWeakModelHandle, AsyncAppContext, Entity, ModelContext, ModelHandle,
-    MutableAppContext, Task,
+    action, AnyModelHandle, AnyViewHandle, AnyWeakModelHandle, AnyWeakViewHandle, AsyncAppContext,
+    Entity, ModelContext, ModelHandle, MutableAppContext, Task, View, ViewContext, ViewHandle,
 };
 use http::HttpClient;
 use lazy_static::lazy_static;
@@ -45,7 +45,7 @@ pub use user::*;
 lazy_static! {
     static ref ZED_SERVER_URL: String =
         std::env::var("ZED_SERVER_URL").unwrap_or("https://zed.dev".to_string());
-    static ref IMPERSONATE_LOGIN: Option<String> = std::env::var("ZED_IMPERSONATE")
+    pub static ref IMPERSONATE_LOGIN: Option<String> = std::env::var("ZED_IMPERSONATE")
         .ok()
         .and_then(|s| if s.is_empty() { None } else { Some(s) });
 }
@@ -55,7 +55,7 @@ action!(Authenticate);
 pub fn init(rpc: Arc<Client>, cx: &mut MutableAppContext) {
     cx.add_global_action(move |_: &Authenticate, cx| {
         let rpc = rpc.clone();
-        cx.spawn(|cx| async move { rpc.authenticate_and_connect(&cx).log_err().await })
+        cx.spawn(|cx| async move { rpc.authenticate_and_connect(true, &cx).log_err().await })
             .detach();
     });
 }
@@ -136,26 +136,37 @@ impl Status {
 struct ClientState {
     credentials: Option<Credentials>,
     status: (watch::Sender<Status>, watch::Receiver<Status>),
-    entity_id_extractors: HashMap<TypeId, Box<dyn Send + Sync + Fn(&dyn AnyTypedEnvelope) -> u64>>,
+    entity_id_extractors: HashMap<TypeId, fn(&dyn AnyTypedEnvelope) -> u64>,
     _reconnect_task: Option<Task<()>>,
     reconnect_interval: Duration,
-    models_by_entity_type_and_remote_id: HashMap<(TypeId, u64), AnyWeakModelHandle>,
+    entities_by_type_and_remote_id: HashMap<(TypeId, u64), AnyWeakEntityHandle>,
     models_by_message_type: HashMap<TypeId, AnyWeakModelHandle>,
-    model_types_by_message_type: HashMap<TypeId, TypeId>,
+    entity_types_by_message_type: HashMap<TypeId, TypeId>,
     message_handlers: HashMap<
         TypeId,
         Arc<
             dyn Send
                 + Sync
                 + Fn(
-                    AnyModelHandle,
+                    AnyEntityHandle,
                     Box<dyn AnyTypedEnvelope>,
+                    &Arc<Client>,
                     AsyncAppContext,
                 ) -> LocalBoxFuture<'static, Result<()>>,
         >,
     >,
 }
 
+enum AnyWeakEntityHandle {
+    Model(AnyWeakModelHandle),
+    View(AnyWeakViewHandle),
+}
+
+enum AnyEntityHandle {
+    Model(AnyModelHandle),
+    View(AnyViewHandle),
+}
+
 #[derive(Clone, Debug)]
 pub struct Credentials {
     pub user_id: u64,
@@ -171,8 +182,8 @@ impl Default for ClientState {
             _reconnect_task: None,
             reconnect_interval: Duration::from_secs(5),
             models_by_message_type: Default::default(),
-            models_by_entity_type_and_remote_id: Default::default(),
-            model_types_by_message_type: Default::default(),
+            entities_by_type_and_remote_id: Default::default(),
+            entity_types_by_message_type: Default::default(),
             message_handlers: Default::default(),
         }
     }
@@ -195,13 +206,13 @@ impl Drop for Subscription {
             Subscription::Entity { client, id } => {
                 if let Some(client) = client.upgrade() {
                     let mut state = client.state.write();
-                    let _ = state.models_by_entity_type_and_remote_id.remove(id);
+                    let _ = state.entities_by_type_and_remote_id.remove(id);
                 }
             }
             Subscription::Message { client, id } => {
                 if let Some(client) = client.upgrade() {
                     let mut state = client.state.write();
-                    let _ = state.model_types_by_message_type.remove(id);
+                    let _ = state.entity_types_by_message_type.remove(id);
                     let _ = state.message_handlers.remove(id);
                 }
             }
@@ -239,7 +250,7 @@ impl Client {
         state._reconnect_task.take();
         state.message_handlers.clear();
         state.models_by_message_type.clear();
-        state.models_by_entity_type_and_remote_id.clear();
+        state.entities_by_type_and_remote_id.clear();
         state.entity_id_extractors.clear();
         self.peer.reset();
     }
@@ -291,7 +302,7 @@ impl Client {
                 state._reconnect_task = Some(cx.spawn(|cx| async move {
                     let mut rng = StdRng::from_entropy();
                     let mut delay = Duration::from_millis(100);
-                    while let Err(error) = this.authenticate_and_connect(&cx).await {
+                    while let Err(error) = this.authenticate_and_connect(true, &cx).await {
                         log::error!("failed to connect {}", error);
                         this.set_status(
                             Status::ReconnectionError {
@@ -313,17 +324,32 @@ impl Client {
         }
     }
 
+    pub fn add_view_for_remote_entity<T: View>(
+        self: &Arc<Self>,
+        remote_id: u64,
+        cx: &mut ViewContext<T>,
+    ) -> Subscription {
+        let id = (TypeId::of::<T>(), remote_id);
+        self.state
+            .write()
+            .entities_by_type_and_remote_id
+            .insert(id, AnyWeakEntityHandle::View(cx.weak_handle().into()));
+        Subscription::Entity {
+            client: Arc::downgrade(self),
+            id,
+        }
+    }
+
     pub fn add_model_for_remote_entity<T: Entity>(
         self: &Arc<Self>,
         remote_id: u64,
         cx: &mut ModelContext<T>,
     ) -> Subscription {
-        let handle = AnyModelHandle::from(cx.handle());
-        let mut state = self.state.write();
         let id = (TypeId::of::<T>(), remote_id);
-        state
-            .models_by_entity_type_and_remote_id
-            .insert(id, handle.downgrade());
+        self.state
+            .write()
+            .entities_by_type_and_remote_id
+            .insert(id, AnyWeakEntityHandle::Model(cx.weak_handle().into()));
         Subscription::Entity {
             client: Arc::downgrade(self),
             id,
@@ -346,7 +372,6 @@ impl Client {
     {
         let message_type_id = TypeId::of::<M>();
 
-        let client = Arc::downgrade(self);
         let mut state = self.state.write();
         state
             .models_by_message_type
@@ -354,14 +379,15 @@ impl Client {
 
         let prev_handler = state.message_handlers.insert(
             message_type_id,
-            Arc::new(move |handle, envelope, cx| {
+            Arc::new(move |handle, envelope, client, cx| {
+                let handle = if let AnyEntityHandle::Model(handle) = handle {
+                    handle
+                } else {
+                    unreachable!();
+                };
                 let model = handle.downcast::<E>().unwrap();
                 let envelope = envelope.into_any().downcast::<TypedEnvelope<M>>().unwrap();
-                if let Some(client) = client.upgrade() {
-                    handler(model, *envelope, client.clone(), cx).boxed_local()
-                } else {
-                    async move { Ok(()) }.boxed_local()
-                }
+                handler(model, *envelope, client.clone(), cx).boxed_local()
             }),
         );
         if prev_handler.is_some() {
@@ -374,7 +400,26 @@ impl Client {
         }
     }
 
-    pub fn add_entity_message_handler<M, E, H, F>(self: &Arc<Self>, handler: H)
+    pub fn add_view_message_handler<M, E, H, F>(self: &Arc<Self>, handler: H)
+    where
+        M: EntityMessage,
+        E: View,
+        H: 'static
+            + Send
+            + Sync
+            + Fn(ViewHandle<E>, TypedEnvelope<M>, Arc<Self>, AsyncAppContext) -> F,
+        F: 'static + Future<Output = Result<()>>,
+    {
+        self.add_entity_message_handler::<M, E, _, _>(move |handle, message, client, cx| {
+            if let AnyEntityHandle::View(handle) = handle {
+                handler(handle.downcast::<E>().unwrap(), message, client, cx)
+            } else {
+                unreachable!();
+            }
+        })
+    }
+
+    pub fn add_model_message_handler<M, E, H, F>(self: &Arc<Self>, handler: H)
     where
         M: EntityMessage,
         E: Entity,
@@ -383,38 +428,51 @@ impl Client {
             + Sync
             + Fn(ModelHandle<E>, TypedEnvelope<M>, Arc<Self>, AsyncAppContext) -> F,
         F: 'static + Future<Output = Result<()>>,
+    {
+        self.add_entity_message_handler::<M, E, _, _>(move |handle, message, client, cx| {
+            if let AnyEntityHandle::Model(handle) = handle {
+                handler(handle.downcast::<E>().unwrap(), message, client, cx)
+            } else {
+                unreachable!();
+            }
+        })
+    }
+
+    fn add_entity_message_handler<M, E, H, F>(self: &Arc<Self>, handler: H)
+    where
+        M: EntityMessage,
+        E: Entity,
+        H: 'static
+            + Send
+            + Sync
+            + Fn(AnyEntityHandle, TypedEnvelope<M>, Arc<Self>, AsyncAppContext) -> F,
+        F: 'static + Future<Output = Result<()>>,
     {
         let model_type_id = TypeId::of::<E>();
         let message_type_id = TypeId::of::<M>();
 
-        let client = Arc::downgrade(self);
         let mut state = self.state.write();
         state
-            .model_types_by_message_type
+            .entity_types_by_message_type
             .insert(message_type_id, model_type_id);
         state
             .entity_id_extractors
             .entry(message_type_id)
             .or_insert_with(|| {
-                Box::new(|envelope| {
-                    let envelope = envelope
+                |envelope| {
+                    envelope
                         .as_any()
                         .downcast_ref::<TypedEnvelope<M>>()
-                        .unwrap();
-                    envelope.payload.remote_entity_id()
-                })
+                        .unwrap()
+                        .payload
+                        .remote_entity_id()
+                }
             });
-
         let prev_handler = state.message_handlers.insert(
             message_type_id,
-            Arc::new(move |handle, envelope, cx| {
-                let model = handle.downcast::<E>().unwrap();
+            Arc::new(move |handle, envelope, client, cx| {
                 let envelope = envelope.into_any().downcast::<TypedEnvelope<M>>().unwrap();
-                if let Some(client) = client.upgrade() {
-                    handler(model, *envelope, client.clone(), cx).boxed_local()
-                } else {
-                    async move { Ok(()) }.boxed_local()
-                }
+                handler(handle, *envelope, client.clone(), cx).boxed_local()
             }),
         );
         if prev_handler.is_some() {
@@ -422,7 +480,7 @@ impl Client {
         }
     }
 
-    pub fn add_entity_request_handler<M, E, H, F>(self: &Arc<Self>, handler: H)
+    pub fn add_model_request_handler<M, E, H, F>(self: &Arc<Self>, handler: H)
     where
         M: EntityMessage + RequestMessage,
         E: Entity,
@@ -432,29 +490,56 @@ impl Client {
             + Fn(ModelHandle<E>, TypedEnvelope<M>, Arc<Self>, AsyncAppContext) -> F,
         F: 'static + Future<Output = Result<M::Response>>,
     {
-        self.add_entity_message_handler(move |model, envelope, client, cx| {
-            let receipt = envelope.receipt();
-            let response = handler(model, envelope, client.clone(), cx);
-            async move {
-                match response.await {
-                    Ok(response) => {
-                        client.respond(receipt, response)?;
-                        Ok(())
-                    }
-                    Err(error) => {
-                        client.respond_with_error(
-                            receipt,
-                            proto::Error {
-                                message: error.to_string(),
-                            },
-                        )?;
-                        Err(error)
-                    }
-                }
-            }
+        self.add_model_message_handler(move |entity, envelope, client, cx| {
+            Self::respond_to_request::<M, _>(
+                envelope.receipt(),
+                handler(entity, envelope, client.clone(), cx),
+                client,
+            )
+        })
+    }
+
+    pub fn add_view_request_handler<M, E, H, F>(self: &Arc<Self>, handler: H)
+    where
+        M: EntityMessage + RequestMessage,
+        E: View,
+        H: 'static
+            + Send
+            + Sync
+            + Fn(ViewHandle<E>, TypedEnvelope<M>, Arc<Self>, AsyncAppContext) -> F,
+        F: 'static + Future<Output = Result<M::Response>>,
+    {
+        self.add_view_message_handler(move |entity, envelope, client, cx| {
+            Self::respond_to_request::<M, _>(
+                envelope.receipt(),
+                handler(entity, envelope, client.clone(), cx),
+                client,
+            )
         })
     }
 
+    async fn respond_to_request<T: RequestMessage, F: Future<Output = Result<T::Response>>>(
+        receipt: Receipt<T>,
+        response: F,
+        client: Arc<Self>,
+    ) -> Result<()> {
+        match response.await {
+            Ok(response) => {
+                client.respond(receipt, response)?;
+                Ok(())
+            }
+            Err(error) => {
+                client.respond_with_error(
+                    receipt,
+                    proto::Error {
+                        message: error.to_string(),
+                    },
+                )?;
+                Err(error)
+            }
+        }
+    }
+
     pub fn has_keychain_credentials(&self, cx: &AsyncAppContext) -> bool {
         read_credentials_from_keychain(cx).is_some()
     }
@@ -462,6 +547,7 @@ impl Client {
     #[async_recursion(?Send)]
     pub async fn authenticate_and_connect(
         self: &Arc<Self>,
+        try_keychain: bool,
         cx: &AsyncAppContext,
     ) -> anyhow::Result<()> {
         let was_disconnected = match *self.status().borrow() {
@@ -483,23 +569,22 @@ impl Client {
             self.set_status(Status::Reauthenticating, cx)
         }
 
-        let mut used_keychain = false;
-        let credentials = self.state.read().credentials.clone();
-        let credentials = if let Some(credentials) = credentials {
-            credentials
-        } else if let Some(credentials) = read_credentials_from_keychain(cx) {
-            used_keychain = true;
-            credentials
-        } else {
-            let credentials = match self.authenticate(&cx).await {
+        let mut read_from_keychain = false;
+        let mut credentials = self.state.read().credentials.clone();
+        if credentials.is_none() && try_keychain {
+            credentials = read_credentials_from_keychain(cx);
+            read_from_keychain = credentials.is_some();
+        }
+        if credentials.is_none() {
+            credentials = Some(match self.authenticate(&cx).await {
                 Ok(credentials) => credentials,
                 Err(err) => {
                     self.set_status(Status::ConnectionError, cx);
                     return Err(err);
                 }
-            };
-            credentials
-        };
+            });
+        }
+        let credentials = credentials.unwrap();
 
         if was_disconnected {
             self.set_status(Status::Connecting, cx);
@@ -510,7 +595,7 @@ impl Client {
         match self.establish_connection(&credentials, cx).await {
             Ok(conn) => {
                 self.state.write().credentials = Some(credentials.clone());
-                if !used_keychain && IMPERSONATE_LOGIN.is_none() {
+                if !read_from_keychain && IMPERSONATE_LOGIN.is_none() {
                     write_credentials_to_keychain(&credentials, cx).log_err();
                 }
                 self.set_connection(conn, cx).await;
@@ -518,10 +603,10 @@ impl Client {
             }
             Err(EstablishConnectionError::Unauthorized) => {
                 self.state.write().credentials.take();
-                if used_keychain {
+                if read_from_keychain {
                     cx.platform().delete_credentials(&ZED_SERVER_URL).log_err();
                     self.set_status(Status::SignedOut, cx);
-                    self.authenticate_and_connect(cx).await
+                    self.authenticate_and_connect(false, cx).await
                 } else {
                     self.set_status(Status::ConnectionError, cx);
                     Err(EstablishConnectionError::Unauthorized)?
@@ -561,24 +646,26 @@ impl Client {
                             .models_by_message_type
                             .get(&payload_type_id)
                             .and_then(|model| model.upgrade(&cx))
+                            .map(AnyEntityHandle::Model)
                             .or_else(|| {
-                                let model_type_id =
-                                    *state.model_types_by_message_type.get(&payload_type_id)?;
+                                let entity_type_id =
+                                    *state.entity_types_by_message_type.get(&payload_type_id)?;
                                 let entity_id = state
                                     .entity_id_extractors
                                     .get(&message.payload_type_id())
                                     .map(|extract_entity_id| {
                                         (extract_entity_id)(message.as_ref())
                                     })?;
-                                let model = state
-                                    .models_by_entity_type_and_remote_id
-                                    .get(&(model_type_id, entity_id))?;
-                                if let Some(model) = model.upgrade(&cx) {
-                                    Some(model)
+
+                                let entity = state
+                                    .entities_by_type_and_remote_id
+                                    .get(&(entity_type_id, entity_id))?;
+                                if let Some(entity) = entity.upgrade(&cx) {
+                                    Some(entity)
                                 } else {
                                     state
-                                        .models_by_entity_type_and_remote_id
-                                        .remove(&(model_type_id, entity_id));
+                                        .entities_by_type_and_remote_id
+                                        .remove(&(entity_type_id, entity_id));
                                     None
                                 }
                             });
@@ -593,7 +680,7 @@ impl Client {
                         if let Some(handler) = state.message_handlers.get(&payload_type_id).cloned()
                         {
                             drop(state); // Avoid deadlocks if the handler interacts with rpc::Client
-                            let future = handler(model, message, cx.clone());
+                            let future = handler(model, message, &this, cx.clone());
 
                             let client_id = this.id;
                             log::debug!(
@@ -891,6 +978,15 @@ impl Client {
     }
 }
 
+impl AnyWeakEntityHandle {
+    fn upgrade(&self, cx: &AsyncAppContext) -> Option<AnyEntityHandle> {
+        match self {
+            AnyWeakEntityHandle::Model(handle) => handle.upgrade(cx).map(AnyEntityHandle::Model),
+            AnyWeakEntityHandle::View(handle) => handle.upgrade(cx).map(AnyEntityHandle::View),
+        }
+    }
+}
+
 fn read_credentials_from_keychain(cx: &AsyncAppContext) -> Option<Credentials> {
     if IMPERSONATE_LOGIN.is_some() {
         return None;
@@ -994,7 +1090,7 @@ mod tests {
 
         let (done_tx1, mut done_rx1) = smol::channel::unbounded();
         let (done_tx2, mut done_rx2) = smol::channel::unbounded();
-        client.add_entity_message_handler(
+        client.add_model_message_handler(
             move |model: ModelHandle<Model>, _: TypedEnvelope<proto::UnshareProject>, _, cx| {
                 match model.read_with(&cx, |model, _| model.id) {
                     1 => done_tx1.try_send(()).unwrap(),

crates/client/src/test.rs 🔗

@@ -91,7 +91,7 @@ impl FakeServer {
             });
 
         client
-            .authenticate_and_connect(&cx.to_async())
+            .authenticate_and_connect(false, &cx.to_async())
             .await
             .unwrap();
         server

crates/contacts_panel/src/contacts_panel.rs 🔗

@@ -55,7 +55,7 @@ impl ContactsPanel {
         app_state: Arc<AppState>,
         cx: &mut LayoutContext,
     ) -> ElementBox {
-        let theme = cx.app_state::<Settings>().theme.clone();
+        let theme = cx.global::<Settings>().theme.clone();
         let theme = &theme.contacts_panel;
         let project_count = collaborator.projects.len();
         let font_cache = cx.font_cache();
@@ -236,7 +236,7 @@ impl View for ContactsPanel {
     }
 
     fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
-        let theme = &cx.app_state::<Settings>().theme.contacts_panel;
+        let theme = &cx.global::<Settings>().theme.contacts_panel;
         Container::new(List::new(self.contacts.clone()).boxed())
             .with_style(theme.container)
             .boxed()

crates/diagnostics/src/diagnostics.rs 🔗

@@ -25,7 +25,7 @@ use std::{
     sync::Arc,
 };
 use util::TryFutureExt;
-use workspace::{ItemHandle, ItemNavHistory, ItemViewHandle as _, Settings, Workspace};
+use workspace::{ItemHandle as _, ItemNavHistory, Settings, Workspace};
 
 action!(Deploy);
 
@@ -38,12 +38,8 @@ pub fn init(cx: &mut MutableAppContext) {
 
 type Event = editor::Event;
 
-struct ProjectDiagnostics {
-    project: ModelHandle<Project>,
-}
-
 struct ProjectDiagnosticsEditor {
-    model: ModelHandle<ProjectDiagnostics>,
+    project: ModelHandle<Project>,
     workspace: WeakViewHandle<Workspace>,
     editor: ViewHandle<Editor>,
     summary: DiagnosticSummary,
@@ -65,16 +61,6 @@ struct DiagnosticGroupState {
     block_count: usize,
 }
 
-impl ProjectDiagnostics {
-    fn new(project: ModelHandle<Project>) -> Self {
-        Self { project }
-    }
-}
-
-impl Entity for ProjectDiagnostics {
-    type Event = ();
-}
-
 impl Entity for ProjectDiagnosticsEditor {
     type Event = Event;
 }
@@ -86,7 +72,7 @@ impl View for ProjectDiagnosticsEditor {
 
     fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
         if self.path_states.is_empty() {
-            let theme = &cx.app_state::<Settings>().theme.project_diagnostics;
+            let theme = &cx.global::<Settings>().theme.project_diagnostics;
             Label::new(
                 "No problems in workspace".to_string(),
                 theme.empty_message.clone(),
@@ -109,12 +95,11 @@ impl View for ProjectDiagnosticsEditor {
 
 impl ProjectDiagnosticsEditor {
     fn new(
-        model: ModelHandle<ProjectDiagnostics>,
+        project_handle: ModelHandle<Project>,
         workspace: WeakViewHandle<Workspace>,
         cx: &mut ViewContext<Self>,
     ) -> Self {
-        let project = model.read(cx).project.clone();
-        cx.subscribe(&project, |this, _, event, cx| match event {
+        cx.subscribe(&project_handle, |this, _, event, cx| match event {
             project::Event::DiskBasedDiagnosticsFinished => {
                 this.update_excerpts(cx);
                 this.update_title(cx);
@@ -126,20 +111,22 @@ impl ProjectDiagnosticsEditor {
         })
         .detach();
 
-        let excerpts = cx.add_model(|cx| MultiBuffer::new(project.read(cx).replica_id()));
+        let excerpts = cx.add_model(|cx| MultiBuffer::new(project_handle.read(cx).replica_id()));
         let editor = cx.add_view(|cx| {
-            let mut editor = Editor::for_buffer(excerpts.clone(), Some(project.clone()), cx);
+            let mut editor =
+                Editor::for_multibuffer(excerpts.clone(), Some(project_handle.clone()), cx);
             editor.set_vertical_scroll_margin(5, cx);
             editor
         });
         cx.subscribe(&editor, |_, _, event, cx| cx.emit(*event))
             .detach();
 
-        let project = project.read(cx);
+        let project = project_handle.read(cx);
         let paths_to_update = project.diagnostic_summaries(cx).map(|e| e.0).collect();
+        let summary = project.diagnostic_summary(cx);
         let mut this = Self {
-            model,
-            summary: project.diagnostic_summary(cx),
+            project: project_handle,
+            summary,
             workspace,
             excerpts,
             editor,
@@ -151,18 +138,20 @@ impl ProjectDiagnosticsEditor {
     }
 
     fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext<Workspace>) {
-        if let Some(existing) = workspace.item_of_type::<ProjectDiagnostics>(cx) {
+        if let Some(existing) = workspace.item_of_type::<ProjectDiagnosticsEditor>(cx) {
             workspace.activate_item(&existing, cx);
         } else {
-            let diagnostics =
-                cx.add_model(|_| ProjectDiagnostics::new(workspace.project().clone()));
-            workspace.open_item(diagnostics, cx);
+            let workspace_handle = cx.weak_handle();
+            let diagnostics = cx.add_view(|cx| {
+                ProjectDiagnosticsEditor::new(workspace.project().clone(), workspace_handle, cx)
+            });
+            workspace.add_item(Box::new(diagnostics), cx);
         }
     }
 
     fn update_excerpts(&mut self, cx: &mut ViewContext<Self>) {
         let paths = mem::take(&mut self.paths_to_update);
-        let project = self.model.read(cx).project.clone();
+        let project = self.project.clone();
         cx.spawn(|this, mut cx| {
             async move {
                 for path in paths {
@@ -289,7 +278,7 @@ impl ProjectDiagnosticsEditor {
                             prev_excerpt_id = excerpt_id.clone();
                             first_excerpt_id.get_or_insert_with(|| prev_excerpt_id.clone());
                             group_state.excerpts.push(excerpt_id.clone());
-                            let header_position = (excerpt_id.clone(), language::Anchor::min());
+                            let header_position = (excerpt_id.clone(), language::Anchor::MIN);
 
                             if is_first_excerpt_for_group {
                                 is_first_excerpt_for_group = false;
@@ -378,8 +367,7 @@ impl ProjectDiagnosticsEditor {
             range_a
                 .start
                 .cmp(&range_b.start, &snapshot)
-                .unwrap()
-                .then_with(|| range_a.end.cmp(&range_b.end, &snapshot).unwrap())
+                .then_with(|| range_a.end.cmp(&range_b.end, &snapshot))
         });
 
         if path_state.diagnostic_groups.is_empty() {
@@ -443,42 +431,17 @@ impl ProjectDiagnosticsEditor {
     }
 
     fn update_title(&mut self, cx: &mut ViewContext<Self>) {
-        self.summary = self.model.read(cx).project.read(cx).diagnostic_summary(cx);
+        self.summary = self.project.read(cx).diagnostic_summary(cx);
         cx.emit(Event::TitleChanged);
     }
 }
 
-impl workspace::Item for ProjectDiagnostics {
-    type View = ProjectDiagnosticsEditor;
-
-    fn build_view(
-        handle: ModelHandle<Self>,
-        workspace: &Workspace,
-        nav_history: ItemNavHistory,
-        cx: &mut ViewContext<Self::View>,
-    ) -> Self::View {
-        let diagnostics = ProjectDiagnosticsEditor::new(handle, workspace.weak_handle(), cx);
-        diagnostics
-            .editor
-            .update(cx, |editor, _| editor.set_nav_history(Some(nav_history)));
-        diagnostics
-    }
-
-    fn project_path(&self) -> Option<project::ProjectPath> {
-        None
-    }
-}
-
-impl workspace::ItemView for ProjectDiagnosticsEditor {
-    fn item(&self, _: &AppContext) -> Box<dyn ItemHandle> {
-        Box::new(self.model.clone())
-    }
-
+impl workspace::Item for ProjectDiagnosticsEditor {
     fn tab_content(&self, style: &theme::Tab, cx: &AppContext) -> ElementBox {
         render_summary(
             &self.summary,
             &style.label.text,
-            &cx.app_state::<Settings>().theme.project_diagnostics,
+            &cx.global::<Settings>().theme.project_diagnostics,
         )
     }
 
@@ -486,9 +449,13 @@ impl workspace::ItemView for ProjectDiagnosticsEditor {
         None
     }
 
-    fn navigate(&mut self, data: Box<dyn Any>, cx: &mut ViewContext<Self>) {
+    fn project_entry_id(&self, _: &AppContext) -> Option<project::ProjectEntryId> {
+        None
+    }
+
+    fn navigate(&mut self, data: Box<dyn Any>, cx: &mut ViewContext<Self>) -> bool {
         self.editor
-            .update(cx, |editor, cx| editor.navigate(data, cx));
+            .update(cx, |editor, cx| editor.navigate(data, cx))
     }
 
     fn is_dirty(&self, cx: &AppContext) -> bool {
@@ -532,20 +499,21 @@ impl workspace::ItemView for ProjectDiagnosticsEditor {
         matches!(event, Event::Saved | Event::Dirtied | Event::TitleChanged)
     }
 
-    fn clone_on_split(
-        &self,
-        nav_history: ItemNavHistory,
-        cx: &mut ViewContext<Self>,
-    ) -> Option<Self>
+    fn set_nav_history(&mut self, nav_history: ItemNavHistory, cx: &mut ViewContext<Self>) {
+        self.editor.update(cx, |editor, _| {
+            editor.set_nav_history(Some(nav_history));
+        });
+    }
+
+    fn clone_on_split(&self, cx: &mut ViewContext<Self>) -> Option<Self>
     where
         Self: Sized,
     {
-        let diagnostics =
-            ProjectDiagnosticsEditor::new(self.model.clone(), self.workspace.clone(), cx);
-        diagnostics.editor.update(cx, |editor, _| {
-            editor.set_nav_history(Some(nav_history));
-        });
-        Some(diagnostics)
+        Some(ProjectDiagnosticsEditor::new(
+            self.project.clone(),
+            self.workspace.clone(),
+            cx,
+        ))
     }
 
     fn act_as_type(
@@ -571,7 +539,7 @@ impl workspace::ItemView for ProjectDiagnosticsEditor {
 fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock {
     let (message, highlights) = highlight_diagnostic_message(&diagnostic.message);
     Arc::new(move |cx| {
-        let settings = cx.app_state::<Settings>();
+        let settings = cx.global::<Settings>();
         let theme = &settings.theme.editor;
         let style = &theme.diagnostic_header;
         let font_size = (style.text_scale_factor * settings.buffer_font_size).round();
@@ -829,9 +797,8 @@ mod tests {
         });
 
         // Open the project diagnostics view while there are already diagnostics.
-        let model = cx.add_model(|_| ProjectDiagnostics::new(project.clone()));
         let view = cx.add_view(0, |cx| {
-            ProjectDiagnosticsEditor::new(model, workspace.downgrade(), cx)
+            ProjectDiagnosticsEditor::new(project.clone(), workspace.downgrade(), cx)
         });
 
         view.next_notification(&cx).await;

crates/diagnostics/src/items.rs 🔗

@@ -49,7 +49,7 @@ impl View for DiagnosticSummary {
 
         let in_progress = self.in_progress;
         MouseEventHandler::new::<Tag, _, _>(0, cx, |_, cx| {
-            let theme = &cx.app_state::<Settings>().theme.project_diagnostics;
+            let theme = &cx.global::<Settings>().theme.project_diagnostics;
             if in_progress {
                 Label::new(
                     "Checking... ".to_string(),
@@ -71,7 +71,7 @@ impl View for DiagnosticSummary {
 impl StatusItemView for DiagnosticSummary {
     fn set_active_pane_item(
         &mut self,
-        _: Option<&dyn workspace::ItemViewHandle>,
+        _: Option<&dyn workspace::ItemHandle>,
         _: &mut ViewContext<Self>,
     ) {
     }

crates/editor/Cargo.toml 🔗

@@ -27,6 +27,7 @@ gpui = { path = "../gpui" }
 language = { path = "../language" }
 lsp = { path = "../lsp" }
 project = { path = "../project" }
+rpc = { path = "../rpc" }
 snippet = { path = "../snippet" }
 sum_tree = { path = "../sum_tree" }
 theme = { path = "../theme" }
@@ -34,6 +35,7 @@ util = { path = "../util" }
 workspace = { path = "../workspace" }
 aho-corasick = "0.7"
 anyhow = "1.0"
+futures = "0.3"
 itertools = "0.10"
 lazy_static = "1.4"
 log = "0.4"

crates/editor/src/display_map.rs 🔗

@@ -12,7 +12,7 @@ use gpui::{
     Entity, ModelContext, ModelHandle,
 };
 use language::{Point, Subscription as BufferSubscription};
-use std::{any::TypeId, ops::Range, sync::Arc};
+use std::{any::TypeId, fmt::Debug, ops::Range, sync::Arc};
 use sum_tree::{Bias, TreeMap};
 use tab_map::TabMap;
 use wrap_map::WrapMap;
@@ -36,6 +36,7 @@ pub struct DisplayMap {
     wrap_map: ModelHandle<WrapMap>,
     block_map: BlockMap,
     text_highlights: TextHighlights,
+    pub clip_at_line_ends: bool,
 }
 
 impl Entity for DisplayMap {
@@ -67,6 +68,7 @@ impl DisplayMap {
             wrap_map,
             block_map,
             text_highlights: Default::default(),
+            clip_at_line_ends: false,
         }
     }
 
@@ -87,6 +89,7 @@ impl DisplayMap {
             wraps_snapshot,
             blocks_snapshot,
             text_highlights: self.text_highlights.clone(),
+            clip_at_line_ends: self.clip_at_line_ends,
         }
     }
 
@@ -114,6 +117,7 @@ impl DisplayMap {
     pub fn unfold<T: ToOffset>(
         &mut self,
         ranges: impl IntoIterator<Item = Range<T>>,
+        inclusive: bool,
         cx: &mut ModelContext<Self>,
     ) {
         let snapshot = self.buffer.read(cx).snapshot(cx);
@@ -124,7 +128,7 @@ impl DisplayMap {
             .wrap_map
             .update(cx, |map, cx| map.sync(snapshot, edits, cx));
         self.block_map.read(snapshot, edits);
-        let (snapshot, edits) = fold_map.unfold(ranges);
+        let (snapshot, edits) = fold_map.unfold(ranges, inclusive);
         let (snapshot, edits) = self.tab_map.sync(snapshot, edits);
         let (snapshot, edits) = self
             .wrap_map
@@ -204,6 +208,7 @@ pub struct DisplaySnapshot {
     wraps_snapshot: wrap_map::WrapSnapshot,
     blocks_snapshot: block_map::BlockSnapshot,
     text_highlights: TextHighlights,
+    clip_at_line_ends: bool,
 }
 
 impl DisplaySnapshot {
@@ -331,7 +336,12 @@ impl DisplaySnapshot {
     }
 
     pub fn clip_point(&self, point: DisplayPoint, bias: Bias) -> DisplayPoint {
-        DisplayPoint(self.blocks_snapshot.clip_point(point.0, bias))
+        let mut clipped = self.blocks_snapshot.clip_point(point.0, bias);
+        if self.clip_at_line_ends && clipped.column == self.line_len(clipped.row) {
+            clipped.column = clipped.column.saturating_sub(1);
+            clipped = self.blocks_snapshot.clip_point(clipped, Bias::Left);
+        }
+        DisplayPoint(clipped)
     }
 
     pub fn folds_in_range<'a, T>(
@@ -414,9 +424,19 @@ impl DisplaySnapshot {
     }
 }
 
-#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
+#[derive(Copy, Clone, Default, Eq, Ord, PartialOrd, PartialEq)]
 pub struct DisplayPoint(BlockPoint);
 
+impl Debug for DisplayPoint {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.write_fmt(format_args!(
+            "DisplayPoint({}, {})",
+            self.row(),
+            self.column()
+        ))
+    }
+}
+
 impl DisplayPoint {
     pub fn new(row: u32, column: u32) -> Self {
         Self(BlockPoint(Point::new(row, column)))
@@ -426,7 +446,6 @@ impl DisplayPoint {
         Self::new(0, 0)
     }
 
-    #[cfg(test)]
     pub fn is_zero(&self) -> bool {
         self.0.is_zero()
     }
@@ -478,16 +497,16 @@ impl ToDisplayPoint for Anchor {
 }
 
 #[cfg(test)]
-mod tests {
+pub mod tests {
     use super::*;
-    use crate::movement;
+    use crate::{movement, test::marked_display_snapshot};
     use gpui::{color::Color, elements::*, test::observe, MutableAppContext};
     use language::{Buffer, Language, LanguageConfig, RandomCharIter, SelectionGoal};
     use rand::{prelude::*, Rng};
     use smol::stream::StreamExt;
     use std::{env, sync::Arc};
     use theme::SyntaxTheme;
-    use util::test::sample_text;
+    use util::test::{marked_text_ranges, sample_text};
     use Bias::*;
 
     #[gpui::test(iterations = 100)]
@@ -620,7 +639,7 @@ mod tests {
                     if rng.gen() && fold_count > 0 {
                         log::info!("unfolding ranges: {:?}", ranges);
                         map.update(cx, |map, cx| {
-                            map.unfold(ranges, cx);
+                            map.unfold(ranges, true, cx);
                         });
                     } else {
                         log::info!("folding ranges: {:?}", ranges);
@@ -705,7 +724,7 @@ mod tests {
 
                 log::info!("Moving from point {:?}", point);
 
-                let moved_right = movement::right(&snapshot, point).unwrap();
+                let moved_right = movement::right(&snapshot, point);
                 log::info!("Right {:?}", moved_right);
                 if point < max_point {
                     assert!(moved_right > point);
@@ -719,7 +738,7 @@ mod tests {
                     assert_eq!(moved_right, point);
                 }
 
-                let moved_left = movement::left(&snapshot, point).unwrap();
+                let moved_left = movement::left(&snapshot, point);
                 log::info!("Left {:?}", moved_left);
                 if point > min_point {
                     assert!(moved_left < point);
@@ -777,15 +796,15 @@ mod tests {
             DisplayPoint::new(1, 0)
         );
         assert_eq!(
-            movement::right(&snapshot, DisplayPoint::new(0, 7)).unwrap(),
+            movement::right(&snapshot, DisplayPoint::new(0, 7)),
             DisplayPoint::new(1, 0)
         );
         assert_eq!(
-            movement::left(&snapshot, DisplayPoint::new(1, 0)).unwrap(),
+            movement::left(&snapshot, DisplayPoint::new(1, 0)),
             DisplayPoint::new(0, 7)
         );
         assert_eq!(
-            movement::up(&snapshot, DisplayPoint::new(1, 10), SelectionGoal::None).unwrap(),
+            movement::up(&snapshot, DisplayPoint::new(1, 10), SelectionGoal::None),
             (DisplayPoint::new(0, 7), SelectionGoal::Column(10))
         );
         assert_eq!(
@@ -793,8 +812,7 @@ mod tests {
                 &snapshot,
                 DisplayPoint::new(0, 7),
                 SelectionGoal::Column(10)
-            )
-            .unwrap(),
+            ),
             (DisplayPoint::new(1, 10), SelectionGoal::Column(10))
         );
         assert_eq!(
@@ -802,8 +820,7 @@ mod tests {
                 &snapshot,
                 DisplayPoint::new(1, 10),
                 SelectionGoal::Column(10)
-            )
-            .unwrap(),
+            ),
             (DisplayPoint::new(2, 4), SelectionGoal::Column(10))
         );
 
@@ -922,7 +939,7 @@ mod tests {
         let map = cx
             .add_model(|cx| DisplayMap::new(buffer, tab_size, font_id, font_size, None, 1, 1, cx));
         assert_eq!(
-            cx.update(|cx| chunks(0..5, &map, &theme, cx)),
+            cx.update(|cx| syntax_chunks(0..5, &map, &theme, cx)),
             vec![
                 ("fn ".to_string(), None),
                 ("outer".to_string(), Some(Color::blue())),
@@ -933,7 +950,7 @@ mod tests {
             ]
         );
         assert_eq!(
-            cx.update(|cx| chunks(3..5, &map, &theme, cx)),
+            cx.update(|cx| syntax_chunks(3..5, &map, &theme, cx)),
             vec![
                 ("    fn ".to_string(), Some(Color::red())),
                 ("inner".to_string(), Some(Color::blue())),
@@ -945,7 +962,7 @@ mod tests {
             map.fold(vec![Point::new(0, 6)..Point::new(3, 2)], cx)
         });
         assert_eq!(
-            cx.update(|cx| chunks(0..2, &map, &theme, cx)),
+            cx.update(|cx| syntax_chunks(0..2, &map, &theme, cx)),
             vec![
                 ("fn ".to_string(), None),
                 ("out".to_string(), Some(Color::blue())),
@@ -1011,7 +1028,7 @@ mod tests {
             DisplayMap::new(buffer, tab_size, font_id, font_size, Some(40.0), 1, 1, cx)
         });
         assert_eq!(
-            cx.update(|cx| chunks(0..5, &map, &theme, cx)),
+            cx.update(|cx| syntax_chunks(0..5, &map, &theme, cx)),
             [
                 ("fn \n".to_string(), None),
                 ("oute\nr".to_string(), Some(Color::blue())),
@@ -1019,7 +1036,7 @@ mod tests {
             ]
         );
         assert_eq!(
-            cx.update(|cx| chunks(3..5, &map, &theme, cx)),
+            cx.update(|cx| syntax_chunks(3..5, &map, &theme, cx)),
             [("{}\n\n".to_string(), None)]
         );
 
@@ -1027,7 +1044,7 @@ mod tests {
             map.fold(vec![Point::new(0, 6)..Point::new(3, 2)], cx)
         });
         assert_eq!(
-            cx.update(|cx| chunks(1..4, &map, &theme, cx)),
+            cx.update(|cx| syntax_chunks(1..4, &map, &theme, cx)),
             [
                 ("out".to_string(), Some(Color::blue())),
                 ("…\n".to_string(), None),
@@ -1038,50 +1055,151 @@ mod tests {
     }
 
     #[gpui::test]
-    fn test_clip_point(cx: &mut gpui::MutableAppContext) {
-        use Bias::{Left, Right};
+    async fn test_chunks_with_text_highlights(cx: &mut gpui::TestAppContext) {
+        cx.foreground().set_block_on_ticks(usize::MAX..=usize::MAX);
 
-        let text = "\n'a', 'α',\t'✋',\t'❎', '🍐'\n";
-        let display_text = "\n'a', 'α',   '✋',    '❎', '🍐'\n";
-        let buffer = MultiBuffer::build_simple(text, cx);
+        let theme = SyntaxTheme::new(vec![
+            ("operator".to_string(), Color::red().into()),
+            ("string".to_string(), Color::green().into()),
+        ]);
+        let language = Arc::new(
+            Language::new(
+                LanguageConfig {
+                    name: "Test".into(),
+                    path_suffixes: vec![".test".to_string()],
+                    ..Default::default()
+                },
+                Some(tree_sitter_rust::language()),
+            )
+            .with_highlights_query(
+                r#"
+                ":" @operator
+                (string_literal) @string
+                "#,
+            )
+            .unwrap(),
+        );
+        language.set_theme(&theme);
+
+        let (text, highlighted_ranges) = marked_text_ranges(r#"const[] [a]: B = "c [d]""#);
+
+        let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx));
+        buffer.condition(&cx, |buf, _| !buf.is_parsing()).await;
+
+        let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
+        let buffer_snapshot = buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx));
 
-        let tab_size = 4;
         let font_cache = cx.font_cache();
-        let family_id = font_cache.load_family(&["Helvetica"]).unwrap();
+        let tab_size = 4;
+        let family_id = font_cache.load_family(&["Courier"]).unwrap();
         let font_id = font_cache
             .select_font(family_id, &Default::default())
             .unwrap();
-        let font_size = 14.0;
-        let map = cx.add_model(|cx| {
-            DisplayMap::new(buffer.clone(), tab_size, font_id, font_size, None, 1, 1, cx)
+        let font_size = 16.0;
+        let map = cx
+            .add_model(|cx| DisplayMap::new(buffer, tab_size, font_id, font_size, None, 1, 1, cx));
+
+        enum MyType {}
+
+        let style = HighlightStyle {
+            color: Some(Color::blue()),
+            ..Default::default()
+        };
+
+        map.update(cx, |map, _cx| {
+            map.highlight_text(
+                TypeId::of::<MyType>(),
+                highlighted_ranges
+                    .into_iter()
+                    .map(|range| {
+                        buffer_snapshot.anchor_before(range.start)
+                            ..buffer_snapshot.anchor_before(range.end)
+                    })
+                    .collect(),
+                style,
+            );
         });
-        let map = map.update(cx, |map, cx| map.snapshot(cx));
 
-        assert_eq!(map.text(), display_text);
-        for (input_column, bias, output_column) in vec![
-            ("'a', '".len(), Left, "'a', '".len()),
-            ("'a', '".len() + 1, Left, "'a', '".len()),
-            ("'a', '".len() + 1, Right, "'a', 'α".len()),
-            ("'a', 'α', ".len(), Left, "'a', 'α',".len()),
-            ("'a', 'α', ".len(), Right, "'a', 'α',   ".len()),
-            ("'a', 'α',   '".len() + 1, Left, "'a', 'α',   '".len()),
-            ("'a', 'α',   '".len() + 1, Right, "'a', 'α',   '✋".len()),
-            ("'a', 'α',   '✋',".len(), Right, "'a', 'α',   '✋',".len()),
-            ("'a', 'α',   '✋', ".len(), Left, "'a', 'α',   '✋',".len()),
-            (
-                "'a', 'α',   '✋', ".len(),
-                Right,
-                "'a', 'α',   '✋',    ".len(),
-            ),
-        ] {
+        assert_eq!(
+            cx.update(|cx| chunks(0..10, &map, &theme, cx)),
+            [
+                ("const ".to_string(), None, None),
+                ("a".to_string(), None, Some(Color::blue())),
+                (":".to_string(), Some(Color::red()), None),
+                (" B = ".to_string(), None, None),
+                ("\"c ".to_string(), Some(Color::green()), None),
+                ("d".to_string(), Some(Color::green()), Some(Color::blue())),
+                ("\"".to_string(), Some(Color::green()), None),
+            ]
+        );
+    }
+
+    #[gpui::test]
+    fn test_clip_point(cx: &mut gpui::MutableAppContext) {
+        fn assert(text: &str, shift_right: bool, bias: Bias, cx: &mut gpui::MutableAppContext) {
+            let (unmarked_snapshot, mut markers) = marked_display_snapshot(text, cx);
+
+            match bias {
+                Bias::Left => {
+                    if shift_right {
+                        *markers[1].column_mut() += 1;
+                    }
+
+                    assert_eq!(unmarked_snapshot.clip_point(markers[1], bias), markers[0])
+                }
+                Bias::Right => {
+                    if shift_right {
+                        *markers[0].column_mut() += 1;
+                    }
+
+                    assert_eq!(
+                        unmarked_snapshot.clip_point(dbg!(markers[0]), bias),
+                        markers[1]
+                    )
+                }
+            };
+        }
+
+        use Bias::{Left, Right};
+        assert("||α", false, Left, cx);
+        assert("||α", true, Left, cx);
+        assert("||α", false, Right, cx);
+        assert("|α|", true, Right, cx);
+        assert("||✋", false, Left, cx);
+        assert("||✋", true, Left, cx);
+        assert("||✋", false, Right, cx);
+        assert("|✋|", true, Right, cx);
+        assert("||🍐", false, Left, cx);
+        assert("||🍐", true, Left, cx);
+        assert("||🍐", false, Right, cx);
+        assert("|🍐|", true, Right, cx);
+        assert("||\t", false, Left, cx);
+        assert("||\t", true, Left, cx);
+        assert("||\t", false, Right, cx);
+        assert("|\t|", true, Right, cx);
+        assert(" ||\t", false, Left, cx);
+        assert(" ||\t", true, Left, cx);
+        assert(" ||\t", false, Right, cx);
+        assert(" |\t|", true, Right, cx);
+        assert("   ||\t", false, Left, cx);
+        assert("   ||\t", false, Right, cx);
+    }
+
+    #[gpui::test]
+    fn test_clip_at_line_ends(cx: &mut gpui::MutableAppContext) {
+        fn assert(text: &str, cx: &mut gpui::MutableAppContext) {
+            let (mut unmarked_snapshot, markers) = marked_display_snapshot(text, cx);
+            unmarked_snapshot.clip_at_line_ends = true;
             assert_eq!(
-                map.clip_point(DisplayPoint::new(1, input_column as u32), bias),
-                DisplayPoint::new(1, output_column as u32),
-                "clip_point(({}, {}))",
-                1,
-                input_column,
+                unmarked_snapshot.clip_point(markers[1], Bias::Left),
+                markers[0]
             );
         }
+
+        assert("||", cx);
+        assert("|a|", cx);
+        assert("a|b|", cx);
+        assert("a|α|", cx);
     }
 
     #[gpui::test]
@@ -1163,27 +1281,38 @@ mod tests {
         )
     }
 
-    fn chunks<'a>(
+    fn syntax_chunks<'a>(
         rows: Range<u32>,
         map: &ModelHandle<DisplayMap>,
         theme: &'a SyntaxTheme,
         cx: &mut MutableAppContext,
     ) -> Vec<(String, Option<Color>)> {
+        chunks(rows, map, theme, cx)
+            .into_iter()
+            .map(|(text, color, _)| (text, color))
+            .collect()
+    }
+
+    fn chunks<'a>(
+        rows: Range<u32>,
+        map: &ModelHandle<DisplayMap>,
+        theme: &'a SyntaxTheme,
+        cx: &mut MutableAppContext,
+    ) -> Vec<(String, Option<Color>, Option<Color>)> {
         let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
-        let mut chunks: Vec<(String, Option<Color>)> = Vec::new();
+        let mut chunks: Vec<(String, Option<Color>, Option<Color>)> = Vec::new();
         for chunk in snapshot.chunks(rows, true) {
-            let color = chunk
+            let syntax_color = chunk
                 .syntax_highlight_id
                 .and_then(|id| id.style(theme)?.color);
-            if let Some((last_chunk, last_color)) = chunks.last_mut() {
-                if color == *last_color {
+            let highlight_color = chunk.highlight_style.and_then(|style| style.color);
+            if let Some((last_chunk, last_syntax_color, last_highlight_color)) = chunks.last_mut() {
+                if syntax_color == *last_syntax_color && highlight_color == *last_highlight_color {
                     last_chunk.push_str(chunk.text);
-                } else {
-                    chunks.push((chunk.text.to_string(), color));
+                    continue;
                 }
-            } else {
-                chunks.push((chunk.text.to_string(), color));
             }
+            chunks.push((chunk.text.to_string(), syntax_color, highlight_color));
         }
         chunks
     }

crates/editor/src/display_map/block_map.rs 🔗

@@ -499,7 +499,7 @@ impl<'a> BlockMapWriter<'a> {
             let block_ix = match self
                 .0
                 .blocks
-                .binary_search_by(|probe| probe.position.cmp(&position, &buffer).unwrap())
+                .binary_search_by(|probe| probe.position.cmp(&position, &buffer))
             {
                 Ok(ix) | Err(ix) => ix,
             };

crates/editor/src/display_map/fold_map.rs 🔗

@@ -140,13 +140,14 @@ impl<'a> FoldMapWriter<'a> {
     pub fn unfold<T: ToOffset>(
         &mut self,
         ranges: impl IntoIterator<Item = Range<T>>,
+        inclusive: bool,
     ) -> (FoldSnapshot, Vec<FoldEdit>) {
         let mut edits = Vec::new();
         let mut fold_ixs_to_delete = Vec::new();
         let buffer = self.0.buffer.lock().clone();
         for range in ranges.into_iter() {
             // Remove intersecting folds and add their ranges to edits that are passed to sync.
-            let mut folds_cursor = intersecting_folds(&buffer, &self.0.folds, range, true);
+            let mut folds_cursor = intersecting_folds(&buffer, &self.0.folds, range, inclusive);
             while let Some(fold) = folds_cursor.item() {
                 let offset_range = fold.0.start.to_offset(&buffer)..fold.0.end.to_offset(&buffer);
                 if offset_range.end > offset_range.start {
@@ -256,7 +257,7 @@ impl FoldMap {
             let mut folds = self.folds.iter().peekable();
             while let Some(fold) = folds.next() {
                 if let Some(next_fold) = folds.peek() {
-                    let comparison = fold.0.cmp(&next_fold.0, &self.buffer.lock()).unwrap();
+                    let comparison = fold.0.cmp(&next_fold.0, &self.buffer.lock());
                     assert!(comparison.is_le());
                 }
             }
@@ -699,10 +700,7 @@ impl FoldSnapshot {
                             let ranges = &highlights.1;
 
                             let start_ix = match ranges.binary_search_by(|probe| {
-                                let cmp = probe
-                                    .end
-                                    .cmp(&transform_start, &self.buffer_snapshot())
-                                    .unwrap();
+                                let cmp = probe.end.cmp(&transform_start, &self.buffer_snapshot());
                                 if cmp.is_gt() {
                                     Ordering::Greater
                                 } else {
@@ -715,7 +713,6 @@ impl FoldSnapshot {
                                 if range
                                     .start
                                     .cmp(&transform_end, &self.buffer_snapshot)
-                                    .unwrap()
                                     .is_ge()
                                 {
                                     break;
@@ -820,8 +817,8 @@ where
     let start = buffer.anchor_before(range.start.to_offset(buffer));
     let end = buffer.anchor_after(range.end.to_offset(buffer));
     let mut cursor = folds.filter::<_, usize>(move |summary| {
-        let start_cmp = start.cmp(&summary.max_end, buffer).unwrap();
-        let end_cmp = end.cmp(&summary.min_start, buffer).unwrap();
+        let start_cmp = start.cmp(&summary.max_end, buffer);
+        let end_cmp = end.cmp(&summary.min_start, buffer);
 
         if inclusive {
             start_cmp <= Ordering::Equal && end_cmp >= Ordering::Equal
@@ -962,19 +959,19 @@ impl sum_tree::Summary for FoldSummary {
     type Context = MultiBufferSnapshot;
 
     fn add_summary(&mut self, other: &Self, buffer: &MultiBufferSnapshot) {
-        if other.min_start.cmp(&self.min_start, buffer).unwrap() == Ordering::Less {
+        if other.min_start.cmp(&self.min_start, buffer) == Ordering::Less {
             self.min_start = other.min_start.clone();
         }
-        if other.max_end.cmp(&self.max_end, buffer).unwrap() == Ordering::Greater {
+        if other.max_end.cmp(&self.max_end, buffer) == Ordering::Greater {
             self.max_end = other.max_end.clone();
         }
 
         #[cfg(debug_assertions)]
         {
-            let start_comparison = self.start.cmp(&other.start, buffer).unwrap();
+            let start_comparison = self.start.cmp(&other.start, buffer);
             assert!(start_comparison <= Ordering::Equal);
             if start_comparison == Ordering::Equal {
-                assert!(self.end.cmp(&other.end, buffer).unwrap() >= Ordering::Equal);
+                assert!(self.end.cmp(&other.end, buffer) >= Ordering::Equal);
             }
         }
 
@@ -993,7 +990,7 @@ impl<'a> sum_tree::Dimension<'a, FoldSummary> for Fold {
 
 impl<'a> sum_tree::SeekTarget<'a, FoldSummary, Fold> for Fold {
     fn cmp(&self, other: &Self, buffer: &MultiBufferSnapshot) -> Ordering {
-        self.0.cmp(&other.0, buffer).unwrap()
+        self.0.cmp(&other.0, buffer)
     }
 }
 
@@ -1156,7 +1153,7 @@ impl Ord for HighlightEndpoint {
     fn cmp(&self, other: &Self) -> Ordering {
         self.offset
             .cmp(&other.offset)
-            .then_with(|| self.is_start.cmp(&other.is_start))
+            .then_with(|| other.is_start.cmp(&self.is_start))
     }
 }
 
@@ -1282,9 +1279,14 @@ mod tests {
         assert_eq!(snapshot4.text(), "123a…c123456eee");
 
         let (mut writer, _, _) = map.write(buffer_snapshot.clone(), vec![]);
-        writer.unfold(Some(Point::new(0, 4)..Point::new(0, 5)));
+        writer.unfold(Some(Point::new(0, 4)..Point::new(0, 4)), false);
         let (snapshot5, _) = map.read(buffer_snapshot.clone(), vec![]);
-        assert_eq!(snapshot5.text(), "123aaaaa\nbbbbbb\nccc123456eee");
+        assert_eq!(snapshot5.text(), "123a…c123456eee");
+
+        let (mut writer, _, _) = map.write(buffer_snapshot.clone(), vec![]);
+        writer.unfold(Some(Point::new(0, 4)..Point::new(0, 4)), true);
+        let (snapshot6, _) = map.read(buffer_snapshot.clone(), vec![]);
+        assert_eq!(snapshot6.text(), "123aaaaa\nbbbbbb\nccc123456eee");
     }
 
     #[gpui::test]
@@ -1600,9 +1602,8 @@ mod tests {
                     .filter(|fold| {
                         let start = buffer_snapshot.anchor_before(start);
                         let end = buffer_snapshot.anchor_after(end);
-                        start.cmp(&fold.0.end, &buffer_snapshot).unwrap() == Ordering::Less
-                            && end.cmp(&fold.0.start, &buffer_snapshot).unwrap()
-                                == Ordering::Greater
+                        start.cmp(&fold.0.end, &buffer_snapshot) == Ordering::Less
+                            && end.cmp(&fold.0.start, &buffer_snapshot) == Ordering::Greater
                     })
                     .map(|fold| fold.0)
                     .collect::<Vec<_>>();
@@ -1680,7 +1681,7 @@ mod tests {
             let buffer = self.buffer.lock().clone();
             let mut folds = self.folds.items(&buffer);
             // Ensure sorting doesn't change how folds get merged and displayed.
-            folds.sort_by(|a, b| a.0.cmp(&b.0, &buffer).unwrap());
+            folds.sort_by(|a, b| a.0.cmp(&b.0, &buffer));
             let mut fold_ranges = folds
                 .iter()
                 .map(|fold| fold.0.start.to_offset(&buffer)..fold.0.end.to_offset(&buffer))

crates/editor/src/editor.rs 🔗

@@ -10,7 +10,7 @@ mod test;
 use aho_corasick::AhoCorasick;
 use anyhow::Result;
 use clock::ReplicaId;
-use collections::{BTreeMap, Bound, HashMap, HashSet};
+use collections::{BTreeMap, Bound, HashMap, HashSet, VecDeque};
 pub use display_map::DisplayPoint;
 use display_map::*;
 pub use element::*;
@@ -28,7 +28,6 @@ use gpui::{
     ModelHandle, MutableAppContext, RenderContext, Task, View, ViewContext, ViewHandle,
     WeakViewHandle,
 };
-use items::{BufferItemHandle, MultiBufferItemHandle};
 use itertools::Itertools as _;
 pub use language::{char_kind, CharKind};
 use language::{
@@ -58,22 +57,26 @@ pub use sum_tree::Bias;
 use text::rope::TextDimension;
 use theme::DiagnosticStyle;
 use util::{post_inc, ResultExt, TryFutureExt};
-use workspace::{settings, ItemNavHistory, PathOpener, Settings, Workspace};
+use workspace::{settings, ItemNavHistory, Settings, Workspace};
 
 const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500);
 const MAX_LINE_LEN: usize = 1024;
 const MIN_NAVIGATION_HISTORY_ROW_DELTA: i64 = 10;
+const MAX_SELECTION_HISTORY_LEN: usize = 1024;
 
 action!(Cancel);
 action!(Backspace);
 action!(Delete);
 action!(Input, String);
 action!(Newline);
-action!(Tab);
+action!(Tab, Direction);
+action!(Indent);
 action!(Outdent);
 action!(DeleteLine);
-action!(DeleteToPreviousWordBoundary);
-action!(DeleteToNextWordBoundary);
+action!(DeleteToPreviousWordStart);
+action!(DeleteToPreviousSubwordStart);
+action!(DeleteToNextWordEnd);
+action!(DeleteToNextSubwordEnd);
 action!(DeleteToBeginningOfLine);
 action!(DeleteToEndOfLine);
 action!(CutToEndOfLine);
@@ -89,8 +92,10 @@ action!(MoveUp);
 action!(MoveDown);
 action!(MoveLeft);
 action!(MoveRight);
-action!(MoveToPreviousWordBoundary);
-action!(MoveToNextWordBoundary);
+action!(MoveToPreviousWordStart);
+action!(MoveToPreviousSubwordStart);
+action!(MoveToNextWordEnd);
+action!(MoveToNextSubwordEnd);
 action!(MoveToBeginningOfLine);
 action!(MoveToEndOfLine);
 action!(MoveToBeginning);
@@ -99,8 +104,10 @@ action!(SelectUp);
 action!(SelectDown);
 action!(SelectLeft);
 action!(SelectRight);
-action!(SelectToPreviousWordBoundary);
-action!(SelectToNextWordBoundary);
+action!(SelectToPreviousWordStart);
+action!(SelectToPreviousSubwordStart);
+action!(SelectToNextWordEnd);
+action!(SelectToNextSubwordEnd);
 action!(SelectToBeginningOfLine, bool);
 action!(SelectToEndOfLine, bool);
 action!(SelectToBeginning);
@@ -115,6 +122,8 @@ action!(ToggleComments);
 action!(SelectLargerSyntaxNode);
 action!(SelectSmallerSyntaxNode);
 action!(MoveToEnclosingBracket);
+action!(UndoSelection);
+action!(RedoSelection);
 action!(GoToDiagnostic, Direction);
 action!(GoToDefinition);
 action!(FindAllReferences);
@@ -123,7 +132,7 @@ action!(ConfirmRename);
 action!(PageUp);
 action!(PageDown);
 action!(Fold);
-action!(Unfold);
+action!(UnfoldLines);
 action!(FoldSelectedRanges);
 action!(Scroll, Vector2F);
 action!(Select, SelectPhase);
@@ -142,8 +151,7 @@ pub enum Direction {
     Next,
 }
 
-pub fn init(cx: &mut MutableAppContext, path_openers: &mut Vec<Box<dyn PathOpener>>) {
-    path_openers.push(Box::new(items::BufferOpener));
+pub fn init(cx: &mut MutableAppContext) {
     cx.add_bindings(vec![
         Binding::new("escape", Cancel, Some("Editor")),
         Binding::new("backspace", Backspace, Some("Editor")),
@@ -167,22 +175,28 @@ pub fn init(cx: &mut MutableAppContext, path_openers: &mut Vec<Box<dyn PathOpene
             Some("Editor && showing_code_actions"),
         ),
         Binding::new("enter", ConfirmRename, Some("Editor && renaming")),
-        Binding::new("tab", Tab, Some("Editor")),
+        Binding::new("tab", Tab(Direction::Next), Some("Editor")),
+        Binding::new("shift-tab", Tab(Direction::Prev), Some("Editor")),
         Binding::new(
             "tab",
             ConfirmCompletion(None),
             Some("Editor && showing_completions"),
         ),
-        Binding::new("shift-tab", Outdent, Some("Editor")),
+        Binding::new("cmd-[", Outdent, Some("Editor")),
+        Binding::new("cmd-]", Indent, Some("Editor")),
         Binding::new("ctrl-shift-K", DeleteLine, Some("Editor")),
+        Binding::new("alt-backspace", DeleteToPreviousWordStart, Some("Editor")),
+        Binding::new("alt-h", DeleteToPreviousWordStart, Some("Editor")),
         Binding::new(
-            "alt-backspace",
-            DeleteToPreviousWordBoundary,
+            "ctrl-alt-backspace",
+            DeleteToPreviousSubwordStart,
             Some("Editor"),
         ),
-        Binding::new("alt-h", DeleteToPreviousWordBoundary, Some("Editor")),
-        Binding::new("alt-delete", DeleteToNextWordBoundary, Some("Editor")),
-        Binding::new("alt-d", DeleteToNextWordBoundary, Some("Editor")),
+        Binding::new("ctrl-alt-h", DeleteToPreviousSubwordStart, Some("Editor")),
+        Binding::new("alt-delete", DeleteToNextWordEnd, Some("Editor")),
+        Binding::new("alt-d", DeleteToNextWordEnd, Some("Editor")),
+        Binding::new("ctrl-alt-delete", DeleteToNextSubwordEnd, Some("Editor")),
+        Binding::new("ctrl-alt-d", DeleteToNextSubwordEnd, Some("Editor")),
         Binding::new("cmd-backspace", DeleteToBeginningOfLine, Some("Editor")),
         Binding::new("cmd-delete", DeleteToEndOfLine, Some("Editor")),
         Binding::new("ctrl-k", CutToEndOfLine, Some("Editor")),
@@ -202,10 +216,14 @@ pub fn init(cx: &mut MutableAppContext, path_openers: &mut Vec<Box<dyn PathOpene
         Binding::new("ctrl-n", MoveDown, Some("Editor")),
         Binding::new("ctrl-b", MoveLeft, Some("Editor")),
         Binding::new("ctrl-f", MoveRight, Some("Editor")),
-        Binding::new("alt-left", MoveToPreviousWordBoundary, Some("Editor")),
-        Binding::new("alt-b", MoveToPreviousWordBoundary, Some("Editor")),
-        Binding::new("alt-right", MoveToNextWordBoundary, Some("Editor")),
-        Binding::new("alt-f", MoveToNextWordBoundary, Some("Editor")),
+        Binding::new("alt-left", MoveToPreviousWordStart, Some("Editor")),
+        Binding::new("alt-b", MoveToPreviousWordStart, Some("Editor")),
+        Binding::new("ctrl-alt-left", MoveToPreviousSubwordStart, Some("Editor")),
+        Binding::new("ctrl-alt-b", MoveToPreviousSubwordStart, Some("Editor")),
+        Binding::new("alt-right", MoveToNextWordEnd, Some("Editor")),
+        Binding::new("alt-f", MoveToNextWordEnd, Some("Editor")),
+        Binding::new("ctrl-alt-right", MoveToNextSubwordEnd, Some("Editor")),
+        Binding::new("ctrl-alt-f", MoveToNextSubwordEnd, Some("Editor")),
         Binding::new("cmd-left", MoveToBeginningOfLine, Some("Editor")),
         Binding::new("ctrl-a", MoveToBeginningOfLine, Some("Editor")),
         Binding::new("cmd-right", MoveToEndOfLine, Some("Editor")),
@@ -220,19 +238,31 @@ pub fn init(cx: &mut MutableAppContext, path_openers: &mut Vec<Box<dyn PathOpene
         Binding::new("ctrl-shift-B", SelectLeft, Some("Editor")),
         Binding::new("shift-right", SelectRight, Some("Editor")),
         Binding::new("ctrl-shift-F", SelectRight, Some("Editor")),
+        Binding::new("alt-shift-left", SelectToPreviousWordStart, Some("Editor")),
+        Binding::new("alt-shift-B", SelectToPreviousWordStart, Some("Editor")),
         Binding::new(
-            "alt-shift-left",
-            SelectToPreviousWordBoundary,
+            "ctrl-alt-shift-left",
+            SelectToPreviousSubwordStart,
             Some("Editor"),
         ),
-        Binding::new("alt-shift-B", SelectToPreviousWordBoundary, Some("Editor")),
-        Binding::new("alt-shift-right", SelectToNextWordBoundary, Some("Editor")),
-        Binding::new("alt-shift-F", SelectToNextWordBoundary, Some("Editor")),
+        Binding::new(
+            "ctrl-alt-shift-B",
+            SelectToPreviousSubwordStart,
+            Some("Editor"),
+        ),
+        Binding::new("alt-shift-right", SelectToNextWordEnd, Some("Editor")),
+        Binding::new("alt-shift-F", SelectToNextWordEnd, Some("Editor")),
         Binding::new(
             "cmd-shift-left",
             SelectToBeginningOfLine(true),
             Some("Editor"),
         ),
+        Binding::new(
+            "ctrl-alt-shift-right",
+            SelectToNextSubwordEnd,
+            Some("Editor"),
+        ),
+        Binding::new("ctrl-alt-shift-F", SelectToNextSubwordEnd, Some("Editor")),
         Binding::new(
             "ctrl-shift-A",
             SelectToBeginningOfLine(true),
@@ -256,6 +286,8 @@ pub fn init(cx: &mut MutableAppContext, path_openers: &mut Vec<Box<dyn PathOpene
         Binding::new("ctrl-w", SelectLargerSyntaxNode, Some("Editor")),
         Binding::new("alt-down", SelectSmallerSyntaxNode, Some("Editor")),
         Binding::new("ctrl-shift-W", SelectSmallerSyntaxNode, Some("Editor")),
+        Binding::new("cmd-u", UndoSelection, Some("Editor")),
+        Binding::new("cmd-shift-U", RedoSelection, Some("Editor")),
         Binding::new("f8", GoToDiagnostic(Direction::Next), Some("Editor")),
         Binding::new("shift-f8", GoToDiagnostic(Direction::Prev), Some("Editor")),
         Binding::new("f2", Rename, Some("Editor")),
@@ -265,7 +297,7 @@ pub fn init(cx: &mut MutableAppContext, path_openers: &mut Vec<Box<dyn PathOpene
         Binding::new("pageup", PageUp, Some("Editor")),
         Binding::new("pagedown", PageDown, Some("Editor")),
         Binding::new("alt-cmd-[", Fold, Some("Editor")),
-        Binding::new("alt-cmd-]", Unfold, Some("Editor")),
+        Binding::new("alt-cmd-]", UnfoldLines, Some("Editor")),
         Binding::new("alt-cmd-f", FoldSelectedRanges, Some("Editor")),
         Binding::new("ctrl-space", ShowCompletions, Some("Editor")),
         Binding::new("cmd-.", ToggleCodeActions(false), Some("Editor")),
@@ -281,10 +313,13 @@ pub fn init(cx: &mut MutableAppContext, path_openers: &mut Vec<Box<dyn PathOpene
     cx.add_action(Editor::backspace);
     cx.add_action(Editor::delete);
     cx.add_action(Editor::tab);
+    cx.add_action(Editor::indent);
     cx.add_action(Editor::outdent);
     cx.add_action(Editor::delete_line);
-    cx.add_action(Editor::delete_to_previous_word_boundary);
-    cx.add_action(Editor::delete_to_next_word_boundary);
+    cx.add_action(Editor::delete_to_previous_word_start);
+    cx.add_action(Editor::delete_to_previous_subword_start);
+    cx.add_action(Editor::delete_to_next_word_end);
+    cx.add_action(Editor::delete_to_next_subword_end);
     cx.add_action(Editor::delete_to_beginning_of_line);
     cx.add_action(Editor::delete_to_end_of_line);
     cx.add_action(Editor::cut_to_end_of_line);
@@ -300,8 +335,10 @@ pub fn init(cx: &mut MutableAppContext, path_openers: &mut Vec<Box<dyn PathOpene
     cx.add_action(Editor::move_down);
     cx.add_action(Editor::move_left);
     cx.add_action(Editor::move_right);
-    cx.add_action(Editor::move_to_previous_word_boundary);
-    cx.add_action(Editor::move_to_next_word_boundary);
+    cx.add_action(Editor::move_to_previous_word_start);
+    cx.add_action(Editor::move_to_previous_subword_start);
+    cx.add_action(Editor::move_to_next_word_end);
+    cx.add_action(Editor::move_to_next_subword_end);
     cx.add_action(Editor::move_to_beginning_of_line);
     cx.add_action(Editor::move_to_end_of_line);
     cx.add_action(Editor::move_to_beginning);
@@ -310,8 +347,10 @@ pub fn init(cx: &mut MutableAppContext, path_openers: &mut Vec<Box<dyn PathOpene
     cx.add_action(Editor::select_down);
     cx.add_action(Editor::select_left);
     cx.add_action(Editor::select_right);
-    cx.add_action(Editor::select_to_previous_word_boundary);
-    cx.add_action(Editor::select_to_next_word_boundary);
+    cx.add_action(Editor::select_to_previous_word_start);
+    cx.add_action(Editor::select_to_previous_subword_start);
+    cx.add_action(Editor::select_to_next_word_end);
+    cx.add_action(Editor::select_to_next_subword_end);
     cx.add_action(Editor::select_to_beginning_of_line);
     cx.add_action(Editor::select_to_end_of_line);
     cx.add_action(Editor::select_to_beginning);
@@ -326,12 +365,14 @@ pub fn init(cx: &mut MutableAppContext, path_openers: &mut Vec<Box<dyn PathOpene
     cx.add_action(Editor::select_larger_syntax_node);
     cx.add_action(Editor::select_smaller_syntax_node);
     cx.add_action(Editor::move_to_enclosing_bracket);
+    cx.add_action(Editor::undo_selection);
+    cx.add_action(Editor::redo_selection);
     cx.add_action(Editor::go_to_diagnostic);
     cx.add_action(Editor::go_to_definition);
     cx.add_action(Editor::page_up);
     cx.add_action(Editor::page_down);
     cx.add_action(Editor::fold);
-    cx.add_action(Editor::unfold);
+    cx.add_action(Editor::unfold_lines);
     cx.add_action(Editor::fold_selected_ranges);
     cx.add_action(Editor::show_completions);
     cx.add_action(Editor::toggle_code_actions);
@@ -341,14 +382,9 @@ pub fn init(cx: &mut MutableAppContext, path_openers: &mut Vec<Box<dyn PathOpene
     cx.add_async_action(Editor::rename);
     cx.add_async_action(Editor::confirm_rename);
     cx.add_async_action(Editor::find_all_references);
-}
 
-trait SelectionExt {
-    fn offset_range(&self, buffer: &MultiBufferSnapshot) -> Range<usize>;
-    fn point_range(&self, buffer: &MultiBufferSnapshot) -> Range<Point>;
-    fn display_range(&self, map: &DisplaySnapshot) -> Range<DisplayPoint>;
-    fn spanned_rows(&self, include_end_if_at_line_start: bool, map: &DisplaySnapshot)
-        -> Range<u32>;
+    workspace::register_project_item::<Editor>(cx);
+    workspace::register_followable_item::<Editor>(cx);
 }
 
 trait InvalidationRegion {
@@ -430,15 +466,14 @@ pub struct Editor {
     columnar_selection_tail: Option<Anchor>,
     add_selections_state: Option<AddSelectionsState>,
     select_next_state: Option<SelectNextState>,
-    selection_history:
-        HashMap<TransactionId, (Arc<[Selection<Anchor>]>, Option<Arc<[Selection<Anchor>]>>)>,
+    selection_history: SelectionHistory,
     autoclose_stack: InvalidationStack<BracketPairState>,
     snippet_stack: InvalidationStack<SnippetState>,
     select_larger_syntax_node_stack: Vec<Box<[Selection<usize>]>>,
     active_diagnostics: Option<ActiveDiagnosticGroup>,
     scroll_position: Vector2F,
-    scroll_top_anchor: Option<Anchor>,
-    autoscroll_request: Option<Autoscroll>,
+    scroll_top_anchor: Anchor,
+    autoscroll_request: Option<(Autoscroll, bool)>,
     soft_wrap_mode_override: Option<settings::SoftWrap>,
     get_field_editor_theme: Option<GetFieldEditorTheme>,
     override_text_style: Option<Box<OverrideTextStyle>>,
@@ -463,6 +498,9 @@ pub struct Editor {
     pending_rename: Option<RenameState>,
     searchable: bool,
     cursor_shape: CursorShape,
+    keymap_context_layers: BTreeMap<TypeId, gpui::keymap::Context>,
+    input_enabled: bool,
+    leader_replica_id: Option<u16>,
 }
 
 pub struct EditorSnapshot {
@@ -471,7 +509,7 @@ pub struct EditorSnapshot {
     pub placeholder_text: Option<Arc<str>>,
     is_focused: bool,
     scroll_position: Vector2F,
-    scroll_top_anchor: Option<Anchor>,
+    scroll_top_anchor: Anchor,
 }
 
 #[derive(Clone)]
@@ -480,11 +518,105 @@ pub struct PendingSelection {
     mode: SelectMode,
 }
 
+#[derive(Clone)]
+struct SelectionHistoryEntry {
+    selections: Arc<[Selection<Anchor>]>,
+    select_next_state: Option<SelectNextState>,
+    add_selections_state: Option<AddSelectionsState>,
+}
+
+enum SelectionHistoryMode {
+    Normal,
+    Undoing,
+    Redoing,
+}
+
+impl Default for SelectionHistoryMode {
+    fn default() -> Self {
+        Self::Normal
+    }
+}
+
+#[derive(Default)]
+struct SelectionHistory {
+    selections_by_transaction:
+        HashMap<TransactionId, (Arc<[Selection<Anchor>]>, Option<Arc<[Selection<Anchor>]>>)>,
+    mode: SelectionHistoryMode,
+    undo_stack: VecDeque<SelectionHistoryEntry>,
+    redo_stack: VecDeque<SelectionHistoryEntry>,
+}
+
+impl SelectionHistory {
+    fn insert_transaction(
+        &mut self,
+        transaction_id: TransactionId,
+        selections: Arc<[Selection<Anchor>]>,
+    ) {
+        self.selections_by_transaction
+            .insert(transaction_id, (selections, None));
+    }
+
+    fn transaction(
+        &self,
+        transaction_id: TransactionId,
+    ) -> Option<&(Arc<[Selection<Anchor>]>, Option<Arc<[Selection<Anchor>]>>)> {
+        self.selections_by_transaction.get(&transaction_id)
+    }
+
+    fn transaction_mut(
+        &mut self,
+        transaction_id: TransactionId,
+    ) -> Option<&mut (Arc<[Selection<Anchor>]>, Option<Arc<[Selection<Anchor>]>>)> {
+        self.selections_by_transaction.get_mut(&transaction_id)
+    }
+
+    fn push(&mut self, entry: SelectionHistoryEntry) {
+        if !entry.selections.is_empty() {
+            match self.mode {
+                SelectionHistoryMode::Normal => {
+                    self.push_undo(entry);
+                    self.redo_stack.clear();
+                }
+                SelectionHistoryMode::Undoing => self.push_redo(entry),
+                SelectionHistoryMode::Redoing => self.push_undo(entry),
+            }
+        }
+    }
+
+    fn push_undo(&mut self, entry: SelectionHistoryEntry) {
+        if self
+            .undo_stack
+            .back()
+            .map_or(true, |e| e.selections != entry.selections)
+        {
+            self.undo_stack.push_back(entry);
+            if self.undo_stack.len() > MAX_SELECTION_HISTORY_LEN {
+                self.undo_stack.pop_front();
+            }
+        }
+    }
+
+    fn push_redo(&mut self, entry: SelectionHistoryEntry) {
+        if self
+            .redo_stack
+            .back()
+            .map_or(true, |e| e.selections != entry.selections)
+        {
+            self.redo_stack.push_back(entry);
+            if self.redo_stack.len() > MAX_SELECTION_HISTORY_LEN {
+                self.redo_stack.pop_front();
+            }
+        }
+    }
+}
+
+#[derive(Clone)]
 struct AddSelectionsState {
     above: bool,
     stack: Vec<usize>,
 }
 
+#[derive(Clone)]
 struct SelectNextState {
     query: AhoCorasick,
     wordwise: bool,
@@ -802,6 +934,8 @@ pub struct NavigationData {
     offset: usize,
 }
 
+pub struct EditorCreated(pub ViewHandle<Editor>);
+
 impl Editor {
     pub fn single_line(
         field_editor_style: Option<GetFieldEditorTheme>,
@@ -829,6 +963,15 @@ impl Editor {
     }
 
     pub fn for_buffer(
+        buffer: ModelHandle<Buffer>,
+        project: Option<ModelHandle<Project>>,
+        cx: &mut ViewContext<Self>,
+    ) -> Self {
+        let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
+        Self::new(EditorMode::Full, buffer, project, None, cx)
+    }
+
+    pub fn for_multibuffer(
         buffer: ModelHandle<MultiBuffer>,
         project: Option<ModelHandle<Project>>,
         cx: &mut ViewContext<Self>,
@@ -836,7 +979,7 @@ impl Editor {
         Self::new(EditorMode::Full, buffer, project, None, cx)
     }
 
-    pub fn clone(&self, nav_history: ItemNavHistory, cx: &mut ViewContext<Self>) -> Self {
+    pub fn clone(&self, cx: &mut ViewContext<Self>) -> Self {
         let mut clone = Self::new(
             self.mode,
             self.buffer.clone(),
@@ -846,7 +989,6 @@ impl Editor {
         );
         clone.scroll_position = self.scroll_position;
         clone.scroll_top_anchor = self.scroll_top_anchor.clone();
-        clone.nav_history = Some(nav_history);
         clone.searchable = self.searchable;
         clone
     }
@@ -859,7 +1001,7 @@ impl Editor {
         cx: &mut ViewContext<Self>,
     ) -> Self {
         let display_map = cx.add_model(|cx| {
-            let settings = cx.app_state::<Settings>();
+            let settings = cx.global::<Settings>();
             let style = build_style(&*settings, get_field_editor_theme, None, cx);
             DisplayMap::new(
                 buffer.clone(),
@@ -905,7 +1047,7 @@ impl Editor {
             get_field_editor_theme,
             project,
             scroll_position: Vector2F::zero(),
-            scroll_top_anchor: None,
+            scroll_top_anchor: Anchor::min(),
             autoscroll_request: None,
             focused: false,
             show_local_cursors: false,
@@ -928,8 +1070,15 @@ impl Editor {
             searchable: true,
             override_text_style: None,
             cursor_shape: Default::default(),
+            keymap_context_layers: Default::default(),
+            input_enabled: true,
+            leader_replica_id: None,
         };
         this.end_selection(cx);
+
+        let editor_created_event = EditorCreated(cx.handle());
+        cx.emit_global(editor_created_event);
+
         this
     }
 
@@ -938,14 +1087,17 @@ impl Editor {
         _: &workspace::OpenNew,
         cx: &mut ViewContext<Workspace>,
     ) {
-        let project = workspace.project();
+        let project = workspace.project().clone();
         if project.read(cx).is_remote() {
             cx.propagate_action();
         } else if let Some(buffer) = project
             .update(cx, |project, cx| project.create_buffer(cx))
             .log_err()
         {
-            workspace.open_item(BufferItemHandle(buffer), cx);
+            workspace.add_item(
+                Box::new(cx.add_view(|cx| Editor::for_buffer(buffer, Some(project.clone()), cx))),
+                cx,
+            );
         }
     }
 
@@ -981,13 +1133,17 @@ impl Editor {
 
     fn style(&self, cx: &AppContext) -> EditorStyle {
         build_style(
-            cx.app_state::<Settings>(),
+            cx.global::<Settings>(),
             self.get_field_editor_theme,
             self.override_text_style.as_deref(),
             cx,
         )
     }
 
+    pub fn mode(&self) -> EditorMode {
+        self.mode
+    }
+
     pub fn set_placeholder_text(
         &mut self,
         placeholder_text: impl Into<Arc<str>>,
@@ -1003,10 +1159,19 @@ impl Editor {
     }
 
     pub fn set_scroll_position(&mut self, scroll_position: Vector2F, cx: &mut ViewContext<Self>) {
+        self.set_scroll_position_internal(scroll_position, true, cx);
+    }
+
+    fn set_scroll_position_internal(
+        &mut self,
+        scroll_position: Vector2F,
+        local: bool,
+        cx: &mut ViewContext<Self>,
+    ) {
         let map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
 
         if scroll_position.y() == 0. {
-            self.scroll_top_anchor = None;
+            self.scroll_top_anchor = Anchor::min();
             self.scroll_position = scroll_position;
         } else {
             let scroll_top_buffer_offset =
@@ -1018,9 +1183,22 @@ impl Editor {
                 scroll_position.x(),
                 scroll_position.y() - anchor.to_display_point(&map).row() as f32,
             );
-            self.scroll_top_anchor = Some(anchor);
+            self.scroll_top_anchor = anchor;
         }
 
+        cx.emit(Event::ScrollPositionChanged { local });
+        cx.notify();
+    }
+
+    fn set_scroll_top_anchor(
+        &mut self,
+        anchor: Anchor,
+        position: Vector2F,
+        cx: &mut ViewContext<Self>,
+    ) {
+        self.scroll_top_anchor = anchor;
+        self.scroll_position = position;
+        cx.emit(Event::ScrollPositionChanged { local: false });
         cx.notify();
     }
 
@@ -1029,6 +1207,24 @@ impl Editor {
         cx.notify();
     }
 
+    pub fn set_clip_at_line_ends(&mut self, clip: bool, cx: &mut ViewContext<Self>) {
+        self.display_map
+            .update(cx, |map, _| map.clip_at_line_ends = clip);
+    }
+
+    pub fn set_keymap_context_layer<Tag: 'static>(&mut self, context: gpui::keymap::Context) {
+        self.keymap_context_layers
+            .insert(TypeId::of::<Tag>(), context);
+    }
+
+    pub fn remove_keymap_context_layer<Tag: 'static>(&mut self) {
+        self.keymap_context_layers.remove(&TypeId::of::<Tag>());
+    }
+
+    pub fn set_input_enabled(&mut self, input_enabled: bool) {
+        self.input_enabled = input_enabled;
+    }
+
     pub fn scroll_position(&self, cx: &mut ViewContext<Self>) -> Vector2F {
         let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
         compute_scroll_position(&display_map, self.scroll_position, &self.scroll_top_anchor)
@@ -1063,7 +1259,7 @@ impl Editor {
             self.set_scroll_position(scroll_position, cx);
         }
 
-        let autoscroll = if let Some(autoscroll) = self.autoscroll_request.take() {
+        let (autoscroll, local) = if let Some(autoscroll) = self.autoscroll_request.take() {
             autoscroll
         } else {
             return false;
@@ -1115,15 +1311,15 @@ impl Editor {
 
                 if target_top < start_row {
                     scroll_position.set_y(target_top);
-                    self.set_scroll_position(scroll_position, cx);
+                    self.set_scroll_position_internal(scroll_position, local, cx);
                 } else if target_bottom >= end_row {
                     scroll_position.set_y(target_bottom - visible_lines);
-                    self.set_scroll_position(scroll_position, cx);
+                    self.set_scroll_position_internal(scroll_position, local, cx);
                 }
             }
             Autoscroll::Center => {
                 scroll_position.set_y((first_cursor_top - margin).max(0.0));
-                self.set_scroll_position(scroll_position, cx);
+                self.set_scroll_position_internal(scroll_position, local, cx);
             }
         }
 
@@ -1189,6 +1385,66 @@ impl Editor {
         }
     }
 
+    pub fn move_selections(
+        &mut self,
+        cx: &mut ViewContext<Self>,
+        move_selection: impl Fn(&DisplaySnapshot, &mut Selection<DisplayPoint>),
+    ) {
+        let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
+        let selections = self
+            .local_selections::<Point>(cx)
+            .into_iter()
+            .map(|selection| {
+                let mut selection = Selection {
+                    id: selection.id,
+                    start: selection.start.to_display_point(&display_map),
+                    end: selection.end.to_display_point(&display_map),
+                    reversed: selection.reversed,
+                    goal: selection.goal,
+                };
+                move_selection(&display_map, &mut selection);
+                Selection {
+                    id: selection.id,
+                    start: selection.start.to_point(&display_map),
+                    end: selection.end.to_point(&display_map),
+                    reversed: selection.reversed,
+                    goal: selection.goal,
+                }
+            })
+            .collect();
+        self.update_selections(selections, Some(Autoscroll::Fit), cx);
+    }
+
+    pub fn move_selection_heads(
+        &mut self,
+        cx: &mut ViewContext<Self>,
+        update_head: impl Fn(
+            &DisplaySnapshot,
+            DisplayPoint,
+            SelectionGoal,
+        ) -> (DisplayPoint, SelectionGoal),
+    ) {
+        self.move_selections(cx, |map, selection| {
+            let (new_head, new_goal) = update_head(map, selection.head(), selection.goal);
+            selection.set_head(new_head, new_goal);
+        });
+    }
+
+    pub fn move_cursors(
+        &mut self,
+        cx: &mut ViewContext<Self>,
+        update_cursor_position: impl Fn(
+            &DisplaySnapshot,
+            DisplayPoint,
+            SelectionGoal,
+        ) -> (DisplayPoint, SelectionGoal),
+    ) {
+        self.move_selections(cx, |map, selection| {
+            let (cursor, new_goal) = update_cursor_position(map, selection.head(), selection.goal);
+            selection.collapse_to(cursor, new_goal)
+        });
+    }
+
     fn select(&mut self, Select(phase): &Select, cx: &mut ViewContext<Self>) {
         self.hide_context_menu(cx);
 
@@ -1245,7 +1501,7 @@ impl Editor {
             _ => {}
         }
 
-        self.set_selections(self.selections.clone(), Some(pending), cx);
+        self.set_selections(self.selections.clone(), Some(pending), true, cx);
     }
 
     fn begin_selection(
@@ -1299,8 +1555,6 @@ impl Editor {
             }
         }
 
-        self.push_to_nav_history(newest_selection.head(), Some(end.to_point(&buffer)), cx);
-
         let selection = Selection {
             id: post_inc(&mut self.next_selection_id),
             start,
@@ -1325,7 +1579,12 @@ impl Editor {
         } else {
             selections = Arc::from([]);
         }
-        self.set_selections(selections, Some(PendingSelection { selection, mode }), cx);
+        self.set_selections(
+            selections,
+            Some(PendingSelection { selection, mode }),
+            true,
+            cx,
+        );
 
         cx.notify();
     }
@@ -1439,7 +1698,7 @@ impl Editor {
                 pending.selection.end = buffer.anchor_before(head);
                 pending.selection.reversed = false;
             }
-            self.set_selections(self.selections.clone(), Some(pending), cx);
+            self.set_selections(self.selections.clone(), Some(pending), true, cx);
         } else {
             log::error!("update_selection dispatched with no pending selection");
             return;
@@ -1526,7 +1785,7 @@ impl Editor {
             if selections.is_empty() {
                 selections = Arc::from([pending.selection]);
             }
-            self.set_selections(selections, None, cx);
+            self.set_selections(selections, None, true, cx);
             self.request_autoscroll(Autoscroll::Fit, cx);
         } else {
             let mut oldest_selection = self.oldest_selection::<usize>(&cx);
@@ -1546,7 +1805,7 @@ impl Editor {
     #[cfg(any(test, feature = "test-support"))]
     pub fn selected_ranges<D: TextDimension + Ord + Sub<D, Output = D>>(
         &self,
-        cx: &mut MutableAppContext,
+        cx: &AppContext,
     ) -> Vec<Range<D>> {
         self.local_selections::<D>(cx)
             .iter()
@@ -1645,136 +1904,142 @@ impl Editor {
     }
 
     pub fn handle_input(&mut self, action: &Input, cx: &mut ViewContext<Self>) {
+        if !self.input_enabled {
+            cx.propagate_action();
+            return;
+        }
+
         let text = action.0.as_ref();
         if !self.skip_autoclose_end(text, cx) {
-            self.start_transaction(cx);
-            if !self.surround_with_bracket_pair(text, cx) {
-                self.insert(text, cx);
-                self.autoclose_bracket_pairs(cx);
-            }
-            self.end_transaction(cx);
+            self.transact(cx, |this, cx| {
+                if !this.surround_with_bracket_pair(text, cx) {
+                    this.insert(text, cx);
+                    this.autoclose_bracket_pairs(cx);
+                }
+            });
             self.trigger_completion_on_input(text, cx);
         }
     }
 
     pub fn newline(&mut self, _: &Newline, cx: &mut ViewContext<Self>) {
-        self.start_transaction(cx);
-        let mut old_selections = SmallVec::<[_; 32]>::new();
-        {
-            let selections = self.local_selections::<usize>(cx);
-            let buffer = self.buffer.read(cx).snapshot(cx);
-            for selection in selections.iter() {
-                let start_point = selection.start.to_point(&buffer);
-                let indent = buffer
-                    .indent_column_for_line(start_point.row)
-                    .min(start_point.column);
-                let start = selection.start;
-                let end = selection.end;
-
-                let mut insert_extra_newline = false;
-                if let Some(language) = buffer.language() {
-                    let leading_whitespace_len = buffer
-                        .reversed_chars_at(start)
-                        .take_while(|c| c.is_whitespace() && *c != '\n')
-                        .map(|c| c.len_utf8())
-                        .sum::<usize>();
-
-                    let trailing_whitespace_len = buffer
-                        .chars_at(end)
-                        .take_while(|c| c.is_whitespace() && *c != '\n')
-                        .map(|c| c.len_utf8())
-                        .sum::<usize>();
-
-                    insert_extra_newline = language.brackets().iter().any(|pair| {
-                        let pair_start = pair.start.trim_end();
-                        let pair_end = pair.end.trim_start();
-
-                        pair.newline
-                            && buffer.contains_str_at(end + trailing_whitespace_len, pair_end)
-                            && buffer.contains_str_at(
-                                (start - leading_whitespace_len).saturating_sub(pair_start.len()),
-                                pair_start,
-                            )
-                    });
-                }
+        self.transact(cx, |this, cx| {
+            let mut old_selections = SmallVec::<[_; 32]>::new();
+            {
+                let selections = this.local_selections::<usize>(cx);
+                let buffer = this.buffer.read(cx).snapshot(cx);
+                for selection in selections.iter() {
+                    let start_point = selection.start.to_point(&buffer);
+                    let indent = buffer
+                        .indent_column_for_line(start_point.row)
+                        .min(start_point.column);
+                    let start = selection.start;
+                    let end = selection.end;
+
+                    let mut insert_extra_newline = false;
+                    if let Some(language) = buffer.language() {
+                        let leading_whitespace_len = buffer
+                            .reversed_chars_at(start)
+                            .take_while(|c| c.is_whitespace() && *c != '\n')
+                            .map(|c| c.len_utf8())
+                            .sum::<usize>();
+
+                        let trailing_whitespace_len = buffer
+                            .chars_at(end)
+                            .take_while(|c| c.is_whitespace() && *c != '\n')
+                            .map(|c| c.len_utf8())
+                            .sum::<usize>();
+
+                        insert_extra_newline = language.brackets().iter().any(|pair| {
+                            let pair_start = pair.start.trim_end();
+                            let pair_end = pair.end.trim_start();
+
+                            pair.newline
+                                && buffer.contains_str_at(end + trailing_whitespace_len, pair_end)
+                                && buffer.contains_str_at(
+                                    (start - leading_whitespace_len)
+                                        .saturating_sub(pair_start.len()),
+                                    pair_start,
+                                )
+                        });
+                    }
 
-                old_selections.push((
-                    selection.id,
-                    buffer.anchor_after(end),
-                    start..end,
-                    indent,
-                    insert_extra_newline,
-                ));
+                    old_selections.push((
+                        selection.id,
+                        buffer.anchor_after(end),
+                        start..end,
+                        indent,
+                        insert_extra_newline,
+                    ));
+                }
             }
-        }
 
-        self.buffer.update(cx, |buffer, cx| {
-            let mut delta = 0_isize;
-            let mut pending_edit: Option<PendingEdit> = None;
-            for (_, _, range, indent, insert_extra_newline) in &old_selections {
-                if pending_edit.as_ref().map_or(false, |pending| {
-                    pending.indent != *indent
-                        || pending.insert_extra_newline != *insert_extra_newline
-                }) {
-                    let pending = pending_edit.take().unwrap();
-                    let mut new_text = String::with_capacity(1 + pending.indent as usize);
-                    new_text.push('\n');
-                    new_text.extend(iter::repeat(' ').take(pending.indent as usize));
-                    if pending.insert_extra_newline {
-                        new_text = new_text.repeat(2);
+            this.buffer.update(cx, |buffer, cx| {
+                let mut delta = 0_isize;
+                let mut pending_edit: Option<PendingEdit> = None;
+                for (_, _, range, indent, insert_extra_newline) in &old_selections {
+                    if pending_edit.as_ref().map_or(false, |pending| {
+                        pending.indent != *indent
+                            || pending.insert_extra_newline != *insert_extra_newline
+                    }) {
+                        let pending = pending_edit.take().unwrap();
+                        let mut new_text = String::with_capacity(1 + pending.indent as usize);
+                        new_text.push('\n');
+                        new_text.extend(iter::repeat(' ').take(pending.indent as usize));
+                        if pending.insert_extra_newline {
+                            new_text = new_text.repeat(2);
+                        }
+                        buffer.edit_with_autoindent(pending.ranges, new_text, cx);
+                        delta += pending.delta;
                     }
-                    buffer.edit_with_autoindent(pending.ranges, new_text, cx);
-                    delta += pending.delta;
-                }
 
-                let start = (range.start as isize + delta) as usize;
-                let end = (range.end as isize + delta) as usize;
-                let mut text_len = *indent as usize + 1;
-                if *insert_extra_newline {
-                    text_len *= 2;
+                    let start = (range.start as isize + delta) as usize;
+                    let end = (range.end as isize + delta) as usize;
+                    let mut text_len = *indent as usize + 1;
+                    if *insert_extra_newline {
+                        text_len *= 2;
+                    }
+
+                    let pending = pending_edit.get_or_insert_with(Default::default);
+                    pending.delta += text_len as isize - (end - start) as isize;
+                    pending.indent = *indent;
+                    pending.insert_extra_newline = *insert_extra_newline;
+                    pending.ranges.push(start..end);
                 }
 
-                let pending = pending_edit.get_or_insert_with(Default::default);
-                pending.delta += text_len as isize - (end - start) as isize;
-                pending.indent = *indent;
-                pending.insert_extra_newline = *insert_extra_newline;
-                pending.ranges.push(start..end);
-            }
+                let pending = pending_edit.unwrap();
+                let mut new_text = String::with_capacity(1 + pending.indent as usize);
+                new_text.push('\n');
+                new_text.extend(iter::repeat(' ').take(pending.indent as usize));
+                if pending.insert_extra_newline {
+                    new_text = new_text.repeat(2);
+                }
+                buffer.edit_with_autoindent(pending.ranges, new_text, cx);
 
-            let pending = pending_edit.unwrap();
-            let mut new_text = String::with_capacity(1 + pending.indent as usize);
-            new_text.push('\n');
-            new_text.extend(iter::repeat(' ').take(pending.indent as usize));
-            if pending.insert_extra_newline {
-                new_text = new_text.repeat(2);
-            }
-            buffer.edit_with_autoindent(pending.ranges, new_text, cx);
+                let buffer = buffer.read(cx);
+                this.selections = this
+                    .selections
+                    .iter()
+                    .cloned()
+                    .zip(old_selections)
+                    .map(
+                        |(mut new_selection, (_, end_anchor, _, _, insert_extra_newline))| {
+                            let mut cursor = end_anchor.to_point(&buffer);
+                            if insert_extra_newline {
+                                cursor.row -= 1;
+                                cursor.column = buffer.line_len(cursor.row);
+                            }
+                            let anchor = buffer.anchor_after(cursor);
+                            new_selection.start = anchor.clone();
+                            new_selection.end = anchor;
+                            new_selection
+                        },
+                    )
+                    .collect();
+            });
 
-            let buffer = buffer.read(cx);
-            self.selections = self
-                .selections
-                .iter()
-                .cloned()
-                .zip(old_selections)
-                .map(
-                    |(mut new_selection, (_, end_anchor, _, _, insert_extra_newline))| {
-                        let mut cursor = end_anchor.to_point(&buffer);
-                        if insert_extra_newline {
-                            cursor.row -= 1;
-                            cursor.column = buffer.line_len(cursor.row);
-                        }
-                        let anchor = buffer.anchor_after(cursor);
-                        new_selection.start = anchor.clone();
-                        new_selection.end = anchor;
-                        new_selection
-                    },
-                )
-                .collect();
+            this.request_autoscroll(Autoscroll::Fit, cx);
         });
 
-        self.request_autoscroll(Autoscroll::Fit, cx);
-        self.end_transaction(cx);
-
         #[derive(Default)]
         struct PendingEdit {
             indent: u32,

crates/editor/src/element.rs 🔗

@@ -21,7 +21,7 @@ use gpui::{
     MutableAppContext, PaintContext, Quad, Scene, SizeConstraint, ViewContext, WeakViewHandle,
 };
 use json::json;
-use language::Bias;
+use language::{Bias, DiagnosticSeverity};
 use smallvec::SmallVec;
 use std::{
     cmp::{self, Ordering},
@@ -665,19 +665,22 @@ impl EditorElement {
                     }
                 }
 
-                let mut diagnostic_highlight = HighlightStyle {
-                    ..Default::default()
-                };
+                let mut diagnostic_highlight = HighlightStyle::default();
 
                 if chunk.is_unnecessary {
                     diagnostic_highlight.fade_out = Some(style.unnecessary_code_fade);
-                } else if let Some(severity) = chunk.diagnostic_severity {
-                    let diagnostic_style = super::diagnostic_style(severity, true, style);
-                    diagnostic_highlight.underline = Some(Underline {
-                        color: Some(diagnostic_style.message.text.color),
-                        thickness: 1.0.into(),
-                        squiggly: true,
-                    });
+                }
+
+                if let Some(severity) = chunk.diagnostic_severity {
+                    // Omit underlines for HINT/INFO diagnostics on 'unnecessary' code.
+                    if severity <= DiagnosticSeverity::WARNING || !chunk.is_unnecessary {
+                        let diagnostic_style = super::diagnostic_style(severity, true, style);
+                        diagnostic_highlight.underline = Some(Underline {
+                            color: Some(diagnostic_style.message.text.color),
+                            thickness: 1.0.into(),
+                            squiggly: true,
+                        });
+                    }
                 }
 
                 if let Some(highlight_style) = highlight_style.as_mut() {
@@ -906,7 +909,7 @@ impl Element for EditorElement {
                 .anchor_before(DisplayPoint::new(end_row, 0).to_offset(&snapshot, Bias::Right))
         };
 
-        let mut selections = HashMap::default();
+        let mut selections = Vec::new();
         let mut active_rows = BTreeMap::new();
         let mut highlighted_rows = None;
         let mut highlighted_ranges = Vec::new();
@@ -919,11 +922,32 @@ impl Element for EditorElement {
                 &display_map,
             );
 
+            let mut remote_selections = HashMap::default();
+            for (replica_id, selection) in display_map
+                .buffer_snapshot
+                .remote_selections_in_range(&(start_anchor.clone()..end_anchor.clone()))
+            {
+                // The local selections match the leader's selections.
+                if Some(replica_id) == view.leader_replica_id {
+                    continue;
+                }
+
+                remote_selections
+                    .entry(replica_id)
+                    .or_insert(Vec::new())
+                    .push(crate::Selection {
+                        id: selection.id,
+                        goal: selection.goal,
+                        reversed: selection.reversed,
+                        start: selection.start.to_display_point(&display_map),
+                        end: selection.end.to_display_point(&display_map),
+                    });
+            }
+            selections.extend(remote_selections);
+
             if view.show_local_selections {
-                let local_selections = view.local_selections_in_range(
-                    start_anchor.clone()..end_anchor.clone(),
-                    &display_map,
-                );
+                let local_selections =
+                    view.local_selections_in_range(start_anchor..end_anchor, &display_map);
                 for selection in &local_selections {
                     let is_empty = selection.start == selection.end;
                     let selection_start = snapshot.prev_line_boundary(selection.start).1;
@@ -936,8 +960,12 @@ impl Element for EditorElement {
                         *contains_non_empty_selection |= !is_empty;
                     }
                 }
-                selections.insert(
-                    view.replica_id(cx),
+
+                // Render the local selections in the leader's color when following.
+                let local_replica_id = view.leader_replica_id.unwrap_or(view.replica_id(cx));
+
+                selections.push((
+                    local_replica_id,
                     local_selections
                         .into_iter()
                         .map(|selection| crate::Selection {
@@ -948,23 +976,7 @@ impl Element for EditorElement {
                             end: selection.end.to_display_point(&display_map),
                         })
                         .collect(),
-                );
-            }
-
-            for (replica_id, selection) in display_map
-                .buffer_snapshot
-                .remote_selections_in_range(&(start_anchor..end_anchor))
-            {
-                selections
-                    .entry(replica_id)
-                    .or_insert(Vec::new())
-                    .push(crate::Selection {
-                        id: selection.id,
-                        goal: selection.goal,
-                        reversed: selection.reversed,
-                        start: selection.start.to_display_point(&display_map),
-                        end: selection.end.to_display_point(&display_map),
-                    });
+                ));
             }
         });
 
@@ -1210,7 +1222,7 @@ pub struct LayoutState {
     em_width: f32,
     em_advance: f32,
     highlighted_ranges: Vec<(Range<DisplayPoint>, Color)>,
-    selections: HashMap<ReplicaId, Vec<text::Selection<DisplayPoint>>>,
+    selections: Vec<(ReplicaId, Vec<text::Selection<DisplayPoint>>)>,
     context_menu: Option<(DisplayPoint, ElementBox)>,
     code_actions_indicator: Option<(u32, ElementBox)>,
 }
@@ -1280,7 +1292,7 @@ impl PaintState {
     }
 }
 
-#[derive(Copy, Clone)]
+#[derive(Copy, Clone, PartialEq, Eq)]
 pub enum CursorShape {
     Bar,
     Block,
@@ -1487,7 +1499,7 @@ mod tests {
 
     #[gpui::test]
     fn test_layout_line_numbers(cx: &mut gpui::MutableAppContext) {
-        cx.add_app_state(Settings::test(cx));
+        cx.set_global(Settings::test(cx));
         let buffer = MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx);
         let (window_id, editor) = cx.add_window(Default::default(), |cx| {
             Editor::new(EditorMode::Full, buffer, None, None, cx)
@@ -1509,7 +1521,7 @@ mod tests {
 
     #[gpui::test]
     fn test_layout_with_placeholder_text_and_blocks(cx: &mut gpui::MutableAppContext) {
-        cx.add_app_state(Settings::test(cx));
+        cx.set_global(Settings::test(cx));
         let buffer = MultiBuffer::build_simple("", cx);
         let (window_id, editor) = cx.add_window(Default::default(), |cx| {
             Editor::new(EditorMode::Full, buffer, None, None, cx)

crates/editor/src/items.rs 🔗

@@ -1,162 +1,251 @@
-use crate::{Autoscroll, Editor, Event, MultiBuffer, NavigationData, ToOffset, ToPoint as _};
-use anyhow::Result;
+use crate::{Anchor, Autoscroll, Editor, Event, ExcerptId, NavigationData, ToOffset, ToPoint as _};
+use anyhow::{anyhow, Result};
+use futures::FutureExt;
 use gpui::{
-    elements::*, AppContext, Entity, ModelContext, ModelHandle, MutableAppContext, RenderContext,
-    Subscription, Task, View, ViewContext, ViewHandle, WeakModelHandle,
+    elements::*, geometry::vector::vec2f, AppContext, Entity, ModelHandle, MutableAppContext,
+    RenderContext, Subscription, Task, View, ViewContext, ViewHandle,
 };
-use language::{Bias, Buffer, Diagnostic, File as _};
-use project::{File, Project, ProjectPath};
-use std::path::PathBuf;
-use std::rc::Rc;
-use std::{cell::RefCell, fmt::Write};
+use language::{Bias, Buffer, Diagnostic, File as _, SelectionGoal};
+use project::{File, Project, ProjectEntryId, ProjectPath};
+use rpc::proto::{self, update_view};
+use std::{fmt::Write, path::PathBuf, time::Duration};
 use text::{Point, Selection};
-use util::ResultExt;
+use util::TryFutureExt;
 use workspace::{
-    ItemHandle, ItemNavHistory, ItemView, ItemViewHandle, NavHistory, PathOpener, Settings,
-    StatusItemView, WeakItemHandle, Workspace,
+    FollowableItem, Item, ItemHandle, ItemNavHistory, ProjectItem, Settings, StatusItemView,
 };
 
-pub struct BufferOpener;
+pub const FORMAT_TIMEOUT: Duration = Duration::from_secs(2);
 
-#[derive(Clone)]
-pub struct BufferItemHandle(pub ModelHandle<Buffer>);
-
-#[derive(Clone)]
-struct WeakBufferItemHandle(WeakModelHandle<Buffer>);
-
-#[derive(Clone)]
-pub struct MultiBufferItemHandle(pub ModelHandle<MultiBuffer>);
-
-#[derive(Clone)]
-struct WeakMultiBufferItemHandle(WeakModelHandle<MultiBuffer>);
+impl FollowableItem for Editor {
+    fn from_state_proto(
+        pane: ViewHandle<workspace::Pane>,
+        project: ModelHandle<Project>,
+        state: &mut Option<proto::view::Variant>,
+        cx: &mut MutableAppContext,
+    ) -> Option<Task<Result<ViewHandle<Self>>>> {
+        let state = if matches!(state, Some(proto::view::Variant::Editor(_))) {
+            if let Some(proto::view::Variant::Editor(state)) = state.take() {
+                state
+            } else {
+                unreachable!()
+            }
+        } else {
+            return None;
+        };
 
-impl PathOpener for BufferOpener {
-    fn open(
-        &self,
-        project: &mut Project,
-        project_path: ProjectPath,
-        cx: &mut ModelContext<Project>,
-    ) -> Option<Task<Result<Box<dyn ItemHandle>>>> {
-        let buffer = project.open_buffer(project_path, cx);
-        let task = cx.spawn(|_, _| async move {
-            let buffer = buffer.await?;
-            Ok(Box::new(BufferItemHandle(buffer)) as Box<dyn ItemHandle>)
+        let buffer = project.update(cx, |project, cx| {
+            project.open_buffer_by_id(state.buffer_id, cx)
         });
-        Some(task)
-    }
-}
-
-impl ItemHandle for BufferItemHandle {
-    fn add_view(
-        &self,
-        window_id: usize,
-        workspace: &Workspace,
-        nav_history: Rc<RefCell<NavHistory>>,
-        cx: &mut MutableAppContext,
-    ) -> Box<dyn ItemViewHandle> {
-        let buffer = cx.add_model(|cx| MultiBuffer::singleton(self.0.clone(), cx));
-        Box::new(cx.add_view(window_id, |cx| {
-            let mut editor = Editor::for_buffer(buffer, Some(workspace.project().clone()), cx);
-            editor.nav_history = Some(ItemNavHistory::new(nav_history, &cx.handle()));
-            editor
+        Some(cx.spawn(|mut cx| async move {
+            let buffer = buffer.await?;
+            let editor = pane
+                .read_with(&cx, |pane, cx| {
+                    pane.items_of_type::<Self>().find(|editor| {
+                        editor.read(cx).buffer.read(cx).as_singleton().as_ref() == Some(&buffer)
+                    })
+                })
+                .unwrap_or_else(|| {
+                    cx.add_view(pane.window_id(), |cx| {
+                        Editor::for_buffer(buffer, Some(project), cx)
+                    })
+                });
+            editor.update(&mut cx, |editor, cx| {
+                let excerpt_id;
+                let buffer_id;
+                {
+                    let buffer = editor.buffer.read(cx).read(cx);
+                    let singleton = buffer.as_singleton().unwrap();
+                    excerpt_id = singleton.0.clone();
+                    buffer_id = singleton.1;
+                }
+                let selections = state
+                    .selections
+                    .into_iter()
+                    .map(|selection| {
+                        deserialize_selection(&excerpt_id, buffer_id, selection)
+                            .ok_or_else(|| anyhow!("invalid selection"))
+                    })
+                    .collect::<Result<Vec<_>>>()?;
+                if !selections.is_empty() {
+                    editor.set_selections_from_remote(selections.into(), cx);
+                }
+
+                if let Some(anchor) = state.scroll_top_anchor {
+                    editor.set_scroll_top_anchor(
+                        Anchor {
+                            buffer_id: Some(state.buffer_id as usize),
+                            excerpt_id: excerpt_id.clone(),
+                            text_anchor: language::proto::deserialize_anchor(anchor)
+                                .ok_or_else(|| anyhow!("invalid scroll top"))?,
+                        },
+                        vec2f(state.scroll_x, state.scroll_y),
+                        cx,
+                    );
+                }
+
+                Ok::<_, anyhow::Error>(())
+            })?;
+            Ok(editor)
         }))
     }
 
-    fn boxed_clone(&self) -> Box<dyn ItemHandle> {
-        Box::new(self.clone())
-    }
-
-    fn to_any(&self) -> gpui::AnyModelHandle {
-        self.0.clone().into()
-    }
-
-    fn downgrade(&self) -> Box<dyn workspace::WeakItemHandle> {
-        Box::new(WeakBufferItemHandle(self.0.downgrade()))
-    }
-
-    fn project_path(&self, cx: &AppContext) -> Option<ProjectPath> {
-        File::from_dyn(self.0.read(cx).file()).map(|f| ProjectPath {
-            worktree_id: f.worktree_id(cx),
-            path: f.path().clone(),
-        })
-    }
-
-    fn id(&self) -> usize {
-        self.0.id()
+    fn set_leader_replica_id(
+        &mut self,
+        leader_replica_id: Option<u16>,
+        cx: &mut ViewContext<Self>,
+    ) {
+        self.leader_replica_id = leader_replica_id;
+        if self.leader_replica_id.is_some() {
+            self.buffer.update(cx, |buffer, cx| {
+                buffer.remove_active_selections(cx);
+            });
+        } else {
+            self.buffer.update(cx, |buffer, cx| {
+                if self.focused {
+                    buffer.set_active_selections(&self.selections, cx);
+                }
+            });
+        }
+        cx.notify();
     }
-}
 
-impl ItemHandle for MultiBufferItemHandle {
-    fn add_view(
-        &self,
-        window_id: usize,
-        workspace: &Workspace,
-        nav_history: Rc<RefCell<NavHistory>>,
-        cx: &mut MutableAppContext,
-    ) -> Box<dyn ItemViewHandle> {
-        Box::new(cx.add_view(window_id, |cx| {
-            let mut editor =
-                Editor::for_buffer(self.0.clone(), Some(workspace.project().clone()), cx);
-            editor.nav_history = Some(ItemNavHistory::new(nav_history, &cx.handle()));
-            editor
+    fn to_state_proto(&self, cx: &AppContext) -> Option<proto::view::Variant> {
+        let buffer_id = self.buffer.read(cx).as_singleton()?.read(cx).remote_id();
+        Some(proto::view::Variant::Editor(proto::view::Editor {
+            buffer_id,
+            scroll_top_anchor: Some(language::proto::serialize_anchor(
+                &self.scroll_top_anchor.text_anchor,
+            )),
+            scroll_x: self.scroll_position.x(),
+            scroll_y: self.scroll_position.y(),
+            selections: self.selections.iter().map(serialize_selection).collect(),
         }))
     }
 
-    fn boxed_clone(&self) -> Box<dyn ItemHandle> {
-        Box::new(self.clone())
-    }
-
-    fn to_any(&self) -> gpui::AnyModelHandle {
-        self.0.clone().into()
-    }
-
-    fn downgrade(&self) -> Box<dyn WeakItemHandle> {
-        Box::new(WeakMultiBufferItemHandle(self.0.downgrade()))
+    fn add_event_to_update_proto(
+        &self,
+        event: &Self::Event,
+        update: &mut Option<proto::update_view::Variant>,
+        _: &AppContext,
+    ) -> bool {
+        let update =
+            update.get_or_insert_with(|| proto::update_view::Variant::Editor(Default::default()));
+
+        match update {
+            proto::update_view::Variant::Editor(update) => match event {
+                Event::ScrollPositionChanged { .. } => {
+                    update.scroll_top_anchor = Some(language::proto::serialize_anchor(
+                        &self.scroll_top_anchor.text_anchor,
+                    ));
+                    update.scroll_x = self.scroll_position.x();
+                    update.scroll_y = self.scroll_position.y();
+                    true
+                }
+                Event::SelectionsChanged { .. } => {
+                    update.selections = self
+                        .selections
+                        .iter()
+                        .chain(self.pending_selection.as_ref().map(|p| &p.selection))
+                        .map(serialize_selection)
+                        .collect();
+                    true
+                }
+                _ => false,
+            },
+        }
     }
 
-    fn project_path(&self, _: &AppContext) -> Option<ProjectPath> {
-        None
+    fn apply_update_proto(
+        &mut self,
+        message: update_view::Variant,
+        cx: &mut ViewContext<Self>,
+    ) -> Result<()> {
+        match message {
+            update_view::Variant::Editor(message) => {
+                let buffer = self.buffer.read(cx);
+                let buffer = buffer.read(cx);
+                let (excerpt_id, buffer_id, _) = buffer.as_singleton().unwrap();
+                let excerpt_id = excerpt_id.clone();
+                drop(buffer);
+
+                let selections = message
+                    .selections
+                    .into_iter()
+                    .filter_map(|selection| {
+                        deserialize_selection(&excerpt_id, buffer_id, selection)
+                    })
+                    .collect::<Vec<_>>();
+
+                if !selections.is_empty() {
+                    self.set_selections_from_remote(selections, cx);
+                    self.request_autoscroll_remotely(Autoscroll::Newest, cx);
+                } else {
+                    if let Some(anchor) = message.scroll_top_anchor {
+                        self.set_scroll_top_anchor(
+                            Anchor {
+                                buffer_id: Some(buffer_id),
+                                excerpt_id: excerpt_id.clone(),
+                                text_anchor: language::proto::deserialize_anchor(anchor)
+                                    .ok_or_else(|| anyhow!("invalid scroll top"))?,
+                            },
+                            vec2f(message.scroll_x, message.scroll_y),
+                            cx,
+                        );
+                    }
+                }
+            }
+        }
+        Ok(())
     }
 
-    fn id(&self) -> usize {
-        self.0.id()
+    fn should_unfollow_on_event(event: &Self::Event, _: &AppContext) -> bool {
+        match event {
+            Event::Edited => true,
+            Event::SelectionsChanged { local } => *local,
+            Event::ScrollPositionChanged { local } => *local,
+            _ => false,
+        }
     }
 }
 
-impl WeakItemHandle for WeakBufferItemHandle {
-    fn upgrade(&self, cx: &AppContext) -> Option<Box<dyn ItemHandle>> {
-        self.0
-            .upgrade(cx)
-            .map(|buffer| Box::new(BufferItemHandle(buffer)) as Box<dyn ItemHandle>)
-    }
-
-    fn id(&self) -> usize {
-        self.0.id()
+fn serialize_selection(selection: &Selection<Anchor>) -> proto::Selection {
+    proto::Selection {
+        id: selection.id as u64,
+        start: Some(language::proto::serialize_anchor(
+            &selection.start.text_anchor,
+        )),
+        end: Some(language::proto::serialize_anchor(
+            &selection.end.text_anchor,
+        )),
+        reversed: selection.reversed,
     }
 }
 
-impl WeakItemHandle for WeakMultiBufferItemHandle {
-    fn upgrade(&self, cx: &AppContext) -> Option<Box<dyn ItemHandle>> {
-        self.0
-            .upgrade(cx)
-            .map(|buffer| Box::new(MultiBufferItemHandle(buffer)) as Box<dyn ItemHandle>)
-    }
-
-    fn id(&self) -> usize {
-        self.0.id()
-    }
+fn deserialize_selection(
+    excerpt_id: &ExcerptId,
+    buffer_id: usize,
+    selection: proto::Selection,
+) -> Option<Selection<Anchor>> {
+    Some(Selection {
+        id: selection.id as usize,
+        start: Anchor {
+            buffer_id: Some(buffer_id),
+            excerpt_id: excerpt_id.clone(),
+            text_anchor: language::proto::deserialize_anchor(selection.start?)?,
+        },
+        end: Anchor {
+            buffer_id: Some(buffer_id),
+            excerpt_id: excerpt_id.clone(),
+            text_anchor: language::proto::deserialize_anchor(selection.end?)?,
+        },
+        reversed: selection.reversed,
+        goal: SelectionGoal::None,
+    })
 }
 
-impl ItemView for Editor {
-    fn item(&self, cx: &AppContext) -> Box<dyn ItemHandle> {
-        if let Some(buffer) = self.buffer.read(cx).as_singleton() {
-            Box::new(BufferItemHandle(buffer))
-        } else {
-            Box::new(MultiBufferItemHandle(self.buffer.clone()))
-        }
-    }
-
-    fn navigate(&mut self, data: Box<dyn std::any::Any>, cx: &mut ViewContext<Self>) {
+impl Item for Editor {
+    fn navigate(&mut self, data: Box<dyn std::any::Any>, cx: &mut ViewContext<Self>) -> bool {
         if let Some(data) = data.downcast_ref::<NavigationData>() {
             let buffer = self.buffer.read(cx).read(cx);
             let offset = if buffer.can_resolve(&data.anchor) {
@@ -164,11 +253,19 @@ impl ItemView for Editor {
             } else {
                 buffer.clip_offset(data.offset, Bias::Left)
             };
-
+            let newest_selection = self.newest_selection_with_snapshot::<usize>(&buffer);
             drop(buffer);
-            let nav_history = self.nav_history.take();
-            self.select_ranges([offset..offset], Some(Autoscroll::Fit), cx);
-            self.nav_history = nav_history;
+
+            if newest_selection.head() == offset {
+                false
+            } else {
+                let nav_history = self.nav_history.take();
+                self.select_ranges([offset..offset], Some(Autoscroll::Fit), cx);
+                self.nav_history = nav_history;
+                true
+            }
+        } else {
+            false
         }
     }
 
@@ -184,15 +281,19 @@ impl ItemView for Editor {
         })
     }
 
-    fn clone_on_split(
-        &self,
-        nav_history: ItemNavHistory,
-        cx: &mut ViewContext<Self>,
-    ) -> Option<Self>
+    fn project_entry_id(&self, cx: &AppContext) -> Option<ProjectEntryId> {
+        File::from_dyn(self.buffer().read(cx).file(cx)).and_then(|file| file.project_entry_id(cx))
+    }
+
+    fn clone_on_split(&self, cx: &mut ViewContext<Self>) -> Option<Self>
     where
         Self: Sized,
     {
-        Some(self.clone(nav_history, cx))
+        Some(self.clone(cx))
+    }
+
+    fn set_nav_history(&mut self, history: ItemNavHistory, _: &mut ViewContext<Self>) {
+        self.nav_history = Some(history);
     }
 
     fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
@@ -219,9 +320,17 @@ impl ItemView for Editor {
     ) -> Task<Result<()>> {
         let buffer = self.buffer().clone();
         let buffers = buffer.read(cx).all_buffers();
-        let transaction = project.update(cx, |project, cx| project.format(buffers, true, cx));
+        let mut timeout = cx.background().timer(FORMAT_TIMEOUT).fuse();
+        let format = project.update(cx, |project, cx| project.format(buffers, true, cx));
         cx.spawn(|this, mut cx| async move {
-            let transaction = transaction.await.log_err();
+            let transaction = futures::select_biased! {
+                _ = timeout => {
+                    log::warn!("timed out waiting for formatting");
+                    None
+                }
+                transaction = format.log_err().fuse() => transaction,
+            };
+
             this.update(&mut cx, |editor, cx| {
                 editor.request_autoscroll(Autoscroll::Fit, cx)
             });
@@ -275,6 +384,18 @@ impl ItemView for Editor {
     }
 }
 
+impl ProjectItem for Editor {
+    type Item = Buffer;
+
+    fn for_project_item(
+        project: ModelHandle<Project>,
+        buffer: ModelHandle<Buffer>,
+        cx: &mut ViewContext<Self>,
+    ) -> Self {
+        Self::for_buffer(buffer, Some(project), cx)
+    }
+}
+
 pub struct CursorPosition {
     position: Option<Point>,
     selected_count: usize,
@@ -322,7 +443,7 @@ impl View for CursorPosition {
 
     fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
         if let Some(position) = self.position {
-            let theme = &cx.app_state::<Settings>().theme.workspace.status_bar;
+            let theme = &cx.global::<Settings>().theme.workspace.status_bar;
             let mut text = format!("{},{}", position.row + 1, position.column + 1);
             if self.selected_count > 0 {
                 write!(text, " ({} selected)", self.selected_count).unwrap();
@@ -337,7 +458,7 @@ impl View for CursorPosition {
 impl StatusItemView for CursorPosition {
     fn set_active_pane_item(
         &mut self,
-        active_pane_item: Option<&dyn ItemViewHandle>,
+        active_pane_item: Option<&dyn ItemHandle>,
         cx: &mut ViewContext<Self>,
     ) {
         if let Some(editor) = active_pane_item.and_then(|item| item.downcast::<Editor>()) {
@@ -395,7 +516,7 @@ impl View for DiagnosticMessage {
 
     fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
         if let Some(diagnostic) = &self.diagnostic {
-            let theme = &cx.app_state::<Settings>().theme.workspace.status_bar;
+            let theme = &cx.global::<Settings>().theme.workspace.status_bar;
             Label::new(
                 diagnostic.message.split('\n').next().unwrap().to_string(),
                 theme.diagnostic_message.clone(),
@@ -410,7 +531,7 @@ impl View for DiagnosticMessage {
 impl StatusItemView for DiagnosticMessage {
     fn set_active_pane_item(
         &mut self,
-        active_pane_item: Option<&dyn ItemViewHandle>,
+        active_pane_item: Option<&dyn ItemHandle>,
         cx: &mut ViewContext<Self>,
     ) {
         if let Some(editor) = active_pane_item.and_then(|item| item.downcast::<Editor>()) {

crates/editor/src/movement.rs 🔗

@@ -1,20 +1,19 @@
 use super::{Bias, DisplayPoint, DisplaySnapshot, SelectionGoal, ToDisplayPoint};
 use crate::{char_kind, CharKind, ToPoint};
-use anyhow::Result;
 use language::Point;
 use std::ops::Range;
 
-pub fn left(map: &DisplaySnapshot, mut point: DisplayPoint) -> Result<DisplayPoint> {
+pub fn left(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
     if point.column() > 0 {
         *point.column_mut() -= 1;
     } else if point.row() > 0 {
         *point.row_mut() -= 1;
         *point.column_mut() = map.line_len(point.row());
     }
-    Ok(map.clip_point(point, Bias::Left))
+    map.clip_point(point, Bias::Left)
 }
 
-pub fn right(map: &DisplaySnapshot, mut point: DisplayPoint) -> Result<DisplayPoint> {
+pub fn right(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
     let max_column = map.line_len(point.row());
     if point.column() < max_column {
         *point.column_mut() += 1;
@@ -22,14 +21,14 @@ pub fn right(map: &DisplaySnapshot, mut point: DisplayPoint) -> Result<DisplayPo
         *point.row_mut() += 1;
         *point.column_mut() = 0;
     }
-    Ok(map.clip_point(point, Bias::Right))
+    map.clip_point(point, Bias::Right)
 }
 
 pub fn up(
     map: &DisplaySnapshot,
     start: DisplayPoint,
     goal: SelectionGoal,
-) -> Result<(DisplayPoint, SelectionGoal)> {
+) -> (DisplayPoint, SelectionGoal) {
     let mut goal_column = if let SelectionGoal::Column(column) = goal {
         column
     } else {
@@ -54,17 +53,17 @@ pub fn up(
         Bias::Right
     };
 
-    Ok((
+    (
         map.clip_point(point, clip_bias),
         SelectionGoal::Column(goal_column),
-    ))
+    )
 }
 
 pub fn down(
     map: &DisplaySnapshot,
     start: DisplayPoint,
     goal: SelectionGoal,
-) -> Result<(DisplayPoint, SelectionGoal)> {
+) -> (DisplayPoint, SelectionGoal) {
     let mut goal_column = if let SelectionGoal::Column(column) = goal {
         column
     } else {
@@ -86,10 +85,10 @@ pub fn down(
         Bias::Right
     };
 
-    Ok((
+    (
         map.clip_point(point, clip_bias),
         SelectionGoal::Column(goal_column),
-    ))
+    )
 }
 
 pub fn line_beginning(
@@ -132,68 +131,110 @@ pub fn line_end(
     }
 }
 
-pub fn prev_word_boundary(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
-    let mut line_start = 0;
-    if point.row() > 0 {
-        if let Some(indent) = map.soft_wrap_indent(point.row() - 1) {
-            line_start = indent;
-        }
-    }
+pub fn previous_word_start(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
+    find_preceding_boundary(map, point, |left, right| {
+        (char_kind(left) != char_kind(right) && !right.is_whitespace()) || left == '\n'
+    })
+}
 
-    if point.column() == line_start {
-        if point.row() == 0 {
-            return DisplayPoint::new(0, 0);
-        } else {
-            let row = point.row() - 1;
-            point = map.clip_point(DisplayPoint::new(row, map.line_len(row)), Bias::Left);
-        }
-    }
+pub fn previous_subword_start(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
+    find_preceding_boundary(map, point, |left, right| {
+        let is_word_start = char_kind(left) != char_kind(right) && !right.is_whitespace();
+        let is_subword_start =
+            left == '_' && right != '_' || left.is_lowercase() && right.is_uppercase();
+        is_word_start || is_subword_start || left == '\n'
+    })
+}
 
-    let mut boundary = DisplayPoint::new(point.row(), 0);
-    let mut column = 0;
-    let mut prev_char_kind = CharKind::Newline;
-    for c in map.chars_at(DisplayPoint::new(point.row(), 0)) {
-        if column >= point.column() {
-            break;
+pub fn next_word_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
+    find_boundary(map, point, |left, right| {
+        (char_kind(left) != char_kind(right) && !left.is_whitespace()) || right == '\n'
+    })
+}
+
+pub fn next_subword_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
+    find_boundary(map, point, |left, right| {
+        let is_word_end = (char_kind(left) != char_kind(right)) && !left.is_whitespace();
+        let is_subword_end =
+            left != '_' && right == '_' || left.is_lowercase() && right.is_uppercase();
+        is_word_end || is_subword_end || right == '\n'
+    })
+}
+
+/// Scans for a boundary from the start of each line preceding the given end point until a boundary
+/// is found, indicated by the given predicate returning true. The predicate is called with the
+/// character to the left and right of the candidate boundary location, and will be called with `\n`
+/// characters indicating the start or end of a line. If the predicate returns true multiple times
+/// on a line, the *rightmost* boundary is returned.
+pub fn find_preceding_boundary(
+    map: &DisplaySnapshot,
+    end: DisplayPoint,
+    mut is_boundary: impl FnMut(char, char) -> bool,
+) -> DisplayPoint {
+    let mut point = end;
+    loop {
+        *point.column_mut() = 0;
+        if point.row() > 0 {
+            if let Some(indent) = map.soft_wrap_indent(point.row() - 1) {
+                *point.column_mut() = indent;
+            }
         }
 
-        let char_kind = char_kind(c);
-        if char_kind != prev_char_kind
-            && char_kind != CharKind::Whitespace
-            && char_kind != CharKind::Newline
-        {
-            *boundary.column_mut() = column;
+        let mut boundary = None;
+        let mut prev_ch = if point.is_zero() { None } else { Some('\n') };
+        for ch in map.chars_at(point) {
+            if point >= end {
+                break;
+            }
+
+            if let Some(prev_ch) = prev_ch {
+                if is_boundary(prev_ch, ch) {
+                    boundary = Some(point);
+                }
+            }
+
+            if ch == '\n' {
+                break;
+            }
+
+            prev_ch = Some(ch);
+            *point.column_mut() += ch.len_utf8() as u32;
         }
 
-        prev_char_kind = char_kind;
-        column += c.len_utf8() as u32;
+        if let Some(boundary) = boundary {
+            return boundary;
+        } else if point.row() == 0 {
+            return DisplayPoint::zero();
+        } else {
+            *point.row_mut() -= 1;
+        }
     }
-    boundary
 }
 
-pub fn next_word_boundary(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
-    let mut prev_char_kind = None;
-    for c in map.chars_at(point) {
-        let char_kind = char_kind(c);
-        if let Some(prev_char_kind) = prev_char_kind {
-            if c == '\n' {
-                break;
-            }
-            if prev_char_kind != char_kind
-                && prev_char_kind != CharKind::Whitespace
-                && prev_char_kind != CharKind::Newline
-            {
+/// Scans for a boundary following the given start point until a boundary is found, indicated by the
+/// given predicate returning true. The predicate is called with the character to the left and right
+/// of the candidate boundary location, and will be called with `\n` characters indicating the start
+/// or end of a line.
+pub fn find_boundary(
+    map: &DisplaySnapshot,
+    mut point: DisplayPoint,
+    mut is_boundary: impl FnMut(char, char) -> bool,
+) -> DisplayPoint {
+    let mut prev_ch = None;
+    for ch in map.chars_at(point) {
+        if let Some(prev_ch) = prev_ch {
+            if is_boundary(prev_ch, ch) {
                 break;
             }
         }
 
-        if c == '\n' {
+        if ch == '\n' {
             *point.row_mut() += 1;
             *point.column_mut() = 0;
         } else {
-            *point.column_mut() += c.len_utf8() as u32;
+            *point.column_mut() += ch.len_utf8() as u32;
         }
-        prev_char_kind = Some(char_kind);
+        prev_ch = Some(ch);
     }
     map.clip_point(point, Bias::Right)
 }
@@ -225,9 +266,205 @@ pub fn surrounding_word(map: &DisplaySnapshot, position: DisplayPoint) -> Range<
 #[cfg(test)]
 mod tests {
     use super::*;
-    use crate::{Buffer, DisplayMap, MultiBuffer};
+    use crate::{test::marked_display_snapshot, Buffer, DisplayMap, MultiBuffer};
     use language::Point;
 
+    #[gpui::test]
+    fn test_previous_word_start(cx: &mut gpui::MutableAppContext) {
+        fn assert(marked_text: &str, cx: &mut gpui::MutableAppContext) {
+            let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
+            assert_eq!(
+                previous_word_start(&snapshot, display_points[1]),
+                display_points[0]
+            );
+        }
+
+        assert("\n|   |lorem", cx);
+        assert("|\n|   lorem", cx);
+        assert("    |lorem|", cx);
+        assert("|    |lorem", cx);
+        assert("    |lor|em", cx);
+        assert("\nlorem\n|   |ipsum", cx);
+        assert("\n\n|\n|", cx);
+        assert("    |lorem  |ipsum", cx);
+        assert("lorem|-|ipsum", cx);
+        assert("lorem|-#$@|ipsum", cx);
+        assert("|lorem_|ipsum", cx);
+        assert(" |defγ|", cx);
+        assert(" |bcΔ|", cx);
+        assert(" ab|——|cd", cx);
+    }
+
+    #[gpui::test]
+    fn test_previous_subword_start(cx: &mut gpui::MutableAppContext) {
+        fn assert(marked_text: &str, cx: &mut gpui::MutableAppContext) {
+            let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
+            assert_eq!(
+                previous_subword_start(&snapshot, display_points[1]),
+                display_points[0]
+            );
+        }
+
+        // Subword boundaries are respected
+        assert("lorem_|ip|sum", cx);
+        assert("lorem_|ipsum|", cx);
+        assert("|lorem_|ipsum", cx);
+        assert("lorem_|ipsum_|dolor", cx);
+        assert("lorem|Ip|sum", cx);
+        assert("lorem|Ipsum|", cx);
+
+        // Word boundaries are still respected
+        assert("\n|   |lorem", cx);
+        assert("    |lorem|", cx);
+        assert("    |lor|em", cx);
+        assert("\nlorem\n|   |ipsum", cx);
+        assert("\n\n|\n|", cx);
+        assert("    |lorem  |ipsum", cx);
+        assert("lorem|-|ipsum", cx);
+        assert("lorem|-#$@|ipsum", cx);
+        assert(" |defγ|", cx);
+        assert(" bc|Δ|", cx);
+        assert(" |bcδ|", cx);
+        assert(" ab|——|cd", cx);
+    }
+
+    #[gpui::test]
+    fn test_find_preceding_boundary(cx: &mut gpui::MutableAppContext) {
+        fn assert(
+            marked_text: &str,
+            cx: &mut gpui::MutableAppContext,
+            is_boundary: impl FnMut(char, char) -> bool,
+        ) {
+            let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
+            assert_eq!(
+                find_preceding_boundary(&snapshot, display_points[1], is_boundary),
+                display_points[0]
+            );
+        }
+
+        assert("abc|def\ngh\nij|k", cx, |left, right| {
+            left == 'c' && right == 'd'
+        });
+        assert("abcdef\n|gh\nij|k", cx, |left, right| {
+            left == '\n' && right == 'g'
+        });
+        let mut line_count = 0;
+        assert("abcdef\n|gh\nij|k", cx, |left, _| {
+            if left == '\n' {
+                line_count += 1;
+                line_count == 2
+            } else {
+                false
+            }
+        });
+    }
+
+    #[gpui::test]
+    fn test_next_word_end(cx: &mut gpui::MutableAppContext) {
+        fn assert(marked_text: &str, cx: &mut gpui::MutableAppContext) {
+            let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
+            assert_eq!(
+                next_word_end(&snapshot, display_points[0]),
+                display_points[1]
+            );
+        }
+
+        assert("\n|   lorem|", cx);
+        assert("    |lorem|", cx);
+        assert("    lor|em|", cx);
+        assert("    lorem|    |\nipsum\n", cx);
+        assert("\n|\n|\n\n", cx);
+        assert("lorem|    ipsum|   ", cx);
+        assert("lorem|-|ipsum", cx);
+        assert("lorem|#$@-|ipsum", cx);
+        assert("lorem|_ipsum|", cx);
+        assert(" |bcΔ|", cx);
+        assert(" ab|——|cd", cx);
+    }
+
+    #[gpui::test]
+    fn test_next_subword_end(cx: &mut gpui::MutableAppContext) {
+        fn assert(marked_text: &str, cx: &mut gpui::MutableAppContext) {
+            let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
+            assert_eq!(
+                next_subword_end(&snapshot, display_points[0]),
+                display_points[1]
+            );
+        }
+
+        // Subword boundaries are respected
+        assert("lo|rem|_ipsum", cx);
+        assert("|lorem|_ipsum", cx);
+        assert("lorem|_ipsum|", cx);
+        assert("lorem|_ipsum|_dolor", cx);
+        assert("lo|rem|Ipsum", cx);
+        assert("lorem|Ipsum|Dolor", cx);
+
+        // Word boundaries are still respected
+        assert("\n|   lorem|", cx);
+        assert("    |lorem|", cx);
+        assert("    lor|em|", cx);
+        assert("    lorem|    |\nipsum\n", cx);
+        assert("\n|\n|\n\n", cx);
+        assert("lorem|    ipsum|   ", cx);
+        assert("lorem|-|ipsum", cx);
+        assert("lorem|#$@-|ipsum", cx);
+        assert("lorem|_ipsum|", cx);
+        assert(" |bc|Δ", cx);
+        assert(" ab|——|cd", cx);
+    }
+
+    #[gpui::test]
+    fn test_find_boundary(cx: &mut gpui::MutableAppContext) {
+        fn assert(
+            marked_text: &str,
+            cx: &mut gpui::MutableAppContext,
+            is_boundary: impl FnMut(char, char) -> bool,
+        ) {
+            let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
+            assert_eq!(
+                find_boundary(&snapshot, display_points[0], is_boundary),
+                display_points[1]
+            );
+        }
+
+        assert("abc|def\ngh\nij|k", cx, |left, right| {
+            left == 'j' && right == 'k'
+        });
+        assert("ab|cdef\ngh\n|ijk", cx, |left, right| {
+            left == '\n' && right == 'i'
+        });
+        let mut line_count = 0;
+        assert("abc|def\ngh\n|ijk", cx, |left, _| {
+            if left == '\n' {
+                line_count += 1;
+                line_count == 2
+            } else {
+                false
+            }
+        });
+    }
+
+    #[gpui::test]
+    fn test_surrounding_word(cx: &mut gpui::MutableAppContext) {
+        fn assert(marked_text: &str, cx: &mut gpui::MutableAppContext) {
+            let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
+            assert_eq!(
+                surrounding_word(&snapshot, display_points[1]),
+                display_points[0]..display_points[2]
+            );
+        }
+
+        assert("||lorem|  ipsum", cx);
+        assert("|lo|rem|  ipsum", cx);
+        assert("|lorem||  ipsum", cx);
+        assert("lorem| |  |ipsum", cx);
+        assert("lorem\n|||\nipsum", cx);
+        assert("lorem\n||ipsum|", cx);
+        assert("lorem,|| |ipsum", cx);
+        assert("|lorem||, ipsum", cx);
+    }
+
     #[gpui::test]
     fn test_move_up_and_down_with_excerpts(cx: &mut gpui::MutableAppContext) {
         let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap();
@@ -249,180 +486,50 @@ mod tests {
             );
             multibuffer
         });
-
         let display_map =
             cx.add_model(|cx| DisplayMap::new(multibuffer, 2, font_id, 14.0, None, 2, 2, cx));
-
         let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx));
+
         assert_eq!(snapshot.text(), "\n\nabc\ndefg\n\n\nhijkl\nmn");
 
         // Can't move up into the first excerpt's header
         assert_eq!(
-            up(&snapshot, DisplayPoint::new(2, 2), SelectionGoal::Column(2)).unwrap(),
+            up(&snapshot, DisplayPoint::new(2, 2), SelectionGoal::Column(2)),
             (DisplayPoint::new(2, 0), SelectionGoal::Column(0)),
         );
         assert_eq!(
-            up(&snapshot, DisplayPoint::new(2, 0), SelectionGoal::None).unwrap(),
+            up(&snapshot, DisplayPoint::new(2, 0), SelectionGoal::None),
             (DisplayPoint::new(2, 0), SelectionGoal::Column(0)),
         );
 
         // Move up and down within first excerpt
         assert_eq!(
-            up(&snapshot, DisplayPoint::new(3, 4), SelectionGoal::Column(4)).unwrap(),
+            up(&snapshot, DisplayPoint::new(3, 4), SelectionGoal::Column(4)),
             (DisplayPoint::new(2, 3), SelectionGoal::Column(4)),
         );
         assert_eq!(
-            down(&snapshot, DisplayPoint::new(2, 3), SelectionGoal::Column(4)).unwrap(),
+            down(&snapshot, DisplayPoint::new(2, 3), SelectionGoal::Column(4)),
             (DisplayPoint::new(3, 4), SelectionGoal::Column(4)),
         );
 
         // Move up and down across second excerpt's header
         assert_eq!(
-            up(&snapshot, DisplayPoint::new(6, 5), SelectionGoal::Column(5)).unwrap(),
+            up(&snapshot, DisplayPoint::new(6, 5), SelectionGoal::Column(5)),
             (DisplayPoint::new(3, 4), SelectionGoal::Column(5)),
         );
         assert_eq!(
-            down(&snapshot, DisplayPoint::new(3, 4), SelectionGoal::Column(5)).unwrap(),
+            down(&snapshot, DisplayPoint::new(3, 4), SelectionGoal::Column(5)),
             (DisplayPoint::new(6, 5), SelectionGoal::Column(5)),
         );
 
         // Can't move down off the end
         assert_eq!(
-            down(&snapshot, DisplayPoint::new(7, 0), SelectionGoal::Column(0)).unwrap(),
+            down(&snapshot, DisplayPoint::new(7, 0), SelectionGoal::Column(0)),
             (DisplayPoint::new(7, 2), SelectionGoal::Column(2)),
         );
         assert_eq!(
-            down(&snapshot, DisplayPoint::new(7, 2), SelectionGoal::Column(2)).unwrap(),
+            down(&snapshot, DisplayPoint::new(7, 2), SelectionGoal::Column(2)),
             (DisplayPoint::new(7, 2), SelectionGoal::Column(2)),
         );
     }
-
-    #[gpui::test]
-    fn test_prev_next_word_boundary_multibyte(cx: &mut gpui::MutableAppContext) {
-        let tab_size = 4;
-        let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap();
-        let font_id = cx
-            .font_cache()
-            .select_font(family_id, &Default::default())
-            .unwrap();
-        let font_size = 14.0;
-
-        let buffer = MultiBuffer::build_simple("a bcΔ defγ hi—jk", cx);
-        let display_map = cx
-            .add_model(|cx| DisplayMap::new(buffer, tab_size, font_id, font_size, None, 1, 1, cx));
-        let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx));
-        assert_eq!(
-            prev_word_boundary(&snapshot, DisplayPoint::new(0, 12)),
-            DisplayPoint::new(0, 7)
-        );
-        assert_eq!(
-            prev_word_boundary(&snapshot, DisplayPoint::new(0, 7)),
-            DisplayPoint::new(0, 2)
-        );
-        assert_eq!(
-            prev_word_boundary(&snapshot, DisplayPoint::new(0, 6)),
-            DisplayPoint::new(0, 2)
-        );
-        assert_eq!(
-            prev_word_boundary(&snapshot, DisplayPoint::new(0, 2)),
-            DisplayPoint::new(0, 0)
-        );
-        assert_eq!(
-            prev_word_boundary(&snapshot, DisplayPoint::new(0, 1)),
-            DisplayPoint::new(0, 0)
-        );
-
-        assert_eq!(
-            next_word_boundary(&snapshot, DisplayPoint::new(0, 0)),
-            DisplayPoint::new(0, 1)
-        );
-        assert_eq!(
-            next_word_boundary(&snapshot, DisplayPoint::new(0, 1)),
-            DisplayPoint::new(0, 6)
-        );
-        assert_eq!(
-            next_word_boundary(&snapshot, DisplayPoint::new(0, 2)),
-            DisplayPoint::new(0, 6)
-        );
-        assert_eq!(
-            next_word_boundary(&snapshot, DisplayPoint::new(0, 6)),
-            DisplayPoint::new(0, 12)
-        );
-        assert_eq!(
-            next_word_boundary(&snapshot, DisplayPoint::new(0, 7)),
-            DisplayPoint::new(0, 12)
-        );
-    }
-
-    #[gpui::test]
-    fn test_surrounding_word(cx: &mut gpui::MutableAppContext) {
-        let tab_size = 4;
-        let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap();
-        let font_id = cx
-            .font_cache()
-            .select_font(family_id, &Default::default())
-            .unwrap();
-        let font_size = 14.0;
-        let buffer = MultiBuffer::build_simple("lorem ipsum   dolor\n    sit", cx);
-        let display_map = cx
-            .add_model(|cx| DisplayMap::new(buffer, tab_size, font_id, font_size, None, 1, 1, cx));
-        let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx));
-
-        assert_eq!(
-            surrounding_word(&snapshot, DisplayPoint::new(0, 0)),
-            DisplayPoint::new(0, 0)..DisplayPoint::new(0, 5),
-        );
-        assert_eq!(
-            surrounding_word(&snapshot, DisplayPoint::new(0, 2)),
-            DisplayPoint::new(0, 0)..DisplayPoint::new(0, 5),
-        );
-        assert_eq!(
-            surrounding_word(&snapshot, DisplayPoint::new(0, 5)),
-            DisplayPoint::new(0, 0)..DisplayPoint::new(0, 5),
-        );
-        assert_eq!(
-            surrounding_word(&snapshot, DisplayPoint::new(0, 6)),
-            DisplayPoint::new(0, 6)..DisplayPoint::new(0, 11),
-        );
-        assert_eq!(
-            surrounding_word(&snapshot, DisplayPoint::new(0, 7)),
-            DisplayPoint::new(0, 6)..DisplayPoint::new(0, 11),
-        );
-        assert_eq!(
-            surrounding_word(&snapshot, DisplayPoint::new(0, 11)),
-            DisplayPoint::new(0, 6)..DisplayPoint::new(0, 11),
-        );
-        assert_eq!(
-            surrounding_word(&snapshot, DisplayPoint::new(0, 13)),
-            DisplayPoint::new(0, 11)..DisplayPoint::new(0, 14),
-        );
-        assert_eq!(
-            surrounding_word(&snapshot, DisplayPoint::new(0, 14)),
-            DisplayPoint::new(0, 14)..DisplayPoint::new(0, 19),
-        );
-        assert_eq!(
-            surrounding_word(&snapshot, DisplayPoint::new(0, 17)),
-            DisplayPoint::new(0, 14)..DisplayPoint::new(0, 19),
-        );
-        assert_eq!(
-            surrounding_word(&snapshot, DisplayPoint::new(0, 19)),
-            DisplayPoint::new(0, 14)..DisplayPoint::new(0, 19),
-        );
-        assert_eq!(
-            surrounding_word(&snapshot, DisplayPoint::new(1, 0)),
-            DisplayPoint::new(1, 0)..DisplayPoint::new(1, 4),
-        );
-        assert_eq!(
-            surrounding_word(&snapshot, DisplayPoint::new(1, 1)),
-            DisplayPoint::new(1, 0)..DisplayPoint::new(1, 4),
-        );
-        assert_eq!(
-            surrounding_word(&snapshot, DisplayPoint::new(1, 6)),
-            DisplayPoint::new(1, 4)..DisplayPoint::new(1, 7),
-        );
-        assert_eq!(
-            surrounding_word(&snapshot, DisplayPoint::new(1, 7)),
-            DisplayPoint::new(1, 4)..DisplayPoint::new(1, 7),
-        );
-    }
 }

crates/editor/src/multi_buffer.rs 🔗

@@ -211,7 +211,7 @@ impl MultiBuffer {
     pub fn singleton(buffer: ModelHandle<Buffer>, cx: &mut ModelContext<Self>) -> Self {
         let mut this = Self::new(buffer.read(cx).replica_id());
         this.singleton = true;
-        this.push_excerpts(buffer, [text::Anchor::min()..text::Anchor::max()], cx);
+        this.push_excerpts(buffer, [text::Anchor::MIN..text::Anchor::MAX], cx);
         this.snapshot.borrow_mut().singleton = true;
         this
     }
@@ -522,24 +522,14 @@ impl MultiBuffer {
             self.buffers.borrow()[&buffer_id]
                 .buffer
                 .update(cx, |buffer, cx| {
-                    selections.sort_unstable_by(|a, b| a.start.cmp(&b.start, buffer).unwrap());
+                    selections.sort_unstable_by(|a, b| a.start.cmp(&b.start, buffer));
                     let mut selections = selections.into_iter().peekable();
                     let merged_selections = Arc::from_iter(iter::from_fn(|| {
                         let mut selection = selections.next()?;
                         while let Some(next_selection) = selections.peek() {
-                            if selection
-                                .end
-                                .cmp(&next_selection.start, buffer)
-                                .unwrap()
-                                .is_ge()
-                            {
+                            if selection.end.cmp(&next_selection.start, buffer).is_ge() {
                                 let next_selection = selections.next().unwrap();
-                                if next_selection
-                                    .end
-                                    .cmp(&selection.end, buffer)
-                                    .unwrap()
-                                    .is_ge()
-                                {
+                                if next_selection.end.cmp(&selection.end, buffer).is_ge() {
                                     selection.end = next_selection.end;
                                 }
                             } else {
@@ -814,11 +804,38 @@ impl MultiBuffer {
         cx.notify();
     }
 
-    pub fn excerpt_ids_for_buffer(&self, buffer: &ModelHandle<Buffer>) -> Vec<ExcerptId> {
+    pub fn excerpts_for_buffer(
+        &self,
+        buffer: &ModelHandle<Buffer>,
+        cx: &AppContext,
+    ) -> Vec<(ExcerptId, Range<text::Anchor>)> {
+        let mut excerpts = Vec::new();
+        let snapshot = self.read(cx);
+        let buffers = self.buffers.borrow();
+        let mut cursor = snapshot.excerpts.cursor::<Option<&ExcerptId>>();
+        for excerpt_id in buffers
+            .get(&buffer.id())
+            .map(|state| &state.excerpts)
+            .into_iter()
+            .flatten()
+        {
+            cursor.seek_forward(&Some(excerpt_id), Bias::Left, &());
+            if let Some(excerpt) = cursor.item() {
+                if excerpt.id == *excerpt_id {
+                    excerpts.push((excerpt.id.clone(), excerpt.range.clone()));
+                }
+            }
+        }
+
+        excerpts
+    }
+
+    pub fn excerpt_ids(&self) -> Vec<ExcerptId> {
         self.buffers
             .borrow()
-            .get(&buffer.id())
-            .map_or(Vec::new(), |state| state.excerpts.clone())
+            .values()
+            .flat_map(|state| state.excerpts.iter().cloned())
+            .collect()
     }
 
     pub fn excerpt_containing(
@@ -1407,7 +1424,7 @@ impl MultiBufferSnapshot {
         );
 
         for ch in prev_chars {
-            if Some(char_kind(ch)) == word_kind {
+            if Some(char_kind(ch)) == word_kind && ch != '\n' {
                 start -= ch.len_utf8();
             } else {
                 break;
@@ -1415,7 +1432,7 @@ impl MultiBufferSnapshot {
         }
 
         for ch in next_chars {
-            if Some(char_kind(ch)) == word_kind {
+            if Some(char_kind(ch)) == word_kind && ch != '\n' {
                 end += ch.len_utf8();
             } else {
                 break;
@@ -1909,11 +1926,7 @@ impl MultiBufferSnapshot {
                             .range
                             .start
                             .bias(anchor.text_anchor.bias, &excerpt.buffer);
-                        if text_anchor
-                            .cmp(&excerpt.range.end, &excerpt.buffer)
-                            .unwrap()
-                            .is_gt()
-                        {
+                        if text_anchor.cmp(&excerpt.range.end, &excerpt.buffer).is_gt() {
                             text_anchor = excerpt.range.end.clone();
                         }
                         Anchor {
@@ -1928,7 +1941,6 @@ impl MultiBufferSnapshot {
                             .bias(anchor.text_anchor.bias, &excerpt.buffer);
                         if text_anchor
                             .cmp(&excerpt.range.start, &excerpt.buffer)
-                            .unwrap()
                             .is_lt()
                         {
                             text_anchor = excerpt.range.start.clone();
@@ -1948,7 +1960,7 @@ impl MultiBufferSnapshot {
                 result.push((anchor_ix, anchor, kept_position));
             }
         }
-        result.sort_unstable_by(|a, b| a.1.cmp(&b.1, self).unwrap());
+        result.sort_unstable_by(|a, b| a.1.cmp(&b.1, self));
         result
     }
 
@@ -2295,10 +2307,10 @@ impl MultiBufferSnapshot {
                                 excerpt_id: excerpt.id.clone(),
                                 text_anchor: selection.end.clone(),
                             };
-                            if range.start.cmp(&start, self).unwrap().is_gt() {
+                            if range.start.cmp(&start, self).is_gt() {
                                 start = range.start.clone();
                             }
-                            if range.end.cmp(&end, self).unwrap().is_lt() {
+                            if range.end.cmp(&end, self).is_lt() {
                                 end = range.end.clone();
                             }
 
@@ -2522,17 +2534,9 @@ impl Excerpt {
     }
 
     fn clip_anchor(&self, text_anchor: text::Anchor) -> text::Anchor {
-        if text_anchor
-            .cmp(&self.range.start, &self.buffer)
-            .unwrap()
-            .is_lt()
-        {
+        if text_anchor.cmp(&self.range.start, &self.buffer).is_lt() {
             self.range.start.clone()
-        } else if text_anchor
-            .cmp(&self.range.end, &self.buffer)
-            .unwrap()
-            .is_gt()
-        {
+        } else if text_anchor.cmp(&self.range.end, &self.buffer).is_gt() {
             self.range.end.clone()
         } else {
             text_anchor
@@ -2545,13 +2549,11 @@ impl Excerpt {
                 .range
                 .start
                 .cmp(&anchor.text_anchor, &self.buffer)
-                .unwrap()
                 .is_le()
             && self
                 .range
                 .end
                 .cmp(&anchor.text_anchor, &self.buffer)
-                .unwrap()
                 .is_ge()
     }
 }
@@ -3062,7 +3064,8 @@ mod tests {
         );
 
         let snapshot = multibuffer.update(cx, |multibuffer, cx| {
-            let buffer_2_excerpt_id = multibuffer.excerpt_ids_for_buffer(&buffer_2)[0].clone();
+            let (buffer_2_excerpt_id, _) =
+                multibuffer.excerpts_for_buffer(&buffer_2, cx)[0].clone();
             multibuffer.remove_excerpts(&[buffer_2_excerpt_id], cx);
             multibuffer.snapshot(cx)
         });
@@ -3357,7 +3360,7 @@ mod tests {
                     let bias = if rng.gen() { Bias::Left } else { Bias::Right };
                     log::info!("Creating anchor at {} with bias {:?}", offset, bias);
                     anchors.push(multibuffer.anchor_at(offset, bias));
-                    anchors.sort_by(|a, b| a.cmp(&b, &multibuffer).unwrap());
+                    anchors.sort_by(|a, b| a.cmp(&b, &multibuffer));
                 }
                 40..=44 if !anchors.is_empty() => {
                     let multibuffer = multibuffer.read(cx).read(cx);

crates/editor/src/multi_buffer/anchor.rs 🔗

@@ -1,5 +1,4 @@
 use super::{ExcerptId, MultiBufferSnapshot, ToOffset, ToPoint};
-use anyhow::Result;
 use std::{
     cmp::Ordering,
     ops::{Range, Sub},
@@ -19,7 +18,7 @@ impl Anchor {
         Self {
             buffer_id: None,
             excerpt_id: ExcerptId::min(),
-            text_anchor: text::Anchor::min(),
+            text_anchor: text::Anchor::MIN,
         }
     }
 
@@ -27,7 +26,7 @@ impl Anchor {
         Self {
             buffer_id: None,
             excerpt_id: ExcerptId::max(),
-            text_anchor: text::Anchor::max(),
+            text_anchor: text::Anchor::MAX,
         }
     }
 
@@ -35,18 +34,18 @@ impl Anchor {
         &self.excerpt_id
     }
 
-    pub fn cmp<'a>(&self, other: &Anchor, snapshot: &MultiBufferSnapshot) -> Result<Ordering> {
+    pub fn cmp<'a>(&self, other: &Anchor, snapshot: &MultiBufferSnapshot) -> Ordering {
         let excerpt_id_cmp = self.excerpt_id.cmp(&other.excerpt_id);
         if excerpt_id_cmp.is_eq() {
             if self.excerpt_id == ExcerptId::min() || self.excerpt_id == ExcerptId::max() {
-                Ok(Ordering::Equal)
+                Ordering::Equal
             } else if let Some(excerpt) = snapshot.excerpt(&self.excerpt_id) {
                 self.text_anchor.cmp(&other.text_anchor, &excerpt.buffer)
             } else {
-                Ok(Ordering::Equal)
+                Ordering::Equal
             }
         } else {
-            Ok(excerpt_id_cmp)
+            excerpt_id_cmp
         }
     }
 
@@ -97,17 +96,17 @@ impl ToPoint for Anchor {
 }
 
 pub trait AnchorRangeExt {
-    fn cmp(&self, b: &Range<Anchor>, buffer: &MultiBufferSnapshot) -> Result<Ordering>;
+    fn cmp(&self, b: &Range<Anchor>, buffer: &MultiBufferSnapshot) -> Ordering;
     fn to_offset(&self, content: &MultiBufferSnapshot) -> Range<usize>;
     fn to_point(&self, content: &MultiBufferSnapshot) -> Range<Point>;
 }
 
 impl AnchorRangeExt for Range<Anchor> {
-    fn cmp(&self, other: &Range<Anchor>, buffer: &MultiBufferSnapshot) -> Result<Ordering> {
-        Ok(match self.start.cmp(&other.start, buffer)? {
-            Ordering::Equal => other.end.cmp(&self.end, buffer)?,
+    fn cmp(&self, other: &Range<Anchor>, buffer: &MultiBufferSnapshot) -> Ordering {
+        match self.start.cmp(&other.start, buffer) {
+            Ordering::Equal => other.end.cmp(&self.end, buffer),
             ord @ _ => ord,
-        })
+        }
     }
 
     fn to_offset(&self, content: &MultiBufferSnapshot) -> Range<usize> {

crates/editor/src/test.rs 🔗

@@ -1,3 +1,10 @@
+use util::test::marked_text;
+
+use crate::{
+    display_map::{DisplayMap, DisplaySnapshot, ToDisplayPoint},
+    DisplayPoint, MultiBuffer,
+};
+
 #[cfg(test)]
 #[ctor::ctor]
 fn init_logger() {
@@ -5,3 +12,30 @@ fn init_logger() {
         env_logger::init();
     }
 }
+
+// Returns a snapshot from text containing '|' character markers with the markers removed, and DisplayPoints for each one.
+pub fn marked_display_snapshot(
+    text: &str,
+    cx: &mut gpui::MutableAppContext,
+) -> (DisplaySnapshot, Vec<DisplayPoint>) {
+    let (unmarked_text, markers) = marked_text(text);
+
+    let tab_size = 4;
+    let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap();
+    let font_id = cx
+        .font_cache()
+        .select_font(family_id, &Default::default())
+        .unwrap();
+    let font_size = 14.0;
+
+    let buffer = MultiBuffer::build_simple(&unmarked_text, cx);
+    let display_map =
+        cx.add_model(|cx| DisplayMap::new(buffer, tab_size, font_id, font_size, None, 1, 1, cx));
+    let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx));
+    let markers = markers
+        .into_iter()
+        .map(|offset| offset.to_display_point(&snapshot))
+        .collect();
+
+    (snapshot, markers)
+}

crates/file_finder/Cargo.toml 🔗

@@ -21,3 +21,5 @@ postage = { version = "0.4.1", features = ["futures-traits"] }
 gpui = { path = "../gpui", features = ["test-support"] }
 serde_json = { version = "1.0.64", features = ["preserve_order"] }
 workspace = { path = "../workspace", features = ["test-support"] }
+ctor = "0.1"
+env_logger = "0.8"

crates/file_finder/src/file_finder.rs 🔗

@@ -67,7 +67,7 @@ impl View for FileFinder {
     }
 
     fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
-        let settings = cx.app_state::<Settings>();
+        let settings = cx.global::<Settings>();
         Align::new(
             ConstrainedBox::new(
                 Container::new(
@@ -106,7 +106,7 @@ impl View for FileFinder {
 impl FileFinder {
     fn render_matches(&self, cx: &AppContext) -> ElementBox {
         if self.matches.is_empty() {
-            let settings = cx.app_state::<Settings>();
+            let settings = cx.global::<Settings>();
             return Container::new(
                 Label::new(
                     "No matches".into(),
@@ -142,7 +142,7 @@ impl FileFinder {
 
     fn render_match(&self, path_match: &PathMatch, index: usize, cx: &AppContext) -> ElementBox {
         let selected_index = self.selected_index();
-        let settings = cx.app_state::<Settings>();
+        let settings = cx.global::<Settings>();
         let style = if index == selected_index {
             &settings.theme.selector.active_item
         } else {
@@ -291,7 +291,7 @@ impl FileFinder {
         cx: &mut ViewContext<Self>,
     ) {
         match event {
-            editor::Event::Edited => {
+            editor::Event::BufferEdited { .. } => {
                 let query = self.query_editor.update(cx, |buffer, cx| buffer.text(cx));
                 if query.is_empty() {
                     self.latest_search_id = post_inc(&mut self.search_count);
@@ -407,16 +407,21 @@ mod tests {
     use std::path::PathBuf;
     use workspace::{Workspace, WorkspaceParams};
 
+    #[ctor::ctor]
+    fn init_logger() {
+        if std::env::var("RUST_LOG").is_ok() {
+            env_logger::init();
+        }
+    }
+
     #[gpui::test]
     async fn test_matching_paths(cx: &mut gpui::TestAppContext) {
-        let mut path_openers = Vec::new();
         cx.update(|cx| {
             super::init(cx);
-            editor::init(cx, &mut path_openers);
+            editor::init(cx);
         });
 
-        let mut params = cx.update(WorkspaceParams::test);
-        params.path_openers = Arc::from(path_openers);
+        let params = cx.update(WorkspaceParams::test);
         params
             .fs
             .as_fake()

crates/go_to_line/src/go_to_line.rs 🔗

@@ -59,7 +59,8 @@ impl GoToLine {
     }
 
     fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
-        if let Some(editor) = workspace.active_item(cx)
+        if let Some(editor) = workspace
+            .active_item(cx)
             .and_then(|active_item| active_item.downcast::<Editor>())
         {
             workspace.toggle_modal(cx, |cx, _| {
@@ -101,7 +102,7 @@ impl GoToLine {
     ) {
         match event {
             editor::Event::Blurred => cx.emit(Event::Dismissed),
-            editor::Event::Edited => {
+            editor::Event::BufferEdited { .. } => {
                 let line_editor = self.line_editor.read(cx).buffer().read(cx).read(cx).text();
                 let mut components = line_editor.trim().split(&[',', ':'][..]);
                 let row = components.next().and_then(|row| row.parse::<u32>().ok());
@@ -148,7 +149,7 @@ impl View for GoToLine {
     }
 
     fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
-        let theme = &cx.app_state::<Settings>().theme.selector;
+        let theme = &cx.global::<Settings>().theme.selector;
 
         let label = format!(
             "{},{} of {} lines",

crates/gpui/src/app.rs 🔗

@@ -8,6 +8,7 @@ use crate::{
     AssetCache, AssetSource, ClipboardItem, FontCache, PathPromptOptions, TextLayoutCache,
 };
 use anyhow::{anyhow, Result};
+use collections::btree_map;
 use keymap::MatchResult;
 use lazy_static::lazy_static;
 use parking_lot::Mutex;
@@ -93,6 +94,8 @@ pub trait UpgradeModelHandle {
 
 pub trait UpgradeViewHandle {
     fn upgrade_view_handle<T: View>(&self, handle: &WeakViewHandle<T>) -> Option<ViewHandle<T>>;
+
+    fn upgrade_any_view_handle(&self, handle: &AnyWeakViewHandle) -> Option<AnyViewHandle>;
 }
 
 pub trait ReadView {
@@ -182,6 +185,12 @@ macro_rules! action {
                 Box::new(self.clone())
             }
         }
+
+        impl From<$arg> for $name {
+            fn from(arg: $arg) -> Self {
+                Self(arg)
+            }
+        }
     };
 
     ($name:ident) => {
@@ -433,13 +442,32 @@ impl TestAppContext {
     }
 
     pub fn dispatch_keystroke(
-        &self,
+        &mut self,
         window_id: usize,
-        responder_chain: Vec<usize>,
-        keystroke: &Keystroke,
-    ) -> Result<bool> {
-        let mut state = self.cx.borrow_mut();
-        state.dispatch_keystroke(window_id, responder_chain, keystroke)
+        keystroke: Keystroke,
+        input: Option<String>,
+        is_held: bool,
+    ) {
+        self.cx.borrow_mut().update(|cx| {
+            let presenter = cx
+                .presenters_and_platform_windows
+                .get(&window_id)
+                .unwrap()
+                .0
+                .clone();
+            let responder_chain = presenter.borrow().dispatch_path(cx.as_ref());
+
+            if !cx.dispatch_keystroke(window_id, responder_chain, &keystroke) {
+                presenter.borrow_mut().dispatch_event(
+                    Event::KeyDown {
+                        keystroke,
+                        input,
+                        is_held,
+                    },
+                    cx,
+                );
+            }
+        });
     }
 
     pub fn add_model<T, F>(&mut self, build_model: F) -> ModelHandle<T>
@@ -494,7 +522,7 @@ impl TestAppContext {
 
     pub fn update<T, F: FnOnce(&mut MutableAppContext) -> T>(&mut self, callback: F) -> T {
         let mut state = self.cx.borrow_mut();
-        // Don't increment pending flushes in order to effects to be flushed before the callback
+        // Don't increment pending flushes in order for effects to be flushed before the callback
         // completes, which is helpful in tests.
         let result = callback(&mut *state);
         // Flush effects after the callback just in case there are any. This can happen in edge
@@ -595,6 +623,14 @@ impl AsyncAppContext {
         self.update(|cx| cx.add_model(build_model))
     }
 
+    pub fn add_view<T, F>(&mut self, window_id: usize, build_view: F) -> ViewHandle<T>
+    where
+        T: View,
+        F: FnOnce(&mut ViewContext<T>) -> T,
+    {
+        self.update(|cx| cx.add_view(window_id, build_view))
+    }
+
     pub fn platform(&self) -> Arc<dyn Platform> {
         self.0.borrow().platform()
     }
@@ -639,6 +675,10 @@ impl UpgradeViewHandle for AsyncAppContext {
     fn upgrade_view_handle<T: View>(&self, handle: &WeakViewHandle<T>) -> Option<ViewHandle<T>> {
         self.0.borrow_mut().upgrade_view_handle(handle)
     }
+
+    fn upgrade_any_view_handle(&self, handle: &AnyWeakViewHandle) -> Option<AnyViewHandle> {
+        self.0.borrow_mut().upgrade_any_view_handle(handle)
+    }
 }
 
 impl ReadModelWith for AsyncAppContext {
@@ -742,6 +782,7 @@ type GlobalActionCallback = dyn FnMut(&dyn AnyAction, &mut MutableAppContext);
 type SubscriptionCallback = Box<dyn FnMut(&dyn Any, &mut MutableAppContext) -> bool>;
 type GlobalSubscriptionCallback = Box<dyn FnMut(&dyn Any, &mut MutableAppContext)>;
 type ObservationCallback = Box<dyn FnMut(&mut MutableAppContext) -> bool>;
+type GlobalObservationCallback = Box<dyn FnMut(&dyn Any, &mut MutableAppContext)>;
 type ReleaseObservationCallback = Box<dyn FnMut(&dyn Any, &mut MutableAppContext)>;
 
 pub struct MutableAppContext {
@@ -761,12 +802,15 @@ pub struct MutableAppContext {
     global_subscriptions:
         Arc<Mutex<HashMap<TypeId, BTreeMap<usize, Option<GlobalSubscriptionCallback>>>>>,
     observations: Arc<Mutex<HashMap<usize, BTreeMap<usize, Option<ObservationCallback>>>>>,
+    global_observations:
+        Arc<Mutex<HashMap<TypeId, BTreeMap<usize, Option<GlobalObservationCallback>>>>>,
     release_observations: Arc<Mutex<HashMap<usize, BTreeMap<usize, ReleaseObservationCallback>>>>,
     presenters_and_platform_windows:
         HashMap<usize, (Rc<RefCell<Presenter>>, Box<dyn platform::Window>)>,
     foreground: Rc<executor::Foreground>,
     pending_effects: VecDeque<Effect>,
     pending_notifications: HashSet<usize>,
+    pending_global_notifications: HashSet<TypeId>,
     pending_flushes: usize,
     flushing_effects: bool,
     next_cursor_style_handle_id: Arc<AtomicUsize>,
@@ -791,7 +835,7 @@ impl MutableAppContext {
                 models: Default::default(),
                 views: Default::default(),
                 windows: Default::default(),
-                app_states: Default::default(),
+                globals: Default::default(),
                 element_states: Default::default(),
                 ref_counts: Arc::new(Mutex::new(ref_counts)),
                 background,
@@ -810,10 +854,12 @@ impl MutableAppContext {
             global_subscriptions: Default::default(),
             observations: Default::default(),
             release_observations: Default::default(),
+            global_observations: Default::default(),
             presenters_and_platform_windows: HashMap::new(),
             foreground,
             pending_effects: VecDeque::new(),
             pending_notifications: HashSet::new(),
+            pending_global_notifications: HashSet::new(),
             pending_flushes: 0,
             flushing_effects: false,
             next_cursor_style_handle_id: Default::default(),
@@ -1090,21 +1136,18 @@ impl MutableAppContext {
         E: Any,
         F: 'static + FnMut(&E, &mut Self),
     {
-        let id = post_inc(&mut self.next_subscription_id);
+        let subscription_id = post_inc(&mut self.next_subscription_id);
         let type_id = TypeId::of::<E>();
-        self.global_subscriptions
-            .lock()
-            .entry(type_id)
-            .or_default()
-            .insert(
-                id,
-                Some(Box::new(move |payload, cx| {
-                    let payload = payload.downcast_ref().expect("downcast is type safe");
-                    callback(payload, cx)
-                })),
-            );
+        self.pending_effects.push_back(Effect::GlobalSubscription {
+            type_id,
+            subscription_id,
+            callback: Box::new(move |payload, cx| {
+                let payload = payload.downcast_ref().expect("downcast is type safe");
+                callback(payload, cx)
+            }),
+        });
         Subscription::GlobalSubscription {
-            id,
+            id: subscription_id,
             type_id,
             subscriptions: Some(Arc::downgrade(&self.global_subscriptions)),
         }
@@ -1130,25 +1173,22 @@ impl MutableAppContext {
         H: Handle<E>,
         F: 'static + FnMut(H, &E::Event, &mut Self) -> bool,
     {
-        let id = post_inc(&mut self.next_subscription_id);
+        let subscription_id = post_inc(&mut self.next_subscription_id);
         let emitter = handle.downgrade();
-        self.subscriptions
-            .lock()
-            .entry(handle.id())
-            .or_default()
-            .insert(
-                id,
-                Some(Box::new(move |payload, cx| {
-                    if let Some(emitter) = H::upgrade_from(&emitter, cx.as_ref()) {
-                        let payload = payload.downcast_ref().expect("downcast is type safe");
-                        callback(emitter, payload, cx)
-                    } else {
-                        false
-                    }
-                })),
-            );
+        self.pending_effects.push_back(Effect::Subscription {
+            entity_id: handle.id(),
+            subscription_id,
+            callback: Box::new(move |payload, cx| {
+                if let Some(emitter) = H::upgrade_from(&emitter, cx.as_ref()) {
+                    let payload = payload.downcast_ref().expect("downcast is type safe");
+                    callback(emitter, payload, cx)
+                } else {
+                    false
+                }
+            }),
+        });
         Subscription::Subscription {
-            id,
+            id: subscription_id,
             entity_id: handle.id(),
             subscriptions: Some(Arc::downgrade(&self.subscriptions)),
         }
@@ -1161,26 +1201,52 @@ impl MutableAppContext {
         H: Handle<E>,
         F: 'static + FnMut(H, &mut Self) -> bool,
     {
-        let id = post_inc(&mut self.next_subscription_id);
+        let subscription_id = post_inc(&mut self.next_subscription_id);
         let observed = handle.downgrade();
-        self.observations
+        let entity_id = handle.id();
+        self.pending_effects.push_back(Effect::Observation {
+            entity_id,
+            subscription_id,
+            callback: Box::new(move |cx| {
+                if let Some(observed) = H::upgrade_from(&observed, cx) {
+                    callback(observed, cx)
+                } else {
+                    false
+                }
+            }),
+        });
+        Subscription::Observation {
+            id: subscription_id,
+            entity_id,
+            observations: Some(Arc::downgrade(&self.observations)),
+        }
+    }
+
+    pub fn observe_global<G, F>(&mut self, mut observe: F) -> Subscription
+    where
+        G: Any,
+        F: 'static + FnMut(&G, &mut MutableAppContext),
+    {
+        let type_id = TypeId::of::<G>();
+        let id = post_inc(&mut self.next_subscription_id);
+
+        self.global_observations
             .lock()
-            .entry(handle.id())
+            .entry(type_id)
             .or_default()
             .insert(
                 id,
-                Some(Box::new(move |cx| {
-                    if let Some(observed) = H::upgrade_from(&observed, cx) {
-                        callback(observed, cx)
-                    } else {
-                        false
-                    }
-                })),
+                Some(
+                    Box::new(move |global: &dyn Any, cx: &mut MutableAppContext| {
+                        observe(global.downcast_ref().unwrap(), cx)
+                    }) as GlobalObservationCallback,
+                ),
             );
-        Subscription::Observation {
+
+        Subscription::GlobalObservation {
             id,
-            entity_id: handle.id(),
-            observations: Some(Arc::downgrade(&self.observations)),
+            type_id,
+            observations: Some(Arc::downgrade(&self.global_observations)),
         }
     }
 
@@ -1210,8 +1276,18 @@ impl MutableAppContext {
         }
     }
 
-    fn defer(&mut self, callback: Box<dyn FnOnce(&mut MutableAppContext)>) {
-        self.pending_effects.push_back(Effect::Deferred(callback))
+    pub fn defer(&mut self, callback: impl 'static + FnOnce(&mut MutableAppContext)) {
+        self.pending_effects.push_back(Effect::Deferred {
+            callback: Box::new(callback),
+            after_window_update: false,
+        })
+    }
+
+    pub fn after_window_update(&mut self, callback: impl 'static + FnOnce(&mut MutableAppContext)) {
+        self.pending_effects.push_back(Effect::Deferred {
+            callback: Box::new(callback),
+            after_window_update: true,
+        })
     }
 
     pub(crate) fn notify_model(&mut self, model_id: usize) {
@@ -1228,6 +1304,13 @@ impl MutableAppContext {
         }
     }
 
+    pub(crate) fn notify_global(&mut self, type_id: TypeId) {
+        if self.pending_global_notifications.insert(type_id) {
+            self.pending_effects
+                .push_back(Effect::GlobalNotification { type_id });
+        }
+    }
+
     pub fn dispatch_action<A: Action>(
         &mut self,
         window_id: usize,
@@ -1322,17 +1405,15 @@ impl MutableAppContext {
         window_id: usize,
         responder_chain: Vec<usize>,
         keystroke: &Keystroke,
-    ) -> Result<bool> {
+    ) -> bool {
         let mut context_chain = Vec::new();
         for view_id in &responder_chain {
-            if let Some(view) = self.cx.views.get(&(window_id, *view_id)) {
-                context_chain.push(view.keymap_context(self.as_ref()));
-            } else {
-                return Err(anyhow!(
-                    "View {} in responder chain does not exist",
-                    view_id
-                ));
-            }
+            let view = self
+                .cx
+                .views
+                .get(&(window_id, *view_id))
+                .expect("view in responder chain does not exist");
+            context_chain.push(view.keymap_context(self.as_ref()));
         }
 
         let mut pending = false;
@@ -1347,34 +1428,74 @@ impl MutableAppContext {
                     if self.dispatch_action_any(window_id, &responder_chain[0..=i], action.as_ref())
                     {
                         self.keystroke_matcher.clear_pending();
-                        return Ok(true);
+                        return true;
                     }
                 }
             }
         }
 
-        Ok(pending)
+        pending
     }
 
-    pub fn add_app_state<T: 'static>(&mut self, state: T) {
-        self.cx
-            .app_states
-            .insert(TypeId::of::<T>(), Box::new(state));
+    pub fn default_global<T: 'static + Default>(&mut self) -> &T {
+        let type_id = TypeId::of::<T>();
+        self.update(|this| {
+            if !this.globals.contains_key(&type_id) {
+                this.notify_global(type_id);
+            }
+
+            this.cx
+                .globals
+                .entry(type_id)
+                .or_insert_with(|| Box::new(T::default()));
+        });
+        self.globals.get(&type_id).unwrap().downcast_ref().unwrap()
     }
 
-    pub fn update_app_state<T: 'static, F, U>(&mut self, update: F) -> U
+    pub fn set_global<T: 'static>(&mut self, state: T) {
+        self.update(|this| {
+            let type_id = TypeId::of::<T>();
+            this.cx.globals.insert(type_id, Box::new(state));
+            this.notify_global(type_id);
+        });
+    }
+
+    pub fn update_default_global<T, F, U>(&mut self, update: F) -> U
     where
+        T: 'static + Default,
         F: FnOnce(&mut T, &mut MutableAppContext) -> U,
     {
-        let type_id = TypeId::of::<T>();
-        let mut state = self
-            .cx
-            .app_states
-            .remove(&type_id)
-            .expect("no app state has been added for this type");
-        let result = update(state.downcast_mut().unwrap(), self);
-        self.cx.app_states.insert(type_id, state);
-        result
+        self.update(|this| {
+            let type_id = TypeId::of::<T>();
+            let mut state = this
+                .cx
+                .globals
+                .remove(&type_id)
+                .unwrap_or_else(|| Box::new(T::default()));
+            let result = update(state.downcast_mut().unwrap(), this);
+            this.cx.globals.insert(type_id, state);
+            this.notify_global(type_id);
+            result
+        })
+    }
+
+    pub fn update_global<T, F, U>(&mut self, update: F) -> U
+    where
+        T: 'static,
+        F: FnOnce(&mut T, &mut MutableAppContext) -> U,
+    {
+        self.update(|this| {
+            let type_id = TypeId::of::<T>();
+            let mut state = this
+                .cx
+                .globals
+                .remove(&type_id)
+                .expect("no global has been added for this type");
+            let result = update(state.downcast_mut().unwrap(), this);
+            this.cx.globals.insert(type_id, state);
+            this.notify_global(type_id);
+            result
+        })
     }
 
     pub fn add_model<T, F>(&mut self, build_model: F) -> ModelHandle<T>
@@ -1413,11 +1534,10 @@ impl MutableAppContext {
                     invalidation: None,
                 },
             );
-            this.open_platform_window(window_id, window_options);
             root_view.update(this, |view, cx| {
                 view.on_focus(cx);
-                cx.notify();
             });
+            this.open_platform_window(window_id, window_options);
 
             (window_id, root_view)
         })
@@ -1444,14 +1564,11 @@ impl MutableAppContext {
             window.on_event(Box::new(move |event| {
                 app.update(|cx| {
                     if let Event::KeyDown { keystroke, .. } = &event {
-                        if cx
-                            .dispatch_keystroke(
-                                window_id,
-                                presenter.borrow().dispatch_path(cx.as_ref()),
-                                keystroke,
-                            )
-                            .unwrap()
-                        {
+                        if cx.dispatch_keystroke(
+                            window_id,
+                            presenter.borrow().dispatch_path(cx.as_ref()),
+                            keystroke,
+                        ) {
                             return;
                         }
                     }
@@ -1475,6 +1592,11 @@ impl MutableAppContext {
             }));
         }
 
+        let scene =
+            presenter
+                .borrow_mut()
+                .build_scene(window.size(), window.scale_factor(), false, self);
+        window.present_scene(scene);
         self.presenters_and_platform_windows
             .insert(window_id, (presenter.clone(), window));
     }
@@ -1599,6 +1721,7 @@ impl MutableAppContext {
 
     fn flush_effects(&mut self) {
         self.pending_flushes = self.pending_flushes.saturating_sub(1);
+        let mut after_window_update_callbacks = Vec::new();
 
         if !self.flushing_effects && self.pending_flushes == 0 {
             self.flushing_effects = true;
@@ -1607,15 +1730,46 @@ impl MutableAppContext {
             loop {
                 if let Some(effect) = self.pending_effects.pop_front() {
                     match effect {
+                        Effect::Subscription {
+                            entity_id,
+                            subscription_id,
+                            callback,
+                        } => self.handle_subscription_effect(entity_id, subscription_id, callback),
                         Effect::Event { entity_id, payload } => self.emit_event(entity_id, payload),
+                        Effect::GlobalSubscription {
+                            type_id,
+                            subscription_id,
+                            callback,
+                        } => self.handle_global_subscription_effect(
+                            type_id,
+                            subscription_id,
+                            callback,
+                        ),
                         Effect::GlobalEvent { payload } => self.emit_global_event(payload),
+                        Effect::Observation {
+                            entity_id,
+                            subscription_id,
+                            callback,
+                        } => self.handle_observation_effect(entity_id, subscription_id, callback),
                         Effect::ModelNotification { model_id } => {
                             self.notify_model_observers(model_id)
                         }
                         Effect::ViewNotification { window_id, view_id } => {
                             self.notify_view_observers(window_id, view_id)
                         }
-                        Effect::Deferred(callback) => callback(self),
+                        Effect::GlobalNotification { type_id } => {
+                            self.notify_global_observers(type_id)
+                        }
+                        Effect::Deferred {
+                            callback,
+                            after_window_update,
+                        } => {
+                            if after_window_update {
+                                after_window_update_callbacks.push(callback);
+                            } else {
+                                callback(self)
+                            }
+                        }
                         Effect::ModelRelease { model_id, model } => {
                             self.notify_release_observers(model_id, model.as_any())
                         }
@@ -1647,12 +1801,19 @@ impl MutableAppContext {
                     }
 
                     if self.pending_effects.is_empty() {
-                        self.flushing_effects = false;
-                        self.pending_notifications.clear();
-                        break;
-                    } else {
-                        refreshing = false;
+                        for callback in after_window_update_callbacks.drain(..) {
+                            callback(self);
+                        }
+
+                        if self.pending_effects.is_empty() {
+                            self.flushing_effects = false;
+                            self.pending_notifications.clear();
+                            self.pending_global_notifications.clear();
+                            break;
+                        }
                     }
+
+                    refreshing = false;
                 }
             }
         }
@@ -1666,13 +1827,13 @@ impl MutableAppContext {
             }
         }
 
-        for (window_id, invalidation) in invalidations {
+        for (window_id, mut invalidation) in invalidations {
             if let Some((presenter, mut window)) =
                 self.presenters_and_platform_windows.remove(&window_id)
             {
                 {
                     let mut presenter = presenter.borrow_mut();
-                    presenter.invalidate(invalidation, self);
+                    presenter.invalidate(&mut invalidation, self);
                     let scene =
                         presenter.build_scene(window.size(), window.scale_factor(), false, self);
                     window.present_scene(scene);
@@ -1695,7 +1856,7 @@ impl MutableAppContext {
     fn perform_window_refresh(&mut self) {
         let mut presenters = mem::take(&mut self.presenters_and_platform_windows);
         for (window_id, (presenter, window)) in &mut presenters {
-            let invalidation = self
+            let mut invalidation = self
                 .cx
                 .windows
                 .get_mut(&window_id)
@@ -1703,7 +1864,10 @@ impl MutableAppContext {
                 .invalidation
                 .take();
             let mut presenter = presenter.borrow_mut();
-            presenter.refresh(invalidation, self);
+            presenter.refresh(
+                invalidation.as_mut().unwrap_or(&mut Default::default()),
+                self,
+            );
             let scene = presenter.build_scene(window.size(), window.scale_factor(), true, self);
             window.present_scene(scene);
         }
@@ -1720,6 +1884,30 @@ impl MutableAppContext {
         }
     }
 
+    fn handle_subscription_effect(
+        &mut self,
+        entity_id: usize,
+        subscription_id: usize,
+        callback: SubscriptionCallback,
+    ) {
+        match self
+            .subscriptions
+            .lock()
+            .entry(entity_id)
+            .or_default()
+            .entry(subscription_id)
+        {
+            btree_map::Entry::Vacant(entry) => {
+                entry.insert(Some(callback));
+            }
+            // Subscription was dropped before effect was processed
+            btree_map::Entry::Occupied(entry) => {
+                debug_assert!(entry.get().is_none());
+                entry.remove();
+            }
+        }
+    }
+
     fn emit_event(&mut self, entity_id: usize, payload: Box<dyn Any>) {
         let callbacks = self.subscriptions.lock().remove(&entity_id);
         if let Some(callbacks) = callbacks {
@@ -1734,10 +1922,10 @@ impl MutableAppContext {
                             .or_default()
                             .entry(id)
                         {
-                            collections::btree_map::Entry::Vacant(entry) => {
+                            btree_map::Entry::Vacant(entry) => {
                                 entry.insert(Some(callback));
                             }
-                            collections::btree_map::Entry::Occupied(entry) => {
+                            btree_map::Entry::Occupied(entry) => {
                                 entry.remove();
                             }
                         }
@@ -1747,6 +1935,30 @@ impl MutableAppContext {
         }
     }
 
+    fn handle_global_subscription_effect(
+        &mut self,
+        type_id: TypeId,
+        subscription_id: usize,
+        callback: GlobalSubscriptionCallback,
+    ) {
+        match self
+            .global_subscriptions
+            .lock()
+            .entry(type_id)
+            .or_default()
+            .entry(subscription_id)
+        {
+            btree_map::Entry::Vacant(entry) => {
+                entry.insert(Some(callback));
+            }
+            // Subscription was dropped before effect was processed
+            btree_map::Entry::Occupied(entry) => {
+                debug_assert!(entry.get().is_none());
+                entry.remove();
+            }
+        }
+    }
+
     fn emit_global_event(&mut self, payload: Box<dyn Any>) {
         let type_id = (&*payload).type_id();
         let callbacks = self.global_subscriptions.lock().remove(&type_id);
@@ -1761,10 +1973,10 @@ impl MutableAppContext {
                         .or_default()
                         .entry(id)
                     {
-                        collections::btree_map::Entry::Vacant(entry) => {
+                        btree_map::Entry::Vacant(entry) => {
                             entry.insert(Some(callback));
                         }
-                        collections::btree_map::Entry::Occupied(entry) => {
+                        btree_map::Entry::Occupied(entry) => {
                             entry.remove();
                         }
                     }
@@ -1773,6 +1985,30 @@ impl MutableAppContext {
         }
     }
 
+    fn handle_observation_effect(
+        &mut self,
+        entity_id: usize,
+        subscription_id: usize,
+        callback: ObservationCallback,
+    ) {
+        match self
+            .observations
+            .lock()
+            .entry(entity_id)
+            .or_default()
+            .entry(subscription_id)
+        {
+            btree_map::Entry::Vacant(entry) => {
+                entry.insert(Some(callback));
+            }
+            // Observation was dropped before effect was processed
+            btree_map::Entry::Occupied(entry) => {
+                debug_assert!(entry.get().is_none());
+                entry.remove();
+            }
+        }
+    }
+
     fn notify_model_observers(&mut self, observed_id: usize) {
         let callbacks = self.observations.lock().remove(&observed_id);
         if let Some(callbacks) = callbacks {
@@ -1788,10 +2024,10 @@ impl MutableAppContext {
                                 .or_default()
                                 .entry(id)
                             {
-                                collections::btree_map::Entry::Vacant(entry) => {
+                                btree_map::Entry::Vacant(entry) => {
                                     entry.insert(Some(callback));
                                 }
-                                collections::btree_map::Entry::Occupied(entry) => {
+                                btree_map::Entry::Occupied(entry) => {
                                     entry.remove();
                                 }
                             }
@@ -1829,10 +2065,10 @@ impl MutableAppContext {
                                 .or_default()
                                 .entry(id)
                             {
-                                collections::btree_map::Entry::Vacant(entry) => {
+                                btree_map::Entry::Vacant(entry) => {
                                     entry.insert(Some(callback));
                                 }
-                                collections::btree_map::Entry::Occupied(entry) => {
+                                btree_map::Entry::Occupied(entry) => {
                                     entry.remove();
                                 }
                             }
@@ -1843,6 +2079,34 @@ impl MutableAppContext {
         }
     }
 
+    fn notify_global_observers(&mut self, observed_type_id: TypeId) {
+        let callbacks = self.global_observations.lock().remove(&observed_type_id);
+        if let Some(callbacks) = callbacks {
+            if let Some(global) = self.cx.globals.remove(&observed_type_id) {
+                for (id, callback) in callbacks {
+                    if let Some(mut callback) = callback {
+                        callback(global.as_ref(), self);
+                        match self
+                            .global_observations
+                            .lock()
+                            .entry(observed_type_id)
+                            .or_default()
+                            .entry(id)
+                        {
+                            collections::btree_map::Entry::Vacant(entry) => {
+                                entry.insert(Some(callback));
+                            }
+                            collections::btree_map::Entry::Occupied(entry) => {
+                                entry.remove();
+                            }
+                        }
+                    }
+                }
+                self.cx.globals.insert(observed_type_id, global);
+            }
+        }
+    }
+
     fn notify_release_observers(&mut self, entity_id: usize, entity: &dyn Any) {
         let callbacks = self.release_observations.lock().remove(&entity_id);
         if let Some(callbacks) = callbacks {
@@ -1978,6 +2242,10 @@ impl UpgradeViewHandle for MutableAppContext {
     fn upgrade_view_handle<T: View>(&self, handle: &WeakViewHandle<T>) -> Option<ViewHandle<T>> {
         self.cx.upgrade_view_handle(handle)
     }
+
+    fn upgrade_any_view_handle(&self, handle: &AnyWeakViewHandle) -> Option<AnyViewHandle> {
+        self.cx.upgrade_any_view_handle(handle)
+    }
 }
 
 impl ReadView for MutableAppContext {
@@ -2039,7 +2307,7 @@ pub struct AppContext {
     models: HashMap<usize, Box<dyn AnyModel>>,
     views: HashMap<(usize, usize), Box<dyn AnyView>>,
     windows: HashMap<usize, Window>,
-    app_states: HashMap<TypeId, Box<dyn Any>>,
+    globals: HashMap<TypeId, Box<dyn Any>>,
     element_states: HashMap<ElementStateId, Box<dyn Any>>,
     background: Arc<executor::Background>,
     ref_counts: Arc<Mutex<RefCounts>>,
@@ -2072,8 +2340,12 @@ impl AppContext {
         &self.platform
     }
 
-    pub fn app_state<T: 'static>(&self) -> &T {
-        self.app_states
+    pub fn has_global<T: 'static>(&self) -> bool {
+        self.globals.contains_key(&TypeId::of::<T>())
+    }
+
+    pub fn global<T: 'static>(&self) -> &T {
+        self.globals
             .get(&TypeId::of::<T>())
             .expect("no app state has been added for this type")
             .downcast_ref()
@@ -2135,6 +2407,19 @@ impl UpgradeViewHandle for AppContext {
             None
         }
     }
+
+    fn upgrade_any_view_handle(&self, handle: &AnyWeakViewHandle) -> Option<AnyViewHandle> {
+        if self.ref_counts.lock().is_entity_alive(handle.view_id) {
+            Some(AnyViewHandle::new(
+                handle.window_id,
+                handle.view_id,
+                handle.view_type,
+                self.ref_counts.clone(),
+            ))
+        } else {
+            None
+        }
+    }
 }
 
 impl ReadView for AppContext {
@@ -2162,13 +2447,28 @@ pub struct WindowInvalidation {
 }
 
 pub enum Effect {
+    Subscription {
+        entity_id: usize,
+        subscription_id: usize,
+        callback: SubscriptionCallback,
+    },
     Event {
         entity_id: usize,
         payload: Box<dyn Any>,
     },
+    GlobalSubscription {
+        type_id: TypeId,
+        subscription_id: usize,
+        callback: GlobalSubscriptionCallback,
+    },
     GlobalEvent {
         payload: Box<dyn Any>,
     },
+    Observation {
+        entity_id: usize,
+        subscription_id: usize,
+        callback: ObservationCallback,
+    },
     ModelNotification {
         model_id: usize,
     },
@@ -2176,7 +2476,13 @@ pub enum Effect {
         window_id: usize,
         view_id: usize,
     },
-    Deferred(Box<dyn FnOnce(&mut MutableAppContext)>),
+    Deferred {
+        callback: Box<dyn FnOnce(&mut MutableAppContext)>,
+        after_window_update: bool,
+    },
+    GlobalNotification {
+        type_id: TypeId,
+    },
     ModelRelease {
         model_id: usize,
         model: Box<dyn AnyModel>,
@@ -2198,14 +2504,41 @@ pub enum Effect {
 impl Debug for Effect {
     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
         match self {
+            Effect::Subscription {
+                entity_id,
+                subscription_id,
+                ..
+            } => f
+                .debug_struct("Effect::Subscribe")
+                .field("entity_id", entity_id)
+                .field("subscription_id", subscription_id)
+                .finish(),
             Effect::Event { entity_id, .. } => f
                 .debug_struct("Effect::Event")
                 .field("entity_id", entity_id)
                 .finish(),
+            Effect::GlobalSubscription {
+                type_id,
+                subscription_id,
+                ..
+            } => f
+                .debug_struct("Effect::Subscribe")
+                .field("type_id", type_id)
+                .field("subscription_id", subscription_id)
+                .finish(),
             Effect::GlobalEvent { payload, .. } => f
                 .debug_struct("Effect::GlobalEvent")
                 .field("type_id", &(&*payload).type_id())
                 .finish(),
+            Effect::Observation {
+                entity_id,
+                subscription_id,
+                ..
+            } => f
+                .debug_struct("Effect::Observation")
+                .field("entity_id", entity_id)
+                .field("subscription_id", subscription_id)
+                .finish(),
             Effect::ModelNotification { model_id } => f
                 .debug_struct("Effect::ModelNotification")
                 .field("model_id", model_id)
@@ -2215,7 +2548,11 @@ impl Debug for Effect {
                 .field("window_id", window_id)
                 .field("view_id", view_id)
                 .finish(),
-            Effect::Deferred(_) => f.debug_struct("Effect::Deferred").finish(),
+            Effect::GlobalNotification { type_id } => f
+                .debug_struct("Effect::GlobalNotification")
+                .field("type_id", type_id)
+                .finish(),
+            Effect::Deferred { .. } => f.debug_struct("Effect::Deferred").finish(),
             Effect::ModelRelease { model_id, .. } => f
                 .debug_struct("Effect::ModelRelease")
                 .field("model_id", model_id)
@@ -2394,6 +2731,15 @@ impl<'a, T: Entity> ModelContext<'a, T> {
         self.app.add_model(build_model)
     }
 
+    pub fn defer(&mut self, callback: impl 'static + FnOnce(&mut T, &mut ModelContext<T>)) {
+        let handle = self.handle();
+        self.app.defer(move |cx| {
+            handle.update(cx, |model, cx| {
+                callback(model, cx);
+            })
+        })
+    }
+
     pub fn emit(&mut self, payload: T::Event) {
         self.app.pending_effects.push_back(Effect::Event {
             entity_id: self.model_id,
@@ -2740,11 +3086,23 @@ impl<'a, T: View> ViewContext<'a, T> {
 
     pub fn defer(&mut self, callback: impl 'static + FnOnce(&mut T, &mut ViewContext<T>)) {
         let handle = self.handle();
-        self.app.defer(Box::new(move |cx| {
+        self.app.defer(move |cx| {
             handle.update(cx, |view, cx| {
                 callback(view, cx);
             })
-        }))
+        })
+    }
+
+    pub fn after_window_update(
+        &mut self,
+        callback: impl 'static + FnOnce(&mut T, &mut ViewContext<T>),
+    ) {
+        let handle = self.handle();
+        self.app.after_window_update(move |cx| {
+            handle.update(cx, |view, cx| {
+                callback(view, cx);
+            })
+        })
     }
 
     pub fn propagate_action(&mut self) {
@@ -2892,6 +3250,10 @@ impl<V> UpgradeViewHandle for ViewContext<'_, V> {
     fn upgrade_view_handle<T: View>(&self, handle: &WeakViewHandle<T>) -> Option<ViewHandle<T>> {
         self.cx.upgrade_view_handle(handle)
     }
+
+    fn upgrade_any_view_handle(&self, handle: &AnyWeakViewHandle) -> Option<AnyViewHandle> {
+        self.cx.upgrade_any_view_handle(handle)
+    }
 }
 
 impl<V: View> UpdateModel for ViewContext<'_, V> {
@@ -3338,9 +3700,9 @@ impl<T: View> ViewHandle<T> {
         F: 'static + FnOnce(&mut T, &mut ViewContext<T>),
     {
         let this = self.clone();
-        cx.as_mut().defer(Box::new(move |cx| {
+        cx.as_mut().defer(move |cx| {
             this.update(cx, |view, cx| update(view, cx));
-        }));
+        });
     }
 
     pub fn is_focused(&self, cx: &AppContext) -> bool {

crates/gpui/src/executor.rs 🔗

@@ -337,7 +337,7 @@ impl Deterministic {
 
             if let Some((_, wakeup_time, _)) = state.pending_timers.first() {
                 let wakeup_time = *wakeup_time;
-                if wakeup_time < new_now {
+                if wakeup_time <= new_now {
                     let timer_count = state
                         .pending_timers
                         .iter()

crates/gpui/src/keymap.rs 🔗

@@ -224,15 +224,19 @@ impl Keystroke {
             key: key.unwrap(),
         })
     }
+
+    pub fn modified(&self) -> bool {
+        self.ctrl || self.alt || self.shift || self.cmd
+    }
 }
 
 impl Context {
-    pub fn extend(&mut self, other: Context) {
-        for v in other.set {
-            self.set.insert(v);
+    pub fn extend(&mut self, other: &Context) {
+        for v in &other.set {
+            self.set.insert(v.clone());
         }
-        for (k, v) in other.map {
-            self.map.insert(k, v);
+        for (k, v) in &other.map {
+            self.map.insert(k.clone(), v.clone());
         }
     }
 }

crates/gpui/src/platform/mac/renderer.rs 🔗

@@ -561,9 +561,10 @@ impl Renderer {
         }
 
         for icon in icons {
-            let origin = icon.bounds.origin() * scale_factor;
-            let target_size = icon.bounds.size() * scale_factor;
-            let source_size = (target_size * 2.).ceil().to_i32();
+            // Snap sprite to pixel grid.
+            let origin = (icon.bounds.origin() * scale_factor).floor();
+            let target_size = (icon.bounds.size() * scale_factor).ceil();
+            let source_size = (target_size * 2.).to_i32();
 
             let sprite =
                 self.sprite_cache

crates/gpui/src/presenter.rs 🔗

@@ -20,7 +20,7 @@ use std::{
 
 pub struct Presenter {
     window_id: usize,
-    rendered_views: HashMap<usize, ElementBox>,
+    pub(crate) rendered_views: HashMap<usize, ElementBox>,
     parents: HashMap<usize, usize>,
     font_cache: Arc<FontCache>,
     text_layout_cache: TextLayoutCache,
@@ -63,39 +63,34 @@ impl Presenter {
         path
     }
 
-    pub fn invalidate(&mut self, mut invalidation: WindowInvalidation, cx: &mut MutableAppContext) {
+    pub fn invalidate(
+        &mut self,
+        invalidation: &mut WindowInvalidation,
+        cx: &mut MutableAppContext,
+    ) {
         cx.start_frame();
-        for view_id in invalidation.removed {
+        for view_id in &invalidation.removed {
             invalidation.updated.remove(&view_id);
             self.rendered_views.remove(&view_id);
             self.parents.remove(&view_id);
         }
-        for view_id in invalidation.updated {
+        for view_id in &invalidation.updated {
             self.rendered_views.insert(
-                view_id,
-                cx.render_view(self.window_id, view_id, self.titlebar_height, false)
+                *view_id,
+                cx.render_view(self.window_id, *view_id, self.titlebar_height, false)
                     .unwrap(),
             );
         }
     }
 
-    pub fn refresh(
-        &mut self,
-        invalidation: Option<WindowInvalidation>,
-        cx: &mut MutableAppContext,
-    ) {
-        cx.start_frame();
-        if let Some(invalidation) = invalidation {
-            for view_id in invalidation.removed {
-                self.rendered_views.remove(&view_id);
-                self.parents.remove(&view_id);
-            }
-        }
-
+    pub fn refresh(&mut self, invalidation: &mut WindowInvalidation, cx: &mut MutableAppContext) {
+        self.invalidate(invalidation, cx);
         for (view_id, view) in &mut self.rendered_views {
-            *view = cx
-                .render_view(self.window_id, *view_id, self.titlebar_height, true)
-                .unwrap();
+            if !invalidation.updated.contains(view_id) {
+                *view = cx
+                    .render_view(self.window_id, *view_id, self.titlebar_height, true)
+                    .unwrap();
+            }
         }
     }
 
@@ -304,6 +299,10 @@ impl<'a> UpgradeViewHandle for LayoutContext<'a> {
     fn upgrade_view_handle<T: View>(&self, handle: &WeakViewHandle<T>) -> Option<ViewHandle<T>> {
         self.app.upgrade_view_handle(handle)
     }
+
+    fn upgrade_any_view_handle(&self, handle: &crate::AnyWeakViewHandle) -> Option<AnyViewHandle> {
+        self.app.upgrade_any_view_handle(handle)
+    }
 }
 
 impl<'a> ElementStateContext for LayoutContext<'a> {

crates/language/src/buffer.rs 🔗

@@ -271,7 +271,6 @@ pub(crate) struct DiagnosticEndpoint {
 
 #[derive(Copy, Clone, Eq, PartialEq, PartialOrd, Ord, Debug)]
 pub enum CharKind {
-    Newline,
     Punctuation,
     Whitespace,
     Word,
@@ -1621,8 +1620,13 @@ impl BufferSnapshot {
         let range = range.start.to_offset(self)..range.end.to_offset(self);
         let mut cursor = tree.root_node().walk();
 
-        // Descend to smallest leaf that touches or exceeds the start of the range.
-        while cursor.goto_first_child_for_byte(range.start).is_some() {}
+        // Descend to the first leaf that touches the start of the range,
+        // and if the range is non-empty, extends beyond the start.
+        while cursor.goto_first_child_for_byte(range.start).is_some() {
+            if !range.is_empty() && cursor.node().end_byte() == range.start {
+                cursor.goto_next_sibling();
+            }
+        }
 
         // Ascend to the smallest ancestor that strictly contains the range.
         loop {
@@ -1656,6 +1660,9 @@ impl BufferSnapshot {
                 }
             }
 
+            // If there is a candidate node on both sides of the (empty) range, then
+            // decide between the two by favoring a named node over an anonymous token.
+            // If both nodes are the same in that regard, favor the right one.
             if let Some(right_node) = right_node {
                 if right_node.is_named() || !left_node.is_named() {
                     return Some(right_node.byte_range());
@@ -1822,12 +1829,6 @@ impl BufferSnapshot {
             .min_by_key(|(open_range, close_range)| close_range.end - open_range.start)
     }
 
-    /*
-    impl BufferSnapshot
-      pub fn remote_selections_in_range(&self, Range<Anchor>) -> impl Iterator<Item = (ReplicaId, impl Iterator<Item = &Selection<Anchor>>)>
-      pub fn remote_selections_in_range(&self, Range<Anchor>) -> impl Iterator<Item = (ReplicaId, i
-    */
-
     pub fn remote_selections_in_range<'a>(
         &'a self,
         range: Range<Anchor>,
@@ -1840,20 +1841,12 @@ impl BufferSnapshot {
             })
             .map(move |(replica_id, set)| {
                 let start_ix = match set.selections.binary_search_by(|probe| {
-                    probe
-                        .end
-                        .cmp(&range.start, self)
-                        .unwrap()
-                        .then(Ordering::Greater)
+                    probe.end.cmp(&range.start, self).then(Ordering::Greater)
                 }) {
                     Ok(ix) | Err(ix) => ix,
                 };
                 let end_ix = match set.selections.binary_search_by(|probe| {
-                    probe
-                        .start
-                        .cmp(&range.end, self)
-                        .unwrap()
-                        .then(Ordering::Less)
+                    probe.start.cmp(&range.end, self).then(Ordering::Less)
                 }) {
                     Ok(ix) | Err(ix) => ix,
                 };
@@ -2280,9 +2273,7 @@ pub fn contiguous_ranges(
 }
 
 pub fn char_kind(c: char) -> CharKind {
-    if c == '\n' {
-        CharKind::Newline
-    } else if c.is_whitespace() {
+    if c.is_whitespace() {
         CharKind::Whitespace
     } else if c.is_alphanumeric() || c == '_' {
         CharKind::Word

crates/language/src/diagnostic_set.rs 🔗

@@ -81,8 +81,8 @@ impl DiagnosticSet {
         let range = buffer.anchor_before(range.start)..buffer.anchor_at(range.end, end_bias);
         let mut cursor = self.diagnostics.filter::<_, ()>({
             move |summary: &Summary| {
-                let start_cmp = range.start.cmp(&summary.max_end, buffer).unwrap();
-                let end_cmp = range.end.cmp(&summary.min_start, buffer).unwrap();
+                let start_cmp = range.start.cmp(&summary.max_end, buffer);
+                let end_cmp = range.end.cmp(&summary.min_start, buffer);
                 if inclusive {
                     start_cmp <= Ordering::Equal && end_cmp >= Ordering::Equal
                 } else {
@@ -123,7 +123,7 @@ impl DiagnosticSet {
 
         let start_ix = output.len();
         output.extend(groups.into_values().filter_map(|mut entries| {
-            entries.sort_unstable_by(|a, b| a.range.start.cmp(&b.range.start, buffer).unwrap());
+            entries.sort_unstable_by(|a, b| a.range.start.cmp(&b.range.start, buffer));
             entries
                 .iter()
                 .position(|entry| entry.diagnostic.is_primary)
@@ -137,7 +137,6 @@ impl DiagnosticSet {
                 .range
                 .start
                 .cmp(&b.entries[b.primary_ix].range.start, buffer)
-                .unwrap()
         });
     }
 
@@ -187,10 +186,10 @@ impl DiagnosticEntry<Anchor> {
 impl Default for Summary {
     fn default() -> Self {
         Self {
-            start: Anchor::min(),
-            end: Anchor::max(),
-            min_start: Anchor::max(),
-            max_end: Anchor::min(),
+            start: Anchor::MIN,
+            end: Anchor::MAX,
+            min_start: Anchor::MAX,
+            max_end: Anchor::MIN,
             count: 0,
         }
     }
@@ -200,15 +199,10 @@ impl sum_tree::Summary for Summary {
     type Context = text::BufferSnapshot;
 
     fn add_summary(&mut self, other: &Self, buffer: &Self::Context) {
-        if other
-            .min_start
-            .cmp(&self.min_start, buffer)
-            .unwrap()
-            .is_lt()
-        {
+        if other.min_start.cmp(&self.min_start, buffer).is_lt() {
             self.min_start = other.min_start.clone();
         }
-        if other.max_end.cmp(&self.max_end, buffer).unwrap().is_gt() {
+        if other.max_end.cmp(&self.max_end, buffer).is_gt() {
             self.max_end = other.max_end.clone();
         }
         self.start = other.start.clone();

crates/language/src/proto.rs 🔗

@@ -100,15 +100,16 @@ pub fn serialize_undo_map_entry(
 }
 
 pub fn serialize_selections(selections: &Arc<[Selection<Anchor>]>) -> Vec<proto::Selection> {
-    selections
-        .iter()
-        .map(|selection| proto::Selection {
-            id: selection.id as u64,
-            start: Some(serialize_anchor(&selection.start)),
-            end: Some(serialize_anchor(&selection.end)),
-            reversed: selection.reversed,
-        })
-        .collect()
+    selections.iter().map(serialize_selection).collect()
+}
+
+pub fn serialize_selection(selection: &Selection<Anchor>) -> proto::Selection {
+    proto::Selection {
+        id: selection.id as u64,
+        start: Some(serialize_anchor(&selection.start)),
+        end: Some(serialize_anchor(&selection.end)),
+        reversed: selection.reversed,
+    }
 }
 
 pub fn serialize_diagnostics<'a>(
@@ -274,19 +275,21 @@ pub fn deserialize_selections(selections: Vec<proto::Selection>) -> Arc<[Selecti
     Arc::from(
         selections
             .into_iter()
-            .filter_map(|selection| {
-                Some(Selection {
-                    id: selection.id as usize,
-                    start: deserialize_anchor(selection.start?)?,
-                    end: deserialize_anchor(selection.end?)?,
-                    reversed: selection.reversed,
-                    goal: SelectionGoal::None,
-                })
-            })
+            .filter_map(deserialize_selection)
             .collect::<Vec<_>>(),
     )
 }
 
+pub fn deserialize_selection(selection: proto::Selection) -> Option<Selection<Anchor>> {
+    Some(Selection {
+        id: selection.id as usize,
+        start: deserialize_anchor(selection.start?)?,
+        end: deserialize_anchor(selection.end?)?,
+        reversed: selection.reversed,
+        goal: SelectionGoal::None,
+    })
+}
+
 pub fn deserialize_diagnostics(
     diagnostics: Vec<proto::Diagnostic>,
 ) -> Arc<[DiagnosticEntry<Anchor>]> {

crates/language/src/tests.rs 🔗

@@ -508,6 +508,44 @@ fn test_enclosing_bracket_ranges(cx: &mut MutableAppContext) {
     );
 }
 
+#[gpui::test]
+fn test_range_for_syntax_ancestor(cx: &mut MutableAppContext) {
+    cx.add_model(|cx| {
+        let text = "fn a() { b(|c| {}) }";
+        let buffer = Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx);
+        let snapshot = buffer.snapshot();
+
+        assert_eq!(
+            snapshot.range_for_syntax_ancestor(empty_range_at(text, "|")),
+            Some(range_of(text, "|"))
+        );
+        assert_eq!(
+            snapshot.range_for_syntax_ancestor(range_of(text, "|")),
+            Some(range_of(text, "|c|"))
+        );
+        assert_eq!(
+            snapshot.range_for_syntax_ancestor(range_of(text, "|c|")),
+            Some(range_of(text, "|c| {}"))
+        );
+        assert_eq!(
+            snapshot.range_for_syntax_ancestor(range_of(text, "|c| {}")),
+            Some(range_of(text, "(|c| {})"))
+        );
+
+        buffer
+    });
+
+    fn empty_range_at(text: &str, part: &str) -> Range<usize> {
+        let start = text.find(part).unwrap();
+        start..start
+    }
+
+    fn range_of(text: &str, part: &str) -> Range<usize> {
+        let start = text.find(part).unwrap();
+        start..start + part.len()
+    }
+}
+
 #[gpui::test]
 fn test_edit_with_autoindent(cx: &mut MutableAppContext) {
     cx.add_model(|cx| {
@@ -839,7 +877,7 @@ fn test_random_collaboration(cx: &mut MutableAppContext, mut rng: StdRng) {
     for buffer in &buffers {
         let buffer = buffer.read(cx).snapshot();
         let actual_remote_selections = buffer
-            .remote_selections_in_range(Anchor::min()..Anchor::max())
+            .remote_selections_in_range(Anchor::MIN..Anchor::MAX)
             .map(|(replica_id, selections)| (replica_id, selections.collect::<Vec<_>>()))
             .collect::<Vec<_>>();
         let expected_remote_selections = active_selections

crates/lsp/src/lsp.rs 🔗

@@ -556,7 +556,14 @@ type FakeLanguageServerHandlers = Arc<
     Mutex<
         HashMap<
             &'static str,
-            Box<dyn Send + FnMut(usize, &[u8], gpui::AsyncAppContext) -> Vec<u8>>,
+            Box<
+                dyn Send
+                    + FnMut(
+                        usize,
+                        &[u8],
+                        gpui::AsyncAppContext,
+                    ) -> futures::future::BoxFuture<'static, Vec<u8>>,
+            >,
         >,
     >,
 >;
@@ -585,11 +592,16 @@ impl LanguageServer {
         let (stdout_writer, stdout_reader) = async_pipe::pipe();
 
         let mut fake = FakeLanguageServer::new(stdin_reader, stdout_writer, cx);
-        fake.handle_request::<request::Initialize, _>({
+        fake.handle_request::<request::Initialize, _, _>({
             let capabilities = capabilities.clone();
-            move |_, _| InitializeResult {
-                capabilities: capabilities.clone(),
-                ..Default::default()
+            move |_, _| {
+                let capabilities = capabilities.clone();
+                async move {
+                    InitializeResult {
+                        capabilities,
+                        ..Default::default()
+                    }
+                }
             }
         });
 
@@ -628,7 +640,8 @@ impl FakeLanguageServer {
                         let response;
                         if let Some(handler) = handlers.lock().get_mut(request.method) {
                             response =
-                                handler(request.id, request.params.get().as_bytes(), cx.clone());
+                                handler(request.id, request.params.get().as_bytes(), cx.clone())
+                                    .await;
                             log::debug!("handled lsp request. method:{}", request.method);
                         } else {
                             response = serde_json::to_vec(&AnyResponse {
@@ -704,28 +717,36 @@ impl FakeLanguageServer {
         }
     }
 
-    pub fn handle_request<T, F>(
+    pub fn handle_request<T, F, Fut>(
         &mut self,
         mut handler: F,
     ) -> futures::channel::mpsc::UnboundedReceiver<()>
     where
         T: 'static + request::Request,
-        F: 'static + Send + FnMut(T::Params, gpui::AsyncAppContext) -> T::Result,
+        F: 'static + Send + FnMut(T::Params, gpui::AsyncAppContext) -> Fut,
+        Fut: 'static + Send + Future<Output = T::Result>,
     {
+        use futures::FutureExt as _;
+
         let (responded_tx, responded_rx) = futures::channel::mpsc::unbounded();
         self.handlers.lock().insert(
             T::METHOD,
             Box::new(move |id, params, cx| {
                 let result = handler(serde_json::from_slice::<T::Params>(params).unwrap(), cx);
-                let result = serde_json::to_string(&result).unwrap();
-                let result = serde_json::from_str::<&RawValue>(&result).unwrap();
-                let response = AnyResponse {
-                    id,
-                    error: None,
-                    result: Some(result),
-                };
-                responded_tx.unbounded_send(()).ok();
-                serde_json::to_vec(&response).unwrap()
+                let responded_tx = responded_tx.clone();
+                async move {
+                    let result = result.await;
+                    let result = serde_json::to_string(&result).unwrap();
+                    let result = serde_json::from_str::<&RawValue>(&result).unwrap();
+                    let response = AnyResponse {
+                        id,
+                        error: None,
+                        result: Some(result),
+                    };
+                    responded_tx.unbounded_send(()).ok();
+                    serde_json::to_vec(&response).unwrap()
+                }
+                .boxed()
             }),
         );
         responded_rx
@@ -844,7 +865,7 @@ mod tests {
             "file://b/c"
         );
 
-        fake.handle_request::<request::Shutdown, _>(|_, _| ());
+        fake.handle_request::<request::Shutdown, _, _>(|_, _| async move {});
 
         drop(server);
         fake.receive_notification::<notification::Exit>().await;

crates/outline/src/outline.rs 🔗

@@ -69,7 +69,7 @@ impl View for OutlineView {
     }
 
     fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
-        let settings = cx.app_state::<Settings>();
+        let settings = cx.global::<Settings>();
 
         Flex::new(Axis::Vertical)
             .with_child(
@@ -124,9 +124,12 @@ impl OutlineView {
             .active_item(cx)
             .and_then(|item| item.downcast::<Editor>())
         {
-            let buffer = editor.read(cx).buffer().read(cx).read(cx).outline(Some(
-                cx.app_state::<Settings>().theme.editor.syntax.as_ref(),
-            ));
+            let buffer = editor
+                .read(cx)
+                .buffer()
+                .read(cx)
+                .read(cx)
+                .outline(Some(cx.global::<Settings>().theme.editor.syntax.as_ref()));
             if let Some(outline) = buffer {
                 workspace.toggle_modal(cx, |cx, _| {
                     let view = cx.add_view(|cx| OutlineView::new(outline, editor, cx));
@@ -221,7 +224,7 @@ impl OutlineView {
     ) {
         match event {
             editor::Event::Blurred => cx.emit(Event::Dismissed),
-            editor::Event::Edited => self.update_matches(cx),
+            editor::Event::BufferEdited { .. } => self.update_matches(cx),
             _ => {}
         }
     }
@@ -288,7 +291,7 @@ impl OutlineView {
 
     fn render_matches(&self, cx: &AppContext) -> ElementBox {
         if self.matches.is_empty() {
-            let settings = cx.app_state::<Settings>();
+            let settings = cx.global::<Settings>();
             return Container::new(
                 Label::new(
                     "No matches".into(),
@@ -330,7 +333,7 @@ impl OutlineView {
         index: usize,
         cx: &AppContext,
     ) -> ElementBox {
-        let settings = cx.app_state::<Settings>();
+        let settings = cx.global::<Settings>();
         let style = if index == self.selected_match_index {
             &settings.theme.selector.active_item
         } else {

crates/project/src/project.rs 🔗

@@ -11,15 +11,15 @@ use collections::{hash_map, BTreeMap, HashMap, HashSet};
 use futures::{future::Shared, Future, FutureExt, StreamExt, TryFutureExt};
 use fuzzy::{PathMatch, PathMatchCandidate, PathMatchCandidateSet};
 use gpui::{
-    AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext, Task,
-    UpgradeModelHandle, WeakModelHandle,
+    AnyModelHandle, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle,
+    MutableAppContext, Task, UpgradeModelHandle, WeakModelHandle,
 };
 use language::{
     proto::{deserialize_anchor, deserialize_version, serialize_anchor, serialize_version},
     range_from_lsp, Anchor, Bias, Buffer, CodeAction, CodeLabel, Completion, Diagnostic,
     DiagnosticEntry, DiagnosticSet, Event as BufferEvent, File as _, Language, LanguageRegistry,
-    LocalFile, OffsetRangeExt, Operation, PointUtf16, TextBufferSnapshot, ToLspPosition, ToOffset,
-    ToPointUtf16, Transaction,
+    LocalFile, OffsetRangeExt, Operation, Patch, PointUtf16, TextBufferSnapshot, ToLspPosition,
+    ToOffset, ToPointUtf16, Transaction,
 };
 use lsp::{DiagnosticSeverity, DiagnosticTag, DocumentHighlightKind, LanguageServer};
 use lsp_command::*;
@@ -39,7 +39,7 @@ use std::{
     path::{Component, Path, PathBuf},
     rc::Rc,
     sync::{
-        atomic::{AtomicBool, AtomicUsize},
+        atomic::{AtomicBool, AtomicUsize, Ordering::SeqCst},
         Arc,
     },
     time::Instant,
@@ -49,9 +49,13 @@ use util::{post_inc, ResultExt, TryFutureExt as _};
 pub use fs::*;
 pub use worktree::*;
 
+pub trait Item: Entity {
+    fn entry_id(&self, cx: &AppContext) -> Option<ProjectEntryId>;
+}
+
 pub struct Project {
     worktrees: Vec<WorktreeHandle>,
-    active_entry: Option<ProjectEntry>,
+    active_entry: Option<ProjectEntryId>,
     languages: Arc<LanguageRegistry>,
     language_servers: HashMap<(WorktreeId, Arc<str>), Arc<LanguageServer>>,
     started_language_servers: HashMap<(WorktreeId, Arc<str>), Task<Option<Arc<LanguageServer>>>>,
@@ -114,12 +118,14 @@ pub struct Collaborator {
 
 #[derive(Clone, Debug, PartialEq)]
 pub enum Event {
-    ActiveEntryChanged(Option<ProjectEntry>),
+    ActiveEntryChanged(Option<ProjectEntryId>),
     WorktreeRemoved(WorktreeId),
     DiskBasedDiagnosticsStarted,
     DiskBasedDiagnosticsUpdated,
     DiskBasedDiagnosticsFinished,
     DiagnosticsUpdated(ProjectPath),
+    RemoteIdChanged(Option<u64>),
+    CollaboratorLeft(PeerId),
 }
 
 enum LanguageServerEvent {
@@ -226,42 +232,58 @@ impl DiagnosticSummary {
     }
 }
 
-#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
-pub struct ProjectEntry {
-    pub worktree_id: WorktreeId,
-    pub entry_id: usize,
+#[derive(Clone, Copy, Debug, Default, Hash, PartialEq, Eq, PartialOrd, Ord)]
+pub struct ProjectEntryId(usize);
+
+impl ProjectEntryId {
+    pub fn new(counter: &AtomicUsize) -> Self {
+        Self(counter.fetch_add(1, SeqCst))
+    }
+
+    pub fn from_proto(id: u64) -> Self {
+        Self(id as usize)
+    }
+
+    pub fn to_proto(&self) -> u64 {
+        self.0 as u64
+    }
+
+    pub fn to_usize(&self) -> usize {
+        self.0
+    }
 }
 
 impl Project {
     pub fn init(client: &Arc<Client>) {
-        client.add_entity_message_handler(Self::handle_add_collaborator);
-        client.add_entity_message_handler(Self::handle_buffer_reloaded);
-        client.add_entity_message_handler(Self::handle_buffer_saved);
-        client.add_entity_message_handler(Self::handle_start_language_server);
-        client.add_entity_message_handler(Self::handle_update_language_server);
-        client.add_entity_message_handler(Self::handle_remove_collaborator);
-        client.add_entity_message_handler(Self::handle_register_worktree);
-        client.add_entity_message_handler(Self::handle_unregister_worktree);
-        client.add_entity_message_handler(Self::handle_unshare_project);
-        client.add_entity_message_handler(Self::handle_update_buffer_file);
-        client.add_entity_message_handler(Self::handle_update_buffer);
-        client.add_entity_message_handler(Self::handle_update_diagnostic_summary);
-        client.add_entity_message_handler(Self::handle_update_worktree);
-        client.add_entity_request_handler(Self::handle_apply_additional_edits_for_completion);
-        client.add_entity_request_handler(Self::handle_apply_code_action);
-        client.add_entity_request_handler(Self::handle_format_buffers);
-        client.add_entity_request_handler(Self::handle_get_code_actions);
-        client.add_entity_request_handler(Self::handle_get_completions);
-        client.add_entity_request_handler(Self::handle_lsp_command::<GetDefinition>);
-        client.add_entity_request_handler(Self::handle_lsp_command::<GetDocumentHighlights>);
-        client.add_entity_request_handler(Self::handle_lsp_command::<GetReferences>);
-        client.add_entity_request_handler(Self::handle_lsp_command::<PrepareRename>);
-        client.add_entity_request_handler(Self::handle_lsp_command::<PerformRename>);
-        client.add_entity_request_handler(Self::handle_search_project);
-        client.add_entity_request_handler(Self::handle_get_project_symbols);
-        client.add_entity_request_handler(Self::handle_open_buffer_for_symbol);
-        client.add_entity_request_handler(Self::handle_open_buffer);
-        client.add_entity_request_handler(Self::handle_save_buffer);
+        client.add_model_message_handler(Self::handle_add_collaborator);
+        client.add_model_message_handler(Self::handle_buffer_reloaded);
+        client.add_model_message_handler(Self::handle_buffer_saved);
+        client.add_model_message_handler(Self::handle_start_language_server);
+        client.add_model_message_handler(Self::handle_update_language_server);
+        client.add_model_message_handler(Self::handle_remove_collaborator);
+        client.add_model_message_handler(Self::handle_register_worktree);
+        client.add_model_message_handler(Self::handle_unregister_worktree);
+        client.add_model_message_handler(Self::handle_unshare_project);
+        client.add_model_message_handler(Self::handle_update_buffer_file);
+        client.add_model_message_handler(Self::handle_update_buffer);
+        client.add_model_message_handler(Self::handle_update_diagnostic_summary);
+        client.add_model_message_handler(Self::handle_update_worktree);
+        client.add_model_request_handler(Self::handle_apply_additional_edits_for_completion);
+        client.add_model_request_handler(Self::handle_apply_code_action);
+        client.add_model_request_handler(Self::handle_format_buffers);
+        client.add_model_request_handler(Self::handle_get_code_actions);
+        client.add_model_request_handler(Self::handle_get_completions);
+        client.add_model_request_handler(Self::handle_lsp_command::<GetDefinition>);
+        client.add_model_request_handler(Self::handle_lsp_command::<GetDocumentHighlights>);
+        client.add_model_request_handler(Self::handle_lsp_command::<GetReferences>);
+        client.add_model_request_handler(Self::handle_lsp_command::<PrepareRename>);
+        client.add_model_request_handler(Self::handle_lsp_command::<PerformRename>);
+        client.add_model_request_handler(Self::handle_search_project);
+        client.add_model_request_handler(Self::handle_get_project_symbols);
+        client.add_model_request_handler(Self::handle_open_buffer_for_symbol);
+        client.add_model_request_handler(Self::handle_open_buffer_by_id);
+        client.add_model_request_handler(Self::handle_open_buffer_by_path);
+        client.add_model_request_handler(Self::handle_save_buffer);
     }
 
     pub fn local(
@@ -280,31 +302,11 @@ impl Project {
                         let mut status = rpc.status();
                         while let Some(status) = status.next().await {
                             if let Some(this) = this.upgrade(&cx) {
-                                let remote_id = if status.is_connected() {
-                                    let response = rpc.request(proto::RegisterProject {}).await?;
-                                    Some(response.project_id)
+                                if status.is_connected() {
+                                    this.update(&mut cx, |this, cx| this.register(cx)).await?;
                                 } else {
-                                    None
-                                };
-
-                                if let Some(project_id) = remote_id {
-                                    let mut registrations = Vec::new();
-                                    this.update(&mut cx, |this, cx| {
-                                        for worktree in this.worktrees(cx).collect::<Vec<_>>() {
-                                            registrations.push(worktree.update(
-                                                cx,
-                                                |worktree, cx| {
-                                                    let worktree = worktree.as_local_mut().unwrap();
-                                                    worktree.register(project_id, cx)
-                                                },
-                                            ));
-                                        }
-                                    });
-                                    for registration in registrations {
-                                        registration.await?;
-                                    }
+                                    this.update(&mut cx, |this, cx| this.unregister(cx));
                                 }
-                                this.update(&mut cx, |this, cx| this.set_remote_id(remote_id, cx));
                             }
                         }
                         Ok(())
@@ -355,7 +357,7 @@ impl Project {
         fs: Arc<dyn Fs>,
         cx: &mut AsyncAppContext,
     ) -> Result<ModelHandle<Self>> {
-        client.authenticate_and_connect(&cx).await?;
+        client.authenticate_and_connect(true, &cx).await?;
 
         let response = client
             .request(proto::JoinProject {
@@ -468,7 +470,6 @@ impl Project {
         cx.update(|cx| Project::local(client, user_store, languages, fs, cx))
     }
 
-    #[cfg(any(test, feature = "test-support"))]
     pub fn buffer_for_id(&self, remote_id: u64, cx: &AppContext) -> Option<ModelHandle<Buffer>> {
         self.opened_buffers
             .get(&remote_id)
@@ -537,16 +538,54 @@ impl Project {
         &self.fs
     }
 
-    fn set_remote_id(&mut self, remote_id: Option<u64>, cx: &mut ModelContext<Self>) {
+    fn unregister(&mut self, cx: &mut ModelContext<Self>) {
+        self.unshare(cx);
+        for worktree in &self.worktrees {
+            if let Some(worktree) = worktree.upgrade(cx) {
+                worktree.update(cx, |worktree, _| {
+                    worktree.as_local_mut().unwrap().unregister();
+                });
+            }
+        }
+
         if let ProjectClientState::Local { remote_id_tx, .. } = &mut self.client_state {
-            *remote_id_tx.borrow_mut() = remote_id;
+            *remote_id_tx.borrow_mut() = None;
         }
 
         self.subscriptions.clear();
-        if let Some(remote_id) = remote_id {
-            self.subscriptions
-                .push(self.client.add_model_for_remote_entity(remote_id, cx));
-        }
+    }
+
+    fn register(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
+        self.unregister(cx);
+
+        let response = self.client.request(proto::RegisterProject {});
+        cx.spawn(|this, mut cx| async move {
+            let remote_id = response.await?.project_id;
+
+            let mut registrations = Vec::new();
+            this.update(&mut cx, |this, cx| {
+                if let ProjectClientState::Local { remote_id_tx, .. } = &mut this.client_state {
+                    *remote_id_tx.borrow_mut() = Some(remote_id);
+                }
+
+                cx.emit(Event::RemoteIdChanged(Some(remote_id)));
+
+                this.subscriptions
+                    .push(this.client.add_model_for_remote_entity(remote_id, cx));
+
+                for worktree in &this.worktrees {
+                    if let Some(worktree) = worktree.upgrade(cx) {
+                        registrations.push(worktree.update(cx, |worktree, cx| {
+                            let worktree = worktree.as_local_mut().unwrap();
+                            worktree.register(remote_id, cx)
+                        }));
+                    }
+                }
+            });
+
+            futures::future::try_join_all(registrations).await?;
+            Ok(())
+        })
     }
 
     pub fn remote_id(&self) -> Option<u64> {
@@ -623,6 +662,24 @@ impl Project {
             .find(|worktree| worktree.read(cx).id() == id)
     }
 
+    pub fn worktree_for_entry(
+        &self,
+        entry_id: ProjectEntryId,
+        cx: &AppContext,
+    ) -> Option<ModelHandle<Worktree>> {
+        self.worktrees(cx)
+            .find(|worktree| worktree.read(cx).contains_entry(entry_id))
+    }
+
+    pub fn worktree_id_for_entry(
+        &self,
+        entry_id: ProjectEntryId,
+        cx: &AppContext,
+    ) -> Option<WorktreeId> {
+        self.worktree_for_entry(entry_id, cx)
+            .map(|worktree| worktree.read(cx).id())
+    }
+
     pub fn share(&self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
         let rpc = self.client.clone();
         cx.spawn(|this, mut cx| async move {
@@ -685,59 +742,51 @@ impl Project {
         })
     }
 
-    pub fn unshare(&self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
+    pub fn unshare(&mut self, cx: &mut ModelContext<Self>) {
         let rpc = self.client.clone();
-        cx.spawn(|this, mut cx| async move {
-            let project_id = this.update(&mut cx, |this, cx| {
-                if let ProjectClientState::Local {
-                    is_shared,
-                    remote_id_rx,
-                    ..
-                } = &mut this.client_state
-                {
-                    *is_shared = false;
 
-                    for open_buffer in this.opened_buffers.values_mut() {
-                        match open_buffer {
-                            OpenBuffer::Strong(buffer) => {
-                                *open_buffer = OpenBuffer::Weak(buffer.downgrade());
-                            }
-                            _ => {}
-                        }
-                    }
+        if let ProjectClientState::Local {
+            is_shared,
+            remote_id_rx,
+            ..
+        } = &mut self.client_state
+        {
+            if !*is_shared {
+                return;
+            }
 
-                    for worktree_handle in this.worktrees.iter_mut() {
-                        match worktree_handle {
-                            WorktreeHandle::Strong(worktree) => {
-                                if !worktree.read(cx).is_visible() {
-                                    *worktree_handle = WorktreeHandle::Weak(worktree.downgrade());
-                                }
-                            }
-                            _ => {}
-                        }
+            *is_shared = false;
+            self.collaborators.clear();
+            self.shared_buffers.clear();
+            for worktree_handle in self.worktrees.iter_mut() {
+                if let WorktreeHandle::Strong(worktree) = worktree_handle {
+                    let is_visible = worktree.update(cx, |worktree, _| {
+                        worktree.as_local_mut().unwrap().unshare();
+                        worktree.is_visible()
+                    });
+                    if !is_visible {
+                        *worktree_handle = WorktreeHandle::Weak(worktree.downgrade());
                     }
-
-                    remote_id_rx
-                        .borrow()
-                        .ok_or_else(|| anyhow!("no project id"))
-                } else {
-                    Err(anyhow!("can't share a remote project"))
                 }
-            })?;
+            }
 
-            rpc.send(proto::UnshareProject { project_id })?;
-            this.update(&mut cx, |this, cx| {
-                this.collaborators.clear();
-                this.shared_buffers.clear();
-                for worktree in this.worktrees(cx).collect::<Vec<_>>() {
-                    worktree.update(cx, |worktree, _| {
-                        worktree.as_local_mut().unwrap().unshare();
-                    });
+            for open_buffer in self.opened_buffers.values_mut() {
+                match open_buffer {
+                    OpenBuffer::Strong(buffer) => {
+                        *open_buffer = OpenBuffer::Weak(buffer.downgrade());
+                    }
+                    _ => {}
                 }
-                cx.notify()
-            });
-            Ok(())
-        })
+            }
+
+            if let Some(project_id) = *remote_id_rx.borrow() {
+                rpc.send(proto::UnshareProject { project_id }).log_err();
+            }
+
+            cx.notify();
+        } else {
+            log::error!("attempted to unshare a remote project");
+        }
     }
 
     fn project_unshared(&mut self, cx: &mut ModelContext<Self>) {
@@ -785,6 +834,23 @@ impl Project {
         Ok(buffer)
     }
 
+    pub fn open_path(
+        &mut self,
+        path: impl Into<ProjectPath>,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<(ProjectEntryId, AnyModelHandle)>> {
+        let task = self.open_buffer(path, cx);
+        cx.spawn_weak(|_, cx| async move {
+            let buffer = task.await?;
+            let project_entry_id = buffer
+                .read_with(&cx, |buffer, cx| {
+                    File::from_dyn(buffer.file()).and_then(|file| file.project_entry_id(cx))
+                })
+                .ok_or_else(|| anyhow!("no project entry"))?;
+            Ok((project_entry_id, buffer.into()))
+        })
+    }
+
     pub fn open_buffer(
         &mut self,
         path: impl Into<ProjectPath>,
@@ -876,7 +942,7 @@ impl Project {
         let path_string = path.to_string_lossy().to_string();
         cx.spawn(|this, mut cx| async move {
             let response = rpc
-                .request(proto::OpenBuffer {
+                .request(proto::OpenBufferByPath {
                     project_id,
                     worktree_id: remote_worktree_id.to_proto(),
                     path: path_string,
@@ -925,6 +991,32 @@ impl Project {
         })
     }
 
+    pub fn open_buffer_by_id(
+        &mut self,
+        id: u64,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<ModelHandle<Buffer>>> {
+        if let Some(buffer) = self.buffer_for_id(id, cx) {
+            Task::ready(Ok(buffer))
+        } else if self.is_local() {
+            Task::ready(Err(anyhow!("buffer {} does not exist", id)))
+        } else if let Some(project_id) = self.remote_id() {
+            let request = self
+                .client
+                .request(proto::OpenBufferById { project_id, id });
+            cx.spawn(|this, mut cx| async move {
+                let buffer = request
+                    .await?
+                    .buffer
+                    .ok_or_else(|| anyhow!("invalid buffer"))?;
+                this.update(&mut cx, |this, cx| this.deserialize_buffer(buffer, cx))
+                    .await
+            })
+        } else {
+            Task::ready(Err(anyhow!("cannot open buffer while disconnected")))
+        }
+    }
+
     pub fn save_buffer_as(
         &mut self,
         buffer: ModelHandle<Buffer>,
@@ -1096,7 +1188,7 @@ impl Project {
                 });
                 cx.background().spawn(request).detach_and_log_err(cx);
             }
-            BufferEvent::Edited => {
+            BufferEvent::Edited { .. } => {
                 let language_server = self
                     .language_server_for_buffer(buffer.read(cx), cx)?
                     .clone();
@@ -1783,38 +1875,23 @@ impl Project {
         });
 
         let mut sanitized_diagnostics = Vec::new();
-        let mut edits_since_save = snapshot
-            .edits_since::<PointUtf16>(buffer.read(cx).saved_version())
-            .peekable();
-        let mut last_edit_old_end = PointUtf16::zero();
-        let mut last_edit_new_end = PointUtf16::zero();
-        'outer: for entry in diagnostics {
-            let mut start = entry.range.start;
-            let mut end = entry.range.end;
-
-            // Some diagnostics are based on files on disk instead of buffers'
-            // current contents. Adjust these diagnostics' ranges to reflect
-            // any unsaved edits.
+        let edits_since_save = Patch::new(
+            snapshot
+                .edits_since::<PointUtf16>(buffer.read(cx).saved_version())
+                .collect(),
+        );
+        for entry in diagnostics {
+            let start;
+            let end;
             if entry.diagnostic.is_disk_based {
-                while let Some(edit) = edits_since_save.peek() {
-                    if edit.old.end <= start {
-                        last_edit_old_end = edit.old.end;
-                        last_edit_new_end = edit.new.end;
-                        edits_since_save.next();
-                    } else if edit.old.start <= end && edit.old.end >= start {
-                        continue 'outer;
-                    } else {
-                        break;
-                    }
-                }
-
-                let start_overshoot = start - last_edit_old_end;
-                start = last_edit_new_end;
-                start += start_overshoot;
-
-                let end_overshoot = end - last_edit_old_end;
-                end = last_edit_new_end;
-                end += end_overshoot;
+                // Some diagnostics are based on files on disk instead of buffers'
+                // current contents. Adjust these diagnostics' ranges to reflect
+                // any unsaved edits.
+                start = edits_since_save.old_to_new(entry.range.start);
+                end = edits_since_save.old_to_new(entry.range.end);
+            } else {
+                start = entry.range.start;
+                end = entry.range.end;
             }
 
             let mut range = snapshot.clip_point_utf16(start, Bias::Left)
@@ -3163,10 +3240,7 @@ impl Project {
         let new_active_entry = entry.and_then(|project_path| {
             let worktree = self.worktree_for_id(project_path.worktree_id, cx)?;
             let entry = worktree.read(cx).entry_for_path(project_path.path)?;
-            Some(ProjectEntry {
-                worktree_id: project_path.worktree_id,
-                entry_id: entry.id,
-            })
+            Some(entry.id)
         });
         if new_active_entry != self.active_entry {
             self.active_entry = new_active_entry;
@@ -3217,10 +3291,25 @@ impl Project {
         }
     }
 
-    pub fn active_entry(&self) -> Option<ProjectEntry> {
+    pub fn active_entry(&self) -> Option<ProjectEntryId> {
         self.active_entry
     }
 
+    pub fn entry_for_path(&self, path: &ProjectPath, cx: &AppContext) -> Option<ProjectEntryId> {
+        self.worktree_for_id(path.worktree_id, cx)?
+            .read(cx)
+            .entry_for_path(&path.path)
+            .map(|entry| entry.id)
+    }
+
+    pub fn path_for_entry(&self, entry_id: ProjectEntryId, cx: &AppContext) -> Option<ProjectPath> {
+        let worktree = self.worktree_for_entry(entry_id, cx)?;
+        let worktree = worktree.read(cx);
+        let worktree_id = worktree.id();
+        let path = worktree.entry_for_id(entry_id)?.path.clone();
+        Some(ProjectPath { worktree_id, path })
+    }
+
     // RPC message handlers
 
     async fn handle_unshare_project(
@@ -3274,6 +3363,7 @@ impl Project {
                     buffer.update(cx, |buffer, cx| buffer.remove_peer(replica_id, cx));
                 }
             }
+            cx.emit(Event::CollaboratorLeft(peer_id));
             cx.notify();
             Ok(())
         })
@@ -3821,9 +3911,28 @@ impl Project {
         hasher.finalize().as_slice().try_into().unwrap()
     }
 
-    async fn handle_open_buffer(
+    async fn handle_open_buffer_by_id(
+        this: ModelHandle<Self>,
+        envelope: TypedEnvelope<proto::OpenBufferById>,
+        _: Arc<Client>,
+        mut cx: AsyncAppContext,
+    ) -> Result<proto::OpenBufferResponse> {
+        let peer_id = envelope.original_sender_id()?;
+        let buffer = this
+            .update(&mut cx, |this, cx| {
+                this.open_buffer_by_id(envelope.payload.id, cx)
+            })
+            .await?;
+        this.update(&mut cx, |this, cx| {
+            Ok(proto::OpenBufferResponse {
+                buffer: Some(this.serialize_buffer_for_peer(&buffer, peer_id, cx)),
+            })
+        })
+    }
+
+    async fn handle_open_buffer_by_path(
         this: ModelHandle<Self>,
-        envelope: TypedEnvelope<proto::OpenBuffer>,
+        envelope: TypedEnvelope<proto::OpenBufferByPath>,
         _: Arc<Client>,
         mut cx: AsyncAppContext,
     ) -> Result<proto::OpenBufferResponse> {
@@ -4477,6 +4586,12 @@ fn relativize_path(base: &Path, path: &Path) -> PathBuf {
     components.iter().map(|c| c.as_os_str()).collect()
 }
 
+impl Item for Buffer {
+    fn entry_id(&self, cx: &AppContext) -> Option<ProjectEntryId> {
+        File::from_dyn(self.file()).and_then(|file| file.project_entry_id(cx))
+    }
+}
+
 #[cfg(test)]
 mod tests {
     use super::{Event, *};
@@ -4897,7 +5012,7 @@ mod tests {
     }
 
     #[gpui::test]
-    async fn test_transforming_disk_based_diagnostics(cx: &mut gpui::TestAppContext) {
+    async fn test_transforming_diagnostics(cx: &mut gpui::TestAppContext) {
         cx.foreground().forbid_parking();
 
         let (mut lsp_config, mut fake_servers) = LanguageServerConfig::fake();
@@ -5122,11 +5237,13 @@ mod tests {
         buffer.update(cx, |buffer, cx| {
             buffer.edit(Some(Point::new(2, 0)..Point::new(2, 0)), "    ", cx);
             buffer.edit(Some(Point::new(2, 8)..Point::new(2, 10)), "(x: usize)", cx);
+            buffer.edit(Some(Point::new(3, 10)..Point::new(3, 10)), "xxx", cx);
         });
-        let change_notification_2 =
-            fake_server.receive_notification::<lsp::notification::DidChangeTextDocument>();
+        let change_notification_2 = fake_server
+            .receive_notification::<lsp::notification::DidChangeTextDocument>()
+            .await;
         assert!(
-            change_notification_2.await.text_document.version
+            change_notification_2.text_document.version
                 > change_notification_1.text_document.version
         );
 
@@ -5134,7 +5251,7 @@ mod tests {
         fake_server.notify::<lsp::notification::PublishDiagnostics>(
             lsp::PublishDiagnosticsParams {
                 uri: lsp::Url::from_file_path("/dir/a.rs").unwrap(),
-                version: Some(open_notification.text_document.version),
+                version: Some(change_notification_2.text_document.version),
                 diagnostics: vec![
                     lsp::Diagnostic {
                         range: lsp::Range::new(lsp::Position::new(1, 9), lsp::Position::new(1, 11)),
@@ -5174,7 +5291,7 @@ mod tests {
                         }
                     },
                     DiagnosticEntry {
-                        range: Point::new(3, 9)..Point::new(3, 11),
+                        range: Point::new(3, 9)..Point::new(3, 14),
                         diagnostic: Diagnostic {
                             severity: DiagnosticSeverity::ERROR,
                             message: "undefined variable 'BB'".to_string(),
@@ -5672,7 +5789,7 @@ mod tests {
             .unwrap();
 
         let mut fake_server = fake_servers.next().await.unwrap();
-        fake_server.handle_request::<lsp::request::GotoDefinition, _>(move |params, _| {
+        fake_server.handle_request::<lsp::request::GotoDefinition, _, _>(|params, _| async move {
             let params = params.text_document_position_params;
             assert_eq!(
                 params.text_document.uri.to_file_path().unwrap(),
@@ -6607,7 +6724,7 @@ mod tests {
             project.prepare_rename(buffer.clone(), 7, cx)
         });
         fake_server
-            .handle_request::<lsp::request::PrepareRenameRequest, _>(|params, _| {
+            .handle_request::<lsp::request::PrepareRenameRequest, _, _>(|params, _| async move {
                 assert_eq!(params.text_document.uri.as_str(), "file:///dir/one.rs");
                 assert_eq!(params.position, lsp::Position::new(0, 7));
                 Some(lsp::PrepareRenameResponse::Range(lsp::Range::new(
@@ -6626,7 +6743,7 @@ mod tests {
             project.perform_rename(buffer.clone(), 7, "THREE".to_string(), true, cx)
         });
         fake_server
-            .handle_request::<lsp::request::Rename, _>(|params, _| {
+            .handle_request::<lsp::request::Rename, _, _>(|params, _| async move {
                 assert_eq!(
                     params.text_document_position.text_document.uri.as_str(),
                     "file:///dir/one.rs"

crates/project/src/worktree.rs 🔗

@@ -1,3 +1,5 @@
+use crate::ProjectEntryId;
+
 use super::{
     fs::{self, Fs},
     ignore::IgnoreStack,
@@ -39,10 +41,7 @@ use std::{
     future::Future,
     ops::{Deref, DerefMut},
     path::{Path, PathBuf},
-    sync::{
-        atomic::{AtomicUsize, Ordering::SeqCst},
-        Arc,
-    },
+    sync::{atomic::AtomicUsize, Arc},
     time::{Duration, SystemTime},
 };
 use sum_tree::{Bias, Edit, SeekTarget, SumTree, TreeMap};
@@ -101,7 +100,7 @@ pub struct LocalSnapshot {
     abs_path: Arc<Path>,
     scan_id: usize,
     ignores: HashMap<Arc<Path>, (Arc<Gitignore>, usize)>,
-    removed_entry_ids: HashMap<u64, usize>,
+    removed_entry_ids: HashMap<u64, ProjectEntryId>,
     next_entry_id: Arc<AtomicUsize>,
     snapshot: Snapshot,
 }
@@ -712,7 +711,9 @@ impl LocalWorktree {
                 let worktree = this.as_local_mut().unwrap();
                 match response {
                     Ok(_) => {
-                        worktree.registration = Registration::Done { project_id };
+                        if worktree.registration == Registration::Pending {
+                            worktree.registration = Registration::Done { project_id };
+                        }
                         Ok(())
                     }
                     Err(error) => {
@@ -809,6 +810,11 @@ impl LocalWorktree {
         })
     }
 
+    pub fn unregister(&mut self) {
+        self.unshare();
+        self.registration = Registration::None;
+    }
+
     pub fn unshare(&mut self) {
         self.share.take();
     }
@@ -856,13 +862,16 @@ impl Snapshot {
         self.id
     }
 
+    pub fn contains_entry(&self, entry_id: ProjectEntryId) -> bool {
+        self.entries_by_id.get(&entry_id, &()).is_some()
+    }
+
     pub(crate) fn apply_remote_update(&mut self, update: proto::UpdateWorktree) -> Result<()> {
         let mut entries_by_path_edits = Vec::new();
         let mut entries_by_id_edits = Vec::new();
         for entry_id in update.removed_entries {
-            let entry_id = entry_id as usize;
             let entry = self
-                .entry_for_id(entry_id)
+                .entry_for_id(ProjectEntryId::from_proto(entry_id))
                 .ok_or_else(|| anyhow!("unknown entry"))?;
             entries_by_path_edits.push(Edit::Remove(PathKey(entry.path.clone())));
             entries_by_id_edits.push(Edit::Remove(entry.id));
@@ -985,7 +994,7 @@ impl Snapshot {
             })
     }
 
-    pub fn entry_for_id(&self, id: usize) -> Option<&Entry> {
+    pub fn entry_for_id(&self, id: ProjectEntryId) -> Option<&Entry> {
         let entry = self.entries_by_id.get(&id, &())?;
         self.entry_for_path(&entry.path)
     }
@@ -1062,7 +1071,7 @@ impl LocalSnapshot {
                             other_entries.next();
                         }
                         Ordering::Greater => {
-                            removed_entries.push(other_entry.id as u64);
+                            removed_entries.push(other_entry.id.to_proto());
                             other_entries.next();
                         }
                     }
@@ -1073,7 +1082,7 @@ impl LocalSnapshot {
                     self_entries.next();
                 }
                 (None, Some(other_entry)) => {
-                    removed_entries.push(other_entry.id as u64);
+                    removed_entries.push(other_entry.id.to_proto());
                     other_entries.next();
                 }
                 (None, None) => break,
@@ -1326,7 +1335,7 @@ pub struct File {
     pub worktree: ModelHandle<Worktree>,
     pub path: Arc<Path>,
     pub mtime: SystemTime,
-    pub(crate) entry_id: Option<usize>,
+    pub(crate) entry_id: Option<ProjectEntryId>,
     pub(crate) is_local: bool,
 }
 
@@ -1423,7 +1432,7 @@ impl language::File for File {
     fn to_proto(&self) -> rpc::proto::File {
         rpc::proto::File {
             worktree_id: self.worktree.id() as u64,
-            entry_id: self.entry_id.map(|entry_id| entry_id as u64),
+            entry_id: self.entry_id.map(|entry_id| entry_id.to_proto()),
             path: self.path.to_string_lossy().into(),
             mtime: Some(self.mtime.into()),
         }
@@ -1490,7 +1499,7 @@ impl File {
             worktree,
             path: Path::new(&proto.path).into(),
             mtime: proto.mtime.ok_or_else(|| anyhow!("no timestamp"))?.into(),
-            entry_id: proto.entry_id.map(|entry_id| entry_id as usize),
+            entry_id: proto.entry_id.map(ProjectEntryId::from_proto),
             is_local: false,
         })
     }
@@ -1502,11 +1511,15 @@ impl File {
     pub fn worktree_id(&self, cx: &AppContext) -> WorktreeId {
         self.worktree.read(cx).id()
     }
+
+    pub fn project_entry_id(&self, _: &AppContext) -> Option<ProjectEntryId> {
+        self.entry_id
+    }
 }
 
 #[derive(Clone, Debug, PartialEq, Eq)]
 pub struct Entry {
-    pub id: usize,
+    pub id: ProjectEntryId,
     pub kind: EntryKind,
     pub path: Arc<Path>,
     pub inode: u64,
@@ -1530,7 +1543,7 @@ impl Entry {
         root_char_bag: CharBag,
     ) -> Self {
         Self {
-            id: next_entry_id.fetch_add(1, SeqCst),
+            id: ProjectEntryId::new(next_entry_id),
             kind: if metadata.is_dir {
                 EntryKind::PendingDir
             } else {
@@ -1620,7 +1633,7 @@ impl sum_tree::Summary for EntrySummary {
 
 #[derive(Clone, Debug)]
 struct PathEntry {
-    id: usize,
+    id: ProjectEntryId,
     path: Arc<Path>,
     is_ignored: bool,
     scan_id: usize,
@@ -1635,7 +1648,7 @@ impl sum_tree::Item for PathEntry {
 }
 
 impl sum_tree::KeyedItem for PathEntry {
-    type Key = usize;
+    type Key = ProjectEntryId;
 
     fn key(&self) -> Self::Key {
         self.id
@@ -1644,7 +1657,7 @@ impl sum_tree::KeyedItem for PathEntry {
 
 #[derive(Clone, Debug, Default)]
 struct PathEntrySummary {
-    max_id: usize,
+    max_id: ProjectEntryId,
 }
 
 impl sum_tree::Summary for PathEntrySummary {
@@ -1655,7 +1668,7 @@ impl sum_tree::Summary for PathEntrySummary {
     }
 }
 
-impl<'a> sum_tree::Dimension<'a, PathEntrySummary> for usize {
+impl<'a> sum_tree::Dimension<'a, PathEntrySummary> for ProjectEntryId {
     fn add_summary(&mut self, summary: &'a PathEntrySummary, _: &()) {
         *self = summary.max_id;
     }
@@ -2345,7 +2358,7 @@ impl<'a> Iterator for ChildEntriesIter<'a> {
 impl<'a> From<&'a Entry> for proto::Entry {
     fn from(entry: &'a Entry) -> Self {
         Self {
-            id: entry.id as u64,
+            id: entry.id.to_proto(),
             is_dir: entry.is_dir(),
             path: entry.path.to_string_lossy().to_string(),
             inode: entry.inode,
@@ -2370,7 +2383,7 @@ impl<'a> TryFrom<(&'a CharBag, proto::Entry)> for Entry {
             };
             let path: Arc<Path> = Arc::from(Path::new(&entry.path));
             Ok(Entry {
-                id: entry.id as usize,
+                id: ProjectEntryId::from_proto(entry.id),
                 kind,
                 path: path.clone(),
                 inode: entry.inode,

crates/project_panel/src/project_panel.rs 🔗

@@ -9,7 +9,7 @@ use gpui::{
     AppContext, Element, ElementBox, Entity, ModelHandle, MutableAppContext, View, ViewContext,
     ViewHandle, WeakViewHandle,
 };
-use project::{Project, ProjectEntry, ProjectPath, Worktree, WorktreeId};
+use project::{Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId};
 use std::{
     collections::{hash_map, HashMap},
     ffi::OsStr,
@@ -24,7 +24,7 @@ pub struct ProjectPanel {
     project: ModelHandle<Project>,
     list: UniformListState,
     visible_entries: Vec<(WorktreeId, Vec<usize>)>,
-    expanded_dir_ids: HashMap<WorktreeId, Vec<usize>>,
+    expanded_dir_ids: HashMap<WorktreeId, Vec<ProjectEntryId>>,
     selection: Option<Selection>,
     handle: WeakViewHandle<Self>,
 }
@@ -32,7 +32,7 @@ pub struct ProjectPanel {
 #[derive(Copy, Clone)]
 struct Selection {
     worktree_id: WorktreeId,
-    entry_id: usize,
+    entry_id: ProjectEntryId,
     index: usize,
 }
 
@@ -47,8 +47,8 @@ struct EntryDetails {
 
 action!(ExpandSelectedEntry);
 action!(CollapseSelectedEntry);
-action!(ToggleExpanded, ProjectEntry);
-action!(Open, ProjectEntry);
+action!(ToggleExpanded, ProjectEntryId);
+action!(Open, ProjectEntryId);
 
 pub fn init(cx: &mut MutableAppContext) {
     cx.add_action(ProjectPanel::expand_selected_entry);
@@ -64,10 +64,7 @@ pub fn init(cx: &mut MutableAppContext) {
 }
 
 pub enum Event {
-    OpenedEntry {
-        worktree_id: WorktreeId,
-        entry_id: usize,
-    },
+    OpenedEntry(ProjectEntryId),
 }
 
 impl ProjectPanel {
@@ -78,15 +75,15 @@ impl ProjectPanel {
                 cx.notify();
             })
             .detach();
-            cx.subscribe(&project, |this, _, event, cx| match event {
-                project::Event::ActiveEntryChanged(Some(ProjectEntry {
-                    worktree_id,
-                    entry_id,
-                })) => {
-                    this.expand_entry(*worktree_id, *entry_id, cx);
-                    this.update_visible_entries(Some((*worktree_id, *entry_id)), cx);
-                    this.autoscroll();
-                    cx.notify();
+            cx.subscribe(&project, |this, project, event, cx| match event {
+                project::Event::ActiveEntryChanged(Some(entry_id)) => {
+                    if let Some(worktree_id) = project.read(cx).worktree_id_for_entry(*entry_id, cx)
+                    {
+                        this.expand_entry(worktree_id, *entry_id, cx);
+                        this.update_visible_entries(Some((worktree_id, *entry_id)), cx);
+                        this.autoscroll();
+                        cx.notify();
+                    }
                 }
                 project::Event::WorktreeRemoved(id) => {
                     this.expanded_dir_ids.remove(id);
@@ -109,16 +106,13 @@ impl ProjectPanel {
             this
         });
         cx.subscribe(&project_panel, move |workspace, _, event, cx| match event {
-            &Event::OpenedEntry {
-                worktree_id,
-                entry_id,
-            } => {
-                if let Some(worktree) = project.read(cx).worktree_for_id(worktree_id, cx) {
+            &Event::OpenedEntry(entry_id) => {
+                if let Some(worktree) = project.read(cx).worktree_for_entry(entry_id, cx) {
                     if let Some(entry) = worktree.read(cx).entry_for_id(entry_id) {
                         workspace
                             .open_path(
                                 ProjectPath {
-                                    worktree_id,
+                                    worktree_id: worktree.read(cx).id(),
                                     path: entry.path.clone(),
                                 },
                                 cx,
@@ -152,10 +146,7 @@ impl ProjectPanel {
                     }
                 }
             } else {
-                let event = Event::OpenedEntry {
-                    worktree_id: worktree.id(),
-                    entry_id: entry.id,
-                };
+                let event = Event::OpenedEntry(entry.id);
                 cx.emit(event);
             }
         }
@@ -193,22 +184,20 @@ impl ProjectPanel {
     }
 
     fn toggle_expanded(&mut self, action: &ToggleExpanded, cx: &mut ViewContext<Self>) {
-        let ProjectEntry {
-            worktree_id,
-            entry_id,
-        } = action.0;
-
-        if let Some(expanded_dir_ids) = self.expanded_dir_ids.get_mut(&worktree_id) {
-            match expanded_dir_ids.binary_search(&entry_id) {
-                Ok(ix) => {
-                    expanded_dir_ids.remove(ix);
-                }
-                Err(ix) => {
-                    expanded_dir_ids.insert(ix, entry_id);
+        let entry_id = action.0;
+        if let Some(worktree_id) = self.project.read(cx).worktree_id_for_entry(entry_id, cx) {
+            if let Some(expanded_dir_ids) = self.expanded_dir_ids.get_mut(&worktree_id) {
+                match expanded_dir_ids.binary_search(&entry_id) {
+                    Ok(ix) => {
+                        expanded_dir_ids.remove(ix);
+                    }
+                    Err(ix) => {
+                        expanded_dir_ids.insert(ix, entry_id);
+                    }
                 }
+                self.update_visible_entries(Some((worktree_id, entry_id)), cx);
+                cx.focus_self();
             }
-            self.update_visible_entries(Some((worktree_id, entry_id)), cx);
-            cx.focus_self();
         }
     }
 
@@ -229,10 +218,7 @@ impl ProjectPanel {
     }
 
     fn open_entry(&mut self, action: &Open, cx: &mut ViewContext<Self>) {
-        cx.emit(Event::OpenedEntry {
-            worktree_id: action.0.worktree_id,
-            entry_id: action.0.entry_id,
-        });
+        cx.emit(Event::OpenedEntry(action.0));
     }
 
     fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
@@ -313,7 +299,7 @@ impl ProjectPanel {
 
     fn update_visible_entries(
         &mut self,
-        new_selected_entry: Option<(WorktreeId, usize)>,
+        new_selected_entry: Option<(WorktreeId, ProjectEntryId)>,
         cx: &mut ViewContext<Self>,
     ) {
         let worktrees = self
@@ -379,7 +365,7 @@ impl ProjectPanel {
     fn expand_entry(
         &mut self,
         worktree_id: WorktreeId,
-        entry_id: usize,
+        entry_id: ProjectEntryId,
         cx: &mut ViewContext<Self>,
     ) {
         let project = self.project.read(cx);
@@ -411,7 +397,7 @@ impl ProjectPanel {
         &self,
         range: Range<usize>,
         cx: &mut ViewContext<ProjectPanel>,
-        mut callback: impl FnMut(ProjectEntry, EntryDetails, &mut ViewContext<ProjectPanel>),
+        mut callback: impl FnMut(ProjectEntryId, EntryDetails, &mut ViewContext<ProjectPanel>),
     ) {
         let mut ix = 0;
         for (worktree_id, visible_worktree_entries) in &self.visible_entries {
@@ -450,11 +436,7 @@ impl ProjectPanel {
                                 e.worktree_id == snapshot.id() && e.entry_id == entry.id
                             }),
                         };
-                        let entry = ProjectEntry {
-                            worktree_id: snapshot.id(),
-                            entry_id: entry.id,
-                        };
-                        callback(entry, details, cx);
+                        callback(entry.id, details, cx);
                     }
                 }
             }
@@ -463,13 +445,13 @@ impl ProjectPanel {
     }
 
     fn render_entry(
-        entry: ProjectEntry,
+        entry_id: ProjectEntryId,
         details: EntryDetails,
         theme: &theme::ProjectPanel,
         cx: &mut ViewContext<Self>,
     ) -> ElementBox {
         let is_dir = details.is_dir;
-        MouseEventHandler::new::<Self, _, _>(entry.entry_id, cx, |state, _| {
+        MouseEventHandler::new::<Self, _, _>(entry_id.to_usize(), cx, |state, _| {
             let style = match (details.is_selected, state.hovered) {
                 (false, false) => &theme.entry,
                 (false, true) => &theme.hovered_entry,
@@ -519,9 +501,9 @@ impl ProjectPanel {
         })
         .on_click(move |cx| {
             if is_dir {
-                cx.dispatch_action(ToggleExpanded(entry))
+                cx.dispatch_action(ToggleExpanded(entry_id))
             } else {
-                cx.dispatch_action(Open(entry))
+                cx.dispatch_action(Open(entry_id))
             }
         })
         .with_cursor_style(CursorStyle::PointingHand)
@@ -535,7 +517,7 @@ impl View for ProjectPanel {
     }
 
     fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> gpui::ElementBox {
-        let theme = &cx.app_state::<Settings>().theme.project_panel;
+        let theme = &cx.global::<Settings>().theme.project_panel;
         let mut container_style = theme.container;
         let padding = std::mem::take(&mut container_style.padding);
         let handle = self.handle.clone();
@@ -546,7 +528,7 @@ impl View for ProjectPanel {
                 .map(|(_, worktree_entries)| worktree_entries.len())
                 .sum(),
             move |range, items, cx| {
-                let theme = cx.app_state::<Settings>().theme.clone();
+                let theme = cx.global::<Settings>().theme.clone();
                 let this = handle.upgrade(cx).unwrap();
                 this.update(cx.app, |this, cx| {
                     this.for_each_visible_entry(range.clone(), cx, |entry, details, cx| {
@@ -830,13 +812,7 @@ mod tests {
                     let worktree = worktree.read(cx);
                     if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) {
                         let entry_id = worktree.entry_for_path(relative_path).unwrap().id;
-                        panel.toggle_expanded(
-                            &ToggleExpanded(ProjectEntry {
-                                worktree_id: worktree.id(),
-                                entry_id,
-                            }),
-                            cx,
-                        );
+                        panel.toggle_expanded(&ToggleExpanded(entry_id), cx);
                         return;
                     }
                 }

crates/project_symbols/src/project_symbols.rs 🔗

@@ -1,6 +1,5 @@
 use editor::{
-    combine_syntax_and_fuzzy_match_highlights, items::BufferItemHandle, styled_runs_for_code_label,
-    Autoscroll, Bias, Editor,
+    combine_syntax_and_fuzzy_match_highlights, styled_runs_for_code_label, Autoscroll, Bias, Editor,
 };
 use fuzzy::{StringMatch, StringMatchCandidate};
 use gpui::{
@@ -70,7 +69,7 @@ impl View for ProjectSymbolsView {
     }
 
     fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
-        let settings = cx.app_state::<Settings>();
+        let settings = cx.global::<Settings>();
         Flex::new(Axis::Vertical)
             .with_child(
                 Container::new(ChildView::new(&self.query_editor).boxed())
@@ -234,7 +233,7 @@ impl ProjectSymbolsView {
 
     fn render_matches(&self, cx: &AppContext) -> ElementBox {
         if self.matches.is_empty() {
-            let settings = cx.app_state::<Settings>();
+            let settings = cx.global::<Settings>();
             return Container::new(
                 Label::new(
                     "No matches".into(),
@@ -277,7 +276,7 @@ impl ProjectSymbolsView {
         show_worktree_root_name: bool,
         cx: &AppContext,
     ) -> ElementBox {
-        let settings = cx.app_state::<Settings>();
+        let settings = cx.global::<Settings>();
         let style = if index == self.selected_match_index {
             &settings.theme.selector.active_item
         } else {
@@ -329,7 +328,7 @@ impl ProjectSymbolsView {
     ) {
         match event {
             editor::Event::Blurred => cx.emit(Event::Dismissed),
-            editor::Event::Edited => self.update_matches(cx),
+            editor::Event::BufferEdited { .. } => self.update_matches(cx),
             _ => {}
         }
     }
@@ -346,6 +345,7 @@ impl ProjectSymbolsView {
                 let buffer = workspace
                     .project()
                     .update(cx, |project, cx| project.open_buffer_for_symbol(symbol, cx));
+
                 let symbol = symbol.clone();
                 cx.spawn(|workspace, mut cx| async move {
                     let buffer = buffer.await?;
@@ -353,10 +353,8 @@ impl ProjectSymbolsView {
                         let position = buffer
                             .read(cx)
                             .clip_point_utf16(symbol.range.start, Bias::Left);
-                        let editor = workspace
-                            .open_item(BufferItemHandle(buffer), cx)
-                            .downcast::<Editor>()
-                            .unwrap();
+
+                        let editor = workspace.open_project_item::<Editor>(buffer, cx);
                         editor.update(cx, |editor, cx| {
                             editor.select_ranges(
                                 [position..position],

crates/rpc/proto/zed.proto 🔗

@@ -40,8 +40,9 @@ message Envelope {
         StartLanguageServer start_language_server = 33;
         UpdateLanguageServer update_language_server = 34;
 
-        OpenBuffer open_buffer = 35;
-        OpenBufferResponse open_buffer_response = 36;
+        OpenBufferById open_buffer_by_id = 35;
+        OpenBufferByPath open_buffer_by_path = 36;
+        OpenBufferResponse open_buffer_response = 37;
         UpdateBuffer update_buffer = 38;
         UpdateBufferFile update_buffer_file = 39;
         SaveBuffer save_buffer = 40;
@@ -79,6 +80,11 @@ message Envelope {
 
         GetUsers get_users = 70;
         GetUsersResponse get_users_response = 71;
+
+        Follow follow = 72;
+        FollowResponse follow_response = 73;
+        UpdateFollowers update_followers = 74;
+        Unfollow unfollow = 75;
     }
 }
 
@@ -241,12 +247,17 @@ message OpenBufferForSymbolResponse {
     Buffer buffer = 1;
 }
 
-message OpenBuffer {
+message OpenBufferByPath {
     uint64 project_id = 1;
     uint64 worktree_id = 2;
     string path = 3;
 }
 
+message OpenBufferById {
+    uint64 project_id = 1;
+    uint64 id = 2;
+}
+
 message OpenBufferResponse {
     Buffer buffer = 1;
 }
@@ -521,8 +532,77 @@ message UpdateContacts {
     repeated Contact contacts = 1;
 }
 
+message UpdateDiagnostics {
+    uint32 replica_id = 1;
+    uint32 lamport_timestamp = 2;
+    repeated Diagnostic diagnostics = 3;
+}
+
+message Follow {
+    uint64 project_id = 1;
+    uint32 leader_id = 2;
+}
+
+message FollowResponse {
+    optional uint64 active_view_id = 1;
+    repeated View views = 2;
+}
+
+message UpdateFollowers {
+    uint64 project_id = 1;
+    repeated uint32 follower_ids = 2;
+    oneof variant {
+        UpdateActiveView update_active_view = 3;
+        View create_view = 4;
+        UpdateView update_view = 5;
+    }
+}
+
+message Unfollow {
+    uint64 project_id = 1;
+    uint32 leader_id = 2;
+}
+
 // Entities
 
+message UpdateActiveView {
+    optional uint64 id = 1;
+    optional uint32 leader_id = 2;
+}
+
+message UpdateView {
+    uint64 id = 1;
+    optional uint32 leader_id = 2;
+
+    oneof variant {
+        Editor editor = 3;
+    }
+
+    message Editor {
+        repeated Selection selections = 1;
+        Anchor scroll_top_anchor = 2;
+        float scroll_x = 3;
+        float scroll_y = 4;
+    }
+}
+
+message View {
+    uint64 id = 1;
+    optional uint32 leader_id = 2;
+
+    oneof variant {
+        Editor editor = 3;
+    }
+
+    message Editor {
+        uint64 buffer_id = 1;
+        repeated Selection selections = 2;
+        Anchor scroll_top_anchor = 3;
+        float scroll_x = 4;
+        float scroll_y = 5;
+    }
+}
+
 message Collaborator {
     uint32 peer_id = 1;
     uint32 replica_id = 2;
@@ -578,17 +658,6 @@ message BufferState {
     repeated string completion_triggers = 8;
 }
 
-message BufferFragment {
-    uint32 replica_id = 1;
-    uint32 local_timestamp = 2;
-    uint32 lamport_timestamp = 3;
-    uint32 insertion_offset = 4;
-    uint32 len = 5;
-    bool visible = 6;
-    repeated VectorClockEntry deletions = 7;
-    repeated VectorClockEntry max_undos = 8;
-}
-
 message SelectionSet {
     uint32 replica_id = 1;
     repeated Selection selections = 2;
@@ -614,12 +683,6 @@ enum Bias {
     Right = 1;
 }
 
-message UpdateDiagnostics {
-    uint32 replica_id = 1;
-    uint32 lamport_timestamp = 2;
-    repeated Diagnostic diagnostics = 3;
-}
-
 message Diagnostic {
     Anchor start = 1;
     Anchor end = 2;

crates/rpc/src/peer.rs 🔗

@@ -96,7 +96,7 @@ pub struct ConnectionState {
 
 const KEEPALIVE_INTERVAL: Duration = Duration::from_secs(1);
 const WRITE_TIMEOUT: Duration = Duration::from_secs(2);
-const RECEIVE_TIMEOUT: Duration = Duration::from_secs(30);
+pub const RECEIVE_TIMEOUT: Duration = Duration::from_secs(5);
 
 impl Peer {
     pub fn new() -> Arc<Self> {

crates/rpc/src/proto.rs 🔗

@@ -147,6 +147,8 @@ messages!(
     (BufferSaved, Foreground),
     (ChannelMessageSent, Foreground),
     (Error, Foreground),
+    (Follow, Foreground),
+    (FollowResponse, Foreground),
     (FormatBuffers, Foreground),
     (FormatBuffersResponse, Foreground),
     (GetChannelMessages, Foreground),
@@ -175,7 +177,8 @@ messages!(
     (UpdateLanguageServer, Foreground),
     (LeaveChannel, Foreground),
     (LeaveProject, Foreground),
-    (OpenBuffer, Background),
+    (OpenBufferById, Background),
+    (OpenBufferByPath, Background),
     (OpenBufferForSymbol, Background),
     (OpenBufferForSymbolResponse, Background),
     (OpenBufferResponse, Background),
@@ -195,13 +198,15 @@ messages!(
     (SendChannelMessageResponse, Foreground),
     (ShareProject, Foreground),
     (Test, Foreground),
+    (Unfollow, Foreground),
     (UnregisterProject, Foreground),
     (UnregisterWorktree, Foreground),
     (UnshareProject, Foreground),
-    (UpdateBuffer, Background),
+    (UpdateBuffer, Foreground),
     (UpdateBufferFile, Foreground),
     (UpdateContacts, Foreground),
     (UpdateDiagnosticSummary, Foreground),
+    (UpdateFollowers, Foreground),
     (UpdateWorktree, Foreground),
 );
 
@@ -211,6 +216,7 @@ request_messages!(
         ApplyCompletionAdditionalEdits,
         ApplyCompletionAdditionalEditsResponse
     ),
+    (Follow, FollowResponse),
     (FormatBuffers, FormatBuffersResponse),
     (GetChannelMessages, GetChannelMessagesResponse),
     (GetChannels, GetChannelsResponse),
@@ -223,7 +229,8 @@ request_messages!(
     (GetUsers, GetUsersResponse),
     (JoinChannel, JoinChannelResponse),
     (JoinProject, JoinProjectResponse),
-    (OpenBuffer, OpenBufferResponse),
+    (OpenBufferById, OpenBufferResponse),
+    (OpenBufferByPath, OpenBufferResponse),
     (OpenBufferForSymbol, OpenBufferForSymbolResponse),
     (Ping, Ack),
     (PerformRename, PerformRenameResponse),
@@ -246,6 +253,7 @@ entity_messages!(
     ApplyCompletionAdditionalEdits,
     BufferReloaded,
     BufferSaved,
+    Follow,
     FormatBuffers,
     GetCodeActions,
     GetCompletions,
@@ -255,7 +263,8 @@ entity_messages!(
     GetProjectSymbols,
     JoinProject,
     LeaveProject,
-    OpenBuffer,
+    OpenBufferById,
+    OpenBufferByPath,
     OpenBufferForSymbol,
     PerformRename,
     PrepareRename,
@@ -263,11 +272,13 @@ entity_messages!(
     SaveBuffer,
     SearchProject,
     StartLanguageServer,
+    Unfollow,
     UnregisterWorktree,
     UnshareProject,
     UpdateBuffer,
     UpdateBufferFile,
     UpdateDiagnosticSummary,
+    UpdateFollowers,
     UpdateLanguageServer,
     RegisterWorktree,
     UpdateWorktree,

crates/rpc/src/rpc.rs 🔗

@@ -5,4 +5,4 @@ pub mod proto;
 pub use conn::Connection;
 pub use peer::*;
 
-pub const PROTOCOL_VERSION: u32 = 11;
+pub const PROTOCOL_VERSION: u32 = 12;

crates/search/src/buffer_search.rs 🔗

@@ -8,7 +8,7 @@ use gpui::{
 use language::OffsetRangeExt;
 use project::search::SearchQuery;
 use std::ops::Range;
-use workspace::{ItemViewHandle, Pane, Settings, Toolbar, Workspace};
+use workspace::{ItemHandle, Pane, Settings, Toolbar, Workspace};
 
 action!(Deploy, bool);
 action!(Dismiss);
@@ -66,7 +66,7 @@ impl View for SearchBar {
     }
 
     fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
-        let theme = cx.app_state::<Settings>().theme.clone();
+        let theme = cx.global::<Settings>().theme.clone();
         let editor_container = if self.query_contains_error {
             theme.search.invalid_editor
         } else {
@@ -126,7 +126,7 @@ impl View for SearchBar {
 impl Toolbar for SearchBar {
     fn active_item_changed(
         &mut self,
-        item: Option<Box<dyn ItemViewHandle>>,
+        item: Option<Box<dyn ItemHandle>>,
         cx: &mut ViewContext<Self>,
     ) -> bool {
         self.active_editor_subscription.take();
@@ -197,7 +197,7 @@ impl SearchBar {
     ) -> ElementBox {
         let is_active = self.is_search_option_enabled(search_option);
         MouseEventHandler::new::<Self, _, _>(search_option as usize, cx, |state, cx| {
-            let theme = &cx.app_state::<Settings>().theme.search;
+            let theme = &cx.global::<Settings>().theme.search;
             let style = match (is_active, state.hovered) {
                 (false, false) => &theme.option_button,
                 (false, true) => &theme.hovered_option_button,
@@ -222,7 +222,7 @@ impl SearchBar {
     ) -> ElementBox {
         enum NavButton {}
         MouseEventHandler::new::<NavButton, _, _>(direction as usize, cx, |state, cx| {
-            let theme = &cx.app_state::<Settings>().theme.search;
+            let theme = &cx.global::<Settings>().theme.search;
             let style = if state.hovered {
                 &theme.hovered_option_button
             } else {
@@ -336,11 +336,9 @@ impl SearchBar {
                             direction,
                             &editor.buffer().read(cx).read(cx),
                         );
-                        editor.select_ranges(
-                            [ranges[new_index].clone()],
-                            Some(Autoscroll::Fit),
-                            cx,
-                        );
+                        let range_to_select = ranges[new_index].clone();
+                        editor.unfold_ranges([range_to_select.clone()], false, cx);
+                        editor.select_ranges([range_to_select], Some(Autoscroll::Fit), cx);
                     }
                 });
             }
@@ -360,7 +358,7 @@ impl SearchBar {
         cx: &mut ViewContext<Self>,
     ) {
         match event {
-            editor::Event::Edited => {
+            editor::Event::BufferEdited { .. } => {
                 self.query_contains_error = false;
                 self.clear_matches(cx);
                 self.update_matches(true, cx);
@@ -377,8 +375,8 @@ impl SearchBar {
         cx: &mut ViewContext<Self>,
     ) {
         match event {
-            editor::Event::Edited => self.update_matches(false, cx),
-            editor::Event::SelectionsChanged => self.update_match_index(cx),
+            editor::Event::BufferEdited { .. } => self.update_matches(false, cx),
+            editor::Event::SelectionsChanged { .. } => self.update_match_index(cx),
             _ => {}
         }
     }
@@ -475,7 +473,7 @@ impl SearchBar {
                                         }
                                     }
 
-                                    let theme = &cx.app_state::<Settings>().theme.search;
+                                    let theme = &cx.global::<Settings>().theme.search;
                                     editor.highlight_background::<Self>(
                                         ranges,
                                         theme.match_background,
@@ -510,8 +508,9 @@ impl SearchBar {
 #[cfg(test)]
 mod tests {
     use super::*;
-    use editor::{DisplayPoint, Editor, MultiBuffer};
+    use editor::{DisplayPoint, Editor};
     use gpui::{color::Color, TestAppContext};
+    use language::Buffer;
     use std::sync::Arc;
     use unindent::Unindent as _;
 
@@ -521,11 +520,12 @@ mod tests {
         let mut theme = gpui::fonts::with_font_cache(fonts.clone(), || theme::Theme::default());
         theme.search.match_background = Color::red();
         let settings = Settings::new("Courier", &fonts, Arc::new(theme)).unwrap();
-        cx.update(|cx| cx.add_app_state(settings));
+        cx.update(|cx| cx.set_global(settings));
 
-        let buffer = cx.update(|cx| {
-            MultiBuffer::build_simple(
-                &r#"
+        let buffer = cx.add_model(|cx| {
+            Buffer::new(
+                0,
+                r#"
                 A regular expression (shortened as regex or regexp;[1] also referred to as
                 rational expression[2][3]) is a sequence of characters that specifies a search
                 pattern in text. Usually such patterns are used by string-searching algorithms

crates/search/src/project_search.rs 🔗

@@ -7,7 +7,7 @@ use editor::{Anchor, Autoscroll, Editor, MultiBuffer, SelectAll};
 use gpui::{
     action, elements::*, keymap::Binding, platform::CursorStyle, AppContext, ElementBox, Entity,
     ModelContext, ModelHandle, MutableAppContext, RenderContext, Task, View, ViewContext,
-    ViewHandle, WeakModelHandle,
+    ViewHandle, WeakModelHandle, WeakViewHandle,
 };
 use project::{search::SearchQuery, Project};
 use std::{
@@ -16,7 +16,7 @@ use std::{
     path::PathBuf,
 };
 use util::ResultExt as _;
-use workspace::{Item, ItemHandle, ItemNavHistory, ItemView, Settings, Workspace};
+use workspace::{Item, ItemNavHistory, Settings, Workspace};
 
 action!(Deploy);
 action!(Search);
@@ -26,10 +26,10 @@ action!(ToggleFocus);
 const MAX_TAB_TITLE_LEN: usize = 24;
 
 #[derive(Default)]
-struct ActiveSearches(HashMap<WeakModelHandle<Project>, WeakModelHandle<ProjectSearch>>);
+struct ActiveSearches(HashMap<WeakModelHandle<Project>, WeakViewHandle<ProjectSearchView>>);
 
 pub fn init(cx: &mut MutableAppContext) {
-    cx.add_app_state(ActiveSearches::default());
+    cx.set_global(ActiveSearches::default());
     cx.add_bindings([
         Binding::new("cmd-shift-F", ToggleFocus, Some("ProjectSearchView")),
         Binding::new("cmd-f", ToggleFocus, Some("ProjectSearchView")),
@@ -139,23 +139,6 @@ impl ProjectSearch {
     }
 }
 
-impl Item for ProjectSearch {
-    type View = ProjectSearchView;
-
-    fn build_view(
-        model: ModelHandle<Self>,
-        _: &Workspace,
-        nav_history: ItemNavHistory,
-        cx: &mut gpui::ViewContext<Self::View>,
-    ) -> Self::View {
-        ProjectSearchView::new(model, Some(nav_history), cx)
-    }
-
-    fn project_path(&self) -> Option<project::ProjectPath> {
-        None
-    }
-}
-
 enum ViewEvent {
     UpdateTab,
 }
@@ -172,7 +155,7 @@ impl View for ProjectSearchView {
     fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
         let model = &self.model.read(cx);
         let results = if model.match_ranges.is_empty() {
-            let theme = &cx.app_state::<Settings>().theme;
+            let theme = &cx.global::<Settings>().theme;
             let text = if self.query_editor.read(cx).text(cx).is_empty() {
                 ""
             } else if model.pending_search.is_some() {
@@ -199,11 +182,11 @@ impl View for ProjectSearchView {
     }
 
     fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
-        cx.update_app_state(|state: &mut ActiveSearches, cx| {
-            state.0.insert(
-                self.model.read(cx).project.downgrade(),
-                self.model.downgrade(),
-            )
+        let handle = cx.weak_handle();
+        cx.update_global(|state: &mut ActiveSearches, cx| {
+            state
+                .0
+                .insert(self.model.read(cx).project.downgrade(), handle)
         });
 
         if self.model.read(cx).match_ranges.is_empty() {
@@ -214,7 +197,7 @@ impl View for ProjectSearchView {
     }
 }
 
-impl ItemView for ProjectSearchView {
+impl Item for ProjectSearchView {
     fn act_as_type(
         &self,
         type_id: TypeId,
@@ -235,12 +218,8 @@ impl ItemView for ProjectSearchView {
             .update(cx, |editor, cx| editor.deactivated(cx));
     }
 
-    fn item(&self, _: &gpui::AppContext) -> Box<dyn ItemHandle> {
-        Box::new(self.model.clone())
-    }
-
     fn tab_content(&self, tab_theme: &theme::Tab, cx: &gpui::AppContext) -> ElementBox {
-        let settings = cx.app_state::<Settings>();
+        let settings = cx.global::<Settings>();
         let search_theme = &settings.theme.search;
         Flex::row()
             .with_child(
@@ -271,6 +250,10 @@ impl ItemView for ProjectSearchView {
         None
     }
 
+    fn project_entry_id(&self, _: &AppContext) -> Option<project::ProjectEntryId> {
+        None
+    }
+
     fn can_save(&self, _: &gpui::AppContext) -> bool {
         true
     }
@@ -305,21 +288,23 @@ impl ItemView for ProjectSearchView {
         unreachable!("save_as should not have been called")
     }
 
-    fn clone_on_split(
-        &self,
-        nav_history: ItemNavHistory,
-        cx: &mut ViewContext<Self>,
-    ) -> Option<Self>
+    fn clone_on_split(&self, cx: &mut ViewContext<Self>) -> Option<Self>
     where
         Self: Sized,
     {
         let model = self.model.update(cx, |model, cx| model.clone(cx));
-        Some(Self::new(model, Some(nav_history), cx))
+        Some(Self::new(model, cx))
     }
 
-    fn navigate(&mut self, data: Box<dyn Any>, cx: &mut ViewContext<Self>) {
+    fn set_nav_history(&mut self, nav_history: ItemNavHistory, cx: &mut ViewContext<Self>) {
+        self.results_editor.update(cx, |editor, _| {
+            editor.set_nav_history(Some(nav_history));
+        });
+    }
+
+    fn navigate(&mut self, data: Box<dyn Any>, cx: &mut ViewContext<Self>) -> bool {
         self.results_editor
-            .update(cx, |editor, cx| editor.navigate(data, cx));
+            .update(cx, |editor, cx| editor.navigate(data, cx))
     }
 
     fn should_update_tab_on_event(event: &ViewEvent) -> bool {
@@ -328,11 +313,7 @@ impl ItemView for ProjectSearchView {
 }
 
 impl ProjectSearchView {
-    fn new(
-        model: ModelHandle<ProjectSearch>,
-        nav_history: Option<ItemNavHistory>,
-        cx: &mut ViewContext<Self>,
-    ) -> Self {
+    fn new(model: ModelHandle<ProjectSearch>, cx: &mut ViewContext<Self>) -> Self {
         let project;
         let excerpts;
         let mut query_text = String::new();
@@ -362,15 +343,14 @@ impl ProjectSearchView {
         });
 
         let results_editor = cx.add_view(|cx| {
-            let mut editor = Editor::for_buffer(excerpts, Some(project), cx);
+            let mut editor = Editor::for_multibuffer(excerpts, Some(project), cx);
             editor.set_searchable(false);
-            editor.set_nav_history(nav_history);
             editor
         });
         cx.observe(&results_editor, |_, _, cx| cx.emit(ViewEvent::UpdateTab))
             .detach();
         cx.subscribe(&results_editor, |this, _, event, cx| {
-            if matches!(event, editor::Event::SelectionsChanged) {
+            if matches!(event, editor::Event::SelectionsChanged { .. }) {
                 this.update_match_index(cx);
             }
         })
@@ -394,28 +374,31 @@ impl ProjectSearchView {
     // If no search exists in the workspace, create a new one.
     fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext<Workspace>) {
         // Clean up entries for dropped projects
-        cx.update_app_state(|state: &mut ActiveSearches, cx| {
+        cx.update_global(|state: &mut ActiveSearches, cx| {
             state.0.retain(|project, _| project.is_upgradable(cx))
         });
 
         let active_search = cx
-            .app_state::<ActiveSearches>()
+            .global::<ActiveSearches>()
             .0
             .get(&workspace.project().downgrade());
 
         let existing = active_search
             .and_then(|active_search| {
                 workspace
-                    .items_of_type::<ProjectSearch>(cx)
+                    .items_of_type::<ProjectSearchView>(cx)
                     .find(|search| search == active_search)
             })
-            .or_else(|| workspace.item_of_type::<ProjectSearch>(cx));
+            .or_else(|| workspace.item_of_type::<ProjectSearchView>(cx));
 
         if let Some(existing) = existing {
             workspace.activate_item(&existing, cx);
         } else {
             let model = cx.add_model(|cx| ProjectSearch::new(workspace.project().clone(), cx));
-            workspace.open_item(model, cx);
+            workspace.add_item(
+                Box::new(cx.add_view(|cx| ProjectSearchView::new(model, cx))),
+                cx,
+            );
         }
     }
 
@@ -450,7 +433,10 @@ impl ProjectSearchView {
                     model.search(new_query, cx);
                     model
                 });
-                workspace.open_item(model, cx);
+                workspace.add_item(
+                    Box::new(cx.add_view(|cx| ProjectSearchView::new(model, cx))),
+                    cx,
+                );
             }
         }
     }
@@ -503,6 +489,7 @@ impl ProjectSearchView {
             );
             let range_to_select = model.match_ranges[new_index].clone();
             self.results_editor.update(cx, |editor, cx| {
+                editor.unfold_ranges([range_to_select.clone()], false, cx);
                 editor.select_ranges([range_to_select], Some(Autoscroll::Fit), cx);
             });
         }
@@ -552,7 +539,7 @@ impl ProjectSearchView {
                 if reset_selections {
                     editor.select_ranges(match_ranges.first().cloned(), Some(Autoscroll::Fit), cx);
                 }
-                let theme = &cx.app_state::<Settings>().theme.search;
+                let theme = &cx.global::<Settings>().theme.search;
                 editor.highlight_background::<Self>(match_ranges, theme.match_background, cx);
             });
             if self.query_editor.is_focused(cx) {
@@ -578,7 +565,7 @@ impl ProjectSearchView {
     }
 
     fn render_query_editor(&self, cx: &mut RenderContext<Self>) -> ElementBox {
-        let theme = cx.app_state::<Settings>().theme.clone();
+        let theme = cx.global::<Settings>().theme.clone();
         let editor_container = if self.query_contains_error {
             theme.search.invalid_editor
         } else {
@@ -642,7 +629,7 @@ impl ProjectSearchView {
     ) -> ElementBox {
         let is_active = self.is_option_enabled(option);
         MouseEventHandler::new::<Self, _, _>(option as usize, cx, |state, cx| {
-            let theme = &cx.app_state::<Settings>().theme.search;
+            let theme = &cx.global::<Settings>().theme.search;
             let style = match (is_active, state.hovered) {
                 (false, false) => &theme.option_button,
                 (false, true) => &theme.hovered_option_button,
@@ -675,7 +662,7 @@ impl ProjectSearchView {
     ) -> ElementBox {
         enum NavButton {}
         MouseEventHandler::new::<NavButton, _, _>(direction as usize, cx, |state, cx| {
-            let theme = &cx.app_state::<Settings>().theme.search;
+            let theme = &cx.global::<Settings>().theme.search;
             let style = if state.hovered {
                 &theme.hovered_option_button
             } else {
@@ -707,7 +694,7 @@ mod tests {
         let mut theme = gpui::fonts::with_font_cache(fonts.clone(), || theme::Theme::default());
         theme.search.match_background = Color::red();
         let settings = Settings::new("Courier", &fonts, Arc::new(theme)).unwrap();
-        cx.update(|cx| cx.add_app_state(settings));
+        cx.update(|cx| cx.set_global(settings));
 
         let fs = FakeFs::new(cx.background());
         fs.insert_tree(
@@ -732,7 +719,7 @@ mod tests {
 
         let search = cx.add_model(|cx| ProjectSearch::new(project, cx));
         let search_view = cx.add_view(Default::default(), |cx| {
-            ProjectSearchView::new(search.clone(), None, cx)
+            ProjectSearchView::new(search.clone(), cx)
         });
 
         search_view.update(cx, |search_view, cx| {

crates/search/src/search.rs 🔗

@@ -39,9 +39,9 @@ pub(crate) fn active_match_index(
         None
     } else {
         match ranges.binary_search_by(|probe| {
-            if probe.end.cmp(&cursor, &*buffer).unwrap().is_lt() {
+            if probe.end.cmp(&cursor, &*buffer).is_lt() {
                 Ordering::Less
-            } else if probe.start.cmp(&cursor, &*buffer).unwrap().is_gt() {
+            } else if probe.start.cmp(&cursor, &*buffer).is_gt() {
                 Ordering::Greater
             } else {
                 Ordering::Equal
@@ -59,7 +59,7 @@ pub(crate) fn match_index_for_direction(
     direction: Direction,
     buffer: &MultiBufferSnapshot,
 ) -> usize {
-    if ranges[index].start.cmp(&cursor, &buffer).unwrap().is_gt() {
+    if ranges[index].start.cmp(&cursor, &buffer).is_gt() {
         if direction == Direction::Prev {
             if index == 0 {
                 index = ranges.len() - 1;
@@ -67,7 +67,7 @@ pub(crate) fn match_index_for_direction(
                 index -= 1;
             }
         }
-    } else if ranges[index].end.cmp(&cursor, &buffer).unwrap().is_lt() {
+    } else if ranges[index].end.cmp(&cursor, &buffer).is_lt() {
         if direction == Direction::Next {
             index = 0;
         }

crates/server/src/rpc.rs 🔗

@@ -92,7 +92,8 @@ impl Server {
             .add_request_handler(Server::forward_project_request::<proto::GetDocumentHighlights>)
             .add_request_handler(Server::forward_project_request::<proto::GetProjectSymbols>)
             .add_request_handler(Server::forward_project_request::<proto::OpenBufferForSymbol>)
-            .add_request_handler(Server::forward_project_request::<proto::OpenBuffer>)
+            .add_request_handler(Server::forward_project_request::<proto::OpenBufferById>)
+            .add_request_handler(Server::forward_project_request::<proto::OpenBufferByPath>)
             .add_request_handler(Server::forward_project_request::<proto::GetCompletions>)
             .add_request_handler(
                 Server::forward_project_request::<proto::ApplyCompletionAdditionalEdits>,
@@ -112,6 +113,9 @@ impl Server {
             .add_request_handler(Server::join_channel)
             .add_message_handler(Server::leave_channel)
             .add_request_handler(Server::send_channel_message)
+            .add_request_handler(Server::follow)
+            .add_message_handler(Server::unfollow)
+            .add_message_handler(Server::update_followers)
             .add_request_handler(Server::get_channel_messages);
 
         Arc::new(server)
@@ -669,6 +673,72 @@ impl Server {
         Ok(())
     }
 
+    async fn follow(
+        self: Arc<Self>,
+        request: TypedEnvelope<proto::Follow>,
+    ) -> tide::Result<proto::FollowResponse> {
+        let leader_id = ConnectionId(request.payload.leader_id);
+        let follower_id = request.sender_id;
+        if !self
+            .state()
+            .project_connection_ids(request.payload.project_id, follower_id)?
+            .contains(&leader_id)
+        {
+            Err(anyhow!("no such peer"))?;
+        }
+        let mut response = self
+            .peer
+            .forward_request(request.sender_id, leader_id, request.payload)
+            .await?;
+        response
+            .views
+            .retain(|view| view.leader_id != Some(follower_id.0));
+        Ok(response)
+    }
+
+    async fn unfollow(
+        self: Arc<Self>,
+        request: TypedEnvelope<proto::Unfollow>,
+    ) -> tide::Result<()> {
+        let leader_id = ConnectionId(request.payload.leader_id);
+        if !self
+            .state()
+            .project_connection_ids(request.payload.project_id, request.sender_id)?
+            .contains(&leader_id)
+        {
+            Err(anyhow!("no such peer"))?;
+        }
+        self.peer
+            .forward_send(request.sender_id, leader_id, request.payload)?;
+        Ok(())
+    }
+
+    async fn update_followers(
+        self: Arc<Self>,
+        request: TypedEnvelope<proto::UpdateFollowers>,
+    ) -> tide::Result<()> {
+        let connection_ids = self
+            .state()
+            .project_connection_ids(request.payload.project_id, request.sender_id)?;
+        let leader_id = request
+            .payload
+            .variant
+            .as_ref()
+            .and_then(|variant| match variant {
+                proto::update_followers::Variant::CreateView(payload) => payload.leader_id,
+                proto::update_followers::Variant::UpdateView(payload) => payload.leader_id,
+                proto::update_followers::Variant::UpdateActiveView(payload) => payload.leader_id,
+            });
+        for follower_id in &request.payload.follower_ids {
+            let follower_id = ConnectionId(*follower_id);
+            if connection_ids.contains(&follower_id) && Some(follower_id.0) != leader_id {
+                self.peer
+                    .forward_send(request.sender_id, follower_id, request.payload.clone())?;
+            }
+        }
+        Ok(())
+    }
+
     async fn get_channels(
         self: Arc<Server>,
         request: TypedEnvelope<proto::GetChannels>,
@@ -1013,10 +1083,10 @@ mod tests {
     };
     use collections::BTreeMap;
     use editor::{
-        self, ConfirmCodeAction, ConfirmCompletion, ConfirmRename, Editor, Input, MultiBuffer,
-        Redo, Rename, ToOffset, ToggleCodeActions, Undo,
+        self, ConfirmCodeAction, ConfirmCompletion, ConfirmRename, Editor, Input, Redo, Rename,
+        ToOffset, ToggleCodeActions, Undo,
     };
-    use gpui::{executor, ModelHandle, TestAppContext};
+    use gpui::{executor, geometry::vector::vec2f, ModelHandle, TestAppContext, ViewHandle};
     use language::{
         tree_sitter_rust, Diagnostic, DiagnosticEntry, Language, LanguageConfig, LanguageRegistry,
         LanguageServerConfig, OffsetRangeExt, Point, ToLspPosition,
@@ -1028,7 +1098,7 @@ mod tests {
         fs::{FakeFs, Fs as _},
         search::SearchQuery,
         worktree::WorktreeHandle,
-        DiagnosticSummary, Project, ProjectPath,
+        DiagnosticSummary, Project, ProjectPath, WorktreeId,
     };
     use rand::prelude::*;
     use rpc::PeerId;
@@ -1046,7 +1116,7 @@ mod tests {
         },
         time::Duration,
     };
-    use workspace::{Settings, Workspace, WorkspaceParams};
+    use workspace::{Item, Settings, SplitDirection, Workspace, WorkspaceParams};
 
     #[cfg(test)]
     #[ctor::ctor]
@@ -1140,10 +1210,7 @@ mod tests {
             .update(cx_b, |p, cx| p.open_buffer((worktree_id, "b.txt"), cx))
             .await
             .unwrap();
-        let buffer_b = cx_b.add_model(|cx| MultiBuffer::singleton(buffer_b, cx));
-        buffer_b.read_with(cx_b, |buf, cx| {
-            assert_eq!(buf.read(cx).text(), "b-contents")
-        });
+        buffer_b.read_with(cx_b, |buf, _| assert_eq!(buf.text(), "b-contents"));
         project_a.read_with(cx_a, |project, cx| {
             assert!(project.has_open_buffer((worktree_id, "b.txt"), cx))
         });
@@ -1243,10 +1310,105 @@ mod tests {
             .unwrap();
 
         // Unshare the project as client A
+        project_a.update(cx_a, |project, cx| project.unshare(cx));
+        project_b
+            .condition(cx_b, |project, _| project.is_read_only())
+            .await;
+        assert!(worktree_a.read_with(cx_a, |tree, _| !tree.as_local().unwrap().is_shared()));
+        cx_b.update(|_| {
+            drop(project_b);
+        });
+
+        // Share the project again and ensure guests can still join.
         project_a
-            .update(cx_a, |project, cx| project.unshare(cx))
+            .update(cx_a, |project, cx| project.share(cx))
             .await
             .unwrap();
+        assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared()));
+
+        let project_b2 = Project::remote(
+            project_id,
+            client_b.clone(),
+            client_b.user_store.clone(),
+            lang_registry.clone(),
+            fs.clone(),
+            &mut cx_b.to_async(),
+        )
+        .await
+        .unwrap();
+        project_b2
+            .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
+            .await
+            .unwrap();
+    }
+
+    #[gpui::test(iterations = 10)]
+    async fn test_host_disconnect(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
+        let lang_registry = Arc::new(LanguageRegistry::test());
+        let fs = FakeFs::new(cx_a.background());
+        cx_a.foreground().forbid_parking();
+
+        // Connect to a server as 2 clients.
+        let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
+        let client_a = server.create_client(cx_a, "user_a").await;
+        let client_b = server.create_client(cx_b, "user_b").await;
+
+        // Share a project as client A
+        fs.insert_tree(
+            "/a",
+            json!({
+                ".zed.toml": r#"collaborators = ["user_b"]"#,
+                "a.txt": "a-contents",
+                "b.txt": "b-contents",
+            }),
+        )
+        .await;
+        let project_a = cx_a.update(|cx| {
+            Project::local(
+                client_a.clone(),
+                client_a.user_store.clone(),
+                lang_registry.clone(),
+                fs.clone(),
+                cx,
+            )
+        });
+        let (worktree_a, _) = project_a
+            .update(cx_a, |p, cx| {
+                p.find_or_create_local_worktree("/a", true, cx)
+            })
+            .await
+            .unwrap();
+        worktree_a
+            .read_with(cx_a, |tree, _| tree.as_local().unwrap().scan_complete())
+            .await;
+        let project_id = project_a.update(cx_a, |p, _| p.next_remote_id()).await;
+        let worktree_id = worktree_a.read_with(cx_a, |tree, _| tree.id());
+        project_a.update(cx_a, |p, cx| p.share(cx)).await.unwrap();
+        assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared()));
+
+        // Join that project as client B
+        let project_b = Project::remote(
+            project_id,
+            client_b.clone(),
+            client_b.user_store.clone(),
+            lang_registry.clone(),
+            fs.clone(),
+            &mut cx_b.to_async(),
+        )
+        .await
+        .unwrap();
+        project_b
+            .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
+            .await
+            .unwrap();
+
+        // Drop client A's connection. Collaborators should disappear and the project should not be shown as shared.
+        server.disconnect_client(client_a.current_user_id(cx_a));
+        cx_a.foreground().advance_clock(rpc::RECEIVE_TIMEOUT);
+        project_a
+            .condition(cx_a, |project, _| project.collaborators().is_empty())
+            .await;
+        project_a.read_with(cx_a, |project, _| assert!(!project.is_shared()));
         project_b
             .condition(cx_b, |project, _| project.is_read_only())
             .await;
@@ -1255,6 +1417,9 @@ mod tests {
             drop(project_b);
         });
 
+        // Await reconnection
+        let project_id = project_a.update(cx_a, |p, _| p.next_remote_id()).await;
+
         // Share the project again and ensure guests can still join.
         project_a
             .update(cx_a, |project, cx| project.share(cx))
@@ -2176,11 +2341,7 @@ mod tests {
             .unwrap();
         let (window_b, _) = cx_b.add_window(|_| EmptyView);
         let editor_b = cx_b.add_view(window_b, |cx| {
-            Editor::for_buffer(
-                cx.add_model(|cx| MultiBuffer::singleton(buffer_b.clone(), cx)),
-                Some(project_b.clone()),
-                cx,
-            )
+            Editor::for_buffer(buffer_b.clone(), Some(project_b.clone()), cx)
         });
 
         let mut fake_language_server = fake_language_servers.next().await.unwrap();
@@ -2199,7 +2360,7 @@ mod tests {
         // Return some completions from the host's language server.
         cx_a.foreground().start_waiting();
         fake_language_server
-            .handle_request::<lsp::request::Completion, _>(|params, _| {
+            .handle_request::<lsp::request::Completion, _, _>(|params, _| async move {
                 assert_eq!(
                     params.text_document_position.text_document.uri,
                     lsp::Url::from_file_path("/a/main.rs").unwrap(),
@@ -2263,8 +2424,8 @@ mod tests {
 
         // Return a resolved completion from the host's language server.
         // The resolved completion has an additional text edit.
-        fake_language_server.handle_request::<lsp::request::ResolveCompletionItem, _>(
-            |params, _| {
+        fake_language_server.handle_request::<lsp::request::ResolveCompletionItem, _, _>(
+            |params, _| async move {
                 assert_eq!(params.label, "first_method(…)");
                 lsp::CompletionItem {
                     label: "first_method(…)".into(),
@@ -2374,7 +2535,7 @@ mod tests {
             .unwrap();
 
         let mut fake_language_server = fake_language_servers.next().await.unwrap();
-        fake_language_server.handle_request::<lsp::request::Formatting, _>(|_, _| {
+        fake_language_server.handle_request::<lsp::request::Formatting, _, _>(|_, _| async move {
             Some(vec![
                 lsp::TextEdit {
                     range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 4)),
@@ -2483,12 +2644,14 @@ mod tests {
 
         // Request the definition of a symbol as the guest.
         let mut fake_language_server = fake_language_servers.next().await.unwrap();
-        fake_language_server.handle_request::<lsp::request::GotoDefinition, _>(|_, _| {
-            Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location::new(
-                lsp::Url::from_file_path("/root-2/b.rs").unwrap(),
-                lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
-            )))
-        });
+        fake_language_server.handle_request::<lsp::request::GotoDefinition, _, _>(
+            |_, _| async move {
+                Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location::new(
+                    lsp::Url::from_file_path("/root-2/b.rs").unwrap(),
+                    lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
+                )))
+            },
+        );
 
         let definitions_1 = project_b
             .update(cx_b, |p, cx| p.definition(&buffer_b, 23, cx))
@@ -2510,12 +2673,14 @@ mod tests {
 
         // Try getting more definitions for the same buffer, ensuring the buffer gets reused from
         // the previous call to `definition`.
-        fake_language_server.handle_request::<lsp::request::GotoDefinition, _>(|_, _| {
-            Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location::new(
-                lsp::Url::from_file_path("/root-2/b.rs").unwrap(),
-                lsp::Range::new(lsp::Position::new(1, 6), lsp::Position::new(1, 11)),
-            )))
-        });
+        fake_language_server.handle_request::<lsp::request::GotoDefinition, _, _>(
+            |_, _| async move {
+                Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location::new(
+                    lsp::Url::from_file_path("/root-2/b.rs").unwrap(),
+                    lsp::Range::new(lsp::Position::new(1, 6), lsp::Position::new(1, 11)),
+                )))
+            },
+        );
 
         let definitions_2 = project_b
             .update(cx_b, |p, cx| p.definition(&buffer_b, 33, cx))
@@ -2622,26 +2787,37 @@ mod tests {
 
         // Request references to a symbol as the guest.
         let mut fake_language_server = fake_language_servers.next().await.unwrap();
-        fake_language_server.handle_request::<lsp::request::References, _>(|params, _| {
-            assert_eq!(
-                params.text_document_position.text_document.uri.as_str(),
-                "file:///root-1/one.rs"
-            );
-            Some(vec![
-                lsp::Location {
-                    uri: lsp::Url::from_file_path("/root-1/two.rs").unwrap(),
-                    range: lsp::Range::new(lsp::Position::new(0, 24), lsp::Position::new(0, 27)),
-                },
-                lsp::Location {
-                    uri: lsp::Url::from_file_path("/root-1/two.rs").unwrap(),
-                    range: lsp::Range::new(lsp::Position::new(0, 35), lsp::Position::new(0, 38)),
-                },
-                lsp::Location {
-                    uri: lsp::Url::from_file_path("/root-2/three.rs").unwrap(),
-                    range: lsp::Range::new(lsp::Position::new(0, 37), lsp::Position::new(0, 40)),
-                },
-            ])
-        });
+        fake_language_server.handle_request::<lsp::request::References, _, _>(
+            |params, _| async move {
+                assert_eq!(
+                    params.text_document_position.text_document.uri.as_str(),
+                    "file:///root-1/one.rs"
+                );
+                Some(vec![
+                    lsp::Location {
+                        uri: lsp::Url::from_file_path("/root-1/two.rs").unwrap(),
+                        range: lsp::Range::new(
+                            lsp::Position::new(0, 24),
+                            lsp::Position::new(0, 27),
+                        ),
+                    },
+                    lsp::Location {
+                        uri: lsp::Url::from_file_path("/root-1/two.rs").unwrap(),
+                        range: lsp::Range::new(
+                            lsp::Position::new(0, 35),
+                            lsp::Position::new(0, 38),
+                        ),
+                    },
+                    lsp::Location {
+                        uri: lsp::Url::from_file_path("/root-2/three.rs").unwrap(),
+                        range: lsp::Range::new(
+                            lsp::Position::new(0, 37),
+                            lsp::Position::new(0, 40),
+                        ),
+                    },
+                ])
+            },
+        );
 
         let references = project_b
             .update(cx_b, |p, cx| p.references(&buffer_b, 7, cx))
@@ -2851,8 +3027,8 @@ mod tests {
 
         // Request document highlights as the guest.
         let mut fake_language_server = fake_language_servers.next().await.unwrap();
-        fake_language_server.handle_request::<lsp::request::DocumentHighlightRequest, _>(
-            |params, _| {
+        fake_language_server.handle_request::<lsp::request::DocumentHighlightRequest, _, _>(
+            |params, _| async move {
                 assert_eq!(
                     params
                         .text_document_position_params
@@ -2997,20 +3173,22 @@ mod tests {
             .unwrap();
 
         let mut fake_language_server = fake_language_servers.next().await.unwrap();
-        fake_language_server.handle_request::<lsp::request::WorkspaceSymbol, _>(|_, _| {
-            #[allow(deprecated)]
-            Some(vec![lsp::SymbolInformation {
-                name: "TWO".into(),
-                location: lsp::Location {
-                    uri: lsp::Url::from_file_path("/code/crate-2/two.rs").unwrap(),
-                    range: lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
-                },
-                kind: lsp::SymbolKind::CONSTANT,
-                tags: None,
-                container_name: None,
-                deprecated: None,
-            }])
-        });
+        fake_language_server.handle_request::<lsp::request::WorkspaceSymbol, _, _>(
+            |_, _| async move {
+                #[allow(deprecated)]
+                Some(vec![lsp::SymbolInformation {
+                    name: "TWO".into(),
+                    location: lsp::Location {
+                        uri: lsp::Url::from_file_path("/code/crate-2/two.rs").unwrap(),
+                        range: lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
+                    },
+                    kind: lsp::SymbolKind::CONSTANT,
+                    tags: None,
+                    container_name: None,
+                    deprecated: None,
+                }])
+            },
+        );
 
         // Request the definition of a symbol as the guest.
         let symbols = project_b
@@ -3128,12 +3306,14 @@ mod tests {
             .unwrap();
 
         let mut fake_language_server = fake_language_servers.next().await.unwrap();
-        fake_language_server.handle_request::<lsp::request::GotoDefinition, _>(|_, _| {
-            Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location::new(
-                lsp::Url::from_file_path("/root/b.rs").unwrap(),
-                lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
-            )))
-        });
+        fake_language_server.handle_request::<lsp::request::GotoDefinition, _, _>(
+            |_, _| async move {
+                Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location::new(
+                    lsp::Url::from_file_path("/root/b.rs").unwrap(),
+                    lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
+                )))
+            },
+        );
 
         let definitions;
         let buffer_b2;
@@ -3159,8 +3339,7 @@ mod tests {
         cx_a.foreground().forbid_parking();
         let mut lang_registry = Arc::new(LanguageRegistry::test());
         let fs = FakeFs::new(cx_a.background());
-        let mut path_openers_b = Vec::new();
-        cx_b.update(|cx| editor::init(cx, &mut path_openers_b));
+        cx_b.update(|cx| editor::init(cx));
 
         // Set up a fake language server.
         let (language_server_config, mut fake_language_servers) = LanguageServerConfig::fake();
@@ -3229,12 +3408,11 @@ mod tests {
         params.client = client_b.client.clone();
         params.user_store = client_b.user_store.clone();
         params.project = project_b;
-        params.path_openers = path_openers_b.into();
 
         let (_window_b, workspace_b) = cx_b.add_window(|cx| Workspace::new(&params, cx));
         let editor_b = workspace_b
             .update(cx_b, |workspace, cx| {
-                workspace.open_path((worktree_id, "main.rs").into(), cx)
+                workspace.open_path((worktree_id, "main.rs"), cx)
             })
             .await
             .unwrap()
@@ -3243,7 +3421,7 @@ mod tests {
 
         let mut fake_language_server = fake_language_servers.next().await.unwrap();
         fake_language_server
-            .handle_request::<lsp::request::CodeActionRequest, _>(|params, _| {
+            .handle_request::<lsp::request::CodeActionRequest, _, _>(|params, _| async move {
                 assert_eq!(
                     params.text_document.uri,
                     lsp::Url::from_file_path("/a/main.rs").unwrap(),
@@ -3262,7 +3440,7 @@ mod tests {
         });
 
         fake_language_server
-            .handle_request::<lsp::request::CodeActionRequest, _>(|params, _| {
+            .handle_request::<lsp::request::CodeActionRequest, _, _>(|params, _| async move {
                 assert_eq!(
                     params.text_document.uri,
                     lsp::Url::from_file_path("/a/main.rs").unwrap(),
@@ -3333,41 +3511,43 @@ mod tests {
                 Editor::confirm_code_action(workspace, &ConfirmCodeAction(Some(0)), cx)
             })
             .unwrap();
-        fake_language_server.handle_request::<lsp::request::CodeActionResolveRequest, _>(|_, _| {
-            lsp::CodeAction {
-                title: "Inline into all callers".to_string(),
-                edit: Some(lsp::WorkspaceEdit {
-                    changes: Some(
-                        [
-                            (
-                                lsp::Url::from_file_path("/a/main.rs").unwrap(),
-                                vec![lsp::TextEdit::new(
-                                    lsp::Range::new(
-                                        lsp::Position::new(1, 22),
-                                        lsp::Position::new(1, 34),
-                                    ),
-                                    "4".to_string(),
-                                )],
-                            ),
-                            (
-                                lsp::Url::from_file_path("/a/other.rs").unwrap(),
-                                vec![lsp::TextEdit::new(
-                                    lsp::Range::new(
-                                        lsp::Position::new(0, 0),
-                                        lsp::Position::new(0, 27),
-                                    ),
-                                    "".to_string(),
-                                )],
-                            ),
-                        ]
-                        .into_iter()
-                        .collect(),
-                    ),
+        fake_language_server.handle_request::<lsp::request::CodeActionResolveRequest, _, _>(
+            |_, _| async move {
+                lsp::CodeAction {
+                    title: "Inline into all callers".to_string(),
+                    edit: Some(lsp::WorkspaceEdit {
+                        changes: Some(
+                            [
+                                (
+                                    lsp::Url::from_file_path("/a/main.rs").unwrap(),
+                                    vec![lsp::TextEdit::new(
+                                        lsp::Range::new(
+                                            lsp::Position::new(1, 22),
+                                            lsp::Position::new(1, 34),
+                                        ),
+                                        "4".to_string(),
+                                    )],
+                                ),
+                                (
+                                    lsp::Url::from_file_path("/a/other.rs").unwrap(),
+                                    vec![lsp::TextEdit::new(
+                                        lsp::Range::new(
+                                            lsp::Position::new(0, 0),
+                                            lsp::Position::new(0, 27),
+                                        ),
+                                        "".to_string(),
+                                    )],
+                                ),
+                            ]
+                            .into_iter()
+                            .collect(),
+                        ),
+                        ..Default::default()
+                    }),
                     ..Default::default()
-                }),
-                ..Default::default()
-            }
-        });
+                }
+            },
+        );
 
         // After the action is confirmed, an editor containing both modified files is opened.
         confirm_action.await.unwrap();
@@ -3395,8 +3575,7 @@ mod tests {
         cx_a.foreground().forbid_parking();
         let mut lang_registry = Arc::new(LanguageRegistry::test());
         let fs = FakeFs::new(cx_a.background());
-        let mut path_openers_b = Vec::new();
-        cx_b.update(|cx| editor::init(cx, &mut path_openers_b));
+        cx_b.update(|cx| editor::init(cx));
 
         // Set up a fake language server.
         let (language_server_config, mut fake_language_servers) = LanguageServerConfig::fake();
@@ -3465,12 +3644,11 @@ mod tests {
         params.client = client_b.client.clone();
         params.user_store = client_b.user_store.clone();
         params.project = project_b;
-        params.path_openers = path_openers_b.into();
 
         let (_window_b, workspace_b) = cx_b.add_window(|cx| Workspace::new(&params, cx));
         let editor_b = workspace_b
             .update(cx_b, |workspace, cx| {
-                workspace.open_path((worktree_id, "one.rs").into(), cx)
+                workspace.open_path((worktree_id, "one.rs"), cx)
             })
             .await
             .unwrap()
@@ -3485,7 +3663,7 @@ mod tests {
         });
 
         fake_language_server
-            .handle_request::<lsp::request::PrepareRenameRequest, _>(|params, _| {
+            .handle_request::<lsp::request::PrepareRenameRequest, _, _>(|params, _| async move {
                 assert_eq!(params.text_document.uri.as_str(), "file:///dir/one.rs");
                 assert_eq!(params.position, lsp::Position::new(0, 7));
                 Some(lsp::PrepareRenameResponse::Range(lsp::Range::new(
@@ -3515,7 +3693,7 @@ mod tests {
             Editor::confirm_rename(workspace, &ConfirmRename, cx).unwrap()
         });
         fake_language_server
-            .handle_request::<lsp::request::Rename, _>(|params, _| {
+            .handle_request::<lsp::request::Rename, _, _>(|params, _| async move {
                 assert_eq!(
                     params.text_document_position.text_document.uri.as_str(),
                     "file:///dir/one.rs"
@@ -4159,6 +4337,494 @@ mod tests {
         }
     }
 
+    #[gpui::test(iterations = 10)]
+    async fn test_following(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
+        cx_a.foreground().forbid_parking();
+        let fs = FakeFs::new(cx_a.background());
+
+        // 2 clients connect to a server.
+        let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
+        let mut client_a = server.create_client(cx_a, "user_a").await;
+        let mut client_b = server.create_client(cx_b, "user_b").await;
+        cx_a.update(editor::init);
+        cx_b.update(editor::init);
+
+        // Client A shares a project.
+        fs.insert_tree(
+            "/a",
+            json!({
+                ".zed.toml": r#"collaborators = ["user_b"]"#,
+                "1.txt": "one",
+                "2.txt": "two",
+                "3.txt": "three",
+            }),
+        )
+        .await;
+        let (project_a, worktree_id) = client_a.build_local_project(fs.clone(), "/a", cx_a).await;
+        project_a
+            .update(cx_a, |project, cx| project.share(cx))
+            .await
+            .unwrap();
+
+        // Client B joins the project.
+        let project_b = client_b
+            .build_remote_project(
+                project_a
+                    .read_with(cx_a, |project, _| project.remote_id())
+                    .unwrap(),
+                cx_b,
+            )
+            .await;
+
+        // Client A opens some editors.
+        let workspace_a = client_a.build_workspace(&project_a, cx_a);
+        let pane_a = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone());
+        let editor_a1 = workspace_a
+            .update(cx_a, |workspace, cx| {
+                workspace.open_path((worktree_id, "1.txt"), cx)
+            })
+            .await
+            .unwrap()
+            .downcast::<Editor>()
+            .unwrap();
+        let editor_a2 = workspace_a
+            .update(cx_a, |workspace, cx| {
+                workspace.open_path((worktree_id, "2.txt"), cx)
+            })
+            .await
+            .unwrap()
+            .downcast::<Editor>()
+            .unwrap();
+
+        // Client B opens an editor.
+        let workspace_b = client_b.build_workspace(&project_b, cx_b);
+        let editor_b1 = workspace_b
+            .update(cx_b, |workspace, cx| {
+                workspace.open_path((worktree_id, "1.txt"), cx)
+            })
+            .await
+            .unwrap()
+            .downcast::<Editor>()
+            .unwrap();
+
+        let client_a_id = project_b.read_with(cx_b, |project, _| {
+            project.collaborators().values().next().unwrap().peer_id
+        });
+        let client_b_id = project_a.read_with(cx_a, |project, _| {
+            project.collaborators().values().next().unwrap().peer_id
+        });
+
+        // When client B starts following client A, all visible view states are replicated to client B.
+        editor_a1.update(cx_a, |editor, cx| editor.select_ranges([0..1], None, cx));
+        editor_a2.update(cx_a, |editor, cx| editor.select_ranges([2..3], None, cx));
+        workspace_b
+            .update(cx_b, |workspace, cx| {
+                workspace.toggle_follow(&client_a_id.into(), cx).unwrap()
+            })
+            .await
+            .unwrap();
+        let editor_b2 = workspace_b.read_with(cx_b, |workspace, cx| {
+            workspace
+                .active_item(cx)
+                .unwrap()
+                .downcast::<Editor>()
+                .unwrap()
+        });
+        assert!(cx_b.read(|cx| editor_b2.is_focused(cx)));
+        assert_eq!(
+            editor_b2.read_with(cx_b, |editor, cx| editor.project_path(cx)),
+            Some((worktree_id, "2.txt").into())
+        );
+        assert_eq!(
+            editor_b2.read_with(cx_b, |editor, cx| editor.selected_ranges(cx)),
+            vec![2..3]
+        );
+        assert_eq!(
+            editor_b1.read_with(cx_b, |editor, cx| editor.selected_ranges(cx)),
+            vec![0..1]
+        );
+
+        // When client A activates a different editor, client B does so as well.
+        workspace_a.update(cx_a, |workspace, cx| {
+            workspace.activate_item(&editor_a1, cx)
+        });
+        workspace_b
+            .condition(cx_b, |workspace, cx| {
+                workspace.active_item(cx).unwrap().id() == editor_b1.id()
+            })
+            .await;
+
+        // Changes to client A's editor are reflected on client B.
+        editor_a1.update(cx_a, |editor, cx| {
+            editor.select_ranges([1..1, 2..2], None, cx);
+        });
+        editor_b1
+            .condition(cx_b, |editor, cx| {
+                editor.selected_ranges(cx) == vec![1..1, 2..2]
+            })
+            .await;
+
+        editor_a1.update(cx_a, |editor, cx| editor.set_text("TWO", cx));
+        editor_b1
+            .condition(cx_b, |editor, cx| editor.text(cx) == "TWO")
+            .await;
+
+        editor_a1.update(cx_a, |editor, cx| {
+            editor.select_ranges([3..3], None, cx);
+            editor.set_scroll_position(vec2f(0., 100.), cx);
+        });
+        editor_b1
+            .condition(cx_b, |editor, cx| editor.selected_ranges(cx) == vec![3..3])
+            .await;
+
+        // After unfollowing, client B stops receiving updates from client A.
+        workspace_b.update(cx_b, |workspace, cx| {
+            workspace.unfollow(&workspace.active_pane().clone(), cx)
+        });
+        workspace_a.update(cx_a, |workspace, cx| {
+            workspace.activate_item(&editor_a2, cx)
+        });
+        cx_a.foreground().run_until_parked();
+        assert_eq!(
+            workspace_b.read_with(cx_b, |workspace, cx| workspace
+                .active_item(cx)
+                .unwrap()
+                .id()),
+            editor_b1.id()
+        );
+
+        // Client A starts following client B.
+        workspace_a
+            .update(cx_a, |workspace, cx| {
+                workspace.toggle_follow(&client_b_id.into(), cx).unwrap()
+            })
+            .await
+            .unwrap();
+        assert_eq!(
+            workspace_a.read_with(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),
+            Some(client_b_id)
+        );
+        assert_eq!(
+            workspace_a.read_with(cx_a, |workspace, cx| workspace
+                .active_item(cx)
+                .unwrap()
+                .id()),
+            editor_a1.id()
+        );
+
+        // Following interrupts when client B disconnects.
+        client_b.disconnect(&cx_b.to_async()).unwrap();
+        cx_a.foreground().run_until_parked();
+        assert_eq!(
+            workspace_a.read_with(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),
+            None
+        );
+    }
+
+    #[gpui::test(iterations = 10)]
+    async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
+        cx_a.foreground().forbid_parking();
+        let fs = FakeFs::new(cx_a.background());
+
+        // 2 clients connect to a server.
+        let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
+        let mut client_a = server.create_client(cx_a, "user_a").await;
+        let mut client_b = server.create_client(cx_b, "user_b").await;
+        cx_a.update(editor::init);
+        cx_b.update(editor::init);
+
+        // Client A shares a project.
+        fs.insert_tree(
+            "/a",
+            json!({
+                ".zed.toml": r#"collaborators = ["user_b"]"#,
+                "1.txt": "one",
+                "2.txt": "two",
+                "3.txt": "three",
+                "4.txt": "four",
+            }),
+        )
+        .await;
+        let (project_a, worktree_id) = client_a.build_local_project(fs.clone(), "/a", cx_a).await;
+        project_a
+            .update(cx_a, |project, cx| project.share(cx))
+            .await
+            .unwrap();
+
+        // Client B joins the project.
+        let project_b = client_b
+            .build_remote_project(
+                project_a
+                    .read_with(cx_a, |project, _| project.remote_id())
+                    .unwrap(),
+                cx_b,
+            )
+            .await;
+
+        // Client A opens some editors.
+        let workspace_a = client_a.build_workspace(&project_a, cx_a);
+        let pane_a1 = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone());
+        let _editor_a1 = workspace_a
+            .update(cx_a, |workspace, cx| {
+                workspace.open_path((worktree_id, "1.txt"), cx)
+            })
+            .await
+            .unwrap()
+            .downcast::<Editor>()
+            .unwrap();
+
+        // Client B opens an editor.
+        let workspace_b = client_b.build_workspace(&project_b, cx_b);
+        let pane_b1 = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone());
+        let _editor_b1 = workspace_b
+            .update(cx_b, |workspace, cx| {
+                workspace.open_path((worktree_id, "2.txt"), cx)
+            })
+            .await
+            .unwrap()
+            .downcast::<Editor>()
+            .unwrap();
+
+        // Clients A and B follow each other in split panes
+        workspace_a
+            .update(cx_a, |workspace, cx| {
+                workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx);
+                assert_ne!(*workspace.active_pane(), pane_a1);
+                let leader_id = *project_a.read(cx).collaborators().keys().next().unwrap();
+                workspace
+                    .toggle_follow(&workspace::ToggleFollow(leader_id), cx)
+                    .unwrap()
+            })
+            .await
+            .unwrap();
+        workspace_b
+            .update(cx_b, |workspace, cx| {
+                workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx);
+                assert_ne!(*workspace.active_pane(), pane_b1);
+                let leader_id = *project_b.read(cx).collaborators().keys().next().unwrap();
+                workspace
+                    .toggle_follow(&workspace::ToggleFollow(leader_id), cx)
+                    .unwrap()
+            })
+            .await
+            .unwrap();
+
+        workspace_a
+            .update(cx_a, |workspace, cx| {
+                workspace.activate_next_pane(cx);
+                assert_eq!(*workspace.active_pane(), pane_a1);
+                workspace.open_path((worktree_id, "3.txt"), cx)
+            })
+            .await
+            .unwrap();
+        workspace_b
+            .update(cx_b, |workspace, cx| {
+                workspace.activate_next_pane(cx);
+                assert_eq!(*workspace.active_pane(), pane_b1);
+                workspace.open_path((worktree_id, "4.txt"), cx)
+            })
+            .await
+            .unwrap();
+        cx_a.foreground().run_until_parked();
+
+        // Ensure leader updates don't change the active pane of followers
+        workspace_a.read_with(cx_a, |workspace, _| {
+            assert_eq!(*workspace.active_pane(), pane_a1);
+        });
+        workspace_b.read_with(cx_b, |workspace, _| {
+            assert_eq!(*workspace.active_pane(), pane_b1);
+        });
+
+        // Ensure peers following each other doesn't cause an infinite loop.
+        assert_eq!(
+            workspace_a.read_with(cx_a, |workspace, cx| workspace
+                .active_item(cx)
+                .unwrap()
+                .project_path(cx)),
+            Some((worktree_id, "3.txt").into())
+        );
+        workspace_a.update(cx_a, |workspace, cx| {
+            assert_eq!(
+                workspace.active_item(cx).unwrap().project_path(cx),
+                Some((worktree_id, "3.txt").into())
+            );
+            workspace.activate_next_pane(cx);
+            assert_eq!(
+                workspace.active_item(cx).unwrap().project_path(cx),
+                Some((worktree_id, "4.txt").into())
+            );
+        });
+        workspace_b.update(cx_b, |workspace, cx| {
+            assert_eq!(
+                workspace.active_item(cx).unwrap().project_path(cx),
+                Some((worktree_id, "4.txt").into())
+            );
+            workspace.activate_next_pane(cx);
+            assert_eq!(
+                workspace.active_item(cx).unwrap().project_path(cx),
+                Some((worktree_id, "3.txt").into())
+            );
+        });
+    }
+
+    #[gpui::test(iterations = 10)]
+    async fn test_auto_unfollowing(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
+        cx_a.foreground().forbid_parking();
+        let fs = FakeFs::new(cx_a.background());
+
+        // 2 clients connect to a server.
+        let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
+        let mut client_a = server.create_client(cx_a, "user_a").await;
+        let mut client_b = server.create_client(cx_b, "user_b").await;
+        cx_a.update(editor::init);
+        cx_b.update(editor::init);
+
+        // Client A shares a project.
+        fs.insert_tree(
+            "/a",
+            json!({
+                ".zed.toml": r#"collaborators = ["user_b"]"#,
+                "1.txt": "one",
+                "2.txt": "two",
+                "3.txt": "three",
+            }),
+        )
+        .await;
+        let (project_a, worktree_id) = client_a.build_local_project(fs.clone(), "/a", cx_a).await;
+        project_a
+            .update(cx_a, |project, cx| project.share(cx))
+            .await
+            .unwrap();
+
+        // Client B joins the project.
+        let project_b = client_b
+            .build_remote_project(
+                project_a
+                    .read_with(cx_a, |project, _| project.remote_id())
+                    .unwrap(),
+                cx_b,
+            )
+            .await;
+
+        // Client A opens some editors.
+        let workspace_a = client_a.build_workspace(&project_a, cx_a);
+        let _editor_a1 = workspace_a
+            .update(cx_a, |workspace, cx| {
+                workspace.open_path((worktree_id, "1.txt"), cx)
+            })
+            .await
+            .unwrap()
+            .downcast::<Editor>()
+            .unwrap();
+
+        // Client B starts following client A.
+        let workspace_b = client_b.build_workspace(&project_b, cx_b);
+        let pane_b = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone());
+        let leader_id = project_b.read_with(cx_b, |project, _| {
+            project.collaborators().values().next().unwrap().peer_id
+        });
+        workspace_b
+            .update(cx_b, |workspace, cx| {
+                workspace.toggle_follow(&leader_id.into(), cx).unwrap()
+            })
+            .await
+            .unwrap();
+        assert_eq!(
+            workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
+            Some(leader_id)
+        );
+        let editor_b2 = workspace_b.read_with(cx_b, |workspace, cx| {
+            workspace
+                .active_item(cx)
+                .unwrap()
+                .downcast::<Editor>()
+                .unwrap()
+        });
+
+        // When client B moves, it automatically stops following client A.
+        editor_b2.update(cx_b, |editor, cx| editor.move_right(&editor::MoveRight, cx));
+        assert_eq!(
+            workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
+            None
+        );
+
+        workspace_b
+            .update(cx_b, |workspace, cx| {
+                workspace.toggle_follow(&leader_id.into(), cx).unwrap()
+            })
+            .await
+            .unwrap();
+        assert_eq!(
+            workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
+            Some(leader_id)
+        );
+
+        // When client B edits, it automatically stops following client A.
+        editor_b2.update(cx_b, |editor, cx| editor.insert("X", cx));
+        assert_eq!(
+            workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
+            None
+        );
+
+        workspace_b
+            .update(cx_b, |workspace, cx| {
+                workspace.toggle_follow(&leader_id.into(), cx).unwrap()
+            })
+            .await
+            .unwrap();
+        assert_eq!(
+            workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
+            Some(leader_id)
+        );
+
+        // When client B scrolls, it automatically stops following client A.
+        editor_b2.update(cx_b, |editor, cx| {
+            editor.set_scroll_position(vec2f(0., 3.), cx)
+        });
+        assert_eq!(
+            workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
+            None
+        );
+
+        workspace_b
+            .update(cx_b, |workspace, cx| {
+                workspace.toggle_follow(&leader_id.into(), cx).unwrap()
+            })
+            .await
+            .unwrap();
+        assert_eq!(
+            workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
+            Some(leader_id)
+        );
+
+        // When client B activates a different pane, it continues following client A in the original pane.
+        workspace_b.update(cx_b, |workspace, cx| {
+            workspace.split_pane(pane_b.clone(), SplitDirection::Right, cx)
+        });
+        assert_eq!(
+            workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
+            Some(leader_id)
+        );
+
+        workspace_b.update(cx_b, |workspace, cx| workspace.activate_next_pane(cx));
+        assert_eq!(
+            workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
+            Some(leader_id)
+        );
+
+        // When client B activates a different item in the original pane, it automatically stops following client A.
+        workspace_b
+            .update(cx_b, |workspace, cx| {
+                workspace.open_path((worktree_id, "2.txt"), cx)
+            })
+            .await
+            .unwrap();
+        assert_eq!(
+            workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
+            None
+        );
+    }
+
     #[gpui::test(iterations = 100)]
     async fn test_random_collaboration(cx: &mut TestAppContext, rng: StdRng) {
         cx.foreground().forbid_parking();

crates/text/src/anchor.rs 🔗

@@ -12,23 +12,19 @@ pub struct Anchor {
 }
 
 impl Anchor {
-    pub fn min() -> Self {
-        Self {
-            timestamp: clock::Local::MIN,
-            offset: usize::MIN,
-            bias: Bias::Left,
-        }
-    }
+    pub const MIN: Self = Self {
+        timestamp: clock::Local::MIN,
+        offset: usize::MIN,
+        bias: Bias::Left,
+    };
 
-    pub fn max() -> Self {
-        Self {
-            timestamp: clock::Local::MAX,
-            offset: usize::MAX,
-            bias: Bias::Right,
-        }
-    }
+    pub const MAX: Self = Self {
+        timestamp: clock::Local::MAX,
+        offset: usize::MAX,
+        bias: Bias::Right,
+    };
 
-    pub fn cmp(&self, other: &Anchor, buffer: &BufferSnapshot) -> Result<Ordering> {
+    pub fn cmp(&self, other: &Anchor, buffer: &BufferSnapshot) -> Ordering {
         let fragment_id_comparison = if self.timestamp == other.timestamp {
             Ordering::Equal
         } else {
@@ -37,9 +33,25 @@ impl Anchor {
                 .cmp(&buffer.fragment_id_for_anchor(other))
         };
 
-        Ok(fragment_id_comparison
+        fragment_id_comparison
             .then_with(|| self.offset.cmp(&other.offset))
-            .then_with(|| self.bias.cmp(&other.bias)))
+            .then_with(|| self.bias.cmp(&other.bias))
+    }
+
+    pub fn min(&self, other: &Self, buffer: &BufferSnapshot) -> Self {
+        if self.cmp(other, buffer).is_le() {
+            self.clone()
+        } else {
+            other.clone()
+        }
+    }
+
+    pub fn max(&self, other: &Self, buffer: &BufferSnapshot) -> Self {
+        if self.cmp(other, buffer).is_ge() {
+            self.clone()
+        } else {
+            other.clone()
+        }
     }
 
     pub fn bias(&self, bias: Bias, buffer: &BufferSnapshot) -> Anchor {
@@ -105,8 +117,8 @@ pub trait AnchorRangeExt {
 
 impl AnchorRangeExt for Range<Anchor> {
     fn cmp(&self, other: &Range<Anchor>, buffer: &BufferSnapshot) -> Result<Ordering> {
-        Ok(match self.start.cmp(&other.start, buffer)? {
-            Ordering::Equal => other.end.cmp(&self.end, buffer)?,
+        Ok(match self.start.cmp(&other.start, buffer) {
+            Ordering::Equal => other.end.cmp(&self.end, buffer),
             ord @ _ => ord,
         })
     }

crates/text/src/patch.rs 🔗

@@ -199,6 +199,28 @@ where
             self.0.push(edit);
         }
     }
+
+    pub fn old_to_new(&self, old: T) -> T {
+        let ix = match self.0.binary_search_by(|probe| probe.old.start.cmp(&old)) {
+            Ok(ix) => ix,
+            Err(ix) => {
+                if ix == 0 {
+                    return old;
+                } else {
+                    ix - 1
+                }
+            }
+        };
+        if let Some(edit) = self.0.get(ix) {
+            if old >= edit.old.end {
+                edit.new.end + (old - edit.old.end)
+            } else {
+                edit.new.start
+            }
+        } else {
+            old
+        }
+    }
 }
 
 impl<T: Clone> IntoIterator for Patch<T> {
@@ -399,26 +421,6 @@ mod tests {
         );
     }
 
-    // #[test]
-    // fn test_compose_edits() {
-    //     assert_eq!(
-    //         compose_edits(
-    //             &Edit {
-    //                 old: 3..3,
-    //                 new: 3..6,
-    //             },
-    //             &Edit {
-    //                 old: 2..7,
-    //                 new: 2..4,
-    //             },
-    //         ),
-    //         Edit {
-    //             old: 2..4,
-    //             new: 2..4
-    //         }
-    //     );
-    // }
-
     #[gpui::test]
     fn test_two_new_edits_touching_one_old_edit() {
         assert_patch_composition(
@@ -455,6 +457,30 @@ mod tests {
         );
     }
 
+    #[gpui::test]
+    fn test_old_to_new() {
+        let patch = Patch(vec![
+            Edit {
+                old: 2..4,
+                new: 2..4,
+            },
+            Edit {
+                old: 7..8,
+                new: 7..11,
+            },
+        ]);
+        assert_eq!(patch.old_to_new(0), 0);
+        assert_eq!(patch.old_to_new(1), 1);
+        assert_eq!(patch.old_to_new(2), 2);
+        assert_eq!(patch.old_to_new(3), 2);
+        assert_eq!(patch.old_to_new(4), 4);
+        assert_eq!(patch.old_to_new(5), 5);
+        assert_eq!(patch.old_to_new(6), 6);
+        assert_eq!(patch.old_to_new(7), 7);
+        assert_eq!(patch.old_to_new(8), 11);
+        assert_eq!(patch.old_to_new(9), 12);
+    }
+
     #[gpui::test(iterations = 100)]
     fn test_random_patch_compositions(mut rng: StdRng) {
         let operations = env::var("OPERATIONS")

crates/text/src/selection.rs 🔗

@@ -1,5 +1,5 @@
 use crate::Anchor;
-use crate::{rope::TextDimension, BufferSnapshot, ToOffset, ToPoint};
+use crate::{rope::TextDimension, BufferSnapshot};
 use std::cmp::Ordering;
 
 #[derive(Copy, Clone, Debug, Eq, PartialEq)]
@@ -18,6 +18,12 @@ pub struct Selection<T> {
     pub goal: SelectionGoal,
 }
 
+impl Default for SelectionGoal {
+    fn default() -> Self {
+        Self::None
+    }
+}
+
 impl<T: Clone> Selection<T> {
     pub fn head(&self) -> T {
         if self.reversed {
@@ -34,14 +40,27 @@ impl<T: Clone> Selection<T> {
             self.start.clone()
         }
     }
+
+    pub fn map<F, S>(&self, f: F) -> Selection<S>
+    where
+        F: Fn(T) -> S,
+    {
+        Selection::<S> {
+            id: self.id,
+            start: f(self.start.clone()),
+            end: f(self.end.clone()),
+            reversed: self.reversed,
+            goal: self.goal,
+        }
+    }
 }
 
-impl<T: ToOffset + ToPoint + Copy + Ord> Selection<T> {
+impl<T: Copy + Ord> Selection<T> {
     pub fn is_empty(&self) -> bool {
         self.start == self.end
     }
 
-    pub fn set_head(&mut self, head: T) {
+    pub fn set_head(&mut self, head: T, new_goal: SelectionGoal) {
         if head.cmp(&self.tail()) < Ordering::Equal {
             if !self.reversed {
                 self.end = self.start;
@@ -55,6 +74,14 @@ impl<T: ToOffset + ToPoint + Copy + Ord> Selection<T> {
             }
             self.end = head;
         }
+        self.goal = new_goal;
+    }
+
+    pub fn collapse_to(&mut self, point: T, new_goal: SelectionGoal) {
+        self.start = point;
+        self.end = point;
+        self.goal = new_goal;
+        self.reversed = false;
     }
 }
 

crates/text/src/tests.rs 🔗

@@ -340,59 +340,41 @@ fn test_anchors() {
     let anchor_at_offset_2 = buffer.anchor_before(2);
 
     assert_eq!(
-        anchor_at_offset_0
-            .cmp(&anchor_at_offset_0, &buffer)
-            .unwrap(),
+        anchor_at_offset_0.cmp(&anchor_at_offset_0, &buffer),
         Ordering::Equal
     );
     assert_eq!(
-        anchor_at_offset_1
-            .cmp(&anchor_at_offset_1, &buffer)
-            .unwrap(),
+        anchor_at_offset_1.cmp(&anchor_at_offset_1, &buffer),
         Ordering::Equal
     );
     assert_eq!(
-        anchor_at_offset_2
-            .cmp(&anchor_at_offset_2, &buffer)
-            .unwrap(),
+        anchor_at_offset_2.cmp(&anchor_at_offset_2, &buffer),
         Ordering::Equal
     );
 
     assert_eq!(
-        anchor_at_offset_0
-            .cmp(&anchor_at_offset_1, &buffer)
-            .unwrap(),
+        anchor_at_offset_0.cmp(&anchor_at_offset_1, &buffer),
         Ordering::Less
     );
     assert_eq!(
-        anchor_at_offset_1
-            .cmp(&anchor_at_offset_2, &buffer)
-            .unwrap(),
+        anchor_at_offset_1.cmp(&anchor_at_offset_2, &buffer),
         Ordering::Less
     );
     assert_eq!(
-        anchor_at_offset_0
-            .cmp(&anchor_at_offset_2, &buffer)
-            .unwrap(),
+        anchor_at_offset_0.cmp(&anchor_at_offset_2, &buffer),
         Ordering::Less
     );
 
     assert_eq!(
-        anchor_at_offset_1
-            .cmp(&anchor_at_offset_0, &buffer)
-            .unwrap(),
+        anchor_at_offset_1.cmp(&anchor_at_offset_0, &buffer),
         Ordering::Greater
     );
     assert_eq!(
-        anchor_at_offset_2
-            .cmp(&anchor_at_offset_1, &buffer)
-            .unwrap(),
+        anchor_at_offset_2.cmp(&anchor_at_offset_1, &buffer),
         Ordering::Greater
     );
     assert_eq!(
-        anchor_at_offset_2
-            .cmp(&anchor_at_offset_0, &buffer)
-            .unwrap(),
+        anchor_at_offset_2.cmp(&anchor_at_offset_0, &buffer),
         Ordering::Greater
     );
 }

crates/text/src/text.rs 🔗

@@ -1318,8 +1318,8 @@ impl Buffer {
         let mut futures = Vec::new();
         for anchor in anchors {
             if !self.version.observed(anchor.timestamp)
-                && *anchor != Anchor::max()
-                && *anchor != Anchor::min()
+                && *anchor != Anchor::MAX
+                && *anchor != Anchor::MIN
             {
                 let (tx, rx) = oneshot::channel();
                 self.edit_id_resolvers
@@ -1638,9 +1638,9 @@ impl BufferSnapshot {
         let mut position = D::default();
 
         anchors.map(move |anchor| {
-            if *anchor == Anchor::min() {
+            if *anchor == Anchor::MIN {
                 return D::default();
-            } else if *anchor == Anchor::max() {
+            } else if *anchor == Anchor::MAX {
                 return D::from_text_summary(&self.visible_text.summary());
             }
 
@@ -1680,9 +1680,9 @@ impl BufferSnapshot {
     where
         D: TextDimension,
     {
-        if *anchor == Anchor::min() {
+        if *anchor == Anchor::MIN {
             D::default()
-        } else if *anchor == Anchor::max() {
+        } else if *anchor == Anchor::MAX {
             D::from_text_summary(&self.visible_text.summary())
         } else {
             let anchor_key = InsertionFragmentKey {
@@ -1718,9 +1718,9 @@ impl BufferSnapshot {
     }
 
     fn fragment_id_for_anchor(&self, anchor: &Anchor) -> &Locator {
-        if *anchor == Anchor::min() {
+        if *anchor == Anchor::MIN {
             &locator::MIN
-        } else if *anchor == Anchor::max() {
+        } else if *anchor == Anchor::MAX {
             &locator::MAX
         } else {
             let anchor_key = InsertionFragmentKey {
@@ -1758,9 +1758,9 @@ impl BufferSnapshot {
     pub fn anchor_at<T: ToOffset>(&self, position: T, bias: Bias) -> Anchor {
         let offset = position.to_offset(self);
         if bias == Bias::Left && offset == 0 {
-            Anchor::min()
+            Anchor::MIN
         } else if bias == Bias::Right && offset == self.len() {
-            Anchor::max()
+            Anchor::MAX
         } else {
             let mut fragment_cursor = self.fragments.cursor::<usize>();
             fragment_cursor.seek(&offset, bias, &None);
@@ -1775,9 +1775,7 @@ impl BufferSnapshot {
     }
 
     pub fn can_resolve(&self, anchor: &Anchor) -> bool {
-        *anchor == Anchor::min()
-            || *anchor == Anchor::max()
-            || self.version.observed(anchor.timestamp)
+        *anchor == Anchor::MIN || *anchor == Anchor::MAX || self.version.observed(anchor.timestamp)
     }
 
     pub fn clip_offset(&self, offset: usize, bias: Bias) -> usize {
@@ -1799,7 +1797,7 @@ impl BufferSnapshot {
     where
         D: TextDimension + Ord,
     {
-        self.edits_since_in_range(since, Anchor::min()..Anchor::max())
+        self.edits_since_in_range(since, Anchor::MIN..Anchor::MAX)
     }
 
     pub fn edited_ranges_for_transaction<'a, D>(

crates/theme/src/theme.rs 🔗

@@ -35,6 +35,8 @@ pub struct Workspace {
     pub tab: Tab,
     pub active_tab: Tab,
     pub pane_divider: Border,
+    pub leader_border_opacity: f32,
+    pub leader_border_width: f32,
     pub left_sidebar: Sidebar,
     pub right_sidebar: Sidebar,
     pub status_bar: StatusBar,

crates/theme_selector/src/theme_selector.rs 🔗

@@ -54,7 +54,7 @@ impl ThemeSelector {
         cx.subscribe(&query_editor, Self::on_query_editor_event)
             .detach();
 
-        let original_theme = cx.app_state::<Settings>().theme.clone();
+        let original_theme = cx.global::<Settings>().theme.clone();
 
         let mut this = Self {
             themes: registry,
@@ -82,7 +82,7 @@ impl ThemeSelector {
     }
 
     fn reload(_: &mut Workspace, action: &Reload, cx: &mut ViewContext<Workspace>) {
-        let current_theme_name = cx.app_state::<Settings>().theme.name.clone();
+        let current_theme_name = cx.global::<Settings>().theme.name.clone();
         action.0.clear();
         match action.0.get(&current_theme_name) {
             Ok(theme) => {
@@ -204,9 +204,9 @@ impl ThemeSelector {
         cx: &mut ViewContext<Self>,
     ) {
         match event {
-            editor::Event::Edited => {
+            editor::Event::BufferEdited { .. } => {
                 self.update_matches(cx);
-                self.select_if_matching(&cx.app_state::<Settings>().theme.name);
+                self.select_if_matching(&cx.global::<Settings>().theme.name);
                 self.show_selected_theme(cx);
             }
             editor::Event::Blurred => cx.emit(Event::Dismissed),
@@ -216,7 +216,7 @@ impl ThemeSelector {
 
     fn render_matches(&self, cx: &mut RenderContext<Self>) -> ElementBox {
         if self.matches.is_empty() {
-            let settings = cx.app_state::<Settings>();
+            let settings = cx.global::<Settings>();
             return Container::new(
                 Label::new(
                     "No matches".into(),
@@ -251,7 +251,7 @@ impl ThemeSelector {
     }
 
     fn render_match(&self, theme_match: &StringMatch, index: usize, cx: &AppContext) -> ElementBox {
-        let settings = cx.app_state::<Settings>();
+        let settings = cx.global::<Settings>();
         let theme = &settings.theme;
 
         let container = Container::new(
@@ -276,7 +276,7 @@ impl ThemeSelector {
     }
 
     fn set_theme(theme: Arc<Theme>, cx: &mut MutableAppContext) {
-        cx.update_app_state::<Settings, _, _>(|settings, cx| {
+        cx.update_global::<Settings, _, _>(|settings, cx| {
             settings.theme = theme;
             cx.refresh_windows();
         });
@@ -299,7 +299,7 @@ impl View for ThemeSelector {
     }
 
     fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
-        let theme = cx.app_state::<Settings>().theme.clone();
+        let theme = cx.global::<Settings>().theme.clone();
         Align::new(
             ConstrainedBox::new(
                 Container::new(

crates/util/src/test.rs 🔗

@@ -1,4 +1,8 @@
-use std::path::{Path, PathBuf};
+use std::{
+    collections::HashMap,
+    ops::Range,
+    path::{Path, PathBuf},
+};
 use tempdir::TempDir;
 
 pub fn temp_tree(tree: serde_json::Value) -> TempDir {
@@ -48,3 +52,44 @@ pub fn sample_text(rows: usize, cols: usize, start_char: char) -> String {
     }
     text
 }
+
+pub fn marked_text_by(
+    marked_text: &str,
+    markers: Vec<char>,
+) -> (String, HashMap<char, Vec<usize>>) {
+    let mut extracted_markers: HashMap<char, Vec<usize>> = Default::default();
+    let mut unmarked_text = String::new();
+
+    for char in marked_text.chars() {
+        if markers.contains(&char) {
+            let char_offsets = extracted_markers.entry(char).or_insert(Vec::new());
+            char_offsets.push(unmarked_text.len());
+        } else {
+            unmarked_text.push(char);
+        }
+    }
+
+    (unmarked_text, extracted_markers)
+}
+
+pub fn marked_text(marked_text: &str) -> (String, Vec<usize>) {
+    let (unmarked_text, mut markers) = marked_text_by(marked_text, vec!['|']);
+    (unmarked_text, markers.remove(&'|').unwrap_or_else(Vec::new))
+}
+
+pub fn marked_text_ranges(marked_text: &str) -> (String, Vec<Range<usize>>) {
+    let (unmarked_text, mut markers) = marked_text_by(marked_text, vec!['[', ']']);
+    let opens = markers.remove(&'[').unwrap_or_default();
+    let closes = markers.remove(&']').unwrap_or_default();
+    assert_eq!(opens.len(), closes.len(), "marked ranges are unbalanced");
+
+    let ranges = opens
+        .into_iter()
+        .zip(closes)
+        .map(|(open, close)| {
+            assert!(close >= open, "marked ranges must be disjoint");
+            open..close
+        })
+        .collect();
+    (unmarked_text, ranges)
+}

crates/vim/Cargo.toml 🔗

@@ -0,0 +1,25 @@
+[package]
+name = "vim"
+version = "0.1.0"
+edition = "2021"
+
+[lib]
+path = "src/vim.rs"
+doctest = false
+
+[dependencies]
+collections = { path = "../collections" }
+editor = { path = "../editor" }
+gpui = { path = "../gpui" }
+language = { path = "../language" }
+workspace = { path = "../workspace" }
+log = "0.4"
+
+[dev-dependencies]
+indoc = "1.0.4"
+editor = { path = "../editor", features = ["test-support"] }
+gpui = { path = "../gpui", features = ["test-support"] }
+project = { path = "../project", features = ["test-support"] }
+language = { path = "../language", features = ["test-support"] }
+util = { path = "../util", features = ["test-support"] }
+workspace = { path = "../workspace", features = ["test-support"] }

crates/vim/src/editor_events.rs 🔗

@@ -0,0 +1,53 @@
+use editor::{EditorBlurred, EditorCreated, EditorFocused, EditorMode, EditorReleased};
+use gpui::MutableAppContext;
+
+use crate::{mode::Mode, SwitchMode, VimState};
+
+pub fn init(cx: &mut MutableAppContext) {
+    cx.subscribe_global(editor_created).detach();
+    cx.subscribe_global(editor_focused).detach();
+    cx.subscribe_global(editor_blurred).detach();
+    cx.subscribe_global(editor_released).detach();
+}
+
+fn editor_created(EditorCreated(editor): &EditorCreated, cx: &mut MutableAppContext) {
+    cx.update_default_global(|vim_state: &mut VimState, cx| {
+        vim_state.editors.insert(editor.id(), editor.downgrade());
+        vim_state.sync_editor_options(cx);
+    })
+}
+
+fn editor_focused(EditorFocused(editor): &EditorFocused, cx: &mut MutableAppContext) {
+    let mode = if matches!(editor.read(cx).mode(), EditorMode::SingleLine) {
+        Mode::Insert
+    } else {
+        Mode::Normal
+    };
+
+    VimState::update_global(cx, |state, cx| {
+        state.active_editor = Some(editor.downgrade());
+        state.switch_mode(&SwitchMode(mode), cx);
+    });
+}
+
+fn editor_blurred(EditorBlurred(editor): &EditorBlurred, cx: &mut MutableAppContext) {
+    VimState::update_global(cx, |state, cx| {
+        if let Some(previous_editor) = state.active_editor.clone() {
+            if previous_editor == editor.clone() {
+                state.active_editor = None;
+            }
+        }
+        state.sync_editor_options(cx);
+    })
+}
+
+fn editor_released(EditorReleased(editor): &EditorReleased, cx: &mut MutableAppContext) {
+    cx.update_default_global(|vim_state: &mut VimState, _| {
+        vim_state.editors.remove(&editor.id());
+        if let Some(previous_editor) = vim_state.active_editor.clone() {
+            if previous_editor == editor.clone() {
+                vim_state.active_editor = None;
+            }
+        }
+    });
+}

crates/vim/src/insert.rs 🔗

@@ -0,0 +1,30 @@
+use editor::Bias;
+use gpui::{action, keymap::Binding, MutableAppContext, ViewContext};
+use language::SelectionGoal;
+use workspace::Workspace;
+
+use crate::{mode::Mode, SwitchMode, VimState};
+
+action!(NormalBefore);
+
+pub fn init(cx: &mut MutableAppContext) {
+    let context = Some("Editor && vim_mode == insert");
+    cx.add_bindings(vec![
+        Binding::new("escape", NormalBefore, context),
+        Binding::new("ctrl-c", NormalBefore, context),
+    ]);
+
+    cx.add_action(normal_before);
+}
+
+fn normal_before(_: &mut Workspace, _: &NormalBefore, cx: &mut ViewContext<Workspace>) {
+    VimState::update_global(cx, |state, cx| {
+        state.update_active_editor(cx, |editor, cx| {
+            editor.move_cursors(cx, |map, mut cursor, _| {
+                *cursor.column_mut() = cursor.column().saturating_sub(1);
+                (map.clip_point(cursor, Bias::Left), SelectionGoal::None)
+            });
+        });
+        state.switch_mode(&SwitchMode(Mode::Normal), cx);
+    })
+}

crates/vim/src/mode.rs 🔗

@@ -0,0 +1,36 @@
+use editor::CursorShape;
+use gpui::keymap::Context;
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum Mode {
+    Normal,
+    Insert,
+}
+
+impl Mode {
+    pub fn cursor_shape(&self) -> CursorShape {
+        match self {
+            Mode::Normal => CursorShape::Block,
+            Mode::Insert => CursorShape::Bar,
+        }
+    }
+
+    pub fn keymap_context_layer(&self) -> Context {
+        let mut context = Context::default();
+        context.map.insert(
+            "vim_mode".to_string(),
+            match self {
+                Self::Normal => "normal",
+                Self::Insert => "insert",
+            }
+            .to_string(),
+        );
+        context
+    }
+}
+
+impl Default for Mode {
+    fn default() -> Self {
+        Self::Normal
+    }
+}

crates/vim/src/normal.rs 🔗

@@ -0,0 +1,66 @@
+use editor::{movement, Bias};
+use gpui::{action, keymap::Binding, MutableAppContext, ViewContext};
+use language::SelectionGoal;
+use workspace::Workspace;
+
+use crate::{Mode, SwitchMode, VimState};
+
+action!(InsertBefore);
+action!(MoveLeft);
+action!(MoveDown);
+action!(MoveUp);
+action!(MoveRight);
+
+pub fn init(cx: &mut MutableAppContext) {
+    let context = Some("Editor && vim_mode == normal");
+    cx.add_bindings(vec![
+        Binding::new("i", SwitchMode(Mode::Insert), context),
+        Binding::new("h", MoveLeft, context),
+        Binding::new("j", MoveDown, context),
+        Binding::new("k", MoveUp, context),
+        Binding::new("l", MoveRight, context),
+    ]);
+
+    cx.add_action(move_left);
+    cx.add_action(move_down);
+    cx.add_action(move_up);
+    cx.add_action(move_right);
+}
+
+fn move_left(_: &mut Workspace, _: &MoveLeft, cx: &mut ViewContext<Workspace>) {
+    VimState::update_global(cx, |state, cx| {
+        state.update_active_editor(cx, |editor, cx| {
+            editor.move_cursors(cx, |map, mut cursor, _| {
+                *cursor.column_mut() = cursor.column().saturating_sub(1);
+                (map.clip_point(cursor, Bias::Left), SelectionGoal::None)
+            });
+        });
+    })
+}
+
+fn move_down(_: &mut Workspace, _: &MoveDown, cx: &mut ViewContext<Workspace>) {
+    VimState::update_global(cx, |state, cx| {
+        state.update_active_editor(cx, |editor, cx| {
+            editor.move_cursors(cx, movement::down);
+        });
+    });
+}
+
+fn move_up(_: &mut Workspace, _: &MoveUp, cx: &mut ViewContext<Workspace>) {
+    VimState::update_global(cx, |state, cx| {
+        state.update_active_editor(cx, |editor, cx| {
+            editor.move_cursors(cx, movement::up);
+        });
+    });
+}
+
+fn move_right(_: &mut Workspace, _: &MoveRight, cx: &mut ViewContext<Workspace>) {
+    VimState::update_global(cx, |state, cx| {
+        state.update_active_editor(cx, |editor, cx| {
+            editor.move_cursors(cx, |map, mut cursor, _| {
+                *cursor.column_mut() += 1;
+                (map.clip_point(cursor, Bias::Right), SelectionGoal::None)
+            });
+        });
+    });
+}

crates/vim/src/vim.rs 🔗

@@ -0,0 +1,97 @@
+mod editor_events;
+mod insert;
+mod mode;
+mod normal;
+#[cfg(test)]
+mod vim_tests;
+
+use collections::HashMap;
+use editor::{CursorShape, Editor};
+use gpui::{action, MutableAppContext, ViewContext, WeakViewHandle};
+
+use mode::Mode;
+use workspace::{self, Settings, Workspace};
+
+action!(SwitchMode, Mode);
+
+pub fn init(cx: &mut MutableAppContext) {
+    editor_events::init(cx);
+    insert::init(cx);
+    normal::init(cx);
+
+    cx.add_action(|_: &mut Workspace, action: &SwitchMode, cx| {
+        VimState::update_global(cx, |state, cx| state.switch_mode(action, cx))
+    });
+
+    cx.observe_global::<Settings, _>(|settings, cx| {
+        VimState::update_global(cx, |state, cx| state.set_enabled(settings.vim_mode, cx))
+    })
+    .detach();
+}
+
+#[derive(Default)]
+pub struct VimState {
+    editors: HashMap<usize, WeakViewHandle<Editor>>,
+    active_editor: Option<WeakViewHandle<Editor>>,
+
+    enabled: bool,
+    mode: Mode,
+}
+
+impl VimState {
+    fn update_global<F, S>(cx: &mut MutableAppContext, update: F) -> S
+    where
+        F: FnOnce(&mut Self, &mut MutableAppContext) -> S,
+    {
+        cx.update_default_global(update)
+    }
+
+    fn update_active_editor<S>(
+        &self,
+        cx: &mut MutableAppContext,
+        update: impl FnOnce(&mut Editor, &mut ViewContext<Editor>) -> S,
+    ) -> Option<S> {
+        self.active_editor
+            .clone()
+            .and_then(|ae| ae.upgrade(cx))
+            .map(|ae| ae.update(cx, update))
+    }
+
+    fn switch_mode(&mut self, SwitchMode(mode): &SwitchMode, cx: &mut MutableAppContext) {
+        self.mode = *mode;
+        self.sync_editor_options(cx);
+    }
+
+    fn set_enabled(&mut self, enabled: bool, cx: &mut MutableAppContext) {
+        if self.enabled != enabled {
+            self.enabled = enabled;
+            if enabled {
+                self.mode = Mode::Normal;
+            }
+            self.sync_editor_options(cx);
+        }
+    }
+
+    fn sync_editor_options(&self, cx: &mut MutableAppContext) {
+        let mode = self.mode;
+        let cursor_shape = mode.cursor_shape();
+        for editor in self.editors.values() {
+            if let Some(editor) = editor.upgrade(cx) {
+                editor.update(cx, |editor, cx| {
+                    if self.enabled {
+                        editor.set_cursor_shape(cursor_shape, cx);
+                        editor.set_clip_at_line_ends(cursor_shape == CursorShape::Block, cx);
+                        editor.set_input_enabled(mode == Mode::Insert);
+                        let context_layer = mode.keymap_context_layer();
+                        editor.set_keymap_context_layer::<Self>(context_layer);
+                    } else {
+                        editor.set_cursor_shape(CursorShape::Bar, cx);
+                        editor.set_clip_at_line_ends(false, cx);
+                        editor.set_input_enabled(true);
+                        editor.remove_keymap_context_layer::<Self>();
+                    }
+                });
+            }
+        }
+    }
+}

crates/vim/src/vim_tests.rs 🔗

@@ -0,0 +1,253 @@
+use indoc::indoc;
+use std::ops::Deref;
+
+use editor::{display_map::ToDisplayPoint, DisplayPoint};
+use gpui::{json::json, keymap::Keystroke, ViewHandle};
+use language::{Point, Selection};
+use util::test::marked_text;
+use workspace::{WorkspaceHandle, WorkspaceParams};
+
+use crate::*;
+
+#[gpui::test]
+async fn test_insert_mode(cx: &mut gpui::TestAppContext) {
+    let mut cx = VimTestAppContext::new(cx, true, "").await;
+    cx.simulate_keystroke("i");
+    assert_eq!(cx.mode(), Mode::Insert);
+    cx.simulate_keystrokes(&["T", "e", "s", "t"]);
+    cx.assert_newest_selection_head("Test|");
+    cx.simulate_keystroke("escape");
+    assert_eq!(cx.mode(), Mode::Normal);
+    cx.assert_newest_selection_head("Tes|t");
+}
+
+#[gpui::test]
+async fn test_normal_hjkl(cx: &mut gpui::TestAppContext) {
+    let mut cx = VimTestAppContext::new(cx, true, "Test\nTestTest\nTest").await;
+    cx.simulate_keystroke("l");
+    cx.assert_newest_selection_head(indoc! {"
+        T|est
+        TestTest
+        Test"});
+    cx.simulate_keystroke("h");
+    cx.assert_newest_selection_head(indoc! {"
+        |Test
+        TestTest
+        Test"});
+    cx.simulate_keystroke("j");
+    cx.assert_newest_selection_head(indoc! {"
+        Test
+        |TestTest
+        Test"});
+    cx.simulate_keystroke("k");
+    cx.assert_newest_selection_head(indoc! {"
+        |Test
+        TestTest
+        Test"});
+    cx.simulate_keystroke("j");
+    cx.assert_newest_selection_head(indoc! {"
+        Test
+        |TestTest
+        Test"});
+
+    // When moving left, cursor does not wrap to the previous line
+    cx.simulate_keystroke("h");
+    cx.assert_newest_selection_head(indoc! {"
+        Test
+        |TestTest
+        Test"});
+
+    // When moving right, cursor does not reach the line end or wrap to the next line
+    for _ in 0..9 {
+        cx.simulate_keystroke("l");
+    }
+    cx.assert_newest_selection_head(indoc! {"
+        Test
+        TestTes|t
+        Test"});
+
+    // Goal column respects the inability to reach the end of the line
+    cx.simulate_keystroke("k");
+    cx.assert_newest_selection_head(indoc! {"
+        Tes|t
+        TestTest
+        Test"});
+    cx.simulate_keystroke("j");
+    cx.assert_newest_selection_head(indoc! {"
+        Test
+        TestTes|t
+        Test"});
+}
+
+#[gpui::test]
+async fn test_toggle_through_settings(cx: &mut gpui::TestAppContext) {
+    let mut cx = VimTestAppContext::new(cx, true, "").await;
+
+    cx.simulate_keystroke("i");
+    assert_eq!(cx.mode(), Mode::Insert);
+
+    // Editor acts as though vim is disabled
+    cx.disable_vim();
+    cx.simulate_keystrokes(&["h", "j", "k", "l"]);
+    cx.assert_newest_selection_head("hjkl|");
+
+    // Enabling dynamically sets vim mode again and restores normal mode
+    cx.enable_vim();
+    assert_eq!(cx.mode(), Mode::Normal);
+    cx.simulate_keystrokes(&["h", "h", "h", "l"]);
+    assert_eq!(cx.editor_text(), "hjkl".to_owned());
+    cx.assert_newest_selection_head("hj|kl");
+    cx.simulate_keystrokes(&["i", "T", "e", "s", "t"]);
+    cx.assert_newest_selection_head("hjTest|kl");
+
+    // Disabling and enabling resets to normal mode
+    assert_eq!(cx.mode(), Mode::Insert);
+    cx.disable_vim();
+    assert_eq!(cx.mode(), Mode::Insert);
+    cx.enable_vim();
+    assert_eq!(cx.mode(), Mode::Normal);
+}
+
+#[gpui::test]
+async fn test_initially_disabled(cx: &mut gpui::TestAppContext) {
+    let mut cx = VimTestAppContext::new(cx, false, "").await;
+    cx.simulate_keystrokes(&["h", "j", "k", "l"]);
+    cx.assert_newest_selection_head("hjkl|");
+}
+
+struct VimTestAppContext<'a> {
+    cx: &'a mut gpui::TestAppContext,
+    window_id: usize,
+    editor: ViewHandle<Editor>,
+}
+
+impl<'a> VimTestAppContext<'a> {
+    async fn new(
+        cx: &'a mut gpui::TestAppContext,
+        enabled: bool,
+        initial_editor_text: &str,
+    ) -> VimTestAppContext<'a> {
+        cx.update(|cx| {
+            editor::init(cx);
+            crate::init(cx);
+        });
+        let params = cx.update(WorkspaceParams::test);
+
+        cx.update(|cx| {
+            cx.update_global(|settings: &mut Settings, _| {
+                settings.vim_mode = enabled;
+            });
+        });
+
+        params
+            .fs
+            .as_fake()
+            .insert_tree(
+                "/root",
+                json!({ "dir": { "test.txt": initial_editor_text } }),
+            )
+            .await;
+
+        let (window_id, workspace) = cx.add_window(|cx| Workspace::new(&params, cx));
+        params
+            .project
+            .update(cx, |project, cx| {
+                project.find_or_create_local_worktree("/root", true, cx)
+            })
+            .await
+            .unwrap();
+        cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx))
+            .await;
+
+        let file = cx.read(|cx| workspace.file_project_paths(cx)[0].clone());
+        let item = workspace
+            .update(cx, |workspace, cx| workspace.open_path(file, cx))
+            .await
+            .expect("Could not open test file");
+
+        let editor = cx.update(|cx| {
+            item.act_as::<Editor>(cx)
+                .expect("Opened test file wasn't an editor")
+        });
+        editor.update(cx, |_, cx| cx.focus_self());
+
+        Self {
+            cx,
+            window_id,
+            editor,
+        }
+    }
+
+    fn enable_vim(&mut self) {
+        self.cx.update(|cx| {
+            cx.update_global(|settings: &mut Settings, _| {
+                settings.vim_mode = true;
+            });
+        })
+    }
+
+    fn disable_vim(&mut self) {
+        self.cx.update(|cx| {
+            cx.update_global(|settings: &mut Settings, _| {
+                settings.vim_mode = false;
+            });
+        })
+    }
+
+    fn newest_selection(&mut self) -> Selection<DisplayPoint> {
+        self.editor.update(self.cx, |editor, cx| {
+            let snapshot = editor.snapshot(cx);
+            editor
+                .newest_selection::<Point>(cx)
+                .map(|point| point.to_display_point(&snapshot.display_snapshot))
+        })
+    }
+
+    fn mode(&mut self) -> Mode {
+        self.cx.update(|cx| cx.global::<VimState>().mode)
+    }
+
+    fn editor_text(&mut self) -> String {
+        self.editor
+            .update(self.cx, |editor, cx| editor.snapshot(cx).text())
+    }
+
+    fn simulate_keystroke(&mut self, keystroke_text: &str) {
+        let keystroke = Keystroke::parse(keystroke_text).unwrap();
+        let input = if keystroke.modified() {
+            None
+        } else {
+            Some(keystroke.key.clone())
+        };
+        self.cx
+            .dispatch_keystroke(self.window_id, keystroke, input, false);
+    }
+
+    fn simulate_keystrokes(&mut self, keystroke_texts: &[&str]) {
+        for keystroke_text in keystroke_texts.into_iter() {
+            self.simulate_keystroke(keystroke_text);
+        }
+    }
+
+    fn assert_newest_selection_head(&mut self, text: &str) {
+        let (unmarked_text, markers) = marked_text(&text);
+        assert_eq!(
+            self.editor_text(),
+            unmarked_text,
+            "Unmarked text doesn't match editor text"
+        );
+        let newest_selection = self.newest_selection();
+        let expected_head = self.editor.update(self.cx, |editor, cx| {
+            markers[0].to_display_point(&editor.snapshot(cx))
+        });
+        assert_eq!(newest_selection.head(), expected_head)
+    }
+}
+
+impl<'a> Deref for VimTestAppContext<'a> {
+    type Target = gpui::TestAppContext;
+
+    fn deref(&self) -> &Self::Target {
+        self.cx
+    }
+}

crates/workspace/src/lsp_status.rs 🔗

@@ -1,4 +1,4 @@
-use crate::{ItemViewHandle, Settings, StatusItemView};
+use crate::{ItemHandle, Settings, StatusItemView};
 use futures::StreamExt;
 use gpui::AppContext;
 use gpui::{
@@ -116,7 +116,7 @@ impl View for LspStatus {
     }
 
     fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
-        let theme = &cx.app_state::<Settings>().theme;
+        let theme = &cx.global::<Settings>().theme;
 
         let mut pending_work = self.pending_language_server_work(cx);
         if let Some((lang_server_name, progress_token, progress)) = pending_work.next() {
@@ -166,7 +166,7 @@ impl View for LspStatus {
         } else if !self.failed.is_empty() {
             drop(pending_work);
             MouseEventHandler::new::<Self, _, _>(0, cx, |_, cx| {
-                let theme = &cx.app_state::<Settings>().theme;
+                let theme = &cx.global::<Settings>().theme;
                 Label::new(
                     format!(
                         "Failed to download {} language server{}. Click to dismiss.",
@@ -187,5 +187,5 @@ impl View for LspStatus {
 }
 
 impl StatusItemView for LspStatus {
-    fn set_active_pane_item(&mut self, _: Option<&dyn ItemViewHandle>, _: &mut ViewContext<Self>) {}
+    fn set_active_pane_item(&mut self, _: Option<&dyn ItemHandle>, _: &mut ViewContext<Self>) {}
 }

crates/workspace/src/pane.rs 🔗

@@ -1,5 +1,5 @@
-use super::{ItemViewHandle, SplitDirection};
-use crate::{ItemHandle, ItemView, Settings, WeakItemViewHandle, Workspace};
+use super::{ItemHandle, SplitDirection};
+use crate::{Item, Settings, WeakItemHandle, Workspace};
 use collections::{HashMap, VecDeque};
 use gpui::{
     action,
@@ -7,10 +7,10 @@ use gpui::{
     geometry::{rect::RectF, vector::vec2f},
     keymap::Binding,
     platform::{CursorStyle, NavigationDirection},
-    AnyViewHandle, Entity, MutableAppContext, Quad, RenderContext, Task, View, ViewContext,
-    ViewHandle, WeakViewHandle,
+    AnyViewHandle, AppContext, Entity, MutableAppContext, Quad, RenderContext, Task, View,
+    ViewContext, ViewHandle, WeakViewHandle,
 };
-use project::ProjectPath;
+use project::{ProjectEntryId, ProjectPath};
 use std::{
     any::{Any, TypeId},
     cell::RefCell,
@@ -33,7 +33,7 @@ const MAX_NAVIGATION_HISTORY_LEN: usize = 1024;
 
 pub fn init(cx: &mut MutableAppContext) {
     cx.add_action(|pane: &mut Pane, action: &ActivateItem, cx| {
-        pane.activate_item(action.0, cx);
+        pane.activate_item(action.0, true, cx);
     });
     cx.add_action(|pane: &mut Pane, _: &ActivatePrevItem, cx| {
         pane.activate_prev_item(cx);
@@ -92,12 +92,13 @@ pub fn init(cx: &mut MutableAppContext) {
 
 pub enum Event {
     Activate,
+    ActivateItem { local: bool },
     Remove,
     Split(SplitDirection),
 }
 
 pub struct Pane {
-    item_views: Vec<(usize, Box<dyn ItemViewHandle>)>,
+    items: Vec<Box<dyn ItemHandle>>,
     active_item_index: usize,
     nav_history: Rc<RefCell<NavHistory>>,
     toolbars: HashMap<TypeId, Box<dyn ToolbarHandle>>,
@@ -108,7 +109,7 @@ pub struct Pane {
 pub trait Toolbar: View {
     fn active_item_changed(
         &mut self,
-        item: Option<Box<dyn ItemViewHandle>>,
+        item: Option<Box<dyn ItemHandle>>,
         cx: &mut ViewContext<Self>,
     ) -> bool;
     fn on_dismiss(&mut self, cx: &mut ViewContext<Self>);
@@ -117,7 +118,7 @@ pub trait Toolbar: View {
 trait ToolbarHandle {
     fn active_item_changed(
         &self,
-        item: Option<Box<dyn ItemViewHandle>>,
+        item: Option<Box<dyn ItemHandle>>,
         cx: &mut MutableAppContext,
     ) -> bool;
     fn on_dismiss(&self, cx: &mut MutableAppContext);
@@ -126,7 +127,7 @@ trait ToolbarHandle {
 
 pub struct ItemNavHistory {
     history: Rc<RefCell<NavHistory>>,
-    item_view: Rc<dyn WeakItemViewHandle>,
+    item: Rc<dyn WeakItemHandle>,
 }
 
 #[derive(Default)]
@@ -152,14 +153,14 @@ impl Default for NavigationMode {
 }
 
 pub struct NavigationEntry {
-    pub item_view: Rc<dyn WeakItemViewHandle>,
+    pub item: Rc<dyn WeakItemHandle>,
     pub data: Option<Box<dyn Any>>,
 }
 
 impl Pane {
     pub fn new() -> Self {
         Self {
-            item_views: Vec::new(),
+            items: Vec::new(),
             active_item_index: 0,
             nav_history: Default::default(),
             toolbars: Default::default(),
@@ -211,40 +212,47 @@ impl Pane {
         workspace.activate_pane(pane.clone(), cx);
 
         let to_load = pane.update(cx, |pane, cx| {
-            // Retrieve the weak item handle from the history.
-            let entry = pane.nav_history.borrow_mut().pop(mode)?;
-
-            // If the item is still present in this pane, then activate it.
-            if let Some(index) = entry
-                .item_view
-                .upgrade(cx)
-                .and_then(|v| pane.index_for_item_view(v.as_ref()))
-            {
-                if let Some(item_view) = pane.active_item() {
-                    pane.nav_history.borrow_mut().set_mode(mode);
-                    item_view.deactivated(cx);
-                    pane.nav_history
-                        .borrow_mut()
-                        .set_mode(NavigationMode::Normal);
-                }
+            loop {
+                // Retrieve the weak item handle from the history.
+                let entry = pane.nav_history.borrow_mut().pop(mode)?;
+
+                // If the item is still present in this pane, then activate it.
+                if let Some(index) = entry
+                    .item
+                    .upgrade(cx)
+                    .and_then(|v| pane.index_for_item(v.as_ref()))
+                {
+                    if let Some(item) = pane.active_item() {
+                        pane.nav_history.borrow_mut().set_mode(mode);
+                        item.deactivated(cx);
+                        pane.nav_history
+                            .borrow_mut()
+                            .set_mode(NavigationMode::Normal);
+                    }
 
-                pane.active_item_index = index;
-                pane.focus_active_item(cx);
-                if let Some(data) = entry.data {
-                    pane.active_item()?.navigate(data, cx);
+                    let prev_active_index = mem::replace(&mut pane.active_item_index, index);
+                    pane.focus_active_item(cx);
+                    let mut navigated = prev_active_index != pane.active_item_index;
+                    if let Some(data) = entry.data {
+                        navigated |= pane.active_item()?.navigate(data, cx);
+                    }
+
+                    if navigated {
+                        cx.notify();
+                        break None;
+                    }
+                }
+                // If the item is no longer present in this pane, then retrieve its
+                // project path in order to reopen it.
+                else {
+                    break pane
+                        .nav_history
+                        .borrow_mut()
+                        .paths_by_item
+                        .get(&entry.item.id())
+                        .cloned()
+                        .map(|project_path| (project_path, entry));
                 }
-                cx.notify();
-                None
-            }
-            // If the item is no longer present in this pane, then retrieve its
-            // project path in order to reopen it.
-            else {
-                pane.nav_history
-                    .borrow_mut()
-                    .paths_by_item
-                    .get(&entry.item_view.id())
-                    .cloned()
-                    .map(|project_path| (project_path, entry))
             }
         });
 
@@ -253,18 +261,27 @@ impl Pane {
             let pane = pane.downgrade();
             let task = workspace.load_path(project_path, cx);
             cx.spawn(|workspace, mut cx| async move {
-                let item = task.await;
+                let task = task.await;
                 if let Some(pane) = pane.upgrade(&cx) {
-                    if let Some(item) = item.log_err() {
-                        workspace.update(&mut cx, |workspace, cx| {
-                            pane.update(cx, |p, _| p.nav_history.borrow_mut().set_mode(mode));
-                            let item_view = workspace.open_item_in_pane(item, &pane, cx);
-                            pane.update(cx, |p, _| {
-                                p.nav_history.borrow_mut().set_mode(NavigationMode::Normal)
-                            });
-
+                    if let Some((project_entry_id, build_item)) = task.log_err() {
+                        pane.update(&mut cx, |pane, _| {
+                            pane.nav_history.borrow_mut().set_mode(mode);
+                        });
+                        let item = workspace.update(&mut cx, |workspace, cx| {
+                            Self::open_item(
+                                workspace,
+                                pane.clone(),
+                                project_entry_id,
+                                cx,
+                                build_item,
+                            )
+                        });
+                        pane.update(&mut cx, |pane, cx| {
+                            pane.nav_history
+                                .borrow_mut()
+                                .set_mode(NavigationMode::Normal);
                             if let Some(data) = entry.data {
-                                item_view.navigate(data, cx);
+                                item.navigate(data, cx);
                             }
                         });
                     } else {
@@ -281,80 +298,115 @@ impl Pane {
         }
     }
 
-    pub fn open_item<T>(
-        &mut self,
-        item_handle: T,
-        workspace: &Workspace,
-        cx: &mut ViewContext<Self>,
-    ) -> Box<dyn ItemViewHandle>
-    where
-        T: 'static + ItemHandle,
-    {
-        for (ix, (item_id, item_view)) in self.item_views.iter().enumerate() {
-            if *item_id == item_handle.id() {
-                let item_view = item_view.boxed_clone();
-                self.activate_item(ix, cx);
-                return item_view;
+    pub(crate) fn open_item(
+        workspace: &mut Workspace,
+        pane: ViewHandle<Pane>,
+        project_entry_id: ProjectEntryId,
+        cx: &mut ViewContext<Workspace>,
+        build_item: impl FnOnce(&mut MutableAppContext) -> Box<dyn ItemHandle>,
+    ) -> Box<dyn ItemHandle> {
+        let existing_item = pane.update(cx, |pane, cx| {
+            for (ix, item) in pane.items.iter().enumerate() {
+                if item.project_entry_id(cx) == Some(project_entry_id) {
+                    let item = item.boxed_clone();
+                    pane.activate_item(ix, true, cx);
+                    return Some(item);
+                }
             }
+            None
+        });
+        if let Some(existing_item) = existing_item {
+            existing_item
+        } else {
+            let item = build_item(cx);
+            Self::add_item(workspace, pane, item.boxed_clone(), true, cx);
+            item
         }
-
-        let item_view =
-            item_handle.add_view(cx.window_id(), workspace, self.nav_history.clone(), cx);
-        self.add_item_view(item_view.boxed_clone(), cx);
-        item_view
     }
 
-    pub fn add_item_view(
-        &mut self,
-        mut item_view: Box<dyn ItemViewHandle>,
-        cx: &mut ViewContext<Self>,
+    pub(crate) fn add_item(
+        workspace: &mut Workspace,
+        pane: ViewHandle<Pane>,
+        item: Box<dyn ItemHandle>,
+        local: bool,
+        cx: &mut ViewContext<Workspace>,
     ) {
-        item_view.added_to_pane(cx);
-        let item_idx = cmp::min(self.active_item_index + 1, self.item_views.len());
-        self.item_views
-            .insert(item_idx, (item_view.item(cx).id(), item_view));
-        self.activate_item(item_idx, cx);
-        cx.notify();
+        // Prevent adding the same item to the pane more than once.
+        if let Some(item_ix) = pane.read(cx).items.iter().position(|i| i.id() == item.id()) {
+            pane.update(cx, |pane, cx| pane.activate_item(item_ix, local, cx));
+            return;
+        }
+
+        item.set_nav_history(pane.read(cx).nav_history.clone(), cx);
+        item.added_to_pane(workspace, pane.clone(), cx);
+        pane.update(cx, |pane, cx| {
+            let item_idx = cmp::min(pane.active_item_index + 1, pane.items.len());
+            pane.items.insert(item_idx, item);
+            pane.activate_item(item_idx, local, cx);
+            cx.notify();
+        });
+    }
+
+    pub fn items(&self) -> impl Iterator<Item = &Box<dyn ItemHandle>> {
+        self.items.iter()
     }
 
-    pub fn contains_item(&self, item: &dyn ItemHandle) -> bool {
-        let item_id = item.id();
-        self.item_views
+    pub fn items_of_type<'a, T: View>(&'a self) -> impl 'a + Iterator<Item = ViewHandle<T>> {
+        self.items
             .iter()
-            .any(|(existing_item_id, _)| *existing_item_id == item_id)
+            .filter_map(|item| item.to_any().downcast())
     }
 
-    pub fn item_views(&self) -> impl Iterator<Item = &Box<dyn ItemViewHandle>> {
-        self.item_views.iter().map(|(_, view)| view)
+    pub fn active_item(&self) -> Option<Box<dyn ItemHandle>> {
+        self.items.get(self.active_item_index).cloned()
     }
 
-    pub fn active_item(&self) -> Option<Box<dyn ItemViewHandle>> {
-        self.item_views
-            .get(self.active_item_index)
-            .map(|(_, view)| view.clone())
+    pub fn project_entry_id_for_item(
+        &self,
+        item: &dyn ItemHandle,
+        cx: &AppContext,
+    ) -> Option<ProjectEntryId> {
+        self.items.iter().find_map(|existing| {
+            if existing.id() == item.id() {
+                existing.project_entry_id(cx)
+            } else {
+                None
+            }
+        })
     }
 
-    pub fn index_for_item_view(&self, item_view: &dyn ItemViewHandle) -> Option<usize> {
-        self.item_views
-            .iter()
-            .position(|(_, i)| i.id() == item_view.id())
+    pub fn item_for_entry(
+        &self,
+        entry_id: ProjectEntryId,
+        cx: &AppContext,
+    ) -> Option<Box<dyn ItemHandle>> {
+        self.items.iter().find_map(|item| {
+            if item.project_entry_id(cx) == Some(entry_id) {
+                Some(item.boxed_clone())
+            } else {
+                None
+            }
+        })
     }
 
     pub fn index_for_item(&self, item: &dyn ItemHandle) -> Option<usize> {
-        self.item_views.iter().position(|(id, _)| *id == item.id())
+        self.items.iter().position(|i| i.id() == item.id())
     }
 
-    pub fn activate_item(&mut self, index: usize, cx: &mut ViewContext<Self>) {
-        if index < self.item_views.len() {
+    pub fn activate_item(&mut self, index: usize, local: bool, cx: &mut ViewContext<Self>) {
+        if index < self.items.len() {
             let prev_active_item_ix = mem::replace(&mut self.active_item_index, index);
             if prev_active_item_ix != self.active_item_index
-                && prev_active_item_ix < self.item_views.len()
+                && prev_active_item_ix < self.items.len()
             {
-                self.item_views[prev_active_item_ix].1.deactivated(cx);
+                self.items[prev_active_item_ix].deactivated(cx);
+                cx.emit(Event::ActivateItem { local });
             }
             self.update_active_toolbar(cx);
-            self.focus_active_item(cx);
-            self.activate(cx);
+            if local {
+                self.focus_active_item(cx);
+                self.activate(cx);
+            }
             cx.notify();
         }
     }
@@ -363,31 +415,31 @@ impl Pane {
         let mut index = self.active_item_index;
         if index > 0 {
             index -= 1;
-        } else if self.item_views.len() > 0 {
-            index = self.item_views.len() - 1;
+        } else if self.items.len() > 0 {
+            index = self.items.len() - 1;
         }
-        self.activate_item(index, cx);
+        self.activate_item(index, true, cx);
     }
 
     pub fn activate_next_item(&mut self, cx: &mut ViewContext<Self>) {
         let mut index = self.active_item_index;
-        if index + 1 < self.item_views.len() {
+        if index + 1 < self.items.len() {
             index += 1;
         } else {
             index = 0;
         }
-        self.activate_item(index, cx);
+        self.activate_item(index, true, cx);
     }
 
     pub fn close_active_item(&mut self, cx: &mut ViewContext<Self>) {
-        if !self.item_views.is_empty() {
-            self.close_item(self.item_views[self.active_item_index].1.id(), cx)
+        if !self.items.is_empty() {
+            self.close_item(self.items[self.active_item_index].id(), cx)
         }
     }
 
     pub fn close_inactive_items(&mut self, cx: &mut ViewContext<Self>) {
-        if !self.item_views.is_empty() {
-            let active_item_id = self.item_views[self.active_item_index].1.id();
+        if !self.items.is_empty() {
+            let active_item_id = self.items[self.active_item_index].id();
             self.close_items(cx, |id| id != active_item_id);
         }
     }
@@ -403,10 +455,10 @@ impl Pane {
     ) {
         let mut item_ix = 0;
         let mut new_active_item_index = self.active_item_index;
-        self.item_views.retain(|(_, item_view)| {
-            if should_close(item_view.id()) {
+        self.items.retain(|item| {
+            if should_close(item.id()) {
                 if item_ix == self.active_item_index {
-                    item_view.deactivated(cx);
+                    item.deactivated(cx);
                 }
 
                 if item_ix < self.active_item_index {
@@ -414,10 +466,10 @@ impl Pane {
                 }
 
                 let mut nav_history = self.nav_history.borrow_mut();
-                if let Some(path) = item_view.project_path(cx) {
-                    nav_history.paths_by_item.insert(item_view.id(), path);
+                if let Some(path) = item.project_path(cx) {
+                    nav_history.paths_by_item.insert(item.id(), path);
                 } else {
-                    nav_history.paths_by_item.remove(&item_view.id());
+                    nav_history.paths_by_item.remove(&item.id());
                 }
 
                 item_ix += 1;
@@ -428,10 +480,10 @@ impl Pane {
             }
         });
 
-        if self.item_views.is_empty() {
+        if self.items.is_empty() {
             cx.emit(Event::Remove);
         } else {
-            self.active_item_index = cmp::min(new_active_item_index, self.item_views.len() - 1);
+            self.active_item_index = cmp::min(new_active_item_index, self.items.len() - 1);
             self.focus_active_item(cx);
             self.activate(cx);
         }
@@ -440,7 +492,7 @@ impl Pane {
         cx.notify();
     }
 
-    fn focus_active_item(&mut self, cx: &mut ViewContext<Self>) {
+    pub fn focus_active_item(&mut self, cx: &mut ViewContext<Self>) {
         if let Some(active_item) = self.active_item() {
             cx.focus(active_item);
         }
@@ -500,9 +552,9 @@ impl Pane {
     }
 
     fn update_active_toolbar(&mut self, cx: &mut ViewContext<Self>) {
-        let active_item = self.item_views.get(self.active_item_index);
+        let active_item = self.items.get(self.active_item_index);
         for (toolbar_type_id, toolbar) in &self.toolbars {
-            let visible = toolbar.active_item_changed(active_item.map(|i| i.1.clone()), cx);
+            let visible = toolbar.active_item_changed(active_item.cloned(), cx);
             if Some(*toolbar_type_id) == self.active_toolbar_type {
                 self.active_toolbar_visible = visible;
             }
@@ -510,12 +562,12 @@ impl Pane {
     }
 
     fn render_tabs(&self, cx: &mut RenderContext<Self>) -> ElementBox {
-        let theme = cx.app_state::<Settings>().theme.clone();
+        let theme = cx.global::<Settings>().theme.clone();
 
         enum Tabs {}
         let tabs = MouseEventHandler::new::<Tabs, _, _>(0, cx, |mouse_state, cx| {
             let mut row = Flex::row();
-            for (ix, (_, item_view)) in self.item_views.iter().enumerate() {
+            for (ix, item) in self.items.iter().enumerate() {
                 let is_active = ix == self.active_item_index;
 
                 row.add_child({
@@ -524,7 +576,7 @@ impl Pane {
                     } else {
                         theme.workspace.tab.clone()
                     };
-                    let title = item_view.tab_content(&tab_style, cx);
+                    let title = item.tab_content(&tab_style, cx);
 
                     let mut style = if is_active {
                         theme.workspace.active_tab.clone()
@@ -541,9 +593,9 @@ impl Pane {
                                 .with_child(
                                     Align::new({
                                         let diameter = 7.0;
-                                        let icon_color = if item_view.has_conflict(cx) {
+                                        let icon_color = if item.has_conflict(cx) {
                                             Some(style.icon_conflict)
-                                        } else if item_view.is_dirty(cx) {
+                                        } else if item.is_dirty(cx) {
                                             Some(style.icon_dirty)
                                         } else {
                                             None
@@ -587,7 +639,7 @@ impl Pane {
                                 .with_child(
                                     Align::new(
                                         ConstrainedBox::new(if mouse_state.hovered {
-                                            let item_id = item_view.id();
+                                            let item_id = item.id();
                                             enum TabCloseButton {}
                                             let icon = Svg::new("icons/x.svg");
                                             MouseEventHandler::new::<TabCloseButton, _, _>(
@@ -691,7 +743,7 @@ impl View for Pane {
 impl<T: Toolbar> ToolbarHandle for ViewHandle<T> {
     fn active_item_changed(
         &self,
-        item: Option<Box<dyn ItemViewHandle>>,
+        item: Option<Box<dyn ItemHandle>>,
         cx: &mut MutableAppContext,
     ) -> bool {
         self.update(cx, |this, cx| this.active_item_changed(item, cx))
@@ -707,10 +759,10 @@ impl<T: Toolbar> ToolbarHandle for ViewHandle<T> {
 }
 
 impl ItemNavHistory {
-    pub fn new<T: ItemView>(history: Rc<RefCell<NavHistory>>, item_view: &ViewHandle<T>) -> Self {
+    pub fn new<T: Item>(history: Rc<RefCell<NavHistory>>, item: &ViewHandle<T>) -> Self {
         Self {
             history,
-            item_view: Rc::new(item_view.downgrade()),
+            item: Rc::new(item.downgrade()),
         }
     }
 
@@ -719,7 +771,7 @@ impl ItemNavHistory {
     }
 
     pub fn push<D: 'static + Any>(&self, data: Option<D>) {
-        self.history.borrow_mut().push(data, self.item_view.clone());
+        self.history.borrow_mut().push(data, self.item.clone());
     }
 }
 
@@ -752,11 +804,7 @@ impl NavHistory {
         self.mode = mode;
     }
 
-    pub fn push<D: 'static + Any>(
-        &mut self,
-        data: Option<D>,
-        item_view: Rc<dyn WeakItemViewHandle>,
-    ) {
+    pub fn push<D: 'static + Any>(&mut self, data: Option<D>, item: Rc<dyn WeakItemHandle>) {
         match self.mode {
             NavigationMode::Disabled => {}
             NavigationMode::Normal => {
@@ -764,7 +812,7 @@ impl NavHistory {
                     self.backward_stack.pop_front();
                 }
                 self.backward_stack.push_back(NavigationEntry {
-                    item_view,
+                    item,
                     data: data.map(|data| Box::new(data) as Box<dyn Any>),
                 });
                 self.forward_stack.clear();
@@ -774,7 +822,7 @@ impl NavHistory {
                     self.forward_stack.pop_front();
                 }
                 self.forward_stack.push_back(NavigationEntry {
-                    item_view,
+                    item,
                     data: data.map(|data| Box::new(data) as Box<dyn Any>),
                 });
             }
@@ -783,7 +831,7 @@ impl NavHistory {
                     self.backward_stack.pop_front();
                 }
                 self.backward_stack.push_back(NavigationEntry {
-                    item_view,
+                    item,
                     data: data.map(|data| Box::new(data) as Box<dyn Any>),
                 });
             }

crates/workspace/src/pane_group.rs 🔗

@@ -1,9 +1,11 @@
+use crate::{FollowerStatesByLeader, Pane};
 use anyhow::{anyhow, Result};
-use gpui::{elements::*, Axis, ViewHandle};
+use client::PeerId;
+use collections::HashMap;
+use gpui::{elements::*, Axis, Border, ViewHandle};
+use project::Collaborator;
 use theme::Theme;
 
-use crate::Pane;
-
 #[derive(Clone, Debug, Eq, PartialEq)]
 pub struct PaneGroup {
     root: Member,
@@ -47,8 +49,19 @@ impl PaneGroup {
         }
     }
 
-    pub fn render<'a>(&self, theme: &Theme) -> ElementBox {
-        self.root.render(theme)
+    pub(crate) fn render<'a>(
+        &self,
+        theme: &Theme,
+        follower_states: &FollowerStatesByLeader,
+        collaborators: &HashMap<PeerId, Collaborator>,
+    ) -> ElementBox {
+        self.root.render(theme, follower_states, collaborators)
+    }
+
+    pub(crate) fn panes(&self) -> Vec<&ViewHandle<Pane>> {
+        let mut panes = Vec::new();
+        self.root.collect_panes(&mut panes);
+        panes
     }
 }
 
@@ -80,10 +93,50 @@ impl Member {
         Member::Axis(PaneAxis { axis, members })
     }
 
-    pub fn render(&self, theme: &Theme) -> ElementBox {
+    pub fn render(
+        &self,
+        theme: &Theme,
+        follower_states: &FollowerStatesByLeader,
+        collaborators: &HashMap<PeerId, Collaborator>,
+    ) -> ElementBox {
         match self {
-            Member::Pane(pane) => ChildView::new(pane).boxed(),
-            Member::Axis(axis) => axis.render(theme),
+            Member::Pane(pane) => {
+                let mut border = Border::default();
+                let leader = follower_states
+                    .iter()
+                    .find_map(|(leader_id, follower_states)| {
+                        if follower_states.contains_key(pane) {
+                            Some(leader_id)
+                        } else {
+                            None
+                        }
+                    })
+                    .and_then(|leader_id| collaborators.get(leader_id));
+                if let Some(leader) = leader {
+                    let leader_color = theme
+                        .editor
+                        .replica_selection_style(leader.replica_id)
+                        .cursor;
+                    border = Border::all(theme.workspace.leader_border_width, leader_color);
+                    border
+                        .color
+                        .fade_out(1. - theme.workspace.leader_border_opacity);
+                    border.overlay = true;
+                }
+                ChildView::new(pane).contained().with_border(border).boxed()
+            }
+            Member::Axis(axis) => axis.render(theme, follower_states, collaborators),
+        }
+    }
+
+    fn collect_panes<'a>(&'a self, panes: &mut Vec<&'a ViewHandle<Pane>>) {
+        match self {
+            Member::Axis(axis) => {
+                for member in &axis.members {
+                    member.collect_panes(panes);
+                }
+            }
+            Member::Pane(pane) => panes.push(pane),
         }
     }
 }
@@ -172,11 +225,16 @@ impl PaneAxis {
         }
     }
 
-    fn render<'a>(&self, theme: &Theme) -> ElementBox {
+    fn render(
+        &self,
+        theme: &Theme,
+        follower_state: &FollowerStatesByLeader,
+        collaborators: &HashMap<PeerId, Collaborator>,
+    ) -> ElementBox {
         let last_member_ix = self.members.len() - 1;
         Flex::new(self.axis)
             .with_children(self.members.iter().enumerate().map(|(ix, member)| {
-                let mut member = member.render(theme);
+                let mut member = member.render(theme, follower_state, collaborators);
                 if ix < last_member_ix {
                     let mut border = theme.workspace.pane_divider;
                     border.left = false;

crates/workspace/src/settings.rs 🔗

@@ -17,6 +17,7 @@ use util::ResultExt;
 pub struct Settings {
     pub buffer_font_family: FamilyId,
     pub buffer_font_size: f32,
+    pub vim_mode: bool,
     pub tab_size: usize,
     pub soft_wrap: SoftWrap,
     pub preferred_line_length: u32,
@@ -48,6 +49,8 @@ struct SettingsFileContent {
     buffer_font_family: Option<String>,
     #[serde(default)]
     buffer_font_size: Option<f32>,
+    #[serde(default)]
+    vim_mode: Option<bool>,
     #[serde(flatten)]
     editor: LanguageOverride,
     #[serde(default)]
@@ -130,6 +133,7 @@ impl Settings {
         Ok(Self {
             buffer_font_family: font_cache.load_family(&[buffer_font_family])?,
             buffer_font_size: 15.,
+            vim_mode: false,
             tab_size: 4,
             soft_wrap: SoftWrap::None,
             preferred_line_length: 80,
@@ -174,6 +178,7 @@ impl Settings {
         Settings {
             buffer_font_family: cx.font_cache().load_family(&["Monaco"]).unwrap(),
             buffer_font_size: 14.,
+            vim_mode: false,
             tab_size: 4,
             soft_wrap: SoftWrap::None,
             preferred_line_length: 80,
@@ -200,6 +205,7 @@ impl Settings {
         }
 
         merge(&mut self.buffer_font_size, data.buffer_font_size);
+        merge(&mut self.vim_mode, data.vim_mode);
         merge(&mut self.soft_wrap, data.editor.soft_wrap);
         merge(&mut self.tab_size, data.editor.tab_size);
         merge(

crates/workspace/src/status_bar.rs 🔗

@@ -1,4 +1,4 @@
-use crate::{ItemViewHandle, Pane, Settings};
+use crate::{ItemHandle, Pane, Settings};
 use gpui::{
     elements::*, AnyViewHandle, ElementBox, Entity, MutableAppContext, RenderContext, Subscription,
     View, ViewContext, ViewHandle,
@@ -7,7 +7,7 @@ use gpui::{
 pub trait StatusItemView: View {
     fn set_active_pane_item(
         &mut self,
-        active_pane_item: Option<&dyn crate::ItemViewHandle>,
+        active_pane_item: Option<&dyn crate::ItemHandle>,
         cx: &mut ViewContext<Self>,
     );
 }
@@ -16,7 +16,7 @@ trait StatusItemViewHandle {
     fn to_any(&self) -> AnyViewHandle;
     fn set_active_pane_item(
         &self,
-        active_pane_item: Option<&dyn ItemViewHandle>,
+        active_pane_item: Option<&dyn ItemHandle>,
         cx: &mut MutableAppContext,
     );
 }
@@ -38,7 +38,7 @@ impl View for StatusBar {
     }
 
     fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
-        let theme = &cx.app_state::<Settings>().theme.workspace.status_bar;
+        let theme = &cx.global::<Settings>().theme.workspace.status_bar;
         Flex::row()
             .with_children(self.left_items.iter().map(|i| {
                 ChildView::new(i.as_ref())
@@ -114,7 +114,7 @@ impl<T: StatusItemView> StatusItemViewHandle for ViewHandle<T> {
 
     fn set_active_pane_item(
         &self,
-        active_pane_item: Option<&dyn ItemViewHandle>,
+        active_pane_item: Option<&dyn ItemHandle>,
         cx: &mut MutableAppContext,
     ) {
         self.update(cx, |this, cx| {

crates/workspace/src/workspace.rs 🔗

@@ -6,10 +6,12 @@ pub mod settings;
 pub mod sidebar;
 mod status_bar;
 
-use anyhow::{anyhow, Result};
-use client::{Authenticate, ChannelList, Client, User, UserStore};
+use anyhow::{anyhow, Context, Result};
+use client::{
+    proto, Authenticate, ChannelList, Client, PeerId, Subscription, TypedEnvelope, User, UserStore,
+};
 use clock::ReplicaId;
-use collections::BTreeMap;
+use collections::{hash_map, HashMap, HashSet};
 use gpui::{
     action,
     color::Color,
@@ -18,16 +20,16 @@ use gpui::{
     json::{self, to_string_pretty, ToJson},
     keymap::Binding,
     platform::{CursorStyle, WindowOptions},
-    AnyModelHandle, AnyViewHandle, AppContext, ClipboardItem, Entity, ImageData, ModelContext,
-    ModelHandle, MutableAppContext, PathPromptOptions, PromptLevel, RenderContext, Task, View,
-    ViewContext, ViewHandle, WeakModelHandle, WeakViewHandle,
+    AnyModelHandle, AnyViewHandle, AppContext, AsyncAppContext, Border, ClipboardItem, Entity,
+    ImageData, ModelHandle, MutableAppContext, PathPromptOptions, PromptLevel, RenderContext, Task,
+    View, ViewContext, ViewHandle, WeakViewHandle,
 };
 use language::LanguageRegistry;
 use log::error;
 pub use pane::*;
 pub use pane_group::*;
 use postage::prelude::Stream;
-use project::{fs, Fs, Project, ProjectPath, Worktree};
+use project::{fs, Fs, Project, ProjectEntryId, ProjectPath, Worktree};
 pub use settings::Settings;
 use sidebar::{Side, Sidebar, SidebarItemId, ToggleSidebarItem, ToggleSidebarItemFocus};
 use status_bar::StatusBar;
@@ -35,24 +37,51 @@ pub use status_bar::StatusItemView;
 use std::{
     any::{Any, TypeId},
     cell::RefCell,
-    cmp::Reverse,
+    fmt,
     future::Future,
-    hash::{Hash, Hasher},
     path::{Path, PathBuf},
     rc::Rc,
-    sync::Arc,
+    sync::{
+        atomic::{AtomicBool, Ordering::SeqCst},
+        Arc,
+    },
 };
 use theme::{Theme, ThemeRegistry};
+use util::ResultExt;
+
+type ProjectItemBuilders = HashMap<
+    TypeId,
+    fn(usize, ModelHandle<Project>, AnyModelHandle, &mut MutableAppContext) -> Box<dyn ItemHandle>,
+>;
+
+type FollowableItemBuilder = fn(
+    ViewHandle<Pane>,
+    ModelHandle<Project>,
+    &mut Option<proto::view::Variant>,
+    &mut MutableAppContext,
+) -> Option<Task<Result<Box<dyn FollowableItemHandle>>>>;
+type FollowableItemBuilders = HashMap<
+    TypeId,
+    (
+        FollowableItemBuilder,
+        fn(AnyViewHandle) -> Box<dyn FollowableItemHandle>,
+    ),
+>;
 
 action!(Open, Arc<AppState>);
 action!(OpenNew, Arc<AppState>);
 action!(OpenPaths, OpenParams);
 action!(ToggleShare);
+action!(ToggleFollow, PeerId);
+action!(FollowNextCollaborator);
+action!(Unfollow);
 action!(JoinProject, JoinProjectParams);
 action!(Save);
 action!(DebugElements);
+action!(ActivatePreviousPane);
+action!(ActivateNextPane);
 
-pub fn init(cx: &mut MutableAppContext) {
+pub fn init(client: &Arc<Client>, cx: &mut MutableAppContext) {
     pane::init(cx);
     menu::init(cx);
 
@@ -68,6 +97,14 @@ pub fn init(cx: &mut MutableAppContext) {
     });
 
     cx.add_action(Workspace::toggle_share);
+    cx.add_async_action(Workspace::toggle_follow);
+    cx.add_async_action(Workspace::follow_next_collaborator);
+    cx.add_action(
+        |workspace: &mut Workspace, _: &Unfollow, cx: &mut ViewContext<Workspace>| {
+            let pane = workspace.active_pane().clone();
+            workspace.unfollow(&pane, cx);
+        },
+    );
     cx.add_action(
         |workspace: &mut Workspace, _: &Save, cx: &mut ViewContext<Workspace>| {
             workspace.save_active_item(cx).detach_and_log_err(cx);
@@ -76,9 +113,18 @@ pub fn init(cx: &mut MutableAppContext) {
     cx.add_action(Workspace::debug_elements);
     cx.add_action(Workspace::toggle_sidebar_item);
     cx.add_action(Workspace::toggle_sidebar_item_focus);
+    cx.add_action(|workspace: &mut Workspace, _: &ActivatePreviousPane, cx| {
+        workspace.activate_previous_pane(cx)
+    });
+    cx.add_action(|workspace: &mut Workspace, _: &ActivateNextPane, cx| {
+        workspace.activate_next_pane(cx)
+    });
     cx.add_bindings(vec![
+        Binding::new("ctrl-alt-cmd-f", FollowNextCollaborator, None),
         Binding::new("cmd-s", Save, None),
         Binding::new("cmd-alt-i", DebugElements, None),
+        Binding::new("cmd-k cmd-left", ActivatePreviousPane, None),
+        Binding::new("cmd-k cmd-right", ActivateNextPane, None),
         Binding::new(
             "cmd-shift-!",
             ToggleSidebarItem(SidebarItemId {
@@ -96,6 +142,36 @@ pub fn init(cx: &mut MutableAppContext) {
             None,
         ),
     ]);
+
+    client.add_view_request_handler(Workspace::handle_follow);
+    client.add_view_message_handler(Workspace::handle_unfollow);
+    client.add_view_message_handler(Workspace::handle_update_followers);
+}
+
+pub fn register_project_item<I: ProjectItem>(cx: &mut MutableAppContext) {
+    cx.update_default_global(|builders: &mut ProjectItemBuilders, _| {
+        builders.insert(TypeId::of::<I::Item>(), |window_id, project, model, cx| {
+            let item = model.downcast::<I::Item>().unwrap();
+            Box::new(cx.add_view(window_id, |cx| I::for_project_item(project, item, cx)))
+        });
+    });
+}
+
+pub fn register_followable_item<I: FollowableItem>(cx: &mut MutableAppContext) {
+    cx.update_default_global(|builders: &mut FollowableItemBuilders, _| {
+        builders.insert(
+            TypeId::of::<I>(),
+            (
+                |pane, project, state, cx| {
+                    I::from_state_proto(pane, project, state, cx).map(|task| {
+                        cx.foreground()
+                            .spawn(async move { Ok(Box::new(task.await?) as Box<_>) })
+                    })
+                },
+                |this| Box::new(this.downcast::<I>().unwrap()),
+            ),
+        );
+    });
 }
 
 pub struct AppState {
@@ -105,7 +181,6 @@ pub struct AppState {
     pub user_store: ModelHandle<client::UserStore>,
     pub fs: Arc<dyn fs::Fs>,
     pub channel_list: ModelHandle<client::ChannelList>,
-    pub path_openers: Arc<[Box<dyn PathOpener>]>,
     pub build_window_options: &'static dyn Fn() -> WindowOptions<'static>,
     pub build_workspace: &'static dyn Fn(
         ModelHandle<Project>,
@@ -126,35 +201,16 @@ pub struct JoinProjectParams {
     pub app_state: Arc<AppState>,
 }
 
-pub trait PathOpener {
-    fn open(
-        &self,
-        project: &mut Project,
-        path: ProjectPath,
-        cx: &mut ModelContext<Project>,
-    ) -> Option<Task<Result<Box<dyn ItemHandle>>>>;
-}
-
-pub trait Item: Entity + Sized {
-    type View: ItemView;
-
-    fn build_view(
-        handle: ModelHandle<Self>,
-        workspace: &Workspace,
-        nav_history: ItemNavHistory,
-        cx: &mut ViewContext<Self::View>,
-    ) -> Self::View;
-
-    fn project_path(&self) -> Option<ProjectPath>;
-}
-
-pub trait ItemView: View {
+pub trait Item: View {
     fn deactivated(&mut self, _: &mut ViewContext<Self>) {}
-    fn navigate(&mut self, _: Box<dyn Any>, _: &mut ViewContext<Self>) {}
-    fn item(&self, cx: &AppContext) -> Box<dyn ItemHandle>;
+    fn navigate(&mut self, _: Box<dyn Any>, _: &mut ViewContext<Self>) -> bool {
+        false
+    }
     fn tab_content(&self, style: &theme::Tab, cx: &AppContext) -> ElementBox;
     fn project_path(&self, cx: &AppContext) -> Option<ProjectPath>;
-    fn clone_on_split(&self, _: ItemNavHistory, _: &mut ViewContext<Self>) -> Option<Self>
+    fn project_entry_id(&self, cx: &AppContext) -> Option<ProjectEntryId>;
+    fn set_nav_history(&mut self, _: ItemNavHistory, _: &mut ViewContext<Self>);
+    fn clone_on_split(&self, _: &mut ViewContext<Self>) -> Option<Self>
     where
         Self: Sized,
     {
@@ -202,39 +258,113 @@ pub trait ItemView: View {
     }
 }
 
-pub trait ItemHandle: Send + Sync {
-    fn id(&self) -> usize;
-    fn add_view(
+pub trait ProjectItem: Item {
+    type Item: project::Item;
+
+    fn for_project_item(
+        project: ModelHandle<Project>,
+        item: ModelHandle<Self::Item>,
+        cx: &mut ViewContext<Self>,
+    ) -> Self;
+}
+
+pub trait FollowableItem: Item {
+    fn to_state_proto(&self, cx: &AppContext) -> Option<proto::view::Variant>;
+    fn from_state_proto(
+        pane: ViewHandle<Pane>,
+        project: ModelHandle<Project>,
+        state: &mut Option<proto::view::Variant>,
+        cx: &mut MutableAppContext,
+    ) -> Option<Task<Result<ViewHandle<Self>>>>;
+    fn add_event_to_update_proto(
         &self,
-        window_id: usize,
-        workspace: &Workspace,
-        nav_history: Rc<RefCell<NavHistory>>,
+        event: &Self::Event,
+        update: &mut Option<proto::update_view::Variant>,
+        cx: &AppContext,
+    ) -> bool;
+    fn apply_update_proto(
+        &mut self,
+        message: proto::update_view::Variant,
+        cx: &mut ViewContext<Self>,
+    ) -> Result<()>;
+
+    fn set_leader_replica_id(&mut self, leader_replica_id: Option<u16>, cx: &mut ViewContext<Self>);
+    fn should_unfollow_on_event(event: &Self::Event, cx: &AppContext) -> bool;
+}
+
+pub trait FollowableItemHandle: ItemHandle {
+    fn set_leader_replica_id(&self, leader_replica_id: Option<u16>, cx: &mut MutableAppContext);
+    fn to_state_proto(&self, cx: &AppContext) -> Option<proto::view::Variant>;
+    fn add_event_to_update_proto(
+        &self,
+        event: &dyn Any,
+        update: &mut Option<proto::update_view::Variant>,
+        cx: &AppContext,
+    ) -> bool;
+    fn apply_update_proto(
+        &self,
+        message: proto::update_view::Variant,
         cx: &mut MutableAppContext,
-    ) -> Box<dyn ItemViewHandle>;
-    fn boxed_clone(&self) -> Box<dyn ItemHandle>;
-    fn downgrade(&self) -> Box<dyn WeakItemHandle>;
-    fn to_any(&self) -> AnyModelHandle;
-    fn project_path(&self, cx: &AppContext) -> Option<ProjectPath>;
+    ) -> Result<()>;
+    fn should_unfollow_on_event(&self, event: &dyn Any, cx: &AppContext) -> bool;
 }
 
-pub trait WeakItemHandle {
-    fn id(&self) -> usize;
-    fn upgrade(&self, cx: &AppContext) -> Option<Box<dyn ItemHandle>>;
+impl<T: FollowableItem> FollowableItemHandle for ViewHandle<T> {
+    fn set_leader_replica_id(&self, leader_replica_id: Option<u16>, cx: &mut MutableAppContext) {
+        self.update(cx, |this, cx| {
+            this.set_leader_replica_id(leader_replica_id, cx)
+        })
+    }
+
+    fn to_state_proto(&self, cx: &AppContext) -> Option<proto::view::Variant> {
+        self.read(cx).to_state_proto(cx)
+    }
+
+    fn add_event_to_update_proto(
+        &self,
+        event: &dyn Any,
+        update: &mut Option<proto::update_view::Variant>,
+        cx: &AppContext,
+    ) -> bool {
+        if let Some(event) = event.downcast_ref() {
+            self.read(cx).add_event_to_update_proto(event, update, cx)
+        } else {
+            false
+        }
+    }
+
+    fn apply_update_proto(
+        &self,
+        message: proto::update_view::Variant,
+        cx: &mut MutableAppContext,
+    ) -> Result<()> {
+        self.update(cx, |this, cx| this.apply_update_proto(message, cx))
+    }
+
+    fn should_unfollow_on_event(&self, event: &dyn Any, cx: &AppContext) -> bool {
+        if let Some(event) = event.downcast_ref() {
+            T::should_unfollow_on_event(event, cx)
+        } else {
+            false
+        }
+    }
 }
 
-pub trait ItemViewHandle: 'static {
-    fn item(&self, cx: &AppContext) -> Box<dyn ItemHandle>;
+pub trait ItemHandle: 'static + fmt::Debug {
     fn tab_content(&self, style: &theme::Tab, cx: &AppContext) -> ElementBox;
     fn project_path(&self, cx: &AppContext) -> Option<ProjectPath>;
-    fn boxed_clone(&self) -> Box<dyn ItemViewHandle>;
-    fn clone_on_split(
+    fn project_entry_id(&self, cx: &AppContext) -> Option<ProjectEntryId>;
+    fn boxed_clone(&self) -> Box<dyn ItemHandle>;
+    fn set_nav_history(&self, nav_history: Rc<RefCell<NavHistory>>, cx: &mut MutableAppContext);
+    fn clone_on_split(&self, cx: &mut MutableAppContext) -> Option<Box<dyn ItemHandle>>;
+    fn added_to_pane(
         &self,
-        nav_history: Rc<RefCell<NavHistory>>,
-        cx: &mut MutableAppContext,
-    ) -> Option<Box<dyn ItemViewHandle>>;
-    fn added_to_pane(&mut self, cx: &mut ViewContext<Pane>);
+        workspace: &mut Workspace,
+        pane: ViewHandle<Pane>,
+        cx: &mut ViewContext<Workspace>,
+    );
     fn deactivated(&self, cx: &mut MutableAppContext);
-    fn navigate(&self, data: Box<dyn Any>, cx: &mut MutableAppContext);
+    fn navigate(&self, data: Box<dyn Any>, cx: &mut MutableAppContext) -> bool;
     fn id(&self) -> usize;
     fn to_any(&self) -> AnyViewHandle;
     fn is_dirty(&self, cx: &AppContext) -> bool;
@@ -249,105 +379,15 @@ pub trait ItemViewHandle: 'static {
         cx: &mut MutableAppContext,
     ) -> Task<Result<()>>;
     fn act_as_type(&self, type_id: TypeId, cx: &AppContext) -> Option<AnyViewHandle>;
+    fn to_followable_item_handle(&self, cx: &AppContext) -> Option<Box<dyn FollowableItemHandle>>;
 }
 
-pub trait WeakItemViewHandle {
+pub trait WeakItemHandle {
     fn id(&self) -> usize;
-    fn upgrade(&self, cx: &AppContext) -> Option<Box<dyn ItemViewHandle>>;
-}
-
-impl<T: Item> ItemHandle for ModelHandle<T> {
-    fn id(&self) -> usize {
-        self.id()
-    }
-
-    fn add_view(
-        &self,
-        window_id: usize,
-        workspace: &Workspace,
-        nav_history: Rc<RefCell<NavHistory>>,
-        cx: &mut MutableAppContext,
-    ) -> Box<dyn ItemViewHandle> {
-        Box::new(cx.add_view(window_id, |cx| {
-            let nav_history = ItemNavHistory::new(nav_history, &cx.handle());
-            T::build_view(self.clone(), workspace, nav_history, cx)
-        }))
-    }
-
-    fn boxed_clone(&self) -> Box<dyn ItemHandle> {
-        Box::new(self.clone())
-    }
-
-    fn downgrade(&self) -> Box<dyn WeakItemHandle> {
-        Box::new(self.downgrade())
-    }
-
-    fn to_any(&self) -> AnyModelHandle {
-        self.clone().into()
-    }
-
-    fn project_path(&self, cx: &AppContext) -> Option<ProjectPath> {
-        self.read(cx).project_path()
-    }
-}
-
-impl ItemHandle for Box<dyn ItemHandle> {
-    fn id(&self) -> usize {
-        ItemHandle::id(self.as_ref())
-    }
-
-    fn add_view(
-        &self,
-        window_id: usize,
-        workspace: &Workspace,
-        nav_history: Rc<RefCell<NavHistory>>,
-        cx: &mut MutableAppContext,
-    ) -> Box<dyn ItemViewHandle> {
-        ItemHandle::add_view(self.as_ref(), window_id, workspace, nav_history, cx)
-    }
-
-    fn boxed_clone(&self) -> Box<dyn ItemHandle> {
-        self.as_ref().boxed_clone()
-    }
-
-    fn downgrade(&self) -> Box<dyn WeakItemHandle> {
-        self.as_ref().downgrade()
-    }
-
-    fn to_any(&self) -> AnyModelHandle {
-        self.as_ref().to_any()
-    }
-
-    fn project_path(&self, cx: &AppContext) -> Option<ProjectPath> {
-        self.as_ref().project_path(cx)
-    }
-}
-
-impl<T: Item> WeakItemHandle for WeakModelHandle<T> {
-    fn id(&self) -> usize {
-        WeakModelHandle::id(self)
-    }
-
-    fn upgrade(&self, cx: &AppContext) -> Option<Box<dyn ItemHandle>> {
-        WeakModelHandle::<T>::upgrade(self, cx).map(|i| Box::new(i) as Box<dyn ItemHandle>)
-    }
-}
-
-impl Hash for Box<dyn WeakItemHandle> {
-    fn hash<H: Hasher>(&self, state: &mut H) {
-        self.id().hash(state);
-    }
-}
-
-impl PartialEq for Box<dyn WeakItemHandle> {
-    fn eq(&self, other: &Self) -> bool {
-        self.id() == other.id()
-    }
+    fn upgrade(&self, cx: &AppContext) -> Option<Box<dyn ItemHandle>>;
 }
 
-impl Eq for Box<dyn WeakItemHandle> {}
-
-impl dyn ItemViewHandle {
+impl dyn ItemHandle {
     pub fn downcast<T: View>(&self) -> Option<ViewHandle<T>> {
         self.to_any().downcast()
     }
@@ -358,11 +398,7 @@ impl dyn ItemViewHandle {
     }
 }
 
-impl<T: ItemView> ItemViewHandle for ViewHandle<T> {
-    fn item(&self, cx: &AppContext) -> Box<dyn ItemHandle> {
-        self.read(cx).item(cx)
-    }
-
+impl<T: Item> ItemHandle for ViewHandle<T> {
     fn tab_content(&self, style: &theme::Tab, cx: &AppContext) -> ElementBox {
         self.read(cx).tab_content(style, cx)
     }
@@ -371,37 +407,102 @@ impl<T: ItemView> ItemViewHandle for ViewHandle<T> {
         self.read(cx).project_path(cx)
     }
 
-    fn boxed_clone(&self) -> Box<dyn ItemViewHandle> {
+    fn project_entry_id(&self, cx: &AppContext) -> Option<ProjectEntryId> {
+        self.read(cx).project_entry_id(cx)
+    }
+
+    fn boxed_clone(&self) -> Box<dyn ItemHandle> {
         Box::new(self.clone())
     }
 
-    fn clone_on_split(
-        &self,
-        nav_history: Rc<RefCell<NavHistory>>,
-        cx: &mut MutableAppContext,
-    ) -> Option<Box<dyn ItemViewHandle>> {
+    fn clone_on_split(&self, cx: &mut MutableAppContext) -> Option<Box<dyn ItemHandle>> {
         self.update(cx, |item, cx| {
-            cx.add_option_view(|cx| {
-                item.clone_on_split(ItemNavHistory::new(nav_history, &cx.handle()), cx)
-            })
+            cx.add_option_view(|cx| item.clone_on_split(cx))
         })
-        .map(|handle| Box::new(handle) as Box<dyn ItemViewHandle>)
+        .map(|handle| Box::new(handle) as Box<dyn ItemHandle>)
     }
 
-    fn added_to_pane(&mut self, cx: &mut ViewContext<Pane>) {
-        cx.subscribe(self, |pane, item, event, cx| {
+    fn set_nav_history(&self, nav_history: Rc<RefCell<NavHistory>>, cx: &mut MutableAppContext) {
+        self.update(cx, |item, cx| {
+            item.set_nav_history(ItemNavHistory::new(nav_history, &cx.handle()), cx);
+        })
+    }
+
+    fn added_to_pane(
+        &self,
+        workspace: &mut Workspace,
+        pane: ViewHandle<Pane>,
+        cx: &mut ViewContext<Workspace>,
+    ) {
+        if let Some(followed_item) = self.to_followable_item_handle(cx) {
+            if let Some(message) = followed_item.to_state_proto(cx) {
+                workspace.update_followers(
+                    proto::update_followers::Variant::CreateView(proto::View {
+                        id: followed_item.id() as u64,
+                        variant: Some(message),
+                        leader_id: workspace.leader_for_pane(&pane).map(|id| id.0),
+                    }),
+                    cx,
+                );
+            }
+        }
+
+        let pending_update = Rc::new(RefCell::new(None));
+        let pending_update_scheduled = Rc::new(AtomicBool::new(false));
+        let pane = pane.downgrade();
+        cx.subscribe(self, move |workspace, item, event, cx| {
+            let pane = if let Some(pane) = pane.upgrade(cx) {
+                pane
+            } else {
+                log::error!("unexpected item event after pane was dropped");
+                return;
+            };
+
+            if let Some(item) = item.to_followable_item_handle(cx) {
+                let leader_id = workspace.leader_for_pane(&pane);
+
+                if leader_id.is_some() && item.should_unfollow_on_event(event, cx) {
+                    workspace.unfollow(&pane, cx);
+                }
+
+                if item.add_event_to_update_proto(event, &mut *pending_update.borrow_mut(), cx)
+                    && !pending_update_scheduled.load(SeqCst)
+                {
+                    pending_update_scheduled.store(true, SeqCst);
+                    cx.after_window_update({
+                        let pending_update = pending_update.clone();
+                        let pending_update_scheduled = pending_update_scheduled.clone();
+                        move |this, cx| {
+                            pending_update_scheduled.store(false, SeqCst);
+                            this.update_followers(
+                                proto::update_followers::Variant::UpdateView(proto::UpdateView {
+                                    id: item.id() as u64,
+                                    variant: pending_update.borrow_mut().take(),
+                                    leader_id: leader_id.map(|id| id.0),
+                                }),
+                                cx,
+                            );
+                        }
+                    });
+                }
+            }
+
             if T::should_close_item_on_event(event) {
-                pane.close_item(item.id(), cx);
+                pane.update(cx, |pane, cx| pane.close_item(item.id(), cx));
                 return;
             }
+
             if T::should_activate_item_on_event(event) {
-                if let Some(ix) = pane.index_for_item_view(&item) {
-                    pane.activate_item(ix, cx);
-                    pane.activate(cx);
-                }
+                pane.update(cx, |pane, cx| {
+                    if let Some(ix) = pane.index_for_item(&item) {
+                        pane.activate_item(ix, true, cx);
+                        pane.activate(cx);
+                    }
+                });
             }
+
             if T::should_update_tab_on_event(event) {
-                cx.notify()
+                pane.update(cx, |_, cx| cx.notify());
             }
         })
         .detach();
@@ -411,8 +512,8 @@ impl<T: ItemView> ItemViewHandle for ViewHandle<T> {
         self.update(cx, |this, cx| this.deactivated(cx));
     }
 
-    fn navigate(&self, data: Box<dyn Any>, cx: &mut MutableAppContext) {
-        self.update(cx, |this, cx| this.navigate(data, cx));
+    fn navigate(&self, data: Box<dyn Any>, cx: &mut MutableAppContext) -> bool {
+        self.update(cx, |this, cx| this.navigate(data, cx))
     }
 
     fn save(&self, project: ModelHandle<Project>, cx: &mut MutableAppContext) -> Task<Result<()>> {
@@ -455,17 +556,21 @@ impl<T: ItemView> ItemViewHandle for ViewHandle<T> {
     fn act_as_type(&self, type_id: TypeId, cx: &AppContext) -> Option<AnyViewHandle> {
         self.read(cx).act_as_type(type_id, self, cx)
     }
-}
 
-impl Into<AnyViewHandle> for Box<dyn ItemViewHandle> {
-    fn into(self) -> AnyViewHandle {
-        self.to_any()
+    fn to_followable_item_handle(&self, cx: &AppContext) -> Option<Box<dyn FollowableItemHandle>> {
+        if cx.has_global::<FollowableItemBuilders>() {
+            let builders = cx.global::<FollowableItemBuilders>();
+            let item = self.to_any();
+            Some(builders.get(&item.view_type())?.1(item))
+        } else {
+            None
+        }
     }
 }
 
-impl Clone for Box<dyn ItemViewHandle> {
-    fn clone(&self) -> Box<dyn ItemViewHandle> {
-        self.boxed_clone()
+impl Into<AnyViewHandle> for Box<dyn ItemHandle> {
+    fn into(self) -> AnyViewHandle {
+        self.to_any()
     }
 }
 
@@ -475,14 +580,13 @@ impl Clone for Box<dyn ItemHandle> {
     }
 }
 
-impl<T: ItemView> WeakItemViewHandle for WeakViewHandle<T> {
+impl<T: Item> WeakItemHandle for WeakViewHandle<T> {
     fn id(&self) -> usize {
         self.id()
     }
 
-    fn upgrade(&self, cx: &AppContext) -> Option<Box<dyn ItemViewHandle>> {
-        self.upgrade(cx)
-            .map(|v| Box::new(v) as Box<dyn ItemViewHandle>)
+    fn upgrade(&self, cx: &AppContext) -> Option<Box<dyn ItemHandle>> {
+        self.upgrade(cx).map(|v| Box::new(v) as Box<dyn ItemHandle>)
     }
 }
 
@@ -494,14 +598,13 @@ pub struct WorkspaceParams {
     pub languages: Arc<LanguageRegistry>,
     pub user_store: ModelHandle<UserStore>,
     pub channel_list: ModelHandle<ChannelList>,
-    pub path_openers: Arc<[Box<dyn PathOpener>]>,
 }
 
 impl WorkspaceParams {
     #[cfg(any(test, feature = "test-support"))]
     pub fn test(cx: &mut MutableAppContext) -> Self {
         let settings = Settings::test(cx);
-        cx.add_app_state(settings);
+        cx.set_global(settings);
 
         let fs = project::FakeFs::new(cx.background().clone());
         let languages = Arc::new(LanguageRegistry::test());
@@ -525,7 +628,6 @@ impl WorkspaceParams {
             fs,
             languages,
             user_store,
-            path_openers: Arc::from([]),
         }
     }
 
@@ -544,7 +646,6 @@ impl WorkspaceParams {
             languages: app_state.languages.clone(),
             user_store: app_state.user_store.clone(),
             channel_list: app_state.channel_list.clone(),
-            path_openers: app_state.path_openers.clone(),
         }
     }
 }
@@ -553,6 +654,7 @@ pub struct Workspace {
     weak_self: WeakViewHandle<Self>,
     client: Arc<Client>,
     user_store: ModelHandle<client::UserStore>,
+    remote_entity_subscription: Option<Subscription>,
     fs: Arc<dyn Fs>,
     modal: Option<AnyViewHandle>,
     center: PaneGroup,
@@ -562,11 +664,31 @@ pub struct Workspace {
     active_pane: ViewHandle<Pane>,
     status_bar: ViewHandle<StatusBar>,
     project: ModelHandle<Project>,
-    path_openers: Arc<[Box<dyn PathOpener>]>,
-    items: BTreeMap<Reverse<usize>, Box<dyn WeakItemHandle>>,
+    leader_state: LeaderState,
+    follower_states_by_leader: FollowerStatesByLeader,
+    last_leaders_by_pane: HashMap<WeakViewHandle<Pane>, PeerId>,
     _observe_current_user: Task<()>,
 }
 
+#[derive(Default)]
+struct LeaderState {
+    followers: HashSet<PeerId>,
+}
+
+type FollowerStatesByLeader = HashMap<PeerId, HashMap<ViewHandle<Pane>, FollowerState>>;
+
+#[derive(Default)]
+struct FollowerState {
+    active_view_id: Option<u64>,
+    items_by_leader_view_id: HashMap<u64, FollowerItem>,
+}
+
+#[derive(Debug)]
+enum FollowerItem {
+    Loading(Vec<proto::update_view::Variant>),
+    Loaded(Box<dyn FollowableItemHandle>),
+}
+
 impl Workspace {
     pub fn new(params: &WorkspaceParams, cx: &mut ViewContext<Self>) -> Self {
         cx.observe(&params.project, |_, project, cx| {
@@ -577,6 +699,23 @@ impl Workspace {
         })
         .detach();
 
+        cx.subscribe(&params.project, move |this, project, event, cx| {
+            match event {
+                project::Event::RemoteIdChanged(remote_id) => {
+                    this.project_remote_id_changed(*remote_id, cx);
+                }
+                project::Event::CollaboratorLeft(peer_id) => {
+                    this.collaborator_left(*peer_id, cx);
+                }
+                _ => {}
+            }
+            if project.read(cx).is_read_only() {
+                cx.blur();
+            }
+            cx.notify()
+        })
+        .detach();
+
         let pane = cx.add_view(|_| Pane::new());
         let pane_id = pane.id();
         cx.observe(&pane, move |me, _, cx| {
@@ -613,7 +752,7 @@ impl Workspace {
 
         cx.emit_global(WorkspaceCreated(weak_self.clone()));
 
-        Workspace {
+        let mut this = Workspace {
             modal: None,
             weak_self,
             center: PaneGroup::new(pane.clone()),
@@ -621,15 +760,19 @@ impl Workspace {
             active_pane: pane.clone(),
             status_bar,
             client: params.client.clone(),
+            remote_entity_subscription: None,
             user_store: params.user_store.clone(),
             fs: params.fs.clone(),
             left_sidebar: Sidebar::new(Side::Left),
             right_sidebar: Sidebar::new(Side::Right),
             project: params.project.clone(),
-            path_openers: params.path_openers.clone(),
-            items: Default::default(),
+            leader_state: Default::default(),
+            follower_states_by_leader: Default::default(),
+            last_leaders_by_pane: Default::default(),
             _observe_current_user,
-        }
+        };
+        this.project_remote_id_changed(this.project.read(cx).remote_id(), cx);
+        this
     }
 
     pub fn weak_handle(&self) -> WeakViewHandle<Self> {
@@ -690,7 +833,7 @@ impl Workspace {
         &mut self,
         abs_paths: &[PathBuf],
         cx: &mut ViewContext<Self>,
-    ) -> Task<Vec<Option<Result<Box<dyn ItemViewHandle>, Arc<anyhow::Error>>>>> {
+    ) -> Task<Vec<Option<Result<Box<dyn ItemHandle>, Arc<anyhow::Error>>>>> {
         let entries = abs_paths
             .iter()
             .cloned()
@@ -782,68 +925,27 @@ impl Workspace {
         }
     }
 
-    pub fn open_path(
-        &mut self,
-        path: ProjectPath,
-        cx: &mut ViewContext<Self>,
-    ) -> Task<Result<Box<dyn ItemViewHandle>, Arc<anyhow::Error>>> {
-        let load_task = self.load_path(path, cx);
-        let pane = self.active_pane().clone().downgrade();
-        cx.spawn(|this, mut cx| async move {
-            let item = load_task.await?;
-            this.update(&mut cx, |this, cx| {
-                let pane = pane
-                    .upgrade(cx)
-                    .ok_or_else(|| anyhow!("could not upgrade pane reference"))?;
-                Ok(this.open_item_in_pane(item, &pane, cx))
-            })
-        })
-    }
-
-    pub fn load_path(
-        &mut self,
-        path: ProjectPath,
-        cx: &mut ViewContext<Self>,
-    ) -> Task<Result<Box<dyn ItemHandle>>> {
-        if let Some(existing_item) = self.item_for_path(&path, cx) {
-            return Task::ready(Ok(existing_item));
-        }
-
-        let project_path = path.clone();
-        let path_openers = self.path_openers.clone();
-        self.project.update(cx, |project, cx| {
-            for opener in path_openers.iter() {
-                if let Some(task) = opener.open(project, project_path.clone(), cx) {
-                    return task;
-                }
-            }
-            Task::ready(Err(anyhow!("no opener found for path {:?}", project_path)))
-        })
-    }
-
-    fn item_for_path(&self, path: &ProjectPath, cx: &AppContext) -> Option<Box<dyn ItemHandle>> {
-        self.items
-            .values()
-            .filter_map(|i| i.upgrade(cx))
-            .find(|i| i.project_path(cx).as_ref() == Some(path))
+    pub fn items<'a>(
+        &'a self,
+        cx: &'a AppContext,
+    ) -> impl 'a + Iterator<Item = &Box<dyn ItemHandle>> {
+        self.panes.iter().flat_map(|pane| pane.read(cx).items())
     }
 
-    pub fn item_of_type<T: Item>(&self, cx: &AppContext) -> Option<ModelHandle<T>> {
-        self.items
-            .values()
-            .find_map(|i| i.upgrade(cx).and_then(|i| i.to_any().downcast()))
+    pub fn item_of_type<T: Item>(&self, cx: &AppContext) -> Option<ViewHandle<T>> {
+        self.items_of_type(cx).max_by_key(|item| item.id())
     }
 
     pub fn items_of_type<'a, T: Item>(
         &'a self,
         cx: &'a AppContext,
-    ) -> impl 'a + Iterator<Item = ModelHandle<T>> {
-        self.items
-            .values()
-            .filter_map(|i| i.upgrade(cx).and_then(|i| i.to_any().downcast()))
+    ) -> impl 'a + Iterator<Item = ViewHandle<T>> {
+        self.panes
+            .iter()
+            .flat_map(|pane| pane.read(cx).items_of_type())
     }
 
-    pub fn active_item(&self, cx: &AppContext) -> Option<Box<dyn ItemViewHandle>> {
+    pub fn active_item(&self, cx: &AppContext) -> Option<Box<dyn ItemHandle>> {
         self.active_pane().read(cx).active_item()
     }
 
@@ -962,49 +1064,84 @@ impl Workspace {
         pane
     }
 
-    pub fn open_item<T>(
+    pub fn add_item(&mut self, item: Box<dyn ItemHandle>, cx: &mut ViewContext<Self>) {
+        let pane = self.active_pane().clone();
+        Pane::add_item(self, pane, item, true, cx);
+    }
+
+    pub fn open_path(
         &mut self,
-        item_handle: T,
+        path: impl Into<ProjectPath>,
         cx: &mut ViewContext<Self>,
-    ) -> Box<dyn ItemViewHandle>
-    where
-        T: 'static + ItemHandle,
-    {
-        self.open_item_in_pane(item_handle, &self.active_pane().clone(), cx)
+    ) -> Task<Result<Box<dyn ItemHandle>, Arc<anyhow::Error>>> {
+        let pane = self.active_pane().downgrade();
+        let task = self.load_path(path.into(), cx);
+        cx.spawn(|this, mut cx| async move {
+            let (project_entry_id, build_item) = task.await?;
+            let pane = pane
+                .upgrade(&cx)
+                .ok_or_else(|| anyhow!("pane was closed"))?;
+            this.update(&mut cx, |this, cx| {
+                Ok(Pane::open_item(
+                    this,
+                    pane,
+                    project_entry_id,
+                    cx,
+                    build_item,
+                ))
+            })
+        })
     }
 
-    pub fn open_item_in_pane<T>(
+    pub(crate) fn load_path(
         &mut self,
-        item_handle: T,
-        pane: &ViewHandle<Pane>,
+        path: ProjectPath,
         cx: &mut ViewContext<Self>,
-    ) -> Box<dyn ItemViewHandle>
-    where
-        T: 'static + ItemHandle,
-    {
-        self.items
-            .insert(Reverse(item_handle.id()), item_handle.downgrade());
-        pane.update(cx, |pane, cx| pane.open_item(item_handle, self, cx))
+    ) -> Task<
+        Result<(
+            ProjectEntryId,
+            impl 'static + FnOnce(&mut MutableAppContext) -> Box<dyn ItemHandle>,
+        )>,
+    > {
+        let project = self.project().clone();
+        let project_item = project.update(cx, |project, cx| project.open_path(path, cx));
+        let window_id = cx.window_id();
+        cx.as_mut().spawn(|mut cx| async move {
+            let (project_entry_id, project_item) = project_item.await?;
+            let build_item = cx.update(|cx| {
+                cx.default_global::<ProjectItemBuilders>()
+                    .get(&project_item.model_type())
+                    .ok_or_else(|| anyhow!("no item builder for project item"))
+                    .cloned()
+            })?;
+            let build_item =
+                move |cx: &mut MutableAppContext| build_item(window_id, project, project_item, cx);
+            Ok((project_entry_id, build_item))
+        })
     }
 
-    pub fn activate_pane_for_item(
+    pub fn open_project_item<T>(
         &mut self,
-        item: &dyn ItemHandle,
+        project_item: ModelHandle<T::Item>,
         cx: &mut ViewContext<Self>,
-    ) -> bool {
-        let pane = self.panes.iter().find_map(|pane| {
-            if pane.read(cx).contains_item(item) {
-                Some(pane.clone())
-            } else {
-                None
-            }
-        });
-        if let Some(pane) = pane {
-            self.activate_pane(pane.clone(), cx);
-            true
-        } else {
-            false
+    ) -> ViewHandle<T>
+    where
+        T: ProjectItem,
+    {
+        use project::Item as _;
+
+        let entry_id = project_item.read(cx).entry_id(cx);
+        if let Some(item) = entry_id
+            .and_then(|entry_id| self.active_pane().read(cx).item_for_entry(entry_id, cx))
+            .and_then(|item| item.downcast())
+        {
+            self.activate_item(&item, cx);
+            return item;
         }
+
+        let item = cx.add_view(|cx| T::for_project_item(self.project().clone(), project_item, cx));
+        self.add_item(Box::new(item.clone()), cx);
+        item
     }
 
     pub fn activate_item(&mut self, item: &dyn ItemHandle, cx: &mut ViewContext<Self>) -> bool {
@@ -1017,7 +1154,7 @@ impl Workspace {
         });
         if let Some((pane, ix)) = result {
             self.activate_pane(pane.clone(), cx);
-            pane.update(cx, |pane, cx| pane.activate_item(ix, cx));
+            pane.update(cx, |pane, cx| pane.activate_item(ix, true, cx));
             true
         } else {
             false
@@ -1025,24 +1162,48 @@ impl Workspace {
     }
 
     pub fn activate_next_pane(&mut self, cx: &mut ViewContext<Self>) {
-        let ix = self
-            .panes
-            .iter()
-            .position(|pane| pane == &self.active_pane)
-            .unwrap();
-        let next_ix = (ix + 1) % self.panes.len();
-        self.activate_pane(self.panes[next_ix].clone(), cx);
+        let next_pane = {
+            let panes = self.center.panes();
+            let ix = panes
+                .iter()
+                .position(|pane| **pane == self.active_pane)
+                .unwrap();
+            let next_ix = (ix + 1) % panes.len();
+            panes[next_ix].clone()
+        };
+        self.activate_pane(next_pane, cx);
+    }
+
+    pub fn activate_previous_pane(&mut self, cx: &mut ViewContext<Self>) {
+        let prev_pane = {
+            let panes = self.center.panes();
+            let ix = panes
+                .iter()
+                .position(|pane| **pane == self.active_pane)
+                .unwrap();
+            let prev_ix = if ix == 0 { panes.len() - 1 } else { ix - 1 };
+            panes[prev_ix].clone()
+        };
+        self.activate_pane(prev_pane, cx);
     }
 
     fn activate_pane(&mut self, pane: ViewHandle<Pane>, cx: &mut ViewContext<Self>) {
         if self.active_pane != pane {
-            self.active_pane = pane;
+            self.active_pane = pane.clone();
             self.status_bar.update(cx, |status_bar, cx| {
                 status_bar.set_active_pane(&self.active_pane, cx);
             });
             cx.focus(&self.active_pane);
             cx.notify();
         }
+
+        self.update_followers(
+            proto::update_followers::Variant::UpdateActiveView(proto::UpdateActiveView {
+                id: self.active_item(cx).map(|item| item.id() as u64),
+                leader_id: self.leader_for_pane(&pane).map(|id| id.0),
+            }),
+            cx,
+        );
     }
 
     fn handle_pane_event(

crates/zed/Cargo.toml 🔗

@@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathansobo@gmail.com>"]
 description = "The fast, collaborative code editor."
 edition = "2021"
 name = "zed"
-version = "0.21.0"
+version = "0.23.0"
 
 [lib]
 name = "zed"
@@ -55,6 +55,7 @@ text = { path = "../text" }
 theme = { path = "../theme" }
 theme_selector = { path = "../theme_selector" }
 util = { path = "../util" }
+vim = { path = "../vim" }
 workspace = { path = "../workspace" }
 anyhow = "1.0.38"
 async-compression = { version = "0.3", features = ["gzip", "futures-bufread"] }
@@ -64,6 +65,7 @@ crossbeam-channel = "0.5.0"
 ctor = "0.1.20"
 dirs = "3.0"
 easy-parallel = "3.1.0"
+env_logger = "0.8"
 futures = "0.3"
 http-auth-basic = "0.1.3"
 ignore = "0.4"

crates/zed/assets/themes/_base.toml 🔗

@@ -4,6 +4,8 @@ base = { family = "Zed Sans", size = 14 }
 [workspace]
 background = "$surface.0"
 pane_divider = { width = 1, color = "$border.0" }
+leader_border_opacity = 0.7
+leader_border_width = 2.0
 
 [workspace.titlebar]
 height = 32

crates/zed/src/main.rs 🔗

@@ -9,7 +9,6 @@ use gpui::{App, AssetSource, Task};
 use log::LevelFilter;
 use parking_lot::Mutex;
 use project::Fs;
-use simplelog::SimpleLogger;
 use smol::process::Command;
 use std::{env, fs, path::PathBuf, sync::Arc};
 use theme::{ThemeRegistry, DEFAULT_THEME_NAME};
@@ -61,7 +60,6 @@ fn main() {
     app.run(move |cx| {
         let http = http::client();
         let client = client::Client::new(http.clone());
-        let mut path_openers = Vec::new();
         let mut languages = language::build_language_registry(login_shell_env_loaded);
         let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http.clone(), cx));
         let channel_list =
@@ -70,8 +68,8 @@ fn main() {
         project::Project::init(&client);
         client::Channel::init(&client);
         client::init(client.clone(), cx);
-        workspace::init(cx);
-        editor::init(cx, &mut path_openers);
+        workspace::init(&client, cx);
+        editor::init(cx);
         go_to_line::init(cx);
         file_finder::init(cx);
         chat_panel::init(cx);
@@ -80,11 +78,18 @@ fn main() {
         project_panel::init(cx);
         diagnostics::init(cx);
         search::init(cx);
+        vim::init(cx);
         cx.spawn({
             let client = client.clone();
             |cx| async move {
-                if client.has_keychain_credentials(&cx) {
-                    client.authenticate_and_connect(&cx).await?;
+                if stdout_is_a_pty() {
+                    if client::IMPERSONATE_LOGIN.is_some() {
+                        client.authenticate_and_connect(false, &cx).await?;
+                    }
+                } else {
+                    if client.has_keychain_credentials(&cx) {
+                        client.authenticate_and_connect(true, &cx).await?;
+                    }
                 }
                 Ok::<_, anyhow::Error>(())
             }
@@ -102,7 +107,7 @@ fn main() {
         cx.spawn(|mut cx| async move {
             while let Some(settings) = settings_rx.next().await {
                 cx.update(|cx| {
-                    cx.update_app_state(|s, _| *s = settings);
+                    cx.update_global(|s, _| *s = settings);
                     cx.refresh_windows();
                 });
             }
@@ -111,7 +116,7 @@ fn main() {
 
         languages.set_language_server_download_dir(zed::ROOT_PATH.clone());
         languages.set_theme(&settings.theme.editor.syntax);
-        cx.add_app_state(settings);
+        cx.set_global(settings);
 
         let app_state = Arc::new(AppState {
             languages: Arc::new(languages),
@@ -120,7 +125,6 @@ fn main() {
             client,
             user_store,
             fs,
-            path_openers: Arc::from(path_openers),
             build_window_options: &build_window_options,
             build_workspace: &build_workspace,
         });
@@ -144,11 +148,10 @@ fn main() {
 }
 
 fn init_logger() {
-    let level = LevelFilter::Info;
-
     if stdout_is_a_pty() {
-        SimpleLogger::init(level, Default::default()).expect("could not initialize logger");
+        env_logger::init();
     } else {
+        let level = LevelFilter::Info;
         let log_dir_path = dirs::home_dir()
             .expect("could not locate home directory for logging")
             .join("Library/Logs/");

crates/zed/src/test.rs 🔗

@@ -17,9 +17,8 @@ fn init_logger() {
 
 pub fn test_app_state(cx: &mut MutableAppContext) -> Arc<AppState> {
     let settings = Settings::test(cx);
-    let mut path_openers = Vec::new();
-    editor::init(cx, &mut path_openers);
-    cx.add_app_state(settings);
+    editor::init(cx);
+    cx.set_global(settings);
     let themes = ThemeRegistry::new(Assets, cx.font_cache().clone());
     let http = FakeHttpClient::with_404_response();
     let client = Client::new(http.clone());
@@ -40,7 +39,6 @@ pub fn test_app_state(cx: &mut MutableAppContext) -> Arc<AppState> {
         client,
         user_store,
         fs: FakeFs::new(cx.background().clone()),
-        path_openers: Arc::from(path_openers),
         build_window_options: &build_window_options,
         build_workspace: &build_workspace,
     })

crates/zed/src/zed.rs 🔗

@@ -43,7 +43,7 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::MutableAppContext) {
     cx.add_global_action(quit);
     cx.add_global_action({
         move |action: &AdjustBufferFontSize, cx| {
-            cx.update_app_state::<Settings, _, _>(|settings, cx| {
+            cx.update_global::<Settings, _, _>(|settings, cx| {
                 settings.buffer_font_size =
                     (settings.buffer_font_size + action.0).max(MIN_FONT_SIZE);
                 cx.refresh_windows();
@@ -111,7 +111,6 @@ pub fn build_workspace(
         languages: app_state.languages.clone(),
         user_store: app_state.user_store.clone(),
         channel_list: app_state.channel_list.clone(),
-        path_openers: app_state.path_openers.clone(),
     };
     let mut workspace = Workspace::new(&workspace_params, cx);
     let project = workspace.project().clone();
@@ -193,7 +192,7 @@ mod tests {
     use theme::{Theme, ThemeRegistry, DEFAULT_THEME_NAME};
     use util::test::temp_tree;
     use workspace::{
-        open_paths, pane, ItemView, ItemViewHandle, OpenNew, Pane, SplitDirection, WorkspaceHandle,
+        open_paths, pane, Item, ItemHandle, OpenNew, Pane, SplitDirection, WorkspaceHandle,
     };
 
     #[gpui::test]
@@ -253,7 +252,7 @@ mod tests {
     async fn test_new_empty_workspace(cx: &mut TestAppContext) {
         let app_state = cx.update(test_app_state);
         cx.update(|cx| {
-            workspace::init(cx);
+            workspace::init(&app_state.client, cx);
         });
         cx.dispatch_global_action(workspace::OpenNew(app_state.clone()));
         let window_id = *cx.window_ids().first().unwrap();
@@ -325,7 +324,7 @@ mod tests {
                 pane.active_item().unwrap().project_path(cx),
                 Some(file1.clone())
             );
-            assert_eq!(pane.item_views().count(), 1);
+            assert_eq!(pane.items().count(), 1);
         });
 
         // Open the second entry
@@ -339,7 +338,7 @@ mod tests {
                 pane.active_item().unwrap().project_path(cx),
                 Some(file2.clone())
             );
-            assert_eq!(pane.item_views().count(), 2);
+            assert_eq!(pane.items().count(), 2);
         });
 
         // Open the first entry again. The existing pane item is activated.
@@ -355,7 +354,7 @@ mod tests {
                 pane.active_item().unwrap().project_path(cx),
                 Some(file1.clone())
             );
-            assert_eq!(pane.item_views().count(), 2);
+            assert_eq!(pane.items().count(), 2);
         });
 
         // Split the pane with the first entry, then open the second entry again.
@@ -394,7 +393,7 @@ mod tests {
                 Some(file3.clone())
             );
             let pane_entries = pane
-                .item_views()
+                .items()
                 .map(|i| i.project_path(cx).unwrap())
                 .collect::<Vec<_>>();
             assert_eq!(pane_entries, &[file1, file2, file3]);
@@ -894,6 +893,52 @@ mod tests {
             (file3.clone(), DisplayPoint::new(0, 0))
         );
 
+        // Modify file to remove nav history location, and ensure duplicates are skipped
+        editor1.update(cx, |editor, cx| {
+            editor.select_display_ranges(&[DisplayPoint::new(15, 0)..DisplayPoint::new(15, 0)], cx)
+        });
+
+        for _ in 0..5 {
+            editor1.update(cx, |editor, cx| {
+                editor
+                    .select_display_ranges(&[DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0)], cx);
+            });
+            editor1.update(cx, |editor, cx| {
+                editor.select_display_ranges(
+                    &[DisplayPoint::new(13, 0)..DisplayPoint::new(13, 0)],
+                    cx,
+                )
+            });
+        }
+
+        editor1.update(cx, |editor, cx| {
+            editor.transact(cx, |editor, cx| {
+                editor.select_display_ranges(
+                    &[DisplayPoint::new(2, 0)..DisplayPoint::new(14, 0)],
+                    cx,
+                );
+                editor.insert("", cx);
+            })
+        });
+
+        editor1.update(cx, |editor, cx| {
+            editor.select_display_ranges(&[DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)], cx)
+        });
+        workspace
+            .update(cx, |w, cx| Pane::go_back(w, None, cx))
+            .await;
+        assert_eq!(
+            active_location(&workspace, cx),
+            (file1.clone(), DisplayPoint::new(2, 0))
+        );
+        workspace
+            .update(cx, |w, cx| Pane::go_back(w, None, cx))
+            .await;
+        assert_eq!(
+            active_location(&workspace, cx),
+            (file1.clone(), DisplayPoint::new(3, 0))
+        );
+
         fn active_location(
             workspace: &ViewHandle<Workspace>,
             cx: &mut TestAppContext,