on master help sign in
Auto indent on `insert_at_line_start` (#5837)
993c68aAlex
helix-term/src/commands.rs+73−10
@@ -2775,24 +2775,87 @@ fn last_picker(cx: &mut Context) {
2775 }));
2775 }));
2776}
2776}
2777
2777
2778+/// Fallback position to use for [`insert_with_indent`].
2779+enum IndentFallbackPos {
2780+ LineStart,
2781+ LineEnd,
2782+}
2783+
2778−// I inserts at the first nonwhitespace character of each line with a selection
2784+// `I` inserts at the first nonwhitespace character of each line with a selection.
2785+// If the line is empty, automatically indent.
2779fn insert_at_line_start(cx: &mut Context) {
2786fn insert_at_line_start(cx: &mut Context) {
2780− goto_first_nonwhitespace(cx);
2787+ insert_with_indent(cx, IndentFallbackPos::LineStart);
2781− enter_insert_mode(cx);
2782}
2788}
2783
2789
2784−// A inserts at the end of each line with a selection
2790+// `A` inserts at the end of each line with a selection.
2791+// If the line is empty, automatically indent.
2785fn insert_at_line_end(cx: &mut Context) {
2792fn insert_at_line_end(cx: &mut Context) {
2793+ insert_with_indent(cx, IndentFallbackPos::LineEnd);
2794+}
2795+
2796+// Enter insert mode and auto-indent the current line if it is empty.
2797+// If the line is not empty, move the cursor to the specified fallback position.
2798+fn insert_with_indent(cx: &mut Context, cursor_fallback: IndentFallbackPos) {
2786 enter_insert_mode(cx);
2799 enter_insert_mode(cx);
2800+
2787 let (view, doc) = current!(cx.editor);
2801 let (view, doc) = current!(cx.editor);
2788
2802
2803+ let text = doc.text().slice(..);
2804+ let contents = doc.text();
2789− let selection = doc.selection(view.id).clone().transform(|range| {
2805+ let selection = doc.selection(view.id);
2790− let text = doc.text().slice(..);
2806+
2807+ let language_config = doc.language_config();
2808+ let syntax = doc.syntax();
2809+ let tab_width = doc.tab_width();
2810+
2811+ let mut ranges = SmallVec::with_capacity(selection.len());
2812+ let mut offs = 0;
2813+
2814+ let mut transaction = Transaction::change_by_selection(contents, selection, |range| {
2791− let line = range.cursor_line(text);
2815+ let cursor_line = range.cursor_line(text);
2816+ let cursor_line_start = text.line_to_char(cursor_line);
2817+
2792− let pos = line_end_char_index(&text, line);
2818+ if line_end_char_index(&text, cursor_line) == cursor_line_start {
2793− Range::new(pos, pos)
2819+ // line is empty => auto indent
2820+ let line_end_index = cursor_line_start;
2821+
2822+ let indent = indent::indent_for_newline(
2823+ language_config,
2824+ syntax,
2825+ &doc.indent_style,
2826+ tab_width,
2827+ text,
2828+ cursor_line,
2829+ line_end_index,
2830+ cursor_line,
2831+ );
2832+
2833+ // calculate new selection ranges
2834+ let pos = offs + cursor_line_start;
2835+ let indent_width = indent.chars().count();
2836+ ranges.push(Range::point(pos + indent_width));
2837+ offs += indent_width;
2838+
2839+ (line_end_index, line_end_index, Some(indent.into()))
2840+ } else {
2841+ // move cursor to the fallback position
2842+ let pos = match cursor_fallback {
2843+ IndentFallbackPos::LineStart => {
2844+ find_first_non_whitespace_char(text.line(cursor_line))
2845+ .map(|ws_offset| ws_offset + cursor_line_start)
2846+ .unwrap_or(cursor_line_start)
2847+ }
2848+ IndentFallbackPos::LineEnd => line_end_char_index(&text, cursor_line),
2849+ };
2850+
2851+ ranges.push(range.put_cursor(text, pos + offs, cx.editor.mode == Mode::Select));
2852+
2853+ (cursor_line_start, cursor_line_start, None)
2854+ }
2794 });
2855 });
2795− doc.set_selection(view.id, selection);
2856+
2857+ transaction = transaction.with_selection(Selection::new(ranges, selection.primary_index()));
2858+ doc.apply(&transaction, view.id);
2796}
2859}
2797
2860
2798// Creates an LspCallback that waits for formatting changes to be computed. When they're done,
2861// Creates an LspCallback that waits for formatting changes to be computed. When they're done,
helix-term/tests/test/commands.rs+53
@@ -426,3 +426,56 @@ async fn test_delete_char_forward() -> anyhow::Result<()> {
426
426
427 Ok(())
427 Ok(())
428}
428}
429+
430+#[tokio::test(flavor = "multi_thread")]
431+async fn test_insert_with_indent() -> anyhow::Result<()> {
432+ const INPUT: &str = "\
433+#[f|]#n foo() {
434+ if let Some(_) = None {
435+
436+ }
437+\x20
438+}
439+
440+fn bar() {
441+
442+}";
443+
444+ // insert_at_line_start
445+ test((
446+ INPUT,
447+ ":lang rust<ret>%<A-s>I",
448+ "\
449+#[f|]#n foo() {
450+ #(i|)#f let Some(_) = None {
451+ #(\n|)#\
452+\x20 #(}|)#
453+#(\x20|)#
454+#(}|)#
455+#(\n|)#\
456+#(f|)#n bar() {
457+ #(\n|)#\
458+#(}|)#",
459+ ))
460+ .await?;
461+
462+ // insert_at_line_end
463+ test((
464+ INPUT,
465+ ":lang rust<ret>%<A-s>A",
466+ "\
467+fn foo() {#[\n|]#\
468+\x20 if let Some(_) = None {#(\n|)#\
469+\x20 #(\n|)#\
470+\x20 }#(\n|)#\
471+\x20#(\n|)#\
472+}#(\n|)#\
473+#(\n|)#\
474+fn bar() {#(\n|)#\
475+\x20 #(\n|)#\
476+}#(|)#",
477+ ))
478+ .await?;
479+
480+ Ok(())
481+}