From 237efc841e372466d83842a3c8c5296c6b0e3646 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Fri, 1 Dec 2023 19:39:53 +0100 Subject: [PATCH] Another batch of tests --- crates/editor2/src/display_map/block_map.rs | 1338 +++++++++---------- crates/editor2/src/editor_tests.rs | 652 ++++----- 2 files changed, 987 insertions(+), 1003 deletions(-) diff --git a/crates/editor2/src/display_map/block_map.rs b/crates/editor2/src/display_map/block_map.rs index 00778c2eddc8eec3cccf3a3a2a9fe89355d26ded..64e46549fd6c7b9ae2576ca68d7c0f2af52b750e 100644 --- a/crates/editor2/src/display_map/block_map.rs +++ b/crates/editor2/src/display_map/block_map.rs @@ -988,680 +988,664 @@ fn offset_for_row(s: &str, target: u32) -> (u32, usize) { (row, offset) } -// #[cfg(test)] -// mod tests { -// use super::*; -// use crate::display_map::inlay_map::InlayMap; -// use crate::display_map::{fold_map::FoldMap, tab_map::TabMap, wrap_map::WrapMap}; -// use gpui::Element; -// use multi_buffer::MultiBuffer; -// use rand::prelude::*; -// use settings::SettingsStore; -// use std::env; -// use util::RandomCharIter; - -// #[gpui::test] -// fn test_offset_for_row() { -// assert_eq!(offset_for_row("", 0), (0, 0)); -// assert_eq!(offset_for_row("", 1), (0, 0)); -// assert_eq!(offset_for_row("abcd", 0), (0, 0)); -// assert_eq!(offset_for_row("abcd", 1), (0, 4)); -// assert_eq!(offset_for_row("\n", 0), (0, 0)); -// assert_eq!(offset_for_row("\n", 1), (1, 1)); -// assert_eq!(offset_for_row("abc\ndef\nghi", 0), (0, 0)); -// assert_eq!(offset_for_row("abc\ndef\nghi", 1), (1, 4)); -// assert_eq!(offset_for_row("abc\ndef\nghi", 2), (2, 8)); -// assert_eq!(offset_for_row("abc\ndef\nghi", 3), (2, 11)); -// } - -// #[gpui::test] -// fn test_basic_blocks(cx: &mut gpui::AppContext) { -// init_test(cx); - -// let family_id = cx -// .font_cache() -// .load_family(&["Helvetica"], &Default::default()) -// .unwrap(); -// let font_id = cx -// .font_cache() -// .select_font(family_id, &Default::default()) -// .unwrap(); - -// let text = "aaa\nbbb\nccc\nddd"; - -// let buffer = MultiBuffer::build_simple(text, cx); -// let buffer_snapshot = buffer.read(cx).snapshot(cx); -// let subscription = buffer.update(cx, |buffer, _| buffer.subscribe()); -// let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone()); -// let (mut fold_map, fold_snapshot) = FoldMap::new(inlay_snapshot); -// let (mut tab_map, tab_snapshot) = TabMap::new(fold_snapshot, 1.try_into().unwrap()); -// let (wrap_map, wraps_snapshot) = WrapMap::new(tab_snapshot, font_id, 14.0, None, cx); -// let mut block_map = BlockMap::new(wraps_snapshot.clone(), 1, 1); - -// let mut writer = block_map.write(wraps_snapshot.clone(), Default::default()); -// let block_ids = writer.insert(vec![ -// BlockProperties { -// style: BlockStyle::Fixed, -// position: buffer_snapshot.anchor_after(Point::new(1, 0)), -// height: 1, -// disposition: BlockDisposition::Above, -// render: Arc::new(|_| Empty::new().into_any_named("block 1")), -// }, -// BlockProperties { -// style: BlockStyle::Fixed, -// position: buffer_snapshot.anchor_after(Point::new(1, 2)), -// height: 2, -// disposition: BlockDisposition::Above, -// render: Arc::new(|_| Empty::new().into_any_named("block 2")), -// }, -// BlockProperties { -// style: BlockStyle::Fixed, -// position: buffer_snapshot.anchor_after(Point::new(3, 3)), -// height: 3, -// disposition: BlockDisposition::Below, -// render: Arc::new(|_| Empty::new().into_any_named("block 3")), -// }, -// ]); - -// let snapshot = block_map.read(wraps_snapshot, Default::default()); -// assert_eq!(snapshot.text(), "aaa\n\n\n\nbbb\nccc\nddd\n\n\n"); - -// let blocks = snapshot -// .blocks_in_range(0..8) -// .map(|(start_row, block)| { -// let block = block.as_custom().unwrap(); -// (start_row..start_row + block.height as u32, block.id) -// }) -// .collect::>(); - -// // When multiple blocks are on the same line, the newer blocks appear first. -// assert_eq!( -// blocks, -// &[ -// (1..2, block_ids[0]), -// (2..4, block_ids[1]), -// (7..10, block_ids[2]), -// ] -// ); - -// assert_eq!( -// snapshot.to_block_point(WrapPoint::new(0, 3)), -// BlockPoint::new(0, 3) -// ); -// assert_eq!( -// snapshot.to_block_point(WrapPoint::new(1, 0)), -// BlockPoint::new(4, 0) -// ); -// assert_eq!( -// snapshot.to_block_point(WrapPoint::new(3, 3)), -// BlockPoint::new(6, 3) -// ); - -// assert_eq!( -// snapshot.to_wrap_point(BlockPoint::new(0, 3)), -// WrapPoint::new(0, 3) -// ); -// assert_eq!( -// snapshot.to_wrap_point(BlockPoint::new(1, 0)), -// WrapPoint::new(1, 0) -// ); -// assert_eq!( -// snapshot.to_wrap_point(BlockPoint::new(3, 0)), -// WrapPoint::new(1, 0) -// ); -// assert_eq!( -// snapshot.to_wrap_point(BlockPoint::new(7, 0)), -// WrapPoint::new(3, 3) -// ); - -// assert_eq!( -// snapshot.clip_point(BlockPoint::new(1, 0), Bias::Left), -// BlockPoint::new(0, 3) -// ); -// assert_eq!( -// snapshot.clip_point(BlockPoint::new(1, 0), Bias::Right), -// BlockPoint::new(4, 0) -// ); -// assert_eq!( -// snapshot.clip_point(BlockPoint::new(1, 1), Bias::Left), -// BlockPoint::new(0, 3) -// ); -// assert_eq!( -// snapshot.clip_point(BlockPoint::new(1, 1), Bias::Right), -// BlockPoint::new(4, 0) -// ); -// assert_eq!( -// snapshot.clip_point(BlockPoint::new(4, 0), Bias::Left), -// BlockPoint::new(4, 0) -// ); -// assert_eq!( -// snapshot.clip_point(BlockPoint::new(4, 0), Bias::Right), -// BlockPoint::new(4, 0) -// ); -// assert_eq!( -// snapshot.clip_point(BlockPoint::new(6, 3), Bias::Left), -// BlockPoint::new(6, 3) -// ); -// assert_eq!( -// snapshot.clip_point(BlockPoint::new(6, 3), Bias::Right), -// BlockPoint::new(6, 3) -// ); -// assert_eq!( -// snapshot.clip_point(BlockPoint::new(7, 0), Bias::Left), -// BlockPoint::new(6, 3) -// ); -// assert_eq!( -// snapshot.clip_point(BlockPoint::new(7, 0), Bias::Right), -// BlockPoint::new(6, 3) -// ); - -// assert_eq!( -// snapshot.buffer_rows(0).collect::>(), -// &[ -// Some(0), -// None, -// None, -// None, -// Some(1), -// Some(2), -// Some(3), -// None, -// None, -// None -// ] -// ); - -// // Insert a line break, separating two block decorations into separate lines. -// let buffer_snapshot = buffer.update(cx, |buffer, cx| { -// buffer.edit([(Point::new(1, 1)..Point::new(1, 1), "!!!\n")], None, cx); -// buffer.snapshot(cx) -// }); - -// let (inlay_snapshot, inlay_edits) = -// inlay_map.sync(buffer_snapshot, subscription.consume().into_inner()); -// let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits); -// let (tab_snapshot, tab_edits) = -// tab_map.sync(fold_snapshot, fold_edits, 4.try_into().unwrap()); -// let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| { -// wrap_map.sync(tab_snapshot, tab_edits, cx) -// }); -// let snapshot = block_map.read(wraps_snapshot, wrap_edits); -// assert_eq!(snapshot.text(), "aaa\n\nb!!!\n\n\nbb\nccc\nddd\n\n\n"); -// } - -// #[gpui::test] -// fn test_blocks_on_wrapped_lines(cx: &mut gpui::AppContext) { -// init_test(cx); - -// let family_id = cx -// .font_cache() -// .load_family(&["Helvetica"], &Default::default()) -// .unwrap(); -// let font_id = cx -// .font_cache() -// .select_font(family_id, &Default::default()) -// .unwrap(); - -// let text = "one two three\nfour five six\nseven eight"; - -// let buffer = MultiBuffer::build_simple(text, cx); -// let buffer_snapshot = buffer.read(cx).snapshot(cx); -// let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone()); -// let (_, fold_snapshot) = FoldMap::new(inlay_snapshot); -// let (_, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap()); -// let (_, wraps_snapshot) = WrapMap::new(tab_snapshot, font_id, 14.0, Some(60.), cx); -// let mut block_map = BlockMap::new(wraps_snapshot.clone(), 1, 1); - -// let mut writer = block_map.write(wraps_snapshot.clone(), Default::default()); -// writer.insert(vec![ -// BlockProperties { -// style: BlockStyle::Fixed, -// position: buffer_snapshot.anchor_after(Point::new(1, 12)), -// disposition: BlockDisposition::Above, -// render: Arc::new(|_| Empty::new().into_any_named("block 1")), -// height: 1, -// }, -// BlockProperties { -// style: BlockStyle::Fixed, -// position: buffer_snapshot.anchor_after(Point::new(1, 1)), -// disposition: BlockDisposition::Below, -// render: Arc::new(|_| Empty::new().into_any_named("block 2")), -// height: 1, -// }, -// ]); - -// // Blocks with an 'above' disposition go above their corresponding buffer line. -// // Blocks with a 'below' disposition go below their corresponding buffer line. -// let snapshot = block_map.read(wraps_snapshot, Default::default()); -// assert_eq!( -// snapshot.text(), -// "one two \nthree\n\nfour five \nsix\n\nseven \neight" -// ); -// } - -// #[gpui::test(iterations = 100)] -// fn test_random_blocks(cx: &mut gpui::AppContext, mut rng: StdRng) { -// init_test(cx); - -// let operations = env::var("OPERATIONS") -// .map(|i| i.parse().expect("invalid `OPERATIONS` variable")) -// .unwrap_or(10); - -// let wrap_width = if rng.gen_bool(0.2) { -// None -// } else { -// Some(rng.gen_range(0.0..=100.0)) -// }; -// let tab_size = 1.try_into().unwrap(); -// let family_id = cx -// .font_cache() -// .load_family(&["Helvetica"], &Default::default()) -// .unwrap(); -// let font_id = cx -// .font_cache() -// .select_font(family_id, &Default::default()) -// .unwrap(); -// let font_size = 14.0; -// let buffer_start_header_height = rng.gen_range(1..=5); -// let excerpt_header_height = rng.gen_range(1..=5); - -// log::info!("Wrap width: {:?}", wrap_width); -// log::info!("Excerpt Header Height: {:?}", excerpt_header_height); - -// let buffer = if rng.gen() { -// let len = rng.gen_range(0..10); -// let text = RandomCharIter::new(&mut rng).take(len).collect::(); -// log::info!("initial buffer text: {:?}", text); -// MultiBuffer::build_simple(&text, cx) -// } else { -// MultiBuffer::build_random(&mut rng, cx) -// }; - -// let mut buffer_snapshot = buffer.read(cx).snapshot(cx); -// let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone()); -// let (mut fold_map, fold_snapshot) = FoldMap::new(inlay_snapshot); -// let (mut tab_map, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap()); -// let (wrap_map, wraps_snapshot) = -// WrapMap::new(tab_snapshot, font_id, font_size, wrap_width, cx); -// let mut block_map = BlockMap::new( -// wraps_snapshot, -// buffer_start_header_height, -// excerpt_header_height, -// ); -// let mut custom_blocks = Vec::new(); - -// for _ in 0..operations { -// let mut buffer_edits = Vec::new(); -// match rng.gen_range(0..=100) { -// 0..=19 => { -// let wrap_width = if rng.gen_bool(0.2) { -// None -// } else { -// Some(rng.gen_range(0.0..=100.0)) -// }; -// log::info!("Setting wrap width to {:?}", wrap_width); -// wrap_map.update(cx, |map, cx| map.set_wrap_width(wrap_width, cx)); -// } -// 20..=39 => { -// let block_count = rng.gen_range(1..=5); -// let block_properties = (0..block_count) -// .map(|_| { -// let buffer = buffer.read(cx).read(cx); -// let position = buffer.anchor_after( -// buffer.clip_offset(rng.gen_range(0..=buffer.len()), Bias::Left), -// ); - -// let disposition = if rng.gen() { -// BlockDisposition::Above -// } else { -// BlockDisposition::Below -// }; -// let height = rng.gen_range(1..5); -// log::info!( -// "inserting block {:?} {:?} with height {}", -// disposition, -// position.to_point(&buffer), -// height -// ); -// BlockProperties { -// style: BlockStyle::Fixed, -// position, -// height, -// disposition, -// render: Arc::new(|_| Empty::new().into_any()), -// } -// }) -// .collect::>(); - -// let (inlay_snapshot, inlay_edits) = -// inlay_map.sync(buffer_snapshot.clone(), vec![]); -// let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits); -// let (tab_snapshot, tab_edits) = -// tab_map.sync(fold_snapshot, fold_edits, tab_size); -// let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| { -// wrap_map.sync(tab_snapshot, tab_edits, cx) -// }); -// let mut block_map = block_map.write(wraps_snapshot, wrap_edits); -// let block_ids = block_map.insert(block_properties.clone()); -// for (block_id, props) in block_ids.into_iter().zip(block_properties) { -// custom_blocks.push((block_id, props)); -// } -// } -// 40..=59 if !custom_blocks.is_empty() => { -// let block_count = rng.gen_range(1..=4.min(custom_blocks.len())); -// let block_ids_to_remove = (0..block_count) -// .map(|_| { -// custom_blocks -// .remove(rng.gen_range(0..custom_blocks.len())) -// .0 -// }) -// .collect(); - -// let (inlay_snapshot, inlay_edits) = -// inlay_map.sync(buffer_snapshot.clone(), vec![]); -// let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits); -// let (tab_snapshot, tab_edits) = -// tab_map.sync(fold_snapshot, fold_edits, tab_size); -// let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| { -// wrap_map.sync(tab_snapshot, tab_edits, cx) -// }); -// let mut block_map = block_map.write(wraps_snapshot, wrap_edits); -// block_map.remove(block_ids_to_remove); -// } -// _ => { -// buffer.update(cx, |buffer, cx| { -// let mutation_count = rng.gen_range(1..=5); -// let subscription = buffer.subscribe(); -// buffer.randomly_mutate(&mut rng, mutation_count, cx); -// buffer_snapshot = buffer.snapshot(cx); -// buffer_edits.extend(subscription.consume()); -// log::info!("buffer text: {:?}", buffer_snapshot.text()); -// }); -// } -// } - -// let (inlay_snapshot, inlay_edits) = -// inlay_map.sync(buffer_snapshot.clone(), buffer_edits); -// let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits); -// let (tab_snapshot, tab_edits) = tab_map.sync(fold_snapshot, fold_edits, tab_size); -// let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| { -// wrap_map.sync(tab_snapshot, tab_edits, cx) -// }); -// let blocks_snapshot = block_map.read(wraps_snapshot.clone(), wrap_edits); -// assert_eq!( -// blocks_snapshot.transforms.summary().input_rows, -// wraps_snapshot.max_point().row() + 1 -// ); -// log::info!("blocks text: {:?}", blocks_snapshot.text()); - -// let mut expected_blocks = Vec::new(); -// expected_blocks.extend(custom_blocks.iter().map(|(id, block)| { -// let mut position = block.position.to_point(&buffer_snapshot); -// match block.disposition { -// BlockDisposition::Above => { -// position.column = 0; -// } -// BlockDisposition::Below => { -// position.column = buffer_snapshot.line_len(position.row); -// } -// }; -// let row = wraps_snapshot.make_wrap_point(position, Bias::Left).row(); -// ( -// row, -// ExpectedBlock::Custom { -// disposition: block.disposition, -// id: *id, -// height: block.height, -// }, -// ) -// })); -// expected_blocks.extend(buffer_snapshot.excerpt_boundaries_in_range(0..).map( -// |boundary| { -// let position = -// wraps_snapshot.make_wrap_point(Point::new(boundary.row, 0), Bias::Left); -// ( -// position.row(), -// ExpectedBlock::ExcerptHeader { -// height: if boundary.starts_new_buffer { -// buffer_start_header_height -// } else { -// excerpt_header_height -// }, -// starts_new_buffer: boundary.starts_new_buffer, -// }, -// ) -// }, -// )); -// expected_blocks.sort_unstable(); -// let mut sorted_blocks_iter = expected_blocks.into_iter().peekable(); - -// let input_buffer_rows = buffer_snapshot.buffer_rows(0).collect::>(); -// let mut expected_buffer_rows = Vec::new(); -// let mut expected_text = String::new(); -// let mut expected_block_positions = Vec::new(); -// let input_text = wraps_snapshot.text(); -// for (row, input_line) in input_text.split('\n').enumerate() { -// let row = row as u32; -// if row > 0 { -// expected_text.push('\n'); -// } - -// let buffer_row = input_buffer_rows[wraps_snapshot -// .to_point(WrapPoint::new(row, 0), Bias::Left) -// .row as usize]; - -// while let Some((block_row, block)) = sorted_blocks_iter.peek() { -// if *block_row == row && block.disposition() == BlockDisposition::Above { -// let (_, block) = sorted_blocks_iter.next().unwrap(); -// let height = block.height() as usize; -// expected_block_positions -// .push((expected_text.matches('\n').count() as u32, block)); -// let text = "\n".repeat(height); -// expected_text.push_str(&text); -// for _ in 0..height { -// expected_buffer_rows.push(None); -// } -// } else { -// break; -// } -// } - -// let soft_wrapped = wraps_snapshot.to_tab_point(WrapPoint::new(row, 0)).column() > 0; -// expected_buffer_rows.push(if soft_wrapped { None } else { buffer_row }); -// expected_text.push_str(input_line); - -// while let Some((block_row, block)) = sorted_blocks_iter.peek() { -// if *block_row == row && block.disposition() == BlockDisposition::Below { -// let (_, block) = sorted_blocks_iter.next().unwrap(); -// let height = block.height() as usize; -// expected_block_positions -// .push((expected_text.matches('\n').count() as u32 + 1, block)); -// let text = "\n".repeat(height); -// expected_text.push_str(&text); -// for _ in 0..height { -// expected_buffer_rows.push(None); -// } -// } else { -// break; -// } -// } -// } - -// let expected_lines = expected_text.split('\n').collect::>(); -// let expected_row_count = expected_lines.len(); -// for start_row in 0..expected_row_count { -// let expected_text = expected_lines[start_row..].join("\n"); -// let actual_text = blocks_snapshot -// .chunks( -// start_row as u32..blocks_snapshot.max_point().row + 1, -// false, -// Highlights::default(), -// ) -// .map(|chunk| chunk.text) -// .collect::(); -// assert_eq!( -// actual_text, expected_text, -// "incorrect text starting from row {}", -// start_row -// ); -// assert_eq!( -// blocks_snapshot -// .buffer_rows(start_row as u32) -// .collect::>(), -// &expected_buffer_rows[start_row..] -// ); -// } - -// assert_eq!( -// blocks_snapshot -// .blocks_in_range(0..(expected_row_count as u32)) -// .map(|(row, block)| (row, block.clone().into())) -// .collect::>(), -// expected_block_positions -// ); - -// let mut expected_longest_rows = Vec::new(); -// let mut longest_line_len = -1_isize; -// for (row, line) in expected_lines.iter().enumerate() { -// let row = row as u32; - -// assert_eq!( -// blocks_snapshot.line_len(row), -// line.len() as u32, -// "invalid line len for row {}", -// row -// ); - -// let line_char_count = line.chars().count() as isize; -// match line_char_count.cmp(&longest_line_len) { -// Ordering::Less => {} -// Ordering::Equal => expected_longest_rows.push(row), -// Ordering::Greater => { -// longest_line_len = line_char_count; -// expected_longest_rows.clear(); -// expected_longest_rows.push(row); -// } -// } -// } - -// let longest_row = blocks_snapshot.longest_row(); -// assert!( -// expected_longest_rows.contains(&longest_row), -// "incorrect longest row {}. expected {:?} with length {}", -// longest_row, -// expected_longest_rows, -// longest_line_len, -// ); - -// for row in 0..=blocks_snapshot.wrap_snapshot.max_point().row() { -// let wrap_point = WrapPoint::new(row, 0); -// let block_point = blocks_snapshot.to_block_point(wrap_point); -// assert_eq!(blocks_snapshot.to_wrap_point(block_point), wrap_point); -// } - -// let mut block_point = BlockPoint::new(0, 0); -// for c in expected_text.chars() { -// let left_point = blocks_snapshot.clip_point(block_point, Bias::Left); -// let left_buffer_point = blocks_snapshot.to_point(left_point, Bias::Left); -// assert_eq!( -// blocks_snapshot.to_block_point(blocks_snapshot.to_wrap_point(left_point)), -// left_point -// ); -// assert_eq!( -// left_buffer_point, -// buffer_snapshot.clip_point(left_buffer_point, Bias::Right), -// "{:?} is not valid in buffer coordinates", -// left_point -// ); - -// let right_point = blocks_snapshot.clip_point(block_point, Bias::Right); -// let right_buffer_point = blocks_snapshot.to_point(right_point, Bias::Right); -// assert_eq!( -// blocks_snapshot.to_block_point(blocks_snapshot.to_wrap_point(right_point)), -// right_point -// ); -// assert_eq!( -// right_buffer_point, -// buffer_snapshot.clip_point(right_buffer_point, Bias::Left), -// "{:?} is not valid in buffer coordinates", -// right_point -// ); - -// if c == '\n' { -// block_point.0 += Point::new(1, 0); -// } else { -// block_point.column += c.len_utf8() as u32; -// } -// } -// } - -// #[derive(Debug, Eq, PartialEq, Ord, PartialOrd)] -// enum ExpectedBlock { -// ExcerptHeader { -// height: u8, -// starts_new_buffer: bool, -// }, -// Custom { -// disposition: BlockDisposition, -// id: BlockId, -// height: u8, -// }, -// } - -// impl ExpectedBlock { -// fn height(&self) -> u8 { -// match self { -// ExpectedBlock::ExcerptHeader { height, .. } => *height, -// ExpectedBlock::Custom { height, .. } => *height, -// } -// } - -// fn disposition(&self) -> BlockDisposition { -// match self { -// ExpectedBlock::ExcerptHeader { .. } => BlockDisposition::Above, -// ExpectedBlock::Custom { disposition, .. } => *disposition, -// } -// } -// } - -// impl From for ExpectedBlock { -// fn from(block: TransformBlock) -> Self { -// match block { -// TransformBlock::Custom(block) => ExpectedBlock::Custom { -// id: block.id, -// disposition: block.disposition, -// height: block.height, -// }, -// TransformBlock::ExcerptHeader { -// height, -// starts_new_buffer, -// .. -// } => ExpectedBlock::ExcerptHeader { -// height, -// starts_new_buffer, -// }, -// } -// } -// } -// } - -// fn init_test(cx: &mut gpui::AppContext) { -// cx.set_global(SettingsStore::test(cx)); -// theme::init(cx); -// } - -// impl TransformBlock { -// fn as_custom(&self) -> Option<&Block> { -// match self { -// TransformBlock::Custom(block) => Some(block), -// TransformBlock::ExcerptHeader { .. } => None, -// } -// } -// } - -// impl BlockSnapshot { -// fn to_point(&self, point: BlockPoint, bias: Bias) -> Point { -// self.wrap_snapshot.to_point(self.to_wrap_point(point), bias) -// } -// } -// } +#[cfg(test)] +mod tests { + use super::*; + use crate::display_map::inlay_map::InlayMap; + use crate::display_map::{fold_map::FoldMap, tab_map::TabMap, wrap_map::WrapMap}; + use gpui::{div, font, px, Element, Platform as _}; + use multi_buffer::MultiBuffer; + use rand::prelude::*; + use settings::SettingsStore; + use std::env; + use util::RandomCharIter; + + #[gpui::test] + fn test_offset_for_row() { + assert_eq!(offset_for_row("", 0), (0, 0)); + assert_eq!(offset_for_row("", 1), (0, 0)); + assert_eq!(offset_for_row("abcd", 0), (0, 0)); + assert_eq!(offset_for_row("abcd", 1), (0, 4)); + assert_eq!(offset_for_row("\n", 0), (0, 0)); + assert_eq!(offset_for_row("\n", 1), (1, 1)); + assert_eq!(offset_for_row("abc\ndef\nghi", 0), (0, 0)); + assert_eq!(offset_for_row("abc\ndef\nghi", 1), (1, 4)); + assert_eq!(offset_for_row("abc\ndef\nghi", 2), (2, 8)); + assert_eq!(offset_for_row("abc\ndef\nghi", 3), (2, 11)); + } + + #[gpui::test] + fn test_basic_blocks(cx: &mut gpui::TestAppContext) { + cx.update(|cx| init_test(cx)); + + let text = "aaa\nbbb\nccc\nddd"; + + let buffer = cx.update(|cx| MultiBuffer::build_simple(text, cx)); + let buffer_snapshot = cx.update(|cx| buffer.read(cx).snapshot(cx)); + let subscription = buffer.update(cx, |buffer, _| buffer.subscribe()); + let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone()); + let (mut fold_map, fold_snapshot) = FoldMap::new(inlay_snapshot); + let (mut tab_map, tab_snapshot) = TabMap::new(fold_snapshot, 1.try_into().unwrap()); + let (wrap_map, wraps_snapshot) = + cx.update(|cx| WrapMap::new(tab_snapshot, font("Helvetica"), px(14.0), None, cx)); + let mut block_map = BlockMap::new(wraps_snapshot.clone(), 1, 1); + + let mut writer = block_map.write(wraps_snapshot.clone(), Default::default()); + let block_ids = writer.insert(vec![ + BlockProperties { + style: BlockStyle::Fixed, + position: buffer_snapshot.anchor_after(Point::new(1, 0)), + height: 1, + disposition: BlockDisposition::Above, + render: Arc::new(|_| div().into_any()), + }, + BlockProperties { + style: BlockStyle::Fixed, + position: buffer_snapshot.anchor_after(Point::new(1, 2)), + height: 2, + disposition: BlockDisposition::Above, + render: Arc::new(|_| div().into_any()), + }, + BlockProperties { + style: BlockStyle::Fixed, + position: buffer_snapshot.anchor_after(Point::new(3, 3)), + height: 3, + disposition: BlockDisposition::Below, + render: Arc::new(|_| div().into_any()), + }, + ]); + + let snapshot = block_map.read(wraps_snapshot, Default::default()); + assert_eq!(snapshot.text(), "aaa\n\n\n\nbbb\nccc\nddd\n\n\n"); + + let blocks = snapshot + .blocks_in_range(0..8) + .map(|(start_row, block)| { + let block = block.as_custom().unwrap(); + (start_row..start_row + block.height as u32, block.id) + }) + .collect::>(); + + // When multiple blocks are on the same line, the newer blocks appear first. + assert_eq!( + blocks, + &[ + (1..2, block_ids[0]), + (2..4, block_ids[1]), + (7..10, block_ids[2]), + ] + ); + + assert_eq!( + snapshot.to_block_point(WrapPoint::new(0, 3)), + BlockPoint::new(0, 3) + ); + assert_eq!( + snapshot.to_block_point(WrapPoint::new(1, 0)), + BlockPoint::new(4, 0) + ); + assert_eq!( + snapshot.to_block_point(WrapPoint::new(3, 3)), + BlockPoint::new(6, 3) + ); + + assert_eq!( + snapshot.to_wrap_point(BlockPoint::new(0, 3)), + WrapPoint::new(0, 3) + ); + assert_eq!( + snapshot.to_wrap_point(BlockPoint::new(1, 0)), + WrapPoint::new(1, 0) + ); + assert_eq!( + snapshot.to_wrap_point(BlockPoint::new(3, 0)), + WrapPoint::new(1, 0) + ); + assert_eq!( + snapshot.to_wrap_point(BlockPoint::new(7, 0)), + WrapPoint::new(3, 3) + ); + + assert_eq!( + snapshot.clip_point(BlockPoint::new(1, 0), Bias::Left), + BlockPoint::new(0, 3) + ); + assert_eq!( + snapshot.clip_point(BlockPoint::new(1, 0), Bias::Right), + BlockPoint::new(4, 0) + ); + assert_eq!( + snapshot.clip_point(BlockPoint::new(1, 1), Bias::Left), + BlockPoint::new(0, 3) + ); + assert_eq!( + snapshot.clip_point(BlockPoint::new(1, 1), Bias::Right), + BlockPoint::new(4, 0) + ); + assert_eq!( + snapshot.clip_point(BlockPoint::new(4, 0), Bias::Left), + BlockPoint::new(4, 0) + ); + assert_eq!( + snapshot.clip_point(BlockPoint::new(4, 0), Bias::Right), + BlockPoint::new(4, 0) + ); + assert_eq!( + snapshot.clip_point(BlockPoint::new(6, 3), Bias::Left), + BlockPoint::new(6, 3) + ); + assert_eq!( + snapshot.clip_point(BlockPoint::new(6, 3), Bias::Right), + BlockPoint::new(6, 3) + ); + assert_eq!( + snapshot.clip_point(BlockPoint::new(7, 0), Bias::Left), + BlockPoint::new(6, 3) + ); + assert_eq!( + snapshot.clip_point(BlockPoint::new(7, 0), Bias::Right), + BlockPoint::new(6, 3) + ); + + assert_eq!( + snapshot.buffer_rows(0).collect::>(), + &[ + Some(0), + None, + None, + None, + Some(1), + Some(2), + Some(3), + None, + None, + None + ] + ); + + // Insert a line break, separating two block decorations into separate lines. + let buffer_snapshot = buffer.update(cx, |buffer, cx| { + buffer.edit([(Point::new(1, 1)..Point::new(1, 1), "!!!\n")], None, cx); + buffer.snapshot(cx) + }); + + let (inlay_snapshot, inlay_edits) = + inlay_map.sync(buffer_snapshot, subscription.consume().into_inner()); + let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits); + let (tab_snapshot, tab_edits) = + tab_map.sync(fold_snapshot, fold_edits, 4.try_into().unwrap()); + let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| { + wrap_map.sync(tab_snapshot, tab_edits, cx) + }); + let snapshot = block_map.read(wraps_snapshot, wrap_edits); + assert_eq!(snapshot.text(), "aaa\n\nb!!!\n\n\nbb\nccc\nddd\n\n\n"); + } + + #[gpui::test] + fn test_blocks_on_wrapped_lines(cx: &mut gpui::TestAppContext) { + cx.update(|cx| init_test(cx)); + + let font_id = cx + .test_platform + .text_system() + .font_id(&font("Helvetica")) + .unwrap(); + + let text = "one two three\nfour five six\nseven eight"; + + let buffer = cx.update(|cx| MultiBuffer::build_simple(text, cx)); + let buffer_snapshot = cx.update(|cx| buffer.read(cx).snapshot(cx)); + let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone()); + let (_, fold_snapshot) = FoldMap::new(inlay_snapshot); + let (_, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap()); + let (_, wraps_snapshot) = cx.update(|cx| { + WrapMap::new(tab_snapshot, font("Helvetica"), px(14.0), Some(px(60.)), cx) + }); + let mut block_map = BlockMap::new(wraps_snapshot.clone(), 1, 1); + + let mut writer = block_map.write(wraps_snapshot.clone(), Default::default()); + writer.insert(vec![ + BlockProperties { + style: BlockStyle::Fixed, + position: buffer_snapshot.anchor_after(Point::new(1, 12)), + disposition: BlockDisposition::Above, + render: Arc::new(|_| div().into_any()), + height: 1, + }, + BlockProperties { + style: BlockStyle::Fixed, + position: buffer_snapshot.anchor_after(Point::new(1, 1)), + disposition: BlockDisposition::Below, + render: Arc::new(|_| div().into_any()), + height: 1, + }, + ]); + + // Blocks with an 'above' disposition go above their corresponding buffer line. + // Blocks with a 'below' disposition go below their corresponding buffer line. + let snapshot = block_map.read(wraps_snapshot, Default::default()); + assert_eq!( + snapshot.text(), + "one two \nthree\n\nfour five \nsix\n\nseven \neight" + ); + } + + #[gpui::test(iterations = 100)] + fn test_random_blocks(cx: &mut gpui::TestAppContext, mut rng: StdRng) { + cx.update(|cx| init_test(cx)); + + let operations = env::var("OPERATIONS") + .map(|i| i.parse().expect("invalid `OPERATIONS` variable")) + .unwrap_or(10); + + let wrap_width = if rng.gen_bool(0.2) { + None + } else { + Some(px(rng.gen_range(0.0..=100.0))) + }; + let tab_size = 1.try_into().unwrap(); + let font_size = px(14.0); + let buffer_start_header_height = rng.gen_range(1..=5); + let excerpt_header_height = rng.gen_range(1..=5); + + log::info!("Wrap width: {:?}", wrap_width); + log::info!("Excerpt Header Height: {:?}", excerpt_header_height); + + let buffer = if rng.gen() { + let len = rng.gen_range(0..10); + let text = RandomCharIter::new(&mut rng).take(len).collect::(); + log::info!("initial buffer text: {:?}", text); + cx.update(|cx| MultiBuffer::build_simple(&text, cx)) + } else { + cx.update(|cx| MultiBuffer::build_random(&mut rng, cx)) + }; + + let mut buffer_snapshot = cx.update(|cx| buffer.read(cx).snapshot(cx)); + let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone()); + let (mut fold_map, fold_snapshot) = FoldMap::new(inlay_snapshot); + let (mut tab_map, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap()); + let (wrap_map, wraps_snapshot) = cx + .update(|cx| WrapMap::new(tab_snapshot, font("Helvetica"), font_size, wrap_width, cx)); + let mut block_map = BlockMap::new( + wraps_snapshot, + buffer_start_header_height, + excerpt_header_height, + ); + let mut custom_blocks = Vec::new(); + + for _ in 0..operations { + let mut buffer_edits = Vec::new(); + match rng.gen_range(0..=100) { + 0..=19 => { + let wrap_width = if rng.gen_bool(0.2) { + None + } else { + Some(px(rng.gen_range(0.0..=100.0))) + }; + log::info!("Setting wrap width to {:?}", wrap_width); + wrap_map.update(cx, |map, cx| map.set_wrap_width(wrap_width, cx)); + } + 20..=39 => { + let block_count = rng.gen_range(1..=5); + let block_properties = (0..block_count) + .map(|_| { + let buffer = cx.update(|cx| buffer.read(cx).read(cx).clone()); + let position = buffer.anchor_after( + buffer.clip_offset(rng.gen_range(0..=buffer.len()), Bias::Left), + ); + + let disposition = if rng.gen() { + BlockDisposition::Above + } else { + BlockDisposition::Below + }; + let height = rng.gen_range(1..5); + log::info!( + "inserting block {:?} {:?} with height {}", + disposition, + position.to_point(&buffer), + height + ); + BlockProperties { + style: BlockStyle::Fixed, + position, + height, + disposition, + render: Arc::new(|_| div().into_any()), + } + }) + .collect::>(); + + let (inlay_snapshot, inlay_edits) = + inlay_map.sync(buffer_snapshot.clone(), vec![]); + let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits); + let (tab_snapshot, tab_edits) = + tab_map.sync(fold_snapshot, fold_edits, tab_size); + let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| { + wrap_map.sync(tab_snapshot, tab_edits, cx) + }); + let mut block_map = block_map.write(wraps_snapshot, wrap_edits); + let block_ids = block_map.insert(block_properties.clone()); + for (block_id, props) in block_ids.into_iter().zip(block_properties) { + custom_blocks.push((block_id, props)); + } + } + 40..=59 if !custom_blocks.is_empty() => { + let block_count = rng.gen_range(1..=4.min(custom_blocks.len())); + let block_ids_to_remove = (0..block_count) + .map(|_| { + custom_blocks + .remove(rng.gen_range(0..custom_blocks.len())) + .0 + }) + .collect(); + + let (inlay_snapshot, inlay_edits) = + inlay_map.sync(buffer_snapshot.clone(), vec![]); + let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits); + let (tab_snapshot, tab_edits) = + tab_map.sync(fold_snapshot, fold_edits, tab_size); + let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| { + wrap_map.sync(tab_snapshot, tab_edits, cx) + }); + let mut block_map = block_map.write(wraps_snapshot, wrap_edits); + block_map.remove(block_ids_to_remove); + } + _ => { + buffer.update(cx, |buffer, cx| { + let mutation_count = rng.gen_range(1..=5); + let subscription = buffer.subscribe(); + buffer.randomly_mutate(&mut rng, mutation_count, cx); + buffer_snapshot = buffer.snapshot(cx); + buffer_edits.extend(subscription.consume()); + log::info!("buffer text: {:?}", buffer_snapshot.text()); + }); + } + } + + let (inlay_snapshot, inlay_edits) = + inlay_map.sync(buffer_snapshot.clone(), buffer_edits); + let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits); + let (tab_snapshot, tab_edits) = tab_map.sync(fold_snapshot, fold_edits, tab_size); + let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| { + wrap_map.sync(tab_snapshot, tab_edits, cx) + }); + let blocks_snapshot = block_map.read(wraps_snapshot.clone(), wrap_edits); + assert_eq!( + blocks_snapshot.transforms.summary().input_rows, + wraps_snapshot.max_point().row() + 1 + ); + log::info!("blocks text: {:?}", blocks_snapshot.text()); + + let mut expected_blocks = Vec::new(); + expected_blocks.extend(custom_blocks.iter().map(|(id, block)| { + let mut position = block.position.to_point(&buffer_snapshot); + match block.disposition { + BlockDisposition::Above => { + position.column = 0; + } + BlockDisposition::Below => { + position.column = buffer_snapshot.line_len(position.row); + } + }; + let row = wraps_snapshot.make_wrap_point(position, Bias::Left).row(); + ( + row, + ExpectedBlock::Custom { + disposition: block.disposition, + id: *id, + height: block.height, + }, + ) + })); + expected_blocks.extend(buffer_snapshot.excerpt_boundaries_in_range(0..).map( + |boundary| { + let position = + wraps_snapshot.make_wrap_point(Point::new(boundary.row, 0), Bias::Left); + ( + position.row(), + ExpectedBlock::ExcerptHeader { + height: if boundary.starts_new_buffer { + buffer_start_header_height + } else { + excerpt_header_height + }, + starts_new_buffer: boundary.starts_new_buffer, + }, + ) + }, + )); + expected_blocks.sort_unstable(); + let mut sorted_blocks_iter = expected_blocks.into_iter().peekable(); + + let input_buffer_rows = buffer_snapshot.buffer_rows(0).collect::>(); + let mut expected_buffer_rows = Vec::new(); + let mut expected_text = String::new(); + let mut expected_block_positions = Vec::new(); + let input_text = wraps_snapshot.text(); + for (row, input_line) in input_text.split('\n').enumerate() { + let row = row as u32; + if row > 0 { + expected_text.push('\n'); + } + + let buffer_row = input_buffer_rows[wraps_snapshot + .to_point(WrapPoint::new(row, 0), Bias::Left) + .row as usize]; + + while let Some((block_row, block)) = sorted_blocks_iter.peek() { + if *block_row == row && block.disposition() == BlockDisposition::Above { + let (_, block) = sorted_blocks_iter.next().unwrap(); + let height = block.height() as usize; + expected_block_positions + .push((expected_text.matches('\n').count() as u32, block)); + let text = "\n".repeat(height); + expected_text.push_str(&text); + for _ in 0..height { + expected_buffer_rows.push(None); + } + } else { + break; + } + } + + let soft_wrapped = wraps_snapshot.to_tab_point(WrapPoint::new(row, 0)).column() > 0; + expected_buffer_rows.push(if soft_wrapped { None } else { buffer_row }); + expected_text.push_str(input_line); + + while let Some((block_row, block)) = sorted_blocks_iter.peek() { + if *block_row == row && block.disposition() == BlockDisposition::Below { + let (_, block) = sorted_blocks_iter.next().unwrap(); + let height = block.height() as usize; + expected_block_positions + .push((expected_text.matches('\n').count() as u32 + 1, block)); + let text = "\n".repeat(height); + expected_text.push_str(&text); + for _ in 0..height { + expected_buffer_rows.push(None); + } + } else { + break; + } + } + } + + let expected_lines = expected_text.split('\n').collect::>(); + let expected_row_count = expected_lines.len(); + for start_row in 0..expected_row_count { + let expected_text = expected_lines[start_row..].join("\n"); + let actual_text = blocks_snapshot + .chunks( + start_row as u32..blocks_snapshot.max_point().row + 1, + false, + Highlights::default(), + ) + .map(|chunk| chunk.text) + .collect::(); + assert_eq!( + actual_text, expected_text, + "incorrect text starting from row {}", + start_row + ); + assert_eq!( + blocks_snapshot + .buffer_rows(start_row as u32) + .collect::>(), + &expected_buffer_rows[start_row..] + ); + } + + assert_eq!( + blocks_snapshot + .blocks_in_range(0..(expected_row_count as u32)) + .map(|(row, block)| (row, block.clone().into())) + .collect::>(), + expected_block_positions + ); + + let mut expected_longest_rows = Vec::new(); + let mut longest_line_len = -1_isize; + for (row, line) in expected_lines.iter().enumerate() { + let row = row as u32; + + assert_eq!( + blocks_snapshot.line_len(row), + line.len() as u32, + "invalid line len for row {}", + row + ); + + let line_char_count = line.chars().count() as isize; + match line_char_count.cmp(&longest_line_len) { + Ordering::Less => {} + Ordering::Equal => expected_longest_rows.push(row), + Ordering::Greater => { + longest_line_len = line_char_count; + expected_longest_rows.clear(); + expected_longest_rows.push(row); + } + } + } + + let longest_row = blocks_snapshot.longest_row(); + assert!( + expected_longest_rows.contains(&longest_row), + "incorrect longest row {}. expected {:?} with length {}", + longest_row, + expected_longest_rows, + longest_line_len, + ); + + for row in 0..=blocks_snapshot.wrap_snapshot.max_point().row() { + let wrap_point = WrapPoint::new(row, 0); + let block_point = blocks_snapshot.to_block_point(wrap_point); + assert_eq!(blocks_snapshot.to_wrap_point(block_point), wrap_point); + } + + let mut block_point = BlockPoint::new(0, 0); + for c in expected_text.chars() { + let left_point = blocks_snapshot.clip_point(block_point, Bias::Left); + let left_buffer_point = blocks_snapshot.to_point(left_point, Bias::Left); + assert_eq!( + blocks_snapshot.to_block_point(blocks_snapshot.to_wrap_point(left_point)), + left_point + ); + assert_eq!( + left_buffer_point, + buffer_snapshot.clip_point(left_buffer_point, Bias::Right), + "{:?} is not valid in buffer coordinates", + left_point + ); + + let right_point = blocks_snapshot.clip_point(block_point, Bias::Right); + let right_buffer_point = blocks_snapshot.to_point(right_point, Bias::Right); + assert_eq!( + blocks_snapshot.to_block_point(blocks_snapshot.to_wrap_point(right_point)), + right_point + ); + assert_eq!( + right_buffer_point, + buffer_snapshot.clip_point(right_buffer_point, Bias::Left), + "{:?} is not valid in buffer coordinates", + right_point + ); + + if c == '\n' { + block_point.0 += Point::new(1, 0); + } else { + block_point.column += c.len_utf8() as u32; + } + } + } + + #[derive(Debug, Eq, PartialEq, Ord, PartialOrd)] + enum ExpectedBlock { + ExcerptHeader { + height: u8, + starts_new_buffer: bool, + }, + Custom { + disposition: BlockDisposition, + id: BlockId, + height: u8, + }, + } + + impl ExpectedBlock { + fn height(&self) -> u8 { + match self { + ExpectedBlock::ExcerptHeader { height, .. } => *height, + ExpectedBlock::Custom { height, .. } => *height, + } + } + + fn disposition(&self) -> BlockDisposition { + match self { + ExpectedBlock::ExcerptHeader { .. } => BlockDisposition::Above, + ExpectedBlock::Custom { disposition, .. } => *disposition, + } + } + } + + impl From for ExpectedBlock { + fn from(block: TransformBlock) -> Self { + match block { + TransformBlock::Custom(block) => ExpectedBlock::Custom { + id: block.id, + disposition: block.disposition, + height: block.height, + }, + TransformBlock::ExcerptHeader { + height, + starts_new_buffer, + .. + } => ExpectedBlock::ExcerptHeader { + height, + starts_new_buffer, + }, + } + } + } + } + + fn init_test(cx: &mut gpui::AppContext) { + let settings = SettingsStore::test(cx); + cx.set_global(settings); + theme::init(theme::LoadThemes::JustBase, cx); + } + + impl TransformBlock { + fn as_custom(&self) -> Option<&Block> { + match self { + TransformBlock::Custom(block) => Some(block), + TransformBlock::ExcerptHeader { .. } => None, + } + } + } + + impl BlockSnapshot { + fn to_point(&self, point: BlockPoint, bias: Bias) -> Point { + self.wrap_snapshot.to_point(self.to_wrap_point(point), bias) + } + } +} diff --git a/crates/editor2/src/editor_tests.rs b/crates/editor2/src/editor_tests.rs index 265bde908b564fcb34549cfea42f377e10837dbc..fab223ae230586d2ced268462ff5577bebf6c2c5 100644 --- a/crates/editor2/src/editor_tests.rs +++ b/crates/editor2/src/editor_tests.rs @@ -4809,114 +4809,113 @@ async fn test_delete_autoclose_pair(cx: &mut gpui::TestAppContext) { } // todo!(select_anchor_ranges) -// #[gpui::test] -// async fn test_snippets(cx: &mut gpui::TestAppContext) { -// init_test(cx, |_| {}); +#[gpui::test] +async fn test_snippets(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); -// let (text, insertion_ranges) = marked_text_ranges( -// indoc! {" -// a.ˇ b -// a.ˇ b -// a.ˇ b -// "}, -// false, -// ); + let (text, insertion_ranges) = marked_text_ranges( + indoc! {" + a.ˇ b + a.ˇ b + a.ˇ b + "}, + false, + ); -// let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx)); -// let (editor, mut cx) = cx.add_window_view(|cx| build_editor(buffer, cx)); -// let cx = &mut cx; + let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx)); + let (editor, mut cx) = cx.add_window_view(|cx| build_editor(buffer, cx)); -// editor.update(cx, |editor, cx| { -// let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap(); + editor.update(cx, |editor, cx| { + let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap(); -// editor -// .insert_snippet(&insertion_ranges, snippet, cx) -// .unwrap(); + editor + .insert_snippet(&insertion_ranges, snippet, cx) + .unwrap(); -// fn assert(editor: &mut Editor, cx: &mut ViewContext, marked_text: &str) { -// let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false); -// assert_eq!(editor.text(cx), expected_text); -// assert_eq!(editor.selections.ranges::(cx), selection_ranges); -// } + fn assert(editor: &mut Editor, cx: &mut ViewContext, marked_text: &str) { + let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false); + assert_eq!(editor.text(cx), expected_text); + assert_eq!(editor.selections.ranges::(cx), selection_ranges); + } -// assert( -// editor, -// cx, -// indoc! {" -// a.f(«one», two, «three») b -// a.f(«one», two, «three») b -// a.f(«one», two, «three») b -// "}, -// ); + assert( + editor, + cx, + indoc! {" + a.f(«one», two, «three») b + a.f(«one», two, «three») b + a.f(«one», two, «three») b + "}, + ); -// // Can't move earlier than the first tab stop -// assert!(!editor.move_to_prev_snippet_tabstop(cx)); -// assert( -// editor, -// cx, -// indoc! {" -// a.f(«one», two, «three») b -// a.f(«one», two, «three») b -// a.f(«one», two, «three») b -// "}, -// ); + // Can't move earlier than the first tab stop + assert!(!editor.move_to_prev_snippet_tabstop(cx)); + assert( + editor, + cx, + indoc! {" + a.f(«one», two, «three») b + a.f(«one», two, «three») b + a.f(«one», two, «three») b + "}, + ); -// assert!(editor.move_to_next_snippet_tabstop(cx)); -// assert( -// editor, -// cx, -// indoc! {" -// a.f(one, «two», three) b -// a.f(one, «two», three) b -// a.f(one, «two», three) b -// "}, -// ); + assert!(editor.move_to_next_snippet_tabstop(cx)); + assert( + editor, + cx, + indoc! {" + a.f(one, «two», three) b + a.f(one, «two», three) b + a.f(one, «two», three) b + "}, + ); -// editor.move_to_prev_snippet_tabstop(cx); -// assert( -// editor, -// cx, -// indoc! {" -// a.f(«one», two, «three») b -// a.f(«one», two, «three») b -// a.f(«one», two, «three») b -// "}, -// ); + editor.move_to_prev_snippet_tabstop(cx); + assert( + editor, + cx, + indoc! {" + a.f(«one», two, «three») b + a.f(«one», two, «three») b + a.f(«one», two, «three») b + "}, + ); -// assert!(editor.move_to_next_snippet_tabstop(cx)); -// assert( -// editor, -// cx, -// indoc! {" -// a.f(one, «two», three) b -// a.f(one, «two», three) b -// a.f(one, «two», three) b -// "}, -// ); -// assert!(editor.move_to_next_snippet_tabstop(cx)); -// assert( -// editor, -// cx, -// indoc! {" -// a.f(one, two, three)ˇ b -// a.f(one, two, three)ˇ b -// a.f(one, two, three)ˇ b -// "}, -// ); + assert!(editor.move_to_next_snippet_tabstop(cx)); + assert( + editor, + cx, + indoc! {" + a.f(one, «two», three) b + a.f(one, «two», three) b + a.f(one, «two», three) b + "}, + ); + assert!(editor.move_to_next_snippet_tabstop(cx)); + assert( + editor, + cx, + indoc! {" + a.f(one, two, three)ˇ b + a.f(one, two, three)ˇ b + a.f(one, two, three)ˇ b + "}, + ); -// // As soon as the last tab stop is reached, snippet state is gone -// editor.move_to_prev_snippet_tabstop(cx); -// assert( -// editor, -// cx, -// indoc! {" -// a.f(one, two, three)ˇ b -// a.f(one, two, three)ˇ b -// a.f(one, two, three)ˇ b -// "}, -// ); -// }); -// } + // As soon as the last tab stop is reached, snippet state is gone + editor.move_to_prev_snippet_tabstop(cx); + assert( + editor, + cx, + indoc! {" + a.f(one, two, three)ˇ b + a.f(one, two, three)ˇ b + a.f(one, two, three)ˇ b + "}, + ); + }); +} #[gpui::test] async fn test_document_format_during_save(cx: &mut gpui::TestAppContext) { @@ -7046,255 +7045,256 @@ async fn test_move_to_enclosing_bracket(cx: &mut gpui::TestAppContext) { } // todo!(completions) -// #[gpui::test(iterations = 10)] -// async fn test_copilot(executor: BackgroundExecutor, cx: &mut gpui::TestAppContext) { -// init_test(cx, |_| {}); +#[gpui::test(iterations = 10)] +async fn test_copilot(executor: BackgroundExecutor, cx: &mut gpui::TestAppContext) { + // flaky + init_test(cx, |_| {}); -// let (copilot, copilot_lsp) = Copilot::fake(cx); -// cx.update(|cx| cx.set_global(copilot)); -// let mut cx = EditorLspTestContext::new_rust( -// lsp::ServerCapabilities { -// completion_provider: Some(lsp::CompletionOptions { -// trigger_characters: Some(vec![".".to_string(), ":".to_string()]), -// ..Default::default() -// }), -// ..Default::default() -// }, -// cx, -// ) -// .await; + let (copilot, copilot_lsp) = Copilot::fake(cx); + cx.update(|cx| cx.set_global(copilot)); + let mut cx = EditorLspTestContext::new_rust( + lsp::ServerCapabilities { + completion_provider: Some(lsp::CompletionOptions { + trigger_characters: Some(vec![".".to_string(), ":".to_string()]), + ..Default::default() + }), + ..Default::default() + }, + cx, + ) + .await; -// // When inserting, ensure autocompletion is favored over Copilot suggestions. -// cx.set_state(indoc! {" -// oneˇ -// two -// three -// "}); -// cx.simulate_keystroke("."); -// let _ = handle_completion_request( -// &mut cx, -// indoc! {" -// one.|<> -// two -// three -// "}, -// vec!["completion_a", "completion_b"], -// ); -// handle_copilot_completion_request( -// &copilot_lsp, -// vec![copilot::request::Completion { -// text: "one.copilot1".into(), -// range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)), -// ..Default::default() -// }], -// vec![], -// ); -// executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); -// cx.update_editor(|editor, cx| { -// assert!(editor.context_menu_visible()); -// assert!(!editor.has_active_copilot_suggestion(cx)); + // When inserting, ensure autocompletion is favored over Copilot suggestions. + cx.set_state(indoc! {" + oneˇ + two + three + "}); + cx.simulate_keystroke("."); + let _ = handle_completion_request( + &mut cx, + indoc! {" + one.|<> + two + three + "}, + vec!["completion_a", "completion_b"], + ); + handle_copilot_completion_request( + &copilot_lsp, + vec![copilot::request::Completion { + text: "one.copilot1".into(), + range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)), + ..Default::default() + }], + vec![], + ); + executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); + cx.update_editor(|editor, cx| { + assert!(editor.context_menu_visible()); + assert!(!editor.has_active_copilot_suggestion(cx)); -// // Confirming a completion inserts it and hides the context menu, without showing -// // the copilot suggestion afterwards. -// editor -// .confirm_completion(&Default::default(), cx) -// .unwrap() -// .detach(); -// assert!(!editor.context_menu_visible()); -// assert!(!editor.has_active_copilot_suggestion(cx)); -// assert_eq!(editor.text(cx), "one.completion_a\ntwo\nthree\n"); -// assert_eq!(editor.display_text(cx), "one.completion_a\ntwo\nthree\n"); -// }); + // Confirming a completion inserts it and hides the context menu, without showing + // the copilot suggestion afterwards. + editor + .confirm_completion(&Default::default(), cx) + .unwrap() + .detach(); + assert!(!editor.context_menu_visible()); + assert!(!editor.has_active_copilot_suggestion(cx)); + assert_eq!(editor.text(cx), "one.completion_a\ntwo\nthree\n"); + assert_eq!(editor.display_text(cx), "one.completion_a\ntwo\nthree\n"); + }); -// // Ensure Copilot suggestions are shown right away if no autocompletion is available. -// cx.set_state(indoc! {" -// oneˇ -// two -// three -// "}); -// cx.simulate_keystroke("."); -// let _ = handle_completion_request( -// &mut cx, -// indoc! {" -// one.|<> -// two -// three -// "}, -// vec![], -// ); -// handle_copilot_completion_request( -// &copilot_lsp, -// vec![copilot::request::Completion { -// text: "one.copilot1".into(), -// range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)), -// ..Default::default() -// }], -// vec![], -// ); -// executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); -// cx.update_editor(|editor, cx| { -// assert!(!editor.context_menu_visible()); -// assert!(editor.has_active_copilot_suggestion(cx)); -// assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n"); -// assert_eq!(editor.text(cx), "one.\ntwo\nthree\n"); -// }); + // Ensure Copilot suggestions are shown right away if no autocompletion is available. + cx.set_state(indoc! {" + oneˇ + two + three + "}); + cx.simulate_keystroke("."); + let _ = handle_completion_request( + &mut cx, + indoc! {" + one.|<> + two + three + "}, + vec![], + ); + handle_copilot_completion_request( + &copilot_lsp, + vec![copilot::request::Completion { + text: "one.copilot1".into(), + range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)), + ..Default::default() + }], + vec![], + ); + executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); + cx.update_editor(|editor, cx| { + assert!(!editor.context_menu_visible()); + assert!(editor.has_active_copilot_suggestion(cx)); + assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n"); + assert_eq!(editor.text(cx), "one.\ntwo\nthree\n"); + }); -// // Reset editor, and ensure autocompletion is still favored over Copilot suggestions. -// cx.set_state(indoc! {" -// oneˇ -// two -// three -// "}); -// cx.simulate_keystroke("."); -// let _ = handle_completion_request( -// &mut cx, -// indoc! {" -// one.|<> -// two -// three -// "}, -// vec!["completion_a", "completion_b"], -// ); -// handle_copilot_completion_request( -// &copilot_lsp, -// vec![copilot::request::Completion { -// text: "one.copilot1".into(), -// range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)), -// ..Default::default() -// }], -// vec![], -// ); -// executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); -// cx.update_editor(|editor, cx| { -// assert!(editor.context_menu_visible()); -// assert!(!editor.has_active_copilot_suggestion(cx)); + // Reset editor, and ensure autocompletion is still favored over Copilot suggestions. + cx.set_state(indoc! {" + oneˇ + two + three + "}); + cx.simulate_keystroke("."); + let _ = handle_completion_request( + &mut cx, + indoc! {" + one.|<> + two + three + "}, + vec!["completion_a", "completion_b"], + ); + handle_copilot_completion_request( + &copilot_lsp, + vec![copilot::request::Completion { + text: "one.copilot1".into(), + range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)), + ..Default::default() + }], + vec![], + ); + executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); + cx.update_editor(|editor, cx| { + assert!(editor.context_menu_visible()); + assert!(!editor.has_active_copilot_suggestion(cx)); -// // When hiding the context menu, the Copilot suggestion becomes visible. -// editor.hide_context_menu(cx); -// assert!(!editor.context_menu_visible()); -// assert!(editor.has_active_copilot_suggestion(cx)); -// assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n"); -// assert_eq!(editor.text(cx), "one.\ntwo\nthree\n"); -// }); + // When hiding the context menu, the Copilot suggestion becomes visible. + editor.hide_context_menu(cx); + assert!(!editor.context_menu_visible()); + assert!(editor.has_active_copilot_suggestion(cx)); + assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n"); + assert_eq!(editor.text(cx), "one.\ntwo\nthree\n"); + }); -// // Ensure existing completion is interpolated when inserting again. -// cx.simulate_keystroke("c"); -// executor.run_until_parked(); -// cx.update_editor(|editor, cx| { -// assert!(!editor.context_menu_visible()); -// assert!(editor.has_active_copilot_suggestion(cx)); -// assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n"); -// assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n"); -// }); + // Ensure existing completion is interpolated when inserting again. + cx.simulate_keystroke("c"); + executor.run_until_parked(); + cx.update_editor(|editor, cx| { + assert!(!editor.context_menu_visible()); + assert!(editor.has_active_copilot_suggestion(cx)); + assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n"); + assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n"); + }); -// // After debouncing, new Copilot completions should be requested. -// handle_copilot_completion_request( -// &copilot_lsp, -// vec![copilot::request::Completion { -// text: "one.copilot2".into(), -// range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 5)), -// ..Default::default() -// }], -// vec![], -// ); -// executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); -// cx.update_editor(|editor, cx| { -// assert!(!editor.context_menu_visible()); -// assert!(editor.has_active_copilot_suggestion(cx)); -// assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n"); -// assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n"); + // After debouncing, new Copilot completions should be requested. + handle_copilot_completion_request( + &copilot_lsp, + vec![copilot::request::Completion { + text: "one.copilot2".into(), + range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 5)), + ..Default::default() + }], + vec![], + ); + executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); + cx.update_editor(|editor, cx| { + assert!(!editor.context_menu_visible()); + assert!(editor.has_active_copilot_suggestion(cx)); + assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n"); + assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n"); -// // Canceling should remove the active Copilot suggestion. -// editor.cancel(&Default::default(), cx); -// assert!(!editor.has_active_copilot_suggestion(cx)); -// assert_eq!(editor.display_text(cx), "one.c\ntwo\nthree\n"); -// assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n"); + // Canceling should remove the active Copilot suggestion. + editor.cancel(&Default::default(), cx); + assert!(!editor.has_active_copilot_suggestion(cx)); + assert_eq!(editor.display_text(cx), "one.c\ntwo\nthree\n"); + assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n"); -// // After canceling, tabbing shouldn't insert the previously shown suggestion. -// editor.tab(&Default::default(), cx); -// assert!(!editor.has_active_copilot_suggestion(cx)); -// assert_eq!(editor.display_text(cx), "one.c \ntwo\nthree\n"); -// assert_eq!(editor.text(cx), "one.c \ntwo\nthree\n"); + // After canceling, tabbing shouldn't insert the previously shown suggestion. + editor.tab(&Default::default(), cx); + assert!(!editor.has_active_copilot_suggestion(cx)); + assert_eq!(editor.display_text(cx), "one.c \ntwo\nthree\n"); + assert_eq!(editor.text(cx), "one.c \ntwo\nthree\n"); -// // When undoing the previously active suggestion is shown again. -// editor.undo(&Default::default(), cx); -// assert!(editor.has_active_copilot_suggestion(cx)); -// assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n"); -// assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n"); -// }); + // When undoing the previously active suggestion is shown again. + editor.undo(&Default::default(), cx); + assert!(editor.has_active_copilot_suggestion(cx)); + assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n"); + assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n"); + }); -// // If an edit occurs outside of this editor, the suggestion is still correctly interpolated. -// cx.update_buffer(|buffer, cx| buffer.edit([(5..5, "o")], None, cx)); -// cx.update_editor(|editor, cx| { -// assert!(editor.has_active_copilot_suggestion(cx)); -// assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n"); -// assert_eq!(editor.text(cx), "one.co\ntwo\nthree\n"); + // If an edit occurs outside of this editor, the suggestion is still correctly interpolated. + cx.update_buffer(|buffer, cx| buffer.edit([(5..5, "o")], None, cx)); + cx.update_editor(|editor, cx| { + assert!(editor.has_active_copilot_suggestion(cx)); + assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n"); + assert_eq!(editor.text(cx), "one.co\ntwo\nthree\n"); -// // Tabbing when there is an active suggestion inserts it. -// editor.tab(&Default::default(), cx); -// assert!(!editor.has_active_copilot_suggestion(cx)); -// assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n"); -// assert_eq!(editor.text(cx), "one.copilot2\ntwo\nthree\n"); + // Tabbing when there is an active suggestion inserts it. + editor.tab(&Default::default(), cx); + assert!(!editor.has_active_copilot_suggestion(cx)); + assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n"); + assert_eq!(editor.text(cx), "one.copilot2\ntwo\nthree\n"); -// // When undoing the previously active suggestion is shown again. -// editor.undo(&Default::default(), cx); -// assert!(editor.has_active_copilot_suggestion(cx)); -// assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n"); -// assert_eq!(editor.text(cx), "one.co\ntwo\nthree\n"); + // When undoing the previously active suggestion is shown again. + editor.undo(&Default::default(), cx); + assert!(editor.has_active_copilot_suggestion(cx)); + assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n"); + assert_eq!(editor.text(cx), "one.co\ntwo\nthree\n"); -// // Hide suggestion. -// editor.cancel(&Default::default(), cx); -// assert!(!editor.has_active_copilot_suggestion(cx)); -// assert_eq!(editor.display_text(cx), "one.co\ntwo\nthree\n"); -// assert_eq!(editor.text(cx), "one.co\ntwo\nthree\n"); -// }); + // Hide suggestion. + editor.cancel(&Default::default(), cx); + assert!(!editor.has_active_copilot_suggestion(cx)); + assert_eq!(editor.display_text(cx), "one.co\ntwo\nthree\n"); + assert_eq!(editor.text(cx), "one.co\ntwo\nthree\n"); + }); -// // If an edit occurs outside of this editor but no suggestion is being shown, -// // we won't make it visible. -// cx.update_buffer(|buffer, cx| buffer.edit([(6..6, "p")], None, cx)); -// cx.update_editor(|editor, cx| { -// assert!(!editor.has_active_copilot_suggestion(cx)); -// assert_eq!(editor.display_text(cx), "one.cop\ntwo\nthree\n"); -// assert_eq!(editor.text(cx), "one.cop\ntwo\nthree\n"); -// }); + // If an edit occurs outside of this editor but no suggestion is being shown, + // we won't make it visible. + cx.update_buffer(|buffer, cx| buffer.edit([(6..6, "p")], None, cx)); + cx.update_editor(|editor, cx| { + assert!(!editor.has_active_copilot_suggestion(cx)); + assert_eq!(editor.display_text(cx), "one.cop\ntwo\nthree\n"); + assert_eq!(editor.text(cx), "one.cop\ntwo\nthree\n"); + }); -// // Reset the editor to verify how suggestions behave when tabbing on leading indentation. -// cx.update_editor(|editor, cx| { -// editor.set_text("fn foo() {\n \n}", cx); -// editor.change_selections(None, cx, |s| { -// s.select_ranges([Point::new(1, 2)..Point::new(1, 2)]) -// }); -// }); -// handle_copilot_completion_request( -// &copilot_lsp, -// vec![copilot::request::Completion { -// text: " let x = 4;".into(), -// range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 2)), -// ..Default::default() -// }], -// vec![], -// ); + // Reset the editor to verify how suggestions behave when tabbing on leading indentation. + cx.update_editor(|editor, cx| { + editor.set_text("fn foo() {\n \n}", cx); + editor.change_selections(None, cx, |s| { + s.select_ranges([Point::new(1, 2)..Point::new(1, 2)]) + }); + }); + handle_copilot_completion_request( + &copilot_lsp, + vec![copilot::request::Completion { + text: " let x = 4;".into(), + range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 2)), + ..Default::default() + }], + vec![], + ); -// cx.update_editor(|editor, cx| editor.next_copilot_suggestion(&Default::default(), cx)); -// executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); -// cx.update_editor(|editor, cx| { -// assert!(editor.has_active_copilot_suggestion(cx)); -// assert_eq!(editor.display_text(cx), "fn foo() {\n let x = 4;\n}"); -// assert_eq!(editor.text(cx), "fn foo() {\n \n}"); + cx.update_editor(|editor, cx| editor.next_copilot_suggestion(&Default::default(), cx)); + executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); + cx.update_editor(|editor, cx| { + assert!(editor.has_active_copilot_suggestion(cx)); + assert_eq!(editor.display_text(cx), "fn foo() {\n let x = 4;\n}"); + assert_eq!(editor.text(cx), "fn foo() {\n \n}"); -// // Tabbing inside of leading whitespace inserts indentation without accepting the suggestion. -// editor.tab(&Default::default(), cx); -// assert!(editor.has_active_copilot_suggestion(cx)); -// assert_eq!(editor.text(cx), "fn foo() {\n \n}"); -// assert_eq!(editor.display_text(cx), "fn foo() {\n let x = 4;\n}"); + // Tabbing inside of leading whitespace inserts indentation without accepting the suggestion. + editor.tab(&Default::default(), cx); + assert!(editor.has_active_copilot_suggestion(cx)); + assert_eq!(editor.text(cx), "fn foo() {\n \n}"); + assert_eq!(editor.display_text(cx), "fn foo() {\n let x = 4;\n}"); -// // Tabbing again accepts the suggestion. -// editor.tab(&Default::default(), cx); -// assert!(!editor.has_active_copilot_suggestion(cx)); -// assert_eq!(editor.text(cx), "fn foo() {\n let x = 4;\n}"); -// assert_eq!(editor.display_text(cx), "fn foo() {\n let x = 4;\n}"); -// }); -// } + // Tabbing again accepts the suggestion. + editor.tab(&Default::default(), cx); + assert!(!editor.has_active_copilot_suggestion(cx)); + assert_eq!(editor.text(cx), "fn foo() {\n let x = 4;\n}"); + assert_eq!(editor.display_text(cx), "fn foo() {\n let x = 4;\n}"); + }); +} #[gpui::test] async fn test_copilot_completion_invalidation(