use std::{cmp, sync::Arc};

use collections::HashMap;
use editor::{
    display_map::{DisplaySnapshot, ToDisplayPoint},
    movement,
    scroll::autoscroll::Autoscroll,
    Bias, DisplayPoint, Editor,
};
use gpui::{actions, AppContext, ViewContext, WindowContext};
use language::{Selection, SelectionGoal};
use workspace::Workspace;

use crate::{
    motion::Motion,
    object::Object,
    state::{Mode, Operator},
    utils::copy_selections_content,
    Vim,
};

actions!(
    vim,
    [
        ToggleVisual,
        ToggleVisualLine,
        ToggleVisualBlock,
        VisualDelete,
        VisualYank,
        OtherEnd,
    ]
);

pub fn init(cx: &mut AppContext) {
    cx.add_action(|_, _: &ToggleVisual, cx: &mut ViewContext<Workspace>| {
        toggle_mode(Mode::Visual, cx)
    });
    cx.add_action(|_, _: &ToggleVisualLine, cx: &mut ViewContext<Workspace>| {
        toggle_mode(Mode::VisualLine, cx)
    });
    cx.add_action(
        |_, _: &ToggleVisualBlock, cx: &mut ViewContext<Workspace>| {
            toggle_mode(Mode::VisualBlock, cx)
        },
    );
    cx.add_action(other_end);
    cx.add_action(delete);
    cx.add_action(yank);
}

pub fn visual_motion(motion: Motion, times: Option<usize>, cx: &mut WindowContext) {
    Vim::update(cx, |vim, cx| {
        vim.update_active_editor(cx, |editor, cx| {
            if vim.state().mode == Mode::VisualBlock
                && !matches!(
                    motion,
                    Motion::EndOfLine {
                        display_lines: false
                    }
                )
            {
                let is_up_or_down = matches!(motion, Motion::Up { .. } | Motion::Down { .. });
                visual_block_motion(is_up_or_down, editor, cx, |map, point, goal| {
                    motion.move_point(map, point, goal, times)
                })
            } else {
                editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
                    s.move_with(|map, selection| {
                        let was_reversed = selection.reversed;
                        let mut current_head = selection.head();

                        // our motions assume the current character is after the cursor,
                        // but in (forward) visual mode the current character is just
                        // before the end of the selection.

                        // If the file ends with a newline (which is common) we don't do this.
                        // so that if you go to the end of such a file you can use "up" to go
                        // to the previous line and have it work somewhat as expected.
                        if !selection.reversed
                            && !selection.is_empty()
                            && !(selection.end.column() == 0 && selection.end == map.max_point())
                        {
                            current_head = movement::left(map, selection.end)
                        }

                        let Some((new_head, goal)) =
                            motion.move_point(map, current_head, selection.goal, times)
                        else {
                            return;
                        };

                        selection.set_head(new_head, goal);

                        // ensure the current character is included in the selection.
                        if !selection.reversed {
                            let next_point = if vim.state().mode == Mode::VisualBlock {
                                movement::saturating_right(map, selection.end)
                            } else {
                                movement::right(map, selection.end)
                            };

                            if !(next_point.column() == 0 && next_point == map.max_point()) {
                                selection.end = next_point;
                            }
                        }

                        // vim always ensures the anchor character stays selected.
                        // if our selection has reversed, we need to move the opposite end
                        // to ensure the anchor is still selected.
                        if was_reversed && !selection.reversed {
                            selection.start = movement::left(map, selection.start);
                        } else if !was_reversed && selection.reversed {
                            selection.end = movement::right(map, selection.end);
                        }
                    })
                });
            }
        });
    });
}

