Don't render invisibles with elements (#20841)

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.
This commit is contained in:
Conrad Irwin 2024-11-18 16:47:25 -07:00 committed by GitHub
parent f0c7e62adc
commit d4c5c0f05e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 119 additions and 65 deletions

View file

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

View file

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

View file

@ -44,6 +44,21 @@ impl ShapedLine {
self.layout.len 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. /// Paint the line of text to the window.
pub fn paint( pub fn paint(
&self, &self,

View file

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