on master help sign in
don't move cursor while forward deleting in append mode

Currently, when forward deleting (`delete_char_forward` bound to `del`, `delete_word_forward`, `kill_to_line_end`) the cursor is moved to the left in append mode (or generally when the cursor is at the end of the selection). For example in a document `|abc|def` (|indicates selection) if enter append mode the cursor is moved to `c` and the selection becomes: `|abcd|ef`. When deleting forward (`del`) `d` is deleted. The expectation would be that the selection doesn't shrink so that `del` again deletes `e` and then `f`. This would look as follows: `|abcd|ef` `|abce|f` `|abcf|` `|abc |` This is inline with how other editors like kakoune work. However, helix currently moves the selection backwards leading to the following behavior: `|abcd|ef` `|abc|ef` `|ab|ef` `ef` This means that `delete_char_forward` essentially acts like `delete_char_backward` after deleting the first character in append mode. To fix the problem the cursor must be moved to the right while deleting forward (first fix in this commit). Furthermore, when the EOF char is reached a newline char must be inserted (just like when entering appendmode) to prevent the cursor from moving to the right

commit25d4ebe30d7920bc087f004075048f62f53726af authorPascal Kuthe <pascal.kuthe@semimod.de> dateMon 20 Feb 2023, 16:31 (4 years ago) parent2c3ccc3
3 files changed+124−45
helix-core/src/transaction.rs+5 helix-term/src/commands.rs+96−45 helix-term/tests/test/commands.rs+23
helix-core/src/transaction.rs+5
@@ -570,6 +570,11 @@ impl Transaction {
570570 Self::from(changeset)
571571 }
572572
573+ pub fn insert_at_eof(mut self, text: Tendril) -> Transaction {
574+ self.changes.insert(text);
575+ self
576+ }
577+
573578 /// Generate a transaction with a change per selection range.
574579 pub fn change_by_selection<F>(doc: &Rope, selection: &Selection, f: F) -> Self
575580 where
helix-term/src/commands.rs+96−45
@@ -795,42 +795,50 @@ fn extend_to_line_start(cx: &mut Context) {
795795}
796796
797797fn kill_to_line_start(cx: &mut Context) {
798− delete_by_selection_insert_mode(cx, move |text, range| {
799− let line = range.cursor_line(text);
800− let first_char = text.line_to_char(line);
801− let anchor = range.cursor(text);
802− let head = if anchor == first_char && line != 0 {
803− // select until previous line
804− line_end_char_index(&text, line - 1)
805− } else if let Some(pos) = find_first_non_whitespace_char(text.line(line)) {
806− if first_char + pos < anchor {
807− // select until first non-blank in line if cursor is after it
808− first_char + pos
798+ delete_by_selection_insert_mode(
799+ cx,
800+ move |text, range| {
801+ let line = range.cursor_line(text);
802+ let first_char = text.line_to_char(line);
803+ let anchor = range.cursor(text);
804+ let head = if anchor == first_char && line != 0 {
805+ // select until previous line
806+ line_end_char_index(&text, line - 1)
807+ } else if let Some(pos) = find_first_non_whitespace_char(text.line(line)) {
808+ if first_char + pos < anchor {
809+ // select until first non-blank in line if cursor is after it
810+ first_char + pos
811+ } else {
812+ // select until start of line
813+ first_char
814+ }
809815 } else {
810816 // select until start of line
811817 first_char
812− }
813− } else {
814− // select until start of line
815− first_char
816− };
817− (head, anchor)
818− });
818+ };
819+ (head, anchor)
820+ },
821+ Direction::Backward,
822+ );
819823}
820824
821825fn kill_to_line_end(cx: &mut Context) {
822− delete_by_selection_insert_mode(cx, |text, range| {
823− let line = range.cursor_line(text);
824− let line_end_pos = line_end_char_index(&text, line);
825− let pos = range.cursor(text);
826+ delete_by_selection_insert_mode(
827+ cx,
828+ |text, range| {
829+ let line = range.cursor_line(text);
830+ let line_end_pos = line_end_char_index(&text, line);
831+ let pos = range.cursor(text);
826832
827− // if the cursor is on the newline char delete that
828− if pos == line_end_pos {
829− (pos, text.line_to_char(line + 1))
830− } else {
831− (pos, line_end_pos)
832− }
833− });
833+ // if the cursor is on the newline char delete that
834+ if pos == line_end_pos {
835+ (pos, text.line_to_char(line + 1))
836+ } else {
837+ (pos, line_end_pos)
838+ }
839+ },
840+ Direction::Forward,
841+ );
834842}
835843
836844fn goto_first_nonwhitespace(cx: &mut Context) {
@@ -2322,13 +2330,44 @@ fn delete_selection_impl(cx: &mut Context, op: Operation) {
23222330fn delete_by_selection_insert_mode(
23232331 cx: &mut Context,
23242332 mut f: impl FnMut(RopeSlice, &Range) -> Deletion,
2333+ direction: Direction,
23252334) {
23262335 let (view, doc) = current!(cx.editor);
23272336 let text = doc.text().slice(..);
2328− let transaction =
2337+ let mut selection = SmallVec::new();
2338+ let mut insert_newline = false;
2339+ let text_len = text.len_chars();
2340+ let mut transaction =
23292341 Transaction::delete_by_selection(doc.text(), doc.selection(view.id), |range| {
2330− f(text, range)
2342+ let (start, end) = f(text, range);
2343+ if direction == Direction::Forward {
2344+ let mut range = *range;
2345+ if range.head > range.anchor {
2346+ insert_newline |= end == text_len;
2347+ // move the cursor to the right so that the selection
2348+ // doesn't shrink when deleting forward (so the text appears to
2349+ // move to left)
2350+ // += 1 is enough here as the range is normalized to grapheme boundaries
2351+ // later anyway
2352+ range.head += 1;
2353+ }
2354+ selection.push(range);
2355+ }
2356+ (start, end)
23312357 });
2358+
2359+ // in case we delete the last character and the cursor would be moved to the EOF char
2360+ // insert a newline, just like when entering append mode
2361+ if insert_newline {
2362+ transaction = transaction.insert_at_eof(doc.line_ending.as_str().into());
2363+ }
2364+
2365+ if direction == Direction::Forward {
2366+ doc.set_selection(
2367+ view.id,
2368+ Selection::new(selection, doc.selection(view.id).primary_index()),
2369+ );
2370+ }
23322371 doc.apply(&transaction, view.id);
23332372 lsp::signature_help_impl(cx, SignatureHelpInvoked::Automatic);
23342373}
@@ -3490,28 +3529,40 @@ pub mod insert {
34903529
34913530 pub fn delete_char_forward(cx: &mut Context) {
34923531 let count = cx.count();
3493− delete_by_selection_insert_mode(cx, |text, range| {
3494− let pos = range.cursor(text);
3495− (pos, graphemes::nth_next_grapheme_boundary(text, pos, count))
3496− })
3532+ delete_by_selection_insert_mode(
3533+ cx,
3534+ |text, range| {
3535+ let pos = range.cursor(text);
3536+ (pos, graphemes::nth_next_grapheme_boundary(text, pos, count))
3537+ },
3538+ Direction::Forward,
3539+ )
34973540 }
34983541
34993542 pub fn delete_word_backward(cx: &mut Context) {
35003543 let count = cx.count();
3501− delete_by_selection_insert_mode(cx, |text, range| {
3502− let anchor = movement::move_prev_word_start(text, *range, count).from();
3503− let next = Range::new(anchor, range.cursor(text));
3504− let range = exclude_cursor(text, next, *range);
3505− (range.from(), range.to())
3506− });
3544+ delete_by_selection_insert_mode(
3545+ cx,
3546+ |text, range| {
3547+ let anchor = movement::move_prev_word_start(text, *range, count).from();
3548+ let next = Range::new(anchor, range.cursor(text));
3549+ let range = exclude_cursor(text, next, *range);
3550+ (range.from(), range.to())
3551+ },
3552+ Direction::Backward,
3553+ );
35073554 }
35083555
35093556 pub fn delete_word_forward(cx: &mut Context) {
35103557 let count = cx.count();
3511− delete_by_selection_insert_mode(cx, |text, range| {
3512− let head = movement::move_next_word_end(text, *range, count).to();
3513− (range.cursor(text), head)
3514− });
3558+ delete_by_selection_insert_mode(
3559+ cx,
3560+ |text, range| {
3561+ let head = movement::move_next_word_end(text, *range, count).to();
3562+ (range.cursor(text), head)
3563+ },
3564+ Direction::Forward,
3565+ );
35153566 }
35163567}
35173568
helix-term/tests/test/commands.rs+23
@@ -429,3 +429,26 @@ async fn test_delete_word_forward() -> anyhow::Result<()> {
429429 Ok(())
430430}
431431
432+#[tokio::test(flavor = "multi_thread")]
433+async fn test_delete_char_forward() -> anyhow::Result<()> {
434+ test((
435+ platform_line(indoc! {"\
436+ #[abc|]#def
437+ #(abc|)#ef
438+ #(abc|)#f
439+ #(abc|)#
440+ "})
441+ .as_str(),
442+ "a<del><esc>",
443+ platform_line(indoc! {"\
444+ #[abc|]#ef
445+ #(abc|)#f
446+ #(abc|)#
447+ #(abc|)#
448+ "})
449+ .as_str(),
450+ ))
451+ .await?;
452+
453+ Ok(())
454+}