use std::{cell::RefCell, rc::Rc, time::Instant}; use futures::StreamExt; use indoc::indoc; use unindent::Unindent; use super::*; use crate::test::{ assert_text_with_selections, build_editor, editor_lsp_test_context::EditorLspTestContext, editor_test_context::EditorTestContext, select_ranges, }; use gpui::{ geometry::rect::RectF, platform::{WindowBounds, WindowOptions}, }; use language::{FakeLspAdapter, LanguageConfig, LanguageRegistry, Point}; use project::FakeFs; use settings::EditorSettings; use util::{ assert_set_eq, test::{marked_text_ranges, marked_text_ranges_by, sample_text, TextRangeMarker}, }; use workspace::{FollowableItem, ItemHandle, NavigationEntry, Pane}; #[gpui::test] fn test_edit_events(cx: &mut MutableAppContext) { cx.set_global(Settings::test(cx)); let buffer = cx.add_model(|cx| language::Buffer::new(0, "123456", cx)); let events = Rc::new(RefCell::new(Vec::new())); let (_, editor1) = cx.add_window(Default::default(), { 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)); } }) .detach(); Editor::for_buffer(buffer.clone(), None, cx) } }); let (_, editor2) = cx.add_window(Default::default(), { 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)); } }) .detach(); Editor::for_buffer(buffer.clone(), None, 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 MutableAppContext) { cx.set_global(Settings::test(cx)); let mut now = Instant::now(); let buffer = cx.add_model(|cx| language::Buffer::new(0, "123456", cx)); let group_interval = buffer.read(cx).transaction_group_interval(); let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); let (_, editor) = cx.add_window(Default::default(), |cx| build_editor(buffer.clone(), 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 MutableAppContext) { cx.set_global(Settings::test(cx)); let buffer = cx.add_model(|cx| { let mut buffer = language::Buffer::new(0, "abcde", cx); // 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(Default::default(), |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 gpui::MutableAppContext) { cx.set_global(Settings::test(cx)); let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx); let (_, editor) = cx.add_window(Default::default(), |cx| build_editor(buffer, 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 gpui::MutableAppContext) { cx.set_global(Settings::test(cx)); let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx); let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, 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 gpui::MutableAppContext) { let (text, selection_ranges) = marked_text_ranges( indoc! {" one two threeˇ four fiveˇ "}, true, ); cx.set_global(Settings::test(cx)); let buffer = MultiBuffer::build_simple(&text, cx); let (_, editor) = cx.add_window(Default::default(), |cx| build_editor(buffer, 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), ], cx, ); }); let (_, cloned_editor) = editor.update(cx, |editor, cx| { cx.add_window(Default::default(), |cx| editor.clone(cx)) }); let snapshot = editor.update(cx, |e, cx| e.snapshot(cx)); let cloned_snapshot = cloned_editor.update(cx, |e, cx| e.snapshot(cx)); assert_eq!( cloned_editor.update(cx, |e, cx| e.display_text(cx)), editor.update(cx, |e, cx| e.display_text(cx)) ); assert_eq!( cloned_snapshot .folds_in_range(0..text.len()) .collect::>(), snapshot.folds_in_range(0..text.len()).collect::>(), ); assert_set_eq!( cloned_editor.read(cx).selections.ranges::(cx), editor.read(cx).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] fn test_navigation_history(cx: &mut gpui::MutableAppContext) { cx.set_global(Settings::test(cx)); use workspace::Item; let (_, pane) = cx.add_window(Default::default(), |cx| Pane::new(None, cx)); let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx); cx.add_view(&pane, |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 MutableAppContext) -> Option { editor.nav_history.as_mut().unwrap().pop_backward(cx) } // Move the cursor a small distance. // Nothing is added to the navigation history. editor.change_selections(None, cx, |s| { s.select_display_ranges([DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)]) }); editor.change_selections(None, cx, |s| { s.select_display_ranges([DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0)]) }); assert!(pop_history(&mut editor, cx).is_none()); // Move the cursor a large distance. // The history can jump back to the previous position. editor.change_selections(None, cx, |s| { s.select_display_ranges([DisplayPoint::new(13, 0)..DisplayPoint::new(13, 3)]) }); let nav_entry = pop_history(&mut editor, cx).unwrap(); editor.navigate(nav_entry.data.unwrap(), cx); assert_eq!(nav_entry.item.id(), cx.view_id()); assert_eq!( editor.selections.display_ranges(cx), &[DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0)] ); assert!(pop_history(&mut editor, cx).is_none()); // Move the cursor a small distance via the mouse. // Nothing is added to the navigation history. editor.begin_selection(DisplayPoint::new(5, 0), false, 1, cx); editor.end_selection(cx); assert_eq!( editor.selections.display_ranges(cx), &[DisplayPoint::new(5, 0)..DisplayPoint::new(5, 0)] ); assert!(pop_history(&mut editor, cx).is_none()); // Move the cursor a large distance via the mouse. // The history can jump back to the previous position. editor.begin_selection(DisplayPoint::new(15, 0), false, 1, cx); editor.end_selection(cx); assert_eq!( editor.selections.display_ranges(cx), &[DisplayPoint::new(15, 0)..DisplayPoint::new(15, 0)] ); let nav_entry = pop_history(&mut editor, cx).unwrap(); editor.navigate(nav_entry.data.unwrap(), cx); assert_eq!(nav_entry.item.id(), cx.view_id()); assert_eq!( editor.selections.display_ranges(cx), &[DisplayPoint::new(5, 0)..DisplayPoint::new(5, 0)] ); assert!(pop_history(&mut editor, cx).is_none()); // Set scroll position to check later editor.set_scroll_position(Vector2F::new(5.5, 5.5), cx); let original_scroll_position = editor.scroll_position; let original_scroll_top_anchor = editor.scroll_top_anchor.clone(); // 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_position, original_scroll_position); assert_ne!(editor.scroll_top_anchor, original_scroll_top_anchor); let nav_entry = pop_history(&mut editor, cx).unwrap(); editor.navigate(nav_entry.data.unwrap(), cx); assert_eq!(editor.scroll_position, original_scroll_position); assert_eq!(editor.scroll_top_anchor, original_scroll_top_anchor); // Ensure we don't panic when navigation data contains invalid anchors *and* points. let mut invalid_anchor = editor.scroll_top_anchor.clone(); invalid_anchor.text_anchor.buffer_id = Some(999); let invalid_point = Point::new(9999, 0); editor.navigate( Box::new(NavigationData { cursor_anchor: invalid_anchor.clone(), cursor_position: invalid_point, scroll_top_anchor: invalid_anchor, scroll_top_row: invalid_point.row, scroll_position: Default::default(), }), 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 gpui::MutableAppContext) { cx.set_global(Settings::test(cx)); let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx); let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, 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(cx: &mut gpui::MutableAppContext) { cx.set_global(Settings::test(cx)); let buffer = MultiBuffer::build_simple( &" impl Foo { // Hello! fn a() { 1 } fn b() { 2 } fn c() { 3 } } " .unindent(), cx, ); let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer.clone(), 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), buffer.read(cx).read(cx).text()); }); } #[gpui::test] fn test_move_cursor(cx: &mut gpui::MutableAppContext) { cx.set_global(Settings::test(cx)); let buffer = MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx); let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer.clone(), 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 gpui::MutableAppContext) { cx.set_global(Settings::test(cx)); let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcde\nαβγδε\n", cx); let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer.clone(), 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), ], cx, ); assert_eq!(view.display_text(cx), "ⓐⓑ…ⓔ\nab…e\nαβ…ε\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…".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_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())] ); 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 gpui::MutableAppContext) { cx.set_global(Settings::test(cx)); let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx); let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer.clone(), 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 gpui::MutableAppContext) { cx.set_global(Settings::test(cx)); let buffer = MultiBuffer::build_simple("abc\n def", cx); let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, 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 gpui::MutableAppContext) { cx.set_global(Settings::test(cx)); let buffer = MultiBuffer::build_simple("use std::str::{foo, bar}\n\n {baz.qux()}", cx); let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, 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 gpui::MutableAppContext) { cx.set_global(Settings::test(cx)); let buffer = MultiBuffer::build_simple("use one::{\n two::three::four::five\n};", cx); let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, 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_delete_to_beginning_of_line(cx: &mut gpui::TestAppContext) { let mut cx = EditorTestContext::new(cx); 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 gpui::MutableAppContext) { cx.set_global(Settings::test(cx)); let buffer = MultiBuffer::build_simple("one two three four", cx); let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer.clone(), 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!(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!(buffer.read(cx).read(cx).text(), "e t te our"); } #[gpui::test] fn test_newline(cx: &mut gpui::MutableAppContext) { cx.set_global(Settings::test(cx)); let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx); let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer.clone(), 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 gpui::MutableAppContext) { cx.set_global(Settings::test(cx)); let buffer = MultiBuffer::build_simple( " a b( X ) c( X ) " .unindent() .as_str(), cx, ); let (_, editor) = cx.add_window(Default::default(), |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 }); // Edit the buffer directly, deleting ranges surrounding the editor's selections 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() ); }); editor.update(cx, |editor, cx| { 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_below(cx: &mut gpui::TestAppContext) { let mut cx = EditorTestContext::new(cx); cx.update(|cx| { cx.update_global::(|settings, _| { settings.editor_overrides.tab_size = Some(NonZeroU32::new(4).unwrap()); }); }); 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)); 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] fn test_insert_with_old_selections(cx: &mut gpui::MutableAppContext) { cx.set_global(Settings::test(cx)); let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx); let (_, editor) = cx.add_window(Default::default(), |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 }); // Edit the buffer directly, deleting ranges surrounding the editor's selections 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()); }); editor.update(cx, |editor, cx| { 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) { let mut cx = EditorTestContext::new(cx); cx.update(|cx| { cx.update_global::(|settings, _| { settings.editor_overrides.tab_size = Some(NonZeroU32::new(3).unwrap()); }); }); 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_on_blank_line_auto_indents(cx: &mut gpui::TestAppContext) { let mut cx = EditorTestContext::new(cx); 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_indent_outdent(cx: &mut gpui::TestAppContext) { let mut cx = EditorTestContext::new(cx); 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) { let mut cx = EditorTestContext::new(cx); cx.update(|cx| { cx.update_global::(|settings, _| { settings.editor_overrides.hard_tabs = Some(true); }); }); // 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 gpui::MutableAppContext) { cx.set_global( Settings::test(cx) .with_language_defaults( "TOML", EditorSettings { tab_size: Some(2.try_into().unwrap()), ..Default::default() }, ) .with_language_defaults( "Rust", EditorSettings { tab_size: Some(4.try_into().unwrap()), ..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, "a = 1\nb = 2\n", cx).with_language(toml_language, cx)); let rust_buffer = cx.add_model(|cx| { Buffer::new(0, "const c: usize = 3;\n", cx).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(Default::default(), |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) { let mut cx = EditorTestContext::new(cx); // 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) { let mut cx = EditorTestContext::new(cx); 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 gpui::MutableAppContext) { cx.set_global(Settings::test(cx)); let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx); let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, 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) ] ); }); cx.set_global(Settings::test(cx)); let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx); let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, 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_duplicate_line(cx: &mut gpui::MutableAppContext) { cx.set_global(Settings::test(cx)); let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx); let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, 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 buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx); let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, 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 gpui::MutableAppContext) { cx.set_global(Settings::test(cx)); let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx); let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, 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), ], 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 gpui::MutableAppContext) { cx.set_global(Settings::test(cx)); let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx); let snapshot = buffer.read(cx).snapshot(cx); let (_, editor) = cx.add_window(Default::default(), |cx| build_editor(buffer, cx)); editor.update(cx, |editor, 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().boxed()), }], 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 gpui::MutableAppContext) { cx.set_global(Settings::test(cx)); _ = cx .add_window(Default::default(), |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 }) .1; _ = cx .add_window(Default::default(), |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 }) .1; _ = cx .add_window(Default::default(), |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 }) .1; _ = cx .add_window(Default::default(), |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 }) .1; } #[gpui::test] async fn test_clipboard(cx: &mut gpui::TestAppContext) { let mut cx = EditorTestContext::new(cx); 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(indoc! {" ( one✅ three five ) ˇtwo one✅ four three six five ( one✅ three five ) ˇ"}); // 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) { let mut cx = EditorTestContext::new(cx); 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 gpui::MutableAppContext) { cx.set_global(Settings::test(cx)); let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx); let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, 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 gpui::MutableAppContext) { cx.set_global(Settings::test(cx)); let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx); let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, 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 gpui::MutableAppContext) { cx.set_global(Settings::test(cx)); let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx); let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, 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), ], 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 gpui::MutableAppContext) { cx.set_global(Settings::test(cx)); let buffer = MultiBuffer::build_simple("abc\ndefghi\n\njk\nlmno\n", cx); let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, 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) { let mut cx = EditorTestContext::new(cx); cx.set_state("abc\nˇabc abc\ndefabc\nabc"); cx.update_editor(|e, cx| e.select_next(&SelectNext::default(), cx)); cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc"); cx.update_editor(|e, cx| e.select_next(&SelectNext::default(), cx)); 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)); cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»"); cx.update_editor(|e, cx| e.select_next(&SelectNext::default(), cx)); cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»"); } #[gpui::test] async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) { cx.update(|cx| cx.set_global(Settings::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, text, cx).with_language(language, cx)); let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); let (_, view) = cx.add_window(|cx| build_editor(buffer, 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), ], 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) { cx.update(|cx| cx.set_global(Settings::test(cx))); let language = Arc::new( Language::new( LanguageConfig { brackets: 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() }, 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, text, cx).with_language(language, cx)); let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); let (_, editor) = cx.add_window(|cx| build_editor(buffer, 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) { let mut cx = EditorTestContext::new(cx); let language = Arc::new(Language::new( LanguageConfig { brackets: 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: false, newline: true, }, ], 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(), ); // 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"); } #[gpui::test] async fn test_autoclose_with_embedded_language(cx: &mut gpui::TestAppContext) { let mut cx = EditorTestContext::new(cx); let html_language = Arc::new( Language::new( LanguageConfig { name: "HTML".into(), brackets: 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() }, ], 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: 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() }, ], autoclose_before: "})]>".into(), ..Default::default() }, Some(tree_sitter_javascript::language()), )); let registry = Arc::new(LanguageRegistry::test()); registry.add(html_language.clone()); registry.add(javascript_language.clone()); cx.update_buffer(|buffer, cx| { buffer.set_language_registry(registry); buffer.set_language(Some(html_language), cx); }); cx.set_state( &r#" ˇ ˇ "# .unindent(), ); // Precondition: different languages are active at different locations. cx.update_editor(|editor, cx| { let snapshot = editor.snapshot(cx); let cursors = editor.selections.ranges::(cx); let languages = cursors .iter() .map(|c| snapshot.language_at(c.start).unwrap().name()) .collect::>(); assert_eq!( languages, &["HTML".into(), "JavaScript".into(), "HTML".into()] ); }); // Angle brackets autoclose in HTML, but not JavaScript. cx.update_editor(|editor, cx| { editor.handle_input("<", cx); editor.handle_input("a", cx); }); cx.assert_editor_state( &r#" "# .unindent(), ); // Curly braces and parens autoclose in both HTML and JavaScript. cx.update_editor(|editor, cx| { editor.handle_input(" b=", cx); editor.handle_input("{", cx); editor.handle_input("c", cx); editor.handle_input("(", cx); }); cx.assert_editor_state( &r#" "# .unindent(), ); // Brackets that were already autoclosed are skipped. cx.update_editor(|editor, cx| { editor.handle_input(")", cx); editor.handle_input("d", cx); editor.handle_input("}", cx); }); cx.assert_editor_state( &r#" "# .unindent(), ); cx.update_editor(|editor, cx| { editor.handle_input(">", cx); }); cx.assert_editor_state( &r#" ˇ ˇ "# .unindent(), ); // Reset cx.set_state( &r#" ˇ ˇ "# .unindent(), ); cx.update_editor(|editor, cx| { editor.handle_input("<", cx); }); cx.assert_editor_state( &r#" <ˇ> <ˇ> "# .unindent(), ); // When backspacing, the closing angle brackets are removed. cx.update_editor(|editor, cx| { editor.backspace(&Backspace, cx); }); cx.assert_editor_state( &r#" ˇ ˇ "# .unindent(), ); // Block comments autoclose in JavaScript, but not HTML. cx.update_editor(|editor, cx| { editor.handle_input("/", cx); editor.handle_input("*", cx); }); cx.assert_editor_state( &r#" /*ˇ /*ˇ "# .unindent(), ); } #[gpui::test] async fn test_surround_with_pair(cx: &mut gpui::TestAppContext) { cx.update(|cx| cx.set_global(Settings::test(cx))); let language = Arc::new(Language::new( LanguageConfig { brackets: vec![BracketPair { start: "{".to_string(), end: "}".to_string(), close: true, newline: true, }], ..Default::default() }, Some(tree_sitter_rust::language()), )); let text = r#" a b c "# .unindent(); let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx)); let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); let (_, view) = cx.add_window(|cx| build_editor(buffer, 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); 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) ] ); }); } #[gpui::test] async fn test_delete_autoclose_pair(cx: &mut gpui::TestAppContext) { cx.update(|cx| cx.set_global(Settings::test(cx))); let language = Arc::new(Language::new( LanguageConfig { brackets: vec![BracketPair { start: "{".to_string(), end: "}".to_string(), close: true, newline: true, }], 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, text, cx).with_language(language, cx)); let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); let (_, editor) = cx.add_window(|cx| build_editor(buffer, cx)); editor .condition(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx)) .await; editor.update(cx, |editor, cx| { editor.change_selections(None, cx, |s| { s.select_ranges([ Point::new(0, 1)..Point::new(0, 1), Point::new(1, 1)..Point::new(1, 1), Point::new(2, 1)..Point::new(2, 1), ]) }); editor.handle_input("{", cx); editor.handle_input("{", cx); editor.handle_input("_", cx); assert_eq!( editor.text(cx), " a{{_}} b{{_}} c{{_}} " .unindent() ); assert_eq!( editor.selections.ranges::(cx), [ Point::new(0, 4)..Point::new(0, 4), Point::new(1, 4)..Point::new(1, 4), Point::new(2, 4)..Point::new(2, 4) ] ); editor.backspace(&Default::default(), cx); editor.backspace(&Default::default(), cx); assert_eq!( editor.text(cx), " a{} b{} c{} " .unindent() ); assert_eq!( editor.selections.ranges::(cx), [ Point::new(0, 2)..Point::new(0, 2), Point::new(1, 2)..Point::new(1, 2), Point::new(2, 2)..Point::new(2, 2) ] ); editor.delete_to_previous_word_start(&Default::default(), cx); assert_eq!( editor.text(cx), " a b c " .unindent() ); assert_eq!( editor.selections.ranges::(cx), [ Point::new(0, 1)..Point::new(0, 1), Point::new(1, 1)..Point::new(1, 1), Point::new(2, 1)..Point::new(2, 1) ] ); }); } #[gpui::test] async fn test_snippets(cx: &mut gpui::TestAppContext) { cx.update(|cx| cx.set_global(Settings::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)); editor.update(cx, |editor, cx| { let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap(); editor .insert_snippet(&insertion_ranges, snippet, cx) .unwrap(); fn assert(editor: &mut Editor, cx: &mut ViewContext, marked_text: &str) { let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false); assert_eq!(editor.text(cx), expected_text); assert_eq!(editor.selections.ranges::(cx), selection_ranges); } assert( editor, cx, indoc! {" a.f(«one», two, «three») b a.f(«one», two, «three») b a.f(«one», two, «three») b "}, ); // Can't move earlier than the first tab stop assert!(!editor.move_to_prev_snippet_tabstop(cx)); assert( editor, cx, indoc! {" a.f(«one», two, «three») b a.f(«one», two, «three») b a.f(«one», two, «three») b "}, ); assert!(editor.move_to_next_snippet_tabstop(cx)); assert( editor, cx, indoc! {" a.f(one, «two», three) b a.f(one, «two», three) b a.f(one, «two», three) b "}, ); editor.move_to_prev_snippet_tabstop(cx); assert( editor, cx, indoc! {" a.f(«one», two, «three») b a.f(«one», two, «three») b a.f(«one», two, «three») b "}, ); assert!(editor.move_to_next_snippet_tabstop(cx)); assert( editor, cx, indoc! {" a.f(one, «two», three) b a.f(one, «two», three) b a.f(one, «two», three) b "}, ); assert!(editor.move_to_next_snippet_tabstop(cx)); assert( editor, cx, indoc! {" a.f(one, two, three)ˇ b a.f(one, two, three)ˇ b a.f(one, two, three)ˇ b "}, ); // As soon as the last tab stop is reached, snippet state is gone editor.move_to_prev_snippet_tabstop(cx); assert( editor, cx, indoc! {" a.f(one, two, three)ˇ b a.f(one, two, three)ˇ b a.f(one, two, three)ˇ b "}, ); }); } #[gpui::test] async fn test_document_format_during_save(cx: &mut gpui::TestAppContext) { cx.foreground().forbid_parking(); 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)); editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx)); assert!(cx.read(|cx| editor.is_dirty(cx))); let save = cx.update(|cx| editor.save(project.clone(), cx)); fake_server .handle_request::(move |params, _| async move { assert_eq!( params.text_document.uri, lsp::Url::from_file_path("/file.rs").unwrap() ); assert_eq!(params.options.tab_size, 4); Ok(Some(vec![lsp::TextEdit::new( lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)), ", ".to_string(), )])) }) .next() .await; cx.foreground().start_waiting(); save.await.unwrap(); assert_eq!( editor.read_with(cx, |editor, cx| editor.text(cx)), "one, two\nthree\n" ); assert!(!cx.read(|cx| editor.is_dirty(cx))); editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx)); assert!(cx.read(|cx| editor.is_dirty(cx))); // Ensure we can still save even if formatting hangs. fake_server.handle_request::(move |params, _| async move { assert_eq!( params.text_document.uri, lsp::Url::from_file_path("/file.rs").unwrap() ); futures::future::pending::<()>().await; unreachable!() }); let save = cx.update(|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 overriden tabsize is sent to language server cx.update(|cx| { cx.update_global::(|settings, _| { settings.language_overrides.insert( "Rust".into(), EditorSettings { tab_size: Some(8.try_into().unwrap()), ..Default::default() }, ); }) }); let save = cx.update(|cx| editor.save(project.clone(), cx)); fake_server .handle_request::(move |params, _| async move { assert_eq!( params.text_document.uri, lsp::Url::from_file_path("/file.rs").unwrap() ); assert_eq!(params.options.tab_size, 8); Ok(Some(vec![])) }) .next() .await; cx.foreground().start_waiting(); save.await.unwrap(); } #[gpui::test] async fn test_range_format_during_save(cx: &mut gpui::TestAppContext) { cx.foreground().forbid_parking(); 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)); editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx)); assert!(cx.read(|cx| editor.is_dirty(cx))); let save = cx.update(|cx| editor.save(project.clone(), cx)); fake_server .handle_request::(move |params, _| async move { assert_eq!( params.text_document.uri, lsp::Url::from_file_path("/file.rs").unwrap() ); assert_eq!(params.options.tab_size, 4); Ok(Some(vec![lsp::TextEdit::new( lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)), ", ".to_string(), )])) }) .next() .await; cx.foreground().start_waiting(); save.await.unwrap(); assert_eq!( editor.read_with(cx, |editor, cx| editor.text(cx)), "one, two\nthree\n" ); assert!(!cx.read(|cx| editor.is_dirty(cx))); editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx)); assert!(cx.read(|cx| editor.is_dirty(cx))); // Ensure we can still save even if formatting hangs. fake_server.handle_request::( move |params, _| async move { assert_eq!( params.text_document.uri, lsp::Url::from_file_path("/file.rs").unwrap() ); futures::future::pending::<()>().await; unreachable!() }, ); let save = cx.update(|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 overriden tabsize is sent to language server cx.update(|cx| { cx.update_global::(|settings, _| { settings.language_overrides.insert( "Rust".into(), EditorSettings { tab_size: Some(8.try_into().unwrap()), ..Default::default() }, ); }) }); let save = cx.update(|cx| editor.save(project.clone(), cx)); fake_server .handle_request::(move |params, _| async move { assert_eq!( params.text_document.uri, lsp::Url::from_file_path("/file.rs").unwrap() ); assert_eq!(params.options.tab_size, 8); Ok(Some(vec![])) }) .next() .await; cx.foreground().start_waiting(); save.await.unwrap(); } #[gpui::test] async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) { cx.foreground().forbid_parking(); 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)); 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(), cx)); fake_server .handle_request::(move |params, _| async move { assert_eq!( params.text_document.uri, lsp::Url::from_file_path("/file.rs").unwrap() ); assert_eq!(params.options.tab_size, 4); Ok(Some(vec![lsp::TextEdit::new( lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)), ", ".to_string(), )])) }) .next() .await; cx.foreground().start_waiting(); format.await.unwrap(); assert_eq!( editor.read_with(cx, |editor, cx| editor.text(cx)), "one, two\nthree\n" ); editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx)); // Ensure we don't lock if formatting hangs. fake_server.handle_request::(move |params, _| async move { assert_eq!( params.text_document.uri, lsp::Url::from_file_path("/file.rs").unwrap() ); futures::future::pending::<()>().await; unreachable!() }); let format = editor.update(cx, |editor, cx| editor.perform_format(project, 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) { cx.foreground().forbid_parking(); let mut cx = EditorLspTestContext::new_rust( lsp::ServerCapabilities { document_formatting_provider: Some(lsp::OneOf::Left(true)), ..Default::default() }, cx, ) .await; cx.set_state(indoc! {" one.twoˇ "}); // The format request takes a long time. When it completes, it inserts // a newline and an indent before the `.` cx.lsp .handle_request::(move |_, cx| { let executor = cx.background(); async move { executor.timer(Duration::from_millis(100)).await; Ok(Some(vec![lsp::TextEdit { range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)), new_text: "\n ".into(), }])) } }); // Submit a format request. let format_1 = cx .update_editor(|editor, cx| editor.format(&Format, cx)) .unwrap(); cx.foreground().run_until_parked(); // Submit a second format request. let format_2 = cx .update_editor(|editor, cx| editor.format(&Format, cx)) .unwrap(); cx.foreground().run_until_parked(); // Wait for both format requests to complete cx.foreground().advance_clock(Duration::from_millis(200)); cx.foreground().start_waiting(); format_1.await.unwrap(); cx.foreground().start_waiting(); format_2.await.unwrap(); // The formatting edits only happens once. cx.assert_editor_state(indoc! {" one .twoˇ "}); } #[gpui::test] async fn test_completion(cx: &mut gpui::TestAppContext) { 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ˇ 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.move_down(&MoveDown, 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(( 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.is_none())); cx.simulate_keystroke("s"); assert!(cx.editor(|e, _| e.context_menu.is_none())); cx.assert_editor_state(indoc! {" one.second_completion two sˇ three sˇ additional edit "}); handle_completion_request( &mut cx, indoc! {" one.second_completion two s three additional edit "}, vec!["fourth_completion", "fifth_completion", "sixth_completion"], ) .await; cx.condition(|editor, _| editor.context_menu_visible()) .await; cx.simulate_keystroke("i"); handle_completion_request( &mut cx, indoc! {" one.second_completion two si three additional edit "}, vec!["fourth_completion", "fifth_completion", "sixth_completion"], ) .await; cx.condition(|editor, _| editor.context_menu_visible()) .await; let apply_additional_edits = cx.update_editor(|editor, cx| { editor .confirm_completion(&ConfirmCompletion::default(), cx) .unwrap() }); cx.assert_editor_state(indoc! {" one.second_completion two sixth_completionˇ three sixth_completionˇ additional edit "}); handle_resolve_completion_request(&mut cx, None).await; apply_additional_edits.await.unwrap(); cx.update(|cx| { cx.update_global::(|settings, _| { settings.show_completions_on_input = false; }) }); cx.set_state("editorˇ"); cx.simulate_keystroke("."); assert!(cx.editor(|e, _| e.context_menu.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.is_none())); cx.update_editor(|editor, cx| { editor.show_completions(&ShowCompletions, cx); }); handle_completion_request(&mut cx, "editor.", vec!["close", "clobber"]).await; cx.condition(|editor, _| editor.context_menu_visible()) .await; let apply_additional_edits = cx.update_editor(|editor, cx| { editor .confirm_completion(&ConfirmCompletion::default(), cx) .unwrap() }); cx.assert_editor_state("editor.closeˇ"); handle_resolve_completion_request(&mut cx, None).await; apply_additional_edits.await.unwrap(); // 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 async fn handle_completion_request<'a>( cx: &mut EditorLspTestContext<'a>, marked_string: &str, completions: Vec<&'static str>, ) { 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()); cx.handle_request::(move |url, params, _| { let completions = completions.clone(); async move { assert_eq!(params.text_document_position.text_document.uri, url.clone()); assert_eq!( params.text_document_position.position, complete_from_position ); Ok(Some(lsp::CompletionResponse::Array( completions .iter() .map(|completion_text| lsp::CompletionItem { label: completion_text.to_string(), text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { range: replace_range, new_text: completion_text.to_string(), })), ..Default::default() }) .collect(), ))) } }) .next() .await; } async fn handle_resolve_completion_request<'a>( cx: &mut EditorLspTestContext<'a>, edit: Option<(&'static str, &'static str)>, ) { let edit = edit.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()); vec![lsp::TextEdit::new(replace_range, new_text.to_string())] }); cx.handle_request::(move |_, _, _| { let edit = edit.clone(); async move { Ok(lsp::CompletionItem { additional_text_edits: edit, ..Default::default() }) } }) .next() .await; } } #[gpui::test] async fn test_toggle_comment(cx: &mut gpui::TestAppContext) { cx.update(|cx| cx.set_global(Settings::test(cx))); let language = Arc::new(Language::new( LanguageConfig { line_comment: Some("// ".into()), ..Default::default() }, Some(tree_sitter_rust::language()), )); let text = " fn a() { //b(); // c(); // d(); } " .unindent(); let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx)); let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); let (_, view) = cx.add_window(|cx| build_editor(buffer, cx)); view.update(cx, |editor, cx| { // If multiple selections intersect a line, the line is only // toggled once. editor.change_selections(None, cx, |s| { s.select_display_ranges([ DisplayPoint::new(1, 3)..DisplayPoint::new(2, 3), DisplayPoint::new(3, 5)..DisplayPoint::new(3, 6), ]) }); editor.toggle_comments(&ToggleComments, cx); assert_eq!( editor.text(cx), " fn a() { b(); c(); d(); } " .unindent() ); // The comment prefix is inserted at the same column for every line // in a selection. editor.change_selections(None, cx, |s| { s.select_display_ranges([DisplayPoint::new(1, 3)..DisplayPoint::new(3, 6)]) }); editor.toggle_comments(&ToggleComments, cx); assert_eq!( editor.text(cx), " fn a() { // b(); // c(); // d(); } " .unindent() ); // If a selection ends at the beginning of a line, that line is not toggled. editor.change_selections(None, cx, |s| { s.select_display_ranges([DisplayPoint::new(2, 0)..DisplayPoint::new(3, 0)]) }); editor.toggle_comments(&ToggleComments, cx); assert_eq!( editor.text(cx), " fn a() { // b(); c(); // d(); } " .unindent() ); }); } #[gpui::test] async fn test_toggle_block_comment(cx: &mut gpui::TestAppContext) { let mut cx = EditorTestContext::new(cx); let html_language = Arc::new( Language::new( LanguageConfig { name: "HTML".into(), block_comment: Some(("".into())), ..Default::default() }, Some(tree_sitter_html::language()), ) .with_injection_query( r#" (script_element (raw_text) @content (#set! "language" "javascript")) "#, ) .unwrap(), ); let javascript_language = Arc::new(Language::new( LanguageConfig { name: "JavaScript".into(), line_comment: Some("// ".into()), ..Default::default() }, Some(tree_sitter_javascript::language()), )); let registry = Arc::new(LanguageRegistry::test()); registry.add(html_language.clone()); registry.add(javascript_language.clone()); cx.update_buffer(|buffer, cx| { buffer.set_language_registry(registry); buffer.set_language(Some(html_language), cx); }); // Toggle comments for empty selections cx.set_state( &r#"

