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}