sabiql 1.11.0

A fast, driver-less TUI for browsing and editing PostgreSQL databases
Documentation
use crate::app::update::action::{
    Action, CursorMove, InputTarget, ScrollAmount, ScrollDirection, ScrollTarget,
};

use super::scroll;
use crate::app::update::input::vim::types::{
    SqlModalVimContext, VimCommand, VimModeTransition, VimNavigation, VimOperator,
};

pub(in crate::app::update::input::vim) fn command(
    command: VimCommand,
    ctx: SqlModalVimContext,
) -> Option<Action> {
    match ctx {
        SqlModalVimContext::QueryNormal => match command {
            VimCommand::Navigation(navigation) => query_navigation(navigation),
            VimCommand::ModeTransition(VimModeTransition::Escape) => Some(Action::CloseSqlModal),
            VimCommand::ModeTransition(VimModeTransition::Append) => {
                Some(Action::SqlModalAppendInsert)
            }
            VimCommand::ModeTransition(VimModeTransition::Insert) => {
                Some(Action::SqlModalEnterInsert)
            }
            VimCommand::Operator(VimOperator::Yank) => Some(Action::SqlModalYank),
            _ => None,
        },
        SqlModalVimContext::QueryEditing => match command {
            VimCommand::ModeTransition(VimModeTransition::Escape) => {
                Some(Action::SqlModalEnterNormal)
            }
            _ => None,
        },
        SqlModalVimContext::PlanViewer => viewer(command, ScrollTarget::ExplainPlan),
        SqlModalVimContext::CompareViewer => viewer(command, ScrollTarget::ExplainCompare),
    }
}

fn query_navigation(navigation: VimNavigation) -> Option<Action> {
    let direction = match navigation {
        VimNavigation::MoveLeft => CursorMove::Left,
        VimNavigation::MoveRight => CursorMove::Right,
        VimNavigation::MoveUp => CursorMove::Up,
        VimNavigation::MoveDown => CursorMove::Down,
        VimNavigation::MoveToFirst => CursorMove::FirstLine,
        VimNavigation::MoveToLast => CursorMove::LastLine,
        VimNavigation::MoveLineStart => CursorMove::LineStart,
        VimNavigation::MoveLineEnd => CursorMove::LineEnd,
        VimNavigation::MoveWordForward => CursorMove::WordForward,
        VimNavigation::MoveWordBackward => CursorMove::WordBackward,
        VimNavigation::ViewportTop => CursorMove::ViewportTop,
        VimNavigation::ViewportMiddle => CursorMove::ViewportMiddle,
        VimNavigation::ViewportBottom => CursorMove::ViewportBottom,
        _ => return None,
    };

    Some(Action::TextMoveCursor {
        target: InputTarget::SqlModal,
        direction,
    })
}

fn viewer(command: VimCommand, target: ScrollTarget) -> Option<Action> {
    match command {
        VimCommand::Navigation(VimNavigation::MoveDown) => {
            Some(scroll(target, ScrollDirection::Down, ScrollAmount::Line))
        }
        VimCommand::Navigation(VimNavigation::MoveUp) => {
            Some(scroll(target, ScrollDirection::Up, ScrollAmount::Line))
        }
        VimCommand::ModeTransition(VimModeTransition::Escape) => Some(Action::CloseSqlModal),
        VimCommand::Operator(VimOperator::Yank) => Some(Action::SqlModalYank),
        _ => None,
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::app::model::shared::key_sequence::Prefix;
    use crate::app::update::input::keybindings::{Key, KeyCombo};
    use crate::app::update::input::vim::{VimSurfaceContext, action_for_key};
    use rstest::rstest;

    fn combo(key: Key) -> KeyCombo {
        KeyCombo::plain(key)
    }

    fn combo_ctrl(key: Key) -> KeyCombo {
        KeyCombo::ctrl(key)
    }

    #[rstest]
    #[case(Key::Char('i'), false)]
    #[case(Key::Char('A'), true)]
    fn insert_and_confirm_enter_insert(#[case] key: Key, #[case] append: bool) {
        let ctx = VimSurfaceContext::SqlModal(SqlModalVimContext::QueryNormal);

        let action = action_for_key(&combo(key), ctx);

        if append {
            assert!(matches!(action, Some(Action::SqlModalAppendInsert)));
        } else {
            assert!(matches!(action, Some(Action::SqlModalEnterInsert)));
        }
    }

    #[test]
    fn yank_copies_query() {
        let ctx = VimSurfaceContext::SqlModal(SqlModalVimContext::QueryNormal);

        let action = action_for_key(&combo(Key::Char('y')), ctx);

        assert!(matches!(action, Some(Action::SqlModalYank)));
    }

    #[rstest]
    #[case(Key::Char('h'), CursorMove::Left)]
    #[case(Key::Char('j'), CursorMove::Down)]
    #[case(Key::Char('k'), CursorMove::Up)]
    #[case(Key::Char('l'), CursorMove::Right)]
    #[case(Key::Char('G'), CursorMove::LastLine)]
    #[case(Key::Char('0'), CursorMove::LineStart)]
    #[case(Key::Char('$'), CursorMove::LineEnd)]
    #[case(Key::Char('w'), CursorMove::WordForward)]
    #[case(Key::Char('b'), CursorMove::WordBackward)]
    #[case(Key::Char('H'), CursorMove::ViewportTop)]
    #[case(Key::Char('M'), CursorMove::ViewportMiddle)]
    #[case(Key::Char('L'), CursorMove::ViewportBottom)]
    fn normal_navigation_moves_sql_cursor(#[case] key: Key, #[case] expected: CursorMove) {
        let action = action_for_key(
            &combo(key),
            VimSurfaceContext::SqlModal(SqlModalVimContext::QueryNormal),
        );

        assert!(matches!(
            action,
            Some(Action::TextMoveCursor {
                target: InputTarget::SqlModal,
                direction,
            }) if direction == expected
        ));
    }

    #[test]
    fn gg_moves_to_first_line() {
        let action = crate::app::update::input::vim::action_for_input(
            &combo(Key::Char('g')),
            Some(Prefix::G),
            VimSurfaceContext::SqlModal(SqlModalVimContext::QueryNormal),
        );

        assert!(matches!(
            action,
            Some(Action::TextMoveCursor {
                target: InputTarget::SqlModal,
                direction: CursorMove::FirstLine,
            })
        ));
    }

    #[rstest]
    #[case(Key::Char('n'), ScrollDirection::Down)]
    #[case(Key::Char('p'), ScrollDirection::Up)]
    fn ctrl_aliases_scroll_by_line(#[case] key: Key, #[case] expected_direction: ScrollDirection) {
        let action = action_for_key(
            &combo_ctrl(key),
            VimSurfaceContext::SqlModal(SqlModalVimContext::PlanViewer),
        );

        assert!(matches!(
            action,
            Some(Action::Scroll {
                target: ScrollTarget::ExplainPlan,
                direction,
                amount: ScrollAmount::Line,
            }) if direction == expected_direction
        ));
    }
}