Repository: ilai-deutel/kibi (language: rust)

Issue:
Ctrl + Arrow should move to previous/next word



Discussion / hints:

Fix patch:
diff --git a/README.md b/README.md
index 0e506408..396bd6a9 100644
--- a/README.md
+++ b/README.md
@@ -201,6 +201,8 @@ kibi --version    # Print version information and exit
 | Ctrl-C            | Copies the entire line                                        |
 | Ctrl-X            | Cuts the entire line                                          |
 | Ctrl-V            | Will paste the copied line                                    |
+| Ctrl-LeftArrow    | Moves cursor to previous word                                 |
+| Ctrl-RightArrow   | Moves cursor to next word                                     |
 
 ### Configuration
 
diff --git a/src/editor.rs b/src/editor.rs
index e9cc605c..b8d4b47f 100644
--- a/src/editor.rs
+++ b/src/editor.rs
@@ -187,19 +187,28 @@ impl Editor {
     fn rx(&self) -> usize { self.current_row().map_or(0, |r| r.cx2rx[self.cursor.x]) }
 
     /// Move the cursor following an arrow key (← → ↑ ↓).
-    fn move_cursor(&mut self, key: &AKey) {
+    fn move_cursor(&mut self, key: &AKey, ctrl: bool) {
+        let mut cursor_x = self.cursor.x;
         match (key, self.current_row()) {
-            (AKey::Left, Some(row)) if self.cursor.x > 0 =>
-                self.cursor.x -= row.get_char_size(row.cx2rx[self.cursor.x] - 1),
-            (AKey::Left, _) if self.cursor.y > 0 => {
-                // ← at the beginning of the line: move to the end of the previous line. The x
-                // position will be adjusted after this `match` to accommodate the current row
-                // length, so we can just set here to the maximum possible value here.
-                self.cursor.y -= 1;
-                self.cursor.x = usize::MAX;
+            (AKey::Left, Some(row)) if self.cursor.x > 0 => {
+                cursor_x -= row.get_char_size(row.cx2rx[cursor_x] - 1);
+                // ← moving to previous word
+                while ctrl && cursor_x > 0 && row.chars[cursor_x - 1] != b' ' {
+                    cursor_x -= row.get_char_size(row.cx2rx[cursor_x] - 1);
+                }
+            }
+            // ← at the beginning of the line: move to the end of the previous line. The x
+            // position will be adjusted after this `match` to accommodate the current row
+            // length, so we can just set here to the maximum possible value here.
+            (AKey::Left, _) if self.cursor.y > 0 =>
+                (self.cursor.y, cursor_x) = (self.cursor.y - 1, usize::MAX),
+            (AKey::Right, Some(row)) if self.cursor.x < row.chars.len() => {
+                cursor_x += row.get_char_size(row.cx2rx[cursor_x]);
+                // → moving to next word
+                while ctrl && cursor_x < row.chars.len() && row.chars[cursor_x] != b' ' {
+                    cursor_x += row.get_char_size(row.cx2rx[cursor_x]);
+                }
             }
-            (AKey::Right, Some(row)) if self.cursor.x < row.chars.len() =>
-                self.cursor.x += row.get_char_size(row.cx2rx[self.cursor.x]),
             (AKey::Right, Some(_)) => self.cursor.move_to_next_line(),
             // TODO: For Up and Down, move self.cursor.x to be consistent with tabs and UTF-8
             //  characters, i.e. according to rx
@@ -207,6 +216,7 @@ impl Editor {
             (AKey::Down, Some(_)) => self.cursor.y += 1,
             _ => (),
         }
+        self.cursor.x = cursor_x;
         self.update_cursor_x_position();
     }
 
@@ -345,8 +355,7 @@ impl Editor {
             self.update_screen_cols();
         }
         self.update_row(self.cursor.y, false);
-        (self.cursor.x, self.n_bytes) = (self.cursor.x + 1, self.n_bytes + 1);
-        self.dirty = true;
+        (self.cursor.x, self.n_bytes, self.dirty) = (self.cursor.x + 1, self.n_bytes + 1, true);
     }
 
     /// Insert a new line at the current cursor position and move the cursor to the start of the new
@@ -393,7 +402,7 @@ impl Editor {
         } else if self.cursor.y == self.rows.len() {
             // If the cursor is located after the last row, pressing backspace is equivalent to
             // pressing the left arrow key.
-            self.move_cursor(&AKey::Left);
+            self.move_cursor(&AKey::Left, false);
         }
     }
 
@@ -600,7 +609,8 @@ impl Editor {
 
         match key {
             // TODO: CtrlArrow should move to next word
-            Key::Arrow(arrow) | Key::CtrlArrow(arrow) => self.move_cursor(arrow),
+            Key::Arrow(arrow) => self.move_cursor(arrow, false),
+            Key::CtrlArrow(arrow) => self.move_cursor(arrow, true),
             Key::Page(PageKey::Up) => {
                 self.cursor.y = self.cursor.roff.saturating_sub(self.screen_rows);
                 self.update_cursor_x_position();
@@ -615,7 +625,7 @@ impl Editor {
             Key::Char(BACKSPACE | DELETE_BIS) => self.delete_char(), // Backspace or Ctrl + H
             Key::Char(REMOVE_LINE) => self.delete_current_row(),
             Key::Delete => {
-                self.move_cursor(&AKey::Right);
+                self.move_cursor(&AKey::Right, false);
                 self.delete_char();
             }
             Key::Escape | Key::Char(REFRESH_SCREEN) => (),
@@ -664,10 +674,9 @@ impl Editor {
             current = (current + if forward { 1 } else { num_rows - 1 }) % num_rows;
             let row = &mut self.rows[current];
             if let Some(cx) = slice_find(&row.chars, query.as_bytes()) {
-                (self.cursor.x, self.cursor.y) = (cx, current);
-                // Try to reset the column offset; if the match is after the offset, this
+                // self.cursor.coff: Try to reset the column offset; if the match is after the offset, this
                 // will be updated in self.cursor.scroll() so that the result is visible
-                self.cursor.coff = 0;
+                (self.cursor.x, self.cursor.y, self.cursor.coff) = (cx, current, 0);
                 let rx = row.cx2rx[cx];
                 row.match_segment = Some(rx..rx + query.len());
                 return Some(current);
@@ -781,8 +790,7 @@ impl PromptMode {
                 PromptState::Active(b) => return Ok(Some(Self::GoTo(b))),
                 PromptState::Cancelled => (),
                 PromptState::Completed(b) => {
-                    let mut split = b
-                        .splitn(2, ':')
+                    let mut split = b.splitn(2, ':')
                         // saturating_sub: Lines and cols are 1-indexed
                         .map(|u| u.trim().parse().map(|s: usize| s.saturating_sub(1)));
                     match (split.next().transpose(), split.next().transpose()) {
@@ -905,14 +913,15 @@ mod tests {
     #[test]
     fn editor_delete_char() {
         let mut editor = Editor::default();
-        for b in "Hello!".as_bytes() {
+        for b in "Hello world!".as_bytes() {
             editor.insert_byte(*b);
         }
         editor.delete_char();
-        assert_eq!(editor.rows[0].chars, "Hello".as_bytes());
-        editor.move_cursor(&AKey::Left);
-        editor.move_cursor(&AKey::Left);
+        assert_eq!(editor.rows[0].chars, "Hello world".as_bytes());
+        editor.move_cursor(&AKey::Left, true);
+        editor.move_cursor(&AKey::Left, false);
+        editor.move_cursor(&AKey::Left, false);
         editor.delete_char();
-        assert_eq!(editor.rows[0].chars, "Helo".as_bytes());
+        assert_eq!(editor.rows[0].chars, "Helo world".as_bytes());
     }
 }
diff --git a/src/row.rs b/src/row.rs
index f1decd07..53c11b72 100644
--- a/src/row.rs
+++ b/src/row.rs
@@ -65,8 +65,7 @@ impl Row {
             self.render.push_str(&(if c == '\t' { " ".repeat(n_rend_chars) } else { c.into() }));
             self.cx2rx.extend(std::iter::repeat(rx).take(n_bytes));
             self.rx2cx.extend(std::iter::repeat(cx).take(n_rend_chars));
-            rx += n_rend_chars;
-            cx += n_bytes;
+            (rx, cx) = (rx + n_rend_chars, cx + n_bytes);
         }
         self.cx2rx.push(rx);
         self.rx2cx.push(cx);
diff --git a/src/unix.rs b/src/unix.rs
index 4dd89d87..8a239f52 100644
--- a/src/unix.rs
+++ b/src/unix.rs
@@ -70,10 +70,9 @@ pub fn enable_raw_mode() -> Result<TermMode, Error> {
     let orig_term = unsafe { maybe_term.assume_init() };
     let mut term = orig_term;
     unsafe { libc::cfmakeraw(&mut term) };
-    // Set the minimum number of characters for non-canonical reads
-    term.c_cc[VMIN] = 0;
-    // Set the timeout in deciseconds for non-canonical reads
-    term.c_cc[VTIME] = 1;
+    // First sets the minimum number of characters for non-canonical reads
+    // Second sets the timeout in deciseconds for non-canonical reads
+    (term.c_cc[VMIN], term.c_cc[VTIME]) = (0, 1);
     set_term_mode(&term)?;
     Ok(orig_term)
 }


Test patch: