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 { 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) }