on master help sign in
w, b, e: Match kakoune's behavior in selecting by default.
59a0fc7Blaž Hrastnik
helix-core/src/movement.rs+61−44
@@ -61,99 +61,110 @@ pub fn move_vertically(
61 range
61 range
62}
62}
63
63
64−pub fn move_next_word_start(slice: RopeSlice, mut pos: usize, count: usize) -> usize {
64+pub fn move_next_word_start(slice: RopeSlice, mut begin: usize, count: usize) -> Option<Range> {
65+ let mut end = begin;
66+
65 for _ in 0..count {
67 for _ in 0..count {
66− if pos + 1 == slice.len_chars() {
68+ if begin + 1 == slice.len_chars() {
67− return pos;
69+ return None;
68 }
70 }
69
71
70− let mut ch = slice.char(pos);
72+ let mut ch = slice.char(begin);
71− let next = slice.char(pos + 1);
73+ let next = slice.char(begin + 1);
72
74
73 // if we're at the end of a word, or on whitespce right before new one
75 // if we're at the end of a word, or on whitespce right before new one
74 if categorize(ch) != categorize(next) {
76 if categorize(ch) != categorize(next) {
75− pos += 1;
77+ begin += 1;
76− ch = next;
77 }
78 }
78
79
80+ // return if not skip while?
81+ skip_over_next(slice, &mut begin, |ch| ch == '\n');
82+ ch = slice.char(begin);
83+
84+ end = begin + 1;
85+
79 if is_word(ch) {
86 if is_word(ch) {
80− skip_over_next(slice, &mut pos, is_word);
87+ skip_over_next(slice, &mut end, is_word);
81 } else if ch.is_ascii_punctuation() {
88 } else if ch.is_ascii_punctuation() {
82− skip_over_next(slice, &mut pos, |ch| ch.is_ascii_punctuation());
89+ skip_over_next(slice, &mut end, |ch| ch.is_ascii_punctuation());
83 }
90 }
84
91
85− // TODO: don't include newline?
86− skip_over_next(slice, &mut pos, |ch| ch.is_ascii_whitespace());
92+ skip_over_next(slice, &mut end, is_horiz_blank);
87 }
93 }
88
94
89− pos
95+ Some(Range::new(begin, end - 1))
90}
96}
91
97
92−pub fn move_prev_word_start(slice: RopeSlice, mut pos: usize, count: usize) -> usize {
98+pub fn move_prev_word_start(slice: RopeSlice, mut begin: usize, count: usize) -> Option<Range> {
99+ let mut with_end = false;
100+ let mut end = begin;
101+
93 for _ in 0..count {
102 for _ in 0..count {
94− if pos == 0 {
103+ if begin == 0 {
95− return pos;
104+ return None;
96 }
105 }
97
106
98− let ch = slice.char(pos);
107+ let ch = slice.char(begin);
99− let prev = slice.char(pos - 1);
108+ let prev = slice.char(begin - 1);
100
109
101 if categorize(ch) != categorize(prev) {
110 if categorize(ch) != categorize(prev) {
102− pos -= 1;
111+ begin -= 1;
103 }
112 }
104
113
105− // match (category c1, category c2) => {
114+ // return if not skip while?
106− // if c1 != c2 {
115+ skip_over_prev(slice, &mut begin, |ch| ch == '\n');
107− // }
108− // }
109
116
110− // TODO: skip while eol
117+ end = begin;
111
118
112− // TODO: don't include newline?
113− skip_over_prev(slice, &mut pos, |ch| ch.is_ascii_whitespace());
119+ with_end = skip_over_prev(slice, &mut end, is_horiz_blank);
114
120
115 // refetch
121 // refetch
116− let ch = slice.char(pos);
122+ let ch = slice.char(end);
117
123
118 if is_word(ch) {
124 if is_word(ch) {
119− skip_over_prev(slice, &mut pos, is_word);
125+ with_end = skip_over_prev(slice, &mut end, is_word);
120 } else if ch.is_ascii_punctuation() {
126 } else if ch.is_ascii_punctuation() {
121− skip_over_prev(slice, &mut pos, |ch| ch.is_ascii_punctuation());
127+ with_end = skip_over_prev(slice, &mut end, |ch| ch.is_ascii_punctuation());
122 }
128 }
123− pos = pos.saturating_add(1)
124 }
129 }
125
130
126− pos
131+ // we want to include begin
132+ Some(Range::new(begin + 1, if with_end { end } else { end + 1 }))
127}
133}
128
134
129−pub fn move_next_word_end(slice: RopeSlice, mut pos: usize, count: usize) -> usize {
135+pub fn move_next_word_end(slice: RopeSlice, mut begin: usize, count: usize) -> Option<Range> {
136+ let mut end = begin;
137+
130 for _ in 0..count {
138 for _ in 0..count {
131− if pos + 1 == slice.len_chars() {
139+ if begin + 1 == slice.len_chars() {
132− return pos;
140+ return None;
133 }
141 }
134
142
135− let ch = slice.char(pos);
143+ let ch = slice.char(begin);
136− let next = slice.char(pos + 1);
144+ let next = slice.char(begin + 1);
137
145
138 if categorize(ch) != categorize(next) {
146 if categorize(ch) != categorize(next) {
139− pos += 1;
147+ begin += 1;
140 }
148 }
141
149
142− // TODO: don't include newline?
150+ // return if not skip while?
143− skip_over_next(slice, &mut pos, |ch| ch.is_ascii_whitespace());
151+ skip_over_next(slice, &mut begin, |ch| ch == '\n');
152+
153+ end = begin;
154+
155+ skip_over_next(slice, &mut end, is_horiz_blank);
144
156
145 // refetch
157 // refetch
146− let ch = slice.char(pos);
158+ let ch = slice.char(end);
147
159
148 if is_word(ch) {
160 if is_word(ch) {
149− skip_over_next(slice, &mut pos, is_word);
161+ skip_over_next(slice, &mut end, is_word);
150 } else if ch.is_ascii_punctuation() {
162 } else if ch.is_ascii_punctuation() {
151− skip_over_next(slice, &mut pos, |ch| ch.is_ascii_punctuation());
163+ skip_over_next(slice, &mut end, |ch| ch.is_ascii_punctuation());
152 }
164 }
153− pos -= 1
154 }
165 }
155
166
156− pos
167+ Some(Range::new(begin, end - 1))
157}
168}
158
169
159// ---- util ------------
170// ---- util ------------
@@ -164,6 +175,10 @@ fn is_word(ch: char) -> bool {
164 ch.is_alphanumeric() || ch == '_'
175 ch.is_alphanumeric() || ch == '_'
165}
176}
166
177
178+fn is_horiz_blank(ch: char) -> bool {
179+ matches!(ch, ' ' | '\t')
180+}
181+
167#[derive(Debug, Eq, PartialEq)]
182#[derive(Debug, Eq, PartialEq)]
168enum Category {
183enum Category {
169 Whitespace,
184 Whitespace,
@@ -201,7 +216,8 @@ where
201}
216}
202
217
203#[inline]
218#[inline]
219+/// Returns true if the final pos matches the predicate.
204−pub fn skip_over_prev<F>(slice: RopeSlice, pos: &mut usize, fun: F)
220+pub fn skip_over_prev<F>(slice: RopeSlice, pos: &mut usize, fun: F) -> bool
205where
221where
206 F: Fn(char) -> bool,
222 F: Fn(char) -> bool,
207{
223{
@@ -214,6 +230,7 @@ where
214 }
230 }
215 *pos = pos.saturating_sub(1);
231 *pos = pos.saturating_sub(1);
216 }
232 }
233+ return fun(slice.char(*pos));
217}
234}
218
235
219#[cfg(test)]
236#[cfg(test)]
helix-term/src/commands.rs+11−11
@@ -218,8 +218,7 @@ pub fn move_next_word_start(cx: &mut Context) {
218 let text = doc.text().slice(..);
218 let text = doc.text().slice(..);
219
219
220 let selection = doc.selection(view.id).transform(|range| {
220 let selection = doc.selection(view.id).transform(|range| {
221− let pos = movement::move_next_word_start(text, range.head, count);
221+ movement::move_next_word_start(text, range.head, count).unwrap_or(range)
222− Range::new(pos, pos)
223 });
222 });
224
223
225 doc.set_selection(view.id, selection);
224 doc.set_selection(view.id, selection);
@@ -231,8 +230,7 @@ pub fn move_prev_word_start(cx: &mut Context) {
231 let text = doc.text().slice(..);
230 let text = doc.text().slice(..);
232
231
233 let selection = doc.selection(view.id).transform(|range| {
232 let selection = doc.selection(view.id).transform(|range| {
234− let pos = movement::move_prev_word_start(text, range.head, count);
233+ movement::move_prev_word_start(text, range.head, count).unwrap_or(range)
235− Range::new(pos, pos)
236 });
234 });
237
235
238 doc.set_selection(view.id, selection);
236 doc.set_selection(view.id, selection);
@@ -243,10 +241,9 @@ pub fn move_next_word_end(cx: &mut Context) {
243 let (view, doc) = cx.current();
241 let (view, doc) = cx.current();
244 let text = doc.text().slice(..);
242 let text = doc.text().slice(..);
245
243
246− let selection = doc.selection(view.id).transform(|range| {
244+ let selection = doc
245+ .selection(view.id)
247− let pos = movement::move_next_word_end(text, range.head, count);
246+ .transform(|range| movement::move_next_word_end(text, range.head, count).unwrap_or(range));
248− Range::new(pos, pos)
249− });
250
247
251 doc.set_selection(view.id, selection);
248 doc.set_selection(view.id, selection);
252}
249}
@@ -271,7 +268,8 @@ pub fn extend_next_word_start(cx: &mut Context) {
271 let text = doc.text().slice(..);
268 let text = doc.text().slice(..);
272
269
273 let selection = doc.selection(view.id).transform(|mut range| {
270 let selection = doc.selection(view.id).transform(|mut range| {
274− let pos = movement::move_next_word_start(text, range.head, count);
271+ let word = movement::move_next_word_start(text, range.head, count).unwrap_or(range);
272+ let pos = word.head;
275 Range::new(range.anchor, pos)
273 Range::new(range.anchor, pos)
276 });
274 });
277
275
@@ -284,7 +282,8 @@ pub fn extend_prev_word_start(cx: &mut Context) {
284 let text = doc.text().slice(..);
282 let text = doc.text().slice(..);
285
283
286 let selection = doc.selection(view.id).transform(|mut range| {
284 let selection = doc.selection(view.id).transform(|mut range| {
287− let pos = movement::move_prev_word_start(text, range.head, count);
285+ let word = movement::move_prev_word_start(text, range.head, count).unwrap_or(range);
286+ let pos = word.head;
288 Range::new(range.anchor, pos)
287 Range::new(range.anchor, pos)
289 });
288 });
290 doc.set_selection(view.id, selection);
289 doc.set_selection(view.id, selection);
@@ -296,7 +295,8 @@ pub fn extend_next_word_end(cx: &mut Context) {
296 let text = doc.text().slice(..);
295 let text = doc.text().slice(..);
297
296
298 let selection = doc.selection(view.id).transform(|mut range| {
297 let selection = doc.selection(view.id).transform(|mut range| {
299− let pos = movement::move_next_word_end(text, range.head, count);
298+ let word = movement::move_next_word_end(text, range.head, count).unwrap_or(range);
299+ let pos = word.head;
300 Range::new(range.anchor, pos)
300 Range::new(range.anchor, pos)
301 });
301 });
302
302