Merge pull request #973 from zed-industries/selections-refactor

Keith Simmons created

Pull selections out of editor into selections collection

Change summary

crates/breadcrumbs/src/breadcrumbs.rs         |   2 
crates/collab/src/rpc.rs                      |  30 
crates/diagnostics/src/diagnostics.rs         |  15 
crates/diagnostics/src/items.rs               |   4 
crates/editor/src/editor.rs                   | 619 ++++++-----------
crates/editor/src/element.rs                  |   9 
crates/editor/src/items.rs                    |  22 
crates/editor/src/selections_collection.rs    | 729 +++++++++++++++++++++
crates/editor/src/test.rs                     |   4 
crates/go_to_line/src/go_to_line.rs           |   6 
crates/journal/src/journal.rs                 |   4 
crates/outline/src/outline.rs                 |   8 
crates/project_symbols/src/project_symbols.rs |   8 
crates/search/src/buffer_search.rs            |  64 +
crates/search/src/project_search.rs           |  28 
crates/vim/src/insert.rs                      |  10 
crates/vim/src/normal.rs                      |  44 
crates/vim/src/normal/change.rs               |  29 
crates/vim/src/normal/delete.rs               |  30 
crates/vim/src/vim_test_context.rs            |   9 
crates/vim/src/visual.rs                      |  78 +-
crates/zed/src/zed.rs                         |  42 
22 files changed, 1,225 insertions(+), 569 deletions(-)

Detailed changes

crates/breadcrumbs/src/breadcrumbs.rs 🔗

@@ -37,7 +37,7 @@ impl Breadcrumbs {
         cx: &AppContext,
     ) -> Option<(ModelHandle<Buffer>, Vec<OutlineItem<Anchor>>)> {
         let editor = self.editor.as_ref()?.read(cx);
-        let cursor = editor.newest_anchor_selection().head();
+        let cursor = editor.selections.newest_anchor().head();
         let multibuffer = &editor.buffer().read(cx);
         let (buffer_id, symbols) = multibuffer
             .read(cx)

crates/collab/src/rpc.rs 🔗

@@ -3003,7 +3003,7 @@ mod tests {
 
         // Type a completion trigger character as the guest.
         editor_b.update(cx_b, |editor, cx| {
-            editor.select_ranges([13..13], None, cx);
+            editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
             editor.handle_input(&Input(".".into()), cx);
             cx.focus(&editor_b);
         });
@@ -4215,7 +4215,9 @@ mod tests {
 
         // Move cursor to a location that contains code actions.
         editor_b.update(cx_b, |editor, cx| {
-            editor.select_ranges([Point::new(1, 31)..Point::new(1, 31)], None, cx);
+            editor.change_selections(None, cx, |s| {
+                s.select_ranges([Point::new(1, 31)..Point::new(1, 31)])
+            });
             cx.focus(&editor_b);
         });
 
@@ -4452,7 +4454,7 @@ mod tests {
 
         // Move cursor to a location that can be renamed.
         let prepare_rename = editor_b.update(cx_b, |editor, cx| {
-            editor.select_ranges([7..7], None, cx);
+            editor.change_selections(None, cx, |s| s.select_ranges([7..7]));
             editor.rename(&Rename, cx).unwrap()
         });
 
@@ -5460,8 +5462,12 @@ mod tests {
         });
 
         // When client B starts following client A, all visible view states are replicated to client B.
-        editor_a1.update(cx_a, |editor, cx| editor.select_ranges([0..1], None, cx));
-        editor_a2.update(cx_a, |editor, cx| editor.select_ranges([2..3], None, cx));
+        editor_a1.update(cx_a, |editor, cx| {
+            editor.change_selections(None, cx, |s| s.select_ranges([0..1]))
+        });
+        editor_a2.update(cx_a, |editor, cx| {
+            editor.change_selections(None, cx, |s| s.select_ranges([2..3]))
+        });
         workspace_b
             .update(cx_b, |workspace, cx| {
                 workspace
@@ -5483,11 +5489,11 @@ mod tests {
             Some((worktree_id, "2.txt").into())
         );
         assert_eq!(
-            editor_b2.read_with(cx_b, |editor, cx| editor.selected_ranges(cx)),
+            editor_b2.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)),
             vec![2..3]
         );
         assert_eq!(
-            editor_b1.read_with(cx_b, |editor, cx| editor.selected_ranges(cx)),
+            editor_b1.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)),
             vec![0..1]
         );
 
@@ -5526,11 +5532,11 @@ mod tests {
 
         // Changes to client A's editor are reflected on client B.
         editor_a1.update(cx_a, |editor, cx| {
-            editor.select_ranges([1..1, 2..2], None, cx);
+            editor.change_selections(None, cx, |s| s.select_ranges([1..1, 2..2]));
         });
         editor_b1
             .condition(cx_b, |editor, cx| {
-                editor.selected_ranges(cx) == vec![1..1, 2..2]
+                editor.selections.ranges(cx) == vec![1..1, 2..2]
             })
             .await;
 
@@ -5540,11 +5546,13 @@ mod tests {
             .await;
 
         editor_a1.update(cx_a, |editor, cx| {
-            editor.select_ranges([3..3], None, 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.selected_ranges(cx) == vec![3..3])
+            .condition(cx_b, |editor, cx| {
+                editor.selections.ranges(cx) == vec![3..3]
+            })
             .await;
 
         // After unfollowing, client B stops receiving updates from client A.

crates/diagnostics/src/diagnostics.rs 🔗

@@ -5,7 +5,7 @@ use collections::{BTreeSet, HashSet};
 use editor::{
     diagnostic_block_renderer,
     display_map::{BlockDisposition, BlockId, BlockProperties, RenderBlock},
-    highlight_diagnostic_message, Editor, ExcerptId, MultiBuffer, ToOffset,
+    highlight_diagnostic_message, Autoscroll, Editor, ExcerptId, MultiBuffer, ToOffset,
 };
 use gpui::{
     actions, elements::*, fonts::TextStyle, serde_json, AnyViewHandle, AppContext, Entity,
@@ -417,8 +417,9 @@ impl ProjectDiagnosticsEditor {
                 }];
             } else {
                 groups = self.path_states.get(path_ix)?.diagnostic_groups.as_slice();
-                new_excerpt_ids_by_selection_id = editor.refresh_selections(cx);
-                selections = editor.local_selections::<usize>(cx);
+                new_excerpt_ids_by_selection_id =
+                    editor.change_selections(Some(Autoscroll::Fit), cx, |s| s.refresh());
+                selections = editor.selections.all::<usize>(cx);
             }
 
             // If any selection has lost its position, move it to start of the next primary diagnostic.
@@ -441,7 +442,9 @@ impl ProjectDiagnosticsEditor {
                     }
                 }
             }
-            editor.update_selections(selections, None, cx);
+            editor.change_selections(None, cx, |s| {
+                s.select(selections);
+            });
             Some(())
         });
 
