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}