use super::*;use crate::Buffer;use clock::ReplicaId;use collections::BTreeMap;use futures::FutureExt as _;use gpui::{App, AppContext as _, BorrowAppContext, Entity};use gpui::{HighlightStyle, TestAppContext};use indoc::indoc;use pretty_assertions::assert_eq;use proto::deserialize_operation;use rand::prelude::*;use regex::RegexBuilder;use settings::SettingsStore;use settings::{AllLanguageSettingsContent, LanguageSettingsContent};use std::collections::BTreeSet;use std::{ env, ops::Range, sync::LazyLock, time::{Duration, Instant},};use syntax_map::TreeSitterOptions;use text::network::Network;use text::{BufferId, LineEnding};use text::{Point, ToPoint};use theme::ActiveTheme;use unindent::Unindent as _;use util::rel_path::rel_path;use util::test::marked_text_offsets;use util::{RandomCharIter, assert_set_eq, post_inc, test::marked_text_ranges}; pub static TRAILING_WHITESPACE_REGEX: LazyLock<regex::Regex> = LazyLock::new(|| { RegexBuilder::new(r"[ \t]+$") .multi_line(true) .build() .expect("Failed to create TRAILING_WHITESPACE_REGEX")}); #[cfg(test)]#[ctor::ctor]fn init_logger() { zlog::init_test();} #[gpui::test]fn test_line_endings(cx: &mut gpui::App) { init_settings(cx, |_| {}); cx.new(|cx| { let mut buffer = Buffer::local("one\r\ntwo\rthree", cx).with_language(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_set_line_ending(cx: &mut TestAppContext) { let base = cx.new(|cx| Buffer::local("one\ntwo\nthree\n", cx)); let base_replica = cx.new(|cx| { Buffer::from_proto( ReplicaId::new(1), Capability::ReadWrite, base.read(cx).to_proto(cx), None, ) .unwrap() }); base.update(cx, |_buffer, cx| { cx.subscribe(&base_replica, |this, _, event, cx| { if let BufferEvent::Operation { operation, is_local: true, } = event { this.apply_ops([operation.clone()], cx); } }) .detach(); }); base_replica.update(cx, |_buffer, cx| { cx.subscribe(&base, |this, _, event, cx| { if let BufferEvent::Operation { operation, is_local: true, } = event { this.apply_ops([operation.clone()], cx); } }) .detach(); }); // Base base_replica.read_with(cx, |buffer, _| { assert_eq!(buffer.line_ending(), LineEnding::Unix); }); base.update(cx, |buffer, cx| { assert_eq!(buffer.line_ending(), LineEnding::Unix); buffer.set_line_ending(LineEnding::Windows, cx); assert_eq!(buffer.line_ending(), LineEnding::Windows); }); base_replica.read_with(cx, |buffer, _| { assert_eq!(buffer.line_ending(), LineEnding::Windows); }); base.update(cx, |buffer, cx| { buffer.set_line_ending(LineEnding::Unix, cx); assert_eq!(buffer.line_ending(), LineEnding::Unix); }); base_replica.read_with(cx, |buffer, _| { assert_eq!(buffer.line_ending(), LineEnding::Unix); }); // Replica base.read_with(cx, |buffer, _| { assert_eq!(buffer.line_ending(), LineEnding::Unix); }); base_replica.update(cx, |buffer, cx| { assert_eq!(buffer.line_ending(), LineEnding::Unix); buffer.set_line_ending(LineEnding::Windows, cx); assert_eq!(buffer.line_ending(), LineEnding::Windows); }); base.read_with(cx, |buffer, _| { assert_eq!(buffer.line_ending(), LineEnding::Windows); }); base_replica.update(cx, |buffer, cx| { buffer.set_line_ending(LineEnding::Unix, cx); assert_eq!(buffer.line_ending(), LineEnding::Unix); }); base.read_with(cx, |buffer, _| { assert_eq!(buffer.line_ending(), LineEnding::Unix); });} #[gpui::test]fn test_select_language(cx: &mut App) { init_settings(cx, |_| {}); let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone())); registry.add(Arc::new(Language::new( LanguageConfig { name: LanguageName::new_static("Rust"), matcher: LanguageMatcher { path_suffixes: vec!["rs".to_string()], ..Default::default() }, ..Default::default() }, Some(tree_sitter_rust::LANGUAGE.into()), ))); registry.add(Arc::new(Language::new( LanguageConfig { name: "Rust with longer extension".into(), matcher: LanguageMatcher { path_suffixes: vec!["longer.rs".to_string()], ..Default::default() }, ..Default::default() }, Some(tree_sitter_rust::LANGUAGE.into()), ))); registry.add(Arc::new(Language::new( LanguageConfig { name: LanguageName::new_static("Make"), matcher: LanguageMatcher { path_suffixes: vec!["Makefile".to_string(), "mk".to_string()], ..Default::default() }, ..Default::default() }, Some(tree_sitter_rust::LANGUAGE.into()), ))); // matching file extension assert_eq!( registry .language_for_file(&file("src/lib.rs"), None, cx) .map(|l| l.name()), Some("Rust".into()) ); assert_eq!( registry .language_for_file(&file("src/lib.mk"), None, cx) .map(|l| l.name()), Some("Make".into()) ); // matching longer, compound extension, part of which could also match another lang assert_eq!( registry .language_for_file(&file("src/lib.longer.rs"), None, cx) .map(|l| l.name()), Some("Rust with longer extension".into()) ); // matching filename assert_eq!( registry .language_for_file(&file("src/Makefile"), None, cx) .map(|l| l.name()), Some("Make".into()) ); // matching suffix that is not the full file extension or filename assert_eq!( registry .language_for_file(&file("zed/cars"), None, cx) .map(|l| l.name()), None ); assert_eq!( registry .language_for_file(&file("zed/a.cars"), None, cx) .map(|l| l.name()), None ); assert_eq!( registry .language_for_file(&file("zed/sumk"), None, cx) .map(|l| l.name()), None );} #[gpui::test(iterations = 10)]async fn test_first_line_pattern(cx: &mut TestAppContext) { cx.update(|cx| init_settings(cx, |_| {})); let languages = LanguageRegistry::test(cx.executor()); let languages = Arc::new(languages); languages.register_test_language(LanguageConfig { name: "JavaScript".into(), matcher: LanguageMatcher { path_suffixes: vec!["js".into()], first_line_pattern: Some(Regex::new(r"\bnode\b").unwrap()), }, ..Default::default() }); assert!( cx.read(|cx| languages.language_for_file(&file("the/script"), None, cx)) .is_none() ); assert!( cx.read(|cx| languages.language_for_file(&file("the/script"), Some(&"nothing".into()), cx)) .is_none() ); assert_eq!( cx.read(|cx| languages.language_for_file( &file("the/script"), Some(&"#!/bin/env node".into()), cx )) .unwrap() .name(), "JavaScript" );} #[gpui::test]async fn test_language_for_file_with_custom_file_types(cx: &mut TestAppContext) { cx.update(|cx| { init_settings(cx, |settings| { settings.file_types.get_or_insert_default().extend([ ("TypeScript".into(), vec!["js".into()].into()), ( "JavaScript".into(), vec!["*longer.ts".into(), "ecmascript".into()].into(), ), ("C++".into(), vec!["c".into(), "*.dev".into()].into()), ( "Dockerfile".into(), vec!["Dockerfile".into(), "Dockerfile.*".into()].into(), ), ]); }) }); let languages = Arc::new(LanguageRegistry::test(cx.executor())); for config in [ LanguageConfig { name: "JavaScript".into(), matcher: LanguageMatcher { path_suffixes: vec!["js".to_string()], ..Default::default() }, ..Default::default() }, LanguageConfig { name: "TypeScript".into(), matcher: LanguageMatcher { path_suffixes: vec!["ts".to_string(), "ts.ecmascript".to_string()], ..Default::default() }, ..Default::default() }, LanguageConfig { name: "C++".into(), matcher: LanguageMatcher { path_suffixes: vec!["cpp".to_string()], ..Default::default() }, ..Default::default() }, LanguageConfig { name: "C".into(), matcher: LanguageMatcher { path_suffixes: vec!["c".to_string()], ..Default::default() }, ..Default::default() }, LanguageConfig { name: "Dockerfile".into(), matcher: LanguageMatcher { path_suffixes: vec!["Dockerfile".to_string()], ..Default::default() }, ..Default::default() }, ] { languages.add(Arc::new(Language::new(config, None))); } // matches system-provided lang extension let language = cx .read(|cx| languages.language_for_file(&file("foo.ts"), None, cx)) .unwrap(); assert_eq!(language.name(), "TypeScript"); let language = cx .read(|cx| languages.language_for_file(&file("foo.ts.ecmascript"), None, cx)) .unwrap(); assert_eq!(language.name(), "TypeScript"); let language = cx .read(|cx| languages.language_for_file(&file("foo.cpp"), None, cx)) .unwrap(); assert_eq!(language.name(), "C++"); // user configured lang extension, same length as system-provided let language = cx .read(|cx| languages.language_for_file(&file("foo.js"), None, cx)) .unwrap(); assert_eq!(language.name(), "TypeScript"); let language = cx .read(|cx| languages.language_for_file(&file("foo.c"), None, cx)) .unwrap(); assert_eq!(language.name(), "C++"); // user configured lang extension, longer than system-provided let language = cx .read(|cx| languages.language_for_file(&file("foo.longer.ts"), None, cx)) .unwrap(); assert_eq!(language.name(), "JavaScript"); // user configured lang extension, shorter than system-provided let language = cx .read(|cx| languages.language_for_file(&file("foo.ecmascript"), None, cx)) .unwrap(); assert_eq!(language.name(), "JavaScript"); // user configured glob matches let language = cx .read(|cx| languages.language_for_file(&file("c-plus-plus.dev"), None, cx)) .unwrap(); assert_eq!(language.name(), "C++"); // should match Dockerfile.* => Dockerfile, not *.dev => C++ let language = cx .read(|cx| languages.language_for_file(&file("Dockerfile.dev"), None, cx)) .unwrap(); assert_eq!(language.name(), "Dockerfile");} fn file(path: &str) -> Arc<dyn File> { Arc::new(TestFile { path: Arc::from(rel_path(path)), root_name: "zed".into(), local_root: None, })} #[gpui::test]fn test_edit_events(cx: &mut gpui::App) { 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(|cx| Buffer::local("abcdef", cx)); let buffer2 = cx.new(|cx| { Buffer::remote( BufferId::from(cx.entity_id().as_non_zero_u64()), ReplicaId::new(1), Capability::ReadWrite, "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() { BufferEvent::Operation { operation, is_local: true, } => buffer1_ops.lock().push(operation), event => buffer_1_events.lock().push(event), }) .detach(); let buffer_2_events = buffer_2_events.clone(); cx.subscribe(&buffer2, move |_, _, event, _| match event.clone() { BufferEvent::Operation { is_local: false, .. } => {} event => buffer_2_events.lock().push(event), }) .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); }); assert_eq!( mem::take(&mut *buffer_1_events.lock()), vec![ BufferEvent::Edited { is_local: true }, BufferEvent::DirtyChanged, BufferEvent::Edited { is_local: true }, BufferEvent::Edited { is_local: true }, ] ); assert_eq!( mem::take(&mut *buffer_2_events.lock()), vec![ BufferEvent::Edited { is_local: false }, BufferEvent::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); }); assert_eq!( mem::take(&mut *buffer_1_events.lock()), vec![ BufferEvent::Edited { is_local: true }, BufferEvent::DirtyChanged, ] ); assert_eq!( mem::take(&mut *buffer_2_events.lock()), vec![ BufferEvent::Edited { is_local: false }, BufferEvent::DirtyChanged ] );} Lines 1–500 of 4167. Load more
use super::*; use crate::Buffer; use clock::ReplicaId; use collections::BTreeMap; use futures::FutureExt as _; use gpui::{App, AppContext as _, BorrowAppContext, Entity}; use gpui::{HighlightStyle, TestAppContext}; use indoc::indoc; use pretty_assertions::assert_eq; use proto::deserialize_operation; use rand::prelude::*; use regex::RegexBuilder; use settings::SettingsStore; use settings::{AllLanguageSettingsContent, LanguageSettingsContent}; use std::collections::BTreeSet; use std::{ env, ops::Range, sync::LazyLock, time::{Duration, Instant}, }; use syntax_map::TreeSitterOptions; use text::network::Network; use text::{BufferId, LineEnding}; use text::{Point, ToPoint}; use theme::ActiveTheme; use unindent::Unindent as _; use util::rel_path::rel_path; use util::test::marked_text_offsets; use util::{RandomCharIter, assert_set_eq, post_inc, test::marked_text_ranges}; pub static TRAILING_WHITESPACE_REGEX: LazyLock<regex::Regex> = LazyLock::new(|| { RegexBuilder::new(r"[ \t]+$") .multi_line(true) .build() .expect("Failed to create TRAILING_WHITESPACE_REGEX") }); #[cfg(test)] #[ctor::ctor] fn init_logger() { zlog::init_test(); } #[gpui::test] fn test_line_endings(cx: &mut gpui::App) { init_settings(cx, |_| {}); cx.new(|cx| { let mut buffer = Buffer::local("one\r\ntwo\rthree", cx).with_language(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_set_line_ending(cx: &mut TestAppContext) { let base = cx.new(|cx| Buffer::local("one\ntwo\nthree\n", cx)); let base_replica = cx.new(|cx| { Buffer::from_proto( ReplicaId::new(1), Capability::ReadWrite, base.read(cx).to_proto(cx), None, ) .unwrap() }); base.update(cx, |_buffer, cx| { cx.subscribe(&base_replica, |this, _, event, cx| { if let BufferEvent::Operation { operation, is_local: true, } = event { this.apply_ops([operation.clone()], cx); } }) .detach(); }); base_replica.update(cx, |_buffer, cx| { cx.subscribe(&base, |this, _, event, cx| { if let BufferEvent::Operation { operation, is_local: true, } = event { this.apply_ops([operation.clone()], cx); } }) .detach(); }); // Base base_replica.read_with(cx, |buffer, _| { assert_eq!(buffer.line_ending(), LineEnding::Unix); }); base.update(cx, |buffer, cx| { assert_eq!(buffer.line_ending(), LineEnding::Unix); buffer.set_line_ending(LineEnding::Windows, cx); assert_eq!(buffer.line_ending(), LineEnding::Windows); }); base_replica.read_with(cx, |buffer, _| { assert_eq!(buffer.line_ending(), LineEnding::Windows); }); base.update(cx, |buffer, cx| { buffer.set_line_ending(LineEnding::Unix, cx); assert_eq!(buffer.line_ending(), LineEnding::Unix); }); base_replica.read_with(cx, |buffer, _| { assert_eq!(buffer.line_ending(), LineEnding::Unix); }); // Replica base.read_with(cx, |buffer, _| { assert_eq!(buffer.line_ending(), LineEnding::Unix); }); base_replica.update(cx, |buffer, cx| { assert_eq!(buffer.line_ending(), LineEnding::Unix); buffer.set_line_ending(LineEnding::Windows, cx); assert_eq!(buffer.line_ending(), LineEnding::Windows); }); base.read_with(cx, |buffer, _| { assert_eq!(buffer.line_ending(), LineEnding::Windows); }); base_replica.update(cx, |buffer, cx| { buffer.set_line_ending(LineEnding::Unix, cx); assert_eq!(buffer.line_ending(), LineEnding::Unix); }); base.read_with(cx, |buffer, _| { assert_eq!(buffer.line_ending(), LineEnding::Unix); }); } #[gpui::test] fn test_select_language(cx: &mut App) { init_settings(cx, |_| {}); let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone())); registry.add(Arc::new(Language::new( LanguageConfig { name: LanguageName::new_static("Rust"), matcher: LanguageMatcher { path_suffixes: vec!["rs".to_string()], ..Default::default() }, ..Default::default() }, Some(tree_sitter_rust::LANGUAGE.into()), ))); registry.add(Arc::new(Language::new( LanguageConfig { name: "Rust with longer extension".into(), matcher: LanguageMatcher { path_suffixes: vec!["longer.rs".to_string()], ..Default::default() }, ..Default::default() }, Some(tree_sitter_rust::LANGUAGE.into()), ))); registry.add(Arc::new(Language::new( LanguageConfig { name: LanguageName::new_static("Make"), matcher: LanguageMatcher { path_suffixes: vec!["Makefile".to_string(), "mk".to_string()], ..Default::default() }, ..Default::default() }, Some(tree_sitter_rust::LANGUAGE.into()), ))); // matching file extension assert_eq!( registry .language_for_file(&file("src/lib.rs"), None, cx) .map(|l| l.name()), Some("Rust".into()) ); assert_eq!( registry .language_for_file(&file("src/lib.mk"), None, cx) .map(|l| l.name()), Some("Make".into()) ); // matching longer, compound extension, part of which could also match another lang assert_eq!( registry .language_for_file(&file("src/lib.longer.rs"), None, cx) .map(|l| l.name()), Some("Rust with longer extension".into()) ); // matching filename assert_eq!( registry .language_for_file(&file("src/Makefile"), None, cx) .map(|l| l.name()), Some("Make".into()) ); // matching suffix that is not the full file extension or filename assert_eq!( registry .language_for_file(&file("zed/cars"), None, cx) .map(|l| l.name()), None ); assert_eq!( registry .language_for_file(&file("zed/a.cars"), None, cx) .map(|l| l.name()), None ); assert_eq!( registry .language_for_file(&file("zed/sumk"), None, cx) .map(|l| l.name()), None ); } #[gpui::test(iterations = 10)] async fn test_first_line_pattern(cx: &mut TestAppContext) { cx.update(|cx| init_settings(cx, |_| {})); let languages = LanguageRegistry::test(cx.executor()); let languages = Arc::new(languages); languages.register_test_language(LanguageConfig { name: "JavaScript".into(), matcher: LanguageMatcher { path_suffixes: vec!["js".into()], first_line_pattern: Some(Regex::new(r"\bnode\b").unwrap()), }, ..Default::default() }); assert!( cx.read(|cx| languages.language_for_file(&file("the/script"), None, cx)) .is_none() ); assert!( cx.read(|cx| languages.language_for_file(&file("the/script"), Some(&"nothing".into()), cx)) .is_none() ); assert_eq!( cx.read(|cx| languages.language_for_file( &file("the/script"), Some(&"#!/bin/env node".into()), cx )) .unwrap() .name(), "JavaScript" ); } #[gpui::test] async fn test_language_for_file_with_custom_file_types(cx: &mut TestAppContext) { cx.update(|cx| { init_settings(cx, |settings| { settings.file_types.get_or_insert_default().extend([ ("TypeScript".into(), vec!["js".into()].into()), ( "JavaScript".into(), vec!["*longer.ts".into(), "ecmascript".into()].into(), ), ("C++".into(), vec!["c".into(), "*.dev".into()].into()), ( "Dockerfile".into(), vec!["Dockerfile".into(), "Dockerfile.*".into()].into(), ), ]); }) }); let languages = Arc::new(LanguageRegistry::test(cx.executor())); for config in [ LanguageConfig { name: "JavaScript".into(), matcher: LanguageMatcher { path_suffixes: vec!["js".to_string()], ..Default::default() }, ..Default::default() }, LanguageConfig { name: "TypeScript".into(), matcher: LanguageMatcher { path_suffixes: vec!["ts".to_string(), "ts.ecmascript".to_string()], ..Default::default() }, ..Default::default() }, LanguageConfig { name: "C++".into(), matcher: LanguageMatcher { path_suffixes: vec!["cpp".to_string()], ..Default::default() }, ..Default::default() }, LanguageConfig { name: "C".into(), matcher: LanguageMatcher { path_suffixes: vec!["c".to_string()], ..Default::default() }, ..Default::default() }, LanguageConfig { name: "Dockerfile".into(), matcher: LanguageMatcher { path_suffixes: vec!["Dockerfile".to_string()], ..Default::default() }, ..Default::default() }, ] { languages.add(Arc::new(Language::new(config, None))); } // matches system-provided lang extension let language = cx .read(|cx| languages.language_for_file(&file("foo.ts"), None, cx)) .unwrap(); assert_eq!(language.name(), "TypeScript"); let language = cx .read(|cx| languages.language_for_file(&file("foo.ts.ecmascript"), None, cx)) .unwrap(); assert_eq!(language.name(), "TypeScript"); let language = cx .read(|cx| languages.language_for_file(&file("foo.cpp"), None, cx)) .unwrap(); assert_eq!(language.name(), "C++"); // user configured lang extension, same length as system-provided let language = cx .read(|cx| languages.language_for_file(&file("foo.js"), None, cx)) .unwrap(); assert_eq!(language.name(), "TypeScript"); let language = cx .read(|cx| languages.language_for_file(&file("foo.c"), None, cx)) .unwrap(); assert_eq!(language.name(), "C++"); // user configured lang extension, longer than system-provided let language = cx .read(|cx| languages.language_for_file(&file("foo.longer.ts"), None, cx)) .unwrap(); assert_eq!(language.name(), "JavaScript"); // user configured lang extension, shorter than system-provided let language = cx .read(|cx| languages.language_for_file(&file("foo.ecmascript"), None, cx)) .unwrap(); assert_eq!(language.name(), "JavaScript"); // user configured glob matches let language = cx .read(|cx| languages.language_for_file(&file("c-plus-plus.dev"), None, cx)) .unwrap(); assert_eq!(language.name(), "C++"); // should match Dockerfile.* => Dockerfile, not *.dev => C++ let language = cx .read(|cx| languages.language_for_file(&file("Dockerfile.dev"), None, cx)) .unwrap(); assert_eq!(language.name(), "Dockerfile"); } fn file(path: &str) -> Arc<dyn File> { Arc::new(TestFile { path: Arc::from(rel_path(path)), root_name: "zed".into(), local_root: None, }) } #[gpui::test] fn test_edit_events(cx: &mut gpui::App) { 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(|cx| Buffer::local("abcdef", cx)); let buffer2 = cx.new(|cx| { Buffer::remote( BufferId::from(cx.entity_id().as_non_zero_u64()), ReplicaId::new(1), Capability::ReadWrite, "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() { BufferEvent::Operation { operation, is_local: true, } => buffer1_ops.lock().push(operation), event => buffer_1_events.lock().push(event), }) .detach(); let buffer_2_events = buffer_2_events.clone(); cx.subscribe(&buffer2, move |_, _, event, _| match event.clone() { BufferEvent::Operation { is_local: false, .. } => {} event => buffer_2_events.lock().push(event), }) .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); }); assert_eq!( mem::take(&mut *buffer_1_events.lock()), vec![ BufferEvent::Edited { is_local: true }, BufferEvent::DirtyChanged, BufferEvent::Edited { is_local: true }, BufferEvent::Edited { is_local: true }, ] ); assert_eq!( mem::take(&mut *buffer_2_events.lock()), vec![ BufferEvent::Edited { is_local: false }, BufferEvent::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); }); assert_eq!( mem::take(&mut *buffer_1_events.lock()), vec![ BufferEvent::Edited { is_local: true }, BufferEvent::DirtyChanged, ] ); assert_eq!( mem::take(&mut *buffer_2_events.lock()), vec![ BufferEvent::Edited { is_local: false }, BufferEvent::DirtyChanged ] ); }