Skip to content

Commit

Permalink
Auto indent on insert_at_line_start (helix-editor#5837)
Browse files Browse the repository at this point in the history
  • Loading branch information
CptPotato authored and mtoohey31 committed Jun 2, 2024
1 parent 2db0fbf commit 220e090
Show file tree
Hide file tree
Showing 2 changed files with 126 additions and 10 deletions.
83 changes: 73 additions & 10 deletions helix-term/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2775,24 +2775,87 @@ fn last_picker(cx: &mut Context) {
}));
}

// I inserts at the first nonwhitespace character of each line with a selection
/// Fallback position to use for [`insert_with_indent`].
enum IndentFallbackPos {
LineStart,
LineEnd,
}

// `I` inserts at the first nonwhitespace character of each line with a selection.
// If the line is empty, automatically indent.
fn insert_at_line_start(cx: &mut Context) {
goto_first_nonwhitespace(cx);
enter_insert_mode(cx);
insert_with_indent(cx, IndentFallbackPos::LineStart);
}

// A inserts at the end of each line with a selection
// `A` inserts at the end of each line with a selection.
// If the line is empty, automatically indent.
fn insert_at_line_end(cx: &mut Context) {
insert_with_indent(cx, IndentFallbackPos::LineEnd);
}

// Enter insert mode and auto-indent the current line if it is empty.
// If the line is not empty, move the cursor to the specified fallback position.
fn insert_with_indent(cx: &mut Context, cursor_fallback: IndentFallbackPos) {
enter_insert_mode(cx);

let (view, doc) = current!(cx.editor);

let selection = doc.selection(view.id).clone().transform(|range| {
let text = doc.text().slice(..);
let line = range.cursor_line(text);
let pos = line_end_char_index(&text, line);
Range::new(pos, pos)
let text = doc.text().slice(..);
let contents = doc.text();
let selection = doc.selection(view.id);

let language_config = doc.language_config();
let syntax = doc.syntax();
let tab_width = doc.tab_width();

let mut ranges = SmallVec::with_capacity(selection.len());
let mut offs = 0;

let mut transaction = Transaction::change_by_selection(contents, selection, |range| {
let cursor_line = range.cursor_line(text);
let cursor_line_start = text.line_to_char(cursor_line);

if line_end_char_index(&text, cursor_line) == cursor_line_start {
// line is empty => auto indent
let line_end_index = cursor_line_start;

let indent = indent::indent_for_newline(
language_config,
syntax,
&doc.indent_style,
tab_width,
text,
cursor_line,
line_end_index,
cursor_line,
);

// calculate new selection ranges
let pos = offs + cursor_line_start;
let indent_width = indent.chars().count();
ranges.push(Range::point(pos + indent_width));
offs += indent_width;

(line_end_index, line_end_index, Some(indent.into()))
} else {
// move cursor to the fallback position
let pos = match cursor_fallback {
IndentFallbackPos::LineStart => {
find_first_non_whitespace_char(text.line(cursor_line))
.map(|ws_offset| ws_offset + cursor_line_start)
.unwrap_or(cursor_line_start)
}
IndentFallbackPos::LineEnd => line_end_char_index(&text, cursor_line),
};

ranges.push(range.put_cursor(text, pos + offs, cx.editor.mode == Mode::Select));

(cursor_line_start, cursor_line_start, None)
}
});
doc.set_selection(view.id, selection);

transaction = transaction.with_selection(Selection::new(ranges, selection.primary_index()));
doc.apply(&transaction, view.id);
}

// Creates an LspCallback that waits for formatting changes to be computed. When they're done,
Expand Down
53 changes: 53 additions & 0 deletions helix-term/tests/test/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -426,3 +426,56 @@ async fn test_delete_char_forward() -> anyhow::Result<()> {

Ok(())
}

#[tokio::test(flavor = "multi_thread")]
async fn test_insert_with_indent() -> anyhow::Result<()> {
const INPUT: &str = "\
#[f|]#n foo() {
if let Some(_) = None {
}
\x20
}
fn bar() {
}";

// insert_at_line_start
test((
INPUT,
":lang rust<ret>%<A-s>I",
"\
#[f|]#n foo() {
#(i|)#f let Some(_) = None {
#(\n|)#\
\x20 #(}|)#
#(\x20|)#
#(}|)#
#(\n|)#\
#(f|)#n bar() {
#(\n|)#\
#(}|)#",
))
.await?;

// insert_at_line_end
test((
INPUT,
":lang rust<ret>%<A-s>A",
"\
fn foo() {#[\n|]#\
\x20 if let Some(_) = None {#(\n|)#\
\x20 #(\n|)#\
\x20 }#(\n|)#\
\x20#(\n|)#\
}#(\n|)#\
#(\n|)#\
fn bar() {#(\n|)#\
\x20 #(\n|)#\
}#(|)#",
))
.await?;

Ok(())
}

0 comments on commit 220e090

Please sign in to comment.