diff --git a/Cargo.lock b/Cargo.lock index e08eb58f5e..29509ad4b5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4408,6 +4408,7 @@ dependencies = [ "lsp2", "parking_lot 0.11.2", "postage", + "pulldown-cmark", "rand 0.8.5", "regex", "rpc2", diff --git a/crates/editor2/src/display_map.rs b/crates/editor2/src/display_map.rs index a26b07efec..5848a12531 100644 --- a/crates/editor2/src/display_map.rs +++ b/crates/editor2/src/display_map.rs @@ -11,11 +11,7 @@ use crate::{ pub use block_map::{BlockMap, BlockPoint}; use collections::{BTreeMap, HashMap, HashSet}; use fold_map::FoldMap; -use gpui::{ - fonts::{FontId, HighlightStyle, Underline}, - text_layout::{Line, RunStyle}, - Entity, Hsla, Model, ModelContext, -}; +use gpui::{FontId, HighlightStyle, Hsla, Line, Model, ModelContext, UnderlineStyle}; use inlay_map::InlayMap; use language::{ language_settings::language_settings, OffsetUtf16, Point, Subscription as BufferSubscription, @@ -60,10 +56,6 @@ pub struct DisplayMap { pub clip_at_line_ends: bool, } -impl Entity for DisplayMap { - type Event = (); -} - impl DisplayMap { pub fn new( buffer: Model, @@ -253,7 +245,7 @@ impl DisplayMap { .update(cx, |map, cx| map.set_font(font_id, font_size, cx)) } - pub fn set_fold_ellipses_color(&mut self, color: Color) -> bool { + pub fn set_fold_ellipses_color(&mut self, color: Hsla) -> bool { self.fold_map.set_ellipses_color(color) } @@ -295,7 +287,7 @@ impl DisplayMap { self.block_map.read(snapshot, edits); } - fn tab_size(buffer: &ModelHandle, cx: &mut ModelContext) -> NonZeroU32 { + fn tab_size(buffer: &Model, cx: &mut ModelContext) -> NonZeroU32 { let language = buffer .read(cx) .as_singleton() @@ -540,10 +532,10 @@ impl DisplaySnapshot { // Omit underlines for HINT/INFO diagnostics on 'unnecessary' code. if severity <= DiagnosticSeverity::WARNING || !chunk.is_unnecessary { let diagnostic_style = super::diagnostic_style(severity, true, style); - diagnostic_highlight.underline = Some(Underline { + diagnostic_highlight.underline = Some(UnderlineStyle { color: Some(diagnostic_style.message.text.color), thickness: 1.0.into(), - squiggly: true, + wavy: true, }); } } @@ -566,8 +558,8 @@ impl DisplaySnapshot { &self, display_row: u32, TextLayoutDetails { - font_cache, - text_layout_cache, + text_system: font_cache, + text_system: text_layout_cache, editor_style, }: &TextLayoutDetails, ) -> Line { @@ -591,14 +583,12 @@ impl DisplaySnapshot { }; ended_in_newline = chunk.chunk.ends_with("\n"); - styles.push(( - chunk.chunk.len(), - RunStyle { - font_id: text_style.font_id, - color: text_style.color, - underline: text_style.underline, - }, - )); + styles.push( + todo!(), // len: chunk.chunk.len(), + // font_id: text_style.font_id, + // color: text_style.color, + // underline: text_style.underline, + ); } // our pixel positioning logic assumes each line ends in \n, @@ -607,17 +597,16 @@ impl DisplaySnapshot { if !ended_in_newline && display_row == self.max_point().row() { line.push_str("\n"); - styles.push(( - "\n".len(), - RunStyle { - font_id: editor_style.text.font_id, - color: editor_style.text_color, - underline: editor_style.text.underline, - }, - )); + todo!(); + // styles.push(RunStyle { + // len: "\n".len(), + // font_id: editor_style.text.font_id, + // color: editor_style.text_color, + // underline: editor_style.text.underline, + // }); } - text_layout_cache.layout_str(&line, editor_style.text.font_size, &styles) + text_layout_cache.layout_text(&line, editor_style.text.font_size, &styles) } pub fn x_for_point( @@ -1007,905 +996,905 @@ 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::{elements::*, test::observe, 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(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(), Color::red().into()), - ("fn.name".to_string(), Color::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(Color::blue())), - ("() {}\n\nmod module ".to_string(), None), - ("{\n fn ".to_string(), Some(Color::red())), - ("inner".to_string(), Some(Color::blue())), - ("() {}\n}".to_string(), Some(Color::red())), - ] - ); - assert_eq!( - cx.update(|cx| syntax_chunks(3..5, &map, &theme, cx)), - vec![ - (" fn ".to_string(), Some(Color::red())), - ("inner".to_string(), Some(Color::blue())), - ("() {}\n}".to_string(), Some(Color::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(Color::blue())), - ("⋯".to_string(), None), - (" fn ".to_string(), Some(Color::red())), - ("inner".to_string(), Some(Color::blue())), - ("() {}\n}".to_string(), Some(Color::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(), Color::red().into()), - ("fn.name".to_string(), Color::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(Color::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(Color::blue())), - ("⋯\n".to_string(), None), - (" \nfn ".to_string(), Some(Color::red())), - ("i\n".to_string(), Some(Color::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(), Color::red().into()), - ("string".to_string(), Color::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(Color::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(Color::blue())), - (":".to_string(), Some(Color::red()), None), - (" B = ".to_string(), None, None), - ("\"c ".to_string(), Some(Color::green()), None), - ("d".to_string(), Some(Color::green()), Some(Color::blue())), - ("\"".to_string(), Some(Color::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: &ModelHandle, - 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: &ModelHandle, - 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::{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(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); +// }); +// } +// } diff --git a/crates/editor2/src/display_map/fold_map.rs b/crates/editor2/src/display_map/fold_map.rs index 4645f64451..61047043df 100644 --- a/crates/editor2/src/display_map/fold_map.rs +++ b/crates/editor2/src/display_map/fold_map.rs @@ -3,7 +3,7 @@ use super::{ Highlights, }; use crate::{Anchor, AnchorRangeExt, MultiBufferSnapshot, ToOffset}; -use gpui::{fonts::HighlightStyle, Hsla}; +use gpui::{HighlightStyle, Hsla}; use language::{Chunk, Edit, Point, TextSummary}; use std::{ any::TypeId, @@ -221,7 +221,7 @@ impl FoldMap { (FoldMapWriter(self), snapshot, edits) } - pub fn set_ellipses_color(&mut self, color: Color) -> bool { + pub fn set_ellipses_color(&mut self, color: Hsla) -> bool { if self.ellipses_color != Some(color) { self.ellipses_color = Some(color); true @@ -469,7 +469,7 @@ pub struct FoldSnapshot { folds: SumTree, pub inlay_snapshot: InlaySnapshot, pub version: usize, - pub ellipses_color: Option, + pub ellipses_color: Option, } impl FoldSnapshot { @@ -959,7 +959,7 @@ pub struct FoldChunks<'a> { inlay_offset: InlayOffset, output_offset: usize, max_output_offset: usize, - ellipses_color: Option, + ellipses_color: Option, } impl<'a> Iterator for FoldChunks<'a> { diff --git a/crates/editor2/src/display_map/inlay_map.rs b/crates/editor2/src/display_map/inlay_map.rs index fbd638429a..0afed4028d 100644 --- a/crates/editor2/src/display_map/inlay_map.rs +++ b/crates/editor2/src/display_map/inlay_map.rs @@ -1,6 +1,6 @@ use crate::{Anchor, InlayId, MultiBufferSnapshot, ToOffset}; use collections::{BTreeMap, BTreeSet}; -use gpui::fonts::HighlightStyle; +use gpui::HighlightStyle; use language::{Chunk, Edit, Point, TextSummary}; use multi_buffer::{MultiBufferChunks, MultiBufferRows}; use std::{ diff --git a/crates/editor2/src/display_map/wrap_map.rs b/crates/editor2/src/display_map/wrap_map.rs index 706e16c24f..c2f181efcd 100644 --- a/crates/editor2/src/display_map/wrap_map.rs +++ b/crates/editor2/src/display_map/wrap_map.rs @@ -4,7 +4,7 @@ use super::{ Highlights, }; use crate::MultiBufferSnapshot; -use gpui::{AppContext, Entity, Model, ModelContext, Task}; +use gpui::{AppContext, FontId, Model, ModelContext, Pixels, Task}; use language::{Chunk, Point}; use lazy_static::lazy_static; use smol::future::yield_now; @@ -22,7 +22,7 @@ pub struct WrapMap { edits_since_sync: Patch, wrap_width: Option, background_task: Option>, - font: (FontId, f32), + font: (FontId, Pixels), } #[derive(Clone)] diff --git a/crates/editor2/src/editor.rs b/crates/editor2/src/editor.rs index 196527acdb..a7378aa705 100644 --- a/crates/editor2/src/editor.rs +++ b/crates/editor2/src/editor.rs @@ -38,9 +38,8 @@ pub use element::{ use futures::FutureExt; use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{ - serde_json, AnyElement, AppContext, AsyncAppContext, ClipboardItem, - Element, Entity, Hsla, Model, Subscription, Task, View, ViewContext, - WindowContext, + serde_json, AnyElement, AppContext, AsyncAppContext, ClipboardItem, Element, Entity, Hsla, + Model, Quad, Subscription, Task, Text, View, ViewContext, WeakView, WindowContext, }; use highlight_matching_bracket::refresh_matching_bracket_highlights; use hover_popover::{hide_hover, HoverState}; @@ -50,10 +49,10 @@ use itertools::Itertools; pub use language::{char_kind, CharKind}; use language::{ language_settings::{self, all_language_settings, InlayHintSettings}, - point_from_lsp, AutoindentMode, BracketPair, Buffer, CodeAction, CodeLabel, - Completion, CursorShape, Diagnostic, DiagnosticSeverity, File, IndentKind, - IndentSize, Language, LanguageRegistry, LanguageServerName, OffsetRangeExt, OffsetUtf16, Point, - Selection, SelectionGoal, TransactionId, + point_from_lsp, AutoindentMode, BracketPair, Buffer, CodeAction, CodeLabel, Completion, + CursorShape, Diagnostic, DiagnosticSeverity, File, IndentKind, IndentSize, Language, + LanguageRegistry, LanguageServerName, OffsetRangeExt, OffsetUtf16, Point, Selection, + SelectionGoal, TransactionId, }; use link_go_to_definition::{ hide_link_definition, show_link_definition, GoToDefinitionLink, InlayHighlight, @@ -113,7 +112,7 @@ pub const FORMAT_TIMEOUT: Duration = Duration::from_secs(2); pub fn render_parsed_markdown( parsed: &language::ParsedMarkdown, editor_style: &EditorStyle, - workspace: Option>, + workspace: Option>, cx: &mut ViewContext, ) -> Text { enum RenderedMarkdown {} @@ -124,51 +123,55 @@ pub fn render_parsed_markdown( let mut region_id = 0; - Text::new(parsed.text, editor_style.text.clone()) - .with_highlights( - parsed - .highlights - .iter() - .filter_map(|(range, highlight)| { - let highlight = highlight.to_highlight_style(&editor_style.syntax)?; - Some((range.clone(), highlight)) - }) - .collect::>(), - ) - .with_custom_runs(parsed.region_ranges, move |ix, bounds, cx| { - region_id += 1; - let region = parsed.regions[ix].clone(); + todo!() + // Text::new(parsed.text, editor_style.text.clone()) + // .with_highlights( + // parsed + // .highlights + // .iter() + // .filter_map(|(range, highlight)| { + // let highlight = highlight.to_highlight_style(&editor_style.syntax)?; + // Some((range.clone(), highlight)) + // }) + // .collect::>(), + // ) + // .with_custom_runs(parsed.region_ranges, move |ix, bounds, cx| { + // region_id += 1; + // let region = parsed.regions[ix].clone(); - if let Some(link) = region.link { - cx.scene().push_cursor_region(CursorRegion { - bounds, - style: CursorStyle::PointingHand, - }); - cx.scene().push_mouse_region( - MouseRegion::new::<(RenderedMarkdown, Tag)>(view_id, region_id, bounds) - .on_down::(MouseButton::Left, move |_, _, cx| match &link { - markdown::Link::Web { url } => cx.platform().open_url(url), - markdown::Link::Path { path } => { - if let Some(workspace) = &workspace { - _ = workspace.update(cx, |workspace, cx| { - workspace.open_abs_path(path.clone(), false, cx).detach(); - }); - } - } - }), - ); - } + // if let Some(link) = region.link { + // cx.scene().push_cursor_region(CursorRegion { + // bounds, + // style: CursorStyle::PointingHand, + // }); + // cx.scene().push_mouse_region( + // MouseRegion::new::<(RenderedMarkdown, Tag)>(view_id, region_id, bounds) + // .on_down::(MouseButton::Left, move |_, _, cx| match &link { + // markdown::Link::Web { url } => cx.platform().open_url(url), + // markdown::Link::Path { path } => { + // if let Some(workspace) = &workspace { + // _ = workspace.update(cx, |workspace, cx| { + // workspace.open_abs_path(path.clone(), false, cx).detach(); + // }); + // } + // } + // }), + // ); + // } - if region.code { - cx.scene().push_quad(gpui::Quad { - bounds, - background: Some(code_span_background_color), - border: Default::default(), - corner_radii: (2.0).into(), - }); - } - }) - .with_soft_wrap(true) + // if region.code { + // cx.draw_quad(Quad { + // bounds, + // background: Some(code_span_background_color), + // corner_radii: (2.0).into(), + // order: todo!(), + // content_mask: todo!(), + // border_color: todo!(), + // border_widths: todo!(), + // }); + // } + // }) + // .with_soft_wrap(true) } #[derive(Clone, Deserialize, PartialEq, Default)] @@ -416,133 +419,133 @@ pub fn init_settings(cx: &mut AppContext) { pub fn init(cx: &mut AppContext) { init_settings(cx); - cx.add_action(Editor::new_file); - cx.add_action(Editor::new_file_in_direction); - cx.add_action(Editor::cancel); - cx.add_action(Editor::newline); - cx.add_action(Editor::newline_above); - cx.add_action(Editor::newline_below); - cx.add_action(Editor::backspace); - cx.add_action(Editor::delete); - cx.add_action(Editor::tab); - cx.add_action(Editor::tab_prev); - cx.add_action(Editor::indent); - cx.add_action(Editor::outdent); - cx.add_action(Editor::delete_line); - cx.add_action(Editor::join_lines); - cx.add_action(Editor::sort_lines_case_sensitive); - cx.add_action(Editor::sort_lines_case_insensitive); - cx.add_action(Editor::reverse_lines); - cx.add_action(Editor::shuffle_lines); - cx.add_action(Editor::convert_to_upper_case); - cx.add_action(Editor::convert_to_lower_case); - cx.add_action(Editor::convert_to_title_case); - cx.add_action(Editor::convert_to_snake_case); - cx.add_action(Editor::convert_to_kebab_case); - cx.add_action(Editor::convert_to_upper_camel_case); - cx.add_action(Editor::convert_to_lower_camel_case); - cx.add_action(Editor::delete_to_previous_word_start); - cx.add_action(Editor::delete_to_previous_subword_start); - cx.add_action(Editor::delete_to_next_word_end); - cx.add_action(Editor::delete_to_next_subword_end); - cx.add_action(Editor::delete_to_beginning_of_line); - cx.add_action(Editor::delete_to_end_of_line); - cx.add_action(Editor::cut_to_end_of_line); - cx.add_action(Editor::duplicate_line); - cx.add_action(Editor::move_line_up); - cx.add_action(Editor::move_line_down); - cx.add_action(Editor::transpose); - cx.add_action(Editor::cut); - cx.add_action(Editor::copy); - cx.add_action(Editor::paste); - cx.add_action(Editor::undo); - cx.add_action(Editor::redo); - cx.add_action(Editor::move_up); - cx.add_action(Editor::move_page_up); - cx.add_action(Editor::move_down); - cx.add_action(Editor::move_page_down); - cx.add_action(Editor::next_screen); - cx.add_action(Editor::move_left); - cx.add_action(Editor::move_right); - cx.add_action(Editor::move_to_previous_word_start); - cx.add_action(Editor::move_to_previous_subword_start); - cx.add_action(Editor::move_to_next_word_end); - cx.add_action(Editor::move_to_next_subword_end); - cx.add_action(Editor::move_to_beginning_of_line); - cx.add_action(Editor::move_to_end_of_line); - cx.add_action(Editor::move_to_start_of_paragraph); - cx.add_action(Editor::move_to_end_of_paragraph); - cx.add_action(Editor::move_to_beginning); - cx.add_action(Editor::move_to_end); - cx.add_action(Editor::select_up); - cx.add_action(Editor::select_down); - cx.add_action(Editor::select_left); - cx.add_action(Editor::select_right); - cx.add_action(Editor::select_to_previous_word_start); - cx.add_action(Editor::select_to_previous_subword_start); - cx.add_action(Editor::select_to_next_word_end); - cx.add_action(Editor::select_to_next_subword_end); - cx.add_action(Editor::select_to_beginning_of_line); - cx.add_action(Editor::select_to_end_of_line); - cx.add_action(Editor::select_to_start_of_paragraph); - cx.add_action(Editor::select_to_end_of_paragraph); - cx.add_action(Editor::select_to_beginning); - cx.add_action(Editor::select_to_end); - cx.add_action(Editor::select_all); - cx.add_action(Editor::select_all_matches); - cx.add_action(Editor::select_line); - cx.add_action(Editor::split_selection_into_lines); - cx.add_action(Editor::add_selection_above); - cx.add_action(Editor::add_selection_below); - cx.add_action(Editor::select_next); - cx.add_action(Editor::select_previous); - cx.add_action(Editor::toggle_comments); - cx.add_action(Editor::select_larger_syntax_node); - cx.add_action(Editor::select_smaller_syntax_node); - cx.add_action(Editor::move_to_enclosing_bracket); - cx.add_action(Editor::undo_selection); - cx.add_action(Editor::redo_selection); - cx.add_action(Editor::go_to_diagnostic); - cx.add_action(Editor::go_to_prev_diagnostic); - cx.add_action(Editor::go_to_hunk); - cx.add_action(Editor::go_to_prev_hunk); - cx.add_action(Editor::go_to_definition); - cx.add_action(Editor::go_to_definition_split); - cx.add_action(Editor::go_to_type_definition); - cx.add_action(Editor::go_to_type_definition_split); - cx.add_action(Editor::fold); - cx.add_action(Editor::fold_at); - cx.add_action(Editor::unfold_lines); - cx.add_action(Editor::unfold_at); - cx.add_action(Editor::gutter_hover); - cx.add_action(Editor::fold_selected_ranges); - cx.add_action(Editor::show_completions); - cx.add_action(Editor::toggle_code_actions); - cx.add_action(Editor::open_excerpts); - cx.add_action(Editor::toggle_soft_wrap); - cx.add_action(Editor::toggle_inlay_hints); - cx.add_action(Editor::reveal_in_finder); - cx.add_action(Editor::copy_path); - cx.add_action(Editor::copy_relative_path); - cx.add_action(Editor::copy_highlight_json); - cx.add_async_action(Editor::format); - cx.add_action(Editor::restart_language_server); - cx.add_action(Editor::show_character_palette); - cx.add_async_action(Editor::confirm_completion); - cx.add_async_action(Editor::confirm_code_action); - cx.add_async_action(Editor::rename); - cx.add_async_action(Editor::confirm_rename); - cx.add_async_action(Editor::find_all_references); - cx.add_action(Editor::next_copilot_suggestion); - cx.add_action(Editor::previous_copilot_suggestion); - cx.add_action(Editor::copilot_suggest); - cx.add_action(Editor::context_menu_first); - cx.add_action(Editor::context_menu_prev); - cx.add_action(Editor::context_menu_next); - cx.add_action(Editor::context_menu_last); + // cx.add_action(Editor::new_file); + // cx.add_action(Editor::new_file_in_direction); + // cx.add_action(Editor::cancel); + // cx.add_action(Editor::newline); + // cx.add_action(Editor::newline_above); + // cx.add_action(Editor::newline_below); + // cx.add_action(Editor::backspace); + // cx.add_action(Editor::delete); + // cx.add_action(Editor::tab); + // cx.add_action(Editor::tab_prev); + // cx.add_action(Editor::indent); + // cx.add_action(Editor::outdent); + // cx.add_action(Editor::delete_line); + // cx.add_action(Editor::join_lines); + // cx.add_action(Editor::sort_lines_case_sensitive); + // cx.add_action(Editor::sort_lines_case_insensitive); + // cx.add_action(Editor::reverse_lines); + // cx.add_action(Editor::shuffle_lines); + // cx.add_action(Editor::convert_to_upper_case); + // cx.add_action(Editor::convert_to_lower_case); + // cx.add_action(Editor::convert_to_title_case); + // cx.add_action(Editor::convert_to_snake_case); + // cx.add_action(Editor::convert_to_kebab_case); + // cx.add_action(Editor::convert_to_upper_camel_case); + // cx.add_action(Editor::convert_to_lower_camel_case); + // cx.add_action(Editor::delete_to_previous_word_start); + // cx.add_action(Editor::delete_to_previous_subword_start); + // cx.add_action(Editor::delete_to_next_word_end); + // cx.add_action(Editor::delete_to_next_subword_end); + // cx.add_action(Editor::delete_to_beginning_of_line); + // cx.add_action(Editor::delete_to_end_of_line); + // cx.add_action(Editor::cut_to_end_of_line); + // cx.add_action(Editor::duplicate_line); + // cx.add_action(Editor::move_line_up); + // cx.add_action(Editor::move_line_down); + // cx.add_action(Editor::transpose); + // cx.add_action(Editor::cut); + // cx.add_action(Editor::copy); + // cx.add_action(Editor::paste); + // cx.add_action(Editor::undo); + // cx.add_action(Editor::redo); + // cx.add_action(Editor::move_up); + // cx.add_action(Editor::move_page_up); + // cx.add_action(Editor::move_down); + // cx.add_action(Editor::move_page_down); + // cx.add_action(Editor::next_screen); + // cx.add_action(Editor::move_left); + // cx.add_action(Editor::move_right); + // cx.add_action(Editor::move_to_previous_word_start); + // cx.add_action(Editor::move_to_previous_subword_start); + // cx.add_action(Editor::move_to_next_word_end); + // cx.add_action(Editor::move_to_next_subword_end); + // cx.add_action(Editor::move_to_beginning_of_line); + // cx.add_action(Editor::move_to_end_of_line); + // cx.add_action(Editor::move_to_start_of_paragraph); + // cx.add_action(Editor::move_to_end_of_paragraph); + // cx.add_action(Editor::move_to_beginning); + // cx.add_action(Editor::move_to_end); + // cx.add_action(Editor::select_up); + // cx.add_action(Editor::select_down); + // cx.add_action(Editor::select_left); + // cx.add_action(Editor::select_right); + // cx.add_action(Editor::select_to_previous_word_start); + // cx.add_action(Editor::select_to_previous_subword_start); + // cx.add_action(Editor::select_to_next_word_end); + // cx.add_action(Editor::select_to_next_subword_end); + // cx.add_action(Editor::select_to_beginning_of_line); + // cx.add_action(Editor::select_to_end_of_line); + // cx.add_action(Editor::select_to_start_of_paragraph); + // cx.add_action(Editor::select_to_end_of_paragraph); + // cx.add_action(Editor::select_to_beginning); + // cx.add_action(Editor::select_to_end); + // cx.add_action(Editor::select_all); + // cx.add_action(Editor::select_all_matches); + // cx.add_action(Editor::select_line); + // cx.add_action(Editor::split_selection_into_lines); + // cx.add_action(Editor::add_selection_above); + // cx.add_action(Editor::add_selection_below); + // cx.add_action(Editor::select_next); + // cx.add_action(Editor::select_previous); + // cx.add_action(Editor::toggle_comments); + // cx.add_action(Editor::select_larger_syntax_node); + // cx.add_action(Editor::select_smaller_syntax_node); + // cx.add_action(Editor::move_to_enclosing_bracket); + // cx.add_action(Editor::undo_selection); + // cx.add_action(Editor::redo_selection); + // cx.add_action(Editor::go_to_diagnostic); + // cx.add_action(Editor::go_to_prev_diagnostic); + // cx.add_action(Editor::go_to_hunk); + // cx.add_action(Editor::go_to_prev_hunk); + // cx.add_action(Editor::go_to_definition); + // cx.add_action(Editor::go_to_definition_split); + // cx.add_action(Editor::go_to_type_definition); + // cx.add_action(Editor::go_to_type_definition_split); + // cx.add_action(Editor::fold); + // cx.add_action(Editor::fold_at); + // cx.add_action(Editor::unfold_lines); + // cx.add_action(Editor::unfold_at); + // cx.add_action(Editor::gutter_hover); + // cx.add_action(Editor::fold_selected_ranges); + // cx.add_action(Editor::show_completions); + // cx.add_action(Editor::toggle_code_actions); + // cx.add_action(Editor::open_excerpts); + // cx.add_action(Editor::toggle_soft_wrap); + // cx.add_action(Editor::toggle_inlay_hints); + // cx.add_action(Editor::reveal_in_finder); + // cx.add_action(Editor::copy_path); + // cx.add_action(Editor::copy_relative_path); + // cx.add_action(Editor::copy_highlight_json); + // cx.add_async_action(Editor::format); + // cx.add_action(Editor::restart_language_server); + // cx.add_action(Editor::show_character_palette); + // cx.add_async_action(Editor::confirm_completion); + // cx.add_async_action(Editor::confirm_code_action); + // cx.add_async_action(Editor::rename); + // cx.add_async_action(Editor::confirm_rename); + // cx.add_async_action(Editor::find_all_references); + // cx.add_action(Editor::next_copilot_suggestion); + // cx.add_action(Editor::previous_copilot_suggestion); + // cx.add_action(Editor::copilot_suggest); + // cx.add_action(Editor::context_menu_first); + // cx.add_action(Editor::context_menu_prev); + // cx.add_action(Editor::context_menu_next); + // cx.add_action(Editor::context_menu_last); hover_popover::init(cx); - /scroll::actions::init(cx); + scroll::actions::init(cx); workspace::register_project_item::(cx); workspace::register_followable_item::(cx); @@ -571,7 +574,7 @@ pub enum SelectPhase { Update { position: DisplayPoint, goal_column: u32, - scroll_position: Vector2F, + scroll_position: Point, }, End, } @@ -612,11 +615,11 @@ type CompletionId = usize; type GetFieldEditorTheme = dyn Fn(&theme::Theme) -> theme::FieldEditor; type OverrideTextStyle = dyn Fn(&EditorStyle) -> Option; -type BackgroundHighlight = (fn(&Theme) -> Color, Vec>); -type InlayBackgroundHighlight = (fn(&Theme) -> Color, Vec); +type BackgroundHighlight = (fn(&Theme) -> Hsla, Vec>); +type InlayBackgroundHighlight = (fn(&Theme) -> Hsla, Vec); pub struct Editor { - handle: WeakViewHandle, + handle: WeakView, buffer: Model, display_map: Model, pub selections: SelectionsCollection, @@ -648,7 +651,7 @@ pub struct Editor { inlay_background_highlights: TreeMap, InlayBackgroundHighlight>, nav_history: Option, context_menu: RwLock>, - mouse_context_menu: ViewHandle, + mouse_context_menu: View, completion_tasks: Vec<(CompletionId, Task>)>, next_completion_id: CompletionId, available_code_actions: Option<(Model, Arc<[CodeAction]>)>, @@ -659,7 +662,7 @@ pub struct Editor { cursor_shape: CursorShape, collapse_matches: bool, autoindent_mode: Option, - workspace: Option<(WeakViewHandle, i64)>, + workspace: Option<(WeakView, i64)>, keymap_context_layers: BTreeMap, input_enabled: bool, read_only: bool, @@ -672,7 +675,7 @@ pub struct Editor { // inlay_hint_cache: InlayHintCache, next_inlay_id: usize, _subscriptions: Vec, - pixel_position_of_newest_cursor: Option, + pixel_position_of_newest_cursor: Option>, } pub struct EditorSnapshot { @@ -828,7 +831,7 @@ struct SnippetState { pub struct RenameState { pub range: Range, pub old_name: Arc, - pub editor: ViewHandle, + pub editor: View, block_id: BlockId, } @@ -915,7 +918,7 @@ impl ContextMenu { &self, cursor_position: DisplayPoint, style: EditorStyle, - workspace: Option>, + workspace: Option>, cx: &mut ViewContext, ) -> (DisplayPoint, AnyElement) { match self { @@ -938,22 +941,14 @@ struct CompletionsMenu { } impl CompletionsMenu { - fn select_first( - &mut self, - project: Option<&Model>, - cx: &mut ViewContext, - ) { + fn select_first(&mut self, project: Option<&Model>, cx: &mut ViewContext) { self.selected_item = 0; self.list.scroll_to(ScrollTarget::Show(self.selected_item)); self.attempt_resolve_selected_completion_documentation(project, cx); cx.notify(); } - fn select_prev( - &mut self, - project: Option<&Model>, - cx: &mut ViewContext, - ) { + fn select_prev(&mut self, project: Option<&Model>, cx: &mut ViewContext) { if self.selected_item > 0 { self.selected_item -= 1; } else { @@ -964,11 +959,7 @@ impl CompletionsMenu { cx.notify(); } - fn select_next( - &mut self, - project: Option<&Model>, - cx: &mut ViewContext, - ) { + fn select_next(&mut self, project: Option<&Model>, cx: &mut ViewContext) { if self.selected_item + 1 < self.matches.len() { self.selected_item += 1; } else { @@ -979,11 +970,7 @@ impl CompletionsMenu { cx.notify(); } - fn select_last( - &mut self, - project: Option<&Model>, - cx: &mut ViewContext, - ) { + fn select_last(&mut self, project: Option<&Model>, cx: &mut ViewContext) { self.selected_item = self.matches.len() - 1; self.list.scroll_to(ScrollTarget::Show(self.selected_item)); self.attempt_resolve_selected_completion_documentation(project, cx); @@ -1241,7 +1228,7 @@ impl CompletionsMenu { fn render( &self, style: EditorStyle, - workspace: Option>, + workspace: Option>, cx: &mut ViewContext, ) -> AnyElement { enum CompletionTag {} @@ -1760,7 +1747,7 @@ pub struct NavigationData { scroll_top_row: u32, } -pub struct EditorCreated(pub ViewHandle); +pub struct EditorCreated(pub View); enum GotoDefinitionKind { Symbol, @@ -3845,8 +3832,8 @@ impl InlayHintRefreshReason { // } // async fn open_project_transaction( -// this: &WeakViewHandle, -// workspace: WeakViewHandle, +// this: &WeakViewHandle Vector2F { + pub fn scroll_position(&self) -> Point { self.scroll_anchor.scroll_position(&self.display_snapshot) } } @@ -9286,9 +9273,9 @@ pub enum Event { Closed, } -pub struct EditorFocused(pub ViewHandle); -pub struct EditorBlurred(pub ViewHandle); -pub struct EditorReleased(pub WeakViewHandle); +pub struct EditorFocused(pub View); +pub struct EditorBlurred(pub View); +pub struct EditorReleased(pub WeakView); impl Entity for Editor { type Event = Event; @@ -9323,7 +9310,7 @@ impl View for Editor { "Editor" } - fn focus_in(&mut self, focused: AnyViewHandle, cx: &mut ViewContext) { + fn focus_in(&mut self, focused: AnyView, cx: &mut ViewContext) { if cx.is_self_focused() { let focused_event = EditorFocused(cx.handle()); cx.emit(Event::Focused); @@ -9350,7 +9337,7 @@ impl View for Editor { } } - fn focus_out(&mut self, _: AnyViewHandle, cx: &mut ViewContext) { + fn focus_out(&mut self, _: AnyView, cx: &mut ViewContext) { let blurred_event = EditorBlurred(cx.handle()); cx.emit_global(blurred_event); self.focused = false; @@ -9649,7 +9636,7 @@ fn build_style( settings: &ThemeSettings, get_field_editor_theme: Option<&GetFieldEditorTheme>, override_text_style: Option<&OverrideTextStyle>, - cx: &AppContext, + cx: &mut AppContext, ) -> EditorStyle { let font_cache = cx.font_cache(); let line_height_scalar = settings.line_height(); diff --git a/crates/editor2/src/editor_tests.rs b/crates/editor2/src/editor_tests.rs index feca741737..5b5a40ba8e 100644 --- a/crates/editor2/src/editor_tests.rs +++ b/crates/editor2/src/editor_tests.rs @@ -1,8195 +1,8191 @@ -use super::*; -use crate::{ - scroll::scroll_amount::ScrollAmount, - test::{ - assert_text_with_selections, build_editor, editor_lsp_test_context::EditorLspTestContext, - editor_test_context::EditorTestContext, select_ranges, - }, - JoinLines, -}; -use drag_and_drop::DragAndDrop; -use futures::StreamExt; -use gpui::{ - executor::Deterministic, - geometry::{rect::RectF, vector::vec2f}, - platform::{WindowBounds, WindowOptions}, - serde_json::{self, json}, - TestAppContext, -}; -use indoc::indoc; -use language::{ - language_settings::{AllLanguageSettings, AllLanguageSettingsContent, LanguageSettingsContent}, - BracketPairConfig, FakeLspAdapter, LanguageConfig, LanguageConfigOverride, LanguageRegistry, - Override, Point, -}; -use parking_lot::Mutex; -use project::project_settings::{LspSettings, ProjectSettings}; -use project::FakeFs; -use std::sync::atomic; -use std::sync::atomic::AtomicUsize; -use std::{cell::RefCell, future::Future, rc::Rc, time::Instant}; -use unindent::Unindent; -use util::{ - assert_set_eq, - test::{marked_text_ranges, marked_text_ranges_by, sample_text, TextRangeMarker}, -}; -use workspace::{ - item::{FollowableItem, Item, ItemHandle}, - NavigationEntry, ViewId, -}; - -#[gpui::test] -fn test_edit_events(cx: &mut TestAppContext) { - init_test(cx, |_| {}); - - let buffer = cx.add_model(|cx| { - let mut buffer = language::Buffer::new(0, cx.model_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| { - cx.subscribe(&cx.handle(), move |_, _, event, _| { - if matches!( - event, - Event::Edited | Event::BufferEdited | Event::DirtyChanged - ) { - events.borrow_mut().push(("editor1", event.clone())); - } - }) - .detach(); - Editor::for_buffer(buffer.clone(), None, cx) - } - }) - .root(cx); - let editor2 = cx - .add_window({ - let events = events.clone(); - |cx| { - cx.subscribe(&cx.handle(), move |_, _, event, _| { - if matches!( - event, - Event::Edited | Event::BufferEdited | Event::DirtyChanged - ) { - events.borrow_mut().push(("editor2", event.clone())); - } - }) - .detach(); - Editor::for_buffer(buffer.clone(), None, cx) - } - }) - .root(cx); - 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), - ("editor1", Event::DirtyChanged), - ("editor2", Event::DirtyChanged) - ] - ); - - // 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), - ] - ); - - // 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), - ("editor1", Event::DirtyChanged), - ("editor2", Event::DirtyChanged), - ] - ); - - // 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), - ("editor1", Event::DirtyChanged), - ("editor2", Event::DirtyChanged), - ] - ); - - // 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), - ("editor1", Event::DirtyChanged), - ("editor2", Event::DirtyChanged), - ] - ); - - // 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), - ("editor1", Event::DirtyChanged), - ("editor2", Event::DirtyChanged), - ] - ); - - // 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()), []); -} - -#[gpui::test] -fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) { - init_test(cx, |_| {}); - - let mut now = Instant::now(); - let buffer = cx.add_model(|cx| language::Buffer::new(0, cx.model_id() as u64, "123456")); - let group_interval = buffer.read_with(cx, |buffer, _| buffer.transaction_group_interval()); - let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); - let editor = cx - .add_window(|cx| build_editor(buffer.clone(), cx)) - .root(cx); - - editor.update(cx, |editor, cx| { - editor.start_transaction_at(now, cx); - editor.change_selections(None, cx, |s| s.select_ranges([2..4])); - - editor.insert("cd", cx); - editor.end_transaction_at(now, cx); - assert_eq!(editor.text(cx), "12cd56"); - assert_eq!(editor.selections.ranges(cx), vec![4..4]); - - editor.start_transaction_at(now, cx); - editor.change_selections(None, cx, |s| s.select_ranges([4..5])); - editor.insert("e", cx); - editor.end_transaction_at(now, cx); - assert_eq!(editor.text(cx), "12cde6"); - assert_eq!(editor.selections.ranges(cx), vec![5..5]); - - now += group_interval + Duration::from_millis(1); - editor.change_selections(None, cx, |s| s.select_ranges([2..2])); - - // Simulate an edit in another editor - buffer.update(cx, |buffer, cx| { - buffer.start_transaction_at(now, cx); - buffer.edit([(0..1, "a")], None, cx); - buffer.edit([(1..1, "b")], None, cx); - buffer.end_transaction_at(now, cx); - }); - - assert_eq!(editor.text(cx), "ab2cde6"); - assert_eq!(editor.selections.ranges(cx), vec![3..3]); - - // Last transaction happened past the group interval in a different editor. - // Undo it individually and don't restore selections. - editor.undo(&Undo, cx); - assert_eq!(editor.text(cx), "12cde6"); - assert_eq!(editor.selections.ranges(cx), vec![2..2]); - - // First two transactions happened within the group interval in this editor. - // Undo them together and restore selections. - editor.undo(&Undo, cx); - editor.undo(&Undo, cx); // Undo stack is empty here, so this is a no-op. - assert_eq!(editor.text(cx), "123456"); - assert_eq!(editor.selections.ranges(cx), vec![0..0]); - - // Redo the first two transactions together. - editor.redo(&Redo, cx); - assert_eq!(editor.text(cx), "12cde6"); - assert_eq!(editor.selections.ranges(cx), vec![5..5]); - - // Redo the last transaction on its own. - editor.redo(&Redo, cx); - assert_eq!(editor.text(cx), "ab2cde6"); - assert_eq!(editor.selections.ranges(cx), vec![6..6]); - - // Test empty transactions. - editor.start_transaction_at(now, cx); - editor.end_transaction_at(now, cx); - editor.undo(&Undo, cx); - assert_eq!(editor.text(cx), "12cde6"); - }); -} - -#[gpui::test] -fn test_ime_composition(cx: &mut TestAppContext) { - init_test(cx, |_| {}); - - let buffer = cx.add_model(|cx| { - let mut buffer = language::Buffer::new(0, cx.model_id() as u64, "abcde"); - // Ensure automatic grouping doesn't occur. - buffer.set_group_interval(Duration::ZERO); - buffer - }); - - let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); - cx.add_window(|cx| { - let mut editor = build_editor(buffer.clone(), cx); - - // Start a new IME composition. - editor.replace_and_mark_text_in_range(Some(0..1), "à", None, cx); - editor.replace_and_mark_text_in_range(Some(0..1), "á", None, cx); - editor.replace_and_mark_text_in_range(Some(0..1), "ä", None, cx); - assert_eq!(editor.text(cx), "äbcde"); - assert_eq!( - editor.marked_text_ranges(cx), - Some(vec![OffsetUtf16(0)..OffsetUtf16(1)]) - ); - - // Finalize IME composition. - editor.replace_text_in_range(None, "ā", cx); - assert_eq!(editor.text(cx), "ābcde"); - assert_eq!(editor.marked_text_ranges(cx), None); - - // IME composition edits are grouped and are undone/redone at once. - editor.undo(&Default::default(), cx); - assert_eq!(editor.text(cx), "abcde"); - assert_eq!(editor.marked_text_ranges(cx), None); - editor.redo(&Default::default(), cx); - assert_eq!(editor.text(cx), "ābcde"); - assert_eq!(editor.marked_text_ranges(cx), None); - - // Start a new IME composition. - editor.replace_and_mark_text_in_range(Some(0..1), "à", None, cx); - assert_eq!( - editor.marked_text_ranges(cx), - Some(vec![OffsetUtf16(0)..OffsetUtf16(1)]) - ); - - // Undoing during an IME composition cancels it. - editor.undo(&Default::default(), cx); - assert_eq!(editor.text(cx), "ābcde"); - assert_eq!(editor.marked_text_ranges(cx), None); - - // Start a new IME composition with an invalid marked range, ensuring it gets clipped. - editor.replace_and_mark_text_in_range(Some(4..999), "è", None, cx); - assert_eq!(editor.text(cx), "ābcdè"); - assert_eq!( - editor.marked_text_ranges(cx), - Some(vec![OffsetUtf16(4)..OffsetUtf16(5)]) - ); - - // Finalize IME composition with an invalid replacement range, ensuring it gets clipped. - editor.replace_text_in_range(Some(4..999), "ę", cx); - assert_eq!(editor.text(cx), "ābcdę"); - assert_eq!(editor.marked_text_ranges(cx), None); - - // Start a new IME composition with multiple cursors. - editor.change_selections(None, cx, |s| { - s.select_ranges([ - OffsetUtf16(1)..OffsetUtf16(1), - OffsetUtf16(3)..OffsetUtf16(3), - OffsetUtf16(5)..OffsetUtf16(5), - ]) - }); - editor.replace_and_mark_text_in_range(Some(4..5), "XYZ", None, cx); - assert_eq!(editor.text(cx), "XYZbXYZdXYZ"); - assert_eq!( - editor.marked_text_ranges(cx), - Some(vec![ - OffsetUtf16(0)..OffsetUtf16(3), - OffsetUtf16(4)..OffsetUtf16(7), - OffsetUtf16(8)..OffsetUtf16(11) - ]) - ); - - // Ensure the newly-marked range gets treated as relative to the previously-marked ranges. - editor.replace_and_mark_text_in_range(Some(1..2), "1", None, cx); - assert_eq!(editor.text(cx), "X1ZbX1ZdX1Z"); - assert_eq!( - editor.marked_text_ranges(cx), - Some(vec![ - OffsetUtf16(1)..OffsetUtf16(2), - OffsetUtf16(5)..OffsetUtf16(6), - OffsetUtf16(9)..OffsetUtf16(10) - ]) - ); - - // Finalize IME composition with multiple cursors. - editor.replace_text_in_range(Some(9..10), "2", cx); - assert_eq!(editor.text(cx), "X2ZbX2ZdX2Z"); - assert_eq!(editor.marked_text_ranges(cx), None); - - editor - }); -} - -#[gpui::test] -fn test_selection_with_mouse(cx: &mut TestAppContext) { - init_test(cx, |_| {}); - - let editor = cx - .add_window(|cx| { - let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx); - build_editor(buffer, cx) - }) - .root(cx); - editor.update(cx, |view, cx| { - view.begin_selection(DisplayPoint::new(2, 2), false, 1, cx); - }); - assert_eq!( - editor.update(cx, |view, cx| view.selections.display_ranges(cx)), - [DisplayPoint::new(2, 2)..DisplayPoint::new(2, 2)] - ); - - editor.update(cx, |view, cx| { - view.update_selection(DisplayPoint::new(3, 3), 0, Vector2F::zero(), cx); - }); - - assert_eq!( - editor.update(cx, |view, cx| view.selections.display_ranges(cx)), - [DisplayPoint::new(2, 2)..DisplayPoint::new(3, 3)] - ); - - editor.update(cx, |view, cx| { - view.update_selection(DisplayPoint::new(1, 1), 0, Vector2F::zero(), cx); - }); - - assert_eq!( - editor.update(cx, |view, cx| view.selections.display_ranges(cx)), - [DisplayPoint::new(2, 2)..DisplayPoint::new(1, 1)] - ); - - editor.update(cx, |view, cx| { - view.end_selection(cx); - view.update_selection(DisplayPoint::new(3, 3), 0, Vector2F::zero(), cx); - }); - - assert_eq!( - editor.update(cx, |view, cx| view.selections.display_ranges(cx)), - [DisplayPoint::new(2, 2)..DisplayPoint::new(1, 1)] - ); - - editor.update(cx, |view, cx| { - view.begin_selection(DisplayPoint::new(3, 3), true, 1, cx); - view.update_selection(DisplayPoint::new(0, 0), 0, Vector2F::zero(), cx); - }); - - assert_eq!( - editor.update(cx, |view, cx| view.selections.display_ranges(cx)), - [ - DisplayPoint::new(2, 2)..DisplayPoint::new(1, 1), - DisplayPoint::new(3, 3)..DisplayPoint::new(0, 0) - ] - ); - - editor.update(cx, |view, cx| { - view.end_selection(cx); - }); - - assert_eq!( - editor.update(cx, |view, cx| view.selections.display_ranges(cx)), - [DisplayPoint::new(3, 3)..DisplayPoint::new(0, 0)] - ); -} - -#[gpui::test] -fn test_canceling_pending_selection(cx: &mut TestAppContext) { - init_test(cx, |_| {}); - - let view = cx - .add_window(|cx| { - let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx); - build_editor(buffer, cx) - }) - .root(cx); - - view.update(cx, |view, cx| { - view.begin_selection(DisplayPoint::new(2, 2), false, 1, cx); - assert_eq!( - view.selections.display_ranges(cx), - [DisplayPoint::new(2, 2)..DisplayPoint::new(2, 2)] - ); - }); - - view.update(cx, |view, cx| { - view.update_selection(DisplayPoint::new(3, 3), 0, Vector2F::zero(), cx); - assert_eq!( - view.selections.display_ranges(cx), - [DisplayPoint::new(2, 2)..DisplayPoint::new(3, 3)] - ); - }); - - view.update(cx, |view, cx| { - view.cancel(&Cancel, cx); - view.update_selection(DisplayPoint::new(1, 1), 0, Vector2F::zero(), cx); - assert_eq!( - view.selections.display_ranges(cx), - [DisplayPoint::new(2, 2)..DisplayPoint::new(3, 3)] - ); - }); -} - -#[gpui::test] -fn test_clone(cx: &mut TestAppContext) { - init_test(cx, |_| {}); - - let (text, selection_ranges) = marked_text_ranges( - indoc! {" - one - two - threeˇ - four - fiveˇ - "}, - true, - ); - - let editor = cx - .add_window(|cx| { - let buffer = MultiBuffer::build_simple(&text, cx); - build_editor(buffer, cx) - }) - .root(cx); - - editor.update(cx, |editor, cx| { - editor.change_selections(None, cx, |s| s.select_ranges(selection_ranges.clone())); - editor.fold_ranges( - [ - Point::new(1, 0)..Point::new(2, 0), - Point::new(3, 0)..Point::new(4, 0), - ], - true, - cx, - ); - }); - - let cloned_editor = editor - .update(cx, |editor, cx| { - cx.add_window(Default::default(), |cx| editor.clone(cx)) - }) - .root(cx); - - let snapshot = editor.update(cx, |e, cx| e.snapshot(cx)); - let cloned_snapshot = cloned_editor.update(cx, |e, cx| e.snapshot(cx)); - - assert_eq!( - cloned_editor.update(cx, |e, cx| e.display_text(cx)), - editor.update(cx, |e, cx| e.display_text(cx)) - ); - assert_eq!( - cloned_snapshot - .folds_in_range(0..text.len()) - .collect::>(), - snapshot.folds_in_range(0..text.len()).collect::>(), - ); - assert_set_eq!( - cloned_editor.read_with(cx, |editor, cx| editor.selections.ranges::(cx)), - editor.read_with(cx, |editor, cx| editor.selections.ranges(cx)) - ); - assert_set_eq!( - cloned_editor.update(cx, |e, cx| e.selections.display_ranges(cx)), - editor.update(cx, |e, cx| e.selections.display_ranges(cx)) - ); -} - -#[gpui::test] -async fn test_navigation_history(cx: &mut TestAppContext) { - init_test(cx, |_| {}); - - cx.set_global(DragAndDrop::::default()); - use workspace::item::Item; - - let fs = FakeFs::new(cx.background()); - let project = Project::test(fs, [], cx).await; - let window = cx.add_window(|cx| Workspace::test_new(project, cx)); - let workspace = window.root(cx); - let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - window.add_view(cx, |cx| { - let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx); - let mut editor = build_editor(buffer.clone(), cx); - let handle = cx.handle(); - 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.view_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.view_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(Vector2F::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(Vector2F::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), - vec2f(0., editor.max_point(cx).row() as f32) - ); - - editor - }); -} - -#[gpui::test] -fn test_cancel(cx: &mut TestAppContext) { - init_test(cx, |_| {}); - - let view = cx - .add_window(|cx| { - let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx); - build_editor(buffer, cx) - }) - .root(cx); - - view.update(cx, |view, cx| { - view.begin_selection(DisplayPoint::new(3, 4), false, 1, cx); - view.update_selection(DisplayPoint::new(1, 1), 0, Vector2F::zero(), cx); - view.end_selection(cx); - - view.begin_selection(DisplayPoint::new(0, 1), true, 1, cx); - view.update_selection(DisplayPoint::new(0, 3), 0, Vector2F::zero(), cx); - view.end_selection(cx); - assert_eq!( - view.selections.display_ranges(cx), - [ - DisplayPoint::new(0, 1)..DisplayPoint::new(0, 3), - DisplayPoint::new(3, 4)..DisplayPoint::new(1, 1), - ] - ); - }); - - view.update(cx, |view, cx| { - view.cancel(&Cancel, cx); - assert_eq!( - view.selections.display_ranges(cx), - [DisplayPoint::new(3, 4)..DisplayPoint::new(1, 1)] - ); - }); - - view.update(cx, |view, cx| { - view.cancel(&Cancel, cx); - assert_eq!( - view.selections.display_ranges(cx), - [DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1)] - ); - }); -} - -#[gpui::test] -fn test_fold_action(cx: &mut TestAppContext) { - init_test(cx, |_| {}); - - let view = cx - .add_window(|cx| { - let buffer = MultiBuffer::build_simple( - &" - impl Foo { - // Hello! - - fn a() { - 1 - } - - fn b() { - 2 - } - - fn c() { - 3 - } - } - " - .unindent(), - cx, - ); - build_editor(buffer.clone(), cx) - }) - .root(cx); - - view.update(cx, |view, cx| { - view.change_selections(None, cx, |s| { - s.select_display_ranges([DisplayPoint::new(8, 0)..DisplayPoint::new(12, 0)]); - }); - view.fold(&Fold, cx); - assert_eq!( - view.display_text(cx), - " - impl Foo { - // Hello! - - fn a() { - 1 - } - - fn b() {⋯ - } - - fn c() {⋯ - } - } - " - .unindent(), - ); - - view.fold(&Fold, cx); - assert_eq!( - view.display_text(cx), - " - impl Foo {⋯ - } - " - .unindent(), - ); - - view.unfold_lines(&UnfoldLines, cx); - assert_eq!( - view.display_text(cx), - " - impl Foo { - // Hello! - - fn a() { - 1 - } - - fn b() {⋯ - } - - fn c() {⋯ - } - } - " - .unindent(), - ); - - view.unfold_lines(&UnfoldLines, cx); - assert_eq!(view.display_text(cx), view.buffer.read(cx).read(cx).text()); - }); -} - -#[gpui::test] -fn test_move_cursor(cx: &mut TestAppContext) { - init_test(cx, |_| {}); - - let buffer = cx.update(|cx| MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx)); - let view = cx - .add_window(|cx| build_editor(buffer.clone(), cx)) - .root(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"), - ], - None, - cx, - ); - }); - view.update(cx, |view, cx| { - assert_eq!( - view.selections.display_ranges(cx), - &[DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)] - ); - - view.move_down(&MoveDown, cx); - assert_eq!( - view.selections.display_ranges(cx), - &[DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)] - ); - - view.move_right(&MoveRight, cx); - assert_eq!( - view.selections.display_ranges(cx), - &[DisplayPoint::new(1, 4)..DisplayPoint::new(1, 4)] - ); - - view.move_left(&MoveLeft, cx); - assert_eq!( - view.selections.display_ranges(cx), - &[DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)] - ); - - view.move_up(&MoveUp, cx); - assert_eq!( - view.selections.display_ranges(cx), - &[DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)] - ); - - view.move_to_end(&MoveToEnd, cx); - assert_eq!( - view.selections.display_ranges(cx), - &[DisplayPoint::new(5, 6)..DisplayPoint::new(5, 6)] - ); - - view.move_to_beginning(&MoveToBeginning, cx); - assert_eq!( - view.selections.display_ranges(cx), - &[DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)] - ); - - view.change_selections(None, cx, |s| { - s.select_display_ranges([DisplayPoint::new(0, 1)..DisplayPoint::new(0, 2)]); - }); - view.select_to_beginning(&SelectToBeginning, cx); - assert_eq!( - view.selections.display_ranges(cx), - &[DisplayPoint::new(0, 1)..DisplayPoint::new(0, 0)] - ); - - view.select_to_end(&SelectToEnd, cx); - assert_eq!( - view.selections.display_ranges(cx), - &[DisplayPoint::new(0, 1)..DisplayPoint::new(5, 6)] - ); - }); -} - -#[gpui::test] -fn test_move_cursor_multibyte(cx: &mut TestAppContext) { - init_test(cx, |_| {}); - - let view = cx - .add_window(|cx| { - let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcde\nαβγδε", cx); - build_editor(buffer.clone(), cx) - }) - .root(cx); - - assert_eq!('ⓐ'.len_utf8(), 3); - assert_eq!('α'.len_utf8(), 2); - - view.update(cx, |view, cx| { - view.fold_ranges( - vec![ - Point::new(0, 6)..Point::new(0, 12), - Point::new(1, 2)..Point::new(1, 4), - Point::new(2, 4)..Point::new(2, 8), - ], - true, - cx, - ); - assert_eq!(view.display_text(cx), "ⓐⓑ⋯ⓔ\nab⋯e\nαβ⋯ε"); - - view.move_right(&MoveRight, cx); - assert_eq!( - view.selections.display_ranges(cx), - &[empty_range(0, "ⓐ".len())] - ); - view.move_right(&MoveRight, cx); - assert_eq!( - view.selections.display_ranges(cx), - &[empty_range(0, "ⓐⓑ".len())] - ); - view.move_right(&MoveRight, cx); - assert_eq!( - view.selections.display_ranges(cx), - &[empty_range(0, "ⓐⓑ⋯".len())] - ); - - view.move_down(&MoveDown, cx); - assert_eq!( - view.selections.display_ranges(cx), - &[empty_range(1, "ab⋯e".len())] - ); - view.move_left(&MoveLeft, cx); - assert_eq!( - view.selections.display_ranges(cx), - &[empty_range(1, "ab⋯".len())] - ); - view.move_left(&MoveLeft, cx); - assert_eq!( - view.selections.display_ranges(cx), - &[empty_range(1, "ab".len())] - ); - view.move_left(&MoveLeft, cx); - assert_eq!( - view.selections.display_ranges(cx), - &[empty_range(1, "a".len())] - ); - - view.move_down(&MoveDown, cx); - assert_eq!( - view.selections.display_ranges(cx), - &[empty_range(2, "α".len())] - ); - view.move_right(&MoveRight, cx); - assert_eq!( - view.selections.display_ranges(cx), - &[empty_range(2, "αβ".len())] - ); - view.move_right(&MoveRight, cx); - assert_eq!( - view.selections.display_ranges(cx), - &[empty_range(2, "αβ⋯".len())] - ); - view.move_right(&MoveRight, 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(1, "ab⋯e".len())] - ); - view.move_down(&MoveDown, 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(1, "ab⋯e".len())] - ); - - view.move_up(&MoveUp, cx); - assert_eq!( - view.selections.display_ranges(cx), - &[empty_range(0, "ⓐⓑ".len())] - ); - view.move_left(&MoveLeft, cx); - assert_eq!( - view.selections.display_ranges(cx), - &[empty_range(0, "ⓐ".len())] - ); - view.move_left(&MoveLeft, cx); - assert_eq!( - view.selections.display_ranges(cx), - &[empty_range(0, "".len())] - ); - }); -} - -#[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) - }) - .root(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(3, "abcd".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(2, "αβγ".len())] - ); - }); -} - -#[gpui::test] -fn test_beginning_end_of_line(cx: &mut TestAppContext) { - init_test(cx, |_| {}); - - let view = cx - .add_window(|cx| { - let buffer = MultiBuffer::build_simple("abc\n def", cx); - build_editor(buffer, cx) - }) - .root(cx); - view.update(cx, |view, cx| { - view.change_selections(None, cx, |s| { - s.select_display_ranges([ - DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1), - DisplayPoint::new(1, 4)..DisplayPoint::new(1, 4), - ]); - }); - }); - - view.update(cx, |view, cx| { - view.move_to_beginning_of_line(&MoveToBeginningOfLine, cx); - assert_eq!( - view.selections.display_ranges(cx), - &[ - DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0), - DisplayPoint::new(1, 2)..DisplayPoint::new(1, 2), - ] - ); - }); - - view.update(cx, |view, cx| { - view.move_to_beginning_of_line(&MoveToBeginningOfLine, cx); - assert_eq!( - view.selections.display_ranges(cx), - &[ - DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0), - DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0), - ] - ); - }); - - view.update(cx, |view, cx| { - view.move_to_beginning_of_line(&MoveToBeginningOfLine, cx); - assert_eq!( - view.selections.display_ranges(cx), - &[ - DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0), - DisplayPoint::new(1, 2)..DisplayPoint::new(1, 2), - ] - ); - }); - - view.update(cx, |view, cx| { - view.move_to_end_of_line(&MoveToEndOfLine, cx); - assert_eq!( - view.selections.display_ranges(cx), - &[ - DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3), - DisplayPoint::new(1, 5)..DisplayPoint::new(1, 5), - ] - ); - }); - - // Moving to the end of line again is a no-op. - view.update(cx, |view, cx| { - view.move_to_end_of_line(&MoveToEndOfLine, cx); - assert_eq!( - view.selections.display_ranges(cx), - &[ - DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3), - DisplayPoint::new(1, 5)..DisplayPoint::new(1, 5), - ] - ); - }); - - view.update(cx, |view, cx| { - view.move_left(&MoveLeft, cx); - view.select_to_beginning_of_line( - &SelectToBeginningOfLine { - stop_at_soft_wraps: true, - }, - cx, - ); - assert_eq!( - view.selections.display_ranges(cx), - &[ - DisplayPoint::new(0, 2)..DisplayPoint::new(0, 0), - DisplayPoint::new(1, 4)..DisplayPoint::new(1, 2), - ] - ); - }); - - view.update(cx, |view, cx| { - view.select_to_beginning_of_line( - &SelectToBeginningOfLine { - stop_at_soft_wraps: true, - }, - cx, - ); - assert_eq!( - view.selections.display_ranges(cx), - &[ - DisplayPoint::new(0, 2)..DisplayPoint::new(0, 0), - DisplayPoint::new(1, 4)..DisplayPoint::new(1, 0), - ] - ); - }); - - view.update(cx, |view, cx| { - view.select_to_beginning_of_line( - &SelectToBeginningOfLine { - stop_at_soft_wraps: true, - }, - cx, - ); - assert_eq!( - view.selections.display_ranges(cx), - &[ - DisplayPoint::new(0, 2)..DisplayPoint::new(0, 0), - DisplayPoint::new(1, 4)..DisplayPoint::new(1, 2), - ] - ); - }); - - view.update(cx, |view, cx| { - view.select_to_end_of_line( - &SelectToEndOfLine { - stop_at_soft_wraps: true, - }, - cx, - ); - assert_eq!( - view.selections.display_ranges(cx), - &[ - DisplayPoint::new(0, 2)..DisplayPoint::new(0, 3), - DisplayPoint::new(1, 4)..DisplayPoint::new(1, 5), - ] - ); - }); - - view.update(cx, |view, cx| { - view.delete_to_end_of_line(&DeleteToEndOfLine, cx); - assert_eq!(view.display_text(cx), "ab\n de"); - assert_eq!( - view.selections.display_ranges(cx), - &[ - DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2), - DisplayPoint::new(1, 4)..DisplayPoint::new(1, 4), - ] - ); - }); - - view.update(cx, |view, cx| { - view.delete_to_beginning_of_line(&DeleteToBeginningOfLine, cx); - assert_eq!(view.display_text(cx), "\n"); - assert_eq!( - view.selections.display_ranges(cx), - &[ - DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0), - DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0), - ] - ); - }); -} - -#[gpui::test] -fn test_prev_next_word_boundary(cx: &mut TestAppContext) { - init_test(cx, |_| {}); - - let view = cx - .add_window(|cx| { - let buffer = MultiBuffer::build_simple("use std::str::{foo, bar}\n\n {baz.qux()}", cx); - build_editor(buffer, cx) - }) - .root(cx); - view.update(cx, |view, cx| { - view.change_selections(None, cx, |s| { - s.select_display_ranges([ - DisplayPoint::new(0, 11)..DisplayPoint::new(0, 11), - DisplayPoint::new(2, 4)..DisplayPoint::new(2, 4), - ]) - }); - - view.move_to_previous_word_start(&MoveToPreviousWordStart, cx); - assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", view, cx); - - view.move_to_previous_word_start(&MoveToPreviousWordStart, cx); - assert_selection_ranges("use stdˇ::str::{foo, bar}\n\n ˇ{baz.qux()}", view, cx); - - view.move_to_previous_word_start(&MoveToPreviousWordStart, cx); - assert_selection_ranges("use ˇstd::str::{foo, bar}\n\nˇ {baz.qux()}", view, cx); - - view.move_to_previous_word_start(&MoveToPreviousWordStart, cx); - assert_selection_ranges("ˇuse std::str::{foo, bar}\nˇ\n {baz.qux()}", view, cx); - - view.move_to_previous_word_start(&MoveToPreviousWordStart, cx); - assert_selection_ranges("ˇuse std::str::{foo, barˇ}\n\n {baz.qux()}", view, cx); - - view.move_to_next_word_end(&MoveToNextWordEnd, cx); - assert_selection_ranges("useˇ std::str::{foo, bar}ˇ\n\n {baz.qux()}", view, cx); - - view.move_to_next_word_end(&MoveToNextWordEnd, cx); - assert_selection_ranges("use stdˇ::str::{foo, bar}\nˇ\n {baz.qux()}", view, cx); - - view.move_to_next_word_end(&MoveToNextWordEnd, cx); - assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", view, cx); - - view.move_right(&MoveRight, cx); - view.select_to_previous_word_start(&SelectToPreviousWordStart, cx); - assert_selection_ranges("use std::«ˇs»tr::{foo, bar}\n\n {«ˇb»az.qux()}", view, cx); - - view.select_to_previous_word_start(&SelectToPreviousWordStart, cx); - assert_selection_ranges("use std«ˇ::s»tr::{foo, bar}\n\n «ˇ{b»az.qux()}", view, cx); - - view.select_to_next_word_end(&SelectToNextWordEnd, cx); - assert_selection_ranges("use std::«ˇs»tr::{foo, bar}\n\n {«ˇb»az.qux()}", view, 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) - }) - .root(cx); - - view.update(cx, |view, cx| { - view.set_wrap_width(Some(140.), 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.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(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_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)] - ); - }); -} - -#[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(vec2f(100., 4. * line_height), &mut cx); - - cx.set_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 - - 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 - - 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 - ˇ - 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 - - three - four - five - - 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(vec2f(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| { - assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 0.)); - editor.scroll_screen(&ScrollAmount::Page(1.), cx); - assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 3.)); - editor.scroll_screen(&ScrollAmount::Page(1.), cx); - assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 6.)); - editor.scroll_screen(&ScrollAmount::Page(-1.), cx); - assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 3.)); - - editor.scroll_screen(&ScrollAmount::Page(-0.5), cx); - assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 1.)); - editor.scroll_screen(&ScrollAmount::Page(0.5), cx); - assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 3.)); - }); -} - -#[gpui::test] -async fn test_autoscroll(cx: &mut gpui::TestAppContext) { - init_test(cx, |_| {}); - let mut cx = EditorTestContext::new(cx).await; - - let line_height = cx.update_editor(|editor, cx| { - editor.set_vertical_scroll_margin(2, cx); - editor.style(cx).text.line_height(cx.font_cache()) - }); - - let window = cx.window; - window.simulate_resize(vec2f(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(), vec2f(0., 0.0)); - }); - - // 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(), vec2f(0., 3.0)); - }); - - // 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(), vec2f(0., 4.0)); - }); - - // 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(), vec2f(0., 1.0)); - }); -} - -#[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(vec2f(100., 4. * line_height), &mut cx); - - 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) { - init_test(cx, |_| {}); - let mut cx = EditorTestContext::new(cx).await; - cx.set_state("one «two threeˇ» four"); - cx.update_editor(|editor, cx| { - editor.delete_to_beginning_of_line(&DeleteToBeginningOfLine, cx); - assert_eq!(editor.text(cx), " four"); - }); -} - -#[gpui::test] -fn test_delete_to_word_boundary(cx: &mut TestAppContext) { - init_test(cx, |_| {}); - - let view = cx - .add_window(|cx| { - let buffer = MultiBuffer::build_simple("one two three four", cx); - build_editor(buffer.clone(), cx) - }) - .root(cx); - - view.update(cx, |view, cx| { - view.change_selections(None, cx, |s| { - s.select_display_ranges([ - // an empty selection - the preceding word fragment is deleted - DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2), - // characters selected - they are deleted - DisplayPoint::new(0, 9)..DisplayPoint::new(0, 12), - ]) - }); - view.delete_to_previous_word_start(&DeleteToPreviousWordStart, cx); - assert_eq!(view.buffer.read(cx).read(cx).text(), "e two te four"); - }); - - view.update(cx, |view, cx| { - view.change_selections(None, cx, |s| { - s.select_display_ranges([ - // an empty selection - the following word fragment is deleted - DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3), - // characters selected - they are deleted - DisplayPoint::new(0, 9)..DisplayPoint::new(0, 10), - ]) - }); - view.delete_to_next_word_end(&DeleteToNextWordEnd, cx); - assert_eq!(view.buffer.read(cx).read(cx).text(), "e t te our"); - }); -} - -#[gpui::test] -fn test_newline(cx: &mut TestAppContext) { - init_test(cx, |_| {}); - - let view = cx - .add_window(|cx| { - let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx); - build_editor(buffer.clone(), cx) - }) - .root(cx); - - view.update(cx, |view, cx| { - view.change_selections(None, cx, |s| { - s.select_display_ranges([ - DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2), - DisplayPoint::new(1, 2)..DisplayPoint::new(1, 2), - DisplayPoint::new(1, 6)..DisplayPoint::new(1, 6), - ]) - }); - - view.newline(&Newline, cx); - assert_eq!(view.text(cx), "aa\naa\n \n bb\n bb\n"); - }); -} - -#[gpui::test] -fn test_newline_with_old_selections(cx: &mut TestAppContext) { - init_test(cx, |_| {}); - - let editor = cx - .add_window(|cx| { - let buffer = MultiBuffer::build_simple( - " - a - b( - X - ) - c( - X - ) - " - .unindent() - .as_str(), - cx, - ); - let mut editor = build_editor(buffer.clone(), cx); - editor.change_selections(None, cx, |s| { - s.select_ranges([ - Point::new(2, 4)..Point::new(2, 5), - Point::new(5, 4)..Point::new(5, 5), - ]) - }); - editor - }) - .root(cx); - - editor.update(cx, |editor, cx| { - // Edit the buffer directly, deleting ranges surrounding the editor's selections - editor.buffer.update(cx, |buffer, cx| { - buffer.edit( - [ - (Point::new(1, 2)..Point::new(3, 0), ""), - (Point::new(4, 2)..Point::new(6, 0), ""), - ], - None, - cx, - ); - assert_eq!( - buffer.read(cx).text(), - " - a - b() - c() - " - .unindent() - ); - }); - assert_eq!( - editor.selections.ranges(cx), - &[ - Point::new(1, 2)..Point::new(1, 2), - Point::new(2, 2)..Point::new(2, 2), - ], - ); - - editor.newline(&Newline, cx); - assert_eq!( - editor.text(cx), - " - a - b( - ) - c( - ) - " - .unindent() - ); - - // The selections are moved after the inserted newlines - assert_eq!( - editor.selections.ranges(cx), - &[ - Point::new(2, 0)..Point::new(2, 0), - Point::new(4, 0)..Point::new(4, 0), - ], - ); - }); -} - -#[gpui::test] -async fn test_newline_above(cx: &mut gpui::TestAppContext) { - init_test(cx, |settings| { - settings.defaults.tab_size = NonZeroU32::new(4) - }); - - let language = Arc::new( - Language::new( - LanguageConfig::default(), - Some(tree_sitter_rust::language()), - ) - .with_indents_query(r#"(_ "(" ")" @end) @indent"#) - .unwrap(), - ); - - let mut cx = EditorTestContext::new(cx).await; - cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx)); - cx.set_state(indoc! {" - const a: ˇA = ( - (ˇ - «const_functionˇ»(ˇ), - so«mˇ»et«hˇ»ing_ˇelse,ˇ - )ˇ - ˇ);ˇ - "}); - - cx.update_editor(|e, cx| e.newline_above(&NewlineAbove, cx)); - cx.assert_editor_state(indoc! {" - ˇ - const a: A = ( - ˇ - ( - ˇ - ˇ - const_function(), - ˇ - ˇ - ˇ - ˇ - something_else, - ˇ - ) - ˇ - ˇ - ); - "}); -} - -#[gpui::test] -async fn test_newline_below(cx: &mut gpui::TestAppContext) { - init_test(cx, |settings| { - settings.defaults.tab_size = NonZeroU32::new(4) - }); - - let language = Arc::new( - Language::new( - LanguageConfig::default(), - Some(tree_sitter_rust::language()), - ) - .with_indents_query(r#"(_ "(" ")" @end) @indent"#) - .unwrap(), - ); - - let mut cx = EditorTestContext::new(cx).await; - cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx)); - cx.set_state(indoc! {" - const a: ˇA = ( - (ˇ - «const_functionˇ»(ˇ), - so«mˇ»et«hˇ»ing_ˇelse,ˇ - )ˇ - ˇ);ˇ - "}); - - cx.update_editor(|e, cx| e.newline_below(&NewlineBelow, cx)); - cx.assert_editor_state(indoc! {" - const a: A = ( - ˇ - ( - ˇ - const_function(), - ˇ - ˇ - something_else, - ˇ - ˇ - ˇ - ˇ - ) - ˇ - ); - ˇ - ˇ - "}); -} - -#[gpui::test] -async fn test_newline_comments(cx: &mut gpui::TestAppContext) { - init_test(cx, |settings| { - settings.defaults.tab_size = NonZeroU32::new(4) - }); - - let language = Arc::new(Language::new( - LanguageConfig { - line_comment: Some("//".into()), - ..LanguageConfig::default() - }, - None, - )); - { - let mut cx = EditorTestContext::new(cx).await; - cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx)); - cx.set_state(indoc! {" - // Fooˇ - "}); - - cx.update_editor(|e, cx| e.newline(&Newline, cx)); - cx.assert_editor_state(indoc! {" - // Foo - //ˇ - "}); - // Ensure that if cursor is before the comment start, we do not actually insert a comment prefix. - cx.set_state(indoc! {" - ˇ// Foo - "}); - cx.update_editor(|e, cx| e.newline(&Newline, cx)); - cx.assert_editor_state(indoc! {" - - ˇ// Foo - "}); - } - // Ensure that comment continuations can be disabled. - update_test_language_settings(cx, |settings| { - settings.defaults.extend_comment_on_newline = Some(false); - }); - let mut cx = EditorTestContext::new(cx).await; - cx.set_state(indoc! {" - // Fooˇ - "}); - cx.update_editor(|e, cx| e.newline(&Newline, cx)); - cx.assert_editor_state(indoc! {" - // Foo - ˇ - "}); -} - -#[gpui::test] -fn test_insert_with_old_selections(cx: &mut TestAppContext) { - init_test(cx, |_| {}); - - let editor = cx - .add_window(|cx| { - let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx); - let mut editor = build_editor(buffer.clone(), cx); - editor.change_selections(None, cx, |s| s.select_ranges([3..4, 11..12, 19..20])); - editor - }) - .root(cx); - - editor.update(cx, |editor, cx| { - // Edit the buffer directly, deleting ranges surrounding the editor's selections - editor.buffer.update(cx, |buffer, cx| { - buffer.edit([(2..5, ""), (10..13, ""), (18..21, "")], None, cx); - assert_eq!(buffer.read(cx).text(), "a(), b(), c()".unindent()); - }); - assert_eq!(editor.selections.ranges(cx), &[2..2, 7..7, 12..12],); - - editor.insert("Z", cx); - assert_eq!(editor.text(cx), "a(Z), b(Z), c(Z)"); - - // The selections are moved after the inserted characters - assert_eq!(editor.selections.ranges(cx), &[3..3, 9..9, 15..15],); - }); -} - -#[gpui::test] -async fn test_tab(cx: &mut gpui::TestAppContext) { - init_test(cx, |settings| { - settings.defaults.tab_size = NonZeroU32::new(3) - }); - - let mut cx = EditorTestContext::new(cx).await; - cx.set_state(indoc! {" - ˇabˇc - ˇ🏀ˇ🏀ˇefg - dˇ - "}); - cx.update_editor(|e, cx| e.tab(&Tab, cx)); - cx.assert_editor_state(indoc! {" - ˇab ˇc - ˇ🏀 ˇ🏀 ˇefg - d ˇ - "}); - - cx.set_state(indoc! {" - a - «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ» - "}); - cx.update_editor(|e, cx| e.tab(&Tab, cx)); - cx.assert_editor_state(indoc! {" - a - «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ» - "}); -} - -#[gpui::test] -async fn test_tab_in_leading_whitespace_auto_indents_lines(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()), - ) - .with_indents_query(r#"(_ "(" ")" @end) @indent"#) - .unwrap(), - ); - cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx)); - - // cursors that are already at the suggested indent level insert - // a soft tab. cursors that are to the left of the suggested indent - // auto-indent their line. - cx.set_state(indoc! {" - ˇ - const a: B = ( - c( - d( - ˇ - ) - ˇ - ˇ ) - ); - "}); - cx.update_editor(|e, cx| e.tab(&Tab, cx)); - cx.assert_editor_state(indoc! {" - ˇ - const a: B = ( - c( - d( - ˇ - ) - ˇ - ˇ) - ); - "}); - - // handle auto-indent when there are multiple cursors on the same line - cx.set_state(indoc! {" - const a: B = ( - c( - ˇ ˇ - ˇ ) - ); - "}); - cx.update_editor(|e, cx| e.tab(&Tab, cx)); - cx.assert_editor_state(indoc! {" - const a: B = ( - c( - ˇ - ˇ) - ); - "}); -} - -#[gpui::test] -async fn test_tab_with_mixed_whitespace(cx: &mut gpui::TestAppContext) { - init_test(cx, |settings| { - settings.defaults.tab_size = NonZeroU32::new(4) - }); - - let language = Arc::new( - Language::new( - LanguageConfig::default(), - Some(tree_sitter_rust::language()), - ) - .with_indents_query(r#"(_ "{" "}" @end) @indent"#) - .unwrap(), - ); - - let mut cx = EditorTestContext::new(cx).await; - cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx)); - cx.set_state(indoc! {" - fn a() { - if b { - \t ˇc - } - } - "}); - - cx.update_editor(|e, cx| e.tab(&Tab, cx)); - cx.assert_editor_state(indoc! {" - fn a() { - if b { - ˇc - } - } - "}); -} - -#[gpui::test] -async fn test_indent_outdent(cx: &mut gpui::TestAppContext) { - init_test(cx, |settings| { - settings.defaults.tab_size = NonZeroU32::new(4); - }); - - let mut cx = EditorTestContext::new(cx).await; - - cx.set_state(indoc! {" - «oneˇ» «twoˇ» - three - four - "}); - cx.update_editor(|e, cx| e.tab(&Tab, cx)); - cx.assert_editor_state(indoc! {" - «oneˇ» «twoˇ» - three - four - "}); - - cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx)); - cx.assert_editor_state(indoc! {" - «oneˇ» «twoˇ» - three - four - "}); - - // select across line ending - cx.set_state(indoc! {" - one two - t«hree - ˇ» four - "}); - cx.update_editor(|e, cx| e.tab(&Tab, cx)); - cx.assert_editor_state(indoc! {" - one two - t«hree - ˇ» four - "}); - - cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx)); - cx.assert_editor_state(indoc! {" - one two - t«hree - ˇ» four - "}); - - // Ensure that indenting/outdenting works when the cursor is at column 0. - cx.set_state(indoc! {" - one two - ˇthree - four - "}); - cx.update_editor(|e, cx| e.tab(&Tab, cx)); - cx.assert_editor_state(indoc! {" - one two - ˇthree - four - "}); - - cx.set_state(indoc! {" - one two - ˇ three - four - "}); - cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx)); - cx.assert_editor_state(indoc! {" - one two - ˇthree - four - "}); -} - -#[gpui::test] -async fn test_indent_outdent_with_hard_tabs(cx: &mut gpui::TestAppContext) { - init_test(cx, |settings| { - settings.defaults.hard_tabs = Some(true); - }); - - let mut cx = EditorTestContext::new(cx).await; - - // select two ranges on one line - cx.set_state(indoc! {" - «oneˇ» «twoˇ» - three - four - "}); - cx.update_editor(|e, cx| e.tab(&Tab, cx)); - cx.assert_editor_state(indoc! {" - \t«oneˇ» «twoˇ» - three - four - "}); - cx.update_editor(|e, cx| e.tab(&Tab, cx)); - cx.assert_editor_state(indoc! {" - \t\t«oneˇ» «twoˇ» - three - four - "}); - cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx)); - cx.assert_editor_state(indoc! {" - \t«oneˇ» «twoˇ» - three - four - "}); - cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx)); - cx.assert_editor_state(indoc! {" - «oneˇ» «twoˇ» - three - four - "}); - - // select across a line ending - cx.set_state(indoc! {" - one two - t«hree - ˇ»four - "}); - cx.update_editor(|e, cx| e.tab(&Tab, cx)); - cx.assert_editor_state(indoc! {" - one two - \tt«hree - ˇ»four - "}); - cx.update_editor(|e, cx| e.tab(&Tab, cx)); - cx.assert_editor_state(indoc! {" - one two - \t\tt«hree - ˇ»four - "}); - cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx)); - cx.assert_editor_state(indoc! {" - one two - \tt«hree - ˇ»four - "}); - cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx)); - cx.assert_editor_state(indoc! {" - one two - t«hree - ˇ»four - "}); - - // Ensure that indenting/outdenting works when the cursor is at column 0. - cx.set_state(indoc! {" - one two - ˇthree - four - "}); - cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx)); - cx.assert_editor_state(indoc! {" - one two - ˇthree - four - "}); - cx.update_editor(|e, cx| e.tab(&Tab, cx)); - cx.assert_editor_state(indoc! {" - one two - \tˇthree - four - "}); - cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx)); - cx.assert_editor_state(indoc! {" - one two - ˇthree - four - "}); -} - -#[gpui::test] -fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) { - init_test(cx, |settings| { - settings.languages.extend([ - ( - "TOML".into(), - LanguageSettingsContent { - tab_size: NonZeroU32::new(2), - ..Default::default() - }, - ), - ( - "Rust".into(), - LanguageSettingsContent { - tab_size: NonZeroU32::new(4), - ..Default::default() - }, - ), - ]); - }); - - let toml_language = Arc::new(Language::new( - LanguageConfig { - name: "TOML".into(), - ..Default::default() - }, - None, - )); - let rust_language = Arc::new(Language::new( - LanguageConfig { - name: "Rust".into(), - ..Default::default() - }, - None, - )); - - let toml_buffer = cx.add_model(|cx| { - Buffer::new(0, cx.model_id() as u64, "a = 1\nb = 2\n").with_language(toml_language, cx) - }); - let rust_buffer = cx.add_model(|cx| { - Buffer::new(0, cx.model_id() as u64, "const c: usize = 3;\n") - .with_language(rust_language, cx) - }); - let multibuffer = cx.add_model(|cx| { - let mut multibuffer = MultiBuffer::new(0); - multibuffer.push_excerpts( - toml_buffer.clone(), - [ExcerptRange { - context: Point::new(0, 0)..Point::new(2, 0), - primary: None, - }], - cx, - ); - multibuffer.push_excerpts( - rust_buffer.clone(), - [ExcerptRange { - context: Point::new(0, 0)..Point::new(1, 0), - primary: None, - }], - cx, - ); - multibuffer - }); - - cx.add_window(|cx| { - let mut editor = build_editor(multibuffer, cx); - - assert_eq!( - editor.text(cx), - indoc! {" - a = 1 - b = 2 - - const c: usize = 3; - "} - ); - - select_ranges( - &mut editor, - indoc! {" - «aˇ» = 1 - b = 2 - - «const c:ˇ» usize = 3; - "}, - cx, - ); - - editor.tab(&Tab, cx); - assert_text_with_selections( - &mut editor, - indoc! {" - «aˇ» = 1 - b = 2 - - «const c:ˇ» usize = 3; - "}, - cx, - ); - editor.tab_prev(&TabPrev, cx); - assert_text_with_selections( - &mut editor, - indoc! {" - «aˇ» = 1 - b = 2 - - «const c:ˇ» usize = 3; - "}, - cx, - ); - - editor - }); -} - -#[gpui::test] -async fn test_backspace(cx: &mut gpui::TestAppContext) { - init_test(cx, |_| {}); - - let mut cx = EditorTestContext::new(cx).await; - - // Basic backspace - cx.set_state(indoc! {" - onˇe two three - fou«rˇ» five six - seven «ˇeight nine - »ten - "}); - cx.update_editor(|e, cx| e.backspace(&Backspace, cx)); - cx.assert_editor_state(indoc! {" - oˇe two three - fouˇ five six - seven ˇten - "}); - - // Test backspace inside and around indents - cx.set_state(indoc! {" - zero - ˇone - ˇtwo - ˇ ˇ ˇ three - ˇ ˇ four - "}); - cx.update_editor(|e, cx| e.backspace(&Backspace, cx)); - cx.assert_editor_state(indoc! {" - zero - ˇone - ˇtwo - ˇ threeˇ four - "}); - - // Test backspace with line_mode set to true - cx.update_editor(|e, _| e.selections.line_mode = true); - cx.set_state(indoc! {" - The ˇquick ˇbrown - fox jumps over - the lazy dog - ˇThe qu«ick bˇ»rown"}); - cx.update_editor(|e, cx| e.backspace(&Backspace, cx)); - cx.assert_editor_state(indoc! {" - ˇfox jumps over - the lazy dogˇ"}); -} - -#[gpui::test] -async fn test_delete(cx: &mut gpui::TestAppContext) { - init_test(cx, |_| {}); - - let mut cx = EditorTestContext::new(cx).await; - cx.set_state(indoc! {" - onˇe two three - fou«rˇ» five six - seven «ˇeight nine - »ten - "}); - cx.update_editor(|e, cx| e.delete(&Delete, cx)); - cx.assert_editor_state(indoc! {" - onˇ two three - fouˇ five six - seven ˇten - "}); - - // Test backspace with line_mode set to true - cx.update_editor(|e, _| e.selections.line_mode = true); - cx.set_state(indoc! {" - The ˇquick ˇbrown - fox «ˇjum»ps over - the lazy dog - ˇThe qu«ick bˇ»rown"}); - cx.update_editor(|e, cx| e.backspace(&Backspace, cx)); - cx.assert_editor_state("ˇthe lazy dogˇ"); -} - -#[gpui::test] -fn test_delete_line(cx: &mut TestAppContext) { - init_test(cx, |_| {}); - - let view = cx - .add_window(|cx| { - let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx); - build_editor(buffer, cx) - }) - .root(cx); - view.update(cx, |view, cx| { - view.change_selections(None, cx, |s| { - s.select_display_ranges([ - DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1), - DisplayPoint::new(1, 0)..DisplayPoint::new(1, 1), - DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0), - ]) - }); - view.delete_line(&DeleteLine, cx); - assert_eq!(view.display_text(cx), "ghi"); - assert_eq!( - view.selections.display_ranges(cx), - vec![ - DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0), - DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1) - ] - ); - }); - - let view = cx - .add_window(|cx| { - let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx); - build_editor(buffer, cx) - }) - .root(cx); - view.update(cx, |view, cx| { - view.change_selections(None, cx, |s| { - s.select_display_ranges([DisplayPoint::new(2, 0)..DisplayPoint::new(0, 1)]) - }); - view.delete_line(&DeleteLine, cx); - assert_eq!(view.display_text(cx), "ghi\n"); - assert_eq!( - view.selections.display_ranges(cx), - vec![DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1)] - ); - }); -} - -#[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(); - - 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 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)] - ); - - // 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 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, - ) - }); - - // 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 ignore any leading tabs - editor.join_lines(&JoinLines, cx); - assert_eq!(buffer.read(cx).text(), "aaa bbb c d"); - - editor - }); -} - -#[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(); - - 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"); - - 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) { - init_test(cx, |_| {}); - - let mut cx = EditorTestContext::new(cx).await; - - // Test sort_lines_case_insensitive() - cx.set_state(indoc! {" - «z - y - x - Z - Y - Xˇ» - "}); - cx.update_editor(|e, cx| e.sort_lines_case_insensitive(&SortLinesCaseInsensitive, cx)); - cx.assert_editor_state(indoc! {" - «x - X - y - Y - z - Zˇ» - "}); - - // Test reverse_lines() - cx.set_state(indoc! {" - «5 - 4 - 3 - 2 - 1ˇ» - "}); - cx.update_editor(|e, cx| e.reverse_lines(&ReverseLines, cx)); - cx.assert_editor_state(indoc! {" - «1 - 2 - 3 - 4 - 5ˇ» - "}); - - // Skip testing shuffle_line() - - // From here on out, test more complex cases of manipulate_lines() with a single driver method: sort_lines_case_sensitive() - // Since all methods calling manipulate_lines() are doing the exact same general thing (reordering lines) - - // Don't manipulate when cursor is on single line, but expand the selection - cx.set_state(indoc! {" - ddˇdd - ccc - bb - a - "}); - cx.update_editor(|e, cx| e.sort_lines_case_sensitive(&SortLinesCaseSensitive, cx)); - cx.assert_editor_state(indoc! {" - «ddddˇ» - ccc - bb - a - "}); - - // Basic manipulate case - // Start selection moves to column 0 - // End of selection shrinks to fit shorter line - cx.set_state(indoc! {" - dd«d - ccc - bb - aaaaaˇ» - "}); - cx.update_editor(|e, cx| e.sort_lines_case_sensitive(&SortLinesCaseSensitive, cx)); - cx.assert_editor_state(indoc! {" - «aaaaa - bb - ccc - dddˇ» - "}); - - // Manipulate case with newlines - cx.set_state(indoc! {" - dd«d - ccc - - bb - aaaaa - - ˇ» - "}); - cx.update_editor(|e, cx| e.sort_lines_case_sensitive(&SortLinesCaseSensitive, cx)); - cx.assert_editor_state(indoc! {" - « - - aaaaa - bb - ccc - dddˇ» - - "}); -} - -#[gpui::test] -async fn test_manipulate_lines_with_multi_selection(cx: &mut TestAppContext) { - init_test(cx, |_| {}); - - let mut cx = EditorTestContext::new(cx).await; - - // Manipulate with multiple selections on a single line - cx.set_state(indoc! {" - dd«dd - cˇ»c«c - bb - aaaˇ»aa - "}); - cx.update_editor(|e, cx| e.sort_lines_case_sensitive(&SortLinesCaseSensitive, cx)); - cx.assert_editor_state(indoc! {" - «aaaaa - bb - ccc - ddddˇ» - "}); - - // Manipulate with multiple disjoin selections - cx.set_state(indoc! {" - 5« - 4 - 3 - 2 - 1ˇ» - - dd«dd - ccc - bb - aaaˇ»aa - "}); - cx.update_editor(|e, cx| e.sort_lines_case_sensitive(&SortLinesCaseSensitive, cx)); - cx.assert_editor_state(indoc! {" - «1 - 2 - 3 - 4 - 5ˇ» - - «aaaaa - bb - ccc - ddddˇ» - "}); -} - -#[gpui::test] -async fn test_manipulate_text(cx: &mut TestAppContext) { - init_test(cx, |_| {}); - - let mut cx = EditorTestContext::new(cx).await; - - // Test convert_to_upper_case() - cx.set_state(indoc! {" - «hello worldˇ» - "}); - cx.update_editor(|e, cx| e.convert_to_upper_case(&ConvertToUpperCase, cx)); - cx.assert_editor_state(indoc! {" - «HELLO WORLDˇ» - "}); - - // Test convert_to_lower_case() - cx.set_state(indoc! {" - «HELLO WORLDˇ» - "}); - cx.update_editor(|e, cx| e.convert_to_lower_case(&ConvertToLowerCase, cx)); - cx.assert_editor_state(indoc! {" - «hello worldˇ» - "}); - - // Test multiple line, single selection case - // Test code hack that covers the fact that to_case crate doesn't support '\n' as a word boundary - cx.set_state(indoc! {" - «The quick brown - fox jumps over - the lazy dogˇ» - "}); - cx.update_editor(|e, cx| e.convert_to_title_case(&ConvertToTitleCase, cx)); - cx.assert_editor_state(indoc! {" - «The Quick Brown - Fox Jumps Over - The Lazy Dogˇ» - "}); - - // Test multiple line, single selection case - // Test code hack that covers the fact that to_case crate doesn't support '\n' as a word boundary - cx.set_state(indoc! {" - «The quick brown - fox jumps over - the lazy dogˇ» - "}); - cx.update_editor(|e, cx| e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, cx)); - cx.assert_editor_state(indoc! {" - «TheQuickBrown - FoxJumpsOver - TheLazyDogˇ» - "}); - - // From here on out, test more complex cases of manipulate_text() - - // Test no selection case - should affect words cursors are in - // Cursor at beginning, middle, and end of word - cx.set_state(indoc! {" - ˇhello big beauˇtiful worldˇ - "}); - cx.update_editor(|e, cx| e.convert_to_upper_case(&ConvertToUpperCase, cx)); - cx.assert_editor_state(indoc! {" - «HELLOˇ» big «BEAUTIFULˇ» «WORLDˇ» - "}); - - // Test multiple selections on a single line and across multiple lines - cx.set_state(indoc! {" - «Theˇ» quick «brown - foxˇ» jumps «overˇ» - the «lazyˇ» dog - "}); - cx.update_editor(|e, cx| e.convert_to_upper_case(&ConvertToUpperCase, cx)); - cx.assert_editor_state(indoc! {" - «THEˇ» quick «BROWN - FOXˇ» jumps «OVERˇ» - the «LAZYˇ» dog - "}); - - // Test case where text length grows - cx.set_state(indoc! {" - «tschüߡ» - "}); - cx.update_editor(|e, cx| e.convert_to_upper_case(&ConvertToUpperCase, cx)); - cx.assert_editor_state(indoc! {" - «TSCHÜSSˇ» - "}); - - // Test to make sure we don't crash when text shrinks - cx.set_state(indoc! {" - aaa_bbbˇ - "}); - cx.update_editor(|e, cx| e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, cx)); - cx.assert_editor_state(indoc! {" - «aaaBbbˇ» - "}); - - // Test to make sure we all aware of the fact that each word can grow and shrink - // Final selections should be aware of this fact - cx.set_state(indoc! {" - aaa_bˇbb bbˇb_ccc ˇccc_ddd - "}); - cx.update_editor(|e, cx| e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, cx)); - cx.assert_editor_state(indoc! {" - «aaaBbbˇ» «bbbCccˇ» «cccDddˇ» - "}); -} - -#[gpui::test] -fn test_duplicate_line(cx: &mut TestAppContext) { - init_test(cx, |_| {}); - - let view = cx - .add_window(|cx| { - let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx); - build_editor(buffer, cx) - }) - .root(cx); - view.update(cx, |view, cx| { - view.change_selections(None, cx, |s| { - s.select_display_ranges([ - DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1), - DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2), - DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0), - DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0), - ]) - }); - view.duplicate_line(&DuplicateLine, cx); - assert_eq!(view.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n"); - assert_eq!( - view.selections.display_ranges(cx), - vec![ - DisplayPoint::new(1, 0)..DisplayPoint::new(1, 1), - DisplayPoint::new(1, 2)..DisplayPoint::new(1, 2), - DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0), - DisplayPoint::new(6, 0)..DisplayPoint::new(6, 0), - ] - ); - }); - - let view = cx - .add_window(|cx| { - let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx); - build_editor(buffer, cx) - }) - .root(cx); - view.update(cx, |view, cx| { - view.change_selections(None, cx, |s| { - s.select_display_ranges([ - DisplayPoint::new(0, 1)..DisplayPoint::new(1, 1), - DisplayPoint::new(1, 2)..DisplayPoint::new(2, 1), - ]) - }); - view.duplicate_line(&DuplicateLine, cx); - assert_eq!(view.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n"); - assert_eq!( - view.selections.display_ranges(cx), - vec![ - DisplayPoint::new(3, 1)..DisplayPoint::new(4, 1), - DisplayPoint::new(4, 2)..DisplayPoint::new(5, 1), - ] - ); - }); -} - -#[gpui::test] -fn test_move_line_up_down(cx: &mut TestAppContext) { - init_test(cx, |_| {}); - - let view = cx - .add_window(|cx| { - let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx); - build_editor(buffer, cx) - }) - .root(cx); - view.update(cx, |view, cx| { - view.fold_ranges( - vec![ - Point::new(0, 2)..Point::new(1, 2), - Point::new(2, 3)..Point::new(4, 1), - Point::new(7, 0)..Point::new(8, 4), - ], - true, - cx, - ); - view.change_selections(None, cx, |s| { - s.select_display_ranges([ - DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1), - DisplayPoint::new(3, 1)..DisplayPoint::new(3, 1), - DisplayPoint::new(3, 2)..DisplayPoint::new(4, 3), - DisplayPoint::new(5, 0)..DisplayPoint::new(5, 2), - ]) - }); - assert_eq!( - view.display_text(cx), - "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i\njjjjj" - ); - - view.move_line_up(&MoveLineUp, cx); - assert_eq!( - view.display_text(cx), - "aa⋯bbb\nccc⋯eeee\nggggg\n⋯i\njjjjj\nfffff" - ); - assert_eq!( - view.selections.display_ranges(cx), - vec![ - DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1), - DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1), - DisplayPoint::new(2, 2)..DisplayPoint::new(3, 3), - DisplayPoint::new(4, 0)..DisplayPoint::new(4, 2) - ] - ); - }); - - view.update(cx, |view, cx| { - view.move_line_down(&MoveLineDown, cx); - assert_eq!( - view.display_text(cx), - "ccc⋯eeee\naa⋯bbb\nfffff\nggggg\n⋯i\njjjjj" - ); - assert_eq!( - view.selections.display_ranges(cx), - vec![ - DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1), - DisplayPoint::new(3, 1)..DisplayPoint::new(3, 1), - DisplayPoint::new(3, 2)..DisplayPoint::new(4, 3), - DisplayPoint::new(5, 0)..DisplayPoint::new(5, 2) - ] - ); - }); - - view.update(cx, |view, cx| { - view.move_line_down(&MoveLineDown, cx); - assert_eq!( - view.display_text(cx), - "ccc⋯eeee\nfffff\naa⋯bbb\nggggg\n⋯i\njjjjj" - ); - assert_eq!( - view.selections.display_ranges(cx), - vec![ - DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1), - DisplayPoint::new(3, 1)..DisplayPoint::new(3, 1), - DisplayPoint::new(3, 2)..DisplayPoint::new(4, 3), - DisplayPoint::new(5, 0)..DisplayPoint::new(5, 2) - ] - ); - }); - - view.update(cx, |view, cx| { - view.move_line_up(&MoveLineUp, cx); - assert_eq!( - view.display_text(cx), - "ccc⋯eeee\naa⋯bbb\nggggg\n⋯i\njjjjj\nfffff" - ); - assert_eq!( - view.selections.display_ranges(cx), - vec![ - DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1), - DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1), - DisplayPoint::new(2, 2)..DisplayPoint::new(3, 3), - DisplayPoint::new(4, 0)..DisplayPoint::new(4, 2) - ] - ); - }); -} - -#[gpui::test] -fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) { - init_test(cx, |_| {}); - - let editor = cx - .add_window(|cx| { - let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx); - build_editor(buffer, cx) - }) - .root(cx); - editor.update(cx, |editor, cx| { - let snapshot = editor.buffer.read(cx).snapshot(cx); - editor.insert_blocks( - [BlockProperties { - style: BlockStyle::Fixed, - position: snapshot.anchor_after(Point::new(2, 0)), - disposition: BlockDisposition::Below, - height: 1, - render: Arc::new(|_| Empty::new().into_any()), - }], - Some(Autoscroll::fit()), - cx, - ); - editor.change_selections(None, cx, |s| { - s.select_ranges([Point::new(2, 0)..Point::new(2, 0)]) - }); - editor.move_line_down(&MoveLineDown, cx); - }); -} - -#[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]); - - editor - }); - - _ = cx.add_window(|cx| { - let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), 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.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.transpose(&Default::default(), cx); - assert_eq!(editor.text(cx), "acbde\n"); - assert_eq!(editor.selections.ranges(cx), [6..6]); - - editor.transpose(&Default::default(), cx); - assert_eq!(editor.text(cx), "acbd\ne"); - assert_eq!(editor.selections.ranges(cx), [6..6]); - - editor - }); - - _ = cx.add_window(|cx| { - let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), 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), "bcade\n"); - assert_eq!(editor.selections.ranges(cx), [3..3, 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), "bcade\n"); - 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 - }); - - _ = cx.add_window(|cx| { - let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), 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 - }); -} - -#[gpui::test] -async fn test_clipboard(cx: &mut gpui::TestAppContext) { - init_test(cx, |_| {}); - - 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")), - ); - - // 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"}); -} - -#[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)); - - // 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 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(), - ˇ); - "}); - - // 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 - ) - ˇ - ) - ); - "}); -} - -#[gpui::test] -fn test_select_all(cx: &mut TestAppContext) { - init_test(cx, |_| {}); - - let view = cx - .add_window(|cx| { - let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx); - build_editor(buffer, cx) - }) - .root(cx); - view.update(cx, |view, cx| { - view.select_all(&SelectAll, cx); - assert_eq!( - view.selections.display_ranges(cx), - &[DisplayPoint::new(0, 0)..DisplayPoint::new(2, 3)] - ); - }); -} - -#[gpui::test] -fn test_select_line(cx: &mut TestAppContext) { - init_test(cx, |_| {}); - - let view = cx - .add_window(|cx| { - let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx); - build_editor(buffer, cx) - }) - .root(cx); - view.update(cx, |view, cx| { - view.change_selections(None, cx, |s| { - s.select_display_ranges([ - DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1), - DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2), - DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0), - DisplayPoint::new(4, 2)..DisplayPoint::new(4, 2), - ]) - }); - view.select_line(&SelectLine, cx); - assert_eq!( - view.selections.display_ranges(cx), - vec![ - DisplayPoint::new(0, 0)..DisplayPoint::new(2, 0), - DisplayPoint::new(4, 0)..DisplayPoint::new(5, 0), - ] - ); - }); - - view.update(cx, |view, cx| { - view.select_line(&SelectLine, cx); - assert_eq!( - view.selections.display_ranges(cx), - vec![ - DisplayPoint::new(0, 0)..DisplayPoint::new(3, 0), - DisplayPoint::new(4, 0)..DisplayPoint::new(5, 5), - ] - ); - }); - - view.update(cx, |view, cx| { - view.select_line(&SelectLine, cx); - assert_eq!( - view.selections.display_ranges(cx), - vec![DisplayPoint::new(0, 0)..DisplayPoint::new(5, 5)] - ); - }); -} - -#[gpui::test] -fn test_split_selection_into_lines(cx: &mut TestAppContext) { - init_test(cx, |_| {}); - - let view = cx - .add_window(|cx| { - let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx); - build_editor(buffer, cx) - }) - .root(cx); - view.update(cx, |view, cx| { - view.fold_ranges( - vec![ - Point::new(0, 2)..Point::new(1, 2), - Point::new(2, 3)..Point::new(4, 1), - Point::new(7, 0)..Point::new(8, 4), - ], - true, - cx, - ); - view.change_selections(None, cx, |s| { - s.select_display_ranges([ - DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1), - DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2), - DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0), - DisplayPoint::new(4, 4)..DisplayPoint::new(4, 4), - ]) - }); - assert_eq!(view.display_text(cx), "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i"); - }); - - view.update(cx, |view, cx| { - view.split_selection_into_lines(&SplitSelectionIntoLines, cx); - assert_eq!( - view.display_text(cx), - "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i" - ); - assert_eq!( - view.selections.display_ranges(cx), - [ - DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1), - DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2), - DisplayPoint::new(2, 0)..DisplayPoint::new(2, 0), - DisplayPoint::new(5, 4)..DisplayPoint::new(5, 4) - ] - ); - }); - - view.update(cx, |view, cx| { - view.change_selections(None, cx, |s| { - s.select_display_ranges([DisplayPoint::new(5, 0)..DisplayPoint::new(0, 1)]) - }); - view.split_selection_into_lines(&SplitSelectionIntoLines, cx); - assert_eq!( - view.display_text(cx), - "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii" - ); - assert_eq!( - view.selections.display_ranges(cx), - [ - DisplayPoint::new(0, 5)..DisplayPoint::new(0, 5), - DisplayPoint::new(1, 5)..DisplayPoint::new(1, 5), - DisplayPoint::new(2, 5)..DisplayPoint::new(2, 5), - DisplayPoint::new(3, 5)..DisplayPoint::new(3, 5), - DisplayPoint::new(4, 5)..DisplayPoint::new(4, 5), - DisplayPoint::new(5, 5)..DisplayPoint::new(5, 5), - DisplayPoint::new(6, 5)..DisplayPoint::new(6, 5), - DisplayPoint::new(7, 0)..DisplayPoint::new(7, 0) - ] - ); - }); -} - -#[gpui::test] -fn test_add_selection_above_below(cx: &mut TestAppContext) { - init_test(cx, |_| {}); - - let view = cx - .add_window(|cx| { - let buffer = MultiBuffer::build_simple("abc\ndefghi\n\njk\nlmno\n", cx); - build_editor(buffer, cx) - }) - .root(cx); - - view.update(cx, |view, cx| { - view.change_selections(None, cx, |s| { - s.select_display_ranges([DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3)]) - }); - }); - view.update(cx, |view, cx| { - view.add_selection_above(&AddSelectionAbove, cx); - assert_eq!( - view.selections.display_ranges(cx), - vec![ - DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3), - DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3) - ] - ); - }); - - view.update(cx, |view, cx| { - view.add_selection_above(&AddSelectionAbove, cx); - assert_eq!( - view.selections.display_ranges(cx), - vec![ - DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3), - DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3) - ] - ); - }); - - view.update(cx, |view, cx| { - view.add_selection_below(&AddSelectionBelow, cx); - assert_eq!( - view.selections.display_ranges(cx), - vec![DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3)] - ); - - view.undo_selection(&UndoSelection, cx); - assert_eq!( - view.selections.display_ranges(cx), - vec![ - DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3), - DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3) - ] - ); - - view.redo_selection(&RedoSelection, cx); - assert_eq!( - view.selections.display_ranges(cx), - vec![DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3)] - ); - }); - - view.update(cx, |view, cx| { - view.add_selection_below(&AddSelectionBelow, cx); - assert_eq!( - view.selections.display_ranges(cx), - vec![ - DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3), - DisplayPoint::new(4, 3)..DisplayPoint::new(4, 3) - ] - ); - }); - - view.update(cx, |view, cx| { - view.add_selection_below(&AddSelectionBelow, cx); - assert_eq!( - view.selections.display_ranges(cx), - vec![ - DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3), - DisplayPoint::new(4, 3)..DisplayPoint::new(4, 3) - ] - ); - }); - - view.update(cx, |view, cx| { - view.change_selections(None, cx, |s| { - s.select_display_ranges([DisplayPoint::new(1, 4)..DisplayPoint::new(1, 3)]) - }); - }); - view.update(cx, |view, cx| { - view.add_selection_below(&AddSelectionBelow, cx); - assert_eq!( - view.selections.display_ranges(cx), - vec![ - DisplayPoint::new(1, 4)..DisplayPoint::new(1, 3), - DisplayPoint::new(4, 4)..DisplayPoint::new(4, 3) - ] - ); - }); - - view.update(cx, |view, cx| { - view.add_selection_below(&AddSelectionBelow, cx); - assert_eq!( - view.selections.display_ranges(cx), - vec![ - DisplayPoint::new(1, 4)..DisplayPoint::new(1, 3), - DisplayPoint::new(4, 4)..DisplayPoint::new(4, 3) - ] - ); - }); - - view.update(cx, |view, cx| { - view.add_selection_above(&AddSelectionAbove, cx); - assert_eq!( - view.selections.display_ranges(cx), - vec![DisplayPoint::new(1, 4)..DisplayPoint::new(1, 3)] - ); - }); - - view.update(cx, |view, cx| { - view.add_selection_above(&AddSelectionAbove, cx); - assert_eq!( - view.selections.display_ranges(cx), - vec![DisplayPoint::new(1, 4)..DisplayPoint::new(1, 3)] - ); - }); - - view.update(cx, |view, cx| { - view.change_selections(None, cx, |s| { - s.select_display_ranges([DisplayPoint::new(0, 1)..DisplayPoint::new(1, 4)]) - }); - view.add_selection_below(&AddSelectionBelow, cx); - assert_eq!( - view.selections.display_ranges(cx), - vec![ - DisplayPoint::new(0, 1)..DisplayPoint::new(0, 3), - DisplayPoint::new(1, 1)..DisplayPoint::new(1, 4), - DisplayPoint::new(3, 1)..DisplayPoint::new(3, 2), - ] - ); - }); - - view.update(cx, |view, cx| { - view.add_selection_below(&AddSelectionBelow, cx); - assert_eq!( - view.selections.display_ranges(cx), - vec![ - DisplayPoint::new(0, 1)..DisplayPoint::new(0, 3), - DisplayPoint::new(1, 1)..DisplayPoint::new(1, 4), - DisplayPoint::new(3, 1)..DisplayPoint::new(3, 2), - DisplayPoint::new(4, 1)..DisplayPoint::new(4, 4), - ] - ); - }); - - view.update(cx, |view, cx| { - view.add_selection_above(&AddSelectionAbove, cx); - assert_eq!( - view.selections.display_ranges(cx), - vec![ - DisplayPoint::new(0, 1)..DisplayPoint::new(0, 3), - DisplayPoint::new(1, 1)..DisplayPoint::new(1, 4), - DisplayPoint::new(3, 1)..DisplayPoint::new(3, 2), - ] - ); - }); - - view.update(cx, |view, cx| { - view.change_selections(None, cx, |s| { - s.select_display_ranges([DisplayPoint::new(4, 3)..DisplayPoint::new(1, 1)]) - }); - }); - view.update(cx, |view, cx| { - view.add_selection_above(&AddSelectionAbove, cx); - assert_eq!( - view.selections.display_ranges(cx), - vec![ - DisplayPoint::new(0, 3)..DisplayPoint::new(0, 1), - DisplayPoint::new(1, 3)..DisplayPoint::new(1, 1), - DisplayPoint::new(3, 2)..DisplayPoint::new(3, 1), - DisplayPoint::new(4, 3)..DisplayPoint::new(4, 1), - ] - ); - }); - - view.update(cx, |view, cx| { - view.add_selection_below(&AddSelectionBelow, cx); - assert_eq!( - view.selections.display_ranges(cx), - vec![ - DisplayPoint::new(1, 3)..DisplayPoint::new(1, 1), - DisplayPoint::new(3, 2)..DisplayPoint::new(3, 1), - DisplayPoint::new(4, 3)..DisplayPoint::new(4, 1), - ] - ); - }); -} - -#[gpui::test] -async fn test_select_next(cx: &mut gpui::TestAppContext) { - init_test(cx, |_| {}); - - let mut cx = EditorTestContext::new(cx).await; - cx.set_state("abc\nˇabc abc\ndefabc\nabc"); - - cx.update_editor(|e, cx| e.select_next(&SelectNext::default(), cx)) - .unwrap(); - cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc"); - - cx.update_editor(|e, cx| e.select_next(&SelectNext::default(), cx)) - .unwrap(); - cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc"); - - cx.update_editor(|view, cx| view.undo_selection(&UndoSelection, cx)); - cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc"); - - cx.update_editor(|view, cx| view.redo_selection(&RedoSelection, cx)); - cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc"); - - cx.update_editor(|e, cx| e.select_next(&SelectNext::default(), cx)) - .unwrap(); - cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»"); - - cx.update_editor(|e, cx| e.select_next(&SelectNext::default(), cx)) - .unwrap(); - cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»"); -} - -#[gpui::test] -async fn test_select_previous(cx: &mut gpui::TestAppContext) { - init_test(cx, |_| {}); - { - // `Select previous` without a selection (selects wordwise) - let mut cx = EditorTestContext::new(cx).await; - cx.set_state("abc\nˇabc abc\ndefabc\nabc"); - - cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)) - .unwrap(); - cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc"); - - cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)) - .unwrap(); - cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc"); - - cx.update_editor(|view, cx| view.undo_selection(&UndoSelection, cx)); - cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc"); - - cx.update_editor(|view, cx| view.redo_selection(&RedoSelection, cx)); - cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc"); - - cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)) - .unwrap(); - cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»"); - - cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)) - .unwrap(); - cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»"); - } - { - // `Select previous` with a selection - let mut cx = EditorTestContext::new(cx).await; - cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc"); - - cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)) - .unwrap(); - cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\nabc"); - - cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)) - .unwrap(); - cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\n«abcˇ»"); - - cx.update_editor(|view, cx| view.undo_selection(&UndoSelection, cx)); - cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\nabc"); - - cx.update_editor(|view, cx| view.redo_selection(&RedoSelection, cx)); - cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\n«abcˇ»"); - - cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)) - .unwrap(); - cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndef«abcˇ»\n«abcˇ»"); - - cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)) - .unwrap(); - cx.assert_editor_state("«abcˇ»\n«ˇabc» «abcˇ»\ndef«abcˇ»\n«abcˇ»"); - } -} - -#[gpui::test] -async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) { - init_test(cx, |_| {}); - - let language = Arc::new(Language::new( - LanguageConfig::default(), - Some(tree_sitter_rust::language()), - )); - - let text = r#" - use mod1::mod2::{mod3, mod4}; - - fn fn_1(param1: bool, param2: &str) { - let var1 = "text"; - } - "# - .unindent(); - - let buffer = - cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, text).with_language(language, cx)); - let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); - let view = cx.add_window(|cx| build_editor(buffer, cx)).root(cx); - view.condition(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx)) - .await; - - view.update(cx, |view, cx| { - view.change_selections(None, cx, |s| { - s.select_display_ranges([ - DisplayPoint::new(0, 25)..DisplayPoint::new(0, 25), - DisplayPoint::new(2, 24)..DisplayPoint::new(2, 12), - DisplayPoint::new(3, 18)..DisplayPoint::new(3, 18), - ]); - }); - view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx); - }); - assert_eq!( - view.update(cx, |view, cx| { view.selections.display_ranges(cx) }), - &[ - DisplayPoint::new(0, 23)..DisplayPoint::new(0, 27), - DisplayPoint::new(2, 35)..DisplayPoint::new(2, 7), - DisplayPoint::new(3, 15)..DisplayPoint::new(3, 21), - ] - ); - - view.update(cx, |view, cx| { - view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx); - }); - assert_eq!( - view.update(cx, |view, cx| view.selections.display_ranges(cx)), - &[ - DisplayPoint::new(0, 16)..DisplayPoint::new(0, 28), - DisplayPoint::new(4, 1)..DisplayPoint::new(2, 0), - ] - ); - - view.update(cx, |view, cx| { - view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx); - }); - assert_eq!( - view.update(cx, |view, cx| view.selections.display_ranges(cx)), - &[DisplayPoint::new(5, 0)..DisplayPoint::new(0, 0)] - ); - - // Trying to expand the selected syntax node one more time has no effect. - view.update(cx, |view, cx| { - view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx); - }); - assert_eq!( - view.update(cx, |view, cx| view.selections.display_ranges(cx)), - &[DisplayPoint::new(5, 0)..DisplayPoint::new(0, 0)] - ); - - view.update(cx, |view, cx| { - view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx); - }); - assert_eq!( - view.update(cx, |view, cx| view.selections.display_ranges(cx)), - &[ - DisplayPoint::new(0, 16)..DisplayPoint::new(0, 28), - DisplayPoint::new(4, 1)..DisplayPoint::new(2, 0), - ] - ); - - view.update(cx, |view, cx| { - view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx); - }); - assert_eq!( - view.update(cx, |view, cx| view.selections.display_ranges(cx)), - &[ - DisplayPoint::new(0, 23)..DisplayPoint::new(0, 27), - DisplayPoint::new(2, 35)..DisplayPoint::new(2, 7), - DisplayPoint::new(3, 15)..DisplayPoint::new(3, 21), - ] - ); - - view.update(cx, |view, cx| { - view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx); - }); - assert_eq!( - view.update(cx, |view, cx| view.selections.display_ranges(cx)), - &[ - DisplayPoint::new(0, 25)..DisplayPoint::new(0, 25), - DisplayPoint::new(2, 24)..DisplayPoint::new(2, 12), - DisplayPoint::new(3, 18)..DisplayPoint::new(3, 18), - ] - ); - - // Trying to shrink the selected syntax node one more time has no effect. - view.update(cx, |view, cx| { - view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx); - }); - assert_eq!( - view.update(cx, |view, cx| view.selections.display_ranges(cx)), - &[ - DisplayPoint::new(0, 25)..DisplayPoint::new(0, 25), - DisplayPoint::new(2, 24)..DisplayPoint::new(2, 12), - DisplayPoint::new(3, 18)..DisplayPoint::new(3, 18), - ] - ); - - // Ensure that we keep expanding the selection if the larger selection starts or ends within - // a fold. - view.update(cx, |view, cx| { - view.fold_ranges( - vec![ - Point::new(0, 21)..Point::new(0, 24), - Point::new(3, 20)..Point::new(3, 22), - ], - true, - cx, - ); - view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx); - }); - assert_eq!( - view.update(cx, |view, cx| view.selections.display_ranges(cx)), - &[ - DisplayPoint::new(0, 16)..DisplayPoint::new(0, 28), - DisplayPoint::new(2, 35)..DisplayPoint::new(2, 7), - DisplayPoint::new(3, 4)..DisplayPoint::new(3, 23), - ] - ); -} - -#[gpui::test] -async fn test_autoindent_selections(cx: &mut gpui::TestAppContext) { - init_test(cx, |_| {}); - - let language = Arc::new( - Language::new( - LanguageConfig { - 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_indents_query( - r#" - (_ "(" ")" @end) @indent - (_ "{" "}" @end) @indent - "#, - ) - .unwrap(), - ); - - let text = "fn a() {}"; - - let buffer = - cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, text).with_language(language, cx)); - let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); - let editor = cx.add_window(|cx| build_editor(buffer, cx)).root(cx); - editor - .condition(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx)) - .await; - - editor.update(cx, |editor, cx| { - editor.change_selections(None, cx, |s| s.select_ranges([5..5, 8..8, 9..9])); - editor.newline(&Newline, cx); - assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n"); - assert_eq!( - editor.selections.ranges(cx), - &[ - Point::new(1, 4)..Point::new(1, 4), - Point::new(3, 4)..Point::new(3, 4), - Point::new(5, 0)..Point::new(5, 0) - ] - ); - }); -} - -#[gpui::test] -async fn test_autoclose_pairs(cx: &mut gpui::TestAppContext) { - init_test(cx, |_| {}); - - let mut cx = EditorTestContext::new(cx).await; - - let language = Arc::new(Language::new( - LanguageConfig { - brackets: BracketPairConfig { - pairs: vec![ - BracketPair { - start: "{".to_string(), - end: "}".to_string(), - close: true, - newline: true, - }, - BracketPair { - start: "(".to_string(), - end: ")".to_string(), - close: true, - newline: true, - }, - BracketPair { - start: "/*".to_string(), - end: " */".to_string(), - close: true, - newline: true, - }, - BracketPair { - start: "[".to_string(), - end: "]".to_string(), - close: false, - newline: true, - }, - BracketPair { - start: "\"".to_string(), - end: "\"".to_string(), - close: true, - newline: false, - }, - ], - ..Default::default() - }, - autoclose_before: "})]".to_string(), - ..Default::default() - }, - Some(tree_sitter_rust::language()), - )); - - let registry = Arc::new(LanguageRegistry::test()); - registry.add(language.clone()); - cx.update_buffer(|buffer, cx| { - buffer.set_language_registry(registry); - buffer.set_language(Some(language), cx); - }); - - cx.set_state( - &r#" - 🏀ˇ - εˇ - ❤️ˇ - "# - .unindent(), - ); - - // autoclose multiple nested brackets at multiple cursors - cx.update_editor(|view, cx| { - view.handle_input("{", cx); - view.handle_input("{", cx); - view.handle_input("{", cx); - }); - cx.assert_editor_state( - &" - 🏀{{{ˇ}}} - ε{{{ˇ}}} - ❤️{{{ˇ}}} - " - .unindent(), - ); - - // insert a different closing bracket - cx.update_editor(|view, cx| { - view.handle_input(")", cx); - }); - cx.assert_editor_state( - &" - 🏀{{{)ˇ}}} - ε{{{)ˇ}}} - ❤️{{{)ˇ}}} - " - .unindent(), - ); - - // skip over the auto-closed brackets when typing a closing bracket - cx.update_editor(|view, cx| { - view.move_right(&MoveRight, cx); - view.handle_input("}", cx); - view.handle_input("}", cx); - view.handle_input("}", cx); - }); - cx.assert_editor_state( - &" - 🏀{{{)}}}}ˇ - ε{{{)}}}}ˇ - ❤️{{{)}}}}ˇ - " - .unindent(), - ); - - // autoclose multi-character pairs - cx.set_state( - &" - ˇ - ˇ - " - .unindent(), - ); - cx.update_editor(|view, cx| { - view.handle_input("/", cx); - view.handle_input("*", cx); - }); - cx.assert_editor_state( - &" - /*ˇ */ - /*ˇ */ - " - .unindent(), - ); - - // one cursor autocloses a multi-character pair, one cursor - // does not autoclose. - cx.set_state( - &" - /ˇ - ˇ - " - .unindent(), - ); - cx.update_editor(|view, cx| view.handle_input("*", cx)); - cx.assert_editor_state( - &" - /*ˇ */ - *ˇ - " - .unindent(), - ); - - // Don't autoclose if the next character isn't whitespace and isn't - // listed in the language's "autoclose_before" section. - cx.set_state("ˇa b"); - cx.update_editor(|view, cx| view.handle_input("{", cx)); - cx.assert_editor_state("{ˇa b"); - - // Don't autoclose if `close` is false for the bracket pair - cx.set_state("ˇ"); - cx.update_editor(|view, cx| view.handle_input("[", cx)); - cx.assert_editor_state("[ˇ"); - - // Surround with brackets if text is selected - cx.set_state("«aˇ» b"); - cx.update_editor(|view, cx| view.handle_input("{", cx)); - cx.assert_editor_state("{«aˇ»} b"); - - // Autclose pair where the start and end characters are the same - cx.set_state("aˇ"); - cx.update_editor(|view, cx| view.handle_input("\"", cx)); - cx.assert_editor_state("a\"ˇ\""); - cx.update_editor(|view, cx| view.handle_input("\"", cx)); - cx.assert_editor_state("a\"\"ˇ"); -} - -#[gpui::test] -async fn test_autoclose_with_embedded_language(cx: &mut gpui::TestAppContext) { - init_test(cx, |_| {}); - - let mut cx = EditorTestContext::new(cx).await; - - let html_language = Arc::new( - Language::new( - LanguageConfig { - name: "HTML".into(), - brackets: BracketPairConfig { - pairs: vec![ - BracketPair { - start: "<".into(), - end: ">".into(), - close: true, - ..Default::default() - }, - BracketPair { - start: "{".into(), - end: "}".into(), - close: true, - ..Default::default() - }, - BracketPair { - start: "(".into(), - end: ")".into(), - close: true, - ..Default::default() - }, - ], - ..Default::default() - }, - autoclose_before: "})]>".into(), - ..Default::default() - }, - Some(tree_sitter_html::language()), - ) - .with_injection_query( - r#" - (script_element - (raw_text) @content - (#set! "language" "javascript")) - "#, - ) - .unwrap(), - ); - - let javascript_language = Arc::new(Language::new( - LanguageConfig { - name: "JavaScript".into(), - brackets: BracketPairConfig { - pairs: vec![ - BracketPair { - start: "/*".into(), - end: " */".into(), - close: true, - ..Default::default() - }, - BracketPair { - start: "{".into(), - end: "}".into(), - close: true, - ..Default::default() - }, - BracketPair { - start: "(".into(), - end: ")".into(), - close: true, - ..Default::default() - }, - ], - ..Default::default() - }, - autoclose_before: "})]>".into(), - ..Default::default() - }, - Some(tree_sitter_typescript::language_tsx()), - )); - - let registry = Arc::new(LanguageRegistry::test()); - registry.add(html_language.clone()); - registry.add(javascript_language.clone()); - - cx.update_buffer(|buffer, cx| { - buffer.set_language_registry(registry); - buffer.set_language(Some(html_language), cx); - }); - - cx.set_state( - &r#" - ˇ - - ˇ - "# - .unindent(), - ); - - // Precondition: different languages are active at different locations. - cx.update_editor(|editor, cx| { - let snapshot = editor.snapshot(cx); - let cursors = editor.selections.ranges::(cx); - let languages = cursors - .iter() - .map(|c| snapshot.language_at(c.start).unwrap().name()) - .collect::>(); - assert_eq!( - languages, - &["HTML".into(), "JavaScript".into(), "HTML".into()] - ); - }); - - // Angle brackets autoclose in HTML, but not JavaScript. - cx.update_editor(|editor, cx| { - editor.handle_input("<", cx); - editor.handle_input("a", cx); - }); - cx.assert_editor_state( - &r#" - - - - "# - .unindent(), - ); - - // Curly braces and parens autoclose in both HTML and JavaScript. - cx.update_editor(|editor, cx| { - editor.handle_input(" b=", cx); - editor.handle_input("{", cx); - editor.handle_input("c", cx); - editor.handle_input("(", cx); - }); - cx.assert_editor_state( - &r#" - - - - "# - .unindent(), - ); - - // Brackets that were already autoclosed are skipped. - cx.update_editor(|editor, cx| { - editor.handle_input(")", cx); - editor.handle_input("d", cx); - editor.handle_input("}", cx); - }); - cx.assert_editor_state( - &r#" - - - - "# - .unindent(), - ); - cx.update_editor(|editor, cx| { - editor.handle_input(">", cx); - }); - cx.assert_editor_state( - &r#" - ˇ - - ˇ - "# - .unindent(), - ); - - // Reset - cx.set_state( - &r#" - ˇ - - ˇ - "# - .unindent(), - ); - - cx.update_editor(|editor, cx| { - editor.handle_input("<", cx); - }); - cx.assert_editor_state( - &r#" - <ˇ> - - <ˇ> - "# - .unindent(), - ); - - // When backspacing, the closing angle brackets are removed. - cx.update_editor(|editor, cx| { - editor.backspace(&Backspace, cx); - }); - cx.assert_editor_state( - &r#" - ˇ - - ˇ - "# - .unindent(), - ); - - // Block comments autoclose in JavaScript, but not HTML. - cx.update_editor(|editor, cx| { - editor.handle_input("/", cx); - editor.handle_input("*", cx); - }); - cx.assert_editor_state( - &r#" - /*ˇ - - /*ˇ - "# - .unindent(), - ); -} - -#[gpui::test] -async fn test_autoclose_with_overrides(cx: &mut gpui::TestAppContext) { - init_test(cx, |_| {}); - - let mut cx = EditorTestContext::new(cx).await; - - let rust_language = Arc::new( - Language::new( - LanguageConfig { - name: "Rust".into(), - brackets: serde_json::from_value(json!([ - { "start": "{", "end": "}", "close": true, "newline": true }, - { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] }, - ])) - .unwrap(), - autoclose_before: "})]>".into(), - ..Default::default() - }, - Some(tree_sitter_rust::language()), - ) - .with_override_query("(string_literal) @string") - .unwrap(), - ); - - let registry = Arc::new(LanguageRegistry::test()); - registry.add(rust_language.clone()); - - cx.update_buffer(|buffer, cx| { - buffer.set_language_registry(registry); - buffer.set_language(Some(rust_language), cx); - }); - - cx.set_state( - &r#" - let x = ˇ - "# - .unindent(), - ); - - // Inserting a quotation mark. A closing quotation mark is automatically inserted. - cx.update_editor(|editor, cx| { - editor.handle_input("\"", cx); - }); - cx.assert_editor_state( - &r#" - let x = "ˇ" - "# - .unindent(), - ); - - // Inserting another quotation mark. The cursor moves across the existing - // automatically-inserted quotation mark. - cx.update_editor(|editor, cx| { - editor.handle_input("\"", cx); - }); - cx.assert_editor_state( - &r#" - let x = ""ˇ - "# - .unindent(), - ); - - // Reset - cx.set_state( - &r#" - let x = ˇ - "# - .unindent(), - ); - - // Inserting a quotation mark inside of a string. A second quotation mark is not inserted. - cx.update_editor(|editor, cx| { - editor.handle_input("\"", cx); - editor.handle_input(" ", cx); - editor.move_left(&Default::default(), cx); - editor.handle_input("\\", cx); - editor.handle_input("\"", cx); - }); - cx.assert_editor_state( - &r#" - let x = "\"ˇ " - "# - .unindent(), - ); - - // Inserting a closing quotation mark at the position of an automatically-inserted quotation - // mark. Nothing is inserted. - cx.update_editor(|editor, cx| { - editor.move_right(&Default::default(), cx); - editor.handle_input("\"", cx); - }); - cx.assert_editor_state( - &r#" - let x = "\" "ˇ - "# - .unindent(), - ); -} - -#[gpui::test] -async fn test_surround_with_pair(cx: &mut gpui::TestAppContext) { - init_test(cx, |_| {}); - - let language = Arc::new(Language::new( - LanguageConfig { - brackets: BracketPairConfig { - pairs: vec![ - BracketPair { - start: "{".to_string(), - end: "}".to_string(), - close: true, - newline: true, - }, - BracketPair { - start: "/* ".to_string(), - end: "*/".to_string(), - close: true, - ..Default::default() - }, - ], - ..Default::default() - }, - ..Default::default() - }, - Some(tree_sitter_rust::language()), - )); - - let text = r#" - a - b - c - "# - .unindent(); - - let buffer = - cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, text).with_language(language, cx)); - let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); - let view = cx.add_window(|cx| build_editor(buffer, cx)).root(cx); - view.condition(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx)) - .await; - - view.update(cx, |view, cx| { - view.change_selections(None, cx, |s| { - s.select_display_ranges([ - DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1), - DisplayPoint::new(1, 0)..DisplayPoint::new(1, 1), - DisplayPoint::new(2, 0)..DisplayPoint::new(2, 1), - ]) - }); - - view.handle_input("{", cx); - view.handle_input("{", cx); - view.handle_input("{", cx); - assert_eq!( - view.text(cx), - " - {{{a}}} - {{{b}}} - {{{c}}} - " - .unindent() - ); - assert_eq!( - view.selections.display_ranges(cx), - [ - DisplayPoint::new(0, 3)..DisplayPoint::new(0, 4), - DisplayPoint::new(1, 3)..DisplayPoint::new(1, 4), - DisplayPoint::new(2, 3)..DisplayPoint::new(2, 4) - ] - ); - - view.undo(&Undo, cx); - view.undo(&Undo, cx); - view.undo(&Undo, cx); - assert_eq!( - view.text(cx), - " - a - b - c - " - .unindent() - ); - assert_eq!( - view.selections.display_ranges(cx), - [ - DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1), - DisplayPoint::new(1, 0)..DisplayPoint::new(1, 1), - DisplayPoint::new(2, 0)..DisplayPoint::new(2, 1) - ] - ); - - // Ensure inserting the first character of a multi-byte bracket pair - // doesn't surround the selections with the bracket. - view.handle_input("/", cx); - assert_eq!( - view.text(cx), - " - / - / - / - " - .unindent() - ); - assert_eq!( - view.selections.display_ranges(cx), - [ - DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1), - DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1), - DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1) - ] - ); - - view.undo(&Undo, cx); - assert_eq!( - view.text(cx), - " - a - b - c - " - .unindent() - ); - assert_eq!( - view.selections.display_ranges(cx), - [ - DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1), - DisplayPoint::new(1, 0)..DisplayPoint::new(1, 1), - DisplayPoint::new(2, 0)..DisplayPoint::new(2, 1) - ] - ); - - // Ensure inserting the last character of a multi-byte bracket pair - // doesn't surround the selections with the bracket. - view.handle_input("*", cx); - assert_eq!( - view.text(cx), - " - * - * - * - " - .unindent() - ); - assert_eq!( - view.selections.display_ranges(cx), - [ - DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1), - DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1), - DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1) - ] - ); - }); -} - -#[gpui::test] -async fn test_delete_autoclose_pair(cx: &mut gpui::TestAppContext) { - init_test(cx, |_| {}); - - let language = Arc::new(Language::new( - LanguageConfig { - brackets: BracketPairConfig { - pairs: vec![BracketPair { - start: "{".to_string(), - end: "}".to_string(), - close: true, - newline: true, - }], - ..Default::default() - }, - autoclose_before: "}".to_string(), - ..Default::default() - }, - Some(tree_sitter_rust::language()), - )); - - let text = r#" - a - b - c - "# - .unindent(); - - let buffer = - cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, text).with_language(language, cx)); - let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); - let editor = cx.add_window(|cx| build_editor(buffer, cx)).root(cx); - editor - .condition(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx)) - .await; - - editor.update(cx, |editor, cx| { - editor.change_selections(None, cx, |s| { - s.select_ranges([ - Point::new(0, 1)..Point::new(0, 1), - Point::new(1, 1)..Point::new(1, 1), - Point::new(2, 1)..Point::new(2, 1), - ]) - }); - - editor.handle_input("{", cx); - editor.handle_input("{", cx); - editor.handle_input("_", cx); - assert_eq!( - editor.text(cx), - " - a{{_}} - b{{_}} - c{{_}} - " - .unindent() - ); - assert_eq!( - editor.selections.ranges::(cx), - [ - Point::new(0, 4)..Point::new(0, 4), - Point::new(1, 4)..Point::new(1, 4), - Point::new(2, 4)..Point::new(2, 4) - ] - ); - - editor.backspace(&Default::default(), cx); - editor.backspace(&Default::default(), cx); - assert_eq!( - editor.text(cx), - " - a{} - b{} - c{} - " - .unindent() - ); - assert_eq!( - editor.selections.ranges::(cx), - [ - Point::new(0, 2)..Point::new(0, 2), - Point::new(1, 2)..Point::new(1, 2), - Point::new(2, 2)..Point::new(2, 2) - ] - ); - - editor.delete_to_previous_word_start(&Default::default(), cx); - assert_eq!( - editor.text(cx), - " - a - b - c - " - .unindent() - ); - assert_eq!( - editor.selections.ranges::(cx), - [ - Point::new(0, 1)..Point::new(0, 1), - Point::new(1, 1)..Point::new(1, 1), - Point::new(2, 1)..Point::new(2, 1) - ] - ); - }); -} - -#[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 buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx)); - let editor = cx.add_window(|cx| build_editor(buffer, cx)).root(cx); - - 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(); - - 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 - "}, - ); - - // 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 - "}, - ); - - 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 - "}, - ); - - // 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) { - init_test(cx, |_| {}); - - 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 { - document_formatting_provider: Some(lsp::OneOf::Left(true)), - ..Default::default() - }, - ..Default::default() - })) - .await; - - let fs = FakeFs::new(cx.background()); - fs.insert_file("/file.rs", Default::default()).await; - - let project = Project::test(fs, ["/file.rs".as_ref()], cx).await; - project.update(cx, |project, _| project.languages().add(Arc::new(language))); - let buffer = project - .update(cx, |project, cx| project.open_local_buffer("/file.rs", cx)) - .await - .unwrap(); - - cx.foreground().start_waiting(); - let fake_server = fake_servers.next().await.unwrap(); - - let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); - let editor = cx.add_window(|cx| build_editor(buffer, cx)).root(cx); - editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx)); - assert!(cx.read(|cx| editor.is_dirty(cx))); - - let save = editor.update(cx, |editor, cx| editor.save(project.clone(), cx)); - fake_server - .handle_request::(move |params, _| async move { - assert_eq!( - params.text_document.uri, - lsp::Url::from_file_path("/file.rs").unwrap() - ); - assert_eq!(params.options.tab_size, 4); - Ok(Some(vec![lsp::TextEdit::new( - lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)), - ", ".to_string(), - )])) - }) - .next() - .await; - cx.foreground().start_waiting(); - save.await.unwrap(); - assert_eq!( - editor.read_with(cx, |editor, cx| editor.text(cx)), - "one, two\nthree\n" - ); - assert!(!cx.read(|cx| editor.is_dirty(cx))); - - editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx)); - assert!(cx.read(|cx| editor.is_dirty(cx))); - - // Ensure we can still save even if formatting hangs. - fake_server.handle_request::(move |params, _| async move { - assert_eq!( - params.text_document.uri, - lsp::Url::from_file_path("/file.rs").unwrap() - ); - futures::future::pending::<()>().await; - unreachable!() - }); - let save = editor.update(cx, |editor, cx| editor.save(project.clone(), cx)); - cx.foreground().advance_clock(super::FORMAT_TIMEOUT); - cx.foreground().start_waiting(); - save.await.unwrap(); - assert_eq!( - editor.read_with(cx, |editor, cx| editor.text(cx)), - "one\ntwo\nthree\n" - ); - assert!(!cx.read(|cx| editor.is_dirty(cx))); - - // Set rust language override and assert overridden tabsize is sent to language server - update_test_language_settings(cx, |settings| { - settings.languages.insert( - "Rust".into(), - LanguageSettingsContent { - tab_size: NonZeroU32::new(8), - ..Default::default() - }, - ); - }); - - let save = editor.update(cx, |editor, cx| editor.save(project.clone(), cx)); - fake_server - .handle_request::(move |params, _| async move { - assert_eq!( - params.text_document.uri, - lsp::Url::from_file_path("/file.rs").unwrap() - ); - assert_eq!(params.options.tab_size, 8); - Ok(Some(vec![])) - }) - .next() - .await; - cx.foreground().start_waiting(); - save.await.unwrap(); -} - -#[gpui::test] -async fn test_range_format_during_save(cx: &mut gpui::TestAppContext) { - init_test(cx, |_| {}); - - 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 { - document_range_formatting_provider: Some(lsp::OneOf::Left(true)), - ..Default::default() - }, - ..Default::default() - })) - .await; - - let fs = FakeFs::new(cx.background()); - fs.insert_file("/file.rs", Default::default()).await; - - let project = Project::test(fs, ["/file.rs".as_ref()], cx).await; - project.update(cx, |project, _| project.languages().add(Arc::new(language))); - let buffer = project - .update(cx, |project, cx| project.open_local_buffer("/file.rs", cx)) - .await - .unwrap(); - - cx.foreground().start_waiting(); - let fake_server = fake_servers.next().await.unwrap(); - - let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); - let editor = cx.add_window(|cx| build_editor(buffer, cx)).root(cx); - editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx)); - assert!(cx.read(|cx| editor.is_dirty(cx))); - - let save = editor.update(cx, |editor, cx| editor.save(project.clone(), cx)); - fake_server - .handle_request::(move |params, _| async move { - assert_eq!( - params.text_document.uri, - lsp::Url::from_file_path("/file.rs").unwrap() - ); - assert_eq!(params.options.tab_size, 4); - Ok(Some(vec![lsp::TextEdit::new( - lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)), - ", ".to_string(), - )])) - }) - .next() - .await; - cx.foreground().start_waiting(); - save.await.unwrap(); - assert_eq!( - editor.read_with(cx, |editor, cx| editor.text(cx)), - "one, two\nthree\n" - ); - assert!(!cx.read(|cx| editor.is_dirty(cx))); - - editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx)); - assert!(cx.read(|cx| editor.is_dirty(cx))); - - // Ensure we can still save even if formatting hangs. - fake_server.handle_request::( - move |params, _| async move { - assert_eq!( - params.text_document.uri, - lsp::Url::from_file_path("/file.rs").unwrap() - ); - futures::future::pending::<()>().await; - unreachable!() - }, - ); - let save = editor.update(cx, |editor, cx| editor.save(project.clone(), cx)); - cx.foreground().advance_clock(super::FORMAT_TIMEOUT); - cx.foreground().start_waiting(); - save.await.unwrap(); - assert_eq!( - editor.read_with(cx, |editor, cx| editor.text(cx)), - "one\ntwo\nthree\n" - ); - assert!(!cx.read(|cx| editor.is_dirty(cx))); - - // Set rust language override and assert overridden tabsize is sent to language server - update_test_language_settings(cx, |settings| { - settings.languages.insert( - "Rust".into(), - LanguageSettingsContent { - tab_size: NonZeroU32::new(8), - ..Default::default() - }, - ); - }); - - let save = editor.update(cx, |editor, cx| editor.save(project.clone(), cx)); - fake_server - .handle_request::(move |params, _| async move { - assert_eq!( - params.text_document.uri, - lsp::Url::from_file_path("/file.rs").unwrap() - ); - assert_eq!(params.options.tab_size, 8); - Ok(Some(vec![])) - }) - .next() - .await; - cx.foreground().start_waiting(); - save.await.unwrap(); -} - -#[gpui::test] -async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) { - init_test(cx, |settings| { - settings.defaults.formatter = Some(language_settings::Formatter::LanguageServer) - }); - - let mut language = Language::new( - LanguageConfig { - name: "Rust".into(), - path_suffixes: vec!["rs".to_string()], - // Enable Prettier formatting for the same buffer, and ensure - // LSP is called instead of Prettier. - prettier_parser_name: Some("test_parser".to_string()), - ..Default::default() - }, - Some(tree_sitter_rust::language()), - ); - let mut fake_servers = language - .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { - capabilities: lsp::ServerCapabilities { - document_formatting_provider: Some(lsp::OneOf::Left(true)), - ..Default::default() - }, - ..Default::default() - })) - .await; - - let fs = FakeFs::new(cx.background()); - fs.insert_file("/file.rs", Default::default()).await; - - let project = Project::test(fs, ["/file.rs".as_ref()], cx).await; - project.update(cx, |project, _| { - project.languages().add(Arc::new(language)); - }); - let buffer = project - .update(cx, |project, cx| project.open_local_buffer("/file.rs", cx)) - .await - .unwrap(); - - cx.foreground().start_waiting(); - let fake_server = fake_servers.next().await.unwrap(); - - let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); - let editor = cx.add_window(|cx| build_editor(buffer, cx)).root(cx); - editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx)); - - let format = editor.update(cx, |editor, cx| { - editor.perform_format(project.clone(), FormatTrigger::Manual, cx) - }); - fake_server - .handle_request::(move |params, _| async move { - assert_eq!( - params.text_document.uri, - lsp::Url::from_file_path("/file.rs").unwrap() - ); - assert_eq!(params.options.tab_size, 4); - Ok(Some(vec![lsp::TextEdit::new( - lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)), - ", ".to_string(), - )])) - }) - .next() - .await; - cx.foreground().start_waiting(); - format.await.unwrap(); - assert_eq!( - editor.read_with(cx, |editor, cx| editor.text(cx)), - "one, two\nthree\n" - ); - - editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx)); - // Ensure we don't lock if formatting hangs. - fake_server.handle_request::(move |params, _| async move { - assert_eq!( - params.text_document.uri, - lsp::Url::from_file_path("/file.rs").unwrap() - ); - futures::future::pending::<()>().await; - unreachable!() - }); - let format = editor.update(cx, |editor, cx| { - editor.perform_format(project, FormatTrigger::Manual, cx) - }); - cx.foreground().advance_clock(super::FORMAT_TIMEOUT); - cx.foreground().start_waiting(); - format.await.unwrap(); - assert_eq!( - editor.read_with(cx, |editor, cx| editor.text(cx)), - "one\ntwo\nthree\n" - ); -} - -#[gpui::test] -async fn test_concurrent_format_requests(cx: &mut gpui::TestAppContext) { - init_test(cx, |_| {}); - - let mut cx = EditorLspTestContext::new_rust( - lsp::ServerCapabilities { - document_formatting_provider: Some(lsp::OneOf::Left(true)), - ..Default::default() - }, - cx, - ) - .await; - - cx.set_state(indoc! {" - one.twoˇ - "}); - - // The format request takes a long time. When it completes, it inserts - // a newline and an indent before the `.` - cx.lsp - .handle_request::(move |_, cx| { - let executor = cx.background(); - async move { - executor.timer(Duration::from_millis(100)).await; - Ok(Some(vec![lsp::TextEdit { - range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)), - new_text: "\n ".into(), - }])) - } - }); - - // Submit a format request. - let format_1 = cx - .update_editor(|editor, cx| editor.format(&Format, cx)) - .unwrap(); - cx.foreground().run_until_parked(); - - // Submit a second format request. - let format_2 = cx - .update_editor(|editor, cx| editor.format(&Format, cx)) - .unwrap(); - cx.foreground().run_until_parked(); - - // Wait for both format requests to complete - cx.foreground().advance_clock(Duration::from_millis(200)); - cx.foreground().start_waiting(); - format_1.await.unwrap(); - cx.foreground().start_waiting(); - format_2.await.unwrap(); - - // The formatting edits only happens once. - cx.assert_editor_state(indoc! {" - one - .twoˇ - "}); -} - -#[gpui::test] -async fn test_strip_whitespace_and_format_via_lsp(cx: &mut gpui::TestAppContext) { - init_test(cx, |settings| { - settings.defaults.formatter = Some(language_settings::Formatter::Auto) - }); - - let mut cx = EditorLspTestContext::new_rust( - lsp::ServerCapabilities { - document_formatting_provider: Some(lsp::OneOf::Left(true)), - ..Default::default() - }, - cx, - ) - .await; - - // Set up a buffer white some trailing whitespace and no trailing newline. - cx.set_state( - &[ - "one ", // - "twoˇ", // - "three ", // - "four", // - ] - .join("\n"), - ); - - // Submit a format request. - let format = cx - .update_editor(|editor, cx| editor.format(&Format, cx)) - .unwrap(); - - // Record which buffer changes have been sent to the language server - let buffer_changes = Arc::new(Mutex::new(Vec::new())); - cx.lsp - .handle_notification::({ - let buffer_changes = buffer_changes.clone(); - move |params, _| { - buffer_changes.lock().extend( - params - .content_changes - .into_iter() - .map(|e| (e.range.unwrap(), e.text)), - ); - } - }); - - // Handle formatting requests to the language server. - cx.lsp.handle_request::({ - let buffer_changes = buffer_changes.clone(); - move |_, _| { - // When formatting is requested, trailing whitespace has already been stripped, - // and the trailing newline has already been added. - assert_eq!( - &buffer_changes.lock()[1..], - &[ - ( - lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)), - "".into() - ), - ( - lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)), - "".into() - ), - ( - lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)), - "\n".into() - ), - ] - ); - - // Insert blank lines between each line of the buffer. - async move { - Ok(Some(vec![ - lsp::TextEdit { - range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 0)), - new_text: "\n".into(), - }, - lsp::TextEdit { - range: lsp::Range::new(lsp::Position::new(2, 0), lsp::Position::new(2, 0)), - new_text: "\n".into(), - }, - ])) - } - } - }); - - // After formatting the buffer, the trailing whitespace is stripped, - // a newline is appended, and the edits provided by the language server - // have been applied. - format.await.unwrap(); - cx.assert_editor_state( - &[ - "one", // - "", // - "twoˇ", // - "", // - "three", // - "four", // - "", // - ] - .join("\n"), - ); - - // Undoing the formatting undoes the trailing whitespace removal, the - // trailing newline, and the LSP edits. - cx.update_buffer(|buffer, cx| buffer.undo(cx)); - cx.assert_editor_state( - &[ - "one ", // - "twoˇ", // - "three ", // - "four", // - ] - .join("\n"), - ); -} - -#[gpui::test] -async fn test_completion(cx: &mut gpui::TestAppContext) { - init_test(cx, |_| {}); - - let mut cx = EditorLspTestContext::new_rust( - lsp::ServerCapabilities { - completion_provider: Some(lsp::CompletionOptions { - trigger_characters: Some(vec![".".to_string(), ":".to_string()]), - resolve_provider: Some(true), - ..Default::default() - }), - ..Default::default() - }, - cx, - ) - .await; - - cx.set_state(indoc! {" - oneˇ - two - three - "}); - cx.simulate_keystroke("."); - handle_completion_request( - &mut cx, - indoc! {" - one.|<> - two - three - "}, - vec!["first_completion", "second_completion"], - ) - .await; - cx.condition(|editor, _| editor.context_menu_visible()) - .await; - let apply_additional_edits = cx.update_editor(|editor, cx| { - editor.context_menu_next(&Default::default(), cx); - editor - .confirm_completion(&ConfirmCompletion::default(), cx) - .unwrap() - }); - cx.assert_editor_state(indoc! {" - one.second_completionˇ - two - three - "}); - - handle_resolve_completion_request( - &mut cx, - Some(vec![ - ( - //This overlaps with the primary completion edit which is - //misbehavior from the LSP spec, test that we filter it out - indoc! {" - one.second_ˇcompletion - two - threeˇ - "}, - "overlapping additional edit", - ), - ( - indoc! {" - one.second_completion - two - threeˇ - "}, - "\nadditional edit", - ), - ]), - ) - .await; - apply_additional_edits.await.unwrap(); - cx.assert_editor_state(indoc! {" - one.second_completionˇ - two - three - additional edit - "}); - - cx.set_state(indoc! {" - one.second_completion - twoˇ - threeˇ - additional edit - "}); - cx.simulate_keystroke(" "); - assert!(cx.editor(|e, _| e.context_menu.read().is_none())); - cx.simulate_keystroke("s"); - assert!(cx.editor(|e, _| e.context_menu.read().is_none())); - - cx.assert_editor_state(indoc! {" - one.second_completion - two sˇ - three sˇ - additional edit - "}); - handle_completion_request( - &mut cx, - indoc! {" - one.second_completion - two s - three - additional edit - "}, - vec!["fourth_completion", "fifth_completion", "sixth_completion"], - ) - .await; - cx.condition(|editor, _| editor.context_menu_visible()) - .await; - - cx.simulate_keystroke("i"); - - handle_completion_request( - &mut cx, - indoc! {" - one.second_completion - two si - three - additional edit - "}, - vec!["fourth_completion", "fifth_completion", "sixth_completion"], - ) - .await; - cx.condition(|editor, _| editor.context_menu_visible()) - .await; - - let apply_additional_edits = cx.update_editor(|editor, cx| { - editor - .confirm_completion(&ConfirmCompletion::default(), cx) - .unwrap() - }); - cx.assert_editor_state(indoc! {" - one.second_completion - two sixth_completionˇ - three sixth_completionˇ - additional edit - "}); - - handle_resolve_completion_request(&mut cx, None).await; - apply_additional_edits.await.unwrap(); - - cx.update(|cx| { - cx.update_global::(|settings, cx| { - settings.update_user_settings::(cx, |settings| { - settings.show_completions_on_input = Some(false); - }); - }) - }); - cx.set_state("editorˇ"); - cx.simulate_keystroke("."); - assert!(cx.editor(|e, _| e.context_menu.read().is_none())); - cx.simulate_keystroke("c"); - cx.simulate_keystroke("l"); - cx.simulate_keystroke("o"); - cx.assert_editor_state("editor.cloˇ"); - assert!(cx.editor(|e, _| e.context_menu.read().is_none())); - cx.update_editor(|editor, cx| { - editor.show_completions(&ShowCompletions, cx); - }); - handle_completion_request(&mut cx, "editor.", vec!["close", "clobber"]).await; - cx.condition(|editor, _| editor.context_menu_visible()) - .await; - let apply_additional_edits = cx.update_editor(|editor, cx| { - editor - .confirm_completion(&ConfirmCompletion::default(), cx) - .unwrap() - }); - cx.assert_editor_state("editor.closeˇ"); - handle_resolve_completion_request(&mut cx, None).await; - apply_additional_edits.await.unwrap(); -} - -#[gpui::test] -async fn test_toggle_comment(cx: &mut gpui::TestAppContext) { - init_test(cx, |_| {}); - let mut cx = EditorTestContext::new(cx).await; - let language = Arc::new(Language::new( - LanguageConfig { - line_comment: Some("// ".into()), - ..Default::default() - }, - Some(tree_sitter_rust::language()), - )); - cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx)); - - // If multiple selections intersect a line, the line is only toggled once. - cx.set_state(indoc! {" - fn a() { - «//b(); - ˇ»// «c(); - //ˇ» d(); - } - "}); - - cx.update_editor(|e, cx| e.toggle_comments(&ToggleComments::default(), cx)); - - cx.assert_editor_state(indoc! {" - fn a() { - «b(); - c(); - ˇ» d(); - } - "}); - - // The comment prefix is inserted at the same column for every line in a - // selection. - cx.update_editor(|e, cx| e.toggle_comments(&ToggleComments::default(), cx)); - - cx.assert_editor_state(indoc! {" - fn a() { - // «b(); - // c(); - ˇ»// d(); - } - "}); - - // If a selection ends at the beginning of a line, that line is not toggled. - cx.set_selections_state(indoc! {" - fn a() { - // b(); - «// c(); - ˇ» // d(); - } - "}); - - cx.update_editor(|e, cx| e.toggle_comments(&ToggleComments::default(), cx)); - - cx.assert_editor_state(indoc! {" - fn a() { - // b(); - «c(); - ˇ» // d(); - } - "}); - - // If a selection span a single line and is empty, the line is toggled. - cx.set_state(indoc! {" - fn a() { - a(); - b(); - ˇ - } - "}); - - cx.update_editor(|e, cx| e.toggle_comments(&ToggleComments::default(), cx)); - - cx.assert_editor_state(indoc! {" - fn a() { - a(); - b(); - //•ˇ - } - "}); - - // If a selection span multiple lines, empty lines are not toggled. - cx.set_state(indoc! {" - fn a() { - «a(); - - c();ˇ» - } - "}); - - cx.update_editor(|e, cx| e.toggle_comments(&ToggleComments::default(), cx)); - - cx.assert_editor_state(indoc! {" - fn a() { - // «a(); - - // c();ˇ» - } - "}); -} - -#[gpui::test] -async fn test_advance_downward_on_toggle_comment(cx: &mut gpui::TestAppContext) { - init_test(cx, |_| {}); - - let language = Arc::new(Language::new( - LanguageConfig { - line_comment: Some("// ".into()), - ..Default::default() - }, - Some(tree_sitter_rust::language()), - )); - - let registry = Arc::new(LanguageRegistry::test()); - registry.add(language.clone()); - - let mut cx = EditorTestContext::new(cx).await; - cx.update_buffer(|buffer, cx| { - buffer.set_language_registry(registry); - buffer.set_language(Some(language), cx); - }); - - let toggle_comments = &ToggleComments { - advance_downwards: true, - }; - - // Single cursor on one line -> advance - // Cursor moves horizontally 3 characters as well on non-blank line - cx.set_state(indoc!( - "fn a() { - ˇdog(); - cat(); - }" - )); - cx.update_editor(|editor, cx| { - editor.toggle_comments(toggle_comments, cx); - }); - cx.assert_editor_state(indoc!( - "fn a() { - // dog(); - catˇ(); - }" - )); - - // Single selection on one line -> don't advance - cx.set_state(indoc!( - "fn a() { - «dog()ˇ»; - cat(); - }" - )); - cx.update_editor(|editor, cx| { - editor.toggle_comments(toggle_comments, cx); - }); - cx.assert_editor_state(indoc!( - "fn a() { - // «dog()ˇ»; - cat(); - }" - )); - - // Multiple cursors on one line -> advance - cx.set_state(indoc!( - "fn a() { - ˇdˇog(); - cat(); - }" - )); - cx.update_editor(|editor, cx| { - editor.toggle_comments(toggle_comments, cx); - }); - cx.assert_editor_state(indoc!( - "fn a() { - // dog(); - catˇ(ˇ); - }" - )); - - // Multiple cursors on one line, with selection -> don't advance - cx.set_state(indoc!( - "fn a() { - ˇdˇog«()ˇ»; - cat(); - }" - )); - cx.update_editor(|editor, cx| { - editor.toggle_comments(toggle_comments, cx); - }); - cx.assert_editor_state(indoc!( - "fn a() { - // ˇdˇog«()ˇ»; - cat(); - }" - )); - - // Single cursor on one line -> advance - // Cursor moves to column 0 on blank line - cx.set_state(indoc!( - "fn a() { - ˇdog(); - - cat(); - }" - )); - cx.update_editor(|editor, cx| { - editor.toggle_comments(toggle_comments, cx); - }); - cx.assert_editor_state(indoc!( - "fn a() { - // dog(); - ˇ - cat(); - }" - )); - - // Single cursor on one line -> advance - // Cursor starts and ends at column 0 - cx.set_state(indoc!( - "fn a() { - ˇ dog(); - cat(); - }" - )); - cx.update_editor(|editor, cx| { - editor.toggle_comments(toggle_comments, cx); - }); - cx.assert_editor_state(indoc!( - "fn a() { - // dog(); - ˇ cat(); - }" - )); -} - -#[gpui::test] -async fn test_toggle_block_comment(cx: &mut gpui::TestAppContext) { - init_test(cx, |_| {}); - - let mut cx = EditorTestContext::new(cx).await; - - let html_language = Arc::new( - Language::new( - LanguageConfig { - name: "HTML".into(), - block_comment: Some(("".into())), - ..Default::default() - }, - Some(tree_sitter_html::language()), - ) - .with_injection_query( - r#" - (script_element - (raw_text) @content - (#set! "language" "javascript")) - "#, - ) - .unwrap(), - ); - - let javascript_language = Arc::new(Language::new( - LanguageConfig { - name: "JavaScript".into(), - line_comment: Some("// ".into()), - ..Default::default() - }, - Some(tree_sitter_typescript::language_tsx()), - )); - - let registry = Arc::new(LanguageRegistry::test()); - registry.add(html_language.clone()); - registry.add(javascript_language.clone()); - - cx.update_buffer(|buffer, cx| { - buffer.set_language_registry(registry); - buffer.set_language(Some(html_language), cx); - }); - - // Toggle comments for empty selections - cx.set_state( - &r#" -

A

ˇ -

B

ˇ -

C

ˇ - "# - .unindent(), - ); - cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments::default(), cx)); - cx.assert_editor_state( - &r#" - - - - "# - .unindent(), - ); - cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments::default(), cx)); - cx.assert_editor_state( - &r#" -

A

ˇ -

B

ˇ -

C

ˇ - "# - .unindent(), - ); - - // Toggle comments for mixture of empty and non-empty selections, where - // multiple selections occupy a given line. - cx.set_state( - &r#" -

-

ˇ»B

ˇ -

-

ˇ»D

ˇ - "# - .unindent(), - ); - - cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments::default(), cx)); - cx.assert_editor_state( - &r#" - - - "# - .unindent(), - ); - cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments::default(), cx)); - cx.assert_editor_state( - &r#" -

-

ˇ»B

ˇ -

-

ˇ»D

ˇ - "# - .unindent(), - ); - - // Toggle comments when different languages are active for different - // selections. - cx.set_state( - &r#" - ˇ - "# - .unindent(), - ); - cx.foreground().run_until_parked(); - cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments::default(), cx)); - cx.assert_editor_state( - &r#" - - // ˇvar x = new Y(); - - "# - .unindent(), - ); -} - -#[gpui::test] -fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) { - init_test(cx, |_| {}); - - let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, sample_text(3, 4, 'a'))); - 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(0, 4), - primary: None, - }, - ExcerptRange { - context: Point::new(1, 0)..Point::new(1, 4), - primary: None, - }, - ], - cx, - ); - assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb"); - multibuffer - }); - - let view = cx.add_window(|cx| build_editor(multibuffer, cx)).root(cx); - view.update(cx, |view, cx| { - assert_eq!(view.text(cx), "aaaa\nbbbb"); - view.change_selections(None, cx, |s| { - s.select_ranges([ - Point::new(0, 0)..Point::new(0, 0), - Point::new(1, 0)..Point::new(1, 0), - ]) - }); - - view.handle_input("X", cx); - assert_eq!(view.text(cx), "Xaaaa\nXbbbb"); - assert_eq!( - view.selections.ranges(cx), - [ - Point::new(0, 1)..Point::new(0, 1), - Point::new(1, 1)..Point::new(1, 1), - ] - ); - - // Ensure the cursor's head is respected when deleting across an excerpt boundary. - view.change_selections(None, cx, |s| { - s.select_ranges([Point::new(0, 2)..Point::new(1, 2)]) - }); - view.backspace(&Default::default(), cx); - assert_eq!(view.text(cx), "Xa\nbbb"); - assert_eq!( - view.selections.ranges(cx), - [Point::new(1, 0)..Point::new(1, 0)] - ); - - view.change_selections(None, cx, |s| { - s.select_ranges([Point::new(1, 1)..Point::new(0, 1)]) - }); - view.backspace(&Default::default(), cx); - assert_eq!(view.text(cx), "X\nbb"); - assert_eq!( - view.selections.ranges(cx), - [Point::new(0, 1)..Point::new(0, 1)] - ); - }); -} - -#[gpui::test] -fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) { - init_test(cx, |_| {}); - - let markers = vec![('[', ']').into(), ('(', ')').into()]; - let (initial_text, mut excerpt_ranges) = marked_text_ranges_by( - indoc! {" - [aaaa - (bbbb] - cccc)", - }, - markers.clone(), - ); - let excerpt_ranges = markers.into_iter().map(|marker| { - let context = excerpt_ranges.remove(&marker).unwrap()[0].clone(); - ExcerptRange { - context, - primary: None, - } - }); - let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, initial_text)); - let multibuffer = cx.add_model(|cx| { - let mut multibuffer = MultiBuffer::new(0); - multibuffer.push_excerpts(buffer, excerpt_ranges, cx); - multibuffer - }); - - let view = cx.add_window(|cx| build_editor(multibuffer, cx)).root(cx); - view.update(cx, |view, cx| { - let (expected_text, selection_ranges) = marked_text_ranges( - indoc! {" - aaaa - bˇbbb - bˇbbˇb - cccc" - }, - true, - ); - assert_eq!(view.text(cx), expected_text); - view.change_selections(None, cx, |s| s.select_ranges(selection_ranges)); - - view.handle_input("X", cx); - - let (expected_text, expected_selections) = marked_text_ranges( - indoc! {" - aaaa - bXˇbbXb - bXˇbbXˇb - cccc" - }, - false, - ); - assert_eq!(view.text(cx), expected_text); - assert_eq!(view.selections.ranges(cx), expected_selections); - - view.newline(&Newline, cx); - let (expected_text, expected_selections) = marked_text_ranges( - indoc! {" - aaaa - bX - ˇbbX - b - bX - ˇbbX - ˇb - cccc" - }, - false, - ); - assert_eq!(view.text(cx), expected_text); - assert_eq!(view.selections.ranges(cx), expected_selections); - }); -} - -#[gpui::test] -fn test_refresh_selections(cx: &mut TestAppContext) { - init_test(cx, |_| {}); - - let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, sample_text(3, 4, 'a'))); - let mut excerpt1_id = None; - let multibuffer = cx.add_model(|cx| { - let mut multibuffer = MultiBuffer::new(0); - excerpt1_id = multibuffer - .push_excerpts( - buffer.clone(), - [ - ExcerptRange { - context: Point::new(0, 0)..Point::new(1, 4), - primary: None, - }, - ExcerptRange { - context: Point::new(1, 0)..Point::new(2, 4), - primary: None, - }, - ], - cx, - ) - .into_iter() - .next(); - assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc"); - multibuffer - }); - - let editor = cx - .add_window(|cx| { - let mut editor = build_editor(multibuffer.clone(), cx); - let snapshot = editor.snapshot(cx); - editor.change_selections(None, cx, |s| { - s.select_ranges([Point::new(1, 3)..Point::new(1, 3)]) - }); - editor.begin_selection(Point::new(2, 1).to_display_point(&snapshot), true, 1, cx); - assert_eq!( - editor.selections.ranges(cx), - [ - Point::new(1, 3)..Point::new(1, 3), - Point::new(2, 1)..Point::new(2, 1), - ] - ); - editor - }) - .root(cx); - - // Refreshing selections is a no-op when excerpts haven't changed. - editor.update(cx, |editor, cx| { - editor.change_selections(None, cx, |s| s.refresh()); - assert_eq!( - editor.selections.ranges(cx), - [ - Point::new(1, 3)..Point::new(1, 3), - Point::new(2, 1)..Point::new(2, 1), - ] - ); - }); - - multibuffer.update(cx, |multibuffer, cx| { - multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx); - }); - editor.update(cx, |editor, cx| { - // Removing an excerpt causes the first selection to become degenerate. - assert_eq!( - editor.selections.ranges(cx), - [ - Point::new(0, 0)..Point::new(0, 0), - Point::new(0, 1)..Point::new(0, 1) - ] - ); - - // Refreshing selections will relocate the first selection to the original buffer - // location. - editor.change_selections(None, cx, |s| s.refresh()); - assert_eq!( - editor.selections.ranges(cx), - [ - Point::new(0, 1)..Point::new(0, 1), - Point::new(0, 3)..Point::new(0, 3) - ] - ); - assert!(editor.selections.pending_anchor().is_some()); - }); -} - -#[gpui::test] -fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) { - init_test(cx, |_| {}); - - let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, sample_text(3, 4, 'a'))); - let mut excerpt1_id = None; - let multibuffer = cx.add_model(|cx| { - let mut multibuffer = MultiBuffer::new(0); - excerpt1_id = multibuffer - .push_excerpts( - buffer.clone(), - [ - ExcerptRange { - context: Point::new(0, 0)..Point::new(1, 4), - primary: None, - }, - ExcerptRange { - context: Point::new(1, 0)..Point::new(2, 4), - primary: None, - }, - ], - cx, - ) - .into_iter() - .next(); - assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc"); - multibuffer - }); - - let editor = cx - .add_window(|cx| { - let mut editor = build_editor(multibuffer.clone(), cx); - let snapshot = editor.snapshot(cx); - editor.begin_selection(Point::new(1, 3).to_display_point(&snapshot), false, 1, cx); - assert_eq!( - editor.selections.ranges(cx), - [Point::new(1, 3)..Point::new(1, 3)] - ); - editor - }) - .root(cx); - - multibuffer.update(cx, |multibuffer, cx| { - multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx); - }); - editor.update(cx, |editor, cx| { - assert_eq!( - editor.selections.ranges(cx), - [Point::new(0, 0)..Point::new(0, 0)] - ); - - // Ensure we don't panic when selections are refreshed and that the pending selection is finalized. - editor.change_selections(None, cx, |s| s.refresh()); - assert_eq!( - editor.selections.ranges(cx), - [Point::new(0, 3)..Point::new(0, 3)] - ); - assert!(editor.selections.pending_anchor().is_some()); - }); -} - -#[gpui::test] -async fn test_extra_newline_insertion(cx: &mut gpui::TestAppContext) { - init_test(cx, |_| {}); - - let language = Arc::new( - Language::new( - LanguageConfig { - brackets: BracketPairConfig { - pairs: vec![ - BracketPair { - start: "{".to_string(), - end: "}".to_string(), - close: true, - newline: true, - }, - BracketPair { - start: "/* ".to_string(), - end: " */".to_string(), - close: true, - newline: true, - }, - ], - ..Default::default() - }, - ..Default::default() - }, - Some(tree_sitter_rust::language()), - ) - .with_indents_query("") - .unwrap(), - ); - - let text = concat!( - "{ }\n", // - " x\n", // - " /* */\n", // - "x\n", // - "{{} }\n", // - ); - - let buffer = - cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, text).with_language(language, cx)); - let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); - let view = cx.add_window(|cx| build_editor(buffer, cx)).root(cx); - view.condition(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx)) - .await; - - view.update(cx, |view, cx| { - view.change_selections(None, cx, |s| { - s.select_display_ranges([ - DisplayPoint::new(0, 2)..DisplayPoint::new(0, 3), - DisplayPoint::new(2, 5)..DisplayPoint::new(2, 5), - DisplayPoint::new(4, 4)..DisplayPoint::new(4, 4), - ]) - }); - view.newline(&Newline, cx); - - assert_eq!( - view.buffer().read(cx).read(cx).text(), - concat!( - "{ \n", // Suppress rustfmt - "\n", // - "}\n", // - " x\n", // - " /* \n", // - " \n", // - " */\n", // - "x\n", // - "{{} \n", // - "}\n", // - ) - ); - }); -} - -#[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) - }) - .root(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)), - ], - |_| Color::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)), - ], - |_| Color::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, - theme::current(cx).as_ref(), - ); - // Enforce a consistent ordering based on color without relying on the ordering of the - // highlight's `TypeId` which is non-deterministic. - highlighted_ranges.sort_unstable_by_key(|(_, color)| *color); - assert_eq!( - highlighted_ranges, - &[ - ( - DisplayPoint::new(3, 2)..DisplayPoint::new(3, 5), - Color::green(), - ), - ( - DisplayPoint::new(5, 3)..DisplayPoint::new(5, 6), - Color::green(), - ), - ( - DisplayPoint::new(4, 2)..DisplayPoint::new(4, 4), - Color::red(), - ), - ( - DisplayPoint::new(6, 3)..DisplayPoint::new(6, 5), - Color::red(), - ), - ] - ); - assert_eq!( - editor.background_highlights_in_range( - anchor_range(Point::new(5, 6)..Point::new(6, 4)), - &snapshot, - theme::current(cx).as_ref(), - ), - &[( - DisplayPoint::new(6, 3)..DisplayPoint::new(6, 5), - Color::red(), - )] - ); - }); -} - -#[gpui::test] -async fn test_following(cx: &mut gpui::TestAppContext) { - init_test(cx, |_| {}); - - let fs = FakeFs::new(cx.background()); - let project = Project::test(fs, ["/file.rs".as_ref()], cx).await; - - let buffer = project.update(cx, |project, cx| { - let buffer = project - .create_buffer(&sample_text(16, 8, 'a'), None, cx) - .unwrap(); - cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)) - }); - let leader = cx - .add_window(|cx| build_editor(buffer.clone(), cx)) - .root(cx); - let follower = cx - .update(|cx| { - cx.add_window( - WindowOptions { - bounds: WindowBounds::Fixed(RectF::from_points(vec2f(0., 0.), vec2f(10., 80.))), - ..Default::default() - }, - |cx| build_editor(buffer.clone(), cx), - ) - }) - .root(cx); - - 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, move |_, leader, event, cx| { - leader - .read(cx) - .add_event_to_update_proto(event, &mut *update.borrow_mut(), cx); - }) - .detach(); - - cx.subscribe(&follower, move |_, _, event, cx| { - if Editor::should_unfollow_on_event(event, cx) { - *is_still_following.borrow_mut() = false; - } - if let Event::BufferEdited = event { - *follower_edit_event_count.borrow_mut() += 1; - } - }) - .detach(); - } - }); - - // 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) - }) - .await - .unwrap(); - follower.read_with(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 scroll position only - leader.update(cx, |leader, cx| { - leader.set_scroll_position(vec2f(1.5, 3.5), cx); - }); - follower - .update(cx, |follower, cx| { - follower.apply_update_proto(&project, pending_update.borrow_mut().take().unwrap(), cx) - }) - .await - .unwrap(); - assert_eq!( - follower.update(cx, |follower, cx| follower.scroll_position(cx)), - vec2f(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(vec2f(1.5, 3.5), cx); - }); - follower - .update(cx, |follower, cx| { - follower.apply_update_proto(&project, pending_update.borrow_mut().take().unwrap(), cx) - }) - .await - .unwrap(); - follower.update(cx, |follower, cx| { - assert_eq!(follower.scroll_position(cx), vec2f(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) - }) - .await - .unwrap(); - follower.read_with(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) - }) - .await - .unwrap(); - follower.read_with(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: vec2f(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) { - init_test(cx, |_| {}); - - let fs = FakeFs::new(cx.background()); - let project = Project::test(fs, ["/file.rs".as_ref()], cx).await; - let workspace = cx - .add_window(|cx| Workspace::test_new(project.clone(), cx)) - .root(cx); - let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - - let leader = pane.update(cx, |_, cx| { - let multibuffer = cx.add_model(|_| MultiBuffer::new(0)); - cx.add_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.clone(), - 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(); - } - }); - - 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(), - ) - }); - - // 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, - ); - }); - }); - - // 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.read_with(cx, |editor, cx| editor.text(cx)), - leader.read_with(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.read_with(cx, |editor, cx| editor.text(cx)), - leader.read_with(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.read_with(cx, |editor, cx| editor.text(cx)), - leader.read_with(cx, |editor, cx| editor.text(cx)) - ); -} - -#[test] -fn test_combine_syntax_and_fuzzy_match_highlights() { - let string = "abcdefghijklmnop"; - let syntax_ranges = [ - ( - 0..3, - HighlightStyle { - color: Some(Color::red()), - ..Default::default() - }, - ), - ( - 4..8, - HighlightStyle { - color: Some(Color::green()), - ..Default::default() - }, - ), - ]; - let match_indices = [4, 6, 7, 8]; - assert_eq!( - combine_syntax_and_fuzzy_match_highlights( - string, - Default::default(), - syntax_ranges.into_iter(), - &match_indices, - ), - &[ - ( - 0..3, - HighlightStyle { - color: Some(Color::red()), - ..Default::default() - }, - ), - ( - 4..5, - HighlightStyle { - color: Some(Color::green()), - weight: Some(fonts::Weight::BOLD), - ..Default::default() - }, - ), - ( - 5..6, - HighlightStyle { - color: Some(Color::green()), - ..Default::default() - }, - ), - ( - 6..8, - HighlightStyle { - color: Some(Color::green()), - weight: Some(fonts::Weight::BOLD), - ..Default::default() - }, - ), - ( - 8..9, - HighlightStyle { - weight: Some(fonts::Weight::BOLD), - ..Default::default() - }, - ), - ] - ); -} - -#[gpui::test] -async fn go_to_prev_overlapping_diagnostic( - deterministic: Arc, - cx: &mut gpui::TestAppContext, -) { - init_test(cx, |_| {}); - - let mut cx = EditorTestContext::new(cx).await; - let project = cx.update_editor(|editor, _| editor.project.clone().unwrap()); - - cx.set_state(indoc! {" - ˇfn func(abc def: i32) -> u32 { - } - "}); - - cx.update(|cx| { - project.update(cx, |project, cx| { - project - .update_diagnostics( - LanguageServerId(0), - lsp::PublishDiagnosticsParams { - uri: lsp::Url::from_file_path("/root/file").unwrap(), - version: None, - diagnostics: vec![ - lsp::Diagnostic { - range: lsp::Range::new( - lsp::Position::new(0, 11), - lsp::Position::new(0, 12), - ), - severity: Some(lsp::DiagnosticSeverity::ERROR), - ..Default::default() - }, - lsp::Diagnostic { - range: lsp::Range::new( - lsp::Position::new(0, 12), - lsp::Position::new(0, 15), - ), - severity: Some(lsp::DiagnosticSeverity::ERROR), - ..Default::default() - }, - lsp::Diagnostic { - range: lsp::Range::new( - lsp::Position::new(0, 25), - lsp::Position::new(0, 28), - ), - severity: Some(lsp::DiagnosticSeverity::ERROR), - ..Default::default() - }, - ], - }, - &[], - cx, - ) - .unwrap() - }); - }); - - deterministic.run_until_parked(); - - cx.update_editor(|editor, cx| { - editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, cx); - }); - - cx.assert_editor_state(indoc! {" - fn func(abc def: i32) -> ˇu32 { - } - "}); - - cx.update_editor(|editor, cx| { - editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, cx); - }); - - cx.assert_editor_state(indoc! {" - fn func(abc ˇdef: i32) -> u32 { - } - "}); - - cx.update_editor(|editor, cx| { - editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, cx); - }); - - cx.assert_editor_state(indoc! {" - fn func(abcˇ def: i32) -> u32 { - } - "}); - - cx.update_editor(|editor, cx| { - editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, cx); - }); - - cx.assert_editor_state(indoc! {" - fn func(abc def: i32) -> ˇu32 { - } - "}); -} - -#[gpui::test] -async fn go_to_hunk(deterministic: Arc, cx: &mut gpui::TestAppContext) { - init_test(cx, |_| {}); - - let mut cx = EditorTestContext::new(cx).await; - - let diff_base = r#" - use some::mod; - - const A: u32 = 42; - - fn main() { - println!("hello"); - - println!("world"); - } - "# - .unindent(); - - // Edits are modified, removed, modified, added - cx.set_state( - &r#" - use some::modified; - - ˇ - fn main() { - println!("hello there"); - - println!("around the"); - println!("world"); - } - "# - .unindent(), - ); - - cx.set_diff_base(Some(&diff_base)); - deterministic.run_until_parked(); - - cx.update_editor(|editor, cx| { - //Wrap around the bottom of the buffer - for _ in 0..3 { - editor.go_to_hunk(&GoToHunk, cx); - } - }); - - cx.assert_editor_state( - &r#" - ˇuse some::modified; - - - fn main() { - println!("hello there"); - - println!("around the"); - println!("world"); - } - "# - .unindent(), - ); - - cx.update_editor(|editor, cx| { - //Wrap around the top of the buffer - for _ in 0..2 { - editor.go_to_prev_hunk(&GoToPrevHunk, cx); - } - }); - - cx.assert_editor_state( - &r#" - use some::modified; - - - fn main() { - ˇ println!("hello there"); - - println!("around the"); - println!("world"); - } - "# - .unindent(), - ); - - cx.update_editor(|editor, cx| { - editor.go_to_prev_hunk(&GoToPrevHunk, cx); - }); - - cx.assert_editor_state( - &r#" - use some::modified; - - ˇ - fn main() { - println!("hello there"); - - println!("around the"); - println!("world"); - } - "# - .unindent(), - ); - - cx.update_editor(|editor, cx| { - for _ in 0..3 { - editor.go_to_prev_hunk(&GoToPrevHunk, cx); - } - }); - - cx.assert_editor_state( - &r#" - use some::modified; - - - fn main() { - ˇ println!("hello there"); - - println!("around the"); - println!("world"); - } - "# - .unindent(), - ); - - cx.update_editor(|editor, cx| { - editor.fold(&Fold, cx); - - //Make sure that the fold only gets one hunk - for _ in 0..4 { - editor.go_to_hunk(&GoToHunk, cx); - } - }); - - cx.assert_editor_state( - &r#" - ˇuse some::modified; - - - fn main() { - println!("hello there"); - - println!("around the"); - println!("world"); - } - "# - .unindent(), - ); -} - -#[test] -fn test_split_words() { - fn split<'a>(text: &'a str) -> Vec<&'a str> { - split_words(text).collect() - } - - assert_eq!(split("HelloWorld"), &["Hello", "World"]); - assert_eq!(split("hello_world"), &["hello_", "world"]); - assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]); - assert_eq!(split("Hello_World"), &["Hello_", "World"]); - assert_eq!(split("helloWOrld"), &["hello", "WOrld"]); - assert_eq!(split("helloworld"), &["helloworld"]); -} - -#[gpui::test] -async fn test_move_to_enclosing_bracket(cx: &mut gpui::TestAppContext) { - init_test(cx, |_| {}); - - let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await; - let mut assert = |before, after| { - let _state_context = cx.set_state(before); - cx.update_editor(|editor, cx| { - editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, cx) - }); - cx.assert_editor_state(after); - }; - - // Outside bracket jumps to outside of matching bracket - assert("console.logˇ(var);", "console.log(var)ˇ;"); - assert("console.log(var)ˇ;", "console.logˇ(var);"); - - // Inside bracket jumps to inside of matching bracket - assert("console.log(ˇvar);", "console.log(varˇ);"); - assert("console.log(varˇ);", "console.log(ˇvar);"); - - // When outside a bracket and inside, favor jumping to the inside bracket - assert( - "console.log('foo', [1, 2, 3]ˇ);", - "console.log(ˇ'foo', [1, 2, 3]);", - ); - assert( - "console.log(ˇ'foo', [1, 2, 3]);", - "console.log('foo', [1, 2, 3]ˇ);", - ); - - // Bias forward if two options are equally likely - assert( - "let result = curried_fun()ˇ();", - "let result = curried_fun()()ˇ;", - ); - - // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller - assert( - indoc! {" - function test() { - console.log('test')ˇ - }"}, - indoc! {" - function test() { - console.logˇ('test') - }"}, - ); -} - -#[gpui::test(iterations = 10)] -async fn test_copilot(deterministic: Arc, cx: &mut gpui::TestAppContext) { - 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; - - // 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![], - ); - deterministic.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"); - }); - - // 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![], - ); - deterministic.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![], - ); - deterministic.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"); - }); - - // Ensure existing completion is interpolated when inserting again. - cx.simulate_keystroke("c"); - deterministic.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![], - ); - deterministic.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"); - - // 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"); - }); - - // 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"); - - // 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"); - }); - - // 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![], - ); - - cx.update_editor(|editor, cx| editor.next_copilot_suggestion(&Default::default(), cx)); - deterministic.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 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( - deterministic: Arc, - cx: &mut gpui::TestAppContext, -) { - 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; - - cx.set_state(indoc! {" - one - twˇ - three - "}); - - handle_copilot_completion_request( - &copilot_lsp, - vec![copilot::request::Completion { - text: "two.foo()".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)); - deterministic.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); - cx.update_editor(|editor, cx| { - assert!(editor.has_active_copilot_suggestion(cx)); - assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n"); - assert_eq!(editor.text(cx), "one\ntw\nthree\n"); - - editor.backspace(&Default::default(), cx); - assert!(editor.has_active_copilot_suggestion(cx)); - assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n"); - assert_eq!(editor.text(cx), "one\nt\nthree\n"); - - editor.backspace(&Default::default(), cx); - assert!(editor.has_active_copilot_suggestion(cx)); - assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n"); - assert_eq!(editor.text(cx), "one\n\nthree\n"); - - // Deleting across the original suggestion range invalidates it. - editor.backspace(&Default::default(), cx); - assert!(!editor.has_active_copilot_suggestion(cx)); - assert_eq!(editor.display_text(cx), "one\nthree\n"); - assert_eq!(editor.text(cx), "one\nthree\n"); - - // Undoing the deletion restores the suggestion. - editor.undo(&Default::default(), cx); - assert!(editor.has_active_copilot_suggestion(cx)); - assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n"); - assert_eq!(editor.text(cx), "one\n\nthree\n"); - }); -} - -#[gpui::test] -async fn test_copilot_multibuffer( - deterministic: Arc, - 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.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "a = 1\nb = 2\n")); - let buffer_2 = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "c = 3\nd = 4\n")); - let multibuffer = cx.add_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)).root(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); - }); - deterministic.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. - deterministic.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( - deterministic: Arc, - cx: &mut gpui::TestAppContext, -) { - init_test(cx, |settings| { - settings - .copilot - .get_or_insert(Default::default()) - .disabled_globs = Some(vec![".env*".to_string()]); - }); - - let (copilot, copilot_lsp) = Copilot::fake(cx); - cx.update(|cx| cx.set_global(copilot)); - - let fs = FakeFs::new(cx.background()); - fs.insert_tree( - "/test", - json!({ - ".env": "SECRET=something\n", - "README.md": "hello\n" - }), - ) - .await; - let project = Project::test(fs, ["/test".as_ref()], cx).await; - - let private_buffer = project - .update(cx, |project, cx| { - project.open_local_buffer("/test/.env", cx) - }) - .await - .unwrap(); - let public_buffer = project - .update(cx, |project, cx| { - project.open_local_buffer("/test/README.md", cx) - }) - .await - .unwrap(); - - let multibuffer = cx.add_model(|cx| { - let mut multibuffer = MultiBuffer::new(0); - multibuffer.push_excerpts( - private_buffer.clone(), - [ExcerptRange { - context: Point::new(0, 0)..Point::new(1, 0), - primary: None, - }], - cx, - ); - multibuffer.push_excerpts( - public_buffer.clone(), - [ExcerptRange { - context: Point::new(0, 0)..Point::new(1, 0), - primary: None, - }], - cx, - ); - multibuffer - }); - let editor = cx.add_window(|cx| build_editor(multibuffer, cx)).root(cx); - - let mut copilot_requests = copilot_lsp - .handle_request::(move |_params, _cx| async move { - Ok(copilot::request::GetCompletionsResult { - completions: vec![copilot::request::Completion { - text: "next line".into(), - range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 0)), - ..Default::default() - }], - }) - }); - - editor.update(cx, |editor, cx| { - editor.change_selections(None, cx, |selections| { - selections.select_ranges([Point::new(0, 0)..Point::new(0, 0)]) - }); - editor.next_copilot_suggestion(&Default::default(), cx); - }); - - deterministic.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); - assert!(copilot_requests.try_next().is_err()); - - editor.update(cx, |editor, cx| { - editor.change_selections(None, cx, |s| { - s.select_ranges([Point::new(2, 0)..Point::new(2, 0)]) - }); - editor.next_copilot_suggestion(&Default::default(), cx); - }); - - deterministic.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); - assert!(copilot_requests.try_next().is_ok()); -} - -#[gpui::test] -async fn test_on_type_formatting_not_triggered(cx: &mut gpui::TestAppContext) { - init_test(cx, |_| {}); - - let mut language = Language::new( - LanguageConfig { - name: "Rust".into(), - path_suffixes: vec!["rs".to_string()], - brackets: BracketPairConfig { - pairs: vec![BracketPair { - start: "{".to_string(), - end: "}".to_string(), - close: true, - newline: true, - }], - disabled_scopes_by_bracket_ix: Vec::new(), - }, - ..Default::default() - }, - Some(tree_sitter_rust::language()), - ); - let mut fake_servers = language - .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { - capabilities: lsp::ServerCapabilities { - document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions { - first_trigger_character: "{".to_string(), - more_trigger_character: None, - }), - ..Default::default() - }, - ..Default::default() - })) - .await; - - let fs = FakeFs::new(cx.background()); - fs.insert_tree( - "/a", - json!({ - "main.rs": "fn main() { let a = 5; }", - "other.rs": "// Test file", - }), - ) - .await; - let project = Project::test(fs, ["/a".as_ref()], cx).await; - project.update(cx, |project, _| project.languages().add(Arc::new(language))); - let workspace = cx - .add_window(|cx| Workspace::test_new(project.clone(), cx)) - .root(cx); - let worktree_id = workspace.update(cx, |workspace, cx| { - workspace.project().read_with(cx, |project, cx| { - project.worktrees(cx).next().unwrap().read(cx).id() - }) - }); - - let buffer = project - .update(cx, |project, cx| { - project.open_local_buffer("/a/main.rs", cx) - }) - .await - .unwrap(); - cx.foreground().run_until_parked(); - cx.foreground().start_waiting(); - let fake_server = fake_servers.next().await.unwrap(); - let editor_handle = workspace - .update(cx, |workspace, cx| { - workspace.open_path((worktree_id, "main.rs"), None, true, cx) - }) - .await - .unwrap() - .downcast::() - .unwrap(); - - fake_server.handle_request::(|params, _| async move { - assert_eq!( - params.text_document_position.text_document.uri, - lsp::Url::from_file_path("/a/main.rs").unwrap(), - ); - assert_eq!( - params.text_document_position.position, - lsp::Position::new(0, 21), - ); - - Ok(Some(vec![lsp::TextEdit { - new_text: "]".to_string(), - range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)), - }])) - }); - - editor_handle.update(cx, |editor, cx| { - cx.focus(&editor_handle); - editor.change_selections(None, cx, |s| { - s.select_ranges([Point::new(0, 21)..Point::new(0, 20)]) - }); - editor.handle_input("{", cx); - }); - - cx.foreground().run_until_parked(); - - buffer.read_with(cx, |buffer, _| { - assert_eq!( - buffer.text(), - "fn main() { let a = {5}; }", - "No extra braces from on type formatting should appear in the buffer" - ) - }); -} - -#[gpui::test] -async fn test_language_server_restart_due_to_settings_change(cx: &mut gpui::TestAppContext) { - init_test(cx, |_| {}); - - let language_name: Arc = "Rust".into(); - let mut language = Language::new( - LanguageConfig { - name: Arc::clone(&language_name), - path_suffixes: vec!["rs".to_string()], - ..Default::default() - }, - Some(tree_sitter_rust::language()), - ); - - let server_restarts = Arc::new(AtomicUsize::new(0)); - let closure_restarts = Arc::clone(&server_restarts); - let language_server_name = "test language server"; - let mut fake_servers = language - .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { - name: language_server_name, - initialization_options: Some(json!({ - "testOptionValue": true - })), - initializer: Some(Box::new(move |fake_server| { - let task_restarts = Arc::clone(&closure_restarts); - fake_server.handle_request::(move |_, _| { - task_restarts.fetch_add(1, atomic::Ordering::Release); - futures::future::ready(Ok(())) - }); - })), - ..Default::default() - })) - .await; - - let fs = FakeFs::new(cx.background()); - fs.insert_tree( - "/a", - json!({ - "main.rs": "fn main() { let a = 5; }", - "other.rs": "// Test file", - }), - ) - .await; - let project = Project::test(fs, ["/a".as_ref()], cx).await; - project.update(cx, |project, _| project.languages().add(Arc::new(language))); - let _window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); - let _buffer = project - .update(cx, |project, cx| { - project.open_local_buffer("/a/main.rs", cx) - }) - .await - .unwrap(); - let _fake_server = fake_servers.next().await.unwrap(); - update_test_language_settings(cx, |language_settings| { - language_settings.languages.insert( - Arc::clone(&language_name), - LanguageSettingsContent { - tab_size: NonZeroU32::new(8), - ..Default::default() - }, - ); - }); - cx.foreground().run_until_parked(); - assert_eq!( - server_restarts.load(atomic::Ordering::Acquire), - 0, - "Should not restart LSP server on an unrelated change" - ); - - update_test_project_settings(cx, |project_settings| { - project_settings.lsp.insert( - "Some other server name".into(), - LspSettings { - initialization_options: Some(json!({ - "some other init value": false - })), - }, - ); - }); - cx.foreground().run_until_parked(); - assert_eq!( - server_restarts.load(atomic::Ordering::Acquire), - 0, - "Should not restart LSP server on an unrelated LSP settings change" - ); - - update_test_project_settings(cx, |project_settings| { - project_settings.lsp.insert( - language_server_name.into(), - LspSettings { - initialization_options: Some(json!({ - "anotherInitValue": false - })), - }, - ); - }); - cx.foreground().run_until_parked(); - assert_eq!( - server_restarts.load(atomic::Ordering::Acquire), - 1, - "Should restart LSP server on a related LSP settings change" - ); - - update_test_project_settings(cx, |project_settings| { - project_settings.lsp.insert( - language_server_name.into(), - LspSettings { - initialization_options: Some(json!({ - "anotherInitValue": false - })), - }, - ); - }); - cx.foreground().run_until_parked(); - assert_eq!( - server_restarts.load(atomic::Ordering::Acquire), - 1, - "Should not restart LSP server on a related LSP settings change that is the same" - ); - - update_test_project_settings(cx, |project_settings| { - project_settings.lsp.insert( - language_server_name.into(), - LspSettings { - initialization_options: None, - }, - ); - }); - cx.foreground().run_until_parked(); - assert_eq!( - server_restarts.load(atomic::Ordering::Acquire), - 2, - "Should restart LSP server on another related LSP settings change" - ); -} - -#[gpui::test] -async fn test_completions_with_additional_edits(cx: &mut gpui::TestAppContext) { - init_test(cx, |_| {}); - - let mut cx = EditorLspTestContext::new_rust( - lsp::ServerCapabilities { - completion_provider: Some(lsp::CompletionOptions { - trigger_characters: Some(vec![".".to_string()]), - resolve_provider: Some(true), - ..Default::default() - }), - ..Default::default() - }, - cx, - ) - .await; - - cx.set_state(indoc! {"fn main() { let a = 2ˇ; }"}); - cx.simulate_keystroke("."); - let completion_item = lsp::CompletionItem { - label: "some".into(), - kind: Some(lsp::CompletionItemKind::SNIPPET), - detail: Some("Wrap the expression in an `Option::Some`".to_string()), - documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent { - kind: lsp::MarkupKind::Markdown, - value: "```rust\nSome(2)\n```".to_string(), - })), - deprecated: Some(false), - sort_text: Some("fffffff2".to_string()), - filter_text: Some("some".to_string()), - insert_text_format: Some(lsp::InsertTextFormat::SNIPPET), - text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { - range: lsp::Range { - start: lsp::Position { - line: 0, - character: 22, - }, - end: lsp::Position { - line: 0, - character: 22, - }, - }, - new_text: "Some(2)".to_string(), - })), - additional_text_edits: Some(vec![lsp::TextEdit { - range: lsp::Range { - start: lsp::Position { - line: 0, - character: 20, - }, - end: lsp::Position { - line: 0, - character: 22, - }, - }, - new_text: "".to_string(), - }]), - ..Default::default() - }; - - let closure_completion_item = completion_item.clone(); - let mut request = cx.handle_request::(move |_, _, _| { - let task_completion_item = closure_completion_item.clone(); - async move { - Ok(Some(lsp::CompletionResponse::Array(vec![ - task_completion_item, - ]))) - } - }); - - request.next().await; - - cx.condition(|editor, _| editor.context_menu_visible()) - .await; - let apply_additional_edits = cx.update_editor(|editor, cx| { - editor - .confirm_completion(&ConfirmCompletion::default(), cx) - .unwrap() - }); - cx.assert_editor_state(indoc! {"fn main() { let a = 2.Some(2)ˇ; }"}); - - cx.handle_request::(move |_, _, _| { - let task_completion_item = completion_item.clone(); - async move { Ok(task_completion_item) } - }) - .next() - .await - .unwrap(); - apply_additional_edits.await.unwrap(); - cx.assert_editor_state(indoc! {"fn main() { let a = Some(2)ˇ; }"}); -} - -#[gpui::test] -async fn test_completions_in_languages_with_extra_word_characters(cx: &mut gpui::TestAppContext) { - init_test(cx, |_| {}); - - let mut cx = EditorLspTestContext::new( - Language::new( - LanguageConfig { - path_suffixes: vec!["jsx".into()], - overrides: [( - "element".into(), - LanguageConfigOverride { - word_characters: Override::Set(['-'].into_iter().collect()), - ..Default::default() - }, - )] - .into_iter() - .collect(), - ..Default::default() - }, - Some(tree_sitter_typescript::language_tsx()), - ) - .with_override_query("(jsx_self_closing_element) @element") - .unwrap(), - lsp::ServerCapabilities { - completion_provider: Some(lsp::CompletionOptions { - trigger_characters: Some(vec![":".to_string()]), - ..Default::default() - }), - ..Default::default() - }, - cx, - ) - .await; - - cx.lsp - .handle_request::(move |_, _| async move { - Ok(Some(lsp::CompletionResponse::Array(vec![ - lsp::CompletionItem { - label: "bg-blue".into(), - ..Default::default() - }, - lsp::CompletionItem { - label: "bg-red".into(), - ..Default::default() - }, - lsp::CompletionItem { - label: "bg-yellow".into(), - ..Default::default() - }, - ]))) - }); - - cx.set_state(r#"

"#); - - // Trigger completion when typing a dash, because the dash is an extra - // word character in the 'element' scope, which contains the cursor. - cx.simulate_keystroke("-"); - cx.foreground().run_until_parked(); - cx.update_editor(|editor, _| { - if let Some(ContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() { - assert_eq!( - menu.matches.iter().map(|m| &m.string).collect::>(), - &["bg-red", "bg-blue", "bg-yellow"] - ); - } else { - panic!("expected completion menu to be open"); - } - }); - - cx.simulate_keystroke("l"); - cx.foreground().run_until_parked(); - cx.update_editor(|editor, _| { - if let Some(ContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() { - assert_eq!( - menu.matches.iter().map(|m| &m.string).collect::>(), - &["bg-blue", "bg-yellow"] - ); - } else { - panic!("expected completion menu to be open"); - } - }); - - // When filtering completions, consider the character after the '-' to - // be the start of a subword. - cx.set_state(r#"

"#); - cx.simulate_keystroke("l"); - cx.foreground().run_until_parked(); - cx.update_editor(|editor, _| { - if let Some(ContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() { - assert_eq!( - menu.matches.iter().map(|m| &m.string).collect::>(), - &["bg-yellow"] - ); - } else { - panic!("expected completion menu to be open"); - } - }); -} - -#[gpui::test] -async fn test_document_format_with_prettier(cx: &mut gpui::TestAppContext) { - init_test(cx, |settings| { - settings.defaults.formatter = Some(language_settings::Formatter::Prettier) - }); - - let mut language = Language::new( - LanguageConfig { - name: "Rust".into(), - path_suffixes: vec!["rs".to_string()], - prettier_parser_name: Some("test_parser".to_string()), - ..Default::default() - }, - Some(tree_sitter_rust::language()), - ); - - let test_plugin = "test_plugin"; - let _ = language - .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { - prettier_plugins: vec![test_plugin], - ..Default::default() - })) - .await; - - let fs = FakeFs::new(cx.background()); - fs.insert_file("/file.rs", Default::default()).await; - - let project = Project::test(fs, ["/file.rs".as_ref()], cx).await; - let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX; - project.update(cx, |project, _| { - project.languages().add(Arc::new(language)); - }); - let buffer = project - .update(cx, |project, cx| project.open_local_buffer("/file.rs", cx)) - .await - .unwrap(); - - let buffer_text = "one\ntwo\nthree\n"; - let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); - let editor = cx.add_window(|cx| build_editor(buffer, cx)).root(cx); - editor.update(cx, |editor, cx| editor.set_text(buffer_text, cx)); - - let format = editor.update(cx, |editor, cx| { - editor.perform_format(project.clone(), FormatTrigger::Manual, cx) - }); - format.await.unwrap(); - assert_eq!( - editor.read_with(cx, |editor, cx| editor.text(cx)), - buffer_text.to_string() + prettier_format_suffix, - "Test prettier formatting was not applied to the original buffer text", - ); - - update_test_language_settings(cx, |settings| { - settings.defaults.formatter = Some(language_settings::Formatter::Auto) - }); - let format = editor.update(cx, |editor, cx| { - editor.perform_format(project.clone(), FormatTrigger::Manual, cx) - }); - format.await.unwrap(); - assert_eq!( - editor.read_with(cx, |editor, cx| editor.text(cx)), - buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix, - "Autoformatting (via test prettier) was not applied to the original buffer text", - ); -} - -fn empty_range(row: usize, column: usize) -> Range { - let point = DisplayPoint::new(row as u32, column as u32); - point..point -} - -fn assert_selection_ranges(marked_text: &str, view: &mut Editor, cx: &mut ViewContext) { - let (text, ranges) = marked_text_ranges(marked_text, true); - assert_eq!(view.text(cx), text); - assert_eq!( - view.selections.ranges(cx), - ranges, - "Assert selections are {}", - marked_text - ); -} - -/// Handle completion request passing a marked string specifying where the completion -/// should be triggered from using '|' character, what range should be replaced, and what completions -/// should be returned using '<' and '>' to delimit the range -pub fn handle_completion_request<'a>( - cx: &mut EditorLspTestContext<'a>, - marked_string: &str, - completions: Vec<&'static str>, -) -> impl Future { - let complete_from_marker: TextRangeMarker = '|'.into(); - let replace_range_marker: TextRangeMarker = ('<', '>').into(); - let (_, mut marked_ranges) = marked_text_ranges_by( - marked_string, - vec![complete_from_marker.clone(), replace_range_marker.clone()], - ); - - let complete_from_position = - cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start); - let replace_range = - cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone()); - - let mut request = cx.handle_request::(move |url, params, _| { - let completions = completions.clone(); - async move { - assert_eq!(params.text_document_position.text_document.uri, url.clone()); - assert_eq!( - params.text_document_position.position, - complete_from_position - ); - Ok(Some(lsp::CompletionResponse::Array( - completions - .iter() - .map(|completion_text| lsp::CompletionItem { - label: completion_text.to_string(), - text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { - range: replace_range, - new_text: completion_text.to_string(), - })), - ..Default::default() - }) - .collect(), - ))) - } - }); - - async move { - request.next().await; - } -} - -fn handle_resolve_completion_request<'a>( - cx: &mut EditorLspTestContext<'a>, - edits: Option>, -) -> impl Future { - let edits = edits.map(|edits| { - edits - .iter() - .map(|(marked_string, new_text)| { - let (_, marked_ranges) = marked_text_ranges(marked_string, false); - let replace_range = cx.to_lsp_range(marked_ranges[0].clone()); - lsp::TextEdit::new(replace_range, new_text.to_string()) - }) - .collect::>() - }); - - let mut request = - cx.handle_request::(move |_, _, _| { - let edits = edits.clone(); - async move { - Ok(lsp::CompletionItem { - additional_text_edits: edits, - ..Default::default() - }) - } - }); - - async move { - request.next().await; - } -} - -fn handle_copilot_completion_request( - lsp: &lsp::FakeLanguageServer, - completions: Vec, - completions_cycling: Vec, -) { - lsp.handle_request::(move |_params, _cx| { - let completions = completions.clone(); - async move { - Ok(copilot::request::GetCompletionsResult { - completions: completions.clone(), - }) - } - }); - lsp.handle_request::(move |_params, _cx| { - let completions_cycling = completions_cycling.clone(); - async move { - Ok(copilot::request::GetCompletionsResult { - completions: completions_cycling.clone(), - }) - } - }); -} - -pub(crate) fn update_test_language_settings( - cx: &mut TestAppContext, - f: impl Fn(&mut AllLanguageSettingsContent), -) { - cx.update(|cx| { - cx.update_global::(|store, cx| { - store.update_user_settings::(cx, f); - }); - }); -} - -pub(crate) fn update_test_project_settings( - cx: &mut TestAppContext, - f: impl Fn(&mut ProjectSettings), -) { - cx.update(|cx| { - cx.update_global::(|store, cx| { - store.update_user_settings::(cx, f); - }); - }); -} - -pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) { - cx.foreground().forbid_parking(); - - cx.update(|cx| { - cx.set_global(SettingsStore::test(cx)); - theme::init((), cx); - client::init_settings(cx); - language::init(cx); - Project::init_settings(cx); - workspace::init_settings(cx); - crate::init(cx); - }); - - update_test_language_settings(cx, f); -} +// use super::*; +// use crate::{ +// scroll::scroll_amount::ScrollAmount, +// test::{ +// assert_text_with_selections, build_editor, editor_lsp_test_context::EditorLspTestContext, +// editor_test_context::EditorTestContext, select_ranges, +// }, +// JoinLines, +// }; +// use drag_and_drop::DragAndDrop; +// use futures::StreamExt; +// use gpui::{ +// executor::Deterministic, +// geometry::{rect::RectF, vector::vec2f}, +// platform::{WindowBounds, WindowOptions}, +// serde_json::{self, json}, +// TestAppContext, +// }; +// use indoc::indoc; +// use language::{ +// language_settings::{AllLanguageSettings, AllLanguageSettingsContent, LanguageSettingsContent}, +// BracketPairConfig, FakeLspAdapter, LanguageConfig, LanguageConfigOverride, LanguageRegistry, +// Override, Point, +// }; +// use parking_lot::Mutex; +// use project::project_settings::{LspSettings, ProjectSettings}; +// use project::FakeFs; +// use std::sync::atomic; +// use std::sync::atomic::AtomicUsize; +// use std::{cell::RefCell, future::Future, rc::Rc, time::Instant}; +// use unindent::Unindent; +// use util::{ +// assert_set_eq, +// test::{marked_text_ranges, marked_text_ranges_by, sample_text, TextRangeMarker}, +// }; +// use workspace::{ +// item::{FollowableItem, Item, ItemHandle}, +// NavigationEntry, ViewId, +// }; + +// #[gpui::test] +// fn test_edit_events(cx: &mut TestAppContext) { +// init_test(cx, |_| {}); + +// let buffer = cx.add_model(|cx| { +// let mut buffer = language::Buffer::new(0, cx.model_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| { +// cx.subscribe(&cx.handle(), move |_, _, event, _| { +// if matches!( +// event, +// Event::Edited | Event::BufferEdited | Event::DirtyChanged +// ) { +// events.borrow_mut().push(("editor1", event.clone())); +// } +// }) +// .detach(); +// Editor::for_buffer(buffer.clone(), None, cx) +// } +// }) +// .root(cx); +// let editor2 = cx +// .add_window({ +// let events = events.clone(); +// |cx| { +// cx.subscribe(&cx.handle(), move |_, _, event, _| { +// if matches!( +// event, +// Event::Edited | Event::BufferEdited | Event::DirtyChanged +// ) { +// events.borrow_mut().push(("editor2", event.clone())); +// } +// }) +// .detach(); +// Editor::for_buffer(buffer.clone(), None, cx) +// } +// }) +// .root(cx); +// 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), +// ("editor1", Event::DirtyChanged), +// ("editor2", Event::DirtyChanged) +// ] +// ); + +// // 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), +// ] +// ); + +// // 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), +// ("editor1", Event::DirtyChanged), +// ("editor2", Event::DirtyChanged), +// ] +// ); + +// // 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), +// ("editor1", Event::DirtyChanged), +// ("editor2", Event::DirtyChanged), +// ] +// ); + +// // 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), +// ("editor1", Event::DirtyChanged), +// ("editor2", Event::DirtyChanged), +// ] +// ); + +// // 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), +// ("editor1", Event::DirtyChanged), +// ("editor2", Event::DirtyChanged), +// ] +// ); + +// // 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()), []); +// } + +// #[gpui::test] +// fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) { +// init_test(cx, |_| {}); + +// let mut now = Instant::now(); +// let buffer = cx.add_model(|cx| language::Buffer::new(0, cx.model_id() as u64, "123456")); +// let group_interval = buffer.read_with(cx, |buffer, _| buffer.transaction_group_interval()); +// let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); +// let editor = cx +// .add_window(|cx| build_editor(buffer.clone(), cx)) +// .root(cx); + +// editor.update(cx, |editor, cx| { +// editor.start_transaction_at(now, cx); +// editor.change_selections(None, cx, |s| s.select_ranges([2..4])); + +// editor.insert("cd", cx); +// editor.end_transaction_at(now, cx); +// assert_eq!(editor.text(cx), "12cd56"); +// assert_eq!(editor.selections.ranges(cx), vec![4..4]); + +// editor.start_transaction_at(now, cx); +// editor.change_selections(None, cx, |s| s.select_ranges([4..5])); +// editor.insert("e", cx); +// editor.end_transaction_at(now, cx); +// assert_eq!(editor.text(cx), "12cde6"); +// assert_eq!(editor.selections.ranges(cx), vec![5..5]); + +// now += group_interval + Duration::from_millis(1); +// editor.change_selections(None, cx, |s| s.select_ranges([2..2])); + +// // Simulate an edit in another editor +// buffer.update(cx, |buffer, cx| { +// buffer.start_transaction_at(now, cx); +// buffer.edit([(0..1, "a")], None, cx); +// buffer.edit([(1..1, "b")], None, cx); +// buffer.end_transaction_at(now, cx); +// }); + +// assert_eq!(editor.text(cx), "ab2cde6"); +// assert_eq!(editor.selections.ranges(cx), vec![3..3]); + +// // Last transaction happened past the group interval in a different editor. +// // Undo it individually and don't restore selections. +// editor.undo(&Undo, cx); +// assert_eq!(editor.text(cx), "12cde6"); +// assert_eq!(editor.selections.ranges(cx), vec![2..2]); + +// // First two transactions happened within the group interval in this editor. +// // Undo them together and restore selections. +// editor.undo(&Undo, cx); +// editor.undo(&Undo, cx); // Undo stack is empty here, so this is a no-op. +// assert_eq!(editor.text(cx), "123456"); +// assert_eq!(editor.selections.ranges(cx), vec![0..0]); + +// // Redo the first two transactions together. +// editor.redo(&Redo, cx); +// assert_eq!(editor.text(cx), "12cde6"); +// assert_eq!(editor.selections.ranges(cx), vec![5..5]); + +// // Redo the last transaction on its own. +// editor.redo(&Redo, cx); +// assert_eq!(editor.text(cx), "ab2cde6"); +// assert_eq!(editor.selections.ranges(cx), vec![6..6]); + +// // Test empty transactions. +// editor.start_transaction_at(now, cx); +// editor.end_transaction_at(now, cx); +// editor.undo(&Undo, cx); +// assert_eq!(editor.text(cx), "12cde6"); +// }); +// } + +// #[gpui::test] +// fn test_ime_composition(cx: &mut TestAppContext) { +// init_test(cx, |_| {}); + +// let buffer = cx.add_model(|cx| { +// let mut buffer = language::Buffer::new(0, cx.model_id() as u64, "abcde"); +// // Ensure automatic grouping doesn't occur. +// buffer.set_group_interval(Duration::ZERO); +// buffer +// }); + +// let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); +// cx.add_window(|cx| { +// let mut editor = build_editor(buffer.clone(), cx); + +// // Start a new IME composition. +// editor.replace_and_mark_text_in_range(Some(0..1), "à", None, cx); +// editor.replace_and_mark_text_in_range(Some(0..1), "á", None, cx); +// editor.replace_and_mark_text_in_range(Some(0..1), "ä", None, cx); +// assert_eq!(editor.text(cx), "äbcde"); +// assert_eq!( +// editor.marked_text_ranges(cx), +// Some(vec![OffsetUtf16(0)..OffsetUtf16(1)]) +// ); + +// // Finalize IME composition. +// editor.replace_text_in_range(None, "ā", cx); +// assert_eq!(editor.text(cx), "ābcde"); +// assert_eq!(editor.marked_text_ranges(cx), None); + +// // IME composition edits are grouped and are undone/redone at once. +// editor.undo(&Default::default(), cx); +// assert_eq!(editor.text(cx), "abcde"); +// assert_eq!(editor.marked_text_ranges(cx), None); +// editor.redo(&Default::default(), cx); +// assert_eq!(editor.text(cx), "ābcde"); +// assert_eq!(editor.marked_text_ranges(cx), None); + +// // Start a new IME composition. +// editor.replace_and_mark_text_in_range(Some(0..1), "à", None, cx); +// assert_eq!( +// editor.marked_text_ranges(cx), +// Some(vec![OffsetUtf16(0)..OffsetUtf16(1)]) +// ); + +// // Undoing during an IME composition cancels it. +// editor.undo(&Default::default(), cx); +// assert_eq!(editor.text(cx), "ābcde"); +// assert_eq!(editor.marked_text_ranges(cx), None); + +// // Start a new IME composition with an invalid marked range, ensuring it gets clipped. +// editor.replace_and_mark_text_in_range(Some(4..999), "è", None, cx); +// assert_eq!(editor.text(cx), "ābcdè"); +// assert_eq!( +// editor.marked_text_ranges(cx), +// Some(vec![OffsetUtf16(4)..OffsetUtf16(5)]) +// ); + +// // Finalize IME composition with an invalid replacement range, ensuring it gets clipped. +// editor.replace_text_in_range(Some(4..999), "ę", cx); +// assert_eq!(editor.text(cx), "ābcdę"); +// assert_eq!(editor.marked_text_ranges(cx), None); + +// // Start a new IME composition with multiple cursors. +// editor.change_selections(None, cx, |s| { +// s.select_ranges([ +// OffsetUtf16(1)..OffsetUtf16(1), +// OffsetUtf16(3)..OffsetUtf16(3), +// OffsetUtf16(5)..OffsetUtf16(5), +// ]) +// }); +// editor.replace_and_mark_text_in_range(Some(4..5), "XYZ", None, cx); +// assert_eq!(editor.text(cx), "XYZbXYZdXYZ"); +// assert_eq!( +// editor.marked_text_ranges(cx), +// Some(vec![ +// OffsetUtf16(0)..OffsetUtf16(3), +// OffsetUtf16(4)..OffsetUtf16(7), +// OffsetUtf16(8)..OffsetUtf16(11) +// ]) +// ); + +// // Ensure the newly-marked range gets treated as relative to the previously-marked ranges. +// editor.replace_and_mark_text_in_range(Some(1..2), "1", None, cx); +// assert_eq!(editor.text(cx), "X1ZbX1ZdX1Z"); +// assert_eq!( +// editor.marked_text_ranges(cx), +// Some(vec![ +// OffsetUtf16(1)..OffsetUtf16(2), +// OffsetUtf16(5)..OffsetUtf16(6), +// OffsetUtf16(9)..OffsetUtf16(10) +// ]) +// ); + +// // Finalize IME composition with multiple cursors. +// editor.replace_text_in_range(Some(9..10), "2", cx); +// assert_eq!(editor.text(cx), "X2ZbX2ZdX2Z"); +// assert_eq!(editor.marked_text_ranges(cx), None); + +// editor +// }); +// } + +// #[gpui::test] +// fn test_selection_with_mouse(cx: &mut TestAppContext) { +// init_test(cx, |_| {}); + +// let editor = cx +// .add_window(|cx| { +// let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx); +// build_editor(buffer, cx) +// }) +// .root(cx); +// editor.update(cx, |view, cx| { +// view.begin_selection(DisplayPoint::new(2, 2), false, 1, cx); +// }); +// assert_eq!( +// editor.update(cx, |view, cx| view.selections.display_ranges(cx)), +// [DisplayPoint::new(2, 2)..DisplayPoint::new(2, 2)] +// ); + +// editor.update(cx, |view, cx| { +// view.update_selection(DisplayPoint::new(3, 3), 0, Point::zero(), cx); +// }); + +// assert_eq!( +// editor.update(cx, |view, cx| view.selections.display_ranges(cx)), +// [DisplayPoint::new(2, 2)..DisplayPoint::new(3, 3)] +// ); + +// editor.update(cx, |view, cx| { +// view.update_selection(DisplayPoint::new(1, 1), 0, Point::zero(), cx); +// }); + +// assert_eq!( +// editor.update(cx, |view, cx| view.selections.display_ranges(cx)), +// [DisplayPoint::new(2, 2)..DisplayPoint::new(1, 1)] +// ); + +// editor.update(cx, |view, cx| { +// view.end_selection(cx); +// view.update_selection(DisplayPoint::new(3, 3), 0, Point::zero(), cx); +// }); + +// assert_eq!( +// editor.update(cx, |view, cx| view.selections.display_ranges(cx)), +// [DisplayPoint::new(2, 2)..DisplayPoint::new(1, 1)] +// ); + +// editor.update(cx, |view, cx| { +// view.begin_selection(DisplayPoint::new(3, 3), true, 1, cx); +// view.update_selection(DisplayPoint::new(0, 0), 0, Point::zero(), cx); +// }); + +// assert_eq!( +// editor.update(cx, |view, cx| view.selections.display_ranges(cx)), +// [ +// DisplayPoint::new(2, 2)..DisplayPoint::new(1, 1), +// DisplayPoint::new(3, 3)..DisplayPoint::new(0, 0) +// ] +// ); + +// editor.update(cx, |view, cx| { +// view.end_selection(cx); +// }); + +// assert_eq!( +// editor.update(cx, |view, cx| view.selections.display_ranges(cx)), +// [DisplayPoint::new(3, 3)..DisplayPoint::new(0, 0)] +// ); +// } + +// #[gpui::test] +// fn test_canceling_pending_selection(cx: &mut TestAppContext) { +// init_test(cx, |_| {}); + +// let view = cx +// .add_window(|cx| { +// let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx); +// build_editor(buffer, cx) +// }) +// .root(cx); + +// view.update(cx, |view, cx| { +// view.begin_selection(DisplayPoint::new(2, 2), false, 1, cx); +// assert_eq!( +// view.selections.display_ranges(cx), +// [DisplayPoint::new(2, 2)..DisplayPoint::new(2, 2)] +// ); +// }); + +// view.update(cx, |view, cx| { +// view.update_selection(DisplayPoint::new(3, 3), 0, Point::zero(), cx); +// assert_eq!( +// view.selections.display_ranges(cx), +// [DisplayPoint::new(2, 2)..DisplayPoint::new(3, 3)] +// ); +// }); + +// view.update(cx, |view, cx| { +// view.cancel(&Cancel, cx); +// view.update_selection(DisplayPoint::new(1, 1), 0, Point::zero(), cx); +// assert_eq!( +// view.selections.display_ranges(cx), +// [DisplayPoint::new(2, 2)..DisplayPoint::new(3, 3)] +// ); +// }); +// } + +// #[gpui::test] +// fn test_clone(cx: &mut TestAppContext) { +// init_test(cx, |_| {}); + +// let (text, selection_ranges) = marked_text_ranges( +// indoc! {" +// one +// two +// threeˇ +// four +// fiveˇ +// "}, +// true, +// ); + +// let editor = cx +// .add_window(|cx| { +// let buffer = MultiBuffer::build_simple(&text, cx); +// build_editor(buffer, cx) +// }) +// .root(cx); + +// editor.update(cx, |editor, cx| { +// editor.change_selections(None, cx, |s| s.select_ranges(selection_ranges.clone())); +// editor.fold_ranges( +// [ +// Point::new(1, 0)..Point::new(2, 0), +// Point::new(3, 0)..Point::new(4, 0), +// ], +// true, +// cx, +// ); +// }); + +// let cloned_editor = editor +// .update(cx, |editor, cx| { +// cx.add_window(Default::default(), |cx| editor.clone(cx)) +// }) +// .root(cx); + +// let snapshot = editor.update(cx, |e, cx| e.snapshot(cx)); +// let cloned_snapshot = cloned_editor.update(cx, |e, cx| e.snapshot(cx)); + +// assert_eq!( +// cloned_editor.update(cx, |e, cx| e.display_text(cx)), +// editor.update(cx, |e, cx| e.display_text(cx)) +// ); +// assert_eq!( +// cloned_snapshot +// .folds_in_range(0..text.len()) +// .collect::>(), +// snapshot.folds_in_range(0..text.len()).collect::>(), +// ); +// assert_set_eq!( +// cloned_editor.read_with(cx, |editor, cx| editor.selections.ranges::(cx)), +// editor.read_with(cx, |editor, cx| editor.selections.ranges(cx)) +// ); +// assert_set_eq!( +// cloned_editor.update(cx, |e, cx| e.selections.display_ranges(cx)), +// editor.update(cx, |e, cx| e.selections.display_ranges(cx)) +// ); +// } + +// #[gpui::test] +// async fn test_navigation_history(cx: &mut TestAppContext) { +// init_test(cx, |_| {}); + +// cx.set_global(DragAndDrop::::default()); +// use workspace::item::Item; + +// let fs = FakeFs::new(cx.background()); +// let project = Project::test(fs, [], cx).await; +// let window = cx.add_window(|cx| Workspace::test_new(project, cx)); +// let workspace = window.root(cx); +// let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); +// window.add_view(cx, |cx| { +// let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx); +// let mut editor = build_editor(buffer.clone(), cx); +// let handle = cx.handle(); +// 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.view_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.view_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(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(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), +// vec2f(0., editor.max_point(cx).row() as f32) +// ); + +// editor +// }); +// } + +// #[gpui::test] +// fn test_cancel(cx: &mut TestAppContext) { +// init_test(cx, |_| {}); + +// let view = cx +// .add_window(|cx| { +// let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx); +// build_editor(buffer, cx) +// }) +// .root(cx); + +// view.update(cx, |view, cx| { +// view.begin_selection(DisplayPoint::new(3, 4), false, 1, cx); +// view.update_selection(DisplayPoint::new(1, 1), 0, Point::zero(), cx); +// view.end_selection(cx); + +// view.begin_selection(DisplayPoint::new(0, 1), true, 1, cx); +// view.update_selection(DisplayPoint::new(0, 3), 0, Point::zero(), cx); +// view.end_selection(cx); +// assert_eq!( +// view.selections.display_ranges(cx), +// [ +// DisplayPoint::new(0, 1)..DisplayPoint::new(0, 3), +// DisplayPoint::new(3, 4)..DisplayPoint::new(1, 1), +// ] +// ); +// }); + +// view.update(cx, |view, cx| { +// view.cancel(&Cancel, cx); +// assert_eq!( +// view.selections.display_ranges(cx), +// [DisplayPoint::new(3, 4)..DisplayPoint::new(1, 1)] +// ); +// }); + +// view.update(cx, |view, cx| { +// view.cancel(&Cancel, cx); +// assert_eq!( +// view.selections.display_ranges(cx), +// [DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1)] +// ); +// }); +// } + +// #[gpui::test] +// fn test_fold_action(cx: &mut TestAppContext) { +// init_test(cx, |_| {}); + +// let view = cx +// .add_window(|cx| { +// let buffer = MultiBuffer::build_simple( +// &" +// impl Foo { +// // Hello! + +// fn a() { +// 1 +// } + +// fn b() { +// 2 +// } + +// fn c() { +// 3 +// } +// } +// " +// .unindent(), +// cx, +// ); +// build_editor(buffer.clone(), cx) +// }) +// .root(cx); + +// view.update(cx, |view, cx| { +// view.change_selections(None, cx, |s| { +// s.select_display_ranges([DisplayPoint::new(8, 0)..DisplayPoint::new(12, 0)]); +// }); +// view.fold(&Fold, cx); +// assert_eq!( +// view.display_text(cx), +// " +// impl Foo { +// // Hello! + +// fn a() { +// 1 +// } + +// fn b() {⋯ +// } + +// fn c() {⋯ +// } +// } +// " +// .unindent(), +// ); + +// view.fold(&Fold, cx); +// assert_eq!( +// view.display_text(cx), +// " +// impl Foo {⋯ +// } +// " +// .unindent(), +// ); + +// view.unfold_lines(&UnfoldLines, cx); +// assert_eq!( +// view.display_text(cx), +// " +// impl Foo { +// // Hello! + +// fn a() { +// 1 +// } + +// fn b() {⋯ +// } + +// fn c() {⋯ +// } +// } +// " +// .unindent(), +// ); + +// view.unfold_lines(&UnfoldLines, cx); +// assert_eq!(view.display_text(cx), view.buffer.read(cx).read(cx).text()); +// }); +// } + +// #[gpui::test] +// fn test_move_cursor(cx: &mut TestAppContext) { +// init_test(cx, |_| {}); + +// let buffer = cx.update(|cx| MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx)); +// let view = cx +// .add_window(|cx| build_editor(buffer.clone(), cx)) +// .root(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"), +// ], +// None, +// cx, +// ); +// }); +// view.update(cx, |view, cx| { +// assert_eq!( +// view.selections.display_ranges(cx), +// &[DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)] +// ); + +// view.move_down(&MoveDown, cx); +// assert_eq!( +// view.selections.display_ranges(cx), +// &[DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)] +// ); + +// view.move_right(&MoveRight, cx); +// assert_eq!( +// view.selections.display_ranges(cx), +// &[DisplayPoint::new(1, 4)..DisplayPoint::new(1, 4)] +// ); + +// view.move_left(&MoveLeft, cx); +// assert_eq!( +// view.selections.display_ranges(cx), +// &[DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)] +// ); + +// view.move_up(&MoveUp, cx); +// assert_eq!( +// view.selections.display_ranges(cx), +// &[DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)] +// ); + +// view.move_to_end(&MoveToEnd, cx); +// assert_eq!( +// view.selections.display_ranges(cx), +// &[DisplayPoint::new(5, 6)..DisplayPoint::new(5, 6)] +// ); + +// view.move_to_beginning(&MoveToBeginning, cx); +// assert_eq!( +// view.selections.display_ranges(cx), +// &[DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)] +// ); + +// view.change_selections(None, cx, |s| { +// s.select_display_ranges([DisplayPoint::new(0, 1)..DisplayPoint::new(0, 2)]); +// }); +// view.select_to_beginning(&SelectToBeginning, cx); +// assert_eq!( +// view.selections.display_ranges(cx), +// &[DisplayPoint::new(0, 1)..DisplayPoint::new(0, 0)] +// ); + +// view.select_to_end(&SelectToEnd, cx); +// assert_eq!( +// view.selections.display_ranges(cx), +// &[DisplayPoint::new(0, 1)..DisplayPoint::new(5, 6)] +// ); +// }); +// } + +// #[gpui::test] +// fn test_move_cursor_multibyte(cx: &mut TestAppContext) { +// init_test(cx, |_| {}); + +// let view = cx +// .add_window(|cx| { +// let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcde\nαβγδε", cx); +// build_editor(buffer.clone(), cx) +// }) +// .root(cx); + +// assert_eq!('ⓐ'.len_utf8(), 3); +// assert_eq!('α'.len_utf8(), 2); + +// view.update(cx, |view, cx| { +// view.fold_ranges( +// vec![ +// Point::new(0, 6)..Point::new(0, 12), +// Point::new(1, 2)..Point::new(1, 4), +// Point::new(2, 4)..Point::new(2, 8), +// ], +// true, +// cx, +// ); +// assert_eq!(view.display_text(cx), "ⓐⓑ⋯ⓔ\nab⋯e\nαβ⋯ε"); + +// view.move_right(&MoveRight, cx); +// assert_eq!( +// view.selections.display_ranges(cx), +// &[empty_range(0, "ⓐ".len())] +// ); +// view.move_right(&MoveRight, cx); +// assert_eq!( +// view.selections.display_ranges(cx), +// &[empty_range(0, "ⓐⓑ".len())] +// ); +// view.move_right(&MoveRight, cx); +// assert_eq!( +// view.selections.display_ranges(cx), +// &[empty_range(0, "ⓐⓑ⋯".len())] +// ); + +// view.move_down(&MoveDown, cx); +// assert_eq!( +// view.selections.display_ranges(cx), +// &[empty_range(1, "ab⋯e".len())] +// ); +// view.move_left(&MoveLeft, cx); +// assert_eq!( +// view.selections.display_ranges(cx), +// &[empty_range(1, "ab⋯".len())] +// ); +// view.move_left(&MoveLeft, cx); +// assert_eq!( +// view.selections.display_ranges(cx), +// &[empty_range(1, "ab".len())] +// ); +// view.move_left(&MoveLeft, cx); +// assert_eq!( +// view.selections.display_ranges(cx), +// &[empty_range(1, "a".len())] +// ); + +// view.move_down(&MoveDown, cx); +// assert_eq!( +// view.selections.display_ranges(cx), +// &[empty_range(2, "α".len())] +// ); +// view.move_right(&MoveRight, cx); +// assert_eq!( +// view.selections.display_ranges(cx), +// &[empty_range(2, "αβ".len())] +// ); +// view.move_right(&MoveRight, cx); +// assert_eq!( +// view.selections.display_ranges(cx), +// &[empty_range(2, "αβ⋯".len())] +// ); +// view.move_right(&MoveRight, 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(1, "ab⋯e".len())] +// ); +// view.move_down(&MoveDown, 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(1, "ab⋯e".len())] +// ); + +// view.move_up(&MoveUp, cx); +// assert_eq!( +// view.selections.display_ranges(cx), +// &[empty_range(0, "ⓐⓑ".len())] +// ); +// view.move_left(&MoveLeft, cx); +// assert_eq!( +// view.selections.display_ranges(cx), +// &[empty_range(0, "ⓐ".len())] +// ); +// view.move_left(&MoveLeft, cx); +// assert_eq!( +// view.selections.display_ranges(cx), +// &[empty_range(0, "".len())] +// ); +// }); +// } + +// #[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) +// }) +// .root(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(3, "abcd".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(2, "αβγ".len())] +// ); +// }); +// } + +// #[gpui::test] +// fn test_beginning_end_of_line(cx: &mut TestAppContext) { +// init_test(cx, |_| {}); + +// let view = cx +// .add_window(|cx| { +// let buffer = MultiBuffer::build_simple("abc\n def", cx); +// build_editor(buffer, cx) +// }) +// .root(cx); +// view.update(cx, |view, cx| { +// view.change_selections(None, cx, |s| { +// s.select_display_ranges([ +// DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1), +// DisplayPoint::new(1, 4)..DisplayPoint::new(1, 4), +// ]); +// }); +// }); + +// view.update(cx, |view, cx| { +// view.move_to_beginning_of_line(&MoveToBeginningOfLine, cx); +// assert_eq!( +// view.selections.display_ranges(cx), +// &[ +// DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0), +// DisplayPoint::new(1, 2)..DisplayPoint::new(1, 2), +// ] +// ); +// }); + +// view.update(cx, |view, cx| { +// view.move_to_beginning_of_line(&MoveToBeginningOfLine, cx); +// assert_eq!( +// view.selections.display_ranges(cx), +// &[ +// DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0), +// DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0), +// ] +// ); +// }); + +// view.update(cx, |view, cx| { +// view.move_to_beginning_of_line(&MoveToBeginningOfLine, cx); +// assert_eq!( +// view.selections.display_ranges(cx), +// &[ +// DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0), +// DisplayPoint::new(1, 2)..DisplayPoint::new(1, 2), +// ] +// ); +// }); + +// view.update(cx, |view, cx| { +// view.move_to_end_of_line(&MoveToEndOfLine, cx); +// assert_eq!( +// view.selections.display_ranges(cx), +// &[ +// DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3), +// DisplayPoint::new(1, 5)..DisplayPoint::new(1, 5), +// ] +// ); +// }); + +// // Moving to the end of line again is a no-op. +// view.update(cx, |view, cx| { +// view.move_to_end_of_line(&MoveToEndOfLine, cx); +// assert_eq!( +// view.selections.display_ranges(cx), +// &[ +// DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3), +// DisplayPoint::new(1, 5)..DisplayPoint::new(1, 5), +// ] +// ); +// }); + +// view.update(cx, |view, cx| { +// view.move_left(&MoveLeft, cx); +// view.select_to_beginning_of_line( +// &SelectToBeginningOfLine { +// stop_at_soft_wraps: true, +// }, +// cx, +// ); +// assert_eq!( +// view.selections.display_ranges(cx), +// &[ +// DisplayPoint::new(0, 2)..DisplayPoint::new(0, 0), +// DisplayPoint::new(1, 4)..DisplayPoint::new(1, 2), +// ] +// ); +// }); + +// view.update(cx, |view, cx| { +// view.select_to_beginning_of_line( +// &SelectToBeginningOfLine { +// stop_at_soft_wraps: true, +// }, +// cx, +// ); +// assert_eq!( +// view.selections.display_ranges(cx), +// &[ +// DisplayPoint::new(0, 2)..DisplayPoint::new(0, 0), +// DisplayPoint::new(1, 4)..DisplayPoint::new(1, 0), +// ] +// ); +// }); + +// view.update(cx, |view, cx| { +// view.select_to_beginning_of_line( +// &SelectToBeginningOfLine { +// stop_at_soft_wraps: true, +// }, +// cx, +// ); +// assert_eq!( +// view.selections.display_ranges(cx), +// &[ +// DisplayPoint::new(0, 2)..DisplayPoint::new(0, 0), +// DisplayPoint::new(1, 4)..DisplayPoint::new(1, 2), +// ] +// ); +// }); + +// view.update(cx, |view, cx| { +// view.select_to_end_of_line( +// &SelectToEndOfLine { +// stop_at_soft_wraps: true, +// }, +// cx, +// ); +// assert_eq!( +// view.selections.display_ranges(cx), +// &[ +// DisplayPoint::new(0, 2)..DisplayPoint::new(0, 3), +// DisplayPoint::new(1, 4)..DisplayPoint::new(1, 5), +// ] +// ); +// }); + +// view.update(cx, |view, cx| { +// view.delete_to_end_of_line(&DeleteToEndOfLine, cx); +// assert_eq!(view.display_text(cx), "ab\n de"); +// assert_eq!( +// view.selections.display_ranges(cx), +// &[ +// DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2), +// DisplayPoint::new(1, 4)..DisplayPoint::new(1, 4), +// ] +// ); +// }); + +// view.update(cx, |view, cx| { +// view.delete_to_beginning_of_line(&DeleteToBeginningOfLine, cx); +// assert_eq!(view.display_text(cx), "\n"); +// assert_eq!( +// view.selections.display_ranges(cx), +// &[ +// DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0), +// DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0), +// ] +// ); +// }); +// } + +// #[gpui::test] +// fn test_prev_next_word_boundary(cx: &mut TestAppContext) { +// init_test(cx, |_| {}); + +// let view = cx +// .add_window(|cx| { +// let buffer = MultiBuffer::build_simple("use std::str::{foo, bar}\n\n {baz.qux()}", cx); +// build_editor(buffer, cx) +// }) +// .root(cx); +// view.update(cx, |view, cx| { +// view.change_selections(None, cx, |s| { +// s.select_display_ranges([ +// DisplayPoint::new(0, 11)..DisplayPoint::new(0, 11), +// DisplayPoint::new(2, 4)..DisplayPoint::new(2, 4), +// ]) +// }); + +// view.move_to_previous_word_start(&MoveToPreviousWordStart, cx); +// assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", view, cx); + +// view.move_to_previous_word_start(&MoveToPreviousWordStart, cx); +// assert_selection_ranges("use stdˇ::str::{foo, bar}\n\n ˇ{baz.qux()}", view, cx); + +// view.move_to_previous_word_start(&MoveToPreviousWordStart, cx); +// assert_selection_ranges("use ˇstd::str::{foo, bar}\n\nˇ {baz.qux()}", view, cx); + +// view.move_to_previous_word_start(&MoveToPreviousWordStart, cx); +// assert_selection_ranges("ˇuse std::str::{foo, bar}\nˇ\n {baz.qux()}", view, cx); + +// view.move_to_previous_word_start(&MoveToPreviousWordStart, cx); +// assert_selection_ranges("ˇuse std::str::{foo, barˇ}\n\n {baz.qux()}", view, cx); + +// view.move_to_next_word_end(&MoveToNextWordEnd, cx); +// assert_selection_ranges("useˇ std::str::{foo, bar}ˇ\n\n {baz.qux()}", view, cx); + +// view.move_to_next_word_end(&MoveToNextWordEnd, cx); +// assert_selection_ranges("use stdˇ::str::{foo, bar}\nˇ\n {baz.qux()}", view, cx); + +// view.move_to_next_word_end(&MoveToNextWordEnd, cx); +// assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", view, cx); + +// view.move_right(&MoveRight, cx); +// view.select_to_previous_word_start(&SelectToPreviousWordStart, cx); +// assert_selection_ranges("use std::«ˇs»tr::{foo, bar}\n\n {«ˇb»az.qux()}", view, cx); + +// view.select_to_previous_word_start(&SelectToPreviousWordStart, cx); +// assert_selection_ranges("use std«ˇ::s»tr::{foo, bar}\n\n «ˇ{b»az.qux()}", view, cx); + +// view.select_to_next_word_end(&SelectToNextWordEnd, cx); +// assert_selection_ranges("use std::«ˇs»tr::{foo, bar}\n\n {«ˇb»az.qux()}", view, 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) +// }) +// .root(cx); + +// view.update(cx, |view, cx| { +// view.set_wrap_width(Some(140.), 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.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(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_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)] +// ); +// }); +// } + +// #[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(vec2f(100., 4. * line_height), &mut cx); + +// cx.set_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 + +// 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 + +// 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 +// ˇ +// 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 + +// three +// four +// five + +// 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(vec2f(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| { +// assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 0.)); +// editor.scroll_screen(&ScrollAmount::Page(1.), cx); +// assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 3.)); +// editor.scroll_screen(&ScrollAmount::Page(1.), cx); +// assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 6.)); +// editor.scroll_screen(&ScrollAmount::Page(-1.), cx); +// assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 3.)); + +// editor.scroll_screen(&ScrollAmount::Page(-0.5), cx); +// assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 1.)); +// editor.scroll_screen(&ScrollAmount::Page(0.5), cx); +// assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 3.)); +// }); +// } + +// #[gpui::test] +// async fn test_autoscroll(cx: &mut gpui::TestAppContext) { +// init_test(cx, |_| {}); +// let mut cx = EditorTestContext::new(cx).await; + +// let line_height = cx.update_editor(|editor, cx| { +// editor.set_vertical_scroll_margin(2, cx); +// editor.style(cx).text.line_height(cx.font_cache()) +// }); + +// let window = cx.window; +// window.simulate_resize(vec2f(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(), vec2f(0., 0.0)); +// }); + +// // 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(), vec2f(0., 3.0)); +// }); + +// // 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(), vec2f(0., 4.0)); +// }); + +// // 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(), vec2f(0., 1.0)); +// }); +// } + +// #[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(vec2f(100., 4. * line_height), &mut cx); + +// 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) { +// init_test(cx, |_| {}); +// let mut cx = EditorTestContext::new(cx).await; +// cx.set_state("one «two threeˇ» four"); +// cx.update_editor(|editor, cx| { +// editor.delete_to_beginning_of_line(&DeleteToBeginningOfLine, cx); +// assert_eq!(editor.text(cx), " four"); +// }); +// } + +// #[gpui::test] +// fn test_delete_to_word_boundary(cx: &mut TestAppContext) { +// init_test(cx, |_| {}); + +// let view = cx +// .add_window(|cx| { +// let buffer = MultiBuffer::build_simple("one two three four", cx); +// build_editor(buffer.clone(), cx) +// }) +// .root(cx); + +// view.update(cx, |view, cx| { +// view.change_selections(None, cx, |s| { +// s.select_display_ranges([ +// // an empty selection - the preceding word fragment is deleted +// DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2), +// // characters selected - they are deleted +// DisplayPoint::new(0, 9)..DisplayPoint::new(0, 12), +// ]) +// }); +// view.delete_to_previous_word_start(&DeleteToPreviousWordStart, cx); +// assert_eq!(view.buffer.read(cx).read(cx).text(), "e two te four"); +// }); + +// view.update(cx, |view, cx| { +// view.change_selections(None, cx, |s| { +// s.select_display_ranges([ +// // an empty selection - the following word fragment is deleted +// DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3), +// // characters selected - they are deleted +// DisplayPoint::new(0, 9)..DisplayPoint::new(0, 10), +// ]) +// }); +// view.delete_to_next_word_end(&DeleteToNextWordEnd, cx); +// assert_eq!(view.buffer.read(cx).read(cx).text(), "e t te our"); +// }); +// } + +// #[gpui::test] +// fn test_newline(cx: &mut TestAppContext) { +// init_test(cx, |_| {}); + +// let view = cx +// .add_window(|cx| { +// let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx); +// build_editor(buffer.clone(), cx) +// }) +// .root(cx); + +// view.update(cx, |view, cx| { +// view.change_selections(None, cx, |s| { +// s.select_display_ranges([ +// DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2), +// DisplayPoint::new(1, 2)..DisplayPoint::new(1, 2), +// DisplayPoint::new(1, 6)..DisplayPoint::new(1, 6), +// ]) +// }); + +// view.newline(&Newline, cx); +// assert_eq!(view.text(cx), "aa\naa\n \n bb\n bb\n"); +// }); +// } + +// #[gpui::test] +// fn test_newline_with_old_selections(cx: &mut TestAppContext) { +// init_test(cx, |_| {}); + +// let editor = cx +// .add_window(|cx| { +// let buffer = MultiBuffer::build_simple( +// " +// a +// b( +// X +// ) +// c( +// X +// ) +// " +// .unindent() +// .as_str(), +// cx, +// ); +// let mut editor = build_editor(buffer.clone(), cx); +// editor.change_selections(None, cx, |s| { +// s.select_ranges([ +// Point::new(2, 4)..Point::new(2, 5), +// Point::new(5, 4)..Point::new(5, 5), +// ]) +// }); +// editor +// }) +// .root(cx); + +// editor.update(cx, |editor, cx| { +// // Edit the buffer directly, deleting ranges surrounding the editor's selections +// editor.buffer.update(cx, |buffer, cx| { +// buffer.edit( +// [ +// (Point::new(1, 2)..Point::new(3, 0), ""), +// (Point::new(4, 2)..Point::new(6, 0), ""), +// ], +// None, +// cx, +// ); +// assert_eq!( +// buffer.read(cx).text(), +// " +// a +// b() +// c() +// " +// .unindent() +// ); +// }); +// assert_eq!( +// editor.selections.ranges(cx), +// &[ +// Point::new(1, 2)..Point::new(1, 2), +// Point::new(2, 2)..Point::new(2, 2), +// ], +// ); + +// editor.newline(&Newline, cx); +// assert_eq!( +// editor.text(cx), +// " +// a +// b( +// ) +// c( +// ) +// " +// .unindent() +// ); + +// // The selections are moved after the inserted newlines +// assert_eq!( +// editor.selections.ranges(cx), +// &[ +// Point::new(2, 0)..Point::new(2, 0), +// Point::new(4, 0)..Point::new(4, 0), +// ], +// ); +// }); +// } + +// #[gpui::test] +// async fn test_newline_above(cx: &mut gpui::TestAppContext) { +// init_test(cx, |settings| { +// settings.defaults.tab_size = NonZeroU32::new(4) +// }); + +// let language = Arc::new( +// Language::new( +// LanguageConfig::default(), +// Some(tree_sitter_rust::language()), +// ) +// .with_indents_query(r#"(_ "(" ")" @end) @indent"#) +// .unwrap(), +// ); + +// let mut cx = EditorTestContext::new(cx).await; +// cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx)); +// cx.set_state(indoc! {" +// const a: ˇA = ( +// (ˇ +// «const_functionˇ»(ˇ), +// so«mˇ»et«hˇ»ing_ˇelse,ˇ +// )ˇ +// ˇ);ˇ +// "}); + +// cx.update_editor(|e, cx| e.newline_above(&NewlineAbove, cx)); +// cx.assert_editor_state(indoc! {" +// ˇ +// const a: A = ( +// ˇ +// ( +// ˇ +// ˇ +// const_function(), +// ˇ +// ˇ +// ˇ +// ˇ +// something_else, +// ˇ +// ) +// ˇ +// ˇ +// ); +// "}); +// } + +// #[gpui::test] +// async fn test_newline_below(cx: &mut gpui::TestAppContext) { +// init_test(cx, |settings| { +// settings.defaults.tab_size = NonZeroU32::new(4) +// }); + +// let language = Arc::new( +// Language::new( +// LanguageConfig::default(), +// Some(tree_sitter_rust::language()), +// ) +// .with_indents_query(r#"(_ "(" ")" @end) @indent"#) +// .unwrap(), +// ); + +// let mut cx = EditorTestContext::new(cx).await; +// cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx)); +// cx.set_state(indoc! {" +// const a: ˇA = ( +// (ˇ +// «const_functionˇ»(ˇ), +// so«mˇ»et«hˇ»ing_ˇelse,ˇ +// )ˇ +// ˇ);ˇ +// "}); + +// cx.update_editor(|e, cx| e.newline_below(&NewlineBelow, cx)); +// cx.assert_editor_state(indoc! {" +// const a: A = ( +// ˇ +// ( +// ˇ +// const_function(), +// ˇ +// ˇ +// something_else, +// ˇ +// ˇ +// ˇ +// ˇ +// ) +// ˇ +// ); +// ˇ +// ˇ +// "}); +// } + +// #[gpui::test] +// async fn test_newline_comments(cx: &mut gpui::TestAppContext) { +// init_test(cx, |settings| { +// settings.defaults.tab_size = NonZeroU32::new(4) +// }); + +// let language = Arc::new(Language::new( +// LanguageConfig { +// line_comment: Some("//".into()), +// ..LanguageConfig::default() +// }, +// None, +// )); +// { +// let mut cx = EditorTestContext::new(cx).await; +// cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx)); +// cx.set_state(indoc! {" +// // Fooˇ +// "}); + +// cx.update_editor(|e, cx| e.newline(&Newline, cx)); +// cx.assert_editor_state(indoc! {" +// // Foo +// //ˇ +// "}); +// // Ensure that if cursor is before the comment start, we do not actually insert a comment prefix. +// cx.set_state(indoc! {" +// ˇ// Foo +// "}); +// cx.update_editor(|e, cx| e.newline(&Newline, cx)); +// cx.assert_editor_state(indoc! {" + +// ˇ// Foo +// "}); +// } +// // Ensure that comment continuations can be disabled. +// update_test_language_settings(cx, |settings| { +// settings.defaults.extend_comment_on_newline = Some(false); +// }); +// let mut cx = EditorTestContext::new(cx).await; +// cx.set_state(indoc! {" +// // Fooˇ +// "}); +// cx.update_editor(|e, cx| e.newline(&Newline, cx)); +// cx.assert_editor_state(indoc! {" +// // Foo +// ˇ +// "}); +// } + +// #[gpui::test] +// fn test_insert_with_old_selections(cx: &mut TestAppContext) { +// init_test(cx, |_| {}); + +// let editor = cx +// .add_window(|cx| { +// let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx); +// let mut editor = build_editor(buffer.clone(), cx); +// editor.change_selections(None, cx, |s| s.select_ranges([3..4, 11..12, 19..20])); +// editor +// }) +// .root(cx); + +// editor.update(cx, |editor, cx| { +// // Edit the buffer directly, deleting ranges surrounding the editor's selections +// editor.buffer.update(cx, |buffer, cx| { +// buffer.edit([(2..5, ""), (10..13, ""), (18..21, "")], None, cx); +// assert_eq!(buffer.read(cx).text(), "a(), b(), c()".unindent()); +// }); +// assert_eq!(editor.selections.ranges(cx), &[2..2, 7..7, 12..12],); + +// editor.insert("Z", cx); +// assert_eq!(editor.text(cx), "a(Z), b(Z), c(Z)"); + +// // The selections are moved after the inserted characters +// assert_eq!(editor.selections.ranges(cx), &[3..3, 9..9, 15..15],); +// }); +// } + +// #[gpui::test] +// async fn test_tab(cx: &mut gpui::TestAppContext) { +// init_test(cx, |settings| { +// settings.defaults.tab_size = NonZeroU32::new(3) +// }); + +// let mut cx = EditorTestContext::new(cx).await; +// cx.set_state(indoc! {" +// ˇabˇc +// ˇ🏀ˇ🏀ˇefg +// dˇ +// "}); +// cx.update_editor(|e, cx| e.tab(&Tab, cx)); +// cx.assert_editor_state(indoc! {" +// ˇab ˇc +// ˇ🏀 ˇ🏀 ˇefg +// d ˇ +// "}); + +// cx.set_state(indoc! {" +// a +// «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ» +// "}); +// cx.update_editor(|e, cx| e.tab(&Tab, cx)); +// cx.assert_editor_state(indoc! {" +// a +// «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ» +// "}); +// } + +// #[gpui::test] +// async fn test_tab_in_leading_whitespace_auto_indents_lines(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()), +// ) +// .with_indents_query(r#"(_ "(" ")" @end) @indent"#) +// .unwrap(), +// ); +// cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx)); + +// // cursors that are already at the suggested indent level insert +// // a soft tab. cursors that are to the left of the suggested indent +// // auto-indent their line. +// cx.set_state(indoc! {" +// ˇ +// const a: B = ( +// c( +// d( +// ˇ +// ) +// ˇ +// ˇ ) +// ); +// "}); +// cx.update_editor(|e, cx| e.tab(&Tab, cx)); +// cx.assert_editor_state(indoc! {" +// ˇ +// const a: B = ( +// c( +// d( +// ˇ +// ) +// ˇ +// ˇ) +// ); +// "}); + +// // handle auto-indent when there are multiple cursors on the same line +// cx.set_state(indoc! {" +// const a: B = ( +// c( +// ˇ ˇ +// ˇ ) +// ); +// "}); +// cx.update_editor(|e, cx| e.tab(&Tab, cx)); +// cx.assert_editor_state(indoc! {" +// const a: B = ( +// c( +// ˇ +// ˇ) +// ); +// "}); +// } + +// #[gpui::test] +// async fn test_tab_with_mixed_whitespace(cx: &mut gpui::TestAppContext) { +// init_test(cx, |settings| { +// settings.defaults.tab_size = NonZeroU32::new(4) +// }); + +// let language = Arc::new( +// Language::new( +// LanguageConfig::default(), +// Some(tree_sitter_rust::language()), +// ) +// .with_indents_query(r#"(_ "{" "}" @end) @indent"#) +// .unwrap(), +// ); + +// let mut cx = EditorTestContext::new(cx).await; +// cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx)); +// cx.set_state(indoc! {" +// fn a() { +// if b { +// \t ˇc +// } +// } +// "}); + +// cx.update_editor(|e, cx| e.tab(&Tab, cx)); +// cx.assert_editor_state(indoc! {" +// fn a() { +// if b { +// ˇc +// } +// } +// "}); +// } + +// #[gpui::test] +// async fn test_indent_outdent(cx: &mut gpui::TestAppContext) { +// init_test(cx, |settings| { +// settings.defaults.tab_size = NonZeroU32::new(4); +// }); + +// let mut cx = EditorTestContext::new(cx).await; + +// cx.set_state(indoc! {" +// «oneˇ» «twoˇ» +// three +// four +// "}); +// cx.update_editor(|e, cx| e.tab(&Tab, cx)); +// cx.assert_editor_state(indoc! {" +// «oneˇ» «twoˇ» +// three +// four +// "}); + +// cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx)); +// cx.assert_editor_state(indoc! {" +// «oneˇ» «twoˇ» +// three +// four +// "}); + +// // select across line ending +// cx.set_state(indoc! {" +// one two +// t«hree +// ˇ» four +// "}); +// cx.update_editor(|e, cx| e.tab(&Tab, cx)); +// cx.assert_editor_state(indoc! {" +// one two +// t«hree +// ˇ» four +// "}); + +// cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx)); +// cx.assert_editor_state(indoc! {" +// one two +// t«hree +// ˇ» four +// "}); + +// // Ensure that indenting/outdenting works when the cursor is at column 0. +// cx.set_state(indoc! {" +// one two +// ˇthree +// four +// "}); +// cx.update_editor(|e, cx| e.tab(&Tab, cx)); +// cx.assert_editor_state(indoc! {" +// one two +// ˇthree +// four +// "}); + +// cx.set_state(indoc! {" +// one two +// ˇ three +// four +// "}); +// cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx)); +// cx.assert_editor_state(indoc! {" +// one two +// ˇthree +// four +// "}); +// } + +// #[gpui::test] +// async fn test_indent_outdent_with_hard_tabs(cx: &mut gpui::TestAppContext) { +// init_test(cx, |settings| { +// settings.defaults.hard_tabs = Some(true); +// }); + +// let mut cx = EditorTestContext::new(cx).await; + +// // select two ranges on one line +// cx.set_state(indoc! {" +// «oneˇ» «twoˇ» +// three +// four +// "}); +// cx.update_editor(|e, cx| e.tab(&Tab, cx)); +// cx.assert_editor_state(indoc! {" +// \t«oneˇ» «twoˇ» +// three +// four +// "}); +// cx.update_editor(|e, cx| e.tab(&Tab, cx)); +// cx.assert_editor_state(indoc! {" +// \t\t«oneˇ» «twoˇ» +// three +// four +// "}); +// cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx)); +// cx.assert_editor_state(indoc! {" +// \t«oneˇ» «twoˇ» +// three +// four +// "}); +// cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx)); +// cx.assert_editor_state(indoc! {" +// «oneˇ» «twoˇ» +// three +// four +// "}); + +// // select across a line ending +// cx.set_state(indoc! {" +// one two +// t«hree +// ˇ»four +// "}); +// cx.update_editor(|e, cx| e.tab(&Tab, cx)); +// cx.assert_editor_state(indoc! {" +// one two +// \tt«hree +// ˇ»four +// "}); +// cx.update_editor(|e, cx| e.tab(&Tab, cx)); +// cx.assert_editor_state(indoc! {" +// one two +// \t\tt«hree +// ˇ»four +// "}); +// cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx)); +// cx.assert_editor_state(indoc! {" +// one two +// \tt«hree +// ˇ»four +// "}); +// cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx)); +// cx.assert_editor_state(indoc! {" +// one two +// t«hree +// ˇ»four +// "}); + +// // Ensure that indenting/outdenting works when the cursor is at column 0. +// cx.set_state(indoc! {" +// one two +// ˇthree +// four +// "}); +// cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx)); +// cx.assert_editor_state(indoc! {" +// one two +// ˇthree +// four +// "}); +// cx.update_editor(|e, cx| e.tab(&Tab, cx)); +// cx.assert_editor_state(indoc! {" +// one two +// \tˇthree +// four +// "}); +// cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx)); +// cx.assert_editor_state(indoc! {" +// one two +// ˇthree +// four +// "}); +// } + +// #[gpui::test] +// fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) { +// init_test(cx, |settings| { +// settings.languages.extend([ +// ( +// "TOML".into(), +// LanguageSettingsContent { +// tab_size: NonZeroU32::new(2), +// ..Default::default() +// }, +// ), +// ( +// "Rust".into(), +// LanguageSettingsContent { +// tab_size: NonZeroU32::new(4), +// ..Default::default() +// }, +// ), +// ]); +// }); + +// let toml_language = Arc::new(Language::new( +// LanguageConfig { +// name: "TOML".into(), +// ..Default::default() +// }, +// None, +// )); +// let rust_language = Arc::new(Language::new( +// LanguageConfig { +// name: "Rust".into(), +// ..Default::default() +// }, +// None, +// )); + +// let toml_buffer = cx.add_model(|cx| { +// Buffer::new(0, cx.model_id() as u64, "a = 1\nb = 2\n").with_language(toml_language, cx) +// }); +// let rust_buffer = cx.add_model(|cx| { +// Buffer::new(0, cx.model_id() as u64, "const c: usize = 3;\n") +// .with_language(rust_language, cx) +// }); +// let multibuffer = cx.add_model(|cx| { +// let mut multibuffer = MultiBuffer::new(0); +// multibuffer.push_excerpts( +// toml_buffer.clone(), +// [ExcerptRange { +// context: Point::new(0, 0)..Point::new(2, 0), +// primary: None, +// }], +// cx, +// ); +// multibuffer.push_excerpts( +// rust_buffer.clone(), +// [ExcerptRange { +// context: Point::new(0, 0)..Point::new(1, 0), +// primary: None, +// }], +// cx, +// ); +// multibuffer +// }); + +// cx.add_window(|cx| { +// let mut editor = build_editor(multibuffer, cx); + +// assert_eq!( +// editor.text(cx), +// indoc! {" +// a = 1 +// b = 2 + +// const c: usize = 3; +// "} +// ); + +// select_ranges( +// &mut editor, +// indoc! {" +// «aˇ» = 1 +// b = 2 + +// «const c:ˇ» usize = 3; +// "}, +// cx, +// ); + +// editor.tab(&Tab, cx); +// assert_text_with_selections( +// &mut editor, +// indoc! {" +// «aˇ» = 1 +// b = 2 + +// «const c:ˇ» usize = 3; +// "}, +// cx, +// ); +// editor.tab_prev(&TabPrev, cx); +// assert_text_with_selections( +// &mut editor, +// indoc! {" +// «aˇ» = 1 +// b = 2 + +// «const c:ˇ» usize = 3; +// "}, +// cx, +// ); + +// editor +// }); +// } + +// #[gpui::test] +// async fn test_backspace(cx: &mut gpui::TestAppContext) { +// init_test(cx, |_| {}); + +// let mut cx = EditorTestContext::new(cx).await; + +// // Basic backspace +// cx.set_state(indoc! {" +// onˇe two three +// fou«rˇ» five six +// seven «ˇeight nine +// »ten +// "}); +// cx.update_editor(|e, cx| e.backspace(&Backspace, cx)); +// cx.assert_editor_state(indoc! {" +// oˇe two three +// fouˇ five six +// seven ˇten +// "}); + +// // Test backspace inside and around indents +// cx.set_state(indoc! {" +// zero +// ˇone +// ˇtwo +// ˇ ˇ ˇ three +// ˇ ˇ four +// "}); +// cx.update_editor(|e, cx| e.backspace(&Backspace, cx)); +// cx.assert_editor_state(indoc! {" +// zero +// ˇone +// ˇtwo +// ˇ threeˇ four +// "}); + +// // Test backspace with line_mode set to true +// cx.update_editor(|e, _| e.selections.line_mode = true); +// cx.set_state(indoc! {" +// The ˇquick ˇbrown +// fox jumps over +// the lazy dog +// ˇThe qu«ick bˇ»rown"}); +// cx.update_editor(|e, cx| e.backspace(&Backspace, cx)); +// cx.assert_editor_state(indoc! {" +// ˇfox jumps over +// the lazy dogˇ"}); +// } + +// #[gpui::test] +// async fn test_delete(cx: &mut gpui::TestAppContext) { +// init_test(cx, |_| {}); + +// let mut cx = EditorTestContext::new(cx).await; +// cx.set_state(indoc! {" +// onˇe two three +// fou«rˇ» five six +// seven «ˇeight nine +// »ten +// "}); +// cx.update_editor(|e, cx| e.delete(&Delete, cx)); +// cx.assert_editor_state(indoc! {" +// onˇ two three +// fouˇ five six +// seven ˇten +// "}); + +// // Test backspace with line_mode set to true +// cx.update_editor(|e, _| e.selections.line_mode = true); +// cx.set_state(indoc! {" +// The ˇquick ˇbrown +// fox «ˇjum»ps over +// the lazy dog +// ˇThe qu«ick bˇ»rown"}); +// cx.update_editor(|e, cx| e.backspace(&Backspace, cx)); +// cx.assert_editor_state("ˇthe lazy dogˇ"); +// } + +// #[gpui::test] +// fn test_delete_line(cx: &mut TestAppContext) { +// init_test(cx, |_| {}); + +// let view = cx +// .add_window(|cx| { +// let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx); +// build_editor(buffer, cx) +// }) +// .root(cx); +// view.update(cx, |view, cx| { +// view.change_selections(None, cx, |s| { +// s.select_display_ranges([ +// DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1), +// DisplayPoint::new(1, 0)..DisplayPoint::new(1, 1), +// DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0), +// ]) +// }); +// view.delete_line(&DeleteLine, cx); +// assert_eq!(view.display_text(cx), "ghi"); +// assert_eq!( +// view.selections.display_ranges(cx), +// vec![ +// DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0), +// DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1) +// ] +// ); +// }); + +// let view = cx +// .add_window(|cx| { +// let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx); +// build_editor(buffer, cx) +// }) +// .root(cx); +// view.update(cx, |view, cx| { +// view.change_selections(None, cx, |s| { +// s.select_display_ranges([DisplayPoint::new(2, 0)..DisplayPoint::new(0, 1)]) +// }); +// view.delete_line(&DeleteLine, cx); +// assert_eq!(view.display_text(cx), "ghi\n"); +// assert_eq!( +// view.selections.display_ranges(cx), +// vec![DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1)] +// ); +// }); +// } + +// #[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(); + +// 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 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)] +// ); + +// // 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 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, +// ) +// }); + +// // 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 ignore any leading tabs +// editor.join_lines(&JoinLines, cx); +// assert_eq!(buffer.read(cx).text(), "aaa bbb c d"); + +// editor +// }); +// } + +// #[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(); + +// 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"); + +// 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) { +// init_test(cx, |_| {}); + +// let mut cx = EditorTestContext::new(cx).await; + +// // Test sort_lines_case_insensitive() +// cx.set_state(indoc! {" +// «z +// y +// x +// Z +// Y +// Xˇ» +// "}); +// cx.update_editor(|e, cx| e.sort_lines_case_insensitive(&SortLinesCaseInsensitive, cx)); +// cx.assert_editor_state(indoc! {" +// «x +// X +// y +// Y +// z +// Zˇ» +// "}); + +// // Test reverse_lines() +// cx.set_state(indoc! {" +// «5 +// 4 +// 3 +// 2 +// 1ˇ» +// "}); +// cx.update_editor(|e, cx| e.reverse_lines(&ReverseLines, cx)); +// cx.assert_editor_state(indoc! {" +// «1 +// 2 +// 3 +// 4 +// 5ˇ» +// "}); + +// // Skip testing shuffle_line() + +// // From here on out, test more complex cases of manipulate_lines() with a single driver method: sort_lines_case_sensitive() +// // Since all methods calling manipulate_lines() are doing the exact same general thing (reordering lines) + +// // Don't manipulate when cursor is on single line, but expand the selection +// cx.set_state(indoc! {" +// ddˇdd +// ccc +// bb +// a +// "}); +// cx.update_editor(|e, cx| e.sort_lines_case_sensitive(&SortLinesCaseSensitive, cx)); +// cx.assert_editor_state(indoc! {" +// «ddddˇ» +// ccc +// bb +// a +// "}); + +// // Basic manipulate case +// // Start selection moves to column 0 +// // End of selection shrinks to fit shorter line +// cx.set_state(indoc! {" +// dd«d +// ccc +// bb +// aaaaaˇ» +// "}); +// cx.update_editor(|e, cx| e.sort_lines_case_sensitive(&SortLinesCaseSensitive, cx)); +// cx.assert_editor_state(indoc! {" +// «aaaaa +// bb +// ccc +// dddˇ» +// "}); + +// // Manipulate case with newlines +// cx.set_state(indoc! {" +// dd«d +// ccc + +// bb +// aaaaa + +// ˇ» +// "}); +// cx.update_editor(|e, cx| e.sort_lines_case_sensitive(&SortLinesCaseSensitive, cx)); +// cx.assert_editor_state(indoc! {" +// « + +// aaaaa +// bb +// ccc +// dddˇ» + +// "}); +// } + +// #[gpui::test] +// async fn test_manipulate_lines_with_multi_selection(cx: &mut TestAppContext) { +// init_test(cx, |_| {}); + +// let mut cx = EditorTestContext::new(cx).await; + +// // Manipulate with multiple selections on a single line +// cx.set_state(indoc! {" +// dd«dd +// cˇ»c«c +// bb +// aaaˇ»aa +// "}); +// cx.update_editor(|e, cx| e.sort_lines_case_sensitive(&SortLinesCaseSensitive, cx)); +// cx.assert_editor_state(indoc! {" +// «aaaaa +// bb +// ccc +// ddddˇ» +// "}); + +// // Manipulate with multiple disjoin selections +// cx.set_state(indoc! {" +// 5« +// 4 +// 3 +// 2 +// 1ˇ» + +// dd«dd +// ccc +// bb +// aaaˇ»aa +// "}); +// cx.update_editor(|e, cx| e.sort_lines_case_sensitive(&SortLinesCaseSensitive, cx)); +// cx.assert_editor_state(indoc! {" +// «1 +// 2 +// 3 +// 4 +// 5ˇ» + +// «aaaaa +// bb +// ccc +// ddddˇ» +// "}); +// } + +// #[gpui::test] +// async fn test_manipulate_text(cx: &mut TestAppContext) { +// init_test(cx, |_| {}); + +// let mut cx = EditorTestContext::new(cx).await; + +// // Test convert_to_upper_case() +// cx.set_state(indoc! {" +// «hello worldˇ» +// "}); +// cx.update_editor(|e, cx| e.convert_to_upper_case(&ConvertToUpperCase, cx)); +// cx.assert_editor_state(indoc! {" +// «HELLO WORLDˇ» +// "}); + +// // Test convert_to_lower_case() +// cx.set_state(indoc! {" +// «HELLO WORLDˇ» +// "}); +// cx.update_editor(|e, cx| e.convert_to_lower_case(&ConvertToLowerCase, cx)); +// cx.assert_editor_state(indoc! {" +// «hello worldˇ» +// "}); + +// // Test multiple line, single selection case +// // Test code hack that covers the fact that to_case crate doesn't support '\n' as a word boundary +// cx.set_state(indoc! {" +// «The quick brown +// fox jumps over +// the lazy dogˇ» +// "}); +// cx.update_editor(|e, cx| e.convert_to_title_case(&ConvertToTitleCase, cx)); +// cx.assert_editor_state(indoc! {" +// «The Quick Brown +// Fox Jumps Over +// The Lazy Dogˇ» +// "}); + +// // Test multiple line, single selection case +// // Test code hack that covers the fact that to_case crate doesn't support '\n' as a word boundary +// cx.set_state(indoc! {" +// «The quick brown +// fox jumps over +// the lazy dogˇ» +// "}); +// cx.update_editor(|e, cx| e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, cx)); +// cx.assert_editor_state(indoc! {" +// «TheQuickBrown +// FoxJumpsOver +// TheLazyDogˇ» +// "}); + +// // From here on out, test more complex cases of manipulate_text() + +// // Test no selection case - should affect words cursors are in +// // Cursor at beginning, middle, and end of word +// cx.set_state(indoc! {" +// ˇhello big beauˇtiful worldˇ +// "}); +// cx.update_editor(|e, cx| e.convert_to_upper_case(&ConvertToUpperCase, cx)); +// cx.assert_editor_state(indoc! {" +// «HELLOˇ» big «BEAUTIFULˇ» «WORLDˇ» +// "}); + +// // Test multiple selections on a single line and across multiple lines +// cx.set_state(indoc! {" +// «Theˇ» quick «brown +// foxˇ» jumps «overˇ» +// the «lazyˇ» dog +// "}); +// cx.update_editor(|e, cx| e.convert_to_upper_case(&ConvertToUpperCase, cx)); +// cx.assert_editor_state(indoc! {" +// «THEˇ» quick «BROWN +// FOXˇ» jumps «OVERˇ» +// the «LAZYˇ» dog +// "}); + +// // Test case where text length grows +// cx.set_state(indoc! {" +// «tschüߡ» +// "}); +// cx.update_editor(|e, cx| e.convert_to_upper_case(&ConvertToUpperCase, cx)); +// cx.assert_editor_state(indoc! {" +// «TSCHÜSSˇ» +// "}); + +// // Test to make sure we don't crash when text shrinks +// cx.set_state(indoc! {" +// aaa_bbbˇ +// "}); +// cx.update_editor(|e, cx| e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, cx)); +// cx.assert_editor_state(indoc! {" +// «aaaBbbˇ» +// "}); + +// // Test to make sure we all aware of the fact that each word can grow and shrink +// // Final selections should be aware of this fact +// cx.set_state(indoc! {" +// aaa_bˇbb bbˇb_ccc ˇccc_ddd +// "}); +// cx.update_editor(|e, cx| e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, cx)); +// cx.assert_editor_state(indoc! {" +// «aaaBbbˇ» «bbbCccˇ» «cccDddˇ» +// "}); +// } + +// #[gpui::test] +// fn test_duplicate_line(cx: &mut TestAppContext) { +// init_test(cx, |_| {}); + +// let view = cx +// .add_window(|cx| { +// let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx); +// build_editor(buffer, cx) +// }) +// .root(cx); +// view.update(cx, |view, cx| { +// view.change_selections(None, cx, |s| { +// s.select_display_ranges([ +// DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1), +// DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2), +// DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0), +// DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0), +// ]) +// }); +// view.duplicate_line(&DuplicateLine, cx); +// assert_eq!(view.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n"); +// assert_eq!( +// view.selections.display_ranges(cx), +// vec![ +// DisplayPoint::new(1, 0)..DisplayPoint::new(1, 1), +// DisplayPoint::new(1, 2)..DisplayPoint::new(1, 2), +// DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0), +// DisplayPoint::new(6, 0)..DisplayPoint::new(6, 0), +// ] +// ); +// }); + +// let view = cx +// .add_window(|cx| { +// let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx); +// build_editor(buffer, cx) +// }) +// .root(cx); +// view.update(cx, |view, cx| { +// view.change_selections(None, cx, |s| { +// s.select_display_ranges([ +// DisplayPoint::new(0, 1)..DisplayPoint::new(1, 1), +// DisplayPoint::new(1, 2)..DisplayPoint::new(2, 1), +// ]) +// }); +// view.duplicate_line(&DuplicateLine, cx); +// assert_eq!(view.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n"); +// assert_eq!( +// view.selections.display_ranges(cx), +// vec![ +// DisplayPoint::new(3, 1)..DisplayPoint::new(4, 1), +// DisplayPoint::new(4, 2)..DisplayPoint::new(5, 1), +// ] +// ); +// }); +// } + +// #[gpui::test] +// fn test_move_line_up_down(cx: &mut TestAppContext) { +// init_test(cx, |_| {}); + +// let view = cx +// .add_window(|cx| { +// let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx); +// build_editor(buffer, cx) +// }) +// .root(cx); +// view.update(cx, |view, cx| { +// view.fold_ranges( +// vec![ +// Point::new(0, 2)..Point::new(1, 2), +// Point::new(2, 3)..Point::new(4, 1), +// Point::new(7, 0)..Point::new(8, 4), +// ], +// true, +// cx, +// ); +// view.change_selections(None, cx, |s| { +// s.select_display_ranges([ +// DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1), +// DisplayPoint::new(3, 1)..DisplayPoint::new(3, 1), +// DisplayPoint::new(3, 2)..DisplayPoint::new(4, 3), +// DisplayPoint::new(5, 0)..DisplayPoint::new(5, 2), +// ]) +// }); +// assert_eq!( +// view.display_text(cx), +// "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i\njjjjj" +// ); + +// view.move_line_up(&MoveLineUp, cx); +// assert_eq!( +// view.display_text(cx), +// "aa⋯bbb\nccc⋯eeee\nggggg\n⋯i\njjjjj\nfffff" +// ); +// assert_eq!( +// view.selections.display_ranges(cx), +// vec![ +// DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1), +// DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1), +// DisplayPoint::new(2, 2)..DisplayPoint::new(3, 3), +// DisplayPoint::new(4, 0)..DisplayPoint::new(4, 2) +// ] +// ); +// }); + +// view.update(cx, |view, cx| { +// view.move_line_down(&MoveLineDown, cx); +// assert_eq!( +// view.display_text(cx), +// "ccc⋯eeee\naa⋯bbb\nfffff\nggggg\n⋯i\njjjjj" +// ); +// assert_eq!( +// view.selections.display_ranges(cx), +// vec![ +// DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1), +// DisplayPoint::new(3, 1)..DisplayPoint::new(3, 1), +// DisplayPoint::new(3, 2)..DisplayPoint::new(4, 3), +// DisplayPoint::new(5, 0)..DisplayPoint::new(5, 2) +// ] +// ); +// }); + +// view.update(cx, |view, cx| { +// view.move_line_down(&MoveLineDown, cx); +// assert_eq!( +// view.display_text(cx), +// "ccc⋯eeee\nfffff\naa⋯bbb\nggggg\n⋯i\njjjjj" +// ); +// assert_eq!( +// view.selections.display_ranges(cx), +// vec![ +// DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1), +// DisplayPoint::new(3, 1)..DisplayPoint::new(3, 1), +// DisplayPoint::new(3, 2)..DisplayPoint::new(4, 3), +// DisplayPoint::new(5, 0)..DisplayPoint::new(5, 2) +// ] +// ); +// }); + +// view.update(cx, |view, cx| { +// view.move_line_up(&MoveLineUp, cx); +// assert_eq!( +// view.display_text(cx), +// "ccc⋯eeee\naa⋯bbb\nggggg\n⋯i\njjjjj\nfffff" +// ); +// assert_eq!( +// view.selections.display_ranges(cx), +// vec![ +// DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1), +// DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1), +// DisplayPoint::new(2, 2)..DisplayPoint::new(3, 3), +// DisplayPoint::new(4, 0)..DisplayPoint::new(4, 2) +// ] +// ); +// }); +// } + +// #[gpui::test] +// fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) { +// init_test(cx, |_| {}); + +// let editor = cx +// .add_window(|cx| { +// let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx); +// build_editor(buffer, cx) +// }) +// .root(cx); +// editor.update(cx, |editor, cx| { +// let snapshot = editor.buffer.read(cx).snapshot(cx); +// editor.insert_blocks( +// [BlockProperties { +// style: BlockStyle::Fixed, +// position: snapshot.anchor_after(Point::new(2, 0)), +// disposition: BlockDisposition::Below, +// height: 1, +// render: Arc::new(|_| Empty::new().into_any()), +// }], +// Some(Autoscroll::fit()), +// cx, +// ); +// editor.change_selections(None, cx, |s| { +// s.select_ranges([Point::new(2, 0)..Point::new(2, 0)]) +// }); +// editor.move_line_down(&MoveLineDown, cx); +// }); +// } + +// #[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]); + +// editor +// }); + +// _ = cx.add_window(|cx| { +// let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), 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.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.transpose(&Default::default(), cx); +// assert_eq!(editor.text(cx), "acbde\n"); +// assert_eq!(editor.selections.ranges(cx), [6..6]); + +// editor.transpose(&Default::default(), cx); +// assert_eq!(editor.text(cx), "acbd\ne"); +// assert_eq!(editor.selections.ranges(cx), [6..6]); + +// editor +// }); + +// _ = cx.add_window(|cx| { +// let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), 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), "bcade\n"); +// assert_eq!(editor.selections.ranges(cx), [3..3, 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), "bcade\n"); +// 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 +// }); + +// _ = cx.add_window(|cx| { +// let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), 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 +// }); +// } + +// #[gpui::test] +// async fn test_clipboard(cx: &mut gpui::TestAppContext) { +// init_test(cx, |_| {}); + +// 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")), +// ); + +// // 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"}); +// } + +// #[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)); + +// // 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 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(), +// ˇ); +// "}); + +// // 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 +// ) +// ˇ +// ) +// ); +// "}); +// } + +// #[gpui::test] +// fn test_select_all(cx: &mut TestAppContext) { +// init_test(cx, |_| {}); + +// let view = cx +// .add_window(|cx| { +// let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx); +// build_editor(buffer, cx) +// }) +// .root(cx); +// view.update(cx, |view, cx| { +// view.select_all(&SelectAll, cx); +// assert_eq!( +// view.selections.display_ranges(cx), +// &[DisplayPoint::new(0, 0)..DisplayPoint::new(2, 3)] +// ); +// }); +// } + +// #[gpui::test] +// fn test_select_line(cx: &mut TestAppContext) { +// init_test(cx, |_| {}); + +// let view = cx +// .add_window(|cx| { +// let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx); +// build_editor(buffer, cx) +// }) +// .root(cx); +// view.update(cx, |view, cx| { +// view.change_selections(None, cx, |s| { +// s.select_display_ranges([ +// DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1), +// DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2), +// DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0), +// DisplayPoint::new(4, 2)..DisplayPoint::new(4, 2), +// ]) +// }); +// view.select_line(&SelectLine, cx); +// assert_eq!( +// view.selections.display_ranges(cx), +// vec![ +// DisplayPoint::new(0, 0)..DisplayPoint::new(2, 0), +// DisplayPoint::new(4, 0)..DisplayPoint::new(5, 0), +// ] +// ); +// }); + +// view.update(cx, |view, cx| { +// view.select_line(&SelectLine, cx); +// assert_eq!( +// view.selections.display_ranges(cx), +// vec![ +// DisplayPoint::new(0, 0)..DisplayPoint::new(3, 0), +// DisplayPoint::new(4, 0)..DisplayPoint::new(5, 5), +// ] +// ); +// }); + +// view.update(cx, |view, cx| { +// view.select_line(&SelectLine, cx); +// assert_eq!( +// view.selections.display_ranges(cx), +// vec![DisplayPoint::new(0, 0)..DisplayPoint::new(5, 5)] +// ); +// }); +// } + +// #[gpui::test] +// fn test_split_selection_into_lines(cx: &mut TestAppContext) { +// init_test(cx, |_| {}); + +// let view = cx +// .add_window(|cx| { +// let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx); +// build_editor(buffer, cx) +// }) +// .root(cx); +// view.update(cx, |view, cx| { +// view.fold_ranges( +// vec![ +// Point::new(0, 2)..Point::new(1, 2), +// Point::new(2, 3)..Point::new(4, 1), +// Point::new(7, 0)..Point::new(8, 4), +// ], +// true, +// cx, +// ); +// view.change_selections(None, cx, |s| { +// s.select_display_ranges([ +// DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1), +// DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2), +// DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0), +// DisplayPoint::new(4, 4)..DisplayPoint::new(4, 4), +// ]) +// }); +// assert_eq!(view.display_text(cx), "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i"); +// }); + +// view.update(cx, |view, cx| { +// view.split_selection_into_lines(&SplitSelectionIntoLines, cx); +// assert_eq!( +// view.display_text(cx), +// "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i" +// ); +// assert_eq!( +// view.selections.display_ranges(cx), +// [ +// DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1), +// DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2), +// DisplayPoint::new(2, 0)..DisplayPoint::new(2, 0), +// DisplayPoint::new(5, 4)..DisplayPoint::new(5, 4) +// ] +// ); +// }); + +// view.update(cx, |view, cx| { +// view.change_selections(None, cx, |s| { +// s.select_display_ranges([DisplayPoint::new(5, 0)..DisplayPoint::new(0, 1)]) +// }); +// view.split_selection_into_lines(&SplitSelectionIntoLines, cx); +// assert_eq!( +// view.display_text(cx), +// "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii" +// ); +// assert_eq!( +// view.selections.display_ranges(cx), +// [ +// DisplayPoint::new(0, 5)..DisplayPoint::new(0, 5), +// DisplayPoint::new(1, 5)..DisplayPoint::new(1, 5), +// DisplayPoint::new(2, 5)..DisplayPoint::new(2, 5), +// DisplayPoint::new(3, 5)..DisplayPoint::new(3, 5), +// DisplayPoint::new(4, 5)..DisplayPoint::new(4, 5), +// DisplayPoint::new(5, 5)..DisplayPoint::new(5, 5), +// DisplayPoint::new(6, 5)..DisplayPoint::new(6, 5), +// DisplayPoint::new(7, 0)..DisplayPoint::new(7, 0) +// ] +// ); +// }); +// } + +// #[gpui::test] +// fn test_add_selection_above_below(cx: &mut TestAppContext) { +// init_test(cx, |_| {}); + +// let view = cx +// .add_window(|cx| { +// let buffer = MultiBuffer::build_simple("abc\ndefghi\n\njk\nlmno\n", cx); +// build_editor(buffer, cx) +// }) +// .root(cx); + +// view.update(cx, |view, cx| { +// view.change_selections(None, cx, |s| { +// s.select_display_ranges([DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3)]) +// }); +// }); +// view.update(cx, |view, cx| { +// view.add_selection_above(&AddSelectionAbove, cx); +// assert_eq!( +// view.selections.display_ranges(cx), +// vec![ +// DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3), +// DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3) +// ] +// ); +// }); + +// view.update(cx, |view, cx| { +// view.add_selection_above(&AddSelectionAbove, cx); +// assert_eq!( +// view.selections.display_ranges(cx), +// vec![ +// DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3), +// DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3) +// ] +// ); +// }); + +// view.update(cx, |view, cx| { +// view.add_selection_below(&AddSelectionBelow, cx); +// assert_eq!( +// view.selections.display_ranges(cx), +// vec![DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3)] +// ); + +// view.undo_selection(&UndoSelection, cx); +// assert_eq!( +// view.selections.display_ranges(cx), +// vec![ +// DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3), +// DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3) +// ] +// ); + +// view.redo_selection(&RedoSelection, cx); +// assert_eq!( +// view.selections.display_ranges(cx), +// vec![DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3)] +// ); +// }); + +// view.update(cx, |view, cx| { +// view.add_selection_below(&AddSelectionBelow, cx); +// assert_eq!( +// view.selections.display_ranges(cx), +// vec![ +// DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3), +// DisplayPoint::new(4, 3)..DisplayPoint::new(4, 3) +// ] +// ); +// }); + +// view.update(cx, |view, cx| { +// view.add_selection_below(&AddSelectionBelow, cx); +// assert_eq!( +// view.selections.display_ranges(cx), +// vec![ +// DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3), +// DisplayPoint::new(4, 3)..DisplayPoint::new(4, 3) +// ] +// ); +// }); + +// view.update(cx, |view, cx| { +// view.change_selections(None, cx, |s| { +// s.select_display_ranges([DisplayPoint::new(1, 4)..DisplayPoint::new(1, 3)]) +// }); +// }); +// view.update(cx, |view, cx| { +// view.add_selection_below(&AddSelectionBelow, cx); +// assert_eq!( +// view.selections.display_ranges(cx), +// vec![ +// DisplayPoint::new(1, 4)..DisplayPoint::new(1, 3), +// DisplayPoint::new(4, 4)..DisplayPoint::new(4, 3) +// ] +// ); +// }); + +// view.update(cx, |view, cx| { +// view.add_selection_below(&AddSelectionBelow, cx); +// assert_eq!( +// view.selections.display_ranges(cx), +// vec![ +// DisplayPoint::new(1, 4)..DisplayPoint::new(1, 3), +// DisplayPoint::new(4, 4)..DisplayPoint::new(4, 3) +// ] +// ); +// }); + +// view.update(cx, |view, cx| { +// view.add_selection_above(&AddSelectionAbove, cx); +// assert_eq!( +// view.selections.display_ranges(cx), +// vec![DisplayPoint::new(1, 4)..DisplayPoint::new(1, 3)] +// ); +// }); + +// view.update(cx, |view, cx| { +// view.add_selection_above(&AddSelectionAbove, cx); +// assert_eq!( +// view.selections.display_ranges(cx), +// vec![DisplayPoint::new(1, 4)..DisplayPoint::new(1, 3)] +// ); +// }); + +// view.update(cx, |view, cx| { +// view.change_selections(None, cx, |s| { +// s.select_display_ranges([DisplayPoint::new(0, 1)..DisplayPoint::new(1, 4)]) +// }); +// view.add_selection_below(&AddSelectionBelow, cx); +// assert_eq!( +// view.selections.display_ranges(cx), +// vec![ +// DisplayPoint::new(0, 1)..DisplayPoint::new(0, 3), +// DisplayPoint::new(1, 1)..DisplayPoint::new(1, 4), +// DisplayPoint::new(3, 1)..DisplayPoint::new(3, 2), +// ] +// ); +// }); + +// view.update(cx, |view, cx| { +// view.add_selection_below(&AddSelectionBelow, cx); +// assert_eq!( +// view.selections.display_ranges(cx), +// vec![ +// DisplayPoint::new(0, 1)..DisplayPoint::new(0, 3), +// DisplayPoint::new(1, 1)..DisplayPoint::new(1, 4), +// DisplayPoint::new(3, 1)..DisplayPoint::new(3, 2), +// DisplayPoint::new(4, 1)..DisplayPoint::new(4, 4), +// ] +// ); +// }); + +// view.update(cx, |view, cx| { +// view.add_selection_above(&AddSelectionAbove, cx); +// assert_eq!( +// view.selections.display_ranges(cx), +// vec![ +// DisplayPoint::new(0, 1)..DisplayPoint::new(0, 3), +// DisplayPoint::new(1, 1)..DisplayPoint::new(1, 4), +// DisplayPoint::new(3, 1)..DisplayPoint::new(3, 2), +// ] +// ); +// }); + +// view.update(cx, |view, cx| { +// view.change_selections(None, cx, |s| { +// s.select_display_ranges([DisplayPoint::new(4, 3)..DisplayPoint::new(1, 1)]) +// }); +// }); +// view.update(cx, |view, cx| { +// view.add_selection_above(&AddSelectionAbove, cx); +// assert_eq!( +// view.selections.display_ranges(cx), +// vec![ +// DisplayPoint::new(0, 3)..DisplayPoint::new(0, 1), +// DisplayPoint::new(1, 3)..DisplayPoint::new(1, 1), +// DisplayPoint::new(3, 2)..DisplayPoint::new(3, 1), +// DisplayPoint::new(4, 3)..DisplayPoint::new(4, 1), +// ] +// ); +// }); + +// view.update(cx, |view, cx| { +// view.add_selection_below(&AddSelectionBelow, cx); +// assert_eq!( +// view.selections.display_ranges(cx), +// vec![ +// DisplayPoint::new(1, 3)..DisplayPoint::new(1, 1), +// DisplayPoint::new(3, 2)..DisplayPoint::new(3, 1), +// DisplayPoint::new(4, 3)..DisplayPoint::new(4, 1), +// ] +// ); +// }); +// } + +// #[gpui::test] +// async fn test_select_next(cx: &mut gpui::TestAppContext) { +// init_test(cx, |_| {}); + +// let mut cx = EditorTestContext::new(cx).await; +// cx.set_state("abc\nˇabc abc\ndefabc\nabc"); + +// cx.update_editor(|e, cx| e.select_next(&SelectNext::default(), cx)) +// .unwrap(); +// cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc"); + +// cx.update_editor(|e, cx| e.select_next(&SelectNext::default(), cx)) +// .unwrap(); +// cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc"); + +// cx.update_editor(|view, cx| view.undo_selection(&UndoSelection, cx)); +// cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc"); + +// cx.update_editor(|view, cx| view.redo_selection(&RedoSelection, cx)); +// cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc"); + +// cx.update_editor(|e, cx| e.select_next(&SelectNext::default(), cx)) +// .unwrap(); +// cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»"); + +// cx.update_editor(|e, cx| e.select_next(&SelectNext::default(), cx)) +// .unwrap(); +// cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»"); +// } + +// #[gpui::test] +// async fn test_select_previous(cx: &mut gpui::TestAppContext) { +// init_test(cx, |_| {}); +// { +// // `Select previous` without a selection (selects wordwise) +// let mut cx = EditorTestContext::new(cx).await; +// cx.set_state("abc\nˇabc abc\ndefabc\nabc"); + +// cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)) +// .unwrap(); +// cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc"); + +// cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)) +// .unwrap(); +// cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc"); + +// cx.update_editor(|view, cx| view.undo_selection(&UndoSelection, cx)); +// cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc"); + +// cx.update_editor(|view, cx| view.redo_selection(&RedoSelection, cx)); +// cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc"); + +// cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)) +// .unwrap(); +// cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»"); + +// cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)) +// .unwrap(); +// cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»"); +// } +// { +// // `Select previous` with a selection +// let mut cx = EditorTestContext::new(cx).await; +// cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc"); + +// cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)) +// .unwrap(); +// cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\nabc"); + +// cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)) +// .unwrap(); +// cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\n«abcˇ»"); + +// cx.update_editor(|view, cx| view.undo_selection(&UndoSelection, cx)); +// cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\nabc"); + +// cx.update_editor(|view, cx| view.redo_selection(&RedoSelection, cx)); +// cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\n«abcˇ»"); + +// cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)) +// .unwrap(); +// cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndef«abcˇ»\n«abcˇ»"); + +// cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)) +// .unwrap(); +// cx.assert_editor_state("«abcˇ»\n«ˇabc» «abcˇ»\ndef«abcˇ»\n«abcˇ»"); +// } +// } + +// #[gpui::test] +// async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) { +// init_test(cx, |_| {}); + +// let language = Arc::new(Language::new( +// LanguageConfig::default(), +// Some(tree_sitter_rust::language()), +// )); + +// let text = r#" +// use mod1::mod2::{mod3, mod4}; + +// fn fn_1(param1: bool, param2: &str) { +// let var1 = "text"; +// } +// "# +// .unindent(); + +// let buffer = +// cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, text).with_language(language, cx)); +// let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); +// let view = cx.add_window(|cx| build_editor(buffer, cx)).root(cx); +// view.condition(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx)) +// .await; + +// view.update(cx, |view, cx| { +// view.change_selections(None, cx, |s| { +// s.select_display_ranges([ +// DisplayPoint::new(0, 25)..DisplayPoint::new(0, 25), +// DisplayPoint::new(2, 24)..DisplayPoint::new(2, 12), +// DisplayPoint::new(3, 18)..DisplayPoint::new(3, 18), +// ]); +// }); +// view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx); +// }); +// assert_eq!( +// view.update(cx, |view, cx| { view.selections.display_ranges(cx) }), +// &[ +// DisplayPoint::new(0, 23)..DisplayPoint::new(0, 27), +// DisplayPoint::new(2, 35)..DisplayPoint::new(2, 7), +// DisplayPoint::new(3, 15)..DisplayPoint::new(3, 21), +// ] +// ); + +// view.update(cx, |view, cx| { +// view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx); +// }); +// assert_eq!( +// view.update(cx, |view, cx| view.selections.display_ranges(cx)), +// &[ +// DisplayPoint::new(0, 16)..DisplayPoint::new(0, 28), +// DisplayPoint::new(4, 1)..DisplayPoint::new(2, 0), +// ] +// ); + +// view.update(cx, |view, cx| { +// view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx); +// }); +// assert_eq!( +// view.update(cx, |view, cx| view.selections.display_ranges(cx)), +// &[DisplayPoint::new(5, 0)..DisplayPoint::new(0, 0)] +// ); + +// // Trying to expand the selected syntax node one more time has no effect. +// view.update(cx, |view, cx| { +// view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx); +// }); +// assert_eq!( +// view.update(cx, |view, cx| view.selections.display_ranges(cx)), +// &[DisplayPoint::new(5, 0)..DisplayPoint::new(0, 0)] +// ); + +// view.update(cx, |view, cx| { +// view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx); +// }); +// assert_eq!( +// view.update(cx, |view, cx| view.selections.display_ranges(cx)), +// &[ +// DisplayPoint::new(0, 16)..DisplayPoint::new(0, 28), +// DisplayPoint::new(4, 1)..DisplayPoint::new(2, 0), +// ] +// ); + +// view.update(cx, |view, cx| { +// view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx); +// }); +// assert_eq!( +// view.update(cx, |view, cx| view.selections.display_ranges(cx)), +// &[ +// DisplayPoint::new(0, 23)..DisplayPoint::new(0, 27), +// DisplayPoint::new(2, 35)..DisplayPoint::new(2, 7), +// DisplayPoint::new(3, 15)..DisplayPoint::new(3, 21), +// ] +// ); + +// view.update(cx, |view, cx| { +// view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx); +// }); +// assert_eq!( +// view.update(cx, |view, cx| view.selections.display_ranges(cx)), +// &[ +// DisplayPoint::new(0, 25)..DisplayPoint::new(0, 25), +// DisplayPoint::new(2, 24)..DisplayPoint::new(2, 12), +// DisplayPoint::new(3, 18)..DisplayPoint::new(3, 18), +// ] +// ); + +// // Trying to shrink the selected syntax node one more time has no effect. +// view.update(cx, |view, cx| { +// view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx); +// }); +// assert_eq!( +// view.update(cx, |view, cx| view.selections.display_ranges(cx)), +// &[ +// DisplayPoint::new(0, 25)..DisplayPoint::new(0, 25), +// DisplayPoint::new(2, 24)..DisplayPoint::new(2, 12), +// DisplayPoint::new(3, 18)..DisplayPoint::new(3, 18), +// ] +// ); + +// // Ensure that we keep expanding the selection if the larger selection starts or ends within +// // a fold. +// view.update(cx, |view, cx| { +// view.fold_ranges( +// vec![ +// Point::new(0, 21)..Point::new(0, 24), +// Point::new(3, 20)..Point::new(3, 22), +// ], +// true, +// cx, +// ); +// view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx); +// }); +// assert_eq!( +// view.update(cx, |view, cx| view.selections.display_ranges(cx)), +// &[ +// DisplayPoint::new(0, 16)..DisplayPoint::new(0, 28), +// DisplayPoint::new(2, 35)..DisplayPoint::new(2, 7), +// DisplayPoint::new(3, 4)..DisplayPoint::new(3, 23), +// ] +// ); +// } + +// #[gpui::test] +// async fn test_autoindent_selections(cx: &mut gpui::TestAppContext) { +// init_test(cx, |_| {}); + +// let language = Arc::new( +// Language::new( +// LanguageConfig { +// 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_indents_query( +// r#" +// (_ "(" ")" @end) @indent +// (_ "{" "}" @end) @indent +// "#, +// ) +// .unwrap(), +// ); + +// let text = "fn a() {}"; + +// let buffer = +// cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, text).with_language(language, cx)); +// let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); +// let editor = cx.add_window(|cx| build_editor(buffer, cx)).root(cx); +// editor +// .condition(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx)) +// .await; + +// editor.update(cx, |editor, cx| { +// editor.change_selections(None, cx, |s| s.select_ranges([5..5, 8..8, 9..9])); +// editor.newline(&Newline, cx); +// assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n"); +// assert_eq!( +// editor.selections.ranges(cx), +// &[ +// Point::new(1, 4)..Point::new(1, 4), +// Point::new(3, 4)..Point::new(3, 4), +// Point::new(5, 0)..Point::new(5, 0) +// ] +// ); +// }); +// } + +// #[gpui::test] +// async fn test_autoclose_pairs(cx: &mut gpui::TestAppContext) { +// init_test(cx, |_| {}); + +// let mut cx = EditorTestContext::new(cx).await; + +// let language = Arc::new(Language::new( +// LanguageConfig { +// brackets: BracketPairConfig { +// pairs: vec![ +// BracketPair { +// start: "{".to_string(), +// end: "}".to_string(), +// close: true, +// newline: true, +// }, +// BracketPair { +// start: "(".to_string(), +// end: ")".to_string(), +// close: true, +// newline: true, +// }, +// BracketPair { +// start: "/*".to_string(), +// end: " */".to_string(), +// close: true, +// newline: true, +// }, +// BracketPair { +// start: "[".to_string(), +// end: "]".to_string(), +// close: false, +// newline: true, +// }, +// BracketPair { +// start: "\"".to_string(), +// end: "\"".to_string(), +// close: true, +// newline: false, +// }, +// ], +// ..Default::default() +// }, +// autoclose_before: "})]".to_string(), +// ..Default::default() +// }, +// Some(tree_sitter_rust::language()), +// )); + +// let registry = Arc::new(LanguageRegistry::test()); +// registry.add(language.clone()); +// cx.update_buffer(|buffer, cx| { +// buffer.set_language_registry(registry); +// buffer.set_language(Some(language), cx); +// }); + +// cx.set_state( +// &r#" +// 🏀ˇ +// εˇ +// ❤️ˇ +// "# +// .unindent(), +// ); + +// // autoclose multiple nested brackets at multiple cursors +// cx.update_editor(|view, cx| { +// view.handle_input("{", cx); +// view.handle_input("{", cx); +// view.handle_input("{", cx); +// }); +// cx.assert_editor_state( +// &" +// 🏀{{{ˇ}}} +// ε{{{ˇ}}} +// ❤️{{{ˇ}}} +// " +// .unindent(), +// ); + +// // insert a different closing bracket +// cx.update_editor(|view, cx| { +// view.handle_input(")", cx); +// }); +// cx.assert_editor_state( +// &" +// 🏀{{{)ˇ}}} +// ε{{{)ˇ}}} +// ❤️{{{)ˇ}}} +// " +// .unindent(), +// ); + +// // skip over the auto-closed brackets when typing a closing bracket +// cx.update_editor(|view, cx| { +// view.move_right(&MoveRight, cx); +// view.handle_input("}", cx); +// view.handle_input("}", cx); +// view.handle_input("}", cx); +// }); +// cx.assert_editor_state( +// &" +// 🏀{{{)}}}}ˇ +// ε{{{)}}}}ˇ +// ❤️{{{)}}}}ˇ +// " +// .unindent(), +// ); + +// // autoclose multi-character pairs +// cx.set_state( +// &" +// ˇ +// ˇ +// " +// .unindent(), +// ); +// cx.update_editor(|view, cx| { +// view.handle_input("/", cx); +// view.handle_input("*", cx); +// }); +// cx.assert_editor_state( +// &" +// /*ˇ */ +// /*ˇ */ +// " +// .unindent(), +// ); + +// // one cursor autocloses a multi-character pair, one cursor +// // does not autoclose. +// cx.set_state( +// &" +// /ˇ +// ˇ +// " +// .unindent(), +// ); +// cx.update_editor(|view, cx| view.handle_input("*", cx)); +// cx.assert_editor_state( +// &" +// /*ˇ */ +// *ˇ +// " +// .unindent(), +// ); + +// // Don't autoclose if the next character isn't whitespace and isn't +// // listed in the language's "autoclose_before" section. +// cx.set_state("ˇa b"); +// cx.update_editor(|view, cx| view.handle_input("{", cx)); +// cx.assert_editor_state("{ˇa b"); + +// // Don't autoclose if `close` is false for the bracket pair +// cx.set_state("ˇ"); +// cx.update_editor(|view, cx| view.handle_input("[", cx)); +// cx.assert_editor_state("[ˇ"); + +// // Surround with brackets if text is selected +// cx.set_state("«aˇ» b"); +// cx.update_editor(|view, cx| view.handle_input("{", cx)); +// cx.assert_editor_state("{«aˇ»} b"); + +// // Autclose pair where the start and end characters are the same +// cx.set_state("aˇ"); +// cx.update_editor(|view, cx| view.handle_input("\"", cx)); +// cx.assert_editor_state("a\"ˇ\""); +// cx.update_editor(|view, cx| view.handle_input("\"", cx)); +// cx.assert_editor_state("a\"\"ˇ"); +// } + +// #[gpui::test] +// async fn test_autoclose_with_embedded_language(cx: &mut gpui::TestAppContext) { +// init_test(cx, |_| {}); + +// let mut cx = EditorTestContext::new(cx).await; + +// let html_language = Arc::new( +// Language::new( +// LanguageConfig { +// name: "HTML".into(), +// brackets: BracketPairConfig { +// pairs: vec![ +// BracketPair { +// start: "<".into(), +// end: ">".into(), +// close: true, +// ..Default::default() +// }, +// BracketPair { +// start: "{".into(), +// end: "}".into(), +// close: true, +// ..Default::default() +// }, +// BracketPair { +// start: "(".into(), +// end: ")".into(), +// close: true, +// ..Default::default() +// }, +// ], +// ..Default::default() +// }, +// autoclose_before: "})]>".into(), +// ..Default::default() +// }, +// Some(tree_sitter_html::language()), +// ) +// .with_injection_query( +// r#" +// (script_element +// (raw_text) @content +// (#set! "language" "javascript")) +// "#, +// ) +// .unwrap(), +// ); + +// let javascript_language = Arc::new(Language::new( +// LanguageConfig { +// name: "JavaScript".into(), +// brackets: BracketPairConfig { +// pairs: vec![ +// BracketPair { +// start: "/*".into(), +// end: " */".into(), +// close: true, +// ..Default::default() +// }, +// BracketPair { +// start: "{".into(), +// end: "}".into(), +// close: true, +// ..Default::default() +// }, +// BracketPair { +// start: "(".into(), +// end: ")".into(), +// close: true, +// ..Default::default() +// }, +// ], +// ..Default::default() +// }, +// autoclose_before: "})]>".into(), +// ..Default::default() +// }, +// Some(tree_sitter_typescript::language_tsx()), +// )); + +// let registry = Arc::new(LanguageRegistry::test()); +// registry.add(html_language.clone()); +// registry.add(javascript_language.clone()); + +// cx.update_buffer(|buffer, cx| { +// buffer.set_language_registry(registry); +// buffer.set_language(Some(html_language), cx); +// }); + +// cx.set_state( +// &r#" +// ˇ +// +// ˇ +// "# +// .unindent(), +// ); + +// // Precondition: different languages are active at different locations. +// cx.update_editor(|editor, cx| { +// let snapshot = editor.snapshot(cx); +// let cursors = editor.selections.ranges::(cx); +// let languages = cursors +// .iter() +// .map(|c| snapshot.language_at(c.start).unwrap().name()) +// .collect::>(); +// assert_eq!( +// languages, +// &["HTML".into(), "JavaScript".into(), "HTML".into()] +// ); +// }); + +// // Angle brackets autoclose in HTML, but not JavaScript. +// cx.update_editor(|editor, cx| { +// editor.handle_input("<", cx); +// editor.handle_input("a", cx); +// }); +// cx.assert_editor_state( +// &r#" +// +// +// +// "# +// .unindent(), +// ); + +// // Curly braces and parens autoclose in both HTML and JavaScript. +// cx.update_editor(|editor, cx| { +// editor.handle_input(" b=", cx); +// editor.handle_input("{", cx); +// editor.handle_input("c", cx); +// editor.handle_input("(", cx); +// }); +// cx.assert_editor_state( +// &r#" +// +// +// +// "# +// .unindent(), +// ); + +// // Brackets that were already autoclosed are skipped. +// cx.update_editor(|editor, cx| { +// editor.handle_input(")", cx); +// editor.handle_input("d", cx); +// editor.handle_input("}", cx); +// }); +// cx.assert_editor_state( +// &r#" +// +// +// +// "# +// .unindent(), +// ); +// cx.update_editor(|editor, cx| { +// editor.handle_input(">", cx); +// }); +// cx.assert_editor_state( +// &r#" +// ˇ +// +// ˇ +// "# +// .unindent(), +// ); + +// // Reset +// cx.set_state( +// &r#" +// ˇ +// +// ˇ +// "# +// .unindent(), +// ); + +// cx.update_editor(|editor, cx| { +// editor.handle_input("<", cx); +// }); +// cx.assert_editor_state( +// &r#" +// <ˇ> +// +// <ˇ> +// "# +// .unindent(), +// ); + +// // When backspacing, the closing angle brackets are removed. +// cx.update_editor(|editor, cx| { +// editor.backspace(&Backspace, cx); +// }); +// cx.assert_editor_state( +// &r#" +// ˇ +// +// ˇ +// "# +// .unindent(), +// ); + +// // Block comments autoclose in JavaScript, but not HTML. +// cx.update_editor(|editor, cx| { +// editor.handle_input("/", cx); +// editor.handle_input("*", cx); +// }); +// cx.assert_editor_state( +// &r#" +// /*ˇ +// +// /*ˇ +// "# +// .unindent(), +// ); +// } + +// #[gpui::test] +// async fn test_autoclose_with_overrides(cx: &mut gpui::TestAppContext) { +// init_test(cx, |_| {}); + +// let mut cx = EditorTestContext::new(cx).await; + +// let rust_language = Arc::new( +// Language::new( +// LanguageConfig { +// name: "Rust".into(), +// brackets: serde_json::from_value(json!([ +// { "start": "{", "end": "}", "close": true, "newline": true }, +// { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] }, +// ])) +// .unwrap(), +// autoclose_before: "})]>".into(), +// ..Default::default() +// }, +// Some(tree_sitter_rust::language()), +// ) +// .with_override_query("(string_literal) @string") +// .unwrap(), +// ); + +// let registry = Arc::new(LanguageRegistry::test()); +// registry.add(rust_language.clone()); + +// cx.update_buffer(|buffer, cx| { +// buffer.set_language_registry(registry); +// buffer.set_language(Some(rust_language), cx); +// }); + +// cx.set_state( +// &r#" +// let x = ˇ +// "# +// .unindent(), +// ); + +// // Inserting a quotation mark. A closing quotation mark is automatically inserted. +// cx.update_editor(|editor, cx| { +// editor.handle_input("\"", cx); +// }); +// cx.assert_editor_state( +// &r#" +// let x = "ˇ" +// "# +// .unindent(), +// ); + +// // Inserting another quotation mark. The cursor moves across the existing +// // automatically-inserted quotation mark. +// cx.update_editor(|editor, cx| { +// editor.handle_input("\"", cx); +// }); +// cx.assert_editor_state( +// &r#" +// let x = ""ˇ +// "# +// .unindent(), +// ); + +// // Reset +// cx.set_state( +// &r#" +// let x = ˇ +// "# +// .unindent(), +// ); + +// // Inserting a quotation mark inside of a string. A second quotation mark is not inserted. +// cx.update_editor(|editor, cx| { +// editor.handle_input("\"", cx); +// editor.handle_input(" ", cx); +// editor.move_left(&Default::default(), cx); +// editor.handle_input("\\", cx); +// editor.handle_input("\"", cx); +// }); +// cx.assert_editor_state( +// &r#" +// let x = "\"ˇ " +// "# +// .unindent(), +// ); + +// // Inserting a closing quotation mark at the position of an automatically-inserted quotation +// // mark. Nothing is inserted. +// cx.update_editor(|editor, cx| { +// editor.move_right(&Default::default(), cx); +// editor.handle_input("\"", cx); +// }); +// cx.assert_editor_state( +// &r#" +// let x = "\" "ˇ +// "# +// .unindent(), +// ); +// } + +// #[gpui::test] +// async fn test_surround_with_pair(cx: &mut gpui::TestAppContext) { +// init_test(cx, |_| {}); + +// let language = Arc::new(Language::new( +// LanguageConfig { +// brackets: BracketPairConfig { +// pairs: vec![ +// BracketPair { +// start: "{".to_string(), +// end: "}".to_string(), +// close: true, +// newline: true, +// }, +// BracketPair { +// start: "/* ".to_string(), +// end: "*/".to_string(), +// close: true, +// ..Default::default() +// }, +// ], +// ..Default::default() +// }, +// ..Default::default() +// }, +// Some(tree_sitter_rust::language()), +// )); + +// let text = r#" +// a +// b +// c +// "# +// .unindent(); + +// let buffer = +// cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, text).with_language(language, cx)); +// let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); +// let view = cx.add_window(|cx| build_editor(buffer, cx)).root(cx); +// view.condition(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx)) +// .await; + +// view.update(cx, |view, cx| { +// view.change_selections(None, cx, |s| { +// s.select_display_ranges([ +// DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1), +// DisplayPoint::new(1, 0)..DisplayPoint::new(1, 1), +// DisplayPoint::new(2, 0)..DisplayPoint::new(2, 1), +// ]) +// }); + +// view.handle_input("{", cx); +// view.handle_input("{", cx); +// view.handle_input("{", cx); +// assert_eq!( +// view.text(cx), +// " +// {{{a}}} +// {{{b}}} +// {{{c}}} +// " +// .unindent() +// ); +// assert_eq!( +// view.selections.display_ranges(cx), +// [ +// DisplayPoint::new(0, 3)..DisplayPoint::new(0, 4), +// DisplayPoint::new(1, 3)..DisplayPoint::new(1, 4), +// DisplayPoint::new(2, 3)..DisplayPoint::new(2, 4) +// ] +// ); + +// view.undo(&Undo, cx); +// view.undo(&Undo, cx); +// view.undo(&Undo, cx); +// assert_eq!( +// view.text(cx), +// " +// a +// b +// c +// " +// .unindent() +// ); +// assert_eq!( +// view.selections.display_ranges(cx), +// [ +// DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1), +// DisplayPoint::new(1, 0)..DisplayPoint::new(1, 1), +// DisplayPoint::new(2, 0)..DisplayPoint::new(2, 1) +// ] +// ); + +// // Ensure inserting the first character of a multi-byte bracket pair +// // doesn't surround the selections with the bracket. +// view.handle_input("/", cx); +// assert_eq!( +// view.text(cx), +// " +// / +// / +// / +// " +// .unindent() +// ); +// assert_eq!( +// view.selections.display_ranges(cx), +// [ +// DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1), +// DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1), +// DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1) +// ] +// ); + +// view.undo(&Undo, cx); +// assert_eq!( +// view.text(cx), +// " +// a +// b +// c +// " +// .unindent() +// ); +// assert_eq!( +// view.selections.display_ranges(cx), +// [ +// DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1), +// DisplayPoint::new(1, 0)..DisplayPoint::new(1, 1), +// DisplayPoint::new(2, 0)..DisplayPoint::new(2, 1) +// ] +// ); + +// // Ensure inserting the last character of a multi-byte bracket pair +// // doesn't surround the selections with the bracket. +// view.handle_input("*", cx); +// assert_eq!( +// view.text(cx), +// " +// * +// * +// * +// " +// .unindent() +// ); +// assert_eq!( +// view.selections.display_ranges(cx), +// [ +// DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1), +// DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1), +// DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1) +// ] +// ); +// }); +// } + +// #[gpui::test] +// async fn test_delete_autoclose_pair(cx: &mut gpui::TestAppContext) { +// init_test(cx, |_| {}); + +// let language = Arc::new(Language::new( +// LanguageConfig { +// brackets: BracketPairConfig { +// pairs: vec![BracketPair { +// start: "{".to_string(), +// end: "}".to_string(), +// close: true, +// newline: true, +// }], +// ..Default::default() +// }, +// autoclose_before: "}".to_string(), +// ..Default::default() +// }, +// Some(tree_sitter_rust::language()), +// )); + +// let text = r#" +// a +// b +// c +// "# +// .unindent(); + +// let buffer = +// cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, text).with_language(language, cx)); +// let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); +// let editor = cx.add_window(|cx| build_editor(buffer, cx)).root(cx); +// editor +// .condition(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx)) +// .await; + +// editor.update(cx, |editor, cx| { +// editor.change_selections(None, cx, |s| { +// s.select_ranges([ +// Point::new(0, 1)..Point::new(0, 1), +// Point::new(1, 1)..Point::new(1, 1), +// Point::new(2, 1)..Point::new(2, 1), +// ]) +// }); + +// editor.handle_input("{", cx); +// editor.handle_input("{", cx); +// editor.handle_input("_", cx); +// assert_eq!( +// editor.text(cx), +// " +// a{{_}} +// b{{_}} +// c{{_}} +// " +// .unindent() +// ); +// assert_eq!( +// editor.selections.ranges::(cx), +// [ +// Point::new(0, 4)..Point::new(0, 4), +// Point::new(1, 4)..Point::new(1, 4), +// Point::new(2, 4)..Point::new(2, 4) +// ] +// ); + +// editor.backspace(&Default::default(), cx); +// editor.backspace(&Default::default(), cx); +// assert_eq!( +// editor.text(cx), +// " +// a{} +// b{} +// c{} +// " +// .unindent() +// ); +// assert_eq!( +// editor.selections.ranges::(cx), +// [ +// Point::new(0, 2)..Point::new(0, 2), +// Point::new(1, 2)..Point::new(1, 2), +// Point::new(2, 2)..Point::new(2, 2) +// ] +// ); + +// editor.delete_to_previous_word_start(&Default::default(), cx); +// assert_eq!( +// editor.text(cx), +// " +// a +// b +// c +// " +// .unindent() +// ); +// assert_eq!( +// editor.selections.ranges::(cx), +// [ +// Point::new(0, 1)..Point::new(0, 1), +// Point::new(1, 1)..Point::new(1, 1), +// Point::new(2, 1)..Point::new(2, 1) +// ] +// ); +// }); +// } + +// #[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 buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx)); +// let editor = cx.add_window(|cx| build_editor(buffer, cx)).root(cx); + +// 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(); + +// 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 +// "}, +// ); + +// // 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 +// "}, +// ); + +// 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 +// "}, +// ); + +// // 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) { +// init_test(cx, |_| {}); + +// 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 { +// document_formatting_provider: Some(lsp::OneOf::Left(true)), +// ..Default::default() +// }, +// ..Default::default() +// })) +// .await; + +// let fs = FakeFs::new(cx.background()); +// fs.insert_file("/file.rs", Default::default()).await; + +// let project = Project::test(fs, ["/file.rs".as_ref()], cx).await; +// project.update(cx, |project, _| project.languages().add(Arc::new(language))); +// let buffer = project +// .update(cx, |project, cx| project.open_local_buffer("/file.rs", cx)) +// .await +// .unwrap(); + +// cx.foreground().start_waiting(); +// let fake_server = fake_servers.next().await.unwrap(); + +// let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); +// let editor = cx.add_window(|cx| build_editor(buffer, cx)).root(cx); +// editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx)); +// assert!(cx.read(|cx| editor.is_dirty(cx))); + +// let save = editor.update(cx, |editor, cx| editor.save(project.clone(), cx)); +// fake_server +// .handle_request::(move |params, _| async move { +// assert_eq!( +// params.text_document.uri, +// lsp::Url::from_file_path("/file.rs").unwrap() +// ); +// assert_eq!(params.options.tab_size, 4); +// Ok(Some(vec![lsp::TextEdit::new( +// lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)), +// ", ".to_string(), +// )])) +// }) +// .next() +// .await; +// cx.foreground().start_waiting(); +// save.await.unwrap(); +// assert_eq!( +// editor.read_with(cx, |editor, cx| editor.text(cx)), +// "one, two\nthree\n" +// ); +// assert!(!cx.read(|cx| editor.is_dirty(cx))); + +// editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx)); +// assert!(cx.read(|cx| editor.is_dirty(cx))); + +// // Ensure we can still save even if formatting hangs. +// fake_server.handle_request::(move |params, _| async move { +// assert_eq!( +// params.text_document.uri, +// lsp::Url::from_file_path("/file.rs").unwrap() +// ); +// futures::future::pending::<()>().await; +// unreachable!() +// }); +// let save = editor.update(cx, |editor, cx| editor.save(project.clone(), cx)); +// cx.foreground().advance_clock(super::FORMAT_TIMEOUT); +// cx.foreground().start_waiting(); +// save.await.unwrap(); +// assert_eq!( +// editor.read_with(cx, |editor, cx| editor.text(cx)), +// "one\ntwo\nthree\n" +// ); +// assert!(!cx.read(|cx| editor.is_dirty(cx))); + +// // Set rust language override and assert overridden tabsize is sent to language server +// update_test_language_settings(cx, |settings| { +// settings.languages.insert( +// "Rust".into(), +// LanguageSettingsContent { +// tab_size: NonZeroU32::new(8), +// ..Default::default() +// }, +// ); +// }); + +// let save = editor.update(cx, |editor, cx| editor.save(project.clone(), cx)); +// fake_server +// .handle_request::(move |params, _| async move { +// assert_eq!( +// params.text_document.uri, +// lsp::Url::from_file_path("/file.rs").unwrap() +// ); +// assert_eq!(params.options.tab_size, 8); +// Ok(Some(vec![])) +// }) +// .next() +// .await; +// cx.foreground().start_waiting(); +// save.await.unwrap(); +// } + +// #[gpui::test] +// async fn test_range_format_during_save(cx: &mut gpui::TestAppContext) { +// init_test(cx, |_| {}); + +// 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 { +// document_range_formatting_provider: Some(lsp::OneOf::Left(true)), +// ..Default::default() +// }, +// ..Default::default() +// })) +// .await; + +// let fs = FakeFs::new(cx.background()); +// fs.insert_file("/file.rs", Default::default()).await; + +// let project = Project::test(fs, ["/file.rs".as_ref()], cx).await; +// project.update(cx, |project, _| project.languages().add(Arc::new(language))); +// let buffer = project +// .update(cx, |project, cx| project.open_local_buffer("/file.rs", cx)) +// .await +// .unwrap(); + +// cx.foreground().start_waiting(); +// let fake_server = fake_servers.next().await.unwrap(); + +// let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); +// let editor = cx.add_window(|cx| build_editor(buffer, cx)).root(cx); +// editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx)); +// assert!(cx.read(|cx| editor.is_dirty(cx))); + +// let save = editor.update(cx, |editor, cx| editor.save(project.clone(), cx)); +// fake_server +// .handle_request::(move |params, _| async move { +// assert_eq!( +// params.text_document.uri, +// lsp::Url::from_file_path("/file.rs").unwrap() +// ); +// assert_eq!(params.options.tab_size, 4); +// Ok(Some(vec![lsp::TextEdit::new( +// lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)), +// ", ".to_string(), +// )])) +// }) +// .next() +// .await; +// cx.foreground().start_waiting(); +// save.await.unwrap(); +// assert_eq!( +// editor.read_with(cx, |editor, cx| editor.text(cx)), +// "one, two\nthree\n" +// ); +// assert!(!cx.read(|cx| editor.is_dirty(cx))); + +// editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx)); +// assert!(cx.read(|cx| editor.is_dirty(cx))); + +// // Ensure we can still save even if formatting hangs. +// fake_server.handle_request::( +// move |params, _| async move { +// assert_eq!( +// params.text_document.uri, +// lsp::Url::from_file_path("/file.rs").unwrap() +// ); +// futures::future::pending::<()>().await; +// unreachable!() +// }, +// ); +// let save = editor.update(cx, |editor, cx| editor.save(project.clone(), cx)); +// cx.foreground().advance_clock(super::FORMAT_TIMEOUT); +// cx.foreground().start_waiting(); +// save.await.unwrap(); +// assert_eq!( +// editor.read_with(cx, |editor, cx| editor.text(cx)), +// "one\ntwo\nthree\n" +// ); +// assert!(!cx.read(|cx| editor.is_dirty(cx))); + +// // Set rust language override and assert overridden tabsize is sent to language server +// update_test_language_settings(cx, |settings| { +// settings.languages.insert( +// "Rust".into(), +// LanguageSettingsContent { +// tab_size: NonZeroU32::new(8), +// ..Default::default() +// }, +// ); +// }); + +// let save = editor.update(cx, |editor, cx| editor.save(project.clone(), cx)); +// fake_server +// .handle_request::(move |params, _| async move { +// assert_eq!( +// params.text_document.uri, +// lsp::Url::from_file_path("/file.rs").unwrap() +// ); +// assert_eq!(params.options.tab_size, 8); +// Ok(Some(vec![])) +// }) +// .next() +// .await; +// cx.foreground().start_waiting(); +// save.await.unwrap(); +// } + +// #[gpui::test] +// async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) { +// init_test(cx, |settings| { +// settings.defaults.formatter = Some(language_settings::Formatter::LanguageServer) +// }); + +// let mut language = Language::new( +// LanguageConfig { +// name: "Rust".into(), +// path_suffixes: vec!["rs".to_string()], +// // Enable Prettier formatting for the same buffer, and ensure +// // LSP is called instead of Prettier. +// prettier_parser_name: Some("test_parser".to_string()), +// ..Default::default() +// }, +// Some(tree_sitter_rust::language()), +// ); +// let mut fake_servers = language +// .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { +// capabilities: lsp::ServerCapabilities { +// document_formatting_provider: Some(lsp::OneOf::Left(true)), +// ..Default::default() +// }, +// ..Default::default() +// })) +// .await; + +// let fs = FakeFs::new(cx.background()); +// fs.insert_file("/file.rs", Default::default()).await; + +// let project = Project::test(fs, ["/file.rs".as_ref()], cx).await; +// project.update(cx, |project, _| { +// project.languages().add(Arc::new(language)); +// }); +// let buffer = project +// .update(cx, |project, cx| project.open_local_buffer("/file.rs", cx)) +// .await +// .unwrap(); + +// cx.foreground().start_waiting(); +// let fake_server = fake_servers.next().await.unwrap(); + +// let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); +// let editor = cx.add_window(|cx| build_editor(buffer, cx)).root(cx); +// editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx)); + +// let format = editor.update(cx, |editor, cx| { +// editor.perform_format(project.clone(), FormatTrigger::Manual, cx) +// }); +// fake_server +// .handle_request::(move |params, _| async move { +// assert_eq!( +// params.text_document.uri, +// lsp::Url::from_file_path("/file.rs").unwrap() +// ); +// assert_eq!(params.options.tab_size, 4); +// Ok(Some(vec![lsp::TextEdit::new( +// lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)), +// ", ".to_string(), +// )])) +// }) +// .next() +// .await; +// cx.foreground().start_waiting(); +// format.await.unwrap(); +// assert_eq!( +// editor.read_with(cx, |editor, cx| editor.text(cx)), +// "one, two\nthree\n" +// ); + +// editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx)); +// // Ensure we don't lock if formatting hangs. +// fake_server.handle_request::(move |params, _| async move { +// assert_eq!( +// params.text_document.uri, +// lsp::Url::from_file_path("/file.rs").unwrap() +// ); +// futures::future::pending::<()>().await; +// unreachable!() +// }); +// let format = editor.update(cx, |editor, cx| { +// editor.perform_format(project, FormatTrigger::Manual, cx) +// }); +// cx.foreground().advance_clock(super::FORMAT_TIMEOUT); +// cx.foreground().start_waiting(); +// format.await.unwrap(); +// assert_eq!( +// editor.read_with(cx, |editor, cx| editor.text(cx)), +// "one\ntwo\nthree\n" +// ); +// } + +// #[gpui::test] +// async fn test_concurrent_format_requests(cx: &mut gpui::TestAppContext) { +// init_test(cx, |_| {}); + +// let mut cx = EditorLspTestContext::new_rust( +// lsp::ServerCapabilities { +// document_formatting_provider: Some(lsp::OneOf::Left(true)), +// ..Default::default() +// }, +// cx, +// ) +// .await; + +// cx.set_state(indoc! {" +// one.twoˇ +// "}); + +// // The format request takes a long time. When it completes, it inserts +// // a newline and an indent before the `.` +// cx.lsp +// .handle_request::(move |_, cx| { +// let executor = cx.background(); +// async move { +// executor.timer(Duration::from_millis(100)).await; +// Ok(Some(vec![lsp::TextEdit { +// range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)), +// new_text: "\n ".into(), +// }])) +// } +// }); + +// // Submit a format request. +// let format_1 = cx +// .update_editor(|editor, cx| editor.format(&Format, cx)) +// .unwrap(); +// cx.foreground().run_until_parked(); + +// // Submit a second format request. +// let format_2 = cx +// .update_editor(|editor, cx| editor.format(&Format, cx)) +// .unwrap(); +// cx.foreground().run_until_parked(); + +// // Wait for both format requests to complete +// cx.foreground().advance_clock(Duration::from_millis(200)); +// cx.foreground().start_waiting(); +// format_1.await.unwrap(); +// cx.foreground().start_waiting(); +// format_2.await.unwrap(); + +// // The formatting edits only happens once. +// cx.assert_editor_state(indoc! {" +// one +// .twoˇ +// "}); +// } + +// #[gpui::test] +// async fn test_strip_whitespace_and_format_via_lsp(cx: &mut gpui::TestAppContext) { +// init_test(cx, |settings| { +// settings.defaults.formatter = Some(language_settings::Formatter::Auto) +// }); + +// let mut cx = EditorLspTestContext::new_rust( +// lsp::ServerCapabilities { +// document_formatting_provider: Some(lsp::OneOf::Left(true)), +// ..Default::default() +// }, +// cx, +// ) +// .await; + +// // Set up a buffer white some trailing whitespace and no trailing newline. +// cx.set_state( +// &[ +// "one ", // +// "twoˇ", // +// "three ", // +// "four", // +// ] +// .join("\n"), +// ); + +// // Submit a format request. +// let format = cx +// .update_editor(|editor, cx| editor.format(&Format, cx)) +// .unwrap(); + +// // Record which buffer changes have been sent to the language server +// let buffer_changes = Arc::new(Mutex::new(Vec::new())); +// cx.lsp +// .handle_notification::({ +// let buffer_changes = buffer_changes.clone(); +// move |params, _| { +// buffer_changes.lock().extend( +// params +// .content_changes +// .into_iter() +// .map(|e| (e.range.unwrap(), e.text)), +// ); +// } +// }); + +// // Handle formatting requests to the language server. +// cx.lsp.handle_request::({ +// let buffer_changes = buffer_changes.clone(); +// move |_, _| { +// // When formatting is requested, trailing whitespace has already been stripped, +// // and the trailing newline has already been added. +// assert_eq!( +// &buffer_changes.lock()[1..], +// &[ +// ( +// lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)), +// "".into() +// ), +// ( +// lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)), +// "".into() +// ), +// ( +// lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)), +// "\n".into() +// ), +// ] +// ); + +// // Insert blank lines between each line of the buffer. +// async move { +// Ok(Some(vec![ +// lsp::TextEdit { +// range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 0)), +// new_text: "\n".into(), +// }, +// lsp::TextEdit { +// range: lsp::Range::new(lsp::Position::new(2, 0), lsp::Position::new(2, 0)), +// new_text: "\n".into(), +// }, +// ])) +// } +// } +// }); + +// // After formatting the buffer, the trailing whitespace is stripped, +// // a newline is appended, and the edits provided by the language server +// // have been applied. +// format.await.unwrap(); +// cx.assert_editor_state( +// &[ +// "one", // +// "", // +// "twoˇ", // +// "", // +// "three", // +// "four", // +// "", // +// ] +// .join("\n"), +// ); + +// // Undoing the formatting undoes the trailing whitespace removal, the +// // trailing newline, and the LSP edits. +// cx.update_buffer(|buffer, cx| buffer.undo(cx)); +// cx.assert_editor_state( +// &[ +// "one ", // +// "twoˇ", // +// "three ", // +// "four", // +// ] +// .join("\n"), +// ); +// } + +// #[gpui::test] +// async fn test_completion(cx: &mut gpui::TestAppContext) { +// init_test(cx, |_| {}); + +// let mut cx = EditorLspTestContext::new_rust( +// lsp::ServerCapabilities { +// completion_provider: Some(lsp::CompletionOptions { +// trigger_characters: Some(vec![".".to_string(), ":".to_string()]), +// resolve_provider: Some(true), +// ..Default::default() +// }), +// ..Default::default() +// }, +// cx, +// ) +// .await; + +// cx.set_state(indoc! {" +// oneˇ +// two +// three +// "}); +// cx.simulate_keystroke("."); +// handle_completion_request( +// &mut cx, +// indoc! {" +// one.|<> +// two +// three +// "}, +// vec!["first_completion", "second_completion"], +// ) +// .await; +// cx.condition(|editor, _| editor.context_menu_visible()) +// .await; +// let apply_additional_edits = cx.update_editor(|editor, cx| { +// editor.context_menu_next(&Default::default(), cx); +// editor +// .confirm_completion(&ConfirmCompletion::default(), cx) +// .unwrap() +// }); +// cx.assert_editor_state(indoc! {" +// one.second_completionˇ +// two +// three +// "}); + +// handle_resolve_completion_request( +// &mut cx, +// Some(vec![ +// ( +// //This overlaps with the primary completion edit which is +// //misbehavior from the LSP spec, test that we filter it out +// indoc! {" +// one.second_ˇcompletion +// two +// threeˇ +// "}, +// "overlapping additional edit", +// ), +// ( +// indoc! {" +// one.second_completion +// two +// threeˇ +// "}, +// "\nadditional edit", +// ), +// ]), +// ) +// .await; +// apply_additional_edits.await.unwrap(); +// cx.assert_editor_state(indoc! {" +// one.second_completionˇ +// two +// three +// additional edit +// "}); + +// cx.set_state(indoc! {" +// one.second_completion +// twoˇ +// threeˇ +// additional edit +// "}); +// cx.simulate_keystroke(" "); +// assert!(cx.editor(|e, _| e.context_menu.read().is_none())); +// cx.simulate_keystroke("s"); +// assert!(cx.editor(|e, _| e.context_menu.read().is_none())); + +// cx.assert_editor_state(indoc! {" +// one.second_completion +// two sˇ +// three sˇ +// additional edit +// "}); +// handle_completion_request( +// &mut cx, +// indoc! {" +// one.second_completion +// two s +// three +// additional edit +// "}, +// vec!["fourth_completion", "fifth_completion", "sixth_completion"], +// ) +// .await; +// cx.condition(|editor, _| editor.context_menu_visible()) +// .await; + +// cx.simulate_keystroke("i"); + +// handle_completion_request( +// &mut cx, +// indoc! {" +// one.second_completion +// two si +// three +// additional edit +// "}, +// vec!["fourth_completion", "fifth_completion", "sixth_completion"], +// ) +// .await; +// cx.condition(|editor, _| editor.context_menu_visible()) +// .await; + +// let apply_additional_edits = cx.update_editor(|editor, cx| { +// editor +// .confirm_completion(&ConfirmCompletion::default(), cx) +// .unwrap() +// }); +// cx.assert_editor_state(indoc! {" +// one.second_completion +// two sixth_completionˇ +// three sixth_completionˇ +// additional edit +// "}); + +// handle_resolve_completion_request(&mut cx, None).await; +// apply_additional_edits.await.unwrap(); + +// cx.update(|cx| { +// cx.update_global::(|settings, cx| { +// settings.update_user_settings::(cx, |settings| { +// settings.show_completions_on_input = Some(false); +// }); +// }) +// }); +// cx.set_state("editorˇ"); +// cx.simulate_keystroke("."); +// assert!(cx.editor(|e, _| e.context_menu.read().is_none())); +// cx.simulate_keystroke("c"); +// cx.simulate_keystroke("l"); +// cx.simulate_keystroke("o"); +// cx.assert_editor_state("editor.cloˇ"); +// assert!(cx.editor(|e, _| e.context_menu.read().is_none())); +// cx.update_editor(|editor, cx| { +// editor.show_completions(&ShowCompletions, cx); +// }); +// handle_completion_request(&mut cx, "editor.", vec!["close", "clobber"]).await; +// cx.condition(|editor, _| editor.context_menu_visible()) +// .await; +// let apply_additional_edits = cx.update_editor(|editor, cx| { +// editor +// .confirm_completion(&ConfirmCompletion::default(), cx) +// .unwrap() +// }); +// cx.assert_editor_state("editor.closeˇ"); +// handle_resolve_completion_request(&mut cx, None).await; +// apply_additional_edits.await.unwrap(); +// } + +// #[gpui::test] +// async fn test_toggle_comment(cx: &mut gpui::TestAppContext) { +// init_test(cx, |_| {}); +// let mut cx = EditorTestContext::new(cx).await; +// let language = Arc::new(Language::new( +// LanguageConfig { +// line_comment: Some("// ".into()), +// ..Default::default() +// }, +// Some(tree_sitter_rust::language()), +// )); +// cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx)); + +// // If multiple selections intersect a line, the line is only toggled once. +// cx.set_state(indoc! {" +// fn a() { +// «//b(); +// ˇ»// «c(); +// //ˇ» d(); +// } +// "}); + +// cx.update_editor(|e, cx| e.toggle_comments(&ToggleComments::default(), cx)); + +// cx.assert_editor_state(indoc! {" +// fn a() { +// «b(); +// c(); +// ˇ» d(); +// } +// "}); + +// // The comment prefix is inserted at the same column for every line in a +// // selection. +// cx.update_editor(|e, cx| e.toggle_comments(&ToggleComments::default(), cx)); + +// cx.assert_editor_state(indoc! {" +// fn a() { +// // «b(); +// // c(); +// ˇ»// d(); +// } +// "}); + +// // If a selection ends at the beginning of a line, that line is not toggled. +// cx.set_selections_state(indoc! {" +// fn a() { +// // b(); +// «// c(); +// ˇ» // d(); +// } +// "}); + +// cx.update_editor(|e, cx| e.toggle_comments(&ToggleComments::default(), cx)); + +// cx.assert_editor_state(indoc! {" +// fn a() { +// // b(); +// «c(); +// ˇ» // d(); +// } +// "}); + +// // If a selection span a single line and is empty, the line is toggled. +// cx.set_state(indoc! {" +// fn a() { +// a(); +// b(); +// ˇ +// } +// "}); + +// cx.update_editor(|e, cx| e.toggle_comments(&ToggleComments::default(), cx)); + +// cx.assert_editor_state(indoc! {" +// fn a() { +// a(); +// b(); +// //•ˇ +// } +// "}); + +// // If a selection span multiple lines, empty lines are not toggled. +// cx.set_state(indoc! {" +// fn a() { +// «a(); + +// c();ˇ» +// } +// "}); + +// cx.update_editor(|e, cx| e.toggle_comments(&ToggleComments::default(), cx)); + +// cx.assert_editor_state(indoc! {" +// fn a() { +// // «a(); + +// // c();ˇ» +// } +// "}); +// } + +// #[gpui::test] +// async fn test_advance_downward_on_toggle_comment(cx: &mut gpui::TestAppContext) { +// init_test(cx, |_| {}); + +// let language = Arc::new(Language::new( +// LanguageConfig { +// line_comment: Some("// ".into()), +// ..Default::default() +// }, +// Some(tree_sitter_rust::language()), +// )); + +// let registry = Arc::new(LanguageRegistry::test()); +// registry.add(language.clone()); + +// let mut cx = EditorTestContext::new(cx).await; +// cx.update_buffer(|buffer, cx| { +// buffer.set_language_registry(registry); +// buffer.set_language(Some(language), cx); +// }); + +// let toggle_comments = &ToggleComments { +// advance_downwards: true, +// }; + +// // Single cursor on one line -> advance +// // Cursor moves horizontally 3 characters as well on non-blank line +// cx.set_state(indoc!( +// "fn a() { +// ˇdog(); +// cat(); +// }" +// )); +// cx.update_editor(|editor, cx| { +// editor.toggle_comments(toggle_comments, cx); +// }); +// cx.assert_editor_state(indoc!( +// "fn a() { +// // dog(); +// catˇ(); +// }" +// )); + +// // Single selection on one line -> don't advance +// cx.set_state(indoc!( +// "fn a() { +// «dog()ˇ»; +// cat(); +// }" +// )); +// cx.update_editor(|editor, cx| { +// editor.toggle_comments(toggle_comments, cx); +// }); +// cx.assert_editor_state(indoc!( +// "fn a() { +// // «dog()ˇ»; +// cat(); +// }" +// )); + +// // Multiple cursors on one line -> advance +// cx.set_state(indoc!( +// "fn a() { +// ˇdˇog(); +// cat(); +// }" +// )); +// cx.update_editor(|editor, cx| { +// editor.toggle_comments(toggle_comments, cx); +// }); +// cx.assert_editor_state(indoc!( +// "fn a() { +// // dog(); +// catˇ(ˇ); +// }" +// )); + +// // Multiple cursors on one line, with selection -> don't advance +// cx.set_state(indoc!( +// "fn a() { +// ˇdˇog«()ˇ»; +// cat(); +// }" +// )); +// cx.update_editor(|editor, cx| { +// editor.toggle_comments(toggle_comments, cx); +// }); +// cx.assert_editor_state(indoc!( +// "fn a() { +// // ˇdˇog«()ˇ»; +// cat(); +// }" +// )); + +// // Single cursor on one line -> advance +// // Cursor moves to column 0 on blank line +// cx.set_state(indoc!( +// "fn a() { +// ˇdog(); + +// cat(); +// }" +// )); +// cx.update_editor(|editor, cx| { +// editor.toggle_comments(toggle_comments, cx); +// }); +// cx.assert_editor_state(indoc!( +// "fn a() { +// // dog(); +// ˇ +// cat(); +// }" +// )); + +// // Single cursor on one line -> advance +// // Cursor starts and ends at column 0 +// cx.set_state(indoc!( +// "fn a() { +// ˇ dog(); +// cat(); +// }" +// )); +// cx.update_editor(|editor, cx| { +// editor.toggle_comments(toggle_comments, cx); +// }); +// cx.assert_editor_state(indoc!( +// "fn a() { +// // dog(); +// ˇ cat(); +// }" +// )); +// } + +// #[gpui::test] +// async fn test_toggle_block_comment(cx: &mut gpui::TestAppContext) { +// init_test(cx, |_| {}); + +// let mut cx = EditorTestContext::new(cx).await; + +// let html_language = Arc::new( +// Language::new( +// LanguageConfig { +// name: "HTML".into(), +// block_comment: Some(("".into())), +// ..Default::default() +// }, +// Some(tree_sitter_html::language()), +// ) +// .with_injection_query( +// r#" +// (script_element +// (raw_text) @content +// (#set! "language" "javascript")) +// "#, +// ) +// .unwrap(), +// ); + +// let javascript_language = Arc::new(Language::new( +// LanguageConfig { +// name: "JavaScript".into(), +// line_comment: Some("// ".into()), +// ..Default::default() +// }, +// Some(tree_sitter_typescript::language_tsx()), +// )); + +// let registry = Arc::new(LanguageRegistry::test()); +// registry.add(html_language.clone()); +// registry.add(javascript_language.clone()); + +// cx.update_buffer(|buffer, cx| { +// buffer.set_language_registry(registry); +// buffer.set_language(Some(html_language), cx); +// }); + +// // Toggle comments for empty selections +// cx.set_state( +// &r#" +//

A

ˇ +//

B

ˇ +//

C

ˇ +// "# +// .unindent(), +// ); +// cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments::default(), cx)); +// cx.assert_editor_state( +// &r#" +// +// +// +// "# +// .unindent(), +// ); +// cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments::default(), cx)); +// cx.assert_editor_state( +// &r#" +//

A

ˇ +//

B

ˇ +//

C

ˇ +// "# +// .unindent(), +// ); + +// // Toggle comments for mixture of empty and non-empty selections, where +// // multiple selections occupy a given line. +// cx.set_state( +// &r#" +//

+//

ˇ»B

ˇ +//

+//

ˇ»D

ˇ +// "# +// .unindent(), +// ); + +// cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments::default(), cx)); +// cx.assert_editor_state( +// &r#" +// +// +// "# +// .unindent(), +// ); +// cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments::default(), cx)); +// cx.assert_editor_state( +// &r#" +//

+//

ˇ»B

ˇ +//

+//

ˇ»D

ˇ +// "# +// .unindent(), +// ); + +// // Toggle comments when different languages are active for different +// // selections. +// cx.set_state( +// &r#" +// ˇ +// "# +// .unindent(), +// ); +// cx.foreground().run_until_parked(); +// cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments::default(), cx)); +// cx.assert_editor_state( +// &r#" +// +// // ˇvar x = new Y(); +// +// "# +// .unindent(), +// ); +// } + +// #[gpui::test] +// fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) { +// init_test(cx, |_| {}); + +// let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, sample_text(3, 4, 'a'))); +// 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(0, 4), +// primary: None, +// }, +// ExcerptRange { +// context: Point::new(1, 0)..Point::new(1, 4), +// primary: None, +// }, +// ], +// cx, +// ); +// assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb"); +// multibuffer +// }); + +// let view = cx.add_window(|cx| build_editor(multibuffer, cx)).root(cx); +// view.update(cx, |view, cx| { +// assert_eq!(view.text(cx), "aaaa\nbbbb"); +// view.change_selections(None, cx, |s| { +// s.select_ranges([ +// Point::new(0, 0)..Point::new(0, 0), +// Point::new(1, 0)..Point::new(1, 0), +// ]) +// }); + +// view.handle_input("X", cx); +// assert_eq!(view.text(cx), "Xaaaa\nXbbbb"); +// assert_eq!( +// view.selections.ranges(cx), +// [ +// Point::new(0, 1)..Point::new(0, 1), +// Point::new(1, 1)..Point::new(1, 1), +// ] +// ); + +// // Ensure the cursor's head is respected when deleting across an excerpt boundary. +// view.change_selections(None, cx, |s| { +// s.select_ranges([Point::new(0, 2)..Point::new(1, 2)]) +// }); +// view.backspace(&Default::default(), cx); +// assert_eq!(view.text(cx), "Xa\nbbb"); +// assert_eq!( +// view.selections.ranges(cx), +// [Point::new(1, 0)..Point::new(1, 0)] +// ); + +// view.change_selections(None, cx, |s| { +// s.select_ranges([Point::new(1, 1)..Point::new(0, 1)]) +// }); +// view.backspace(&Default::default(), cx); +// assert_eq!(view.text(cx), "X\nbb"); +// assert_eq!( +// view.selections.ranges(cx), +// [Point::new(0, 1)..Point::new(0, 1)] +// ); +// }); +// } + +// #[gpui::test] +// fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) { +// init_test(cx, |_| {}); + +// let markers = vec![('[', ']').into(), ('(', ')').into()]; +// let (initial_text, mut excerpt_ranges) = marked_text_ranges_by( +// indoc! {" +// [aaaa +// (bbbb] +// cccc)", +// }, +// markers.clone(), +// ); +// let excerpt_ranges = markers.into_iter().map(|marker| { +// let context = excerpt_ranges.remove(&marker).unwrap()[0].clone(); +// ExcerptRange { +// context, +// primary: None, +// } +// }); +// let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, initial_text)); +// let multibuffer = cx.add_model(|cx| { +// let mut multibuffer = MultiBuffer::new(0); +// multibuffer.push_excerpts(buffer, excerpt_ranges, cx); +// multibuffer +// }); + +// let view = cx.add_window(|cx| build_editor(multibuffer, cx)).root(cx); +// view.update(cx, |view, cx| { +// let (expected_text, selection_ranges) = marked_text_ranges( +// indoc! {" +// aaaa +// bˇbbb +// bˇbbˇb +// cccc" +// }, +// true, +// ); +// assert_eq!(view.text(cx), expected_text); +// view.change_selections(None, cx, |s| s.select_ranges(selection_ranges)); + +// view.handle_input("X", cx); + +// let (expected_text, expected_selections) = marked_text_ranges( +// indoc! {" +// aaaa +// bXˇbbXb +// bXˇbbXˇb +// cccc" +// }, +// false, +// ); +// assert_eq!(view.text(cx), expected_text); +// assert_eq!(view.selections.ranges(cx), expected_selections); + +// view.newline(&Newline, cx); +// let (expected_text, expected_selections) = marked_text_ranges( +// indoc! {" +// aaaa +// bX +// ˇbbX +// b +// bX +// ˇbbX +// ˇb +// cccc" +// }, +// false, +// ); +// assert_eq!(view.text(cx), expected_text); +// assert_eq!(view.selections.ranges(cx), expected_selections); +// }); +// } + +// #[gpui::test] +// fn test_refresh_selections(cx: &mut TestAppContext) { +// init_test(cx, |_| {}); + +// let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, sample_text(3, 4, 'a'))); +// let mut excerpt1_id = None; +// let multibuffer = cx.add_model(|cx| { +// let mut multibuffer = MultiBuffer::new(0); +// excerpt1_id = multibuffer +// .push_excerpts( +// buffer.clone(), +// [ +// ExcerptRange { +// context: Point::new(0, 0)..Point::new(1, 4), +// primary: None, +// }, +// ExcerptRange { +// context: Point::new(1, 0)..Point::new(2, 4), +// primary: None, +// }, +// ], +// cx, +// ) +// .into_iter() +// .next(); +// assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc"); +// multibuffer +// }); + +// let editor = cx +// .add_window(|cx| { +// let mut editor = build_editor(multibuffer.clone(), cx); +// let snapshot = editor.snapshot(cx); +// editor.change_selections(None, cx, |s| { +// s.select_ranges([Point::new(1, 3)..Point::new(1, 3)]) +// }); +// editor.begin_selection(Point::new(2, 1).to_display_point(&snapshot), true, 1, cx); +// assert_eq!( +// editor.selections.ranges(cx), +// [ +// Point::new(1, 3)..Point::new(1, 3), +// Point::new(2, 1)..Point::new(2, 1), +// ] +// ); +// editor +// }) +// .root(cx); + +// // Refreshing selections is a no-op when excerpts haven't changed. +// editor.update(cx, |editor, cx| { +// editor.change_selections(None, cx, |s| s.refresh()); +// assert_eq!( +// editor.selections.ranges(cx), +// [ +// Point::new(1, 3)..Point::new(1, 3), +// Point::new(2, 1)..Point::new(2, 1), +// ] +// ); +// }); + +// multibuffer.update(cx, |multibuffer, cx| { +// multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx); +// }); +// editor.update(cx, |editor, cx| { +// // Removing an excerpt causes the first selection to become degenerate. +// assert_eq!( +// editor.selections.ranges(cx), +// [ +// Point::new(0, 0)..Point::new(0, 0), +// Point::new(0, 1)..Point::new(0, 1) +// ] +// ); + +// // Refreshing selections will relocate the first selection to the original buffer +// // location. +// editor.change_selections(None, cx, |s| s.refresh()); +// assert_eq!( +// editor.selections.ranges(cx), +// [ +// Point::new(0, 1)..Point::new(0, 1), +// Point::new(0, 3)..Point::new(0, 3) +// ] +// ); +// assert!(editor.selections.pending_anchor().is_some()); +// }); +// } + +// #[gpui::test] +// fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) { +// init_test(cx, |_| {}); + +// let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, sample_text(3, 4, 'a'))); +// let mut excerpt1_id = None; +// let multibuffer = cx.add_model(|cx| { +// let mut multibuffer = MultiBuffer::new(0); +// excerpt1_id = multibuffer +// .push_excerpts( +// buffer.clone(), +// [ +// ExcerptRange { +// context: Point::new(0, 0)..Point::new(1, 4), +// primary: None, +// }, +// ExcerptRange { +// context: Point::new(1, 0)..Point::new(2, 4), +// primary: None, +// }, +// ], +// cx, +// ) +// .into_iter() +// .next(); +// assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc"); +// multibuffer +// }); + +// let editor = cx +// .add_window(|cx| { +// let mut editor = build_editor(multibuffer.clone(), cx); +// let snapshot = editor.snapshot(cx); +// editor.begin_selection(Point::new(1, 3).to_display_point(&snapshot), false, 1, cx); +// assert_eq!( +// editor.selections.ranges(cx), +// [Point::new(1, 3)..Point::new(1, 3)] +// ); +// editor +// }) +// .root(cx); + +// multibuffer.update(cx, |multibuffer, cx| { +// multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx); +// }); +// editor.update(cx, |editor, cx| { +// assert_eq!( +// editor.selections.ranges(cx), +// [Point::new(0, 0)..Point::new(0, 0)] +// ); + +// // Ensure we don't panic when selections are refreshed and that the pending selection is finalized. +// editor.change_selections(None, cx, |s| s.refresh()); +// assert_eq!( +// editor.selections.ranges(cx), +// [Point::new(0, 3)..Point::new(0, 3)] +// ); +// assert!(editor.selections.pending_anchor().is_some()); +// }); +// } + +// #[gpui::test] +// async fn test_extra_newline_insertion(cx: &mut gpui::TestAppContext) { +// init_test(cx, |_| {}); + +// let language = Arc::new( +// Language::new( +// LanguageConfig { +// brackets: BracketPairConfig { +// pairs: vec![ +// BracketPair { +// start: "{".to_string(), +// end: "}".to_string(), +// close: true, +// newline: true, +// }, +// BracketPair { +// start: "/* ".to_string(), +// end: " */".to_string(), +// close: true, +// newline: true, +// }, +// ], +// ..Default::default() +// }, +// ..Default::default() +// }, +// Some(tree_sitter_rust::language()), +// ) +// .with_indents_query("") +// .unwrap(), +// ); + +// let text = concat!( +// "{ }\n", // +// " x\n", // +// " /* */\n", // +// "x\n", // +// "{{} }\n", // +// ); + +// let buffer = +// cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, text).with_language(language, cx)); +// let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); +// let view = cx.add_window(|cx| build_editor(buffer, cx)).root(cx); +// view.condition(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx)) +// .await; + +// view.update(cx, |view, cx| { +// view.change_selections(None, cx, |s| { +// s.select_display_ranges([ +// DisplayPoint::new(0, 2)..DisplayPoint::new(0, 3), +// DisplayPoint::new(2, 5)..DisplayPoint::new(2, 5), +// DisplayPoint::new(4, 4)..DisplayPoint::new(4, 4), +// ]) +// }); +// view.newline(&Newline, cx); + +// assert_eq!( +// view.buffer().read(cx).read(cx).text(), +// concat!( +// "{ \n", // Suppress rustfmt +// "\n", // +// "}\n", // +// " x\n", // +// " /* \n", // +// " \n", // +// " */\n", // +// "x\n", // +// "{{} \n", // +// "}\n", // +// ) +// ); +// }); +// } + +// #[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) +// }) +// .root(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, +// ); + +// 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, +// theme::current(cx).as_ref(), +// ); +// // Enforce a consistent ordering based on color without relying on the ordering of the +// // highlight's `TypeId` which is non-deterministic. +// 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, +// theme::current(cx).as_ref(), +// ), +// &[( +// DisplayPoint::new(6, 3)..DisplayPoint::new(6, 5), +// Hsla::red(), +// )] +// ); +// }); +// } + +// #[gpui::test] +// async fn test_following(cx: &mut gpui::TestAppContext) { +// init_test(cx, |_| {}); + +// let fs = FakeFs::new(cx.background()); +// let project = Project::test(fs, ["/file.rs".as_ref()], cx).await; + +// let buffer = project.update(cx, |project, cx| { +// let buffer = project +// .create_buffer(&sample_text(16, 8, 'a'), None, cx) +// .unwrap(); +// cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)) +// }); +// let leader = cx +// .add_window(|cx| build_editor(buffer.clone(), cx)) +// .root(cx); +// let follower = cx +// .update(|cx| { +// cx.add_window( +// WindowOptions { +// bounds: WindowBounds::Fixed(RectF::from_points(vec2f(0., 0.), vec2f(10., 80.))), +// ..Default::default() +// }, +// |cx| build_editor(buffer.clone(), cx), +// ) +// }) +// .root(cx); + +// 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, move |_, leader, event, cx| { +// leader +// .read(cx) +// .add_event_to_update_proto(event, &mut *update.borrow_mut(), cx); +// }) +// .detach(); + +// cx.subscribe(&follower, move |_, _, event, cx| { +// if Editor::should_unfollow_on_event(event, cx) { +// *is_still_following.borrow_mut() = false; +// } +// if let Event::BufferEdited = event { +// *follower_edit_event_count.borrow_mut() += 1; +// } +// }) +// .detach(); +// } +// }); + +// // 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) +// }) +// .await +// .unwrap(); +// follower.read_with(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 scroll position only +// leader.update(cx, |leader, cx| { +// leader.set_scroll_position(vec2f(1.5, 3.5), cx); +// }); +// follower +// .update(cx, |follower, cx| { +// follower.apply_update_proto(&project, pending_update.borrow_mut().take().unwrap(), cx) +// }) +// .await +// .unwrap(); +// assert_eq!( +// follower.update(cx, |follower, cx| follower.scroll_position(cx)), +// vec2f(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(vec2f(1.5, 3.5), cx); +// }); +// follower +// .update(cx, |follower, cx| { +// follower.apply_update_proto(&project, pending_update.borrow_mut().take().unwrap(), cx) +// }) +// .await +// .unwrap(); +// follower.update(cx, |follower, cx| { +// assert_eq!(follower.scroll_position(cx), vec2f(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) +// }) +// .await +// .unwrap(); +// follower.read_with(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) +// }) +// .await +// .unwrap(); +// follower.read_with(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: vec2f(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) { +// init_test(cx, |_| {}); + +// let fs = FakeFs::new(cx.background()); +// let project = Project::test(fs, ["/file.rs".as_ref()], cx).await; +// let workspace = cx +// .add_window(|cx| Workspace::test_new(project.clone(), cx)) +// .root(cx); +// let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); + +// let leader = pane.update(cx, |_, cx| { +// let multibuffer = cx.add_model(|_| MultiBuffer::new(0)); +// cx.add_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.clone(), +// 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(); +// } +// }); + +// 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(), +// ) +// }); + +// // 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, +// ); +// }); +// }); + +// // 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.read_with(cx, |editor, cx| editor.text(cx)), +// leader.read_with(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.read_with(cx, |editor, cx| editor.text(cx)), +// leader.read_with(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.read_with(cx, |editor, cx| editor.text(cx)), +// leader.read_with(cx, |editor, cx| editor.text(cx)) +// ); +// } + +// #[test] +// fn test_combine_syntax_and_fuzzy_match_highlights() { +// let string = "abcdefghijklmnop"; +// let syntax_ranges = [ +// ( +// 0..3, +// HighlightStyle { +// color: Some(Hsla::red()), +// ..Default::default() +// }, +// ), +// ( +// 4..8, +// HighlightStyle { +// color: Some(Hsla::green()), +// ..Default::default() +// }, +// ), +// ]; +// let match_indices = [4, 6, 7, 8]; +// assert_eq!( +// combine_syntax_and_fuzzy_match_highlights( +// string, +// Default::default(), +// syntax_ranges.into_iter(), +// &match_indices, +// ), +// &[ +// ( +// 0..3, +// HighlightStyle { +// color: Some(Hsla::red()), +// ..Default::default() +// }, +// ), +// ( +// 4..5, +// HighlightStyle { +// color: Some(Hsla::green()), +// weight: Some(fonts::Weight::BOLD), +// ..Default::default() +// }, +// ), +// ( +// 5..6, +// HighlightStyle { +// color: Some(Hsla::green()), +// ..Default::default() +// }, +// ), +// ( +// 6..8, +// HighlightStyle { +// color: Some(Hsla::green()), +// weight: Some(fonts::Weight::BOLD), +// ..Default::default() +// }, +// ), +// ( +// 8..9, +// HighlightStyle { +// weight: Some(fonts::Weight::BOLD), +// ..Default::default() +// }, +// ), +// ] +// ); +// } + +// #[gpui::test] +// async fn go_to_prev_overlapping_diagnostic( +// deterministic: Arc, +// cx: &mut gpui::TestAppContext, +// ) { +// init_test(cx, |_| {}); + +// let mut cx = EditorTestContext::new(cx).await; +// let project = cx.update_editor(|editor, _| editor.project.clone().unwrap()); + +// cx.set_state(indoc! {" +// ˇfn func(abc def: i32) -> u32 { +// } +// "}); + +// cx.update(|cx| { +// project.update(cx, |project, cx| { +// project +// .update_diagnostics( +// LanguageServerId(0), +// lsp::PublishDiagnosticsParams { +// uri: lsp::Url::from_file_path("/root/file").unwrap(), +// version: None, +// diagnostics: vec![ +// lsp::Diagnostic { +// range: lsp::Range::new( +// lsp::Position::new(0, 11), +// lsp::Position::new(0, 12), +// ), +// severity: Some(lsp::DiagnosticSeverity::ERROR), +// ..Default::default() +// }, +// lsp::Diagnostic { +// range: lsp::Range::new( +// lsp::Position::new(0, 12), +// lsp::Position::new(0, 15), +// ), +// severity: Some(lsp::DiagnosticSeverity::ERROR), +// ..Default::default() +// }, +// lsp::Diagnostic { +// range: lsp::Range::new( +// lsp::Position::new(0, 25), +// lsp::Position::new(0, 28), +// ), +// severity: Some(lsp::DiagnosticSeverity::ERROR), +// ..Default::default() +// }, +// ], +// }, +// &[], +// cx, +// ) +// .unwrap() +// }); +// }); + +// deterministic.run_until_parked(); + +// cx.update_editor(|editor, cx| { +// editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, cx); +// }); + +// cx.assert_editor_state(indoc! {" +// fn func(abc def: i32) -> ˇu32 { +// } +// "}); + +// cx.update_editor(|editor, cx| { +// editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, cx); +// }); + +// cx.assert_editor_state(indoc! {" +// fn func(abc ˇdef: i32) -> u32 { +// } +// "}); + +// cx.update_editor(|editor, cx| { +// editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, cx); +// }); + +// cx.assert_editor_state(indoc! {" +// fn func(abcˇ def: i32) -> u32 { +// } +// "}); + +// cx.update_editor(|editor, cx| { +// editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, cx); +// }); + +// cx.assert_editor_state(indoc! {" +// fn func(abc def: i32) -> ˇu32 { +// } +// "}); +// } + +// #[gpui::test] +// async fn go_to_hunk(deterministic: Arc, cx: &mut gpui::TestAppContext) { +// init_test(cx, |_| {}); + +// let mut cx = EditorTestContext::new(cx).await; + +// let diff_base = r#" +// use some::mod; + +// const A: u32 = 42; + +// fn main() { +// println!("hello"); + +// println!("world"); +// } +// "# +// .unindent(); + +// // Edits are modified, removed, modified, added +// cx.set_state( +// &r#" +// use some::modified; + +// ˇ +// fn main() { +// println!("hello there"); + +// println!("around the"); +// println!("world"); +// } +// "# +// .unindent(), +// ); + +// cx.set_diff_base(Some(&diff_base)); +// deterministic.run_until_parked(); + +// cx.update_editor(|editor, cx| { +// //Wrap around the bottom of the buffer +// for _ in 0..3 { +// editor.go_to_hunk(&GoToHunk, cx); +// } +// }); + +// cx.assert_editor_state( +// &r#" +// ˇuse some::modified; + +// fn main() { +// println!("hello there"); + +// println!("around the"); +// println!("world"); +// } +// "# +// .unindent(), +// ); + +// cx.update_editor(|editor, cx| { +// //Wrap around the top of the buffer +// for _ in 0..2 { +// editor.go_to_prev_hunk(&GoToPrevHunk, cx); +// } +// }); + +// cx.assert_editor_state( +// &r#" +// use some::modified; + +// fn main() { +// ˇ println!("hello there"); + +// println!("around the"); +// println!("world"); +// } +// "# +// .unindent(), +// ); + +// cx.update_editor(|editor, cx| { +// editor.go_to_prev_hunk(&GoToPrevHunk, cx); +// }); + +// cx.assert_editor_state( +// &r#" +// use some::modified; + +// ˇ +// fn main() { +// println!("hello there"); + +// println!("around the"); +// println!("world"); +// } +// "# +// .unindent(), +// ); + +// cx.update_editor(|editor, cx| { +// for _ in 0..3 { +// editor.go_to_prev_hunk(&GoToPrevHunk, cx); +// } +// }); + +// cx.assert_editor_state( +// &r#" +// use some::modified; + +// fn main() { +// ˇ println!("hello there"); + +// println!("around the"); +// println!("world"); +// } +// "# +// .unindent(), +// ); + +// cx.update_editor(|editor, cx| { +// editor.fold(&Fold, cx); + +// //Make sure that the fold only gets one hunk +// for _ in 0..4 { +// editor.go_to_hunk(&GoToHunk, cx); +// } +// }); + +// cx.assert_editor_state( +// &r#" +// ˇuse some::modified; + +// fn main() { +// println!("hello there"); + +// println!("around the"); +// println!("world"); +// } +// "# +// .unindent(), +// ); +// } + +// #[test] +// fn test_split_words() { +// fn split<'a>(text: &'a str) -> Vec<&'a str> { +// split_words(text).collect() +// } + +// assert_eq!(split("HelloWorld"), &["Hello", "World"]); +// assert_eq!(split("hello_world"), &["hello_", "world"]); +// assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]); +// assert_eq!(split("Hello_World"), &["Hello_", "World"]); +// assert_eq!(split("helloWOrld"), &["hello", "WOrld"]); +// assert_eq!(split("helloworld"), &["helloworld"]); +// } + +// #[gpui::test] +// async fn test_move_to_enclosing_bracket(cx: &mut gpui::TestAppContext) { +// init_test(cx, |_| {}); + +// let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await; +// let mut assert = |before, after| { +// let _state_context = cx.set_state(before); +// cx.update_editor(|editor, cx| { +// editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, cx) +// }); +// cx.assert_editor_state(after); +// }; + +// // Outside bracket jumps to outside of matching bracket +// assert("console.logˇ(var);", "console.log(var)ˇ;"); +// assert("console.log(var)ˇ;", "console.logˇ(var);"); + +// // Inside bracket jumps to inside of matching bracket +// assert("console.log(ˇvar);", "console.log(varˇ);"); +// assert("console.log(varˇ);", "console.log(ˇvar);"); + +// // When outside a bracket and inside, favor jumping to the inside bracket +// assert( +// "console.log('foo', [1, 2, 3]ˇ);", +// "console.log(ˇ'foo', [1, 2, 3]);", +// ); +// assert( +// "console.log(ˇ'foo', [1, 2, 3]);", +// "console.log('foo', [1, 2, 3]ˇ);", +// ); + +// // Bias forward if two options are equally likely +// assert( +// "let result = curried_fun()ˇ();", +// "let result = curried_fun()()ˇ;", +// ); + +// // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller +// assert( +// indoc! {" +// function test() { +// console.log('test')ˇ +// }"}, +// indoc! {" +// function test() { +// console.logˇ('test') +// }"}, +// ); +// } + +// #[gpui::test(iterations = 10)] +// async fn test_copilot(deterministic: Arc, cx: &mut gpui::TestAppContext) { +// 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; + +// // 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![], +// ); +// deterministic.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"); +// }); + +// // 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![], +// ); +// deterministic.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![], +// ); +// deterministic.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"); +// }); + +// // Ensure existing completion is interpolated when inserting again. +// cx.simulate_keystroke("c"); +// deterministic.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![], +// ); +// deterministic.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"); + +// // 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"); +// }); + +// // 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"); + +// // 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"); +// }); + +// // 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![], +// ); + +// cx.update_editor(|editor, cx| editor.next_copilot_suggestion(&Default::default(), cx)); +// deterministic.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 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( +// deterministic: Arc, +// cx: &mut gpui::TestAppContext, +// ) { +// 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; + +// cx.set_state(indoc! {" +// one +// twˇ +// three +// "}); + +// handle_copilot_completion_request( +// &copilot_lsp, +// vec![copilot::request::Completion { +// text: "two.foo()".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)); +// deterministic.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); +// cx.update_editor(|editor, cx| { +// assert!(editor.has_active_copilot_suggestion(cx)); +// assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n"); +// assert_eq!(editor.text(cx), "one\ntw\nthree\n"); + +// editor.backspace(&Default::default(), cx); +// assert!(editor.has_active_copilot_suggestion(cx)); +// assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n"); +// assert_eq!(editor.text(cx), "one\nt\nthree\n"); + +// editor.backspace(&Default::default(), cx); +// assert!(editor.has_active_copilot_suggestion(cx)); +// assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n"); +// assert_eq!(editor.text(cx), "one\n\nthree\n"); + +// // Deleting across the original suggestion range invalidates it. +// editor.backspace(&Default::default(), cx); +// assert!(!editor.has_active_copilot_suggestion(cx)); +// assert_eq!(editor.display_text(cx), "one\nthree\n"); +// assert_eq!(editor.text(cx), "one\nthree\n"); + +// // Undoing the deletion restores the suggestion. +// editor.undo(&Default::default(), cx); +// assert!(editor.has_active_copilot_suggestion(cx)); +// assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n"); +// assert_eq!(editor.text(cx), "one\n\nthree\n"); +// }); +// } + +// #[gpui::test] +// async fn test_copilot_multibuffer( +// deterministic: Arc, +// 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.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "a = 1\nb = 2\n")); +// let buffer_2 = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "c = 3\nd = 4\n")); +// let multibuffer = cx.add_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)).root(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); +// }); +// deterministic.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. +// deterministic.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( +// deterministic: Arc, +// cx: &mut gpui::TestAppContext, +// ) { +// init_test(cx, |settings| { +// settings +// .copilot +// .get_or_insert(Default::default()) +// .disabled_globs = Some(vec![".env*".to_string()]); +// }); + +// let (copilot, copilot_lsp) = Copilot::fake(cx); +// cx.update(|cx| cx.set_global(copilot)); + +// let fs = FakeFs::new(cx.background()); +// fs.insert_tree( +// "/test", +// json!({ +// ".env": "SECRET=something\n", +// "README.md": "hello\n" +// }), +// ) +// .await; +// let project = Project::test(fs, ["/test".as_ref()], cx).await; + +// let private_buffer = project +// .update(cx, |project, cx| { +// project.open_local_buffer("/test/.env", cx) +// }) +// .await +// .unwrap(); +// let public_buffer = project +// .update(cx, |project, cx| { +// project.open_local_buffer("/test/README.md", cx) +// }) +// .await +// .unwrap(); + +// let multibuffer = cx.add_model(|cx| { +// let mut multibuffer = MultiBuffer::new(0); +// multibuffer.push_excerpts( +// private_buffer.clone(), +// [ExcerptRange { +// context: Point::new(0, 0)..Point::new(1, 0), +// primary: None, +// }], +// cx, +// ); +// multibuffer.push_excerpts( +// public_buffer.clone(), +// [ExcerptRange { +// context: Point::new(0, 0)..Point::new(1, 0), +// primary: None, +// }], +// cx, +// ); +// multibuffer +// }); +// let editor = cx.add_window(|cx| build_editor(multibuffer, cx)).root(cx); + +// let mut copilot_requests = copilot_lsp +// .handle_request::(move |_params, _cx| async move { +// Ok(copilot::request::GetCompletionsResult { +// completions: vec![copilot::request::Completion { +// text: "next line".into(), +// range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 0)), +// ..Default::default() +// }], +// }) +// }); + +// editor.update(cx, |editor, cx| { +// editor.change_selections(None, cx, |selections| { +// selections.select_ranges([Point::new(0, 0)..Point::new(0, 0)]) +// }); +// editor.next_copilot_suggestion(&Default::default(), cx); +// }); + +// deterministic.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); +// assert!(copilot_requests.try_next().is_err()); + +// editor.update(cx, |editor, cx| { +// editor.change_selections(None, cx, |s| { +// s.select_ranges([Point::new(2, 0)..Point::new(2, 0)]) +// }); +// editor.next_copilot_suggestion(&Default::default(), cx); +// }); + +// deterministic.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); +// assert!(copilot_requests.try_next().is_ok()); +// } + +// #[gpui::test] +// async fn test_on_type_formatting_not_triggered(cx: &mut gpui::TestAppContext) { +// init_test(cx, |_| {}); + +// let mut language = Language::new( +// LanguageConfig { +// name: "Rust".into(), +// path_suffixes: vec!["rs".to_string()], +// brackets: BracketPairConfig { +// pairs: vec![BracketPair { +// start: "{".to_string(), +// end: "}".to_string(), +// close: true, +// newline: true, +// }], +// disabled_scopes_by_bracket_ix: Vec::new(), +// }, +// ..Default::default() +// }, +// Some(tree_sitter_rust::language()), +// ); +// let mut fake_servers = language +// .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { +// capabilities: lsp::ServerCapabilities { +// document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions { +// first_trigger_character: "{".to_string(), +// more_trigger_character: None, +// }), +// ..Default::default() +// }, +// ..Default::default() +// })) +// .await; + +// let fs = FakeFs::new(cx.background()); +// fs.insert_tree( +// "/a", +// json!({ +// "main.rs": "fn main() { let a = 5; }", +// "other.rs": "// Test file", +// }), +// ) +// .await; +// let project = Project::test(fs, ["/a".as_ref()], cx).await; +// project.update(cx, |project, _| project.languages().add(Arc::new(language))); +// let workspace = cx +// .add_window(|cx| Workspace::test_new(project.clone(), cx)) +// .root(cx); +// let worktree_id = workspace.update(cx, |workspace, cx| { +// workspace.project().read_with(cx, |project, cx| { +// project.worktrees(cx).next().unwrap().read(cx).id() +// }) +// }); + +// let buffer = project +// .update(cx, |project, cx| { +// project.open_local_buffer("/a/main.rs", cx) +// }) +// .await +// .unwrap(); +// cx.foreground().run_until_parked(); +// cx.foreground().start_waiting(); +// let fake_server = fake_servers.next().await.unwrap(); +// let editor_handle = workspace +// .update(cx, |workspace, cx| { +// workspace.open_path((worktree_id, "main.rs"), None, true, cx) +// }) +// .await +// .unwrap() +// .downcast::() +// .unwrap(); + +// fake_server.handle_request::(|params, _| async move { +// assert_eq!( +// params.text_document_position.text_document.uri, +// lsp::Url::from_file_path("/a/main.rs").unwrap(), +// ); +// assert_eq!( +// params.text_document_position.position, +// lsp::Position::new(0, 21), +// ); + +// Ok(Some(vec![lsp::TextEdit { +// new_text: "]".to_string(), +// range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)), +// }])) +// }); + +// editor_handle.update(cx, |editor, cx| { +// cx.focus(&editor_handle); +// editor.change_selections(None, cx, |s| { +// s.select_ranges([Point::new(0, 21)..Point::new(0, 20)]) +// }); +// editor.handle_input("{", cx); +// }); + +// cx.foreground().run_until_parked(); + +// buffer.read_with(cx, |buffer, _| { +// assert_eq!( +// buffer.text(), +// "fn main() { let a = {5}; }", +// "No extra braces from on type formatting should appear in the buffer" +// ) +// }); +// } + +// #[gpui::test] +// async fn test_language_server_restart_due_to_settings_change(cx: &mut gpui::TestAppContext) { +// init_test(cx, |_| {}); + +// let language_name: Arc = "Rust".into(); +// let mut language = Language::new( +// LanguageConfig { +// name: Arc::clone(&language_name), +// path_suffixes: vec!["rs".to_string()], +// ..Default::default() +// }, +// Some(tree_sitter_rust::language()), +// ); + +// let server_restarts = Arc::new(AtomicUsize::new(0)); +// let closure_restarts = Arc::clone(&server_restarts); +// let language_server_name = "test language server"; +// let mut fake_servers = language +// .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { +// name: language_server_name, +// initialization_options: Some(json!({ +// "testOptionValue": true +// })), +// initializer: Some(Box::new(move |fake_server| { +// let task_restarts = Arc::clone(&closure_restarts); +// fake_server.handle_request::(move |_, _| { +// task_restarts.fetch_add(1, atomic::Ordering::Release); +// futures::future::ready(Ok(())) +// }); +// })), +// ..Default::default() +// })) +// .await; + +// let fs = FakeFs::new(cx.background()); +// fs.insert_tree( +// "/a", +// json!({ +// "main.rs": "fn main() { let a = 5; }", +// "other.rs": "// Test file", +// }), +// ) +// .await; +// let project = Project::test(fs, ["/a".as_ref()], cx).await; +// project.update(cx, |project, _| project.languages().add(Arc::new(language))); +// let _window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); +// let _buffer = project +// .update(cx, |project, cx| { +// project.open_local_buffer("/a/main.rs", cx) +// }) +// .await +// .unwrap(); +// let _fake_server = fake_servers.next().await.unwrap(); +// update_test_language_settings(cx, |language_settings| { +// language_settings.languages.insert( +// Arc::clone(&language_name), +// LanguageSettingsContent { +// tab_size: NonZeroU32::new(8), +// ..Default::default() +// }, +// ); +// }); +// cx.foreground().run_until_parked(); +// assert_eq!( +// server_restarts.load(atomic::Ordering::Acquire), +// 0, +// "Should not restart LSP server on an unrelated change" +// ); + +// update_test_project_settings(cx, |project_settings| { +// project_settings.lsp.insert( +// "Some other server name".into(), +// LspSettings { +// initialization_options: Some(json!({ +// "some other init value": false +// })), +// }, +// ); +// }); +// cx.foreground().run_until_parked(); +// assert_eq!( +// server_restarts.load(atomic::Ordering::Acquire), +// 0, +// "Should not restart LSP server on an unrelated LSP settings change" +// ); + +// update_test_project_settings(cx, |project_settings| { +// project_settings.lsp.insert( +// language_server_name.into(), +// LspSettings { +// initialization_options: Some(json!({ +// "anotherInitValue": false +// })), +// }, +// ); +// }); +// cx.foreground().run_until_parked(); +// assert_eq!( +// server_restarts.load(atomic::Ordering::Acquire), +// 1, +// "Should restart LSP server on a related LSP settings change" +// ); + +// update_test_project_settings(cx, |project_settings| { +// project_settings.lsp.insert( +// language_server_name.into(), +// LspSettings { +// initialization_options: Some(json!({ +// "anotherInitValue": false +// })), +// }, +// ); +// }); +// cx.foreground().run_until_parked(); +// assert_eq!( +// server_restarts.load(atomic::Ordering::Acquire), +// 1, +// "Should not restart LSP server on a related LSP settings change that is the same" +// ); + +// update_test_project_settings(cx, |project_settings| { +// project_settings.lsp.insert( +// language_server_name.into(), +// LspSettings { +// initialization_options: None, +// }, +// ); +// }); +// cx.foreground().run_until_parked(); +// assert_eq!( +// server_restarts.load(atomic::Ordering::Acquire), +// 2, +// "Should restart LSP server on another related LSP settings change" +// ); +// } + +// #[gpui::test] +// async fn test_completions_with_additional_edits(cx: &mut gpui::TestAppContext) { +// init_test(cx, |_| {}); + +// let mut cx = EditorLspTestContext::new_rust( +// lsp::ServerCapabilities { +// completion_provider: Some(lsp::CompletionOptions { +// trigger_characters: Some(vec![".".to_string()]), +// resolve_provider: Some(true), +// ..Default::default() +// }), +// ..Default::default() +// }, +// cx, +// ) +// .await; + +// cx.set_state(indoc! {"fn main() { let a = 2ˇ; }"}); +// cx.simulate_keystroke("."); +// let completion_item = lsp::CompletionItem { +// label: "some".into(), +// kind: Some(lsp::CompletionItemKind::SNIPPET), +// detail: Some("Wrap the expression in an `Option::Some`".to_string()), +// documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent { +// kind: lsp::MarkupKind::Markdown, +// value: "```rust\nSome(2)\n```".to_string(), +// })), +// deprecated: Some(false), +// sort_text: Some("fffffff2".to_string()), +// filter_text: Some("some".to_string()), +// insert_text_format: Some(lsp::InsertTextFormat::SNIPPET), +// text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { +// range: lsp::Range { +// start: lsp::Position { +// line: 0, +// character: 22, +// }, +// end: lsp::Position { +// line: 0, +// character: 22, +// }, +// }, +// new_text: "Some(2)".to_string(), +// })), +// additional_text_edits: Some(vec![lsp::TextEdit { +// range: lsp::Range { +// start: lsp::Position { +// line: 0, +// character: 20, +// }, +// end: lsp::Position { +// line: 0, +// character: 22, +// }, +// }, +// new_text: "".to_string(), +// }]), +// ..Default::default() +// }; + +// let closure_completion_item = completion_item.clone(); +// let mut request = cx.handle_request::(move |_, _, _| { +// let task_completion_item = closure_completion_item.clone(); +// async move { +// Ok(Some(lsp::CompletionResponse::Array(vec![ +// task_completion_item, +// ]))) +// } +// }); + +// request.next().await; + +// cx.condition(|editor, _| editor.context_menu_visible()) +// .await; +// let apply_additional_edits = cx.update_editor(|editor, cx| { +// editor +// .confirm_completion(&ConfirmCompletion::default(), cx) +// .unwrap() +// }); +// cx.assert_editor_state(indoc! {"fn main() { let a = 2.Some(2)ˇ; }"}); + +// cx.handle_request::(move |_, _, _| { +// let task_completion_item = completion_item.clone(); +// async move { Ok(task_completion_item) } +// }) +// .next() +// .await +// .unwrap(); +// apply_additional_edits.await.unwrap(); +// cx.assert_editor_state(indoc! {"fn main() { let a = Some(2)ˇ; }"}); +// } + +// #[gpui::test] +// async fn test_completions_in_languages_with_extra_word_characters(cx: &mut gpui::TestAppContext) { +// init_test(cx, |_| {}); + +// let mut cx = EditorLspTestContext::new( +// Language::new( +// LanguageConfig { +// path_suffixes: vec!["jsx".into()], +// overrides: [( +// "element".into(), +// LanguageConfigOverride { +// word_characters: Override::Set(['-'].into_iter().collect()), +// ..Default::default() +// }, +// )] +// .into_iter() +// .collect(), +// ..Default::default() +// }, +// Some(tree_sitter_typescript::language_tsx()), +// ) +// .with_override_query("(jsx_self_closing_element) @element") +// .unwrap(), +// lsp::ServerCapabilities { +// completion_provider: Some(lsp::CompletionOptions { +// trigger_characters: Some(vec![":".to_string()]), +// ..Default::default() +// }), +// ..Default::default() +// }, +// cx, +// ) +// .await; + +// cx.lsp +// .handle_request::(move |_, _| async move { +// Ok(Some(lsp::CompletionResponse::Array(vec![ +// lsp::CompletionItem { +// label: "bg-blue".into(), +// ..Default::default() +// }, +// lsp::CompletionItem { +// label: "bg-red".into(), +// ..Default::default() +// }, +// lsp::CompletionItem { +// label: "bg-yellow".into(), +// ..Default::default() +// }, +// ]))) +// }); + +// cx.set_state(r#"

"#); + +// // Trigger completion when typing a dash, because the dash is an extra +// // word character in the 'element' scope, which contains the cursor. +// cx.simulate_keystroke("-"); +// cx.foreground().run_until_parked(); +// cx.update_editor(|editor, _| { +// if let Some(ContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() { +// assert_eq!( +// menu.matches.iter().map(|m| &m.string).collect::>(), +// &["bg-red", "bg-blue", "bg-yellow"] +// ); +// } else { +// panic!("expected completion menu to be open"); +// } +// }); + +// cx.simulate_keystroke("l"); +// cx.foreground().run_until_parked(); +// cx.update_editor(|editor, _| { +// if let Some(ContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() { +// assert_eq!( +// menu.matches.iter().map(|m| &m.string).collect::>(), +// &["bg-blue", "bg-yellow"] +// ); +// } else { +// panic!("expected completion menu to be open"); +// } +// }); + +// // When filtering completions, consider the character after the '-' to +// // be the start of a subword. +// cx.set_state(r#"

"#); +// cx.simulate_keystroke("l"); +// cx.foreground().run_until_parked(); +// cx.update_editor(|editor, _| { +// if let Some(ContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() { +// assert_eq!( +// menu.matches.iter().map(|m| &m.string).collect::>(), +// &["bg-yellow"] +// ); +// } else { +// panic!("expected completion menu to be open"); +// } +// }); +// } + +// #[gpui::test] +// async fn test_document_format_with_prettier(cx: &mut gpui::TestAppContext) { +// init_test(cx, |settings| { +// settings.defaults.formatter = Some(language_settings::Formatter::Prettier) +// }); + +// let mut language = Language::new( +// LanguageConfig { +// name: "Rust".into(), +// path_suffixes: vec!["rs".to_string()], +// prettier_parser_name: Some("test_parser".to_string()), +// ..Default::default() +// }, +// Some(tree_sitter_rust::language()), +// ); + +// let test_plugin = "test_plugin"; +// let _ = language +// .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { +// prettier_plugins: vec![test_plugin], +// ..Default::default() +// })) +// .await; + +// let fs = FakeFs::new(cx.background()); +// fs.insert_file("/file.rs", Default::default()).await; + +// let project = Project::test(fs, ["/file.rs".as_ref()], cx).await; +// let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX; +// project.update(cx, |project, _| { +// project.languages().add(Arc::new(language)); +// }); +// let buffer = project +// .update(cx, |project, cx| project.open_local_buffer("/file.rs", cx)) +// .await +// .unwrap(); + +// let buffer_text = "one\ntwo\nthree\n"; +// let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); +// let editor = cx.add_window(|cx| build_editor(buffer, cx)).root(cx); +// editor.update(cx, |editor, cx| editor.set_text(buffer_text, cx)); + +// let format = editor.update(cx, |editor, cx| { +// editor.perform_format(project.clone(), FormatTrigger::Manual, cx) +// }); +// format.await.unwrap(); +// assert_eq!( +// editor.read_with(cx, |editor, cx| editor.text(cx)), +// buffer_text.to_string() + prettier_format_suffix, +// "Test prettier formatting was not applied to the original buffer text", +// ); + +// update_test_language_settings(cx, |settings| { +// settings.defaults.formatter = Some(language_settings::Formatter::Auto) +// }); +// let format = editor.update(cx, |editor, cx| { +// editor.perform_format(project.clone(), FormatTrigger::Manual, cx) +// }); +// format.await.unwrap(); +// assert_eq!( +// editor.read_with(cx, |editor, cx| editor.text(cx)), +// buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix, +// "Autoformatting (via test prettier) was not applied to the original buffer text", +// ); +// } + +// fn empty_range(row: usize, column: usize) -> Range { +// let point = DisplayPoint::new(row as u32, column as u32); +// point..point +// } + +// fn assert_selection_ranges(marked_text: &str, view: &mut Editor, cx: &mut ViewContext) { +// let (text, ranges) = marked_text_ranges(marked_text, true); +// assert_eq!(view.text(cx), text); +// assert_eq!( +// view.selections.ranges(cx), +// ranges, +// "Assert selections are {}", +// marked_text +// ); +// } + +// /// Handle completion request passing a marked string specifying where the completion +// /// should be triggered from using '|' character, what range should be replaced, and what completions +// /// should be returned using '<' and '>' to delimit the range +// pub fn handle_completion_request<'a>( +// cx: &mut EditorLspTestContext<'a>, +// marked_string: &str, +// completions: Vec<&'static str>, +// ) -> impl Future { +// let complete_from_marker: TextRangeMarker = '|'.into(); +// let replace_range_marker: TextRangeMarker = ('<', '>').into(); +// let (_, mut marked_ranges) = marked_text_ranges_by( +// marked_string, +// vec![complete_from_marker.clone(), replace_range_marker.clone()], +// ); + +// let complete_from_position = +// cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start); +// let replace_range = +// cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone()); + +// let mut request = cx.handle_request::(move |url, params, _| { +// let completions = completions.clone(); +// async move { +// assert_eq!(params.text_document_position.text_document.uri, url.clone()); +// assert_eq!( +// params.text_document_position.position, +// complete_from_position +// ); +// Ok(Some(lsp::CompletionResponse::Array( +// completions +// .iter() +// .map(|completion_text| lsp::CompletionItem { +// label: completion_text.to_string(), +// text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { +// range: replace_range, +// new_text: completion_text.to_string(), +// })), +// ..Default::default() +// }) +// .collect(), +// ))) +// } +// }); + +// async move { +// request.next().await; +// } +// } + +// fn handle_resolve_completion_request<'a>( +// cx: &mut EditorLspTestContext<'a>, +// edits: Option>, +// ) -> impl Future { +// let edits = edits.map(|edits| { +// edits +// .iter() +// .map(|(marked_string, new_text)| { +// let (_, marked_ranges) = marked_text_ranges(marked_string, false); +// let replace_range = cx.to_lsp_range(marked_ranges[0].clone()); +// lsp::TextEdit::new(replace_range, new_text.to_string()) +// }) +// .collect::>() +// }); + +// let mut request = +// cx.handle_request::(move |_, _, _| { +// let edits = edits.clone(); +// async move { +// Ok(lsp::CompletionItem { +// additional_text_edits: edits, +// ..Default::default() +// }) +// } +// }); + +// async move { +// request.next().await; +// } +// } + +// fn handle_copilot_completion_request( +// lsp: &lsp::FakeLanguageServer, +// completions: Vec, +// completions_cycling: Vec, +// ) { +// lsp.handle_request::(move |_params, _cx| { +// let completions = completions.clone(); +// async move { +// Ok(copilot::request::GetCompletionsResult { +// completions: completions.clone(), +// }) +// } +// }); +// lsp.handle_request::(move |_params, _cx| { +// let completions_cycling = completions_cycling.clone(); +// async move { +// Ok(copilot::request::GetCompletionsResult { +// completions: completions_cycling.clone(), +// }) +// } +// }); +// } + +// pub(crate) fn update_test_language_settings( +// cx: &mut TestAppContext, +// f: impl Fn(&mut AllLanguageSettingsContent), +// ) { +// cx.update(|cx| { +// cx.update_global::(|store, cx| { +// store.update_user_settings::(cx, f); +// }); +// }); +// } + +// pub(crate) fn update_test_project_settings( +// cx: &mut TestAppContext, +// f: impl Fn(&mut ProjectSettings), +// ) { +// cx.update(|cx| { +// cx.update_global::(|store, cx| { +// store.update_user_settings::(cx, f); +// }); +// }); +// } + +// pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) { +// cx.foreground().forbid_parking(); + +// cx.update(|cx| { +// cx.set_global(SettingsStore::test(cx)); +// theme::init((), cx); +// client::init_settings(cx); +// language::init(cx); +// Project::init_settings(cx); +// workspace::init_settings(cx); +// crate::init(cx); +// }); + +// update_test_language_settings(cx, f); +// } diff --git a/crates/editor2/src/git.rs b/crates/editor2/src/git.rs index f8c6ef9a1f..e04372f0a7 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::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()); +// 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.foreground().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.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 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/highlight_matching_bracket.rs b/crates/editor2/src/highlight_matching_bracket.rs index a0baf6882f..e0fc2c0d00 100644 --- a/crates/editor2/src/highlight_matching_bracket.rs +++ b/crates/editor2/src/highlight_matching_bracket.rs @@ -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); +// } +// "#}); +// } +// } diff --git a/crates/editor2/src/hover_popover.rs b/crates/editor2/src/hover_popover.rs index 89946638b1..784b912c8c 100644 --- a/crates/editor2/src/hover_popover.rs +++ b/crates/editor2/src/hover_popover.rs @@ -6,10 +6,7 @@ use crate::{ }; use futures::FutureExt; use gpui::{ - actions, - elements::{Flex, MouseEventHandler, Padding, ParentElement, Text}, - platform::{CursorStyle, MouseButton}, - AnyElement, AppContext, Element, Model, Task, ViewContext, WeakViewHandle, + AnyElement, AppContext, CursorStyle, Element, Model, MouseButton, Task, ViewContext, WeakView, }; use language::{ markdown, Bias, DiagnosticEntry, DiagnosticSeverity, Language, LanguageRegistry, ParsedMarkdown, @@ -26,22 +23,23 @@ pub const MIN_POPOVER_CHARACTER_WIDTH: f32 = 20.; pub const MIN_POPOVER_LINE_HEIGHT: f32 = 4.; pub const HOVER_POPOVER_GAP: f32 = 10.; -actions!(editor, [Hover]); +// actions!(editor, [Hover]); pub fn init(cx: &mut AppContext) { - cx.add_action(hover); + // cx.add_action(hover); } -/// Bindable action which uses the most recent selection head to trigger a hover -pub fn hover(editor: &mut Editor, _: &Hover, cx: &mut ViewContext) { - let head = editor.selections.newest_display(cx).head(); - show_hover(editor, head, true, cx); -} +// todo!() +// /// Bindable action which uses the most recent selection head to trigger a hover +// pub fn hover(editor: &mut Editor, _: &Hover, cx: &mut ViewContext) { +// let head = editor.selections.newest_display(cx).head(); +// show_hover(editor, head, true, cx); +// } /// The internal hover action dispatches between `show_hover` or `hide_hover` /// depending on whether a point to hover over is provided. pub fn hover_at(editor: &mut Editor, point: Option, cx: &mut ViewContext) { - if settings::get::(cx).hover_popover_enabled { + if EditorSettings::get_global(cx).hover_popover_enabled { if let Some(point) = point { show_hover(editor, point, false, cx); } else { @@ -79,7 +77,7 @@ pub fn find_hovered_hint_part( } pub fn hover_at_inlay(editor: &mut Editor, inlay_hover: InlayHover, cx: &mut ViewContext) { - if settings::get::(cx).hover_popover_enabled { + if EditorSettings::get_global(cx).hover_popover_enabled { if editor.pending_rename.is_some() { return; } @@ -423,7 +421,7 @@ impl HoverState { snapshot: &EditorSnapshot, style: &EditorStyle, visible_rows: Range, - workspace: Option>, + workspace: Option>, cx: &mut ViewContext, ) -> Option<(DisplayPoint, Vec>)> { // If there is a diagnostic, position the popovers based on that. @@ -462,7 +460,7 @@ impl HoverState { #[derive(Debug, Clone)] pub struct InfoPopover { - pub project: ModelHandle, + pub project: Model, symbol_range: RangeInEditor, pub blocks: Vec, parsed_content: ParsedMarkdown, @@ -472,7 +470,7 @@ impl InfoPopover { pub fn render( &mut self, style: &EditorStyle, - workspace: Option>, + workspace: Option>, cx: &mut ViewContext, ) -> AnyElement { MouseEventHandler::new::(0, cx, |_, cx| { @@ -506,55 +504,56 @@ pub struct DiagnosticPopover { impl DiagnosticPopover { pub fn render(&self, style: &EditorStyle, cx: &mut ViewContext) -> AnyElement { - enum PrimaryDiagnostic {} + todo!() + // enum PrimaryDiagnostic {} - let mut text_style = style.hover_popover.prose.clone(); - text_style.font_size = style.text.font_size; - let diagnostic_source_style = style.hover_popover.diagnostic_source_highlight.clone(); + // let mut text_style = style.hover_popover.prose.clone(); + // text_style.font_size = style.text.font_size; + // let diagnostic_source_style = style.hover_popover.diagnostic_source_highlight.clone(); - let text = match &self.local_diagnostic.diagnostic.source { - Some(source) => Text::new( - format!("{source}: {}", self.local_diagnostic.diagnostic.message), - text_style, - ) - .with_highlights(vec![(0..source.len(), diagnostic_source_style)]), + // let text = match &self.local_diagnostic.diagnostic.source { + // Some(source) => Text::new( + // format!("{source}: {}", self.local_diagnostic.diagnostic.message), + // text_style, + // ) + // .with_highlights(vec![(0..source.len(), diagnostic_source_style)]), - None => Text::new(self.local_diagnostic.diagnostic.message.clone(), text_style), - }; + // None => Text::new(self.local_diagnostic.diagnostic.message.clone(), text_style), + // }; - let container_style = match self.local_diagnostic.diagnostic.severity { - DiagnosticSeverity::HINT => style.hover_popover.info_container, - DiagnosticSeverity::INFORMATION => style.hover_popover.info_container, - DiagnosticSeverity::WARNING => style.hover_popover.warning_container, - DiagnosticSeverity::ERROR => style.hover_popover.error_container, - _ => style.hover_popover.container, - }; + // let container_style = match self.local_diagnostic.diagnostic.severity { + // DiagnosticSeverity::HINT => style.hover_popover.info_container, + // DiagnosticSeverity::INFORMATION => style.hover_popover.info_container, + // DiagnosticSeverity::WARNING => style.hover_popover.warning_container, + // DiagnosticSeverity::ERROR => style.hover_popover.error_container, + // _ => style.hover_popover.container, + // }; - let tooltip_style = theme::current(cx).tooltip.clone(); + // let tooltip_style = theme::current(cx).tooltip.clone(); - MouseEventHandler::new::(0, cx, |_, _| { - text.with_soft_wrap(true) - .contained() - .with_style(container_style) - }) - .with_padding(Padding { - top: HOVER_POPOVER_GAP, - bottom: HOVER_POPOVER_GAP, - ..Default::default() - }) - .on_move(|_, _, _| {}) // Consume move events so they don't reach regions underneath. - .on_click(MouseButton::Left, |_, this, cx| { - this.go_to_diagnostic(&Default::default(), cx) - }) - .with_cursor_style(CursorStyle::PointingHand) - .with_tooltip::( - 0, - "Go To Diagnostic".to_string(), - Some(Box::new(crate::GoToDiagnostic)), - tooltip_style, - cx, - ) - .into_any() + // MouseEventHandler::new::(0, cx, |_, _| { + // text.with_soft_wrap(true) + // .contained() + // .with_style(container_style) + // }) + // .with_padding(Padding { + // top: HOVER_POPOVER_GAP, + // bottom: HOVER_POPOVER_GAP, + // ..Default::default() + // }) + // .on_move(|_, _, _| {}) // Consume move events so they don't reach regions underneath. + // .on_click(MouseButton::Left, |_, this, cx| { + // this.go_to_diagnostic(&Default::default(), cx) + // }) + // .with_cursor_style(CursorStyle::PointingHand) + // .with_tooltip::( + // 0, + // "Go To Diagnostic".to_string(), + // Some(Box::new(crate::GoToDiagnostic)), + // tooltip_style, + // cx, + // ) + // .into_any() } pub fn activation_info(&self) -> (usize, Anchor) { @@ -567,763 +566,763 @@ impl DiagnosticPopover { } } -#[cfg(test)] -mod tests { - use super::*; - use crate::{ - editor_tests::init_test, - element::PointForPosition, - inlay_hint_cache::tests::{cached_hint_labels, visible_hint_labels}, - link_go_to_definition::update_inlay_link_and_hover_points, - test::editor_lsp_test_context::EditorLspTestContext, - InlayId, - }; - use collections::BTreeSet; - use gpui::fonts::{HighlightStyle, Underline, Weight}; - use indoc::indoc; - use language::{language_settings::InlayHintSettings, Diagnostic, DiagnosticSet}; - use lsp::LanguageServerId; - use project::{HoverBlock, HoverBlockKind}; - use smol::stream::StreamExt; - use unindent::Unindent; - use util::test::marked_text_ranges; - - #[gpui::test] - async fn test_mouse_hover_info_popover(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; - - // Basic hover delays and then pops without moving the mouse - cx.set_state(indoc! {" - fn ˇtest() { println!(); } - "}); - let hover_point = cx.display_point(indoc! {" - fn test() { printˇln!(); } - "}); - - cx.update_editor(|editor, cx| hover_at(editor, Some(hover_point), cx)); - assert!(!cx.editor(|editor, _| editor.hover_state.visible())); - - // After delay, hover should be visible. - let symbol_range = cx.lsp_range(indoc! {" - fn test() { «println!»(); } - "}); - let mut requests = - cx.handle_request::(move |_, _, _| async move { - Ok(Some(lsp::Hover { - contents: lsp::HoverContents::Markup(lsp::MarkupContent { - kind: lsp::MarkupKind::Markdown, - value: "some basic docs".to_string(), - }), - range: Some(symbol_range), - })) - }); - cx.foreground() - .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100)); - requests.next().await; - - cx.editor(|editor, _| { - assert!(editor.hover_state.visible()); - assert_eq!( - editor.hover_state.info_popover.clone().unwrap().blocks, - vec![HoverBlock { - text: "some basic docs".to_string(), - kind: HoverBlockKind::Markdown, - },] - ) - }); - - // Mouse moved with no hover response dismisses - let hover_point = cx.display_point(indoc! {" - fn teˇst() { println!(); } - "}); - let mut request = cx - .lsp - .handle_request::(|_, _| async move { Ok(None) }); - cx.update_editor(|editor, cx| hover_at(editor, Some(hover_point), cx)); - cx.foreground() - .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100)); - request.next().await; - cx.editor(|editor, _| { - assert!(!editor.hover_state.visible()); - }); - } - - #[gpui::test] - async fn test_keyboard_hover_info_popover(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; - - // Hover with keyboard has no delay - cx.set_state(indoc! {" - fˇn test() { println!(); } - "}); - cx.update_editor(|editor, cx| hover(editor, &Hover, cx)); - let symbol_range = cx.lsp_range(indoc! {" - «fn» test() { println!(); } - "}); - cx.handle_request::(move |_, _, _| async move { - Ok(Some(lsp::Hover { - contents: lsp::HoverContents::Markup(lsp::MarkupContent { - kind: lsp::MarkupKind::Markdown, - value: "some other basic docs".to_string(), - }), - range: Some(symbol_range), - })) - }) - .next() - .await; - - cx.condition(|editor, _| editor.hover_state.visible()).await; - cx.editor(|editor, _| { - assert_eq!( - editor.hover_state.info_popover.clone().unwrap().blocks, - vec![HoverBlock { - text: "some other basic docs".to_string(), - kind: HoverBlockKind::Markdown, - }] - ) - }); - } - - #[gpui::test] - async fn test_empty_hovers_filtered(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; - - // Hover with keyboard has no delay - cx.set_state(indoc! {" - fˇn test() { println!(); } - "}); - cx.update_editor(|editor, cx| hover(editor, &Hover, cx)); - let symbol_range = cx.lsp_range(indoc! {" - «fn» test() { println!(); } - "}); - cx.handle_request::(move |_, _, _| async move { - Ok(Some(lsp::Hover { - contents: lsp::HoverContents::Array(vec![ - lsp::MarkedString::String("regular text for hover to show".to_string()), - lsp::MarkedString::String("".to_string()), - lsp::MarkedString::LanguageString(lsp::LanguageString { - language: "Rust".to_string(), - value: "".to_string(), - }), - ]), - range: Some(symbol_range), - })) - }) - .next() - .await; - - cx.condition(|editor, _| editor.hover_state.visible()).await; - cx.editor(|editor, _| { - assert_eq!( - editor.hover_state.info_popover.clone().unwrap().blocks, - vec![HoverBlock { - text: "regular text for hover to show".to_string(), - kind: HoverBlockKind::Markdown, - }], - "No empty string hovers should be shown" - ); - }); - } - - #[gpui::test] - async fn test_line_ends_trimmed(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; - - // Hover with keyboard has no delay - cx.set_state(indoc! {" - fˇn test() { println!(); } - "}); - cx.update_editor(|editor, cx| hover(editor, &Hover, cx)); - let symbol_range = cx.lsp_range(indoc! {" - «fn» test() { println!(); } - "}); - - let code_str = "\nlet hovered_point: Vector2F // size = 8, align = 0x4\n"; - let markdown_string = format!("\n```rust\n{code_str}```"); - - let closure_markdown_string = markdown_string.clone(); - cx.handle_request::(move |_, _, _| { - let future_markdown_string = closure_markdown_string.clone(); - async move { - Ok(Some(lsp::Hover { - contents: lsp::HoverContents::Markup(lsp::MarkupContent { - kind: lsp::MarkupKind::Markdown, - value: future_markdown_string, - }), - range: Some(symbol_range), - })) - } - }) - .next() - .await; - - cx.condition(|editor, _| editor.hover_state.visible()).await; - cx.editor(|editor, _| { - let blocks = editor.hover_state.info_popover.clone().unwrap().blocks; - assert_eq!( - blocks, - vec![HoverBlock { - text: markdown_string, - kind: HoverBlockKind::Markdown, - }], - ); - - let rendered = smol::block_on(parse_blocks(&blocks, &Default::default(), None)); - assert_eq!( - rendered.text, - code_str.trim(), - "Should not have extra line breaks at end of rendered hover" - ); - }); - } - - #[gpui::test] - async fn test_hover_diagnostic_and_info_popovers(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; - - // Hover with just diagnostic, pops DiagnosticPopover immediately and then - // info popover once request completes - cx.set_state(indoc! {" - fn teˇst() { println!(); } - "}); - - // Send diagnostic to client - let range = cx.text_anchor_range(indoc! {" - fn «test»() { println!(); } - "}); - cx.update_buffer(|buffer, cx| { - let snapshot = buffer.text_snapshot(); - let set = DiagnosticSet::from_sorted_entries( - vec![DiagnosticEntry { - range, - diagnostic: Diagnostic { - message: "A test diagnostic message.".to_string(), - ..Default::default() - }, - }], - &snapshot, - ); - buffer.update_diagnostics(LanguageServerId(0), set, cx); - }); - - // Hover pops diagnostic immediately - cx.update_editor(|editor, cx| hover(editor, &Hover, cx)); - cx.foreground().run_until_parked(); - - cx.editor(|Editor { hover_state, .. }, _| { - assert!(hover_state.diagnostic_popover.is_some() && hover_state.info_popover.is_none()) - }); - - // Info Popover shows after request responded to - let range = cx.lsp_range(indoc! {" - fn «test»() { println!(); } - "}); - cx.handle_request::(move |_, _, _| async move { - Ok(Some(lsp::Hover { - contents: lsp::HoverContents::Markup(lsp::MarkupContent { - kind: lsp::MarkupKind::Markdown, - value: "some new docs".to_string(), - }), - range: Some(range), - })) - }); - cx.foreground() - .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100)); - - cx.foreground().run_until_parked(); - cx.editor(|Editor { hover_state, .. }, _| { - hover_state.diagnostic_popover.is_some() && hover_state.info_task.is_some() - }); - } - - #[gpui::test] - fn test_render_blocks(cx: &mut gpui::TestAppContext) { - init_test(cx, |_| {}); - - cx.add_window(|cx| { - let editor = Editor::single_line(None, cx); - let style = editor.style(cx); - - struct Row { - blocks: Vec, - expected_marked_text: String, - expected_styles: Vec, - } - - let rows = &[ - // Strong emphasis - Row { - blocks: vec![HoverBlock { - text: "one **two** three".to_string(), - kind: HoverBlockKind::Markdown, - }], - expected_marked_text: "one «two» three".to_string(), - expected_styles: vec![HighlightStyle { - weight: Some(Weight::BOLD), - ..Default::default() - }], - }, - // Links - Row { - blocks: vec![HoverBlock { - text: "one [two](https://the-url) three".to_string(), - kind: HoverBlockKind::Markdown, - }], - expected_marked_text: "one «two» three".to_string(), - expected_styles: vec![HighlightStyle { - underline: Some(Underline { - thickness: 1.0.into(), - ..Default::default() - }), - ..Default::default() - }], - }, - // Lists - Row { - blocks: vec![HoverBlock { - text: " - lists: - * one - - a - - b - * two - - [c](https://the-url) - - d" - .unindent(), - kind: HoverBlockKind::Markdown, - }], - expected_marked_text: " - lists: - - one - - a - - b - - two - - «c» - - d" - .unindent(), - expected_styles: vec![HighlightStyle { - underline: Some(Underline { - thickness: 1.0.into(), - ..Default::default() - }), - ..Default::default() - }], - }, - // Multi-paragraph list items - Row { - blocks: vec![HoverBlock { - text: " - * one two - three - - * four five - * six seven - eight - - nine - * ten - * six" - .unindent(), - kind: HoverBlockKind::Markdown, - }], - expected_marked_text: " - - one two three - - four five - - six seven eight - - nine - - ten - - six" - .unindent(), - expected_styles: vec![HighlightStyle { - underline: Some(Underline { - thickness: 1.0.into(), - ..Default::default() - }), - ..Default::default() - }], - }, - ]; - - for Row { - blocks, - expected_marked_text, - expected_styles, - } in &rows[0..] - { - let rendered = smol::block_on(parse_blocks(&blocks, &Default::default(), None)); - - let (expected_text, ranges) = marked_text_ranges(expected_marked_text, false); - let expected_highlights = ranges - .into_iter() - .zip(expected_styles.iter().cloned()) - .collect::>(); - assert_eq!( - rendered.text, expected_text, - "wrong text for input {blocks:?}" - ); - - let rendered_highlights: Vec<_> = rendered - .highlights - .iter() - .filter_map(|(range, highlight)| { - let highlight = highlight.to_highlight_style(&style.syntax)?; - Some((range.clone(), highlight)) - }) - .collect(); - - assert_eq!( - rendered_highlights, expected_highlights, - "wrong highlights for input {blocks:?}" - ); - } - - editor - }); - } - - #[gpui::test] - async fn test_hover_inlay_label_parts(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::Right( - lsp::InlayHintServerCapabilities::Options(lsp::InlayHintOptions { - resolve_provider: Some(true), - ..Default::default() - }), - )), - ..Default::default() - }, - cx, - ) - .await; - - cx.set_state(indoc! {" - struct TestStruct; - - // ================== - - struct TestNewType(T); - - fn main() { - let variableˇ = TestNewType(TestStruct); - } - "}); - - let hint_start_offset = cx.ranges(indoc! {" - struct TestStruct; - - // ================== - - struct TestNewType(T); - - fn main() { - let variableˇ = TestNewType(TestStruct); - } - "})[0] - .start; - let hint_position = cx.to_lsp(hint_start_offset); - let new_type_target_range = cx.lsp_range(indoc! {" - struct TestStruct; - - // ================== - - struct «TestNewType»(T); - - fn main() { - let variable = TestNewType(TestStruct); - } - "}); - let struct_target_range = cx.lsp_range(indoc! {" - struct «TestStruct»; - - // ================== - - struct TestNewType(T); - - fn main() { - let variable = TestNewType(TestStruct); - } - "}); - - let uri = cx.buffer_lsp_url.clone(); - let new_type_label = "TestNewType"; - let struct_label = "TestStruct"; - let entire_hint_label = ": TestNewType"; - let closure_uri = uri.clone(); - cx.lsp - .handle_request::(move |params, _| { - let task_uri = closure_uri.clone(); - async move { - assert_eq!(params.text_document.uri, task_uri); - Ok(Some(vec![lsp::InlayHint { - position: hint_position, - label: lsp::InlayHintLabel::LabelParts(vec![lsp::InlayHintLabelPart { - value: entire_hint_label.to_string(), - ..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![entire_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; - - // ================== - - struct TestNewType(T); - - fn main() { - let variable« »= TestNewType(TestStruct); - } - "}) - .get(0) - .cloned() - .unwrap(); - let new_type_hint_part_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() - + (entire_hint_label.find(new_type_label).unwrap() + new_type_label.len() / 2) - as u32, - ); - PointForPosition { - previous_valid, - next_valid, - exact_unclipped, - column_overshoot_after_line_end: 0, - } - }); - cx.update_editor(|editor, cx| { - update_inlay_link_and_hover_points( - &editor.snapshot(cx), - new_type_hint_part_hover_position, - editor, - true, - false, - cx, - ); - }); - - let resolve_closure_uri = uri.clone(); - cx.lsp - .handle_request::( - move |mut hint_to_resolve, _| { - let mut resolved_hint_positions = BTreeSet::new(); - let task_uri = resolve_closure_uri.clone(); - async move { - let inserted = resolved_hint_positions.insert(hint_to_resolve.position); - assert!(inserted, "Hint {hint_to_resolve:?} was resolved twice"); - - // `: TestNewType` - hint_to_resolve.label = lsp::InlayHintLabel::LabelParts(vec![ - lsp::InlayHintLabelPart { - value: ": ".to_string(), - ..Default::default() - }, - lsp::InlayHintLabelPart { - value: new_type_label.to_string(), - location: Some(lsp::Location { - uri: task_uri.clone(), - range: new_type_target_range, - }), - tooltip: Some(lsp::InlayHintLabelPartTooltip::String(format!( - "A tooltip for `{new_type_label}`" - ))), - ..Default::default() - }, - lsp::InlayHintLabelPart { - value: "<".to_string(), - ..Default::default() - }, - lsp::InlayHintLabelPart { - value: struct_label.to_string(), - location: Some(lsp::Location { - uri: task_uri, - range: struct_target_range, - }), - tooltip: Some(lsp::InlayHintLabelPartTooltip::MarkupContent( - lsp::MarkupContent { - kind: lsp::MarkupKind::Markdown, - value: format!("A tooltip for `{struct_label}`"), - }, - )), - ..Default::default() - }, - lsp::InlayHintLabelPart { - value: ">".to_string(), - ..Default::default() - }, - ]); - - Ok(hint_to_resolve) - } - }, - ) - .next() - .await; - cx.foreground().run_until_parked(); - - cx.update_editor(|editor, cx| { - update_inlay_link_and_hover_points( - &editor.snapshot(cx), - new_type_hint_part_hover_position, - editor, - true, - false, - cx, - ); - }); - cx.foreground() - .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100)); - cx.foreground().run_until_parked(); - cx.update_editor(|editor, cx| { - let hover_state = &editor.hover_state; - assert!(hover_state.diagnostic_popover.is_none() && hover_state.info_popover.is_some()); - let popover = hover_state.info_popover.as_ref().unwrap(); - let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx)); - assert_eq!( - popover.symbol_range, - RangeInEditor::Inlay(InlayHighlight { - inlay: InlayId::Hint(0), - inlay_position: buffer_snapshot.anchor_at(inlay_range.start, Bias::Right), - range: ": ".len()..": ".len() + new_type_label.len(), - }), - "Popover range should match the new type label part" - ); - assert_eq!( - popover.parsed_content.text, - format!("A tooltip for `{new_type_label}`"), - "Rendered text should not anyhow alter backticks" - ); - }); - - let struct_hint_part_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() - + (entire_hint_label.find(struct_label).unwrap() + struct_label.len() / 2) - as u32, - ); - PointForPosition { - previous_valid, - next_valid, - exact_unclipped, - column_overshoot_after_line_end: 0, - } - }); - cx.update_editor(|editor, cx| { - update_inlay_link_and_hover_points( - &editor.snapshot(cx), - struct_hint_part_hover_position, - editor, - true, - false, - cx, - ); - }); - cx.foreground() - .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100)); - cx.foreground().run_until_parked(); - cx.update_editor(|editor, cx| { - let hover_state = &editor.hover_state; - assert!(hover_state.diagnostic_popover.is_none() && hover_state.info_popover.is_some()); - let popover = hover_state.info_popover.as_ref().unwrap(); - let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx)); - assert_eq!( - popover.symbol_range, - RangeInEditor::Inlay(InlayHighlight { - inlay: InlayId::Hint(0), - inlay_position: buffer_snapshot.anchor_at(inlay_range.start, Bias::Right), - range: ": ".len() + new_type_label.len() + "<".len() - ..": ".len() + new_type_label.len() + "<".len() + struct_label.len(), - }), - "Popover range should match the struct label part" - ); - assert_eq!( - popover.parsed_content.text, - format!("A tooltip for {struct_label}"), - "Rendered markdown element should remove backticks from text" - ); - }); - } -} +// #[cfg(test)] +// mod tests { +// use super::*; +// use crate::{ +// editor_tests::init_test, +// element::PointForPosition, +// inlay_hint_cache::tests::{cached_hint_labels, visible_hint_labels}, +// link_go_to_definition::update_inlay_link_and_hover_points, +// test::editor_lsp_test_context::EditorLspTestContext, +// InlayId, +// }; +// use collections::BTreeSet; +// use gpui::fonts::{HighlightStyle, Underline, Weight}; +// use indoc::indoc; +// use language::{language_settings::InlayHintSettings, Diagnostic, DiagnosticSet}; +// use lsp::LanguageServerId; +// use project::{HoverBlock, HoverBlockKind}; +// use smol::stream::StreamExt; +// use unindent::Unindent; +// use util::test::marked_text_ranges; + +// #[gpui::test] +// async fn test_mouse_hover_info_popover(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; + +// // Basic hover delays and then pops without moving the mouse +// cx.set_state(indoc! {" +// fn ˇtest() { println!(); } +// "}); +// let hover_point = cx.display_point(indoc! {" +// fn test() { printˇln!(); } +// "}); + +// cx.update_editor(|editor, cx| hover_at(editor, Some(hover_point), cx)); +// assert!(!cx.editor(|editor, _| editor.hover_state.visible())); + +// // After delay, hover should be visible. +// let symbol_range = cx.lsp_range(indoc! {" +// fn test() { «println!»(); } +// "}); +// let mut requests = +// cx.handle_request::(move |_, _, _| async move { +// Ok(Some(lsp::Hover { +// contents: lsp::HoverContents::Markup(lsp::MarkupContent { +// kind: lsp::MarkupKind::Markdown, +// value: "some basic docs".to_string(), +// }), +// range: Some(symbol_range), +// })) +// }); +// cx.foreground() +// .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100)); +// requests.next().await; + +// cx.editor(|editor, _| { +// assert!(editor.hover_state.visible()); +// assert_eq!( +// editor.hover_state.info_popover.clone().unwrap().blocks, +// vec![HoverBlock { +// text: "some basic docs".to_string(), +// kind: HoverBlockKind::Markdown, +// },] +// ) +// }); + +// // Mouse moved with no hover response dismisses +// let hover_point = cx.display_point(indoc! {" +// fn teˇst() { println!(); } +// "}); +// let mut request = cx +// .lsp +// .handle_request::(|_, _| async move { Ok(None) }); +// cx.update_editor(|editor, cx| hover_at(editor, Some(hover_point), cx)); +// cx.foreground() +// .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100)); +// request.next().await; +// cx.editor(|editor, _| { +// assert!(!editor.hover_state.visible()); +// }); +// } + +// #[gpui::test] +// async fn test_keyboard_hover_info_popover(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; + +// // Hover with keyboard has no delay +// cx.set_state(indoc! {" +// fˇn test() { println!(); } +// "}); +// cx.update_editor(|editor, cx| hover(editor, &Hover, cx)); +// let symbol_range = cx.lsp_range(indoc! {" +// «fn» test() { println!(); } +// "}); +// cx.handle_request::(move |_, _, _| async move { +// Ok(Some(lsp::Hover { +// contents: lsp::HoverContents::Markup(lsp::MarkupContent { +// kind: lsp::MarkupKind::Markdown, +// value: "some other basic docs".to_string(), +// }), +// range: Some(symbol_range), +// })) +// }) +// .next() +// .await; + +// cx.condition(|editor, _| editor.hover_state.visible()).await; +// cx.editor(|editor, _| { +// assert_eq!( +// editor.hover_state.info_popover.clone().unwrap().blocks, +// vec![HoverBlock { +// text: "some other basic docs".to_string(), +// kind: HoverBlockKind::Markdown, +// }] +// ) +// }); +// } + +// #[gpui::test] +// async fn test_empty_hovers_filtered(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; + +// // Hover with keyboard has no delay +// cx.set_state(indoc! {" +// fˇn test() { println!(); } +// "}); +// cx.update_editor(|editor, cx| hover(editor, &Hover, cx)); +// let symbol_range = cx.lsp_range(indoc! {" +// «fn» test() { println!(); } +// "}); +// cx.handle_request::(move |_, _, _| async move { +// Ok(Some(lsp::Hover { +// contents: lsp::HoverContents::Array(vec![ +// lsp::MarkedString::String("regular text for hover to show".to_string()), +// lsp::MarkedString::String("".to_string()), +// lsp::MarkedString::LanguageString(lsp::LanguageString { +// language: "Rust".to_string(), +// value: "".to_string(), +// }), +// ]), +// range: Some(symbol_range), +// })) +// }) +// .next() +// .await; + +// cx.condition(|editor, _| editor.hover_state.visible()).await; +// cx.editor(|editor, _| { +// assert_eq!( +// editor.hover_state.info_popover.clone().unwrap().blocks, +// vec![HoverBlock { +// text: "regular text for hover to show".to_string(), +// kind: HoverBlockKind::Markdown, +// }], +// "No empty string hovers should be shown" +// ); +// }); +// } + +// #[gpui::test] +// async fn test_line_ends_trimmed(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; + +// // Hover with keyboard has no delay +// cx.set_state(indoc! {" +// fˇn test() { println!(); } +// "}); +// cx.update_editor(|editor, cx| hover(editor, &Hover, cx)); +// let symbol_range = cx.lsp_range(indoc! {" +// «fn» test() { println!(); } +// "}); + +// let code_str = "\nlet hovered_point: Vector2F // size = 8, align = 0x4\n"; +// let markdown_string = format!("\n```rust\n{code_str}```"); + +// let closure_markdown_string = markdown_string.clone(); +// cx.handle_request::(move |_, _, _| { +// let future_markdown_string = closure_markdown_string.clone(); +// async move { +// Ok(Some(lsp::Hover { +// contents: lsp::HoverContents::Markup(lsp::MarkupContent { +// kind: lsp::MarkupKind::Markdown, +// value: future_markdown_string, +// }), +// range: Some(symbol_range), +// })) +// } +// }) +// .next() +// .await; + +// cx.condition(|editor, _| editor.hover_state.visible()).await; +// cx.editor(|editor, _| { +// let blocks = editor.hover_state.info_popover.clone().unwrap().blocks; +// assert_eq!( +// blocks, +// vec![HoverBlock { +// text: markdown_string, +// kind: HoverBlockKind::Markdown, +// }], +// ); + +// let rendered = smol::block_on(parse_blocks(&blocks, &Default::default(), None)); +// assert_eq!( +// rendered.text, +// code_str.trim(), +// "Should not have extra line breaks at end of rendered hover" +// ); +// }); +// } + +// #[gpui::test] +// async fn test_hover_diagnostic_and_info_popovers(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; + +// // Hover with just diagnostic, pops DiagnosticPopover immediately and then +// // info popover once request completes +// cx.set_state(indoc! {" +// fn teˇst() { println!(); } +// "}); + +// // Send diagnostic to client +// let range = cx.text_anchor_range(indoc! {" +// fn «test»() { println!(); } +// "}); +// cx.update_buffer(|buffer, cx| { +// let snapshot = buffer.text_snapshot(); +// let set = DiagnosticSet::from_sorted_entries( +// vec![DiagnosticEntry { +// range, +// diagnostic: Diagnostic { +// message: "A test diagnostic message.".to_string(), +// ..Default::default() +// }, +// }], +// &snapshot, +// ); +// buffer.update_diagnostics(LanguageServerId(0), set, cx); +// }); + +// // Hover pops diagnostic immediately +// cx.update_editor(|editor, cx| hover(editor, &Hover, cx)); +// cx.foreground().run_until_parked(); + +// cx.editor(|Editor { hover_state, .. }, _| { +// assert!(hover_state.diagnostic_popover.is_some() && hover_state.info_popover.is_none()) +// }); + +// // Info Popover shows after request responded to +// let range = cx.lsp_range(indoc! {" +// fn «test»() { println!(); } +// "}); +// cx.handle_request::(move |_, _, _| async move { +// Ok(Some(lsp::Hover { +// contents: lsp::HoverContents::Markup(lsp::MarkupContent { +// kind: lsp::MarkupKind::Markdown, +// value: "some new docs".to_string(), +// }), +// range: Some(range), +// })) +// }); +// cx.foreground() +// .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100)); + +// cx.foreground().run_until_parked(); +// cx.editor(|Editor { hover_state, .. }, _| { +// hover_state.diagnostic_popover.is_some() && hover_state.info_task.is_some() +// }); +// } + +// #[gpui::test] +// fn test_render_blocks(cx: &mut gpui::TestAppContext) { +// init_test(cx, |_| {}); + +// cx.add_window(|cx| { +// let editor = Editor::single_line(None, cx); +// let style = editor.style(cx); + +// struct Row { +// blocks: Vec, +// expected_marked_text: String, +// expected_styles: Vec, +// } + +// let rows = &[ +// // Strong emphasis +// Row { +// blocks: vec![HoverBlock { +// text: "one **two** three".to_string(), +// kind: HoverBlockKind::Markdown, +// }], +// expected_marked_text: "one «two» three".to_string(), +// expected_styles: vec![HighlightStyle { +// weight: Some(Weight::BOLD), +// ..Default::default() +// }], +// }, +// // Links +// Row { +// blocks: vec![HoverBlock { +// text: "one [two](https://the-url) three".to_string(), +// kind: HoverBlockKind::Markdown, +// }], +// expected_marked_text: "one «two» three".to_string(), +// expected_styles: vec![HighlightStyle { +// underline: Some(Underline { +// thickness: 1.0.into(), +// ..Default::default() +// }), +// ..Default::default() +// }], +// }, +// // Lists +// Row { +// blocks: vec![HoverBlock { +// text: " +// lists: +// * one +// - a +// - b +// * two +// - [c](https://the-url) +// - d" +// .unindent(), +// kind: HoverBlockKind::Markdown, +// }], +// expected_marked_text: " +// lists: +// - one +// - a +// - b +// - two +// - «c» +// - d" +// .unindent(), +// expected_styles: vec![HighlightStyle { +// underline: Some(Underline { +// thickness: 1.0.into(), +// ..Default::default() +// }), +// ..Default::default() +// }], +// }, +// // Multi-paragraph list items +// Row { +// blocks: vec![HoverBlock { +// text: " +// * one two +// three + +// * four five +// * six seven +// eight + +// nine +// * ten +// * six" +// .unindent(), +// kind: HoverBlockKind::Markdown, +// }], +// expected_marked_text: " +// - one two three +// - four five +// - six seven eight + +// nine +// - ten +// - six" +// .unindent(), +// expected_styles: vec![HighlightStyle { +// underline: Some(Underline { +// thickness: 1.0.into(), +// ..Default::default() +// }), +// ..Default::default() +// }], +// }, +// ]; + +// for Row { +// blocks, +// expected_marked_text, +// expected_styles, +// } in &rows[0..] +// { +// let rendered = smol::block_on(parse_blocks(&blocks, &Default::default(), None)); + +// let (expected_text, ranges) = marked_text_ranges(expected_marked_text, false); +// let expected_highlights = ranges +// .into_iter() +// .zip(expected_styles.iter().cloned()) +// .collect::>(); +// assert_eq!( +// rendered.text, expected_text, +// "wrong text for input {blocks:?}" +// ); + +// let rendered_highlights: Vec<_> = rendered +// .highlights +// .iter() +// .filter_map(|(range, highlight)| { +// let highlight = highlight.to_highlight_style(&style.syntax)?; +// Some((range.clone(), highlight)) +// }) +// .collect(); + +// assert_eq!( +// rendered_highlights, expected_highlights, +// "wrong highlights for input {blocks:?}" +// ); +// } + +// editor +// }); +// } + +// #[gpui::test] +// async fn test_hover_inlay_label_parts(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::Right( +// lsp::InlayHintServerCapabilities::Options(lsp::InlayHintOptions { +// resolve_provider: Some(true), +// ..Default::default() +// }), +// )), +// ..Default::default() +// }, +// cx, +// ) +// .await; + +// cx.set_state(indoc! {" +// struct TestStruct; + +// // ================== + +// struct TestNewType(T); + +// fn main() { +// let variableˇ = TestNewType(TestStruct); +// } +// "}); + +// let hint_start_offset = cx.ranges(indoc! {" +// struct TestStruct; + +// // ================== + +// struct TestNewType(T); + +// fn main() { +// let variableˇ = TestNewType(TestStruct); +// } +// "})[0] +// .start; +// let hint_position = cx.to_lsp(hint_start_offset); +// let new_type_target_range = cx.lsp_range(indoc! {" +// struct TestStruct; + +// // ================== + +// struct «TestNewType»(T); + +// fn main() { +// let variable = TestNewType(TestStruct); +// } +// "}); +// let struct_target_range = cx.lsp_range(indoc! {" +// struct «TestStruct»; + +// // ================== + +// struct TestNewType(T); + +// fn main() { +// let variable = TestNewType(TestStruct); +// } +// "}); + +// let uri = cx.buffer_lsp_url.clone(); +// let new_type_label = "TestNewType"; +// let struct_label = "TestStruct"; +// let entire_hint_label = ": TestNewType"; +// let closure_uri = uri.clone(); +// cx.lsp +// .handle_request::(move |params, _| { +// let task_uri = closure_uri.clone(); +// async move { +// assert_eq!(params.text_document.uri, task_uri); +// Ok(Some(vec![lsp::InlayHint { +// position: hint_position, +// label: lsp::InlayHintLabel::LabelParts(vec![lsp::InlayHintLabelPart { +// value: entire_hint_label.to_string(), +// ..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![entire_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; + +// // ================== + +// struct TestNewType(T); + +// fn main() { +// let variable« »= TestNewType(TestStruct); +// } +// "}) +// .get(0) +// .cloned() +// .unwrap(); +// let new_type_hint_part_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() +// + (entire_hint_label.find(new_type_label).unwrap() + new_type_label.len() / 2) +// as u32, +// ); +// PointForPosition { +// previous_valid, +// next_valid, +// exact_unclipped, +// column_overshoot_after_line_end: 0, +// } +// }); +// cx.update_editor(|editor, cx| { +// update_inlay_link_and_hover_points( +// &editor.snapshot(cx), +// new_type_hint_part_hover_position, +// editor, +// true, +// false, +// cx, +// ); +// }); + +// let resolve_closure_uri = uri.clone(); +// cx.lsp +// .handle_request::( +// move |mut hint_to_resolve, _| { +// let mut resolved_hint_positions = BTreeSet::new(); +// let task_uri = resolve_closure_uri.clone(); +// async move { +// let inserted = resolved_hint_positions.insert(hint_to_resolve.position); +// assert!(inserted, "Hint {hint_to_resolve:?} was resolved twice"); + +// // `: TestNewType` +// hint_to_resolve.label = lsp::InlayHintLabel::LabelParts(vec![ +// lsp::InlayHintLabelPart { +// value: ": ".to_string(), +// ..Default::default() +// }, +// lsp::InlayHintLabelPart { +// value: new_type_label.to_string(), +// location: Some(lsp::Location { +// uri: task_uri.clone(), +// range: new_type_target_range, +// }), +// tooltip: Some(lsp::InlayHintLabelPartTooltip::String(format!( +// "A tooltip for `{new_type_label}`" +// ))), +// ..Default::default() +// }, +// lsp::InlayHintLabelPart { +// value: "<".to_string(), +// ..Default::default() +// }, +// lsp::InlayHintLabelPart { +// value: struct_label.to_string(), +// location: Some(lsp::Location { +// uri: task_uri, +// range: struct_target_range, +// }), +// tooltip: Some(lsp::InlayHintLabelPartTooltip::MarkupContent( +// lsp::MarkupContent { +// kind: lsp::MarkupKind::Markdown, +// value: format!("A tooltip for `{struct_label}`"), +// }, +// )), +// ..Default::default() +// }, +// lsp::InlayHintLabelPart { +// value: ">".to_string(), +// ..Default::default() +// }, +// ]); + +// Ok(hint_to_resolve) +// } +// }, +// ) +// .next() +// .await; +// cx.foreground().run_until_parked(); + +// cx.update_editor(|editor, cx| { +// update_inlay_link_and_hover_points( +// &editor.snapshot(cx), +// new_type_hint_part_hover_position, +// editor, +// true, +// false, +// cx, +// ); +// }); +// cx.foreground() +// .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100)); +// cx.foreground().run_until_parked(); +// cx.update_editor(|editor, cx| { +// let hover_state = &editor.hover_state; +// assert!(hover_state.diagnostic_popover.is_none() && hover_state.info_popover.is_some()); +// let popover = hover_state.info_popover.as_ref().unwrap(); +// let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx)); +// assert_eq!( +// popover.symbol_range, +// RangeInEditor::Inlay(InlayHighlight { +// inlay: InlayId::Hint(0), +// inlay_position: buffer_snapshot.anchor_at(inlay_range.start, Bias::Right), +// range: ": ".len()..": ".len() + new_type_label.len(), +// }), +// "Popover range should match the new type label part" +// ); +// assert_eq!( +// popover.parsed_content.text, +// format!("A tooltip for `{new_type_label}`"), +// "Rendered text should not anyhow alter backticks" +// ); +// }); + +// let struct_hint_part_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() +// + (entire_hint_label.find(struct_label).unwrap() + struct_label.len() / 2) +// as u32, +// ); +// PointForPosition { +// previous_valid, +// next_valid, +// exact_unclipped, +// column_overshoot_after_line_end: 0, +// } +// }); +// cx.update_editor(|editor, cx| { +// update_inlay_link_and_hover_points( +// &editor.snapshot(cx), +// struct_hint_part_hover_position, +// editor, +// true, +// false, +// cx, +// ); +// }); +// cx.foreground() +// .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100)); +// cx.foreground().run_until_parked(); +// cx.update_editor(|editor, cx| { +// let hover_state = &editor.hover_state; +// assert!(hover_state.diagnostic_popover.is_none() && hover_state.info_popover.is_some()); +// let popover = hover_state.info_popover.as_ref().unwrap(); +// let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx)); +// assert_eq!( +// popover.symbol_range, +// RangeInEditor::Inlay(InlayHighlight { +// inlay: InlayId::Hint(0), +// inlay_position: buffer_snapshot.anchor_at(inlay_range.start, Bias::Right), +// range: ": ".len() + new_type_label.len() + "<".len() +// ..": ".len() + new_type_label.len() + "<".len() + struct_label.len(), +// }), +// "Popover range should match the struct label part" +// ); +// assert_eq!( +// popover.parsed_content.text, +// format!("A tooltip for {struct_label}"), +// "Rendered markdown element should remove backticks from text" +// ); +// }); +// } +// } diff --git a/crates/editor2/src/inlay_hint_cache.rs b/crates/editor2/src/inlay_hint_cache.rs index c5f090084e..759323ecdf 100644 --- a/crates/editor2/src/inlay_hint_cache.rs +++ b/crates/editor2/src/inlay_hint_cache.rs @@ -250,7 +250,7 @@ impl InlayHintCache { pub fn update_settings( &mut self, - multi_buffer: &ModelHandle, + multi_buffer: &Model, new_hint_settings: InlayHintSettings, visible_hints: Vec, cx: &mut ViewContext, @@ -302,7 +302,7 @@ impl InlayHintCache { pub fn spawn_hint_refresh( &mut self, reason: &'static str, - excerpts_to_query: HashMap, Global, Range)>, + excerpts_to_query: HashMap, Global, Range)>, invalidate: InvalidationStrategy, cx: &mut ViewContext, ) -> Option { @@ -355,7 +355,7 @@ impl InlayHintCache { fn new_allowed_hint_kinds_splice( &self, - multi_buffer: &ModelHandle, + multi_buffer: &Model, visible_hints: &[Inlay], new_kinds: &HashSet>, cx: &mut ViewContext, @@ -579,7 +579,7 @@ impl InlayHintCache { fn spawn_new_update_tasks( editor: &mut Editor, reason: &'static str, - excerpts_to_query: HashMap, Global, Range)>, + excerpts_to_query: HashMap, Global, Range)>, invalidate: InvalidationStrategy, update_cache_version: usize, cx: &mut ViewContext<'_, '_, Editor>, @@ -684,7 +684,7 @@ impl QueryRanges { fn determine_query_ranges( multi_buffer: &mut MultiBuffer, excerpt_id: ExcerptId, - excerpt_buffer: &ModelHandle, + excerpt_buffer: &Model, excerpt_visible_range: Range, cx: &mut ModelContext<'_, MultiBuffer>, ) -> Option { @@ -837,7 +837,7 @@ fn new_update_task( } async fn fetch_and_update_hints( - editor: gpui::WeakViewHandle, + editor: gpui::WeakView, multi_buffer_snapshot: MultiBufferSnapshot, buffer_snapshot: BufferSnapshot, visible_hints: Arc>, @@ -1194,2156 +1194,2156 @@ fn apply_hint_update( } } -#[cfg(test)] -pub mod tests { - use std::sync::atomic::{AtomicBool, AtomicU32, AtomicUsize, Ordering}; - - use crate::{ - scroll::{autoscroll::Autoscroll, scroll_amount::ScrollAmount}, - serde_json::json, - ExcerptRange, - }; - use futures::StreamExt; - use gpui::{executor::Deterministic, TestAppContext, ViewHandle}; - use itertools::Itertools; - use language::{ - language_settings::AllLanguageSettingsContent, FakeLspAdapter, Language, LanguageConfig, - }; - use lsp::FakeLanguageServer; - use parking_lot::Mutex; - use project::{FakeFs, Project}; - use settings::SettingsStore; - use text::{Point, ToPoint}; - use workspace::Workspace; - - use crate::editor_tests::update_test_language_settings; - - use super::*; - - #[gpui::test] - async fn test_basic_cache_update_with_duplicate_hints(cx: &mut gpui::TestAppContext) { - let allowed_hint_kinds = HashSet::from_iter([None, Some(InlayHintKind::Type)]); - init_test(cx, |settings| { - settings.defaults.inlay_hints = Some(InlayHintSettings { - enabled: true, - show_type_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Type)), - show_parameter_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Parameter)), - show_other_hints: allowed_hint_kinds.contains(&None), - }) - }); - - let (file_with_hints, editor, fake_server) = prepare_test_objects(cx).await; - let lsp_request_count = Arc::new(AtomicU32::new(0)); - fake_server - .handle_request::(move |params, _| { - let task_lsp_request_count = Arc::clone(&lsp_request_count); - async move { - assert_eq!( - params.text_document.uri, - lsp::Url::from_file_path(file_with_hints).unwrap(), - ); - let current_call_id = - Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst); - let mut new_hints = Vec::with_capacity(2 * current_call_id as usize); - for _ in 0..2 { - let mut i = current_call_id; - loop { - new_hints.push(lsp::InlayHint { - position: lsp::Position::new(0, i), - label: lsp::InlayHintLabel::String(i.to_string()), - kind: None, - text_edits: None, - tooltip: None, - padding_left: None, - padding_right: None, - data: None, - }); - if i == 0 { - break; - } - i -= 1; - } - } - - Ok(Some(new_hints)) - } - }) - .next() - .await; - cx.foreground().run_until_parked(); - - let mut edits_made = 1; - editor.update(cx, |editor, cx| { - let expected_hints = vec!["0".to_string()]; - assert_eq!( - expected_hints, - cached_hint_labels(editor), - "Should get its first hints when opening the editor" - ); - assert_eq!(expected_hints, visible_hint_labels(editor, cx)); - let inlay_cache = editor.inlay_hint_cache(); - assert_eq!( - inlay_cache.allowed_hint_kinds, allowed_hint_kinds, - "Cache should use editor settings to get the allowed hint kinds" - ); - assert_eq!( - inlay_cache.version, edits_made, - "The editor update the cache version after every cache/view change" - ); - }); - - editor.update(cx, |editor, cx| { - editor.change_selections(None, cx, |s| s.select_ranges([13..13])); - editor.handle_input("some change", cx); - edits_made += 1; - }); - cx.foreground().run_until_parked(); - editor.update(cx, |editor, cx| { - let expected_hints = vec!["0".to_string(), "1".to_string()]; - assert_eq!( - expected_hints, - cached_hint_labels(editor), - "Should get new hints after an edit" - ); - assert_eq!(expected_hints, visible_hint_labels(editor, cx)); - let inlay_cache = editor.inlay_hint_cache(); - assert_eq!( - inlay_cache.allowed_hint_kinds, allowed_hint_kinds, - "Cache should use editor settings to get the allowed hint kinds" - ); - assert_eq!( - inlay_cache.version, edits_made, - "The editor update the cache version after every cache/view change" - ); - }); - - fake_server - .request::(()) - .await - .expect("inlay refresh request failed"); - edits_made += 1; - cx.foreground().run_until_parked(); - editor.update(cx, |editor, cx| { - let expected_hints = vec!["0".to_string(), "1".to_string(), "2".to_string()]; - assert_eq!( - expected_hints, - cached_hint_labels(editor), - "Should get new hints after hint refresh/ request" - ); - assert_eq!(expected_hints, visible_hint_labels(editor, cx)); - let inlay_cache = editor.inlay_hint_cache(); - assert_eq!( - inlay_cache.allowed_hint_kinds, allowed_hint_kinds, - "Cache should use editor settings to get the allowed hint kinds" - ); - assert_eq!( - inlay_cache.version, edits_made, - "The editor update the cache version after every cache/view change" - ); - }); - } - - #[gpui::test] - async fn test_cache_update_on_lsp_completion_tasks(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 (file_with_hints, editor, fake_server) = prepare_test_objects(cx).await; - let lsp_request_count = Arc::new(AtomicU32::new(0)); - fake_server - .handle_request::(move |params, _| { - let task_lsp_request_count = Arc::clone(&lsp_request_count); - async move { - assert_eq!( - params.text_document.uri, - lsp::Url::from_file_path(file_with_hints).unwrap(), - ); - let current_call_id = - Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst); - Ok(Some(vec![lsp::InlayHint { - position: lsp::Position::new(0, current_call_id), - label: lsp::InlayHintLabel::String(current_call_id.to_string()), - kind: None, - text_edits: None, - tooltip: None, - padding_left: None, - padding_right: None, - data: None, - }])) - } - }) - .next() - .await; - cx.foreground().run_until_parked(); - - let mut edits_made = 1; - editor.update(cx, |editor, cx| { - let expected_hints = vec!["0".to_string()]; - assert_eq!( - expected_hints, - cached_hint_labels(editor), - "Should get its first hints when opening the editor" - ); - assert_eq!(expected_hints, visible_hint_labels(editor, cx)); - assert_eq!( - editor.inlay_hint_cache().version, - edits_made, - "The editor update the cache version after every cache/view change" - ); - }); - - let progress_token = "test_progress_token"; - fake_server - .request::(lsp::WorkDoneProgressCreateParams { - token: lsp::ProgressToken::String(progress_token.to_string()), - }) - .await - .expect("work done progress create request failed"); - cx.foreground().run_until_parked(); - fake_server.notify::(lsp::ProgressParams { - token: lsp::ProgressToken::String(progress_token.to_string()), - value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Begin( - lsp::WorkDoneProgressBegin::default(), - )), - }); - cx.foreground().run_until_parked(); - - editor.update(cx, |editor, cx| { - let expected_hints = vec!["0".to_string()]; - assert_eq!( - expected_hints, - cached_hint_labels(editor), - "Should not update hints while the work task is running" - ); - assert_eq!(expected_hints, visible_hint_labels(editor, cx)); - assert_eq!( - editor.inlay_hint_cache().version, - edits_made, - "Should not update the cache while the work task is running" - ); - }); - - fake_server.notify::(lsp::ProgressParams { - token: lsp::ProgressToken::String(progress_token.to_string()), - value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::End( - lsp::WorkDoneProgressEnd::default(), - )), - }); - cx.foreground().run_until_parked(); - - edits_made += 1; - editor.update(cx, |editor, cx| { - let expected_hints = vec!["1".to_string()]; - assert_eq!( - expected_hints, - cached_hint_labels(editor), - "New hints should be queried after the work task is done" - ); - assert_eq!(expected_hints, visible_hint_labels(editor, cx)); - assert_eq!( - editor.inlay_hint_cache().version, - edits_made, - "Cache version should udpate once after the work task is done" - ); - }); - } - - #[gpui::test] - async fn test_no_hint_updates_for_unrelated_language_files(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 fs = FakeFs::new(cx.background()); - fs.insert_tree( - "/a", - json!({ - "main.rs": "fn main() { a } // and some long comment to ensure inlays are not trimmed out", - "other.md": "Test md file with some text", - }), - ) - .await; - let project = Project::test(fs, ["/a".as_ref()], cx).await; - let workspace = cx - .add_window(|cx| Workspace::test_new(project.clone(), cx)) - .root(cx); - let worktree_id = workspace.update(cx, |workspace, cx| { - workspace.project().read_with(cx, |project, cx| { - project.worktrees(cx).next().unwrap().read(cx).id() - }) - }); - - let mut rs_fake_servers = None; - let mut md_fake_servers = None; - for (name, path_suffix) in [("Rust", "rs"), ("Markdown", "md")] { - let mut language = Language::new( - LanguageConfig { - name: name.into(), - path_suffixes: vec![path_suffix.to_string()], - ..Default::default() - }, - Some(tree_sitter_rust::language()), - ); - let fake_servers = language - .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { - name, - capabilities: lsp::ServerCapabilities { - inlay_hint_provider: Some(lsp::OneOf::Left(true)), - ..Default::default() - }, - ..Default::default() - })) - .await; - match name { - "Rust" => rs_fake_servers = Some(fake_servers), - "Markdown" => md_fake_servers = Some(fake_servers), - _ => unreachable!(), - } - project.update(cx, |project, _| { - project.languages().add(Arc::new(language)); - }); - } - - let _rs_buffer = project - .update(cx, |project, cx| { - project.open_local_buffer("/a/main.rs", cx) - }) - .await - .unwrap(); - cx.foreground().run_until_parked(); - cx.foreground().start_waiting(); - let rs_fake_server = rs_fake_servers.unwrap().next().await.unwrap(); - let rs_editor = workspace - .update(cx, |workspace, cx| { - workspace.open_path((worktree_id, "main.rs"), None, true, cx) - }) - .await - .unwrap() - .downcast::() - .unwrap(); - let rs_lsp_request_count = Arc::new(AtomicU32::new(0)); - rs_fake_server - .handle_request::(move |params, _| { - let task_lsp_request_count = Arc::clone(&rs_lsp_request_count); - async move { - assert_eq!( - params.text_document.uri, - lsp::Url::from_file_path("/a/main.rs").unwrap(), - ); - let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst); - Ok(Some(vec![lsp::InlayHint { - position: lsp::Position::new(0, i), - label: lsp::InlayHintLabel::String(i.to_string()), - kind: None, - text_edits: None, - tooltip: None, - padding_left: None, - padding_right: None, - data: None, - }])) - } - }) - .next() - .await; - cx.foreground().run_until_parked(); - rs_editor.update(cx, |editor, cx| { - let expected_hints = vec!["0".to_string()]; - assert_eq!( - expected_hints, - cached_hint_labels(editor), - "Should get its first hints when opening the editor" - ); - assert_eq!(expected_hints, visible_hint_labels(editor, cx)); - assert_eq!( - editor.inlay_hint_cache().version, - 1, - "Rust editor update the cache version after every cache/view change" - ); - }); - - cx.foreground().run_until_parked(); - let _md_buffer = project - .update(cx, |project, cx| { - project.open_local_buffer("/a/other.md", cx) - }) - .await - .unwrap(); - cx.foreground().run_until_parked(); - cx.foreground().start_waiting(); - let md_fake_server = md_fake_servers.unwrap().next().await.unwrap(); - let md_editor = workspace - .update(cx, |workspace, cx| { - workspace.open_path((worktree_id, "other.md"), None, true, cx) - }) - .await - .unwrap() - .downcast::() - .unwrap(); - let md_lsp_request_count = Arc::new(AtomicU32::new(0)); - md_fake_server - .handle_request::(move |params, _| { - let task_lsp_request_count = Arc::clone(&md_lsp_request_count); - async move { - assert_eq!( - params.text_document.uri, - lsp::Url::from_file_path("/a/other.md").unwrap(), - ); - let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst); - Ok(Some(vec![lsp::InlayHint { - position: lsp::Position::new(0, i), - label: lsp::InlayHintLabel::String(i.to_string()), - kind: None, - text_edits: None, - tooltip: None, - padding_left: None, - padding_right: None, - data: None, - }])) - } - }) - .next() - .await; - cx.foreground().run_until_parked(); - md_editor.update(cx, |editor, cx| { - let expected_hints = vec!["0".to_string()]; - assert_eq!( - expected_hints, - cached_hint_labels(editor), - "Markdown editor should have a separate verison, repeating Rust editor rules" - ); - assert_eq!(expected_hints, visible_hint_labels(editor, cx)); - assert_eq!(editor.inlay_hint_cache().version, 1); - }); - - rs_editor.update(cx, |editor, cx| { - editor.change_selections(None, cx, |s| s.select_ranges([13..13])); - editor.handle_input("some rs change", cx); - }); - cx.foreground().run_until_parked(); - rs_editor.update(cx, |editor, cx| { - let expected_hints = vec!["1".to_string()]; - assert_eq!( - expected_hints, - cached_hint_labels(editor), - "Rust inlay cache should change after the edit" - ); - assert_eq!(expected_hints, visible_hint_labels(editor, cx)); - assert_eq!( - editor.inlay_hint_cache().version, - 2, - "Every time hint cache changes, cache version should be incremented" - ); - }); - md_editor.update(cx, |editor, cx| { - let expected_hints = vec!["0".to_string()]; - assert_eq!( - expected_hints, - cached_hint_labels(editor), - "Markdown editor should not be affected by Rust editor changes" - ); - assert_eq!(expected_hints, visible_hint_labels(editor, cx)); - assert_eq!(editor.inlay_hint_cache().version, 1); - }); - - md_editor.update(cx, |editor, cx| { - editor.change_selections(None, cx, |s| s.select_ranges([13..13])); - editor.handle_input("some md change", cx); - }); - cx.foreground().run_until_parked(); - md_editor.update(cx, |editor, cx| { - let expected_hints = vec!["1".to_string()]; - assert_eq!( - expected_hints, - cached_hint_labels(editor), - "Rust editor should not be affected by Markdown editor changes" - ); - assert_eq!(expected_hints, visible_hint_labels(editor, cx)); - assert_eq!(editor.inlay_hint_cache().version, 2); - }); - rs_editor.update(cx, |editor, cx| { - let expected_hints = vec!["1".to_string()]; - assert_eq!( - expected_hints, - cached_hint_labels(editor), - "Markdown editor should also change independently" - ); - assert_eq!(expected_hints, visible_hint_labels(editor, cx)); - assert_eq!(editor.inlay_hint_cache().version, 2); - }); - } - - #[gpui::test] - async fn test_hint_setting_changes(cx: &mut gpui::TestAppContext) { - let allowed_hint_kinds = HashSet::from_iter([None, Some(InlayHintKind::Type)]); - init_test(cx, |settings| { - settings.defaults.inlay_hints = Some(InlayHintSettings { - enabled: true, - show_type_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Type)), - show_parameter_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Parameter)), - show_other_hints: allowed_hint_kinds.contains(&None), - }) - }); - - let (file_with_hints, editor, fake_server) = prepare_test_objects(cx).await; - let lsp_request_count = Arc::new(AtomicU32::new(0)); - let another_lsp_request_count = Arc::clone(&lsp_request_count); - fake_server - .handle_request::(move |params, _| { - let task_lsp_request_count = Arc::clone(&another_lsp_request_count); - async move { - Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst); - assert_eq!( - params.text_document.uri, - lsp::Url::from_file_path(file_with_hints).unwrap(), - ); - Ok(Some(vec![ - lsp::InlayHint { - position: lsp::Position::new(0, 1), - label: lsp::InlayHintLabel::String("type hint".to_string()), - kind: Some(lsp::InlayHintKind::TYPE), - text_edits: None, - tooltip: None, - padding_left: None, - padding_right: None, - data: None, - }, - lsp::InlayHint { - position: lsp::Position::new(0, 2), - label: lsp::InlayHintLabel::String("parameter hint".to_string()), - kind: Some(lsp::InlayHintKind::PARAMETER), - text_edits: None, - tooltip: None, - padding_left: None, - padding_right: None, - data: None, - }, - lsp::InlayHint { - position: lsp::Position::new(0, 3), - label: lsp::InlayHintLabel::String("other hint".to_string()), - kind: None, - text_edits: None, - tooltip: None, - padding_left: None, - padding_right: None, - data: None, - }, - ])) - } - }) - .next() - .await; - cx.foreground().run_until_parked(); - - let mut edits_made = 1; - editor.update(cx, |editor, cx| { - assert_eq!( - lsp_request_count.load(Ordering::Relaxed), - 1, - "Should query new hints once" - ); - assert_eq!( - vec![ - "other hint".to_string(), - "parameter hint".to_string(), - "type hint".to_string(), - ], - cached_hint_labels(editor), - "Should get its first hints when opening the editor" - ); - assert_eq!( - vec!["other hint".to_string(), "type hint".to_string()], - visible_hint_labels(editor, cx) - ); - let inlay_cache = editor.inlay_hint_cache(); - assert_eq!( - inlay_cache.allowed_hint_kinds, allowed_hint_kinds, - "Cache should use editor settings to get the allowed hint kinds" - ); - assert_eq!( - inlay_cache.version, edits_made, - "The editor update the cache version after every cache/view change" - ); - }); - - fake_server - .request::(()) - .await - .expect("inlay refresh request failed"); - cx.foreground().run_until_parked(); - editor.update(cx, |editor, cx| { - assert_eq!( - lsp_request_count.load(Ordering::Relaxed), - 2, - "Should load new hints twice" - ); - assert_eq!( - vec![ - "other hint".to_string(), - "parameter hint".to_string(), - "type hint".to_string(), - ], - cached_hint_labels(editor), - "Cached hints should not change due to allowed hint kinds settings update" - ); - assert_eq!( - vec!["other hint".to_string(), "type hint".to_string()], - visible_hint_labels(editor, cx) - ); - assert_eq!( - editor.inlay_hint_cache().version, - edits_made, - "Should not update cache version due to new loaded hints being the same" - ); - }); - - for (new_allowed_hint_kinds, expected_visible_hints) in [ - (HashSet::from_iter([None]), vec!["other hint".to_string()]), - ( - HashSet::from_iter([Some(InlayHintKind::Type)]), - vec!["type hint".to_string()], - ), - ( - HashSet::from_iter([Some(InlayHintKind::Parameter)]), - vec!["parameter hint".to_string()], - ), - ( - HashSet::from_iter([None, Some(InlayHintKind::Type)]), - vec!["other hint".to_string(), "type hint".to_string()], - ), - ( - HashSet::from_iter([None, Some(InlayHintKind::Parameter)]), - vec!["other hint".to_string(), "parameter hint".to_string()], - ), - ( - HashSet::from_iter([Some(InlayHintKind::Type), Some(InlayHintKind::Parameter)]), - vec!["parameter hint".to_string(), "type hint".to_string()], - ), - ( - HashSet::from_iter([ - None, - Some(InlayHintKind::Type), - Some(InlayHintKind::Parameter), - ]), - vec![ - "other hint".to_string(), - "parameter hint".to_string(), - "type hint".to_string(), - ], - ), - ] { - edits_made += 1; - update_test_language_settings(cx, |settings| { - settings.defaults.inlay_hints = Some(InlayHintSettings { - enabled: true, - show_type_hints: new_allowed_hint_kinds.contains(&Some(InlayHintKind::Type)), - show_parameter_hints: new_allowed_hint_kinds - .contains(&Some(InlayHintKind::Parameter)), - show_other_hints: new_allowed_hint_kinds.contains(&None), - }) - }); - cx.foreground().run_until_parked(); - editor.update(cx, |editor, cx| { - assert_eq!( - lsp_request_count.load(Ordering::Relaxed), - 2, - "Should not load new hints on allowed hint kinds change for hint kinds {new_allowed_hint_kinds:?}" - ); - assert_eq!( - vec![ - "other hint".to_string(), - "parameter hint".to_string(), - "type hint".to_string(), - ], - cached_hint_labels(editor), - "Should get its cached hints unchanged after the settings change for hint kinds {new_allowed_hint_kinds:?}" - ); - assert_eq!( - expected_visible_hints, - visible_hint_labels(editor, cx), - "Should get its visible hints filtered after the settings change for hint kinds {new_allowed_hint_kinds:?}" - ); - let inlay_cache = editor.inlay_hint_cache(); - assert_eq!( - inlay_cache.allowed_hint_kinds, new_allowed_hint_kinds, - "Cache should use editor settings to get the allowed hint kinds for hint kinds {new_allowed_hint_kinds:?}" - ); - assert_eq!( - inlay_cache.version, edits_made, - "The editor should update the cache version after every cache/view change for hint kinds {new_allowed_hint_kinds:?} due to visible hints change" - ); - }); - } - - edits_made += 1; - let another_allowed_hint_kinds = HashSet::from_iter([Some(InlayHintKind::Type)]); - update_test_language_settings(cx, |settings| { - settings.defaults.inlay_hints = Some(InlayHintSettings { - enabled: false, - show_type_hints: another_allowed_hint_kinds.contains(&Some(InlayHintKind::Type)), - show_parameter_hints: another_allowed_hint_kinds - .contains(&Some(InlayHintKind::Parameter)), - show_other_hints: another_allowed_hint_kinds.contains(&None), - }) - }); - cx.foreground().run_until_parked(); - editor.update(cx, |editor, cx| { - assert_eq!( - lsp_request_count.load(Ordering::Relaxed), - 2, - "Should not load new hints when hints got disabled" - ); - assert!( - cached_hint_labels(editor).is_empty(), - "Should clear the cache when hints got disabled" - ); - assert!( - visible_hint_labels(editor, cx).is_empty(), - "Should clear visible hints when hints got disabled" - ); - let inlay_cache = editor.inlay_hint_cache(); - assert_eq!( - inlay_cache.allowed_hint_kinds, another_allowed_hint_kinds, - "Should update its allowed hint kinds even when hints got disabled" - ); - assert_eq!( - inlay_cache.version, edits_made, - "The editor should update the cache version after hints got disabled" - ); - }); - - fake_server - .request::(()) - .await - .expect("inlay refresh request failed"); - cx.foreground().run_until_parked(); - editor.update(cx, |editor, cx| { - assert_eq!( - lsp_request_count.load(Ordering::Relaxed), - 2, - "Should not load new hints when they got disabled" - ); - assert!(cached_hint_labels(editor).is_empty()); - assert!(visible_hint_labels(editor, cx).is_empty()); - assert_eq!( - editor.inlay_hint_cache().version, edits_made, - "The editor should not update the cache version after /refresh query without updates" - ); - }); - - let final_allowed_hint_kinds = HashSet::from_iter([Some(InlayHintKind::Parameter)]); - edits_made += 1; - update_test_language_settings(cx, |settings| { - settings.defaults.inlay_hints = Some(InlayHintSettings { - enabled: true, - show_type_hints: final_allowed_hint_kinds.contains(&Some(InlayHintKind::Type)), - show_parameter_hints: final_allowed_hint_kinds - .contains(&Some(InlayHintKind::Parameter)), - show_other_hints: final_allowed_hint_kinds.contains(&None), - }) - }); - cx.foreground().run_until_parked(); - editor.update(cx, |editor, cx| { - assert_eq!( - lsp_request_count.load(Ordering::Relaxed), - 3, - "Should query for new hints when they got reenabled" - ); - assert_eq!( - vec![ - "other hint".to_string(), - "parameter hint".to_string(), - "type hint".to_string(), - ], - cached_hint_labels(editor), - "Should get its cached hints fully repopulated after the hints got reenabled" - ); - assert_eq!( - vec!["parameter hint".to_string()], - visible_hint_labels(editor, cx), - "Should get its visible hints repopulated and filtered after the h" - ); - let inlay_cache = editor.inlay_hint_cache(); - assert_eq!( - inlay_cache.allowed_hint_kinds, final_allowed_hint_kinds, - "Cache should update editor settings when hints got reenabled" - ); - assert_eq!( - inlay_cache.version, edits_made, - "Cache should update its version after hints got reenabled" - ); - }); - - fake_server - .request::(()) - .await - .expect("inlay refresh request failed"); - cx.foreground().run_until_parked(); - editor.update(cx, |editor, cx| { - assert_eq!( - lsp_request_count.load(Ordering::Relaxed), - 4, - "Should query for new hints again" - ); - assert_eq!( - vec![ - "other hint".to_string(), - "parameter hint".to_string(), - "type hint".to_string(), - ], - cached_hint_labels(editor), - ); - assert_eq!( - vec!["parameter hint".to_string()], - visible_hint_labels(editor, cx), - ); - assert_eq!(editor.inlay_hint_cache().version, edits_made); - }); - } - - #[gpui::test] - async fn test_hint_request_cancellation(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 (file_with_hints, editor, fake_server) = prepare_test_objects(cx).await; - let fake_server = Arc::new(fake_server); - let lsp_request_count = Arc::new(AtomicU32::new(0)); - let another_lsp_request_count = Arc::clone(&lsp_request_count); - fake_server - .handle_request::(move |params, _| { - let task_lsp_request_count = Arc::clone(&another_lsp_request_count); - async move { - let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst) + 1; - assert_eq!( - params.text_document.uri, - lsp::Url::from_file_path(file_with_hints).unwrap(), - ); - Ok(Some(vec![lsp::InlayHint { - position: lsp::Position::new(0, i), - label: lsp::InlayHintLabel::String(i.to_string()), - kind: None, - text_edits: None, - tooltip: None, - padding_left: None, - padding_right: None, - data: None, - }])) - } - }) - .next() - .await; - - let mut expected_changes = Vec::new(); - for change_after_opening in [ - "initial change #1", - "initial change #2", - "initial change #3", - ] { - editor.update(cx, |editor, cx| { - editor.change_selections(None, cx, |s| s.select_ranges([13..13])); - editor.handle_input(change_after_opening, cx); - }); - expected_changes.push(change_after_opening); - } - - cx.foreground().run_until_parked(); - - editor.update(cx, |editor, cx| { - let current_text = editor.text(cx); - for change in &expected_changes { - assert!( - current_text.contains(change), - "Should apply all changes made" - ); - } - assert_eq!( - lsp_request_count.load(Ordering::Relaxed), - 2, - "Should query new hints twice: for editor init and for the last edit that interrupted all others" - ); - let expected_hints = vec!["2".to_string()]; - assert_eq!( - expected_hints, - cached_hint_labels(editor), - "Should get hints from the last edit landed only" - ); - assert_eq!(expected_hints, visible_hint_labels(editor, cx)); - assert_eq!( - editor.inlay_hint_cache().version, 1, - "Only one update should be registered in the cache after all cancellations" - ); - }); - - let mut edits = Vec::new(); - for async_later_change in [ - "another change #1", - "another change #2", - "another change #3", - ] { - expected_changes.push(async_later_change); - let task_editor = editor.clone(); - let mut task_cx = cx.clone(); - edits.push(cx.foreground().spawn(async move { - task_editor.update(&mut task_cx, |editor, cx| { - editor.change_selections(None, cx, |s| s.select_ranges([13..13])); - editor.handle_input(async_later_change, cx); - }); - })); - } - let _ = future::join_all(edits).await; - cx.foreground().run_until_parked(); - - editor.update(cx, |editor, cx| { - let current_text = editor.text(cx); - for change in &expected_changes { - assert!( - current_text.contains(change), - "Should apply all changes made" - ); - } - assert_eq!( - lsp_request_count.load(Ordering::SeqCst), - 3, - "Should query new hints one more time, for the last edit only" - ); - let expected_hints = vec!["3".to_string()]; - assert_eq!( - expected_hints, - cached_hint_labels(editor), - "Should get hints from the last edit landed only" - ); - assert_eq!(expected_hints, visible_hint_labels(editor, cx)); - assert_eq!( - editor.inlay_hint_cache().version, - 2, - "Should update the cache version once more, for the new change" - ); - }); - } - - #[gpui::test(iterations = 10)] - async fn test_large_buffer_inlay_requests_split(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 fs = FakeFs::new(cx.background()); - fs.insert_tree( - "/a", - json!({ - "main.rs": format!("fn main() {{\n{}\n}}", "let i = 5;\n".repeat(500)), - "other.rs": "// Test file", - }), - ) - .await; - let project = Project::test(fs, ["/a".as_ref()], cx).await; - project.update(cx, |project, _| project.languages().add(Arc::new(language))); - let workspace = cx - .add_window(|cx| Workspace::test_new(project.clone(), cx)) - .root(cx); - let worktree_id = workspace.update(cx, |workspace, cx| { - workspace.project().read_with(cx, |project, cx| { - project.worktrees(cx).next().unwrap().read(cx).id() - }) - }); - - let _buffer = project - .update(cx, |project, cx| { - project.open_local_buffer("/a/main.rs", cx) - }) - .await - .unwrap(); - cx.foreground().run_until_parked(); - cx.foreground().start_waiting(); - let fake_server = fake_servers.next().await.unwrap(); - let editor = workspace - .update(cx, |workspace, cx| { - workspace.open_path((worktree_id, "main.rs"), None, true, cx) - }) - .await - .unwrap() - .downcast::() - .unwrap(); - let lsp_request_ranges = Arc::new(Mutex::new(Vec::new())); - let lsp_request_count = Arc::new(AtomicUsize::new(0)); - let closure_lsp_request_ranges = Arc::clone(&lsp_request_ranges); - let closure_lsp_request_count = Arc::clone(&lsp_request_count); - fake_server - .handle_request::(move |params, _| { - let task_lsp_request_ranges = Arc::clone(&closure_lsp_request_ranges); - let task_lsp_request_count = Arc::clone(&closure_lsp_request_count); - async move { - assert_eq!( - params.text_document.uri, - lsp::Url::from_file_path("/a/main.rs").unwrap(), - ); - - task_lsp_request_ranges.lock().push(params.range); - let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::Release) + 1; - Ok(Some(vec![lsp::InlayHint { - position: params.range.end, - label: lsp::InlayHintLabel::String(i.to_string()), - kind: None, - text_edits: None, - tooltip: None, - padding_left: None, - padding_right: None, - data: None, - }])) - } - }) - .next() - .await; - fn editor_visible_range( - editor: &ViewHandle, - cx: &mut gpui::TestAppContext, - ) -> Range { - let ranges = editor.update(cx, |editor, cx| editor.excerpt_visible_offsets(None, cx)); - assert_eq!( - ranges.len(), - 1, - "Single buffer should produce a single excerpt with visible range" - ); - let (_, (excerpt_buffer, _, excerpt_visible_range)) = - ranges.into_iter().next().unwrap(); - excerpt_buffer.update(cx, |buffer, _| { - let snapshot = buffer.snapshot(); - let start = buffer - .anchor_before(excerpt_visible_range.start) - .to_point(&snapshot); - let end = buffer - .anchor_after(excerpt_visible_range.end) - .to_point(&snapshot); - start..end - }) - } - - // in large buffers, requests are made for more than visible range of a buffer. - // invisible parts are queried later, to avoid excessive requests on quick typing. - // wait the timeout needed to get all requests. - cx.foreground().advance_clock(Duration::from_millis( - INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100, - )); - cx.foreground().run_until_parked(); - let initial_visible_range = editor_visible_range(&editor, cx); - let lsp_initial_visible_range = lsp::Range::new( - lsp::Position::new( - initial_visible_range.start.row, - initial_visible_range.start.column, - ), - lsp::Position::new( - initial_visible_range.end.row, - initial_visible_range.end.column, - ), - ); - let expected_initial_query_range_end = - lsp::Position::new(initial_visible_range.end.row * 2, 2); - let mut expected_invisible_query_start = lsp_initial_visible_range.end; - expected_invisible_query_start.character += 1; - editor.update(cx, |editor, cx| { - let ranges = lsp_request_ranges.lock().drain(..).collect::>(); - assert_eq!(ranges.len(), 2, - "When scroll is at the edge of a big document, its visible part and the same range further should be queried in order, but got: {ranges:?}"); - let visible_query_range = &ranges[0]; - assert_eq!(visible_query_range.start, lsp_initial_visible_range.start); - assert_eq!(visible_query_range.end, lsp_initial_visible_range.end); - let invisible_query_range = &ranges[1]; - - assert_eq!(invisible_query_range.start, expected_invisible_query_start, "Should initially query visible edge of the document"); - assert_eq!(invisible_query_range.end, expected_initial_query_range_end, "Should initially query visible edge of the document"); - - let requests_count = lsp_request_count.load(Ordering::Acquire); - assert_eq!(requests_count, 2, "Visible + invisible request"); - let expected_hints = vec!["1".to_string(), "2".to_string()]; - assert_eq!( - expected_hints, - cached_hint_labels(editor), - "Should have hints from both LSP requests made for a big file" - ); - assert_eq!(expected_hints, visible_hint_labels(editor, cx), "Should display only hints from the visible range"); - assert_eq!( - editor.inlay_hint_cache().version, requests_count, - "LSP queries should've bumped the cache version" - ); - }); - - editor.update(cx, |editor, cx| { - editor.scroll_screen(&ScrollAmount::Page(1.0), cx); - editor.scroll_screen(&ScrollAmount::Page(1.0), cx); - }); - cx.foreground().advance_clock(Duration::from_millis( - INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100, - )); - cx.foreground().run_until_parked(); - let visible_range_after_scrolls = editor_visible_range(&editor, cx); - let visible_line_count = - editor.update(cx, |editor, _| editor.visible_line_count().unwrap()); - let selection_in_cached_range = editor.update(cx, |editor, cx| { - let ranges = lsp_request_ranges - .lock() - .drain(..) - .sorted_by_key(|r| r.start) - .collect::>(); - assert_eq!( - ranges.len(), - 2, - "Should query 2 ranges after both scrolls, but got: {ranges:?}" - ); - let first_scroll = &ranges[0]; - let second_scroll = &ranges[1]; - assert_eq!( - first_scroll.end, second_scroll.start, - "Should query 2 adjacent ranges after the scrolls, but got: {ranges:?}" - ); - assert_eq!( - first_scroll.start, expected_initial_query_range_end, - "First scroll should start the query right after the end of the original scroll", - ); - assert_eq!( - second_scroll.end, - lsp::Position::new( - visible_range_after_scrolls.end.row - + visible_line_count.ceil() as u32, - 1, - ), - "Second scroll should query one more screen down after the end of the visible range" - ); - - let lsp_requests = lsp_request_count.load(Ordering::Acquire); - assert_eq!(lsp_requests, 4, "Should query for hints after every scroll"); - let expected_hints = vec![ - "1".to_string(), - "2".to_string(), - "3".to_string(), - "4".to_string(), - ]; - assert_eq!( - expected_hints, - cached_hint_labels(editor), - "Should have hints from the new LSP response after the edit" - ); - assert_eq!(expected_hints, visible_hint_labels(editor, cx)); - assert_eq!( - editor.inlay_hint_cache().version, - lsp_requests, - "Should update the cache for every LSP response with hints added" - ); - - let mut selection_in_cached_range = visible_range_after_scrolls.end; - selection_in_cached_range.row -= visible_line_count.ceil() as u32; - selection_in_cached_range - }); - - editor.update(cx, |editor, cx| { - editor.change_selections(Some(Autoscroll::center()), cx, |s| { - s.select_ranges([selection_in_cached_range..selection_in_cached_range]) - }); - }); - cx.foreground().advance_clock(Duration::from_millis( - INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100, - )); - cx.foreground().run_until_parked(); - editor.update(cx, |_, _| { - let ranges = lsp_request_ranges - .lock() - .drain(..) - .sorted_by_key(|r| r.start) - .collect::>(); - assert!(ranges.is_empty(), "No new ranges or LSP queries should be made after returning to the selection with cached hints"); - assert_eq!(lsp_request_count.load(Ordering::Acquire), 4); - }); - - editor.update(cx, |editor, cx| { - editor.handle_input("++++more text++++", cx); - }); - cx.foreground().advance_clock(Duration::from_millis( - INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100, - )); - cx.foreground().run_until_parked(); - editor.update(cx, |editor, cx| { - let mut ranges = lsp_request_ranges.lock().drain(..).collect::>(); - ranges.sort_by_key(|r| r.start); - - assert_eq!(ranges.len(), 3, - "On edit, should scroll to selection and query a range around it: visible + same range above and below. Instead, got query ranges {ranges:?}"); - let above_query_range = &ranges[0]; - let visible_query_range = &ranges[1]; - let below_query_range = &ranges[2]; - assert!(above_query_range.end.character < visible_query_range.start.character || above_query_range.end.line + 1 == visible_query_range.start.line, - "Above range {above_query_range:?} should be before visible range {visible_query_range:?}"); - assert!(visible_query_range.end.character < below_query_range.start.character || visible_query_range.end.line + 1 == below_query_range.start.line, - "Visible range {visible_query_range:?} should be before below range {below_query_range:?}"); - assert!(above_query_range.start.line < selection_in_cached_range.row, - "Hints should be queried with the selected range after the query range start"); - assert!(below_query_range.end.line > selection_in_cached_range.row, - "Hints should be queried with the selected range before the query range end"); - assert!(above_query_range.start.line <= selection_in_cached_range.row - (visible_line_count * 3.0 / 2.0) as u32, - "Hints query range should contain one more screen before"); - assert!(below_query_range.end.line >= selection_in_cached_range.row + (visible_line_count * 3.0 / 2.0) as u32, - "Hints query range should contain one more screen after"); - - let lsp_requests = lsp_request_count.load(Ordering::Acquire); - assert_eq!(lsp_requests, 7, "There should be a visible range and two ranges above and below it queried"); - let expected_hints = vec!["5".to_string(), "6".to_string(), "7".to_string()]; - assert_eq!(expected_hints, cached_hint_labels(editor), - "Should have hints from the new LSP response after the edit"); - assert_eq!(expected_hints, visible_hint_labels(editor, cx)); - assert_eq!(editor.inlay_hint_cache().version, lsp_requests, "Should update the cache for every LSP response with hints added"); - }); - } - - #[gpui::test(iterations = 10)] - async fn test_multiple_excerpts_large_multibuffer( - deterministic: Arc, - 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()); - 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 workspace = cx - .add_window(|cx| Workspace::test_new(project.clone(), cx)) - .root(cx); - let worktree_id = workspace.update(cx, |workspace, cx| { - workspace.project().read_with(cx, |project, cx| { - project.worktrees(cx).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.add_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 - }); - - deterministic.run_until_parked(); - cx.foreground().run_until_parked(); - let editor = cx - .add_window(|cx| Editor::for_multibuffer(multibuffer, Some(project.clone()), cx)) - .root(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.foreground().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(), - ]; - 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.foreground().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.foreground().advance_clock(Duration::from_millis( - INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100, - )); - cx.foreground().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() - }); - - 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.foreground().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.foreground().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( - deterministic: Arc, - cx: &mut gpui::TestAppContext, - ) { - init_test(cx, |settings| { - settings.defaults.inlay_hints = Some(InlayHintSettings { - enabled: true, - show_type_hints: false, - show_parameter_hints: false, - show_other_hints: false, - }) - }); - - 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()); - 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 workspace = cx - .add_window(|cx| Workspace::test_new(project.clone(), cx)) - .root(cx); - let worktree_id = workspace.update(cx, |workspace, cx| { - workspace.project().read_with(cx, |project, cx| { - project.worktrees(cx).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.add_model(|_| MultiBuffer::new(0)); - let (buffer_1_excerpts, buffer_2_excerpts) = multibuffer.update(cx, |multibuffer, cx| { - let buffer_1_excerpts = multibuffer.push_excerpts( - buffer_1.clone(), - [ExcerptRange { - context: Point::new(0, 0)..Point::new(2, 0), - primary: None, - }], - cx, - ); - let buffer_2_excerpts = multibuffer.push_excerpts( - buffer_2.clone(), - [ExcerptRange { - context: Point::new(0, 1)..Point::new(2, 1), - primary: None, - }], - cx, - ); - (buffer_1_excerpts, buffer_2_excerpts) - }); - - assert!(!buffer_1_excerpts.is_empty()); - assert!(!buffer_2_excerpts.is_empty()); - - deterministic.run_until_parked(); - cx.foreground().run_until_parked(); - let editor = cx - .add_window(|cx| Editor::for_multibuffer(multibuffer, Some(project.clone()), cx)) - .root(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); - }; - - 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.foreground().run_until_parked(); - - editor.update(cx, |editor, cx| { - assert_eq!( - vec!["main hint #0".to_string(), "other hint #0".to_string()], - cached_hint_labels(editor), - "Cache should update for both excerpts despite hints display was disabled" - ); - assert!( - visible_hint_labels(editor, cx).is_empty(), - "All hints are disabled and should not be shown despite being present in the cache" - ); - assert_eq!( - editor.inlay_hint_cache().version, - 2, - "Cache should update once per excerpt query" - ); - }); - - editor.update(cx, |editor, cx| { - editor.buffer().update(cx, |multibuffer, cx| { - multibuffer.remove_excerpts(buffer_2_excerpts, cx) - }) - }); - cx.foreground().run_until_parked(); - editor.update(cx, |editor, cx| { - assert_eq!( - vec!["main hint #0".to_string()], - cached_hint_labels(editor), - "For the removed excerpt, should clean corresponding cached hints" - ); - assert!( - visible_hint_labels(editor, cx).is_empty(), - "All hints are disabled and should not be shown despite being present in the cache" - ); - assert_eq!( - editor.inlay_hint_cache().version, - 3, - "Excerpt removal should trigger a cache update" - ); - }); - - update_test_language_settings(cx, |settings| { - settings.defaults.inlay_hints = Some(InlayHintSettings { - enabled: true, - show_type_hints: true, - show_parameter_hints: true, - show_other_hints: true, - }) - }); - cx.foreground().run_until_parked(); - editor.update(cx, |editor, cx| { - let expected_hints = vec!["main hint #0".to_string()]; - assert_eq!( - expected_hints, - cached_hint_labels(editor), - "Hint display settings change should not change the cache" - ); - assert_eq!( - expected_hints, - visible_hint_labels(editor, cx), - "Settings change should make cached hints visible" - ); - assert_eq!( - editor.inlay_hint_cache().version, - 4, - "Settings change should trigger a cache update" - ); - }); - } - - #[gpui::test] - async fn test_inside_char_boundary_range_hints(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 fs = FakeFs::new(cx.background()); - fs.insert_tree( - "/a", - json!({ - "main.rs": format!(r#"fn main() {{\n{}\n}}"#, format!("let i = {};\n", "√".repeat(10)).repeat(500)), - "other.rs": "// Test file", - }), - ) - .await; - let project = Project::test(fs, ["/a".as_ref()], cx).await; - project.update(cx, |project, _| project.languages().add(Arc::new(language))); - let workspace = cx - .add_window(|cx| Workspace::test_new(project.clone(), cx)) - .root(cx); - let worktree_id = workspace.update(cx, |workspace, cx| { - workspace.project().read_with(cx, |project, cx| { - project.worktrees(cx).next().unwrap().read(cx).id() - }) - }); - - let _buffer = project - .update(cx, |project, cx| { - project.open_local_buffer("/a/main.rs", cx) - }) - .await - .unwrap(); - cx.foreground().run_until_parked(); - cx.foreground().start_waiting(); - let fake_server = fake_servers.next().await.unwrap(); - let editor = workspace - .update(cx, |workspace, cx| { - workspace.open_path((worktree_id, "main.rs"), None, true, cx) - }) - .await - .unwrap() - .downcast::() - .unwrap(); - let lsp_request_count = Arc::new(AtomicU32::new(0)); - let closure_lsp_request_count = Arc::clone(&lsp_request_count); - fake_server - .handle_request::(move |params, _| { - let task_lsp_request_count = Arc::clone(&closure_lsp_request_count); - async move { - assert_eq!( - params.text_document.uri, - lsp::Url::from_file_path("/a/main.rs").unwrap(), - ); - let query_start = params.range.start; - let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::Release) + 1; - Ok(Some(vec![lsp::InlayHint { - position: query_start, - label: lsp::InlayHintLabel::String(i.to_string()), - kind: None, - text_edits: None, - tooltip: None, - padding_left: None, - padding_right: None, - data: None, - }])) - } - }) - .next() - .await; - - cx.foreground().run_until_parked(); - editor.update(cx, |editor, cx| { - editor.change_selections(None, cx, |s| { - s.select_ranges([Point::new(10, 0)..Point::new(10, 0)]) - }) - }); - cx.foreground().run_until_parked(); - editor.update(cx, |editor, cx| { - let expected_hints = vec!["1".to_string()]; - assert_eq!(expected_hints, cached_hint_labels(editor)); - assert_eq!(expected_hints, visible_hint_labels(editor, cx)); - assert_eq!(editor.inlay_hint_cache().version, 1); - }); - } - - #[gpui::test] - async fn test_toggle_inlay_hints(cx: &mut gpui::TestAppContext) { - init_test(cx, |settings| { - settings.defaults.inlay_hints = Some(InlayHintSettings { - enabled: false, - show_type_hints: true, - show_parameter_hints: true, - show_other_hints: true, - }) - }); - - let (file_with_hints, editor, fake_server) = prepare_test_objects(cx).await; - - editor.update(cx, |editor, cx| { - editor.toggle_inlay_hints(&crate::ToggleInlayHints, cx) - }); - cx.foreground().start_waiting(); - let lsp_request_count = Arc::new(AtomicU32::new(0)); - let closure_lsp_request_count = Arc::clone(&lsp_request_count); - fake_server - .handle_request::(move |params, _| { - let task_lsp_request_count = Arc::clone(&closure_lsp_request_count); - async move { - assert_eq!( - params.text_document.uri, - lsp::Url::from_file_path(file_with_hints).unwrap(), - ); - - let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst) + 1; - Ok(Some(vec![lsp::InlayHint { - position: lsp::Position::new(0, i), - label: lsp::InlayHintLabel::String(i.to_string()), - kind: None, - text_edits: None, - tooltip: None, - padding_left: None, - padding_right: None, - data: None, - }])) - } - }) - .next() - .await; - cx.foreground().run_until_parked(); - editor.update(cx, |editor, cx| { - let expected_hints = vec!["1".to_string()]; - assert_eq!( - expected_hints, - cached_hint_labels(editor), - "Should display inlays after toggle despite them disabled in settings" - ); - assert_eq!(expected_hints, visible_hint_labels(editor, cx)); - assert_eq!( - editor.inlay_hint_cache().version, - 1, - "First toggle should be cache's first update" - ); - }); - - editor.update(cx, |editor, cx| { - editor.toggle_inlay_hints(&crate::ToggleInlayHints, cx) - }); - cx.foreground().run_until_parked(); - editor.update(cx, |editor, cx| { - assert!( - cached_hint_labels(editor).is_empty(), - "Should clear hints after 2nd toggle" - ); - assert!(visible_hint_labels(editor, cx).is_empty()); - assert_eq!(editor.inlay_hint_cache().version, 2); - }); - - update_test_language_settings(cx, |settings| { - settings.defaults.inlay_hints = Some(InlayHintSettings { - enabled: true, - show_type_hints: true, - show_parameter_hints: true, - show_other_hints: true, - }) - }); - cx.foreground().run_until_parked(); - editor.update(cx, |editor, cx| { - let expected_hints = vec!["2".to_string()]; - assert_eq!( - expected_hints, - cached_hint_labels(editor), - "Should query LSP hints for the 2nd time after enabling hints in settings" - ); - assert_eq!(expected_hints, visible_hint_labels(editor, cx)); - assert_eq!(editor.inlay_hint_cache().version, 3); - }); - - editor.update(cx, |editor, cx| { - editor.toggle_inlay_hints(&crate::ToggleInlayHints, cx) - }); - cx.foreground().run_until_parked(); - editor.update(cx, |editor, cx| { - assert!( - cached_hint_labels(editor).is_empty(), - "Should clear hints after enabling in settings and a 3rd toggle" - ); - assert!(visible_hint_labels(editor, cx).is_empty()); - assert_eq!(editor.inlay_hint_cache().version, 4); - }); - - editor.update(cx, |editor, cx| { - editor.toggle_inlay_hints(&crate::ToggleInlayHints, cx) - }); - cx.foreground().run_until_parked(); - editor.update(cx, |editor, cx| { - let expected_hints = vec!["3".to_string()]; - assert_eq!( - expected_hints, - cached_hint_labels(editor), - "Should query LSP hints for the 3rd time after enabling hints in settings and toggling them back on" - ); - assert_eq!(expected_hints, visible_hint_labels(editor, cx)); - assert_eq!(editor.inlay_hint_cache().version, 5); - }); - } - - pub(crate) fn init_test(cx: &mut TestAppContext, f: impl Fn(&mut AllLanguageSettingsContent)) { - cx.foreground().forbid_parking(); - - cx.update(|cx| { - cx.set_global(SettingsStore::test(cx)); - theme::init(cx); - client::init_settings(cx); - language::init(cx); - Project::init_settings(cx); - workspace::init_settings(cx); - crate::init(cx); - }); - - update_test_language_settings(cx, f); - } - - async fn prepare_test_objects( - cx: &mut TestAppContext, - ) -> (&'static str, ViewHandle, FakeLanguageServer) { - 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 fs = FakeFs::new(cx.background()); - fs.insert_tree( - "/a", - json!({ - "main.rs": "fn main() { a } // and some long comment to ensure inlays are not trimmed out", - "other.rs": "// Test file", - }), - ) - .await; - - let project = Project::test(fs, ["/a".as_ref()], cx).await; - project.update(cx, |project, _| project.languages().add(Arc::new(language))); - let workspace = cx - .add_window(|cx| Workspace::test_new(project.clone(), cx)) - .root(cx); - let worktree_id = workspace.update(cx, |workspace, cx| { - workspace.project().read_with(cx, |project, cx| { - project.worktrees(cx).next().unwrap().read(cx).id() - }) - }); - - let _buffer = project - .update(cx, |project, cx| { - project.open_local_buffer("/a/main.rs", cx) - }) - .await - .unwrap(); - cx.foreground().run_until_parked(); - cx.foreground().start_waiting(); - let fake_server = fake_servers.next().await.unwrap(); - let editor = workspace - .update(cx, |workspace, cx| { - workspace.open_path((worktree_id, "main.rs"), None, true, cx) - }) - .await - .unwrap() - .downcast::() - .unwrap(); - - editor.update(cx, |editor, cx| { - assert!(cached_hint_labels(editor).is_empty()); - assert!(visible_hint_labels(editor, cx).is_empty()); - assert_eq!(editor.inlay_hint_cache().version, 0); - }); - - ("/a/main.rs", editor, fake_server) - } - - pub fn cached_hint_labels(editor: &Editor) -> Vec { - let mut labels = Vec::new(); - for (_, excerpt_hints) in &editor.inlay_hint_cache().hints { - let excerpt_hints = excerpt_hints.read(); - for id in &excerpt_hints.ordered_hints { - labels.push(excerpt_hints.hints_by_id[id].text()); - } - } - - labels.sort(); - labels - } - - pub fn visible_hint_labels(editor: &Editor, cx: &ViewContext<'_, '_, Editor>) -> Vec { - let mut hints = editor - .visible_inlay_hints(cx) - .into_iter() - .map(|hint| hint.text.to_string()) - .collect::>(); - hints.sort(); - hints - } -} +// #[cfg(test)] +// pub mod tests { +// use std::sync::atomic::{AtomicBool, AtomicU32, AtomicUsize, Ordering}; + +// use crate::{ +// scroll::{autoscroll::Autoscroll, scroll_amount::ScrollAmount}, +// serde_json::json, +// ExcerptRange, +// }; +// use futures::StreamExt; +// use gpui::{executor::Deterministic, TestAppContext, View}; +// use itertools::Itertools; +// use language::{ +// language_settings::AllLanguageSettingsContent, FakeLspAdapter, Language, LanguageConfig, +// }; +// use lsp::FakeLanguageServer; +// use parking_lot::Mutex; +// use project::{FakeFs, Project}; +// use settings::SettingsStore; +// use text::{Point, ToPoint}; +// use workspace::Workspace; + +// use crate::editor_tests::update_test_language_settings; + +// use super::*; + +// #[gpui::test] +// async fn test_basic_cache_update_with_duplicate_hints(cx: &mut gpui::TestAppContext) { +// let allowed_hint_kinds = HashSet::from_iter([None, Some(InlayHintKind::Type)]); +// init_test(cx, |settings| { +// settings.defaults.inlay_hints = Some(InlayHintSettings { +// enabled: true, +// show_type_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Type)), +// show_parameter_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Parameter)), +// show_other_hints: allowed_hint_kinds.contains(&None), +// }) +// }); + +// let (file_with_hints, editor, fake_server) = prepare_test_objects(cx).await; +// let lsp_request_count = Arc::new(AtomicU32::new(0)); +// fake_server +// .handle_request::(move |params, _| { +// let task_lsp_request_count = Arc::clone(&lsp_request_count); +// async move { +// assert_eq!( +// params.text_document.uri, +// lsp::Url::from_file_path(file_with_hints).unwrap(), +// ); +// let current_call_id = +// Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst); +// let mut new_hints = Vec::with_capacity(2 * current_call_id as usize); +// for _ in 0..2 { +// let mut i = current_call_id; +// loop { +// new_hints.push(lsp::InlayHint { +// position: lsp::Position::new(0, i), +// label: lsp::InlayHintLabel::String(i.to_string()), +// kind: None, +// text_edits: None, +// tooltip: None, +// padding_left: None, +// padding_right: None, +// data: None, +// }); +// if i == 0 { +// break; +// } +// i -= 1; +// } +// } + +// Ok(Some(new_hints)) +// } +// }) +// .next() +// .await; +// cx.foreground().run_until_parked(); + +// let mut edits_made = 1; +// editor.update(cx, |editor, cx| { +// let expected_hints = vec!["0".to_string()]; +// assert_eq!( +// expected_hints, +// cached_hint_labels(editor), +// "Should get its first hints when opening the editor" +// ); +// assert_eq!(expected_hints, visible_hint_labels(editor, cx)); +// let inlay_cache = editor.inlay_hint_cache(); +// assert_eq!( +// inlay_cache.allowed_hint_kinds, allowed_hint_kinds, +// "Cache should use editor settings to get the allowed hint kinds" +// ); +// assert_eq!( +// inlay_cache.version, edits_made, +// "The editor update the cache version after every cache/view change" +// ); +// }); + +// editor.update(cx, |editor, cx| { +// editor.change_selections(None, cx, |s| s.select_ranges([13..13])); +// editor.handle_input("some change", cx); +// edits_made += 1; +// }); +// cx.foreground().run_until_parked(); +// editor.update(cx, |editor, cx| { +// let expected_hints = vec!["0".to_string(), "1".to_string()]; +// assert_eq!( +// expected_hints, +// cached_hint_labels(editor), +// "Should get new hints after an edit" +// ); +// assert_eq!(expected_hints, visible_hint_labels(editor, cx)); +// let inlay_cache = editor.inlay_hint_cache(); +// assert_eq!( +// inlay_cache.allowed_hint_kinds, allowed_hint_kinds, +// "Cache should use editor settings to get the allowed hint kinds" +// ); +// assert_eq!( +// inlay_cache.version, edits_made, +// "The editor update the cache version after every cache/view change" +// ); +// }); + +// fake_server +// .request::(()) +// .await +// .expect("inlay refresh request failed"); +// edits_made += 1; +// cx.foreground().run_until_parked(); +// editor.update(cx, |editor, cx| { +// let expected_hints = vec!["0".to_string(), "1".to_string(), "2".to_string()]; +// assert_eq!( +// expected_hints, +// cached_hint_labels(editor), +// "Should get new hints after hint refresh/ request" +// ); +// assert_eq!(expected_hints, visible_hint_labels(editor, cx)); +// let inlay_cache = editor.inlay_hint_cache(); +// assert_eq!( +// inlay_cache.allowed_hint_kinds, allowed_hint_kinds, +// "Cache should use editor settings to get the allowed hint kinds" +// ); +// assert_eq!( +// inlay_cache.version, edits_made, +// "The editor update the cache version after every cache/view change" +// ); +// }); +// } + +// #[gpui::test] +// async fn test_cache_update_on_lsp_completion_tasks(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 (file_with_hints, editor, fake_server) = prepare_test_objects(cx).await; +// let lsp_request_count = Arc::new(AtomicU32::new(0)); +// fake_server +// .handle_request::(move |params, _| { +// let task_lsp_request_count = Arc::clone(&lsp_request_count); +// async move { +// assert_eq!( +// params.text_document.uri, +// lsp::Url::from_file_path(file_with_hints).unwrap(), +// ); +// let current_call_id = +// Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst); +// Ok(Some(vec![lsp::InlayHint { +// position: lsp::Position::new(0, current_call_id), +// label: lsp::InlayHintLabel::String(current_call_id.to_string()), +// kind: None, +// text_edits: None, +// tooltip: None, +// padding_left: None, +// padding_right: None, +// data: None, +// }])) +// } +// }) +// .next() +// .await; +// cx.foreground().run_until_parked(); + +// let mut edits_made = 1; +// editor.update(cx, |editor, cx| { +// let expected_hints = vec!["0".to_string()]; +// assert_eq!( +// expected_hints, +// cached_hint_labels(editor), +// "Should get its first hints when opening the editor" +// ); +// assert_eq!(expected_hints, visible_hint_labels(editor, cx)); +// assert_eq!( +// editor.inlay_hint_cache().version, +// edits_made, +// "The editor update the cache version after every cache/view change" +// ); +// }); + +// let progress_token = "test_progress_token"; +// fake_server +// .request::(lsp::WorkDoneProgressCreateParams { +// token: lsp::ProgressToken::String(progress_token.to_string()), +// }) +// .await +// .expect("work done progress create request failed"); +// cx.foreground().run_until_parked(); +// fake_server.notify::(lsp::ProgressParams { +// token: lsp::ProgressToken::String(progress_token.to_string()), +// value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Begin( +// lsp::WorkDoneProgressBegin::default(), +// )), +// }); +// cx.foreground().run_until_parked(); + +// editor.update(cx, |editor, cx| { +// let expected_hints = vec!["0".to_string()]; +// assert_eq!( +// expected_hints, +// cached_hint_labels(editor), +// "Should not update hints while the work task is running" +// ); +// assert_eq!(expected_hints, visible_hint_labels(editor, cx)); +// assert_eq!( +// editor.inlay_hint_cache().version, +// edits_made, +// "Should not update the cache while the work task is running" +// ); +// }); + +// fake_server.notify::(lsp::ProgressParams { +// token: lsp::ProgressToken::String(progress_token.to_string()), +// value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::End( +// lsp::WorkDoneProgressEnd::default(), +// )), +// }); +// cx.foreground().run_until_parked(); + +// edits_made += 1; +// editor.update(cx, |editor, cx| { +// let expected_hints = vec!["1".to_string()]; +// assert_eq!( +// expected_hints, +// cached_hint_labels(editor), +// "New hints should be queried after the work task is done" +// ); +// assert_eq!(expected_hints, visible_hint_labels(editor, cx)); +// assert_eq!( +// editor.inlay_hint_cache().version, +// edits_made, +// "Cache version should udpate once after the work task is done" +// ); +// }); +// } + +// #[gpui::test] +// async fn test_no_hint_updates_for_unrelated_language_files(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 fs = FakeFs::new(cx.background()); +// fs.insert_tree( +// "/a", +// json!({ +// "main.rs": "fn main() { a } // and some long comment to ensure inlays are not trimmed out", +// "other.md": "Test md file with some text", +// }), +// ) +// .await; +// let project = Project::test(fs, ["/a".as_ref()], cx).await; +// let workspace = cx +// .add_window(|cx| Workspace::test_new(project.clone(), cx)) +// .root(cx); +// let worktree_id = workspace.update(cx, |workspace, cx| { +// workspace.project().read_with(cx, |project, cx| { +// project.worktrees(cx).next().unwrap().read(cx).id() +// }) +// }); + +// let mut rs_fake_servers = None; +// let mut md_fake_servers = None; +// for (name, path_suffix) in [("Rust", "rs"), ("Markdown", "md")] { +// let mut language = Language::new( +// LanguageConfig { +// name: name.into(), +// path_suffixes: vec![path_suffix.to_string()], +// ..Default::default() +// }, +// Some(tree_sitter_rust::language()), +// ); +// let fake_servers = language +// .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { +// name, +// capabilities: lsp::ServerCapabilities { +// inlay_hint_provider: Some(lsp::OneOf::Left(true)), +// ..Default::default() +// }, +// ..Default::default() +// })) +// .await; +// match name { +// "Rust" => rs_fake_servers = Some(fake_servers), +// "Markdown" => md_fake_servers = Some(fake_servers), +// _ => unreachable!(), +// } +// project.update(cx, |project, _| { +// project.languages().add(Arc::new(language)); +// }); +// } + +// let _rs_buffer = project +// .update(cx, |project, cx| { +// project.open_local_buffer("/a/main.rs", cx) +// }) +// .await +// .unwrap(); +// cx.foreground().run_until_parked(); +// cx.foreground().start_waiting(); +// let rs_fake_server = rs_fake_servers.unwrap().next().await.unwrap(); +// let rs_editor = workspace +// .update(cx, |workspace, cx| { +// workspace.open_path((worktree_id, "main.rs"), None, true, cx) +// }) +// .await +// .unwrap() +// .downcast::() +// .unwrap(); +// let rs_lsp_request_count = Arc::new(AtomicU32::new(0)); +// rs_fake_server +// .handle_request::(move |params, _| { +// let task_lsp_request_count = Arc::clone(&rs_lsp_request_count); +// async move { +// assert_eq!( +// params.text_document.uri, +// lsp::Url::from_file_path("/a/main.rs").unwrap(), +// ); +// let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst); +// Ok(Some(vec![lsp::InlayHint { +// position: lsp::Position::new(0, i), +// label: lsp::InlayHintLabel::String(i.to_string()), +// kind: None, +// text_edits: None, +// tooltip: None, +// padding_left: None, +// padding_right: None, +// data: None, +// }])) +// } +// }) +// .next() +// .await; +// cx.foreground().run_until_parked(); +// rs_editor.update(cx, |editor, cx| { +// let expected_hints = vec!["0".to_string()]; +// assert_eq!( +// expected_hints, +// cached_hint_labels(editor), +// "Should get its first hints when opening the editor" +// ); +// assert_eq!(expected_hints, visible_hint_labels(editor, cx)); +// assert_eq!( +// editor.inlay_hint_cache().version, +// 1, +// "Rust editor update the cache version after every cache/view change" +// ); +// }); + +// cx.foreground().run_until_parked(); +// let _md_buffer = project +// .update(cx, |project, cx| { +// project.open_local_buffer("/a/other.md", cx) +// }) +// .await +// .unwrap(); +// cx.foreground().run_until_parked(); +// cx.foreground().start_waiting(); +// let md_fake_server = md_fake_servers.unwrap().next().await.unwrap(); +// let md_editor = workspace +// .update(cx, |workspace, cx| { +// workspace.open_path((worktree_id, "other.md"), None, true, cx) +// }) +// .await +// .unwrap() +// .downcast::() +// .unwrap(); +// let md_lsp_request_count = Arc::new(AtomicU32::new(0)); +// md_fake_server +// .handle_request::(move |params, _| { +// let task_lsp_request_count = Arc::clone(&md_lsp_request_count); +// async move { +// assert_eq!( +// params.text_document.uri, +// lsp::Url::from_file_path("/a/other.md").unwrap(), +// ); +// let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst); +// Ok(Some(vec![lsp::InlayHint { +// position: lsp::Position::new(0, i), +// label: lsp::InlayHintLabel::String(i.to_string()), +// kind: None, +// text_edits: None, +// tooltip: None, +// padding_left: None, +// padding_right: None, +// data: None, +// }])) +// } +// }) +// .next() +// .await; +// cx.foreground().run_until_parked(); +// md_editor.update(cx, |editor, cx| { +// let expected_hints = vec!["0".to_string()]; +// assert_eq!( +// expected_hints, +// cached_hint_labels(editor), +// "Markdown editor should have a separate verison, repeating Rust editor rules" +// ); +// assert_eq!(expected_hints, visible_hint_labels(editor, cx)); +// assert_eq!(editor.inlay_hint_cache().version, 1); +// }); + +// rs_editor.update(cx, |editor, cx| { +// editor.change_selections(None, cx, |s| s.select_ranges([13..13])); +// editor.handle_input("some rs change", cx); +// }); +// cx.foreground().run_until_parked(); +// rs_editor.update(cx, |editor, cx| { +// let expected_hints = vec!["1".to_string()]; +// assert_eq!( +// expected_hints, +// cached_hint_labels(editor), +// "Rust inlay cache should change after the edit" +// ); +// assert_eq!(expected_hints, visible_hint_labels(editor, cx)); +// assert_eq!( +// editor.inlay_hint_cache().version, +// 2, +// "Every time hint cache changes, cache version should be incremented" +// ); +// }); +// md_editor.update(cx, |editor, cx| { +// let expected_hints = vec!["0".to_string()]; +// assert_eq!( +// expected_hints, +// cached_hint_labels(editor), +// "Markdown editor should not be affected by Rust editor changes" +// ); +// assert_eq!(expected_hints, visible_hint_labels(editor, cx)); +// assert_eq!(editor.inlay_hint_cache().version, 1); +// }); + +// md_editor.update(cx, |editor, cx| { +// editor.change_selections(None, cx, |s| s.select_ranges([13..13])); +// editor.handle_input("some md change", cx); +// }); +// cx.foreground().run_until_parked(); +// md_editor.update(cx, |editor, cx| { +// let expected_hints = vec!["1".to_string()]; +// assert_eq!( +// expected_hints, +// cached_hint_labels(editor), +// "Rust editor should not be affected by Markdown editor changes" +// ); +// assert_eq!(expected_hints, visible_hint_labels(editor, cx)); +// assert_eq!(editor.inlay_hint_cache().version, 2); +// }); +// rs_editor.update(cx, |editor, cx| { +// let expected_hints = vec!["1".to_string()]; +// assert_eq!( +// expected_hints, +// cached_hint_labels(editor), +// "Markdown editor should also change independently" +// ); +// assert_eq!(expected_hints, visible_hint_labels(editor, cx)); +// assert_eq!(editor.inlay_hint_cache().version, 2); +// }); +// } + +// #[gpui::test] +// async fn test_hint_setting_changes(cx: &mut gpui::TestAppContext) { +// let allowed_hint_kinds = HashSet::from_iter([None, Some(InlayHintKind::Type)]); +// init_test(cx, |settings| { +// settings.defaults.inlay_hints = Some(InlayHintSettings { +// enabled: true, +// show_type_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Type)), +// show_parameter_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Parameter)), +// show_other_hints: allowed_hint_kinds.contains(&None), +// }) +// }); + +// let (file_with_hints, editor, fake_server) = prepare_test_objects(cx).await; +// let lsp_request_count = Arc::new(AtomicU32::new(0)); +// let another_lsp_request_count = Arc::clone(&lsp_request_count); +// fake_server +// .handle_request::(move |params, _| { +// let task_lsp_request_count = Arc::clone(&another_lsp_request_count); +// async move { +// Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst); +// assert_eq!( +// params.text_document.uri, +// lsp::Url::from_file_path(file_with_hints).unwrap(), +// ); +// Ok(Some(vec![ +// lsp::InlayHint { +// position: lsp::Position::new(0, 1), +// label: lsp::InlayHintLabel::String("type hint".to_string()), +// kind: Some(lsp::InlayHintKind::TYPE), +// text_edits: None, +// tooltip: None, +// padding_left: None, +// padding_right: None, +// data: None, +// }, +// lsp::InlayHint { +// position: lsp::Position::new(0, 2), +// label: lsp::InlayHintLabel::String("parameter hint".to_string()), +// kind: Some(lsp::InlayHintKind::PARAMETER), +// text_edits: None, +// tooltip: None, +// padding_left: None, +// padding_right: None, +// data: None, +// }, +// lsp::InlayHint { +// position: lsp::Position::new(0, 3), +// label: lsp::InlayHintLabel::String("other hint".to_string()), +// kind: None, +// text_edits: None, +// tooltip: None, +// padding_left: None, +// padding_right: None, +// data: None, +// }, +// ])) +// } +// }) +// .next() +// .await; +// cx.foreground().run_until_parked(); + +// let mut edits_made = 1; +// editor.update(cx, |editor, cx| { +// assert_eq!( +// lsp_request_count.load(Ordering::Relaxed), +// 1, +// "Should query new hints once" +// ); +// assert_eq!( +// vec![ +// "other hint".to_string(), +// "parameter hint".to_string(), +// "type hint".to_string(), +// ], +// cached_hint_labels(editor), +// "Should get its first hints when opening the editor" +// ); +// assert_eq!( +// vec!["other hint".to_string(), "type hint".to_string()], +// visible_hint_labels(editor, cx) +// ); +// let inlay_cache = editor.inlay_hint_cache(); +// assert_eq!( +// inlay_cache.allowed_hint_kinds, allowed_hint_kinds, +// "Cache should use editor settings to get the allowed hint kinds" +// ); +// assert_eq!( +// inlay_cache.version, edits_made, +// "The editor update the cache version after every cache/view change" +// ); +// }); + +// fake_server +// .request::(()) +// .await +// .expect("inlay refresh request failed"); +// cx.foreground().run_until_parked(); +// editor.update(cx, |editor, cx| { +// assert_eq!( +// lsp_request_count.load(Ordering::Relaxed), +// 2, +// "Should load new hints twice" +// ); +// assert_eq!( +// vec![ +// "other hint".to_string(), +// "parameter hint".to_string(), +// "type hint".to_string(), +// ], +// cached_hint_labels(editor), +// "Cached hints should not change due to allowed hint kinds settings update" +// ); +// assert_eq!( +// vec!["other hint".to_string(), "type hint".to_string()], +// visible_hint_labels(editor, cx) +// ); +// assert_eq!( +// editor.inlay_hint_cache().version, +// edits_made, +// "Should not update cache version due to new loaded hints being the same" +// ); +// }); + +// for (new_allowed_hint_kinds, expected_visible_hints) in [ +// (HashSet::from_iter([None]), vec!["other hint".to_string()]), +// ( +// HashSet::from_iter([Some(InlayHintKind::Type)]), +// vec!["type hint".to_string()], +// ), +// ( +// HashSet::from_iter([Some(InlayHintKind::Parameter)]), +// vec!["parameter hint".to_string()], +// ), +// ( +// HashSet::from_iter([None, Some(InlayHintKind::Type)]), +// vec!["other hint".to_string(), "type hint".to_string()], +// ), +// ( +// HashSet::from_iter([None, Some(InlayHintKind::Parameter)]), +// vec!["other hint".to_string(), "parameter hint".to_string()], +// ), +// ( +// HashSet::from_iter([Some(InlayHintKind::Type), Some(InlayHintKind::Parameter)]), +// vec!["parameter hint".to_string(), "type hint".to_string()], +// ), +// ( +// HashSet::from_iter([ +// None, +// Some(InlayHintKind::Type), +// Some(InlayHintKind::Parameter), +// ]), +// vec![ +// "other hint".to_string(), +// "parameter hint".to_string(), +// "type hint".to_string(), +// ], +// ), +// ] { +// edits_made += 1; +// update_test_language_settings(cx, |settings| { +// settings.defaults.inlay_hints = Some(InlayHintSettings { +// enabled: true, +// show_type_hints: new_allowed_hint_kinds.contains(&Some(InlayHintKind::Type)), +// show_parameter_hints: new_allowed_hint_kinds +// .contains(&Some(InlayHintKind::Parameter)), +// show_other_hints: new_allowed_hint_kinds.contains(&None), +// }) +// }); +// cx.foreground().run_until_parked(); +// editor.update(cx, |editor, cx| { +// assert_eq!( +// lsp_request_count.load(Ordering::Relaxed), +// 2, +// "Should not load new hints on allowed hint kinds change for hint kinds {new_allowed_hint_kinds:?}" +// ); +// assert_eq!( +// vec![ +// "other hint".to_string(), +// "parameter hint".to_string(), +// "type hint".to_string(), +// ], +// cached_hint_labels(editor), +// "Should get its cached hints unchanged after the settings change for hint kinds {new_allowed_hint_kinds:?}" +// ); +// assert_eq!( +// expected_visible_hints, +// visible_hint_labels(editor, cx), +// "Should get its visible hints filtered after the settings change for hint kinds {new_allowed_hint_kinds:?}" +// ); +// let inlay_cache = editor.inlay_hint_cache(); +// assert_eq!( +// inlay_cache.allowed_hint_kinds, new_allowed_hint_kinds, +// "Cache should use editor settings to get the allowed hint kinds for hint kinds {new_allowed_hint_kinds:?}" +// ); +// assert_eq!( +// inlay_cache.version, edits_made, +// "The editor should update the cache version after every cache/view change for hint kinds {new_allowed_hint_kinds:?} due to visible hints change" +// ); +// }); +// } + +// edits_made += 1; +// let another_allowed_hint_kinds = HashSet::from_iter([Some(InlayHintKind::Type)]); +// update_test_language_settings(cx, |settings| { +// settings.defaults.inlay_hints = Some(InlayHintSettings { +// enabled: false, +// show_type_hints: another_allowed_hint_kinds.contains(&Some(InlayHintKind::Type)), +// show_parameter_hints: another_allowed_hint_kinds +// .contains(&Some(InlayHintKind::Parameter)), +// show_other_hints: another_allowed_hint_kinds.contains(&None), +// }) +// }); +// cx.foreground().run_until_parked(); +// editor.update(cx, |editor, cx| { +// assert_eq!( +// lsp_request_count.load(Ordering::Relaxed), +// 2, +// "Should not load new hints when hints got disabled" +// ); +// assert!( +// cached_hint_labels(editor).is_empty(), +// "Should clear the cache when hints got disabled" +// ); +// assert!( +// visible_hint_labels(editor, cx).is_empty(), +// "Should clear visible hints when hints got disabled" +// ); +// let inlay_cache = editor.inlay_hint_cache(); +// assert_eq!( +// inlay_cache.allowed_hint_kinds, another_allowed_hint_kinds, +// "Should update its allowed hint kinds even when hints got disabled" +// ); +// assert_eq!( +// inlay_cache.version, edits_made, +// "The editor should update the cache version after hints got disabled" +// ); +// }); + +// fake_server +// .request::(()) +// .await +// .expect("inlay refresh request failed"); +// cx.foreground().run_until_parked(); +// editor.update(cx, |editor, cx| { +// assert_eq!( +// lsp_request_count.load(Ordering::Relaxed), +// 2, +// "Should not load new hints when they got disabled" +// ); +// assert!(cached_hint_labels(editor).is_empty()); +// assert!(visible_hint_labels(editor, cx).is_empty()); +// assert_eq!( +// editor.inlay_hint_cache().version, edits_made, +// "The editor should not update the cache version after /refresh query without updates" +// ); +// }); + +// let final_allowed_hint_kinds = HashSet::from_iter([Some(InlayHintKind::Parameter)]); +// edits_made += 1; +// update_test_language_settings(cx, |settings| { +// settings.defaults.inlay_hints = Some(InlayHintSettings { +// enabled: true, +// show_type_hints: final_allowed_hint_kinds.contains(&Some(InlayHintKind::Type)), +// show_parameter_hints: final_allowed_hint_kinds +// .contains(&Some(InlayHintKind::Parameter)), +// show_other_hints: final_allowed_hint_kinds.contains(&None), +// }) +// }); +// cx.foreground().run_until_parked(); +// editor.update(cx, |editor, cx| { +// assert_eq!( +// lsp_request_count.load(Ordering::Relaxed), +// 3, +// "Should query for new hints when they got reenabled" +// ); +// assert_eq!( +// vec![ +// "other hint".to_string(), +// "parameter hint".to_string(), +// "type hint".to_string(), +// ], +// cached_hint_labels(editor), +// "Should get its cached hints fully repopulated after the hints got reenabled" +// ); +// assert_eq!( +// vec!["parameter hint".to_string()], +// visible_hint_labels(editor, cx), +// "Should get its visible hints repopulated and filtered after the h" +// ); +// let inlay_cache = editor.inlay_hint_cache(); +// assert_eq!( +// inlay_cache.allowed_hint_kinds, final_allowed_hint_kinds, +// "Cache should update editor settings when hints got reenabled" +// ); +// assert_eq!( +// inlay_cache.version, edits_made, +// "Cache should update its version after hints got reenabled" +// ); +// }); + +// fake_server +// .request::(()) +// .await +// .expect("inlay refresh request failed"); +// cx.foreground().run_until_parked(); +// editor.update(cx, |editor, cx| { +// assert_eq!( +// lsp_request_count.load(Ordering::Relaxed), +// 4, +// "Should query for new hints again" +// ); +// assert_eq!( +// vec![ +// "other hint".to_string(), +// "parameter hint".to_string(), +// "type hint".to_string(), +// ], +// cached_hint_labels(editor), +// ); +// assert_eq!( +// vec!["parameter hint".to_string()], +// visible_hint_labels(editor, cx), +// ); +// assert_eq!(editor.inlay_hint_cache().version, edits_made); +// }); +// } + +// #[gpui::test] +// async fn test_hint_request_cancellation(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 (file_with_hints, editor, fake_server) = prepare_test_objects(cx).await; +// let fake_server = Arc::new(fake_server); +// let lsp_request_count = Arc::new(AtomicU32::new(0)); +// let another_lsp_request_count = Arc::clone(&lsp_request_count); +// fake_server +// .handle_request::(move |params, _| { +// let task_lsp_request_count = Arc::clone(&another_lsp_request_count); +// async move { +// let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst) + 1; +// assert_eq!( +// params.text_document.uri, +// lsp::Url::from_file_path(file_with_hints).unwrap(), +// ); +// Ok(Some(vec![lsp::InlayHint { +// position: lsp::Position::new(0, i), +// label: lsp::InlayHintLabel::String(i.to_string()), +// kind: None, +// text_edits: None, +// tooltip: None, +// padding_left: None, +// padding_right: None, +// data: None, +// }])) +// } +// }) +// .next() +// .await; + +// let mut expected_changes = Vec::new(); +// for change_after_opening in [ +// "initial change #1", +// "initial change #2", +// "initial change #3", +// ] { +// editor.update(cx, |editor, cx| { +// editor.change_selections(None, cx, |s| s.select_ranges([13..13])); +// editor.handle_input(change_after_opening, cx); +// }); +// expected_changes.push(change_after_opening); +// } + +// cx.foreground().run_until_parked(); + +// editor.update(cx, |editor, cx| { +// let current_text = editor.text(cx); +// for change in &expected_changes { +// assert!( +// current_text.contains(change), +// "Should apply all changes made" +// ); +// } +// assert_eq!( +// lsp_request_count.load(Ordering::Relaxed), +// 2, +// "Should query new hints twice: for editor init and for the last edit that interrupted all others" +// ); +// let expected_hints = vec!["2".to_string()]; +// assert_eq!( +// expected_hints, +// cached_hint_labels(editor), +// "Should get hints from the last edit landed only" +// ); +// assert_eq!(expected_hints, visible_hint_labels(editor, cx)); +// assert_eq!( +// editor.inlay_hint_cache().version, 1, +// "Only one update should be registered in the cache after all cancellations" +// ); +// }); + +// let mut edits = Vec::new(); +// for async_later_change in [ +// "another change #1", +// "another change #2", +// "another change #3", +// ] { +// expected_changes.push(async_later_change); +// let task_editor = editor.clone(); +// let mut task_cx = cx.clone(); +// edits.push(cx.foreground().spawn(async move { +// task_editor.update(&mut task_cx, |editor, cx| { +// editor.change_selections(None, cx, |s| s.select_ranges([13..13])); +// editor.handle_input(async_later_change, cx); +// }); +// })); +// } +// let _ = future::join_all(edits).await; +// cx.foreground().run_until_parked(); + +// editor.update(cx, |editor, cx| { +// let current_text = editor.text(cx); +// for change in &expected_changes { +// assert!( +// current_text.contains(change), +// "Should apply all changes made" +// ); +// } +// assert_eq!( +// lsp_request_count.load(Ordering::SeqCst), +// 3, +// "Should query new hints one more time, for the last edit only" +// ); +// let expected_hints = vec!["3".to_string()]; +// assert_eq!( +// expected_hints, +// cached_hint_labels(editor), +// "Should get hints from the last edit landed only" +// ); +// assert_eq!(expected_hints, visible_hint_labels(editor, cx)); +// assert_eq!( +// editor.inlay_hint_cache().version, +// 2, +// "Should update the cache version once more, for the new change" +// ); +// }); +// } + +// #[gpui::test(iterations = 10)] +// async fn test_large_buffer_inlay_requests_split(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 fs = FakeFs::new(cx.background()); +// fs.insert_tree( +// "/a", +// json!({ +// "main.rs": format!("fn main() {{\n{}\n}}", "let i = 5;\n".repeat(500)), +// "other.rs": "// Test file", +// }), +// ) +// .await; +// let project = Project::test(fs, ["/a".as_ref()], cx).await; +// project.update(cx, |project, _| project.languages().add(Arc::new(language))); +// let workspace = cx +// .add_window(|cx| Workspace::test_new(project.clone(), cx)) +// .root(cx); +// let worktree_id = workspace.update(cx, |workspace, cx| { +// workspace.project().read_with(cx, |project, cx| { +// project.worktrees(cx).next().unwrap().read(cx).id() +// }) +// }); + +// let _buffer = project +// .update(cx, |project, cx| { +// project.open_local_buffer("/a/main.rs", cx) +// }) +// .await +// .unwrap(); +// cx.foreground().run_until_parked(); +// cx.foreground().start_waiting(); +// let fake_server = fake_servers.next().await.unwrap(); +// let editor = workspace +// .update(cx, |workspace, cx| { +// workspace.open_path((worktree_id, "main.rs"), None, true, cx) +// }) +// .await +// .unwrap() +// .downcast::() +// .unwrap(); +// let lsp_request_ranges = Arc::new(Mutex::new(Vec::new())); +// let lsp_request_count = Arc::new(AtomicUsize::new(0)); +// let closure_lsp_request_ranges = Arc::clone(&lsp_request_ranges); +// let closure_lsp_request_count = Arc::clone(&lsp_request_count); +// fake_server +// .handle_request::(move |params, _| { +// let task_lsp_request_ranges = Arc::clone(&closure_lsp_request_ranges); +// let task_lsp_request_count = Arc::clone(&closure_lsp_request_count); +// async move { +// assert_eq!( +// params.text_document.uri, +// lsp::Url::from_file_path("/a/main.rs").unwrap(), +// ); + +// task_lsp_request_ranges.lock().push(params.range); +// let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::Release) + 1; +// Ok(Some(vec![lsp::InlayHint { +// position: params.range.end, +// label: lsp::InlayHintLabel::String(i.to_string()), +// kind: None, +// text_edits: None, +// tooltip: None, +// padding_left: None, +// padding_right: None, +// data: None, +// }])) +// } +// }) +// .next() +// .await; +// fn editor_visible_range( +// editor: &ViewHandle, +// cx: &mut gpui::TestAppContext, +// ) -> Range { +// let ranges = editor.update(cx, |editor, cx| editor.excerpt_visible_offsets(None, cx)); +// assert_eq!( +// ranges.len(), +// 1, +// "Single buffer should produce a single excerpt with visible range" +// ); +// let (_, (excerpt_buffer, _, excerpt_visible_range)) = +// ranges.into_iter().next().unwrap(); +// excerpt_buffer.update(cx, |buffer, _| { +// let snapshot = buffer.snapshot(); +// let start = buffer +// .anchor_before(excerpt_visible_range.start) +// .to_point(&snapshot); +// let end = buffer +// .anchor_after(excerpt_visible_range.end) +// .to_point(&snapshot); +// start..end +// }) +// } + +// // in large buffers, requests are made for more than visible range of a buffer. +// // invisible parts are queried later, to avoid excessive requests on quick typing. +// // wait the timeout needed to get all requests. +// cx.foreground().advance_clock(Duration::from_millis( +// INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100, +// )); +// cx.foreground().run_until_parked(); +// let initial_visible_range = editor_visible_range(&editor, cx); +// let lsp_initial_visible_range = lsp::Range::new( +// lsp::Position::new( +// initial_visible_range.start.row, +// initial_visible_range.start.column, +// ), +// lsp::Position::new( +// initial_visible_range.end.row, +// initial_visible_range.end.column, +// ), +// ); +// let expected_initial_query_range_end = +// lsp::Position::new(initial_visible_range.end.row * 2, 2); +// let mut expected_invisible_query_start = lsp_initial_visible_range.end; +// expected_invisible_query_start.character += 1; +// editor.update(cx, |editor, cx| { +// let ranges = lsp_request_ranges.lock().drain(..).collect::>(); +// assert_eq!(ranges.len(), 2, +// "When scroll is at the edge of a big document, its visible part and the same range further should be queried in order, but got: {ranges:?}"); +// let visible_query_range = &ranges[0]; +// assert_eq!(visible_query_range.start, lsp_initial_visible_range.start); +// assert_eq!(visible_query_range.end, lsp_initial_visible_range.end); +// let invisible_query_range = &ranges[1]; + +// assert_eq!(invisible_query_range.start, expected_invisible_query_start, "Should initially query visible edge of the document"); +// assert_eq!(invisible_query_range.end, expected_initial_query_range_end, "Should initially query visible edge of the document"); + +// let requests_count = lsp_request_count.load(Ordering::Acquire); +// assert_eq!(requests_count, 2, "Visible + invisible request"); +// let expected_hints = vec!["1".to_string(), "2".to_string()]; +// assert_eq!( +// expected_hints, +// cached_hint_labels(editor), +// "Should have hints from both LSP requests made for a big file" +// ); +// assert_eq!(expected_hints, visible_hint_labels(editor, cx), "Should display only hints from the visible range"); +// assert_eq!( +// editor.inlay_hint_cache().version, requests_count, +// "LSP queries should've bumped the cache version" +// ); +// }); + +// editor.update(cx, |editor, cx| { +// editor.scroll_screen(&ScrollAmount::Page(1.0), cx); +// editor.scroll_screen(&ScrollAmount::Page(1.0), cx); +// }); +// cx.foreground().advance_clock(Duration::from_millis( +// INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100, +// )); +// cx.foreground().run_until_parked(); +// let visible_range_after_scrolls = editor_visible_range(&editor, cx); +// let visible_line_count = +// editor.update(cx, |editor, _| editor.visible_line_count().unwrap()); +// let selection_in_cached_range = editor.update(cx, |editor, cx| { +// let ranges = lsp_request_ranges +// .lock() +// .drain(..) +// .sorted_by_key(|r| r.start) +// .collect::>(); +// assert_eq!( +// ranges.len(), +// 2, +// "Should query 2 ranges after both scrolls, but got: {ranges:?}" +// ); +// let first_scroll = &ranges[0]; +// let second_scroll = &ranges[1]; +// assert_eq!( +// first_scroll.end, second_scroll.start, +// "Should query 2 adjacent ranges after the scrolls, but got: {ranges:?}" +// ); +// assert_eq!( +// first_scroll.start, expected_initial_query_range_end, +// "First scroll should start the query right after the end of the original scroll", +// ); +// assert_eq!( +// second_scroll.end, +// lsp::Position::new( +// visible_range_after_scrolls.end.row +// + visible_line_count.ceil() as u32, +// 1, +// ), +// "Second scroll should query one more screen down after the end of the visible range" +// ); + +// let lsp_requests = lsp_request_count.load(Ordering::Acquire); +// assert_eq!(lsp_requests, 4, "Should query for hints after every scroll"); +// let expected_hints = vec![ +// "1".to_string(), +// "2".to_string(), +// "3".to_string(), +// "4".to_string(), +// ]; +// assert_eq!( +// expected_hints, +// cached_hint_labels(editor), +// "Should have hints from the new LSP response after the edit" +// ); +// assert_eq!(expected_hints, visible_hint_labels(editor, cx)); +// assert_eq!( +// editor.inlay_hint_cache().version, +// lsp_requests, +// "Should update the cache for every LSP response with hints added" +// ); + +// let mut selection_in_cached_range = visible_range_after_scrolls.end; +// selection_in_cached_range.row -= visible_line_count.ceil() as u32; +// selection_in_cached_range +// }); + +// editor.update(cx, |editor, cx| { +// editor.change_selections(Some(Autoscroll::center()), cx, |s| { +// s.select_ranges([selection_in_cached_range..selection_in_cached_range]) +// }); +// }); +// cx.foreground().advance_clock(Duration::from_millis( +// INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100, +// )); +// cx.foreground().run_until_parked(); +// editor.update(cx, |_, _| { +// let ranges = lsp_request_ranges +// .lock() +// .drain(..) +// .sorted_by_key(|r| r.start) +// .collect::>(); +// assert!(ranges.is_empty(), "No new ranges or LSP queries should be made after returning to the selection with cached hints"); +// assert_eq!(lsp_request_count.load(Ordering::Acquire), 4); +// }); + +// editor.update(cx, |editor, cx| { +// editor.handle_input("++++more text++++", cx); +// }); +// cx.foreground().advance_clock(Duration::from_millis( +// INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100, +// )); +// cx.foreground().run_until_parked(); +// editor.update(cx, |editor, cx| { +// let mut ranges = lsp_request_ranges.lock().drain(..).collect::>(); +// ranges.sort_by_key(|r| r.start); + +// assert_eq!(ranges.len(), 3, +// "On edit, should scroll to selection and query a range around it: visible + same range above and below. Instead, got query ranges {ranges:?}"); +// let above_query_range = &ranges[0]; +// let visible_query_range = &ranges[1]; +// let below_query_range = &ranges[2]; +// assert!(above_query_range.end.character < visible_query_range.start.character || above_query_range.end.line + 1 == visible_query_range.start.line, +// "Above range {above_query_range:?} should be before visible range {visible_query_range:?}"); +// assert!(visible_query_range.end.character < below_query_range.start.character || visible_query_range.end.line + 1 == below_query_range.start.line, +// "Visible range {visible_query_range:?} should be before below range {below_query_range:?}"); +// assert!(above_query_range.start.line < selection_in_cached_range.row, +// "Hints should be queried with the selected range after the query range start"); +// assert!(below_query_range.end.line > selection_in_cached_range.row, +// "Hints should be queried with the selected range before the query range end"); +// assert!(above_query_range.start.line <= selection_in_cached_range.row - (visible_line_count * 3.0 / 2.0) as u32, +// "Hints query range should contain one more screen before"); +// assert!(below_query_range.end.line >= selection_in_cached_range.row + (visible_line_count * 3.0 / 2.0) as u32, +// "Hints query range should contain one more screen after"); + +// let lsp_requests = lsp_request_count.load(Ordering::Acquire); +// assert_eq!(lsp_requests, 7, "There should be a visible range and two ranges above and below it queried"); +// let expected_hints = vec!["5".to_string(), "6".to_string(), "7".to_string()]; +// assert_eq!(expected_hints, cached_hint_labels(editor), +// "Should have hints from the new LSP response after the edit"); +// assert_eq!(expected_hints, visible_hint_labels(editor, cx)); +// assert_eq!(editor.inlay_hint_cache().version, lsp_requests, "Should update the cache for every LSP response with hints added"); +// }); +// } + +// #[gpui::test(iterations = 10)] +// async fn test_multiple_excerpts_large_multibuffer( +// deterministic: Arc, +// 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()); +// 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 workspace = cx +// .add_window(|cx| Workspace::test_new(project.clone(), cx)) +// .root(cx); +// let worktree_id = workspace.update(cx, |workspace, cx| { +// workspace.project().read_with(cx, |project, cx| { +// project.worktrees(cx).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.add_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 +// }); + +// deterministic.run_until_parked(); +// cx.foreground().run_until_parked(); +// let editor = cx +// .add_window(|cx| Editor::for_multibuffer(multibuffer, Some(project.clone()), cx)) +// .root(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.foreground().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(), +// ]; +// 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.foreground().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.foreground().advance_clock(Duration::from_millis( +// INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100, +// )); +// cx.foreground().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() +// }); + +// 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.foreground().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.foreground().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( +// deterministic: Arc, +// cx: &mut gpui::TestAppContext, +// ) { +// init_test(cx, |settings| { +// settings.defaults.inlay_hints = Some(InlayHintSettings { +// enabled: true, +// show_type_hints: false, +// show_parameter_hints: false, +// show_other_hints: false, +// }) +// }); + +// 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()); +// 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 workspace = cx +// .add_window(|cx| Workspace::test_new(project.clone(), cx)) +// .root(cx); +// let worktree_id = workspace.update(cx, |workspace, cx| { +// workspace.project().read_with(cx, |project, cx| { +// project.worktrees(cx).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.add_model(|_| MultiBuffer::new(0)); +// let (buffer_1_excerpts, buffer_2_excerpts) = multibuffer.update(cx, |multibuffer, cx| { +// let buffer_1_excerpts = multibuffer.push_excerpts( +// buffer_1.clone(), +// [ExcerptRange { +// context: Point::new(0, 0)..Point::new(2, 0), +// primary: None, +// }], +// cx, +// ); +// let buffer_2_excerpts = multibuffer.push_excerpts( +// buffer_2.clone(), +// [ExcerptRange { +// context: Point::new(0, 1)..Point::new(2, 1), +// primary: None, +// }], +// cx, +// ); +// (buffer_1_excerpts, buffer_2_excerpts) +// }); + +// assert!(!buffer_1_excerpts.is_empty()); +// assert!(!buffer_2_excerpts.is_empty()); + +// deterministic.run_until_parked(); +// cx.foreground().run_until_parked(); +// let editor = cx +// .add_window(|cx| Editor::for_multibuffer(multibuffer, Some(project.clone()), cx)) +// .root(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); +// }; + +// 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.foreground().run_until_parked(); + +// editor.update(cx, |editor, cx| { +// assert_eq!( +// vec!["main hint #0".to_string(), "other hint #0".to_string()], +// cached_hint_labels(editor), +// "Cache should update for both excerpts despite hints display was disabled" +// ); +// assert!( +// visible_hint_labels(editor, cx).is_empty(), +// "All hints are disabled and should not be shown despite being present in the cache" +// ); +// assert_eq!( +// editor.inlay_hint_cache().version, +// 2, +// "Cache should update once per excerpt query" +// ); +// }); + +// editor.update(cx, |editor, cx| { +// editor.buffer().update(cx, |multibuffer, cx| { +// multibuffer.remove_excerpts(buffer_2_excerpts, cx) +// }) +// }); +// cx.foreground().run_until_parked(); +// editor.update(cx, |editor, cx| { +// assert_eq!( +// vec!["main hint #0".to_string()], +// cached_hint_labels(editor), +// "For the removed excerpt, should clean corresponding cached hints" +// ); +// assert!( +// visible_hint_labels(editor, cx).is_empty(), +// "All hints are disabled and should not be shown despite being present in the cache" +// ); +// assert_eq!( +// editor.inlay_hint_cache().version, +// 3, +// "Excerpt removal should trigger a cache update" +// ); +// }); + +// update_test_language_settings(cx, |settings| { +// settings.defaults.inlay_hints = Some(InlayHintSettings { +// enabled: true, +// show_type_hints: true, +// show_parameter_hints: true, +// show_other_hints: true, +// }) +// }); +// cx.foreground().run_until_parked(); +// editor.update(cx, |editor, cx| { +// let expected_hints = vec!["main hint #0".to_string()]; +// assert_eq!( +// expected_hints, +// cached_hint_labels(editor), +// "Hint display settings change should not change the cache" +// ); +// assert_eq!( +// expected_hints, +// visible_hint_labels(editor, cx), +// "Settings change should make cached hints visible" +// ); +// assert_eq!( +// editor.inlay_hint_cache().version, +// 4, +// "Settings change should trigger a cache update" +// ); +// }); +// } + +// #[gpui::test] +// async fn test_inside_char_boundary_range_hints(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 fs = FakeFs::new(cx.background()); +// fs.insert_tree( +// "/a", +// json!({ +// "main.rs": format!(r#"fn main() {{\n{}\n}}"#, format!("let i = {};\n", "√".repeat(10)).repeat(500)), +// "other.rs": "// Test file", +// }), +// ) +// .await; +// let project = Project::test(fs, ["/a".as_ref()], cx).await; +// project.update(cx, |project, _| project.languages().add(Arc::new(language))); +// let workspace = cx +// .add_window(|cx| Workspace::test_new(project.clone(), cx)) +// .root(cx); +// let worktree_id = workspace.update(cx, |workspace, cx| { +// workspace.project().read_with(cx, |project, cx| { +// project.worktrees(cx).next().unwrap().read(cx).id() +// }) +// }); + +// let _buffer = project +// .update(cx, |project, cx| { +// project.open_local_buffer("/a/main.rs", cx) +// }) +// .await +// .unwrap(); +// cx.foreground().run_until_parked(); +// cx.foreground().start_waiting(); +// let fake_server = fake_servers.next().await.unwrap(); +// let editor = workspace +// .update(cx, |workspace, cx| { +// workspace.open_path((worktree_id, "main.rs"), None, true, cx) +// }) +// .await +// .unwrap() +// .downcast::() +// .unwrap(); +// let lsp_request_count = Arc::new(AtomicU32::new(0)); +// let closure_lsp_request_count = Arc::clone(&lsp_request_count); +// fake_server +// .handle_request::(move |params, _| { +// let task_lsp_request_count = Arc::clone(&closure_lsp_request_count); +// async move { +// assert_eq!( +// params.text_document.uri, +// lsp::Url::from_file_path("/a/main.rs").unwrap(), +// ); +// let query_start = params.range.start; +// let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::Release) + 1; +// Ok(Some(vec![lsp::InlayHint { +// position: query_start, +// label: lsp::InlayHintLabel::String(i.to_string()), +// kind: None, +// text_edits: None, +// tooltip: None, +// padding_left: None, +// padding_right: None, +// data: None, +// }])) +// } +// }) +// .next() +// .await; + +// cx.foreground().run_until_parked(); +// editor.update(cx, |editor, cx| { +// editor.change_selections(None, cx, |s| { +// s.select_ranges([Point::new(10, 0)..Point::new(10, 0)]) +// }) +// }); +// cx.foreground().run_until_parked(); +// editor.update(cx, |editor, cx| { +// let expected_hints = vec!["1".to_string()]; +// assert_eq!(expected_hints, cached_hint_labels(editor)); +// assert_eq!(expected_hints, visible_hint_labels(editor, cx)); +// assert_eq!(editor.inlay_hint_cache().version, 1); +// }); +// } + +// #[gpui::test] +// async fn test_toggle_inlay_hints(cx: &mut gpui::TestAppContext) { +// init_test(cx, |settings| { +// settings.defaults.inlay_hints = Some(InlayHintSettings { +// enabled: false, +// show_type_hints: true, +// show_parameter_hints: true, +// show_other_hints: true, +// }) +// }); + +// let (file_with_hints, editor, fake_server) = prepare_test_objects(cx).await; + +// editor.update(cx, |editor, cx| { +// editor.toggle_inlay_hints(&crate::ToggleInlayHints, cx) +// }); +// cx.foreground().start_waiting(); +// let lsp_request_count = Arc::new(AtomicU32::new(0)); +// let closure_lsp_request_count = Arc::clone(&lsp_request_count); +// fake_server +// .handle_request::(move |params, _| { +// let task_lsp_request_count = Arc::clone(&closure_lsp_request_count); +// async move { +// assert_eq!( +// params.text_document.uri, +// lsp::Url::from_file_path(file_with_hints).unwrap(), +// ); + +// let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst) + 1; +// Ok(Some(vec![lsp::InlayHint { +// position: lsp::Position::new(0, i), +// label: lsp::InlayHintLabel::String(i.to_string()), +// kind: None, +// text_edits: None, +// tooltip: None, +// padding_left: None, +// padding_right: None, +// data: None, +// }])) +// } +// }) +// .next() +// .await; +// cx.foreground().run_until_parked(); +// editor.update(cx, |editor, cx| { +// let expected_hints = vec!["1".to_string()]; +// assert_eq!( +// expected_hints, +// cached_hint_labels(editor), +// "Should display inlays after toggle despite them disabled in settings" +// ); +// assert_eq!(expected_hints, visible_hint_labels(editor, cx)); +// assert_eq!( +// editor.inlay_hint_cache().version, +// 1, +// "First toggle should be cache's first update" +// ); +// }); + +// editor.update(cx, |editor, cx| { +// editor.toggle_inlay_hints(&crate::ToggleInlayHints, cx) +// }); +// cx.foreground().run_until_parked(); +// editor.update(cx, |editor, cx| { +// assert!( +// cached_hint_labels(editor).is_empty(), +// "Should clear hints after 2nd toggle" +// ); +// assert!(visible_hint_labels(editor, cx).is_empty()); +// assert_eq!(editor.inlay_hint_cache().version, 2); +// }); + +// update_test_language_settings(cx, |settings| { +// settings.defaults.inlay_hints = Some(InlayHintSettings { +// enabled: true, +// show_type_hints: true, +// show_parameter_hints: true, +// show_other_hints: true, +// }) +// }); +// cx.foreground().run_until_parked(); +// editor.update(cx, |editor, cx| { +// let expected_hints = vec!["2".to_string()]; +// assert_eq!( +// expected_hints, +// cached_hint_labels(editor), +// "Should query LSP hints for the 2nd time after enabling hints in settings" +// ); +// assert_eq!(expected_hints, visible_hint_labels(editor, cx)); +// assert_eq!(editor.inlay_hint_cache().version, 3); +// }); + +// editor.update(cx, |editor, cx| { +// editor.toggle_inlay_hints(&crate::ToggleInlayHints, cx) +// }); +// cx.foreground().run_until_parked(); +// editor.update(cx, |editor, cx| { +// assert!( +// cached_hint_labels(editor).is_empty(), +// "Should clear hints after enabling in settings and a 3rd toggle" +// ); +// assert!(visible_hint_labels(editor, cx).is_empty()); +// assert_eq!(editor.inlay_hint_cache().version, 4); +// }); + +// editor.update(cx, |editor, cx| { +// editor.toggle_inlay_hints(&crate::ToggleInlayHints, cx) +// }); +// cx.foreground().run_until_parked(); +// editor.update(cx, |editor, cx| { +// let expected_hints = vec!["3".to_string()]; +// assert_eq!( +// expected_hints, +// cached_hint_labels(editor), +// "Should query LSP hints for the 3rd time after enabling hints in settings and toggling them back on" +// ); +// assert_eq!(expected_hints, visible_hint_labels(editor, cx)); +// assert_eq!(editor.inlay_hint_cache().version, 5); +// }); +// } + +// pub(crate) fn init_test(cx: &mut TestAppContext, f: impl Fn(&mut AllLanguageSettingsContent)) { +// cx.foreground().forbid_parking(); + +// cx.update(|cx| { +// cx.set_global(SettingsStore::test(cx)); +// theme::init(cx); +// client::init_settings(cx); +// language::init(cx); +// Project::init_settings(cx); +// workspace::init_settings(cx); +// crate::init(cx); +// }); + +// update_test_language_settings(cx, f); +// } + +// async fn prepare_test_objects( +// cx: &mut TestAppContext, +// ) -> (&'static str, ViewHandle, FakeLanguageServer) { +// 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 fs = FakeFs::new(cx.background()); +// fs.insert_tree( +// "/a", +// json!({ +// "main.rs": "fn main() { a } // and some long comment to ensure inlays are not trimmed out", +// "other.rs": "// Test file", +// }), +// ) +// .await; + +// let project = Project::test(fs, ["/a".as_ref()], cx).await; +// project.update(cx, |project, _| project.languages().add(Arc::new(language))); +// let workspace = cx +// .add_window(|cx| Workspace::test_new(project.clone(), cx)) +// .root(cx); +// let worktree_id = workspace.update(cx, |workspace, cx| { +// workspace.project().read_with(cx, |project, cx| { +// project.worktrees(cx).next().unwrap().read(cx).id() +// }) +// }); + +// let _buffer = project +// .update(cx, |project, cx| { +// project.open_local_buffer("/a/main.rs", cx) +// }) +// .await +// .unwrap(); +// cx.foreground().run_until_parked(); +// cx.foreground().start_waiting(); +// let fake_server = fake_servers.next().await.unwrap(); +// let editor = workspace +// .update(cx, |workspace, cx| { +// workspace.open_path((worktree_id, "main.rs"), None, true, cx) +// }) +// .await +// .unwrap() +// .downcast::() +// .unwrap(); + +// editor.update(cx, |editor, cx| { +// assert!(cached_hint_labels(editor).is_empty()); +// assert!(visible_hint_labels(editor, cx).is_empty()); +// assert_eq!(editor.inlay_hint_cache().version, 0); +// }); + +// ("/a/main.rs", editor, fake_server) +// } + +// pub fn cached_hint_labels(editor: &Editor) -> Vec { +// let mut labels = Vec::new(); +// for (_, excerpt_hints) in &editor.inlay_hint_cache().hints { +// let excerpt_hints = excerpt_hints.read(); +// for id in &excerpt_hints.ordered_hints { +// labels.push(excerpt_hints.hints_by_id[id].text()); +// } +// } + +// labels.sort(); +// labels +// } + +// pub fn visible_hint_labels(editor: &Editor, cx: &ViewContext<'_, '_, Editor>) -> Vec { +// let mut hints = editor +// .visible_inlay_hints(cx) +// .into_iter() +// .map(|hint| hint.text.to_string()) +// .collect::>(); +// hints.sort(); +// hints +// } +// } diff --git a/crates/editor2/src/items.rs b/crates/editor2/src/items.rs index 2d249f0374..d5c479ef6b 100644 --- a/crates/editor2/src/items.rs +++ b/crates/editor2/src/items.rs @@ -20,23 +20,18 @@ use smallvec::SmallVec; use std::{ borrow::Cow, cmp::{self, Ordering}, - fmt::Write, iter, ops::Range, path::{Path, PathBuf}, sync::Arc, }; use text::Selection; -use util::{ - paths::{PathExt, FILE_ROW_COLUMN_DELIMITER}, - ResultExt, TryFutureExt, -}; +use util::{paths::PathExt, ResultExt, TryFutureExt}; use workspace::item::{BreadcrumbText, FollowableItemHandle}; use workspace::{ item::{FollowableItem, Item, ItemEvent, ItemHandle, ProjectItem}, searchable::{Direction, SearchEvent, SearchableItem, SearchableItemHandle}, - ItemId, ItemNavHistory, Pane, StatusItemView, ToolbarItemLocation, ViewId, Workspace, - WorkspaceId, + ItemId, ItemNavHistory, Pane, ToolbarItemLocation, ViewId, Workspace, WorkspaceId, }; pub const MAX_TAB_TITLE_LEN: usize = 24; @@ -607,7 +602,7 @@ impl Item for Editor { where Self: Sized, { - Some(self.clone(cx)) + Some(self.clone()) } fn set_nav_history(&mut self, history: ItemNavHistory, _: &mut ViewContext) { diff --git a/crates/editor2/src/link_go_to_definition.rs b/crates/editor2/src/link_go_to_definition.rs index 7da0b88622..8ac27b480f 100644 --- a/crates/editor2/src/link_go_to_definition.rs +++ b/crates/editor2/src/link_go_to_definition.rs @@ -483,39 +483,40 @@ pub fn show_link_definition( }); if any_definition_does_not_contain_current_location { - // Highlight symbol using theme link definition highlight style - let style = theme::current(cx).editor.link_definition; - let highlight_range = - symbol_range.unwrap_or_else(|| match &trigger_point { - TriggerPoint::Text(trigger_anchor) => { - let snapshot = &snapshot.buffer_snapshot; - // If no symbol range returned from language server, use the surrounding word. - let (offset_range, _) = - snapshot.surrounding_word(*trigger_anchor); - RangeInEditor::Text( - snapshot.anchor_before(offset_range.start) - ..snapshot.anchor_after(offset_range.end), - ) - } - TriggerPoint::InlayHint(highlight, _, _) => { - RangeInEditor::Inlay(highlight.clone()) - } - }); + // todo!() + // // Highlight symbol using theme link definition highlight style + // let style = theme::current(cx).editor.link_definition; + // let highlight_range = + // symbol_range.unwrap_or_else(|| match &trigger_point { + // TriggerPoint::Text(trigger_anchor) => { + // let snapshot = &snapshot.buffer_snapshot; + // // If no symbol range returned from language server, use the surrounding word. + // let (offset_range, _) = + // snapshot.surrounding_word(*trigger_anchor); + // RangeInEditor::Text( + // snapshot.anchor_before(offset_range.start) + // ..snapshot.anchor_after(offset_range.end), + // ) + // } + // TriggerPoint::InlayHint(highlight, _, _) => { + // RangeInEditor::Inlay(highlight.clone()) + // } + // }); - match highlight_range { - RangeInEditor::Text(text_range) => this - .highlight_text::( - vec![text_range], - style, - cx, - ), - RangeInEditor::Inlay(highlight) => this - .highlight_inlays::( - vec![highlight], - style, - cx, - ), - } + // match highlight_range { + // RangeInEditor::Text(text_range) => this + // .highlight_text::( + // vec![text_range], + // style, + // cx, + // ), + // RangeInEditor::Inlay(highlight) => this + // .highlight_inlays::( + // vec![highlight], + // style, + // cx, + // ), + // } } else { hide_link_definition(this, cx); } @@ -599,671 +600,671 @@ 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; +// #[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, |_| {}); +// #[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; +// 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; - "}); +// 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; - "}); +// // 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, - }, - ]))) - }); +// 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; - "}); +// // 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; - "}); +// // 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; - "}); +// // 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, - }, - ]))) - }); +// 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.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; - "}); - } +// 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, |_| {}); +// #[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; +// 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(); } - "}); +// 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(); } - "}); +// // 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, - }, - ]))) - }); +// 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(); } - "}); +// 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); - }); +// // 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(); } - "}); +// // 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(); +// // 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(); } - "}); +// 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(); +// // 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(); } - "}); +// // 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(); +// // 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(); } - "}); +// // 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 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(); +// 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»(); } - "}); +// 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(); } - "}); +// // 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 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»(); } - "}); +// // 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(); } - "}); +// // 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(); } - "}); +// // 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(); } - "}); +// // 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(); } - "}); +// 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, - }, - ]))) - }); +// // 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(); - } +// // 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, - }) - }); +// #[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; +// 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; +// } +// "}); +// 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; +// } +// "})[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; - } - "}); +// 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 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; +// 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::>(); +// 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]); - }); +// 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(); +// // 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:?}"); - }); +// 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ˇ»; +// // 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; - } - "}); - } -} +// fn main() { +// let variable = TestStruct; +// } +// "}); +// } +// } diff --git a/crates/editor2/src/mouse_context_menu.rs b/crates/editor2/src/mouse_context_menu.rs index 8dfdcdff53..84c670c79d 100644 --- a/crates/editor2/src/mouse_context_menu.rs +++ b/crates/editor2/src/mouse_context_menu.rs @@ -1,13 +1,10 @@ -use crate::{ - DisplayPoint, Editor, EditorMode, FindAllReferences, GoToDefinition, GoToTypeDefinition, - Rename, RevealInFinder, SelectMode, ToggleCodeActions, -}; +use crate::{DisplayPoint, Editor, EditorMode, SelectMode}; use context_menu::ContextMenuItem; -use gpui::{elements::AnchorCorner, geometry::vector::Vector2F, ViewContext}; +use gpui::{Pixels, Point, ViewContext}; pub fn deploy_context_menu( editor: &mut Editor, - position: Vector2F, + position: Point, point: DisplayPoint, cx: &mut ViewContext, ) { @@ -31,66 +28,67 @@ pub fn deploy_context_menu( s.set_pending_display_range(point..point, SelectMode::Character); }); - editor.mouse_context_menu.update(cx, |menu, cx| { - menu.show( - position, - AnchorCorner::TopLeft, - vec![ - ContextMenuItem::action("Rename Symbol", Rename), - ContextMenuItem::action("Go to Definition", GoToDefinition), - ContextMenuItem::action("Go to Type Definition", GoToTypeDefinition), - ContextMenuItem::action("Find All References", FindAllReferences), - ContextMenuItem::action( - "Code Actions", - ToggleCodeActions { - deployed_from_indicator: false, - }, - ), - ContextMenuItem::Separator, - ContextMenuItem::action("Reveal in Finder", RevealInFinder), - ], - cx, - ); - }); + // todo!() + // editor.mouse_context_menu.update(cx, |menu, cx| { + // menu.show( + // position, + // AnchorCorner::TopLeft, + // vec![ + // ContextMenuItem::action("Rename Symbol", Rename), + // ContextMenuItem::action("Go to Definition", GoToDefinition), + // ContextMenuItem::action("Go to Type Definition", GoToTypeDefinition), + // ContextMenuItem::action("Find All References", FindAllReferences), + // ContextMenuItem::action( + // "Code Actions", + // ToggleCodeActions { + // deployed_from_indicator: false, + // }, + // ), + // ContextMenuItem::Separator, + // ContextMenuItem::action("Reveal in Finder", RevealInFinder), + // ], + // cx, + // ); + // }); 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.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.read(app).visible())); +// } +// } diff --git a/crates/editor2/src/movement.rs b/crates/editor2/src/movement.rs index 05b9b039c4..593f7a4831 100644 --- a/crates/editor2/src/movement.rs +++ b/crates/editor2/src/movement.rs @@ -1,6 +1,6 @@ use super::{Bias, DisplayPoint, DisplaySnapshot, SelectionGoal, ToDisplayPoint}; use crate::{char_kind, CharKind, EditorStyle, ToOffset, ToPoint}; -use gpui::{FontCache, TextLayoutCache}; +use gpui::TextSystem; use language::Point; use std::{ops::Range, sync::Arc}; @@ -13,8 +13,7 @@ pub enum FindRange { /// TextLayoutDetails encompasses everything we need to move vertically /// taking into account variable width characters. pub struct TextLayoutDetails { - pub font_cache: Arc, - pub text_layout_cache: Arc, + pub text_system: TextSystem, pub editor_style: EditorStyle, } diff --git a/crates/editor2/src/scroll.rs b/crates/editor2/src/scroll.rs index 8233f92a1a..9a6af2961a 100644 --- a/crates/editor2/src/scroll.rs +++ b/crates/editor2/src/scroll.rs @@ -2,19 +2,6 @@ pub mod actions; pub mod autoscroll; pub mod scroll_amount; -use std::{ - cmp::Ordering, - time::{Duration, Instant}, -}; - -use gpui::{ - geometry::vector::{vec2f, Vector2F}, - AppContext, Axis, Task, ViewContext, -}; -use language::{Bias, Point}; -use util::ResultExt; -use workspace::WorkspaceId; - use crate::{ display_map::{DisplaySnapshot, ToDisplayPoint}, hover_popover::hide_hover, @@ -22,6 +9,14 @@ use crate::{ Anchor, DisplayPoint, Editor, EditorMode, Event, InlayHintRefreshReason, MultiBufferSnapshot, ToPoint, }; +use gpui::{point, AppContext, Pixels, Task, ViewContext}; +use language::{Bias, Point}; +use std::{ + cmp::Ordering, + time::{Duration, Instant}, +}; +use util::ResultExt; +use workspace::WorkspaceId; use self::{ autoscroll::{Autoscroll, AutoscrollStrategy}, @@ -37,19 +32,19 @@ pub struct ScrollbarAutoHide(pub bool); #[derive(Clone, Copy, Debug, PartialEq)] pub struct ScrollAnchor { - pub offset: Vector2F, + pub offset: gpui::Point, pub anchor: Anchor, } impl ScrollAnchor { fn new() -> Self { Self { - offset: Vector2F::zero(), + offset: Point::zero(), anchor: Anchor::min(), } } - pub fn scroll_position(&self, snapshot: &DisplaySnapshot) -> Vector2F { + pub fn scroll_position(&self, snapshot: &DisplaySnapshot) -> Point { let mut scroll_position = self.offset; if self.anchor != Anchor::min() { let scroll_top = self.anchor.to_display_point(snapshot).row() as f32; @@ -65,6 +60,12 @@ impl ScrollAnchor { } } +#[derive(Copy, Clone, PartialEq, Eq, Debug)] +pub enum Axis { + Vertical, + Horizontal, +} + #[derive(Clone, Copy, Debug)] pub struct OngoingScroll { last_event: Instant, @@ -79,7 +80,7 @@ impl OngoingScroll { } } - pub fn filter(&self, delta: &mut Vector2F) -> Option { + pub fn filter(&self, delta: &mut Point) -> Option { const UNLOCK_PERCENT: f32 = 1.9; const UNLOCK_LOWER_BOUND: f32 = 6.; let mut axis = self.axis; @@ -114,8 +115,8 @@ impl OngoingScroll { } match axis { - Some(Axis::Vertical) => *delta = vec2f(0., delta.y()), - Some(Axis::Horizontal) => *delta = vec2f(delta.x(), 0.), + Some(Axis::Vertical) => *delta = point(0., delta.y()), + Some(Axis::Horizontal) => *delta = point(delta.x(), 0.), None => {} } @@ -128,7 +129,7 @@ pub struct ScrollManager { anchor: ScrollAnchor, ongoing: OngoingScroll, autoscroll_request: Option<(Autoscroll, bool)>, - last_autoscroll: Option<(Vector2F, f32, f32, AutoscrollStrategy)>, + last_autoscroll: Option<(gpui::Point, f32, f32, AutoscrollStrategy)>, show_scrollbars: bool, hide_scrollbar_task: Option>, visible_line_count: Option, @@ -166,13 +167,13 @@ impl ScrollManager { self.ongoing.axis = axis; } - pub fn scroll_position(&self, snapshot: &DisplaySnapshot) -> Vector2F { + pub fn scroll_position(&self, snapshot: &DisplaySnapshot) -> Point { self.anchor.scroll_position(snapshot) } fn set_scroll_position( &mut self, - scroll_position: Vector2F, + scroll_position: Point, map: &DisplaySnapshot, local: bool, autoscroll: bool, @@ -183,7 +184,7 @@ impl ScrollManager { ( ScrollAnchor { anchor: Anchor::min(), - offset: scroll_position.max(vec2f(0., 0.)), + offset: scroll_position.max(Point::zero()), }, 0, ) @@ -197,7 +198,7 @@ impl ScrollManager { ( ScrollAnchor { anchor: top_anchor, - offset: vec2f( + offset: point( scroll_position.x(), scroll_position.y() - top_anchor.to_display_point(&map).row() as f32, ), @@ -310,13 +311,17 @@ impl Editor { } } - pub fn set_scroll_position(&mut self, scroll_position: Vector2F, cx: &mut ViewContext) { + pub fn set_scroll_position( + &mut self, + scroll_position: Point, + cx: &mut ViewContext, + ) { self.set_scroll_position_internal(scroll_position, true, false, cx); } pub(crate) fn set_scroll_position_internal( &mut self, - scroll_position: Vector2F, + scroll_position: Point, local: bool, autoscroll: bool, cx: &mut ViewContext, @@ -337,7 +342,7 @@ impl Editor { self.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx); } - pub fn scroll_position(&self, cx: &mut ViewContext) -> Vector2F { + pub fn scroll_position(&self, cx: &mut ViewContext) -> Point { let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); self.scroll_manager.anchor.scroll_position(&display_map) } @@ -379,7 +384,7 @@ impl Editor { } let cur_position = self.scroll_position(cx); - let new_pos = cur_position + vec2f(0., amount.lines(self)); + let new_pos = cur_position + point(0., amount.lines(self)); self.set_scroll_position(new_pos, cx); } @@ -427,7 +432,7 @@ impl Editor { .snapshot(cx) .anchor_at(Point::new(top_row as u32, 0), Bias::Left); let scroll_anchor = ScrollAnchor { - offset: Vector2F::new(x, y), + offset: Point::new(x, y), anchor: top_anchor, }; self.set_scroll_anchor(scroll_anchor, cx); diff --git a/crates/editor2/src/scroll/actions.rs b/crates/editor2/src/scroll/actions.rs index e943bed767..ba39d3849b 100644 --- a/crates/editor2/src/scroll/actions.rs +++ b/crates/editor2/src/scroll/actions.rs @@ -17,7 +17,7 @@ use gpui::AppContext; // ); pub fn init(cx: &mut AppContext) { - /// todo!() + // todo!() // cx.add_action(Editor::next_screen); // cx.add_action(Editor::scroll_cursor_top); // cx.add_action(Editor::scroll_cursor_center); diff --git a/crates/editor2/src/selections_collection.rs b/crates/editor2/src/selections_collection.rs index 151a27c901..e1ef69ac8a 100644 --- a/crates/editor2/src/selections_collection.rs +++ b/crates/editor2/src/selections_collection.rs @@ -25,8 +25,8 @@ pub struct PendingSelection { #[derive(Debug, Clone)] pub struct SelectionsCollection { - display_map: ModelHandle, - buffer: ModelHandle, + display_map: Model, + buffer: Model, pub next_selection_id: usize, pub line_mode: bool, disjoint: Arc<[Selection]>, @@ -34,7 +34,7 @@ pub struct SelectionsCollection { } impl SelectionsCollection { - pub fn new(display_map: ModelHandle, buffer: ModelHandle) -> Self { + pub fn new(display_map: Model, buffer: Model) -> Self { Self { display_map, buffer, diff --git a/crates/editor2/src/test.rs b/crates/editor2/src/test.rs index 45a61e58de..8087569925 100644 --- a/crates/editor2/src/test.rs +++ b/crates/editor2/src/test.rs @@ -67,16 +67,13 @@ pub fn assert_text_with_selections( // RA thinks this is dead code even though it is used in a whole lot of tests #[allow(dead_code)] #[cfg(any(test, feature = "test-support"))] -pub(crate) fn build_editor( - buffer: ModelHandle, - cx: &mut ViewContext, -) -> Editor { +pub(crate) fn build_editor(buffer: Model, cx: &mut ViewContext) -> Editor { Editor::new(EditorMode::Full, buffer, None, None, cx) } pub(crate) fn build_editor_with_project( - project: ModelHandle, - buffer: ModelHandle, + project: Model, + buffer: Model, cx: &mut ViewContext, ) -> Editor { Editor::new(EditorMode::Full, buffer, Some(project), None, cx) diff --git a/crates/editor2/src/test/editor_lsp_test_context.rs b/crates/editor2/src/test/editor_lsp_test_context.rs index 3e2f38a0b6..6d1662857d 100644 --- a/crates/editor2/src/test/editor_lsp_test_context.rs +++ b/crates/editor2/src/test/editor_lsp_test_context.rs @@ -9,7 +9,7 @@ use anyhow::Result; use crate::{Editor, ToPoint}; use collections::HashSet; use futures::Future; -use gpui::{json, ViewContext, ViewHandle}; +use gpui::{json, View, ViewContext}; use indoc::indoc; use language::{point_to_lsp, FakeLspAdapter, Language, LanguageConfig, LanguageQueries}; use lsp::{notification, request}; diff --git a/crates/editor2/src/test/editor_test_context.rs b/crates/editor2/src/test/editor_test_context.rs index c083ce0e68..c856afeefe 100644 --- a/crates/editor2/src/test/editor_test_context.rs +++ b/crates/editor2/src/test/editor_test_context.rs @@ -3,8 +3,7 @@ use crate::{ }; use futures::Future; use gpui::{ - executor::Foreground, keymap_matcher::Keystroke, AnyWindowHandle, AppContext, ContextHandle, - ModelContext, ViewContext, ViewHandle, + AnyWindowHandle, AppContext, ForegroundExecutor, Keystroke, ModelContext, View, ViewContext, }; use indoc::indoc; use language::{Buffer, BufferSnapshot}; @@ -23,7 +22,7 @@ use super::build_editor_with_project; pub struct EditorTestContext<'a> { pub cx: &'a mut gpui::TestAppContext, pub window: AnyWindowHandle, - pub editor: ViewHandle, + pub editor: View, } impl<'a> EditorTestContext<'a> { @@ -119,37 +118,37 @@ impl<'a> EditorTestContext<'a> { self.buffer(|buffer, _| buffer.snapshot()) } - pub fn simulate_keystroke(&mut self, keystroke_text: &str) -> ContextHandle { - let keystroke_under_test_handle = - self.add_assertion_context(format!("Simulated Keystroke: {:?}", keystroke_text)); - let keystroke = Keystroke::parse(keystroke_text).unwrap(); + // pub fn simulate_keystroke(&mut self, keystroke_text: &str) -> ContextHandle { + // let keystroke_under_test_handle = + // self.add_assertion_context(format!("Simulated Keystroke: {:?}", keystroke_text)); + // let keystroke = Keystroke::parse(keystroke_text).unwrap(); - self.cx.dispatch_keystroke(self.window, keystroke, false); + // self.cx.dispatch_keystroke(self.window, keystroke, false); - keystroke_under_test_handle - } + // keystroke_under_test_handle + // } - pub fn simulate_keystrokes( - &mut self, - keystroke_texts: [&str; COUNT], - ) -> ContextHandle { - let keystrokes_under_test_handle = - self.add_assertion_context(format!("Simulated Keystrokes: {:?}", keystroke_texts)); - for keystroke_text in keystroke_texts.into_iter() { - self.simulate_keystroke(keystroke_text); - } - // it is common for keyboard shortcuts to kick off async actions, so this ensures that they are complete - // before returning. - // NOTE: we don't do this in simulate_keystroke() because a possible cause of bugs is that typing too - // quickly races with async actions. - if let Foreground::Deterministic { cx_id: _, executor } = self.cx.foreground().as_ref() { - executor.run_until_parked(); - } else { - unreachable!(); - } + // pub fn simulate_keystrokes( + // &mut self, + // keystroke_texts: [&str; COUNT], + // ) -> ContextHandle { + // let keystrokes_under_test_handle = + // self.add_assertion_context(format!("Simulated Keystrokes: {:?}", keystroke_texts)); + // for keystroke_text in keystroke_texts.into_iter() { + // self.simulate_keystroke(keystroke_text); + // } + // // it is common for keyboard shortcuts to kick off async actions, so this ensures that they are complete + // // before returning. + // // NOTE: we don't do this in simulate_keystroke() because a possible cause of bugs is that typing too + // // quickly races with async actions. + // if let Foreground::Deterministic { cx_id: _, executor } = self.cx.foreground().as_ref() { + // executor.run_until_parked(); + // } else { + // unreachable!(); + // } - keystrokes_under_test_handle - } + // keystrokes_under_test_handle + // } pub fn ranges(&self, marked_text: &str) -> Vec> { let (unmarked_text, ranges) = marked_text_ranges(marked_text, false); @@ -177,144 +176,144 @@ impl<'a> EditorTestContext<'a> { self.update_buffer(|buffer, cx| buffer.set_diff_base(diff_base, cx)); } - /// Change the editor's text and selections using a string containing - /// embedded range markers that represent the ranges and directions of - /// each selection. - /// - /// Returns a context handle so that assertion failures can print what - /// editor state was needed to cause the failure. - /// - /// See the `util::test::marked_text_ranges` function for more information. - pub fn set_state(&mut self, marked_text: &str) -> ContextHandle { - let state_context = self.add_assertion_context(format!( - "Initial Editor State: \"{}\"", - marked_text.escape_debug().to_string() - )); - let (unmarked_text, selection_ranges) = marked_text_ranges(marked_text, true); - self.editor.update(self.cx, |editor, cx| { - editor.set_text(unmarked_text, cx); - editor.change_selections(Some(Autoscroll::fit()), cx, |s| { - s.select_ranges(selection_ranges) - }) - }); - state_context - } + // /// Change the editor's text and selections using a string containing + // /// embedded range markers that represent the ranges and directions of + // /// each selection. + // /// + // /// Returns a context handle so that assertion failures can print what + // /// editor state was needed to cause the failure. + // /// + // /// See the `util::test::marked_text_ranges` function for more information. + // pub fn set_state(&mut self, marked_text: &str) -> ContextHandle { + // let state_context = self.add_assertion_context(format!( + // "Initial Editor State: \"{}\"", + // marked_text.escape_debug().to_string() + // )); + // let (unmarked_text, selection_ranges) = marked_text_ranges(marked_text, true); + // self.editor.update(self.cx, |editor, cx| { + // editor.set_text(unmarked_text, cx); + // editor.change_selections(Some(Autoscroll::fit()), cx, |s| { + // s.select_ranges(selection_ranges) + // }) + // }); + // state_context + // } - /// Only change the editor's selections - pub fn set_selections_state(&mut self, marked_text: &str) -> ContextHandle { - let state_context = self.add_assertion_context(format!( - "Initial Editor State: \"{}\"", - marked_text.escape_debug().to_string() - )); - let (unmarked_text, selection_ranges) = marked_text_ranges(marked_text, true); - self.editor.update(self.cx, |editor, cx| { - assert_eq!(editor.text(cx), unmarked_text); - editor.change_selections(Some(Autoscroll::fit()), cx, |s| { - s.select_ranges(selection_ranges) - }) - }); - state_context - } + // /// Only change the editor's selections + // pub fn set_selections_state(&mut self, marked_text: &str) -> ContextHandle { + // let state_context = self.add_assertion_context(format!( + // "Initial Editor State: \"{}\"", + // marked_text.escape_debug().to_string() + // )); + // let (unmarked_text, selection_ranges) = marked_text_ranges(marked_text, true); + // self.editor.update(self.cx, |editor, cx| { + // assert_eq!(editor.text(cx), unmarked_text); + // editor.change_selections(Some(Autoscroll::fit()), cx, |s| { + // s.select_ranges(selection_ranges) + // }) + // }); + // state_context + // } - /// Make an assertion about the editor's text and the ranges and directions - /// of its selections using a string containing embedded range markers. - /// - /// See the `util::test::marked_text_ranges` function for more information. - #[track_caller] - pub fn assert_editor_state(&mut self, marked_text: &str) { - let (unmarked_text, expected_selections) = marked_text_ranges(marked_text, true); - let buffer_text = self.buffer_text(); + // /// Make an assertion about the editor's text and the ranges and directions + // /// of its selections using a string containing embedded range markers. + // /// + // /// See the `util::test::marked_text_ranges` function for more information. + // #[track_caller] + // pub fn assert_editor_state(&mut self, marked_text: &str) { + // let (unmarked_text, expected_selections) = marked_text_ranges(marked_text, true); + // let buffer_text = self.buffer_text(); - if buffer_text != unmarked_text { - panic!("Unmarked text doesn't match buffer text\nBuffer text: {buffer_text:?}\nUnmarked text: {unmarked_text:?}\nRaw buffer text\n{buffer_text}Raw unmarked text\n{unmarked_text}"); - } + // if buffer_text != unmarked_text { + // panic!("Unmarked text doesn't match buffer text\nBuffer text: {buffer_text:?}\nUnmarked text: {unmarked_text:?}\nRaw buffer text\n{buffer_text}Raw unmarked text\n{unmarked_text}"); + // } - self.assert_selections(expected_selections, marked_text.to_string()) - } + // self.assert_selections(expected_selections, marked_text.to_string()) + // } - pub fn editor_state(&mut self) -> String { - generate_marked_text(self.buffer_text().as_str(), &self.editor_selections(), true) - } + // pub fn editor_state(&mut self) -> String { + // generate_marked_text(self.buffer_text().as_str(), &self.editor_selections(), true) + // } - #[track_caller] - pub fn assert_editor_background_highlights(&mut self, marked_text: &str) { - let expected_ranges = self.ranges(marked_text); - let actual_ranges: Vec> = self.update_editor(|editor, cx| { - let snapshot = editor.snapshot(cx); - editor - .background_highlights - .get(&TypeId::of::()) - .map(|h| h.1.clone()) - .unwrap_or_default() - .into_iter() - .map(|range| range.to_offset(&snapshot.buffer_snapshot)) - .collect() - }); - assert_set_eq!(actual_ranges, expected_ranges); - } + // #[track_caller] + // pub fn assert_editor_background_highlights(&mut self, marked_text: &str) { + // let expected_ranges = self.ranges(marked_text); + // let actual_ranges: Vec> = self.update_editor(|editor, cx| { + // let snapshot = editor.snapshot(cx); + // editor + // .background_highlights + // .get(&TypeId::of::()) + // .map(|h| h.1.clone()) + // .unwrap_or_default() + // .into_iter() + // .map(|range| range.to_offset(&snapshot.buffer_snapshot)) + // .collect() + // }); + // assert_set_eq!(actual_ranges, expected_ranges); + // } - #[track_caller] - pub fn assert_editor_text_highlights(&mut self, marked_text: &str) { - let expected_ranges = self.ranges(marked_text); - let snapshot = self.update_editor(|editor, cx| editor.snapshot(cx)); - let actual_ranges: Vec> = snapshot - .text_highlight_ranges::() - .map(|ranges| ranges.as_ref().clone().1) - .unwrap_or_default() - .into_iter() - .map(|range| range.to_offset(&snapshot.buffer_snapshot)) - .collect(); - assert_set_eq!(actual_ranges, expected_ranges); - } + // #[track_caller] + // pub fn assert_editor_text_highlights(&mut self, marked_text: &str) { + // let expected_ranges = self.ranges(marked_text); + // let snapshot = self.update_editor(|editor, cx| editor.snapshot(cx)); + // let actual_ranges: Vec> = snapshot + // .text_highlight_ranges::() + // .map(|ranges| ranges.as_ref().clone().1) + // .unwrap_or_default() + // .into_iter() + // .map(|range| range.to_offset(&snapshot.buffer_snapshot)) + // .collect(); + // assert_set_eq!(actual_ranges, expected_ranges); + // } - #[track_caller] - pub fn assert_editor_selections(&mut self, expected_selections: Vec>) { - let expected_marked_text = - generate_marked_text(&self.buffer_text(), &expected_selections, true); - self.assert_selections(expected_selections, expected_marked_text) - } + // #[track_caller] + // pub fn assert_editor_selections(&mut self, expected_selections: Vec>) { + // let expected_marked_text = + // generate_marked_text(&self.buffer_text(), &expected_selections, true); + // self.assert_selections(expected_selections, expected_marked_text) + // } - fn editor_selections(&self) -> Vec> { - self.editor - .read_with(self.cx, |editor, cx| editor.selections.all::(cx)) - .into_iter() - .map(|s| { - if s.reversed { - s.end..s.start - } else { - s.start..s.end - } - }) - .collect::>() - } + // fn editor_selections(&self) -> Vec> { + // self.editor + // .read_with(self.cx, |editor, cx| editor.selections.all::(cx)) + // .into_iter() + // .map(|s| { + // if s.reversed { + // s.end..s.start + // } else { + // s.start..s.end + // } + // }) + // .collect::>() + // } - #[track_caller] - fn assert_selections( - &mut self, - expected_selections: Vec>, - expected_marked_text: String, - ) { - let actual_selections = self.editor_selections(); - let actual_marked_text = - generate_marked_text(&self.buffer_text(), &actual_selections, true); - if expected_selections != actual_selections { - panic!( - indoc! {" + // #[track_caller] + // fn assert_selections( + // &mut self, + // expected_selections: Vec>, + // expected_marked_text: String, + // ) { + // let actual_selections = self.editor_selections(); + // let actual_marked_text = + // generate_marked_text(&self.buffer_text(), &actual_selections, true); + // if expected_selections != actual_selections { + // panic!( + // indoc! {" - {}Editor has unexpected selections. + // {}Editor has unexpected selections. - Expected selections: - {} + // Expected selections: + // {} - Actual selections: - {} - "}, - self.assertion_context(), - expected_marked_text, - actual_marked_text, - ); - } - } + // Actual selections: + // {} + // "}, + // self.assertion_context(), + // expected_marked_text, + // actual_marked_text, + // ); + // } + // } } impl<'a> Deref for EditorTestContext<'a> { diff --git a/crates/gpui2/src/text_system.rs b/crates/gpui2/src/text_system.rs index 7c96b1a180..ee8c653866 100644 --- a/crates/gpui2/src/text_system.rs +++ b/crates/gpui2/src/text_system.rs @@ -7,7 +7,7 @@ use anyhow::anyhow; pub use font_features::*; pub use line::*; pub use line_layout::*; -use line_wrapper::*; +pub use line_wrapper::*; use smallvec::SmallVec; use crate::{ diff --git a/crates/language2/Cargo.toml b/crates/language2/Cargo.toml index 4fca16bcb5..bd43465b55 100644 --- a/crates/language2/Cargo.toml +++ b/crates/language2/Cargo.toml @@ -58,6 +58,7 @@ unicase = "2.6" rand = { workspace = true, optional = true } tree-sitter-rust = { workspace = true, optional = true } tree-sitter-typescript = { workspace = true, optional = true } +pulldown-cmark = { version = "0.9.2", default-features = false } [dev-dependencies] client = { package = "client2", path = "../client2", features = ["test-support"] } diff --git a/crates/language2/src/language2.rs b/crates/language2/src/language2.rs index 381284659b..826817c99a 100644 --- a/crates/language2/src/language2.rs +++ b/crates/language2/src/language2.rs @@ -8,6 +8,7 @@ mod syntax_map; #[cfg(test)] mod buffer_tests; +pub mod markdown; use anyhow::{anyhow, Context, Result}; use async_trait::async_trait; diff --git a/crates/language2/src/markdown.rs b/crates/language2/src/markdown.rs new file mode 100644 index 0000000000..df75b610ef --- /dev/null +++ b/crates/language2/src/markdown.rs @@ -0,0 +1,301 @@ +use std::sync::Arc; +use std::{ops::Range, path::PathBuf}; + +use crate::{HighlightId, Language, LanguageRegistry}; +use gpui::{px, FontStyle, FontWeight, HighlightStyle, UnderlineStyle}; +use pulldown_cmark::{CodeBlockKind, Event, Options, Parser, Tag}; + +#[derive(Debug, Clone)] +pub struct ParsedMarkdown { + pub text: String, + pub highlights: Vec<(Range, MarkdownHighlight)>, + pub region_ranges: Vec>, + pub regions: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum MarkdownHighlight { + Style(MarkdownHighlightStyle), + Code(HighlightId), +} + +impl MarkdownHighlight { + pub fn to_highlight_style(&self, theme: &theme::SyntaxTheme) -> Option { + match self { + MarkdownHighlight::Style(style) => { + let mut highlight = HighlightStyle::default(); + + if style.italic { + highlight.font_style = Some(FontStyle::Italic); + } + + if style.underline { + highlight.underline = Some(UnderlineStyle { + thickness: px(1.), + ..Default::default() + }); + } + + if style.weight != FontWeight::default() { + highlight.font_weight = Some(style.weight); + } + + Some(highlight) + } + + MarkdownHighlight::Code(id) => id.style(theme), + } + } +} + +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct MarkdownHighlightStyle { + pub italic: bool, + pub underline: bool, + pub weight: FontWeight, +} + +#[derive(Debug, Clone)] +pub struct ParsedRegion { + pub code: bool, + pub link: Option, +} + +#[derive(Debug, Clone)] +pub enum Link { + Web { url: String }, + Path { path: PathBuf }, +} + +impl Link { + fn identify(text: String) -> Option { + if text.starts_with("http") { + return Some(Link::Web { url: text }); + } + + let path = PathBuf::from(text); + if path.is_absolute() { + return Some(Link::Path { path }); + } + + None + } +} + +pub async fn parse_markdown( + markdown: &str, + language_registry: &Arc, + language: Option>, +) -> ParsedMarkdown { + let mut text = String::new(); + let mut highlights = Vec::new(); + let mut region_ranges = Vec::new(); + let mut regions = Vec::new(); + + parse_markdown_block( + markdown, + language_registry, + language, + &mut text, + &mut highlights, + &mut region_ranges, + &mut regions, + ) + .await; + + ParsedMarkdown { + text, + highlights, + region_ranges, + regions, + } +} + +pub async fn parse_markdown_block( + markdown: &str, + language_registry: &Arc, + language: Option>, + text: &mut String, + highlights: &mut Vec<(Range, MarkdownHighlight)>, + region_ranges: &mut Vec>, + regions: &mut Vec, +) { + let mut bold_depth = 0; + let mut italic_depth = 0; + let mut link_url = None; + let mut current_language = None; + let mut list_stack = Vec::new(); + + for event in Parser::new_ext(&markdown, Options::all()) { + let prev_len = text.len(); + match event { + Event::Text(t) => { + if let Some(language) = ¤t_language { + highlight_code(text, highlights, t.as_ref(), language); + } else { + text.push_str(t.as_ref()); + + let mut style = MarkdownHighlightStyle::default(); + + if bold_depth > 0 { + style.weight = FontWeight::BOLD; + } + + if italic_depth > 0 { + style.italic = true; + } + + if let Some(link) = link_url.clone().and_then(|u| Link::identify(u)) { + region_ranges.push(prev_len..text.len()); + regions.push(ParsedRegion { + code: false, + link: Some(link), + }); + style.underline = true; + } + + if style != MarkdownHighlightStyle::default() { + let mut new_highlight = true; + if let Some((last_range, MarkdownHighlight::Style(last_style))) = + highlights.last_mut() + { + if last_range.end == prev_len && last_style == &style { + last_range.end = text.len(); + new_highlight = false; + } + } + if new_highlight { + let range = prev_len..text.len(); + highlights.push((range, MarkdownHighlight::Style(style))); + } + } + } + } + + Event::Code(t) => { + text.push_str(t.as_ref()); + region_ranges.push(prev_len..text.len()); + + let link = link_url.clone().and_then(|u| Link::identify(u)); + if link.is_some() { + highlights.push(( + prev_len..text.len(), + MarkdownHighlight::Style(MarkdownHighlightStyle { + underline: true, + ..Default::default() + }), + )); + } + regions.push(ParsedRegion { code: true, link }); + } + + Event::Start(tag) => match tag { + Tag::Paragraph => new_paragraph(text, &mut list_stack), + + Tag::Heading(_, _, _) => { + new_paragraph(text, &mut list_stack); + bold_depth += 1; + } + + Tag::CodeBlock(kind) => { + new_paragraph(text, &mut list_stack); + current_language = if let CodeBlockKind::Fenced(language) = kind { + language_registry + .language_for_name(language.as_ref()) + .await + .ok() + } else { + language.clone() + } + } + + Tag::Emphasis => italic_depth += 1, + + Tag::Strong => bold_depth += 1, + + Tag::Link(_, url, _) => link_url = Some(url.to_string()), + + Tag::List(number) => { + list_stack.push((number, false)); + } + + Tag::Item => { + let len = list_stack.len(); + if let Some((list_number, has_content)) = list_stack.last_mut() { + *has_content = false; + if !text.is_empty() && !text.ends_with('\n') { + text.push('\n'); + } + for _ in 0..len - 1 { + text.push_str(" "); + } + if let Some(number) = list_number { + text.push_str(&format!("{}. ", number)); + *number += 1; + *has_content = false; + } else { + text.push_str("- "); + } + } + } + + _ => {} + }, + + Event::End(tag) => match tag { + Tag::Heading(_, _, _) => bold_depth -= 1, + Tag::CodeBlock(_) => current_language = None, + Tag::Emphasis => italic_depth -= 1, + Tag::Strong => bold_depth -= 1, + Tag::Link(_, _, _) => link_url = None, + Tag::List(_) => drop(list_stack.pop()), + _ => {} + }, + + Event::HardBreak => text.push('\n'), + + Event::SoftBreak => text.push(' '), + + _ => {} + } + } +} + +pub fn highlight_code( + text: &mut String, + highlights: &mut Vec<(Range, MarkdownHighlight)>, + content: &str, + language: &Arc, +) { + let prev_len = text.len(); + text.push_str(content); + for (range, highlight_id) in language.highlight_text(&content.into(), 0..content.len()) { + let highlight = MarkdownHighlight::Code(highlight_id); + highlights.push((prev_len + range.start..prev_len + range.end, highlight)); + } +} + +pub fn new_paragraph(text: &mut String, list_stack: &mut Vec<(Option, bool)>) { + let mut is_subsequent_paragraph_of_list = false; + if let Some((_, has_content)) = list_stack.last_mut() { + if *has_content { + is_subsequent_paragraph_of_list = true; + } else { + *has_content = true; + return; + } + } + + if !text.is_empty() { + if !text.ends_with('\n') { + text.push('\n'); + } + text.push('\n'); + } + for _ in 0..list_stack.len().saturating_sub(1) { + text.push_str(" "); + } + if is_subsequent_paragraph_of_list { + text.push_str(" "); + } +}