pub fn visual_block_motion(
    preserve_goal: bool,
    editor: &mut Editor,
    cx: &mut ViewContext<Editor>,
    mut move_selection: impl FnMut(
        &DisplaySnapshot,
        DisplayPoint,
        SelectionGoal,
    ) -> Option<(DisplayPoint, SelectionGoal)>,
) {
    editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
        let map = &s.display_map();
        let mut head = s.newest_anchor().head().to_display_point(map);
        let mut tail = s.oldest_anchor().tail().to_display_point(map);

        let (start, end) = match s.newest_anchor().goal {
            SelectionGoal::ColumnRange { start, end } if preserve_goal => (start, end),
            SelectionGoal::Column(start) if preserve_goal => (start, start + 1),
            _ => (tail.column(), head.column()),
        };
        let goal = SelectionGoal::ColumnRange { start, end };

        let was_reversed = tail.column() > head.column();
        if !was_reversed && !preserve_goal {
            head = movement::saturating_left(map, head);
        }

        let Some((new_head, _)) = move_selection(&map, head, goal) else {
            return;
        };
        head = new_head;

        let is_reversed = tail.column() > head.column();
        if was_reversed && !is_reversed {
            tail = movement::left(map, tail)
        } else if !was_reversed && is_reversed {
            tail = movement::right(map, tail)
        }
        if !is_reversed && !preserve_goal {
            head = movement::saturating_right(map, head)
        }

        let columns = if is_reversed {
            head.column()..tail.column()
        } else if head.column() == tail.column() {
            head.column()..(head.column() + 1)
        } else {
            tail.column()..head.column()
        };

        let mut selections = Vec::new();
        let mut row = tail.row();

        loop {
            let start = map.clip_point(DisplayPoint::new(row, columns.start), Bias::Left);
            let end = map.clip_point(DisplayPoint::new(row, columns.end), Bias::Left);
            if columns.start <= map.line_len(row) {
                let selection = Selection {
                    id: s.new_selection_id(),
                    start: start.to_point(map),
                    end: end.to_point(map),
                    reversed: is_reversed,
                    goal: goal.clone(),
                };

                selections.push(selection);
            }
            if row == head.row() {
                break;
            }
            if tail.row() > head.row() {
                row -= 1
            } else {
                row += 1
            }
        }

        s.select(selections);
    })
}

pub fn visual_object(object: Object, cx: &mut WindowContext) {
    Vim::update(cx, |vim, cx| {
        if let Some(Operator::Object { around }) = vim.active_operator() {
            vim.pop_operator(cx);
            let current_mode = vim.state().mode;
            let target_mode = object.target_visual_mode(current_mode);
            if target_mode != current_mode {
                vim.switch_mode(target_mode, true, cx);
            }

            vim.update_active_editor(cx, |editor, cx| {
                editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
                    s.move_with(|map, selection| {
                        let mut head = selection.head();

                        // all our motions assume that the current character is
                        // after the cursor; however in the case of a visual selection
                        // the current character is before the cursor.
                        if !selection.reversed {
                            head = movement::left(map, head);
                        }

                        if let Some(range) = object.range(map, head, around) {
                            if !range.is_empty() {
                                let expand_both_ways =
                                    if object.always_expands_both_ways() || selection.is_empty() {
                                        true
                                        // contains only one character
                                    } else if let Some((_, start)) =
                                        map.reverse_chars_at(selection.end).next()
                                    {
                                        selection.start == start
                                    } else {
                                        false
                                    };

                                if expand_both_ways {
                                    selection.start = cmp::min(selection.start, range.start);
                                    selection.end = cmp::max(selection.end, range.end);
                                } else if selection.reversed {
                                    selection.start = range.start;
                                } else {
                                    selection.end = range.end;
                                }
                            }
                        }
                    });
                });
            });
        }
    });
}

fn toggle_mode(mode: Mode, cx: &mut ViewContext<Workspace>) {
    Vim::update(cx, |vim, cx| {
        if vim.state().mode == mode {
            vim.switch_mode(Mode::Normal, false, cx);
        } else {
            vim.switch_mode(mode, false, cx);
        }
    })
}

pub fn other_end(_: &mut Workspace, _: &OtherEnd, cx: &mut ViewContext<Workspace>) {
    Vim::update(cx, |vim, cx| {
        vim.update_active_editor(cx, |editor, cx| {
            editor.change_selections(None, cx, |s| {
                s.move_with(|_, selection| {
                    selection.reversed = !selection.reversed;
                })
            })
        })
    });
}

