Don't render invisibles with elements (#20841)

Conrad Irwin created

Turns out that in the case you have a somehow valid utf-8 file that
contains almost all ascii control characters, we run out of element
arena space.

Fixes: #20652

Release Notes:

- Fixed a crash when opening a file containing a very large number of
ascii control characters on one line.

Change summary

crates/editor/src/display_map.rs           |  51 ++++++----
crates/editor/src/element.rs               | 116 +++++++++++++++--------
crates/gpui/src/text_system/line.rs        |  15 +++
crates/gpui/src/text_system/line_layout.rs |   2 
4 files changed, 119 insertions(+), 65 deletions(-)

Detailed changes

crates/editor/src/display_map.rs 🔗

@@ -66,7 +66,7 @@ use std::{
 use sum_tree::{Bias, TreeMap};
 use tab_map::{TabMap, TabSnapshot};
 use text::LineIndent;
-use ui::{div, px, IntoElement, ParentElement, SharedString, Styled, WindowContext};
+use ui::{px, SharedString, WindowContext};
 use unicode_segmentation::UnicodeSegmentation;
 use wrap_map::{WrapMap, WrapSnapshot};
 
@@ -541,11 +541,17 @@ pub struct HighlightStyles {
     pub suggestion: Option<HighlightStyle>,
 }
 
+#[derive(Clone)]
+pub enum ChunkReplacement {
+    Renderer(ChunkRenderer),
+    Str(SharedString),
+}
+
 pub struct HighlightedChunk<'a> {
     pub text: &'a str,
     pub style: Option<HighlightStyle>,
     pub is_tab: bool,
-    pub renderer: Option<ChunkRenderer>,
+    pub replacement: Option<ChunkReplacement>,
 }
 
 impl<'a> HighlightedChunk<'a> {
@@ -557,7 +563,7 @@ impl<'a> HighlightedChunk<'a> {
         let mut text = self.text;
         let style = self.style;
         let is_tab = self.is_tab;
-        let renderer = self.renderer;
+        let renderer = self.replacement;
         iter::from_fn(move || {
             let mut prefix_len = 0;
             while let Some(&ch) = chars.peek() {
@@ -573,30 +579,33 @@ impl<'a> HighlightedChunk<'a> {
                         text: prefix,
                         style,
                         is_tab,
-                        renderer: renderer.clone(),
+                        replacement: renderer.clone(),
                     });
                 }
                 chars.next();
                 let (prefix, suffix) = text.split_at(ch.len_utf8());
                 text = suffix;
                 if let Some(replacement) = replacement(ch) {
-                    let background = editor_style.status.hint_background;
-                    let underline = editor_style.status.hint;
+                    let invisible_highlight = HighlightStyle {
+                        background_color: Some(editor_style.status.hint_background),
+                        underline: Some(UnderlineStyle {
+                            color: Some(editor_style.status.hint),
+                            thickness: px(1.),
+                            wavy: false,
+                        }),
+                        ..Default::default()
+                    };
+                    let invisible_style = if let Some(mut style) = style {
+                        style.highlight(invisible_highlight);
+                        style
+                    } else {
+                        invisible_highlight
+                    };
                     return Some(HighlightedChunk {
                         text: prefix,
-                        style: None,
+                        style: Some(invisible_style),
                         is_tab: false,
-                        renderer: Some(ChunkRenderer {
-                            render: Arc::new(move |_| {
-                                div()
-                                    .child(replacement)
-                                    .bg(background)
-                                    .text_decoration_1()
-                                    .text_decoration_color(underline)
-                                    .into_any_element()
-                            }),
-                            constrain_width: false,
-                        }),
+                        replacement: Some(ChunkReplacement::Str(replacement.into())),
                     });
                 } else {
                     let invisible_highlight = HighlightStyle {
@@ -619,7 +628,7 @@ impl<'a> HighlightedChunk<'a> {
                         text: prefix,
                         style: Some(invisible_style),
                         is_tab: false,
-                        renderer: renderer.clone(),
+                        replacement: renderer.clone(),
                     });
                 }
             }
@@ -631,7 +640,7 @@ impl<'a> HighlightedChunk<'a> {
                     text: remainder,
                     style,
                     is_tab,
-                    renderer: renderer.clone(),
+                    replacement: renderer.clone(),
                 })
             } else {
                 None
@@ -895,7 +904,7 @@ impl DisplaySnapshot {
                 text: chunk.text,
                 style: highlight_style,
                 is_tab: chunk.is_tab,
-                renderer: chunk.renderer,
+                replacement: chunk.renderer.map(ChunkReplacement::Renderer),
             }
             .highlight_invisibles(editor_style)
         })

