use super::{Buffer, IndentSettings, char_to_byte};
impl Buffer {
pub fn insert_char(&mut self, c: char) {
let line = &mut self.lines[self.cursor.row];
let byte_idx = char_to_byte(line, self.cursor.col);
line.insert(byte_idx, c);
self.cursor.col += 1;
self.touch();
}
pub fn insert_char_smart(&mut self, c: char, indent: IndentSettings) {
let next = self.char_at_cursor();
let prev = self.char_before_cursor();
if is_auto_pair_closer(c) && next == Some(c) {
self.cursor.col += 1;
return;
}
if matches!(c, '}' | ')' | ']') && self.line_is_blank_before_cursor() {
self.dedent_current_line(indent);
}
self.insert_char(c);
if let Some(closer) = auto_pair_closer(c)
&& should_auto_pair(c, prev, next)
{
self.insert_char(closer);
self.cursor.col -= 1;
}
}
pub fn char_at_cursor(&self) -> Option<char> {
self.lines
.get(self.cursor.row)
.and_then(|line| line.chars().nth(self.cursor.col))
}
pub fn char_before_cursor(&self) -> Option<char> {
if self.cursor.col == 0 {
return None;
}
self.lines
.get(self.cursor.row)
.and_then(|line| line.chars().nth(self.cursor.col - 1))
}
pub fn line_is_blank_before_cursor(&self) -> bool {
let line = &self.lines[self.cursor.row];
line.chars().take(self.cursor.col).all(|c| c.is_whitespace())
}
pub fn indent_line(&mut self, row: usize, indent: IndentSettings) {
if row >= self.lines.len() {
return;
}
let line = &self.lines[row];
let leading: String = line.chars().take_while(|c| c.is_whitespace()).collect();
let use_tabs = if leading.is_empty() {
indent.use_tabs
} else {
leading.contains('\t')
};
let prefix: String = if use_tabs {
"\t".to_string()
} else {
" ".repeat(indent.width.max(1))
};
let added_chars = prefix.chars().count();
self.lines[row].insert_str(0, &prefix);
if self.cursor.row == row {
self.cursor.col += added_chars;
}
self.touch();
}
pub fn dedent_line(&mut self, row: usize, indent: IndentSettings) {
if row >= self.lines.len() {
return;
}
let line = self.lines[row].clone();
let leading: String = line.chars().take_while(|c| c.is_whitespace()).collect();
if leading.is_empty() {
return;
}
let remove_chars = if leading.ends_with('\t') {
1
} else {
let trailing_spaces = leading.chars().rev().take_while(|c| *c == ' ').count();
let w = indent.width.max(1);
let target = (trailing_spaces.saturating_sub(1) / w) * w;
trailing_spaces - target
};
if remove_chars == 0 {
return;
}
let leading_char_count = leading.chars().count();
let delete_start_char = leading_char_count - remove_chars;
let delete_start_byte = char_to_byte(&line, delete_start_char);
let delete_end_byte = char_to_byte(&line, delete_start_char + remove_chars);
self.lines[row].replace_range(delete_start_byte..delete_end_byte, "");
if self.cursor.row == row {
self.cursor.col = self.cursor.col.saturating_sub(remove_chars);
}
self.touch();
}
pub fn dedent_current_line(&mut self, indent: IndentSettings) {
let line = self.lines[self.cursor.row].clone();
let leading: String = line.chars().take_while(|c| c.is_whitespace()).collect();
if leading.is_empty() {
return;
}
let remove_chars = if leading.ends_with('\t') {
1
} else {
let trailing_spaces = leading.chars().rev().take_while(|c| *c == ' ').count();
let w = indent.width.max(1);
let target = (trailing_spaces.saturating_sub(1) / w) * w;
trailing_spaces - target
};
let leading_char_count = leading.chars().count();
let delete_start_char = leading_char_count - remove_chars;
let delete_start_byte = char_to_byte(&line, delete_start_char);
let delete_end_byte = char_to_byte(&line, delete_start_char + remove_chars);
self.lines[self.cursor.row].replace_range(delete_start_byte..delete_end_byte, "");
self.cursor.col = self.cursor.col.saturating_sub(remove_chars);
self.touch();
}
pub fn insert_newline(&mut self, indent: IndentSettings) {
if self.cursor.col == 0 {
self.lines.insert(self.cursor.row, String::new());
self.cursor.row += 1;
self.touch();
return;
}
let line = self.lines[self.cursor.row].clone();
let byte_idx = char_to_byte(&line, self.cursor.col);
let (left, right) = line.split_at(byte_idx);
let left_owned = left.to_string();
let right_owned = right.to_string();
let prev = left_owned.chars().last();
let next_ch = right_owned.chars().next();
let base = copy_leading_indent(&left_owned, indent);
let ts_begin = self
.highlighter
.as_ref()
.is_some_and(|h| h.indent_begins_at(self.cursor.row));
let next_is_opener = matches!(next_ch, Some('{' | '(' | '['));
let mut new_indent = if ts_begin && !next_is_opener {
add_one_indent_level(&base, indent)
} else {
base
};
let is_empty_pair = match (prev, next_ch) {
(Some(p), Some(n)) => auto_pair_closer(p) == Some(n),
_ => false,
};
if is_empty_pair {
let base_indent = copy_leading_indent(&left_owned, indent);
let mut closer_line = base_indent.clone();
closer_line.push_str(&right_owned);
let middle = add_one_indent_level(&base_indent, indent);
self.cursor.col = middle.chars().count();
self.lines[self.cursor.row] = left_owned;
self.lines.insert(self.cursor.row + 1, middle);
self.lines.insert(self.cursor.row + 2, closer_line);
self.cursor.row += 1;
self.touch();
return;
}
if matches!(next_ch, Some('}' | ')' | ']')) {
new_indent = strip_one_indent_level(&new_indent, indent);
}
self.lines[self.cursor.row] = left_owned;
let mut next = new_indent.clone();
next.push_str(&right_owned);
self.lines.insert(self.cursor.row + 1, next);
self.cursor.row += 1;
self.cursor.col = new_indent.chars().count();
self.touch();
}
pub fn insert_line_below(&mut self, indent: IndentSettings) {
let reference = self.lines[self.cursor.row].clone();
let new_indent =
compute_new_line_indent(&reference, self.cursor.row, &self.highlighter, indent);
let col = new_indent.chars().count();
self.lines.insert(self.cursor.row + 1, new_indent);
self.cursor.row += 1;
self.cursor.col = col;
self.touch();
}
pub fn insert_line_above(&mut self, indent: IndentSettings) {
let new_indent = copy_leading_indent(&self.lines[self.cursor.row], indent);
let col = new_indent.chars().count();
self.lines.insert(self.cursor.row, new_indent);
self.cursor.col = col;
self.touch();
}
pub fn delete_char_under_cursor(&mut self) {
let line = &mut self.lines[self.cursor.row];
if self.cursor.col < line.chars().count() {
let byte_idx = char_to_byte(line, self.cursor.col);
let ch = line[byte_idx..].chars().next().unwrap();
line.replace_range(byte_idx..byte_idx + ch.len_utf8(), "");
self.touch();
self.clamp_col(false);
}
}
pub fn delete_char_before(&mut self) {
if self.cursor.col > 0 {
let line = &mut self.lines[self.cursor.row];
let byte_idx = char_to_byte(line, self.cursor.col - 1);
let ch = line[byte_idx..].chars().next().unwrap();
line.replace_range(byte_idx..byte_idx + ch.len_utf8(), "");
self.cursor.col -= 1;
self.touch();
} else if self.cursor.row > 0 {
let line = self.lines.remove(self.cursor.row);
self.cursor.row -= 1;
self.cursor.col = self.lines[self.cursor.row].chars().count();
self.lines[self.cursor.row].push_str(&line);
self.touch();
}
}
pub fn replace_char(&mut self, ch: char) {
let line = &mut self.lines[self.cursor.row];
if self.cursor.col >= line.chars().count() {
return;
}
let byte_idx = char_to_byte(line, self.cursor.col);
let old_ch = line[byte_idx..].chars().next().unwrap();
line.replace_range(byte_idx..byte_idx + old_ch.len_utf8(), &ch.to_string());
self.touch();
}
pub fn delete_char_before_smart(&mut self, indent: IndentSettings) {
let prev = self.char_before_cursor();
let next = self.char_at_cursor();
if let (Some(p), Some(n)) = (prev, next)
&& auto_pair_closer(p) == Some(n)
{
let line = &mut self.lines[self.cursor.row];
let start_byte = char_to_byte(line, self.cursor.col - 1);
let end_byte = char_to_byte(line, self.cursor.col + 1);
line.replace_range(start_byte..end_byte, "");
self.cursor.col -= 1;
self.touch();
return;
}
if self.cursor.col > 0 && self.line_is_blank_before_cursor() {
self.dedent_current_line(indent);
return;
}
if self.cursor.col == 0 && self.cursor.row > 0 {
let prev_blank = self.lines[self.cursor.row - 1]
.chars()
.all(|c| c.is_whitespace());
let curr_starts_with_closer = matches!(
self.lines[self.cursor.row]
.chars()
.find(|c| !c.is_whitespace()),
Some('}' | ')' | ']')
);
if prev_blank && curr_starts_with_closer {
self.delete_char_before();
self.dedent_current_line(indent);
return;
}
}
self.delete_char_before();
}
}
fn auto_pair_closer(c: char) -> Option<char> {
match c {
'(' => Some(')'),
'[' => Some(']'),
'{' => Some('}'),
'"' => Some('"'),
'\'' => Some('\''),
'`' => Some('`'),
_ => None,
}
}
fn is_auto_pair_closer(c: char) -> bool {
matches!(c, ')' | ']' | '}' | '"' | '\'' | '`')
}
fn should_auto_pair(opener: char, prev: Option<char>, next: Option<char>) -> bool {
if let Some(n) = next
&& (n.is_alphanumeric() || n == '_')
{
return false;
}
if matches!(opener, '"' | '\'' | '`')
&& let Some(p) = prev
&& (p.is_alphanumeric() || p == '_' || p == opener)
{
return false;
}
true
}
fn strip_one_indent_level(indent: &str, settings: IndentSettings) -> String {
if indent.is_empty() {
return String::new();
}
if indent.ends_with('\t') {
let mut out = indent.to_string();
out.pop();
return out;
}
let trailing_spaces = indent.chars().rev().take_while(|c| *c == ' ').count();
if trailing_spaces == 0 {
return indent.to_string();
}
let w = settings.width.max(1);
let target = (trailing_spaces.saturating_sub(1) / w) * w;
let remove = trailing_spaces - target;
indent[..indent.len() - remove].to_string()
}
fn copy_leading_indent(line: &str, _settings: IndentSettings) -> String {
line.chars()
.take_while(|c| c.is_whitespace() && *c != '\n')
.collect()
}
fn compute_new_line_indent(
reference_line: &str,
ref_row: usize,
highlighter: &Option<crate::syntax::Highlighter>,
settings: IndentSettings,
) -> String {
let base = copy_leading_indent(reference_line, settings);
let ts_begin = highlighter
.as_ref()
.is_some_and(|h| h.indent_begins_at(ref_row));
let trailing_opener = reference_line
.trim_end()
.chars()
.last()
.is_some_and(|c| matches!(c, '{' | '(' | '['));
if ts_begin || trailing_opener {
add_one_indent_level(&base, settings)
} else {
base
}
}
fn add_one_indent_level(base: &str, settings: IndentSettings) -> String {
let use_tabs = if base.is_empty() {
settings.use_tabs
} else {
base.contains('\t')
};
let mut out = base.to_string();
if use_tabs {
out.push('\t');
} else {
for _ in 0..settings.width.max(1) {
out.push(' ');
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
fn settings() -> IndentSettings {
IndentSettings {
width: 4,
use_tabs: false,
}
}
#[test]
fn indent_line_adds_spaces_for_empty_leading() {
let mut b = Buffer::new();
b.lines = vec!["let x = 1;".into()];
b.cursor.row = 0;
b.cursor.col = 4;
b.indent_line(0, settings());
assert_eq!(b.lines[0], " let x = 1;");
assert_eq!(b.cursor.col, 8);
}
#[test]
fn indent_line_uses_tab_when_leading_has_tab() {
let mut b = Buffer::new();
b.lines = vec!["\tx".into()];
b.cursor.row = 0;
b.indent_line(0, settings());
assert_eq!(b.lines[0], "\t\tx");
}
#[test]
fn indent_line_falls_back_to_use_tabs_on_blank_leading() {
let mut b = Buffer::new();
b.lines = vec!["x".into()];
let s = IndentSettings { width: 4, use_tabs: true };
b.indent_line(0, s);
assert_eq!(b.lines[0], "\tx");
}
#[test]
fn dedent_line_removes_one_level_of_spaces() {
let mut b = Buffer::new();
b.lines = vec![" x".into()];
b.cursor.row = 0;
b.cursor.col = 8;
b.dedent_line(0, settings());
assert_eq!(b.lines[0], " x");
assert_eq!(b.cursor.col, 4);
}
#[test]
fn dedent_line_rounds_partial_indent_down() {
let mut b = Buffer::new();
b.lines = vec![" x".into()]; b.dedent_line(0, settings());
assert_eq!(b.lines[0], " x");
}
#[test]
fn dedent_line_strips_trailing_tab() {
let mut b = Buffer::new();
b.lines = vec!["\t\tx".into()];
b.dedent_line(0, settings());
assert_eq!(b.lines[0], "\tx");
}
#[test]
fn dedent_line_noop_on_no_leading_whitespace() {
let mut b = Buffer::new();
b.lines = vec!["x".into()];
b.cursor.col = 0;
b.dedent_line(0, settings());
assert_eq!(b.lines[0], "x");
assert_eq!(b.cursor.col, 0);
}
#[test]
fn open_below_copies_leading_whitespace() {
let mut b = Buffer::new();
b.lines = vec![" let x = 1;".into(), " let y = 2;".into()];
b.cursor.row = 0;
b.insert_line_below(settings());
assert_eq!(b.lines[1], " ");
assert_eq!(b.cursor.row, 1);
assert_eq!(b.cursor.col, 4);
}
#[test]
fn open_below_adds_level_after_opening_brace() {
let mut b = Buffer::new();
b.lines = vec!["fn foo() {".into(), "}".into()];
b.cursor.row = 0;
b.insert_line_below(settings());
assert_eq!(b.lines[1], " ");
assert_eq!(b.cursor.col, 4);
}
#[test]
fn open_below_uses_tabs_when_reference_does() {
let mut b = Buffer::new();
b.lines = vec!["\tfn foo() {".into(), "}".into()];
b.cursor.row = 0;
b.insert_line_below(settings());
assert_eq!(b.lines[1], "\t\t");
}
#[test]
fn open_above_copies_indent_without_adding_level() {
let mut b = Buffer::new();
b.lines = vec![" let x = 1;".into()];
b.cursor.row = 0;
b.insert_line_above(settings());
assert_eq!(b.lines[0], " ");
assert_eq!(b.cursor.row, 0);
assert_eq!(b.cursor.col, 4);
}
#[test]
fn newline_at_col_zero_leaves_original_line_unindented() {
let mut b = Buffer::new();
b.lines = vec!["func main() {".into()];
b.cursor.row = 0;
b.cursor.col = 0;
b.insert_newline(settings());
assert_eq!(b.lines[0], "");
assert_eq!(b.lines[1], "func main() {");
assert_eq!(b.cursor.row, 1);
assert_eq!(b.cursor.col, 0);
}
#[test]
fn newline_at_col_zero_preserves_existing_indent() {
let mut b = Buffer::new();
b.lines = vec![" let x = 1;".into()];
b.cursor.row = 0;
b.cursor.col = 0;
b.insert_newline(settings());
assert_eq!(b.lines[0], "");
assert_eq!(b.lines[1], " let x = 1;");
assert_eq!(b.cursor.row, 1);
assert_eq!(b.cursor.col, 0);
}
#[test]
fn newline_after_trailing_opener_does_not_auto_indent() {
let mut b = Buffer::new();
b.lines = vec!["func main() {".into()];
b.cursor.row = 0;
b.cursor.col = 13; b.insert_newline(settings());
assert_eq!(b.lines[0], "func main() {");
assert_eq!(b.lines[1], "");
assert_eq!(b.cursor.row, 1);
assert_eq!(b.cursor.col, 0);
}
#[test]
fn newline_before_opener_keeps_base_indent() {
let mut b = Buffer::new();
b.lines = vec!["func main {".into()];
b.cursor.row = 0;
b.cursor.col = 10; b.insert_newline(settings());
assert_eq!(b.lines[0], "func main ");
assert_eq!(b.lines[1], "{");
assert_eq!(b.cursor.row, 1);
assert_eq!(b.cursor.col, 0);
}
#[test]
fn newline_before_opener_preserves_outer_indent() {
let mut b = Buffer::new();
b.lines = vec![" if cond ".into(), "(x) {}".into()];
b.lines = vec![" if cond (x".into()];
b.cursor.row = 0;
b.cursor.col = 12; b.insert_newline(settings());
assert_eq!(b.lines[0], " if cond ");
assert_eq!(b.lines[1], " (x");
assert_eq!(b.cursor.col, 4);
}
#[test]
fn newline_splits_and_carries_indent() {
let mut b = Buffer::new();
b.lines = vec![" let x = foo + bar;".into()];
b.cursor.row = 0;
b.cursor.col = 16; b.insert_newline(settings());
assert_eq!(b.lines[0], " let x = foo ");
assert_eq!(b.lines[1], " + bar;");
assert_eq!(b.cursor.col, 4);
}
#[test]
fn close_bracket_dedents_when_line_is_blank() {
let mut b = Buffer::new();
b.lines = vec!["fn foo() {".into(), " ".into()];
b.cursor.row = 1;
b.cursor.col = 8;
b.insert_char_smart('}', settings());
assert_eq!(b.lines[1], " }");
assert_eq!(b.cursor.col, 5);
}
#[test]
fn close_bracket_no_dedent_when_text_precedes() {
let mut b = Buffer::new();
b.lines = vec![" let x = HashMap::new(".into()];
b.cursor.row = 0;
b.cursor.col = 25;
b.insert_char_smart(')', settings());
assert_eq!(b.lines[0], " let x = HashMap::new()");
assert_eq!(b.cursor.col, 26);
}
#[test]
fn close_bracket_dedents_partial_indent() {
let mut b = Buffer::new();
b.lines = vec![" ".into()];
b.cursor.row = 0;
b.cursor.col = 7;
b.insert_char_smart(']', settings());
assert_eq!(b.lines[0], " ]");
assert_eq!(b.cursor.col, 5);
}
#[test]
fn close_bracket_dedents_tab_indent() {
let mut b = Buffer::new();
b.lines = vec!["\t\t".into()];
b.cursor.row = 0;
b.cursor.col = 2;
b.insert_char_smart('}', settings());
assert_eq!(b.lines[0], "\t}");
assert_eq!(b.cursor.col, 2);
}
#[test]
fn close_bracket_clears_indent_when_already_at_one_level() {
let mut b = Buffer::new();
b.lines = vec![" ".into()];
b.cursor.row = 0;
b.cursor.col = 4;
b.insert_char_smart('}', settings());
assert_eq!(b.lines[0], "}");
assert_eq!(b.cursor.col, 1);
}
#[test]
fn auto_pair_inserts_matching_closer_and_keeps_cursor_between() {
let mut b = Buffer::new();
b.lines = vec!["foo".into()];
b.cursor.row = 0;
b.cursor.col = 3;
b.insert_char_smart('(', settings());
assert_eq!(b.lines[0], "foo()");
assert_eq!(b.cursor.col, 4);
}
#[test]
fn auto_pair_skip_over_closer_when_next_char_matches() {
let mut b = Buffer::new();
b.lines = vec!["()".into()];
b.cursor.row = 0;
b.cursor.col = 1; b.insert_char_smart(')', settings());
assert_eq!(b.lines[0], "()");
assert_eq!(b.cursor.col, 2);
}
#[test]
fn auto_pair_suppressed_when_next_char_is_word() {
let mut b = Buffer::new();
b.lines = vec!["foo".into()];
b.cursor.row = 0;
b.cursor.col = 0; b.insert_char_smart('(', settings());
assert_eq!(b.lines[0], "(foo");
assert_eq!(b.cursor.col, 1);
}
#[test]
fn auto_pair_quote_suppressed_after_word_char() {
let mut b = Buffer::new();
b.lines = vec!["it".into()];
b.cursor.row = 0;
b.cursor.col = 2;
b.insert_char_smart('\'', settings());
assert_eq!(b.lines[0], "it'");
assert_eq!(b.cursor.col, 3);
}
#[test]
fn auto_pair_quote_pairs_after_punctuation() {
let mut b = Buffer::new();
b.lines = vec!["print(".into()];
b.cursor.row = 0;
b.cursor.col = 6;
b.insert_char_smart('"', settings());
assert_eq!(b.lines[0], "print(\"\"");
assert_eq!(b.cursor.col, 7);
}
#[test]
fn delete_char_before_smart_removes_empty_pair() {
let mut b = Buffer::new();
b.lines = vec!["foo()".into()];
b.cursor.row = 0;
b.cursor.col = 4; b.delete_char_before_smart(settings());
assert_eq!(b.lines[0], "foo");
assert_eq!(b.cursor.col, 3);
}
#[test]
fn delete_char_before_smart_falls_through_when_not_empty_pair() {
let mut b = Buffer::new();
b.lines = vec!["(x)".into()];
b.cursor.row = 0;
b.cursor.col = 1;
b.delete_char_before_smart(settings());
assert_eq!(b.lines[0], "x)");
assert_eq!(b.cursor.col, 0);
}
#[test]
fn newline_inside_empty_braces_spreads_three_lines() {
let mut b = Buffer::new();
b.lines = vec!["fn foo() {}".into()];
b.cursor.row = 0;
b.cursor.col = 10; b.insert_newline(settings());
assert_eq!(b.lines[0], "fn foo() {");
assert_eq!(b.lines[1], " ");
assert_eq!(b.lines[2], "}");
assert_eq!(b.cursor.row, 1);
assert_eq!(b.cursor.col, 4);
}
#[test]
fn newline_inside_empty_braces_preserves_outer_indent() {
let mut b = Buffer::new();
b.lines = vec![" if cond {}".into()];
b.cursor.row = 0;
b.cursor.col = 13; b.insert_newline(settings());
assert_eq!(b.lines[0], " if cond {");
assert_eq!(b.lines[1], " ");
assert_eq!(b.lines[2], " }");
assert_eq!(b.cursor.row, 1);
assert_eq!(b.cursor.col, 8);
}
#[test]
fn newline_inside_empty_parens_also_spreads() {
let mut b = Buffer::new();
b.lines = vec!["foo()".into()];
b.cursor.row = 0;
b.cursor.col = 4; b.insert_newline(settings());
assert_eq!(b.lines[0], "foo(");
assert_eq!(b.lines[1], " ");
assert_eq!(b.lines[2], ")");
assert_eq!(b.cursor.col, 4);
}
#[test]
fn newline_before_closer_strips_one_indent_level() {
let mut b = Buffer::new();
b.lines = vec![" bar();}".into()];
b.cursor.row = 0;
b.cursor.col = 14; b.insert_newline(settings());
assert_eq!(b.lines[0], " bar();");
assert_eq!(b.lines[1], " }");
assert_eq!(b.cursor.col, 4);
}
#[test]
fn newline_before_closer_clears_indent_at_one_level() {
let mut b = Buffer::new();
b.lines = vec![" bar();]".into()];
b.cursor.row = 0;
b.cursor.col = 10; b.insert_newline(settings());
assert_eq!(b.lines[0], " bar();");
assert_eq!(b.lines[1], "]");
assert_eq!(b.cursor.col, 0);
}
#[test]
fn newline_before_closer_strips_one_tab() {
let mut b = Buffer::new();
b.lines = vec!["\t\tbar();)".into()];
b.cursor.row = 0;
b.cursor.col = 8; b.insert_newline(settings());
assert_eq!(b.lines[0], "\t\tbar();");
assert_eq!(b.lines[1], "\t)");
}
#[test]
fn backspace_before_closer_collapses_one_indent_level() {
let mut b = Buffer::new();
b.lines = vec![" }".into()];
b.cursor.row = 0;
b.cursor.col = 8; b.delete_char_before_smart(settings());
assert_eq!(b.lines[0], " }");
assert_eq!(b.cursor.col, 4);
}
#[test]
fn backspace_before_closer_clears_indent_at_one_level() {
let mut b = Buffer::new();
b.lines = vec![" }".into()];
b.cursor.row = 0;
b.cursor.col = 4;
b.delete_char_before_smart(settings());
assert_eq!(b.lines[0], "}");
assert_eq!(b.cursor.col, 0);
}
#[test]
fn backspace_before_closer_strips_tab() {
let mut b = Buffer::new();
b.lines = vec!["\t\t]".into()];
b.cursor.row = 0;
b.cursor.col = 2;
b.delete_char_before_smart(settings());
assert_eq!(b.lines[0], "\t]");
assert_eq!(b.cursor.col, 1);
}
#[test]
fn backspace_does_not_dedent_when_text_precedes_closer() {
let mut b = Buffer::new();
b.lines = vec![" x)".into()];
b.cursor.row = 0;
b.cursor.col = 5; b.delete_char_before_smart(settings());
assert_eq!(b.lines[0], " )");
assert_eq!(b.cursor.col, 4);
}
#[test]
fn backspace_inside_closer_line_indent_dedents() {
let mut b = Buffer::new();
b.lines = vec![" }".into()];
b.cursor.row = 0;
b.cursor.col = 4; b.delete_char_before_smart(settings());
assert_eq!(b.lines[0], " }");
assert_eq!(b.cursor.col, 0);
}
#[test]
fn backspace_on_closer_line_with_trailing_content_dedents() {
let mut b = Buffer::new();
b.lines = vec![" });".into()];
b.cursor.row = 0;
b.cursor.col = 8; b.delete_char_before_smart(settings());
assert_eq!(b.lines[0], " });");
assert_eq!(b.cursor.col, 4);
}
#[test]
fn backspace_inside_pure_whitespace_line_dedents() {
let mut b = Buffer::new();
b.lines = vec![" ".into()];
b.cursor.row = 0;
b.cursor.col = 4;
b.delete_char_before_smart(settings());
assert_eq!(b.lines[0], "");
assert_eq!(b.cursor.col, 0);
}
#[test]
fn backspace_in_indent_before_content_dedents() {
let mut b = Buffer::new();
b.lines = vec![" let x = 1;".into()];
b.cursor.row = 0;
b.cursor.col = 8; b.delete_char_before_smart(settings());
assert_eq!(b.lines[0], " let x = 1;");
assert_eq!(b.cursor.col, 4);
}
#[test]
fn backspace_past_first_non_blank_is_normal_one_char() {
let mut b = Buffer::new();
b.lines = vec![" let x = 1;".into()];
b.cursor.row = 0;
b.cursor.col = 7; b.delete_char_before_smart(settings());
assert_eq!(b.lines[0], " le x = 1;");
assert_eq!(b.cursor.col, 6);
}
#[test]
fn backspace_at_col0_of_closer_line_above_empty_joins_and_dedents() {
let mut b = Buffer::new();
b.lines = vec!["".into(), " }".into()];
b.cursor.row = 1;
b.cursor.col = 0;
b.delete_char_before_smart(settings());
assert_eq!(b.lines, vec!["}".to_string()]);
assert_eq!(b.cursor.row, 0);
assert_eq!(b.cursor.col, 0);
}
#[test]
fn backspace_at_col0_of_closer_line_above_blank_joins_and_dedents() {
let mut b = Buffer::new();
b.lines = vec![" ".into(), " }".into()];
b.cursor.row = 1;
b.cursor.col = 0;
b.delete_char_before_smart(settings());
assert_eq!(b.lines, vec![" }".to_string()]);
assert_eq!(b.cursor.row, 0);
}
#[test]
fn backspace_at_col0_above_content_does_not_dedent() {
let mut b = Buffer::new();
b.lines = vec!["foo".into(), " }".into()];
b.cursor.row = 1;
b.cursor.col = 0;
b.delete_char_before_smart(settings());
assert_eq!(b.lines, vec!["foo }".to_string()]);
assert_eq!(b.cursor.row, 0);
assert_eq!(b.cursor.col, 3);
}
}