Improve Tab Map performance (#32243)
## Context While looking into: #32051 and #16120 with instruments, I noticed that `TabSnapshot::to_tab_point` and `TabSnapshot::to_fold_point` are a common bottleneck between the two issues. This PR takes the first steps into closing the stated issues by improving the performance of both those functions. ### Method `to_tab_point` and `to_fold_point` iterate through each character in their rows to find tab characters and translate those characters into their respective transformations. This PR changes this iteration to take advantage of the tab character bitmap in the `Rope` data structure and goes directly to each tab character when iterating. The tab bitmap is now passed from each layer in-between the `Rope` to the `TabMap`. ### Testing I added several randomized tests to ensure that the new `to_tab_point` and `to_fold_point` functions have the same behavior as the old methods they're replacing. I also added `test_random_chunk_bitmap` on each layer the tab bitmap is passed up to the `TabMap` to make sure that the bitmap being passed is transformed correctly between the layers of `DisplayMap`. `test_random_chunk_bitmap` was added to these layers: - buffer - multi buffer - custom_highlights - inlay_map - fold_map ## Benchmarking I setup benchmarks with criterion that is runnable via `cargo bench -p editor --profile=release-fast`. When benchmarking I had my laptop plugged in and did so from the terminal with a minimal amount of processes running. I'm also on a m4 max ### Results #### To Tab Point Went from completing 6.8M iterations in 5s with an average time of `736.13 ns` to `683.38 ns` which is a `-7.1875%` improvement #### To Fold Point Went from completing 6.8M iterations in 5s with an average time of `736.55 ns` to `682.40 ns` which is a `-7.1659%` improvement #### Editor render Went from having an average render time of `62.561 µs` to `57.216 µs` which is a `-8.8248%` improvement #### Build Buffer with one long line Went from having an average buffer build time of `3.2549 ms` to `3.2635 ms` which is a `+0.2151%` regression within the margin of error #### Editor with 1000 multi cursor input Went from having an average edit time of `133.05 ms` to `122.96 ms` which is a `-7.5776%` improvement Release Notes: - N/A --------- Co-authored-by: Remco Smits <djsmits12@gmail.com> Co-authored-by: Cole Miller <cole@zed.dev> Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com>
This commit is contained in:
co-authored by
Remco Smits
Cole Miller
Piotr Osiewicz
parent
cb75c2aeb7
commit
b8c30f448f
@@ -1737,6 +1737,7 @@ impl<'a> Iterator for BlockChunks<'a> {
|
||||
|
||||
return Some(Chunk {
|
||||
text: unsafe { std::str::from_utf8_unchecked(&NEWLINES[..line_count as usize]) },
|
||||
chars: (1 << line_count) - 1,
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
@@ -1766,17 +1767,26 @@ impl<'a> Iterator for BlockChunks<'a> {
|
||||
|
||||
let (mut prefix, suffix) = self.input_chunk.text.split_at(prefix_bytes);
|
||||
self.input_chunk.text = suffix;
|
||||
self.input_chunk.tabs >>= prefix_bytes.saturating_sub(1);
|
||||
self.input_chunk.chars >>= prefix_bytes.saturating_sub(1);
|
||||
|
||||
let mut tabs = self.input_chunk.tabs;
|
||||
let mut chars = self.input_chunk.chars;
|
||||
|
||||
if self.masked {
|
||||
// Not great for multibyte text because to keep cursor math correct we
|
||||
// need to have the same number of bytes in the input as output.
|
||||
let chars = prefix.chars().count();
|
||||
let bullet_len = chars;
|
||||
let chars_count = prefix.chars().count();
|
||||
let bullet_len = chars_count;
|
||||
prefix = &BULLETS[..bullet_len];
|
||||
chars = (1 << bullet_len) - 1;
|
||||
tabs = 0;
|
||||
}
|
||||
|
||||
let chunk = Chunk {
|
||||
text: prefix,
|
||||
tabs,
|
||||
chars,
|
||||
..self.input_chunk.clone()
|
||||
};
|
||||
|
||||
|
||||
@@ -134,19 +134,33 @@ impl<'a> Iterator for CustomHighlightsChunks<'a> {
|
||||
|
||||
let chunk = self
|
||||
.buffer_chunk
|
||||
.get_or_insert_with(|| self.buffer_chunks.next().unwrap());
|
||||
.get_or_insert_with(|| self.buffer_chunks.next().unwrap_or_default());
|
||||
if chunk.text.is_empty() {
|
||||
*chunk = self.buffer_chunks.next().unwrap();
|
||||
*chunk = self.buffer_chunks.next()?;
|
||||
}
|
||||
|
||||
let (prefix, suffix) = chunk
|
||||
.text
|
||||
.split_at(chunk.text.len().min(next_highlight_endpoint - self.offset));
|
||||
let split_idx = chunk.text.len().min(next_highlight_endpoint - self.offset);
|
||||
let (prefix, suffix) = chunk.text.split_at(split_idx);
|
||||
|
||||
let (chars, tabs) = if split_idx == 128 {
|
||||
let output = (chunk.chars, chunk.tabs);
|
||||
chunk.chars = 0;
|
||||
chunk.tabs = 0;
|
||||
output
|
||||
} else {
|
||||
let mask = (1 << split_idx) - 1;
|
||||
let output = (chunk.chars & mask, chunk.tabs & mask);
|
||||
chunk.chars = chunk.chars >> split_idx;
|
||||
chunk.tabs = chunk.tabs >> split_idx;
|
||||
output
|
||||
};
|
||||
|
||||
chunk.text = suffix;
|
||||
self.offset += prefix.len();
|
||||
let mut prefix = Chunk {
|
||||
text: prefix,
|
||||
chars,
|
||||
tabs,
|
||||
..chunk.clone()
|
||||
};
|
||||
if !self.active_highlights.is_empty() {
|
||||
@@ -173,3 +187,140 @@ impl Ord for HighlightEndpoint {
|
||||
.then_with(|| self.style.is_some().cmp(&other.style.is_some()))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{any::TypeId, sync::Arc};
|
||||
|
||||
use super::*;
|
||||
use crate::MultiBuffer;
|
||||
use gpui::App;
|
||||
use rand::prelude::*;
|
||||
use util::RandomCharIter;
|
||||
|
||||
#[gpui::test(iterations = 100)]
|
||||
fn test_random_chunk_bitmaps(cx: &mut App, mut rng: StdRng) {
|
||||
// Generate random buffer using existing test infrastructure
|
||||
let len = rng.random_range(10..10000);
|
||||
let buffer = if rng.random() {
|
||||
let text = RandomCharIter::new(&mut rng).take(len).collect::<String>();
|
||||
MultiBuffer::build_simple(&text, cx)
|
||||
} else {
|
||||
MultiBuffer::build_random(&mut rng, cx)
|
||||
};
|
||||
|
||||
let buffer_snapshot = buffer.read(cx).snapshot(cx);
|
||||
|
||||
// Create random highlights
|
||||
let mut highlights = sum_tree::TreeMap::default();
|
||||
let highlight_count = rng.random_range(1..10);
|
||||
|
||||
for _i in 0..highlight_count {
|
||||
let style = HighlightStyle {
|
||||
color: Some(gpui::Hsla {
|
||||
h: rng.random::<f32>(),
|
||||
s: rng.random::<f32>(),
|
||||
l: rng.random::<f32>(),
|
||||
a: 1.0,
|
||||
}),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let mut ranges = Vec::new();
|
||||
let range_count = rng.random_range(1..10);
|
||||
let text = buffer_snapshot.text();
|
||||
for _ in 0..range_count {
|
||||
if buffer_snapshot.len() == 0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut start = rng.random_range(0..=buffer_snapshot.len().saturating_sub(10));
|
||||
|
||||
while !text.is_char_boundary(start) {
|
||||
start = start.saturating_sub(1);
|
||||
}
|
||||
|
||||
let end_end = buffer_snapshot.len().min(start + 100);
|
||||
let mut end = rng.random_range(start..=end_end);
|
||||
while !text.is_char_boundary(end) {
|
||||
end = end.saturating_sub(1);
|
||||
}
|
||||
|
||||
if start < end {
|
||||
start = end;
|
||||
}
|
||||
let start_anchor = buffer_snapshot.anchor_before(start);
|
||||
let end_anchor = buffer_snapshot.anchor_after(end);
|
||||
ranges.push(start_anchor..end_anchor);
|
||||
}
|
||||
|
||||
let type_id = TypeId::of::<()>(); // Simple type ID for testing
|
||||
highlights.insert(HighlightKey::Type(type_id), Arc::new((style, ranges)));
|
||||
}
|
||||
|
||||
// Get all chunks and verify their bitmaps
|
||||
let chunks =
|
||||
CustomHighlightsChunks::new(0..buffer_snapshot.len(), false, None, &buffer_snapshot);
|
||||
|
||||
for chunk in chunks {
|
||||
let chunk_text = chunk.text;
|
||||
let chars_bitmap = chunk.chars;
|
||||
let tabs_bitmap = chunk.tabs;
|
||||
|
||||
// Check empty chunks have empty bitmaps
|
||||
if chunk_text.is_empty() {
|
||||
assert_eq!(
|
||||
chars_bitmap, 0,
|
||||
"Empty chunk should have empty chars bitmap"
|
||||
);
|
||||
assert_eq!(tabs_bitmap, 0, "Empty chunk should have empty tabs bitmap");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Verify that chunk text doesn't exceed 128 bytes
|
||||
assert!(
|
||||
chunk_text.len() <= 128,
|
||||
"Chunk text length {} exceeds 128 bytes",
|
||||
chunk_text.len()
|
||||
);
|
||||
|
||||
// Verify chars bitmap
|
||||
let char_indices = chunk_text
|
||||
.char_indices()
|
||||
.map(|(i, _)| i)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
for byte_idx in 0..chunk_text.len() {
|
||||
let should_have_bit = char_indices.contains(&byte_idx);
|
||||
let has_bit = chars_bitmap & (1 << byte_idx) != 0;
|
||||
|
||||
if has_bit != should_have_bit {
|
||||
eprintln!("Chunk text bytes: {:?}", chunk_text.as_bytes());
|
||||
eprintln!("Char indices: {:?}", char_indices);
|
||||
eprintln!("Chars bitmap: {:#b}", chars_bitmap);
|
||||
assert_eq!(
|
||||
has_bit, should_have_bit,
|
||||
"Chars bitmap mismatch at byte index {} in chunk {:?}. Expected bit: {}, Got bit: {}",
|
||||
byte_idx, chunk_text, should_have_bit, has_bit
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Verify tabs bitmap
|
||||
for (byte_idx, byte) in chunk_text.bytes().enumerate() {
|
||||
let is_tab = byte == b'\t';
|
||||
let has_bit = tabs_bitmap & (1 << byte_idx) != 0;
|
||||
|
||||
if has_bit != is_tab {
|
||||
eprintln!("Chunk text bytes: {:?}", chunk_text.as_bytes());
|
||||
eprintln!("Tabs bitmap: {:#b}", tabs_bitmap);
|
||||
assert_eq!(
|
||||
has_bit, is_tab,
|
||||
"Tabs bitmap mismatch at byte index {} in chunk {:?}. Byte: {:?}, Expected bit: {}, Got bit: {}",
|
||||
byte_idx, chunk_text, byte as char, is_tab, has_bit
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -320,13 +320,13 @@ impl FoldMapWriter<'_> {
|
||||
/// Decides where the fold indicators should be; also tracks parts of a source file that are currently folded.
|
||||
///
|
||||
/// See the [`display_map` module documentation](crate::display_map) for more information.
|
||||
pub(crate) struct FoldMap {
|
||||
pub struct FoldMap {
|
||||
snapshot: FoldSnapshot,
|
||||
next_fold_id: FoldId,
|
||||
}
|
||||
|
||||
impl FoldMap {
|
||||
pub(crate) fn new(inlay_snapshot: InlaySnapshot) -> (Self, FoldSnapshot) {
|
||||
pub fn new(inlay_snapshot: InlaySnapshot) -> (Self, FoldSnapshot) {
|
||||
let this = Self {
|
||||
snapshot: FoldSnapshot {
|
||||
folds: SumTree::new(&inlay_snapshot.buffer),
|
||||
@@ -360,7 +360,7 @@ impl FoldMap {
|
||||
(self.snapshot.clone(), edits)
|
||||
}
|
||||
|
||||
pub fn write(
|
||||
pub(crate) fn write(
|
||||
&mut self,
|
||||
inlay_snapshot: InlaySnapshot,
|
||||
edits: Vec<InlayEdit>,
|
||||
@@ -529,6 +529,7 @@ impl FoldMap {
|
||||
},
|
||||
placeholder: Some(TransformPlaceholder {
|
||||
text: ELLIPSIS,
|
||||
chars: 1,
|
||||
renderer: ChunkRenderer {
|
||||
id: ChunkRendererId::Fold(fold.id),
|
||||
render: Arc::new(move |cx| {
|
||||
@@ -872,6 +873,14 @@ impl FoldSnapshot {
|
||||
.flat_map(|chunk| chunk.text.chars())
|
||||
}
|
||||
|
||||
pub fn chunks_at(&self, start: FoldPoint) -> FoldChunks<'_> {
|
||||
self.chunks(
|
||||
start.to_offset(self)..self.len(),
|
||||
false,
|
||||
Highlights::default(),
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn clip_offset(&self, offset: FoldOffset, bias: Bias) -> FoldOffset {
|
||||
if offset > self.len() {
|
||||
@@ -1034,6 +1043,7 @@ struct Transform {
|
||||
#[derive(Clone, Debug)]
|
||||
struct TransformPlaceholder {
|
||||
text: &'static str,
|
||||
chars: u128,
|
||||
renderer: ChunkRenderer,
|
||||
}
|
||||
|
||||
@@ -1274,6 +1284,10 @@ pub struct Chunk<'a> {
|
||||
pub is_inlay: bool,
|
||||
/// An optional recipe for how the chunk should be presented.
|
||||
pub renderer: Option<ChunkRenderer>,
|
||||
/// Bitmap of tab character locations in chunk
|
||||
pub tabs: u128,
|
||||
/// Bitmap of character locations in chunk
|
||||
pub chars: u128,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||
@@ -1391,6 +1405,7 @@ impl<'a> Iterator for FoldChunks<'a> {
|
||||
self.output_offset.0 += placeholder.text.len();
|
||||
return Some(Chunk {
|
||||
text: placeholder.text,
|
||||
chars: placeholder.chars,
|
||||
renderer: Some(placeholder.renderer.clone()),
|
||||
..Default::default()
|
||||
});
|
||||
@@ -1429,6 +1444,16 @@ impl<'a> Iterator for FoldChunks<'a> {
|
||||
chunk.text = &chunk.text
|
||||
[(self.inlay_offset - buffer_chunk_start).0..(chunk_end - buffer_chunk_start).0];
|
||||
|
||||
let bit_end = (chunk_end - buffer_chunk_start).0;
|
||||
let mask = if bit_end >= 128 {
|
||||
u128::MAX
|
||||
} else {
|
||||
(1u128 << bit_end) - 1
|
||||
};
|
||||
|
||||
chunk.tabs = (chunk.tabs >> (self.inlay_offset - buffer_chunk_start).0) & mask;
|
||||
chunk.chars = (chunk.chars >> (self.inlay_offset - buffer_chunk_start).0) & mask;
|
||||
|
||||
if chunk_end == transform_end {
|
||||
self.transform_cursor.next();
|
||||
} else if chunk_end == buffer_chunk_end {
|
||||
@@ -1439,6 +1464,8 @@ impl<'a> Iterator for FoldChunks<'a> {
|
||||
self.output_offset.0 += chunk.text.len();
|
||||
return Some(Chunk {
|
||||
text: chunk.text,
|
||||
tabs: chunk.tabs,
|
||||
chars: chunk.chars,
|
||||
syntax_highlight_id: chunk.syntax_highlight_id,
|
||||
highlight_style: chunk.highlight_style,
|
||||
diagnostic_severity: chunk.diagnostic_severity,
|
||||
@@ -2072,6 +2099,97 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 100)]
|
||||
fn test_random_chunk_bitmaps(cx: &mut gpui::App, mut rng: StdRng) {
|
||||
init_test(cx);
|
||||
|
||||
// Generate random buffer using existing test infrastructure
|
||||
let text_len = rng.random_range(0..10000);
|
||||
let buffer = if rng.random() {
|
||||
let text = RandomCharIter::new(&mut rng)
|
||||
.take(text_len)
|
||||
.collect::<String>();
|
||||
MultiBuffer::build_simple(&text, cx)
|
||||
} else {
|
||||
MultiBuffer::build_random(&mut rng, cx)
|
||||
};
|
||||
let buffer_snapshot = buffer.read(cx).snapshot(cx);
|
||||
let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot);
|
||||
let (mut fold_map, _) = FoldMap::new(inlay_snapshot.clone());
|
||||
|
||||
// Perform random mutations
|
||||
let mutation_count = rng.random_range(1..10);
|
||||
for _ in 0..mutation_count {
|
||||
fold_map.randomly_mutate(&mut rng);
|
||||
}
|
||||
|
||||
let (snapshot, _) = fold_map.read(inlay_snapshot, vec![]);
|
||||
|
||||
// Get all chunks and verify their bitmaps
|
||||
let chunks = snapshot.chunks(
|
||||
FoldOffset(0)..FoldOffset(snapshot.len().0),
|
||||
false,
|
||||
Highlights::default(),
|
||||
);
|
||||
|
||||
for chunk in chunks {
|
||||
let chunk_text = chunk.text;
|
||||
let chars_bitmap = chunk.chars;
|
||||
let tabs_bitmap = chunk.tabs;
|
||||
|
||||
// Check empty chunks have empty bitmaps
|
||||
if chunk_text.is_empty() {
|
||||
assert_eq!(
|
||||
chars_bitmap, 0,
|
||||
"Empty chunk should have empty chars bitmap"
|
||||
);
|
||||
assert_eq!(tabs_bitmap, 0, "Empty chunk should have empty tabs bitmap");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Verify that chunk text doesn't exceed 128 bytes
|
||||
assert!(
|
||||
chunk_text.len() <= 128,
|
||||
"Chunk text length {} exceeds 128 bytes",
|
||||
chunk_text.len()
|
||||
);
|
||||
|
||||
// Verify chars bitmap
|
||||
let char_indices = chunk_text
|
||||
.char_indices()
|
||||
.map(|(i, _)| i)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
for byte_idx in 0..chunk_text.len() {
|
||||
let should_have_bit = char_indices.contains(&byte_idx);
|
||||
let has_bit = chars_bitmap & (1 << byte_idx) != 0;
|
||||
|
||||
if has_bit != should_have_bit {
|
||||
eprintln!("Chunk text bytes: {:?}", chunk_text.as_bytes());
|
||||
eprintln!("Char indices: {:?}", char_indices);
|
||||
eprintln!("Chars bitmap: {:#b}", chars_bitmap);
|
||||
assert_eq!(
|
||||
has_bit, should_have_bit,
|
||||
"Chars bitmap mismatch at byte index {} in chunk {:?}. Expected bit: {}, Got bit: {}",
|
||||
byte_idx, chunk_text, should_have_bit, has_bit
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Verify tabs bitmap
|
||||
for (byte_idx, byte) in chunk_text.bytes().enumerate() {
|
||||
let is_tab = byte == b'\t';
|
||||
let has_bit = tabs_bitmap & (1 << byte_idx) != 0;
|
||||
|
||||
assert_eq!(
|
||||
has_bit, is_tab,
|
||||
"Tabs bitmap mismatch at byte index {} in chunk {:?}. Byte: {:?}, Expected bit: {}, Got bit: {}",
|
||||
byte_idx, chunk_text, byte as char, is_tab, has_bit
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn init_test(cx: &mut gpui::App) {
|
||||
let store = SettingsStore::test(cx);
|
||||
cx.set_global(store);
|
||||
|
||||
@@ -11,7 +11,7 @@ use std::{
|
||||
sync::Arc,
|
||||
};
|
||||
use sum_tree::{Bias, Cursor, Dimensions, SumTree};
|
||||
use text::{Patch, Rope};
|
||||
use text::{ChunkBitmaps, Patch, Rope};
|
||||
use ui::{ActiveTheme, IntoElement as _, ParentElement as _, Styled as _, div};
|
||||
|
||||
use super::{Highlights, custom_highlights::CustomHighlightsChunks, fold_map::ChunkRendererId};
|
||||
@@ -245,8 +245,9 @@ pub struct InlayChunks<'a> {
|
||||
transforms: Cursor<'a, Transform, Dimensions<InlayOffset, usize>>,
|
||||
buffer_chunks: CustomHighlightsChunks<'a>,
|
||||
buffer_chunk: Option<Chunk<'a>>,
|
||||
inlay_chunks: Option<text::Chunks<'a>>,
|
||||
inlay_chunk: Option<&'a str>,
|
||||
inlay_chunks: Option<text::ChunkWithBitmaps<'a>>,
|
||||
/// text, char bitmap, tabs bitmap
|
||||
inlay_chunk: Option<ChunkBitmaps<'a>>,
|
||||
output_offset: InlayOffset,
|
||||
max_output_offset: InlayOffset,
|
||||
highlight_styles: HighlightStyles,
|
||||
@@ -316,11 +317,25 @@ impl<'a> Iterator for InlayChunks<'a> {
|
||||
|
||||
let (prefix, suffix) = chunk.text.split_at(split_index);
|
||||
|
||||
let (chars, tabs) = if split_index == 128 {
|
||||
let output = (chunk.chars, chunk.tabs);
|
||||
chunk.chars = 0;
|
||||
chunk.tabs = 0;
|
||||
output
|
||||
} else {
|
||||
let mask = (1 << split_index) - 1;
|
||||
let output = (chunk.chars & mask, chunk.tabs & mask);
|
||||
chunk.chars = chunk.chars >> split_index;
|
||||
chunk.tabs = chunk.tabs >> split_index;
|
||||
output
|
||||
};
|
||||
chunk.text = suffix;
|
||||
self.output_offset.0 += prefix.len();
|
||||
InlayChunk {
|
||||
chunk: Chunk {
|
||||
text: prefix,
|
||||
chars,
|
||||
tabs,
|
||||
..chunk.clone()
|
||||
},
|
||||
renderer: None,
|
||||
@@ -397,9 +412,14 @@ impl<'a> Iterator for InlayChunks<'a> {
|
||||
let start = offset_in_inlay;
|
||||
let end = cmp::min(self.max_output_offset, self.transforms.end().0)
|
||||
- self.transforms.start().0;
|
||||
inlay.text.chunks_in_range(start.0..end.0)
|
||||
let chunks = inlay.text.chunks_in_range(start.0..end.0);
|
||||
text::ChunkWithBitmaps(chunks)
|
||||
});
|
||||
let inlay_chunk = self
|
||||
let ChunkBitmaps {
|
||||
text: inlay_chunk,
|
||||
chars,
|
||||
tabs,
|
||||
} = self
|
||||
.inlay_chunk
|
||||
.get_or_insert_with(|| inlay_chunks.next().unwrap());
|
||||
|
||||
@@ -421,6 +441,20 @@ impl<'a> Iterator for InlayChunks<'a> {
|
||||
|
||||
let (chunk, remainder) = inlay_chunk.split_at(split_index);
|
||||
*inlay_chunk = remainder;
|
||||
|
||||
let (chars, tabs) = if split_index == 128 {
|
||||
let output = (*chars, *tabs);
|
||||
*chars = 0;
|
||||
*tabs = 0;
|
||||
output
|
||||
} else {
|
||||
let mask = (1 << split_index as u32) - 1;
|
||||
let output = (*chars & mask, *tabs & mask);
|
||||
*chars = *chars >> split_index;
|
||||
*tabs = *tabs >> split_index;
|
||||
output
|
||||
};
|
||||
|
||||
if inlay_chunk.is_empty() {
|
||||
self.inlay_chunk = None;
|
||||
}
|
||||
@@ -430,6 +464,8 @@ impl<'a> Iterator for InlayChunks<'a> {
|
||||
InlayChunk {
|
||||
chunk: Chunk {
|
||||
text: chunk,
|
||||
chars,
|
||||
tabs,
|
||||
highlight_style,
|
||||
is_inlay: true,
|
||||
..Chunk::default()
|
||||
@@ -1224,6 +1260,7 @@ mod tests {
|
||||
use std::{any::TypeId, cmp::Reverse, env, sync::Arc};
|
||||
use sum_tree::TreeMap;
|
||||
use text::Patch;
|
||||
use util::RandomCharIter;
|
||||
use util::post_inc;
|
||||
|
||||
#[test]
|
||||
@@ -1966,6 +2003,102 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 100)]
|
||||
fn test_random_chunk_bitmaps(cx: &mut gpui::App, mut rng: StdRng) {
|
||||
init_test(cx);
|
||||
|
||||
// Generate random buffer using existing test infrastructure
|
||||
let text_len = rng.random_range(0..10000);
|
||||
let buffer = if rng.random() {
|
||||
let text = RandomCharIter::new(&mut rng)
|
||||
.take(text_len)
|
||||
.collect::<String>();
|
||||
MultiBuffer::build_simple(&text, cx)
|
||||
} else {
|
||||
MultiBuffer::build_random(&mut rng, cx)
|
||||
};
|
||||
|
||||
let buffer_snapshot = buffer.read(cx).snapshot(cx);
|
||||
let (mut inlay_map, _) = InlayMap::new(buffer_snapshot.clone());
|
||||
|
||||
// Perform random mutations to add inlays
|
||||
let mut next_inlay_id = 0;
|
||||
let mutation_count = rng.random_range(1..10);
|
||||
for _ in 0..mutation_count {
|
||||
inlay_map.randomly_mutate(&mut next_inlay_id, &mut rng);
|
||||
}
|
||||
|
||||
let (snapshot, _) = inlay_map.sync(buffer_snapshot, vec![]);
|
||||
|
||||
// Get all chunks and verify their bitmaps
|
||||
let chunks = snapshot.chunks(
|
||||
InlayOffset(0)..InlayOffset(snapshot.len().0),
|
||||
false,
|
||||
Highlights::default(),
|
||||
);
|
||||
|
||||
for chunk in chunks.into_iter().map(|inlay_chunk| inlay_chunk.chunk) {
|
||||
let chunk_text = chunk.text;
|
||||
let chars_bitmap = chunk.chars;
|
||||
let tabs_bitmap = chunk.tabs;
|
||||
|
||||
// Check empty chunks have empty bitmaps
|
||||
if chunk_text.is_empty() {
|
||||
assert_eq!(
|
||||
chars_bitmap, 0,
|
||||
"Empty chunk should have empty chars bitmap"
|
||||
);
|
||||
assert_eq!(tabs_bitmap, 0, "Empty chunk should have empty tabs bitmap");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Verify that chunk text doesn't exceed 128 bytes
|
||||
assert!(
|
||||
chunk_text.len() <= 128,
|
||||
"Chunk text length {} exceeds 128 bytes",
|
||||
chunk_text.len()
|
||||
);
|
||||
|
||||
// Verify chars bitmap
|
||||
let char_indices = chunk_text
|
||||
.char_indices()
|
||||
.map(|(i, _)| i)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
for byte_idx in 0..chunk_text.len() {
|
||||
let should_have_bit = char_indices.contains(&byte_idx);
|
||||
let has_bit = chars_bitmap & (1 << byte_idx) != 0;
|
||||
|
||||
if has_bit != should_have_bit {
|
||||
eprintln!("Chunk text bytes: {:?}", chunk_text.as_bytes());
|
||||
eprintln!("Char indices: {:?}", char_indices);
|
||||
eprintln!("Chars bitmap: {:#b}", chars_bitmap);
|
||||
assert_eq!(
|
||||
has_bit, should_have_bit,
|
||||
"Chars bitmap mismatch at byte index {} in chunk {:?}. Expected bit: {}, Got bit: {}",
|
||||
byte_idx, chunk_text, should_have_bit, has_bit
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Verify tabs bitmap
|
||||
for (byte_idx, byte) in chunk_text.bytes().enumerate() {
|
||||
let is_tab = byte == b'\t';
|
||||
let has_bit = tabs_bitmap & (1 << byte_idx) != 0;
|
||||
|
||||
if has_bit != is_tab {
|
||||
eprintln!("Chunk text bytes: {:?}", chunk_text.as_bytes());
|
||||
eprintln!("Tabs bitmap: {:#b}", tabs_bitmap);
|
||||
assert_eq!(
|
||||
has_bit, is_tab,
|
||||
"Tabs bitmap mismatch at byte index {} in chunk {:?}. Byte: {:?}, Expected bit: {}, Got bit: {}",
|
||||
byte_idx, chunk_text, byte as char, is_tab, has_bit
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn init_test(cx: &mut App) {
|
||||
let store = SettingsStore::test(cx);
|
||||
cx.set_global(store);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -970,9 +970,25 @@ impl<'a> Iterator for WrapChunks<'a> {
|
||||
}
|
||||
|
||||
let (prefix, suffix) = self.input_chunk.text.split_at(input_len);
|
||||
|
||||
let (chars, tabs) = if input_len == 128 {
|
||||
let output = (self.input_chunk.chars, self.input_chunk.tabs);
|
||||
self.input_chunk.chars = 0;
|
||||
self.input_chunk.tabs = 0;
|
||||
output
|
||||
} else {
|
||||
let mask = (1 << input_len) - 1;
|
||||
let output = (self.input_chunk.chars & mask, self.input_chunk.tabs & mask);
|
||||
self.input_chunk.chars = self.input_chunk.chars >> input_len;
|
||||
self.input_chunk.tabs = self.input_chunk.tabs >> input_len;
|
||||
output
|
||||
};
|
||||
|
||||
self.input_chunk.text = suffix;
|
||||
Some(Chunk {
|
||||
text: prefix,
|
||||
chars,
|
||||
tabs,
|
||||
..self.input_chunk.clone()
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user