@@ -894,7 +897,7 @@ mod tests {
             // Cursor is at the first diagnostic
             view.editor.update(cx, |editor, cx| {
                 assert_eq!(
-                    editor.selected_display_ranges(cx),
+                    editor.selections.display_ranges(cx),
                     [DisplayPoint::new(12, 6)..DisplayPoint::new(12, 6)]
                 );
             });
@@ -995,7 +998,7 @@ mod tests {
             // Cursor keeps its position.
             view.editor.update(cx, |editor, cx| {
                 assert_eq!(
-                    editor.selected_display_ranges(cx),
+                    editor.selections.display_ranges(cx),
                     [DisplayPoint::new(19, 6)..DisplayPoint::new(19, 6)]
                 );
             });

crates/diagnostics/src/items.rs 🔗

@@ -58,9 +58,7 @@ impl DiagnosticIndicator {
     fn update(&mut self, editor: ViewHandle<Editor>, cx: &mut ViewContext<Self>) {
         let editor = editor.read(cx);
         let buffer = editor.buffer().read(cx);
-        let cursor_position = editor
-            .newest_selection_with_snapshot::<usize>(&buffer.read(cx))
-            .head();
+        let cursor_position = editor.selections.newest::<usize>(cx).head();
         let new_diagnostic = buffer
             .read(cx)
             .diagnostics_in_range::<_, usize>(cursor_position..cursor_position, false)

crates/editor/src/editor.rs 🔗

@@ -3,6 +3,7 @@ mod element;
 pub mod items;
 pub mod movement;
 mod multi_buffer;
+mod selections_collection;
 
 #[cfg(test)]
 mod test;
@@ -28,7 +29,6 @@ use gpui::{
     ModelHandle, MutableAppContext, RenderContext, Task, View, ViewContext, ViewHandle,
     WeakViewHandle,
 };
-use itertools::Itertools as _;
 pub use language::{char_kind, CharKind};
 use language::{
     BracketPair, Buffer, CodeAction, CodeLabel, Completion, Diagnostic, DiagnosticSeverity,
@@ -40,6 +40,7 @@ pub use multi_buffer::{
 };
 use ordered_float::OrderedFloat;
 use project::{Project, ProjectTransaction};
+use selections_collection::{resolve_multiple, MutableSelectionsCollection, SelectionsCollection};
 use serde::{Deserialize, Serialize};
 use settings::Settings;
 use smallvec::SmallVec;
@@ -49,14 +50,12 @@ use std::{
     any::TypeId,
     borrow::Cow,
     cmp::{self, Ordering, Reverse},
-    iter::{self, FromIterator},
-    mem,
-    ops::{Deref, DerefMut, Range, RangeInclusive, Sub},
+    iter, mem,
+    ops::{Deref, DerefMut, Range, RangeInclusive},
     sync::Arc,
     time::{Duration, Instant},
 };
 pub use sum_tree::Bias;
-use text::rope::TextDimension;
 use theme::{DiagnosticStyle, Theme};
 use util::{post_inc, ResultExt, TryFutureExt};
 use workspace::{ItemNavHistory, Workspace};
@@ -377,9 +376,7 @@ pub struct Editor {
     handle: WeakViewHandle<Self>,
     buffer: ModelHandle<MultiBuffer>,
     display_map: ModelHandle<DisplayMap>,
-    next_selection_id: usize,
-    selections: Arc<[Selection<Anchor>]>,
-    pending_selection: Option<PendingSelection>,
+    pub selections: SelectionsCollection,
     columnar_selection_tail: Option<Anchor>,
     add_selections_state: Option<AddSelectionsState>,
     select_next_state: Option<SelectNextState>,
@@ -429,13 +426,7 @@ pub struct EditorSnapshot {
     scroll_top_anchor: Anchor,
 }
 
-#[derive(Clone)]
-pub struct PendingSelection {
-    selection: Selection<Anchor>,
-    mode: SelectMode,
-}
-
-#[derive(Clone)]
+#[derive(Clone, Debug)]
 struct SelectionHistoryEntry {
     selections: Arc<[Selection<Anchor>]>,
     select_next_state: Option<SelectNextState>,
@@ -527,13 +518,13 @@ impl SelectionHistory {
     }
 }
 
-#[derive(Clone)]
+#[derive(Clone, Debug)]
 struct AddSelectionsState {
     above: bool,
     stack: Vec<usize>,
 }
 
-#[derive(Clone)]
+#[derive(Clone, Debug)]
 struct SelectNextState {
     query: AhoCorasick,
     wordwise: bool,
@@ -545,6 +536,7 @@ struct BracketPairState {
     pair: BracketPair,
 }
 
+#[derive(Debug)]
 struct SnippetState {
     ranges: Vec<Vec<Range<Anchor>>>,
     active_index: usize,
@@ -945,23 +937,14 @@ impl Editor {
         cx.observe(&display_map, Self::on_display_map_changed)
             .detach();
 
+        let selections = SelectionsCollection::new(display_map.clone(), buffer.clone());
+
         let mut this = Self {
             handle: cx.weak_handle(),
             buffer,
             display_map,
-            selections: Arc::from([]),
-            pending_selection: Some(PendingSelection {
-                selection: Selection {
-                    id: 0,
-                    start: Anchor::min(),
-                    end: Anchor::min(),
-                    reversed: false,
-                    goal: SelectionGoal::None,
-                },
-                mode: SelectMode::Character,
-            }),
+            selections,
             columnar_selection_tail: None,
-            next_selection_id: 1,
             add_selections_state: None,
             select_next_state: None,
             selection_history: Default::default(),
@@ -1204,12 +1187,11 @@ impl Editor {
             first_cursor_top = highlighted_rows.start as f32;
             last_cursor_bottom = first_cursor_top + 1.;
         } else if autoscroll == Autoscroll::Newest {
-            let newest_selection =
-                self.newest_selection_with_snapshot::<Point>(&display_map.buffer_snapshot);
+            let newest_selection = self.selections.newest::<Point>(cx);
             first_cursor_top = newest_selection.head().to_display_point(&display_map).row() as f32;
             last_cursor_bottom = first_cursor_top + 1.;
         } else {
-            let selections = self.local_selections::<Point>(cx);
+            let selections = self.selections.all::<Point>(cx);
             first_cursor_top = selections
                 .first()
                 .unwrap()
@@ -1269,7 +1251,7 @@ impl Editor {
         cx: &mut ViewContext<Self>,
     ) -> bool {
         let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
-        let selections = self.local_selections::<Point>(cx);
+        let selections = self.selections.all::<Point>(cx);
 
         let mut target_left;
         let mut target_right;
@@ -1318,83 +1300,96 @@ impl Editor {
         }
     }
 
-    pub fn replace_selections_with(
+    fn selections_did_change(
         &mut self,
+        local: bool,
+        old_cursor_position: &Anchor,
         cx: &mut ViewContext<Self>,
-        mut find_replacement: impl FnMut(&DisplaySnapshot) -> DisplayPoint,
     ) {
-        let display_map = self.snapshot(cx);
-        let cursor = find_replacement(&display_map);
-        let selection = Selection {
-            id: post_inc(&mut self.next_selection_id),
-            start: cursor,
-            end: cursor,
-            reversed: false,
-            goal: SelectionGoal::None,
+        if self.focused && self.leader_replica_id.is_none() {
+            self.buffer.update(cx, |buffer, cx| {
+                buffer.set_active_selections(&self.selections.disjoint_anchors(), cx)
+            });
         }
-        .map(|display_point| display_point.to_point(&display_map));
-        self.update_selections(vec![selection], None, cx);
-    }
 
-    pub fn display_selections(
-        &mut self,
-        cx: &mut ViewContext<Self>,
-    ) -> (DisplaySnapshot, Vec<Selection<DisplayPoint>>) {
-        let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
-        let selections = self
-            .local_selections::<Point>(cx)
-            .into_iter()
-            .map(|selection| selection.map(|point| point.to_display_point(&display_map)))
-            .collect();
-        (display_map, selections)
-    }
+        let display_map = self
+            .display_map
+            .update(cx, |display_map, cx| display_map.snapshot(cx));
+        let buffer = &display_map.buffer_snapshot;
+        self.add_selections_state = None;
+        self.select_next_state = None;
+        self.select_larger_syntax_node_stack.clear();
+        self.autoclose_stack
+            .invalidate(&self.selections.disjoint_anchors(), buffer);
+        self.snippet_stack
+            .invalidate(&self.selections.disjoint_anchors(), buffer);
+        self.take_rename(false, cx);
 
-    pub fn move_selections(
-        &mut self,
-        cx: &mut ViewContext<Self>,
-        mut move_selection: impl FnMut(&DisplaySnapshot, &mut Selection<DisplayPoint>),
-    ) {
-        let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
-        let selections = self
-            .local_selections::<Point>(cx)
-            .into_iter()
-            .map(|selection| {
-                let mut selection = selection.map(|point| point.to_display_point(&display_map));
-                move_selection(&display_map, &mut selection);
-                selection.map(|display_point| display_point.to_point(&display_map))
-            })
-            .collect();
-        self.update_selections(selections, Some(Autoscroll::Fit), cx);
-    }
+        let new_cursor_position = self.selections.newest_anchor().head();
 
-    pub fn move_selection_heads(
-        &mut self,
-        cx: &mut ViewContext<Self>,
-        mut update_head: impl FnMut(
-            &DisplaySnapshot,
-            DisplayPoint,
-            SelectionGoal,
-        ) -> (DisplayPoint, SelectionGoal),
-    ) {
-        self.move_selections(cx, |map, selection| {
-            let (new_head, new_goal) = update_head(map, selection.head(), selection.goal);
-            selection.set_head(new_head, new_goal);
-        });
+        self.push_to_nav_history(
+            old_cursor_position.clone(),
+            Some(new_cursor_position.to_point(buffer)),
+            cx,
+        );
+
+        if local {
+            let new_cursor_position = self.selections.newest_anchor().head();
+            let completion_menu = match self.context_menu.as_mut() {
+                Some(ContextMenu::Completions(menu)) => Some(menu),
+                _ => {
+                    self.context_menu.take();
+                    None
+                }
+            };
+
+            if let Some(completion_menu) = completion_menu {
+                let cursor_position = new_cursor_position.to_offset(buffer);
+                let (word_range, kind) =
+                    buffer.surrounding_word(completion_menu.initial_position.clone());
+                if kind == Some(CharKind::Word)
+                    && word_range.to_inclusive().contains(&cursor_position)
+                {
+                    let query = Self::completion_query(buffer, cursor_position);
+                    cx.background()
+                        .block(completion_menu.filter(query.as_deref(), cx.background().clone()));
+                    self.show_completions(&ShowCompletions, cx);
+                } else {
+                    self.hide_context_menu(cx);
+                }
+            }
+
+            if old_cursor_position.to_display_point(&display_map).row()
+                != new_cursor_position.to_display_point(&display_map).row()
+            {
+                self.available_code_actions.take();
+            }
+            self.refresh_code_actions(cx);
+            self.refresh_document_highlights(cx);
+        }
+
+        self.pause_cursor_blinking(cx);
+        cx.emit(Event::SelectionsChanged { local });
+        cx.notify();
     }
 
-    pub fn move_cursors(
+    pub fn change_selections<R>(
         &mut self,
+        autoscroll: Option<Autoscroll>,
         cx: &mut ViewContext<Self>,
-        mut update_cursor_position: impl FnMut(
-            &DisplaySnapshot,
-            DisplayPoint,
-            SelectionGoal,
-        ) -> (DisplayPoint, SelectionGoal),
-    ) {
-        self.move_selections(cx, |map, selection| {
-            let (cursor, new_goal) = update_cursor_position(map, selection.head(), selection.goal);
-            selection.collapse_to(cursor, new_goal)
-        });
+        change: impl FnOnce(&mut MutableSelectionsCollection<'_>) -> R,
+    ) -> R {
+        let old_cursor_position = self.selections.newest_anchor().head();
+        self.push_to_selection_history();
+
+        let result = self.selections.change_with(cx, change);
+
+        if let Some(autoscroll) = autoscroll {
+            self.request_autoscroll(autoscroll, cx);
+        }
+        self.selections_did_change(true, &old_cursor_position, cx);
+
+        result
     }
 
     pub fn edit<I, S, T>(&mut self, edits: I, cx: &mut ViewContext<Self>)
@@ -1449,30 +1444,34 @@ impl Editor {
         cx: &mut ViewContext<Self>,
     ) {
         let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
-        let tail = self
-            .newest_selection_with_snapshot::<usize>(&display_map.buffer_snapshot)
-            .tail();
+        let tail = self.selections.newest::<usize>(cx).tail();
         self.begin_selection(position, false, click_count, cx);
 
         let position = position.to_offset(&display_map, Bias::Left);
         let tail_anchor = display_map.buffer_snapshot.anchor_before(tail);
-        let mut pending = self.pending_selection.clone().unwrap();
 
+        let mut pending_selection = self
+            .selections
+            .pending_anchor()
+            .expect("extend_selection not called with pending selection");
         if position >= tail {
-            pending.selection.start = tail_anchor.clone();
+            pending_selection.start = tail_anchor.clone();
         } else {
-            pending.selection.end = tail_anchor.clone();
-            pending.selection.reversed = true;
+            pending_selection.end = tail_anchor.clone();
+            pending_selection.reversed = true;
         }
 
-        match &mut pending.mode {
+        let mut pending_mode = self.selections.pending_mode().unwrap();
+        match &mut pending_mode {
             SelectMode::Word(range) | SelectMode::Line(range) => {
                 *range = tail_anchor.clone()..tail_anchor
             }
             _ => {}
         }
 
-        self.set_selections(self.selections.clone(), Some(pending), true, cx);
+        self.change_selections(Some(Autoscroll::Fit), cx, |s| {
+            s.set_pending(pending_selection, pending_mode)
+        });
     }
 
     fn begin_selection(
@@ -1489,7 +1488,7 @@ impl Editor {
 
         let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
         let buffer = &display_map.buffer_snapshot;
-        let newest_selection = self.newest_anchor_selection().clone();
+        let newest_selection = self.selections.newest_anchor().clone();
         let position = display_map.clip_point(position, Bias::Left);
 
         let start;
@@ -1527,38 +1526,17 @@ impl Editor {
             }
         }
 
-        let selection = Selection {
-            id: post_inc(&mut self.next_selection_id),
-            start,
-            end,
-            reversed: false,
-            goal: SelectionGoal::None,
-        };
-
-        let mut selections;
-        if add {
-            selections = self.selections.clone();
-            // Remove the newest selection if it was added due to a previous mouse up
-            // within this multi-click.
-            if click_count > 1 {
-                selections = self
-                    .selections
-                    .iter()
-                    .filter(|selection| selection.id != newest_selection.id)
-                    .cloned()
-                    .collect();
+        self.change_selections(Some(Autoscroll::Fit), cx, |s| {
+            if add {
+                if click_count > 1 {
+                    s.delete(newest_selection.id);
+                }
+            } else {
+                s.clear_disjoint();
             }
-        } else {
-            selections = Arc::from([]);
-        }
-        self.set_selections(
-            selections,
-            Some(PendingSelection { selection, mode }),
-            true,
-            cx,
-        );
 
-        cx.notify();
+            s.set_pending_range(start..end, mode);
+        });
     }
 
     fn begin_columnar_selection(
@@ -1573,9 +1551,7 @@ impl Editor {
         }
 
         let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
-        let tail = self
-            .newest_selection_with_snapshot::<Point>(&display_map.buffer_snapshot)
-            .tail();
+        let tail = self.selections.newest::<Point>(cx).tail();
         self.columnar_selection_tail = Some(display_map.buffer_snapshot.anchor_before(tail));
 
         self.select_columns(
@@ -1599,14 +1575,15 @@ impl Editor {
         if let Some(tail) = self.columnar_selection_tail.as_ref() {
             let tail = tail.to_display_point(&display_map);
             self.select_columns(tail, position, overshoot, &display_map, cx);
-        } else if let Some(mut pending) = self.pending_selection.clone() {
+        } else if let Some(mut pending) = self.selections.pending_anchor().clone() {
             let buffer = self.buffer.read(cx).snapshot(cx);
             let head;
             let tail;
-            match &pending.mode {
+            let mode = self.selections.pending_mode().unwrap();
+            match &mode {
                 SelectMode::Character => {
                     head = position.to_point(&display_map);
-                    tail = pending.selection.tail().to_point(&buffer);
+                    tail = pending.tail().to_point(&buffer);
                 }
                 SelectMode::Word(original_range) => {
                     let original_display_range = original_range.start.to_display_point(&display_map)
@@ -1662,15 +1639,18 @@ impl Editor {
             };
 
             if head < tail {
-                pending.selection.start = buffer.anchor_before(head);
-                pending.selection.end = buffer.anchor_before(tail);
-                pending.selection.reversed = true;
+                pending.start = buffer.anchor_before(head);
+                pending.end = buffer.anchor_before(tail);
+                pending.reversed = true;
             } else {
-                pending.selection.start = buffer.anchor_before(tail);
-                pending.selection.end = buffer.anchor_before(head);
-                pending.selection.reversed = false;
+                pending.start = buffer.anchor_before(tail);
+                pending.end = buffer.anchor_before(head);
+                pending.reversed = false;
             }
-            self.set_selections(self.selections.clone(), Some(pending), true, cx);
+
+            self.change_selections(None, cx, |s| {
+                s.set_pending(pending, mode);
+            });
         } else {
             log::error!("update_selection dispatched with no pending selection");
             return;
@@ -1682,9 +1662,12 @@ impl Editor {
 
     fn end_selection(&mut self, cx: &mut ViewContext<Self>) {
         self.columnar_selection_tail.take();
-        if self.pending_selection.is_some() {
-            let selections = self.local_selections::<usize>(cx);
-            self.update_selections(selections, None, cx);
+        if self.selections.pending_anchor().is_some() {
+            let selections = self.selections.all::<usize>(cx);
+            self.change_selections(None, cx, |s| {
+                s.select(selections);
+                s.clear_pending();
+            });
         }
     }
 
@@ -1702,7 +1685,7 @@ impl Editor {
         let end_column = cmp::max(tail.column(), head.column() + overshoot);
         let reversed = start_column < tail.column();
 
-        let selections = (start_row..=end_row)
+        let selection_ranges = (start_row..=end_row)
             .filter_map(|row| {
                 if start_column <= display_map.line_len(row) && !display_map.is_block_line(row) {
                     let start = display_map
@@ -1711,25 +1694,25 @@ impl Editor {
                     let end = display_map
                         .clip_point(DisplayPoint::new(row, end_column), Bias::Right)
                         .to_point(&display_map);
-                    Some(Selection {
-                        id: post_inc(&mut self.next_selection_id),
-                        start,
-                        end,
-                        reversed,
-                        goal: SelectionGoal::None,
-                    })
+                    if reversed {
+                        Some(end..start)
+                    } else {
+                        Some(start..end)
+                    }
                 } else {
                     None
                 }
             })
             .collect::<Vec<_>>();
 
-        self.update_selections(selections, None, cx);
+        self.change_selections(None, cx, |s| {
+            s.select_ranges(selection_ranges);
+        });
         cx.notify();
     }
 
     pub fn is_selecting(&self) -> bool {
-        self.pending_selection.is_some() || self.columnar_selection_tail.is_some()
+        self.selections.pending_anchor().is_some() || self.columnar_selection_tail.is_some()
     }
 
     pub fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
@@ -1751,26 +1734,7 @@ impl Editor {
                 return;
             }
 
-            if let Some(pending) = self.pending_selection.clone() {
-                let mut selections = self.selections.clone();
-                if selections.is_empty() {
-                    selections = Arc::from([pending.selection]);
-                }
-                self.set_selections(selections, None, true, cx);
-                self.request_autoscroll(Autoscroll::Fit, cx);
-                return;
-            }
-
-            let mut oldest_selection = self.oldest_selection::<usize>(&cx);
-            if self.selection_count() > 1 {
-                self.update_selections(vec![oldest_selection], Some(Autoscroll::Fit), cx);
-                return;
-            }
-
-            if !oldest_selection.is_empty() {
-                oldest_selection.start = oldest_selection.head().clone();
-                oldest_selection.end = oldest_selection.head().clone();
-                self.update_selections(vec![oldest_selection], Some(Autoscroll::Fit), cx);
+            if self.change_selections(Some(Autoscroll::Fit), cx, |s| s.try_cancel()) {
                 return;
             }
         }
@@ -1778,107 +1742,6 @@ impl Editor {
         cx.propagate_action();
     }
 
-    #[cfg(any(test, feature = "test-support"))]
-    pub fn selected_ranges<D: TextDimension + Ord + Sub<D, Output = D>>(
-        &self,
-        cx: &AppContext,
-    ) -> Vec<Range<D>> {
-        self.local_selections::<D>(cx)
-            .iter()
-            .map(|s| {
-                if s.reversed {
-                    s.end.clone()..s.start.clone()
-                } else {
-                    s.start.clone()..s.end.clone()
-                }
-            })
-            .collect()
-    }
-
-    #[cfg(any(test, feature = "test-support"))]
-    pub fn selected_display_ranges(&self, cx: &mut MutableAppContext) -> Vec<Range<DisplayPoint>> {
-        let display_map = self
-            .display_map
-            .update(cx, |display_map, cx| display_map.snapshot(cx));
-        self.selections
-            .iter()
-            .chain(
-                self.pending_selection
-                    .as_ref()
-                    .map(|pending| &pending.selection),
-            )
-            .map(|s| {
-                if s.reversed {
-                    s.end.to_display_point(&display_map)..s.start.to_display_point(&display_map)
-                } else {
-                    s.start.to_display_point(&display_map)..s.end.to_display_point(&display_map)
-                }
-            })
-            .collect()
-    }
-
-    pub fn select_ranges<I, T>(
-        &mut self,
-        ranges: I,
-        autoscroll: Option<Autoscroll>,
-        cx: &mut ViewContext<Self>,
-    ) where
-        I: IntoIterator<Item = Range<T>>,
-        T: ToOffset,
-    {
-        let buffer = self.buffer.read(cx).snapshot(cx);
-        let selections = ranges
-            .into_iter()
-            .map(|range| {
-                let mut start = range.start.to_offset(&buffer);
-                let mut end = range.end.to_offset(&buffer);
-                let reversed = if start > end {
-                    mem::swap(&mut start, &mut end);
-                    true
-                } else {
-                    false
-                };
-                Selection {
-                    id: post_inc(&mut self.next_selection_id),
-                    start,
-                    end,
-                    reversed,
-                    goal: SelectionGoal::None,
-                }
-            })
-            .collect::<Vec<_>>();
-        self.update_selections(selections, autoscroll, cx);
-    }
-
-    #[cfg(any(test, feature = "test-support"))]
-    pub fn select_display_ranges<'a, T>(&mut self, ranges: T, cx: &mut ViewContext<Self>)
-    where
-        T: IntoIterator<Item = &'a Range<DisplayPoint>>,
-    {
-        let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
-        let selections = ranges
-            .into_iter()
-            .map(|range| {
-                let mut start = range.start;
-                let mut end = range.end;
-                let reversed = if start > end {
-                    mem::swap(&mut start, &mut end);
-                    true
-                } else {
-                    false
-                };
-                Selection {
-                    id: post_inc(&mut self.next_selection_id),
-                    start: start.to_point(&display_map),
-                    end: end.to_point(&display_map),
-                    reversed,
-                    goal: SelectionGoal::None,
-                }
-            })
-            .collect();
-        self.update_selections(selections, None, cx);
-    }
-
     pub fn handle_input(&mut self, action: &Input, cx: &mut ViewContext<Self>) {
         if !self.input_enabled {
             cx.propagate_action();
@@ -1900,7 +1763,8 @@ impl Editor {
     pub fn newline(&mut self, _: &Newline, cx: &mut ViewContext<Self>) {
         self.transact(cx, |this, cx| {
             let (edits, selection_fixup_info): (Vec<_>, Vec<_>) = {
-                let selections = this.local_selections::<usize>(cx);
+                let selections = this.selections.all::<usize>(cx);
+
                 let buffer = this.buffer.read(cx).snapshot(cx);
                 selections
                     .iter()
@@ -1947,9 +1811,12 @@ impl Editor {
                         if insert_extra_newline {
                             new_text = new_text.repeat(2);
                         }
+
+                        let anchor = buffer.anchor_after(end);
+                        let new_selection = selection.map(|_| anchor.clone());
                         (
                             (start..end, new_text),
-                            (insert_extra_newline, buffer.anchor_after(end)),
+                            (insert_extra_newline, new_selection),
                         )
                     })
                     .unzip()
@@ -1957,35 +1824,28 @@ impl Editor {
 
             this.buffer.update(cx, |buffer, cx| {
                 buffer.edit_with_autoindent(edits, cx);
-
-                let buffer = buffer.read(cx);
-                this.selections = this
-                    .selections
-                    .iter()
-                    .cloned()
-                    .zip(selection_fixup_info)
-                    .map(|(mut new_selection, (extra_newline_inserted, end))| {
-                        let mut cursor = end.to_point(&buffer);
-                        if extra_newline_inserted {
-                            cursor.row -= 1;
-                            cursor.column = buffer.line_len(cursor.row);
-                        }
-                        let anchor = buffer.anchor_after(cursor);
-                        new_selection.start = anchor.clone();
-                        new_selection.end = anchor;
-                        new_selection
-                    })
-                    .collect();
             });
+            let buffer = this.buffer.read(cx).snapshot(cx);
+            let new_selections = selection_fixup_info
+                .into_iter()
+                .map(|(extra_newline_inserted, new_selection)| {
+                    let mut cursor = new_selection.end.to_point(&buffer);
+                    if extra_newline_inserted {
+                        cursor.row -= 1;
+                        cursor.column = buffer.line_len(cursor.row);
+                    }
+                    new_selection.map(|_| cursor.clone())
+                })
+                .collect();
 
-            this.request_autoscroll(Autoscroll::Fit, cx);
+            this.change_selections(Some(Autoscroll::Fit), cx, |s| s.select(new_selections));
         });
     }
 
     pub fn insert(&mut self, text: &str, cx: &mut ViewContext<Self>) {
         let text: Arc<str> = text.into();
         self.transact(cx, |this, cx| {
-            let old_selections = this.local_selections::<usize>(cx);
+            let old_selections = this.selections.all::<usize>(cx);
             let selection_anchors = this.buffer.update(cx, |buffer, cx| {
                 let anchors = {
                     let snapshot = buffer.read(cx);
@@ -2019,12 +1879,15 @@ impl Editor {
                     })
                     .collect()
             };
-            this.update_selections(selections, Some(Autoscroll::Fit), cx);
+
+            this.change_selections(Some(Autoscroll::Fit), cx, |s| {
+                s.select(selections);
+            })
         });
     }
 
     fn trigger_completion_on_input(&mut self, text: &str, cx: &mut ViewContext<Self>) {
-        let selection = self.newest_anchor_selection();
+        let selection = self.selections.newest_anchor();
         if self
             .buffer
             .read(cx)
@@ -2044,64 +1907,58 @@ impl Editor {
             .cloned()
         {
             if self
-                .local_selections::<usize>(cx)
+                .selections
+                .all::<usize>(cx)
                 .iter()
                 .any(|selection| selection.is_empty())
             {
-                false
-            } else {
-                let mut selections = self.selections.to_vec();
-                for selection in &mut selections {
-                    selection.end = selection.end.bias_left(&snapshot);
-                }
-                drop(snapshot);
+                return false;
+            }
 
-                self.buffer.update(cx, |buffer, cx| {
-                    let pair_start: Arc<str> = pair.start.clone().into();
-                    buffer.edit(
-                        selections
-                            .iter()
-                            .map(|s| (s.start.clone()..s.start.clone(), pair_start.clone())),
-                        cx,
-                    );
-                    let pair_end: Arc<str> = pair.end.clone().into();
-                    buffer.edit(
-                        selections
-                            .iter()
-                            .map(|s| (s.end.clone()..s.end.clone(), pair_end.clone())),
-                        cx,
-                    );
-                });
+            let mut selections = self.selections.disjoint_anchors().to_vec();
+            for selection in &mut selections {
+                selection.end = selection.end.bias_left(&snapshot);
+            }
+            drop(snapshot);
 
-                let snapshot = self.buffer.read(cx).read(cx);
-                for selection in &mut selections {
-                    selection.end = selection.end.bias_right(&snapshot);
-                }
-                drop(snapshot);
+            self.buffer.update(cx, |buffer, cx| {
+                let pair_start: Arc<str> = pair.start.clone().into();
+                let pair_end: Arc<str> = pair.end.clone().into();
+                buffer.edit(
+                    selections
+                        .iter()
+                        .map(|s| (s.start.clone()..s.start.clone(), pair_start.clone()))
+                        .chain(
+                            selections
+                                .iter()
+                                .map(|s| (s.end.clone()..s.end.clone(), pair_end.clone())),
+                        ),
+                    cx,
+                );
+            });
 
-                self.set_selections(selections.into(), None, true, cx);
-                true
+            let snapshot = self.buffer.read(cx).read(cx);
+            for selection in &mut selections {
+                selection.end = selection.end.bias_right(&snapshot);
             }
+            drop(snapshot);
+
+            self.change_selections(None, cx, |s| s.select_anchors(selections));
+            true
         } else {
             false
         }
     }
 
     fn autoclose_bracket_pairs(&mut self, cx: &mut ViewContext<Self>) {
-        let selections = self.local_selections::<usize>(cx);
+        let selections = self.selections.all::<usize>(cx);
         let mut bracket_pair_state = None;
         let mut new_selections = None;
         self.buffer.update(cx, |buffer, cx| {
             let mut snapshot = buffer.snapshot(cx);
             let left_biased_selections = selections
                 .iter()
-                .map(|selection| Selection {
-                    id: selection.id,
-                    start: snapshot.anchor_before(selection.start),
-                    end: snapshot.anchor_before(selection.end),
-                    reversed: selection.reversed,
-                    goal: selection.goal,
-                })
+                .map(|selection| selection.map(|p| snapshot.anchor_before(p)))
                 .collect::<Vec<_>>();
 
             let autoclose_pair = snapshot.language().and_then(|language| {
@@ -2155,7 +2012,7 @@ impl Editor {
                 snapshot = buffer.snapshot(cx);
 
                 new_selections = Some(
-                    self.resolve_selections::<usize, _>(left_biased_selections.iter(), &snapshot)
+                    resolve_multiple::<usize, _>(left_biased_selections.iter(), &snapshot)
                         .collect::<Vec<_>>(),
                 );
 
@@ -2177,7 +2034,9 @@ impl Editor {
         });
 
         if let Some(new_selections) = new_selections {
-            self.update_selections(new_selections, None, cx);
+            self.change_selections(None, cx, |s| {
+                s.select(new_selections);
+            });
         }
         if let Some(bracket_pair_state) = bracket_pair_state {
             self.autoclose_stack.push(bracket_pair_state);
@@ -2185,7 +2044,8 @@ impl Editor {
     }
 
     fn skip_autoclose_end(&mut self, text: &str, cx: &mut ViewContext<Self>) -> bool {
-        let old_selections = self.local_selections::<usize>(cx);
+        let buffer = self.buffer.read(cx).snapshot(cx);
+        let old_selections = self.selections.all::<usize>(cx);
         let autoclose_pair = if let Some(autoclose_pair) = self.autoclose_stack.last() {
             autoclose_pair
         } else {
@@ -2197,7 +2057,6 @@ impl Editor {
 
         debug_assert_eq!(old_selections.len(), autoclose_pair.ranges.len());
 
-        let buffer = self.buffer.read(cx).snapshot(cx);
         if old_selections
             .iter()
             .zip(autoclose_pair.ranges.iter().map(|r| r.to_offset(&buffer)))
@@ -2220,7 +2079,9 @@ impl Editor {
                 })
                 .collect();
             self.autoclose_stack.pop();
-            self.update_selections(new_selections, Some(Autoscroll::Fit), cx);
+            self.change_selections(Some(Autoscroll::Fit), cx, |s| {
+                s.select(new_selections);
+            });
             true
         } else {
             false
@@ -2252,7 +2113,7 @@ impl Editor {
             return;
         };
 
-        let position = self.newest_anchor_selection().head();
+        let position = self.selections.newest_anchor().head();
         let (buffer, buffer_position) = if let Some(output) = self
             .buffer
             .read(cx)
@@ -2353,12 +2214,12 @@ impl Editor {
             snippet = None;
             text = completion.new_text.clone();
         };
+        let selections = self.selections.all::<usize>(cx);
         let buffer = buffer_handle.read(cx);
         let old_range = completion.old_range.to_offset(&buffer);
         let old_text = buffer.text_for_range(old_range.clone()).collect::<String>();
 
-        let selections = self.local_selections::<usize>(cx);
-        let newest_selection = self.newest_anchor_selection();
+        let newest_selection = self.selections.newest_anchor();
         if newest_selection.start.buffer_id != Some(buffer_handle.id()) {
             return None;
         }
@@ -2524,7 +2385,7 @@ impl Editor {
                     editor
                         .buffer()
                         .read(cx)
-                        .excerpt_containing(editor.newest_anchor_selection().head(), cx)
+                        .excerpt_containing(editor.selections.newest_anchor().head(), cx)
                 });
                 if let Some((excerpted_buffer, excerpt_range)) = excerpt {
                     if excerpted_buffer == *buffer {
@@ -2585,7 +2446,7 @@ impl Editor {
     fn refresh_code_actions(&mut self, cx: &mut ViewContext<Self>) -> Option<()> {
         let project = self.project.as_ref()?;
         let buffer = self.buffer.read(cx);
-        let newest_selection = self.newest_anchor_selection().clone();
+        let newest_selection = self.selections.newest_anchor().clone();
         let (start_buffer, start) = buffer.text_anchor_for_position(newest_selection.start, cx)?;
         let (end_buffer, end) = buffer.text_anchor_for_position(newest_selection.end, cx)?;
         if start_buffer != end_buffer {
@@ -2620,7 +2481,7 @@ impl Editor {
 
         let project = self.project.as_ref()?;
         let buffer = self.buffer.read(cx);
-        let newest_selection = self.newest_anchor_selection().clone();
+        let newest_selection = self.selections.newest_anchor().clone();
         let cursor_position = newest_selection.head();
         let (cursor_buffer, cursor_buffer_position) =
             buffer.text_anchor_for_position(cursor_position.clone(), cx)?;
@@ -2808,7 +2669,9 @@ impl Editor {
         });
 
         if let Some(tabstop) = tabstops.first() {
-            self.select_ranges(tabstop.iter().cloned(), Some(Autoscroll::Fit), cx);
+            self.change_selections(Some(Autoscroll::Fit), cx, |s| {
+                s.select_ranges(tabstop.iter().cloned());
+            });
             self.snippet_stack.push(SnippetState {
                 active_index: 0,
                 ranges: tabstops,
@@ -2827,14 +2690,13 @@ impl Editor {
     }
 
     pub fn move_to_snippet_tabstop(&mut self, bias: Bias, cx: &mut ViewContext<Self>) -> bool {
-        let buffer = self.buffer.read(cx).snapshot(cx);
-
-        if let Some(snippet) = self.snippet_stack.last_mut() {
+        if let Some(mut snippet) = self.snippet_stack.pop() {
             match bias {
                 Bias::Left => {
                     if snippet.active_index > 0 {
                         snippet.active_index -= 1;
                     } else {
+                        self.snippet_stack.push(snippet);
                         return false;
                     }
                 }
@@ -2842,34 +2704,21 @@ impl Editor {
                     if snippet.active_index + 1 < snippet.ranges.len() {
                         snippet.active_index += 1;
                     } else {
+                        self.snippet_stack.push(snippet);
                         return false;
                     }
                 }
             }
             if let Some(current_ranges) = snippet.ranges.get(snippet.active_index) {
-                let new_selections = current_ranges
-                    .iter()
-                    .map(|new_range| {
-                        let new_range = new_range.to_offset(&buffer);
-                        Selection {
-                            id: post_inc(&mut self.next_selection_id),
-                            start: new_range.start,
-                            end: new_range.end,
-                            reversed: false,
-                            goal: SelectionGoal::None,
-                        }
-                    })
-                    .collect();
-
-                // Remove the snippet state when moving to the last tabstop.
-                if snippet.active_index + 1 == snippet.ranges.len() {
-                    self.snippet_stack.pop();
+                self.change_selections(Some(Autoscroll::Fit), cx, |s| {
+                    s.select_anchor_ranges(current_ranges.into_iter().cloned())
+                });
+                // If snippet state is not at the last tabstop, push it back on the stack
+                if snippet.active_index + 1 < snippet.ranges.len() {
+                    self.snippet_stack.push(snippet);
                 }
-
-                self.update_selections(new_selections, Some(Autoscroll::Fit), cx);
                 return true;
             }
-            self.snippet_stack.pop();
         }
 
         false

crates/editor/src/element.rs 🔗

@@ -957,8 +957,10 @@ impl Element for EditorElement {
             selections.extend(remote_selections);
 
             if view.show_local_selections {
-                let local_selections =
-                    view.local_selections_in_range(start_anchor..end_anchor, &display_map);
+                let mut local_selections = view
+                    .selections
+                    .disjoint_in_range(start_anchor..end_anchor, cx);
+                local_selections.extend(view.selections.pending(cx));
                 for selection in &local_selections {
                     let is_empty = selection.start == selection.end;
                     let selection_start = snapshot.prev_line_boundary(selection.start).1;
@@ -1041,7 +1043,8 @@ impl Element for EditorElement {
             }
 
             let newest_selection_head = view
-                .newest_selection_with_snapshot::<usize>(&snapshot.buffer_snapshot)
+                .selections
+                .newest::<usize>(cx)
                 .head()
                 .to_display_point(&snapshot);
 

crates/editor/src/items.rs 🔗

@@ -102,7 +102,7 @@ impl FollowableItem for Editor {
         } else {
             self.buffer.update(cx, |buffer, cx| {
                 if self.focused {
-                    buffer.set_active_selections(&self.selections, cx);
+                    buffer.set_active_selections(&self.selections.disjoint_anchors(), cx);
                 }
             });
         }
@@ -118,7 +118,12 @@ impl FollowableItem for Editor {
             )),
             scroll_x: self.scroll_position.x(),
             scroll_y: self.scroll_position.y(),
-            selections: self.selections.iter().map(serialize_selection).collect(),
+            selections: self
+                .selections
+                .disjoint_anchors()
+                .iter()
+                .map(serialize_selection)
+                .collect(),
         }))
     }
 
@@ -144,8 +149,9 @@ impl FollowableItem for Editor {
                 Event::SelectionsChanged { .. } => {
                     update.selections = self
                         .selections
+                        .disjoint_anchors()
                         .iter()
-                        .chain(self.pending_selection.as_ref().map(|p| &p.selection))
+                        .chain(self.selections.pending_anchor().as_ref())
                         .map(serialize_selection)
                         .collect();
                     true
@@ -246,13 +252,13 @@ fn deserialize_selection(
 impl Item for Editor {
     fn navigate(&mut self, data: Box<dyn std::any::Any>, cx: &mut ViewContext<Self>) -> bool {
         if let Ok(data) = data.downcast::<NavigationData>() {
+            let newest_selection = self.selections.newest::<Point>(cx);
             let buffer = self.buffer.read(cx).read(cx);
             let offset = if buffer.can_resolve(&data.cursor_anchor) {
                 data.cursor_anchor.to_point(&buffer)
             } else {
                 buffer.clip_point(data.cursor_position, Bias::Left)
             };
-            let newest_selection = self.newest_selection_with_snapshot::<Point>(&buffer);
 
             let scroll_top_anchor = if buffer.can_resolve(&data.scroll_top_anchor) {
                 data.scroll_top_anchor
@@ -270,7 +276,9 @@ impl Item for Editor {
                 let nav_history = self.nav_history.take();
                 self.scroll_position = data.scroll_position;
                 self.scroll_top_anchor = scroll_top_anchor;
-                self.select_ranges([offset..offset], Some(Autoscroll::Fit), cx);
+                self.change_selections(Some(Autoscroll::Fit), cx, |s| {
+                    s.select_ranges([offset..offset])
+                });
                 self.nav_history = nav_history;
                 true
             }
@@ -307,7 +315,7 @@ impl Item for Editor {
     }
 
     fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
-        let selection = self.newest_anchor_selection();
+        let selection = self.selections.newest_anchor();
         self.push_to_nav_history(selection.head(), None, cx);
     }
 
@@ -457,7 +465,7 @@ impl CursorPosition {
 
         self.selected_count = 0;
         let mut last_selection: Option<Selection<usize>> = None;
-        for selection in editor.local_selections::<usize>(cx) {
+        for selection in editor.selections.all::<usize>(cx) {
             self.selected_count += selection.end - selection.start;
             if last_selection
                 .as_ref()

crates/editor/src/selections_collection.rs 🔗

@@ -0,0 +1,729 @@
+use std::{
+    cell::Ref,
+    cmp, iter, mem,
+    ops::{Deref, Range, Sub},
+    sync::Arc,
+};
+
+use collections::HashMap;
+use gpui::{AppContext, ModelHandle, MutableAppContext};
+use itertools::Itertools;
+use language::{rope::TextDimension, Bias, Point, Selection, SelectionGoal, ToPoint};
+use util::post_inc;
+
+use crate::{
+    display_map::{DisplayMap, DisplaySnapshot, ToDisplayPoint},
+    Anchor, DisplayPoint, ExcerptId, MultiBuffer, MultiBufferSnapshot, SelectMode, ToOffset,
+};
+
+#[derive(Clone)]
+pub struct PendingSelection {
+    pub selection: Selection<Anchor>,
+    pub mode: SelectMode,
+}
+
+pub struct SelectionsCollection {
+    display_map: ModelHandle<DisplayMap>,
+    buffer: ModelHandle<MultiBuffer>,
+    pub next_selection_id: usize,
+    disjoint: Arc<[Selection<Anchor>]>,
+    pending: Option<PendingSelection>,
+}
+
+impl SelectionsCollection {
+    pub fn new(display_map: ModelHandle<DisplayMap>, buffer: ModelHandle<MultiBuffer>) -> Self {
+        Self {
+            display_map,
+            buffer,
+            next_selection_id: 1,
+            disjoint: Arc::from([]),
+            pending: Some(PendingSelection {
+                selection: Selection {
+                    id: 0,
+                    start: Anchor::min(),
+                    end: Anchor::min(),
+                    reversed: false,
+                    goal: SelectionGoal::None,
+                },
+                mode: SelectMode::Character,
+            }),
+        }
+    }
+
+    fn display_map(&self, cx: &mut MutableAppContext) -> DisplaySnapshot {
+        self.display_map.update(cx, |map, cx| map.snapshot(cx))
+    }
+
+    fn buffer<'a>(&self, cx: &'a AppContext) -> Ref<'a, MultiBufferSnapshot> {
+        self.buffer.read(cx).read(cx)
+    }
+
+    pub fn count<'a>(&self) -> usize {
+        let mut count = self.disjoint.len();
+        if self.pending.is_some() {
+            count += 1;
+        }
+        count
+    }
+
+    pub fn disjoint_anchors(&self) -> Arc<[Selection<Anchor>]> {
+        self.disjoint.clone()
+    }
+
+    pub fn pending_anchor(&self) -> Option<Selection<Anchor>> {
+        self.pending
+            .as_ref()
+            .map(|pending| pending.selection.clone())
+    }
+
+    pub fn pending<D: TextDimension + Ord + Sub<D, Output = D>>(
+        &self,
+        cx: &AppContext,
+    ) -> Option<Selection<D>> {
+        self.pending_anchor()
+            .as_ref()
+            .map(|pending| pending.map(|p| p.summary::<D>(&self.buffer(cx))))
+    }
+
+    pub fn pending_mode(&self) -> Option<SelectMode> {
+        self.pending.as_ref().map(|pending| pending.mode.clone())
+    }
+
+    pub fn all<'a, D>(&self, cx: &AppContext) -> Vec<Selection<D>>
+    where
+        D: 'a + TextDimension + Ord + Sub<D, Output = D> + std::fmt::Debug,
+    {
+        let disjoint_anchors = &self.disjoint;
+        let mut disjoint =
+            resolve_multiple::<D, _>(disjoint_anchors.iter(), &self.buffer(cx)).peekable();
+
+        let mut pending_opt = self.pending::<D>(cx);
+
+        iter::from_fn(move || {
+            if let Some(pending) = pending_opt.as_mut() {
+                while let Some(next_selection) = disjoint.peek() {
+                    if pending.start <= next_selection.end && pending.end >= next_selection.start {
+                        let next_selection = disjoint.next().unwrap();
+                        if next_selection.start < pending.start {
+                            pending.start = next_selection.start;
+                        }
+                        if next_selection.end > pending.end {
+                            pending.end = next_selection.end;
+                        }
+                    } else if next_selection.end < pending.start {
+                        return disjoint.next();
+                    } else {
+                        break;
+                    }
+                }
+
+                pending_opt.take()
+            } else {
+                disjoint.next()
+            }
+        })
+        .collect()
+    }
+
+    pub fn disjoint_in_range<'a, D>(
+        &self,
+        range: Range<Anchor>,
+        cx: &AppContext,
+    ) -> Vec<Selection<D>>
+    where
+        D: 'a + TextDimension + Ord + Sub<D, Output = D> + std::fmt::Debug,
+    {
+        let buffer = self.buffer(cx);
+        let start_ix = match self
+            .disjoint
+            .binary_search_by(|probe| probe.end.cmp(&range.start, &buffer))
+        {
+            Ok(ix) | Err(ix) => ix,
+        };
+        let end_ix = match self
+            .disjoint
+            .binary_search_by(|probe| probe.start.cmp(&range.end, &buffer))
+        {
+            Ok(ix) => ix + 1,
+            Err(ix) => ix,
+        };
+        resolve_multiple(&self.disjoint[start_ix..end_ix], &buffer).collect()
+    }
+
+    pub fn all_display(
+        &mut self,
+        cx: &mut MutableAppContext,
+    ) -> (DisplaySnapshot, Vec<Selection<DisplayPoint>>) {
+        let display_map = self.display_map(cx);
+        let selections = self
+            .all::<Point>(cx)
+            .into_iter()
+            .map(|selection| selection.map(|point| point.to_display_point(&display_map)))
+            .collect();
+        (display_map, selections)
+    }
+
+    pub fn newest_anchor(&self) -> &Selection<Anchor> {
+        self.pending
+            .as_ref()
+            .map(|s| &s.selection)
+            .or_else(|| self.disjoint.iter().max_by_key(|s| s.id))
+            .unwrap()
+    }
+
+    pub fn newest<D: TextDimension + Ord + Sub<D, Output = D>>(
+        &self,
+        cx: &AppContext,
+    ) -> Selection<D> {
+        resolve(self.newest_anchor(), &self.buffer(cx))
+    }
+
+    pub fn oldest_anchor(&self) -> &Selection<Anchor> {
+        self.disjoint
+            .iter()
+            .min_by_key(|s| s.id)
+            .or_else(|| self.pending.as_ref().map(|p| &p.selection))
+            .unwrap()
+    }
+
+    pub fn oldest<D: TextDimension + Ord + Sub<D, Output = D>>(
+        &self,
+        cx: &AppContext,
+    ) -> Selection<D> {
+        resolve(self.oldest_anchor(), &self.buffer(cx))
+    }
+
+    pub fn first<D: TextDimension + Ord + Sub<D, Output = D>>(
+        &self,
+        cx: &AppContext,
+    ) -> Selection<D> {
+        self.all(cx).first().unwrap().clone()
+    }
+
+    pub fn last<D: TextDimension + Ord + Sub<D, Output = D>>(
+        &self,
+        cx: &AppContext,
+    ) -> Selection<D> {
+        self.all(cx).last().unwrap().clone()
+    }
+
+    #[cfg(any(test, feature = "test-support"))]
+    pub fn ranges<D: TextDimension + Ord + Sub<D, Output = D> + std::fmt::Debug>(
+        &self,
+        cx: &AppContext,
+    ) -> Vec<Range<D>> {
+        self.all::<D>(cx)
+            .iter()
+            .map(|s| {
+                if s.reversed {
+                    s.end.clone()..s.start.clone()
+                } else {
+                    s.start.clone()..s.end.clone()
+                }
+            })
+            .collect()
+    }
+
+    #[cfg(any(test, feature = "test-support"))]
+    pub fn display_ranges(&self, cx: &mut MutableAppContext) -> Vec<Range<DisplayPoint>> {
+        let display_map = self.display_map(cx);
+        self.disjoint_anchors()
+            .iter()
+            .chain(self.pending_anchor().as_ref())
+            .map(|s| {
+                if s.reversed {
+                    s.end.to_display_point(&display_map)..s.start.to_display_point(&display_map)
+                } else {
+                    s.start.to_display_point(&display_map)..s.end.to_display_point(&display_map)
+                }
+            })
+            .collect()
+    }
+
+    pub fn build_columnar_selection(
+        &mut self,
+        display_map: &DisplaySnapshot,
+        row: u32,
+        columns: &Range<u32>,
+        reversed: bool,
+    ) -> Option<Selection<Point>> {
+        let is_empty = columns.start == columns.end;
+        let line_len = display_map.line_len(row);
+        if columns.start < line_len || (is_empty && columns.start == line_len) {
+            let start = DisplayPoint::new(row, columns.start);
+            let end = DisplayPoint::new(row, cmp::min(columns.end, line_len));
+
+            Some(Selection {
+                id: post_inc(&mut self.next_selection_id),
+                start: start.to_point(display_map),
+                end: end.to_point(display_map),
+                reversed,
+                goal: SelectionGoal::ColumnRange {
+                    start: columns.start,
+                    end: columns.end,
+                },
+            })
+        } else {
+            None
+        }
+    }
+
+    pub(crate) fn change_with<R>(
+        &mut self,
+        cx: &mut MutableAppContext,
+        change: impl FnOnce(&mut MutableSelectionsCollection) -> R,
+    ) -> R {
+        let mut mutable_collection = MutableSelectionsCollection {
+            collection: self,
+            cx,
+        };
+
+        let result = change(&mut mutable_collection);
+        assert!(
+            !mutable_collection.disjoint.is_empty() || mutable_collection.pending.is_some(),
+            "There must be at least one selection"
+        );
+        result
+    }
+}
+
+pub struct MutableSelectionsCollection<'a> {
+    collection: &'a mut SelectionsCollection,
+    cx: &'a mut MutableAppContext,
+}
+
+impl<'a> MutableSelectionsCollection<'a> {
+    fn display_map(&mut self) -> DisplaySnapshot {
+        self.collection.display_map(self.cx)
+    }
+
+    fn buffer(&self) -> Ref<MultiBufferSnapshot> {
+        self.collection.buffer(self.cx)
+    }
+
+    pub fn clear_disjoint(&mut self) {
+        self.collection.disjoint = Arc::from([]);
+    }
+
+    pub fn delete(&mut self, selection_id: usize) {
+        self.collection.disjoint = self
+            .disjoint
+            .into_iter()
+            .filter(|selection| selection.id != selection_id)
+            .cloned()
+            .collect();
+    }
+
+    pub fn clear_pending(&mut self) {
+        self.collection.pending = None;
+    }
+
+    pub fn set_pending_range(&mut self, range: Range<Anchor>, mode: SelectMode) {
+        self.collection.pending = Some(PendingSelection {
+            selection: Selection {
+                id: post_inc(&mut self.collection.next_selection_id),
+                start: range.start,
+                end: range.end,
+                reversed: false,
+                goal: SelectionGoal::None,
+            },
+            mode,
+        })
+    }
+
+    pub fn set_pending(&mut self, selection: Selection<Anchor>, mode: SelectMode) {
+        self.collection.pending = Some(PendingSelection { selection, mode });
+    }
+
+    pub fn try_cancel(&mut self) -> bool {
+        if let Some(pending) = self.collection.pending.take() {
+            if self.disjoint.is_empty() {
+                self.collection.disjoint = Arc::from([pending.selection]);
+            }
+            return true;
+        }
+
+        let mut oldest = self.oldest_anchor().clone();
+        if self.count() > 1 {
+            self.collection.disjoint = Arc::from([oldest]);
+            return true;
+        }
+
+        if !oldest.start.cmp(&oldest.end, &self.buffer()).is_eq() {
+            let head = oldest.head();
+            oldest.start = head.clone();
+            oldest.end = head;
+            self.collection.disjoint = Arc::from([oldest]);
+            return true;
+        }
+
+        return false;
+    }
+
+    pub fn reset_biases(&mut self) {
+        let buffer = self.buffer.read(self.cx).snapshot(self.cx);
+        self.collection.disjoint = self
+            .collection
+            .disjoint
+            .into_iter()
+            .cloned()
+            .map(|selection| reset_biases(selection, &buffer))
+            .collect();
+
+        if let Some(pending) = self.collection.pending.as_mut() {
+            pending.selection = reset_biases(pending.selection.clone(), &buffer);
+        }
+    }
+
+    pub fn insert_range<T>(&mut self, range: Range<T>)
+    where
+        T: 'a + ToOffset + ToPoint + TextDimension + Ord + Sub<T, Output = T> + std::marker::Copy,
+    {
+        let mut selections = self.all(self.cx);
+        let mut start = range.start.to_offset(&self.buffer());
+        let mut end = range.end.to_offset(&self.buffer());
+        let reversed = if start > end {
+            mem::swap(&mut start, &mut end);
+            true
+        } else {
+            false
+        };
+        selections.push(Selection {
+            id: post_inc(&mut self.collection.next_selection_id),
+            start,
+            end,
+            reversed,
+            goal: SelectionGoal::None,
+        });
+        self.select(selections);
+    }
+
+    pub fn select<T>(&mut self, mut selections: Vec<Selection<T>>)
+    where
+        T: ToOffset + ToPoint + Ord + std::marker::Copy + std::fmt::Debug,
+    {
+        let buffer = self.buffer.read(self.cx).snapshot(self.cx);
+        selections.sort_unstable_by_key(|s| s.start);
+        // Merge overlapping selections.
+        let mut i = 1;
+        while i < selections.len() {
+            if selections[i - 1].end >= selections[i].start {
+                let removed = selections.remove(i);
+                if removed.start < selections[i - 1].start {
+                    selections[i - 1].start = removed.start;
+                }
+                if removed.end > selections[i - 1].end {
+                    selections[i - 1].end = removed.end;
+                }
+            } else {
+                i += 1;
+            }
+        }
+
+        self.collection.disjoint = Arc::from_iter(selections.into_iter().map(|selection| {
+            let end_bias = if selection.end > selection.start {
+                Bias::Left
+            } else {
+                Bias::Right
+            };
+            Selection {
+                id: selection.id,
+                start: buffer.anchor_after(selection.start),
+                end: buffer.anchor_at(selection.end, end_bias),
+                reversed: selection.reversed,
+                goal: selection.goal,
+            }
+        }));
+
+        self.collection.pending = None;
+    }
+
+    pub fn select_anchors(&mut self, selections: Vec<Selection<Anchor>>) {
+        let buffer = self.buffer.read(self.cx).snapshot(self.cx);
+        let resolved_selections =
+            resolve_multiple::<usize, _>(&selections, &buffer).collect::<Vec<_>>();
+        self.select(resolved_selections);
+    }
+
+    pub fn select_ranges<I, T>(&mut self, ranges: I)
+    where
+        I: IntoIterator<Item = Range<T>>,
+        T: ToOffset,
+    {
+        let buffer = self.buffer.read(self.cx).snapshot(self.cx);
+        let selections = ranges
+            .into_iter()
+            .map(|range| {
+                let mut start = range.start.to_offset(&buffer);
+                let mut end = range.end.to_offset(&buffer);
+                let reversed = if start > end {
+                    mem::swap(&mut start, &mut end);
+                    true
+                } else {
+                    false
+                };
+                Selection {
+                    id: post_inc(&mut self.collection.next_selection_id),
+                    start,
+                    end,
+                    reversed,
+                    goal: SelectionGoal::None,
+                }
+            })
+            .collect::<Vec<_>>();
+
+        self.select(selections)
+    }
+
+    pub fn select_anchor_ranges<I: IntoIterator<Item = Range<Anchor>>>(&mut self, ranges: I) {
+        let buffer = self.buffer.read(self.cx).snapshot(self.cx);
+        let selections = ranges
+            .into_iter()
+            .map(|range| {
+                let mut start = range.start;
+                let mut end = range.end;
+                let reversed = if start.cmp(&end, &buffer).is_gt() {
+                    mem::swap(&mut start, &mut end);
+                    true
+                } else {
+                    false
+                };
+                Selection {
+                    id: post_inc(&mut self.collection.next_selection_id),
+                    start,
+                    end,
+                    reversed,
+                    goal: SelectionGoal::None,
+                }
+            })
+            .collect::<Vec<_>>();
+
+        self.select_anchors(selections)
+    }
+
+    #[cfg(any(test, feature = "test-support"))]
+    pub fn select_display_ranges<T>(&mut self, ranges: T)
+    where
+        T: IntoIterator<Item = Range<DisplayPoint>>,
+    {
+        let display_map = self.display_map();
+        let selections = ranges
+            .into_iter()
+            .map(|range| {
+                let mut start = range.start;
+                let mut end = range.end;
+                let reversed = if start > end {
+                    mem::swap(&mut start, &mut end);
+                    true
+                } else {
+                    false
+                };
+                Selection {
+                    id: post_inc(&mut self.collection.next_selection_id),
+                    start: start.to_point(&display_map),
+                    end: end.to_point(&display_map),
+                    reversed,
+                    goal: SelectionGoal::None,
+                }
+            })
+            .collect();
+        self.select(selections);
+    }
+
+    pub fn move_with(
+        &mut self,
+        mut move_selection: impl FnMut(&DisplaySnapshot, &mut Selection<DisplayPoint>),
+    ) {
+        let display_map = self.display_map();
+        let selections = self
+            .all::<Point>(self.cx)
+            .into_iter()
+            .map(|selection| {
+                let mut selection = selection.map(|point| point.to_display_point(&display_map));
+                move_selection(&display_map, &mut selection);
+                selection.map(|display_point| display_point.to_point(&display_map))
+            })
+            .collect();
+
+        self.select(selections)
+    }
+
+    pub fn move_heads_with(
+        &mut self,
+        mut update_head: impl FnMut(
+            &DisplaySnapshot,
+            DisplayPoint,
+            SelectionGoal,
+        ) -> (DisplayPoint, SelectionGoal),
+    ) {
+        self.move_with(|map, selection| {
+            let (new_head, new_goal) = update_head(map, selection.head(), selection.goal);
+            selection.set_head(new_head, new_goal);
+        });
+    }
+
+    pub fn move_cursors_with(
+        &mut self,
+        mut update_cursor_position: impl FnMut(
+            &DisplaySnapshot,
+            DisplayPoint,
+            SelectionGoal,
+        ) -> (DisplayPoint, SelectionGoal),
+    ) {
+        self.move_with(|map, selection| {
+            let (cursor, new_goal) = update_cursor_position(map, selection.head(), selection.goal);
+            selection.collapse_to(cursor, new_goal)
+        });
+    }
+
+    pub fn replace_cursors_with(
+        &mut self,
+        mut find_replacement_cursors: impl FnMut(&DisplaySnapshot) -> Vec<DisplayPoint>,
+    ) {
+        let display_map = self.display_map();
+        let new_selections = find_replacement_cursors(&display_map)
+            .into_iter()
+            .map(|cursor| {
+                let cursor_point = cursor.to_point(&display_map);
+                Selection {
+                    id: post_inc(&mut self.collection.next_selection_id),
+                    start: cursor_point,
+                    end: cursor_point,
+                    reversed: false,
+                    goal: SelectionGoal::None,
+                }
+            })
+            .collect();
+        self.select(new_selections);
+    }
+
+    /// Compute new ranges for any selections that were located in excerpts that have
+    /// since been removed.
+    ///
+    /// Returns a `HashMap` indicating which selections whose former head position
+    /// was no longer present. The keys of the map are selection ids. The values are
+    /// the id of the new excerpt where the head of the selection has been moved.
+    pub fn refresh(&mut self) -> HashMap<usize, ExcerptId> {
+        let mut pending = self.collection.pending.take();
+        let mut selections_with_lost_position = HashMap::default();
+
+        let anchors_with_status = {
+            let buffer = self.buffer();
+            let disjoint_anchors = self
+                .disjoint
+                .iter()
+                .flat_map(|selection| [&selection.start, &selection.end]);
+            buffer.refresh_anchors(disjoint_anchors)
+        };
+        let adjusted_disjoint: Vec<_> = anchors_with_status
+            .chunks(2)
+            .map(|selection_anchors| {
+                let (anchor_ix, start, kept_start) = selection_anchors[0].clone();
+                let (_, end, kept_end) = selection_anchors[1].clone();
+                let selection = &self.disjoint[anchor_ix / 2];
+                let kept_head = if selection.reversed {
+                    kept_start
+                } else {
+                    kept_end
+                };
+                if !kept_head {
+                    selections_with_lost_position
+                        .insert(selection.id, selection.head().excerpt_id.clone());
+                }
+
+                Selection {
+                    id: selection.id,
+                    start,
+                    end,
+                    reversed: selection.reversed,
+                    goal: selection.goal,
+                }
+            })
+            .collect();
+
+        if !adjusted_disjoint.is_empty() {
+            let resolved_selections =
+                resolve_multiple(adjusted_disjoint.iter(), &self.buffer()).collect();
+            self.select::<usize>(resolved_selections);
+        }
+
+        if let Some(pending) = pending.as_mut() {
+            let buffer = self.buffer();
+            let anchors =
+                buffer.refresh_anchors([&pending.selection.start, &pending.selection.end]);
+            let (_, start, kept_start) = anchors[0].clone();
+            let (_, end, kept_end) = anchors[1].clone();
+            let kept_head = if pending.selection.reversed {
+                kept_start
+            } else {
+                kept_end
+            };
+            if !kept_head {
+                selections_with_lost_position.insert(
+                    pending.selection.id,
+                    pending.selection.head().excerpt_id.clone(),
+                );
+            }
+
+            pending.selection.start = start;
+            pending.selection.end = end;
+        }
+        self.collection.pending = pending;
+
+        selections_with_lost_position
+    }
+}
+
+impl<'a> Deref for MutableSelectionsCollection<'a> {
+    type Target = SelectionsCollection;
+    fn deref(&self) -> &Self::Target {
+        self.collection
+    }
+}
+
+// Panics if passed selections are not in order
+pub fn resolve_multiple<'a, D, I>(
+    selections: I,
+    snapshot: &MultiBufferSnapshot,
+) -> impl 'a + Iterator<Item = Selection<D>>
+where
+    D: TextDimension + Ord + Sub<D, Output = D> + std::fmt::Debug,
+    I: 'a + IntoIterator<Item = &'a Selection<Anchor>>,
+{
+    let (to_summarize, selections) = selections.into_iter().tee();
+    let mut summaries = snapshot
+        .summaries_for_anchors::<D, _>(
+            to_summarize
+                .flat_map(|s| [&s.start, &s.end])
+                .collect::<Vec<_>>(),
+        )
+        .into_iter();
+    selections.map(move |s| Selection {
+        id: s.id,
+        start: summaries.next().unwrap(),
+        end: summaries.next().unwrap(),
+        reversed: s.reversed,
+        goal: s.goal,
+    })
+}
+
+fn resolve<D: TextDimension + Ord + Sub<D, Output = D>>(
+    selection: &Selection<Anchor>,
+    buffer: &MultiBufferSnapshot,
+) -> Selection<D> {
+    selection.map(|p| p.summary::<D>(&buffer))
+}
+
+fn reset_biases(
+    mut selection: Selection<Anchor>,
+    buffer: &MultiBufferSnapshot,
+) -> Selection<Anchor> {
+    let end_bias = if selection.end.to_offset(buffer) > selection.start.to_offset(buffer) {
+        Bias::Left
+    } else {
+        Bias::Right
+    };
+    selection.start = buffer.anchor_after(selection.start);
+    selection.end = buffer.anchor_at(selection.end, end_bias);
+    selection
+}

crates/editor/src/test.rs 🔗

@@ -43,7 +43,7 @@ pub fn marked_display_snapshot(
 pub fn select_ranges(editor: &mut Editor, marked_text: &str, cx: &mut ViewContext<Editor>) {
     let (umarked_text, text_ranges) = marked_text_ranges(marked_text);
     assert_eq!(editor.text(cx), umarked_text);
-    editor.select_ranges(text_ranges, None, cx);
+    editor.change_selections(None, cx, |s| s.select_ranges(text_ranges));
 }
 
 pub fn assert_text_with_selections(
@@ -54,5 +54,5 @@ pub fn assert_text_with_selections(
     let (unmarked_text, text_ranges) = marked_text_ranges(marked_text);
 
     assert_eq!(editor.text(cx), unmarked_text);
-    assert_eq!(editor.selected_ranges(cx), text_ranges);
+    assert_eq!(editor.selections.ranges(cx), text_ranges);
 }

crates/go_to_line/src/go_to_line.rs 🔗

@@ -43,7 +43,7 @@ impl GoToLine {
             let buffer = editor.buffer().read(cx).read(cx);
             (
                 Some(scroll_position),
-                editor.newest_selection_with_snapshot(&buffer).head(),
+                editor.selections.newest(cx).head(),
                 buffer.max_point(),
             )
         });
@@ -80,7 +80,9 @@ impl GoToLine {
             if let Some(rows) = active_editor.highlighted_rows() {
                 let snapshot = active_editor.snapshot(cx).display_snapshot;
                 let position = DisplayPoint::new(rows.start, 0).to_point(&snapshot);
-                active_editor.select_ranges([position..position], Some(Autoscroll::Center), cx);
+                active_editor.change_selections(Some(Autoscroll::Center), cx, |s| {
+                    s.select_ranges([position..position])
+                });
             }
         });
         cx.emit(Event::Dismissed);

crates/journal/src/journal.rs 🔗

@@ -57,7 +57,9 @@ pub fn new_journal_entry(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
                 if let Some(editor) = item.downcast::<Editor>() {
                     editor.update(&mut cx, |editor, cx| {
                         let len = editor.buffer().read(cx).read(cx).len();
-                        editor.select_ranges([len..len], Some(Autoscroll::Center), cx);
+                        editor.change_selections(Some(Autoscroll::Center), cx, |s| {
+                            s.select_ranges([len..len])
+                        });
                         if len > 0 {
                             editor.insert("\n\n", cx);
                         }

crates/outline/src/outline.rs 🔗

@@ -172,9 +172,7 @@ impl PickerDelegate for OutlineView {
 
             let editor = self.active_editor.read(cx);
             let buffer = editor.buffer().read(cx).read(cx);
-            let cursor_offset = editor
-                .newest_selection_with_snapshot::<usize>(&buffer)
-                .head();
+            let cursor_offset = editor.selections.newest::<usize>(cx).head();
             selected_index = self
                 .outline
                 .items
@@ -217,7 +215,9 @@ impl PickerDelegate for OutlineView {
             if let Some(rows) = active_editor.highlighted_rows() {
                 let snapshot = active_editor.snapshot(cx).display_snapshot;
                 let position = DisplayPoint::new(rows.start, 0).to_point(&snapshot);
-                active_editor.select_ranges([position..position], Some(Autoscroll::Center), cx);
+                active_editor.change_selections(Some(Autoscroll::Center), cx, |s| {
+                    s.select_ranges([position..position])
+                });
             }
         });
         cx.emit(Event::Dismissed);

crates/project_symbols/src/project_symbols.rs 🔗

@@ -145,11 +145,9 @@ impl ProjectSymbolsView {
 
                         let editor = workspace.open_project_item::<Editor>(buffer, cx);
                         editor.update(cx, |editor, cx| {
-                            editor.select_ranges(
-                                [position..position],
-                                Some(Autoscroll::Center),
-                                cx,
-                            );
+                            editor.change_selections(Some(Autoscroll::Center), cx, |s| {
+                                s.select_ranges([position..position])
+                            });
                         });
                     });
                     Ok::<_, anyhow::Error>(())

crates/search/src/buffer_search.rs 🔗

@@ -225,9 +225,7 @@ impl BufferSearchBar {
         let display_map = editor
             .update(cx, |editor, cx| editor.snapshot(cx))
             .display_snapshot;
-        let selection = editor
-            .read(cx)
-            .newest_selection_with_snapshot::<usize>(&display_map.buffer_snapshot);
+        let selection = editor.read(cx).selections.newest::<usize>(cx);
 
         let mut text: String;
         if selection.start == selection.end {
@@ -387,14 +385,16 @@ impl BufferSearchBar {
                     if let Some(ranges) = self.editors_with_matches.get(&cx.weak_handle()) {
                         let new_index = match_index_for_direction(
                             ranges,
-                            &editor.newest_anchor_selection().head(),
+                            &editor.selections.newest_anchor().head(),
                             index,
                             direction,
                             &editor.buffer().read(cx).read(cx),
                         );
                         let range_to_select = ranges[new_index].clone();
                         editor.unfold_ranges([range_to_select.clone()], false, cx);
-                        editor.select_ranges([range_to_select], Some(Autoscroll::Fit), cx);
+                        editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
+                            s.select_ranges([range_to_select])
+                        });
                     }
                 });
             }
@@ -535,10 +535,10 @@ impl BufferSearchBar {
                                 editor.update(cx, |editor, cx| {
                                     if select_closest_match {
                                         if let Some(match_ix) = this.active_match_index {
-                                            editor.select_ranges(
-                                                [ranges[match_ix].clone()],
+                                            editor.change_selections(
                                                 Some(Autoscroll::Fit),
                                                 cx,
+                                                |s| s.select_ranges([ranges[match_ix].clone()]),
                                             );
                                         }
                                     }
@@ -564,7 +564,7 @@ impl BufferSearchBar {
             let editor = editor.read(cx);
             active_match_index(
                 &ranges,
-                &editor.newest_anchor_selection().head(),
+                &editor.selections.newest_anchor().head(),
                 &editor.buffer().read(cx).read(cx),
             )
         });
@@ -721,13 +721,15 @@ mod tests {
         });
 
         editor.update(cx, |editor, cx| {
-            editor.select_display_ranges(&[DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)], cx);
+            editor.change_selections(None, cx, |s| {
+                s.select_display_ranges([DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)])
+            });
         });
         search_bar.update(cx, |search_bar, cx| {
             assert_eq!(search_bar.active_match_index, Some(0));
             search_bar.select_next_match(&SelectNextMatch, cx);
             assert_eq!(
-                editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
+                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
                 [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
             );
         });
@@ -738,7 +740,7 @@ mod tests {
         search_bar.update(cx, |search_bar, cx| {
             search_bar.select_next_match(&SelectNextMatch, cx);
             assert_eq!(
-                editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
+                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
                 [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)]
             );
         });
@@ -749,7 +751,7 @@ mod tests {
         search_bar.update(cx, |search_bar, cx| {
             search_bar.select_next_match(&SelectNextMatch, cx);
             assert_eq!(
-                editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
+                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
                 [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
             );
         });
@@ -760,7 +762,7 @@ mod tests {
         search_bar.update(cx, |search_bar, cx| {
             search_bar.select_next_match(&SelectNextMatch, cx);
             assert_eq!(
-                editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
+                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
                 [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
             );
         });
@@ -771,7 +773,7 @@ mod tests {
         search_bar.update(cx, |search_bar, cx| {
             search_bar.select_prev_match(&SelectPrevMatch, cx);
             assert_eq!(
-                editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
+                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
                 [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
             );
         });
@@ -782,7 +784,7 @@ mod tests {
         search_bar.update(cx, |search_bar, cx| {
             search_bar.select_prev_match(&SelectPrevMatch, cx);
             assert_eq!(
-                editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
+                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
                 [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)]
             );
         });
@@ -793,7 +795,7 @@ mod tests {
         search_bar.update(cx, |search_bar, cx| {
             search_bar.select_prev_match(&SelectPrevMatch, cx);
             assert_eq!(
-                editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
+                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
                 [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
             );
         });
@@ -804,13 +806,15 @@ mod tests {
         // Park the cursor in between matches and ensure that going to the previous match selects
         // the closest match to the left.
         editor.update(cx, |editor, cx| {
-            editor.select_display_ranges(&[DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)], cx);
+            editor.change_selections(None, cx, |s| {
+                s.select_display_ranges([DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)])
+            });
         });
         search_bar.update(cx, |search_bar, cx| {
             assert_eq!(search_bar.active_match_index, Some(1));
             search_bar.select_prev_match(&SelectPrevMatch, cx);
             assert_eq!(
-                editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
+                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
                 [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
             );
         });
@@ -821,13 +825,15 @@ mod tests {
         // Park the cursor in between matches and ensure that going to the next match selects the
         // closest match to the right.
         editor.update(cx, |editor, cx| {
-            editor.select_display_ranges(&[DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)], cx);
+            editor.change_selections(None, cx, |s| {
+                s.select_display_ranges([DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)])
+            });
         });
         search_bar.update(cx, |search_bar, cx| {
             assert_eq!(search_bar.active_match_index, Some(1));
             search_bar.select_next_match(&SelectNextMatch, cx);
             assert_eq!(
-                editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
+                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
                 [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)]
             );
         });
@@ -838,13 +844,15 @@ mod tests {
         // Park the cursor after the last match and ensure that going to the previous match selects
         // the last match.
         editor.update(cx, |editor, cx| {
-            editor.select_display_ranges(&[DisplayPoint::new(3, 60)..DisplayPoint::new(3, 60)], cx);
+            editor.change_selections(None, cx, |s| {
+                s.select_display_ranges([DisplayPoint::new(3, 60)..DisplayPoint::new(3, 60)])
+            });
         });
         search_bar.update(cx, |search_bar, cx| {
             assert_eq!(search_bar.active_match_index, Some(2));
             search_bar.select_prev_match(&SelectPrevMatch, cx);
             assert_eq!(
-                editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
+                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
                 [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
             );
         });
@@ -855,13 +863,15 @@ mod tests {
         // Park the cursor after the last match and ensure that going to the next match selects the
         // first match.
         editor.update(cx, |editor, cx| {
-            editor.select_display_ranges(&[DisplayPoint::new(3, 60)..DisplayPoint::new(3, 60)], cx);
+            editor.change_selections(None, cx, |s| {
+                s.select_display_ranges([DisplayPoint::new(3, 60)..DisplayPoint::new(3, 60)])
+            });
         });
         search_bar.update(cx, |search_bar, cx| {
             assert_eq!(search_bar.active_match_index, Some(2));
             search_bar.select_next_match(&SelectNextMatch, cx);
             assert_eq!(
-                editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
+                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
                 [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
             );
         });
@@ -872,13 +882,15 @@ mod tests {
         // Park the cursor before the first match and ensure that going to the previous match
         // selects the last match.
         editor.update(cx, |editor, cx| {
-            editor.select_display_ranges(&[DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)], cx);
+            editor.change_selections(None, cx, |s| {
+                s.select_display_ranges([DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)])
+            });
         });
         search_bar.update(cx, |search_bar, cx| {
             assert_eq!(search_bar.active_match_index, Some(0));
             search_bar.select_prev_match(&SelectPrevMatch, cx);
             assert_eq!(
-                editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
+                editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
                 [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
             );
         });

crates/search/src/project_search.rs 🔗

@@ -454,7 +454,7 @@ impl ProjectSearchView {
             let results_editor = self.results_editor.read(cx);
             let new_index = match_index_for_direction(
                 &model.match_ranges,
-                &results_editor.newest_anchor_selection().head(),
+                &results_editor.selections.newest_anchor().head(),
                 index,
                 direction,
                 &results_editor.buffer().read(cx).read(cx),
@@ -462,7 +462,9 @@ impl ProjectSearchView {
             let range_to_select = model.match_ranges[new_index].clone();
             self.results_editor.update(cx, |editor, cx| {
                 editor.unfold_ranges([range_to_select.clone()], false, cx);
-                editor.select_ranges([range_to_select], Some(Autoscroll::Fit), cx);
+                editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
+                    s.select_ranges([range_to_select])
+                });
             });
         }
     }
@@ -476,8 +478,8 @@ impl ProjectSearchView {
 
     fn focus_results_editor(&self, cx: &mut ViewContext<Self>) {
         self.query_editor.update(cx, |query_editor, cx| {
-            let cursor = query_editor.newest_anchor_selection().head();
-            query_editor.select_ranges([cursor.clone()..cursor], None, cx);
+            let cursor = query_editor.selections.newest_anchor().head();
+            query_editor.change_selections(None, cx, |s| s.select_ranges([cursor.clone()..cursor]));
         });
         cx.focus(&self.results_editor);
     }
@@ -489,7 +491,9 @@ impl ProjectSearchView {
         } else {
             self.results_editor.update(cx, |editor, cx| {
                 if reset_selections {
-                    editor.select_ranges(match_ranges.first().cloned(), Some(Autoscroll::Fit), cx);
+                    editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
+                        s.select_ranges(match_ranges.first().cloned())
+                    });
                 }
                 editor.highlight_background::<Self>(
                     match_ranges,
@@ -510,7 +514,7 @@ impl ProjectSearchView {
         let results_editor = self.results_editor.read(cx);
         let new_index = active_match_index(
             &self.model.read(cx).match_ranges,
-            &results_editor.newest_anchor_selection().head(),
+            &results_editor.selections.newest_anchor().head(),
             &results_editor.buffer().read(cx).read(cx),
         );
         if self.active_match_index != new_index {
@@ -887,7 +891,7 @@ mod tests {
             assert_eq!(
                 search_view
                     .results_editor
-                    .update(cx, |editor, cx| editor.selected_display_ranges(cx)),
+                    .update(cx, |editor, cx| editor.selections.display_ranges(cx)),
                 [DisplayPoint::new(2, 32)..DisplayPoint::new(2, 35)]
             );
 
@@ -899,7 +903,7 @@ mod tests {
             assert_eq!(
                 search_view
                     .results_editor
-                    .update(cx, |editor, cx| editor.selected_display_ranges(cx)),
+                    .update(cx, |editor, cx| editor.selections.display_ranges(cx)),
                 [DisplayPoint::new(2, 37)..DisplayPoint::new(2, 40)]
             );
             search_view.select_match(Direction::Next, cx);
@@ -910,7 +914,7 @@ mod tests {
             assert_eq!(
                 search_view
                     .results_editor
-                    .update(cx, |editor, cx| editor.selected_display_ranges(cx)),
+                    .update(cx, |editor, cx| editor.selections.display_ranges(cx)),
                 [DisplayPoint::new(5, 6)..DisplayPoint::new(5, 9)]
             );
             search_view.select_match(Direction::Next, cx);
@@ -921,7 +925,7 @@ mod tests {
             assert_eq!(
                 search_view
                     .results_editor
-                    .update(cx, |editor, cx| editor.selected_display_ranges(cx)),
+                    .update(cx, |editor, cx| editor.selections.display_ranges(cx)),
                 [DisplayPoint::new(2, 32)..DisplayPoint::new(2, 35)]
             );
             search_view.select_match(Direction::Prev, cx);
@@ -932,7 +936,7 @@ mod tests {
             assert_eq!(
                 search_view
                     .results_editor
-                    .update(cx, |editor, cx| editor.selected_display_ranges(cx)),
+                    .update(cx, |editor, cx| editor.selections.display_ranges(cx)),
                 [DisplayPoint::new(5, 6)..DisplayPoint::new(5, 9)]
             );
             search_view.select_match(Direction::Prev, cx);
@@ -943,7 +947,7 @@ mod tests {
             assert_eq!(
                 search_view
                     .results_editor
-                    .update(cx, |editor, cx| editor.selected_display_ranges(cx)),
+                    .update(cx, |editor, cx| editor.selections.display_ranges(cx)),
                 [DisplayPoint::new(2, 37)..DisplayPoint::new(2, 40)]
             );
         });

crates/vim/src/insert.rs 🔗

@@ -1,5 +1,5 @@
 use crate::{state::Mode, Vim};
-use editor::Bias;
+use editor::{Autoscroll, Bias};
 use gpui::{actions, MutableAppContext, ViewContext};
 use language::SelectionGoal;
 use workspace::Workspace;
@@ -13,9 +13,11 @@ pub fn init(cx: &mut MutableAppContext) {
 fn normal_before(_: &mut Workspace, _: &NormalBefore, cx: &mut ViewContext<Workspace>) {
     Vim::update(cx, |state, cx| {
         state.update_active_editor(cx, |editor, cx| {
-            editor.move_cursors(cx, |map, mut cursor, _| {
-                *cursor.column_mut() = cursor.column().saturating_sub(1);
-                (map.clip_point(cursor, Bias::Left), SelectionGoal::None)
+            editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
+                s.move_cursors_with(|map, mut cursor, _| {
+                    *cursor.column_mut() = cursor.column().saturating_sub(1);
+                    (map.clip_point(cursor, Bias::Left), SelectionGoal::None)
+                });
             });
         });
         state.switch_mode(Mode::Normal, cx);

crates/vim/src/normal.rs 🔗

@@ -8,7 +8,7 @@ use crate::{
 };
 use change::init as change_init;
 use collections::HashSet;
-use editor::{Bias, DisplayPoint};
+use editor::{Autoscroll, Bias, DisplayPoint};
 use gpui::{actions, MutableAppContext, ViewContext};
 use language::SelectionGoal;
 use workspace::Workspace;
@@ -76,7 +76,9 @@ pub fn normal_motion(motion: Motion, cx: &mut MutableAppContext) {
 
 fn move_cursor(vim: &mut Vim, motion: Motion, cx: &mut MutableAppContext) {
     vim.update_active_editor(cx, |editor, cx| {
-        editor.move_cursors(cx, |map, cursor, goal| motion.move_point(map, cursor, goal))
+        editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
+            s.move_cursors_with(|map, cursor, goal| motion.move_point(map, cursor, goal))
+        })
     });
 }
 
@@ -84,8 +86,10 @@ fn insert_after(_: &mut Workspace, _: &InsertAfter, cx: &mut ViewContext<Workspa
     Vim::update(cx, |vim, cx| {
         vim.switch_mode(Mode::Insert, cx);
         vim.update_active_editor(cx, |editor, cx| {
-            editor.move_cursors(cx, |map, cursor, goal| {
-                Motion::Right.move_point(map, cursor, goal)
+            editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
+                s.move_cursors_with(|map, cursor, goal| {
+                    Motion::Right.move_point(map, cursor, goal)
+                });
             });
         });
     });
@@ -99,8 +103,10 @@ fn insert_first_non_whitespace(
     Vim::update(cx, |vim, cx| {
         vim.switch_mode(Mode::Insert, cx);
         vim.update_active_editor(cx, |editor, cx| {
-            editor.move_cursors(cx, |map, cursor, goal| {
-                Motion::FirstNonWhitespace.move_point(map, cursor, goal)
+            editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
+                s.move_cursors_with(|map, cursor, goal| {
+                    Motion::FirstNonWhitespace.move_point(map, cursor, goal)
+                });
             });
         });
     });
@@ -110,8 +116,10 @@ fn insert_end_of_line(_: &mut Workspace, _: &InsertEndOfLine, cx: &mut ViewConte
     Vim::update(cx, |vim, cx| {
         vim.switch_mode(Mode::Insert, cx);
         vim.update_active_editor(cx, |editor, cx| {
-            editor.move_cursors(cx, |map, cursor, goal| {
-                Motion::EndOfLine.move_point(map, cursor, goal)
+            editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
+                s.move_cursors_with(|map, cursor, goal| {
+                    Motion::EndOfLine.move_point(map, cursor, goal)
+                });
             });
         });
     });
@@ -122,7 +130,7 @@ fn insert_line_above(_: &mut Workspace, _: &InsertLineAbove, cx: &mut ViewContex
         vim.switch_mode(Mode::Insert, cx);
         vim.update_active_editor(cx, |editor, cx| {
             editor.transact(cx, |editor, cx| {
-                let (map, old_selections) = editor.display_selections(cx);
+                let (map, old_selections) = editor.selections.all_display(cx);
                 let selection_start_rows: HashSet<u32> = old_selections
                     .into_iter()
                     .map(|selection| selection.start.row())
@@ -137,10 +145,12 @@ fn insert_line_above(_: &mut Workspace, _: &InsertLineAbove, cx: &mut ViewContex
                     (start_of_line..start_of_line, new_text)
                 });
                 editor.edit_with_autoindent(edits, cx);
-                editor.move_cursors(cx, |map, mut cursor, _| {
-                    *cursor.row_mut() -= 1;
-                    *cursor.column_mut() = map.line_len(cursor.row());
-                    (map.clip_point(cursor, Bias::Left), SelectionGoal::None)
+                editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
+                    s.move_cursors_with(|map, mut cursor, _| {
+                        *cursor.row_mut() -= 1;
+                        *cursor.column_mut() = map.line_len(cursor.row());
+                        (map.clip_point(cursor, Bias::Left), SelectionGoal::None)
+                    });
                 });
             });
         });
@@ -152,7 +162,7 @@ fn insert_line_below(_: &mut Workspace, _: &InsertLineBelow, cx: &mut ViewContex
         vim.switch_mode(Mode::Insert, cx);
         vim.update_active_editor(cx, |editor, cx| {
             editor.transact(cx, |editor, cx| {
-                let (map, old_selections) = editor.display_selections(cx);
+                let (map, old_selections) = editor.selections.all_display(cx);
                 let selection_end_rows: HashSet<u32> = old_selections
                     .into_iter()
                     .map(|selection| selection.end.row())
@@ -166,8 +176,10 @@ fn insert_line_below(_: &mut Workspace, _: &InsertLineBelow, cx: &mut ViewContex
                     new_text.push_str(&" ".repeat(indent as usize));
                     (end_of_line..end_of_line, new_text)
                 });
-                editor.move_cursors(cx, |map, cursor, goal| {
-                    Motion::EndOfLine.move_point(map, cursor, goal)
+                editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
+                    s.move_cursors_with(|map, cursor, goal| {
+                        Motion::EndOfLine.move_point(map, cursor, goal)
+                    });
                 });
                 editor.edit_with_autoindent(edits, cx);
             });

crates/vim/src/normal/change.rs 🔗

@@ -1,5 +1,5 @@
 use crate::{motion::Motion, state::Mode, Vim};
-use editor::{char_kind, movement};
+use editor::{char_kind, movement, Autoscroll};
 use gpui::{impl_actions, MutableAppContext, ViewContext};
 use serde::Deserialize;
 use workspace::Workspace;
@@ -22,8 +22,10 @@ pub fn change_over(vim: &mut Vim, motion: Motion, cx: &mut MutableAppContext) {
         editor.transact(cx, |editor, cx| {
             // We are swapping to insert mode anyway. Just set the line end clipping behavior now
             editor.set_clip_at_line_ends(false, cx);
-            editor.move_selections(cx, |map, selection| {
-                motion.expand_selection(map, selection, false);
+            editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
+                s.move_with(|map, selection| {
+                    motion.expand_selection(map, selection, false);
+                });
             });
             editor.insert(&"", cx);
         });
@@ -46,16 +48,21 @@ fn change_word(
             editor.transact(cx, |editor, cx| {
                 // We are swapping to insert mode anyway. Just set the line end clipping behavior now
                 editor.set_clip_at_line_ends(false, cx);
-                editor.move_selections(cx, |map, selection| {
-                    if selection.end.column() == map.line_len(selection.end.row()) {
-                        return;
-                    }
+                editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
+                    s.move_with(|map, selection| {
+                        if selection.end.column() == map.line_len(selection.end.row()) {
+                            return;
+                        }
 
-                    selection.end = movement::find_boundary(map, selection.end, |left, right| {
-                        let left_kind = char_kind(left).coerce_punctuation(ignore_punctuation);
-                        let right_kind = char_kind(right).coerce_punctuation(ignore_punctuation);
+                        selection.end =
+                            movement::find_boundary(map, selection.end, |left, right| {
+                                let left_kind =
+                                    char_kind(left).coerce_punctuation(ignore_punctuation);
+                                let right_kind =
+                                    char_kind(right).coerce_punctuation(ignore_punctuation);
 
-                        left_kind != right_kind || left == '\n' || right == '\n'
+                                left_kind != right_kind || left == '\n' || right == '\n'
+                            });
                     });
                 });
                 editor.insert(&"", cx);

crates/vim/src/normal/delete.rs 🔗

@@ -1,6 +1,6 @@
 use crate::{motion::Motion, Vim};
 use collections::HashMap;
-use editor::Bias;
+use editor::{Autoscroll, Bias};
 use gpui::MutableAppContext;
 
 pub fn delete_over(vim: &mut Vim, motion: Motion, cx: &mut MutableAppContext) {
@@ -8,24 +8,28 @@ pub fn delete_over(vim: &mut Vim, motion: Motion, cx: &mut MutableAppContext) {
         editor.transact(cx, |editor, cx| {
             editor.set_clip_at_line_ends(false, cx);
             let mut original_columns: HashMap<_, _> = Default::default();
-            editor.move_selections(cx, |map, selection| {
-                let original_head = selection.head();
-                motion.expand_selection(map, selection, true);
-                original_columns.insert(selection.id, original_head.column());
+            editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
+                s.move_with(|map, selection| {
+                    let original_head = selection.head();
+                    motion.expand_selection(map, selection, true);
+                    original_columns.insert(selection.id, original_head.column());
+                });
             });
             editor.insert(&"", cx);
 
             // Fixup cursor position after the deletion
             editor.set_clip_at_line_ends(true, cx);
-            editor.move_selections(cx, |map, selection| {
-                let mut cursor = selection.head();
-                if motion.linewise() {
-                    if let Some(column) = original_columns.get(&selection.id) {
-                        *cursor.column_mut() = *column
+            editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
+                s.move_with(|map, selection| {
+                    let mut cursor = selection.head();
+                    if motion.linewise() {
+                        if let Some(column) = original_columns.get(&selection.id) {
+                            *cursor.column_mut() = *column
+                        }
                     }
-                }
-                cursor = map.clip_point(cursor, Bias::Left);
-                selection.collapse_to(cursor, selection.goal)
+                    cursor = map.clip_point(cursor, Bias::Left);
+                    selection.collapse_to(cursor, selection.goal)
+                });
             });
         });
     });

crates/vim/src/vim_test_context.rs 🔗

@@ -3,7 +3,7 @@ use std::ops::{Deref, Range};
 use collections::BTreeMap;
 use itertools::{Either, Itertools};
 
-use editor::display_map::ToDisplayPoint;
+use editor::{display_map::ToDisplayPoint, Autoscroll};
 use gpui::{json::json, keymap::Keystroke, ViewHandle};
 use indoc::indoc;
 use language::Selection;
@@ -128,7 +128,9 @@ impl<'a> VimTestContext<'a> {
             let (unmarked_text, markers) = marked_text(&text);
             editor.set_text(unmarked_text, cx);
             let cursor_offset = markers[0];
-            editor.replace_selections_with(cx, |map| cursor_offset.to_display_point(map));
+            editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
+                s.replace_cursors_with(|map| vec![cursor_offset.to_display_point(map)])
+            });
         })
     }
 
@@ -197,7 +199,8 @@ impl<'a> VimTestContext<'a> {
         let (empty_selections, reverse_selections, forward_selections) =
             self.editor.read_with(self.cx, |editor, cx| {
                 let (empty_selections, non_empty_selections): (Vec<_>, Vec<_>) = editor
-                    .local_selections::<usize>(cx)
+                    .selections
+                    .all::<usize>(cx)
                     .into_iter()
                     .partition_map(|selection| {
                         if selection.is_empty() {

crates/vim/src/visual.rs 🔗

@@ -1,4 +1,4 @@
-use editor::Bias;
+use editor::{Autoscroll, Bias};
 use gpui::{actions, MutableAppContext, ViewContext};
 use workspace::Workspace;
 
@@ -14,23 +14,25 @@ pub fn init(cx: &mut MutableAppContext) {
 pub fn visual_motion(motion: Motion, cx: &mut MutableAppContext) {
     Vim::update(cx, |vim, cx| {
         vim.update_active_editor(cx, |editor, cx| {
-            editor.move_selections(cx, |map, selection| {
-                let (new_head, goal) = motion.move_point(map, selection.head(), selection.goal);
-                let new_head = map.clip_at_line_end(new_head);
-                let was_reversed = selection.reversed;
-                selection.set_head(new_head, goal);
+            editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
+                s.move_with(|map, selection| {
+                    let (new_head, goal) = motion.move_point(map, selection.head(), selection.goal);
+                    let new_head = map.clip_at_line_end(new_head);
+                    let was_reversed = selection.reversed;
+                    selection.set_head(new_head, goal);
 
-                if was_reversed && !selection.reversed {
-                    // Head was at the start of the selection, and now is at the end. We need to move the start
-                    // back by one if possible in order to compensate for this change.
-                    *selection.start.column_mut() = selection.start.column().saturating_sub(1);
-                    selection.start = map.clip_point(selection.start, Bias::Left);
-                } else if !was_reversed && selection.reversed {
-                    // Head was at the end of the selection, and now is at the start. We need to move the end
-                    // forward by one if possible in order to compensate for this change.
-                    *selection.end.column_mut() = selection.end.column() + 1;
-                    selection.end = map.clip_point(selection.end, Bias::Left);
-                }
+                    if was_reversed && !selection.reversed {
+                        // Head was at the start of the selection, and now is at the end. We need to move the start
+                        // back by one if possible in order to compensate for this change.
+                        *selection.start.column_mut() = selection.start.column().saturating_sub(1);
+                        selection.start = map.clip_point(selection.start, Bias::Left);
+                    } else if !was_reversed && selection.reversed {
+                        // Head was at the end of the selection, and now is at the start. We need to move the end
+                        // forward by one if possible in order to compensate for this change.
+                        *selection.end.column_mut() = selection.end.column() + 1;
+                        selection.end = map.clip_point(selection.end, Bias::Left);
+                    }
+                });
             });
         });
     });
@@ -40,13 +42,15 @@ pub fn change(_: &mut Workspace, _: &VisualChange, cx: &mut ViewContext<Workspac
     Vim::update(cx, |vim, cx| {
         vim.update_active_editor(cx, |editor, cx| {
             editor.set_clip_at_line_ends(false, cx);
-            editor.move_selections(cx, |map, selection| {
-                if !selection.reversed {
-                    // Head was at the end of the selection, and now is at the start. We need to move the end
-                    // forward by one if possible in order to compensate for this change.
-                    *selection.end.column_mut() = selection.end.column() + 1;
-                    selection.end = map.clip_point(selection.end, Bias::Left);
-                }
+            editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
+                s.move_with(|map, selection| {
+                    if !selection.reversed {
+                        // Head was at the end of the selection, and now is at the start. We need to move the end
+                        // forward by one if possible in order to compensate for this change.
+                        *selection.end.column_mut() = selection.end.column() + 1;
+                        selection.end = map.clip_point(selection.end, Bias::Left);
+                    }
+                });
             });
             editor.insert("", cx);
         });
@@ -59,22 +63,26 @@ pub fn delete(_: &mut Workspace, _: &VisualDelete, cx: &mut ViewContext<Workspac
         vim.switch_mode(Mode::Normal, cx);
         vim.update_active_editor(cx, |editor, cx| {
             editor.set_clip_at_line_ends(false, cx);
-            editor.move_selections(cx, |map, selection| {
-                if !selection.reversed {
-                    // Head was at the end of the selection, and now is at the start. We need to move the end
-                    // forward by one if possible in order to compensate for this change.
-                    *selection.end.column_mut() = selection.end.column() + 1;
-                    selection.end = map.clip_point(selection.end, Bias::Left);
-                }
+            editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
+                s.move_with(|map, selection| {
+                    if !selection.reversed {
+                        // Head was at the end of the selection, and now is at the start. We need to move the end
+                        // forward by one if possible in order to compensate for this change.
+                        *selection.end.column_mut() = selection.end.column() + 1;
+                        selection.end = map.clip_point(selection.end, Bias::Left);
+                    }
+                });
             });
             editor.insert("", cx);
 
             // Fixup cursor position after the deletion
             editor.set_clip_at_line_ends(true, cx);
-            editor.move_selections(cx, |map, selection| {
-                let mut cursor = selection.head();
-                cursor = map.clip_point(cursor, Bias::Left);
-                selection.collapse_to(cursor, selection.goal)
+            editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
+                s.move_with(|map, selection| {
+                    let mut cursor = selection.head();
+                    cursor = map.clip_point(cursor, Bias::Left);
+                    selection.collapse_to(cursor, selection.goal)
+                });
             });
         });
     });

crates/zed/src/zed.rs 🔗

@@ -310,7 +310,7 @@ fn open_config_file(
 mod tests {
     use super::*;
     use assets::Assets;
-    use editor::{DisplayPoint, Editor};
+    use editor::{Autoscroll, DisplayPoint, Editor};
     use gpui::{AssetSource, MutableAppContext, TestAppContext, ViewHandle};
     use project::{Fs, ProjectPath};
     use serde_json::json;
@@ -963,7 +963,9 @@ mod tests {
             .downcast::<Editor>()
             .unwrap();
         editor1.update(cx, |editor, cx| {
-            editor.select_display_ranges(&[DisplayPoint::new(10, 0)..DisplayPoint::new(10, 0)], cx);
+            editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
+                s.select_display_ranges([DisplayPoint::new(10, 0)..DisplayPoint::new(10, 0)])
+            });
         });
         let editor2 = workspace
             .update(cx, |w, cx| w.open_path(file2.clone(), true, cx))
@@ -980,10 +982,9 @@ mod tests {
 
         editor3
             .update(cx, |editor, cx| {
-                editor.select_display_ranges(
-                    &[DisplayPoint::new(12, 0)..DisplayPoint::new(12, 0)],
-                    cx,
-                );
+                editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
+                    s.select_display_ranges([DisplayPoint::new(12, 0)..DisplayPoint::new(12, 0)])
+                });
                 editor.newline(&Default::default(), cx);
                 editor.newline(&Default::default(), cx);
                 editor.move_down(&Default::default(), cx);
@@ -1124,34 +1125,37 @@ mod tests {
         // Modify file to collapse multiple nav history entries into the same location.
         // Ensure we don't visit the same location twice when navigating.
         editor1.update(cx, |editor, cx| {
-            editor.select_display_ranges(&[DisplayPoint::new(15, 0)..DisplayPoint::new(15, 0)], cx)
+            editor.change_selections(None, cx, |s| {
+                s.select_display_ranges([DisplayPoint::new(15, 0)..DisplayPoint::new(15, 0)])
+            })
         });
 
         for _ in 0..5 {
             editor1.update(cx, |editor, cx| {
-                editor
-                    .select_display_ranges(&[DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0)], cx);
+                editor.change_selections(None, cx, |s| {
+                    s.select_display_ranges([DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0)])
+                });
             });
             editor1.update(cx, |editor, cx| {
-                editor.select_display_ranges(
-                    &[DisplayPoint::new(13, 0)..DisplayPoint::new(13, 0)],
-                    cx,
-                )
+                editor.change_selections(None, cx, |s| {
+                    s.select_display_ranges([DisplayPoint::new(13, 0)..DisplayPoint::new(13, 0)])
+                })
             });
         }
 
         editor1.update(cx, |editor, cx| {
             editor.transact(cx, |editor, cx| {
-                editor.select_display_ranges(
-                    &[DisplayPoint::new(2, 0)..DisplayPoint::new(14, 0)],
-                    cx,
-                );
+                editor.change_selections(None, cx, |s| {
+                    s.select_display_ranges([DisplayPoint::new(2, 0)..DisplayPoint::new(14, 0)])
+                });
                 editor.insert("", cx);
             })
         });
 
         editor1.update(cx, |editor, cx| {
-            editor.select_display_ranges(&[DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)], cx)
+            editor.change_selections(None, cx, |s| {
+                s.select_display_ranges([DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)])
+            })
         });
         workspace
             .update(cx, |w, cx| Pane::go_back(w, None, cx))
@@ -1177,7 +1181,7 @@ mod tests {
                 let editor = item.downcast::<Editor>().unwrap();
                 let (selections, scroll_position) = editor.update(cx, |editor, cx| {
                     (
-                        editor.selected_display_ranges(cx),
+                        editor.selections.display_ranges(cx),
                         editor.scroll_position(cx),
                     )
                 });