mod boundary;mod duplicate;mod object;mod paste;mod select;mod surround; use editor::display_map::DisplaySnapshot;use editor::{ DisplayPoint, Editor, EditorSettings, HideMouseCursorOrigin, MultiBufferOffset, SelectionEffects, ToOffset, ToPoint, movement,};use gpui::actions;use gpui::{Context, Window};use language::{CharClassifier, CharKind, Point};use search::{BufferSearchBar, SearchOptions};use settings::Settings;use text::{Bias, SelectionGoal};use workspace::searchable::FilteredSearchRange;use workspace::searchable::{self, Direction}; use crate::motion::{self, MotionKind};use crate::state::{Operator, SearchState};use crate::{ PushHelixSurroundAdd, PushHelixSurroundDelete, PushHelixSurroundReplace, Vim, motion::{Motion, right}, state::Mode,}; actions!( vim, [ /// Yanks the current selection or character if no selection. HelixYank, /// Inserts at the beginning of the selection. HelixInsert, /// Appends at the end of the selection. HelixAppend, /// Goes to the location of the last modification. HelixGotoLastModification, /// Select entire line or multiple lines, extending downwards. HelixSelectLine, /// Select all matches of a given pattern within the current selection. HelixSelectRegex, /// Removes all but the one selection that was created last. /// `Newest` can eventually be `Primary`. HelixKeepNewestSelection, /// Copies all selections below. HelixDuplicateBelow, /// Copies all selections above. HelixDuplicateAbove, /// Delete the selection and enter edit mode. HelixSubstitute, /// Delete the selection and enter edit mode, without yanking the selection. HelixSubstituteNoYank, /// Select the next match for the current search query. HelixSelectNext, /// Select the previous match for the current search query. HelixSelectPrevious, ]); pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) { Vim::action(editor, cx, Vim::helix_select_lines); Vim::action(editor, cx, Vim::helix_insert); Vim::action(editor, cx, Vim::helix_append); Vim::action(editor, cx, Vim::helix_yank); Vim::action(editor, cx, Vim::helix_goto_last_modification); Vim::action(editor, cx, Vim::helix_paste); Vim::action(editor, cx, Vim::helix_select_regex); Vim::action(editor, cx, Vim::helix_keep_newest_selection); Vim::action(editor, cx, |vim, _: &HelixDuplicateBelow, window, cx| { let times = Vim::take_count(cx); vim.helix_duplicate_selections_below(times, window, cx); }); Vim::action(editor, cx, |vim, _: &HelixDuplicateAbove, window, cx| { let times = Vim::take_count(cx); vim.helix_duplicate_selections_above(times, window, cx); }); Vim::action(editor, cx, Vim::helix_substitute); Vim::action(editor, cx, Vim::helix_substitute_no_yank); Vim::action(editor, cx, Vim::helix_select_next); Vim::action(editor, cx, Vim::helix_select_previous); Vim::action(editor, cx, |vim, _: &PushHelixSurroundAdd, window, cx| { vim.clear_operator(window, cx); vim.push_operator(Operator::HelixSurroundAdd, window, cx); }); Vim::action( editor, cx, |vim, _: &PushHelixSurroundReplace, window, cx| { vim.clear_operator(window, cx); vim.push_operator( Operator::HelixSurroundReplace { replaced_char: None, }, window, cx, ); }, ); Vim::action( editor, cx, |vim, _: &PushHelixSurroundDelete, window, cx| { vim.clear_operator(window, cx); vim.push_operator(Operator::HelixSurroundDelete, window, cx); }, );} impl Vim { 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); } pub fn helix_select_motion( &mut self, motion: Motion, times: Option<usize>, window: &mut Window, cx: &mut Context<Self>, ) { self.update_editor(cx, |_, editor, cx| { let text_layout_details = editor.text_layout_details(window, cx); editor.change_selections(Default::default(), window, cx, |s| { if let Motion::ZedSearchResult { new_selections, .. } = &motion { s.select_anchor_ranges(new_selections.clone()); return; }; s.move_with(&mut |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 (new_head, goal) = match motion { // Going to next word start is special cased // since Vim differs from Helix in that motion // Vim: `w` goes to the first character of a word // Helix: `w` goes to the character before a word Motion::NextWordStart { ignore_punctuation } => { let mut head = movement::right(map, current_head); let classifier = map.buffer_snapshot().char_classifier_at(head.to_point(map)); for _ in 0..times.unwrap_or(1) { let (_, new_head) = movement::find_boundary_trail(map, head, &mut |left, right| { Self::is_boundary_right(ignore_punctuation)( left, right, &classifier, ) }); head = new_head; } head = movement::left(map, head); (head, SelectionGoal::None) } _ => motion .move_point( map, current_head, selection.goal, times, &text_layout_details, ) .unwrap_or((current_head, selection.goal)), }; selection.set_head(new_head, goal); // ensure the current character is included in the selection. if !selection.reversed { let next_point = 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); } }) }); }); } /// Updates all selections based on where the cursors are. fn helix_new_selections( &mut self, window: &mut Window, cx: &mut Context<Self>, change: &mut dyn FnMut( // the start of the cursor DisplayPoint, &DisplaySnapshot, ) -> Option<(DisplayPoint, DisplayPoint)>, ) { self.update_editor(cx, |_, editor, cx| { editor.change_selections(Default::default(), window, cx, |s| { s.move_with(&mut |map, selection| { let cursor_start = if selection.reversed || selection.is_empty() { selection.head() } else { movement::left(map, selection.head()) }; let Some((head, tail)) = change(cursor_start, map) else { return; }; selection.set_head_tail(head, tail, SelectionGoal::None); }); }); }); } fn helix_find_range_forward( &mut self, times: Option<usize>, window: &mut Window, cx: &mut Context<Self>, is_boundary: &mut dyn FnMut(char, char, &CharClassifier) -> bool, ) { let times = times.unwrap_or(1); self.helix_new_selections(window, cx, &mut |cursor, map| { let mut head = movement::right(map, cursor); let mut tail = cursor; let classifier = map.buffer_snapshot().char_classifier_at(head.to_point(map)); if head == map.max_point() { return None; } for _ in 0..times { let (maybe_next_tail, next_head) = movement::find_boundary_trail(map, head, &mut |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; } } Some((head, tail)) }); } fn helix_find_range_backward( &mut self, times: Option<usize>, window: &mut Window, cx: &mut Context<Self>, is_boundary: &mut dyn FnMut(char, char, &CharClassifier) -> bool, ) { let times = times.unwrap_or(1); self.helix_new_selections(window, cx, &mut |cursor, map| { let mut head = cursor; // The original cursor was one character wide, // but the search starts from the left side of it, // so to include that space the selection must end one character to the right. let mut tail = movement::right(map, cursor); let classifier = map.buffer_snapshot().char_classifier_at(head.to_point(map)); if head == DisplayPoint::zero() { return None; } for _ in 0..times { let (maybe_next_tail, next_head) = movement::find_preceding_boundary_trail(map, head, &mut |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; } } Some((head, tail)) }); } pub fn helix_move_and_collapse( &mut self, motion: Motion, times: Option<usize>, window: &mut Window, cx: &mut Context<Self>, ) { self.update_editor(cx, |_, editor, cx| { let text_layout_details = editor.text_layout_details(window, cx); editor.change_selections(Default::default(), window, cx, |s| { s.move_with(&mut |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) }) }); }); } fn is_boundary_right( ignore_punctuation: bool, ) -> impl FnMut(char, char, &CharClassifier) -> bool { move |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'); (left_kind != right_kind && right_kind != CharKind::Whitespace) || at_newline } } fn is_boundary_left( ignore_punctuation: bool, ) -> impl FnMut(char, char, &CharClassifier) -> bool { move |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'); (left_kind != right_kind && left_kind != CharKind::Whitespace) || at_newline } } 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 } => { let mut is_boundary = Self::is_boundary_right(ignore_punctuation); self.helix_find_range_forward(times, window, cx, &mut is_boundary) } Motion::NextWordEnd { ignore_punctuation } => { let mut is_boundary = Self::is_boundary_left(ignore_punctuation); self.helix_find_range_forward(times, window, cx, &mut is_boundary) } Motion::PreviousWordStart { ignore_punctuation } => { let mut is_boundary = Self::is_boundary_left(ignore_punctuation); self.helix_find_range_backward(times, window, cx, &mut is_boundary) } Motion::PreviousWordEnd { ignore_punctuation } => { let mut is_boundary = Self::is_boundary_right(ignore_punctuation); self.helix_find_range_backward(times, window, cx, &mut is_boundary) } Motion::EndOfLine { .. } => { // In Helix mode, EndOfLine should position cursor ON the last character, // not after it. We therefore need special handling for it. self.update_editor(cx, |_, editor, cx| { let text_layout_details = editor.text_layout_details(window, cx); editor.change_selections(Default::default(), window, cx, |s| { s.move_with(&mut |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, goal, times, &text_layout_details) .unwrap_or((cursor, goal)); // Move left by one character to position on the last character let adjusted_point = movement::saturating_left(map, point); selection.collapse_to(adjusted_point, SelectionGoal::None) }) }); }); } Motion::FindForward { before, char, mode, smartcase, } => { self.helix_new_selections(window, cx, &mut |cursor, map| { let start = cursor; let mut last_boundary = start; for _ in 0..times.unwrap_or(1) { last_boundary = movement::find_boundary( map, movement::right(map, last_boundary), mode, &mut |left, right| { let current_char = if before { right } else { left }; motion::is_character_match(char, current_char, smartcase) }, ); } Some((last_boundary, start)) }); } Motion::FindBackward { after, char, mode, smartcase, } => { self.helix_new_selections(window, cx, &mut |cursor, map| { let start = cursor; let mut last_boundary = start; for _ in 0..times.unwrap_or(1) { last_boundary = movement::find_preceding_boundary_display_point( map, last_boundary, mode, &mut |left, right| { let current_char = if after { left } else { right }; motion::is_character_match(char, current_char, smartcase) }, ); } // The original cursor was one character wide, // but the search started from the left side of it, // so to include that space the selection must end one character to the right. Some((last_boundary, movement::right(map, start))) }); } _ => self.helix_move_and_collapse(motion, times, window, cx), } } pub fn helix_yank(&mut self, _: &HelixYank, window: &mut Window, cx: &mut Context<Self>) { self.update_editor(cx, |vim, editor, cx| { let has_selection = editor .selections .all_adjusted(&editor.display_snapshot(cx)) .iter() .any(|selection| !selection.is_empty()); if !has_selection { // If no selection, expand to current character (like 'v' does) editor.change_selections(Default::default(), window, cx, |s| { s.move_with(&mut |map, selection| { let head = selection.head(); let new_head = movement::saturating_right(map, head); selection.set_tail(head, SelectionGoal::None); selection.set_head(new_head, SelectionGoal::None); }); }); vim.yank_selections_content( editor, crate::motion::MotionKind::Exclusive, window, cx, ); editor.change_selections(Default::default(), window, cx, |s| { s.move_with(&mut |_map, selection| { selection.collapse_to(selection.start, SelectionGoal::None); }); }); } else { // Yank the selection(s) vim.yank_selections_content( editor,Lines 1–500 of 1851. Load more
mod boundary; mod duplicate; mod object; mod paste; mod select; mod surround; use editor::display_map::DisplaySnapshot; use editor::{ DisplayPoint, Editor, EditorSettings, HideMouseCursorOrigin, MultiBufferOffset, SelectionEffects, ToOffset, ToPoint, movement, }; use gpui::actions; use gpui::{Context, Window}; use language::{CharClassifier, CharKind, Point}; use search::{BufferSearchBar, SearchOptions}; use settings::Settings; use text::{Bias, SelectionGoal}; use workspace::searchable::FilteredSearchRange; use workspace::searchable::{self, Direction}; use crate::motion::{self, MotionKind}; use crate::state::{Operator, SearchState}; use crate::{ PushHelixSurroundAdd, PushHelixSurroundDelete, PushHelixSurroundReplace, Vim, motion::{Motion, right}, state::Mode, }; actions!( vim, [ /// Yanks the current selection or character if no selection. HelixYank, /// Inserts at the beginning of the selection. HelixInsert, /// Appends at the end of the selection. HelixAppend, /// Goes to the location of the last modification. HelixGotoLastModification, /// Select entire line or multiple lines, extending downwards. HelixSelectLine, /// Select all matches of a given pattern within the current selection. HelixSelectRegex, /// Removes all but the one selection that was created last. /// `Newest` can eventually be `Primary`. HelixKeepNewestSelection, /// Copies all selections below. HelixDuplicateBelow, /// Copies all selections above. HelixDuplicateAbove, /// Delete the selection and enter edit mode. HelixSubstitute, /// Delete the selection and enter edit mode, without yanking the selection. HelixSubstituteNoYank, /// Select the next match for the current search query. HelixSelectNext, /// Select the previous match for the current search query. HelixSelectPrevious, ] ); pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) { Vim::action(editor, cx, Vim::helix_select_lines); Vim::action(editor, cx, Vim::helix_insert); Vim::action(editor, cx, Vim::helix_append); Vim::action(editor, cx, Vim::helix_yank); Vim::action(editor, cx, Vim::helix_goto_last_modification); Vim::action(editor, cx, Vim::helix_paste); Vim::action(editor, cx, Vim::helix_select_regex); Vim::action(editor, cx, Vim::helix_keep_newest_selection); Vim::action(editor, cx, |vim, _: &HelixDuplicateBelow, window, cx| { let times = Vim::take_count(cx); vim.helix_duplicate_selections_below(times, window, cx); }); Vim::action(editor, cx, |vim, _: &HelixDuplicateAbove, window, cx| { let times = Vim::take_count(cx); vim.helix_duplicate_selections_above(times, window, cx); }); Vim::action(editor, cx, Vim::helix_substitute); Vim::action(editor, cx, Vim::helix_substitute_no_yank); Vim::action(editor, cx, Vim::helix_select_next); Vim::action(editor, cx, Vim::helix_select_previous); Vim::action(editor, cx, |vim, _: &PushHelixSurroundAdd, window, cx| { vim.clear_operator(window, cx); vim.push_operator(Operator::HelixSurroundAdd, window, cx); }); Vim::action( editor, cx, |vim, _: &PushHelixSurroundReplace, window, cx| { vim.clear_operator(window, cx); vim.push_operator( Operator::HelixSurroundReplace { replaced_char: None, }, window, cx, ); }, ); Vim::action( editor, cx, |vim, _: &PushHelixSurroundDelete, window, cx| { vim.clear_operator(window, cx); vim.push_operator(Operator::HelixSurroundDelete, window, cx); }, ); } impl Vim { 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); } pub fn helix_select_motion( &mut self, motion: Motion, times: Option<usize>, window: &mut Window, cx: &mut Context<Self>, ) { self.update_editor(cx, |_, editor, cx| { let text_layout_details = editor.text_layout_details(window, cx); editor.change_selections(Default::default(), window, cx, |s| { if let Motion::ZedSearchResult { new_selections, .. } = &motion { s.select_anchor_ranges(new_selections.clone()); return; }; s.move_with(&mut |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 (new_head, goal) = match motion { // Going to next word start is special cased // since Vim differs from Helix in that motion // Vim: `w` goes to the first character of a word // Helix: `w` goes to the character before a word Motion::NextWordStart { ignore_punctuation } => { let mut head = movement::right(map, current_head); let classifier = map.buffer_snapshot().char_classifier_at(head.to_point(map)); for _ in 0..times.unwrap_or(1) { let (_, new_head) = movement::find_boundary_trail(map, head, &mut |left, right| { Self::is_boundary_right(ignore_punctuation)( left, right, &classifier, ) }); head = new_head; } head = movement::left(map, head); (head, SelectionGoal::None) } _ => motion .move_point( map, current_head, selection.goal, times, &text_layout_details, ) .unwrap_or((current_head, selection.goal)), }; selection.set_head(new_head, goal); // ensure the current character is included in the selection. if !selection.reversed { let next_point = 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); } }) }); }); } /// Updates all selections based on where the cursors are. fn helix_new_selections( &mut self, window: &mut Window, cx: &mut Context<Self>, change: &mut dyn FnMut( // the start of the cursor DisplayPoint, &DisplaySnapshot, ) -> Option<(DisplayPoint, DisplayPoint)>, ) { self.update_editor(cx, |_, editor, cx| { editor.change_selections(Default::default(), window, cx, |s| { s.move_with(&mut |map, selection| { let cursor_start = if selection.reversed || selection.is_empty() { selection.head() } else { movement::left(map, selection.head()) }; let Some((head, tail)) = change(cursor_start, map) else { return; }; selection.set_head_tail(head, tail, SelectionGoal::None); }); }); }); } fn helix_find_range_forward( &mut self, times: Option<usize>, window: &mut Window, cx: &mut Context<Self>, is_boundary: &mut dyn FnMut(char, char, &CharClassifier) -> bool, ) { let times = times.unwrap_or(1); self.helix_new_selections(window, cx, &mut |cursor, map| { let mut head = movement::right(map, cursor); let mut tail = cursor; let classifier = map.buffer_snapshot().char_classifier_at(head.to_point(map)); if head == map.max_point() { return None; } for _ in 0..times { let (maybe_next_tail, next_head) = movement::find_boundary_trail(map, head, &mut |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; } } Some((head, tail)) }); } fn helix_find_range_backward( &mut self, times: Option<usize>, window: &mut Window, cx: &mut Context<Self>, is_boundary: &mut dyn FnMut(char, char, &CharClassifier) -> bool, ) { let times = times.unwrap_or(1); self.helix_new_selections(window, cx, &mut |cursor, map| { let mut head = cursor; // The original cursor was one character wide, // but the search starts from the left side of it, // so to include that space the selection must end one character to the right. let mut tail = movement::right(map, cursor); let classifier = map.buffer_snapshot().char_classifier_at(head.to_point(map)); if head == DisplayPoint::zero() { return None; } for _ in 0..times { let (maybe_next_tail, next_head) = movement::find_preceding_boundary_trail(map, head, &mut |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; } } Some((head, tail)) }); } pub fn helix_move_and_collapse( &mut self, motion: Motion, times: Option<usize>, window: &mut Window, cx: &mut Context<Self>, ) { self.update_editor(cx, |_, editor, cx| { let text_layout_details = editor.text_layout_details(window, cx); editor.change_selections(Default::default(), window, cx, |s| { s.move_with(&mut |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) }) }); }); } fn is_boundary_right( ignore_punctuation: bool, ) -> impl FnMut(char, char, &CharClassifier) -> bool { move |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'); (left_kind != right_kind && right_kind != CharKind::Whitespace) || at_newline } } fn is_boundary_left( ignore_punctuation: bool, ) -> impl FnMut(char, char, &CharClassifier) -> bool { move |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'); (left_kind != right_kind && left_kind != CharKind::Whitespace) || at_newline } } 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 } => { let mut is_boundary = Self::is_boundary_right(ignore_punctuation); self.helix_find_range_forward(times, window, cx, &mut is_boundary) } Motion::NextWordEnd { ignore_punctuation } => { let mut is_boundary = Self::is_boundary_left(ignore_punctuation); self.helix_find_range_forward(times, window, cx, &mut is_boundary) } Motion::PreviousWordStart { ignore_punctuation } => { let mut is_boundary = Self::is_boundary_left(ignore_punctuation); self.helix_find_range_backward(times, window, cx, &mut is_boundary) } Motion::PreviousWordEnd { ignore_punctuation } => { let mut is_boundary = Self::is_boundary_right(ignore_punctuation); self.helix_find_range_backward(times, window, cx, &mut is_boundary) } Motion::EndOfLine { .. } => { // In Helix mode, EndOfLine should position cursor ON the last character, // not after it. We therefore need special handling for it. self.update_editor(cx, |_, editor, cx| { let text_layout_details = editor.text_layout_details(window, cx); editor.change_selections(Default::default(), window, cx, |s| { s.move_with(&mut |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, goal, times, &text_layout_details) .unwrap_or((cursor, goal)); // Move left by one character to position on the last character let adjusted_point = movement::saturating_left(map, point); selection.collapse_to(adjusted_point, SelectionGoal::None) }) }); }); } Motion::FindForward { before, char, mode, smartcase, } => { self.helix_new_selections(window, cx, &mut |cursor, map| { let start = cursor; let mut last_boundary = start; for _ in 0..times.unwrap_or(1) { last_boundary = movement::find_boundary( map, movement::right(map, last_boundary), mode, &mut |left, right| { let current_char = if before { right } else { left }; motion::is_character_match(char, current_char, smartcase) }, ); } Some((last_boundary, start)) }); } Motion::FindBackward { after, char, mode, smartcase, } => { self.helix_new_selections(window, cx, &mut |cursor, map| { let start = cursor; let mut last_boundary = start; for _ in 0..times.unwrap_or(1) { last_boundary = movement::find_preceding_boundary_display_point( map, last_boundary, mode, &mut |left, right| { let current_char = if after { left } else { right }; motion::is_character_match(char, current_char, smartcase) }, ); } // The original cursor was one character wide, // but the search started from the left side of it, // so to include that space the selection must end one character to the right. Some((last_boundary, movement::right(map, start))) }); } _ => self.helix_move_and_collapse(motion, times, window, cx), } } pub fn helix_yank(&mut self, _: &HelixYank, window: &mut Window, cx: &mut Context<Self>) { self.update_editor(cx, |vim, editor, cx| { let has_selection = editor .selections .all_adjusted(&editor.display_snapshot(cx)) .iter() .any(|selection| !selection.is_empty()); if !has_selection { // If no selection, expand to current character (like 'v' does) editor.change_selections(Default::default(), window, cx, |s| { s.move_with(&mut |map, selection| { let head = selection.head(); let new_head = movement::saturating_right(map, head); selection.set_tail(head, SelectionGoal::None); selection.set_head(new_head, SelectionGoal::None); }); }); vim.yank_selections_content( editor, crate::motion::MotionKind::Exclusive, window, cx, ); editor.change_selections(Default::default(), window, cx, |s| { s.move_with(&mut |_map, selection| { selection.collapse_to(selection.start, SelectionGoal::None); }); }); } else { // Yank the selection(s) vim.yank_selections_content( editor,