Cargo.lock 🔗
@@ -4408,6 +4408,7 @@ dependencies = [
"lsp2",
"parking_lot 0.11.2",
"postage",
+ "pulldown-cmark",
"rand 0.8.5",
"regex",
"rpc2",
Nathan Sobo created
Cargo.lock | 1
crates/editor2/src/display_map.rs | 1857
crates/editor2/src/display_map/fold_map.rs | 8
crates/editor2/src/display_map/inlay_map.rs | 2
crates/editor2/src/display_map/wrap_map.rs | 4
crates/editor2/src/editor.rs | 423
crates/editor2/src/editor_tests.rs | 16386 +++++++--------
crates/editor2/src/git.rs | 364
crates/editor2/src/highlight_matching_bracket.rs | 198
crates/editor2/src/hover_popover.rs | 1649
crates/editor2/src/inlay_hint_cache.rs | 4318 ++--
crates/editor2/src/items.rs | 11
crates/editor2/src/link_go_to_definition.rs | 1403
crates/editor2/src/mouse_context_menu.rs | 122
crates/editor2/src/movement.rs | 5
crates/editor2/src/scroll.rs | 63
crates/editor2/src/scroll/actions.rs | 2
crates/editor2/src/selections_collection.rs | 6
crates/editor2/src/test.rs | 9
crates/editor2/src/test/editor_lsp_test_context.rs | 2
crates/editor2/src/test/editor_test_context.rs | 343
crates/gpui2/src/text_system.rs | 2
crates/language2/Cargo.toml | 1
crates/language2/src/language2.rs | 1
crates/language2/src/markdown.rs | 301
25 files changed, 13,875 insertions(+), 13,606 deletions(-)
@@ -4408,6 +4408,7 @@ dependencies = [
"lsp2",
"parking_lot 0.11.2",
"postage",
+ "pulldown-cmark",
"rand 0.8.5",
"regex",
"rpc2",
@@ -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<MultiBuffer>,
@@ -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<MultiBuffer>, cx: &mut ModelContext<Self>) -> NonZeroU32 {
+ fn tab_size(buffer: &Model<MultiBuffer>, cx: &mut ModelContext<Self>) -> 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::<String>();
- 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::<SettingsStore, _, _>(|store, cx| {
- store.update_user_settings::<AllLanguageSettings>(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::<Vec<_>>();
- 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::<String>(),
- "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::<String>(),
- "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::<String>(),
- "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::<String>()
- .lines()
- .next(),
- Some(" b bbbbb")
- );
- assert_eq!(
- map.update(cx, |map, cx| map.snapshot(cx))
- .text_chunks(2)
- .collect::<String>()
- .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::<MyType>(),
- 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::<String>(),
- "✅ α\nβ \n🏀β γ"
- );
- assert_eq!(map.text_chunks(1).collect::<String>(), "β \n🏀β γ");
- assert_eq!(map.text_chunks(2).collect::<String>(), "🏀β γ");
-
- 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<u32>,
- map: &ModelHandle<DisplayMap>,
- theme: &'a SyntaxTheme,
- cx: &mut AppContext,
- ) -> Vec<(String, Option<Color>)> {
- chunks(rows, map, theme, cx)
- .into_iter()
- .map(|(text, color, _)| (text, color))
- .collect()
- }
-
- fn chunks<'a>(
- rows: Range<u32>,
- map: &ModelHandle<DisplayMap>,
- theme: &'a SyntaxTheme,
- cx: &mut AppContext,
- ) -> Vec<(String, Option<Color>, Option<Color>)> {
- let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
- let mut chunks: Vec<(String, Option<Color>, Option<Color>)> = 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::<SettingsStore, _, _>(|store, cx| {
- store.update_user_settings::<AllLanguageSettings>(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::<String>();
+// 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::<SettingsStore, _, _>(|store, cx| {
+// store.update_user_settings::<AllLanguageSettings>(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::<Vec<_>>();
+// 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::<String>(),
+// "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::<String>(),
+// "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::<String>(),
+// "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::<String>()
+// .lines()
+// .next(),
+// Some(" b bbbbb")
+// );
+// assert_eq!(
+// map.update(cx, |map, cx| map.snapshot(cx))
+// .text_chunks(2)
+// .collect::<String>()
+// .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::<MyType>(),
+// 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::<String>(),
+// "✅ α\nβ \n🏀β γ"
+// );
+// assert_eq!(map.text_chunks(1).collect::<String>(), "β \n🏀β γ");
+// assert_eq!(map.text_chunks(2).collect::<String>(), "🏀β γ");
+
+// 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<u32>,
+// map: &Model<DisplayMap>,
+// theme: &'a SyntaxTheme,
+// cx: &mut AppContext,
+// ) -> Vec<(String, Option<Hsla>)> {
+// chunks(rows, map, theme, cx)
+// .into_iter()
+// .map(|(text, color, _)| (text, color))
+// .collect()
+// }
+
+// fn chunks<'a>(
+// rows: Range<u32>,
+// map: &Model<DisplayMap>,
+// theme: &'a SyntaxTheme,
+// cx: &mut AppContext,
+// ) -> Vec<(String, Option<Hsla>, Option<Hsla>)> {
+// let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
+// let mut chunks: Vec<(String, Option<Hsla>, Option<Hsla>)> = 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::<SettingsStore, _, _>(|store, cx| {
+// store.update_user_settings::<AllLanguageSettings>(cx, f);
+// });
+// }
+// }
@@ -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<Fold>,
pub inlay_snapshot: InlaySnapshot,
pub version: usize,
- pub ellipses_color: Option<Color>,
+ pub ellipses_color: Option<Hsla>,
}
impl FoldSnapshot {
@@ -959,7 +959,7 @@ pub struct FoldChunks<'a> {
inlay_offset: InlayOffset,
output_offset: usize,
max_output_offset: usize,
- ellipses_color: Option<Color>,
+ ellipses_color: Option<Hsla>,
}
impl<'a> Iterator for FoldChunks<'a> {
@@ -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::{
@@ -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<u32>,
wrap_width: Option<f32>,
background_task: Option<Task<()>>,
- font: (FontId, f32),
+ font: (FontId, Pixels),
}
#[derive(Clone)]
@@ -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<Tag: 'static>(
parsed: &language::ParsedMarkdown,
editor_style: &EditorStyle,
- workspace: Option<WeakViewHandle<Workspace>>,
+ workspace: Option<WeakView<Workspace>>,
cx: &mut ViewContext<Editor>,
) -> Text {
enum RenderedMarkdown {}
@@ -124,51 +123,55 @@ pub fn render_parsed_markdown<Tag: 'static>(
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::<Vec<_>>(),
- )
- .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::<Editor, _>(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)
+ 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::<Vec<_>>(),
+ // )
+ // .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::<Editor, _>(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.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::<Editor>(cx);
workspace::register_followable_item::<Editor>(cx);
@@ -571,7 +574,7 @@ pub enum SelectPhase {
Update {
position: DisplayPoint,
goal_column: u32,
- scroll_position: Vector2F,
+ scroll_position: Point<Pixels>,
},
End,
}
@@ -612,11 +615,11 @@ type CompletionId = usize;
type GetFieldEditorTheme = dyn Fn(&theme::Theme) -> theme::FieldEditor;
type OverrideTextStyle = dyn Fn(&EditorStyle) -> Option<HighlightStyle>;
-type BackgroundHighlight = (fn(&Theme) -> Color, Vec<Range<Anchor>>);
-type InlayBackgroundHighlight = (fn(&Theme) -> Color, Vec<InlayHighlight>);
+type BackgroundHighlight = (fn(&Theme) -> Hsla, Vec<Range<Anchor>>);
+type InlayBackgroundHighlight = (fn(&Theme) -> Hsla, Vec<InlayHighlight>);
pub struct Editor {
- handle: WeakViewHandle<Self>,
+ handle: WeakView<Self>,
buffer: Model<MultiBuffer>,
display_map: Model<DisplayMap>,
pub selections: SelectionsCollection,
@@ -648,7 +651,7 @@ pub struct Editor {
inlay_background_highlights: TreeMap<Option<TypeId>, InlayBackgroundHighlight>,
nav_history: Option<ItemNavHistory>,
context_menu: RwLock<Option<ContextMenu>>,
- mouse_context_menu: ViewHandle<context_menu::ContextMenu>,
+ mouse_context_menu: View<context_menu::ContextMenu>,
completion_tasks: Vec<(CompletionId, Task<Option<()>>)>,
next_completion_id: CompletionId,
available_code_actions: Option<(Model<Buffer>, Arc<[CodeAction]>)>,
@@ -659,7 +662,7 @@ pub struct Editor {
cursor_shape: CursorShape,
collapse_matches: bool,
autoindent_mode: Option<AutoindentMode>,
- workspace: Option<(WeakViewHandle<Workspace>, i64)>,
+ workspace: Option<(WeakView<Workspace>, i64)>,
keymap_context_layers: BTreeMap<TypeId, KeymapContext>,
input_enabled: bool,
read_only: bool,
@@ -672,7 +675,7 @@ pub struct Editor {
// inlay_hint_cache: InlayHintCache,
next_inlay_id: usize,
_subscriptions: Vec<Subscription>,
- pixel_position_of_newest_cursor: Option<Vector2F>,
+ pixel_position_of_newest_cursor: Option<Point<Pixels>>,
}
pub struct EditorSnapshot {
@@ -828,7 +831,7 @@ struct SnippetState {
pub struct RenameState {
pub range: Range<Anchor>,
pub old_name: Arc<str>,
- pub editor: ViewHandle<Editor>,
+ pub editor: View<Editor>,
block_id: BlockId,
}
@@ -915,7 +918,7 @@ impl ContextMenu {
&self,
cursor_position: DisplayPoint,
style: EditorStyle,
- workspace: Option<WeakViewHandle<Workspace>>,
+ workspace: Option<WeakView<Workspace>>,
cx: &mut ViewContext<Editor>,
) -> (DisplayPoint, AnyElement<Editor>) {
match self {
@@ -938,22 +941,14 @@ struct CompletionsMenu {
}
impl CompletionsMenu {
- fn select_first(
- &mut self,
- project: Option<&Model<Project>>,
- cx: &mut ViewContext<Editor>,
- ) {
+ fn select_first(&mut self, project: Option<&Model<Project>>, cx: &mut ViewContext<Editor>) {
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<Project>>,
- cx: &mut ViewContext<Editor>,
- ) {
+ fn select_prev(&mut self, project: Option<&Model<Project>>, cx: &mut ViewContext<Editor>) {
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<Project>>,
- cx: &mut ViewContext<Editor>,
- ) {
+ fn select_next(&mut self, project: Option<&Model<Project>>, cx: &mut ViewContext<Editor>) {
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<Project>>,
- cx: &mut ViewContext<Editor>,
- ) {
+ fn select_last(&mut self, project: Option<&Model<Project>>, cx: &mut ViewContext<Editor>) {
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<WeakViewHandle<Workspace>>,
+ workspace: Option<WeakView<Workspace>>,
cx: &mut ViewContext<Editor>,
) -> AnyElement<Editor> {
enum CompletionTag {}
@@ -1760,7 +1747,7 @@ pub struct NavigationData {
scroll_top_row: u32,
}
-pub struct EditorCreated(pub ViewHandle<Editor>);
+pub struct EditorCreated(pub View<Editor>);
enum GotoDefinitionKind {
Symbol,
@@ -3845,8 +3832,8 @@ impl InlayHintRefreshReason {
// }
// async fn open_project_transaction(
-// this: &WeakViewHandle<Editor>,
-// workspace: WeakViewHandle<Workspace>,
+// this: &WeakViewHandle<Editor
+// workspace: WeakViewHandle<Workspace
// transaction: ProjectTransaction,
// title: String,
// mut cx: AsyncAppContext,
@@ -9237,7 +9224,7 @@ impl EditorSnapshot {
self.placeholder_text.as_ref()
}
- pub fn scroll_position(&self) -> Vector2F {
+ pub fn scroll_position(&self) -> Point<Pixels> {
self.scroll_anchor.scroll_position(&self.display_snapshot)
}
}
@@ -9286,9 +9273,9 @@ pub enum Event {
Closed,
}
-pub struct EditorFocused(pub ViewHandle<Editor>);
-pub struct EditorBlurred(pub ViewHandle<Editor>);
-pub struct EditorReleased(pub WeakViewHandle<Editor>);
+pub struct EditorFocused(pub View<Editor>);
+pub struct EditorBlurred(pub View<Editor>);
+pub struct EditorReleased(pub WeakView<Editor>);
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<Self>) {
+ fn focus_in(&mut self, focused: AnyView, cx: &mut ViewContext<Self>) {
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<Self>) {
+ fn focus_out(&mut self, _: AnyView, cx: &mut ViewContext<Self>) {
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();
@@ -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::<Vec<_>>(),
- snapshot.folds_in_range(0..text.len()).collect::<Vec<_>>(),
- );
- assert_set_eq!(
- cloned_editor.read_with(cx, |editor, cx| editor.selections.ranges::<Point>(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::<Workspace>::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<NavigationEntry> {
- 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::<Point>(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::<Point>(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::<Point>(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::<Point>(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::<Point>(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::<Point>(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::<Point>(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::<Point>(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#"
- <body>ˇ
- <script>
- var x = 1;ˇ
- </script>
- </body>ˇ
- "#
- .unindent(),
- );
-
- // Precondition: different languages are active at different locations.
- cx.update_editor(|editor, cx| {
- let snapshot = editor.snapshot(cx);
- let cursors = editor.selections.ranges::<usize>(cx);
- let languages = cursors
- .iter()
- .map(|c| snapshot.language_at(c.start).unwrap().name())
- .collect::<Vec<_>>();
- 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#"
- <body><aˇ>
- <script>
- var x = 1;<aˇ
- </script>
- </body><aˇ>
- "#
- .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#"
- <body><a b={c(ˇ)}>
- <script>
- var x = 1;<a b={c(ˇ)}
- </script>
- </body><a b={c(ˇ)}>
- "#
- .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#"
- <body><a b={c()d}ˇ>
- <script>
- var x = 1;<a b={c()d}ˇ
- </script>
- </body><a b={c()d}ˇ>
- "#
- .unindent(),
- );
- cx.update_editor(|editor, cx| {
- editor.handle_input(">", cx);
- });
- cx.assert_editor_state(
- &r#"
- <body><a b={c()d}>ˇ
- <script>
- var x = 1;<a b={c()d}>ˇ
- </script>
- </body><a b={c()d}>ˇ
- "#
- .unindent(),
- );
-
- // Reset
- cx.set_state(
- &r#"
- <body>ˇ
- <script>
- var x = 1;ˇ
- </script>
- </body>ˇ
- "#
- .unindent(),
- );
-
- cx.update_editor(|editor, cx| {
- editor.handle_input("<", cx);
- });
- cx.assert_editor_state(
- &r#"
- <body><ˇ>
- <script>
- var x = 1;<ˇ
- </script>
- </body><ˇ>
- "#
- .unindent(),
- );
-
- // When backspacing, the closing angle brackets are removed.
- cx.update_editor(|editor, cx| {
- editor.backspace(&Backspace, cx);
- });
- cx.assert_editor_state(
- &r#"
- <body>ˇ
- <script>
- var x = 1;ˇ
- </script>
- </body>ˇ
- "#
- .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#"
- <body>/*ˇ
- <script>
- var x = 1;/*ˇ */
- </script>
- </body>/*ˇ
- "#
- .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::<Point>(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::<Point>(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::<Point>(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<Editor>, 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::<usize>(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::<lsp::request::Formatting, _, _>(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::<lsp::request::Formatting, _, _>(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::<lsp::request::Formatting, _, _>(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::<lsp::request::RangeFormatting, _, _>(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::<lsp::request::RangeFormatting, _, _>(
- 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::<lsp::request::RangeFormatting, _, _>(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::<lsp::request::Formatting, _, _>(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::<lsp::request::Formatting, _, _>(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::<lsp::request::Formatting, _, _>(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::<lsp::notification::DidChangeTextDocument, _>({
- 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::<lsp::request::Formatting, _, _>({
- 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 <s|>
- 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 <si|>
- 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::<SettingsStore, _, _>(|settings, cx| {
- settings.update_user_settings::<EditorSettings>(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.<clo|>", 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(), " -->".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#"
- <p>A</p>ˇ
- <p>B</p>ˇ
- <p>C</p>ˇ
- "#
- .unindent(),
- );
- cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments::default(), cx));
- cx.assert_editor_state(
- &r#"
- <!-- <p>A</p>ˇ -->
- <!-- <p>B</p>ˇ -->
- <!-- <p>C</p>ˇ -->
- "#
- .unindent(),
- );
- cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments::default(), cx));
- cx.assert_editor_state(
- &r#"
- <p>A</p>ˇ
- <p>B</p>ˇ
- <p>C</p>ˇ
- "#
- .unindent(),
- );
-
- // Toggle comments for mixture of empty and non-empty selections, where
- // multiple selections occupy a given line.
- cx.set_state(
- &r#"
- <p>A«</p>
- <p>ˇ»B</p>ˇ
- <p>C«</p>
- <p>ˇ»D</p>ˇ
- "#
- .unindent(),
- );
-
- cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments::default(), cx));
- cx.assert_editor_state(
- &r#"
- <!-- <p>A«</p>
- <p>ˇ»B</p>ˇ -->
- <!-- <p>C«</p>
- <p>ˇ»D</p>ˇ -->
- "#
- .unindent(),
- );
- cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments::default(), cx));
- cx.assert_editor_state(
- &r#"
- <p>A«</p>
- <p>ˇ»B</p>ˇ
- <p>C«</p>
- <p>ˇ»D</p>ˇ
- "#
- .unindent(),
- );
-
- // Toggle comments when different languages are active for different
- // selections.
- cx.set_state(
- &r#"
- ˇ<script>
- ˇvar x = new Y();
- ˇ</script>
- "#
- .unindent(),
- );
- cx.foreground().run_until_parked();
- cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments::default(), cx));
- cx.assert_editor_state(
- &r#"
- <!-- ˇ<script> -->
- // ˇvar x = new Y();
- <!-- ˇ</script> -->
- "#
- .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<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
-
- editor.highlight_background::<Type1>(
- 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::<Type2>(
- 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<Deterministic>,
- 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<Deterministic>, 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<Deterministic>, 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<Deterministic>,
- 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<Deterministic>,
- 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<Deterministic>,
- 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::<copilot::request::GetCompletions, _, _>(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::<Editor>()
- .unwrap();
-
- fake_server.handle_request::<lsp::request::OnTypeFormatting, _, _>(|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<str> = "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::<lsp::request::Shutdown, _, _>(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::<lsp::request::Completion, _, _>(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::<lsp::request::ResolveCompletionItem, _, _>(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::<lsp::request::Completion, _, _>(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#"<p class="bgˇ" />"#);
-
- // 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::<Vec<_>>(),
- &["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::<Vec<_>>(),
- &["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#"<p class="yelˇ" />"#);
- 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::<Vec<_>>(),
- &["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<DisplayPoint> {
- 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<Editor>) {
- 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<Output = ()> {
- 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::<lsp::request::Completion, _, _>(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<Vec<(&'static str, &'static str)>>,
-) -> impl Future<Output = ()> {
- 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::<Vec<_>>()
- });
-
- let mut request =
- cx.handle_request::<lsp::request::ResolveCompletionItem, _, _>(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<copilot::request::Completion>,
- completions_cycling: Vec<copilot::request::Completion>,
-) {
- lsp.handle_request::<copilot::request::GetCompletions, _, _>(move |_params, _cx| {
- let completions = completions.clone();
- async move {
- Ok(copilot::request::GetCompletionsResult {
- completions: completions.clone(),
- })
- }
- });
- lsp.handle_request::<copilot::request::GetCompletionsCycling, _, _>(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::<SettingsStore, _, _>(|store, cx| {
- store.update_user_settings::<AllLanguageSettings>(cx, f);
- });
- });
-}
-
-pub(crate) fn update_test_project_settings(
- cx: &mut TestAppContext,
- f: impl Fn(&mut ProjectSettings),
-) {
- cx.update(|cx| {
- cx.update_global::<SettingsStore, _, _>(|store, cx| {
- store.update_user_settings::<ProjectSettings>(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<Pixels>::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<Pixels>::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<Pixels>::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<Pixels>::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<Pixels>::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<Pixels>::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::<Vec<_>>(),
+// snapshot.folds_in_range(0..text.len()).collect::<Vec<_>>(),
+// );
+// assert_set_eq!(
+// cloned_editor.read_with(cx, |editor, cx| editor.selections.ranges::<Point>(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::<Workspace>::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<NavigationEntry> {
+// 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<Pixels>::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<Pixels>::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<Pixels>::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<Pixels>::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::<Point>(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::<Point>(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::<Point>(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::<Point>(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::<Point>(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::<Point>(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::<Point>(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::<Point>(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#"
+// <body>ˇ
+// <script>
+// var x = 1;ˇ
+// </script>
+// </body>ˇ
+// "#
+// .unindent(),
+// );
+
+// // Precondition: different languages are active at different locations.
+// cx.update_editor(|editor, cx| {
+// let snapshot = editor.snapshot(cx);
+// let cursors = editor.selections.ranges::<usize>(cx);
+// let languages = cursors
+// .iter()
+// .map(|c| snapshot.language_at(c.start).unwrap().name())
+// .collect::<Vec<_>>();
+// 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#"
+// <body><aˇ>
+// <script>
+// var x = 1;<aˇ
+// </script>
+// </body><aˇ>
+// "#
+// .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#"
+// <body><a b={c(ˇ)}>
+// <script>
+// var x = 1;<a b={c(ˇ)}
+// </script>
+// </body><a b={c(ˇ)}>
+// "#
+// .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#"
+// <body><a b={c()d}ˇ>
+// <script>
+// var x = 1;<a b={c()d}ˇ
+// </script>
+// </body><a b={c()d}ˇ>
+// "#
+// .unindent(),
+// );
+// cx.update_editor(|editor, cx| {
+// editor.handle_input(">", cx);
+// });
+// cx.assert_editor_state(
+// &r#"
+// <body><a b={c()d}>ˇ
+// <script>
+// var x = 1;<a b={c()d}>ˇ
+// </script>
+// </body><a b={c()d}>ˇ
+// "#
+// .unindent(),
+// );
+
+// // Reset
+// cx.set_state(
+// &r#"
+// <body>ˇ
+// <script>
+// var x = 1;ˇ
+// </script>
+// </body>ˇ
+// "#
+// .unindent(),
+// );
+
+// cx.update_editor(|editor, cx| {
+// editor.handle_input("<", cx);
+// });
+// cx.assert_editor_state(
+// &r#"
+// <body><ˇ>
+// <script>
+// var x = 1;<ˇ
+// </script>
+// </body><ˇ>
+// "#
+// .unindent(),
+// );
+
+// // When backspacing, the closing angle brackets are removed.
+// cx.update_editor(|editor, cx| {
+// editor.backspace(&Backspace, cx);
+// });
+// cx.assert_editor_state(
+// &r#"
+// <body>ˇ
+// <script>
+// var x = 1;ˇ
+// </script>
+// </body>ˇ
+// "#
+// .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#"
+// <body>/*ˇ
+// <script>
+// var x = 1;/*ˇ */
+// </script>
+// </body>/*ˇ
+// "#
+// .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::<Point>(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::<Point>(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::<Point>(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<Editor>, 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::<usize>(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::<lsp::request::Formatting, _, _>(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::<lsp::request::Formatting, _, _>(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::<lsp::request::Formatting, _, _>(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::<lsp::request::RangeFormatting, _, _>(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::<lsp::request::RangeFormatting, _, _>(
+// 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::<lsp::request::RangeFormatting, _, _>(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::<lsp::request::Formatting, _, _>(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::<lsp::request::Formatting, _, _>(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::<lsp::request::Formatting, _, _>(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::<lsp::notification::DidChangeTextDocument, _>({
+// 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::<lsp::request::Formatting, _, _>({
+// 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 <s|>
+// 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 <si|>
+// 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::<SettingsStore, _, _>(|settings, cx| {
+// settings.update_user_settings::<EditorSettings>(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.<clo|>", 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(), " -->".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#"
+// <p>A</p>ˇ
+// <p>B</p>ˇ
+// <p>C</p>ˇ
+// "#
+// .unindent(),
+// );
+// cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments::default(), cx));
+// cx.assert_editor_state(
+// &r#"
+// <!-- <p>A</p>ˇ -->
+// <!-- <p>B</p>ˇ -->
+// <!-- <p>C</p>ˇ -->
+// "#
+// .unindent(),
+// );
+// cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments::default(), cx));
+// cx.assert_editor_state(
+// &r#"
+// <p>A</p>ˇ
+// <p>B</p>ˇ
+// <p>C</p>ˇ
+// "#
+// .unindent(),
+// );
+
+// // Toggle comments for mixture of empty and non-empty selections, where
+// // multiple selections occupy a given line.
+// cx.set_state(
+// &r#"
+// <p>A«</p>
+// <p>ˇ»B</p>ˇ
+// <p>C«</p>
+// <p>ˇ»D</p>ˇ
+// "#
+// .unindent(),
+// );
+
+// cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments::default(), cx));
+// cx.assert_editor_state(
+// &r#"
+// <!-- <p>A«</p>
+// <p>ˇ»B</p>ˇ -->
+// <!-- <p>C«</p>
+// <p>ˇ»D</p>ˇ -->
+// "#
+// .unindent(),
+// );
+// cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments::default(), cx));
+// cx.assert_editor_state(
+// &r#"
+// <p>A«</p>
+// <p>ˇ»B</p>ˇ
+// <p>C«</p>
+// <p>ˇ»D</p>ˇ
+// "#
+// .unindent(),
+// );
+
+// // Toggle comments when different languages are active for different
+// // selections.
+// cx.set_state(
+// &r#"
+// ˇ<script>
+// ˇvar x = new Y();
+// ˇ</script>
+// "#
+// .unindent(),
+// );
+// cx.foreground().run_until_parked();
+// cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments::default(), cx));
+// cx.assert_editor_state(
+// &r#"
+// <!-- ˇ<script> -->
+// // ˇvar x = new Y();
+// <!-- ˇ</script> -->
+// "#
+// .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<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
+
+// editor.highlight_background::<Type1>(
+// 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::<Type2>(
+// 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<Deterministic>,
+// 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<Deterministic>, 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<Deterministic>, 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<Deterministic>,
+// 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<Deterministic>,
+// 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<Deterministic>,
+// 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::<copilot::request::GetCompletions, _, _>(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::<Editor>()
+// .unwrap();
+
+// fake_server.handle_request::<lsp::request::OnTypeFormatting, _, _>(|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<str> = "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::<lsp::request::Shutdown, _, _>(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::<lsp::request::Completion, _, _>(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::<lsp::request::ResolveCompletionItem, _, _>(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::<lsp::request::Completion, _, _>(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#"<p class="bgˇ" />"#);
+
+// // 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::<Vec<_>>(),
+// &["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::<Vec<_>>(),
+// &["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#"<p class="yelˇ" />"#);
+// 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::<Vec<_>>(),
+// &["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<DisplayPoint> {
+// 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<Editor>) {
+// 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<Output = ()> {
+// 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::<lsp::request::Completion, _, _>(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<Vec<(&'static str, &'static str)>>,
+// ) -> impl Future<Output = ()> {
+// 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::<Vec<_>>()
+// });
+
+// let mut request =
+// cx.handle_request::<lsp::request::ResolveCompletionItem, _, _>(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<copilot::request::Completion>,
+// completions_cycling: Vec<copilot::request::Completion>,
+// ) {
+// lsp.handle_request::<copilot::request::GetCompletions, _, _>(move |_params, _cx| {
+// let completions = completions.clone();
+// async move {
+// Ok(copilot::request::GetCompletionsResult {
+// completions: completions.clone(),
+// })
+// }
+// });
+// lsp.handle_request::<copilot::request::GetCompletionsCycling, _, _>(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::<SettingsStore, _, _>(|store, cx| {
+// store.update_user_settings::<AllLanguageSettings>(cx, f);
+// });
+// });
+// }
+
+// pub(crate) fn update_test_project_settings(
+// cx: &mut TestAppContext,
+// f: impl Fn(&mut ProjectSettings),
+// ) {
+// cx.update(|cx| {
+// cx.update_global::<SettingsStore, _, _>(|store, cx| {
+// store.update_user_settings::<ProjectSettings>(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);
+// }
@@ -88,195 +88,195 @@ pub fn diff_hunk_to_display(hunk: DiffHunk<u32>, 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::<Vec<_>>(),
- &expected,
- );
+// assert_eq!(
+// snapshot
+// .git_diff_hunks_in_range(0..12)
+// .map(|hunk| (hunk.status(), hunk.buffer_range))
+// .collect::<Vec<_>>(),
+// &expected,
+// );
- assert_eq!(
- snapshot
- .git_diff_hunks_in_range_rev(0..12)
- .map(|hunk| (hunk.status(), hunk.buffer_range))
- .collect::<Vec<_>>(),
- expected
- .iter()
- .rev()
- .cloned()
- .collect::<Vec<_>>()
- .as_slice(),
- );
- }
-}
+// assert_eq!(
+// snapshot
+// .git_diff_hunks_in_range_rev(0..12)
+// .map(|hunk| (hunk.status(), hunk.buffer_range))
+// .collect::<Vec<_>>(),
+// expected
+// .iter()
+// .rev()
+// .cloned()
+// .collect::<Vec<_>>()
+// .as_slice(),
+// );
+// }
+// }
@@ -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::<MatchingBracketHighlight>(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::<MatchingBracketHighlight>(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::<MatchingBracketHighlight>(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::<MatchingBracketHighlight>(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::<MatchingBracketHighlight>(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::<MatchingBracketHighlight>(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::<MatchingBracketHighlight>(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::<MatchingBracketHighlight>(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::<MatchingBracketHighlight>(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::<MatchingBracketHighlight>(indoc! {r#"
+// pub fn test("Test argument") {
+// another_test(1, 2, 3);
+// }
+// "#});
+// }
+// }
@@ -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<Editor>) {
- 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<Editor>) {
+// 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<DisplayPoint>, cx: &mut ViewContext<Editor>) {
- if settings::get::<EditorSettings>(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<Editor>) {
- if settings::get::<EditorSettings>(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<u32>,
- workspace: Option<WeakViewHandle<Workspace>>,
+ workspace: Option<WeakView<Workspace>>,
cx: &mut ViewContext<Editor>,
) -> Option<(DisplayPoint, Vec<AnyElement<Editor>>)> {
// 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<Project>,
+ pub project: Model<Project>,
symbol_range: RangeInEditor,
pub blocks: Vec<HoverBlock>,
parsed_content: ParsedMarkdown,
@@ -472,7 +470,7 @@ impl InfoPopover {
pub fn render(
&mut self,
style: &EditorStyle,
- workspace: Option<WeakViewHandle<Workspace>>,
+ workspace: Option<WeakView<Workspace>>,
cx: &mut ViewContext<Editor>,
) -> AnyElement<Editor> {
MouseEventHandler::new::<InfoPopover, _>(0, cx, |_, cx| {
@@ -506,55 +504,56 @@ pub struct DiagnosticPopover {
impl DiagnosticPopover {
pub fn render(&self, style: &EditorStyle, cx: &mut ViewContext<Editor>) -> AnyElement<Editor> {
- 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 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),
- };
-
- 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();
-
- MouseEventHandler::new::<DiagnosticPopover, _>(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::<PrimaryDiagnostic>(
- 0,
- "Go To Diagnostic".to_string(),
- Some(Box::new(crate::GoToDiagnostic)),
- tooltip_style,
- cx,
- )
- .into_any()
+ 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 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),
+ // };
+
+ // 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();
+
+ // MouseEventHandler::new::<DiagnosticPopover, _>(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::<PrimaryDiagnostic>(
+ // 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::<lsp::request::HoverRequest, _, _>(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::<lsp::request::HoverRequest, _, _>(|_, _| 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::<lsp::request::HoverRequest, _, _>(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::<lsp::request::HoverRequest, _, _>(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::<lsp::request::HoverRequest, _, _>(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::<lsp::request::HoverRequest, _, _>(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<HoverBlock>,
- expected_marked_text: String,
- expected_styles: Vec<HighlightStyle>,
- }
-
- 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 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
- - 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::<Vec<_>>();
- 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>(T);
-
- fn main() {
- let variableˇ = TestNewType(TestStruct);
- }
- "});
-
- let hint_start_offset = cx.ranges(indoc! {"
- struct TestStruct;
-
- // ==================
-
- struct TestNewType<T>(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>(T);
-
- fn main() {
- let variable = TestNewType(TestStruct);
- }
- "});
- let struct_target_range = cx.lsp_range(indoc! {"
- struct «TestStruct»;
-
- // ==================
-
- struct TestNewType<T>(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<TestStruct>";
- let closure_uri = uri.clone();
- cx.lsp
- .handle_request::<lsp::request::InlayHintRequest, _, _>(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>(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::<lsp::request::InlayHintResolveRequest, _, _>(
- 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<TestStruct>`
- 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::<lsp::request::HoverRequest, _, _>(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::<lsp::request::HoverRequest, _, _>(|_, _| 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::<lsp::request::HoverRequest, _, _>(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::<lsp::request::HoverRequest, _, _>(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::<lsp::request::HoverRequest, _, _>(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::<lsp::request::HoverRequest, _, _>(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<HoverBlock>,
+// expected_marked_text: String,
+// expected_styles: Vec<HighlightStyle>,
+// }
+
+// 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 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
+// - 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::<Vec<_>>();
+// 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>(T);
+
+// fn main() {
+// let variableˇ = TestNewType(TestStruct);
+// }
+// "});
+
+// let hint_start_offset = cx.ranges(indoc! {"
+// struct TestStruct;
+
+// // ==================
+
+// struct TestNewType<T>(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>(T);
+
+// fn main() {
+// let variable = TestNewType(TestStruct);
+// }
+// "});
+// let struct_target_range = cx.lsp_range(indoc! {"
+// struct «TestStruct»;
+
+// // ==================
+
+// struct TestNewType<T>(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<TestStruct>";
+// let closure_uri = uri.clone();
+// cx.lsp
+// .handle_request::<lsp::request::InlayHintRequest, _, _>(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>(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::<lsp::request::InlayHintResolveRequest, _, _>(
+// 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<TestStruct>`
+// 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"
+// );
+// });
+// }
+// }
@@ -250,7 +250,7 @@ impl InlayHintCache {
pub fn update_settings(
&mut self,
- multi_buffer: &ModelHandle<MultiBuffer>,
+ multi_buffer: &Model<MultiBuffer>,
new_hint_settings: InlayHintSettings,
visible_hints: Vec<Inlay>,
cx: &mut ViewContext<Editor>,
@@ -302,7 +302,7 @@ impl InlayHintCache {
pub fn spawn_hint_refresh(
&mut self,
reason: &'static str,
- excerpts_to_query: HashMap<ExcerptId, (ModelHandle<Buffer>, Global, Range<usize>)>,
+ excerpts_to_query: HashMap<ExcerptId, (Model<Buffer>, Global, Range<usize>)>,
invalidate: InvalidationStrategy,
cx: &mut ViewContext<Editor>,
) -> Option<InlaySplice> {
@@ -355,7 +355,7 @@ impl InlayHintCache {
fn new_allowed_hint_kinds_splice(
&self,
- multi_buffer: &ModelHandle<MultiBuffer>,
+ multi_buffer: &Model<MultiBuffer>,
visible_hints: &[Inlay],
new_kinds: &HashSet<Option<InlayHintKind>>,
cx: &mut ViewContext<Editor>,
@@ -579,7 +579,7 @@ impl InlayHintCache {
fn spawn_new_update_tasks(
editor: &mut Editor,
reason: &'static str,
- excerpts_to_query: HashMap<ExcerptId, (ModelHandle<Buffer>, Global, Range<usize>)>,
+ excerpts_to_query: HashMap<ExcerptId, (Model<Buffer>, Global, Range<usize>)>,
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<Buffer>,
+ excerpt_buffer: &Model<Buffer>,
excerpt_visible_range: Range<usize>,
cx: &mut ModelContext<'_, MultiBuffer>,
) -> Option<QueryRanges> {
@@ -837,7 +837,7 @@ fn new_update_task(
}
async fn fetch_and_update_hints(
- editor: gpui::WeakViewHandle<Editor>,
+ editor: gpui::WeakView<Editor>,
multi_buffer_snapshot: MultiBufferSnapshot,
buffer_snapshot: BufferSnapshot,
visible_hints: Arc<Vec<Inlay>>,
@@ -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::<lsp::request::InlayHintRequest, _, _>(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::<lsp::request::InlayHintRefreshRequest>(())
- .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::<lsp::request::InlayHintRequest, _, _>(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::request::WorkDoneProgressCreate>(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::notification::Progress>(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::notification::Progress>(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::<Editor>()
- .unwrap();
- let rs_lsp_request_count = Arc::new(AtomicU32::new(0));
- rs_fake_server
- .handle_request::<lsp::request::InlayHintRequest, _, _>(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::<Editor>()
- .unwrap();
- let md_lsp_request_count = Arc::new(AtomicU32::new(0));
- md_fake_server
- .handle_request::<lsp::request::InlayHintRequest, _, _>(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::<lsp::request::InlayHintRequest, _, _>(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::<lsp::request::InlayHintRefreshRequest>(())
- .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::<lsp::request::InlayHintRefreshRequest>(())
- .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::<lsp::request::InlayHintRefreshRequest>(())
- .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::<lsp::request::InlayHintRequest, _, _>(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::<Editor>()
- .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::<lsp::request::InlayHintRequest, _, _>(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<Editor>,
- cx: &mut gpui::TestAppContext,
- ) -> Range<Point> {
- 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::<Vec<_>>();
- 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::<Vec<_>>();
- 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::<Vec<_>>();
- 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::<Vec<_>>();
- 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<Deterministic>,
- 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::<Vec<_>>().join("")),
- "other.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|j| format!("let j = {j};\n")).collect::<Vec<_>>().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::<lsp::request::InlayHintRequest, _, _>(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<Deterministic>,
- 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::<Vec<_>>().join("")),
- "other.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|j| format!("let j = {j};\n")).collect::<Vec<_>>().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::<lsp::request::InlayHintRequest, _, _>(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::<Editor>()
- .unwrap();
- let lsp_request_count = Arc::new(AtomicU32::new(0));
- let closure_lsp_request_count = Arc::clone(&lsp_request_count);
- fake_server
- .handle_request::<lsp::request::InlayHintRequest, _, _>(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::<lsp::request::InlayHintRequest, _, _>(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<Editor>, 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::<Editor>()
- .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<String> {
- 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<String> {
- let mut hints = editor
- .visible_inlay_hints(cx)
- .into_iter()
- .map(|hint| hint.text.to_string())
- .collect::<Vec<_>>();
- 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::<lsp::request::InlayHintRequest, _, _>(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::<lsp::request::InlayHintRefreshRequest>(())
+// .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::<lsp::request::InlayHintRequest, _, _>(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::request::WorkDoneProgressCreate>(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::notification::Progress>(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::notification::Progress>(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::<Editor>()
+// .unwrap();
+// let rs_lsp_request_count = Arc::new(AtomicU32::new(0));
+// rs_fake_server
+// .handle_request::<lsp::request::InlayHintRequest, _, _>(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::<Editor>()
+// .unwrap();
+// let md_lsp_request_count = Arc::new(AtomicU32::new(0));
+// md_fake_server
+// .handle_request::<lsp::request::InlayHintRequest, _, _>(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::<lsp::request::InlayHintRequest, _, _>(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::<lsp::request::InlayHintRefreshRequest>(())
+// .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::<lsp::request::InlayHintRefreshRequest>(())
+// .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::<lsp::request::InlayHintRefreshRequest>(())
+// .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::<lsp::request::InlayHintRequest, _, _>(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::<Editor>()
+// .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::<lsp::request::InlayHintRequest, _, _>(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<Editor>,
+// cx: &mut gpui::TestAppContext,
+// ) -> Range<Point> {
+// 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::<Vec<_>>();
+// 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::<Vec<_>>();
+// 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::<Vec<_>>();
+// 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::<Vec<_>>();
+// 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<Deterministic>,
+// 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::<Vec<_>>().join("")),
+// "other.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|j| format!("let j = {j};\n")).collect::<Vec<_>>().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::<lsp::request::InlayHintRequest, _, _>(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<Deterministic>,
+// 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::<Vec<_>>().join("")),
+// "other.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|j| format!("let j = {j};\n")).collect::<Vec<_>>().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::<lsp::request::InlayHintRequest, _, _>(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::<Editor>()
+// .unwrap();
+// let lsp_request_count = Arc::new(AtomicU32::new(0));
+// let closure_lsp_request_count = Arc::clone(&lsp_request_count);
+// fake_server
+// .handle_request::<lsp::request::InlayHintRequest, _, _>(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::<lsp::request::InlayHintRequest, _, _>(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<Editor>, 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::<Editor>()
+// .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<String> {
+// 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<String> {
+// let mut hints = editor
+// .visible_inlay_hints(cx)
+// .into_iter()
+// .map(|hint| hint.text.to_string())
+// .collect::<Vec<_>>();
+// hints.sort();
+// hints
+// }
+// }
@@ -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<Self>) {
@@ -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())
- }
- });
-
- match highlight_range {
- RangeInEditor::Text(text_range) => this
- .highlight_text::<LinkGoToDefinitionState>(
- vec![text_range],
- style,
- cx,
- ),
- RangeInEditor::Inlay(highlight) => this
- .highlight_inlays::<LinkGoToDefinitionState>(
- vec![highlight],
- style,
- cx,
- ),
- }
+ // 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::<LinkGoToDefinitionState>(
+ // vec![text_range],
+ // style,
+ // cx,
+ // ),
+ // RangeInEditor::Inlay(highlight) => this
+ // .highlight_inlays::<LinkGoToDefinitionState>(
+ // 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;
-
- #[gpui::test]
- async fn test_link_go_to_type_definition(cx: &mut gpui::TestAppContext) {
- init_test(cx, |_| {});
-
- let mut cx = EditorLspTestContext::new_rust(
- lsp::ServerCapabilities {
- hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
- type_definition_provider: Some(lsp::TypeDefinitionProviderCapability::Simple(true)),
- ..Default::default()
- },
- cx,
- )
- .await;
-
- cx.set_state(indoc! {"
- struct A;
- let vˇariable = A;
- "});
-
- // Basic hold cmd+shift, expect highlight in region if response contains type definition
- let hover_point = cx.display_point(indoc! {"
- struct A;
- let vˇariable = A;
- "});
- let symbol_range = cx.lsp_range(indoc! {"
- struct A;
- let «variable» = A;
- "});
- let target_range = cx.lsp_range(indoc! {"
- struct «A»;
- let variable = A;
- "});
-
- let mut requests =
- cx.handle_request::<GotoTypeDefinition, _, _>(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::<LinkGoToDefinitionState>(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::<LinkGoToDefinitionState>(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::<GotoTypeDefinition, _, _>(move |url, _, _| async move {
- Ok(Some(lsp::GotoTypeDefinitionResponse::Link(vec![
- lsp::LocationLink {
- origin_selection_range: None,
- target_uri: url,
- target_range,
- target_selection_range: target_range,
- },
- ])))
- });
-
- cx.update_editor(|editor, cx| {
- go_to_fetched_type_definition(editor, PointForPosition::valid(hover_point), false, cx);
- });
- requests.next().await;
- cx.foreground().run_until_parked();
-
- cx.assert_editor_state(indoc! {"
- struct «Aˇ»;
- let variable = A;
- "});
- }
-
- #[gpui::test]
- async fn test_link_go_to_definition(cx: &mut gpui::TestAppContext) {
- init_test(cx, |_| {});
-
- let mut cx = EditorLspTestContext::new_rust(
- lsp::ServerCapabilities {
- hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
- ..Default::default()
- },
- cx,
- )
- .await;
-
- cx.set_state(indoc! {"
- fn ˇtest() { do_work(); }
- fn do_work() { test(); }
- "});
-
- // Basic hold cmd, expect highlight in region if response contains definition
- let hover_point = cx.display_point(indoc! {"
- fn test() { do_wˇork(); }
- fn do_work() { test(); }
- "});
- let symbol_range = cx.lsp_range(indoc! {"
- fn test() { «do_work»(); }
- fn do_work() { test(); }
- "});
- let target_range = cx.lsp_range(indoc! {"
- fn test() { do_work(); }
- fn «do_work»() { test(); }
- "});
-
- let mut requests = cx.handle_request::<GotoDefinition, _, _>(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::<LinkGoToDefinitionState>(indoc! {"
- fn test() { «do_work»(); }
- fn do_work() { test(); }
- "});
-
- // Unpress cmd causes highlight to go away
- cx.update_editor(|editor, cx| {
- editor.modifiers_changed(&Default::default(), cx);
- });
-
- // Assert no link highlights
- cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(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::<GotoDefinition, _, _>(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::<LinkGoToDefinitionState>(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::<GotoDefinition, _, _>(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::<LinkGoToDefinitionState>(indoc! {"
- fn test() { do_work(); }
- fn do_work() { test(); }
- "});
-
- // Move mouse without cmd and then pressing cmd triggers highlight
- let hover_point = cx.display_point(indoc! {"
- fn test() { do_work(); }
- fn do_work() { teˇst(); }
- "});
- cx.update_editor(|editor, cx| {
- update_go_to_definition_link(
- editor,
- Some(GoToDefinitionTrigger::Text(hover_point)),
- false,
- false,
- cx,
- );
- });
- cx.foreground().run_until_parked();
-
- // Assert no link highlights
- cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(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::<GotoDefinition, _, _>(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::<LinkGoToDefinitionState>(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::<LinkGoToDefinitionState>(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::<LinkGoToDefinitionState>(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::<LinkGoToDefinitionState>(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::<GotoDefinition, _, _>(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::<LinkGoToDefinitionState>(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::<GotoDefinition, _, _>(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::<GotoDefinition, _, _>(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::<LinkGoToDefinitionState>(indoc! {"
- fn test() { do_work(); }
- fn do_work() { test(); }
- "});
- cx.foreground().run_until_parked();
- }
-
- #[gpui::test]
- async fn test_link_go_to_inlay(cx: &mut gpui::TestAppContext) {
- init_test(cx, |settings| {
- settings.defaults.inlay_hints = Some(InlayHintSettings {
- enabled: true,
- show_type_hints: true,
- show_parameter_hints: true,
- show_other_hints: true,
- })
- });
-
- let mut cx = EditorLspTestContext::new_rust(
- lsp::ServerCapabilities {
- inlay_hint_provider: Some(lsp::OneOf::Left(true)),
- ..Default::default()
- },
- cx,
- )
- .await;
- cx.set_state(indoc! {"
- struct TestStruct;
-
- fn main() {
- let variableˇ = TestStruct;
- }
- "});
- let hint_start_offset = cx.ranges(indoc! {"
- struct TestStruct;
-
- fn main() {
- let variableˇ = TestStruct;
- }
- "})[0]
- .start;
- let hint_position = cx.to_lsp(hint_start_offset);
- let target_range = cx.lsp_range(indoc! {"
- struct «TestStruct»;
-
- fn main() {
- let variable = TestStruct;
- }
- "});
-
- let expected_uri = cx.buffer_lsp_url.clone();
- let hint_label = ": TestStruct";
- cx.lsp
- .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
- let expected_uri = expected_uri.clone();
- async move {
- assert_eq!(params.text_document.uri, expected_uri);
- Ok(Some(vec![lsp::InlayHint {
- position: hint_position,
- label: lsp::InlayHintLabel::LabelParts(vec![lsp::InlayHintLabelPart {
- value: hint_label.to_string(),
- location: Some(lsp::Location {
- uri: params.text_document.uri,
- range: target_range,
- }),
- ..Default::default()
- }]),
- kind: Some(lsp::InlayHintKind::TYPE),
- text_edits: None,
- tooltip: None,
- padding_left: Some(false),
- padding_right: Some(false),
- data: None,
- }]))
- }
- })
- .next()
- .await;
- cx.foreground().run_until_parked();
- cx.update_editor(|editor, cx| {
- let expected_layers = vec![hint_label.to_string()];
- assert_eq!(expected_layers, cached_hint_labels(editor));
- assert_eq!(expected_layers, visible_hint_labels(editor, cx));
- });
-
- let inlay_range = cx
- .ranges(indoc! {"
- struct TestStruct;
-
- fn main() {
- let variable« »= TestStruct;
- }
- "})
- .get(0)
- .cloned()
- .unwrap();
- let hint_hover_position = cx.update_editor(|editor, cx| {
- let snapshot = editor.snapshot(cx);
- let previous_valid = inlay_range.start.to_display_point(&snapshot);
- let next_valid = inlay_range.end.to_display_point(&snapshot);
- assert_eq!(previous_valid.row(), next_valid.row());
- assert!(previous_valid.column() < next_valid.column());
- let exact_unclipped = DisplayPoint::new(
- previous_valid.row(),
- previous_valid.column() + (hint_label.len() / 2) as u32,
- );
- PointForPosition {
- previous_valid,
- next_valid,
- exact_unclipped,
- column_overshoot_after_line_end: 0,
- }
- });
- // Press cmd to trigger highlight
- cx.update_editor(|editor, cx| {
- update_inlay_link_and_hover_points(
- &editor.snapshot(cx),
- hint_hover_position,
- editor,
- true,
- false,
- cx,
- );
- });
- cx.foreground().run_until_parked();
- cx.update_editor(|editor, cx| {
- let snapshot = editor.snapshot(cx);
- let actual_highlights = snapshot
- .inlay_highlights::<LinkGoToDefinitionState>()
- .into_iter()
- .flat_map(|highlights| highlights.values().map(|(_, highlight)| highlight))
- .collect::<Vec<_>>();
-
- 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::<LinkGoToDefinitionState>()
- .map(|ranges| ranges.as_ref().clone().1)
- .unwrap_or_default();
-
- assert!(actual_ranges.is_empty(), "When no cmd is pressed, should have no hint label selected, but got: {actual_ranges:?}");
- });
-
- // Cmd+click without existing definition requests and jumps
- cx.update_editor(|editor, cx| {
- editor.modifiers_changed(
- &platform::ModifiersChangedEvent {
- modifiers: Modifiers {
- cmd: true,
- ..Default::default()
- },
- ..Default::default()
- },
- cx,
- );
- update_inlay_link_and_hover_points(
- &editor.snapshot(cx),
- hint_hover_position,
- editor,
- true,
- false,
- cx,
- );
- });
- cx.foreground().run_until_parked();
- cx.update_editor(|editor, cx| {
- go_to_fetched_type_definition(editor, hint_hover_position, false, cx);
- });
- cx.foreground().run_until_parked();
- cx.assert_editor_state(indoc! {"
- struct «TestStructˇ»;
-
- fn main() {
- let variable = TestStruct;
- }
- "});
- }
-}
+// #[cfg(test)]
+// mod tests {
+// use super::*;
+// use crate::{
+// display_map::ToDisplayPoint,
+// editor_tests::init_test,
+// inlay_hint_cache::tests::{cached_hint_labels, visible_hint_labels},
+// test::editor_lsp_test_context::EditorLspTestContext,
+// };
+// use futures::StreamExt;
+// use gpui::{
+// platform::{self, Modifiers, ModifiersChangedEvent},
+// View,
+// };
+// use indoc::indoc;
+// use language::language_settings::InlayHintSettings;
+// use lsp::request::{GotoDefinition, GotoTypeDefinition};
+// use util::assert_set_eq;
+
+// #[gpui::test]
+// async fn test_link_go_to_type_definition(cx: &mut gpui::TestAppContext) {
+// init_test(cx, |_| {});
+
+// let mut cx = EditorLspTestContext::new_rust(
+// lsp::ServerCapabilities {
+// hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
+// type_definition_provider: Some(lsp::TypeDefinitionProviderCapability::Simple(true)),
+// ..Default::default()
+// },
+// cx,
+// )
+// .await;
+
+// cx.set_state(indoc! {"
+// struct A;
+// let vˇariable = A;
+// "});
+
+// // Basic hold cmd+shift, expect highlight in region if response contains type definition
+// let hover_point = cx.display_point(indoc! {"
+// struct A;
+// let vˇariable = A;
+// "});
+// let symbol_range = cx.lsp_range(indoc! {"
+// struct A;
+// let «variable» = A;
+// "});
+// let target_range = cx.lsp_range(indoc! {"
+// struct «A»;
+// let variable = A;
+// "});
+
+// let mut requests =
+// cx.handle_request::<GotoTypeDefinition, _, _>(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::<LinkGoToDefinitionState>(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::<LinkGoToDefinitionState>(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::<GotoTypeDefinition, _, _>(move |url, _, _| async move {
+// Ok(Some(lsp::GotoTypeDefinitionResponse::Link(vec![
+// lsp::LocationLink {
+// origin_selection_range: None,
+// target_uri: url,
+// target_range,
+// target_selection_range: target_range,
+// },
+// ])))
+// });
+
+// cx.update_editor(|editor, cx| {
+// go_to_fetched_type_definition(editor, PointForPosition::valid(hover_point), false, cx);
+// });
+// requests.next().await;
+// cx.foreground().run_until_parked();
+
+// cx.assert_editor_state(indoc! {"
+// struct «Aˇ»;
+// let variable = A;
+// "});
+// }
+
+// #[gpui::test]
+// async fn test_link_go_to_definition(cx: &mut gpui::TestAppContext) {
+// init_test(cx, |_| {});
+
+// let mut cx = EditorLspTestContext::new_rust(
+// lsp::ServerCapabilities {
+// hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
+// ..Default::default()
+// },
+// cx,
+// )
+// .await;
+
+// cx.set_state(indoc! {"
+// fn ˇtest() { do_work(); }
+// fn do_work() { test(); }
+// "});
+
+// // Basic hold cmd, expect highlight in region if response contains definition
+// let hover_point = cx.display_point(indoc! {"
+// fn test() { do_wˇork(); }
+// fn do_work() { test(); }
+// "});
+// let symbol_range = cx.lsp_range(indoc! {"
+// fn test() { «do_work»(); }
+// fn do_work() { test(); }
+// "});
+// let target_range = cx.lsp_range(indoc! {"
+// fn test() { do_work(); }
+// fn «do_work»() { test(); }
+// "});
+
+// let mut requests = cx.handle_request::<GotoDefinition, _, _>(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::<LinkGoToDefinitionState>(indoc! {"
+// fn test() { «do_work»(); }
+// fn do_work() { test(); }
+// "});
+
+// // Unpress cmd causes highlight to go away
+// cx.update_editor(|editor, cx| {
+// editor.modifiers_changed(&Default::default(), cx);
+// });
+
+// // Assert no link highlights
+// cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(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::<GotoDefinition, _, _>(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::<LinkGoToDefinitionState>(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::<GotoDefinition, _, _>(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::<LinkGoToDefinitionState>(indoc! {"
+// fn test() { do_work(); }
+// fn do_work() { test(); }
+// "});
+
+// // Move mouse without cmd and then pressing cmd triggers highlight
+// let hover_point = cx.display_point(indoc! {"
+// fn test() { do_work(); }
+// fn do_work() { teˇst(); }
+// "});
+// cx.update_editor(|editor, cx| {
+// update_go_to_definition_link(
+// editor,
+// Some(GoToDefinitionTrigger::Text(hover_point)),
+// false,
+// false,
+// cx,
+// );
+// });
+// cx.foreground().run_until_parked();
+
+// // Assert no link highlights
+// cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(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::<GotoDefinition, _, _>(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::<LinkGoToDefinitionState>(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::<LinkGoToDefinitionState>(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::<LinkGoToDefinitionState>(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::<LinkGoToDefinitionState>(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::<GotoDefinition, _, _>(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::<LinkGoToDefinitionState>(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::<GotoDefinition, _, _>(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::<GotoDefinition, _, _>(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::<LinkGoToDefinitionState>(indoc! {"
+// fn test() { do_work(); }
+// fn do_work() { test(); }
+// "});
+// cx.foreground().run_until_parked();
+// }
+
+// #[gpui::test]
+// async fn test_link_go_to_inlay(cx: &mut gpui::TestAppContext) {
+// init_test(cx, |settings| {
+// settings.defaults.inlay_hints = Some(InlayHintSettings {
+// enabled: true,
+// show_type_hints: true,
+// show_parameter_hints: true,
+// show_other_hints: true,
+// })
+// });
+
+// let mut cx = EditorLspTestContext::new_rust(
+// lsp::ServerCapabilities {
+// inlay_hint_provider: Some(lsp::OneOf::Left(true)),
+// ..Default::default()
+// },
+// cx,
+// )
+// .await;
+// cx.set_state(indoc! {"
+// struct TestStruct;
+
+// fn main() {
+// let variableˇ = TestStruct;
+// }
+// "});
+// let hint_start_offset = cx.ranges(indoc! {"
+// struct TestStruct;
+
+// fn main() {
+// let variableˇ = TestStruct;
+// }
+// "})[0]
+// .start;
+// let hint_position = cx.to_lsp(hint_start_offset);
+// let target_range = cx.lsp_range(indoc! {"
+// struct «TestStruct»;
+
+// fn main() {
+// let variable = TestStruct;
+// }
+// "});
+
+// let expected_uri = cx.buffer_lsp_url.clone();
+// let hint_label = ": TestStruct";
+// cx.lsp
+// .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
+// let expected_uri = expected_uri.clone();
+// async move {
+// assert_eq!(params.text_document.uri, expected_uri);
+// Ok(Some(vec![lsp::InlayHint {
+// position: hint_position,
+// label: lsp::InlayHintLabel::LabelParts(vec![lsp::InlayHintLabelPart {
+// value: hint_label.to_string(),
+// location: Some(lsp::Location {
+// uri: params.text_document.uri,
+// range: target_range,
+// }),
+// ..Default::default()
+// }]),
+// kind: Some(lsp::InlayHintKind::TYPE),
+// text_edits: None,
+// tooltip: None,
+// padding_left: Some(false),
+// padding_right: Some(false),
+// data: None,
+// }]))
+// }
+// })
+// .next()
+// .await;
+// cx.foreground().run_until_parked();
+// cx.update_editor(|editor, cx| {
+// let expected_layers = vec![hint_label.to_string()];
+// assert_eq!(expected_layers, cached_hint_labels(editor));
+// assert_eq!(expected_layers, visible_hint_labels(editor, cx));
+// });
+
+// let inlay_range = cx
+// .ranges(indoc! {"
+// struct TestStruct;
+
+// fn main() {
+// let variable« »= TestStruct;
+// }
+// "})
+// .get(0)
+// .cloned()
+// .unwrap();
+// let hint_hover_position = cx.update_editor(|editor, cx| {
+// let snapshot = editor.snapshot(cx);
+// let previous_valid = inlay_range.start.to_display_point(&snapshot);
+// let next_valid = inlay_range.end.to_display_point(&snapshot);
+// assert_eq!(previous_valid.row(), next_valid.row());
+// assert!(previous_valid.column() < next_valid.column());
+// let exact_unclipped = DisplayPoint::new(
+// previous_valid.row(),
+// previous_valid.column() + (hint_label.len() / 2) as u32,
+// );
+// PointForPosition {
+// previous_valid,
+// next_valid,
+// exact_unclipped,
+// column_overshoot_after_line_end: 0,
+// }
+// });
+// // Press cmd to trigger highlight
+// cx.update_editor(|editor, cx| {
+// update_inlay_link_and_hover_points(
+// &editor.snapshot(cx),
+// hint_hover_position,
+// editor,
+// true,
+// false,
+// cx,
+// );
+// });
+// cx.foreground().run_until_parked();
+// cx.update_editor(|editor, cx| {
+// let snapshot = editor.snapshot(cx);
+// let actual_highlights = snapshot
+// .inlay_highlights::<LinkGoToDefinitionState>()
+// .into_iter()
+// .flat_map(|highlights| highlights.values().map(|(_, highlight)| highlight))
+// .collect::<Vec<_>>();
+
+// 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::<LinkGoToDefinitionState>()
+// .map(|ranges| ranges.as_ref().clone().1)
+// .unwrap_or_default();
+
+// assert!(actual_ranges.is_empty(), "When no cmd is pressed, should have no hint label selected, but got: {actual_ranges:?}");
+// });
+
+// // Cmd+click without existing definition requests and jumps
+// cx.update_editor(|editor, cx| {
+// editor.modifiers_changed(
+// &platform::ModifiersChangedEvent {
+// modifiers: Modifiers {
+// cmd: true,
+// ..Default::default()
+// },
+// ..Default::default()
+// },
+// cx,
+// );
+// update_inlay_link_and_hover_points(
+// &editor.snapshot(cx),
+// hint_hover_position,
+// editor,
+// true,
+// false,
+// cx,
+// );
+// });
+// cx.foreground().run_until_parked();
+// cx.update_editor(|editor, cx| {
+// go_to_fetched_type_definition(editor, hint_hover_position, false, cx);
+// });
+// cx.foreground().run_until_parked();
+// cx.assert_editor_state(indoc! {"
+// struct «TestStructˇ»;
+
+// fn main() {
+// let variable = TestStruct;
+// }
+// "});
+// }
+// }
@@ -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<Pixels>,
point: DisplayPoint,
cx: &mut ViewContext<Editor>,
) {
@@ -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()));
+// }
+// }
@@ -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<FontCache>,
- pub text_layout_cache: Arc<TextLayoutCache>,
+ pub text_system: TextSystem,
pub editor_style: EditorStyle,
}
@@ -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<f32>,
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<Pixels> {
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<Axis> {
+ pub fn filter(&self, delta: &mut Point<Pixels>) -> Option<Axis> {
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<Pixels>, f32, f32, AutoscrollStrategy)>,
show_scrollbars: bool,
hide_scrollbar_task: Option<Task<()>>,
visible_line_count: Option<f32>,
@@ -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<Pixels> {
self.anchor.scroll_position(snapshot)
}
fn set_scroll_position(
&mut self,
- scroll_position: Vector2F,
+ scroll_position: Point<Pixels>,
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<Self>) {
+ pub fn set_scroll_position(
+ &mut self,
+ scroll_position: Point<Pixels>,
+ cx: &mut ViewContext<Self>,
+ ) {
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<Pixels>,
local: bool,
autoscroll: bool,
cx: &mut ViewContext<Self>,
@@ -337,7 +342,7 @@ impl Editor {
self.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx);
}
- pub fn scroll_position(&self, cx: &mut ViewContext<Self>) -> Vector2F {
+ pub fn scroll_position(&self, cx: &mut ViewContext<Self>) -> Point<Pixels> {
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);
@@ -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);
@@ -25,8 +25,8 @@ pub struct PendingSelection {
#[derive(Debug, Clone)]
pub struct SelectionsCollection {
- display_map: ModelHandle<DisplayMap>,
- buffer: ModelHandle<MultiBuffer>,
+ display_map: Model<DisplayMap>,
+ buffer: Model<MultiBuffer>,
pub next_selection_id: usize,
pub line_mode: bool,
disjoint: Arc<[Selection<Anchor>]>,
@@ -34,7 +34,7 @@ pub struct SelectionsCollection {
}
impl SelectionsCollection {
- pub fn new(display_map: ModelHandle<DisplayMap>, buffer: ModelHandle<MultiBuffer>) -> Self {
+ pub fn new(display_map: Model<DisplayMap>, buffer: Model<MultiBuffer>) -> Self {
Self {
display_map,
buffer,
@@ -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<MultiBuffer>,
- cx: &mut ViewContext<Editor>,
-) -> Editor {
+pub(crate) fn build_editor(buffer: Model<MultiBuffer>, cx: &mut ViewContext<Editor>) -> Editor {
Editor::new(EditorMode::Full, buffer, None, None, cx)
}
pub(crate) fn build_editor_with_project(
- project: ModelHandle<Project>,
- buffer: ModelHandle<MultiBuffer>,
+ project: Model<Project>,
+ buffer: Model<MultiBuffer>,
cx: &mut ViewContext<Editor>,
) -> Editor {
Editor::new(EditorMode::Full, buffer, Some(project), None, cx)
@@ -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};
@@ -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<Editor>,
+ pub editor: View<Editor>,
}
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();
-
- self.cx.dispatch_keystroke(self.window, keystroke, false);
-
- keystroke_under_test_handle
- }
-
- pub fn simulate_keystrokes<const COUNT: usize>(
- &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
- }
+ // 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);
+
+ // keystroke_under_test_handle
+ // }
+
+ // pub fn simulate_keystrokes<const COUNT: usize>(
+ // &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
+ // }
pub fn ranges(&self, marked_text: &str) -> Vec<Range<usize>> {
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
- }
-
- /// 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();
-
- 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())
- }
-
- 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<Tag: 'static>(&mut self, marked_text: &str) {
- let expected_ranges = self.ranges(marked_text);
- let actual_ranges: Vec<Range<usize>> = self.update_editor(|editor, cx| {
- let snapshot = editor.snapshot(cx);
- editor
- .background_highlights
- .get(&TypeId::of::<Tag>())
- .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<Tag: ?Sized + 'static>(&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<Range<usize>> = snapshot
- .text_highlight_ranges::<Tag>()
- .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<Range<usize>>) {
- 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<Range<usize>> {
- self.editor
- .read_with(self.cx, |editor, cx| editor.selections.all::<usize>(cx))
- .into_iter()
- .map(|s| {
- if s.reversed {
- s.end..s.start
- } else {
- s.start..s.end
- }
- })
- .collect::<Vec<_>>()
- }
-
- #[track_caller]
- fn assert_selections(
- &mut self,
- expected_selections: Vec<Range<usize>>,
- 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.
-
- Expected selections:
- {}
-
- Actual selections:
- {}
- "},
- self.assertion_context(),
- expected_marked_text,
- actual_marked_text,
- );
- }
- }
+ // /// 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
+ // }
+
+ // /// 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}");
+ // }
+
+ // 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)
+ // }
+
+ // #[track_caller]
+ // pub fn assert_editor_background_highlights<Tag: 'static>(&mut self, marked_text: &str) {
+ // let expected_ranges = self.ranges(marked_text);
+ // let actual_ranges: Vec<Range<usize>> = self.update_editor(|editor, cx| {
+ // let snapshot = editor.snapshot(cx);
+ // editor
+ // .background_highlights
+ // .get(&TypeId::of::<Tag>())
+ // .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<Tag: ?Sized + 'static>(&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<Range<usize>> = snapshot
+ // .text_highlight_ranges::<Tag>()
+ // .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<Range<usize>>) {
+ // 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<Range<usize>> {
+ // self.editor
+ // .read_with(self.cx, |editor, cx| editor.selections.all::<usize>(cx))
+ // .into_iter()
+ // .map(|s| {
+ // if s.reversed {
+ // s.end..s.start
+ // } else {
+ // s.start..s.end
+ // }
+ // })
+ // .collect::<Vec<_>>()
+ // }
+
+ // #[track_caller]
+ // fn assert_selections(
+ // &mut self,
+ // expected_selections: Vec<Range<usize>>,
+ // 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.
+
+ // Expected selections:
+ // {}
+
+ // Actual selections:
+ // {}
+ // "},
+ // self.assertion_context(),
+ // expected_marked_text,
+ // actual_marked_text,
+ // );
+ // }
+ // }
}
impl<'a> Deref for EditorTestContext<'a> {
@@ -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::{
@@ -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"] }
@@ -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;
@@ -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<usize>, MarkdownHighlight)>,
+ pub region_ranges: Vec<Range<usize>>,
+ pub regions: Vec<ParsedRegion>,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub enum MarkdownHighlight {
+ Style(MarkdownHighlightStyle),
+ Code(HighlightId),
+}
+
+impl MarkdownHighlight {
+ pub fn to_highlight_style(&self, theme: &theme::SyntaxTheme) -> Option<HighlightStyle> {
+ 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<Link>,
+}
+
+#[derive(Debug, Clone)]
+pub enum Link {
+ Web { url: String },
+ Path { path: PathBuf },
+}
+
+impl Link {
+ fn identify(text: String) -> Option<Link> {
+ 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<LanguageRegistry>,
+ language: Option<Arc<Language>>,
+) -> 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<LanguageRegistry>,
+ language: Option<Arc<Language>>,
+ text: &mut String,
+ highlights: &mut Vec<(Range<usize>, MarkdownHighlight)>,
+ region_ranges: &mut Vec<Range<usize>>,
+ regions: &mut Vec<ParsedRegion>,
+) {
+ 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<usize>, MarkdownHighlight)>,
+ content: &str,
+ language: &Arc<Language>,
+) {
+ 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<u64>, 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(" ");
+ }
+}