on master help sign in
Merge pull request #376 from cessen/great_line_ending_and_cursor_range_cleanup

The Great Line Ending & Cursor Range Cleanup

commit05d20e196f81c8b71c2aecaf46f5d443d6b6b582 authorBlaž Hrastnik <blaz@mxxn.io> dateThu 29 Jul 2021, 18:43 (5 years ago) parent8a2fa69e4d41d0
18 files changed+1743−1055
helix-core/src/comment.rs+4−2 helix-core/src/graphemes.rs+21−2 helix-core/src/line_ending.rs+7 helix-core/src/match_brackets.rs+4−3 helix-core/src/movement.rs+391−348 helix-core/src/object.rs+1−1 helix-core/src/position.rs+115−29 helix-core/src/search.rs+10−28 helix-core/src/selection.rs+529−133 helix-core/src/surround.rs+44−31 helix-core/src/syntax.rs+14−4 helix-core/src/textobject.rs+128−133 helix-term/src/commands.rs+356−265 helix-term/src/ui/completion.rs+16−4 helix-term/src/ui/editor.rs+69−34 helix-view/src/document.rs+16−29 helix-view/src/editor.rs+9−4 helix-view/src/view.rs+9−5
helix-core/src/comment.rs+4−2
@@ -64,8 +64,10 @@ pub fn toggle_line_comments(doc: &Rope, selection: &Selection, token: Option<&st
6464
6565 let mut min_next_line = 0;
6666 for selection in selection {
67− let start = text.char_to_line(selection.from()).max(min_next_line);
68− let end = text.char_to_line(selection.to()) + 1;
67+ let (start, end) = selection.line_range(text);
68+ let start = start.max(min_next_line).min(text.len_lines());
69+ let end = (end + 1).min(text.len_lines());
70+
6971 lines.extend(start..end);
7072 min_next_line = end + 1;
7173 }
helix-core/src/graphemes.rs+21−2
@@ -71,6 +71,8 @@ pub fn nth_prev_grapheme_boundary(slice: RopeSlice, char_idx: usize, n: usize) -
7171}
7272
7373/// Finds the previous grapheme boundary before the given char position.
74+#[must_use]
75+#[inline(always)]
7476pub fn prev_grapheme_boundary(slice: RopeSlice, char_idx: usize) -> usize {
7577 nth_prev_grapheme_boundary(slice, char_idx, 1)
7678}
@@ -117,21 +119,38 @@ pub fn nth_next_grapheme_boundary(slice: RopeSlice, char_idx: usize, n: usize) -
117119}
118120
119121/// Finds the next grapheme boundary after the given char position.
122+#[must_use]
123+#[inline(always)]
120124pub fn next_grapheme_boundary(slice: RopeSlice, char_idx: usize) -> usize {
121125 nth_next_grapheme_boundary(slice, char_idx, 1)
122126}
123127
124128/// Returns the passed char index if it's already a grapheme boundary,
125129/// or the next grapheme boundary char index if not.
126−pub fn ensure_grapheme_boundary(slice: RopeSlice, char_idx: usize) -> usize {
130+#[must_use]
131+#[inline]
132+pub fn ensure_grapheme_boundary_next(slice: RopeSlice, char_idx: usize) -> usize {
127133 if char_idx == 0 {
128− 0
134+ char_idx
129135 } else {
130136 next_grapheme_boundary(slice, char_idx - 1)
131137 }
132138}
133139
140+/// Returns the passed char index if it's already a grapheme boundary,
141+/// or the prev grapheme boundary char index if not.
142+#[must_use]
143+#[inline]
144+pub fn ensure_grapheme_boundary_prev(slice: RopeSlice, char_idx: usize) -> usize {
145+ if char_idx == slice.len_chars() {
146+ char_idx
147+ } else {
148+ prev_grapheme_boundary(slice, char_idx + 1)
149+ }
150+}
151+
134152/// Returns whether the given char position is a grapheme boundary.
153+#[must_use]
135154pub fn is_grapheme_boundary(slice: RopeSlice, char_idx: usize) -> bool {
136155 // Bounds check
137156 debug_assert!(char_idx <= slice.len_chars());
helix-core/src/line_ending.rs+7
@@ -159,6 +159,13 @@ pub fn line_end_char_index(slice: &RopeSlice, line: usize) -> usize {
159159 .unwrap_or(0)
160160}
161161
162+/// Fetches line `line_idx` from the passed rope slice, sans any line ending.
163+pub fn line_without_line_ending<'a>(slice: &'a RopeSlice, line_idx: usize) -> RopeSlice<'a> {
164+ let start = slice.line_to_char(line_idx);
165+ let end = line_end_char_index(slice, line_idx);
166+ slice.slice(start..end)
167+}
168+
162169/// Returns the char index of the end of the given RopeSlice, not including
163170/// any final line ending.
164171pub fn rope_end_without_line_ending(slice: &RopeSlice) -> usize {
helix-core/src/match_brackets.rs+4−3
@@ -24,12 +24,13 @@ pub fn find(syntax: &Syntax, doc: &Rope, pos: usize) -> Option<usize> {
2424 return None;
2525 }
2626
27− let start_byte = node.start_byte();
2827 let len = doc.len_bytes();
29− if start_byte >= len {
28+ let start_byte = node.start_byte();
29+ let end_byte = node.end_byte() - 1; // it's end exclusive
30+ if start_byte >= len || end_byte >= len {
3031 return None;
3132 }
32− let end_byte = node.end_byte() - 1; // it's end exclusive
33+
3334 let start_char = doc.byte_to_char(start_byte);
3435 let end_char = doc.byte_to_char(end_byte);
3536
helix-core/src/movement.rs+391−348
@@ -1,12 +1,14 @@
1−use std::iter::{self, from_fn};
1+use std::iter;
22
33use ropey::iter::Chars;
44
55use crate::{
66 chars::{categorize_char, char_is_line_ending, CharCategory},
77 coords_at_pos,
8− graphemes::{nth_next_grapheme_boundary, nth_prev_grapheme_boundary},
9− line_ending::{get_line_ending, line_end_char_index},
8+ graphemes::{
9+ next_grapheme_boundary, nth_next_grapheme_boundary, nth_prev_grapheme_boundary,
10+ prev_grapheme_boundary,
11+ },
1012 pos_at_coords, Position, Range, RopeSlice,
1113};
1214
@@ -29,25 +31,16 @@ pub fn move_horizontally(
2931 count: usize,
3032 behaviour: Movement,
3133) -> Range {
32− let pos = range.head;
33− let line = slice.char_to_line(pos);
34− // TODO: we can optimize clamping by passing in RopeSlice limited to current line. that way
35− // we stop calculating past start/end of line.
36− let pos = match dir {
37− Direction::Backward => {
38− let start = slice.line_to_char(line);
39− nth_prev_grapheme_boundary(slice, pos, count).max(start)
40− }
41− Direction::Forward => {
42− let end_char_idx = line_end_char_index(&slice, line);
43− nth_next_grapheme_boundary(slice, pos, count).min(end_char_idx)
44− }
45− };
46− let anchor = match behaviour {
47− Movement::Extend => range.anchor,
48− Movement::Move => pos,
34+ let pos = range.cursor(slice);
35+
36+ // Compute the new position.
37+ let new_pos = match dir {
38+ Direction::Forward => nth_next_grapheme_boundary(slice, pos, count),
39+ Direction::Backward => nth_prev_grapheme_boundary(slice, pos, count),
4940 };
50− Range::new(anchor, pos)
41+
42+ // Compute the final new range.
43+ range.put_cursor(slice, new_pos, behaviour == Movement::Extend)
5144}
5245
5346pub fn move_vertically(
@@ -57,36 +50,28 @@ pub fn move_vertically(
5750 count: usize,
5851 behaviour: Movement,
5952) -> Range {
60− let Position { row, col } = coords_at_pos(slice, range.head);
53+ let pos = range.cursor(slice);
6154
55+ // Compute the current position's 2d coordinates.
56+ let Position { row, col } = coords_at_pos(slice, pos);
6257 let horiz = range.horiz.unwrap_or(col as u32);
6358
64− let new_line = match dir {
59+ // Compute the new position.
60+ let new_row = match dir {
61+ Direction::Forward => (row + count).min(slice.len_lines().saturating_sub(1)),
6562 Direction::Backward => row.saturating_sub(count),
66− Direction::Forward => std::cmp::min(
67− row.saturating_add(count),
68− slice.len_lines().saturating_sub(2),
69− ),
7063 };
64+ let new_col = col.max(horiz as usize);
65+ let new_pos = pos_at_coords(slice, Position::new(new_row, new_col), true);
7166
72− // Length of the line sans line-ending.
73− let new_line_len = {
74− let line = slice.line(new_line);
75− line.len_chars() - get_line_ending(&line).map(|le| le.len_chars()).unwrap_or(0)
76− };
77−
78− let new_col = std::cmp::min(horiz as usize, new_line_len);
79−
80− let pos = pos_at_coords(slice, Position::new(new_line, new_col));
81−
82− let anchor = match behaviour {
83− Movement::Extend => range.anchor,
84− Movement::Move => pos,
85− };
67+ // Special-case to avoid moving to the end of the last non-empty line.
68+ if behaviour == Movement::Extend && slice.line(new_row).len_chars() == 0 {
69+ return range;
70+ }
8671
87− let mut range = Range::new(anchor, pos);
88− range.horiz = Some(horiz);
89− range
72+ let mut new_range = range.put_cursor(slice, new_pos, behaviour == Movement::Extend);
73+ new_range.horiz = Some(horiz);
74+ new_range
9075}
9176
9277pub fn move_next_word_start(slice: RopeSlice, range: Range, count: usize) -> Range {
@@ -118,8 +103,41 @@ pub fn move_prev_word_end(slice: RopeSlice, range: Range, count: usize) -> Range
118103}
119104
120105fn word_move(slice: RopeSlice, range: Range, count: usize, target: WordMotionTarget) -> Range {
121− (0..count).fold(range, |range, _| {
122− slice.chars_at(range.head).range_to_target(target, range)
106+ let is_prev = matches!(
107+ target,
108+ WordMotionTarget::PrevWordStart
109+ | WordMotionTarget::PrevLongWordStart
110+ | WordMotionTarget::PrevWordEnd
111+ );
112+
113+ // Special-case early-out.
114+ if (is_prev && range.head == 0) || (!is_prev && range.head == slice.len_chars()) {
115+ return range;
116+ }
117+
118+ // Prepare the range appropriately based on the target movement
119+ // direction. This is addressing two things at once:
120+ //
121+ // 1. Block-cursor semantics.
122+ // 2. The anchor position being irrelevant to the output result.
123+ #[allow(clippy::collapsible_else_if)] // Makes the structure clearer in this case.
124+ let start_range = if is_prev {
125+ if range.anchor < range.head {
126+ Range::new(range.head, prev_grapheme_boundary(slice, range.head))
127+ } else {
128+ Range::new(next_grapheme_boundary(slice, range.head), range.head)
129+ }
130+ } else {
131+ if range.anchor < range.head {
132+ Range::new(prev_grapheme_boundary(slice, range.head), range.head)
133+ } else {
134+ Range::new(range.head, next_grapheme_boundary(slice, range.head))
135+ }
136+ };
137+
138+ // Do the main work.
139+ (0..count).fold(start_range, |r, _| {
140+ slice.chars_at(r.head).range_to_target(target, r)
123141 })
124142}
125143
@@ -176,79 +194,75 @@ pub trait CharHelpers {
176194 fn range_to_target(&mut self, target: WordMotionTarget, origin: Range) -> Range;
177195}
178196
179−enum WordMotionPhase {
180− Start,
181− SkipNewlines,
182− ReachTarget,
183−}
184−
185197impl CharHelpers for Chars<'_> {
198+ /// Note: this only changes the anchor of the range if the head is effectively
199+ /// starting on a boundary (either directly or after skipping newline characters).
200+ /// Any other changes to the anchor should be handled by the calling code.
186201 fn range_to_target(&mut self, target: WordMotionTarget, origin: Range) -> Range {
187− // Characters are iterated forward or backwards depending on the motion direction.
188− let characters: Box<dyn Iterator<Item = char>> = match target {
202+ let is_prev = matches!(
203+ target,
189204 WordMotionTarget::PrevWordStart
190− | WordMotionTarget::PrevLongWordStart
191− | WordMotionTarget::PrevWordEnd => {
205+ | WordMotionTarget::PrevLongWordStart
206+ | WordMotionTarget::PrevWordEnd
207+ );
208+
209+ // Reverse the iterator if needed for the motion direction.
210+ if is_prev {
211+ self.reverse();
212+ }
213+
214+ // Function to advance index in the appropriate motion direction.
215+ let advance: &dyn Fn(&mut usize) = if is_prev {
216+ &|idx| *idx = idx.saturating_sub(1)
217+ } else {
218+ &|idx| *idx += 1
219+ };
220+
221+ // Initialize state variables.
222+ let mut anchor = origin.anchor;
223+ let mut head = origin.head;
224+ let mut prev_ch = {
225+ let ch = self.prev();
226+ if ch.is_some() {
192227 self.next();
193− Box::new(from_fn(|| self.prev()))
194228 }
195− _ => Box::new(self),
229+ ch
196230 };
197231
198− // Index advancement also depends on the direction.
199− let advance: &dyn Fn(&mut usize) = match target {
200− WordMotionTarget::PrevWordStart
201− | WordMotionTarget::PrevLongWordStart
202− | WordMotionTarget::PrevWordEnd => &|u| *u = u.saturating_sub(1),
203− _ => &|u| *u += 1,
204− };
232+ // Skip any initial newline characters.
233+ while let Some(ch) = self.next() {
234+ if char_is_line_ending(ch) {
235+ prev_ch = Some(ch);
236+ advance(&mut head);
237+ } else {
238+ self.prev();
239+ break;
240+ }
241+ }
242+ if prev_ch.map(char_is_line_ending).unwrap_or(false) {
243+ anchor = head;
244+ }
205245
206− let mut characters = characters.peekable();
207− let mut phase = WordMotionPhase::Start;
208− let mut head = origin.head;
209− let mut anchor: Option<usize> = None;
210− let is_boundary =
211− |a: char, b: Option<char>| categorize_char(a) != categorize_char(b.unwrap_or(a));
212− while let Some(peek) = characters.peek().copied() {
213− phase = match phase {
214− WordMotionPhase::Start => {
215− characters.next();
216− if characters.peek().is_none() {
217− break; // We're at the end, so there's nothing to do.
218− }
219− // Anchor may remain here if the head wasn't at a boundary
220− if !is_boundary(peek, characters.peek().copied()) && !char_is_line_ending(peek)
221− {
222− anchor = Some(head);
223− }
224− // First character is always skipped by the head
225− advance(&mut head);
226− WordMotionPhase::SkipNewlines
227− }
228− WordMotionPhase::SkipNewlines => {
229− if char_is_line_ending(peek) {
230− characters.next();
231− if characters.peek().is_some() {
232− advance(&mut head);
233− }
234− WordMotionPhase::SkipNewlines
235− } else {
236− WordMotionPhase::ReachTarget
237− }
238− }
239− WordMotionPhase::ReachTarget => {
240− characters.next();
241− anchor = anchor.or(Some(head));
242− if reached_target(target, peek, characters.peek()) {
243− break;
244− } else {
245− advance(&mut head);
246− }
247− WordMotionPhase::ReachTarget
246+ // Find our target position(s).
247+ let head_start = head;
248+ while let Some(next_ch) = self.next() {
249+ if prev_ch.is_none() || reached_target(target, prev_ch.unwrap(), next_ch) {
250+ if head == head_start {
251+ anchor = head;
252+ } else {
253+ break;
248254 }
249255 }
256+ prev_ch = Some(next_ch);
257+ advance(&mut head);
258+ }
259+
260+ // Un-reverse the iterator if needed.
261+ if is_prev {
262+ self.reverse();
250263 }
251− Range::new(anchor.unwrap_or(origin.anchor), head)
264+
265+ Range::new(anchor, head)
252266 }
253267}
254268
@@ -265,28 +279,23 @@ fn is_long_word_boundary(a: char, b: char) -> bool {
265279 }
266280}
267281
268−fn reached_target(target: WordMotionTarget, peek: char, next_peek: Option<&char>) -> bool {
269− let next_peek = match next_peek {
270− Some(next_peek) => next_peek,
271− None => return true,
272− };
273−
282+fn reached_target(target: WordMotionTarget, prev_ch: char, next_ch: char) -> bool {
274283 match target {
275284 WordMotionTarget::NextWordStart | WordMotionTarget::PrevWordEnd => {
276− is_word_boundary(peek, *next_peek)
277− && (char_is_line_ending(*next_peek) || !next_peek.is_whitespace())
285+ is_word_boundary(prev_ch, next_ch)
286+ && (char_is_line_ending(next_ch) || !next_ch.is_whitespace())
278287 }
279288 WordMotionTarget::NextWordEnd | WordMotionTarget::PrevWordStart => {
280− is_word_boundary(peek, *next_peek)
281− && (!peek.is_whitespace() || char_is_line_ending(*next_peek))
289+ is_word_boundary(prev_ch, next_ch)
290+ && (!prev_ch.is_whitespace() || char_is_line_ending(next_ch))
282291 }
283292 WordMotionTarget::NextLongWordStart => {
284− is_long_word_boundary(peek, *next_peek)
285− && (char_is_line_ending(*next_peek) || !next_peek.is_whitespace())
293+ is_long_word_boundary(prev_ch, next_ch)
294+ && (char_is_line_ending(next_ch) || !next_ch.is_whitespace())
286295 }
287296 WordMotionTarget::NextLongWordEnd | WordMotionTarget::PrevLongWordStart => {
288− is_long_word_boundary(peek, *next_peek)
289− && (!peek.is_whitespace() || char_is_line_ending(*next_peek))
297+ is_long_word_boundary(prev_ch, next_ch)
298+ && (!prev_ch.is_whitespace() || char_is_line_ending(next_ch))
290299 }
291300 }
292301}
@@ -317,7 +326,7 @@ mod test {
317326 fn test_vertical_move() {
318327 let text = Rope::from("abcd\nefg\nwrs");
319328 let slice = text.slice(..);
320− let pos = pos_at_coords(slice, (0, 4).into());
329+ let pos = pos_at_coords(slice, (0, 4).into(), true);
321330
322331 let range = Range::new(pos, pos);
323332 assert_eq!(
@@ -330,10 +339,10 @@ mod test {
330339 }
331340
332341 #[test]
333− fn horizontal_moves_through_single_line_in_single_line_text() {
342+ fn horizontal_moves_through_single_line_text() {
334343 let text = Rope::from(SINGLE_LINE_SAMPLE);
335344 let slice = text.slice(..);
336− let position = pos_at_coords(slice, (0, 0).into());
345+ let position = pos_at_coords(slice, (0, 0).into(), true);
337346
338347 let mut range = Range::point(position);
339348
@@ -353,23 +362,23 @@ mod test {
353362 }
354363
355364 #[test]
356− fn horizontal_moves_through_single_line_in_multiline_text() {
365+ fn horizontal_moves_through_multiline_text() {
357366 let text = Rope::from(MULTILINE_SAMPLE);
358367 let slice = text.slice(..);
359− let position = pos_at_coords(slice, (0, 0).into());
368+ let position = pos_at_coords(slice, (0, 0).into(), true);
360369
361370 let mut range = Range::point(position);
362371
363372 let moves_and_expected_coordinates = IntoIter::new([
364− ((Direction::Forward, 1usize), (0, 1)), // M|ultiline\n
365− ((Direction::Forward, 2usize), (0, 3)), // Mul|tiline\n
366− ((Direction::Backward, 6usize), (0, 0)), // |Multiline\n
367− ((Direction::Backward, 999usize), (0, 0)), // |Multiline\n
368− ((Direction::Forward, 3usize), (0, 3)), // Mul|tiline\n
369− ((Direction::Forward, 0usize), (0, 3)), // Mul|tiline\n
370− ((Direction::Backward, 0usize), (0, 3)), // Mul|tiline\n
371− ((Direction::Forward, 999usize), (0, 9)), // Multiline|\n
372− ((Direction::Forward, 999usize), (0, 9)), // Multiline|\n
373+ ((Direction::Forward, 11usize), (1, 1)), // Multiline\nt|ext sample\n...
374+ ((Direction::Backward, 1usize), (1, 0)), // Multiline\n|text sample\n...
375+ ((Direction::Backward, 5usize), (0, 5)), // Multi|line\ntext sample\n...
376+ ((Direction::Backward, 999usize), (0, 0)), // |Multiline\ntext sample\n...
377+ ((Direction::Forward, 3usize), (0, 3)), // Mul|tiline\ntext sample\n...
378+ ((Direction::Forward, 0usize), (0, 3)), // Mul|tiline\ntext sample\n...
379+ ((Direction::Backward, 0usize), (0, 3)), // Mul|tiline\ntext sample\n...
380+ ((Direction::Forward, 999usize), (5, 0)), // ...and whitespaced\n|
381+ ((Direction::Forward, 999usize), (5, 0)), // ...and whitespaced\n|
373382 ]);
374383
375384 for ((direction, amount), coordinates) in moves_and_expected_coordinates {
@@ -383,7 +392,7 @@ mod test {
383392 fn selection_extending_moves_in_single_line_text() {
384393 let text = Rope::from(SINGLE_LINE_SAMPLE);
385394 let slice = text.slice(..);
386− let position = pos_at_coords(slice, (0, 0).into());
395+ let position = pos_at_coords(slice, (0, 0).into(), true);
387396
388397 let mut range = Range::point(position);
389398 let original_anchor = range.anchor;
@@ -403,18 +412,19 @@ mod test {
403412 #[test]
404413 fn vertical_moves_in_single_column() {
405414 let text = Rope::from(MULTILINE_SAMPLE);
406− let slice = dbg!(&text).slice(..);
407− let position = pos_at_coords(slice, (0, 0).into());
415+ let slice = text.slice(..);
416+ let position = pos_at_coords(slice, (0, 0).into(), true);
408417 let mut range = Range::point(position);
409418 let moves_and_expected_coordinates = IntoIter::new([
410419 ((Direction::Forward, 1usize), (1, 0)),
411420 ((Direction::Forward, 2usize), (3, 0)),
421+ ((Direction::Forward, 1usize), (4, 0)),
412422 ((Direction::Backward, 999usize), (0, 0)),
413− ((Direction::Forward, 3usize), (3, 0)),
414− ((Direction::Forward, 0usize), (3, 0)),
415− ((Direction::Backward, 0usize), (3, 0)),
416− ((Direction::Forward, 5), (4, 0)),
417− ((Direction::Forward, 999usize), (4, 0)),
423+ ((Direction::Forward, 4usize), (4, 0)),
424+ ((Direction::Forward, 0usize), (4, 0)),
425+ ((Direction::Backward, 0usize), (4, 0)),
426+ ((Direction::Forward, 5), (5, 0)),
427+ ((Direction::Forward, 999usize), (5, 0)),
418428 ]);
419429
420430 for ((direction, amount), coordinates) in moves_and_expected_coordinates {
@@ -428,7 +438,7 @@ mod test {
428438 fn vertical_moves_jumping_column() {
429439 let text = Rope::from(MULTILINE_SAMPLE);
430440 let slice = text.slice(..);
431− let position = pos_at_coords(slice, (0, 0).into());
441+ let position = pos_at_coords(slice, (0, 0).into(), true);
432442 let mut range = Range::point(position);
433443
434444 enum Axis {
@@ -446,7 +456,8 @@ mod test {
446456 ((Axis::V, Direction::Forward, 1usize), (3, 8)),
447457 // Behaviour is preserved even through long jumps
448458 ((Axis::V, Direction::Backward, 999usize), (0, 8)),
449− ((Axis::V, Direction::Forward, 999usize), (4, 8)),
459+ ((Axis::V, Direction::Forward, 4usize), (4, 8)),
460+ ((Axis::V, Direction::Forward, 999usize), (5, 0)),
450461 ]);
451462
452463 for ((axis, direction, amount), coordinates) in moves_and_expected_coordinates {
@@ -460,10 +471,10 @@ mod test {
460471 }
461472
462473 #[test]
463− fn multibyte_character_column_jumps() {
474+ fn multibyte_character_wide_column_jumps() {
464475 let text = Rope::from(MULTIBYTE_CHARACTER_SAMPLE);
465476 let slice = text.slice(..);
466− let position = pos_at_coords(slice, (0, 0).into());
477+ let position = pos_at_coords(slice, (0, 0).into(), true);
467478 let mut range = Range::point(position);
468479
469480 // FIXME: The behaviour captured in this test diverges from both Kakoune and Vim. These
@@ -474,10 +485,14 @@ mod test {
474485 V,
475486 }
476487 let moves_and_expected_coordinates = IntoIter::new([
477− // Places cursor at the fourth kana
488+ // Places cursor at the fourth kana.
478489 ((Axis::H, Direction::Forward, 4), (0, 4)),
479− // Descent places cursor at the fourth character.
490+ // Descent places cursor at the 4th character.
480491 ((Axis::V, Direction::Forward, 1usize), (1, 4)),
492+ // Moving back 1 character.
493+ ((Axis::H, Direction::Backward, 1usize), (1, 3)),
494+ // Jumping back up 1 line.
495+ ((Axis::V, Direction::Backward, 1usize), (0, 3)),
481496 ]);
482497
483498 for ((axis, direction, amount), coordinates) in moves_and_expected_coordinates {
@@ -512,42 +527,42 @@ mod test {
512527 fn test_behaviour_when_moving_to_start_of_next_words() {
513528 let tests = array::IntoIter::new([
514529 ("Basic forward motion stops at the first space",
515− vec![(1, Range::new(0, 0), Range::new(0, 5))]),
530+ vec![(1, Range::new(0, 0), Range::new(0, 6))]),
516531 (" Starting from a boundary advances the anchor",
517− vec![(1, Range::new(0, 0), Range::new(1, 9))]),
532+ vec![(1, Range::new(0, 0), Range::new(1, 10))]),
518533 ("Long whitespace gap is bridged by the head",
519− vec![(1, Range::new(0, 0), Range::new(0, 10))]),
534+ vec![(1, Range::new(0, 0), Range::new(0, 11))]),
520535 ("Previous anchor is irrelevant for forward motions",
521− vec![(1, Range::new(12, 0), Range::new(0, 8))]),
536+ vec![(1, Range::new(12, 0), Range::new(0, 9))]),
522537 (" Starting from whitespace moves to last space in sequence",
523− vec![(1, Range::new(0, 0), Range::new(0, 3))]),
538+ vec![(1, Range::new(0, 0), Range::new(0, 4))]),
524539 ("Starting from mid-word leaves anchor at start position and moves head",
525− vec![(1, Range::new(3, 3), Range::new(3, 8))]),
540+ vec![(1, Range::new(3, 3), Range::new(3, 9))]),
526541 ("Identifiers_with_underscores are considered a single word",
527− vec![(1, Range::new(0, 0), Range::new(0, 28))]),
542+ vec![(1, Range::new(0, 0), Range::new(0, 29))]),
528543 ("Jumping\n into starting whitespace selects the spaces before 'into'",
529− vec![(1, Range::new(0, 6), Range::new(8, 11))]),
544+ vec![(1, Range::new(0, 7), Range::new(8, 12))]),
530545 ("alphanumeric.!,and.?=punctuation are considered 'words' for the purposes of word motion",
531546 vec![
532− (1, Range::new(0, 0), Range::new(0, 11)),
533− (1, Range::new(0, 11), Range::new(12, 14)),
534− (1, Range::new(12, 14), Range::new(15, 17))
547+ (1, Range::new(0, 0), Range::new(0, 12)),
548+ (1, Range::new(0, 12), Range::new(12, 15)),
549+ (1, Range::new(12, 15), Range::new(15, 18))
535550 ]),
536551 ("... ... punctuation and spaces behave as expected",
537552 vec![
538− (1, Range::new(0, 0), Range::new(0, 5)),
539− (1, Range::new(0, 5), Range::new(6, 9)),
553+ (1, Range::new(0, 0), Range::new(0, 6)),
554+ (1, Range::new(0, 6), Range::new(6, 10)),
540555 ]),
541556 (".._.._ punctuation is not joined by underscores into a single block",
542− vec![(1, Range::new(0, 0), Range::new(0, 1))]),
557+ vec![(1, Range::new(0, 0), Range::new(0, 2))]),
543558 ("Newlines\n\nare bridged seamlessly.",
544559 vec![
545− (1, Range::new(0, 0), Range::new(0, 7)),
546− (1, Range::new(0, 7), Range::new(10, 13)),
560+ (1, Range::new(0, 0), Range::new(0, 8)),
561+ (1, Range::new(0, 8), Range::new(10, 14)),
547562 ]),
548563 ("Jumping\n\n\n\n\n\n from newlines to whitespace selects whitespace.",
549564 vec![
550− (1, Range::new(0, 8), Range::new(13, 15)),
565+ (1, Range::new(0, 9), Range::new(13, 16)),
551566 ]),
552567 ("A failed motion does not modify the range",
553568 vec![
@@ -555,17 +570,17 @@ mod test {
555570 ]),
556571 ("oh oh oh two character words!",
557572 vec![
558− (1, Range::new(0, 0), Range::new(0, 2)),
559− (1, Range::new(0, 2), Range::new(3, 5)),
560− (1, Range::new(0, 1), Range::new(2, 2)),
573+ (1, Range::new(0, 0), Range::new(0, 3)),
574+ (1, Range::new(0, 3), Range::new(3, 6)),
575+ (1, Range::new(0, 2), Range::new(1, 3)),
561576 ]),
562577 ("Multiple motions at once resolve correctly",
563578 vec![
564− (3, Range::new(0, 0), Range::new(17, 19)),
579+ (3, Range::new(0, 0), Range::new(17, 20)),
565580 ]),
566581 ("Excessive motions are performed partially",
567582 vec![
568− (999, Range::new(0, 0), Range::new(32, 40)),
583+ (999, Range::new(0, 0), Range::new(32, 41)),
569584 ]),
570585 ("", // Edge case of moving forward in empty string
571586 vec![
@@ -573,16 +588,16 @@ mod test {
573588 ]),
574589 ("\n\n\n\n\n", // Edge case of moving forward in all newlines
575590 vec![
576− (1, Range::new(0, 0), Range::new(0, 4)),
591+ (1, Range::new(0, 0), Range::new(5, 5)),
577592 ]),
578593 ("\n \n \n Jumping through alternated space blocks and newlines selects the space blocks",
579594 vec![
580− (1, Range::new(0, 0), Range::new(1, 3)),
581− (1, Range::new(1, 3), Range::new(5, 7)),
595+ (1, Range::new(0, 0), Range::new(1, 4)),
596+ (1, Range::new(1, 4), Range::new(5, 8)),
582597 ]),
583598 ("ヒーリクス multibyte characters behave as normal characters",
584599 vec![
585− (1, Range::new(0, 0), Range::new(0, 5)),
600+ (1, Range::new(0, 0), Range::new(0, 6)),
586601 ]),
587602 ]);
588603
@@ -598,40 +613,40 @@ mod test {
598613 fn test_behaviour_when_moving_to_start_of_next_long_words() {
599614 let tests = array::IntoIter::new([
600615 ("Basic forward motion stops at the first space",
601− vec![(1, Range::new(0, 0), Range::new(0, 5))]),
616+ vec![(1, Range::new(0, 0), Range::new(0, 6))]),
602617 (" Starting from a boundary advances the anchor",
603− vec![(1, Range::new(0, 0), Range::new(1, 9))]),
618+ vec![(1, Range::new(0, 0), Range::new(1, 10))]),
604619 ("Long whitespace gap is bridged by the head",
605− vec![(1, Range::new(0, 0), Range::new(0, 10))]),
620+ vec![(1, Range::new(0, 0), Range::new(0, 11))]),
606621 ("Previous anchor is irrelevant for forward motions",
607− vec![(1, Range::new(12, 0), Range::new(0, 8))]),
622+ vec![(1, Range::new(12, 0), Range::new(0, 9))]),
608623 (" Starting from whitespace moves to last space in sequence",
609− vec![(1, Range::new(0, 0), Range::new(0, 3))]),
624+ vec![(1, Range::new(0, 0), Range::new(0, 4))]),
610625 ("Starting from mid-word leaves anchor at start position and moves head",
611− vec![(1, Range::new(3, 3), Range::new(3, 8))]),
626+ vec![(1, Range::new(3, 3), Range::new(3, 9))]),
612627 ("Identifiers_with_underscores are considered a single word",
613− vec![(1, Range::new(0, 0), Range::new(0, 28))]),
628+ vec![(1, Range::new(0, 0), Range::new(0, 29))]),
614629 ("Jumping\n into starting whitespace selects the spaces before 'into'",
615− vec![(1, Range::new(0, 6), Range::new(8, 11))]),
630+ vec![(1, Range::new(0, 7), Range::new(8, 12))]),
616631 ("alphanumeric.!,and.?=punctuation are not treated any differently than alphanumerics",
617632 vec![
618− (1, Range::new(0, 0), Range::new(0, 32)),
633+ (1, Range::new(0, 0), Range::new(0, 33)),
619634 ]),
620635 ("... ... punctuation and spaces behave as expected",
621636 vec![
622− (1, Range::new(0, 0), Range::new(0, 5)),
623− (1, Range::new(0, 5), Range::new(6, 9)),
637+ (1, Range::new(0, 0), Range::new(0, 6)),
638+ (1, Range::new(0, 6), Range::new(6, 10)),
624639 ]),
625640 (".._.._ punctuation is joined by underscores into a single word, as it behaves like alphanumerics",
626− vec![(1, Range::new(0, 0), Range::new(0, 6))]),
641+ vec![(1, Range::new(0, 0), Range::new(0, 7))]),
627642 ("Newlines\n\nare bridged seamlessly.",
628643 vec![
629− (1, Range::new(0, 0), Range::new(0, 7)),
630− (1, Range::new(0, 7), Range::new(10, 13)),
644+ (1, Range::new(0, 0), Range::new(0, 8)),
645+ (1, Range::new(0, 8), Range::new(10, 14)),
631646 ]),
632647 ("Jumping\n\n\n\n\n\n from newlines to whitespace selects whitespace.",
633648 vec![
634− (1, Range::new(0, 8), Range::new(13, 15)),
649+ (1, Range::new(0, 9), Range::new(13, 16)),
635650 ]),
636651 ("A failed motion does not modify the range",
637652 vec![
@@ -639,17 +654,17 @@ mod test {
639654 ]),
640655 ("oh oh oh two character words!",
641656 vec![
642− (1, Range::new(0, 0), Range::new(0, 2)),
643− (1, Range::new(0, 2), Range::new(3, 5)),
644− (1, Range::new(0, 1), Range::new(2, 2)),
657+ (1, Range::new(0, 0), Range::new(0, 3)),
658+ (1, Range::new(0, 3), Range::new(3, 6)),
659+ (1, Range::new(0, 1), Range::new(0, 3)),
645660 ]),
646661 ("Multiple motions at once resolve correctly",
647662 vec![
648− (3, Range::new(0, 0), Range::new(17, 19)),
663+ (3, Range::new(0, 0), Range::new(17, 20)),
649664 ]),
650665 ("Excessive motions are performed partially",
651666 vec![
652− (999, Range::new(0, 0), Range::new(32, 40)),
667+ (999, Range::new(0, 0), Range::new(32, 41)),
653668 ]),
654669 ("", // Edge case of moving forward in empty string
655670 vec![
@@ -657,16 +672,16 @@ mod test {
657672 ]),
658673 ("\n\n\n\n\n", // Edge case of moving forward in all newlines
659674 vec![
660− (1, Range::new(0, 0), Range::new(0, 4)),
675+ (1, Range::new(0, 0), Range::new(5, 5)),
661676 ]),
662677 ("\n \n \n Jumping through alternated space blocks and newlines selects the space blocks",
663678 vec![
664− (1, Range::new(0, 0), Range::new(1, 3)),
665− (1, Range::new(1, 3), Range::new(5, 7)),
679+ (1, Range::new(0, 0), Range::new(1, 4)),
680+ (1, Range::new(1, 4), Range::new(5, 8)),
666681 ]),
667682 ("ヒー..リクス multibyte characters behave as normal characters, including their interaction with punctuation",
668683 vec![
669− (1, Range::new(0, 0), Range::new(0, 7)),
684+ (1, Range::new(0, 0), Range::new(0, 8)),
670685 ]),
671686 ]);
672687
@@ -682,44 +697,47 @@ mod test {
682697 fn test_behaviour_when_moving_to_start_of_previous_words() {
683698 let tests = array::IntoIter::new([
684699 ("Basic backward motion from the middle of a word",
685− vec![(1, Range::new(3, 3), Range::new(3, 0))]),
686− ("Starting from after boundary retreats the anchor",
687− vec![(1, Range::new(0, 8), Range::new(7, 0))]),
700+ vec![(1, Range::new(3, 3), Range::new(4, 0))]),
701+
702+ // // Why do we want this behavior? The current behavior fails this
703+ // // test, but seems better and more consistent.
704+ // ("Starting from after boundary retreats the anchor",
705+ // vec![(1, Range::new(0, 9), Range::new(8, 0))]),
706+
688707 (" Jump to start of a word preceded by whitespace",
689− vec![(1, Range::new(5, 5), Range::new(5, 4))]),
708+ vec![(1, Range::new(5, 5), Range::new(6, 4))]),
690709 (" Jump to start of line from start of word preceded by whitespace",
691− vec![(1, Range::new(4, 4), Range::new(3, 0))]),
710+ vec![(1, Range::new(4, 4), Range::new(4, 0))]),
692711 ("Previous anchor is irrelevant for backward motions",
693− vec![(1, Range::new(12, 5), Range::new(5, 0))]),
712+ vec![(1, Range::new(12, 5), Range::new(6, 0))]),
694713 (" Starting from whitespace moves to first space in sequence",
695− vec![(1, Range::new(0, 3), Range::new(3, 0))]),
714+ vec![(1, Range::new(0, 4), Range::new(4, 0))]),
696715 ("Identifiers_with_underscores are considered a single word",
697716 vec![(1, Range::new(0, 20), Range::new(20, 0))]),
698717 ("Jumping\n \nback through a newline selects whitespace",
699− vec![(1, Range::new(0, 13), Range::new(11, 8))]),
718+ vec![(1, Range::new(0, 13), Range::new(12, 8))]),
700719 ("Jumping to start of word from the end selects the word",
701− vec![(1, Range::new(6, 6), Range::new(6, 0))]),
720+ vec![(1, Range::new(6, 7), Range::new(7, 0))]),
702721 ("alphanumeric.!,and.?=punctuation are considered 'words' for the purposes of word motion",
703722 vec![
704− (1, Range::new(30, 30), Range::new(30, 21)),
705− (1, Range::new(30, 21), Range::new(20, 18)),
706− (1, Range::new(20, 18), Range::new(17, 15))
723+ (1, Range::new(29, 30), Range::new(30, 21)),
724+ (1, Range::new(30, 21), Range::new(21, 18)),
725+ (1, Range::new(21, 18), Range::new(18, 15))
707726 ]),
708−
709727 ("... ... punctuation and spaces behave as expected",
710728 vec![
711− (1, Range::new(0, 10), Range::new(9, 6)),
712− (1, Range::new(9, 6), Range::new(5, 0)),
729+ (1, Range::new(0, 10), Range::new(10, 6)),
730+ (1, Range::new(10, 6), Range::new(6, 0)),
713731 ]),
714732 (".._.._ punctuation is not joined by underscores into a single block",
715− vec![(1, Range::new(0, 5), Range::new(4, 3))]),
733+ vec![(1, Range::new(0, 6), Range::new(5, 3))]),
716734 ("Newlines\n\nare bridged seamlessly.",
717735 vec![
718− (1, Range::new(0, 10), Range::new(7, 0)),
736+ (1, Range::new(0, 10), Range::new(8, 0)),
719737 ]),
720738 ("Jumping \n\n\n\n\nback from within a newline group selects previous block",
721739 vec![
722− (1, Range::new(0, 13), Range::new(10, 0)),
740+ (1, Range::new(0, 13), Range::new(11, 0)),
723741 ]),
724742 ("Failed motions do not modify the range",
725743 vec![
@@ -727,11 +745,11 @@ mod test {
727745 ]),
728746 ("Multiple motions at once resolve correctly",
729747 vec![
730− (3, Range::new(18, 18), Range::new(8, 0)),
748+ (3, Range::new(18, 18), Range::new(9, 0)),
731749 ]),
732750 ("Excessive motions are performed partially",
733751 vec![
734− (999, Range::new(40, 40), Range::new(9, 0)),
752+ (999, Range::new(40, 40), Range::new(10, 0)),
735753 ]),
736754 ("", // Edge case of moving backwards in empty string
737755 vec![
@@ -739,16 +757,16 @@ mod test {
739757 ]),
740758 ("\n\n\n\n\n", // Edge case of moving backwards in all newlines
741759 vec![
742− (1, Range::new(0, 0), Range::new(0, 0)),
760+ (1, Range::new(5, 5), Range::new(0, 0)),
743761 ]),
744762 (" \n \nJumping back through alternated space blocks and newlines selects the space blocks",
745763 vec![
746− (1, Range::new(0, 7), Range::new(6, 4)),
747− (1, Range::new(6, 4), Range::new(2, 0)),
764+ (1, Range::new(0, 8), Range::new(7, 4)),
765+ (1, Range::new(7, 4), Range::new(3, 0)),
748766 ]),
749767 ("ヒーリクス multibyte characters behave as normal characters",
750768 vec![
751− (1, Range::new(0, 5), Range::new(4, 0)),
769+ (1, Range::new(0, 6), Range::new(6, 0)),
752770 ]),
753771 ]);
754772
@@ -763,72 +781,89 @@ mod test {
763781 #[test]
764782 fn test_behaviour_when_moving_to_start_of_previous_long_words() {
765783 let tests = array::IntoIter::new([
766− ("Basic backward motion from the middle of a word",
767− vec![(1, Range::new(3, 3), Range::new(3, 0))]),
768− ("Starting from after boundary retreats the anchor",
769− vec![(1, Range::new(0, 8), Range::new(7, 0))]),
770− (" Jump to start of a word preceded by whitespace",
771− vec![(1, Range::new(5, 5), Range::new(5, 4))]),
772− (" Jump to start of line from start of word preceded by whitespace",
773− vec![(1, Range::new(4, 4), Range::new(3, 0))]),
784+ (
785+ "Basic backward motion from the middle of a word",
786+ vec![(1, Range::new(3, 3), Range::new(4, 0))],
787+ ),
788+
789+ // // Why do we want this behavior? The current behavior fails this
790+ // // test, but seems better and more consistent.
791+ // ("Starting from after boundary retreats the anchor",
792+ // vec![(1, Range::new(0, 9), Range::new(8, 0))]),
793+
794+ (
795+ " Jump to start of a word preceded by whitespace",
796+ vec![(1, Range::new(5, 5), Range::new(6, 4))],
797+ ),
798+ (
799+ " Jump to start of line from start of word preceded by whitespace",
800+ vec![(1, Range::new(3, 4), Range::new(4, 0))],
801+ ),
774802 ("Previous anchor is irrelevant for backward motions",
775− vec![(1, Range::new(12, 5), Range::new(5, 0))]),
776− (" Starting from whitespace moves to first space in sequence",
777− vec![(1, Range::new(0, 3), Range::new(3, 0))]),
803+ vec![(1, Range::new(12, 5), Range::new(6, 0))]),
804+ (
805+ " Starting from whitespace moves to first space in sequence",
806+ vec![(1, Range::new(0, 4), Range::new(4, 0))],
807+ ),
778808 ("Identifiers_with_underscores are considered a single word",
779809 vec![(1, Range::new(0, 20), Range::new(20, 0))]),
780− ("Jumping\n \nback through a newline selects whitespace",
781− vec![(1, Range::new(0, 13), Range::new(11, 8))]),
782− ("Jumping to start of word from the end selects the word",
783− vec![(1, Range::new(6, 6), Range::new(6, 0))]),
784− ("alphanumeric.!,and.?=punctuation are treated exactly the same",
785− vec![
786− (1, Range::new(30, 30), Range::new(30, 0)),
787− ]),
788−
789− ("... ... punctuation and spaces behave as expected",
810+ (
811+ "Jumping\n \nback through a newline selects whitespace",
812+ vec![(1, Range::new(0, 13), Range::new(12, 8))],
813+ ),
814+ (
815+ "Jumping to start of word from the end selects the word",
816+ vec![(1, Range::new(6, 7), Range::new(7, 0))],
817+ ),
818+ (
819+ "alphanumeric.!,and.?=punctuation are treated exactly the same",
820+ vec![(1, Range::new(29, 30), Range::new(30, 0))],
821+ ),
822+ (
823+ "... ... punctuation and spaces behave as expected",
790824 vec![
791− (1, Range::new(0, 10), Range::new(9, 6)),
792− (1, Range::new(9, 6), Range::new(5, 0)),
793− ]),
825+ (1, Range::new(0, 10), Range::new(10, 6)),
826+ (1, Range::new(10, 6), Range::new(6, 0)),
827+ ],
828+ ),
794829 (".._.._ punctuation is joined by underscores into a single block",
795− vec![(1, Range::new(0, 5), Range::new(4, 0))]),
796− ("Newlines\n\nare bridged seamlessly.",
797− vec![
798− (1, Range::new(0, 10), Range::new(7, 0)),
799− ]),
800− ("Jumping \n\n\n\n\nback from within a newline group selects previous block",
801− vec![
802− (1, Range::new(0, 13), Range::new(10, 0)),
803− ]),
804− ("Failed motions do not modify the range",
805− vec![
806− (0, Range::new(3, 0), Range::new(3, 0)),
807− ]),
808− ("Multiple motions at once resolve correctly",
809− vec![
810− (3, Range::new(18, 18), Range::new(8, 0)),
811− ]),
812− ("Excessive motions are performed partially",
813− vec![
814− (999, Range::new(40, 40), Range::new(9, 0)),
815− ]),
816− ("", // Edge case of moving backwards in empty string
817− vec![
818− (1, Range::new(0, 0), Range::new(0, 0)),
819− ]),
820− ("\n\n\n\n\n", // Edge case of moving backwards in all newlines
821− vec![
822− (1, Range::new(0, 0), Range::new(0, 0)),
823− ]),
830+ vec![(1, Range::new(0, 6), Range::new(6, 0))]),
831+ (
832+ "Newlines\n\nare bridged seamlessly.",
833+ vec![(1, Range::new(0, 10), Range::new(8, 0))],
834+ ),
835+ (
836+ "Jumping \n\n\n\n\nback from within a newline group selects previous block",
837+ vec![(1, Range::new(0, 13), Range::new(11, 0))],
838+ ),
839+ (
840+ "Failed motions do not modify the range",
841+ vec![(0, Range::new(3, 0), Range::new(3, 0))],
842+ ),
843+ (
844+ "Multiple motions at once resolve correctly",
845+ vec![(3, Range::new(19, 19), Range::new(9, 0))],
846+ ),
847+ (
848+ "Excessive motions are performed partially",
849+ vec![(999, Range::new(40, 40), Range::new(10, 0))],
850+ ),
851+ (
852+ "", // Edge case of moving backwards in empty string
853+ vec![(1, Range::new(0, 0), Range::new(0, 0))],
854+ ),
855+ (
856+ "\n\n\n\n\n", // Edge case of moving backwards in all newlines
857+ vec![(1, Range::new(5, 5), Range::new(0, 0))],
858+ ),
824859 (" \n \nJumping back through alternated space blocks and newlines selects the space blocks",
825860 vec![
826− (1, Range::new(0, 7), Range::new(6, 4)),
827− (1, Range::new(6, 4), Range::new(2, 0)),
861+ (1, Range::new(0, 8), Range::new(7, 4)),
862+ (1, Range::new(7, 4), Range::new(3, 0)),
828863 ]),
829864 ("ヒーリ..クス multibyte characters behave as normal characters, including when interacting with punctuation",
830865 vec![
831− (1, Range::new(0, 7), Range::new(6, 0)),
866+ (1, Range::new(0, 8), Range::new(8, 0)),
832867 ]),
833868 ]);
834869
@@ -844,42 +879,46 @@ mod test {
844879 fn test_behaviour_when_moving_to_end_of_next_words() {
845880 let tests = array::IntoIter::new([
846881 ("Basic forward motion from the start of a word to the end of it",
847− vec![(1, Range::new(0, 0), Range::new(0, 4))]),
882+ vec![(1, Range::new(0, 0), Range::new(0, 5))]),
848883 ("Basic forward motion from the end of a word to the end of the next",
849− vec![(1, Range::new(0, 4), Range::new(5, 12))]),
884+ vec![(1, Range::new(0, 5), Range::new(5, 13))]),
850885 ("Basic forward motion from the middle of a word to the end of it",
851− vec![(1, Range::new(2, 2), Range::new(2, 4))]),
886+ vec![(1, Range::new(2, 2), Range::new(2, 5))]),
852887 (" Jumping to end of a word preceded by whitespace",
853− vec![(1, Range::new(0, 0), Range::new(0, 10))]),
854− (" Starting from a boundary advances the anchor",
855− vec![(1, Range::new(0, 0), Range::new(1, 8))]),
888+ vec![(1, Range::new(0, 0), Range::new(0, 11))]),
889+
890+ // // Why do we want this behavior? The current behavior fails this
891+ // // test, but seems better and more consistent.
892+ // (" Starting from a boundary advances the anchor",
893+ // vec![(1, Range::new(0, 0), Range::new(1, 9))]),
894+
856895 ("Previous anchor is irrelevant for end of word motion",
857− vec![(1, Range::new(12, 2), Range::new(2, 7))]),
896+ vec![(1, Range::new(12, 2), Range::new(2, 8))]),
858897 ("Identifiers_with_underscores are considered a single word",
859− vec![(1, Range::new(0, 0), Range::new(0, 27))]),
898+ vec![(1, Range::new(0, 0), Range::new(0, 28))]),
860899 ("Jumping\n into starting whitespace selects up to the end of next word",
861− vec![(1, Range::new(0, 6), Range::new(8, 15))]),
900+ vec![(1, Range::new(0, 7), Range::new(8, 16))]),
862901 ("alphanumeric.!,and.?=punctuation are considered 'words' for the purposes of word motion",
863902 vec![
864− (1, Range::new(0, 0), Range::new(0, 11)),
865− (1, Range::new(0, 11), Range::new(12, 14)),
866− (1, Range::new(12, 14), Range::new(15, 17))
903+ (1, Range::new(0, 0), Range::new(0, 12)),
904+ (1, Range::new(0, 12), Range::new(12, 15)),
905+ (1, Range::new(12, 15), Range::new(15, 18))
867906 ]),
868907 ("... ... punctuation and spaces behave as expected",
869908 vec![
870− (1, Range::new(0, 0), Range::new(0, 2)),
871− (1, Range::new(0, 2), Range::new(3, 8)),
909+ (1, Range::new(0, 0), Range::new(0, 3)),
910+ (1, Range::new(0, 3), Range::new(3, 9)),
872911 ]),
873912 (".._.._ punctuation is not joined by underscores into a single block",
874− vec![(1, Range::new(0, 0), Range::new(0, 1))]),
913+ vec![(1, Range::new(0, 0), Range::new(0, 2))]),
875914 ("Newlines\n\nare bridged seamlessly.",
876915 vec![
877− (1, Range::new(0, 0), Range::new(0, 7)),
878− (1, Range::new(0, 7), Range::new(10, 12)),
916+ (1, Range::new(0, 0), Range::new(0, 8)),
917+ (1, Range::new(0, 8), Range::new(10, 13)),
879918 ]),
880919 ("Jumping\n\n\n\n\n\n from newlines to whitespace selects to end of next word.",
881920 vec![
882− (1, Range::new(0, 8), Range::new(13, 19)),
921+ (1, Range::new(0, 8), Range::new(13, 20)),
883922 ]),
884923 ("A failed motion does not modify the range",
885924 vec![
@@ -887,11 +926,11 @@ mod test {
887926 ]),
888927 ("Multiple motions at once resolve correctly",
889928 vec![
890− (3, Range::new(0, 0), Range::new(16, 18)),
929+ (3, Range::new(0, 0), Range::new(16, 19)),
891930 ]),
892931 ("Excessive motions are performed partially",
893932 vec![
894− (999, Range::new(0, 0), Range::new(31, 40)),
933+ (999, Range::new(0, 0), Range::new(31, 41)),
895934 ]),
896935 ("", // Edge case of moving forward in empty string
897936 vec![
@@ -899,16 +938,16 @@ mod test {
899938 ]),
900939 ("\n\n\n\n\n", // Edge case of moving forward in all newlines
901940 vec![
902− (1, Range::new(0, 0), Range::new(0, 4)),
941+ (1, Range::new(0, 0), Range::new(5, 5)),
903942 ]),
904943 ("\n \n \n Jumping through alternated space blocks and newlines selects the space blocks",
905944 vec![
906− (1, Range::new(0, 0), Range::new(1, 3)),
907− (1, Range::new(1, 3), Range::new(5, 7)),
945+ (1, Range::new(0, 0), Range::new(1, 4)),
946+ (1, Range::new(1, 4), Range::new(5, 8)),
908947 ]),
909948 ("ヒーリクス multibyte characters behave as normal characters",
910949 vec![
911− (1, Range::new(0, 0), Range::new(0, 4)),
950+ (1, Range::new(0, 0), Range::new(0, 5)),
912951 ]),
913952 ]);
914953
@@ -924,44 +963,44 @@ mod test {
924963 fn test_behaviour_when_moving_to_end_of_previous_words() {
925964 let tests = array::IntoIter::new([
926965 ("Basic backward motion from the middle of a word",
927− vec![(1, Range::new(9, 9), Range::new(9, 5))]),
966+ vec![(1, Range::new(9, 9), Range::new(10, 5))]),
928967 ("Starting from after boundary retreats the anchor",
929− vec![(1, Range::new(0, 13), Range::new(12, 8))]),
968+ vec![(1, Range::new(0, 14), Range::new(13, 8))]),
930969 ("Jump to end of a word succeeded by whitespace",
931− vec![(1, Range::new(10, 10), Range::new(10, 4))]),
970+ vec![(1, Range::new(11, 11), Range::new(11, 4))]),
932971 (" Jump to start of line from end of word preceded by whitespace",
933− vec![(1, Range::new(7, 7), Range::new(7, 0))]),
972+ vec![(1, Range::new(8, 8), Range::new(8, 0))]),
934973 ("Previous anchor is irrelevant for backward motions",
935− vec![(1, Range::new(26, 12), Range::new(12, 8))]),
974+ vec![(1, Range::new(26, 12), Range::new(13, 8))]),
936975 (" Starting from whitespace moves to first space in sequence",
937− vec![(1, Range::new(0, 3), Range::new(3, 0))]),
976+ vec![(1, Range::new(0, 4), Range::new(4, 0))]),
938977 ("Test identifiers_with_underscores are considered a single word",
939978 vec![(1, Range::new(0, 25), Range::new(25, 4))]),
940979 ("Jumping\n \nback through a newline selects whitespace",
941− vec![(1, Range::new(0, 13), Range::new(11, 8))]),
980+ vec![(1, Range::new(0, 13), Range::new(12, 8))]),
942981 ("Jumping to start of word from the end selects the whole word",
943− vec![(1, Range::new(15, 15), Range::new(15, 10))]),
982+ vec![(1, Range::new(16, 16), Range::new(16, 10))]),
944983 ("alphanumeric.!,and.?=punctuation are considered 'words' for the purposes of word motion",
945984 vec![
946− (1, Range::new(30, 30), Range::new(30, 21)),
947− (1, Range::new(30, 21), Range::new(20, 18)),
948− (1, Range::new(20, 18), Range::new(17, 15))
985+ (1, Range::new(30, 30), Range::new(31, 21)),
986+ (1, Range::new(31, 21), Range::new(21, 18)),
987+ (1, Range::new(21, 18), Range::new(18, 15))
949988 ]),
950989
951990 ("... ... punctuation and spaces behave as expected",
952991 vec![
953− (1, Range::new(0, 10), Range::new(9, 9)),
954− (1, Range::new(9, 6), Range::new(5, 3)),
992+ (1, Range::new(0, 10), Range::new(9, 3)),
993+ (1, Range::new(9, 3), Range::new(3, 0)),
955994 ]),
956995 (".._.._ punctuation is not joined by underscores into a single block",
957− vec![(1, Range::new(0, 5), Range::new(4, 3))]),
996+ vec![(1, Range::new(0, 5), Range::new(5, 3))]),
958997 ("Newlines\n\nare bridged seamlessly.",
959998 vec![
960− (1, Range::new(0, 10), Range::new(7, 0)),
999+ (1, Range::new(0, 10), Range::new(8, 0)),
9611000 ]),
9621001 ("Jumping \n\n\n\n\nback from within a newline group selects previous block",
9631002 vec![
964− (1, Range::new(0, 13), Range::new(10, 7)),
1003+ (1, Range::new(0, 13), Range::new(11, 7)),
9651004 ]),
9661005 ("Failed motions do not modify the range",
9671006 vec![
@@ -969,11 +1008,11 @@ mod test {
9691008 ]),
9701009 ("Multiple motions at once resolve correctly",
9711010 vec![
972− (3, Range::new(23, 23), Range::new(15, 8)),
1011+ (3, Range::new(24, 24), Range::new(16, 8)),
9731012 ]),
9741013 ("Excessive motions are performed partially",
9751014 vec![
976− (999, Range::new(40, 40), Range::new(8, 0)),
1015+ (999, Range::new(40, 40), Range::new(9, 0)),
9771016 ]),
9781017 ("", // Edge case of moving backwards in empty string
9791018 vec![
@@ -981,16 +1020,16 @@ mod test {
9811020 ]),
9821021 ("\n\n\n\n\n", // Edge case of moving backwards in all newlines
9831022 vec![
984− (1, Range::new(0, 0), Range::new(0, 0)),
1023+ (1, Range::new(5, 5), Range::new(0, 0)),
9851024 ]),
9861025 (" \n \nJumping back through alternated space blocks and newlines selects the space blocks",
9871026 vec![
988− (1, Range::new(0, 7), Range::new(6, 4)),
989− (1, Range::new(6, 4), Range::new(2, 0)),
1027+ (1, Range::new(0, 8), Range::new(7, 4)),
1028+ (1, Range::new(7, 4), Range::new(3, 0)),
9901029 ]),
9911030 ("Test ヒーリクス multibyte characters behave as normal characters",
9921031 vec![
993− (1, Range::new(0, 9), Range::new(9, 4)),
1032+ (1, Range::new(0, 10), Range::new(10, 4)),
9941033 ]),
9951034 ]);
9961035
@@ -1006,40 +1045,44 @@ mod test {
10061045 fn test_behaviour_when_moving_to_end_of_next_long_words() {
10071046 let tests = array::IntoIter::new([
10081047 ("Basic forward motion from the start of a word to the end of it",
1009− vec![(1, Range::new(0, 0), Range::new(0, 4))]),
1048+ vec![(1, Range::new(0, 0), Range::new(0, 5))]),
10101049 ("Basic forward motion from the end of a word to the end of the next",
1011− vec![(1, Range::new(0, 4), Range::new(5, 12))]),
1050+ vec![(1, Range::new(0, 5), Range::new(5, 13))]),
10121051 ("Basic forward motion from the middle of a word to the end of it",
1013− vec![(1, Range::new(2, 2), Range::new(2, 4))]),
1052+ vec![(1, Range::new(2, 2), Range::new(2, 5))]),
10141053 (" Jumping to end of a word preceded by whitespace",
1015− vec![(1, Range::new(0, 0), Range::new(0, 10))]),
1016− (" Starting from a boundary advances the anchor",
1017− vec![(1, Range::new(0, 0), Range::new(1, 8))]),
1054+ vec![(1, Range::new(0, 0), Range::new(0, 11))]),
1055+
1056+ // // Why do we want this behavior? The current behavior fails this
1057+ // // test, but seems better and more consistent.
1058+ // (" Starting from a boundary advances the anchor",
1059+ // vec![(1, Range::new(0, 0), Range::new(1, 9))]),
1060+
10181061 ("Previous anchor is irrelevant for end of word motion",
1019− vec![(1, Range::new(12, 2), Range::new(2, 7))]),
1062+ vec![(1, Range::new(12, 2), Range::new(2, 8))]),
10201063 ("Identifiers_with_underscores are considered a single word",
1021− vec![(1, Range::new(0, 0), Range::new(0, 27))]),
1064+ vec![(1, Range::new(0, 0), Range::new(0, 28))]),
10221065 ("Jumping\n into starting whitespace selects up to the end of next word",
1023− vec![(1, Range::new(0, 6), Range::new(8, 15))]),
1066+ vec![(1, Range::new(0, 7), Range::new(8, 16))]),
10241067 ("alphanumeric.!,and.?=punctuation are treated the same way",
10251068 vec![
1026− (1, Range::new(0, 0), Range::new(0, 31)),
1069+ (1, Range::new(0, 0), Range::new(0, 32)),
10271070 ]),
10281071 ("... ... punctuation and spaces behave as expected",
10291072 vec![
1030− (1, Range::new(0, 0), Range::new(0, 2)),
1031− (1, Range::new(0, 2), Range::new(3, 8)),
1073+ (1, Range::new(0, 0), Range::new(0, 3)),
1074+ (1, Range::new(0, 3), Range::new(3, 9)),
10321075 ]),
10331076 (".._.._ punctuation is joined by underscores into a single block",
1034− vec![(1, Range::new(0, 0), Range::new(0, 5))]),
1077+ vec![(1, Range::new(0, 0), Range::new(0, 6))]),
10351078 ("Newlines\n\nare bridged seamlessly.",
10361079 vec![
1037− (1, Range::new(0, 0), Range::new(0, 7)),
1038− (1, Range::new(0, 7), Range::new(10, 12)),
1080+ (1, Range::new(0, 0), Range::new(0, 8)),
1081+ (1, Range::new(0, 8), Range::new(10, 13)),
10391082 ]),
10401083 ("Jumping\n\n\n\n\n\n from newlines to whitespace selects to end of next word.",
10411084 vec![
1042− (1, Range::new(0, 8), Range::new(13, 19)),
1085+ (1, Range::new(0, 9), Range::new(13, 20)),
10431086 ]),
10441087 ("A failed motion does not modify the range",
10451088 vec![
@@ -1047,11 +1090,11 @@ mod test {
10471090 ]),
10481091 ("Multiple motions at once resolve correctly",
10491092 vec![
1050− (3, Range::new(0, 0), Range::new(16, 18)),
1093+ (3, Range::new(0, 0), Range::new(16, 19)),
10511094 ]),
10521095 ("Excessive motions are performed partially",
10531096 vec![
1054− (999, Range::new(0, 0), Range::new(31, 40)),
1097+ (999, Range::new(0, 0), Range::new(31, 41)),
10551098 ]),
10561099 ("", // Edge case of moving forward in empty string
10571100 vec![
@@ -1059,16 +1102,16 @@ mod test {
10591102 ]),
10601103 ("\n\n\n\n\n", // Edge case of moving forward in all newlines
10611104 vec![
1062− (1, Range::new(0, 0), Range::new(0, 4)),
1105+ (1, Range::new(0, 0), Range::new(5, 5)),
10631106 ]),
10641107 ("\n \n \n Jumping through alternated space blocks and newlines selects the space blocks",
10651108 vec![
1066− (1, Range::new(0, 0), Range::new(1, 3)),
1067− (1, Range::new(1, 3), Range::new(5, 7)),
1109+ (1, Range::new(0, 0), Range::new(1, 4)),
1110+ (1, Range::new(1, 4), Range::new(5, 8)),
10681111 ]),
10691112 ("ヒーリ..クス multibyte characters behave as normal characters, including when they interact with punctuation",
10701113 vec![
1071− (1, Range::new(0, 0), Range::new(0, 6)),
1114+ (1, Range::new(0, 0), Range::new(0, 7)),
10721115 ]),
10731116 ]);
10741117
helix-core/src/object.rs+1−1
@@ -5,7 +5,7 @@ use crate::{Range, RopeSlice, Selection, Syntax};
55pub fn expand_selection(syntax: &Syntax, text: RopeSlice, selection: &Selection) -> Selection {
66 let tree = syntax.tree();
77
8− selection.transform(|range| {
8+ selection.clone().transform(|range| {
99 let from = text.char_to_byte(range.from());
1010 let to = text.char_to_byte(range.to());
1111
helix-core/src/position.rs+115−29
@@ -1,6 +1,7 @@
11use crate::{
22 chars::char_is_line_ending,
3− graphemes::{nth_next_grapheme_boundary, RopeGraphemes},
3+ graphemes::{ensure_grapheme_boundary_prev, RopeGraphemes},
4+ line_ending::line_end_char_index,
45 RopeSlice,
56};
67
@@ -52,19 +53,50 @@ impl From<Position> for tree_sitter::Point {
5253 }
5354}
5455/// Convert a character index to (line, column) coordinates.
56+///
57+/// TODO: this should be split into two methods: one for visual
58+/// row/column, and one for "objective" row/column (possibly with
59+/// the column specified in `char`s). The former would be used
60+/// for cursor movement, and the latter would be used for e.g. the
61+/// row:column display in the status line.
5562pub fn coords_at_pos(text: RopeSlice, pos: usize) -> Position {
5663 let line = text.char_to_line(pos);
64+
5765 let line_start = text.line_to_char(line);
66+ let pos = ensure_grapheme_boundary_prev(text, pos);
5867 let col = RopeGraphemes::new(text.slice(line_start..pos)).count();
68+
5969 Position::new(line, col)
6070}
6171
6272/// Convert (line, column) coordinates to a character index.
63−pub fn pos_at_coords(text: RopeSlice, coords: Position) -> usize {
73+///
74+/// `is_1_width` specifies whether the position should be treated
75+/// as a block cursor or not. This effects how line-ends are handled.
76+/// `false` corresponds to properly round-tripping with `coords_at_pos()`,
77+/// whereas `true` will ensure that block cursors don't jump off the
78+/// end of the line.
79+///
80+/// TODO: this should be changed to work in terms of visual row/column, not
81+/// graphemes.
82+pub fn pos_at_coords(text: RopeSlice, coords: Position, is_1_width: bool) -> usize {
6483 let Position { row, col } = coords;
6584 let line_start = text.line_to_char(row);
66− // line_start + col
67− nth_next_grapheme_boundary(text, line_start, col)
85+ let line_end = if is_1_width {
86+ line_end_char_index(&text, row)
87+ } else {
88+ text.line_to_char((row + 1).min(text.len_lines()))
89+ };
90+
91+ let mut col_char_offset = 0;
92+ for (i, g) in RopeGraphemes::new(text.slice(line_start..line_end)).enumerate() {
93+ if i == col {
94+ break;
95+ }
96+ col_char_offset += g.chars().count();
97+ }
98+
99+ line_start + col_char_offset
68100}
69101
70102#[cfg(test)]
@@ -80,55 +112,109 @@ mod test {
80112
81113 #[test]
82114 fn test_coords_at_pos() {
83− // let text = Rope::from("ḧëḷḷö\nẅöṛḷḋ");
84− // let slice = text.slice(..);
85− // assert_eq!(coords_at_pos(slice, 0), (0, 0).into());
86− // assert_eq!(coords_at_pos(slice, 5), (0, 5).into()); // position on \n
87− // assert_eq!(coords_at_pos(slice, 6), (1, 0).into()); // position on w
88− // assert_eq!(coords_at_pos(slice, 7), (1, 1).into()); // position on o
89− // assert_eq!(coords_at_pos(slice, 10), (1, 4).into()); // position on d
90−
91− // test with grapheme clusters
115+ let text = Rope::from("ḧëḷḷö\nẅöṛḷḋ");
116+ let slice = text.slice(..);
117+ assert_eq!(coords_at_pos(slice, 0), (0, 0).into());
118+ assert_eq!(coords_at_pos(slice, 5), (0, 5).into()); // position on \n
119+ assert_eq!(coords_at_pos(slice, 6), (1, 0).into()); // position on w
120+ assert_eq!(coords_at_pos(slice, 7), (1, 1).into()); // position on o
121+ assert_eq!(coords_at_pos(slice, 10), (1, 4).into()); // position on d
122+
123+ // Test with wide characters.
124+ // TODO: account for character width.
125+ let text = Rope::from("今日はいい\n");
126+ let slice = text.slice(..);
127+ assert_eq!(coords_at_pos(slice, 0), (0, 0).into());
128+ assert_eq!(coords_at_pos(slice, 1), (0, 1).into());
129+ assert_eq!(coords_at_pos(slice, 2), (0, 2).into());
130+ assert_eq!(coords_at_pos(slice, 3), (0, 3).into());
131+ assert_eq!(coords_at_pos(slice, 4), (0, 4).into());
132+ assert_eq!(coords_at_pos(slice, 5), (0, 5).into());
133+ assert_eq!(coords_at_pos(slice, 6), (1, 0).into());
134+
135+ // Test with grapheme clusters.
92136 let text = Rope::from("a̐éö̲\r\n");
93137 let slice = text.slice(..);
94138 assert_eq!(coords_at_pos(slice, 0), (0, 0).into());
95139 assert_eq!(coords_at_pos(slice, 2), (0, 1).into());
96140 assert_eq!(coords_at_pos(slice, 4), (0, 2).into());
97141 assert_eq!(coords_at_pos(slice, 7), (0, 3).into());
142+ assert_eq!(coords_at_pos(slice, 9), (1, 0).into());
98143
99− let text = Rope::from("किमपि");
144+ // Test with wide-character grapheme clusters.
145+ // TODO: account for character width.
146+ let text = Rope::from("किमपि\n");
100147 let slice = text.slice(..);
101148 assert_eq!(coords_at_pos(slice, 0), (0, 0).into());
102149 assert_eq!(coords_at_pos(slice, 2), (0, 1).into());
103150 assert_eq!(coords_at_pos(slice, 3), (0, 2).into());
104151 assert_eq!(coords_at_pos(slice, 5), (0, 3).into());
152+ assert_eq!(coords_at_pos(slice, 6), (1, 0).into());
153+
154+ // Test with tabs.
155+ // Todo: account for tab stops.
156+ let text = Rope::from("\tHello\n");
157+ let slice = text.slice(..);
158+ assert_eq!(coords_at_pos(slice, 0), (0, 0).into());
159+ assert_eq!(coords_at_pos(slice, 1), (0, 1).into());
160+ assert_eq!(coords_at_pos(slice, 2), (0, 2).into());
105161 }
106162
107163 #[test]
108164 fn test_pos_at_coords() {
109165 let text = Rope::from("ḧëḷḷö\nẅöṛḷḋ");
110166 let slice = text.slice(..);
111− assert_eq!(pos_at_coords(slice, (0, 0).into()), 0);
112− assert_eq!(pos_at_coords(slice, (0, 5).into()), 5); // position on \n
113− assert_eq!(pos_at_coords(slice, (1, 0).into()), 6); // position on w
114− assert_eq!(pos_at_coords(slice, (1, 1).into()), 7); // position on o
115− assert_eq!(pos_at_coords(slice, (1, 4).into()), 10); // position on d
167+ assert_eq!(pos_at_coords(slice, (0, 0).into(), false), 0);
168+ assert_eq!(pos_at_coords(slice, (0, 5).into(), false), 5); // position on \n
169+ assert_eq!(pos_at_coords(slice, (0, 6).into(), false), 6); // position after \n
170+ assert_eq!(pos_at_coords(slice, (0, 6).into(), true), 5); // position after \n
171+ assert_eq!(pos_at_coords(slice, (1, 0).into(), false), 6); // position on w
172+ assert_eq!(pos_at_coords(slice, (1, 1).into(), false), 7); // position on o
173+ assert_eq!(pos_at_coords(slice, (1, 4).into(), false), 10); // position on d
116174
117− // test with grapheme clusters
175+ // Test with wide characters.
176+ // TODO: account for character width.
177+ let text = Rope::from("今日はいい\n");
178+ let slice = text.slice(..);
179+ assert_eq!(pos_at_coords(slice, (0, 0).into(), false), 0);
180+ assert_eq!(pos_at_coords(slice, (0, 1).into(), false), 1);
181+ assert_eq!(pos_at_coords(slice, (0, 2).into(), false), 2);
182+ assert_eq!(pos_at_coords(slice, (0, 3).into(), false), 3);
183+ assert_eq!(pos_at_coords(slice, (0, 4).into(), false), 4);
184+ assert_eq!(pos_at_coords(slice, (0, 5).into(), false), 5);
185+ assert_eq!(pos_at_coords(slice, (0, 6).into(), false), 6);
186+ assert_eq!(pos_at_coords(slice, (0, 6).into(), true), 5);
187+ assert_eq!(pos_at_coords(slice, (1, 0).into(), false), 6);
188+
189+ // Test with grapheme clusters.
118190 let text = Rope::from("a̐éö̲\r\n");
119191 let slice = text.slice(..);
120− assert_eq!(pos_at_coords(slice, (0, 0).into()), 0);
121− assert_eq!(pos_at_coords(slice, (0, 1).into()), 2);
122− assert_eq!(pos_at_coords(slice, (0, 2).into()), 4);
123− assert_eq!(pos_at_coords(slice, (0, 3).into()), 7); // \r\n is one char here
124− assert_eq!(pos_at_coords(slice, (0, 4).into()), 9);
192+ assert_eq!(pos_at_coords(slice, (0, 0).into(), false), 0);
193+ assert_eq!(pos_at_coords(slice, (0, 1).into(), false), 2);
194+ assert_eq!(pos_at_coords(slice, (0, 2).into(), false), 4);
195+ assert_eq!(pos_at_coords(slice, (0, 3).into(), false), 7); // \r\n is one char here
196+ assert_eq!(pos_at_coords(slice, (0, 4).into(), false), 9);
197+ assert_eq!(pos_at_coords(slice, (0, 4).into(), true), 7);
198+ assert_eq!(pos_at_coords(slice, (1, 0).into(), false), 9);
199+
200+ // Test with wide-character grapheme clusters.
201+ // TODO: account for character width.
125202 let text = Rope::from("किमपि");
126203 // 2 - 1 - 2 codepoints
127204 // TODO: delete handling as per https://news.ycombinator.com/item?id=20058454
128205 let slice = text.slice(..);
129− assert_eq!(pos_at_coords(slice, (0, 0).into()), 0);
130− assert_eq!(pos_at_coords(slice, (0, 1).into()), 2);
131− assert_eq!(pos_at_coords(slice, (0, 2).into()), 3);
132− assert_eq!(pos_at_coords(slice, (0, 3).into()), 5); // eol
206+ assert_eq!(pos_at_coords(slice, (0, 0).into(), false), 0);
207+ assert_eq!(pos_at_coords(slice, (0, 1).into(), false), 2);
208+ assert_eq!(pos_at_coords(slice, (0, 2).into(), false), 3);
209+ assert_eq!(pos_at_coords(slice, (0, 3).into(), false), 5);
210+ assert_eq!(pos_at_coords(slice, (0, 3).into(), true), 5);
211+
212+ // Test with tabs.
213+ // Todo: account for tab stops.
214+ let text = Rope::from("\tHello\n");
215+ let slice = text.slice(..);
216+ assert_eq!(pos_at_coords(slice, (0, 0).into(), false), 0);
217+ assert_eq!(pos_at_coords(slice, (0, 1).into(), false), 1);
218+ assert_eq!(pos_at_coords(slice, (0, 2).into(), false), 2);
133219 }
134220}
helix-core/src/search.rs+10−28
@@ -1,18 +1,11 @@
11use crate::RopeSlice;
22
3−pub fn find_nth_next(
4− text: RopeSlice,
5− ch: char,
6− mut pos: usize,
7− n: usize,
8− inclusive: bool,
9−) -> Option<usize> {
10− if pos >= text.len_chars() {
3+pub fn find_nth_next(text: RopeSlice, ch: char, mut pos: usize, n: usize) -> Option<usize> {
4+ if pos >= text.len_chars() || n == 0 {
115 return None;
126 }
137
14− // start searching right after pos
15− let mut chars = text.chars_at(pos + 1);
8+ let mut chars = text.chars_at(pos);
169
1710 for _ in 0..n {
1811 loop {
@@ -26,28 +19,21 @@ pub fn find_nth_next(
2619 }
2720 }
2821
29− if !inclusive {
30− pos -= 1;
31− }
32−
33− Some(pos)
22+ Some(pos - 1)
3423}
3524
36−pub fn find_nth_prev(
37− text: RopeSlice,
38− ch: char,
39− mut pos: usize,
40− n: usize,
41− inclusive: bool,
42−) -> Option<usize> {
43− // start searching right before pos
25+pub fn find_nth_prev(text: RopeSlice, ch: char, mut pos: usize, n: usize) -> Option<usize> {
26+ if pos == 0 || n == 0 {
27+ return None;
28+ }
29+
4430 let mut chars = text.chars_at(pos);
4531
4632 for _ in 0..n {
4733 loop {
4834 let c = chars.prev()?;
4935
50− pos = pos.saturating_sub(1);
36+ pos -= 1;
5137
5238 if c == ch {
5339 break;
@@ -55,9 +41,5 @@ pub fn find_nth_prev(
5541 }
5642 }
5743
58− if !inclusive {
59− pos += 1;
60− }
61−
6244 Some(pos)
6345}
helix-core/src/selection.rs+529−133
@@ -1,30 +1,59 @@
1−//! Selections are the primary editing construct. Even a single cursor is defined as an empty
2−//! single selection range.
1+//! Selections are the primary editing construct. Even cursors are
2+//! defined as a selection range.
33//!
44//! All positioning is done via `char` offsets into the buffer.
5−use crate::{Assoc, ChangeSet, RopeSlice};
5+use crate::{
6+ graphemes::{
7+ ensure_grapheme_boundary_next, ensure_grapheme_boundary_prev, next_grapheme_boundary,
8+ prev_grapheme_boundary,
9+ },
10+ Assoc, ChangeSet, RopeSlice,
11+};
612use smallvec::{smallvec, SmallVec};
713use std::borrow::Cow;
814
9−#[inline]
10−fn abs_difference(x: usize, y: usize) -> usize {
11− if x < y {
12− y - x
13− } else {
14− x - y
15− }
16−}
17−
18−/// A single selection range. Anchor-inclusive, head-exclusive.
15+/// A single selection range.
16+///
17+/// A range consists of an "anchor" and "head" position in
18+/// the text. The head is the part that the user moves when
19+/// directly extending a selection. The head and anchor
20+/// can be in any order, or even share the same position.
21+///
22+/// The anchor and head positions use gap indexing, meaning
23+/// that their indices represent the the gaps *between* `char`s
24+/// rather than the `char`s themselves. For example, 1
25+/// represents the position between the first and second `char`.
26+///
27+/// Below are some example `Range` configurations to better
28+/// illustrate. The anchor and head indices are show as
29+/// "(anchor, head)", followed by example text with "[" and "]"
30+/// inserted to represent the anchor and head positions:
31+///
32+/// - (0, 3): [Som]e text.
33+/// - (3, 0): ]Som[e text.
34+/// - (2, 7): So[me te]xt.
35+/// - (1, 1): S[]ome text.
36+///
37+/// Ranges are considered to be inclusive on the left and
38+/// exclusive on the right, regardless of anchor-head ordering.
39+/// This means, for example, that non-zero-width ranges that
40+/// are directly adjecent, sharing an edge, do not overlap.
41+/// However, a zero-width range will overlap with the shared
42+/// left-edge of another range.
43+///
44+/// By convention, user-facing ranges are considered to have
45+/// a block cursor on the head-side of the range that spans a
46+/// single grapheme inward from the range's edge. There are a
47+/// variety of helper methods on `Range` for working in terms of
48+/// that block cursor, all of which have `cursor` in their name.
1949#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2050pub struct Range {
21− // TODO: optimize into u32
2251 /// The anchor of the range: the side that doesn't move when extending.
2352 pub anchor: usize,
2453 /// The head of the range, moved when extending.
2554 pub head: usize,
2655 pub horiz: Option<u32>,
27−} // TODO: might be cheaper to store normalized as from/to and an inverted flag
56+}
2857
2958impl Range {
3059 pub fn new(anchor: usize, head: usize) -> Self {
@@ -53,6 +82,20 @@ impl Range {
5382 std::cmp::max(self.anchor, self.head)
5483 }
5584
85+ /// The (inclusive) range of lines that the range overlaps.
86+ #[inline]
87+ #[must_use]
88+ pub fn line_range(&self, text: RopeSlice) -> (usize, usize) {
89+ let from = self.from();
90+ let to = if self.is_empty() {
91+ self.to()
92+ } else {
93+ prev_grapheme_boundary(text, self.to()).max(from)
94+ };
95+
96+ (text.char_to_line(from), text.char_to_line(to))
97+ }
98+
5699 /// `true` when head and anchor are at the same position.
57100 #[inline]
58101 pub fn is_empty(&self) -> bool {
@@ -62,37 +105,39 @@ impl Range {
62105 /// Check two ranges for overlap.
63106 #[must_use]
64107 pub fn overlaps(&self, other: &Self) -> bool {
65− // cursor overlap is checked differently
66− if self.is_empty() {
67− let pos = self.head;
68− pos >= other.from() && other.to() >= pos
69− } else {
70− self.to() > other.from() && other.to() > self.from()
71− }
108+ // To my eye, it's non-obvious why this works, but I arrived
109+ // at it after transforming the slower version that explicitly
110+ // enumerated more cases. The unit tests are thorough.
111+ self.from() == other.from() || (self.to() > other.from() && other.to() > self.from())
72112 }
73113
74114 pub fn contains(&self, pos: usize) -> bool {
75− if self.is_empty() {
76− return false;
77− }
78−
79− if self.anchor < self.head {
80− self.anchor <= pos && pos < self.head
81− } else {
82− self.head < pos && pos <= self.anchor
83− }
115+ self.from() <= pos && pos < self.to()
84116 }
85117
86118 /// Map a range through a set of changes. Returns a new range representing the same position
87119 /// after the changes are applied.
88120 pub fn map(self, changes: &ChangeSet) -> Self {
89− let anchor = changes.map_pos(self.anchor, Assoc::After);
90− let head = changes.map_pos(self.head, Assoc::After);
91−
92− // TODO: possibly unnecessary
93− if self.anchor == anchor && self.head == head {
94− return self;
95− }
121+ use std::cmp::Ordering;
122+ let (anchor, head) = match self.anchor.cmp(&self.head) {
123+ Ordering::Equal => (
124+ changes.map_pos(self.anchor, Assoc::After),
125+ changes.map_pos(self.head, Assoc::After),
126+ ),
127+ Ordering::Less => (
128+ changes.map_pos(self.anchor, Assoc::After),
129+ changes.map_pos(self.head, Assoc::Before),
130+ ),
131+ Ordering::Greater => (
132+ changes.map_pos(self.anchor, Assoc::Before),
133+ changes.map_pos(self.head, Assoc::After),
134+ ),
135+ };
136+
137+ // We want to return a new `Range` with `horiz == None` every time,
138+ // even if the anchor and head haven't changed, because we don't
139+ // know if the *visual* position hasn't changed due to
140+ // character-width or grapheme changes earlier in the text.
96141 Self {
97142 anchor,
98143 head,
@@ -103,22 +148,41 @@ impl Range {
103148 /// Extend the range to cover at least `from` `to`.
104149 #[must_use]
105150 pub fn extend(&self, from: usize, to: usize) -> Self {
106− if from <= self.anchor && to >= self.anchor {
107− return Self {
108− anchor: from,
109− head: to,
151+ debug_assert!(from <= to);
152+
153+ if self.anchor <= self.head {
154+ Self {
155+ anchor: self.anchor.min(from),
156+ head: self.head.max(to),
110157 horiz: None,
111− };
158+ }
159+ } else {
160+ Self {
161+ anchor: self.anchor.max(to),
162+ head: self.head.min(from),
163+ horiz: None,
164+ }
112165 }
166+ }
113167
114− Self {
115− anchor: self.anchor,
116− head: if abs_difference(from, self.anchor) > abs_difference(to, self.anchor) {
117− from
118− } else {
119− to
120− },
121− horiz: None,
168+ /// Returns a range that encompasses both input ranges.
169+ ///
170+ /// This is like `extend()`, but tries to negotiate the
171+ /// anchor/head ordering between the two input ranges.
172+ #[must_use]
173+ pub fn merge(&self, other: Self) -> Self {
174+ if self.anchor > self.head && other.anchor > other.head {
175+ Range {
176+ anchor: self.anchor.max(other.anchor),
177+ head: self.head.min(other.head),
178+ horiz: None,
179+ }
180+ } else {
181+ Range {
182+ anchor: self.from().min(other.from()),
183+ head: self.to().max(other.to()),
184+ horiz: None,
185+ }
122186 }
123187 }
124188
@@ -126,7 +190,120 @@ impl Range {
126190
127191 #[inline]
128192 pub fn fragment<'a, 'b: 'a>(&'a self, text: RopeSlice<'b>) -> Cow<'b, str> {
129− Cow::from(text.slice(self.from()..self.to() + 1))
193+ text.slice(self.from()..self.to()).into()
194+ }
195+
196+ //--------------------------------
197+ // Alignment methods.
198+
199+ /// Compute a possibly new range from this range, with its ends
200+ /// shifted as needed to align with grapheme boundaries.
201+ ///
202+ /// Zero-width ranges will always stay zero-width, and non-zero-width
203+ /// ranges will never collapse to zero-width.
204+ #[must_use]
205+ pub fn grapheme_aligned(&self, slice: RopeSlice) -> Self {
206+ use std::cmp::Ordering;
207+ let (new_anchor, new_head) = match self.anchor.cmp(&self.head) {
208+ Ordering::Equal => {
209+ let pos = ensure_grapheme_boundary_prev(slice, self.anchor);
210+ (pos, pos)
211+ }
212+ Ordering::Less => (
213+ ensure_grapheme_boundary_prev(slice, self.anchor),
214+ ensure_grapheme_boundary_next(slice, self.head),
215+ ),
216+ Ordering::Greater => (
217+ ensure_grapheme_boundary_next(slice, self.anchor),
218+ ensure_grapheme_boundary_prev(slice, self.head),
219+ ),
220+ };
221+ Range {
222+ anchor: new_anchor,
223+ head: new_head,
224+ horiz: if new_anchor == self.anchor {
225+ self.horiz
226+ } else {
227+ None
228+ },
229+ }
230+ }
231+
232+ /// Compute a possibly new range from this range, attempting to ensure
233+ /// a minimum range width of 1 char by shifting the head in the forward
234+ /// direction as needed.
235+ ///
236+ /// This method will never shift the anchor, and will only shift the
237+ /// head in the forward direction. Therefore, this method can fail
238+ /// at ensuring the minimum width if and only if the passed range is
239+ /// both zero-width and at the end of the `RopeSlice`.
240+ ///
241+ /// If the input range is grapheme-boundary aligned, the returned range
242+ /// will also be. Specifically, if the head needs to shift to achieve
243+ /// the minimum width, it will shift to the next grapheme boundary.
244+ #[must_use]
245+ #[inline]
246+ pub fn min_width_1(&self, slice: RopeSlice) -> Self {
247+ if self.anchor == self.head {
248+ Range {
249+ anchor: self.anchor,
250+ head: next_grapheme_boundary(slice, self.head),
251+ horiz: self.horiz,
252+ }
253+ } else {
254+ *self
255+ }
256+ }
257+
258+ //--------------------------------
259+ // Block-cursor methods.
260+
261+ /// Gets the left-side position of the block cursor.
262+ #[must_use]
263+ #[inline]
264+ pub fn cursor(self, text: RopeSlice) -> usize {
265+ if self.head > self.anchor {
266+ prev_grapheme_boundary(text, self.head)
267+ } else {
268+ self.head
269+ }
270+ }
271+
272+ /// Puts the left side of the block cursor at `char_idx`, optionally extending.
273+ ///
274+ /// This follows "1-width" semantics, and therefore does a combination of anchor
275+ /// and head moves to behave as if both the front and back of the range are 1-width
276+ /// blocks
277+ ///
278+ /// This method assumes that the range and `char_idx` are already properly
279+ /// grapheme-aligned.
280+ #[must_use]
281+ #[inline]
282+ pub fn put_cursor(self, text: RopeSlice, char_idx: usize, extend: bool) -> Range {
283+ if extend {
284+ let anchor = if self.head >= self.anchor && char_idx < self.anchor {
285+ next_grapheme_boundary(text, self.anchor)
286+ } else if self.head < self.anchor && char_idx >= self.anchor {
287+ prev_grapheme_boundary(text, self.anchor)
288+ } else {
289+ self.anchor
290+ };
291+
292+ if anchor <= char_idx {
293+ Range::new(anchor, next_grapheme_boundary(text, char_idx))
294+ } else {
295+ Range::new(anchor, char_idx)
296+ }
297+ } else {
298+ Range::point(char_idx)
299+ }
300+ }
301+
302+ /// The line number that the block-cursor is on.
303+ #[inline]
304+ #[must_use]
305+ pub fn cursor_line(&self, text: RopeSlice) -> usize {
306+ text.char_to_line(self.cursor(text))
130307 }
131308}
132309
@@ -157,11 +334,6 @@ impl Selection {
157334 self.ranges[self.primary_index]
158335 }
159336
160− #[must_use]
161− pub fn cursor(&self) -> usize {
162− self.primary().head
163− }
164−
165337 /// Ensure selection containing only the primary selection.
166338 pub fn into_single(self) -> Self {
167339 if self.ranges.len() == 1 {
@@ -174,13 +346,12 @@ impl Selection {
174346 }
175347 }
176348
349+ /// Adds a new range to the selection and makes it the primary range.
177350 pub fn push(mut self, range: Range) -> Self {
178− let index = self.ranges.len();
179351 self.ranges.push(range);
180−
181− Self::normalize(self.ranges, index)
352+ self.set_primary_index(self.ranges().len() - 1);
353+ self.normalize()
182354 }
183− // replace_range
184355
185356 /// Map selections over a set of changes. Useful for adjusting the selection position after
186357 /// applying changes to a document.
@@ -206,6 +377,11 @@ impl Selection {
206377 self.primary_index
207378 }
208379
380+ pub fn set_primary_index(&mut self, idx: usize) {
381+ assert!(idx < self.ranges.len());
382+ self.primary_index = idx;
383+ }
384+
209385 #[must_use]
210386 /// Constructs a selection holding a single range.
211387 pub fn single(anchor: usize, head: usize) -> Self {
@@ -224,80 +400,79 @@ impl Selection {
224400 Self::single(pos, pos)
225401 }
226402
227− fn normalize(mut ranges: SmallVec<[Range; 1]>, mut primary_index: usize) -> Self {
228− let primary = ranges[primary_index];
229− ranges.sort_unstable_by_key(Range::from);
230− primary_index = ranges.iter().position(|&range| range == primary).unwrap();
231−
232− let mut result = SmallVec::with_capacity(ranges.len()); // approx
233−
234− // TODO: we could do with one vec by removing elements as we mutate
235−
236− let mut i = 0;
237−
238− for range in ranges.into_iter() {
239− // if previous value exists
240− if let Some(prev) = result.last_mut() {
241− // and we overlap it
242−
243− // TODO: we used to simply check range.from() <(=) prev.to()
244− // avoiding two comparisons
245− if range.overlaps(prev) {
246− let from = prev.from();
247− let to = std::cmp::max(range.to(), prev.to());
248−
249− if i <= primary_index {
250− primary_index -= 1
251− }
252−
253− // merge into previous
254− if range.anchor > range.head {
255− prev.anchor = to;
256− prev.head = from;
257− } else {
258− prev.anchor = from;
259− prev.head = to;
260− }
261− continue;
262− }
403+ /// Normalizes a `Selection`.
404+ fn normalize(mut self) -> Self {
405+ let primary = self.ranges[self.primary_index];
406+ self.ranges.sort_unstable_by_key(Range::from);
407+ self.primary_index = self
408+ .ranges
409+ .iter()
410+ .position(|&range| range == primary)
411+ .unwrap();
412+
413+ let mut prev_i = 0;
414+ for i in 1..self.ranges.len() {
415+ if self.ranges[prev_i].overlaps(&self.ranges[i]) {
416+ self.ranges[prev_i] = self.ranges[prev_i].merge(self.ranges[i]);
417+ } else {
418+ prev_i += 1;
419+ self.ranges[prev_i] = self.ranges[i];
420+ }
421+ if i == self.primary_index {
422+ self.primary_index = prev_i;
263423 }
264−
265− result.push(range);
266− i += 1
267424 }
268425
269− Self {
270− ranges: result,
271− primary_index,
272− }
426+ self.ranges.truncate(prev_i + 1);
427+
428+ self
273429 }
274430
275431 // TODO: consume an iterator or a vec to reduce allocations?
276432 #[must_use]
277433 pub fn new(ranges: SmallVec<[Range; 1]>, primary_index: usize) -> Self {
278434 assert!(!ranges.is_empty());
435+ debug_assert!(primary_index < ranges.len());
279436
280− // fast path for a single selection (cursor)
281− if ranges.len() == 1 {
282− return Self {
283− ranges,
284− primary_index: 0,
285− };
437+ let mut selection = Self {
438+ ranges,
439+ primary_index,
440+ };
441+
442+ if selection.ranges.len() > 1 {
443+ // TODO: only normalize if needed (any ranges out of order)
444+ selection = selection.normalize();
286445 }
287446
288− // TODO: only normalize if needed (any ranges out of order)
289− Self::normalize(ranges, primary_index)
447+ selection
290448 }
291449
292− /// Takes a closure and maps each selection over the closure.
293− pub fn transform<F>(&self, f: F) -> Self
450+ /// Takes a closure and maps each `Range` over the closure.
451+ pub fn transform<F>(mut self, f: F) -> Self
294452 where
295453 F: Fn(Range) -> Range,
296454 {
297− Self::new(
298− self.ranges.iter().copied().map(f).collect(),
299− self.primary_index,
300− )
455+ for range in self.ranges.iter_mut() {
456+ *range = f(*range)
457+ }
458+ self.normalize()
459+ }
460+
461+ // Ensures the selection adheres to the following invariants:
462+ // 1. All ranges are grapheme aligned.
463+ // 2. All ranges are at least 1 character wide, unless at the
464+ // very end of the document.
465+ // 3. Ranges are non-overlapping.
466+ // 4. Ranges are sorted by their position in the text.
467+ pub fn ensure_invariants(self, text: RopeSlice) -> Self {
468+ self.transform(|r| r.min_width_1(text).grapheme_aligned(text))
469+ .normalize()
470+ }
471+
472+ /// Transforms the selection into all of the left-side head positions,
473+ /// using block-cursor semantics.
474+ pub fn cursors(self, text: RopeSlice) -> Self {
475+ self.transform(|range| Range::point(range.cursor(text)))
301476 }
302477
303478 pub fn fragments<'a>(&'a self, text: RopeSlice<'a>) -> impl Iterator<Item = Cow<str>> + 'a {
@@ -363,7 +538,7 @@ pub fn select_on_matches(
363538
364539 let start = text.byte_to_char(start_byte + mat.start());
365540 let end = text.byte_to_char(start_byte + mat.end());
366− result.push(Range::new(start, end.saturating_sub(1)));
541+ result.push(Range::new(start, end));
367542 }
368543 }
369544
@@ -384,6 +559,12 @@ pub fn split_on_matches(
384559 let mut result = SmallVec::with_capacity(selection.len());
385560
386561 for sel in selection {
562+ // Special case: zero-width selection.
563+ if sel.from() == sel.to() {
564+ result.push(*sel);
565+ continue;
566+ }
567+
387568 // TODO: can't avoid occasional allocations since Regex can't operate on chunks yet
388569 let fragment = sel.fragment(text);
389570
@@ -396,13 +577,12 @@ pub fn split_on_matches(
396577
397578 for mat in regex.find_iter(&fragment) {
398579 // TODO: retain range direction
399−
400580 let end = text.byte_to_char(start_byte + mat.start());
401− result.push(Range::new(start, end.saturating_sub(1)));
581+ result.push(Range::new(start, end));
402582 start = text.byte_to_char(start_byte + mat.end());
403583 }
404584
405− if start <= sel_end {
585+ if start < sel_end {
406586 result.push(Range::new(start, sel_end));
407587 }
408588 }
@@ -484,7 +664,7 @@ mod test {
484664 .collect::<Vec<String>>()
485665 .join(",");
486666
487− assert_eq!(res, "8/10,10/12");
667+ assert_eq!(res, "8/10,10/12,12/12");
488668 }
489669
490670 #[test]
@@ -498,35 +678,251 @@ mod test {
498678 assert_eq!(range.contains(13), false);
499679
500680 let range = Range::new(9, 6);
501− assert_eq!(range.contains(9), true);
681+ assert_eq!(range.contains(9), false);
502682 assert_eq!(range.contains(7), true);
503− assert_eq!(range.contains(6), false);
683+ assert_eq!(range.contains(6), true);
684+ }
685+
686+ #[test]
687+ fn test_overlaps() {
688+ fn overlaps(a: (usize, usize), b: (usize, usize)) -> bool {
689+ Range::new(a.0, a.1).overlaps(&Range::new(b.0, b.1))
690+ }
691+
692+ // Two non-zero-width ranges, no overlap.
693+ assert!(!overlaps((0, 3), (3, 6)));
694+ assert!(!overlaps((0, 3), (6, 3)));
695+ assert!(!overlaps((3, 0), (3, 6)));
696+ assert!(!overlaps((3, 0), (6, 3)));
697+ assert!(!overlaps((3, 6), (0, 3)));
698+ assert!(!overlaps((3, 6), (3, 0)));
699+ assert!(!overlaps((6, 3), (0, 3)));
700+ assert!(!overlaps((6, 3), (3, 0)));
701+
702+ // Two non-zero-width ranges, overlap.
703+ assert!(overlaps((0, 4), (3, 6)));
704+ assert!(overlaps((0, 4), (6, 3)));
705+ assert!(overlaps((4, 0), (3, 6)));
706+ assert!(overlaps((4, 0), (6, 3)));
707+ assert!(overlaps((3, 6), (0, 4)));
708+ assert!(overlaps((3, 6), (4, 0)));
709+ assert!(overlaps((6, 3), (0, 4)));
710+ assert!(overlaps((6, 3), (4, 0)));
711+
712+ // Zero-width and non-zero-width range, no overlap.
713+ assert!(!overlaps((0, 3), (3, 3)));
714+ assert!(!overlaps((3, 0), (3, 3)));
715+ assert!(!overlaps((3, 3), (0, 3)));
716+ assert!(!overlaps((3, 3), (3, 0)));
717+
718+ // Zero-width and non-zero-width range, overlap.
719+ assert!(overlaps((1, 4), (1, 1)));
720+ assert!(overlaps((4, 1), (1, 1)));
721+ assert!(overlaps((1, 1), (1, 4)));
722+ assert!(overlaps((1, 1), (4, 1)));
723+
724+ assert!(overlaps((1, 4), (3, 3)));
725+ assert!(overlaps((4, 1), (3, 3)));
726+ assert!(overlaps((3, 3), (1, 4)));
727+ assert!(overlaps((3, 3), (4, 1)));
728+
729+ // Two zero-width ranges, no overlap.
730+ assert!(!overlaps((0, 0), (1, 1)));
731+ assert!(!overlaps((1, 1), (0, 0)));
732+
733+ // Two zero-width ranges, overlap.
734+ assert!(overlaps((1, 1), (1, 1)));
735+ }
736+
737+ #[test]
738+ fn test_graphem_aligned() {
739+ let r = Rope::from_str("\r\nHi\r\n");
740+ let s = r.slice(..);
741+
742+ // Zero-width.
743+ assert_eq!(Range::new(0, 0).grapheme_aligned(s), Range::new(0, 0));
744+ assert_eq!(Range::new(1, 1).grapheme_aligned(s), Range::new(0, 0));
745+ assert_eq!(Range::new(2, 2).grapheme_aligned(s), Range::new(2, 2));
746+ assert_eq!(Range::new(3, 3).grapheme_aligned(s), Range::new(3, 3));
747+ assert_eq!(Range::new(4, 4).grapheme_aligned(s), Range::new(4, 4));
748+ assert_eq!(Range::new(5, 5).grapheme_aligned(s), Range::new(4, 4));
749+ assert_eq!(Range::new(6, 6).grapheme_aligned(s), Range::new(6, 6));
750+
751+ // Forward.
752+ assert_eq!(Range::new(0, 1).grapheme_aligned(s), Range::new(0, 2));
753+ assert_eq!(Range::new(1, 2).grapheme_aligned(s), Range::new(0, 2));
754+ assert_eq!(Range::new(2, 3).grapheme_aligned(s), Range::new(2, 3));
755+ assert_eq!(Range::new(3, 4).grapheme_aligned(s), Range::new(3, 4));
756+ assert_eq!(Range::new(4, 5).grapheme_aligned(s), Range::new(4, 6));
757+ assert_eq!(Range::new(5, 6).grapheme_aligned(s), Range::new(4, 6));
758+
759+ assert_eq!(Range::new(0, 2).grapheme_aligned(s), Range::new(0, 2));
760+ assert_eq!(Range::new(1, 3).grapheme_aligned(s), Range::new(0, 3));
761+ assert_eq!(Range::new(2, 4).grapheme_aligned(s), Range::new(2, 4));
762+ assert_eq!(Range::new(3, 5).grapheme_aligned(s), Range::new(3, 6));
763+ assert_eq!(Range::new(4, 6).grapheme_aligned(s), Range::new(4, 6));
764+
765+ // Reverse.
766+ assert_eq!(Range::new(1, 0).grapheme_aligned(s), Range::new(2, 0));
767+ assert_eq!(Range::new(2, 1).grapheme_aligned(s), Range::new(2, 0));
768+ assert_eq!(Range::new(3, 2).grapheme_aligned(s), Range::new(3, 2));
769+ assert_eq!(Range::new(4, 3).grapheme_aligned(s), Range::new(4, 3));
770+ assert_eq!(Range::new(5, 4).grapheme_aligned(s), Range::new(6, 4));
771+ assert_eq!(Range::new(6, 5).grapheme_aligned(s), Range::new(6, 4));
772+
773+ assert_eq!(Range::new(2, 0).grapheme_aligned(s), Range::new(2, 0));
774+ assert_eq!(Range::new(3, 1).grapheme_aligned(s), Range::new(3, 0));
775+ assert_eq!(Range::new(4, 2).grapheme_aligned(s), Range::new(4, 2));
776+ assert_eq!(Range::new(5, 3).grapheme_aligned(s), Range::new(6, 3));
777+ assert_eq!(Range::new(6, 4).grapheme_aligned(s), Range::new(6, 4));
778+ }
779+
780+ #[test]
781+ fn test_min_width_1() {
782+ let r = Rope::from_str("\r\nHi\r\n");
783+ let s = r.slice(..);
784+
785+ // Zero-width.
786+ assert_eq!(Range::new(0, 0).min_width_1(s), Range::new(0, 2));
787+ assert_eq!(Range::new(1, 1).min_width_1(s), Range::new(1, 2));
788+ assert_eq!(Range::new(2, 2).min_width_1(s), Range::new(2, 3));
789+ assert_eq!(Range::new(3, 3).min_width_1(s), Range::new(3, 4));
790+ assert_eq!(Range::new(4, 4).min_width_1(s), Range::new(4, 6));
791+ assert_eq!(Range::new(5, 5).min_width_1(s), Range::new(5, 6));
792+ assert_eq!(Range::new(6, 6).min_width_1(s), Range::new(6, 6));
793+
794+ // Forward.
795+ assert_eq!(Range::new(0, 1).min_width_1(s), Range::new(0, 1));
796+ assert_eq!(Range::new(1, 2).min_width_1(s), Range::new(1, 2));
797+ assert_eq!(Range::new(2, 3).min_width_1(s), Range::new(2, 3));
798+ assert_eq!(Range::new(3, 4).min_width_1(s), Range::new(3, 4));
799+ assert_eq!(Range::new(4, 5).min_width_1(s), Range::new(4, 5));
800+ assert_eq!(Range::new(5, 6).min_width_1(s), Range::new(5, 6));
801+
802+ // Reverse.
803+ assert_eq!(Range::new(1, 0).min_width_1(s), Range::new(1, 0));
804+ assert_eq!(Range::new(2, 1).min_width_1(s), Range::new(2, 1));
805+ assert_eq!(Range::new(3, 2).min_width_1(s), Range::new(3, 2));
806+ assert_eq!(Range::new(4, 3).min_width_1(s), Range::new(4, 3));
807+ assert_eq!(Range::new(5, 4).min_width_1(s), Range::new(5, 4));
808+ assert_eq!(Range::new(6, 5).min_width_1(s), Range::new(6, 5));
809+ }
810+
811+ #[test]
812+ fn test_line_range() {
813+ let r = Rope::from_str("\r\nHi\r\nthere!");
814+ let s = r.slice(..);
815+
816+ // Zero-width ranges.
817+ assert_eq!(Range::new(0, 0).line_range(s), (0, 0));
818+ assert_eq!(Range::new(1, 1).line_range(s), (0, 0));
819+ assert_eq!(Range::new(2, 2).line_range(s), (1, 1));
820+ assert_eq!(Range::new(3, 3).line_range(s), (1, 1));
821+
822+ // Forward ranges.
823+ assert_eq!(Range::new(0, 1).line_range(s), (0, 0));
824+ assert_eq!(Range::new(0, 2).line_range(s), (0, 0));
825+ assert_eq!(Range::new(0, 3).line_range(s), (0, 1));
826+ assert_eq!(Range::new(1, 2).line_range(s), (0, 0));
827+ assert_eq!(Range::new(2, 3).line_range(s), (1, 1));
828+ assert_eq!(Range::new(3, 8).line_range(s), (1, 2));
829+ assert_eq!(Range::new(0, 12).line_range(s), (0, 2));
830+
831+ // Reverse ranges.
832+ assert_eq!(Range::new(1, 0).line_range(s), (0, 0));
833+ assert_eq!(Range::new(2, 0).line_range(s), (0, 0));
834+ assert_eq!(Range::new(3, 0).line_range(s), (0, 1));
835+ assert_eq!(Range::new(2, 1).line_range(s), (0, 0));
836+ assert_eq!(Range::new(3, 2).line_range(s), (1, 1));
837+ assert_eq!(Range::new(8, 3).line_range(s), (1, 2));
838+ assert_eq!(Range::new(12, 0).line_range(s), (0, 2));
839+ }
840+
841+ #[test]
842+ fn test_cursor() {
843+ let r = Rope::from_str("\r\nHi\r\nthere!");
844+ let s = r.slice(..);
845+
846+ // Zero-width ranges.
847+ assert_eq!(Range::new(0, 0).cursor(s), 0);
848+ assert_eq!(Range::new(2, 2).cursor(s), 2);
849+ assert_eq!(Range::new(3, 3).cursor(s), 3);
850+
851+ // Forward ranges.
852+ assert_eq!(Range::new(0, 2).cursor(s), 0);
853+ assert_eq!(Range::new(0, 3).cursor(s), 2);
854+ assert_eq!(Range::new(3, 6).cursor(s), 4);
855+
856+ // Reverse ranges.
857+ assert_eq!(Range::new(2, 0).cursor(s), 0);
858+ assert_eq!(Range::new(6, 2).cursor(s), 2);
859+ assert_eq!(Range::new(6, 3).cursor(s), 3);
860+ }
861+
862+ #[test]
863+ fn test_put_cursor() {
864+ let r = Rope::from_str("\r\nHi\r\nthere!");
865+ let s = r.slice(..);
866+
867+ // Zero-width ranges.
868+ assert_eq!(Range::new(0, 0).put_cursor(s, 0, true), Range::new(0, 2));
869+ assert_eq!(Range::new(0, 0).put_cursor(s, 2, true), Range::new(0, 3));
870+ assert_eq!(Range::new(2, 3).put_cursor(s, 4, true), Range::new(2, 6));
871+ assert_eq!(Range::new(2, 8).put_cursor(s, 4, true), Range::new(2, 6));
872+ assert_eq!(Range::new(8, 8).put_cursor(s, 4, true), Range::new(9, 4));
873+
874+ // Forward ranges.
875+ assert_eq!(Range::new(3, 6).put_cursor(s, 0, true), Range::new(4, 0));
876+ assert_eq!(Range::new(3, 6).put_cursor(s, 2, true), Range::new(4, 2));
877+ assert_eq!(Range::new(3, 6).put_cursor(s, 3, true), Range::new(3, 4));
878+ assert_eq!(Range::new(3, 6).put_cursor(s, 4, true), Range::new(3, 6));
879+ assert_eq!(Range::new(3, 6).put_cursor(s, 6, true), Range::new(3, 7));
880+ assert_eq!(Range::new(3, 6).put_cursor(s, 8, true), Range::new(3, 9));
881+
882+ // Reverse ranges.
883+ assert_eq!(Range::new(6, 3).put_cursor(s, 0, true), Range::new(6, 0));
884+ assert_eq!(Range::new(6, 3).put_cursor(s, 2, true), Range::new(6, 2));
885+ assert_eq!(Range::new(6, 3).put_cursor(s, 3, true), Range::new(6, 3));
886+ assert_eq!(Range::new(6, 3).put_cursor(s, 4, true), Range::new(6, 4));
887+ assert_eq!(Range::new(6, 3).put_cursor(s, 6, true), Range::new(4, 7));
888+ assert_eq!(Range::new(6, 3).put_cursor(s, 8, true), Range::new(4, 9));
504889 }
505890
506891 #[test]
507892 fn test_split_on_matches() {
508893 use crate::regex::Regex;
509894
510− let text = Rope::from("abcd efg wrs xyz 123 456");
895+ let text = Rope::from(" abcd efg wrs xyz 123 456");
511896
512− let selection = Selection::new(smallvec![Range::new(0, 8), Range::new(10, 19),], 0);
897+ let selection = Selection::new(smallvec![Range::new(0, 9), Range::new(11, 20),], 0);
513898
514899 let result = split_on_matches(text.slice(..), &selection, &Regex::new(r"\s+").unwrap());
515900
516901 assert_eq!(
517902 result.ranges(),
518903 &[
519− Range::new(0, 3),
520− Range::new(5, 7),
521− Range::new(10, 11),
522− Range::new(15, 17),
523− Range::new(19, 19),
904+ // TODO: rather than this behavior, maybe we want it
905+ // to be based on which side is the anchor?
906+ //
907+ // We get a leading zero-width range when there's
908+ // a leading match because ranges are inclusive on
909+ // the left. Imagine, for example, if the entire
910+ // selection range were matched: you'd still want
911+ // at least one range to remain after the split.
912+ Range::new(0, 0),
913+ Range::new(1, 5),
914+ Range::new(6, 9),
915+ Range::new(11, 13),
916+ Range::new(16, 19),
917+ // In contrast to the comment above, there is no
918+ // _trailing_ zero-width range despite the trailing
919+ // match, because ranges are exclusive on the right.
524920 ]
525921 );
526922
527923 assert_eq!(
528924 result.fragments(text.slice(..)).collect::<Vec<_>>(),
529− &["abcd", "efg", "rs", "xyz", "1"]
925+ &["", "abcd", "efg", "rs", "xyz"]
530926 );
531927 }
532928}
helix-core/src/surround.rs+44−31
@@ -1,3 +1,4 @@
1+use crate::graphemes::next_grapheme_boundary;
12use crate::{search, Selection};
23use ropey::RopeSlice;
34
@@ -40,23 +41,34 @@ pub fn find_nth_pairs_pos(
4041) -> Option<(usize, usize)> {
4142 let (open, close) = get_pair(ch);
4243
43− let (open_pos, close_pos) = if open == close {
44− let prev = search::find_nth_prev(text, open, pos, n, true);
45− let next = search::find_nth_next(text, close, pos, n, true);
46− if text.char(pos) == open {
47− // cursor is *on* a pair
48− next.map(|n| (pos, n)).or_else(|| prev.map(|p| (p, pos)))?
44+ if text.len_chars() < 2 || pos >= text.len_chars() {
45+ return None;
46+ }
47+
48+ if open == close {
49+ if Some(open) == text.get_char(pos) {
50+ // Special case: cursor is directly on a matching char.
51+ match pos {
52+ 0 => Some((pos, search::find_nth_next(text, close, pos + 1, n)? + 1)),
53+ _ if (pos + 1) == text.len_chars() => {
54+ Some((search::find_nth_prev(text, open, pos, n)?, text.len_chars()))
55+ }
56+ // We return no match because there's no way to know which
57+ // side of the char we should be searching on.
58+ _ => None,
59+ }
4960 } else {
50− (prev?, next?)
61+ Some((
62+ search::find_nth_prev(text, open, pos, n)?,
63+ search::find_nth_next(text, close, pos, n)? + 1,
64+ ))
5165 }
5266 } else {
53− (
67+ Some((
5468 find_nth_open_pair(text, open, close, pos, n)?,
55− find_nth_close_pair(text, open, close, pos, n)?,
56− )
57− };
58−
59− Some((open_pos, close_pos))
69+ next_grapheme_boundary(text, find_nth_close_pair(text, open, close, pos, n)?),
70+ ))
71+ }
6072}
6173
6274fn find_nth_open_pair(
@@ -173,12 +185,13 @@ mod test {
173185 let slice = doc.slice(..);
174186
175187 // cursor on [t]ext
176− assert_eq!(find_nth_pairs_pos(slice, '(', 6, 1), Some((5, 10)));
177− assert_eq!(find_nth_pairs_pos(slice, ')', 6, 1), Some((5, 10)));
188+ assert_eq!(find_nth_pairs_pos(slice, '(', 6, 1), Some((5, 11)));
189+ assert_eq!(find_nth_pairs_pos(slice, ')', 6, 1), Some((5, 11)));
178190 // cursor on so[m]e
179191 assert_eq!(find_nth_pairs_pos(slice, '(', 2, 1), None);
180192 // cursor on bracket itself
181− assert_eq!(find_nth_pairs_pos(slice, '(', 5, 1), Some((5, 10)));
193+ assert_eq!(find_nth_pairs_pos(slice, '(', 5, 1), Some((5, 11)));
194+ assert_eq!(find_nth_pairs_pos(slice, '(', 10, 1), Some((5, 11)));
182195 }
183196
184197 #[test]
@@ -187,9 +200,9 @@ mod test {
187200 let slice = doc.slice(..);
188201
189202 // cursor on go[o]d
190− assert_eq!(find_nth_pairs_pos(slice, '(', 13, 1), Some((10, 15)));
191− assert_eq!(find_nth_pairs_pos(slice, '(', 13, 2), Some((4, 21)));
192− assert_eq!(find_nth_pairs_pos(slice, '(', 13, 3), Some((0, 27)));
203+ assert_eq!(find_nth_pairs_pos(slice, '(', 13, 1), Some((10, 16)));
204+ assert_eq!(find_nth_pairs_pos(slice, '(', 13, 2), Some((4, 22)));
205+ assert_eq!(find_nth_pairs_pos(slice, '(', 13, 3), Some((0, 28)));
193206 }
194207
195208 #[test]
@@ -198,14 +211,14 @@ mod test {
198211 let slice = doc.slice(..);
199212
200213 // cursor on go[o]d
201− assert_eq!(find_nth_pairs_pos(slice, '\'', 13, 1), Some((10, 15)));
202− assert_eq!(find_nth_pairs_pos(slice, '\'', 13, 2), Some((4, 21)));
203− assert_eq!(find_nth_pairs_pos(slice, '\'', 13, 3), Some((0, 27)));
214+ assert_eq!(find_nth_pairs_pos(slice, '\'', 13, 1), Some((10, 16)));
215+ assert_eq!(find_nth_pairs_pos(slice, '\'', 13, 2), Some((4, 22)));
216+ assert_eq!(find_nth_pairs_pos(slice, '\'', 13, 3), Some((0, 28)));
204217 // cursor on the quotes
205− assert_eq!(find_nth_pairs_pos(slice, '\'', 10, 1), Some((10, 15)));
218+ assert_eq!(find_nth_pairs_pos(slice, '\'', 10, 1), None);
206219 // this is the best we can do since opening and closing pairs are same
207− assert_eq!(find_nth_pairs_pos(slice, '\'', 0, 1), Some((0, 4)));
208− assert_eq!(find_nth_pairs_pos(slice, '\'', 27, 1), Some((21, 27)));
220+ assert_eq!(find_nth_pairs_pos(slice, '\'', 0, 1), Some((0, 5)));
221+ assert_eq!(find_nth_pairs_pos(slice, '\'', 27, 1), Some((21, 28)));
209222 }
210223
211224 #[test]
@@ -214,8 +227,8 @@ mod test {
214227 let slice = doc.slice(..);
215228
216229 // cursor on go[o]d
217− assert_eq!(find_nth_pairs_pos(slice, '(', 15, 1), Some((5, 24)));
218− assert_eq!(find_nth_pairs_pos(slice, '(', 15, 2), Some((0, 31)));
230+ assert_eq!(find_nth_pairs_pos(slice, '(', 15, 1), Some((5, 25)));
231+ assert_eq!(find_nth_pairs_pos(slice, '(', 15, 2), Some((0, 32)));
219232 }
220233
221234 #[test]
@@ -224,9 +237,9 @@ mod test {
224237 let slice = doc.slice(..);
225238
226239 // cursor on go[o]d
227− assert_eq!(find_nth_pairs_pos(slice, '{', 13, 1), Some((10, 15)));
228− assert_eq!(find_nth_pairs_pos(slice, '[', 13, 1), Some((4, 21)));
229− assert_eq!(find_nth_pairs_pos(slice, '(', 13, 1), Some((0, 27)));
240+ assert_eq!(find_nth_pairs_pos(slice, '{', 13, 1), Some((10, 16)));
241+ assert_eq!(find_nth_pairs_pos(slice, '[', 13, 1), Some((4, 22)));
242+ assert_eq!(find_nth_pairs_pos(slice, '(', 13, 1), Some((0, 28)));
230243 }
231244
232245 #[test]
@@ -243,7 +256,7 @@ mod test {
243256 get_surround_pos(slice, &selection, '(', 1)
244257 .unwrap()
245258 .as_slice(),
246− &[0, 5, 7, 13, 15, 23]
259+ &[0, 6, 7, 14, 15, 24]
247260 );
248261 }
249262
helix-core/src/syntax.rs+14−4
@@ -1760,10 +1760,20 @@ impl<I: Iterator<Item = HighlightEvent>> Iterator for Merge<I> {
17601760 self.next_event = self.iter.next();
17611761 Some(event)
17621762 }
1763− // can happen if deleting and cursor at EOF, and diagnostic reaches past the end
1764− (None, Some((_, _))) => {
1765− self.next_span = None;
1766− None
1763+ // Can happen if cursor at EOF and/or diagnostic reaches past the end.
1764+ // We need to actually emit events for the cursor-at-EOF situation,
1765+ // even though the range is past the end of the text. This needs to be
1766+ // handled appropriately by the drawing code by not assuming that
1767+ // all `Source` events point to valid indices in the rope.
1768+ (None, Some((span, range))) => {
1769+ let event = HighlightStart(Highlight(*span));
1770+ self.queue.push(HighlightEnd);
1771+ self.queue.push(Source {
1772+ start: range.start,
1773+ end: range.end,
1774+ });
1775+ self.next_span = self.spans.next();
1776+ Some(event)
17671777 }
17681778 (None, None) => None,
17691779 e => unreachable!("{:?}", e),
helix-core/src/textobject.rs+128−133
@@ -1,21 +1,16 @@
11use ropey::RopeSlice;
22
3−use crate::chars::{categorize_char, char_is_line_ending, char_is_whitespace, CharCategory};
4−use crate::movement::{self, Direction};
3+use crate::chars::{categorize_char, CharCategory};
4+use crate::graphemes::{next_grapheme_boundary, prev_grapheme_boundary};
5+use crate::movement::Direction;
56use crate::surround;
67use crate::Range;
78
8−fn this_word_end_pos(slice: RopeSlice, pos: usize) -> usize {
9− this_word_bound_pos(slice, pos, Direction::Forward)
10−}
11−
12−fn this_word_start_pos(slice: RopeSlice, pos: usize) -> usize {
13− this_word_bound_pos(slice, pos, Direction::Backward)
14−}
9+fn find_word_boundary(slice: RopeSlice, mut pos: usize, direction: Direction) -> usize {
10+ use CharCategory::{Eol, Whitespace};
1511
16−fn this_word_bound_pos(slice: RopeSlice, mut pos: usize, direction: Direction) -> usize {
1712 let iter = match direction {
18− Direction::Forward => slice.chars_at(pos + 1),
13+ Direction::Forward => slice.chars_at(pos),
1914 Direction::Backward => {
2015 let mut iter = slice.chars_at(pos);
2116 iter.reverse();
@@ -23,25 +18,31 @@ fn this_word_bound_pos(slice: RopeSlice, mut pos: usize, direction: Direction) -
2318 }
2419 };
2520
26− match categorize_char(slice.char(pos)) {
27− CharCategory::Eol | CharCategory::Whitespace => pos,
28− category => {
29− for peek in iter {
30− let curr_category = categorize_char(peek);
31− if curr_category != category
32− || curr_category == CharCategory::Eol
33− || curr_category == CharCategory::Whitespace
34− {
21+ let mut prev_category = match direction {
22+ Direction::Forward if pos == 0 => Whitespace,
23+ Direction::Forward => categorize_char(slice.char(pos - 1)),
24+ Direction::Backward if pos == slice.len_chars() => Whitespace,
25+ Direction::Backward => categorize_char(slice.char(pos)),
26+ };
27+
28+ for ch in iter {
29+ match categorize_char(ch) {
30+ Eol | Whitespace => return pos,
31+ category => {
32+ if category != prev_category && pos != 0 && pos != slice.len_chars() {
3533 return pos;
36− }
37− pos = match direction {
38− Direction::Forward => pos + 1,
39− Direction::Backward => pos.saturating_sub(1),
34+ } else {
35+ match direction {
36+ Direction::Forward => pos += 1,
37+ Direction::Backward => pos = pos.saturating_sub(1),
38+ }
39+ prev_category = category;
4040 }
4141 }
42− pos
4342 }
4443 }
44+
45+ pos
4546}
4647
4748#[derive(Copy, Clone, PartialEq, Eq, Debug)]
@@ -55,46 +56,37 @@ pub fn textobject_word(
5556 slice: RopeSlice,
5657 range: Range,
5758 textobject: TextObject,
58− count: usize,
59+ _count: usize,
5960) -> Range {
60− let this_word_start = this_word_start_pos(slice, range.head);
61− let this_word_end = this_word_end_pos(slice, range.head);
61+ let pos = range.cursor(slice);
62+
63+ let word_start = find_word_boundary(slice, pos, Direction::Backward);
64+ let word_end = match slice.get_char(pos).map(categorize_char) {
65+ None | Some(CharCategory::Whitespace | CharCategory::Eol) => pos,
66+ _ => find_word_boundary(slice, pos + 1, Direction::Forward),
67+ };
68+
69+ // Special case.
70+ if word_start == word_end {
71+ return Range::new(word_start, word_end);
72+ }
6273
63− let (anchor, head);
6474 match textobject {
65− TextObject::Inside => {
66− anchor = this_word_start;
67− head = this_word_end;
68− }
69− TextObject::Around => {
70− if slice
71− .get_char(this_word_end + 1)
72− .map_or(true, char_is_line_ending)
75+ TextObject::Inside => Range::new(word_start, word_end),
76+ TextObject::Around => Range::new(
77+ match slice
78+ .get_char(word_start.saturating_sub(1))
79+ .map(categorize_char)
7380 {
74− head = this_word_end;
75− if slice
76− .get_char(this_word_start.saturating_sub(1))
77− .map_or(true, char_is_line_ending)
78− {
79− // single word on a line
80− anchor = this_word_start;
81− } else {
82− // last word on a line, select the whitespace before it too
83− anchor = movement::move_prev_word_end(slice, range, count).head;
84− }
85− } else if char_is_whitespace(slice.char(range.head)) {
86− // select whole whitespace and next word
87− head = movement::move_next_word_end(slice, range, count).head;
88− anchor = movement::backwards_skip_while(slice, range.head, |c| c.is_whitespace())
89− .map(|p| p + 1) // p is first *non* whitespace char, so +1 to get whitespace pos
90− .unwrap_or(0);
91− } else {
92− head = movement::move_next_word_start(slice, range, count).head;
93− anchor = this_word_start;
94− }
95− }
96− };
97− Range::new(anchor, head)
81+ None | Some(CharCategory::Eol) => word_start,
82+ _ => prev_grapheme_boundary(slice, word_start),
83+ },
84+ match slice.get_char(word_end).map(categorize_char) {
85+ None | Some(CharCategory::Eol) => word_end,
86+ _ => next_grapheme_boundary(slice, word_end),
87+ },
88+ ),
89+ }
9890}
9991
10092pub fn textobject_surround(
@@ -106,7 +98,10 @@ pub fn textobject_surround(
10698) -> Range {
10799 surround::find_nth_pairs_pos(slice, ch, range.head, count)
108100 .map(|(anchor, head)| match textobject {
109− TextObject::Inside => Range::new(anchor + 1, head.saturating_sub(1)),
101+ TextObject::Inside => Range::new(
102+ next_grapheme_boundary(slice, anchor),
103+ prev_grapheme_boundary(slice, head),
104+ ),
110105 TextObject::Around => Range::new(anchor, head),
111106 })
112107 .unwrap_or(range)
@@ -126,70 +121,70 @@ mod test {
126121 let tests = &[
127122 (
128123 "cursor at beginning of doc",
129− vec![(0, Inside, (0, 5)), (0, Around, (0, 6))],
124+ vec![(0, Inside, (0, 6)), (0, Around, (0, 7))],
130125 ),
131126 (
132127 "cursor at middle of word",
133128 vec![
134− (13, Inside, (10, 15)),
135− (10, Inside, (10, 15)),
136− (15, Inside, (10, 15)),
137− (13, Around, (10, 16)),
138− (10, Around, (10, 16)),
139− (15, Around, (10, 16)),
129+ (13, Inside, (10, 16)),
130+ (10, Inside, (10, 16)),
131+ (15, Inside, (10, 16)),
132+ (13, Around, (9, 17)),
133+ (10, Around, (9, 17)),
134+ (15, Around, (9, 17)),
140135 ],
141136 ),
142137 (
143138 "cursor between word whitespace",
144− vec![(6, Inside, (6, 6)), (6, Around, (6, 13))],
139+ vec![(6, Inside, (6, 6)), (6, Around, (6, 6))],
145140 ),
146141 (
147142 "cursor on word before newline\n",
148143 vec![
149− (22, Inside, (22, 28)),
150− (28, Inside, (22, 28)),
151− (25, Inside, (22, 28)),
152− (22, Around, (21, 28)),
153− (28, Around, (21, 28)),
154− (25, Around, (21, 28)),
144+ (22, Inside, (22, 29)),
145+ (28, Inside, (22, 29)),
146+ (25, Inside, (22, 29)),
147+ (22, Around, (21, 29)),
148+ (28, Around, (21, 29)),
149+ (25, Around, (21, 29)),
155150 ],
156151 ),
157152 (
158153 "cursor on newline\nnext line",
159− vec![(17, Inside, (17, 17)), (17, Around, (17, 22))],
154+ vec![(17, Inside, (17, 17)), (17, Around, (17, 17))],
160155 ),
161156 (
162157 "cursor on word after newline\nnext line",
163158 vec![
164− (29, Inside, (29, 32)),
165− (30, Inside, (29, 32)),
166− (32, Inside, (29, 32)),
167− (29, Around, (29, 33)),
168− (30, Around, (29, 33)),
169− (32, Around, (29, 33)),
159+ (29, Inside, (29, 33)),
160+ (30, Inside, (29, 33)),
161+ (32, Inside, (29, 33)),
162+ (29, Around, (29, 34)),
163+ (30, Around, (29, 34)),
164+ (32, Around, (29, 34)),
170165 ],
171166 ),
172167 (
173168 "cursor on #$%:;* punctuation",
174169 vec![
175− (13, Inside, (10, 15)),
176− (10, Inside, (10, 15)),
177− (15, Inside, (10, 15)),
178− (13, Around, (10, 16)),
179− (10, Around, (10, 16)),
180− (15, Around, (10, 16)),
170+ (13, Inside, (10, 16)),
171+ (10, Inside, (10, 16)),
172+ (15, Inside, (10, 16)),
173+ (13, Around, (9, 17)),
174+ (10, Around, (9, 17)),
175+ (15, Around, (9, 17)),
181176 ],
182177 ),
183178 (
184179 "cursor on punc%^#$:;.tuation",
185180 vec![
186− (14, Inside, (14, 20)),
187− (20, Inside, (14, 20)),
188− (17, Inside, (14, 20)),
189− (14, Around, (14, 20)),
181+ (14, Inside, (14, 21)),
182+ (20, Inside, (14, 21)),
183+ (17, Inside, (14, 21)),
184+ (14, Around, (13, 22)),
190185 // FIXME: edge case
191186 // (20, Around, (14, 20)),
192− (17, Around, (14, 20)),
187+ (17, Around, (13, 22)),
193188 ],
194189 ),
195190 (
@@ -198,14 +193,14 @@ mod test {
198193 (9, Inside, (9, 9)),
199194 (10, Inside, (10, 10)),
200195 (11, Inside, (11, 11)),
201− (9, Around, (9, 16)),
202− (10, Around, (9, 16)),
203− (11, Around, (9, 16)),
196+ (9, Around, (9, 9)),
197+ (10, Around, (10, 10)),
198+ (11, Around, (11, 11)),
204199 ],
205200 ),
206201 (
207202 "cursor at end of doc",
208− vec![(19, Inside, (17, 19)), (19, Around, (16, 19))],
203+ vec![(19, Inside, (17, 20)), (19, Around, (16, 20))],
209204 ),
210205 ];
211206
@@ -234,67 +229,67 @@ mod test {
234229 "simple (single) surround pairs",
235230 vec![
236231 (3, Inside, (3, 3), '(', 1),
237− (7, Inside, (8, 13), ')', 1),
238− (10, Inside, (8, 13), '(', 1),
239− (14, Inside, (8, 13), ')', 1),
232+ (7, Inside, (8, 14), ')', 1),
233+ (10, Inside, (8, 14), '(', 1),
234+ (14, Inside, (8, 14), ')', 1),
240235 (3, Around, (3, 3), '(', 1),
241− (7, Around, (7, 14), ')', 1),
242− (10, Around, (7, 14), '(', 1),
243− (14, Around, (7, 14), ')', 1),
236+ (7, Around, (7, 15), ')', 1),
237+ (10, Around, (7, 15), '(', 1),
238+ (14, Around, (7, 15), ')', 1),
244239 ],
245240 ),
246241 (
247242 "samexx 'single' surround pairs",
248243 vec![
249244 (3, Inside, (3, 3), '\'', 1),
250− (7, Inside, (8, 13), '\'', 1),
251− (10, Inside, (8, 13), '\'', 1),
252− (14, Inside, (8, 13), '\'', 1),
245+ (7, Inside, (7, 7), '\'', 1),
246+ (10, Inside, (8, 14), '\'', 1),
247+ (14, Inside, (14, 14), '\'', 1),
253248 (3, Around, (3, 3), '\'', 1),
254− (7, Around, (7, 14), '\'', 1),
255− (10, Around, (7, 14), '\'', 1),
256− (14, Around, (7, 14), '\'', 1),
249+ (7, Around, (7, 7), '\'', 1),
250+ (10, Around, (7, 15), '\'', 1),
251+ (14, Around, (14, 14), '\'', 1),
257252 ],
258253 ),
259254 (
260255 "(nested (surround (pairs)) 3 levels)",
261256 vec![
262− (0, Inside, (1, 34), '(', 1),
263− (6, Inside, (1, 34), ')', 1),
264− (8, Inside, (9, 24), '(', 1),
265− (8, Inside, (9, 34), ')', 2),
266− (20, Inside, (9, 24), '(', 2),
267− (20, Inside, (1, 34), ')', 3),
268− (0, Around, (0, 35), '(', 1),
269− (6, Around, (0, 35), ')', 1),
270− (8, Around, (8, 25), '(', 1),
271− (8, Around, (8, 35), ')', 2),
272− (20, Around, (8, 25), '(', 2),
273− (20, Around, (0, 35), ')', 3),
257+ (0, Inside, (1, 35), '(', 1),
258+ (6, Inside, (1, 35), ')', 1),
259+ (8, Inside, (9, 25), '(', 1),
260+ (8, Inside, (9, 35), ')', 2),
261+ (20, Inside, (9, 25), '(', 2),
262+ (20, Inside, (1, 35), ')', 3),
263+ (0, Around, (0, 36), '(', 1),
264+ (6, Around, (0, 36), ')', 1),
265+ (8, Around, (8, 26), '(', 1),
266+ (8, Around, (8, 36), ')', 2),
267+ (20, Around, (8, 26), '(', 2),
268+ (20, Around, (0, 36), ')', 3),
274269 ],
275270 ),
276271 (
277272 "(mixed {surround [pair] same} line)",
278273 vec![
279− (2, Inside, (1, 33), '(', 1),
280− (9, Inside, (8, 27), '{', 1),
281− (18, Inside, (18, 21), '[', 1),
282− (2, Around, (0, 34), '(', 1),
283− (9, Around, (7, 28), '{', 1),
284− (18, Around, (17, 22), '[', 1),
274+ (2, Inside, (1, 34), '(', 1),
275+ (9, Inside, (8, 28), '{', 1),
276+ (18, Inside, (18, 22), '[', 1),
277+ (2, Around, (0, 35), '(', 1),
278+ (9, Around, (7, 29), '{', 1),
279+ (18, Around, (17, 23), '[', 1),
285280 ],
286281 ),
287282 (
288283 "(stepped (surround) pairs (should) skip)",
289− vec![(22, Inside, (1, 38), '(', 1), (22, Around, (0, 39), '(', 1)],
284+ vec![(22, Inside, (1, 39), '(', 1), (22, Around, (0, 40), '(', 1)],
290285 ),
291286 (
292287 "[surround pairs{\non different]\nlines}",
293288 vec![
294− (7, Inside, (1, 28), '[', 1),
295− (15, Inside, (16, 35), '{', 1),
296− (7, Around, (0, 29), '[', 1),
297− (15, Around, (15, 36), '{', 1),
289+ (7, Inside, (1, 29), '[', 1),
290+ (15, Inside, (16, 36), '{', 1),
291+ (7, Around, (0, 30), '[', 1),
292+ (15, Around, (15, 37), '{', 1),
298293 ],
299294 ),
300295 ];
helix-term/src/commands.rs+356−265
@@ -1,9 +1,6 @@
11use helix_core::{
22 comment, coords_at_pos, find_first_non_whitespace_char, find_root, graphemes, indent,
3− line_ending::{
4− get_line_ending_of_str, line_end_char_index, rope_end_without_line_ending,
5− str_is_line_ending,
6− },
3+ line_ending::{get_line_ending_of_str, line_end_char_index, str_is_line_ending},
74 match_brackets,
85 movement::{self, Direction},
96 object, pos_at_coords,
@@ -110,7 +107,10 @@ enum Align {
110107}
111108
112109fn align_view(doc: &Document, view: &mut View, align: Align) {
113− let pos = doc.selection(view.id).cursor();
110+ let pos = doc
111+ .selection(view.id)
112+ .primary()
113+ .cursor(doc.text().slice(..));
114114 let line = doc.text().char_to_line(pos);
115115
116116 let relative = match align {
@@ -340,7 +340,8 @@ fn move_char_left(cx: &mut Context) {
340340 let count = cx.count();
341341 let (view, doc) = current!(cx.editor);
342342 let text = doc.text().slice(..);
343− let selection = doc.selection(view.id).transform(|range| {
343+
344+ let selection = doc.selection(view.id).clone().transform(|range| {
344345 movement::move_horizontally(text, range, Direction::Backward, count, Movement::Move)
345346 });
346347 doc.set_selection(view.id, selection);
@@ -350,7 +351,8 @@ fn move_char_right(cx: &mut Context) {
350351 let count = cx.count();
351352 let (view, doc) = current!(cx.editor);
352353 let text = doc.text().slice(..);
353− let selection = doc.selection(view.id).transform(|range| {
354+
355+ let selection = doc.selection(view.id).clone().transform(|range| {
354356 movement::move_horizontally(text, range, Direction::Forward, count, Movement::Move)
355357 });
356358 doc.set_selection(view.id, selection);
@@ -360,7 +362,8 @@ fn move_line_up(cx: &mut Context) {
360362 let count = cx.count();
361363 let (view, doc) = current!(cx.editor);
362364 let text = doc.text().slice(..);
363− let selection = doc.selection(view.id).transform(|range| {
365+
366+ let selection = doc.selection(view.id).clone().transform(|range| {
364367 movement::move_vertically(text, range, Direction::Backward, count, Movement::Move)
365368 });
366369 doc.set_selection(view.id, selection);
@@ -370,7 +373,8 @@ fn move_line_down(cx: &mut Context) {
370373 let count = cx.count();
371374 let (view, doc) = current!(cx.editor);
372375 let text = doc.text().slice(..);
373− let selection = doc.selection(view.id).transform(|range| {
376+
377+ let selection = doc.selection(view.id).clone().transform(|range| {
374378 movement::move_vertically(text, range, Direction::Forward, count, Movement::Move)
375379 });
376380 doc.set_selection(view.id, selection);
@@ -378,84 +382,61 @@ fn move_line_down(cx: &mut Context) {
378382
379383fn goto_line_end(cx: &mut Context) {
380384 let (view, doc) = current!(cx.editor);
385+ let text = doc.text().slice(..);
381386
382− let selection = doc.selection(view.id).transform(|range| {
383− let text = doc.text();
384− let line = text.char_to_line(range.head);
387+ let selection = doc.selection(view.id).clone().transform(|range| {
388+ let line = range.cursor_line(text);
389+ let line_start = text.line_to_char(line);
385390
386− let pos = line_end_char_index(&text.slice(..), line);
387− let pos = graphemes::nth_prev_grapheme_boundary(text.slice(..), pos, 1);
388− let pos = range.head.max(pos).max(text.line_to_char(line));
391+ let pos = graphemes::prev_grapheme_boundary(text, line_end_char_index(&text, line))
392+ .max(line_start);
389393
390− Range::new(
391− match doc.mode {
392− Mode::Normal | Mode::Insert => pos,
393− Mode::Select => range.anchor,
394− },
395− pos,
396− )
394+ range.put_cursor(text, pos, doc.mode == Mode::Select)
397395 });
398−
399396 doc.set_selection(view.id, selection);
400397}
401398
402399fn goto_line_end_newline(cx: &mut Context) {
403400 let (view, doc) = current!(cx.editor);
401+ let text = doc.text().slice(..);
404402
405− let selection = doc.selection(view.id).transform(|range| {
406− let text = doc.text();
407− let line = text.char_to_line(range.head);
403+ let selection = doc.selection(view.id).clone().transform(|range| {
404+ let line = range.cursor_line(text);
405+ let pos = line_end_char_index(&text, line);
408406
409− let pos = line_end_char_index(&text.slice(..), line);
410− Range::new(pos, pos)
407+ range.put_cursor(text, pos, doc.mode == Mode::Select)
411408 });
412−
413409 doc.set_selection(view.id, selection);
414410}
415411
416412fn goto_line_start(cx: &mut Context) {
417413 let (view, doc) = current!(cx.editor);
414+ let text = doc.text().slice(..);
418415
419− let selection = doc.selection(view.id).transform(|range| {
420− let text = doc.text();
421− let line = text.char_to_line(range.head);
416+ let selection = doc.selection(view.id).clone().transform(|range| {
417+ let line = range.cursor_line(text);
422418
423419 // adjust to start of the line
424420 let pos = text.line_to_char(line);
425− Range::new(
426− match doc.mode {
427− Mode::Normal => range.anchor,
428− Mode::Select | Mode::Insert => pos,
429− },
430− pos,
431− )
421+ range.put_cursor(text, pos, doc.mode == Mode::Select)
432422 });
433−
434423 doc.set_selection(view.id, selection);
435424}
436425
437426fn goto_first_nonwhitespace(cx: &mut Context) {
438427 let (view, doc) = current!(cx.editor);
428+ let text = doc.text().slice(..);
439429
440− let selection = doc.selection(view.id).transform(|range| {
441− let text = doc.text();
442− let line_idx = text.char_to_line(range.head);
430+ let selection = doc.selection(view.id).clone().transform(|range| {
431+ let line = range.cursor_line(text);
443432
444− if let Some(pos) = find_first_non_whitespace_char(text.line(line_idx)) {
445− let pos = pos + text.line_to_char(line_idx);
446− Range::new(
447− match doc.mode {
448− Mode::Normal => pos,
449− Mode::Select => range.anchor,
450− Mode::Insert => unreachable!(),
451− },
452− pos,
453− )
433+ if let Some(pos) = find_first_non_whitespace_char(text.line(line)) {
434+ let pos = pos + text.line_to_char(line);
435+ range.put_cursor(text, pos, doc.mode == Mode::Select)
454436 } else {
455437 range
456438 }
457439 });
458−
459440 doc.set_selection(view.id, selection);
460441}
461442
@@ -501,8 +482,8 @@ fn move_next_word_start(cx: &mut Context) {
501482
502483 let selection = doc
503484 .selection(view.id)
485+ .clone()
504486 .transform(|range| movement::move_next_word_start(text, range, count));
505−
506487 doc.set_selection(view.id, selection);
507488}
508489
@@ -513,8 +494,8 @@ fn move_prev_word_start(cx: &mut Context) {
513494
514495 let selection = doc
515496 .selection(view.id)
497+ .clone()
516498 .transform(|range| movement::move_prev_word_start(text, range, count));
517−
518499 doc.set_selection(view.id, selection);
519500}
520501
@@ -525,8 +506,8 @@ fn move_next_word_end(cx: &mut Context) {
525506
526507 let selection = doc
527508 .selection(view.id)
509+ .clone()
528510 .transform(|range| movement::move_next_word_end(text, range, count));
529−
530511 doc.set_selection(view.id, selection);
531512}
532513
@@ -537,8 +518,8 @@ fn move_next_long_word_start(cx: &mut Context) {
537518
538519 let selection = doc
539520 .selection(view.id)
521+ .clone()
540522 .transform(|range| movement::move_next_long_word_start(text, range, count));
541−
542523 doc.set_selection(view.id, selection);
543524}
544525
@@ -549,8 +530,8 @@ fn move_prev_long_word_start(cx: &mut Context) {
549530
550531 let selection = doc
551532 .selection(view.id)
533+ .clone()
552534 .transform(|range| movement::move_prev_long_word_start(text, range, count));
553−
554535 doc.set_selection(view.id, selection);
555536}
556537
@@ -561,8 +542,8 @@ fn move_next_long_word_end(cx: &mut Context) {
561542
562543 let selection = doc
563544 .selection(view.id)
545+ .clone()
564546 .transform(|range| movement::move_next_long_word_end(text, range, count));
565−
566547 doc.set_selection(view.id, selection);
567548}
568549
@@ -579,9 +560,7 @@ fn goto_file_start(cx: &mut Context) {
579560fn goto_file_end(cx: &mut Context) {
580561 push_jump(cx.editor);
581562 let (view, doc) = current!(cx.editor);
582− let text = doc.text();
583− let last_line = text.line_to_char(text.len_lines().saturating_sub(2));
584− doc.set_selection(view.id, Selection::point(last_line));
563+ doc.set_selection(view.id, Selection::point(doc.text().len_chars()));
585564}
586565
587566fn extend_next_word_start(cx: &mut Context) {
@@ -589,12 +568,11 @@ fn extend_next_word_start(cx: &mut Context) {
589568 let (view, doc) = current!(cx.editor);
590569 let text = doc.text().slice(..);
591570
592− let selection = doc.selection(view.id).transform(|range| {
571+ let selection = doc.selection(view.id).clone().transform(|range| {
593572 let word = movement::move_next_word_start(text, range, count);
594− let pos = word.head;
595− Range::new(range.anchor, pos)
573+ let pos = word.cursor(text);
574+ range.put_cursor(text, pos, true)
596575 });
597−
598576 doc.set_selection(view.id, selection);
599577}
600578
@@ -603,10 +581,10 @@ fn extend_prev_word_start(cx: &mut Context) {
603581 let (view, doc) = current!(cx.editor);
604582 let text = doc.text().slice(..);
605583
606− let selection = doc.selection(view.id).transform(|range| {
584+ let selection = doc.selection(view.id).clone().transform(|range| {
607585 let word = movement::move_prev_word_start(text, range, count);
608− let pos = word.head;
609− Range::new(range.anchor, pos)
586+ let pos = word.cursor(text);
587+ range.put_cursor(text, pos, true)
610588 });
611589 doc.set_selection(view.id, selection);
612590}
@@ -616,12 +594,11 @@ fn extend_next_word_end(cx: &mut Context) {
616594 let (view, doc) = current!(cx.editor);
617595 let text = doc.text().slice(..);
618596
619− let selection = doc.selection(view.id).transform(|range| {
597+ let selection = doc.selection(view.id).clone().transform(|range| {
620598 let word = movement::move_next_word_end(text, range, count);
621− let pos = word.head;
622− Range::new(range.anchor, pos)
599+ let pos = word.cursor(text);
600+ range.put_cursor(text, pos, true)
623601 });
624−
625602 doc.set_selection(view.id, selection);
626603}
627604
@@ -667,26 +644,56 @@ where
667644 let (view, doc) = current!(cx.editor);
668645 let text = doc.text().slice(..);
669646
670− let selection = doc.selection(view.id).transform(|range| {
671− search_fn(text, ch, range.head, count, inclusive).map_or(range, |pos| {
672− if extend {
673− Range::new(range.anchor, pos)
674− } else {
675− // select
676− Range::new(range.head, pos)
677− }
678− // or (pos, pos) to move to found val
679− })
680− });
647+ let selection = doc.selection(view.id).clone().transform(|range| {
648+ // TODO: use `Range::cursor()` here instead. However, that works in terms of
649+ // graphemes, whereas this function doesn't yet. So we're doing the same logic
650+ // here, but just in terms of chars instead.
651+ let search_start_pos = if range.anchor < range.head {
652+ range.head - 1
653+ } else {
654+ range.head
655+ };
681656
657+ search_fn(text, ch, search_start_pos, count, inclusive)
658+ .map_or(range, |pos| range.put_cursor(text, pos, extend))
659+ });
682660 doc.set_selection(view.id, selection);
683661 })
684662}
685663
664+fn find_next_char_impl(
665+ text: RopeSlice,
666+ ch: char,
667+ pos: usize,
668+ n: usize,
669+ inclusive: bool,
670+) -> Option<usize> {
671+ let pos = (pos + 1).min(text.len_chars());
672+ if inclusive {
673+ search::find_nth_next(text, ch, pos, n)
674+ } else {
675+ search::find_nth_next(text, ch, pos, n).map(|n| n.saturating_sub(1))
676+ }
677+}
678+
679+fn find_prev_char_impl(
680+ text: RopeSlice,
681+ ch: char,
682+ pos: usize,
683+ n: usize,
684+ inclusive: bool,
685+) -> Option<usize> {
686+ if inclusive {
687+ search::find_nth_prev(text, ch, pos, n)
688+ } else {
689+ search::find_nth_prev(text, ch, pos, n).map(|n| (n + 1).min(text.len_chars()))
690+ }
691+}
692+
686693fn find_till_char(cx: &mut Context) {
687694 find_char_impl(
688695 cx,
689− search::find_nth_next,
696+ find_next_char_impl,
690697 false, /* inclusive */
691698 false, /* extend */
692699 )
@@ -695,7 +702,7 @@ fn find_till_char(cx: &mut Context) {
695702fn find_next_char(cx: &mut Context) {
696703 find_char_impl(
697704 cx,
698− search::find_nth_next,
705+ find_next_char_impl,
699706 true, /* inclusive */
700707 false, /* extend */
701708 )
@@ -704,7 +711,7 @@ fn find_next_char(cx: &mut Context) {
704711fn extend_till_char(cx: &mut Context) {
705712 find_char_impl(
706713 cx,
707− search::find_nth_next,
714+ find_next_char_impl,
708715 false, /* inclusive */
709716 true, /* extend */
710717 )
@@ -713,7 +720,7 @@ fn extend_till_char(cx: &mut Context) {
713720fn extend_next_char(cx: &mut Context) {
714721 find_char_impl(
715722 cx,
716− search::find_nth_next,
723+ find_next_char_impl,
717724 true, /* inclusive */
718725 true, /* extend */
719726 )
@@ -722,7 +729,7 @@ fn extend_next_char(cx: &mut Context) {
722729fn till_prev_char(cx: &mut Context) {
723730 find_char_impl(
724731 cx,
725− search::find_nth_prev,
732+ find_prev_char_impl,
726733 false, /* inclusive */
727734 false, /* extend */
728735 )
@@ -731,7 +738,7 @@ fn till_prev_char(cx: &mut Context) {
731738fn find_prev_char(cx: &mut Context) {
732739 find_char_impl(
733740 cx,
734− search::find_nth_prev,
741+ find_prev_char_impl,
735742 true, /* inclusive */
736743 false, /* extend */
737744 )
@@ -740,7 +747,7 @@ fn find_prev_char(cx: &mut Context) {
740747fn extend_till_prev_char(cx: &mut Context) {
741748 find_char_impl(
742749 cx,
743− search::find_nth_prev,
750+ find_prev_char_impl,
744751 false, /* inclusive */
745752 true, /* extend */
746753 )
@@ -749,7 +756,7 @@ fn extend_till_prev_char(cx: &mut Context) {
749756fn extend_prev_char(cx: &mut Context) {
750757 find_char_impl(
751758 cx,
752− search::find_nth_prev,
759+ find_prev_char_impl,
753760 true, /* inclusive */
754761 true, /* extend */
755762 )
@@ -773,24 +780,29 @@ fn replace(cx: &mut Context) {
773780 _ => None,
774781 };
775782
776− if let Some(ch) = ch {
777− let transaction =
778− Transaction::change_by_selection(doc.text(), doc.selection(view.id), |range| {
779− let max_to = rope_end_without_line_ending(&doc.text().slice(..));
780− let to = std::cmp::min(max_to, range.to() + 1);
781− let text: String = RopeGraphemes::new(doc.text().slice(range.from()..to))
782− .map(|g| {
783− let cow: Cow<str> = g.into();
784− if str_is_line_ending(&cow) {
785− cow
786− } else {
787− ch.into()
788− }
789− })
790− .collect();
783+ let selection = doc.selection(view.id);
791784
792− (range.from(), to, Some(text.into()))
793− });
785+ if let Some(ch) = ch {
786+ let transaction = Transaction::change_by_selection(doc.text(), selection, |range| {
787+ if !range.is_empty() {
788+ let text: String =
789+ RopeGraphemes::new(doc.text().slice(range.from()..range.to()))
790+ .map(|g| {
791+ let cow: Cow<str> = g.into();
792+ if str_is_line_ending(&cow) {
793+ cow
794+ } else {
795+ ch.into()
796+ }
797+ })
798+ .collect();
799+
800+ (range.from(), range.to(), Some(text.into()))
801+ } else {
802+ // No change.
803+ (range.from(), range.to(), None)
804+ }
805+ });
794806
795807 doc.apply(&transaction, view.id);
796808 doc.append_changes_to_history(view.id);
@@ -800,24 +812,24 @@ fn replace(cx: &mut Context) {
800812
801813fn switch_case(cx: &mut Context) {
802814 let (view, doc) = current!(cx.editor);
803− let transaction =
804− Transaction::change_by_selection(doc.text(), doc.selection(view.id), |range| {
805− let text: Tendril = range
806− .fragment(doc.text().slice(..))
807− .chars()
808− .flat_map(|ch| {
809− if ch.is_lowercase() {
810− ch.to_uppercase().collect()
811− } else if ch.is_uppercase() {
812− ch.to_lowercase().collect()
813− } else {
814− vec![ch]
815− }
816− })
817− .collect();
815+ let selection = doc.selection(view.id);
816+ let transaction = Transaction::change_by_selection(doc.text(), selection, |range| {
817+ let text: Tendril = range
818+ .fragment(doc.text().slice(..))
819+ .chars()
820+ .flat_map(|ch| {
821+ if ch.is_lowercase() {
822+ ch.to_uppercase().collect()
823+ } else if ch.is_uppercase() {
824+ ch.to_lowercase().collect()
825+ } else {
826+ vec![ch]
827+ }
828+ })
829+ .collect();
818830
819− (range.from(), range.to() + 1, Some(text))
820− });
831+ (range.from(), range.to(), Some(text))
832+ });
821833
822834 doc.apply(&transaction, view.id);
823835 doc.append_changes_to_history(view.id);
@@ -825,12 +837,12 @@ fn switch_case(cx: &mut Context) {
825837
826838fn switch_to_uppercase(cx: &mut Context) {
827839 let (view, doc) = current!(cx.editor);
828− let transaction =
829− Transaction::change_by_selection(doc.text(), doc.selection(view.id), |range| {
830− let text: Tendril = range.fragment(doc.text().slice(..)).to_uppercase().into();
840+ let selection = doc.selection(view.id);
841+ let transaction = Transaction::change_by_selection(doc.text(), selection, |range| {
842+ let text: Tendril = range.fragment(doc.text().slice(..)).to_uppercase().into();
831843
832− (range.from(), range.to() + 1, Some(text))
833− });
844+ (range.from(), range.to(), Some(text))
845+ });
834846
835847 doc.apply(&transaction, view.id);
836848 doc.append_changes_to_history(view.id);
@@ -838,12 +850,12 @@ fn switch_to_uppercase(cx: &mut Context) {
838850
839851fn switch_to_lowercase(cx: &mut Context) {
840852 let (view, doc) = current!(cx.editor);
841− let transaction =
842− Transaction::change_by_selection(doc.text(), doc.selection(view.id), |range| {
843− let text: Tendril = range.fragment(doc.text().slice(..)).to_lowercase().into();
853+ let selection = doc.selection(view.id);
854+ let transaction = Transaction::change_by_selection(doc.text(), selection, |range| {
855+ let text: Tendril = range.fragment(doc.text().slice(..)).to_lowercase().into();
844856
845− (range.from(), range.to() + 1, Some(text))
846− });
857+ (range.from(), range.to(), Some(text))
858+ });
847859
848860 doc.apply(&transaction, view.id);
849861 doc.append_changes_to_history(view.id);
@@ -852,7 +864,12 @@ fn switch_to_lowercase(cx: &mut Context) {
852864fn scroll(cx: &mut Context, offset: usize, direction: Direction) {
853865 use Direction::*;
854866 let (view, doc) = current!(cx.editor);
855− let cursor = coords_at_pos(doc.text().slice(..), doc.selection(view.id).cursor());
867+ let cursor = coords_at_pos(
868+ doc.text().slice(..),
869+ doc.selection(view.id)
870+ .primary()
871+ .cursor(doc.text().slice(..)),
872+ );
856873 let doc_last_line = doc.text().len_lines() - 1;
857874
858875 let last_line = view.last_line(doc);
@@ -881,7 +898,7 @@ fn scroll(cx: &mut Context, offset: usize, direction: Direction) {
881898 .min(last_line.saturating_sub(scrolloff));
882899
883900 let text = doc.text().slice(..);
884− let pos = pos_at_coords(text, Position::new(line, cursor.col)); // this func will properly truncate to line end
901+ let pos = pos_at_coords(text, Position::new(line, cursor.col), true); // this func will properly truncate to line end
885902
886903 // TODO: only manipulate main selection
887904 doc.set_selection(view.id, Selection::point(pos));
@@ -915,7 +932,8 @@ fn extend_char_left(cx: &mut Context) {
915932 let count = cx.count();
916933 let (view, doc) = current!(cx.editor);
917934 let text = doc.text().slice(..);
918− let selection = doc.selection(view.id).transform(|range| {
935+
936+ let selection = doc.selection(view.id).clone().transform(|range| {
919937 movement::move_horizontally(text, range, Direction::Backward, count, Movement::Extend)
920938 });
921939 doc.set_selection(view.id, selection);
@@ -925,7 +943,8 @@ fn extend_char_right(cx: &mut Context) {
925943 let count = cx.count();
926944 let (view, doc) = current!(cx.editor);
927945 let text = doc.text().slice(..);
928− let selection = doc.selection(view.id).transform(|range| {
946+
947+ let selection = doc.selection(view.id).clone().transform(|range| {
929948 movement::move_horizontally(text, range, Direction::Forward, count, Movement::Extend)
930949 });
931950 doc.set_selection(view.id, selection);
@@ -935,7 +954,8 @@ fn extend_line_up(cx: &mut Context) {
935954 let count = cx.count();
936955 let (view, doc) = current!(cx.editor);
937956 let text = doc.text().slice(..);
938− let selection = doc.selection(view.id).transform(|range| {
957+
958+ let selection = doc.selection(view.id).clone().transform(|range| {
939959 movement::move_vertically(text, range, Direction::Backward, count, Movement::Extend)
940960 });
941961 doc.set_selection(view.id, selection);
@@ -945,7 +965,8 @@ fn extend_line_down(cx: &mut Context) {
945965 let count = cx.count();
946966 let (view, doc) = current!(cx.editor);
947967 let text = doc.text().slice(..);
948− let selection = doc.selection(view.id).transform(|range| {
968+
969+ let selection = doc.selection(view.id).clone().transform(|range| {
949970 movement::move_vertically(text, range, Direction::Forward, count, Movement::Extend)
950971 });
951972 doc.set_selection(view.id, selection);
@@ -954,7 +975,7 @@ fn extend_line_down(cx: &mut Context) {
954975fn select_all(cx: &mut Context) {
955976 let (view, doc) = current!(cx.editor);
956977
957− let end = rope_end_without_line_ending(&doc.text().slice(..));
978+ let end = doc.text().len_chars();
958979 doc.set_selection(view.id, Selection::single(0, end))
959980}
960981
@@ -992,9 +1013,14 @@ fn split_selection_on_newline(cx: &mut Context) {
9921013}
9931014
9941015fn search_impl(doc: &mut Document, view: &mut View, contents: &str, regex: &Regex, extend: bool) {
995− let text = doc.text();
1016+ let text = doc.text().slice(..);
9961017 let selection = doc.selection(view.id);
997− let start = text.char_to_byte(selection.cursor());
1018+
1019+ // Get the right side of the primary block cursor.
1020+ let start = text.char_to_byte(graphemes::next_grapheme_boundary(
1021+ text,
1022+ selection.primary().cursor(text),
1023+ ));
9981024
9991025 // use find_at to find the next match after the cursor, loop around the end
10001026 // Careful, `Regex` uses `bytes` as offsets, not character indices!
@@ -1011,12 +1037,10 @@ fn search_impl(doc: &mut Document, view: &mut View, contents: &str, regex: &Rege
10111037 return;
10121038 }
10131039
1014− let head = end - 1;
1015−
10161040 let selection = if extend {
1017− selection.clone().push(Range::new(start, head))
1041+ selection.clone().push(Range::new(start, end))
10181042 } else {
1019− Selection::single(start, head)
1043+ Selection::single(start, end)
10201044 };
10211045
10221046 doc.set_selection(view.id, selection);
@@ -1079,16 +1103,15 @@ fn extend_line(cx: &mut Context) {
10791103 let count = cx.count();
10801104 let (view, doc) = current!(cx.editor);
10811105
1082− let pos = doc.selection(view.id).primary();
10831106 let text = doc.text();
1107+ let range = doc.selection(view.id).primary();
10841108
1085− let line_start = text.char_to_line(pos.anchor);
1086− let start = text.line_to_char(line_start);
1087− let line_end = text.char_to_line(pos.head);
1088− let mut end = line_end_char_index(&text.slice(..), line_end + count.saturating_sub(1));
1109+ let (start_line, end_line) = range.line_range(text.slice(..));
1110+ let start = text.line_to_char(start_line);
1111+ let mut end = text.line_to_char((end_line + count).min(text.len_lines()));
10891112
1090− if pos.anchor == start && pos.head == end && line_end < (text.len_lines() - 2) {
1091− end = line_end_char_index(&text.slice(..), line_end + 1);
1113+ if range.from() == start && range.to() == end {
1114+ end = text.line_to_char((end_line + count + 1).min(text.len_lines()));
10921115 }
10931116
10941117 doc.set_selection(view.id, Selection::single(start, end));
@@ -1097,41 +1120,36 @@ fn extend_line(cx: &mut Context) {
10971120fn extend_to_line_bounds(cx: &mut Context) {
10981121 let (view, doc) = current!(cx.editor);
10991122
1100− let text = doc.text();
1101− let selection = doc.selection(view.id).transform(|range| {
1102− let start = text.line_to_char(text.char_to_line(range.from()));
1103− let end = text
1104− .line_to_char(text.char_to_line(range.to()) + 1)
1105− .saturating_sub(1);
1106−
1107− if range.anchor < range.head {
1108− Range::new(start, end)
1109− } else {
1110− Range::new(end, start)
1111− }
1112− });
1123+ doc.set_selection(
1124+ view.id,
1125+ doc.selection(view.id).clone().transform(|range| {
1126+ let text = doc.text();
11131127
1114− doc.set_selection(view.id, selection);
1128+ let (start_line, end_line) = range.line_range(text.slice(..));
1129+ let start = text.line_to_char(start_line);
1130+ let end = text.line_to_char((end_line + 1).min(text.len_lines()));
1131+
1132+ if range.anchor <= range.head {
1133+ Range::new(start, end)
1134+ } else {
1135+ Range::new(end, start)
1136+ }
1137+ }),
1138+ );
11151139}
11161140
11171141fn delete_selection_impl(reg: &mut Register, doc: &mut Document, view_id: ViewId) {
1118− // first yank the selection
1119− let values: Vec<String> = doc
1120− .selection(view_id)
1121− .fragments(doc.text().slice(..))
1122− .map(Cow::into_owned)
1123− .collect();
1142+ let text = doc.text().slice(..);
1143+ let selection = doc.selection(view_id);
11241144
1145+ // first yank the selection
1146+ let values: Vec<String> = selection.fragments(text).map(Cow::into_owned).collect();
11251147 reg.write(values);
11261148
11271149 // then delete
1128− let transaction =
1129− Transaction::change_by_selection(doc.text(), doc.selection(view_id), |range| {
1130− let alltext = doc.text().slice(..);
1131− let max_to = rope_end_without_line_ending(&alltext);
1132− let to = std::cmp::min(max_to, range.to() + 1);
1133− (range.from(), to, None)
1134− });
1150+ let transaction = Transaction::change_by_selection(doc.text(), selection, |range| {
1151+ (range.from(), range.to(), None)
1152+ });
11351153 doc.apply(&transaction, view_id);
11361154}
11371155
@@ -1159,19 +1177,22 @@ fn change_selection(cx: &mut Context) {
11591177
11601178fn collapse_selection(cx: &mut Context) {
11611179 let (view, doc) = current!(cx.editor);
1162− let selection = doc
1163− .selection(view.id)
1164− .transform(|range| Range::new(range.head, range.head));
1180+ let text = doc.text().slice(..);
11651181
1182+ let selection = doc.selection(view.id).clone().transform(|range| {
1183+ let pos = range.cursor(text);
1184+ Range::new(pos, pos)
1185+ });
11661186 doc.set_selection(view.id, selection);
11671187}
11681188
11691189fn flip_selections(cx: &mut Context) {
11701190 let (view, doc) = current!(cx.editor);
1191+
11711192 let selection = doc
11721193 .selection(view.id)
1194+ .clone()
11731195 .transform(|range| Range::new(range.head, range.anchor));
1174−
11751196 doc.set_selection(view.id, selection);
11761197}
11771198
@@ -1186,6 +1207,7 @@ fn insert_mode(cx: &mut Context) {
11861207
11871208 let selection = doc
11881209 .selection(view.id)
1210+ .clone()
11891211 .transform(|range| Range::new(range.to(), range.from()));
11901212 doc.set_selection(view.id, selection);
11911213}
@@ -1195,18 +1217,13 @@ fn append_mode(cx: &mut Context) {
11951217 let (view, doc) = current!(cx.editor);
11961218 enter_insert_mode(doc);
11971219 doc.restore_cursor = true;
1198−
11991220 let text = doc.text().slice(..);
1200− let selection = doc.selection(view.id).transform(|range| {
1201− Range::new(
1202− range.from(),
1203− graphemes::next_grapheme_boundary(text, range.to()), // to() + next char
1204− )
1205− });
12061221
1222+ // Make sure there's room at the end of the document if the last
1223+ // selection butts up against it.
12071224 let end = text.len_chars();
1208−
1209− if selection.iter().any(|range| range.head == end) {
1225+ let last_range = doc.selection(view.id).iter().last().unwrap();
1226+ if !last_range.is_empty() && last_range.head == end {
12101227 let transaction = Transaction::change(
12111228 doc.text(),
12121229 std::array::IntoIter::new([(end, end, Some(doc.line_ending.as_str().into()))]),
@@ -1214,6 +1231,12 @@ fn append_mode(cx: &mut Context) {
12141231 doc.apply(&transaction, view.id);
12151232 }
12161233
1234+ let selection = doc.selection(view.id).clone().transform(|range| {
1235+ Range::new(
1236+ range.from(),
1237+ graphemes::next_grapheme_boundary(doc.text().slice(..), range.to()),
1238+ )
1239+ });
12171240 doc.set_selection(view.id, selection);
12181241}
12191242
@@ -1636,11 +1659,10 @@ mod cmd {
16361659
16371660 match cx.editor.clipboard_provider.get_contents() {
16381661 Ok(contents) => {
1662+ let selection = doc.selection(view.id);
16391663 let transaction =
1640− Transaction::change_by_selection(doc.text(), doc.selection(view.id), |range| {
1641− let max_to = rope_end_without_line_ending(&doc.text().slice(..));
1642− let to = std::cmp::min(max_to, range.to() + 1);
1643− (range.from(), to, Some(contents.as_str().into()))
1664+ Transaction::change_by_selection(doc.text(), &selection, |range| {
1665+ (range.from(), range.to(), Some(contents.as_str().into()))
16441666 });
16451667
16461668 doc.apply(&transaction, view.id);
@@ -2236,10 +2258,10 @@ fn append_to_line(cx: &mut Context) {
22362258 let (view, doc) = current!(cx.editor);
22372259 enter_insert_mode(doc);
22382260
2239− let selection = doc.selection(view.id).transform(|range| {
2240− let text = doc.text();
2241− let line = text.char_to_line(range.head);
2242− let pos = line_end_char_index(&text.slice(..), line);
2261+ let selection = doc.selection(view.id).clone().transform(|range| {
2262+ let text = doc.text().slice(..);
2263+ let line = range.cursor_line(text);
2264+ let pos = line_end_char_index(&text, line);
22432265 Range::new(pos, pos)
22442266 });
22452267 doc.set_selection(view.id, selection);
@@ -2299,7 +2321,7 @@ fn open(cx: &mut Context, open: Open) {
22992321 let mut offs = 0;
23002322
23012323 let mut transaction = Transaction::change_by_selection(contents, selection, |range| {
2302− let line = text.char_to_line(range.head);
2324+ let line = range.cursor_line(text);
23032325
23042326 let line = match open {
23052327 // adjust position to the end of the line (next line - 1)
@@ -2373,7 +2395,7 @@ fn normal_mode(cx: &mut Context) {
23732395 // if leaving append mode, move cursor back by 1
23742396 if doc.restore_cursor {
23752397 let text = doc.text().slice(..);
2376− let selection = doc.selection(view.id).transform(|range| {
2398+ let selection = doc.selection(view.id).clone().transform(|range| {
23772399 Range::new(
23782400 range.from(),
23792401 graphemes::prev_grapheme_boundary(text, range.to()),
@@ -2413,6 +2435,23 @@ fn goto_last_accessed_file(cx: &mut Context) {
24132435}
24142436
24152437fn select_mode(cx: &mut Context) {
2438+ let (view, doc) = current!(cx.editor);
2439+ let text = doc.text().slice(..);
2440+
2441+ // Make sure end-of-document selections are also 1-width.
2442+ // (With the exception of being in an empty document, of course.)
2443+ let selection = doc.selection(view.id).clone().transform(|range| {
2444+ if range.is_empty() && range.head == text.len_chars() {
2445+ Range::new(
2446+ graphemes::prev_grapheme_boundary(text, range.anchor),
2447+ range.head,
2448+ )
2449+ } else {
2450+ range
2451+ }
2452+ });
2453+ doc.set_selection(view.id, selection);
2454+
24162455 doc_mut!(cx.editor).mode = Mode::Select;
24172456}
24182457
@@ -2485,7 +2524,13 @@ fn goto_definition(cx: &mut Context) {
24852524
24862525 let offset_encoding = language_server.offset_encoding();
24872526
2488− let pos = pos_to_lsp_pos(doc.text(), doc.selection(view.id).cursor(), offset_encoding);
2527+ let pos = pos_to_lsp_pos(
2528+ doc.text(),
2529+ doc.selection(view.id)
2530+ .primary()
2531+ .cursor(doc.text().slice(..)),
2532+ offset_encoding,
2533+ );
24892534
24902535 // TODO: handle fails
24912536 let future = language_server.goto_definition(doc.identifier(), pos, None);
@@ -2522,7 +2567,13 @@ fn goto_type_definition(cx: &mut Context) {
25222567
25232568 let offset_encoding = language_server.offset_encoding();
25242569
2525− let pos = pos_to_lsp_pos(doc.text(), doc.selection(view.id).cursor(), offset_encoding);
2570+ let pos = pos_to_lsp_pos(
2571+ doc.text(),
2572+ doc.selection(view.id)
2573+ .primary()
2574+ .cursor(doc.text().slice(..)),
2575+ offset_encoding,
2576+ );
25262577
25272578 // TODO: handle fails
25282579 let future = language_server.goto_type_definition(doc.identifier(), pos, None);
@@ -2559,7 +2610,13 @@ fn goto_implementation(cx: &mut Context) {
25592610
25602611 let offset_encoding = language_server.offset_encoding();
25612612
2562− let pos = pos_to_lsp_pos(doc.text(), doc.selection(view.id).cursor(), offset_encoding);
2613+ let pos = pos_to_lsp_pos(
2614+ doc.text(),
2615+ doc.selection(view.id)
2616+ .primary()
2617+ .cursor(doc.text().slice(..)),
2618+ offset_encoding,
2619+ );
25632620
25642621 // TODO: handle fails
25652622 let future = language_server.goto_implementation(doc.identifier(), pos, None);
@@ -2596,7 +2653,13 @@ fn goto_reference(cx: &mut Context) {
25962653
25972654 let offset_encoding = language_server.offset_encoding();
25982655
2599− let pos = pos_to_lsp_pos(doc.text(), doc.selection(view.id).cursor(), offset_encoding);
2656+ let pos = pos_to_lsp_pos(
2657+ doc.text(),
2658+ doc.selection(view.id)
2659+ .primary()
2660+ .cursor(doc.text().slice(..)),
2661+ offset_encoding,
2662+ );
26002663
26012664 // TODO: handle fails
26022665 let future = language_server.goto_reference(doc.identifier(), pos, None);
@@ -2655,7 +2718,10 @@ fn goto_next_diag(cx: &mut Context) {
26552718 let editor = &mut cx.editor;
26562719 let (view, doc) = current!(editor);
26572720
2658− let cursor_pos = doc.selection(view.id).cursor();
2721+ let cursor_pos = doc
2722+ .selection(view.id)
2723+ .primary()
2724+ .cursor(doc.text().slice(..));
26592725 let diag = if let Some(diag) = doc
26602726 .diagnostics()
26612727 .iter()
@@ -2676,7 +2742,10 @@ fn goto_prev_diag(cx: &mut Context) {
26762742 let editor = &mut cx.editor;
26772743 let (view, doc) = current!(editor);
26782744
2679− let cursor_pos = doc.selection(view.id).cursor();
2745+ let cursor_pos = doc
2746+ .selection(view.id)
2747+ .primary()
2748+ .cursor(doc.text().slice(..));
26802749 let diag = if let Some(diag) = doc
26812750 .diagnostics()
26822751 .iter()
@@ -2704,7 +2773,9 @@ fn signature_help(cx: &mut Context) {
27042773
27052774 let pos = pos_to_lsp_pos(
27062775 doc.text(),
2707− doc.selection(view.id).cursor(),
2776+ doc.selection(view.id)
2777+ .primary()
2778+ .cursor(doc.text().slice(..)),
27082779 language_server.offset_encoding(),
27092780 );
27102781
@@ -2827,11 +2898,11 @@ pub mod insert {
28272898 let (view, doc) = current!(cx.editor);
28282899
28292900 let text = doc.text();
2830− let selection = doc.selection(view.id);
2901+ let selection = doc.selection(view.id).clone().cursors(text.slice(..));
28312902
28322903 // run through insert hooks, stopping on the first one that returns Some(t)
28332904 for hook in HOOKS {
2834− if let Some(transaction) = hook(text, selection, c) {
2905+ if let Some(transaction) = hook(text, &selection, c) {
28352906 doc.apply(&transaction, view.id);
28362907 break;
28372908 }
@@ -2851,7 +2922,11 @@ pub mod insert {
28512922 // indent by one to reach 4 spaces).
28522923
28532924 let indent = Tendril::from(doc.indent_unit());
2854− let transaction = Transaction::insert(doc.text(), doc.selection(view.id), indent);
2925+ let transaction = Transaction::insert(
2926+ doc.text(),
2927+ &doc.selection(view.id).clone().cursors(doc.text().slice(..)),
2928+ indent,
2929+ );
28552930 doc.apply(&transaction, view.id);
28562931 }
28572932
@@ -2860,13 +2935,13 @@ pub mod insert {
28602935 let text = doc.text().slice(..);
28612936
28622937 let contents = doc.text();
2863− let selection = doc.selection(view.id);
2938+ let selection = doc.selection(view.id).clone().cursors(text);
28642939 let mut ranges = SmallVec::with_capacity(selection.len());
28652940
28662941 // TODO: this is annoying, but we need to do it to properly calculate pos after edits
28672942 let mut offs = 0;
28682943
2869− let mut transaction = Transaction::change_by_selection(contents, selection, |range| {
2944+ let mut transaction = Transaction::change_by_selection(contents, &selection, |range| {
28702945 let pos = range.head;
28712946
28722947 let prev = if pos == 0 {
@@ -2929,9 +3004,10 @@ pub mod insert {
29293004 let text = doc.text().slice(..);
29303005 let transaction =
29313006 Transaction::change_by_selection(doc.text(), doc.selection(view.id), |range| {
3007+ let pos = range.cursor(text);
29323008 (
2933− graphemes::nth_prev_grapheme_boundary(text, range.head, count),
2934− range.head,
3009+ graphemes::nth_prev_grapheme_boundary(text, pos, count),
3010+ pos,
29353011 None,
29363012 )
29373013 });
@@ -2944,9 +3020,10 @@ pub mod insert {
29443020 let text = doc.text().slice(..);
29453021 let transaction =
29463022 Transaction::change_by_selection(doc.text(), doc.selection(view.id), |range| {
3023+ let pos = range.cursor(text);
29473024 (
2948− range.head,
2949− graphemes::nth_next_grapheme_boundary(text, range.head, count),
3025+ pos,
3026+ graphemes::nth_next_grapheme_boundary(text, pos, count),
29503027 None,
29513028 )
29523029 });
@@ -2957,8 +3034,10 @@ pub mod insert {
29573034 let count = cx.count();
29583035 let (view, doc) = current!(cx.editor);
29593036 let text = doc.text().slice(..);
3037+
29603038 let selection = doc
29613039 .selection(view.id)
3040+ .clone()
29623041 .transform(|range| movement::move_prev_word_start(text, range, count));
29633042 doc.set_selection(view.id, selection);
29643043 delete_selection(cx)
@@ -2986,9 +3065,11 @@ fn redo(cx: &mut Context) {
29863065
29873066fn yank(cx: &mut Context) {
29883067 let (view, doc) = current!(cx.editor);
3068+ let text = doc.text().slice(..);
3069+
29893070 let values: Vec<String> = doc
29903071 .selection(view.id)
2991− .fragments(doc.text().slice(..))
3072+ .fragments(text)
29923073 .map(Cow::into_owned)
29933074 .collect();
29943075
@@ -3007,10 +3088,11 @@ fn yank(cx: &mut Context) {
30073088
30083089fn yank_joined_to_clipboard_impl(editor: &mut Editor, separator: &str) -> anyhow::Result<()> {
30093090 let (view, doc) = current!(editor);
3091+ let text = doc.text().slice(..);
30103092
30113093 let values: Vec<String> = doc
30123094 .selection(view.id)
3013− .fragments(doc.text().slice(..))
3095+ .fragments(text)
30143096 .map(Cow::into_owned)
30153097 .collect();
30163098
@@ -3038,11 +3120,9 @@ fn yank_joined_to_clipboard(cx: &mut Context) {
30383120
30393121fn yank_main_selection_to_clipboard_impl(editor: &mut Editor) -> anyhow::Result<()> {
30403122 let (view, doc) = current!(editor);
3123+ let text = doc.text().slice(..);
30413124
3042− let value = doc
3043− .selection(view.id)
3044− .primary()
3045− .fragment(doc.text().slice(..));
3125+ let value = doc.selection(view.id).primary().fragment(text);
30463126
30473127 if let Err(e) = editor.clipboard_provider.set_contents(value.into_owned()) {
30483128 bail!("Couldn't set system clipboard content: {:?}", e);
@@ -3083,17 +3163,21 @@ fn paste_impl(
30833163 let mut values = values.iter().cloned().map(Tendril::from).chain(repeat);
30843164
30853165 let text = doc.text();
3166+ let selection = doc.selection(view.id);
30863167
3087− let transaction = Transaction::change_by_selection(text, doc.selection(view.id), |range| {
3168+ let transaction = Transaction::change_by_selection(text, selection, |range| {
30883169 let pos = match (action, linewise) {
30893170 // paste linewise before
30903171 (Paste::Before, true) => text.line_to_char(text.char_to_line(range.from())),
30913172 // paste linewise after
3092− (Paste::After, true) => text.line_to_char(text.char_to_line(range.to()) + 1),
3173+ (Paste::After, true) => {
3174+ let line = range.line_range(text.slice(..)).1;
3175+ text.line_to_char((line + 1).min(text.len_lines()))
3176+ }
30933177 // paste insert
30943178 (Paste::Before, false) => range.from(),
30953179 // paste append
3096− (Paste::After, false) => range.to() + 1,
3180+ (Paste::After, false) => range.to(),
30973181 };
30983182 (pos, pos, Some(values.next().unwrap()))
30993183 });
@@ -3134,12 +3218,14 @@ fn replace_with_yanked(cx: &mut Context) {
31343218
31353219 if let Some(values) = registers.read(reg_name) {
31363220 if let Some(yank) = values.first() {
3137− let transaction =
3138− Transaction::change_by_selection(doc.text(), doc.selection(view.id), |range| {
3139− let max_to = rope_end_without_line_ending(&doc.text().slice(..));
3140− let to = std::cmp::min(max_to, range.to() + 1);
3141− (range.from(), to, Some(yank.as_str().into()))
3142− });
3221+ let selection = doc.selection(view.id);
3222+ let transaction = Transaction::change_by_selection(doc.text(), selection, |range| {
3223+ if !range.is_empty() {
3224+ (range.from(), range.to(), Some(yank.as_str().into()))
3225+ } else {
3226+ (range.from(), range.to(), None)
3227+ }
3228+ });
31433229
31443230 doc.apply(&transaction, view.id);
31453231 doc.append_changes_to_history(view.id);
@@ -3152,12 +3238,10 @@ fn replace_selections_with_clipboard_impl(editor: &mut Editor) -> anyhow::Result
31523238
31533239 match editor.clipboard_provider.get_contents() {
31543240 Ok(contents) => {
3155− let transaction =
3156− Transaction::change_by_selection(doc.text(), doc.selection(view.id), |range| {
3157− let max_to = rope_end_without_line_ending(&doc.text().slice(..));
3158− let to = std::cmp::min(max_to, range.to() + 1);
3159− (range.from(), to, Some(contents.as_str().into()))
3160− });
3241+ let selection = doc.selection(view.id);
3242+ let transaction = Transaction::change_by_selection(doc.text(), selection, |range| {
3243+ (range.from(), range.to(), Some(contents.as_str().into()))
3244+ });
31613245
31623246 doc.apply(&transaction, view.id);
31633247 doc.append_changes_to_history(view.id);
@@ -3204,8 +3288,7 @@ fn get_lines(doc: &Document, view_id: ViewId) -> Vec<usize> {
32043288
32053289 // Get all line numbers
32063290 for range in doc.selection(view_id) {
3207− let start = doc.text().char_to_line(range.from());
3208− let end = doc.text().char_to_line(range.to());
3291+ let (start, end) = range.line_range(doc.text().slice(..));
32093292
32103293 for line in start..=end {
32113294 lines.push(line)
@@ -3333,10 +3416,9 @@ fn join_selections(cx: &mut Context) {
33333416 let fragment = Tendril::from(" ");
33343417
33353418 for selection in doc.selection(view.id) {
3336− let start = text.char_to_line(selection.from());
3337− let mut end = text.char_to_line(selection.to());
3419+ let (start, mut end) = selection.line_range(slice);
33383420 if start == end {
3339− end += 1
3421+ end = (end + 1).min(text.len_lines() - 1);
33403422 }
33413423 let lines = start..end;
33423424
@@ -3433,13 +3515,17 @@ fn completion(cx: &mut Context) {
34333515 };
34343516
34353517 let offset_encoding = language_server.offset_encoding();
3518+ let cursor = doc
3519+ .selection(view.id)
3520+ .primary()
3521+ .cursor(doc.text().slice(..));
34363522
3437− let pos = pos_to_lsp_pos(doc.text(), doc.selection(view.id).cursor(), offset_encoding);
3523+ let pos = pos_to_lsp_pos(doc.text(), cursor, offset_encoding);
34383524
34393525 // TODO: handle fails
34403526 let future = language_server.completion(doc.identifier(), pos, None);
34413527
3442− let trigger_offset = doc.selection(view.id).cursor();
3528+ let trigger_offset = cursor;
34433529
34443530 cx.callback(
34453531 future,
@@ -3489,7 +3575,9 @@ fn hover(cx: &mut Context) {
34893575
34903576 let pos = pos_to_lsp_pos(
34913577 doc.text(),
3492− doc.selection(view.id).cursor(),
3578+ doc.selection(view.id)
3579+ .primary()
3580+ .cursor(doc.text().slice(..)),
34933581 language_server.offset_encoding(),
34943582 );
34953583
@@ -3555,7 +3643,10 @@ fn match_brackets(cx: &mut Context) {
35553643 let (view, doc) = current!(cx.editor);
35563644
35573645 if let Some(syntax) = doc.syntax() {
3558− let pos = doc.selection(view.id).cursor();
3646+ let pos = doc
3647+ .selection(view.id)
3648+ .primary()
3649+ .cursor(doc.text().slice(..));
35593650 if let Some(pos) = match_brackets::find(syntax, doc.text(), pos) {
35603651 let selection = Selection::point(pos);
35613652 doc.set_selection(view.id, selection);
@@ -3655,7 +3746,10 @@ fn align_view_bottom(cx: &mut Context) {
36553746
36563747fn align_view_middle(cx: &mut Context) {
36573748 let (view, doc) = current!(cx.editor);
3658− let pos = doc.selection(view.id).cursor();
3749+ let pos = doc
3750+ .selection(view.id)
3751+ .primary()
3752+ .cursor(doc.text().slice(..));
36593753 let pos = coords_at_pos(doc.text().slice(..), pos);
36603754
36613755 const OFFSET: usize = 7; // gutters
@@ -3687,7 +3781,7 @@ fn select_textobject(cx: &mut Context, objtype: textobject::TextObject) {
36873781 let (view, doc) = current!(cx.editor);
36883782 let text = doc.text().slice(..);
36893783
3690− let selection = doc.selection(view.id).transform(|range| {
3784+ let selection = doc.selection(view.id).clone().transform(|range| {
36913785 match ch {
36923786 'w' => textobject::textobject_word(text, range, objtype, count),
36933787 // TODO: cancel new ranges if inconsistent surround matches across lines
@@ -3697,7 +3791,6 @@ fn select_textobject(cx: &mut Context, objtype: textobject::TextObject) {
36973791 _ => range,
36983792 }
36993793 });
3700−
37013794 doc.set_selection(view.id, selection);
37023795 }
37033796 })
@@ -3707,18 +3800,13 @@ fn surround_add(cx: &mut Context) {
37073800 cx.on_next_key(move |cx, event| {
37083801 if let Some(ch) = event.char() {
37093802 let (view, doc) = current!(cx.editor);
3710− let text = doc.text().slice(..);
37113803 let selection = doc.selection(view.id);
37123804 let (open, close) = surround::get_pair(ch);
37133805
37143806 let mut changes = Vec::new();
37153807 for range in selection.iter() {
3716− let from = range.from();
3717− let max_to = rope_end_without_line_ending(&text);
3718− let to = std::cmp::min(range.to() + 1, max_to);
3719−
3720− changes.push((from, from, Some(Tendril::from_char(open))));
3721− changes.push((to, to, Some(Tendril::from_char(close))));
3808+ changes.push((range.from(), range.from(), Some(Tendril::from_char(open))));
3809+ changes.push((range.to(), range.to(), Some(Tendril::from_char(close))));
37223810 }
37233811
37243812 let transaction = Transaction::change(doc.text(), changes.into_iter());
@@ -3748,8 +3836,11 @@ fn surround_replace(cx: &mut Context) {
37483836 let transaction = Transaction::change(
37493837 doc.text(),
37503838 change_pos.iter().enumerate().map(|(i, &pos)| {
3751− let ch = if i % 2 == 0 { open } else { close };
3752− (pos, pos + 1, Some(Tendril::from_char(ch)))
3839+ if i % 2 == 0 {
3840+ (pos, pos + 1, Some(Tendril::from_char(open)))
3841+ } else {
3842+ (pos.saturating_sub(1), pos, Some(Tendril::from_char(close)))
3843+ }
37533844 }),
37543845 );
37553846 doc.apply(&transaction, view.id);
helix-term/src/ui/completion.rs+16−4
@@ -86,7 +86,10 @@ impl Completion {
8686 let item = item.unwrap();
8787
8888 // if more text was entered, remove it
89− let cursor = doc.selection(view.id).cursor();
89+ let cursor = doc
90+ .selection(view.id)
91+ .primary()
92+ .cursor(doc.text().slice(..));
9093 if trigger_offset < cursor {
9194 let remove = Transaction::change(
9295 doc.text(),
@@ -109,7 +112,10 @@ impl Completion {
109112 )
110113 } else {
111114 let text = item.insert_text.as_ref().unwrap_or(&item.label);
112− let cursor = doc.selection(view.id).cursor();
115+ let cursor = doc
116+ .selection(view.id)
117+ .primary()
118+ .cursor(doc.text().slice(..));
113119 Transaction::change(
114120 doc.text(),
115121 vec![(cursor, cursor, Some(text.as_str().into()))].into_iter(),
@@ -155,7 +161,10 @@ impl Completion {
155161 // TODO: hooks should get processed immediately so maybe do it after select!(), before
156162 // looping?
157163
158− let cursor = doc.selection(view.id).cursor();
164+ let cursor = doc
165+ .selection(view.id)
166+ .primary()
167+ .cursor(doc.text().slice(..));
159168 if self.trigger_offset <= cursor {
160169 let fragment = doc.text().slice(self.trigger_offset..cursor);
161170 let text = Cow::from(fragment);
@@ -212,7 +221,10 @@ impl Component for Completion {
212221 .language()
213222 .and_then(|scope| scope.strip_prefix("source."))
214223 .unwrap_or("");
215− let cursor_pos = doc.selection(view.id).cursor();
224+ let cursor_pos = doc
225+ .selection(view.id)
226+ .primary()
227+ .cursor(doc.text().slice(..));
216228 let cursor_pos = (helix_core::coords_at_pos(doc.text().slice(..), cursor_pos).row
217229 - view.first_line) as u16;
218230
helix-term/src/ui/editor.rs+69−34
@@ -8,7 +8,7 @@ use crate::{
88
99use helix_core::{
1010 coords_at_pos,
11− graphemes::{ensure_grapheme_boundary, next_grapheme_boundary},
11+ graphemes::{ensure_grapheme_boundary_next, next_grapheme_boundary, prev_grapheme_boundary},
1212 syntax::{self, HighlightEvent},
1313 unicode::segmentation::UnicodeSegmentation,
1414 unicode::width::UnicodeWidthStr,
@@ -166,8 +166,8 @@ impl EditorView {
166166 let highlights = highlights.into_iter().map(|event| match event.unwrap() {
167167 // convert byte offsets to char offset
168168 HighlightEvent::Source { start, end } => {
169− let start = ensure_grapheme_boundary(text, text.byte_to_char(start));
170− let end = ensure_grapheme_boundary(text, text.byte_to_char(end));
169+ let start = ensure_grapheme_boundary_next(text, text.byte_to_char(start));
170+ let end = ensure_grapheme_boundary_next(text, text.byte_to_char(end));
171171 HighlightEvent::Source { start, end }
172172 }
173173 event => event,
@@ -191,21 +191,18 @@ impl EditorView {
191191 }
192192 .unwrap_or(base_cursor_scope);
193193
194− let primary_selection_scope = theme
195− .find_scope_index("ui.selection.primary")
196− .unwrap_or(selection_scope);
197−
198194 let highlights: Box<dyn Iterator<Item = HighlightEvent>> = if is_focused {
199− // inject selections as highlight scopes
200− let mut spans: Vec<(usize, std::ops::Range<usize>)> = Vec::new();
201−
202195 // TODO: primary + insert mode patching:
203196 // (ui.cursor.primary).patch(mode).unwrap_or(cursor)
204−
205197 let primary_cursor_scope = theme
206198 .find_scope_index("ui.cursor.primary")
207199 .unwrap_or(cursor_scope);
200+ let primary_selection_scope = theme
201+ .find_scope_index("ui.selection.primary")
202+ .unwrap_or(selection_scope);
208203
204+ // inject selections as highlight scopes
205+ let mut spans: Vec<(usize, std::ops::Range<usize>)> = Vec::new();
209206 for (i, range) in selections.iter().enumerate() {
210207 let (cursor_scope, selection_scope) = if i == primary_idx {
211208 (primary_cursor_scope, primary_selection_scope)
@@ -213,24 +210,23 @@ impl EditorView {
213210 (cursor_scope, selection_scope)
214211 };
215212
216− let cursor_end = next_grapheme_boundary(text, range.head); // Used in every case below.
217−
218− if range.head == range.anchor {
219− spans.push((cursor_scope, range.head..cursor_end));
213+ // Special-case: cursor at end of the rope.
214+ if range.head == range.anchor && range.head == text.len_chars() {
215+ spans.push((cursor_scope, range.head..range.head + 1));
220216 continue;
221217 }
222218
223− let reverse = range.head < range.anchor;
224−
225− if reverse {
226− spans.push((cursor_scope, range.head..cursor_end));
227− spans.push((
228− selection_scope,
229− cursor_end..next_grapheme_boundary(text, range.anchor),
230− ));
219+ let range = range.min_width_1(text);
220+ if range.head > range.anchor {
221+ // Standard case.
222+ let cursor_start = prev_grapheme_boundary(text, range.head);
223+ spans.push((selection_scope, range.anchor..cursor_start));
224+ spans.push((cursor_scope, cursor_start..range.head));
231225 } else {
232− spans.push((selection_scope, range.anchor..range.head));
226+ // Reverse case.
227+ let cursor_end = next_grapheme_boundary(text, range.head);
233228 spans.push((cursor_scope, range.head..cursor_end));
229+ spans.push((selection_scope, cursor_end..range.anchor));
234230 }
235231 }
236232
@@ -263,7 +259,10 @@ impl EditorView {
263259 spans.pop();
264260 }
265261 HighlightEvent::Source { start, end } => {
266− let text = text.slice(start..end);
262+ // `unwrap_or_else` part is for off-the-end indices of
263+ // the rope, to allow cursor highlighting at the end
264+ // of the rope.
265+ let text = text.get_slice(start..end).unwrap_or_else(|| " ".into());
267266
268267 use helix_core::graphemes::{grapheme_width, RopeGraphemes};
269268
@@ -332,7 +331,11 @@ impl EditorView {
332331 let info: Style = theme.get("info");
333332 let hint: Style = theme.get("hint");
334333
335− for (i, line) in (view.first_line..last_line).enumerate() {
334+ // Whether to draw the line number for the last line of the
335+ // document or not. We only draw it if it's not an empty line.
336+ let draw_last = text.line_to_byte(last_line) < text.len_bytes();
337+
338+ for (i, line) in (view.first_line..(last_line + 1)).enumerate() {
336339 use helix_core::diagnostic::Severity;
337340 if let Some(diagnostic) = doc.diagnostics().iter().find(|d| d.line == line) {
338341 surface.set_stringn(
@@ -349,11 +352,17 @@ impl EditorView {
349352 );
350353 }
351354
352− // line numbers having selections are rendered differently
355+ // Line numbers having selections are rendered
356+ // differently, further below.
357+ let line_number_text = if line == last_line && !draw_last {
358+ " ~".into()
359+ } else {
360+ format!("{:>5}", line + 1)
361+ };
353362 surface.set_stringn(
354363 viewport.x + 1 - OFFSET,
355364 viewport.y + i as u16,
356− format!("{:>5}", line + 1),
365+ line_number_text,
357366 5,
358367 linenr,
359368 );
@@ -367,19 +376,34 @@ impl EditorView {
367376 if is_focused {
368377 let screen = {
369378 let start = text.line_to_char(view.first_line);
370− let end = text.line_to_char(last_line + 1);
379+ let end = text.line_to_char(last_line + 1) + 1; // +1 for cursor at end of text.
371380 Range::new(start, end)
372381 };
373382
374383 let selection = doc.selection(view.id);
375384
376385 for selection in selection.iter().filter(|range| range.overlaps(&screen)) {
377− let head = view.screen_coords_at_pos(doc, text, selection.head);
386+ let head = view.screen_coords_at_pos(
387+ doc,
388+ text,
389+ if selection.head > selection.anchor {
390+ selection.head - 1
391+ } else {
392+ selection.head
393+ },
394+ );
378395 if let Some(head) = head {
396+ // Draw line number for selected lines.
397+ let line_number = view.first_line + head.row;
398+ let line_number_text = if line_number == last_line && !draw_last {
399+ " ~".into()
400+ } else {
401+ format!("{:>5}", line_number + 1)
402+ };
379403 surface.set_stringn(
380404 viewport.x + 1 - OFFSET,
381405 viewport.y + head.row as u16,
382− format!("{:>5}", view.first_line + head.row + 1),
406+ line_number_text,
383407 5,
384408 linenr_select,
385409 );
@@ -387,7 +411,10 @@ impl EditorView {
387411 // TODO: set cursor position for IME
388412 if let Some(syntax) = doc.syntax() {
389413 use helix_core::match_brackets;
390− let pos = doc.selection(view.id).cursor();
414+ let pos = doc
415+ .selection(view.id)
416+ .primary()
417+ .cursor(doc.text().slice(..));
391418 let pos = match_brackets::find(syntax, doc.text(), pos)
392419 .and_then(|pos| view.screen_coords_at_pos(doc, text, pos));
393420
@@ -432,7 +459,10 @@ impl EditorView {
432459 widgets::{Paragraph, Widget},
433460 };
434461
435− let cursor = doc.selection(view.id).cursor();
462+ let cursor = doc
463+ .selection(view.id)
464+ .primary()
465+ .cursor(doc.text().slice(..));
436466
437467 let diagnostics = doc.diagnostics().iter().filter(|diagnostic| {
438468 diagnostic.range.start <= cursor && diagnostic.range.end >= cursor
@@ -544,7 +574,12 @@ impl EditorView {
544574 // _ => "indent:ERROR",
545575 // };
546576 let position_info = {
547− let pos = coords_at_pos(doc.text().slice(..), doc.selection(view.id).cursor());
577+ let pos = coords_at_pos(
578+ doc.text().slice(..),
579+ doc.selection(view.id)
580+ .primary()
581+ .cursor(doc.text().slice(..)),
582+ );
548583 format!("{}:{}", pos.row + 1, pos.col + 1) // convert to 1-indexing
549584 };
550585
helix-view/src/document.rs+16−29
@@ -306,19 +306,6 @@ pub async fn to_writer<'a, W: tokio::io::AsyncWriteExt + Unpin + ?Sized>(
306306 Ok(())
307307}
308308
309−/// Inserts the final line ending into `rope` if it's missing. [Why?](https://stackoverflow.com/questions/729692/why-should-text-files-end-with-a-newline)
310−pub fn with_line_ending(rope: &mut Rope) -> LineEnding {
311− // search for line endings
312− let line_ending = auto_detect_line_ending(rope).unwrap_or(DEFAULT_LINE_ENDING);
313−
314− // add missing newline at the end of file
315− if rope.len_bytes() == 0 || !char_is_line_ending(rope.char(rope.len_chars() - 1)) {
316− rope.insert(rope.len_chars(), line_ending.as_str());
317− }
318−
319− line_ending
320−}
321−
322309/// Like std::mem::replace() except it allows the replacement value to be mapped from the
323310/// original value.
324311fn take_with<T, F>(mut_ref: &mut T, closure: F)
@@ -456,7 +443,7 @@ impl Document {
456443 theme: Option<&Theme>,
457444 config_loader: Option<&syntax::Loader>,
458445 ) -> Result<Self, Error> {
459− let (mut rope, encoding) = if path.exists() {
446+ let (rope, encoding) = if path.exists() {
460447 let mut file =
461448 std::fs::File::open(&path).context(format!("unable to open {:?}", path))?;
462449 from_reader(&mut file, encoding)?
@@ -465,7 +452,6 @@ impl Document {
465452 (Rope::from(DEFAULT_LINE_ENDING.as_str()), encoding)
466453 };
467454
468− let line_ending = with_line_ending(&mut rope);
469455 let mut doc = Self::from(rope, Some(encoding));
470456
471457 // set the path and try detecting the language
@@ -474,9 +460,9 @@ impl Document {
474460 doc.detect_language(theme, loader);
475461 }
476462
477− // Detect indentation style and set line ending.
463+ // Detect indentation style and line ending.
478464 doc.detect_indent_style();
479− doc.line_ending = line_ending;
465+ doc.line_ending = auto_detect_line_ending(&doc.text).unwrap_or(DEFAULT_LINE_ENDING);
480466
481467 Ok(doc)
482468 }
@@ -605,17 +591,16 @@ impl Document {
605591 }
606592
607593 let mut file = std::fs::File::open(path.unwrap())?;
608− let (mut rope, ..) = from_reader(&mut file, Some(encoding))?;
609− let line_ending = with_line_ending(&mut rope);
594+ let (rope, ..) = from_reader(&mut file, Some(encoding))?;
610595
611596 let transaction = helix_core::diff::compare_ropes(self.text(), &rope);
612597 self.apply(&transaction, view_id);
613598 self.append_changes_to_history(view_id);
614599 self.reset_modified();
615600
616− // Detect indentation style and set line ending.
601+ // Detect indentation style and line ending.
617602 self.detect_indent_style();
618− self.line_ending = line_ending;
603+ self.line_ending = auto_detect_line_ending(&self.text).unwrap_or(DEFAULT_LINE_ENDING);
619604
620605 Ok(())
621606 }
@@ -807,7 +792,8 @@ impl Document {
807792
808793 pub fn set_selection(&mut self, view_id: ViewId, selection: Selection) {
809794 // TODO: use a transaction?
810− self.selections.insert(view_id, selection);
795+ self.selections
796+ .insert(view_id, selection.ensure_invariants(self.text().slice(..)));
811797 }
812798
813799 fn apply_impl(&mut self, transaction: &Transaction, view_id: ViewId) -> bool {
@@ -822,7 +808,12 @@ impl Document {
822808 .selection()
823809 .cloned()
824810 .unwrap_or_else(|| self.selection(view_id).clone().map(transaction.changes()));
825− self.set_selection(view_id, selection);
811+ self.selections.insert(view_id, selection);
812+
813+ // Ensure all selections accross all views still adhere to invariants.
814+ for selection in self.selections.values_mut() {
815+ *selection = selection.clone().ensure_invariants(self.text.slice(..));
816+ }
826817 }
827818
828819 if !transaction.changes().is_empty() {
@@ -1089,7 +1080,7 @@ impl Document {
10891080
10901081impl Default for Document {
10911082 fn default() -> Self {
1092− let text = Rope::from(DEFAULT_LINE_ENDING.as_str());
1083+ let text = Rope::from("");
10931084 Self::from(text, None)
10941085 }
10951086}
@@ -1214,11 +1205,7 @@ mod test {
12141205
12151206 #[test]
12161207 fn test_line_ending() {
1217− if cfg!(windows) {
1218− assert_eq!(Document::default().text().to_string(), "\r\n");
1219− } else {
1220− assert_eq!(Document::default().text().to_string(), "\n");
1221− }
1208+ assert_eq!(Document::default().text().to_string(), "");
12221209 }
12231210
12241211 macro_rules! test_decode {
helix-view/src/editor.rs+9−4
@@ -138,12 +138,14 @@ impl Editor {
138138 let (view, doc) = current!(self);
139139
140140 // initialize selection for view
141− let selection = doc
142− .selections
141+ doc.selections
143142 .entry(view.id)
144143 .or_insert_with(|| Selection::point(0));
145144 // TODO: reuse align_view
146− let pos = selection.cursor();
145+ let pos = doc
146+ .selection(view.id)
147+ .primary()
148+ .cursor(doc.text().slice(..));
147149 let line = doc.text().char_to_line(pos);
148150 view.first_line = line.saturating_sub(view.area.height as usize / 2);
149151
@@ -293,7 +295,10 @@ impl Editor {
293295 const OFFSET: u16 = 7; // 1 diagnostic + 5 linenr + 1 gutter
294296 let view = view!(self);
295297 let doc = &self.documents[view.doc];
296− let cursor = doc.selection(view.id).cursor();
298+ let cursor = doc
299+ .selection(view.id)
300+ .primary()
301+ .cursor(doc.text().slice(..));
297302 if let Some(mut pos) = view.screen_coords_at_pos(doc, doc.text().slice(..), cursor) {
298303 pos.col += view.area.x as usize + OFFSET as usize;
299304 pos.row += view.area.y as usize;
helix-view/src/view.rs+9−5
@@ -84,18 +84,21 @@ impl View {
8484 }
8585
8686 pub fn ensure_cursor_in_view(&mut self, doc: &Document) {
87− let cursor = doc.selection(self.id).cursor();
87+ let cursor = doc
88+ .selection(self.id)
89+ .primary()
90+ .cursor(doc.text().slice(..));
8891 let pos = coords_at_pos(doc.text().slice(..), cursor);
8992 let line = pos.row;
9093 let col = pos.col;
9194 let height = self.area.height.saturating_sub(1); // - 1 for statusline
92− let last_line = self.first_line + height as usize;
95+ let last_line = (self.first_line + height as usize).saturating_sub(1);
9396
9497 let scrolloff = PADDING.min(self.area.height as usize / 2); // TODO: user pref
9598
9699 // TODO: not ideal
97100 const OFFSET: usize = 7; // 1 diagnostic + 5 linenr + 1 gutter
98− let last_col = self.first_col + (self.area.width as usize - OFFSET);
101+ let last_col = (self.first_col + self.area.width as usize).saturating_sub(OFFSET + 1);
99102
100103 if line > last_line.saturating_sub(scrolloff) {
101104 // scroll down
@@ -119,8 +122,9 @@ impl View {
119122 pub fn last_line(&self, doc: &Document) -> usize {
120123 let height = self.area.height.saturating_sub(1); // - 1 for statusline
121124 std::cmp::min(
122− self.first_line + height as usize,
123− doc.text().len_lines() - 1,
125+ // Saturating subs to make it inclusive zero indexing.
126+ (self.first_line + height as usize).saturating_sub(1),
127+ doc.text().len_lines().saturating_sub(1),
124128 )
125129 }
126130