Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implemented CTRL + Arrows to move to next/previous word #214

Merged
merged 14 commits into from
Jan 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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());
}
ilai-deutel marked this conversation as resolved.
Show resolved Hide resolved
}
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