Replicate multibuffer excerpt additions and removals to followers

This commit is contained in:
Max Brunsfeld 2022-11-30 13:20:13 -08:00
parent a48cd9125b
commit 9314c0e313
5 changed files with 254 additions and 41 deletions

View file

@ -6377,6 +6377,18 @@ impl Editor {
self.refresh_code_actions(cx); self.refresh_code_actions(cx);
cx.emit(Event::BufferEdited); 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::Reparsed => cx.emit(Event::Reparsed),
multi_buffer::Event::DirtyChanged => cx.emit(Event::DirtyChanged), multi_buffer::Event::DirtyChanged => cx.emit(Event::DirtyChanged),
multi_buffer::Event::Saved => cx.emit(Event::Saved), multi_buffer::Event::Saved => cx.emit(Event::Saved),
@ -6386,7 +6398,6 @@ impl Editor {
multi_buffer::Event::DiagnosticsUpdated => { multi_buffer::Event::DiagnosticsUpdated => {
self.refresh_active_diagnostics(cx); self.refresh_active_diagnostics(cx);
} }
_ => {}
} }
} }

View file

@ -4967,19 +4967,27 @@ fn test_highlighted_ranges(cx: &mut gpui::MutableAppContext) {
} }
#[gpui::test] #[gpui::test]
fn test_following(cx: &mut gpui::MutableAppContext) { async fn test_following(cx: &mut gpui::TestAppContext) {
let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx); 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 buffer = project.update(cx, |project, cx| {
let buffer = project
let (_, leader) = cx.add_window(Default::default(), |cx| build_editor(buffer.clone(), cx)); .create_buffer(&sample_text(16, 8, 'a'), None, cx)
let (_, follower) = cx.add_window( .unwrap();
WindowOptions { cx.add_model(|cx| MultiBuffer::singleton(buffer, cx))
bounds: WindowBounds::Fixed(RectF::from_points(vec2f(0., 0.), vec2f(10., 80.))), });
..Default::default() let (_, leader) = cx.add_window(|cx| build_editor(buffer.clone(), cx));
}, let (_, follower) = cx.update(|cx| {
|cx| build_editor(buffer.clone(), 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)); let pending_update = Rc::new(RefCell::new(None));
follower.update(cx, { follower.update(cx, {
@ -5000,10 +5008,12 @@ fn test_following(cx: &mut gpui::MutableAppContext) {
}); });
follower.update(cx, |follower, cx| { follower.update(cx, |follower, cx| {
follower follower
.apply_update_proto(pending_update.borrow_mut().take().unwrap(), cx) .apply_update_proto(&project, pending_update.borrow_mut().take().unwrap(), cx)
.unwrap(); .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 // Update the scroll position only
leader.update(cx, |leader, cx| { leader.update(cx, |leader, cx| {
@ -5011,7 +5021,7 @@ fn test_following(cx: &mut gpui::MutableAppContext) {
}); });
follower.update(cx, |follower, cx| { follower.update(cx, |follower, cx| {
follower follower
.apply_update_proto(pending_update.borrow_mut().take().unwrap(), cx) .apply_update_proto(&project, pending_update.borrow_mut().take().unwrap(), cx)
.unwrap(); .unwrap();
}); });
assert_eq!( assert_eq!(
@ -5028,12 +5038,14 @@ fn test_following(cx: &mut gpui::MutableAppContext) {
follower.update(cx, |follower, cx| { follower.update(cx, |follower, cx| {
let initial_scroll_position = follower.scroll_position(cx); let initial_scroll_position = follower.scroll_position(cx);
follower follower
.apply_update_proto(pending_update.borrow_mut().take().unwrap(), cx) .apply_update_proto(&project, pending_update.borrow_mut().take().unwrap(), cx)
.unwrap(); .unwrap();
assert_eq!(follower.scroll_position(cx), initial_scroll_position); assert_eq!(follower.scroll_position(cx), initial_scroll_position);
assert!(follower.autoscroll_request.is_some()); 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 // Creating a pending selection that precedes another selection
leader.update(cx, |leader, cx| { leader.update(cx, |leader, cx| {
@ -5042,10 +5054,12 @@ fn test_following(cx: &mut gpui::MutableAppContext) {
}); });
follower.update(cx, |follower, cx| { follower.update(cx, |follower, cx| {
follower follower
.apply_update_proto(pending_update.borrow_mut().take().unwrap(), cx) .apply_update_proto(&project, pending_update.borrow_mut().take().unwrap(), cx)
.unwrap(); .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 // Extend the pending selection so that it surrounds another selection
leader.update(cx, |leader, cx| { leader.update(cx, |leader, cx| {
@ -5053,10 +5067,143 @@ fn test_following(cx: &mut gpui::MutableAppContext) {
}); });
follower.update(cx, |follower, cx| { follower.update(cx, |follower, cx| {
follower follower
.apply_update_proto(pending_update.borrow_mut().take().unwrap(), cx) .apply_update_proto(&project, pending_update.borrow_mut().take().unwrap(), cx)
.unwrap(); .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] #[test]

View file

@ -72,10 +72,6 @@ impl FollowableItem for Editor {
editors.find(|editor| { editors.find(|editor| {
editor.read(cx).buffer.read(cx).as_singleton().as_ref() == Some(&buffers[0]) 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 { } else {
None None
} }
@ -231,16 +227,17 @@ impl FollowableItem for Editor {
predecessor, predecessor,
excerpts, excerpts,
} => { } => {
let buffer_id = buffer.read(cx).remote_id();
let mut excerpts = excerpts.iter(); let mut excerpts = excerpts.iter();
if let Some((id, range)) = excerpts.next() { if let Some((id, range)) = excerpts.next() {
update.inserted_excerpts.push(proto::ExcerptInsertion { update.inserted_excerpts.push(proto::ExcerptInsertion {
previous_excerpt_id: Some(predecessor.to_proto()), 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)| { update.inserted_excerpts.extend(excerpts.map(|(id, range)| {
proto::ExcerptInsertion { proto::ExcerptInsertion {
previous_excerpt_id: None, 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( fn apply_update_proto(
&mut self, &mut self,
project: &ModelHandle<Project>,
message: update_view::Variant, message: update_view::Variant,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) -> Result<()> { ) -> Result<()> {
match message { match message {
update_view::Variant::Editor(message) => { update_view::Variant::Editor(message) => {
let buffer = self.buffer.read(cx); let multibuffer = self.buffer.read(cx);
let buffer = buffer.read(cx); let multibuffer = multibuffer.read(cx);
let mut removals = message
.deleted_excerpts
.into_iter()
.map(ExcerptId::from_proto)
.collect::<Vec<_>>();
removals.sort_by(|a, b| a.cmp(&b, &multibuffer));
let selections = message let selections = message
.selections .selections
.into_iter() .into_iter()
.filter_map(|selection| deserialize_selection(&buffer, selection)) .filter_map(|selection| deserialize_selection(&multibuffer, selection))
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let scroll_top_anchor = message let scroll_top_anchor = message
.scroll_top_anchor .scroll_top_anchor
.and_then(|anchor| deserialize_anchor(&buffer, anchor)); .and_then(|anchor| deserialize_anchor(&multibuffer, anchor));
drop(buffer); 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() { if !selections.is_empty() {
self.set_selections_from_remote(selections, cx); self.set_selections_from_remote(selections, cx);
@ -318,14 +362,13 @@ impl FollowableItem for Editor {
} }
fn serialize_excerpt( fn serialize_excerpt(
buffer: &ModelHandle<Buffer>, buffer_id: u64,
id: &ExcerptId, id: &ExcerptId,
range: &ExcerptRange<language::Anchor>, range: &ExcerptRange<language::Anchor>,
cx: &AppContext,
) -> Option<proto::Excerpt> { ) -> Option<proto::Excerpt> {
Some(proto::Excerpt { Some(proto::Excerpt {
id: id.to_proto(), id: id.to_proto(),
buffer_id: buffer.read(cx).remote_id(), buffer_id,
context_start: Some(serialize_text_anchor(&range.context.start)), context_start: Some(serialize_text_anchor(&range.context.start)),
context_end: Some(serialize_text_anchor(&range.context.end)), context_end: Some(serialize_text_anchor(&range.context.end)),
primary_start: range primary_start: range
@ -390,7 +433,7 @@ fn deserialize_anchor(buffer: &MultiBufferSnapshot, anchor: proto::EditorAnchor)
Some(Anchor { Some(Anchor {
excerpt_id, excerpt_id,
text_anchor: language::proto::deserialize_anchor(anchor.anchor?)?, 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),
}) })
} }

View file

@ -463,6 +463,7 @@ pub trait FollowableItem: Item {
) -> bool; ) -> bool;
fn apply_update_proto( fn apply_update_proto(
&mut self, &mut self,
project: &ModelHandle<Project>,
message: proto::update_view::Variant, message: proto::update_view::Variant,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) -> Result<()>; ) -> Result<()>;
@ -482,6 +483,7 @@ pub trait FollowableItemHandle: ItemHandle {
) -> bool; ) -> bool;
fn apply_update_proto( fn apply_update_proto(
&self, &self,
project: &ModelHandle<Project>,
message: proto::update_view::Variant, message: proto::update_view::Variant,
cx: &mut MutableAppContext, cx: &mut MutableAppContext,
) -> Result<()>; ) -> Result<()>;
@ -514,10 +516,11 @@ impl<T: FollowableItem> FollowableItemHandle for ViewHandle<T> {
fn apply_update_proto( fn apply_update_proto(
&self, &self,
project: &ModelHandle<Project>,
message: proto::update_view::Variant, message: proto::update_view::Variant,
cx: &mut MutableAppContext, cx: &mut MutableAppContext,
) -> Result<()> { ) -> 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 { fn should_unfollow_on_event(&self, event: &dyn Any, cx: &AppContext) -> bool {
@ -2477,6 +2480,7 @@ impl Workspace {
let variant = update_view let variant = update_view
.variant .variant
.ok_or_else(|| anyhow!("missing 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| { this.update_leader_state(leader_id, cx, |state, cx| {
let variant = variant.clone(); let variant = variant.clone();
match state match state
@ -2485,7 +2489,7 @@ impl Workspace {
.or_insert(FollowerItem::Loading(Vec::new())) .or_insert(FollowerItem::Loading(Vec::new()))
{ {
FollowerItem::Loaded(item) => { 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), FollowerItem::Loading(updates) => updates.push(variant),
} }
@ -2576,7 +2580,7 @@ impl Workspace {
let e = e.into_mut(); let e = e.into_mut();
if let FollowerItem::Loading(updates) = e { if let FollowerItem::Loading(updates) = e {
for update in updates.drain(..) { 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") .context("failed to apply view update")
.log_err(); .log_err();
} }

View file

@ -12,8 +12,16 @@ function isStyleSet(key: any): key is StyleSets {
"negative", "negative",
].includes(key); ].includes(key);
} }
function isStyle(key: any): key is Styles { 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( function getStyle(
layer: Layer, layer: Layer,