pub fn delete(_: &mut Workspace, _: &VisualDelete, cx: &mut ViewContext<Workspace>) {
    Vim::update(cx, |vim, cx| {
        vim.update_active_editor(cx, |editor, cx| {
            let mut original_columns: HashMap<_, _> = Default::default();
            let line_mode = editor.selections.line_mode;

            editor.transact(cx, |editor, cx| {
                editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
                    s.move_with(|map, selection| {
                        if line_mode {
                            let mut position = selection.head();
                            if !selection.reversed {
                                position = movement::left(map, position);
                            }
                            original_columns.insert(selection.id, position.to_point(map).column);
                        }
                        selection.goal = SelectionGoal::None;
                    });
                });
                copy_selections_content(editor, line_mode, cx);
                editor.insert("", cx);

                // Fixup cursor position after the deletion
                editor.set_clip_at_line_ends(true, cx);
                editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
                    s.move_with(|map, selection| {
                        let mut cursor = selection.head().to_point(map);

                        if let Some(column) = original_columns.get(&selection.id) {
                            cursor.column = *column
                        }
                        let cursor = map.clip_point(cursor.to_display_point(map), Bias::Left);
                        selection.collapse_to(cursor, selection.goal)
                    });
                    if vim.state().mode == Mode::VisualBlock {
                        s.select_anchors(vec![s.first_anchor()])
                    }
                });
            })
        });
        vim.switch_mode(Mode::Normal, true, cx);
    });
}

pub fn yank(_: &mut Workspace, _: &VisualYank, cx: &mut ViewContext<Workspace>) {
    Vim::update(cx, |vim, cx| {
        vim.update_active_editor(cx, |editor, cx| {
            let line_mode = editor.selections.line_mode;
            copy_selections_content(editor, line_mode, cx);
            editor.change_selections(None, cx, |s| {
                s.move_with(|_, selection| {
                    selection.collapse_to(selection.start, SelectionGoal::None)
                });
                if vim.state().mode == Mode::VisualBlock {
                    s.select_anchors(vec![s.first_anchor()])
                }
            });
        });
        vim.switch_mode(Mode::Normal, true, cx);
    });
}

pub(crate) fn visual_replace(text: Arc<str>, cx: &mut WindowContext) {
    Vim::update(cx, |vim, cx| {
        vim.update_active_editor(cx, |editor, cx| {
            editor.transact(cx, |editor, cx| {
                let (display_map, selections) = editor.selections.all_adjusted_display(cx);

                // Selections are biased right at the start. So we need to store
                // anchors that are biased left so that we can restore the selections
                // after the change
                let stable_anchors = editor
                    .selections
                    .disjoint_anchors()
                    .into_iter()
                    .map(|selection| {
                        let start = selection.start.bias_left(&display_map.buffer_snapshot);
                        start..start
                    })
                    .collect::<Vec<_>>();

                let mut edits = Vec::new();
                for selection in selections.iter() {
                    let selection = selection.clone();
                    for row_range in
                        movement::split_display_range_by_lines(&display_map, selection.range())
                    {
                        let range = row_range.start.to_offset(&display_map, Bias::Right)
                            ..row_range.end.to_offset(&display_map, Bias::Right);
                        let text = text.repeat(range.len());
                        edits.push((range, text));
                    }
                }

                editor.buffer().update(cx, |buffer, cx| {
                    buffer.edit(edits, None, cx);
                });
                editor.change_selections(None, cx, |s| s.select_ranges(stable_anchors));
            });
        });
        vim.switch_mode(Mode::Normal, false, cx);
    });
}

#[cfg(test)]
mod test {
    use indoc::indoc;
    use workspace::item::Item;

    use crate::{
        state::Mode,
        test::{NeovimBackedTestContext, VimTestContext},
    };

