From 09ae00fd6d337c89400cf2f109295d9e69f4fca8 Mon Sep 17 00:00:00 2001 From: ktunprasert Date: Sat, 19 Jul 2025 04:38:24 +0100 Subject: [PATCH 1/7] [text-object/indent] feat: select & extend based on indentation level --- helix-term/src/commands.rs | 102 +++++++++++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index a3417ea1b..f9e51bc5e 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 scope indentaion", + goto_indent_end, "Goto end of scope indentation", + extend_to_indent_start, "Extend to start of scope indentation", + extend_to_indent_end, "Extend to end of scope indentation", 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 line_idx = range.cursor_line(text); + let current_line = text.line(line_idx); + + let indent_count: u64 = current_line.chars().map_while(count_indent).sum(); + if indent_count > 0 { + // If the line is empty, just return the current range. + return range; + } + + let first_char_pos = current_line.first_non_whitespace_char(); + + // let indentation_level: u64 = current_line + // .clone() + // .chars() + // .map_while(count_indent) + // .sum(); + + // let mut target_idx = match direction { + // Direction::Forward => line_idx.saturating_add(1), + // Direction::Backward => line_idx.saturating_sub(1), + // }; + + let mut target_idx = line_idx; + loop { + target_idx = match direction { + Direction::Forward => { + if target_idx + 1 >= text.len_lines() { + break; + } + target_idx + 1 + } + Direction::Backward => { + if target_idx == 0 { + break; + } + target_idx - 1 + } + }; + + 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 indentation changes + 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), + }; + + 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); } From f63e0a5a51da7c73a2ed00b759cef11cee433b7a Mon Sep 17 00:00:00 2001 From: ktunprasert Date: Sat, 19 Jul 2025 13:00:06 +0100 Subject: [PATCH 2/7] [text-object/indent] fix: detect when empty line and use "next" non-empty --- helix-term/src/commands.rs | 46 +++++++++++++------------------------- 1 file changed, 16 insertions(+), 30 deletions(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index f9e51bc5e..db940ddb9 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -3878,45 +3878,31 @@ fn goto_indent_impl(cx: &mut Context, movement: Movement, direction: Direction) }; let selection = doc.selection(view.id).clone().transform(|range| { - let line_idx = range.cursor_line(text); - let current_line = text.line(line_idx); + let mut line_idx = range.cursor_line(text); + let mut current_line = text.line(line_idx); - let indent_count: u64 = current_line.chars().map_while(count_indent).sum(); - if indent_count > 0 { - // If the line is empty, just return the current range. - return range; + // 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); } let first_char_pos = current_line.first_non_whitespace_char(); - - // let indentation_level: u64 = current_line - // .clone() - // .chars() - // .map_while(count_indent) - // .sum(); - - // let mut target_idx = match direction { - // Direction::Forward => line_idx.saturating_add(1), - // Direction::Backward => line_idx.saturating_sub(1), - // }; - let mut target_idx = line_idx; loop { target_idx = match direction { - Direction::Forward => { - if target_idx + 1 >= text.len_lines() { - break; - } - target_idx + 1 - } - Direction::Backward => { - if target_idx == 0 { - break; - } - target_idx - 1 - } + 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(); From 851f9c73dd8ce1b78c340cf90a63fdac4f632714 Mon Sep 17 00:00:00 2001 From: ktunprasert Date: Sat, 19 Jul 2025 14:53:18 +0100 Subject: [PATCH 3/7] [text-object/indent] fix: final target should be less indent scope --- helix-term/src/commands.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index db940ddb9..3561f6ea5 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -3900,7 +3900,7 @@ fn goto_indent_impl(cx: &mut Context, movement: Movement, direction: Direction) }; if target_idx >= text.len_lines() || target_idx == 0 { - break + break; } let target_line = text.line(target_idx); @@ -3911,8 +3911,8 @@ fn goto_indent_impl(cx: &mut Context, movement: Movement, direction: Direction) continue; } - // Stop when indentation changes - if target_first_char_pos != first_char_pos { + // Stop when scoped up + if target_first_char_pos < first_char_pos { break; } } From c0062c9c374e1cc8d1e02fbd9dcb1905263489bf Mon Sep 17 00:00:00 2001 From: ktunprasert Date: Sat, 19 Jul 2025 14:53:51 +0100 Subject: [PATCH 4/7] [text-object/indent] fix: ensure zero-indent scope are skipped --- helix-term/src/commands.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 3561f6ea5..631adda35 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -3891,6 +3891,11 @@ fn goto_indent_impl(cx: &mut Context, movement: Movement, direction: Direction) 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 { @@ -3923,6 +3928,15 @@ fn goto_indent_impl(cx: &mut Context, movement: Movement, direction: Direction) Direction::Backward => target_idx.saturating_add(1), }; + let indents: Vec = (line_idx..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), From 3402f45742a8579caca59849771319d272131ce9 Mon Sep 17 00:00:00 2001 From: ktunprasert Date: Sat, 19 Jul 2025 15:06:16 +0100 Subject: [PATCH 5/7] [text-object/indent] fix: range should work both directions --- helix-term/src/commands.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 631adda35..dfe5ca2e9 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -3928,7 +3928,7 @@ fn goto_indent_impl(cx: &mut Context, movement: Movement, direction: Direction) Direction::Backward => target_idx.saturating_add(1), }; - let indents: Vec = (line_idx..target_idx) + 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(); From e727650cb0ffd370e71b8ecd8ad80fdaf2efd544 Mon Sep 17 00:00:00 2001 From: ktunprasert Date: Sat, 19 Jul 2025 15:08:13 +0100 Subject: [PATCH 6/7] [text-object/indent] fix: corrected command description --- helix-term/src/commands.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index dfe5ca2e9..a73fe92f7 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -463,10 +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 scope indentaion", - goto_indent_end, "Goto end of scope indentation", - extend_to_indent_start, "Extend to start of scope indentation", - extend_to_indent_end, "Extend to end of scope indentation", + 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", From 0eddb61873ced78eaef89e1f4a80d6071f599f09 Mon Sep 17 00:00:00 2001 From: ktunprasert Date: Sat, 19 Jul 2025 15:08:27 +0100 Subject: [PATCH 7/7] [text-object/indent] feat: set default ]i [i to move indent scope --- helix-term/src/keymap/default.rs | 2 ++ 1 file changed, 2 insertions(+) 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,