diff options
Diffstat (limited to '0003-Add-support-for-moving-lines.patch')
-rw-r--r-- | 0003-Add-support-for-moving-lines.patch | 519 |
1 files changed, 519 insertions, 0 deletions
diff --git a/0003-Add-support-for-moving-lines.patch b/0003-Add-support-for-moving-lines.patch new file mode 100644 index 00000000..afa15cf2 --- /dev/null +++ b/0003-Add-support-for-moving-lines.patch @@ -0,0 +1,519 @@ +From a46db95dc5cdae4deca31b8e021805768d1a8390 Mon Sep 17 00:00:00 2001 +From: JJ <git@toki.la> +Date: Sat, 15 Jul 2023 17:55:37 -0700 +Subject: [PATCH 1/2] Add support for moving lines and selections above and + below + +ref: https://github.com/helix-editor/helix/pull/4545 +--- + helix-term/src/commands.rs | 213 ++++++++++++++++++++++++++++++- + helix-term/src/keymap/default.rs | 2 + + 2 files changed, 214 insertions(+), 1 deletion(-) + +diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs +index 58c17296..dc8b5b5c 100644 +--- a/helix-term/src/commands.rs ++++ b/helix-term/src/commands.rs +@@ -28,7 +28,7 @@ + textobject, + tree_sitter::Node, + unicode::width::UnicodeWidthChar, +- visual_offset_from_block, Deletion, LineEnding, Position, Range, Rope, RopeGraphemes, ++ visual_offset_from_block, Change, Deletion, LineEnding, Position, Range, Rope, RopeGraphemes, + RopeReader, RopeSlice, Selection, SmallVec, Tendril, Transaction, + }; + use helix_view::{ +@@ -326,6 +326,8 @@ pub fn doc(&self) -> &str { + goto_declaration, "Goto declaration", + add_newline_above, "Add newline above", + add_newline_below, "Add newline below", ++ move_selection_above, "Move current line or selection up", ++ move_selection_below, "Move current line or selection down", + goto_type_definition, "Goto type definition", + goto_implementation, "Goto implementation", + goto_file_start, "Goto line number <n> else file start", +@@ -5510,6 +5512,215 @@ fn add_newline_impl(cx: &mut Context, open: Open) { + doc.apply(&transaction, view.id); + } + ++#[derive(Debug, PartialEq, Eq)] ++pub enum MoveSelection { ++ Below, ++ Above, ++} ++ ++type ExtendedChange = (usize, usize, Option<Tendril>, Option<(usize, usize)>); ++ ++/// Move line or block of text in specified direction. ++/// The function respects single line, single selection, multiple lines using ++/// several cursors and multiple selections. ++fn move_selection(cx: &mut Context, direction: MoveSelection) { ++ let (view, doc) = current!(cx.editor); ++ let selection = doc.selection(view.id); ++ let text = doc.text(); ++ let slice = text.slice(..); ++ let mut last_step_changes: Vec<ExtendedChange> = vec![]; ++ let mut at_doc_edge = false; ++ let all_changes = selection.into_iter().map(|range| { ++ let (start, end) = range.line_range(slice); ++ let line_start = text.line_to_char(start); ++ let line_end = line_end_char_index(&slice, end); ++ let line = text.slice(line_start..line_end).to_string(); ++ ++ let next_line = match direction { ++ MoveSelection::Above => start.saturating_sub(1), ++ MoveSelection::Below => end + 1, ++ }; ++ ++ let rel_pos_anchor = range.anchor - line_start; ++ let rel_pos_head = range.head - line_start; ++ ++ if next_line == start || next_line >= text.len_lines() || at_doc_edge { ++ at_doc_edge = true; ++ let cursor_rel_pos = (rel_pos_anchor, rel_pos_head); ++ let changes = vec![( ++ line_start, ++ line_end, ++ Some(line.into()), ++ Some(cursor_rel_pos), ++ )]; ++ last_step_changes = changes.clone(); ++ changes ++ } else { ++ let next_line_start = text.line_to_char(next_line); ++ let next_line_end = line_end_char_index(&slice, next_line); ++ let next_line_text = text.slice(next_line_start..next_line_end).to_string(); ++ ++ let cursor_rel_pos = (rel_pos_anchor, rel_pos_head); ++ let changes = match direction { ++ MoveSelection::Above => vec![ ++ ( ++ next_line_start, ++ next_line_end, ++ Some(line.into()), ++ Some(cursor_rel_pos), ++ ), ++ (line_start, line_end, Some(next_line_text.into()), None), ++ ], ++ MoveSelection::Below => vec![ ++ (line_start, line_end, Some(next_line_text.into()), None), ++ ( ++ next_line_start, ++ next_line_end, ++ Some(line.into()), ++ Some(cursor_rel_pos), ++ ), ++ ], ++ }; ++ ++ let changes = if last_step_changes.len() > 1 { ++ evaluate_changes(last_step_changes.clone(), changes.clone(), &direction) ++ } else { ++ changes ++ }; ++ last_step_changes = changes.clone(); ++ changes ++ } ++ }); ++ ++ /// Merge changes from subsequent cursors ++ fn evaluate_changes( ++ mut last_changes: Vec<ExtendedChange>, ++ current_changes: Vec<ExtendedChange>, ++ direction: &MoveSelection, ++ ) -> Vec<ExtendedChange> { ++ let mut current_it = current_changes.into_iter(); ++ ++ if let (Some(mut last), Some(mut current_first), Some(current_last)) = ++ (last_changes.pop(), current_it.next(), current_it.next()) ++ { ++ if last.0 == current_first.0 { ++ match direction { ++ MoveSelection::Above => { ++ last.0 = current_last.0; ++ last.1 = current_last.1; ++ if let Some(first) = last_changes.pop() { ++ last_changes.push(first) ++ } ++ last_changes.extend(vec![current_first, last.to_owned()]); ++ last_changes ++ } ++ MoveSelection::Below => { ++ current_first.0 = last_changes[0].0; ++ current_first.1 = last_changes[0].1; ++ last_changes[0] = current_first; ++ last_changes.extend(vec![last.to_owned(), current_last]); ++ last_changes ++ } ++ } ++ } else { ++ if let Some(first) = last_changes.pop() { ++ last_changes.push(first) ++ } ++ last_changes.extend(vec![last.to_owned(), current_first, current_last]); ++ last_changes ++ } ++ } else { ++ last_changes ++ } ++ } ++ ++ let mut flattened: Vec<Vec<ExtendedChange>> = all_changes.into_iter().collect(); ++ let last_changes = flattened.pop().unwrap_or(vec![]); ++ ++ let acc_cursors = get_adjusted_selection(&doc, &last_changes, direction, at_doc_edge); ++ ++ let changes: Vec<Change> = last_changes ++ .into_iter() ++ .map(|change| (change.0, change.1, change.2.to_owned())) ++ .collect(); ++ ++ let new_sel = Selection::new(acc_cursors.into(), 0); ++ let transaction = Transaction::change(doc.text(), changes.into_iter()); ++ ++ doc.apply(&transaction, view.id); ++ doc.set_selection(view.id, new_sel); ++} ++ ++/// Returns selection range that is valid for the updated document ++/// This logic is necessary because it's not possible to apply changes ++/// to the document first and then set selection. ++fn get_adjusted_selection( ++ doc: &Document, ++ last_changes: &Vec<ExtendedChange>, ++ direction: MoveSelection, ++ at_doc_edge: bool, ++) -> Vec<Range> { ++ let mut first_change_len = 0; ++ let mut next_start = 0; ++ let mut acc_cursors: Vec<Range> = vec![]; ++ ++ for change in last_changes.iter() { ++ let change_len = change.2.as_ref().map_or(0, |x| x.len()); ++ ++ if let Some((rel_anchor, rel_head)) = change.3 { ++ let (anchor, head) = if at_doc_edge { ++ let anchor = change.0 + rel_anchor; ++ let head = change.0 + rel_head; ++ (anchor, head) ++ } else { ++ match direction { ++ MoveSelection::Above => { ++ if next_start == 0 { ++ next_start = change.0; ++ } ++ let anchor = next_start + rel_anchor; ++ let head = next_start + rel_head; ++ ++ // If there is next cursor below, selection position should be adjusted ++ // according to the length of the current line. ++ next_start += change_len + doc.line_ending.len_chars(); ++ (anchor, head) ++ } ++ MoveSelection::Below => { ++ let anchor = change.0 + first_change_len + rel_anchor - change_len; ++ let head = change.0 + first_change_len + rel_head - change_len; ++ (anchor, head) ++ } ++ } ++ }; ++ ++ let cursor = Range::new(anchor, head); ++ if let Some(last) = acc_cursors.pop() { ++ if cursor.overlaps(&last) { ++ acc_cursors.push(last); ++ } else { ++ acc_cursors.push(last); ++ acc_cursors.push(cursor); ++ }; ++ } else { ++ acc_cursors.push(cursor); ++ }; ++ } else { ++ first_change_len = change.2.as_ref().map_or(0, |x| x.len()); ++ next_start = 0; ++ }; ++ } ++ acc_cursors ++} ++ ++fn move_selection_below(cx: &mut Context) { ++ move_selection(cx, MoveSelection::Below) ++} ++ ++fn move_selection_above(cx: &mut Context) { ++ move_selection(cx, MoveSelection::Above) ++} ++ + enum IncrementDirection { + Increase, + Decrease, +diff --git a/helix-term/src/keymap/default.rs b/helix-term/src/keymap/default.rs +index c84c616c..f384c868 100644 +--- a/helix-term/src/keymap/default.rs ++++ b/helix-term/src/keymap/default.rs +@@ -321,6 +321,8 @@ pub fn default() -> HashMap<Mode, KeyTrie> { + + "C-a" => increment, + "C-x" => decrement, ++ "C-k" => move_selection_above, ++ "C-j" => move_selection_below, + }); + let mut select = normal.clone(); + select.merge_nodes(keymap!({ "Select mode" +-- +2.41.0 + + +From 3888d20b74bf8a723af276973485493231ab0f85 Mon Sep 17 00:00:00 2001 +From: JJ <git@toki.la> +Date: Sat, 15 Jul 2023 17:55:58 -0700 +Subject: [PATCH 2/2] Unit test moving lines and selections + +--- + helix-term/tests/test/commands.rs | 186 ++++++++++++++++++++++++++++-- + 1 file changed, 176 insertions(+), 10 deletions(-) + +diff --git a/helix-term/tests/test/commands.rs b/helix-term/tests/test/commands.rs +index b13c37bc..4ca7e9f2 100644 +--- a/helix-term/tests/test/commands.rs ++++ b/helix-term/tests/test/commands.rs +@@ -96,6 +96,172 @@ async fn test_selection_duplication() -> anyhow::Result<()> { + Ok(()) + } + ++// Line selection movement tests ++ ++#[tokio::test(flavor = "multi_thread")] ++async fn test_move_selection_single_selection_up() -> anyhow::Result<()> { ++ test(( ++ platform_line(indoc! {" ++ aaaaaa ++ bbbbbb ++ cc#[|c]#ccc ++ dddddd ++ "}) ++ .as_str(), ++ "<C-k>", ++ platform_line(indoc! {" ++ aaaaaa ++ cc#[|c]#ccc ++ bbbbbb ++ dddddd ++ "}) ++ .as_str(), ++ )) ++ .await?; ++ ++ Ok(()) ++} ++ ++#[tokio::test(flavor = "multi_thread")] ++async fn test_move_selection_single_selection_down() -> anyhow::Result<()> { ++ test(( ++ platform_line(indoc! {" ++ aa#[|a]#aaa ++ bbbbbb ++ cccccc ++ dddddd ++ "}) ++ .as_str(), ++ "<C-j>", ++ platform_line(indoc! {" ++ bbbbbb ++ aa#[|a]#aaa ++ cccccc ++ dddddd ++ "}) ++ .as_str(), ++ )) ++ .await?; ++ ++ Ok(()) ++} ++ ++#[tokio::test(flavor = "multi_thread")] ++async fn test_move_selection_single_selection_top_up() -> anyhow::Result<()> { ++ // if already on top of the file and going up, nothing should change ++ test(( ++ platform_line(indoc! {" ++ aa#[|a]#aaa ++ bbbbbb ++ cccccc ++ dddddd"}) ++ .as_str(), ++ "<C-k>", ++ platform_line(indoc! {" ++ aa#[|a]#aaa ++ bbbbbb ++ cccccc ++ dddddd"}) ++ .as_str(), ++ )) ++ .await?; ++ ++ Ok(()) ++} ++ ++#[tokio::test(flavor = "multi_thread")] ++async fn test_move_selection_single_selection_bottom_down() -> anyhow::Result<()> { ++ // If going down on the bottom line, nothing should change ++ // Note that platform_line is not used here, because it inserts trailing ++ // linebreak, making it impossible to test ++ test(( ++ "aaaaaa\nbbbbbb\ncccccc\ndd#[|d]#ddd", ++ "<C-j><C-j>", ++ "aaaaaa\nbbbbbb\ncccccc\ndd#[|d]#ddd", ++ )) ++ .await?; ++ ++ Ok(()) ++} ++ ++#[tokio::test(flavor = "multi_thread")] ++async fn test_move_selection_block_up() -> anyhow::Result<()> { ++ test(( ++ platform_line(indoc! {" ++ aaaaaa ++ bb#[bbbb ++ ccc|]#ccc ++ dddddd ++ eeeeee ++ "}) ++ .as_str(), ++ "<C-k>", ++ platform_line(indoc! {" ++ bb#[bbbb ++ ccc|]#ccc ++ aaaaaa ++ dddddd ++ eeeeee ++ "}) ++ .as_str(), ++ )) ++ .await?; ++ ++ Ok(()) ++} ++ ++#[tokio::test(flavor = "multi_thread")] ++async fn test_move_selection_block_down() -> anyhow::Result<()> { ++ test(( ++ platform_line(indoc! {" ++ #[|aaaaaa ++ bbbbbb ++ ccc]#ccc ++ dddddd ++ eeeeee ++ "}) ++ .as_str(), ++ "<C-j>", ++ platform_line(indoc! {" ++ dddddd ++ #[|aaaaaa ++ bbbbbb ++ ccc]#ccc ++ eeeeee ++ "}) ++ .as_str(), ++ )) ++ .await?; ++ ++ Ok(()) ++} ++ ++#[tokio::test(flavor = "multi_thread")] ++async fn test_move_two_cursors_down() -> anyhow::Result<()> { ++ test(( ++ platform_line(indoc! {" ++ aaaaaa ++ bb#[|b]#bbb ++ cccccc ++ d#(dd|)#ddd ++ eeeeee ++ "}) ++ .as_str(), ++ "<C-j>", ++ platform_line(indoc! {" ++ aaaaaa ++ cccccc ++ bb#[|b]#bbb ++ eeeeee ++ d#(dd|)#ddd ++ "}) ++ .as_str(), ++ )) ++ .await?; ++ ++ Ok(()) ++} ++ + #[tokio::test(flavor = "multi_thread")] + async fn test_goto_file_impl() -> anyhow::Result<()> { + let file = tempfile::NamedTempFile::new()?; +@@ -187,11 +353,11 @@ async fn test_multi_selection_shell_commands() -> anyhow::Result<()> { + "|echo foo<ret>", + platform_line(indoc! {"\ + #[|foo\n]# +- ++ + #(|foo\n)# +- ++ + #(|foo\n)# +- ++ + "}), + )) + .await?; +@@ -225,11 +391,11 @@ async fn test_multi_selection_shell_commands() -> anyhow::Result<()> { + "<A-!>echo foo<ret>", + platform_line(indoc! {"\ + lorem#[|foo\n]# +- ++ + ipsum#(|foo\n)# +- ++ + dolor#(|foo\n)# +- ++ + "}), + )) + .await?; +@@ -273,14 +439,14 @@ async fn test_extend_line() -> anyhow::Result<()> { + #[l|]#orem + ipsum + dolor +- ++ + "}), + "x2x", + platform_line(indoc! {"\ + #[lorem + ipsum + dolor\n|]# +- ++ + "}), + )) + .await?; +@@ -290,13 +456,13 @@ async fn test_extend_line() -> anyhow::Result<()> { + platform_line(indoc! {"\ + #[l|]#orem + ipsum +- ++ + "}), + "2x", + platform_line(indoc! {"\ + #[lorem + ipsum\n|]# +- ++ + "}), + )) + .await?; +-- +2.41.0 + |