diff --git a/book/src/keymap.md b/book/src/keymap.md index 705c4ab1e..c7ffbb26e 100644 --- a/book/src/keymap.md +++ b/book/src/keymap.md @@ -133,6 +133,7 @@ ## Goto mode | e | Go to the end of the file | | h | Go to the start of the line | | l | Go to the end of the line | +| s | Go to first non-whitespace character of the line | | t | Go to the top of the screen | | m | Go to the middle of the screen | | b | Go to the bottom of the screen | diff --git a/helix-core/src/lib.rs b/helix-core/src/lib.rs index cfe466ed2..da48ba7ec 100644 --- a/helix-core/src/lib.rs +++ b/helix-core/src/lib.rs @@ -18,16 +18,8 @@ mod transaction; pub mod words; -pub(crate) fn find_first_non_whitespace_char2(line: RopeSlice) -> Option { - // find first non-whitespace char - for (start, ch) in line.chars().enumerate() { - // TODO: could use memchr with chunks? - if ch != ' ' && ch != '\t' && ch != '\n' { - return Some(start); - } - } - - None +pub fn find_first_non_whitespace_char2(line: RopeSlice) -> Option { + line.chars().position(|ch| !ch.is_whitespace()) } pub(crate) fn find_first_non_whitespace_char(text: RopeSlice, line_num: usize) -> Option { let line = text.line(line_num); diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 2412e55d3..fc1b363c9 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -1,5 +1,6 @@ use helix_core::{ - comment, coords_at_pos, find_root, graphemes, indent, match_brackets, + comment, coords_at_pos, find_first_non_whitespace_char2, find_root, graphemes, indent, + match_brackets, movement::{self, Direction}, object, pos_at_coords, regex::{self, Regex}, @@ -216,6 +217,24 @@ pub fn move_line_start(cx: &mut Context) { doc.set_selection(view.id, selection); } +pub fn move_first_nonwhitespace(cx: &mut Context) { + let (view, doc) = cx.current(); + + let selection = doc.selection(view.id).transform(|range| { + let text = doc.text(); + let line_idx = text.char_to_line(range.head); + + if let Some(pos) = find_first_non_whitespace_char2(text.line(line_idx)) { + let pos = pos + text.line_to_char(line_idx); + Range::new(pos, pos) + } else { + range + } + }); + + doc.set_selection(view.id, selection); +} + // TODO: move vs extend could take an extra type Extend/Move that would // Range::new(if Move { pos } if Extend { range.anchor }, pos) // since these all really do the same thing @@ -421,6 +440,24 @@ pub fn extend_prev_char(cx: &mut Context) { ) } +pub fn extend_first_nonwhitespace(cx: &mut Context) { + let (view, doc) = cx.current(); + + let selection = doc.selection(view.id).transform(|range| { + let text = doc.text(); + let line_idx = text.char_to_line(range.head); + + if let Some(pos) = find_first_non_whitespace_char2(text.line(line_idx)) { + let pos = pos + text.line_to_char(line_idx); + Range::new(range.anchor, pos) + } else { + range + } + }); + + doc.set_selection(view.id, selection); +} + pub fn replace(cx: &mut Context) { // need to wait for next key cx.on_next_key(move |cx, event| { @@ -1288,6 +1325,8 @@ pub fn goto_mode(cx: &mut Context) { (_, 'y') => goto_type_definition(cx), (_, 'r') => goto_reference(cx), (_, 'i') => goto_implementation(cx), + (Mode::Normal, 's') => move_first_nonwhitespace(cx), + (Mode::Select, 's') => extend_first_nonwhitespace(cx), (_, 't') | (_, 'm') | (_, 'b') => { let (view, doc) = cx.current();