edit predictions: Fix cursor popover edit preview panic (cherry-pick #24866) (#24875)

gcp-cherry-pick-bot[bot] , Agus Zubiaga , and Antonio created

Cherry-picked edit predictions: Fix cursor popover edit preview panic
(#24866)

Release Notes:

- Fixed a panic when displaying a whitespace-only line in the edit
prediction preview

---------

Co-authored-by: Antonio <antonio@zed.dev>

Co-authored-by: Agus Zubiaga <agus@zed.dev>
Co-authored-by: Antonio <antonio@zed.dev>

Change summary

crates/editor/src/editor.rs   | 43 ++++++------------------------------
crates/language/src/buffer.rs | 35 ++++++++++++++++++++++++++++++
2 files changed, 42 insertions(+), 36 deletions(-)

Detailed changes

crates/editor/src/editor.rs 🔗

@@ -164,7 +164,7 @@ use ui::{
     h_flex, prelude::*, ButtonSize, ButtonStyle, Disclosure, IconButton, IconName, IconSize, Key,
     Tooltip,
 };
-use util::{defer, maybe, post_inc, RangeExt, ResultExt, TakeUntilExt, TryFutureExt};
+use util::{defer, maybe, post_inc, RangeExt, ResultExt, TryFutureExt};
 use workspace::item::{ItemHandle, PreviewTabsSettings};
 use workspace::notifications::{DetachAndPromptErr, NotificationId, NotifyTaskExt};
 use workspace::{
@@ -5984,52 +5984,23 @@ impl Editor {
             } => {
                 let first_edit_row = edits.first()?.0.start.text_anchor.to_point(&snapshot).row;
 
-                let highlighted_edits = crate::inline_completion_edit_text(
+                let (highlighted_edits, has_more_lines) = crate::inline_completion_edit_text(
                     &snapshot,
                     &edits,
                     edit_preview.as_ref()?,
                     true,
                     cx,
-                );
-
-                let len_total = highlighted_edits.text.len();
-                let first_line = &highlighted_edits.text
-                    [..highlighted_edits.text.find('\n').unwrap_or(len_total)];
-                let first_line_len = first_line.len();
-
-                let first_highlight_start = highlighted_edits
-                    .highlights
-                    .first()
-                    .map_or(0, |(range, _)| range.start);
-                let drop_prefix_len = first_line
-                    .char_indices()
-                    .find(|(_, c)| !c.is_whitespace())
-                    .map_or(first_highlight_start, |(ix, _)| {
-                        ix.min(first_highlight_start)
-                    });
-
-                let preview_text = &first_line[drop_prefix_len..];
-                let preview_len = preview_text.len();
-                let highlights = highlighted_edits
-                    .highlights
-                    .into_iter()
-                    .take_until(|(range, _)| range.start > first_line_len)
-                    .map(|(range, style)| {
-                        (
-                            range.start - drop_prefix_len
-                                ..(range.end - drop_prefix_len).min(preview_len),
-                            style,
-                        )
-                    });
+                )
+                .first_line_preview();
 
-                let styled_text = gpui::StyledText::new(SharedString::new(preview_text))
-                    .with_highlights(&style.text, highlights);
+                let styled_text = gpui::StyledText::new(highlighted_edits.text)
+                    .with_highlights(&style.text, highlighted_edits.highlights);
 
                 let preview = h_flex()
                     .gap_1()
                     .min_w_16()
                     .child(styled_text)
-                    .when(len_total > first_line_len, |parent| parent.child("…"));
+                    .when(has_more_lines, |parent| parent.child("…"));
 
                 let left = if first_edit_row != cursor_point.row {
                     render_relative_row_jump("", cursor_point.row, first_edit_row)

crates/language/src/buffer.rs 🔗

@@ -622,6 +622,41 @@ impl HighlightedText {
         gpui::StyledText::new(self.text.clone())
             .with_highlights(default_style, self.highlights.iter().cloned())
     }
+
+    /// Returns the first line without leading whitespace unless highlighted
+    /// and a boolean indicating if there are more lines after
+    pub fn first_line_preview(self) -> (Self, bool) {
+        let newline_ix = self.text.find('\n').unwrap_or(self.text.len());
+        let first_line = &self.text[..newline_ix];
+
+        // Trim leading whitespace, unless an edit starts prior to it.
+        let mut preview_start_ix = first_line.len() - first_line.trim_start().len();
+        if let Some((first_highlight_range, _)) = self.highlights.first() {
+            preview_start_ix = preview_start_ix.min(first_highlight_range.start);
+        }
+
+        let preview_text = &first_line[preview_start_ix..];
+        let preview_highlights = self
+            .highlights
+            .into_iter()
+            .take_while(|(range, _)| range.start < newline_ix)
+            .filter_map(|(mut range, highlight)| {
+                range.start = range.start.saturating_sub(preview_start_ix);
+                range.end = range.end.saturating_sub(preview_start_ix).min(newline_ix);
+                if range.is_empty() {
+                    None
+                } else {
+                    Some((range, highlight))
+                }
+            });
+
+        let preview = Self {
+            text: SharedString::new(preview_text),
+            highlights: preview_highlights.collect(),
+        };
+
+        (preview, self.text.len() > newline_ix)
+    }
 }
 
 impl HighlightedTextBuilder {