Simplify stuff

Lukas Wirth created

Change summary

crates/editor/src/code_context_menus.rs     |   8 
crates/gpui/src/elements/text.rs            |  34 -
crates/gpui/src/text_system/line_wrapper.rs | 327 +++++++++++++---------
3 files changed, 215 insertions(+), 154 deletions(-)

Detailed changes

crates/editor/src/code_context_menus.rs 🔗

@@ -1615,8 +1615,12 @@ impl CodeActionsMenu {
             window.text_style().font(),
             window.text_style().font_size.to_pixels(window.rem_size()),
         );
-        let is_truncated =
-            line_wrapper.should_truncate_line(&label, CODE_ACTION_MENU_MAX_WIDTH, "…");
+        let is_truncated = line_wrapper.should_truncate_line(
+            &label,
+            CODE_ACTION_MENU_MAX_WIDTH,
+            "…",
+            gpui::TruncateFrom::End,
+        );
 
         if is_truncated.is_none() {
             return None;

crates/gpui/src/elements/text.rs 🔗

@@ -2,8 +2,8 @@ use crate::{
     ActiveTooltip, AnyView, App, Bounds, DispatchPhase, Element, ElementId, GlobalElementId,
     HighlightStyle, Hitbox, HitboxBehavior, InspectorElementId, IntoElement, LayoutId,
     MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, Point, SharedString, Size, TextOverflow,
-    TextRun, TextStyle, TooltipId, WhiteSpace, Window, WrappedLine, WrappedLineLayout,
-    register_tooltip_mouse_handlers, set_tooltip_on_window,
+    TextRun, TextStyle, TooltipId, TruncateFrom, WhiteSpace, Window, WrappedLine,
+    WrappedLineLayout, register_tooltip_mouse_handlers, set_tooltip_on_window,
 };
 use anyhow::Context as _;
 use itertools::Itertools;
@@ -354,7 +354,7 @@ impl TextLayout {
                     None
                 };
 
-                let (truncate_width, truncation_suffix, truncate_start) =
+                let (truncate_width, truncation_affix, truncate_from) =
                     if let Some(text_overflow) = text_style.text_overflow.clone() {
                         let width = known_dimensions.width.or(match available_space.width {
                             crate::AvailableSpace::Definite(x) => match text_style.line_clamp {
@@ -365,11 +365,11 @@ impl TextLayout {
                         });
 
                         match text_overflow {
-                            TextOverflow::Truncate(s) => (width, s, false),
-                            TextOverflow::TruncateStart(s) => (width, s, true),
+                            TextOverflow::Truncate(s) => (width, s, TruncateFrom::End),
+                            TextOverflow::TruncateStart(s) => (width, s, TruncateFrom::Start),
                         }
                     } else {
-                        (None, "".into(), false)
+                        (None, "".into(), TruncateFrom::End)
                     };
 
                 if let Some(text_layout) = element_state.0.borrow().as_ref()
@@ -381,21 +381,13 @@ impl TextLayout {
 
                 let mut line_wrapper = cx.text_system().line_wrapper(text_style.font(), font_size);
                 let (text, runs) = if let Some(truncate_width) = truncate_width {
-                    if truncate_start {
-                        line_wrapper.truncate_line_start(
-                            text.clone(),
-                            truncate_width,
-                            &truncation_suffix,
-                            &runs,
-                        )
-                    } else {
-                        line_wrapper.truncate_line(
-                            text.clone(),
-                            truncate_width,
-                            &truncation_suffix,
-                            &runs,
-                        )
-                    }
+                    line_wrapper.truncate_line(
+                        text.clone(),
+                        truncate_width,
+                        &truncation_affix,
+                        &runs,
+                        truncate_from,
+                    )
                 } else {
                     (text.clone(), Cow::Borrowed(&*runs))
                 };

crates/gpui/src/text_system/line_wrapper.rs 🔗

@@ -2,6 +2,15 @@ use crate::{FontId, FontRun, Pixels, PlatformTextSystem, SharedString, TextRun,
 use collections::HashMap;
 use std::{borrow::Cow, iter, sync::Arc};
 
+/// Determines whether to truncate text from the start or end.
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
+pub enum TruncateFrom {
+    /// Truncate text from the start.
+    Start,
+    /// Truncate text from the end.
+    End,
+}
+
 /// The GPUI line wrapper, used to wrap lines of text to a given width.
 pub struct LineWrapper {
     platform_text_system: Arc<dyn PlatformTextSystem>,
@@ -129,29 +138,50 @@ impl LineWrapper {
     }
 
     /// Determines if a line should be truncated based on its width.
+    ///
+    /// Returns the truncation index in `line`.
     pub fn should_truncate_line(
         &mut self,
         line: &str,
         truncate_width: Pixels,
-        truncation_suffix: &str,
+        truncation_affix: &str,
+        truncate_from: TruncateFrom,
     ) -> Option<usize> {
         let mut width = px(0.);
-        let suffix_width = truncation_suffix
+        let suffix_width = truncation_affix
             .chars()
             .map(|c| self.width_for_char(c))
             .fold(px(0.0), |a, x| a + x);
         let mut truncate_ix = 0;
 
-        for (ix, c) in line.char_indices() {
-            if width + suffix_width < truncate_width {
-                truncate_ix = ix;
+        match truncate_from {
+            TruncateFrom::Start => {
+                for (ix, c) in line.char_indices().rev() {
+                    if width + suffix_width < truncate_width {
+                        truncate_ix = ix;
+                    }
+
+                    let char_width = self.width_for_char(c);
+                    width += char_width;
+
+                    if width.floor() > truncate_width {
+                        return Some(truncate_ix);
+                    }
+                }
             }
+            TruncateFrom::End => {
+                for (ix, c) in line.char_indices() {
+                    if width + suffix_width < truncate_width {
+                        truncate_ix = ix;
+                    }
 
-            let char_width = self.width_for_char(c);
-            width += char_width;
+                    let char_width = self.width_for_char(c);
+                    width += char_width;
 
-            if width.floor() > truncate_width {
-                return Some(truncate_ix);
+                    if width.floor() > truncate_width {
+                        return Some(truncate_ix);
+                    }
+                }
             }
         }
 
@@ -163,81 +193,29 @@ impl LineWrapper {
         &mut self,
         line: SharedString,
         truncate_width: Pixels,
-        truncation_suffix: &str,
+        truncation_affix: &str,
         runs: &'a [TextRun],
+        truncate_from: TruncateFrom,
     ) -> (SharedString, Cow<'a, [TextRun]>) {
         if let Some(truncate_ix) =
-            self.should_truncate_line(&line, truncate_width, truncation_suffix)
+            self.should_truncate_line(&line, truncate_width, truncation_affix, truncate_from)
         {
-            let result =
-                SharedString::from(format!("{}{}", &line[..truncate_ix], truncation_suffix));
+            let result = match truncate_from {
+                TruncateFrom::Start => {
+                    SharedString::from(format!("{truncation_affix}{}", &line[truncate_ix + 1..]))
+                }
+                TruncateFrom::End => {
+                    SharedString::from(format!("{}{truncation_affix}", &line[..truncate_ix]))
+                }
+            };
             let mut runs = runs.to_vec();
-            update_runs_after_truncation(&result, truncation_suffix, &mut runs);
+            update_runs_after_truncation(&result, truncation_affix, &mut runs, truncate_from);
             (result, Cow::Owned(runs))
         } else {
             (line, Cow::Borrowed(runs))
         }
     }
 
-    /// Truncate a line of text from the start to the given width.
-    /// Truncates from the beginning, e.g., "…ong text here"
-    pub fn truncate_line_start<'a>(
-        &mut self,
-        line: SharedString,
-        truncate_width: Pixels,
-        truncation_prefix: &str,
-        runs: &'a [TextRun],
-    ) -> (SharedString, Cow<'a, [TextRun]>) {
-        // First, measure the full line width to see if truncation is needed
-        let full_width: Pixels = line.chars().map(|c| self.width_for_char(c)).sum();
-
-        if full_width <= truncate_width {
-            return (line, Cow::Borrowed(runs));
-        }
-
-        let prefix_width: Pixels = truncation_prefix
-            .chars()
-            .map(|c| self.width_for_char(c))
-            .sum();
-
-        let available_width = truncate_width - prefix_width;
-
-        if available_width <= px(0.) {
-            return (
-                SharedString::from(truncation_prefix.to_string()),
-                Cow::Owned(vec![]),
-            );
-        }
-
-        // Work backwards from the end to find where to start the visible text
-        let char_indices: Vec<(usize, char)> = line.char_indices().collect();
-        let mut width_from_end = px(0.);
-        let mut start_byte_index = line.len();
-
-        for (byte_index, c) in char_indices.iter().rev() {
-            let char_width = self.width_for_char(*c);
-            if width_from_end + char_width > available_width {
-                break;
-            }
-            width_from_end += char_width;
-            start_byte_index = *byte_index;
-        }
-
-        if start_byte_index == 0 {
-            return (line, Cow::Borrowed(runs));
-        }
-
-        let result = SharedString::from(format!(
-            "{}{}",
-            truncation_prefix,
-            &line[start_byte_index..]
-        ));
-        let mut runs = runs.to_vec();
-        update_runs_after_start_truncation(&result, truncation_prefix, start_byte_index, &mut runs);
-
-        (result, Cow::Owned(runs))
-    }
-
     /// Any character in this list should be treated as a word character,
     /// meaning it can be part of a word that should not be wrapped.
     pub(crate) fn is_word_char(c: char) -> bool {
@@ -304,61 +282,35 @@ impl LineWrapper {
     }
 }
 
-fn update_runs_after_truncation(result: &str, ellipsis: &str, runs: &mut Vec<TextRun>) {
-    let mut truncate_at = result.len() - ellipsis.len();
-    for (run_index, run) in runs.iter_mut().enumerate() {
-        if run.len <= truncate_at {
-            truncate_at -= run.len;
-        } else {
-            run.len = truncate_at + ellipsis.len();
-            runs.truncate(run_index + 1);
-            break;
-        }
-    }
-}
-
-fn update_runs_after_start_truncation(
+fn update_runs_after_truncation(
     result: &str,
-    prefix: &str,
-    bytes_removed: usize,
+    ellipsis: &str,
     runs: &mut Vec<TextRun>,
+    truncate_from: TruncateFrom,
 ) {
-    let prefix_len = prefix.len();
-
-    let mut bytes_to_skip = bytes_removed;
-    let mut first_relevant_run = 0;
-
-    for (index, run) in runs.iter().enumerate() {
-        if bytes_to_skip >= run.len {
-            bytes_to_skip -= run.len;
-            first_relevant_run = index + 1;
-        } else {
-            break;
+    let mut truncate_at = result.len() - ellipsis.len();
+    match truncate_from {
+        TruncateFrom::Start => {
+            for (run_index, run) in runs.iter_mut().enumerate().rev() {
+                if run.len <= truncate_at {
+                    truncate_at -= run.len;
+                } else {
+                    run.len = truncate_at + ellipsis.len();
+                    runs.splice(..run_index, std::iter::empty());
+                    break;
+                }
+            }
         }
-    }
-
-    if first_relevant_run > 0 {
-        runs.drain(0..first_relevant_run);
-    }
-
-    if !runs.is_empty() && bytes_to_skip > 0 {
-        runs[0].len -= bytes_to_skip;
-    }
-
-    if !runs.is_empty() {
-        runs[0].len += prefix_len;
-    } else {
-        runs.push(TextRun {
-            len: result.len(),
-            ..Default::default()
-        });
-    }
-
-    let total_run_len: usize = runs.iter().map(|r| r.len).sum();
-    if total_run_len != result.len() && !runs.is_empty() {
-        let diff = result.len() as isize - total_run_len as isize;
-        if let Some(last) = runs.last_mut() {
-            last.len = (last.len as isize + diff) as usize;
+        TruncateFrom::End => {
+            for (run_index, run) in runs.iter_mut().enumerate() {
+                if run.len <= truncate_at {
+                    truncate_at -= run.len;
+                } else {
+                    run.len = truncate_at + ellipsis.len();
+                    runs.truncate(run_index + 1);
+                    break;
+                }
+            }
         }
     }
 }
@@ -608,7 +560,7 @@ mod tests {
     }
 
     #[test]
-    fn test_truncate_line() {
+    fn test_truncate_line_end() {
         let mut wrapper = build_wrapper();
 
         fn perform_test(
@@ -619,8 +571,13 @@ mod tests {
         ) {
             let dummy_run_lens = vec![text.len()];
             let dummy_runs = generate_test_runs(&dummy_run_lens);
-            let (result, dummy_runs) =
-                wrapper.truncate_line(text.into(), px(220.), ellipsis, &dummy_runs);
+            let (result, dummy_runs) = wrapper.truncate_line(
+                text.into(),
+                px(220.),
+                ellipsis,
+                &dummy_runs,
+                TruncateFrom::End,
+            );
             assert_eq!(result, expected);
             assert_eq!(dummy_runs.first().unwrap().len, result.len());
         }
@@ -646,7 +603,50 @@ mod tests {
     }
 
     #[test]
-    fn test_truncate_multiple_runs() {
+    fn test_truncate_line_start() {
+        let mut wrapper = build_wrapper();
+
+        fn perform_test(
+            wrapper: &mut LineWrapper,
+            text: &'static str,
+            expected: &'static str,
+            ellipsis: &str,
+        ) {
+            let dummy_run_lens = vec![text.len()];
+            let dummy_runs = generate_test_runs(&dummy_run_lens);
+            let (result, dummy_runs) = wrapper.truncate_line(
+                text.into(),
+                px(220.),
+                ellipsis,
+                &dummy_runs,
+                TruncateFrom::Start,
+            );
+            assert_eq!(result, expected);
+            assert_eq!(dummy_runs.first().unwrap().len, result.len());
+        }
+
+        perform_test(
+            &mut wrapper,
+            "aaaa bbbb cccc ddddd eeee fff gg",
+            "cccc ddddd eeee fff gg",
+            "",
+        );
+        perform_test(
+            &mut wrapper,
+            "aaaa bbbb cccc ddddd eeee fff gg",
+            "…ccc ddddd eeee fff gg",
+            "…",
+        );
+        perform_test(
+            &mut wrapper,
+            "aaaa bbbb cccc ddddd eeee fff gg",
+            "......dddd eeee fff gg",
+            "......",
+        );
+    }
+
+    #[test]
+    fn test_truncate_multiple_runs_end() {
         let mut wrapper = build_wrapper();
 
         fn perform_test(
@@ -659,7 +659,7 @@ mod tests {
         ) {
             let dummy_runs = generate_test_runs(run_lens);
             let (result, dummy_runs) =
-                wrapper.truncate_line(text.into(), line_width, "…", &dummy_runs);
+                wrapper.truncate_line(text.into(), line_width, "…", &dummy_runs, TruncateFrom::End);
             assert_eq!(result, expected);
             for (run, result_len) in dummy_runs.iter().zip(result_run_len) {
                 assert_eq!(run.len, *result_len);
@@ -705,10 +705,75 @@ mod tests {
     }
 
     #[test]
-    fn test_update_run_after_truncation() {
+    fn test_truncate_multiple_runs_start() {
+        let mut wrapper = build_wrapper();
+
+        #[track_caller]
+        fn perform_test(
+            wrapper: &mut LineWrapper,
+            text: &'static str,
+            expected: &str,
+            run_lens: &[usize],
+            result_run_len: &[usize],
+            line_width: Pixels,
+        ) {
+            let dummy_runs = generate_test_runs(run_lens);
+            let (result, dummy_runs) = wrapper.truncate_line(
+                text.into(),
+                line_width,
+                "…",
+                &dummy_runs,
+                TruncateFrom::Start,
+            );
+            assert_eq!(result, expected);
+            for (run, result_len) in dummy_runs.iter().zip(result_run_len) {
+                assert_eq!(run.len, *result_len);
+            }
+        }
+        // Case 0: Normal
+        // Text: abcdefghijkl
+        // Runs: Run0 { len: 12, ... }
+        //
+        // Truncate res: …ijkl (truncate_at = 9)
+        // Run res: Run0 { string: …ijkl, len: 7, ... }
+        perform_test(&mut wrapper, "abcdefghijkl", "…ijkl", &[12], &[7], px(50.));
+        // Case 1: Drop some runs
+        // Text: abcdefghijkl
+        // Runs: Run0 { len: 4, ... }, Run1 { len: 4, ... }, Run2 { len: 4, ... }
+        //
+        // Truncate res: …ghijkl (truncate_at = 7)
+        // Runs res: Run0 { string: …gh, len: 5, ... }, Run1 { string: ijkl, len:
+        // 4, ... }
+        perform_test(
+            &mut wrapper,
+            "abcdefghijkl",
+            "…ghijkl",
+            &[4, 4, 4],
+            &[5, 4],
+            px(70.),
+        );
+        // Case 2: Truncate at start of some run
+        // Text: abcdefghijkl
+        // Runs: Run0 { len: 4, ... }, Run1 { len: 4, ... }, Run2 { len: 4, ... }
+        //
+        // Truncate res: abcdefgh… (truncate_at = 3)
+        // Runs res: Run0 { string: …, len: 3, ... }, Run1 { string: efgh, len:
+        // 4, ... }, Run2 { string: ijkl, len: 4, ... }
+        perform_test(
+            &mut wrapper,
+            "abcdefghijkl",
+            "…efghijkl",
+            &[4, 4, 4],
+            &[3, 4, 4],
+            px(90.),
+        );
+    }
+
+    #[test]
+    fn test_update_run_after_truncation_end() {
         fn perform_test(result: &str, run_lens: &[usize], result_run_lens: &[usize]) {
             let mut dummy_runs = generate_test_runs(run_lens);
-            update_runs_after_truncation(result, "…", &mut dummy_runs);
+            update_runs_after_truncation(result, "…", &mut dummy_runs, TruncateFrom::End);
             for (run, result_len) in dummy_runs.iter().zip(result_run_lens) {
                 assert_eq!(run.len, *result_len);
             }