mirror of
https://github.com/zed-industries/zed.git
synced 2025-01-16 15:11:25 +00:00
d7473ad6e7
Nothing earth-shattering here, but all our geometry types are now fully documented. Release Notes: - N/A
8278 lines
245 KiB
Rust
8278 lines
245 KiB
Rust
use super::*;
|
||
use crate::{
|
||
scroll::scroll_amount::ScrollAmount,
|
||
test::{
|
||
assert_text_with_selections, build_editor, editor_lsp_test_context::EditorLspTestContext,
|
||
editor_test_context::EditorTestContext, select_ranges,
|
||
},
|
||
JoinLines,
|
||
};
|
||
|
||
use futures::StreamExt;
|
||
use gpui::{
|
||
div,
|
||
serde_json::{self, json},
|
||
Div, Flatten, TestAppContext, VisualTestContext, WindowBounds, WindowOptions,
|
||
};
|
||
use indoc::indoc;
|
||
use language::{
|
||
language_settings::{AllLanguageSettings, AllLanguageSettingsContent, LanguageSettingsContent},
|
||
BracketPairConfig, FakeLspAdapter, LanguageConfig, LanguageConfigOverride, LanguageRegistry,
|
||
Override, Point,
|
||
};
|
||
use parking_lot::Mutex;
|
||
use project::project_settings::{LspSettings, ProjectSettings};
|
||
use project::FakeFs;
|
||
use std::sync::atomic;
|
||
use std::sync::atomic::AtomicUsize;
|
||
use std::{cell::RefCell, future::Future, rc::Rc, time::Instant};
|
||
use unindent::Unindent;
|
||
use util::{
|
||
assert_set_eq,
|
||
test::{marked_text_ranges, marked_text_ranges_by, sample_text, TextRangeMarker},
|
||
};
|
||
use workspace::{
|
||
item::{FollowEvent, FollowableItem, Item, ItemHandle},
|
||
NavigationEntry, ViewId,
|
||
};
|
||
|
||
#[gpui::test]
|
||
fn test_edit_events(cx: &mut TestAppContext) {
|
||
init_test(cx, |_| {});
|
||
|
||
let buffer = cx.build_model(|cx| {
|
||
let mut buffer = language::Buffer::new(0, cx.entity_id().as_u64(), "123456");
|
||
buffer.set_group_interval(Duration::from_secs(1));
|
||
buffer
|
||
});
|
||
|
||
let events = Rc::new(RefCell::new(Vec::new()));
|
||
let editor1 = cx.add_window({
|
||
let events = events.clone();
|
||
|cx| {
|
||
let view = cx.view().clone();
|
||
cx.subscribe(&view, move |_, _, event: &EditorEvent, _| {
|
||
if matches!(event, EditorEvent::Edited | EditorEvent::BufferEdited) {
|
||
events.borrow_mut().push(("editor1", event.clone()));
|
||
}
|
||
})
|
||
.detach();
|
||
Editor::for_buffer(buffer.clone(), None, cx)
|
||
}
|
||
});
|
||
|
||
let editor2 = cx.add_window({
|
||
let events = events.clone();
|
||
|cx| {
|
||
cx.subscribe(&cx.view().clone(), move |_, _, event: &EditorEvent, _| {
|
||
if matches!(event, EditorEvent::Edited | EditorEvent::BufferEdited) {
|
||
events.borrow_mut().push(("editor2", event.clone()));
|
||
}
|
||
})
|
||
.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", EditorEvent::Edited),
|
||
("editor1", EditorEvent::BufferEdited),
|
||
("editor2", EditorEvent::BufferEdited),
|
||
]
|
||
);
|
||
|
||
// 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", EditorEvent::Edited),
|
||
("editor1", EditorEvent::BufferEdited),
|
||
("editor2", EditorEvent::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", EditorEvent::Edited),
|
||
("editor1", EditorEvent::BufferEdited),
|
||
("editor2", EditorEvent::BufferEdited),
|
||
]
|
||
);
|
||
|
||
// 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", EditorEvent::Edited),
|
||
("editor1", EditorEvent::BufferEdited),
|
||
("editor2", EditorEvent::BufferEdited),
|
||
]
|
||
);
|
||
|
||
// 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", EditorEvent::Edited),
|
||
("editor1", EditorEvent::BufferEdited),
|
||
("editor2", EditorEvent::BufferEdited),
|
||
]
|
||
);
|
||
|
||
// 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", EditorEvent::Edited),
|
||
("editor1", EditorEvent::BufferEdited),
|
||
("editor2", EditorEvent::BufferEdited),
|
||
]
|
||
);
|
||
|
||
// 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 TestAppContext) {
|
||
init_test(cx, |_| {});
|
||
|
||
let mut now = Instant::now();
|
||
let buffer = cx.build_model(|cx| language::Buffer::new(0, cx.entity_id().as_u64(), "123456"));
|
||
let group_interval = buffer.update(cx, |buffer, _| buffer.transaction_group_interval());
|
||
let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
|
||
let editor = cx.add_window(|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 TestAppContext) {
|
||
init_test(cx, |_| {});
|
||
|
||
let buffer = cx.build_model(|cx| {
|
||
let mut buffer = language::Buffer::new(0, cx.entity_id().as_u64(), "abcde");
|
||
// Ensure automatic grouping doesn't occur.
|
||
buffer.set_group_interval(Duration::ZERO);
|
||
buffer
|
||
});
|
||
|
||
let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
|
||
cx.add_window(|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 TestAppContext) {
|
||
init_test(cx, |_| {});
|
||
|
||
let editor = cx.add_window(|cx| {
|
||
let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", 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))
|
||
.unwrap(),
|
||
[DisplayPoint::new(2, 2)..DisplayPoint::new(2, 2)]
|
||
);
|
||
|
||
editor.update(cx, |view, cx| {
|
||
view.update_selection(
|
||
DisplayPoint::new(3, 3),
|
||
0,
|
||
gpui::Point::<f32>::default(),
|
||
cx,
|
||
);
|
||
});
|
||
|
||
assert_eq!(
|
||
editor
|
||
.update(cx, |view, cx| view.selections.display_ranges(cx))
|
||
.unwrap(),
|
||
[DisplayPoint::new(2, 2)..DisplayPoint::new(3, 3)]
|
||
);
|
||
|
||
editor.update(cx, |view, cx| {
|
||
view.update_selection(
|
||
DisplayPoint::new(1, 1),
|
||
0,
|
||
gpui::Point::<f32>::default(),
|
||
cx,
|
||
);
|
||
});
|
||
|
||
assert_eq!(
|
||
editor
|
||
.update(cx, |view, cx| view.selections.display_ranges(cx))
|
||
.unwrap(),
|
||
[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,
|
||
gpui::Point::<f32>::default(),
|
||
cx,
|
||
);
|
||
});
|
||
|
||
assert_eq!(
|
||
editor
|
||
.update(cx, |view, cx| view.selections.display_ranges(cx))
|
||
.unwrap(),
|
||
[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,
|
||
gpui::Point::<f32>::default(),
|
||
cx,
|
||
);
|
||
});
|
||
|
||
assert_eq!(
|
||
editor
|
||
.update(cx, |view, cx| view.selections.display_ranges(cx))
|
||
.unwrap(),
|
||
[
|
||
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))
|
||
.unwrap(),
|
||
[DisplayPoint::new(3, 3)..DisplayPoint::new(0, 0)]
|
||
);
|
||
}
|
||
|
||
#[gpui::test]
|
||
fn test_canceling_pending_selection(cx: &mut TestAppContext) {
|
||
init_test(cx, |_| {});
|
||
|
||
let view = cx.add_window(|cx| {
|
||
let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", 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,
|
||
gpui::Point::<f32>::default(),
|
||
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,
|
||
gpui::Point::<f32>::default(),
|
||
cx,
|
||
);
|
||
assert_eq!(
|
||
view.selections.display_ranges(cx),
|
||
[DisplayPoint::new(2, 2)..DisplayPoint::new(3, 3)]
|
||
);
|
||
});
|
||
}
|
||
|
||
#[gpui::test]
|
||
fn test_clone(cx: &mut TestAppContext) {
|
||
init_test(cx, |_| {});
|
||
|
||
let (text, selection_ranges) = marked_text_ranges(
|
||
indoc! {"
|
||
one
|
||
two
|
||
threeˇ
|
||
four
|
||
fiveˇ
|
||
"},
|
||
true,
|
||
);
|
||
|
||
let editor = cx.add_window(|cx| {
|
||
let buffer = MultiBuffer::build_simple(&text, 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),
|
||
],
|
||
true,
|
||
cx,
|
||
);
|
||
});
|
||
|
||
let cloned_editor = editor
|
||
.update(cx, |editor, cx| {
|
||
cx.open_window(Default::default(), |cx| {
|
||
cx.build_view(|cx| editor.clone(cx))
|
||
})
|
||
})
|
||
.unwrap();
|
||
|
||
let snapshot = editor.update(cx, |e, cx| e.snapshot(cx)).unwrap();
|
||
let cloned_snapshot = cloned_editor.update(cx, |e, cx| e.snapshot(cx)).unwrap();
|
||
|
||
assert_eq!(
|
||
cloned_editor
|
||
.update(cx, |e, cx| e.display_text(cx))
|
||
.unwrap(),
|
||
editor.update(cx, |e, cx| e.display_text(cx)).unwrap()
|
||
);
|
||
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
|
||
.update(cx, |editor, cx| editor.selections.ranges::<Point>(cx))
|
||
.unwrap(),
|
||
editor
|
||
.update(cx, |editor, cx| editor.selections.ranges(cx))
|
||
.unwrap()
|
||
);
|
||
assert_set_eq!(
|
||
cloned_editor
|
||
.update(cx, |e, cx| e.selections.display_ranges(cx))
|
||
.unwrap(),
|
||
editor
|
||
.update(cx, |e, cx| e.selections.display_ranges(cx))
|
||
.unwrap()
|
||
);
|
||
}
|
||
|
||
//todo!(editor navigate)
|
||
#[gpui::test]
|
||
async fn test_navigation_history(cx: &mut TestAppContext) {
|
||
init_test(cx, |_| {});
|
||
|
||
use workspace::item::Item;
|
||
|
||
let fs = FakeFs::new(cx.executor());
|
||
let project = Project::test(fs, [], cx).await;
|
||
let workspace = cx.add_window(|cx| Workspace::test_new(project, cx));
|
||
let pane = workspace
|
||
.update(cx, |workspace, _| workspace.active_pane().clone())
|
||
.unwrap();
|
||
|
||
workspace.update(cx, |v, cx| {
|
||
cx.build_view(|cx| {
|
||
let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx);
|
||
let mut editor = build_editor(buffer.clone(), cx);
|
||
let handle = cx.view();
|
||
editor.set_nav_history(Some(pane.read(cx).nav_history_for_item(&handle)));
|
||
|
||
fn pop_history(editor: &mut Editor, cx: &mut WindowContext) -> 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.entity_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.entity_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(gpui::Point::<f32>::new(5.5, 5.5), cx);
|
||
let original_scroll_position = editor.scroll_manager.anchor();
|
||
|
||
// Jump to the end of the document and adjust scroll
|
||
editor.move_to_end(&MoveToEnd, cx);
|
||
editor.set_scroll_position(gpui::Point::<f32>::new(-2.5, -0.5), cx);
|
||
assert_ne!(editor.scroll_manager.anchor(), original_scroll_position);
|
||
|
||
let nav_entry = pop_history(&mut editor, cx).unwrap();
|
||
editor.navigate(nav_entry.data.unwrap(), cx);
|
||
assert_eq!(editor.scroll_manager.anchor(), original_scroll_position);
|
||
|
||
// Ensure we don't panic when navigation data contains invalid anchors *and* points.
|
||
let mut invalid_anchor = editor.scroll_manager.anchor().anchor;
|
||
invalid_anchor.text_anchor.buffer_id = Some(999);
|
||
let invalid_point = Point::new(9999, 0);
|
||
editor.navigate(
|
||
Box::new(NavigationData {
|
||
cursor_anchor: invalid_anchor,
|
||
cursor_position: invalid_point,
|
||
scroll_anchor: ScrollAnchor {
|
||
anchor: invalid_anchor,
|
||
offset: Default::default(),
|
||
},
|
||
scroll_top_row: invalid_point.row,
|
||
}),
|
||
cx,
|
||
);
|
||
assert_eq!(
|
||
editor.selections.display_ranges(cx),
|
||
&[editor.max_point(cx)..editor.max_point(cx)]
|
||
);
|
||
assert_eq!(
|
||
editor.scroll_position(cx),
|
||
gpui::Point::new(0., editor.max_point(cx).row() as f32)
|
||
);
|
||
|
||
editor
|
||
})
|
||
});
|
||
}
|
||
|
||
#[gpui::test]
|
||
fn test_cancel(cx: &mut TestAppContext) {
|
||
init_test(cx, |_| {});
|
||
|
||
let view = cx.add_window(|cx| {
|
||
let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", 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,
|
||
gpui::Point::<f32>::default(),
|
||
cx,
|
||
);
|
||
view.end_selection(cx);
|
||
|
||
view.begin_selection(DisplayPoint::new(0, 1), true, 1, cx);
|
||
view.update_selection(
|
||
DisplayPoint::new(0, 3),
|
||
0,
|
||
gpui::Point::<f32>::default(),
|
||
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_action(cx: &mut TestAppContext) {
|
||
init_test(cx, |_| {});
|
||
|
||
let view = cx.add_window(|cx| {
|
||
let buffer = MultiBuffer::build_simple(
|
||
&"
|
||
impl Foo {
|
||
// Hello!
|
||
|
||
fn a() {
|
||
1
|
||
}
|
||
|
||
fn b() {
|
||
2
|
||
}
|
||
|
||
fn c() {
|
||
3
|
||
}
|
||
}
|
||
"
|
||
.unindent(),
|
||
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), view.buffer.read(cx).read(cx).text());
|
||
});
|
||
}
|
||
|
||
#[gpui::test]
|
||
fn test_move_cursor(cx: &mut TestAppContext) {
|
||
init_test(cx, |_| {});
|
||
|
||
let buffer = cx.update(|cx| MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx));
|
||
let view = cx.add_window(|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 TestAppContext) {
|
||
init_test(cx, |_| {});
|
||
|
||
let view = cx.add_window(|cx| {
|
||
let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcde\nαβγδε", 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),
|
||
],
|
||
true,
|
||
cx,
|
||
);
|
||
assert_eq!(view.display_text(cx), "ⓐⓑ⋯ⓔ\nab⋯e\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⋯e".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, "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_down(&MoveDown, 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())]
|
||
);
|
||
});
|
||
}
|
||
|
||
//todo!(finish editor tests)
|
||
#[gpui::test]
|
||
fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) {
|
||
init_test(cx, |_| {});
|
||
|
||
let view = cx.add_window(|cx| {
|
||
let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", 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 TestAppContext) {
|
||
init_test(cx, |_| {});
|
||
|
||
let view = cx.add_window(|cx| {
|
||
let buffer = MultiBuffer::build_simple("abc\n def", 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 TestAppContext) {
|
||
init_test(cx, |_| {});
|
||
|
||
let view = cx.add_window(|cx| {
|
||
let buffer = MultiBuffer::build_simple("use std::str::{foo, bar}\n\n {baz.qux()}", 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);
|
||
});
|
||
}
|
||
|
||
//todo!(finish editor tests)
|
||
#[gpui::test]
|
||
fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) {
|
||
init_test(cx, |_| {});
|
||
|
||
let view = cx.add_window(|cx| {
|
||
let buffer = MultiBuffer::build_simple("use one::{\n two::three::four::five\n};", cx);
|
||
build_editor(buffer, cx)
|
||
});
|
||
|
||
view.update(cx, |view, cx| {
|
||
view.set_wrap_width(Some(140.0.into()), 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)]
|
||
);
|
||
});
|
||
}
|
||
|
||
//todo!(simulate_resize)
|
||
#[gpui::test]
|
||
async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut gpui::TestAppContext) {
|
||
init_test(cx, |_| {});
|
||
let mut cx = EditorTestContext::new(cx).await;
|
||
|
||
let line_height = cx.editor(|editor, cx| {
|
||
editor
|
||
.style()
|
||
.unwrap()
|
||
.text
|
||
.line_height_in_pixels(cx.rem_size())
|
||
});
|
||
cx.simulate_window_resize(cx.window, size(px(100.), 4. * line_height));
|
||
|
||
cx.set_state(
|
||
&r#"ˇone
|
||
two
|
||
|
||
three
|
||
fourˇ
|
||
five
|
||
|
||
six"#
|
||
.unindent(),
|
||
);
|
||
|
||
cx.update_editor(|editor, cx| editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, cx));
|
||
cx.assert_editor_state(
|
||
&r#"one
|
||
two
|
||
ˇ
|
||
three
|
||
four
|
||
five
|
||
ˇ
|
||
six"#
|
||
.unindent(),
|
||
);
|
||
|
||
cx.update_editor(|editor, cx| editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, cx));
|
||
cx.assert_editor_state(
|
||
&r#"one
|
||
two
|
||
|
||
three
|
||
four
|
||
five
|
||
ˇ
|
||
sixˇ"#
|
||
.unindent(),
|
||
);
|
||
|
||
cx.update_editor(|editor, cx| editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, cx));
|
||
cx.assert_editor_state(
|
||
&r#"one
|
||
two
|
||
|
||
three
|
||
four
|
||
five
|
||
|
||
sixˇ"#
|
||
.unindent(),
|
||
);
|
||
|
||
cx.update_editor(|editor, cx| editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, cx));
|
||
cx.assert_editor_state(
|
||
&r#"one
|
||
two
|
||
|
||
three
|
||
four
|
||
five
|
||
ˇ
|
||
six"#
|
||
.unindent(),
|
||
);
|
||
|
||
cx.update_editor(|editor, cx| editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, cx));
|
||
cx.assert_editor_state(
|
||
&r#"one
|
||
two
|
||
ˇ
|
||
three
|
||
four
|
||
five
|
||
|
||
six"#
|
||
.unindent(),
|
||
);
|
||
|
||
cx.update_editor(|editor, cx| editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, cx));
|
||
cx.assert_editor_state(
|
||
&r#"ˇone
|
||
two
|
||
|
||
three
|
||
four
|
||
five
|
||
|
||
six"#
|
||
.unindent(),
|
||
);
|
||
}
|
||
|
||
#[gpui::test]
|
||
async fn test_scroll_page_up_page_down(cx: &mut gpui::TestAppContext) {
|
||
init_test(cx, |_| {});
|
||
let mut cx = EditorTestContext::new(cx).await;
|
||
let line_height = cx.editor(|editor, cx| {
|
||
editor
|
||
.style()
|
||
.unwrap()
|
||
.text
|
||
.line_height_in_pixels(cx.rem_size())
|
||
});
|
||
let window = cx.window;
|
||
cx.simulate_window_resize(window, size(px(1000.), 4. * line_height + px(0.5)));
|
||
|
||
cx.set_state(
|
||
&r#"ˇone
|
||
two
|
||
three
|
||
four
|
||
five
|
||
six
|
||
seven
|
||
eight
|
||
nine
|
||
ten
|
||
"#,
|
||
);
|
||
|
||
cx.update_editor(|editor, cx| {
|
||
assert_eq!(
|
||
editor.snapshot(cx).scroll_position(),
|
||
gpui::Point::new(0., 0.)
|
||
);
|
||
editor.scroll_screen(&ScrollAmount::Page(1.), cx);
|
||
assert_eq!(
|
||
editor.snapshot(cx).scroll_position(),
|
||
gpui::Point::new(0., 3.)
|
||
);
|
||
editor.scroll_screen(&ScrollAmount::Page(1.), cx);
|
||
assert_eq!(
|
||
editor.snapshot(cx).scroll_position(),
|
||
gpui::Point::new(0., 6.)
|
||
);
|
||
editor.scroll_screen(&ScrollAmount::Page(-1.), cx);
|
||
assert_eq!(
|
||
editor.snapshot(cx).scroll_position(),
|
||
gpui::Point::new(0., 3.)
|
||
);
|
||
|
||
editor.scroll_screen(&ScrollAmount::Page(-0.5), cx);
|
||
assert_eq!(
|
||
editor.snapshot(cx).scroll_position(),
|
||
gpui::Point::new(0., 1.)
|
||
);
|
||
editor.scroll_screen(&ScrollAmount::Page(0.5), cx);
|
||
assert_eq!(
|
||
editor.snapshot(cx).scroll_position(),
|
||
gpui::Point::new(0., 3.)
|
||
);
|
||
});
|
||
}
|
||
|
||
#[gpui::test]
|
||
async fn test_autoscroll(cx: &mut gpui::TestAppContext) {
|
||
init_test(cx, |_| {});
|
||
let mut cx = EditorTestContext::new(cx).await;
|
||
|
||
let line_height = cx.update_editor(|editor, cx| {
|
||
editor.set_vertical_scroll_margin(2, cx);
|
||
editor
|
||
.style()
|
||
.unwrap()
|
||
.text
|
||
.line_height_in_pixels(cx.rem_size())
|
||
});
|
||
let window = cx.window;
|
||
cx.simulate_window_resize(window, size(px(1000.), 6. * line_height));
|
||
|
||
cx.set_state(
|
||
&r#"ˇone
|
||
two
|
||
three
|
||
four
|
||
five
|
||
six
|
||
seven
|
||
eight
|
||
nine
|
||
ten
|
||
"#,
|
||
);
|
||
cx.update_editor(|editor, cx| {
|
||
assert_eq!(
|
||
editor.snapshot(cx).scroll_position(),
|
||
gpui::Point::new(0., 0.0)
|
||
);
|
||
});
|
||
|
||
// Add a cursor below the visible area. Since both cursors cannot fit
|
||
// on screen, the editor autoscrolls to reveal the newest cursor, and
|
||
// allows the vertical scroll margin below that cursor.
|
||
cx.update_editor(|editor, cx| {
|
||
editor.change_selections(Some(Autoscroll::fit()), cx, |selections| {
|
||
selections.select_ranges([
|
||
Point::new(0, 0)..Point::new(0, 0),
|
||
Point::new(6, 0)..Point::new(6, 0),
|
||
]);
|
||
})
|
||
});
|
||
cx.update_editor(|editor, cx| {
|
||
assert_eq!(
|
||
editor.snapshot(cx).scroll_position(),
|
||
gpui::Point::new(0., 3.0)
|
||
);
|
||
});
|
||
|
||
// Move down. The editor cursor scrolls down to track the newest cursor.
|
||
cx.update_editor(|editor, cx| {
|
||
editor.move_down(&Default::default(), cx);
|
||
});
|
||
cx.update_editor(|editor, cx| {
|
||
assert_eq!(
|
||
editor.snapshot(cx).scroll_position(),
|
||
gpui::Point::new(0., 4.0)
|
||
);
|
||
});
|
||
|
||
// Add a cursor above the visible area. Since both cursors fit on screen,
|
||
// the editor scrolls to show both.
|
||
cx.update_editor(|editor, cx| {
|
||
editor.change_selections(Some(Autoscroll::fit()), cx, |selections| {
|
||
selections.select_ranges([
|
||
Point::new(1, 0)..Point::new(1, 0),
|
||
Point::new(6, 0)..Point::new(6, 0),
|
||
]);
|
||
})
|
||
});
|
||
cx.update_editor(|editor, cx| {
|
||
assert_eq!(
|
||
editor.snapshot(cx).scroll_position(),
|
||
gpui::Point::new(0., 1.0)
|
||
);
|
||
});
|
||
}
|
||
|
||
#[gpui::test]
|
||
async fn test_move_page_up_page_down(cx: &mut gpui::TestAppContext) {
|
||
init_test(cx, |_| {});
|
||
let mut cx = EditorTestContext::new(cx).await;
|
||
|
||
let line_height = cx.editor(|editor, cx| {
|
||
editor
|
||
.style()
|
||
.unwrap()
|
||
.text
|
||
.line_height_in_pixels(cx.rem_size())
|
||
});
|
||
let window = cx.window;
|
||
cx.simulate_window_resize(window, size(px(100.), 4. * line_height));
|
||
cx.set_state(
|
||
&r#"
|
||
ˇone
|
||
two
|
||
threeˇ
|
||
four
|
||
five
|
||
six
|
||
seven
|
||
eight
|
||
nine
|
||
ten
|
||
"#
|
||
.unindent(),
|
||
);
|
||
|
||
cx.update_editor(|editor, cx| editor.move_page_down(&MovePageDown::default(), cx));
|
||
cx.assert_editor_state(
|
||
&r#"
|
||
one
|
||
two
|
||
three
|
||
ˇfour
|
||
five
|
||
sixˇ
|
||
seven
|
||
eight
|
||
nine
|
||
ten
|
||
"#
|
||
.unindent(),
|
||
);
|
||
|
||
cx.update_editor(|editor, cx| editor.move_page_down(&MovePageDown::default(), cx));
|
||
cx.assert_editor_state(
|
||
&r#"
|
||
one
|
||
two
|
||
three
|
||
four
|
||
five
|
||
six
|
||
ˇseven
|
||
eight
|
||
nineˇ
|
||
ten
|
||
"#
|
||
.unindent(),
|
||
);
|
||
|
||
cx.update_editor(|editor, cx| editor.move_page_up(&MovePageUp::default(), cx));
|
||
cx.assert_editor_state(
|
||
&r#"
|
||
one
|
||
two
|
||
three
|
||
ˇfour
|
||
five
|
||
sixˇ
|
||
seven
|
||
eight
|
||
nine
|
||
ten
|
||
"#
|
||
.unindent(),
|
||
);
|
||
|
||
cx.update_editor(|editor, cx| editor.move_page_up(&MovePageUp::default(), cx));
|
||
cx.assert_editor_state(
|
||
&r#"
|
||
ˇone
|
||
two
|
||
threeˇ
|
||
four
|
||
five
|
||
six
|
||
seven
|
||
eight
|
||
nine
|
||
ten
|
||
"#
|
||
.unindent(),
|
||
);
|
||
|
||
// Test select collapsing
|
||
cx.update_editor(|editor, cx| {
|
||
editor.move_page_down(&MovePageDown::default(), cx);
|
||
editor.move_page_down(&MovePageDown::default(), cx);
|
||
editor.move_page_down(&MovePageDown::default(), cx);
|
||
});
|
||
cx.assert_editor_state(
|
||
&r#"
|
||
one
|
||
two
|
||
three
|
||
four
|
||
five
|
||
six
|
||
seven
|
||
eight
|
||
nine
|
||
ˇten
|
||
ˇ"#
|
||
.unindent(),
|
||
);
|
||
}
|
||
|
||
#[gpui::test]
|
||
async fn test_delete_to_beginning_of_line(cx: &mut gpui::TestAppContext) {
|
||
init_test(cx, |_| {});
|
||
let mut cx = EditorTestContext::new(cx).await;
|
||
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 TestAppContext) {
|
||
init_test(cx, |_| {});
|
||
|
||
let view = cx.add_window(|cx| {
|
||
let buffer = MultiBuffer::build_simple("one two three four", 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!(view.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!(view.buffer.read(cx).read(cx).text(), "e t te our");
|
||
});
|
||
}
|
||
|
||
#[gpui::test]
|
||
fn test_newline(cx: &mut TestAppContext) {
|
||
init_test(cx, |_| {});
|
||
|
||
let view = cx.add_window(|cx| {
|
||
let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", 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 TestAppContext) {
|
||
init_test(cx, |_| {});
|
||
|
||
let editor = cx.add_window(|cx| {
|
||
let buffer = MultiBuffer::build_simple(
|
||
"
|
||
a
|
||
b(
|
||
X
|
||
)
|
||
c(
|
||
X
|
||
)
|
||
"
|
||
.unindent()
|
||
.as_str(),
|
||
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
|
||
});
|
||
|
||
editor.update(cx, |editor, cx| {
|
||
// Edit the buffer directly, deleting ranges surrounding the editor's selections
|
||
editor.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()
|
||
);
|
||
});
|
||
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_above(cx: &mut gpui::TestAppContext) {
|
||
init_test(cx, |settings| {
|
||
settings.defaults.tab_size = NonZeroU32::new(4)
|
||
});
|
||
|
||
let language = Arc::new(
|
||
Language::new(
|
||
LanguageConfig::default(),
|
||
Some(tree_sitter_rust::language()),
|
||
)
|
||
.with_indents_query(r#"(_ "(" ")" @end) @indent"#)
|
||
.unwrap(),
|
||
);
|
||
|
||
let mut cx = EditorTestContext::new(cx).await;
|
||
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_above(&NewlineAbove, cx));
|
||
cx.assert_editor_state(indoc! {"
|
||
ˇ
|
||
const a: A = (
|
||
ˇ
|
||
(
|
||
ˇ
|
||
ˇ
|
||
const_function(),
|
||
ˇ
|
||
ˇ
|
||
ˇ
|
||
ˇ
|
||
something_else,
|
||
ˇ
|
||
)
|
||
ˇ
|
||
ˇ
|
||
);
|
||
"});
|
||
}
|
||
|
||
#[gpui::test]
|
||
async fn test_newline_below(cx: &mut gpui::TestAppContext) {
|
||
init_test(cx, |settings| {
|
||
settings.defaults.tab_size = NonZeroU32::new(4)
|
||
});
|
||
|
||
let language = Arc::new(
|
||
Language::new(
|
||
LanguageConfig::default(),
|
||
Some(tree_sitter_rust::language()),
|
||
)
|
||
.with_indents_query(r#"(_ "(" ")" @end) @indent"#)
|
||
.unwrap(),
|
||
);
|
||
|
||
let mut cx = EditorTestContext::new(cx).await;
|
||
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]
|
||
async fn test_newline_comments(cx: &mut gpui::TestAppContext) {
|
||
init_test(cx, |settings| {
|
||
settings.defaults.tab_size = NonZeroU32::new(4)
|
||
});
|
||
|
||
let language = Arc::new(Language::new(
|
||
LanguageConfig {
|
||
line_comment: Some("//".into()),
|
||
..LanguageConfig::default()
|
||
},
|
||
None,
|
||
));
|
||
{
|
||
let mut cx = EditorTestContext::new(cx).await;
|
||
cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
|
||
cx.set_state(indoc! {"
|
||
// Fooˇ
|
||
"});
|
||
|
||
cx.update_editor(|e, cx| e.newline(&Newline, cx));
|
||
cx.assert_editor_state(indoc! {"
|
||
// Foo
|
||
//ˇ
|
||
"});
|
||
// Ensure that if cursor is before the comment start, we do not actually insert a comment prefix.
|
||
cx.set_state(indoc! {"
|
||
ˇ// Foo
|
||
"});
|
||
cx.update_editor(|e, cx| e.newline(&Newline, cx));
|
||
cx.assert_editor_state(indoc! {"
|
||
|
||
ˇ// Foo
|
||
"});
|
||
}
|
||
// Ensure that comment continuations can be disabled.
|
||
update_test_language_settings(cx, |settings| {
|
||
settings.defaults.extend_comment_on_newline = Some(false);
|
||
});
|
||
let mut cx = EditorTestContext::new(cx).await;
|
||
cx.set_state(indoc! {"
|
||
// Fooˇ
|
||
"});
|
||
cx.update_editor(|e, cx| e.newline(&Newline, cx));
|
||
cx.assert_editor_state(indoc! {"
|
||
// Foo
|
||
ˇ
|
||
"});
|
||
}
|
||
|
||
#[gpui::test]
|
||
fn test_insert_with_old_selections(cx: &mut TestAppContext) {
|
||
init_test(cx, |_| {});
|
||
|
||
let editor = cx.add_window(|cx| {
|
||
let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", 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
|
||
});
|
||
|
||
editor.update(cx, |editor, cx| {
|
||
// Edit the buffer directly, deleting ranges surrounding the editor's selections
|
||
editor.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());
|
||
});
|
||
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) {
|
||
init_test(cx, |settings| {
|
||
settings.defaults.tab_size = NonZeroU32::new(3)
|
||
});
|
||
|
||
let mut cx = EditorTestContext::new(cx).await;
|
||
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_in_leading_whitespace_auto_indents_lines(cx: &mut gpui::TestAppContext) {
|
||
init_test(cx, |_| {});
|
||
|
||
let mut cx = EditorTestContext::new(cx).await;
|
||
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_tab_with_mixed_whitespace(cx: &mut gpui::TestAppContext) {
|
||
init_test(cx, |settings| {
|
||
settings.defaults.tab_size = NonZeroU32::new(4)
|
||
});
|
||
|
||
let language = Arc::new(
|
||
Language::new(
|
||
LanguageConfig::default(),
|
||
Some(tree_sitter_rust::language()),
|
||
)
|
||
.with_indents_query(r#"(_ "{" "}" @end) @indent"#)
|
||
.unwrap(),
|
||
);
|
||
|
||
let mut cx = EditorTestContext::new(cx).await;
|
||
cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
|
||
cx.set_state(indoc! {"
|
||
fn a() {
|
||
if b {
|
||
\t ˇc
|
||
}
|
||
}
|
||
"});
|
||
|
||
cx.update_editor(|e, cx| e.tab(&Tab, cx));
|
||
cx.assert_editor_state(indoc! {"
|
||
fn a() {
|
||
if b {
|
||
ˇc
|
||
}
|
||
}
|
||
"});
|
||
}
|
||
|
||
#[gpui::test]
|
||
async fn test_indent_outdent(cx: &mut gpui::TestAppContext) {
|
||
init_test(cx, |settings| {
|
||
settings.defaults.tab_size = NonZeroU32::new(4);
|
||
});
|
||
|
||
let mut cx = EditorTestContext::new(cx).await;
|
||
|
||
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) {
|
||
init_test(cx, |settings| {
|
||
settings.defaults.hard_tabs = Some(true);
|
||
});
|
||
|
||
let mut cx = EditorTestContext::new(cx).await;
|
||
|
||
// 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 TestAppContext) {
|
||
init_test(cx, |settings| {
|
||
settings.languages.extend([
|
||
(
|
||
"TOML".into(),
|
||
LanguageSettingsContent {
|
||
tab_size: NonZeroU32::new(2),
|
||
..Default::default()
|
||
},
|
||
),
|
||
(
|
||
"Rust".into(),
|
||
LanguageSettingsContent {
|
||
tab_size: NonZeroU32::new(4),
|
||
..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.build_model(|cx| {
|
||
Buffer::new(0, cx.entity_id().as_u64(), "a = 1\nb = 2\n").with_language(toml_language, cx)
|
||
});
|
||
let rust_buffer = cx.build_model(|cx| {
|
||
Buffer::new(0, cx.entity_id().as_u64(), "const c: usize = 3;\n")
|
||
.with_language(rust_language, cx)
|
||
});
|
||
let multibuffer = cx.build_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(|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) {
|
||
init_test(cx, |_| {});
|
||
|
||
let mut cx = EditorTestContext::new(cx).await;
|
||
|
||
// 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) {
|
||
init_test(cx, |_| {});
|
||
|
||
let mut cx = EditorTestContext::new(cx).await;
|
||
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 TestAppContext) {
|
||
init_test(cx, |_| {});
|
||
|
||
let view = cx.add_window(|cx| {
|
||
let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", 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)
|
||
]
|
||
);
|
||
});
|
||
|
||
let view = cx.add_window(|cx| {
|
||
let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", 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)]
|
||
);
|
||
});
|
||
}
|
||
|
||
//todo!(select_anchor_ranges)
|
||
#[gpui::test]
|
||
fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
|
||
init_test(cx, |_| {});
|
||
|
||
cx.add_window(|cx| {
|
||
let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
|
||
let mut editor = build_editor(buffer.clone(), cx);
|
||
let buffer = buffer.read(cx).as_singleton().unwrap();
|
||
|
||
assert_eq!(
|
||
editor.selections.ranges::<Point>(cx),
|
||
&[Point::new(0, 0)..Point::new(0, 0)]
|
||
);
|
||
|
||
// When on single line, replace newline at end by space
|
||
editor.join_lines(&JoinLines, cx);
|
||
assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
|
||
assert_eq!(
|
||
editor.selections.ranges::<Point>(cx),
|
||
&[Point::new(0, 3)..Point::new(0, 3)]
|
||
);
|
||
|
||
// When multiple lines are selected, remove newlines that are spanned by the selection
|
||
editor.change_selections(None, cx, |s| {
|
||
s.select_ranges([Point::new(0, 5)..Point::new(2, 2)])
|
||
});
|
||
editor.join_lines(&JoinLines, cx);
|
||
assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n");
|
||
assert_eq!(
|
||
editor.selections.ranges::<Point>(cx),
|
||
&[Point::new(0, 11)..Point::new(0, 11)]
|
||
);
|
||
|
||
// Undo should be transactional
|
||
editor.undo(&Undo, cx);
|
||
assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
|
||
assert_eq!(
|
||
editor.selections.ranges::<Point>(cx),
|
||
&[Point::new(0, 5)..Point::new(2, 2)]
|
||
);
|
||
|
||
// When joining an empty line don't insert a space
|
||
editor.change_selections(None, cx, |s| {
|
||
s.select_ranges([Point::new(2, 1)..Point::new(2, 2)])
|
||
});
|
||
editor.join_lines(&JoinLines, cx);
|
||
assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n");
|
||
assert_eq!(
|
||
editor.selections.ranges::<Point>(cx),
|
||
[Point::new(2, 3)..Point::new(2, 3)]
|
||
);
|
||
|
||
// We can remove trailing newlines
|
||
editor.join_lines(&JoinLines, cx);
|
||
assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
|
||
assert_eq!(
|
||
editor.selections.ranges::<Point>(cx),
|
||
[Point::new(2, 3)..Point::new(2, 3)]
|
||
);
|
||
|
||
// We don't blow up on the last line
|
||
editor.join_lines(&JoinLines, cx);
|
||
assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
|
||
assert_eq!(
|
||
editor.selections.ranges::<Point>(cx),
|
||
[Point::new(2, 3)..Point::new(2, 3)]
|
||
);
|
||
|
||
// reset to test indentation
|
||
editor.buffer.update(cx, |buffer, cx| {
|
||
buffer.edit(
|
||
[
|
||
(Point::new(1, 0)..Point::new(1, 2), " "),
|
||
(Point::new(2, 0)..Point::new(2, 3), " \n\td"),
|
||
],
|
||
None,
|
||
cx,
|
||
)
|
||
});
|
||
|
||
// We remove any leading spaces
|
||
assert_eq!(buffer.read(cx).text(), "aaa bbb\n c\n \n\td");
|
||
editor.change_selections(None, cx, |s| {
|
||
s.select_ranges([Point::new(0, 1)..Point::new(0, 1)])
|
||
});
|
||
editor.join_lines(&JoinLines, cx);
|
||
assert_eq!(buffer.read(cx).text(), "aaa bbb c\n \n\td");
|
||
|
||
// We don't insert a space for a line containing only spaces
|
||
editor.join_lines(&JoinLines, cx);
|
||
assert_eq!(buffer.read(cx).text(), "aaa bbb c\n\td");
|
||
|
||
// We ignore any leading tabs
|
||
editor.join_lines(&JoinLines, cx);
|
||
assert_eq!(buffer.read(cx).text(), "aaa bbb c d");
|
||
|
||
editor
|
||
});
|
||
}
|
||
|
||
#[gpui::test]
|
||
fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) {
|
||
init_test(cx, |_| {});
|
||
|
||
cx.add_window(|cx| {
|
||
let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
|
||
let mut editor = build_editor(buffer.clone(), cx);
|
||
let buffer = buffer.read(cx).as_singleton().unwrap();
|
||
|
||
editor.change_selections(None, cx, |s| {
|
||
s.select_ranges([
|
||
Point::new(0, 2)..Point::new(1, 1),
|
||
Point::new(1, 2)..Point::new(1, 2),
|
||
Point::new(3, 1)..Point::new(3, 2),
|
||
])
|
||
});
|
||
|
||
editor.join_lines(&JoinLines, cx);
|
||
assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n");
|
||
|
||
assert_eq!(
|
||
editor.selections.ranges::<Point>(cx),
|
||
[
|
||
Point::new(0, 7)..Point::new(0, 7),
|
||
Point::new(1, 3)..Point::new(1, 3)
|
||
]
|
||
);
|
||
editor
|
||
});
|
||
}
|
||
|
||
#[gpui::test]
|
||
async fn test_manipulate_lines_with_single_selection(cx: &mut TestAppContext) {
|
||
init_test(cx, |_| {});
|
||
|
||
let mut cx = EditorTestContext::new(cx).await;
|
||
|
||
// Test sort_lines_case_insensitive()
|
||
cx.set_state(indoc! {"
|
||
«z
|
||
y
|
||
x
|
||
Z
|
||
Y
|
||
Xˇ»
|
||
"});
|
||
cx.update_editor(|e, cx| e.sort_lines_case_insensitive(&SortLinesCaseInsensitive, cx));
|
||
cx.assert_editor_state(indoc! {"
|
||
«x
|
||
X
|
||
y
|
||
Y
|
||
z
|
||
Zˇ»
|
||
"});
|
||
|
||
// Test reverse_lines()
|
||
cx.set_state(indoc! {"
|
||
«5
|
||
4
|
||
3
|
||
2
|
||
1ˇ»
|
||
"});
|
||
cx.update_editor(|e, cx| e.reverse_lines(&ReverseLines, cx));
|
||
cx.assert_editor_state(indoc! {"
|
||
«1
|
||
2
|
||
3
|
||
4
|
||
5ˇ»
|
||
"});
|
||
|
||
// Skip testing shuffle_line()
|
||
|
||
// From here on out, test more complex cases of manipulate_lines() with a single driver method: sort_lines_case_sensitive()
|
||
// Since all methods calling manipulate_lines() are doing the exact same general thing (reordering lines)
|
||
|
||
// Don't manipulate when cursor is on single line, but expand the selection
|
||
cx.set_state(indoc! {"
|
||
ddˇdd
|
||
ccc
|
||
bb
|
||
a
|
||
"});
|
||
cx.update_editor(|e, cx| e.sort_lines_case_sensitive(&SortLinesCaseSensitive, cx));
|
||
cx.assert_editor_state(indoc! {"
|
||
«ddddˇ»
|
||
ccc
|
||
bb
|
||
a
|
||
"});
|
||
|
||
// Basic manipulate case
|
||
// Start selection moves to column 0
|
||
// End of selection shrinks to fit shorter line
|
||
cx.set_state(indoc! {"
|
||
dd«d
|
||
ccc
|
||
bb
|
||
aaaaaˇ»
|
||
"});
|
||
cx.update_editor(|e, cx| e.sort_lines_case_sensitive(&SortLinesCaseSensitive, cx));
|
||
cx.assert_editor_state(indoc! {"
|
||
«aaaaa
|
||
bb
|
||
ccc
|
||
dddˇ»
|
||
"});
|
||
|
||
// Manipulate case with newlines
|
||
cx.set_state(indoc! {"
|
||
dd«d
|
||
ccc
|
||
|
||
bb
|
||
aaaaa
|
||
|
||
ˇ»
|
||
"});
|
||
cx.update_editor(|e, cx| e.sort_lines_case_sensitive(&SortLinesCaseSensitive, cx));
|
||
cx.assert_editor_state(indoc! {"
|
||
«
|
||
|
||
aaaaa
|
||
bb
|
||
ccc
|
||
dddˇ»
|
||
|
||
"});
|
||
}
|
||
|
||
#[gpui::test]
|
||
async fn test_manipulate_lines_with_multi_selection(cx: &mut TestAppContext) {
|
||
init_test(cx, |_| {});
|
||
|
||
let mut cx = EditorTestContext::new(cx).await;
|
||
|
||
// Manipulate with multiple selections on a single line
|
||
cx.set_state(indoc! {"
|
||
dd«dd
|
||
cˇ»c«c
|
||
bb
|
||
aaaˇ»aa
|
||
"});
|
||
cx.update_editor(|e, cx| e.sort_lines_case_sensitive(&SortLinesCaseSensitive, cx));
|
||
cx.assert_editor_state(indoc! {"
|
||
«aaaaa
|
||
bb
|
||
ccc
|
||
ddddˇ»
|
||
"});
|
||
|
||
// Manipulate with multiple disjoin selections
|
||
cx.set_state(indoc! {"
|
||
5«
|
||
4
|
||
3
|
||
2
|
||
1ˇ»
|
||
|
||
dd«dd
|
||
ccc
|
||
bb
|
||
aaaˇ»aa
|
||
"});
|
||
cx.update_editor(|e, cx| e.sort_lines_case_sensitive(&SortLinesCaseSensitive, cx));
|
||
cx.assert_editor_state(indoc! {"
|
||
«1
|
||
2
|
||
3
|
||
4
|
||
5ˇ»
|
||
|
||
«aaaaa
|
||
bb
|
||
ccc
|
||
ddddˇ»
|
||
"});
|
||
}
|
||
|
||
#[gpui::test]
|
||
async fn test_manipulate_text(cx: &mut TestAppContext) {
|
||
init_test(cx, |_| {});
|
||
|
||
let mut cx = EditorTestContext::new(cx).await;
|
||
|
||
// Test convert_to_upper_case()
|
||
cx.set_state(indoc! {"
|
||
«hello worldˇ»
|
||
"});
|
||
cx.update_editor(|e, cx| e.convert_to_upper_case(&ConvertToUpperCase, cx));
|
||
cx.assert_editor_state(indoc! {"
|
||
«HELLO WORLDˇ»
|
||
"});
|
||
|
||
// Test convert_to_lower_case()
|
||
cx.set_state(indoc! {"
|
||
«HELLO WORLDˇ»
|
||
"});
|
||
cx.update_editor(|e, cx| e.convert_to_lower_case(&ConvertToLowerCase, cx));
|
||
cx.assert_editor_state(indoc! {"
|
||
«hello worldˇ»
|
||
"});
|
||
|
||
// Test multiple line, single selection case
|
||
// Test code hack that covers the fact that to_case crate doesn't support '\n' as a word boundary
|
||
cx.set_state(indoc! {"
|
||
«The quick brown
|
||
fox jumps over
|
||
the lazy dogˇ»
|
||
"});
|
||
cx.update_editor(|e, cx| e.convert_to_title_case(&ConvertToTitleCase, cx));
|
||
cx.assert_editor_state(indoc! {"
|
||
«The Quick Brown
|
||
Fox Jumps Over
|
||
The Lazy Dogˇ»
|
||
"});
|
||
|
||
// Test multiple line, single selection case
|
||
// Test code hack that covers the fact that to_case crate doesn't support '\n' as a word boundary
|
||
cx.set_state(indoc! {"
|
||
«The quick brown
|
||
fox jumps over
|
||
the lazy dogˇ»
|
||
"});
|
||
cx.update_editor(|e, cx| e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, cx));
|
||
cx.assert_editor_state(indoc! {"
|
||
«TheQuickBrown
|
||
FoxJumpsOver
|
||
TheLazyDogˇ»
|
||
"});
|
||
|
||
// From here on out, test more complex cases of manipulate_text()
|
||
|
||
// Test no selection case - should affect words cursors are in
|
||
// Cursor at beginning, middle, and end of word
|
||
cx.set_state(indoc! {"
|
||
ˇhello big beauˇtiful worldˇ
|
||
"});
|
||
cx.update_editor(|e, cx| e.convert_to_upper_case(&ConvertToUpperCase, cx));
|
||
cx.assert_editor_state(indoc! {"
|
||
«HELLOˇ» big «BEAUTIFULˇ» «WORLDˇ»
|
||
"});
|
||
|
||
// Test multiple selections on a single line and across multiple lines
|
||
cx.set_state(indoc! {"
|
||
«Theˇ» quick «brown
|
||
foxˇ» jumps «overˇ»
|
||
the «lazyˇ» dog
|
||
"});
|
||
cx.update_editor(|e, cx| e.convert_to_upper_case(&ConvertToUpperCase, cx));
|
||
cx.assert_editor_state(indoc! {"
|
||
«THEˇ» quick «BROWN
|
||
FOXˇ» jumps «OVERˇ»
|
||
the «LAZYˇ» dog
|
||
"});
|
||
|
||
// Test case where text length grows
|
||
cx.set_state(indoc! {"
|
||
«tschüߡ»
|
||
"});
|
||
cx.update_editor(|e, cx| e.convert_to_upper_case(&ConvertToUpperCase, cx));
|
||
cx.assert_editor_state(indoc! {"
|
||
«TSCHÜSSˇ»
|
||
"});
|
||
|
||
// Test to make sure we don't crash when text shrinks
|
||
cx.set_state(indoc! {"
|
||
aaa_bbbˇ
|
||
"});
|
||
cx.update_editor(|e, cx| e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, cx));
|
||
cx.assert_editor_state(indoc! {"
|
||
«aaaBbbˇ»
|
||
"});
|
||
|
||
// Test to make sure we all aware of the fact that each word can grow and shrink
|
||
// Final selections should be aware of this fact
|
||
cx.set_state(indoc! {"
|
||
aaa_bˇbb bbˇb_ccc ˇccc_ddd
|
||
"});
|
||
cx.update_editor(|e, cx| e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, cx));
|
||
cx.assert_editor_state(indoc! {"
|
||
«aaaBbbˇ» «bbbCccˇ» «cccDddˇ»
|
||
"});
|
||
}
|
||
|
||
#[gpui::test]
|
||
fn test_duplicate_line(cx: &mut TestAppContext) {
|
||
init_test(cx, |_| {});
|
||
|
||
let view = cx.add_window(|cx| {
|
||
let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", 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 view = cx.add_window(|cx| {
|
||
let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", 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 TestAppContext) {
|
||
init_test(cx, |_| {});
|
||
|
||
let view = cx.add_window(|cx| {
|
||
let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), 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),
|
||
],
|
||
true,
|
||
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 TestAppContext) {
|
||
init_test(cx, |_| {});
|
||
|
||
let editor = cx.add_window(|cx| {
|
||
let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
|
||
build_editor(buffer, cx)
|
||
});
|
||
editor.update(cx, |editor, cx| {
|
||
let snapshot = editor.buffer.read(cx).snapshot(cx);
|
||
editor.insert_blocks(
|
||
[BlockProperties {
|
||
style: BlockStyle::Fixed,
|
||
position: snapshot.anchor_after(Point::new(2, 0)),
|
||
disposition: BlockDisposition::Below,
|
||
height: 1,
|
||
render: Arc::new(|_| div().into_any()),
|
||
}],
|
||
Some(Autoscroll::fit()),
|
||
cx,
|
||
);
|
||
editor.change_selections(None, cx, |s| {
|
||
s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
|
||
});
|
||
editor.move_line_down(&MoveLineDown, cx);
|
||
});
|
||
}
|
||
|
||
//todo!(test_transpose)
|
||
#[gpui::test]
|
||
fn test_transpose(cx: &mut TestAppContext) {
|
||
init_test(cx, |_| {});
|
||
|
||
_ = cx.add_window(|cx| {
|
||
let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), cx);
|
||
editor.set_style(EditorStyle::default(), 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
|
||
});
|
||
|
||
_ = cx.add_window(|cx| {
|
||
let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), cx);
|
||
editor.set_style(EditorStyle::default(), 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
|
||
});
|
||
|
||
_ = cx.add_window(|cx| {
|
||
let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), cx);
|
||
editor.set_style(EditorStyle::default(), 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
|
||
});
|
||
|
||
_ = cx.add_window(|cx| {
|
||
let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), cx);
|
||
editor.set_style(EditorStyle::default(), 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
|
||
});
|
||
}
|
||
|
||
#[gpui::test]
|
||
async fn test_clipboard(cx: &mut gpui::TestAppContext) {
|
||
init_test(cx, |_| {});
|
||
|
||
let mut cx = EditorTestContext::new(cx).await;
|
||
|
||
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(
|
||
&([
|
||
"( one✅ ",
|
||
"three ",
|
||
"five ) ˇtwo one✅ four three six five ( one✅ ",
|
||
"three ",
|
||
"five ) ˇ",
|
||
]
|
||
.join("\n")),
|
||
);
|
||
|
||
// 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));
|
||
assert_eq!(
|
||
cx.read_from_clipboard().map(|item| item.text().to_owned()),
|
||
Some("fox jumps over\n".to_owned())
|
||
);
|
||
|
||
// 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) {
|
||
init_test(cx, |_| {});
|
||
|
||
let mut cx = EditorTestContext::new(cx).await;
|
||
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 TestAppContext) {
|
||
init_test(cx, |_| {});
|
||
|
||
let view = cx.add_window(|cx| {
|
||
let buffer = MultiBuffer::build_simple("abc\nde\nfgh", 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 TestAppContext) {
|
||
init_test(cx, |_| {});
|
||
|
||
let view = cx.add_window(|cx| {
|
||
let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), 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 TestAppContext) {
|
||
init_test(cx, |_| {});
|
||
|
||
let view = cx.add_window(|cx| {
|
||
let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), 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),
|
||
],
|
||
true,
|
||
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]
|
||
async fn test_add_selection_above_below(cx: &mut TestAppContext) {
|
||
init_test(cx, |_| {});
|
||
|
||
let mut cx = EditorTestContext::new(cx).await;
|
||
|
||
// let buffer = MultiBuffer::build_simple("abc\ndefghi\n\njk\nlmno\n", cx);
|
||
cx.set_state(indoc!(
|
||
r#"abc
|
||
defˇghi
|
||
|
||
jk
|
||
nlmo
|
||
"#
|
||
));
|
||
|
||
cx.update_editor(|editor, cx| {
|
||
editor.add_selection_above(&Default::default(), cx);
|
||
});
|
||
|
||
cx.assert_editor_state(indoc!(
|
||
r#"abcˇ
|
||
defˇghi
|
||
|
||
jk
|
||
nlmo
|
||
"#
|
||
));
|
||
|
||
cx.update_editor(|editor, cx| {
|
||
editor.add_selection_above(&Default::default(), cx);
|
||
});
|
||
|
||
cx.assert_editor_state(indoc!(
|
||
r#"abcˇ
|
||
defˇghi
|
||
|
||
jk
|
||
nlmo
|
||
"#
|
||
));
|
||
|
||
cx.update_editor(|view, cx| {
|
||
view.add_selection_below(&Default::default(), cx);
|
||
});
|
||
|
||
cx.assert_editor_state(indoc!(
|
||
r#"abc
|
||
defˇghi
|
||
|
||
jk
|
||
nlmo
|
||
"#
|
||
));
|
||
|
||
cx.update_editor(|view, cx| {
|
||
view.undo_selection(&Default::default(), cx);
|
||
});
|
||
|
||
cx.assert_editor_state(indoc!(
|
||
r#"abcˇ
|
||
defˇghi
|
||
|
||
jk
|
||
nlmo
|
||
"#
|
||
));
|
||
|
||
cx.update_editor(|view, cx| {
|
||
view.redo_selection(&Default::default(), cx);
|
||
});
|
||
|
||
cx.assert_editor_state(indoc!(
|
||
r#"abc
|
||
defˇghi
|
||
|
||
jk
|
||
nlmo
|
||
"#
|
||
));
|
||
|
||
cx.update_editor(|view, cx| {
|
||
view.add_selection_below(&Default::default(), cx);
|
||
});
|
||
|
||
cx.assert_editor_state(indoc!(
|
||
r#"abc
|
||
defˇghi
|
||
|
||
jk
|
||
nlmˇo
|
||
"#
|
||
));
|
||
|
||
cx.update_editor(|view, cx| {
|
||
view.add_selection_below(&Default::default(), cx);
|
||
});
|
||
|
||
cx.assert_editor_state(indoc!(
|
||
r#"abc
|
||
defˇghi
|
||
|
||
jk
|
||
nlmˇo
|
||
"#
|
||
));
|
||
|
||
// change selections
|
||
cx.set_state(indoc!(
|
||
r#"abc
|
||
def«ˇg»hi
|
||
|
||
jk
|
||
nlmo
|
||
"#
|
||
));
|
||
|
||
cx.update_editor(|view, cx| {
|
||
view.add_selection_below(&Default::default(), cx);
|
||
});
|
||
|
||
cx.assert_editor_state(indoc!(
|
||
r#"abc
|
||
def«ˇg»hi
|
||
|
||
jk
|
||
nlm«ˇo»
|
||
"#
|
||
));
|
||
|
||
cx.update_editor(|view, cx| {
|
||
view.add_selection_below(&Default::default(), cx);
|
||
});
|
||
|
||
cx.assert_editor_state(indoc!(
|
||
r#"abc
|
||
def«ˇg»hi
|
||
|
||
jk
|
||
nlm«ˇo»
|
||
"#
|
||
));
|
||
|
||
cx.update_editor(|view, cx| {
|
||
view.add_selection_above(&Default::default(), cx);
|
||
});
|
||
|
||
cx.assert_editor_state(indoc!(
|
||
r#"abc
|
||
def«ˇg»hi
|
||
|
||
jk
|
||
nlmo
|
||
"#
|
||
));
|
||
|
||
cx.update_editor(|view, cx| {
|
||
view.add_selection_above(&Default::default(), cx);
|
||
});
|
||
|
||
cx.assert_editor_state(indoc!(
|
||
r#"abc
|
||
def«ˇg»hi
|
||
|
||
jk
|
||
nlmo
|
||
"#
|
||
));
|
||
|
||
// Change selections again
|
||
cx.set_state(indoc!(
|
||
r#"a«bc
|
||
defgˇ»hi
|
||
|
||
jk
|
||
nlmo
|
||
"#
|
||
));
|
||
|
||
cx.update_editor(|view, cx| {
|
||
view.add_selection_below(&Default::default(), cx);
|
||
});
|
||
|
||
cx.assert_editor_state(indoc!(
|
||
r#"a«bcˇ»
|
||
d«efgˇ»hi
|
||
|
||
j«kˇ»
|
||
nlmo
|
||
"#
|
||
));
|
||
|
||
cx.update_editor(|view, cx| {
|
||
view.add_selection_below(&Default::default(), cx);
|
||
});
|
||
cx.assert_editor_state(indoc!(
|
||
r#"a«bcˇ»
|
||
d«efgˇ»hi
|
||
|
||
j«kˇ»
|
||
n«lmoˇ»
|
||
"#
|
||
));
|
||
cx.update_editor(|view, cx| {
|
||
view.add_selection_above(&Default::default(), cx);
|
||
});
|
||
|
||
cx.assert_editor_state(indoc!(
|
||
r#"a«bcˇ»
|
||
d«efgˇ»hi
|
||
|
||
j«kˇ»
|
||
nlmo
|
||
"#
|
||
));
|
||
|
||
// Change selections again
|
||
cx.set_state(indoc!(
|
||
r#"abc
|
||
d«ˇefghi
|
||
|
||
jk
|
||
nlm»o
|
||
"#
|
||
));
|
||
|
||
cx.update_editor(|view, cx| {
|
||
view.add_selection_above(&Default::default(), cx);
|
||
});
|
||
|
||
cx.assert_editor_state(indoc!(
|
||
r#"a«ˇbc»
|
||
d«ˇef»ghi
|
||
|
||
j«ˇk»
|
||
n«ˇlm»o
|
||
"#
|
||
));
|
||
|
||
cx.update_editor(|view, cx| {
|
||
view.add_selection_below(&Default::default(), cx);
|
||
});
|
||
|
||
cx.assert_editor_state(indoc!(
|
||
r#"abc
|
||
d«ˇef»ghi
|
||
|
||
j«ˇk»
|
||
n«ˇlm»o
|
||
"#
|
||
));
|
||
}
|
||
|
||
#[gpui::test]
|
||
async fn test_select_next(cx: &mut gpui::TestAppContext) {
|
||
init_test(cx, |_| {});
|
||
|
||
let mut cx = EditorTestContext::new(cx).await;
|
||
cx.set_state("abc\nˇabc abc\ndefabc\nabc");
|
||
|
||
cx.update_editor(|e, cx| e.select_next(&SelectNext::default(), cx))
|
||
.unwrap();
|
||
cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
|
||
|
||
cx.update_editor(|e, cx| e.select_next(&SelectNext::default(), cx))
|
||
.unwrap();
|
||
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))
|
||
.unwrap();
|
||
cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
|
||
|
||
cx.update_editor(|e, cx| e.select_next(&SelectNext::default(), cx))
|
||
.unwrap();
|
||
cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
|
||
}
|
||
|
||
#[gpui::test]
|
||
async fn test_select_previous(cx: &mut gpui::TestAppContext) {
|
||
init_test(cx, |_| {});
|
||
{
|
||
// `Select previous` without a selection (selects wordwise)
|
||
let mut cx = EditorTestContext::new(cx).await;
|
||
cx.set_state("abc\nˇabc abc\ndefabc\nabc");
|
||
|
||
cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx))
|
||
.unwrap();
|
||
cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
|
||
|
||
cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx))
|
||
.unwrap();
|
||
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_previous(&SelectPrevious::default(), cx))
|
||
.unwrap();
|
||
cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
|
||
|
||
cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx))
|
||
.unwrap();
|
||
cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
|
||
}
|
||
{
|
||
// `Select previous` with a selection
|
||
let mut cx = EditorTestContext::new(cx).await;
|
||
cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
|
||
|
||
cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx))
|
||
.unwrap();
|
||
cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\nabc");
|
||
|
||
cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx))
|
||
.unwrap();
|
||
cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\n«abcˇ»");
|
||
|
||
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\n«abcˇ»");
|
||
|
||
cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx))
|
||
.unwrap();
|
||
cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndef«abcˇ»\n«abcˇ»");
|
||
|
||
cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx))
|
||
.unwrap();
|
||
cx.assert_editor_state("«abcˇ»\n«ˇabc» «abcˇ»\ndef«abcˇ»\n«abcˇ»");
|
||
}
|
||
}
|
||
|
||
#[gpui::test]
|
||
async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) {
|
||
init_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.build_model(|cx| {
|
||
Buffer::new(0, cx.entity_id().as_u64(), text).with_language(language, cx)
|
||
});
|
||
let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
|
||
let (view, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
|
||
|
||
view.condition::<crate::EditorEvent>(&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),
|
||
],
|
||
true,
|
||
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) {
|
||
init_test(cx, |_| {});
|
||
|
||
let language = Arc::new(
|
||
Language::new(
|
||
LanguageConfig {
|
||
brackets: BracketPairConfig {
|
||
pairs: 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()
|
||
},
|
||
..Default::default()
|
||
},
|
||
Some(tree_sitter_rust::language()),
|
||
)
|
||
.with_indents_query(
|
||
r#"
|
||
(_ "(" ")" @end) @indent
|
||
(_ "{" "}" @end) @indent
|
||
"#,
|
||
)
|
||
.unwrap(),
|
||
);
|
||
|
||
let text = "fn a() {}";
|
||
|
||
let buffer = cx.build_model(|cx| {
|
||
Buffer::new(0, cx.entity_id().as_u64(), text).with_language(language, cx)
|
||
});
|
||
let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
|
||
let (editor, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
|
||
editor
|
||
.condition::<crate::EditorEvent>(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) {
|
||
init_test(cx, |_| {});
|
||
|
||
let mut cx = EditorTestContext::new(cx).await;
|
||
|
||
let language = Arc::new(Language::new(
|
||
LanguageConfig {
|
||
brackets: BracketPairConfig {
|
||
pairs: 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: true,
|
||
newline: true,
|
||
},
|
||
BracketPair {
|
||
start: "[".to_string(),
|
||
end: "]".to_string(),
|
||
close: false,
|
||
newline: true,
|
||
},
|
||
BracketPair {
|
||
start: "\"".to_string(),
|
||
end: "\"".to_string(),
|
||
close: true,
|
||
newline: false,
|
||
},
|
||
],
|
||
..Default::default()
|
||
},
|
||
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(),
|
||
);
|
||
|
||
// insert a different closing bracket
|
||
cx.update_editor(|view, cx| {
|
||
view.handle_input(")", cx);
|
||
});
|
||
cx.assert_editor_state(
|
||
&"
|
||
🏀{{{)ˇ}}}
|
||
ε{{{)ˇ}}}
|
||
❤️{{{)ˇ}}}
|
||
"
|
||
.unindent(),
|
||
);
|
||
|
||
// skip over the auto-closed brackets when typing a closing bracket
|
||
cx.update_editor(|view, cx| {
|
||
view.move_right(&MoveRight, cx);
|
||
view.handle_input("}", cx);
|
||
view.handle_input("}", cx);
|
||
view.handle_input("}", cx);
|
||
});
|
||
cx.assert_editor_state(
|
||
&"
|
||
🏀{{{)}}}}ˇ
|
||
ε{{{)}}}}ˇ
|
||
❤️{{{)}}}}ˇ
|
||
"
|
||
.unindent(),
|
||
);
|
||
|
||
// autoclose multi-character pairs
|
||
cx.set_state(
|
||
&"
|
||
ˇ
|
||
ˇ
|
||
"
|
||
.unindent(),
|
||
);
|
||
cx.update_editor(|view, cx| {
|
||
view.handle_input("/", cx);
|
||
view.handle_input("*", cx);
|
||
});
|
||
cx.assert_editor_state(
|
||
&"
|
||
/*ˇ */
|
||
/*ˇ */
|
||
"
|
||
.unindent(),
|
||
);
|
||
|
||
// one cursor autocloses a multi-character pair, one cursor
|
||
// does not autoclose.
|
||
cx.set_state(
|
||
&"
|
||
/ˇ
|
||
ˇ
|
||
"
|
||
.unindent(),
|
||
);
|
||
cx.update_editor(|view, cx| view.handle_input("*", cx));
|
||
cx.assert_editor_state(
|
||
&"
|
||
/*ˇ */
|
||
*ˇ
|
||
"
|
||
.unindent(),
|
||
);
|
||
|
||
// Don't autoclose if the next character isn't whitespace and isn't
|
||
// listed in the language's "autoclose_before" section.
|
||
cx.set_state("ˇa b");
|
||
cx.update_editor(|view, cx| view.handle_input("{", cx));
|
||
cx.assert_editor_state("{ˇa b");
|
||
|
||
// Don't autoclose if `close` is false for the bracket pair
|
||
cx.set_state("ˇ");
|
||
cx.update_editor(|view, cx| view.handle_input("[", cx));
|
||
cx.assert_editor_state("[ˇ");
|
||
|
||
// Surround with brackets if text is selected
|
||
cx.set_state("«aˇ» b");
|
||
cx.update_editor(|view, cx| view.handle_input("{", cx));
|
||
cx.assert_editor_state("{«aˇ»} b");
|
||
|
||
// Autclose pair where the start and end characters are the same
|
||
cx.set_state("aˇ");
|
||
cx.update_editor(|view, cx| view.handle_input("\"", cx));
|
||
cx.assert_editor_state("a\"ˇ\"");
|
||
cx.update_editor(|view, cx| view.handle_input("\"", cx));
|
||
cx.assert_editor_state("a\"\"ˇ");
|
||
}
|
||
|
||
#[gpui::test]
|
||
async fn test_autoclose_with_embedded_language(cx: &mut gpui::TestAppContext) {
|
||
init_test(cx, |_| {});
|
||
|
||
let mut cx = EditorTestContext::new(cx).await;
|
||
|
||
let html_language = Arc::new(
|
||
Language::new(
|
||
LanguageConfig {
|
||
name: "HTML".into(),
|
||
brackets: BracketPairConfig {
|
||
pairs: vec![
|
||
BracketPair {
|
||
start: "<".into(),
|
||
end: ">".into(),
|
||
close: true,
|
||
..Default::default()
|
||
},
|
||
BracketPair {
|
||
start: "{".into(),
|
||
end: "}".into(),
|
||
close: true,
|
||
..Default::default()
|
||
},
|
||
BracketPair {
|
||
start: "(".into(),
|
||
end: ")".into(),
|
||
close: true,
|
||
..Default::default()
|
||
},
|
||
],
|
||
..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: BracketPairConfig {
|
||
pairs: vec![
|
||
BracketPair {
|
||
start: "/*".into(),
|
||
end: " */".into(),
|
||
close: true,
|
||
..Default::default()
|
||
},
|
||
BracketPair {
|
||
start: "{".into(),
|
||
end: "}".into(),
|
||
close: true,
|
||
..Default::default()
|
||
},
|
||
BracketPair {
|
||
start: "(".into(),
|
||
end: ")".into(),
|
||
close: true,
|
||
..Default::default()
|
||
},
|
||
],
|
||
..Default::default()
|
||
},
|
||
autoclose_before: "})]>".into(),
|
||
..Default::default()
|
||
},
|
||
Some(tree_sitter_typescript::language_tsx()),
|
||
));
|
||
|
||
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_autoclose_with_overrides(cx: &mut gpui::TestAppContext) {
|
||
init_test(cx, |_| {});
|
||
|
||
let mut cx = EditorTestContext::new(cx).await;
|
||
|
||
let rust_language = Arc::new(
|
||
Language::new(
|
||
LanguageConfig {
|
||
name: "Rust".into(),
|
||
brackets: serde_json::from_value(json!([
|
||
{ "start": "{", "end": "}", "close": true, "newline": true },
|
||
{ "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
|
||
]))
|
||
.unwrap(),
|
||
autoclose_before: "})]>".into(),
|
||
..Default::default()
|
||
},
|
||
Some(tree_sitter_rust::language()),
|
||
)
|
||
.with_override_query("(string_literal) @string")
|
||
.unwrap(),
|
||
);
|
||
|
||
let registry = Arc::new(LanguageRegistry::test());
|
||
registry.add(rust_language.clone());
|
||
|
||
cx.update_buffer(|buffer, cx| {
|
||
buffer.set_language_registry(registry);
|
||
buffer.set_language(Some(rust_language), cx);
|
||
});
|
||
|
||
cx.set_state(
|
||
&r#"
|
||
let x = ˇ
|
||
"#
|
||
.unindent(),
|
||
);
|
||
|
||
// Inserting a quotation mark. A closing quotation mark is automatically inserted.
|
||
cx.update_editor(|editor, cx| {
|
||
editor.handle_input("\"", cx);
|
||
});
|
||
cx.assert_editor_state(
|
||
&r#"
|
||
let x = "ˇ"
|
||
"#
|
||
.unindent(),
|
||
);
|
||
|
||
// Inserting another quotation mark. The cursor moves across the existing
|
||
// automatically-inserted quotation mark.
|
||
cx.update_editor(|editor, cx| {
|
||
editor.handle_input("\"", cx);
|
||
});
|
||
cx.assert_editor_state(
|
||
&r#"
|
||
let x = ""ˇ
|
||
"#
|
||
.unindent(),
|
||
);
|
||
|
||
// Reset
|
||
cx.set_state(
|
||
&r#"
|
||
let x = ˇ
|
||
"#
|
||
.unindent(),
|
||
);
|
||
|
||
// Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
|
||
cx.update_editor(|editor, cx| {
|
||
editor.handle_input("\"", cx);
|
||
editor.handle_input(" ", cx);
|
||
editor.move_left(&Default::default(), cx);
|
||
editor.handle_input("\\", cx);
|
||
editor.handle_input("\"", cx);
|
||
});
|
||
cx.assert_editor_state(
|
||
&r#"
|
||
let x = "\"ˇ "
|
||
"#
|
||
.unindent(),
|
||
);
|
||
|
||
// Inserting a closing quotation mark at the position of an automatically-inserted quotation
|
||
// mark. Nothing is inserted.
|
||
cx.update_editor(|editor, cx| {
|
||
editor.move_right(&Default::default(), cx);
|
||
editor.handle_input("\"", cx);
|
||
});
|
||
cx.assert_editor_state(
|
||
&r#"
|
||
let x = "\" "ˇ
|
||
"#
|
||
.unindent(),
|
||
);
|
||
}
|
||
|
||
#[gpui::test]
|
||
async fn test_surround_with_pair(cx: &mut gpui::TestAppContext) {
|
||
init_test(cx, |_| {});
|
||
|
||
let language = Arc::new(Language::new(
|
||
LanguageConfig {
|
||
brackets: BracketPairConfig {
|
||
pairs: vec![
|
||
BracketPair {
|
||
start: "{".to_string(),
|
||
end: "}".to_string(),
|
||
close: true,
|
||
newline: true,
|
||
},
|
||
BracketPair {
|
||
start: "/* ".to_string(),
|
||
end: "*/".to_string(),
|
||
close: true,
|
||
..Default::default()
|
||
},
|
||
],
|
||
..Default::default()
|
||
},
|
||
..Default::default()
|
||
},
|
||
Some(tree_sitter_rust::language()),
|
||
));
|
||
|
||
let text = r#"
|
||
a
|
||
b
|
||
c
|
||
"#
|
||
.unindent();
|
||
|
||
let buffer = cx.build_model(|cx| {
|
||
Buffer::new(0, cx.entity_id().as_u64(), text).with_language(language, cx)
|
||
});
|
||
let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
|
||
let (view, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
|
||
view.condition::<crate::EditorEvent>(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);
|
||
view.undo(&Undo, cx);
|
||
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)
|
||
]
|
||
);
|
||
|
||
// Ensure inserting the first character of a multi-byte bracket pair
|
||
// doesn't surround the selections with the bracket.
|
||
view.handle_input("/", cx);
|
||
assert_eq!(
|
||
view.text(cx),
|
||
"
|
||
/
|
||
/
|
||
/
|
||
"
|
||
.unindent()
|
||
);
|
||
assert_eq!(
|
||
view.selections.display_ranges(cx),
|
||
[
|
||
DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1),
|
||
DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1),
|
||
DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1)
|
||
]
|
||
);
|
||
|
||
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)
|
||
]
|
||
);
|
||
|
||
// Ensure inserting the last character of a multi-byte bracket pair
|
||
// doesn't surround the selections with the bracket.
|
||
view.handle_input("*", cx);
|
||
assert_eq!(
|
||
view.text(cx),
|
||
"
|
||
*
|
||
*
|
||
*
|
||
"
|
||
.unindent()
|
||
);
|
||
assert_eq!(
|
||
view.selections.display_ranges(cx),
|
||
[
|
||
DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1),
|
||
DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1),
|
||
DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1)
|
||
]
|
||
);
|
||
});
|
||
}
|
||
|
||
#[gpui::test]
|
||
async fn test_delete_autoclose_pair(cx: &mut gpui::TestAppContext) {
|
||
init_test(cx, |_| {});
|
||
|
||
let language = Arc::new(Language::new(
|
||
LanguageConfig {
|
||
brackets: BracketPairConfig {
|
||
pairs: vec![BracketPair {
|
||
start: "{".to_string(),
|
||
end: "}".to_string(),
|
||
close: true,
|
||
newline: true,
|
||
}],
|
||
..Default::default()
|
||
},
|
||
autoclose_before: "}".to_string(),
|
||
..Default::default()
|
||
},
|
||
Some(tree_sitter_rust::language()),
|
||
));
|
||
|
||
let text = r#"
|
||
a
|
||
b
|
||
c
|
||
"#
|
||
.unindent();
|
||
|
||
let buffer = cx.build_model(|cx| {
|
||
Buffer::new(0, cx.entity_id().as_u64(), text).with_language(language, cx)
|
||
});
|
||
let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
|
||
let (editor, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
|
||
editor
|
||
.condition::<crate::EditorEvent>(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)
|
||
]
|
||
);
|
||
});
|
||
}
|
||
|
||
// todo!(select_anchor_ranges)
|
||
#[gpui::test]
|
||
async fn test_snippets(cx: &mut gpui::TestAppContext) {
|
||
init_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, mut cx) = cx.add_window_view(|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) {
|
||
init_test(cx, |_| {});
|
||
|
||
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.executor());
|
||
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.executor().start_waiting();
|
||
let fake_server = fake_servers.next().await.unwrap();
|
||
|
||
let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
|
||
let (editor, cx) = cx.add_window_view(|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 = editor
|
||
.update(cx, |editor, cx| editor.save(project.clone(), cx))
|
||
.unwrap();
|
||
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.executor().start_waiting();
|
||
let x = save.await;
|
||
|
||
assert_eq!(
|
||
editor.update(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 = editor
|
||
.update(cx, |editor, cx| editor.save(project.clone(), cx))
|
||
.unwrap();
|
||
cx.executor().advance_clock(super::FORMAT_TIMEOUT);
|
||
cx.executor().start_waiting();
|
||
save.await;
|
||
assert_eq!(
|
||
editor.update(cx, |editor, cx| editor.text(cx)),
|
||
"one\ntwo\nthree\n"
|
||
);
|
||
assert!(!cx.read(|cx| editor.is_dirty(cx)));
|
||
|
||
// Set rust language override and assert overridden tabsize is sent to language server
|
||
update_test_language_settings(cx, |settings| {
|
||
settings.languages.insert(
|
||
"Rust".into(),
|
||
LanguageSettingsContent {
|
||
tab_size: NonZeroU32::new(8),
|
||
..Default::default()
|
||
},
|
||
);
|
||
});
|
||
|
||
let save = editor
|
||
.update(cx, |editor, cx| editor.save(project.clone(), cx))
|
||
.unwrap();
|
||
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.executor().start_waiting();
|
||
save.await;
|
||
}
|
||
|
||
#[gpui::test]
|
||
async fn test_range_format_during_save(cx: &mut gpui::TestAppContext) {
|
||
init_test(cx, |_| {});
|
||
|
||
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.executor());
|
||
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.executor().start_waiting();
|
||
let fake_server = fake_servers.next().await.unwrap();
|
||
|
||
let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
|
||
let (editor, cx) = cx.add_window_view(|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 = editor
|
||
.update(cx, |editor, cx| editor.save(project.clone(), cx))
|
||
.unwrap();
|
||
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.executor().start_waiting();
|
||
save.await;
|
||
assert_eq!(
|
||
editor.update(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 = editor
|
||
.update(cx, |editor, cx| editor.save(project.clone(), cx))
|
||
.unwrap();
|
||
cx.executor().advance_clock(super::FORMAT_TIMEOUT);
|
||
cx.executor().start_waiting();
|
||
save.await;
|
||
assert_eq!(
|
||
editor.update(cx, |editor, cx| editor.text(cx)),
|
||
"one\ntwo\nthree\n"
|
||
);
|
||
assert!(!cx.read(|cx| editor.is_dirty(cx)));
|
||
|
||
// Set rust language override and assert overridden tabsize is sent to language server
|
||
update_test_language_settings(cx, |settings| {
|
||
settings.languages.insert(
|
||
"Rust".into(),
|
||
LanguageSettingsContent {
|
||
tab_size: NonZeroU32::new(8),
|
||
..Default::default()
|
||
},
|
||
);
|
||
});
|
||
|
||
let save = editor
|
||
.update(cx, |editor, cx| editor.save(project.clone(), cx))
|
||
.unwrap();
|
||
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.executor().start_waiting();
|
||
save.await;
|
||
}
|
||
|
||
#[gpui::test]
|
||
async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) {
|
||
init_test(cx, |settings| {
|
||
settings.defaults.formatter = Some(language_settings::Formatter::LanguageServer)
|
||
});
|
||
|
||
let mut language = Language::new(
|
||
LanguageConfig {
|
||
name: "Rust".into(),
|
||
path_suffixes: vec!["rs".to_string()],
|
||
// Enable Prettier formatting for the same buffer, and ensure
|
||
// LSP is called instead of Prettier.
|
||
prettier_parser_name: Some("test_parser".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.executor());
|
||
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.executor().start_waiting();
|
||
let fake_server = fake_servers.next().await.unwrap();
|
||
|
||
let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
|
||
let (editor, cx) = cx.add_window_view(|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(), FormatTrigger::Manual, cx)
|
||
})
|
||
.unwrap();
|
||
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.executor().start_waiting();
|
||
format.await;
|
||
assert_eq!(
|
||
editor.update(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, FormatTrigger::Manual, cx)
|
||
})
|
||
.unwrap();
|
||
cx.executor().advance_clock(super::FORMAT_TIMEOUT);
|
||
cx.executor().start_waiting();
|
||
format.await;
|
||
assert_eq!(
|
||
editor.update(cx, |editor, cx| editor.text(cx)),
|
||
"one\ntwo\nthree\n"
|
||
);
|
||
}
|
||
|
||
#[gpui::test]
|
||
async fn test_concurrent_format_requests(cx: &mut gpui::TestAppContext) {
|
||
init_test(cx, |_| {});
|
||
|
||
let mut cx = EditorLspTestContext::new_rust(
|
||
lsp::ServerCapabilities {
|
||
document_formatting_provider: Some(lsp::OneOf::Left(true)),
|
||
..Default::default()
|
||
},
|
||
cx,
|
||
)
|
||
.await;
|
||
|
||
cx.set_state(indoc! {"
|
||
one.twoˇ
|
||
"});
|
||
|
||
// The format request takes a long time. When it completes, it inserts
|
||
// a newline and an indent before the `.`
|
||
cx.lsp
|
||
.handle_request::<lsp::request::Formatting, _, _>(move |_, cx| {
|
||
let executor = cx.background_executor().clone();
|
||
async move {
|
||
executor.timer(Duration::from_millis(100)).await;
|
||
Ok(Some(vec![lsp::TextEdit {
|
||
range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
|
||
new_text: "\n ".into(),
|
||
}]))
|
||
}
|
||
});
|
||
|
||
// Submit a format request.
|
||
let format_1 = cx
|
||
.update_editor(|editor, cx| editor.format(&Format, cx))
|
||
.unwrap();
|
||
cx.executor().run_until_parked();
|
||
|
||
// Submit a second format request.
|
||
let format_2 = cx
|
||
.update_editor(|editor, cx| editor.format(&Format, cx))
|
||
.unwrap();
|
||
cx.executor().run_until_parked();
|
||
|
||
// Wait for both format requests to complete
|
||
cx.executor().advance_clock(Duration::from_millis(200));
|
||
cx.executor().start_waiting();
|
||
format_1.await.unwrap();
|
||
cx.executor().start_waiting();
|
||
format_2.await.unwrap();
|
||
|
||
// The formatting edits only happens once.
|
||
cx.assert_editor_state(indoc! {"
|
||
one
|
||
.twoˇ
|
||
"});
|
||
}
|
||
|
||
#[gpui::test]
|
||
async fn test_strip_whitespace_and_format_via_lsp(cx: &mut gpui::TestAppContext) {
|
||
init_test(cx, |settings| {
|
||
settings.defaults.formatter = Some(language_settings::Formatter::Auto)
|
||
});
|
||
|
||
let mut cx = EditorLspTestContext::new_rust(
|
||
lsp::ServerCapabilities {
|
||
document_formatting_provider: Some(lsp::OneOf::Left(true)),
|
||
..Default::default()
|
||
},
|
||
cx,
|
||
)
|
||
.await;
|
||
|
||
// Set up a buffer white some trailing whitespace and no trailing newline.
|
||
cx.set_state(
|
||
&[
|
||
"one ", //
|
||
"twoˇ", //
|
||
"three ", //
|
||
"four", //
|
||
]
|
||
.join("\n"),
|
||
);
|
||
|
||
// Submit a format request.
|
||
let format = cx
|
||
.update_editor(|editor, cx| editor.format(&Format, cx))
|
||
.unwrap();
|
||
|
||
// Record which buffer changes have been sent to the language server
|
||
let buffer_changes = Arc::new(Mutex::new(Vec::new()));
|
||
cx.lsp
|
||
.handle_notification::<lsp::notification::DidChangeTextDocument, _>({
|
||
let buffer_changes = buffer_changes.clone();
|
||
move |params, _| {
|
||
buffer_changes.lock().extend(
|
||
params
|
||
.content_changes
|
||
.into_iter()
|
||
.map(|e| (e.range.unwrap(), e.text)),
|
||
);
|
||
}
|
||
});
|
||
|
||
// Handle formatting requests to the language server.
|
||
cx.lsp.handle_request::<lsp::request::Formatting, _, _>({
|
||
let buffer_changes = buffer_changes.clone();
|
||
move |_, _| {
|
||
// When formatting is requested, trailing whitespace has already been stripped,
|
||
// and the trailing newline has already been added.
|
||
assert_eq!(
|
||
&buffer_changes.lock()[1..],
|
||
&[
|
||
(
|
||
lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
|
||
"".into()
|
||
),
|
||
(
|
||
lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
|
||
"".into()
|
||
),
|
||
(
|
||
lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
|
||
"\n".into()
|
||
),
|
||
]
|
||
);
|
||
|
||
// Insert blank lines between each line of the buffer.
|
||
async move {
|
||
Ok(Some(vec![
|
||
lsp::TextEdit {
|
||
range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 0)),
|
||
new_text: "\n".into(),
|
||
},
|
||
lsp::TextEdit {
|
||
range: lsp::Range::new(lsp::Position::new(2, 0), lsp::Position::new(2, 0)),
|
||
new_text: "\n".into(),
|
||
},
|
||
]))
|
||
}
|
||
}
|
||
});
|
||
|
||
// After formatting the buffer, the trailing whitespace is stripped,
|
||
// a newline is appended, and the edits provided by the language server
|
||
// have been applied.
|
||
format.await.unwrap();
|
||
cx.assert_editor_state(
|
||
&[
|
||
"one", //
|
||
"", //
|
||
"twoˇ", //
|
||
"", //
|
||
"three", //
|
||
"four", //
|
||
"", //
|
||
]
|
||
.join("\n"),
|
||
);
|
||
|
||
// Undoing the formatting undoes the trailing whitespace removal, the
|
||
// trailing newline, and the LSP edits.
|
||
cx.update_buffer(|buffer, cx| buffer.undo(cx));
|
||
cx.assert_editor_state(
|
||
&[
|
||
"one ", //
|
||
"twoˇ", //
|
||
"three ", //
|
||
"four", //
|
||
]
|
||
.join("\n"),
|
||
);
|
||
}
|
||
|
||
#[gpui::test]
|
||
async fn test_completion(cx: &mut gpui::TestAppContext) {
|
||
init_test(cx, |_| {});
|
||
|
||
let mut cx = EditorLspTestContext::new_rust(
|
||
lsp::ServerCapabilities {
|
||
completion_provider: Some(lsp::CompletionOptions {
|
||
trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
|
||
resolve_provider: Some(true),
|
||
..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.context_menu_next(&Default::default(), 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(vec![
|
||
(
|
||
//This overlaps with the primary completion edit which is
|
||
//misbehavior from the LSP spec, test that we filter it out
|
||
indoc! {"
|
||
one.second_ˇcompletion
|
||
two
|
||
threeˇ
|
||
"},
|
||
"overlapping additional edit",
|
||
),
|
||
(
|
||
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.read().is_none()));
|
||
cx.simulate_keystroke("s");
|
||
assert!(cx.editor(|e, _| e.context_menu.read().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::<SettingsStore, _>(|settings, cx| {
|
||
settings.update_user_settings::<EditorSettings>(cx, |settings| {
|
||
settings.show_completions_on_input = Some(false);
|
||
});
|
||
})
|
||
});
|
||
cx.set_state("editorˇ");
|
||
cx.simulate_keystroke(".");
|
||
assert!(cx.editor(|e, _| e.context_menu.read().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.read().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();
|
||
}
|
||
|
||
#[gpui::test]
|
||
async fn test_toggle_comment(cx: &mut gpui::TestAppContext) {
|
||
init_test(cx, |_| {});
|
||
let mut cx = EditorTestContext::new(cx).await;
|
||
let language = Arc::new(Language::new(
|
||
LanguageConfig {
|
||
line_comment: Some("// ".into()),
|
||
..Default::default()
|
||
},
|
||
Some(tree_sitter_rust::language()),
|
||
));
|
||
cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
|
||
|
||
// If multiple selections intersect a line, the line is only toggled once.
|
||
cx.set_state(indoc! {"
|
||
fn a() {
|
||
«//b();
|
||
ˇ»// «c();
|
||
//ˇ» d();
|
||
}
|
||
"});
|
||
|
||
cx.update_editor(|e, cx| e.toggle_comments(&ToggleComments::default(), cx));
|
||
|
||
cx.assert_editor_state(indoc! {"
|
||
fn a() {
|
||
«b();
|
||
c();
|
||
ˇ» d();
|
||
}
|
||
"});
|
||
|
||
// The comment prefix is inserted at the same column for every line in a
|
||
// selection.
|
||
cx.update_editor(|e, cx| e.toggle_comments(&ToggleComments::default(), cx));
|
||
|
||
cx.assert_editor_state(indoc! {"
|
||
fn a() {
|
||
// «b();
|
||
// c();
|
||
ˇ»// d();
|
||
}
|
||
"});
|
||
|
||
// If a selection ends at the beginning of a line, that line is not toggled.
|
||
cx.set_selections_state(indoc! {"
|
||
fn a() {
|
||
// b();
|
||
«// c();
|
||
ˇ» // d();
|
||
}
|
||
"});
|
||
|
||
cx.update_editor(|e, cx| e.toggle_comments(&ToggleComments::default(), cx));
|
||
|
||
cx.assert_editor_state(indoc! {"
|
||
fn a() {
|
||
// b();
|
||
«c();
|
||
ˇ» // d();
|
||
}
|
||
"});
|
||
|
||
// If a selection span a single line and is empty, the line is toggled.
|
||
cx.set_state(indoc! {"
|
||
fn a() {
|
||
a();
|
||
b();
|
||
ˇ
|
||
}
|
||
"});
|
||
|
||
cx.update_editor(|e, cx| e.toggle_comments(&ToggleComments::default(), cx));
|
||
|
||
cx.assert_editor_state(indoc! {"
|
||
fn a() {
|
||
a();
|
||
b();
|
||
//•ˇ
|
||
}
|
||
"});
|
||
|
||
// If a selection span multiple lines, empty lines are not toggled.
|
||
cx.set_state(indoc! {"
|
||
fn a() {
|
||
«a();
|
||
|
||
c();ˇ»
|
||
}
|
||
"});
|
||
|
||
cx.update_editor(|e, cx| e.toggle_comments(&ToggleComments::default(), cx));
|
||
|
||
cx.assert_editor_state(indoc! {"
|
||
fn a() {
|
||
// «a();
|
||
|
||
// c();ˇ»
|
||
}
|
||
"});
|
||
}
|
||
|
||
#[gpui::test]
|
||
async fn test_advance_downward_on_toggle_comment(cx: &mut gpui::TestAppContext) {
|
||
init_test(cx, |_| {});
|
||
|
||
let language = Arc::new(Language::new(
|
||
LanguageConfig {
|
||
line_comment: Some("// ".into()),
|
||
..Default::default()
|
||
},
|
||
Some(tree_sitter_rust::language()),
|
||
));
|
||
|
||
let registry = Arc::new(LanguageRegistry::test());
|
||
registry.add(language.clone());
|
||
|
||
let mut cx = EditorTestContext::new(cx).await;
|
||
cx.update_buffer(|buffer, cx| {
|
||
buffer.set_language_registry(registry);
|
||
buffer.set_language(Some(language), cx);
|
||
});
|
||
|
||
let toggle_comments = &ToggleComments {
|
||
advance_downwards: true,
|
||
};
|
||
|
||
// Single cursor on one line -> advance
|
||
// Cursor moves horizontally 3 characters as well on non-blank line
|
||
cx.set_state(indoc!(
|
||
"fn a() {
|
||
ˇdog();
|
||
cat();
|
||
}"
|
||
));
|
||
cx.update_editor(|editor, cx| {
|
||
editor.toggle_comments(toggle_comments, cx);
|
||
});
|
||
cx.assert_editor_state(indoc!(
|
||
"fn a() {
|
||
// dog();
|
||
catˇ();
|
||
}"
|
||
));
|
||
|
||
// Single selection on one line -> don't advance
|
||
cx.set_state(indoc!(
|
||
"fn a() {
|
||
«dog()ˇ»;
|
||
cat();
|
||
}"
|
||
));
|
||
cx.update_editor(|editor, cx| {
|
||
editor.toggle_comments(toggle_comments, cx);
|
||
});
|
||
cx.assert_editor_state(indoc!(
|
||
"fn a() {
|
||
// «dog()ˇ»;
|
||
cat();
|
||
}"
|
||
));
|
||
|
||
// Multiple cursors on one line -> advance
|
||
cx.set_state(indoc!(
|
||
"fn a() {
|
||
ˇdˇog();
|
||
cat();
|
||
}"
|
||
));
|
||
cx.update_editor(|editor, cx| {
|
||
editor.toggle_comments(toggle_comments, cx);
|
||
});
|
||
cx.assert_editor_state(indoc!(
|
||
"fn a() {
|
||
// dog();
|
||
catˇ(ˇ);
|
||
}"
|
||
));
|
||
|
||
// Multiple cursors on one line, with selection -> don't advance
|
||
cx.set_state(indoc!(
|
||
"fn a() {
|
||
ˇdˇog«()ˇ»;
|
||
cat();
|
||
}"
|
||
));
|
||
cx.update_editor(|editor, cx| {
|
||
editor.toggle_comments(toggle_comments, cx);
|
||
});
|
||
cx.assert_editor_state(indoc!(
|
||
"fn a() {
|
||
// ˇdˇog«()ˇ»;
|
||
cat();
|
||
}"
|
||
));
|
||
|
||
// Single cursor on one line -> advance
|
||
// Cursor moves to column 0 on blank line
|
||
cx.set_state(indoc!(
|
||
"fn a() {
|
||
ˇdog();
|
||
|
||
cat();
|
||
}"
|
||
));
|
||
cx.update_editor(|editor, cx| {
|
||
editor.toggle_comments(toggle_comments, cx);
|
||
});
|
||
cx.assert_editor_state(indoc!(
|
||
"fn a() {
|
||
// dog();
|
||
ˇ
|
||
cat();
|
||
}"
|
||
));
|
||
|
||
// Single cursor on one line -> advance
|
||
// Cursor starts and ends at column 0
|
||
cx.set_state(indoc!(
|
||
"fn a() {
|
||
ˇ dog();
|
||
cat();
|
||
}"
|
||
));
|
||
cx.update_editor(|editor, cx| {
|
||
editor.toggle_comments(toggle_comments, cx);
|
||
});
|
||
cx.assert_editor_state(indoc!(
|
||
"fn a() {
|
||
// dog();
|
||
ˇ cat();
|
||
}"
|
||
));
|
||
}
|
||
|
||
#[gpui::test]
|
||
async fn test_toggle_block_comment(cx: &mut gpui::TestAppContext) {
|
||
init_test(cx, |_| {});
|
||
|
||
let mut cx = EditorTestContext::new(cx).await;
|
||
|
||
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_typescript::language_tsx()),
|
||
));
|
||
|
||
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::default(), 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::default(), 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::default(), 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::default(), 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.executor().run_until_parked();
|
||
cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments::default(), cx));
|
||
cx.assert_editor_state(
|
||
&r#"
|
||
<!-- ˇ<script> -->
|
||
// ˇvar x = new Y();
|
||
<!-- ˇ</script> -->
|
||
"#
|
||
.unindent(),
|
||
);
|
||
}
|
||
|
||
#[gpui::test]
|
||
fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
|
||
init_test(cx, |_| {});
|
||
|
||
let buffer =
|
||
cx.build_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), sample_text(3, 4, 'a')));
|
||
let multibuffer = cx.build_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,
|
||
);
|
||
assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
|
||
multibuffer
|
||
});
|
||
|
||
let (view, cx) = cx.add_window_view(|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),
|
||
]
|
||
);
|
||
|
||
// Ensure the cursor's head is respected when deleting across an excerpt boundary.
|
||
view.change_selections(None, cx, |s| {
|
||
s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
|
||
});
|
||
view.backspace(&Default::default(), cx);
|
||
assert_eq!(view.text(cx), "Xa\nbbb");
|
||
assert_eq!(
|
||
view.selections.ranges(cx),
|
||
[Point::new(1, 0)..Point::new(1, 0)]
|
||
);
|
||
|
||
view.change_selections(None, cx, |s| {
|
||
s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
|
||
});
|
||
view.backspace(&Default::default(), cx);
|
||
assert_eq!(view.text(cx), "X\nbb");
|
||
assert_eq!(
|
||
view.selections.ranges(cx),
|
||
[Point::new(0, 1)..Point::new(0, 1)]
|
||
);
|
||
});
|
||
}
|
||
|
||
#[gpui::test]
|
||
fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
|
||
init_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.build_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), initial_text));
|
||
let multibuffer = cx.build_model(|cx| {
|
||
let mut multibuffer = MultiBuffer::new(0);
|
||
multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
|
||
multibuffer
|
||
});
|
||
|
||
let (view, cx) = cx.add_window_view(|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 TestAppContext) {
|
||
init_test(cx, |_| {});
|
||
|
||
let buffer =
|
||
cx.build_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), sample_text(3, 4, 'a')));
|
||
let mut excerpt1_id = None;
|
||
let multibuffer = cx.build_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();
|
||
assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
|
||
multibuffer
|
||
});
|
||
|
||
let editor = cx.add_window(|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 TestAppContext) {
|
||
init_test(cx, |_| {});
|
||
|
||
let buffer =
|
||
cx.build_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), sample_text(3, 4, 'a')));
|
||
let mut excerpt1_id = None;
|
||
let multibuffer = cx.build_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();
|
||
assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
|
||
multibuffer
|
||
});
|
||
|
||
let editor = cx.add_window(|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) {
|
||
init_test(cx, |_| {});
|
||
|
||
let language = Arc::new(
|
||
Language::new(
|
||
LanguageConfig {
|
||
brackets: BracketPairConfig {
|
||
pairs: 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()
|
||
},
|
||
..Default::default()
|
||
},
|
||
Some(tree_sitter_rust::language()),
|
||
)
|
||
.with_indents_query("")
|
||
.unwrap(),
|
||
);
|
||
|
||
let text = concat!(
|
||
"{ }\n", //
|
||
" x\n", //
|
||
" /* */\n", //
|
||
"x\n", //
|
||
"{{} }\n", //
|
||
);
|
||
|
||
let buffer = cx.build_model(|cx| {
|
||
Buffer::new(0, cx.entity_id().as_u64(), text).with_language(language, cx)
|
||
});
|
||
let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
|
||
let (view, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
|
||
view.condition::<crate::EditorEvent>(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 TestAppContext) {
|
||
init_test(cx, |_| {});
|
||
|
||
let editor = cx.add_window(|cx| {
|
||
let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
|
||
build_editor(buffer.clone(), cx)
|
||
});
|
||
|
||
editor.update(cx, |editor, cx| {
|
||
struct Type1;
|
||
struct Type2;
|
||
|
||
let buffer = editor.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)),
|
||
],
|
||
|_| Hsla::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)),
|
||
],
|
||
|_| Hsla::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.theme().colors(),
|
||
);
|
||
// Enforce a consistent ordering based on color without relying on the ordering of the
|
||
// highlight's `TypeId` which is non-executor.
|
||
highlighted_ranges.sort_unstable_by_key(|(_, color)| *color);
|
||
assert_eq!(
|
||
highlighted_ranges,
|
||
&[
|
||
(
|
||
DisplayPoint::new(4, 2)..DisplayPoint::new(4, 4),
|
||
Hsla::red(),
|
||
),
|
||
(
|
||
DisplayPoint::new(6, 3)..DisplayPoint::new(6, 5),
|
||
Hsla::red(),
|
||
),
|
||
(
|
||
DisplayPoint::new(3, 2)..DisplayPoint::new(3, 5),
|
||
Hsla::green(),
|
||
),
|
||
(
|
||
DisplayPoint::new(5, 3)..DisplayPoint::new(5, 6),
|
||
Hsla::green(),
|
||
),
|
||
]
|
||
);
|
||
assert_eq!(
|
||
editor.background_highlights_in_range(
|
||
anchor_range(Point::new(5, 6)..Point::new(6, 4)),
|
||
&snapshot,
|
||
cx.theme().colors(),
|
||
),
|
||
&[(
|
||
DisplayPoint::new(6, 3)..DisplayPoint::new(6, 5),
|
||
Hsla::red(),
|
||
)]
|
||
);
|
||
});
|
||
}
|
||
|
||
// todo!(following)
|
||
#[gpui::test]
|
||
async fn test_following(cx: &mut gpui::TestAppContext) {
|
||
init_test(cx, |_| {});
|
||
|
||
let fs = FakeFs::new(cx.executor());
|
||
let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
|
||
|
||
let buffer = project.update(cx, |project, cx| {
|
||
let buffer = project
|
||
.create_buffer(&sample_text(16, 8, 'a'), None, cx)
|
||
.unwrap();
|
||
cx.build_model(|cx| MultiBuffer::singleton(buffer, cx))
|
||
});
|
||
let leader = cx.add_window(|cx| build_editor(buffer.clone(), cx));
|
||
let follower = cx.update(|cx| {
|
||
cx.open_window(
|
||
WindowOptions {
|
||
bounds: WindowBounds::Fixed(Bounds::from_corners(
|
||
gpui::Point::new((0. as f64).into(), (0. as f64).into()),
|
||
gpui::Point::new((10. as f64).into(), (80. as f64).into()),
|
||
)),
|
||
..Default::default()
|
||
},
|
||
|cx| cx.build_view(|cx| build_editor(buffer.clone(), cx)),
|
||
)
|
||
});
|
||
|
||
let is_still_following = Rc::new(RefCell::new(true));
|
||
let follower_edit_event_count = Rc::new(RefCell::new(0));
|
||
let pending_update = Rc::new(RefCell::new(None));
|
||
follower.update(cx, {
|
||
let update = pending_update.clone();
|
||
let is_still_following = is_still_following.clone();
|
||
let follower_edit_event_count = follower_edit_event_count.clone();
|
||
|_, cx| {
|
||
cx.subscribe(
|
||
&leader.root_view(cx).unwrap(),
|
||
move |_, leader, event, cx| {
|
||
leader
|
||
.read(cx)
|
||
.add_event_to_update_proto(event, &mut *update.borrow_mut(), cx);
|
||
},
|
||
)
|
||
.detach();
|
||
|
||
cx.subscribe(
|
||
&follower.root_view(cx).unwrap(),
|
||
move |_, _, event: &EditorEvent, cx| {
|
||
if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
|
||
*is_still_following.borrow_mut() = false;
|
||
}
|
||
|
||
if let EditorEvent::BufferEdited = event {
|
||
*follower_edit_event_count.borrow_mut() += 1;
|
||
}
|
||
},
|
||
)
|
||
.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(&project, pending_update.borrow_mut().take().unwrap(), cx)
|
||
})
|
||
.unwrap()
|
||
.await
|
||
.unwrap();
|
||
follower.update(cx, |follower, cx| {
|
||
assert_eq!(follower.selections.ranges(cx), vec![1..1]);
|
||
});
|
||
assert_eq!(*is_still_following.borrow(), true);
|
||
assert_eq!(*follower_edit_event_count.borrow(), 0);
|
||
|
||
// Update the scroll position only
|
||
leader.update(cx, |leader, cx| {
|
||
leader.set_scroll_position(gpui::Point::new(1.5, 3.5), cx);
|
||
});
|
||
follower
|
||
.update(cx, |follower, cx| {
|
||
follower.apply_update_proto(&project, pending_update.borrow_mut().take().unwrap(), cx)
|
||
})
|
||
.unwrap()
|
||
.await
|
||
.unwrap();
|
||
assert_eq!(
|
||
follower
|
||
.update(cx, |follower, cx| follower.scroll_position(cx))
|
||
.unwrap(),
|
||
gpui::Point::new(1.5, 3.5)
|
||
);
|
||
assert_eq!(*is_still_following.borrow(), true);
|
||
assert_eq!(*follower_edit_event_count.borrow(), 0);
|
||
|
||
// Update the selections and scroll position. The follower's scroll position is updated
|
||
// via autoscroll, not via the leader's exact 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(gpui::Point::new(1.5, 3.5), cx);
|
||
});
|
||
follower
|
||
.update(cx, |follower, cx| {
|
||
follower.apply_update_proto(&project, pending_update.borrow_mut().take().unwrap(), cx)
|
||
})
|
||
.unwrap()
|
||
.await
|
||
.unwrap();
|
||
follower.update(cx, |follower, cx| {
|
||
assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
|
||
assert_eq!(follower.selections.ranges(cx), vec![0..0]);
|
||
});
|
||
assert_eq!(*is_still_following.borrow(), true);
|
||
|
||
// 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(&project, pending_update.borrow_mut().take().unwrap(), cx)
|
||
})
|
||
.unwrap()
|
||
.await
|
||
.unwrap();
|
||
follower.update(cx, |follower, cx| {
|
||
assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]);
|
||
});
|
||
assert_eq!(*is_still_following.borrow(), true);
|
||
|
||
// 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(&project, pending_update.borrow_mut().take().unwrap(), cx)
|
||
})
|
||
.unwrap()
|
||
.await
|
||
.unwrap();
|
||
follower.update(cx, |follower, cx| {
|
||
assert_eq!(follower.selections.ranges(cx), vec![0..2]);
|
||
});
|
||
|
||
// Scrolling locally breaks the follow
|
||
follower.update(cx, |follower, cx| {
|
||
let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
|
||
follower.set_scroll_anchor(
|
||
ScrollAnchor {
|
||
anchor: top_anchor,
|
||
offset: gpui::Point::new(0.0, 0.5),
|
||
},
|
||
cx,
|
||
);
|
||
});
|
||
assert_eq!(*is_still_following.borrow(), false);
|
||
}
|
||
|
||
#[gpui::test]
|
||
async fn test_following_with_multiple_excerpts(cx: &mut gpui::TestAppContext) {
|
||
init_test(cx, |_| {});
|
||
|
||
let fs = FakeFs::new(cx.executor());
|
||
let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
|
||
let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
|
||
let pane = workspace
|
||
.update(cx, |workspace, _| workspace.active_pane().clone())
|
||
.unwrap();
|
||
|
||
let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
|
||
|
||
let leader = pane.update(cx, |_, cx| {
|
||
let multibuffer = cx.build_model(|_| MultiBuffer::new(0));
|
||
cx.build_view(|cx| build_editor(multibuffer.clone(), cx))
|
||
});
|
||
|
||
// Start following the editor when it has no excerpts.
|
||
let mut state_message = leader.update(cx, |leader, cx| leader.to_state_proto(cx));
|
||
let follower_1 = cx
|
||
.update_window(*workspace.deref(), |_, cx| {
|
||
Editor::from_state_proto(
|
||
pane.clone(),
|
||
workspace.root_view(cx).unwrap(),
|
||
ViewId {
|
||
creator: Default::default(),
|
||
id: 0,
|
||
},
|
||
&mut state_message,
|
||
cx,
|
||
)
|
||
})
|
||
.unwrap()
|
||
.unwrap()
|
||
.await
|
||
.unwrap();
|
||
|
||
let update_message = Rc::new(RefCell::new(None));
|
||
follower_1.update(cx, {
|
||
let update = update_message.clone();
|
||
|_, cx| {
|
||
cx.subscribe(&leader, move |_, leader, event, cx| {
|
||
leader
|
||
.read(cx)
|
||
.add_event_to_update_proto(event, &mut *update.borrow_mut(), cx);
|
||
})
|
||
.detach();
|
||
}
|
||
});
|
||
|
||
let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
|
||
(
|
||
project
|
||
.create_buffer("abc\ndef\nghi\njkl\n", None, cx)
|
||
.unwrap(),
|
||
project
|
||
.create_buffer("mno\npqr\nstu\nvwx\n", None, cx)
|
||
.unwrap(),
|
||
)
|
||
});
|
||
|
||
// Insert some excerpts.
|
||
leader.update(cx, |leader, cx| {
|
||
leader.buffer.update(cx, |multibuffer, cx| {
|
||
let excerpt_ids = multibuffer.push_excerpts(
|
||
buffer_1.clone(),
|
||
[
|
||
ExcerptRange {
|
||
context: 1..6,
|
||
primary: None,
|
||
},
|
||
ExcerptRange {
|
||
context: 12..15,
|
||
primary: None,
|
||
},
|
||
ExcerptRange {
|
||
context: 0..3,
|
||
primary: None,
|
||
},
|
||
],
|
||
cx,
|
||
);
|
||
multibuffer.insert_excerpts_after(
|
||
excerpt_ids[0],
|
||
buffer_2.clone(),
|
||
[
|
||
ExcerptRange {
|
||
context: 8..12,
|
||
primary: None,
|
||
},
|
||
ExcerptRange {
|
||
context: 0..6,
|
||
primary: None,
|
||
},
|
||
],
|
||
cx,
|
||
);
|
||
});
|
||
});
|
||
|
||
// Apply the update of adding the excerpts.
|
||
follower_1
|
||
.update(cx, |follower, cx| {
|
||
follower.apply_update_proto(&project, update_message.borrow().clone().unwrap(), cx)
|
||
})
|
||
.await
|
||
.unwrap();
|
||
assert_eq!(
|
||
follower_1.update(cx, |editor, cx| editor.text(cx)),
|
||
leader.update(cx, |editor, cx| editor.text(cx))
|
||
);
|
||
update_message.borrow_mut().take();
|
||
|
||
// Start following separately after it already has excerpts.
|
||
let mut state_message = leader.update(cx, |leader, cx| leader.to_state_proto(cx));
|
||
let follower_2 = cx
|
||
.update_window(*workspace.deref(), |_, cx| {
|
||
Editor::from_state_proto(
|
||
pane.clone(),
|
||
workspace.root_view(cx).unwrap().clone(),
|
||
ViewId {
|
||
creator: Default::default(),
|
||
id: 0,
|
||
},
|
||
&mut state_message,
|
||
cx,
|
||
)
|
||
})
|
||
.unwrap()
|
||
.unwrap()
|
||
.await
|
||
.unwrap();
|
||
assert_eq!(
|
||
follower_2.update(cx, |editor, cx| editor.text(cx)),
|
||
leader.update(cx, |editor, cx| editor.text(cx))
|
||
);
|
||
|
||
// Remove some excerpts.
|
||
leader.update(cx, |leader, cx| {
|
||
leader.buffer.update(cx, |multibuffer, cx| {
|
||
let excerpt_ids = multibuffer.excerpt_ids();
|
||
multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
|
||
multibuffer.remove_excerpts([excerpt_ids[0]], cx);
|
||
});
|
||
});
|
||
|
||
// Apply the update of removing the excerpts.
|
||
follower_1
|
||
.update(cx, |follower, cx| {
|
||
follower.apply_update_proto(&project, update_message.borrow().clone().unwrap(), cx)
|
||
})
|
||
.await
|
||
.unwrap();
|
||
follower_2
|
||
.update(cx, |follower, cx| {
|
||
follower.apply_update_proto(&project, update_message.borrow().clone().unwrap(), cx)
|
||
})
|
||
.await
|
||
.unwrap();
|
||
update_message.borrow_mut().take();
|
||
assert_eq!(
|
||
follower_1.update(cx, |editor, cx| editor.text(cx)),
|
||
leader.update(cx, |editor, cx| editor.text(cx))
|
||
);
|
||
}
|
||
|
||
#[gpui::test]
|
||
async fn go_to_prev_overlapping_diagnostic(
|
||
executor: BackgroundExecutor,
|
||
cx: &mut gpui::TestAppContext,
|
||
) {
|
||
init_test(cx, |_| {});
|
||
|
||
let mut cx = EditorTestContext::new(cx).await;
|
||
let project = cx.update_editor(|editor, _| editor.project.clone().unwrap());
|
||
|
||
cx.set_state(indoc! {"
|
||
ˇfn func(abc def: i32) -> u32 {
|
||
}
|
||
"});
|
||
|
||
cx.update(|cx| {
|
||
project.update(cx, |project, cx| {
|
||
project
|
||
.update_diagnostics(
|
||
LanguageServerId(0),
|
||
lsp::PublishDiagnosticsParams {
|
||
uri: lsp::Url::from_file_path("/root/file").unwrap(),
|
||
version: None,
|
||
diagnostics: vec![
|
||
lsp::Diagnostic {
|
||
range: lsp::Range::new(
|
||
lsp::Position::new(0, 11),
|
||
lsp::Position::new(0, 12),
|
||
),
|
||
severity: Some(lsp::DiagnosticSeverity::ERROR),
|
||
..Default::default()
|
||
},
|
||
lsp::Diagnostic {
|
||
range: lsp::Range::new(
|
||
lsp::Position::new(0, 12),
|
||
lsp::Position::new(0, 15),
|
||
),
|
||
severity: Some(lsp::DiagnosticSeverity::ERROR),
|
||
..Default::default()
|
||
},
|
||
lsp::Diagnostic {
|
||
range: lsp::Range::new(
|
||
lsp::Position::new(0, 25),
|
||
lsp::Position::new(0, 28),
|
||
),
|
||
severity: Some(lsp::DiagnosticSeverity::ERROR),
|
||
..Default::default()
|
||
},
|
||
],
|
||
},
|
||
&[],
|
||
cx,
|
||
)
|
||
.unwrap()
|
||
});
|
||
});
|
||
|
||
executor.run_until_parked();
|
||
|
||
cx.update_editor(|editor, cx| {
|
||
editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, cx);
|
||
});
|
||
|
||
cx.assert_editor_state(indoc! {"
|
||
fn func(abc def: i32) -> ˇu32 {
|
||
}
|
||
"});
|
||
|
||
cx.update_editor(|editor, cx| {
|
||
editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, cx);
|
||
});
|
||
|
||
cx.assert_editor_state(indoc! {"
|
||
fn func(abc ˇdef: i32) -> u32 {
|
||
}
|
||
"});
|
||
|
||
cx.update_editor(|editor, cx| {
|
||
editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, cx);
|
||
});
|
||
|
||
cx.assert_editor_state(indoc! {"
|
||
fn func(abcˇ def: i32) -> u32 {
|
||
}
|
||
"});
|
||
|
||
cx.update_editor(|editor, cx| {
|
||
editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, cx);
|
||
});
|
||
|
||
cx.assert_editor_state(indoc! {"
|
||
fn func(abc def: i32) -> ˇu32 {
|
||
}
|
||
"});
|
||
}
|
||
|
||
#[gpui::test]
|
||
async fn go_to_hunk(executor: BackgroundExecutor, cx: &mut gpui::TestAppContext) {
|
||
init_test(cx, |_| {});
|
||
|
||
let mut cx = EditorTestContext::new(cx).await;
|
||
|
||
let diff_base = r#"
|
||
use some::mod;
|
||
|
||
const A: u32 = 42;
|
||
|
||
fn main() {
|
||
println!("hello");
|
||
|
||
println!("world");
|
||
}
|
||
"#
|
||
.unindent();
|
||
|
||
// Edits are modified, removed, modified, added
|
||
cx.set_state(
|
||
&r#"
|
||
use some::modified;
|
||
|
||
ˇ
|
||
fn main() {
|
||
println!("hello there");
|
||
|
||
println!("around the");
|
||
println!("world");
|
||
}
|
||
"#
|
||
.unindent(),
|
||
);
|
||
|
||
cx.set_diff_base(Some(&diff_base));
|
||
executor.run_until_parked();
|
||
|
||
cx.update_editor(|editor, cx| {
|
||
//Wrap around the bottom of the buffer
|
||
for _ in 0..3 {
|
||
editor.go_to_hunk(&GoToHunk, cx);
|
||
}
|
||
});
|
||
|
||
cx.assert_editor_state(
|
||
&r#"
|
||
ˇuse some::modified;
|
||
|
||
|
||
fn main() {
|
||
println!("hello there");
|
||
|
||
println!("around the");
|
||
println!("world");
|
||
}
|
||
"#
|
||
.unindent(),
|
||
);
|
||
|
||
cx.update_editor(|editor, cx| {
|
||
//Wrap around the top of the buffer
|
||
for _ in 0..2 {
|
||
editor.go_to_prev_hunk(&GoToPrevHunk, cx);
|
||
}
|
||
});
|
||
|
||
cx.assert_editor_state(
|
||
&r#"
|
||
use some::modified;
|
||
|
||
|
||
fn main() {
|
||
ˇ println!("hello there");
|
||
|
||
println!("around the");
|
||
println!("world");
|
||
}
|
||
"#
|
||
.unindent(),
|
||
);
|
||
|
||
cx.update_editor(|editor, cx| {
|
||
editor.go_to_prev_hunk(&GoToPrevHunk, cx);
|
||
});
|
||
|
||
cx.assert_editor_state(
|
||
&r#"
|
||
use some::modified;
|
||
|
||
ˇ
|
||
fn main() {
|
||
println!("hello there");
|
||
|
||
println!("around the");
|
||
println!("world");
|
||
}
|
||
"#
|
||
.unindent(),
|
||
);
|
||
|
||
cx.update_editor(|editor, cx| {
|
||
for _ in 0..3 {
|
||
editor.go_to_prev_hunk(&GoToPrevHunk, cx);
|
||
}
|
||
});
|
||
|
||
cx.assert_editor_state(
|
||
&r#"
|
||
use some::modified;
|
||
|
||
|
||
fn main() {
|
||
ˇ println!("hello there");
|
||
|
||
println!("around the");
|
||
println!("world");
|
||
}
|
||
"#
|
||
.unindent(),
|
||
);
|
||
|
||
cx.update_editor(|editor, cx| {
|
||
editor.fold(&Fold, cx);
|
||
|
||
//Make sure that the fold only gets one hunk
|
||
for _ in 0..4 {
|
||
editor.go_to_hunk(&GoToHunk, cx);
|
||
}
|
||
});
|
||
|
||
cx.assert_editor_state(
|
||
&r#"
|
||
ˇuse some::modified;
|
||
|
||
|
||
fn main() {
|
||
println!("hello there");
|
||
|
||
println!("around the");
|
||
println!("world");
|
||
}
|
||
"#
|
||
.unindent(),
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn test_split_words() {
|
||
fn split<'a>(text: &'a str) -> Vec<&'a str> {
|
||
split_words(text).collect()
|
||
}
|
||
|
||
assert_eq!(split("HelloWorld"), &["Hello", "World"]);
|
||
assert_eq!(split("hello_world"), &["hello_", "world"]);
|
||
assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
|
||
assert_eq!(split("Hello_World"), &["Hello_", "World"]);
|
||
assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
|
||
assert_eq!(split("helloworld"), &["helloworld"]);
|
||
}
|
||
|
||
#[gpui::test]
|
||
async fn test_move_to_enclosing_bracket(cx: &mut gpui::TestAppContext) {
|
||
init_test(cx, |_| {});
|
||
|
||
let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
|
||
let mut assert = |before, after| {
|
||
let _state_context = cx.set_state(before);
|
||
cx.update_editor(|editor, cx| {
|
||
editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, cx)
|
||
});
|
||
cx.assert_editor_state(after);
|
||
};
|
||
|
||
// Outside bracket jumps to outside of matching bracket
|
||
assert("console.logˇ(var);", "console.log(var)ˇ;");
|
||
assert("console.log(var)ˇ;", "console.logˇ(var);");
|
||
|
||
// Inside bracket jumps to inside of matching bracket
|
||
assert("console.log(ˇvar);", "console.log(varˇ);");
|
||
assert("console.log(varˇ);", "console.log(ˇvar);");
|
||
|
||
// When outside a bracket and inside, favor jumping to the inside bracket
|
||
assert(
|
||
"console.log('foo', [1, 2, 3]ˇ);",
|
||
"console.log(ˇ'foo', [1, 2, 3]);",
|
||
);
|
||
assert(
|
||
"console.log(ˇ'foo', [1, 2, 3]);",
|
||
"console.log('foo', [1, 2, 3]ˇ);",
|
||
);
|
||
|
||
// Bias forward if two options are equally likely
|
||
assert(
|
||
"let result = curried_fun()ˇ();",
|
||
"let result = curried_fun()()ˇ;",
|
||
);
|
||
|
||
// If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
|
||
assert(
|
||
indoc! {"
|
||
function test() {
|
||
console.log('test')ˇ
|
||
}"},
|
||
indoc! {"
|
||
function test() {
|
||
console.logˇ('test')
|
||
}"},
|
||
);
|
||
}
|
||
|
||
// todo!(completions)
|
||
#[gpui::test(iterations = 10)]
|
||
async fn test_copilot(executor: BackgroundExecutor, cx: &mut gpui::TestAppContext) {
|
||
// flaky
|
||
init_test(cx, |_| {});
|
||
|
||
let (copilot, copilot_lsp) = Copilot::fake(cx);
|
||
cx.update(|cx| cx.set_global(copilot));
|
||
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;
|
||
|
||
// When inserting, ensure autocompletion is favored over Copilot suggestions.
|
||
cx.set_state(indoc! {"
|
||
oneˇ
|
||
two
|
||
three
|
||
"});
|
||
cx.simulate_keystroke(".");
|
||
let _ = handle_completion_request(
|
||
&mut cx,
|
||
indoc! {"
|
||
one.|<>
|
||
two
|
||
three
|
||
"},
|
||
vec!["completion_a", "completion_b"],
|
||
);
|
||
handle_copilot_completion_request(
|
||
&copilot_lsp,
|
||
vec![copilot::request::Completion {
|
||
text: "one.copilot1".into(),
|
||
range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)),
|
||
..Default::default()
|
||
}],
|
||
vec![],
|
||
);
|
||
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
|
||
cx.update_editor(|editor, cx| {
|
||
assert!(editor.context_menu_visible());
|
||
assert!(!editor.has_active_copilot_suggestion(cx));
|
||
|
||
// Confirming a completion inserts it and hides the context menu, without showing
|
||
// the copilot suggestion afterwards.
|
||
editor
|
||
.confirm_completion(&Default::default(), cx)
|
||
.unwrap()
|
||
.detach();
|
||
assert!(!editor.context_menu_visible());
|
||
assert!(!editor.has_active_copilot_suggestion(cx));
|
||
assert_eq!(editor.text(cx), "one.completion_a\ntwo\nthree\n");
|
||
assert_eq!(editor.display_text(cx), "one.completion_a\ntwo\nthree\n");
|
||
});
|
||
|
||
// Ensure Copilot suggestions are shown right away if no autocompletion is available.
|
||
cx.set_state(indoc! {"
|
||
oneˇ
|
||
two
|
||
three
|
||
"});
|
||
cx.simulate_keystroke(".");
|
||
let _ = handle_completion_request(
|
||
&mut cx,
|
||
indoc! {"
|
||
one.|<>
|
||
two
|
||
three
|
||
"},
|
||
vec![],
|
||
);
|
||
handle_copilot_completion_request(
|
||
&copilot_lsp,
|
||
vec![copilot::request::Completion {
|
||
text: "one.copilot1".into(),
|
||
range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)),
|
||
..Default::default()
|
||
}],
|
||
vec![],
|
||
);
|
||
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
|
||
cx.update_editor(|editor, cx| {
|
||
assert!(!editor.context_menu_visible());
|
||
assert!(editor.has_active_copilot_suggestion(cx));
|
||
assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n");
|
||
assert_eq!(editor.text(cx), "one.\ntwo\nthree\n");
|
||
});
|
||
|
||
// Reset editor, and ensure autocompletion is still favored over Copilot suggestions.
|
||
cx.set_state(indoc! {"
|
||
oneˇ
|
||
two
|
||
three
|
||
"});
|
||
cx.simulate_keystroke(".");
|
||
let _ = handle_completion_request(
|
||
&mut cx,
|
||
indoc! {"
|
||
one.|<>
|
||
two
|
||
three
|
||
"},
|
||
vec!["completion_a", "completion_b"],
|
||
);
|
||
handle_copilot_completion_request(
|
||
&copilot_lsp,
|
||
vec![copilot::request::Completion {
|
||
text: "one.copilot1".into(),
|
||
range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)),
|
||
..Default::default()
|
||
}],
|
||
vec![],
|
||
);
|
||
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
|
||
cx.update_editor(|editor, cx| {
|
||
assert!(editor.context_menu_visible());
|
||
assert!(!editor.has_active_copilot_suggestion(cx));
|
||
|
||
// When hiding the context menu, the Copilot suggestion becomes visible.
|
||
editor.hide_context_menu(cx);
|
||
assert!(!editor.context_menu_visible());
|
||
assert!(editor.has_active_copilot_suggestion(cx));
|
||
assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n");
|
||
assert_eq!(editor.text(cx), "one.\ntwo\nthree\n");
|
||
});
|
||
|
||
// Ensure existing completion is interpolated when inserting again.
|
||
cx.simulate_keystroke("c");
|
||
executor.run_until_parked();
|
||
cx.update_editor(|editor, cx| {
|
||
assert!(!editor.context_menu_visible());
|
||
assert!(editor.has_active_copilot_suggestion(cx));
|
||
assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n");
|
||
assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n");
|
||
});
|
||
|
||
// After debouncing, new Copilot completions should be requested.
|
||
handle_copilot_completion_request(
|
||
&copilot_lsp,
|
||
vec![copilot::request::Completion {
|
||
text: "one.copilot2".into(),
|
||
range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 5)),
|
||
..Default::default()
|
||
}],
|
||
vec![],
|
||
);
|
||
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
|
||
cx.update_editor(|editor, cx| {
|
||
assert!(!editor.context_menu_visible());
|
||
assert!(editor.has_active_copilot_suggestion(cx));
|
||
assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n");
|
||
assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n");
|
||
|
||
// Canceling should remove the active Copilot suggestion.
|
||
editor.cancel(&Default::default(), cx);
|
||
assert!(!editor.has_active_copilot_suggestion(cx));
|
||
assert_eq!(editor.display_text(cx), "one.c\ntwo\nthree\n");
|
||
assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n");
|
||
|
||
// After canceling, tabbing shouldn't insert the previously shown suggestion.
|
||
editor.tab(&Default::default(), cx);
|
||
assert!(!editor.has_active_copilot_suggestion(cx));
|
||
assert_eq!(editor.display_text(cx), "one.c \ntwo\nthree\n");
|
||
assert_eq!(editor.text(cx), "one.c \ntwo\nthree\n");
|
||
|
||
// When undoing the previously active suggestion is shown again.
|
||
editor.undo(&Default::default(), cx);
|
||
assert!(editor.has_active_copilot_suggestion(cx));
|
||
assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n");
|
||
assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n");
|
||
});
|
||
|
||
// If an edit occurs outside of this editor, the suggestion is still correctly interpolated.
|
||
cx.update_buffer(|buffer, cx| buffer.edit([(5..5, "o")], None, cx));
|
||
cx.update_editor(|editor, cx| {
|
||
assert!(editor.has_active_copilot_suggestion(cx));
|
||
assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n");
|
||
assert_eq!(editor.text(cx), "one.co\ntwo\nthree\n");
|
||
|
||
// Tabbing when there is an active suggestion inserts it.
|
||
editor.tab(&Default::default(), cx);
|
||
assert!(!editor.has_active_copilot_suggestion(cx));
|
||
assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n");
|
||
assert_eq!(editor.text(cx), "one.copilot2\ntwo\nthree\n");
|
||
|
||
// When undoing the previously active suggestion is shown again.
|
||
editor.undo(&Default::default(), cx);
|
||
assert!(editor.has_active_copilot_suggestion(cx));
|
||
assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n");
|
||
assert_eq!(editor.text(cx), "one.co\ntwo\nthree\n");
|
||
|
||
// Hide suggestion.
|
||
editor.cancel(&Default::default(), cx);
|
||
assert!(!editor.has_active_copilot_suggestion(cx));
|
||
assert_eq!(editor.display_text(cx), "one.co\ntwo\nthree\n");
|
||
assert_eq!(editor.text(cx), "one.co\ntwo\nthree\n");
|
||
});
|
||
|
||
// If an edit occurs outside of this editor but no suggestion is being shown,
|
||
// we won't make it visible.
|
||
cx.update_buffer(|buffer, cx| buffer.edit([(6..6, "p")], None, cx));
|
||
cx.update_editor(|editor, cx| {
|
||
assert!(!editor.has_active_copilot_suggestion(cx));
|
||
assert_eq!(editor.display_text(cx), "one.cop\ntwo\nthree\n");
|
||
assert_eq!(editor.text(cx), "one.cop\ntwo\nthree\n");
|
||
});
|
||
|
||
// Reset the editor to verify how suggestions behave when tabbing on leading indentation.
|
||
cx.update_editor(|editor, cx| {
|
||
editor.set_text("fn foo() {\n \n}", cx);
|
||
editor.change_selections(None, cx, |s| {
|
||
s.select_ranges([Point::new(1, 2)..Point::new(1, 2)])
|
||
});
|
||
});
|
||
handle_copilot_completion_request(
|
||
&copilot_lsp,
|
||
vec![copilot::request::Completion {
|
||
text: " let x = 4;".into(),
|
||
range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 2)),
|
||
..Default::default()
|
||
}],
|
||
vec![],
|
||
);
|
||
|
||
cx.update_editor(|editor, cx| editor.next_copilot_suggestion(&Default::default(), cx));
|
||
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
|
||
cx.update_editor(|editor, cx| {
|
||
assert!(editor.has_active_copilot_suggestion(cx));
|
||
assert_eq!(editor.display_text(cx), "fn foo() {\n let x = 4;\n}");
|
||
assert_eq!(editor.text(cx), "fn foo() {\n \n}");
|
||
|
||
// Tabbing inside of leading whitespace inserts indentation without accepting the suggestion.
|
||
editor.tab(&Default::default(), cx);
|
||
assert!(editor.has_active_copilot_suggestion(cx));
|
||
assert_eq!(editor.text(cx), "fn foo() {\n \n}");
|
||
assert_eq!(editor.display_text(cx), "fn foo() {\n let x = 4;\n}");
|
||
|
||
// Tabbing again accepts the suggestion.
|
||
editor.tab(&Default::default(), cx);
|
||
assert!(!editor.has_active_copilot_suggestion(cx));
|
||
assert_eq!(editor.text(cx), "fn foo() {\n let x = 4;\n}");
|
||
assert_eq!(editor.display_text(cx), "fn foo() {\n let x = 4;\n}");
|
||
});
|
||
}
|
||
|
||
#[gpui::test]
|
||
async fn test_copilot_completion_invalidation(
|
||
executor: BackgroundExecutor,
|
||
cx: &mut gpui::TestAppContext,
|
||
) {
|
||
init_test(cx, |_| {});
|
||
|
||
let (copilot, copilot_lsp) = Copilot::fake(cx);
|
||
cx.update(|cx| cx.set_global(copilot));
|
||
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
|
||
twˇ
|
||
three
|
||
"});
|
||
|
||
handle_copilot_completion_request(
|
||
&copilot_lsp,
|
||
vec![copilot::request::Completion {
|
||
text: "two.foo()".into(),
|
||
range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 2)),
|
||
..Default::default()
|
||
}],
|
||
vec![],
|
||
);
|
||
cx.update_editor(|editor, cx| editor.next_copilot_suggestion(&Default::default(), cx));
|
||
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
|
||
cx.update_editor(|editor, cx| {
|
||
assert!(editor.has_active_copilot_suggestion(cx));
|
||
assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n");
|
||
assert_eq!(editor.text(cx), "one\ntw\nthree\n");
|
||
|
||
editor.backspace(&Default::default(), cx);
|
||
assert!(editor.has_active_copilot_suggestion(cx));
|
||
assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n");
|
||
assert_eq!(editor.text(cx), "one\nt\nthree\n");
|
||
|
||
editor.backspace(&Default::default(), cx);
|
||
assert!(editor.has_active_copilot_suggestion(cx));
|
||
assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n");
|
||
assert_eq!(editor.text(cx), "one\n\nthree\n");
|
||
|
||
// Deleting across the original suggestion range invalidates it.
|
||
editor.backspace(&Default::default(), cx);
|
||
assert!(!editor.has_active_copilot_suggestion(cx));
|
||
assert_eq!(editor.display_text(cx), "one\nthree\n");
|
||
assert_eq!(editor.text(cx), "one\nthree\n");
|
||
|
||
// Undoing the deletion restores the suggestion.
|
||
editor.undo(&Default::default(), cx);
|
||
assert!(editor.has_active_copilot_suggestion(cx));
|
||
assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n");
|
||
assert_eq!(editor.text(cx), "one\n\nthree\n");
|
||
});
|
||
}
|
||
|
||
#[gpui::test]
|
||
async fn test_copilot_multibuffer(executor: BackgroundExecutor, cx: &mut gpui::TestAppContext) {
|
||
init_test(cx, |_| {});
|
||
|
||
let (copilot, copilot_lsp) = Copilot::fake(cx);
|
||
cx.update(|cx| cx.set_global(copilot));
|
||
|
||
let buffer_1 = cx.build_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), "a = 1\nb = 2\n"));
|
||
let buffer_2 = cx.build_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), "c = 3\nd = 4\n"));
|
||
let multibuffer = cx.build_model(|cx| {
|
||
let mut multibuffer = MultiBuffer::new(0);
|
||
multibuffer.push_excerpts(
|
||
buffer_1.clone(),
|
||
[ExcerptRange {
|
||
context: Point::new(0, 0)..Point::new(2, 0),
|
||
primary: None,
|
||
}],
|
||
cx,
|
||
);
|
||
multibuffer.push_excerpts(
|
||
buffer_2.clone(),
|
||
[ExcerptRange {
|
||
context: Point::new(0, 0)..Point::new(2, 0),
|
||
primary: None,
|
||
}],
|
||
cx,
|
||
);
|
||
multibuffer
|
||
});
|
||
let editor = cx.add_window(|cx| build_editor(multibuffer, cx));
|
||
|
||
handle_copilot_completion_request(
|
||
&copilot_lsp,
|
||
vec![copilot::request::Completion {
|
||
text: "b = 2 + a".into(),
|
||
range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 5)),
|
||
..Default::default()
|
||
}],
|
||
vec![],
|
||
);
|
||
editor.update(cx, |editor, cx| {
|
||
// Ensure copilot suggestions are shown for the first excerpt.
|
||
editor.change_selections(None, cx, |s| {
|
||
s.select_ranges([Point::new(1, 5)..Point::new(1, 5)])
|
||
});
|
||
editor.next_copilot_suggestion(&Default::default(), cx);
|
||
});
|
||
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
|
||
editor.update(cx, |editor, cx| {
|
||
assert!(editor.has_active_copilot_suggestion(cx));
|
||
assert_eq!(
|
||
editor.display_text(cx),
|
||
"\n\na = 1\nb = 2 + a\n\n\n\nc = 3\nd = 4\n"
|
||
);
|
||
assert_eq!(editor.text(cx), "a = 1\nb = 2\n\nc = 3\nd = 4\n");
|
||
});
|
||
|
||
handle_copilot_completion_request(
|
||
&copilot_lsp,
|
||
vec![copilot::request::Completion {
|
||
text: "d = 4 + c".into(),
|
||
range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 6)),
|
||
..Default::default()
|
||
}],
|
||
vec![],
|
||
);
|
||
editor.update(cx, |editor, cx| {
|
||
// Move to another excerpt, ensuring the suggestion gets cleared.
|
||
editor.change_selections(None, cx, |s| {
|
||
s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
|
||
});
|
||
assert!(!editor.has_active_copilot_suggestion(cx));
|
||
assert_eq!(
|
||
editor.display_text(cx),
|
||
"\n\na = 1\nb = 2\n\n\n\nc = 3\nd = 4\n"
|
||
);
|
||
assert_eq!(editor.text(cx), "a = 1\nb = 2\n\nc = 3\nd = 4\n");
|
||
|
||
// Type a character, ensuring we don't even try to interpolate the previous suggestion.
|
||
editor.handle_input(" ", cx);
|
||
assert!(!editor.has_active_copilot_suggestion(cx));
|
||
assert_eq!(
|
||
editor.display_text(cx),
|
||
"\n\na = 1\nb = 2\n\n\n\nc = 3\nd = 4 \n"
|
||
);
|
||
assert_eq!(editor.text(cx), "a = 1\nb = 2\n\nc = 3\nd = 4 \n");
|
||
});
|
||
|
||
// Ensure the new suggestion is displayed when the debounce timeout expires.
|
||
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
|
||
editor.update(cx, |editor, cx| {
|
||
assert!(editor.has_active_copilot_suggestion(cx));
|
||
assert_eq!(
|
||
editor.display_text(cx),
|
||
"\n\na = 1\nb = 2\n\n\n\nc = 3\nd = 4 + c\n"
|
||
);
|
||
assert_eq!(editor.text(cx), "a = 1\nb = 2\n\nc = 3\nd = 4 \n");
|
||
});
|
||
}
|
||
|
||
#[gpui::test]
|
||
async fn test_copilot_disabled_globs(executor: BackgroundExecutor, cx: &mut gpui::TestAppContext) {
|
||
init_test(cx, |settings| {
|
||
settings
|
||
.copilot
|
||
.get_or_insert(Default::default())
|
||
.disabled_globs = Some(vec![".env*".to_string()]);
|
||
});
|
||
|
||
let (copilot, copilot_lsp) = Copilot::fake(cx);
|
||
cx.update(|cx| cx.set_global(copilot));
|
||
|
||
let fs = FakeFs::new(cx.executor());
|
||
fs.insert_tree(
|
||
"/test",
|
||
json!({
|
||
".env": "SECRET=something\n",
|
||
"README.md": "hello\n"
|
||
}),
|
||
)
|
||
.await;
|
||
let project = Project::test(fs, ["/test".as_ref()], cx).await;
|
||
|
||
let private_buffer = project
|
||
.update(cx, |project, cx| {
|
||
project.open_local_buffer("/test/.env", cx)
|
||
})
|
||
.await
|
||
.unwrap();
|
||
let public_buffer = project
|
||
.update(cx, |project, cx| {
|
||
project.open_local_buffer("/test/README.md", cx)
|
||
})
|
||
.await
|
||
.unwrap();
|
||
|
||
let multibuffer = cx.build_model(|cx| {
|
||
let mut multibuffer = MultiBuffer::new(0);
|
||
multibuffer.push_excerpts(
|
||
private_buffer.clone(),
|
||
[ExcerptRange {
|
||
context: Point::new(0, 0)..Point::new(1, 0),
|
||
primary: None,
|
||
}],
|
||
cx,
|
||
);
|
||
multibuffer.push_excerpts(
|
||
public_buffer.clone(),
|
||
[ExcerptRange {
|
||
context: Point::new(0, 0)..Point::new(1, 0),
|
||
primary: None,
|
||
}],
|
||
cx,
|
||
);
|
||
multibuffer
|
||
});
|
||
let editor = cx.add_window(|cx| build_editor(multibuffer, cx));
|
||
|
||
let mut copilot_requests = copilot_lsp
|
||
.handle_request::<copilot::request::GetCompletions, _, _>(move |_params, _cx| async move {
|
||
Ok(copilot::request::GetCompletionsResult {
|
||
completions: vec![copilot::request::Completion {
|
||
text: "next line".into(),
|
||
range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 0)),
|
||
..Default::default()
|
||
}],
|
||
})
|
||
});
|
||
|
||
editor.update(cx, |editor, cx| {
|
||
editor.change_selections(None, cx, |selections| {
|
||
selections.select_ranges([Point::new(0, 0)..Point::new(0, 0)])
|
||
});
|
||
editor.next_copilot_suggestion(&Default::default(), cx);
|
||
});
|
||
|
||
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
|
||
assert!(copilot_requests.try_next().is_err());
|
||
|
||
editor.update(cx, |editor, cx| {
|
||
editor.change_selections(None, cx, |s| {
|
||
s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
|
||
});
|
||
editor.next_copilot_suggestion(&Default::default(), cx);
|
||
});
|
||
|
||
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
|
||
assert!(copilot_requests.try_next().is_ok());
|
||
}
|
||
|
||
#[gpui::test]
|
||
async fn test_on_type_formatting_not_triggered(cx: &mut gpui::TestAppContext) {
|
||
init_test(cx, |_| {});
|
||
|
||
let mut language = Language::new(
|
||
LanguageConfig {
|
||
name: "Rust".into(),
|
||
path_suffixes: vec!["rs".to_string()],
|
||
brackets: BracketPairConfig {
|
||
pairs: vec![BracketPair {
|
||
start: "{".to_string(),
|
||
end: "}".to_string(),
|
||
close: true,
|
||
newline: true,
|
||
}],
|
||
disabled_scopes_by_bracket_ix: Vec::new(),
|
||
},
|
||
..Default::default()
|
||
},
|
||
Some(tree_sitter_rust::language()),
|
||
);
|
||
let mut fake_servers = language
|
||
.set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
|
||
capabilities: lsp::ServerCapabilities {
|
||
document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
|
||
first_trigger_character: "{".to_string(),
|
||
more_trigger_character: None,
|
||
}),
|
||
..Default::default()
|
||
},
|
||
..Default::default()
|
||
}))
|
||
.await;
|
||
|
||
let fs = FakeFs::new(cx.executor());
|
||
fs.insert_tree(
|
||
"/a",
|
||
json!({
|
||
"main.rs": "fn main() { let a = 5; }",
|
||
"other.rs": "// Test file",
|
||
}),
|
||
)
|
||
.await;
|
||
let project = Project::test(fs, ["/a".as_ref()], cx).await;
|
||
project.update(cx, |project, _| project.languages().add(Arc::new(language)));
|
||
let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
|
||
|
||
let cx = &mut VisualTestContext::from_window(*workspace, cx);
|
||
|
||
let worktree_id = workspace
|
||
.update(cx, |workspace, cx| {
|
||
workspace.project().update(cx, |project, cx| {
|
||
project.worktrees().next().unwrap().read(cx).id()
|
||
})
|
||
})
|
||
.unwrap();
|
||
|
||
let buffer = project
|
||
.update(cx, |project, cx| {
|
||
project.open_local_buffer("/a/main.rs", cx)
|
||
})
|
||
.await
|
||
.unwrap();
|
||
cx.executor().run_until_parked();
|
||
cx.executor().start_waiting();
|
||
let fake_server = fake_servers.next().await.unwrap();
|
||
let editor_handle = workspace
|
||
.update(cx, |workspace, cx| {
|
||
workspace.open_path((worktree_id, "main.rs"), None, true, cx)
|
||
})
|
||
.unwrap()
|
||
.await
|
||
.unwrap()
|
||
.downcast::<Editor>()
|
||
.unwrap();
|
||
|
||
fake_server.handle_request::<lsp::request::OnTypeFormatting, _, _>(|params, _| async move {
|
||
assert_eq!(
|
||
params.text_document_position.text_document.uri,
|
||
lsp::Url::from_file_path("/a/main.rs").unwrap(),
|
||
);
|
||
assert_eq!(
|
||
params.text_document_position.position,
|
||
lsp::Position::new(0, 21),
|
||
);
|
||
|
||
Ok(Some(vec![lsp::TextEdit {
|
||
new_text: "]".to_string(),
|
||
range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
|
||
}]))
|
||
});
|
||
|
||
editor_handle.update(cx, |editor, cx| {
|
||
editor.focus(cx);
|
||
editor.change_selections(None, cx, |s| {
|
||
s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
|
||
});
|
||
editor.handle_input("{", cx);
|
||
});
|
||
|
||
cx.executor().run_until_parked();
|
||
|
||
buffer.update(cx, |buffer, _| {
|
||
assert_eq!(
|
||
buffer.text(),
|
||
"fn main() { let a = {5}; }",
|
||
"No extra braces from on type formatting should appear in the buffer"
|
||
)
|
||
});
|
||
}
|
||
|
||
#[gpui::test]
|
||
async fn test_language_server_restart_due_to_settings_change(cx: &mut gpui::TestAppContext) {
|
||
init_test(cx, |_| {});
|
||
|
||
let language_name: Arc<str> = "Rust".into();
|
||
let mut language = Language::new(
|
||
LanguageConfig {
|
||
name: Arc::clone(&language_name),
|
||
path_suffixes: vec!["rs".to_string()],
|
||
..Default::default()
|
||
},
|
||
Some(tree_sitter_rust::language()),
|
||
);
|
||
|
||
let server_restarts = Arc::new(AtomicUsize::new(0));
|
||
let closure_restarts = Arc::clone(&server_restarts);
|
||
let language_server_name = "test language server";
|
||
let mut fake_servers = language
|
||
.set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
|
||
name: language_server_name,
|
||
initialization_options: Some(json!({
|
||
"testOptionValue": true
|
||
})),
|
||
initializer: Some(Box::new(move |fake_server| {
|
||
let task_restarts = Arc::clone(&closure_restarts);
|
||
fake_server.handle_request::<lsp::request::Shutdown, _, _>(move |_, _| {
|
||
task_restarts.fetch_add(1, atomic::Ordering::Release);
|
||
futures::future::ready(Ok(()))
|
||
});
|
||
})),
|
||
..Default::default()
|
||
}))
|
||
.await;
|
||
|
||
let fs = FakeFs::new(cx.executor());
|
||
fs.insert_tree(
|
||
"/a",
|
||
json!({
|
||
"main.rs": "fn main() { let a = 5; }",
|
||
"other.rs": "// Test file",
|
||
}),
|
||
)
|
||
.await;
|
||
let project = Project::test(fs, ["/a".as_ref()], cx).await;
|
||
project.update(cx, |project, _| project.languages().add(Arc::new(language)));
|
||
let _window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
|
||
let _buffer = project
|
||
.update(cx, |project, cx| {
|
||
project.open_local_buffer("/a/main.rs", cx)
|
||
})
|
||
.await
|
||
.unwrap();
|
||
let _fake_server = fake_servers.next().await.unwrap();
|
||
update_test_language_settings(cx, |language_settings| {
|
||
language_settings.languages.insert(
|
||
Arc::clone(&language_name),
|
||
LanguageSettingsContent {
|
||
tab_size: NonZeroU32::new(8),
|
||
..Default::default()
|
||
},
|
||
);
|
||
});
|
||
cx.executor().run_until_parked();
|
||
assert_eq!(
|
||
server_restarts.load(atomic::Ordering::Acquire),
|
||
0,
|
||
"Should not restart LSP server on an unrelated change"
|
||
);
|
||
|
||
update_test_project_settings(cx, |project_settings| {
|
||
project_settings.lsp.insert(
|
||
"Some other server name".into(),
|
||
LspSettings {
|
||
initialization_options: Some(json!({
|
||
"some other init value": false
|
||
})),
|
||
},
|
||
);
|
||
});
|
||
cx.executor().run_until_parked();
|
||
assert_eq!(
|
||
server_restarts.load(atomic::Ordering::Acquire),
|
||
0,
|
||
"Should not restart LSP server on an unrelated LSP settings change"
|
||
);
|
||
|
||
update_test_project_settings(cx, |project_settings| {
|
||
project_settings.lsp.insert(
|
||
language_server_name.into(),
|
||
LspSettings {
|
||
initialization_options: Some(json!({
|
||
"anotherInitValue": false
|
||
})),
|
||
},
|
||
);
|
||
});
|
||
cx.executor().run_until_parked();
|
||
assert_eq!(
|
||
server_restarts.load(atomic::Ordering::Acquire),
|
||
1,
|
||
"Should restart LSP server on a related LSP settings change"
|
||
);
|
||
|
||
update_test_project_settings(cx, |project_settings| {
|
||
project_settings.lsp.insert(
|
||
language_server_name.into(),
|
||
LspSettings {
|
||
initialization_options: Some(json!({
|
||
"anotherInitValue": false
|
||
})),
|
||
},
|
||
);
|
||
});
|
||
cx.executor().run_until_parked();
|
||
assert_eq!(
|
||
server_restarts.load(atomic::Ordering::Acquire),
|
||
1,
|
||
"Should not restart LSP server on a related LSP settings change that is the same"
|
||
);
|
||
|
||
update_test_project_settings(cx, |project_settings| {
|
||
project_settings.lsp.insert(
|
||
language_server_name.into(),
|
||
LspSettings {
|
||
initialization_options: None,
|
||
},
|
||
);
|
||
});
|
||
cx.executor().run_until_parked();
|
||
assert_eq!(
|
||
server_restarts.load(atomic::Ordering::Acquire),
|
||
2,
|
||
"Should restart LSP server on another related LSP settings change"
|
||
);
|
||
}
|
||
|
||
#[gpui::test]
|
||
async fn test_completions_with_additional_edits(cx: &mut gpui::TestAppContext) {
|
||
init_test(cx, |_| {});
|
||
|
||
let mut cx = EditorLspTestContext::new_rust(
|
||
lsp::ServerCapabilities {
|
||
completion_provider: Some(lsp::CompletionOptions {
|
||
trigger_characters: Some(vec![".".to_string()]),
|
||
resolve_provider: Some(true),
|
||
..Default::default()
|
||
}),
|
||
..Default::default()
|
||
},
|
||
cx,
|
||
)
|
||
.await;
|
||
|
||
cx.set_state(indoc! {"fn main() { let a = 2ˇ; }"});
|
||
cx.simulate_keystroke(".");
|
||
let completion_item = lsp::CompletionItem {
|
||
label: "some".into(),
|
||
kind: Some(lsp::CompletionItemKind::SNIPPET),
|
||
detail: Some("Wrap the expression in an `Option::Some`".to_string()),
|
||
documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
|
||
kind: lsp::MarkupKind::Markdown,
|
||
value: "```rust\nSome(2)\n```".to_string(),
|
||
})),
|
||
deprecated: Some(false),
|
||
sort_text: Some("fffffff2".to_string()),
|
||
filter_text: Some("some".to_string()),
|
||
insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
|
||
text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
|
||
range: lsp::Range {
|
||
start: lsp::Position {
|
||
line: 0,
|
||
character: 22,
|
||
},
|
||
end: lsp::Position {
|
||
line: 0,
|
||
character: 22,
|
||
},
|
||
},
|
||
new_text: "Some(2)".to_string(),
|
||
})),
|
||
additional_text_edits: Some(vec![lsp::TextEdit {
|
||
range: lsp::Range {
|
||
start: lsp::Position {
|
||
line: 0,
|
||
character: 20,
|
||
},
|
||
end: lsp::Position {
|
||
line: 0,
|
||
character: 22,
|
||
},
|
||
},
|
||
new_text: "".to_string(),
|
||
}]),
|
||
..Default::default()
|
||
};
|
||
|
||
let closure_completion_item = completion_item.clone();
|
||
let mut request = cx.handle_request::<lsp::request::Completion, _, _>(move |_, _, _| {
|
||
let task_completion_item = closure_completion_item.clone();
|
||
async move {
|
||
Ok(Some(lsp::CompletionResponse::Array(vec![
|
||
task_completion_item,
|
||
])))
|
||
}
|
||
});
|
||
|
||
request.next().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! {"fn main() { let a = 2.Some(2)ˇ; }"});
|
||
|
||
cx.handle_request::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
|
||
let task_completion_item = completion_item.clone();
|
||
async move { Ok(task_completion_item) }
|
||
})
|
||
.next()
|
||
.await
|
||
.unwrap();
|
||
apply_additional_edits.await.unwrap();
|
||
cx.assert_editor_state(indoc! {"fn main() { let a = Some(2)ˇ; }"});
|
||
}
|
||
|
||
#[gpui::test]
|
||
async fn test_completions_in_languages_with_extra_word_characters(cx: &mut gpui::TestAppContext) {
|
||
init_test(cx, |_| {});
|
||
|
||
let mut cx = EditorLspTestContext::new(
|
||
Language::new(
|
||
LanguageConfig {
|
||
path_suffixes: vec!["jsx".into()],
|
||
overrides: [(
|
||
"element".into(),
|
||
LanguageConfigOverride {
|
||
word_characters: Override::Set(['-'].into_iter().collect()),
|
||
..Default::default()
|
||
},
|
||
)]
|
||
.into_iter()
|
||
.collect(),
|
||
..Default::default()
|
||
},
|
||
Some(tree_sitter_typescript::language_tsx()),
|
||
)
|
||
.with_override_query("(jsx_self_closing_element) @element")
|
||
.unwrap(),
|
||
lsp::ServerCapabilities {
|
||
completion_provider: Some(lsp::CompletionOptions {
|
||
trigger_characters: Some(vec![":".to_string()]),
|
||
..Default::default()
|
||
}),
|
||
..Default::default()
|
||
},
|
||
cx,
|
||
)
|
||
.await;
|
||
|
||
cx.lsp
|
||
.handle_request::<lsp::request::Completion, _, _>(move |_, _| async move {
|
||
Ok(Some(lsp::CompletionResponse::Array(vec![
|
||
lsp::CompletionItem {
|
||
label: "bg-blue".into(),
|
||
..Default::default()
|
||
},
|
||
lsp::CompletionItem {
|
||
label: "bg-red".into(),
|
||
..Default::default()
|
||
},
|
||
lsp::CompletionItem {
|
||
label: "bg-yellow".into(),
|
||
..Default::default()
|
||
},
|
||
])))
|
||
});
|
||
|
||
cx.set_state(r#"<p class="bgˇ" />"#);
|
||
|
||
// Trigger completion when typing a dash, because the dash is an extra
|
||
// word character in the 'element' scope, which contains the cursor.
|
||
cx.simulate_keystroke("-");
|
||
cx.executor().run_until_parked();
|
||
cx.update_editor(|editor, _| {
|
||
if let Some(ContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() {
|
||
assert_eq!(
|
||
menu.matches.iter().map(|m| &m.string).collect::<Vec<_>>(),
|
||
&["bg-red", "bg-blue", "bg-yellow"]
|
||
);
|
||
} else {
|
||
panic!("expected completion menu to be open");
|
||
}
|
||
});
|
||
|
||
cx.simulate_keystroke("l");
|
||
cx.executor().run_until_parked();
|
||
cx.update_editor(|editor, _| {
|
||
if let Some(ContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() {
|
||
assert_eq!(
|
||
menu.matches.iter().map(|m| &m.string).collect::<Vec<_>>(),
|
||
&["bg-blue", "bg-yellow"]
|
||
);
|
||
} else {
|
||
panic!("expected completion menu to be open");
|
||
}
|
||
});
|
||
|
||
// When filtering completions, consider the character after the '-' to
|
||
// be the start of a subword.
|
||
cx.set_state(r#"<p class="yelˇ" />"#);
|
||
cx.simulate_keystroke("l");
|
||
cx.executor().run_until_parked();
|
||
cx.update_editor(|editor, _| {
|
||
if let Some(ContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() {
|
||
assert_eq!(
|
||
menu.matches.iter().map(|m| &m.string).collect::<Vec<_>>(),
|
||
&["bg-yellow"]
|
||
);
|
||
} else {
|
||
panic!("expected completion menu to be open");
|
||
}
|
||
});
|
||
}
|
||
|
||
#[gpui::test]
|
||
async fn test_document_format_with_prettier(cx: &mut gpui::TestAppContext) {
|
||
init_test(cx, |settings| {
|
||
settings.defaults.formatter = Some(language_settings::Formatter::Prettier)
|
||
});
|
||
|
||
let mut language = Language::new(
|
||
LanguageConfig {
|
||
name: "Rust".into(),
|
||
path_suffixes: vec!["rs".to_string()],
|
||
prettier_parser_name: Some("test_parser".to_string()),
|
||
..Default::default()
|
||
},
|
||
Some(tree_sitter_rust::language()),
|
||
);
|
||
|
||
let test_plugin = "test_plugin";
|
||
let _ = language
|
||
.set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
|
||
prettier_plugins: vec![test_plugin],
|
||
..Default::default()
|
||
}))
|
||
.await;
|
||
|
||
let fs = FakeFs::new(cx.executor());
|
||
fs.insert_file("/file.rs", Default::default()).await;
|
||
|
||
let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
|
||
let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
|
||
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();
|
||
|
||
let buffer_text = "one\ntwo\nthree\n";
|
||
let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
|
||
let (editor, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
|
||
editor.update(cx, |editor, cx| editor.set_text(buffer_text, cx));
|
||
|
||
editor
|
||
.update(cx, |editor, cx| {
|
||
editor.perform_format(project.clone(), FormatTrigger::Manual, cx)
|
||
})
|
||
.unwrap()
|
||
.await;
|
||
assert_eq!(
|
||
editor.update(cx, |editor, cx| editor.text(cx)),
|
||
buffer_text.to_string() + prettier_format_suffix,
|
||
"Test prettier formatting was not applied to the original buffer text",
|
||
);
|
||
|
||
update_test_language_settings(cx, |settings| {
|
||
settings.defaults.formatter = Some(language_settings::Formatter::Auto)
|
||
});
|
||
let format = editor.update(cx, |editor, cx| {
|
||
editor.perform_format(project.clone(), FormatTrigger::Manual, cx)
|
||
});
|
||
format.await.unwrap();
|
||
assert_eq!(
|
||
editor.update(cx, |editor, cx| editor.text(cx)),
|
||
buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
|
||
"Autoformatting (via test prettier) was not applied to the original buffer text",
|
||
);
|
||
}
|
||
|
||
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
|
||
);
|
||
}
|
||
|
||
/// 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
|
||
pub fn handle_completion_request<'a>(
|
||
cx: &mut EditorLspTestContext<'a>,
|
||
marked_string: &str,
|
||
completions: Vec<&'static str>,
|
||
) -> impl Future<Output = ()> {
|
||
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());
|
||
|
||
let mut request = 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(),
|
||
)))
|
||
}
|
||
});
|
||
|
||
async move {
|
||
request.next().await;
|
||
}
|
||
}
|
||
|
||
fn handle_resolve_completion_request<'a>(
|
||
cx: &mut EditorLspTestContext<'a>,
|
||
edits: Option<Vec<(&'static str, &'static str)>>,
|
||
) -> impl Future<Output = ()> {
|
||
let edits = edits.map(|edits| {
|
||
edits
|
||
.iter()
|
||
.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());
|
||
lsp::TextEdit::new(replace_range, new_text.to_string())
|
||
})
|
||
.collect::<Vec<_>>()
|
||
});
|
||
|
||
let mut request =
|
||
cx.handle_request::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
|
||
let edits = edits.clone();
|
||
async move {
|
||
Ok(lsp::CompletionItem {
|
||
additional_text_edits: edits,
|
||
..Default::default()
|
||
})
|
||
}
|
||
});
|
||
|
||
async move {
|
||
request.next().await;
|
||
}
|
||
}
|
||
|
||
fn handle_copilot_completion_request(
|
||
lsp: &lsp::FakeLanguageServer,
|
||
completions: Vec<copilot::request::Completion>,
|
||
completions_cycling: Vec<copilot::request::Completion>,
|
||
) {
|
||
lsp.handle_request::<copilot::request::GetCompletions, _, _>(move |_params, _cx| {
|
||
let completions = completions.clone();
|
||
async move {
|
||
Ok(copilot::request::GetCompletionsResult {
|
||
completions: completions.clone(),
|
||
})
|
||
}
|
||
});
|
||
lsp.handle_request::<copilot::request::GetCompletionsCycling, _, _>(move |_params, _cx| {
|
||
let completions_cycling = completions_cycling.clone();
|
||
async move {
|
||
Ok(copilot::request::GetCompletionsResult {
|
||
completions: completions_cycling.clone(),
|
||
})
|
||
}
|
||
});
|
||
}
|
||
|
||
pub(crate) fn update_test_language_settings(
|
||
cx: &mut TestAppContext,
|
||
f: impl Fn(&mut AllLanguageSettingsContent),
|
||
) {
|
||
cx.update(|cx| {
|
||
cx.update_global(|store: &mut SettingsStore, cx| {
|
||
store.update_user_settings::<AllLanguageSettings>(cx, f);
|
||
});
|
||
});
|
||
}
|
||
|
||
pub(crate) fn update_test_project_settings(
|
||
cx: &mut TestAppContext,
|
||
f: impl Fn(&mut ProjectSettings),
|
||
) {
|
||
cx.update(|cx| {
|
||
cx.update_global(|store: &mut SettingsStore, cx| {
|
||
store.update_user_settings::<ProjectSettings>(cx, f);
|
||
});
|
||
});
|
||
}
|
||
|
||
pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
|
||
cx.update(|cx| {
|
||
let store = SettingsStore::test(cx);
|
||
cx.set_global(store);
|
||
theme::init(theme::LoadThemes::JustBase, cx);
|
||
client::init_settings(cx);
|
||
language::init(cx);
|
||
Project::init_settings(cx);
|
||
workspace::init_settings(cx);
|
||
crate::init(cx);
|
||
});
|
||
|
||
update_test_language_settings(cx, f);
|
||
}
|