Merge branch 'main' into fix-reconnects-after-deploy

Antonio Scandurra created

Change summary

crates/collab/src/integration_tests.rs | 273 +++++++++++-------
crates/diagnostics/src/diagnostics.rs  |   2 
crates/editor/src/editor.rs            |  53 ++
crates/editor/src/editor_tests.rs      | 268 +++++++++++++++---
crates/editor/src/items.rs             | 402 ++++++++++++++++++++-------
crates/editor/src/multi_buffer.rs      | 208 ++++++++++++-
crates/language/src/proto.rs           |  20 
crates/rpc/proto/zed.proto             |  66 ++-
crates/search/src/project_search.rs    |   4 
crates/text/src/text.rs                |   4 
crates/workspace/src/item.rs           |  41 ++
crates/workspace/src/workspace.rs      | 293 +++++++++++--------
crates/zed/src/zed.rs                  |  31 ++
script/bundle                          |  33 +
script/start-local-collaboration       |  50 +++
styles/src/styleTree/components.ts     |  10 
16 files changed, 1,277 insertions(+), 481 deletions(-)

Detailed changes

crates/collab/src/integration_tests.rs 🔗

@@ -12,8 +12,8 @@ use client::{
 };
 use collections::{BTreeMap, HashMap, HashSet};
 use editor::{
-    self, ConfirmCodeAction, ConfirmCompletion, ConfirmRename, Editor, Redo, Rename, ToOffset,
-    ToggleCodeActions, Undo,
+    self, ConfirmCodeAction, ConfirmCompletion, ConfirmRename, Editor, ExcerptRange, MultiBuffer,
+    Redo, Rename, ToOffset, ToggleCodeActions, Undo,
 };
 use fs::{FakeFs, Fs as _, HomeDir, LineEnding};
 use futures::{channel::oneshot, StreamExt as _};
@@ -22,7 +22,7 @@ use gpui::{
     TestAppContext, ViewHandle,
 };
 use language::{
-    range_to_lsp, tree_sitter_rust, Diagnostic, DiagnosticEntry, FakeLspAdapter, Language,
+    range_to_lsp, tree_sitter_rust, Anchor, Diagnostic, DiagnosticEntry, FakeLspAdapter, Language,
     LanguageConfig, LanguageRegistry, OffsetRangeExt, Point, PointUtf16, Rope,
 };
 use live_kit_client::MacOSDisplay;
