Files
galaxy/app/src/code/editor/view/vim_handler_tests.rs
T

1439 lines
49 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use crate::auth::AuthStateProvider;
use crate::cloud_object::model::persistence::CloudModel;
use crate::code::editor::view::CodeEditorRenderOptions;
use crate::notebooks::editor::keys::NotebookKeybindings;
use crate::workspace::ActiveSession;
use crate::{
code::editor::view::{CodeEditorView, CodeEditorViewAction},
server::server_api::{team::MockTeamClient, workspace::MockWorkspaceClient},
settings::AppEditorSettings,
settings_view::keybindings::KeybindingChangedNotifier,
test_util::settings::initialize_settings_for_tests,
vim_registers::VimRegisters,
workspace::sync_inputs::SyncedInputState,
workspaces::user_workspaces::UserWorkspaces,
};
use std::sync::Arc;
use unindent::Unindent;
use vim::vim::{MotionType, VimMode};
use galaxy_core::{features::FeatureFlag, settings::Setting, ui::appearance::Appearance};
use galaxy_editor::model::CoreEditorModel;
use galaxy_editor::{
content::buffer::{InitialBufferState, ToBufferCharOffset, ToBufferPoint},
render::element::VerticalExpansionBehavior,
};
use galaxy_util::user_input::UserInput;
use galaxyui::text::point::Point;
use galaxyui::{
keymap::Keystroke, platform::WindowStyle, App, SingletonEntity, TypedActionView, UpdateModel,
ViewHandle,
};
// Await render/layout completion for a CodeEditorView in tests.
async fn layout_editor_view(app: &mut App, editor: &ViewHandle<CodeEditorView>) {
app.read(|ctx| {
let model = editor.as_ref(ctx).model.as_ref(ctx);
model.render_state().as_ref(ctx).layout_complete()
})
.await;
}
/// Helper function to initialize all required singleton models for CodeEditorView tests.
fn initialize_code_editor_app(app: &mut App) {
initialize_settings_for_tests(app);
// Add required singleton models for CodeEditorView
app.add_singleton_model(|_| AuthStateProvider::new_for_test());
app.add_singleton_model(|_| Appearance::mock());
app.add_singleton_model(|_| SyncedInputState::mock());
app.add_singleton_model(|_| VimRegisters::new());
app.add_singleton_model(|_| KeybindingChangedNotifier::mock());
#[cfg(feature = "voice_input")]
app.add_singleton_model(voice_input::VoiceInput::new);
// Add mocks required by rich text editor (used in the CommentEditor)
app.add_singleton_model(CloudModel::mock);
app.add_singleton_model(|_| ActiveSession::default());
app.add_singleton_model(NotebookKeybindings::new);
// Add UserWorkspaces mock (required by CodeEditorView)
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,
)
});
// Enable vim mode in editor settings
app.update_model(
&AppEditorSettings::handle(app),
|settings: &mut AppEditorSettings, ctx| {
settings.vim_mode.set_value(true, ctx).unwrap();
},
);
}
/// Helper function for creating a code editor with buffer text.
fn add_code_editor(buffer_content: &str, app: &mut App) -> ViewHandle<CodeEditorView> {
let (_, editor) = app.add_window(WindowStyle::NotStealFocus, move |ctx| {
let mut editor = CodeEditorView::new(
None,
None,
CodeEditorRenderOptions::new(VerticalExpansionBehavior::GrowToMaxHeight),
ctx,
);
let base_text = buffer_content.unindent();
editor.reset(InitialBufferState::plain_text(base_text.as_str()), ctx);
editor.handle_action(&CodeEditorViewAction::CursorAtBufferStart, ctx);
editor
});
editor
}
/// Helper function for simulating vim user input.
fn vim_user_insert(editor: &ViewHandle<CodeEditorView>, text: &str, app: &mut App) {
editor.update(app, |view, ctx| {
view.handle_action(
&CodeEditorViewAction::VimUserTyped(UserInput::new(text)),
ctx,
);
});
}
/// Helper function to get buffer text from CodeEditorView.
fn buffer_text(editor: &ViewHandle<CodeEditorView>, app: &App) -> String {
editor.read(app, |view, ctx| view.text(ctx).into_string())
}
/// Helper function to get current cursor position (head of first selection).
fn cursor_position(editor: &ViewHandle<CodeEditorView>, app: &App) -> (usize, usize) {
editor.read(app, |view, ctx| {
let buffer_selection = view.model.as_ref(ctx).buffer_selection_model().as_ref(ctx);
let buffer = view.model.as_ref(ctx).buffer().as_ref(ctx);
let head_point = buffer_selection
.first_selection_head()
.to_buffer_point(buffer);
(head_point.row as usize, head_point.column as usize)
})
}
/// Helper function to get vim mode from CodeEditorView.
fn vim_mode(editor: &ViewHandle<CodeEditorView>, app: &App) -> Option<VimMode> {
editor.read(app, |view, ctx| view.vim_mode(ctx))
}
/// Helper to set the cursor to a specific (row, col). Rows are 1-based to match test expectations.
fn set_cursor_position(editor: &ViewHandle<CodeEditorView>, row: usize, col: usize, app: &mut App) {
editor.update(app, |view, ctx| {
view.model.update(ctx, |model, ctx| {
let buffer = model.buffer().as_ref(ctx);
let point = Point::new(row as u32, col as u32);
let offset = point.to_buffer_char_offset(buffer);
model
.selection()
.update(ctx, |sel, ctx| sel.set_cursor(offset, ctx));
});
});
}
#[test]
fn test_code_editor_vim_basic_mode_switching() {
let _feature_flag_guard = FeatureFlag::VimCodeEditor.override_enabled(true);
App::test((), |mut app| async move {
initialize_code_editor_app(&mut app);
let editor = add_code_editor("hello", &mut app);
assert_eq!(vim_mode(&editor, &app), Some(VimMode::Normal));
vim_user_insert(&editor, "i", &mut app);
assert_eq!(vim_mode(&editor, &app), Some(VimMode::Insert));
editor.update(&mut app, |view, ctx| {
view.vim_keystroke(&Keystroke::parse("escape").unwrap(), ctx);
});
assert_eq!(vim_mode(&editor, &app), Some(VimMode::Normal));
});
}
#[test]
fn test_vim_number_repeat_action() {
let _feature_flag_guard = FeatureFlag::VimCodeEditor.override_enabled(true);
App::test((), |mut app| async move {
initialize_code_editor_app(&mut app);
let editor = add_code_editor(
"line 1
line 2
line 3
line 4",
&mut app,
);
assert_eq!(cursor_position(&editor, &app), (1, 0));
vim_user_insert(&editor, "3dd", &mut app);
assert_eq!(buffer_text(&editor, &app), "line 4",);
assert_eq!(vim_mode(&editor, &app), Some(VimMode::Normal));
assert_eq!(cursor_position(&editor, &app), (1, 0));
});
}
#[test]
fn test_vim_number_repeat_word_motion() {
let _feature_flag_guard = FeatureFlag::VimCodeEditor.override_enabled(true);
App::test((), |mut app| async move {
initialize_code_editor_app(&mut app);
let editor = add_code_editor(
"abc de f
gh ijkl m nop"
.unindent()
.as_str(),
&mut app,
);
vim_user_insert(&editor, "3w", &mut app);
assert_eq!(cursor_position(&editor, &app), (2, 0));
// Try to go past the end of the buffer.
vim_user_insert(&editor, "4w", &mut app);
assert_eq!(cursor_position(&editor, &app), (2, 12));
vim_user_insert(&editor, "2b", &mut app);
assert_eq!(cursor_position(&editor, &app), (2, 8));
vim_user_insert(&editor, "2b2e", &mut app);
assert_eq!(cursor_position(&editor, &app), (2, 6));
});
}
#[test]
fn test_vim_number_repeat_line_motion() {
let _feature_flag_guard = FeatureFlag::VimCodeEditor.override_enabled(true);
App::test((), |mut app| async move {
initialize_code_editor_app(&mut app);
let editor = add_code_editor(
"aa
bbbbb
ccc
dddd"
.unindent()
.as_str(),
&mut app,
);
// Ensure layout is completed before vertical/line motions like count + '$' or '^'.
layout_editor_view(&mut app, &editor).await;
// Start at buffer start (helper already positions at start). Apply "2$".
// Expect to end on line 2, column 4 (1-based row indexing in CodeEditorView tests).
vim_user_insert(&editor, "2$", &mut app);
assert_eq!(cursor_position(&editor, &app), (2, 4));
// Apply "3$" from there should move to end of last line (line 4), column 3.
vim_user_insert(&editor, "3$", &mut app);
assert_eq!(cursor_position(&editor, &app), (4, 3));
// "^" ignores the repetition count should move to first non-blank in current line.
vim_user_insert(&editor, "3^", &mut app);
assert_eq!(cursor_position(&editor, &app), (4, 0));
});
}
#[test]
fn test_vim_number_repeat_character_motion() {
let _feature_flag_guard = FeatureFlag::VimCodeEditor.override_enabled(true);
App::test((), |mut app| async move {
initialize_code_editor_app(&mut app);
let editor = add_code_editor(
"aa
bbbbb
ccc
dddd"
.unindent()
.as_str(),
&mut app,
);
// Ensure layout so vertical motions (j/k) use real geometry for goal columns.
layout_editor_view(&mut app, &editor).await;
// #j: from (1,0) go down 3 lines -> (4,0)
vim_user_insert(&editor, "3j", &mut app);
assert_eq!(cursor_position(&editor, &app), (4, 0));
// #l: move right 3 -> (4,3)
vim_user_insert(&editor, "3l", &mut app);
assert_eq!(cursor_position(&editor, &app), (4, 3));
// #k: move up 2 -> (2,3) (line 2 has enough width)
vim_user_insert(&editor, "2k", &mut app);
assert_eq!(cursor_position(&editor, &app), (2, 3));
// #h (attempt to go past start): from col 3 left 4 -> clamp to 0 -> (2,0)
vim_user_insert(&editor, "4h", &mut app);
assert_eq!(cursor_position(&editor, &app), (2, 0));
});
}
#[test]
fn test_vim_number_repeat_op_word_motion() {
let _feature_flag_guard = FeatureFlag::VimCodeEditor.override_enabled(true);
App::test((), |mut app| async move {
initialize_code_editor_app(&mut app);
let editor = add_code_editor(
"line 1
line 2
line 3
line 4",
&mut app,
);
set_cursor_position(&editor, 2, 0, &mut app);
vim_user_insert(&editor, "d3w", &mut app);
assert_eq!(
buffer_text(&editor, &app),
"line 1
3
line 4"
.unindent()
.as_str(),
);
assert_eq!(vim_mode(&editor, &app), Some(VimMode::Normal));
// Change two words starting at start of the first line
set_cursor_position(&editor, 1, 0, &mut app);
vim_user_insert(&editor, "c2w", &mut app);
assert_eq!(buffer_text(&editor, &app), "\n3\nline 4");
assert_eq!(vim_mode(&editor, &app), Some(VimMode::Insert));
// Insert replacement, return to Normal
vim_user_insert(&editor, "replacement", &mut app);
editor.update(&mut app, |view, ctx| {
view.vim_keystroke(&Keystroke::parse("escape").unwrap(), ctx);
});
assert_eq!(
buffer_text(&editor, &app),
"replacement
3
line 4"
.unindent()
.as_str(),
);
assert_eq!(vim_mode(&editor, &app), Some(VimMode::Normal));
// Attempt to delete more words than remain after the cursor
set_cursor_position(&editor, 3, 0, &mut app);
vim_user_insert(&editor, "d3w", &mut app);
assert_eq!(buffer_text(&editor, &app), "replacement\n3\n");
assert_eq!(vim_mode(&editor, &app), Some(VimMode::Normal));
});
}
#[test]
fn test_vim_number_repeat_op_line_object() {
let _feature_flag_guard = FeatureFlag::VimCodeEditor.override_enabled(true);
App::test((), |mut app| async move {
initialize_code_editor_app(&mut app);
let editor = add_code_editor(
"line 1
line 2
line 3 is longer
line 4 is long
line 5
line 6
line 7 too",
&mut app,
);
// Begin on line 2
set_cursor_position(&editor, 2, 0, &mut app);
// Delete 2 lines starting at current line (use canonical Vim count form for CodeEditorView)
vim_user_insert(&editor, "d2d", &mut app);
assert_eq!(
buffer_text(&editor, &app),
"line 1
line 4 is long
line 5
line 6
line 7 too"
.unindent()
.as_str(),
);
assert_eq!(vim_mode(&editor, &app), Some(VimMode::Normal));
// Change 3 lines starting at current line
vim_user_insert(&editor, "c3c", &mut app);
assert_eq!(
buffer_text(&editor, &app),
"line 1
line 7 too"
.unindent()
.as_str(),
);
assert_eq!(vim_mode(&editor, &app), Some(VimMode::Insert));
// Insert replacement text; remain in insert mode
vim_user_insert(&editor, "replacement", &mut app);
assert_eq!(cursor_position(&editor, &app), (2, 11));
assert_eq!(
buffer_text(&editor, &app),
"line 1
replacement
line 7 too"
.unindent()
.as_str(),
);
assert_eq!(vim_mode(&editor, &app), Some(VimMode::Insert));
});
}
#[test]
fn test_vim_number_repeat_op_line_motions() {
let _feature_flag_guard = FeatureFlag::VimCodeEditor.override_enabled(true);
App::test((), |mut app| async move {
initialize_code_editor_app(&mut app);
let editor = add_code_editor(
"The quick brown fox
jumped over
the lazy dog.",
&mut app,
);
// Start at (1, 10) after "The quick "
set_cursor_position(&editor, 1, 10, &mut app);
// Change through end-of-line twice (across line break)
vim_user_insert(&editor, "c2$", &mut app);
assert_eq!(buffer_text(&editor, &app), "The quick \nthe lazy dog.");
assert_eq!(vim_mode(&editor, &app), Some(VimMode::Insert));
// Insert replacement lines, then return to Normal
vim_user_insert(&editor, "bear\nhigh-fived", &mut app);
editor.update(&mut app, |view, ctx| {
view.vim_keystroke(&Keystroke::parse("escape").unwrap(), ctx);
});
assert_eq!(cursor_position(&editor, &app), (2, 9)); // TODO is this ok
assert_eq!(
buffer_text(&editor, &app),
"The quick bear
high-fived
the lazy dog."
.unindent()
.as_str(),
);
assert_eq!(vim_mode(&editor, &app), Some(VimMode::Normal));
// Now delete through end-of-line twice from (1,9)
set_cursor_position(&editor, 1, 9, &mut app);
vim_user_insert(&editor, "d2$", &mut app);
assert_eq!(
buffer_text(&editor, &app),
"The quick
the lazy dog."
.unindent()
.as_str(),
);
assert_eq!(cursor_position(&editor, &app), (1, 8));
assert_eq!(vim_mode(&editor, &app), Some(VimMode::Normal));
});
}
#[test]
fn test_vim_number_repeat_character_motions_right() {
let _feature_flag_guard = FeatureFlag::VimCodeEditor.override_enabled(true);
App::test((), |mut app| async move {
initialize_code_editor_app(&mut app);
let editor = add_code_editor(
"The quick brown fox
jumped",
&mut app,
);
// Replace to the right (5 characters)
set_cursor_position(&editor, 1, 10, &mut app);
vim_user_insert(&editor, "c5l", &mut app);
assert_eq!(buffer_text(&editor, &app), "The quick fox\njumped");
assert_eq!(vim_mode(&editor, &app), Some(VimMode::Insert));
// Insert replacement text, escape back to Normal, then move right once
vim_user_insert(&editor, "purple", &mut app);
editor.update(&mut app, |view, ctx| {
view.vim_keystroke(&Keystroke::parse("escape").unwrap(), ctx);
});
vim_user_insert(&editor, "l", &mut app);
assert_eq!(cursor_position(&editor, &app), (1, 16));
assert_eq!(buffer_text(&editor, &app), "The quick purple fox\njumped");
assert_eq!(vim_mode(&editor, &app), Some(VimMode::Normal));
// Delete to the right (attempting to go past end of line)
vim_user_insert(&editor, "d5l", &mut app);
assert_eq!(buffer_text(&editor, &app), "The quick purple\njumped");
assert_eq!(cursor_position(&editor, &app), (1, 15));
assert_eq!(vim_mode(&editor, &app), Some(VimMode::Normal));
});
}
#[test]
fn test_vim_number_repeat_character_motions_left() {
let _feature_flag_guard = FeatureFlag::VimCodeEditor.override_enabled(true);
App::test((), |mut app| async move {
initialize_code_editor_app(&mut app);
let editor = add_code_editor(
"The quick
brown fox jumped",
&mut app,
);
// Replace to the left (3 characters)
set_cursor_position(&editor, 2, 9, &mut app);
vim_user_insert(&editor, "c3h", &mut app);
assert_eq!(buffer_text(&editor, &app), "The quick\nbrown jumped");
assert_eq!(cursor_position(&editor, &app), (2, 6));
assert_eq!(vim_mode(&editor, &app), Some(VimMode::Insert));
// Insert replacement and escape
vim_user_insert(&editor, "bear", &mut app);
editor.update(&mut app, |view, ctx| {
view.vim_keystroke(&Keystroke::parse("escape").unwrap(), ctx);
});
assert_eq!(buffer_text(&editor, &app), "The quick\nbrown bear jumped");
assert_eq!(cursor_position(&editor, &app), (2, 9));
assert_eq!(vim_mode(&editor, &app), Some(VimMode::Normal));
// Delete to the left, attempting to delete past line start
vim_user_insert(&editor, "d12h", &mut app);
assert_eq!(buffer_text(&editor, &app), "The quick\nr jumped");
assert_eq!(cursor_position(&editor, &app), (2, 0));
assert_eq!(vim_mode(&editor, &app), Some(VimMode::Normal));
});
}
#[test]
fn test_vim_number_repeat_op_combination() {
let _feature_flag_guard = FeatureFlag::VimCodeEditor.override_enabled(true);
App::test((), |mut app| async move {
initialize_code_editor_app(&mut app);
let editor = add_code_editor(
"line 1
line 2
line 3
line 4
line 5",
&mut app,
);
// Start on line 2 and delete 2 * (3 words) => removes lines 2..4
set_cursor_position(&editor, 2, 0, &mut app);
vim_user_insert(&editor, "2d3w", &mut app);
assert_eq!(
buffer_text(&editor, &app),
"line 1
line 5"
.unindent()
.as_str(),
);
// Reset buffer and try 3d2w
editor.update(&mut app, |view, ctx| {
view.reset(
InitialBufferState::plain_text(
"line 1\nline 2\nline 3\nline 4\nline 5".unindent().as_str(),
),
ctx,
);
view.handle_action(&CodeEditorViewAction::CursorAtBufferStart, ctx);
view.vim_keystroke(&Keystroke::parse("escape").unwrap(), ctx);
});
set_cursor_position(&editor, 2, 0, &mut app);
vim_user_insert(&editor, "3d2w", &mut app);
assert_eq!(
buffer_text(&editor, &app),
"line 1
line 5"
.unindent()
.as_str(),
);
});
}
#[test]
fn test_vim_number_repeat_yank_paste_linewise() {
let _feature_flag_guard = FeatureFlag::VimCodeEditor.override_enabled(true);
App::test((), |mut app| async move {
initialize_code_editor_app(&mut app);
let editor = add_code_editor(
"aaa
b b b
cc"
.unindent()
.as_str(),
&mut app,
);
// Yank 2 lines from line 2
set_cursor_position(&editor, 2, 0, &mut app);
vim_user_insert(&editor, "y2y", &mut app);
assert_eq!(
buffer_text(&editor, &app),
"aaa
b b b
cc"
.unindent()
.as_str()
);
// Paste 3 times before cursor
vim_user_insert(&editor, "3P", &mut app);
assert_eq!(
buffer_text(&editor, &app),
"aaa
b b b
cc
b b b
cc
b b b
cc
b b b
cc"
.unindent()
.as_str()
);
});
}
#[test]
fn test_vim_number_repeat_yank_paste_charwise() {
let _feature_flag_guard = FeatureFlag::VimCodeEditor.override_enabled(true);
App::test((), |mut app| async move {
initialize_code_editor_app(&mut app);
let editor = add_code_editor("a bc d efg h", &mut app);
set_cursor_position(&editor, 1, 2, &mut app);
vim_user_insert(&editor, "y2w", &mut app);
assert_eq!(buffer_text(&editor, &app), "a bc d efg h");
vim_user_insert(&editor, "3P", &mut app);
assert_eq!(buffer_text(&editor, &app), "a bc d bc d bc d bc d efg h");
assert_eq!(cursor_position(&editor, &app), (1, 16));
});
}
#[test]
fn test_vim_delete_lines_d0() {
let _feature_flag_guard = FeatureFlag::VimCodeEditor.override_enabled(true);
App::test((), |mut app| async move {
initialize_code_editor_app(&mut app);
let editor = add_code_editor("abcd\n efgh\nijkl", &mut app);
// delete to start of line (including non-whitespace)
set_cursor_position(&editor, 2, 4, &mut app);
vim_user_insert(&editor, "d0", &mut app);
assert_eq!(buffer_text(&editor, &app), "abcd\ngh\nijkl");
assert_eq!(cursor_position(&editor, &app), (2, 0));
assert_eq!(vim_mode(&editor, &app), Some(VimMode::Normal));
});
}
#[test]
fn test_vim_delete_lines_d_caret() {
let _feature_flag_guard = FeatureFlag::VimCodeEditor.override_enabled(true);
App::test((), |mut app| async move {
initialize_code_editor_app(&mut app);
let editor = add_code_editor("abcd\n efgh\nijkl", &mut app);
// delete to first non-whitespace
set_cursor_position(&editor, 2, 4, &mut app);
vim_user_insert(&editor, "d^", &mut app);
assert_eq!(buffer_text(&editor, &app), "abcd\n gh\nijkl");
assert_eq!(cursor_position(&editor, &app), (2, 2));
assert_eq!(vim_mode(&editor, &app), Some(VimMode::Normal));
});
}
#[test]
fn test_vim_delete_lines_d_dollar() {
let _feature_flag_guard = FeatureFlag::VimCodeEditor.override_enabled(true);
App::test((), |mut app| async move {
initialize_code_editor_app(&mut app);
let editor = add_code_editor("abcd\n efgh\nijkl", &mut app);
// delete to end of line
set_cursor_position(&editor, 2, 4, &mut app);
vim_user_insert(&editor, "d$", &mut app);
assert_eq!(buffer_text(&editor, &app), "abcd\n ef\nijkl");
assert_eq!(cursor_position(&editor, &app), (2, 3));
assert_eq!(vim_mode(&editor, &app), Some(VimMode::Normal));
// delete to end of line using D alias
set_cursor_position(&editor, 3, 2, &mut app);
vim_user_insert(&editor, "D", &mut app);
assert_eq!(buffer_text(&editor, &app), "abcd\n ef\nij");
assert_eq!(cursor_position(&editor, &app), (3, 1));
assert_eq!(vim_mode(&editor, &app), Some(VimMode::Normal));
});
}
#[test]
fn test_vim_replace_simple() {
let _feature_flag_guard = FeatureFlag::VimCodeEditor.override_enabled(true);
App::test((), |mut app| async move {
initialize_code_editor_app(&mut app);
let editor = add_code_editor("abcdef", &mut app);
// Replace first character
vim_user_insert(&editor, "r", &mut app);
assert_eq!(vim_mode(&editor, &app), Some(VimMode::Replace));
assert_eq!(cursor_position(&editor, &app), (1, 0));
assert_eq!(buffer_text(&editor, &app), "abcdef");
vim_user_insert(&editor, "g", &mut app);
assert_eq!(vim_mode(&editor, &app), Some(VimMode::Normal));
assert_eq!(cursor_position(&editor, &app), (1, 0));
assert_eq!(buffer_text(&editor, &app), "gbcdef");
// Replace last character
set_cursor_position(&editor, 1, 5, &mut app);
vim_user_insert(&editor, "r", &mut app);
assert_eq!(vim_mode(&editor, &app), Some(VimMode::Replace));
assert_eq!(cursor_position(&editor, &app), (1, 5));
assert_eq!(buffer_text(&editor, &app), "gbcdef");
vim_user_insert(&editor, "h", &mut app);
assert_eq!(vim_mode(&editor, &app), Some(VimMode::Normal));
assert_eq!(cursor_position(&editor, &app), (1, 5));
assert_eq!(buffer_text(&editor, &app), "gbcdeh");
});
}
#[test]
fn test_vim_replace_number_repeat() {
let _feature_flag_guard = FeatureFlag::VimCodeEditor.override_enabled(true);
App::test((), |mut app| async move {
initialize_code_editor_app(&mut app);
let editor = add_code_editor("abcdef\nghijk", &mut app);
// Replace first three characters with 'l'
vim_user_insert(&editor, "3r", &mut app);
assert_eq!(buffer_text(&editor, &app), "abcdef\nghijk");
assert_eq!(vim_mode(&editor, &app), Some(VimMode::Replace));
assert_eq!(cursor_position(&editor, &app), (1, 0));
vim_user_insert(&editor, "l", &mut app);
assert_eq!(buffer_text(&editor, &app), "llldef\nghijk");
assert_eq!(vim_mode(&editor, &app), Some(VimMode::Normal));
assert_eq!(cursor_position(&editor, &app), (1, 2));
});
}
#[test]
fn test_vim_replace_number_repeat_end_of_line() {
// If count > remaining chars, cancel
let _feature_flag_guard = FeatureFlag::VimCodeEditor.override_enabled(true);
App::test((), |mut app| async move {
initialize_code_editor_app(&mut app);
let editor = add_code_editor("abcdef\nghijk", &mut app);
set_cursor_position(&editor, 1, 4, &mut app);
vim_user_insert(&editor, "3r", &mut app);
assert_eq!(buffer_text(&editor, &app), "abcdef\nghijk");
assert_eq!(vim_mode(&editor, &app), Some(VimMode::Replace));
assert_eq!(cursor_position(&editor, &app), (1, 4));
vim_user_insert(&editor, "m", &mut app);
// Cancelled; no change
assert_eq!(buffer_text(&editor, &app), "abcdef\nghijk");
assert_eq!(vim_mode(&editor, &app), Some(VimMode::Normal));
assert_eq!(cursor_position(&editor, &app), (1, 4));
});
}
#[test]
fn test_vim_delete_char_x() {
let _feature_flag_guard = FeatureFlag::VimCodeEditor.override_enabled(true);
App::test((), |mut app| async move {
initialize_code_editor_app(&mut app);
let editor = add_code_editor("abcde", &mut app);
// delete first character in the buffer
set_cursor_position(&editor, 1, 0, &mut app);
vim_user_insert(&editor, "x", &mut app);
assert_eq!(buffer_text(&editor, &app), "bcde");
assert_eq!(cursor_position(&editor, &app), (1, 0));
// delete last character in the buffer
set_cursor_position(&editor, 1, 3, &mut app);
vim_user_insert(&editor, "x", &mut app);
assert_eq!(buffer_text(&editor, &app), "bcd");
// cursor moves back to cover last char
assert_eq!(cursor_position(&editor, &app), (1, 2));
// delete last character again
vim_user_insert(&editor, "x", &mut app);
assert_eq!(buffer_text(&editor, &app), "bc");
assert_eq!(cursor_position(&editor, &app), (1, 1));
});
}
#[test]
fn test_vim_delete_char_motion_sideways() {
let _feature_flag_guard = FeatureFlag::VimCodeEditor.override_enabled(true);
App::test((), |mut app| async move {
initialize_code_editor_app(&mut app);
let editor = add_code_editor("echo hello", &mut app);
vim_user_insert(&editor, "w", &mut app);
vim_user_insert(&editor, "dl", &mut app);
assert_eq!(buffer_text(&editor, &app), "echo ello");
assert_eq!(cursor_position(&editor, &app), (1, 5));
vim_user_insert(&editor, "dh", &mut app);
assert_eq!(buffer_text(&editor, &app), "echoello");
assert_eq!(cursor_position(&editor, &app), (1, 4));
vim_user_insert(&editor, "$", &mut app);
assert_eq!(cursor_position(&editor, &app), (1, 7));
vim_user_insert(&editor, "dl", &mut app);
assert_eq!(buffer_text(&editor, &app), "echoell");
assert_eq!(cursor_position(&editor, &app), (1, 6));
});
}
#[test]
fn test_vim_delete_char_space() {
let _feature_flag_guard = FeatureFlag::VimCodeEditor.override_enabled(true);
App::test((), |mut app| async move {
initialize_code_editor_app(&mut app);
let editor = add_code_editor(
"echo hi
echo there",
&mut app,
);
// Deleting forward at the start of the buffer works (d<space>)
vim_user_insert(&editor, "d ", &mut app);
assert_eq!(buffer_text(&editor, &app), "cho hi\necho there");
assert_eq!(cursor_position(&editor, &app), (1, 0));
// Move to the end of the line.
vim_user_insert(&editor, "$", &mut app);
// Deleting forward at the end of the line deletes the last character.
vim_user_insert(&editor, "d ", &mut app);
assert_eq!(buffer_text(&editor, &app), "cho h\necho there");
assert_eq!(cursor_position(&editor, &app), (1, 4));
// Deleting forward wraps around to the next line with count
vim_user_insert(&editor, "3d ", &mut app);
assert_eq!(buffer_text(&editor, &app), "cho cho there");
assert_eq!(cursor_position(&editor, &app), (1, 4));
});
}
#[test]
fn test_vim_delete_char_backspace() {
let _feature_flag_guard = FeatureFlag::VimCodeEditor.override_enabled(true);
App::test((), |mut app| async move {
initialize_code_editor_app(&mut app);
let editor = add_code_editor(
"echo hi
echo there",
&mut app,
);
// Deleting backward at the start of the buffer does nothing.
vim_user_insert(&editor, "d", &mut app);
editor.update(&mut app, |view, ctx| {
view.vim_keystroke(&Keystroke::parse("backspace").unwrap(), ctx);
});
assert_eq!(buffer_text(&editor, &app), "echo hi\necho there");
assert_eq!(cursor_position(&editor, &app), (1, 0));
// Move to the end of the first line.
vim_user_insert(&editor, "$", &mut app);
// Deleting backward at the end of the line removes the second-to-last character.
vim_user_insert(&editor, "d", &mut app);
editor.update(&mut app, |view, ctx| {
view.vim_keystroke(&Keystroke::parse("backspace").unwrap(), ctx);
});
assert_eq!(buffer_text(&editor, &app), "echo i\necho there");
assert_eq!(cursor_position(&editor, &app), (1, 5));
// Move to the next line.
vim_user_insert(&editor, "w", &mut app);
// Deleting backward wraps around to the previous line with count
vim_user_insert(&editor, "3d", &mut app);
editor.update(&mut app, |view, ctx| {
view.vim_keystroke(&Keystroke::parse("backspace").unwrap(), ctx);
});
assert_eq!(buffer_text(&editor, &app), "echoecho there");
assert_eq!(cursor_position(&editor, &app), (1, 4));
});
}
#[test]
fn test_vim_delete_word_dw() {
let _feature_flag_guard = FeatureFlag::VimCodeEditor.override_enabled(true);
App::test((), |mut app| async move {
initialize_code_editor_app(&mut app);
let editor = add_code_editor(
"abc de.f g-hi-j
kL",
&mut app,
);
// dw
vim_user_insert(&editor, "dw", &mut app);
assert_eq!(buffer_text(&editor, &app), "de.f g-hi-j\nkL".unindent());
// dW
vim_user_insert(&editor, "dW", &mut app);
assert_eq!(buffer_text(&editor, &app), "g-hi-j\nkL".unindent());
// dW again does not cross a line break
vim_user_insert(&editor, "dW", &mut app);
assert_eq!(buffer_text(&editor, &app), "\nkL");
});
}
#[test]
fn test_vim_delete_word_de() {
let _feature_flag_guard = FeatureFlag::VimCodeEditor.override_enabled(true);
App::test((), |mut app| async move {
initialize_code_editor_app(&mut app);
let editor = add_code_editor(
"g-hi-j kL
mNop.qr st u v wX/yZ",
&mut app,
);
// dE
vim_user_insert(&editor, "dE", &mut app);
assert_eq!(
buffer_text(&editor, &app),
" kL\nmNop.qr st u v wX/yZ".unindent()
);
// de
vim_user_insert(&editor, "de", &mut app);
assert_eq!(buffer_text(&editor, &app), "\nmNop.qr st u v wX/yZ");
// dE across line break
vim_user_insert(&editor, "dE", &mut app);
assert_eq!(buffer_text(&editor, &app), " st u v wX/yZ");
});
}
#[test]
fn test_vim_delete_word_db() {
let _feature_flag_guard = FeatureFlag::VimCodeEditor.override_enabled(true);
App::test((), |mut app| async move {
initialize_code_editor_app(&mut app);
let editor = add_code_editor("mNop.qr st u v wX/yZ", &mut app);
// Move cursor to end of line (simulate Vim quirk of block cursor)
editor.update(&mut app, |view, ctx| {
view.handle_action(&CodeEditorViewAction::MoveToLineEnd, ctx);
view.handle_action(&CodeEditorViewAction::MoveLeft, ctx);
});
assert_eq!(cursor_position(&editor, &app), (1, 19));
// dB
vim_user_insert(&editor, "dB", &mut app);
assert_eq!(buffer_text(&editor, &app), "mNop.qr st u v Z");
// db
vim_user_insert(&editor, "db", &mut app);
assert_eq!(buffer_text(&editor, &app), "mNop.qr st u Z");
assert_eq!(cursor_position(&editor, &app), (1, 13));
// dw to remove the last Z
vim_user_insert(&editor, "dw", &mut app);
assert_eq!(buffer_text(&editor, &app), "mNop.qr st u ");
assert_eq!(cursor_position(&editor, &app), (1, 12));
// db twice more
vim_user_insert(&editor, "db", &mut app);
assert_eq!(buffer_text(&editor, &app), "mNop.qr st ");
assert_eq!(cursor_position(&editor, &app), (1, 11));
vim_user_insert(&editor, "db", &mut app);
assert_eq!(buffer_text(&editor, &app), "mNop.qr ");
assert_eq!(cursor_position(&editor, &app), (1, 8));
// dB to remove symbol
vim_user_insert(&editor, "dB", &mut app);
assert_eq!(buffer_text(&editor, &app), " ");
assert_eq!(cursor_position(&editor, &app), (1, 0));
});
}
#[test]
fn test_vim_delete_word_dge() {
let _feature_flag_guard = FeatureFlag::VimCodeEditor.override_enabled(true);
App::test((), |mut app| async move {
initialize_code_editor_app(&mut app);
let editor = add_code_editor("echo hello-hi warp-dev", &mut app);
set_cursor_position(&editor, 1, 20, &mut app);
vim_user_insert(&editor, "dge", &mut app);
assert_eq!(buffer_text(&editor, &app), "echo hello-hi warpev");
assert_eq!(cursor_position(&editor, &app), (1, 18));
vim_user_insert(&editor, "dge", &mut app);
assert_eq!(buffer_text(&editor, &app), "echo hello-hev");
assert_eq!(cursor_position(&editor, &app), (1, 12));
vim_user_insert(&editor, "dgE", &mut app);
assert_eq!(buffer_text(&editor, &app), "echev");
assert_eq!(cursor_position(&editor, &app), (1, 3));
});
}
#[test]
fn test_vim_delete_word_empty() {
let _feature_flag_guard = FeatureFlag::VimCodeEditor.override_enabled(true);
App::test((), |mut app| async move {
initialize_code_editor_app(&mut app);
let editor = add_code_editor("", &mut app);
// try each operation on an empty buffer
for seq in ["dw", "dW", "de", "dE", "db", "dB", "dge", "dgE"] {
vim_user_insert(&editor, seq, &mut app);
}
assert_eq!(buffer_text(&editor, &app), "");
});
}
#[test]
fn test_vim_dw_newline_quirks() {
let _feature_flag_guard = FeatureFlag::VimCodeEditor.override_enabled(true);
App::test((), |mut app| async move {
initialize_code_editor_app(&mut app);
let editor = add_code_editor("echo foo \necho bar", &mut app);
// A "w" motion can traverse the newline.
vim_user_insert(&editor, "eelw", &mut app);
assert_eq!(cursor_position(&editor, &app), (2, 0));
// An "e" motion can also traverse the newline.
vim_user_insert(&editor, "ggeee", &mut app);
assert_eq!(cursor_position(&editor, &app), (2, 3));
// An "e" motion with an operator can traverse the newline.
vim_user_insert(&editor, "gelde", &mut app);
assert_eq!(buffer_text(&editor, &app), "echo foo bar");
// However, a "w" motion with an operator cannot traverse the newline.
editor.update(&mut app, |view, ctx| {
view.reset(InitialBufferState::plain_text("echo foo \necho bar"), ctx);
view.handle_action(&CodeEditorViewAction::CursorAtBufferStart, ctx);
view.vim_keystroke(&Keystroke::parse("escape").unwrap(), ctx);
});
vim_user_insert(&editor, "ggeeldw", &mut app);
assert_eq!(buffer_text(&editor, &app), "echo foo\necho bar");
vim_user_insert(&editor, "dw", &mut app);
assert_eq!(buffer_text(&editor, &app), "echo fo\necho bar");
// Including a count allows it to traverse the newline
vim_user_insert(&editor, "2dw", &mut app);
assert_eq!(buffer_text(&editor, &app), "echo fbar");
});
}
#[test]
fn test_vim_jump_to_end_and_beginning() {
let _feature_flag_guard = FeatureFlag::VimCodeEditor.override_enabled(true);
App::test((), |mut app| async move {
initialize_code_editor_app(&mut app);
let editor = add_code_editor(
"abc
def
ghi",
&mut app,
);
vim_user_insert(&editor, "G", &mut app);
assert_eq!(cursor_position(&editor, &app), (3, 0));
vim_user_insert(&editor, "gg", &mut app);
assert_eq!(cursor_position(&editor, &app), (1, 0));
});
}
#[test]
fn test_vim_begin_line_below() {
let _feature_flag_guard = FeatureFlag::VimCodeEditor.override_enabled(true);
App::test((), |mut app| async move {
initialize_code_editor_app(&mut app);
let editor = add_code_editor(
"abc
def
ghi",
&mut app,
);
set_cursor_position(&editor, 2, 0, &mut app);
vim_user_insert(&editor, "o", &mut app);
assert_eq!(vim_mode(&editor, &app), Some(VimMode::Insert));
assert_eq!(cursor_position(&editor, &app), (3, 0));
assert_eq!(buffer_text(&editor, &app), "abc\ndef\n\nghi".unindent());
});
}
#[test]
fn test_vim_begin_line_above() {
let _feature_flag_guard = FeatureFlag::VimCodeEditor.override_enabled(true);
App::test((), |mut app| async move {
initialize_code_editor_app(&mut app);
let editor = add_code_editor(
"abcdef
ghijkl
mnopqr",
&mut app,
);
set_cursor_position(&editor, 2, 4, &mut app);
layout_editor_view(&mut app, &editor).await;
vim_user_insert(&editor, "O", &mut app);
assert_eq!(vim_mode(&editor, &app), Some(VimMode::Insert));
assert_eq!(cursor_position(&editor, &app), (2, 0));
assert_eq!(
buffer_text(&editor, &app),
"abcdef\n\nghijkl\nmnopqr".unindent()
);
});
}
#[test]
fn test_vim_substitute_char() {
let _feature_flag_guard = FeatureFlag::VimCodeEditor.override_enabled(true);
App::test((), |mut app| async move {
initialize_code_editor_app(&mut app);
let editor = add_code_editor("abcdef", &mut app);
// Substitute at start of line
set_cursor_position(&editor, 1, 0, &mut app);
vim_user_insert(&editor, "s", &mut app);
assert_eq!(vim_mode(&editor, &app), Some(VimMode::Insert));
assert_eq!(buffer_text(&editor, &app), "bcdef");
assert_eq!(cursor_position(&editor, &app), (1, 0));
vim_user_insert(&editor, "xyz", &mut app);
assert_eq!(buffer_text(&editor, &app), "xyzbcdef");
assert_eq!(cursor_position(&editor, &app), (1, 3));
editor.update(&mut app, |view, ctx| {
view.vim_keystroke(&Keystroke::parse("escape").unwrap(), ctx);
});
assert_eq!(vim_mode(&editor, &app), Some(VimMode::Normal));
assert_eq!(cursor_position(&editor, &app), (1, 2));
// Substitute at end of line
set_cursor_position(&editor, 1, 7, &mut app);
vim_user_insert(&editor, "s", &mut app);
assert_eq!(vim_mode(&editor, &app), Some(VimMode::Insert));
assert_eq!(buffer_text(&editor, &app), "xyzbcde");
assert_eq!(cursor_position(&editor, &app), (1, 7));
vim_user_insert(&editor, "jkl", &mut app);
assert_eq!(buffer_text(&editor, &app), "xyzbcdejkl");
assert_eq!(cursor_position(&editor, &app), (1, 10));
editor.update(&mut app, |view, ctx| {
view.vim_keystroke(&Keystroke::parse("escape").unwrap(), ctx);
});
assert_eq!(vim_mode(&editor, &app), Some(VimMode::Normal));
assert_eq!(cursor_position(&editor, &app), (1, 9));
});
}
#[test]
fn test_vim_substitute_line() {
let _feature_flag_guard = FeatureFlag::VimCodeEditor.override_enabled(true);
App::test((), |mut app| async move {
initialize_code_editor_app(&mut app);
let editor = add_code_editor(
"aaa
bbb
ccc",
&mut app,
);
// substitute first line
set_cursor_position(&editor, 1, 0, &mut app);
vim_user_insert(&editor, "S", &mut app);
assert_eq!(cursor_position(&editor, &app), (1, 0));
assert_eq!(buffer_text(&editor, &app), "\n\nbbb\nccc".unindent());
assert_eq!(vim_mode(&editor, &app), Some(VimMode::Insert));
vim_user_insert(&editor, "new text", &mut app);
assert_eq!(cursor_position(&editor, &app), (1, 8));
assert_eq!(buffer_text(&editor, &app), "new text\nbbb\nccc".unindent());
assert_eq!(vim_mode(&editor, &app), Some(VimMode::Insert));
editor.update(&mut app, |view, ctx| {
view.vim_keystroke(&Keystroke::parse("escape").unwrap(), ctx);
});
assert_eq!(cursor_position(&editor, &app), (1, 7));
assert_eq!(vim_mode(&editor, &app), Some(VimMode::Normal));
// substitute last line
set_cursor_position(&editor, 3, 2, &mut app);
vim_user_insert(&editor, "S", &mut app);
assert_eq!(cursor_position(&editor, &app), (3, 0));
assert_eq!(buffer_text(&editor, &app), "new text\nbbb\n\n".unindent());
assert_eq!(vim_mode(&editor, &app), Some(VimMode::Insert));
vim_user_insert(&editor, "replacement", &mut app);
assert_eq!(cursor_position(&editor, &app), (3, 11));
assert_eq!(
buffer_text(&editor, &app),
"new text\nbbb\nreplacement".unindent()
);
assert_eq!(vim_mode(&editor, &app), Some(VimMode::Insert));
editor.update(&mut app, |view, ctx| {
view.vim_keystroke(&Keystroke::parse("escape").unwrap(), ctx);
});
assert_eq!(cursor_position(&editor, &app), (3, 10));
assert_eq!(vim_mode(&editor, &app), Some(VimMode::Normal));
});
}
#[test]
fn test_vim_visual_selection_with_newlines() {
let _feature_flag_guard = FeatureFlag::VimCodeEditor.override_enabled(true);
App::test((), |mut app| async move {
initialize_code_editor_app(&mut app);
let editor = add_code_editor(
"ddd
aaa
bbb
ccc",
&mut app,
);
// select aaa, bbb, and the newline
set_cursor_position(&editor, 2, 0, &mut app);
vim_user_insert(&editor, "V", &mut app);
vim_user_insert(&editor, "2j", &mut app);
assert_eq!(
vim_mode(&editor, &app),
Some(VimMode::Visual(MotionType::Linewise))
);
// delete selection
vim_user_insert(&editor, "d", &mut app);
assert_eq!(buffer_text(&editor, &app), "ddd\nccc".unindent());
// select onto a smaller line above
editor.update(&mut app, |view, ctx| {
view.reset(InitialBufferState::plain_text("aaa\nbbbbb\nc\nddd"), ctx);
});
set_cursor_position(&editor, 2, 3, &mut app);
vim_user_insert(&editor, "V", &mut app);
vim_user_insert(&editor, "j", &mut app);
// delete selection
vim_user_insert(&editor, "d", &mut app);
assert_eq!(buffer_text(&editor, &app), "aaa\nddd".unindent());
});
}
#[test]
fn test_vim_k_at_top_of_file_does_not_panic() {
let _feature_flag_guard = FeatureFlag::VimCodeEditor.override_enabled(true);
App::test((), |mut app| async move {
initialize_code_editor_app(&mut app);
let editor = add_code_editor(
"line 1
line 2
line 3",
&mut app,
);
layout_editor_view(&mut app, &editor).await;
// Cursor already at row 1 (top). Pressing `k` should not panic.
assert_eq!(cursor_position(&editor, &app), (1, 0));
vim_user_insert(&editor, "k", &mut app);
assert_eq!(cursor_position(&editor, &app), (1, 0));
});
}
#[test]
fn test_vim_counted_k_overflow_does_not_panic() {
let _feature_flag_guard = FeatureFlag::VimCodeEditor.override_enabled(true);
App::test((), |mut app| async move {
initialize_code_editor_app(&mut app);
let editor = add_code_editor(
"line 1
line 2
line 3
line 4
line 5
line 6
line 7
line 8
line 9
line 10",
&mut app,
);
layout_editor_view(&mut app, &editor).await;
// Move to line 10 (row 10)
set_cursor_position(&editor, 10, 0, &mut app);
assert_eq!(cursor_position(&editor, &app), (10, 0));
// 100k should clamp to row 1 without panicking
vim_user_insert(&editor, "100k", &mut app);
assert_eq!(cursor_position(&editor, &app), (1, 0));
});
}
#[test]
fn test_vim_dgg_at_first_line_does_not_panic() {
let _feature_flag_guard = FeatureFlag::VimCodeEditor.override_enabled(true);
App::test((), |mut app| async move {
initialize_code_editor_app(&mut app);
let editor = add_code_editor(
"aaa
bbb
ccc",
&mut app,
);
// dgg from line 1 — vim_select_to_buffer_start produces CharOffset(0)
assert_eq!(cursor_position(&editor, &app), (1, 0));
vim_user_insert(&editor, "dgg", &mut app);
assert_eq!(cursor_position(&editor, &app), (1, 0));
});
}
#[test]
fn test_vim_gg_at_first_line_does_not_panic() {
let _feature_flag_guard = FeatureFlag::VimCodeEditor.override_enabled(true);
App::test((), |mut app| async move {
initialize_code_editor_app(&mut app);
let editor = add_code_editor(
"aaa
bbb
ccc",
&mut app,
);
// gg when already at first line — jump_to_line_column(0, ...)
assert_eq!(cursor_position(&editor, &app), (1, 0));
vim_user_insert(&editor, "gg", &mut app);
assert_eq!(cursor_position(&editor, &app), (1, 0));
});
}
#[test]
fn test_vim_linewise_delete_at_first_line_does_not_panic() {
let _feature_flag_guard = FeatureFlag::VimCodeEditor.override_enabled(true);
App::test((), |mut app| async move {
initialize_code_editor_app(&mut app);
let editor = add_code_editor(
"aaa
bbb
ccc",
&mut app,
);
// dd on line 1 — vim_extend_selection_linewise with row 0
assert_eq!(cursor_position(&editor, &app), (1, 0));
vim_user_insert(&editor, "dd", &mut app);
assert_eq!(buffer_text(&editor, &app), "bbb\nccc");
});
}
#[test]
fn test_vim_visual_linewise_delete_first_line_does_not_panic() {
let _feature_flag_guard = FeatureFlag::VimCodeEditor.override_enabled(true);
App::test((), |mut app| async move {
initialize_code_editor_app(&mut app);
let editor = add_code_editor(
"aaa
bbb
ccc",
&mut app,
);
// V then d on line 1 — visual linewise at row 0
assert_eq!(cursor_position(&editor, &app), (1, 0));
vim_user_insert(&editor, "V", &mut app);
vim_user_insert(&editor, "d", &mut app);
assert_eq!(buffer_text(&editor, &app), "bbb\nccc");
});
}