use super::*;use crate::{ JoinLines, code_context_menus::CodeContextMenu, edit_prediction_tests::FakeEditPredictionDelegate, element::StickyHeader, linked_editing_ranges::LinkedEditingRanges, scroll::scroll_amount::ScrollAmount, test::{ assert_text_with_selections, build_editor, editor_lsp_test_context::{EditorLspTestContext, git_commit_lang}, editor_test_context::EditorTestContext, select_ranges, },};use buffer_diff::{BufferDiff, DiffHunkSecondaryStatus, DiffHunkStatus, DiffHunkStatusKind};use collections::HashMap;use futures::{StreamExt, channel::oneshot};use gpui::{ BackgroundExecutor, DismissEvent, TestAppContext, UpdateGlobal, VisualTestContext, WindowBounds, WindowOptions, div,};use indoc::indoc;use language::{ BracketPairConfig, Capability::ReadWrite, DiagnosticSourceKind, FakeLspAdapter, IndentGuideSettings, LanguageConfig, LanguageConfigOverride, LanguageMatcher, LanguageName, Override, Point, language_settings::{ CompletionSettingsContent, FormatterList, LanguageSettingsContent, LspInsertMode, }, tree_sitter_python,};use language_settings::Formatter;use languages::markdown_lang;use languages::rust_lang;use lsp::{CompletionParams, DEFAULT_LSP_REQUEST_TIMEOUT};use multi_buffer::{ ExcerptRange, IndentGuide, MultiBuffer, MultiBufferOffset, MultiBufferOffsetUtf16, PathKey,};use parking_lot::Mutex;use pretty_assertions::{assert_eq, assert_ne};use project::{ FakeFs, Project, debugger::breakpoint_store::{BreakpointState, SourceBreakpoint}, project_settings::LspSettings, trusted_worktrees::{PathTrust, TrustedWorktrees},};use serde_json::{self, json};use settings::{ AllLanguageSettingsContent, DelayMs, EditorSettingsContent, GlobalLspSettingsContent, IndentGuideBackgroundColoring, IndentGuideColoring, InlayHintSettingsContent, ProjectSettingsContent, SearchSettingsContent, SettingsContent, SettingsStore,};use std::{cell::RefCell, future::Future, rc::Rc, sync::atomic::AtomicBool, time::Instant};use std::{ iter, sync::atomic::{self, AtomicUsize},};use test::build_editor_with_project;use text::ToPoint as _;use unindent::Unindent;use util::{ assert_set_eq, path, rel_path::rel_path, test::{TextRangeMarker, marked_text_ranges, marked_text_ranges_by, sample_text}, uri,};use workspace::{ CloseActiveItem, CloseAllItems, CloseOtherItems, MultiWorkspace, NavigationEntry, OpenOptions, ViewId, item::{FollowEvent, FollowableItem, Item, ItemHandle, SaveOptions}, register_project_item,}; fn display_ranges(editor: &Editor, cx: &mut Context<'_, Editor>) -> Vec<Range<DisplayPoint>> { editor .selections .display_ranges(&editor.display_snapshot(cx))} #[gpui::test]fn test_edit_events(cx: &mut TestAppContext) { init_test(cx, |_| {}); let buffer = cx.new(|cx| { let mut buffer = language::Buffer::local("123456", cx); 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(); |window, cx| { let entity = cx.entity(); cx.subscribe_in( &entity, window, move |_, _, event: &EditorEvent, _, _| match event { EditorEvent::Edited { .. } => events.borrow_mut().push(("editor1", "edited")), EditorEvent::BufferEdited => { events.borrow_mut().push(("editor1", "buffer edited")) } _ => {} }, ) .detach(); Editor::for_buffer(buffer.clone(), None, window, cx) } }); let editor2 = cx.add_window({ let events = events.clone(); |window, cx| { cx.subscribe_in( &cx.entity(), window, move |_, _, event: &EditorEvent, _, _| match event { EditorEvent::Edited { .. } => events.borrow_mut().push(("editor2", "edited")), EditorEvent::BufferEdited => { events.borrow_mut().push(("editor2", "buffer edited")) } _ => {} }, ) .detach(); Editor::for_buffer(buffer.clone(), None, window, 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, window, cx| editor.insert("X", window, cx)); assert_eq!( mem::take(&mut *events.borrow_mut()), [ ("editor1", "edited"), ("editor1", "buffer edited"), ("editor2", "buffer edited"), ] ); // Mutating editor 2 will emit an `Edited` event only for that editor. _ = editor2.update(cx, |editor, window, cx| editor.delete(&Delete, window, cx)); assert_eq!( mem::take(&mut *events.borrow_mut()), [ ("editor2", "edited"), ("editor1", "buffer edited"), ("editor2", "buffer edited"), ] ); // Undoing on editor 1 will emit an `Edited` event only for that editor. _ = editor1.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx)); assert_eq!( mem::take(&mut *events.borrow_mut()), [ ("editor1", "edited"), ("editor1", "buffer edited"), ("editor2", "buffer edited"), ] ); // Redoing on editor 1 will emit an `Edited` event only for that editor. _ = editor1.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx)); assert_eq!( mem::take(&mut *events.borrow_mut()), [ ("editor1", "edited"), ("editor1", "buffer edited"), ("editor2", "buffer edited"), ] ); // Undoing on editor 2 will emit an `Edited` event only for that editor. _ = editor2.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx)); assert_eq!( mem::take(&mut *events.borrow_mut()), [ ("editor2", "edited"), ("editor1", "buffer edited"), ("editor2", "buffer edited"), ] ); // Redoing on editor 2 will emit an `Edited` event only for that editor. _ = editor2.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx)); assert_eq!( mem::take(&mut *events.borrow_mut()), [ ("editor2", "edited"), ("editor1", "buffer edited"), ("editor2", "buffer edited"), ] ); // No event is emitted when the mutation is a no-op. _ = editor2.update(cx, |editor, window, cx| { editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.select_ranges([MultiBufferOffset(0)..MultiBufferOffset(0)]) }); editor.backspace(&Backspace, window, 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 group_interval = Duration::from_millis(1); let buffer = cx.new(|cx| { let mut buf = language::Buffer::local("123456", cx); buf.set_group_interval(group_interval); buf }); let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx)); let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx)); _ = editor.update(cx, |editor, window, cx| { editor.start_transaction_at(now, window, cx); editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.select_ranges([MultiBufferOffset(2)..MultiBufferOffset(4)]) }); editor.insert("cd", window, cx); editor.end_transaction_at(now, cx); assert_eq!(editor.text(cx), "12cd56"); assert_eq!( editor.selections.ranges(&editor.display_snapshot(cx)), vec![MultiBufferOffset(4)..MultiBufferOffset(4)] ); editor.start_transaction_at(now, window, cx); editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.select_ranges([MultiBufferOffset(4)..MultiBufferOffset(5)]) }); editor.insert("e", window, cx); editor.end_transaction_at(now, cx); assert_eq!(editor.text(cx), "12cde6"); assert_eq!( editor.selections.ranges(&editor.display_snapshot(cx)), vec![MultiBufferOffset(5)..MultiBufferOffset(5)] ); now += group_interval + Duration::from_millis(1); editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.select_ranges([MultiBufferOffset(2)..MultiBufferOffset(2)]) }); // Simulate an edit in another editor buffer.update(cx, |buffer, cx| { buffer.start_transaction_at(now, cx); buffer.edit( [(MultiBufferOffset(0)..MultiBufferOffset(1), "a")], None, cx, ); buffer.edit( [(MultiBufferOffset(1)..MultiBufferOffset(1), "b")], None, cx, ); buffer.end_transaction_at(now, cx); }); assert_eq!(editor.text(cx), "ab2cde6"); assert_eq!( editor.selections.ranges(&editor.display_snapshot(cx)), vec![MultiBufferOffset(3)..MultiBufferOffset(3)] ); // Last transaction happened past the group interval in a different editor. // Undo it individually and don't restore selections. editor.undo(&Undo, window, cx); assert_eq!(editor.text(cx), "12cde6"); assert_eq!( editor.selections.ranges(&editor.display_snapshot(cx)), vec![MultiBufferOffset(2)..MultiBufferOffset(2)] ); // First two transactions happened within the group interval in this editor. // Undo them together and restore selections. editor.undo(&Undo, window, cx); editor.undo(&Undo, window, cx); // Undo stack is empty here, so this is a no-op. assert_eq!(editor.text(cx), "123456"); assert_eq!( editor.selections.ranges(&editor.display_snapshot(cx)), vec![MultiBufferOffset(0)..MultiBufferOffset(0)] ); // Redo the first two transactions together. editor.redo(&Redo, window, cx); assert_eq!(editor.text(cx), "12cde6"); assert_eq!( editor.selections.ranges(&editor.display_snapshot(cx)), vec![MultiBufferOffset(5)..MultiBufferOffset(5)] ); // Redo the last transaction on its own. editor.redo(&Redo, window, cx); assert_eq!(editor.text(cx), "ab2cde6"); assert_eq!( editor.selections.ranges(&editor.display_snapshot(cx)), vec![MultiBufferOffset(6)..MultiBufferOffset(6)] ); // Test empty transactions. editor.start_transaction_at(now, window, cx); editor.end_transaction_at(now, cx); editor.undo(&Undo, window, cx); assert_eq!(editor.text(cx), "12cde6"); });} #[gpui::test]fn test_ime_composition(cx: &mut TestAppContext) { init_test(cx, |_| {}); let buffer = cx.new(|cx| { let mut buffer = language::Buffer::local("abcde", cx); // Ensure automatic grouping doesn't occur. buffer.set_group_interval(Duration::ZERO); buffer }); let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx)); cx.add_window(|window, cx| { let mut editor = build_editor(buffer.clone(), window, cx); // Start a new IME composition. editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx); editor.replace_and_mark_text_in_range(Some(0..1), "á", None, window, cx); editor.replace_and_mark_text_in_range(Some(0..1), "ä", None, window, cx); assert_eq!(editor.text(cx), "äbcde"); assert_eq!( editor.marked_text_ranges(cx), Some(vec![ MultiBufferOffsetUtf16(OffsetUtf16(0))..MultiBufferOffsetUtf16(OffsetUtf16(1)) ]) ); // Finalize IME composition. editor.replace_text_in_range(None, "ā", window, 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(), window, cx); assert_eq!(editor.text(cx), "abcde"); assert_eq!(editor.marked_text_ranges(cx), None); editor.redo(&Default::default(), window, 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, window, cx); assert_eq!( editor.marked_text_ranges(cx), Some(vec![ MultiBufferOffsetUtf16(OffsetUtf16(0))..MultiBufferOffsetUtf16(OffsetUtf16(1)) ]) ); // Undoing during an IME composition cancels it. editor.undo(&Default::default(), window, 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, window, cx); assert_eq!(editor.text(cx), "ābcdè"); assert_eq!( editor.marked_text_ranges(cx), Some(vec![ MultiBufferOffsetUtf16(OffsetUtf16(4))..MultiBufferOffsetUtf16(OffsetUtf16(5)) ]) ); // Finalize IME composition with an invalid replacement range, ensuring it gets clipped. editor.replace_text_in_range(Some(4..999), "ę", window, 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(SelectionEffects::no_scroll(), window, cx, |s| { s.select_ranges([ MultiBufferOffsetUtf16(OffsetUtf16(1))..MultiBufferOffsetUtf16(OffsetUtf16(1)), MultiBufferOffsetUtf16(OffsetUtf16(3))..MultiBufferOffsetUtf16(OffsetUtf16(3)), MultiBufferOffsetUtf16(OffsetUtf16(5))..MultiBufferOffsetUtf16(OffsetUtf16(5)), ]) }); editor.replace_and_mark_text_in_range(Some(4..5), "XYZ", None, window, cx); assert_eq!(editor.text(cx), "XYZbXYZdXYZ"); assert_eq!( editor.marked_text_ranges(cx), Some(vec![ MultiBufferOffsetUtf16(OffsetUtf16(0))..MultiBufferOffsetUtf16(OffsetUtf16(3)), MultiBufferOffsetUtf16(OffsetUtf16(4))..MultiBufferOffsetUtf16(OffsetUtf16(7)), MultiBufferOffsetUtf16(OffsetUtf16(8))..MultiBufferOffsetUtf16(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, window, cx); assert_eq!(editor.text(cx), "X1ZbX1ZdX1Z"); assert_eq!( editor.marked_text_ranges(cx), Some(vec![ MultiBufferOffsetUtf16(OffsetUtf16(1))..MultiBufferOffsetUtf16(OffsetUtf16(2)), MultiBufferOffsetUtf16(OffsetUtf16(5))..MultiBufferOffsetUtf16(OffsetUtf16(6)), MultiBufferOffsetUtf16(OffsetUtf16(9))..MultiBufferOffsetUtf16(OffsetUtf16(10)) ]) ); // Finalize IME composition with multiple cursors. editor.replace_text_in_range(Some(9..10), "2", window, 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(|window, cx| { let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx); build_editor(buffer, window, cx) }); _ = editor.update(cx, |editor, window, cx| { editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx); }); assert_eq!( editor .update(cx, |editor, _, cx| display_ranges(editor, cx)) .unwrap(), [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)] ); _ = editor.update(cx, |editor, window, cx| { editor.update_selection( DisplayPoint::new(DisplayRow(3), 3), 0, gpui::Point::<f32>::default(), window, cx, ); }); assert_eq!( editor .update(cx, |editor, _, cx| display_ranges(editor, cx)) .unwrap(), [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)] ); _ = editor.update(cx, |editor, window, cx| { editor.update_selection( DisplayPoint::new(DisplayRow(1), 1), 0, gpui::Point::<f32>::default(), window, cx, ); }); assert_eq!( editor .update(cx, |editor, _, cx| display_ranges(editor, cx)) .unwrap(), [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)] ); _ = editor.update(cx, |editor, window, cx| { editor.end_selection(window, cx); editor.update_selection( DisplayPoint::new(DisplayRow(3), 3), 0, gpui::Point::<f32>::default(), window, cx, ); }); assert_eq!( editor .update(cx, |editor, _, cx| display_ranges(editor, cx)) .unwrap(), [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)] ); Lines 1–500 of 33039. Load more
use super::*; use crate::{ JoinLines, code_context_menus::CodeContextMenu, edit_prediction_tests::FakeEditPredictionDelegate, element::StickyHeader, linked_editing_ranges::LinkedEditingRanges, scroll::scroll_amount::ScrollAmount, test::{ assert_text_with_selections, build_editor, editor_lsp_test_context::{EditorLspTestContext, git_commit_lang}, editor_test_context::EditorTestContext, select_ranges, }, }; use buffer_diff::{BufferDiff, DiffHunkSecondaryStatus, DiffHunkStatus, DiffHunkStatusKind}; use collections::HashMap; use futures::{StreamExt, channel::oneshot}; use gpui::{ BackgroundExecutor, DismissEvent, TestAppContext, UpdateGlobal, VisualTestContext, WindowBounds, WindowOptions, div, }; use indoc::indoc; use language::{ BracketPairConfig, Capability::ReadWrite, DiagnosticSourceKind, FakeLspAdapter, IndentGuideSettings, LanguageConfig, LanguageConfigOverride, LanguageMatcher, LanguageName, Override, Point, language_settings::{ CompletionSettingsContent, FormatterList, LanguageSettingsContent, LspInsertMode, }, tree_sitter_python, }; use language_settings::Formatter; use languages::markdown_lang; use languages::rust_lang; use lsp::{CompletionParams, DEFAULT_LSP_REQUEST_TIMEOUT}; use multi_buffer::{ ExcerptRange, IndentGuide, MultiBuffer, MultiBufferOffset, MultiBufferOffsetUtf16, PathKey, }; use parking_lot::Mutex; use pretty_assertions::{assert_eq, assert_ne}; use project::{ FakeFs, Project, debugger::breakpoint_store::{BreakpointState, SourceBreakpoint}, project_settings::LspSettings, trusted_worktrees::{PathTrust, TrustedWorktrees}, }; use serde_json::{self, json}; use settings::{ AllLanguageSettingsContent, DelayMs, EditorSettingsContent, GlobalLspSettingsContent, IndentGuideBackgroundColoring, IndentGuideColoring, InlayHintSettingsContent, ProjectSettingsContent, SearchSettingsContent, SettingsContent, SettingsStore, }; use std::{cell::RefCell, future::Future, rc::Rc, sync::atomic::AtomicBool, time::Instant}; use std::{ iter, sync::atomic::{self, AtomicUsize}, }; use test::build_editor_with_project; use text::ToPoint as _; use unindent::Unindent; use util::{ assert_set_eq, path, rel_path::rel_path, test::{TextRangeMarker, marked_text_ranges, marked_text_ranges_by, sample_text}, uri, }; use workspace::{ CloseActiveItem, CloseAllItems, CloseOtherItems, MultiWorkspace, NavigationEntry, OpenOptions, ViewId, item::{FollowEvent, FollowableItem, Item, ItemHandle, SaveOptions}, register_project_item, }; fn display_ranges(editor: &Editor, cx: &mut Context<'_, Editor>) -> Vec<Range<DisplayPoint>> { editor .selections .display_ranges(&editor.display_snapshot(cx)) } #[gpui::test] fn test_edit_events(cx: &mut TestAppContext) { init_test(cx, |_| {}); let buffer = cx.new(|cx| { let mut buffer = language::Buffer::local("123456", cx); 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(); |window, cx| { let entity = cx.entity(); cx.subscribe_in( &entity, window, move |_, _, event: &EditorEvent, _, _| match event { EditorEvent::Edited { .. } => events.borrow_mut().push(("editor1", "edited")), EditorEvent::BufferEdited => { events.borrow_mut().push(("editor1", "buffer edited")) } _ => {} }, ) .detach(); Editor::for_buffer(buffer.clone(), None, window, cx) } }); let editor2 = cx.add_window({ let events = events.clone(); |window, cx| { cx.subscribe_in( &cx.entity(), window, move |_, _, event: &EditorEvent, _, _| match event { EditorEvent::Edited { .. } => events.borrow_mut().push(("editor2", "edited")), EditorEvent::BufferEdited => { events.borrow_mut().push(("editor2", "buffer edited")) } _ => {} }, ) .detach(); Editor::for_buffer(buffer.clone(), None, window, 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, window, cx| editor.insert("X", window, cx)); assert_eq!( mem::take(&mut *events.borrow_mut()), [ ("editor1", "edited"), ("editor1", "buffer edited"), ("editor2", "buffer edited"), ] ); // Mutating editor 2 will emit an `Edited` event only for that editor. _ = editor2.update(cx, |editor, window, cx| editor.delete(&Delete, window, cx)); assert_eq!( mem::take(&mut *events.borrow_mut()), [ ("editor2", "edited"), ("editor1", "buffer edited"), ("editor2", "buffer edited"), ] ); // Undoing on editor 1 will emit an `Edited` event only for that editor. _ = editor1.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx)); assert_eq!( mem::take(&mut *events.borrow_mut()), [ ("editor1", "edited"), ("editor1", "buffer edited"), ("editor2", "buffer edited"), ] ); // Redoing on editor 1 will emit an `Edited` event only for that editor. _ = editor1.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx)); assert_eq!( mem::take(&mut *events.borrow_mut()), [ ("editor1", "edited"), ("editor1", "buffer edited"), ("editor2", "buffer edited"), ] ); // Undoing on editor 2 will emit an `Edited` event only for that editor. _ = editor2.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx)); assert_eq!( mem::take(&mut *events.borrow_mut()), [ ("editor2", "edited"), ("editor1", "buffer edited"), ("editor2", "buffer edited"), ] ); // Redoing on editor 2 will emit an `Edited` event only for that editor. _ = editor2.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx)); assert_eq!( mem::take(&mut *events.borrow_mut()), [ ("editor2", "edited"), ("editor1", "buffer edited"), ("editor2", "buffer edited"), ] ); // No event is emitted when the mutation is a no-op. _ = editor2.update(cx, |editor, window, cx| { editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.select_ranges([MultiBufferOffset(0)..MultiBufferOffset(0)]) }); editor.backspace(&Backspace, window, 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 group_interval = Duration::from_millis(1); let buffer = cx.new(|cx| { let mut buf = language::Buffer::local("123456", cx); buf.set_group_interval(group_interval); buf }); let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx)); let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx)); _ = editor.update(cx, |editor, window, cx| { editor.start_transaction_at(now, window, cx); editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.select_ranges([MultiBufferOffset(2)..MultiBufferOffset(4)]) }); editor.insert("cd", window, cx); editor.end_transaction_at(now, cx); assert_eq!(editor.text(cx), "12cd56"); assert_eq!( editor.selections.ranges(&editor.display_snapshot(cx)), vec![MultiBufferOffset(4)..MultiBufferOffset(4)] ); editor.start_transaction_at(now, window, cx); editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.select_ranges([MultiBufferOffset(4)..MultiBufferOffset(5)]) }); editor.insert("e", window, cx); editor.end_transaction_at(now, cx); assert_eq!(editor.text(cx), "12cde6"); assert_eq!( editor.selections.ranges(&editor.display_snapshot(cx)), vec![MultiBufferOffset(5)..MultiBufferOffset(5)] ); now += group_interval + Duration::from_millis(1); editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.select_ranges([MultiBufferOffset(2)..MultiBufferOffset(2)]) }); // Simulate an edit in another editor buffer.update(cx, |buffer, cx| { buffer.start_transaction_at(now, cx); buffer.edit( [(MultiBufferOffset(0)..MultiBufferOffset(1), "a")], None, cx, ); buffer.edit( [(MultiBufferOffset(1)..MultiBufferOffset(1), "b")], None, cx, ); buffer.end_transaction_at(now, cx); }); assert_eq!(editor.text(cx), "ab2cde6"); assert_eq!( editor.selections.ranges(&editor.display_snapshot(cx)), vec![MultiBufferOffset(3)..MultiBufferOffset(3)] ); // Last transaction happened past the group interval in a different editor. // Undo it individually and don't restore selections. editor.undo(&Undo, window, cx); assert_eq!(editor.text(cx), "12cde6"); assert_eq!( editor.selections.ranges(&editor.display_snapshot(cx)), vec![MultiBufferOffset(2)..MultiBufferOffset(2)] ); // First two transactions happened within the group interval in this editor. // Undo them together and restore selections. editor.undo(&Undo, window, cx); editor.undo(&Undo, window, cx); // Undo stack is empty here, so this is a no-op. assert_eq!(editor.text(cx), "123456"); assert_eq!( editor.selections.ranges(&editor.display_snapshot(cx)), vec![MultiBufferOffset(0)..MultiBufferOffset(0)] ); // Redo the first two transactions together. editor.redo(&Redo, window, cx); assert_eq!(editor.text(cx), "12cde6"); assert_eq!( editor.selections.ranges(&editor.display_snapshot(cx)), vec![MultiBufferOffset(5)..MultiBufferOffset(5)] ); // Redo the last transaction on its own. editor.redo(&Redo, window, cx); assert_eq!(editor.text(cx), "ab2cde6"); assert_eq!( editor.selections.ranges(&editor.display_snapshot(cx)), vec![MultiBufferOffset(6)..MultiBufferOffset(6)] ); // Test empty transactions. editor.start_transaction_at(now, window, cx); editor.end_transaction_at(now, cx); editor.undo(&Undo, window, cx); assert_eq!(editor.text(cx), "12cde6"); }); } #[gpui::test] fn test_ime_composition(cx: &mut TestAppContext) { init_test(cx, |_| {}); let buffer = cx.new(|cx| { let mut buffer = language::Buffer::local("abcde", cx); // Ensure automatic grouping doesn't occur. buffer.set_group_interval(Duration::ZERO); buffer }); let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx)); cx.add_window(|window, cx| { let mut editor = build_editor(buffer.clone(), window, cx); // Start a new IME composition. editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx); editor.replace_and_mark_text_in_range(Some(0..1), "á", None, window, cx); editor.replace_and_mark_text_in_range(Some(0..1), "ä", None, window, cx); assert_eq!(editor.text(cx), "äbcde"); assert_eq!( editor.marked_text_ranges(cx), Some(vec![ MultiBufferOffsetUtf16(OffsetUtf16(0))..MultiBufferOffsetUtf16(OffsetUtf16(1)) ]) ); // Finalize IME composition. editor.replace_text_in_range(None, "ā", window, 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(), window, cx); assert_eq!(editor.text(cx), "abcde"); assert_eq!(editor.marked_text_ranges(cx), None); editor.redo(&Default::default(), window, 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, window, cx); assert_eq!( editor.marked_text_ranges(cx), Some(vec![ MultiBufferOffsetUtf16(OffsetUtf16(0))..MultiBufferOffsetUtf16(OffsetUtf16(1)) ]) ); // Undoing during an IME composition cancels it. editor.undo(&Default::default(), window, 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, window, cx); assert_eq!(editor.text(cx), "ābcdè"); assert_eq!( editor.marked_text_ranges(cx), Some(vec![ MultiBufferOffsetUtf16(OffsetUtf16(4))..MultiBufferOffsetUtf16(OffsetUtf16(5)) ]) ); // Finalize IME composition with an invalid replacement range, ensuring it gets clipped. editor.replace_text_in_range(Some(4..999), "ę", window, 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(SelectionEffects::no_scroll(), window, cx, |s| { s.select_ranges([ MultiBufferOffsetUtf16(OffsetUtf16(1))..MultiBufferOffsetUtf16(OffsetUtf16(1)), MultiBufferOffsetUtf16(OffsetUtf16(3))..MultiBufferOffsetUtf16(OffsetUtf16(3)), MultiBufferOffsetUtf16(OffsetUtf16(5))..MultiBufferOffsetUtf16(OffsetUtf16(5)), ]) }); editor.replace_and_mark_text_in_range(Some(4..5), "XYZ", None, window, cx); assert_eq!(editor.text(cx), "XYZbXYZdXYZ"); assert_eq!( editor.marked_text_ranges(cx), Some(vec![ MultiBufferOffsetUtf16(OffsetUtf16(0))..MultiBufferOffsetUtf16(OffsetUtf16(3)), MultiBufferOffsetUtf16(OffsetUtf16(4))..MultiBufferOffsetUtf16(OffsetUtf16(7)), MultiBufferOffsetUtf16(OffsetUtf16(8))..MultiBufferOffsetUtf16(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, window, cx); assert_eq!(editor.text(cx), "X1ZbX1ZdX1Z"); assert_eq!( editor.marked_text_ranges(cx), Some(vec![ MultiBufferOffsetUtf16(OffsetUtf16(1))..MultiBufferOffsetUtf16(OffsetUtf16(2)), MultiBufferOffsetUtf16(OffsetUtf16(5))..MultiBufferOffsetUtf16(OffsetUtf16(6)), MultiBufferOffsetUtf16(OffsetUtf16(9))..MultiBufferOffsetUtf16(OffsetUtf16(10)) ]) ); // Finalize IME composition with multiple cursors. editor.replace_text_in_range(Some(9..10), "2", window, 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(|window, cx| { let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx); build_editor(buffer, window, cx) }); _ = editor.update(cx, |editor, window, cx| { editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx); }); assert_eq!( editor .update(cx, |editor, _, cx| display_ranges(editor, cx)) .unwrap(), [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)] ); _ = editor.update(cx, |editor, window, cx| { editor.update_selection( DisplayPoint::new(DisplayRow(3), 3), 0, gpui::Point::<f32>::default(), window, cx, ); }); assert_eq!( editor .update(cx, |editor, _, cx| display_ranges(editor, cx)) .unwrap(), [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)] ); _ = editor.update(cx, |editor, window, cx| { editor.update_selection( DisplayPoint::new(DisplayRow(1), 1), 0, gpui::Point::<f32>::default(), window, cx, ); }); assert_eq!( editor .update(cx, |editor, _, cx| display_ranges(editor, cx)) .unwrap(), [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)] ); _ = editor.update(cx, |editor, window, cx| { editor.end_selection(window, cx); editor.update_selection( DisplayPoint::new(DisplayRow(3), 3), 0, gpui::Point::<f32>::default(), window, cx, ); }); assert_eq!( editor .update(cx, |editor, _, cx| display_ranges(editor, cx)) .unwrap(), [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)] );