editor: Fix panics in `CursorPosition::update_position` (#41237)

Lukas Wirth created

Fixes a regression introduced in
https://github.com/zed-industries/zed/pull/39857. As for the exact
reason this causes this issue I am not yet sure will investigate (as per
the todos in code)

Fixes ZED-23R

Release Notes:

- N/A *or* Added/Fixed/Improved ...

Change summary

Cargo.lock                                      |   1 
crates/editor/src/display_map.rs                |   1 
crates/editor/src/display_map/block_map.rs      |   7 
crates/editor/src/edit_prediction_tests.rs      |  28 ++
crates/editor/src/editor.rs                     | 139 +++++++++++-------
crates/editor/src/editor_tests.rs               |   4 
crates/editor/src/highlight_matching_bracket.rs |  88 ++++++-----
crates/editor/src/selections_collection.rs      |  47 ++---
crates/go_to_line/src/cursor_position.rs        |  21 +-
crates/multi_buffer/src/multi_buffer.rs         |  11 -
crates/rope/Cargo.toml                          |   1 
crates/rope/src/chunk.rs                        |   1 
crates/rope/src/rope.rs                         |  30 +--
crates/sum_tree/src/sum_tree.rs                 |   2 
14 files changed, 205 insertions(+), 176 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -14226,7 +14226,6 @@ dependencies = [
  "rand 0.9.2",
  "rayon",
  "regex",
- "smallvec",
  "sum_tree",
  "unicode-segmentation",
  "util",

crates/editor/src/display_map.rs 🔗

@@ -1417,6 +1417,7 @@ impl std::ops::Deref for DisplaySnapshot {
     }
 }
 
+/// A zero-indexed point in a text buffer consisting of a row and column adjusted for inserted blocks.
 #[derive(Copy, Clone, Default, Eq, Ord, PartialOrd, PartialEq)]
 pub struct DisplayPoint(BlockPoint);
 

crates/editor/src/display_map/block_map.rs 🔗

@@ -69,6 +69,8 @@ impl From<CustomBlockId> for ElementId {
     }
 }
 
+/// A zero-indexed point in a text buffer consisting of a row and column
+/// adjusted for inserted blocks, wrapped rows, tabs, folds and inlays.
 #[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
 pub struct BlockPoint(pub Point);
 
@@ -80,11 +82,16 @@ struct WrapRow(u32);
 
 pub type RenderBlock = Arc<dyn Send + Sync + Fn(&mut BlockContext) -> AnyElement>;
 
+/// Where to place a block.
 #[derive(Clone, Debug, Eq, PartialEq)]
 pub enum BlockPlacement<T> {
+    /// Place the block above the given position.
     Above(T),
+    /// Place the block below the given position.
     Below(T),
+    /// Place the block next the given position.
     Near(T),
+    /// Replace the given range of positions with the block.
     Replace(RangeInclusive<T>),
 }
 

crates/editor/src/edit_prediction_tests.rs 🔗

@@ -19,7 +19,9 @@ async fn test_edit_prediction_insert(cx: &mut gpui::TestAppContext) {
     cx.set_state("let absolute_zero_celsius = ˇ;");
 
     propose_edits(&provider, vec![(28..28, "-273.15")], &mut cx);
-    cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx));
+    cx.update_editor(|editor, window, cx| {
+        editor.update_visible_edit_prediction(&editor.display_snapshot(cx), window, cx)
+    });
 
     assert_editor_active_edit_completion(&mut cx, |_, edits| {
         assert_eq!(edits.len(), 1);
@@ -41,7 +43,9 @@ async fn test_edit_prediction_modification(cx: &mut gpui::TestAppContext) {
     cx.set_state("let pi = ˇ\"foo\";");
 
     propose_edits(&provider, vec![(9..14, "3.14159")], &mut cx);
-    cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx));
+    cx.update_editor(|editor, window, cx| {
+        editor.update_visible_edit_prediction(&editor.display_snapshot(cx), window, cx)
+    });
 
     assert_editor_active_edit_completion(&mut cx, |_, edits| {
         assert_eq!(edits.len(), 1);
@@ -76,7 +80,9 @@ async fn test_edit_prediction_jump_button(cx: &mut gpui::TestAppContext) {
         &mut cx,
     );
 
-    cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx));
+    cx.update_editor(|editor, window, cx| {
+        editor.update_visible_edit_prediction(&editor.display_snapshot(cx), window, cx)
+    });
     assert_editor_active_move_completion(&mut cx, |snapshot, move_target| {
         assert_eq!(move_target.to_point(&snapshot), Point::new(4, 3));
     });
@@ -106,7 +112,9 @@ async fn test_edit_prediction_jump_button(cx: &mut gpui::TestAppContext) {
         &mut cx,
     );
 
-    cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx));
+    cx.update_editor(|editor, window, cx| {
+        editor.update_visible_edit_prediction(&editor.display_snapshot(cx), window, cx)
+    });
     assert_editor_active_move_completion(&mut cx, |snapshot, move_target| {
         assert_eq!(move_target.to_point(&snapshot), Point::new(1, 3));
     });
