706 lines · 23.3 KB · Rust
RawDownload
use editor::{DisplayPoint, Editor, SelectionEffects, ToOffset, ToPoint, movement};use gpui::{Action, actions};use gpui::{Context, Window};use language::{CharClassifier, CharKind};use text::{Bias, SelectionGoal}; use crate::{    Vim,    motion::{Motion, right},    state::Mode,}; actions!(    vim,    [        /// Switches to normal mode after the cursor (Helix-style).        HelixNormalAfter,        /// Inserts at the beginning of the selection.        HelixInsert,        /// Appends at the end of the selection.        HelixAppend,    ]); pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {    Vim::action(editor, cx, Vim::helix_normal_after);    Vim::action(editor, cx, Vim::helix_insert);    Vim::action(editor, cx, Vim::helix_append);} impl Vim {    pub fn helix_normal_after(        &mut self,        action: &HelixNormalAfter,        window: &mut Window,        cx: &mut Context<Self>,    ) {        if self.active_operator().is_some() {            self.operator_stack.clear();            self.sync_vim_settings(window, cx);            return;        }        self.stop_recording_immediately(action.boxed_clone(), cx);        self.switch_mode(Mode::HelixNormal, false, window, cx);        return;    }     pub fn helix_normal_motion(        &mut self,        motion: Motion,        times: Option<usize>,        window: &mut Window,        cx: &mut Context<Self>,    ) {        self.helix_move_cursor(motion, times, window, cx);    }     fn helix_find_range_forward(        &mut self,        times: Option<usize>,        window: &mut Window,        cx: &mut Context<Self>,        mut is_boundary: impl FnMut(char, char, &CharClassifier) -> bool,    ) {        self.update_editor(window, cx, |_, editor, window, cx| {            editor.change_selections(Default::default(), window, cx, |s| {                s.move_with(|map, selection| {                    let times = times.unwrap_or(1);                    let new_goal = SelectionGoal::None;                    let mut head = selection.head();                    let mut tail = selection.tail();                     if head == map.max_point() {                        return;                    }                     // collapse to block cursor                    if tail < head {                        tail = movement::left(map, head);                    } else {                        tail = head;                        head = movement::right(map, head);                    }                     // create a classifier                    let classifier = map.buffer_snapshot.char_classifier_at(head.to_point(map));                     for _ in 0..times {                        let (maybe_next_tail, next_head) =                            movement::find_boundary_trail(map, head, |left, right| {                                is_boundary(left, right, &classifier)                            });                         if next_head == head && maybe_next_tail.unwrap_or(next_head) == tail {                            break;                        }                         head = next_head;                        if let Some(next_tail) = maybe_next_tail {                            tail = next_tail;                        }                    }                     selection.set_tail(tail, new_goal);                    selection.set_head(head, new_goal);                });            });        });    }     fn helix_find_range_backward(        &mut self,        times: Option<usize>,        window: &mut Window,        cx: &mut Context<Self>,        mut is_boundary: impl FnMut(char, char, &CharClassifier) -> bool,    ) {        self.update_editor(window, cx, |_, editor, window, cx| {            editor.change_selections(Default::default(), window, cx, |s| {                s.move_with(|map, selection| {                    let times = times.unwrap_or(1);                    let new_goal = SelectionGoal::None;                    let mut head = selection.head();                    let mut tail = selection.tail();                     if head == DisplayPoint::zero() {                        return;                    }                     // collapse to block cursor                    if tail < head {                        tail = movement::left(map, head);                    } else {                        tail = head;                        head = movement::right(map, head);                    }                     selection.set_head(head, new_goal);                    selection.set_tail(tail, new_goal);                    // flip the selection                    selection.swap_head_tail();                    head = selection.head();                    tail = selection.tail();                     // create a classifier                    let classifier = map.buffer_snapshot.char_classifier_at(head.to_point(map));                     for _ in 0..times {                        let (maybe_next_tail, next_head) =                            movement::find_preceding_boundary_trail(map, head, |left, right| {                                is_boundary(left, right, &classifier)                            });                         if next_head == head && maybe_next_tail.unwrap_or(next_head) == tail {                            break;                        }                         head = next_head;                        if let Some(next_tail) = maybe_next_tail {                            tail = next_tail;                        }                    }                     selection.set_tail(tail, new_goal);                    selection.set_head(head, new_goal);                });            })        });    }     pub fn helix_move_and_collapse(        &mut self,        motion: Motion,        times: Option<usize>,        window: &mut Window,        cx: &mut Context<Self>,    ) {        self.update_editor(window, cx, |_, editor, window, cx| {            let text_layout_details = editor.text_layout_details(window);            editor.change_selections(Default::default(), window, cx, |s| {                s.move_with(|map, selection| {                    let goal = selection.goal;                    let cursor = if selection.is_empty() || selection.reversed {                        selection.head()                    } else {                        movement::left(map, selection.head())                    };                     let (point, goal) = motion                        .move_point(map, cursor, selection.goal, times, &text_layout_details)                        .unwrap_or((cursor, goal));                     selection.collapse_to(point, goal)                })            });        });    }     pub fn helix_move_cursor(        &mut self,        motion: Motion,        times: Option<usize>,        window: &mut Window,        cx: &mut Context<Self>,    ) {        match motion {            Motion::NextWordStart { ignore_punctuation } => {                self.helix_find_range_forward(times, window, cx, |left, right, classifier| {                    let left_kind = classifier.kind_with(left, ignore_punctuation);                    let right_kind = classifier.kind_with(right, ignore_punctuation);                    let at_newline = (left == '\n') ^ (right == '\n');                     let found = (left_kind != right_kind && right_kind != CharKind::Whitespace)                        || at_newline;                     found                })            }            Motion::NextWordEnd { ignore_punctuation } => {                self.helix_find_range_forward(times, window, cx, |left, right, classifier| {                    let left_kind = classifier.kind_with(left, ignore_punctuation);                    let right_kind = classifier.kind_with(right, ignore_punctuation);                    let at_newline = (left == '\n') ^ (right == '\n');                     let found = (left_kind != right_kind && left_kind != CharKind::Whitespace)                        || at_newline;                     found                })            }            Motion::PreviousWordStart { ignore_punctuation } => {                self.helix_find_range_backward(times, window, cx, |left, right, classifier| {                    let left_kind = classifier.kind_with(left, ignore_punctuation);                    let right_kind = classifier.kind_with(right, ignore_punctuation);                    let at_newline = (left == '\n') ^ (right == '\n');                     let found = (left_kind != right_kind && left_kind != CharKind::Whitespace)                        || at_newline;                     found                })            }            Motion::PreviousWordEnd { ignore_punctuation } => {                self.helix_find_range_backward(times, window, cx, |left, right, classifier| {                    let left_kind = classifier.kind_with(left, ignore_punctuation);                    let right_kind = classifier.kind_with(right, ignore_punctuation);                    let at_newline = (left == '\n') ^ (right == '\n');                     let found = (left_kind != right_kind && right_kind != CharKind::Whitespace)                        || at_newline;                     found                })            }            Motion::FindForward { .. } => {                self.update_editor(window, cx, |_, editor, window, cx| {                    let text_layout_details = editor.text_layout_details(window);                    editor.change_selections(Default::default(), window, cx, |s| {                        s.move_with(|map, selection| {                            let goal = selection.goal;                            let cursor = if selection.is_empty() || selection.reversed {                                selection.head()                            } else {                                movement::left(map, selection.head())                            };                             let (point, goal) = motion                                .move_point(                                    map,                                    cursor,                                    selection.goal,                                    times,                                    &text_layout_details,                                )                                .unwrap_or((cursor, goal));                            selection.set_tail(selection.head(), goal);                            selection.set_head(movement::right(map, point), goal);                        })                    });                });            }            Motion::FindBackward { .. } => {                self.update_editor(window, cx, |_, editor, window, cx| {                    let text_layout_details = editor.text_layout_details(window);                    editor.change_selections(Default::default(), window, cx, |s| {                        s.move_with(|map, selection| {                            let goal = selection.goal;                            let cursor = if selection.is_empty() || selection.reversed {                                selection.head()                            } else {                                movement::left(map, selection.head())                            };                             let (point, goal) = motion                                .move_point(                                    map,                                    cursor,                                    selection.goal,                                    times,                                    &text_layout_details,                                )                                .unwrap_or((cursor, goal));                            selection.set_tail(selection.head(), goal);                            selection.set_head(point, goal);                        })                    });                });            }            _ => self.helix_move_and_collapse(motion, times, window, cx),        }    }     fn helix_insert(&mut self, _: &HelixInsert, window: &mut Window, cx: &mut Context<Self>) {        self.start_recording(cx);        self.update_editor(window, cx, |_, editor, window, cx| {            editor.change_selections(Default::default(), window, cx, |s| {                s.move_with(|_map, selection| {                    // In helix normal mode, move cursor to start of selection and collapse                    if !selection.is_empty() {                        selection.collapse_to(selection.start, SelectionGoal::None);                    }                });            });        });        self.switch_mode(Mode::Insert, false, window, cx);    }     fn helix_append(&mut self, _: &HelixAppend, window: &mut Window, cx: &mut Context<Self>) {        self.start_recording(cx);        self.switch_mode(Mode::Insert, false, window, cx);        self.update_editor(window, cx, |_, editor, window, cx| {            editor.change_selections(Default::default(), window, cx, |s| {                s.move_with(|map, selection| {                    let point = if selection.is_empty() {                        right(map, selection.head(), 1)                    } else {                        selection.end                    };                    selection.collapse_to(point, SelectionGoal::None);                });            });        });    }     pub fn helix_replace(&mut self, text: &str, window: &mut Window, cx: &mut Context<Self>) {        self.update_editor(window, cx, |_, editor, window, cx| {            editor.transact(window, cx, |editor, window, cx| {                let (map, selections) = editor.selections.all_display(cx);                 // Store selection info for positioning after edit                let selection_info: Vec<_> = selections                    .iter()                    .map(|selection| {                        let range = selection.range();                        let start_offset = range.start.to_offset(&map, Bias::Left);                        let end_offset = range.end.to_offset(&map, Bias::Left);                        let was_empty = range.is_empty();                        let was_reversed = selection.reversed;                        (                            map.buffer_snapshot.anchor_at(start_offset, Bias::Left),                            end_offset - start_offset,                            was_empty,                            was_reversed,                        )                    })                    .collect();                 let mut edits = Vec::new();                for selection in &selections {                    let mut range = selection.range();                     // For empty selections, extend to replace one character                    if range.is_empty() {                        range.end = movement::saturating_right(&map, range.start);                    }                     let byte_range = range.start.to_offset(&map, Bias::Left)                        ..range.end.to_offset(&map, Bias::Left);                     if !byte_range.is_empty() {                        let replacement_text = text.repeat(byte_range.len());                        edits.push((byte_range, replacement_text));                    }                }                 editor.edit(edits, cx);                 // Restore selections based on original info                let snapshot = editor.buffer().read(cx).snapshot(cx);                let ranges: Vec<_> = selection_info                    .into_iter()                    .map(|(start_anchor, original_len, was_empty, was_reversed)| {                        let start_point = start_anchor.to_point(&snapshot);                        if was_empty {                            // For cursor-only, collapse to start                            start_point..start_point                        } else {                            // For selections, span the replaced text                            let replacement_len = text.len() * original_len;                            let end_offset = start_anchor.to_offset(&snapshot) + replacement_len;                            let end_point = snapshot.offset_to_point(end_offset);                            if was_reversed {                                end_point..start_point                            } else {                                start_point..end_point                            }                        }                    })                    .collect();                 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {                    s.select_ranges(ranges);                });            });        });        self.switch_mode(Mode::HelixNormal, true, window, cx);    }} #[cfg(test)]mod test {    use indoc::indoc;     use crate::{state::Mode, test::VimTestContext};     #[gpui::test]    async fn test_word_motions(cx: &mut gpui::TestAppContext) {        let mut cx = VimTestContext::new(cx, true).await;        // «        // ˇ        // »        cx.set_state(            indoc! {"            Th«e quiˇ»ck brown            fox jumps over            the lazy dog."},            Mode::HelixNormal,        );         cx.simulate_keystrokes("w");         cx.assert_state(            indoc! {"            The qu«ick ˇ»brown            fox jumps over            the lazy dog."},            Mode::HelixNormal,        );         cx.simulate_keystrokes("w");         cx.assert_state(            indoc! {"            The quick «brownˇ»            fox jumps over            the lazy dog."},            Mode::HelixNormal,        );         cx.simulate_keystrokes("2 b");         cx.assert_state(            indoc! {"            The «ˇquick »brown            fox jumps over            the lazy dog."},            Mode::HelixNormal,        );         cx.simulate_keystrokes("down e up");         cx.assert_state(            indoc! {"            The quicˇk brown            fox jumps over            the lazy dog."},            Mode::HelixNormal,        );         cx.set_state("aa\n  «ˇbb»", Mode::HelixNormal);         cx.simulate_keystroke("b");         cx.assert_state("aa\n«ˇ  »bb", Mode::HelixNormal);    }     #[gpui::test]    async fn test_delete(cx: &mut gpui::TestAppContext) {        let mut cx = VimTestContext::new(cx, true).await;         // test delete a selection        cx.set_state(            indoc! {"            The qu«ick ˇ»brown            fox jumps over            the lazy dog."},            Mode::HelixNormal,        );         cx.simulate_keystrokes("d");

Lines 1–500 of 706.