From e0ccaa60ffb10eca9f8ffbf8dbc6c6ec2f9dffa0 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Fri, 1 Dec 2023 17:22:12 +0100 Subject: [PATCH 01/35] editor tests: Reintroduce block_on_ticks. Co-authored-by: Antonio --- crates/editor2/src/display_map/wrap_map.rs | 668 +++++++++---------- crates/gpui2/src/executor.rs | 41 +- crates/gpui2/src/platform/test/dispatcher.rs | 17 +- 3 files changed, 383 insertions(+), 343 deletions(-) diff --git a/crates/editor2/src/display_map/wrap_map.rs b/crates/editor2/src/display_map/wrap_map.rs index 5aeecbae978cd27620cd0b560c8af14baf7762d4..c8025c7da91a32cf00665669c43bcd8cffd63d35 100644 --- a/crates/editor2/src/display_map/wrap_map.rs +++ b/crates/editor2/src/display_map/wrap_map.rs @@ -1026,337 +1026,337 @@ fn consolidate_wrap_edits(edits: &mut Vec) { } } -// #[cfg(test)] -// mod tests { -// use super::*; -// use crate::{ -// display_map::{fold_map::FoldMap, inlay_map::InlayMap, tab_map::TabMap}, -// MultiBuffer, -// }; -// use gpui::test::observe; -// use rand::prelude::*; -// use settings::SettingsStore; -// use smol::stream::StreamExt; -// use std::{cmp, env, num::NonZeroU32}; -// use text::Rope; - -// #[gpui::test(iterations = 100)] -// async fn test_random_wraps(cx: &mut gpui::TestAppContext, mut rng: StdRng) { -// init_test(cx); - -// cx.foreground().set_block_on_ticks(0..=50); -// let operations = env::var("OPERATIONS") -// .map(|i| i.parse().expect("invalid `OPERATIONS` variable")) -// .unwrap_or(10); - -// let font_cache = cx.font_cache().clone(); -// let font_system = cx.platform().fonts(); -// let mut wrap_width = if rng.gen_bool(0.1) { -// None -// } else { -// Some(rng.gen_range(0.0..=1000.0)) -// }; -// let tab_size = NonZeroU32::new(rng.gen_range(1..=4)).unwrap(); -// let family_id = font_cache -// .load_family(&["Helvetica"], &Default::default()) -// .unwrap(); -// let font_id = font_cache -// .select_font(family_id, &Default::default()) -// .unwrap(); -// let font_size = 14.0; - -// log::info!("Tab size: {}", tab_size); -// log::info!("Wrap width: {:?}", wrap_width); - -// let buffer = cx.update(|cx| { -// if rng.gen() { -// MultiBuffer::build_random(&mut rng, cx) -// } else { -// let len = rng.gen_range(0..10); -// let text = util::RandomCharIter::new(&mut rng) -// .take(len) -// .collect::(); -// MultiBuffer::build_simple(&text, cx) -// } -// }); -// let mut buffer_snapshot = buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx)); -// log::info!("Buffer text: {:?}", buffer_snapshot.text()); -// let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone()); -// log::info!("InlayMap text: {:?}", inlay_snapshot.text()); -// let (mut fold_map, fold_snapshot) = FoldMap::new(inlay_snapshot.clone()); -// log::info!("FoldMap text: {:?}", fold_snapshot.text()); -// let (mut tab_map, _) = TabMap::new(fold_snapshot.clone(), tab_size); -// let tabs_snapshot = tab_map.set_max_expansion_column(32); -// log::info!("TabMap text: {:?}", tabs_snapshot.text()); - -// let mut line_wrapper = LineWrapper::new(font_id, font_size, font_system); -// let unwrapped_text = tabs_snapshot.text(); -// let expected_text = wrap_text(&unwrapped_text, wrap_width, &mut line_wrapper); - -// let (wrap_map, _) = -// cx.update(|cx| WrapMap::new(tabs_snapshot.clone(), font_id, font_size, wrap_width, cx)); -// let mut notifications = observe(&wrap_map, cx); - -// if wrap_map.read_with(cx, |map, _| map.is_rewrapping()) { -// notifications.next().await.unwrap(); -// } - -// let (initial_snapshot, _) = wrap_map.update(cx, |map, cx| { -// assert!(!map.is_rewrapping()); -// map.sync(tabs_snapshot.clone(), Vec::new(), cx) -// }); - -// let actual_text = initial_snapshot.text(); -// assert_eq!( -// actual_text, expected_text, -// "unwrapped text is: {:?}", -// unwrapped_text -// ); -// log::info!("Wrapped text: {:?}", actual_text); - -// let mut next_inlay_id = 0; -// let mut edits = Vec::new(); -// for _i in 0..operations { -// log::info!("{} ==============================================", _i); - -// let mut buffer_edits = Vec::new(); -// match rng.gen_range(0..=100) { -// 0..=19 => { -// wrap_width = if rng.gen_bool(0.2) { -// None -// } else { -// Some(rng.gen_range(0.0..=1000.0)) -// }; -// log::info!("Setting wrap width to {:?}", wrap_width); -// wrap_map.update(cx, |map, cx| map.set_wrap_width(wrap_width, cx)); -// } -// 20..=39 => { -// for (fold_snapshot, fold_edits) in fold_map.randomly_mutate(&mut rng) { -// let (tabs_snapshot, tab_edits) = -// tab_map.sync(fold_snapshot, fold_edits, tab_size); -// let (mut snapshot, wrap_edits) = -// wrap_map.update(cx, |map, cx| map.sync(tabs_snapshot, tab_edits, cx)); -// snapshot.check_invariants(); -// snapshot.verify_chunks(&mut rng); -// edits.push((snapshot, wrap_edits)); -// } -// } -// 40..=59 => { -// let (inlay_snapshot, inlay_edits) = -// inlay_map.randomly_mutate(&mut next_inlay_id, &mut rng); -// let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits); -// let (tabs_snapshot, tab_edits) = -// tab_map.sync(fold_snapshot, fold_edits, tab_size); -// let (mut snapshot, wrap_edits) = -// wrap_map.update(cx, |map, cx| map.sync(tabs_snapshot, tab_edits, cx)); -// snapshot.check_invariants(); -// snapshot.verify_chunks(&mut rng); -// edits.push((snapshot, wrap_edits)); -// } -// _ => { -// buffer.update(cx, |buffer, cx| { -// let subscription = buffer.subscribe(); -// let edit_count = rng.gen_range(1..=5); -// buffer.randomly_mutate(&mut rng, edit_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); -// log::info!("InlayMap text: {:?}", inlay_snapshot.text()); -// let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits); -// log::info!("FoldMap text: {:?}", fold_snapshot.text()); -// let (tabs_snapshot, tab_edits) = tab_map.sync(fold_snapshot, fold_edits, tab_size); -// log::info!("TabMap text: {:?}", tabs_snapshot.text()); - -// let unwrapped_text = tabs_snapshot.text(); -// let expected_text = wrap_text(&unwrapped_text, wrap_width, &mut line_wrapper); -// let (mut snapshot, wrap_edits) = -// wrap_map.update(cx, |map, cx| map.sync(tabs_snapshot.clone(), tab_edits, cx)); -// snapshot.check_invariants(); -// snapshot.verify_chunks(&mut rng); -// edits.push((snapshot, wrap_edits)); - -// if wrap_map.read_with(cx, |map, _| map.is_rewrapping()) && rng.gen_bool(0.4) { -// log::info!("Waiting for wrapping to finish"); -// while wrap_map.read_with(cx, |map, _| map.is_rewrapping()) { -// notifications.next().await.unwrap(); -// } -// wrap_map.read_with(cx, |map, _| assert!(map.pending_edits.is_empty())); -// } - -// if !wrap_map.read_with(cx, |map, _| map.is_rewrapping()) { -// let (mut wrapped_snapshot, wrap_edits) = -// wrap_map.update(cx, |map, cx| map.sync(tabs_snapshot, Vec::new(), cx)); -// let actual_text = wrapped_snapshot.text(); -// let actual_longest_row = wrapped_snapshot.longest_row(); -// log::info!("Wrapping finished: {:?}", actual_text); -// wrapped_snapshot.check_invariants(); -// wrapped_snapshot.verify_chunks(&mut rng); -// edits.push((wrapped_snapshot.clone(), wrap_edits)); -// assert_eq!( -// actual_text, expected_text, -// "unwrapped text is: {:?}", -// unwrapped_text -// ); - -// let mut summary = TextSummary::default(); -// for (ix, item) in wrapped_snapshot -// .transforms -// .items(&()) -// .into_iter() -// .enumerate() -// { -// summary += &item.summary.output; -// log::info!("{} summary: {:?}", ix, item.summary.output,); -// } - -// if tab_size.get() == 1 -// || !wrapped_snapshot -// .tab_snapshot -// .fold_snapshot -// .text() -// .contains('\t') -// { -// let mut expected_longest_rows = Vec::new(); -// let mut longest_line_len = -1; -// for (row, line) in expected_text.split('\n').enumerate() { -// let line_char_count = line.chars().count() as isize; -// if line_char_count > longest_line_len { -// expected_longest_rows.clear(); -// longest_line_len = line_char_count; -// } -// if line_char_count >= longest_line_len { -// expected_longest_rows.push(row as u32); -// } -// } - -// assert!( -// expected_longest_rows.contains(&actual_longest_row), -// "incorrect longest row {}. expected {:?} with length {}", -// actual_longest_row, -// expected_longest_rows, -// longest_line_len, -// ) -// } -// } -// } - -// let mut initial_text = Rope::from(initial_snapshot.text().as_str()); -// for (snapshot, patch) in edits { -// let snapshot_text = Rope::from(snapshot.text().as_str()); -// for edit in &patch { -// let old_start = initial_text.point_to_offset(Point::new(edit.new.start, 0)); -// let old_end = initial_text.point_to_offset(cmp::min( -// Point::new(edit.new.start + edit.old.len() as u32, 0), -// initial_text.max_point(), -// )); -// let new_start = snapshot_text.point_to_offset(Point::new(edit.new.start, 0)); -// let new_end = snapshot_text.point_to_offset(cmp::min( -// Point::new(edit.new.end, 0), -// snapshot_text.max_point(), -// )); -// let new_text = snapshot_text -// .chunks_in_range(new_start..new_end) -// .collect::(); - -// initial_text.replace(old_start..old_end, &new_text); -// } -// assert_eq!(initial_text.to_string(), snapshot_text.to_string()); -// } - -// if wrap_map.read_with(cx, |map, _| map.is_rewrapping()) { -// log::info!("Waiting for wrapping to finish"); -// while wrap_map.read_with(cx, |map, _| map.is_rewrapping()) { -// notifications.next().await.unwrap(); -// } -// } -// wrap_map.read_with(cx, |map, _| assert!(map.pending_edits.is_empty())); -// } - -// fn init_test(cx: &mut gpui::TestAppContext) { -// cx.foreground().forbid_parking(); -// cx.update(|cx| { -// cx.set_global(SettingsStore::test(cx)); -// theme::init((), cx); -// }); -// } - -// fn wrap_text( -// unwrapped_text: &str, -// wrap_width: Option, -// line_wrapper: &mut LineWrapper, -// ) -> String { -// if let Some(wrap_width) = wrap_width { -// let mut wrapped_text = String::new(); -// for (row, line) in unwrapped_text.split('\n').enumerate() { -// if row > 0 { -// wrapped_text.push('\n') -// } - -// let mut prev_ix = 0; -// for boundary in line_wrapper.wrap_line(line, wrap_width) { -// wrapped_text.push_str(&line[prev_ix..boundary.ix]); -// wrapped_text.push('\n'); -// wrapped_text.push_str(&" ".repeat(boundary.next_indent as usize)); -// prev_ix = boundary.ix; -// } -// wrapped_text.push_str(&line[prev_ix..]); -// } -// wrapped_text -// } else { -// unwrapped_text.to_string() -// } -// } - -// impl WrapSnapshot { -// pub fn text(&self) -> String { -// self.text_chunks(0).collect() -// } - -// pub fn text_chunks(&self, wrap_row: u32) -> impl Iterator { -// self.chunks( -// wrap_row..self.max_point().row() + 1, -// false, -// Highlights::default(), -// ) -// .map(|h| h.text) -// } - -// fn verify_chunks(&mut self, rng: &mut impl Rng) { -// for _ in 0..5 { -// let mut end_row = rng.gen_range(0..=self.max_point().row()); -// let start_row = rng.gen_range(0..=end_row); -// end_row += 1; - -// let mut expected_text = self.text_chunks(start_row).collect::(); -// if expected_text.ends_with('\n') { -// expected_text.push('\n'); -// } -// let mut expected_text = expected_text -// .lines() -// .take((end_row - start_row) as usize) -// .collect::>() -// .join("\n"); -// if end_row <= self.max_point().row() { -// expected_text.push('\n'); -// } - -// let actual_text = self -// .chunks(start_row..end_row, true, Highlights::default()) -// .map(|c| c.text) -// .collect::(); -// assert_eq!( -// expected_text, -// actual_text, -// "chunks != highlighted_chunks for rows {:?}", -// start_row..end_row -// ); -// } -// } -// } -// } +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + display_map::{fold_map::FoldMap, inlay_map::InlayMap, tab_map::TabMap}, + MultiBuffer, + }; + use gpui::test::observe; + use rand::prelude::*; + use settings::SettingsStore; + use smol::stream::StreamExt; + use std::{cmp, env, num::NonZeroU32}; + use text::Rope; + + #[gpui::test(iterations = 100)] + async fn test_random_wraps(cx: &mut gpui::TestAppContext, mut rng: StdRng) { + init_test(cx); + + cx.background_executor.set_block_on_ticks(0..=50); + let operations = env::var("OPERATIONS") + .map(|i| i.parse().expect("invalid `OPERATIONS` variable")) + .unwrap_or(10); + + let font_cache = cx.read(|cx| cx.font_cache().clone()); + let font_system = cx.platform().fonts(); + let mut wrap_width = if rng.gen_bool(0.1) { + None + } else { + Some(rng.gen_range(0.0..=1000.0)) + }; + let tab_size = NonZeroU32::new(rng.gen_range(1..=4)).unwrap(); + let family_id = font_cache + .load_family(&["Helvetica"], &Default::default()) + .unwrap(); + let font_id = font_cache + .select_font(family_id, &Default::default()) + .unwrap(); + let font_size = 14.0; + + log::info!("Tab size: {}", tab_size); + log::info!("Wrap width: {:?}", wrap_width); + + let buffer = cx.update(|cx| { + if rng.gen() { + MultiBuffer::build_random(&mut rng, cx) + } else { + let len = rng.gen_range(0..10); + let text = util::RandomCharIter::new(&mut rng) + .take(len) + .collect::(); + MultiBuffer::build_simple(&text, cx) + } + }); + let mut buffer_snapshot = buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx)); + log::info!("Buffer text: {:?}", buffer_snapshot.text()); + let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone()); + log::info!("InlayMap text: {:?}", inlay_snapshot.text()); + let (mut fold_map, fold_snapshot) = FoldMap::new(inlay_snapshot.clone()); + log::info!("FoldMap text: {:?}", fold_snapshot.text()); + let (mut tab_map, _) = TabMap::new(fold_snapshot.clone(), tab_size); + let tabs_snapshot = tab_map.set_max_expansion_column(32); + log::info!("TabMap text: {:?}", tabs_snapshot.text()); + + let mut line_wrapper = LineWrapper::new(font_id, font_size, font_system); + let unwrapped_text = tabs_snapshot.text(); + let expected_text = wrap_text(&unwrapped_text, wrap_width, &mut line_wrapper); + + let (wrap_map, _) = + cx.update(|cx| WrapMap::new(tabs_snapshot.clone(), font_id, font_size, wrap_width, cx)); + let mut notifications = observe(&wrap_map, cx); + + if wrap_map.read_with(cx, |map, _| map.is_rewrapping()) { + notifications.next().await.unwrap(); + } + + let (initial_snapshot, _) = wrap_map.update(cx, |map, cx| { + assert!(!map.is_rewrapping()); + map.sync(tabs_snapshot.clone(), Vec::new(), cx) + }); + + let actual_text = initial_snapshot.text(); + assert_eq!( + actual_text, expected_text, + "unwrapped text is: {:?}", + unwrapped_text + ); + log::info!("Wrapped text: {:?}", actual_text); + + let mut next_inlay_id = 0; + let mut edits = Vec::new(); + for _i in 0..operations { + log::info!("{} ==============================================", _i); + + let mut buffer_edits = Vec::new(); + match rng.gen_range(0..=100) { + 0..=19 => { + wrap_width = if rng.gen_bool(0.2) { + None + } else { + Some(rng.gen_range(0.0..=1000.0)) + }; + log::info!("Setting wrap width to {:?}", wrap_width); + wrap_map.update(cx, |map, cx| map.set_wrap_width(wrap_width, cx)); + } + 20..=39 => { + for (fold_snapshot, fold_edits) in fold_map.randomly_mutate(&mut rng) { + let (tabs_snapshot, tab_edits) = + tab_map.sync(fold_snapshot, fold_edits, tab_size); + let (mut snapshot, wrap_edits) = + wrap_map.update(cx, |map, cx| map.sync(tabs_snapshot, tab_edits, cx)); + snapshot.check_invariants(); + snapshot.verify_chunks(&mut rng); + edits.push((snapshot, wrap_edits)); + } + } + 40..=59 => { + let (inlay_snapshot, inlay_edits) = + inlay_map.randomly_mutate(&mut next_inlay_id, &mut rng); + let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits); + let (tabs_snapshot, tab_edits) = + tab_map.sync(fold_snapshot, fold_edits, tab_size); + let (mut snapshot, wrap_edits) = + wrap_map.update(cx, |map, cx| map.sync(tabs_snapshot, tab_edits, cx)); + snapshot.check_invariants(); + snapshot.verify_chunks(&mut rng); + edits.push((snapshot, wrap_edits)); + } + _ => { + buffer.update(cx, |buffer, cx| { + let subscription = buffer.subscribe(); + let edit_count = rng.gen_range(1..=5); + buffer.randomly_mutate(&mut rng, edit_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); + log::info!("InlayMap text: {:?}", inlay_snapshot.text()); + let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits); + log::info!("FoldMap text: {:?}", fold_snapshot.text()); + let (tabs_snapshot, tab_edits) = tab_map.sync(fold_snapshot, fold_edits, tab_size); + log::info!("TabMap text: {:?}", tabs_snapshot.text()); + + let unwrapped_text = tabs_snapshot.text(); + let expected_text = wrap_text(&unwrapped_text, wrap_width, &mut line_wrapper); + let (mut snapshot, wrap_edits) = + wrap_map.update(cx, |map, cx| map.sync(tabs_snapshot.clone(), tab_edits, cx)); + snapshot.check_invariants(); + snapshot.verify_chunks(&mut rng); + edits.push((snapshot, wrap_edits)); + + if wrap_map.read_with(cx, |map, _| map.is_rewrapping()) && rng.gen_bool(0.4) { + log::info!("Waiting for wrapping to finish"); + while wrap_map.read_with(cx, |map, _| map.is_rewrapping()) { + notifications.next().await.unwrap(); + } + wrap_map.read_with(cx, |map, _| assert!(map.pending_edits.is_empty())); + } + + if !wrap_map.read_with(cx, |map, _| map.is_rewrapping()) { + let (mut wrapped_snapshot, wrap_edits) = + wrap_map.update(cx, |map, cx| map.sync(tabs_snapshot, Vec::new(), cx)); + let actual_text = wrapped_snapshot.text(); + let actual_longest_row = wrapped_snapshot.longest_row(); + log::info!("Wrapping finished: {:?}", actual_text); + wrapped_snapshot.check_invariants(); + wrapped_snapshot.verify_chunks(&mut rng); + edits.push((wrapped_snapshot.clone(), wrap_edits)); + assert_eq!( + actual_text, expected_text, + "unwrapped text is: {:?}", + unwrapped_text + ); + + let mut summary = TextSummary::default(); + for (ix, item) in wrapped_snapshot + .transforms + .items(&()) + .into_iter() + .enumerate() + { + summary += &item.summary.output; + log::info!("{} summary: {:?}", ix, item.summary.output,); + } + + if tab_size.get() == 1 + || !wrapped_snapshot + .tab_snapshot + .fold_snapshot + .text() + .contains('\t') + { + let mut expected_longest_rows = Vec::new(); + let mut longest_line_len = -1; + for (row, line) in expected_text.split('\n').enumerate() { + let line_char_count = line.chars().count() as isize; + if line_char_count > longest_line_len { + expected_longest_rows.clear(); + longest_line_len = line_char_count; + } + if line_char_count >= longest_line_len { + expected_longest_rows.push(row as u32); + } + } + + assert!( + expected_longest_rows.contains(&actual_longest_row), + "incorrect longest row {}. expected {:?} with length {}", + actual_longest_row, + expected_longest_rows, + longest_line_len, + ) + } + } + } + + let mut initial_text = Rope::from(initial_snapshot.text().as_str()); + for (snapshot, patch) in edits { + let snapshot_text = Rope::from(snapshot.text().as_str()); + for edit in &patch { + let old_start = initial_text.point_to_offset(Point::new(edit.new.start, 0)); + let old_end = initial_text.point_to_offset(cmp::min( + Point::new(edit.new.start + edit.old.len() as u32, 0), + initial_text.max_point(), + )); + let new_start = snapshot_text.point_to_offset(Point::new(edit.new.start, 0)); + let new_end = snapshot_text.point_to_offset(cmp::min( + Point::new(edit.new.end, 0), + snapshot_text.max_point(), + )); + let new_text = snapshot_text + .chunks_in_range(new_start..new_end) + .collect::(); + + initial_text.replace(old_start..old_end, &new_text); + } + assert_eq!(initial_text.to_string(), snapshot_text.to_string()); + } + + if wrap_map.read_with(cx, |map, _| map.is_rewrapping()) { + log::info!("Waiting for wrapping to finish"); + while wrap_map.read_with(cx, |map, _| map.is_rewrapping()) { + notifications.next().await.unwrap(); + } + } + wrap_map.read_with(cx, |map, _| assert!(map.pending_edits.is_empty())); + } + + fn init_test(cx: &mut gpui::TestAppContext) { + cx.foreground_executor().forbid_parking(); + cx.update(|cx| { + cx.set_global(SettingsStore::test(cx)); + theme::init((), cx); + }); + } + + fn wrap_text( + unwrapped_text: &str, + wrap_width: Option, + line_wrapper: &mut LineWrapper, + ) -> String { + if let Some(wrap_width) = wrap_width { + let mut wrapped_text = String::new(); + for (row, line) in unwrapped_text.split('\n').enumerate() { + if row > 0 { + wrapped_text.push('\n') + } + + let mut prev_ix = 0; + for boundary in line_wrapper.wrap_line(line, wrap_width) { + wrapped_text.push_str(&line[prev_ix..boundary.ix]); + wrapped_text.push('\n'); + wrapped_text.push_str(&" ".repeat(boundary.next_indent as usize)); + prev_ix = boundary.ix; + } + wrapped_text.push_str(&line[prev_ix..]); + } + wrapped_text + } else { + unwrapped_text.to_string() + } + } + + impl WrapSnapshot { + pub fn text(&self) -> String { + self.text_chunks(0).collect() + } + + pub fn text_chunks(&self, wrap_row: u32) -> impl Iterator { + self.chunks( + wrap_row..self.max_point().row() + 1, + false, + Highlights::default(), + ) + .map(|h| h.text) + } + + fn verify_chunks(&mut self, rng: &mut impl Rng) { + for _ in 0..5 { + let mut end_row = rng.gen_range(0..=self.max_point().row()); + let start_row = rng.gen_range(0..=end_row); + end_row += 1; + + let mut expected_text = self.text_chunks(start_row).collect::(); + if expected_text.ends_with('\n') { + expected_text.push('\n'); + } + let mut expected_text = expected_text + .lines() + .take((end_row - start_row) as usize) + .collect::>() + .join("\n"); + if end_row <= self.max_point().row() { + expected_text.push('\n'); + } + + let actual_text = self + .chunks(start_row..end_row, true, Highlights::default()) + .map(|c| c.text) + .collect::(); + assert_eq!( + expected_text, + actual_text, + "chunks != highlighted_chunks for rows {:?}", + start_row..end_row + ); + } + } + } +} diff --git a/crates/gpui2/src/executor.rs b/crates/gpui2/src/executor.rs index cf138a90db1b177e052d79788754d446474ce5be..e446a0cb1ea287b4b20c668f2adfe36e9d4227c4 100644 --- a/crates/gpui2/src/executor.rs +++ b/crates/gpui2/src/executor.rs @@ -128,11 +128,19 @@ impl BackgroundExecutor { #[cfg(any(test, feature = "test-support"))] #[track_caller] pub fn block_test(&self, future: impl Future) -> R { - self.block_internal(false, future) + if let Ok(value) = self.block_internal(false, future, usize::MAX) { + value + } else { + unreachable!() + } } pub fn block(&self, future: impl Future) -> R { - self.block_internal(true, future) + if let Ok(value) = self.block_internal(true, future, usize::MAX) { + value + } else { + unreachable!() + } } #[track_caller] @@ -140,7 +148,8 @@ impl BackgroundExecutor { &self, background_only: bool, future: impl Future, - ) -> R { + mut max_ticks: usize, + ) -> Result { pin_mut!(future); let unparker = self.dispatcher.unparker(); let awoken = Arc::new(AtomicBool::new(false)); @@ -156,8 +165,13 @@ impl BackgroundExecutor { loop { match future.as_mut().poll(&mut cx) { - Poll::Ready(result) => return result, + Poll::Ready(result) => return Ok(result), Poll::Pending => { + if max_ticks == 0 { + return Err(()); + } + max_ticks -= 1; + if !self.dispatcher.tick(background_only) { if awoken.swap(false, SeqCst) { continue; @@ -192,16 +206,24 @@ impl BackgroundExecutor { return Err(future); } + let max_ticks = if cfg!(any(test, feature = "test-support")) { + self.dispatcher + .as_test() + .map_or(usize::MAX, |dispatcher| dispatcher.gen_block_on_ticks()) + } else { + usize::MAX + }; let mut timer = self.timer(duration).fuse(); + let timeout = async { futures::select_biased! { value = future => Ok(value), _ = timer => Err(()), } }; - match self.block(timeout) { - Ok(value) => Ok(value), - Err(_) => Err(future), + match self.block_internal(true, timeout, max_ticks) { + Ok(Ok(value)) => Ok(value), + _ => Err(future), } } @@ -281,6 +303,11 @@ impl BackgroundExecutor { pub fn is_main_thread(&self) -> bool { self.dispatcher.is_main_thread() } + + #[cfg(any(test, feature = "test-support"))] + pub fn set_block_on_ticks(&self, range: std::ops::RangeInclusive) { + self.dispatcher.as_test().unwrap().set_block_on_ticks(range); + } } impl ForegroundExecutor { diff --git a/crates/gpui2/src/platform/test/dispatcher.rs b/crates/gpui2/src/platform/test/dispatcher.rs index e77c1c052903f44b2e346af5b3d7a6fb57cda65f..9023627d1e2d777188d1b5bc96f89cabc4fbe903 100644 --- a/crates/gpui2/src/platform/test/dispatcher.rs +++ b/crates/gpui2/src/platform/test/dispatcher.rs @@ -7,6 +7,7 @@ use parking_lot::Mutex; use rand::prelude::*; use std::{ future::Future, + ops::RangeInclusive, pin::Pin, sync::Arc, task::{Context, Poll}, @@ -36,6 +37,7 @@ struct TestDispatcherState { allow_parking: bool, waiting_backtrace: Option, deprioritized_task_labels: HashSet, + block_on_ticks: RangeInclusive, } impl TestDispatcher { @@ -53,6 +55,7 @@ impl TestDispatcher { allow_parking: false, waiting_backtrace: None, deprioritized_task_labels: Default::default(), + block_on_ticks: 0..=1000, }; TestDispatcher { @@ -82,8 +85,8 @@ impl TestDispatcher { } pub fn simulate_random_delay(&self) -> impl 'static + Send + Future { - pub struct YieldNow { - count: usize, + struct YieldNow { + pub(crate) count: usize, } impl Future for YieldNow { @@ -142,6 +145,16 @@ impl TestDispatcher { pub fn rng(&self) -> StdRng { self.state.lock().random.clone() } + + pub fn set_block_on_ticks(&self, range: std::ops::RangeInclusive) { + self.state.lock().block_on_ticks = range; + } + + pub fn gen_block_on_ticks(&self) -> usize { + let mut lock = self.state.lock(); + let block_on_ticks = lock.block_on_ticks.clone(); + lock.random.gen_range(block_on_ticks) + } } impl Clone for TestDispatcher { From 4c4ec221afe4c3c3286a9eb9af0ea5a4577e15b6 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Fri, 1 Dec 2023 18:11:27 +0100 Subject: [PATCH 02/35] Uncomment a bunch of tests in the editor --- crates/editor2/src/editor_tests.rs | 1 - crates/editor2/src/element.rs | 2 +- crates/editor2/src/git.rs | 364 ++--- crates/editor2/src/link_go_to_definition.rs | 1337 ++++++++++--------- 4 files changed, 852 insertions(+), 852 deletions(-) diff --git a/crates/editor2/src/editor_tests.rs b/crates/editor2/src/editor_tests.rs index e640be8efe030250876138ed23a885119a5b7dbb..265bde908b564fcb34549cfea42f377e10837dbc 100644 --- a/crates/editor2/src/editor_tests.rs +++ b/crates/editor2/src/editor_tests.rs @@ -6325,7 +6325,6 @@ async fn test_extra_newline_insertion(cx: &mut gpui::TestAppContext) { }); } -//todo!(finish editor tests) // #[gpui::test] // fn test_highlighted_ranges(cx: &mut TestAppContext) { // init_test(cx, |_| {}); diff --git a/crates/editor2/src/element.rs b/crates/editor2/src/element.rs index 3abe5a37f97b9ce2d23f0c0c1d215e05883588b9..8f555ba9de714d9bcddedc8178ad94be88716327 100644 --- a/crates/editor2/src/element.rs +++ b/crates/editor2/src/element.rs @@ -328,7 +328,7 @@ impl EditorElement { }); } - fn modifiers_changed( + pub(crate) fn modifiers_changed( editor: &mut Editor, event: &ModifiersChangedEvent, cx: &mut ViewContext, diff --git a/crates/editor2/src/git.rs b/crates/editor2/src/git.rs index 6e408cd3a01f7603ccdf97660b8896fdee833d65..9190eed05a40a24063ccf78ca30a6b2966f43c47 100644 --- a/crates/editor2/src/git.rs +++ b/crates/editor2/src/git.rs @@ -88,195 +88,195 @@ pub fn diff_hunk_to_display(hunk: DiffHunk, snapshot: &DisplaySnapshot) -> } } -// #[cfg(any(test, feature = "test_support"))] -// mod tests { -// // use crate::editor_tests::init_test; -// use crate::Point; -// use gpui::TestAppContext; -// use multi_buffer::{ExcerptRange, MultiBuffer}; -// use project::{FakeFs, Project}; -// use unindent::Unindent; -// #[gpui::test] -// async fn test_diff_hunks_in_range(cx: &mut TestAppContext) { -// use git::diff::DiffHunkStatus; -// init_test(cx, |_| {}); +#[cfg(any(test, feature = "test_support"))] +mod tests { + use crate::editor_tests::init_test; + use crate::Point; + use gpui::{Context, TestAppContext}; + use multi_buffer::{ExcerptRange, MultiBuffer}; + use project::{FakeFs, Project}; + use unindent::Unindent; + #[gpui::test] + async fn test_diff_hunks_in_range(cx: &mut TestAppContext) { + use git::diff::DiffHunkStatus; + init_test(cx, |_| {}); -// let fs = FakeFs::new(cx.background()); -// let project = Project::test(fs, [], cx).await; + let fs = FakeFs::new(cx.background_executor.clone()); + let project = Project::test(fs, [], cx).await; -// // buffer has two modified hunks with two rows each -// let buffer_1 = project -// .update(cx, |project, cx| { -// project.create_buffer( -// " -// 1.zero -// 1.ONE -// 1.TWO -// 1.three -// 1.FOUR -// 1.FIVE -// 1.six -// " -// .unindent() -// .as_str(), -// None, -// cx, -// ) -// }) -// .unwrap(); -// buffer_1.update(cx, |buffer, cx| { -// buffer.set_diff_base( -// Some( -// " -// 1.zero -// 1.one -// 1.two -// 1.three -// 1.four -// 1.five -// 1.six -// " -// .unindent(), -// ), -// cx, -// ); -// }); + // buffer has two modified hunks with two rows each + let buffer_1 = project + .update(cx, |project, cx| { + project.create_buffer( + " + 1.zero + 1.ONE + 1.TWO + 1.three + 1.FOUR + 1.FIVE + 1.six + " + .unindent() + .as_str(), + None, + cx, + ) + }) + .unwrap(); + buffer_1.update(cx, |buffer, cx| { + buffer.set_diff_base( + Some( + " + 1.zero + 1.one + 1.two + 1.three + 1.four + 1.five + 1.six + " + .unindent(), + ), + cx, + ); + }); -// // buffer has a deletion hunk and an insertion hunk -// let buffer_2 = project -// .update(cx, |project, cx| { -// project.create_buffer( -// " -// 2.zero -// 2.one -// 2.two -// 2.three -// 2.four -// 2.five -// 2.six -// " -// .unindent() -// .as_str(), -// None, -// cx, -// ) -// }) -// .unwrap(); -// buffer_2.update(cx, |buffer, cx| { -// buffer.set_diff_base( -// Some( -// " -// 2.zero -// 2.one -// 2.one-and-a-half -// 2.two -// 2.three -// 2.four -// 2.six -// " -// .unindent(), -// ), -// cx, -// ); -// }); + // buffer has a deletion hunk and an insertion hunk + let buffer_2 = project + .update(cx, |project, cx| { + project.create_buffer( + " + 2.zero + 2.one + 2.two + 2.three + 2.four + 2.five + 2.six + " + .unindent() + .as_str(), + None, + cx, + ) + }) + .unwrap(); + buffer_2.update(cx, |buffer, cx| { + buffer.set_diff_base( + Some( + " + 2.zero + 2.one + 2.one-and-a-half + 2.two + 2.three + 2.four + 2.six + " + .unindent(), + ), + cx, + ); + }); -// cx.foreground().run_until_parked(); + cx.background_executor.run_until_parked(); -// let multibuffer = cx.add_model(|cx| { -// let mut multibuffer = MultiBuffer::new(0); -// multibuffer.push_excerpts( -// buffer_1.clone(), -// [ -// // excerpt ends in the middle of a modified hunk -// ExcerptRange { -// context: Point::new(0, 0)..Point::new(1, 5), -// primary: Default::default(), -// }, -// // excerpt begins in the middle of a modified hunk -// ExcerptRange { -// context: Point::new(5, 0)..Point::new(6, 5), -// primary: Default::default(), -// }, -// ], -// cx, -// ); -// multibuffer.push_excerpts( -// buffer_2.clone(), -// [ -// // excerpt ends at a deletion -// ExcerptRange { -// context: Point::new(0, 0)..Point::new(1, 5), -// primary: Default::default(), -// }, -// // excerpt starts at a deletion -// ExcerptRange { -// context: Point::new(2, 0)..Point::new(2, 5), -// primary: Default::default(), -// }, -// // excerpt fully contains a deletion hunk -// ExcerptRange { -// context: Point::new(1, 0)..Point::new(2, 5), -// primary: Default::default(), -// }, -// // excerpt fully contains an insertion hunk -// ExcerptRange { -// context: Point::new(4, 0)..Point::new(6, 5), -// primary: Default::default(), -// }, -// ], -// cx, -// ); -// multibuffer -// }); + let multibuffer = cx.build_model(|cx| { + let mut multibuffer = MultiBuffer::new(0); + multibuffer.push_excerpts( + buffer_1.clone(), + [ + // excerpt ends in the middle of a modified hunk + ExcerptRange { + context: Point::new(0, 0)..Point::new(1, 5), + primary: Default::default(), + }, + // excerpt begins in the middle of a modified hunk + ExcerptRange { + context: Point::new(5, 0)..Point::new(6, 5), + primary: Default::default(), + }, + ], + cx, + ); + multibuffer.push_excerpts( + buffer_2.clone(), + [ + // excerpt ends at a deletion + ExcerptRange { + context: Point::new(0, 0)..Point::new(1, 5), + primary: Default::default(), + }, + // excerpt starts at a deletion + ExcerptRange { + context: Point::new(2, 0)..Point::new(2, 5), + primary: Default::default(), + }, + // excerpt fully contains a deletion hunk + ExcerptRange { + context: Point::new(1, 0)..Point::new(2, 5), + primary: Default::default(), + }, + // excerpt fully contains an insertion hunk + ExcerptRange { + context: Point::new(4, 0)..Point::new(6, 5), + primary: Default::default(), + }, + ], + cx, + ); + multibuffer + }); -// let snapshot = multibuffer.read_with(cx, |b, cx| b.snapshot(cx)); + let snapshot = multibuffer.read_with(cx, |b, cx| b.snapshot(cx)); -// assert_eq!( -// snapshot.text(), -// " -// 1.zero -// 1.ONE -// 1.FIVE -// 1.six -// 2.zero -// 2.one -// 2.two -// 2.one -// 2.two -// 2.four -// 2.five -// 2.six" -// .unindent() -// ); + assert_eq!( + snapshot.text(), + " + 1.zero + 1.ONE + 1.FIVE + 1.six + 2.zero + 2.one + 2.two + 2.one + 2.two + 2.four + 2.five + 2.six" + .unindent() + ); -// let expected = [ -// (DiffHunkStatus::Modified, 1..2), -// (DiffHunkStatus::Modified, 2..3), -// //TODO: Define better when and where removed hunks show up at range extremities -// (DiffHunkStatus::Removed, 6..6), -// (DiffHunkStatus::Removed, 8..8), -// (DiffHunkStatus::Added, 10..11), -// ]; + let expected = [ + (DiffHunkStatus::Modified, 1..2), + (DiffHunkStatus::Modified, 2..3), + //TODO: Define better when and where removed hunks show up at range extremities + (DiffHunkStatus::Removed, 6..6), + (DiffHunkStatus::Removed, 8..8), + (DiffHunkStatus::Added, 10..11), + ]; -// assert_eq!( -// snapshot -// .git_diff_hunks_in_range(0..12) -// .map(|hunk| (hunk.status(), hunk.buffer_range)) -// .collect::>(), -// &expected, -// ); + assert_eq!( + snapshot + .git_diff_hunks_in_range(0..12) + .map(|hunk| (hunk.status(), hunk.buffer_range)) + .collect::>(), + &expected, + ); -// assert_eq!( -// snapshot -// .git_diff_hunks_in_range_rev(0..12) -// .map(|hunk| (hunk.status(), hunk.buffer_range)) -// .collect::>(), -// expected -// .iter() -// .rev() -// .cloned() -// .collect::>() -// .as_slice(), -// ); -// } -// } + assert_eq!( + snapshot + .git_diff_hunks_in_range_rev(0..12) + .map(|hunk| (hunk.status(), hunk.buffer_range)) + .collect::>(), + expected + .iter() + .rev() + .cloned() + .collect::>() + .as_slice(), + ); + } +} diff --git a/crates/editor2/src/link_go_to_definition.rs b/crates/editor2/src/link_go_to_definition.rs index 092882573c59961dc9e6cba6ee65aa022367107d..60c966d4c7cf68ea0e420ee3d7270be1d671cbff 100644 --- a/crates/editor2/src/link_go_to_definition.rs +++ b/crates/editor2/src/link_go_to_definition.rs @@ -608,671 +608,672 @@ fn go_to_fetched_definition_of_kind( } } -// #[cfg(test)] -// mod tests { -// use super::*; -// use crate::{ -// display_map::ToDisplayPoint, -// editor_tests::init_test, -// inlay_hint_cache::tests::{cached_hint_labels, visible_hint_labels}, -// test::editor_lsp_test_context::EditorLspTestContext, -// }; -// use futures::StreamExt; -// use gpui::{ -// platform::{self, Modifiers, ModifiersChangedEvent}, -// View, -// }; -// use indoc::indoc; -// use language::language_settings::InlayHintSettings; -// use lsp::request::{GotoDefinition, GotoTypeDefinition}; -// use util::assert_set_eq; - -// #[gpui::test] -// async fn test_link_go_to_type_definition(cx: &mut gpui::TestAppContext) { -// init_test(cx, |_| {}); - -// let mut cx = EditorLspTestContext::new_rust( -// lsp::ServerCapabilities { -// hover_provider: Some(lsp::HoverProviderCapability::Simple(true)), -// type_definition_provider: Some(lsp::TypeDefinitionProviderCapability::Simple(true)), -// ..Default::default() -// }, -// cx, -// ) -// .await; - -// cx.set_state(indoc! {" -// struct A; -// let vˇariable = A; -// "}); - -// // Basic hold cmd+shift, expect highlight in region if response contains type definition -// let hover_point = cx.display_point(indoc! {" -// struct A; -// let vˇariable = A; -// "}); -// let symbol_range = cx.lsp_range(indoc! {" -// struct A; -// let «variable» = A; -// "}); -// let target_range = cx.lsp_range(indoc! {" -// struct «A»; -// let variable = A; -// "}); - -// let mut requests = -// cx.handle_request::(move |url, _, _| async move { -// Ok(Some(lsp::GotoTypeDefinitionResponse::Link(vec![ -// lsp::LocationLink { -// origin_selection_range: Some(symbol_range), -// target_uri: url.clone(), -// target_range, -// target_selection_range: target_range, -// }, -// ]))) -// }); - -// // Press cmd+shift to trigger highlight -// cx.update_editor(|editor, cx| { -// update_go_to_definition_link( -// editor, -// Some(GoToDefinitionTrigger::Text(hover_point)), -// true, -// true, -// cx, -// ); -// }); -// requests.next().await; -// cx.foreground().run_until_parked(); -// cx.assert_editor_text_highlights::(indoc! {" -// struct A; -// let «variable» = A; -// "}); - -// // Unpress shift causes highlight to go away (normal goto-definition is not valid here) -// cx.update_editor(|editor, cx| { -// editor.modifiers_changed( -// &platform::ModifiersChangedEvent { -// modifiers: Modifiers { -// cmd: true, -// ..Default::default() -// }, -// ..Default::default() -// }, -// cx, -// ); -// }); -// // Assert no link highlights -// cx.assert_editor_text_highlights::(indoc! {" -// struct A; -// let variable = A; -// "}); - -// // Cmd+shift click without existing definition requests and jumps -// let hover_point = cx.display_point(indoc! {" -// struct A; -// let vˇariable = A; -// "}); -// let target_range = cx.lsp_range(indoc! {" -// struct «A»; -// let variable = A; -// "}); - -// let mut requests = -// cx.handle_request::(move |url, _, _| async move { -// Ok(Some(lsp::GotoTypeDefinitionResponse::Link(vec![ -// lsp::LocationLink { -// origin_selection_range: None, -// target_uri: url, -// target_range, -// target_selection_range: target_range, -// }, -// ]))) -// }); - -// cx.update_editor(|editor, cx| { -// go_to_fetched_type_definition(editor, PointForPosition::valid(hover_point), false, cx); -// }); -// requests.next().await; -// cx.foreground().run_until_parked(); - -// cx.assert_editor_state(indoc! {" -// struct «Aˇ»; -// let variable = A; -// "}); -// } - -// #[gpui::test] -// async fn test_link_go_to_definition(cx: &mut gpui::TestAppContext) { -// init_test(cx, |_| {}); - -// let mut cx = EditorLspTestContext::new_rust( -// lsp::ServerCapabilities { -// hover_provider: Some(lsp::HoverProviderCapability::Simple(true)), -// ..Default::default() -// }, -// cx, -// ) -// .await; - -// cx.set_state(indoc! {" -// fn ˇtest() { do_work(); } -// fn do_work() { test(); } -// "}); - -// // Basic hold cmd, expect highlight in region if response contains definition -// let hover_point = cx.display_point(indoc! {" -// fn test() { do_wˇork(); } -// fn do_work() { test(); } -// "}); -// let symbol_range = cx.lsp_range(indoc! {" -// fn test() { «do_work»(); } -// fn do_work() { test(); } -// "}); -// let target_range = cx.lsp_range(indoc! {" -// fn test() { do_work(); } -// fn «do_work»() { test(); } -// "}); - -// let mut requests = cx.handle_request::(move |url, _, _| async move { -// Ok(Some(lsp::GotoDefinitionResponse::Link(vec![ -// lsp::LocationLink { -// origin_selection_range: Some(symbol_range), -// target_uri: url.clone(), -// target_range, -// target_selection_range: target_range, -// }, -// ]))) -// }); - -// cx.update_editor(|editor, cx| { -// update_go_to_definition_link( -// editor, -// Some(GoToDefinitionTrigger::Text(hover_point)), -// true, -// false, -// cx, -// ); -// }); -// requests.next().await; -// cx.foreground().run_until_parked(); -// cx.assert_editor_text_highlights::(indoc! {" -// fn test() { «do_work»(); } -// fn do_work() { test(); } -// "}); - -// // Unpress cmd causes highlight to go away -// cx.update_editor(|editor, cx| { -// editor.modifiers_changed(&Default::default(), cx); -// }); - -// // Assert no link highlights -// cx.assert_editor_text_highlights::(indoc! {" -// fn test() { do_work(); } -// fn do_work() { test(); } -// "}); - -// // Response without source range still highlights word -// cx.update_editor(|editor, _| editor.link_go_to_definition_state.last_trigger_point = None); -// let mut requests = cx.handle_request::(move |url, _, _| async move { -// Ok(Some(lsp::GotoDefinitionResponse::Link(vec![ -// lsp::LocationLink { -// // No origin range -// origin_selection_range: None, -// target_uri: url.clone(), -// target_range, -// target_selection_range: target_range, -// }, -// ]))) -// }); -// cx.update_editor(|editor, cx| { -// update_go_to_definition_link( -// editor, -// Some(GoToDefinitionTrigger::Text(hover_point)), -// true, -// false, -// cx, -// ); -// }); -// requests.next().await; -// cx.foreground().run_until_parked(); - -// cx.assert_editor_text_highlights::(indoc! {" -// fn test() { «do_work»(); } -// fn do_work() { test(); } -// "}); - -// // Moving mouse to location with no response dismisses highlight -// let hover_point = cx.display_point(indoc! {" -// fˇn test() { do_work(); } -// fn do_work() { test(); } -// "}); -// let mut requests = cx -// .lsp -// .handle_request::(move |_, _| async move { -// // No definitions returned -// Ok(Some(lsp::GotoDefinitionResponse::Link(vec![]))) -// }); -// cx.update_editor(|editor, cx| { -// update_go_to_definition_link( -// editor, -// Some(GoToDefinitionTrigger::Text(hover_point)), -// true, -// false, -// cx, -// ); -// }); -// requests.next().await; -// cx.foreground().run_until_parked(); - -// // Assert no link highlights -// cx.assert_editor_text_highlights::(indoc! {" -// fn test() { do_work(); } -// fn do_work() { test(); } -// "}); - -// // Move mouse without cmd and then pressing cmd triggers highlight -// let hover_point = cx.display_point(indoc! {" -// fn test() { do_work(); } -// fn do_work() { teˇst(); } -// "}); -// cx.update_editor(|editor, cx| { -// update_go_to_definition_link( -// editor, -// Some(GoToDefinitionTrigger::Text(hover_point)), -// false, -// false, -// cx, -// ); -// }); -// cx.foreground().run_until_parked(); - -// // Assert no link highlights -// cx.assert_editor_text_highlights::(indoc! {" -// fn test() { do_work(); } -// fn do_work() { test(); } -// "}); - -// let symbol_range = cx.lsp_range(indoc! {" -// fn test() { do_work(); } -// fn do_work() { «test»(); } -// "}); -// let target_range = cx.lsp_range(indoc! {" -// fn «test»() { do_work(); } -// fn do_work() { test(); } -// "}); - -// let mut requests = cx.handle_request::(move |url, _, _| async move { -// Ok(Some(lsp::GotoDefinitionResponse::Link(vec![ -// lsp::LocationLink { -// origin_selection_range: Some(symbol_range), -// target_uri: url, -// target_range, -// target_selection_range: target_range, -// }, -// ]))) -// }); -// cx.update_editor(|editor, cx| { -// editor.modifiers_changed( -// &ModifiersChangedEvent { -// modifiers: Modifiers { -// cmd: true, -// ..Default::default() -// }, -// }, -// cx, -// ); -// }); -// requests.next().await; -// cx.foreground().run_until_parked(); - -// cx.assert_editor_text_highlights::(indoc! {" -// fn test() { do_work(); } -// fn do_work() { «test»(); } -// "}); - -// // Deactivating the window dismisses the highlight -// cx.update_workspace(|workspace, cx| { -// workspace.on_window_activation_changed(false, cx); -// }); -// cx.assert_editor_text_highlights::(indoc! {" -// fn test() { do_work(); } -// fn do_work() { test(); } -// "}); - -// // Moving the mouse restores the highlights. -// cx.update_editor(|editor, cx| { -// update_go_to_definition_link( -// editor, -// Some(GoToDefinitionTrigger::Text(hover_point)), -// true, -// false, -// cx, -// ); -// }); -// cx.foreground().run_until_parked(); -// cx.assert_editor_text_highlights::(indoc! {" -// fn test() { do_work(); } -// fn do_work() { «test»(); } -// "}); - -// // Moving again within the same symbol range doesn't re-request -// let hover_point = cx.display_point(indoc! {" -// fn test() { do_work(); } -// fn do_work() { tesˇt(); } -// "}); -// cx.update_editor(|editor, cx| { -// update_go_to_definition_link( -// editor, -// Some(GoToDefinitionTrigger::Text(hover_point)), -// true, -// false, -// cx, -// ); -// }); -// cx.foreground().run_until_parked(); -// cx.assert_editor_text_highlights::(indoc! {" -// fn test() { do_work(); } -// fn do_work() { «test»(); } -// "}); - -// // Cmd click with existing definition doesn't re-request and dismisses highlight -// cx.update_editor(|editor, cx| { -// go_to_fetched_definition(editor, PointForPosition::valid(hover_point), false, cx); -// }); -// // Assert selection moved to to definition -// cx.lsp -// .handle_request::(move |_, _| async move { -// // Empty definition response to make sure we aren't hitting the lsp and using -// // the cached location instead -// Ok(Some(lsp::GotoDefinitionResponse::Link(vec![]))) -// }); -// cx.foreground().run_until_parked(); -// cx.assert_editor_state(indoc! {" -// fn «testˇ»() { do_work(); } -// fn do_work() { test(); } -// "}); - -// // Assert no link highlights after jump -// cx.assert_editor_text_highlights::(indoc! {" -// fn test() { do_work(); } -// fn do_work() { test(); } -// "}); - -// // Cmd click without existing definition requests and jumps -// let hover_point = cx.display_point(indoc! {" -// fn test() { do_wˇork(); } -// fn do_work() { test(); } -// "}); -// let target_range = cx.lsp_range(indoc! {" -// fn test() { do_work(); } -// fn «do_work»() { test(); } -// "}); - -// let mut requests = cx.handle_request::(move |url, _, _| async move { -// Ok(Some(lsp::GotoDefinitionResponse::Link(vec![ -// lsp::LocationLink { -// origin_selection_range: None, -// target_uri: url, -// target_range, -// target_selection_range: target_range, -// }, -// ]))) -// }); -// cx.update_editor(|editor, cx| { -// go_to_fetched_definition(editor, PointForPosition::valid(hover_point), false, cx); -// }); -// requests.next().await; -// cx.foreground().run_until_parked(); -// cx.assert_editor_state(indoc! {" -// fn test() { do_work(); } -// fn «do_workˇ»() { test(); } -// "}); - -// // 1. We have a pending selection, mouse point is over a symbol that we have a response for, hitting cmd and nothing happens -// // 2. Selection is completed, hovering -// let hover_point = cx.display_point(indoc! {" -// fn test() { do_wˇork(); } -// fn do_work() { test(); } -// "}); -// let target_range = cx.lsp_range(indoc! {" -// fn test() { do_work(); } -// fn «do_work»() { test(); } -// "}); -// let mut requests = cx.handle_request::(move |url, _, _| async move { -// Ok(Some(lsp::GotoDefinitionResponse::Link(vec![ -// lsp::LocationLink { -// origin_selection_range: None, -// target_uri: url, -// target_range, -// target_selection_range: target_range, -// }, -// ]))) -// }); - -// // create a pending selection -// let selection_range = cx.ranges(indoc! {" -// fn «test() { do_w»ork(); } -// fn do_work() { test(); } -// "})[0] -// .clone(); -// cx.update_editor(|editor, cx| { -// let snapshot = editor.buffer().read(cx).snapshot(cx); -// let anchor_range = snapshot.anchor_before(selection_range.start) -// ..snapshot.anchor_after(selection_range.end); -// editor.change_selections(Some(crate::Autoscroll::fit()), cx, |s| { -// s.set_pending_anchor_range(anchor_range, crate::SelectMode::Character) -// }); -// }); -// cx.update_editor(|editor, cx| { -// update_go_to_definition_link( -// editor, -// Some(GoToDefinitionTrigger::Text(hover_point)), -// true, -// false, -// cx, -// ); -// }); -// cx.foreground().run_until_parked(); -// assert!(requests.try_next().is_err()); -// cx.assert_editor_text_highlights::(indoc! {" -// fn test() { do_work(); } -// fn do_work() { test(); } -// "}); -// cx.foreground().run_until_parked(); -// } - -// #[gpui::test] -// async fn test_link_go_to_inlay(cx: &mut gpui::TestAppContext) { -// init_test(cx, |settings| { -// settings.defaults.inlay_hints = Some(InlayHintSettings { -// enabled: true, -// show_type_hints: true, -// show_parameter_hints: true, -// show_other_hints: true, -// }) -// }); - -// let mut cx = EditorLspTestContext::new_rust( -// lsp::ServerCapabilities { -// inlay_hint_provider: Some(lsp::OneOf::Left(true)), -// ..Default::default() -// }, -// cx, -// ) -// .await; -// cx.set_state(indoc! {" -// struct TestStruct; - -// fn main() { -// let variableˇ = TestStruct; -// } -// "}); -// let hint_start_offset = cx.ranges(indoc! {" -// struct TestStruct; - -// fn main() { -// let variableˇ = TestStruct; -// } -// "})[0] -// .start; -// let hint_position = cx.to_lsp(hint_start_offset); -// let target_range = cx.lsp_range(indoc! {" -// struct «TestStruct»; - -// fn main() { -// let variable = TestStruct; -// } -// "}); - -// let expected_uri = cx.buffer_lsp_url.clone(); -// let hint_label = ": TestStruct"; -// cx.lsp -// .handle_request::(move |params, _| { -// let expected_uri = expected_uri.clone(); -// async move { -// assert_eq!(params.text_document.uri, expected_uri); -// Ok(Some(vec![lsp::InlayHint { -// position: hint_position, -// label: lsp::InlayHintLabel::LabelParts(vec![lsp::InlayHintLabelPart { -// value: hint_label.to_string(), -// location: Some(lsp::Location { -// uri: params.text_document.uri, -// range: target_range, -// }), -// ..Default::default() -// }]), -// kind: Some(lsp::InlayHintKind::TYPE), -// text_edits: None, -// tooltip: None, -// padding_left: Some(false), -// padding_right: Some(false), -// data: None, -// }])) -// } -// }) -// .next() -// .await; -// cx.foreground().run_until_parked(); -// cx.update_editor(|editor, cx| { -// let expected_layers = vec![hint_label.to_string()]; -// assert_eq!(expected_layers, cached_hint_labels(editor)); -// assert_eq!(expected_layers, visible_hint_labels(editor, cx)); -// }); - -// let inlay_range = cx -// .ranges(indoc! {" -// struct TestStruct; - -// fn main() { -// let variable« »= TestStruct; -// } -// "}) -// .get(0) -// .cloned() -// .unwrap(); -// let hint_hover_position = cx.update_editor(|editor, cx| { -// let snapshot = editor.snapshot(cx); -// let previous_valid = inlay_range.start.to_display_point(&snapshot); -// let next_valid = inlay_range.end.to_display_point(&snapshot); -// assert_eq!(previous_valid.row(), next_valid.row()); -// assert!(previous_valid.column() < next_valid.column()); -// let exact_unclipped = DisplayPoint::new( -// previous_valid.row(), -// previous_valid.column() + (hint_label.len() / 2) as u32, -// ); -// PointForPosition { -// previous_valid, -// next_valid, -// exact_unclipped, -// column_overshoot_after_line_end: 0, -// } -// }); -// // Press cmd to trigger highlight -// cx.update_editor(|editor, cx| { -// update_inlay_link_and_hover_points( -// &editor.snapshot(cx), -// hint_hover_position, -// editor, -// true, -// false, -// cx, -// ); -// }); -// cx.foreground().run_until_parked(); -// cx.update_editor(|editor, cx| { -// let snapshot = editor.snapshot(cx); -// let actual_highlights = snapshot -// .inlay_highlights::() -// .into_iter() -// .flat_map(|highlights| highlights.values().map(|(_, highlight)| highlight)) -// .collect::>(); - -// let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx)); -// let expected_highlight = InlayHighlight { -// inlay: InlayId::Hint(0), -// inlay_position: buffer_snapshot.anchor_at(inlay_range.start, Bias::Right), -// range: 0..hint_label.len(), -// }; -// assert_set_eq!(actual_highlights, vec![&expected_highlight]); -// }); - -// // Unpress cmd causes highlight to go away -// cx.update_editor(|editor, cx| { -// editor.modifiers_changed( -// &platform::ModifiersChangedEvent { -// modifiers: Modifiers { -// cmd: false, -// ..Default::default() -// }, -// ..Default::default() -// }, -// cx, -// ); -// }); -// // Assert no link highlights -// cx.update_editor(|editor, cx| { -// let snapshot = editor.snapshot(cx); -// let actual_ranges = snapshot -// .text_highlight_ranges::() -// .map(|ranges| ranges.as_ref().clone().1) -// .unwrap_or_default(); - -// assert!(actual_ranges.is_empty(), "When no cmd is pressed, should have no hint label selected, but got: {actual_ranges:?}"); -// }); - -// // Cmd+click without existing definition requests and jumps -// cx.update_editor(|editor, cx| { -// editor.modifiers_changed( -// &platform::ModifiersChangedEvent { -// modifiers: Modifiers { -// cmd: true, -// ..Default::default() -// }, -// ..Default::default() -// }, -// cx, -// ); -// update_inlay_link_and_hover_points( -// &editor.snapshot(cx), -// hint_hover_position, -// editor, -// true, -// false, -// cx, -// ); -// }); -// cx.foreground().run_until_parked(); -// cx.update_editor(|editor, cx| { -// go_to_fetched_type_definition(editor, hint_hover_position, false, cx); -// }); -// cx.foreground().run_until_parked(); -// cx.assert_editor_state(indoc! {" -// struct «TestStructˇ»; - -// fn main() { -// let variable = TestStruct; -// } -// "}); -// } -// } +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + display_map::ToDisplayPoint, + editor_tests::init_test, + inlay_hint_cache::tests::{cached_hint_labels, visible_hint_labels}, + test::editor_lsp_test_context::EditorLspTestContext, + }; + use futures::StreamExt; + use gpui::{Modifiers, ModifiersChangedEvent, View}; + use indoc::indoc; + use language::language_settings::InlayHintSettings; + use lsp::request::{GotoDefinition, GotoTypeDefinition}; + use util::assert_set_eq; + + #[gpui::test] + async fn test_link_go_to_type_definition(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorLspTestContext::new_rust( + lsp::ServerCapabilities { + hover_provider: Some(lsp::HoverProviderCapability::Simple(true)), + type_definition_provider: Some(lsp::TypeDefinitionProviderCapability::Simple(true)), + ..Default::default() + }, + cx, + ) + .await; + + cx.set_state(indoc! {" + struct A; + let vˇariable = A; + "}); + + // Basic hold cmd+shift, expect highlight in region if response contains type definition + let hover_point = cx.display_point(indoc! {" + struct A; + let vˇariable = A; + "}); + let symbol_range = cx.lsp_range(indoc! {" + struct A; + let «variable» = A; + "}); + let target_range = cx.lsp_range(indoc! {" + struct «A»; + let variable = A; + "}); + + let mut requests = + cx.handle_request::(move |url, _, _| async move { + Ok(Some(lsp::GotoTypeDefinitionResponse::Link(vec![ + lsp::LocationLink { + origin_selection_range: Some(symbol_range), + target_uri: url.clone(), + target_range, + target_selection_range: target_range, + }, + ]))) + }); + + // Press cmd+shift to trigger highlight + cx.update_editor(|editor, cx| { + update_go_to_definition_link( + editor, + Some(GoToDefinitionTrigger::Text(hover_point)), + true, + true, + cx, + ); + }); + requests.next().await; + cx.background_executor.run_until_parked(); + cx.assert_editor_text_highlights::(indoc! {" + struct A; + let «variable» = A; + "}); + + // Unpress shift causes highlight to go away (normal goto-definition is not valid here) + cx.update_editor(|editor, cx| { + crate::element::EditorElement::modifiers_changed( + editor, + &ModifiersChangedEvent { + modifiers: Modifiers { + command: true, + ..Default::default() + }, + ..Default::default() + }, + cx, + ); + }); + // Assert no link highlights + cx.assert_editor_text_highlights::(indoc! {" + struct A; + let variable = A; + "}); + + // Cmd+shift click without existing definition requests and jumps + let hover_point = cx.display_point(indoc! {" + struct A; + let vˇariable = A; + "}); + let target_range = cx.lsp_range(indoc! {" + struct «A»; + let variable = A; + "}); + + let mut requests = + cx.handle_request::(move |url, _, _| async move { + Ok(Some(lsp::GotoTypeDefinitionResponse::Link(vec![ + lsp::LocationLink { + origin_selection_range: None, + target_uri: url, + target_range, + target_selection_range: target_range, + }, + ]))) + }); + + cx.update_editor(|editor, cx| { + go_to_fetched_type_definition(editor, PointForPosition::valid(hover_point), false, cx); + }); + requests.next().await; + cx.background_executor.run_until_parked(); + + cx.assert_editor_state(indoc! {" + struct «Aˇ»; + let variable = A; + "}); + } + + #[gpui::test] + async fn test_link_go_to_definition(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorLspTestContext::new_rust( + lsp::ServerCapabilities { + hover_provider: Some(lsp::HoverProviderCapability::Simple(true)), + ..Default::default() + }, + cx, + ) + .await; + + cx.set_state(indoc! {" + fn ˇtest() { do_work(); } + fn do_work() { test(); } + "}); + + // Basic hold cmd, expect highlight in region if response contains definition + let hover_point = cx.display_point(indoc! {" + fn test() { do_wˇork(); } + fn do_work() { test(); } + "}); + let symbol_range = cx.lsp_range(indoc! {" + fn test() { «do_work»(); } + fn do_work() { test(); } + "}); + let target_range = cx.lsp_range(indoc! {" + fn test() { do_work(); } + fn «do_work»() { test(); } + "}); + + let mut requests = cx.handle_request::(move |url, _, _| async move { + Ok(Some(lsp::GotoDefinitionResponse::Link(vec![ + lsp::LocationLink { + origin_selection_range: Some(symbol_range), + target_uri: url.clone(), + target_range, + target_selection_range: target_range, + }, + ]))) + }); + + cx.update_editor(|editor, cx| { + update_go_to_definition_link( + editor, + Some(GoToDefinitionTrigger::Text(hover_point)), + true, + false, + cx, + ); + }); + requests.next().await; + cx.background_executor.run_until_parked(); + cx.assert_editor_text_highlights::(indoc! {" + fn test() { «do_work»(); } + fn do_work() { test(); } + "}); + + // Unpress cmd causes highlight to go away + cx.update_editor(|editor, cx| { + crate::element::EditorElement::modifiers_changed(editor, &Default::default(), cx); + }); + + // Assert no link highlights + cx.assert_editor_text_highlights::(indoc! {" + fn test() { do_work(); } + fn do_work() { test(); } + "}); + + // Response without source range still highlights word + cx.update_editor(|editor, _| editor.link_go_to_definition_state.last_trigger_point = None); + let mut requests = cx.handle_request::(move |url, _, _| async move { + Ok(Some(lsp::GotoDefinitionResponse::Link(vec![ + lsp::LocationLink { + // No origin range + origin_selection_range: None, + target_uri: url.clone(), + target_range, + target_selection_range: target_range, + }, + ]))) + }); + cx.update_editor(|editor, cx| { + update_go_to_definition_link( + editor, + Some(GoToDefinitionTrigger::Text(hover_point)), + true, + false, + cx, + ); + }); + requests.next().await; + cx.background_executor.run_until_parked(); + + cx.assert_editor_text_highlights::(indoc! {" + fn test() { «do_work»(); } + fn do_work() { test(); } + "}); + + // Moving mouse to location with no response dismisses highlight + let hover_point = cx.display_point(indoc! {" + fˇn test() { do_work(); } + fn do_work() { test(); } + "}); + let mut requests = cx + .lsp + .handle_request::(move |_, _| async move { + // No definitions returned + Ok(Some(lsp::GotoDefinitionResponse::Link(vec![]))) + }); + cx.update_editor(|editor, cx| { + update_go_to_definition_link( + editor, + Some(GoToDefinitionTrigger::Text(hover_point)), + true, + false, + cx, + ); + }); + requests.next().await; + cx.background_executor.run_until_parked(); + + // Assert no link highlights + cx.assert_editor_text_highlights::(indoc! {" + fn test() { do_work(); } + fn do_work() { test(); } + "}); + + // Move mouse without cmd and then pressing cmd triggers highlight + let hover_point = cx.display_point(indoc! {" + fn test() { do_work(); } + fn do_work() { teˇst(); } + "}); + cx.update_editor(|editor, cx| { + update_go_to_definition_link( + editor, + Some(GoToDefinitionTrigger::Text(hover_point)), + false, + false, + cx, + ); + }); + cx.background_executor.run_until_parked(); + + // Assert no link highlights + cx.assert_editor_text_highlights::(indoc! {" + fn test() { do_work(); } + fn do_work() { test(); } + "}); + + let symbol_range = cx.lsp_range(indoc! {" + fn test() { do_work(); } + fn do_work() { «test»(); } + "}); + let target_range = cx.lsp_range(indoc! {" + fn «test»() { do_work(); } + fn do_work() { test(); } + "}); + + let mut requests = cx.handle_request::(move |url, _, _| async move { + Ok(Some(lsp::GotoDefinitionResponse::Link(vec![ + lsp::LocationLink { + origin_selection_range: Some(symbol_range), + target_uri: url, + target_range, + target_selection_range: target_range, + }, + ]))) + }); + cx.update_editor(|editor, cx| { + crate::element::EditorElement::modifiers_changed( + editor, + &ModifiersChangedEvent { + modifiers: Modifiers { + command: true, + ..Default::default() + }, + }, + cx, + ); + }); + requests.next().await; + cx.background_executor.run_until_parked(); + + cx.assert_editor_text_highlights::(indoc! {" + fn test() { do_work(); } + fn do_work() { «test»(); } + "}); + + // Deactivating the window dismisses the highlight + cx.update_workspace(|workspace, cx| { + workspace.on_window_activation_changed(cx); + }); + cx.assert_editor_text_highlights::(indoc! {" + fn test() { do_work(); } + fn do_work() { test(); } + "}); + + // Moving the mouse restores the highlights. + cx.update_editor(|editor, cx| { + update_go_to_definition_link( + editor, + Some(GoToDefinitionTrigger::Text(hover_point)), + true, + false, + cx, + ); + }); + cx.background_executor.run_until_parked(); + cx.assert_editor_text_highlights::(indoc! {" + fn test() { do_work(); } + fn do_work() { «test»(); } + "}); + + // Moving again within the same symbol range doesn't re-request + let hover_point = cx.display_point(indoc! {" + fn test() { do_work(); } + fn do_work() { tesˇt(); } + "}); + cx.update_editor(|editor, cx| { + update_go_to_definition_link( + editor, + Some(GoToDefinitionTrigger::Text(hover_point)), + true, + false, + cx, + ); + }); + cx.background_executor.run_until_parked(); + cx.assert_editor_text_highlights::(indoc! {" + fn test() { do_work(); } + fn do_work() { «test»(); } + "}); + + // Cmd click with existing definition doesn't re-request and dismisses highlight + cx.update_editor(|editor, cx| { + go_to_fetched_definition(editor, PointForPosition::valid(hover_point), false, cx); + }); + // Assert selection moved to to definition + cx.lsp + .handle_request::(move |_, _| async move { + // Empty definition response to make sure we aren't hitting the lsp and using + // the cached location instead + Ok(Some(lsp::GotoDefinitionResponse::Link(vec![]))) + }); + cx.background_executor.run_until_parked(); + cx.assert_editor_state(indoc! {" + fn «testˇ»() { do_work(); } + fn do_work() { test(); } + "}); + + // Assert no link highlights after jump + cx.assert_editor_text_highlights::(indoc! {" + fn test() { do_work(); } + fn do_work() { test(); } + "}); + + // Cmd click without existing definition requests and jumps + let hover_point = cx.display_point(indoc! {" + fn test() { do_wˇork(); } + fn do_work() { test(); } + "}); + let target_range = cx.lsp_range(indoc! {" + fn test() { do_work(); } + fn «do_work»() { test(); } + "}); + + let mut requests = cx.handle_request::(move |url, _, _| async move { + Ok(Some(lsp::GotoDefinitionResponse::Link(vec![ + lsp::LocationLink { + origin_selection_range: None, + target_uri: url, + target_range, + target_selection_range: target_range, + }, + ]))) + }); + cx.update_editor(|editor, cx| { + go_to_fetched_definition(editor, PointForPosition::valid(hover_point), false, cx); + }); + requests.next().await; + cx.background_executor.run_until_parked(); + cx.assert_editor_state(indoc! {" + fn test() { do_work(); } + fn «do_workˇ»() { test(); } + "}); + + // 1. We have a pending selection, mouse point is over a symbol that we have a response for, hitting cmd and nothing happens + // 2. Selection is completed, hovering + let hover_point = cx.display_point(indoc! {" + fn test() { do_wˇork(); } + fn do_work() { test(); } + "}); + let target_range = cx.lsp_range(indoc! {" + fn test() { do_work(); } + fn «do_work»() { test(); } + "}); + let mut requests = cx.handle_request::(move |url, _, _| async move { + Ok(Some(lsp::GotoDefinitionResponse::Link(vec![ + lsp::LocationLink { + origin_selection_range: None, + target_uri: url, + target_range, + target_selection_range: target_range, + }, + ]))) + }); + + // create a pending selection + let selection_range = cx.ranges(indoc! {" + fn «test() { do_w»ork(); } + fn do_work() { test(); } + "})[0] + .clone(); + cx.update_editor(|editor, cx| { + let snapshot = editor.buffer().read(cx).snapshot(cx); + let anchor_range = snapshot.anchor_before(selection_range.start) + ..snapshot.anchor_after(selection_range.end); + editor.change_selections(Some(crate::Autoscroll::fit()), cx, |s| { + s.set_pending_anchor_range(anchor_range, crate::SelectMode::Character) + }); + }); + cx.update_editor(|editor, cx| { + update_go_to_definition_link( + editor, + Some(GoToDefinitionTrigger::Text(hover_point)), + true, + false, + cx, + ); + }); + cx.background_executor.run_until_parked(); + assert!(requests.try_next().is_err()); + cx.assert_editor_text_highlights::(indoc! {" + fn test() { do_work(); } + fn do_work() { test(); } + "}); + cx.background_executor.run_until_parked(); + } + + #[gpui::test] + async fn test_link_go_to_inlay(cx: &mut gpui::TestAppContext) { + init_test(cx, |settings| { + settings.defaults.inlay_hints = Some(InlayHintSettings { + enabled: true, + show_type_hints: true, + show_parameter_hints: true, + show_other_hints: true, + }) + }); + + let mut cx = EditorLspTestContext::new_rust( + lsp::ServerCapabilities { + inlay_hint_provider: Some(lsp::OneOf::Left(true)), + ..Default::default() + }, + cx, + ) + .await; + cx.set_state(indoc! {" + struct TestStruct; + + fn main() { + let variableˇ = TestStruct; + } + "}); + let hint_start_offset = cx.ranges(indoc! {" + struct TestStruct; + + fn main() { + let variableˇ = TestStruct; + } + "})[0] + .start; + let hint_position = cx.to_lsp(hint_start_offset); + let target_range = cx.lsp_range(indoc! {" + struct «TestStruct»; + + fn main() { + let variable = TestStruct; + } + "}); + + let expected_uri = cx.buffer_lsp_url.clone(); + let hint_label = ": TestStruct"; + cx.lsp + .handle_request::(move |params, _| { + let expected_uri = expected_uri.clone(); + async move { + assert_eq!(params.text_document.uri, expected_uri); + Ok(Some(vec![lsp::InlayHint { + position: hint_position, + label: lsp::InlayHintLabel::LabelParts(vec![lsp::InlayHintLabelPart { + value: hint_label.to_string(), + location: Some(lsp::Location { + uri: params.text_document.uri, + range: target_range, + }), + ..Default::default() + }]), + kind: Some(lsp::InlayHintKind::TYPE), + text_edits: None, + tooltip: None, + padding_left: Some(false), + padding_right: Some(false), + data: None, + }])) + } + }) + .next() + .await; + cx.background_executor.run_until_parked(); + cx.update_editor(|editor, cx| { + let expected_layers = vec![hint_label.to_string()]; + assert_eq!(expected_layers, cached_hint_labels(editor)); + assert_eq!(expected_layers, visible_hint_labels(editor, cx)); + }); + + let inlay_range = cx + .ranges(indoc! {" + struct TestStruct; + + fn main() { + let variable« »= TestStruct; + } + "}) + .get(0) + .cloned() + .unwrap(); + let hint_hover_position = cx.update_editor(|editor, cx| { + let snapshot = editor.snapshot(cx); + let previous_valid = inlay_range.start.to_display_point(&snapshot); + let next_valid = inlay_range.end.to_display_point(&snapshot); + assert_eq!(previous_valid.row(), next_valid.row()); + assert!(previous_valid.column() < next_valid.column()); + let exact_unclipped = DisplayPoint::new( + previous_valid.row(), + previous_valid.column() + (hint_label.len() / 2) as u32, + ); + PointForPosition { + previous_valid, + next_valid, + exact_unclipped, + column_overshoot_after_line_end: 0, + } + }); + // Press cmd to trigger highlight + cx.update_editor(|editor, cx| { + update_inlay_link_and_hover_points( + &editor.snapshot(cx), + hint_hover_position, + editor, + true, + false, + cx, + ); + }); + cx.background_executor.run_until_parked(); + cx.update_editor(|editor, cx| { + let snapshot = editor.snapshot(cx); + let actual_highlights = snapshot + .inlay_highlights::() + .into_iter() + .flat_map(|highlights| highlights.values().map(|(_, highlight)| highlight)) + .collect::>(); + + let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx)); + let expected_highlight = InlayHighlight { + inlay: InlayId::Hint(0), + inlay_position: buffer_snapshot.anchor_at(inlay_range.start, Bias::Right), + range: 0..hint_label.len(), + }; + assert_set_eq!(actual_highlights, vec![&expected_highlight]); + }); + + // Unpress cmd causes highlight to go away + cx.update_editor(|editor, cx| { + crate::element::EditorElement::modifiers_changed( + editor, + &ModifiersChangedEvent { + modifiers: Modifiers { + command: false, + ..Default::default() + }, + ..Default::default() + }, + cx, + ); + }); + // Assert no link highlights + cx.update_editor(|editor, cx| { + let snapshot = editor.snapshot(cx); + let actual_ranges = snapshot + .text_highlight_ranges::() + .map(|ranges| ranges.as_ref().clone().1) + .unwrap_or_default(); + + assert!(actual_ranges.is_empty(), "When no cmd is pressed, should have no hint label selected, but got: {actual_ranges:?}"); + }); + + // Cmd+click without existing definition requests and jumps + cx.update_editor(|editor, cx| { + crate::element::EditorElement::modifiers_changed( + editor, + &ModifiersChangedEvent { + modifiers: Modifiers { + command: true, + ..Default::default() + }, + ..Default::default() + }, + cx, + ); + update_inlay_link_and_hover_points( + &editor.snapshot(cx), + hint_hover_position, + editor, + true, + false, + cx, + ); + }); + cx.background_executor.run_until_parked(); + cx.update_editor(|editor, cx| { + go_to_fetched_type_definition(editor, hint_hover_position, false, cx); + }); + cx.background_executor.run_until_parked(); + cx.assert_editor_state(indoc! {" + struct «TestStructˇ»; + + fn main() { + let variable = TestStruct; + } + "}); + } +} From 53f3f960d2cf82200a617843d8cfd28aefb1d9dc Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Fri, 1 Dec 2023 18:43:44 +0100 Subject: [PATCH 03/35] Another batch of tests --- crates/editor2/src/display_map/wrap_map.rs | 668 +++++++-------- crates/editor2/src/inlay_hint_cache.rs | 680 +++++++-------- crates/editor2/src/movement.rs | 952 ++++++++++----------- 3 files changed, 1146 insertions(+), 1154 deletions(-) diff --git a/crates/editor2/src/display_map/wrap_map.rs b/crates/editor2/src/display_map/wrap_map.rs index c8025c7da91a32cf00665669c43bcd8cffd63d35..c2325fa96d711de7bc49558dfcd4ed760a9fb4be 100644 --- a/crates/editor2/src/display_map/wrap_map.rs +++ b/crates/editor2/src/display_map/wrap_map.rs @@ -1026,337 +1026,337 @@ fn consolidate_wrap_edits(edits: &mut Vec) { } } -#[cfg(test)] -mod tests { - use super::*; - use crate::{ - display_map::{fold_map::FoldMap, inlay_map::InlayMap, tab_map::TabMap}, - MultiBuffer, - }; - use gpui::test::observe; - use rand::prelude::*; - use settings::SettingsStore; - use smol::stream::StreamExt; - use std::{cmp, env, num::NonZeroU32}; - use text::Rope; - - #[gpui::test(iterations = 100)] - async fn test_random_wraps(cx: &mut gpui::TestAppContext, mut rng: StdRng) { - init_test(cx); - - cx.background_executor.set_block_on_ticks(0..=50); - let operations = env::var("OPERATIONS") - .map(|i| i.parse().expect("invalid `OPERATIONS` variable")) - .unwrap_or(10); - - let font_cache = cx.read(|cx| cx.font_cache().clone()); - let font_system = cx.platform().fonts(); - let mut wrap_width = if rng.gen_bool(0.1) { - None - } else { - Some(rng.gen_range(0.0..=1000.0)) - }; - let tab_size = NonZeroU32::new(rng.gen_range(1..=4)).unwrap(); - let family_id = font_cache - .load_family(&["Helvetica"], &Default::default()) - .unwrap(); - let font_id = font_cache - .select_font(family_id, &Default::default()) - .unwrap(); - let font_size = 14.0; - - log::info!("Tab size: {}", tab_size); - log::info!("Wrap width: {:?}", wrap_width); - - let buffer = cx.update(|cx| { - if rng.gen() { - MultiBuffer::build_random(&mut rng, cx) - } else { - let len = rng.gen_range(0..10); - let text = util::RandomCharIter::new(&mut rng) - .take(len) - .collect::(); - MultiBuffer::build_simple(&text, cx) - } - }); - let mut buffer_snapshot = buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx)); - log::info!("Buffer text: {:?}", buffer_snapshot.text()); - let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone()); - log::info!("InlayMap text: {:?}", inlay_snapshot.text()); - let (mut fold_map, fold_snapshot) = FoldMap::new(inlay_snapshot.clone()); - log::info!("FoldMap text: {:?}", fold_snapshot.text()); - let (mut tab_map, _) = TabMap::new(fold_snapshot.clone(), tab_size); - let tabs_snapshot = tab_map.set_max_expansion_column(32); - log::info!("TabMap text: {:?}", tabs_snapshot.text()); - - let mut line_wrapper = LineWrapper::new(font_id, font_size, font_system); - let unwrapped_text = tabs_snapshot.text(); - let expected_text = wrap_text(&unwrapped_text, wrap_width, &mut line_wrapper); - - let (wrap_map, _) = - cx.update(|cx| WrapMap::new(tabs_snapshot.clone(), font_id, font_size, wrap_width, cx)); - let mut notifications = observe(&wrap_map, cx); - - if wrap_map.read_with(cx, |map, _| map.is_rewrapping()) { - notifications.next().await.unwrap(); - } - - let (initial_snapshot, _) = wrap_map.update(cx, |map, cx| { - assert!(!map.is_rewrapping()); - map.sync(tabs_snapshot.clone(), Vec::new(), cx) - }); - - let actual_text = initial_snapshot.text(); - assert_eq!( - actual_text, expected_text, - "unwrapped text is: {:?}", - unwrapped_text - ); - log::info!("Wrapped text: {:?}", actual_text); - - let mut next_inlay_id = 0; - let mut edits = Vec::new(); - for _i in 0..operations { - log::info!("{} ==============================================", _i); - - let mut buffer_edits = Vec::new(); - match rng.gen_range(0..=100) { - 0..=19 => { - wrap_width = if rng.gen_bool(0.2) { - None - } else { - Some(rng.gen_range(0.0..=1000.0)) - }; - log::info!("Setting wrap width to {:?}", wrap_width); - wrap_map.update(cx, |map, cx| map.set_wrap_width(wrap_width, cx)); - } - 20..=39 => { - for (fold_snapshot, fold_edits) in fold_map.randomly_mutate(&mut rng) { - let (tabs_snapshot, tab_edits) = - tab_map.sync(fold_snapshot, fold_edits, tab_size); - let (mut snapshot, wrap_edits) = - wrap_map.update(cx, |map, cx| map.sync(tabs_snapshot, tab_edits, cx)); - snapshot.check_invariants(); - snapshot.verify_chunks(&mut rng); - edits.push((snapshot, wrap_edits)); - } - } - 40..=59 => { - let (inlay_snapshot, inlay_edits) = - inlay_map.randomly_mutate(&mut next_inlay_id, &mut rng); - let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits); - let (tabs_snapshot, tab_edits) = - tab_map.sync(fold_snapshot, fold_edits, tab_size); - let (mut snapshot, wrap_edits) = - wrap_map.update(cx, |map, cx| map.sync(tabs_snapshot, tab_edits, cx)); - snapshot.check_invariants(); - snapshot.verify_chunks(&mut rng); - edits.push((snapshot, wrap_edits)); - } - _ => { - buffer.update(cx, |buffer, cx| { - let subscription = buffer.subscribe(); - let edit_count = rng.gen_range(1..=5); - buffer.randomly_mutate(&mut rng, edit_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); - log::info!("InlayMap text: {:?}", inlay_snapshot.text()); - let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits); - log::info!("FoldMap text: {:?}", fold_snapshot.text()); - let (tabs_snapshot, tab_edits) = tab_map.sync(fold_snapshot, fold_edits, tab_size); - log::info!("TabMap text: {:?}", tabs_snapshot.text()); - - let unwrapped_text = tabs_snapshot.text(); - let expected_text = wrap_text(&unwrapped_text, wrap_width, &mut line_wrapper); - let (mut snapshot, wrap_edits) = - wrap_map.update(cx, |map, cx| map.sync(tabs_snapshot.clone(), tab_edits, cx)); - snapshot.check_invariants(); - snapshot.verify_chunks(&mut rng); - edits.push((snapshot, wrap_edits)); - - if wrap_map.read_with(cx, |map, _| map.is_rewrapping()) && rng.gen_bool(0.4) { - log::info!("Waiting for wrapping to finish"); - while wrap_map.read_with(cx, |map, _| map.is_rewrapping()) { - notifications.next().await.unwrap(); - } - wrap_map.read_with(cx, |map, _| assert!(map.pending_edits.is_empty())); - } - - if !wrap_map.read_with(cx, |map, _| map.is_rewrapping()) { - let (mut wrapped_snapshot, wrap_edits) = - wrap_map.update(cx, |map, cx| map.sync(tabs_snapshot, Vec::new(), cx)); - let actual_text = wrapped_snapshot.text(); - let actual_longest_row = wrapped_snapshot.longest_row(); - log::info!("Wrapping finished: {:?}", actual_text); - wrapped_snapshot.check_invariants(); - wrapped_snapshot.verify_chunks(&mut rng); - edits.push((wrapped_snapshot.clone(), wrap_edits)); - assert_eq!( - actual_text, expected_text, - "unwrapped text is: {:?}", - unwrapped_text - ); - - let mut summary = TextSummary::default(); - for (ix, item) in wrapped_snapshot - .transforms - .items(&()) - .into_iter() - .enumerate() - { - summary += &item.summary.output; - log::info!("{} summary: {:?}", ix, item.summary.output,); - } - - if tab_size.get() == 1 - || !wrapped_snapshot - .tab_snapshot - .fold_snapshot - .text() - .contains('\t') - { - let mut expected_longest_rows = Vec::new(); - let mut longest_line_len = -1; - for (row, line) in expected_text.split('\n').enumerate() { - let line_char_count = line.chars().count() as isize; - if line_char_count > longest_line_len { - expected_longest_rows.clear(); - longest_line_len = line_char_count; - } - if line_char_count >= longest_line_len { - expected_longest_rows.push(row as u32); - } - } - - assert!( - expected_longest_rows.contains(&actual_longest_row), - "incorrect longest row {}. expected {:?} with length {}", - actual_longest_row, - expected_longest_rows, - longest_line_len, - ) - } - } - } - - let mut initial_text = Rope::from(initial_snapshot.text().as_str()); - for (snapshot, patch) in edits { - let snapshot_text = Rope::from(snapshot.text().as_str()); - for edit in &patch { - let old_start = initial_text.point_to_offset(Point::new(edit.new.start, 0)); - let old_end = initial_text.point_to_offset(cmp::min( - Point::new(edit.new.start + edit.old.len() as u32, 0), - initial_text.max_point(), - )); - let new_start = snapshot_text.point_to_offset(Point::new(edit.new.start, 0)); - let new_end = snapshot_text.point_to_offset(cmp::min( - Point::new(edit.new.end, 0), - snapshot_text.max_point(), - )); - let new_text = snapshot_text - .chunks_in_range(new_start..new_end) - .collect::(); - - initial_text.replace(old_start..old_end, &new_text); - } - assert_eq!(initial_text.to_string(), snapshot_text.to_string()); - } - - if wrap_map.read_with(cx, |map, _| map.is_rewrapping()) { - log::info!("Waiting for wrapping to finish"); - while wrap_map.read_with(cx, |map, _| map.is_rewrapping()) { - notifications.next().await.unwrap(); - } - } - wrap_map.read_with(cx, |map, _| assert!(map.pending_edits.is_empty())); - } - - fn init_test(cx: &mut gpui::TestAppContext) { - cx.foreground_executor().forbid_parking(); - cx.update(|cx| { - cx.set_global(SettingsStore::test(cx)); - theme::init((), cx); - }); - } - - fn wrap_text( - unwrapped_text: &str, - wrap_width: Option, - line_wrapper: &mut LineWrapper, - ) -> String { - if let Some(wrap_width) = wrap_width { - let mut wrapped_text = String::new(); - for (row, line) in unwrapped_text.split('\n').enumerate() { - if row > 0 { - wrapped_text.push('\n') - } - - let mut prev_ix = 0; - for boundary in line_wrapper.wrap_line(line, wrap_width) { - wrapped_text.push_str(&line[prev_ix..boundary.ix]); - wrapped_text.push('\n'); - wrapped_text.push_str(&" ".repeat(boundary.next_indent as usize)); - prev_ix = boundary.ix; - } - wrapped_text.push_str(&line[prev_ix..]); - } - wrapped_text - } else { - unwrapped_text.to_string() - } - } - - impl WrapSnapshot { - pub fn text(&self) -> String { - self.text_chunks(0).collect() - } - - pub fn text_chunks(&self, wrap_row: u32) -> impl Iterator { - self.chunks( - wrap_row..self.max_point().row() + 1, - false, - Highlights::default(), - ) - .map(|h| h.text) - } - - fn verify_chunks(&mut self, rng: &mut impl Rng) { - for _ in 0..5 { - let mut end_row = rng.gen_range(0..=self.max_point().row()); - let start_row = rng.gen_range(0..=end_row); - end_row += 1; - - let mut expected_text = self.text_chunks(start_row).collect::(); - if expected_text.ends_with('\n') { - expected_text.push('\n'); - } - let mut expected_text = expected_text - .lines() - .take((end_row - start_row) as usize) - .collect::>() - .join("\n"); - if end_row <= self.max_point().row() { - expected_text.push('\n'); - } - - let actual_text = self - .chunks(start_row..end_row, true, Highlights::default()) - .map(|c| c.text) - .collect::(); - assert_eq!( - expected_text, - actual_text, - "chunks != highlighted_chunks for rows {:?}", - start_row..end_row - ); - } - } - } -} +// #[cfg(test)] +// mod tests { +// use super::*; +// use crate::{ +// display_map::{fold_map::FoldMap, inlay_map::InlayMap, tab_map::TabMap}, +// MultiBuffer, +// }; +// use gpui::test::observe; +// use rand::prelude::*; +// use settings::SettingsStore; +// use smol::stream::StreamExt; +// use std::{cmp, env, num::NonZeroU32}; +// use text::Rope; + +// #[gpui::test(iterations = 100)] +// async fn test_random_wraps(cx: &mut gpui::TestAppContext, mut rng: StdRng) { +// init_test(cx); + +// cx.background_executor.set_block_on_ticks(0..=50); +// let operations = env::var("OPERATIONS") +// .map(|i| i.parse().expect("invalid `OPERATIONS` variable")) +// .unwrap_or(10); + +// let font_cache = cx.read(|cx| cx.font_cache().clone()); +// let font_system = cx.platform().fonts(); +// let mut wrap_width = if rng.gen_bool(0.1) { +// None +// } else { +// Some(rng.gen_range(0.0..=1000.0)) +// }; +// let tab_size = NonZeroU32::new(rng.gen_range(1..=4)).unwrap(); +// let family_id = font_cache +// .load_family(&["Helvetica"], &Default::default()) +// .unwrap(); +// let font_id = font_cache +// .select_font(family_id, &Default::default()) +// .unwrap(); +// let font_size = 14.0; + +// log::info!("Tab size: {}", tab_size); +// log::info!("Wrap width: {:?}", wrap_width); + +// let buffer = cx.update(|cx| { +// if rng.gen() { +// MultiBuffer::build_random(&mut rng, cx) +// } else { +// let len = rng.gen_range(0..10); +// let text = util::RandomCharIter::new(&mut rng) +// .take(len) +// .collect::(); +// MultiBuffer::build_simple(&text, cx) +// } +// }); +// let mut buffer_snapshot = buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx)); +// log::info!("Buffer text: {:?}", buffer_snapshot.text()); +// let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone()); +// log::info!("InlayMap text: {:?}", inlay_snapshot.text()); +// let (mut fold_map, fold_snapshot) = FoldMap::new(inlay_snapshot.clone()); +// log::info!("FoldMap text: {:?}", fold_snapshot.text()); +// let (mut tab_map, _) = TabMap::new(fold_snapshot.clone(), tab_size); +// let tabs_snapshot = tab_map.set_max_expansion_column(32); +// log::info!("TabMap text: {:?}", tabs_snapshot.text()); + +// let mut line_wrapper = LineWrapper::new(font_id, font_size, font_system); +// let unwrapped_text = tabs_snapshot.text(); +// let expected_text = wrap_text(&unwrapped_text, wrap_width, &mut line_wrapper); + +// let (wrap_map, _) = +// cx.update(|cx| WrapMap::new(tabs_snapshot.clone(), font_id, font_size, wrap_width, cx)); +// let mut notifications = observe(&wrap_map, cx); + +// if wrap_map.read_with(cx, |map, _| map.is_rewrapping()) { +// notifications.next().await.unwrap(); +// } + +// let (initial_snapshot, _) = wrap_map.update(cx, |map, cx| { +// assert!(!map.is_rewrapping()); +// map.sync(tabs_snapshot.clone(), Vec::new(), cx) +// }); + +// let actual_text = initial_snapshot.text(); +// assert_eq!( +// actual_text, expected_text, +// "unwrapped text is: {:?}", +// unwrapped_text +// ); +// log::info!("Wrapped text: {:?}", actual_text); + +// let mut next_inlay_id = 0; +// let mut edits = Vec::new(); +// for _i in 0..operations { +// log::info!("{} ==============================================", _i); + +// let mut buffer_edits = Vec::new(); +// match rng.gen_range(0..=100) { +// 0..=19 => { +// wrap_width = if rng.gen_bool(0.2) { +// None +// } else { +// Some(rng.gen_range(0.0..=1000.0)) +// }; +// log::info!("Setting wrap width to {:?}", wrap_width); +// wrap_map.update(cx, |map, cx| map.set_wrap_width(wrap_width, cx)); +// } +// 20..=39 => { +// for (fold_snapshot, fold_edits) in fold_map.randomly_mutate(&mut rng) { +// let (tabs_snapshot, tab_edits) = +// tab_map.sync(fold_snapshot, fold_edits, tab_size); +// let (mut snapshot, wrap_edits) = +// wrap_map.update(cx, |map, cx| map.sync(tabs_snapshot, tab_edits, cx)); +// snapshot.check_invariants(); +// snapshot.verify_chunks(&mut rng); +// edits.push((snapshot, wrap_edits)); +// } +// } +// 40..=59 => { +// let (inlay_snapshot, inlay_edits) = +// inlay_map.randomly_mutate(&mut next_inlay_id, &mut rng); +// let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits); +// let (tabs_snapshot, tab_edits) = +// tab_map.sync(fold_snapshot, fold_edits, tab_size); +// let (mut snapshot, wrap_edits) = +// wrap_map.update(cx, |map, cx| map.sync(tabs_snapshot, tab_edits, cx)); +// snapshot.check_invariants(); +// snapshot.verify_chunks(&mut rng); +// edits.push((snapshot, wrap_edits)); +// } +// _ => { +// buffer.update(cx, |buffer, cx| { +// let subscription = buffer.subscribe(); +// let edit_count = rng.gen_range(1..=5); +// buffer.randomly_mutate(&mut rng, edit_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); +// log::info!("InlayMap text: {:?}", inlay_snapshot.text()); +// let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits); +// log::info!("FoldMap text: {:?}", fold_snapshot.text()); +// let (tabs_snapshot, tab_edits) = tab_map.sync(fold_snapshot, fold_edits, tab_size); +// log::info!("TabMap text: {:?}", tabs_snapshot.text()); + +// let unwrapped_text = tabs_snapshot.text(); +// let expected_text = wrap_text(&unwrapped_text, wrap_width, &mut line_wrapper); +// let (mut snapshot, wrap_edits) = +// wrap_map.update(cx, |map, cx| map.sync(tabs_snapshot.clone(), tab_edits, cx)); +// snapshot.check_invariants(); +// snapshot.verify_chunks(&mut rng); +// edits.push((snapshot, wrap_edits)); + +// if wrap_map.read_with(cx, |map, _| map.is_rewrapping()) && rng.gen_bool(0.4) { +// log::info!("Waiting for wrapping to finish"); +// while wrap_map.read_with(cx, |map, _| map.is_rewrapping()) { +// notifications.next().await.unwrap(); +// } +// wrap_map.read_with(cx, |map, _| assert!(map.pending_edits.is_empty())); +// } + +// if !wrap_map.read_with(cx, |map, _| map.is_rewrapping()) { +// let (mut wrapped_snapshot, wrap_edits) = +// wrap_map.update(cx, |map, cx| map.sync(tabs_snapshot, Vec::new(), cx)); +// let actual_text = wrapped_snapshot.text(); +// let actual_longest_row = wrapped_snapshot.longest_row(); +// log::info!("Wrapping finished: {:?}", actual_text); +// wrapped_snapshot.check_invariants(); +// wrapped_snapshot.verify_chunks(&mut rng); +// edits.push((wrapped_snapshot.clone(), wrap_edits)); +// assert_eq!( +// actual_text, expected_text, +// "unwrapped text is: {:?}", +// unwrapped_text +// ); + +// let mut summary = TextSummary::default(); +// for (ix, item) in wrapped_snapshot +// .transforms +// .items(&()) +// .into_iter() +// .enumerate() +// { +// summary += &item.summary.output; +// log::info!("{} summary: {:?}", ix, item.summary.output,); +// } + +// if tab_size.get() == 1 +// || !wrapped_snapshot +// .tab_snapshot +// .fold_snapshot +// .text() +// .contains('\t') +// { +// let mut expected_longest_rows = Vec::new(); +// let mut longest_line_len = -1; +// for (row, line) in expected_text.split('\n').enumerate() { +// let line_char_count = line.chars().count() as isize; +// if line_char_count > longest_line_len { +// expected_longest_rows.clear(); +// longest_line_len = line_char_count; +// } +// if line_char_count >= longest_line_len { +// expected_longest_rows.push(row as u32); +// } +// } + +// assert!( +// expected_longest_rows.contains(&actual_longest_row), +// "incorrect longest row {}. expected {:?} with length {}", +// actual_longest_row, +// expected_longest_rows, +// longest_line_len, +// ) +// } +// } +// } + +// let mut initial_text = Rope::from(initial_snapshot.text().as_str()); +// for (snapshot, patch) in edits { +// let snapshot_text = Rope::from(snapshot.text().as_str()); +// for edit in &patch { +// let old_start = initial_text.point_to_offset(Point::new(edit.new.start, 0)); +// let old_end = initial_text.point_to_offset(cmp::min( +// Point::new(edit.new.start + edit.old.len() as u32, 0), +// initial_text.max_point(), +// )); +// let new_start = snapshot_text.point_to_offset(Point::new(edit.new.start, 0)); +// let new_end = snapshot_text.point_to_offset(cmp::min( +// Point::new(edit.new.end, 0), +// snapshot_text.max_point(), +// )); +// let new_text = snapshot_text +// .chunks_in_range(new_start..new_end) +// .collect::(); + +// initial_text.replace(old_start..old_end, &new_text); +// } +// assert_eq!(initial_text.to_string(), snapshot_text.to_string()); +// } + +// if wrap_map.read_with(cx, |map, _| map.is_rewrapping()) { +// log::info!("Waiting for wrapping to finish"); +// while wrap_map.read_with(cx, |map, _| map.is_rewrapping()) { +// notifications.next().await.unwrap(); +// } +// } +// wrap_map.read_with(cx, |map, _| assert!(map.pending_edits.is_empty())); +// } + +// fn init_test(cx: &mut gpui::TestAppContext) { +// cx.foreground_executor().forbid_parking(); +// cx.update(|cx| { +// cx.set_global(SettingsStore::test(cx)); +// theme::init((), cx); +// }); +// } + +// fn wrap_text( +// unwrapped_text: &str, +// wrap_width: Option, +// line_wrapper: &mut LineWrapper, +// ) -> String { +// if let Some(wrap_width) = wrap_width { +// let mut wrapped_text = String::new(); +// for (row, line) in unwrapped_text.split('\n').enumerate() { +// if row > 0 { +// wrapped_text.push('\n') +// } + +// let mut prev_ix = 0; +// for boundary in line_wrapper.wrap_line(line, wrap_width) { +// wrapped_text.push_str(&line[prev_ix..boundary.ix]); +// wrapped_text.push('\n'); +// wrapped_text.push_str(&" ".repeat(boundary.next_indent as usize)); +// prev_ix = boundary.ix; +// } +// wrapped_text.push_str(&line[prev_ix..]); +// } +// wrapped_text +// } else { +// unwrapped_text.to_string() +// } +// } + +// impl WrapSnapshot { +// pub fn text(&self) -> String { +// self.text_chunks(0).collect() +// } + +// pub fn text_chunks(&self, wrap_row: u32) -> impl Iterator { +// self.chunks( +// wrap_row..self.max_point().row() + 1, +// false, +// Highlights::default(), +// ) +// .map(|h| h.text) +// } + +// fn verify_chunks(&mut self, rng: &mut impl Rng) { +// for _ in 0..5 { +// let mut end_row = rng.gen_range(0..=self.max_point().row()); +// let start_row = rng.gen_range(0..=end_row); +// end_row += 1; + +// let mut expected_text = self.text_chunks(start_row).collect::(); +// if expected_text.ends_with('\n') { +// expected_text.push('\n'); +// } +// let mut expected_text = expected_text +// .lines() +// .take((end_row - start_row) as usize) +// .collect::>() +// .join("\n"); +// if end_row <= self.max_point().row() { +// expected_text.push('\n'); +// } + +// let actual_text = self +// .chunks(start_row..end_row, true, Highlights::default()) +// .map(|c| c.text) +// .collect::(); +// assert_eq!( +// expected_text, +// actual_text, +// "chunks != highlighted_chunks for rows {:?}", +// start_row..end_row +// ); +// } +// } +// } +// } diff --git a/crates/editor2/src/inlay_hint_cache.rs b/crates/editor2/src/inlay_hint_cache.rs index 1610c4826e7d984cffd9d59656ba445f8ec9558b..36b4e6af6699f927b117ca6eeb8588bb21f30d67 100644 --- a/crates/editor2/src/inlay_hint_cache.rs +++ b/crates/editor2/src/inlay_hint_cache.rs @@ -2401,346 +2401,346 @@ pub mod tests { }); } - #[gpui::test(iterations = 10)] - async fn test_multiple_excerpts_large_multibuffer(cx: &mut gpui::TestAppContext) { - init_test(cx, |settings| { - settings.defaults.inlay_hints = Some(InlayHintSettings { - enabled: true, - show_type_hints: true, - show_parameter_hints: true, - show_other_hints: true, - }) - }); - - let mut language = Language::new( - LanguageConfig { - name: "Rust".into(), - path_suffixes: vec!["rs".to_string()], - ..Default::default() - }, - Some(tree_sitter_rust::language()), - ); - let mut fake_servers = language - .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { - capabilities: lsp::ServerCapabilities { - inlay_hint_provider: Some(lsp::OneOf::Left(true)), - ..Default::default() - }, - ..Default::default() - })) - .await; - let language = Arc::new(language); - let fs = FakeFs::new(cx.background_executor.clone()); - fs.insert_tree( - "/a", - json!({ - "main.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|i| format!("let i = {i};\n")).collect::>().join("")), - "other.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|j| format!("let j = {j};\n")).collect::>().join("")), - }), - ) - .await; - let project = Project::test(fs, ["/a".as_ref()], cx).await; - project.update(cx, |project, _| { - project.languages().add(Arc::clone(&language)) - }); - let worktree_id = project.update(cx, |project, cx| { - project.worktrees().next().unwrap().read(cx).id() - }); - - let buffer_1 = project - .update(cx, |project, cx| { - project.open_buffer((worktree_id, "main.rs"), cx) - }) - .await - .unwrap(); - let buffer_2 = project - .update(cx, |project, cx| { - project.open_buffer((worktree_id, "other.rs"), cx) - }) - .await - .unwrap(); - let multibuffer = cx.build_model(|cx| { - let mut multibuffer = MultiBuffer::new(0); - multibuffer.push_excerpts( - buffer_1.clone(), - [ - ExcerptRange { - context: Point::new(0, 0)..Point::new(2, 0), - primary: None, - }, - ExcerptRange { - context: Point::new(4, 0)..Point::new(11, 0), - primary: None, - }, - ExcerptRange { - context: Point::new(22, 0)..Point::new(33, 0), - primary: None, - }, - ExcerptRange { - context: Point::new(44, 0)..Point::new(55, 0), - primary: None, - }, - ExcerptRange { - context: Point::new(56, 0)..Point::new(66, 0), - primary: None, - }, - ExcerptRange { - context: Point::new(67, 0)..Point::new(77, 0), - primary: None, - }, - ], - cx, - ); - multibuffer.push_excerpts( - buffer_2.clone(), - [ - ExcerptRange { - context: Point::new(0, 1)..Point::new(2, 1), - primary: None, - }, - ExcerptRange { - context: Point::new(4, 1)..Point::new(11, 1), - primary: None, - }, - ExcerptRange { - context: Point::new(22, 1)..Point::new(33, 1), - primary: None, - }, - ExcerptRange { - context: Point::new(44, 1)..Point::new(55, 1), - primary: None, - }, - ExcerptRange { - context: Point::new(56, 1)..Point::new(66, 1), - primary: None, - }, - ExcerptRange { - context: Point::new(67, 1)..Point::new(77, 1), - primary: None, - }, - ], - cx, - ); - multibuffer - }); - - cx.executor().run_until_parked(); - let editor = - cx.add_window(|cx| Editor::for_multibuffer(multibuffer, Some(project.clone()), cx)); - let editor_edited = Arc::new(AtomicBool::new(false)); - let fake_server = fake_servers.next().await.unwrap(); - let closure_editor_edited = Arc::clone(&editor_edited); - fake_server - .handle_request::(move |params, _| { - let task_editor_edited = Arc::clone(&closure_editor_edited); - async move { - let hint_text = if params.text_document.uri - == lsp::Url::from_file_path("/a/main.rs").unwrap() - { - "main hint" - } else if params.text_document.uri - == lsp::Url::from_file_path("/a/other.rs").unwrap() - { - "other hint" - } else { - panic!("unexpected uri: {:?}", params.text_document.uri); - }; - - // one hint per excerpt - let positions = [ - lsp::Position::new(0, 2), - lsp::Position::new(4, 2), - lsp::Position::new(22, 2), - lsp::Position::new(44, 2), - lsp::Position::new(56, 2), - lsp::Position::new(67, 2), - ]; - let out_of_range_hint = lsp::InlayHint { - position: lsp::Position::new( - params.range.start.line + 99, - params.range.start.character + 99, - ), - label: lsp::InlayHintLabel::String( - "out of excerpt range, should be ignored".to_string(), - ), - kind: None, - text_edits: None, - tooltip: None, - padding_left: None, - padding_right: None, - data: None, - }; - - let edited = task_editor_edited.load(Ordering::Acquire); - Ok(Some( - std::iter::once(out_of_range_hint) - .chain(positions.into_iter().enumerate().map(|(i, position)| { - lsp::InlayHint { - position, - label: lsp::InlayHintLabel::String(format!( - "{hint_text}{} #{i}", - if edited { "(edited)" } else { "" }, - )), - kind: None, - text_edits: None, - tooltip: None, - padding_left: None, - padding_right: None, - data: None, - } - })) - .collect(), - )) - } - }) - .next() - .await; - cx.executor().run_until_parked(); - - editor.update(cx, |editor, cx| { - let expected_hints = vec![ - "main hint #0".to_string(), - "main hint #1".to_string(), - "main hint #2".to_string(), - "main hint #3".to_string(), - // todo!() there used to be no these hints, but new gpui2 presumably scrolls a bit farther - // (or renders less?) note that tests below pass - "main hint #4".to_string(), - "main hint #5".to_string(), - ]; - assert_eq!( - expected_hints, - cached_hint_labels(editor), - "When scroll is at the edge of a multibuffer, its visible excerpts only should be queried for inlay hints" - ); - assert_eq!(expected_hints, visible_hint_labels(editor, cx)); - assert_eq!(editor.inlay_hint_cache().version, expected_hints.len(), "Every visible excerpt hints should bump the verison"); - }); - - editor.update(cx, |editor, cx| { - editor.change_selections(Some(Autoscroll::Next), cx, |s| { - s.select_ranges([Point::new(4, 0)..Point::new(4, 0)]) - }); - editor.change_selections(Some(Autoscroll::Next), cx, |s| { - s.select_ranges([Point::new(22, 0)..Point::new(22, 0)]) - }); - editor.change_selections(Some(Autoscroll::Next), cx, |s| { - s.select_ranges([Point::new(50, 0)..Point::new(50, 0)]) - }); - }); - cx.executor().run_until_parked(); - editor.update(cx, |editor, cx| { - let expected_hints = vec![ - "main hint #0".to_string(), - "main hint #1".to_string(), - "main hint #2".to_string(), - "main hint #3".to_string(), - "main hint #4".to_string(), - "main hint #5".to_string(), - "other hint #0".to_string(), - "other hint #1".to_string(), - "other hint #2".to_string(), - ]; - assert_eq!(expected_hints, cached_hint_labels(editor), - "With more scrolls of the multibuffer, more hints should be added into the cache and nothing invalidated without edits"); - assert_eq!(expected_hints, visible_hint_labels(editor, cx)); - assert_eq!(editor.inlay_hint_cache().version, expected_hints.len(), - "Due to every excerpt having one hint, we update cache per new excerpt scrolled"); - }); - - editor.update(cx, |editor, cx| { - editor.change_selections(Some(Autoscroll::Next), cx, |s| { - s.select_ranges([Point::new(100, 0)..Point::new(100, 0)]) - }); - }); - cx.executor().advance_clock(Duration::from_millis( - INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100, - )); - cx.executor().run_until_parked(); - let last_scroll_update_version = editor.update(cx, |editor, cx| { - let expected_hints = vec![ - "main hint #0".to_string(), - "main hint #1".to_string(), - "main hint #2".to_string(), - "main hint #3".to_string(), - "main hint #4".to_string(), - "main hint #5".to_string(), - "other hint #0".to_string(), - "other hint #1".to_string(), - "other hint #2".to_string(), - "other hint #3".to_string(), - "other hint #4".to_string(), - "other hint #5".to_string(), - ]; - assert_eq!(expected_hints, cached_hint_labels(editor), - "After multibuffer was scrolled to the end, all hints for all excerpts should be fetched"); - assert_eq!(expected_hints, visible_hint_labels(editor, cx)); - assert_eq!(editor.inlay_hint_cache().version, expected_hints.len()); - expected_hints.len() - }).unwrap(); - - editor.update(cx, |editor, cx| { - editor.change_selections(Some(Autoscroll::Next), cx, |s| { - s.select_ranges([Point::new(4, 0)..Point::new(4, 0)]) - }); - }); - cx.executor().run_until_parked(); - editor.update(cx, |editor, cx| { - let expected_hints = vec![ - "main hint #0".to_string(), - "main hint #1".to_string(), - "main hint #2".to_string(), - "main hint #3".to_string(), - "main hint #4".to_string(), - "main hint #5".to_string(), - "other hint #0".to_string(), - "other hint #1".to_string(), - "other hint #2".to_string(), - "other hint #3".to_string(), - "other hint #4".to_string(), - "other hint #5".to_string(), - ]; - assert_eq!(expected_hints, cached_hint_labels(editor), - "After multibuffer was scrolled to the end, further scrolls up should not bring more hints"); - assert_eq!(expected_hints, visible_hint_labels(editor, cx)); - assert_eq!(editor.inlay_hint_cache().version, last_scroll_update_version, "No updates should happen during scrolling already scolled buffer"); - }); - - editor_edited.store(true, Ordering::Release); - editor.update(cx, |editor, cx| { - editor.change_selections(None, cx, |s| { - s.select_ranges([Point::new(56, 0)..Point::new(56, 0)]) - }); - editor.handle_input("++++more text++++", cx); - }); - cx.executor().run_until_parked(); - editor.update(cx, |editor, cx| { - let expected_hints = vec![ - "main hint(edited) #0".to_string(), - "main hint(edited) #1".to_string(), - "main hint(edited) #2".to_string(), - "main hint(edited) #3".to_string(), - "main hint(edited) #4".to_string(), - "main hint(edited) #5".to_string(), - "other hint(edited) #0".to_string(), - "other hint(edited) #1".to_string(), - ]; - assert_eq!( - expected_hints, - cached_hint_labels(editor), - "After multibuffer edit, editor gets scolled back to the last selection; \ -all hints should be invalidated and requeried for all of its visible excerpts" - ); - assert_eq!(expected_hints, visible_hint_labels(editor, cx)); - - let current_cache_version = editor.inlay_hint_cache().version; - let minimum_expected_version = last_scroll_update_version + expected_hints.len(); - assert!( - current_cache_version == minimum_expected_version || current_cache_version == minimum_expected_version + 1, - "Due to every excerpt having one hint, cache should update per new excerpt received + 1 potential sporadic update" - ); - }); - } + // #[gpui::test(iterations = 10)] + // async fn test_multiple_excerpts_large_multibuffer(cx: &mut gpui::TestAppContext) { + // init_test(cx, |settings| { + // settings.defaults.inlay_hints = Some(InlayHintSettings { + // enabled: true, + // show_type_hints: true, + // show_parameter_hints: true, + // show_other_hints: true, + // }) + // }); + + // let mut language = Language::new( + // LanguageConfig { + // name: "Rust".into(), + // path_suffixes: vec!["rs".to_string()], + // ..Default::default() + // }, + // Some(tree_sitter_rust::language()), + // ); + // let mut fake_servers = language + // .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { + // capabilities: lsp::ServerCapabilities { + // inlay_hint_provider: Some(lsp::OneOf::Left(true)), + // ..Default::default() + // }, + // ..Default::default() + // })) + // .await; + // let language = Arc::new(language); + // let fs = FakeFs::new(cx.background_executor.clone()); + // fs.insert_tree( + // "/a", + // json!({ + // "main.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|i| format!("let i = {i};\n")).collect::>().join("")), + // "other.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|j| format!("let j = {j};\n")).collect::>().join("")), + // }), + // ) + // .await; + // let project = Project::test(fs, ["/a".as_ref()], cx).await; + // project.update(cx, |project, _| { + // project.languages().add(Arc::clone(&language)) + // }); + // let worktree_id = project.update(cx, |project, cx| { + // project.worktrees().next().unwrap().read(cx).id() + // }); + + // let buffer_1 = project + // .update(cx, |project, cx| { + // project.open_buffer((worktree_id, "main.rs"), cx) + // }) + // .await + // .unwrap(); + // let buffer_2 = project + // .update(cx, |project, cx| { + // project.open_buffer((worktree_id, "other.rs"), cx) + // }) + // .await + // .unwrap(); + // let multibuffer = cx.build_model(|cx| { + // let mut multibuffer = MultiBuffer::new(0); + // multibuffer.push_excerpts( + // buffer_1.clone(), + // [ + // ExcerptRange { + // context: Point::new(0, 0)..Point::new(2, 0), + // primary: None, + // }, + // ExcerptRange { + // context: Point::new(4, 0)..Point::new(11, 0), + // primary: None, + // }, + // ExcerptRange { + // context: Point::new(22, 0)..Point::new(33, 0), + // primary: None, + // }, + // ExcerptRange { + // context: Point::new(44, 0)..Point::new(55, 0), + // primary: None, + // }, + // ExcerptRange { + // context: Point::new(56, 0)..Point::new(66, 0), + // primary: None, + // }, + // ExcerptRange { + // context: Point::new(67, 0)..Point::new(77, 0), + // primary: None, + // }, + // ], + // cx, + // ); + // multibuffer.push_excerpts( + // buffer_2.clone(), + // [ + // ExcerptRange { + // context: Point::new(0, 1)..Point::new(2, 1), + // primary: None, + // }, + // ExcerptRange { + // context: Point::new(4, 1)..Point::new(11, 1), + // primary: None, + // }, + // ExcerptRange { + // context: Point::new(22, 1)..Point::new(33, 1), + // primary: None, + // }, + // ExcerptRange { + // context: Point::new(44, 1)..Point::new(55, 1), + // primary: None, + // }, + // ExcerptRange { + // context: Point::new(56, 1)..Point::new(66, 1), + // primary: None, + // }, + // ExcerptRange { + // context: Point::new(67, 1)..Point::new(77, 1), + // primary: None, + // }, + // ], + // cx, + // ); + // multibuffer + // }); + + // cx.executor().run_until_parked(); + // let editor = + // cx.add_window(|cx| Editor::for_multibuffer(multibuffer, Some(project.clone()), cx)); + // let editor_edited = Arc::new(AtomicBool::new(false)); + // let fake_server = fake_servers.next().await.unwrap(); + // let closure_editor_edited = Arc::clone(&editor_edited); + // fake_server + // .handle_request::(move |params, _| { + // let task_editor_edited = Arc::clone(&closure_editor_edited); + // async move { + // let hint_text = if params.text_document.uri + // == lsp::Url::from_file_path("/a/main.rs").unwrap() + // { + // "main hint" + // } else if params.text_document.uri + // == lsp::Url::from_file_path("/a/other.rs").unwrap() + // { + // "other hint" + // } else { + // panic!("unexpected uri: {:?}", params.text_document.uri); + // }; + + // // one hint per excerpt + // let positions = [ + // lsp::Position::new(0, 2), + // lsp::Position::new(4, 2), + // lsp::Position::new(22, 2), + // lsp::Position::new(44, 2), + // lsp::Position::new(56, 2), + // lsp::Position::new(67, 2), + // ]; + // let out_of_range_hint = lsp::InlayHint { + // position: lsp::Position::new( + // params.range.start.line + 99, + // params.range.start.character + 99, + // ), + // label: lsp::InlayHintLabel::String( + // "out of excerpt range, should be ignored".to_string(), + // ), + // kind: None, + // text_edits: None, + // tooltip: None, + // padding_left: None, + // padding_right: None, + // data: None, + // }; + + // let edited = task_editor_edited.load(Ordering::Acquire); + // Ok(Some( + // std::iter::once(out_of_range_hint) + // .chain(positions.into_iter().enumerate().map(|(i, position)| { + // lsp::InlayHint { + // position, + // label: lsp::InlayHintLabel::String(format!( + // "{hint_text}{} #{i}", + // if edited { "(edited)" } else { "" }, + // )), + // kind: None, + // text_edits: None, + // tooltip: None, + // padding_left: None, + // padding_right: None, + // data: None, + // } + // })) + // .collect(), + // )) + // } + // }) + // .next() + // .await; + // cx.executor().run_until_parked(); + + // editor.update(cx, |editor, cx| { + // let expected_hints = vec![ + // "main hint #0".to_string(), + // "main hint #1".to_string(), + // "main hint #2".to_string(), + // "main hint #3".to_string(), + // // todo!() there used to be no these hints, but new gpui2 presumably scrolls a bit farther + // // (or renders less?) note that tests below pass + // "main hint #4".to_string(), + // "main hint #5".to_string(), + // ]; + // assert_eq!( + // expected_hints, + // cached_hint_labels(editor), + // "When scroll is at the edge of a multibuffer, its visible excerpts only should be queried for inlay hints" + // ); + // assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + // assert_eq!(editor.inlay_hint_cache().version, expected_hints.len(), "Every visible excerpt hints should bump the verison"); + // }); + + // editor.update(cx, |editor, cx| { + // editor.change_selections(Some(Autoscroll::Next), cx, |s| { + // s.select_ranges([Point::new(4, 0)..Point::new(4, 0)]) + // }); + // editor.change_selections(Some(Autoscroll::Next), cx, |s| { + // s.select_ranges([Point::new(22, 0)..Point::new(22, 0)]) + // }); + // editor.change_selections(Some(Autoscroll::Next), cx, |s| { + // s.select_ranges([Point::new(50, 0)..Point::new(50, 0)]) + // }); + // }); + // cx.executor().run_until_parked(); + // editor.update(cx, |editor, cx| { + // let expected_hints = vec![ + // "main hint #0".to_string(), + // "main hint #1".to_string(), + // "main hint #2".to_string(), + // "main hint #3".to_string(), + // "main hint #4".to_string(), + // "main hint #5".to_string(), + // "other hint #0".to_string(), + // "other hint #1".to_string(), + // "other hint #2".to_string(), + // ]; + // assert_eq!(expected_hints, cached_hint_labels(editor), + // "With more scrolls of the multibuffer, more hints should be added into the cache and nothing invalidated without edits"); + // assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + // assert_eq!(editor.inlay_hint_cache().version, expected_hints.len(), + // "Due to every excerpt having one hint, we update cache per new excerpt scrolled"); + // }); + + // editor.update(cx, |editor, cx| { + // editor.change_selections(Some(Autoscroll::Next), cx, |s| { + // s.select_ranges([Point::new(100, 0)..Point::new(100, 0)]) + // }); + // }); + // cx.executor().advance_clock(Duration::from_millis( + // INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100, + // )); + // cx.executor().run_until_parked(); + // let last_scroll_update_version = editor.update(cx, |editor, cx| { + // let expected_hints = vec![ + // "main hint #0".to_string(), + // "main hint #1".to_string(), + // "main hint #2".to_string(), + // "main hint #3".to_string(), + // "main hint #4".to_string(), + // "main hint #5".to_string(), + // "other hint #0".to_string(), + // "other hint #1".to_string(), + // "other hint #2".to_string(), + // "other hint #3".to_string(), + // "other hint #4".to_string(), + // "other hint #5".to_string(), + // ]; + // assert_eq!(expected_hints, cached_hint_labels(editor), + // "After multibuffer was scrolled to the end, all hints for all excerpts should be fetched"); + // assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + // assert_eq!(editor.inlay_hint_cache().version, expected_hints.len()); + // expected_hints.len() + // }).unwrap(); + + // editor.update(cx, |editor, cx| { + // editor.change_selections(Some(Autoscroll::Next), cx, |s| { + // s.select_ranges([Point::new(4, 0)..Point::new(4, 0)]) + // }); + // }); + // cx.executor().run_until_parked(); + // editor.update(cx, |editor, cx| { + // let expected_hints = vec![ + // "main hint #0".to_string(), + // "main hint #1".to_string(), + // "main hint #2".to_string(), + // "main hint #3".to_string(), + // "main hint #4".to_string(), + // "main hint #5".to_string(), + // "other hint #0".to_string(), + // "other hint #1".to_string(), + // "other hint #2".to_string(), + // "other hint #3".to_string(), + // "other hint #4".to_string(), + // "other hint #5".to_string(), + // ]; + // assert_eq!(expected_hints, cached_hint_labels(editor), + // "After multibuffer was scrolled to the end, further scrolls up should not bring more hints"); + // assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + // assert_eq!(editor.inlay_hint_cache().version, last_scroll_update_version, "No updates should happen during scrolling already scolled buffer"); + // }); + + // editor_edited.store(true, Ordering::Release); + // editor.update(cx, |editor, cx| { + // editor.change_selections(None, cx, |s| { + // s.select_ranges([Point::new(56, 0)..Point::new(56, 0)]) + // }); + // editor.handle_input("++++more text++++", cx); + // }); + // cx.executor().run_until_parked(); + // editor.update(cx, |editor, cx| { + // let expected_hints = vec![ + // "main hint(edited) #0".to_string(), + // "main hint(edited) #1".to_string(), + // "main hint(edited) #2".to_string(), + // "main hint(edited) #3".to_string(), + // "main hint(edited) #4".to_string(), + // "main hint(edited) #5".to_string(), + // "other hint(edited) #0".to_string(), + // "other hint(edited) #1".to_string(), + // ]; + // assert_eq!( + // expected_hints, + // cached_hint_labels(editor), + // "After multibuffer edit, editor gets scolled back to the last selection; \ + // all hints should be invalidated and requeried for all of its visible excerpts" + // ); + // assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + + // let current_cache_version = editor.inlay_hint_cache().version; + // let minimum_expected_version = last_scroll_update_version + expected_hints.len(); + // assert!( + // current_cache_version == minimum_expected_version || current_cache_version == minimum_expected_version + 1, + // "Due to every excerpt having one hint, cache should update per new excerpt received + 1 potential sporadic update" + // ); + // }); + // } #[gpui::test] async fn test_excerpts_removed(cx: &mut gpui::TestAppContext) { diff --git a/crates/editor2/src/movement.rs b/crates/editor2/src/movement.rs index 1414ae702dc4f772e3fcd895345eb849ccbc9217..ab25bb8499aa323178d3573ad563797f1ec0a712 100644 --- a/crates/editor2/src/movement.rs +++ b/crates/editor2/src/movement.rs @@ -452,483 +452,475 @@ pub fn split_display_range_by_lines( result } -// #[cfg(test)] -// mod tests { -// use super::*; -// use crate::{ -// display_map::Inlay, -// test::{}, -// Buffer, DisplayMap, ExcerptRange, InlayId, MultiBuffer, -// }; -// use project::Project; -// use settings::SettingsStore; -// use util::post_inc; - -// #[gpui::test] -// fn test_previous_word_start(cx: &mut gpui::AppContext) { -// init_test(cx); - -// fn assert(marked_text: &str, cx: &mut gpui::AppContext) { -// let (snapshot, display_points) = marked_display_snapshot(marked_text, cx); -// assert_eq!( -// previous_word_start(&snapshot, display_points[1]), -// display_points[0] -// ); -// } - -// assert("\nˇ ˇlorem", cx); -// assert("ˇ\nˇ lorem", cx); -// assert(" ˇloremˇ", cx); -// assert("ˇ ˇlorem", cx); -// assert(" ˇlorˇem", cx); -// assert("\nlorem\nˇ ˇipsum", cx); -// assert("\n\nˇ\nˇ", cx); -// assert(" ˇlorem ˇipsum", cx); -// assert("loremˇ-ˇipsum", cx); -// assert("loremˇ-#$@ˇipsum", cx); -// assert("ˇlorem_ˇipsum", cx); -// assert(" ˇdefγˇ", cx); -// assert(" ˇbcΔˇ", cx); -// assert(" abˇ——ˇcd", cx); -// } - -// #[gpui::test] -// fn test_previous_subword_start(cx: &mut gpui::AppContext) { -// init_test(cx); - -// fn assert(marked_text: &str, cx: &mut gpui::AppContext) { -// let (snapshot, display_points) = marked_display_snapshot(marked_text, cx); -// assert_eq!( -// previous_subword_start(&snapshot, display_points[1]), -// display_points[0] -// ); -// } - -// // Subword boundaries are respected -// assert("lorem_ˇipˇsum", cx); -// assert("lorem_ˇipsumˇ", cx); -// assert("ˇlorem_ˇipsum", cx); -// assert("lorem_ˇipsum_ˇdolor", cx); -// assert("loremˇIpˇsum", cx); -// assert("loremˇIpsumˇ", cx); - -// // Word boundaries are still respected -// assert("\nˇ ˇlorem", cx); -// assert(" ˇloremˇ", cx); -// assert(" ˇlorˇem", cx); -// assert("\nlorem\nˇ ˇipsum", cx); -// assert("\n\nˇ\nˇ", cx); -// assert(" ˇlorem ˇipsum", cx); -// assert("loremˇ-ˇipsum", cx); -// assert("loremˇ-#$@ˇipsum", cx); -// assert(" ˇdefγˇ", cx); -// assert(" bcˇΔˇ", cx); -// assert(" ˇbcδˇ", cx); -// assert(" abˇ——ˇcd", cx); -// } - -// #[gpui::test] -// fn test_find_preceding_boundary(cx: &mut gpui::AppContext) { -// init_test(cx); - -// fn assert( -// marked_text: &str, -// cx: &mut gpui::AppContext, -// is_boundary: impl FnMut(char, char) -> bool, -// ) { -// let (snapshot, display_points) = marked_display_snapshot(marked_text, cx); -// assert_eq!( -// find_preceding_boundary( -// &snapshot, -// display_points[1], -// FindRange::MultiLine, -// is_boundary -// ), -// display_points[0] -// ); -// } - -// assert("abcˇdef\ngh\nijˇk", cx, |left, right| { -// left == 'c' && right == 'd' -// }); -// assert("abcdef\nˇgh\nijˇk", cx, |left, right| { -// left == '\n' && right == 'g' -// }); -// let mut line_count = 0; -// assert("abcdef\nˇgh\nijˇk", cx, |left, _| { -// if left == '\n' { -// line_count += 1; -// line_count == 2 -// } else { -// false -// } -// }); -// } - -// #[gpui::test] -// fn test_find_preceding_boundary_with_inlays(cx: &mut gpui::AppContext) { -// init_test(cx); - -// let input_text = "abcdefghijklmnopqrstuvwxys"; -// 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 = MultiBuffer::build_simple(input_text, cx); -// let buffer_snapshot = buffer.read(cx).snapshot(cx); -// let display_map = -// cx.add_model(|cx| DisplayMap::new(buffer, font_id, font_size, None, 1, 1, cx)); - -// // add all kinds of inlays between two word boundaries: we should be able to cross them all, when looking for another boundary -// let mut id = 0; -// let inlays = (0..buffer_snapshot.len()) -// .map(|offset| { -// [ -// Inlay { -// id: InlayId::Suggestion(post_inc(&mut id)), -// position: buffer_snapshot.anchor_at(offset, Bias::Left), -// text: format!("test").into(), -// }, -// Inlay { -// id: InlayId::Suggestion(post_inc(&mut id)), -// position: buffer_snapshot.anchor_at(offset, Bias::Right), -// text: format!("test").into(), -// }, -// Inlay { -// id: InlayId::Hint(post_inc(&mut id)), -// position: buffer_snapshot.anchor_at(offset, Bias::Left), -// text: format!("test").into(), -// }, -// Inlay { -// id: InlayId::Hint(post_inc(&mut id)), -// position: buffer_snapshot.anchor_at(offset, Bias::Right), -// text: format!("test").into(), -// }, -// ] -// }) -// .flatten() -// .collect(); -// let snapshot = display_map.update(cx, |map, cx| { -// map.splice_inlays(Vec::new(), inlays, cx); -// map.snapshot(cx) -// }); - -// assert_eq!( -// find_preceding_boundary( -// &snapshot, -// buffer_snapshot.len().to_display_point(&snapshot), -// FindRange::MultiLine, -// |left, _| left == 'e', -// ), -// snapshot -// .buffer_snapshot -// .offset_to_point(5) -// .to_display_point(&snapshot), -// "Should not stop at inlays when looking for boundaries" -// ); -// } - -// #[gpui::test] -// fn test_next_word_end(cx: &mut gpui::AppContext) { -// init_test(cx); - -// fn assert(marked_text: &str, cx: &mut gpui::AppContext) { -// let (snapshot, display_points) = marked_display_snapshot(marked_text, cx); -// assert_eq!( -// next_word_end(&snapshot, display_points[0]), -// display_points[1] -// ); -// } - -// assert("\nˇ loremˇ", cx); -// assert(" ˇloremˇ", cx); -// assert(" lorˇemˇ", cx); -// assert(" loremˇ ˇ\nipsum\n", cx); -// assert("\nˇ\nˇ\n\n", cx); -// assert("loremˇ ipsumˇ ", cx); -// assert("loremˇ-ˇipsum", cx); -// assert("loremˇ#$@-ˇipsum", cx); -// assert("loremˇ_ipsumˇ", cx); -// assert(" ˇbcΔˇ", cx); -// assert(" abˇ——ˇcd", cx); -// } - -// #[gpui::test] -// fn test_next_subword_end(cx: &mut gpui::AppContext) { -// init_test(cx); - -// fn assert(marked_text: &str, cx: &mut gpui::AppContext) { -// let (snapshot, display_points) = marked_display_snapshot(marked_text, cx); -// assert_eq!( -// next_subword_end(&snapshot, display_points[0]), -// display_points[1] -// ); -// } - -// // Subword boundaries are respected -// assert("loˇremˇ_ipsum", cx); -// assert("ˇloremˇ_ipsum", cx); -// assert("loremˇ_ipsumˇ", cx); -// assert("loremˇ_ipsumˇ_dolor", cx); -// assert("loˇremˇIpsum", cx); -// assert("loremˇIpsumˇDolor", cx); - -// // Word boundaries are still respected -// assert("\nˇ loremˇ", cx); -// assert(" ˇloremˇ", cx); -// assert(" lorˇemˇ", cx); -// assert(" loremˇ ˇ\nipsum\n", cx); -// assert("\nˇ\nˇ\n\n", cx); -// assert("loremˇ ipsumˇ ", cx); -// assert("loremˇ-ˇipsum", cx); -// assert("loremˇ#$@-ˇipsum", cx); -// assert("loremˇ_ipsumˇ", cx); -// assert(" ˇbcˇΔ", cx); -// assert(" abˇ——ˇcd", cx); -// } - -// #[gpui::test] -// fn test_find_boundary(cx: &mut gpui::AppContext) { -// init_test(cx); - -// fn assert( -// marked_text: &str, -// cx: &mut gpui::AppContext, -// is_boundary: impl FnMut(char, char) -> bool, -// ) { -// let (snapshot, display_points) = marked_display_snapshot(marked_text, cx); -// assert_eq!( -// find_boundary( -// &snapshot, -// display_points[0], -// FindRange::MultiLine, -// is_boundary -// ), -// display_points[1] -// ); -// } - -// assert("abcˇdef\ngh\nijˇk", cx, |left, right| { -// left == 'j' && right == 'k' -// }); -// assert("abˇcdef\ngh\nˇijk", cx, |left, right| { -// left == '\n' && right == 'i' -// }); -// let mut line_count = 0; -// assert("abcˇdef\ngh\nˇijk", cx, |left, _| { -// if left == '\n' { -// line_count += 1; -// line_count == 2 -// } else { -// false -// } -// }); -// } - -// #[gpui::test] -// fn test_surrounding_word(cx: &mut gpui::AppContext) { -// init_test(cx); - -// fn assert(marked_text: &str, cx: &mut gpui::AppContext) { -// let (snapshot, display_points) = marked_display_snapshot(marked_text, cx); -// assert_eq!( -// surrounding_word(&snapshot, display_points[1]), -// display_points[0]..display_points[2], -// "{}", -// marked_text.to_string() -// ); -// } - -// assert("ˇˇloremˇ ipsum", cx); -// assert("ˇloˇremˇ ipsum", cx); -// assert("ˇloremˇˇ ipsum", cx); -// assert("loremˇ ˇ ˇipsum", cx); -// assert("lorem\nˇˇˇ\nipsum", cx); -// assert("lorem\nˇˇipsumˇ", cx); -// assert("loremˇ,ˇˇ ipsum", cx); -// assert("ˇloremˇˇ, ipsum", cx); -// } - -// #[gpui::test] -// async fn test_move_up_and_down_with_excerpts(cx: &mut gpui::TestAppContext) { -// cx.update(|cx| { -// init_test(cx); -// }); - -// let mut cx = EditorTestContext::new(cx).await; -// let editor = cx.editor.clone(); -// let window = cx.window.clone(); -// cx.update_window(window, |cx| { -// let text_layout_details = -// editor.read_with(cx, |editor, cx| editor.text_layout_details(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 buffer = -// cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "abc\ndefg\nhijkl\nmn")); -// let multibuffer = cx.add_model(|cx| { -// let mut multibuffer = MultiBuffer::new(0); -// multibuffer.push_excerpts( -// buffer.clone(), -// [ -// ExcerptRange { -// context: Point::new(0, 0)..Point::new(1, 4), -// primary: None, -// }, -// ExcerptRange { -// context: Point::new(2, 0)..Point::new(3, 2), -// primary: None, -// }, -// ], -// cx, -// ); -// multibuffer -// }); -// let display_map = -// cx.add_model(|cx| DisplayMap::new(multibuffer, font_id, 14.0, None, 2, 2, cx)); -// let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx)); - -// assert_eq!(snapshot.text(), "\n\nabc\ndefg\n\n\nhijkl\nmn"); - -// let col_2_x = snapshot.x_for_point(DisplayPoint::new(2, 2), &text_layout_details); - -// // Can't move up into the first excerpt's header -// assert_eq!( -// up( -// &snapshot, -// DisplayPoint::new(2, 2), -// SelectionGoal::HorizontalPosition(col_2_x), -// false, -// &text_layout_details -// ), -// ( -// DisplayPoint::new(2, 0), -// SelectionGoal::HorizontalPosition(0.0) -// ), -// ); -// assert_eq!( -// up( -// &snapshot, -// DisplayPoint::new(2, 0), -// SelectionGoal::None, -// false, -// &text_layout_details -// ), -// ( -// DisplayPoint::new(2, 0), -// SelectionGoal::HorizontalPosition(0.0) -// ), -// ); - -// let col_4_x = snapshot.x_for_point(DisplayPoint::new(3, 4), &text_layout_details); - -// // Move up and down within first excerpt -// assert_eq!( -// up( -// &snapshot, -// DisplayPoint::new(3, 4), -// SelectionGoal::HorizontalPosition(col_4_x), -// false, -// &text_layout_details -// ), -// ( -// DisplayPoint::new(2, 3), -// SelectionGoal::HorizontalPosition(col_4_x) -// ), -// ); -// assert_eq!( -// down( -// &snapshot, -// DisplayPoint::new(2, 3), -// SelectionGoal::HorizontalPosition(col_4_x), -// false, -// &text_layout_details -// ), -// ( -// DisplayPoint::new(3, 4), -// SelectionGoal::HorizontalPosition(col_4_x) -// ), -// ); - -// let col_5_x = snapshot.x_for_point(DisplayPoint::new(6, 5), &text_layout_details); - -// // Move up and down across second excerpt's header -// assert_eq!( -// up( -// &snapshot, -// DisplayPoint::new(6, 5), -// SelectionGoal::HorizontalPosition(col_5_x), -// false, -// &text_layout_details -// ), -// ( -// DisplayPoint::new(3, 4), -// SelectionGoal::HorizontalPosition(col_5_x) -// ), -// ); -// assert_eq!( -// down( -// &snapshot, -// DisplayPoint::new(3, 4), -// SelectionGoal::HorizontalPosition(col_5_x), -// false, -// &text_layout_details -// ), -// ( -// DisplayPoint::new(6, 5), -// SelectionGoal::HorizontalPosition(col_5_x) -// ), -// ); - -// let max_point_x = snapshot.x_for_point(DisplayPoint::new(7, 2), &text_layout_details); - -// // Can't move down off the end -// assert_eq!( -// down( -// &snapshot, -// DisplayPoint::new(7, 0), -// SelectionGoal::HorizontalPosition(0.0), -// false, -// &text_layout_details -// ), -// ( -// DisplayPoint::new(7, 2), -// SelectionGoal::HorizontalPosition(max_point_x) -// ), -// ); -// assert_eq!( -// down( -// &snapshot, -// DisplayPoint::new(7, 2), -// SelectionGoal::HorizontalPosition(max_point_x), -// false, -// &text_layout_details -// ), -// ( -// DisplayPoint::new(7, 2), -// SelectionGoal::HorizontalPosition(max_point_x) -// ), -// ); -// }); -// } - -// fn init_test(cx: &mut gpui::AppContext) { -// cx.set_global(SettingsStore::test(cx)); -// theme::init(cx); -// language::init(cx); -// crate::init(cx); -// Project::init_settings(cx); -// } -// } +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + display_map::Inlay, + test::{editor_test_context::EditorTestContext, marked_display_snapshot}, + Buffer, DisplayMap, ExcerptRange, InlayId, MultiBuffer, + }; + use gpui::{font, Context as _}; + use project::Project; + use settings::SettingsStore; + use util::post_inc; + + #[gpui::test] + fn test_previous_word_start(cx: &mut gpui::AppContext) { + init_test(cx); + + fn assert(marked_text: &str, cx: &mut gpui::AppContext) { + let (snapshot, display_points) = marked_display_snapshot(marked_text, cx); + assert_eq!( + previous_word_start(&snapshot, display_points[1]), + display_points[0] + ); + } + + assert("\nˇ ˇlorem", cx); + assert("ˇ\nˇ lorem", cx); + assert(" ˇloremˇ", cx); + assert("ˇ ˇlorem", cx); + assert(" ˇlorˇem", cx); + assert("\nlorem\nˇ ˇipsum", cx); + assert("\n\nˇ\nˇ", cx); + assert(" ˇlorem ˇipsum", cx); + assert("loremˇ-ˇipsum", cx); + assert("loremˇ-#$@ˇipsum", cx); + assert("ˇlorem_ˇipsum", cx); + assert(" ˇdefγˇ", cx); + assert(" ˇbcΔˇ", cx); + assert(" abˇ——ˇcd", cx); + } + + #[gpui::test] + fn test_previous_subword_start(cx: &mut gpui::AppContext) { + init_test(cx); + + fn assert(marked_text: &str, cx: &mut gpui::AppContext) { + let (snapshot, display_points) = marked_display_snapshot(marked_text, cx); + assert_eq!( + previous_subword_start(&snapshot, display_points[1]), + display_points[0] + ); + } + + // Subword boundaries are respected + assert("lorem_ˇipˇsum", cx); + assert("lorem_ˇipsumˇ", cx); + assert("ˇlorem_ˇipsum", cx); + assert("lorem_ˇipsum_ˇdolor", cx); + assert("loremˇIpˇsum", cx); + assert("loremˇIpsumˇ", cx); + + // Word boundaries are still respected + assert("\nˇ ˇlorem", cx); + assert(" ˇloremˇ", cx); + assert(" ˇlorˇem", cx); + assert("\nlorem\nˇ ˇipsum", cx); + assert("\n\nˇ\nˇ", cx); + assert(" ˇlorem ˇipsum", cx); + assert("loremˇ-ˇipsum", cx); + assert("loremˇ-#$@ˇipsum", cx); + assert(" ˇdefγˇ", cx); + assert(" bcˇΔˇ", cx); + assert(" ˇbcδˇ", cx); + assert(" abˇ——ˇcd", cx); + } + + #[gpui::test] + fn test_find_preceding_boundary(cx: &mut gpui::AppContext) { + init_test(cx); + + fn assert( + marked_text: &str, + cx: &mut gpui::AppContext, + is_boundary: impl FnMut(char, char) -> bool, + ) { + let (snapshot, display_points) = marked_display_snapshot(marked_text, cx); + assert_eq!( + find_preceding_boundary( + &snapshot, + display_points[1], + FindRange::MultiLine, + is_boundary + ), + display_points[0] + ); + } + + assert("abcˇdef\ngh\nijˇk", cx, |left, right| { + left == 'c' && right == 'd' + }); + assert("abcdef\nˇgh\nijˇk", cx, |left, right| { + left == '\n' && right == 'g' + }); + let mut line_count = 0; + assert("abcdef\nˇgh\nijˇk", cx, |left, _| { + if left == '\n' { + line_count += 1; + line_count == 2 + } else { + false + } + }); + } + + #[gpui::test] + fn test_find_preceding_boundary_with_inlays(cx: &mut gpui::AppContext) { + init_test(cx); + + let input_text = "abcdefghijklmnopqrstuvwxys"; + let font = font("Helvetica"); + let font_size = px(14.0); + let buffer = MultiBuffer::build_simple(input_text, cx); + let buffer_snapshot = buffer.read(cx).snapshot(cx); + let display_map = + cx.build_model(|cx| DisplayMap::new(buffer, font, font_size, None, 1, 1, cx)); + + // add all kinds of inlays between two word boundaries: we should be able to cross them all, when looking for another boundary + let mut id = 0; + let inlays = (0..buffer_snapshot.len()) + .map(|offset| { + [ + Inlay { + id: InlayId::Suggestion(post_inc(&mut id)), + position: buffer_snapshot.anchor_at(offset, Bias::Left), + text: format!("test").into(), + }, + Inlay { + id: InlayId::Suggestion(post_inc(&mut id)), + position: buffer_snapshot.anchor_at(offset, Bias::Right), + text: format!("test").into(), + }, + Inlay { + id: InlayId::Hint(post_inc(&mut id)), + position: buffer_snapshot.anchor_at(offset, Bias::Left), + text: format!("test").into(), + }, + Inlay { + id: InlayId::Hint(post_inc(&mut id)), + position: buffer_snapshot.anchor_at(offset, Bias::Right), + text: format!("test").into(), + }, + ] + }) + .flatten() + .collect(); + let snapshot = display_map.update(cx, |map, cx| { + map.splice_inlays(Vec::new(), inlays, cx); + map.snapshot(cx) + }); + + assert_eq!( + find_preceding_boundary( + &snapshot, + buffer_snapshot.len().to_display_point(&snapshot), + FindRange::MultiLine, + |left, _| left == 'e', + ), + snapshot + .buffer_snapshot + .offset_to_point(5) + .to_display_point(&snapshot), + "Should not stop at inlays when looking for boundaries" + ); + } + + #[gpui::test] + fn test_next_word_end(cx: &mut gpui::AppContext) { + init_test(cx); + + fn assert(marked_text: &str, cx: &mut gpui::AppContext) { + let (snapshot, display_points) = marked_display_snapshot(marked_text, cx); + assert_eq!( + next_word_end(&snapshot, display_points[0]), + display_points[1] + ); + } + + assert("\nˇ loremˇ", cx); + assert(" ˇloremˇ", cx); + assert(" lorˇemˇ", cx); + assert(" loremˇ ˇ\nipsum\n", cx); + assert("\nˇ\nˇ\n\n", cx); + assert("loremˇ ipsumˇ ", cx); + assert("loremˇ-ˇipsum", cx); + assert("loremˇ#$@-ˇipsum", cx); + assert("loremˇ_ipsumˇ", cx); + assert(" ˇbcΔˇ", cx); + assert(" abˇ——ˇcd", cx); + } + + #[gpui::test] + fn test_next_subword_end(cx: &mut gpui::AppContext) { + init_test(cx); + + fn assert(marked_text: &str, cx: &mut gpui::AppContext) { + let (snapshot, display_points) = marked_display_snapshot(marked_text, cx); + assert_eq!( + next_subword_end(&snapshot, display_points[0]), + display_points[1] + ); + } + + // Subword boundaries are respected + assert("loˇremˇ_ipsum", cx); + assert("ˇloremˇ_ipsum", cx); + assert("loremˇ_ipsumˇ", cx); + assert("loremˇ_ipsumˇ_dolor", cx); + assert("loˇremˇIpsum", cx); + assert("loremˇIpsumˇDolor", cx); + + // Word boundaries are still respected + assert("\nˇ loremˇ", cx); + assert(" ˇloremˇ", cx); + assert(" lorˇemˇ", cx); + assert(" loremˇ ˇ\nipsum\n", cx); + assert("\nˇ\nˇ\n\n", cx); + assert("loremˇ ipsumˇ ", cx); + assert("loremˇ-ˇipsum", cx); + assert("loremˇ#$@-ˇipsum", cx); + assert("loremˇ_ipsumˇ", cx); + assert(" ˇbcˇΔ", cx); + assert(" abˇ——ˇcd", cx); + } + + #[gpui::test] + fn test_find_boundary(cx: &mut gpui::AppContext) { + init_test(cx); + + fn assert( + marked_text: &str, + cx: &mut gpui::AppContext, + is_boundary: impl FnMut(char, char) -> bool, + ) { + let (snapshot, display_points) = marked_display_snapshot(marked_text, cx); + assert_eq!( + find_boundary( + &snapshot, + display_points[0], + FindRange::MultiLine, + is_boundary + ), + display_points[1] + ); + } + + assert("abcˇdef\ngh\nijˇk", cx, |left, right| { + left == 'j' && right == 'k' + }); + assert("abˇcdef\ngh\nˇijk", cx, |left, right| { + left == '\n' && right == 'i' + }); + let mut line_count = 0; + assert("abcˇdef\ngh\nˇijk", cx, |left, _| { + if left == '\n' { + line_count += 1; + line_count == 2 + } else { + false + } + }); + } + + #[gpui::test] + fn test_surrounding_word(cx: &mut gpui::AppContext) { + init_test(cx); + + fn assert(marked_text: &str, cx: &mut gpui::AppContext) { + let (snapshot, display_points) = marked_display_snapshot(marked_text, cx); + assert_eq!( + surrounding_word(&snapshot, display_points[1]), + display_points[0]..display_points[2], + "{}", + marked_text.to_string() + ); + } + + assert("ˇˇloremˇ ipsum", cx); + assert("ˇloˇremˇ ipsum", cx); + assert("ˇloremˇˇ ipsum", cx); + assert("loremˇ ˇ ˇipsum", cx); + assert("lorem\nˇˇˇ\nipsum", cx); + assert("lorem\nˇˇipsumˇ", cx); + assert("loremˇ,ˇˇ ipsum", cx); + assert("ˇloremˇˇ, ipsum", cx); + } + + #[gpui::test] + async fn test_move_up_and_down_with_excerpts(cx: &mut gpui::TestAppContext) { + cx.update(|cx| { + init_test(cx); + }); + + let mut cx = EditorTestContext::new(cx).await; + let editor = cx.editor.clone(); + let window = cx.window.clone(); + cx.update_window(window, |_, cx| { + let text_layout_details = + editor.update(cx, |editor, cx| editor.text_layout_details(cx)); + + let font = font("Helvetica"); + + let buffer = cx + .build_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), "abc\ndefg\nhijkl\nmn")); + let multibuffer = cx.build_model(|cx| { + let mut multibuffer = MultiBuffer::new(0); + multibuffer.push_excerpts( + buffer.clone(), + [ + ExcerptRange { + context: Point::new(0, 0)..Point::new(1, 4), + primary: None, + }, + ExcerptRange { + context: Point::new(2, 0)..Point::new(3, 2), + primary: None, + }, + ], + cx, + ); + multibuffer + }); + let display_map = + cx.build_model(|cx| DisplayMap::new(multibuffer, font, px(14.0), None, 2, 2, cx)); + let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx)); + + assert_eq!(snapshot.text(), "\n\nabc\ndefg\n\n\nhijkl\nmn"); + + let col_2_x = + snapshot.x_for_display_point(DisplayPoint::new(2, 2), &text_layout_details); + + // Can't move up into the first excerpt's header + assert_eq!( + up( + &snapshot, + DisplayPoint::new(2, 2), + SelectionGoal::HorizontalPosition(col_2_x.0), + false, + &text_layout_details + ), + ( + DisplayPoint::new(2, 0), + SelectionGoal::HorizontalPosition(0.0) + ), + ); + assert_eq!( + up( + &snapshot, + DisplayPoint::new(2, 0), + SelectionGoal::None, + false, + &text_layout_details + ), + ( + DisplayPoint::new(2, 0), + SelectionGoal::HorizontalPosition(0.0) + ), + ); + + let col_4_x = + snapshot.x_for_display_point(DisplayPoint::new(3, 4), &text_layout_details); + + // Move up and down within first excerpt + assert_eq!( + up( + &snapshot, + DisplayPoint::new(3, 4), + SelectionGoal::HorizontalPosition(col_4_x.0), + false, + &text_layout_details + ), + ( + DisplayPoint::new(2, 3), + SelectionGoal::HorizontalPosition(col_4_x.0) + ), + ); + assert_eq!( + down( + &snapshot, + DisplayPoint::new(2, 3), + SelectionGoal::HorizontalPosition(col_4_x.0), + false, + &text_layout_details + ), + ( + DisplayPoint::new(3, 4), + SelectionGoal::HorizontalPosition(col_4_x.0) + ), + ); + + let col_5_x = + snapshot.x_for_display_point(DisplayPoint::new(6, 5), &text_layout_details); + + // Move up and down across second excerpt's header + assert_eq!( + up( + &snapshot, + DisplayPoint::new(6, 5), + SelectionGoal::HorizontalPosition(col_5_x.0), + false, + &text_layout_details + ), + ( + DisplayPoint::new(3, 4), + SelectionGoal::HorizontalPosition(col_5_x.0) + ), + ); + assert_eq!( + down( + &snapshot, + DisplayPoint::new(3, 4), + SelectionGoal::HorizontalPosition(col_5_x.0), + false, + &text_layout_details + ), + ( + DisplayPoint::new(6, 5), + SelectionGoal::HorizontalPosition(col_5_x.0) + ), + ); + + let max_point_x = + snapshot.x_for_display_point(DisplayPoint::new(7, 2), &text_layout_details); + + // Can't move down off the end + assert_eq!( + down( + &snapshot, + DisplayPoint::new(7, 0), + SelectionGoal::HorizontalPosition(0.0), + false, + &text_layout_details + ), + ( + DisplayPoint::new(7, 2), + SelectionGoal::HorizontalPosition(max_point_x.0) + ), + ); + assert_eq!( + down( + &snapshot, + DisplayPoint::new(7, 2), + SelectionGoal::HorizontalPosition(max_point_x.0), + false, + &text_layout_details + ), + ( + DisplayPoint::new(7, 2), + SelectionGoal::HorizontalPosition(max_point_x.0) + ), + ); + }); + } + + fn init_test(cx: &mut gpui::AppContext) { + let settings_store = SettingsStore::test(cx); + cx.set_global(settings_store); + theme::init(theme::LoadThemes::JustBase, cx); + language::init(cx); + crate::init(cx); + Project::init_settings(cx); + } +} From 1c52b936bcba445ba64a5389b3e46b5cc3f51c3f Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Fri, 1 Dec 2023 19:21:30 +0100 Subject: [PATCH 04/35] Uncomment flaky tests --- crates/editor2/src/display_map/wrap_map.rs | 665 ++++++++++---------- crates/editor2/src/inlay_hint_cache.rs | 681 +++++++++++---------- crates/gpui2/src/gpui2.rs | 2 +- crates/gpui2/src/platform.rs | 4 +- crates/gpui2/src/scene.rs | 4 +- crates/gpui2/src/test.rs | 31 +- 6 files changed, 707 insertions(+), 680 deletions(-) diff --git a/crates/editor2/src/display_map/wrap_map.rs b/crates/editor2/src/display_map/wrap_map.rs index c2325fa96d711de7bc49558dfcd4ed760a9fb4be..817f7165accea1ad14044f9a22a6b232ef63883b 100644 --- a/crates/editor2/src/display_map/wrap_map.rs +++ b/crates/editor2/src/display_map/wrap_map.rs @@ -1026,337 +1026,334 @@ fn consolidate_wrap_edits(edits: &mut Vec) { } } -// #[cfg(test)] -// mod tests { -// use super::*; -// use crate::{ -// display_map::{fold_map::FoldMap, inlay_map::InlayMap, tab_map::TabMap}, -// MultiBuffer, -// }; -// use gpui::test::observe; -// use rand::prelude::*; -// use settings::SettingsStore; -// use smol::stream::StreamExt; -// use std::{cmp, env, num::NonZeroU32}; -// use text::Rope; - -// #[gpui::test(iterations = 100)] -// async fn test_random_wraps(cx: &mut gpui::TestAppContext, mut rng: StdRng) { -// init_test(cx); - -// cx.background_executor.set_block_on_ticks(0..=50); -// let operations = env::var("OPERATIONS") -// .map(|i| i.parse().expect("invalid `OPERATIONS` variable")) -// .unwrap_or(10); - -// let font_cache = cx.read(|cx| cx.font_cache().clone()); -// let font_system = cx.platform().fonts(); -// let mut wrap_width = if rng.gen_bool(0.1) { -// None -// } else { -// Some(rng.gen_range(0.0..=1000.0)) -// }; -// let tab_size = NonZeroU32::new(rng.gen_range(1..=4)).unwrap(); -// let family_id = font_cache -// .load_family(&["Helvetica"], &Default::default()) -// .unwrap(); -// let font_id = font_cache -// .select_font(family_id, &Default::default()) -// .unwrap(); -// let font_size = 14.0; - -// log::info!("Tab size: {}", tab_size); -// log::info!("Wrap width: {:?}", wrap_width); - -// let buffer = cx.update(|cx| { -// if rng.gen() { -// MultiBuffer::build_random(&mut rng, cx) -// } else { -// let len = rng.gen_range(0..10); -// let text = util::RandomCharIter::new(&mut rng) -// .take(len) -// .collect::(); -// MultiBuffer::build_simple(&text, cx) -// } -// }); -// let mut buffer_snapshot = buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx)); -// log::info!("Buffer text: {:?}", buffer_snapshot.text()); -// let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone()); -// log::info!("InlayMap text: {:?}", inlay_snapshot.text()); -// let (mut fold_map, fold_snapshot) = FoldMap::new(inlay_snapshot.clone()); -// log::info!("FoldMap text: {:?}", fold_snapshot.text()); -// let (mut tab_map, _) = TabMap::new(fold_snapshot.clone(), tab_size); -// let tabs_snapshot = tab_map.set_max_expansion_column(32); -// log::info!("TabMap text: {:?}", tabs_snapshot.text()); - -// let mut line_wrapper = LineWrapper::new(font_id, font_size, font_system); -// let unwrapped_text = tabs_snapshot.text(); -// let expected_text = wrap_text(&unwrapped_text, wrap_width, &mut line_wrapper); - -// let (wrap_map, _) = -// cx.update(|cx| WrapMap::new(tabs_snapshot.clone(), font_id, font_size, wrap_width, cx)); -// let mut notifications = observe(&wrap_map, cx); - -// if wrap_map.read_with(cx, |map, _| map.is_rewrapping()) { -// notifications.next().await.unwrap(); -// } - -// let (initial_snapshot, _) = wrap_map.update(cx, |map, cx| { -// assert!(!map.is_rewrapping()); -// map.sync(tabs_snapshot.clone(), Vec::new(), cx) -// }); - -// let actual_text = initial_snapshot.text(); -// assert_eq!( -// actual_text, expected_text, -// "unwrapped text is: {:?}", -// unwrapped_text -// ); -// log::info!("Wrapped text: {:?}", actual_text); - -// let mut next_inlay_id = 0; -// let mut edits = Vec::new(); -// for _i in 0..operations { -// log::info!("{} ==============================================", _i); - -// let mut buffer_edits = Vec::new(); -// match rng.gen_range(0..=100) { -// 0..=19 => { -// wrap_width = if rng.gen_bool(0.2) { -// None -// } else { -// Some(rng.gen_range(0.0..=1000.0)) -// }; -// log::info!("Setting wrap width to {:?}", wrap_width); -// wrap_map.update(cx, |map, cx| map.set_wrap_width(wrap_width, cx)); -// } -// 20..=39 => { -// for (fold_snapshot, fold_edits) in fold_map.randomly_mutate(&mut rng) { -// let (tabs_snapshot, tab_edits) = -// tab_map.sync(fold_snapshot, fold_edits, tab_size); -// let (mut snapshot, wrap_edits) = -// wrap_map.update(cx, |map, cx| map.sync(tabs_snapshot, tab_edits, cx)); -// snapshot.check_invariants(); -// snapshot.verify_chunks(&mut rng); -// edits.push((snapshot, wrap_edits)); -// } -// } -// 40..=59 => { -// let (inlay_snapshot, inlay_edits) = -// inlay_map.randomly_mutate(&mut next_inlay_id, &mut rng); -// let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits); -// let (tabs_snapshot, tab_edits) = -// tab_map.sync(fold_snapshot, fold_edits, tab_size); -// let (mut snapshot, wrap_edits) = -// wrap_map.update(cx, |map, cx| map.sync(tabs_snapshot, tab_edits, cx)); -// snapshot.check_invariants(); -// snapshot.verify_chunks(&mut rng); -// edits.push((snapshot, wrap_edits)); -// } -// _ => { -// buffer.update(cx, |buffer, cx| { -// let subscription = buffer.subscribe(); -// let edit_count = rng.gen_range(1..=5); -// buffer.randomly_mutate(&mut rng, edit_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); -// log::info!("InlayMap text: {:?}", inlay_snapshot.text()); -// let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits); -// log::info!("FoldMap text: {:?}", fold_snapshot.text()); -// let (tabs_snapshot, tab_edits) = tab_map.sync(fold_snapshot, fold_edits, tab_size); -// log::info!("TabMap text: {:?}", tabs_snapshot.text()); - -// let unwrapped_text = tabs_snapshot.text(); -// let expected_text = wrap_text(&unwrapped_text, wrap_width, &mut line_wrapper); -// let (mut snapshot, wrap_edits) = -// wrap_map.update(cx, |map, cx| map.sync(tabs_snapshot.clone(), tab_edits, cx)); -// snapshot.check_invariants(); -// snapshot.verify_chunks(&mut rng); -// edits.push((snapshot, wrap_edits)); - -// if wrap_map.read_with(cx, |map, _| map.is_rewrapping()) && rng.gen_bool(0.4) { -// log::info!("Waiting for wrapping to finish"); -// while wrap_map.read_with(cx, |map, _| map.is_rewrapping()) { -// notifications.next().await.unwrap(); -// } -// wrap_map.read_with(cx, |map, _| assert!(map.pending_edits.is_empty())); -// } - -// if !wrap_map.read_with(cx, |map, _| map.is_rewrapping()) { -// let (mut wrapped_snapshot, wrap_edits) = -// wrap_map.update(cx, |map, cx| map.sync(tabs_snapshot, Vec::new(), cx)); -// let actual_text = wrapped_snapshot.text(); -// let actual_longest_row = wrapped_snapshot.longest_row(); -// log::info!("Wrapping finished: {:?}", actual_text); -// wrapped_snapshot.check_invariants(); -// wrapped_snapshot.verify_chunks(&mut rng); -// edits.push((wrapped_snapshot.clone(), wrap_edits)); -// assert_eq!( -// actual_text, expected_text, -// "unwrapped text is: {:?}", -// unwrapped_text -// ); - -// let mut summary = TextSummary::default(); -// for (ix, item) in wrapped_snapshot -// .transforms -// .items(&()) -// .into_iter() -// .enumerate() -// { -// summary += &item.summary.output; -// log::info!("{} summary: {:?}", ix, item.summary.output,); -// } - -// if tab_size.get() == 1 -// || !wrapped_snapshot -// .tab_snapshot -// .fold_snapshot -// .text() -// .contains('\t') -// { -// let mut expected_longest_rows = Vec::new(); -// let mut longest_line_len = -1; -// for (row, line) in expected_text.split('\n').enumerate() { -// let line_char_count = line.chars().count() as isize; -// if line_char_count > longest_line_len { -// expected_longest_rows.clear(); -// longest_line_len = line_char_count; -// } -// if line_char_count >= longest_line_len { -// expected_longest_rows.push(row as u32); -// } -// } - -// assert!( -// expected_longest_rows.contains(&actual_longest_row), -// "incorrect longest row {}. expected {:?} with length {}", -// actual_longest_row, -// expected_longest_rows, -// longest_line_len, -// ) -// } -// } -// } - -// let mut initial_text = Rope::from(initial_snapshot.text().as_str()); -// for (snapshot, patch) in edits { -// let snapshot_text = Rope::from(snapshot.text().as_str()); -// for edit in &patch { -// let old_start = initial_text.point_to_offset(Point::new(edit.new.start, 0)); -// let old_end = initial_text.point_to_offset(cmp::min( -// Point::new(edit.new.start + edit.old.len() as u32, 0), -// initial_text.max_point(), -// )); -// let new_start = snapshot_text.point_to_offset(Point::new(edit.new.start, 0)); -// let new_end = snapshot_text.point_to_offset(cmp::min( -// Point::new(edit.new.end, 0), -// snapshot_text.max_point(), -// )); -// let new_text = snapshot_text -// .chunks_in_range(new_start..new_end) -// .collect::(); - -// initial_text.replace(old_start..old_end, &new_text); -// } -// assert_eq!(initial_text.to_string(), snapshot_text.to_string()); -// } - -// if wrap_map.read_with(cx, |map, _| map.is_rewrapping()) { -// log::info!("Waiting for wrapping to finish"); -// while wrap_map.read_with(cx, |map, _| map.is_rewrapping()) { -// notifications.next().await.unwrap(); -// } -// } -// wrap_map.read_with(cx, |map, _| assert!(map.pending_edits.is_empty())); -// } - -// fn init_test(cx: &mut gpui::TestAppContext) { -// cx.foreground_executor().forbid_parking(); -// cx.update(|cx| { -// cx.set_global(SettingsStore::test(cx)); -// theme::init((), cx); -// }); -// } - -// fn wrap_text( -// unwrapped_text: &str, -// wrap_width: Option, -// line_wrapper: &mut LineWrapper, -// ) -> String { -// if let Some(wrap_width) = wrap_width { -// let mut wrapped_text = String::new(); -// for (row, line) in unwrapped_text.split('\n').enumerate() { -// if row > 0 { -// wrapped_text.push('\n') -// } - -// let mut prev_ix = 0; -// for boundary in line_wrapper.wrap_line(line, wrap_width) { -// wrapped_text.push_str(&line[prev_ix..boundary.ix]); -// wrapped_text.push('\n'); -// wrapped_text.push_str(&" ".repeat(boundary.next_indent as usize)); -// prev_ix = boundary.ix; -// } -// wrapped_text.push_str(&line[prev_ix..]); -// } -// wrapped_text -// } else { -// unwrapped_text.to_string() -// } -// } - -// impl WrapSnapshot { -// pub fn text(&self) -> String { -// self.text_chunks(0).collect() -// } - -// pub fn text_chunks(&self, wrap_row: u32) -> impl Iterator { -// self.chunks( -// wrap_row..self.max_point().row() + 1, -// false, -// Highlights::default(), -// ) -// .map(|h| h.text) -// } - -// fn verify_chunks(&mut self, rng: &mut impl Rng) { -// for _ in 0..5 { -// let mut end_row = rng.gen_range(0..=self.max_point().row()); -// let start_row = rng.gen_range(0..=end_row); -// end_row += 1; - -// let mut expected_text = self.text_chunks(start_row).collect::(); -// if expected_text.ends_with('\n') { -// expected_text.push('\n'); -// } -// let mut expected_text = expected_text -// .lines() -// .take((end_row - start_row) as usize) -// .collect::>() -// .join("\n"); -// if end_row <= self.max_point().row() { -// expected_text.push('\n'); -// } - -// let actual_text = self -// .chunks(start_row..end_row, true, Highlights::default()) -// .map(|c| c.text) -// .collect::(); -// assert_eq!( -// expected_text, -// actual_text, -// "chunks != highlighted_chunks for rows {:?}", -// start_row..end_row -// ); -// } -// } -// } -// } +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + display_map::{fold_map::FoldMap, inlay_map::InlayMap, tab_map::TabMap}, + MultiBuffer, + }; + use gpui::{font, px, test::observe, Platform}; + use rand::prelude::*; + use settings::SettingsStore; + use smol::stream::StreamExt; + use std::{cmp, env, num::NonZeroU32}; + use text::Rope; + use theme::LoadThemes; + + #[gpui::test(iterations = 100)] + async fn test_random_wraps(cx: &mut gpui::TestAppContext, mut rng: StdRng) { + // todo!() this test is flaky + init_test(cx); + + cx.background_executor.set_block_on_ticks(0..=50); + let operations = env::var("OPERATIONS") + .map(|i| i.parse().expect("invalid `OPERATIONS` variable")) + .unwrap_or(10); + + let text_system = cx.test_platform.text_system(); + let mut wrap_width = if rng.gen_bool(0.1) { + None + } else { + Some(px(rng.gen_range(0.0..=1000.0))) + }; + let tab_size = NonZeroU32::new(rng.gen_range(1..=4)).unwrap(); + let font = font("Helvetica"); + let font_id = text_system.font_id(&font).unwrap(); + let font_size = px(14.0); + + log::info!("Tab size: {}", tab_size); + log::info!("Wrap width: {:?}", wrap_width); + + let buffer = cx.update(|cx| { + if rng.gen() { + MultiBuffer::build_random(&mut rng, cx) + } else { + let len = rng.gen_range(0..10); + let text = util::RandomCharIter::new(&mut rng) + .take(len) + .collect::(); + MultiBuffer::build_simple(&text, cx) + } + }); + let mut buffer_snapshot = buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx)); + log::info!("Buffer text: {:?}", buffer_snapshot.text()); + let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone()); + log::info!("InlayMap text: {:?}", inlay_snapshot.text()); + let (mut fold_map, fold_snapshot) = FoldMap::new(inlay_snapshot.clone()); + log::info!("FoldMap text: {:?}", fold_snapshot.text()); + let (mut tab_map, _) = TabMap::new(fold_snapshot.clone(), tab_size); + let tabs_snapshot = tab_map.set_max_expansion_column(32); + log::info!("TabMap text: {:?}", tabs_snapshot.text()); + + let mut line_wrapper = LineWrapper::new(font_id, font_size, text_system); + let unwrapped_text = tabs_snapshot.text(); + let expected_text = wrap_text(&unwrapped_text, wrap_width, &mut line_wrapper); + + let (wrap_map, _) = + cx.update(|cx| WrapMap::new(tabs_snapshot.clone(), font, font_size, wrap_width, cx)); + let mut notifications = observe(&wrap_map, cx); + + if wrap_map.read_with(cx, |map, _| map.is_rewrapping()) { + notifications.next().await.unwrap(); + } + + let (initial_snapshot, _) = wrap_map.update(cx, |map, cx| { + assert!(!map.is_rewrapping()); + map.sync(tabs_snapshot.clone(), Vec::new(), cx) + }); + + let actual_text = initial_snapshot.text(); + assert_eq!( + actual_text, expected_text, + "unwrapped text is: {:?}", + unwrapped_text + ); + log::info!("Wrapped text: {:?}", actual_text); + + let mut next_inlay_id = 0; + let mut edits = Vec::new(); + for _i in 0..operations { + log::info!("{} ==============================================", _i); + + let mut buffer_edits = Vec::new(); + match rng.gen_range(0..=100) { + 0..=19 => { + wrap_width = if rng.gen_bool(0.2) { + None + } else { + Some(px(rng.gen_range(0.0..=1000.0))) + }; + log::info!("Setting wrap width to {:?}", wrap_width); + wrap_map.update(cx, |map, cx| map.set_wrap_width(wrap_width, cx)); + } + 20..=39 => { + for (fold_snapshot, fold_edits) in fold_map.randomly_mutate(&mut rng) { + let (tabs_snapshot, tab_edits) = + tab_map.sync(fold_snapshot, fold_edits, tab_size); + let (mut snapshot, wrap_edits) = + wrap_map.update(cx, |map, cx| map.sync(tabs_snapshot, tab_edits, cx)); + snapshot.check_invariants(); + snapshot.verify_chunks(&mut rng); + edits.push((snapshot, wrap_edits)); + } + } + 40..=59 => { + let (inlay_snapshot, inlay_edits) = + inlay_map.randomly_mutate(&mut next_inlay_id, &mut rng); + let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits); + let (tabs_snapshot, tab_edits) = + tab_map.sync(fold_snapshot, fold_edits, tab_size); + let (mut snapshot, wrap_edits) = + wrap_map.update(cx, |map, cx| map.sync(tabs_snapshot, tab_edits, cx)); + snapshot.check_invariants(); + snapshot.verify_chunks(&mut rng); + edits.push((snapshot, wrap_edits)); + } + _ => { + buffer.update(cx, |buffer, cx| { + let subscription = buffer.subscribe(); + let edit_count = rng.gen_range(1..=5); + buffer.randomly_mutate(&mut rng, edit_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); + log::info!("InlayMap text: {:?}", inlay_snapshot.text()); + let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits); + log::info!("FoldMap text: {:?}", fold_snapshot.text()); + let (tabs_snapshot, tab_edits) = tab_map.sync(fold_snapshot, fold_edits, tab_size); + log::info!("TabMap text: {:?}", tabs_snapshot.text()); + + let unwrapped_text = tabs_snapshot.text(); + let expected_text = wrap_text(&unwrapped_text, wrap_width, &mut line_wrapper); + let (mut snapshot, wrap_edits) = + wrap_map.update(cx, |map, cx| map.sync(tabs_snapshot.clone(), tab_edits, cx)); + snapshot.check_invariants(); + snapshot.verify_chunks(&mut rng); + edits.push((snapshot, wrap_edits)); + + if wrap_map.read_with(cx, |map, _| map.is_rewrapping()) && rng.gen_bool(0.4) { + log::info!("Waiting for wrapping to finish"); + while wrap_map.read_with(cx, |map, _| map.is_rewrapping()) { + notifications.next().await.unwrap(); + } + wrap_map.read_with(cx, |map, _| assert!(map.pending_edits.is_empty())); + } + + if !wrap_map.read_with(cx, |map, _| map.is_rewrapping()) { + let (mut wrapped_snapshot, wrap_edits) = + wrap_map.update(cx, |map, cx| map.sync(tabs_snapshot, Vec::new(), cx)); + let actual_text = wrapped_snapshot.text(); + let actual_longest_row = wrapped_snapshot.longest_row(); + log::info!("Wrapping finished: {:?}", actual_text); + wrapped_snapshot.check_invariants(); + wrapped_snapshot.verify_chunks(&mut rng); + edits.push((wrapped_snapshot.clone(), wrap_edits)); + assert_eq!( + actual_text, expected_text, + "unwrapped text is: {:?}", + unwrapped_text + ); + + let mut summary = TextSummary::default(); + for (ix, item) in wrapped_snapshot + .transforms + .items(&()) + .into_iter() + .enumerate() + { + summary += &item.summary.output; + log::info!("{} summary: {:?}", ix, item.summary.output,); + } + + if tab_size.get() == 1 + || !wrapped_snapshot + .tab_snapshot + .fold_snapshot + .text() + .contains('\t') + { + let mut expected_longest_rows = Vec::new(); + let mut longest_line_len = -1; + for (row, line) in expected_text.split('\n').enumerate() { + let line_char_count = line.chars().count() as isize; + if line_char_count > longest_line_len { + expected_longest_rows.clear(); + longest_line_len = line_char_count; + } + if line_char_count >= longest_line_len { + expected_longest_rows.push(row as u32); + } + } + + assert!( + expected_longest_rows.contains(&actual_longest_row), + "incorrect longest row {}. expected {:?} with length {}", + actual_longest_row, + expected_longest_rows, + longest_line_len, + ) + } + } + } + + let mut initial_text = Rope::from(initial_snapshot.text().as_str()); + for (snapshot, patch) in edits { + let snapshot_text = Rope::from(snapshot.text().as_str()); + for edit in &patch { + let old_start = initial_text.point_to_offset(Point::new(edit.new.start, 0)); + let old_end = initial_text.point_to_offset(cmp::min( + Point::new(edit.new.start + edit.old.len() as u32, 0), + initial_text.max_point(), + )); + let new_start = snapshot_text.point_to_offset(Point::new(edit.new.start, 0)); + let new_end = snapshot_text.point_to_offset(cmp::min( + Point::new(edit.new.end, 0), + snapshot_text.max_point(), + )); + let new_text = snapshot_text + .chunks_in_range(new_start..new_end) + .collect::(); + + initial_text.replace(old_start..old_end, &new_text); + } + assert_eq!(initial_text.to_string(), snapshot_text.to_string()); + } + + if wrap_map.read_with(cx, |map, _| map.is_rewrapping()) { + log::info!("Waiting for wrapping to finish"); + while wrap_map.read_with(cx, |map, _| map.is_rewrapping()) { + notifications.next().await.unwrap(); + } + } + wrap_map.read_with(cx, |map, _| assert!(map.pending_edits.is_empty())); + } + + fn init_test(cx: &mut gpui::TestAppContext) { + cx.update(|cx| { + let settings = SettingsStore::test(cx); + cx.set_global(settings); + theme::init(LoadThemes::JustBase, cx); + }); + } + + fn wrap_text( + unwrapped_text: &str, + wrap_width: Option, + line_wrapper: &mut LineWrapper, + ) -> String { + if let Some(wrap_width) = wrap_width { + let mut wrapped_text = String::new(); + for (row, line) in unwrapped_text.split('\n').enumerate() { + if row > 0 { + wrapped_text.push('\n') + } + + let mut prev_ix = 0; + for boundary in line_wrapper.wrap_line(line, wrap_width) { + wrapped_text.push_str(&line[prev_ix..boundary.ix]); + wrapped_text.push('\n'); + wrapped_text.push_str(&" ".repeat(boundary.next_indent as usize)); + prev_ix = boundary.ix; + } + wrapped_text.push_str(&line[prev_ix..]); + } + wrapped_text + } else { + unwrapped_text.to_string() + } + } + + impl WrapSnapshot { + pub fn text(&self) -> String { + self.text_chunks(0).collect() + } + + pub fn text_chunks(&self, wrap_row: u32) -> impl Iterator { + self.chunks( + wrap_row..self.max_point().row() + 1, + false, + Highlights::default(), + ) + .map(|h| h.text) + } + + fn verify_chunks(&mut self, rng: &mut impl Rng) { + for _ in 0..5 { + let mut end_row = rng.gen_range(0..=self.max_point().row()); + let start_row = rng.gen_range(0..=end_row); + end_row += 1; + + let mut expected_text = self.text_chunks(start_row).collect::(); + if expected_text.ends_with('\n') { + expected_text.push('\n'); + } + let mut expected_text = expected_text + .lines() + .take((end_row - start_row) as usize) + .collect::>() + .join("\n"); + if end_row <= self.max_point().row() { + expected_text.push('\n'); + } + + let actual_text = self + .chunks(start_row..end_row, true, Highlights::default()) + .map(|c| c.text) + .collect::(); + assert_eq!( + expected_text, + actual_text, + "chunks != highlighted_chunks for rows {:?}", + start_row..end_row + ); + } + } + } +} diff --git a/crates/editor2/src/inlay_hint_cache.rs b/crates/editor2/src/inlay_hint_cache.rs index 36b4e6af6699f927b117ca6eeb8588bb21f30d67..c3722e214cf79cf518cbc614978ca098bb97fee8 100644 --- a/crates/editor2/src/inlay_hint_cache.rs +++ b/crates/editor2/src/inlay_hint_cache.rs @@ -2401,346 +2401,347 @@ pub mod tests { }); } - // #[gpui::test(iterations = 10)] - // async fn test_multiple_excerpts_large_multibuffer(cx: &mut gpui::TestAppContext) { - // init_test(cx, |settings| { - // settings.defaults.inlay_hints = Some(InlayHintSettings { - // enabled: true, - // show_type_hints: true, - // show_parameter_hints: true, - // show_other_hints: true, - // }) - // }); - - // let mut language = Language::new( - // LanguageConfig { - // name: "Rust".into(), - // path_suffixes: vec!["rs".to_string()], - // ..Default::default() - // }, - // Some(tree_sitter_rust::language()), - // ); - // let mut fake_servers = language - // .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { - // capabilities: lsp::ServerCapabilities { - // inlay_hint_provider: Some(lsp::OneOf::Left(true)), - // ..Default::default() - // }, - // ..Default::default() - // })) - // .await; - // let language = Arc::new(language); - // let fs = FakeFs::new(cx.background_executor.clone()); - // fs.insert_tree( - // "/a", - // json!({ - // "main.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|i| format!("let i = {i};\n")).collect::>().join("")), - // "other.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|j| format!("let j = {j};\n")).collect::>().join("")), - // }), - // ) - // .await; - // let project = Project::test(fs, ["/a".as_ref()], cx).await; - // project.update(cx, |project, _| { - // project.languages().add(Arc::clone(&language)) - // }); - // let worktree_id = project.update(cx, |project, cx| { - // project.worktrees().next().unwrap().read(cx).id() - // }); - - // let buffer_1 = project - // .update(cx, |project, cx| { - // project.open_buffer((worktree_id, "main.rs"), cx) - // }) - // .await - // .unwrap(); - // let buffer_2 = project - // .update(cx, |project, cx| { - // project.open_buffer((worktree_id, "other.rs"), cx) - // }) - // .await - // .unwrap(); - // let multibuffer = cx.build_model(|cx| { - // let mut multibuffer = MultiBuffer::new(0); - // multibuffer.push_excerpts( - // buffer_1.clone(), - // [ - // ExcerptRange { - // context: Point::new(0, 0)..Point::new(2, 0), - // primary: None, - // }, - // ExcerptRange { - // context: Point::new(4, 0)..Point::new(11, 0), - // primary: None, - // }, - // ExcerptRange { - // context: Point::new(22, 0)..Point::new(33, 0), - // primary: None, - // }, - // ExcerptRange { - // context: Point::new(44, 0)..Point::new(55, 0), - // primary: None, - // }, - // ExcerptRange { - // context: Point::new(56, 0)..Point::new(66, 0), - // primary: None, - // }, - // ExcerptRange { - // context: Point::new(67, 0)..Point::new(77, 0), - // primary: None, - // }, - // ], - // cx, - // ); - // multibuffer.push_excerpts( - // buffer_2.clone(), - // [ - // ExcerptRange { - // context: Point::new(0, 1)..Point::new(2, 1), - // primary: None, - // }, - // ExcerptRange { - // context: Point::new(4, 1)..Point::new(11, 1), - // primary: None, - // }, - // ExcerptRange { - // context: Point::new(22, 1)..Point::new(33, 1), - // primary: None, - // }, - // ExcerptRange { - // context: Point::new(44, 1)..Point::new(55, 1), - // primary: None, - // }, - // ExcerptRange { - // context: Point::new(56, 1)..Point::new(66, 1), - // primary: None, - // }, - // ExcerptRange { - // context: Point::new(67, 1)..Point::new(77, 1), - // primary: None, - // }, - // ], - // cx, - // ); - // multibuffer - // }); - - // cx.executor().run_until_parked(); - // let editor = - // cx.add_window(|cx| Editor::for_multibuffer(multibuffer, Some(project.clone()), cx)); - // let editor_edited = Arc::new(AtomicBool::new(false)); - // let fake_server = fake_servers.next().await.unwrap(); - // let closure_editor_edited = Arc::clone(&editor_edited); - // fake_server - // .handle_request::(move |params, _| { - // let task_editor_edited = Arc::clone(&closure_editor_edited); - // async move { - // let hint_text = if params.text_document.uri - // == lsp::Url::from_file_path("/a/main.rs").unwrap() - // { - // "main hint" - // } else if params.text_document.uri - // == lsp::Url::from_file_path("/a/other.rs").unwrap() - // { - // "other hint" - // } else { - // panic!("unexpected uri: {:?}", params.text_document.uri); - // }; - - // // one hint per excerpt - // let positions = [ - // lsp::Position::new(0, 2), - // lsp::Position::new(4, 2), - // lsp::Position::new(22, 2), - // lsp::Position::new(44, 2), - // lsp::Position::new(56, 2), - // lsp::Position::new(67, 2), - // ]; - // let out_of_range_hint = lsp::InlayHint { - // position: lsp::Position::new( - // params.range.start.line + 99, - // params.range.start.character + 99, - // ), - // label: lsp::InlayHintLabel::String( - // "out of excerpt range, should be ignored".to_string(), - // ), - // kind: None, - // text_edits: None, - // tooltip: None, - // padding_left: None, - // padding_right: None, - // data: None, - // }; - - // let edited = task_editor_edited.load(Ordering::Acquire); - // Ok(Some( - // std::iter::once(out_of_range_hint) - // .chain(positions.into_iter().enumerate().map(|(i, position)| { - // lsp::InlayHint { - // position, - // label: lsp::InlayHintLabel::String(format!( - // "{hint_text}{} #{i}", - // if edited { "(edited)" } else { "" }, - // )), - // kind: None, - // text_edits: None, - // tooltip: None, - // padding_left: None, - // padding_right: None, - // data: None, - // } - // })) - // .collect(), - // )) - // } - // }) - // .next() - // .await; - // cx.executor().run_until_parked(); - - // editor.update(cx, |editor, cx| { - // let expected_hints = vec![ - // "main hint #0".to_string(), - // "main hint #1".to_string(), - // "main hint #2".to_string(), - // "main hint #3".to_string(), - // // todo!() there used to be no these hints, but new gpui2 presumably scrolls a bit farther - // // (or renders less?) note that tests below pass - // "main hint #4".to_string(), - // "main hint #5".to_string(), - // ]; - // assert_eq!( - // expected_hints, - // cached_hint_labels(editor), - // "When scroll is at the edge of a multibuffer, its visible excerpts only should be queried for inlay hints" - // ); - // assert_eq!(expected_hints, visible_hint_labels(editor, cx)); - // assert_eq!(editor.inlay_hint_cache().version, expected_hints.len(), "Every visible excerpt hints should bump the verison"); - // }); - - // editor.update(cx, |editor, cx| { - // editor.change_selections(Some(Autoscroll::Next), cx, |s| { - // s.select_ranges([Point::new(4, 0)..Point::new(4, 0)]) - // }); - // editor.change_selections(Some(Autoscroll::Next), cx, |s| { - // s.select_ranges([Point::new(22, 0)..Point::new(22, 0)]) - // }); - // editor.change_selections(Some(Autoscroll::Next), cx, |s| { - // s.select_ranges([Point::new(50, 0)..Point::new(50, 0)]) - // }); - // }); - // cx.executor().run_until_parked(); - // editor.update(cx, |editor, cx| { - // let expected_hints = vec![ - // "main hint #0".to_string(), - // "main hint #1".to_string(), - // "main hint #2".to_string(), - // "main hint #3".to_string(), - // "main hint #4".to_string(), - // "main hint #5".to_string(), - // "other hint #0".to_string(), - // "other hint #1".to_string(), - // "other hint #2".to_string(), - // ]; - // assert_eq!(expected_hints, cached_hint_labels(editor), - // "With more scrolls of the multibuffer, more hints should be added into the cache and nothing invalidated without edits"); - // assert_eq!(expected_hints, visible_hint_labels(editor, cx)); - // assert_eq!(editor.inlay_hint_cache().version, expected_hints.len(), - // "Due to every excerpt having one hint, we update cache per new excerpt scrolled"); - // }); - - // editor.update(cx, |editor, cx| { - // editor.change_selections(Some(Autoscroll::Next), cx, |s| { - // s.select_ranges([Point::new(100, 0)..Point::new(100, 0)]) - // }); - // }); - // cx.executor().advance_clock(Duration::from_millis( - // INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100, - // )); - // cx.executor().run_until_parked(); - // let last_scroll_update_version = editor.update(cx, |editor, cx| { - // let expected_hints = vec![ - // "main hint #0".to_string(), - // "main hint #1".to_string(), - // "main hint #2".to_string(), - // "main hint #3".to_string(), - // "main hint #4".to_string(), - // "main hint #5".to_string(), - // "other hint #0".to_string(), - // "other hint #1".to_string(), - // "other hint #2".to_string(), - // "other hint #3".to_string(), - // "other hint #4".to_string(), - // "other hint #5".to_string(), - // ]; - // assert_eq!(expected_hints, cached_hint_labels(editor), - // "After multibuffer was scrolled to the end, all hints for all excerpts should be fetched"); - // assert_eq!(expected_hints, visible_hint_labels(editor, cx)); - // assert_eq!(editor.inlay_hint_cache().version, expected_hints.len()); - // expected_hints.len() - // }).unwrap(); - - // editor.update(cx, |editor, cx| { - // editor.change_selections(Some(Autoscroll::Next), cx, |s| { - // s.select_ranges([Point::new(4, 0)..Point::new(4, 0)]) - // }); - // }); - // cx.executor().run_until_parked(); - // editor.update(cx, |editor, cx| { - // let expected_hints = vec![ - // "main hint #0".to_string(), - // "main hint #1".to_string(), - // "main hint #2".to_string(), - // "main hint #3".to_string(), - // "main hint #4".to_string(), - // "main hint #5".to_string(), - // "other hint #0".to_string(), - // "other hint #1".to_string(), - // "other hint #2".to_string(), - // "other hint #3".to_string(), - // "other hint #4".to_string(), - // "other hint #5".to_string(), - // ]; - // assert_eq!(expected_hints, cached_hint_labels(editor), - // "After multibuffer was scrolled to the end, further scrolls up should not bring more hints"); - // assert_eq!(expected_hints, visible_hint_labels(editor, cx)); - // assert_eq!(editor.inlay_hint_cache().version, last_scroll_update_version, "No updates should happen during scrolling already scolled buffer"); - // }); - - // editor_edited.store(true, Ordering::Release); - // editor.update(cx, |editor, cx| { - // editor.change_selections(None, cx, |s| { - // s.select_ranges([Point::new(56, 0)..Point::new(56, 0)]) - // }); - // editor.handle_input("++++more text++++", cx); - // }); - // cx.executor().run_until_parked(); - // editor.update(cx, |editor, cx| { - // let expected_hints = vec![ - // "main hint(edited) #0".to_string(), - // "main hint(edited) #1".to_string(), - // "main hint(edited) #2".to_string(), - // "main hint(edited) #3".to_string(), - // "main hint(edited) #4".to_string(), - // "main hint(edited) #5".to_string(), - // "other hint(edited) #0".to_string(), - // "other hint(edited) #1".to_string(), - // ]; - // assert_eq!( - // expected_hints, - // cached_hint_labels(editor), - // "After multibuffer edit, editor gets scolled back to the last selection; \ - // all hints should be invalidated and requeried for all of its visible excerpts" - // ); - // assert_eq!(expected_hints, visible_hint_labels(editor, cx)); - - // let current_cache_version = editor.inlay_hint_cache().version; - // let minimum_expected_version = last_scroll_update_version + expected_hints.len(); - // assert!( - // current_cache_version == minimum_expected_version || current_cache_version == minimum_expected_version + 1, - // "Due to every excerpt having one hint, cache should update per new excerpt received + 1 potential sporadic update" - // ); - // }); - // } + #[gpui::test(iterations = 10)] + async fn test_multiple_excerpts_large_multibuffer(cx: &mut gpui::TestAppContext) { + // todo!() this test is flaky + init_test(cx, |settings| { + settings.defaults.inlay_hints = Some(InlayHintSettings { + enabled: true, + show_type_hints: true, + show_parameter_hints: true, + show_other_hints: true, + }) + }); + + let mut language = Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ); + let mut fake_servers = language + .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { + capabilities: lsp::ServerCapabilities { + inlay_hint_provider: Some(lsp::OneOf::Left(true)), + ..Default::default() + }, + ..Default::default() + })) + .await; + let language = Arc::new(language); + let fs = FakeFs::new(cx.background_executor.clone()); + fs.insert_tree( + "/a", + json!({ + "main.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|i| format!("let i = {i};\n")).collect::>().join("")), + "other.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|j| format!("let j = {j};\n")).collect::>().join("")), + }), + ) + .await; + let project = Project::test(fs, ["/a".as_ref()], cx).await; + project.update(cx, |project, _| { + project.languages().add(Arc::clone(&language)) + }); + let worktree_id = project.update(cx, |project, cx| { + project.worktrees().next().unwrap().read(cx).id() + }); + + let buffer_1 = project + .update(cx, |project, cx| { + project.open_buffer((worktree_id, "main.rs"), cx) + }) + .await + .unwrap(); + let buffer_2 = project + .update(cx, |project, cx| { + project.open_buffer((worktree_id, "other.rs"), cx) + }) + .await + .unwrap(); + let multibuffer = cx.build_model(|cx| { + let mut multibuffer = MultiBuffer::new(0); + multibuffer.push_excerpts( + buffer_1.clone(), + [ + ExcerptRange { + context: Point::new(0, 0)..Point::new(2, 0), + primary: None, + }, + ExcerptRange { + context: Point::new(4, 0)..Point::new(11, 0), + primary: None, + }, + ExcerptRange { + context: Point::new(22, 0)..Point::new(33, 0), + primary: None, + }, + ExcerptRange { + context: Point::new(44, 0)..Point::new(55, 0), + primary: None, + }, + ExcerptRange { + context: Point::new(56, 0)..Point::new(66, 0), + primary: None, + }, + ExcerptRange { + context: Point::new(67, 0)..Point::new(77, 0), + primary: None, + }, + ], + cx, + ); + multibuffer.push_excerpts( + buffer_2.clone(), + [ + ExcerptRange { + context: Point::new(0, 1)..Point::new(2, 1), + primary: None, + }, + ExcerptRange { + context: Point::new(4, 1)..Point::new(11, 1), + primary: None, + }, + ExcerptRange { + context: Point::new(22, 1)..Point::new(33, 1), + primary: None, + }, + ExcerptRange { + context: Point::new(44, 1)..Point::new(55, 1), + primary: None, + }, + ExcerptRange { + context: Point::new(56, 1)..Point::new(66, 1), + primary: None, + }, + ExcerptRange { + context: Point::new(67, 1)..Point::new(77, 1), + primary: None, + }, + ], + cx, + ); + multibuffer + }); + + cx.executor().run_until_parked(); + let editor = + cx.add_window(|cx| Editor::for_multibuffer(multibuffer, Some(project.clone()), cx)); + let editor_edited = Arc::new(AtomicBool::new(false)); + let fake_server = fake_servers.next().await.unwrap(); + let closure_editor_edited = Arc::clone(&editor_edited); + fake_server + .handle_request::(move |params, _| { + let task_editor_edited = Arc::clone(&closure_editor_edited); + async move { + let hint_text = if params.text_document.uri + == lsp::Url::from_file_path("/a/main.rs").unwrap() + { + "main hint" + } else if params.text_document.uri + == lsp::Url::from_file_path("/a/other.rs").unwrap() + { + "other hint" + } else { + panic!("unexpected uri: {:?}", params.text_document.uri); + }; + + // one hint per excerpt + let positions = [ + lsp::Position::new(0, 2), + lsp::Position::new(4, 2), + lsp::Position::new(22, 2), + lsp::Position::new(44, 2), + lsp::Position::new(56, 2), + lsp::Position::new(67, 2), + ]; + let out_of_range_hint = lsp::InlayHint { + position: lsp::Position::new( + params.range.start.line + 99, + params.range.start.character + 99, + ), + label: lsp::InlayHintLabel::String( + "out of excerpt range, should be ignored".to_string(), + ), + kind: None, + text_edits: None, + tooltip: None, + padding_left: None, + padding_right: None, + data: None, + }; + + let edited = task_editor_edited.load(Ordering::Acquire); + Ok(Some( + std::iter::once(out_of_range_hint) + .chain(positions.into_iter().enumerate().map(|(i, position)| { + lsp::InlayHint { + position, + label: lsp::InlayHintLabel::String(format!( + "{hint_text}{} #{i}", + if edited { "(edited)" } else { "" }, + )), + kind: None, + text_edits: None, + tooltip: None, + padding_left: None, + padding_right: None, + data: None, + } + })) + .collect(), + )) + } + }) + .next() + .await; + cx.executor().run_until_parked(); + + editor.update(cx, |editor, cx| { + let expected_hints = vec![ + "main hint #0".to_string(), + "main hint #1".to_string(), + "main hint #2".to_string(), + "main hint #3".to_string(), + // todo!() there used to be no these hints, but new gpui2 presumably scrolls a bit farther + // (or renders less?) note that tests below pass + "main hint #4".to_string(), + "main hint #5".to_string(), + ]; + assert_eq!( + expected_hints, + cached_hint_labels(editor), + "When scroll is at the edge of a multibuffer, its visible excerpts only should be queried for inlay hints" + ); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + assert_eq!(editor.inlay_hint_cache().version, expected_hints.len(), "Every visible excerpt hints should bump the verison"); + }); + + editor.update(cx, |editor, cx| { + editor.change_selections(Some(Autoscroll::Next), cx, |s| { + s.select_ranges([Point::new(4, 0)..Point::new(4, 0)]) + }); + editor.change_selections(Some(Autoscroll::Next), cx, |s| { + s.select_ranges([Point::new(22, 0)..Point::new(22, 0)]) + }); + editor.change_selections(Some(Autoscroll::Next), cx, |s| { + s.select_ranges([Point::new(50, 0)..Point::new(50, 0)]) + }); + }); + cx.executor().run_until_parked(); + editor.update(cx, |editor, cx| { + let expected_hints = vec![ + "main hint #0".to_string(), + "main hint #1".to_string(), + "main hint #2".to_string(), + "main hint #3".to_string(), + "main hint #4".to_string(), + "main hint #5".to_string(), + "other hint #0".to_string(), + "other hint #1".to_string(), + "other hint #2".to_string(), + ]; + assert_eq!(expected_hints, cached_hint_labels(editor), + "With more scrolls of the multibuffer, more hints should be added into the cache and nothing invalidated without edits"); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + assert_eq!(editor.inlay_hint_cache().version, expected_hints.len(), + "Due to every excerpt having one hint, we update cache per new excerpt scrolled"); + }); + + editor.update(cx, |editor, cx| { + editor.change_selections(Some(Autoscroll::Next), cx, |s| { + s.select_ranges([Point::new(100, 0)..Point::new(100, 0)]) + }); + }); + cx.executor().advance_clock(Duration::from_millis( + INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100, + )); + cx.executor().run_until_parked(); + let last_scroll_update_version = editor.update(cx, |editor, cx| { + let expected_hints = vec![ + "main hint #0".to_string(), + "main hint #1".to_string(), + "main hint #2".to_string(), + "main hint #3".to_string(), + "main hint #4".to_string(), + "main hint #5".to_string(), + "other hint #0".to_string(), + "other hint #1".to_string(), + "other hint #2".to_string(), + "other hint #3".to_string(), + "other hint #4".to_string(), + "other hint #5".to_string(), + ]; + assert_eq!(expected_hints, cached_hint_labels(editor), + "After multibuffer was scrolled to the end, all hints for all excerpts should be fetched"); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + assert_eq!(editor.inlay_hint_cache().version, expected_hints.len()); + expected_hints.len() + }).unwrap(); + + editor.update(cx, |editor, cx| { + editor.change_selections(Some(Autoscroll::Next), cx, |s| { + s.select_ranges([Point::new(4, 0)..Point::new(4, 0)]) + }); + }); + cx.executor().run_until_parked(); + editor.update(cx, |editor, cx| { + let expected_hints = vec![ + "main hint #0".to_string(), + "main hint #1".to_string(), + "main hint #2".to_string(), + "main hint #3".to_string(), + "main hint #4".to_string(), + "main hint #5".to_string(), + "other hint #0".to_string(), + "other hint #1".to_string(), + "other hint #2".to_string(), + "other hint #3".to_string(), + "other hint #4".to_string(), + "other hint #5".to_string(), + ]; + assert_eq!(expected_hints, cached_hint_labels(editor), + "After multibuffer was scrolled to the end, further scrolls up should not bring more hints"); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + assert_eq!(editor.inlay_hint_cache().version, last_scroll_update_version, "No updates should happen during scrolling already scolled buffer"); + }); + + editor_edited.store(true, Ordering::Release); + editor.update(cx, |editor, cx| { + editor.change_selections(None, cx, |s| { + s.select_ranges([Point::new(56, 0)..Point::new(56, 0)]) + }); + editor.handle_input("++++more text++++", cx); + }); + cx.executor().run_until_parked(); + editor.update(cx, |editor, cx| { + let expected_hints = vec![ + "main hint(edited) #0".to_string(), + "main hint(edited) #1".to_string(), + "main hint(edited) #2".to_string(), + "main hint(edited) #3".to_string(), + "main hint(edited) #4".to_string(), + "main hint(edited) #5".to_string(), + "other hint(edited) #0".to_string(), + "other hint(edited) #1".to_string(), + ]; + assert_eq!( + expected_hints, + cached_hint_labels(editor), + "After multibuffer edit, editor gets scolled back to the last selection; \ + all hints should be invalidated and requeried for all of its visible excerpts" + ); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + + let current_cache_version = editor.inlay_hint_cache().version; + let minimum_expected_version = last_scroll_update_version + expected_hints.len(); + assert!( + current_cache_version == minimum_expected_version || current_cache_version == minimum_expected_version + 1, + "Due to every excerpt having one hint, cache should update per new excerpt received + 1 potential sporadic update" + ); + }); + } #[gpui::test] async fn test_excerpts_removed(cx: &mut gpui::TestAppContext) { diff --git a/crates/gpui2/src/gpui2.rs b/crates/gpui2/src/gpui2.rs index 984859f1b005f8fa2edd3256de75c1aa3010ce2b..5b88286240e1ef35df7ab4dd5356b7498ff2bb69 100644 --- a/crates/gpui2/src/gpui2.rs +++ b/crates/gpui2/src/gpui2.rs @@ -21,7 +21,7 @@ mod subscription; mod svg_renderer; mod taffy; #[cfg(any(test, feature = "test-support"))] -mod test; +pub mod test; mod text_system; mod util; mod view; diff --git a/crates/gpui2/src/platform.rs b/crates/gpui2/src/platform.rs index 7375f47939899d8d9902e400b337da723f96dc34..37b156e34893771663f6b3826e0cf88a76f83fc5 100644 --- a/crates/gpui2/src/platform.rs +++ b/crates/gpui2/src/platform.rs @@ -44,7 +44,7 @@ pub(crate) fn current_platform() -> Rc { Rc::new(MacPlatform::new()) } -pub(crate) trait Platform: 'static { +pub trait Platform: 'static { fn background_executor(&self) -> BackgroundExecutor; fn foreground_executor(&self) -> ForegroundExecutor; fn text_system(&self) -> Arc; @@ -128,7 +128,7 @@ impl Debug for DisplayId { unsafe impl Send for DisplayId {} -pub(crate) trait PlatformWindow { +pub trait PlatformWindow { fn bounds(&self) -> WindowBounds; fn content_size(&self) -> Size; fn scale_factor(&self) -> f32; diff --git a/crates/gpui2/src/scene.rs b/crates/gpui2/src/scene.rs index 549260560236ffb43179caf6cb8fce5340c10b7a..ca0a50546e0f56b80ca8b6c055fde0ab57f430f9 100644 --- a/crates/gpui2/src/scene.rs +++ b/crates/gpui2/src/scene.rs @@ -198,7 +198,7 @@ impl SceneBuilder { } } -pub(crate) struct Scene { +pub struct Scene { pub shadows: Vec, pub quads: Vec, pub paths: Vec>, @@ -214,7 +214,7 @@ impl Scene { &self.paths } - pub fn batches(&self) -> impl Iterator { + pub(crate) fn batches(&self) -> impl Iterator { BatchIterator { shadows: &self.shadows, shadows_start: 0, diff --git a/crates/gpui2/src/test.rs b/crates/gpui2/src/test.rs index 3f2697f7e3f2d2c6c44165728503b7fa1accf6e0..5a21576fb26178ff67c750ca7ee63652690f1700 100644 --- a/crates/gpui2/src/test.rs +++ b/crates/gpui2/src/test.rs @@ -1,5 +1,7 @@ -use crate::TestDispatcher; +use crate::{Entity, Subscription, TestAppContext, TestDispatcher}; +use futures::StreamExt as _; use rand::prelude::*; +use smol::channel; use std::{ env, panic::{self, RefUnwindSafe}, @@ -49,3 +51,30 @@ pub fn run_test( } } } + +pub struct Observation { + rx: channel::Receiver, + _subscription: Subscription, +} + +impl futures::Stream for Observation { + type Item = T; + + fn poll_next( + mut self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + self.rx.poll_next_unpin(cx) + } +} + +pub fn observe(entity: &impl Entity, cx: &mut TestAppContext) -> Observation<()> { + let (tx, rx) = smol::channel::unbounded(); + let _subscription = cx.update(|cx| { + cx.observe(entity, move |_, _| { + let _ = smol::block_on(tx.send(())); + }) + }); + + Observation { rx, _subscription } +} 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 05/35] 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( From e821e1fc35cd40607713132994ad2805252b8e08 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Fri, 1 Dec 2023 20:14:14 +0100 Subject: [PATCH 06/35] Display map tests (3 flaky tests for chunks) --- crates/editor2/src/display_map.rs | 1768 ++++++++++++++--------------- 1 file changed, 866 insertions(+), 902 deletions(-) diff --git a/crates/editor2/src/display_map.rs b/crates/editor2/src/display_map.rs index 533abcd871b6165bc3f400dbceda69122b71b361..1aee04dd0ae02b8d4ea98025be177a82e3801ef7 100644 --- a/crates/editor2/src/display_map.rs +++ b/crates/editor2/src/display_map.rs @@ -990,905 +990,869 @@ pub fn next_rows(display_row: u32, display_map: &DisplaySnapshot) -> impl Iterat }) } -// #[cfg(test)] -// pub mod tests { -// use super::*; -// use crate::{ -// movement, -// test::{editor_test_context::EditorTestContext, marked_display_snapshot}, -// }; -// use gpui::{AppContext, Hsla}; -// use language::{ -// language_settings::{AllLanguageSettings, AllLanguageSettingsContent}, -// Buffer, Language, LanguageConfig, SelectionGoal, -// }; -// use project::Project; -// use rand::{prelude::*, Rng}; -// use settings::SettingsStore; -// use smol::stream::StreamExt; -// use std::{env, sync::Arc}; -// use theme::SyntaxTheme; -// use util::test::{marked_text_ranges, sample_text}; -// use Bias::*; - -// #[gpui::test(iterations = 100)] -// async fn test_random_display_map(cx: &mut gpui::TestAppContext, mut rng: StdRng) { -// cx.foreground().set_block_on_ticks(0..=50); -// cx.foreground().forbid_parking(); -// let operations = env::var("OPERATIONS") -// .map(|i| i.parse().expect("invalid `OPERATIONS` variable")) -// .unwrap_or(10); - -// let font_cache = cx.font_cache().clone(); -// let mut tab_size = rng.gen_range(1..=4); -// let buffer_start_excerpt_header_height = rng.gen_range(1..=5); -// let excerpt_header_height = rng.gen_range(1..=5); -// let family_id = font_cache -// .load_family(&["Helvetica"], &Default::default()) -// .unwrap(); -// let font_id = font_cache -// .select_font(family_id, &Default::default()) -// .unwrap(); -// let font_size = 14.0; -// let max_wrap_width = 300.0; -// let mut wrap_width = if rng.gen_bool(0.1) { -// None -// } else { -// Some(rng.gen_range(0.0..=max_wrap_width)) -// }; - -// log::info!("tab size: {}", tab_size); -// log::info!("wrap width: {:?}", wrap_width); - -// cx.update(|cx| { -// init_test(cx, |s| s.defaults.tab_size = NonZeroU32::new(tab_size)); -// }); - -// let buffer = cx.update(|cx| { -// if rng.gen() { -// let len = rng.gen_range(0..10); -// let text = util::RandomCharIter::new(&mut rng) -// .take(len) -// .collect::(); -// MultiBuffer::build_simple(&text, cx) -// } else { -// MultiBuffer::build_random(&mut rng, cx) -// } -// }); - -// let map = cx.add_model(|cx| { -// DisplayMap::new( -// buffer.clone(), -// font_id, -// font_size, -// wrap_width, -// buffer_start_excerpt_header_height, -// excerpt_header_height, -// cx, -// ) -// }); -// let mut notifications = observe(&map, cx); -// let mut fold_count = 0; -// let mut blocks = Vec::new(); - -// let snapshot = map.update(cx, |map, cx| map.snapshot(cx)); -// log::info!("buffer text: {:?}", snapshot.buffer_snapshot.text()); -// log::info!("fold text: {:?}", snapshot.fold_snapshot.text()); -// log::info!("tab text: {:?}", snapshot.tab_snapshot.text()); -// log::info!("wrap text: {:?}", snapshot.wrap_snapshot.text()); -// log::info!("block text: {:?}", snapshot.block_snapshot.text()); -// log::info!("display text: {:?}", snapshot.text()); - -// for _i in 0..operations { -// match rng.gen_range(0..100) { -// 0..=19 => { -// wrap_width = if rng.gen_bool(0.2) { -// None -// } else { -// Some(rng.gen_range(0.0..=max_wrap_width)) -// }; -// log::info!("setting wrap width to {:?}", wrap_width); -// map.update(cx, |map, cx| map.set_wrap_width(wrap_width, cx)); -// } -// 20..=29 => { -// let mut tab_sizes = vec![1, 2, 3, 4]; -// tab_sizes.remove((tab_size - 1) as usize); -// tab_size = *tab_sizes.choose(&mut rng).unwrap(); -// log::info!("setting tab size to {:?}", tab_size); -// cx.update(|cx| { -// cx.update_global::(|store, cx| { -// store.update_user_settings::(cx, |s| { -// s.defaults.tab_size = NonZeroU32::new(tab_size); -// }); -// }); -// }); -// } -// 30..=44 => { -// map.update(cx, |map, cx| { -// if rng.gen() || blocks.is_empty() { -// let buffer = map.snapshot(cx).buffer_snapshot; -// let block_properties = (0..rng.gen_range(1..=1)) -// .map(|_| { -// 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::>(); -// blocks.extend(map.insert_blocks(block_properties, cx)); -// } else { -// blocks.shuffle(&mut rng); -// let remove_count = rng.gen_range(1..=4.min(blocks.len())); -// let block_ids_to_remove = (0..remove_count) -// .map(|_| blocks.remove(rng.gen_range(0..blocks.len()))) -// .collect(); -// log::info!("removing block ids {:?}", block_ids_to_remove); -// map.remove_blocks(block_ids_to_remove, cx); -// } -// }); -// } -// 45..=79 => { -// let mut ranges = Vec::new(); -// for _ in 0..rng.gen_range(1..=3) { -// buffer.read_with(cx, |buffer, cx| { -// let buffer = buffer.read(cx); -// let end = buffer.clip_offset(rng.gen_range(0..=buffer.len()), Right); -// let start = buffer.clip_offset(rng.gen_range(0..=end), Left); -// ranges.push(start..end); -// }); -// } - -// if rng.gen() && fold_count > 0 { -// log::info!("unfolding ranges: {:?}", ranges); -// map.update(cx, |map, cx| { -// map.unfold(ranges, true, cx); -// }); -// } else { -// log::info!("folding ranges: {:?}", ranges); -// map.update(cx, |map, cx| { -// map.fold(ranges, cx); -// }); -// } -// } -// _ => { -// buffer.update(cx, |buffer, cx| buffer.randomly_mutate(&mut rng, 5, cx)); -// } -// } - -// if map.read_with(cx, |map, cx| map.is_rewrapping(cx)) { -// notifications.next().await.unwrap(); -// } - -// let snapshot = map.update(cx, |map, cx| map.snapshot(cx)); -// fold_count = snapshot.fold_count(); -// log::info!("buffer text: {:?}", snapshot.buffer_snapshot.text()); -// log::info!("fold text: {:?}", snapshot.fold_snapshot.text()); -// log::info!("tab text: {:?}", snapshot.tab_snapshot.text()); -// log::info!("wrap text: {:?}", snapshot.wrap_snapshot.text()); -// log::info!("block text: {:?}", snapshot.block_snapshot.text()); -// log::info!("display text: {:?}", snapshot.text()); - -// // Line boundaries -// let buffer = &snapshot.buffer_snapshot; -// for _ in 0..5 { -// let row = rng.gen_range(0..=buffer.max_point().row); -// let column = rng.gen_range(0..=buffer.line_len(row)); -// let point = buffer.clip_point(Point::new(row, column), Left); - -// let (prev_buffer_bound, prev_display_bound) = snapshot.prev_line_boundary(point); -// let (next_buffer_bound, next_display_bound) = snapshot.next_line_boundary(point); - -// assert!(prev_buffer_bound <= point); -// assert!(next_buffer_bound >= point); -// assert_eq!(prev_buffer_bound.column, 0); -// assert_eq!(prev_display_bound.column(), 0); -// if next_buffer_bound < buffer.max_point() { -// assert_eq!(buffer.chars_at(next_buffer_bound).next(), Some('\n')); -// } - -// assert_eq!( -// prev_display_bound, -// prev_buffer_bound.to_display_point(&snapshot), -// "row boundary before {:?}. reported buffer row boundary: {:?}", -// point, -// prev_buffer_bound -// ); -// assert_eq!( -// next_display_bound, -// next_buffer_bound.to_display_point(&snapshot), -// "display row boundary after {:?}. reported buffer row boundary: {:?}", -// point, -// next_buffer_bound -// ); -// assert_eq!( -// prev_buffer_bound, -// prev_display_bound.to_point(&snapshot), -// "row boundary before {:?}. reported display row boundary: {:?}", -// point, -// prev_display_bound -// ); -// assert_eq!( -// next_buffer_bound, -// next_display_bound.to_point(&snapshot), -// "row boundary after {:?}. reported display row boundary: {:?}", -// point, -// next_display_bound -// ); -// } - -// // Movement -// let min_point = snapshot.clip_point(DisplayPoint::new(0, 0), Left); -// let max_point = snapshot.clip_point(snapshot.max_point(), Right); -// for _ in 0..5 { -// let row = rng.gen_range(0..=snapshot.max_point().row()); -// let column = rng.gen_range(0..=snapshot.line_len(row)); -// let point = snapshot.clip_point(DisplayPoint::new(row, column), Left); - -// log::info!("Moving from point {:?}", point); - -// let moved_right = movement::right(&snapshot, point); -// log::info!("Right {:?}", moved_right); -// if point < max_point { -// assert!(moved_right > point); -// if point.column() == snapshot.line_len(point.row()) -// || snapshot.soft_wrap_indent(point.row()).is_some() -// && point.column() == snapshot.line_len(point.row()) - 1 -// { -// assert!(moved_right.row() > point.row()); -// } -// } else { -// assert_eq!(moved_right, point); -// } - -// let moved_left = movement::left(&snapshot, point); -// log::info!("Left {:?}", moved_left); -// if point > min_point { -// assert!(moved_left < point); -// if point.column() == 0 { -// assert!(moved_left.row() < point.row()); -// } -// } else { -// assert_eq!(moved_left, point); -// } -// } -// } -// } - -// #[gpui::test(retries = 5)] -// async fn test_soft_wraps(cx: &mut gpui::TestAppContext) { -// cx.foreground().set_block_on_ticks(usize::MAX..=usize::MAX); -// cx.update(|cx| { -// init_test(cx, |_| {}); -// }); - -// let mut cx = EditorTestContext::new(cx).await; -// let editor = cx.editor.clone(); -// let window = cx.window.clone(); - -// cx.update_window(window, |cx| { -// let text_layout_details = -// editor.read_with(cx, |editor, cx| editor.text_layout_details(cx)); - -// let font_cache = cx.font_cache().clone(); - -// let family_id = font_cache -// .load_family(&["Helvetica"], &Default::default()) -// .unwrap(); -// let font_id = font_cache -// .select_font(family_id, &Default::default()) -// .unwrap(); -// let font_size = 12.0; -// let wrap_width = Some(64.); - -// let text = "one two three four five\nsix seven eight"; -// let buffer = MultiBuffer::build_simple(text, cx); -// let map = cx.add_model(|cx| { -// DisplayMap::new(buffer.clone(), font_id, font_size, wrap_width, 1, 1, cx) -// }); - -// let snapshot = map.update(cx, |map, cx| map.snapshot(cx)); -// assert_eq!( -// snapshot.text_chunks(0).collect::(), -// "one two \nthree four \nfive\nsix seven \neight" -// ); -// assert_eq!( -// snapshot.clip_point(DisplayPoint::new(0, 8), Bias::Left), -// DisplayPoint::new(0, 7) -// ); -// assert_eq!( -// snapshot.clip_point(DisplayPoint::new(0, 8), Bias::Right), -// DisplayPoint::new(1, 0) -// ); -// assert_eq!( -// movement::right(&snapshot, DisplayPoint::new(0, 7)), -// DisplayPoint::new(1, 0) -// ); -// assert_eq!( -// movement::left(&snapshot, DisplayPoint::new(1, 0)), -// DisplayPoint::new(0, 7) -// ); - -// let x = snapshot.x_for_point(DisplayPoint::new(1, 10), &text_layout_details); -// assert_eq!( -// movement::up( -// &snapshot, -// DisplayPoint::new(1, 10), -// SelectionGoal::None, -// false, -// &text_layout_details, -// ), -// ( -// DisplayPoint::new(0, 7), -// SelectionGoal::HorizontalPosition(x) -// ) -// ); -// assert_eq!( -// movement::down( -// &snapshot, -// DisplayPoint::new(0, 7), -// SelectionGoal::HorizontalPosition(x), -// false, -// &text_layout_details -// ), -// ( -// DisplayPoint::new(1, 10), -// SelectionGoal::HorizontalPosition(x) -// ) -// ); -// assert_eq!( -// movement::down( -// &snapshot, -// DisplayPoint::new(1, 10), -// SelectionGoal::HorizontalPosition(x), -// false, -// &text_layout_details -// ), -// ( -// DisplayPoint::new(2, 4), -// SelectionGoal::HorizontalPosition(x) -// ) -// ); - -// let ix = snapshot.buffer_snapshot.text().find("seven").unwrap(); -// buffer.update(cx, |buffer, cx| { -// buffer.edit([(ix..ix, "and ")], None, cx); -// }); - -// let snapshot = map.update(cx, |map, cx| map.snapshot(cx)); -// assert_eq!( -// snapshot.text_chunks(1).collect::(), -// "three four \nfive\nsix and \nseven eight" -// ); - -// // Re-wrap on font size changes -// map.update(cx, |map, cx| map.set_font_with_size(font_id, font_size + 3., cx)); - -// let snapshot = map.update(cx, |map, cx| map.snapshot(cx)); -// assert_eq!( -// snapshot.text_chunks(1).collect::(), -// "three \nfour five\nsix and \nseven \neight" -// ) -// }); -// } - -// #[gpui::test] -// fn test_text_chunks(cx: &mut gpui::AppContext) { -// init_test(cx, |_| {}); - -// let text = sample_text(6, 6, 'a'); -// let buffer = MultiBuffer::build_simple(&text, 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 font_size = 14.0; -// let map = -// cx.add_model(|cx| DisplayMap::new(buffer.clone(), font_id, font_size, None, 1, 1, cx)); - -// buffer.update(cx, |buffer, cx| { -// buffer.edit( -// vec![ -// (Point::new(1, 0)..Point::new(1, 0), "\t"), -// (Point::new(1, 1)..Point::new(1, 1), "\t"), -// (Point::new(2, 1)..Point::new(2, 1), "\t"), -// ], -// None, -// cx, -// ) -// }); - -// assert_eq!( -// map.update(cx, |map, cx| map.snapshot(cx)) -// .text_chunks(1) -// .collect::() -// .lines() -// .next(), -// Some(" b bbbbb") -// ); -// assert_eq!( -// map.update(cx, |map, cx| map.snapshot(cx)) -// .text_chunks(2) -// .collect::() -// .lines() -// .next(), -// Some("c ccccc") -// ); -// } - -// #[gpui::test] -// async fn test_chunks(cx: &mut gpui::TestAppContext) { -// use unindent::Unindent as _; - -// let text = r#" -// fn outer() {} - -// mod module { -// fn inner() {} -// }"# -// .unindent(); - -// let theme = SyntaxTheme::new(vec![ -// ("mod.body".to_string(), Hsla::red().into()), -// ("fn.name".to_string(), Hsla::blue().into()), -// ]); -// let language = Arc::new( -// Language::new( -// LanguageConfig { -// name: "Test".into(), -// path_suffixes: vec![".test".to_string()], -// ..Default::default() -// }, -// Some(tree_sitter_rust::language()), -// ) -// .with_highlights_query( -// r#" -// (mod_item name: (identifier) body: _ @mod.body) -// (function_item name: (identifier) @fn.name) -// "#, -// ) -// .unwrap(), -// ); -// language.set_theme(&theme); - -// cx.update(|cx| init_test(cx, |s| s.defaults.tab_size = Some(2.try_into().unwrap()))); - -// let buffer = cx -// .add_model(|cx| Buffer::new(0, cx.model_id() as u64, text).with_language(language, cx)); -// buffer.condition(cx, |buf, _| !buf.is_parsing()).await; -// let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); - -// let font_cache = cx.font_cache(); -// let family_id = font_cache -// .load_family(&["Helvetica"], &Default::default()) -// .unwrap(); -// let font_id = font_cache -// .select_font(family_id, &Default::default()) -// .unwrap(); -// let font_size = 14.0; - -// let map = cx.add_model(|cx| DisplayMap::new(buffer, font_id, font_size, None, 1, 1, cx)); -// assert_eq!( -// cx.update(|cx| syntax_chunks(0..5, &map, &theme, cx)), -// vec![ -// ("fn ".to_string(), None), -// ("outer".to_string(), Some(Hsla::blue())), -// ("() {}\n\nmod module ".to_string(), None), -// ("{\n fn ".to_string(), Some(Hsla::red())), -// ("inner".to_string(), Some(Hsla::blue())), -// ("() {}\n}".to_string(), Some(Hsla::red())), -// ] -// ); -// assert_eq!( -// cx.update(|cx| syntax_chunks(3..5, &map, &theme, cx)), -// vec![ -// (" fn ".to_string(), Some(Hsla::red())), -// ("inner".to_string(), Some(Hsla::blue())), -// ("() {}\n}".to_string(), Some(Hsla::red())), -// ] -// ); - -// map.update(cx, |map, cx| { -// map.fold(vec![Point::new(0, 6)..Point::new(3, 2)], cx) -// }); -// assert_eq!( -// cx.update(|cx| syntax_chunks(0..2, &map, &theme, cx)), -// vec![ -// ("fn ".to_string(), None), -// ("out".to_string(), Some(Hsla::blue())), -// ("⋯".to_string(), None), -// (" fn ".to_string(), Some(Hsla::red())), -// ("inner".to_string(), Some(Hsla::blue())), -// ("() {}\n}".to_string(), Some(Hsla::red())), -// ] -// ); -// } - -// #[gpui::test] -// async fn test_chunks_with_soft_wrapping(cx: &mut gpui::TestAppContext) { -// use unindent::Unindent as _; - -// cx.foreground().set_block_on_ticks(usize::MAX..=usize::MAX); - -// let text = r#" -// fn outer() {} - -// mod module { -// fn inner() {} -// }"# -// .unindent(); - -// let theme = SyntaxTheme::new(vec![ -// ("mod.body".to_string(), Hsla::red().into()), -// ("fn.name".to_string(), Hsla::blue().into()), -// ]); -// let language = Arc::new( -// Language::new( -// LanguageConfig { -// name: "Test".into(), -// path_suffixes: vec![".test".to_string()], -// ..Default::default() -// }, -// Some(tree_sitter_rust::language()), -// ) -// .with_highlights_query( -// r#" -// (mod_item name: (identifier) body: _ @mod.body) -// (function_item name: (identifier) @fn.name) -// "#, -// ) -// .unwrap(), -// ); -// language.set_theme(&theme); - -// cx.update(|cx| init_test(cx, |_| {})); - -// let buffer = cx -// .add_model(|cx| Buffer::new(0, cx.model_id() as u64, text).with_language(language, cx)); -// buffer.condition(cx, |buf, _| !buf.is_parsing()).await; -// let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); - -// let font_cache = cx.font_cache(); - -// let family_id = font_cache -// .load_family(&["Courier"], &Default::default()) -// .unwrap(); -// let font_id = font_cache -// .select_font(family_id, &Default::default()) -// .unwrap(); -// let font_size = 16.0; - -// let map = -// cx.add_model(|cx| DisplayMap::new(buffer, font_id, font_size, Some(40.0), 1, 1, cx)); -// assert_eq!( -// cx.update(|cx| syntax_chunks(0..5, &map, &theme, cx)), -// [ -// ("fn \n".to_string(), None), -// ("oute\nr".to_string(), Some(Hsla::blue())), -// ("() \n{}\n\n".to_string(), None), -// ] -// ); -// assert_eq!( -// cx.update(|cx| syntax_chunks(3..5, &map, &theme, cx)), -// [("{}\n\n".to_string(), None)] -// ); - -// map.update(cx, |map, cx| { -// map.fold(vec![Point::new(0, 6)..Point::new(3, 2)], cx) -// }); -// assert_eq!( -// cx.update(|cx| syntax_chunks(1..4, &map, &theme, cx)), -// [ -// ("out".to_string(), Some(Hsla::blue())), -// ("⋯\n".to_string(), None), -// (" \nfn ".to_string(), Some(Hsla::red())), -// ("i\n".to_string(), Some(Hsla::blue())) -// ] -// ); -// } - -// #[gpui::test] -// async fn test_chunks_with_text_highlights(cx: &mut gpui::TestAppContext) { -// cx.update(|cx| init_test(cx, |_| {})); - -// let theme = SyntaxTheme::new(vec![ -// ("operator".to_string(), Hsla::red().into()), -// ("string".to_string(), Hsla::green().into()), -// ]); -// let language = Arc::new( -// Language::new( -// LanguageConfig { -// name: "Test".into(), -// path_suffixes: vec![".test".to_string()], -// ..Default::default() -// }, -// Some(tree_sitter_rust::language()), -// ) -// .with_highlights_query( -// r#" -// ":" @operator -// (string_literal) @string -// "#, -// ) -// .unwrap(), -// ); -// language.set_theme(&theme); - -// let (text, highlighted_ranges) = marked_text_ranges(r#"constˇ «a»: B = "c «d»""#, false); - -// let buffer = cx -// .add_model(|cx| Buffer::new(0, cx.model_id() as u64, text).with_language(language, cx)); -// buffer.condition(cx, |buf, _| !buf.is_parsing()).await; - -// let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); -// let buffer_snapshot = buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx)); - -// let font_cache = cx.font_cache(); -// let family_id = font_cache -// .load_family(&["Courier"], &Default::default()) -// .unwrap(); -// let font_id = font_cache -// .select_font(family_id, &Default::default()) -// .unwrap(); -// let font_size = 16.0; -// let map = cx.add_model(|cx| DisplayMap::new(buffer, font_id, font_size, None, 1, 1, cx)); - -// enum MyType {} - -// let style = HighlightStyle { -// color: Some(Hsla::blue()), -// ..Default::default() -// }; - -// map.update(cx, |map, _cx| { -// map.highlight_text( -// TypeId::of::(), -// highlighted_ranges -// .into_iter() -// .map(|range| { -// buffer_snapshot.anchor_before(range.start) -// ..buffer_snapshot.anchor_before(range.end) -// }) -// .collect(), -// style, -// ); -// }); - -// assert_eq!( -// cx.update(|cx| chunks(0..10, &map, &theme, cx)), -// [ -// ("const ".to_string(), None, None), -// ("a".to_string(), None, Some(Hsla::blue())), -// (":".to_string(), Some(Hsla::red()), None), -// (" B = ".to_string(), None, None), -// ("\"c ".to_string(), Some(Hsla::green()), None), -// ("d".to_string(), Some(Hsla::green()), Some(Hsla::blue())), -// ("\"".to_string(), Some(Hsla::green()), None), -// ] -// ); -// } - -// #[gpui::test] -// fn test_clip_point(cx: &mut gpui::AppContext) { -// init_test(cx, |_| {}); - -// fn assert(text: &str, shift_right: bool, bias: Bias, cx: &mut gpui::AppContext) { -// let (unmarked_snapshot, mut markers) = marked_display_snapshot(text, cx); - -// match bias { -// Bias::Left => { -// if shift_right { -// *markers[1].column_mut() += 1; -// } - -// assert_eq!(unmarked_snapshot.clip_point(markers[1], bias), markers[0]) -// } -// Bias::Right => { -// if shift_right { -// *markers[0].column_mut() += 1; -// } - -// assert_eq!(unmarked_snapshot.clip_point(markers[0], bias), markers[1]) -// } -// }; -// } - -// use Bias::{Left, Right}; -// assert("ˇˇα", false, Left, cx); -// assert("ˇˇα", true, Left, cx); -// assert("ˇˇα", false, Right, cx); -// assert("ˇαˇ", true, Right, cx); -// assert("ˇˇ✋", false, Left, cx); -// assert("ˇˇ✋", true, Left, cx); -// assert("ˇˇ✋", false, Right, cx); -// assert("ˇ✋ˇ", true, Right, cx); -// assert("ˇˇ🍐", false, Left, cx); -// assert("ˇˇ🍐", true, Left, cx); -// assert("ˇˇ🍐", false, Right, cx); -// assert("ˇ🍐ˇ", true, Right, cx); -// assert("ˇˇ\t", false, Left, cx); -// assert("ˇˇ\t", true, Left, cx); -// assert("ˇˇ\t", false, Right, cx); -// assert("ˇ\tˇ", true, Right, cx); -// assert(" ˇˇ\t", false, Left, cx); -// assert(" ˇˇ\t", true, Left, cx); -// assert(" ˇˇ\t", false, Right, cx); -// assert(" ˇ\tˇ", true, Right, cx); -// assert(" ˇˇ\t", false, Left, cx); -// assert(" ˇˇ\t", false, Right, cx); -// } - -// #[gpui::test] -// fn test_clip_at_line_ends(cx: &mut gpui::AppContext) { -// init_test(cx, |_| {}); - -// fn assert(text: &str, cx: &mut gpui::AppContext) { -// let (mut unmarked_snapshot, markers) = marked_display_snapshot(text, cx); -// unmarked_snapshot.clip_at_line_ends = true; -// assert_eq!( -// unmarked_snapshot.clip_point(markers[1], Bias::Left), -// markers[0] -// ); -// } - -// assert("ˇˇ", cx); -// assert("ˇaˇ", cx); -// assert("aˇbˇ", cx); -// assert("aˇαˇ", cx); -// } - -// #[gpui::test] -// fn test_tabs_with_multibyte_chars(cx: &mut gpui::AppContext) { -// init_test(cx, |_| {}); - -// let text = "✅\t\tα\nβ\t\n🏀β\t\tγ"; -// let buffer = MultiBuffer::build_simple(text, cx); -// let font_cache = cx.font_cache(); -// let family_id = font_cache -// .load_family(&["Helvetica"], &Default::default()) -// .unwrap(); -// let font_id = font_cache -// .select_font(family_id, &Default::default()) -// .unwrap(); -// let font_size = 14.0; - -// let map = -// cx.add_model(|cx| DisplayMap::new(buffer.clone(), font_id, font_size, None, 1, 1, cx)); -// let map = map.update(cx, |map, cx| map.snapshot(cx)); -// assert_eq!(map.text(), "✅ α\nβ \n🏀β γ"); -// assert_eq!( -// map.text_chunks(0).collect::(), -// "✅ α\nβ \n🏀β γ" -// ); -// assert_eq!(map.text_chunks(1).collect::(), "β \n🏀β γ"); -// assert_eq!(map.text_chunks(2).collect::(), "🏀β γ"); - -// let point = Point::new(0, "✅\t\t".len() as u32); -// let display_point = DisplayPoint::new(0, "✅ ".len() as u32); -// assert_eq!(point.to_display_point(&map), display_point); -// assert_eq!(display_point.to_point(&map), point); - -// let point = Point::new(1, "β\t".len() as u32); -// let display_point = DisplayPoint::new(1, "β ".len() as u32); -// assert_eq!(point.to_display_point(&map), display_point); -// assert_eq!(display_point.to_point(&map), point,); - -// let point = Point::new(2, "🏀β\t\t".len() as u32); -// let display_point = DisplayPoint::new(2, "🏀β ".len() as u32); -// assert_eq!(point.to_display_point(&map), display_point); -// assert_eq!(display_point.to_point(&map), point,); - -// // Display points inside of expanded tabs -// assert_eq!( -// DisplayPoint::new(0, "✅ ".len() as u32).to_point(&map), -// Point::new(0, "✅\t".len() as u32), -// ); -// assert_eq!( -// DisplayPoint::new(0, "✅ ".len() as u32).to_point(&map), -// Point::new(0, "✅".len() as u32), -// ); - -// // Clipping display points inside of multi-byte characters -// assert_eq!( -// map.clip_point(DisplayPoint::new(0, "✅".len() as u32 - 1), Left), -// DisplayPoint::new(0, 0) -// ); -// assert_eq!( -// map.clip_point(DisplayPoint::new(0, "✅".len() as u32 - 1), Bias::Right), -// DisplayPoint::new(0, "✅".len() as u32) -// ); -// } - -// #[gpui::test] -// fn test_max_point(cx: &mut gpui::AppContext) { -// init_test(cx, |_| {}); - -// let buffer = MultiBuffer::build_simple("aaa\n\t\tbbb", cx); -// let font_cache = cx.font_cache(); -// let family_id = font_cache -// .load_family(&["Helvetica"], &Default::default()) -// .unwrap(); -// let font_id = font_cache -// .select_font(family_id, &Default::default()) -// .unwrap(); -// let font_size = 14.0; -// let map = -// cx.add_model(|cx| DisplayMap::new(buffer.clone(), font_id, font_size, None, 1, 1, cx)); -// assert_eq!( -// map.update(cx, |map, cx| map.snapshot(cx)).max_point(), -// DisplayPoint::new(1, 11) -// ) -// } - -// fn syntax_chunks<'a>( -// rows: Range, -// map: &Model, -// theme: &'a SyntaxTheme, -// cx: &mut AppContext, -// ) -> Vec<(String, Option)> { -// chunks(rows, map, theme, cx) -// .into_iter() -// .map(|(text, color, _)| (text, color)) -// .collect() -// } - -// fn chunks<'a>( -// rows: Range, -// map: &Model, -// theme: &'a SyntaxTheme, -// cx: &mut AppContext, -// ) -> Vec<(String, Option, Option)> { -// let snapshot = map.update(cx, |map, cx| map.snapshot(cx)); -// let mut chunks: Vec<(String, Option, Option)> = Vec::new(); -// for chunk in snapshot.chunks(rows, true, None, None) { -// let syntax_color = chunk -// .syntax_highlight_id -// .and_then(|id| id.style(theme)?.color); -// let highlight_color = chunk.highlight_style.and_then(|style| style.color); -// if let Some((last_chunk, last_syntax_color, last_highlight_color)) = chunks.last_mut() { -// if syntax_color == *last_syntax_color && highlight_color == *last_highlight_color { -// last_chunk.push_str(chunk.text); -// continue; -// } -// } -// chunks.push((chunk.text.to_string(), syntax_color, highlight_color)); -// } -// chunks -// } - -// fn init_test(cx: &mut AppContext, f: impl Fn(&mut AllLanguageSettingsContent)) { -// cx.foreground().forbid_parking(); -// cx.set_global(SettingsStore::test(cx)); -// language::init(cx); -// crate::init(cx); -// Project::init_settings(cx); -// theme::init((), cx); -// cx.update_global::(|store, cx| { -// store.update_user_settings::(cx, f); -// }); -// } -// } +#[cfg(test)] +pub mod tests { + use super::*; + use crate::{ + movement, + test::{editor_test_context::EditorTestContext, marked_display_snapshot}, + }; + use gpui::{div, font, observe, px, AppContext, Context, Element, Hsla}; + use language::{ + language_settings::{AllLanguageSettings, AllLanguageSettingsContent}, + Buffer, Language, LanguageConfig, SelectionGoal, + }; + use project::Project; + use rand::{prelude::*, Rng}; + use settings::SettingsStore; + use smol::stream::StreamExt; + use std::{env, sync::Arc}; + use theme::{LoadThemes, SyntaxTheme}; + use util::test::{marked_text_ranges, sample_text}; + use Bias::*; + + #[gpui::test(iterations = 100)] + async fn test_random_display_map(cx: &mut gpui::TestAppContext, mut rng: StdRng) { + cx.background_executor.set_block_on_ticks(0..=50); + let operations = env::var("OPERATIONS") + .map(|i| i.parse().expect("invalid `OPERATIONS` variable")) + .unwrap_or(10); + + let test_platform = &cx.test_platform; + let mut tab_size = rng.gen_range(1..=4); + let buffer_start_excerpt_header_height = rng.gen_range(1..=5); + let excerpt_header_height = rng.gen_range(1..=5); + let font_size = px(14.0); + let max_wrap_width = 300.0; + let mut wrap_width = if rng.gen_bool(0.1) { + None + } else { + Some(px(rng.gen_range(0.0..=max_wrap_width))) + }; + + log::info!("tab size: {}", tab_size); + log::info!("wrap width: {:?}", wrap_width); + + cx.update(|cx| { + init_test(cx, |s| s.defaults.tab_size = NonZeroU32::new(tab_size)); + }); + + let buffer = cx.update(|cx| { + if rng.gen() { + let len = rng.gen_range(0..10); + let text = util::RandomCharIter::new(&mut rng) + .take(len) + .collect::(); + MultiBuffer::build_simple(&text, cx) + } else { + MultiBuffer::build_random(&mut rng, cx) + } + }); + + let map = cx.build_model(|cx| { + DisplayMap::new( + buffer.clone(), + font("Helvetica"), + font_size, + wrap_width, + buffer_start_excerpt_header_height, + excerpt_header_height, + cx, + ) + }); + let mut notifications = observe(&map, cx); + let mut fold_count = 0; + let mut blocks = Vec::new(); + + let snapshot = map.update(cx, |map, cx| map.snapshot(cx)); + log::info!("buffer text: {:?}", snapshot.buffer_snapshot.text()); + log::info!("fold text: {:?}", snapshot.fold_snapshot.text()); + log::info!("tab text: {:?}", snapshot.tab_snapshot.text()); + log::info!("wrap text: {:?}", snapshot.wrap_snapshot.text()); + log::info!("block text: {:?}", snapshot.block_snapshot.text()); + log::info!("display text: {:?}", snapshot.text()); + + for _i in 0..operations { + match rng.gen_range(0..100) { + 0..=19 => { + wrap_width = if rng.gen_bool(0.2) { + None + } else { + Some(px(rng.gen_range(0.0..=max_wrap_width))) + }; + log::info!("setting wrap width to {:?}", wrap_width); + map.update(cx, |map, cx| map.set_wrap_width(wrap_width, cx)); + } + 20..=29 => { + let mut tab_sizes = vec![1, 2, 3, 4]; + tab_sizes.remove((tab_size - 1) as usize); + tab_size = *tab_sizes.choose(&mut rng).unwrap(); + log::info!("setting tab size to {:?}", tab_size); + cx.update(|cx| { + cx.update_global::(|store, cx| { + store.update_user_settings::(cx, |s| { + s.defaults.tab_size = NonZeroU32::new(tab_size); + }); + }); + }); + } + 30..=44 => { + map.update(cx, |map, cx| { + if rng.gen() || blocks.is_empty() { + let buffer = map.snapshot(cx).buffer_snapshot; + let block_properties = (0..rng.gen_range(1..=1)) + .map(|_| { + 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::>(); + blocks.extend(map.insert_blocks(block_properties, cx)); + } else { + blocks.shuffle(&mut rng); + let remove_count = rng.gen_range(1..=4.min(blocks.len())); + let block_ids_to_remove = (0..remove_count) + .map(|_| blocks.remove(rng.gen_range(0..blocks.len()))) + .collect(); + log::info!("removing block ids {:?}", block_ids_to_remove); + map.remove_blocks(block_ids_to_remove, cx); + } + }); + } + 45..=79 => { + let mut ranges = Vec::new(); + for _ in 0..rng.gen_range(1..=3) { + buffer.read_with(cx, |buffer, cx| { + let buffer = buffer.read(cx); + let end = buffer.clip_offset(rng.gen_range(0..=buffer.len()), Right); + let start = buffer.clip_offset(rng.gen_range(0..=end), Left); + ranges.push(start..end); + }); + } + + if rng.gen() && fold_count > 0 { + log::info!("unfolding ranges: {:?}", ranges); + map.update(cx, |map, cx| { + map.unfold(ranges, true, cx); + }); + } else { + log::info!("folding ranges: {:?}", ranges); + map.update(cx, |map, cx| { + map.fold(ranges, cx); + }); + } + } + _ => { + buffer.update(cx, |buffer, cx| buffer.randomly_mutate(&mut rng, 5, cx)); + } + } + + if map.read_with(cx, |map, cx| map.is_rewrapping(cx)) { + notifications.next().await.unwrap(); + } + + let snapshot = map.update(cx, |map, cx| map.snapshot(cx)); + fold_count = snapshot.fold_count(); + log::info!("buffer text: {:?}", snapshot.buffer_snapshot.text()); + log::info!("fold text: {:?}", snapshot.fold_snapshot.text()); + log::info!("tab text: {:?}", snapshot.tab_snapshot.text()); + log::info!("wrap text: {:?}", snapshot.wrap_snapshot.text()); + log::info!("block text: {:?}", snapshot.block_snapshot.text()); + log::info!("display text: {:?}", snapshot.text()); + + // Line boundaries + let buffer = &snapshot.buffer_snapshot; + for _ in 0..5 { + let row = rng.gen_range(0..=buffer.max_point().row); + let column = rng.gen_range(0..=buffer.line_len(row)); + let point = buffer.clip_point(Point::new(row, column), Left); + + let (prev_buffer_bound, prev_display_bound) = snapshot.prev_line_boundary(point); + let (next_buffer_bound, next_display_bound) = snapshot.next_line_boundary(point); + + assert!(prev_buffer_bound <= point); + assert!(next_buffer_bound >= point); + assert_eq!(prev_buffer_bound.column, 0); + assert_eq!(prev_display_bound.column(), 0); + if next_buffer_bound < buffer.max_point() { + assert_eq!(buffer.chars_at(next_buffer_bound).next(), Some('\n')); + } + + assert_eq!( + prev_display_bound, + prev_buffer_bound.to_display_point(&snapshot), + "row boundary before {:?}. reported buffer row boundary: {:?}", + point, + prev_buffer_bound + ); + assert_eq!( + next_display_bound, + next_buffer_bound.to_display_point(&snapshot), + "display row boundary after {:?}. reported buffer row boundary: {:?}", + point, + next_buffer_bound + ); + assert_eq!( + prev_buffer_bound, + prev_display_bound.to_point(&snapshot), + "row boundary before {:?}. reported display row boundary: {:?}", + point, + prev_display_bound + ); + assert_eq!( + next_buffer_bound, + next_display_bound.to_point(&snapshot), + "row boundary after {:?}. reported display row boundary: {:?}", + point, + next_display_bound + ); + } + + // Movement + let min_point = snapshot.clip_point(DisplayPoint::new(0, 0), Left); + let max_point = snapshot.clip_point(snapshot.max_point(), Right); + for _ in 0..5 { + let row = rng.gen_range(0..=snapshot.max_point().row()); + let column = rng.gen_range(0..=snapshot.line_len(row)); + let point = snapshot.clip_point(DisplayPoint::new(row, column), Left); + + log::info!("Moving from point {:?}", point); + + let moved_right = movement::right(&snapshot, point); + log::info!("Right {:?}", moved_right); + if point < max_point { + assert!(moved_right > point); + if point.column() == snapshot.line_len(point.row()) + || snapshot.soft_wrap_indent(point.row()).is_some() + && point.column() == snapshot.line_len(point.row()) - 1 + { + assert!(moved_right.row() > point.row()); + } + } else { + assert_eq!(moved_right, point); + } + + let moved_left = movement::left(&snapshot, point); + log::info!("Left {:?}", moved_left); + if point > min_point { + assert!(moved_left < point); + if point.column() == 0 { + assert!(moved_left.row() < point.row()); + } + } else { + assert_eq!(moved_left, point); + } + } + } + } + + #[gpui::test(retries = 5)] + async fn test_soft_wraps(cx: &mut gpui::TestAppContext) { + cx.background_executor + .set_block_on_ticks(usize::MAX..=usize::MAX); + cx.update(|cx| { + init_test(cx, |_| {}); + }); + + let mut cx = EditorTestContext::new(cx).await; + let editor = cx.editor.clone(); + let window = cx.window.clone(); + + cx.update_window(window, |_, cx| { + let text_layout_details = + editor.update(cx, |editor, cx| editor.text_layout_details(cx)); + + let font_size = px(12.0); + let wrap_width = Some(px(64.)); + + let text = "one two three four five\nsix seven eight"; + let buffer = MultiBuffer::build_simple(text, cx); + let map = cx.build_model(|cx| { + DisplayMap::new( + buffer.clone(), + font("Helvetica"), + font_size, + wrap_width, + 1, + 1, + cx, + ) + }); + + let snapshot = map.update(cx, |map, cx| map.snapshot(cx)); + assert_eq!( + snapshot.text_chunks(0).collect::(), + "one two \nthree four \nfive\nsix seven \neight" + ); + assert_eq!( + snapshot.clip_point(DisplayPoint::new(0, 8), Bias::Left), + DisplayPoint::new(0, 7) + ); + assert_eq!( + snapshot.clip_point(DisplayPoint::new(0, 8), Bias::Right), + DisplayPoint::new(1, 0) + ); + assert_eq!( + movement::right(&snapshot, DisplayPoint::new(0, 7)), + DisplayPoint::new(1, 0) + ); + assert_eq!( + movement::left(&snapshot, DisplayPoint::new(1, 0)), + DisplayPoint::new(0, 7) + ); + + let x = snapshot.x_for_display_point(DisplayPoint::new(1, 10), &text_layout_details); + assert_eq!( + movement::up( + &snapshot, + DisplayPoint::new(1, 10), + SelectionGoal::None, + false, + &text_layout_details, + ), + ( + DisplayPoint::new(0, 7), + SelectionGoal::HorizontalPosition(x.0) + ) + ); + assert_eq!( + movement::down( + &snapshot, + DisplayPoint::new(0, 7), + SelectionGoal::HorizontalPosition(x.0), + false, + &text_layout_details + ), + ( + DisplayPoint::new(1, 10), + SelectionGoal::HorizontalPosition(x.0) + ) + ); + assert_eq!( + movement::down( + &snapshot, + DisplayPoint::new(1, 10), + SelectionGoal::HorizontalPosition(x.0), + false, + &text_layout_details + ), + ( + DisplayPoint::new(2, 4), + SelectionGoal::HorizontalPosition(x.0) + ) + ); + + let ix = snapshot.buffer_snapshot.text().find("seven").unwrap(); + buffer.update(cx, |buffer, cx| { + buffer.edit([(ix..ix, "and ")], None, cx); + }); + + let snapshot = map.update(cx, |map, cx| map.snapshot(cx)); + assert_eq!( + snapshot.text_chunks(1).collect::(), + "three four \nfive\nsix and \nseven eight" + ); + + // Re-wrap on font size changes + map.update(cx, |map, cx| { + map.set_font(font("Helvetica"), px(font_size.0 + 3.), cx) + }); + + let snapshot = map.update(cx, |map, cx| map.snapshot(cx)); + assert_eq!( + snapshot.text_chunks(1).collect::(), + "three \nfour five\nsix and \nseven \neight" + ) + }); + } + + #[gpui::test] + fn test_text_chunks(cx: &mut gpui::AppContext) { + init_test(cx, |_| {}); + + let text = sample_text(6, 6, 'a'); + let buffer = MultiBuffer::build_simple(&text, cx); + + let font_size = px(14.0); + let map = cx.build_model(|cx| { + DisplayMap::new(buffer.clone(), font("Helvetica"), font_size, None, 1, 1, cx) + }); + + buffer.update(cx, |buffer, cx| { + buffer.edit( + vec![ + (Point::new(1, 0)..Point::new(1, 0), "\t"), + (Point::new(1, 1)..Point::new(1, 1), "\t"), + (Point::new(2, 1)..Point::new(2, 1), "\t"), + ], + None, + cx, + ) + }); + + assert_eq!( + map.update(cx, |map, cx| map.snapshot(cx)) + .text_chunks(1) + .collect::() + .lines() + .next(), + Some(" b bbbbb") + ); + assert_eq!( + map.update(cx, |map, cx| map.snapshot(cx)) + .text_chunks(2) + .collect::() + .lines() + .next(), + Some("c ccccc") + ); + } + + #[gpui::test] + async fn test_chunks(cx: &mut gpui::TestAppContext) { + use unindent::Unindent as _; + + let text = r#" + fn outer() {} + + mod module { + fn inner() {} + }"# + .unindent(); + + let theme = SyntaxTheme::new_test(vec![ + ("mod.body", Hsla::red().into()), + ("fn.name", Hsla::blue().into()), + ]); + let language = Arc::new( + Language::new( + LanguageConfig { + name: "Test".into(), + path_suffixes: vec![".test".to_string()], + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ) + .with_highlights_query( + r#" + (mod_item name: (identifier) body: _ @mod.body) + (function_item name: (identifier) @fn.name) + "#, + ) + .unwrap(), + ); + language.set_theme(&theme); + + cx.update(|cx| init_test(cx, |s| s.defaults.tab_size = Some(2.try_into().unwrap()))); + + let buffer = cx.build_model(|cx| { + Buffer::new(0, cx.entity_id().as_u64(), text).with_language(language, cx) + }); + cx.condition(&buffer, |buf, _| !buf.is_parsing()).await; + let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx)); + + let font_size = px(14.0); + + let map = cx.build_model(|cx| { + DisplayMap::new(buffer, font("Helvetica"), font_size, None, 1, 1, cx) + }); + assert_eq!( + cx.update(|cx| syntax_chunks(0..5, &map, &theme, cx)), + vec![ + ("fn ".to_string(), None), + ("outer".to_string(), Some(Hsla::blue())), + ("() {}\n\nmod module ".to_string(), None), + ("{\n fn ".to_string(), Some(Hsla::red())), + ("inner".to_string(), Some(Hsla::blue())), + ("() {}\n}".to_string(), Some(Hsla::red())), + ] + ); + assert_eq!( + cx.update(|cx| syntax_chunks(3..5, &map, &theme, cx)), + vec![ + (" fn ".to_string(), Some(Hsla::red())), + ("inner".to_string(), Some(Hsla::blue())), + ("() {}\n}".to_string(), Some(Hsla::red())), + ] + ); + + map.update(cx, |map, cx| { + map.fold(vec![Point::new(0, 6)..Point::new(3, 2)], cx) + }); + assert_eq!( + cx.update(|cx| syntax_chunks(0..2, &map, &theme, cx)), + vec![ + ("fn ".to_string(), None), + ("out".to_string(), Some(Hsla::blue())), + ("⋯".to_string(), None), + (" fn ".to_string(), Some(Hsla::red())), + ("inner".to_string(), Some(Hsla::blue())), + ("() {}\n}".to_string(), Some(Hsla::red())), + ] + ); + } + + #[gpui::test] + async fn test_chunks_with_soft_wrapping(cx: &mut gpui::TestAppContext) { + use unindent::Unindent as _; + + cx.background_executor + .set_block_on_ticks(usize::MAX..=usize::MAX); + + let text = r#" + fn outer() {} + + mod module { + fn inner() {} + }"# + .unindent(); + + let theme = SyntaxTheme::new_test(vec![ + ("mod.body", Hsla::red().into()), + ("fn.name", Hsla::blue().into()), + ]); + let language = Arc::new( + Language::new( + LanguageConfig { + name: "Test".into(), + path_suffixes: vec![".test".to_string()], + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ) + .with_highlights_query( + r#" + (mod_item name: (identifier) body: _ @mod.body) + (function_item name: (identifier) @fn.name) + "#, + ) + .unwrap(), + ); + language.set_theme(&theme); + + cx.update(|cx| init_test(cx, |_| {})); + + let buffer = cx.build_model(|cx| { + Buffer::new(0, cx.entity_id().as_u64(), text).with_language(language, cx) + }); + cx.condition(&buffer, |buf, _| !buf.is_parsing()).await; + let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx)); + + let font_size = px(16.0); + + let map = cx.build_model(|cx| { + DisplayMap::new(buffer, font("Courier"), font_size, Some(px(40.0)), 1, 1, cx) + }); + assert_eq!( + cx.update(|cx| syntax_chunks(0..5, &map, &theme, cx)), + [ + ("fn \n".to_string(), None), + ("oute\nr".to_string(), Some(Hsla::blue())), + ("() \n{}\n\n".to_string(), None), + ] + ); + assert_eq!( + cx.update(|cx| syntax_chunks(3..5, &map, &theme, cx)), + [("{}\n\n".to_string(), None)] + ); + + map.update(cx, |map, cx| { + map.fold(vec![Point::new(0, 6)..Point::new(3, 2)], cx) + }); + assert_eq!( + cx.update(|cx| syntax_chunks(1..4, &map, &theme, cx)), + [ + ("out".to_string(), Some(Hsla::blue())), + ("⋯\n".to_string(), None), + (" \nfn ".to_string(), Some(Hsla::red())), + ("i\n".to_string(), Some(Hsla::blue())) + ] + ); + } + + #[gpui::test] + async fn test_chunks_with_text_highlights(cx: &mut gpui::TestAppContext) { + cx.update(|cx| init_test(cx, |_| {})); + + let theme = SyntaxTheme::new_test(vec![ + ("operator", Hsla::red().into()), + ("string", Hsla::green().into()), + ]); + let language = Arc::new( + Language::new( + LanguageConfig { + name: "Test".into(), + path_suffixes: vec![".test".to_string()], + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ) + .with_highlights_query( + r#" + ":" @operator + (string_literal) @string + "#, + ) + .unwrap(), + ); + language.set_theme(&theme); + + let (text, highlighted_ranges) = marked_text_ranges(r#"constˇ «a»: B = "c «d»""#, false); + + let buffer = cx.build_model(|cx| { + Buffer::new(0, cx.entity_id().as_u64(), text).with_language(language, cx) + }); + cx.condition(&buffer, |buf, _| !buf.is_parsing()).await; + + let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx)); + let buffer_snapshot = buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx)); + + let font_size = px(16.0); + let map = cx + .build_model(|cx| DisplayMap::new(buffer, font("Courier"), font_size, None, 1, 1, cx)); + + enum MyType {} + + let style = HighlightStyle { + color: Some(Hsla::blue()), + ..Default::default() + }; + + map.update(cx, |map, _cx| { + map.highlight_text( + TypeId::of::(), + highlighted_ranges + .into_iter() + .map(|range| { + buffer_snapshot.anchor_before(range.start) + ..buffer_snapshot.anchor_before(range.end) + }) + .collect(), + style, + ); + }); + + assert_eq!( + cx.update(|cx| chunks(0..10, &map, &theme, cx)), + [ + ("const ".to_string(), None, None), + ("a".to_string(), None, Some(Hsla::blue())), + (":".to_string(), Some(Hsla::red()), None), + (" B = ".to_string(), None, None), + ("\"c ".to_string(), Some(Hsla::green()), None), + ("d".to_string(), Some(Hsla::green()), Some(Hsla::blue())), + ("\"".to_string(), Some(Hsla::green()), None), + ] + ); + } + + #[gpui::test] + fn test_clip_point(cx: &mut gpui::AppContext) { + init_test(cx, |_| {}); + + fn assert(text: &str, shift_right: bool, bias: Bias, cx: &mut gpui::AppContext) { + let (unmarked_snapshot, mut markers) = marked_display_snapshot(text, cx); + + match bias { + Bias::Left => { + if shift_right { + *markers[1].column_mut() += 1; + } + + assert_eq!(unmarked_snapshot.clip_point(markers[1], bias), markers[0]) + } + Bias::Right => { + if shift_right { + *markers[0].column_mut() += 1; + } + + assert_eq!(unmarked_snapshot.clip_point(markers[0], bias), markers[1]) + } + }; + } + + use Bias::{Left, Right}; + assert("ˇˇα", false, Left, cx); + assert("ˇˇα", true, Left, cx); + assert("ˇˇα", false, Right, cx); + assert("ˇαˇ", true, Right, cx); + assert("ˇˇ✋", false, Left, cx); + assert("ˇˇ✋", true, Left, cx); + assert("ˇˇ✋", false, Right, cx); + assert("ˇ✋ˇ", true, Right, cx); + assert("ˇˇ🍐", false, Left, cx); + assert("ˇˇ🍐", true, Left, cx); + assert("ˇˇ🍐", false, Right, cx); + assert("ˇ🍐ˇ", true, Right, cx); + assert("ˇˇ\t", false, Left, cx); + assert("ˇˇ\t", true, Left, cx); + assert("ˇˇ\t", false, Right, cx); + assert("ˇ\tˇ", true, Right, cx); + assert(" ˇˇ\t", false, Left, cx); + assert(" ˇˇ\t", true, Left, cx); + assert(" ˇˇ\t", false, Right, cx); + assert(" ˇ\tˇ", true, Right, cx); + assert(" ˇˇ\t", false, Left, cx); + assert(" ˇˇ\t", false, Right, cx); + } + + #[gpui::test] + fn test_clip_at_line_ends(cx: &mut gpui::AppContext) { + init_test(cx, |_| {}); + + fn assert(text: &str, cx: &mut gpui::AppContext) { + let (mut unmarked_snapshot, markers) = marked_display_snapshot(text, cx); + unmarked_snapshot.clip_at_line_ends = true; + assert_eq!( + unmarked_snapshot.clip_point(markers[1], Bias::Left), + markers[0] + ); + } + + assert("ˇˇ", cx); + assert("ˇaˇ", cx); + assert("aˇbˇ", cx); + assert("aˇαˇ", cx); + } + + #[gpui::test] + fn test_tabs_with_multibyte_chars(cx: &mut gpui::AppContext) { + init_test(cx, |_| {}); + + let text = "✅\t\tα\nβ\t\n🏀β\t\tγ"; + let buffer = MultiBuffer::build_simple(text, cx); + let font_size = px(14.0); + + let map = cx.build_model(|cx| { + DisplayMap::new(buffer.clone(), font("Helvetica"), font_size, None, 1, 1, cx) + }); + let map = map.update(cx, |map, cx| map.snapshot(cx)); + assert_eq!(map.text(), "✅ α\nβ \n🏀β γ"); + assert_eq!( + map.text_chunks(0).collect::(), + "✅ α\nβ \n🏀β γ" + ); + assert_eq!(map.text_chunks(1).collect::(), "β \n🏀β γ"); + assert_eq!(map.text_chunks(2).collect::(), "🏀β γ"); + + let point = Point::new(0, "✅\t\t".len() as u32); + let display_point = DisplayPoint::new(0, "✅ ".len() as u32); + assert_eq!(point.to_display_point(&map), display_point); + assert_eq!(display_point.to_point(&map), point); + + let point = Point::new(1, "β\t".len() as u32); + let display_point = DisplayPoint::new(1, "β ".len() as u32); + assert_eq!(point.to_display_point(&map), display_point); + assert_eq!(display_point.to_point(&map), point,); + + let point = Point::new(2, "🏀β\t\t".len() as u32); + let display_point = DisplayPoint::new(2, "🏀β ".len() as u32); + assert_eq!(point.to_display_point(&map), display_point); + assert_eq!(display_point.to_point(&map), point,); + + // Display points inside of expanded tabs + assert_eq!( + DisplayPoint::new(0, "✅ ".len() as u32).to_point(&map), + Point::new(0, "✅\t".len() as u32), + ); + assert_eq!( + DisplayPoint::new(0, "✅ ".len() as u32).to_point(&map), + Point::new(0, "✅".len() as u32), + ); + + // Clipping display points inside of multi-byte characters + assert_eq!( + map.clip_point(DisplayPoint::new(0, "✅".len() as u32 - 1), Left), + DisplayPoint::new(0, 0) + ); + assert_eq!( + map.clip_point(DisplayPoint::new(0, "✅".len() as u32 - 1), Bias::Right), + DisplayPoint::new(0, "✅".len() as u32) + ); + } + + #[gpui::test] + fn test_max_point(cx: &mut gpui::AppContext) { + init_test(cx, |_| {}); + + let buffer = MultiBuffer::build_simple("aaa\n\t\tbbb", cx); + let font_size = px(14.0); + let map = cx.build_model(|cx| { + DisplayMap::new(buffer.clone(), font("Helvetica"), font_size, None, 1, 1, cx) + }); + assert_eq!( + map.update(cx, |map, cx| map.snapshot(cx)).max_point(), + DisplayPoint::new(1, 11) + ) + } + + fn syntax_chunks<'a>( + rows: Range, + map: &Model, + theme: &'a SyntaxTheme, + cx: &mut AppContext, + ) -> Vec<(String, Option)> { + chunks(rows, map, theme, cx) + .into_iter() + .map(|(text, color, _)| (text, color)) + .collect() + } + + fn chunks<'a>( + rows: Range, + map: &Model, + theme: &'a SyntaxTheme, + cx: &mut AppContext, + ) -> Vec<(String, Option, Option)> { + let snapshot = map.update(cx, |map, cx| map.snapshot(cx)); + let mut chunks: Vec<(String, Option, Option)> = Vec::new(); + for chunk in snapshot.chunks(rows, true, None, None) { + let syntax_color = chunk + .syntax_highlight_id + .and_then(|id| id.style(theme)?.color); + let highlight_color = chunk.highlight_style.and_then(|style| style.color); + if let Some((last_chunk, last_syntax_color, last_highlight_color)) = chunks.last_mut() { + if syntax_color == *last_syntax_color && highlight_color == *last_highlight_color { + last_chunk.push_str(chunk.text); + continue; + } + } + chunks.push((chunk.text.to_string(), syntax_color, highlight_color)); + } + chunks + } + + fn init_test(cx: &mut AppContext, f: impl Fn(&mut AllLanguageSettingsContent)) { + let settings = SettingsStore::test(cx); + cx.set_global(settings); + language::init(cx); + crate::init(cx); + Project::init_settings(cx); + theme::init(LoadThemes::JustBase, cx); + cx.update_global::(|store, cx| { + store.update_user_settings::(cx, f); + }); + } +} From 2ab84b81da100edf919ed69236a503a75c775eed Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Fri, 1 Dec 2023 20:16:40 +0100 Subject: [PATCH 07/35] test_edit_events --- crates/editor2/src/editor_tests.rs | 205 ++++++++++++++--------------- 1 file changed, 102 insertions(+), 103 deletions(-) diff --git a/crates/editor2/src/editor_tests.rs b/crates/editor2/src/editor_tests.rs index fab223ae230586d2ced268462ff5577bebf6c2c5..adebee061c97eb872446494cc372599ec4772721 100644 --- a/crates/editor2/src/editor_tests.rs +++ b/crates/editor2/src/editor_tests.rs @@ -36,121 +36,120 @@ use workspace::{ NavigationEntry, ViewId, }; -// todo(finish edit tests) -// #[gpui::test] -// fn test_edit_events(cx: &mut TestAppContext) { -// init_test(cx, |_| {}); +#[gpui::test] +fn test_edit_events(cx: &mut TestAppContext) { + init_test(cx, |_| {}); -// let buffer = cx.build_model(|cx| { -// let mut buffer = language::Buffer::new(0, cx.entity_id().as_u64(), "123456"); -// buffer.set_group_interval(Duration::from_secs(1)); -// buffer -// }); + let buffer = cx.build_model(|cx| { + let mut buffer = language::Buffer::new(0, cx.entity_id().as_u64(), "123456"); + buffer.set_group_interval(Duration::from_secs(1)); + buffer + }); -// let events = Rc::new(RefCell::new(Vec::new())); -// let editor1 = cx.add_window({ -// let events = events.clone(); -// |cx| { -// let view = cx.view().clone(); -// cx.subscribe(&view, move |_, _, event, _| { -// if matches!(event, Event::Edited | Event::BufferEdited) { -// events.borrow_mut().push(("editor1", event.clone())); -// } -// }) -// .detach(); -// Editor::for_buffer(buffer.clone(), None, cx) -// } -// }); + let events = Rc::new(RefCell::new(Vec::new())); + let editor1 = cx.add_window({ + let events = events.clone(); + |cx| { + let view = cx.view().clone(); + cx.subscribe(&view, move |_, _, event: &EditorEvent, _| { + if matches!(event, EditorEvent::Edited | EditorEvent::BufferEdited) { + events.borrow_mut().push(("editor1", event.clone())); + } + }) + .detach(); + Editor::for_buffer(buffer.clone(), None, cx) + } + }); -// let editor2 = cx.add_window({ -// let events = events.clone(); -// |cx| { -// cx.subscribe(&cx.view().clone(), move |_, _, event, _| { -// if matches!(event, Event::Edited | Event::BufferEdited) { -// events.borrow_mut().push(("editor2", event.clone())); -// } -// }) -// .detach(); -// Editor::for_buffer(buffer.clone(), None, cx) -// } -// }); + let editor2 = cx.add_window({ + let events = events.clone(); + |cx| { + cx.subscribe(&cx.view().clone(), move |_, _, event: &EditorEvent, _| { + if matches!(event, EditorEvent::Edited | EditorEvent::BufferEdited) { + events.borrow_mut().push(("editor2", event.clone())); + } + }) + .detach(); + Editor::for_buffer(buffer.clone(), None, cx) + } + }); -// assert_eq!(mem::take(&mut *events.borrow_mut()), []); + assert_eq!(mem::take(&mut *events.borrow_mut()), []); -// // Mutating editor 1 will emit an `Edited` event only for that editor. -// editor1.update(cx, |editor, cx| editor.insert("X", cx)); -// assert_eq!( -// mem::take(&mut *events.borrow_mut()), -// [ -// ("editor1", Event::Edited), -// ("editor1", Event::BufferEdited), -// ("editor2", Event::BufferEdited), -// ] -// ); + // Mutating editor 1 will emit an `Edited` event only for that editor. + editor1.update(cx, |editor, cx| editor.insert("X", cx)); + assert_eq!( + mem::take(&mut *events.borrow_mut()), + [ + ("editor1", EditorEvent::Edited), + ("editor1", EditorEvent::BufferEdited), + ("editor2", EditorEvent::BufferEdited), + ] + ); -// // Mutating editor 2 will emit an `Edited` event only for that editor. -// editor2.update(cx, |editor, cx| editor.delete(&Delete, cx)); -// assert_eq!( -// mem::take(&mut *events.borrow_mut()), -// [ -// ("editor2", Event::Edited), -// ("editor1", Event::BufferEdited), -// ("editor2", Event::BufferEdited), -// ] -// ); + // Mutating editor 2 will emit an `Edited` event only for that editor. + editor2.update(cx, |editor, cx| editor.delete(&Delete, cx)); + assert_eq!( + mem::take(&mut *events.borrow_mut()), + [ + ("editor2", EditorEvent::Edited), + ("editor1", EditorEvent::BufferEdited), + ("editor2", EditorEvent::BufferEdited), + ] + ); -// // Undoing on editor 1 will emit an `Edited` event only for that editor. -// editor1.update(cx, |editor, cx| editor.undo(&Undo, cx)); -// assert_eq!( -// mem::take(&mut *events.borrow_mut()), -// [ -// ("editor1", Event::Edited), -// ("editor1", Event::BufferEdited), -// ("editor2", Event::BufferEdited), -// ] -// ); + // Undoing on editor 1 will emit an `Edited` event only for that editor. + editor1.update(cx, |editor, cx| editor.undo(&Undo, cx)); + assert_eq!( + mem::take(&mut *events.borrow_mut()), + [ + ("editor1", EditorEvent::Edited), + ("editor1", EditorEvent::BufferEdited), + ("editor2", EditorEvent::BufferEdited), + ] + ); -// // Redoing on editor 1 will emit an `Edited` event only for that editor. -// editor1.update(cx, |editor, cx| editor.redo(&Redo, cx)); -// assert_eq!( -// mem::take(&mut *events.borrow_mut()), -// [ -// ("editor1", Event::Edited), -// ("editor1", Event::BufferEdited), -// ("editor2", Event::BufferEdited), -// ] -// ); + // Redoing on editor 1 will emit an `Edited` event only for that editor. + editor1.update(cx, |editor, cx| editor.redo(&Redo, cx)); + assert_eq!( + mem::take(&mut *events.borrow_mut()), + [ + ("editor1", EditorEvent::Edited), + ("editor1", EditorEvent::BufferEdited), + ("editor2", EditorEvent::BufferEdited), + ] + ); -// // Undoing on editor 2 will emit an `Edited` event only for that editor. -// editor2.update(cx, |editor, cx| editor.undo(&Undo, cx)); -// assert_eq!( -// mem::take(&mut *events.borrow_mut()), -// [ -// ("editor2", Event::Edited), -// ("editor1", Event::BufferEdited), -// ("editor2", Event::BufferEdited), -// ] -// ); + // Undoing on editor 2 will emit an `Edited` event only for that editor. + editor2.update(cx, |editor, cx| editor.undo(&Undo, cx)); + assert_eq!( + mem::take(&mut *events.borrow_mut()), + [ + ("editor2", EditorEvent::Edited), + ("editor1", EditorEvent::BufferEdited), + ("editor2", EditorEvent::BufferEdited), + ] + ); -// // Redoing on editor 2 will emit an `Edited` event only for that editor. -// editor2.update(cx, |editor, cx| editor.redo(&Redo, cx)); -// assert_eq!( -// mem::take(&mut *events.borrow_mut()), -// [ -// ("editor2", Event::Edited), -// ("editor1", Event::BufferEdited), -// ("editor2", Event::BufferEdited), -// ] -// ); + // Redoing on editor 2 will emit an `Edited` event only for that editor. + editor2.update(cx, |editor, cx| editor.redo(&Redo, cx)); + assert_eq!( + mem::take(&mut *events.borrow_mut()), + [ + ("editor2", EditorEvent::Edited), + ("editor1", EditorEvent::BufferEdited), + ("editor2", EditorEvent::BufferEdited), + ] + ); -// // No event is emitted when the mutation is a no-op. -// editor2.update(cx, |editor, cx| { -// editor.change_selections(None, cx, |s| s.select_ranges([0..0])); + // No event is emitted when the mutation is a no-op. + editor2.update(cx, |editor, cx| { + editor.change_selections(None, cx, |s| s.select_ranges([0..0])); -// editor.backspace(&Backspace, cx); -// }); -// assert_eq!(mem::take(&mut *events.borrow_mut()), []); -// } + editor.backspace(&Backspace, cx); + }); + assert_eq!(mem::take(&mut *events.borrow_mut()), []); +} #[gpui::test] fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) { From 9408eecb6eebd2dfe016c23ab6cba2b371258d60 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Fri, 1 Dec 2023 20:17:41 +0100 Subject: [PATCH 08/35] test_navigation_history (pass) --- crates/editor2/src/editor_tests.rs | 226 ++++++++++++++--------------- 1 file changed, 113 insertions(+), 113 deletions(-) diff --git a/crates/editor2/src/editor_tests.rs b/crates/editor2/src/editor_tests.rs index adebee061c97eb872446494cc372599ec4772721..ecf908924521219be1b1cdbda3a4f8f06f4ba1fd 100644 --- a/crates/editor2/src/editor_tests.rs +++ b/crates/editor2/src/editor_tests.rs @@ -514,123 +514,123 @@ fn test_clone(cx: &mut TestAppContext) { } //todo!(editor navigate) -// #[gpui::test] -// async fn test_navigation_history(cx: &mut TestAppContext) { -// init_test(cx, |_| {}); +#[gpui::test] +async fn test_navigation_history(cx: &mut TestAppContext) { + init_test(cx, |_| {}); -// use workspace::item::Item; + use workspace::item::Item; -// let fs = FakeFs::new(cx.executor()); -// let project = Project::test(fs, [], cx).await; -// let workspace = cx.add_window(|cx| Workspace::test_new(project, cx)); -// let pane = workspace -// .update(cx, |workspace, _| workspace.active_pane().clone()) -// .unwrap(); + let fs = FakeFs::new(cx.executor()); + let project = Project::test(fs, [], cx).await; + let workspace = cx.add_window(|cx| Workspace::test_new(project, cx)); + let pane = workspace + .update(cx, |workspace, _| workspace.active_pane().clone()) + .unwrap(); -// workspace.update(cx, |v, cx| { -// cx.build_view(|cx| { -// let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx); -// let mut editor = build_editor(buffer.clone(), cx); -// let handle = cx.view(); -// editor.set_nav_history(Some(pane.read(cx).nav_history_for_item(&handle))); - -// fn pop_history(editor: &mut Editor, cx: &mut WindowContext) -> Option { -// editor.nav_history.as_mut().unwrap().pop_backward(cx) -// } - -// // Move the cursor a small distance. -// // Nothing is added to the navigation history. -// editor.change_selections(None, cx, |s| { -// s.select_display_ranges([DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)]) -// }); -// editor.change_selections(None, cx, |s| { -// s.select_display_ranges([DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0)]) -// }); -// assert!(pop_history(&mut editor, cx).is_none()); - -// // Move the cursor a large distance. -// // The history can jump back to the previous position. -// editor.change_selections(None, cx, |s| { -// s.select_display_ranges([DisplayPoint::new(13, 0)..DisplayPoint::new(13, 3)]) -// }); -// let nav_entry = pop_history(&mut editor, cx).unwrap(); -// editor.navigate(nav_entry.data.unwrap(), cx); -// assert_eq!(nav_entry.item.id(), cx.entity_id()); -// assert_eq!( -// editor.selections.display_ranges(cx), -// &[DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0)] -// ); -// assert!(pop_history(&mut editor, cx).is_none()); - -// // Move the cursor a small distance via the mouse. -// // Nothing is added to the navigation history. -// editor.begin_selection(DisplayPoint::new(5, 0), false, 1, cx); -// editor.end_selection(cx); -// assert_eq!( -// editor.selections.display_ranges(cx), -// &[DisplayPoint::new(5, 0)..DisplayPoint::new(5, 0)] -// ); -// assert!(pop_history(&mut editor, cx).is_none()); - -// // Move the cursor a large distance via the mouse. -// // The history can jump back to the previous position. -// editor.begin_selection(DisplayPoint::new(15, 0), false, 1, cx); -// editor.end_selection(cx); -// assert_eq!( -// editor.selections.display_ranges(cx), -// &[DisplayPoint::new(15, 0)..DisplayPoint::new(15, 0)] -// ); -// let nav_entry = pop_history(&mut editor, cx).unwrap(); -// editor.navigate(nav_entry.data.unwrap(), cx); -// assert_eq!(nav_entry.item.id(), cx.entity_id()); -// assert_eq!( -// editor.selections.display_ranges(cx), -// &[DisplayPoint::new(5, 0)..DisplayPoint::new(5, 0)] -// ); -// assert!(pop_history(&mut editor, cx).is_none()); - -// // Set scroll position to check later -// editor.set_scroll_position(gpui::Point::::new(5.5, 5.5), cx); -// let original_scroll_position = editor.scroll_manager.anchor(); - -// // Jump to the end of the document and adjust scroll -// editor.move_to_end(&MoveToEnd, cx); -// editor.set_scroll_position(gpui::Point::::new(-2.5, -0.5), cx); -// assert_ne!(editor.scroll_manager.anchor(), original_scroll_position); - -// let nav_entry = pop_history(&mut editor, cx).unwrap(); -// editor.navigate(nav_entry.data.unwrap(), cx); -// assert_eq!(editor.scroll_manager.anchor(), original_scroll_position); - -// // Ensure we don't panic when navigation data contains invalid anchors *and* points. -// let mut invalid_anchor = editor.scroll_manager.anchor().anchor; -// invalid_anchor.text_anchor.buffer_id = Some(999); -// let invalid_point = Point::new(9999, 0); -// editor.navigate( -// Box::new(NavigationData { -// cursor_anchor: invalid_anchor, -// cursor_position: invalid_point, -// scroll_anchor: ScrollAnchor { -// anchor: invalid_anchor, -// offset: Default::default(), -// }, -// scroll_top_row: invalid_point.row, -// }), -// cx, -// ); -// assert_eq!( -// editor.selections.display_ranges(cx), -// &[editor.max_point(cx)..editor.max_point(cx)] -// ); -// assert_eq!( -// editor.scroll_position(cx), -// gpui::Point::new(0., editor.max_point(cx).row() as f32) -// ); + workspace.update(cx, |v, cx| { + cx.build_view(|cx| { + let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx); + let mut editor = build_editor(buffer.clone(), cx); + let handle = cx.view(); + editor.set_nav_history(Some(pane.read(cx).nav_history_for_item(&handle))); -// editor -// }) -// }); -// } + fn pop_history(editor: &mut Editor, cx: &mut WindowContext) -> Option { + editor.nav_history.as_mut().unwrap().pop_backward(cx) + } + + // Move the cursor a small distance. + // Nothing is added to the navigation history. + editor.change_selections(None, cx, |s| { + s.select_display_ranges([DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)]) + }); + editor.change_selections(None, cx, |s| { + s.select_display_ranges([DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0)]) + }); + assert!(pop_history(&mut editor, cx).is_none()); + + // Move the cursor a large distance. + // The history can jump back to the previous position. + editor.change_selections(None, cx, |s| { + s.select_display_ranges([DisplayPoint::new(13, 0)..DisplayPoint::new(13, 3)]) + }); + let nav_entry = pop_history(&mut editor, cx).unwrap(); + editor.navigate(nav_entry.data.unwrap(), cx); + assert_eq!(nav_entry.item.id(), cx.entity_id()); + assert_eq!( + editor.selections.display_ranges(cx), + &[DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0)] + ); + assert!(pop_history(&mut editor, cx).is_none()); + + // Move the cursor a small distance via the mouse. + // Nothing is added to the navigation history. + editor.begin_selection(DisplayPoint::new(5, 0), false, 1, cx); + editor.end_selection(cx); + assert_eq!( + editor.selections.display_ranges(cx), + &[DisplayPoint::new(5, 0)..DisplayPoint::new(5, 0)] + ); + assert!(pop_history(&mut editor, cx).is_none()); + + // Move the cursor a large distance via the mouse. + // The history can jump back to the previous position. + editor.begin_selection(DisplayPoint::new(15, 0), false, 1, cx); + editor.end_selection(cx); + assert_eq!( + editor.selections.display_ranges(cx), + &[DisplayPoint::new(15, 0)..DisplayPoint::new(15, 0)] + ); + let nav_entry = pop_history(&mut editor, cx).unwrap(); + editor.navigate(nav_entry.data.unwrap(), cx); + assert_eq!(nav_entry.item.id(), cx.entity_id()); + assert_eq!( + editor.selections.display_ranges(cx), + &[DisplayPoint::new(5, 0)..DisplayPoint::new(5, 0)] + ); + assert!(pop_history(&mut editor, cx).is_none()); + + // Set scroll position to check later + editor.set_scroll_position(gpui::Point::::new(5.5, 5.5), cx); + let original_scroll_position = editor.scroll_manager.anchor(); + + // Jump to the end of the document and adjust scroll + editor.move_to_end(&MoveToEnd, cx); + editor.set_scroll_position(gpui::Point::::new(-2.5, -0.5), cx); + assert_ne!(editor.scroll_manager.anchor(), original_scroll_position); + + let nav_entry = pop_history(&mut editor, cx).unwrap(); + editor.navigate(nav_entry.data.unwrap(), cx); + assert_eq!(editor.scroll_manager.anchor(), original_scroll_position); + + // Ensure we don't panic when navigation data contains invalid anchors *and* points. + let mut invalid_anchor = editor.scroll_manager.anchor().anchor; + invalid_anchor.text_anchor.buffer_id = Some(999); + let invalid_point = Point::new(9999, 0); + editor.navigate( + Box::new(NavigationData { + cursor_anchor: invalid_anchor, + cursor_position: invalid_point, + scroll_anchor: ScrollAnchor { + anchor: invalid_anchor, + offset: Default::default(), + }, + scroll_top_row: invalid_point.row, + }), + cx, + ); + assert_eq!( + editor.selections.display_ranges(cx), + &[editor.max_point(cx)..editor.max_point(cx)] + ); + assert_eq!( + editor.scroll_position(cx), + gpui::Point::new(0., editor.max_point(cx).row() as f32) + ); + + editor + }) + }); +} #[gpui::test] fn test_cancel(cx: &mut TestAppContext) { From 20ae58eddd863846fa7b3eb64516431c094c51c8 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Fri, 1 Dec 2023 20:23:34 +0100 Subject: [PATCH 09/35] Bunch of new tests --- crates/editor2/src/editor_tests.rs | 618 ++++++++++++++--------------- 1 file changed, 309 insertions(+), 309 deletions(-) diff --git a/crates/editor2/src/editor_tests.rs b/crates/editor2/src/editor_tests.rs index ecf908924521219be1b1cdbda3a4f8f06f4ba1fd..db3604522acb974d2b8dc171f57911be0a8d9e78 100644 --- a/crates/editor2/src/editor_tests.rs +++ b/crates/editor2/src/editor_tests.rs @@ -958,55 +958,55 @@ fn test_move_cursor_multibyte(cx: &mut TestAppContext) { } //todo!(finish editor tests) -// #[gpui::test] -// fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) { -// init_test(cx, |_| {}); +#[gpui::test] +fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) { + init_test(cx, |_| {}); -// let view = cx.add_window(|cx| { -// let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx); -// build_editor(buffer.clone(), cx) -// }); -// view.update(cx, |view, cx| { -// view.change_selections(None, cx, |s| { -// s.select_display_ranges([empty_range(0, "ⓐⓑⓒⓓⓔ".len())]); -// }); -// view.move_down(&MoveDown, cx); -// assert_eq!( -// view.selections.display_ranges(cx), -// &[empty_range(1, "abcd".len())] -// ); + let view = cx.add_window(|cx| { + let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx); + build_editor(buffer.clone(), cx) + }); + view.update(cx, |view, cx| { + view.change_selections(None, cx, |s| { + s.select_display_ranges([empty_range(0, "ⓐⓑⓒⓓⓔ".len())]); + }); + view.move_down(&MoveDown, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[empty_range(1, "abcd".len())] + ); -// view.move_down(&MoveDown, cx); -// assert_eq!( -// view.selections.display_ranges(cx), -// &[empty_range(2, "αβγ".len())] -// ); + view.move_down(&MoveDown, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[empty_range(2, "αβγ".len())] + ); -// view.move_down(&MoveDown, cx); -// assert_eq!( -// view.selections.display_ranges(cx), -// &[empty_range(3, "abcd".len())] -// ); + view.move_down(&MoveDown, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[empty_range(3, "abcd".len())] + ); -// view.move_down(&MoveDown, cx); -// assert_eq!( -// view.selections.display_ranges(cx), -// &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())] -// ); + view.move_down(&MoveDown, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())] + ); -// view.move_up(&MoveUp, cx); -// assert_eq!( -// view.selections.display_ranges(cx), -// &[empty_range(3, "abcd".len())] -// ); + view.move_up(&MoveUp, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[empty_range(3, "abcd".len())] + ); -// view.move_up(&MoveUp, cx); -// assert_eq!( -// view.selections.display_ranges(cx), -// &[empty_range(2, "αβγ".len())] -// ); -// }); -// } + view.move_up(&MoveUp, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[empty_range(2, "αβγ".len())] + ); + }); +} #[gpui::test] fn test_beginning_end_of_line(cx: &mut TestAppContext) { @@ -1224,63 +1224,63 @@ fn test_prev_next_word_boundary(cx: &mut TestAppContext) { } //todo!(finish editor tests) -// #[gpui::test] -// fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) { -// init_test(cx, |_| {}); +#[gpui::test] +fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) { + init_test(cx, |_| {}); -// let view = cx.add_window(|cx| { -// let buffer = MultiBuffer::build_simple("use one::{\n two::three::four::five\n};", cx); -// build_editor(buffer, cx) -// }); + let view = cx.add_window(|cx| { + let buffer = MultiBuffer::build_simple("use one::{\n two::three::four::five\n};", cx); + build_editor(buffer, cx) + }); -// view.update(cx, |view, cx| { -// view.set_wrap_width(Some(140.0.into()), cx); -// assert_eq!( -// view.display_text(cx), -// "use one::{\n two::three::\n four::five\n};" -// ); + view.update(cx, |view, cx| { + view.set_wrap_width(Some(140.0.into()), cx); + assert_eq!( + view.display_text(cx), + "use one::{\n two::three::\n four::five\n};" + ); -// view.change_selections(None, cx, |s| { -// s.select_display_ranges([DisplayPoint::new(1, 7)..DisplayPoint::new(1, 7)]); -// }); + view.change_selections(None, cx, |s| { + s.select_display_ranges([DisplayPoint::new(1, 7)..DisplayPoint::new(1, 7)]); + }); -// view.move_to_next_word_end(&MoveToNextWordEnd, cx); -// assert_eq!( -// view.selections.display_ranges(cx), -// &[DisplayPoint::new(1, 9)..DisplayPoint::new(1, 9)] -// ); + view.move_to_next_word_end(&MoveToNextWordEnd, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[DisplayPoint::new(1, 9)..DisplayPoint::new(1, 9)] + ); -// view.move_to_next_word_end(&MoveToNextWordEnd, cx); -// assert_eq!( -// view.selections.display_ranges(cx), -// &[DisplayPoint::new(1, 14)..DisplayPoint::new(1, 14)] -// ); + view.move_to_next_word_end(&MoveToNextWordEnd, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[DisplayPoint::new(1, 14)..DisplayPoint::new(1, 14)] + ); -// view.move_to_next_word_end(&MoveToNextWordEnd, cx); -// assert_eq!( -// view.selections.display_ranges(cx), -// &[DisplayPoint::new(2, 4)..DisplayPoint::new(2, 4)] -// ); + view.move_to_next_word_end(&MoveToNextWordEnd, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[DisplayPoint::new(2, 4)..DisplayPoint::new(2, 4)] + ); -// view.move_to_next_word_end(&MoveToNextWordEnd, cx); -// assert_eq!( -// view.selections.display_ranges(cx), -// &[DisplayPoint::new(2, 8)..DisplayPoint::new(2, 8)] -// ); + view.move_to_next_word_end(&MoveToNextWordEnd, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[DisplayPoint::new(2, 8)..DisplayPoint::new(2, 8)] + ); -// view.move_to_previous_word_start(&MoveToPreviousWordStart, cx); -// assert_eq!( -// view.selections.display_ranges(cx), -// &[DisplayPoint::new(2, 4)..DisplayPoint::new(2, 4)] -// ); + view.move_to_previous_word_start(&MoveToPreviousWordStart, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[DisplayPoint::new(2, 4)..DisplayPoint::new(2, 4)] + ); -// view.move_to_previous_word_start(&MoveToPreviousWordStart, cx); -// assert_eq!( -// view.selections.display_ranges(cx), -// &[DisplayPoint::new(1, 14)..DisplayPoint::new(1, 14)] -// ); -// }); -// } + view.move_to_previous_word_start(&MoveToPreviousWordStart, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[DisplayPoint::new(1, 14)..DisplayPoint::new(1, 14)] + ); + }); +} //todo!(simulate_resize) // #[gpui::test] @@ -2492,136 +2492,136 @@ fn test_delete_line(cx: &mut TestAppContext) { } //todo!(select_anchor_ranges) -// #[gpui::test] -// fn test_join_lines_with_single_selection(cx: &mut TestAppContext) { -// init_test(cx, |_| {}); +#[gpui::test] +fn test_join_lines_with_single_selection(cx: &mut TestAppContext) { + init_test(cx, |_| {}); -// cx.add_window(|cx| { -// let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx); -// let mut editor = build_editor(buffer.clone(), cx); -// let buffer = buffer.read(cx).as_singleton().unwrap(); + cx.add_window(|cx| { + let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx); + let mut editor = build_editor(buffer.clone(), cx); + let buffer = buffer.read(cx).as_singleton().unwrap(); -// assert_eq!( -// editor.selections.ranges::(cx), -// &[Point::new(0, 0)..Point::new(0, 0)] -// ); + assert_eq!( + editor.selections.ranges::(cx), + &[Point::new(0, 0)..Point::new(0, 0)] + ); -// // When on single line, replace newline at end by space -// editor.join_lines(&JoinLines, cx); -// assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n"); -// assert_eq!( -// editor.selections.ranges::(cx), -// &[Point::new(0, 3)..Point::new(0, 3)] -// ); + // When on single line, replace newline at end by space + editor.join_lines(&JoinLines, cx); + assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n"); + assert_eq!( + editor.selections.ranges::(cx), + &[Point::new(0, 3)..Point::new(0, 3)] + ); -// // When multiple lines are selected, remove newlines that are spanned by the selection -// editor.change_selections(None, cx, |s| { -// s.select_ranges([Point::new(0, 5)..Point::new(2, 2)]) -// }); -// editor.join_lines(&JoinLines, cx); -// assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n"); -// assert_eq!( -// editor.selections.ranges::(cx), -// &[Point::new(0, 11)..Point::new(0, 11)] -// ); + // When multiple lines are selected, remove newlines that are spanned by the selection + editor.change_selections(None, cx, |s| { + s.select_ranges([Point::new(0, 5)..Point::new(2, 2)]) + }); + editor.join_lines(&JoinLines, cx); + assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n"); + assert_eq!( + editor.selections.ranges::(cx), + &[Point::new(0, 11)..Point::new(0, 11)] + ); -// // Undo should be transactional -// editor.undo(&Undo, cx); -// assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n"); -// assert_eq!( -// editor.selections.ranges::(cx), -// &[Point::new(0, 5)..Point::new(2, 2)] -// ); + // Undo should be transactional + editor.undo(&Undo, cx); + assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n"); + assert_eq!( + editor.selections.ranges::(cx), + &[Point::new(0, 5)..Point::new(2, 2)] + ); -// // When joining an empty line don't insert a space -// editor.change_selections(None, cx, |s| { -// s.select_ranges([Point::new(2, 1)..Point::new(2, 2)]) -// }); -// editor.join_lines(&JoinLines, cx); -// assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n"); -// assert_eq!( -// editor.selections.ranges::(cx), -// [Point::new(2, 3)..Point::new(2, 3)] -// ); + // When joining an empty line don't insert a space + editor.change_selections(None, cx, |s| { + s.select_ranges([Point::new(2, 1)..Point::new(2, 2)]) + }); + editor.join_lines(&JoinLines, cx); + assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n"); + assert_eq!( + editor.selections.ranges::(cx), + [Point::new(2, 3)..Point::new(2, 3)] + ); -// // We can remove trailing newlines -// editor.join_lines(&JoinLines, cx); -// assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd"); -// assert_eq!( -// editor.selections.ranges::(cx), -// [Point::new(2, 3)..Point::new(2, 3)] -// ); + // We can remove trailing newlines + editor.join_lines(&JoinLines, cx); + assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd"); + assert_eq!( + editor.selections.ranges::(cx), + [Point::new(2, 3)..Point::new(2, 3)] + ); -// // We don't blow up on the last line -// editor.join_lines(&JoinLines, cx); -// assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd"); -// assert_eq!( -// editor.selections.ranges::(cx), -// [Point::new(2, 3)..Point::new(2, 3)] -// ); + // We don't blow up on the last line + editor.join_lines(&JoinLines, cx); + assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd"); + assert_eq!( + editor.selections.ranges::(cx), + [Point::new(2, 3)..Point::new(2, 3)] + ); -// // reset to test indentation -// editor.buffer.update(cx, |buffer, cx| { -// buffer.edit( -// [ -// (Point::new(1, 0)..Point::new(1, 2), " "), -// (Point::new(2, 0)..Point::new(2, 3), " \n\td"), -// ], -// None, -// cx, -// ) -// }); + // reset to test indentation + editor.buffer.update(cx, |buffer, cx| { + buffer.edit( + [ + (Point::new(1, 0)..Point::new(1, 2), " "), + (Point::new(2, 0)..Point::new(2, 3), " \n\td"), + ], + None, + cx, + ) + }); -// // We remove any leading spaces -// assert_eq!(buffer.read(cx).text(), "aaa bbb\n c\n \n\td"); -// editor.change_selections(None, cx, |s| { -// s.select_ranges([Point::new(0, 1)..Point::new(0, 1)]) -// }); -// editor.join_lines(&JoinLines, cx); -// assert_eq!(buffer.read(cx).text(), "aaa bbb c\n \n\td"); + // We remove any leading spaces + assert_eq!(buffer.read(cx).text(), "aaa bbb\n c\n \n\td"); + editor.change_selections(None, cx, |s| { + s.select_ranges([Point::new(0, 1)..Point::new(0, 1)]) + }); + editor.join_lines(&JoinLines, cx); + assert_eq!(buffer.read(cx).text(), "aaa bbb c\n \n\td"); -// // We don't insert a space for a line containing only spaces -// editor.join_lines(&JoinLines, cx); -// assert_eq!(buffer.read(cx).text(), "aaa bbb c\n\td"); + // We don't insert a space for a line containing only spaces + editor.join_lines(&JoinLines, cx); + assert_eq!(buffer.read(cx).text(), "aaa bbb c\n\td"); -// // We ignore any leading tabs -// editor.join_lines(&JoinLines, cx); -// assert_eq!(buffer.read(cx).text(), "aaa bbb c d"); + // We ignore any leading tabs + editor.join_lines(&JoinLines, cx); + assert_eq!(buffer.read(cx).text(), "aaa bbb c d"); -// editor -// }); -// } + editor + }); +} -// #[gpui::test] -// fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) { -// init_test(cx, |_| {}); +#[gpui::test] +fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) { + init_test(cx, |_| {}); -// cx.add_window(|cx| { -// let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx); -// let mut editor = build_editor(buffer.clone(), cx); -// let buffer = buffer.read(cx).as_singleton().unwrap(); + cx.add_window(|cx| { + let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx); + let mut editor = build_editor(buffer.clone(), cx); + let buffer = buffer.read(cx).as_singleton().unwrap(); -// editor.change_selections(None, cx, |s| { -// s.select_ranges([ -// Point::new(0, 2)..Point::new(1, 1), -// Point::new(1, 2)..Point::new(1, 2), -// Point::new(3, 1)..Point::new(3, 2), -// ]) -// }); + editor.change_selections(None, cx, |s| { + s.select_ranges([ + Point::new(0, 2)..Point::new(1, 1), + Point::new(1, 2)..Point::new(1, 2), + Point::new(3, 1)..Point::new(3, 2), + ]) + }); -// editor.join_lines(&JoinLines, cx); -// assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n"); + editor.join_lines(&JoinLines, cx); + assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n"); -// assert_eq!( -// editor.selections.ranges::(cx), -// [ -// Point::new(0, 7)..Point::new(0, 7), -// Point::new(1, 3)..Point::new(1, 3) -// ] -// ); -// editor -// }); -// } + assert_eq!( + editor.selections.ranges::(cx), + [ + Point::new(0, 7)..Point::new(0, 7), + Point::new(1, 3)..Point::new(1, 3) + ] + ); + editor + }); +} #[gpui::test] async fn test_manipulate_lines_with_single_selection(cx: &mut TestAppContext) { @@ -3237,119 +3237,119 @@ fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) { // tˇhe lazy dog"}); // } -// #[gpui::test] -// async fn test_paste_multiline(cx: &mut gpui::TestAppContext) { -// init_test(cx, |_| {}); +#[gpui::test] +async fn test_paste_multiline(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); -// let mut cx = EditorTestContext::new(cx).await; -// let language = Arc::new(Language::new( -// LanguageConfig::default(), -// Some(tree_sitter_rust::language()), -// )); -// cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx)); + let mut cx = EditorTestContext::new(cx).await; + let language = Arc::new(Language::new( + LanguageConfig::default(), + Some(tree_sitter_rust::language()), + )); + cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx)); -// // Cut an indented block, without the leading whitespace. -// cx.set_state(indoc! {" -// const a: B = ( -// c(), -// «d( -// e, -// f -// )ˇ» -// ); -// "}); -// cx.update_editor(|e, cx| e.cut(&Cut, cx)); -// cx.assert_editor_state(indoc! {" -// const a: B = ( -// c(), -// ˇ -// ); -// "}); + // Cut an indented block, without the leading whitespace. + cx.set_state(indoc! {" + const a: B = ( + c(), + «d( + e, + f + )ˇ» + ); + "}); + cx.update_editor(|e, cx| e.cut(&Cut, cx)); + cx.assert_editor_state(indoc! {" + const a: B = ( + c(), + ˇ + ); + "}); -// // Paste it at the same position. -// cx.update_editor(|e, cx| e.paste(&Paste, cx)); -// cx.assert_editor_state(indoc! {" -// const a: B = ( -// c(), -// d( -// e, -// f -// )ˇ -// ); -// "}); + // Paste it at the same position. + cx.update_editor(|e, cx| e.paste(&Paste, cx)); + cx.assert_editor_state(indoc! {" + const a: B = ( + c(), + d( + e, + f + )ˇ + ); + "}); -// // Paste it at a line with a lower indent level. -// cx.set_state(indoc! {" -// ˇ -// const a: B = ( -// c(), -// ); -// "}); -// cx.update_editor(|e, cx| e.paste(&Paste, cx)); -// cx.assert_editor_state(indoc! {" -// d( -// e, -// f -// )ˇ -// const a: B = ( -// c(), -// ); -// "}); + // Paste it at a line with a lower indent level. + cx.set_state(indoc! {" + ˇ + const a: B = ( + c(), + ); + "}); + cx.update_editor(|e, cx| e.paste(&Paste, cx)); + cx.assert_editor_state(indoc! {" + d( + e, + f + )ˇ + const a: B = ( + c(), + ); + "}); -// // Cut an indented block, with the leading whitespace. -// cx.set_state(indoc! {" -// const a: B = ( -// c(), -// « d( -// e, -// f -// ) -// ˇ»); -// "}); -// cx.update_editor(|e, cx| e.cut(&Cut, cx)); -// cx.assert_editor_state(indoc! {" -// const a: B = ( -// c(), -// ˇ); -// "}); + // Cut an indented block, with the leading whitespace. + cx.set_state(indoc! {" + const a: B = ( + c(), + « d( + e, + f + ) + ˇ»); + "}); + cx.update_editor(|e, cx| e.cut(&Cut, cx)); + cx.assert_editor_state(indoc! {" + const a: B = ( + c(), + ˇ); + "}); -// // Paste it at the same position. -// cx.update_editor(|e, cx| e.paste(&Paste, cx)); -// cx.assert_editor_state(indoc! {" -// const a: B = ( -// c(), -// d( -// e, -// f -// ) -// ˇ); -// "}); + // Paste it at the same position. + cx.update_editor(|e, cx| e.paste(&Paste, cx)); + cx.assert_editor_state(indoc! {" + const a: B = ( + c(), + d( + e, + f + ) + ˇ); + "}); -// // Paste it at a line with a higher indent level. -// cx.set_state(indoc! {" -// const a: B = ( -// c(), -// d( -// e, -// fˇ -// ) -// ); -// "}); -// cx.update_editor(|e, cx| e.paste(&Paste, cx)); -// cx.assert_editor_state(indoc! {" -// const a: B = ( -// c(), -// d( -// e, -// f d( -// e, -// f -// ) -// ˇ -// ) -// ); -// "}); -// } + // Paste it at a line with a higher indent level. + cx.set_state(indoc! {" + const a: B = ( + c(), + d( + e, + fˇ + ) + ); + "}); + cx.update_editor(|e, cx| e.paste(&Paste, cx)); + cx.assert_editor_state(indoc! {" + const a: B = ( + c(), + d( + e, + f d( + e, + f + ) + ˇ + ) + ); + "}); +} #[gpui::test] fn test_select_all(cx: &mut TestAppContext) { From a985b7aab4b4856a2c5fcbd56d3337f09acaafad Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Fri, 1 Dec 2023 20:25:47 +0100 Subject: [PATCH 10/35] test_following (passes :)) --- crates/editor2/src/editor_tests.rs | 306 ++++++++++++++--------------- 1 file changed, 153 insertions(+), 153 deletions(-) diff --git a/crates/editor2/src/editor_tests.rs b/crates/editor2/src/editor_tests.rs index db3604522acb974d2b8dc171f57911be0a8d9e78..75ff85d4aee52acb79a46185204ac358c371b3b1 100644 --- a/crates/editor2/src/editor_tests.rs +++ b/crates/editor2/src/editor_tests.rs @@ -6407,169 +6407,169 @@ async fn test_extra_newline_insertion(cx: &mut gpui::TestAppContext) { // } // todo!(following) -// #[gpui::test] -// async fn test_following(cx: &mut gpui::TestAppContext) { -// init_test(cx, |_| {}); - -// let fs = FakeFs::new(cx.executor()); -// let project = Project::test(fs, ["/file.rs".as_ref()], cx).await; +#[gpui::test] +async fn test_following(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); -// let buffer = project.update(cx, |project, cx| { -// let buffer = project -// .create_buffer(&sample_text(16, 8, 'a'), None, cx) -// .unwrap(); -// cx.build_model(|cx| MultiBuffer::singleton(buffer, cx)) -// }); -// let leader = cx.add_window(|cx| build_editor(buffer.clone(), cx)); -// let follower = cx.update(|cx| { -// cx.open_window( -// WindowOptions { -// bounds: WindowBounds::Fixed(Bounds::from_corners( -// gpui::Point::new((0. as f64).into(), (0. as f64).into()), -// gpui::Point::new((10. as f64).into(), (80. as f64).into()), -// )), -// ..Default::default() -// }, -// |cx| cx.build_view(|cx| build_editor(buffer.clone(), cx)), -// ) -// }); + let fs = FakeFs::new(cx.executor()); + let project = Project::test(fs, ["/file.rs".as_ref()], cx).await; -// let is_still_following = Rc::new(RefCell::new(true)); -// let follower_edit_event_count = Rc::new(RefCell::new(0)); -// let pending_update = Rc::new(RefCell::new(None)); -// follower.update(cx, { -// let update = pending_update.clone(); -// let is_still_following = is_still_following.clone(); -// let follower_edit_event_count = follower_edit_event_count.clone(); -// |_, cx| { -// cx.subscribe( -// &leader.root_view(cx).unwrap(), -// move |_, leader, event, cx| { -// leader -// .read(cx) -// .add_event_to_update_proto(event, &mut *update.borrow_mut(), cx); -// }, -// ) -// .detach(); + let buffer = project.update(cx, |project, cx| { + let buffer = project + .create_buffer(&sample_text(16, 8, 'a'), None, cx) + .unwrap(); + cx.build_model(|cx| MultiBuffer::singleton(buffer, cx)) + }); + let leader = cx.add_window(|cx| build_editor(buffer.clone(), cx)); + let follower = cx.update(|cx| { + cx.open_window( + WindowOptions { + bounds: WindowBounds::Fixed(Bounds::from_corners( + gpui::Point::new((0. as f64).into(), (0. as f64).into()), + gpui::Point::new((10. as f64).into(), (80. as f64).into()), + )), + ..Default::default() + }, + |cx| cx.build_view(|cx| build_editor(buffer.clone(), cx)), + ) + }); -// cx.subscribe( -// &follower.root_view(cx).unwrap(), -// move |_, _, event: &Event, cx| { -// if matches!(event.to_follow_event(), Some(FollowEvent::Unfollow)) { -// *is_still_following.borrow_mut() = false; -// } + let is_still_following = Rc::new(RefCell::new(true)); + let follower_edit_event_count = Rc::new(RefCell::new(0)); + let pending_update = Rc::new(RefCell::new(None)); + follower.update(cx, { + let update = pending_update.clone(); + let is_still_following = is_still_following.clone(); + let follower_edit_event_count = follower_edit_event_count.clone(); + |_, cx| { + cx.subscribe( + &leader.root_view(cx).unwrap(), + move |_, leader, event, cx| { + leader + .read(cx) + .add_event_to_update_proto(event, &mut *update.borrow_mut(), cx); + }, + ) + .detach(); -// if let Event::BufferEdited = event { -// *follower_edit_event_count.borrow_mut() += 1; -// } -// }, -// ) -// .detach(); -// } -// }); + cx.subscribe( + &follower.root_view(cx).unwrap(), + move |_, _, event: &EditorEvent, cx| { + if matches!(event.to_follow_event(), Some(FollowEvent::Unfollow)) { + *is_still_following.borrow_mut() = false; + } -// // Update the selections only -// leader.update(cx, |leader, cx| { -// leader.change_selections(None, cx, |s| s.select_ranges([1..1])); -// }); -// follower -// .update(cx, |follower, cx| { -// follower.apply_update_proto(&project, pending_update.borrow_mut().take().unwrap(), cx) -// }) -// .unwrap() -// .await -// .unwrap(); -// follower.update(cx, |follower, cx| { -// assert_eq!(follower.selections.ranges(cx), vec![1..1]); -// }); -// assert_eq!(*is_still_following.borrow(), true); -// assert_eq!(*follower_edit_event_count.borrow(), 0); + if let EditorEvent::BufferEdited = event { + *follower_edit_event_count.borrow_mut() += 1; + } + }, + ) + .detach(); + } + }); -// // Update the scroll position only -// leader.update(cx, |leader, cx| { -// leader.set_scroll_position(gpui::Point::new(1.5, 3.5), cx); -// }); -// follower -// .update(cx, |follower, cx| { -// follower.apply_update_proto(&project, pending_update.borrow_mut().take().unwrap(), cx) -// }) -// .unwrap() -// .await -// .unwrap(); -// assert_eq!( -// follower -// .update(cx, |follower, cx| follower.scroll_position(cx)) -// .unwrap(), -// gpui::Point::new(1.5, 3.5) -// ); -// assert_eq!(*is_still_following.borrow(), true); -// assert_eq!(*follower_edit_event_count.borrow(), 0); + // Update the selections only + leader.update(cx, |leader, cx| { + leader.change_selections(None, cx, |s| s.select_ranges([1..1])); + }); + follower + .update(cx, |follower, cx| { + follower.apply_update_proto(&project, pending_update.borrow_mut().take().unwrap(), cx) + }) + .unwrap() + .await + .unwrap(); + follower.update(cx, |follower, cx| { + assert_eq!(follower.selections.ranges(cx), vec![1..1]); + }); + assert_eq!(*is_still_following.borrow(), true); + assert_eq!(*follower_edit_event_count.borrow(), 0); -// // Update the selections and scroll position. The follower's scroll position is updated -// // via autoscroll, not via the leader's exact scroll position. -// leader.update(cx, |leader, cx| { -// leader.change_selections(None, cx, |s| s.select_ranges([0..0])); -// leader.request_autoscroll(Autoscroll::newest(), cx); -// leader.set_scroll_position(gpui::Point::new(1.5, 3.5), cx); -// }); -// follower -// .update(cx, |follower, cx| { -// follower.apply_update_proto(&project, pending_update.borrow_mut().take().unwrap(), cx) -// }) -// .unwrap() -// .await -// .unwrap(); -// follower.update(cx, |follower, cx| { -// assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0)); -// assert_eq!(follower.selections.ranges(cx), vec![0..0]); -// }); -// assert_eq!(*is_still_following.borrow(), true); + // Update the scroll position only + leader.update(cx, |leader, cx| { + leader.set_scroll_position(gpui::Point::new(1.5, 3.5), cx); + }); + follower + .update(cx, |follower, cx| { + follower.apply_update_proto(&project, pending_update.borrow_mut().take().unwrap(), cx) + }) + .unwrap() + .await + .unwrap(); + assert_eq!( + follower + .update(cx, |follower, cx| follower.scroll_position(cx)) + .unwrap(), + gpui::Point::new(1.5, 3.5) + ); + assert_eq!(*is_still_following.borrow(), true); + assert_eq!(*follower_edit_event_count.borrow(), 0); + + // Update the selections and scroll position. The follower's scroll position is updated + // via autoscroll, not via the leader's exact scroll position. + leader.update(cx, |leader, cx| { + leader.change_selections(None, cx, |s| s.select_ranges([0..0])); + leader.request_autoscroll(Autoscroll::newest(), cx); + leader.set_scroll_position(gpui::Point::new(1.5, 3.5), cx); + }); + follower + .update(cx, |follower, cx| { + follower.apply_update_proto(&project, pending_update.borrow_mut().take().unwrap(), cx) + }) + .unwrap() + .await + .unwrap(); + follower.update(cx, |follower, cx| { + assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0)); + assert_eq!(follower.selections.ranges(cx), vec![0..0]); + }); + assert_eq!(*is_still_following.borrow(), true); -// // Creating a pending selection that precedes another selection -// leader.update(cx, |leader, cx| { -// leader.change_selections(None, cx, |s| s.select_ranges([1..1])); -// leader.begin_selection(DisplayPoint::new(0, 0), true, 1, cx); -// }); -// follower -// .update(cx, |follower, cx| { -// follower.apply_update_proto(&project, pending_update.borrow_mut().take().unwrap(), cx) -// }) -// .unwrap() -// .await -// .unwrap(); -// follower.update(cx, |follower, cx| { -// assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]); -// }); -// assert_eq!(*is_still_following.borrow(), true); + // Creating a pending selection that precedes another selection + leader.update(cx, |leader, cx| { + leader.change_selections(None, cx, |s| s.select_ranges([1..1])); + leader.begin_selection(DisplayPoint::new(0, 0), true, 1, cx); + }); + follower + .update(cx, |follower, cx| { + follower.apply_update_proto(&project, pending_update.borrow_mut().take().unwrap(), cx) + }) + .unwrap() + .await + .unwrap(); + follower.update(cx, |follower, cx| { + assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]); + }); + assert_eq!(*is_still_following.borrow(), true); -// // Extend the pending selection so that it surrounds another selection -// leader.update(cx, |leader, cx| { -// leader.extend_selection(DisplayPoint::new(0, 2), 1, cx); -// }); -// follower -// .update(cx, |follower, cx| { -// follower.apply_update_proto(&project, pending_update.borrow_mut().take().unwrap(), cx) -// }) -// .unwrap() -// .await -// .unwrap(); -// follower.update(cx, |follower, cx| { -// assert_eq!(follower.selections.ranges(cx), vec![0..2]); -// }); + // Extend the pending selection so that it surrounds another selection + leader.update(cx, |leader, cx| { + leader.extend_selection(DisplayPoint::new(0, 2), 1, cx); + }); + follower + .update(cx, |follower, cx| { + follower.apply_update_proto(&project, pending_update.borrow_mut().take().unwrap(), cx) + }) + .unwrap() + .await + .unwrap(); + follower.update(cx, |follower, cx| { + assert_eq!(follower.selections.ranges(cx), vec![0..2]); + }); -// // Scrolling locally breaks the follow -// follower.update(cx, |follower, cx| { -// let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0); -// follower.set_scroll_anchor( -// ScrollAnchor { -// anchor: top_anchor, -// offset: gpui::Point::new(0.0, 0.5), -// }, -// cx, -// ); -// }); -// assert_eq!(*is_still_following.borrow(), false); -// } + // Scrolling locally breaks the follow + follower.update(cx, |follower, cx| { + let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0); + follower.set_scroll_anchor( + ScrollAnchor { + anchor: top_anchor, + offset: gpui::Point::new(0.0, 0.5), + }, + cx, + ); + }); + assert_eq!(*is_still_following.borrow(), false); +} // #[gpui::test] // async fn test_following_with_multiple_excerpts(cx: &mut gpui::TestAppContext) { From 0a1765b01fdf4d59bdad53a3e605947d397c1482 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Fri, 1 Dec 2023 20:32:12 +0100 Subject: [PATCH 11/35] test_following_with_multiple_excerpts (passes) --- crates/editor2/src/editor_tests.rs | 314 +++++++++++++++-------------- 1 file changed, 158 insertions(+), 156 deletions(-) diff --git a/crates/editor2/src/editor_tests.rs b/crates/editor2/src/editor_tests.rs index 75ff85d4aee52acb79a46185204ac358c371b3b1..87a1ba8dee66bd2f8f7a572dfb1d969933e7cf67 100644 --- a/crates/editor2/src/editor_tests.rs +++ b/crates/editor2/src/editor_tests.rs @@ -12,7 +12,7 @@ use futures::StreamExt; use gpui::{ div, serde_json::{self, json}, - Div, TestAppContext, VisualTestContext, WindowBounds, WindowOptions, + Div, Flatten, TestAppContext, VisualTestContext, WindowBounds, WindowOptions, }; use indoc::indoc; use language::{ @@ -6571,170 +6571,172 @@ async fn test_following(cx: &mut gpui::TestAppContext) { assert_eq!(*is_still_following.borrow(), false); } -// #[gpui::test] -// async fn test_following_with_multiple_excerpts(cx: &mut gpui::TestAppContext) { -// init_test(cx, |_| {}); +#[gpui::test] +async fn test_following_with_multiple_excerpts(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); -// let fs = FakeFs::new(cx.executor()); -// let project = Project::test(fs, ["/file.rs".as_ref()], cx).await; -// let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); -// let pane = workspace -// .update(cx, |workspace, _| workspace.active_pane().clone()) -// .unwrap(); + let fs = FakeFs::new(cx.executor()); + let project = Project::test(fs, ["/file.rs".as_ref()], cx).await; + let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let pane = workspace + .update(cx, |workspace, _| workspace.active_pane().clone()) + .unwrap(); -// let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx); + let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx); -// let leader = pane.update(cx, |_, cx| { -// let multibuffer = cx.build_model(|_| MultiBuffer::new(0)); -// cx.build_view(|cx| build_editor(multibuffer.clone(), cx)) -// }); + let leader = pane.update(cx, |_, cx| { + let multibuffer = cx.build_model(|_| MultiBuffer::new(0)); + cx.build_view(|cx| build_editor(multibuffer.clone(), cx)) + }); -// // Start following the editor when it has no excerpts. -// let mut state_message = leader.update(cx, |leader, cx| leader.to_state_proto(cx)); -// let follower_1 = cx -// .update(|cx| { -// Editor::from_state_proto( -// pane.clone(), -// workspace.root_view(cx).unwrap(), -// ViewId { -// creator: Default::default(), -// id: 0, -// }, -// &mut state_message, -// cx, -// ) -// }) -// .unwrap() -// .await -// .unwrap(); - -// let update_message = Rc::new(RefCell::new(None)); -// follower_1.update(cx, { -// let update = update_message.clone(); -// |_, cx| { -// cx.subscribe(&leader, move |_, leader, event, cx| { -// leader -// .read(cx) -// .add_event_to_update_proto(event, &mut *update.borrow_mut(), cx); -// }) -// .detach(); -// } -// }); + // Start following the editor when it has no excerpts. + let mut state_message = leader.update(cx, |leader, cx| leader.to_state_proto(cx)); + let follower_1 = cx + .update_window(*workspace.deref(), |_, cx| { + Editor::from_state_proto( + pane.clone(), + workspace.root_view(cx).unwrap(), + ViewId { + creator: Default::default(), + id: 0, + }, + &mut state_message, + cx, + ) + }) + .unwrap() + .unwrap() + .await + .unwrap(); -// let (buffer_1, buffer_2) = project.update(cx, |project, cx| { -// ( -// project -// .create_buffer("abc\ndef\nghi\njkl\n", None, cx) -// .unwrap(), -// project -// .create_buffer("mno\npqr\nstu\nvwx\n", None, cx) -// .unwrap(), -// ) -// }); + let update_message = Rc::new(RefCell::new(None)); + follower_1.update(cx, { + let update = update_message.clone(); + |_, cx| { + cx.subscribe(&leader, move |_, leader, event, cx| { + leader + .read(cx) + .add_event_to_update_proto(event, &mut *update.borrow_mut(), cx); + }) + .detach(); + } + }); -// // Insert some excerpts. -// leader.update(cx, |leader, cx| { -// leader.buffer.update(cx, |multibuffer, cx| { -// let excerpt_ids = multibuffer.push_excerpts( -// buffer_1.clone(), -// [ -// ExcerptRange { -// context: 1..6, -// primary: None, -// }, -// ExcerptRange { -// context: 12..15, -// primary: None, -// }, -// ExcerptRange { -// context: 0..3, -// primary: None, -// }, -// ], -// cx, -// ); -// multibuffer.insert_excerpts_after( -// excerpt_ids[0], -// buffer_2.clone(), -// [ -// ExcerptRange { -// context: 8..12, -// primary: None, -// }, -// ExcerptRange { -// context: 0..6, -// primary: None, -// }, -// ], -// cx, -// ); -// }); -// }); + let (buffer_1, buffer_2) = project.update(cx, |project, cx| { + ( + project + .create_buffer("abc\ndef\nghi\njkl\n", None, cx) + .unwrap(), + project + .create_buffer("mno\npqr\nstu\nvwx\n", None, cx) + .unwrap(), + ) + }); -// // Apply the update of adding the excerpts. -// follower_1 -// .update(cx, |follower, cx| { -// follower.apply_update_proto(&project, update_message.borrow().clone().unwrap(), cx) -// }) -// .await -// .unwrap(); -// assert_eq!( -// follower_1.update(cx, |editor, cx| editor.text(cx)), -// leader.update(cx, |editor, cx| editor.text(cx)) -// ); -// update_message.borrow_mut().take(); - -// // Start following separately after it already has excerpts. -// let mut state_message = leader.update(cx, |leader, cx| leader.to_state_proto(cx)); -// let follower_2 = cx -// .update(|cx| { -// Editor::from_state_proto( -// pane.clone(), -// workspace.clone(), -// ViewId { -// creator: Default::default(), -// id: 0, -// }, -// &mut state_message, -// cx, -// ) -// }) -// .unwrap() -// .await -// .unwrap(); -// assert_eq!( -// follower_2.update(cx, |editor, cx| editor.text(cx)), -// leader.update(cx, |editor, cx| editor.text(cx)) -// ); + // Insert some excerpts. + leader.update(cx, |leader, cx| { + leader.buffer.update(cx, |multibuffer, cx| { + let excerpt_ids = multibuffer.push_excerpts( + buffer_1.clone(), + [ + ExcerptRange { + context: 1..6, + primary: None, + }, + ExcerptRange { + context: 12..15, + primary: None, + }, + ExcerptRange { + context: 0..3, + primary: None, + }, + ], + cx, + ); + multibuffer.insert_excerpts_after( + excerpt_ids[0], + buffer_2.clone(), + [ + ExcerptRange { + context: 8..12, + primary: None, + }, + ExcerptRange { + context: 0..6, + primary: None, + }, + ], + cx, + ); + }); + }); -// // Remove some excerpts. -// leader.update(cx, |leader, cx| { -// leader.buffer.update(cx, |multibuffer, cx| { -// let excerpt_ids = multibuffer.excerpt_ids(); -// multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx); -// multibuffer.remove_excerpts([excerpt_ids[0]], cx); -// }); -// }); + // Apply the update of adding the excerpts. + follower_1 + .update(cx, |follower, cx| { + follower.apply_update_proto(&project, update_message.borrow().clone().unwrap(), cx) + }) + .await + .unwrap(); + assert_eq!( + follower_1.update(cx, |editor, cx| editor.text(cx)), + leader.update(cx, |editor, cx| editor.text(cx)) + ); + update_message.borrow_mut().take(); + + // Start following separately after it already has excerpts. + let mut state_message = leader.update(cx, |leader, cx| leader.to_state_proto(cx)); + let follower_2 = cx + .update_window(*workspace.deref(), |_, cx| { + Editor::from_state_proto( + pane.clone(), + workspace.root_view(cx).unwrap().clone(), + ViewId { + creator: Default::default(), + id: 0, + }, + &mut state_message, + cx, + ) + }) + .unwrap() + .unwrap() + .await + .unwrap(); + assert_eq!( + follower_2.update(cx, |editor, cx| editor.text(cx)), + leader.update(cx, |editor, cx| editor.text(cx)) + ); -// // Apply the update of removing the excerpts. -// follower_1 -// .update(cx, |follower, cx| { -// follower.apply_update_proto(&project, update_message.borrow().clone().unwrap(), cx) -// }) -// .await -// .unwrap(); -// follower_2 -// .update(cx, |follower, cx| { -// follower.apply_update_proto(&project, update_message.borrow().clone().unwrap(), cx) -// }) -// .await -// .unwrap(); -// update_message.borrow_mut().take(); -// assert_eq!( -// follower_1.update(cx, |editor, cx| editor.text(cx)), -// leader.update(cx, |editor, cx| editor.text(cx)) -// ); -// } + // Remove some excerpts. + leader.update(cx, |leader, cx| { + leader.buffer.update(cx, |multibuffer, cx| { + let excerpt_ids = multibuffer.excerpt_ids(); + multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx); + multibuffer.remove_excerpts([excerpt_ids[0]], cx); + }); + }); + + // Apply the update of removing the excerpts. + follower_1 + .update(cx, |follower, cx| { + follower.apply_update_proto(&project, update_message.borrow().clone().unwrap(), cx) + }) + .await + .unwrap(); + follower_2 + .update(cx, |follower, cx| { + follower.apply_update_proto(&project, update_message.borrow().clone().unwrap(), cx) + }) + .await + .unwrap(); + update_message.borrow_mut().take(); + assert_eq!( + follower_1.update(cx, |editor, cx| editor.text(cx)), + leader.update(cx, |editor, cx| editor.text(cx)) + ); +} #[gpui::test] async fn go_to_prev_overlapping_diagnostic( From be509a5ce042fdcdb14e45ffc53e3b4bac5c6022 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Fri, 1 Dec 2023 20:50:30 +0100 Subject: [PATCH 12/35] test_clipboard --- crates/editor2/src/editor_tests.rs | 164 +++++++++++---------- crates/gpui2/src/platform/test/platform.rs | 12 +- 2 files changed, 91 insertions(+), 85 deletions(-) diff --git a/crates/editor2/src/editor_tests.rs b/crates/editor2/src/editor_tests.rs index 87a1ba8dee66bd2f8f7a572dfb1d969933e7cf67..3c31d05e98d3ffec8e99264d4055371070c38dc0 100644 --- a/crates/editor2/src/editor_tests.rs +++ b/crates/editor2/src/editor_tests.rs @@ -12,7 +12,7 @@ use futures::StreamExt; use gpui::{ div, serde_json::{self, json}, - Div, Flatten, TestAppContext, VisualTestContext, WindowBounds, WindowOptions, + Div, Flatten, Platform, TestAppContext, VisualTestContext, WindowBounds, WindowOptions, }; use indoc::indoc; use language::{ @@ -3154,88 +3154,92 @@ fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) { // }); // } -//todo!(clipboard) -// #[gpui::test] -// async fn test_clipboard(cx: &mut gpui::TestAppContext) { -// init_test(cx, |_| {}); +#[gpui::test] +async fn test_clipboard(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); -// let mut cx = EditorTestContext::new(cx).await; + let mut cx = EditorTestContext::new(cx).await; -// cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six "); -// cx.update_editor(|e, cx| e.cut(&Cut, cx)); -// cx.assert_editor_state("ˇtwo ˇfour ˇsix "); - -// // Paste with three cursors. Each cursor pastes one slice of the clipboard text. -// cx.set_state("two ˇfour ˇsix ˇ"); -// cx.update_editor(|e, cx| e.paste(&Paste, cx)); -// cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ"); - -// // Paste again but with only two cursors. Since the number of cursors doesn't -// // match the number of slices in the clipboard, the entire clipboard text -// // is pasted at each cursor. -// cx.set_state("ˇtwo one✅ four three six five ˇ"); -// cx.update_editor(|e, cx| { -// e.handle_input("( ", cx); -// e.paste(&Paste, cx); -// e.handle_input(") ", cx); -// }); -// cx.assert_editor_state( -// &([ -// "( one✅ ", -// "three ", -// "five ) ˇtwo one✅ four three six five ( one✅ ", -// "three ", -// "five ) ˇ", -// ] -// .join("\n")), -// ); + cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six "); + cx.update_editor(|e, cx| e.cut(&Cut, cx)); + cx.assert_editor_state("ˇtwo ˇfour ˇsix "); -// // Cut with three selections, one of which is full-line. -// cx.set_state(indoc! {" -// 1«2ˇ»3 -// 4ˇ567 -// «8ˇ»9"}); -// cx.update_editor(|e, cx| e.cut(&Cut, cx)); -// cx.assert_editor_state(indoc! {" -// 1ˇ3 -// ˇ9"}); - -// // Paste with three selections, noticing how the copied selection that was full-line -// // gets inserted before the second cursor. -// cx.set_state(indoc! {" -// 1ˇ3 -// 9ˇ -// «oˇ»ne"}); -// cx.update_editor(|e, cx| e.paste(&Paste, cx)); -// cx.assert_editor_state(indoc! {" -// 12ˇ3 -// 4567 -// 9ˇ -// 8ˇne"}); - -// // Copy with a single cursor only, which writes the whole line into the clipboard. -// cx.set_state(indoc! {" -// The quick brown -// fox juˇmps over -// the lazy dog"}); -// cx.update_editor(|e, cx| e.copy(&Copy, cx)); -// cx.cx.assert_clipboard_content(Some("fox jumps over\n")); - -// // Paste with three selections, noticing how the copied full-line selection is inserted -// // before the empty selections but replaces the selection that is non-empty. -// cx.set_state(indoc! {" -// Tˇhe quick brown -// «foˇ»x jumps over -// tˇhe lazy dog"}); -// cx.update_editor(|e, cx| e.paste(&Paste, cx)); -// cx.assert_editor_state(indoc! {" -// fox jumps over -// Tˇhe quick brown -// fox jumps over -// ˇx jumps over -// fox jumps over -// tˇhe lazy dog"}); -// } + // Paste with three cursors. Each cursor pastes one slice of the clipboard text. + cx.set_state("two ˇfour ˇsix ˇ"); + cx.update_editor(|e, cx| e.paste(&Paste, cx)); + cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ"); + + // Paste again but with only two cursors. Since the number of cursors doesn't + // match the number of slices in the clipboard, the entire clipboard text + // is pasted at each cursor. + cx.set_state("ˇtwo one✅ four three six five ˇ"); + cx.update_editor(|e, cx| { + e.handle_input("( ", cx); + e.paste(&Paste, cx); + e.handle_input(") ", cx); + }); + cx.assert_editor_state( + &([ + "( one✅ ", + "three ", + "five ) ˇtwo one✅ four three six five ( one✅ ", + "three ", + "five ) ˇ", + ] + .join("\n")), + ); + + // Cut with three selections, one of which is full-line. + cx.set_state(indoc! {" + 1«2ˇ»3 + 4ˇ567 + «8ˇ»9"}); + cx.update_editor(|e, cx| e.cut(&Cut, cx)); + cx.assert_editor_state(indoc! {" + 1ˇ3 + ˇ9"}); + + // Paste with three selections, noticing how the copied selection that was full-line + // gets inserted before the second cursor. + cx.set_state(indoc! {" + 1ˇ3 + 9ˇ + «oˇ»ne"}); + cx.update_editor(|e, cx| e.paste(&Paste, cx)); + cx.assert_editor_state(indoc! {" + 12ˇ3 + 4567 + 9ˇ + 8ˇne"}); + + // Copy with a single cursor only, which writes the whole line into the clipboard. + cx.set_state(indoc! {" + The quick brown + fox juˇmps over + the lazy dog"}); + cx.update_editor(|e, cx| e.copy(&Copy, cx)); + assert_eq!( + cx.test_platform + .read_from_clipboard() + .map(|item| item.text().to_owned()), + Some("fox jumps over\n".to_owned()) + ); + + // Paste with three selections, noticing how the copied full-line selection is inserted + // before the empty selections but replaces the selection that is non-empty. + cx.set_state(indoc! {" + Tˇhe quick brown + «foˇ»x jumps over + tˇhe lazy dog"}); + cx.update_editor(|e, cx| e.paste(&Paste, cx)); + cx.assert_editor_state(indoc! {" + fox jumps over + Tˇhe quick brown + fox jumps over + ˇx jumps over + fox jumps over + tˇhe lazy dog"}); +} #[gpui::test] async fn test_paste_multiline(cx: &mut gpui::TestAppContext) { diff --git a/crates/gpui2/src/platform/test/platform.rs b/crates/gpui2/src/platform/test/platform.rs index 4532b33f5037bcf5ab139a7acb3708ade901dd88..0b0c007b3b0645843beb709fa7cbbe7540bb09f5 100644 --- a/crates/gpui2/src/platform/test/platform.rs +++ b/crates/gpui2/src/platform/test/platform.rs @@ -1,6 +1,6 @@ use crate::{ - AnyWindowHandle, BackgroundExecutor, CursorStyle, DisplayId, ForegroundExecutor, Platform, - PlatformDisplay, PlatformTextSystem, TestDisplay, TestWindow, WindowOptions, + AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DisplayId, ForegroundExecutor, + Platform, PlatformDisplay, PlatformTextSystem, TestDisplay, TestWindow, WindowOptions, }; use anyhow::{anyhow, Result}; use collections::VecDeque; @@ -20,6 +20,7 @@ pub struct TestPlatform { active_window: Arc>>, active_display: Rc, active_cursor: Mutex, + current_clipboard_item: Mutex>, pub(crate) prompts: RefCell, weak: Weak, } @@ -39,6 +40,7 @@ impl TestPlatform { active_cursor: Default::default(), active_display: Rc::new(TestDisplay::new()), active_window: Default::default(), + current_clipboard_item: Mutex::new(None), weak: weak.clone(), }) } @@ -236,12 +238,12 @@ impl Platform for TestPlatform { true } - fn write_to_clipboard(&self, _item: crate::ClipboardItem) { - unimplemented!() + fn write_to_clipboard(&self, item: crate::ClipboardItem) { + *self.current_clipboard_item.lock() = Some(item); } fn read_from_clipboard(&self) -> Option { - unimplemented!() + self.current_clipboard_item.lock().clone() } fn write_credentials(&self, _url: &str, _username: &str, _password: &[u8]) -> Result<()> { From d81fb3680ec47583aedd91da32e17972d80179bc Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Fri, 1 Dec 2023 22:04:43 +0100 Subject: [PATCH 13/35] Uncomment copilot2 tests --- Cargo.lock | 2 +- crates/copilot2/Cargo.toml | 2 +- crates/copilot2/src/copilot2.rs | 454 ++++++++++++++++---------------- 3 files changed, 230 insertions(+), 228 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3110a9ff43e75bc698b782d3ec4d79383851fca3..299ff4203bfdaeb54608d63e01d6fa2cf756a69a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2098,7 +2098,7 @@ dependencies = [ "lsp2", "node_runtime", "parking_lot 0.11.2", - "rpc", + "rpc2", "serde", "serde_derive", "settings2", diff --git a/crates/copilot2/Cargo.toml b/crates/copilot2/Cargo.toml index 68b56a6c018b5108c841c37e83f68c0877ee77f5..9a9243b32eecb766451a3f7f89940227fe6059fa 100644 --- a/crates/copilot2/Cargo.toml +++ b/crates/copilot2/Cargo.toml @@ -45,6 +45,6 @@ fs = { path = "../fs", features = ["test-support"] } gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] } language = { package = "language2", path = "../language2", features = ["test-support"] } lsp = { package = "lsp2", path = "../lsp2", features = ["test-support"] } -rpc = { path = "../rpc", features = ["test-support"] } +rpc = { package = "rpc2", path = "../rpc2", features = ["test-support"] } settings = { package = "settings2", path = "../settings2", features = ["test-support"] } util = { path = "../util", features = ["test-support"] } diff --git a/crates/copilot2/src/copilot2.rs b/crates/copilot2/src/copilot2.rs index 53d802dd037f51c3df4183ba57bda44a1ea18e7f..01ee77369adce1bfe29d242ef79ffb4c2b570967 100644 --- a/crates/copilot2/src/copilot2.rs +++ b/crates/copilot2/src/copilot2.rs @@ -1002,229 +1002,231 @@ async fn get_copilot_lsp(http: Arc) -> anyhow::Result { } } -// #[cfg(test)] -// mod tests { -// use super::*; -// use gpui::{executor::Deterministic, TestAppContext}; - -// #[gpui::test(iterations = 10)] -// async fn test_buffer_management(deterministic: Arc, cx: &mut TestAppContext) { -// deterministic.forbid_parking(); -// let (copilot, mut lsp) = Copilot::fake(cx); - -// let buffer_1 = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "Hello")); -// let buffer_1_uri: lsp::Url = format!("buffer://{}", buffer_1.id()).parse().unwrap(); -// copilot.update(cx, |copilot, cx| copilot.register_buffer(&buffer_1, cx)); -// assert_eq!( -// lsp.receive_notification::() -// .await, -// lsp::DidOpenTextDocumentParams { -// text_document: lsp::TextDocumentItem::new( -// buffer_1_uri.clone(), -// "plaintext".into(), -// 0, -// "Hello".into() -// ), -// } -// ); - -// let buffer_2 = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "Goodbye")); -// let buffer_2_uri: lsp::Url = format!("buffer://{}", buffer_2.id()).parse().unwrap(); -// copilot.update(cx, |copilot, cx| copilot.register_buffer(&buffer_2, cx)); -// assert_eq!( -// lsp.receive_notification::() -// .await, -// lsp::DidOpenTextDocumentParams { -// text_document: lsp::TextDocumentItem::new( -// buffer_2_uri.clone(), -// "plaintext".into(), -// 0, -// "Goodbye".into() -// ), -// } -// ); - -// buffer_1.update(cx, |buffer, cx| buffer.edit([(5..5, " world")], None, cx)); -// assert_eq!( -// lsp.receive_notification::() -// .await, -// lsp::DidChangeTextDocumentParams { -// text_document: lsp::VersionedTextDocumentIdentifier::new(buffer_1_uri.clone(), 1), -// content_changes: vec![lsp::TextDocumentContentChangeEvent { -// range: Some(lsp::Range::new( -// lsp::Position::new(0, 5), -// lsp::Position::new(0, 5) -// )), -// range_length: None, -// text: " world".into(), -// }], -// } -// ); - -// // Ensure updates to the file are reflected in the LSP. -// buffer_1 -// .update(cx, |buffer, cx| { -// buffer.file_updated( -// Arc::new(File { -// abs_path: "/root/child/buffer-1".into(), -// path: Path::new("child/buffer-1").into(), -// }), -// cx, -// ) -// }) -// .await; -// assert_eq!( -// lsp.receive_notification::() -// .await, -// lsp::DidCloseTextDocumentParams { -// text_document: lsp::TextDocumentIdentifier::new(buffer_1_uri), -// } -// ); -// let buffer_1_uri = lsp::Url::from_file_path("/root/child/buffer-1").unwrap(); -// assert_eq!( -// lsp.receive_notification::() -// .await, -// lsp::DidOpenTextDocumentParams { -// text_document: lsp::TextDocumentItem::new( -// buffer_1_uri.clone(), -// "plaintext".into(), -// 1, -// "Hello world".into() -// ), -// } -// ); - -// // Ensure all previously-registered buffers are closed when signing out. -// lsp.handle_request::(|_, _| async { -// Ok(request::SignOutResult {}) -// }); -// copilot -// .update(cx, |copilot, cx| copilot.sign_out(cx)) -// .await -// .unwrap(); -// assert_eq!( -// lsp.receive_notification::() -// .await, -// lsp::DidCloseTextDocumentParams { -// text_document: lsp::TextDocumentIdentifier::new(buffer_2_uri.clone()), -// } -// ); -// assert_eq!( -// lsp.receive_notification::() -// .await, -// lsp::DidCloseTextDocumentParams { -// text_document: lsp::TextDocumentIdentifier::new(buffer_1_uri.clone()), -// } -// ); - -// // Ensure all previously-registered buffers are re-opened when signing in. -// lsp.handle_request::(|_, _| async { -// Ok(request::SignInInitiateResult::AlreadySignedIn { -// user: "user-1".into(), -// }) -// }); -// copilot -// .update(cx, |copilot, cx| copilot.sign_in(cx)) -// .await -// .unwrap(); -// assert_eq!( -// lsp.receive_notification::() -// .await, -// lsp::DidOpenTextDocumentParams { -// text_document: lsp::TextDocumentItem::new( -// buffer_2_uri.clone(), -// "plaintext".into(), -// 0, -// "Goodbye".into() -// ), -// } -// ); -// assert_eq!( -// lsp.receive_notification::() -// .await, -// lsp::DidOpenTextDocumentParams { -// text_document: lsp::TextDocumentItem::new( -// buffer_1_uri.clone(), -// "plaintext".into(), -// 0, -// "Hello world".into() -// ), -// } -// ); - -// // Dropping a buffer causes it to be closed on the LSP side as well. -// cx.update(|_| drop(buffer_2)); -// assert_eq!( -// lsp.receive_notification::() -// .await, -// lsp::DidCloseTextDocumentParams { -// text_document: lsp::TextDocumentIdentifier::new(buffer_2_uri), -// } -// ); -// } - -// struct File { -// abs_path: PathBuf, -// path: Arc, -// } - -// impl language2::File for File { -// fn as_local(&self) -> Option<&dyn language2::LocalFile> { -// Some(self) -// } - -// fn mtime(&self) -> std::time::SystemTime { -// unimplemented!() -// } - -// fn path(&self) -> &Arc { -// &self.path -// } - -// fn full_path(&self, _: &AppContext) -> PathBuf { -// unimplemented!() -// } - -// fn file_name<'a>(&'a self, _: &'a AppContext) -> &'a std::ffi::OsStr { -// unimplemented!() -// } - -// fn is_deleted(&self) -> bool { -// unimplemented!() -// } - -// fn as_any(&self) -> &dyn std::any::Any { -// unimplemented!() -// } - -// fn to_proto(&self) -> rpc::proto::File { -// unimplemented!() -// } - -// fn worktree_id(&self) -> usize { -// 0 -// } -// } - -// impl language::LocalFile for File { -// fn abs_path(&self, _: &AppContext) -> PathBuf { -// self.abs_path.clone() -// } - -// fn load(&self, _: &AppContext) -> Task> { -// unimplemented!() -// } - -// fn buffer_reloaded( -// &self, -// _: u64, -// _: &clock::Global, -// _: language::RopeFingerprint, -// _: language::LineEnding, -// _: std::time::SystemTime, -// _: &mut AppContext, -// ) { -// unimplemented!() -// } -// } -// } +#[cfg(test)] +mod tests { + use super::*; + use gpui::TestAppContext; + + #[gpui::test(iterations = 10)] + async fn test_buffer_management(cx: &mut TestAppContext) { + let (copilot, mut lsp) = Copilot::fake(cx); + + let buffer_1 = cx.build_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), "Hello")); + let buffer_1_uri: lsp::Url = format!("buffer://{}", buffer_1.entity_id().as_u64()) + .parse() + .unwrap(); + copilot.update(cx, |copilot, cx| copilot.register_buffer(&buffer_1, cx)); + assert_eq!( + lsp.receive_notification::() + .await, + lsp::DidOpenTextDocumentParams { + text_document: lsp::TextDocumentItem::new( + buffer_1_uri.clone(), + "plaintext".into(), + 0, + "Hello".into() + ), + } + ); + + let buffer_2 = cx.build_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), "Goodbye")); + let buffer_2_uri: lsp::Url = format!("buffer://{}", buffer_2.entity_id().as_u64()) + .parse() + .unwrap(); + copilot.update(cx, |copilot, cx| copilot.register_buffer(&buffer_2, cx)); + assert_eq!( + lsp.receive_notification::() + .await, + lsp::DidOpenTextDocumentParams { + text_document: lsp::TextDocumentItem::new( + buffer_2_uri.clone(), + "plaintext".into(), + 0, + "Goodbye".into() + ), + } + ); + + buffer_1.update(cx, |buffer, cx| buffer.edit([(5..5, " world")], None, cx)); + assert_eq!( + lsp.receive_notification::() + .await, + lsp::DidChangeTextDocumentParams { + text_document: lsp::VersionedTextDocumentIdentifier::new(buffer_1_uri.clone(), 1), + content_changes: vec![lsp::TextDocumentContentChangeEvent { + range: Some(lsp::Range::new( + lsp::Position::new(0, 5), + lsp::Position::new(0, 5) + )), + range_length: None, + text: " world".into(), + }], + } + ); + + // Ensure updates to the file are reflected in the LSP. + buffer_1.update(cx, |buffer, cx| { + buffer.file_updated( + Arc::new(File { + abs_path: "/root/child/buffer-1".into(), + path: Path::new("child/buffer-1").into(), + }), + cx, + ) + }); + assert_eq!( + lsp.receive_notification::() + .await, + lsp::DidCloseTextDocumentParams { + text_document: lsp::TextDocumentIdentifier::new(buffer_1_uri), + } + ); + let buffer_1_uri = lsp::Url::from_file_path("/root/child/buffer-1").unwrap(); + assert_eq!( + lsp.receive_notification::() + .await, + lsp::DidOpenTextDocumentParams { + text_document: lsp::TextDocumentItem::new( + buffer_1_uri.clone(), + "plaintext".into(), + 1, + "Hello world".into() + ), + } + ); + + // Ensure all previously-registered buffers are closed when signing out. + lsp.handle_request::(|_, _| async { + Ok(request::SignOutResult {}) + }); + copilot + .update(cx, |copilot, cx| copilot.sign_out(cx)) + .await + .unwrap(); + // todo!() po: these notifications now happen in reverse order? + assert_eq!( + lsp.receive_notification::() + .await, + lsp::DidCloseTextDocumentParams { + text_document: lsp::TextDocumentIdentifier::new(buffer_2_uri.clone()), + } + ); + assert_eq!( + lsp.receive_notification::() + .await, + lsp::DidCloseTextDocumentParams { + text_document: lsp::TextDocumentIdentifier::new(buffer_1_uri.clone()), + } + ); + + // Ensure all previously-registered buffers are re-opened when signing in. + lsp.handle_request::(|_, _| async { + Ok(request::SignInInitiateResult::AlreadySignedIn { + user: "user-1".into(), + }) + }); + copilot + .update(cx, |copilot, cx| copilot.sign_in(cx)) + .await + .unwrap(); + assert_eq!( + lsp.receive_notification::() + .await, + lsp::DidOpenTextDocumentParams { + text_document: lsp::TextDocumentItem::new( + buffer_2_uri.clone(), + "plaintext".into(), + 0, + "Goodbye".into() + ), + } + ); + assert_eq!( + lsp.receive_notification::() + .await, + lsp::DidOpenTextDocumentParams { + text_document: lsp::TextDocumentItem::new( + buffer_1_uri.clone(), + "plaintext".into(), + 0, + "Hello world".into() + ), + } + ); + + // Dropping a buffer causes it to be closed on the LSP side as well. + cx.update(|_| drop(buffer_2)); + assert_eq!( + lsp.receive_notification::() + .await, + lsp::DidCloseTextDocumentParams { + text_document: lsp::TextDocumentIdentifier::new(buffer_2_uri), + } + ); + } + + struct File { + abs_path: PathBuf, + path: Arc, + } + + impl language::File for File { + fn as_local(&self) -> Option<&dyn language::LocalFile> { + Some(self) + } + + fn mtime(&self) -> std::time::SystemTime { + unimplemented!() + } + + fn path(&self) -> &Arc { + &self.path + } + + fn full_path(&self, _: &AppContext) -> PathBuf { + unimplemented!() + } + + fn file_name<'a>(&'a self, _: &'a AppContext) -> &'a std::ffi::OsStr { + unimplemented!() + } + + fn is_deleted(&self) -> bool { + unimplemented!() + } + + fn as_any(&self) -> &dyn std::any::Any { + unimplemented!() + } + + fn to_proto(&self) -> rpc::proto::File { + unimplemented!() + } + + fn worktree_id(&self) -> usize { + 0 + } + } + + impl language::LocalFile for File { + fn abs_path(&self, _: &AppContext) -> PathBuf { + self.abs_path.clone() + } + + fn load(&self, _: &AppContext) -> Task> { + unimplemented!() + } + + fn buffer_reloaded( + &self, + _: u64, + _: &clock::Global, + _: language::RopeFingerprint, + _: language::LineEnding, + _: std::time::SystemTime, + _: &mut AppContext, + ) { + unimplemented!() + } + } +} From 9ffe78d264b0a4b06f1f2c296075138b93310881 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 4 Dec 2023 12:10:01 +0100 Subject: [PATCH 14/35] Fix up random wrap map test --- crates/editor2/src/display_map/wrap_map.rs | 89 +++++++++++----------- 1 file changed, 44 insertions(+), 45 deletions(-) diff --git a/crates/editor2/src/display_map/wrap_map.rs b/crates/editor2/src/display_map/wrap_map.rs index 817f7165accea1ad14044f9a22a6b232ef63883b..a2ac0ec849bfb9b26983c897a2ae3cc2ebd9878c 100644 --- a/crates/editor2/src/display_map/wrap_map.rs +++ b/crates/editor2/src/display_map/wrap_map.rs @@ -741,49 +741,48 @@ impl WrapSnapshot { } fn check_invariants(&self) { - // todo!() - // #[cfg(test)] - // { - // assert_eq!( - // TabPoint::from(self.transforms.summary().input.lines), - // self.tab_snapshot.max_point() - // ); - - // { - // let mut transforms = self.transforms.cursor::<()>().peekable(); - // while let Some(transform) = transforms.next() { - // if let Some(next_transform) = transforms.peek() { - // assert!(transform.is_isomorphic() != next_transform.is_isomorphic()); - // } - // } - // } - - // let text = language::Rope::from(self.text().as_str()); - // let mut input_buffer_rows = self.tab_snapshot.buffer_rows(0); - // let mut expected_buffer_rows = Vec::new(); - // let mut prev_tab_row = 0; - // for display_row in 0..=self.max_point().row() { - // let tab_point = self.to_tab_point(WrapPoint::new(display_row, 0)); - // if tab_point.row() == prev_tab_row && display_row != 0 { - // expected_buffer_rows.push(None); - // } else { - // expected_buffer_rows.push(input_buffer_rows.next().unwrap()); - // } - - // prev_tab_row = tab_point.row(); - // assert_eq!(self.line_len(display_row), text.line_len(display_row)); - // } - - // for start_display_row in 0..expected_buffer_rows.len() { - // assert_eq!( - // self.buffer_rows(start_display_row as u32) - // .collect::>(), - // &expected_buffer_rows[start_display_row..], - // "invalid buffer_rows({}..)", - // start_display_row - // ); - // } - // } + #[cfg(test)] + { + assert_eq!( + TabPoint::from(self.transforms.summary().input.lines), + self.tab_snapshot.max_point() + ); + + { + let mut transforms = self.transforms.cursor::<()>().peekable(); + while let Some(transform) = transforms.next() { + if let Some(next_transform) = transforms.peek() { + assert!(transform.is_isomorphic() != next_transform.is_isomorphic()); + } + } + } + + let text = language::Rope::from(self.text().as_str()); + let mut input_buffer_rows = self.tab_snapshot.buffer_rows(0); + let mut expected_buffer_rows = Vec::new(); + let mut prev_tab_row = 0; + for display_row in 0..=self.max_point().row() { + let tab_point = self.to_tab_point(WrapPoint::new(display_row, 0)); + if tab_point.row() == prev_tab_row && display_row != 0 { + expected_buffer_rows.push(None); + } else { + expected_buffer_rows.push(input_buffer_rows.next().unwrap()); + } + + prev_tab_row = tab_point.row(); + assert_eq!(self.line_len(display_row), text.line_len(display_row)); + } + + for start_display_row in 0..expected_buffer_rows.len() { + assert_eq!( + self.buffer_rows(start_display_row as u32) + .collect::>(), + &expected_buffer_rows[start_display_row..], + "invalid buffer_rows({}..)", + start_display_row + ); + } + } } } @@ -1051,7 +1050,7 @@ mod tests { .map(|i| i.parse().expect("invalid `OPERATIONS` variable")) .unwrap_or(10); - let text_system = cx.test_platform.text_system(); + let text_system = cx.read(|cx| cx.text_system().clone()); let mut wrap_width = if rng.gen_bool(0.1) { None } else { @@ -1086,7 +1085,7 @@ mod tests { let tabs_snapshot = tab_map.set_max_expansion_column(32); log::info!("TabMap text: {:?}", tabs_snapshot.text()); - let mut line_wrapper = LineWrapper::new(font_id, font_size, text_system); + let mut line_wrapper = text_system.line_wrapper(font.clone(), font_size).unwrap(); let unwrapped_text = tabs_snapshot.text(); let expected_text = wrap_text(&unwrapped_text, wrap_width, &mut line_wrapper); From 0f7fc8c1a03eaec347df101ada66f28aefa3162a Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 4 Dec 2023 13:14:03 +0100 Subject: [PATCH 15/35] fix display map tests These tests failed due to an indefinite hang in buffer.condition in the following code: \`\`\`rust let buffer = cx .add_model(|cx| Buffer::new(0, cx.model_id() as u64, text).with_language(language, cx)); buffer.condition(cx, |buf, _| !buf.is_parsing()).await; `\`\` In both gpui1 and gpui2 \`.with_language\` spawns a task that notifies the context once it's done. The \`condition\` waits for notifications to be raised. The gist of the problem was that in gpui2, the spawned task was scheduled straight away, so we never really saw the notification with \`condition\`, causing us to wait indefinitely. This is probably a difference in test between schedulers in gpui1 and gpui2, but I kind of sidestepped the issue by spawning a condition before firing off a parsing task with \`set_language\`. --- crates/editor2/src/display_map.rs | 24 +++++++++++-------- crates/gpui2/src/app/test_context.rs | 35 +++++++++++++++------------- crates/project2/src/worktree.rs | 14 +++++++---- 3 files changed, 42 insertions(+), 31 deletions(-) diff --git a/crates/editor2/src/display_map.rs b/crates/editor2/src/display_map.rs index 1aee04dd0ae02b8d4ea98025be177a82e3801ef7..74890d9edb4bced394ce9363856dfbf3096f8cff 100644 --- a/crates/editor2/src/display_map.rs +++ b/crates/editor2/src/display_map.rs @@ -1467,10 +1467,12 @@ pub mod tests { cx.update(|cx| init_test(cx, |s| s.defaults.tab_size = Some(2.try_into().unwrap()))); - let buffer = cx.build_model(|cx| { - Buffer::new(0, cx.entity_id().as_u64(), text).with_language(language, cx) + let buffer = cx.build_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), text)); + let condition = cx.condition(&buffer, |buf, _| !buf.is_parsing()); + cx.update_model(&buffer, |this, cx| { + this.set_language(Some(language), cx); }); - cx.condition(&buffer, |buf, _| !buf.is_parsing()).await; + condition.await; let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx)); let font_size = px(14.0); @@ -1554,10 +1556,12 @@ pub mod tests { cx.update(|cx| init_test(cx, |_| {})); - let buffer = cx.build_model(|cx| { - Buffer::new(0, cx.entity_id().as_u64(), text).with_language(language, cx) + let buffer = cx.build_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), text)); + let condition = cx.condition(&buffer, |buf, _| !buf.is_parsing()); + buffer.update(cx, |this, cx| { + this.set_language(Some(language), cx); }); - cx.condition(&buffer, |buf, _| !buf.is_parsing()).await; + condition.await; let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx)); let font_size = px(16.0); @@ -1621,10 +1625,10 @@ pub mod tests { let (text, highlighted_ranges) = marked_text_ranges(r#"constˇ «a»: B = "c «d»""#, false); - let buffer = cx.build_model(|cx| { - Buffer::new(0, cx.entity_id().as_u64(), text).with_language(language, cx) - }); - cx.condition(&buffer, |buf, _| !buf.is_parsing()).await; + let buffer = cx.build_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), text)); + let condition = cx.condition(&buffer, |buf, _| !buf.is_parsing()); + buffer.update(cx, |this, cx| this.set_language(Some(language), cx)); + condition.await; let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx)); let buffer_snapshot = buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx)); diff --git a/crates/gpui2/src/app/test_context.rs b/crates/gpui2/src/app/test_context.rs index c915753749f4f85bb75cf1440df0775b232d7fa4..5c2bf65c02ea1e513cca62b5f69c29c7273a157a 100644 --- a/crates/gpui2/src/app/test_context.rs +++ b/crates/gpui2/src/app/test_context.rs @@ -8,6 +8,7 @@ use crate::{ use anyhow::{anyhow, bail}; use futures::{Stream, StreamExt}; use std::{future::Future, ops::Deref, rc::Rc, sync::Arc, time::Duration}; +use util::ResultExt; #[derive(Clone)] pub struct TestAppContext { @@ -297,7 +298,7 @@ impl TestAppContext { .unwrap() } - pub fn notifications(&mut self, entity: &impl Entity) -> impl Stream { + pub fn notifications(&self, entity: &impl Entity) -> impl Stream { let (tx, rx) = futures::channel::mpsc::unbounded(); self.update(|cx| { cx.observe(entity, { @@ -307,7 +308,7 @@ impl TestAppContext { } }) .detach(); - cx.observe_release(entity, move |_, _| tx.close_channel()) + cx.observe_release(entity, move |_, _| dbg!(tx.close_channel())) .detach() }); rx @@ -331,28 +332,30 @@ impl TestAppContext { rx } - pub async fn condition( - &mut self, + pub fn condition( + &self, model: &Model, - mut predicate: impl FnMut(&mut T, &mut ModelContext) -> bool, - ) { + mut predicate: impl FnMut(&mut T, &mut ModelContext) -> bool + Send + 'static, + ) -> Task<()> { let timer = self.executor().timer(Duration::from_secs(3)); let mut notifications = self.notifications(model); use futures::FutureExt as _; use smol::future::FutureExt as _; - - async { - while notifications.next().await.is_some() { - if model.update(self, &mut predicate) { - return Ok(()); + let model = model.clone(); + self.spawn(move |mut cx| async move { + async move { + while notifications.next().await.is_some() { + if model.update(&mut cx, &mut predicate).log_err().unwrap() { + return Ok(()); + } } + bail!("model dropped") } - bail!("model dropped") - } - .race(timer.map(|_| Err(anyhow!("condition timed out")))) - .await - .unwrap(); + .race(timer.map(|_| Err(anyhow!("condition timed out")))) + .await + .unwrap() + }) } } diff --git a/crates/project2/src/worktree.rs b/crates/project2/src/worktree.rs index e424375220c1c7aa2292d9a4762d7581ea67222e..86cfc6737411aba10d8de8826e855c2bd8342a9d 100644 --- a/crates/project2/src/worktree.rs +++ b/crates/project2/src/worktree.rs @@ -4187,7 +4187,7 @@ impl WorktreeModelHandle for Model { &self, cx: &'a mut gpui::TestAppContext, ) -> futures::future::LocalBoxFuture<'a, ()> { - let file_name = "fs-event-sentinel"; + let file_name: &'static str = "fs-event-sentinel"; let tree = self.clone(); let (fs, root_path) = self.update(cx, |tree, _| { @@ -4200,14 +4200,18 @@ impl WorktreeModelHandle for Model { .await .unwrap(); - cx.condition(&tree, |tree, _| tree.entry_for_path(file_name).is_some()) - .await; + cx.condition(&tree, move |tree, _| { + tree.entry_for_path(file_name).is_some() + }) + .await; fs.remove_file(&root_path.join(file_name), Default::default()) .await .unwrap(); - cx.condition(&tree, |tree, _| tree.entry_for_path(file_name).is_none()) - .await; + cx.condition(&tree, move |tree, _| { + tree.entry_for_path(file_name).is_none() + }) + .await; cx.update(|cx| tree.read(cx).as_local().unwrap().scan_complete()) .await; From 1a5f6f604b1c8a17b327a6728224dd7fa9f9d763 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 4 Dec 2023 13:42:48 +0100 Subject: [PATCH 16/35] Uncomment & fix up test_transpose --- crates/editor2/src/editor_tests.rs | 148 ++++++++++++++--------------- 1 file changed, 74 insertions(+), 74 deletions(-) diff --git a/crates/editor2/src/editor_tests.rs b/crates/editor2/src/editor_tests.rs index 3c31d05e98d3ffec8e99264d4055371070c38dc0..3c06bb23443ca1428a5b7e07c8d332bf683c2e82 100644 --- a/crates/editor2/src/editor_tests.rs +++ b/crates/editor2/src/editor_tests.rs @@ -3060,99 +3060,99 @@ fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) { } //todo!(test_transpose) -// #[gpui::test] -// fn test_transpose(cx: &mut TestAppContext) { -// init_test(cx, |_| {}); - -// _ = cx.add_window(|cx| { -// let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), cx); - -// editor.change_selections(None, cx, |s| s.select_ranges([1..1])); -// editor.transpose(&Default::default(), cx); -// assert_eq!(editor.text(cx), "bac"); -// assert_eq!(editor.selections.ranges(cx), [2..2]); - -// editor.transpose(&Default::default(), cx); -// assert_eq!(editor.text(cx), "bca"); -// assert_eq!(editor.selections.ranges(cx), [3..3]); - -// editor.transpose(&Default::default(), cx); -// assert_eq!(editor.text(cx), "bac"); -// assert_eq!(editor.selections.ranges(cx), [3..3]); +#[gpui::test] +fn test_transpose(cx: &mut TestAppContext) { + init_test(cx, |_| {}); -// editor -// }); + _ = cx.add_window(|cx| { + let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), cx); + editor.set_style(EditorStyle::default(), cx); + editor.change_selections(None, cx, |s| s.select_ranges([1..1])); + editor.transpose(&Default::default(), cx); + assert_eq!(editor.text(cx), "bac"); + assert_eq!(editor.selections.ranges(cx), [2..2]); -// _ = cx.add_window(|cx| { -// let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), cx); + editor.transpose(&Default::default(), cx); + assert_eq!(editor.text(cx), "bca"); + assert_eq!(editor.selections.ranges(cx), [3..3]); -// editor.change_selections(None, cx, |s| s.select_ranges([3..3])); -// editor.transpose(&Default::default(), cx); -// assert_eq!(editor.text(cx), "acb\nde"); -// assert_eq!(editor.selections.ranges(cx), [3..3]); + editor.transpose(&Default::default(), cx); + assert_eq!(editor.text(cx), "bac"); + assert_eq!(editor.selections.ranges(cx), [3..3]); -// editor.change_selections(None, cx, |s| s.select_ranges([4..4])); -// editor.transpose(&Default::default(), cx); -// assert_eq!(editor.text(cx), "acbd\ne"); -// assert_eq!(editor.selections.ranges(cx), [5..5]); + editor + }); -// editor.transpose(&Default::default(), cx); -// assert_eq!(editor.text(cx), "acbde\n"); -// assert_eq!(editor.selections.ranges(cx), [6..6]); + _ = cx.add_window(|cx| { + let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), cx); + editor.set_style(EditorStyle::default(), cx); + editor.change_selections(None, cx, |s| s.select_ranges([3..3])); + editor.transpose(&Default::default(), cx); + assert_eq!(editor.text(cx), "acb\nde"); + assert_eq!(editor.selections.ranges(cx), [3..3]); -// editor.transpose(&Default::default(), cx); -// assert_eq!(editor.text(cx), "acbd\ne"); -// assert_eq!(editor.selections.ranges(cx), [6..6]); + editor.change_selections(None, cx, |s| s.select_ranges([4..4])); + editor.transpose(&Default::default(), cx); + assert_eq!(editor.text(cx), "acbd\ne"); + assert_eq!(editor.selections.ranges(cx), [5..5]); -// editor -// }); + editor.transpose(&Default::default(), cx); + assert_eq!(editor.text(cx), "acbde\n"); + assert_eq!(editor.selections.ranges(cx), [6..6]); -// _ = cx.add_window(|cx| { -// let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), cx); + editor.transpose(&Default::default(), cx); + assert_eq!(editor.text(cx), "acbd\ne"); + assert_eq!(editor.selections.ranges(cx), [6..6]); -// editor.change_selections(None, cx, |s| s.select_ranges([1..1, 2..2, 4..4])); -// editor.transpose(&Default::default(), cx); -// assert_eq!(editor.text(cx), "bacd\ne"); -// assert_eq!(editor.selections.ranges(cx), [2..2, 3..3, 5..5]); + editor + }); -// editor.transpose(&Default::default(), cx); -// assert_eq!(editor.text(cx), "bcade\n"); -// assert_eq!(editor.selections.ranges(cx), [3..3, 4..4, 6..6]); + _ = cx.add_window(|cx| { + let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), cx); + editor.set_style(EditorStyle::default(), cx); + editor.change_selections(None, cx, |s| s.select_ranges([1..1, 2..2, 4..4])); + editor.transpose(&Default::default(), cx); + assert_eq!(editor.text(cx), "bacd\ne"); + assert_eq!(editor.selections.ranges(cx), [2..2, 3..3, 5..5]); -// editor.transpose(&Default::default(), cx); -// assert_eq!(editor.text(cx), "bcda\ne"); -// assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]); + editor.transpose(&Default::default(), cx); + assert_eq!(editor.text(cx), "bcade\n"); + assert_eq!(editor.selections.ranges(cx), [3..3, 4..4, 6..6]); -// editor.transpose(&Default::default(), cx); -// assert_eq!(editor.text(cx), "bcade\n"); -// assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]); + editor.transpose(&Default::default(), cx); + assert_eq!(editor.text(cx), "bcda\ne"); + assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]); -// editor.transpose(&Default::default(), cx); -// assert_eq!(editor.text(cx), "bcaed\n"); -// assert_eq!(editor.selections.ranges(cx), [5..5, 6..6]); + editor.transpose(&Default::default(), cx); + assert_eq!(editor.text(cx), "bcade\n"); + assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]); -// editor -// }); + editor.transpose(&Default::default(), cx); + assert_eq!(editor.text(cx), "bcaed\n"); + assert_eq!(editor.selections.ranges(cx), [5..5, 6..6]); -// _ = cx.add_window(|cx| { -// let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), cx); + editor + }); -// editor.change_selections(None, cx, |s| s.select_ranges([4..4])); -// editor.transpose(&Default::default(), cx); -// assert_eq!(editor.text(cx), "🏀🍐✋"); -// assert_eq!(editor.selections.ranges(cx), [8..8]); + _ = cx.add_window(|cx| { + let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), cx); + editor.set_style(EditorStyle::default(), cx); + editor.change_selections(None, cx, |s| s.select_ranges([4..4])); + editor.transpose(&Default::default(), cx); + assert_eq!(editor.text(cx), "🏀🍐✋"); + assert_eq!(editor.selections.ranges(cx), [8..8]); -// editor.transpose(&Default::default(), cx); -// assert_eq!(editor.text(cx), "🏀✋🍐"); -// assert_eq!(editor.selections.ranges(cx), [11..11]); + editor.transpose(&Default::default(), cx); + assert_eq!(editor.text(cx), "🏀✋🍐"); + assert_eq!(editor.selections.ranges(cx), [11..11]); -// editor.transpose(&Default::default(), cx); -// assert_eq!(editor.text(cx), "🏀🍐✋"); -// assert_eq!(editor.selections.ranges(cx), [11..11]); + editor.transpose(&Default::default(), cx); + assert_eq!(editor.text(cx), "🏀🍐✋"); + assert_eq!(editor.selections.ranges(cx), [11..11]); -// editor -// }); -// } + editor + }); +} #[gpui::test] async fn test_clipboard(cx: &mut gpui::TestAppContext) { From b3e741b397d608ef8dade1103081302824a9fcf6 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 4 Dec 2023 13:47:50 +0100 Subject: [PATCH 17/35] Fix up test_highlighted_ranges Returned highlights were okay, but the test was trying to normalize the output by sorting the highlights by color. The ordering is different between gpui1 Color and gpui2 Hsla. --- crates/editor2/src/editor_tests.rs | 158 ++++++++++++++--------------- 1 file changed, 79 insertions(+), 79 deletions(-) diff --git a/crates/editor2/src/editor_tests.rs b/crates/editor2/src/editor_tests.rs index 3c06bb23443ca1428a5b7e07c8d332bf683c2e82..4925e7c965ad21f3be0886c96f14b1482d7c2978 100644 --- a/crates/editor2/src/editor_tests.rs +++ b/crates/editor2/src/editor_tests.rs @@ -6327,88 +6327,88 @@ async fn test_extra_newline_insertion(cx: &mut gpui::TestAppContext) { }); } -// #[gpui::test] -// fn test_highlighted_ranges(cx: &mut TestAppContext) { -// init_test(cx, |_| {}); +#[gpui::test] +fn test_highlighted_ranges(cx: &mut TestAppContext) { + init_test(cx, |_| {}); -// let editor = cx.add_window(|cx| { -// let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx); -// build_editor(buffer.clone(), cx) -// }); + let editor = cx.add_window(|cx| { + let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx); + build_editor(buffer.clone(), cx) + }); -// editor.update(cx, |editor, cx| { -// struct Type1; -// struct Type2; - -// let buffer = editor.buffer.read(cx).snapshot(cx); - -// let anchor_range = -// |range: Range| buffer.anchor_after(range.start)..buffer.anchor_after(range.end); - -// editor.highlight_background::( -// vec![ -// anchor_range(Point::new(2, 1)..Point::new(2, 3)), -// anchor_range(Point::new(4, 2)..Point::new(4, 4)), -// anchor_range(Point::new(6, 3)..Point::new(6, 5)), -// anchor_range(Point::new(8, 4)..Point::new(8, 6)), -// ], -// |_| Hsla::red(), -// cx, -// ); -// editor.highlight_background::( -// vec![ -// anchor_range(Point::new(3, 2)..Point::new(3, 5)), -// anchor_range(Point::new(5, 3)..Point::new(5, 6)), -// anchor_range(Point::new(7, 4)..Point::new(7, 7)), -// anchor_range(Point::new(9, 5)..Point::new(9, 8)), -// ], -// |_| Hsla::green(), -// cx, -// ); + editor.update(cx, |editor, cx| { + struct Type1; + struct Type2; -// let snapshot = editor.snapshot(cx); -// let mut highlighted_ranges = editor.background_highlights_in_range( -// anchor_range(Point::new(3, 4)..Point::new(7, 4)), -// &snapshot, -// cx.theme().colors(), -// ); -// // Enforce a consistent ordering based on color without relying on the ordering of the -// // highlight's `TypeId` which is non-executor. -// highlighted_ranges.sort_unstable_by_key(|(_, color)| *color); -// assert_eq!( -// highlighted_ranges, -// &[ -// ( -// DisplayPoint::new(3, 2)..DisplayPoint::new(3, 5), -// Hsla::green(), -// ), -// ( -// DisplayPoint::new(5, 3)..DisplayPoint::new(5, 6), -// Hsla::green(), -// ), -// ( -// DisplayPoint::new(4, 2)..DisplayPoint::new(4, 4), -// Hsla::red(), -// ), -// ( -// DisplayPoint::new(6, 3)..DisplayPoint::new(6, 5), -// Hsla::red(), -// ), -// ] -// ); -// assert_eq!( -// editor.background_highlights_in_range( -// anchor_range(Point::new(5, 6)..Point::new(6, 4)), -// &snapshot, -// cx.theme().colors(), -// ), -// &[( -// DisplayPoint::new(6, 3)..DisplayPoint::new(6, 5), -// Hsla::red(), -// )] -// ); -// }); -// } + let buffer = editor.buffer.read(cx).snapshot(cx); + + let anchor_range = + |range: Range| buffer.anchor_after(range.start)..buffer.anchor_after(range.end); + + editor.highlight_background::( + vec![ + anchor_range(Point::new(2, 1)..Point::new(2, 3)), + anchor_range(Point::new(4, 2)..Point::new(4, 4)), + anchor_range(Point::new(6, 3)..Point::new(6, 5)), + anchor_range(Point::new(8, 4)..Point::new(8, 6)), + ], + |_| Hsla::red(), + cx, + ); + editor.highlight_background::( + vec![ + anchor_range(Point::new(3, 2)..Point::new(3, 5)), + anchor_range(Point::new(5, 3)..Point::new(5, 6)), + anchor_range(Point::new(7, 4)..Point::new(7, 7)), + anchor_range(Point::new(9, 5)..Point::new(9, 8)), + ], + |_| Hsla::green(), + cx, + ); + + let snapshot = editor.snapshot(cx); + let mut highlighted_ranges = editor.background_highlights_in_range( + anchor_range(Point::new(3, 4)..Point::new(7, 4)), + &snapshot, + cx.theme().colors(), + ); + // Enforce a consistent ordering based on color without relying on the ordering of the + // highlight's `TypeId` which is non-executor. + highlighted_ranges.sort_unstable_by_key(|(_, color)| *color); + assert_eq!( + highlighted_ranges, + &[ + ( + DisplayPoint::new(4, 2)..DisplayPoint::new(4, 4), + Hsla::red(), + ), + ( + DisplayPoint::new(6, 3)..DisplayPoint::new(6, 5), + Hsla::red(), + ), + ( + DisplayPoint::new(3, 2)..DisplayPoint::new(3, 5), + Hsla::green(), + ), + ( + DisplayPoint::new(5, 3)..DisplayPoint::new(5, 6), + Hsla::green(), + ), + ] + ); + assert_eq!( + editor.background_highlights_in_range( + anchor_range(Point::new(5, 6)..Point::new(6, 4)), + &snapshot, + cx.theme().colors(), + ), + &[( + DisplayPoint::new(6, 3)..DisplayPoint::new(6, 5), + Hsla::red(), + )] + ); + }); +} // todo!(following) #[gpui::test] From 3b1a0652ae36b855dae912374cfbd5fe14e9f0ff Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 4 Dec 2023 14:50:10 +0100 Subject: [PATCH 18/35] inlay hints: Relax the test condition. We've investigated another spurious failure, this time with test_multiple_excerpts_large_multibuffer; sadly it didn't really get us anywhere, so for now we're relaxing an assert. Co-authored-by: Kirill --- crates/editor2/src/inlay_hint_cache.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/editor2/src/inlay_hint_cache.rs b/crates/editor2/src/inlay_hint_cache.rs index c3722e214cf79cf518cbc614978ca098bb97fee8..18a061276e9e5e5f44056dfaf9d8edfc5b6ec7c5 100644 --- a/crates/editor2/src/inlay_hint_cache.rs +++ b/crates/editor2/src/inlay_hint_cache.rs @@ -2737,8 +2737,8 @@ pub mod tests { let current_cache_version = editor.inlay_hint_cache().version; let minimum_expected_version = last_scroll_update_version + expected_hints.len(); assert!( - current_cache_version == minimum_expected_version || current_cache_version == minimum_expected_version + 1, - "Due to every excerpt having one hint, cache should update per new excerpt received + 1 potential sporadic update" + current_cache_version >= minimum_expected_version, + "TODO: Something happens with multi-excerpt buffer when editing it: we query overly many inlay hints instead of just visible excerpts" ); }); } From ff734d494f9ecb53d6d869e09c1dd3c2d52f987b Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 4 Dec 2023 15:06:56 +0100 Subject: [PATCH 19/35] uncomment and augment mouse_context_menu tests --- crates/editor2/src/mouse_context_menu.rs | 71 ++++++++++++------------ 1 file changed, 36 insertions(+), 35 deletions(-) diff --git a/crates/editor2/src/mouse_context_menu.rs b/crates/editor2/src/mouse_context_menu.rs index fdeec9110b97ff5c23b945bf31da590bbe8a30dc..ba989ab011714384627c47f5dc017aa3511dfa18 100644 --- a/crates/editor2/src/mouse_context_menu.rs +++ b/crates/editor2/src/mouse_context_menu.rs @@ -69,42 +69,43 @@ pub fn deploy_context_menu( cx.notify(); } -// #[cfg(test)] -// mod tests { -// use super::*; -// use crate::{editor_tests::init_test, test::editor_lsp_test_context::EditorLspTestContext}; -// use indoc::indoc; +#[cfg(test)] +mod tests { + use super::*; + use crate::{editor_tests::init_test, test::editor_lsp_test_context::EditorLspTestContext}; + use indoc::indoc; -// #[gpui::test] -// async fn test_mouse_context_menu(cx: &mut gpui::TestAppContext) { -// init_test(cx, |_| {}); + #[gpui::test] + async fn test_mouse_context_menu(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); -// let mut cx = EditorLspTestContext::new_rust( -// lsp::ServerCapabilities { -// hover_provider: Some(lsp::HoverProviderCapability::Simple(true)), -// ..Default::default() -// }, -// cx, -// ) -// .await; + let mut cx = EditorLspTestContext::new_rust( + lsp::ServerCapabilities { + hover_provider: Some(lsp::HoverProviderCapability::Simple(true)), + ..Default::default() + }, + cx, + ) + .await; -// cx.set_state(indoc! {" -// fn teˇst() { -// do_work(); -// } -// "}); -// let point = cx.display_point(indoc! {" -// fn test() { -// do_wˇork(); -// } -// "}); -// cx.update_editor(|editor, cx| deploy_context_menu(editor, Default::default(), point, cx)); + cx.set_state(indoc! {" + fn teˇst() { + do_work(); + } + "}); + let point = cx.display_point(indoc! {" + fn test() { + do_wˇork(); + } + "}); + cx.editor(|editor, app| assert!(editor.mouse_context_menu.is_none())); + cx.update_editor(|editor, cx| deploy_context_menu(editor, Default::default(), point, cx)); -// cx.assert_editor_state(indoc! {" -// fn test() { -// do_wˇork(); -// } -// "}); -// cx.editor(|editor, app| assert!(editor.mouse_context_menu.read(app).visible())); -// } -// } + cx.assert_editor_state(indoc! {" + fn test() { + do_wˇork(); + } + "}); + cx.editor(|editor, app| assert!(editor.mouse_context_menu.is_some())); + } +} From 24b08921febdd6ae9168a85ae637de601b6e9547 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 4 Dec 2023 15:22:29 +0100 Subject: [PATCH 20/35] Revert "fix display map tests" This reverts commit 0f7fc8c1a03eaec347df101ada66f28aefa3162a. --- crates/editor2/src/display_map.rs | 24 ++++++++----------- crates/gpui2/src/app/test_context.rs | 35 +++++++++++++--------------- crates/project2/src/worktree.rs | 14 ++++------- 3 files changed, 31 insertions(+), 42 deletions(-) diff --git a/crates/editor2/src/display_map.rs b/crates/editor2/src/display_map.rs index 74890d9edb4bced394ce9363856dfbf3096f8cff..1aee04dd0ae02b8d4ea98025be177a82e3801ef7 100644 --- a/crates/editor2/src/display_map.rs +++ b/crates/editor2/src/display_map.rs @@ -1467,12 +1467,10 @@ pub mod tests { cx.update(|cx| init_test(cx, |s| s.defaults.tab_size = Some(2.try_into().unwrap()))); - let buffer = cx.build_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), text)); - let condition = cx.condition(&buffer, |buf, _| !buf.is_parsing()); - cx.update_model(&buffer, |this, cx| { - this.set_language(Some(language), cx); + let buffer = cx.build_model(|cx| { + Buffer::new(0, cx.entity_id().as_u64(), text).with_language(language, cx) }); - condition.await; + cx.condition(&buffer, |buf, _| !buf.is_parsing()).await; let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx)); let font_size = px(14.0); @@ -1556,12 +1554,10 @@ pub mod tests { cx.update(|cx| init_test(cx, |_| {})); - let buffer = cx.build_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), text)); - let condition = cx.condition(&buffer, |buf, _| !buf.is_parsing()); - buffer.update(cx, |this, cx| { - this.set_language(Some(language), cx); + let buffer = cx.build_model(|cx| { + Buffer::new(0, cx.entity_id().as_u64(), text).with_language(language, cx) }); - condition.await; + cx.condition(&buffer, |buf, _| !buf.is_parsing()).await; let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx)); let font_size = px(16.0); @@ -1625,10 +1621,10 @@ pub mod tests { let (text, highlighted_ranges) = marked_text_ranges(r#"constˇ «a»: B = "c «d»""#, false); - let buffer = cx.build_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), text)); - let condition = cx.condition(&buffer, |buf, _| !buf.is_parsing()); - buffer.update(cx, |this, cx| this.set_language(Some(language), cx)); - condition.await; + let buffer = cx.build_model(|cx| { + Buffer::new(0, cx.entity_id().as_u64(), text).with_language(language, cx) + }); + cx.condition(&buffer, |buf, _| !buf.is_parsing()).await; let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx)); let buffer_snapshot = buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx)); diff --git a/crates/gpui2/src/app/test_context.rs b/crates/gpui2/src/app/test_context.rs index 5c2bf65c02ea1e513cca62b5f69c29c7273a157a..c915753749f4f85bb75cf1440df0775b232d7fa4 100644 --- a/crates/gpui2/src/app/test_context.rs +++ b/crates/gpui2/src/app/test_context.rs @@ -8,7 +8,6 @@ use crate::{ use anyhow::{anyhow, bail}; use futures::{Stream, StreamExt}; use std::{future::Future, ops::Deref, rc::Rc, sync::Arc, time::Duration}; -use util::ResultExt; #[derive(Clone)] pub struct TestAppContext { @@ -298,7 +297,7 @@ impl TestAppContext { .unwrap() } - pub fn notifications(&self, entity: &impl Entity) -> impl Stream { + pub fn notifications(&mut self, entity: &impl Entity) -> impl Stream { let (tx, rx) = futures::channel::mpsc::unbounded(); self.update(|cx| { cx.observe(entity, { @@ -308,7 +307,7 @@ impl TestAppContext { } }) .detach(); - cx.observe_release(entity, move |_, _| dbg!(tx.close_channel())) + cx.observe_release(entity, move |_, _| tx.close_channel()) .detach() }); rx @@ -332,30 +331,28 @@ impl TestAppContext { rx } - pub fn condition( - &self, + pub async fn condition( + &mut self, model: &Model, - mut predicate: impl FnMut(&mut T, &mut ModelContext) -> bool + Send + 'static, - ) -> Task<()> { + mut predicate: impl FnMut(&mut T, &mut ModelContext) -> bool, + ) { let timer = self.executor().timer(Duration::from_secs(3)); let mut notifications = self.notifications(model); use futures::FutureExt as _; use smol::future::FutureExt as _; - let model = model.clone(); - self.spawn(move |mut cx| async move { - async move { - while notifications.next().await.is_some() { - if model.update(&mut cx, &mut predicate).log_err().unwrap() { - return Ok(()); - } + + async { + while notifications.next().await.is_some() { + if model.update(self, &mut predicate) { + return Ok(()); } - bail!("model dropped") } - .race(timer.map(|_| Err(anyhow!("condition timed out")))) - .await - .unwrap() - }) + bail!("model dropped") + } + .race(timer.map(|_| Err(anyhow!("condition timed out")))) + .await + .unwrap(); } } diff --git a/crates/project2/src/worktree.rs b/crates/project2/src/worktree.rs index 86cfc6737411aba10d8de8826e855c2bd8342a9d..e424375220c1c7aa2292d9a4762d7581ea67222e 100644 --- a/crates/project2/src/worktree.rs +++ b/crates/project2/src/worktree.rs @@ -4187,7 +4187,7 @@ impl WorktreeModelHandle for Model { &self, cx: &'a mut gpui::TestAppContext, ) -> futures::future::LocalBoxFuture<'a, ()> { - let file_name: &'static str = "fs-event-sentinel"; + let file_name = "fs-event-sentinel"; let tree = self.clone(); let (fs, root_path) = self.update(cx, |tree, _| { @@ -4200,18 +4200,14 @@ impl WorktreeModelHandle for Model { .await .unwrap(); - cx.condition(&tree, move |tree, _| { - tree.entry_for_path(file_name).is_some() - }) - .await; + cx.condition(&tree, |tree, _| tree.entry_for_path(file_name).is_some()) + .await; fs.remove_file(&root_path.join(file_name), Default::default()) .await .unwrap(); - cx.condition(&tree, move |tree, _| { - tree.entry_for_path(file_name).is_none() - }) - .await; + cx.condition(&tree, |tree, _| tree.entry_for_path(file_name).is_none()) + .await; cx.update(|cx| tree.read(cx).as_local().unwrap().scan_complete()) .await; From b9a917f42a932f75d94c03166935eee8f233790e Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 4 Dec 2023 15:31:35 +0100 Subject: [PATCH 21/35] Fix up condition not checking the condition at the start. Co-authored-by: Antonio --- crates/gpui2/src/app/test_context.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/crates/gpui2/src/app/test_context.rs b/crates/gpui2/src/app/test_context.rs index c915753749f4f85bb75cf1440df0775b232d7fa4..908a418635bfe7262b69a6ced0344c4484bd89dd 100644 --- a/crates/gpui2/src/app/test_context.rs +++ b/crates/gpui2/src/app/test_context.rs @@ -343,12 +343,15 @@ impl TestAppContext { use smol::future::FutureExt as _; async { - while notifications.next().await.is_some() { + loop { if model.update(self, &mut predicate) { return Ok(()); } + + if notifications.next().await.is_none() { + bail!("model dropped") + } } - bail!("model dropped") } .race(timer.map(|_| Err(anyhow!("condition timed out")))) .await From 2108ddf6215a510cb1722d3f7c3be1d05463769a Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 4 Dec 2023 16:15:35 +0100 Subject: [PATCH 22/35] Defer activating Subscriptions that are invoked as a part of an effect. Fixes test test_edit_events. Co-authored-by: Antonio --- crates/gpui2/src/app.rs | 37 ++++++++----- crates/gpui2/src/app/model_context.rs | 24 ++++++--- crates/gpui2/src/subscription.rs | 50 ++++++++++++++--- crates/gpui2/src/window.rs | 78 ++++++++++++++++++--------- 4 files changed, 135 insertions(+), 54 deletions(-) diff --git a/crates/gpui2/src/app.rs b/crates/gpui2/src/app.rs index 94a7d3be0b8b2cf239d29f1a29d7b6bb0b7d2bbf..fec6f150f6c341f916e0173379aba63bebcc1ffd 100644 --- a/crates/gpui2/src/app.rs +++ b/crates/gpui2/src/app.rs @@ -358,7 +358,7 @@ impl AppContext { { let entity_id = entity.entity_id(); let handle = entity.downgrade(); - self.observers.insert( + let (subscription, activate) = self.observers.insert( entity_id, Box::new(move |cx| { if let Some(handle) = E::upgrade_from(&handle) { @@ -367,7 +367,9 @@ impl AppContext { false } }), - ) + ); + self.defer(move |_| activate()); + subscription } pub fn subscribe( @@ -398,8 +400,7 @@ impl AppContext { { let entity_id = entity.entity_id(); let entity = entity.downgrade(); - - self.event_listeners.insert( + let (subscription, activate) = self.event_listeners.insert( entity_id, ( TypeId::of::(), @@ -412,7 +413,9 @@ impl AppContext { } }), ), - ) + ); + self.defer(move |_| activate()); + subscription } pub fn windows(&self) -> Vec { @@ -873,13 +876,15 @@ impl AppContext { &mut self, mut f: impl FnMut(&mut Self) + 'static, ) -> Subscription { - self.global_observers.insert( + let (subscription, activate) = self.global_observers.insert( TypeId::of::(), Box::new(move |cx| { f(cx); true }), - ) + ); + self.defer(move |_| activate()); + subscription } /// Move the global of the given type to the stack. @@ -903,7 +908,7 @@ impl AppContext { &mut self, on_new: impl 'static + Fn(&mut V, &mut ViewContext), ) -> Subscription { - self.new_view_observers.insert( + let (subscription, activate) = self.new_view_observers.insert( TypeId::of::(), Box::new(move |any_view: AnyView, cx: &mut WindowContext| { any_view @@ -913,7 +918,9 @@ impl AppContext { on_new(view_state, cx); }) }), - ) + ); + activate(); + subscription } pub fn observe_release( @@ -925,13 +932,15 @@ impl AppContext { E: Entity, T: 'static, { - self.release_listeners.insert( + let (subscription, activate) = self.release_listeners.insert( handle.entity_id(), Box::new(move |entity, cx| { let entity = entity.downcast_mut().expect("invalid entity type"); on_release(entity, cx) }), - ) + ); + activate(); + subscription } pub(crate) fn push_text_style(&mut self, text_style: TextStyleRefinement) { @@ -996,13 +1005,15 @@ impl AppContext { where Fut: 'static + Future, { - self.quit_observers.insert( + let (subscription, activate) = self.quit_observers.insert( (), Box::new(move |cx| { let future = on_quit(cx); async move { future.await }.boxed_local() }), - ) + ); + activate(); + subscription } } diff --git a/crates/gpui2/src/app/model_context.rs b/crates/gpui2/src/app/model_context.rs index d04f0f22891582c8b90b124ae08756a1a95922c6..26feb2fd1befc604f7bda5b5e5a362a5a3c52dd1 100644 --- a/crates/gpui2/src/app/model_context.rs +++ b/crates/gpui2/src/app/model_context.rs @@ -88,13 +88,15 @@ impl<'a, T: 'static> ModelContext<'a, T> { where T: 'static, { - self.app.release_listeners.insert( + let (subscription, activate) = self.app.release_listeners.insert( self.model_state.entity_id, Box::new(move |this, cx| { let this = this.downcast_mut().expect("invalid entity type"); on_release(this, cx); }), - ) + ); + activate(); + subscription } pub fn observe_release( @@ -109,7 +111,7 @@ impl<'a, T: 'static> ModelContext<'a, T> { { let entity_id = entity.entity_id(); let this = self.weak_model(); - self.app.release_listeners.insert( + let (subscription, activate) = self.app.release_listeners.insert( entity_id, Box::new(move |entity, cx| { let entity = entity.downcast_mut().expect("invalid entity type"); @@ -117,7 +119,9 @@ impl<'a, T: 'static> ModelContext<'a, T> { this.update(cx, |this, cx| on_release(this, entity, cx)); } }), - ) + ); + activate(); + subscription } pub fn observe_global( @@ -128,10 +132,12 @@ impl<'a, T: 'static> ModelContext<'a, T> { T: 'static, { let handle = self.weak_model(); - self.global_observers.insert( + let (subscription, activate) = self.global_observers.insert( TypeId::of::(), Box::new(move |cx| handle.update(cx, |view, cx| f(view, cx)).is_ok()), - ) + ); + self.defer(move |_| activate()); + subscription } pub fn on_app_quit( @@ -143,7 +149,7 @@ impl<'a, T: 'static> ModelContext<'a, T> { T: 'static, { let handle = self.weak_model(); - self.app.quit_observers.insert( + let (subscription, activate) = self.app.quit_observers.insert( (), Box::new(move |cx| { let future = handle.update(cx, |entity, cx| on_quit(entity, cx)).ok(); @@ -154,7 +160,9 @@ impl<'a, T: 'static> ModelContext<'a, T> { } .boxed_local() }), - ) + ); + activate(); + subscription } pub fn notify(&mut self) { diff --git a/crates/gpui2/src/subscription.rs b/crates/gpui2/src/subscription.rs index 7cb023a9074094b27d16b6effa27c68b967f30c8..867c83fcbb31527db4be5d1c912b013a35872290 100644 --- a/crates/gpui2/src/subscription.rs +++ b/crates/gpui2/src/subscription.rs @@ -1,6 +1,6 @@ use collections::{BTreeMap, BTreeSet}; use parking_lot::Mutex; -use std::{fmt::Debug, mem, sync::Arc}; +use std::{cell::Cell, fmt::Debug, mem, rc::Rc, sync::Arc}; use util::post_inc; pub(crate) struct SubscriberSet( @@ -14,11 +14,16 @@ impl Clone for SubscriberSet { } struct SubscriberSetState { - subscribers: BTreeMap>>, + subscribers: BTreeMap>>>, dropped_subscribers: BTreeSet<(EmitterKey, usize)>, next_subscriber_id: usize, } +struct Subscriber { + active: Rc>, + callback: Callback, +} + impl SubscriberSet where EmitterKey: 'static + Ord + Clone + Debug, @@ -32,16 +37,33 @@ where }))) } - pub fn insert(&self, emitter_key: EmitterKey, callback: Callback) -> Subscription { + /// Inserts a new `[Subscription]` for the given `emitter_key`. By default, subscriptions + /// are inert, meaning that they won't be listed when calling `[SubscriberSet::remove]` or `[SubscriberSet::retain]`. + /// This method returns a tuple of a `[Subscription]` and an `impl FnOnce`, and you can use the latter + /// to activate the `[Subscription]`. + #[must_use] + pub fn insert( + &self, + emitter_key: EmitterKey, + callback: Callback, + ) -> (Subscription, impl FnOnce()) { + let active = Rc::new(Cell::new(false)); let mut lock = self.0.lock(); let subscriber_id = post_inc(&mut lock.next_subscriber_id); lock.subscribers .entry(emitter_key.clone()) .or_default() .get_or_insert_with(|| Default::default()) - .insert(subscriber_id, callback); + .insert( + subscriber_id, + Subscriber { + active: active.clone(), + callback, + }, + ); let this = self.0.clone(); - Subscription { + + let subscription = Subscription { unsubscribe: Some(Box::new(move || { let mut lock = this.lock(); let Some(subscribers) = lock.subscribers.get_mut(&emitter_key) else { @@ -63,7 +85,8 @@ where lock.dropped_subscribers .insert((emitter_key, subscriber_id)); })), - } + }; + (subscription, move || active.set(true)) } pub fn remove(&self, emitter: &EmitterKey) -> impl IntoIterator { @@ -73,6 +96,13 @@ where .map(|s| s.into_values()) .into_iter() .flatten() + .filter_map(|subscriber| { + if subscriber.active.get() { + Some(subscriber.callback) + } else { + None + } + }) } /// Call the given callback for each subscriber to the given emitter. @@ -91,7 +121,13 @@ where return; }; - subscribers.retain(|_, callback| f(callback)); + subscribers.retain(|_, subscriber| { + if subscriber.active.get() { + f(&mut subscriber.callback) + } else { + true + } + }); let mut lock = self.0.lock(); // Add any new subscribers that were added while invoking the callback. diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index 5724f1e0701a2b960afb478fad0186649c29debd..40594a71875f8bf826ef3b47df656db5bd8a7ff7 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -490,7 +490,7 @@ impl<'a> WindowContext<'a> { let entity_id = entity.entity_id(); let entity = entity.downgrade(); let window_handle = self.window.handle; - self.app.event_listeners.insert( + let (subscription, activate) = self.app.event_listeners.insert( entity_id, ( TypeId::of::(), @@ -508,7 +508,9 @@ impl<'a> WindowContext<'a> { .unwrap_or(false) }), ), - ) + ); + self.app.defer(move |_| activate()); + subscription } /// Create an `AsyncWindowContext`, which has a static lifetime and can be held across @@ -1453,10 +1455,12 @@ impl<'a> WindowContext<'a> { f: impl Fn(&mut WindowContext<'_>) + 'static, ) -> Subscription { let window_handle = self.window.handle; - self.global_observers.insert( + let (subscription, activate) = self.global_observers.insert( TypeId::of::(), Box::new(move |cx| window_handle.update(cx, |_, cx| f(cx)).is_ok()), - ) + ); + self.app.defer(move |_| activate()); + subscription } pub fn activate_window(&self) { @@ -2096,7 +2100,7 @@ impl<'a, V: 'static> ViewContext<'a, V> { let entity_id = entity.entity_id(); let entity = entity.downgrade(); let window_handle = self.window.handle; - self.app.observers.insert( + let (subscription, activate) = self.app.observers.insert( entity_id, Box::new(move |cx| { window_handle @@ -2110,7 +2114,9 @@ impl<'a, V: 'static> ViewContext<'a, V> { }) .unwrap_or(false) }), - ) + ); + self.app.defer(move |_| activate()); + subscription } pub fn subscribe( @@ -2127,7 +2133,7 @@ impl<'a, V: 'static> ViewContext<'a, V> { let entity_id = entity.entity_id(); let handle = entity.downgrade(); let window_handle = self.window.handle; - self.app.event_listeners.insert( + let (subscription, activate) = self.app.event_listeners.insert( entity_id, ( TypeId::of::(), @@ -2145,7 +2151,9 @@ impl<'a, V: 'static> ViewContext<'a, V> { .unwrap_or(false) }), ), - ) + ); + self.app.defer(move |_| activate()); + subscription } pub fn on_release( @@ -2153,13 +2161,15 @@ impl<'a, V: 'static> ViewContext<'a, V> { on_release: impl FnOnce(&mut V, &mut WindowContext) + 'static, ) -> Subscription { let window_handle = self.window.handle; - self.app.release_listeners.insert( + let (subscription, activate) = self.app.release_listeners.insert( self.view.model.entity_id, Box::new(move |this, cx| { let this = this.downcast_mut().expect("invalid entity type"); let _ = window_handle.update(cx, |_, cx| on_release(this, cx)); }), - ) + ); + activate(); + subscription } pub fn observe_release( @@ -2175,7 +2185,7 @@ impl<'a, V: 'static> ViewContext<'a, V> { let view = self.view().downgrade(); let entity_id = entity.entity_id(); let window_handle = self.window.handle; - self.app.release_listeners.insert( + let (subscription, activate) = self.app.release_listeners.insert( entity_id, Box::new(move |entity, cx| { let entity = entity.downcast_mut().expect("invalid entity type"); @@ -2183,7 +2193,9 @@ impl<'a, V: 'static> ViewContext<'a, V> { view.update(cx, |this, cx| on_release(this, entity, cx)) }); }), - ) + ); + activate(); + subscription } pub fn notify(&mut self) { @@ -2198,10 +2210,12 @@ impl<'a, V: 'static> ViewContext<'a, V> { mut callback: impl FnMut(&mut V, &mut ViewContext) + 'static, ) -> Subscription { let view = self.view.downgrade(); - self.window.bounds_observers.insert( + let (subscription, activate) = self.window.bounds_observers.insert( (), Box::new(move |cx| view.update(cx, |view, cx| callback(view, cx)).is_ok()), - ) + ); + activate(); + subscription } pub fn observe_window_activation( @@ -2209,10 +2223,12 @@ impl<'a, V: 'static> ViewContext<'a, V> { mut callback: impl FnMut(&mut V, &mut ViewContext) + 'static, ) -> Subscription { let view = self.view.downgrade(); - self.window.activation_observers.insert( + let (subscription, activate) = self.window.activation_observers.insert( (), Box::new(move |cx| view.update(cx, |view, cx| callback(view, cx)).is_ok()), - ) + ); + activate(); + subscription } /// Register a listener to be called when the given focus handle receives focus. @@ -2225,7 +2241,7 @@ impl<'a, V: 'static> ViewContext<'a, V> { ) -> Subscription { let view = self.view.downgrade(); let focus_id = handle.id; - self.window.focus_listeners.insert( + let (subscription, activate) = self.window.focus_listeners.insert( (), Box::new(move |event, cx| { view.update(cx, |view, cx| { @@ -2235,7 +2251,9 @@ impl<'a, V: 'static> ViewContext<'a, V> { }) .is_ok() }), - ) + ); + self.app.defer(move |_| activate()); + subscription } /// Register a listener to be called when the given focus handle or one of its descendants receives focus. @@ -2248,7 +2266,7 @@ impl<'a, V: 'static> ViewContext<'a, V> { ) -> Subscription { let view = self.view.downgrade(); let focus_id = handle.id; - self.window.focus_listeners.insert( + let (subscription, activate) = self.window.focus_listeners.insert( (), Box::new(move |event, cx| { view.update(cx, |view, cx| { @@ -2262,7 +2280,9 @@ impl<'a, V: 'static> ViewContext<'a, V> { }) .is_ok() }), - ) + ); + self.app.defer(move |_| activate()); + subscription } /// Register a listener to be called when the given focus handle loses focus. @@ -2275,7 +2295,7 @@ impl<'a, V: 'static> ViewContext<'a, V> { ) -> Subscription { let view = self.view.downgrade(); let focus_id = handle.id; - self.window.focus_listeners.insert( + let (subscription, activate) = self.window.focus_listeners.insert( (), Box::new(move |event, cx| { view.update(cx, |view, cx| { @@ -2285,7 +2305,9 @@ impl<'a, V: 'static> ViewContext<'a, V> { }) .is_ok() }), - ) + ); + self.app.defer(move |_| activate()); + subscription } /// Register a listener to be called when the given focus handle or one of its descendants loses focus. @@ -2298,7 +2320,7 @@ impl<'a, V: 'static> ViewContext<'a, V> { ) -> Subscription { let view = self.view.downgrade(); let focus_id = handle.id; - self.window.focus_listeners.insert( + let (subscription, activate) = self.window.focus_listeners.insert( (), Box::new(move |event, cx| { view.update(cx, |view, cx| { @@ -2312,7 +2334,9 @@ impl<'a, V: 'static> ViewContext<'a, V> { }) .is_ok() }), - ) + ); + self.app.defer(move |_| activate()); + subscription } pub fn spawn( @@ -2343,14 +2367,16 @@ impl<'a, V: 'static> ViewContext<'a, V> { ) -> Subscription { let window_handle = self.window.handle; let view = self.view().downgrade(); - self.global_observers.insert( + let (subscription, activate) = self.global_observers.insert( TypeId::of::(), Box::new(move |cx| { window_handle .update(cx, |_, cx| view.update(cx, |view, cx| f(view, cx)).is_ok()) .unwrap_or(false) }), - ) + ); + self.app.defer(move |_| activate()); + subscription } pub fn on_mouse_event( From fd2f1c25940353a26d7b35dba81d86905812c1f8 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 4 Dec 2023 16:26:34 +0100 Subject: [PATCH 23/35] Fix up copilot2 test Co-authored-by: Antonio --- crates/copilot2/src/copilot2.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/crates/copilot2/src/copilot2.rs b/crates/copilot2/src/copilot2.rs index 01ee77369adce1bfe29d242ef79ffb4c2b570967..b2454728644b212c8736b49876b213123cf039e8 100644 --- a/crates/copilot2/src/copilot2.rs +++ b/crates/copilot2/src/copilot2.rs @@ -1108,14 +1108,14 @@ mod tests { lsp.receive_notification::() .await, lsp::DidCloseTextDocumentParams { - text_document: lsp::TextDocumentIdentifier::new(buffer_2_uri.clone()), + text_document: lsp::TextDocumentIdentifier::new(buffer_1_uri.clone()), } ); assert_eq!( lsp.receive_notification::() .await, lsp::DidCloseTextDocumentParams { - text_document: lsp::TextDocumentIdentifier::new(buffer_1_uri.clone()), + text_document: lsp::TextDocumentIdentifier::new(buffer_2_uri.clone()), } ); @@ -1129,15 +1129,16 @@ mod tests { .update(cx, |copilot, cx| copilot.sign_in(cx)) .await .unwrap(); + assert_eq!( lsp.receive_notification::() .await, lsp::DidOpenTextDocumentParams { text_document: lsp::TextDocumentItem::new( - buffer_2_uri.clone(), + buffer_1_uri.clone(), "plaintext".into(), 0, - "Goodbye".into() + "Hello world".into() ), } ); @@ -1146,14 +1147,13 @@ mod tests { .await, lsp::DidOpenTextDocumentParams { text_document: lsp::TextDocumentItem::new( - buffer_1_uri.clone(), + buffer_2_uri.clone(), "plaintext".into(), 0, - "Hello world".into() + "Goodbye".into() ), } ); - // Dropping a buffer causes it to be closed on the LSP side as well. cx.update(|_| drop(buffer_2)); assert_eq!( From 68d309e79cd921a994efd0935887e9f31f80a166 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 4 Dec 2023 16:39:40 +0100 Subject: [PATCH 24/35] Fix disparity between editor2 and edito1 wrt copilot completions. Fixes test test_copilot. Co-authored-by: Antonio --- crates/editor2/src/editor.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/editor2/src/editor.rs b/crates/editor2/src/editor.rs index c7141fe5879505dd67782dabecb581113c0c52ed..87fa9875d4553589546ba037375753fc2649b426 100644 --- a/crates/editor2/src/editor.rs +++ b/crates/editor2/src/editor.rs @@ -3489,7 +3489,7 @@ impl Editor { drop(context_menu); this.discard_copilot_suggestion(cx); cx.notify(); - } else if this.completion_tasks.is_empty() { + } else if this.completion_tasks.len() <= 1 { // If there are no more completion tasks and the last menu was // empty, we should hide it. If it was already hidden, we should // also show the copilot suggestion when available. From b5924d6b1164e741c9967747f74de86b55271edd Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 4 Dec 2023 17:27:48 +0100 Subject: [PATCH 25/35] Add simulate_window_resize. Fixes up tests for movement in editor/scrolling. Co-authored-by: Antonio --- crates/editor2/src/editor.rs | 5 + crates/editor2/src/editor_tests.rs | 663 ++++++++++++----------- crates/editor2/src/git.rs | 2 +- crates/gpui2/src/app/test_context.rs | 49 +- crates/gpui2/src/executor.rs | 15 +- crates/gpui2/src/platform.rs | 2 +- crates/gpui2/src/platform/test/window.rs | 4 +- crates/gpui2/src/style.rs | 3 +- 8 files changed, 404 insertions(+), 339 deletions(-) diff --git a/crates/editor2/src/editor.rs b/crates/editor2/src/editor.rs index 87fa9875d4553589546ba037375753fc2649b426..c180d49ef27e7a76e5af5453f0264c354b2f2b7f 100644 --- a/crates/editor2/src/editor.rs +++ b/crates/editor2/src/editor.rs @@ -8243,6 +8243,11 @@ impl Editor { self.style = Some(style); } + #[cfg(any(test, feature = "test-support"))] + pub fn style(&self) -> Option<&EditorStyle> { + self.style.as_ref() + } + pub fn set_wrap_width(&self, width: Option, cx: &mut AppContext) -> bool { self.display_map .update(cx, |map, cx| map.set_wrap_width(width, cx)) diff --git a/crates/editor2/src/editor_tests.rs b/crates/editor2/src/editor_tests.rs index 4925e7c965ad21f3be0886c96f14b1482d7c2978..7b989a4a2c48e6984bc5cf56c91434cae0b41861 100644 --- a/crates/editor2/src/editor_tests.rs +++ b/crates/editor2/src/editor_tests.rs @@ -1283,357 +1283,376 @@ fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) { } //todo!(simulate_resize) -// #[gpui::test] -// async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut gpui::TestAppContext) { -// init_test(cx, |_| {}); -// let mut cx = EditorTestContext::new(cx).await; +#[gpui::test] +async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + let mut cx = EditorTestContext::new(cx).await; -// let line_height = cx.editor(|editor, cx| editor.style(cx).text.line_height(cx.font_cache())); -// let window = cx.window; -// window.simulate_resize(gpui::Point::new(100., 4. * line_height), &mut cx); + let line_height = cx.editor(|editor, cx| { + editor + .style() + .unwrap() + .text + .line_height_in_pixels(cx.rem_size()) + }); + cx.simulate_window_resize(cx.window, size(px(100.), 4. * line_height)); -// cx.set_state( -// &r#"ˇone -// two + cx.set_state( + &r#"ˇone + two -// three -// fourˇ -// five + three + fourˇ + five -// six"# -// .unindent(), -// ); + six"# + .unindent(), + ); -// cx.update_editor(|editor, cx| editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, cx)); -// cx.assert_editor_state( -// &r#"one -// two -// ˇ -// three -// four -// five -// ˇ -// six"# -// .unindent(), -// ); + cx.update_editor(|editor, cx| editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, cx)); + cx.assert_editor_state( + &r#"one + two + ˇ + three + four + five + ˇ + six"# + .unindent(), + ); -// cx.update_editor(|editor, cx| editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, cx)); -// cx.assert_editor_state( -// &r#"one -// two - -// three -// four -// five -// ˇ -// sixˇ"# -// .unindent(), -// ); + cx.update_editor(|editor, cx| editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, cx)); + cx.assert_editor_state( + &r#"one + two -// cx.update_editor(|editor, cx| editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, cx)); -// cx.assert_editor_state( -// &r#"one -// two + three + four + five + ˇ + sixˇ"# + .unindent(), + ); -// three -// four -// five + cx.update_editor(|editor, cx| editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, cx)); + cx.assert_editor_state( + &r#"one + two -// sixˇ"# -// .unindent(), -// ); + three + four + five -// cx.update_editor(|editor, cx| editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, cx)); -// cx.assert_editor_state( -// &r#"one -// two - -// three -// four -// five -// ˇ -// six"# -// .unindent(), -// ); + sixˇ"# + .unindent(), + ); -// cx.update_editor(|editor, cx| editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, cx)); -// cx.assert_editor_state( -// &r#"one -// two -// ˇ -// three -// four -// five - -// six"# -// .unindent(), -// ); + cx.update_editor(|editor, cx| editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, cx)); + cx.assert_editor_state( + &r#"one + two -// cx.update_editor(|editor, cx| editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, cx)); -// cx.assert_editor_state( -// &r#"ˇone -// two + three + four + five + ˇ + six"# + .unindent(), + ); -// three -// four -// five + cx.update_editor(|editor, cx| editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, cx)); + cx.assert_editor_state( + &r#"one + two + ˇ + three + four + five -// six"# -// .unindent(), -// ); -// } + six"# + .unindent(), + ); -// #[gpui::test] -// async fn test_scroll_page_up_page_down(cx: &mut gpui::TestAppContext) { -// init_test(cx, |_| {}); -// let mut cx = EditorTestContext::new(cx).await; -// let line_height = cx.editor(|editor, cx| editor.style(cx).text.line_height(cx.font_cache())); -// let window = cx.window; -// window.simulate_resize(Point::new(1000., 4. * line_height + 0.5), &mut cx); - -// cx.set_state( -// &r#"ˇone -// two -// three -// four -// five -// six -// seven -// eight -// nine -// ten -// "#, -// ); + cx.update_editor(|editor, cx| editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, cx)); + cx.assert_editor_state( + &r#"ˇone + two -// cx.update_editor(|editor, cx| { -// assert_eq!( -// editor.snapshot(cx).scroll_position(), -// gpui::Point::new(0., 0.) -// ); -// editor.scroll_screen(&ScrollAmount::Page(1.), cx); -// assert_eq!( -// editor.snapshot(cx).scroll_position(), -// gpui::Point::new(0., 3.) -// ); -// editor.scroll_screen(&ScrollAmount::Page(1.), cx); -// assert_eq!( -// editor.snapshot(cx).scroll_position(), -// gpui::Point::new(0., 6.) -// ); -// editor.scroll_screen(&ScrollAmount::Page(-1.), cx); -// assert_eq!( -// editor.snapshot(cx).scroll_position(), -// gpui::Point::new(0., 3.) -// ); + three + four + five -// editor.scroll_screen(&ScrollAmount::Page(-0.5), cx); -// assert_eq!( -// editor.snapshot(cx).scroll_position(), -// gpui::Point::new(0., 1.) -// ); -// editor.scroll_screen(&ScrollAmount::Page(0.5), cx); -// assert_eq!( -// editor.snapshot(cx).scroll_position(), -// gpui::Point::new(0., 3.) -// ); -// }); -// } + six"# + .unindent(), + ); +} -// #[gpui::test] -// async fn test_autoscroll(cx: &mut gpui::TestAppContext) { -// init_test(cx, |_| {}); -// let mut cx = EditorTestContext::new(cx).await; +#[gpui::test] +async fn test_scroll_page_up_page_down(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + let mut cx = EditorTestContext::new(cx).await; + let line_height = cx.editor(|editor, cx| { + editor + .style() + .unwrap() + .text + .line_height_in_pixels(cx.rem_size()) + }); + let window = cx.window; + cx.simulate_window_resize(window, size(px(1000.), 4. * line_height + px(0.5))); -// let line_height = cx.update_editor(|editor, cx| { -// editor.set_vertical_scroll_margin(2, cx); -// editor.style(cx).text.line_height(cx.font_cache()) -// }); + cx.set_state( + &r#"ˇone + two + three + four + five + six + seven + eight + nine + ten + "#, + ); -// let window = cx.window; -// window.simulate_resize(gpui::Point::new(1000., 6.0 * line_height), &mut cx); - -// cx.set_state( -// &r#"ˇone -// two -// three -// four -// five -// six -// seven -// eight -// nine -// ten -// "#, -// ); -// cx.update_editor(|editor, cx| { -// assert_eq!( -// editor.snapshot(cx).scroll_position(), -// gpui::Point::new(0., 0.0) -// ); -// }); + cx.update_editor(|editor, cx| { + assert_eq!( + editor.snapshot(cx).scroll_position(), + gpui::Point::new(0., 0.) + ); + editor.scroll_screen(&ScrollAmount::Page(1.), cx); + assert_eq!( + editor.snapshot(cx).scroll_position(), + gpui::Point::new(0., 3.) + ); + editor.scroll_screen(&ScrollAmount::Page(1.), cx); + assert_eq!( + editor.snapshot(cx).scroll_position(), + gpui::Point::new(0., 6.) + ); + editor.scroll_screen(&ScrollAmount::Page(-1.), cx); + assert_eq!( + editor.snapshot(cx).scroll_position(), + gpui::Point::new(0., 3.) + ); -// // Add a cursor below the visible area. Since both cursors cannot fit -// // on screen, the editor autoscrolls to reveal the newest cursor, and -// // allows the vertical scroll margin below that cursor. -// cx.update_editor(|editor, cx| { -// editor.change_selections(Some(Autoscroll::fit()), cx, |selections| { -// selections.select_ranges([ -// Point::new(0, 0)..Point::new(0, 0), -// Point::new(6, 0)..Point::new(6, 0), -// ]); -// }) -// }); -// cx.update_editor(|editor, cx| { -// assert_eq!( -// editor.snapshot(cx).scroll_position(), -// gpui::Point::new(0., 3.0) -// ); -// }); + editor.scroll_screen(&ScrollAmount::Page(-0.5), cx); + assert_eq!( + editor.snapshot(cx).scroll_position(), + gpui::Point::new(0., 1.) + ); + editor.scroll_screen(&ScrollAmount::Page(0.5), cx); + assert_eq!( + editor.snapshot(cx).scroll_position(), + gpui::Point::new(0., 3.) + ); + }); +} -// // Move down. The editor cursor scrolls down to track the newest cursor. -// cx.update_editor(|editor, cx| { -// editor.move_down(&Default::default(), cx); -// }); -// cx.update_editor(|editor, cx| { -// assert_eq!( -// editor.snapshot(cx).scroll_position(), -// gpui::Point::new(0., 4.0) -// ); -// }); +#[gpui::test] +async fn test_autoscroll(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + let mut cx = EditorTestContext::new(cx).await; -// // Add a cursor above the visible area. Since both cursors fit on screen, -// // the editor scrolls to show both. -// cx.update_editor(|editor, cx| { -// editor.change_selections(Some(Autoscroll::fit()), cx, |selections| { -// selections.select_ranges([ -// Point::new(1, 0)..Point::new(1, 0), -// Point::new(6, 0)..Point::new(6, 0), -// ]); -// }) -// }); -// cx.update_editor(|editor, cx| { -// assert_eq!( -// editor.snapshot(cx).scroll_position(), -// gpui::Point::new(0., 1.0) -// ); -// }); -// } + let line_height = cx.update_editor(|editor, cx| { + editor.set_vertical_scroll_margin(2, cx); + editor + .style() + .unwrap() + .text + .line_height_in_pixels(cx.rem_size()) + }); + let window = cx.window; + cx.simulate_window_resize(window, size(px(1000.), 6. * line_height)); -// #[gpui::test] -// async fn test_move_page_up_page_down(cx: &mut gpui::TestAppContext) { -// init_test(cx, |_| {}); -// let mut cx = EditorTestContext::new(cx).await; - -// let line_height = cx.editor(|editor, cx| editor.style(cx).text.line_height(cx.font_cache())); -// let window = cx.window; -// window.simulate_resize(gpui::Point::new(100., 4. * line_height), &mut cx); - -// cx.set_state( -// &r#" -// ˇone -// two -// threeˇ -// four -// five -// six -// seven -// eight -// nine -// ten -// "# -// .unindent(), -// ); + cx.set_state( + &r#"ˇone + two + three + four + five + six + seven + eight + nine + ten + "#, + ); + cx.update_editor(|editor, cx| { + assert_eq!( + editor.snapshot(cx).scroll_position(), + gpui::Point::new(0., 0.0) + ); + }); -// cx.update_editor(|editor, cx| editor.move_page_down(&MovePageDown::default(), cx)); -// cx.assert_editor_state( -// &r#" -// one -// two -// three -// ˇfour -// five -// sixˇ -// seven -// eight -// nine -// ten -// "# -// .unindent(), -// ); + // Add a cursor below the visible area. Since both cursors cannot fit + // on screen, the editor autoscrolls to reveal the newest cursor, and + // allows the vertical scroll margin below that cursor. + cx.update_editor(|editor, cx| { + editor.change_selections(Some(Autoscroll::fit()), cx, |selections| { + selections.select_ranges([ + Point::new(0, 0)..Point::new(0, 0), + Point::new(6, 0)..Point::new(6, 0), + ]); + }) + }); + cx.update_editor(|editor, cx| { + assert_eq!( + editor.snapshot(cx).scroll_position(), + gpui::Point::new(0., 3.0) + ); + }); -// cx.update_editor(|editor, cx| editor.move_page_down(&MovePageDown::default(), cx)); -// cx.assert_editor_state( -// &r#" -// one -// two -// three -// four -// five -// six -// ˇseven -// eight -// nineˇ -// ten -// "# -// .unindent(), -// ); + // Move down. The editor cursor scrolls down to track the newest cursor. + cx.update_editor(|editor, cx| { + editor.move_down(&Default::default(), cx); + }); + cx.update_editor(|editor, cx| { + assert_eq!( + editor.snapshot(cx).scroll_position(), + gpui::Point::new(0., 4.0) + ); + }); -// cx.update_editor(|editor, cx| editor.move_page_up(&MovePageUp::default(), cx)); -// cx.assert_editor_state( -// &r#" -// one -// two -// three -// ˇfour -// five -// sixˇ -// seven -// eight -// nine -// ten -// "# -// .unindent(), -// ); + // Add a cursor above the visible area. Since both cursors fit on screen, + // the editor scrolls to show both. + cx.update_editor(|editor, cx| { + editor.change_selections(Some(Autoscroll::fit()), cx, |selections| { + selections.select_ranges([ + Point::new(1, 0)..Point::new(1, 0), + Point::new(6, 0)..Point::new(6, 0), + ]); + }) + }); + cx.update_editor(|editor, cx| { + assert_eq!( + editor.snapshot(cx).scroll_position(), + gpui::Point::new(0., 1.0) + ); + }); +} -// cx.update_editor(|editor, cx| editor.move_page_up(&MovePageUp::default(), cx)); -// cx.assert_editor_state( -// &r#" -// ˇone -// two -// threeˇ -// four -// five -// six -// seven -// eight -// nine -// ten -// "# -// .unindent(), -// ); +#[gpui::test] +async fn test_move_page_up_page_down(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + let mut cx = EditorTestContext::new(cx).await; -// // Test select collapsing -// cx.update_editor(|editor, cx| { -// editor.move_page_down(&MovePageDown::default(), cx); -// editor.move_page_down(&MovePageDown::default(), cx); -// editor.move_page_down(&MovePageDown::default(), cx); -// }); -// cx.assert_editor_state( -// &r#" -// one -// two -// three -// four -// five -// six -// seven -// eight -// nine -// ˇten -// ˇ"# -// .unindent(), -// ); -// } + let line_height = cx.editor(|editor, cx| { + editor + .style() + .unwrap() + .text + .line_height_in_pixels(cx.rem_size()) + }); + let window = cx.window; + cx.simulate_window_resize(window, size(px(100.), 4. * line_height)); + cx.set_state( + &r#" + ˇone + two + threeˇ + four + five + six + seven + eight + nine + ten + "# + .unindent(), + ); + + cx.update_editor(|editor, cx| editor.move_page_down(&MovePageDown::default(), cx)); + cx.assert_editor_state( + &r#" + one + two + three + ˇfour + five + sixˇ + seven + eight + nine + ten + "# + .unindent(), + ); + + cx.update_editor(|editor, cx| editor.move_page_down(&MovePageDown::default(), cx)); + cx.assert_editor_state( + &r#" + one + two + three + four + five + six + ˇseven + eight + nineˇ + ten + "# + .unindent(), + ); + + cx.update_editor(|editor, cx| editor.move_page_up(&MovePageUp::default(), cx)); + cx.assert_editor_state( + &r#" + one + two + three + ˇfour + five + sixˇ + seven + eight + nine + ten + "# + .unindent(), + ); + + cx.update_editor(|editor, cx| editor.move_page_up(&MovePageUp::default(), cx)); + cx.assert_editor_state( + &r#" + ˇone + two + threeˇ + four + five + six + seven + eight + nine + ten + "# + .unindent(), + ); + + // Test select collapsing + cx.update_editor(|editor, cx| { + editor.move_page_down(&MovePageDown::default(), cx); + editor.move_page_down(&MovePageDown::default(), cx); + editor.move_page_down(&MovePageDown::default(), cx); + }); + cx.assert_editor_state( + &r#" + one + two + three + four + five + six + seven + eight + nine + ˇten + ˇ"# + .unindent(), + ); +} #[gpui::test] async fn test_delete_to_beginning_of_line(cx: &mut gpui::TestAppContext) { diff --git a/crates/editor2/src/git.rs b/crates/editor2/src/git.rs index 9190eed05a40a24063ccf78ca30a6b2966f43c47..0e7501c16936be794f1a4ead6f5d7f2e182b0126 100644 --- a/crates/editor2/src/git.rs +++ b/crates/editor2/src/git.rs @@ -88,7 +88,7 @@ pub fn diff_hunk_to_display(hunk: DiffHunk, snapshot: &DisplaySnapshot) -> } } -#[cfg(any(test, feature = "test_support"))] +#[cfg(any(test, feature = "test-support"))] mod tests { use crate::editor_tests::init_test; use crate::Point; diff --git a/crates/gpui2/src/app/test_context.rs b/crates/gpui2/src/app/test_context.rs index 908a418635bfe7262b69a6ced0344c4484bd89dd..a9403de9bce63a62dfedf455d997de34dff4d856 100644 --- a/crates/gpui2/src/app/test_context.rs +++ b/crates/gpui2/src/app/test_context.rs @@ -1,13 +1,13 @@ use crate::{ div, Action, AnyView, AnyWindowHandle, AppCell, AppContext, AsyncAppContext, - BackgroundExecutor, Context, Div, Entity, EventEmitter, ForegroundExecutor, InputEvent, - KeyDownEvent, Keystroke, Model, ModelContext, Render, Result, Task, TestDispatcher, - TestPlatform, TestWindow, TestWindowHandlers, View, ViewContext, VisualContext, WindowContext, - WindowHandle, WindowOptions, + BackgroundExecutor, Bounds, Context, Div, Entity, EventEmitter, ForegroundExecutor, InputEvent, + KeyDownEvent, Keystroke, Model, ModelContext, Pixels, PlatformWindow, Point, Render, Result, + Size, Task, TestDispatcher, TestPlatform, TestWindow, TestWindowHandlers, View, ViewContext, + VisualContext, WindowBounds, WindowContext, WindowHandle, WindowOptions, }; use anyhow::{anyhow, bail}; use futures::{Stream, StreamExt}; -use std::{future::Future, ops::Deref, rc::Rc, sync::Arc, time::Duration}; +use std::{future::Future, mem, ops::Deref, rc::Rc, sync::Arc, time::Duration}; #[derive(Clone)] pub struct TestAppContext { @@ -170,6 +170,45 @@ impl TestAppContext { self.test_platform.has_pending_prompt() } + pub fn simulate_window_resize(&self, window_handle: AnyWindowHandle, size: Size) { + let (mut handlers, scale_factor) = self + .app + .borrow_mut() + .update_window(window_handle, |_, cx| { + let platform_window = cx.window.platform_window.as_test().unwrap(); + let scale_factor = platform_window.scale_factor(); + match &mut platform_window.bounds { + WindowBounds::Fullscreen | WindowBounds::Maximized => { + platform_window.bounds = WindowBounds::Fixed(Bounds { + origin: Point::default(), + size: size.map(|pixels| f64::from(pixels).into()), + }); + } + WindowBounds::Fixed(bounds) => { + bounds.size = size.map(|pixels| f64::from(pixels).into()); + } + } + + ( + mem::take(&mut platform_window.handlers.lock().resize), + scale_factor, + ) + }) + .unwrap(); + + for handler in &mut handlers { + handler(size, scale_factor); + } + + self.app + .borrow_mut() + .update_window(window_handle, |_, cx| { + let platform_window = cx.window.platform_window.as_test().unwrap(); + platform_window.handlers.lock().resize = handlers; + }) + .unwrap(); + } + pub fn spawn(&self, f: impl FnOnce(AsyncAppContext) -> Fut) -> Task where Fut: Future + 'static, diff --git a/crates/gpui2/src/executor.rs b/crates/gpui2/src/executor.rs index e446a0cb1ea287b4b20c668f2adfe36e9d4227c4..e01846c404f0781ebe01d4657f624fe2d00b343b 100644 --- a/crates/gpui2/src/executor.rs +++ b/crates/gpui2/src/executor.rs @@ -206,13 +206,14 @@ impl BackgroundExecutor { return Err(future); } - let max_ticks = if cfg!(any(test, feature = "test-support")) { - self.dispatcher - .as_test() - .map_or(usize::MAX, |dispatcher| dispatcher.gen_block_on_ticks()) - } else { - usize::MAX - }; + #[cfg(any(test, feature = "test-support"))] + let max_ticks = self + .dispatcher + .as_test() + .map_or(usize::MAX, |dispatcher| dispatcher.gen_block_on_ticks()); + #[cfg(not(any(test, feature = "test-support")))] + let max_ticks = usize::MAX; + let mut timer = self.timer(duration).fuse(); let timeout = async { diff --git a/crates/gpui2/src/platform.rs b/crates/gpui2/src/platform.rs index 37b156e34893771663f6b3826e0cf88a76f83fc5..ed45d7bab2a3e27eb7fce08e87dc990188cf88ee 100644 --- a/crates/gpui2/src/platform.rs +++ b/crates/gpui2/src/platform.rs @@ -160,7 +160,7 @@ pub trait PlatformWindow { fn sprite_atlas(&self) -> Arc; #[cfg(any(test, feature = "test-support"))] - fn as_test(&self) -> Option<&TestWindow> { + fn as_test(&mut self) -> Option<&mut TestWindow> { None } } diff --git a/crates/gpui2/src/platform/test/window.rs b/crates/gpui2/src/platform/test/window.rs index 2ad54eff0d0f8d33f97685f5032729291f720fb6..b1bfebad06745f51899421707d2ead4da57bfcb6 100644 --- a/crates/gpui2/src/platform/test/window.rs +++ b/crates/gpui2/src/platform/test/window.rs @@ -19,7 +19,7 @@ pub(crate) struct TestWindowHandlers { } pub struct TestWindow { - bounds: WindowBounds, + pub(crate) bounds: WindowBounds, current_scene: Mutex>, display: Rc, pub(crate) window_title: Option, @@ -170,7 +170,7 @@ impl PlatformWindow for TestWindow { self.sprite_atlas.clone() } - fn as_test(&self) -> Option<&TestWindow> { + fn as_test(&mut self) -> Option<&mut TestWindow> { Some(self) } } diff --git a/crates/gpui2/src/style.rs b/crates/gpui2/src/style.rs index 640538fff0ed204d3af16e24ecd0006d98f8357e..9254eaeb85246253180e6ffc59c3f1f12895ae59 100644 --- a/crates/gpui2/src/style.rs +++ b/crates/gpui2/src/style.rs @@ -208,8 +208,9 @@ impl TextStyle { } } + /// Returns the rounded line height in pixels. pub fn line_height_in_pixels(&self, rem_size: Pixels) -> Pixels { - self.line_height.to_pixels(self.font_size, rem_size) + self.line_height.to_pixels(self.font_size, rem_size).round() } pub fn to_run(&self, len: usize) -> TextRun { From a3e4559b0ea90666047ad4c7d338d7a5daf04d51 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 4 Dec 2023 17:57:43 +0100 Subject: [PATCH 26/35] Fix git test compilation --- crates/editor2/src/git.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/editor2/src/git.rs b/crates/editor2/src/git.rs index 0e7501c16936be794f1a4ead6f5d7f2e182b0126..f798ab9fb636efb46fc806442af301f3e2b05671 100644 --- a/crates/editor2/src/git.rs +++ b/crates/editor2/src/git.rs @@ -88,7 +88,7 @@ pub fn diff_hunk_to_display(hunk: DiffHunk, snapshot: &DisplaySnapshot) -> } } -#[cfg(any(test, feature = "test-support"))] +#[cfg(test)] mod tests { use crate::editor_tests::init_test; use crate::Point; From 948c065f86a3dd87ab18f8050c1122c2dee61edc Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 4 Dec 2023 18:04:10 +0100 Subject: [PATCH 27/35] test_copilot_multibuffer --- crates/editor2/src/editor_tests.rs | 199 ++++++++++++++--------------- 1 file changed, 99 insertions(+), 100 deletions(-) diff --git a/crates/editor2/src/editor_tests.rs b/crates/editor2/src/editor_tests.rs index 7b989a4a2c48e6984bc5cf56c91434cae0b41861..424da8987eb6d673f0e789d4b8ae8b1620967045 100644 --- a/crates/editor2/src/editor_tests.rs +++ b/crates/editor2/src/editor_tests.rs @@ -7387,106 +7387,105 @@ async fn test_copilot_completion_invalidation( }); } -//todo!() -// #[gpui::test] -// async fn test_copilot_multibuffer(executor: BackgroundExecutor, cx: &mut gpui::TestAppContext) { -// init_test(cx, |_| {}); - -// let (copilot, copilot_lsp) = Copilot::fake(cx); -// cx.update(|cx| cx.set_global(copilot)); - -// let buffer_1 = cx.build_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), "a = 1\nb = 2\n")); -// let buffer_2 = cx.build_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), "c = 3\nd = 4\n")); -// let multibuffer = cx.build_model(|cx| { -// let mut multibuffer = MultiBuffer::new(0); -// multibuffer.push_excerpts( -// buffer_1.clone(), -// [ExcerptRange { -// context: Point::new(0, 0)..Point::new(2, 0), -// primary: None, -// }], -// cx, -// ); -// multibuffer.push_excerpts( -// buffer_2.clone(), -// [ExcerptRange { -// context: Point::new(0, 0)..Point::new(2, 0), -// primary: None, -// }], -// cx, -// ); -// multibuffer -// }); -// let editor = cx.add_window(|cx| build_editor(multibuffer, cx)); - -// handle_copilot_completion_request( -// &copilot_lsp, -// vec![copilot::request::Completion { -// text: "b = 2 + a".into(), -// range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 5)), -// ..Default::default() -// }], -// vec![], -// ); -// editor.update(cx, |editor, cx| { -// // Ensure copilot suggestions are shown for the first excerpt. -// editor.change_selections(None, cx, |s| { -// s.select_ranges([Point::new(1, 5)..Point::new(1, 5)]) -// }); -// editor.next_copilot_suggestion(&Default::default(), cx); -// }); -// executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); -// editor.update(cx, |editor, cx| { -// assert!(editor.has_active_copilot_suggestion(cx)); -// assert_eq!( -// editor.display_text(cx), -// "\n\na = 1\nb = 2 + a\n\n\n\nc = 3\nd = 4\n" -// ); -// assert_eq!(editor.text(cx), "a = 1\nb = 2\n\nc = 3\nd = 4\n"); -// }); - -// handle_copilot_completion_request( -// &copilot_lsp, -// vec![copilot::request::Completion { -// text: "d = 4 + c".into(), -// range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 6)), -// ..Default::default() -// }], -// vec![], -// ); -// editor.update(cx, |editor, cx| { -// // Move to another excerpt, ensuring the suggestion gets cleared. -// editor.change_selections(None, cx, |s| { -// s.select_ranges([Point::new(4, 5)..Point::new(4, 5)]) -// }); -// assert!(!editor.has_active_copilot_suggestion(cx)); -// assert_eq!( -// editor.display_text(cx), -// "\n\na = 1\nb = 2\n\n\n\nc = 3\nd = 4\n" -// ); -// assert_eq!(editor.text(cx), "a = 1\nb = 2\n\nc = 3\nd = 4\n"); - -// // Type a character, ensuring we don't even try to interpolate the previous suggestion. -// editor.handle_input(" ", cx); -// assert!(!editor.has_active_copilot_suggestion(cx)); -// assert_eq!( -// editor.display_text(cx), -// "\n\na = 1\nb = 2\n\n\n\nc = 3\nd = 4 \n" -// ); -// assert_eq!(editor.text(cx), "a = 1\nb = 2\n\nc = 3\nd = 4 \n"); -// }); - -// // Ensure the new suggestion is displayed when the debounce timeout expires. -// executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); -// editor.update(cx, |editor, cx| { -// assert!(editor.has_active_copilot_suggestion(cx)); -// assert_eq!( -// editor.display_text(cx), -// "\n\na = 1\nb = 2\n\n\n\nc = 3\nd = 4 + c\n" -// ); -// assert_eq!(editor.text(cx), "a = 1\nb = 2\n\nc = 3\nd = 4 \n"); -// }); -// } +#[gpui::test] +async fn test_copilot_multibuffer(executor: BackgroundExecutor, cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let (copilot, copilot_lsp) = Copilot::fake(cx); + cx.update(|cx| cx.set_global(copilot)); + + let buffer_1 = cx.build_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), "a = 1\nb = 2\n")); + let buffer_2 = cx.build_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), "c = 3\nd = 4\n")); + let multibuffer = cx.build_model(|cx| { + let mut multibuffer = MultiBuffer::new(0); + multibuffer.push_excerpts( + buffer_1.clone(), + [ExcerptRange { + context: Point::new(0, 0)..Point::new(2, 0), + primary: None, + }], + cx, + ); + multibuffer.push_excerpts( + buffer_2.clone(), + [ExcerptRange { + context: Point::new(0, 0)..Point::new(2, 0), + primary: None, + }], + cx, + ); + multibuffer + }); + let editor = cx.add_window(|cx| build_editor(multibuffer, cx)); + + handle_copilot_completion_request( + &copilot_lsp, + vec![copilot::request::Completion { + text: "b = 2 + a".into(), + range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 5)), + ..Default::default() + }], + vec![], + ); + editor.update(cx, |editor, cx| { + // Ensure copilot suggestions are shown for the first excerpt. + editor.change_selections(None, cx, |s| { + s.select_ranges([Point::new(1, 5)..Point::new(1, 5)]) + }); + editor.next_copilot_suggestion(&Default::default(), cx); + }); + executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); + editor.update(cx, |editor, cx| { + assert!(editor.has_active_copilot_suggestion(cx)); + assert_eq!( + editor.display_text(cx), + "\n\na = 1\nb = 2 + a\n\n\n\nc = 3\nd = 4\n" + ); + assert_eq!(editor.text(cx), "a = 1\nb = 2\n\nc = 3\nd = 4\n"); + }); + + handle_copilot_completion_request( + &copilot_lsp, + vec![copilot::request::Completion { + text: "d = 4 + c".into(), + range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 6)), + ..Default::default() + }], + vec![], + ); + editor.update(cx, |editor, cx| { + // Move to another excerpt, ensuring the suggestion gets cleared. + editor.change_selections(None, cx, |s| { + s.select_ranges([Point::new(4, 5)..Point::new(4, 5)]) + }); + assert!(!editor.has_active_copilot_suggestion(cx)); + assert_eq!( + editor.display_text(cx), + "\n\na = 1\nb = 2\n\n\n\nc = 3\nd = 4\n" + ); + assert_eq!(editor.text(cx), "a = 1\nb = 2\n\nc = 3\nd = 4\n"); + + // Type a character, ensuring we don't even try to interpolate the previous suggestion. + editor.handle_input(" ", cx); + assert!(!editor.has_active_copilot_suggestion(cx)); + assert_eq!( + editor.display_text(cx), + "\n\na = 1\nb = 2\n\n\n\nc = 3\nd = 4 \n" + ); + assert_eq!(editor.text(cx), "a = 1\nb = 2\n\nc = 3\nd = 4 \n"); + }); + + // Ensure the new suggestion is displayed when the debounce timeout expires. + executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); + editor.update(cx, |editor, cx| { + assert!(editor.has_active_copilot_suggestion(cx)); + assert_eq!( + editor.display_text(cx), + "\n\na = 1\nb = 2\n\n\n\nc = 3\nd = 4 + c\n" + ); + assert_eq!(editor.text(cx), "a = 1\nb = 2\n\nc = 3\nd = 4 \n"); + }); +} #[gpui::test] async fn test_copilot_disabled_globs(executor: BackgroundExecutor, cx: &mut gpui::TestAppContext) { From e0ec5032e9ca0f81ddb0a8101ad5c8e143b51436 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 4 Dec 2023 18:19:49 +0100 Subject: [PATCH 28/35] Fix highlight tests (and a quirky behaviour where the highlights were not dismissed when user clicks on something that's not a brace) --- .../editor2/src/highlight_matching_bracket.rs | 200 +++++++++--------- 1 file changed, 100 insertions(+), 100 deletions(-) diff --git a/crates/editor2/src/highlight_matching_bracket.rs b/crates/editor2/src/highlight_matching_bracket.rs index d7fd37745f0e1465ba5fec5f018834dec68df52f..1ed7700f37a56cd428015fdffbe25b78b2c02fad 100644 --- a/crates/editor2/src/highlight_matching_bracket.rs +++ b/crates/editor2/src/highlight_matching_bracket.rs @@ -5,7 +5,7 @@ use crate::{Editor, RangeToAnchorExt}; enum MatchingBracketHighlight {} pub fn refresh_matching_bracket_highlights(editor: &mut Editor, cx: &mut ViewContext) { - // editor.clear_background_highlights::(cx); + editor.clear_background_highlights::(cx); let newest_selection = editor.selections.newest::(cx); // Don't highlight brackets if the selection isn't empty @@ -30,109 +30,109 @@ pub fn refresh_matching_bracket_highlights(editor: &mut Editor, cx: &mut ViewCon } } -// #[cfg(test)] -// mod tests { -// use super::*; -// use crate::{editor_tests::init_test, test::editor_lsp_test_context::EditorLspTestContext}; -// use indoc::indoc; -// use language::{BracketPair, BracketPairConfig, Language, LanguageConfig}; +#[cfg(test)] +mod tests { + use super::*; + use crate::{editor_tests::init_test, test::editor_lsp_test_context::EditorLspTestContext}; + use indoc::indoc; + use language::{BracketPair, BracketPairConfig, Language, LanguageConfig}; -// #[gpui::test] -// async fn test_matching_bracket_highlights(cx: &mut gpui::TestAppContext) { -// init_test(cx, |_| {}); + #[gpui::test] + async fn test_matching_bracket_highlights(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); -// let mut cx = EditorLspTestContext::new( -// Language::new( -// LanguageConfig { -// name: "Rust".into(), -// path_suffixes: vec!["rs".to_string()], -// brackets: BracketPairConfig { -// pairs: vec![ -// BracketPair { -// start: "{".to_string(), -// end: "}".to_string(), -// close: false, -// newline: true, -// }, -// BracketPair { -// start: "(".to_string(), -// end: ")".to_string(), -// close: false, -// newline: true, -// }, -// ], -// ..Default::default() -// }, -// ..Default::default() -// }, -// Some(tree_sitter_rust::language()), -// ) -// .with_brackets_query(indoc! {r#" -// ("{" @open "}" @close) -// ("(" @open ")" @close) -// "#}) -// .unwrap(), -// Default::default(), -// cx, -// ) -// .await; + let mut cx = EditorLspTestContext::new( + Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + brackets: BracketPairConfig { + pairs: vec![ + BracketPair { + start: "{".to_string(), + end: "}".to_string(), + close: false, + newline: true, + }, + BracketPair { + start: "(".to_string(), + end: ")".to_string(), + close: false, + newline: true, + }, + ], + ..Default::default() + }, + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ) + .with_brackets_query(indoc! {r#" + ("{" @open "}" @close) + ("(" @open ")" @close) + "#}) + .unwrap(), + Default::default(), + cx, + ) + .await; -// // positioning cursor inside bracket highlights both -// cx.set_state(indoc! {r#" -// pub fn test("Test ˇargument") { -// another_test(1, 2, 3); -// } -// "#}); -// cx.assert_editor_background_highlights::(indoc! {r#" -// pub fn test«(»"Test argument"«)» { -// another_test(1, 2, 3); -// } -// "#}); + // positioning cursor inside bracket highlights both + cx.set_state(indoc! {r#" + pub fn test("Test ˇargument") { + another_test(1, 2, 3); + } + "#}); + cx.assert_editor_background_highlights::(indoc! {r#" + pub fn test«(»"Test argument"«)» { + another_test(1, 2, 3); + } + "#}); -// cx.set_state(indoc! {r#" -// pub fn test("Test argument") { -// another_test(1, ˇ2, 3); -// } -// "#}); -// cx.assert_editor_background_highlights::(indoc! {r#" -// pub fn test("Test argument") { -// another_test«(»1, 2, 3«)»; -// } -// "#}); + cx.set_state(indoc! {r#" + pub fn test("Test argument") { + another_test(1, ˇ2, 3); + } + "#}); + cx.assert_editor_background_highlights::(indoc! {r#" + pub fn test("Test argument") { + another_test«(»1, 2, 3«)»; + } + "#}); -// cx.set_state(indoc! {r#" -// pub fn test("Test argument") { -// anotherˇ_test(1, 2, 3); -// } -// "#}); -// cx.assert_editor_background_highlights::(indoc! {r#" -// pub fn test("Test argument") «{» -// another_test(1, 2, 3); -// «}» -// "#}); + cx.set_state(indoc! {r#" + pub fn test("Test argument") { + anotherˇ_test(1, 2, 3); + } + "#}); + cx.assert_editor_background_highlights::(indoc! {r#" + pub fn test("Test argument") «{» + another_test(1, 2, 3); + «}» + "#}); -// // positioning outside of brackets removes highlight -// cx.set_state(indoc! {r#" -// pub fˇn test("Test argument") { -// another_test(1, 2, 3); -// } -// "#}); -// cx.assert_editor_background_highlights::(indoc! {r#" -// pub fn test("Test argument") { -// another_test(1, 2, 3); -// } -// "#}); + // positioning outside of brackets removes highlight + cx.set_state(indoc! {r#" + pub fˇn test("Test argument") { + another_test(1, 2, 3); + } + "#}); + cx.assert_editor_background_highlights::(indoc! {r#" + pub fn test("Test argument") { + another_test(1, 2, 3); + } + "#}); -// // non empty selection dismisses highlight -// cx.set_state(indoc! {r#" -// pub fn test("Te«st argˇ»ument") { -// another_test(1, 2, 3); -// } -// "#}); -// cx.assert_editor_background_highlights::(indoc! {r#" -// pub fn test("Test argument") { -// another_test(1, 2, 3); -// } -// "#}); -// } -// } + // non empty selection dismisses highlight + cx.set_state(indoc! {r#" + pub fn test("Te«st argˇ»ument") { + another_test(1, 2, 3); + } + "#}); + cx.assert_editor_background_highlights::(indoc! {r#" + pub fn test("Test argument") { + another_test(1, 2, 3); + } + "#}); + } +} From 9695ea1017f12fefa447ad21599a0e4ca8956a50 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 5 Dec 2023 10:29:09 +0100 Subject: [PATCH 29/35] test_shape_line_numbers --- crates/editor2/src/element.rs | 893 +++++++++++++++++----------------- 1 file changed, 451 insertions(+), 442 deletions(-) diff --git a/crates/editor2/src/element.rs b/crates/editor2/src/element.rs index 27d27a5947e30c1a60d290748cb0a7c459d7d615..f30f7337a1af10f21ad11e4203f401b548604372 100644 --- a/crates/editor2/src/element.rs +++ b/crates/editor2/src/element.rs @@ -3227,448 +3227,457 @@ fn scale_horizontal_mouse_autoscroll_delta(delta: Pixels) -> f32 { (delta.pow(1.2) / 300.0).into() } -// #[cfg(test)] -// mod tests { -// use super::*; -// use crate::{ -// display_map::{BlockDisposition, BlockProperties}, -// editor_tests::{init_test, update_test_language_settings}, -// Editor, MultiBuffer, -// }; -// use gpui::TestAppContext; -// use language::language_settings; -// use log::info; -// use std::{num::NonZeroU32, sync::Arc}; -// use util::test::sample_text; - -// #[gpui::test] -// fn test_layout_line_numbers(cx: &mut TestAppContext) { -// init_test(cx, |_| {}); -// let editor = cx -// .add_window(|cx| { -// let buffer = MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx); -// Editor::new(EditorMode::Full, buffer, None, None, cx) -// }) -// .root(cx); -// let element = EditorElement::new(editor.read_with(cx, |editor, cx| editor.style(cx))); - -// let layouts = editor.update(cx, |editor, cx| { -// let snapshot = editor.snapshot(cx); -// element -// .layout_line_numbers( -// 0..6, -// &Default::default(), -// DisplayPoint::new(0, 0), -// false, -// &snapshot, -// cx, -// ) -// .0 -// }); -// assert_eq!(layouts.len(), 6); - -// let relative_rows = editor.update(cx, |editor, cx| { -// let snapshot = editor.snapshot(cx); -// element.calculate_relative_line_numbers(&snapshot, &(0..6), Some(3)) -// }); -// assert_eq!(relative_rows[&0], 3); -// assert_eq!(relative_rows[&1], 2); -// assert_eq!(relative_rows[&2], 1); -// // current line has no relative number -// assert_eq!(relative_rows[&4], 1); -// assert_eq!(relative_rows[&5], 2); - -// // works if cursor is before screen -// let relative_rows = editor.update(cx, |editor, cx| { -// let snapshot = editor.snapshot(cx); - -// element.calculate_relative_line_numbers(&snapshot, &(3..6), Some(1)) -// }); -// assert_eq!(relative_rows.len(), 3); -// assert_eq!(relative_rows[&3], 2); -// assert_eq!(relative_rows[&4], 3); -// assert_eq!(relative_rows[&5], 4); - -// // works if cursor is after screen -// let relative_rows = editor.update(cx, |editor, cx| { -// let snapshot = editor.snapshot(cx); - -// element.calculate_relative_line_numbers(&snapshot, &(0..3), Some(6)) -// }); -// assert_eq!(relative_rows.len(), 3); -// assert_eq!(relative_rows[&0], 5); -// assert_eq!(relative_rows[&1], 4); -// assert_eq!(relative_rows[&2], 3); -// } - -// #[gpui::test] -// async fn test_vim_visual_selections(cx: &mut TestAppContext) { -// init_test(cx, |_| {}); - -// let editor = cx -// .add_window(|cx| { -// let buffer = MultiBuffer::build_simple(&(sample_text(6, 6, 'a') + "\n"), cx); -// Editor::new(EditorMode::Full, buffer, None, None, cx) -// }) -// .root(cx); -// let mut element = EditorElement::new(editor.read_with(cx, |editor, cx| editor.style(cx))); -// let (_, state) = editor.update(cx, |editor, cx| { -// editor.cursor_shape = CursorShape::Block; -// editor.change_selections(None, cx, |s| { -// s.select_ranges([ -// Point::new(0, 0)..Point::new(1, 0), -// Point::new(3, 2)..Point::new(3, 3), -// Point::new(5, 6)..Point::new(6, 0), -// ]); -// }); -// element.layout( -// SizeConstraint::new(point(500., 500.), point(500., 500.)), -// editor, -// cx, -// ) -// }); -// assert_eq!(state.selections.len(), 1); -// let local_selections = &state.selections[0].1; -// assert_eq!(local_selections.len(), 3); -// // moves cursor back one line -// assert_eq!(local_selections[0].head, DisplayPoint::new(0, 6)); -// assert_eq!( -// local_selections[0].range, -// DisplayPoint::new(0, 0)..DisplayPoint::new(1, 0) -// ); - -// // moves cursor back one column -// assert_eq!( -// local_selections[1].range, -// DisplayPoint::new(3, 2)..DisplayPoint::new(3, 3) -// ); -// assert_eq!(local_selections[1].head, DisplayPoint::new(3, 2)); - -// // leaves cursor on the max point -// assert_eq!( -// local_selections[2].range, -// DisplayPoint::new(5, 6)..DisplayPoint::new(6, 0) -// ); -// assert_eq!(local_selections[2].head, DisplayPoint::new(6, 0)); - -// // active lines does not include 1 (even though the range of the selection does) -// assert_eq!( -// state.active_rows.keys().cloned().collect::>(), -// vec![0, 3, 5, 6] -// ); - -// // multi-buffer support -// // in DisplayPoint co-ordinates, this is what we're dealing with: -// // 0: [[file -// // 1: header]] -// // 2: aaaaaa -// // 3: bbbbbb -// // 4: cccccc -// // 5: -// // 6: ... -// // 7: ffffff -// // 8: gggggg -// // 9: hhhhhh -// // 10: -// // 11: [[file -// // 12: header]] -// // 13: bbbbbb -// // 14: cccccc -// // 15: dddddd -// let editor = cx -// .add_window(|cx| { -// let buffer = MultiBuffer::build_multi( -// [ -// ( -// &(sample_text(8, 6, 'a') + "\n"), -// vec![ -// Point::new(0, 0)..Point::new(3, 0), -// Point::new(4, 0)..Point::new(7, 0), -// ], -// ), -// ( -// &(sample_text(8, 6, 'a') + "\n"), -// vec![Point::new(1, 0)..Point::new(3, 0)], -// ), -// ], -// cx, -// ); -// Editor::new(EditorMode::Full, buffer, None, None, cx) -// }) -// .root(cx); -// let mut element = EditorElement::new(editor.read_with(cx, |editor, cx| editor.style(cx))); -// let (_, state) = editor.update(cx, |editor, cx| { -// editor.cursor_shape = CursorShape::Block; -// editor.change_selections(None, cx, |s| { -// s.select_display_ranges([ -// DisplayPoint::new(4, 0)..DisplayPoint::new(7, 0), -// DisplayPoint::new(10, 0)..DisplayPoint::new(13, 0), -// ]); -// }); -// element.layout( -// SizeConstraint::new(point(500., 500.), point(500., 500.)), -// editor, -// cx, -// ) -// }); - -// assert_eq!(state.selections.len(), 1); -// let local_selections = &state.selections[0].1; -// assert_eq!(local_selections.len(), 2); - -// // moves cursor on excerpt boundary back a line -// // and doesn't allow selection to bleed through -// assert_eq!( -// local_selections[0].range, -// DisplayPoint::new(4, 0)..DisplayPoint::new(6, 0) -// ); -// assert_eq!(local_selections[0].head, DisplayPoint::new(5, 0)); - -// // moves cursor on buffer boundary back two lines -// // and doesn't allow selection to bleed through -// assert_eq!( -// local_selections[1].range, -// DisplayPoint::new(10, 0)..DisplayPoint::new(11, 0) -// ); -// assert_eq!(local_selections[1].head, DisplayPoint::new(10, 0)); -// } - -// #[gpui::test] -// fn test_layout_with_placeholder_text_and_blocks(cx: &mut TestAppContext) { -// init_test(cx, |_| {}); - -// let editor = cx -// .add_window(|cx| { -// let buffer = MultiBuffer::build_simple("", cx); -// Editor::new(EditorMode::Full, buffer, None, None, cx) -// }) -// .root(cx); - -// editor.update(cx, |editor, cx| { -// editor.set_placeholder_text("hello", cx); -// editor.insert_blocks( -// [BlockProperties { -// style: BlockStyle::Fixed, -// disposition: BlockDisposition::Above, -// height: 3, -// position: Anchor::min(), -// render: Arc::new(|_| Empty::new().into_any), -// }], -// None, -// cx, -// ); - -// // Blur the editor so that it displays placeholder text. -// cx.blur(); -// }); - -// let mut element = EditorElement::new(editor.read_with(cx, |editor, cx| editor.style(cx))); -// let (size, mut state) = editor.update(cx, |editor, cx| { -// element.layout( -// SizeConstraint::new(point(500., 500.), point(500., 500.)), -// editor, -// cx, -// ) -// }); - -// assert_eq!(state.position_map.line_layouts.len(), 4); -// assert_eq!( -// state -// .line_number_layouts -// .iter() -// .map(Option::is_some) -// .collect::>(), -// &[false, false, false, true] -// ); - -// // Don't panic. -// let bounds = Bounds::::new(Default::default(), size); -// editor.update(cx, |editor, cx| { -// element.paint(bounds, bounds, &mut state, editor, cx); -// }); -// } - -// #[gpui::test] -// fn test_all_invisibles_drawing(cx: &mut TestAppContext) { -// const TAB_SIZE: u32 = 4; - -// let input_text = "\t \t|\t| a b"; -// let expected_invisibles = vec![ -// Invisible::Tab { -// line_start_offset: 0, -// }, -// Invisible::Whitespace { -// line_offset: TAB_SIZE as usize, -// }, -// Invisible::Tab { -// line_start_offset: TAB_SIZE as usize + 1, -// }, -// Invisible::Tab { -// line_start_offset: TAB_SIZE as usize * 2 + 1, -// }, -// Invisible::Whitespace { -// line_offset: TAB_SIZE as usize * 3 + 1, -// }, -// Invisible::Whitespace { -// line_offset: TAB_SIZE as usize * 3 + 3, -// }, -// ]; -// assert_eq!( -// expected_invisibles.len(), -// input_text -// .chars() -// .filter(|initial_char| initial_char.is_whitespace()) -// .count(), -// "Hardcoded expected invisibles differ from the actual ones in '{input_text}'" -// ); - -// init_test(cx, |s| { -// s.defaults.show_whitespaces = Some(ShowWhitespaceSetting::All); -// s.defaults.tab_size = NonZeroU32::new(TAB_SIZE); -// }); - -// let actual_invisibles = -// collect_invisibles_from_new_editor(cx, EditorMode::Full, &input_text, 500.0); - -// assert_eq!(expected_invisibles, actual_invisibles); -// } - -// #[gpui::test] -// fn test_invisibles_dont_appear_in_certain_editors(cx: &mut TestAppContext) { -// init_test(cx, |s| { -// s.defaults.show_whitespaces = Some(ShowWhitespaceSetting::All); -// s.defaults.tab_size = NonZeroU32::new(4); -// }); - -// for editor_mode_without_invisibles in [ -// EditorMode::SingleLine, -// EditorMode::AutoHeight { max_lines: 100 }, -// ] { -// let invisibles = collect_invisibles_from_new_editor( -// cx, -// editor_mode_without_invisibles, -// "\t\t\t| | a b", -// 500.0, -// ); -// assert!(invisibles.is_empty, -// "For editor mode {editor_mode_without_invisibles:?} no invisibles was expected but got {invisibles:?}"); -// } -// } - -// #[gpui::test] -// fn test_wrapped_invisibles_drawing(cx: &mut TestAppContext) { -// let tab_size = 4; -// let input_text = "a\tbcd ".repeat(9); -// let repeated_invisibles = [ -// Invisible::Tab { -// line_start_offset: 1, -// }, -// Invisible::Whitespace { -// line_offset: tab_size as usize + 3, -// }, -// Invisible::Whitespace { -// line_offset: tab_size as usize + 4, -// }, -// Invisible::Whitespace { -// line_offset: tab_size as usize + 5, -// }, -// ]; -// let expected_invisibles = std::iter::once(repeated_invisibles) -// .cycle() -// .take(9) -// .flatten() -// .collect::>(); -// assert_eq!( -// expected_invisibles.len(), -// input_text -// .chars() -// .filter(|initial_char| initial_char.is_whitespace()) -// .count(), -// "Hardcoded expected invisibles differ from the actual ones in '{input_text}'" -// ); -// info!("Expected invisibles: {expected_invisibles:?}"); - -// init_test(cx, |_| {}); - -// // Put the same string with repeating whitespace pattern into editors of various size, -// // take deliberately small steps during resizing, to put all whitespace kinds near the wrap point. -// let resize_step = 10.0; -// let mut editor_width = 200.0; -// while editor_width <= 1000.0 { -// update_test_language_settings(cx, |s| { -// s.defaults.tab_size = NonZeroU32::new(tab_size); -// s.defaults.show_whitespaces = Some(ShowWhitespaceSetting::All); -// s.defaults.preferred_line_length = Some(editor_width as u32); -// s.defaults.soft_wrap = Some(language_settings::SoftWrap::PreferredLineLength); -// }); - -// let actual_invisibles = -// collect_invisibles_from_new_editor(cx, EditorMode::Full, &input_text, editor_width); - -// // Whatever the editor size is, ensure it has the same invisible kinds in the same order -// // (no good guarantees about the offsets: wrapping could trigger padding and its tests should check the offsets). -// let mut i = 0; -// for (actual_index, actual_invisible) in actual_invisibles.iter().enumerate() { -// i = actual_index; -// match expected_invisibles.get(i) { -// Some(expected_invisible) => match (expected_invisible, actual_invisible) { -// (Invisible::Whitespace { .. }, Invisible::Whitespace { .. }) -// | (Invisible::Tab { .. }, Invisible::Tab { .. }) => {} -// _ => { -// panic!("At index {i}, expected invisible {expected_invisible:?} does not match actual {actual_invisible:?} by kind. Actual invisibles: {actual_invisibles:?}") -// } -// }, -// None => panic!("Unexpected extra invisible {actual_invisible:?} at index {i}"), -// } -// } -// let missing_expected_invisibles = &expected_invisibles[i + 1..]; -// assert!( -// missing_expected_invisibles.is_empty, -// "Missing expected invisibles after index {i}: {missing_expected_invisibles:?}" -// ); - -// editor_width += resize_step; -// } -// } - -// fn collect_invisibles_from_new_editor( -// cx: &mut TestAppContext, -// editor_mode: EditorMode, -// input_text: &str, -// editor_width: f32, -// ) -> Vec { -// info!( -// "Creating editor with mode {editor_mode:?}, width {editor_width} and text '{input_text}'" -// ); -// let editor = cx -// .add_window(|cx| { -// let buffer = MultiBuffer::build_simple(&input_text, cx); -// Editor::new(editor_mode, buffer, None, None, cx) -// }) -// .root(cx); - -// let mut element = EditorElement::new(editor.read_with(cx, |editor, cx| editor.style(cx))); -// let (_, layout_state) = editor.update(cx, |editor, cx| { -// editor.set_soft_wrap_mode(language_settings::SoftWrap::EditorWidth, cx); -// editor.set_wrap_width(Some(editor_width), cx); - -// element.layout( -// SizeConstraint::new(point(editor_width, 500.), point(editor_width, 500.)), -// editor, -// cx, -// ) -// }); - -// layout_state -// .position_map -// .line_layouts -// .iter() -// .map(|line_with_invisibles| &line_with_invisibles.invisibles) -// .flatten() -// .cloned() -// .collect() -// } -// } +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + display_map::{BlockDisposition, BlockProperties}, + editor_tests::{init_test, update_test_language_settings}, + Editor, MultiBuffer, + }; + use gpui::TestAppContext; + use language::language_settings; + use log::info; + use std::{num::NonZeroU32, sync::Arc}; + use util::test::sample_text; + + #[gpui::test] + fn test_shape_line_numbers(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + let window = cx.add_window(|cx| { + let buffer = MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx); + Editor::new(EditorMode::Full, buffer, None, cx) + }); + + let editor = window.root(cx).unwrap(); + let style = cx.update(|cx| editor.read(cx).style().unwrap().clone()); + let element = EditorElement::new(&editor, style); + + let layouts = window + .update(cx, |editor, cx| { + let snapshot = editor.snapshot(cx); + element + .shape_line_numbers( + 0..6, + &Default::default(), + DisplayPoint::new(0, 0), + false, + &snapshot, + cx, + ) + .0 + }) + .unwrap(); + assert_eq!(layouts.len(), 6); + + let relative_rows = window + .update(cx, |editor, cx| { + let snapshot = editor.snapshot(cx); + element.calculate_relative_line_numbers(&snapshot, &(0..6), Some(3)) + }) + .unwrap(); + assert_eq!(relative_rows[&0], 3); + assert_eq!(relative_rows[&1], 2); + assert_eq!(relative_rows[&2], 1); + // current line has no relative number + assert_eq!(relative_rows[&4], 1); + assert_eq!(relative_rows[&5], 2); + + // works if cursor is before screen + let relative_rows = window + .update(cx, |editor, cx| { + let snapshot = editor.snapshot(cx); + + element.calculate_relative_line_numbers(&snapshot, &(3..6), Some(1)) + }) + .unwrap(); + assert_eq!(relative_rows.len(), 3); + assert_eq!(relative_rows[&3], 2); + assert_eq!(relative_rows[&4], 3); + assert_eq!(relative_rows[&5], 4); + + // works if cursor is after screen + let relative_rows = window + .update(cx, |editor, cx| { + let snapshot = editor.snapshot(cx); + + element.calculate_relative_line_numbers(&snapshot, &(0..3), Some(6)) + }) + .unwrap(); + assert_eq!(relative_rows.len(), 3); + assert_eq!(relative_rows[&0], 5); + assert_eq!(relative_rows[&1], 4); + assert_eq!(relative_rows[&2], 3); + } + + // #[gpui::test] + // async fn test_vim_visual_selections(cx: &mut TestAppContext) { + // init_test(cx, |_| {}); + + // let editor = cx + // .add_window(|cx| { + // let buffer = MultiBuffer::build_simple(&(sample_text(6, 6, 'a') + "\n"), cx); + // Editor::new(EditorMode::Full, buffer, None, None, cx) + // }) + // .root(cx); + // let mut element = EditorElement::new(editor.read_with(cx, |editor, cx| editor.style(cx))); + // let (_, state) = editor.update(cx, |editor, cx| { + // editor.cursor_shape = CursorShape::Block; + // editor.change_selections(None, cx, |s| { + // s.select_ranges([ + // Point::new(0, 0)..Point::new(1, 0), + // Point::new(3, 2)..Point::new(3, 3), + // Point::new(5, 6)..Point::new(6, 0), + // ]); + // }); + // element.layout( + // SizeConstraint::new(point(500., 500.), point(500., 500.)), + // editor, + // cx, + // ) + // }); + // assert_eq!(state.selections.len(), 1); + // let local_selections = &state.selections[0].1; + // assert_eq!(local_selections.len(), 3); + // // moves cursor back one line + // assert_eq!(local_selections[0].head, DisplayPoint::new(0, 6)); + // assert_eq!( + // local_selections[0].range, + // DisplayPoint::new(0, 0)..DisplayPoint::new(1, 0) + // ); + + // // moves cursor back one column + // assert_eq!( + // local_selections[1].range, + // DisplayPoint::new(3, 2)..DisplayPoint::new(3, 3) + // ); + // assert_eq!(local_selections[1].head, DisplayPoint::new(3, 2)); + + // // leaves cursor on the max point + // assert_eq!( + // local_selections[2].range, + // DisplayPoint::new(5, 6)..DisplayPoint::new(6, 0) + // ); + // assert_eq!(local_selections[2].head, DisplayPoint::new(6, 0)); + + // // active lines does not include 1 (even though the range of the selection does) + // assert_eq!( + // state.active_rows.keys().cloned().collect::>(), + // vec![0, 3, 5, 6] + // ); + + // // multi-buffer support + // // in DisplayPoint co-ordinates, this is what we're dealing with: + // // 0: [[file + // // 1: header]] + // // 2: aaaaaa + // // 3: bbbbbb + // // 4: cccccc + // // 5: + // // 6: ... + // // 7: ffffff + // // 8: gggggg + // // 9: hhhhhh + // // 10: + // // 11: [[file + // // 12: header]] + // // 13: bbbbbb + // // 14: cccccc + // // 15: dddddd + // let editor = cx + // .add_window(|cx| { + // let buffer = MultiBuffer::build_multi( + // [ + // ( + // &(sample_text(8, 6, 'a') + "\n"), + // vec![ + // Point::new(0, 0)..Point::new(3, 0), + // Point::new(4, 0)..Point::new(7, 0), + // ], + // ), + // ( + // &(sample_text(8, 6, 'a') + "\n"), + // vec![Point::new(1, 0)..Point::new(3, 0)], + // ), + // ], + // cx, + // ); + // Editor::new(EditorMode::Full, buffer, None, None, cx) + // }) + // .root(cx); + // let mut element = EditorElement::new(editor.read_with(cx, |editor, cx| editor.style(cx))); + // let (_, state) = editor.update(cx, |editor, cx| { + // editor.cursor_shape = CursorShape::Block; + // editor.change_selections(None, cx, |s| { + // s.select_display_ranges([ + // DisplayPoint::new(4, 0)..DisplayPoint::new(7, 0), + // DisplayPoint::new(10, 0)..DisplayPoint::new(13, 0), + // ]); + // }); + // element.layout( + // SizeConstraint::new(point(500., 500.), point(500., 500.)), + // editor, + // cx, + // ) + // }); + + // assert_eq!(state.selections.len(), 1); + // let local_selections = &state.selections[0].1; + // assert_eq!(local_selections.len(), 2); + + // // moves cursor on excerpt boundary back a line + // // and doesn't allow selection to bleed through + // assert_eq!( + // local_selections[0].range, + // DisplayPoint::new(4, 0)..DisplayPoint::new(6, 0) + // ); + // assert_eq!(local_selections[0].head, DisplayPoint::new(5, 0)); + + // // moves cursor on buffer boundary back two lines + // // and doesn't allow selection to bleed through + // assert_eq!( + // local_selections[1].range, + // DisplayPoint::new(10, 0)..DisplayPoint::new(11, 0) + // ); + // assert_eq!(local_selections[1].head, DisplayPoint::new(10, 0)); + // } + + // #[gpui::test] + // fn test_layout_with_placeholder_text_and_blocks(cx: &mut TestAppContext) { + // init_test(cx, |_| {}); + + // let editor = cx + // .add_window(|cx| { + // let buffer = MultiBuffer::build_simple("", cx); + // Editor::new(EditorMode::Full, buffer, None, None, cx) + // }) + // .root(cx); + + // editor.update(cx, |editor, cx| { + // editor.set_placeholder_text("hello", cx); + // editor.insert_blocks( + // [BlockProperties { + // style: BlockStyle::Fixed, + // disposition: BlockDisposition::Above, + // height: 3, + // position: Anchor::min(), + // render: Arc::new(|_| Empty::new().into_any), + // }], + // None, + // cx, + // ); + + // // Blur the editor so that it displays placeholder text. + // cx.blur(); + // }); + + // let mut element = EditorElement::new(editor.read_with(cx, |editor, cx| editor.style(cx))); + // let (size, mut state) = editor.update(cx, |editor, cx| { + // element.layout( + // SizeConstraint::new(point(500., 500.), point(500., 500.)), + // editor, + // cx, + // ) + // }); + + // assert_eq!(state.position_map.line_layouts.len(), 4); + // assert_eq!( + // state + // .line_number_layouts + // .iter() + // .map(Option::is_some) + // .collect::>(), + // &[false, false, false, true] + // ); + + // // Don't panic. + // let bounds = Bounds::::new(Default::default(), size); + // editor.update(cx, |editor, cx| { + // element.paint(bounds, bounds, &mut state, editor, cx); + // }); + // } + + // #[gpui::test] + // fn test_all_invisibles_drawing(cx: &mut TestAppContext) { + // const TAB_SIZE: u32 = 4; + + // let input_text = "\t \t|\t| a b"; + // let expected_invisibles = vec![ + // Invisible::Tab { + // line_start_offset: 0, + // }, + // Invisible::Whitespace { + // line_offset: TAB_SIZE as usize, + // }, + // Invisible::Tab { + // line_start_offset: TAB_SIZE as usize + 1, + // }, + // Invisible::Tab { + // line_start_offset: TAB_SIZE as usize * 2 + 1, + // }, + // Invisible::Whitespace { + // line_offset: TAB_SIZE as usize * 3 + 1, + // }, + // Invisible::Whitespace { + // line_offset: TAB_SIZE as usize * 3 + 3, + // }, + // ]; + // assert_eq!( + // expected_invisibles.len(), + // input_text + // .chars() + // .filter(|initial_char| initial_char.is_whitespace()) + // .count(), + // "Hardcoded expected invisibles differ from the actual ones in '{input_text}'" + // ); + + // init_test(cx, |s| { + // s.defaults.show_whitespaces = Some(ShowWhitespaceSetting::All); + // s.defaults.tab_size = NonZeroU32::new(TAB_SIZE); + // }); + + // let actual_invisibles = + // collect_invisibles_from_new_editor(cx, EditorMode::Full, &input_text, 500.0); + + // assert_eq!(expected_invisibles, actual_invisibles); + // } + + // #[gpui::test] + // fn test_invisibles_dont_appear_in_certain_editors(cx: &mut TestAppContext) { + // init_test(cx, |s| { + // s.defaults.show_whitespaces = Some(ShowWhitespaceSetting::All); + // s.defaults.tab_size = NonZeroU32::new(4); + // }); + + // for editor_mode_without_invisibles in [ + // EditorMode::SingleLine, + // EditorMode::AutoHeight { max_lines: 100 }, + // ] { + // let invisibles = collect_invisibles_from_new_editor( + // cx, + // editor_mode_without_invisibles, + // "\t\t\t| | a b", + // 500.0, + // ); + // assert!(invisibles.is_empty, + // "For editor mode {editor_mode_without_invisibles:?} no invisibles was expected but got {invisibles:?}"); + // } + // } + + // #[gpui::test] + // fn test_wrapped_invisibles_drawing(cx: &mut TestAppContext) { + // let tab_size = 4; + // let input_text = "a\tbcd ".repeat(9); + // let repeated_invisibles = [ + // Invisible::Tab { + // line_start_offset: 1, + // }, + // Invisible::Whitespace { + // line_offset: tab_size as usize + 3, + // }, + // Invisible::Whitespace { + // line_offset: tab_size as usize + 4, + // }, + // Invisible::Whitespace { + // line_offset: tab_size as usize + 5, + // }, + // ]; + // let expected_invisibles = std::iter::once(repeated_invisibles) + // .cycle() + // .take(9) + // .flatten() + // .collect::>(); + // assert_eq!( + // expected_invisibles.len(), + // input_text + // .chars() + // .filter(|initial_char| initial_char.is_whitespace()) + // .count(), + // "Hardcoded expected invisibles differ from the actual ones in '{input_text}'" + // ); + // info!("Expected invisibles: {expected_invisibles:?}"); + + // init_test(cx, |_| {}); + + // // Put the same string with repeating whitespace pattern into editors of various size, + // // take deliberately small steps during resizing, to put all whitespace kinds near the wrap point. + // let resize_step = 10.0; + // let mut editor_width = 200.0; + // while editor_width <= 1000.0 { + // update_test_language_settings(cx, |s| { + // s.defaults.tab_size = NonZeroU32::new(tab_size); + // s.defaults.show_whitespaces = Some(ShowWhitespaceSetting::All); + // s.defaults.preferred_line_length = Some(editor_width as u32); + // s.defaults.soft_wrap = Some(language_settings::SoftWrap::PreferredLineLength); + // }); + + // let actual_invisibles = + // collect_invisibles_from_new_editor(cx, EditorMode::Full, &input_text, editor_width); + + // // Whatever the editor size is, ensure it has the same invisible kinds in the same order + // // (no good guarantees about the offsets: wrapping could trigger padding and its tests should check the offsets). + // let mut i = 0; + // for (actual_index, actual_invisible) in actual_invisibles.iter().enumerate() { + // i = actual_index; + // match expected_invisibles.get(i) { + // Some(expected_invisible) => match (expected_invisible, actual_invisible) { + // (Invisible::Whitespace { .. }, Invisible::Whitespace { .. }) + // | (Invisible::Tab { .. }, Invisible::Tab { .. }) => {} + // _ => { + // panic!("At index {i}, expected invisible {expected_invisible:?} does not match actual {actual_invisible:?} by kind. Actual invisibles: {actual_invisibles:?}") + // } + // }, + // None => panic!("Unexpected extra invisible {actual_invisible:?} at index {i}"), + // } + // } + // let missing_expected_invisibles = &expected_invisibles[i + 1..]; + // assert!( + // missing_expected_invisibles.is_empty, + // "Missing expected invisibles after index {i}: {missing_expected_invisibles:?}" + // ); + + // editor_width += resize_step; + // } + // } + + // fn collect_invisibles_from_new_editor( + // cx: &mut TestAppContext, + // editor_mode: EditorMode, + // input_text: &str, + // editor_width: f32, + // ) -> Vec { + // info!( + // "Creating editor with mode {editor_mode:?}, width {editor_width} and text '{input_text}'" + // ); + // let editor = cx + // .add_window(|cx| { + // let buffer = MultiBuffer::build_simple(&input_text, cx); + // Editor::new(editor_mode, buffer, None, None, cx) + // }) + // .root(cx); + + // let mut element = EditorElement::new(editor.read_with(cx, |editor, cx| editor.style(cx))); + // let (_, layout_state) = editor.update(cx, |editor, cx| { + // editor.set_soft_wrap_mode(language_settings::SoftWrap::EditorWidth, cx); + // editor.set_wrap_width(Some(editor_width), cx); + + // element.layout( + // SizeConstraint::new(point(editor_width, 500.), point(editor_width, 500.)), + // editor, + // cx, + // ) + // }); + + // layout_state + // .position_map + // .line_layouts + // .iter() + // .map(|line_with_invisibles| &line_with_invisibles.invisibles) + // .flatten() + // .cloned() + // .collect() + // } +} pub fn register_action( view: &View, From ee695bbb349035220a710e23b733f4d576563c4c Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 5 Dec 2023 10:59:09 +0100 Subject: [PATCH 30/35] Fix up test_vim_visual_selections --- crates/editor2/src/element.rs | 262 ++++++++++++++++++---------------- 1 file changed, 139 insertions(+), 123 deletions(-) diff --git a/crates/editor2/src/element.rs b/crates/editor2/src/element.rs index f30f7337a1af10f21ad11e4203f401b548604372..50ae95a83d595ea955861af48f9b312d4f59be99 100644 --- a/crates/editor2/src/element.rs +++ b/crates/editor2/src/element.rs @@ -3310,137 +3310,153 @@ mod tests { assert_eq!(relative_rows[&2], 3); } - // #[gpui::test] - // async fn test_vim_visual_selections(cx: &mut TestAppContext) { - // init_test(cx, |_| {}); + #[gpui::test] + async fn test_vim_visual_selections(cx: &mut TestAppContext) { + init_test(cx, |_| {}); - // let editor = cx - // .add_window(|cx| { - // let buffer = MultiBuffer::build_simple(&(sample_text(6, 6, 'a') + "\n"), cx); - // Editor::new(EditorMode::Full, buffer, None, None, cx) - // }) - // .root(cx); - // let mut element = EditorElement::new(editor.read_with(cx, |editor, cx| editor.style(cx))); - // let (_, state) = editor.update(cx, |editor, cx| { - // editor.cursor_shape = CursorShape::Block; - // editor.change_selections(None, cx, |s| { - // s.select_ranges([ - // Point::new(0, 0)..Point::new(1, 0), - // Point::new(3, 2)..Point::new(3, 3), - // Point::new(5, 6)..Point::new(6, 0), - // ]); - // }); - // element.layout( - // SizeConstraint::new(point(500., 500.), point(500., 500.)), - // editor, - // cx, - // ) - // }); - // assert_eq!(state.selections.len(), 1); - // let local_selections = &state.selections[0].1; - // assert_eq!(local_selections.len(), 3); - // // moves cursor back one line - // assert_eq!(local_selections[0].head, DisplayPoint::new(0, 6)); - // assert_eq!( - // local_selections[0].range, - // DisplayPoint::new(0, 0)..DisplayPoint::new(1, 0) - // ); + let window = cx.add_window(|cx| { + let buffer = MultiBuffer::build_simple(&(sample_text(6, 6, 'a') + "\n"), cx); + Editor::new(EditorMode::Full, buffer, None, cx) + }); + let editor = window.root(cx).unwrap(); + let style = cx.update(|cx| editor.read(cx).style().unwrap().clone()); + let mut element = EditorElement::new(&editor, style); - // // moves cursor back one column - // assert_eq!( - // local_selections[1].range, - // DisplayPoint::new(3, 2)..DisplayPoint::new(3, 3) - // ); - // assert_eq!(local_selections[1].head, DisplayPoint::new(3, 2)); + window + .update(cx, |editor, cx| { + editor.cursor_shape = CursorShape::Block; + editor.change_selections(None, cx, |s| { + s.select_ranges([ + Point::new(0, 0)..Point::new(1, 0), + Point::new(3, 2)..Point::new(3, 3), + Point::new(5, 6)..Point::new(6, 0), + ]); + }); + }) + .unwrap(); + let state = cx + .update_window(window.into(), |_, cx| { + element.compute_layout( + Bounds { + origin: point(px(500.), px(500.)), + size: size(px(500.), px(500.)), + }, + cx, + ) + }) + .unwrap(); - // // leaves cursor on the max point - // assert_eq!( - // local_selections[2].range, - // DisplayPoint::new(5, 6)..DisplayPoint::new(6, 0) - // ); - // assert_eq!(local_selections[2].head, DisplayPoint::new(6, 0)); + assert_eq!(state.selections.len(), 1); + let local_selections = &state.selections[0].1; + assert_eq!(local_selections.len(), 3); + // moves cursor back one line + assert_eq!(local_selections[0].head, DisplayPoint::new(0, 6)); + assert_eq!( + local_selections[0].range, + DisplayPoint::new(0, 0)..DisplayPoint::new(1, 0) + ); - // // active lines does not include 1 (even though the range of the selection does) - // assert_eq!( - // state.active_rows.keys().cloned().collect::>(), - // vec![0, 3, 5, 6] - // ); + // moves cursor back one column + assert_eq!( + local_selections[1].range, + DisplayPoint::new(3, 2)..DisplayPoint::new(3, 3) + ); + assert_eq!(local_selections[1].head, DisplayPoint::new(3, 2)); - // // multi-buffer support - // // in DisplayPoint co-ordinates, this is what we're dealing with: - // // 0: [[file - // // 1: header]] - // // 2: aaaaaa - // // 3: bbbbbb - // // 4: cccccc - // // 5: - // // 6: ... - // // 7: ffffff - // // 8: gggggg - // // 9: hhhhhh - // // 10: - // // 11: [[file - // // 12: header]] - // // 13: bbbbbb - // // 14: cccccc - // // 15: dddddd - // let editor = cx - // .add_window(|cx| { - // let buffer = MultiBuffer::build_multi( - // [ - // ( - // &(sample_text(8, 6, 'a') + "\n"), - // vec![ - // Point::new(0, 0)..Point::new(3, 0), - // Point::new(4, 0)..Point::new(7, 0), - // ], - // ), - // ( - // &(sample_text(8, 6, 'a') + "\n"), - // vec![Point::new(1, 0)..Point::new(3, 0)], - // ), - // ], - // cx, - // ); - // Editor::new(EditorMode::Full, buffer, None, None, cx) - // }) - // .root(cx); - // let mut element = EditorElement::new(editor.read_with(cx, |editor, cx| editor.style(cx))); - // let (_, state) = editor.update(cx, |editor, cx| { - // editor.cursor_shape = CursorShape::Block; - // editor.change_selections(None, cx, |s| { - // s.select_display_ranges([ - // DisplayPoint::new(4, 0)..DisplayPoint::new(7, 0), - // DisplayPoint::new(10, 0)..DisplayPoint::new(13, 0), - // ]); - // }); - // element.layout( - // SizeConstraint::new(point(500., 500.), point(500., 500.)), - // editor, - // cx, - // ) - // }); + // leaves cursor on the max point + assert_eq!( + local_selections[2].range, + DisplayPoint::new(5, 6)..DisplayPoint::new(6, 0) + ); + assert_eq!(local_selections[2].head, DisplayPoint::new(6, 0)); - // assert_eq!(state.selections.len(), 1); - // let local_selections = &state.selections[0].1; - // assert_eq!(local_selections.len(), 2); + // active lines does not include 1 (even though the range of the selection does) + assert_eq!( + state.active_rows.keys().cloned().collect::>(), + vec![0, 3, 5, 6] + ); - // // moves cursor on excerpt boundary back a line - // // and doesn't allow selection to bleed through - // assert_eq!( - // local_selections[0].range, - // DisplayPoint::new(4, 0)..DisplayPoint::new(6, 0) - // ); - // assert_eq!(local_selections[0].head, DisplayPoint::new(5, 0)); + // multi-buffer support + // in DisplayPoint co-ordinates, this is what we're dealing with: + // 0: [[file + // 1: header]] + // 2: aaaaaa + // 3: bbbbbb + // 4: cccccc + // 5: + // 6: ... + // 7: ffffff + // 8: gggggg + // 9: hhhhhh + // 10: + // 11: [[file + // 12: header]] + // 13: bbbbbb + // 14: cccccc + // 15: dddddd + let window = cx.add_window(|cx| { + let buffer = MultiBuffer::build_multi( + [ + ( + &(sample_text(8, 6, 'a') + "\n"), + vec![ + Point::new(0, 0)..Point::new(3, 0), + Point::new(4, 0)..Point::new(7, 0), + ], + ), + ( + &(sample_text(8, 6, 'a') + "\n"), + vec![Point::new(1, 0)..Point::new(3, 0)], + ), + ], + cx, + ); + Editor::new(EditorMode::Full, buffer, None, cx) + }); + let editor = window.root(cx).unwrap(); + let style = cx.update(|cx| editor.read(cx).style().unwrap().clone()); + let mut element = EditorElement::new(&editor, style); + let state = window.update(cx, |editor, cx| { + editor.cursor_shape = CursorShape::Block; + editor.change_selections(None, cx, |s| { + s.select_display_ranges([ + DisplayPoint::new(4, 0)..DisplayPoint::new(7, 0), + DisplayPoint::new(10, 0)..DisplayPoint::new(13, 0), + ]); + }); + }); - // // moves cursor on buffer boundary back two lines - // // and doesn't allow selection to bleed through - // assert_eq!( - // local_selections[1].range, - // DisplayPoint::new(10, 0)..DisplayPoint::new(11, 0) - // ); - // assert_eq!(local_selections[1].head, DisplayPoint::new(10, 0)); - // } + let state = cx + .update_window(window.into(), |_, cx| { + element.compute_layout( + Bounds { + origin: point(px(500.), px(500.)), + size: size(px(500.), px(500.)), + }, + cx, + ) + }) + .unwrap(); + assert_eq!(state.selections.len(), 1); + let local_selections = &state.selections[0].1; + assert_eq!(local_selections.len(), 2); + + // moves cursor on excerpt boundary back a line + // and doesn't allow selection to bleed through + assert_eq!( + local_selections[0].range, + DisplayPoint::new(4, 0)..DisplayPoint::new(6, 0) + ); + assert_eq!(local_selections[0].head, DisplayPoint::new(5, 0)); + dbg!("Hi"); + // moves cursor on buffer boundary back two lines + // and doesn't allow selection to bleed through + assert_eq!( + local_selections[1].range, + DisplayPoint::new(10, 0)..DisplayPoint::new(11, 0) + ); + assert_eq!(local_selections[1].head, DisplayPoint::new(10, 0)); + } // #[gpui::test] // fn test_layout_with_placeholder_text_and_blocks(cx: &mut TestAppContext) { From 1dd6625dd4c2df730c8011445f87a0f6577133d2 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 5 Dec 2023 11:09:23 +0100 Subject: [PATCH 31/35] test_all_invisibles_drawing --- crates/editor2/src/element.rs | 159 ++++++++++++++++++---------------- 1 file changed, 83 insertions(+), 76 deletions(-) diff --git a/crates/editor2/src/element.rs b/crates/editor2/src/element.rs index 50ae95a83d595ea955861af48f9b312d4f59be99..50fcc3a2ba5e6c260f0fd33c75ee0f07c60a680c 100644 --- a/crates/editor2/src/element.rs +++ b/crates/editor2/src/element.rs @@ -3513,50 +3513,50 @@ mod tests { // }); // } - // #[gpui::test] - // fn test_all_invisibles_drawing(cx: &mut TestAppContext) { - // const TAB_SIZE: u32 = 4; + #[gpui::test] + fn test_all_invisibles_drawing(cx: &mut TestAppContext) { + const TAB_SIZE: u32 = 4; - // let input_text = "\t \t|\t| a b"; - // let expected_invisibles = vec![ - // Invisible::Tab { - // line_start_offset: 0, - // }, - // Invisible::Whitespace { - // line_offset: TAB_SIZE as usize, - // }, - // Invisible::Tab { - // line_start_offset: TAB_SIZE as usize + 1, - // }, - // Invisible::Tab { - // line_start_offset: TAB_SIZE as usize * 2 + 1, - // }, - // Invisible::Whitespace { - // line_offset: TAB_SIZE as usize * 3 + 1, - // }, - // Invisible::Whitespace { - // line_offset: TAB_SIZE as usize * 3 + 3, - // }, - // ]; - // assert_eq!( - // expected_invisibles.len(), - // input_text - // .chars() - // .filter(|initial_char| initial_char.is_whitespace()) - // .count(), - // "Hardcoded expected invisibles differ from the actual ones in '{input_text}'" - // ); + let input_text = "\t \t|\t| a b"; + let expected_invisibles = vec![ + Invisible::Tab { + line_start_offset: 0, + }, + Invisible::Whitespace { + line_offset: TAB_SIZE as usize, + }, + Invisible::Tab { + line_start_offset: TAB_SIZE as usize + 1, + }, + Invisible::Tab { + line_start_offset: TAB_SIZE as usize * 2 + 1, + }, + Invisible::Whitespace { + line_offset: TAB_SIZE as usize * 3 + 1, + }, + Invisible::Whitespace { + line_offset: TAB_SIZE as usize * 3 + 3, + }, + ]; + assert_eq!( + expected_invisibles.len(), + input_text + .chars() + .filter(|initial_char| initial_char.is_whitespace()) + .count(), + "Hardcoded expected invisibles differ from the actual ones in '{input_text}'" + ); - // init_test(cx, |s| { - // s.defaults.show_whitespaces = Some(ShowWhitespaceSetting::All); - // s.defaults.tab_size = NonZeroU32::new(TAB_SIZE); - // }); + init_test(cx, |s| { + s.defaults.show_whitespaces = Some(ShowWhitespaceSetting::All); + s.defaults.tab_size = NonZeroU32::new(TAB_SIZE); + }); - // let actual_invisibles = - // collect_invisibles_from_new_editor(cx, EditorMode::Full, &input_text, 500.0); + let actual_invisibles = + collect_invisibles_from_new_editor(cx, EditorMode::Full, &input_text, px(500.0)); - // assert_eq!(expected_invisibles, actual_invisibles); - // } + assert_eq!(expected_invisibles, actual_invisibles); + } // #[gpui::test] // fn test_invisibles_dont_appear_in_certain_editors(cx: &mut TestAppContext) { @@ -3656,43 +3656,50 @@ mod tests { // } // } - // fn collect_invisibles_from_new_editor( - // cx: &mut TestAppContext, - // editor_mode: EditorMode, - // input_text: &str, - // editor_width: f32, - // ) -> Vec { - // info!( - // "Creating editor with mode {editor_mode:?}, width {editor_width} and text '{input_text}'" - // ); - // let editor = cx - // .add_window(|cx| { - // let buffer = MultiBuffer::build_simple(&input_text, cx); - // Editor::new(editor_mode, buffer, None, None, cx) - // }) - // .root(cx); - - // let mut element = EditorElement::new(editor.read_with(cx, |editor, cx| editor.style(cx))); - // let (_, layout_state) = editor.update(cx, |editor, cx| { - // editor.set_soft_wrap_mode(language_settings::SoftWrap::EditorWidth, cx); - // editor.set_wrap_width(Some(editor_width), cx); - - // element.layout( - // SizeConstraint::new(point(editor_width, 500.), point(editor_width, 500.)), - // editor, - // cx, - // ) - // }); + fn collect_invisibles_from_new_editor( + cx: &mut TestAppContext, + editor_mode: EditorMode, + input_text: &str, + editor_width: Pixels, + ) -> Vec { + info!( + "Creating editor with mode {editor_mode:?}, width {}px and text '{input_text}'", + editor_width.0 + ); + let window = cx.add_window(|cx| { + let buffer = MultiBuffer::build_simple(&input_text, cx); + Editor::new(editor_mode, buffer, None, cx) + }); + let editor = window.root(cx).unwrap(); + let style = cx.update(|cx| editor.read(cx).style().unwrap().clone()); + let mut element = EditorElement::new(&editor, style); + window + .update(cx, |editor, cx| { + editor.set_soft_wrap_mode(language_settings::SoftWrap::EditorWidth, cx); + editor.set_wrap_width(Some(editor_width), cx); + }) + .unwrap(); + let layout_state = cx + .update_window(window.into(), |_, cx| { + element.compute_layout( + Bounds { + origin: point(px(500.), px(500.)), + size: size(px(500.), px(500.)), + }, + cx, + ) + }) + .unwrap(); - // layout_state - // .position_map - // .line_layouts - // .iter() - // .map(|line_with_invisibles| &line_with_invisibles.invisibles) - // .flatten() - // .cloned() - // .collect() - // } + layout_state + .position_map + .line_layouts + .iter() + .map(|line_with_invisibles| &line_with_invisibles.invisibles) + .flatten() + .cloned() + .collect() + } } pub fn register_action( From caa5fccbc43499d86d1df4b29b811753bf842a72 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 5 Dec 2023 11:11:06 +0100 Subject: [PATCH 32/35] test_wrapped_invisibles_drawing and test_invisibles_dont_appear_in_certain_editors --- crates/editor2/src/element.rs | 188 +++++++++++++++++----------------- 1 file changed, 96 insertions(+), 92 deletions(-) diff --git a/crates/editor2/src/element.rs b/crates/editor2/src/element.rs index 50fcc3a2ba5e6c260f0fd33c75ee0f07c60a680c..d128b8c7851dbcd86fd7e4633460ac490534ad91 100644 --- a/crates/editor2/src/element.rs +++ b/crates/editor2/src/element.rs @@ -3558,103 +3558,107 @@ mod tests { assert_eq!(expected_invisibles, actual_invisibles); } - // #[gpui::test] - // fn test_invisibles_dont_appear_in_certain_editors(cx: &mut TestAppContext) { - // init_test(cx, |s| { - // s.defaults.show_whitespaces = Some(ShowWhitespaceSetting::All); - // s.defaults.tab_size = NonZeroU32::new(4); - // }); + #[gpui::test] + fn test_invisibles_dont_appear_in_certain_editors(cx: &mut TestAppContext) { + init_test(cx, |s| { + s.defaults.show_whitespaces = Some(ShowWhitespaceSetting::All); + s.defaults.tab_size = NonZeroU32::new(4); + }); - // for editor_mode_without_invisibles in [ - // EditorMode::SingleLine, - // EditorMode::AutoHeight { max_lines: 100 }, - // ] { - // let invisibles = collect_invisibles_from_new_editor( - // cx, - // editor_mode_without_invisibles, - // "\t\t\t| | a b", - // 500.0, - // ); - // assert!(invisibles.is_empty, - // "For editor mode {editor_mode_without_invisibles:?} no invisibles was expected but got {invisibles:?}"); - // } - // } + for editor_mode_without_invisibles in [ + EditorMode::SingleLine, + EditorMode::AutoHeight { max_lines: 100 }, + ] { + let invisibles = collect_invisibles_from_new_editor( + cx, + editor_mode_without_invisibles, + "\t\t\t| | a b", + px(500.0), + ); + assert!(invisibles.is_empty(), + "For editor mode {editor_mode_without_invisibles:?} no invisibles was expected but got {invisibles:?}"); + } + } - // #[gpui::test] - // fn test_wrapped_invisibles_drawing(cx: &mut TestAppContext) { - // let tab_size = 4; - // let input_text = "a\tbcd ".repeat(9); - // let repeated_invisibles = [ - // Invisible::Tab { - // line_start_offset: 1, - // }, - // Invisible::Whitespace { - // line_offset: tab_size as usize + 3, - // }, - // Invisible::Whitespace { - // line_offset: tab_size as usize + 4, - // }, - // Invisible::Whitespace { - // line_offset: tab_size as usize + 5, - // }, - // ]; - // let expected_invisibles = std::iter::once(repeated_invisibles) - // .cycle() - // .take(9) - // .flatten() - // .collect::>(); - // assert_eq!( - // expected_invisibles.len(), - // input_text - // .chars() - // .filter(|initial_char| initial_char.is_whitespace()) - // .count(), - // "Hardcoded expected invisibles differ from the actual ones in '{input_text}'" - // ); - // info!("Expected invisibles: {expected_invisibles:?}"); + #[gpui::test] + fn test_wrapped_invisibles_drawing(cx: &mut TestAppContext) { + let tab_size = 4; + let input_text = "a\tbcd ".repeat(9); + let repeated_invisibles = [ + Invisible::Tab { + line_start_offset: 1, + }, + Invisible::Whitespace { + line_offset: tab_size as usize + 3, + }, + Invisible::Whitespace { + line_offset: tab_size as usize + 4, + }, + Invisible::Whitespace { + line_offset: tab_size as usize + 5, + }, + ]; + let expected_invisibles = std::iter::once(repeated_invisibles) + .cycle() + .take(9) + .flatten() + .collect::>(); + assert_eq!( + expected_invisibles.len(), + input_text + .chars() + .filter(|initial_char| initial_char.is_whitespace()) + .count(), + "Hardcoded expected invisibles differ from the actual ones in '{input_text}'" + ); + info!("Expected invisibles: {expected_invisibles:?}"); - // init_test(cx, |_| {}); + init_test(cx, |_| {}); - // // Put the same string with repeating whitespace pattern into editors of various size, - // // take deliberately small steps during resizing, to put all whitespace kinds near the wrap point. - // let resize_step = 10.0; - // let mut editor_width = 200.0; - // while editor_width <= 1000.0 { - // update_test_language_settings(cx, |s| { - // s.defaults.tab_size = NonZeroU32::new(tab_size); - // s.defaults.show_whitespaces = Some(ShowWhitespaceSetting::All); - // s.defaults.preferred_line_length = Some(editor_width as u32); - // s.defaults.soft_wrap = Some(language_settings::SoftWrap::PreferredLineLength); - // }); - - // let actual_invisibles = - // collect_invisibles_from_new_editor(cx, EditorMode::Full, &input_text, editor_width); - - // // Whatever the editor size is, ensure it has the same invisible kinds in the same order - // // (no good guarantees about the offsets: wrapping could trigger padding and its tests should check the offsets). - // let mut i = 0; - // for (actual_index, actual_invisible) in actual_invisibles.iter().enumerate() { - // i = actual_index; - // match expected_invisibles.get(i) { - // Some(expected_invisible) => match (expected_invisible, actual_invisible) { - // (Invisible::Whitespace { .. }, Invisible::Whitespace { .. }) - // | (Invisible::Tab { .. }, Invisible::Tab { .. }) => {} - // _ => { - // panic!("At index {i}, expected invisible {expected_invisible:?} does not match actual {actual_invisible:?} by kind. Actual invisibles: {actual_invisibles:?}") - // } - // }, - // None => panic!("Unexpected extra invisible {actual_invisible:?} at index {i}"), - // } - // } - // let missing_expected_invisibles = &expected_invisibles[i + 1..]; - // assert!( - // missing_expected_invisibles.is_empty, - // "Missing expected invisibles after index {i}: {missing_expected_invisibles:?}" - // ); + // Put the same string with repeating whitespace pattern into editors of various size, + // take deliberately small steps during resizing, to put all whitespace kinds near the wrap point. + let resize_step = 10.0; + let mut editor_width = 200.0; + while editor_width <= 1000.0 { + update_test_language_settings(cx, |s| { + s.defaults.tab_size = NonZeroU32::new(tab_size); + s.defaults.show_whitespaces = Some(ShowWhitespaceSetting::All); + s.defaults.preferred_line_length = Some(editor_width as u32); + s.defaults.soft_wrap = Some(language_settings::SoftWrap::PreferredLineLength); + }); - // editor_width += resize_step; - // } - // } + let actual_invisibles = collect_invisibles_from_new_editor( + cx, + EditorMode::Full, + &input_text, + px(editor_width), + ); + + // Whatever the editor size is, ensure it has the same invisible kinds in the same order + // (no good guarantees about the offsets: wrapping could trigger padding and its tests should check the offsets). + let mut i = 0; + for (actual_index, actual_invisible) in actual_invisibles.iter().enumerate() { + i = actual_index; + match expected_invisibles.get(i) { + Some(expected_invisible) => match (expected_invisible, actual_invisible) { + (Invisible::Whitespace { .. }, Invisible::Whitespace { .. }) + | (Invisible::Tab { .. }, Invisible::Tab { .. }) => {} + _ => { + panic!("At index {i}, expected invisible {expected_invisible:?} does not match actual {actual_invisible:?} by kind. Actual invisibles: {actual_invisibles:?}") + } + }, + None => panic!("Unexpected extra invisible {actual_invisible:?} at index {i}"), + } + } + let missing_expected_invisibles = &expected_invisibles[i + 1..]; + assert!( + missing_expected_invisibles.is_empty(), + "Missing expected invisibles after index {i}: {missing_expected_invisibles:?}" + ); + + editor_width += resize_step; + } + } fn collect_invisibles_from_new_editor( cx: &mut TestAppContext, From 590238bcca7c08c95c97ffdfe94ec18be90e2b53 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 5 Dec 2023 11:28:25 +0100 Subject: [PATCH 33/35] test_layout_with_placeholder_text_and_blocks (incomplete, one assert commented out) We need to wire up a field from element state --- crates/editor2/src/element.rs | 112 ++++++++++++++++++---------------- 1 file changed, 60 insertions(+), 52 deletions(-) diff --git a/crates/editor2/src/element.rs b/crates/editor2/src/element.rs index d128b8c7851dbcd86fd7e4633460ac490534ad91..a63593fcaf4fc6504c785baeeba5083b674aee57 100644 --- a/crates/editor2/src/element.rs +++ b/crates/editor2/src/element.rs @@ -3235,7 +3235,7 @@ mod tests { editor_tests::{init_test, update_test_language_settings}, Editor, MultiBuffer, }; - use gpui::TestAppContext; + use gpui::{EmptyView, TestAppContext}; use language::language_settings; use log::info; use std::{num::NonZeroU32, sync::Arc}; @@ -3458,60 +3458,68 @@ mod tests { assert_eq!(local_selections[1].head, DisplayPoint::new(10, 0)); } - // #[gpui::test] - // fn test_layout_with_placeholder_text_and_blocks(cx: &mut TestAppContext) { - // init_test(cx, |_| {}); + #[gpui::test] + fn test_layout_with_placeholder_text_and_blocks(cx: &mut TestAppContext) { + init_test(cx, |_| {}); - // let editor = cx - // .add_window(|cx| { - // let buffer = MultiBuffer::build_simple("", cx); - // Editor::new(EditorMode::Full, buffer, None, None, cx) - // }) - // .root(cx); - - // editor.update(cx, |editor, cx| { - // editor.set_placeholder_text("hello", cx); - // editor.insert_blocks( - // [BlockProperties { - // style: BlockStyle::Fixed, - // disposition: BlockDisposition::Above, - // height: 3, - // position: Anchor::min(), - // render: Arc::new(|_| Empty::new().into_any), - // }], - // None, - // cx, - // ); - - // // Blur the editor so that it displays placeholder text. - // cx.blur(); - // }); + let window = cx.add_window(|cx| { + let buffer = MultiBuffer::build_simple("", cx); + Editor::new(EditorMode::Full, buffer, None, cx) + }); + let editor = window.root(cx).unwrap(); + let style = cx.update(|cx| editor.read(cx).style().unwrap().clone()); + window + .update(cx, |editor, cx| { + editor.set_placeholder_text("hello", cx); + editor.insert_blocks( + [BlockProperties { + style: BlockStyle::Fixed, + disposition: BlockDisposition::Above, + height: 3, + position: Anchor::min(), + render: Arc::new(|_| div().into_any()), + }], + None, + cx, + ); - // let mut element = EditorElement::new(editor.read_with(cx, |editor, cx| editor.style(cx))); - // let (size, mut state) = editor.update(cx, |editor, cx| { - // element.layout( - // SizeConstraint::new(point(500., 500.), point(500., 500.)), - // editor, - // cx, - // ) - // }); + // Blur the editor so that it displays placeholder text. + cx.blur(); + }) + .unwrap(); - // assert_eq!(state.position_map.line_layouts.len(), 4); - // assert_eq!( - // state - // .line_number_layouts - // .iter() - // .map(Option::is_some) - // .collect::>(), - // &[false, false, false, true] - // ); - - // // Don't panic. - // let bounds = Bounds::::new(Default::default(), size); - // editor.update(cx, |editor, cx| { - // element.paint(bounds, bounds, &mut state, editor, cx); - // }); - // } + let mut element = EditorElement::new(&editor, style); + let mut state = cx + .update_window(window.into(), |_, cx| { + element.compute_layout( + Bounds { + origin: point(px(500.), px(500.)), + size: size(px(500.), px(500.)), + }, + cx, + ) + }) + .unwrap(); + let size = state.position_map.size; + + assert_eq!(state.position_map.line_layouts.len(), 4); + // todo!() uncomment this assert + // assert_eq!( + // state + // .line_number_layouts + // .iter() + // .map(Option::is_some) + // .collect::>(), + // &[false, false, false, true] + // ); + + // Don't panic. + let bounds = Bounds::::new(Default::default(), size); + cx.update_window(window.into(), |_, cx| { + element.paint(bounds, &mut (), cx); + }) + .unwrap() + } #[gpui::test] fn test_all_invisibles_drawing(cx: &mut TestAppContext) { From dffe323e735a8d3dc5065bfa3ffa57b2b6b65d9e Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 5 Dec 2023 11:31:17 +0100 Subject: [PATCH 34/35] Fix up test_lkayout_with_placeholder_text_and_blocks (uncomment last standing assert) --- crates/editor2/src/element.rs | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/crates/editor2/src/element.rs b/crates/editor2/src/element.rs index a63593fcaf4fc6504c785baeeba5083b674aee57..5cfb72470ff26789e5231365304e5ebe0c902a70 100644 --- a/crates/editor2/src/element.rs +++ b/crates/editor2/src/element.rs @@ -3503,15 +3503,14 @@ mod tests { let size = state.position_map.size; assert_eq!(state.position_map.line_layouts.len(), 4); - // todo!() uncomment this assert - // assert_eq!( - // state - // .line_number_layouts - // .iter() - // .map(Option::is_some) - // .collect::>(), - // &[false, false, false, true] - // ); + assert_eq!( + state + .line_numbers + .iter() + .map(Option::is_some) + .collect::>(), + &[false, false, false, true] + ); // Don't panic. let bounds = Bounds::::new(Default::default(), size); From 001ce47a0c438d0e3f4789bc616f1e2179ac853c Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 5 Dec 2023 14:10:10 +0100 Subject: [PATCH 35/35] Fix up the inlay_hint_cache proper (document the bug around inserting at inlay hint). Co-authored-by: Antonio Co-authored-by: Kirill --- crates/editor2/src/inlay_hint_cache.rs | 52 +++++++++++++------------- 1 file changed, 25 insertions(+), 27 deletions(-) diff --git a/crates/editor2/src/inlay_hint_cache.rs b/crates/editor2/src/inlay_hint_cache.rs index 18a061276e9e5e5f44056dfaf9d8edfc5b6ec7c5..aab985ff9030988481796b0a4181189662f749c9 100644 --- a/crates/editor2/src/inlay_hint_cache.rs +++ b/crates/editor2/src/inlay_hint_cache.rs @@ -2403,7 +2403,6 @@ pub mod tests { #[gpui::test(iterations = 10)] async fn test_multiple_excerpts_large_multibuffer(cx: &mut gpui::TestAppContext) { - // todo!() this test is flaky init_test(cx, |settings| { settings.defaults.inlay_hints = Some(InlayHintSettings { enabled: true, @@ -2604,8 +2603,6 @@ pub mod tests { "main hint #1".to_string(), "main hint #2".to_string(), "main hint #3".to_string(), - // todo!() there used to be no these hints, but new gpui2 presumably scrolls a bit farther - // (or renders less?) note that tests below pass "main hint #4".to_string(), "main hint #5".to_string(), ]; @@ -2710,37 +2707,38 @@ pub mod tests { editor_edited.store(true, Ordering::Release); editor.update(cx, |editor, cx| { editor.change_selections(None, cx, |s| { - s.select_ranges([Point::new(56, 0)..Point::new(56, 0)]) + // TODO if this gets set to hint boundary (e.g. 56) we sometimes get an extra cache version bump, why? + s.select_ranges([Point::new(57, 0)..Point::new(57, 0)]) }); editor.handle_input("++++more text++++", cx); }); cx.executor().run_until_parked(); editor.update(cx, |editor, cx| { - let expected_hints = vec![ - "main hint(edited) #0".to_string(), - "main hint(edited) #1".to_string(), - "main hint(edited) #2".to_string(), - "main hint(edited) #3".to_string(), - "main hint(edited) #4".to_string(), - "main hint(edited) #5".to_string(), - "other hint(edited) #0".to_string(), - "other hint(edited) #1".to_string(), - ]; - assert_eq!( - expected_hints, - cached_hint_labels(editor), - "After multibuffer edit, editor gets scolled back to the last selection; \ + let expected_hints = vec![ + "main hint(edited) #0".to_string(), + "main hint(edited) #1".to_string(), + "main hint(edited) #2".to_string(), + "main hint(edited) #3".to_string(), + "main hint(edited) #4".to_string(), + "main hint(edited) #5".to_string(), + "other hint(edited) #0".to_string(), + "other hint(edited) #1".to_string(), + ]; + assert_eq!( + expected_hints, + cached_hint_labels(editor), + "After multibuffer edit, editor gets scolled back to the last selection; \ all hints should be invalidated and requeried for all of its visible excerpts" - ); - assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + ); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); - let current_cache_version = editor.inlay_hint_cache().version; - let minimum_expected_version = last_scroll_update_version + expected_hints.len(); - assert!( - current_cache_version >= minimum_expected_version, - "TODO: Something happens with multi-excerpt buffer when editing it: we query overly many inlay hints instead of just visible excerpts" - ); - }); + let current_cache_version = editor.inlay_hint_cache().version; + assert_eq!( + current_cache_version, + last_scroll_update_version + expected_hints.len(), + "We should have updated cache N times == N of new hints arrived (separately from each excerpt)" + ); + }); } #[gpui::test]