Skip to content

Commit e0931d5

Browse files
Reuse char-scan invisibles detection in highlight_invisibles (zed-industries#62715)
Follow-up to zed-industries#62478 (comment) New bench results: | corpus | old | new | speedup | |---|---|---|---| | ascii, no invisibles | 83 MB/s | 580 MB/s | **7.0x** | | unicode, no invisibles | 88 MB/s | 442 MB/s | **5.0x** | | sparse invisibles | 63 MB/s | 431 MB/s | **6.9x** | | dense invisibles | 82 MB/s | 109 MB/s | 1.3x | Release Notes: - N/A
1 parent bf65fd4 commit e0931d5

4 files changed

Lines changed: 137 additions & 83 deletions

File tree

crates/benchmarks/benches/display_map.rs

Lines changed: 79 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use criterion::{BenchmarkId, Criterion, black_box, criterion_group, criterion_main};
2-
use editor::{MultiBuffer, display_map::*};
2+
use editor::{EditorStyle, MultiBuffer, display_map::*};
33
use gpui::{AppContext as _, HighlightStyle, Hsla, TestDispatcher, font, px};
44
use itertools::Itertools;
55
use multi_buffer::MultiBufferOffset;
@@ -205,10 +205,87 @@ fn create_highlight_endpoints_benchmark(c: &mut Criterion) {
205205
group.finish();
206206
}
207207

208+
fn highlighted_chunks_benchmark(c: &mut Criterion) {
209+
const LINE_COUNT: usize = 500;
210+
211+
let dispatcher = TestDispatcher::new(1);
212+
let mut cx = gpui::TestAppContext::build(dispatcher, None);
213+
cx.update(|cx| {
214+
let store = SettingsStore::test(cx);
215+
cx.set_global(store);
216+
editor::init(cx);
217+
});
218+
219+
let corpora = [
220+
(
221+
"ascii",
222+
" let chunks = snapshot.highlighted_chunks(rows.clone(), language_aware, style);",
223+
),
224+
(
225+
"unicode",
226+
"の設定を変更する — émojis 🧑\u{200d}\u{fe0f} und Ümläute überall, здесь тоже текст",
227+
),
228+
(
229+
"sparse_invisibles",
230+
"normal text here\u{200b}and some more text that goes on for a while without issues",
231+
),
232+
(
233+
"dense_invisibles",
234+
"a\u{200b}b\u{ad}c\u{2060}d\u{feff}e\u{200b}f\u{ad}g\u{2060}h",
235+
),
236+
];
237+
238+
let mut group = c.benchmark_group("Highlighted chunks");
239+
for (name, line) in corpora {
240+
let text = std::iter::repeat_n(line, LINE_COUNT)
241+
.collect::<Vec<_>>()
242+
.join("\n");
243+
let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
244+
let map = cx.new(|cx| {
245+
DisplayMap::new(
246+
buffer,
247+
font("Courier"),
248+
px(16.0),
249+
None,
250+
1,
251+
1,
252+
FoldPlaceholder::default(),
253+
DiagnosticSeverity::Warning,
254+
cx,
255+
)
256+
});
257+
let snapshot = cx.update(|cx| map.update(cx, |map, cx| map.snapshot(cx)));
258+
let editor_style = EditorStyle::default();
259+
group.bench_with_input(
260+
BenchmarkId::new("highlighted_chunks", name),
261+
&snapshot,
262+
|bench, snapshot| {
263+
bench.iter(|| {
264+
let mut total_len = 0usize;
265+
let chunks = snapshot.highlighted_chunks(
266+
DisplayRow(0)..DisplayRow(LINE_COUNT as u32),
267+
language::LanguageAwareStyling {
268+
tree_sitter: false,
269+
diagnostics: false,
270+
},
271+
&editor_style,
272+
);
273+
for chunk in chunks {
274+
total_len += black_box(chunk.text).len();
275+
}
276+
black_box(total_len);
277+
});
278+
},
279+
);
280+
}
281+
group.finish();
282+
}
283+
208284
criterion_group!(
209285
benches,
210286
to_tab_point_benchmark,
211287
to_fold_point_benchmark,
212-
create_highlight_endpoints_benchmark
288+
create_highlight_endpoints_benchmark,
289+
highlighted_chunks_benchmark
213290
);
214291
criterion_main!(benches);

crates/editor/src/display_map.rs

Lines changed: 46 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ pub use fold_map::{
8989
ChunkRenderer, ChunkRendererContext, ChunkRendererId, Fold, FoldId, FoldPlaceholder, FoldPoint,
9090
};
9191
pub use inlay_map::{InlayOffset, InlayPoint};
92+
use invisibles::is_standalone_grapheme;
9293
pub use invisibles::{is_invisible, replacement};
9394
pub use wrap_map::{WrapPoint, WrapRow, WrapSnapshot};
9495

@@ -1420,24 +1421,25 @@ impl<'a> HighlightedChunk<'a> {
14201421
self,
14211422
editor_style: &'a EditorStyle,
14221423
) -> impl Iterator<Item = Self> + 'a {
1423-
let mut chunks = self.text.graphemes(true).peekable();
14241424
let mut text = self.text;
14251425
let style = self.style;
14261426
let is_tab = self.is_tab;
14271427
let renderer = self.replacement;
14281428
let is_inlay = self.is_inlay;
14291429
iter::from_fn(move || {
1430-
let mut prefix_len = 0;
1431-
while let Some(&chunk) = chunks.peek() {
1432-
let mut chars = chunk.chars();
1433-
let Some(ch) = chars.next() else { break };
1434-
if chunk.len() != ch.len_utf8() || !is_invisible(ch) {
1435-
prefix_len += chunk.len();
1436-
chunks.next();
1430+
if text.is_empty() {
1431+
return None;
1432+
}
1433+
for (offset, ch) in text.char_indices() {
1434+
if !is_invisible(ch) {
1435+
continue;
1436+
}
1437+
let ch_end = offset + ch.len_utf8();
1438+
if !is_standalone_grapheme(text, offset, ch_end) {
14371439
continue;
14381440
}
1439-
if prefix_len > 0 {
1440-
let (prefix, suffix) = text.split_at(prefix_len);
1441+
if offset > 0 {
1442+
let (prefix, suffix) = text.split_at(offset);
14411443
text = suffix;
14421444
return Some(HighlightedChunk {
14431445
text: prefix,
@@ -1447,70 +1449,44 @@ impl<'a> HighlightedChunk<'a> {
14471449
replacement: renderer.clone(),
14481450
});
14491451
}
1450-
chunks.next();
1451-
let (prefix, suffix) = text.split_at(chunk.len());
1452+
let (invisible_text, suffix) = text.split_at(ch_end);
14521453
text = suffix;
1453-
if let Some(replacement) = replacement(ch) {
1454-
let invisible_highlight = HighlightStyle {
1455-
background_color: Some(editor_style.status.hint_background),
1456-
underline: Some(UnderlineStyle {
1457-
color: Some(editor_style.status.hint),
1458-
thickness: px(1.),
1459-
wavy: false,
1460-
}),
1461-
..Default::default()
1462-
};
1463-
let invisible_style = if let Some(style) = style {
1464-
style.highlight(invisible_highlight)
1465-
} else {
1466-
invisible_highlight
1467-
};
1468-
return Some(HighlightedChunk {
1469-
text: prefix,
1470-
style: Some(invisible_style),
1471-
is_tab: false,
1472-
is_inlay,
1473-
replacement: Some(ChunkReplacement::Str(replacement.into())),
1474-
});
1454+
let invisible_highlight = HighlightStyle {
1455+
background_color: Some(editor_style.status.hint_background),
1456+
underline: Some(UnderlineStyle {
1457+
color: Some(editor_style.status.hint),
1458+
thickness: px(1.),
1459+
wavy: false,
1460+
}),
1461+
..Default::default()
1462+
};
1463+
let invisible_style = if let Some(style) = style {
1464+
style.highlight(invisible_highlight)
14751465
} else {
1476-
let invisible_highlight = HighlightStyle {
1477-
background_color: Some(editor_style.status.hint_background),
1478-
underline: Some(UnderlineStyle {
1479-
color: Some(editor_style.status.hint),
1480-
thickness: px(1.),
1481-
wavy: false,
1482-
}),
1483-
..Default::default()
1484-
};
1485-
let invisible_style = if let Some(style) = style {
1486-
style.highlight(invisible_highlight)
1487-
} else {
1488-
invisible_highlight
1489-
};
1490-
1491-
return Some(HighlightedChunk {
1492-
text: prefix,
1493-
style: Some(invisible_style),
1494-
is_tab: false,
1495-
is_inlay,
1496-
replacement: renderer.clone(),
1497-
});
1498-
}
1499-
}
1500-
1501-
if !text.is_empty() {
1502-
let remainder = text;
1503-
text = "";
1504-
Some(HighlightedChunk {
1505-
text: remainder,
1506-
style,
1507-
is_tab,
1466+
invisible_highlight
1467+
};
1468+
return Some(HighlightedChunk {
1469+
text: invisible_text,
1470+
style: Some(invisible_style),
1471+
is_tab: false,
15081472
is_inlay,
1509-
replacement: renderer.clone(),
1510-
})
1511-
} else {
1512-
None
1473+
replacement: match replacement(ch) {
1474+
Some(replacement) => {
1475+
Some(ChunkReplacement::Str(SharedString::from(replacement)))
1476+
}
1477+
None => renderer.clone(),
1478+
},
1479+
});
15131480
}
1481+
let remainder = text;
1482+
text = "";
1483+
Some(HighlightedChunk {
1484+
text: remainder,
1485+
style,
1486+
is_tab,
1487+
is_inlay,
1488+
replacement: renderer.clone(),
1489+
})
15141490
})
15151491
}
15161492
}

crates/editor/src/display_map/invisibles.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@
3030
// ref: https://gist.github.com/ConradIrwin/f759e1fc29267143c4c7895aa495dca5?h=1
3131
// ref: https://unicode.org/Public/emoji/13.0/emoji-test.txt
3232
// https://github.com/bits/UTF-8-Unicode-Test-Documents/blob/master/UTF-8_sequence_separated/utf8_sequence_0-0x10ffff_assigned_including-unprintable-asis.txt
33+
use unicode_segmentation::GraphemeCursor;
34+
3335
#[ztracing::instrument(skip_all)]
3436
pub fn is_invisible(c: char) -> bool {
3537
if c <= '\u{1f}' {
@@ -111,6 +113,15 @@ fn should_preserve_invisible_character(c: char) -> bool {
111113
}
112114
}
113115

116+
pub fn is_standalone_grapheme(text: &str, start: usize, end: usize) -> bool {
117+
let mut cursor = GraphemeCursor::new(start, text.len(), true);
118+
if cursor.is_boundary(text, 0) != Ok(true) {
119+
return false;
120+
}
121+
cursor.set_cursor(end);
122+
cursor.is_boundary(text, 0) == Ok(true)
123+
}
124+
114125
const FIXED_WIDTH_SPACE: char = '\u{2007}';
115126

116127
// IDEOGRAPHIC SPACE is common alongside Chinese and other wide character sets.

crates/editor/src/display_map/wrap_map.rs

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use super::{
22
Highlights,
33
dimensions::RowDelta,
44
fold_map::{Chunk, FoldRows},
5-
invisibles::{is_invisible, replacement},
5+
invisibles::{is_invisible, is_standalone_grapheme, replacement},
66
tab_map::{self, TabEdit, TabPoint, TabSnapshot},
77
};
88

@@ -23,7 +23,6 @@ use std::{
2323
};
2424
use sum_tree::{Bias, Cursor, Dimensions, SumTree};
2525
use text::Patch;
26-
use unicode_segmentation::GraphemeCursor;
2726

2827
pub use super::tab_map::TextSummary;
2928
pub type WrapEdit = text::Edit<WrapRow>;
@@ -142,15 +141,6 @@ impl LineFragmentBuilder {
142141
}
143142
}
144143

145-
fn is_standalone_grapheme(text: &str, start: usize, end: usize) -> bool {
146-
let mut cursor = GraphemeCursor::new(start, text.len(), true);
147-
if cursor.is_boundary(text, 0) != Ok(true) {
148-
return false;
149-
}
150-
cursor.set_cursor(end);
151-
cursor.is_boundary(text, 0) == Ok(true)
152-
}
153-
154144
pub struct WrapChunks<'a> {
155145
input_chunks: tab_map::TabChunks<'a>,
156146
input_chunk: Chunk<'a>,

0 commit comments

Comments
 (0)