Merge pull request #1234 from zed-industries/fix-editor-cloning

Max Brunsfeld created

Clone fold and selection state correctly when splitting an editor

Change summary

crates/editor/src/display_map.rs           | 11 +++
crates/editor/src/editor.rs                | 76 ++++++++++++++++-------
crates/editor/src/element.rs               |  6 
crates/editor/src/selections_collection.rs |  7 ++
crates/editor/src/test.rs                  |  4 
5 files changed, 74 insertions(+), 30 deletions(-)

Detailed changes

crates/editor/src/display_map.rs 🔗

@@ -3,7 +3,7 @@ mod fold_map;
 mod tab_map;
 mod wrap_map;
 
-use crate::{Anchor, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint};
+use crate::{Anchor, AnchorRangeExt, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint};
 use block_map::{BlockMap, BlockPoint};
 use collections::{HashMap, HashSet};
 use fold_map::FoldMap;
@@ -97,6 +97,15 @@ impl DisplayMap {
         }
     }
 
+    pub fn set_state(&mut self, other: &DisplaySnapshot, cx: &mut ModelContext<Self>) {
+        self.fold(
+            other
+                .folds_in_range(0..other.buffer_snapshot.len())
+                .map(|fold| fold.to_offset(&other.buffer_snapshot)),
+            cx,
+        );
+    }
+
     pub fn fold<T: ToOffset>(
         &mut self,
         ranges: impl IntoIterator<Item = Range<T>>,

crates/editor/src/editor.rs 🔗

@@ -892,14 +892,7 @@ impl Editor {
     ) -> Self {
         let buffer = cx.add_model(|cx| Buffer::new(0, String::new(), cx));
         let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
-        Self::new(
-            EditorMode::SingleLine,
-            buffer,
-            None,
-            field_editor_style,
-            None,
-            cx,
-        )
+        Self::new(EditorMode::SingleLine, buffer, None, field_editor_style, cx)
     }
 
     pub fn auto_height(
@@ -914,7 +907,6 @@ impl Editor {
             buffer,
             None,
             field_editor_style,
-            None,
             cx,
         )
     }
@@ -925,7 +917,7 @@ impl Editor {
         cx: &mut ViewContext<Self>,
     ) -> Self {
         let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
-        Self::new(EditorMode::Full, buffer, project, None, None, cx)
+        Self::new(EditorMode::Full, buffer, project, None, cx)
     }
 
     pub fn for_multibuffer(
@@ -933,7 +925,7 @@ impl Editor {
         project: Option<ModelHandle<Project>>,
         cx: &mut ViewContext<Self>,
     ) -> Self {
-        Self::new(EditorMode::Full, buffer, project, None, None, cx)
+        Self::new(EditorMode::Full, buffer, project, None, cx)
     }
 
     pub fn clone(&self, cx: &mut ViewContext<Self>) -> Self {
@@ -942,9 +934,15 @@ impl Editor {
             self.buffer.clone(),
             self.project.clone(),
             self.get_field_editor_theme,
-            Some(self.selections.clone()),
             cx,
         );
+        self.display_map.update(cx, |display_map, cx| {
+            let snapshot = display_map.snapshot(cx);
+            clone.display_map.update(cx, |display_map, cx| {
+                display_map.set_state(&snapshot, cx);
+            });
+        });
+        clone.selections.set_state(&self.selections);
         clone.scroll_position = self.scroll_position;
         clone.scroll_top_anchor = self.scroll_top_anchor.clone();
         clone.searchable = self.searchable;
@@ -956,7 +954,6 @@ impl Editor {
         buffer: ModelHandle<MultiBuffer>,
         project: Option<ModelHandle<Project>>,
         get_field_editor_theme: Option<GetFieldEditorTheme>,
-        selections: Option<SelectionsCollection>,
         cx: &mut ViewContext<Self>,
     ) -> Self {
         let display_map = cx.add_model(|cx| {
@@ -977,8 +974,7 @@ impl Editor {
         cx.observe(&display_map, Self::on_display_map_changed)
             .detach();
 
-        let selections = selections
-            .unwrap_or_else(|| SelectionsCollection::new(display_map.clone(), buffer.clone()));
+        let selections = SelectionsCollection::new(display_map.clone(), buffer.clone());
 
         let mut this = Self {
             handle: cx.weak_handle(),
@@ -6468,23 +6464,55 @@ mod tests {
     }
 
     #[gpui::test]
-    fn test_clone_with_selections(cx: &mut gpui::MutableAppContext) {
+    fn test_clone(cx: &mut gpui::MutableAppContext) {
         let (text, selection_ranges) = marked_text_ranges(indoc! {"
-            The qu[ick brown
-            fox jum]ps over
-            the lazy dog
+            one
+            two
+            three[]
+            four
+            five[]
         "});
         cx.set_global(Settings::test(cx));
         let buffer = MultiBuffer::build_simple(&text, cx);
 
-        let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, cx));
+        let (_, editor) = cx.add_window(Default::default(), |cx| build_editor(buffer, cx));
+
+        editor.update(cx, |editor, cx| {
+            editor.change_selections(None, cx, |s| s.select_ranges(selection_ranges.clone()));
+            editor.fold_ranges(
+                [
+                    Point::new(1, 0)..Point::new(2, 0),
+                    Point::new(3, 0)..Point::new(4, 0),
+                ],
+                cx,
+            );
+        });
 
-        let cloned_editor = view.update(cx, |view, cx| {
-            view.change_selections(None, cx, |s| s.select_ranges(selection_ranges.clone()));
-            view.clone(cx)
+        let (_, cloned_editor) = editor.update(cx, |editor, cx| {
+            cx.add_window(Default::default(), |cx| editor.clone(cx))
         });
 
-        assert_set_eq!(cloned_editor.selections.ranges(cx), selection_ranges);
+        let snapshot = editor.update(cx, |e, cx| e.snapshot(cx));
+        let cloned_snapshot = cloned_editor.update(cx, |e, cx| e.snapshot(cx));
+
+        assert_eq!(
+            cloned_editor.update(cx, |e, cx| e.display_text(cx)),
+            editor.update(cx, |e, cx| e.display_text(cx))
+        );
+        assert_eq!(
+            cloned_snapshot
+                .folds_in_range(0..text.len())
+                .collect::<Vec<_>>(),
+            snapshot.folds_in_range(0..text.len()).collect::<Vec<_>>(),
+        );
+        assert_set_eq!(
+            cloned_editor.read(cx).selections.ranges::<Point>(cx),
+            editor.read(cx).selections.ranges(cx)
+        );
+        assert_set_eq!(
+            cloned_editor.update(cx, |e, cx| dbg!(e.selections.display_ranges(cx))),
+            editor.update(cx, |e, cx| dbg!(e.selections.display_ranges(cx)))
+        );
     }
 
     #[gpui::test]

crates/editor/src/element.rs 🔗

@@ -1444,7 +1444,7 @@ impl Element for EditorElement {
                 {
                     return false;
                 }
-                
+
                 let point = if paint.text_bounds.contains_point(*position) {
                     let (point, overshoot) =
                         paint.point_for_position(&self.snapshot(cx), layout, *position);
@@ -1791,7 +1791,7 @@ mod tests {
         cx.set_global(Settings::test(cx));
         let buffer = MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx);
         let (window_id, editor) = cx.add_window(Default::default(), |cx| {
-            Editor::new(EditorMode::Full, buffer, None, None, None, cx)
+            Editor::new(EditorMode::Full, buffer, None, None, cx)
         });
         let element = EditorElement::new(
             editor.downgrade(),
@@ -1813,7 +1813,7 @@ mod tests {
         cx.set_global(Settings::test(cx));
         let buffer = MultiBuffer::build_simple("", cx);
         let (window_id, editor) = cx.add_window(Default::default(), |cx| {
-            Editor::new(EditorMode::Full, buffer, None, None, None, cx)
+            Editor::new(EditorMode::Full, buffer, None, None, cx)
         });
 
         editor.update(cx, |editor, cx| {

crates/editor/src/selections_collection.rs 🔗

@@ -61,6 +61,13 @@ impl SelectionsCollection {
         self.buffer.read(cx).read(cx)
     }
 
+    pub fn set_state(&mut self, other: &SelectionsCollection) {
+        self.next_selection_id = other.next_selection_id;
+        self.line_mode = other.line_mode;
+        self.disjoint = other.disjoint.clone();
+        self.pending = other.pending.clone();
+    }
+
     pub fn count<'a>(&self) -> usize {
         let mut count = self.disjoint.len();
         if self.pending.is_some() {

crates/editor/src/test.rs 🔗

@@ -77,7 +77,7 @@ pub(crate) fn build_editor(
     buffer: ModelHandle<MultiBuffer>,
     cx: &mut ViewContext<Editor>,
 ) -> Editor {
-    Editor::new(EditorMode::Full, buffer, None, None, None, cx)
+    Editor::new(EditorMode::Full, buffer, None, None, cx)
 }
 
 pub struct EditorTestContext<'a> {
@@ -428,7 +428,7 @@ impl<'a> EditorLspTestContext<'a> {
             let (window_id, editor) = cx.add_window(Default::default(), |cx| {
                 let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
 
-                Editor::new(EditorMode::Full, buffer, Some(project), None, None, cx)
+                Editor::new(EditorMode::Full, buffer, Some(project), None, cx)
             });
 
             editor.update(cx, |_, cx| cx.focus_self());