mod autopair;
mod indent_calc;
use super::{Buffer, Editor, IndentSettings, char_to_byte};
use autopair::{
TagAutopair, auto_pair_closer, detect_open_tag, is_auto_pair_closer, should_auto_pair,
};
use indent_calc::{
add_one_indent_level, compute_new_line_indent, copy_leading_indent, strip_one_indent_level,
};
impl Editor {
pub fn insert_char(&mut self, buf: &mut Buffer, c: char) {
let line = &mut buf.lines[self.cursor.row];
let byte_idx = char_to_byte(line, self.cursor.col);
line.insert(byte_idx, c);
self.cursor.col += 1;
buf.touch();
}
pub fn insert_char_smart(&mut self, buf: &mut Buffer, c: char, indent: IndentSettings) {
let next = self.char_at_cursor(buf);
let prev = self.char_before_cursor(buf);
if is_auto_pair_closer(c) && next == Some(c) {
self.cursor.col += 1;
return;
}
if c == '>'
&& let Some(tag) = self.tag_autopair_close(buf)
{
self.insert_char(buf, '>');
let closing: String = match tag {
TagAutopair::Close(name) => format!("</{}>", name),
};
let back = closing.chars().count();
for ch in closing.chars() {
self.insert_char(buf, ch);
}
self.cursor.col -= back;
return;
}
if matches!(c, '}' | ')' | ']') && self.line_is_blank_before_cursor(buf) {
self.dedent_current_line(buf, indent);
}
self.insert_char(buf, c);
if let Some(closer) = auto_pair_closer(c)
&& should_auto_pair(c, prev, next)
{
self.insert_char(buf, closer);
self.cursor.col -= 1;
}
}
fn tag_autopair_close(&self, buf: &Buffer) -> Option<TagAutopair> {
let ext = buf
.path
.as_ref()
.and_then(|p| p.extension())
.and_then(|e| e.to_str());
detect_open_tag(&buf.lines[self.cursor.row], self.cursor.col, ext)
}
pub fn char_at_cursor(&self, buf: &Buffer) -> Option<char> {
buf.lines
.get(self.cursor.row)
.and_then(|line| line.chars().nth(self.cursor.col))
}
pub fn char_before_cursor(&self, buf: &Buffer) -> Option<char> {
if self.cursor.col == 0 {
return None;
}
buf.lines
.get(self.cursor.row)
.and_then(|line| line.chars().nth(self.cursor.col - 1))
}
pub fn line_is_blank_before_cursor(&self, buf: &Buffer) -> bool {
let line = &buf.lines[self.cursor.row];
line.chars()
.take(self.cursor.col)
.all(|c| c.is_whitespace())
}
pub fn indent_line(&mut self, buf: &mut Buffer, row: usize, indent: IndentSettings) {
if row >= buf.lines.len() {
return;
}
let line = &buf.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();
buf.lines[row].insert_str(0, &prefix);
if self.cursor.row == row {
self.cursor.col += added_chars;
}
buf.touch();
}
pub fn dedent_line(&mut self, buf: &mut Buffer, row: usize, indent: IndentSettings) {
if row >= buf.lines.len() {
return;
}
let line = buf.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);
buf.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);
}
buf.touch();
}
pub fn dedent_current_line(&mut self, buf: &mut Buffer, indent: IndentSettings) {
let line = buf.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);
buf.lines[self.cursor.row].replace_range(delete_start_byte..delete_end_byte, "");
self.cursor.col = self.cursor.col.saturating_sub(remove_chars);
buf.touch();
}
pub fn insert_newline(&mut self, buf: &mut Buffer, indent: IndentSettings) {
if self.cursor.col == 0 {
buf.lines.insert(self.cursor.row, String::new());
self.cursor.row += 1;
buf.touch();
return;
}
let line = buf.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 = buf
.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_tag_pair = prev == Some('>') && right_owned.starts_with("</");
let is_empty_pair = is_empty_tag_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();
buf.lines[self.cursor.row] = left_owned;
buf.lines.insert(self.cursor.row + 1, middle);
buf.lines.insert(self.cursor.row + 2, closer_line);
self.cursor.row += 1;
buf.touch();
return;
}
if matches!(next_ch, Some('}' | ')' | ']')) {
new_indent = strip_one_indent_level(&new_indent, indent);
}
buf.lines[self.cursor.row] = left_owned;
let mut next = new_indent.clone();
next.push_str(&right_owned);
buf.lines.insert(self.cursor.row + 1, next);
self.cursor.row += 1;
self.cursor.col = new_indent.chars().count();
buf.touch();
}
pub fn insert_text_raw(&mut self, buf: &mut Buffer, s: &str) {
if s.is_empty() {
return;
}
let normalized = s.replace("\r\n", "\n").replace('\r', "\n");
let segments: Vec<&str> = normalized.split('\n').collect();
let row = self.cursor.row;
let col = self.cursor.col;
let byte_idx = char_to_byte(&buf.lines[row], col);
if segments.len() == 1 {
buf.lines[row].insert_str(byte_idx, segments[0]);
self.cursor.col += segments[0].chars().count();
} else {
let tail = buf.lines[row].split_off(byte_idx);
buf.lines[row].push_str(segments[0]);
let last_idx = segments.len() - 1;
for (i, seg) in segments.iter().enumerate().skip(1) {
let mut new_line =
String::with_capacity(seg.len() + if i == last_idx { tail.len() } else { 0 });
new_line.push_str(seg);
if i == last_idx {
new_line.push_str(&tail);
}
buf.lines.insert(row + i, new_line);
}
self.cursor.row = row + last_idx;
self.cursor.col = segments[last_idx].chars().count();
}
buf.touch();
}
pub fn insert_line_below(&mut self, buf: &mut Buffer, indent: IndentSettings) {
let reference = buf.lines[self.cursor.row].clone();
let new_indent =
compute_new_line_indent(&reference, self.cursor.row, &buf.highlighter, indent);
let col = new_indent.chars().count();
buf.lines.insert(self.cursor.row + 1, new_indent);
self.cursor.row += 1;
self.cursor.col = col;
buf.touch();
}
pub fn insert_line_above(&mut self, buf: &mut Buffer, indent: IndentSettings) {
let new_indent = copy_leading_indent(&buf.lines[self.cursor.row], indent);
let col = new_indent.chars().count();
buf.lines.insert(self.cursor.row, new_indent);
self.cursor.col = col;
buf.touch();
}
pub fn delete_char_under_cursor(&mut self, buf: &mut Buffer) {
let line = &mut buf.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(), "");
buf.touch();
self.clamp_col(buf, false);
}
}
pub fn delete_char_before(&mut self, buf: &mut Buffer) {
if self.cursor.col > 0 {
let line = &mut buf.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;
buf.touch();
} else if self.cursor.row > 0 {
let line = buf.lines.remove(self.cursor.row);
self.cursor.row -= 1;
self.cursor.col = buf.lines[self.cursor.row].chars().count();
buf.lines[self.cursor.row].push_str(&line);
buf.touch();
}
}
pub fn replace_char(&mut self, buf: &mut Buffer, ch: char) {
let line = &mut buf.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());
buf.touch();
}
pub fn delete_char_before_smart(&mut self, buf: &mut Buffer, indent: IndentSettings) {
let prev = self.char_before_cursor(buf);
let next = self.char_at_cursor(buf);
if let (Some(p), Some(n)) = (prev, next)
&& auto_pair_closer(p) == Some(n)
{
let line = &mut buf.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;
buf.touch();
return;
}
if self.cursor.col > 0 && self.line_is_blank_before_cursor(buf) {
let indent_char = if indent.use_tabs { '\t' } else { ' ' };
if self.char_before_cursor(buf) == Some(indent_char) {
self.dedent_current_line(buf, indent);
return;
}
}
if self.cursor.col == 0 && self.cursor.row > 0 {
let prev_blank = buf.lines[self.cursor.row - 1]
.chars()
.all(|c| c.is_whitespace());
let curr_starts_with_closer = matches!(
buf.lines[self.cursor.row]
.chars()
.find(|c| !c.is_whitespace()),
Some('}' | ')' | ']')
);
if prev_blank && curr_starts_with_closer {
self.delete_char_before(buf);
self.dedent_current_line(buf, indent);
return;
}
}
self.delete_char_before(buf);
}
}
#[cfg(test)]
mod tests {
use super::super::Ed;
use super::*;
fn settings() -> IndentSettings {
IndentSettings {
width: 4,
use_tabs: false,
}
}
#[test]
fn indent_line_adds_spaces_for_empty_leading() {
let mut b = Ed::new();
b.buffer.lines = vec!["let x = 1;".into()];
b.cursor.row = 0;
b.cursor.col = 4;
b.indent_line(0, settings());
assert_eq!(b.buffer.lines[0], " let x = 1;");
assert_eq!(b.cursor.col, 8);
}
#[test]
fn indent_line_uses_tab_when_leading_has_tab() {
let mut b = Ed::new();
b.buffer.lines = vec!["\tx".into()];
b.cursor.row = 0;
b.indent_line(0, settings());
assert_eq!(b.buffer.lines[0], "\t\tx");
}
#[test]
fn indent_line_falls_back_to_use_tabs_on_blank_leading() {
let mut b = Ed::new();
b.buffer.lines = vec!["x".into()];
let s = IndentSettings {
width: 4,
use_tabs: true,
};
b.indent_line(0, s);
assert_eq!(b.buffer.lines[0], "\tx");
}
#[test]
fn dedent_line_removes_one_level_of_spaces() {
let mut b = Ed::new();
b.buffer.lines = vec![" x".into()];
b.cursor.row = 0;
b.cursor.col = 8;
b.dedent_line(0, settings());
assert_eq!(b.buffer.lines[0], " x");
assert_eq!(b.cursor.col, 4);
}
#[test]
fn dedent_line_rounds_partial_indent_down() {
let mut b = Ed::new();
b.buffer.lines = vec![" x".into()]; b.dedent_line(0, settings());
assert_eq!(b.buffer.lines[0], " x");
}
#[test]
fn dedent_line_strips_trailing_tab() {
let mut b = Ed::new();
b.buffer.lines = vec!["\t\tx".into()];
b.dedent_line(0, settings());
assert_eq!(b.buffer.lines[0], "\tx");
}
#[test]
fn dedent_line_noop_on_no_leading_whitespace() {
let mut b = Ed::new();
b.buffer.lines = vec!["x".into()];
b.cursor.col = 0;
b.dedent_line(0, settings());
assert_eq!(b.buffer.lines[0], "x");
assert_eq!(b.cursor.col, 0);
}
#[test]
fn open_below_copies_leading_whitespace() {
let mut b = Ed::new();
b.buffer.lines = vec![" let x = 1;".into(), " let y = 2;".into()];
b.cursor.row = 0;
b.insert_line_below(settings());
assert_eq!(b.buffer.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 = Ed::new();
b.buffer.lines = vec!["fn foo() {".into(), "}".into()];
b.cursor.row = 0;
b.insert_line_below(settings());
assert_eq!(b.buffer.lines[1], " ");
assert_eq!(b.cursor.col, 4);
}
#[test]
fn open_below_uses_tabs_when_reference_does() {
let mut b = Ed::new();
b.buffer.lines = vec!["\tfn foo() {".into(), "}".into()];
b.cursor.row = 0;
b.insert_line_below(settings());
assert_eq!(b.buffer.lines[1], "\t\t");
}
#[test]
fn open_above_copies_indent_without_adding_level() {
let mut b = Ed::new();
b.buffer.lines = vec![" let x = 1;".into()];
b.cursor.row = 0;
b.insert_line_above(settings());
assert_eq!(b.buffer.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 = Ed::new();
b.buffer.lines = vec!["func main() {".into()];
b.cursor.row = 0;
b.cursor.col = 0;
b.insert_newline(settings());
assert_eq!(b.buffer.lines[0], "");
assert_eq!(b.buffer.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 = Ed::new();
b.buffer.lines = vec![" let x = 1;".into()];
b.cursor.row = 0;
b.cursor.col = 0;
b.insert_newline(settings());
assert_eq!(b.buffer.lines[0], "");
assert_eq!(b.buffer.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 = Ed::new();
b.buffer.lines = vec!["func main() {".into()];
b.cursor.row = 0;
b.cursor.col = 13; b.insert_newline(settings());
assert_eq!(b.buffer.lines[0], "func main() {");
assert_eq!(b.buffer.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 = Ed::new();
b.buffer.lines = vec!["func main {".into()];
b.cursor.row = 0;
b.cursor.col = 10; b.insert_newline(settings());
assert_eq!(b.buffer.lines[0], "func main ");
assert_eq!(b.buffer.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 = Ed::new();
b.buffer.lines = vec![" if cond ".into(), "(x) {}".into()];
b.buffer.lines = vec![" if cond (x".into()];
b.cursor.row = 0;
b.cursor.col = 12; b.insert_newline(settings());
assert_eq!(b.buffer.lines[0], " if cond ");
assert_eq!(b.buffer.lines[1], " (x");
assert_eq!(b.cursor.col, 4);
}
#[test]
fn newline_splits_and_carries_indent() {
let mut b = Ed::new();
b.buffer.lines = vec![" let x = foo + bar;".into()];
b.cursor.row = 0;
b.cursor.col = 16; b.insert_newline(settings());
assert_eq!(b.buffer.lines[0], " let x = foo ");
assert_eq!(b.buffer.lines[1], " + bar;");
assert_eq!(b.cursor.col, 4);
}
#[test]
fn close_bracket_dedents_when_line_is_blank() {
let mut b = Ed::new();
b.buffer.lines = vec!["fn foo() {".into(), " ".into()];
b.cursor.row = 1;
b.cursor.col = 8;
b.insert_char_smart('}', settings());
assert_eq!(b.buffer.lines[1], " }");
assert_eq!(b.cursor.col, 5);
}
#[test]
fn close_bracket_no_dedent_when_text_precedes() {
let mut b = Ed::new();
b.buffer.lines = vec![" let x = HashMap::new(".into()];
b.cursor.row = 0;
b.cursor.col = 25;
b.insert_char_smart(')', settings());
assert_eq!(b.buffer.lines[0], " let x = HashMap::new()");
assert_eq!(b.cursor.col, 26);
}
#[test]
fn close_bracket_dedents_partial_indent() {
let mut b = Ed::new();
b.buffer.lines = vec![" ".into()];
b.cursor.row = 0;
b.cursor.col = 7;
b.insert_char_smart(']', settings());
assert_eq!(b.buffer.lines[0], " ]");
assert_eq!(b.cursor.col, 5);
}
#[test]
fn close_bracket_dedents_tab_indent() {
let mut b = Ed::new();
b.buffer.lines = vec!["\t\t".into()];
b.cursor.row = 0;
b.cursor.col = 2;
b.insert_char_smart('}', settings());
assert_eq!(b.buffer.lines[0], "\t}");
assert_eq!(b.cursor.col, 2);
}
#[test]
fn close_bracket_clears_indent_when_already_at_one_level() {
let mut b = Ed::new();
b.buffer.lines = vec![" ".into()];
b.cursor.row = 0;
b.cursor.col = 4;
b.insert_char_smart('}', settings());
assert_eq!(b.buffer.lines[0], "}");
assert_eq!(b.cursor.col, 1);
}
#[test]
fn auto_pair_inserts_matching_closer_and_keeps_cursor_between() {
let mut b = Ed::new();
b.buffer.lines = vec!["foo".into()];
b.cursor.row = 0;
b.cursor.col = 3;
b.insert_char_smart('(', settings());
assert_eq!(b.buffer.lines[0], "foo()");
assert_eq!(b.cursor.col, 4);
}
#[test]
fn auto_pair_skip_over_closer_when_next_char_matches() {
let mut b = Ed::new();
b.buffer.lines = vec!["()".into()];
b.cursor.row = 0;
b.cursor.col = 1; b.insert_char_smart(')', settings());
assert_eq!(b.buffer.lines[0], "()");
assert_eq!(b.cursor.col, 2);
}
#[test]
fn auto_pair_suppressed_when_next_char_is_word() {
let mut b = Ed::new();
b.buffer.lines = vec!["foo".into()];
b.cursor.row = 0;
b.cursor.col = 0; b.insert_char_smart('(', settings());
assert_eq!(b.buffer.lines[0], "(foo");
assert_eq!(b.cursor.col, 1);
}
#[test]
fn auto_pair_quote_suppressed_after_word_char() {
let mut b = Ed::new();
b.buffer.lines = vec!["it".into()];
b.cursor.row = 0;
b.cursor.col = 2;
b.insert_char_smart('\'', settings());
assert_eq!(b.buffer.lines[0], "it'");
assert_eq!(b.cursor.col, 3);
}
#[test]
fn auto_pair_quote_pairs_after_punctuation() {
let mut b = Ed::new();
b.buffer.lines = vec!["print(".into()];
b.cursor.row = 0;
b.cursor.col = 6;
b.insert_char_smart('"', settings());
assert_eq!(b.buffer.lines[0], "print(\"\"");
assert_eq!(b.cursor.col, 7);
}
#[test]
fn delete_char_before_smart_removes_empty_pair() {
let mut b = Ed::new();
b.buffer.lines = vec!["foo()".into()];
b.cursor.row = 0;
b.cursor.col = 4; b.delete_char_before_smart(settings());
assert_eq!(b.buffer.lines[0], "foo");
assert_eq!(b.cursor.col, 3);
}
#[test]
fn delete_char_before_smart_falls_through_when_not_empty_pair() {
let mut b = Ed::new();
b.buffer.lines = vec!["(x)".into()];
b.cursor.row = 0;
b.cursor.col = 1;
b.delete_char_before_smart(settings());
assert_eq!(b.buffer.lines[0], "x)");
assert_eq!(b.cursor.col, 0);
}
#[test]
fn newline_inside_empty_braces_spreads_three_lines() {
let mut b = Ed::new();
b.buffer.lines = vec!["fn foo() {}".into()];
b.cursor.row = 0;
b.cursor.col = 10; b.insert_newline(settings());
assert_eq!(b.buffer.lines[0], "fn foo() {");
assert_eq!(b.buffer.lines[1], " ");
assert_eq!(b.buffer.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 = Ed::new();
b.buffer.lines = vec![" if cond {}".into()];
b.cursor.row = 0;
b.cursor.col = 13; b.insert_newline(settings());
assert_eq!(b.buffer.lines[0], " if cond {");
assert_eq!(b.buffer.lines[1], " ");
assert_eq!(b.buffer.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 = Ed::new();
b.buffer.lines = vec!["foo()".into()];
b.cursor.row = 0;
b.cursor.col = 4; b.insert_newline(settings());
assert_eq!(b.buffer.lines[0], "foo(");
assert_eq!(b.buffer.lines[1], " ");
assert_eq!(b.buffer.lines[2], ")");
assert_eq!(b.cursor.col, 4);
}
#[test]
fn newline_before_closer_strips_one_indent_level() {
let mut b = Ed::new();
b.buffer.lines = vec![" bar();}".into()];
b.cursor.row = 0;
b.cursor.col = 14; b.insert_newline(settings());
assert_eq!(b.buffer.lines[0], " bar();");
assert_eq!(b.buffer.lines[1], " }");
assert_eq!(b.cursor.col, 4);
}
#[test]
fn newline_before_closer_clears_indent_at_one_level() {
let mut b = Ed::new();
b.buffer.lines = vec![" bar();]".into()];
b.cursor.row = 0;
b.cursor.col = 10; b.insert_newline(settings());
assert_eq!(b.buffer.lines[0], " bar();");
assert_eq!(b.buffer.lines[1], "]");
assert_eq!(b.cursor.col, 0);
}
#[test]
fn newline_before_closer_strips_one_tab() {
let mut b = Ed::new();
b.buffer.lines = vec!["\t\tbar();)".into()];
b.cursor.row = 0;
b.cursor.col = 8; b.insert_newline(settings());
assert_eq!(b.buffer.lines[0], "\t\tbar();");
assert_eq!(b.buffer.lines[1], "\t)");
}
#[test]
fn backspace_before_closer_collapses_one_indent_level() {
let mut b = Ed::new();
b.buffer.lines = vec![" }".into()];
b.cursor.row = 0;
b.cursor.col = 8; b.delete_char_before_smart(settings());
assert_eq!(b.buffer.lines[0], " }");
assert_eq!(b.cursor.col, 4);
}
#[test]
fn backspace_before_closer_clears_indent_at_one_level() {
let mut b = Ed::new();
b.buffer.lines = vec![" }".into()];
b.cursor.row = 0;
b.cursor.col = 4;
b.delete_char_before_smart(settings());
assert_eq!(b.buffer.lines[0], "}");
assert_eq!(b.cursor.col, 0);
}
#[test]
fn backspace_before_closer_strips_tab() {
let mut b = Ed::new();
b.buffer.lines = vec!["\t\t]".into()];
b.cursor.row = 0;
b.cursor.col = 2;
b.delete_char_before_smart(settings());
assert_eq!(b.buffer.lines[0], "\t]");
assert_eq!(b.cursor.col, 1);
}
#[test]
fn backspace_does_not_dedent_when_text_precedes_closer() {
let mut b = Ed::new();
b.buffer.lines = vec![" x)".into()];
b.cursor.row = 0;
b.cursor.col = 5; b.delete_char_before_smart(settings());
assert_eq!(b.buffer.lines[0], " )");
assert_eq!(b.cursor.col, 4);
}
#[test]
fn backspace_inside_closer_line_indent_dedents() {
let mut b = Ed::new();
b.buffer.lines = vec![" }".into()];
b.cursor.row = 0;
b.cursor.col = 4; b.delete_char_before_smart(settings());
assert_eq!(b.buffer.lines[0], " }");
assert_eq!(b.cursor.col, 0);
}
#[test]
fn backspace_on_closer_line_with_trailing_content_dedents() {
let mut b = Ed::new();
b.buffer.lines = vec![" });".into()];
b.cursor.row = 0;
b.cursor.col = 8; b.delete_char_before_smart(settings());
assert_eq!(b.buffer.lines[0], " });");
assert_eq!(b.cursor.col, 4);
}
#[test]
fn backspace_inside_pure_whitespace_line_dedents() {
let mut b = Ed::new();
b.buffer.lines = vec![" ".into()];
b.cursor.row = 0;
b.cursor.col = 4;
b.delete_char_before_smart(settings());
assert_eq!(b.buffer.lines[0], "");
assert_eq!(b.cursor.col, 0);
}
#[test]
fn backspace_in_indent_before_content_dedents() {
let mut b = Ed::new();
b.buffer.lines = vec![" let x = 1;".into()];
b.cursor.row = 0;
b.cursor.col = 8; b.delete_char_before_smart(settings());
assert_eq!(b.buffer.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 = Ed::new();
b.buffer.lines = vec![" let x = 1;".into()];
b.cursor.row = 0;
b.cursor.col = 7; b.delete_char_before_smart(settings());
assert_eq!(b.buffer.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 = Ed::new();
b.buffer.lines = vec!["".into(), " }".into()];
b.cursor.row = 1;
b.cursor.col = 0;
b.delete_char_before_smart(settings());
assert_eq!(b.buffer.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 = Ed::new();
b.buffer.lines = vec![" ".into(), " }".into()];
b.cursor.row = 1;
b.cursor.col = 0;
b.delete_char_before_smart(settings());
assert_eq!(b.buffer.lines, vec![" }".to_string()]);
assert_eq!(b.cursor.row, 0);
}
#[test]
fn backspace_at_col0_above_content_does_not_dedent() {
let mut b = Ed::new();
b.buffer.lines = vec!["foo".into(), " }".into()];
b.cursor.row = 1;
b.cursor.col = 0;
b.delete_char_before_smart(settings());
assert_eq!(b.buffer.lines, vec!["foo }".to_string()]);
assert_eq!(b.cursor.row, 0);
assert_eq!(b.cursor.col, 3);
}
#[test]
fn backspace_in_space_indent_under_tab_settings_falls_back_to_one_char() {
let mut b = Ed::new();
b.buffer.lines = vec![" foo".into()];
b.cursor.row = 0;
b.cursor.col = 4;
let indent = IndentSettings {
width: 4,
use_tabs: true,
};
b.delete_char_before_smart(indent);
assert_eq!(b.buffer.lines[0], " foo");
assert_eq!(b.cursor.col, 3);
}
#[test]
fn backspace_in_tab_indent_under_space_settings_falls_back_to_one_char() {
let mut b = Ed::new();
b.buffer.lines = vec!["\tfoo".into()];
b.cursor.row = 0;
b.cursor.col = 1;
b.delete_char_before_smart(settings());
assert_eq!(b.buffer.lines[0], "foo");
assert_eq!(b.cursor.col, 0);
}
#[test]
fn insert_text_raw_single_line_splices_at_cursor() {
let mut b = Ed::new();
b.buffer.lines = vec!["abef".into()];
b.cursor.row = 0;
b.cursor.col = 2;
b.insert_text_raw("cd");
assert_eq!(b.buffer.lines[0], "abcdef");
assert_eq!(b.cursor.col, 4);
}
#[test]
fn insert_text_raw_multiline_preserves_indent_verbatim() {
let mut b = Ed::new();
b.buffer.lines = vec![" pub fn foo() {".into(), " }".into()];
b.cursor.row = 0;
b.cursor.col = 18; b.insert_text_raw("\n let x = 1;\n let y = 2;");
assert_eq!(b.buffer.lines[0], " pub fn foo() {");
assert_eq!(b.buffer.lines[1], " let x = 1;");
assert_eq!(b.buffer.lines[2], " let y = 2;");
assert_eq!(b.buffer.lines[3], " }");
assert_eq!(b.cursor.row, 2);
assert_eq!(b.cursor.col, 18);
}
#[test]
fn insert_text_raw_normalizes_crlf() {
let mut b = Ed::new();
b.buffer.lines = vec!["".into()];
b.insert_text_raw("foo\r\nbar");
assert_eq!(b.buffer.lines, vec!["foo".to_string(), "bar".to_string()]);
}
#[test]
fn insert_text_raw_normalizes_bare_cr() {
let mut b = Ed::new();
b.buffer.lines = vec!["".into()];
b.insert_text_raw("foo\rbar\rbaz");
assert_eq!(
b.buffer.lines,
vec!["foo".to_string(), "bar".to_string(), "baz".to_string()]
);
}
#[test]
fn tag_autopair_closes_on_gt_in_tsx() {
let mut b = Ed::new();
b.buffer.path = Some(std::path::PathBuf::from("app.tsx"));
b.buffer.lines = vec!["<div".into()];
b.cursor.row = 0;
b.cursor.col = 4;
b.insert_char_smart('>', settings());
assert_eq!(b.buffer.lines[0], "<div></div>");
assert_eq!(b.cursor.col, 5);
}
#[test]
fn tag_autopair_fragment_in_tsx() {
let mut b = Ed::new();
b.buffer.path = Some(std::path::PathBuf::from("c.tsx"));
b.buffer.lines = vec!["<".into()];
b.cursor.row = 0;
b.cursor.col = 1;
b.insert_char_smart('>', settings());
assert_eq!(b.buffer.lines[0], "<></>");
assert_eq!(b.cursor.col, 2);
}
#[test]
fn tag_autopair_skipped_outside_supported_ext() {
let mut b = Ed::new();
b.buffer.path = Some(std::path::PathBuf::from("a.rs"));
b.buffer.lines = vec!["if x <y".into()];
b.cursor.row = 0;
b.cursor.col = 7;
b.insert_char_smart('>', settings());
assert_eq!(b.buffer.lines[0], "if x <y>");
assert_eq!(b.cursor.col, 8);
}
#[test]
fn tag_autopair_self_closing_left_alone() {
let mut b = Ed::new();
b.buffer.path = Some(std::path::PathBuf::from("a.tsx"));
b.buffer.lines = vec!["<img /".into()];
b.cursor.row = 0;
b.cursor.col = 6;
b.insert_char_smart('>', settings());
assert_eq!(b.buffer.lines[0], "<img />");
assert_eq!(b.cursor.col, 7);
}
#[test]
fn newline_inside_empty_tag_pair_spreads_three_lines() {
let mut b = Ed::new();
b.buffer.lines = vec![" <div></div>".into()];
b.cursor.row = 0;
b.cursor.col = 9; b.insert_newline(settings());
assert_eq!(b.buffer.lines[0], " <div>");
assert_eq!(b.buffer.lines[1], " ");
assert_eq!(b.buffer.lines[2], " </div>");
assert_eq!(b.cursor.row, 1);
assert_eq!(b.cursor.col, 8);
}
#[test]
fn newline_inside_empty_fragment_spreads_three_lines() {
let mut b = Ed::new();
b.buffer.lines = vec!["<></>".into()];
b.cursor.row = 0;
b.cursor.col = 2; b.insert_newline(settings());
assert_eq!(b.buffer.lines[0], "<>");
assert_eq!(b.buffer.lines[1], " ");
assert_eq!(b.buffer.lines[2], "</>");
assert_eq!(b.cursor.row, 1);
assert_eq!(b.cursor.col, 4);
}
#[test]
fn insert_text_raw_splits_existing_tail_onto_last_line() {
let mut b = Ed::new();
b.buffer.lines = vec!["abcXYZ".into()];
b.cursor.row = 0;
b.cursor.col = 3;
b.insert_text_raw("1\n2\n3");
assert_eq!(b.buffer.lines, vec!["abc1", "2", "3XYZ"]);
assert_eq!(b.cursor.row, 2);
assert_eq!(b.cursor.col, 1);
}
}