2351 lines · 83.0 KB · Rust
RawDownload
use std::cell::RefCell;use std::rc::Rc;use std::sync::Arc; use pathfinder_geometry::vector::{Vector2F, vec2f};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, EntityId, EntityIdSet, Event, Presenter, SingletonEntity, TypedActionView, UpdateModel,    ViewHandle, WindowId, WindowInvalidation,}; use crate::auth::AuthStateProvider;use crate::cloud_object::model::persistence::CloudModel;use crate::code::editor::find::view::CodeEditorFind;use crate::code::editor::view::{CodeEditorRenderOptions, CodeEditorView, CodeEditorViewAction};use crate::editor::{EditorAction, EditorView};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> {    add_code_editor_with_window(buffer_content, app).1} /// Like [`add_code_editor`], but also returns the window the editor was added to. Tests that/// render the view tree and dispatch synthetic window events need the window id.fn add_code_editor_with_window(    buffer_content: &str,    app: &mut App,) -> (WindowId, ViewHandle<CodeEditorView>) {    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    })} /// 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} /// Helper to get the find bar owned by a CodeEditorView.fn find_bar(editor: &ViewHandle<CodeEditorView>, app: &App) -> ViewHandle<CodeEditorFind> {    editor.read(app, |view, _| {        view.find_bar.clone().expect("find bar should exist")    })} /// Helper to check whether the find bar's query input can be edited.fn is_find_input_editable(find_bar: &ViewHandle<CodeEditorFind>, app: &App) -> bool {    find_bar.read(app, |find_bar, ctx| find_bar.is_find_input_editable(ctx))} /// Helper to check whether the find bar's query input holds keyboard focus.fn is_find_input_focused(find_editor: &ViewHandle<EditorView>, app: &App) -> bool {    find_editor.read(app, |_, ctx| find_editor.is_focused(ctx))} /// The window size used by tests that render the view tree.fn test_window_size() -> Vector2F {    vec2f(1200., 800.)} /// Renders the given views into `presenter` and paints a scene, so that element positions are/// recorded and synthetic mouse events can be hit-tested. Every view on the path to the element/// under test must be listed: a `ChildView` lays out to zero size unless its view was rendered.fn render_views(app: &mut App, presenter: &Rc<RefCell<Presenter>>, view_ids: &[EntityId]) {    let mut updated = EntityIdSet::default();    for view_id in view_ids {        updated.insert(*view_id);    }    let invalidation = WindowInvalidation {        updated,        ..Default::default()    };    let presenter = presenter.clone();    app.update(move |ctx| {        presenter.borrow_mut().invalidate(invalidation, ctx);        presenter            .borrow_mut()            .build_scene(test_window_size(), 1., None, ctx);    });} /// Dispatches a full mouse press/release through the window, the same way the platform layer/// delivers a real click.fn click_at(    app: &mut App,    presenter: &Rc<RefCell<Presenter>>,    window_id: WindowId,    position: Vector2F,) {    app.update({        let presenter = presenter.clone();        move |ctx| {            ctx.simulate_window_event(                Event::LeftMouseDown {                    position,                    modifiers: Default::default(),                    click_count: 1,                    is_first_mouse: false,                },                window_id,                presenter,            );        }    });    app.update({        let presenter = presenter.clone();        move |ctx| {            ctx.simulate_window_event(                Event::LeftMouseUp {                    position,                    modifiers: Default::default(),                },                window_id,                presenter,            );        }    });} /// Dispatches typed characters through the window, so they are routed by the rendered element/// tree rather than delivered straight to a chosen view.fn type_characters(    app: &mut App,    presenter: &Rc<RefCell<Presenter>>,    window_id: WindowId,    chars: &str,) {    let presenter = presenter.clone();    let chars = chars.to_string();    app.update(move |ctx| {        ctx.simulate_window_event(Event::TypedCharacters { chars }, window_id, presenter);    });} /// The center of the find bar's query input, as laid out in the last painted scene.fn find_input_center(    find_bar: &ViewHandle<CodeEditorFind>,    presenter: &Rc<RefCell<Presenter>>,    app: &App,) -> Vector2F {    let position_id = find_bar.read(app, |find_bar, _| {        find_bar.find_editor_position_id_for_test().to_string()    });    let bounds = presenter        .borrow()        .position_cache()        .get_position(&position_id)        .expect("find input should have been painted");    bounds.origin() + bounds.size() * 0.5} /// 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)); 

Lines 1–500 of 2351.