diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index a3417ea1b..a73fe92f7 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -463,6 +463,10 @@ impl MappableCommand { goto_previous_buffer, "Goto previous buffer", goto_line_end_newline, "Goto newline at line end", goto_first_nonwhitespace, "Goto first non-blank in line", + goto_indent_start, "Goto start of indent scope", + goto_indent_end, "Goto end of indent scope", + extend_to_indent_start, "Extend to start of indent scope", + extend_to_indent_end, "Extend to end of indent scope", trim_selections, "Trim whitespace from selections", extend_to_line_start, "Extend to line start", extend_to_first_nonwhitespace, "Extend to first non-blank in line", @@ -3845,6 +3849,104 @@ fn goto_last_line_impl(cx: &mut Context, movement: Movement) { doc.set_selection(view.id, selection); } +fn goto_indent_start(cx: &mut Context) { + goto_indent_impl(cx, Movement::Move, Direction::Backward); +} + +fn goto_indent_end(cx: &mut Context) { + goto_indent_impl(cx, Movement::Move, Direction::Forward); +} + +fn extend_to_indent_start(cx: &mut Context) { + goto_indent_impl(cx, Movement::Extend, Direction::Backward); +} + +fn extend_to_indent_end(cx: &mut Context) { + goto_indent_impl(cx, Movement::Extend, Direction::Forward); +} + +fn goto_indent_impl(cx: &mut Context, movement: Movement, direction: Direction) { + let (view, doc) = current!(cx.editor); + let text = doc.text().slice(..); + + let count_indent = |ch: char| -> Option { + match ch { + ' ' => Some(1), + '\t' => Some(4), + _ => None, + } + }; + + let selection = doc.selection(view.id).clone().transform(|range| { + let mut line_idx = range.cursor_line(text); + let mut current_line = text.line(line_idx); + + // If cursor line is empty or contains only whitespace, move to the next line + while current_line.len_chars() == 0 || current_line.chars().all(|ch| ch.is_whitespace()) { + line_idx = match direction { + Direction::Forward => line_idx.saturating_add(1), + Direction::Backward => line_idx.saturating_sub(1), + }; + + current_line = text.line(line_idx); + } + + // If the first significant line is zeroth-indent, don't proceed + if current_line.chars().map_while(count_indent).sum::() == 0 { + return range; + } + + let first_char_pos = current_line.first_non_whitespace_char(); + let mut target_idx = line_idx; + loop { + target_idx = match direction { + Direction::Forward => target_idx.saturating_add(1), + Direction::Backward => target_idx.saturating_sub(1), + }; + + if target_idx >= text.len_lines() || target_idx == 0 { + break; + } + + let target_line = text.line(target_idx); + let target_first_char_pos = target_line.first_non_whitespace_char(); + + // Skip empty lines + if target_line.chars().all(|c| c.is_whitespace()) || target_first_char_pos == None { + continue; + } + + // Stop when scoped up + if target_first_char_pos < first_char_pos { + break; + } + } + + // Off by one + let target_idx = match direction { + Direction::Forward => target_idx.saturating_sub(1), + Direction::Backward => target_idx.saturating_add(1), + }; + + let indents: Vec = (line_idx.min(target_idx)..=line_idx.max(target_idx)) + .map(|i| text.line(i)) + .map(|l| l.chars().map_while(count_indent).sum()).collect(); + + // If there are no indents, or all indents are 0, return the original range. + if indents.is_empty() || indents.into_iter().all(|n| n == 0) { + return range; + } + + range.put_cursor( + text, + text.line_to_char(target_idx), + movement == Movement::Extend, + ) + }); + push_jump(view, doc); + doc.set_selection(view.id, selection); +} + fn goto_column(cx: &mut Context) { goto_column_impl(cx, Movement::Move); } diff --git a/helix-term/src/keymap/default.rs b/helix-term/src/keymap/default.rs index 5bbbd3f40..dc56097a1 100644 --- a/helix-term/src/keymap/default.rs +++ b/helix-term/src/keymap/default.rs @@ -122,6 +122,7 @@ pub fn default() -> HashMap { "p" => goto_prev_paragraph, "x" => goto_prev_xml_element, "space" => add_newline_above, + "i" => goto_indent_start, }, "]" => { "Right bracket" "d" => goto_next_diag, @@ -137,6 +138,7 @@ pub fn default() -> HashMap { "p" => goto_next_paragraph, "x" => goto_next_xml_element, "space" => add_newline_below, + "i" => goto_indent_end, }, "/" => search,