crates/editor/src/element.rs 🔗

@@ -16,8 +16,8 @@ use crate::{
     items::BufferSearchHighlights,
     mouse_context_menu::{self, MenuPosition, MouseContextMenu},
     scroll::scroll_amount::ScrollAmount,
-    BlockId, CodeActionsMenu, CursorShape, CustomBlockId, DisplayPoint, DisplayRow,
-    DocumentHighlightRead, DocumentHighlightWrite, Editor, EditorMode, EditorSettings,
+    BlockId, ChunkReplacement, CodeActionsMenu, CursorShape, CustomBlockId, DisplayPoint,
+    DisplayRow, DocumentHighlightRead, DocumentHighlightWrite, Editor, EditorMode, EditorSettings,
     EditorSnapshot, EditorStyle, ExpandExcerpts, FocusedBlock, GutterDimensions, HalfPageDown,
     HalfPageUp, HandleInput, HoveredCursor, HoveredHunk, JumpData, LineDown, LineUp, OpenExcerpts,
     PageDown, PageUp, Point, RowExt, RowRangeExt, SelectPhase, Selection, SoftWrap, ToPoint,
@@ -34,8 +34,8 @@ use gpui::{
     FontId, GlobalElementId, Hitbox, Hsla, InteractiveElement, IntoElement, Length,
     ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad,
     ParentElement, Pixels, ScrollDelta, ScrollWheelEvent, ShapedLine, SharedString, Size,
-    StatefulInteractiveElement, Style, Styled, TextRun, TextStyle, TextStyleRefinement, View,
-    ViewContext, WeakView, WindowContext,
+    StatefulInteractiveElement, Style, Styled, TextRun, TextStyleRefinement, View, ViewContext,
+    WeakView, WindowContext,
 };
 use gpui::{ClickEvent, Subscription};
 use itertools::Itertools;
@@ -2019,7 +2019,7 @@ impl EditorElement {
             let chunks = snapshot.highlighted_chunks(rows.clone(), true, style);
             LineWithInvisibles::from_chunks(
                 chunks,
-                &style.text,
+                &style,
                 MAX_LINE_LEN,
                 rows.len(),
                 snapshot.mode,
@@ -4372,7 +4372,7 @@ impl LineWithInvisibles {
     #[allow(clippy::too_many_arguments)]
     fn from_chunks<'a>(
         chunks: impl Iterator<Item = HighlightedChunk<'a>>,
-        text_style: &TextStyle,
+        editor_style: &EditorStyle,
         max_line_len: usize,
         max_line_count: usize,
         editor_mode: EditorMode,
@@ -4380,6 +4380,7 @@ impl LineWithInvisibles {
         is_row_soft_wrapped: impl Copy + Fn(usize) -> bool,
         cx: &mut WindowContext,
     ) -> Vec<Self> {
+        let text_style = &editor_style.text;
         let mut layouts = Vec::with_capacity(max_line_count);
         let mut fragments: SmallVec<[LineFragment; 1]> = SmallVec::new();
         let mut line = String::new();
@@ -4398,9 +4399,9 @@ impl LineWithInvisibles {
             text: "\n",
             style: None,
             is_tab: false,
-            renderer: None,
+            replacement: None,
         }]) {
-            if let Some(renderer) = highlighted_chunk.renderer {
+            if let Some(replacement) = highlighted_chunk.replacement {
                 if !line.is_empty() {
                     let shaped_line = cx
                         .text_system()
@@ -4413,42 +4414,71 @@ impl LineWithInvisibles {
                     styles.clear();
                 }
 
-                let available_width = if renderer.constrain_width {
-                    let chunk = if highlighted_chunk.text == ellipsis.as_ref() {
-                        ellipsis.clone()
-                    } else {
-                        SharedString::from(Arc::from(highlighted_chunk.text))
-                    };
-                    let shaped_line = cx
-                        .text_system()
-                        .shape_line(
-                            chunk,
-                            font_size,
-                            &[text_style.to_run(highlighted_chunk.text.len())],
-                        )
-                        .unwrap();
-                    AvailableSpace::Definite(shaped_line.width)
-                } else {
-                    AvailableSpace::MinContent
-                };
+                match replacement {
+                    ChunkReplacement::Renderer(renderer) => {
+                        let available_width = if renderer.constrain_width {
+                            let chunk = if highlighted_chunk.text == ellipsis.as_ref() {
+                                ellipsis.clone()
+                            } else {
+                                SharedString::from(Arc::from(highlighted_chunk.text))
+                            };
+                            let shaped_line = cx
+                                .text_system()
+                                .shape_line(
+                                    chunk,
+                                    font_size,
+                                    &[text_style.to_run(highlighted_chunk.text.len())],
+                                )
+                                .unwrap();
+                            AvailableSpace::Definite(shaped_line.width)
+                        } else {
+                            AvailableSpace::MinContent
+                        };
 
-                let mut element = (renderer.render)(&mut ChunkRendererContext {
-                    context: cx,
-                    max_width: text_width,
-                });
-                let line_height = text_style.line_height_in_pixels(cx.rem_size());
-                let size = element.layout_as_root(
-                    size(available_width, AvailableSpace::Definite(line_height)),
-                    cx,
-                );
+                        let mut element = (renderer.render)(&mut ChunkRendererContext {
+                            context: cx,
+                            max_width: text_width,
+                        });
+                        let line_height = text_style.line_height_in_pixels(cx.rem_size());
+                        let size = element.layout_as_root(
+                            size(available_width, AvailableSpace::Definite(line_height)),
+                            cx,
+                        );
 
-                width += size.width;
-                len += highlighted_chunk.text.len();
-                fragments.push(LineFragment::Element {
-                    element: Some(element),
-                    size,
-                    len: highlighted_chunk.text.len(),
-                });
+                        width += size.width;
+                        len += highlighted_chunk.text.len();
+                        fragments.push(LineFragment::Element {
+                            element: Some(element),
+                            size,
+                            len: highlighted_chunk.text.len(),
+                        });
+                    }
+                    ChunkReplacement::Str(x) => {
+                        let text_style = if let Some(style) = highlighted_chunk.style {
+                            Cow::Owned(text_style.clone().highlight(style))
+                        } else {
+                            Cow::Borrowed(text_style)
+                        };
+
+                        let run = TextRun {
+                            len: x.len(),
+                            font: text_style.font(),
+                            color: text_style.color,
+                            background_color: text_style.background_color,
+                            underline: text_style.underline,
+                            strikethrough: text_style.strikethrough,
+                        };
+                        let line_layout = cx
+                            .text_system()
+                            .shape_line(x, font_size, &[run])
+                            .unwrap()
+                            .with_len(highlighted_chunk.text.len());
+
+                        width += line_layout.width;
+                        len += highlighted_chunk.text.len();
+                        fragments.push(LineFragment::Text(line_layout))
+                    }
+                }
             } else {
                 for (ix, mut line_chunk) in highlighted_chunk.text.split('\n').enumerate() {
                     if ix > 0 {
@@ -5992,7 +6022,7 @@ fn layout_line(
     let chunks = snapshot.highlighted_chunks(row..row + DisplayRow(1), true, style);
     LineWithInvisibles::from_chunks(
         chunks,
-        &style.text,
+        &style,
         MAX_LINE_LEN,
         1,
         snapshot.mode,

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

@@ -44,6 +44,21 @@ impl ShapedLine {
         self.layout.len
     }
 
+    /// Override the len, useful if you're rendering text a
+    /// as text b (e.g. rendering invisibles).
+    pub fn with_len(mut self, len: usize) -> Self {
+        let layout = self.layout.as_ref();
+        self.layout = Arc::new(LineLayout {
+            font_size: layout.font_size,
+            width: layout.width,
+            ascent: layout.ascent,
+            descent: layout.descent,
+            runs: layout.runs.clone(),
+            len,
+        });
+        self
+    }
+
     /// Paint the line of text to the window.
     pub fn paint(
         &self,

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

@@ -29,7 +29,7 @@ pub struct LineLayout {
 }
 
 /// A run of text that has been shaped .
-#[derive(Debug)]
+#[derive(Debug, Clone)]
 pub struct ShapedRun {
     /// The font id for this run
     pub font_id: FontId,