editor: Clean up tab_map (#49606)

Lukas Wirth created

Release Notes:

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

Change summary

crates/buffer_diff/src/buffer_diff.rs      |   2 
crates/editor/src/display_map/inlay_map.rs |   9 
crates/editor/src/display_map/tab_map.rs   | 655 ++++++++++++-----------
crates/editor/src/editor.rs                |  18 
4 files changed, 352 insertions(+), 332 deletions(-)

Detailed changes

crates/buffer_diff/src/buffer_diff.rs 🔗

@@ -1721,7 +1721,7 @@ impl BufferDiff {
             if let Some(language_registry) = language_registry {
                 base_text.set_language_registry(language_registry);
             }
-            base_text.set_language(language, cx);
+            base_text.set_language_async(language, cx);
             base_text.parsing_idle()
         });
         cx.spawn(async move |this, cx| {

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

@@ -17,7 +17,7 @@ use multi_buffer::{
 };
 use project::InlayId;
 use std::{
-    cmp,
+    cmp, iter,
     ops::{Add, AddAssign, Range, Sub, SubAssign},
     sync::Arc,
 };
@@ -546,8 +546,11 @@ impl InlayMap {
     pub fn new(buffer: MultiBufferSnapshot) -> (Self, InlaySnapshot) {
         let version = 0;
         let snapshot = InlaySnapshot {
-            buffer: buffer.clone(),
-            transforms: SumTree::from_iter(Some(Transform::Isomorphic(buffer.text_summary())), ()),
+            transforms: SumTree::from_iter(
+                iter::once(Transform::Isomorphic(buffer.text_summary())),
+                (),
+            ),
+            buffer,
             version,
         };
 

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

@@ -44,121 +44,146 @@ impl TabMap {
         mut fold_edits: Vec<FoldEdit>,
         tab_size: NonZeroU32,
     ) -> (TabSnapshot, Vec<TabEdit>) {
-        let old_snapshot = &mut self.0;
-        let mut new_snapshot = TabSnapshot {
-            fold_snapshot,
-            tab_size: tab_size.min(MAX_TABS),
-            max_expansion_column: old_snapshot.max_expansion_column,
-            version: old_snapshot.version,
-        };
-
-        if old_snapshot.fold_snapshot.version != new_snapshot.fold_snapshot.version {
-            new_snapshot.version += 1;
+        let tab_size = tab_size.min(MAX_TABS);
+
+        if self.0.tab_size != tab_size {
+            let old_max_point = self.0.max_point();
+            self.0.version += 1;
+            self.0.fold_snapshot = fold_snapshot;
+            self.0.tab_size = tab_size;
+            return (
+                self.0.clone(),
+                vec![TabEdit {
+                    old: TabPoint::zero()..old_max_point,
+                    new: TabPoint::zero()..self.0.max_point(),
+                }],
+            );
         }
 
-        let tab_edits = if old_snapshot.tab_size == new_snapshot.tab_size {
-            // Expand each edit to include the next tab on the same line as the edit,
-            // and any subsequent tabs on that line that moved across the tab expansion
-            // boundary.
-            for fold_edit in &mut fold_edits {
-                let old_end = fold_edit.old.end.to_point(&old_snapshot.fold_snapshot);
-                let old_end_row_successor_offset = cmp::min(
-                    FoldPoint::new(old_end.row() + 1, 0),
-                    old_snapshot.fold_snapshot.max_point(),
-                )
-                .to_offset(&old_snapshot.fold_snapshot);
-                let new_end = fold_edit.new.end.to_point(&new_snapshot.fold_snapshot);
+        let old_snapshot = &mut self.0;
+        let mut new_version = old_snapshot.version;
+        if old_snapshot.fold_snapshot.version != fold_snapshot.version {
+            new_version += 1;
+        }
 
-                let mut offset_from_edit = 0;
-                let mut first_tab_offset = None;
-                let mut last_tab_with_changed_expansion_offset = None;
-                'outer: for chunk in old_snapshot.fold_snapshot.chunks(
-                    fold_edit.old.end..old_end_row_successor_offset,
-                    false,
-                    Highlights::default(),
-                ) {
-                    let mut remaining_tabs = chunk.tabs;
-                    while remaining_tabs != 0 {
-                        let ix = remaining_tabs.trailing_zeros();
-                        let offset_from_edit = offset_from_edit + ix;
-                        if first_tab_offset.is_none() {
-                            first_tab_offset = Some(offset_from_edit);
-                        }
-
-                        let old_column = old_end.column() + offset_from_edit;
-                        let new_column = new_end.column() + offset_from_edit;
-                        let was_expanded = old_column < old_snapshot.max_expansion_column;
-                        let is_expanded = new_column < new_snapshot.max_expansion_column;
-                        if was_expanded != is_expanded {
-                            last_tab_with_changed_expansion_offset = Some(offset_from_edit);
-                        } else if !was_expanded && !is_expanded {
-                            break 'outer;
-                        }
-
-                        remaining_tabs &= remaining_tabs - 1;
+        if fold_edits.is_empty() {
+            old_snapshot.version = new_version;
+            old_snapshot.fold_snapshot = fold_snapshot;
+            old_snapshot.tab_size = tab_size;
+            return (old_snapshot.clone(), vec![]);
+        }
+        // Expand each edit to include the next tab on the same line as the edit,
+        // and any subsequent tabs on that line that moved across the tab expansion
+        // boundary.
+        //
+        // This is necessary because a tab's display width depends on its column
+        // position: it expands to fill up to the next tab stop. When an edit
+        // shifts text on a line, any tab character after the edit may now render
+        // at a different width even though the tab byte itself wasn't touched.
+        // Additionally, tabs beyond `max_expansion_column` are rendered as a
+        // single space instead of expanding to the next tab stop. An edit that
+        // shifts a tab across that boundary changes its display width, so the
+        // edit must cover it. We scan forward from the edit end to the end of
+        // the line, extending the edit to include the first subsequent tab (whose
+        // rendered width may have changed) and the last tab that crossed the
+        // expansion boundary (transitioning between expanded and non-expanded).
+        for fold_edit in &mut fold_edits {
+            let old_end = fold_edit.old.end.to_point(&old_snapshot.fold_snapshot);
+            let old_end_row_successor_offset = cmp::min(
+                FoldPoint::new(old_end.row() + 1, 0),
+                old_snapshot.fold_snapshot.max_point(),
+            )
+            .to_offset(&old_snapshot.fold_snapshot);
+            let new_end = fold_edit.new.end.to_point(&fold_snapshot);
+
+            let mut offset_from_edit = 0;
+            let mut first_tab_offset = None;
+            let mut last_tab_with_changed_expansion_offset = None;
+            'outer: for chunk in old_snapshot.fold_snapshot.chunks(
+                fold_edit.old.end..old_end_row_successor_offset,
+                false,
+                Highlights::default(),
+            ) {
+                let mut remaining_tabs = chunk.tabs;
+                while remaining_tabs != 0 {
+                    let ix = remaining_tabs.trailing_zeros();
+                    let offset_from_edit = offset_from_edit + ix;
+                    if first_tab_offset.is_none() {
+                        first_tab_offset = Some(offset_from_edit);
                     }
 
-                    offset_from_edit += chunk.text.len() as u32;
-                    if old_end.column() + offset_from_edit >= old_snapshot.max_expansion_column
-                        && new_end.column() + offset_from_edit >= new_snapshot.max_expansion_column
-                    {
-                        break;
+                    let old_column = old_end.column() + offset_from_edit;
+                    let new_column = new_end.column() + offset_from_edit;
+                    let was_expanded = old_column < old_snapshot.max_expansion_column;
+                    let is_expanded = new_column < old_snapshot.max_expansion_column;
+                    if was_expanded != is_expanded {
+                        last_tab_with_changed_expansion_offset = Some(offset_from_edit);
+                    } else if !was_expanded && !is_expanded {
+                        break 'outer;
                     }
+
+                    remaining_tabs &= remaining_tabs - 1;
                 }
 
-                if let Some(offset) = last_tab_with_changed_expansion_offset.or(first_tab_offset) {
-                    fold_edit.old.end.0 += offset as usize + 1;
-                    fold_edit.new.end.0 += offset as usize + 1;
+                offset_from_edit += chunk.text.len() as u32;
+                if old_end.column() + offset_from_edit >= old_snapshot.max_expansion_column
+                    && new_end.column() + offset_from_edit >= old_snapshot.max_expansion_column
+                {
+                    break;
                 }
             }
 
-            let _old_alloc_ptr = fold_edits.as_ptr();
-            // Combine any edits that overlap due to the expansion.
-            let mut fold_edits = fold_edits.into_iter();
-            if let Some(mut first_edit) = fold_edits.next() {
-                // This code relies on reusing allocations from the Vec<_> - at the time of writing .flatten() prevents them.
-                #[allow(clippy::filter_map_identity)]
-                let mut v: Vec<_> = fold_edits
-                    .scan(&mut first_edit, |state, edit| {
-                        if state.old.end >= edit.old.start {
-                            state.old.end = edit.old.end;
-                            state.new.end = edit.new.end;
-                            Some(None) // Skip this edit, it's merged
-                        } else {
-                            let new_state = edit;
-                            let result = Some(Some(state.clone())); // Yield the previous edit
-                            **state = new_state;
-                            result
-                        }
-                    })
-                    .filter_map(|x| x)
-                    .collect();
-                v.push(first_edit);
-                debug_assert_eq!(v.as_ptr(), _old_alloc_ptr, "Fold edits were reallocated");
-                v.into_iter()
-                    .map(|fold_edit| {
-                        let old_start = fold_edit.old.start.to_point(&old_snapshot.fold_snapshot);
-                        let old_end = fold_edit.old.end.to_point(&old_snapshot.fold_snapshot);
-                        let new_start = fold_edit.new.start.to_point(&new_snapshot.fold_snapshot);
-                        let new_end = fold_edit.new.end.to_point(&new_snapshot.fold_snapshot);
-                        TabEdit {
-                            old: old_snapshot.fold_point_to_tab_point(old_start)
-                                ..old_snapshot.fold_point_to_tab_point(old_end),
-                            new: new_snapshot.fold_point_to_tab_point(new_start)
-                                ..new_snapshot.fold_point_to_tab_point(new_end),
-                        }
-                    })
-                    .collect()
-            } else {
-                vec![]
+            if let Some(offset) = last_tab_with_changed_expansion_offset.or(first_tab_offset) {
+                fold_edit.old.end.0 += offset as usize + 1;
+                fold_edit.new.end.0 += offset as usize + 1;
             }
-        } else {
-            new_snapshot.version += 1;
-            vec![TabEdit {
-                old: TabPoint::zero()..old_snapshot.max_point(),
-                new: TabPoint::zero()..new_snapshot.max_point(),
-            }]
+        }
+
+        let new_snapshot = TabSnapshot {
+            fold_snapshot,
+            tab_size,
+            max_expansion_column: old_snapshot.max_expansion_column,
+            version: new_version,
         };
+
+        let _old_alloc_ptr = fold_edits.as_ptr();
+        // Combine any edits that overlap due to the expansion.
+        let mut fold_edits = fold_edits.into_iter();
+        let mut first_edit = fold_edits.next().unwrap();
+        // This code relies on reusing allocations from the Vec<_> - at the time of writing .flatten() prevents them.
+        #[allow(clippy::filter_map_identity)]
+        let mut v: Vec<_> = fold_edits
+            .scan(&mut first_edit, |state, edit| {
+                if state.old.end >= edit.old.start {
+                    state.old.end = edit.old.end;
+                    state.new.end = edit.new.end;
+                    Some(None) // Skip this edit, it's merged
+                } else {
+                    let new_state = edit;
+                    let result = Some(Some(state.clone())); // Yield the previous edit
+                    **state = new_state;
+                    result
+                }
+            })
+            .filter_map(|x| x)
+            .collect();
+        v.push(first_edit);
+        debug_assert_eq!(v.as_ptr(), _old_alloc_ptr, "Fold edits were reallocated");
+        let tab_edits = v
+            .into_iter()
+            .map(|fold_edit| {
+                let old_start = fold_edit.old.start.to_point(&old_snapshot.fold_snapshot);
+                let old_end = fold_edit.old.end.to_point(&old_snapshot.fold_snapshot);
+                let new_start = fold_edit.new.start.to_point(&new_snapshot.fold_snapshot);
+                let new_end = fold_edit.new.end.to_point(&new_snapshot.fold_snapshot);
+                TabEdit {
+                    old: old_snapshot.fold_point_to_tab_point(old_start)
+                        ..old_snapshot.fold_point_to_tab_point(old_end),
+                    new: new_snapshot.fold_point_to_tab_point(new_start)
+                        ..new_snapshot.fold_point_to_tab_point(new_end),
+                }
+            })
+            .collect();
         *old_snapshot = new_snapshot;
         (old_snapshot.clone(), tab_edits)
     }
@@ -168,6 +193,8 @@ impl TabMap {
 pub struct TabSnapshot {
     pub fold_snapshot: FoldSnapshot,
     pub tab_size: NonZeroU32,
+    /// The maximum column up to which a tab can expand.
+    /// Any tab after this column will not expand.
     pub max_expansion_column: u32,
     pub version: usize,
 }
@@ -365,10 +392,11 @@ impl TabSnapshot {
     }
 
     #[ztracing::instrument(skip_all)]
-    fn expand_tabs<'a, I>(&self, mut cursor: TabStopCursor<'a, I>, column: u32) -> u32
-    where
-        I: Iterator<Item = Chunk<'a>>,
-    {
+    fn expand_tabs<'a>(&self, mut cursor: TabStopCursor<'a>, column: u32) -> u32 {
+        // we only ever act on a single row at a time
+        // so the main difference is that other layers build a transform sumtree, and can then just run through that
+        // we cant quite do this here, as we need to work with the previous layer chunk to understand the tabs of the corresponding row
+        // we can still do forward searches for this though, we search for a row, then traverse the column up to where we need to be
         let tab_size = self.tab_size.get();
 
         let end_column = column.min(self.max_expansion_column);
@@ -376,7 +404,7 @@ impl TabSnapshot {
         let mut tab_count = 0;
         let mut expanded_tab_len = 0;
 
-        while let Some(tab_stop) = cursor.seek(seek_target) {
+        while let Some(tab_stop) = cursor.seek_forward(seek_target) {
             let expanded_chars_old = tab_stop.char_offset + expanded_tab_len - tab_count;
             let tab_len = tab_size - ((expanded_chars_old - 1) % tab_size);
             tab_count += 1;
@@ -399,22 +427,19 @@ impl TabSnapshot {
     }
 
     #[ztracing::instrument(skip_all)]
-    fn collapse_tabs<'a, I>(
+    fn collapse_tabs<'a>(
         &self,
-        mut cursor: TabStopCursor<'a, I>,
+        mut cursor: TabStopCursor<'a>,
         column: u32,
         bias: Bias,
-    ) -> (u32, u32, u32)
-    where
-        I: Iterator<Item = Chunk<'a>>,
-    {
+    ) -> (u32, u32, u32) {
         let tab_size = self.tab_size.get();
         let mut collapsed_column = column;
         let mut seek_target = column.min(self.max_expansion_column);
         let mut tab_count = 0;
         let mut expanded_tab_len = 0;
 
-        while let Some(tab_stop) = cursor.seek(seek_target) {
+        while let Some(tab_stop) = cursor.seek_forward(seek_target) {
             // Calculate how much we want to expand this tab stop (into spaces)
             let expanded_chars_old = tab_stop.char_offset + expanded_tab_len - tab_count;
             let tab_len = tab_size - ((expanded_chars_old - 1) % tab_size);
@@ -617,13 +642,7 @@ impl<'a> Iterator for TabChunks<'a> {
             }
         }
 
-        let first_tab_ix = if self.chunk.tabs != 0 {
-            self.chunk.tabs.trailing_zeros() as usize
-        } else {
-            self.chunk.text.len()
-        };
-
-        if first_tab_ix == 0 {
+        if self.chunk.tabs & 1 != 0 {
             self.chunk.text = &self.chunk.text[1..];
             self.chunk.tabs >>= 1;
             self.chunk.chars >>= 1;
@@ -654,12 +673,46 @@ impl<'a> Iterator for TabChunks<'a> {
             });
         }
 
-        let prefix_len = first_tab_ix;
+        // Fast path: no tabs in the remaining chunk, return it directly
+        if self.chunk.tabs == 0 {
+            let chunk = self.chunk.clone();
+            self.chunk.text = "";
+            self.chunk.tabs = 0;
+            self.chunk.chars = 0;
+            self.chunk.newlines = 0;
+            let chunk_len = chunk.text.len() as u32;
+
+            let newline_count = chunk.newlines.count_ones();
+            if newline_count > 0 {
+                let last_newline_bit = 128 - chunk.newlines.leading_zeros();
+                let chars_after_last_newline =
+                    chunk.chars.unbounded_shr(last_newline_bit).count_ones();
+                let bytes_after_last_newline = chunk_len - last_newline_bit;
+
+                self.column = chars_after_last_newline;
+                self.input_column = bytes_after_last_newline;
+                self.output_position = Point::new(
+                    self.output_position.row + newline_count,
+                    bytes_after_last_newline,
+                );
+            } else {
+                let char_count = chunk.chars.count_ones();
+                self.column += char_count;
+                if !self.inside_leading_tab {
+                    self.input_column += chunk_len;
+                }
+                self.output_position.column += chunk_len;
+            }
+
+            return Some(chunk);
+        }
+
+        // Split at the next tab position
+        let prefix_len = self.chunk.tabs.trailing_zeros() as usize;
         let (prefix, suffix) = self.chunk.text.split_at(prefix_len);
 
         let mask = 1u128.unbounded_shl(prefix_len as u32).wrapping_sub(1);
         let prefix_chars = self.chunk.chars & mask;
-        let prefix_tabs = self.chunk.tabs & mask;
         let prefix_newlines = self.chunk.newlines & mask;
 
         self.chunk.text = suffix;
@@ -692,13 +745,156 @@ impl<'a> Iterator for TabChunks<'a> {
         Some(Chunk {
             text: prefix,
             chars: prefix_chars,
-            tabs: prefix_tabs,
+            tabs: 0,
             newlines: prefix_newlines,
             ..self.chunk.clone()
         })
     }
 }
 
+struct TabStopCursor<'a> {
+    chunks: FoldChunks<'a>,
+    byte_offset: u32,
+    char_offset: u32,
+    /// Chunk
+    /// last tab position iterated through
+    current_chunk: Option<(TabStopChunk<'a>, u32)>,
+}
+
+struct TabStopChunk<'a> {
+    chars: u128,
+    text: &'a str,
+    tabs: u128,
+}
+
+impl<'a> TabStopCursor<'a> {
+    fn new(chunks: FoldChunks<'a>) -> Self {
+        Self {
+            chunks,
+            byte_offset: 0,
+            char_offset: 0,
+            current_chunk: None,
+        }
+    }
+
+    fn bytes_until_next_char(&self) -> Option<usize> {
+        self.current_chunk.as_ref().map(|&(ref chunk, idx)| {
+            let higher_chars = chunk.chars.unbounded_shr(idx + 1);
+
+            if higher_chars != 0 {
+                higher_chars.trailing_zeros() as usize + 1
+            } else {
+                chunk.text.len() - idx as usize
+            }
+        })
+    }
+
+    fn is_char_boundary(&self) -> bool {
+        self.current_chunk
+            .as_ref()
+            .is_some_and(|&(ref chunk, idx)| {
+                (1u128.unbounded_shl(idx) & chunk.chars) != 0 || idx as usize == chunk.text.len()
+            })
+    }
+
+    /// distance: length to move forward while searching for the next tab stop
+    #[ztracing::instrument(skip_all)]
+    fn seek_forward(&mut self, distance: u32) -> Option<TabStop> {
+        if distance == 0 {
+            return None;
+        }
+
+        let mut distance_remaining = distance;
+
+        while let Some((mut chunk, chunk_position)) = self.current_chunk.take().or_else(|| {
+            self.chunks.next().map(|chunk| {
+                (
+                    TabStopChunk {
+                        chars: chunk.chars,
+                        text: chunk.text,
+                        tabs: chunk.tabs,
+                    },
+                    0,
+                )
+            })
+        }) {
+            let chunk_len = chunk.text.len() as u32;
+
+            if chunk.tabs == 0 {
+                let chunk_remaining = chunk_len - chunk_position;
+                if chunk_remaining >= distance_remaining {
+                    let end = chunk_position + distance_remaining;
+                    self.byte_offset += distance_remaining;
+                    self.char_offset +=
+                        count_chars_in_byte_range(chunk_position..(end - 1), chunk.chars);
+                    if end < 128 {
+                        self.current_chunk = Some((chunk, end));
+                    }
+                    return None;
+                }
+
+                self.byte_offset += chunk_remaining;
+                self.char_offset +=
+                    count_chars_in_byte_range(chunk_position..(chunk_len - 1), chunk.chars);
+                distance_remaining -= chunk_remaining;
+                continue;
+            }
+
+            let tab_end = chunk.tabs.trailing_zeros() + 1;
+            let bytes_to_tab = tab_end - chunk_position;
+
+            if bytes_to_tab > distance_remaining {
+                let end = chunk_position + distance_remaining;
+                self.byte_offset += distance_remaining;
+                self.char_offset +=
+                    count_chars_in_byte_range(chunk_position..(end - 1), chunk.chars);
+                self.current_chunk = Some((chunk, end));
+                return None;
+            }
+
+            self.byte_offset += bytes_to_tab;
+            self.char_offset +=
+                count_chars_in_byte_range(chunk_position..(tab_end - 1), chunk.chars);
+
+            let tabstop = TabStop {
+                char_offset: self.char_offset,
+                byte_offset: self.byte_offset,
+            };
+
+            chunk.tabs = (chunk.tabs - 1) & chunk.tabs;
+
+            if tab_end != chunk_len {
+                self.current_chunk = Some((chunk, tab_end));
+            }
+
+            return Some(tabstop);
+        }
+
+        None
+    }
+
+    fn byte_offset(&self) -> u32 {
+        self.byte_offset
+    }
+
+    fn char_offset(&self) -> u32 {
+        self.char_offset
+    }
+}
+
+#[inline(always)]
+fn count_chars_in_byte_range(range: Range<u32>, bitmap: u128) -> u32 {
+    let low_mask = u128::MAX << range.start;
+    let high_mask = u128::MAX >> (127 - range.end);
+    (bitmap & low_mask & high_mask).count_ones()
+}
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+struct TabStop {
+    char_offset: u32,
+    byte_offset: u32,
+}
+
 #[cfg(test)]
 mod tests {
     use std::mem;
@@ -814,40 +1010,21 @@ mod tests {
 
     #[gpui::test]
     fn test_expand_tabs(cx: &mut gpui::App) {
-        let test_values = [
-            ("κg🏀 f\nwo🏀❌by🍐❎β🍗c\tβ❎ \ncλ🎉", 17),
-            (" \twςe", 4),
-            ("fε", 1),
-            ("i❎\t", 3),
-        ];
-        let buffer = MultiBuffer::build_simple("", cx);
+        let input = "A\tBC\tDEF\tG\tHI\tJ\tK\tL\tM";
+
+        let buffer = MultiBuffer::build_simple(input, cx);
         let buffer_snapshot = buffer.read(cx).snapshot(cx);
         let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot);
         let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
         let (_, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
 
-        for (text, column) in test_values {
-            let mut tabs = 0u128;
-            let mut chars = 0u128;
-            for (idx, c) in text.char_indices() {
-                if c == '\t' {
-                    tabs |= 1 << idx;
-                }
-                chars |= 1 << idx;
-            }
-
-            let chunks = [Chunk {
-                text,
-                tabs,
-                chars,
-                ..Default::default()
-            }];
-
-            let cursor = TabStopCursor::new(chunks);
+        for (ix, _) in input.char_indices() {
+            let fold_point = FoldPoint::new(0, ix as u32);
 
             assert_eq!(
-                tab_snapshot.expected_expand_tabs(text.chars(), column),
-                tab_snapshot.expand_tabs(cursor, column)
+                tab_snapshot.expected_to_tab_point(fold_point),
+                tab_snapshot.fold_point_to_tab_point(fold_point),
+                "Failed with fold_point at column {ix}"
             );
         }
     }
@@ -1263,7 +1440,7 @@ mod tests {
             Default::default(),
         );
         let mut cursor = TabStopCursor::new(chunks);
-        assert!(cursor.seek(0).is_none());
+        assert!(cursor.seek_forward(0).is_none());
         let mut tab_stops = Vec::new();
 
         let mut all_tab_stops = Vec::new();
@@ -1279,7 +1456,7 @@ mod tests {
             }
         }
 
-        while let Some(tab_stop) = cursor.seek(u32::MAX) {
+        while let Some(tab_stop) = cursor.seek_forward(u32::MAX) {
             tab_stops.push(tab_stop);
         }
         pretty_assertions::assert_eq!(tab_stops.as_slice(), all_tab_stops.as_slice(),);
@@ -1314,7 +1491,7 @@ mod tests {
             }
         }
 
-        while let Some(tab_stop) = cursor.seek(u32::MAX) {
+        while let Some(tab_stop) = cursor.seek_forward(u32::MAX) {
             actual_tab_stops.push(tab_stop);
         }
         pretty_assertions::assert_eq!(actual_tab_stops.as_slice(), expected_tab_stops.as_slice(),);
@@ -1379,7 +1556,7 @@ mod tests {
 
             let mut found_tab_stops = Vec::new();
             let mut position = distance;
-            while let Some(tab_stop) = cursor.seek(position) {
+            while let Some(tab_stop) = cursor.seek_forward(position) {
                 found_tab_stops.push(tab_stop);
                 position = distance - tab_stop.byte_offset;
             }
@@ -1425,7 +1602,7 @@ mod tests {
             Default::default(),
         );
         let mut cursor = TabStopCursor::new(chunks);
-        assert!(cursor.seek(0).is_none());
+        assert!(cursor.seek_forward(0).is_none());
 
         let mut expected_tab_stops = Vec::new();
         let mut byte_offset = 0;
@@ -1441,7 +1618,7 @@ mod tests {
         }
 
         let mut actual_tab_stops = Vec::new();
-        while let Some(tab_stop) = cursor.seek(u32::MAX) {
+        while let Some(tab_stop) = cursor.seek_forward(u32::MAX) {
             actual_tab_stops.push(tab_stop);
         }
 
@@ -1487,7 +1664,7 @@ mod tests {
 
             let mut found_tab_stops = Vec::new();
             let mut position = distance;
-            while let Some(tab_stop) = cursor.seek(position) {
+            while let Some(tab_stop) = cursor.seek_forward(position) {
                 found_tab_stops.push(tab_stop);
                 position = distance - tab_stop.byte_offset;
             }
@@ -1520,165 +1697,3 @@ mod tests {
         }
     }
 }
-
-struct TabStopCursor<'a, I>
-where
-    I: Iterator<Item = Chunk<'a>>,
-{
-    chunks: I,
-    byte_offset: u32,
-    char_offset: u32,
-    /// Chunk
-    /// last tab position iterated through
-    current_chunk: Option<(Chunk<'a>, u32)>,
-}
-
-impl<'a, I> TabStopCursor<'a, I>
-where
-    I: Iterator<Item = Chunk<'a>>,
-{
-    #[ztracing::instrument(skip_all)]
-    fn new(chunks: impl IntoIterator<Item = Chunk<'a>, IntoIter = I>) -> Self {
-        Self {
-            chunks: chunks.into_iter(),
-            byte_offset: 0,
-            char_offset: 0,
-            current_chunk: None,
-        }
-    }
-
-    #[ztracing::instrument(skip_all)]
-    fn bytes_until_next_char(&self) -> Option<usize> {
-        self.current_chunk.as_ref().and_then(|(chunk, idx)| {
-            let mut idx = *idx;
-            let mut diff = 0;
-            while idx > 0 && chunk.chars & (1u128.unbounded_shl(idx)) == 0 {
-                idx -= 1;
-                diff += 1;
-            }
-
-            if chunk.chars & (1 << idx) != 0 {
-                Some(
-                    (chunk.text[idx as usize..].chars().next()?)
-                        .len_utf8()
-                        .saturating_sub(diff),
-                )
-            } else {
-                None
-            }
-        })
-    }
-
-    #[ztracing::instrument(skip_all)]
-    fn is_char_boundary(&self) -> bool {
-        self.current_chunk
-            .as_ref()
-            .is_some_and(|(chunk, idx)| (chunk.chars & 1u128.unbounded_shl(*idx)) != 0)
-    }
-
-    /// distance: length to move forward while searching for the next tab stop
-    #[ztracing::instrument(skip_all)]
-    fn seek(&mut self, distance: u32) -> Option<TabStop> {
-        if distance == 0 {
-            return None;
-        }
-
-        let mut distance_traversed = 0;
-
-        while let Some((mut chunk, chunk_position)) = self
-            .current_chunk
-            .take()
-            .or_else(|| self.chunks.next().zip(Some(0)))
-        {
-            if chunk.tabs == 0 {
-                let chunk_distance = chunk.text.len() as u32 - chunk_position;
-                if chunk_distance + distance_traversed >= distance {
-                    let overshoot = distance_traversed.abs_diff(distance);
-
-                    self.byte_offset += overshoot;
-                    self.char_offset += get_char_offset(
-                        chunk_position..(chunk_position + overshoot).saturating_sub(1),
-                        chunk.chars,
-                    );
-
-                    if chunk_position + overshoot < 128 {
-                        self.current_chunk = Some((chunk, chunk_position + overshoot));
-                    }
-
-                    return None;
-                }
-
-                self.byte_offset += chunk_distance;
-                self.char_offset += get_char_offset(
-                    chunk_position..(chunk_position + chunk_distance).saturating_sub(1),
-                    chunk.chars,
-                );
-                distance_traversed += chunk_distance;
-                continue;
-            }
-            let tab_position = chunk.tabs.trailing_zeros() + 1;
-
-            if distance_traversed + tab_position - chunk_position > distance {
-                let cursor_position = distance_traversed.abs_diff(distance);
-
-                self.char_offset += get_char_offset(
-                    chunk_position..(chunk_position + cursor_position - 1),
-                    chunk.chars,
-                );
-                self.current_chunk = Some((chunk, cursor_position + chunk_position));
-                self.byte_offset += cursor_position;
-
-                return None;
-            }
-
-            self.byte_offset += tab_position - chunk_position;
-            self.char_offset += get_char_offset(chunk_position..(tab_position - 1), chunk.chars);
-
-            let tabstop = TabStop {
-                char_offset: self.char_offset,
-                byte_offset: self.byte_offset,
-            };
-
-            chunk.tabs = (chunk.tabs - 1) & chunk.tabs;
-
-            if tab_position as usize != chunk.text.len() {
-                self.current_chunk = Some((chunk, tab_position));
-            }
-
-            return Some(tabstop);
-        }
-
-        None
-    }
-
-    fn byte_offset(&self) -> u32 {
-        self.byte_offset
-    }
-
-    fn char_offset(&self) -> u32 {
-        self.char_offset
-    }
-}
-
-#[inline(always)]
-fn get_char_offset(range: Range<u32>, bit_map: u128) -> u32 {
-    if range.start == range.end {
-        return if (1u128 << range.start) & bit_map == 0 {
-            0
-        } else {
-            1
-        };
-    }
-    let end_shift: u128 = 127u128 - range.end as u128;
-    let mut bit_mask = (u128::MAX >> range.start) << range.start;
-    bit_mask = (bit_mask << end_shift) >> end_shift;
-    let bit_map = bit_map & bit_mask;
-
-    bit_map.count_ones()
-}
-
-#[derive(Clone, Copy, Debug, PartialEq, Eq)]
-struct TabStop {
-    char_offset: u32,
-    byte_offset: u32,
-}

crates/editor/src/editor.rs 🔗

@@ -3626,7 +3626,7 @@ impl Editor {
             self.refresh_document_highlights(cx);
             refresh_linked_ranges(self, window, cx);
 
-            self.refresh_selected_text_highlights(false, window, cx);
+            self.refresh_selected_text_highlights(&display_map, false, window, cx);
             self.refresh_matching_bracket_highlights(&display_map, cx);
             self.refresh_outline_symbols_at_cursor(cx);
             self.update_visible_edit_prediction(window, cx);
@@ -7500,7 +7500,7 @@ impl Editor {
 
     fn prepare_highlight_query_from_selection(
         &mut self,
-        window: &Window,
+        snapshot: &DisplaySnapshot,
         cx: &mut Context<Editor>,
     ) -> Option<(String, Range<Anchor>)> {
         if matches!(self.mode, EditorMode::SingleLine) {
@@ -7512,7 +7512,6 @@ impl Editor {
         if self.selections.count() != 1 || self.selections.line_mode() {
             return None;
         }
-        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.start.row != selection.end.row
@@ -7534,6 +7533,7 @@ impl Editor {
     #[ztracing::instrument(skip_all)]
     fn update_selection_occurrence_highlights(
         &mut self,
+        multi_buffer_snapshot: MultiBufferSnapshot,
         query_text: String,
         query_range: Range<Anchor>,
         multi_buffer_range_to_query: Range<Point>,
@@ -7541,7 +7541,6 @@ impl Editor {
         window: &mut Window,
         cx: &mut Context<Editor>,
     ) -> Task<()> {
-        let multi_buffer_snapshot = self.buffer().read(cx).snapshot(cx);
         cx.spawn_in(window, async move |editor, cx| {
             if use_debounce {
                 cx.background_executor()
@@ -7557,7 +7556,7 @@ impl Editor {
                     .filter(|(_, excerpt_visible_range, _)| !excerpt_visible_range.is_empty());
                 let mut match_ranges = Vec::new();
                 let Ok(regex) = project::search::SearchQuery::text(
-                    query_text.clone(),
+                    query_text,
                     false,
                     false,
                     false,
@@ -7719,12 +7718,13 @@ impl Editor {
     #[ztracing::instrument(skip_all)]
     fn refresh_selected_text_highlights(
         &mut self,
+        snapshot: &DisplaySnapshot,
         on_buffer_edit: bool,
         window: &mut Window,
         cx: &mut Context<Editor>,
     ) {
         let Some((query_text, query_range)) =
-            self.prepare_highlight_query_from_selection(window, cx)
+            self.prepare_highlight_query_from_selection(snapshot, cx)
         else {
             self.clear_background_highlights(HighlightKey::SelectedTextHighlight, cx);
             self.quick_selection_highlight_task.take();
@@ -7756,6 +7756,7 @@ impl Editor {
             self.quick_selection_highlight_task = Some((
                 query_range.clone(),
                 self.update_selection_occurrence_highlights(
+                    snapshot.buffer.clone(),
                     query_text.clone(),
                     query_range.clone(),
                     multi_buffer_visible_range,
@@ -7781,6 +7782,7 @@ impl Editor {
             self.debounced_selection_highlight_task = Some((
                 query_range.clone(),
                 self.update_selection_occurrence_highlights(
+                    snapshot.buffer.clone(),
                     query_text,
                     query_range,
                     multi_buffer_full_range,
@@ -24107,7 +24109,7 @@ impl Editor {
                 self.update_lsp_data(Some(buffer_id), window, cx);
                 self.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx);
                 self.colorize_brackets(false, cx);
-                self.refresh_selected_text_highlights(true, window, cx);
+                self.refresh_selected_text_highlights(&self.display_snapshot(cx), true, window, cx);
                 cx.emit(EditorEvent::ExcerptsAdded {
                     buffer: buffer.clone(),
                     predecessor: *predecessor,
@@ -24166,7 +24168,7 @@ impl Editor {
             }
             multi_buffer::Event::Reparsed(buffer_id) => {
                 self.tasks_update_task = Some(self.refresh_runnables(window, cx));
-                self.refresh_selected_text_highlights(true, window, cx);
+                self.refresh_selected_text_highlights(&self.display_snapshot(cx), true, window, cx);
                 self.colorize_brackets(true, cx);
                 jsx_tag_auto_close::refresh_enabled_in_any_buffer(self, multibuffer, cx);