object.rs

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