sabiql 1.11.0

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

use crate::app::update::input::vim::types::{
    JsonbDetailVimContext, SearchContinuation, VimCommand, VimModeTransition, VimNavigation,
    VimOperator,
};

pub(in crate::app::update::input::vim) fn command(
    command: VimCommand,
    ctx: JsonbDetailVimContext,
) -> Option<Action> {
    match ctx {
        JsonbDetailVimContext::Viewing => match command {
            VimCommand::Navigation(navigation) => navigation_action(navigation),
            VimCommand::ModeTransition(VimModeTransition::Escape) => Some(Action::CloseJsonbDetail),
            VimCommand::ModeTransition(VimModeTransition::Insert) => Some(Action::JsonbEnterEdit),
            VimCommand::ModeTransition(VimModeTransition::Append) => {
                Some(Action::JsonbAppendInsert)
            }
            VimCommand::SearchContinuation(SearchContinuation::Next) => {
                Some(Action::JsonbSearchNext)
            }
            VimCommand::SearchContinuation(SearchContinuation::Prev) => {
                Some(Action::JsonbSearchPrev)
            }
            VimCommand::Operator(VimOperator::Yank) => Some(Action::JsonbYankAll),
            VimCommand::ModeTransition(VimModeTransition::ConfirmOrEnter)
            | VimCommand::Operator(VimOperator::Delete) => None,
        },
        JsonbDetailVimContext::Editing => match command {
            VimCommand::ModeTransition(VimModeTransition::Escape) => Some(Action::JsonbExitEdit),
            _ => None,
        },
        JsonbDetailVimContext::Searching => None,
    }
}

fn navigation_action(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::JsonbEdit,
        direction,
    })
}

#[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)
    }

    #[test]
    fn enter_is_ignored_in_viewing_mode() {
        let ctx = VimSurfaceContext::JsonbDetail(JsonbDetailVimContext::Viewing);

        let action = action_for_key(&combo(Key::Enter), ctx);

        assert!(action.is_none());
    }

    #[test]
    fn append_opens_edit_mode() {
        let ctx = VimSurfaceContext::JsonbDetail(JsonbDetailVimContext::Viewing);

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

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

    #[rstest]
    #[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('G'), CursorMove::LastLine)]
    #[case(Key::Char('H'), CursorMove::ViewportTop)]
    #[case(Key::Char('M'), CursorMove::ViewportMiddle)]
    #[case(Key::Char('L'), CursorMove::ViewportBottom)]
    fn extended_navigation_maps_to_cursor_moves(#[case] key: Key, #[case] expected: CursorMove) {
        let ctx = VimSurfaceContext::JsonbDetail(JsonbDetailVimContext::Viewing);

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

        assert!(matches!(
            action,
            Some(Action::TextMoveCursor {
                target: InputTarget::JsonbEdit,
                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::JsonbDetail(JsonbDetailVimContext::Viewing),
        );

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

    #[test]
    fn search_next_moves_to_match() {
        let ctx = VimSurfaceContext::JsonbDetail(JsonbDetailVimContext::Viewing);

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

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

    #[test]
    fn yank_copies_full_json() {
        let ctx = VimSurfaceContext::JsonbDetail(JsonbDetailVimContext::Viewing);

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

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

    #[test]
    fn left_navigation_moves_text_cursor_left() {
        let ctx = VimSurfaceContext::JsonbDetail(JsonbDetailVimContext::Viewing);

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

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