Skip to content

Commit

Permalink
Implemented CTRL + Arrows to move to next/previous word (ilai-deutel#214
Browse files Browse the repository at this point in the history
)

* Add functionality for CTRL + LeftArrow to move to previous word

* Add functionality for CTRL + RightArrow to move to next word

* Modify README.md to reflect changes

* Code refactoring to fit in 1024 lines

* More code refactoring to fit in 1024 lines

* Code refactoring

* Ran formatting tool

* Add unit test for CTRL+Arrow

* Add unit test for CTRL+Arrow

* Fix unit test result

* Fix unit test result

* Fix unit test result
  • Loading branch information
eusuntcarol authored Jan 19, 2023
1 parent 0555333 commit 3984d24
Show file tree
Hide file tree
Showing 4 changed files with 41 additions and 32 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
61 changes: 35 additions & 26 deletions src/editor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -187,26 +187,36 @@ 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
(AKey::Up, _) if self.cursor.y > 0 => self.cursor.y -= 1,
(AKey::Down, Some(_)) => self.cursor.y += 1,
_ => (),
}
self.cursor.x = cursor_x;
self.update_cursor_x_position();
}

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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);
}
}

Expand Down Expand Up @@ -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();
Expand All @@ -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) => (),
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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()) {
Expand Down Expand Up @@ -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());
}
}
3 changes: 1 addition & 2 deletions src/row.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
7 changes: 3 additions & 4 deletions src/unix.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down

0 comments on commit 3984d24

Please sign in to comment.