diff options
Diffstat (limited to 'helix-term')
-rw-r--r-- | helix-term/src/commands.rs | 886 | ||||
-rw-r--r-- | helix-term/src/ui/completion.rs | 8 | ||||
-rw-r--r-- | helix-term/src/ui/editor.rs | 95 |
3 files changed, 584 insertions, 405 deletions
diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 5fc96cd9..72462741 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -1,9 +1,6 @@ use helix_core::{ comment, coords_at_pos, find_first_non_whitespace_char, find_root, graphemes, indent, - line_ending::{ - get_line_ending_of_str, line_end_char_index, rope_end_without_line_ending, - str_is_line_ending, - }, + line_ending::{get_line_ending_of_str, line_end_char_index, str_is_line_ending}, match_brackets, movement::{self, Direction}, object, pos_at_coords, @@ -124,7 +121,7 @@ enum Align { } fn align_view(doc: &Document, view: &mut View, align: Align) { - let pos = doc.selection(view.id).cursor(); + let pos = doc.selection(view.id).cursor(doc.text().slice(..)); let line = doc.text().char_to_line(pos); let relative = match align { @@ -328,124 +325,151 @@ impl PartialEq for Command { fn move_char_left(cx: &mut Context) { let count = cx.count(); let (view, doc) = current!(cx.editor); - let text = doc.text().slice(..); - let selection = doc.selection(view.id).transform(|range| { - movement::move_horizontally(text, range, Direction::Backward, count, Movement::Move) - }); - doc.set_selection(view.id, selection); + doc.set_selection( + view.id, + doc.selection(view.id).clone().transform(|range| { + movement::move_horizontally( + doc.text().slice(..), + range, + Direction::Backward, + count, + Movement::Move, + ) + }), + ); } fn move_char_right(cx: &mut Context) { let count = cx.count(); let (view, doc) = current!(cx.editor); - let text = doc.text().slice(..); - let selection = doc.selection(view.id).transform(|range| { - movement::move_horizontally(text, range, Direction::Forward, count, Movement::Move) - }); - doc.set_selection(view.id, selection); + doc.set_selection( + view.id, + doc.selection(view.id).clone().transform(|range| { + movement::move_horizontally( + doc.text().slice(..), + range, + Direction::Forward, + count, + Movement::Move, + ) + }), + ); } fn move_line_up(cx: &mut Context) { let count = cx.count(); let (view, doc) = current!(cx.editor); - let text = doc.text().slice(..); - let selection = doc.selection(view.id).transform(|range| { - movement::move_vertically(text, range, Direction::Backward, count, Movement::Move) - }); - doc.set_selection(view.id, selection); + doc.set_selection( + view.id, + doc.selection(view.id).clone().transform(|range| { + movement::move_vertically( + doc.text().slice(..), + range, + Direction::Backward, + count, + Movement::Move, + ) + }), + ); } fn move_line_down(cx: &mut Context) { let count = cx.count(); let (view, doc) = current!(cx.editor); - let text = doc.text().slice(..); - let selection = doc.selection(view.id).transform(|range| { - movement::move_vertically(text, range, Direction::Forward, count, Movement::Move) - }); - doc.set_selection(view.id, selection); + doc.set_selection( + view.id, + doc.selection(view.id).clone().transform(|range| { + movement::move_vertically( + doc.text().slice(..), + range, + Direction::Forward, + count, + Movement::Move, + ) + }), + ); } fn goto_line_end(cx: &mut Context) { let (view, doc) = current!(cx.editor); + doc.set_selection( + view.id, + doc.selection(view.id).clone().transform(|range| { + let text = doc.text().slice(..); - let selection = doc.selection(view.id).transform(|range| { - let text = doc.text(); - let line = text.char_to_line(range.head); + let head = if range.anchor < range.head { + graphemes::prev_grapheme_boundary(text, range.head) + } else { + range.head + }; + let line = text.char_to_line(head); - let pos = line_end_char_index(&text.slice(..), line); - let pos = graphemes::nth_prev_grapheme_boundary(text.slice(..), pos, 1); - let pos = range.head.max(pos).max(text.line_to_char(line)); - - Range::new( - match doc.mode { - Mode::Normal | Mode::Insert => pos, - Mode::Select => range.anchor, - }, - pos, - ) - }); + let mut pos = line_end_char_index(&text, line); + if doc.mode != Mode::Select { + pos = graphemes::prev_grapheme_boundary(text, pos); + } + pos = head.max(pos).max(text.line_to_char(line)); - doc.set_selection(view.id, selection); + range.put(text, pos, doc.mode == Mode::Select) + }), + ); } fn goto_line_end_newline(cx: &mut Context) { let (view, doc) = current!(cx.editor); - let selection = doc.selection(view.id).transform(|range| { - let text = doc.text(); - let line = text.char_to_line(range.head); + doc.set_selection( + view.id, + doc.selection(view.id).clone().transform(|range| { + let text = doc.text().slice(..); - let pos = line_end_char_index(&text.slice(..), line); - Range::new(pos, pos) - }); + let head = if range.anchor < range.head { + graphemes::prev_grapheme_boundary(text, range.head) + } else { + range.head + }; + let line = text.char_to_line(head); - doc.set_selection(view.id, selection); + let mut pos = text.line_to_char((line + 1).min(text.len_lines())); + if doc.mode != Mode::Select { + pos = graphemes::prev_grapheme_boundary(text, pos); + } + range.put(text, pos, doc.mode == Mode::Select) + }), + ); } fn goto_line_start(cx: &mut Context) { let (view, doc) = current!(cx.editor); + doc.set_selection( + view.id, + doc.selection(view.id).clone().transform(|range| { + let text = doc.text().slice(..); + let line = text.char_to_line(range.head); - let selection = doc.selection(view.id).transform(|range| { - let text = doc.text(); - let line = text.char_to_line(range.head); - - // adjust to start of the line - let pos = text.line_to_char(line); - Range::new( - match doc.mode { - Mode::Normal => range.anchor, - Mode::Select | Mode::Insert => pos, - }, - pos, - ) - }); - - doc.set_selection(view.id, selection); + // adjust to start of the line + let pos = text.line_to_char(line); + range.put(text, pos, doc.mode == Mode::Select) + }), + ); } fn goto_first_nonwhitespace(cx: &mut Context) { let (view, doc) = current!(cx.editor); + doc.set_selection( + view.id, + doc.selection(view.id).clone().transform(|range| { + let text = doc.text().slice(..); + let line_idx = text.char_to_line(range.head); - let selection = doc.selection(view.id).transform(|range| { - let text = doc.text(); - let line_idx = text.char_to_line(range.head); - - if let Some(pos) = find_first_non_whitespace_char(text.line(line_idx)) { - let pos = pos + text.line_to_char(line_idx); - Range::new( - match doc.mode { - Mode::Normal => pos, - Mode::Select => range.anchor, - Mode::Insert => unreachable!(), - }, - pos, - ) - } else { - range - } - }); - - doc.set_selection(view.id, selection); + if let Some(pos) = find_first_non_whitespace_char(text.line(line_idx)) { + let pos = pos + text.line_to_char(line_idx); + range.put(text, pos, doc.mode == Mode::Select) + } else { + range + } + }), + ); } fn goto_window(cx: &mut Context, align: Align) { @@ -486,73 +510,80 @@ fn goto_window_bottom(cx: &mut Context) { fn move_next_word_start(cx: &mut Context) { let count = cx.count(); let (view, doc) = current!(cx.editor); - let text = doc.text().slice(..); - let selection = doc - .selection(view.id) - .transform(|range| movement::move_next_word_start(text, range, count)); - - doc.set_selection(view.id, selection); + doc.set_selection( + view.id, + doc.selection(view.id) + .clone() + .min_width_1(doc.text().slice(..)) + .transform(|range| movement::move_next_word_start(doc.text().slice(..), range, count)), + ); } fn move_prev_word_start(cx: &mut Context) { let count = cx.count(); let (view, doc) = current!(cx.editor); - let text = doc.text().slice(..); - - let selection = doc - .selection(view.id) - .transform(|range| movement::move_prev_word_start(text, range, count)); - - doc.set_selection(view.id, selection); + doc.set_selection( + view.id, + doc.selection(view.id) + .clone() + .min_width_1(doc.text().slice(..)) + .transform(|range| movement::move_prev_word_start(doc.text().slice(..), range, count)), + ); } fn move_next_word_end(cx: &mut Context) { let count = cx.count(); let (view, doc) = current!(cx.editor); - let text = doc.text().slice(..); - - let selection = doc - .selection(view.id) - .transform(|range| movement::move_next_word_end(text, range, count)); - - doc.set_selection(view.id, selection); + doc.set_selection( + view.id, + doc.selection(view.id) + .clone() + .min_width_1(doc.text().slice(..)) + .transform(|range| movement::move_next_word_end(doc.text().slice(..), range, count)), + ); } fn move_next_long_word_start(cx: &mut Context) { let count = cx.count(); let (view, doc) = current!(cx.editor); - let text = doc.text().slice(..); - - let selection = doc - .selection(view.id) - .transform(|range| movement::move_next_long_word_start(text, range, count)); - - doc.set_selection(view.id, selection); + doc.set_selection( + view.id, + doc.selection(view.id) + .clone() + .min_width_1(doc.text().slice(..)) + .transform(|range| { + movement::move_next_long_word_start(doc.text().slice(..), range, count) + }), + ); } fn move_prev_long_word_start(cx: &mut Context) { let count = cx.count(); let (view, doc) = current!(cx.editor); - let text = doc.text().slice(..); - - let selection = doc - .selection(view.id) - .transform(|range| movement::move_prev_long_word_start(text, range, count)); - - doc.set_selection(view.id, selection); + doc.set_selection( + view.id, + doc.selection(view.id) + .clone() + .min_width_1(doc.text().slice(..)) + .transform(|range| { + movement::move_prev_long_word_start(doc.text().slice(..), range, count) + }), + ); } fn move_next_long_word_end(cx: &mut Context) { let count = cx.count(); let (view, doc) = current!(cx.editor); - let text = doc.text().slice(..); - - let selection = doc - .selection(view.id) - .transform(|range| movement::move_next_long_word_end(text, range, count)); - - doc.set_selection(view.id, selection); + doc.set_selection( + view.id, + doc.selection(view.id) + .clone() + .min_width_1(doc.text().slice(..)) + .transform(|range| { + movement::move_next_long_word_end(doc.text().slice(..), range, count) + }), + ); } fn goto_file_start(cx: &mut Context) { @@ -564,50 +595,58 @@ fn goto_file_start(cx: &mut Context) { fn goto_file_end(cx: &mut Context) { push_jump(cx.editor); let (view, doc) = current!(cx.editor); - let text = doc.text(); - let last_line = text.line_to_char(text.len_lines().saturating_sub(2)); - doc.set_selection(view.id, Selection::point(last_line)); + doc.set_selection(view.id, Selection::point(doc.text().len_chars())); } fn extend_next_word_start(cx: &mut Context) { let count = cx.count(); let (view, doc) = current!(cx.editor); - let text = doc.text().slice(..); - - let selection = doc.selection(view.id).transform(|range| { - let word = movement::move_next_word_start(text, range, count); - let pos = word.head; - Range::new(range.anchor, pos) - }); - - doc.set_selection(view.id, selection); + doc.set_selection( + view.id, + doc.selection(view.id) + .clone() + .min_width_1(doc.text().slice(..)) + .transform(|range| { + let text = doc.text().slice(..); + let word = movement::move_next_word_start(text, range, count); + let pos = word.head; + range.put(text, pos, true) + }), + ); } fn extend_prev_word_start(cx: &mut Context) { let count = cx.count(); let (view, doc) = current!(cx.editor); - let text = doc.text().slice(..); - - let selection = doc.selection(view.id).transform(|range| { - let word = movement::move_prev_word_start(text, range, count); - let pos = word.head; - Range::new(range.anchor, pos) - }); - doc.set_selection(view.id, selection); + doc.set_selection( + view.id, + doc.selection(view.id) + .clone() + .min_width_1(doc.text().slice(..)) + .transform(|range| { + let text = doc.text().slice(..); + let word = movement::move_prev_word_start(text, range, count); + let pos = word.head; + range.put(text, pos, true) + }), + ); } fn extend_next_word_end(cx: &mut Context) { let count = cx.count(); let (view, doc) = current!(cx.editor); - let text = doc.text().slice(..); - - let selection = doc.selection(view.id).transform(|range| { - let word = movement::move_next_word_end(text, range, count); - let pos = word.head; - Range::new(range.anchor, pos) - }); - - doc.set_selection(view.id, selection); + doc.set_selection( + view.id, + doc.selection(view.id) + .clone() + .min_width_1(doc.text().slice(..)) + .transform(|range| { + let text = doc.text().slice(..); + let word = movement::move_next_word_end(text, range, count); + let pos = word.head; + range.put(text, pos, true) + }), + ); } #[inline] @@ -650,21 +689,15 @@ where }; let (view, doc) = current!(cx.editor); - let text = doc.text().slice(..); - let selection = doc.selection(view.id).transform(|range| { - search_fn(text, ch, range.head, count, inclusive).map_or(range, |pos| { - if extend { - Range::new(range.anchor, pos) - } else { - // select - Range::new(range.head, pos) - } - // or (pos, pos) to move to found val - }) - }); - - doc.set_selection(view.id, selection); + doc.set_selection( + view.id, + doc.selection(view.id).clone().transform(|range| { + let text = doc.text().slice(..); + search_fn(text, ch, range.head, count, inclusive) + .map_or(range, |pos| range.put(text, pos, extend)) + }), + ); }) } @@ -758,24 +791,30 @@ fn replace(cx: &mut Context) { _ => None, }; - if let Some(ch) = ch { - let transaction = - Transaction::change_by_selection(doc.text(), doc.selection(view.id), |range| { - let max_to = rope_end_without_line_ending(&doc.text().slice(..)); - let to = std::cmp::min(max_to, range.to() + 1); - let text: String = RopeGraphemes::new(doc.text().slice(range.from()..to)) - .map(|g| { - let cow: Cow<str> = g.into(); - if str_is_line_ending(&cow) { - cow - } else { - ch.into() - } - }) - .collect(); + let text = doc.text().slice(..); + let selection = doc.selection(view.id).clone().min_width_1(text); - (range.from(), to, Some(text.into())) - }); + if let Some(ch) = ch { + let transaction = Transaction::change_by_selection(doc.text(), &selection, |range| { + if !range.is_empty() { + let text: String = + RopeGraphemes::new(doc.text().slice(range.from()..range.to())) + .map(|g| { + let cow: Cow<str> = g.into(); + if str_is_line_ending(&cow) { + cow + } else { + ch.into() + } + }) + .collect(); + + (range.from(), range.to(), Some(text.into())) + } else { + // No change. + (range.from(), range.to(), None) + } + }); doc.apply(&transaction, view.id); doc.append_changes_to_history(view.id); @@ -785,24 +824,27 @@ fn replace(cx: &mut Context) { fn switch_case(cx: &mut Context) { let (view, doc) = current!(cx.editor); - let transaction = - Transaction::change_by_selection(doc.text(), doc.selection(view.id), |range| { - let text: Tendril = range - .fragment(doc.text().slice(..)) - .chars() - .flat_map(|ch| { - if ch.is_lowercase() { - ch.to_uppercase().collect() - } else if ch.is_uppercase() { - ch.to_lowercase().collect() - } else { - vec![ch] - } - }) - .collect(); + let selection = doc + .selection(view.id) + .clone() + .min_width_1(doc.text().slice(..)); + let transaction = Transaction::change_by_selection(doc.text(), &selection, |range| { + let text: Tendril = range + .fragment(doc.text().slice(..)) + .chars() + .flat_map(|ch| { + if ch.is_lowercase() { + ch.to_uppercase().collect() + } else if ch.is_uppercase() { + ch.to_lowercase().collect() + } else { + vec![ch] + } + }) + .collect(); - (range.from(), range.to() + 1, Some(text)) - }); + (range.from(), range.to(), Some(text)) + }); doc.apply(&transaction, view.id); doc.append_changes_to_history(view.id); @@ -810,12 +852,15 @@ fn switch_case(cx: &mut Context) { fn switch_to_uppercase(cx: &mut Context) { let (view, doc) = current!(cx.editor); - let transaction = - Transaction::change_by_selection(doc.text(), doc.selection(view.id), |range| { - let text: Tendril = range.fragment(doc.text().slice(..)).to_uppercase().into(); + let selection = doc + .selection(view.id) + .clone() + .min_width_1(doc.text().slice(..)); + let transaction = Transaction::change_by_selection(doc.text(), &selection, |range| { + let text: Tendril = range.fragment(doc.text().slice(..)).to_uppercase().into(); - (range.from(), range.to() + 1, Some(text)) - }); + (range.from(), range.to(), Some(text)) + }); doc.apply(&transaction, view.id); doc.append_changes_to_history(view.id); @@ -823,12 +868,15 @@ fn switch_to_uppercase(cx: &mut Context) { fn switch_to_lowercase(cx: &mut Context) { let (view, doc) = current!(cx.editor); - let transaction = - Transaction::change_by_selection(doc.text(), doc.selection(view.id), |range| { - let text: Tendril = range.fragment(doc.text().slice(..)).to_lowercase().into(); + let selection = doc + .selection(view.id) + .clone() + .min_width_1(doc.text().slice(..)); + let transaction = Transaction::change_by_selection(doc.text(), &selection, |range| { + let text: Tendril = range.fragment(doc.text().slice(..)).to_lowercase().into(); - (range.from(), range.to() + 1, Some(text)) - }); + (range.from(), range.to(), Some(text)) + }); doc.apply(&transaction, view.id); doc.append_changes_to_history(view.id); @@ -837,7 +885,10 @@ fn switch_to_lowercase(cx: &mut Context) { fn scroll(cx: &mut Context, offset: usize, direction: Direction) { use Direction::*; let (view, doc) = current!(cx.editor); - let cursor = coords_at_pos(doc.text().slice(..), doc.selection(view.id).cursor()); + let cursor = coords_at_pos( + doc.text().slice(..), + doc.selection(view.id).cursor(doc.text().slice(..)), + ); let doc_last_line = doc.text().len_lines() - 1; let last_line = view.last_line(doc); @@ -899,47 +950,75 @@ fn half_page_down(cx: &mut Context) { fn extend_char_left(cx: &mut Context) { let count = cx.count(); let (view, doc) = current!(cx.editor); - let text = doc.text().slice(..); - let selection = doc.selection(view.id).transform(|range| { - movement::move_horizontally(text, range, Direction::Backward, count, Movement::Extend) - }); - doc.set_selection(view.id, selection); + doc.set_selection( + view.id, + doc.selection(view.id).clone().transform(|range| { + movement::move_horizontally( + doc.text().slice(..), + range, + Direction::Backward, + count, + Movement::Extend, + ) + }), + ); } fn extend_char_right(cx: &mut Context) { let count = cx.count(); let (view, doc) = current!(cx.editor); - let text = doc.text().slice(..); - let selection = doc.selection(view.id).transform(|range| { - movement::move_horizontally(text, range, Direction::Forward, count, Movement::Extend) - }); - doc.set_selection(view.id, selection); + doc.set_selection( + view.id, + doc.selection(view.id).clone().transform(|range| { + movement::move_horizontally( + doc.text().slice(..), + range, + Direction::Forward, + count, + Movement::Extend, + ) + }), + ); } fn extend_line_up(cx: &mut Context) { let count = cx.count(); let (view, doc) = current!(cx.editor); - let text = doc.text().slice(..); - let selection = doc.selection(view.id).transform(|range| { - movement::move_vertically(text, range, Direction::Backward, count, Movement::Extend) - }); - doc.set_selection(view.id, selection); + doc.set_selection( + view.id, + doc.selection(view.id).clone().transform(|range| { + movement::move_vertically( + doc.text().slice(..), + range, + Direction::Backward, + count, + Movement::Extend, + ) + }), + ); } fn extend_line_down(cx: &mut Context) { let count = cx.count(); let (view, doc) = current!(cx.editor); - let text = doc.text().slice(..); - let selection = doc.selection(view.id).transform(|range| { - movement::move_vertically(text, range, Direction::Forward, count, Movement::Extend) - }); - doc.set_selection(view.id, selection); + doc.set_selection( + view.id, + doc.selection(view.id).clone().transform(|range| { + movement::move_vertically( + doc.text().slice(..), + range, + Direction::Forward, + count, + Movement::Extend, + ) + }), + ); } fn select_all(cx: &mut Context) { let (view, doc) = current!(cx.editor); - let end = rope_end_without_line_ending(&doc.text().slice(..)); + let end = doc.text().len_chars(); doc.set_selection(view.id, Selection::single(0, end)) } @@ -979,7 +1058,25 @@ fn split_selection_on_newline(cx: &mut Context) { fn search_impl(doc: &mut Document, view: &mut View, contents: &str, regex: &Regex, extend: bool) { let text = doc.text(); let selection = doc.selection(view.id); - let start = text.char_to_byte(selection.cursor()); + let start = { + let range = selection.primary(); + + // This is a little bit weird. Due to 1-width cursor semantics, we + // would typically want the search to always begin at the visual left-side + // of the head. However, when there's already a selection from e.g. a + // previous search result, we don't want to include any of that selection + // in the subsequent search. The code below makes a compromise between the + // two behaviors that hopefully behaves the way most people expect most of + // the time. + if range.anchor <= range.head { + text.char_to_byte(range.head) + } else { + text.char_to_byte(graphemes::next_grapheme_boundary( + text.slice(..), + range.head, + )) + } + }; // use find_at to find the next match after the cursor, loop around the end // Careful, `Regex` uses `bytes` as offsets, not character indices! @@ -996,12 +1093,10 @@ fn search_impl(doc: &mut Document, view: &mut View, contents: &str, regex: &Rege return; } - let head = end - 1; - let selection = if extend { - selection.clone().push(Range::new(start, head)) + selection.clone().push(Range::new(start, end)) } else { - Selection::single(start, head) + Selection::single(start, end) }; doc.set_selection(view.id, selection); @@ -1064,16 +1159,15 @@ fn extend_line(cx: &mut Context) { let count = cx.count(); let (view, doc) = current!(cx.editor); - let pos = doc.selection(view.id).primary(); let text = doc.text(); + let range = doc.selection(view.id).primary().min_width_1(text.slice(..)); - let line_start = text.char_to_line(pos.anchor); - let start = text.line_to_char(line_start); - let line_end = text.char_to_line(pos.head); - let mut end = line_end_char_index(&text.slice(..), line_end + count.saturating_sub(1)); + let (start_line, end_line) = range.line_range(text.slice(..)); + let start = text.line_to_char(start_line); + let mut end = text.line_to_char((end_line + count).min(text.len_lines())); - if pos.anchor == start && pos.head == end && line_end < (text.len_lines() - 2) { - end = line_end_char_index(&text.slice(..), line_end + 1); + if range.from() == start && range.to() == end { + end = text.line_to_char((end_line + count + 1).min(text.len_lines())); } doc.set_selection(view.id, Selection::single(start, end)); @@ -1082,41 +1176,36 @@ fn extend_line(cx: &mut Context) { fn extend_to_line_bounds(cx: &mut Context) { let (view, doc) = current!(cx.editor); - let text = doc.text(); - let selection = doc.selection(view.id).transform(|range| { - let start = text.line_to_char(text.char_to_line(range.from())); - let end = text - .line_to_char(text.char_to_line(range.to()) + 1) - .saturating_sub(1); - - if range.anchor < range.head { - Range::new(start, end) - } else { - Range::new(end, start) - } - }); + doc.set_selection( + view.id, + doc.selection(view.id).clone().transform(|range| { + let text = doc.text(); - doc.set_selection(view.id, selection); + let (start_line, end_line) = range.line_range(text.slice(..)); + let start = text.line_to_char(start_line); + let end = text.line_to_char((end_line + 1).min(text.len_lines())); + + if range.anchor <= range.head { + Range::new(start, end) + } else { + Range::new(end, start) + } + }), + ); } fn delete_selection_impl(reg: &mut Register, doc: &mut Document, view_id: ViewId) { - // first yank the selection - let values: Vec<String> = doc - .selection(view_id) - .fragments(doc.text().slice(..)) - .map(Cow::into_owned) - .collect(); + let text = doc.text().slice(..); + let selection = doc.selection(view_id).clone().min_width_1(text); + // first yank the selection + let values: Vec<String> = selection.fragments(text).map(Cow::into_owned).collect(); reg.write(values); // then delete - let transaction = - Transaction::change_by_selection(doc.text(), doc.selection(view_id), |range| { - let alltext = doc.text().slice(..); - let max_to = rope_end_without_line_ending(&alltext); - let to = std::cmp::min(max_to, range.to() + 1); - (range.from(), to, None) - }); + let transaction = Transaction::change_by_selection(doc.text(), &selection, |range| { + (range.from(), range.to(), None) + }); doc.apply(&transaction, view_id); } @@ -1144,20 +1233,30 @@ fn change_selection(cx: &mut Context) { fn collapse_selection(cx: &mut Context) { let (view, doc) = current!(cx.editor); - let selection = doc - .selection(view.id) - .transform(|range| Range::new(range.head, range.head)); - doc.set_selection(view.id, selection); + doc.set_selection( + view.id, + doc.selection(view.id).clone().transform(|range| { + let pos = if range.head > range.anchor { + // For 1-width cursor semantics. + graphemes::prev_grapheme_boundary(doc.text().slice(..), range.head) + } else { + range.head + }; + Range::new(pos, pos) + }), + ); } fn flip_selections(cx: &mut Context) { let (view, doc) = current!(cx.editor); - let selection = doc - .selection(view.id) - .transform(|range| Range::new(range.head, range.anchor)); - doc.set_selection(view.id, selection); + doc.set_selection( + view.id, + doc.selection(view.id) + .clone() + .transform(|range| Range::new(range.head, range.anchor)), + ); } fn enter_insert_mode(doc: &mut Document) { @@ -1169,10 +1268,12 @@ fn insert_mode(cx: &mut Context) { let (view, doc) = current!(cx.editor); enter_insert_mode(doc); - let selection = doc - .selection(view.id) - .transform(|range| Range::new(range.to(), range.from())); - doc.set_selection(view.id, selection); + doc.set_selection( + view.id, + doc.selection(view.id) + .clone() + .transform(|range| Range::new(range.to(), range.from())), + ); } // inserts at the end of each selection @@ -1181,15 +1282,17 @@ fn append_mode(cx: &mut Context) { enter_insert_mode(doc); doc.restore_cursor = true; - let text = doc.text().slice(..); - let selection = doc.selection(view.id).transform(|range| { - Range::new( - range.from(), - graphemes::next_grapheme_boundary(text, range.to()), // to() + next char - ) + let selection = doc.selection(view.id).clone().transform(|range| { + let to = if range.to() == range.from() { + // For 1-width cursor semantics. + graphemes::next_grapheme_boundary(doc.text().slice(..), range.to()) + } else { + range.to() + }; + Range::new(range.from(), to) }); - let end = text.len_chars(); + let end = doc.text().len_chars(); if selection.iter().any(|range| range.head == end) { let transaction = Transaction::change( @@ -1621,11 +1724,13 @@ mod cmd { match cx.editor.clipboard_provider.get_contents() { Ok(contents) => { + let selection = doc + .selection(view.id) + .clone() + .min_width_1(doc.text().slice(..)); let transaction = - Transaction::change_by_selection(doc.text(), doc.selection(view.id), |range| { - let max_to = rope_end_without_line_ending(&doc.text().slice(..)); - let to = std::cmp::min(max_to, range.to() + 1); - (range.from(), to, Some(contents.as_str().into())) + Transaction::change_by_selection(doc.text(), &selection, |range| { + (range.from(), range.to(), Some(contents.as_str().into())) }); doc.apply(&transaction, view.id); @@ -2103,13 +2208,15 @@ fn append_to_line(cx: &mut Context) { let (view, doc) = current!(cx.editor); enter_insert_mode(doc); - let selection = doc.selection(view.id).transform(|range| { - let text = doc.text(); - let line = text.char_to_line(range.head); - let pos = line_end_char_index(&text.slice(..), line); - Range::new(pos, pos) - }); - doc.set_selection(view.id, selection); + doc.set_selection( + view.id, + doc.selection(view.id).clone().transform(|range| { + let text = &doc.text().slice(..); + let line = text.char_to_line(range.head); + let pos = line_end_char_index(text, line); + Range::new(pos, pos) + }), + ); } /// Sometimes when applying formatting changes we want to mark the buffer as unmodified, for @@ -2239,14 +2346,15 @@ fn normal_mode(cx: &mut Context) { // if leaving append mode, move cursor back by 1 if doc.restore_cursor { - let text = doc.text().slice(..); - let selection = doc.selection(view.id).transform(|range| { - Range::new( - range.from(), - graphemes::prev_grapheme_boundary(text, range.to()), - ) - }); - doc.set_selection(view.id, selection); + doc.set_selection( + view.id, + doc.selection(view.id).clone().transform(|range| { + Range::new( + range.from(), + graphemes::prev_grapheme_boundary(doc.text().slice(..), range.to()), + ) + }), + ); doc.restore_cursor = false; } @@ -2269,6 +2377,24 @@ fn goto_last_accessed_file(cx: &mut Context) { } fn select_mode(cx: &mut Context) { + let (view, doc) = current!(cx.editor); + + // Make sure all selections are at least 1-wide. + // (With the exception of being in an empty document, of course.) + doc.set_selection( + view.id, + doc.selection(view.id).clone().transform(|range| { + if range.is_empty() && range.head == doc.text().len_chars() { + Range::new( + graphemes::prev_grapheme_boundary(doc.text().slice(..), range.anchor), + range.head, + ) + } else { + range.min_width_1(doc.text().slice(..)) + } + }), + ); + doc_mut!(cx.editor).mode = Mode::Select; } @@ -2281,7 +2407,7 @@ fn goto_prehook(cx: &mut Context) -> bool { push_jump(cx.editor); let (view, doc) = current!(cx.editor); - let line_idx = std::cmp::min(count.get() - 1, doc.text().len_lines().saturating_sub(2)); + let line_idx = std::cmp::min(count.get() - 1, doc.text().len_lines().saturating_sub(1)); let pos = doc.text().line_to_char(line_idx); doc.set_selection(view.id, Selection::point(pos)); true @@ -2355,7 +2481,11 @@ fn goto_definition(cx: &mut Context) { let offset_encoding = language_server.offset_encoding(); - let pos = pos_to_lsp_pos(doc.text(), doc.selection(view.id).cursor(), offset_encoding); + let pos = pos_to_lsp_pos( + doc.text(), + doc.selection(view.id).cursor(doc.text().slice(..)), + offset_encoding, + ); // TODO: handle fails let future = language_server.goto_definition(doc.identifier(), pos, None); @@ -2392,7 +2522,11 @@ fn goto_type_definition(cx: &mut Context) { let offset_encoding = language_server.offset_encoding(); - let pos = pos_to_lsp_pos(doc.text(), doc.selection(view.id).cursor(), offset_encoding); + let pos = pos_to_lsp_pos( + doc.text(), + doc.selection(view.id).cursor(doc.text().slice(..)), + offset_encoding, + ); // TODO: handle fails let future = language_server.goto_type_definition(doc.identifier(), pos, None); @@ -2429,7 +2563,11 @@ fn goto_implementation(cx: &mut Context) { let offset_encoding = language_server.offset_encoding(); - let pos = pos_to_lsp_pos(doc.text(), doc.selection(view.id).cursor(), offset_encoding); + let pos = pos_to_lsp_pos( + doc.text(), + doc.selection(view.id).cursor(doc.text().slice(..)), + offset_encoding, + ); // TODO: handle fails let future = language_server.goto_implementation(doc.identifier(), pos, None); @@ -2466,7 +2604,11 @@ fn goto_reference(cx: &mut Context) { let offset_encoding = language_server.offset_encoding(); - let pos = pos_to_lsp_pos(doc.text(), doc.selection(view.id).cursor(), offset_encoding); + let pos = pos_to_lsp_pos( + doc.text(), + doc.selection(view.id).cursor(doc.text().slice(..)), + offset_encoding, + ); // TODO: handle fails let future = language_server.goto_reference(doc.identifier(), pos, None); @@ -2525,7 +2667,7 @@ fn goto_next_diag(cx: &mut Context) { let editor = &mut cx.editor; let (view, doc) = current!(editor); - let cursor_pos = doc.selection(view.id).cursor(); + let cursor_pos = doc.selection(view.id).cursor(doc.text().slice(..)); let diag = if let Some(diag) = doc .diagnostics() .iter() @@ -2546,7 +2688,7 @@ fn goto_prev_diag(cx: &mut Context) { let editor = &mut cx.editor; let (view, doc) = current!(editor); - let cursor_pos = doc.selection(view.id).cursor(); + let cursor_pos = doc.selection(view.id).cursor(doc.text().slice(..)); let diag = if let Some(diag) = doc .diagnostics() .iter() @@ -2574,7 +2716,7 @@ fn signature_help(cx: &mut Context) { let pos = pos_to_lsp_pos( doc.text(), - doc.selection(view.id).cursor(), + doc.selection(view.id).cursor(doc.text().slice(..)), language_server.offset_encoding(), ); @@ -2826,11 +2968,13 @@ pub mod insert { pub fn delete_word_backward(cx: &mut Context) { let count = cx.count(); let (view, doc) = current!(cx.editor); - let text = doc.text().slice(..); - let selection = doc - .selection(view.id) - .transform(|range| movement::move_prev_word_start(text, range, count)); - doc.set_selection(view.id, selection); + + doc.set_selection( + view.id, + doc.selection(view.id).clone().transform(|range| { + movement::move_prev_word_start(doc.text().slice(..), range, count) + }), + ); delete_selection(cx) } } @@ -2953,17 +3097,21 @@ fn paste_impl( let mut values = values.iter().cloned().map(Tendril::from).chain(repeat); let text = doc.text(); + let selection = doc.selection(view.id).clone().min_width_1(text.slice(..)); - let transaction = Transaction::change_by_selection(text, doc.selection(view.id), |range| { + let transaction = Transaction::change_by_selection(text, &selection, |range| { let pos = match (action, linewise) { // paste linewise before (Paste::Before, true) => text.line_to_char(text.char_to_line(range.from())), // paste linewise after - (Paste::After, true) => text.line_to_char(text.char_to_line(range.to()) + 1), + (Paste::After, true) => { + let line = range.line_range(text.slice(..)).1; + text.line_to_char((line + 1).min(text.len_lines())) + } // paste insert (Paste::Before, false) => range.from(), // paste append - (Paste::After, false) => range.to() + 1, + (Paste::After, false) => range.to(), }; (pos, pos, Some(values.next().unwrap())) }); @@ -3004,12 +3152,17 @@ fn replace_with_yanked(cx: &mut Context) { if let Some(values) = registers.read(reg_name) { if let Some(yank) = values.first() { - let transaction = - Transaction::change_by_selection(doc.text(), doc.selection(view.id), |range| { - let max_to = rope_end_without_line_ending(&doc.text().slice(..)); - let to = std::cmp::min(max_to, range.to() + 1); - (range.from(), to, Some(yank.as_str().into())) - }); + let selection = doc + .selection(view.id) + .clone() + .min_width_1(doc.text().slice(..)); + let transaction = Transaction::change_by_selection(doc.text(), &selection, |range| { + if !range.is_empty() { + (range.from(), range.to(), Some(yank.as_str().into())) + } else { + (range.from(), range.to(), None) + } + }); doc.apply(&transaction, view.id); doc.append_changes_to_history(view.id); @@ -3022,12 +3175,13 @@ fn replace_selections_with_clipboard_impl(editor: &mut Editor) -> anyhow::Result match editor.clipboard_provider.get_contents() { Ok(contents) => { - let transaction = - Transaction::change_by_selection(doc.text(), doc.selection(view.id), |range| { - let max_to = rope_end_without_line_ending(&doc.text().slice(..)); - let to = std::cmp::min(max_to, range.to() + 1); - (range.from(), to, Some(contents.as_str().into())) - }); + let selection = doc + .selection(view.id) + .clone() + .min_width_1(doc.text().slice(..)); + let transaction = Transaction::change_by_selection(doc.text(), &selection, |range| { + (range.from(), range.to(), Some(contents.as_str().into())) + }); doc.apply(&transaction, view.id); doc.append_changes_to_history(view.id); @@ -3303,13 +3457,14 @@ fn completion(cx: &mut Context) { }; let offset_encoding = language_server.offset_encoding(); + let cursor = doc.selection(view.id).cursor(doc.text().slice(..)); - let pos = pos_to_lsp_pos(doc.text(), doc.selection(view.id).cursor(), offset_encoding); + let pos = pos_to_lsp_pos(doc.text(), cursor, offset_encoding); // TODO: handle fails let future = language_server.completion(doc.identifier(), pos, None); - let trigger_offset = doc.selection(view.id).cursor(); + let trigger_offset = cursor; cx.callback( future, @@ -3359,7 +3514,7 @@ fn hover(cx: &mut Context) { let pos = pos_to_lsp_pos( doc.text(), - doc.selection(view.id).cursor(), + doc.selection(view.id).cursor(doc.text().slice(..)), language_server.offset_encoding(), ); @@ -3425,7 +3580,7 @@ fn match_brackets(cx: &mut Context) { let (view, doc) = current!(cx.editor); if let Some(syntax) = doc.syntax() { - let pos = doc.selection(view.id).cursor(); + let pos = doc.selection(view.id).cursor(doc.text().slice(..)); if let Some(pos) = match_brackets::find(syntax, doc.text(), pos) { let selection = Selection::point(pos); doc.set_selection(view.id, selection); @@ -3529,7 +3684,7 @@ fn align_view_bottom(cx: &mut Context) { fn align_view_middle(cx: &mut Context) { let (view, doc) = current!(cx.editor); - let pos = doc.selection(view.id).cursor(); + let pos = doc.selection(view.id).cursor(doc.text().slice(..)); let pos = coords_at_pos(doc.text().slice(..), pos); const OFFSET: usize = 7; // gutters @@ -3563,20 +3718,21 @@ fn select_textobject(cx: &mut Context, objtype: textobject::TextObject) { } = event { let (view, doc) = current!(cx.editor); - let text = doc.text().slice(..); - let selection = doc.selection(view.id).transform(|range| { - match ch { - 'w' => textobject::textobject_word(text, range, objtype, count), - // TODO: cancel new ranges if inconsistent surround matches across lines - ch if !ch.is_ascii_alphanumeric() => { - textobject::textobject_surround(text, range, objtype, ch, count) + doc.set_selection( + view.id, + doc.selection(view.id).clone().transform(|range| { + let text = doc.text().slice(..); + match ch { + 'w' => textobject::textobject_word(text, range, objtype, count), + // TODO: cancel new ranges if inconsistent surround matches across lines + ch if !ch.is_ascii_alphanumeric() => { + textobject::textobject_surround(text, range, objtype, ch, count) + } + _ => range, } - _ => range, - } - }); - - doc.set_selection(view.id, selection); + }), + ); } }) } @@ -3590,17 +3746,13 @@ fn surround_add(cx: &mut Context) { { let (view, doc) = current!(cx.editor); let text = doc.text().slice(..); - let selection = doc.selection(view.id); + let selection = doc.selection(view.id).clone().min_width_1(text); let (open, close) = surround::get_pair(ch); let mut changes = Vec::new(); for range in selection.iter() { - let from = range.from(); - let max_to = rope_end_without_line_ending(&text); - let to = std::cmp::min(range.to() + 1, max_to); - - changes.push((from, from, Some(Tendril::from_char(open)))); - changes.push((to, to, Some(Tendril::from_char(close)))); + changes.push((range.from(), range.from(), Some(Tendril::from_char(open)))); + changes.push((range.to(), range.to(), Some(Tendril::from_char(close)))); } let transaction = Transaction::change(doc.text(), changes.into_iter()); @@ -3626,9 +3778,9 @@ fn surround_replace(cx: &mut Context) { { let (view, doc) = current!(cx.editor); let text = doc.text().slice(..); - let selection = doc.selection(view.id); + let selection = doc.selection(view.id).clone().min_width_1(text); - let change_pos = match surround::get_surround_pos(text, selection, from, count) + let change_pos = match surround::get_surround_pos(text, &selection, from, count) { Some(c) => c, None => return, @@ -3660,9 +3812,9 @@ fn surround_delete(cx: &mut Context) { { let (view, doc) = current!(cx.editor); let text = doc.text().slice(..); - let selection = doc.selection(view.id); + let selection = doc.selection(view.id).clone().min_width_1(text); - let change_pos = match surround::get_surround_pos(text, selection, ch, count) { + let change_pos = match surround::get_surround_pos(text, &selection, ch, count) { Some(c) => c, None => return, }; diff --git a/helix-term/src/ui/completion.rs b/helix-term/src/ui/completion.rs index be6db42c..942a2483 100644 --- a/helix-term/src/ui/completion.rs +++ b/helix-term/src/ui/completion.rs @@ -86,7 +86,7 @@ impl Completion { let item = item.unwrap(); // if more text was entered, remove it - let cursor = doc.selection(view.id).cursor(); + let cursor = doc.selection(view.id).cursor(doc.text().slice(..)); if trigger_offset < cursor { let remove = Transaction::change( doc.text(), @@ -109,7 +109,7 @@ impl Completion { ) } else { let text = item.insert_text.as_ref().unwrap_or(&item.label); - let cursor = doc.selection(view.id).cursor(); + let cursor = doc.selection(view.id).cursor(doc.text().slice(..)); Transaction::change( doc.text(), vec![(cursor, cursor, Some(text.as_str().into()))].into_iter(), @@ -155,7 +155,7 @@ impl Completion { // TODO: hooks should get processed immediately so maybe do it after select!(), before // looping? - let cursor = doc.selection(view.id).cursor(); + let cursor = doc.selection(view.id).cursor(doc.text().slice(..)); if self.trigger_offset <= cursor { let fragment = doc.text().slice(self.trigger_offset..cursor); let text = Cow::from(fragment); @@ -212,7 +212,7 @@ impl Component for Completion { .language() .and_then(|scope| scope.strip_prefix("source.")) .unwrap_or(""); - let cursor_pos = doc.selection(view.id).cursor(); + let cursor_pos = doc.selection(view.id).cursor(doc.text().slice(..)); let cursor_pos = (helix_core::coords_at_pos(doc.text().slice(..), cursor_pos).row - view.first_line) as u16; diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index 9a2fbf57..8e29be6c 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -8,7 +8,7 @@ use crate::{ use helix_core::{ coords_at_pos, - graphemes::{ensure_grapheme_boundary, next_grapheme_boundary}, + graphemes::{ensure_grapheme_boundary_next, next_grapheme_boundary, prev_grapheme_boundary}, syntax::{self, HighlightEvent}, LineEnding, Position, Range, }; @@ -161,8 +161,8 @@ impl EditorView { let highlights = highlights.into_iter().map(|event| match event.unwrap() { // convert byte offsets to char offset HighlightEvent::Source { start, end } => { - let start = ensure_grapheme_boundary(text, text.byte_to_char(start)); - let end = ensure_grapheme_boundary(text, text.byte_to_char(end)); + let start = ensure_grapheme_boundary_next(text, text.byte_to_char(start)); + let end = ensure_grapheme_boundary_next(text, text.byte_to_char(end)); HighlightEvent::Source { start, end } } event => event, @@ -186,21 +186,18 @@ impl EditorView { } .unwrap_or(base_cursor_scope); - let primary_selection_scope = theme - .find_scope_index("ui.selection.primary") - .unwrap_or(selection_scope); - let highlights: Box<dyn Iterator<Item = HighlightEvent>> = if is_focused { - // inject selections as highlight scopes - let mut spans: Vec<(usize, std::ops::Range<usize>)> = Vec::new(); - // TODO: primary + insert mode patching: // (ui.cursor.primary).patch(mode).unwrap_or(cursor) - let primary_cursor_scope = theme .find_scope_index("ui.cursor.primary") .unwrap_or(cursor_scope); + let primary_selection_scope = theme + .find_scope_index("ui.selection.primary") + .unwrap_or(selection_scope); + // inject selections as highlight scopes + let mut spans: Vec<(usize, std::ops::Range<usize>)> = Vec::new(); for (i, range) in selections.iter().enumerate() { let (cursor_scope, selection_scope) = if i == primary_idx { (primary_cursor_scope, primary_selection_scope) @@ -208,24 +205,23 @@ impl EditorView { (cursor_scope, selection_scope) }; - let cursor_end = next_grapheme_boundary(text, range.head); // Used in every case below. - - if range.head == range.anchor { - spans.push((cursor_scope, range.head..cursor_end)); + // Special-case: cursor at end of the rope. + if range.head == range.anchor && range.head == text.len_chars() { + spans.push((cursor_scope, range.head..range.head + 1)); continue; } - let reverse = range.head < range.anchor; - - if reverse { - spans.push((cursor_scope, range.head..cursor_end)); - spans.push(( - selection_scope, - cursor_end..next_grapheme_boundary(text, range.anchor), - )); + let range = range.min_width_1(text); + if range.head > range.anchor { + // Standard case. + let cursor_start = prev_grapheme_boundary(text, range.head); + spans.push((selection_scope, range.anchor..cursor_start)); + spans.push((cursor_scope, cursor_start..range.head)); } else { - spans.push((selection_scope, range.anchor..range.head)); + // Reverse case. + let cursor_end = next_grapheme_boundary(text, range.head); spans.push((cursor_scope, range.head..cursor_end)); + spans.push((selection_scope, cursor_end..range.anchor)); } } @@ -258,7 +254,10 @@ impl EditorView { spans.pop(); } HighlightEvent::Source { start, end } => { - let text = text.slice(start..end); + // `unwrap_or_else` part is for off-the-end indices of + // the rope, to allow cursor highlighting at the end + // of the rope. + let text = text.get_slice(start..end).unwrap_or_else(|| " ".into()); use helix_core::graphemes::{grapheme_width, RopeGraphemes}; @@ -327,7 +326,11 @@ impl EditorView { let info: Style = theme.get("info"); let hint: Style = theme.get("hint"); - for (i, line) in (view.first_line..last_line).enumerate() { + // Whether to draw the line number for the last line of the + // document or not. We only draw it if it's not an empty line. + let draw_last = text.line_to_byte(last_line) < text.len_bytes(); + + for (i, line) in (view.first_line..(last_line + 1)).enumerate() { use helix_core::diagnostic::Severity; if let Some(diagnostic) = doc.diagnostics().iter().find(|d| d.line == line) { surface.set_stringn( @@ -344,11 +347,17 @@ impl EditorView { ); } - // line numbers having selections are rendered differently + // Line numbers having selections are rendered + // differently, further below. + let line_number_text = if line == last_line && !draw_last { + " ~".into() + } else { + format!("{:>5}", line + 1) + }; surface.set_stringn( viewport.x + 1 - OFFSET, viewport.y + i as u16, - format!("{:>5}", line + 1), + line_number_text, 5, linenr, ); @@ -362,19 +371,34 @@ impl EditorView { if is_focused { let screen = { let start = text.line_to_char(view.first_line); - let end = text.line_to_char(last_line + 1); + let end = text.line_to_char(last_line + 1) + 1; // +1 for cursor at end of text. Range::new(start, end) }; let selection = doc.selection(view.id); for selection in selection.iter().filter(|range| range.overlaps(&screen)) { - let head = view.screen_coords_at_pos(doc, text, selection.head); + let head = view.screen_coords_at_pos( + doc, + text, + if selection.head > selection.anchor { + selection.head - 1 + } else { + selection.head + }, + ); if let Some(head) = head { + // Draw line number for selected lines. + let line_number = view.first_line + head.row; + let line_number_text = if line_number == last_line && !draw_last { + " ~".into() + } else { + format!("{:>5}", line_number + 1) + }; surface.set_stringn( viewport.x + 1 - OFFSET, viewport.y + head.row as u16, - format!("{:>5}", view.first_line + head.row + 1), + line_number_text, 5, linenr_select, ); @@ -382,7 +406,7 @@ impl EditorView { // TODO: set cursor position for IME if let Some(syntax) = doc.syntax() { use helix_core::match_brackets; - let pos = doc.selection(view.id).cursor(); + let pos = doc.selection(view.id).cursor(doc.text().slice(..)); let pos = match_brackets::find(syntax, doc.text(), pos) .and_then(|pos| view.screen_coords_at_pos(doc, text, pos)); @@ -427,7 +451,7 @@ impl EditorView { widgets::{Paragraph, Widget}, }; - let cursor = doc.selection(view.id).cursor(); + let cursor = doc.selection(view.id).cursor(doc.text().slice(..)); let diagnostics = doc.diagnostics().iter().filter(|diagnostic| { diagnostic.range.start <= cursor && diagnostic.range.end >= cursor @@ -539,7 +563,10 @@ impl EditorView { // _ => "indent:ERROR", // }; let position_info = { - let pos = coords_at_pos(doc.text().slice(..), doc.selection(view.id).cursor()); + let pos = coords_at_pos( + doc.text().slice(..), + doc.selection(view.id).cursor(doc.text().slice(..)), + ); format!("{}:{}", pos.row + 1, pos.col + 1) // convert to 1-indexing }; |