From 070607c821242babaf660cda2cf34e14c255be25 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 3 May 2022 14:21:06 +0200 Subject: [PATCH 1/4] Implement `Editor::transpose` without accounting for multi-byte chars --- assets/keymaps/default.json | 1 + crates/editor/src/editor.rs | 115 ++++++++++++++++++++++++++++++++++++ 2 files changed, 116 insertions(+) diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index af47a165f9..1378022bcf 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -33,6 +33,7 @@ "tab": "editor::Tab", "shift-tab": "editor::TabPrev", "ctrl-k": "editor::CutToEndOfLine", + "ctrl-t": "editor::Transpose", "cmd-backspace": "editor::DeleteToBeginningOfLine", "cmd-delete": "editor::DeleteToEndOfLine", "alt-backspace": "editor::DeleteToPreviousWordStart", diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index d099cbef11..3d5a666038 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -136,6 +136,7 @@ actions!( DuplicateLine, MoveLineUp, MoveLineDown, + Transpose, Cut, Copy, Paste, @@ -239,6 +240,7 @@ pub fn init(cx: &mut MutableAppContext) { cx.add_action(Editor::duplicate_line); cx.add_action(Editor::move_line_up); cx.add_action(Editor::move_line_down); + cx.add_action(Editor::transpose); cx.add_action(Editor::cut); cx.add_action(Editor::copy); cx.add_action(Editor::paste); @@ -3382,6 +3384,41 @@ impl Editor { }); } + pub fn transpose(&mut self, _: &Transpose, cx: &mut ViewContext) { + self.transact(cx, |this, cx| { + let mut edits: Vec<(Range, String)> = Default::default(); + this.move_selections(cx, |display_map, selection| { + if !selection.is_empty() { + return; + } + + let mut head = selection.head(); + let mut transpose_offset = head.to_offset(display_map, Bias::Right); + if head.column() == display_map.line_len(head.row()) { + transpose_offset = transpose_offset.saturating_sub(1); + } + + if transpose_offset == 0 { + return; + } + + *head.column_mut() += 1; + head = display_map.clip_point(head, Bias::Right); + selection.collapse_to(head, SelectionGoal::Column(head.column())); + + let transpose_start = transpose_offset.saturating_sub(1); + if edits.last().map_or(true, |e| e.0.end < transpose_start) { + let transpose_end = transpose_offset + 1; + if let Some(ch) = display_map.buffer_snapshot.chars_at(transpose_start).next() { + edits.push((transpose_start..transpose_offset, String::new())); + edits.push((transpose_end..transpose_end, ch.to_string())); + } + } + }); + this.buffer.update(cx, |buffer, cx| buffer.edit(edits, cx)); + }); + } + pub fn cut(&mut self, _: &Cut, cx: &mut ViewContext) { let mut text = String::new(); let mut selections = self.local_selections::(cx); @@ -8211,6 +8248,84 @@ mod tests { }); } + #[gpui::test] + fn test_transpose(cx: &mut gpui::MutableAppContext) { + cx.set_global(Settings::test(cx)); + + cx.add_window(Default::default(), |cx| { + let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), cx); + + editor.select_ranges([1..1], None, cx); + editor.transpose(&Default::default(), cx); + assert_eq!(editor.text(cx), "bac"); + assert_eq!(editor.selected_ranges(cx), [2..2]); + + editor.transpose(&Default::default(), cx); + assert_eq!(editor.text(cx), "bca"); + assert_eq!(editor.selected_ranges(cx), [3..3]); + + editor.transpose(&Default::default(), cx); + assert_eq!(editor.text(cx), "bac"); + assert_eq!(editor.selected_ranges(cx), [3..3]); + + editor + }) + .1; + + cx.add_window(Default::default(), |cx| { + let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), cx); + + editor.select_ranges([3..3], None, cx); + editor.transpose(&Default::default(), cx); + assert_eq!(editor.text(cx), "acb\nde"); + assert_eq!(editor.selected_ranges(cx), [3..3]); + + editor.select_ranges([4..4], None, cx); + editor.transpose(&Default::default(), cx); + assert_eq!(editor.text(cx), "acbd\ne"); + assert_eq!(editor.selected_ranges(cx), [5..5]); + + editor.transpose(&Default::default(), cx); + assert_eq!(editor.text(cx), "acbde\n"); + assert_eq!(editor.selected_ranges(cx), [6..6]); + + editor.transpose(&Default::default(), cx); + assert_eq!(editor.text(cx), "acbd\ne"); + assert_eq!(editor.selected_ranges(cx), [6..6]); + + editor + }) + .1; + + cx.add_window(Default::default(), |cx| { + let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), cx); + + editor.select_ranges([1..1, 2..2, 4..4], None, cx); + editor.transpose(&Default::default(), cx); + assert_eq!(editor.text(cx), "bacd\ne"); + assert_eq!(editor.selected_ranges(cx), [2..2, 3..3, 5..5]); + + editor.transpose(&Default::default(), cx); + assert_eq!(editor.text(cx), "bcade\n"); + assert_eq!(editor.selected_ranges(cx), [3..3, 4..4, 6..6]); + + editor.transpose(&Default::default(), cx); + assert_eq!(editor.text(cx), "bcdae\n"); + assert_eq!(editor.selected_ranges(cx), [4..4, 5..5, 6..6]); + + editor.transpose(&Default::default(), cx); + assert_eq!(editor.text(cx), "bcdea\n"); + assert_eq!(editor.selected_ranges(cx), [5..5, 6..6]); + + editor.transpose(&Default::default(), cx); + assert_eq!(editor.text(cx), "bcdae\n"); + assert_eq!(editor.selected_ranges(cx), [5..5, 6..6]); + + editor + }) + .1; + } + #[gpui::test] fn test_clipboard(cx: &mut gpui::MutableAppContext) { cx.set_global(Settings::test(cx)); From b6ff07afac8e13d18c4fb6590ee4b9cd83f80938 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 3 May 2022 14:33:57 +0200 Subject: [PATCH 2/4] Add failing test for multi-byte characters --- crates/editor/src/editor.rs | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 3d5a666038..20955153e5 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -8324,6 +8324,26 @@ mod tests { editor }) .1; + + cx.add_window(Default::default(), |cx| { + let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), cx); + + editor.select_ranges([4..4], None, cx); + editor.transpose(&Default::default(), cx); + assert_eq!(editor.text(cx), "🏀🍐✋"); + assert_eq!(editor.selected_ranges(cx), [8..8]); + + editor.transpose(&Default::default(), cx); + assert_eq!(editor.text(cx), "🏀✋🍐"); + assert_eq!(editor.selected_ranges(cx), [11..11]); + + editor.transpose(&Default::default(), cx); + assert_eq!(editor.text(cx), "🏀🍐✋"); + assert_eq!(editor.selected_ranges(cx), [11..11]); + + editor + }) + .1; } #[gpui::test] From 95680aa5f2b8c8cfd2de182300c681ebeee75357 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 3 May 2022 14:35:03 +0200 Subject: [PATCH 3/4] Account for multi-byte characters in `Editor::transpose` --- crates/editor/src/editor.rs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 20955153e5..17ccad9849 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -3395,7 +3395,9 @@ impl Editor { let mut head = selection.head(); let mut transpose_offset = head.to_offset(display_map, Bias::Right); if head.column() == display_map.line_len(head.row()) { - transpose_offset = transpose_offset.saturating_sub(1); + transpose_offset = display_map + .buffer_snapshot + .clip_offset(transpose_offset.saturating_sub(1), Bias::Left); } if transpose_offset == 0 { @@ -3406,9 +3408,13 @@ impl Editor { head = display_map.clip_point(head, Bias::Right); selection.collapse_to(head, SelectionGoal::Column(head.column())); - let transpose_start = transpose_offset.saturating_sub(1); + let transpose_start = display_map + .buffer_snapshot + .clip_offset(transpose_offset.saturating_sub(1), Bias::Left); if edits.last().map_or(true, |e| e.0.end < transpose_start) { - let transpose_end = transpose_offset + 1; + let transpose_end = display_map + .buffer_snapshot + .clip_offset(transpose_offset + 1, Bias::Right); if let Some(ch) = display_map.buffer_snapshot.chars_at(transpose_start).next() { edits.push((transpose_start..transpose_offset, String::new())); edits.push((transpose_end..transpose_end, ch.to_string())); From 9a7c07f539cb41ed58ad3c1d7e895876f2e81e31 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 4 May 2022 09:59:34 +0200 Subject: [PATCH 4/4] Improve transpose when cursors are two chars away from each other --- crates/editor/src/editor.rs | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 17ccad9849..2c16af59f1 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -3411,7 +3411,7 @@ impl Editor { let transpose_start = display_map .buffer_snapshot .clip_offset(transpose_offset.saturating_sub(1), Bias::Left); - if edits.last().map_or(true, |e| e.0.end < transpose_start) { + if edits.last().map_or(true, |e| e.0.end <= transpose_start) { let transpose_end = display_map .buffer_snapshot .clip_offset(transpose_offset + 1, Bias::Right); @@ -3422,6 +3422,11 @@ impl Editor { } }); this.buffer.update(cx, |buffer, cx| buffer.edit(edits, cx)); + this.update_selections( + this.local_selections::(cx), + Some(Autoscroll::Fit), + cx, + ); }); } @@ -8316,15 +8321,15 @@ mod tests { assert_eq!(editor.selected_ranges(cx), [3..3, 4..4, 6..6]); editor.transpose(&Default::default(), cx); - assert_eq!(editor.text(cx), "bcdae\n"); - assert_eq!(editor.selected_ranges(cx), [4..4, 5..5, 6..6]); + assert_eq!(editor.text(cx), "bcda\ne"); + assert_eq!(editor.selected_ranges(cx), [4..4, 6..6]); editor.transpose(&Default::default(), cx); - assert_eq!(editor.text(cx), "bcdea\n"); - assert_eq!(editor.selected_ranges(cx), [5..5, 6..6]); + assert_eq!(editor.text(cx), "bcade\n"); + assert_eq!(editor.selected_ranges(cx), [4..4, 6..6]); editor.transpose(&Default::default(), cx); - assert_eq!(editor.text(cx), "bcdae\n"); + assert_eq!(editor.text(cx), "bcaed\n"); assert_eq!(editor.selected_ranges(cx), [5..5, 6..6]); editor