From 9314c0e313cca95351856449fa61eb1638d03663 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 30 Nov 2022 13:20:13 -0800 Subject: [PATCH] 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 cc873cbffa8de019e61fc5bba26bda08c82099a7..de2db402043da09f6f20f71e7b595a8e6a8cdaa7 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 a9806220775728c2444570af01a197abe82ebef9..b84d304d549117942863cbb9d5521fb5a9ce504f 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); - - cx.set_global(Settings::test(cx)); +async fn test_following(cx: &mut gpui::TestAppContext) { + Settings::test_async(cx); + let fs = FakeFs::new(cx.background()); + let project = Project::test(fs, ["/file.rs".as_ref()], cx).await; - let (_, leader) = cx.add_window(Default::default(), |cx| build_editor(buffer.clone(), cx)); - let (_, follower) = cx.add_window( - WindowOptions { - bounds: WindowBounds::Fixed(RectF::from_points(vec2f(0., 0.), vec2f(10., 80.))), - ..Default::default() - }, - |cx| build_editor(buffer.clone(), cx), - ); + let buffer = project.update(cx, |project, cx| { + let buffer = project + .create_buffer(&sample_text(16, 8, 'a'), None, cx) + .unwrap(); + cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)) + }); + let (_, leader) = cx.add_window(|cx| build_editor(buffer.clone(), cx)); + let (_, follower) = cx.update(|cx| { + cx.add_window( + WindowOptions { + bounds: WindowBounds::Fixed(RectF::from_points(vec2f(0., 0.), vec2f(10., 80.))), + ..Default::default() + }, + |cx| build_editor(buffer.clone(), cx), + ) + }); let 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 2c0b8b834e60378af84798d4ad998fb6836fc070..9b397b6ee500d08d6f0c4be0888a88668454bd2d 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 7082b61949fd468b9e84795aa39ac135c992ecb8..60c46d3ef0c07dce8b85b8483e835b13ecc7686b 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 3244e7e4eab418aa9e365076437da56123dc6aa4..847b937416e0dfc5751bde28f1a84f4d53d8166a 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,