From 2c9b02039bac81cb32309bd0d4e2b08191356b9c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= <blaz@mxxn.io>
Date: Fri, 26 Feb 2021 17:21:59 +0900
Subject: [PATCH] commands: Implement join_selections.

---
 helix-core/src/state.rs    |  6 +++--
 helix-term/src/commands.rs | 45 ++++++++++++++++++++++++++++++++++++++
 helix-term/src/keymap.rs   | 18 ++++++++++++---
 3 files changed, 64 insertions(+), 5 deletions(-)

diff --git a/helix-core/src/state.rs b/helix-core/src/state.rs
index 90e75eb4..55ca0673 100644
--- a/helix-core/src/state.rs
+++ b/helix-core/src/state.rs
@@ -292,7 +292,8 @@ fn categorize(ch: char) -> Category {
     }
 }
 
-fn skip_over_next<F>(slice: RopeSlice, pos: &mut usize, fun: F)
+#[inline]
+pub fn skip_over_next<F>(slice: RopeSlice, pos: &mut usize, fun: F)
 where
     F: Fn(char) -> bool,
 {
@@ -306,7 +307,8 @@ where
     }
 }
 
-fn skip_over_prev<F>(slice: RopeSlice, pos: &mut usize, fun: F)
+#[inline]
+pub fn skip_over_prev<F>(slice: RopeSlice, pos: &mut usize, fun: F)
 where
     F: Fn(char) -> bool,
 {
diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs
index 898981ae..07e71a70 100644
--- a/helix-term/src/commands.rs
+++ b/helix-term/src/commands.rs
@@ -308,6 +308,7 @@ pub fn select_regex(cx: &mut Context) {
     let prompt = ui::regex_prompt(cx, "select:".to_string(), |doc, regex| {
         let text = doc.text().slice(..);
         // TODO: if select on matches returns empty range, we need to abort
+        // if regex empty or no matches, return
         let selection =
             selection::select_on_matches(text, doc.selection(), &regex).expect("no matches");
         doc.set_selection(selection);
@@ -929,6 +930,50 @@ pub fn format_selections(cx: &mut Context) {
     doc.append_changes_to_history();
 }
 
+pub fn join_selections(cx: &mut Context) {
+    use helix_core::state::skip_over_next;
+    let doc = cx.doc();
+    let text = doc.text();
+    let slice = doc.text().slice(..);
+
+    let mut changes = Vec::new();
+    let fragment = Tendril::from(" ");
+
+    for selection in doc.selection().ranges() {
+        let start = text.char_to_line(selection.from());
+        let mut end = text.char_to_line(selection.to());
+        if start == end {
+            end += 1
+        }
+        let lines = start..end;
+
+        changes.reserve(lines.len());
+
+        for line in lines {
+            let mut start = text.line_to_char(line + 1).saturating_sub(1);
+            let mut end = start + 1;
+            skip_over_next(slice, &mut end, |ch| matches!(ch, ' ' | '\t'));
+
+            // need to skip from start, not end
+            let change = (start, end, Some(fragment.clone()));
+            changes.push(change);
+        }
+    }
+
+    changes.sort_unstable_by_key(|(from, _to, _text)| *from);
+    changes.dedup();
+
+    // TODO: joining multiple empty lines should be replaced by a single space.
+    // need to merge change ranges that touch
+
+    let transaction = Transaction::change(&doc.state, changes.into_iter());
+    // TODO: select inserted spaces
+    // .with_selection(selection);
+
+    doc.apply(&transaction);
+    doc.append_changes_to_history();
+}
+
 //
 
 pub fn save(cx: &mut Context) {
diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs
index 589740fc..e684a9ff 100644
--- a/helix-term/src/keymap.rs
+++ b/helix-term/src/keymap.rs
@@ -138,28 +138,35 @@ pub fn default() -> Keymaps {
                 vec![key!('h')] => commands::move_char_left as Command,
                 vec![key!('j')] => commands::move_line_down,
                 vec![key!('k')] => commands::move_line_up,
+                vec![key!('l')] => commands::move_char_right,
+
                 vec![key!('0')] => commands::move_line_start,
                 vec![key!('$')] => commands::move_line_end,
-                vec![key!('l')] => commands::move_char_right,
+
                 vec![shift!('H')] => commands::extend_char_left,
                 vec![shift!('J')] => commands::extend_line_down,
                 vec![shift!('K')] => commands::extend_line_up,
                 vec![shift!('L')] => commands::extend_char_right,
+
                 vec![key!('w')] => commands::move_next_word_start,
                 vec![shift!('W')] => commands::extend_next_word_start,
                 vec![key!('b')] => commands::move_prev_word_start,
                 vec![shift!('B')] => commands::extend_prev_word_start,
                 vec![key!('e')] => commands::move_next_word_end,
                 vec![key!('E')] => commands::extend_next_word_end,
-                // TODO: E
+
                 vec![key!('g')] => commands::goto_mode,
+                vec![key!(':')] => commands::command_mode,
+
                 vec![key!('i')] => commands::insert_mode,
                 vec![shift!('I')] => commands::prepend_to_line,
                 vec![key!('a')] => commands::append_mode,
                 vec![shift!('A')] => commands::append_to_line,
                 vec![key!('o')] => commands::open_below,
+
                 vec![key!('d')] => commands::delete_selection,
                 vec![key!('c')] => commands::change_selection,
+
                 vec![key!('s')] => commands::select_regex,
                 vec![alt!('s')] => commands::split_selection_on_newline,
                 vec![shift!('S')] => commands::split_selection,
@@ -167,19 +174,24 @@ pub fn default() -> Keymaps {
                 vec![alt!(';')] => commands::flip_selections,
                 vec![key!('%')] => commands::select_all,
                 vec![key!('x')] => commands::select_line,
+
                 // TODO: figure out what key to use
                 vec![key!('[')] => commands::expand_selection,
+
                 vec![key!('/')] => commands::search,
                 vec![key!('n')] => commands::search_next,
                 vec![key!('*')] => commands::search_selection,
+
                 vec![key!('u')] => commands::undo,
                 vec![shift!('U')] => commands::redo,
+
                 vec![key!('y')] => commands::yank,
                 vec![key!('p')] => commands::paste,
+
                 vec![key!('>')] => commands::indent,
                 vec![key!('<')] => commands::unindent,
                 vec![key!('=')] => commands::format_selections,
-                vec![key!(':')] => commands::command_mode,
+                vec![ctrl!('j')] => commands::join_selections,
                 vec![Key {
                     code: KeyCode::Esc,
                     modifiers: Modifiers::NONE