    #[gpui::test]
    async fn test_enter_visual_mode(cx: &mut gpui::TestAppContext) {
        let mut cx = NeovimBackedTestContext::new(cx).await;

        cx.set_shared_state(indoc! {
            "The ˇquick brown
            fox jumps over
            the lazy dog"
        })
        .await;
        let cursor = cx.update_editor(|editor, cx| editor.pixel_position_of_cursor(cx));

        // entering visual mode should select the character
        // under cursor
        cx.simulate_shared_keystrokes(["v"]).await;
        cx.assert_shared_state(indoc! { "The «qˇ»uick brown
            fox jumps over
            the lazy dog"})
            .await;
        cx.update_editor(|editor, cx| assert_eq!(cursor, editor.pixel_position_of_cursor(cx)));

        // forwards motions should extend the selection
        cx.simulate_shared_keystrokes(["w", "j"]).await;
        cx.assert_shared_state(indoc! { "The «quick brown
            fox jumps oˇ»ver
            the lazy dog"})
            .await;

        cx.simulate_shared_keystrokes(["escape"]).await;
        assert_eq!(Mode::Normal, cx.neovim_mode().await);
        cx.assert_shared_state(indoc! { "The quick brown
            fox jumps ˇover
            the lazy dog"})
            .await;

        // motions work backwards
        cx.simulate_shared_keystrokes(["v", "k", "b"]).await;
        cx.assert_shared_state(indoc! { "The «ˇquick brown
            fox jumps o»ver
            the lazy dog"})
            .await;

        // works on empty lines
        cx.set_shared_state(indoc! {"
            a
            ˇ
            b
            "})
            .await;
        let cursor = cx.update_editor(|editor, cx| editor.pixel_position_of_cursor(cx));
        cx.simulate_shared_keystrokes(["v"]).await;
        cx.assert_shared_state(indoc! {"
            a
            «
            ˇ»b
        "})
            .await;
        cx.update_editor(|editor, cx| assert_eq!(cursor, editor.pixel_position_of_cursor(cx)));

        // toggles off again
        cx.simulate_shared_keystrokes(["v"]).await;
        cx.assert_shared_state(indoc! {"
            a
            ˇ
            b
            "})
            .await;

        // works at the end of a document
        cx.set_shared_state(indoc! {"
            a
            b
            ˇ"})
            .await;

        cx.simulate_shared_keystrokes(["v"]).await;
        cx.assert_shared_state(indoc! {"
            a
            b
            ˇ"})
            .await;
        assert_eq!(cx.mode(), cx.neovim_mode().await);
    }

    #[gpui::test]
    async fn test_enter_visual_line_mode(cx: &mut gpui::TestAppContext) {
        let mut cx = NeovimBackedTestContext::new(cx).await;

        cx.set_shared_state(indoc! {
            "The ˇquick brown
            fox jumps over
            the lazy dog"
        })
        .await;
        cx.simulate_shared_keystrokes(["shift-v"]).await;
        cx.assert_shared_state(indoc! { "The «qˇ»uick brown
            fox jumps over
            the lazy dog"})
            .await;
        assert_eq!(cx.mode(), cx.neovim_mode().await);
        cx.simulate_shared_keystrokes(["x"]).await;
        cx.assert_shared_state(indoc! { "fox ˇjumps over
        the lazy dog"})
            .await;

        // it should work on empty lines
        cx.set_shared_state(indoc! {"
            a
            ˇ
            b"})
            .await;
        cx.simulate_shared_keystrokes(["shift-v"]).await;
        cx.assert_shared_state(indoc! { "
            a
            «
            ˇ»b"})
            .await;
        cx.simulate_shared_keystrokes(["x"]).await;
        cx.assert_shared_state(indoc! { "
            a
            ˇb"})
            .await;

        // it should work at the end of the document
        cx.set_shared_state(indoc! {"
            a
            b
            ˇ"})
            .await;
        let cursor = cx.update_editor(|editor, cx| editor.pixel_position_of_cursor(cx));
        cx.simulate_shared_keystrokes(["shift-v"]).await;
        cx.assert_shared_state(indoc! {"
            a
            b
            ˇ"})
            .await;
        assert_eq!(cx.mode(), cx.neovim_mode().await);
        cx.update_editor(|editor, cx| assert_eq!(cursor, editor.pixel_position_of_cursor(cx)));
        cx.simulate_shared_keystrokes(["x"]).await;
        cx.assert_shared_state(indoc! {"
            a
            ˇb"})
            .await;
    }

    #[gpui::test]
    async fn test_visual_delete(cx: &mut gpui::TestAppContext) {
        let mut cx = NeovimBackedTestContext::new(cx).await;

        cx.assert_binding_matches(["v", "w"], "The quick ˇbrown")
            .await;

        cx.assert_binding_matches(["v", "w", "x"], "The quick ˇbrown")
            .await;
        cx.assert_binding_matches(
            ["v", "w", "j", "x"],
            indoc! {"
                The ˇquick brown
                fox jumps over
                the lazy dog"},
        )
        .await;
        // Test pasting code copied on delete
        cx.simulate_shared_keystrokes(["j", "p"]).await;
        cx.assert_state_matches().await;

        let mut cx = cx.binding(["v", "w", "j", "x"]);
        cx.assert_all(indoc! {"
                The ˇquick brown
                fox jumps over
                the ˇlazy dog"})
            .await;
        let mut cx = cx.binding(["v", "b", "k", "x"]);
        cx.assert_all(indoc! {"
                The ˇquick brown
                fox jumps ˇover
                the ˇlazy dog"})
            .await;
    }

    #[gpui::test]
    async fn test_visual_line_delete(cx: &mut gpui::TestAppContext) {
        let mut cx = NeovimBackedTestContext::new(cx).await;

        cx.set_shared_state(indoc! {"
                The quˇick brown
                fox jumps over
                the lazy dog"})
            .await;
        cx.simulate_shared_keystrokes(["shift-v", "x"]).await;
        cx.assert_state_matches().await;

        // Test pasting code copied on delete
        cx.simulate_shared_keystroke("p").await;
        cx.assert_state_matches().await;

        cx.set_shared_state(indoc! {"
                The quick brown
                fox jumps over
                the laˇzy dog"})
            .await;
        cx.simulate_shared_keystrokes(["shift-v", "x"]).await;
        cx.assert_state_matches().await;
        cx.assert_shared_clipboard("the lazy dog\n").await;

        for marked_text in cx.each_marked_position(indoc! {"
                        The quˇick brown
                        fox jumps over
                        the lazy dog"})
        {
            cx.set_shared_state(&marked_text).await;
            cx.simulate_shared_keystrokes(["shift-v", "j", "x"]).await;
            cx.assert_state_matches().await;
            // Test pasting code copied on delete
            cx.simulate_shared_keystroke("p").await;
            cx.assert_state_matches().await;
        }

        cx.set_shared_state(indoc! {"
            The ˇlong line
            should not
            crash
            "})
            .await;
        cx.simulate_shared_keystrokes(["shift-v", "$", "x"]).await;
        cx.assert_state_matches().await;
    }

    #[gpui::test]
    async fn test_visual_yank(cx: &mut gpui::TestAppContext) {
        let mut cx = NeovimBackedTestContext::new(cx).await;

        cx.set_shared_state("The quick ˇbrown").await;
        cx.simulate_shared_keystrokes(["v", "w", "y"]).await;
        cx.assert_shared_state("The quick ˇbrown").await;
        cx.assert_shared_clipboard("brown").await;

        cx.set_shared_state(indoc! {"
                The ˇquick brown
                fox jumps over
                the lazy dog"})
            .await;
        cx.simulate_shared_keystrokes(["v", "w", "j", "y"]).await;
        cx.assert_shared_state(indoc! {"
                    The ˇquick brown
                    fox jumps over
                    the lazy dog"})
            .await;
        cx.assert_shared_clipboard(indoc! {"
                quick brown
                fox jumps o"})
            .await;

        cx.set_shared_state(indoc! {"
                    The quick brown
                    fox jumps over
                    the ˇlazy dog"})
            .await;
        cx.simulate_shared_keystrokes(["v", "w", "j", "y"]).await;
        cx.assert_shared_state(indoc! {"
                    The quick brown
                    fox jumps over
                    the ˇlazy dog"})
            .await;
        cx.assert_shared_clipboard("lazy d").await;
        cx.simulate_shared_keystrokes(["shift-v", "y"]).await;
        cx.assert_shared_clipboard("the lazy dog\n").await;

        let mut cx = cx.binding(["v", "b", "k", "y"]);
        cx.set_shared_state(indoc! {"
                    The ˇquick brown
                    fox jumps over
                    the lazy dog"})
            .await;
        cx.simulate_shared_keystrokes(["v", "b", "k", "y"]).await;
        cx.assert_shared_state(indoc! {"
                    ˇThe quick brown
                    fox jumps over
                    the lazy dog"})
            .await;
        cx.assert_clipboard_content(Some("The q"));
    }

    #[gpui::test]
    async fn test_visual_block_mode(cx: &mut gpui::TestAppContext) {
        let mut cx = NeovimBackedTestContext::new(cx).await;

        cx.set_shared_state(indoc! {
            "The ˇquick brown
             fox jumps over
             the lazy dog"
        })
        .await;
        cx.simulate_shared_keystrokes(["ctrl-v"]).await;
        cx.assert_shared_state(indoc! {
            "The «qˇ»uick brown
            fox jumps over
            the lazy dog"
        })
        .await;
        cx.simulate_shared_keystrokes(["2", "down"]).await;
        cx.assert_shared_state(indoc! {
            "The «qˇ»uick brown
            fox «jˇ»umps over
            the «lˇ»azy dog"
        })
        .await;
        cx.simulate_shared_keystrokes(["e"]).await;
        cx.assert_shared_state(indoc! {
            "The «quicˇ»k brown
            fox «jumpˇ»s over
            the «lazyˇ» dog"
        })
        .await;
        cx.simulate_shared_keystrokes(["^"]).await;
        cx.assert_shared_state(indoc! {
            "«ˇThe q»uick brown
            «ˇfox j»umps over
            «ˇthe l»azy dog"
        })
        .await;
        cx.simulate_shared_keystrokes(["$"]).await;
        cx.assert_shared_state(indoc! {
            "The «quick brownˇ»
            fox «jumps overˇ»
            the «lazy dogˇ»"
        })
        .await;
        cx.simulate_shared_keystrokes(["shift-f", " "]).await;
        cx.assert_shared_state(indoc! {
            "The «quickˇ» brown
            fox «jumpsˇ» over
            the «lazy ˇ»dog"
        })
        .await;

        // toggling through visual mode works as expected
        cx.simulate_shared_keystrokes(["v"]).await;
        cx.assert_shared_state(indoc! {
            "The «quick brown
            fox jumps over
            the lazy ˇ»dog"
        })
        .await;
        cx.simulate_shared_keystrokes(["ctrl-v"]).await;
        cx.assert_shared_state(indoc! {
            "The «quickˇ» brown
            fox «jumpsˇ» over
            the «lazy ˇ»dog"
        })
        .await;

        cx.set_shared_state(indoc! {
            "The ˇquick
             brown
             fox
             jumps over the

             lazy dog
            "
        })
        .await;
        cx.simulate_shared_keystrokes(["ctrl-v", "down", "down"])
            .await;
        cx.assert_shared_state(indoc! {
            "The«ˇ q»uick
            bro«ˇwn»
            foxˇ
            jumps over the

            lazy dog
            "
        })
        .await;
        cx.simulate_shared_keystrokes(["down"]).await;
        cx.assert_shared_state(indoc! {
            "The «qˇ»uick
            brow«nˇ»
            fox
            jump«sˇ» over the

            lazy dog
            "
        })
        .await;
        cx.simulate_shared_keystroke("left").await;
        cx.assert_shared_state(indoc! {
            "The«ˇ q»uick
            bro«ˇwn»
            foxˇ
            jum«ˇps» over the

            lazy dog
            "
        })
        .await;
        cx.simulate_shared_keystrokes(["s", "o", "escape"]).await;
        cx.assert_shared_state(indoc! {
            "Theˇouick
            broo
            foxo
            jumo over the

            lazy dog
            "
        })
        .await;

        //https://github.com/zed-industries/community/issues/1950
        cx.set_shared_state(indoc! {
            "Theˇ quick brown

            fox jumps over
            the lazy dog
            "
        })
        .await;
        cx.simulate_shared_keystrokes(["l", "ctrl-v", "j", "j"])
            .await;
        cx.assert_shared_state(indoc! {
            "The «qˇ»uick brown

            fox «jˇ»umps over
            the lazy dog
            "
        })
        .await;
    }

    #[gpui::test]
    async fn test_visual_block_insert(cx: &mut gpui::TestAppContext) {
        let mut cx = NeovimBackedTestContext::new(cx).await;

        cx.set_shared_state(indoc! {
            "ˇThe quick brown
            fox jumps over
            the lazy dog
            "
        })
        .await;
        cx.simulate_shared_keystrokes(["ctrl-v", "9", "down"]).await;
        cx.assert_shared_state(indoc! {
            "«Tˇ»he quick brown
            «fˇ»ox jumps over
            «tˇ»he lazy dog
            ˇ"
        })
        .await;

        cx.simulate_shared_keystrokes(["shift-i", "k", "escape"])
            .await;
        cx.assert_shared_state(indoc! {
            "ˇkThe quick brown
            kfox jumps over
            kthe lazy dog
            k"
        })
        .await;

        cx.set_shared_state(indoc! {
            "ˇThe quick brown
            fox jumps over
            the lazy dog
            "
        })
        .await;
        cx.simulate_shared_keystrokes(["ctrl-v", "9", "down"]).await;
        cx.assert_shared_state(indoc! {
            "«Tˇ»he quick brown
            «fˇ»ox jumps over
            «tˇ»he lazy dog
            ˇ"
        })
        .await;
        cx.simulate_shared_keystrokes(["c", "k", "escape"]).await;
        cx.assert_shared_state(indoc! {
            "ˇkhe quick brown
            kox jumps over
            khe lazy dog
            k"
        })
        .await;
    }

    #[gpui::test]
    async fn test_visual_object(cx: &mut gpui::TestAppContext) {
        let mut cx = NeovimBackedTestContext::new(cx).await;

        cx.set_shared_state("hello (in [parˇens] o)").await;
        cx.simulate_shared_keystrokes(["ctrl-v", "l"]).await;
        cx.simulate_shared_keystrokes(["a", "]"]).await;
        cx.assert_shared_state("hello (in «[parens]ˇ» o)").await;
        assert_eq!(cx.mode(), Mode::Visual);
        cx.simulate_shared_keystrokes(["i", "("]).await;
        cx.assert_shared_state("hello («in [parens] oˇ»)").await;

        cx.set_shared_state("hello in a wˇord again.").await;
        cx.simulate_shared_keystrokes(["ctrl-v", "l", "i", "w"])
            .await;
        cx.assert_shared_state("hello in a w«ordˇ» again.").await;
        assert_eq!(cx.mode(), Mode::VisualBlock);
        cx.simulate_shared_keystrokes(["o", "a", "s"]).await;
        cx.assert_shared_state("«ˇhello in a word» again.").await;
        assert_eq!(cx.mode(), Mode::Visual);
    }

    #[gpui::test]
    async fn test_mode_across_command(cx: &mut gpui::TestAppContext) {
        let mut cx = VimTestContext::new(cx, true).await;

        cx.set_state("aˇbc", Mode::Normal);
        cx.simulate_keystrokes(["ctrl-v"]);
        assert_eq!(cx.mode(), Mode::VisualBlock);
        cx.simulate_keystrokes(["cmd-shift-p", "escape"]);
        assert_eq!(cx.mode(), Mode::VisualBlock);
    }
}
