1use std::{
  2    error::Error,
  3    fmt::{self, Display},
  4    ops::Range,
  5};
  6
  7use editor::{DisplayPoint, display_map::DisplaySnapshot, movement};
  8use text::Selection;
  9
 10use crate::{
 11    helix::boundary::{FuzzyBoundary, ImmediateBoundary},
 12    object::Object as VimObject,
 13};
 14
 15/// A text object from helix or an extra one
 16pub trait HelixTextObject {
 17    fn range(
 18        &self,
 19        map: &DisplaySnapshot,
 20        relative_to: Range<DisplayPoint>,
 21        around: bool,
 22    ) -> Option<Range<DisplayPoint>>;
 23
 24    fn next_range(
 25        &self,
 26        map: &DisplaySnapshot,
 27        relative_to: Range<DisplayPoint>,
 28        around: bool,
 29    ) -> Option<Range<DisplayPoint>>;
 30
 31    fn previous_range(
 32        &self,
 33        map: &DisplaySnapshot,
 34        relative_to: Range<DisplayPoint>,
 35        around: bool,
 36    ) -> Option<Range<DisplayPoint>>;
 37}
 38
 39impl VimObject {
 40    /// Returns the range of the object the cursor is over.
 41    /// Follows helix convention.
 42    pub fn helix_range(
 43        self,
 44        map: &DisplaySnapshot,
 45        selection: Selection<DisplayPoint>,
 46        around: bool,
 47    ) -> Result<Option<Range<DisplayPoint>>, VimToHelixError> {
 48        let cursor = cursor_range(&selection, map);
 49        if let Some(helix_object) = self.to_helix_object() {
 50            Ok(helix_object.range(map, cursor, around))
 51        } else {
 52            Err(VimToHelixError)
 53        }
 54    }
 55    /// Returns the range of the next object the cursor is not over.
 56    /// Follows helix convention.
 57    pub fn helix_next_range(
 58        self,
 59        map: &DisplaySnapshot,
 60        selection: Selection<DisplayPoint>,
 61        around: bool,
 62    ) -> Result<Option<Range<DisplayPoint>>, VimToHelixError> {
 63        let cursor = cursor_range(&selection, map);
 64        if let Some(helix_object) = self.to_helix_object() {
 65            Ok(helix_object.next_range(map, cursor, around))
 66        } else {
 67            Err(VimToHelixError)
 68        }
 69    }
 70    /// Returns the range of the previous object the cursor is not over.
 71    /// Follows helix convention.
 72    pub fn helix_previous_range(
 73        self,
 74        map: &DisplaySnapshot,
 75        selection: Selection<DisplayPoint>,
 76        around: bool,
 77    ) -> Result<Option<Range<DisplayPoint>>, VimToHelixError> {
 78        let cursor = cursor_range(&selection, map);
 79        if let Some(helix_object) = self.to_helix_object() {
 80            Ok(helix_object.previous_range(map, cursor, around))
 81        } else {
 82            Err(VimToHelixError)
 83        }
 84    }
 85}
 86
 87#[derive(Debug)]
 88pub struct VimToHelixError;
 89impl Display for VimToHelixError {
 90    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
 91        write!(
 92            f,
 93            "Not all vim text objects have an implemented helix equivalent"
 94        )
 95    }
 96}
 97impl Error for VimToHelixError {}
 98
 99impl VimObject {
100    fn to_helix_object(self) -> Option<Box<dyn HelixTextObject>> {
101        Some(match self {
102            Self::AngleBrackets => Box::new(ImmediateBoundary::AngleBrackets),
103            Self::BackQuotes => Box::new(ImmediateBoundary::BackQuotes),
104            Self::CurlyBrackets => Box::new(ImmediateBoundary::CurlyBrackets),
105            Self::DoubleQuotes => Box::new(ImmediateBoundary::DoubleQuotes),
106            Self::Paragraph => Box::new(FuzzyBoundary::Paragraph),
107            Self::Parentheses => Box::new(ImmediateBoundary::Parentheses),
108            Self::Quotes => Box::new(ImmediateBoundary::SingleQuotes),
109            Self::Sentence => Box::new(FuzzyBoundary::Sentence),
110            Self::SquareBrackets => Box::new(ImmediateBoundary::SquareBrackets),
111            Self::Subword { ignore_punctuation } => {
112                Box::new(ImmediateBoundary::Subword { ignore_punctuation })
113            }
114            Self::VerticalBars => Box::new(ImmediateBoundary::VerticalBars),
115            Self::Word { ignore_punctuation } => {
116                Box::new(ImmediateBoundary::Word { ignore_punctuation })
117            }
118            _ => return None,
119        })
120    }
121}
122
123/// Returns the start of the cursor of a selection, whether that is collapsed or not.
124pub(crate) fn cursor_range(
125    selection: &Selection<DisplayPoint>,
126    map: &DisplaySnapshot,
127) -> Range<DisplayPoint> {
128    if selection.is_empty() | selection.reversed {
129        selection.head()..movement::right(map, selection.head())
130    } else {
131        movement::left(map, selection.head())..selection.head()
132    }
133}
134
135#[cfg(test)]
136mod test {
137    use db::indoc;
138
139    use crate::{state::Mode, test::VimTestContext};
140
141    #[gpui::test]
142    async fn test_select_word_object(cx: &mut gpui::TestAppContext) {
143        let mut cx = VimTestContext::new(cx, true).await;
144        let start = indoc! {"
145                The quick brˇowˇnˇ
146                fox «ˇjumps» ov«er
147                the laˇ»zy dogˇ
148
149                "
150        };
151
152        cx.set_state(start, Mode::HelixNormal);
153
154        cx.simulate_keystrokes("m i w");
155
156        cx.assert_state(
157            indoc! {"
158            The quick «brownˇ»
159            fox «jumpsˇ» over
160            the «lazyˇ» dogˇ
161
162            "
163            },
164            Mode::HelixNormal,
165        );
166
167        cx.set_state(start, Mode::HelixNormal);
168
169        cx.simulate_keystrokes("m a w");
170
171        cx.assert_state(
172            indoc! {"
173            The quick« brownˇ»
174            fox «jumps ˇ»over
175            the «lazy ˇ»dogˇ
176
177            "
178            },
179            Mode::HelixNormal,
180        );
181    }
182}