@@ -9,6 +9,8 @@ pub mod movement;
mod multi_buffer;
pub mod selections_collection;
+#[cfg(test)]
+mod editor_tests;
#[cfg(any(test, feature = "test-support"))]
pub mod test;
@@ -6805,4895 +6807,6 @@ pub fn styled_runs_for_code_label<'a>(
})
}
-#[cfg(test)]
-mod tests {
- use crate::test::{
- assert_text_with_selections, build_editor, select_ranges, EditorLspTestContext,
- EditorTestContext,
- };
-
- use super::*;
- use futures::StreamExt;
- use gpui::{
- geometry::rect::RectF,
- platform::{WindowBounds, WindowOptions},
- };
- use indoc::indoc;
- use language::{FakeLspAdapter, LanguageConfig, LanguageRegistry};
- use project::FakeFs;
- use settings::EditorSettings;
- use std::{cell::RefCell, rc::Rc, time::Instant};
- use text::Point;
- use unindent::Unindent;
- 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::<Vec<_>>(),
- snapshot.folds_in_range(0..text.len()).collect::<Vec<_>>(),
- );
- assert_set_eq!(
- cloned_editor.read(cx).selections.ranges::<Point>(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<NavigationEntry> {
- editor.nav_history.as_mut().unwrap().pop_backward(cx)
- }
-
- // Move the cursor a small distance.
- // Nothing is added to the navigation history.
- editor.change_selections(None, cx, |s| {
- s.select_display_ranges([DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)])
- });
- editor.change_selections(None, cx, |s| {
- s.select_display_ranges([DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0)])
- });
- assert!(pop_history(&mut editor, cx).is_none());
-
- // Move the cursor a large distance.
- // The history can jump back to the previous position.
- editor.change_selections(None, cx, |s| {
- s.select_display_ranges([DisplayPoint::new(13, 0)..DisplayPoint::new(13, 3)])
- });
- let nav_entry = pop_history(&mut editor, cx).unwrap();
- editor.navigate(nav_entry.data.unwrap(), cx);
- assert_eq!(nav_entry.item.id(), cx.view_id());
- assert_eq!(
- editor.selections.display_ranges(cx),
- &[DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0)]
- );
- assert!(pop_history(&mut editor, cx).is_none());
-
- // Move the cursor a small distance via the mouse.
- // Nothing is added to the navigation history.
- editor.begin_selection(DisplayPoint::new(5, 0), false, 1, cx);
- editor.end_selection(cx);
- assert_eq!(
- editor.selections.display_ranges(cx),
- &[DisplayPoint::new(5, 0)..DisplayPoint::new(5, 0)]
- );
- assert!(pop_history(&mut editor, cx).is_none());
-
- // Move the cursor a large distance via the mouse.
- // The history can jump back to the previous position.
- editor.begin_selection(DisplayPoint::new(15, 0), false, 1, cx);
- editor.end_selection(cx);
- assert_eq!(
- editor.selections.display_ranges(cx),
- &[DisplayPoint::new(15, 0)..DisplayPoint::new(15, 0)]
- );
- let nav_entry = pop_history(&mut editor, cx).unwrap();
- editor.navigate(nav_entry.data.unwrap(), cx);
- assert_eq!(nav_entry.item.id(), cx.view_id());
- assert_eq!(
- editor.selections.display_ranges(cx),
- &[DisplayPoint::new(5, 0)..DisplayPoint::new(5, 0)]
- );
- assert!(pop_history(&mut editor, cx).is_none());
-
- // Set scroll position to check later
- editor.set_scroll_position(Vector2F::new(5.5, 5.5), cx);
- let original_scroll_position = editor.scroll_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, _| {
- 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, _| {
- 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, _| {
- 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");
-
- // 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(),
- ..Default::default()
- },
- BracketPair {
- start: "{".into(),
- end: "}".into(),
- ..Default::default()
- },
- BracketPair {
- start: "(".into(),
- end: ")".into(),
- ..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(),
- ..Default::default()
- },
- BracketPair {
- start: "{".into(),
- end: "}".into(),
- ..Default::default()
- },
- BracketPair {
- start: "(".into(),
- end: ")".into(),
- ..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#"
- <body>ˇ
- <script>
- var x = 1;ˇ
- </script>
- </body>ˇ
- "#
- .unindent(),
- );
-
- // Precondition: different languages are active at different locations.
- cx.update_editor(|editor, cx| {
- let snapshot = editor.snapshot(cx);
- let cursors = editor.selections.ranges::<usize>(cx);
- let languages = cursors
- .iter()
- .map(|c| snapshot.language_at(c.start).unwrap().name())
- .collect::<Vec<_>>();
- assert_eq!(
- languages,
- &["HTML".into(), "JavaScript".into(), "HTML".into()]
- );
- });
-
- // Angle brackets autoclose in HTML, but not JavaScript.
- cx.update_editor(|editor, cx| {
- editor.handle_input("<", cx);
- editor.handle_input("a", cx);
- });
- cx.assert_editor_state(
- &r#"
- <body><aˇ>
- <script>
- var x = 1;<aˇ
- </script>
- </body><aˇ>
- "#
- .unindent(),
- );
-
- // Curly braces and parens autoclose in both HTML and JavaScript.
- cx.update_editor(|editor, cx| {
- editor.handle_input(" b=", cx);
- editor.handle_input("{", cx);
- editor.handle_input("c", cx);
- editor.handle_input("(", cx);
- });
- cx.assert_editor_state(
- &r#"
- <body><a b={c(ˇ)}>
- <script>
- var x = 1;<a b={c(ˇ)}
- </script>
- </body><a b={c(ˇ)}>
- "#
- .unindent(),
- );
-
- // Brackets that were already autoclosed are skipped.
- cx.update_editor(|editor, cx| {
- editor.handle_input(")", cx);
- editor.handle_input("d", cx);
- editor.handle_input("}", cx);
- });
- cx.assert_editor_state(
- &r#"
- <body><a b={c()d}ˇ>
- <script>
- var x = 1;<a b={c()d}ˇ
- </script>
- </body><a b={c()d}ˇ>
- "#
- .unindent(),
- );
- cx.update_editor(|editor, cx| {
- editor.handle_input(">", cx);
- });
- cx.assert_editor_state(
- &r#"
- <body><a b={c()d}>ˇ
- <script>
- var x = 1;<a b={c()d}>ˇ
- </script>
- </body><a b={c()d}>ˇ
- "#
- .unindent(),
- );
-
- // Reset
- cx.set_state(
- &r#"
- <body>ˇ
- <script>
- var x = 1;ˇ
- </script>
- </body>ˇ
- "#
- .unindent(),
- );
-
- cx.update_editor(|editor, cx| {
- editor.handle_input("<", cx);
- });
- cx.assert_editor_state(
- &r#"
- <body><ˇ>
- <script>
- var x = 1;<ˇ
- </script>
- </body><ˇ>
- "#
- .unindent(),
- );
-
- // When backspacing, the closing angle brackets are removed.
- cx.update_editor(|editor, cx| {
- editor.backspace(&Backspace, cx);
- });
- cx.assert_editor_state(
- &r#"
- <body>ˇ
- <script>
- var x = 1;ˇ
- </script>
- </body>ˇ
- "#
- .unindent(),
- );
-
- // Block comments autoclose in JavaScript, but not HTML.
- cx.update_editor(|editor, cx| {
- editor.handle_input("/", cx);
- editor.handle_input("*", cx);
- });
- cx.assert_editor_state(
- &r#"
- <body>/*ˇ
- <script>
- var x = 1;/*ˇ */
- </script>
- </body>/*ˇ
- "#
- .unindent(),
- );
- }
-
- #[gpui::test]
- async fn test_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::<Point>(cx),
- [
- Point::new(0, 4)..Point::new(0, 4),
- Point::new(1, 4)..Point::new(1, 4),
- Point::new(2, 4)..Point::new(2, 4)
- ]
- );
-
- editor.backspace(&Default::default(), cx);
- editor.backspace(&Default::default(), cx);
- assert_eq!(
- editor.text(cx),
- "
- a{}
- b{}
- c{}
- "
- .unindent()
- );
- assert_eq!(
- editor.selections.ranges::<Point>(cx),
- [
- Point::new(0, 2)..Point::new(0, 2),
- Point::new(1, 2)..Point::new(1, 2),
- Point::new(2, 2)..Point::new(2, 2)
- ]
- );
-
- editor.delete_to_previous_word_start(&Default::default(), cx);
- assert_eq!(
- editor.text(cx),
- "
- a
- b
- c
- "
- .unindent()
- );
- assert_eq!(
- editor.selections.ranges::<Point>(cx),
- [
- Point::new(0, 1)..Point::new(0, 1),
- Point::new(1, 1)..Point::new(1, 1),
- Point::new(2, 1)..Point::new(2, 1)
- ]
- );
- });
- }
-
- #[gpui::test]
- async fn test_snippets(cx: &mut gpui::TestAppContext) {
- 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<Editor>, marked_text: &str) {
- let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
- assert_eq!(editor.text(cx), expected_text);
- assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
- }
-
- assert(
- editor,
- cx,
- indoc! {"
- a.f(«one», two, «three») b
- a.f(«one», two, «three») b
- a.f(«one», two, «three») b
- "},
- );
-
- // Can't move earlier than the first tab stop
- assert!(!editor.move_to_prev_snippet_tabstop(cx));
- assert(
- editor,
- cx,
- indoc! {"
- a.f(«one», two, «three») b
- a.f(«one», two, «three») b
- a.f(«one», two, «three») b
- "},
- );
-
- assert!(editor.move_to_next_snippet_tabstop(cx));
- assert(
- editor,
- cx,
- indoc! {"
- a.f(one, «two», three) b
- a.f(one, «two», three) b
- a.f(one, «two», three) b
- "},
- );
-
- editor.move_to_prev_snippet_tabstop(cx);
- assert(
- editor,
- cx,
- indoc! {"
- a.f(«one», two, «three») b
- a.f(«one», two, «three») b
- a.f(«one», two, «three») b
- "},
- );
-
- assert!(editor.move_to_next_snippet_tabstop(cx));
- assert(
- editor,
- cx,
- indoc! {"
- a.f(one, «two», three) b
- a.f(one, «two», three) b
- a.f(one, «two», three) b
- "},
- );
- assert!(editor.move_to_next_snippet_tabstop(cx));
- assert(
- editor,
- cx,
- indoc! {"
- a.f(one, two, three)ˇ b
- a.f(one, two, three)ˇ b
- a.f(one, two, three)ˇ b
- "},
- );
-
- // As soon as the last tab stop is reached, snippet state is gone
- editor.move_to_prev_snippet_tabstop(cx);
- assert(
- editor,
- cx,
- indoc! {"
- a.f(one, two, three)ˇ b
- a.f(one, two, three)ˇ b
- a.f(one, two, three)ˇ b
- "},
- );
- });
- }
-
- #[gpui::test]
- async fn test_document_format_during_save(cx: &mut gpui::TestAppContext) {
- 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::<lsp::request::Formatting, _, _>(move |params, _| async move {
- assert_eq!(
- params.text_document.uri,
- lsp::Url::from_file_path("/file.rs").unwrap()
- );
- assert_eq!(params.options.tab_size, 4);
- Ok(Some(vec![lsp::TextEdit::new(
- lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
- ", ".to_string(),
- )]))
- })
- .next()
- .await;
- cx.foreground().start_waiting();
- save.await.unwrap();
- assert_eq!(
- editor.read_with(cx, |editor, cx| editor.text(cx)),
- "one, two\nthree\n"
- );
- assert!(!cx.read(|cx| editor.is_dirty(cx)));
-
- editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx));
- assert!(cx.read(|cx| editor.is_dirty(cx)));
-
- // Ensure we can still save even if formatting hangs.
- fake_server.handle_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
- assert_eq!(
- params.text_document.uri,
- lsp::Url::from_file_path("/file.rs").unwrap()
- );
- futures::future::pending::<()>().await;
- unreachable!()
- });
- let save = 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, _| {
- 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::<lsp::request::Formatting, _, _>(move |params, _| async move {
- assert_eq!(
- params.text_document.uri,
- lsp::Url::from_file_path("/file.rs").unwrap()
- );
- assert_eq!(params.options.tab_size, 8);
- Ok(Some(vec![]))
- })
- .next()
- .await;
- cx.foreground().start_waiting();
- save.await.unwrap();
- }
-
- #[gpui::test]
- async fn test_range_format_during_save(cx: &mut gpui::TestAppContext) {
- 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::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
- assert_eq!(
- params.text_document.uri,
- lsp::Url::from_file_path("/file.rs").unwrap()
- );
- assert_eq!(params.options.tab_size, 4);
- Ok(Some(vec![lsp::TextEdit::new(
- lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
- ", ".to_string(),
- )]))
- })
- .next()
- .await;
- cx.foreground().start_waiting();
- save.await.unwrap();
- assert_eq!(
- editor.read_with(cx, |editor, cx| editor.text(cx)),
- "one, two\nthree\n"
- );
- assert!(!cx.read(|cx| editor.is_dirty(cx)));
-
- editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx));
- assert!(cx.read(|cx| editor.is_dirty(cx)));
-
- // Ensure we can still save even if formatting hangs.
- fake_server.handle_request::<lsp::request::RangeFormatting, _, _>(
- move |params, _| async move {
- assert_eq!(
- params.text_document.uri,
- lsp::Url::from_file_path("/file.rs").unwrap()
- );
- futures::future::pending::<()>().await;
- unreachable!()
- },
- );
- let save = 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, _| {
- 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::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
- assert_eq!(
- params.text_document.uri,
- lsp::Url::from_file_path("/file.rs").unwrap()
- );
- assert_eq!(params.options.tab_size, 8);
- Ok(Some(vec![]))
- })
- .next()
- .await;
- cx.foreground().start_waiting();
- save.await.unwrap();
- }
-
- #[gpui::test]
- async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) {
- 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::<lsp::request::Formatting, _, _>(move |params, _| async move {
- assert_eq!(
- params.text_document.uri,
- lsp::Url::from_file_path("/file.rs").unwrap()
- );
- assert_eq!(params.options.tab_size, 4);
- Ok(Some(vec![lsp::TextEdit::new(
- lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
- ", ".to_string(),
- )]))
- })
- .next()
- .await;
- cx.foreground().start_waiting();
- format.await.unwrap();
- assert_eq!(
- editor.read_with(cx, |editor, cx| editor.text(cx)),
- "one, two\nthree\n"
- );
-
- editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx));
- // Ensure we don't lock if formatting hangs.
- fake_server.handle_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
- assert_eq!(
- params.text_document.uri,
- lsp::Url::from_file_path("/file.rs").unwrap()
- );
- futures::future::pending::<()>().await;
- unreachable!()
- });
- let format = editor.update(cx, |editor, cx| editor.perform_format(project, 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_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 <s|>
- additional edit
- "},
- vec!["fourth_completion", "fifth_completion", "sixth_completion"],
- )
- .await;
- cx.condition(|editor, _| editor.context_menu_visible())
- .await;
-
- cx.simulate_keystroke("i");
-
- handle_completion_request(
- &mut cx,
- indoc! {"
- one.second_completion
- two si
- three <si|>
- additional edit
- "},
- vec!["fourth_completion", "fifth_completion", "sixth_completion"],
- )
- .await;
- cx.condition(|editor, _| editor.context_menu_visible())
- .await;
-
- let apply_additional_edits = cx.update_editor(|editor, cx| {
- editor
- .confirm_completion(&ConfirmCompletion::default(), cx)
- .unwrap()
- });
- cx.assert_editor_state(indoc! {"
- one.second_completion
- two sixth_completionˇ
- three sixth_completionˇ
- additional edit
- "});
-
- handle_resolve_completion_request(&mut cx, None).await;
- apply_additional_edits.await.unwrap();
-
- cx.update(|cx| {
- cx.update_global::<Settings, _, _>(|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.<clo|>", vec!["close", "clobber"]).await;
- cx.condition(|editor, _| editor.context_menu_visible())
- .await;
- let apply_additional_edits = cx.update_editor(|editor, cx| {
- editor
- .confirm_completion(&ConfirmCompletion::default(), cx)
- .unwrap()
- });
- cx.assert_editor_state("editor.closeˇ");
- handle_resolve_completion_request(&mut cx, None).await;
- apply_additional_edits.await.unwrap();
-
- // 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::<lsp::request::Completion, _, _>(move |url, params, _| {
- let completions = completions.clone();
- async move {
- assert_eq!(params.text_document_position.text_document.uri, url.clone());
- assert_eq!(
- params.text_document_position.position,
- complete_from_position
- );
- Ok(Some(lsp::CompletionResponse::Array(
- completions
- .iter()
- .map(|completion_text| lsp::CompletionItem {
- label: completion_text.to_string(),
- text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
- range: replace_range,
- new_text: completion_text.to_string(),
- })),
- ..Default::default()
- })
- .collect(),
- )))
- }
- })
- .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::<lsp::request::ResolveCompletionItem, _, _>(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(), " -->".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#"
- <p>A</p>ˇ
- <p>B</p>ˇ
- <p>C</p>ˇ
- "#
- .unindent(),
- );
- cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments, cx));
- cx.assert_editor_state(
- &r#"
- <!-- <p>A</p>ˇ -->
- <!-- <p>B</p>ˇ -->
- <!-- <p>C</p>ˇ -->
- "#
- .unindent(),
- );
- cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments, cx));
- cx.assert_editor_state(
- &r#"
- <p>A</p>ˇ
- <p>B</p>ˇ
- <p>C</p>ˇ
- "#
- .unindent(),
- );
-
- // Toggle comments for mixture of empty and non-empty selections, where
- // multiple selections occupy a given line.
- cx.set_state(
- &r#"
- <p>A«</p>
- <p>ˇ»B</p>ˇ
- <p>C«</p>
- <p>ˇ»D</p>ˇ
- "#
- .unindent(),
- );
-
- cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments, cx));
- cx.assert_editor_state(
- &r#"
- <!-- <p>A«</p>
- <p>ˇ»B</p>ˇ -->
- <!-- <p>C«</p>
- <p>ˇ»D</p>ˇ -->
- "#
- .unindent(),
- );
- cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments, cx));
- cx.assert_editor_state(
- &r#"
- <p>A«</p>
- <p>ˇ»B</p>ˇ
- <p>C«</p>
- <p>ˇ»D</p>ˇ
- "#
- .unindent(),
- );
-
- // Toggle comments when different languages are active for different
- // selections.
- cx.set_state(
- &r#"
- ˇ<script>
- ˇvar x = new Y();
- ˇ</script>
- "#
- .unindent(),
- );
- cx.foreground().run_until_parked();
- cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments, cx));
- cx.assert_editor_state(
- &r#"
- <!-- ˇ<script> -->
- // ˇvar x = new Y();
- <!-- ˇ</script> -->
- "#
- .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", // Suppress rustfmt
- " 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<Point>| {
- buffer.anchor_after(range.start)..buffer.anchor_after(range.end)
- };
-
- editor.highlight_background::<Type1>(
- vec![
- anchor_range(Point::new(2, 1)..Point::new(2, 3)),
- anchor_range(Point::new(4, 2)..Point::new(4, 4)),
- anchor_range(Point::new(6, 3)..Point::new(6, 5)),
- anchor_range(Point::new(8, 4)..Point::new(8, 6)),
- ],
- |_| Color::red(),
- cx,
- );
- editor.highlight_background::<Type2>(
- vec![
- anchor_range(Point::new(3, 2)..Point::new(3, 5)),
- anchor_range(Point::new(5, 3)..Point::new(5, 6)),
- anchor_range(Point::new(7, 4)..Point::new(7, 7)),
- anchor_range(Point::new(9, 5)..Point::new(9, 8)),
- ],
- |_| Color::green(),
- cx,
- );
-
- let snapshot = editor.snapshot(cx);
- let mut highlighted_ranges = editor.background_highlights_in_range(
- anchor_range(Point::new(3, 4)..Point::new(7, 4)),
- &snapshot,
- cx.global::<Settings>().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::<Settings>().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<DisplayPoint> {
- let point = DisplayPoint::new(row as u32, column as u32);
- point..point
- }
-
- fn assert_selection_ranges(marked_text: &str, view: &mut Editor, cx: &mut ViewContext<Editor>) {
- let (text, ranges) = marked_text_ranges(marked_text, true);
- assert_eq!(view.text(cx), text);
- assert_eq!(
- view.selections.ranges(cx),
- ranges,
- "Assert selections are {}",
- marked_text
- );
- }
-}
-
trait RangeExt<T> {
fn sorted(&self) -> Range<T>;
fn to_inclusive(&self) -> RangeInclusive<T>;
@@ -0,0 +1,4881 @@
+use crate::test::{
+ assert_text_with_selections, build_editor, select_ranges, EditorLspTestContext,
+ EditorTestContext,
+};
+
+use super::*;
+use futures::StreamExt;
+use gpui::{
+ geometry::rect::RectF,
+ platform::{WindowBounds, WindowOptions},
+};
+use indoc::indoc;
+use language::{FakeLspAdapter, LanguageConfig, LanguageRegistry};
+use project::FakeFs;
+use settings::EditorSettings;
+use std::{cell::RefCell, rc::Rc, time::Instant};
+use text::Point;
+use unindent::Unindent;
+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::<Vec<_>>(),
+ snapshot.folds_in_range(0..text.len()).collect::<Vec<_>>(),
+ );
+ assert_set_eq!(
+ cloned_editor.read(cx).selections.ranges::<Point>(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<NavigationEntry> {
+ editor.nav_history.as_mut().unwrap().pop_backward(cx)
+ }
+
+ // Move the cursor a small distance.
+ // Nothing is added to the navigation history.
+ editor.change_selections(None, cx, |s| {
+ s.select_display_ranges([DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)])
+ });
+ editor.change_selections(None, cx, |s| {
+ s.select_display_ranges([DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0)])
+ });
+ assert!(pop_history(&mut editor, cx).is_none());
+
+ // Move the cursor a large distance.
+ // The history can jump back to the previous position.
+ editor.change_selections(None, cx, |s| {
+ s.select_display_ranges([DisplayPoint::new(13, 0)..DisplayPoint::new(13, 3)])
+ });
+ let nav_entry = pop_history(&mut editor, cx).unwrap();
+ editor.navigate(nav_entry.data.unwrap(), cx);
+ assert_eq!(nav_entry.item.id(), cx.view_id());
+ assert_eq!(
+ editor.selections.display_ranges(cx),
+ &[DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0)]
+ );
+ assert!(pop_history(&mut editor, cx).is_none());
+
+ // Move the cursor a small distance via the mouse.
+ // Nothing is added to the navigation history.
+ editor.begin_selection(DisplayPoint::new(5, 0), false, 1, cx);
+ editor.end_selection(cx);
+ assert_eq!(
+ editor.selections.display_ranges(cx),
+ &[DisplayPoint::new(5, 0)..DisplayPoint::new(5, 0)]
+ );
+ assert!(pop_history(&mut editor, cx).is_none());
+
+ // Move the cursor a large distance via the mouse.
+ // The history can jump back to the previous position.
+ editor.begin_selection(DisplayPoint::new(15, 0), false, 1, cx);
+ editor.end_selection(cx);
+ assert_eq!(
+ editor.selections.display_ranges(cx),
+ &[DisplayPoint::new(15, 0)..DisplayPoint::new(15, 0)]
+ );
+ let nav_entry = pop_history(&mut editor, cx).unwrap();
+ editor.navigate(nav_entry.data.unwrap(), cx);
+ assert_eq!(nav_entry.item.id(), cx.view_id());
+ assert_eq!(
+ editor.selections.display_ranges(cx),
+ &[DisplayPoint::new(5, 0)..DisplayPoint::new(5, 0)]
+ );
+ assert!(pop_history(&mut editor, cx).is_none());
+
+ // Set scroll position to check later
+ editor.set_scroll_position(Vector2F::new(5.5, 5.5), cx);
+ let original_scroll_position = editor.scroll_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, _| {
+ 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, _| {
+ 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, _| {
+ 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");
+
+ // 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(),
+ ..Default::default()
+ },
+ BracketPair {
+ start: "{".into(),
+ end: "}".into(),
+ ..Default::default()
+ },
+ BracketPair {
+ start: "(".into(),
+ end: ")".into(),
+ ..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(),
+ ..Default::default()
+ },
+ BracketPair {
+ start: "{".into(),
+ end: "}".into(),
+ ..Default::default()
+ },
+ BracketPair {
+ start: "(".into(),
+ end: ")".into(),
+ ..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#"
+ <body>ˇ
+ <script>
+ var x = 1;ˇ
+ </script>
+ </body>ˇ
+ "#
+ .unindent(),
+ );
+
+ // Precondition: different languages are active at different locations.
+ cx.update_editor(|editor, cx| {
+ let snapshot = editor.snapshot(cx);
+ let cursors = editor.selections.ranges::<usize>(cx);
+ let languages = cursors
+ .iter()
+ .map(|c| snapshot.language_at(c.start).unwrap().name())
+ .collect::<Vec<_>>();
+ assert_eq!(
+ languages,
+ &["HTML".into(), "JavaScript".into(), "HTML".into()]
+ );
+ });
+
+ // Angle brackets autoclose in HTML, but not JavaScript.
+ cx.update_editor(|editor, cx| {
+ editor.handle_input("<", cx);
+ editor.handle_input("a", cx);
+ });
+ cx.assert_editor_state(
+ &r#"
+ <body><aˇ>
+ <script>
+ var x = 1;<aˇ
+ </script>
+ </body><aˇ>
+ "#
+ .unindent(),
+ );
+
+ // Curly braces and parens autoclose in both HTML and JavaScript.
+ cx.update_editor(|editor, cx| {
+ editor.handle_input(" b=", cx);
+ editor.handle_input("{", cx);
+ editor.handle_input("c", cx);
+ editor.handle_input("(", cx);
+ });
+ cx.assert_editor_state(
+ &r#"
+ <body><a b={c(ˇ)}>
+ <script>
+ var x = 1;<a b={c(ˇ)}
+ </script>
+ </body><a b={c(ˇ)}>
+ "#
+ .unindent(),
+ );
+
+ // Brackets that were already autoclosed are skipped.
+ cx.update_editor(|editor, cx| {
+ editor.handle_input(")", cx);
+ editor.handle_input("d", cx);
+ editor.handle_input("}", cx);
+ });
+ cx.assert_editor_state(
+ &r#"
+ <body><a b={c()d}ˇ>
+ <script>
+ var x = 1;<a b={c()d}ˇ
+ </script>
+ </body><a b={c()d}ˇ>
+ "#
+ .unindent(),
+ );
+ cx.update_editor(|editor, cx| {
+ editor.handle_input(">", cx);
+ });
+ cx.assert_editor_state(
+ &r#"
+ <body><a b={c()d}>ˇ
+ <script>
+ var x = 1;<a b={c()d}>ˇ
+ </script>
+ </body><a b={c()d}>ˇ
+ "#
+ .unindent(),
+ );
+
+ // Reset
+ cx.set_state(
+ &r#"
+ <body>ˇ
+ <script>
+ var x = 1;ˇ
+ </script>
+ </body>ˇ
+ "#
+ .unindent(),
+ );
+
+ cx.update_editor(|editor, cx| {
+ editor.handle_input("<", cx);
+ });
+ cx.assert_editor_state(
+ &r#"
+ <body><ˇ>
+ <script>
+ var x = 1;<ˇ
+ </script>
+ </body><ˇ>
+ "#
+ .unindent(),
+ );
+
+ // When backspacing, the closing angle brackets are removed.
+ cx.update_editor(|editor, cx| {
+ editor.backspace(&Backspace, cx);
+ });
+ cx.assert_editor_state(
+ &r#"
+ <body>ˇ
+ <script>
+ var x = 1;ˇ
+ </script>
+ </body>ˇ
+ "#
+ .unindent(),
+ );
+
+ // Block comments autoclose in JavaScript, but not HTML.
+ cx.update_editor(|editor, cx| {
+ editor.handle_input("/", cx);
+ editor.handle_input("*", cx);
+ });
+ cx.assert_editor_state(
+ &r#"
+ <body>/*ˇ
+ <script>
+ var x = 1;/*ˇ */
+ </script>
+ </body>/*ˇ
+ "#
+ .unindent(),
+ );
+}
+
+#[gpui::test]
+async fn test_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::<Point>(cx),
+ [
+ Point::new(0, 4)..Point::new(0, 4),
+ Point::new(1, 4)..Point::new(1, 4),
+ Point::new(2, 4)..Point::new(2, 4)
+ ]
+ );
+
+ editor.backspace(&Default::default(), cx);
+ editor.backspace(&Default::default(), cx);
+ assert_eq!(
+ editor.text(cx),
+ "
+ a{}
+ b{}
+ c{}
+ "
+ .unindent()
+ );
+ assert_eq!(
+ editor.selections.ranges::<Point>(cx),
+ [
+ Point::new(0, 2)..Point::new(0, 2),
+ Point::new(1, 2)..Point::new(1, 2),
+ Point::new(2, 2)..Point::new(2, 2)
+ ]
+ );
+
+ editor.delete_to_previous_word_start(&Default::default(), cx);
+ assert_eq!(
+ editor.text(cx),
+ "
+ a
+ b
+ c
+ "
+ .unindent()
+ );
+ assert_eq!(
+ editor.selections.ranges::<Point>(cx),
+ [
+ Point::new(0, 1)..Point::new(0, 1),
+ Point::new(1, 1)..Point::new(1, 1),
+ Point::new(2, 1)..Point::new(2, 1)
+ ]
+ );
+ });
+}
+
+#[gpui::test]
+async fn test_snippets(cx: &mut gpui::TestAppContext) {
+ 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<Editor>, marked_text: &str) {
+ let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
+ assert_eq!(editor.text(cx), expected_text);
+ assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
+ }
+
+ assert(
+ editor,
+ cx,
+ indoc! {"
+ a.f(«one», two, «three») b
+ a.f(«one», two, «three») b
+ a.f(«one», two, «three») b
+ "},
+ );
+
+ // Can't move earlier than the first tab stop
+ assert!(!editor.move_to_prev_snippet_tabstop(cx));
+ assert(
+ editor,
+ cx,
+ indoc! {"
+ a.f(«one», two, «three») b
+ a.f(«one», two, «three») b
+ a.f(«one», two, «three») b
+ "},
+ );
+
+ assert!(editor.move_to_next_snippet_tabstop(cx));
+ assert(
+ editor,
+ cx,
+ indoc! {"
+ a.f(one, «two», three) b
+ a.f(one, «two», three) b
+ a.f(one, «two», three) b
+ "},
+ );
+
+ editor.move_to_prev_snippet_tabstop(cx);
+ assert(
+ editor,
+ cx,
+ indoc! {"
+ a.f(«one», two, «three») b
+ a.f(«one», two, «three») b
+ a.f(«one», two, «three») b
+ "},
+ );
+
+ assert!(editor.move_to_next_snippet_tabstop(cx));
+ assert(
+ editor,
+ cx,
+ indoc! {"
+ a.f(one, «two», three) b
+ a.f(one, «two», three) b
+ a.f(one, «two», three) b
+ "},
+ );
+ assert!(editor.move_to_next_snippet_tabstop(cx));
+ assert(
+ editor,
+ cx,
+ indoc! {"
+ a.f(one, two, three)ˇ b
+ a.f(one, two, three)ˇ b
+ a.f(one, two, three)ˇ b
+ "},
+ );
+
+ // As soon as the last tab stop is reached, snippet state is gone
+ editor.move_to_prev_snippet_tabstop(cx);
+ assert(
+ editor,
+ cx,
+ indoc! {"
+ a.f(one, two, three)ˇ b
+ a.f(one, two, three)ˇ b
+ a.f(one, two, three)ˇ b
+ "},
+ );
+ });
+}
+
+#[gpui::test]
+async fn test_document_format_during_save(cx: &mut gpui::TestAppContext) {
+ 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::<lsp::request::Formatting, _, _>(move |params, _| async move {
+ assert_eq!(
+ params.text_document.uri,
+ lsp::Url::from_file_path("/file.rs").unwrap()
+ );
+ assert_eq!(params.options.tab_size, 4);
+ Ok(Some(vec![lsp::TextEdit::new(
+ lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
+ ", ".to_string(),
+ )]))
+ })
+ .next()
+ .await;
+ cx.foreground().start_waiting();
+ save.await.unwrap();
+ assert_eq!(
+ editor.read_with(cx, |editor, cx| editor.text(cx)),
+ "one, two\nthree\n"
+ );
+ assert!(!cx.read(|cx| editor.is_dirty(cx)));
+
+ editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx));
+ assert!(cx.read(|cx| editor.is_dirty(cx)));
+
+ // Ensure we can still save even if formatting hangs.
+ fake_server.handle_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
+ assert_eq!(
+ params.text_document.uri,
+ lsp::Url::from_file_path("/file.rs").unwrap()
+ );
+ futures::future::pending::<()>().await;
+ unreachable!()
+ });
+ let save = 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, _| {
+ 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::<lsp::request::Formatting, _, _>(move |params, _| async move {
+ assert_eq!(
+ params.text_document.uri,
+ lsp::Url::from_file_path("/file.rs").unwrap()
+ );
+ assert_eq!(params.options.tab_size, 8);
+ Ok(Some(vec![]))
+ })
+ .next()
+ .await;
+ cx.foreground().start_waiting();
+ save.await.unwrap();
+}
+
+#[gpui::test]
+async fn test_range_format_during_save(cx: &mut gpui::TestAppContext) {
+ 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::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
+ assert_eq!(
+ params.text_document.uri,
+ lsp::Url::from_file_path("/file.rs").unwrap()
+ );
+ assert_eq!(params.options.tab_size, 4);
+ Ok(Some(vec![lsp::TextEdit::new(
+ lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
+ ", ".to_string(),
+ )]))
+ })
+ .next()
+ .await;
+ cx.foreground().start_waiting();
+ save.await.unwrap();
+ assert_eq!(
+ editor.read_with(cx, |editor, cx| editor.text(cx)),
+ "one, two\nthree\n"
+ );
+ assert!(!cx.read(|cx| editor.is_dirty(cx)));
+
+ editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx));
+ assert!(cx.read(|cx| editor.is_dirty(cx)));
+
+ // Ensure we can still save even if formatting hangs.
+ fake_server.handle_request::<lsp::request::RangeFormatting, _, _>(
+ move |params, _| async move {
+ assert_eq!(
+ params.text_document.uri,
+ lsp::Url::from_file_path("/file.rs").unwrap()
+ );
+ futures::future::pending::<()>().await;
+ unreachable!()
+ },
+ );
+ let save = 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, _| {
+ 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::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
+ assert_eq!(
+ params.text_document.uri,
+ lsp::Url::from_file_path("/file.rs").unwrap()
+ );
+ assert_eq!(params.options.tab_size, 8);
+ Ok(Some(vec![]))
+ })
+ .next()
+ .await;
+ cx.foreground().start_waiting();
+ save.await.unwrap();
+}
+
+#[gpui::test]
+async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) {
+ 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::<lsp::request::Formatting, _, _>(move |params, _| async move {
+ assert_eq!(
+ params.text_document.uri,
+ lsp::Url::from_file_path("/file.rs").unwrap()
+ );
+ assert_eq!(params.options.tab_size, 4);
+ Ok(Some(vec![lsp::TextEdit::new(
+ lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
+ ", ".to_string(),
+ )]))
+ })
+ .next()
+ .await;
+ cx.foreground().start_waiting();
+ format.await.unwrap();
+ assert_eq!(
+ editor.read_with(cx, |editor, cx| editor.text(cx)),
+ "one, two\nthree\n"
+ );
+
+ editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx));
+ // Ensure we don't lock if formatting hangs.
+ fake_server.handle_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
+ assert_eq!(
+ params.text_document.uri,
+ lsp::Url::from_file_path("/file.rs").unwrap()
+ );
+ futures::future::pending::<()>().await;
+ unreachable!()
+ });
+ let format = editor.update(cx, |editor, cx| editor.perform_format(project, 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_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 <s|>
+ additional edit
+ "},
+ vec!["fourth_completion", "fifth_completion", "sixth_completion"],
+ )
+ .await;
+ cx.condition(|editor, _| editor.context_menu_visible())
+ .await;
+
+ cx.simulate_keystroke("i");
+
+ handle_completion_request(
+ &mut cx,
+ indoc! {"
+ one.second_completion
+ two si
+ three <si|>
+ additional edit
+ "},
+ vec!["fourth_completion", "fifth_completion", "sixth_completion"],
+ )
+ .await;
+ cx.condition(|editor, _| editor.context_menu_visible())
+ .await;
+
+ let apply_additional_edits = cx.update_editor(|editor, cx| {
+ editor
+ .confirm_completion(&ConfirmCompletion::default(), cx)
+ .unwrap()
+ });
+ cx.assert_editor_state(indoc! {"
+ one.second_completion
+ two sixth_completionˇ
+ three sixth_completionˇ
+ additional edit
+ "});
+
+ handle_resolve_completion_request(&mut cx, None).await;
+ apply_additional_edits.await.unwrap();
+
+ cx.update(|cx| {
+ cx.update_global::<Settings, _, _>(|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.<clo|>", vec!["close", "clobber"]).await;
+ cx.condition(|editor, _| editor.context_menu_visible())
+ .await;
+ let apply_additional_edits = cx.update_editor(|editor, cx| {
+ editor
+ .confirm_completion(&ConfirmCompletion::default(), cx)
+ .unwrap()
+ });
+ cx.assert_editor_state("editor.closeˇ");
+ handle_resolve_completion_request(&mut cx, None).await;
+ apply_additional_edits.await.unwrap();
+
+ // 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::<lsp::request::Completion, _, _>(move |url, params, _| {
+ let completions = completions.clone();
+ async move {
+ assert_eq!(params.text_document_position.text_document.uri, url.clone());
+ assert_eq!(
+ params.text_document_position.position,
+ complete_from_position
+ );
+ Ok(Some(lsp::CompletionResponse::Array(
+ completions
+ .iter()
+ .map(|completion_text| lsp::CompletionItem {
+ label: completion_text.to_string(),
+ text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
+ range: replace_range,
+ new_text: completion_text.to_string(),
+ })),
+ ..Default::default()
+ })
+ .collect(),
+ )))
+ }
+ })
+ .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::<lsp::request::ResolveCompletionItem, _, _>(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(), " -->".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#"
+ <p>A</p>ˇ
+ <p>B</p>ˇ
+ <p>C</p>ˇ
+ "#
+ .unindent(),
+ );
+ cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments, cx));
+ cx.assert_editor_state(
+ &r#"
+ <!-- <p>A</p>ˇ -->
+ <!-- <p>B</p>ˇ -->
+ <!-- <p>C</p>ˇ -->
+ "#
+ .unindent(),
+ );
+ cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments, cx));
+ cx.assert_editor_state(
+ &r#"
+ <p>A</p>ˇ
+ <p>B</p>ˇ
+ <p>C</p>ˇ
+ "#
+ .unindent(),
+ );
+
+ // Toggle comments for mixture of empty and non-empty selections, where
+ // multiple selections occupy a given line.
+ cx.set_state(
+ &r#"
+ <p>A«</p>
+ <p>ˇ»B</p>ˇ
+ <p>C«</p>
+ <p>ˇ»D</p>ˇ
+ "#
+ .unindent(),
+ );
+
+ cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments, cx));
+ cx.assert_editor_state(
+ &r#"
+ <!-- <p>A«</p>
+ <p>ˇ»B</p>ˇ -->
+ <!-- <p>C«</p>
+ <p>ˇ»D</p>ˇ -->
+ "#
+ .unindent(),
+ );
+ cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments, cx));
+ cx.assert_editor_state(
+ &r#"
+ <p>A«</p>
+ <p>ˇ»B</p>ˇ
+ <p>C«</p>
+ <p>ˇ»D</p>ˇ
+ "#
+ .unindent(),
+ );
+
+ // Toggle comments when different languages are active for different
+ // selections.
+ cx.set_state(
+ &r#"
+ ˇ<script>
+ ˇvar x = new Y();
+ ˇ</script>
+ "#
+ .unindent(),
+ );
+ cx.foreground().run_until_parked();
+ cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments, cx));
+ cx.assert_editor_state(
+ &r#"
+ <!-- ˇ<script> -->
+ // ˇvar x = new Y();
+ <!-- ˇ</script> -->
+ "#
+ .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", // Suppress rustfmt
+ " 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<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
+
+ editor.highlight_background::<Type1>(
+ vec![
+ anchor_range(Point::new(2, 1)..Point::new(2, 3)),
+ anchor_range(Point::new(4, 2)..Point::new(4, 4)),
+ anchor_range(Point::new(6, 3)..Point::new(6, 5)),
+ anchor_range(Point::new(8, 4)..Point::new(8, 6)),
+ ],
+ |_| Color::red(),
+ cx,
+ );
+ editor.highlight_background::<Type2>(
+ vec![
+ anchor_range(Point::new(3, 2)..Point::new(3, 5)),
+ anchor_range(Point::new(5, 3)..Point::new(5, 6)),
+ anchor_range(Point::new(7, 4)..Point::new(7, 7)),
+ anchor_range(Point::new(9, 5)..Point::new(9, 8)),
+ ],
+ |_| Color::green(),
+ cx,
+ );
+
+ let snapshot = editor.snapshot(cx);
+ let mut highlighted_ranges = editor.background_highlights_in_range(
+ anchor_range(Point::new(3, 4)..Point::new(7, 4)),
+ &snapshot,
+ cx.global::<Settings>().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::<Settings>().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<DisplayPoint> {
+ let point = DisplayPoint::new(row as u32, column as u32);
+ point..point
+}
+
+fn assert_selection_ranges(marked_text: &str, view: &mut Editor, cx: &mut ViewContext<Editor>) {
+ let (text, ranges) = marked_text_ranges(marked_text, true);
+ assert_eq!(view.text(cx), text);
+ assert_eq!(
+ view.selections.ranges(cx),
+ ranges,
+ "Assert selections are {}",
+ marked_text
+ );
+}