From 3eac3e20d5db9492beb8556b37b587f1e7e37343 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 28 Nov 2022 15:47:44 -0800 Subject: [PATCH 01/14] Emit events from a multibuffer when adding/removing excerpts --- crates/editor/src/editor.rs | 18 +-- crates/editor/src/multi_buffer.rs | 188 ++++++++++++++++++++++++++---- 2 files changed, 175 insertions(+), 31 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 5bbeed3fb5..4a7a7893a1 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -6368,22 +6368,22 @@ impl Editor { fn on_buffer_event( &mut self, _: ModelHandle, - event: &language::Event, + event: &multi_buffer::Event, cx: &mut ViewContext, ) { 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::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); } _ => {} diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index d758792e6c..ace07a347f 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/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::{ @@ -833,6 +833,30 @@ impl MultiBuffer { ) -> Vec 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( + &mut self, + prev_excerpt_id: ExcerptId, + buffer: ModelHandle, + ranges: impl IntoIterator)>, + cx: &mut ModelContext, + ) where + O: text::ToOffset, { assert_eq!(self.history.transaction_depth, 0); let mut ranges = ranges.into_iter().peekable(); @@ -858,7 +882,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 +907,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 +921,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 +936,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 +955,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.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 +979,7 @@ impl MultiBuffer { new: 0..0, }]); cx.emit(Event::Edited); + cx.emit(Event::ExcerptsRemoved { ids }); cx.notify(); } @@ -1071,12 +1103,14 @@ impl MultiBuffer { cx: &mut ModelContext, ) { self.sync(cx); + let ids = excerpt_ids.into_iter().collect::>(); + 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 +1177,7 @@ impl MultiBuffer { self.subscriptions.publish_mut(edits); cx.emit(Event::Edited); + cx.emit(Event::ExcerptsRemoved { ids }); cx.notify(); } @@ -1165,10 +1200,22 @@ impl MultiBuffer { fn on_buffer_event( &mut self, _: ModelHandle, - event: &Event, + event: &language::Event, cx: &mut ModelContext, ) { - 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> { @@ -1603,8 +1650,28 @@ impl MultiBuffer { } } +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum Event { + ExcerptsAdded { + buffer: ModelHandle, + predecessor: ExcerptId, + excerpts: Vec<(ExcerptId, ExcerptRange)>, + }, + ExcerptsRemoved { + ids: Vec, + }, + Edited, + Reloaded, + Reparsed, + Saved, + FileHandleChanged, + Closed, + DirtyChanged, + DiagnosticsUpdated, +} + impl Entity for MultiBuffer { - type Event = language::Event; + type Event = Event; } impl MultiBufferSnapshot { @@ -3468,7 +3535,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 +3562,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 +3593,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 +3602,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 +3817,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 +3930,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 +4018,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)); From 6d9b55a6546c9d46b03f4a963589c4d53b68e33e Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 28 Nov 2022 18:00:38 -0800 Subject: [PATCH 02/14] Send full multibuffer anchors to following peers --- crates/editor/src/items.rs | 94 +++++++++++++------------------ crates/editor/src/multi_buffer.rs | 12 ++++ crates/language/src/proto.rs | 20 ++++--- crates/rpc/proto/zed.proto | 21 +++---- 4 files changed, 73 insertions(+), 74 deletions(-) diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 0cc8575e99..ad90f90b9d 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -65,34 +65,26 @@ impl FollowableItem for Editor { }) }); editor.update(&mut cx, |editor, cx| { - let excerpt_id; - let buffer_id; - { - let buffer = editor.buffer.read(cx).read(cx); - let singleton = buffer.as_singleton().unwrap(); - excerpt_id = singleton.0.clone(); - buffer_id = singleton.1; - } + let 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::>>()?; + 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_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"))?, - }, + scroll_top_anchor, vec2f(state.scroll_x, state.scroll_y), cx, ); @@ -133,9 +125,7 @@ impl FollowableItem for Editor { let buffer_id = self.buffer.read(cx).as_singleton()?.read(cx).remote_id(); Some(proto::view::Variant::Editor(proto::view::Editor { buffer_id, - scroll_top_anchor: Some(language::proto::serialize_anchor( - &self.scroll_top_anchor.text_anchor, - )), + scroll_top_anchor: Some(serialize_anchor(&self.scroll_top_anchor)), scroll_x: self.scroll_position.x(), scroll_y: self.scroll_position.y(), selections: self @@ -159,9 +149,7 @@ impl FollowableItem for Editor { match update { proto::update_view::Variant::Editor(update) => match event { Event::ScrollPositionChanged { .. } => { - update.scroll_top_anchor = Some(language::proto::serialize_anchor( - &self.scroll_top_anchor.text_anchor, - )); + update.scroll_top_anchor = Some(serialize_anchor(&self.scroll_top_anchor)); update.scroll_x = self.scroll_position.x(); update.scroll_y = self.scroll_position.y(); true @@ -190,29 +178,22 @@ impl FollowableItem for Editor { update_view::Variant::Editor(message) => { let buffer = self.buffer.read(cx); let buffer = buffer.read(cx); - let (excerpt_id, buffer_id, _) = buffer.as_singleton().unwrap(); - let excerpt_id = excerpt_id.clone(); - drop(buffer); - let selections = message .selections .into_iter() - .filter_map(|selection| { - deserialize_selection(&excerpt_id, buffer_id, selection) - }) + .filter_map(|selection| deserialize_selection(&buffer, selection)) .collect::>(); + let scroll_top_anchor = message + .scroll_top_anchor + .and_then(|anchor| deserialize_anchor(&buffer, anchor)); + drop(buffer); 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 { + } else if let Some(anchor) = scroll_top_anchor { self.set_scroll_top_anchor( - Anchor { - buffer_id: Some(buffer_id), - excerpt_id, - text_anchor: language::proto::deserialize_anchor(anchor) - .ok_or_else(|| anyhow!("invalid scroll top"))?, - }, + anchor, vec2f(message.scroll_x, message.scroll_y), cx, ); @@ -235,38 +216,41 @@ impl FollowableItem for Editor { fn serialize_selection(selection: &Selection) -> 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(language::proto::serialize_anchor(&anchor.text_anchor)), + } +} + fn deserialize_selection( - excerpt_id: &ExcerptId, - buffer_id: usize, + buffer: &MultiBufferSnapshot, selection: proto::Selection, ) -> Option> { 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 { + let excerpt_id = ExcerptId::from_proto(anchor.excerpt_id); + Some(Anchor { + excerpt_id, + text_anchor: language::proto::deserialize_anchor(anchor.anchor?)?, + buffer_id: Some(buffer.buffer_id_for_excerpt(excerpt_id)?), + }) +} + impl Item for Editor { fn navigate(&mut self, data: Box, cx: &mut ViewContext) -> bool { if let Ok(data) = data.downcast::() { diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index ace07a347f..35e26534d7 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -2813,6 +2813,10 @@ impl MultiBufferSnapshot { } } + pub fn buffer_id_for_excerpt(&self, excerpt_id: ExcerptId) -> Option { + Some(self.excerpt(excerpt_id)?.buffer_id) + } + fn excerpt<'a>(&'a self, excerpt_id: ExcerptId) -> Option<&'a Excerpt> { let mut cursor = self.excerpts.cursor::>(); let locator = self.excerpt_locator_for_id(excerpt_id); @@ -3147,6 +3151,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); diff --git a/crates/language/src/proto.rs b/crates/language/src/proto.rs index 674ce4f50e..9612deb5bd 100644 --- a/crates/language/src/proto.rs +++ b/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]>) -> Vec) -> 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) -> Arc<[Selecti pub fn deserialize_selection(selection: proto::Selection) -> Option> { 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, }) diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 6bfe7124c9..3bc612bdc4 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -848,7 +848,7 @@ message UpdateView { message Editor { repeated Selection selections = 1; - Anchor scroll_top_anchor = 2; + EditorAnchor scroll_top_anchor = 2; float scroll_x = 3; float scroll_y = 4; } @@ -865,7 +865,7 @@ message View { message Editor { uint64 buffer_id = 1; repeated Selection selections = 2; - Anchor scroll_top_anchor = 3; + EditorAnchor scroll_top_anchor = 3; float scroll_x = 4; float scroll_y = 5; } @@ -920,21 +920,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; From 82abf31ef10a67d382db78de6cdd72aeca75d728 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 29 Nov 2022 14:50:12 -0800 Subject: [PATCH 03/14] Add start-local-collaboration script --- script/start-local-collaboration | 33 ++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100755 script/start-local-collaboration diff --git a/script/start-local-collaboration b/script/start-local-collaboration new file mode 100755 index 0000000000..1d3d74600b --- /dev/null +++ b/script/start-local-collaboration @@ -0,0 +1,33 @@ +#!/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 + +github_login=$(curl -sH "Authorization: bearer $GITHUB_TOKEN" https://api.github.com/user | jq -r .login) + +other_github_login=nathansobo +if [[ $github_login == $other_github_login ]]; then + other_github_login=as-cii +fi + +export ZED_ADMIN_API_TOKEN=secret +export ZED_SERVER_URL=http://localhost:8080 + +trap "trap - SIGTERM && kill -- -$$" SIGINT SIGTERM EXIT +ZED_IMPERSONATE=${github_login} cargo run --quiet $@ & +ZED_IMPERSONATE=${other_github_login} cargo run --quiet & +wait From 6120d6488b824f2f09c5bcdb0b7f6ac8bbcaed21 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 29 Nov 2022 14:50:43 -0800 Subject: [PATCH 04/14] Start work on following in multi-buffers --- crates/diagnostics/src/diagnostics.rs | 2 +- crates/editor/src/editor.rs | 18 ++- crates/editor/src/editor_tests.rs | 4 +- crates/editor/src/items.rs | 181 +++++++++++++++++++++++--- crates/editor/src/multi_buffer.rs | 48 ++++--- crates/rpc/proto/zed.proto | 36 +++-- crates/search/src/project_search.rs | 4 +- crates/text/src/text.rs | 4 + 8 files changed, 241 insertions(+), 56 deletions(-) diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index 6ff7490181..fba9fe4d2f 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -160,7 +160,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); diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 4a7a7893a1..cc873cbffa 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -6587,8 +6587,16 @@ fn compute_scroll_position( scroll_position } -#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq)] pub enum Event { + ExcerptsAdded { + buffer: ModelHandle, + predecessor: ExcerptId, + excerpts: Vec<(ExcerptId, ExcerptRange)>, + }, + ExcerptsRemoved { + ids: Vec, + }, BufferEdited, Edited, Reparsed, @@ -6596,8 +6604,12 @@ pub enum Event { DirtyChanged, Saved, TitleChanged, - SelectionsChanged { local: bool }, - ScrollPositionChanged { local: bool }, + SelectionsChanged { + local: bool, + }, + ScrollPositionChanged { + local: bool, + }, Closed, IgnoredInput, } diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 8ac1f9a3fc..a980622077 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -38,7 +38,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(); @@ -53,7 +53,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(); diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index ad90f90b9d..2c0b8b834e 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -1,14 +1,16 @@ use crate::{ display_map::ToDisplayPoint, link_go_to_definition::hide_link_definition, - movement::surrounding_word, Anchor, Autoscroll, Editor, Event, ExcerptId, MultiBuffer, - MultiBufferSnapshot, NavigationData, ToPoint as _, FORMAT_TIMEOUT, + movement::surrounding_word, Anchor, Autoscroll, Editor, Event, ExcerptId, ExcerptRange, + MultiBuffer, MultiBufferSnapshot, NavigationData, ToPoint as _, FORMAT_TIMEOUT, }; use anyhow::{anyhow, Result}; +use collections::HashSet; use futures::FutureExt; use gpui::{ elements::*, geometry::vector::vec2f, AppContext, Entity, ModelHandle, MutableAppContext, RenderContext, Subscription, Task, View, ViewContext, ViewHandle, }; +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}; @@ -18,6 +20,7 @@ use std::{ borrow::Cow, cmp::{self, Ordering}, fmt::Write, + iter, ops::Range, path::{Path, PathBuf}, }; @@ -48,22 +51,75 @@ impl FollowableItem for Editor { return None; }; - let buffer = project.update(cx, |project, cx| { - project.open_buffer_by_id(state.buffer_id, cx) + let replica_id = project.read(cx).replica_id(); + let buffer_ids = state + .excerpts + .iter() + .map(|excerpt| excerpt.buffer_id) + .collect::>(); + let buffers = project.update(cx, |project, cx| { + buffer_ids + .iter() + .map(|id| project.open_buffer_by_id(*id, cx)) + .collect::>() }); + Some(cx.spawn(|mut cx| async move { - let buffer = buffer.await?; - let editor = pane - .read_with(&cx, |pane, cx| { - pane.items_of_type::().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::(); + if state.singleton && buffers.len() == 1 { + editors.find(|editor| { + editor.read(cx).buffer.read(cx).as_singleton().as_ref() == Some(&buffers[0]) }) + } else if let Some(title) = &state.title { + editors.find(|editor| { + editor.read(cx).buffer().read(cx).title(cx).as_ref() == title + }) + } else { + None + } + }); + + 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)) }) - .unwrap_or_else(|| { - pane.update(&mut cx, |_, cx| { - cx.add_view(|cx| Editor::for_buffer(buffer, Some(project), cx)) - }) - }); + }); + editor.update(&mut cx, |editor, cx| { let buffer = editor.buffer.read(cx).read(cx); let selections = state @@ -90,8 +146,9 @@ impl FollowableItem for Editor { ); } - Ok::<_, anyhow::Error>(()) + anyhow::Ok(()) })?; + Ok(editor) })) } @@ -122,9 +179,30 @@ impl FollowableItem for Editor { } fn to_state_proto(&self, cx: &AppContext) -> Option { - let buffer_id = self.buffer.read(cx).as_singleton()?.read(cx).remote_id(); + let buffer = self.buffer.read(cx); + 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, + singleton: buffer.is_singleton(), + title: (!buffer.is_singleton()).then(|| buffer.title(cx).into()), + excerpts, scroll_top_anchor: Some(serialize_anchor(&self.scroll_top_anchor)), scroll_x: self.scroll_position.x(), scroll_y: self.scroll_position.y(), @@ -141,13 +219,39 @@ impl FollowableItem for Editor { &self, event: &Self::Event, update: &mut Option, - _: &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 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, range, cx), + }); + update.inserted_excerpts.extend(excerpts.map(|(id, range)| { + proto::ExcerptInsertion { + previous_excerpt_id: None, + excerpt: serialize_excerpt(buffer, id, range, cx), + } + })) + } + true + } + Event::ExcerptsRemoved { ids } => { + update + .deleted_excerpts + .extend(ids.iter().map(ExcerptId::to_proto)); + true + } Event::ScrollPositionChanged { .. } => { update.scroll_top_anchor = Some(serialize_anchor(&self.scroll_top_anchor)); update.scroll_x = self.scroll_position.x(); @@ -213,6 +317,28 @@ impl FollowableItem for Editor { } } +fn serialize_excerpt( + buffer: &ModelHandle, + id: &ExcerptId, + range: &ExcerptRange, + cx: &AppContext, +) -> Option { + Some(proto::Excerpt { + id: id.to_proto(), + buffer_id: buffer.read(cx).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(|r| serialize_text_anchor(&r.start)), + primary_end: range + .primary + .as_ref() + .map(|r| serialize_text_anchor(&r.end)), + }) +} + fn serialize_selection(selection: &Selection) -> proto::Selection { proto::Selection { id: selection.id as u64, @@ -225,10 +351,27 @@ fn serialize_selection(selection: &Selection) -> proto::Selection { fn serialize_anchor(anchor: &Anchor) -> proto::EditorAnchor { proto::EditorAnchor { excerpt_id: anchor.excerpt_id.to_proto(), - anchor: Some(language::proto::serialize_anchor(&anchor.text_anchor)), + anchor: Some(serialize_text_anchor(&anchor.text_anchor)), } } +fn deserialize_excerpt_range(excerpt: proto::Excerpt) -> Option> { + 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( buffer: &MultiBufferSnapshot, selection: proto::Selection, diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index 35e26534d7..d0dd34a931 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -50,6 +50,26 @@ pub struct MultiBuffer { title: Option, } +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum Event { + ExcerptsAdded { + buffer: ModelHandle, + predecessor: ExcerptId, + excerpts: Vec<(ExcerptId, ExcerptRange)>, + }, + ExcerptsRemoved { + ids: Vec, + }, + Edited, + Reloaded, + Reparsed, + Saved, + FileHandleChanged, + Closed, + DirtyChanged, + DiagnosticsUpdated, +} + #[derive(Clone)] struct History { next_transaction_id: TransactionId, @@ -1650,26 +1670,6 @@ impl MultiBuffer { } } -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum Event { - ExcerptsAdded { - buffer: ModelHandle, - predecessor: ExcerptId, - excerpts: Vec<(ExcerptId, ExcerptRange)>, - }, - ExcerptsRemoved { - ids: Vec, - }, - Edited, - Reloaded, - Reparsed, - Saved, - FileHandleChanged, - Closed, - DirtyChanged, - DiagnosticsUpdated, -} - impl Entity for MultiBuffer { type Event = Event; } @@ -2517,6 +2517,14 @@ impl MultiBufferSnapshot { } } + pub fn excerpts( + &self, + ) -> impl Iterator)> { + self.excerpts + .iter() + .map(|excerpt| (excerpt.id, &excerpt.buffer, excerpt.range.clone())) + } + pub fn excerpt_boundaries_in_range( &self, range: R, diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 3bc612bdc4..451c1539ea 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -847,10 +847,12 @@ message UpdateView { } message Editor { - repeated Selection selections = 1; - EditorAnchor 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; } } @@ -863,11 +865,13 @@ message View { } message Editor { - uint64 buffer_id = 1; - repeated Selection selections = 2; - EditorAnchor 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; } } @@ -939,6 +943,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; diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 5e935a6ae3..41f48f4b5a 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -388,7 +388,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(); @@ -405,7 +405,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(); diff --git a/crates/text/src/text.rs b/crates/text/src/text.rs index 5c2f7b7a51..5aa91ede8a 100644 --- a/crates/text/src/text.rs +++ b/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 } From a48cd9125b50fe90b56980be56746d3b1e6cd114 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 30 Nov 2022 09:29:49 -0800 Subject: [PATCH 05/14] Start-local-collaboration script: put peers' windows at different positions --- crates/zed/src/zed.rs | 27 +++++++++++++++++++++++++-- script/start-local-collaboration | 10 ++++++++-- 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index bb33109d0d..cbbbe1cfea 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -17,7 +17,10 @@ use lazy_static::lazy_static; use gpui::{ actions, - geometry::vector::vec2f, + geometry::{ + rect::RectF, + vector::{vec2f, Vector2F}, + }, impl_actions, platform::{WindowBounds, WindowOptions}, AssetSource, AsyncAppContext, TitlebarOptions, ViewContext, WindowKind, @@ -71,6 +74,14 @@ actions!( const MIN_FONT_SIZE: f32 = 6.0; lazy_static! { + static ref ZED_WINDOW_SIZE: Option = env::var("ZED_WINDOW_SIZE") + .ok() + .as_deref() + .and_then(parse_pixel_position_env_var); + static ref ZED_WINDOW_POSITION: Option = env::var("ZED_WINDOW_POSITION") + .ok() + .as_deref() + .and_then(parse_pixel_position_env_var); pub static ref RELEASE_CHANNEL_NAME: String = env::var("ZED_RELEASE_CHANNEL").unwrap_or(include_str!("../RELEASE_CHANNEL").to_string()); pub static ref RELEASE_CHANNEL: ReleaseChannel = match RELEASE_CHANNEL_NAME.as_str() { @@ -346,8 +357,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 +628,13 @@ fn schema_file_match(path: &Path) -> &Path { .unwrap() } +fn parse_pixel_position_env_var(value: &str) -> Option { + 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::*; diff --git a/script/start-local-collaboration b/script/start-local-collaboration index 1d3d74600b..643205532c 100755 --- a/script/start-local-collaboration +++ b/script/start-local-collaboration @@ -26,8 +26,14 @@ fi export ZED_ADMIN_API_TOKEN=secret export ZED_SERVER_URL=http://localhost:8080 +export ZED_WINDOW_SIZE=800,600 + +cargo build +sleep 0.1 trap "trap - SIGTERM && kill -- -$$" SIGINT SIGTERM EXIT -ZED_IMPERSONATE=${github_login} cargo run --quiet $@ & -ZED_IMPERSONATE=${other_github_login} cargo run --quiet & +ZED_WINDOW_POSITION=0,0 ZED_IMPERSONATE=${github_login} target/debug/Zed $@ & +sleep 0.1 +ZED_WINDOW_POSITION=800,0 ZED_IMPERSONATE=${other_github_login} target/debug/Zed & +sleep 0.1 wait From 9314c0e313cca95351856449fa61eb1638d03663 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 30 Nov 2022 13:20:13 -0800 Subject: [PATCH 06/14] Replicate multibuffer excerpt additions and removals to followers --- crates/editor/src/editor.rs | 13 +- crates/editor/src/editor_tests.rs | 189 +++++++++++++++++++++++++---- crates/editor/src/items.rs | 73 ++++++++--- crates/workspace/src/workspace.rs | 10 +- styles/src/styleTree/components.ts | 10 +- 5 files changed, 254 insertions(+), 41 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index cc873cbffa..de2db40204 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -6377,6 +6377,18 @@ impl Editor { self.refresh_code_actions(cx); cx.emit(Event::BufferEdited); } + 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), @@ -6386,7 +6398,6 @@ impl Editor { multi_buffer::Event::DiagnosticsUpdated => { self.refresh_active_diagnostics(cx); } - _ => {} } } diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index a980622077..b84d304d54 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -4967,19 +4967,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); +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; - cx.set_global(Settings::test(cx)); - - 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 pending_update = Rc::new(RefCell::new(None)); follower.update(cx, { @@ -5000,10 +5008,12 @@ fn test_following(cx: &mut gpui::MutableAppContext) { }); follower.update(cx, |follower, cx| { follower - .apply_update_proto(pending_update.borrow_mut().take().unwrap(), cx) + .apply_update_proto(&project, pending_update.borrow_mut().take().unwrap(), cx) .unwrap(); }); - assert_eq!(follower.read(cx).selections.ranges(cx), vec![1..1]); + follower.read_with(cx, |follower, cx| { + assert_eq!(follower.selections.ranges(cx), vec![1..1]); + }); // Update the scroll position only leader.update(cx, |leader, cx| { @@ -5011,7 +5021,7 @@ fn test_following(cx: &mut gpui::MutableAppContext) { }); follower.update(cx, |follower, cx| { follower - .apply_update_proto(pending_update.borrow_mut().take().unwrap(), cx) + .apply_update_proto(&project, pending_update.borrow_mut().take().unwrap(), cx) .unwrap(); }); assert_eq!( @@ -5028,12 +5038,14 @@ fn test_following(cx: &mut gpui::MutableAppContext) { follower.update(cx, |follower, cx| { let initial_scroll_position = follower.scroll_position(cx); follower - .apply_update_proto(pending_update.borrow_mut().take().unwrap(), cx) + .apply_update_proto(&project, pending_update.borrow_mut().take().unwrap(), cx) .unwrap(); assert_eq!(follower.scroll_position(cx), initial_scroll_position); assert!(follower.autoscroll_request.is_some()); }); - assert_eq!(follower.read(cx).selections.ranges(cx), vec![0..0]); + follower.read_with(cx, |follower, cx| { + assert_eq!(follower.selections.ranges(cx), vec![0..0]); + }); // Creating a pending selection that precedes another selection leader.update(cx, |leader, cx| { @@ -5042,10 +5054,12 @@ fn test_following(cx: &mut gpui::MutableAppContext) { }); follower.update(cx, |follower, cx| { follower - .apply_update_proto(pending_update.borrow_mut().take().unwrap(), cx) + .apply_update_proto(&project, pending_update.borrow_mut().take().unwrap(), cx) .unwrap(); }); - assert_eq!(follower.read(cx).selections.ranges(cx), vec![0..0, 1..1]); + follower.read_with(cx, |follower, cx| { + assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]); + }); // Extend the pending selection so that it surrounds another selection leader.update(cx, |leader, cx| { @@ -5053,10 +5067,143 @@ fn test_following(cx: &mut gpui::MutableAppContext) { }); follower.update(cx, |follower, cx| { follower - .apply_update_proto(pending_update.borrow_mut().take().unwrap(), cx) + .apply_update_proto(&project, pending_update.borrow_mut().take().unwrap(), cx) .unwrap(); }); - assert_eq!(follower.read(cx).selections.ranges(cx), vec![0..2]); + follower.read_with(cx, |follower, cx| { + assert_eq!(follower.selections.ranges(cx), vec![0..2]); + }); +} + +#[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(), &mut state_message, cx) + }) + .unwrap() + .await + .unwrap(); + + let follower_1_update = Rc::new(RefCell::new(None)); + follower_1.update(cx, { + let update = follower_1_update.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, + ); + }); + }); + + // 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(), &mut state_message, cx) + }) + .unwrap() + .await + .unwrap(); + assert_eq!( + follower_2.read_with(cx, Editor::text), + leader.read_with(cx, Editor::text) + ); + + // Apply the update of adding the excerpts. + follower_1.update(cx, |follower, cx| { + follower + .apply_update_proto(&project, follower_1_update.borrow_mut().take().unwrap(), cx) + .unwrap() + }); + assert_eq!( + follower_1.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, follower_1_update.borrow_mut().take().unwrap(), cx) + .unwrap() + }); + assert_eq!( + follower_1.read_with(cx, Editor::text), + leader.read_with(cx, Editor::text) + ); } #[test] diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 2c0b8b834e..9b397b6ee5 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -72,10 +72,6 @@ impl FollowableItem for Editor { editors.find(|editor| { editor.read(cx).buffer.read(cx).as_singleton().as_ref() == Some(&buffers[0]) }) - } else if let Some(title) = &state.title { - editors.find(|editor| { - editor.read(cx).buffer().read(cx).title(cx).as_ref() == title - }) } else { None } @@ -231,16 +227,17 @@ impl FollowableItem for Editor { 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, range, cx), + 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, range, cx), + excerpt: serialize_excerpt(buffer_id, id, range), } })) } @@ -275,22 +272,69 @@ impl FollowableItem for Editor { fn apply_update_proto( &mut self, + project: &ModelHandle, message: update_view::Variant, cx: &mut ViewContext, ) -> Result<()> { match message { update_view::Variant::Editor(message) => { - let buffer = self.buffer.read(cx); - let buffer = buffer.read(cx); + let multibuffer = self.buffer.read(cx); + let multibuffer = multibuffer.read(cx); + let mut removals = message + .deleted_excerpts + .into_iter() + .map(ExcerptId::from_proto) + .collect::>(); + removals.sort_by(|a, b| a.cmp(&b, &multibuffer)); + let selections = message .selections .into_iter() - .filter_map(|selection| deserialize_selection(&buffer, selection)) + .filter_map(|selection| deserialize_selection(&multibuffer, selection)) .collect::>(); let scroll_top_anchor = message .scroll_top_anchor - .and_then(|anchor| deserialize_anchor(&buffer, anchor)); - drop(buffer); + .and_then(|anchor| deserialize_anchor(&multibuffer, anchor)); + drop(multibuffer); + + self.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); @@ -318,14 +362,13 @@ impl FollowableItem for Editor { } fn serialize_excerpt( - buffer: &ModelHandle, + buffer_id: u64, id: &ExcerptId, range: &ExcerptRange, - cx: &AppContext, ) -> Option { Some(proto::Excerpt { id: id.to_proto(), - buffer_id: buffer.read(cx).remote_id(), + buffer_id, context_start: Some(serialize_text_anchor(&range.context.start)), context_end: Some(serialize_text_anchor(&range.context.end)), primary_start: range @@ -390,7 +433,7 @@ fn deserialize_anchor(buffer: &MultiBufferSnapshot, anchor: proto::EditorAnchor) Some(Anchor { excerpt_id, text_anchor: language::proto::deserialize_anchor(anchor.anchor?)?, - buffer_id: Some(buffer.buffer_id_for_excerpt(excerpt_id)?), + buffer_id: buffer.buffer_id_for_excerpt(excerpt_id), }) } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 7082b61949..60c46d3ef0 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -463,6 +463,7 @@ pub trait FollowableItem: Item { ) -> bool; fn apply_update_proto( &mut self, + project: &ModelHandle, message: proto::update_view::Variant, cx: &mut ViewContext, ) -> Result<()>; @@ -482,6 +483,7 @@ pub trait FollowableItemHandle: ItemHandle { ) -> bool; fn apply_update_proto( &self, + project: &ModelHandle, message: proto::update_view::Variant, cx: &mut MutableAppContext, ) -> Result<()>; @@ -514,10 +516,11 @@ impl FollowableItemHandle for ViewHandle { fn apply_update_proto( &self, + project: &ModelHandle, message: proto::update_view::Variant, cx: &mut MutableAppContext, ) -> Result<()> { - self.update(cx, |this, cx| this.apply_update_proto(message, cx)) + self.update(cx, |this, cx| this.apply_update_proto(project, message, cx)) } fn should_unfollow_on_event(&self, event: &dyn Any, cx: &AppContext) -> bool { @@ -2477,6 +2480,7 @@ impl Workspace { let variant = update_view .variant .ok_or_else(|| anyhow!("missing update view variant"))?; + let project = this.project.clone(); this.update_leader_state(leader_id, cx, |state, cx| { let variant = variant.clone(); match state @@ -2485,7 +2489,7 @@ impl Workspace { .or_insert(FollowerItem::Loading(Vec::new())) { FollowerItem::Loaded(item) => { - item.apply_update_proto(variant, cx).log_err(); + item.apply_update_proto(&project, variant, cx).log_err(); } FollowerItem::Loading(updates) => updates.push(variant), } @@ -2576,7 +2580,7 @@ impl Workspace { let e = e.into_mut(); if let FollowerItem::Loading(updates) = e { for update in updates.drain(..) { - item.apply_update_proto(update, cx) + item.apply_update_proto(&this.project, update, cx) .context("failed to apply view update") .log_err(); } diff --git a/styles/src/styleTree/components.ts b/styles/src/styleTree/components.ts index 3244e7e4ea..847b937416 100644 --- a/styles/src/styleTree/components.ts +++ b/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, From e4507c1d743b9409206f0e51ad71c86342516024 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 1 Dec 2022 15:17:39 -0800 Subject: [PATCH 07/14] Fetch missing buffers when adding excerpts to a multibuffer while following Make FollowableItem::apply_update_proto asynchronous. Use a single task per workspace to process all leader updates, to prevent updates from being interleaved. Co-authored-by: Antonio Scandurra --- crates/editor/src/editor_tests.rs | 92 +++++++++------- crates/editor/src/items.rs | 83 +++++++++------ crates/workspace/src/workspace.rs | 171 ++++++++++++++---------------- 3 files changed, 180 insertions(+), 166 deletions(-) diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index b84d304d54..58a05d3810 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -5006,11 +5006,12 @@ async fn test_following(cx: &mut gpui::TestAppContext) { 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(&project, 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]); }); @@ -5019,31 +5020,32 @@ async fn test_following(cx: &mut gpui::TestAppContext) { leader.update(cx, |leader, 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) - .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) ); - // 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(&project, pending_update.borrow_mut().take().unwrap(), cx) - .unwrap(); - assert_eq!(follower.scroll_position(cx), initial_scroll_position); - assert!(follower.autoscroll_request.is_some()); - }); - follower.read_with(cx, |follower, cx| { + assert_eq!(follower.scroll_position(cx), vec2f(1.5, 0.0)); assert_eq!(follower.selections.ranges(cx), vec![0..0]); }); @@ -5052,11 +5054,12 @@ async fn test_following(cx: &mut gpui::TestAppContext) { 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(&project, 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]); }); @@ -5065,11 +5068,12 @@ async fn test_following(cx: &mut gpui::TestAppContext) { leader.update(cx, |leader, cx| { leader.extend_selection(DisplayPoint::new(0, 2), 1, cx); }); - follower.update(cx, |follower, cx| { - follower - .apply_update_proto(&project, 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]); }); @@ -5175,11 +5179,16 @@ async fn test_following_with_multiple_excerpts(cx: &mut gpui::TestAppContext) { ); // Apply the update of adding the excerpts. - follower_1.update(cx, |follower, cx| { - follower - .apply_update_proto(&project, follower_1_update.borrow_mut().take().unwrap(), cx) - .unwrap() - }); + follower_1 + .update(cx, |follower, cx| { + follower.apply_update_proto( + &project, + follower_1_update.borrow_mut().take().unwrap(), + cx, + ) + }) + .await + .unwrap(); assert_eq!( follower_1.read_with(cx, Editor::text), leader.read_with(cx, Editor::text) @@ -5195,11 +5204,16 @@ async fn test_following_with_multiple_excerpts(cx: &mut gpui::TestAppContext) { }); // Apply the update of removing the excerpts. - follower_1.update(cx, |follower, cx| { - follower - .apply_update_proto(&project, follower_1_update.borrow_mut().take().unwrap(), cx) - .unwrap() - }); + follower_1 + .update(cx, |follower, cx| { + follower.apply_update_proto( + &project, + follower_1_update.borrow_mut().take().unwrap(), + cx, + ) + }) + .await + .unwrap(); assert_eq!( follower_1.read_with(cx, Editor::text), leader.read_with(cx, Editor::text) diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 9b397b6ee5..6447db02d0 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -5,6 +5,7 @@ use crate::{ }; use anyhow::{anyhow, Result}; use collections::HashSet; +use futures::future::try_join_all; use futures::FutureExt; use gpui::{ elements::*, geometry::vector::vec2f, AppContext, Entity, ModelHandle, MutableAppContext, @@ -275,36 +276,53 @@ impl FollowableItem for Editor { project: &ModelHandle, message: update_view::Variant, cx: &mut ViewContext, - ) -> Result<()> { - match message { - update_view::Variant::Editor(message) => { - let multibuffer = self.buffer.read(cx); - let multibuffer = multibuffer.read(cx); - let mut removals = message - .deleted_excerpts - .into_iter() - .map(ExcerptId::from_proto) - .collect::>(); - removals.sort_by(|a, b| a.cmp(&b, &multibuffer)); + ) -> Task> { + let update_view::Variant::Editor(message) = message; + let multibuffer = self.buffer.read(cx); + let multibuffer = multibuffer.read(cx); - let selections = message - .selections - .into_iter() - .filter_map(|selection| deserialize_selection(&multibuffer, selection)) - .collect::>(); - let scroll_top_anchor = message - .scroll_top_anchor - .and_then(|anchor| deserialize_anchor(&multibuffer, anchor)); - drop(multibuffer); + let buffer_ids = message + .inserted_excerpts + .iter() + .filter_map(|insertion| Some(insertion.excerpt.as_ref()?.buffer_id)) + .collect::>(); - self.buffer.update(cx, |multibuffer, cx| { + let mut removals = message + .deleted_excerpts + .into_iter() + .map(ExcerptId::from_proto) + .collect::>(); + removals.sort_by(|a, b| a.cmp(&b, &multibuffer)); + + let selections = message + .selections + .into_iter() + .filter_map(|selection| deserialize_selection(&multibuffer, selection)) + .collect::>(); + 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::>() + }); + + 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() @@ -315,7 +333,7 @@ impl FollowableItem for Editor { None } }); - + multibuffer.insert_excerpts_with_ids_after( ExcerptId::from_proto(previous_excerpt_id), buffer, @@ -331,24 +349,19 @@ impl FollowableItem for Editor { cx, ); } - + multibuffer.remove_excerpts(removals, cx); }); - if !selections.is_empty() { - self.set_selections_from_remote(selections, cx); - self.request_autoscroll_remotely(Autoscroll::newest(), cx); + this.set_selections_from_remote(selections, cx); + this.request_autoscroll_remotely(Autoscroll::newest(), cx); } else if let Some(anchor) = scroll_top_anchor { - self.set_scroll_top_anchor( - anchor, - vec2f(message.scroll_x, message.scroll_y), - cx, - ); + this.set_scroll_top_anchor(anchor, vec2f(message.scroll_x, message.scroll_y), cx); } - } - } - Ok(()) + }); + Ok(()) + }) } fn should_unfollow_on_event(event: &Self::Event, _: &AppContext) -> bool { diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 60c46d3ef0..e60c1fe3be 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -11,14 +11,18 @@ pub mod sidebar; mod status_bar; mod toolbar; -use anyhow::{anyhow, Context, Result}; +use anyhow::{anyhow, Result}; use call::ActiveCall; use client::{proto, Client, PeerId, TypedEnvelope, UserStore}; use collections::{hash_map, HashMap, HashSet}; use dock::{DefaultItemFactory, Dock, 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::*, @@ -466,7 +470,7 @@ pub trait FollowableItem: Item { project: &ModelHandle, message: proto::update_view::Variant, cx: &mut ViewContext, - ) -> Result<()>; + ) -> Task>; fn set_leader_replica_id(&mut self, leader_replica_id: Option, cx: &mut ViewContext); fn should_unfollow_on_event(event: &Self::Event, cx: &AppContext) -> bool; @@ -486,7 +490,7 @@ pub trait FollowableItemHandle: ItemHandle { project: &ModelHandle, message: proto::update_view::Variant, cx: &mut MutableAppContext, - ) -> Result<()>; + ) -> Task>; fn should_unfollow_on_event(&self, event: &dyn Any, cx: &AppContext) -> bool; } @@ -519,7 +523,7 @@ impl FollowableItemHandle for ViewHandle { project: &ModelHandle, message: proto::update_view::Variant, cx: &mut MutableAppContext, - ) -> Result<()> { + ) -> Task> { self.update(cx, |this, cx| this.apply_update_proto(project, message, cx)) } @@ -1089,6 +1093,8 @@ pub struct Workspace { last_leaders_by_pane: HashMap, PeerId>, window_edited: bool, active_call: Option<(ModelHandle, Vec)>, + leader_updates_tx: mpsc::UnboundedSender<(PeerId, proto::UpdateFollowers)>, + _apply_leader_updates: Task>, _observe_current_user: Task<()>, } @@ -1102,13 +1108,7 @@ type FollowerStatesByLeader = HashMap, Follower #[derive(Default)] struct FollowerState { active_view_id: Option, - items_by_leader_view_id: HashMap, -} - -#[derive(Debug)] -enum FollowerItem { - Loading(Vec), - Loaded(Box), + items_by_leader_view_id: HashMap>, } impl Workspace { @@ -1171,10 +1171,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 dock = Dock::new(cx, dock_default_factory); @@ -1234,6 +1248,8 @@ impl Workspace { window_edited: false, active_call, _observe_current_user, + _apply_leader_updates, + leader_updates_tx, }; this.project_remote_id_changed(this.project.read(cx).remote_id(), cx); cx.defer(|this, cx| this.update_window_title(cx)); @@ -2119,9 +2135,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); } } } @@ -2167,8 +2181,15 @@ impl Workspace { state.active_view_id = response.active_view_id; 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(()) })) @@ -2214,9 +2235,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() { @@ -2459,46 +2478,51 @@ impl Workspace { this: ViewHandle, envelope: TypedEnvelope, _: Arc, - 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, + 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 = update_active_view.id; + } + } + }); } 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"))?; + let variant = update_view + .variant + .ok_or_else(|| anyhow!("missing update view variant"))?; + let mut tasks = Vec::new(); + this.update(cx, |this, cx| { let project = this.project.clone(); - 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(&project, variant, cx).log_err(); + if let Some(state) = this.follower_states_by_leader.get_mut(&leader_id) { + for state in state.values_mut() { + if let Some(item) = state.items_by_leader_view_id.get(&update_view.id) { + tasks.push(item.apply_update_proto(&project, variant.clone(), cx)); } - FollowerItem::Loading(updates) => updates.push(variant), } - }); - 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() @@ -2506,13 +2530,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(()) } @@ -2575,29 +2596,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(&this.project, 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(()) } @@ -2631,23 +2635,6 @@ impl Workspace { }) } - fn update_leader_state( - &mut self, - leader_id: PeerId, - cx: &mut ViewContext, - mut update_fn: impl FnMut(&mut FollowerState, &mut ViewContext), - ) { - 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) -> Option<()> { cx.notify(); @@ -2660,7 +2647,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)) { From 82824f78b6fecf9bfb5b29c9c581a6fe996f3d09 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 1 Dec 2022 16:43:39 -0800 Subject: [PATCH 08/14] Make each Zed instance use half the screen in 'start-local-collaboration' script --- script/start-local-collaboration | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/script/start-local-collaboration b/script/start-local-collaboration index 643205532c..4371969580 100755 --- a/script/start-local-collaboration +++ b/script/start-local-collaboration @@ -17,23 +17,32 @@ MESSAGE exit 1 fi -github_login=$(curl -sH "Authorization: bearer $GITHUB_TOKEN" https://api.github.com/user | jq -r .login) - -other_github_login=nathansobo -if [[ $github_login == $other_github_login ]]; then - other_github_login=as-cii +# 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. +screen_size=($(system_profiler SPDisplaysDataType | grep Resolution | head -n1 | egrep -o '[0-9]+')) +width=$(expr ${screen_size[0]} / 2) +height=${screen_size[1]} +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=800,600 +export ZED_WINDOW_SIZE=${width},${height} cargo build sleep 0.1 +# Start the two Zed child processes. Open the given paths with the first instance. trap "trap - SIGTERM && kill -- -$$" SIGINT SIGTERM EXIT -ZED_WINDOW_POSITION=0,0 ZED_IMPERSONATE=${github_login} target/debug/Zed $@ & +ZED_IMPERSONATE=${username_1} ZED_WINDOW_POSITION=${position_1} target/debug/Zed $@ & sleep 0.1 -ZED_WINDOW_POSITION=800,0 ZED_IMPERSONATE=${other_github_login} target/debug/Zed & +ZED_IMPERSONATE=${username_2} ZED_WINDOW_POSITION=${position_2} target/debug/Zed & sleep 0.1 wait From 09d3fbf04fb7d77819979cfb8bdd90b743a7ed9b Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 14 Dec 2022 11:08:08 -0800 Subject: [PATCH 09/14] In editor following test, apply excerpt removals to both followers --- crates/editor/src/editor_tests.rs | 46 +++++++++++++++---------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 510a94c2f1..74fcd27fc3 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -5129,9 +5129,9 @@ async fn test_following_with_multiple_excerpts(cx: &mut gpui::TestAppContext) { .await .unwrap(); - let follower_1_update = Rc::new(RefCell::new(None)); + let update_message = Rc::new(RefCell::new(None)); follower_1.update(cx, { - let update = follower_1_update.clone(); + let update = update_message.clone(); |_, cx| { cx.subscribe(&leader, move |_, leader, event, cx| { leader @@ -5192,6 +5192,19 @@ async fn test_following_with_multiple_excerpts(cx: &mut gpui::TestAppContext) { }); }); + // 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 @@ -5206,22 +5219,6 @@ async fn test_following_with_multiple_excerpts(cx: &mut gpui::TestAppContext) { leader.read_with(cx, Editor::text) ); - // Apply the update of adding the excerpts. - follower_1 - .update(cx, |follower, cx| { - follower.apply_update_proto( - &project, - follower_1_update.borrow_mut().take().unwrap(), - cx, - ) - }) - .await - .unwrap(); - assert_eq!( - follower_1.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| { @@ -5234,14 +5231,17 @@ async fn test_following_with_multiple_excerpts(cx: &mut gpui::TestAppContext) { // Apply the update of removing the excerpts. follower_1 .update(cx, |follower, cx| { - follower.apply_update_proto( - &project, - follower_1_update.borrow_mut().take().unwrap(), - 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) From f99f581bfc50b72945d585e8810f4d7be42ab8e8 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 14 Dec 2022 11:09:33 -0800 Subject: [PATCH 10/14] Clean up state matching in from_state_proto using let/else statements --- crates/editor/src/items.rs | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index cdabf7193b..6a22c0e289 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -43,15 +43,8 @@ impl FollowableItem for Editor { state: &mut Option, cx: &mut MutableAppContext, ) -> Option>>> { - 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 Some(proto::view::Variant::Editor(_)) = state else { return None }; + let Some(proto::view::Variant::Editor(state)) = state.take() else { unreachable!() }; let replica_id = project.read(cx).replica_id(); let buffer_ids = state From 43b7e16c890b81eb5984121b83d121440a5f7433 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 14 Dec 2022 11:50:15 -0800 Subject: [PATCH 11/14] Handle retina screens in start-local-collaboration script --- script/start-local-collaboration | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/script/start-local-collaboration b/script/start-local-collaboration index 4371969580..9c63b301e5 100755 --- a/script/start-local-collaboration +++ b/script/start-local-collaboration @@ -25,9 +25,13 @@ if [[ $username_1 == $username_2 ]]; then fi # Make each Zed instance take up half of the screen. -screen_size=($(system_profiler SPDisplaysDataType | grep Resolution | head -n1 | egrep -o '[0-9]+')) -width=$(expr ${screen_size[0]} / 2) -height=${screen_size[1]} +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 @@ -37,12 +41,10 @@ export ZED_SERVER_URL=http://localhost:8080 export ZED_WINDOW_SIZE=${width},${height} cargo build -sleep 0.1 +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 $@ & -sleep 0.1 ZED_IMPERSONATE=${username_2} ZED_WINDOW_POSITION=${position_2} target/debug/Zed & -sleep 0.1 wait From 70efd2bebed14e3a4f8573d522ca00c55aaa07b8 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 14 Dec 2022 14:40:07 -0800 Subject: [PATCH 12/14] Introduce a ViewId message, identifying views across calls --- crates/editor/src/editor.rs | 4 +- crates/editor/src/editor_tests.rs | 25 +++++++++-- crates/editor/src/items.rs | 25 +++++++---- crates/rpc/proto/zed.proto | 13 ++++-- crates/workspace/src/item.rs | 30 ++++++++++--- crates/workspace/src/workspace.rs | 75 ++++++++++++++++++++++++------- 6 files changed, 134 insertions(+), 38 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index ddfe0c00e0..8a3c7452ef 100644 --- a/crates/editor/src/editor.rs +++ b/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, input_enabled: bool, leader_replica_id: Option, + remote_id: Option, hover_state: HoverState, link_go_to_definition_state: LinkGoToDefinitionState, _subscriptions: Vec, @@ -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![ diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 74fcd27fc3..c3c15bb5b4 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -3,6 +3,7 @@ use std::{cell::RefCell, rc::Rc, time::Instant}; use drag_and_drop::DragAndDrop; use futures::StreamExt; use indoc::indoc; +use rpc::PeerId; use unindent::Unindent; use super::*; @@ -24,7 +25,7 @@ use util::{ }; use workspace::{ item::{FollowableItem, ItemHandle}, - NavigationEntry, Pane, + NavigationEntry, Pane, ViewId, }; #[gpui::test] @@ -5123,7 +5124,16 @@ async fn test_following_with_multiple_excerpts(cx: &mut gpui::TestAppContext) { 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(), &mut state_message, cx) + Editor::from_state_proto( + pane.clone(), + project.clone(), + ViewId { + creator: PeerId(0), + id: 0, + }, + &mut state_message, + cx, + ) }) .unwrap() .await @@ -5209,7 +5219,16 @@ async fn test_following_with_multiple_excerpts(cx: &mut gpui::TestAppContext) { 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(), &mut state_message, cx) + Editor::from_state_proto( + pane.clone(), + project.clone(), + ViewId { + creator: PeerId(0), + id: 0, + }, + &mut state_message, + cx, + ) }) .unwrap() .await diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 6a22c0e289..0057df778b 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -28,24 +28,32 @@ use std::{ }; 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, + 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 { + self.remote_id + } + fn from_state_proto( pane: ViewHandle, project: ModelHandle, + remote_id: ViewId, state: &mut Option, cx: &mut MutableAppContext, ) -> Option>>> { 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 @@ -63,13 +71,13 @@ impl FollowableItem for Editor { 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::(); - if state.singleton && buffers.len() == 1 { - editors.find(|editor| { - editor.read(cx).buffer.read(cx).as_singleton().as_ref() == Some(&buffers[0]) - }) - } else { - None - } + 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]) + }) }); let editor = editor.unwrap_or_else(|| { @@ -112,6 +120,7 @@ impl FollowableItem for Editor { }); editor.update(&mut cx, |editor, cx| { + editor.remote_id = Some(remote_id); let buffer = editor.buffer.read(cx).read(cx); let selections = state .selections diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 6bfef6c21a..e951e6f3bf 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -796,7 +796,7 @@ message Follow { } message FollowResponse { - optional uint64 active_view_id = 1; + optional ViewId active_view_id = 1; repeated View views = 2; } @@ -824,13 +824,18 @@ message GetPrivateUserInfoResponse { // Entities +message ViewId { + uint32 creator = 1; + uint64 id = 2; +} + message UpdateActiveView { - optional uint64 id = 1; + optional ViewId id = 1; optional uint32 leader_id = 2; } message UpdateView { - uint64 id = 1; + ViewId id = 1; optional uint32 leader_id = 2; oneof variant { @@ -848,7 +853,7 @@ message UpdateView { } message View { - uint64 id = 1; + ViewId id = 1; optional uint32 leader_id = 2; oneof variant { diff --git a/crates/workspace/src/item.rs b/crates/workspace/src/item.rs index 63394f361d..1d6b4a9eb5 100644 --- a/crates/workspace/src/item.rs +++ b/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 ItemHandle for ViewHandle { 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).map(|id| id.0), }), @@ -332,7 +338,9 @@ impl ItemHandle for ViewHandle { 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: leader_id.map(|id| id.0), }, @@ -584,10 +592,12 @@ pub trait ProjectItem: Item { } pub trait FollowableItem: Item { + fn remote_id(&self) -> Option; fn to_state_proto(&self, cx: &AppContext) -> Option; fn from_state_proto( pane: ViewHandle, project: ModelHandle, + id: ViewId, state: &mut Option, cx: &mut MutableAppContext, ) -> Option>>>; @@ -609,6 +619,7 @@ pub trait FollowableItem: Item { } pub trait FollowableItemHandle: ItemHandle { + fn remote_id(&self, client: &Arc, cx: &AppContext) -> Option; fn set_leader_replica_id(&self, leader_replica_id: Option, cx: &mut MutableAppContext); fn to_state_proto(&self, cx: &AppContext) -> Option; fn add_event_to_update_proto( @@ -627,6 +638,15 @@ pub trait FollowableItemHandle: ItemHandle { } impl FollowableItemHandle for ViewHandle { + fn remote_id(&self, client: &Arc, cx: &AppContext) -> Option { + 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, cx: &mut MutableAppContext) { self.update(cx, |this, cx| { this.set_leader_replica_id(leader_replica_id, cx) diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index ecc6a43fcc..5cf65568f8 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -318,6 +318,7 @@ pub fn register_project_item(cx: &mut MutableAppContext) { type FollowableItemBuilder = fn( ViewHandle, ModelHandle, + ViewId, &mut Option, &mut MutableAppContext, ) -> Option>>>; @@ -333,8 +334,8 @@ pub fn register_followable_item(cx: &mut MutableAppContext) { builders.insert( TypeId::of::(), ( - |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<_>) }) }) @@ -496,6 +497,12 @@ pub struct Workspace { _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, @@ -505,8 +512,8 @@ type FollowerStatesByLeader = HashMap, Follower #[derive(Default)] struct FollowerState { - active_view_id: Option, - items_by_leader_view_id: HashMap>, + active_view_id: Option, + items_by_leader_view_id: HashMap>, } impl Workspace { @@ -1454,7 +1461,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).map(|id| id.0), }), cx, @@ -1643,7 +1654,7 @@ 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 = response.active_view_id.map(ViewId::from_proto); Ok::<_, anyhow::Error>(()) })?; Self::add_views_from_leader( @@ -1891,14 +1902,18 @@ impl Workspace { mut cx: AsyncAppContext, ) -> Result { 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 @@ -1909,11 +1924,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), }) @@ -1964,7 +1979,8 @@ impl Workspace { 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 = update_active_view.id; + state.active_view_id = + update_active_view.id.clone().map(ViewId::from_proto); } } }); @@ -1973,12 +1989,18 @@ impl Workspace { 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() { - if let Some(item) = state.items_by_leader_view_id.get(&update_view.id) { + if let Some(item) = state + .items_by_leader_view_id + .get(&ViewId::from_proto(id.clone())) + { tasks.push(item.apply_update_proto(&project, variant.clone(), cx)); } } @@ -2031,16 +2053,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()); @@ -2561,6 +2586,22 @@ impl View for Workspace { } } +impl ViewId { + pub(crate) fn from_proto(message: proto::ViewId) -> Self { + Self { + creator: PeerId(message.creator), + id: message.id, + } + } + + pub(crate) fn to_proto(&self) -> proto::ViewId { + proto::ViewId { + creator: self.creator.0, + id: self.id, + } + } +} + pub trait WorkspaceHandle { fn file_project_paths(&self, cx: &AppContext) -> Vec; } From e4c5dfcf6c91e8f9c7375248d392fd24e07d055e Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 14 Dec 2022 15:05:35 -0800 Subject: [PATCH 13/14] Use run_until_parked instead of 'condition' in all integration tests --- crates/collab/src/integration_tests.rs | 185 ++++++++++++------------- 1 file changed, 88 insertions(+), 97 deletions(-) diff --git a/crates/collab/src/integration_tests.rs b/crates/collab/src/integration_tests.rs index aca5f77fe9..e6a2942702 100644 --- a/crates/collab/src/integration_tests.rs +++ b/crates/collab/src/integration_tests.rs @@ -25,7 +25,7 @@ use gpui::{ ModelHandle, Task, 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; @@ -764,17 +764,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 @@ -797,12 +802,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)] @@ -956,13 +965,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. @@ -1347,9 +1352,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) }); @@ -1999,9 +2003,8 @@ async fn test_buffer_conflict_after_save(cx_a: &mut TestAppContext, cx_b: &mut T }); 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()); }); @@ -2057,12 +2060,9 @@ async fn test_buffer_reloading(cx_a: &mut TestAppContext, cx_b: &mut TestAppCont .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); @@ -2113,7 +2113,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)] @@ -2142,9 +2143,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 @@ -2154,9 +2154,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)] @@ -2671,9 +2670,10 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu }); 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| { @@ -2735,14 +2735,13 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu .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() }"); @@ -2771,16 +2770,19 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu ); // 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)] @@ -2822,9 +2824,8 @@ async fn test_reloading_buffer_manually(cx_a: &mut TestAppContext, cx_b: &mut Te 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 @@ -2835,12 +2836,9 @@ async fn test_reloading_buffer_manually(cx_a: &mut TestAppContext, cx_b: &mut Te ) .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| { @@ -3836,9 +3834,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::(); @@ -4917,11 +4914,10 @@ 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 navigates back and forth, client B does so as well. workspace_a @@ -4929,47 +4925,42 @@ 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() - }) - .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| { From 954c9ac3fd740c0836f6b4ddc24b0bccd23e8802 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 14 Dec 2022 15:28:58 -0800 Subject: [PATCH 14/14] Add integration test coverage for following into multibuffers --- crates/collab/src/integration_tests.rs | 92 ++++++++++++++++++++++---- 1 file changed, 80 insertions(+), 12 deletions(-) diff --git a/crates/collab/src/integration_tests.rs b/crates/collab/src/integration_tests.rs index e6a2942702..fec302f8d4 100644 --- a/crates/collab/src/integration_tests.rs +++ b/crates/collab/src/integration_tests.rs @@ -13,8 +13,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 _}; @@ -4813,9 +4813,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; @@ -4919,7 +4919,67 @@ async fn test_following( 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::() + .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 + .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_b1.id()); + }); + workspace_a .update(cx_a, |workspace, cx| { workspace::Pane::go_back(workspace, None, cx) @@ -5029,13 +5089,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.