feat: Home/End jump to line start/end in multiline composer
Plain Home and End now navigate within the current line instead of jumping to the absolute start/end of the entire input. Ctrl+A and Ctrl+E remain as absolute start/end shortcuts. - Add move_cursor_line_start() / move_cursor_line_end() to App - Wire Home -> move_cursor_line_start(), End -> move_cursor_line_end() - On single-line input the new methods behave identically to the absolute versions (no behaviour change) - End on a newline character skips to the end of the next line - 14 tests covering multiline, singleline, and edge cases
This commit is contained in:
committed by
Hunter Bown
parent
0ce41505bc
commit
f77a07e207
@@ -3218,6 +3218,38 @@ impl App {
|
||||
self.needs_redraw = true;
|
||||
}
|
||||
|
||||
/// In a multiline composer, jump to the start of the current line.
|
||||
/// On single-line input this is equivalent to `move_cursor_start`.
|
||||
pub fn move_cursor_line_start(&mut self) {
|
||||
let byte_pos = byte_index_at_char(&self.input, self.cursor_position);
|
||||
let before = &self.input[..byte_pos];
|
||||
if let Some(last_nl_byte) = before.rfind('\n') {
|
||||
// Position after the '\n' (start of the current line).
|
||||
self.cursor_position = char_count(&self.input[..=last_nl_byte]);
|
||||
} else {
|
||||
self.cursor_position = 0;
|
||||
}
|
||||
self.needs_redraw = true;
|
||||
}
|
||||
|
||||
/// In a multiline composer, jump to the end of the current line
|
||||
/// (just before the next `\n` or at the end of input).
|
||||
/// On single-line input this is equivalent to `move_cursor_end`.
|
||||
pub fn move_cursor_line_end(&mut self) {
|
||||
let mut search_start = byte_index_at_char(&self.input, self.cursor_position);
|
||||
// If the cursor sits on a '\n', skip past it — the user
|
||||
// wants the end of the *next* line.
|
||||
if self.input.as_bytes().get(search_start) == Some(&b'\n') {
|
||||
search_start += 1;
|
||||
}
|
||||
if let Some(offset) = self.input[search_start..].find('\n') {
|
||||
self.cursor_position = char_count(&self.input[..search_start + offset]);
|
||||
} else {
|
||||
self.cursor_position = char_count(&self.input);
|
||||
}
|
||||
self.needs_redraw = true;
|
||||
}
|
||||
|
||||
/// Move forward one word. Skips over the current word then any trailing
|
||||
/// whitespace to land on the first character of the next word.
|
||||
pub fn move_cursor_word_forward(&mut self) {
|
||||
@@ -4235,6 +4267,60 @@ mod tests {
|
||||
assert!(default_composer_arrows_scroll_for_platform(true, true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn move_cursor_line_start_multiline() {
|
||||
let mut app = App::new(test_options(false), &Config::default());
|
||||
app.input = "abc\ndef\nghi".to_string();
|
||||
app.cursor_position = "abc\ndef\nghi".chars().count(); // absolute end
|
||||
app.move_cursor_line_start();
|
||||
assert_eq!(app.cursor_position, "abc\ndef\n".len()); // start of "ghi"
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn move_cursor_line_start_singleline() {
|
||||
let mut app = App::new(test_options(false), &Config::default());
|
||||
app.input = "hello".to_string();
|
||||
app.cursor_position = 3;
|
||||
app.move_cursor_line_start();
|
||||
assert_eq!(app.cursor_position, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn move_cursor_line_end_multiline() {
|
||||
let mut app = App::new(test_options(false), &Config::default());
|
||||
app.input = "abc\ndef\nghi".to_string();
|
||||
app.cursor_position = 0; // start of first line
|
||||
app.move_cursor_line_end();
|
||||
assert_eq!(app.cursor_position, "abc".len()); // before first '\n'
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn move_cursor_line_end_at_newline_skips_to_next_line() {
|
||||
let mut app = App::new(test_options(false), &Config::default());
|
||||
app.input = "abc\ndef\nghi".to_string();
|
||||
app.cursor_position = "abc".len(); // on the '\n'
|
||||
app.move_cursor_line_end();
|
||||
assert_eq!(app.cursor_position, "abc\ndef".len()); // end of second line
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn move_cursor_line_end_last_line() {
|
||||
let mut app = App::new(test_options(false), &Config::default());
|
||||
app.input = "abc\ndef".to_string();
|
||||
app.cursor_position = "abc\n".len(); // start of last line
|
||||
app.move_cursor_line_end();
|
||||
assert_eq!(app.cursor_position, "abc\ndef".chars().count()); // absolute end
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn move_cursor_line_start_already_at_start() {
|
||||
let mut app = App::new(test_options(false), &Config::default());
|
||||
app.input = "abc\ndef".to_string();
|
||||
app.cursor_position = "abc\n".len(); // start of second line
|
||||
app.move_cursor_line_start();
|
||||
assert_eq!(app.cursor_position, "abc\n".len()); // unchanged
|
||||
}
|
||||
|
||||
struct EnvVarGuard {
|
||||
key: &'static str,
|
||||
previous: Option<OsString>,
|
||||
|
||||
@@ -3267,10 +3267,10 @@ async fn run_event_loop(
|
||||
app.move_cursor_start();
|
||||
}
|
||||
KeyCode::Home => {
|
||||
app.move_cursor_start();
|
||||
app.move_cursor_line_start();
|
||||
}
|
||||
KeyCode::End => {
|
||||
app.move_cursor_end();
|
||||
app.move_cursor_line_end();
|
||||
}
|
||||
KeyCode::Char('e') if key.modifiers.contains(KeyModifiers::CONTROL) => {
|
||||
app.move_cursor_end();
|
||||
|
||||
@@ -5524,6 +5524,78 @@ fn composer_arrows_scroll_config_overrides_default() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn home_jumps_to_line_start_multiline() {
|
||||
let mut app = create_test_app();
|
||||
app.input = "line one\nline two\nline three".to_string();
|
||||
app.cursor_position = app.input.chars().count();
|
||||
app.move_cursor_line_start();
|
||||
assert_eq!(app.cursor_position, "line one\nline two\n".len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn home_from_middle_of_line_jumps_to_line_start() {
|
||||
let mut app = create_test_app();
|
||||
app.input = "line one\nline two".to_string();
|
||||
app.cursor_position = "line one\nli".len();
|
||||
app.move_cursor_line_start();
|
||||
assert_eq!(app.cursor_position, "line one\n".len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn home_on_singleline_jumps_to_zero() {
|
||||
let mut app = create_test_app();
|
||||
app.input = "hello world".to_string();
|
||||
app.cursor_position = 6;
|
||||
app.move_cursor_line_start();
|
||||
assert_eq!(app.cursor_position, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn end_jumps_to_line_end_multiline() {
|
||||
let mut app = create_test_app();
|
||||
app.input = "line one\nline two\nline three".to_string();
|
||||
app.cursor_position = 0;
|
||||
app.move_cursor_line_end();
|
||||
assert_eq!(app.cursor_position, "line one".len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn end_from_middle_of_line_jumps_to_line_end() {
|
||||
let mut app = create_test_app();
|
||||
app.input = "line one\nline two".to_string();
|
||||
app.cursor_position = "line one\nli".len();
|
||||
app.move_cursor_line_end();
|
||||
assert_eq!(app.cursor_position, "line one\nline two".len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn end_on_singleline_jumps_to_absolute_end() {
|
||||
let mut app = create_test_app();
|
||||
app.input = "hello world".to_string();
|
||||
app.cursor_position = 0;
|
||||
app.move_cursor_line_end();
|
||||
assert_eq!(app.cursor_position, app.input.chars().count());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn home_at_line_start_stays_put() {
|
||||
let mut app = create_test_app();
|
||||
app.input = "line one\nline two".to_string();
|
||||
app.cursor_position = "line one\n".len();
|
||||
app.move_cursor_line_start();
|
||||
assert_eq!(app.cursor_position, "line one\n".len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn end_at_newline_skips_to_next_line_end() {
|
||||
let mut app = create_test_app();
|
||||
app.input = "line one\nline two\nline three".to_string();
|
||||
app.cursor_position = "line one".len();
|
||||
app.move_cursor_line_end();
|
||||
assert_eq!(app.cursor_position, "line one\nline two".len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn notification_settings_tui_always_keeps_configured_method_no_threshold() {
|
||||
let config = Config {
|
||||
|
||||
Reference in New Issue
Block a user