use std::{ error::Error, fmt::{self, Display}, ops::Range, }; use editor::{DisplayPoint, display_map::DisplaySnapshot, movement}; use text::Selection; use crate::{ helix::boundary::{FuzzyBoundary, ImmediateBoundary}, object::Object as VimObject, }; /// A text object from helix or an extra one pub trait HelixTextObject { fn range( &self, map: &DisplaySnapshot, relative_to: Range, around: bool, ) -> Option>; fn next_range( &self, map: &DisplaySnapshot, relative_to: Range, around: bool, ) -> Option>; fn previous_range( &self, map: &DisplaySnapshot, relative_to: Range, around: bool, ) -> Option>; } impl VimObject { /// Returns the range of the object the cursor is over. /// Follows helix convention. pub fn helix_range( self, map: &DisplaySnapshot, selection: Selection, around: bool, ) -> Result>, VimToHelixError> { let cursor = cursor_range(&selection, map); if let Some(helix_object) = self.to_helix_object() { Ok(helix_object.range(map, cursor, around)) } else { Err(VimToHelixError) } } /// Returns the range of the next object the cursor is not over. /// Follows helix convention. pub fn helix_next_range( self, map: &DisplaySnapshot, selection: Selection, around: bool, ) -> Result>, VimToHelixError> { let cursor = cursor_range(&selection, map); if let Some(helix_object) = self.to_helix_object() { Ok(helix_object.next_range(map, cursor, around)) } else { Err(VimToHelixError) } } /// Returns the range of the previous object the cursor is not over. /// Follows helix convention. pub fn helix_previous_range( self, map: &DisplaySnapshot, selection: Selection, around: bool, ) -> Result>, VimToHelixError> { let cursor = cursor_range(&selection, map); if let Some(helix_object) = self.to_helix_object() { Ok(helix_object.previous_range(map, cursor, around)) } else { Err(VimToHelixError) } } } #[derive(Debug)] pub struct VimToHelixError; impl Display for VimToHelixError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!( f, "Not all vim text objects have an implemented helix equivalent" ) } } impl Error for VimToHelixError {} impl VimObject { fn to_helix_object(self) -> Option> { Some(match self { Self::AngleBrackets => Box::new(ImmediateBoundary::AngleBrackets), Self::BackQuotes => Box::new(ImmediateBoundary::BackQuotes), Self::CurlyBrackets => Box::new(ImmediateBoundary::CurlyBrackets), Self::DoubleQuotes => Box::new(ImmediateBoundary::DoubleQuotes), Self::Paragraph => Box::new(FuzzyBoundary::Paragraph), Self::Parentheses => Box::new(ImmediateBoundary::Parentheses), Self::Quotes => Box::new(ImmediateBoundary::SingleQuotes), Self::Sentence => Box::new(FuzzyBoundary::Sentence), Self::SquareBrackets => Box::new(ImmediateBoundary::SquareBrackets), Self::Subword { ignore_punctuation } => { Box::new(ImmediateBoundary::Subword { ignore_punctuation }) } Self::VerticalBars => Box::new(ImmediateBoundary::VerticalBars), Self::Word { ignore_punctuation } => { Box::new(ImmediateBoundary::Word { ignore_punctuation }) } _ => return None, }) } } /// Returns the start of the cursor of a selection, whether that is collapsed or not. pub(crate) fn cursor_range( selection: &Selection, map: &DisplaySnapshot, ) -> Range { if selection.is_empty() | selection.reversed { selection.head()..movement::right(map, selection.head()) } else { movement::left(map, selection.head())..selection.head() } } #[cfg(test)] mod test { use db::indoc; use crate::{state::Mode, test::VimTestContext}; #[gpui::test] async fn test_select_word_object(cx: &mut gpui::TestAppContext) { let mut cx = VimTestContext::new(cx, true).await; let start = indoc! {" The quick brˇowˇnˇ fox «ˇjumps» ov«er the laˇ»zy dogˇ " }; cx.set_state(start, Mode::HelixNormal); cx.simulate_keystrokes("m i w"); cx.assert_state( indoc! {" The quick «brownˇ» fox «jumpsˇ» over the «lazyˇ» dogˇ " }, Mode::HelixNormal, ); cx.set_state(start, Mode::HelixNormal); cx.simulate_keystrokes("m a w"); cx.assert_state( indoc! {" The quick« brownˇ» fox «jumps ˇ»over the «lazy ˇ»dogˇ " }, Mode::HelixNormal, ); } }