660 lines · 25.8 KB · Rust
RawDownload
use std::{borrow::Cow, sync::Arc}; use collections::HashMap;use editor::{    display_map::ToDisplayPoint, movement, scroll::autoscroll::Autoscroll, Bias, ClipboardSelection,};use gpui::{actions, AppContext, ViewContext, WindowContext};use language::{AutoindentMode, SelectionGoal};use workspace::Workspace; use crate::{    motion::Motion,    object::Object,    state::{Mode, Operator},    utils::copy_selections_content,    Vim,}; actions!(vim, [VisualDelete, VisualChange, VisualYank, VisualPaste]); pub fn init(cx: &mut AppContext) {    cx.add_action(change);    cx.add_action(delete);    cx.add_action(yank);    cx.add_action(paste);} pub fn visual_motion(motion: Motion, times: Option<usize>, cx: &mut WindowContext) {    Vim::update(cx, |vim, cx| {        vim.update_active_editor(cx, |editor, cx| {            editor.change_selections(Some(Autoscroll::fit()), cx, |s| {                s.move_with(|map, selection| {                    let was_reversed = selection.reversed;                     if let Some((new_head, goal)) =                        motion.move_point(map, selection.head(), selection.goal, times)                    {                        selection.set_head(new_head, goal);                         if was_reversed && !selection.reversed {                            // Head was at the start of the selection, and now is at the end. We need to move the start                            // back by one if possible in order to compensate for this change.                            *selection.start.column_mut() =                                selection.start.column().saturating_sub(1);                            selection.start = map.clip_point(selection.start, Bias::Left);                        } else if !was_reversed && selection.reversed {                            // Head was at the end of the selection, and now is at the start. We need to move the end                            // forward by one if possible in order to compensate for this change.                            *selection.end.column_mut() = selection.end.column() + 1;                            selection.end = map.clip_point(selection.end, Bias::Right);                        }                    }                });            });        });    });} 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);             vim.update_active_editor(cx, |editor, cx| {                editor.change_selections(Some(Autoscroll::fit()), cx, |s| {                    s.move_with(|map, selection| {                        let head = selection.head();                        if let Some(mut range) = object.range(map, head, around) {                            if !range.is_empty() {                                if let Some((_, end)) = map.reverse_chars_at(range.end).next() {                                    range.end = end;                                }                                 if selection.is_empty() {                                    selection.start = range.start;                                    selection.end = range.end;                                } else if selection.reversed {                                    selection.start = range.start;                                } else {                                    selection.end = range.end;                                }                            }                        }                    });                });            });        }    });} pub fn change(_: &mut Workspace, _: &VisualChange, cx: &mut ViewContext<Workspace>) {    Vim::update(cx, |vim, cx| {        vim.update_active_editor(cx, |editor, cx| {            editor.set_clip_at_line_ends(false, cx);            // Compute edits and resulting anchor selections. If in line mode, adjust            // the anchor location and additional newline            let mut edits = Vec::new();            let mut new_selections = Vec::new();            let line_mode = editor.selections.line_mode;            editor.change_selections(None, cx, |s| {                s.move_with(|map, selection| {                    if !selection.reversed {                        // Head is at the end of the selection. Adjust the end position to                        // to include the character under the cursor.                        *selection.end.column_mut() = selection.end.column() + 1;                        selection.end = map.clip_point(selection.end, Bias::Right);                    }                     if line_mode {                        let range = selection.map(|p| p.to_point(map)).range();                        let expanded_range = map.expand_to_line(range);                        // If we are at the last line, the anchor needs to be after the newline so that                        // it is on a line of its own. Otherwise, the anchor may be after the newline                        let anchor = if expanded_range.end == map.buffer_snapshot.max_point() {                            map.buffer_snapshot.anchor_after(expanded_range.end)                        } else {                            map.buffer_snapshot.anchor_before(expanded_range.start)                        };                         edits.push((expanded_range, "\n"));                        new_selections.push(selection.map(|_| anchor));                    } else {                        let range = selection.map(|p| p.to_point(map)).range();                        let anchor = map.buffer_snapshot.anchor_after(range.end);                        edits.push((range, ""));                        new_selections.push(selection.map(|_| anchor));                    }                    selection.goal = SelectionGoal::None;                });            });            copy_selections_content(editor, editor.selections.line_mode, cx);            editor.edit_with_autoindent(edits, cx);            editor.change_selections(Some(Autoscroll::fit()), cx, |s| {                s.select_anchors(new_selections);            });        });        vim.switch_mode(Mode::Insert, false, cx);    });} pub fn delete(_: &mut Workspace, _: &VisualDelete, cx: &mut ViewContext<Workspace>) {    Vim::update(cx, |vim, cx| {        vim.update_active_editor(cx, |editor, cx| {            editor.set_clip_at_line_ends(false, cx);            let mut original_columns: HashMap<_, _> = Default::default();            let line_mode = editor.selections.line_mode;            editor.change_selections(Some(Autoscroll::fit()), cx, |s| {                s.move_with(|map, selection| {                    if line_mode {                        original_columns                            .insert(selection.id, selection.head().to_point(map).column);                    } else if !selection.reversed {                        // Head is at the end of the selection. Adjust the end position to                        // to include the character under the cursor.                        *selection.end.column_mut() = selection.end.column() + 1;                        selection.end = map.clip_point(selection.end, Bias::Right);                    }                    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)                });            });        });        vim.switch_mode(Mode::Normal, false, cx);    });} pub fn yank(_: &mut Workspace, _: &VisualYank, cx: &mut ViewContext<Workspace>) {    Vim::update(cx, |vim, cx| {        vim.update_active_editor(cx, |editor, cx| {            editor.set_clip_at_line_ends(false, cx);            let line_mode = editor.selections.line_mode;            if !line_mode {                editor.change_selections(None, cx, |s| {                    s.move_with(|map, selection| {                        if !selection.reversed {                            // Head is at the end of the selection. Adjust the end position to                            // to include the character under the cursor.                            *selection.end.column_mut() = selection.end.column() + 1;                            selection.end = map.clip_point(selection.end, Bias::Right);                        }                    });                });            }            copy_selections_content(editor, line_mode, cx);            editor.change_selections(None, cx, |s| {                s.move_with(|_, selection| {                    selection.collapse_to(selection.start, SelectionGoal::None)                });            });        });        vim.switch_mode(Mode::Normal, false, cx);    });} pub fn paste(_: &mut Workspace, _: &VisualPaste, cx: &mut ViewContext<Workspace>) {    Vim::update(cx, |vim, cx| {        vim.update_active_editor(cx, |editor, cx| {            editor.transact(cx, |editor, cx| {                if let Some(item) = cx.read_from_clipboard() {                    copy_selections_content(editor, editor.selections.line_mode, cx);                    let mut clipboard_text = Cow::Borrowed(item.text());                    if let Some(mut clipboard_selections) =                        item.metadata::<Vec<ClipboardSelection>>()                    {                        let (display_map, selections) = editor.selections.all_adjusted_display(cx);                        let all_selections_were_entire_line =                            clipboard_selections.iter().all(|s| s.is_entire_line);                        if clipboard_selections.len() != selections.len() {                            let mut newline_separated_text = String::new();                            let mut clipboard_selections =                                clipboard_selections.drain(..).peekable();                            let mut ix = 0;                            while let Some(clipboard_selection) = clipboard_selections.next() {                                newline_separated_text                                    .push_str(&clipboard_text[ix..ix + clipboard_selection.len]);                                ix += clipboard_selection.len;                                if clipboard_selections.peek().is_some() {                                    newline_separated_text.push('\n');                                }                            }                            clipboard_text = Cow::Owned(newline_separated_text);                        }                         let mut new_selections = Vec::new();                        editor.buffer().update(cx, |buffer, cx| {                            let snapshot = buffer.snapshot(cx);                            let mut start_offset = 0;                            let mut edits = Vec::new();                            for (ix, selection) in selections.iter().enumerate() {                                let to_insert;                                let linewise;                                if let Some(clipboard_selection) = clipboard_selections.get(ix) {                                    let end_offset = start_offset + clipboard_selection.len;                                    to_insert = &clipboard_text[start_offset..end_offset];                                    linewise = clipboard_selection.is_entire_line;                                    start_offset = end_offset;                                } else {                                    to_insert = clipboard_text.as_str();                                    linewise = all_selections_were_entire_line;                                }                                 let mut selection = selection.clone();                                if !selection.reversed {                                    let mut adjusted = selection.end;                                    // Head is at the end of the selection. Adjust the end position to                                    // to include the character under the cursor.                                    *adjusted.column_mut() = adjusted.column() + 1;                                    adjusted = display_map.clip_point(adjusted, Bias::Right);                                    // If the selection is empty, move both the start and end forward one                                    // character                                    if selection.is_empty() {                                        selection.start = adjusted;                                        selection.end = adjusted;                                    } else {                                        selection.end = adjusted;                                    }                                }                                 let range = selection.map(|p| p.to_point(&display_map)).range();                                 let new_position = if linewise {                                    edits.push((range.start..range.start, "\n"));                                    let mut new_position = range.start;                                    new_position.column = 0;                                    new_position.row += 1;                                    new_position                                } else {                                    range.start                                };                                 new_selections.push(selection.map(|_| new_position));                                 if linewise && to_insert.ends_with('\n') {                                    edits.push((                                        range.clone(),                                        &to_insert[0..to_insert.len().saturating_sub(1)],                                    ))                                } else {                                    edits.push((range.clone(), to_insert));                                }                                 if linewise {                                    edits.push((range.end..range.end, "\n"));                                }                            }                            drop(snapshot);                            buffer.edit(edits, Some(AutoindentMode::EachLine), cx);                        });                         editor.change_selections(Some(Autoscroll::fit()), cx, |s| {                            s.select(new_selections)                        });                    } else {                        editor.insert(&clipboard_text, cx);                    }                }            });        });        vim.switch_mode(Mode::Normal, false, cx);    });} pub(crate) fn visual_replace(text: Arc<str>, line: bool, 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 mut selection = selection.clone();                    if !line && !selection.reversed {                        // Head is at the end of the selection. Adjust the end position to                        // to include the character under the cursor.                        *selection.end.column_mut() = selection.end.column() + 1;                        selection.end = display_map.clip_point(selection.end, Bias::Right);                    }                     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 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            .binding(["v", "w", "j"]);        cx.assert_all(indoc! {"                The ˇquick brown                fox jumps ˇover                the ˇlazy dog"})            .await;        let mut cx = cx.binding(["v", "b", "k"]);        cx.assert_all(indoc! {"                The ˇquick brown                fox jumps ˇover                the ˇlazy dog"})            .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", "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            .binding(["shift-v", "x"]);        cx.assert(indoc! {"                The quˇick brown                fox jumps over                the lazy dog"})            .await;        // Test pasting code copied on delete        cx.simulate_shared_keystroke("p").await;        cx.assert_state_matches().await;         cx.assert_all(indoc! {"                The quick brown                fox juˇmps over                the laˇzy dog"})            .await;        let mut cx = cx.binding(["shift-v", "j", "x"]);        cx.assert(indoc! {"                The quˇick brown                fox jumps over                the lazy dog"})            .await;        // Test pasting code copied on delete        cx.simulate_shared_keystroke("p").await;        cx.assert_state_matches().await;         cx.assert_all(indoc! {"                The quick brown                fox juˇmps over                the laˇzy dog"})            .await;    }     #[gpui::test]    async fn test_visual_change(cx: &mut gpui::TestAppContext) {        let mut cx = NeovimBackedTestContext::new(cx)            .await            .binding(["v", "w", "c"]);        cx.assert("The quick ˇbrown").await;        let mut cx = cx.binding(["v", "w", "j", "c"]);        cx.assert_all(indoc! {"                The ˇquick brown                fox jumps ˇover                the ˇlazy dog"})            .await;        let mut cx = cx.binding(["v", "b", "k", "c"]);        cx.assert_all(indoc! {"                The ˇquick brown                fox jumps ˇover                the ˇlazy dog"})            .await;    }     #[gpui::test]    async fn test_visual_line_change(cx: &mut gpui::TestAppContext) {        let mut cx = NeovimBackedTestContext::new(cx)            .await            .binding(["shift-v", "c"]);        cx.assert(indoc! {"                The quˇick brown                fox jumps over                the lazy dog"})            .await;        // Test pasting code copied on change        cx.simulate_shared_keystrokes(["escape", "j", "p"]).await;        cx.assert_state_matches().await;         cx.assert_all(indoc! {"                The quick brown                fox juˇmps over                the laˇzy dog"})            .await;

Lines 1–500 of 660.