use super::*;use crate::language_settings::{ AllLanguageSettings, AllLanguageSettingsContent, LanguageSettingsContent,};use crate::Buffer;use clock::ReplicaId;use collections::BTreeMap;use futures::FutureExt as _;use gpui::{AppContext, Model};use gpui::{Context, TestAppContext};use indoc::indoc;use proto::deserialize_operation;use rand::prelude::*;use regex::RegexBuilder;use settings::SettingsStore;use std::{ env, ops::Range, time::{Duration, Instant},};use text::network::Network;use text::{BufferId, LineEnding};use text::{Point, ToPoint};use unindent::Unindent as _;use util::{assert_set_eq, post_inc, test::marked_text_ranges, RandomCharIter}; lazy_static! { static ref TRAILING_WHITESPACE_REGEX: Regex = RegexBuilder::new("[ \t]+$") .multi_line(true) .build() .unwrap();} #[cfg(test)]#[ctor::ctor]fn init_logger() { if std::env::var("RUST_LOG").is_ok() { env_logger::init(); }} #[gpui::test]fn test_line_endings(cx: &mut gpui::AppContext) { init_settings(cx, |_| {}); cx.new_model(|cx| { let mut buffer = Buffer::new( 0, BufferId::new(cx.entity_id().as_u64()).unwrap(), "one\r\ntwo\rthree", ) .with_language(Arc::new(rust_lang()), cx); assert_eq!(buffer.text(), "one\ntwo\nthree"); assert_eq!(buffer.line_ending(), LineEnding::Windows); buffer.check_invariants(); buffer.edit( [(buffer.len()..buffer.len(), "\r\nfour")], Some(AutoindentMode::EachLine), cx, ); buffer.edit([(0..0, "zero\r\n")], None, cx); assert_eq!(buffer.text(), "zero\none\ntwo\nthree\nfour"); assert_eq!(buffer.line_ending(), LineEnding::Windows); buffer.check_invariants(); buffer });} #[gpui::test]fn test_select_language() { let registry = Arc::new(LanguageRegistry::test()); registry.add(Arc::new(Language::new( LanguageConfig { name: "Rust".into(), matcher: LanguageMatcher { path_suffixes: vec!["rs".to_string()], ..Default::default() }, ..Default::default() }, Some(tree_sitter_rust::language()), ))); registry.add(Arc::new(Language::new( LanguageConfig { name: "Make".into(), matcher: LanguageMatcher { path_suffixes: vec!["Makefile".to_string(), "mk".to_string()], ..Default::default() }, ..Default::default() }, Some(tree_sitter_rust::language()), ))); // matching file extension assert_eq!( registry .language_for_file("zed/lib.rs", None) .now_or_never() .and_then(|l| Some(l.ok()?.name())), Some("Rust".into()) ); assert_eq!( registry .language_for_file("zed/lib.mk", None) .now_or_never() .and_then(|l| Some(l.ok()?.name())), Some("Make".into()) ); // matching filename assert_eq!( registry .language_for_file("zed/Makefile", None) .now_or_never() .and_then(|l| Some(l.ok()?.name())), Some("Make".into()) ); // matching suffix that is not the full file extension or filename assert_eq!( registry .language_for_file("zed/cars", None) .now_or_never() .and_then(|l| Some(l.ok()?.name())), None ); assert_eq!( registry .language_for_file("zed/a.cars", None) .now_or_never() .and_then(|l| Some(l.ok()?.name())), None ); assert_eq!( registry .language_for_file("zed/sumk", None) .now_or_never() .and_then(|l| Some(l.ok()?.name())), None );} #[gpui::test]fn test_edit_events(cx: &mut gpui::AppContext) { let mut now = Instant::now(); let buffer_1_events = Arc::new(Mutex::new(Vec::new())); let buffer_2_events = Arc::new(Mutex::new(Vec::new())); let buffer1 = cx .new_model(|cx| Buffer::new(0, BufferId::new(cx.entity_id().as_u64()).unwrap(), "abcdef")); let buffer2 = cx .new_model(|cx| Buffer::new(1, BufferId::new(cx.entity_id().as_u64()).unwrap(), "abcdef")); let buffer1_ops = Arc::new(Mutex::new(Vec::new())); buffer1.update(cx, { let buffer1_ops = buffer1_ops.clone(); |buffer, cx| { let buffer_1_events = buffer_1_events.clone(); cx.subscribe(&buffer1, move |_, _, event, _| match event.clone() { Event::Operation(op) => buffer1_ops.lock().push(op), event => buffer_1_events.lock().push(event), }) .detach(); let buffer_2_events = buffer_2_events.clone(); cx.subscribe(&buffer2, move |_, _, event, _| { buffer_2_events.lock().push(event.clone()) }) .detach(); // An edit emits an edited event, followed by a dirty changed event, // since the buffer was previously in a clean state. buffer.edit([(2..4, "XYZ")], None, cx); // An empty transaction does not emit any events. buffer.start_transaction(); buffer.end_transaction(cx); // A transaction containing two edits emits one edited event. now += Duration::from_secs(1); buffer.start_transaction_at(now); buffer.edit([(5..5, "u")], None, cx); buffer.edit([(6..6, "w")], None, cx); buffer.end_transaction_at(now, cx); // Undoing a transaction emits one edited event. buffer.undo(cx); } }); // Incorporating a set of remote ops emits a single edited event, // followed by a dirty changed event. buffer2.update(cx, |buffer, cx| { buffer.apply_ops(buffer1_ops.lock().drain(..), cx).unwrap(); }); assert_eq!( mem::take(&mut *buffer_1_events.lock()), vec![ Event::Edited, Event::DirtyChanged, Event::Edited, Event::Edited, ] ); assert_eq!( mem::take(&mut *buffer_2_events.lock()), vec![Event::Edited, Event::DirtyChanged] ); buffer1.update(cx, |buffer, cx| { // Undoing the first transaction emits edited event, followed by a // dirty changed event, since the buffer is again in a clean state. buffer.undo(cx); }); // Incorporating the remote ops again emits a single edited event, // followed by a dirty changed event. buffer2.update(cx, |buffer, cx| { buffer.apply_ops(buffer1_ops.lock().drain(..), cx).unwrap(); }); assert_eq!( mem::take(&mut *buffer_1_events.lock()), vec![Event::Edited, Event::DirtyChanged,] ); assert_eq!( mem::take(&mut *buffer_2_events.lock()), vec![Event::Edited, Event::DirtyChanged] );} #[gpui::test]async fn test_apply_diff(cx: &mut TestAppContext) { let text = "a\nbb\nccc\ndddd\neeeee\nffffff\n"; let buffer = cx.new_model(|cx| Buffer::new(0, BufferId::new(cx.entity_id().as_u64()).unwrap(), text)); let anchor = buffer.update(cx, |buffer, _| buffer.anchor_before(Point::new(3, 3))); let text = "a\nccc\ndddd\nffffff\n"; let diff = buffer.update(cx, |b, cx| b.diff(text.into(), cx)).await; buffer.update(cx, |buffer, cx| { buffer.apply_diff(diff, cx).unwrap(); assert_eq!(buffer.text(), text); assert_eq!(anchor.to_point(buffer), Point::new(2, 3)); }); let text = "a\n1\n\nccc\ndd2dd\nffffff\n"; let diff = buffer.update(cx, |b, cx| b.diff(text.into(), cx)).await; buffer.update(cx, |buffer, cx| { buffer.apply_diff(diff, cx).unwrap(); assert_eq!(buffer.text(), text); assert_eq!(anchor.to_point(buffer), Point::new(4, 4)); });} #[gpui::test(iterations = 10)]async fn test_normalize_whitespace(cx: &mut gpui::TestAppContext) { let text = [ "zero", // "one ", // 2 trailing spaces "two", // "three ", // 3 trailing spaces "four", // "five ", // 4 trailing spaces ] .join("\n"); let buffer = cx.new_model(|cx| Buffer::new(0, BufferId::new(cx.entity_id().as_u64()).unwrap(), text)); // Spawn a task to format the buffer's whitespace. // Pause so that the foratting task starts running. let format = buffer.update(cx, |buffer, cx| buffer.remove_trailing_whitespace(cx)); smol::future::yield_now().await; // Edit the buffer while the normalization task is running. let version_before_edit = buffer.update(cx, |buffer, _| buffer.version()); buffer.update(cx, |buffer, cx| { buffer.edit( [ (Point::new(0, 1)..Point::new(0, 1), "EE"), (Point::new(3, 5)..Point::new(3, 5), "EEE"), ], None, cx, ); }); let format_diff = format.await; buffer.update(cx, |buffer, cx| { let version_before_format = format_diff.base_version.clone(); buffer.apply_diff(format_diff, cx); // The outcome depends on the order of concurrent tasks. // // If the edit occurred while searching for trailing whitespace ranges, // then the trailing whitespace region touched by the edit is left intact. if version_before_format == version_before_edit { assert_eq!( buffer.text(), [ "zEEero", // "one", // "two", // "threeEEE ", // "four", // "five", // ] .join("\n") ); } // Otherwise, all trailing whitespace is removed. else { assert_eq!( buffer.text(), [ "zEEero", // "one", // "two", // "threeEEE", // "four", // "five", // ] .join("\n") ); } });} #[gpui::test]async fn test_reparse(cx: &mut gpui::TestAppContext) { let text = "fn a() {}"; let buffer = cx.new_model(|cx| { Buffer::new(0, BufferId::new(cx.entity_id().as_u64()).unwrap(), text) .with_language(Arc::new(rust_lang()), cx) }); // Wait for the initial text to parse cx.executor().run_until_parked(); assert!(!buffer.update(cx, |buffer, _| buffer.is_parsing())); assert_eq!( get_tree_sexp(&buffer, cx), concat!( "(source_file (function_item name: (identifier) ", "parameters: (parameters) ", "body: (block)))" ) ); buffer.update(cx, |buffer, _| { buffer.set_sync_parse_timeout(Duration::ZERO) }); // Perform some edits (add parameter and variable reference) // Parsing doesn't begin until the transaction is complete buffer.update(cx, |buf, cx| { buf.start_transaction(); let offset = buf.text().find(')').unwrap(); buf.edit([(offset..offset, "b: C")], None, cx); assert!(!buf.is_parsing()); let offset = buf.text().find('}').unwrap(); buf.edit([(offset..offset, " d; ")], None, cx); assert!(!buf.is_parsing()); buf.end_transaction(cx); assert_eq!(buf.text(), "fn a(b: C) { d; }"); assert!(buf.is_parsing()); }); cx.executor().run_until_parked(); assert!(!buffer.update(cx, |buffer, _| buffer.is_parsing())); assert_eq!( get_tree_sexp(&buffer, cx), concat!( "(source_file (function_item name: (identifier) ", "parameters: (parameters (parameter pattern: (identifier) type: (type_identifier))) ", "body: (block (expression_statement (identifier)))))" ) ); // Perform a series of edits without waiting for the current parse to complete: // * turn identifier into a field expression // * turn field expression into a method call // * add a turbofish to the method call buffer.update(cx, |buf, cx| { let offset = buf.text().find(';').unwrap(); buf.edit([(offset..offset, ".e")], None, cx); assert_eq!(buf.text(), "fn a(b: C) { d.e; }"); assert!(buf.is_parsing()); }); buffer.update(cx, |buf, cx| { let offset = buf.text().find(';').unwrap(); buf.edit([(offset..offset, "(f)")], None, cx); assert_eq!(buf.text(), "fn a(b: C) { d.e(f); }"); assert!(buf.is_parsing()); }); buffer.update(cx, |buf, cx| { let offset = buf.text().find("(f)").unwrap(); buf.edit([(offset..offset, "::<G>")], None, cx); assert_eq!(buf.text(), "fn a(b: C) { d.e::<G>(f); }"); assert!(buf.is_parsing()); }); cx.executor().run_until_parked(); assert_eq!( get_tree_sexp(&buffer, cx), concat!( "(source_file (function_item name: (identifier) ", "parameters: (parameters (parameter pattern: (identifier) type: (type_identifier))) ", "body: (block (expression_statement (call_expression ", "function: (generic_function ", "function: (field_expression value: (identifier) field: (field_identifier)) ", "type_arguments: (type_arguments (type_identifier))) ", "arguments: (arguments (identifier)))))))", ) ); buffer.update(cx, |buf, cx| { buf.undo(cx); buf.undo(cx); buf.undo(cx); buf.undo(cx); assert_eq!(buf.text(), "fn a() {}"); assert!(buf.is_parsing()); }); cx.executor().run_until_parked(); assert_eq!( get_tree_sexp(&buffer, cx), concat!( "(source_file (function_item name: (identifier) ", "parameters: (parameters) ", "body: (block)))" ) ); buffer.update(cx, |buf, cx| { buf.redo(cx); buf.redo(cx); buf.redo(cx); buf.redo(cx); assert_eq!(buf.text(), "fn a(b: C) { d.e::<G>(f); }"); assert!(buf.is_parsing()); }); cx.executor().run_until_parked(); assert_eq!( get_tree_sexp(&buffer, cx), concat!( "(source_file (function_item name: (identifier) ", "parameters: (parameters (parameter pattern: (identifier) type: (type_identifier))) ", "body: (block (expression_statement (call_expression ", "function: (generic_function ", "function: (field_expression value: (identifier) field: (field_identifier)) ", "type_arguments: (type_arguments (type_identifier))) ", "arguments: (arguments (identifier)))))))", ) );} #[gpui::test]async fn test_resetting_language(cx: &mut gpui::TestAppContext) { let buffer = cx.new_model(|cx| { let mut buffer = Buffer::new(0, BufferId::new(cx.entity_id().as_u64()).unwrap(), "{}") .with_language(Arc::new(rust_lang()), cx); buffer.set_sync_parse_timeout(Duration::ZERO); buffer }); // Wait for the initial text to parse cx.executor().run_until_parked(); assert_eq!( get_tree_sexp(&buffer, cx), "(source_file (expression_statement (block)))" ); buffer.update(cx, |buffer, cx| { buffer.set_language(Some(Arc::new(json_lang())), cx) }); cx.executor().run_until_parked(); assert_eq!(get_tree_sexp(&buffer, cx), "(document (object))");} #[gpui::test]async fn test_outline(cx: &mut gpui::TestAppContext) { let text = r#" struct Person { name: String, age: usize, } mod module { enum LoginState { LoggedOut, LoggingOn, LoggedIn { person: Person, time: Instant, } } } Lines 1–500 of 2552. Load more
use super::*; use crate::language_settings::{ AllLanguageSettings, AllLanguageSettingsContent, LanguageSettingsContent, }; use crate::Buffer; use clock::ReplicaId; use collections::BTreeMap; use futures::FutureExt as _; use gpui::{AppContext, Model}; use gpui::{Context, TestAppContext}; use indoc::indoc; use proto::deserialize_operation; use rand::prelude::*; use regex::RegexBuilder; use settings::SettingsStore; use std::{ env, ops::Range, time::{Duration, Instant}, }; use text::network::Network; use text::{BufferId, LineEnding}; use text::{Point, ToPoint}; use unindent::Unindent as _; use util::{assert_set_eq, post_inc, test::marked_text_ranges, RandomCharIter}; lazy_static! { static ref TRAILING_WHITESPACE_REGEX: Regex = RegexBuilder::new("[ \t]+$") .multi_line(true) .build() .unwrap(); } #[cfg(test)] #[ctor::ctor] fn init_logger() { if std::env::var("RUST_LOG").is_ok() { env_logger::init(); } } #[gpui::test] fn test_line_endings(cx: &mut gpui::AppContext) { init_settings(cx, |_| {}); cx.new_model(|cx| { let mut buffer = Buffer::new( 0, BufferId::new(cx.entity_id().as_u64()).unwrap(), "one\r\ntwo\rthree", ) .with_language(Arc::new(rust_lang()), cx); assert_eq!(buffer.text(), "one\ntwo\nthree"); assert_eq!(buffer.line_ending(), LineEnding::Windows); buffer.check_invariants(); buffer.edit( [(buffer.len()..buffer.len(), "\r\nfour")], Some(AutoindentMode::EachLine), cx, ); buffer.edit([(0..0, "zero\r\n")], None, cx); assert_eq!(buffer.text(), "zero\none\ntwo\nthree\nfour"); assert_eq!(buffer.line_ending(), LineEnding::Windows); buffer.check_invariants(); buffer }); } #[gpui::test] fn test_select_language() { let registry = Arc::new(LanguageRegistry::test()); registry.add(Arc::new(Language::new( LanguageConfig { name: "Rust".into(), matcher: LanguageMatcher { path_suffixes: vec!["rs".to_string()], ..Default::default() }, ..Default::default() }, Some(tree_sitter_rust::language()), ))); registry.add(Arc::new(Language::new( LanguageConfig { name: "Make".into(), matcher: LanguageMatcher { path_suffixes: vec!["Makefile".to_string(), "mk".to_string()], ..Default::default() }, ..Default::default() }, Some(tree_sitter_rust::language()), ))); // matching file extension assert_eq!( registry .language_for_file("zed/lib.rs", None) .now_or_never() .and_then(|l| Some(l.ok()?.name())), Some("Rust".into()) ); assert_eq!( registry .language_for_file("zed/lib.mk", None) .now_or_never() .and_then(|l| Some(l.ok()?.name())), Some("Make".into()) ); // matching filename assert_eq!( registry .language_for_file("zed/Makefile", None) .now_or_never() .and_then(|l| Some(l.ok()?.name())), Some("Make".into()) ); // matching suffix that is not the full file extension or filename assert_eq!( registry .language_for_file("zed/cars", None) .now_or_never() .and_then(|l| Some(l.ok()?.name())), None ); assert_eq!( registry .language_for_file("zed/a.cars", None) .now_or_never() .and_then(|l| Some(l.ok()?.name())), None ); assert_eq!( registry .language_for_file("zed/sumk", None) .now_or_never() .and_then(|l| Some(l.ok()?.name())), None ); } #[gpui::test] fn test_edit_events(cx: &mut gpui::AppContext) { let mut now = Instant::now(); let buffer_1_events = Arc::new(Mutex::new(Vec::new())); let buffer_2_events = Arc::new(Mutex::new(Vec::new())); let buffer1 = cx .new_model(|cx| Buffer::new(0, BufferId::new(cx.entity_id().as_u64()).unwrap(), "abcdef")); let buffer2 = cx .new_model(|cx| Buffer::new(1, BufferId::new(cx.entity_id().as_u64()).unwrap(), "abcdef")); let buffer1_ops = Arc::new(Mutex::new(Vec::new())); buffer1.update(cx, { let buffer1_ops = buffer1_ops.clone(); |buffer, cx| { let buffer_1_events = buffer_1_events.clone(); cx.subscribe(&buffer1, move |_, _, event, _| match event.clone() { Event::Operation(op) => buffer1_ops.lock().push(op), event => buffer_1_events.lock().push(event), }) .detach(); let buffer_2_events = buffer_2_events.clone(); cx.subscribe(&buffer2, move |_, _, event, _| { buffer_2_events.lock().push(event.clone()) }) .detach(); // An edit emits an edited event, followed by a dirty changed event, // since the buffer was previously in a clean state. buffer.edit([(2..4, "XYZ")], None, cx); // An empty transaction does not emit any events. buffer.start_transaction(); buffer.end_transaction(cx); // A transaction containing two edits emits one edited event. now += Duration::from_secs(1); buffer.start_transaction_at(now); buffer.edit([(5..5, "u")], None, cx); buffer.edit([(6..6, "w")], None, cx); buffer.end_transaction_at(now, cx); // Undoing a transaction emits one edited event. buffer.undo(cx); } }); // Incorporating a set of remote ops emits a single edited event, // followed by a dirty changed event. buffer2.update(cx, |buffer, cx| { buffer.apply_ops(buffer1_ops.lock().drain(..), cx).unwrap(); }); assert_eq!( mem::take(&mut *buffer_1_events.lock()), vec![ Event::Edited, Event::DirtyChanged, Event::Edited, Event::Edited, ] ); assert_eq!( mem::take(&mut *buffer_2_events.lock()), vec![Event::Edited, Event::DirtyChanged] ); buffer1.update(cx, |buffer, cx| { // Undoing the first transaction emits edited event, followed by a // dirty changed event, since the buffer is again in a clean state. buffer.undo(cx); }); // Incorporating the remote ops again emits a single edited event, // followed by a dirty changed event. buffer2.update(cx, |buffer, cx| { buffer.apply_ops(buffer1_ops.lock().drain(..), cx).unwrap(); }); assert_eq!( mem::take(&mut *buffer_1_events.lock()), vec![Event::Edited, Event::DirtyChanged,] ); assert_eq!( mem::take(&mut *buffer_2_events.lock()), vec![Event::Edited, Event::DirtyChanged] ); } #[gpui::test] async fn test_apply_diff(cx: &mut TestAppContext) { let text = "a\nbb\nccc\ndddd\neeeee\nffffff\n"; let buffer = cx.new_model(|cx| Buffer::new(0, BufferId::new(cx.entity_id().as_u64()).unwrap(), text)); let anchor = buffer.update(cx, |buffer, _| buffer.anchor_before(Point::new(3, 3))); let text = "a\nccc\ndddd\nffffff\n"; let diff = buffer.update(cx, |b, cx| b.diff(text.into(), cx)).await; buffer.update(cx, |buffer, cx| { buffer.apply_diff(diff, cx).unwrap(); assert_eq!(buffer.text(), text); assert_eq!(anchor.to_point(buffer), Point::new(2, 3)); }); let text = "a\n1\n\nccc\ndd2dd\nffffff\n"; let diff = buffer.update(cx, |b, cx| b.diff(text.into(), cx)).await; buffer.update(cx, |buffer, cx| { buffer.apply_diff(diff, cx).unwrap(); assert_eq!(buffer.text(), text); assert_eq!(anchor.to_point(buffer), Point::new(4, 4)); }); } #[gpui::test(iterations = 10)] async fn test_normalize_whitespace(cx: &mut gpui::TestAppContext) { let text = [ "zero", // "one ", // 2 trailing spaces "two", // "three ", // 3 trailing spaces "four", // "five ", // 4 trailing spaces ] .join("\n"); let buffer = cx.new_model(|cx| Buffer::new(0, BufferId::new(cx.entity_id().as_u64()).unwrap(), text)); // Spawn a task to format the buffer's whitespace. // Pause so that the foratting task starts running. let format = buffer.update(cx, |buffer, cx| buffer.remove_trailing_whitespace(cx)); smol::future::yield_now().await; // Edit the buffer while the normalization task is running. let version_before_edit = buffer.update(cx, |buffer, _| buffer.version()); buffer.update(cx, |buffer, cx| { buffer.edit( [ (Point::new(0, 1)..Point::new(0, 1), "EE"), (Point::new(3, 5)..Point::new(3, 5), "EEE"), ], None, cx, ); }); let format_diff = format.await; buffer.update(cx, |buffer, cx| { let version_before_format = format_diff.base_version.clone(); buffer.apply_diff(format_diff, cx); // The outcome depends on the order of concurrent tasks. // // If the edit occurred while searching for trailing whitespace ranges, // then the trailing whitespace region touched by the edit is left intact. if version_before_format == version_before_edit { assert_eq!( buffer.text(), [ "zEEero", // "one", // "two", // "threeEEE ", // "four", // "five", // ] .join("\n") ); } // Otherwise, all trailing whitespace is removed. else { assert_eq!( buffer.text(), [ "zEEero", // "one", // "two", // "threeEEE", // "four", // "five", // ] .join("\n") ); } }); } #[gpui::test] async fn test_reparse(cx: &mut gpui::TestAppContext) { let text = "fn a() {}"; let buffer = cx.new_model(|cx| { Buffer::new(0, BufferId::new(cx.entity_id().as_u64()).unwrap(), text) .with_language(Arc::new(rust_lang()), cx) }); // Wait for the initial text to parse cx.executor().run_until_parked(); assert!(!buffer.update(cx, |buffer, _| buffer.is_parsing())); assert_eq!( get_tree_sexp(&buffer, cx), concat!( "(source_file (function_item name: (identifier) ", "parameters: (parameters) ", "body: (block)))" ) ); buffer.update(cx, |buffer, _| { buffer.set_sync_parse_timeout(Duration::ZERO) }); // Perform some edits (add parameter and variable reference) // Parsing doesn't begin until the transaction is complete buffer.update(cx, |buf, cx| { buf.start_transaction(); let offset = buf.text().find(')').unwrap(); buf.edit([(offset..offset, "b: C")], None, cx); assert!(!buf.is_parsing()); let offset = buf.text().find('}').unwrap(); buf.edit([(offset..offset, " d; ")], None, cx); assert!(!buf.is_parsing()); buf.end_transaction(cx); assert_eq!(buf.text(), "fn a(b: C) { d; }"); assert!(buf.is_parsing()); }); cx.executor().run_until_parked(); assert!(!buffer.update(cx, |buffer, _| buffer.is_parsing())); assert_eq!( get_tree_sexp(&buffer, cx), concat!( "(source_file (function_item name: (identifier) ", "parameters: (parameters (parameter pattern: (identifier) type: (type_identifier))) ", "body: (block (expression_statement (identifier)))))" ) ); // Perform a series of edits without waiting for the current parse to complete: // * turn identifier into a field expression // * turn field expression into a method call // * add a turbofish to the method call buffer.update(cx, |buf, cx| { let offset = buf.text().find(';').unwrap(); buf.edit([(offset..offset, ".e")], None, cx); assert_eq!(buf.text(), "fn a(b: C) { d.e; }"); assert!(buf.is_parsing()); }); buffer.update(cx, |buf, cx| { let offset = buf.text().find(';').unwrap(); buf.edit([(offset..offset, "(f)")], None, cx); assert_eq!(buf.text(), "fn a(b: C) { d.e(f); }"); assert!(buf.is_parsing()); }); buffer.update(cx, |buf, cx| { let offset = buf.text().find("(f)").unwrap(); buf.edit([(offset..offset, "::<G>")], None, cx); assert_eq!(buf.text(), "fn a(b: C) { d.e::<G>(f); }"); assert!(buf.is_parsing()); }); cx.executor().run_until_parked(); assert_eq!( get_tree_sexp(&buffer, cx), concat!( "(source_file (function_item name: (identifier) ", "parameters: (parameters (parameter pattern: (identifier) type: (type_identifier))) ", "body: (block (expression_statement (call_expression ", "function: (generic_function ", "function: (field_expression value: (identifier) field: (field_identifier)) ", "type_arguments: (type_arguments (type_identifier))) ", "arguments: (arguments (identifier)))))))", ) ); buffer.update(cx, |buf, cx| { buf.undo(cx); buf.undo(cx); buf.undo(cx); buf.undo(cx); assert_eq!(buf.text(), "fn a() {}"); assert!(buf.is_parsing()); }); cx.executor().run_until_parked(); assert_eq!( get_tree_sexp(&buffer, cx), concat!( "(source_file (function_item name: (identifier) ", "parameters: (parameters) ", "body: (block)))" ) ); buffer.update(cx, |buf, cx| { buf.redo(cx); buf.redo(cx); buf.redo(cx); buf.redo(cx); assert_eq!(buf.text(), "fn a(b: C) { d.e::<G>(f); }"); assert!(buf.is_parsing()); }); cx.executor().run_until_parked(); assert_eq!( get_tree_sexp(&buffer, cx), concat!( "(source_file (function_item name: (identifier) ", "parameters: (parameters (parameter pattern: (identifier) type: (type_identifier))) ", "body: (block (expression_statement (call_expression ", "function: (generic_function ", "function: (field_expression value: (identifier) field: (field_identifier)) ", "type_arguments: (type_arguments (type_identifier))) ", "arguments: (arguments (identifier)))))))", ) ); } #[gpui::test] async fn test_resetting_language(cx: &mut gpui::TestAppContext) { let buffer = cx.new_model(|cx| { let mut buffer = Buffer::new(0, BufferId::new(cx.entity_id().as_u64()).unwrap(), "{}") .with_language(Arc::new(rust_lang()), cx); buffer.set_sync_parse_timeout(Duration::ZERO); buffer }); // Wait for the initial text to parse cx.executor().run_until_parked(); assert_eq!( get_tree_sexp(&buffer, cx), "(source_file (expression_statement (block)))" ); buffer.update(cx, |buffer, cx| { buffer.set_language(Some(Arc::new(json_lang())), cx) }); cx.executor().run_until_parked(); assert_eq!(get_tree_sexp(&buffer, cx), "(document (object))"); } #[gpui::test] async fn test_outline(cx: &mut gpui::TestAppContext) { let text = r#" struct Person { name: String, age: usize, } mod module { enum LoginState { LoggedOut, LoggingOn, LoggedIn { person: Person, time: Instant, } } }