use std::sync::Arc; use pathfinder_geometry::vector::Vector2F;use unindent::Unindent;use vim::vim::{MotionType, VimMode};use warp_core::features::FeatureFlag;use warp_core::settings::Setting;use warp_core::ui::appearance::Appearance;use warp_editor::content::buffer::{InitialBufferState, ToBufferCharOffset, ToBufferPoint};use warp_editor::model::CoreEditorModel;use warp_editor::render::element::VerticalExpansionBehavior;use warp_editor::render::model::viewport::SizeInfo;use warp_util::user_input::UserInput;use warpui::keymap::Keystroke;use warpui::platform::WindowStyle;use warpui::text::point::Point;use warpui::units::IntoPixels;use warpui::{App, SingletonEntity, TypedActionView, UpdateModel, ViewHandle}; use crate::auth::AuthStateProvider;use crate::cloud_object::model::persistence::CloudModel;use crate::code::editor::view::{CodeEditorRenderOptions, CodeEditorView, CodeEditorViewAction};use crate::notebooks::editor::keys::NotebookKeybindings;use crate::server::server_api::team::MockTeamClient;use crate::server::server_api::workspace::MockWorkspaceClient;use crate::settings::AppEditorSettings;use crate::settings_view::keybindings::KeybindingChangedNotifier;use crate::test_util::settings::initialize_settings_for_tests;use crate::vim_registers::VimRegisters;use crate::workspace::ActiveSession;use crate::workspace::sync_inputs::SyncedInputState;use crate::workspaces::user_workspaces::UserWorkspaces; // 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)); }); });} /// Set the viewport to exactly `lines` rows tall. Returns the line height in pixels.fn set_viewport_lines(editor: &ViewHandle<CodeEditorView>, lines: usize, app: &mut App) -> f32 { let (line_height, render_state) = editor.read(app, |view, ctx| { let model = view.model.as_ref(ctx); (model.line_height(ctx), model.render_state().clone()) }); render_state.update(app, |render_state, ctx| { render_state.set_viewport_size( SizeInfo { viewport_size: Vector2F::new(800.0, line_height * lines as f32), needs_layout: false, }, ctx, ); }); line_height} /// Read the current vertical scroll position.fn scroll_top(editor: &ViewHandle<CodeEditorView>, app: &App) -> f32 { editor.read(app, |view, ctx| { view.model .as_ref(ctx) .render_state() .as_ref(ctx) .viewport() .scroll_top() .as_f32() })} #[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); Lines 1–500 of 2127. Load more
use std::sync::Arc; use pathfinder_geometry::vector::Vector2F; use unindent::Unindent; use vim::vim::{MotionType, VimMode}; use warp_core::features::FeatureFlag; use warp_core::settings::Setting; use warp_core::ui::appearance::Appearance; use warp_editor::content::buffer::{InitialBufferState, ToBufferCharOffset, ToBufferPoint}; use warp_editor::model::CoreEditorModel; use warp_editor::render::element::VerticalExpansionBehavior; use warp_editor::render::model::viewport::SizeInfo; use warp_util::user_input::UserInput; use warpui::keymap::Keystroke; use warpui::platform::WindowStyle; use warpui::text::point::Point; use warpui::units::IntoPixels; use warpui::{App, SingletonEntity, TypedActionView, UpdateModel, ViewHandle}; use crate::auth::AuthStateProvider; use crate::cloud_object::model::persistence::CloudModel; use crate::code::editor::view::{CodeEditorRenderOptions, CodeEditorView, CodeEditorViewAction}; use crate::notebooks::editor::keys::NotebookKeybindings; use crate::server::server_api::team::MockTeamClient; use crate::server::server_api::workspace::MockWorkspaceClient; use crate::settings::AppEditorSettings; use crate::settings_view::keybindings::KeybindingChangedNotifier; use crate::test_util::settings::initialize_settings_for_tests; use crate::vim_registers::VimRegisters; use crate::workspace::ActiveSession; use crate::workspace::sync_inputs::SyncedInputState; use crate::workspaces::user_workspaces::UserWorkspaces; // 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)); }); }); } /// Set the viewport to exactly `lines` rows tall. Returns the line height in pixels. fn set_viewport_lines(editor: &ViewHandle<CodeEditorView>, lines: usize, app: &mut App) -> f32 { let (line_height, render_state) = editor.read(app, |view, ctx| { let model = view.model.as_ref(ctx); (model.line_height(ctx), model.render_state().clone()) }); render_state.update(app, |render_state, ctx| { render_state.set_viewport_size( SizeInfo { viewport_size: Vector2F::new(800.0, line_height * lines as f32), needs_layout: false, }, ctx, ); }); line_height } /// Read the current vertical scroll position. fn scroll_top(editor: &ViewHandle<CodeEditorView>, app: &App) -> f32 { editor.read(app, |view, ctx| { view.model .as_ref(ctx) .render_state() .as_ref(ctx) .viewport() .scroll_top() .as_f32() }) } #[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);