@@ -147,7 +155,9 @@ async fn test_edit_prediction_invalidation_range(cx: &mut gpui::TestAppContext)
         &mut cx,
     );
 
-    cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx));
+    cx.update_editor(|editor, window, cx| {
+        editor.update_visible_edit_prediction(&editor.display_snapshot(cx), window, cx)
+    });
     assert_editor_active_move_completion(&mut cx, |snapshot, move_target| {
         assert_eq!(move_target.to_point(&snapshot), edit_location);
     });
@@ -195,7 +205,9 @@ async fn test_edit_prediction_invalidation_range(cx: &mut gpui::TestAppContext)
         &mut cx,
     );
 
-    cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx));
+    cx.update_editor(|editor, window, cx| {
+        editor.update_visible_edit_prediction(&editor.display_snapshot(cx), window, cx)
+    });
     assert_editor_active_move_completion(&mut cx, |snapshot, move_target| {
         assert_eq!(move_target.to_point(&snapshot), edit_location);
     });
@@ -250,7 +262,9 @@ async fn test_edit_prediction_jump_disabled_for_non_zed_providers(cx: &mut gpui:
         &mut cx,
     );
 
-    cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx));
+    cx.update_editor(|editor, window, cx| {
+        editor.update_visible_edit_prediction(&editor.display_snapshot(cx), window, cx)
+    });
 
     // For non-Zed providers, there should be no move completion (jump functionality disabled)
     cx.editor(|editor, _, _| {

crates/editor/src/editor.rs 🔗

@@ -108,7 +108,6 @@ use gpui::{
     UTF16Selection, UnderlineStyle, UniformListScrollHandle, WeakEntity, WeakFocusHandle, Window,
     div, point, prelude::*, pulsating_between, px, relative, size,
 };
-use highlight_matching_bracket::refresh_matching_bracket_highlights;
 use hover_links::{HoverLink, HoveredLinkState, find_file};
 use hover_popover::{HoverState, hide_hover};
 use indent_guides::ActiveIndentGuidesState;
@@ -163,7 +162,7 @@ use rand::seq::SliceRandom;
 use rpc::{ErrorCode, ErrorExt, proto::PeerId};
 use scroll::{Autoscroll, OngoingScroll, ScrollAnchor, ScrollManager};
 use selections_collection::{
-    MutableSelectionsCollection, SelectionsCollection, resolve_selections,
+    MutableSelectionsCollection, SelectionsCollection, resolve_selections_wrapping_blocks,
 };
 use serde::{Deserialize, Serialize};
 use settings::{GitGutterSetting, Settings, SettingsLocation, SettingsStore, update_settings_file};
@@ -2821,7 +2820,7 @@ impl Editor {
         self.edit_prediction_provider = provider.map(|provider| RegisteredEditPredictionProvider {
             _subscription: cx.observe_in(&provider, window, |this, _, window, cx| {
                 if this.focus_handle.is_focused(window) {
-                    this.update_visible_edit_prediction(window, cx);
+                    this.update_visible_edit_prediction(&this.display_snapshot(cx), window, cx);
                 }
             }),
             provider: Arc::new(provider),
@@ -2910,7 +2909,7 @@ impl Editor {
         if hidden != self.edit_predictions_hidden_for_vim_mode {
             self.edit_predictions_hidden_for_vim_mode = hidden;
             if hidden {
-                self.update_visible_edit_prediction(window, cx);
+                self.update_visible_edit_prediction(&self.display_snapshot(cx), window, cx);
             } else {
                 self.refresh_edit_prediction(true, false, window, cx);
             }
@@ -3135,9 +3134,9 @@ impl Editor {
             self.refresh_document_highlights(cx);
             refresh_linked_ranges(self, window, cx);
 
-            self.refresh_selected_text_highlights(false, window, cx);
-            refresh_matching_bracket_highlights(self, cx);
-            self.update_visible_edit_prediction(window, cx);
+            self.refresh_selected_text_highlights(false, &display_map, window, cx);
+            self.refresh_matching_bracket_highlights(&display_map, cx);
+            self.update_visible_edit_prediction(&display_map, window, cx);
             self.edit_prediction_requires_modifier_in_indent_conflict = true;
             self.inline_blame_popover.take();
             if self.git_blame_inline_enabled {
@@ -4320,16 +4319,17 @@ impl Editor {
             let new_anchor_selections = new_selections.iter().map(|e| &e.0);
             let new_selection_deltas = new_selections.iter().map(|e| e.1);
             let map = this.display_map.update(cx, |map, cx| map.snapshot(cx));
-            let new_selections = resolve_selections::<usize, _>(new_anchor_selections, &map)
-                .zip(new_selection_deltas)
-                .map(|(selection, delta)| Selection {
-                    id: selection.id,
-                    start: selection.start + delta,
-                    end: selection.end + delta,
-                    reversed: selection.reversed,
-                    goal: SelectionGoal::None,
-                })
-                .collect::<Vec<_>>();
+            let new_selections =
+                resolve_selections_wrapping_blocks::<usize, _>(new_anchor_selections, &map)
+                    .zip(new_selection_deltas)
+                    .map(|(selection, delta)| Selection {
+                        id: selection.id,
+                        start: selection.start + delta,
+                        end: selection.end + delta,
+                        reversed: selection.reversed,
+                        goal: SelectionGoal::None,
+                    })
+                    .collect::<Vec<_>>();
 
             let mut i = 0;
             for (position, delta, selection_id, pair) in new_autoclose_regions {
@@ -5651,7 +5651,11 @@ impl Editor {
 
                         crate::hover_popover::hide_hover(editor, cx);
                         if editor.show_edit_predictions_in_menu() {
-                            editor.update_visible_edit_prediction(window, cx);
+                            editor.update_visible_edit_prediction(
+                                &editor.display_snapshot(cx),
+                                window,
+                                cx,
+                            );
                         } else {
                             editor.discard_edit_prediction(false, cx);
                         }
@@ -5666,7 +5670,11 @@ impl Editor {
                         // If it was already hidden and we don't show edit predictions in the menu,
                         // we should also show the edit prediction when available.
                         if was_hidden && editor.show_edit_predictions_in_menu() {
-                            editor.update_visible_edit_prediction(window, cx);
+                            editor.update_visible_edit_prediction(
+                                &editor.display_snapshot(cx),
+                                window,
+                                cx,
+                            );
                         }
                     }
                 })
@@ -6730,6 +6738,7 @@ impl Editor {
 
     fn prepare_highlight_query_from_selection(
         &mut self,
+        window: &Window,
         cx: &mut Context<Editor>,
     ) -> Option<(String, Range<Anchor>)> {
         if matches!(self.mode, EditorMode::SingleLine) {
@@ -6741,24 +6750,23 @@ impl Editor {
         if self.selections.count() != 1 || self.selections.line_mode() {
             return None;
         }
-        let selection = self.selections.newest_anchor();
-        let multi_buffer_snapshot = self.buffer().read(cx).snapshot(cx);
-        let selection_point_range = selection.start.to_point(&multi_buffer_snapshot)
-            ..selection.end.to_point(&multi_buffer_snapshot);
+        let snapshot = self.snapshot(window, cx);
+        let selection = self.selections.newest::<Point>(&snapshot);
         // If the selection spans multiple rows OR it is empty
-        if selection_point_range.start.row != selection_point_range.end.row
-            || selection_point_range.start.column == selection_point_range.end.column
+        if selection.start.row != selection.end.row
+            || selection.start.column == selection.end.column
         {
             return None;
         }
-
-        let query = multi_buffer_snapshot
-            .text_for_range(selection.range())
+        let selection_anchor_range = selection.range().to_anchors(snapshot.buffer_snapshot());
+        let query = snapshot
+            .buffer_snapshot()
+            .text_for_range(selection_anchor_range.clone())
             .collect::<String>();
         if query.trim().is_empty() {
             return None;
         }
-        Some((query, selection.range()))
+        Some((query, selection_anchor_range))
     }
 
     fn update_selection_occurrence_highlights(
@@ -6834,32 +6842,36 @@ impl Editor {
         })
     }
 
-    fn refresh_single_line_folds(&mut self, window: &mut Window, cx: &mut Context<Editor>) {
+    fn refresh_single_line_folds(
+        &mut self,
+        display_snapshot: &DisplaySnapshot,
+        cx: &mut Context<Editor>,
+    ) {
         struct NewlineFold;
         let type_id = std::any::TypeId::of::<NewlineFold>();
         if !self.mode.is_single_line() {
             return;
         }
-        let snapshot = self.snapshot(window, cx);
-        if snapshot.buffer_snapshot().max_point().row == 0 {
+        let display_snapshot = display_snapshot.clone();
+        if display_snapshot.buffer_snapshot().max_point().row == 0 {
             return;
         }
         let task = cx.background_spawn(async move {
-            let new_newlines = snapshot
+            let new_newlines = display_snapshot
                 .buffer_chars_at(0)
                 .filter_map(|(c, i)| {
                     if c == '\n' {
                         Some(
-                            snapshot.buffer_snapshot().anchor_after(i)
-                                ..snapshot.buffer_snapshot().anchor_before(i + 1),
+                            display_snapshot.buffer_snapshot().anchor_after(i)
+                                ..display_snapshot.buffer_snapshot().anchor_before(i + 1),
                         )
                     } else {
                         None
                     }
                 })
                 .collect::<Vec<_>>();
-            let existing_newlines = snapshot
-                .folds_in_range(0..snapshot.buffer_snapshot().len())
+            let existing_newlines = display_snapshot
+                .folds_in_range(0..display_snapshot.buffer_snapshot().len())
                 .filter_map(|fold| {
                     if fold.placeholder.type_tag == Some(type_id) {
                         Some(fold.range.start..fold.range.end)
@@ -6908,17 +6920,19 @@ impl Editor {
     fn refresh_selected_text_highlights(
         &mut self,
         on_buffer_edit: bool,
+        display_snapshot: &DisplaySnapshot,
         window: &mut Window,
         cx: &mut Context<Editor>,
     ) {
-        let Some((query_text, query_range)) = self.prepare_highlight_query_from_selection(cx)
+        let Some((query_text, query_range)) =
+            self.prepare_highlight_query_from_selection(window, cx)
         else {
             self.clear_background_highlights::<SelectedTextHighlight>(cx);
             self.quick_selection_highlight_task.take();
             self.debounced_selection_highlight_task.take();
             return;
         };
-        let multi_buffer_snapshot = self.buffer().read(cx).snapshot(cx);
+        let multi_buffer_snapshot = display_snapshot.buffer_snapshot();
         if on_buffer_edit
             || self
                 .quick_selection_highlight_task
@@ -6996,7 +7010,7 @@ impl Editor {
             return None;
         }
 
-        self.update_visible_edit_prediction(window, cx);
+        self.update_visible_edit_prediction(&self.display_snapshot(cx), window, cx);
 
         if !user_requested
             && (!self.should_show_edit_predictions()
@@ -7158,7 +7172,7 @@ impl Editor {
         }
 
         provider.cycle(buffer, cursor_buffer_position, direction, cx);
-        self.update_visible_edit_prediction(window, cx);
+        self.update_visible_edit_prediction(&self.display_snapshot(cx), window, cx);
 
         Some(())
     }
@@ -7174,7 +7188,7 @@ impl Editor {
             return;
         }
 
-        self.update_visible_edit_prediction(window, cx);
+        self.update_visible_edit_prediction(&self.display_snapshot(cx), window, cx);
     }
 
     pub fn display_cursor_names(
@@ -7313,8 +7327,13 @@ impl Editor {
                 // Store the transaction ID and selections before applying the edit
                 let transaction_id_prev = self.buffer.read(cx).last_transaction_id(cx);
 
-                let snapshot = self.buffer.read(cx).snapshot(cx);
-                let last_edit_end = edits.last().unwrap().0.end.bias_right(&snapshot);
+                let snapshot = self.display_snapshot(cx);
+                let last_edit_end = edits
+                    .last()
+                    .unwrap()
+                    .0
+                    .end
+                    .bias_right(snapshot.buffer_snapshot());
 
                 self.buffer.update(cx, |buffer, cx| {
                     buffer.edit(edits.iter().cloned(), None, cx)
@@ -7333,7 +7352,7 @@ impl Editor {
                     }
                 }
 
-                self.update_visible_edit_prediction(window, cx);
+                self.update_visible_edit_prediction(&snapshot, window, cx);
                 if self.active_edit_prediction.is_none() {
                     self.refresh_edit_prediction(true, true, window, cx);
                 }
@@ -7666,7 +7685,7 @@ impl Editor {
                     since: Instant::now(),
                 };
 
-                self.update_visible_edit_prediction(window, cx);
+                self.update_visible_edit_prediction(&self.display_snapshot(cx), window, cx);
                 cx.notify();
             }
         } else if let EditPredictionPreview::Active {
@@ -7689,13 +7708,14 @@ impl Editor {
                 released_too_fast: since.elapsed() < Duration::from_millis(200),
             };
             self.clear_row_highlights::<EditPredictionPreview>();
-            self.update_visible_edit_prediction(window, cx);
+            self.update_visible_edit_prediction(&self.display_snapshot(cx), window, cx);
             cx.notify();
         }
     }
 
     fn update_visible_edit_prediction(
         &mut self,
+        display_snapshot: &DisplaySnapshot,
         _window: &mut Window,
         cx: &mut Context<Self>,
     ) -> Option<()> {
@@ -7710,7 +7730,7 @@ impl Editor {
 
         let selection = self.selections.newest_anchor();
         let cursor = selection.head();
-        let multibuffer = self.buffer.read(cx).snapshot(cx);
+        let multibuffer = display_snapshot.buffer_snapshot();
         let offset_selection = selection.map(|endpoint| endpoint.to_offset(&multibuffer));
         let excerpt_id = cursor.excerpt_id;
 
@@ -9542,7 +9562,7 @@ impl Editor {
         self.completion_tasks.clear();
         let context_menu = self.context_menu.borrow_mut().take();
         self.stale_edit_prediction_in_menu.take();
-        self.update_visible_edit_prediction(window, cx);
+        self.update_visible_edit_prediction(&self.display_snapshot(cx), window, cx);
         if let Some(CodeContextMenu::Completions(_)) = &context_menu
             && let Some(completion_provider) = &self.completion_provider
         {
@@ -17466,13 +17486,17 @@ impl Editor {
         window.show_character_palette();
     }
 
-    fn refresh_active_diagnostics(&mut self, cx: &mut Context<Editor>) {
+    fn refresh_active_diagnostics(
+        &mut self,
+        display_snapshot: &DisplaySnapshot,
+        cx: &mut Context<Editor>,
+    ) {
         if !self.diagnostics_enabled() {
             return;
         }
 
         if let ActiveDiagnostic::Group(active_diagnostics) = &mut self.active_diagnostics {
-            let buffer = self.buffer.read(cx).snapshot(cx);
+            let buffer = display_snapshot.buffer_snapshot();
             let primary_range_start = active_diagnostics.active_range.start.to_offset(&buffer);
             let primary_range_end = active_diagnostics.active_range.end.to_offset(&buffer);
             let is_valid = buffer
@@ -20762,15 +20786,16 @@ impl Editor {
     ) {
         match event {
             multi_buffer::Event::Edited { edited_buffer } => {
+                let display_snapshot = self.display_snapshot(cx);
                 self.scrollbar_marker_state.dirty = true;
                 self.active_indent_guides_state.dirty = true;
-                self.refresh_active_diagnostics(cx);
+                self.refresh_active_diagnostics(&display_snapshot, cx);
                 self.refresh_code_actions(window, cx);
-                self.refresh_selected_text_highlights(true, window, cx);
-                self.refresh_single_line_folds(window, cx);
-                refresh_matching_bracket_highlights(self, cx);
+                self.refresh_selected_text_highlights(true, &display_snapshot, window, cx);
+                self.refresh_single_line_folds(&display_snapshot, cx);
+                self.refresh_matching_bracket_highlights(&display_snapshot, cx);
                 if self.has_active_edit_prediction() {
-                    self.update_visible_edit_prediction(window, cx);
+                    self.update_visible_edit_prediction(&display_snapshot, window, cx);
                 }
 
                 if let Some(buffer) = edited_buffer {
@@ -20892,7 +20917,7 @@ impl Editor {
         if !self.diagnostics_enabled() {
             return;
         }
-        self.refresh_active_diagnostics(cx);
+        self.refresh_active_diagnostics(&self.display_snapshot(cx), cx);
         self.refresh_inline_diagnostics(true, window, cx);
         self.scrollbar_marker_state.dirty = true;
         cx.notify();

crates/editor/src/editor_tests.rs 🔗

@@ -8534,7 +8534,9 @@ async fn test_undo_edit_prediction_scrolls_to_edit_pos(cx: &mut TestAppContext)
         })
     });
 
-    cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx));
+    cx.update_editor(|editor, window, cx| {
+        editor.update_visible_edit_prediction(&editor.display_snapshot(cx), window, cx)
+    });
     cx.update_editor(|editor, window, cx| {
         editor.accept_edit_prediction(&crate::AcceptEditPrediction, window, cx)
     });

crates/editor/src/highlight_matching_bracket.rs 🔗

@@ -1,57 +1,59 @@
-use crate::{Editor, RangeToAnchorExt};
+use crate::{Editor, RangeToAnchorExt, display_map::DisplaySnapshot};
 use gpui::{Context, HighlightStyle};
 use language::CursorShape;
-use multi_buffer::ToOffset;
 use theme::ActiveTheme;
 
 enum MatchingBracketHighlight {}
 
-pub fn refresh_matching_bracket_highlights(editor: &mut Editor, cx: &mut Context<Editor>) {
-    editor.clear_highlights::<MatchingBracketHighlight>(cx);
+impl Editor {
+    pub fn refresh_matching_bracket_highlights(
+        &mut self,
+        snapshot: &DisplaySnapshot,
+        cx: &mut Context<Editor>,
+    ) {
+        self.clear_highlights::<MatchingBracketHighlight>(cx);
 
-    let buffer_snapshot = editor.buffer.read(cx).snapshot(cx);
-    let newest_selection = editor
-        .selections
-        .newest_anchor()
-        .map(|anchor| anchor.to_offset(&buffer_snapshot));
-    // Don't highlight brackets if the selection isn't empty
-    if !newest_selection.is_empty() {
-        return;
-    }
+        let buffer_snapshot = snapshot.buffer_snapshot();
+        let newest_selection = self.selections.newest::<usize>(snapshot);
+        // Don't highlight brackets if the selection isn't empty
+        if !newest_selection.is_empty() {
+            return;
+        }
 
-    let head = newest_selection.head();
-    if head > buffer_snapshot.len() {
-        log::error!("bug: cursor offset is out of range while refreshing bracket highlights");
-        return;
-    }
+        let head = newest_selection.head();
+        if head > buffer_snapshot.len() {
+            log::error!("bug: cursor offset is out of range while refreshing bracket highlights");
+            return;
+        }
 
-    let mut tail = head;
-    if (editor.cursor_shape == CursorShape::Block || editor.cursor_shape == CursorShape::Hollow)
-        && head < buffer_snapshot.len()
-    {
-        if let Some(tail_ch) = buffer_snapshot.chars_at(tail).next() {
-            tail += tail_ch.len_utf8();
+        let mut tail = head;
+        if (self.cursor_shape == CursorShape::Block || self.cursor_shape == CursorShape::Hollow)
+            && head < buffer_snapshot.len()
+        {
+            if let Some(tail_ch) = buffer_snapshot.chars_at(tail).next() {
+                tail += tail_ch.len_utf8();
+            }
         }
-    }
 
-    if let Some((opening_range, closing_range)) =
-        buffer_snapshot.innermost_enclosing_bracket_ranges(head..tail, None)
-    {
-        editor.highlight_text::<MatchingBracketHighlight>(
-            vec![
-                opening_range.to_anchors(&buffer_snapshot),
-                closing_range.to_anchors(&buffer_snapshot),
-            ],
-            HighlightStyle {
-                background_color: Some(
-                    cx.theme()
-                        .colors()
-                        .editor_document_highlight_bracket_background,
-                ),
-                ..Default::default()
-            },
-            cx,
-        )
+        if let Some((opening_range, closing_range)) =
+            buffer_snapshot.innermost_enclosing_bracket_ranges(head..tail, None)
+        {
+            self.highlight_text::<MatchingBracketHighlight>(
+                vec![
+                    opening_range.to_anchors(&buffer_snapshot),
+                    closing_range.to_anchors(&buffer_snapshot),
+                ],
+                HighlightStyle {
+                    background_color: Some(
+                        cx.theme()
+                            .colors()
+                            .editor_document_highlight_bracket_background,
+                    ),
+                    ..Default::default()
+                },
+                cx,
+            )
+        }
     }
 }
 

crates/editor/src/selections_collection.rs 🔗

@@ -131,7 +131,7 @@ impl SelectionsCollection {
         &self,
         snapshot: &DisplaySnapshot,
     ) -> Option<Selection<D>> {
-        resolve_selections(self.pending_anchor(), &snapshot).next()
+        resolve_selections_wrapping_blocks(self.pending_anchor(), &snapshot).next()
     }
 
     pub(crate) fn pending_mode(&self) -> Option<SelectMode> {
@@ -144,7 +144,8 @@ impl SelectionsCollection {
     {
         let disjoint_anchors = &self.disjoint;
         let mut disjoint =
-            resolve_selections::<D, _>(disjoint_anchors.iter(), &snapshot).peekable();
+            resolve_selections_wrapping_blocks::<D, _>(disjoint_anchors.iter(), &snapshot)
+                .peekable();
         let mut pending_opt = self.pending::<D>(&snapshot);
         iter::from_fn(move || {
             if let Some(pending) = pending_opt.as_mut() {
@@ -185,27 +186,6 @@ impl SelectionsCollection {
         selections
     }
 
-    /// Returns all of the selections, adjusted to take into account the selection line_mode. Uses a provided snapshot to resolve selections.
-    pub fn all_adjusted_with_snapshot(
-        &self,
-        snapshot: &MultiBufferSnapshot,
-    ) -> Vec<Selection<Point>> {
-        let mut selections = self
-            .disjoint
-            .iter()
-            .chain(self.pending_anchor())
-            .map(|anchor| anchor.map(|anchor| anchor.to_point(&snapshot)))
-            .collect::<Vec<_>>();
-        if self.line_mode {
-            for selection in &mut selections {
-                let new_range = snapshot.expand_to_line(selection.range());
-                selection.start = new_range.start;
-                selection.end = new_range.end;
-            }
-        }
-        selections
-    }
-
     /// Returns the newest selection, adjusted to take into account the selection line_mode
     pub fn newest_adjusted(&self, snapshot: &DisplaySnapshot) -> Selection<Point> {
         let mut selection = self.newest::<Point>(&snapshot);
@@ -259,7 +239,7 @@ impl SelectionsCollection {
             Ok(ix) => ix + 1,
             Err(ix) => ix,
         };
-        resolve_selections(&self.disjoint[start_ix..end_ix], snapshot).collect()
+        resolve_selections_wrapping_blocks(&self.disjoint[start_ix..end_ix], snapshot).collect()
     }
 
     pub fn all_display(&self, snapshot: &DisplaySnapshot) -> Vec<Selection<DisplayPoint>> {
@@ -305,7 +285,7 @@ impl SelectionsCollection {
         &self,
         snapshot: &DisplaySnapshot,
     ) -> Selection<D> {
-        resolve_selections([self.newest_anchor()], &snapshot)
+        resolve_selections_wrapping_blocks([self.newest_anchor()], &snapshot)
             .next()
             .unwrap()
     }
@@ -328,7 +308,7 @@ impl SelectionsCollection {
         &self,
         snapshot: &DisplaySnapshot,
     ) -> Selection<D> {
-        resolve_selections([self.oldest_anchor()], &snapshot)
+        resolve_selections_wrapping_blocks([self.oldest_anchor()], &snapshot)
             .next()
             .unwrap()
     }
@@ -658,7 +638,7 @@ impl<'a> MutableSelectionsCollection<'a> {
     pub fn select_anchors(&mut self, selections: Vec<Selection<Anchor>>) {
         let map = self.display_map();
         let resolved_selections =
-            resolve_selections::<usize, _>(&selections, &map).collect::<Vec<_>>();
+            resolve_selections_wrapping_blocks::<usize, _>(&selections, &map).collect::<Vec<_>>();
         self.select(resolved_selections);
     }
 
@@ -940,7 +920,8 @@ impl<'a> MutableSelectionsCollection<'a> {
 
         if !adjusted_disjoint.is_empty() {
             let map = self.display_map();
-            let resolved_selections = resolve_selections(adjusted_disjoint.iter(), &map).collect();
+            let resolved_selections =
+                resolve_selections_wrapping_blocks(adjusted_disjoint.iter(), &map).collect();
             self.select::<usize>(resolved_selections);
         }
 
@@ -1025,6 +1006,7 @@ fn resolve_selections_point<'a>(
 }
 
 /// Panics if passed selections are not in order
+/// Resolves the anchors to display positions
 fn resolve_selections_display<'a>(
     selections: impl 'a + IntoIterator<Item = &'a Selection<Anchor>>,
     map: &'a DisplaySnapshot,
@@ -1056,8 +1038,13 @@ fn resolve_selections_display<'a>(
     coalesce_selections(selections)
 }
 
+/// Resolves the passed in anchors to [`TextDimension`]s `D`
+/// wrapping around blocks inbetween.
+///
+/// # Panics
+///
 /// Panics if passed selections are not in order
-pub(crate) fn resolve_selections<'a, D, I>(
+pub(crate) fn resolve_selections_wrapping_blocks<'a, D, I>(
     selections: I,
     map: &'a DisplaySnapshot,
 ) -> impl 'a + Iterator<Item = Selection<D>>
@@ -1065,6 +1052,8 @@ where
     D: TextDimension + Ord + Sub<D, Output = D>,
     I: 'a + IntoIterator<Item = &'a Selection<Anchor>>,
 {
+    // Transforms `Anchor -> DisplayPoint -> Point -> DisplayPoint -> D`
+    // todo(lw): We should be able to short circuit the `Anchor -> DisplayPoint -> Point` to `Anchor -> Point`
     let (to_convert, selections) = resolve_selections_display(selections, map).tee();
     let mut converted_endpoints =
         map.buffer_snapshot()

crates/go_to_line/src/cursor_position.rs 🔗

@@ -111,15 +111,14 @@ impl CursorPosition {
                             }
                             editor::EditorMode::Full { .. } => {
                                 let mut last_selection = None::<Selection<Point>>;
-                                let snapshot = editor.buffer().read(cx).snapshot(cx);
-                                if snapshot.excerpts().count() > 0 {
-                                    for selection in
-                                        editor.selections.all_adjusted_with_snapshot(&snapshot)
-                                    {
+                                let snapshot = editor.display_snapshot(cx);
+                                if snapshot.buffer_snapshot().excerpts().count() > 0 {
+                                    for selection in editor.selections.all_adjusted(&snapshot) {
                                         let selection_summary = snapshot
+                                            .buffer_snapshot()
                                             .text_summary_for_range::<text::TextSummary, _>(
-                                                selection.start..selection.end,
-                                            );
+                                            selection.start..selection.end,
+                                        );
                                         cursor_position.selected_count.characters +=
                                             selection_summary.chars;
                                         if selection.end != selection.start {
@@ -136,8 +135,12 @@ impl CursorPosition {
                                         }
                                     }
                                 }
-                                cursor_position.position = last_selection
-                                    .map(|s| UserCaretPosition::at_selection_end(&s, &snapshot));
+                                cursor_position.position = last_selection.map(|s| {
+                                    UserCaretPosition::at_selection_end(
+                                        &s,
+                                        snapshot.buffer_snapshot(),
+                                    )
+                                });
                                 cursor_position.context = Some(editor.focus_handle(cx));
                             }
                         }

crates/multi_buffer/src/multi_buffer.rs 🔗

@@ -5742,17 +5742,6 @@ impl MultiBufferSnapshot {
             debug_ranges.insert(key, text_ranges, format!("{value:?}").into())
         });
     }
-
-    // used by line_mode selections and tries to match vim behavior
-    pub fn expand_to_line(&self, range: Range<Point>) -> Range<Point> {
-        let new_start = MultiBufferPoint::new(range.start.row, 0);
-        let new_end = if range.end.column > 0 {
-            MultiBufferPoint::new(range.end.row, self.line_len(MultiBufferRow(range.end.row)))
-        } else {
-            range.end
-        };
-        new_start..new_end
-    }
 }
 
 #[cfg(any(test, feature = "test-support"))]

crates/rope/Cargo.toml 🔗

@@ -16,7 +16,6 @@ arrayvec = "0.7.1"
 log.workspace = true
 rayon.workspace = true
 regex.workspace = true
-smallvec.workspace = true
 sum_tree.workspace = true
 unicode-segmentation.workspace = true
 util.workspace = true

crates/rope/src/chunk.rs 🔗

@@ -131,6 +131,7 @@ impl Chunk {
 
         #[cold]
         #[inline(never)]
+        #[track_caller]
         fn panic_char_boundary(chunk: &Chunk, offset: usize) {
             if offset > chunk.text.len() {
                 panic!(

crates/rope/src/rope.rs 🔗

@@ -4,9 +4,9 @@ mod point;
 mod point_utf16;
 mod unclipped;
 
+use arrayvec::ArrayVec;
 use rayon::iter::{IntoParallelIterator, ParallelIterator as _};
 use regex::Regex;
-use smallvec::SmallVec;
 use std::{
     borrow::Cow,
     cmp, fmt, io, mem,
@@ -283,10 +283,19 @@ impl Rope {
             (),
         );
 
-        if text.len() > 2048 {
+        #[cfg(not(test))]
+        const NUM_CHUNKS: usize = 16;
+        #[cfg(test)]
+        const NUM_CHUNKS: usize = 4;
+
+        // We accommodate for NUM_CHUNKS chunks of size MAX_BASE
+        // but given the chunk boundary can land within a character
+        // we need to accommodate for the worst case where every chunk gets cut short by up to 4 bytes
+        if text.len() > NUM_CHUNKS * chunk::MAX_BASE - NUM_CHUNKS * 4 {
             return self.push_large(text);
         }
-        let mut new_chunks = SmallVec::<[_; 16]>::new();
+        // 16 is enough as otherwise we will hit the branch above
+        let mut new_chunks = ArrayVec::<_, NUM_CHUNKS>::new();
 
         while !text.is_empty() {
             let mut split_ix = cmp::min(chunk::MAX_BASE, text.len());
@@ -297,19 +306,8 @@ impl Rope {
             new_chunks.push(chunk);
             text = remainder;
         }
-
-        #[cfg(test)]
-        const PARALLEL_THRESHOLD: usize = 4;
-        #[cfg(not(test))]
-        const PARALLEL_THRESHOLD: usize = 4 * (2 * sum_tree::TREE_BASE);
-
-        if new_chunks.len() >= PARALLEL_THRESHOLD {
-            self.chunks
-                .par_extend(new_chunks.into_vec().into_par_iter().map(Chunk::new), ());
-        } else {
-            self.chunks
-                .extend(new_chunks.into_iter().map(Chunk::new), ());
-        }
+        self.chunks
+            .extend(new_chunks.into_iter().map(Chunk::new), ());
 
         self.check_invariants();
     }

crates/sum_tree/src/sum_tree.rs 🔗

@@ -3,7 +3,7 @@ mod tree_map;
 
 use arrayvec::ArrayVec;
 pub use cursor::{Cursor, FilterCursor, Iter};
-use rayon::prelude::*;
+use rayon::iter::{IndexedParallelIterator, IntoParallelIterator, ParallelIterator as _};
 use std::marker::PhantomData;
 use std::mem;
 use std::{cmp::Ordering, fmt, iter::FromIterator, sync::Arc};