A

ˇ

B

ˇ

C

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

A

ˇ

B

ˇ

C

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

ˇ»B

ˇ

ˇ»D

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

ˇ»B

ˇ

ˇ»D

ˇ "# .unindent(), ); // Toggle comments when different languages are active for different // selections. cx.set_state( &r#" ˇ "# .unindent(), ); cx.foreground().run_until_parked(); cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments, cx)); cx.assert_editor_state( &r#" // ˇvar x = new Y(); "# .unindent(), ); } #[gpui::test] fn test_editing_disjoint_excerpts(cx: &mut gpui::MutableAppContext) { cx.set_global(Settings::test(cx)); let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(3, 4, 'a'), cx)); 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, ); multibuffer }); assert_eq!(multibuffer.read(cx).read(cx).text(), "aaaa\nbbbb"); let (_, view) = cx.add_window(Default::default(), |cx| build_editor(multibuffer, 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), ] ) }); } #[gpui::test] fn test_editing_overlapping_excerpts(cx: &mut gpui::MutableAppContext) { cx.set_global(Settings::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, initial_text, cx)); 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(Default::default(), |cx| build_editor(multibuffer, 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 gpui::MutableAppContext) { cx.set_global(Settings::test(cx)); let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(3, 4, 'a'), cx)); 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(); multibuffer }); assert_eq!( multibuffer.read(cx).read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc" ); let (_, editor) = cx.add_window(Default::default(), |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 }); // 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 gpui::MutableAppContext) { cx.set_global(Settings::test(cx)); let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(3, 4, 'a'), cx)); 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(); multibuffer }); assert_eq!( multibuffer.read(cx).read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc" ); let (_, editor) = cx.add_window(Default::default(), |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 }); 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) { cx.update(|cx| cx.set_global(Settings::test(cx))); let language = Arc::new( Language::new( LanguageConfig { brackets: 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() }, 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, text, cx).with_language(language, cx)); let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); let (_, view) = cx.add_window(|cx| build_editor(buffer, 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 gpui::MutableAppContext) { let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx); cx.set_global(Settings::test(cx)); let (_, editor) = cx.add_window(Default::default(), |cx| build_editor(buffer.clone(), cx)); editor.update(cx, |editor, cx| { struct Type1; struct Type2; let buffer = buffer.read(cx).snapshot(cx); let anchor_range = |range: Range| buffer.anchor_after(range.start)..buffer.anchor_after(range.end); editor.highlight_background::( vec![ anchor_range(Point::new(2, 1)..Point::new(2, 3)), anchor_range(Point::new(4, 2)..Point::new(4, 4)), anchor_range(Point::new(6, 3)..Point::new(6, 5)), anchor_range(Point::new(8, 4)..Point::new(8, 6)), ], |_| Color::red(), cx, ); editor.highlight_background::( vec![ anchor_range(Point::new(3, 2)..Point::new(3, 5)), anchor_range(Point::new(5, 3)..Point::new(5, 6)), anchor_range(Point::new(7, 4)..Point::new(7, 7)), anchor_range(Point::new(9, 5)..Point::new(9, 8)), ], |_| Color::green(), cx, ); let snapshot = editor.snapshot(cx); let mut highlighted_ranges = editor.background_highlights_in_range( anchor_range(Point::new(3, 4)..Point::new(7, 4)), &snapshot, cx.global::().theme.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, cx.global::().theme.as_ref(), ), &[( DisplayPoint::new(6, 3)..DisplayPoint::new(6, 5), Color::red(), )] ); }); } #[gpui::test] fn test_following(cx: &mut gpui::MutableAppContext) { let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx); cx.set_global(Settings::test(cx)); let (_, leader) = cx.add_window(Default::default(), |cx| build_editor(buffer.clone(), cx)); let (_, follower) = cx.add_window( WindowOptions { bounds: WindowBounds::Fixed(RectF::from_points(vec2f(0., 0.), vec2f(10., 80.))), ..Default::default() }, |cx| build_editor(buffer.clone(), cx), ); let pending_update = Rc::new(RefCell::new(None)); follower.update(cx, { let update = pending_update.clone(); |_, cx| { cx.subscribe(&leader, move |_, leader, event, cx| { leader .read(cx) .add_event_to_update_proto(event, &mut *update.borrow_mut(), cx); }) .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(pending_update.borrow_mut().take().unwrap(), cx) .unwrap(); }); assert_eq!(follower.read(cx).selections.ranges(cx), vec![1..1]); // 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(pending_update.borrow_mut().take().unwrap(), cx) .unwrap(); }); assert_eq!( follower.update(cx, |follower, cx| follower.scroll_position(cx)), vec2f(1.5, 3.5) ); // Update the selections and 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| { let initial_scroll_position = follower.scroll_position(cx); follower .apply_update_proto(pending_update.borrow_mut().take().unwrap(), cx) .unwrap(); assert_eq!(follower.scroll_position(cx), initial_scroll_position); assert!(follower.autoscroll_request.is_some()); }); assert_eq!(follower.read(cx).selections.ranges(cx), vec![0..0]); // 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(pending_update.borrow_mut().take().unwrap(), cx) .unwrap(); }); assert_eq!(follower.read(cx).selections.ranges(cx), vec![0..0, 1..1]); // 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(pending_update.borrow_mut().take().unwrap(), cx) .unwrap(); }); assert_eq!(follower.read(cx).selections.ranges(cx), vec![0..2]); } #[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() }, ), ] ); } fn empty_range(row: usize, column: usize) -> Range { let point = DisplayPoint::new(row as u32, column as u32); point..point } fn assert_selection_ranges(marked_text: &str, view: &mut Editor, cx: &mut ViewContext) { let (text, ranges) = marked_text_ranges(marked_text, true); assert_eq!(view.text(cx), text); assert_eq!( view.selections.ranges(cx), ranges, "Assert selections are {}", marked_text ); }