4528 lines
155 KiB
Rust
4528 lines
155 KiB
Rust
use anyhow::Error;
|
||
use itertools::Itertools;
|
||
use pathfinder_geometry::vector::vec2f;
|
||
use settings::ToggleableSetting;
|
||
use unindent::Unindent;
|
||
use warpui::color::ColorU;
|
||
use warpui::platform::WindowStyle;
|
||
use warpui::text_layout::TextFrame;
|
||
use warpui::windowing::WindowManager;
|
||
use warpui::{AddSingletonModel, App, UpdateModel, UpdateView};
|
||
|
||
use super::*;
|
||
use crate::auth::AuthStateProvider;
|
||
use crate::editor::soft_wrap::FrameLayouts;
|
||
use crate::editor::tests::sample_text;
|
||
use crate::editor::EditorView;
|
||
use crate::report_if_error;
|
||
use crate::server::server_api::team::MockTeamClient;
|
||
use crate::server::server_api::workspace::MockWorkspaceClient;
|
||
use crate::settings_view::keybindings::KeybindingChangedNotifier;
|
||
use crate::test_util::settings::initialize_settings_for_tests;
|
||
use crate::workspace::sync_inputs::SyncedInputState;
|
||
use crate::workspace::ToastStack;
|
||
use crate::workspaces::user_workspaces::UserWorkspaces;
|
||
|
||
impl EditorView {
|
||
fn selected_ranges(&self, app: &AppContext) -> Vec<Range<DisplayPoint>> {
|
||
self.editor_model
|
||
.as_ref(app)
|
||
.local_selections_intersecting_range(DisplayPoint::zero()..self.max_point(app), app)
|
||
.map(|(_, range)| range)
|
||
.collect()
|
||
}
|
||
}
|
||
|
||
fn initialize_app(app: &mut App) {
|
||
initialize_settings_for_tests(app);
|
||
|
||
app.update(|ctx| {
|
||
AppEditorSettings::handle(ctx).update(ctx, |editor_settings, ctx| {
|
||
let _ = editor_settings.vim_mode.set_value(true, ctx);
|
||
})
|
||
});
|
||
|
||
app.add_singleton_model(|_| Appearance::mock());
|
||
app.add_singleton_model(|_| ToastStack);
|
||
|
||
app.add_singleton_model(|_ctx| SyncedInputState::mock());
|
||
app.add_singleton_model(|_ctx| VimRegisters::new());
|
||
app.add_singleton_model(|_| KeybindingChangedNotifier::mock());
|
||
app.add_singleton_model(|_| AuthStateProvider::new_for_test());
|
||
#[cfg(feature = "voice_input")]
|
||
app.add_singleton_model(voice_input::VoiceInput::new);
|
||
|
||
let team_client_mock = Arc::new(MockTeamClient::new());
|
||
let workspace_client_mock = Arc::new(MockWorkspaceClient::new());
|
||
app.add_singleton_model(|ctx| {
|
||
UserWorkspaces::mock(
|
||
team_client_mock.clone(),
|
||
workspace_client_mock.clone(),
|
||
vec![],
|
||
ctx,
|
||
)
|
||
});
|
||
}
|
||
|
||
#[test]
|
||
fn test_selection_with_mouse() {
|
||
App::test((), |mut app| async move {
|
||
initialize_app(&mut app);
|
||
|
||
let (_, buffer_view) = app.add_window(WindowStyle::NotStealFocus, |ctx| {
|
||
EditorView::new_with_base_text(
|
||
"aaaaaa\nbbbbbb\ncccccc\ndddddd\n",
|
||
Default::default(),
|
||
ctx,
|
||
)
|
||
});
|
||
|
||
buffer_view.update(&mut app, |view, ctx| {
|
||
view.begin_selection(DisplayPoint::new(2, 2), Default::default(), false, ctx);
|
||
});
|
||
|
||
buffer_view.read(&app, |view, app| {
|
||
assert_eq!(
|
||
view.selected_ranges(app),
|
||
[DisplayPoint::new(2, 2)..DisplayPoint::new(2, 2)]
|
||
);
|
||
});
|
||
|
||
buffer_view.update(&mut app, |view, ctx| {
|
||
view.update_selection(DisplayPoint::new(3, 3), Vector2F::zero(), ctx);
|
||
});
|
||
|
||
buffer_view.read(&app, |view, app| {
|
||
assert_eq!(
|
||
view.selected_ranges(app),
|
||
[DisplayPoint::new(2, 2)..DisplayPoint::new(3, 3)]
|
||
);
|
||
});
|
||
|
||
buffer_view.update(&mut app, |view, ctx| {
|
||
view.update_selection(DisplayPoint::new(1, 1), Vector2F::zero(), ctx);
|
||
});
|
||
|
||
buffer_view.read(&app, |view, app| {
|
||
assert_eq!(
|
||
view.selected_ranges(app),
|
||
[DisplayPoint::new(2, 2)..DisplayPoint::new(1, 1)]
|
||
);
|
||
});
|
||
|
||
buffer_view.update(&mut app, |view, ctx| {
|
||
view.end_selection(ctx);
|
||
view.update_selection(DisplayPoint::new(3, 3), Vector2F::zero(), ctx);
|
||
});
|
||
|
||
buffer_view.read(&app, |view, app| {
|
||
assert_eq!(
|
||
view.selected_ranges(app),
|
||
[DisplayPoint::new(2, 2)..DisplayPoint::new(1, 1)]
|
||
);
|
||
});
|
||
|
||
buffer_view.update(&mut app, |view, ctx| {
|
||
view.begin_selection(DisplayPoint::new(3, 3), Default::default(), true, ctx);
|
||
view.update_selection(DisplayPoint::new(0, 0), Vector2F::zero(), ctx);
|
||
});
|
||
|
||
buffer_view.read(&app, |view, app| {
|
||
assert_eq!(
|
||
view.selected_ranges(app),
|
||
[
|
||
DisplayPoint::new(2, 2)..DisplayPoint::new(1, 1),
|
||
DisplayPoint::new(3, 3)..DisplayPoint::new(0, 0)
|
||
]
|
||
);
|
||
});
|
||
|
||
buffer_view.update(&mut app, |view, ctx| {
|
||
view.end_selection(ctx);
|
||
});
|
||
|
||
buffer_view.read(&app, |view, app| {
|
||
assert_eq!(
|
||
view.selected_ranges(app),
|
||
[DisplayPoint::new(3, 3)..DisplayPoint::new(0, 0)]
|
||
);
|
||
});
|
||
|
||
buffer_view.update(&mut app, |view, ctx| {
|
||
view.extend_selection(DisplayPoint::new(1, 1), Vector2F::zero(), ctx);
|
||
});
|
||
|
||
buffer_view.read(&app, |view, app| {
|
||
assert_eq!(
|
||
view.selected_ranges(app),
|
||
[DisplayPoint::new(3, 3)..DisplayPoint::new(1, 1)]
|
||
);
|
||
});
|
||
|
||
buffer_view.update(&mut app, |view, ctx| {
|
||
view.extend_selection(DisplayPoint::new(3, 3), Vector2F::zero(), ctx);
|
||
});
|
||
|
||
buffer_view.read(&app, |view, app| {
|
||
assert_eq!(
|
||
view.selected_ranges(app),
|
||
[DisplayPoint::new(3, 3)..DisplayPoint::new(3, 3)]
|
||
);
|
||
});
|
||
|
||
buffer_view.update(&mut app, |view, ctx| {
|
||
view.extend_selection(DisplayPoint::new(2, 2), Vector2F::zero(), ctx);
|
||
});
|
||
|
||
buffer_view.read(&app, |view, app| {
|
||
assert_eq!(
|
||
view.selected_ranges(app),
|
||
[DisplayPoint::new(3, 3)..DisplayPoint::new(2, 2)]
|
||
);
|
||
});
|
||
|
||
buffer_view.update(&mut app, |view, ctx| {
|
||
view.begin_selection(DisplayPoint::new(0, 0), Default::default(), true, ctx);
|
||
view.update_selection(DisplayPoint::new(1, 1), Vector2F::zero(), ctx);
|
||
view.end_selection(ctx);
|
||
});
|
||
|
||
buffer_view.read(&app, |view, app| {
|
||
assert_eq!(
|
||
view.selected_ranges(app),
|
||
[
|
||
DisplayPoint::new(0, 0)..DisplayPoint::new(1, 1),
|
||
DisplayPoint::new(3, 3)..DisplayPoint::new(2, 2)
|
||
]
|
||
);
|
||
});
|
||
|
||
buffer_view.update(&mut app, |view, ctx| {
|
||
view.extend_selection(DisplayPoint::new(1, 2), Vector2F::zero(), ctx);
|
||
});
|
||
|
||
buffer_view.read(&app, |view, app| {
|
||
assert_eq!(
|
||
view.selected_ranges(app),
|
||
[DisplayPoint::new(0, 0)..DisplayPoint::new(1, 2)]
|
||
);
|
||
});
|
||
});
|
||
}
|
||
|
||
// Tests that there's always at least one selection in the `selections` vec, even during the
|
||
// case of a single pending selection
|
||
#[test]
|
||
fn test_pending_selection_always_one_selection() {
|
||
App::test((), |mut app| async move {
|
||
initialize_app(&mut app);
|
||
|
||
let (_, buffer_view) = app.add_window(WindowStyle::NotStealFocus, |ctx| {
|
||
EditorView::new_with_base_text(
|
||
"aaaaaa\nbbbbbb\ncccccc\ndddddd\n",
|
||
Default::default(),
|
||
ctx,
|
||
)
|
||
});
|
||
|
||
buffer_view.update(&mut app, |view, ctx| {
|
||
view.begin_selection(DisplayPoint::new(2, 2), Default::default(), false, ctx);
|
||
});
|
||
|
||
buffer_view.read(&app, |view, app| {
|
||
assert_eq!(
|
||
view.selected_ranges(app),
|
||
[DisplayPoint::new(2, 2)..DisplayPoint::new(2, 2)]
|
||
);
|
||
});
|
||
|
||
// Call a fn on the view that assumes there's at least one selection.
|
||
buffer_view.update(&mut app, |view, ctx| view.indent(ctx));
|
||
|
||
buffer_view.read(&app, |view, app| {
|
||
assert_eq!(
|
||
view.selected_ranges(app),
|
||
[DisplayPoint::new(2, 6)..DisplayPoint::new(2, 6)]
|
||
);
|
||
});
|
||
});
|
||
}
|
||
|
||
#[test]
|
||
fn test_selected_text() {
|
||
App::test((), |mut app| async move {
|
||
initialize_app(&mut app);
|
||
|
||
app.add_window(WindowStyle::NotStealFocus, |ctx| {
|
||
let mut editor =
|
||
EditorView::new_with_base_text(sample_text(6, 6), Default::default(), ctx);
|
||
editor
|
||
.select_ranges(
|
||
vec![
|
||
DisplayPoint::new(2, 2)..DisplayPoint::new(3, 3),
|
||
DisplayPoint::new(4, 4)..DisplayPoint::new(5, 5),
|
||
],
|
||
ctx,
|
||
)
|
||
.unwrap();
|
||
assert_eq!(editor.selected_text(ctx), "cccc\nddd\nee\nfffff");
|
||
editor
|
||
});
|
||
});
|
||
}
|
||
|
||
#[test]
|
||
fn test_select_line() {
|
||
App::test((), |mut app| async move {
|
||
initialize_app(&mut app);
|
||
|
||
app.add_window(WindowStyle::NotStealFocus, |ctx| {
|
||
let mut editor =
|
||
EditorView::new_with_base_text(sample_text(6, 6), Default::default(), ctx);
|
||
editor.select_line(&DisplayPoint::new(2, 2), ctx);
|
||
assert_eq!(editor.selected_text(ctx), "cccccc");
|
||
editor.model().update(ctx, |model, ctx| {
|
||
for was_selecting in [true, false] {
|
||
let a11y = model.delta_for_a11y(
|
||
ByteOffset::from(14)..ByteOffset::from(14),
|
||
was_selecting,
|
||
ctx,
|
||
);
|
||
assert_eq!(a11y.value, "cccccc");
|
||
assert_eq!(a11y.help, Some(", selected".to_string()));
|
||
}
|
||
});
|
||
editor
|
||
});
|
||
});
|
||
}
|
||
|
||
#[test]
|
||
fn test_selection_mode_lines() {
|
||
App::test((), |mut app| async move {
|
||
initialize_app(&mut app);
|
||
|
||
let (_, buffer_view) = app.add_window(WindowStyle::NotStealFocus, |ctx| {
|
||
EditorView::new_with_base_text(
|
||
"aaaaaa\nbbbbbb\ncccccc\ndddddd\n",
|
||
Default::default(),
|
||
ctx,
|
||
)
|
||
});
|
||
|
||
// Set starting selection and selection mode as line
|
||
buffer_view.update(&mut app, |view, ctx| {
|
||
view.select_line(&DisplayPoint::new(2, 2), ctx);
|
||
});
|
||
|
||
buffer_view.read(&app, |view, app| {
|
||
assert_eq!(
|
||
view.selected_ranges(app),
|
||
[DisplayPoint::new(2, 0)..DisplayPoint::new(2, 6)]
|
||
);
|
||
});
|
||
|
||
// Select next line
|
||
buffer_view.update(&mut app, |view, ctx| {
|
||
view.update_selection(DisplayPoint::new(3, 3), Vector2F::zero(), ctx);
|
||
});
|
||
|
||
buffer_view.read(&app, |view, app| {
|
||
assert_eq!(
|
||
view.selected_ranges(app),
|
||
[DisplayPoint::new(2, 0)..DisplayPoint::new(3, 6)]
|
||
);
|
||
});
|
||
|
||
// Select previous line
|
||
buffer_view.update(&mut app, |view, ctx| {
|
||
view.update_selection(DisplayPoint::new(1, 1), Vector2F::zero(), ctx);
|
||
});
|
||
|
||
buffer_view.read(&app, |view, app| {
|
||
assert_eq!(
|
||
view.selected_ranges(app),
|
||
[DisplayPoint::new(2, 6)..DisplayPoint::new(1, 0)]
|
||
);
|
||
});
|
||
|
||
// Select within starting selection
|
||
buffer_view.update(&mut app, |view, ctx| {
|
||
view.update_selection(DisplayPoint::new(2, 4), Vector2F::zero(), ctx);
|
||
});
|
||
|
||
buffer_view.read(&app, |view, app| {
|
||
assert_eq!(
|
||
view.selected_ranges(app),
|
||
[DisplayPoint::new(2, 0)..DisplayPoint::new(2, 6)]
|
||
);
|
||
});
|
||
|
||
buffer_view.update(&mut app, |view, ctx| {
|
||
view.end_selection(ctx);
|
||
view.update_selection(DisplayPoint::new(3, 3), Vector2F::zero(), ctx);
|
||
});
|
||
});
|
||
}
|
||
|
||
#[test]
|
||
fn test_clear_selections() -> Result<()> {
|
||
App::test((), |mut app| async move {
|
||
initialize_app(&mut app);
|
||
|
||
let (_, view) = app.add_window(WindowStyle::NotStealFocus, |ctx| {
|
||
EditorView::new_with_base_text(sample_text(6, 6), Default::default(), ctx)
|
||
});
|
||
|
||
view.update(&mut app, |view, ctx| {
|
||
view.select_ranges(
|
||
vec![
|
||
DisplayPoint::new(0, 1)..DisplayPoint::new(0, 2),
|
||
DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3),
|
||
DisplayPoint::new(1, 4)..DisplayPoint::new(1, 4),
|
||
DisplayPoint::new(3, 5)..DisplayPoint::new(3, 5),
|
||
],
|
||
ctx,
|
||
)?;
|
||
view.change_selections(ctx, |editor_model, ctx| {
|
||
editor_model.clear_selections(ctx);
|
||
|
||
let a11y_label = editor_model
|
||
.delta_for_a11y(ByteOffset::from(0)..ByteOffset::from(0), true, ctx)
|
||
.value;
|
||
assert_eq!(a11y_label, "Unselected");
|
||
});
|
||
assert_eq!(
|
||
view.selected_ranges(ctx),
|
||
vec![DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1)]
|
||
);
|
||
Ok::<(), Error>(())
|
||
})?;
|
||
|
||
Ok(())
|
||
})
|
||
}
|
||
|
||
#[test]
|
||
fn test_select_word() {
|
||
App::test((), |mut app| async move {
|
||
initialize_app(&mut app);
|
||
|
||
// Disable smart select and clear the word-char allowlist so basic
|
||
// word-boundary rules apply.
|
||
app.update(|ctx| {
|
||
SemanticSelection::handle(ctx).update(ctx, |selection, ctx| {
|
||
report_if_error!(selection.smart_select_enabled.toggle_and_save_value(ctx));
|
||
report_if_error!(selection.word_char_allowlist.set_value(String::new(), ctx));
|
||
assert!(!selection.smart_select_enabled());
|
||
});
|
||
});
|
||
|
||
app.add_window(WindowStyle::NotStealFocus, |ctx| {
|
||
let mut editor =
|
||
EditorView::new_with_base_text("word w0rd\\ word%%", Default::default(), ctx);
|
||
editor.select_word(&DisplayPoint::new(0, 4), ctx);
|
||
editor.model().update(ctx, |editor_model, ctx| {
|
||
let a11y = editor_model.delta_for_a11y(
|
||
ByteOffset::from(0)..ByteOffset::from(0),
|
||
false,
|
||
ctx,
|
||
);
|
||
assert_eq!(a11y.value, "word");
|
||
assert_eq!(a11y.help, Some(", selected".to_string()));
|
||
});
|
||
assert_eq!(editor.selected_text(ctx), "word");
|
||
editor.select_word(&DisplayPoint::new(0, 5), ctx);
|
||
editor.model().update(ctx, |editor_model, ctx| {
|
||
let a11y = editor_model.delta_for_a11y(
|
||
ByteOffset::from(5)..ByteOffset::from(0),
|
||
true,
|
||
ctx,
|
||
);
|
||
assert_eq!(a11y.value, "w0rd");
|
||
assert_eq!(a11y.help, Some(", selected".to_string()));
|
||
});
|
||
assert_eq!(editor.selected_text(ctx), "w0rd");
|
||
editor.select_word(&DisplayPoint::new(0, 11), ctx);
|
||
editor.model().update(ctx, |editor_model, ctx| {
|
||
let a11y = editor_model.delta_for_a11y(
|
||
ByteOffset::from(11)..ByteOffset::from(0),
|
||
true,
|
||
ctx,
|
||
);
|
||
assert_eq!(a11y.value, "word");
|
||
assert_eq!(a11y.help, Some(", selected".to_string()));
|
||
});
|
||
assert_eq!(editor.selected_text(ctx), "word");
|
||
editor
|
||
});
|
||
});
|
||
}
|
||
|
||
#[test]
|
||
fn test_select_word_with_smart_select() {
|
||
App::test((), |mut app| async move {
|
||
initialize_app(&mut app);
|
||
|
||
// Smart select is enabled by default; just verify
|
||
app.update(|ctx| {
|
||
SemanticSelection::handle(ctx).update(ctx, |selection, _ctx| {
|
||
assert!(selection.smart_select_enabled());
|
||
});
|
||
});
|
||
|
||
app.add_window(WindowStyle::NotStealFocus, |ctx| {
|
||
let mut editor = EditorView::new_with_base_text(
|
||
"word ~/.warp/themes/foo-bar.yaml thing",
|
||
Default::default(),
|
||
ctx,
|
||
);
|
||
editor.select_word(&DisplayPoint::new(0, 8), ctx);
|
||
assert_eq!(editor.selected_text(ctx), "~/.warp/themes/foo-bar.yaml");
|
||
editor
|
||
});
|
||
});
|
||
}
|
||
|
||
#[test]
|
||
fn test_select_word_with_custom_boundaries() {
|
||
App::test((), |mut app| async move {
|
||
initialize_app(&mut app);
|
||
|
||
app.update(|ctx| {
|
||
SemanticSelection::handle(ctx).update(ctx, |selection, ctx| {
|
||
// Disable smart select so custom word boundaries take effect
|
||
report_if_error!(selection.smart_select_enabled.toggle_and_save_value(ctx));
|
||
report_if_error!(selection
|
||
.word_char_allowlist
|
||
.set_value("/-.".to_owned(), ctx));
|
||
assert!(!selection.smart_select_enabled());
|
||
});
|
||
});
|
||
|
||
app.add_window(WindowStyle::NotStealFocus, |ctx| {
|
||
let mut editor = EditorView::new_with_base_text(
|
||
"word ~/.warp/themes/foo-bar.yaml thing",
|
||
Default::default(),
|
||
ctx,
|
||
);
|
||
editor.select_word(&DisplayPoint::new(0, 8), ctx);
|
||
assert_eq!(editor.selected_text(ctx), "/.warp/themes/foo-bar.yaml");
|
||
editor
|
||
});
|
||
});
|
||
}
|
||
|
||
#[test]
|
||
fn test_smart_select_with_drag() {
|
||
App::test((), |mut app| async move {
|
||
initialize_app(&mut app);
|
||
|
||
// Smart select is enabled by default; just verify
|
||
app.update(|ctx| {
|
||
SemanticSelection::handle(ctx).update(ctx, |selection, _ctx| {
|
||
assert!(selection.smart_select_enabled());
|
||
});
|
||
});
|
||
|
||
app.add_window(WindowStyle::NotStealFocus, |ctx| {
|
||
let mut editor = EditorView::new_with_base_text(
|
||
"word ~/.warp/themes/foo-bar.yaml andy@warp.dev",
|
||
Default::default(),
|
||
ctx,
|
||
);
|
||
editor.select_word(&DisplayPoint::new(0, 8), ctx);
|
||
editor.update_selection(DisplayPoint::new(0, 34), Vector2F::zero(), ctx);
|
||
assert_eq!(
|
||
editor.selected_text(ctx),
|
||
"~/.warp/themes/foo-bar.yaml andy"
|
||
);
|
||
editor
|
||
});
|
||
});
|
||
}
|
||
|
||
#[test]
|
||
fn test_selection_mode_words() {
|
||
App::test((), |mut app| async move {
|
||
initialize_app(&mut app);
|
||
|
||
let (_, buffer_view) = app.add_window(WindowStyle::NotStealFocus, |ctx| {
|
||
EditorView::new_with_base_text("aaaaaa bbbbbb cccccc", Default::default(), ctx)
|
||
});
|
||
|
||
// Set starting selection and selection mode as word
|
||
buffer_view.update(&mut app, |view, ctx| {
|
||
view.select_word(&DisplayPoint::new(0, 9), ctx);
|
||
});
|
||
|
||
buffer_view.read(&app, |view, app| {
|
||
assert_eq!(
|
||
view.selected_ranges(app),
|
||
[DisplayPoint::new(0, 7)..DisplayPoint::new(0, 13)]
|
||
);
|
||
});
|
||
|
||
// Select next word
|
||
buffer_view.update(&mut app, |view, ctx| {
|
||
view.update_selection(DisplayPoint::new(0, 16), Vector2F::zero(), ctx);
|
||
});
|
||
|
||
buffer_view.read(&app, |view, app| {
|
||
assert_eq!(
|
||
view.selected_ranges(app),
|
||
[DisplayPoint::new(0, 7)..DisplayPoint::new(0, 20)]
|
||
);
|
||
});
|
||
|
||
// Select previous word
|
||
buffer_view.update(&mut app, |view, ctx| {
|
||
view.update_selection(DisplayPoint::new(0, 3), Vector2F::zero(), ctx);
|
||
});
|
||
|
||
buffer_view.read(&app, |view, app| {
|
||
assert_eq!(
|
||
view.selected_ranges(app),
|
||
[DisplayPoint::new(0, 13)..DisplayPoint::new(0, 0)]
|
||
);
|
||
});
|
||
|
||
// Select within starting selection
|
||
buffer_view.update(&mut app, |view, ctx| {
|
||
view.update_selection(DisplayPoint::new(0, 11), Vector2F::zero(), ctx);
|
||
});
|
||
|
||
buffer_view.read(&app, |view, app| {
|
||
assert_eq!(
|
||
view.selected_ranges(app),
|
||
[DisplayPoint::new(0, 7)..DisplayPoint::new(0, 13)]
|
||
);
|
||
});
|
||
|
||
buffer_view.update(&mut app, |view, ctx| {
|
||
view.end_selection(ctx);
|
||
view.update_selection(DisplayPoint::new(0, 3), Vector2F::zero(), ctx);
|
||
});
|
||
});
|
||
}
|
||
|
||
#[test]
|
||
fn test_select_right_by_word() {
|
||
App::test((), |mut app| async move {
|
||
initialize_app(&mut app);
|
||
|
||
app.add_window(WindowStyle::NotStealFocus, |ctx| {
|
||
let mut editor = EditorView::new_with_base_text("word word", Default::default(), ctx);
|
||
editor
|
||
.select_ranges(vec![DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1)], ctx)
|
||
.unwrap();
|
||
editor.cursor_forward_one_word(true /* select */, ctx);
|
||
assert_eq!(editor.selected_text(ctx), "ord");
|
||
editor.model().update(ctx, |editor_model, ctx| {
|
||
let a11y = editor_model.delta_for_a11y(
|
||
ByteOffset::from(1)..ByteOffset::from(1),
|
||
false,
|
||
ctx,
|
||
);
|
||
assert_eq!(a11y.value, "ord");
|
||
assert_eq!(a11y.help, Some(", selected".to_string()));
|
||
});
|
||
editor.cursor_forward_one_word(true /* select */, ctx);
|
||
assert_eq!(editor.selected_text(ctx), "ord word");
|
||
editor.model().update(ctx, |editor_model, ctx| {
|
||
let a11y = editor_model.delta_for_a11y(
|
||
ByteOffset::from(5)..ByteOffset::from(1),
|
||
true,
|
||
ctx,
|
||
);
|
||
assert_eq!(a11y.value, "word");
|
||
assert_eq!(a11y.help, Some(", selected".to_string()));
|
||
});
|
||
editor.cursor_forward_one_word(true /* select */, ctx);
|
||
assert_eq!(editor.selected_text(ctx), "ord word");
|
||
editor
|
||
.select_ranges(vec![DisplayPoint::new(0, 1)..DisplayPoint::new(0, 0)], ctx)
|
||
.unwrap();
|
||
editor.cursor_forward_one_word(true /* select */, ctx);
|
||
assert_eq!(editor.selected_text(ctx), "ord");
|
||
editor
|
||
.select_ranges(vec![DisplayPoint::new(0, 5)..DisplayPoint::new(0, 0)], ctx)
|
||
.unwrap();
|
||
editor.cursor_forward_one_word(true /* select */, ctx);
|
||
assert_eq!(editor.selected_text(ctx), " ");
|
||
editor
|
||
});
|
||
});
|
||
}
|
||
|
||
#[test]
|
||
fn test_select_left_by_word() {
|
||
App::test((), |mut app| async move {
|
||
initialize_app(&mut app);
|
||
|
||
app.add_window(WindowStyle::NotStealFocus, |ctx| {
|
||
let mut editor = EditorView::new_with_base_text("word word", Default::default(), ctx);
|
||
editor
|
||
.select_ranges(vec![DisplayPoint::new(0, 6)..DisplayPoint::new(0, 6)], ctx)
|
||
.unwrap();
|
||
editor.cursor_backward_one_word(true /* select */, ctx);
|
||
assert_eq!(editor.selected_text(ctx), "w");
|
||
editor.model().update(ctx, |editor_model, ctx| {
|
||
let a11y = editor_model.delta_for_a11y(
|
||
ByteOffset::from(6)..ByteOffset::from(6),
|
||
false,
|
||
ctx,
|
||
);
|
||
assert_eq!(a11y.value, "w");
|
||
assert_eq!(a11y.help, Some(", selected".to_string()));
|
||
});
|
||
editor.cursor_backward_one_word(true /* select */, ctx);
|
||
assert_eq!(editor.selected_text(ctx), "word w");
|
||
editor.model().update(ctx, |editor_model, ctx| {
|
||
let a11y = editor_model.delta_for_a11y(
|
||
ByteOffset::from(5)..ByteOffset::from(6),
|
||
true,
|
||
ctx,
|
||
);
|
||
assert_eq!(a11y.value, "word ");
|
||
assert_eq!(a11y.help, Some(", selected".to_string()));
|
||
});
|
||
editor.cursor_backward_one_word(true /* select */, ctx);
|
||
assert_eq!(editor.selected_text(ctx), "word w");
|
||
editor
|
||
.select_ranges(vec![DisplayPoint::new(0, 6)..DisplayPoint::new(0, 9)], ctx)
|
||
.unwrap();
|
||
editor.cursor_backward_one_word(true /* select */, ctx);
|
||
assert_eq!(editor.selected_text(ctx), "w");
|
||
editor
|
||
.select_ranges(vec![DisplayPoint::new(0, 0)..DisplayPoint::new(0, 5)], ctx)
|
||
.unwrap();
|
||
editor.cursor_backward_one_word(true /* select */, ctx);
|
||
assert_eq!(editor.selected_text(ctx), "");
|
||
editor
|
||
});
|
||
});
|
||
}
|
||
|
||
#[test]
|
||
fn test_delete_word_left() {
|
||
App::test((), |mut app| async move {
|
||
initialize_app(&mut app);
|
||
|
||
app.add_window(WindowStyle::NotStealFocus, |ctx| {
|
||
let mut editor =
|
||
EditorView::new_with_base_text("word word\\ word%%", Default::default(), ctx);
|
||
editor
|
||
.select_ranges(
|
||
vec![DisplayPoint::new(0, 13)..DisplayPoint::new(0, 13)],
|
||
ctx,
|
||
)
|
||
.unwrap();
|
||
editor.delete_word_left(ctx);
|
||
assert_eq!(editor.buffer_text(ctx), "word word\\ rd%%");
|
||
editor.delete_word_left(ctx);
|
||
assert_eq!(editor.buffer_text(ctx), "word rd%%");
|
||
editor.delete_word_left(ctx);
|
||
assert_eq!(editor.buffer_text(ctx), "rd%%");
|
||
|
||
editor.undo(ctx);
|
||
assert_eq!(editor.buffer_text(ctx), "word rd%%");
|
||
editor.redo(ctx);
|
||
assert_eq!(editor.buffer_text(ctx), "rd%%");
|
||
|
||
editor
|
||
});
|
||
});
|
||
}
|
||
|
||
#[test]
|
||
fn test_select_linewise() -> Result<()> {
|
||
App::test((), |mut app| async move {
|
||
initialize_app(&mut app);
|
||
|
||
let base_text = "abcd
|
||
efg
|
||
|
||
hijk
|
||
lmnop
|
||
q
|
||
r"
|
||
.unindent();
|
||
|
||
let (_, editor) = app.add_window(WindowStyle::NotStealFocus, |view_ctx| {
|
||
let editor = EditorView::new_with_base_text(base_text, Default::default(), view_ctx);
|
||
|
||
assert_eq!(
|
||
editor.buffer_text(view_ctx),
|
||
"abcd\nefg\n\nhijk\nlmnop\nq\nr"
|
||
);
|
||
|
||
editor
|
||
});
|
||
|
||
// given a cursor before "a", select "abcd\n"
|
||
editor.update(&mut app, |view, view_ctx| {
|
||
view.select_ranges(
|
||
vec![DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)],
|
||
view_ctx,
|
||
)
|
||
.unwrap();
|
||
|
||
view.change_selections(view_ctx, |model, ctx| {
|
||
model.extend_selection_linewise(/* include_newline */ true, ctx);
|
||
});
|
||
});
|
||
|
||
editor.read(&app, |view, app| {
|
||
assert_eq!(
|
||
view.selected_ranges(app),
|
||
vec![DisplayPoint::new(0, 0)..DisplayPoint::new(1, 0),]
|
||
);
|
||
});
|
||
|
||
// given a cursor between "e" and "f", select "efg\n"
|
||
editor.update(&mut app, |view, view_ctx| {
|
||
view.select_ranges(
|
||
vec![DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1)],
|
||
view_ctx,
|
||
)
|
||
.unwrap();
|
||
|
||
view.change_selections(view_ctx, |model, ctx| {
|
||
model.extend_selection_linewise(/* include_newline */ true, ctx);
|
||
});
|
||
});
|
||
|
||
editor.read(&app, |view, app| {
|
||
assert_eq!(
|
||
view.selected_ranges(app),
|
||
vec![DisplayPoint::new(1, 0)..DisplayPoint::new(2, 0),]
|
||
);
|
||
});
|
||
|
||
// given a cursor on a blank line, select the newline ("\n")
|
||
editor.update(&mut app, |view, view_ctx| {
|
||
view.select_ranges(
|
||
vec![DisplayPoint::new(2, 0)..DisplayPoint::new(2, 0)],
|
||
view_ctx,
|
||
)
|
||
.unwrap();
|
||
|
||
view.change_selections(view_ctx, |model, ctx| {
|
||
model.extend_selection_linewise(/* include_newline */ true, ctx);
|
||
});
|
||
});
|
||
|
||
editor.read(&app, |view, app| {
|
||
assert_eq!(
|
||
view.selected_ranges(app),
|
||
vec![DisplayPoint::new(2, 0)..DisplayPoint::new(3, 0),]
|
||
);
|
||
});
|
||
|
||
// given a cursor after "k", select "hijk\n"
|
||
editor.update(&mut app, |view, view_ctx| {
|
||
view.select_ranges(
|
||
vec![DisplayPoint::new(3, 4)..DisplayPoint::new(3, 4)],
|
||
view_ctx,
|
||
)
|
||
.unwrap();
|
||
|
||
view.change_selections(view_ctx, |model, ctx| {
|
||
model.extend_selection_linewise(/* include_newline */ true, ctx);
|
||
});
|
||
});
|
||
|
||
editor.read(&app, |view, app| {
|
||
assert_eq!(
|
||
view.selected_ranges(app),
|
||
vec![DisplayPoint::new(3, 0)..DisplayPoint::new(4, 0),]
|
||
);
|
||
});
|
||
|
||
// given a cursor between "m" and "n", select "lmnop"
|
||
editor.update(&mut app, |view, view_ctx| {
|
||
view.select_ranges(
|
||
vec![DisplayPoint::new(4, 3)..DisplayPoint::new(4, 3)],
|
||
view_ctx,
|
||
)
|
||
.unwrap();
|
||
|
||
view.change_selections(view_ctx, |model, ctx| {
|
||
model.extend_selection_linewise(/* include_newline */ false, ctx);
|
||
});
|
||
});
|
||
|
||
editor.read(&app, |view, app| {
|
||
assert_eq!(
|
||
view.selected_ranges(app),
|
||
vec![DisplayPoint::new(4, 0)..DisplayPoint::new(4, 5),]
|
||
);
|
||
});
|
||
|
||
// given a cursor on "q", select "q"
|
||
editor.update(&mut app, |view, view_ctx| {
|
||
view.select_ranges(
|
||
vec![DisplayPoint::new(5, 0)..DisplayPoint::new(5, 0)],
|
||
view_ctx,
|
||
)
|
||
.unwrap();
|
||
|
||
view.change_selections(view_ctx, |model, ctx| {
|
||
model.extend_selection_linewise(/* include_newline */ false, ctx);
|
||
});
|
||
});
|
||
|
||
editor.read(&app, |view, app| {
|
||
assert_eq!(
|
||
view.selected_ranges(app),
|
||
vec![DisplayPoint::new(5, 0)..DisplayPoint::new(5, 1),]
|
||
);
|
||
});
|
||
|
||
// given a cursor on "r", select "\nr"
|
||
editor.update(&mut app, |view, view_ctx| {
|
||
view.select_ranges(
|
||
vec![DisplayPoint::new(6, 0)..DisplayPoint::new(6, 0)],
|
||
view_ctx,
|
||
)
|
||
.unwrap();
|
||
|
||
view.change_selections(view_ctx, |model, ctx| {
|
||
model.extend_selection_linewise(/* include_newline */ true, ctx);
|
||
});
|
||
});
|
||
|
||
editor.read(&app, |view, app| {
|
||
assert_eq!(
|
||
view.selected_ranges(app),
|
||
vec![DisplayPoint::new(5, 1)..DisplayPoint::new(6, 1),]
|
||
);
|
||
});
|
||
|
||
Ok(())
|
||
})
|
||
}
|
||
|
||
#[test]
|
||
fn test_select_lines_below() {
|
||
App::test((), |mut app| async move {
|
||
initialize_app(&mut app);
|
||
|
||
let base_text = "The quick brown fox
|
||
jumped over
|
||
the lazy dog."
|
||
.unindent();
|
||
|
||
let (_, editor) = app.add_window(WindowStyle::NotStealFocus, |view_ctx| {
|
||
EditorView::new_with_base_text(base_text, Default::default(), view_ctx)
|
||
});
|
||
|
||
// Given a cursor on "q" from "quick", extend the selection to cover one line below.
|
||
editor.update(&mut app, |view, view_ctx| {
|
||
view.select_ranges(
|
||
vec![DisplayPoint::new(0, 4)..DisplayPoint::new(0, 4)],
|
||
view_ctx,
|
||
)
|
||
.unwrap();
|
||
|
||
view.change_selections(view_ctx, |model, model_ctx| {
|
||
model.extend_selection_below(1, model_ctx);
|
||
model.selection_line_start_test(model_ctx);
|
||
|
||
model.cursor_line_end(true, model_ctx);
|
||
});
|
||
});
|
||
|
||
editor.read(&app, |view, app| {
|
||
assert_eq!(
|
||
view.selected_ranges(app),
|
||
vec![DisplayPoint::new(0, 0)..DisplayPoint::new(1, 11),]
|
||
);
|
||
});
|
||
|
||
// Given a cursor on "q" from "quick", extend the selection to
|
||
// the character directly below ("d").
|
||
editor.update(&mut app, |view, view_ctx| {
|
||
view.select_ranges(
|
||
vec![DisplayPoint::new(0, 4)..DisplayPoint::new(0, 4)],
|
||
view_ctx,
|
||
)
|
||
.unwrap();
|
||
|
||
view.change_selections(view_ctx, |model, model_ctx| {
|
||
model.extend_selection_below(1, model_ctx);
|
||
});
|
||
});
|
||
|
||
editor.read(&app, |view, app| {
|
||
assert_eq!(
|
||
view.selected_ranges(app),
|
||
vec![DisplayPoint::new(0, 4)..DisplayPoint::new(1, 4),]
|
||
);
|
||
});
|
||
|
||
// Given a cursor on "n" from "brown", extend the selection to cover one line below.
|
||
editor.update(&mut app, |view, view_ctx| {
|
||
view.select_ranges(
|
||
vec![DisplayPoint::new(0, 15)..DisplayPoint::new(0, 15)],
|
||
view_ctx,
|
||
)
|
||
.unwrap();
|
||
|
||
view.change_selections(view_ctx, |model, model_ctx| {
|
||
model.extend_selection_below(1, model_ctx);
|
||
model.selection_line_start_test(model_ctx);
|
||
model.cursor_line_end(true, model_ctx);
|
||
});
|
||
});
|
||
|
||
editor.read(&app, |view, app| {
|
||
assert_eq!(
|
||
view.selected_ranges(app),
|
||
vec![DisplayPoint::new(0, 0)..DisplayPoint::new(1, 11),]
|
||
);
|
||
});
|
||
|
||
// Given a cursor on "n" from "brown", extend the selection to the character
|
||
// directly below, which should default to the end of the line.
|
||
editor.update(&mut app, |view, view_ctx| {
|
||
view.select_ranges(
|
||
vec![DisplayPoint::new(0, 15)..DisplayPoint::new(0, 15)],
|
||
view_ctx,
|
||
)
|
||
.unwrap();
|
||
|
||
view.change_selections(view_ctx, |model, model_ctx| {
|
||
model.extend_selection_below(1, model_ctx);
|
||
});
|
||
});
|
||
|
||
editor.read(&app, |view, app| {
|
||
assert_eq!(
|
||
view.selected_ranges(app),
|
||
vec![DisplayPoint::new(0, 15)..DisplayPoint::new(1, 11),]
|
||
);
|
||
});
|
||
|
||
// Given a cursor on "l" from "lazy", attempt to extend the selection
|
||
// to cover one line below, even though there is nothing there.
|
||
editor.update(&mut app, |view, view_ctx| {
|
||
view.select_ranges(
|
||
vec![DisplayPoint::new(2, 4)..DisplayPoint::new(2, 4)],
|
||
view_ctx,
|
||
)
|
||
.unwrap();
|
||
|
||
view.change_selections(view_ctx, |model, model_ctx| {
|
||
model.extend_selection_below(1, model_ctx);
|
||
model.selection_line_start_test(model_ctx);
|
||
model.cursor_line_end(true, model_ctx);
|
||
});
|
||
});
|
||
|
||
editor.read(&app, |view, app| {
|
||
assert_eq!(
|
||
view.selected_ranges(app),
|
||
vec![DisplayPoint::new(2, 0)..DisplayPoint::new(2, 13),]
|
||
);
|
||
});
|
||
});
|
||
}
|
||
|
||
#[test]
|
||
fn test_select_lines_above() {
|
||
App::test((), |mut app| async move {
|
||
initialize_app(&mut app);
|
||
|
||
let base_text = "The quick brown fox
|
||
jumped
|
||
over the lazy dog."
|
||
.unindent();
|
||
|
||
let (_, editor) = app.add_window(WindowStyle::NotStealFocus, |view_ctx| {
|
||
EditorView::new_with_base_text(base_text, Default::default(), view_ctx)
|
||
});
|
||
|
||
// Given a cursor on "r" from "over", extend the selection to cover one line above.
|
||
editor.update(&mut app, |view, view_ctx| {
|
||
view.select_ranges(
|
||
vec![DisplayPoint::new(2, 3)..DisplayPoint::new(2, 3)],
|
||
view_ctx,
|
||
)
|
||
.unwrap();
|
||
|
||
view.change_selections(view_ctx, |model, model_ctx| {
|
||
model.extend_selection_above(1, model_ctx);
|
||
model.selection_line_start_test(model_ctx);
|
||
model.cursor_line_end(true, model_ctx);
|
||
});
|
||
});
|
||
|
||
editor.read(&app, |view, app| {
|
||
assert_eq!(
|
||
view.selected_ranges(app),
|
||
vec![DisplayPoint::new(1, 0)..DisplayPoint::new(2, 18),]
|
||
);
|
||
});
|
||
|
||
// Given a cursor on "r" from "over", extend the selection to
|
||
// the character directly above ("p").
|
||
editor.update(&mut app, |view, view_ctx| {
|
||
view.select_ranges(
|
||
vec![DisplayPoint::new(2, 3)..DisplayPoint::new(2, 3)],
|
||
view_ctx,
|
||
)
|
||
.unwrap();
|
||
|
||
view.change_selections(view_ctx, |model, model_ctx| {
|
||
model.extend_selection_above(1, model_ctx);
|
||
});
|
||
});
|
||
|
||
editor.read(&app, |view, app| {
|
||
assert_eq!(
|
||
view.selected_ranges(app),
|
||
vec![DisplayPoint::new(1, 3)..DisplayPoint::new(2, 3),]
|
||
);
|
||
});
|
||
|
||
// Given a cursor on "l" from "lazy", extend the selection to cover
|
||
// the line above.
|
||
editor.update(&mut app, |view, view_ctx| {
|
||
view.select_ranges(
|
||
vec![DisplayPoint::new(2, 9)..DisplayPoint::new(2, 9)],
|
||
view_ctx,
|
||
)
|
||
.unwrap();
|
||
|
||
view.change_selections(view_ctx, |model, model_ctx| {
|
||
model.extend_selection_above(1, model_ctx);
|
||
model.selection_line_start_test(model_ctx);
|
||
model.cursor_line_end(true, model_ctx);
|
||
});
|
||
});
|
||
|
||
editor.read(&app, |view, app| {
|
||
assert_eq!(
|
||
view.selected_ranges(app),
|
||
vec![DisplayPoint::new(1, 0)..DisplayPoint::new(2, 18),]
|
||
);
|
||
});
|
||
|
||
// Given a cursor on "l" from "lazy", extend the selection to the
|
||
// character directly above, which should default to the end of the line.
|
||
editor.update(&mut app, |view, view_ctx| {
|
||
view.select_ranges(
|
||
vec![DisplayPoint::new(2, 9)..DisplayPoint::new(2, 9)],
|
||
view_ctx,
|
||
)
|
||
.unwrap();
|
||
|
||
view.change_selections(view_ctx, |model, model_ctx| {
|
||
model.extend_selection_above(1, model_ctx);
|
||
});
|
||
});
|
||
|
||
editor.read(&app, |view, app| {
|
||
assert_eq!(
|
||
view.selected_ranges(app),
|
||
vec![DisplayPoint::new(1, 6)..DisplayPoint::new(2, 9),]
|
||
);
|
||
});
|
||
|
||
// Given a cursor on "q" from "quick", attempt to extend the selection
|
||
// to cover one line above, even though there is nothing there.
|
||
editor.update(&mut app, |view, view_ctx| {
|
||
view.select_ranges(
|
||
vec![DisplayPoint::new(0, 4)..DisplayPoint::new(0, 4)],
|
||
view_ctx,
|
||
)
|
||
.unwrap();
|
||
|
||
view.change_selections(view_ctx, |model, model_ctx| {
|
||
model.extend_selection_above(1, model_ctx);
|
||
model.selection_line_start_test(model_ctx);
|
||
model.cursor_line_end(true, model_ctx);
|
||
});
|
||
});
|
||
|
||
editor.read(&app, |view, app| {
|
||
assert_eq!(
|
||
view.selected_ranges(app),
|
||
vec![DisplayPoint::new(0, 0)..DisplayPoint::new(0, 19),]
|
||
);
|
||
});
|
||
});
|
||
}
|
||
|
||
#[test]
|
||
fn test_delete_word_right() {
|
||
App::test((), |mut app| async move {
|
||
initialize_app(&mut app);
|
||
|
||
app.add_window(WindowStyle::NotStealFocus, |ctx| {
|
||
let mut editor =
|
||
EditorView::new_with_base_text("word word\\ word%%", Default::default(), ctx);
|
||
editor
|
||
.select_ranges(vec![DisplayPoint::new(0, 1)..DisplayPoint::new(0, 13)], ctx)
|
||
.unwrap();
|
||
editor.delete_word_right(ctx);
|
||
assert_eq!(editor.buffer_text(ctx), "w word\\ word%%");
|
||
editor.delete_word_right(ctx);
|
||
assert_eq!(editor.buffer_text(ctx), "w\\ word%%");
|
||
editor.delete_word_right(ctx);
|
||
assert_eq!(editor.buffer_text(ctx), "w%%");
|
||
editor.delete_word_right(ctx);
|
||
assert_eq!(editor.buffer_text(ctx), "w");
|
||
|
||
editor.undo(ctx);
|
||
assert_eq!(editor.buffer_text(ctx), "w%%");
|
||
editor.redo(ctx);
|
||
assert_eq!(editor.buffer_text(ctx), "w");
|
||
|
||
editor
|
||
});
|
||
});
|
||
}
|
||
|
||
#[test]
|
||
fn test_backspace() {
|
||
App::test((), |mut app| async move {
|
||
initialize_app(&mut app);
|
||
|
||
app.add_window(WindowStyle::NotStealFocus, |ctx| {
|
||
let mut editor =
|
||
EditorView::new_with_base_text(sample_text(6, 6), Default::default(), ctx);
|
||
editor
|
||
.select_ranges(
|
||
vec![
|
||
DisplayPoint::new(2, 2)..DisplayPoint::new(2, 2),
|
||
DisplayPoint::new(3, 3)..DisplayPoint::new(3, 3),
|
||
DisplayPoint::new(4, 4)..DisplayPoint::new(5, 5),
|
||
],
|
||
ctx,
|
||
)
|
||
.unwrap();
|
||
editor.backspace(ctx);
|
||
assert_eq!(
|
||
editor.buffer_text(ctx),
|
||
"aaaaaa\nbbbbbb\nccccc\nddddd\neeeef"
|
||
);
|
||
|
||
editor.undo(ctx);
|
||
assert_eq!(
|
||
editor.buffer_text(ctx),
|
||
"aaaaaa\nbbbbbb\ncccccc\ndddddd\neeeeee\nffffff"
|
||
);
|
||
|
||
editor
|
||
});
|
||
});
|
||
}
|
||
|
||
#[test]
|
||
fn test_page_up() {
|
||
App::test((), |mut app| async move {
|
||
initialize_app(&mut app);
|
||
|
||
let (_, editor) = app.add_window(WindowStyle::NotStealFocus, |ctx| {
|
||
EditorView::new_with_base_text(
|
||
"aaaaa\nbbbbbbb\nccccc\nddddddd\neeeeee",
|
||
Default::default(),
|
||
ctx,
|
||
)
|
||
});
|
||
|
||
// Single cursor on top line
|
||
editor.update(&mut app, |view, ctx| {
|
||
view.select_ranges(vec![DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2)], ctx)
|
||
.unwrap();
|
||
view.page_up(ctx);
|
||
});
|
||
|
||
editor.read(&app, |view, app| {
|
||
assert_eq!(
|
||
view.selected_ranges(app),
|
||
vec![DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)]
|
||
);
|
||
});
|
||
|
||
// Single cursor below top line, fewer columns than length of top line
|
||
editor.update(&mut app, |view, ctx| {
|
||
view.select_ranges(vec![DisplayPoint::new(2, 2)..DisplayPoint::new(2, 2)], ctx)
|
||
.unwrap();
|
||
view.page_up(ctx);
|
||
});
|
||
|
||
editor.read(&app, |view, app| {
|
||
assert_eq!(
|
||
view.selected_ranges(app),
|
||
vec![DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2)]
|
||
);
|
||
});
|
||
|
||
// Single cursor below top line, more columns than length of top line
|
||
editor.update(&mut app, |view, ctx| {
|
||
view.select_ranges(vec![DisplayPoint::new(1, 6)..DisplayPoint::new(1, 6)], ctx)
|
||
.unwrap();
|
||
view.page_up(ctx);
|
||
});
|
||
|
||
editor.read(&app, |view, app| {
|
||
assert_eq!(
|
||
view.selected_ranges(app),
|
||
vec![DisplayPoint::new(0, 5)..DisplayPoint::new(0, 5)]
|
||
);
|
||
});
|
||
|
||
// Multiple cursors, same column
|
||
editor.update(&mut app, |view, ctx| {
|
||
view.select_ranges(
|
||
vec![
|
||
DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1),
|
||
DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1),
|
||
DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1),
|
||
],
|
||
ctx,
|
||
)
|
||
.unwrap();
|
||
view.page_up(ctx);
|
||
});
|
||
|
||
editor.read(&app, |view, app| {
|
||
assert_eq!(
|
||
view.selected_ranges(app),
|
||
vec![DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1)]
|
||
);
|
||
});
|
||
|
||
// Multiple cursors, different columns
|
||
editor.update(&mut app, |view, ctx| {
|
||
view.select_ranges(
|
||
vec![
|
||
DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1),
|
||
DisplayPoint::new(1, 2)..DisplayPoint::new(1, 2),
|
||
DisplayPoint::new(2, 3)..DisplayPoint::new(4, 3),
|
||
],
|
||
ctx,
|
||
)
|
||
.unwrap();
|
||
view.page_up(ctx);
|
||
});
|
||
|
||
editor.read(&app, |view, app| {
|
||
assert_eq!(
|
||
view.selected_ranges(app),
|
||
vec![DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)]
|
||
);
|
||
});
|
||
})
|
||
}
|
||
|
||
#[test]
|
||
fn test_page_down() {
|
||
App::test((), |mut app| async move {
|
||
initialize_app(&mut app);
|
||
|
||
let (_, editor) = app.add_window(WindowStyle::NotStealFocus, |ctx| {
|
||
EditorView::new_with_base_text(
|
||
"aaaaa\nbbbbbbb\nccccc\nddddddd\neeeee",
|
||
Default::default(),
|
||
ctx,
|
||
)
|
||
});
|
||
|
||
// Single cursor on bottom line
|
||
editor.update(&mut app, |view, ctx| {
|
||
view.select_ranges(vec![DisplayPoint::new(4, 2)..DisplayPoint::new(4, 2)], ctx)
|
||
.unwrap();
|
||
view.page_down(ctx);
|
||
});
|
||
|
||
editor.read(&app, |view, app| {
|
||
assert_eq!(
|
||
view.selected_ranges(app),
|
||
vec![DisplayPoint::new(4, 5)..DisplayPoint::new(4, 5)]
|
||
);
|
||
});
|
||
|
||
// Single cursor above bottom line, fewer columns than length of bottom line
|
||
editor.update(&mut app, |view, ctx| {
|
||
view.select_ranges(vec![DisplayPoint::new(2, 2)..DisplayPoint::new(2, 2)], ctx)
|
||
.unwrap();
|
||
view.page_down(ctx);
|
||
});
|
||
|
||
editor.read(&app, |view, app| {
|
||
assert_eq!(
|
||
view.selected_ranges(app),
|
||
vec![DisplayPoint::new(4, 2)..DisplayPoint::new(4, 2)]
|
||
);
|
||
});
|
||
|
||
// Single cursor above bottom line, more columns than length of bottom line
|
||
editor.update(&mut app, |view, ctx| {
|
||
view.select_ranges(vec![DisplayPoint::new(3, 7)..DisplayPoint::new(3, 7)], ctx)
|
||
.unwrap();
|
||
view.page_down(ctx);
|
||
});
|
||
|
||
editor.read(&app, |view, app| {
|
||
assert_eq!(
|
||
view.selected_ranges(app),
|
||
vec![DisplayPoint::new(4, 5)..DisplayPoint::new(4, 5)]
|
||
);
|
||
});
|
||
|
||
// Multiple cursors, same column
|
||
editor.update(&mut app, |view, ctx| {
|
||
view.select_ranges(
|
||
vec![
|
||
DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1),
|
||
DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1),
|
||
DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1),
|
||
],
|
||
ctx,
|
||
)
|
||
.unwrap();
|
||
view.page_down(ctx);
|
||
});
|
||
|
||
editor.read(&app, |view, app| {
|
||
assert_eq!(
|
||
view.selected_ranges(app),
|
||
vec![DisplayPoint::new(4, 1)..DisplayPoint::new(4, 1)]
|
||
);
|
||
});
|
||
|
||
// Multiple cursors, different columns
|
||
editor.update(&mut app, |view, ctx| {
|
||
view.select_ranges(
|
||
vec![
|
||
DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1),
|
||
DisplayPoint::new(1, 2)..DisplayPoint::new(1, 2),
|
||
DisplayPoint::new(2, 3)..DisplayPoint::new(4, 3),
|
||
],
|
||
ctx,
|
||
)
|
||
.unwrap();
|
||
view.page_down(ctx);
|
||
});
|
||
|
||
editor.read(&app, |view, app| {
|
||
assert_eq!(
|
||
view.selected_ranges(app),
|
||
vec![DisplayPoint::new(4, 5)..DisplayPoint::new(4, 5)]
|
||
);
|
||
});
|
||
})
|
||
}
|
||
|
||
#[test]
|
||
fn test_delete_basic() {
|
||
App::test((), |mut app| async move {
|
||
initialize_app(&mut app);
|
||
|
||
app.add_window(WindowStyle::NotStealFocus, |ctx| {
|
||
let mut editor =
|
||
EditorView::new_with_base_text(sample_text(6, 6), Default::default(), ctx);
|
||
editor
|
||
.select_ranges(
|
||
vec![
|
||
DisplayPoint::new(2, 2)..DisplayPoint::new(2, 2),
|
||
DisplayPoint::new(3, 3)..DisplayPoint::new(3, 3),
|
||
DisplayPoint::new(4, 4)..DisplayPoint::new(5, 5),
|
||
],
|
||
ctx,
|
||
)
|
||
.unwrap();
|
||
editor.delete(ctx);
|
||
assert_eq!(
|
||
editor.buffer_text(ctx),
|
||
"aaaaaa\nbbbbbb\nccccc\nddddd\neeeef"
|
||
);
|
||
|
||
editor.undo(ctx);
|
||
assert_eq!(
|
||
editor.buffer_text(ctx),
|
||
"aaaaaa\nbbbbbb\ncccccc\ndddddd\neeeeee\nffffff"
|
||
);
|
||
editor
|
||
});
|
||
});
|
||
}
|
||
|
||
#[test]
|
||
fn test_indent_empty_buffer() {
|
||
App::test((), |mut app| async move {
|
||
initialize_app(&mut app);
|
||
|
||
app.add_window(WindowStyle::NotStealFocus, |ctx| {
|
||
let mut editor = EditorView::new(Default::default(), ctx);
|
||
editor.handle_tab(ctx);
|
||
assert_eq!(editor.buffer_text(ctx), " ");
|
||
|
||
editor.undo(ctx);
|
||
assert_eq!(editor.buffer_text(ctx), "");
|
||
editor.redo(ctx);
|
||
assert_eq!(editor.buffer_text(ctx), " ");
|
||
|
||
editor
|
||
});
|
||
});
|
||
}
|
||
|
||
#[test]
|
||
fn test_undo_redo_text_styles() {
|
||
App::test((), |mut app| async move {
|
||
initialize_app(&mut app);
|
||
|
||
let black_text_style = TextStyle::new().with_background_color(ColorU::black());
|
||
app.add_window(WindowStyle::NotStealFocus, |ctx| {
|
||
let mut editor = EditorView::new(Default::default(), ctx);
|
||
editor.insert_with_styles(
|
||
"foo",
|
||
&[(ByteOffset::from(0)..ByteOffset::from(3), black_text_style)],
|
||
PlainTextEditorViewAction::SystemInsert,
|
||
ctx,
|
||
);
|
||
assert_eq!(editor.buffer_text(ctx), "foo");
|
||
|
||
editor.clear_buffer(ctx);
|
||
assert_eq!(editor.buffer_text(ctx), "");
|
||
|
||
editor.undo(ctx);
|
||
assert_eq!(editor.buffer_text(ctx), "foo");
|
||
assert_eq!(
|
||
editor.text_style_runs(ctx).collect_vec(),
|
||
vec![TextRun::new(
|
||
"foo".into(),
|
||
black_text_style,
|
||
ByteOffset::from(0)..ByteOffset::from(3)
|
||
)]
|
||
);
|
||
|
||
editor.redo(ctx);
|
||
assert_eq!(editor.buffer_text(ctx), "");
|
||
assert_eq!(editor.text_style_runs(ctx).count(), 0);
|
||
|
||
editor
|
||
});
|
||
})
|
||
}
|
||
|
||
#[test]
|
||
fn test_indent() {
|
||
App::test((), |mut app| async move {
|
||
initialize_app(&mut app);
|
||
|
||
app.add_window(WindowStyle::NotStealFocus, |ctx| {
|
||
let mut editor =
|
||
EditorView::new_with_base_text(sample_text(6, 6), Default::default(), ctx);
|
||
editor
|
||
.select_ranges(
|
||
vec![
|
||
DisplayPoint::new(0, 0)..DisplayPoint::new(0, 2),
|
||
DisplayPoint::new(1, 1)..DisplayPoint::new(1, 5),
|
||
],
|
||
ctx,
|
||
)
|
||
.unwrap();
|
||
editor.handle_tab(ctx);
|
||
assert_eq!(
|
||
editor.buffer_text(ctx),
|
||
" aaaa\nb b\ncccccc\ndddddd\neeeeee\nffffff"
|
||
);
|
||
|
||
editor
|
||
.select_ranges(vec![DisplayPoint::new(2, 0)..DisplayPoint::new(2, 2)], ctx)
|
||
.unwrap();
|
||
editor.handle_tab(ctx);
|
||
assert_eq!(
|
||
editor.buffer_text(ctx),
|
||
" aaaa\nb b\n cccccc\ndddddd\neeeeee\nffffff"
|
||
);
|
||
|
||
editor
|
||
.select_ranges(vec![DisplayPoint::new(3, 0)..DisplayPoint::new(3, 6)], ctx)
|
||
.unwrap();
|
||
editor.handle_tab(ctx);
|
||
assert_eq!(
|
||
editor.buffer_text(ctx),
|
||
" aaaa\nb b\n cccccc\n dddddd\neeeeee\nffffff"
|
||
);
|
||
|
||
editor
|
||
.select_ranges(vec![DisplayPoint::new(4, 3)..DisplayPoint::new(5, 6)], ctx)
|
||
.unwrap();
|
||
editor.handle_tab(ctx);
|
||
assert_eq!(
|
||
editor.buffer_text(ctx),
|
||
" aaaa\nb b\n cccccc\n dddddd\n eeeeee\n ffffff"
|
||
);
|
||
|
||
editor.undo(ctx);
|
||
assert_eq!(
|
||
editor.buffer_text(ctx),
|
||
" aaaa\nb b\n cccccc\n dddddd\neeeeee\nffffff"
|
||
);
|
||
editor.redo(ctx);
|
||
assert_eq!(
|
||
editor.buffer_text(ctx),
|
||
" aaaa\nb b\n cccccc\n dddddd\n eeeeee\n ffffff"
|
||
);
|
||
editor
|
||
});
|
||
})
|
||
}
|
||
|
||
#[test]
|
||
fn test_unindent() {
|
||
App::test((), |mut app| async move {
|
||
initialize_app(&mut app);
|
||
|
||
app.add_window(WindowStyle::NotStealFocus, |ctx| {
|
||
let mut editor = EditorView::new_with_base_text(
|
||
"word\n word\n word\n word\n",
|
||
Default::default(),
|
||
ctx,
|
||
);
|
||
editor
|
||
.select_ranges(
|
||
vec![
|
||
DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0),
|
||
DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1),
|
||
DisplayPoint::new(2, 2)..DisplayPoint::new(2, 2),
|
||
DisplayPoint::new(3, 5)..DisplayPoint::new(3, 5),
|
||
],
|
||
ctx,
|
||
)
|
||
.unwrap();
|
||
editor.unindent(ctx);
|
||
assert_eq!(editor.buffer_text(ctx), "word\nword\nword\n word\n");
|
||
|
||
editor.undo(ctx);
|
||
assert_eq!(
|
||
editor.buffer_text(ctx),
|
||
"word\n word\n word\n word\n"
|
||
);
|
||
editor.redo(ctx);
|
||
assert_eq!(editor.buffer_text(ctx), "word\nword\nword\n word\n");
|
||
|
||
editor
|
||
});
|
||
})
|
||
}
|
||
|
||
#[test]
|
||
fn test_single_line_multi_cursor_unindent() {
|
||
App::test((), |mut app| async move {
|
||
initialize_app(&mut app);
|
||
|
||
app.add_window(WindowStyle::NotStealFocus, |ctx| {
|
||
let mut editor =
|
||
EditorView::new_with_base_text(" hello world", Default::default(), ctx);
|
||
editor
|
||
.select_ranges(
|
||
vec![
|
||
DisplayPoint::new(0, 4)..DisplayPoint::new(0, 4),
|
||
DisplayPoint::new(0, 5)..DisplayPoint::new(0, 6),
|
||
DisplayPoint::new(0, 8)..DisplayPoint::new(0, 8),
|
||
],
|
||
ctx,
|
||
)
|
||
.unwrap();
|
||
editor.unindent(ctx);
|
||
assert_eq!(editor.buffer_text(ctx), "hello world");
|
||
|
||
editor
|
||
});
|
||
})
|
||
}
|
||
|
||
#[test]
|
||
fn test_multi_line_unindent() {
|
||
App::test((), |mut app| async move {
|
||
initialize_app(&mut app);
|
||
|
||
let (_, editor) = app.add_window(WindowStyle::NotStealFocus, |ctx| {
|
||
EditorView::new(Default::default(), ctx)
|
||
});
|
||
|
||
// Suppose the buffer text starts with an empty line followed by a line
|
||
// with leading whitespace + text.
|
||
//
|
||
// Unindent'ing the whole buffer in this case should leave the
|
||
// first line untouched while removing the leading whitespace from the first line.
|
||
editor.update(&mut app, |editor, ctx| {
|
||
editor.set_buffer_text("\n hello", ctx);
|
||
editor.select_all(ctx);
|
||
editor.unindent(ctx);
|
||
assert_eq!(editor.buffer_text(ctx), "\nhello");
|
||
});
|
||
|
||
// Suppose the buffer text starts with a line with some whitespace
|
||
// followed by some text. Similarly, the second line starts with some
|
||
// whitespace followed by some text.
|
||
//
|
||
// Unindent'ing the whole buffer in this case should remove the leading
|
||
// whitespace from both the first and second lines.
|
||
editor.update(&mut app, |editor, ctx| {
|
||
editor.set_buffer_text(" hello\n world", ctx);
|
||
editor.select_all(ctx);
|
||
editor.unindent(ctx);
|
||
assert_eq!(editor.buffer_text(ctx), "hello\nworld");
|
||
});
|
||
})
|
||
}
|
||
|
||
#[test]
|
||
fn test_cursor_line_start() {
|
||
App::test((), |mut app| async move {
|
||
initialize_app(&mut app);
|
||
|
||
let (_, editor) = app.add_window(WindowStyle::NotStealFocus, |ctx| {
|
||
let mut editor = EditorView::new_with_base_text(
|
||
"word\n word\n word\n word\n",
|
||
Default::default(),
|
||
ctx,
|
||
);
|
||
editor
|
||
.select_ranges(
|
||
vec![
|
||
DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0),
|
||
DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1),
|
||
DisplayPoint::new(2, 2)..DisplayPoint::new(2, 2),
|
||
DisplayPoint::new(3, 5)..DisplayPoint::new(3, 5),
|
||
],
|
||
ctx,
|
||
)
|
||
.unwrap();
|
||
editor.change_selections(ctx, |editor_model, ctx| {
|
||
editor_model.cursor_line_start(false /* keep_selection */, ctx)
|
||
});
|
||
editor
|
||
});
|
||
|
||
editor.read(&app, |view, app| {
|
||
assert_eq!(
|
||
view.selected_ranges(app),
|
||
vec![
|
||
DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0),
|
||
DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0),
|
||
DisplayPoint::new(2, 0)..DisplayPoint::new(2, 0),
|
||
DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0),
|
||
]
|
||
);
|
||
});
|
||
|
||
editor.update(&mut app, |view, ctx| {
|
||
view.select_ranges(vec![DisplayPoint::new(0, 1)..DisplayPoint::new(0, 3)], ctx)
|
||
.unwrap();
|
||
view.change_selections(ctx, |editor_model, ctx| {
|
||
editor_model.cursor_line_start(true /* keep_selection */, ctx)
|
||
});
|
||
});
|
||
|
||
editor.read(&app, |view, app| {
|
||
assert_eq!(
|
||
view.selected_ranges(app),
|
||
vec![DisplayPoint::new(0, 1)..DisplayPoint::new(0, 0),]
|
||
);
|
||
});
|
||
|
||
editor.update(&mut app, |view, ctx| {
|
||
view.select_ranges(vec![DisplayPoint::new(0, 3)..DisplayPoint::new(0, 1)], ctx)
|
||
.unwrap();
|
||
view.change_selections(ctx, |editor_model, ctx| {
|
||
editor_model.cursor_line_start(true /* keep_selection */, ctx)
|
||
});
|
||
});
|
||
|
||
editor.read(&app, |view, app| {
|
||
assert_eq!(
|
||
view.selected_ranges(app),
|
||
vec![DisplayPoint::new(0, 3)..DisplayPoint::new(0, 0),]
|
||
);
|
||
});
|
||
})
|
||
}
|
||
|
||
#[test]
|
||
fn test_cursor_line_end() {
|
||
App::test((), |mut app| async move {
|
||
initialize_app(&mut app);
|
||
|
||
let (_, editor) = app.add_window(WindowStyle::NotStealFocus, |ctx| {
|
||
let mut editor = EditorView::new_with_base_text(
|
||
"word\n word\n word\n word\n",
|
||
Default::default(),
|
||
ctx,
|
||
);
|
||
editor
|
||
.select_ranges(
|
||
vec![
|
||
DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0),
|
||
DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1),
|
||
DisplayPoint::new(2, 2)..DisplayPoint::new(2, 2),
|
||
DisplayPoint::new(3, 5)..DisplayPoint::new(3, 5),
|
||
],
|
||
ctx,
|
||
)
|
||
.unwrap();
|
||
editor.change_selections(ctx, |editor_model, ctx| {
|
||
editor_model.cursor_line_end(false, ctx);
|
||
});
|
||
editor
|
||
});
|
||
|
||
editor.read(&app, |view, app| {
|
||
assert_eq!(
|
||
view.selected_ranges(app),
|
||
vec![
|
||
DisplayPoint::new(0, 4)..DisplayPoint::new(0, 4),
|
||
DisplayPoint::new(1, 6)..DisplayPoint::new(1, 6),
|
||
DisplayPoint::new(2, 8)..DisplayPoint::new(2, 8),
|
||
DisplayPoint::new(3, 12)..DisplayPoint::new(3, 12),
|
||
]
|
||
);
|
||
});
|
||
|
||
editor.update(&mut app, |view, ctx| {
|
||
view.select_ranges(vec![DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1)], ctx)
|
||
.unwrap();
|
||
view.change_selections(ctx, |editor_model, ctx| {
|
||
editor_model.cursor_line_end(true, ctx);
|
||
});
|
||
});
|
||
|
||
editor.read(&app, |view, app| {
|
||
assert_eq!(
|
||
view.selected_ranges(app),
|
||
vec![DisplayPoint::new(0, 0)..DisplayPoint::new(0, 4),]
|
||
);
|
||
});
|
||
|
||
editor.update(&mut app, |view, ctx| {
|
||
view.select_ranges(vec![DisplayPoint::new(0, 3)..DisplayPoint::new(0, 1)], ctx)
|
||
.unwrap();
|
||
view.change_selections(ctx, |editor_model, ctx| {
|
||
editor_model.cursor_line_end(true, ctx);
|
||
});
|
||
});
|
||
|
||
editor.read(&app, |view, app| {
|
||
assert_eq!(
|
||
view.selected_ranges(app),
|
||
vec![DisplayPoint::new(0, 3)..DisplayPoint::new(0, 4),]
|
||
);
|
||
});
|
||
})
|
||
}
|
||
|
||
#[test]
|
||
fn test_clear_lines() -> Result<()> {
|
||
App::test((), |mut app| async move {
|
||
initialize_app(&mut app);
|
||
|
||
let (_, view) = app.add_window(WindowStyle::NotStealFocus, |ctx| {
|
||
EditorView::new_with_base_text(sample_text(6, 6), Default::default(), ctx)
|
||
});
|
||
|
||
view.update(&mut app, |view, ctx| {
|
||
view.select_ranges(
|
||
vec![
|
||
DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1),
|
||
DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3),
|
||
DisplayPoint::new(1, 4)..DisplayPoint::new(1, 4),
|
||
DisplayPoint::new(5, 0)..DisplayPoint::new(5, 0),
|
||
DisplayPoint::new(5, 2)..DisplayPoint::new(5, 2),
|
||
],
|
||
ctx,
|
||
)?;
|
||
view.clear_lines(ctx);
|
||
assert_eq!(view.buffer_text(ctx), "\ncccccc\ndddddd\neeeeee\n");
|
||
|
||
view.undo(ctx);
|
||
assert_eq!(
|
||
view.buffer_text(ctx),
|
||
"aaaaaa\nbbbbbb\ncccccc\ndddddd\neeeeee\nffffff"
|
||
);
|
||
view.redo(ctx);
|
||
assert_eq!(view.buffer_text(ctx), "\ncccccc\ndddddd\neeeeee\n");
|
||
|
||
Ok::<(), Error>(())
|
||
})?;
|
||
|
||
Ok(())
|
||
})
|
||
}
|
||
|
||
#[test]
|
||
fn test_clear_and_copy_lines() -> Result<()> {
|
||
App::test((), |mut app| async move {
|
||
initialize_app(&mut app);
|
||
|
||
let (_, view) = app.add_window(WindowStyle::NotStealFocus, |ctx| {
|
||
EditorView::new_with_base_text(sample_text(6, 6), Default::default(), ctx)
|
||
});
|
||
|
||
view.update(&mut app, |view, ctx| {
|
||
view.select_ranges(
|
||
vec![
|
||
DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1),
|
||
DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3),
|
||
DisplayPoint::new(1, 4)..DisplayPoint::new(1, 4),
|
||
DisplayPoint::new(5, 0)..DisplayPoint::new(5, 0),
|
||
DisplayPoint::new(5, 2)..DisplayPoint::new(5, 2),
|
||
],
|
||
ctx,
|
||
)?;
|
||
view.clear_and_copy_lines(ctx);
|
||
assert_eq!(view.buffer_text(ctx), "\n\ncccccc\ndddddd\neeeeee\n");
|
||
|
||
view.undo(ctx);
|
||
assert_eq!(
|
||
view.buffer_text(ctx),
|
||
"aaaaaa\nbbbbbb\ncccccc\ndddddd\neeeeee\nffffff"
|
||
);
|
||
view.redo(ctx);
|
||
assert_eq!(view.buffer_text(ctx), "\n\ncccccc\ndddddd\neeeeee\n");
|
||
|
||
Ok::<(), Error>(())
|
||
})?;
|
||
|
||
view.read(&app, |view, _| {
|
||
assert_eq!(view.internal_clipboard, "aaaaaa\nbbbbbb\nffffff");
|
||
Result::<()>::Ok(())
|
||
})?;
|
||
|
||
Ok(())
|
||
})
|
||
}
|
||
|
||
#[test]
|
||
fn test_cut_word_left() {
|
||
App::test((), |mut app| async move {
|
||
initialize_app(&mut app);
|
||
|
||
let (_, view) = app.add_window(WindowStyle::NotStealFocus, |ctx| {
|
||
let mut editor =
|
||
EditorView::new_with_base_text("word1 word2 word3", Default::default(), ctx);
|
||
editor.set_shell_family(ShellFamily::Posix);
|
||
editor
|
||
});
|
||
|
||
view.update(&mut app, |view, ctx| {
|
||
view.move_to_buffer_end(ctx);
|
||
// Cutting a word (Ctrl-W) should use the internal shell clipboard
|
||
// (sometimes called the kill ring), not the system clipboard.
|
||
view.cut_word_left(ctx);
|
||
assert_eq!(view.buffer_text(ctx), "word1 word2 ");
|
||
});
|
||
|
||
view.update(&mut app, |view, ctx| {
|
||
// Verify the clipboard states in a separate update, so that the
|
||
// shell cut action is dispatched.
|
||
assert_eq!(view.internal_clipboard, "word3");
|
||
assert!(view.clipboard_content(ctx).is_empty());
|
||
|
||
// Yanking should then paste the cut word from the shell clipboard.
|
||
view.yank(ctx);
|
||
assert_eq!(view.buffer_text(ctx), "word1 word2 word3");
|
||
assert_eq!(view.internal_clipboard, "word3");
|
||
});
|
||
|
||
view.update(&mut app, |view, ctx| {
|
||
// Cutting should also be undoable / redoable.
|
||
view.cut_word_left(ctx);
|
||
assert_eq!(view.buffer_text(ctx), "word1 word2 ");
|
||
view.undo(ctx);
|
||
assert_eq!(view.buffer_text(ctx), "word1 word2 word3");
|
||
view.redo(ctx);
|
||
assert_eq!(view.buffer_text(ctx), "word1 word2 ");
|
||
});
|
||
})
|
||
}
|
||
|
||
#[test]
|
||
fn test_cut_word_right() {
|
||
App::test((), |mut app| async move {
|
||
initialize_app(&mut app);
|
||
|
||
let (_, view) = app.add_window(WindowStyle::NotStealFocus, |ctx| {
|
||
EditorView::new_with_base_text("word1 word2 word3", Default::default(), ctx)
|
||
});
|
||
|
||
view.update(&mut app, |view, ctx| {
|
||
view.move_to_buffer_start(ctx);
|
||
// Cutting a word to the right (Alt-D) should use the internal
|
||
//shell clipboard, not the system clipboard.
|
||
view.cut_word_right(ctx);
|
||
assert_eq!(view.buffer_text(ctx), " word2 word3");
|
||
});
|
||
|
||
view.update(&mut app, |view, ctx| {
|
||
// Verify the clipboard states in a separate update, so that the
|
||
// shell cut action is dispatched.
|
||
assert_eq!(view.internal_clipboard, "word1");
|
||
assert!(view.clipboard_content(ctx).is_empty());
|
||
|
||
// Yanking should then paste the cut word from the shell clipboard.
|
||
view.yank(ctx);
|
||
assert_eq!(view.buffer_text(ctx), "word1 word2 word3");
|
||
assert_eq!(view.internal_clipboard, "word1");
|
||
});
|
||
|
||
view.update(&mut app, |view, ctx| {
|
||
view.move_to_buffer_start(ctx);
|
||
|
||
// Cutting should also be undoable / redoable.
|
||
view.cut_word_right(ctx);
|
||
assert_eq!(view.buffer_text(ctx), " word2 word3");
|
||
view.undo(ctx);
|
||
assert_eq!(view.buffer_text(ctx), "word1 word2 word3");
|
||
view.redo(ctx);
|
||
assert_eq!(view.buffer_text(ctx), " word2 word3");
|
||
});
|
||
})
|
||
}
|
||
|
||
#[test]
|
||
fn test_delete_and_cut_all_right() -> Result<()> {
|
||
App::test((), |mut app| async move {
|
||
initialize_app(&mut app);
|
||
|
||
let (_, view) = app.add_window(WindowStyle::NotStealFocus, |ctx| {
|
||
EditorView::new_with_base_text(sample_text(6, 6), Default::default(), ctx)
|
||
});
|
||
|
||
view.update(&mut app, |view, ctx| {
|
||
view.select_ranges(
|
||
vec![
|
||
DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1),
|
||
DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3),
|
||
DisplayPoint::new(1, 4)..DisplayPoint::new(1, 4),
|
||
DisplayPoint::new(3, 5)..DisplayPoint::new(3, 5),
|
||
],
|
||
ctx,
|
||
)?;
|
||
view.delete_all(CutDirection::Right, true, ctx);
|
||
assert_eq!(
|
||
view.buffer_text(ctx),
|
||
"a\nbbbb\ncccccc\nddddd\neeeeee\nffffff"
|
||
);
|
||
|
||
view.undo(ctx);
|
||
assert_eq!(
|
||
view.buffer_text(ctx),
|
||
"aaaaaa\nbbbbbb\ncccccc\ndddddd\neeeeee\nffffff"
|
||
);
|
||
view.redo(ctx);
|
||
assert_eq!(
|
||
view.buffer_text(ctx),
|
||
"a\nbbbb\ncccccc\nddddd\neeeeee\nffffff"
|
||
);
|
||
|
||
Ok::<(), Error>(())
|
||
})?;
|
||
|
||
view.read(&app, |view, _| {
|
||
assert_eq!(view.internal_clipboard, "aaaaa\nbb\nd");
|
||
Result::<()>::Ok(())
|
||
})?;
|
||
|
||
Ok(())
|
||
})
|
||
}
|
||
|
||
#[test]
|
||
fn test_autosuggestions() -> Result<()> {
|
||
use galaxyui::text_layout::LayoutCache;
|
||
|
||
App::test((), |mut app| async move {
|
||
initialize_app(&mut app);
|
||
|
||
let layout_cache = LayoutCache::new();
|
||
let size = vec2f(f32::MAX, f32::MAX);
|
||
|
||
// Create a view with autosuggestion text "foo\nbar". At this point the editor should
|
||
// have "bazz" within its buffer and "foo\nbar" as an autosuggestion.
|
||
let (_, view) = app.add_window(WindowStyle::NotStealFocus, |ctx| {
|
||
let mut view = EditorView::new_with_base_text("bazz", Default::default(), ctx);
|
||
view.set_autosuggestion(
|
||
"foo\nbar",
|
||
AutosuggestionLocation::EndOfBuffer,
|
||
AutosuggestionType::Command {
|
||
was_intelligent_autosuggestion: false,
|
||
},
|
||
ctx,
|
||
);
|
||
view
|
||
});
|
||
|
||
// The editor should have 2 lines
|
||
view.read(&app, |view, app| {
|
||
let snapshot = view.snapshot(app);
|
||
let layouts =
|
||
snapshot.layout_autosuggestion(0.0, app.font_cache(), &layout_cache, &size, false);
|
||
assert_eq!(layouts.len(), 2);
|
||
Result::<()>::Ok(())
|
||
})?;
|
||
|
||
// Insert "foo\n" into the buffer, the autosuggestion should now be a single line with
|
||
// the text "bar".
|
||
view.update(&mut app, |view, ctx| {
|
||
view.move_to_buffer_end(ctx);
|
||
view.user_insert("foo\n", ctx);
|
||
});
|
||
|
||
view.read(&app, |view, app| {
|
||
assert_eq!("bazzfoo\n", view.displayed_text(app));
|
||
|
||
let snapshot = view.snapshot(app);
|
||
let layouts =
|
||
snapshot.layout_autosuggestion(0.0, app.font_cache(), &layout_cache, &size, false);
|
||
assert_eq!(layouts.len(), 1);
|
||
|
||
assert_eq!(Some("bar"), view.current_autosuggestion_text());
|
||
Result::<()>::Ok(())
|
||
})?;
|
||
|
||
// Replace all the text in the buffer with "a"--the autosuggestion should no longer be
|
||
// visible.
|
||
view.update(&mut app, |view, ctx| {
|
||
view.select_all(ctx);
|
||
view.user_insert("a", ctx);
|
||
});
|
||
|
||
view.read(&app, |view, app| {
|
||
let snapshot = view.snapshot(app);
|
||
let layouts =
|
||
snapshot.layout_autosuggestion(0.0, app.font_cache(), &layout_cache, &size, false);
|
||
assert_eq!(layouts.len(), 0);
|
||
|
||
assert_eq!(None, view.current_autosuggestion_text());
|
||
Result::<()>::Ok(())
|
||
})?;
|
||
|
||
// Insert "bazz" back into the editor and then try to move right--the autosuggestion
|
||
// text should be inserted into the buffer.
|
||
view.update(&mut app, |view, ctx| {
|
||
view.select_all(ctx);
|
||
view.user_insert("bazz", ctx);
|
||
});
|
||
|
||
view.read(&app, |view, app| {
|
||
assert_eq!("bazz", view.displayed_text(app));
|
||
});
|
||
|
||
view.update(&mut app, |view, ctx| {
|
||
view.move_to_buffer_end(ctx);
|
||
view.move_right(/* stop at line end */ false, ctx);
|
||
});
|
||
|
||
view.read(&app, |view, app| {
|
||
let snapshot = view.snapshot(app);
|
||
let layouts =
|
||
snapshot.layout_autosuggestion(0.0, app.font_cache(), &layout_cache, &size, false);
|
||
assert_eq!(layouts.len(), 0);
|
||
|
||
assert_eq!(None, view.current_autosuggestion_text());
|
||
assert_eq!("bazzfoo\nbar", view.displayed_text(app));
|
||
|
||
Result::<()>::Ok(())
|
||
})?;
|
||
|
||
Ok(())
|
||
})
|
||
}
|
||
|
||
#[test]
|
||
fn test_partial_autosuggestion() -> Result<()> {
|
||
App::test((), |mut app| async move {
|
||
initialize_app(&mut app);
|
||
|
||
// Create a view with autosuggestion text "foo\nbar". At this point the editor should
|
||
// have "bazz" within its buffer and "foo\nbar" as an autosuggestion.
|
||
let (_, view) = app.add_window(WindowStyle::NotStealFocus, |ctx| {
|
||
let mut view = EditorView::new_with_base_text("bazz", Default::default(), ctx);
|
||
view.set_autosuggestion(
|
||
"foo\nbar",
|
||
AutosuggestionLocation::EndOfBuffer,
|
||
AutosuggestionType::Command {
|
||
was_intelligent_autosuggestion: false,
|
||
},
|
||
ctx,
|
||
);
|
||
view
|
||
});
|
||
|
||
// Case 1: Test out completion within a word and completion including newlines
|
||
view.update(&mut app, |view, ctx| {
|
||
view.move_to_buffer_end(ctx);
|
||
view.insert_autosuggestion(move_single_word, ctx);
|
||
});
|
||
|
||
view.read(&app, |view, app| {
|
||
assert_eq!("bazzfoo", view.displayed_text(app));
|
||
Result::<()>::Ok(())
|
||
})?;
|
||
|
||
view.update(&mut app, |view, ctx| {
|
||
view.insert_autosuggestion(move_single_word, ctx);
|
||
});
|
||
|
||
view.read(&app, |view, app| {
|
||
assert_eq!("bazzfoo\nbar", view.displayed_text(app));
|
||
Result::<()>::Ok(())
|
||
})?;
|
||
|
||
// Case 2: Test out word boundaries in multibyte characters
|
||
view.update(&mut app, |view, ctx| {
|
||
view.select_all(ctx);
|
||
view.user_insert("test", ctx);
|
||
view.set_autosuggestion(
|
||
" {восибing}",
|
||
AutosuggestionLocation::EndOfBuffer,
|
||
AutosuggestionType::Command {
|
||
was_intelligent_autosuggestion: false,
|
||
},
|
||
ctx,
|
||
);
|
||
});
|
||
|
||
view.update(&mut app, |view, ctx| {
|
||
view.insert_autosuggestion(move_single_word, ctx);
|
||
});
|
||
view.read(&app, |view, app| {
|
||
assert_eq!("test {восибing", view.displayed_text(app));
|
||
Result::<()>::Ok(())
|
||
})?;
|
||
|
||
view.update(&mut app, |view, ctx| {
|
||
view.insert_autosuggestion(move_single_word, ctx);
|
||
});
|
||
view.read(&app, |view, app| {
|
||
assert_eq!("test {восибing}", view.displayed_text(app));
|
||
Result::<()>::Ok(())
|
||
})?;
|
||
|
||
// Case 3: Test out word boundaries within same line
|
||
// Using example supplied by user in Github issue #488
|
||
view.update(&mut app, |view, ctx| {
|
||
view.select_all(ctx);
|
||
view.user_insert("grep", ctx);
|
||
view.set_autosuggestion(
|
||
" --files-without-match \"git\" ~/.oh-my-zsh/themes/*",
|
||
AutosuggestionLocation::EndOfBuffer,
|
||
AutosuggestionType::Command {
|
||
was_intelligent_autosuggestion: false,
|
||
},
|
||
ctx,
|
||
);
|
||
});
|
||
|
||
view.update(&mut app, |view, ctx| {
|
||
view.insert_autosuggestion(move_single_word, ctx);
|
||
});
|
||
view.read(&app, |view, app| {
|
||
assert_eq!("grep --files", view.displayed_text(app));
|
||
Result::<()>::Ok(())
|
||
})?;
|
||
|
||
view.update(&mut app, |view, ctx| {
|
||
view.insert_autosuggestion(move_single_word, ctx);
|
||
});
|
||
view.read(&app, |view, app| {
|
||
assert_eq!("grep --files-without", view.displayed_text(app));
|
||
Result::<()>::Ok(())
|
||
})?;
|
||
|
||
view.update(&mut app, |view, ctx| {
|
||
view.insert_autosuggestion(move_single_word, ctx);
|
||
});
|
||
view.read(&app, |view, app| {
|
||
assert_eq!("grep --files-without-match", view.displayed_text(app));
|
||
Result::<()>::Ok(())
|
||
})?;
|
||
|
||
view.update(&mut app, |view, ctx| {
|
||
view.insert_autosuggestion(move_single_word, ctx);
|
||
});
|
||
view.read(&app, |view, app| {
|
||
assert_eq!("grep --files-without-match \"git", view.displayed_text(app));
|
||
Result::<()>::Ok(())
|
||
})?;
|
||
|
||
view.update(&mut app, |view, ctx| {
|
||
view.insert_autosuggestion(move_single_word, ctx);
|
||
});
|
||
view.read(&app, |view, app| {
|
||
assert_eq!(
|
||
"grep --files-without-match \"git\" ~/.oh",
|
||
view.displayed_text(app)
|
||
);
|
||
Result::<()>::Ok(())
|
||
})?;
|
||
|
||
view.update(&mut app, |view, ctx| {
|
||
view.insert_autosuggestion(move_single_word, ctx);
|
||
});
|
||
view.read(&app, |view, app| {
|
||
assert_eq!(
|
||
"grep --files-without-match \"git\" ~/.oh-my",
|
||
view.displayed_text(app)
|
||
);
|
||
Result::<()>::Ok(())
|
||
})?;
|
||
|
||
view.update(&mut app, |view, ctx| {
|
||
view.insert_autosuggestion(move_single_word, ctx);
|
||
});
|
||
view.read(&app, |view, app| {
|
||
assert_eq!(
|
||
"grep --files-without-match \"git\" ~/.oh-my-zsh",
|
||
view.displayed_text(app)
|
||
);
|
||
Result::<()>::Ok(())
|
||
})?;
|
||
|
||
view.update(&mut app, |view, ctx| {
|
||
view.insert_autosuggestion(move_single_word, ctx);
|
||
});
|
||
view.read(&app, |view, app| {
|
||
assert_eq!(
|
||
"grep --files-without-match \"git\" ~/.oh-my-zsh/themes",
|
||
view.displayed_text(app)
|
||
);
|
||
Result::<()>::Ok(())
|
||
})?;
|
||
|
||
view.update(&mut app, |view, ctx| {
|
||
view.insert_autosuggestion(move_single_word, ctx);
|
||
});
|
||
view.read(&app, |view, app| {
|
||
assert_eq!(
|
||
"grep --files-without-match \"git\" ~/.oh-my-zsh/themes/*",
|
||
view.displayed_text(app)
|
||
);
|
||
Result::<()>::Ok(())
|
||
})?;
|
||
|
||
Ok(())
|
||
})
|
||
}
|
||
|
||
#[test]
|
||
fn test_placeholder_text() {
|
||
|
||
App::test((), |mut app| async move {
|
||
initialize_app(&mut app);
|
||
|
||
let layout_cache = LayoutCache::new();
|
||
|
||
// Create a view with placeholder text
|
||
let (_, view) = app.add_window(WindowStyle::NotStealFocus, |ctx| {
|
||
let mut view = EditorView::new(Default::default(), ctx);
|
||
view.set_placeholder_text("sample instruction\ntwo lines", ctx);
|
||
view
|
||
});
|
||
|
||
let no_constraint_size = Vector2F::new(f32::MAX, f32::MAX);
|
||
|
||
// The editor should have 2 line, from placeholder text (no indent)
|
||
view.read(&app, |view, app| {
|
||
let snapshot = view.snapshot(app);
|
||
let default_placeholder = snapshot.placeholder_texts.get("").unwrap();
|
||
let layouts = snapshot.layout_placeholder_text(
|
||
default_placeholder,
|
||
0.,
|
||
app.font_cache(),
|
||
&layout_cache,
|
||
&no_constraint_size,
|
||
false,
|
||
);
|
||
assert_eq!(layouts.len(), 2);
|
||
assert!(view.displayed_text(app).is_empty());
|
||
});
|
||
|
||
// Call focus, still there
|
||
view.update(&mut app, |_view, ctx| {
|
||
ctx.focus_self();
|
||
});
|
||
|
||
view.read(&app, |view, app| {
|
||
let snapshot = view.snapshot(app);
|
||
let default_placeholder = snapshot.placeholder_texts.get("").unwrap();
|
||
let layouts = snapshot.layout_placeholder_text(
|
||
default_placeholder,
|
||
0.,
|
||
app.font_cache(),
|
||
&layout_cache,
|
||
&no_constraint_size,
|
||
false,
|
||
);
|
||
assert_eq!(layouts.len(), 2);
|
||
assert!(view.displayed_text(app).is_empty());
|
||
});
|
||
|
||
// Test with indent > 0
|
||
view.read(&app, |view, app| {
|
||
let snapshot = view.snapshot(app);
|
||
let default_placeholder = snapshot.placeholder_texts.get("").unwrap();
|
||
let layouts = snapshot.layout_placeholder_text(
|
||
default_placeholder,
|
||
50.,
|
||
app.font_cache(),
|
||
&layout_cache,
|
||
&no_constraint_size,
|
||
false,
|
||
);
|
||
assert_eq!(layouts.len(), 2);
|
||
assert!(view.displayed_text(app).is_empty());
|
||
});
|
||
|
||
// Placeholder text cannot be accepted (while autosuggestion text can)
|
||
view.update(&mut app, |view, ctx| {
|
||
view.move_to_buffer_end(ctx);
|
||
view.move_right(/* stop at line end */ false, ctx);
|
||
});
|
||
|
||
view.read(&app, |view, app| {
|
||
assert!(view.displayed_text(app).is_empty());
|
||
});
|
||
|
||
// Shows autosuggestion instead if available
|
||
view.update(&mut app, |view, ctx| {
|
||
view.set_autosuggestion(
|
||
"single line",
|
||
AutosuggestionLocation::EndOfBuffer,
|
||
AutosuggestionType::Command {
|
||
was_intelligent_autosuggestion: false,
|
||
},
|
||
ctx,
|
||
);
|
||
});
|
||
|
||
view.read(&app, |view, app| {
|
||
assert!(view.displayed_text(app).is_empty());
|
||
});
|
||
|
||
view.update(&mut app, |view, ctx| {
|
||
view.move_to_buffer_end(ctx);
|
||
view.move_right(/* stop at line end */ false, ctx);
|
||
});
|
||
|
||
view.read(&app, |view, app| {
|
||
assert_eq!("single line", view.displayed_text(app));
|
||
});
|
||
|
||
view.update(&mut app, |view, ctx| {
|
||
view.clear_autosuggestion(ctx);
|
||
view.clear_buffer_and_reset_undo_stack(ctx);
|
||
});
|
||
|
||
// Type out matching/anything overwrites placeholder text
|
||
view.update(&mut app, |view, ctx| {
|
||
view.user_insert("ls", ctx);
|
||
});
|
||
|
||
view.read(&app, |view, app| {
|
||
assert_eq!("ls", view.displayed_text(app));
|
||
});
|
||
});
|
||
}
|
||
|
||
#[test]
|
||
fn test_delete_newline_and_end_of_line() -> Result<()> {
|
||
App::test((), |mut app| async move {
|
||
initialize_app(&mut app);
|
||
|
||
let (_, view) = app.add_window(WindowStyle::NotStealFocus, |ctx| {
|
||
EditorView::new_with_base_text("abcde\nfghijk", Default::default(), ctx)
|
||
});
|
||
|
||
view.update(&mut app, |view, ctx| {
|
||
view.select_ranges(
|
||
vec![
|
||
// Selection should get deleted.
|
||
DisplayPoint::new(0, 1)..DisplayPoint::new(0, 2),
|
||
// Single cursor should forward-delete.
|
||
DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3),
|
||
// Single cursor at end of line should delete newline.
|
||
DisplayPoint::new(0, 5)..DisplayPoint::new(0, 5),
|
||
// Cursor at very end of buffer should no-op.
|
||
DisplayPoint::new(1, 6)..DisplayPoint::new(1, 6),
|
||
],
|
||
ctx,
|
||
)?;
|
||
view.delete(ctx);
|
||
assert_eq!(view.buffer_text(ctx), "acefghijk");
|
||
|
||
let model = view.model().as_ref(ctx);
|
||
let mut selections = view
|
||
.selections(ctx)
|
||
.iter()
|
||
.map(|selection| {
|
||
assert!(
|
||
model.is_cursor_only(selection, ctx),
|
||
"Should only have cursors now"
|
||
);
|
||
selection
|
||
.start()
|
||
.to_display_point(model.display_map(ctx), ctx)
|
||
.unwrap()
|
||
})
|
||
.collect::<Vec<_>>();
|
||
selections.sort();
|
||
assert_eq!(
|
||
selections,
|
||
vec![
|
||
DisplayPoint::new(0, 1),
|
||
DisplayPoint::new(0, 2),
|
||
DisplayPoint::new(0, 3),
|
||
DisplayPoint::new(0, 9),
|
||
],
|
||
"Expected cursors at particular locations"
|
||
);
|
||
|
||
view.undo(ctx);
|
||
assert_eq!(view.buffer_text(ctx), "abcde\nfghijk");
|
||
let selections = view
|
||
.selections(ctx)
|
||
.iter()
|
||
.map(|selection| {
|
||
selection
|
||
.start()
|
||
.to_display_point(view.editor_model.as_ref(ctx).display_map(ctx), ctx)
|
||
.unwrap()
|
||
..selection
|
||
.end()
|
||
.to_display_point(view.editor_model.as_ref(ctx).display_map(ctx), ctx)
|
||
.unwrap()
|
||
})
|
||
.collect::<Vec<_>>();
|
||
assert_eq!(
|
||
selections,
|
||
vec![
|
||
DisplayPoint::new(0, 1)..DisplayPoint::new(0, 2),
|
||
DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3),
|
||
DisplayPoint::new(0, 5)..DisplayPoint::new(0, 5),
|
||
DisplayPoint::new(1, 6)..DisplayPoint::new(1, 6),
|
||
],
|
||
"Expected selections to be restored to the original state"
|
||
);
|
||
|
||
view.redo(ctx);
|
||
assert_eq!(view.buffer_text(ctx), "acefghijk");
|
||
|
||
let model = view.model().as_ref(ctx);
|
||
let mut selections = view
|
||
.selections(ctx)
|
||
.iter()
|
||
.map(|selection| {
|
||
assert!(
|
||
model.is_cursor_only(selection, ctx),
|
||
"Should only have cursors now"
|
||
);
|
||
selection
|
||
.start()
|
||
.to_display_point(model.display_map(ctx), ctx)
|
||
.unwrap()
|
||
})
|
||
.collect::<Vec<_>>();
|
||
selections.sort();
|
||
assert_eq!(
|
||
selections,
|
||
vec![
|
||
DisplayPoint::new(0, 1),
|
||
DisplayPoint::new(0, 2),
|
||
DisplayPoint::new(0, 3),
|
||
DisplayPoint::new(0, 9),
|
||
],
|
||
"Expected cursors at particular locations"
|
||
);
|
||
Ok::<(), Error>(())
|
||
})?;
|
||
|
||
Ok(())
|
||
})
|
||
}
|
||
|
||
#[test]
|
||
fn test_delete_empty_buffer() -> Result<()> {
|
||
App::test((), |mut app| async move {
|
||
initialize_app(&mut app);
|
||
|
||
let (_, view) = app.add_window(WindowStyle::NotStealFocus, |ctx| {
|
||
EditorView::new(Default::default(), ctx)
|
||
});
|
||
|
||
view.update(&mut app, |view, ctx| {
|
||
view.select_ranges(vec![DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)], ctx)?;
|
||
view.delete(ctx);
|
||
assert_eq!(view.buffer_text(ctx), "".to_string());
|
||
|
||
view.undo(ctx);
|
||
assert_eq!(view.buffer_text(ctx), "".to_string());
|
||
view.redo(ctx);
|
||
assert_eq!(view.buffer_text(ctx), "".to_string());
|
||
|
||
Ok::<(), Error>(())
|
||
})?;
|
||
|
||
Ok(())
|
||
})
|
||
}
|
||
|
||
#[test]
|
||
fn test_delete_end_of_buffer() -> Result<()> {
|
||
App::test((), |mut app| async move {
|
||
initialize_app(&mut app);
|
||
|
||
let (_, view) = app.add_window(WindowStyle::NotStealFocus, |ctx| {
|
||
EditorView::new_with_base_text("abc", Default::default(), ctx)
|
||
});
|
||
|
||
view.update(&mut app, |view, ctx| {
|
||
view.select_ranges(vec![DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3)], ctx)?;
|
||
view.delete(ctx);
|
||
assert_eq!(view.buffer_text(ctx), "abc".to_string());
|
||
|
||
view.undo(ctx);
|
||
assert_eq!(view.buffer_text(ctx), "abc".to_string());
|
||
view.redo(ctx);
|
||
assert_eq!(view.buffer_text(ctx), "abc".to_string());
|
||
|
||
Ok::<(), Error>(())
|
||
})?;
|
||
|
||
Ok(())
|
||
})
|
||
}
|
||
|
||
#[test]
|
||
fn test_fold() -> Result<()> {
|
||
App::test((), |mut app| async move {
|
||
initialize_app(&mut app);
|
||
|
||
let base_text = "
|
||
impl Foo {
|
||
// Hello!
|
||
|
||
fn a() {
|
||
1
|
||
}
|
||
|
||
fn b() {
|
||
2
|
||
}
|
||
|
||
fn c() {
|
||
3
|
||
}
|
||
}
|
||
"
|
||
.unindent();
|
||
|
||
let (_, view) = app.add_window(WindowStyle::NotStealFocus, |ctx| {
|
||
EditorView::new_with_base_text(base_text, Default::default(), ctx)
|
||
});
|
||
|
||
view.update(&mut app, |view, ctx| {
|
||
view.select_ranges(Some(DisplayPoint::new(8, 0)..DisplayPoint::new(12, 0)), ctx)?;
|
||
view.fold(ctx);
|
||
assert_eq!(
|
||
view.displayed_text(ctx),
|
||
"
|
||
impl Foo {
|
||
// Hello!
|
||
|
||
fn a() {
|
||
1
|
||
}
|
||
|
||
fn b() {…
|
||
}
|
||
|
||
fn c() {…
|
||
}
|
||
}
|
||
"
|
||
.unindent(),
|
||
);
|
||
|
||
view.fold(ctx);
|
||
assert_eq!(
|
||
view.displayed_text(ctx),
|
||
"
|
||
impl Foo {…
|
||
}
|
||
"
|
||
.unindent(),
|
||
);
|
||
|
||
view.unfold(ctx);
|
||
assert_eq!(
|
||
view.displayed_text(ctx),
|
||
"
|
||
impl Foo {
|
||
// Hello!
|
||
|
||
fn a() {
|
||
1
|
||
}
|
||
|
||
fn b() {…
|
||
}
|
||
|
||
fn c() {…
|
||
}
|
||
}
|
||
"
|
||
.unindent(),
|
||
);
|
||
|
||
view.unfold(ctx);
|
||
assert_eq!(view.displayed_text(ctx), view.buffer_text(ctx));
|
||
|
||
Ok::<(), Error>(())
|
||
})?;
|
||
|
||
Ok(())
|
||
})
|
||
}
|
||
|
||
#[test]
|
||
fn test_cursor_top_cursor_bottom() -> Result<()> {
|
||
App::test((), |mut app| async move {
|
||
initialize_app(&mut app);
|
||
|
||
let (_, view) = app.add_window(WindowStyle::NotStealFocus, |ctx| {
|
||
EditorView::new_with_base_text(sample_text(6, 6), Default::default(), ctx)
|
||
});
|
||
|
||
view.update(&mut app, |view, ctx| {
|
||
view.select_ranges(
|
||
vec![
|
||
DisplayPoint::new(0, 1)..DisplayPoint::new(0, 2),
|
||
DisplayPoint::new(3, 5)..DisplayPoint::new(3, 5),
|
||
],
|
||
ctx,
|
||
)?;
|
||
view.cursor_top(ctx);
|
||
assert_eq!(
|
||
view.selected_ranges(ctx),
|
||
&[DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)]
|
||
);
|
||
view.cursor_bottom(ctx);
|
||
assert_eq!(
|
||
view.selected_ranges(ctx),
|
||
&[DisplayPoint::new(5, 6)..DisplayPoint::new(5, 6)]
|
||
);
|
||
Ok::<(), Error>(())
|
||
})?;
|
||
Ok(())
|
||
})
|
||
}
|
||
|
||
#[test]
|
||
fn test_move_cursor() -> Result<()> {
|
||
App::test((), |mut app| async move {
|
||
initialize_app(&mut app);
|
||
|
||
let (_, view) = app.add_window(WindowStyle::NotStealFocus, |ctx| {
|
||
EditorView::new_with_base_text(sample_text(6, 6), Default::default(), ctx)
|
||
});
|
||
|
||
view.update(&mut app, |view, ctx| {
|
||
view.edit(
|
||
ctx,
|
||
Edits::new().with_update_buffer(
|
||
PlainTextEditorViewAction::Tab,
|
||
EditOrigin::UserInitiated,
|
||
|model, ctx| {
|
||
model
|
||
.buffer_edit(
|
||
vec![
|
||
Point::new(1, 0)..Point::new(1, 0),
|
||
Point::new(1, 1)..Point::new(1, 1),
|
||
],
|
||
"\t",
|
||
ctx,
|
||
)
|
||
.unwrap()
|
||
},
|
||
),
|
||
)
|
||
});
|
||
|
||
view.update(&mut app, |view, ctx| {
|
||
view.move_down(ctx);
|
||
assert_eq!(
|
||
view.selected_ranges(ctx),
|
||
&[DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)]
|
||
);
|
||
view.move_right(/* stop at line end */ false, ctx);
|
||
view.model().update(ctx, |editor_model, ctx| {
|
||
let a11y_label = editor_model
|
||
.delta_for_a11y(ByteOffset::from(7)..ByteOffset::from(7), false, ctx)
|
||
.value;
|
||
assert_eq!(a11y_label, "tab");
|
||
});
|
||
assert_eq!(
|
||
view.selected_ranges(ctx),
|
||
&[DisplayPoint::new(1, 4)..DisplayPoint::new(1, 4)]
|
||
);
|
||
Ok::<(), Error>(())
|
||
})?;
|
||
|
||
view.update(&mut app, |view, ctx| {
|
||
view.select_ranges(
|
||
vec![
|
||
DisplayPoint::new(0, 0)..DisplayPoint::new(1, 2),
|
||
DisplayPoint::new(3, 3)..DisplayPoint::new(3, 3),
|
||
],
|
||
ctx,
|
||
)?;
|
||
view.cursor_home(ctx);
|
||
assert_eq!(
|
||
view.selected_ranges(ctx),
|
||
&[
|
||
DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0),
|
||
DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0)
|
||
]
|
||
);
|
||
view.select_ranges(
|
||
vec![
|
||
DisplayPoint::new(3, 3)..DisplayPoint::new(3, 3),
|
||
DisplayPoint::new(4, 4)..DisplayPoint::new(4, 2),
|
||
],
|
||
ctx,
|
||
)?;
|
||
view.cursor_end(ctx);
|
||
assert_eq!(
|
||
view.selected_ranges(ctx),
|
||
&[
|
||
DisplayPoint::new(3, 6)..DisplayPoint::new(3, 6),
|
||
DisplayPoint::new(4, 6)..DisplayPoint::new(4, 6),
|
||
]
|
||
);
|
||
Ok::<(), Error>(())
|
||
})?;
|
||
|
||
view.update(&mut app, |view, ctx| {
|
||
view.edit(
|
||
ctx,
|
||
Edits::new().with_update_buffer(
|
||
PlainTextEditorViewAction::Tab,
|
||
EditOrigin::UserInitiated,
|
||
|model, ctx| {
|
||
model
|
||
.buffer_edit(vec![Point::new(3, 0)..Point::new(3, 0)], " ", ctx)
|
||
.unwrap()
|
||
},
|
||
),
|
||
)
|
||
});
|
||
|
||
view.update(&mut app, |view, ctx| {
|
||
view.select_ranges(vec![DisplayPoint::new(3, 5)..DisplayPoint::new(3, 6)], ctx)?;
|
||
view.cursor_home(ctx);
|
||
assert_eq!(
|
||
view.selected_ranges(ctx),
|
||
&[DisplayPoint::new(3, 4)..DisplayPoint::new(3, 4)]
|
||
);
|
||
view.cursor_home(ctx);
|
||
assert_eq!(
|
||
view.selected_ranges(ctx),
|
||
&[DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0)]
|
||
);
|
||
Ok::<(), Error>(())
|
||
})?;
|
||
|
||
Ok(())
|
||
})
|
||
}
|
||
|
||
#[test]
|
||
fn test_move_forward_one_word() -> Result<()> {
|
||
App::test((), |mut app| async move {
|
||
initialize_app(&mut app);
|
||
|
||
let base_text = r"
|
||
aaa bbb..ccc
|
||
ddd eee"
|
||
.unindent();
|
||
|
||
let (_, view) = app.add_window(WindowStyle::NotStealFocus, |ctx| {
|
||
EditorView::new_with_base_text(base_text, Default::default(), ctx)
|
||
});
|
||
|
||
view.update(&mut app, |view, ctx| {
|
||
view.select_ranges(vec![DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1)], ctx)?;
|
||
view.cursor_forward_one_word(false /* select */, ctx);
|
||
assert_eq!(
|
||
view.selected_ranges(ctx),
|
||
&[DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3)]
|
||
);
|
||
view.cursor_forward_one_word(false /* select */, ctx);
|
||
assert_eq!(
|
||
view.selected_ranges(ctx),
|
||
&[DisplayPoint::new(0, 7)..DisplayPoint::new(0, 7)]
|
||
);
|
||
view.cursor_forward_one_word(false /* select */, ctx);
|
||
assert_eq!(
|
||
view.selected_ranges(ctx),
|
||
&[DisplayPoint::new(0, 12)..DisplayPoint::new(0, 12)]
|
||
);
|
||
view.cursor_forward_one_word(false /* select */, ctx);
|
||
assert_eq!(
|
||
view.selected_ranges(ctx),
|
||
&[DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3)]
|
||
);
|
||
view.cursor_forward_one_word(false /* select */, ctx);
|
||
assert_eq!(
|
||
view.selected_ranges(ctx),
|
||
&[DisplayPoint::new(1, 7)..DisplayPoint::new(1, 7)]
|
||
);
|
||
view.cursor_forward_one_word(false /* select */, ctx);
|
||
assert_eq!(
|
||
view.selected_ranges(ctx),
|
||
&[DisplayPoint::new(1, 7)..DisplayPoint::new(1, 7)]
|
||
);
|
||
Ok::<(), Error>(())
|
||
})?;
|
||
|
||
Ok(())
|
||
})
|
||
}
|
||
|
||
#[test]
|
||
fn test_move_backward_one_word() -> Result<()> {
|
||
App::test((), |mut app| async move {
|
||
initialize_app(&mut app);
|
||
|
||
let base_text = r"
|
||
aaa bbb..ccc
|
||
ddd eee"
|
||
.unindent();
|
||
let (_, view) = app.add_window(WindowStyle::NotStealFocus, |ctx| {
|
||
EditorView::new_with_base_text(base_text, Default::default(), ctx)
|
||
});
|
||
|
||
view.update(&mut app, |view, ctx| {
|
||
view.select_ranges(vec![DisplayPoint::new(1, 7)..DisplayPoint::new(1, 7)], ctx)?;
|
||
view.cursor_backward_one_word(false /* select */, ctx);
|
||
assert_eq!(
|
||
view.selected_ranges(ctx),
|
||
&[DisplayPoint::new(1, 4)..DisplayPoint::new(1, 4)]
|
||
);
|
||
view.cursor_backward_one_word(false /* select */, ctx);
|
||
assert_eq!(
|
||
view.selected_ranges(ctx),
|
||
&[DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)]
|
||
);
|
||
view.cursor_backward_one_word(false /* select */, ctx);
|
||
assert_eq!(
|
||
view.selected_ranges(ctx),
|
||
&[DisplayPoint::new(0, 9)..DisplayPoint::new(0, 9)]
|
||
);
|
||
view.cursor_backward_one_word(false /* select */, ctx);
|
||
assert_eq!(
|
||
view.selected_ranges(ctx),
|
||
&[DisplayPoint::new(0, 4)..DisplayPoint::new(0, 4)]
|
||
);
|
||
view.cursor_backward_one_word(false /* select */, ctx);
|
||
assert_eq!(
|
||
view.selected_ranges(ctx),
|
||
&[DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)]
|
||
);
|
||
view.cursor_backward_one_word(false /* select */, ctx);
|
||
assert_eq!(
|
||
view.selected_ranges(ctx),
|
||
&[DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)]
|
||
);
|
||
Ok::<(), Error>(())
|
||
})?;
|
||
|
||
Ok(())
|
||
})
|
||
}
|
||
|
||
#[test]
|
||
fn test_move_forward_one_subword() -> Result<()> {
|
||
App::test((), |mut app| async move {
|
||
initialize_app(&mut app);
|
||
|
||
let base_text = r"
|
||
aaa_bbb..ccc
|
||
ddd_eEe"
|
||
.unindent();
|
||
let (_, view) = app.add_window(WindowStyle::NotStealFocus, |ctx| {
|
||
EditorView::new_with_base_text(base_text, Default::default(), ctx)
|
||
});
|
||
|
||
view.update(&mut app, |view, ctx| {
|
||
view.select_ranges(vec![DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1)], ctx)?;
|
||
view.cursor_forward_one_subword(false /* select */, ctx);
|
||
assert_eq!(
|
||
view.selected_ranges(ctx),
|
||
&[DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3)]
|
||
);
|
||
view.cursor_forward_one_subword(false /* select */, ctx);
|
||
assert_eq!(
|
||
view.selected_ranges(ctx),
|
||
&[DisplayPoint::new(0, 7)..DisplayPoint::new(0, 7)]
|
||
);
|
||
view.cursor_forward_one_subword(false /* select */, ctx);
|
||
assert_eq!(
|
||
view.selected_ranges(ctx),
|
||
&[DisplayPoint::new(0, 12)..DisplayPoint::new(0, 12)]
|
||
);
|
||
view.cursor_forward_one_subword(false /* select */, ctx);
|
||
assert_eq!(
|
||
view.selected_ranges(ctx),
|
||
&[DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3)]
|
||
);
|
||
view.cursor_forward_one_subword(false /* select */, ctx);
|
||
assert_eq!(
|
||
view.selected_ranges(ctx),
|
||
&[DisplayPoint::new(1, 5)..DisplayPoint::new(1, 5)]
|
||
);
|
||
view.cursor_forward_one_subword(false /* select */, ctx);
|
||
assert_eq!(
|
||
view.selected_ranges(ctx),
|
||
&[DisplayPoint::new(1, 7)..DisplayPoint::new(1, 7)]
|
||
);
|
||
view.cursor_forward_one_subword(false /* select */, ctx);
|
||
assert_eq!(
|
||
view.selected_ranges(ctx),
|
||
&[DisplayPoint::new(1, 7)..DisplayPoint::new(1, 7)]
|
||
);
|
||
Ok::<(), Error>(())
|
||
})?;
|
||
|
||
Ok(())
|
||
})
|
||
}
|
||
|
||
#[test]
|
||
fn test_move_backward_one_subword() -> Result<()> {
|
||
App::test((), |mut app| async move {
|
||
initialize_app(&mut app);
|
||
|
||
let base_text = r"
|
||
aaa_bbb..ccc
|
||
ddd_eEe"
|
||
.unindent();
|
||
let (_, view) = app.add_window(WindowStyle::NotStealFocus, |ctx| {
|
||
EditorView::new_with_base_text(base_text, Default::default(), ctx)
|
||
});
|
||
|
||
view.update(&mut app, |view, ctx| {
|
||
view.select_ranges(vec![DisplayPoint::new(1, 7)..DisplayPoint::new(1, 7)], ctx)?;
|
||
view.cursor_backward_one_subword(false /* select */, ctx);
|
||
assert_eq!(
|
||
view.selected_ranges(ctx),
|
||
&[DisplayPoint::new(1, 5)..DisplayPoint::new(1, 5)]
|
||
);
|
||
view.cursor_backward_one_subword(false /* select */, ctx);
|
||
assert_eq!(
|
||
view.selected_ranges(ctx),
|
||
&[DisplayPoint::new(1, 4)..DisplayPoint::new(1, 4)]
|
||
);
|
||
view.cursor_backward_one_subword(false /* select */, ctx);
|
||
assert_eq!(
|
||
view.selected_ranges(ctx),
|
||
&[DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)]
|
||
);
|
||
view.cursor_backward_one_subword(false /* select */, ctx);
|
||
assert_eq!(
|
||
view.selected_ranges(ctx),
|
||
&[DisplayPoint::new(0, 9)..DisplayPoint::new(0, 9)]
|
||
);
|
||
view.cursor_backward_one_subword(false /* select */, ctx);
|
||
assert_eq!(
|
||
view.selected_ranges(ctx),
|
||
&[DisplayPoint::new(0, 4)..DisplayPoint::new(0, 4)]
|
||
);
|
||
view.cursor_backward_one_subword(false /* select */, ctx);
|
||
assert_eq!(
|
||
view.selected_ranges(ctx),
|
||
&[DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)]
|
||
);
|
||
view.cursor_backward_one_subword(false /* select */, ctx);
|
||
assert_eq!(
|
||
view.selected_ranges(ctx),
|
||
&[DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)]
|
||
);
|
||
Ok::<(), Error>(())
|
||
})?;
|
||
|
||
Ok(())
|
||
})
|
||
}
|
||
|
||
#[test]
|
||
fn test_add_next_occurrence() {
|
||
App::test((), |mut app| async move {
|
||
initialize_app(&mut app);
|
||
|
||
app.add_window(WindowStyle::NotStealFocus, |ctx| {
|
||
let mut editor = EditorView::new_with_base_text(
|
||
"start word word hello word \n hi",
|
||
Default::default(),
|
||
ctx,
|
||
);
|
||
editor
|
||
.select_ranges(vec![DisplayPoint::new(0, 6)..DisplayPoint::new(0, 6)], ctx)
|
||
.unwrap();
|
||
// Select two occurrences of "word"
|
||
editor.add_next_occurrence(ctx);
|
||
editor.add_next_occurrence(ctx);
|
||
editor.backspace(ctx);
|
||
assert_eq!(editor.buffer_text(ctx), "start hello word \n hi");
|
||
|
||
editor.undo(ctx);
|
||
assert_eq!(editor.buffer_text(ctx), "start word word hello word \n hi");
|
||
editor.redo(ctx);
|
||
assert_eq!(editor.buffer_text(ctx), "start hello word \n hi");
|
||
|
||
editor.delete_all(CutDirection::Left, false /* cut */, ctx);
|
||
editor.user_insert("a", ctx);
|
||
assert_eq!(editor.buffer_text(ctx), "a hello word \n hi");
|
||
|
||
editor
|
||
});
|
||
});
|
||
|
||
App::test((), |mut app| async move {
|
||
initialize_app(&mut app);
|
||
|
||
app.add_window(WindowStyle::NotStealFocus, |ctx| {
|
||
let mut editor =
|
||
EditorView::new_with_base_text("word word word", Default::default(), ctx);
|
||
editor
|
||
.select_ranges(
|
||
vec![
|
||
DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2),
|
||
DisplayPoint::new(0, 8)..DisplayPoint::new(0, 8),
|
||
],
|
||
ctx,
|
||
)
|
||
.unwrap();
|
||
editor.add_next_occurrence(ctx);
|
||
editor.backspace(ctx);
|
||
assert_eq!(editor.buffer_text(ctx), " word");
|
||
|
||
editor.undo(ctx);
|
||
assert_eq!(editor.buffer_text(ctx), "word word word");
|
||
editor.redo(ctx);
|
||
assert_eq!(editor.buffer_text(ctx), " word");
|
||
|
||
editor
|
||
});
|
||
});
|
||
|
||
App::test((), |mut app| async move {
|
||
initialize_app(&mut app);
|
||
|
||
app.add_window(WindowStyle::NotStealFocus, |ctx| {
|
||
let mut editor =
|
||
EditorView::new_with_base_text("warpwordwarpwordwarp", Default::default(), ctx);
|
||
editor
|
||
.select_ranges(vec![DisplayPoint::new(0, 0)..DisplayPoint::new(0, 4)], ctx)
|
||
.unwrap();
|
||
// Select three occurrences of "word"
|
||
editor.add_next_occurrence(ctx);
|
||
editor.add_next_occurrence(ctx);
|
||
editor.backspace(ctx);
|
||
assert_eq!(editor.buffer_text(ctx), "wordword");
|
||
|
||
editor.undo(ctx);
|
||
assert_eq!(editor.buffer_text(ctx), "warpwordwarpwordwarp");
|
||
editor.redo(ctx);
|
||
assert_eq!(editor.buffer_text(ctx), "wordword");
|
||
|
||
editor
|
||
});
|
||
});
|
||
}
|
||
|
||
#[test]
|
||
fn test_undo_redo() {
|
||
App::test((), |mut app| async move {
|
||
initialize_app(&mut app);
|
||
|
||
let (_, view) = app.add_window(WindowStyle::NotStealFocus, |ctx| {
|
||
EditorView::new_with_base_text("abcd", Default::default(), ctx)
|
||
});
|
||
|
||
view.update(&mut app, |view, ctx| {
|
||
view.select_ranges(vec![DisplayPoint::new(0, 4)..DisplayPoint::new(0, 4)], ctx)
|
||
.unwrap();
|
||
view.backspace(ctx);
|
||
assert_eq!(view.buffer_text(ctx), "abc");
|
||
|
||
view.move_left(/* stop at line start */ false, ctx);
|
||
view.backspace(ctx);
|
||
assert_eq!(view.buffer_text(ctx), "ac".to_string());
|
||
|
||
view.backspace(ctx);
|
||
assert_eq!(view.buffer_text(ctx), "c".to_string());
|
||
});
|
||
|
||
// Here and below, we make sure to query the displayed text in the next
|
||
// event loop tick to ensure events were processed by the event loop
|
||
// before we read.
|
||
view.update(&mut app, |view, ctx| {
|
||
view.undo(ctx);
|
||
});
|
||
view.read(&app, |view, ctx| {
|
||
assert_eq!(view.buffer_text(ctx), "abc");
|
||
assert_eq!(view.displayed_text(ctx), "abc");
|
||
});
|
||
|
||
view.update(&mut app, |view, ctx| {
|
||
view.backspace(ctx);
|
||
assert_eq!(view.buffer_text(ctx), "ac");
|
||
});
|
||
|
||
view.update(&mut app, |view, ctx| {
|
||
// This do nothing instead of redoing to "c" because we have already
|
||
// made edits on the current state.
|
||
view.redo(ctx);
|
||
});
|
||
view.read(&app, |view, ctx| {
|
||
assert_eq!(view.buffer_text(ctx), "ac");
|
||
assert_eq!(view.displayed_text(ctx), "ac");
|
||
});
|
||
|
||
view.update(&mut app, |view, ctx| {
|
||
view.set_buffer_text_ignoring_undo("foofoofoo", ctx);
|
||
assert_eq!(view.buffer_text(ctx), "foofoofoo");
|
||
});
|
||
|
||
view.update(&mut app, |view, ctx| {
|
||
// Undoing here should remove the temporary replace and restore the last state
|
||
view.undo(ctx);
|
||
});
|
||
view.read(&app, |view, ctx| {
|
||
assert_eq!(view.buffer_text(ctx), "ac");
|
||
assert_eq!(view.displayed_text(ctx), "ac");
|
||
});
|
||
|
||
view.update(&mut app, |view, ctx| {
|
||
// Redo should be no-op as we should already be at the last active
|
||
// state in the undo stack.
|
||
view.redo(ctx);
|
||
});
|
||
view.read(&app, |view, ctx| {
|
||
assert_eq!(view.buffer_text(ctx), "ac");
|
||
assert_eq!(view.displayed_text(ctx), "ac");
|
||
});
|
||
|
||
view.update(&mut app, |view, ctx| {
|
||
view.set_buffer_text_ignoring_undo("foofoofoo", ctx);
|
||
view.backspace(ctx);
|
||
assert_eq!(view.buffer_text(ctx), "foofoofo");
|
||
});
|
||
|
||
view.update(&mut app, |view, ctx| {
|
||
// Temporary replace should be inserted into the undo stack after
|
||
// a real edit.
|
||
view.undo(ctx);
|
||
});
|
||
view.read(&app, |view, ctx| {
|
||
assert_eq!(view.buffer_text(ctx), "foofoofoo");
|
||
assert_eq!(view.displayed_text(ctx), "foofoofoo");
|
||
});
|
||
|
||
view.update(&mut app, |view, ctx| {
|
||
view.redo(ctx);
|
||
});
|
||
view.read(&app, |view, ctx| {
|
||
assert_eq!(view.buffer_text(ctx), "foofoofo");
|
||
assert_eq!(view.displayed_text(ctx), "foofoofo");
|
||
});
|
||
});
|
||
}
|
||
|
||
#[test]
|
||
fn test_delete_all_on_ephemeral_edit() {
|
||
App::test((), |mut app| async move {
|
||
initialize_app(&mut app);
|
||
|
||
let (_, view) = app.add_window(WindowStyle::NotStealFocus, |ctx| {
|
||
EditorView::new_with_base_text("a", Default::default(), ctx)
|
||
});
|
||
|
||
view.update(&mut app, |view, ctx| {
|
||
view.set_buffer_text_ignoring_undo("abc\ndef", ctx);
|
||
assert_eq!(view.buffer_text(ctx), "abc\ndef");
|
||
});
|
||
|
||
view.update(&mut app, |view, ctx| {
|
||
view.select_ranges(vec![DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)], ctx)
|
||
.unwrap();
|
||
view.delete_all(CutDirection::Right, true, ctx);
|
||
assert_eq!(view.buffer_text(ctx), "abc\n");
|
||
});
|
||
});
|
||
}
|
||
|
||
#[test]
|
||
fn test_autocomplete_symbols() {
|
||
App::test((), |mut app| async move {
|
||
initialize_app(&mut app);
|
||
|
||
app.add_window(WindowStyle::NotStealFocus, |ctx| {
|
||
let mut editor = EditorView::new_with_base_text("abc", Default::default(), ctx);
|
||
editor.set_autocomplete_symbols_allowed(true);
|
||
editor
|
||
.select_ranges(vec![DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3)], ctx)
|
||
.unwrap();
|
||
|
||
editor.user_insert("(", ctx);
|
||
assert_eq!(editor.buffer_text(ctx), "abc()");
|
||
|
||
editor.backspace(ctx);
|
||
editor.user_insert("\"", ctx);
|
||
assert_eq!(editor.buffer_text(ctx), "abc\"");
|
||
|
||
editor.user_insert(" ", ctx);
|
||
editor.user_insert("(", ctx);
|
||
assert_eq!(editor.buffer_text(ctx), "abc\" ()");
|
||
|
||
editor.undo(ctx);
|
||
assert_eq!(editor.buffer_text(ctx), "abc\" ");
|
||
editor.redo(ctx);
|
||
assert_eq!(editor.buffer_text(ctx), "abc\" ()");
|
||
|
||
editor.backspace(ctx);
|
||
assert_eq!(editor.buffer_text(ctx), "abc\" ");
|
||
|
||
editor.user_insert("(", ctx);
|
||
editor.user_insert(")", ctx);
|
||
assert_eq!(editor.buffer_text(ctx), "abc\" ()");
|
||
|
||
editor
|
||
});
|
||
});
|
||
|
||
App::test((), |mut app| async move {
|
||
initialize_app(&mut app);
|
||
|
||
app.add_window(WindowStyle::NotStealFocus, |ctx| {
|
||
let mut editor =
|
||
EditorView::new_with_base_text("word warp word", Default::default(), ctx);
|
||
|
||
editor.set_autocomplete_symbols_allowed(true);
|
||
editor
|
||
.select_ranges(
|
||
vec![
|
||
DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1),
|
||
DisplayPoint::new(0, 11)..DisplayPoint::new(0, 11),
|
||
],
|
||
ctx,
|
||
)
|
||
.unwrap();
|
||
editor.add_next_occurrence(ctx);
|
||
editor.user_insert("[", ctx);
|
||
assert_eq!(editor.buffer_text(ctx), "[word] warp [word]");
|
||
|
||
editor.undo(ctx);
|
||
assert_eq!(editor.buffer_text(ctx), "word warp word");
|
||
editor.redo(ctx);
|
||
assert_eq!(editor.buffer_text(ctx), "[word] warp [word]");
|
||
|
||
editor
|
||
});
|
||
});
|
||
|
||
App::test((), |mut app| async move {
|
||
initialize_app(&mut app);
|
||
|
||
// Emulate a user disabling symbol autocompletion in settings.
|
||
app.update_model(&AppEditorSettings::handle(&app), |editor_settings, ctx| {
|
||
let _ = editor_settings.autocomplete_symbols.set_value(false, ctx);
|
||
ctx.notify();
|
||
});
|
||
app.add_window(WindowStyle::NotStealFocus, |ctx| {
|
||
// Construct an editor which allows for symbol autocompletion,
|
||
// if the user has enabled it.
|
||
let options = EditorOptions {
|
||
autocomplete_symbols: true,
|
||
..Default::default()
|
||
};
|
||
let mut editor = EditorView::new_with_base_text("word warp word", options, ctx);
|
||
|
||
editor.cursor_end(ctx);
|
||
editor.user_insert("(", ctx);
|
||
assert_eq!(editor.buffer_text(ctx), "word warp word(");
|
||
|
||
editor
|
||
});
|
||
});
|
||
}
|
||
|
||
#[test]
|
||
fn test_clear_text_styles() {
|
||
App::test((), |mut app| async move {
|
||
initialize_app(&mut app);
|
||
|
||
app.add_window(WindowStyle::NotStealFocus, |ctx| {
|
||
let mut editor =
|
||
EditorView::new_with_base_text("word\n word\n word", Default::default(), ctx);
|
||
|
||
// Move cursor to the beginning of the buffer.
|
||
editor.cursor_top(ctx);
|
||
|
||
let black_text_style = TextStyle::new().with_background_color(ColorU::black());
|
||
editor.insert_with_styles(
|
||
"foo ",
|
||
&[(0.into()..3.into(), black_text_style)],
|
||
PlainTextEditorViewAction::SystemInsert,
|
||
ctx,
|
||
);
|
||
assert_eq!(editor.buffer_text(ctx), "foo word\n word\n word");
|
||
|
||
assert_eq!(
|
||
editor
|
||
.text_style_runs(ctx)
|
||
.filter(|text_style| text_style.text_style().background_color.is_some())
|
||
.collect::<Vec<_>>(),
|
||
vec![TextRun::new(
|
||
"foo".into(),
|
||
black_text_style,
|
||
0.into()..3.into(),
|
||
)],
|
||
);
|
||
|
||
editor.clear_text_style_runs(ctx);
|
||
|
||
// The editor text should stay the same but the text styles should be gone.
|
||
assert_eq!(editor.buffer_text(ctx), "foo word\n word\n word");
|
||
|
||
assert_eq!(
|
||
editor
|
||
.text_style_runs(ctx)
|
||
.filter(|text_style| text_style.text_style().background_color.is_some())
|
||
.count(),
|
||
0
|
||
);
|
||
|
||
// Undoing should undo the edit, not the style.
|
||
editor.undo(ctx);
|
||
assert_eq!(editor.buffer_text(ctx), "word\n word\n word");
|
||
assert_eq!(
|
||
editor
|
||
.text_style_runs(ctx)
|
||
.filter(|text_style| text_style.text_style().background_color.is_some())
|
||
.count(),
|
||
0
|
||
);
|
||
|
||
// Reinsert the text w/ style.
|
||
editor.insert_with_styles(
|
||
"foo ",
|
||
&[(0.into()..3.into(), black_text_style)],
|
||
PlainTextEditorViewAction::SystemInsert,
|
||
ctx,
|
||
);
|
||
assert_eq!(
|
||
editor
|
||
.text_style_runs(ctx)
|
||
.filter(|text_style| text_style.text_style().background_color.is_some())
|
||
.collect::<Vec<_>>(),
|
||
vec![TextRun::new(
|
||
"foo".into(),
|
||
black_text_style,
|
||
0.into()..3.into(),
|
||
)],
|
||
);
|
||
|
||
// Select the top line and ensure it still remains even after the text styles are
|
||
// cleared.
|
||
editor.select_to_line_end(ctx);
|
||
assert_eq!(
|
||
editor
|
||
.model()
|
||
.as_ref(ctx)
|
||
.selections(ctx)
|
||
.iter()
|
||
.map(|selection| editor
|
||
.model()
|
||
.as_ref(ctx)
|
||
.selection_to_byte_offset(selection, ctx))
|
||
.collect::<Vec<_>>(),
|
||
vec![ByteOffset::from(8)..ByteOffset::from(4)]
|
||
);
|
||
|
||
editor.clear_text_style_runs(ctx);
|
||
assert_eq!(
|
||
editor
|
||
.model()
|
||
.as_ref(ctx)
|
||
.selections(ctx)
|
||
.iter()
|
||
.map(|selection| editor
|
||
.model()
|
||
.as_ref(ctx)
|
||
.selection_to_byte_offset(selection, ctx))
|
||
.collect::<Vec<_>>(),
|
||
vec![ByteOffset::from(8)..ByteOffset::from(4)]
|
||
);
|
||
assert_eq!(editor.buffer_text(ctx), "foo word\n word\n word");
|
||
|
||
assert_eq!(
|
||
editor
|
||
.text_style_runs(ctx)
|
||
.filter(|text_style| text_style.text_style().background_color.is_some())
|
||
.count(),
|
||
0
|
||
);
|
||
|
||
editor
|
||
});
|
||
});
|
||
}
|
||
|
||
#[test]
|
||
fn test_add_cursor() {
|
||
// Tests functionality of add cursors above/below when line lengths are equal.
|
||
App::test((), |mut app| async move {
|
||
initialize_app(&mut app);
|
||
|
||
app.add_window(WindowStyle::NotStealFocus, |ctx| {
|
||
let mut editor =
|
||
EditorView::new_with_base_text("word\nword\nword", Default::default(), ctx);
|
||
|
||
// Move cursor to start of second line.
|
||
editor
|
||
.select_ranges(vec![DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)], ctx)
|
||
.unwrap();
|
||
|
||
// Mock the `FrameLayouts` to reflect the contents of the buffer.
|
||
let frame_layouts = FrameLayouts::new(
|
||
vec![
|
||
Arc::new(TextFrame::mock("word")),
|
||
Arc::new(TextFrame::mock("word")),
|
||
Arc::new(TextFrame::mock("word")),
|
||
],
|
||
0,
|
||
4,
|
||
);
|
||
editor
|
||
.editor_model
|
||
.as_ref(ctx)
|
||
.display_map(ctx)
|
||
.soft_wrap_state()
|
||
.update(frame_layouts.clone());
|
||
|
||
// Add cursor above and below.
|
||
// Here, the goal columns are not used because all lines are long enough.
|
||
editor.add_cursor(NewCursorDirection::Up, ctx);
|
||
|
||
editor
|
||
.editor_model
|
||
.as_ref(ctx)
|
||
.display_map(ctx)
|
||
.soft_wrap_state()
|
||
.update(frame_layouts);
|
||
editor.add_cursor(NewCursorDirection::Down, ctx);
|
||
|
||
// Cursors should have been added above and below.
|
||
assert_eq!(
|
||
editor.selected_ranges(ctx),
|
||
&[
|
||
DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0),
|
||
DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0),
|
||
DisplayPoint::new(2, 0)..DisplayPoint::new(2, 0),
|
||
]
|
||
);
|
||
|
||
// Add a space and ensure it's added in front of every cursor.
|
||
editor.user_insert(" ", ctx);
|
||
assert_eq!(editor.buffer_text(ctx), " word\n word\n word");
|
||
|
||
editor
|
||
});
|
||
});
|
||
|
||
// Tests functionality of add cursors when line lengths are not equal (empty selections).
|
||
App::test((), |mut app| async move {
|
||
initialize_app(&mut app);
|
||
|
||
app.add_window(WindowStyle::NotStealFocus, |ctx| {
|
||
let mut editor =
|
||
EditorView::new_with_base_text("word\nwo\nword", Default::default(), ctx);
|
||
|
||
// Move cursor to second last column of first line.
|
||
editor
|
||
.select_ranges(vec![DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3)], ctx)
|
||
.unwrap();
|
||
|
||
// Mock the `FrameLayouts` to reflect the contents of the buffer.
|
||
let frame_layouts = FrameLayouts::new(
|
||
vec![
|
||
Arc::new(TextFrame::mock("word")),
|
||
Arc::new(TextFrame::mock("wo")),
|
||
Arc::new(TextFrame::mock("word")),
|
||
],
|
||
0,
|
||
4,
|
||
);
|
||
editor
|
||
.editor_model
|
||
.as_ref(ctx)
|
||
.display_map(ctx)
|
||
.soft_wrap_state()
|
||
.update(frame_layouts.clone());
|
||
|
||
// Add cursors below.
|
||
editor.add_cursor(NewCursorDirection::Down, ctx);
|
||
|
||
editor
|
||
.editor_model
|
||
.as_ref(ctx)
|
||
.display_map(ctx)
|
||
.soft_wrap_state()
|
||
.update(frame_layouts);
|
||
editor.add_cursor(NewCursorDirection::Down, ctx);
|
||
|
||
// Cursors should have been added below.
|
||
// Since second line is too short, should just go at end of line.
|
||
assert_eq!(
|
||
editor.selected_ranges(ctx),
|
||
&[
|
||
DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3),
|
||
DisplayPoint::new(1, 2)..DisplayPoint::new(1, 2),
|
||
DisplayPoint::new(2, 3)..DisplayPoint::new(2, 3),
|
||
]
|
||
);
|
||
|
||
// Add a space and ensure it's added in front of every cursor.
|
||
editor.user_insert(" ", ctx);
|
||
assert_eq!(editor.buffer_text(ctx), "wor d\nwo \nwor d");
|
||
|
||
editor
|
||
});
|
||
});
|
||
|
||
// Tests functionality of add cursors when line lengths are not equal (non-empty selections).
|
||
App::test((), |mut app| async move {
|
||
initialize_app(&mut app);
|
||
|
||
app.add_window(WindowStyle::NotStealFocus, |ctx| {
|
||
let mut editor = EditorView::new_with_base_text(
|
||
"words\nwo\nwor\nword\nwords",
|
||
Default::default(),
|
||
ctx,
|
||
);
|
||
|
||
// Create a selection of last 2 chars on last line.
|
||
editor
|
||
.select_ranges(vec![DisplayPoint::new(4, 3)..DisplayPoint::new(4, 5)], ctx)
|
||
.unwrap();
|
||
|
||
// Mock the `FrameLayouts` to reflect the contents of the buffer.
|
||
let frame_layouts = FrameLayouts::new(
|
||
vec![
|
||
Arc::new(TextFrame::mock("words")),
|
||
Arc::new(TextFrame::mock("wo")),
|
||
Arc::new(TextFrame::mock("wor")),
|
||
Arc::new(TextFrame::mock("word")),
|
||
Arc::new(TextFrame::mock("words")),
|
||
],
|
||
0,
|
||
5,
|
||
);
|
||
editor
|
||
.editor_model
|
||
.as_ref(ctx)
|
||
.display_map(ctx)
|
||
.soft_wrap_state()
|
||
.update(frame_layouts);
|
||
|
||
// Add cursors above.
|
||
editor.add_cursor(NewCursorDirection::Up, ctx);
|
||
editor.add_cursor(NewCursorDirection::Up, ctx);
|
||
editor.add_cursor(NewCursorDirection::Up, ctx);
|
||
editor.add_cursor(NewCursorDirection::Up, ctx);
|
||
|
||
// Cursors should have been added above.
|
||
// Specifically, since we started with a non-empty selection,
|
||
// we should create new selections wherever appropriate.
|
||
assert_eq!(
|
||
editor.selected_ranges(ctx),
|
||
&[
|
||
DisplayPoint::new(0, 3)..DisplayPoint::new(0, 5),
|
||
DisplayPoint::new(1, 2)..DisplayPoint::new(1, 2),
|
||
DisplayPoint::new(2, 3)..DisplayPoint::new(2, 3),
|
||
DisplayPoint::new(3, 3)..DisplayPoint::new(3, 4),
|
||
DisplayPoint::new(4, 3)..DisplayPoint::new(4, 5),
|
||
]
|
||
);
|
||
|
||
// Replace selections with a space.
|
||
editor.user_insert(" ", ctx);
|
||
assert_eq!(editor.buffer_text(ctx), "wor \nwo \nwor \nwor \nwor ");
|
||
|
||
editor
|
||
});
|
||
});
|
||
|
||
// Tests functionality of add cursors when line lengths are not equal (non-empty, reversed selections).
|
||
App::test((), |mut app| async move {
|
||
initialize_app(&mut app);
|
||
|
||
app.add_window(WindowStyle::NotStealFocus, |ctx| {
|
||
let mut editor = EditorView::new_with_base_text(
|
||
"words\nwo\nwor\nword\nwords",
|
||
Default::default(),
|
||
ctx,
|
||
);
|
||
|
||
// Mock the `FrameLayouts` to reflect the contents of the buffer.
|
||
let frame_layouts = FrameLayouts::new(
|
||
vec![
|
||
Arc::new(TextFrame::mock("words")),
|
||
Arc::new(TextFrame::mock("wo")),
|
||
Arc::new(TextFrame::mock("wor")),
|
||
Arc::new(TextFrame::mock("word")),
|
||
Arc::new(TextFrame::mock("words")),
|
||
],
|
||
0,
|
||
5,
|
||
);
|
||
editor
|
||
.editor_model
|
||
.as_ref(ctx)
|
||
.display_map(ctx)
|
||
.soft_wrap_state()
|
||
.update(frame_layouts.clone());
|
||
|
||
// Create a reversed selection of 2 chars (last 2 chars of last line).
|
||
editor
|
||
.select_ranges(vec![DisplayPoint::new(4, 5)..DisplayPoint::new(4, 5)], ctx)
|
||
.unwrap();
|
||
editor.select_left(ctx);
|
||
editor.select_left(ctx);
|
||
|
||
// Add cursors above.
|
||
for _ in 0..4 {
|
||
editor
|
||
.editor_model
|
||
.as_ref(ctx)
|
||
.display_map(ctx)
|
||
.soft_wrap_state()
|
||
.update(frame_layouts.clone());
|
||
editor.add_cursor(NewCursorDirection::Up, ctx);
|
||
}
|
||
|
||
// Cursors should have been added above.
|
||
// Specifically, since we started with a non-empty selection,
|
||
// we should create new selections wherever appropriate.
|
||
assert_eq!(
|
||
editor.selected_ranges(ctx),
|
||
&[
|
||
DisplayPoint::new(0, 5)..DisplayPoint::new(0, 3),
|
||
DisplayPoint::new(1, 2)..DisplayPoint::new(1, 2),
|
||
DisplayPoint::new(2, 3)..DisplayPoint::new(2, 3),
|
||
DisplayPoint::new(3, 4)..DisplayPoint::new(3, 3),
|
||
DisplayPoint::new(4, 5)..DisplayPoint::new(4, 3),
|
||
]
|
||
);
|
||
|
||
// Replace selections with a space.
|
||
editor.user_insert(" ", ctx);
|
||
assert_eq!(editor.buffer_text(ctx), "wor \nwo \nwor \nwor \nwor ");
|
||
|
||
editor
|
||
});
|
||
});
|
||
|
||
// Tests that existing multi-cursors stay intact.
|
||
App::test((), |mut app| async move {
|
||
initialize_app(&mut app);
|
||
|
||
app.add_window(WindowStyle::NotStealFocus, |ctx| {
|
||
let mut editor =
|
||
EditorView::new_with_base_text("word\nword\nword", Default::default(), ctx);
|
||
|
||
// Mock the `FrameLayouts` to reflect the contents of the buffer.
|
||
let frame_layouts = FrameLayouts::new(
|
||
vec![
|
||
Arc::new(TextFrame::mock("words")),
|
||
Arc::new(TextFrame::mock("word")),
|
||
Arc::new(TextFrame::mock("word")),
|
||
Arc::new(TextFrame::mock("word")),
|
||
],
|
||
0,
|
||
4,
|
||
);
|
||
editor
|
||
.editor_model
|
||
.as_ref(ctx)
|
||
.display_map(ctx)
|
||
.soft_wrap_state()
|
||
.update(frame_layouts);
|
||
|
||
// Add cursors (mimicking cursors added via mouse).
|
||
let existing_cursors = vec![
|
||
DisplayPoint::new(1, 1)..DisplayPoint::new(1, 2),
|
||
DisplayPoint::new(2, 2)..DisplayPoint::new(2, 3),
|
||
DisplayPoint::new(2, 4)..DisplayPoint::new(2, 4),
|
||
];
|
||
editor.select_ranges(existing_cursors, ctx).unwrap();
|
||
|
||
// Add cursor above.
|
||
editor.add_cursor(NewCursorDirection::Up, ctx);
|
||
|
||
// Existing cursors should remain in tact and new cursors
|
||
// should merge with old ones if possible.
|
||
assert_eq!(
|
||
editor.selected_ranges(ctx),
|
||
&[
|
||
DisplayPoint::new(0, 1)..DisplayPoint::new(0, 2),
|
||
DisplayPoint::new(1, 1)..DisplayPoint::new(1, 3),
|
||
DisplayPoint::new(1, 4)..DisplayPoint::new(1, 4),
|
||
DisplayPoint::new(2, 2)..DisplayPoint::new(2, 3),
|
||
DisplayPoint::new(2, 4)..DisplayPoint::new(2, 4),
|
||
]
|
||
);
|
||
|
||
// Add a space and ensure it was inserted at existing/new cursors.
|
||
editor.user_insert(" ", ctx);
|
||
assert_eq!(editor.buffer_text(ctx), "w rd\nw d \nwo d ");
|
||
|
||
editor
|
||
});
|
||
});
|
||
|
||
// Tests that adding cursors above first row and below last row is a no-op.
|
||
App::test((), |mut app| async move {
|
||
initialize_app(&mut app);
|
||
|
||
app.add_window(WindowStyle::NotStealFocus, |ctx| {
|
||
let mut editor = EditorView::new_with_base_text("word", Default::default(), ctx);
|
||
|
||
// Mock the `FrameLayouts` to reflect the contents of the buffer.
|
||
let frame_layouts = FrameLayouts::new(vec![Arc::new(TextFrame::mock("word"))], 0, 2);
|
||
|
||
// Move cursor to middle of first (and only) line.
|
||
editor
|
||
.select_ranges(vec![DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2)], ctx)
|
||
.unwrap();
|
||
|
||
// Add cursor above and below.
|
||
editor.add_cursor(NewCursorDirection::Up, ctx);
|
||
|
||
editor
|
||
.editor_model
|
||
.as_ref(ctx)
|
||
.display_map(ctx)
|
||
.soft_wrap_state()
|
||
.update(frame_layouts);
|
||
editor.add_cursor(NewCursorDirection::Down, ctx);
|
||
|
||
// Ensure that there aren't any new cursors.
|
||
assert_eq!(
|
||
editor.selected_ranges(ctx),
|
||
&[DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2),]
|
||
);
|
||
|
||
editor
|
||
});
|
||
});
|
||
}
|
||
|
||
#[test]
|
||
fn test_autoscroll_vertical() {
|
||
App::test((), |mut app| async move {
|
||
initialize_app(&mut app);
|
||
|
||
// Initialize a frame layout with a single logical line laid out into
|
||
// three softwrapped lines.
|
||
let frame_layouts = FrameLayouts::new(
|
||
vec![
|
||
Arc::new(TextFrame::mock("word")),
|
||
Arc::new(TextFrame::mock("word")),
|
||
Arc::new(TextFrame::mock("word")),
|
||
],
|
||
0,
|
||
1,
|
||
);
|
||
|
||
let frame_layouts_clone = frame_layouts.clone();
|
||
|
||
// Update the softwrapped state in the editor model.
|
||
let (_, view) = app.add_window(WindowStyle::NotStealFocus, move |ctx| {
|
||
let editor = EditorView::new_with_base_text("wordwordword", Default::default(), ctx);
|
||
|
||
editor
|
||
.editor_model
|
||
.as_ref(ctx)
|
||
.display_map(ctx)
|
||
.soft_wrap_state()
|
||
.update(frame_layouts_clone);
|
||
|
||
editor
|
||
});
|
||
|
||
// By default, the scroll position should be fixed to 0.
|
||
view.read(&app, |view, app| {
|
||
let snapshot = view.snapshot(app);
|
||
let scroll_state: ScrollState = view.into();
|
||
|
||
snapshot.autoscroll_vertically(&scroll_state, 3., 1., 0., &frame_layouts, app);
|
||
assert_eq!(scroll_state.scroll_position().y(), 0.);
|
||
});
|
||
|
||
// Scrolling down to 2. should keep the scroll position to 2. since this is valid.
|
||
view.update(&mut app, |view, app| {
|
||
view.scroll(vec2f(0., 2.), app);
|
||
let snapshot = view.snapshot(app);
|
||
let scroll_state = ScrollState::from(&*view);
|
||
|
||
assert_eq!(scroll_state.scroll_position().y(), 2.);
|
||
snapshot.autoscroll_vertically(&scroll_state, 3., 1., 0., &frame_layouts, app);
|
||
assert_eq!(scroll_state.scroll_position().y(), 2.);
|
||
});
|
||
|
||
// Scrolling down to 3. should clamp the scroll position to 2. since this is the max scroll position.
|
||
view.update(&mut app, |view, app| {
|
||
view.scroll(vec2f(0., 3.), app);
|
||
let snapshot = view.snapshot(app);
|
||
let scroll_state = ScrollState::from(&*view);
|
||
|
||
assert_eq!(scroll_state.scroll_position().y(), 3.);
|
||
snapshot.autoscroll_vertically(&scroll_state, 3., 1., 0., &frame_layouts, app);
|
||
assert_eq!(scroll_state.scroll_position().y(), 2.);
|
||
});
|
||
});
|
||
}
|
||
|
||
#[test]
|
||
fn test_cursor_blink() {
|
||
// Tests functionality of cursor blinking behavior
|
||
App::test((), |mut app| async move {
|
||
initialize_app(&mut app);
|
||
|
||
let (window_id, editor_handle) = app.add_window(WindowStyle::NotStealFocus, |ctx| {
|
||
let mut editor = EditorView::single_line(Default::default(), ctx);
|
||
editor.on_focus(&FocusContext::SelfFocused, ctx);
|
||
// cursors not visible in non-active window
|
||
assert!(!editor.cursors_visible);
|
||
editor
|
||
});
|
||
// set active window ID to enable cursor drawing and blinking
|
||
let windowing_state = app.get_singleton_model_handle::<WindowManager>();
|
||
app.update_model(&windowing_state, |state, ctx| {
|
||
state.overwrite_for_test(state.stage(), Some(window_id));
|
||
ctx.notify();
|
||
});
|
||
app.update_view(&editor_handle, |editor, ctx| {
|
||
let mut epoch = editor.next_blink_epoch();
|
||
editor.blink_cursors(epoch, ctx);
|
||
assert!(editor.cursors_visible);
|
||
epoch = editor.next_blink_epoch();
|
||
editor.blink_cursors(epoch, ctx);
|
||
assert!(!editor.cursors_visible);
|
||
});
|
||
// change the blink setting to disabled
|
||
let editor_settings_handle = app.get_singleton_model_handle::<AppEditorSettings>();
|
||
app.update_model(&editor_settings_handle, |settings, ctx| {
|
||
let _ = settings.cursor_blink.set_value(CursorBlink::Disabled, ctx);
|
||
ctx.notify();
|
||
});
|
||
app.update_view(&editor_handle, |editor, ctx| {
|
||
let mut epoch = editor.next_blink_epoch();
|
||
editor.blink_cursors(epoch, ctx);
|
||
assert!(editor.cursors_visible);
|
||
epoch = editor.next_blink_epoch();
|
||
editor.blink_cursors(epoch, ctx);
|
||
assert!(editor.cursors_visible);
|
||
});
|
||
});
|
||
}
|
||
|
||
#[test]
|
||
fn test_interaction_state() {
|
||
App::test((), |mut app| async move {
|
||
initialize_app(&mut app);
|
||
|
||
app.add_window(WindowStyle::NotStealFocus, |ctx| {
|
||
let mut editor =
|
||
EditorView::new_with_base_text("some test sentence", Default::default(), ctx);
|
||
|
||
// When InteractionState::Editable, edits and selections registered
|
||
assert!(matches!(
|
||
editor.interaction_state(ctx),
|
||
InteractionState::Editable
|
||
));
|
||
editor.insert_selected_text("test string and ", ctx);
|
||
let post_edit_str = "test string and some test sentence";
|
||
assert_eq!(editor.buffer_text(ctx), post_edit_str.to_string());
|
||
|
||
let prev_selection = editor.model().as_ref(ctx).first_selection(ctx).clone();
|
||
editor.select_to_line_end(ctx);
|
||
let new_selection = editor.model().as_ref(ctx).first_selection(ctx).clone();
|
||
assert_ne!(prev_selection, new_selection);
|
||
|
||
// When InteractionState::Selectable, selections registered but edits are not
|
||
editor.set_interaction_state(InteractionState::Selectable, ctx);
|
||
editor.insert_selected_text("another entry", ctx);
|
||
assert_eq!(editor.buffer_text(ctx), post_edit_str.to_string());
|
||
|
||
let prev_selection = editor.model().as_ref(ctx).first_selection(ctx).clone();
|
||
editor.move_left(/* stop at line start */ false, ctx);
|
||
let new_selection = editor.model().as_ref(ctx).first_selection(ctx).clone();
|
||
assert_ne!(prev_selection, new_selection);
|
||
|
||
// When InteractionState::Disabled, selections and edits are not registered
|
||
editor.set_interaction_state(InteractionState::Disabled, ctx);
|
||
editor.insert_selected_text("one more entry", ctx);
|
||
assert_eq!(editor.buffer_text(ctx), post_edit_str.to_string());
|
||
|
||
let prev_selection = editor.model().as_ref(ctx).first_selection(ctx).clone();
|
||
editor.move_to_buffer_end(ctx);
|
||
let new_selection = editor.model().as_ref(ctx).first_selection(ctx).clone();
|
||
assert_eq!(prev_selection, new_selection);
|
||
|
||
editor
|
||
});
|
||
});
|
||
}
|
||
|
||
#[test]
|
||
fn test_select_next_occurrence() {
|
||
App::test((), |mut app| async move {
|
||
initialize_app(&mut app);
|
||
|
||
let (_, editor) = app.add_window(WindowStyle::NotStealFocus, |ctx| {
|
||
let mut editor = EditorView::new(Default::default(), ctx);
|
||
editor.user_insert("foo bar foo", ctx);
|
||
|
||
editor
|
||
});
|
||
|
||
editor.update(&mut app, |editor, ctx| {
|
||
editor.add_next_occurrence(ctx);
|
||
});
|
||
|
||
editor.update(&mut app, |editor, ctx| {
|
||
editor.add_next_occurrence(ctx);
|
||
});
|
||
|
||
// Both instances of foo should be selected
|
||
editor.update(&mut app, |editor, ctx| {
|
||
assert_eq!(
|
||
editor.selected_ranges(ctx),
|
||
&[
|
||
DisplayPoint::new(0, 0)..DisplayPoint::new(0, 3),
|
||
DisplayPoint::new(0, 8)..DisplayPoint::new(0, 11),
|
||
]
|
||
);
|
||
})
|
||
});
|
||
}
|
||
|
||
#[test]
|
||
fn test_select_next_occurrence_multi_byte() {
|
||
App::test((), |mut app| async move {
|
||
initialize_app(&mut app);
|
||
|
||
let (_, editor) = app.add_window(WindowStyle::NotStealFocus, |ctx| {
|
||
let mut editor = EditorView::new(Default::default(), ctx);
|
||
editor.user_insert("分分分 bar 分分分", ctx);
|
||
|
||
editor
|
||
});
|
||
|
||
editor.update(&mut app, |editor, ctx| {
|
||
editor.add_next_occurrence(ctx);
|
||
});
|
||
|
||
editor.update(&mut app, |editor, ctx| {
|
||
editor.add_next_occurrence(ctx);
|
||
});
|
||
|
||
// Both instances of `分分分` should be selected.
|
||
editor.update(&mut app, |editor, ctx| {
|
||
assert_eq!(
|
||
editor.selected_ranges(ctx),
|
||
&[
|
||
DisplayPoint::new(0, 0)..DisplayPoint::new(0, 3),
|
||
DisplayPoint::new(0, 8)..DisplayPoint::new(0, 11),
|
||
]
|
||
);
|
||
})
|
||
});
|
||
}
|
||
|
||
#[test]
|
||
fn test_add_non_expanding_space() {
|
||
App::test((), |mut app| async move {
|
||
initialize_app(&mut app);
|
||
|
||
app.add_window(WindowStyle::NotStealFocus, |ctx| {
|
||
let mut editor = EditorView::new_with_base_text("word", Default::default(), ctx);
|
||
editor.move_to_buffer_end(ctx);
|
||
|
||
editor.add_non_expanding_space(ctx);
|
||
assert_eq!(editor.buffer_text(ctx), "word ");
|
||
|
||
editor.undo(ctx);
|
||
assert_eq!(editor.buffer_text(ctx), "word");
|
||
|
||
editor
|
||
});
|
||
});
|
||
}
|
||
|
||
#[test]
|
||
fn test_last_buffer_text() {
|
||
App::test((), |mut app| async move {
|
||
initialize_app(&mut app);
|
||
|
||
app.add_window(WindowStyle::NotStealFocus, |ctx| {
|
||
let mut editor = EditorView::new_with_base_text("a", Default::default(), ctx);
|
||
editor.move_to_buffer_end(ctx);
|
||
assert_eq!(editor.buffer_text(ctx), "a");
|
||
|
||
editor.user_insert("b", ctx);
|
||
assert_eq!(editor.buffer_text(ctx), "ab");
|
||
assert_eq!(editor.last_buffer_text(ctx), "a");
|
||
|
||
editor.undo(ctx);
|
||
assert_eq!(editor.buffer_text(ctx), "a");
|
||
assert_eq!(editor.last_buffer_text(ctx), "ab");
|
||
|
||
editor
|
||
});
|
||
});
|
||
}
|
||
|
||
// This should probably be an element test but it's
|
||
// hard to construct an editor element without an editor view.
|
||
#[test]
|
||
fn test_buffer_points_to_cache() {
|
||
App::test((), |mut app| async move {
|
||
initialize_app(&mut app);
|
||
|
||
let (window_id, editor_handle) = app.add_window(WindowStyle::NotStealFocus, |ctx| {
|
||
let mut editor = EditorView::single_line(Default::default(), ctx);
|
||
editor.user_insert("hello", ctx);
|
||
editor
|
||
});
|
||
|
||
let point = Point::new(0, 3);
|
||
let id = "custom_id";
|
||
editor_handle.update(&mut app, |editor, ctx| {
|
||
editor.cache_buffer_point(point, id, ctx);
|
||
});
|
||
|
||
app.update(move |ctx| {
|
||
// We need to start and end the presenter manually because
|
||
// currently we only do so when rendering within a Stack.
|
||
// We might want to consider calling start / end at the start / end
|
||
// of build_scene (respectively).
|
||
let presenter = ctx.presenter(window_id).unwrap();
|
||
presenter.borrow_mut().position_cache_mut().start();
|
||
presenter
|
||
.borrow_mut()
|
||
.build_scene(vec2f(100., 100.), 1., None, ctx);
|
||
presenter.borrow_mut().position_cache_mut().end();
|
||
|
||
let expected_pos_id = position_id_for_cached_point(editor_handle.id(), id);
|
||
assert!(presenter
|
||
.borrow()
|
||
.position_cache()
|
||
.get_position(expected_pos_id)
|
||
.is_some())
|
||
});
|
||
});
|
||
}
|
||
|
||
#[test]
|
||
fn test_paste_clipboard_with_text_only_should_paste_text_normally() {
|
||
App::test((), |mut app| async move {
|
||
initialize_app(&mut app);
|
||
|
||
let (_, editor) = app.add_window(WindowStyle::NotStealFocus, |ctx| {
|
||
let mut editor = EditorView::new(Default::default(), ctx);
|
||
// Enable image context options to allow image attachment functionality
|
||
// This simulates the state when Agent Mode is active and image attachments are supported
|
||
editor.image_context_options = ImageContextOptions::Enabled {
|
||
unsupported_model: false,
|
||
is_processing_attached_images: false,
|
||
num_images_attached: 0,
|
||
num_images_in_conversation: 0,
|
||
};
|
||
editor
|
||
});
|
||
|
||
// Text-only clipboard - should paste text normally
|
||
app.update(|ctx| {
|
||
let clipboard_content = galaxyui::clipboard::ClipboardContent {
|
||
plain_text: "hello world".to_string(),
|
||
paths: None,
|
||
html: None,
|
||
images: None,
|
||
};
|
||
ctx.clipboard().write(clipboard_content);
|
||
});
|
||
|
||
editor.update(&mut app, |editor, ctx| {
|
||
editor.paste(ctx);
|
||
assert_eq!(editor.buffer_text(ctx), "hello world");
|
||
});
|
||
|
||
// Empty images array should also fall back to text paste
|
||
app.update(|ctx| {
|
||
let clipboard_content = galaxyui::clipboard::ClipboardContent {
|
||
plain_text: "fallback text".to_string(),
|
||
paths: None,
|
||
html: None,
|
||
images: Some(vec![]), // Empty images array
|
||
};
|
||
ctx.clipboard().write(clipboard_content);
|
||
});
|
||
|
||
editor.update(&mut app, |editor, ctx| {
|
||
editor.clear_buffer(ctx);
|
||
editor.paste(ctx);
|
||
// Empty images array should be treated as text-only clipboard content
|
||
// This tests the fallback behavior when images field exists but is empty
|
||
assert_eq!(editor.buffer_text(ctx), "fallback text");
|
||
});
|
||
})
|
||
}
|
||
|
||
#[test]
|
||
fn test_paste_clipboard_with_image_only_should_switch_to_agent_mode() {
|
||
App::test((), |mut app| async move {
|
||
initialize_app(&mut app);
|
||
|
||
let (_, editor) = app.add_window(WindowStyle::NotStealFocus, |ctx| {
|
||
let mut editor = EditorView::new(Default::default(), ctx);
|
||
// Enable image context options for testing
|
||
editor.image_context_options = ImageContextOptions::Enabled {
|
||
unsupported_model: false,
|
||
is_processing_attached_images: false,
|
||
num_images_attached: 0,
|
||
num_images_in_conversation: 0,
|
||
};
|
||
editor
|
||
});
|
||
|
||
// Image-only clipboard - should switch to Agent Mode and attach image
|
||
app.update(|ctx| {
|
||
let png_image = galaxyui::clipboard::ImageData {
|
||
data: vec![137, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13], // PNG header + minimal data
|
||
mime_type: "image/png".to_string(),
|
||
filename: None,
|
||
};
|
||
let clipboard_content = galaxyui::clipboard::ClipboardContent {
|
||
plain_text: "".to_string(), // No text
|
||
paths: None,
|
||
html: None,
|
||
images: Some(vec![png_image]),
|
||
};
|
||
ctx.clipboard().write(clipboard_content);
|
||
});
|
||
|
||
editor.update(&mut app, |editor, ctx| {
|
||
editor.paste(ctx);
|
||
// Image-only clipboard should not paste any text to the buffer
|
||
// The image data should be processed separately via Agent Mode switching
|
||
assert_eq!(editor.buffer_text(ctx), "");
|
||
// TODO: Add assertions for Agent Mode switch and image attachment
|
||
});
|
||
})
|
||
}
|
||
|
||
#[test]
|
||
fn test_paste_clipboard_with_supported_image_and_text_should_handle_both() {
|
||
App::test((), |mut app| async move {
|
||
initialize_app(&mut app);
|
||
|
||
let (_, editor) = app.add_window(WindowStyle::NotStealFocus, |ctx| {
|
||
let mut editor = EditorView::new(Default::default(), ctx);
|
||
// Enable image context options for testing
|
||
editor.image_context_options = ImageContextOptions::Enabled {
|
||
unsupported_model: false,
|
||
is_processing_attached_images: false,
|
||
num_images_attached: 0,
|
||
num_images_in_conversation: 0,
|
||
};
|
||
editor
|
||
});
|
||
|
||
// PNG (supported) image and text clipboard - should switch to Agent Mode, attach image, and paste text
|
||
app.update(|ctx| {
|
||
let png_image = galaxyui::clipboard::ImageData {
|
||
data: vec![137, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13], // PNG header + minimal data
|
||
mime_type: "image/png".to_string(),
|
||
filename: Some("test.png".to_string()),
|
||
};
|
||
let clipboard_content = galaxyui::clipboard::ClipboardContent {
|
||
plain_text: "some descriptive text".to_string(),
|
||
paths: None,
|
||
html: None,
|
||
images: Some(vec![png_image]),
|
||
};
|
||
ctx.clipboard().write(clipboard_content);
|
||
});
|
||
|
||
editor.update(&mut app, |editor, ctx| {
|
||
editor.paste(ctx);
|
||
// When clipboard contains both supported image and text, both should be handled:
|
||
// - Text content gets pasted to the buffer
|
||
// - Image triggers Agent Mode switch and attachment process
|
||
assert_eq!(editor.buffer_text(ctx), "some descriptive text");
|
||
// TODO: Add assertions for Agent Mode switch and image attachment
|
||
});
|
||
})
|
||
}
|
||
|
||
#[test]
|
||
fn test_paste_clipboard_with_unsupported_image_and_text_should_show_error() {
|
||
App::test((), |mut app| async move {
|
||
initialize_app(&mut app);
|
||
|
||
let (_, editor) = app.add_window(WindowStyle::NotStealFocus, |ctx| {
|
||
let mut editor = EditorView::new(Default::default(), ctx);
|
||
// Enable image context options for testing
|
||
editor.image_context_options = ImageContextOptions::Enabled {
|
||
unsupported_model: false,
|
||
is_processing_attached_images: false,
|
||
num_images_attached: 0,
|
||
num_images_in_conversation: 0,
|
||
};
|
||
editor
|
||
});
|
||
|
||
// BMP (unsupported) image and text clipboard - should show error and paste text only
|
||
app.update(|ctx| {
|
||
let bmp_image = galaxyui::clipboard::ImageData {
|
||
data: vec![66, 77, 54, 0, 0, 0, 0, 0, 0, 0, 54, 0, 0, 0], // BMP header + minimal data
|
||
mime_type: "image/bmp".to_string(),
|
||
filename: Some("test.bmp".to_string()),
|
||
};
|
||
let clipboard_content = galaxyui::clipboard::ClipboardContent {
|
||
plain_text: "text with unsupported image".to_string(),
|
||
paths: None,
|
||
html: None,
|
||
images: Some(vec![bmp_image]),
|
||
};
|
||
ctx.clipboard().write(clipboard_content);
|
||
});
|
||
|
||
editor.update(&mut app, |editor, ctx| {
|
||
editor.paste(ctx);
|
||
// When clipboard contains unsupported image format:
|
||
// - Text content should still be pasted normally
|
||
// - Unsupported image should be ignored with appropriate error feedback
|
||
assert_eq!(editor.buffer_text(ctx), "text with unsupported image");
|
||
// TODO: Add assertions for error toast being shown
|
||
});
|
||
})
|
||
}
|
||
|
||
#[test]
|
||
fn test_system_delete_basic() {
|
||
App::test((), |mut app| async move {
|
||
initialize_app(&mut app);
|
||
|
||
let (_, editor) = app.add_window(WindowStyle::NotStealFocus, |ctx| {
|
||
EditorView::new_with_base_text("Hello, World!", Default::default(), ctx)
|
||
});
|
||
|
||
editor.update(&mut app, |editor, ctx| {
|
||
// Delete "Hello" (bytes 0-5)
|
||
editor.system_delete(ByteOffset::from(0)..ByteOffset::from(5), ctx);
|
||
assert_eq!(editor.buffer_text(ctx), ", World!");
|
||
});
|
||
|
||
editor.update(&mut app, |editor, ctx| {
|
||
// Delete ", " (bytes 0-2)
|
||
editor.system_delete(ByteOffset::from(0)..ByteOffset::from(2), ctx);
|
||
assert_eq!(editor.buffer_text(ctx), "World!");
|
||
});
|
||
|
||
editor.update(&mut app, |editor, ctx| {
|
||
// Delete "!") (byte 5)
|
||
editor.system_delete(ByteOffset::from(5)..ByteOffset::from(6), ctx);
|
||
assert_eq!(editor.buffer_text(ctx), "World");
|
||
});
|
||
})
|
||
}
|
||
|
||
#[test]
|
||
fn test_system_delete_multibyte_characters_basic() {
|
||
App::test((), |mut app| async move {
|
||
initialize_app(&mut app);
|
||
|
||
// Test with various multi-byte UTF-8 characters
|
||
let text_with_multibyte = "Hello 世界 🌍 café";
|
||
let (_, editor) = app.add_window(WindowStyle::NotStealFocus, |ctx| {
|
||
EditorView::new_with_base_text(text_with_multibyte, Default::default(), ctx)
|
||
});
|
||
|
||
editor.update(&mut app, |editor, ctx| {
|
||
// Delete "Hello " (6 bytes, 6 chars)
|
||
editor.system_delete(ByteOffset::from(0)..ByteOffset::from(6), ctx);
|
||
assert_eq!(editor.buffer_text(ctx), "世界 🌍 café");
|
||
});
|
||
|
||
editor.update(&mut app, |editor, ctx| {
|
||
// Delete "世界" (6 bytes, 2 chars - each Chinese character is 3 bytes)
|
||
editor.system_delete(ByteOffset::from(0)..ByteOffset::from(6), ctx);
|
||
assert_eq!(editor.buffer_text(ctx), " 🌍 café");
|
||
});
|
||
|
||
editor.update(&mut app, |editor, ctx| {
|
||
// Delete " 🌍" (5 bytes: 1 space + 4 bytes for emoji)
|
||
editor.system_delete(ByteOffset::from(0)..ByteOffset::from(5), ctx);
|
||
assert_eq!(editor.buffer_text(ctx), " café");
|
||
});
|
||
})
|
||
}
|
||
|
||
#[test]
|
||
fn test_system_delete_multibyte_characters_edge_cases() {
|
||
App::test((), |mut app| async move {
|
||
initialize_app(&mut app);
|
||
|
||
// Test with only multi-byte characters
|
||
let (_, editor) = app.add_window(WindowStyle::NotStealFocus, |ctx| {
|
||
EditorView::new_with_base_text("🚀🌟⭐️💫", Default::default(), ctx)
|
||
});
|
||
|
||
editor.update(&mut app, |editor, ctx| {
|
||
let original_text = editor.buffer_text(ctx);
|
||
log::info!(
|
||
"Original text: '{}', bytes: {:?}",
|
||
original_text,
|
||
original_text.as_bytes()
|
||
);
|
||
|
||
// Delete first emoji 🚀 (4 bytes)
|
||
editor.system_delete(ByteOffset::from(0)..ByteOffset::from(4), ctx);
|
||
let after_first_delete = editor.buffer_text(ctx);
|
||
log::info!(
|
||
"After first delete: '{}', bytes: {:?}",
|
||
after_first_delete,
|
||
after_first_delete.as_bytes()
|
||
);
|
||
assert_eq!(after_first_delete, "🌟⭐️💫");
|
||
});
|
||
|
||
editor.update(&mut app, |editor, ctx| {
|
||
// Delete 🌟 (4 bytes)
|
||
editor.system_delete(ByteOffset::from(0)..ByteOffset::from(4), ctx);
|
||
let after_second_delete = editor.buffer_text(ctx);
|
||
log::info!(
|
||
"After second delete: '{}', bytes: {:?}",
|
||
after_second_delete,
|
||
after_second_delete.as_bytes()
|
||
);
|
||
assert_eq!(after_second_delete, "⭐️💫");
|
||
});
|
||
})
|
||
}
|
||
|
||
#[test]
|
||
fn test_system_delete_various_unicode_categories() {
|
||
App::test((), |mut app| async move {
|
||
initialize_app(&mut app);
|
||
|
||
// Test with various Unicode categories: Latin, Cyrillic, Arabic, Emoji, etc.
|
||
let unicode_text = "Hello Привет مرحبا 你好 🌍🎉";
|
||
let (_, editor) = app.add_window(WindowStyle::NotStealFocus, |ctx| {
|
||
EditorView::new_with_base_text(unicode_text, Default::default(), ctx)
|
||
});
|
||
|
||
editor.update(&mut app, |editor, ctx| {
|
||
// Delete "Hello " (ASCII - 6 bytes)
|
||
editor.system_delete(ByteOffset::from(0)..ByteOffset::from(6), ctx);
|
||
let text1 = editor.buffer_text(ctx);
|
||
assert_eq!(text1, "Привет مرحبا 你好 🌍🎉");
|
||
|
||
// Delete "Привет " (Cyrillic - 13 bytes: 6 chars × 2 bytes each + 1 space)
|
||
editor.system_delete(ByteOffset::from(0)..ByteOffset::from(13), ctx);
|
||
let text2 = editor.buffer_text(ctx);
|
||
assert_eq!(text2, "مرحبا 你好 🌍🎉");
|
||
|
||
// Delete "مرحبا " (Arabic - 11 bytes: 5 chars × 2 bytes each + 1 space)
|
||
editor.system_delete(ByteOffset::from(0)..ByteOffset::from(11), ctx);
|
||
let text3 = editor.buffer_text(ctx);
|
||
assert_eq!(text3, "你好 🌍🎉");
|
||
|
||
// Delete "你好 " (Chinese - 7 bytes: 2 chars × 3 bytes each + 1 space)
|
||
editor.system_delete(ByteOffset::from(0)..ByteOffset::from(7), ctx);
|
||
let text4 = editor.buffer_text(ctx);
|
||
assert_eq!(text4, "🌍🎉");
|
||
|
||
// Delete emojis (8 bytes: 2 emojis × 4 bytes each)
|
||
editor.system_delete(ByteOffset::from(0)..ByteOffset::from(8), ctx);
|
||
let text5 = editor.buffer_text(ctx);
|
||
assert_eq!(text5, "");
|
||
});
|
||
})
|
||
}
|
||
|
||
#[test]
|
||
fn test_drag_and_drop_files_applies_path_transformer() {
|
||
App::test((), |mut app| async move {
|
||
initialize_app(&mut app);
|
||
|
||
let (_, view) = app.add_window(WindowStyle::NotStealFocus, |ctx| {
|
||
EditorView::new(Default::default(), ctx)
|
||
});
|
||
|
||
let paths = || {
|
||
vec![
|
||
UserInput::new(r"C:\foo\bar".to_string()),
|
||
UserInput::new(r"D:\baz".to_string()),
|
||
]
|
||
};
|
||
|
||
view.update(&mut app, |view, ctx| {
|
||
view.set_drag_drop_path_transformer(None);
|
||
view.drag_and_drop_files(&paths(), ctx);
|
||
assert_eq!(view.buffer_text(ctx), r"C:\foo\bar D:\baz ");
|
||
});
|
||
|
||
view.update(&mut app, |view, ctx| {
|
||
view.clear_buffer(ctx);
|
||
view.set_drag_drop_path_transformer(Some(Box::new(
|
||
galaxy_util::path::convert_windows_path_to_wsl,
|
||
)));
|
||
view.drag_and_drop_files(&paths(), ctx);
|
||
assert_eq!(view.buffer_text(ctx), "/mnt/c/foo/bar /mnt/d/baz ");
|
||
});
|
||
|
||
view.update(&mut app, |view, ctx| {
|
||
view.clear_buffer(ctx);
|
||
view.set_drag_drop_path_transformer(Some(Box::new(
|
||
galaxy_util::path::convert_windows_path_to_msys2,
|
||
)));
|
||
view.drag_and_drop_files(&paths(), ctx);
|
||
assert_eq!(view.buffer_text(ctx), "/c/foo/bar /d/baz ");
|
||
});
|
||
});
|
||
}
|
||
|
||
#[path = "vim_handler_tests.rs"]
|
||
mod vim_handler_tests;
|
||
|
||
#[path = "marked_text_tests.rs"]
|
||
mod marked_text_tests;
|