@@ -1058,17 +1058,22 @@ async fn test_share_project(
 
     let editor_b = cx_b.add_view(&window_b, |cx| Editor::for_buffer(buffer_b, None, cx));
 
-    // TODO
-    // // Create a selection set as client B and see that selection set as client A.
-    // buffer_a
-    //     .condition(&cx_a, |buffer, _| buffer.selection_sets().count() == 1)
-    //     .await;
+    // Client A sees client B's selection
+    deterministic.run_until_parked();
+    buffer_a.read_with(cx_a, |buffer, _| {
+        buffer
+            .snapshot()
+            .remote_selections_in_range(Anchor::MIN..Anchor::MAX)
+            .count()
+            == 1
+    });
 
     // Edit the buffer as client B and see that edit as client A.
     editor_b.update(cx_b, |editor, cx| editor.handle_input("ok, ", cx));
-    buffer_a
-        .condition(cx_a, |buffer, _| buffer.text() == "ok, b-contents")
-        .await;
+    deterministic.run_until_parked();
+    buffer_a.read_with(cx_a, |buffer, _| {
+        assert_eq!(buffer.text(), "ok, b-contents")
+    });
 
     // Client B can invite client C on a project shared by client A.
     active_call_b
@@ -1091,12 +1096,16 @@ async fn test_share_project(
         .build_remote_project(initial_project.id, cx_c)
         .await;
 
-    // TODO
-    // // Remove the selection set as client B, see those selections disappear as client A.
+    // Client B closes the editor, and client A sees client B's selections removed.
     cx_b.update(move |_| drop(editor_b));
-    // buffer_a
-    //     .condition(&cx_a, |buffer, _| buffer.selection_sets().count() == 0)
-    //     .await;
+    deterministic.run_until_parked();
+    buffer_a.read_with(cx_a, |buffer, _| {
+        buffer
+            .snapshot()
+            .remote_selections_in_range(Anchor::MIN..Anchor::MAX)
+            .count()
+            == 0
+    });
 }
 
 #[gpui::test(iterations = 10)]
@@ -1250,13 +1259,9 @@ async fn test_host_disconnect(
     server.forbid_connections();
     server.disconnect_client(client_a.peer_id().unwrap());
     deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
-    project_a
-        .condition(cx_a, |project, _| project.collaborators().is_empty())
-        .await;
+    project_a.read_with(cx_a, |project, _| project.collaborators().is_empty());
     project_a.read_with(cx_a, |project, _| assert!(!project.is_shared()));
-    project_b
-        .condition(cx_b, |project, _| project.is_read_only())
-        .await;
+    project_b.read_with(cx_b, |project, _| project.is_read_only());
     assert!(worktree_a.read_with(cx_a, |tree, _| !tree.as_local().unwrap().is_shared()));
 
     // Ensure client B's edited state is reset and that the whole window is blurred.
@@ -1641,9 +1646,8 @@ async fn test_propagate_saves_and_fs_changes(
         .await
         .unwrap();
 
-    buffer_a
-        .condition(cx_a, |buf, _| buf.text() == "i-am-c, i-am-b, ")
-        .await;
+    deterministic.run_until_parked();
+    buffer_a.read_with(cx_a, |buf, _| assert_eq!(buf.text(), "i-am-c, i-am-b, "));
     buffer_a.update(cx_a, |buf, cx| {
         buf.edit([(buf.len()..buf.len(), "i-am-a")], None, cx)
     });
@@ -2297,9 +2301,8 @@ async fn test_buffer_conflict_after_save(
     });
 
     buffer_b.update(cx_b, |buf, cx| buf.save(cx)).await.unwrap();
-    buffer_b
-        .condition(cx_b, |buffer_b, _| !buffer_b.is_dirty())
-        .await;
+    cx_a.foreground().forbid_parking();
+    buffer_b.read_with(cx_b, |buffer_b, _| assert!(!buffer_b.is_dirty()));
     buffer_b.read_with(cx_b, |buf, _| {
         assert!(!buf.has_conflict());
     });
@@ -2359,12 +2362,9 @@ async fn test_buffer_reloading(
         .save("/dir/a.txt".as_ref(), &new_contents, LineEnding::Windows)
         .await
         .unwrap();
-    buffer_b
-        .condition(cx_b, |buf, _| {
-            buf.text() == new_contents.to_string() && !buf.is_dirty()
-        })
-        .await;
+    cx_a.foreground().run_until_parked();
     buffer_b.read_with(cx_b, |buf, _| {
+        assert_eq!(buf.text(), new_contents.to_string());
         assert!(!buf.is_dirty());
         assert!(!buf.has_conflict());
         assert_eq!(buf.line_ending(), LineEnding::Windows);
@@ -2416,7 +2416,8 @@ async fn test_editing_while_guest_opens_buffer(
 
     let text = buffer_a.read_with(cx_a, |buf, _| buf.text());
     let buffer_b = buffer_b.await.unwrap();
-    buffer_b.condition(cx_b, |buf, _| buf.text() == text).await;
+    cx_a.foreground().run_until_parked();
+    buffer_b.read_with(cx_b, |buf, _| assert_eq!(buf.text(), text));
 }
 
 #[gpui::test(iterations = 10)]
@@ -2446,9 +2447,8 @@ async fn test_leaving_worktree_while_opening_buffer(
     let project_b = client_b.build_remote_project(project_id, cx_b).await;
 
     // See that a guest has joined as client A.
-    project_a
-        .condition(cx_a, |p, _| p.collaborators().len() == 1)
-        .await;
+    cx_a.foreground().run_until_parked();
+    project_a.read_with(cx_a, |p, _| assert_eq!(p.collaborators().len(), 1));
 
     // Begin opening a buffer as client B, but leave the project before the open completes.
     let buffer_b = cx_b
@@ -2458,9 +2458,8 @@ async fn test_leaving_worktree_while_opening_buffer(
     drop(buffer_b);
 
     // See that the guest has left.
-    project_a
-        .condition(cx_a, |p, _| p.collaborators().is_empty())
-        .await;
+    cx_a.foreground().run_until_parked();
+    project_a.read_with(cx_a, |p, _| assert!(p.collaborators().is_empty()));
 }
 
 #[gpui::test(iterations = 10)]
@@ -2979,9 +2978,10 @@ async fn test_collaborating_with_completion(
     });
 
     let fake_language_server = fake_language_servers.next().await.unwrap();
-    buffer_b
-        .condition(cx_b, |buffer, _| !buffer.completion_triggers().is_empty())
-        .await;
+    cx_a.foreground().run_until_parked();
+    buffer_b.read_with(cx_b, |buffer, _| {
+        assert!(!buffer.completion_triggers().is_empty())
+    });
 
     // Type a completion trigger character as the guest.
     editor_b.update(cx_b, |editor, cx| {
@@ -3043,14 +3043,13 @@ async fn test_collaborating_with_completion(
         .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
         .await
         .unwrap();
-    buffer_a
-        .condition(cx_a, |buffer, _| buffer.text() == "fn main() { a. }")
-        .await;
+    cx_a.foreground().run_until_parked();
+    buffer_a.read_with(cx_a, |buffer, _| {
+        assert_eq!(buffer.text(), "fn main() { a. }")
+    });
 
     // Confirm a completion on the guest.
-    editor_b
-        .condition(cx_b, |editor, _| editor.context_menu_visible())
-        .await;
+    editor_b.read_with(cx_b, |editor, _| assert!(editor.context_menu_visible()));
     editor_b.update(cx_b, |editor, cx| {
         editor.confirm_completion(&ConfirmCompletion { item_ix: Some(0) }, cx);
         assert_eq!(editor.text(cx), "fn main() { a.first_method() }");
@@ -3079,16 +3078,19 @@ async fn test_collaborating_with_completion(
     );
 
     // The additional edit is applied.
-    buffer_a
-        .condition(cx_a, |buffer, _| {
-            buffer.text() == "use d::SomeTrait;\nfn main() { a.first_method() }"
-        })
-        .await;
-    buffer_b
-        .condition(cx_b, |buffer, _| {
-            buffer.text() == "use d::SomeTrait;\nfn main() { a.first_method() }"
-        })
-        .await;
+    cx_a.foreground().run_until_parked();
+    buffer_a.read_with(cx_a, |buffer, _| {
+        assert_eq!(
+            buffer.text(),
+            "use d::SomeTrait;\nfn main() { a.first_method() }"
+        );
+    });
+    buffer_b.read_with(cx_b, |buffer, _| {
+        assert_eq!(
+            buffer.text(),
+            "use d::SomeTrait;\nfn main() { a.first_method() }"
+        );
+    });
 }
 
 #[gpui::test(iterations = 10)]
@@ -3134,9 +3136,8 @@ async fn test_reloading_buffer_manually(
         assert!(buffer.is_dirty());
         assert!(!buffer.has_conflict());
     });
-    buffer_a
-        .condition(cx_a, |buffer, _| buffer.text() == "let six = 6;")
-        .await;
+    cx_a.foreground().run_until_parked();
+    buffer_a.read_with(cx_a, |buffer, _| assert_eq!(buffer.text(), "let six = 6;"));
 
     client_a
         .fs
@@ -3147,12 +3148,9 @@ async fn test_reloading_buffer_manually(
         )
         .await
         .unwrap();
-    buffer_a
-        .condition(cx_a, |buffer, _| buffer.has_conflict())
-        .await;
-    buffer_b
-        .condition(cx_b, |buffer, _| buffer.has_conflict())
-        .await;
+    cx_a.foreground().run_until_parked();
+    buffer_a.read_with(cx_a, |buffer, _| assert!(buffer.has_conflict()));
+    buffer_b.read_with(cx_b, |buffer, _| assert!(buffer.has_conflict()));
 
     project_b
         .update(cx_b, |project, cx| {
@@ -4178,9 +4176,8 @@ async fn test_collaborating_with_code_actions(
             cx,
         );
     });
-    editor_b
-        .condition(cx_b, |editor, _| editor.context_menu_visible())
-        .await;
+    cx_a.foreground().run_until_parked();
+    editor_b.read_with(cx_b, |editor, _| assert!(editor.context_menu_visible()));
 
     fake_language_server.remove_request_handler::<lsp::request::CodeActionRequest>();
 
@@ -5162,9 +5159,9 @@ async fn test_following(
         .insert_tree(
             "/a",
             json!({
-                "1.txt": "one",
-                "2.txt": "two",
-                "3.txt": "three",
+                "1.txt": "one\none\none",
+                "2.txt": "two\ntwo\ntwo",
+                "3.txt": "three\nthree\nthree",
             }),
         )
         .await;
@@ -5263,11 +5260,60 @@ async fn test_following(
     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;
+    deterministic.run_until_parked();
+    workspace_b.read_with(cx_b, |workspace, cx| {
+        assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b1.id());
+    });
+
+    // When client A opens a multibuffer, client B does so as well.
+    let multibuffer_a = cx_a.add_model(|cx| {
+        let buffer_a1 = project_a.update(cx, |project, cx| {
+            project
+                .get_open_buffer(&(worktree_id, "1.txt").into(), cx)
+                .unwrap()
+        });
+        let buffer_a2 = project_a.update(cx, |project, cx| {
+            project
+                .get_open_buffer(&(worktree_id, "2.txt").into(), cx)
+                .unwrap()
+        });
+        let mut result = MultiBuffer::new(0);
+        result.push_excerpts(
+            buffer_a1,
+            [ExcerptRange {
+                context: 0..3,
+                primary: None,
+            }],
+            cx,
+        );
+        result.push_excerpts(
+            buffer_a2,
+            [ExcerptRange {
+                context: 4..7,
+                primary: None,
+            }],
+            cx,
+        );
+        result
+    });
+    let multibuffer_editor_a = workspace_a.update(cx_a, |workspace, cx| {
+        let editor =
+            cx.add_view(|cx| Editor::for_multibuffer(multibuffer_a, Some(project_a.clone()), cx));
+        workspace.add_item(Box::new(editor.clone()), cx);
+        editor
+    });
+    deterministic.run_until_parked();
+    let multibuffer_editor_b = workspace_b.read_with(cx_b, |workspace, cx| {
+        workspace
+            .active_item(cx)
+            .unwrap()
+            .downcast::<Editor>()
+            .unwrap()
+    });
+    assert_eq!(
+        multibuffer_editor_a.read_with(cx_a, |editor, cx| editor.text(cx)),
+        multibuffer_editor_b.read_with(cx_b, |editor, cx| editor.text(cx)),
+    );
 
     // When client A navigates back and forth, client B does so as well.
     workspace_a
@@ -5275,47 +5321,52 @@ async fn test_following(
             workspace::Pane::go_back(workspace, None, cx)
         })
         .await;
-    workspace_b
-        .condition(cx_b, |workspace, cx| {
-            workspace.active_item(cx).unwrap().id() == editor_b2.id()
+    deterministic.run_until_parked();
+    workspace_b.read_with(cx_b, |workspace, cx| {
+        assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b1.id());
+    });
+
+    workspace_a
+        .update(cx_a, |workspace, cx| {
+            workspace::Pane::go_back(workspace, None, cx)
         })
         .await;
+    deterministic.run_until_parked();
+    workspace_b.read_with(cx_b, |workspace, cx| {
+        assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b2.id());
+    });
 
     workspace_a
         .update(cx_a, |workspace, cx| {
             workspace::Pane::go_forward(workspace, None, cx)
         })
         .await;
-    workspace_b
-        .condition(cx_b, |workspace, cx| {
-            workspace.active_item(cx).unwrap().id() == editor_b1.id()
-        })
-        .await;
+    deterministic.run_until_parked();
+    workspace_b.read_with(cx_b, |workspace, cx| {
+        assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b1.id());
+    });
 
     // Changes to client A's editor are reflected on client B.
     editor_a1.update(cx_a, |editor, cx| {
         editor.change_selections(None, cx, |s| s.select_ranges([1..1, 2..2]));
     });
-    editor_b1
-        .condition(cx_b, |editor, cx| {
-            editor.selections.ranges(cx) == vec![1..1, 2..2]
-        })
-        .await;
+    deterministic.run_until_parked();
+    editor_b1.read_with(cx_b, |editor, cx| {
+        assert_eq!(editor.selections.ranges(cx), &[1..1, 2..2]);
+    });
 
     editor_a1.update(cx_a, |editor, cx| editor.set_text("TWO", cx));
-    editor_b1
-        .condition(cx_b, |editor, cx| editor.text(cx) == "TWO")
-        .await;
+    deterministic.run_until_parked();
+    editor_b1.read_with(cx_b, |editor, cx| assert_eq!(editor.text(cx), "TWO"));
 
     editor_a1.update(cx_a, |editor, cx| {
         editor.change_selections(None, cx, |s| s.select_ranges([3..3]));
         editor.set_scroll_position(vec2f(0., 100.), cx);
     });
-    editor_b1
-        .condition(cx_b, |editor, cx| {
-            editor.selections.ranges(cx) == vec![3..3]
-        })
-        .await;
+    deterministic.run_until_parked();
+    editor_b1.read_with(cx_b, |editor, cx| {
+        assert_eq!(editor.selections.ranges(cx), &[3..3]);
+    });
 
     // After unfollowing, client B stops receiving updates from client A.
     workspace_b.update(cx_b, |workspace, cx| {
@@ -5384,13 +5435,21 @@ async fn test_following(
         .await
         .unwrap();
     deterministic.run_until_parked();
-    assert_eq!(
-        workspace_a.read_with(cx_a, |workspace, cx| workspace
-            .active_item(cx)
-            .unwrap()
-            .id()),
-        editor_a1.id()
-    );
+    workspace_a.read_with(cx_a, |workspace, cx| {
+        assert_eq!(workspace.active_item(cx).unwrap().id(), editor_a1.id())
+    });
+
+    // Client B activates a multibuffer that was created by following client A. Client A returns to that multibuffer.
+    workspace_b.update(cx_b, |workspace, cx| {
+        workspace.activate_item(&multibuffer_editor_b, cx)
+    });
+    deterministic.run_until_parked();
+    workspace_a.read_with(cx_a, |workspace, cx| {
+        assert_eq!(
+            workspace.active_item(cx).unwrap().id(),
+            multibuffer_editor_a.id()
+        )
+    });
 
     // Client B activates an external window again, and the previously-opened screen-sharing item
     // gets activated.

crates/diagnostics/src/diagnostics.rs 🔗

@@ -164,7 +164,7 @@ impl ProjectDiagnosticsEditor {
             editor.set_vertical_scroll_margin(5, cx);
             editor
         });
-        cx.subscribe(&editor, |_, _, event, cx| cx.emit(*event))
+        cx.subscribe(&editor, |_, _, event, cx| cx.emit(event.clone()))
             .detach();
 
         let project = project_handle.read(cx);

crates/editor/src/editor.rs 🔗

@@ -84,7 +84,7 @@ use std::{
 pub use sum_tree::Bias;
 use theme::{DiagnosticStyle, Theme};
 use util::{post_inc, ResultExt, TryFutureExt};
-use workspace::{ItemNavHistory, Workspace, WorkspaceId};
+use workspace::{ItemNavHistory, ViewId, Workspace, WorkspaceId};
 
 use crate::git::diff_hunk_to_display;
 
@@ -467,6 +467,7 @@ pub struct Editor {
     keymap_context_layers: BTreeMap<TypeId, gpui::keymap::Context>,
     input_enabled: bool,
     leader_replica_id: Option<u16>,
+    remote_id: Option<ViewId>,
     hover_state: HoverState,
     link_go_to_definition_state: LinkGoToDefinitionState,
     _subscriptions: Vec<Subscription>,
@@ -1108,6 +1109,7 @@ impl Editor {
             keymap_context_layers: Default::default(),
             input_enabled: true,
             leader_replica_id: None,
+            remote_id: None,
             hover_state: Default::default(),
             link_go_to_definition_state: Default::default(),
             _subscriptions: vec![
@@ -5883,25 +5885,36 @@ impl Editor {
     fn on_buffer_event(
         &mut self,
         _: ModelHandle<MultiBuffer>,
-        event: &language::Event,
+        event: &multi_buffer::Event,
         cx: &mut ViewContext<Self>,
     ) {
         match event {
-            language::Event::Edited => {
+            multi_buffer::Event::Edited => {
                 self.refresh_active_diagnostics(cx);
                 self.refresh_code_actions(cx);
                 cx.emit(Event::BufferEdited);
             }
-            language::Event::Reparsed => cx.emit(Event::Reparsed),
-            language::Event::DirtyChanged => cx.emit(Event::DirtyChanged),
-            language::Event::Saved => cx.emit(Event::Saved),
-            language::Event::FileHandleChanged => cx.emit(Event::TitleChanged),
-            language::Event::Reloaded => cx.emit(Event::TitleChanged),
-            language::Event::Closed => cx.emit(Event::Closed),
-            language::Event::DiagnosticsUpdated => {
+            multi_buffer::Event::ExcerptsAdded {
+                buffer,
+                predecessor,
+                excerpts,
+            } => cx.emit(Event::ExcerptsAdded {
+                buffer: buffer.clone(),
+                predecessor: *predecessor,
+                excerpts: excerpts.clone(),
+            }),
+            multi_buffer::Event::ExcerptsRemoved { ids } => {
+                cx.emit(Event::ExcerptsRemoved { ids: ids.clone() })
+            }
+            multi_buffer::Event::Reparsed => cx.emit(Event::Reparsed),
+            multi_buffer::Event::DirtyChanged => cx.emit(Event::DirtyChanged),
+            multi_buffer::Event::Saved => cx.emit(Event::Saved),
+            multi_buffer::Event::FileHandleChanged => cx.emit(Event::TitleChanged),
+            multi_buffer::Event::Reloaded => cx.emit(Event::TitleChanged),
+            multi_buffer::Event::Closed => cx.emit(Event::Closed),
+            multi_buffer::Event::DiagnosticsUpdated => {
                 self.refresh_active_diagnostics(cx);
             }
-            _ => {}
         }
     }
 
@@ -6084,8 +6097,16 @@ impl Deref for EditorSnapshot {
     }
 }
 
-#[derive(Copy, Clone, Debug, PartialEq, Eq)]
+#[derive(Clone, Debug, PartialEq, Eq)]
 pub enum Event {
+    ExcerptsAdded {
+        buffer: ModelHandle<Buffer>,
+        predecessor: ExcerptId,
+        excerpts: Vec<(ExcerptId, ExcerptRange<language::Anchor>)>,
+    },
+    ExcerptsRemoved {
+        ids: Vec<ExcerptId>,
+    },
     BufferEdited,
     Edited,
     Reparsed,
@@ -6093,8 +6114,12 @@ pub enum Event {
     DirtyChanged,
     Saved,
     TitleChanged,
-    SelectionsChanged { local: bool },
-    ScrollPositionChanged { local: bool },
+    SelectionsChanged {
+        local: bool,
+    },
+    ScrollPositionChanged {
+        local: bool,
+    },
     Closed,
 }
 

crates/editor/src/editor_tests.rs 🔗

@@ -1,8 +1,7 @@
-use std::{cell::RefCell, rc::Rc, time::Instant};
-
 use drag_and_drop::DragAndDrop;
 use futures::StreamExt;
 use indoc::indoc;
+use std::{cell::RefCell, rc::Rc, time::Instant};
 use unindent::Unindent;
 
 use super::*;
@@ -24,7 +23,7 @@ use util::{
 };
 use workspace::{
     item::{FollowableItem, ItemHandle},
-    NavigationEntry, Pane,
+    NavigationEntry, Pane, ViewId,
 };
 
 #[gpui::test]
@@ -41,7 +40,7 @@ fn test_edit_events(cx: &mut MutableAppContext) {
                     event,
                     Event::Edited | Event::BufferEdited | Event::DirtyChanged
                 ) {
-                    events.borrow_mut().push(("editor1", *event));
+                    events.borrow_mut().push(("editor1", event.clone()));
                 }
             })
             .detach();
@@ -56,7 +55,7 @@ fn test_edit_events(cx: &mut MutableAppContext) {
                     event,
                     Event::Edited | Event::BufferEdited | Event::DirtyChanged
                 ) {
-                    events.borrow_mut().push(("editor2", *event));
+                    events.borrow_mut().push(("editor2", event.clone()));
                 }
             })
             .detach();
@@ -4969,19 +4968,27 @@ fn test_highlighted_ranges(cx: &mut gpui::MutableAppContext) {
 }
 
 #[gpui::test]
-fn test_following(cx: &mut gpui::MutableAppContext) {
-    let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
-
-    cx.set_global(Settings::test(cx));
+async fn test_following(cx: &mut gpui::TestAppContext) {
+    Settings::test_async(cx);
+    let fs = FakeFs::new(cx.background());
+    let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
 
-    let (_, leader) = cx.add_window(Default::default(), |cx| build_editor(buffer.clone(), cx));
-    let (_, follower) = cx.add_window(
-        WindowOptions {
-            bounds: WindowBounds::Fixed(RectF::from_points(vec2f(0., 0.), vec2f(10., 80.))),
-            ..Default::default()
-        },
-        |cx| build_editor(buffer.clone(), cx),
-    );
+    let buffer = project.update(cx, |project, cx| {
+        let buffer = project
+            .create_buffer(&sample_text(16, 8, 'a'), None, cx)
+            .unwrap();
+        cx.add_model(|cx| MultiBuffer::singleton(buffer, cx))
+    });
+    let (_, leader) = cx.add_window(|cx| build_editor(buffer.clone(), cx));
+    let (_, follower) = cx.update(|cx| {
+        cx.add_window(
+            WindowOptions {
+                bounds: WindowBounds::Fixed(RectF::from_points(vec2f(0., 0.), vec2f(10., 80.))),
+                ..Default::default()
+            },
+            |cx| build_editor(buffer.clone(), cx),
+        )
+    });
 
     let is_still_following = Rc::new(RefCell::new(true));
     let pending_update = Rc::new(RefCell::new(None));
@@ -5009,44 +5016,50 @@ fn test_following(cx: &mut gpui::MutableAppContext) {
     leader.update(cx, |leader, cx| {
         leader.change_selections(None, cx, |s| s.select_ranges([1..1]));
     });
-    follower.update(cx, |follower, cx| {
-        follower
-            .apply_update_proto(pending_update.borrow_mut().take().unwrap(), cx)
-            .unwrap();
+    follower
+        .update(cx, |follower, cx| {
+            follower.apply_update_proto(&project, pending_update.borrow_mut().take().unwrap(), cx)
+        })
+        .await
+        .unwrap();
+    follower.read_with(cx, |follower, cx| {
+        assert_eq!(follower.selections.ranges(cx), vec![1..1]);
     });
-    assert_eq!(follower.read(cx).selections.ranges(cx), vec![1..1]);
     assert_eq!(*is_still_following.borrow(), true);
 
     // Update the scroll position only
     leader.update(cx, |leader, cx| {
         leader.set_scroll_position(vec2f(1.5, 3.5), cx);
     });
-    follower.update(cx, |follower, cx| {
-        follower
-            .apply_update_proto(pending_update.borrow_mut().take().unwrap(), cx)
-            .unwrap();
-    });
+    follower
+        .update(cx, |follower, cx| {
+            follower.apply_update_proto(&project, pending_update.borrow_mut().take().unwrap(), cx)
+        })
+        .await
+        .unwrap();
     assert_eq!(
         follower.update(cx, |follower, cx| follower.scroll_position(cx)),
         vec2f(1.5, 3.5)
     );
     assert_eq!(*is_still_following.borrow(), true);
 
-    // Update the selections and scroll position
+    // Update the selections and scroll position. The follower's scroll position is updated
+    // via autoscroll, not via the leader's exact scroll position.
     leader.update(cx, |leader, cx| {
         leader.change_selections(None, cx, |s| s.select_ranges([0..0]));
         leader.request_autoscroll(Autoscroll::newest(), cx);
         leader.set_scroll_position(vec2f(1.5, 3.5), cx);
     });
+    follower
+        .update(cx, |follower, cx| {
+            follower.apply_update_proto(&project, pending_update.borrow_mut().take().unwrap(), cx)
+        })
+        .await
+        .unwrap();
     follower.update(cx, |follower, cx| {
-        let initial_scroll_position = follower.scroll_position(cx);
-        follower
-            .apply_update_proto(pending_update.borrow_mut().take().unwrap(), cx)
-            .unwrap();
-        assert_eq!(follower.scroll_position(cx), initial_scroll_position);
-        assert!(follower.scroll_manager.has_autoscroll_request());
+        assert_eq!(follower.scroll_position(cx), vec2f(1.5, 0.0));
+        assert_eq!(follower.selections.ranges(cx), vec![0..0]);
     });
-    assert_eq!(follower.read(cx).selections.ranges(cx), vec![0..0]);
     assert_eq!(*is_still_following.borrow(), true);
 
     // Creating a pending selection that precedes another selection
@@ -5054,24 +5067,30 @@ fn test_following(cx: &mut gpui::MutableAppContext) {
         leader.change_selections(None, cx, |s| s.select_ranges([1..1]));
         leader.begin_selection(DisplayPoint::new(0, 0), true, 1, cx);
     });
-    follower.update(cx, |follower, cx| {
-        follower
-            .apply_update_proto(pending_update.borrow_mut().take().unwrap(), cx)
-            .unwrap();
+    follower
+        .update(cx, |follower, cx| {
+            follower.apply_update_proto(&project, pending_update.borrow_mut().take().unwrap(), cx)
+        })
+        .await
+        .unwrap();
+    follower.read_with(cx, |follower, cx| {
+        assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]);
     });
-    assert_eq!(follower.read(cx).selections.ranges(cx), vec![0..0, 1..1]);
     assert_eq!(*is_still_following.borrow(), true);
 
     // Extend the pending selection so that it surrounds another selection
     leader.update(cx, |leader, cx| {
         leader.extend_selection(DisplayPoint::new(0, 2), 1, cx);
     });
-    follower.update(cx, |follower, cx| {
-        follower
-            .apply_update_proto(pending_update.borrow_mut().take().unwrap(), cx)
-            .unwrap();
+    follower
+        .update(cx, |follower, cx| {
+            follower.apply_update_proto(&project, pending_update.borrow_mut().take().unwrap(), cx)
+        })
+        .await
+        .unwrap();
+    follower.read_with(cx, |follower, cx| {
+        assert_eq!(follower.selections.ranges(cx), vec![0..2]);
     });
-    assert_eq!(follower.read(cx).selections.ranges(cx), vec![0..2]);
 
     // Scrolling locally breaks the follow
     follower.update(cx, |follower, cx| {
@@ -5087,6 +5106,165 @@ fn test_following(cx: &mut gpui::MutableAppContext) {
     assert_eq!(*is_still_following.borrow(), false);
 }
 
+#[gpui::test]
+async fn test_following_with_multiple_excerpts(cx: &mut gpui::TestAppContext) {
+    Settings::test_async(cx);
+    let fs = FakeFs::new(cx.background());
+    let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
+    let (_, pane) = cx.add_window(|cx| Pane::new(None, cx));
+
+    let leader = pane.update(cx, |_, cx| {
+        let multibuffer = cx.add_model(|_| MultiBuffer::new(0));
+        cx.add_view(|cx| build_editor(multibuffer.clone(), cx))
+    });
+
+    // Start following the editor when it has no excerpts.
+    let mut state_message = leader.update(cx, |leader, cx| leader.to_state_proto(cx));
+    let follower_1 = cx
+        .update(|cx| {
+            Editor::from_state_proto(
+                pane.clone(),
+                project.clone(),
+                ViewId {
+                    creator: Default::default(),
+                    id: 0,
+                },
+                &mut state_message,
+                cx,
+            )
+        })
+        .unwrap()
+        .await
+        .unwrap();
+
+    let update_message = Rc::new(RefCell::new(None));
+    follower_1.update(cx, {
+        let update = update_message.clone();
+        |_, cx| {
+            cx.subscribe(&leader, move |_, leader, event, cx| {
+                leader
+                    .read(cx)
+                    .add_event_to_update_proto(event, &mut *update.borrow_mut(), cx);
+            })
+            .detach();
+        }
+    });
+
+    let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
+        (
+            project
+                .create_buffer("abc\ndef\nghi\njkl\n", None, cx)
+                .unwrap(),
+            project
+                .create_buffer("mno\npqr\nstu\nvwx\n", None, cx)
+                .unwrap(),
+        )
+    });
+
+    // Insert some excerpts.
+    leader.update(cx, |leader, cx| {
+        leader.buffer.update(cx, |multibuffer, cx| {
+            let excerpt_ids = multibuffer.push_excerpts(
+                buffer_1.clone(),
+                [
+                    ExcerptRange {
+                        context: 1..6,
+                        primary: None,
+                    },
+                    ExcerptRange {
+                        context: 12..15,
+                        primary: None,
+                    },
+                    ExcerptRange {
+                        context: 0..3,
+                        primary: None,
+                    },
+                ],
+                cx,
+            );
+            multibuffer.insert_excerpts_after(
+                excerpt_ids[0],
+                buffer_2.clone(),
+                [
+                    ExcerptRange {
+                        context: 8..12,
+                        primary: None,
+                    },
+                    ExcerptRange {
+                        context: 0..6,
+                        primary: None,
+                    },
+                ],
+                cx,
+            );
+        });
+    });
+
+    // Apply the update of adding the excerpts.
+    follower_1
+        .update(cx, |follower, cx| {
+            follower.apply_update_proto(&project, update_message.borrow().clone().unwrap(), cx)
+        })
+        .await
+        .unwrap();
+    assert_eq!(
+        follower_1.read_with(cx, Editor::text),
+        leader.read_with(cx, Editor::text)
+    );
+    update_message.borrow_mut().take();
+
+    // Start following separately after it already has excerpts.
+    let mut state_message = leader.update(cx, |leader, cx| leader.to_state_proto(cx));
+    let follower_2 = cx
+        .update(|cx| {
+            Editor::from_state_proto(
+                pane.clone(),
+                project.clone(),
+                ViewId {
+                    creator: Default::default(),
+                    id: 0,
+                },
+                &mut state_message,
+                cx,
+            )
+        })
+        .unwrap()
+        .await
+        .unwrap();
+    assert_eq!(
+        follower_2.read_with(cx, Editor::text),
+        leader.read_with(cx, Editor::text)
+    );
+
+    // Remove some excerpts.
+    leader.update(cx, |leader, cx| {
+        leader.buffer.update(cx, |multibuffer, cx| {
+            let excerpt_ids = multibuffer.excerpt_ids();
+            multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
+            multibuffer.remove_excerpts([excerpt_ids[0]], cx);
+        });
+    });
+
+    // Apply the update of removing the excerpts.
+    follower_1
+        .update(cx, |follower, cx| {
+            follower.apply_update_proto(&project, update_message.borrow().clone().unwrap(), cx)
+        })
+        .await
+        .unwrap();
+    follower_2
+        .update(cx, |follower, cx| {
+            follower.apply_update_proto(&project, update_message.borrow().clone().unwrap(), cx)
+        })
+        .await
+        .unwrap();
+    update_message.borrow_mut().take();
+    assert_eq!(
+        follower_1.read_with(cx, Editor::text),
+        leader.read_with(cx, Editor::text)
+    );
+}
+
 #[test]
 fn test_combine_syntax_and_fuzzy_match_highlights() {
     let string = "abcdefghijklmnop";

crates/editor/src/items.rs 🔗

@@ -1,9 +1,18 @@
+use crate::{
+    display_map::ToDisplayPoint, link_go_to_definition::hide_link_definition,
+    movement::surrounding_word, persistence::DB, scroll::ScrollAnchor, Anchor, Autoscroll, Editor,
+    Event, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, NavigationData, ToPoint as _,
+    FORMAT_TIMEOUT,
+};
 use anyhow::{anyhow, Context, Result};
+use collections::HashSet;
+use futures::future::try_join_all;
 use futures::FutureExt;
 use gpui::{
     elements::*, geometry::vector::vec2f, AppContext, Entity, ModelHandle, MutableAppContext,
     RenderContext, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle,
 };
+use language::proto::serialize_anchor as serialize_text_anchor;
 use language::{Bias, Buffer, File as _, OffsetRangeExt, Point, SelectionGoal};
 use project::{File, FormatTrigger, Project, ProjectEntryId, ProjectPath};
 use rpc::proto::{self, update_view};
@@ -13,97 +22,136 @@ use std::{
     borrow::Cow,
     cmp::{self, Ordering},
     fmt::Write,
+    iter,
     ops::Range,
     path::{Path, PathBuf},
 };
 use text::Selection;
 use util::{ResultExt, TryFutureExt};
+use workspace::item::FollowableItemHandle;
 use workspace::{
     item::{FollowableItem, Item, ItemEvent, ItemHandle, ProjectItem},
     searchable::{Direction, SearchEvent, SearchableItem, SearchableItemHandle},
-    ItemId, ItemNavHistory, Pane, StatusItemView, ToolbarItemLocation, Workspace, WorkspaceId,
-};
-
-use crate::{
-    display_map::ToDisplayPoint, link_go_to_definition::hide_link_definition,
-    movement::surrounding_word, persistence::DB, scroll::ScrollAnchor, Anchor, Autoscroll, Editor,
-    Event, ExcerptId, MultiBuffer, MultiBufferSnapshot, NavigationData, ToPoint as _,
-    FORMAT_TIMEOUT,
+    ItemId, ItemNavHistory, Pane, StatusItemView, ToolbarItemLocation, ViewId, Workspace,
+    WorkspaceId,
 };
 
 pub const MAX_TAB_TITLE_LEN: usize = 24;
 
 impl FollowableItem for Editor {
+    fn remote_id(&self) -> Option<ViewId> {
+        self.remote_id
+    }
+
     fn from_state_proto(
         pane: ViewHandle<workspace::Pane>,
         project: ModelHandle<Project>,
+        remote_id: ViewId,
         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;
-        };
-
-        let buffer = project.update(cx, |project, cx| {
-            project.open_buffer_by_id(state.buffer_id, cx)
+        let Some(proto::view::Variant::Editor(_)) = state else { return None };
+        let Some(proto::view::Variant::Editor(state)) = state.take() else { unreachable!() };
+
+        let client = project.read(cx).client();
+        let replica_id = project.read(cx).replica_id();
+        let buffer_ids = state
+            .excerpts
+            .iter()
+            .map(|excerpt| excerpt.buffer_id)
+            .collect::<HashSet<_>>();
+        let buffers = project.update(cx, |project, cx| {
+            buffer_ids
+                .iter()
+                .map(|id| project.open_buffer_by_id(*id, cx))
+                .collect::<Vec<_>>()
         });
+
         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)
-                    })
+            let mut buffers = futures::future::try_join_all(buffers).await?;
+            let editor = pane.read_with(&cx, |pane, cx| {
+                let mut editors = pane.items_of_type::<Self>();
+                editors.find(|editor| {
+                    editor.remote_id(&client, cx) == Some(remote_id)
+                        || state.singleton
+                            && buffers.len() == 1
+                            && editor.read(cx).buffer.read(cx).as_singleton().as_ref()
+                                == Some(&buffers[0])
                 })
-                .unwrap_or_else(|| {
-                    pane.update(&mut cx, |_, cx| {
-                        cx.add_view(|cx| Editor::for_buffer(buffer, Some(project), cx))
-                    })
-                });
+            });
+
+            let editor = editor.unwrap_or_else(|| {
+                pane.update(&mut cx, |_, cx| {
+                    let multibuffer = cx.add_model(|cx| {
+                        let mut multibuffer;
+                        if state.singleton && buffers.len() == 1 {
+                            multibuffer = MultiBuffer::singleton(buffers.pop().unwrap(), cx)
+                        } else {
+                            multibuffer = MultiBuffer::new(replica_id);
+                            let mut excerpts = state.excerpts.into_iter().peekable();
+                            while let Some(excerpt) = excerpts.peek() {
+                                let buffer_id = excerpt.buffer_id;
+                                let buffer_excerpts = iter::from_fn(|| {
+                                    let excerpt = excerpts.peek()?;
+                                    (excerpt.buffer_id == buffer_id)
+                                        .then(|| excerpts.next().unwrap())
+                                });
+                                let buffer =
+                                    buffers.iter().find(|b| b.read(cx).remote_id() == buffer_id);
+                                if let Some(buffer) = buffer {
+                                    multibuffer.push_excerpts(
+                                        buffer.clone(),
+                                        buffer_excerpts.filter_map(deserialize_excerpt_range),
+                                        cx,
+                                    );
+                                }
+                            }
+                        };
+
+                        if let Some(title) = &state.title {
+                            multibuffer = multibuffer.with_title(title.clone())
+                        }
+
+                        multibuffer
+                    });
+
+                    cx.add_view(|cx| Editor::for_multibuffer(multibuffer, 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;
-                }
+                editor.remote_id = Some(remote_id);
+                let buffer = editor.buffer.read(cx).read(cx);
                 let selections = state
                     .selections
                     .into_iter()
                     .map(|selection| {
-                        deserialize_selection(&excerpt_id, buffer_id, selection)
+                        deserialize_selection(&buffer, selection)
                             .ok_or_else(|| anyhow!("invalid selection"))
                     })
                     .collect::<Result<Vec<_>>>()?;
+                let scroll_top_anchor = state
+                    .scroll_top_anchor
+                    .and_then(|anchor| deserialize_anchor(&buffer, anchor));
+                drop(buffer);
+
                 if !selections.is_empty() {
                     editor.set_selections_from_remote(selections, cx);
                 }
 
-                if let Some(anchor) = state.scroll_top_anchor {
+                if let Some(scroll_top_anchor) = scroll_top_anchor {
                     editor.set_scroll_anchor_remote(
                         ScrollAnchor {
-                            top_anchor: Anchor {
-                                buffer_id: Some(state.buffer_id as usize),
-                                excerpt_id,
-                                text_anchor: language::proto::deserialize_anchor(anchor)
-                                    .ok_or_else(|| anyhow!("invalid scroll top"))?,
-                            },
+                            top_anchor: scroll_top_anchor,
                             offset: vec2f(state.scroll_x, state.scroll_y),
                         },
                         cx,
                     );
                 }
 
-                Ok::<_, anyhow::Error>(())
+                anyhow::Ok(())
             })?;
+
             Ok(editor)
         }))
     }
@@ -134,13 +182,32 @@ impl FollowableItem for 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();
+        let buffer = self.buffer.read(cx);
         let scroll_anchor = self.scroll_manager.anchor();
+        let excerpts = buffer
+            .read(cx)
+            .excerpts()
+            .map(|(id, buffer, range)| proto::Excerpt {
+                id: id.to_proto(),
+                buffer_id: buffer.remote_id(),
+                context_start: Some(serialize_text_anchor(&range.context.start)),
+                context_end: Some(serialize_text_anchor(&range.context.end)),
+                primary_start: range
+                    .primary
+                    .as_ref()
+                    .map(|range| serialize_text_anchor(&range.start)),
+                primary_end: range
+                    .primary
+                    .as_ref()
+                    .map(|range| serialize_text_anchor(&range.end)),
+            })
+            .collect();
+
         Some(proto::view::Variant::Editor(proto::view::Editor {
-            buffer_id,
-            scroll_top_anchor: Some(language::proto::serialize_anchor(
-                &scroll_anchor.top_anchor.text_anchor,
-            )),
+            singleton: buffer.is_singleton(),
+            title: (!buffer.is_singleton()).then(|| buffer.title(cx).into()),
+            excerpts,
+            scroll_top_anchor: Some(serialize_anchor(&scroll_anchor.top_anchor)),
             scroll_x: scroll_anchor.offset.x(),
             scroll_y: scroll_anchor.offset.y(),
             selections: self
@@ -156,18 +223,43 @@ impl FollowableItem for Editor {
         &self,
         event: &Self::Event,
         update: &mut Option<proto::update_view::Variant>,
-        _: &AppContext,
+        cx: &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::ExcerptsAdded {
+                    buffer,
+                    predecessor,
+                    excerpts,
+                } => {
+                    let buffer_id = buffer.read(cx).remote_id();
+                    let mut excerpts = excerpts.iter();
+                    if let Some((id, range)) = excerpts.next() {
+                        update.inserted_excerpts.push(proto::ExcerptInsertion {
+                            previous_excerpt_id: Some(predecessor.to_proto()),
+                            excerpt: serialize_excerpt(buffer_id, id, range),
+                        });
+                        update.inserted_excerpts.extend(excerpts.map(|(id, range)| {
+                            proto::ExcerptInsertion {
+                                previous_excerpt_id: None,
+                                excerpt: serialize_excerpt(buffer_id, id, range),
+                            }
+                        }))
+                    }
+                    true
+                }
+                Event::ExcerptsRemoved { ids } => {
+                    update
+                        .deleted_excerpts
+                        .extend(ids.iter().map(ExcerptId::to_proto));
+                    true
+                }
                 Event::ScrollPositionChanged { .. } => {
                     let scroll_anchor = self.scroll_manager.anchor();
-                    update.scroll_top_anchor = Some(language::proto::serialize_anchor(
-                        &scroll_anchor.top_anchor.text_anchor,
-                    ));
+                    update.scroll_top_anchor = Some(serialize_anchor(&scroll_anchor.top_anchor));
                     update.scroll_x = scroll_anchor.offset.x();
                     update.scroll_y = scroll_anchor.offset.y();
                     true
@@ -189,45 +281,98 @@ impl FollowableItem for Editor {
 
     fn apply_update_proto(
         &mut self,
+        project: &ModelHandle<Project>,
         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);
+    ) -> Task<Result<()>> {
+        let update_view::Variant::Editor(message) = message;
+        let multibuffer = self.buffer.read(cx);
+        let multibuffer = multibuffer.read(cx);
+
+        let buffer_ids = message
+            .inserted_excerpts
+            .iter()
+            .filter_map(|insertion| Some(insertion.excerpt.as_ref()?.buffer_id))
+            .collect::<HashSet<_>>();
+
+        let mut removals = message
+            .deleted_excerpts
+            .into_iter()
+            .map(ExcerptId::from_proto)
+            .collect::<Vec<_>>();
+        removals.sort_by(|a, b| a.cmp(&b, &multibuffer));
 
-                let selections = message
-                    .selections
-                    .into_iter()
-                    .filter_map(|selection| {
-                        deserialize_selection(&excerpt_id, buffer_id, selection)
-                    })
-                    .collect::<Vec<_>>();
+        let selections = message
+            .selections
+            .into_iter()
+            .filter_map(|selection| deserialize_selection(&multibuffer, selection))
+            .collect::<Vec<_>>();
+        let scroll_top_anchor = message
+            .scroll_top_anchor
+            .and_then(|anchor| deserialize_anchor(&multibuffer, anchor));
+        drop(multibuffer);
+
+        let buffers = project.update(cx, |project, cx| {
+            buffer_ids
+                .into_iter()
+                .map(|id| project.open_buffer_by_id(id, cx))
+                .collect::<Vec<_>>()
+        });
+
+        let project = project.clone();
+        cx.spawn(|this, mut cx| async move {
+            let _buffers = try_join_all(buffers).await?;
+            this.update(&mut cx, |this, cx| {
+                this.buffer.update(cx, |multibuffer, cx| {
+                    let mut insertions = message.inserted_excerpts.into_iter().peekable();
+                    while let Some(insertion) = insertions.next() {
+                        let Some(excerpt) = insertion.excerpt else { continue };
+                        let Some(previous_excerpt_id) = insertion.previous_excerpt_id else { continue };
+                        let buffer_id = excerpt.buffer_id;
+                        let Some(buffer) = project.read(cx).buffer_for_id(buffer_id, cx) else { continue };
+
+                        let adjacent_excerpts = iter::from_fn(|| {
+                            let insertion = insertions.peek()?;
+                            if insertion.previous_excerpt_id.is_none()
+                                && insertion.excerpt.as_ref()?.buffer_id == buffer_id
+                            {
+                                insertions.next()?.excerpt
+                            } else {
+                                None
+                            }
+                        });
+
+                        multibuffer.insert_excerpts_with_ids_after(
+                            ExcerptId::from_proto(previous_excerpt_id),
+                            buffer,
+                            [excerpt]
+                                .into_iter()
+                                .chain(adjacent_excerpts)
+                                .filter_map(|excerpt| {
+                                    Some((
+                                        ExcerptId::from_proto(excerpt.id),
+                                        deserialize_excerpt_range(excerpt)?,
+                                    ))
+                                }),
+                            cx,
+                        );
+                    }
+
+                    multibuffer.remove_excerpts(removals, cx);
+                });
 
                 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_anchor_remote(
-                        ScrollAnchor {
-                            top_anchor: Anchor {
-                                buffer_id: Some(buffer_id),
-                                excerpt_id,
-                                text_anchor: language::proto::deserialize_anchor(anchor)
-                                    .ok_or_else(|| anyhow!("invalid scroll top"))?,
-                            },
-                            offset: vec2f(message.scroll_x, message.scroll_y),
-                        },
-                        cx,
-                    );
+                    this.set_selections_from_remote(selections, cx);
+                    this.request_autoscroll_remotely(Autoscroll::newest(), cx);
+                } else if let Some(anchor) = scroll_top_anchor {
+                    this.set_scroll_anchor_remote(ScrollAnchor {
+                        top_anchor: anchor,
+                        offset: vec2f(message.scroll_x, message.scroll_y)
+                    }, cx);
                 }
-            }
-        }
-        Ok(())
+            });
+            Ok(())
+        })
     }
 
     fn should_unfollow_on_event(event: &Self::Event, _: &AppContext) -> bool {
@@ -240,41 +385,82 @@ impl FollowableItem for Editor {
     }
 }
 
+fn serialize_excerpt(
+    buffer_id: u64,
+    id: &ExcerptId,
+    range: &ExcerptRange<language::Anchor>,
+) -> Option<proto::Excerpt> {
+    Some(proto::Excerpt {
+        id: id.to_proto(),
+        buffer_id,
+        context_start: Some(serialize_text_anchor(&range.context.start)),
+        context_end: Some(serialize_text_anchor(&range.context.end)),
+        primary_start: range
+            .primary
+            .as_ref()
+            .map(|r| serialize_text_anchor(&r.start)),
+        primary_end: range
+            .primary
+            .as_ref()
+            .map(|r| serialize_text_anchor(&r.end)),
+    })
+}
+
 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,
-        )),
+        start: Some(serialize_anchor(&selection.start)),
+        end: Some(serialize_anchor(&selection.end)),
         reversed: selection.reversed,
     }
 }
 
+fn serialize_anchor(anchor: &Anchor) -> proto::EditorAnchor {
+    proto::EditorAnchor {
+        excerpt_id: anchor.excerpt_id.to_proto(),
+        anchor: Some(serialize_text_anchor(&anchor.text_anchor)),
+    }
+}
+
+fn deserialize_excerpt_range(excerpt: proto::Excerpt) -> Option<ExcerptRange<language::Anchor>> {
+    let context = {
+        let start = language::proto::deserialize_anchor(excerpt.context_start?)?;
+        let end = language::proto::deserialize_anchor(excerpt.context_end?)?;
+        start..end
+    };
+    let primary = excerpt
+        .primary_start
+        .zip(excerpt.primary_end)
+        .and_then(|(start, end)| {
+            let start = language::proto::deserialize_anchor(start)?;
+            let end = language::proto::deserialize_anchor(end)?;
+            Some(start..end)
+        });
+    Some(ExcerptRange { context, primary })
+}
+
 fn deserialize_selection(
-    excerpt_id: &ExcerptId,
-    buffer_id: usize,
+    buffer: &MultiBufferSnapshot,
     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?)?,
-        },
+        start: deserialize_anchor(buffer, selection.start?)?,
+        end: deserialize_anchor(buffer, selection.end?)?,
         reversed: selection.reversed,
         goal: SelectionGoal::None,
     })
 }
 
+fn deserialize_anchor(buffer: &MultiBufferSnapshot, anchor: proto::EditorAnchor) -> Option<Anchor> {
+    let excerpt_id = ExcerptId::from_proto(anchor.excerpt_id);
+    Some(Anchor {
+        excerpt_id,
+        text_anchor: language::proto::deserialize_anchor(anchor.anchor?)?,
+        buffer_id: buffer.buffer_id_for_excerpt(excerpt_id),
+    })
+}
+
 impl Item for Editor {
     fn navigate(&mut self, data: Box<dyn std::any::Any>, cx: &mut ViewContext<Self>) -> bool {
         if let Ok(data) = data.downcast::<NavigationData>() {

crates/editor/src/multi_buffer.rs 🔗

@@ -9,9 +9,9 @@ use gpui::{AppContext, Entity, ModelContext, ModelHandle, Task};
 pub use language::Completion;
 use language::{
     char_kind, AutoindentMode, Buffer, BufferChunks, BufferSnapshot, CharKind, Chunk, CursorShape,
-    DiagnosticEntry, Event, File, IndentSize, Language, OffsetRangeExt, OffsetUtf16, Outline,
-    OutlineItem, Point, PointUtf16, Selection, TextDimension, ToOffset as _, ToOffsetUtf16 as _,
-    ToPoint as _, ToPointUtf16 as _, TransactionId, Unclipped,
+    DiagnosticEntry, File, IndentSize, Language, OffsetRangeExt, OffsetUtf16, Outline, OutlineItem,
+    Point, PointUtf16, Selection, TextDimension, ToOffset as _, ToOffsetUtf16 as _, ToPoint as _,
+    ToPointUtf16 as _, TransactionId, Unclipped,
 };
 use smallvec::SmallVec;
 use std::{
@@ -50,6 +50,26 @@ pub struct MultiBuffer {
     title: Option<String>,
 }
 
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub enum Event {
+    ExcerptsAdded {
+        buffer: ModelHandle<Buffer>,
+        predecessor: ExcerptId,
+        excerpts: Vec<(ExcerptId, ExcerptRange<language::Anchor>)>,
+    },
+    ExcerptsRemoved {
+        ids: Vec<ExcerptId>,
+    },
+    Edited,
+    Reloaded,
+    Reparsed,
+    Saved,
+    FileHandleChanged,
+    Closed,
+    DirtyChanged,
+    DiagnosticsUpdated,
+}
+
 #[derive(Clone)]
 struct History {
     next_transaction_id: TransactionId,
@@ -833,6 +853,30 @@ impl MultiBuffer {
     ) -> Vec<ExcerptId>
     where
         O: text::ToOffset,
+    {
+        let mut ids = Vec::new();
+        let mut next_excerpt_id = self.next_excerpt_id;
+        self.insert_excerpts_with_ids_after(
+            prev_excerpt_id,
+            buffer,
+            ranges.into_iter().map(|range| {
+                let id = ExcerptId(post_inc(&mut next_excerpt_id));
+                ids.push(id);
+                (id, range)
+            }),
+            cx,
+        );
+        ids
+    }
+
+    pub fn insert_excerpts_with_ids_after<O>(
+        &mut self,
+        prev_excerpt_id: ExcerptId,
+        buffer: ModelHandle<Buffer>,
+        ranges: impl IntoIterator<Item = (ExcerptId, ExcerptRange<O>)>,
+        cx: &mut ModelContext<Self>,
+    ) where
+        O: text::ToOffset,
     {
         assert_eq!(self.history.transaction_depth, 0);
         let mut ranges = ranges.into_iter().peekable();
@@ -858,7 +902,7 @@ impl MultiBuffer {
                 cx.observe(&buffer, |_, _, cx| cx.notify()),
                 cx.subscribe(&buffer, Self::on_buffer_event),
             ],
-            buffer,
+            buffer: buffer.clone(),
         });
 
         let mut snapshot = self.snapshot.borrow_mut();
@@ -883,8 +927,8 @@ impl MultiBuffer {
             Locator::max()
         };
 
-        let mut ids = Vec::new();
-        while let Some(range) = ranges.next() {
+        let mut excerpts = Vec::new();
+        while let Some((id, range)) = ranges.next() {
             let locator = Locator::between(&prev_locator, &next_locator);
             if let Err(ix) = buffer_state.excerpts.binary_search(&locator) {
                 buffer_state.excerpts.insert(ix, locator.clone());
@@ -897,7 +941,10 @@ impl MultiBuffer {
                         ..buffer_snapshot.anchor_after(&primary.end)
                 }),
             };
-            let id = ExcerptId(post_inc(&mut self.next_excerpt_id));
+            if id.0 >= self.next_excerpt_id {
+                self.next_excerpt_id = id.0 + 1;
+            }
+            excerpts.push((id, range.clone()));
             let excerpt = Excerpt::new(
                 id,
                 locator.clone(),
@@ -909,7 +956,6 @@ impl MultiBuffer {
             new_excerpts.push(excerpt, &());
             prev_locator = locator.clone();
             new_excerpt_ids.push(ExcerptIdMapping { id, locator }, &());
-            ids.push(id);
         }
 
         let edit_end = new_excerpts.summary().text.len;
@@ -929,12 +975,17 @@ impl MultiBuffer {
             new: edit_start..edit_end,
         }]);
         cx.emit(Event::Edited);
+        cx.emit(Event::ExcerptsAdded {
+            buffer,
+            predecessor: prev_excerpt_id,
+            excerpts,
+        });
         cx.notify();
-        ids
     }
 
     pub fn clear(&mut self, cx: &mut ModelContext<Self>) {
         self.sync(cx);
+        let ids = self.excerpt_ids();
         self.buffers.borrow_mut().clear();
         let mut snapshot = self.snapshot.borrow_mut();
         let prev_len = snapshot.len();
@@ -948,6 +999,7 @@ impl MultiBuffer {
             new: 0..0,
         }]);
         cx.emit(Event::Edited);
+        cx.emit(Event::ExcerptsRemoved { ids });
         cx.notify();
     }
 
@@ -1071,12 +1123,14 @@ impl MultiBuffer {
         cx: &mut ModelContext<Self>,
     ) {
         self.sync(cx);
+        let ids = excerpt_ids.into_iter().collect::<Vec<_>>();
+
         let mut buffers = self.buffers.borrow_mut();
         let mut snapshot = self.snapshot.borrow_mut();
         let mut new_excerpts = SumTree::new();
         let mut cursor = snapshot.excerpts.cursor::<(Option<&Locator>, usize)>();
         let mut edits = Vec::new();
-        let mut excerpt_ids = excerpt_ids.into_iter().peekable();
+        let mut excerpt_ids = ids.iter().copied().peekable();
 
         while let Some(excerpt_id) = excerpt_ids.next() {
             // Seek to the next excerpt to remove, preserving any preceding excerpts.
@@ -1143,6 +1197,7 @@ impl MultiBuffer {
 
         self.subscriptions.publish_mut(edits);
         cx.emit(Event::Edited);
+        cx.emit(Event::ExcerptsRemoved { ids });
         cx.notify();
     }
 
@@ -1165,10 +1220,22 @@ impl MultiBuffer {
     fn on_buffer_event(
         &mut self,
         _: ModelHandle<Buffer>,
-        event: &Event,
+        event: &language::Event,
         cx: &mut ModelContext<Self>,
     ) {
-        cx.emit(event.clone());
+        cx.emit(match event {
+            language::Event::Edited => Event::Edited,
+            language::Event::DirtyChanged => Event::DirtyChanged,
+            language::Event::Saved => Event::Saved,
+            language::Event::FileHandleChanged => Event::FileHandleChanged,
+            language::Event::Reloaded => Event::Reloaded,
+            language::Event::Reparsed => Event::Reparsed,
+            language::Event::DiagnosticsUpdated => Event::DiagnosticsUpdated,
+            language::Event::Closed => Event::Closed,
+
+            //
+            language::Event::Operation(_) => return,
+        });
     }
 
     pub fn all_buffers(&self) -> HashSet<ModelHandle<Buffer>> {
@@ -1604,7 +1671,7 @@ impl MultiBuffer {
 }
 
 impl Entity for MultiBuffer {
-    type Event = language::Event;
+    type Event = Event;
 }
 
 impl MultiBufferSnapshot {
@@ -2450,6 +2517,14 @@ impl MultiBufferSnapshot {
         }
     }
 
+    pub fn excerpts(
+        &self,
+    ) -> impl Iterator<Item = (ExcerptId, &BufferSnapshot, ExcerptRange<text::Anchor>)> {
+        self.excerpts
+            .iter()
+            .map(|excerpt| (excerpt.id, &excerpt.buffer, excerpt.range.clone()))
+    }
+
     pub fn excerpt_boundaries_in_range<R, T>(
         &self,
         range: R,
@@ -2746,6 +2821,10 @@ impl MultiBufferSnapshot {
         }
     }
 
+    pub fn buffer_id_for_excerpt(&self, excerpt_id: ExcerptId) -> Option<usize> {
+        Some(self.excerpt(excerpt_id)?.buffer_id)
+    }
+
     fn excerpt<'a>(&'a self, excerpt_id: ExcerptId) -> Option<&'a Excerpt> {
         let mut cursor = self.excerpts.cursor::<Option<&Locator>>();
         let locator = self.excerpt_locator_for_id(excerpt_id);
@@ -3080,6 +3159,14 @@ impl ExcerptId {
         Self(usize::MAX)
     }
 
+    pub fn to_proto(&self) -> u64 {
+        self.0 as _
+    }
+
+    pub fn from_proto(proto: u64) -> Self {
+        Self(proto as _)
+    }
+
     pub fn cmp(&self, other: &Self, snapshot: &MultiBufferSnapshot) -> cmp::Ordering {
         let a = snapshot.excerpt_locator_for_id(*self);
         let b = snapshot.excerpt_locator_for_id(*other);
@@ -3468,7 +3555,7 @@ mod tests {
     use util::test::sample_text;
 
     #[gpui::test]
-    fn test_singleton_multibuffer(cx: &mut MutableAppContext) {
+    fn test_singleton(cx: &mut MutableAppContext) {
         let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(6, 6, 'a'), cx));
         let multibuffer = cx.add_model(|cx| MultiBuffer::singleton(buffer.clone(), cx));
 
@@ -3495,7 +3582,7 @@ mod tests {
     }
 
     #[gpui::test]
-    fn test_remote_multibuffer(cx: &mut MutableAppContext) {
+    fn test_remote(cx: &mut MutableAppContext) {
         let host_buffer = cx.add_model(|cx| Buffer::new(0, "a", cx));
         let guest_buffer = cx.add_model(|cx| {
             let state = host_buffer.read(cx).to_proto();
@@ -3526,7 +3613,7 @@ mod tests {
     }
 
     #[gpui::test]
-    fn test_excerpt_buffer(cx: &mut MutableAppContext) {
+    fn test_excerpt_boundaries_and_clipping(cx: &mut MutableAppContext) {
         let buffer_1 = cx.add_model(|cx| Buffer::new(0, sample_text(6, 6, 'a'), cx));
         let buffer_2 = cx.add_model(|cx| Buffer::new(0, sample_text(6, 6, 'g'), cx));
         let multibuffer = cx.add_model(|_| MultiBuffer::new(0));
@@ -3535,7 +3622,9 @@ mod tests {
         multibuffer.update(cx, |_, cx| {
             let events = events.clone();
             cx.subscribe(&multibuffer, move |_, _, event, _| {
-                events.borrow_mut().push(event.clone())
+                if let Event::Edited = event {
+                    events.borrow_mut().push(event.clone())
+                }
             })
             .detach();
         });
@@ -3748,7 +3837,84 @@ mod tests {
     }
 
     #[gpui::test]
-    fn test_excerpts_with_context_lines(cx: &mut MutableAppContext) {
+    fn test_excerpt_events(cx: &mut MutableAppContext) {
+        let buffer_1 = cx.add_model(|cx| Buffer::new(0, sample_text(10, 3, 'a'), cx));
+        let buffer_2 = cx.add_model(|cx| Buffer::new(0, sample_text(10, 3, 'm'), cx));
+
+        let leader_multibuffer = cx.add_model(|_| MultiBuffer::new(0));
+        let follower_multibuffer = cx.add_model(|_| MultiBuffer::new(0));
+
+        follower_multibuffer.update(cx, |_, cx| {
+            cx.subscribe(&leader_multibuffer, |follower, _, event, cx| {
+                match event.clone() {
+                    Event::ExcerptsAdded {
+                        buffer,
+                        predecessor,
+                        excerpts,
+                    } => follower.insert_excerpts_with_ids_after(predecessor, buffer, excerpts, cx),
+                    Event::ExcerptsRemoved { ids } => follower.remove_excerpts(ids, cx),
+                    _ => {}
+                }
+            })
+            .detach();
+        });
+
+        leader_multibuffer.update(cx, |leader, cx| {
+            leader.push_excerpts(
+                buffer_1.clone(),
+                [
+                    ExcerptRange {
+                        context: 0..8,
+                        primary: None,
+                    },
+                    ExcerptRange {
+                        context: 12..16,
+                        primary: None,
+                    },
+                ],
+                cx,
+            );
+            leader.insert_excerpts_after(
+                leader.excerpt_ids()[0],
+                buffer_2.clone(),
+                [
+                    ExcerptRange {
+                        context: 0..5,
+                        primary: None,
+                    },
+                    ExcerptRange {
+                        context: 10..15,
+                        primary: None,
+                    },
+                ],
+                cx,
+            )
+        });
+        assert_eq!(
+            leader_multibuffer.read(cx).snapshot(cx).text(),
+            follower_multibuffer.read(cx).snapshot(cx).text(),
+        );
+
+        leader_multibuffer.update(cx, |leader, cx| {
+            let excerpt_ids = leader.excerpt_ids();
+            leader.remove_excerpts([excerpt_ids[1], excerpt_ids[3]], cx);
+        });
+        assert_eq!(
+            leader_multibuffer.read(cx).snapshot(cx).text(),
+            follower_multibuffer.read(cx).snapshot(cx).text(),
+        );
+
+        leader_multibuffer.update(cx, |leader, cx| {
+            leader.clear(cx);
+        });
+        assert_eq!(
+            leader_multibuffer.read(cx).snapshot(cx).text(),
+            follower_multibuffer.read(cx).snapshot(cx).text(),
+        );
+    }
+
+    #[gpui::test]
+    fn test_push_excerpts_with_context_lines(cx: &mut MutableAppContext) {
         let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(20, 3, 'a'), cx));
         let multibuffer = cx.add_model(|_| MultiBuffer::new(0));
         let anchor_ranges = multibuffer.update(cx, |multibuffer, cx| {
@@ -3784,7 +3950,7 @@ mod tests {
     }
 
     #[gpui::test]
-    fn test_empty_excerpt_buffer(cx: &mut MutableAppContext) {
+    fn test_empty_multibuffer(cx: &mut MutableAppContext) {
         let multibuffer = cx.add_model(|_| MultiBuffer::new(0));
 
         let snapshot = multibuffer.read(cx).snapshot(cx);
@@ -3872,9 +4038,7 @@ mod tests {
     }
 
     #[gpui::test]
-    fn test_multibuffer_resolving_anchors_after_replacing_their_excerpts(
-        cx: &mut MutableAppContext,
-    ) {
+    fn test_resolving_anchors_after_replacing_their_excerpts(cx: &mut MutableAppContext) {
         let buffer_1 = cx.add_model(|cx| Buffer::new(0, "abcd", cx));
         let buffer_2 = cx.add_model(|cx| Buffer::new(0, "ABCDEFGHIJKLMNOP", cx));
         let multibuffer = cx.add_model(|_| MultiBuffer::new(0));

crates/language/src/proto.rs 🔗

@@ -9,7 +9,7 @@ use rpc::proto;
 use std::{ops::Range, sync::Arc};
 use text::*;
 
-pub use proto::{BufferState, Operation, SelectionSet};
+pub use proto::{BufferState, Operation};
 
 pub fn deserialize_line_ending(message: proto::LineEnding) -> fs::LineEnding {
     match message {
@@ -122,8 +122,14 @@ pub fn serialize_selections(selections: &Arc<[Selection<Anchor>]>) -> Vec<proto:
 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)),
+        start: Some(proto::EditorAnchor {
+            anchor: Some(serialize_anchor(&selection.start)),
+            excerpt_id: 0,
+        }),
+        end: Some(proto::EditorAnchor {
+            anchor: Some(serialize_anchor(&selection.end)),
+            excerpt_id: 0,
+        }),
         reversed: selection.reversed,
     }
 }
@@ -229,8 +235,8 @@ pub fn deserialize_operation(message: proto::Operation) -> Result<crate::Operati
                     .filter_map(|selection| {
                         Some(Selection {
                             id: selection.id as usize,
-                            start: deserialize_anchor(selection.start?)?,
-                            end: deserialize_anchor(selection.end?)?,
+                            start: deserialize_anchor(selection.start?.anchor?)?,
+                            end: deserialize_anchor(selection.end?.anchor?)?,
                             reversed: selection.reversed,
                             goal: SelectionGoal::None,
                         })
@@ -321,8 +327,8 @@ pub fn deserialize_selections(selections: Vec<proto::Selection>) -> Arc<[Selecti
 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?)?,
+        start: deserialize_anchor(selection.start?.anchor?)?,
+        end: deserialize_anchor(selection.end?.anchor?)?,
         reversed: selection.reversed,
         goal: SelectionGoal::None,
     })

crates/rpc/proto/zed.proto 🔗

@@ -803,7 +803,7 @@ message Follow {
 }
 
 message FollowResponse {
-    optional uint64 active_view_id = 1;
+    optional ViewId active_view_id = 1;
     repeated View views = 2;
 }
 
@@ -831,13 +831,18 @@ message GetPrivateUserInfoResponse {
 
 // Entities
 
+message ViewId {
+    PeerId creator = 1;
+    uint64 id = 2;
+}
+
 message UpdateActiveView {
-    optional uint64 id = 1;
+    optional ViewId id = 1;
     optional PeerId leader_id = 2;
 }
 
 message UpdateView {
-    uint64 id = 1;
+    ViewId id = 1;
     optional PeerId leader_id = 2;
 
     oneof variant {
@@ -845,15 +850,17 @@ message UpdateView {
     }
 
     message Editor {
-        repeated Selection selections = 1;
-        Anchor scroll_top_anchor = 2;
-        float scroll_x = 3;
-        float scroll_y = 4;
+        repeated ExcerptInsertion inserted_excerpts = 1;
+        repeated uint64 deleted_excerpts = 2;
+        repeated Selection selections = 3;
+        EditorAnchor scroll_top_anchor = 4;
+        float scroll_x = 5;
+        float scroll_y = 6;
     }
 }
 
 message View {
-    uint64 id = 1;
+    ViewId id = 1;
     optional PeerId leader_id = 2;
 
     oneof variant {
@@ -861,11 +868,13 @@ message View {
     }
 
     message Editor {
-        uint64 buffer_id = 1;
-        repeated Selection selections = 2;
-        Anchor scroll_top_anchor = 3;
-        float scroll_x = 4;
-        float scroll_y = 5;
+        bool singleton = 1;
+        optional string title = 2;
+        repeated Excerpt excerpts = 3;
+        repeated Selection selections = 4;
+        EditorAnchor scroll_top_anchor = 5;
+        float scroll_x = 6;
+        float scroll_y = 7;
     }
 }
 
@@ -918,21 +927,18 @@ enum LineEnding {
     Windows = 1;
 }
 
-message SelectionSet {
-    uint32 replica_id = 1;
-    repeated Selection selections = 2;
-    uint32 lamport_timestamp = 3;
-    bool line_mode = 4;
-    CursorShape cursor_shape = 5;
-}
-
 message Selection {
     uint64 id = 1;
-    Anchor start = 2;
-    Anchor end = 3;
+    EditorAnchor start = 2;
+    EditorAnchor end = 3;
     bool reversed = 4;
 }
 
+message EditorAnchor {
+    uint64 excerpt_id = 1;
+    Anchor anchor = 2;
+}
+
 enum CursorShape {
     CursorBar = 0;
     CursorBlock = 1;
@@ -940,6 +946,20 @@ enum CursorShape {
     CursorHollow = 3;
 }
 
+message ExcerptInsertion {
+    Excerpt excerpt = 1;
+    optional uint64 previous_excerpt_id = 2;
+}
+
+message Excerpt {
+    uint64 id = 1;
+    uint64 buffer_id = 2;
+    Anchor context_start = 3;
+    Anchor context_end = 4;
+    Anchor primary_start = 5;
+    Anchor primary_end = 6;
+}
+
 message Anchor {
     uint32 replica_id = 1;
     uint32 local_timestamp = 2;

crates/search/src/project_search.rs 🔗

@@ -402,7 +402,7 @@ impl ProjectSearchView {
         });
         // Subcribe to query_editor in order to reraise editor events for workspace item activation purposes
         cx.subscribe(&query_editor, |_, _, event, cx| {
-            cx.emit(ViewEvent::EditorEvent(*event))
+            cx.emit(ViewEvent::EditorEvent(event.clone()))
         })
         .detach();
 
@@ -419,7 +419,7 @@ impl ProjectSearchView {
                 this.update_match_index(cx);
             }
             // Reraise editor events for workspace item activation purposes
-            cx.emit(ViewEvent::EditorEvent(*event));
+            cx.emit(ViewEvent::EditorEvent(event.clone()));
         })
         .detach();
 

crates/text/src/text.rs 🔗

@@ -1496,6 +1496,10 @@ impl BufferSnapshot {
         &self.visible_text
     }
 
+    pub fn remote_id(&self) -> u64 {
+        self.remote_id
+    }
+
     pub fn replica_id(&self) -> ReplicaId {
         self.replica_id
     }

crates/workspace/src/item.rs 🔗

@@ -5,12 +5,15 @@ use std::{
     fmt,
     path::PathBuf,
     rc::Rc,
-    sync::atomic::{AtomicBool, Ordering},
+    sync::{
+        atomic::{AtomicBool, Ordering},
+        Arc,
+    },
     time::Duration,
 };
 
 use anyhow::Result;
-use client::proto;
+use client::{proto, Client};
 use gpui::{
     AnyViewHandle, AppContext, ElementBox, ModelHandle, MutableAppContext, Task, View, ViewContext,
     ViewHandle, WeakViewHandle,
@@ -23,7 +26,8 @@ use util::ResultExt;
 
 use crate::{
     pane, persistence::model::ItemId, searchable::SearchableItemHandle, DelayedDebouncedEditAction,
-    FollowableItemBuilders, ItemNavHistory, Pane, ToolbarItemLocation, Workspace, WorkspaceId,
+    FollowableItemBuilders, ItemNavHistory, Pane, ToolbarItemLocation, ViewId, Workspace,
+    WorkspaceId,
 };
 
 #[derive(Eq, PartialEq, Hash)]
@@ -278,7 +282,9 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
             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,
+                        id: followed_item
+                            .remote_id(&workspace.client, cx)
+                            .map(|id| id.to_proto()),
                         variant: Some(message),
                         leader_id: workspace.leader_for_pane(&pane),
                     }),
@@ -332,7 +338,9 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
                                     this.update_followers(
                                         proto::update_followers::Variant::UpdateView(
                                             proto::UpdateView {
-                                                id: item.id() as u64,
+                                                id: item
+                                                    .remote_id(&this.client, cx)
+                                                    .map(|id| id.to_proto()),
                                                 variant: pending_update.borrow_mut().take(),
                                                 leader_id,
                                             },
@@ -584,10 +592,12 @@ pub trait ProjectItem: Item {
 }
 
 pub trait FollowableItem: Item {
+    fn remote_id(&self) -> Option<ViewId>;
     fn to_state_proto(&self, cx: &AppContext) -> Option<proto::view::Variant>;
     fn from_state_proto(
         pane: ViewHandle<Pane>,
         project: ModelHandle<Project>,
+        id: ViewId,
         state: &mut Option<proto::view::Variant>,
         cx: &mut MutableAppContext,
     ) -> Option<Task<Result<ViewHandle<Self>>>>;
@@ -599,15 +609,17 @@ pub trait FollowableItem: Item {
     ) -> bool;
     fn apply_update_proto(
         &mut self,
+        project: &ModelHandle<Project>,
         message: proto::update_view::Variant,
         cx: &mut ViewContext<Self>,
-    ) -> Result<()>;
+    ) -> Task<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 remote_id(&self, client: &Arc<Client>, cx: &AppContext) -> Option<ViewId>;
     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(
@@ -618,13 +630,23 @@ pub trait FollowableItemHandle: ItemHandle {
     ) -> bool;
     fn apply_update_proto(
         &self,
+        project: &ModelHandle<Project>,
         message: proto::update_view::Variant,
         cx: &mut MutableAppContext,
-    ) -> Result<()>;
+    ) -> Task<Result<()>>;
     fn should_unfollow_on_event(&self, event: &dyn Any, cx: &AppContext) -> bool;
 }
 
 impl<T: FollowableItem> FollowableItemHandle for ViewHandle<T> {
+    fn remote_id(&self, client: &Arc<Client>, cx: &AppContext) -> Option<ViewId> {
+        self.read(cx).remote_id().or_else(|| {
+            client.peer_id().map(|creator| ViewId {
+                creator,
+                id: self.id() as u64,
+            })
+        })
+    }
+
     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)
@@ -650,10 +672,11 @@ impl<T: FollowableItem> FollowableItemHandle for ViewHandle<T> {
 
     fn apply_update_proto(
         &self,
+        project: &ModelHandle<Project>,
         message: proto::update_view::Variant,
         cx: &mut MutableAppContext,
-    ) -> Result<()> {
-        self.update(cx, |this, cx| this.apply_update_proto(message, cx))
+    ) -> Task<Result<()>> {
+        self.update(cx, |this, cx| this.apply_update_proto(project, message, cx))
     }
 
     fn should_unfollow_on_event(&self, event: &dyn Any, cx: &AppContext) -> bool {

crates/workspace/src/workspace.rs 🔗

@@ -14,16 +14,7 @@ pub mod sidebar;
 mod status_bar;
 mod toolbar;
 
-use std::{
-    any::TypeId,
-    borrow::Cow,
-    future::Future,
-    path::{Path, PathBuf},
-    sync::Arc,
-    time::Duration,
-};
-
-use anyhow::{anyhow, Context, Result};
+use anyhow::{anyhow, Result};
 use call::ActiveCall;
 use client::{
     proto::{self, PeerId},
@@ -33,7 +24,11 @@ use collections::{hash_map, HashMap, HashSet};
 use dock::{Dock, DockDefaultItemFactory, ToggleDockButton};
 use drag_and_drop::DragAndDrop;
 use fs::{self, Fs};
-use futures::{channel::oneshot, FutureExt, StreamExt};
+use futures::{
+    channel::{mpsc, oneshot},
+    future::try_join_all,
+    FutureExt, StreamExt,
+};
 use gpui::{
     actions,
     elements::*,
@@ -45,7 +40,19 @@ use gpui::{
 };
 use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ProjectItem};
 use language::LanguageRegistry;
+use std::{
+    any::TypeId,
+    borrow::Cow,
+    future::Future,
+    path::{Path, PathBuf},
+    sync::Arc,
+    time::Duration,
+};
 
+use crate::{
+    notifications::simple_message_notification::{MessageNotification, OsOpen},
+    persistence::model::{SerializedPane, SerializedPaneGroup, SerializedWorkspace},
+};
 use log::{error, warn};
 use notifications::NotificationHandle;
 pub use pane::*;
@@ -67,11 +74,6 @@ use theme::{Theme, ThemeRegistry};
 pub use toolbar::{ToolbarItemLocation, ToolbarItemView};
 use util::ResultExt;
 
-use crate::{
-    notifications::simple_message_notification::{MessageNotification, OsOpen},
-    persistence::model::{SerializedPane, SerializedPaneGroup, SerializedWorkspace},
-};
-
 #[derive(Clone, PartialEq)]
 pub struct RemoveWorktreeFromProject(pub WorktreeId);
 
@@ -319,6 +321,7 @@ pub fn register_project_item<I: ProjectItem>(cx: &mut MutableAppContext) {
 type FollowableItemBuilder = fn(
     ViewHandle<Pane>,
     ModelHandle<Project>,
+    ViewId,
     &mut Option<proto::view::Variant>,
     &mut MutableAppContext,
 ) -> Option<Task<Result<Box<dyn FollowableItemHandle>>>>;
@@ -334,8 +337,8 @@ pub fn register_followable_item<I: FollowableItem>(cx: &mut MutableAppContext) {
         builders.insert(
             TypeId::of::<I>(),
             (
-                |pane, project, state, cx| {
-                    I::from_state_proto(pane, project, state, cx).map(|task| {
+                |pane, project, id, state, cx| {
+                    I::from_state_proto(pane, project, id, state, cx).map(|task| {
                         cx.foreground()
                             .spawn(async move { Ok(Box::new(task.await?) as Box<_>) })
                     })
@@ -461,25 +464,6 @@ impl DelayedDebouncedEditAction {
     }
 }
 
-#[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>),
-}
-
 pub enum Event {
     DockAnchorChanged,
     PaneAdded(ViewHandle<Pane>),
@@ -510,10 +494,31 @@ pub struct Workspace {
     last_leaders_by_pane: HashMap<WeakViewHandle<Pane>, PeerId>,
     window_edited: bool,
     active_call: Option<(ModelHandle<ActiveCall>, Vec<gpui::Subscription>)>,
+    leader_updates_tx: mpsc::UnboundedSender<(PeerId, proto::UpdateFollowers)>,
     database_id: WorkspaceId,
+    _apply_leader_updates: Task<Result<()>>,
     _observe_current_user: Task<()>,
 }
 
+#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
+pub struct ViewId {
+    pub creator: PeerId,
+    pub id: u64,
+}
+
+#[derive(Default)]
+struct LeaderState {
+    followers: HashSet<PeerId>,
+}
+
+type FollowerStatesByLeader = HashMap<PeerId, HashMap<ViewHandle<Pane>, FollowerState>>;
+
+#[derive(Default)]
+struct FollowerState {
+    active_view_id: Option<ViewId>,
+    items_by_leader_view_id: HashMap<ViewId, Box<dyn FollowableItemHandle>>,
+}
+
 impl Workspace {
     pub fn new(
         serialized_workspace: Option<SerializedWorkspace>,
@@ -579,10 +584,24 @@ impl Workspace {
                 })
             }
         });
-
         let handle = cx.handle();
         let weak_handle = cx.weak_handle();
 
+        // All leader updates are enqueued and then processed in a single task, so
+        // that each asynchronous operation can be run in order.
+        let (leader_updates_tx, mut leader_updates_rx) =
+            mpsc::unbounded::<(PeerId, proto::UpdateFollowers)>();
+        let _apply_leader_updates = cx.spawn_weak(|this, mut cx| async move {
+            while let Some((leader_id, update)) = leader_updates_rx.next().await {
+                let Some(this) = this.upgrade(&cx) else { break };
+                Self::process_leader_update(this, leader_id, update, &mut cx)
+                    .await
+                    .log_err();
+            }
+
+            Ok(())
+        });
+
         cx.emit_global(WorkspaceCreated(weak_handle.clone()));
 
         let left_sidebar = cx.add_view(|_| Sidebar::new(SidebarSide::Left));
@@ -640,6 +659,8 @@ impl Workspace {
             active_call,
             database_id: workspace_id,
             _observe_current_user,
+            _apply_leader_updates,
+            leader_updates_tx,
         };
         this.project_remote_id_changed(project.read(cx).remote_id(), cx);
         cx.defer(|this, cx| this.update_window_title(cx));
@@ -1443,7 +1464,11 @@ impl Workspace {
 
         self.update_followers(
             proto::update_followers::Variant::UpdateActiveView(proto::UpdateActiveView {
-                id: self.active_item(cx).map(|item| item.id() as u64),
+                id: self.active_item(cx).and_then(|item| {
+                    item.to_followable_item_handle(cx)?
+                        .remote_id(&self.client, cx)
+                        .map(|id| id.to_proto())
+                }),
                 leader_id: self.leader_for_pane(&pane),
             }),
             cx,
@@ -1589,9 +1614,7 @@ impl Workspace {
         if let Some(states_by_pane) = self.follower_states_by_leader.remove(&peer_id) {
             for state in states_by_pane.into_values() {
                 for item in state.items_by_leader_view_id.into_values() {
-                    if let FollowerItem::Loaded(item) = item {
-                        item.set_leader_replica_id(None, cx);
-                    }
+                    item.set_leader_replica_id(None, cx);
                 }
             }
         }
@@ -1634,11 +1657,22 @@ impl Workspace {
                         .get_mut(&leader_id)
                         .and_then(|states_by_pane| states_by_pane.get_mut(&pane))
                         .ok_or_else(|| anyhow!("following interrupted"))?;
-                    state.active_view_id = response.active_view_id;
+                    state.active_view_id = if let Some(active_view_id) = response.active_view_id {
+                        Some(ViewId::from_proto(active_view_id)?)
+                    } else {
+                        None
+                    };
                     Ok::<_, anyhow::Error>(())
                 })?;
-                Self::add_views_from_leader(this, leader_id, vec![pane], response.views, &mut cx)
-                    .await?;
+                Self::add_views_from_leader(
+                    this.clone(),
+                    leader_id,
+                    vec![pane],
+                    response.views,
+                    &mut cx,
+                )
+                .await?;
+                this.update(&mut cx, |this, cx| this.leader_updated(leader_id, cx));
             }
             Ok(())
         }))
@@ -1684,9 +1718,7 @@ impl Workspace {
             let leader_id = *leader_id;
             if let Some(state) = states_by_pane.remove(pane) {
                 for (_, item) in state.items_by_leader_view_id {
-                    if let FollowerItem::Loaded(item) = item {
-                        item.set_leader_replica_id(None, cx);
-                    }
+                    item.set_leader_replica_id(None, cx);
                 }
 
                 if states_by_pane.is_empty() {
@@ -1877,14 +1909,18 @@ impl Workspace {
         mut cx: AsyncAppContext,
     ) -> Result<proto::FollowResponse> {
         this.update(&mut cx, |this, cx| {
+            let client = &this.client;
             this.leader_state
                 .followers
                 .insert(envelope.original_sender_id()?);
 
-            let active_view_id = this
-                .active_item(cx)
-                .and_then(|i| i.to_followable_item_handle(cx))
-                .map(|i| i.id() as u64);
+            let active_view_id = this.active_item(cx).and_then(|i| {
+                Some(
+                    i.to_followable_item_handle(cx)?
+                        .remote_id(client, cx)?
+                        .to_proto(),
+                )
+            });
             Ok(proto::FollowResponse {
                 active_view_id,
                 views: this
@@ -1895,11 +1931,11 @@ impl Workspace {
                         pane.read(cx).items().filter_map({
                             let cx = &cx;
                             move |item| {
-                                let id = item.id() as u64;
                                 let item = item.to_followable_item_handle(cx)?;
+                                let id = item.remote_id(client, cx)?.to_proto();
                                 let variant = item.to_state_proto(cx)?;
                                 Some(proto::View {
-                                    id,
+                                    id: Some(id),
                                     leader_id,
                                     variant: Some(variant),
                                 })
@@ -1929,45 +1965,62 @@ impl Workspace {
         this: ViewHandle<Self>,
         envelope: TypedEnvelope<proto::UpdateFollowers>,
         _: Arc<Client>,
-        mut cx: AsyncAppContext,
+        cx: AsyncAppContext,
     ) -> Result<()> {
         let leader_id = envelope.original_sender_id()?;
-        match envelope
-            .payload
-            .variant
-            .ok_or_else(|| anyhow!("invalid update"))?
-        {
+        this.read_with(&cx, |this, _| {
+            this.leader_updates_tx
+                .unbounded_send((leader_id, envelope.payload))
+        })?;
+        Ok(())
+    }
+
+    async fn process_leader_update(
+        this: ViewHandle<Self>,
+        leader_id: PeerId,
+        update: proto::UpdateFollowers,
+        cx: &mut AsyncAppContext,
+    ) -> Result<()> {
+        match update.variant.ok_or_else(|| anyhow!("invalid update"))? {
             proto::update_followers::Variant::UpdateActiveView(update_active_view) => {
-                this.update(&mut cx, |this, cx| {
-                    this.update_leader_state(leader_id, cx, |state, _| {
-                        state.active_view_id = update_active_view.id;
-                    });
-                    Ok::<_, anyhow::Error>(())
-                })
+                this.update(cx, |this, _| {
+                    if let Some(state) = this.follower_states_by_leader.get_mut(&leader_id) {
+                        for state in state.values_mut() {
+                            state.active_view_id =
+                                if let Some(active_view_id) = update_active_view.id.clone() {
+                                    Some(ViewId::from_proto(active_view_id)?)
+                                } else {
+                                    None
+                                };
+                        }
+                    }
+                    anyhow::Ok(())
+                })?;
             }
             proto::update_followers::Variant::UpdateView(update_view) => {
-                this.update(&mut cx, |this, cx| {
-                    let variant = update_view
-                        .variant
-                        .ok_or_else(|| anyhow!("missing update view variant"))?;
-                    this.update_leader_state(leader_id, cx, |state, cx| {
-                        let variant = variant.clone();
-                        match state
-                            .items_by_leader_view_id
-                            .entry(update_view.id)
-                            .or_insert(FollowerItem::Loading(Vec::new()))
-                        {
-                            FollowerItem::Loaded(item) => {
-                                item.apply_update_proto(variant, cx).log_err();
+                let variant = update_view
+                    .variant
+                    .ok_or_else(|| anyhow!("missing update view variant"))?;
+                let id = update_view
+                    .id
+                    .ok_or_else(|| anyhow!("missing update view id"))?;
+                let mut tasks = Vec::new();
+                this.update(cx, |this, cx| {
+                    let project = this.project.clone();
+                    if let Some(state) = this.follower_states_by_leader.get_mut(&leader_id) {
+                        for state in state.values_mut() {
+                            let view_id = ViewId::from_proto(id.clone())?;
+                            if let Some(item) = state.items_by_leader_view_id.get(&view_id) {
+                                tasks.push(item.apply_update_proto(&project, variant.clone(), cx));
                             }
-                            FollowerItem::Loading(updates) => updates.push(variant),
                         }
-                    });
-                    Ok(())
-                })
+                    }
+                    anyhow::Ok(())
+                })?;
+                try_join_all(tasks).await.log_err();
             }
             proto::update_followers::Variant::CreateView(view) => {
-                let panes = this.read_with(&cx, |this, _| {
+                let panes = this.read_with(cx, |this, _| {
                     this.follower_states_by_leader
                         .get(&leader_id)
                         .into_iter()
@@ -1975,13 +2028,10 @@ impl Workspace {
                         .cloned()
                         .collect()
                 });
-                Self::add_views_from_leader(this.clone(), leader_id, panes, vec![view], &mut cx)
-                    .await?;
-                Ok(())
+                Self::add_views_from_leader(this.clone(), leader_id, panes, vec![view], cx).await?;
             }
         }
-        .log_err();
-
+        this.update(cx, |this, cx| this.leader_updated(leader_id, cx));
         Ok(())
     }
 
@@ -2014,16 +2064,19 @@ impl Workspace {
             let mut item_tasks = Vec::new();
             let mut leader_view_ids = Vec::new();
             for view in &views {
+                let Some(id) = &view.id else { continue };
+                let id = ViewId::from_proto(id.clone())?;
                 let mut variant = view.variant.clone();
                 if variant.is_none() {
                     Err(anyhow!("missing variant"))?;
                 }
                 for build_item in &item_builders {
-                    let task =
-                        cx.update(|cx| build_item(pane.clone(), project.clone(), &mut variant, cx));
+                    let task = cx.update(|cx| {
+                        build_item(pane.clone(), project.clone(), id, &mut variant, cx)
+                    });
                     if let Some(task) = task {
                         item_tasks.push(task);
-                        leader_view_ids.push(view.id);
+                        leader_view_ids.push(id);
                         break;
                     } else {
                         assert!(variant.is_some());
@@ -2044,29 +2097,12 @@ impl Workspace {
 
                 for (id, item) in leader_view_ids.into_iter().zip(items) {
                     item.set_leader_replica_id(Some(replica_id), cx);
-                    match state.items_by_leader_view_id.entry(id) {
-                        hash_map::Entry::Occupied(e) => {
-                            let e = e.into_mut();
-                            if let FollowerItem::Loading(updates) = e {
-                                for update in updates.drain(..) {
-                                    item.apply_update_proto(update, cx)
-                                        .context("failed to apply view update")
-                                        .log_err();
-                                }
-                            }
-                            *e = FollowerItem::Loaded(item);
-                        }
-                        hash_map::Entry::Vacant(e) => {
-                            e.insert(FollowerItem::Loaded(item));
-                        }
-                    }
+                    state.items_by_leader_view_id.insert(id, item);
                 }
 
                 Some(())
             });
         }
-        this.update(cx, |this, cx| this.leader_updated(leader_id, cx));
-
         Ok(())
     }
 
@@ -2100,23 +2136,6 @@ impl Workspace {
             })
     }
 
-    fn update_leader_state(
-        &mut self,
-        leader_id: PeerId,
-        cx: &mut ViewContext<Self>,
-        mut update_fn: impl FnMut(&mut FollowerState, &mut ViewContext<Self>),
-    ) {
-        for (_, state) in self
-            .follower_states_by_leader
-            .get_mut(&leader_id)
-            .into_iter()
-            .flatten()
-        {
-            update_fn(state, cx);
-        }
-        self.leader_updated(leader_id, cx);
-    }
-
     fn leader_updated(&mut self, leader_id: PeerId, cx: &mut ViewContext<Self>) -> Option<()> {
         cx.notify();
 
@@ -2129,7 +2148,7 @@ impl Workspace {
             call::ParticipantLocation::SharedProject { project_id } => {
                 if Some(project_id) == self.project.read(cx).remote_id() {
                     for (pane, state) in self.follower_states_by_leader.get(&leader_id)? {
-                        if let Some(FollowerItem::Loaded(item)) = state
+                        if let Some(item) = state
                             .active_view_id
                             .and_then(|id| state.items_by_leader_view_id.get(&id))
                         {
@@ -2578,6 +2597,24 @@ impl View for Workspace {
     }
 }
 
+impl ViewId {
+    pub(crate) fn from_proto(message: proto::ViewId) -> Result<Self> {
+        Ok(Self {
+            creator: message
+                .creator
+                .ok_or_else(|| anyhow!("creator is missing"))?,
+            id: message.id,
+        })
+    }
+
+    pub(crate) fn to_proto(&self) -> proto::ViewId {
+        proto::ViewId {
+            creator: Some(self.creator),
+            id: self.id,
+        }
+    }
+}
+
 pub trait WorkspaceHandle {
     fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath>;
 }

crates/zed/src/zed.rs 🔗

@@ -15,12 +15,16 @@ use editor::{Editor, MultiBuffer};
 
 use gpui::{
     actions,
-    geometry::vector::vec2f,
+    geometry::{
+        rect::RectF,
+        vector::{vec2f, Vector2F},
+    },
     impl_actions,
     platform::{WindowBounds, WindowOptions},
     AssetSource, AsyncAppContext, TitlebarOptions, ViewContext, WindowKind,
 };
 use language::Rope;
+use lazy_static::lazy_static;
 pub use lsp;
 pub use project;
 use project_panel::ProjectPanel;
@@ -68,6 +72,17 @@ actions!(
 
 const MIN_FONT_SIZE: f32 = 6.0;
 
+lazy_static! {
+    static ref ZED_WINDOW_SIZE: Option<Vector2F> = env::var("ZED_WINDOW_SIZE")
+        .ok()
+        .as_deref()
+        .and_then(parse_pixel_position_env_var);
+    static ref ZED_WINDOW_POSITION: Option<Vector2F> = env::var("ZED_WINDOW_POSITION")
+        .ok()
+        .as_deref()
+        .and_then(parse_pixel_position_env_var);
+}
+
 pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::MutableAppContext) {
     cx.add_action(about);
     cx.add_global_action(|_: &Hide, cx: &mut gpui::MutableAppContext| {
@@ -336,8 +351,13 @@ pub fn initialize_workspace(
 }
 
 pub fn build_window_options() -> WindowOptions<'static> {
+    let bounds = if let Some((position, size)) = ZED_WINDOW_POSITION.zip(*ZED_WINDOW_SIZE) {
+        WindowBounds::Fixed(RectF::new(position, size))
+    } else {
+        WindowBounds::Maximized
+    };
     WindowOptions {
-        bounds: WindowBounds::Maximized,
+        bounds,
         titlebar: Some(TitlebarOptions {
             title: None,
             appears_transparent: true,
@@ -612,6 +632,13 @@ fn schema_file_match(path: &Path) -> &Path {
         .unwrap()
 }
 
+fn parse_pixel_position_env_var(value: &str) -> Option<Vector2F> {
+    let mut parts = value.split(',');
+    let width: usize = parts.next()?.parse().ok()?;
+    let height: usize = parts.next()?.parse().ok()?;
+    Some(vec2f(width as f32, height as f32))
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;

script/bundle 🔗

@@ -51,13 +51,13 @@ cp -R target/x86_64-apple-darwin/release/WebRTC.framework "${app_path}/Contents/
 
 if [[ -n $MACOS_CERTIFICATE && -n $MACOS_CERTIFICATE_PASSWORD && -n $APPLE_NOTARIZATION_USERNAME && -n $APPLE_NOTARIZATION_PASSWORD ]]; then
     echo "Signing bundle with Apple-issued certificate"
-    security create-keychain -p $MACOS_CERTIFICATE_PASSWORD zed.keychain || echo ""
+    security create-keychain -p "$MACOS_CERTIFICATE_PASSWORD" zed.keychain || echo ""
     security default-keychain -s zed.keychain
-    security unlock-keychain -p $MACOS_CERTIFICATE_PASSWORD zed.keychain
-    echo $MACOS_CERTIFICATE | base64 --decode > /tmp/zed-certificate.p12
-    security import /tmp/zed-certificate.p12 -k zed.keychain -P $MACOS_CERTIFICATE_PASSWORD -T /usr/bin/codesign
+    security unlock-keychain -p "$MACOS_CERTIFICATE_PASSWORD" zed.keychain
+    echo "$MACOS_CERTIFICATE" | base64 --decode > /tmp/zed-certificate.p12
+    security import /tmp/zed-certificate.p12 -k zed.keychain -P "$MACOS_CERTIFICATE_PASSWORD" -T /usr/bin/codesign
     rm /tmp/zed-certificate.p12
-    security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k $MACOS_CERTIFICATE_PASSWORD zed.keychain
+    security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$MACOS_CERTIFICATE_PASSWORD" zed.keychain
     /usr/bin/codesign --force --deep --timestamp --options runtime --sign "Zed Industries, Inc." "${app_path}" -v
     security default-keychain -s login.keychain
 else
@@ -66,22 +66,31 @@ else
     codesign --force --deep --sign - "${app_path}" -v
 fi
 
+dmg_target_directory="target/release"
+dmg_source_directory="${dmg_target_directory}/dmg"
+dmg_file_path="${dmg_target_directory}/Zed.dmg"
+
 echo "Creating DMG"
-mkdir -p target/release/dmg
-rm -rf  target/release/dmg/*
-mv "${app_path}" target/release/dmg/
-hdiutil create -volname Zed -srcfolder target/release/dmg -ov -format UDZO target/release/Zed.dmg
+rm -rf ${dmg_source_directory}
+mkdir -p ${dmg_source_directory}
+mv "${app_path}" "${dmg_source_directory}"
+
+ln -s /Applications ${dmg_source_directory}
+hdiutil create -volname Zed -srcfolder "${dmg_source_directory}" -ov -format UDZO "${dmg_file_path}"
+# If someone runs this bundle script locally, a symlink will be placed in `dmg_source_directory`.
+# This symlink causes CPU issues with Zed if the Zed codebase is the project being worked on, so we simply remove it for now.
+rm ${dmg_source_directory}/Applications
 
 if [[ -n $MACOS_CERTIFICATE && -n $MACOS_CERTIFICATE_PASSWORD && -n $APPLE_NOTARIZATION_USERNAME && -n $APPLE_NOTARIZATION_PASSWORD ]]; then
     echo "Notarizing DMG with Apple"
     npm install -g notarize-cli
-    npx notarize-cli --file target/release/Zed.dmg --bundle-id dev.zed.Zed --username $APPLE_NOTARIZATION_USERNAME --password $APPLE_NOTARIZATION_PASSWORD
+    npx notarize-cli --file ${dmg_file_path} --bundle-id dev.zed.Zed --username "$APPLE_NOTARIZATION_USERNAME" --password "$APPLE_NOTARIZATION_PASSWORD"
 fi
 
-# If -o option is specified, open the target/release directory in Finder to reveal the DMG
+# If -o option is specified, open the $dmg_target_directory directory in Finder to reveal the DMG
 while getopts o flag
 do
     case "${flag}" in
-        o) open target/release;;
+        o) open $dmg_target_directory;;
     esac
 done

script/start-local-collaboration 🔗

@@ -0,0 +1,50 @@
+#!/bin/bash
+
+set -e
+
+if [[ -z "$GITHUB_TOKEN" ]]; then
+  cat <<-MESSAGE
+Missing \`GITHUB_TOKEN\` environment variable. This token is needed
+for fetching your GitHub identity from the command-line.
+
+Create an access token here: https://github.com/settings/tokens
+Then edit your \`~/.zshrc\` (or other shell initialization script),
+adding a line like this:
+
+    export GITHUB_TOKEN="(the token)"
+
+MESSAGE
+  exit 1
+fi
+
+# Start one Zed instance as the current user and a second instance with a different user.
+username_1=$(curl -sH "Authorization: bearer $GITHUB_TOKEN" https://api.github.com/user | jq -r .login)
+username_2=nathansobo
+if [[ $username_1 == $username_2 ]]; then
+  username_2=as-cii
+fi
+
+# Make each Zed instance take up half of the screen.
+resolution_line=$(system_profiler SPDisplaysDataType | grep Resolution | head -n1)
+screen_size=($(echo $resolution_line | egrep -o '[0-9]+'))
+scale_factor=1
+if [[ $resolution_line =~ Retina ]]; then scale_factor=2; fi
+width=$(expr ${screen_size[0]} / 2 / $scale_factor)
+height=${screen_size[1] / $scale_factor}
+
+position_1=0,0
+position_2=${width},0
+
+# Authenticate using the collab server's admin secret.
+export ZED_ADMIN_API_TOKEN=secret
+export ZED_SERVER_URL=http://localhost:8080
+export ZED_WINDOW_SIZE=${width},${height}
+
+cargo build
+sleep 0.5
+
+# Start the two Zed child processes. Open the given paths with the first instance.
+trap "trap - SIGTERM && kill -- -$$" SIGINT SIGTERM EXIT
+ZED_IMPERSONATE=${username_1} ZED_WINDOW_POSITION=${position_1} target/debug/Zed $@ &
+ZED_IMPERSONATE=${username_2} ZED_WINDOW_POSITION=${position_2} target/debug/Zed &
+wait

styles/src/styleTree/components.ts 🔗

@@ -12,8 +12,16 @@ function isStyleSet(key: any): key is StyleSets {
     "negative",
   ].includes(key);
 }
+
 function isStyle(key: any): key is Styles {
-  return ["default", "active", "disabled", "hovered", "pressed", "inverted"].includes(key);
+  return [
+    "default",
+    "active",
+    "disabled",
+    "hovered",
+    "pressed",
+    "inverted",
+  ].includes(key);
 }
 function getStyle(
   layer: Layer,