use core::ops::Range;
use std::borrow::Cow;
use crate::autoclose;
use crate::buffer::Buffer;
use crate::coords::Point;
use crate::document::Document;
use crate::history::{GroupingHint, OpClass};
use crate::patch::Patch;
use crate::row_layout::tail_start_col;
use crate::selection::{Selection, SelectionSet};
use crate::transaction::{Committed, EditOp, TransactionError};
#[must_use]
pub fn default_indent_size() -> u32 {
4
}
impl Document {
pub fn type_char(&mut self, ch: char) {
if self.try_overtype(ch) {
return; }
if self.try_autoclose(ch) {
return; }
if self.try_surround(ch) {
return; }
let mut s = [0u8; 4];
let text: &str = ch.encode_utf8(&mut s);
let ops = self.map_selections(|sel_start, sel_end, _| EditOp::new(sel_start..sel_end, text));
self.run_edit(ops, GroupingHint::mergeable(OpClass::Type));
}
fn can_autoclose(&self, open: char, caret: u32) -> bool {
match self.buffer.char_at(caret) {
None => {}
Some(c) if c.is_whitespace() || autoclose::is_closer(c) => {}
Some(_) => return false,
}
if autoclose::is_quote(open) {
if let Some(p) = self.buffer.char_before(caret) {
if autoclose::is_word_char(p) {
return false;
}
}
let row = self.buffer.offset_to_point(caret).row;
let quotes = self.buffer.line(row).chars().filter(|&c| c == '"').count();
if quotes % 2 == 1 {
return false;
}
}
true
}
fn try_overtype(&mut self, ch: char) -> bool {
if !autoclose::is_closer(ch) {
return false;
}
let ranges = self.autoclose_ranges(); let on_close = |caret: u32| {
let i = ranges.partition_point(|r| r.end < caret);
i < ranges.len() && ranges[i].end == caret
};
let sels = self.selections.all();
let all = !ranges.is_empty()
&& sels.iter().all(|s| {
s.is_empty() && self.buffer.char_at(s.head()) == Some(ch) && on_close(s.head())
});
if !all {
return false;
}
let step = ch.len_utf8() as u32;
let carets: Vec<u32> = sels.iter().map(|s| s.head() + step).collect();
self.selections = SelectionSet::from_offsets(&carets);
self.clear_autoclose(); true
}
fn try_autoclose(&mut self, ch: char) -> bool {
let Some(close) = autoclose::opener_close(ch) else {
return false;
};
if self.selections.all().iter().any(|s| !s.is_empty()) {
return false; }
let plan: Vec<bool> =
self.selections.all().iter().map(|s| self.can_autoclose(ch, s.head())).collect();
let mut pair_text = String::with_capacity(ch.len_utf8() + close.len_utf8());
pair_text.push(ch);
pair_text.push(close);
let mut open_buf = [0u8; 4];
let open_text: &str = ch.encode_utf8(&mut open_buf);
let ops: Vec<EditOp> = self
.selections
.all()
.iter()
.zip(&plan)
.map(|(s, &pair)| {
let at = s.head();
EditOp::new(at..at, if pair { pair_text.as_str() } else { open_text })
})
.collect();
let hint = GroupingHint { op: OpClass::Type, seal_before: true, seal_after: false };
let close_len = close.len_utf8() as u32;
let mut pairs = Vec::new();
let placed = self.edit_placing(ops, hint, |patch, _| {
let edits = patch.edits();
let mut carets = Vec::with_capacity(edits.len());
for (e, &pair) in edits.iter().zip(&plan) {
if pair {
let close_off = e.new.end - close_len; carets.push(close_off); pairs.push((e.new.start, close_off)); } else {
carets.push(e.new.end); }
}
Some(SelectionSet::from_offsets(&carets))
});
if !verb_commit(placed) {
return true; }
if !pairs.is_empty() {
self.clear_autoclose();
self.add_autoclose_pairs(pairs);
}
true
}
fn try_surround(&mut self, ch: char) -> bool {
let Some(close) = autoclose::opener_close(ch) else {
return false;
};
let sels = self.selections.all();
if sels.is_empty() || sels.iter().any(|s| s.is_empty()) {
return false;
}
let (open_len, close_len) = (ch.len_utf8() as u32, close.len_utf8() as u32);
let ops: Vec<EditOp> = sels
.iter()
.map(|s| {
let content = self.buffer.slice(s.start()..s.end());
let mut wrapped = String::with_capacity(content.len() + open_len as usize + close_len as usize);
wrapped.push(ch);
wrapped.push_str(&content);
wrapped.push(close);
EditOp::new(s.start()..s.end(), &wrapped)
})
.collect();
verb_commit(self.edit_placing(ops, GroupingHint::discrete(), |patch, _| {
let ranges: Vec<(u32, u32)> =
patch.edits().iter().map(|e| (e.new.start + open_len, e.new.end - close_len)).collect();
Some(SelectionSet::from_ranges(&ranges, ranges.len() - 1))
}));
true
}
pub fn insert_text(&mut self, text: &str) {
let ops = self.map_selections(|sel_start, sel_end, _| {
EditOp::new(sel_start..sel_end, text)
});
self.run_edit(ops, GroupingHint::discrete());
}
#[must_use]
pub fn clipboard_payload(&self) -> (String, bool) {
let sels = self.selections.all();
if sels.iter().all(Selection::is_empty) {
let mut units: Vec<(u32, u32)> = sels.iter().map(|s| self.line_unit(s.head())).collect();
units.dedup();
let lines: String = units
.iter()
.map(|&(start, end)| {
let slice = self.buffer.slice(start..end);
if slice.ends_with('\n') {
slice.into_owned()
} else {
format!("{slice}\n")
}
})
.collect();
return (lines, true);
}
let joined = sels
.iter()
.filter(|s| !s.is_empty())
.map(|s| self.buffer.slice(s.start()..s.end()))
.collect::<Vec<_>>()
.join("\n");
(joined, false)
}
pub fn cut(&mut self) {
let all_empty = self.selections.all().iter().all(Selection::is_empty);
let ranges: Vec<Range<u32>> = self
.selections
.all()
.iter()
.filter_map(|sel| {
if sel.is_empty() {
all_empty.then(|| self.line_block(sel))
} else {
Some(sel.start()..sel.end())
}
})
.collect();
let ops = merge_delete_ranges(ranges.into_iter());
self.run_edit(ops, GroupingHint::discrete());
}
pub fn paste(&mut self, text: &str, entire_line: bool) {
if entire_line && self.selections.all().iter().all(Selection::is_empty) {
let mut starts: Vec<u32> =
self.selections.all().iter().map(|s| self.line_unit(s.head()).0).collect();
starts.dedup();
let ops = starts.into_iter().map(|at| EditOp::new(at..at, text)).collect();
verb_commit(self.edit_grouped(ops, GroupingHint::discrete()));
return;
}
self.insert_text(text);
}
pub fn backspace(&mut self) {
let ranges = self.autoclose_ranges();
if !ranges.is_empty() {
let empty_pair_at = |caret: u32| -> Option<Range<u32>> {
let i = ranges.partition_point(|r| r.end < caret);
ranges.get(i).filter(|r| r.end == caret && r.start + 1 == r.end).cloned()
};
let pairs: Option<Vec<Range<u32>>> = self
.selections
.all()
.iter()
.map(|s| s.is_empty().then(|| empty_pair_at(s.head())).flatten())
.collect();
if let Some(pairs) = pairs {
let ops = merge_delete_ranges(pairs.iter().map(|r| {
let close_len = self.buffer.char_at(r.end).map_or(1, |c| c.len_utf8() as u32);
r.start..r.end + close_len
}));
self.clear_autoclose();
let hint =
GroupingHint { op: OpClass::Delete, seal_before: true, seal_after: true };
self.run_edit(ops, hint);
return;
}
}
let buffer = &self.buffer;
let ranges = self.selections.all().iter().filter_map(|sel| {
if !sel.is_empty() {
return Some(sel.start()..sel.end());
}
let caret = sel.head();
if caret == 0 {
return None;
}
Some(backspace_target(buffer, caret)..caret)
});
let ops = merge_delete_ranges(ranges);
self.run_edit(ops, GroupingHint::mergeable(OpClass::Delete));
}
pub fn delete_forward(&mut self) {
let len = self.buffer.len();
let buffer = &self.buffer;
let ranges = self.selections.all().iter().filter_map(|sel| {
if !sel.is_empty() {
return Some(sel.start()..sel.end());
}
let caret = sel.head();
if caret >= len {
return None;
}
let next = buffer.char_at(caret).expect("caret < len");
Some(caret..caret + next.len_utf8() as u32)
});
let ops = merge_delete_ranges(ranges);
self.run_edit(ops, GroupingHint::mergeable(OpClass::Delete));
}
pub fn delete_word_back(&mut self) {
let buffer = &self.buffer;
let ranges = self.selections.all().iter().filter_map(|sel| {
if !sel.is_empty() {
return Some(sel.start()..sel.end());
}
let caret = sel.head();
let start = crate::movement::word_delete_left(buffer, caret);
(start < caret).then_some(start..caret)
});
let ops = merge_delete_ranges(ranges);
self.run_edit(ops, GroupingHint { op: OpClass::Delete, seal_before: true, seal_after: true });
}
pub fn delete_word_forward(&mut self) {
let len = self.buffer.len();
let buffer = &self.buffer;
let ranges = self.selections.all().iter().filter_map(|sel| {
if !sel.is_empty() {
return Some(sel.start()..sel.end());
}
let caret = sel.head();
if caret >= len {
return None;
}
let end = crate::movement::word_delete_right(buffer, caret);
(end > caret).then_some(caret..end)
});
let ops = merge_delete_ranges(ranges);
self.run_edit(ops, GroupingHint { op: OpClass::Delete, seal_before: true, seal_after: true });
}
pub fn enter(&mut self) {
let buffer = &self.buffer;
let mut backs: Vec<u32> = Vec::new();
let ops: Vec<_> = self
.selections
.all()
.iter()
.map(|sel| {
let indent = enter_indent(buffer, sel.start());
let opens = line_opens_block(buffer, sel.start());
let between = opens
&& buffer.char_before(sel.start()) == Some('{')
&& buffer.char_at(sel.end()) == Some('}');
let mut t = String::with_capacity(2 * (1 + indent.len()) + 4);
t.push('\n');
t.push_str(&indent);
if opens {
t.push_str(&indent_unit(&indent));
}
if between {
t.push('\n');
t.push_str(&indent);
backs.push(1 + indent.len() as u32); } else {
backs.push(0);
}
EditOp::new(sel.start()..sel.end(), t)
})
.collect();
let hint = GroupingHint { op: OpClass::Type, seal_before: true, seal_after: false };
verb_commit(self.edit_placing(ops, hint, |patch, _| Some(carets_pulled_back(patch, &backs))));
}
pub fn tab(&mut self) {
if self.selection_spans_multiple_lines() {
self.indent_lines(true);
} else {
let buffer = &self.buffer;
let sels = self.selections.all();
let mut ops = Vec::with_capacity(sels.len());
for sel in sels {
let (start, end) = (sel.start(), sel.end());
let p = buffer.offset_to_point(start);
let line = buffer.line(p.row);
let ws = leading_ws(&line);
let unit = IndentUnit::of(ws);
let end_col = p.col + (end - start); if end_col < level_start(ws, unit) {
if start < end {
ops.push(EditOp::delete(start..end));
}
let (at, text) = indent_insert(ws);
let mut at = start - p.col + at;
let over = sels.get(sels.partition_point(|o| o.end() <= at));
if let Some(over) = over.filter(|o| o.start() < at) {
at = over.start();
}
ops.push(EditOp::insert(at, text));
} else {
ops.push(EditOp::new(start..end, unit.text(&line[..p.col as usize])));
}
}
verb_commit(self.edit_placing(ops, GroupingHint::discrete(), |patch, _| {
let carets: Vec<u32> =
patch.edits().iter().filter(|e| !e.new.is_empty()).map(|e| e.new.end).collect();
Some(SelectionSet::from_offsets(&carets))
}));
}
}
pub fn outdent(&mut self) {
self.indent_lines(false);
}
pub fn delete_line(&mut self) {
let ranges: Vec<Range<u32>> =
self.selections.all().iter().map(|sel| self.line_block(sel)).collect();
let ops = merge_delete_ranges(ranges.into_iter());
self.run_edit(ops, GroupingHint::discrete());
}
fn line_block(&self, sel: &Selection) -> Range<u32> {
let buffer = &self.buffer;
let first = buffer.offset_to_point(sel.start()).row;
let last = buffer.offset_to_point(last_spanned_offset(sel)).row;
let start = buffer.point_to_offset(Point::new(first, 0));
if last + 1 < buffer.line_count() {
start..buffer.point_to_offset(Point::new(last + 1, 0))
} else {
start.saturating_sub(1)..buffer.len()
}
}
pub fn insert_line(&mut self, down: bool) {
let buffer = &self.buffer;
let mut backs: Vec<u32> = Vec::new();
let mut ops: Vec<EditOp> = Vec::new();
for sel in self.selections.all() {
let row = buffer.offset_to_point(sel.head()).row;
let line = buffer.line(row);
let indent = leading_ws(&line);
if down {
let at = buffer.point_to_offset(Point::new(row, buffer.line_len(row)));
if ops.last().is_some_and(|op| op.range.start == at) {
continue; }
let mut t = String::with_capacity(1 + indent.len() + 4);
t.push('\n');
t.push_str(indent);
if line_opens_block(buffer, at) {
t.push_str(&indent_unit(indent));
}
ops.push(EditOp::new(at..at, t));
backs.push(0);
} else {
let at = buffer.point_to_offset(Point::new(row, 0));
if ops.last().is_some_and(|op| op.range.start == at) {
continue;
}
ops.push(EditOp::new(at..at, format!("{indent}\n")));
backs.push(1);
}
}
verb_commit(self.edit_placing(ops, GroupingHint::discrete(), |patch, _| {
Some(carets_pulled_back(patch, &backs))
}));
}
pub fn move_line(&mut self, down: bool) {
let line_count = self.buffer.line_count();
let ends_nl = self.buffer.char_before(self.buffer.len()) == Some('\n');
let last_movable = if ends_nl { line_count.saturating_sub(2) } else { line_count - 1 };
self.relocate_line_blocks(true, |buffer, run, _, block| {
let (r0, r1) = (run.first, run.last);
if (down && r1 >= last_movable) || (!down && r0 == 0) {
return None; }
let (a, b) = if down { (r0, r1 + 1) } else { (r0 - 1, r1) };
let has_nl = b + 1 < line_count; let region_start = buffer.point_to_offset(Point::new(a, 0));
let region_end =
if has_nl { buffer.point_to_offset(Point::new(b + 1, 0)) } else { buffer.len() };
let neighbour = buffer.line(if down { b } else { a });
let (mut text, dest) = if down {
(format!("{neighbour}\n{block}"), region_start + neighbour.len() as u32 + 1)
} else {
(format!("{block}\n{neighbour}"), region_start)
};
if has_nl {
text.push('\n');
}
Some(Relocation { op: EditOp::new(region_start..region_end, text), dest, max: Some(region_end) })
});
}
pub fn copy_line(&mut self, down: bool) {
let line_count = self.buffer.line_count();
let mut grown = 0;
self.relocate_line_blocks(false, |buffer, run, block_start, block| {
let (at, text, copy_start) = if !down {
(block_start, format!("{block}\n"), block_start)
} else if run.last + 1 < line_count {
let at = buffer.point_to_offset(Point::new(run.last + 1, 0));
(at, format!("{block}\n"), at)
} else {
let at = buffer.len();
(at, format!("\n{block}"), at + 1)
};
let dest = copy_start + grown;
grown += text.len() as u32;
Some(Relocation { op: EditOp::insert(at, text), dest, max: None })
});
}
pub fn toggle_line_comment(&mut self) {
let Some(prefix) = self.line_comment().map(str::to_owned) else { return };
let rows = self.spanned_rows();
let ops: Vec<EditOp> = {
let buffer = &self.buffer;
let lines: Vec<(u32, Cow<str>)> = rows.iter().map(|&r| (r, buffer.line(r))).collect();
let non_blank: Vec<(u32, &str)> = lines
.iter()
.map(|(r, l)| (*r, l.as_ref()))
.filter(|(_, l)| !l.trim().is_empty())
.collect();
if non_blank.is_empty() {
lines
.iter()
.map(|&(row, _)| {
let at = buffer.point_to_offset(Point::new(row, buffer.line_len(row)));
EditOp::new(at..at, format!("{prefix} "))
})
.collect()
} else if non_blank.iter().any(|(_, l)| !l.trim_start().starts_with(&prefix)) {
let indent = |l: &str| tail_start_col(l);
let min_indent = non_blank.iter().map(|(_, l)| indent(l)).min().unwrap_or(0);
non_blank
.iter()
.map(|&(row, _)| {
let at = buffer.point_to_offset(Point::new(row, min_indent));
EditOp::new(at..at, format!("{prefix} "))
})
.collect()
} else {
non_blank
.iter()
.map(|&(row, line)| {
let ws = tail_start_col(line) as usize;
let start = buffer.point_to_offset(Point::new(row, ws as u32));
let mut end = start + prefix.len() as u32;
if line[ws + prefix.len()..].starts_with(' ') {
end += 1;
}
EditOp::delete(start..end)
})
.collect()
}
};
verb_commit(self.edit_grouped(ops, GroupingHint::discrete()));
}
fn selection_spans_multiple_lines(&self) -> bool {
self.selections.all().iter().any(|s| {
self.buffer.offset_to_point(s.start()).row != self.buffer.offset_to_point(s.end()).row
})
}
fn indent_lines(&mut self, deeper: bool) {
let rows = self.spanned_rows();
let buffer = &self.buffer;
let mut ops = Vec::new();
for row in rows {
let line_start = buffer.point_to_offset(Point::new(row, 0));
let line = buffer.line(row);
let ws = leading_ws(&line);
if deeper {
let (at, text) = indent_insert(ws);
ops.push(EditOp::insert(line_start + at, text));
} else {
let r = outdent_range(ws);
if !r.is_empty() {
ops.push(EditOp::delete(line_start + r.start..line_start + r.end));
}
}
}
verb_commit(self.edit_grouped(ops, GroupingHint::discrete()));
}
fn spanned_rows(&self) -> Vec<u32> {
self.line_runs(false).into_iter().flat_map(|run| run.first..=run.last).collect()
}
fn line_runs(&self, merge_adjacent: bool) -> Vec<LineRun> {
let buffer = &self.buffer;
let reach = u32::from(merge_adjacent);
let mut runs: Vec<LineRun> = Vec::new();
for (i, sel) in self.selections.all().iter().enumerate() {
let first = buffer.offset_to_point(sel.start()).row;
let last = buffer.offset_to_point(last_spanned_offset(sel)).row;
match runs.last_mut() {
Some(run) if first <= run.last + reach => {
run.last = run.last.max(last);
run.sels.end = i + 1;
}
_ => runs.push(LineRun { first, last, sels: i..i + 1 }),
}
}
runs
}
fn relocate_line_blocks(
&mut self,
merge_adjacent: bool,
mut plan: impl FnMut(&Buffer, &LineRun, u32, &str) -> Option<Relocation>,
) {
let mut spans: Vec<(u32, u32)> =
self.selections.all().iter().map(|s| (s.tail(), s.head())).collect();
let mut ops = Vec::new();
for run in self.line_runs(merge_adjacent) {
let block_start = self.buffer.point_to_offset(Point::new(run.first, 0));
let block_end =
self.buffer.point_to_offset(Point::new(run.last, self.buffer.line_len(run.last)));
let block = self.buffer.slice(block_start..block_end);
let Some(Relocation { op, dest, max }) = plan(&self.buffer, &run, block_start, &block)
else {
continue;
};
let to = |o: u32| {
let landed = dest + (o - block_start);
max.map_or(landed, |max| landed.min(max))
};
for (tail, head) in &mut spans[run.sels] {
(*tail, *head) = (to(*tail), to(*head));
}
ops.push(op);
}
if ops.is_empty() {
return;
}
let placed = self.selections.moved_to(&spans);
verb_commit(self.edit_placing(ops, GroupingHint::discrete(), |_, _| Some(placed)));
self.clear_autoclose();
}
fn map_selections(
&self,
mut f: impl FnMut(u32, u32, &Buffer) -> EditOp,
) -> Vec<EditOp> {
self.selections
.all()
.iter()
.map(|sel| f(sel.start(), sel.end(), &self.buffer))
.collect()
}
fn run_edit(&mut self, ops: Vec<EditOp>, hint: GroupingHint) {
verb_commit(self.edit_placing(ops, hint, |patch, _| {
let carets: Vec<u32> = patch.edits().iter().map(|e| e.new.end).collect();
Some(SelectionSet::from_offsets(&carets))
}));
}
}
pub(crate) fn verb_commit(result: Result<Committed, TransactionError>) -> bool {
match result {
Ok(_) => true,
Err(e) => {
debug_assert!(
!matches!(e, TransactionError::Overlap { .. }),
"a verb built overlapping ops (merge them first): {e}"
);
false
}
}
}
struct LineRun {
first: u32,
last: u32,
sels: Range<usize>,
}
struct Relocation {
op: EditOp,
dest: u32,
max: Option<u32>,
}
fn carets_pulled_back(patch: &Patch, backs: &[u32]) -> SelectionSet {
let carets: Vec<u32> = patch.edits().iter().zip(backs).map(|(e, back)| e.new.end - back).collect();
SelectionSet::from_offsets(&carets)
}
fn last_spanned_offset(sel: &Selection) -> u32 {
if sel.end() > sel.start() {
sel.end() - 1
} else {
sel.end()
}
}
fn merge_delete_ranges(ranges: impl Iterator<Item = Range<u32>>) -> Vec<EditOp> {
let mut sorted: Vec<Range<u32>> = ranges.collect();
sorted.sort_by_key(|r| r.start);
let mut merged: Vec<Range<u32>> = Vec::new();
for r in sorted {
match merged.last_mut() {
Some(last) if r.start < last.end => last.end = last.end.max(r.end),
_ => merged.push(r),
}
}
merged.into_iter().map(EditOp::delete).collect()
}
fn backspace_target(buffer: &Buffer, caret: u32) -> u32 {
let p = buffer.offset_to_point(caret);
let line = buffer.line(p.row);
if p.col > 0 && p.col <= tail_start_col(&line) {
let level = outdent_range(&line[..p.col as usize]).len() as u32;
if level > 0 {
return caret - level;
}
}
let prev = buffer.char_before(caret).expect("caret > 0");
caret - prev.len_utf8() as u32
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum IndentUnit {
Tab,
Spaces,
}
impl IndentUnit {
fn of(ws: &str) -> Self {
if ws.ends_with('\t') {
Self::Tab
} else {
Self::Spaces
}
}
fn char(self) -> char {
match self {
Self::Tab => '\t',
Self::Spaces => ' ',
}
}
fn text(self, prefix: &str) -> String {
match self {
Self::Tab => "\t".to_owned(),
Self::Spaces => {
let n = default_indent_size();
" ".repeat((n - stop_col(prefix) % n) as usize)
}
}
}
}
fn stop_col(prefix: &str) -> u32 {
(prefix.len() - prefix.rfind('\t').map_or(0, |i| i + 1)) as u32
}
fn level_start(ws: &str, unit: IndentUnit) -> u32 {
ws.trim_end_matches(unit.char()).len() as u32
}
fn indent_insert(ws: &str) -> (u32, String) {
let unit = IndentUnit::of(ws);
(level_start(ws, unit), unit.text(ws))
}
fn outdent_range(ws: &str) -> Range<u32> {
let unit = IndentUnit::of(ws);
let start = level_start(ws, unit);
let run = ws.len() as u32 - start;
let width = match unit {
IndentUnit::Tab => 1, IndentUnit::Spaces if run == 0 => 0,
IndentUnit::Spaces => (run - 1) % default_indent_size() + 1,
};
start..start + width
}
fn enter_indent(buffer: &Buffer, caret: u32) -> String {
let p = buffer.offset_to_point(caret);
let line = buffer.line(p.row);
let ws = leading_ws(&line);
ws[..ws.len().min(p.col as usize)].to_owned()
}
fn leading_ws(line: &str) -> &str {
&line[..tail_start_col(line) as usize]
}
fn indent_unit(copied: &str) -> String {
IndentUnit::of(copied).text("")
}
fn line_opens_block(buffer: &Buffer, caret: u32) -> bool {
let p = buffer.offset_to_point(caret);
buffer.line(p.row)[..p.col as usize].trim_end().ends_with('{')
}
#[cfg(test)]
mod tests {
use super::*;
fn doc(s: &str) -> Document {
Document::new(s).unwrap()
}
fn ac_count(d: &Document) -> usize {
d.autoclose_ranges().len()
}
#[test]
fn typing_inserts_and_advances_caret() {
let mut d = doc("");
d.type_char('h');
d.type_char('i');
assert_eq!(d.text(), "hi");
assert_eq!(d.selections().all()[0].head(), 2);
assert!(d.undo());
assert_eq!(d.text(), "");
}
#[test]
fn typing_over_a_selection_replaces_it() {
let mut d = doc("hello world");
d.selections = SelectionSet::new(0);
d.selections.set_single(crate::Selection::from_anchor(crate::SelectionId(0), 0, 5));
d.type_char('X');
assert_eq!(d.text(), "X world");
assert_eq!(d.selections().all()[0].head(), 1);
assert!(d.selections().all()[0].is_empty());
}
#[test]
fn autoclose_inserts_pair_then_overtypes() {
let mut d = doc("");
d.type_char('(');
assert_eq!(d.text(), "()");
assert_eq!(d.selections().all()[0].head(), 1); d.type_char(')'); assert_eq!(d.text(), "()", "overtype skips — no second )");
assert_eq!(d.selections().all()[0].head(), 2);
}
#[test]
fn autoclose_survives_typing_inside_the_pair() {
let mut d = doc("");
d.type_char('(');
d.type_char('x'); assert_eq!(d.text(), "(x)");
d.type_char(')'); assert_eq!(d.text(), "(x)");
assert_eq!(d.selections().all()[0].head(), 3);
}
#[test]
fn autoclose_guard_skips_pair_before_a_word() {
let mut d = doc("abc"); d.type_char('(');
assert_eq!(d.text(), "(abc"); assert_eq!(d.selections().all()[0].head(), 1);
}
#[test]
fn delete_word_back_and_forward_respect_word_and_newline_bounds() {
let mut d = doc("foo bar\nbaz");
d.set_selections(crate::SelectionSet::new(11)); d.delete_word_back();
assert_eq!(d.text(), "foo bar\n"); d.delete_word_back(); assert_eq!(d.text(), "foo bar");
d.set_selections(crate::SelectionSet::new(0));
d.delete_word_forward();
assert_eq!(d.text(), " bar"); }
#[test]
fn delete_word_back_eats_only_a_multi_space_run() {
let mut d = doc("foo bar"); d.set_selections(crate::SelectionSet::new(6)); d.delete_word_back();
assert_eq!(d.text(), "foobar", "2+ whitespace → deletes only the run");
}
#[test]
fn multi_caret_word_delete_merges_overlapping_ranges() {
let mut d = doc("hello");
d.set_selections(crate::SelectionSet::from_offsets(&[3, 5]));
d.delete_word_back();
assert_eq!(d.text(), "", "merged into one 0..5 delete, not dropped");
assert_eq!(d.selections().len(), 1);
let mut d = doc("hello");
d.set_selections(crate::SelectionSet::from_offsets(&[0, 2]));
d.delete_word_forward();
assert_eq!(d.text(), "", "forward merge too");
}
#[test]
fn autoclose_pair_backspace_deletes_both() {
let mut d = doc("");
d.type_char('(');
d.backspace();
assert_eq!(d.text(), "");
assert_eq!(d.selections().all()[0].head(), 0);
}
#[test]
fn autoclose_surrounds_a_selection() {
let mut d = doc("foo");
d.selections.set_single(crate::Selection::from_anchor(crate::SelectionId(0), 0, 3));
d.type_char('(');
assert_eq!(d.text(), "(foo)");
let s = d.selections().all()[0];
assert_eq!((s.start(), s.end()), (1, 4)); }
#[test]
fn autoclose_provenance_invalidates_on_caret_move() {
let mut d = doc("");
d.type_char('('); d.move_carets(crate::Motion::Left, false); d.type_char(')'); assert_eq!(d.text(), ")()");
}
#[test]
fn autoclose_quote_parity_closes_an_open_string() {
let mut d = doc("\" ");
d.selections = SelectionSet::new(2); d.type_char('"');
assert_eq!(d.text(), "\" \""); }
#[test]
fn autoclose_pairs_at_every_caret() {
let mut d = doc("a\nb\nc"); d.set_selections(crate::SelectionSet::from_offsets(&[1, 3, 5]));
d.type_char('{');
assert_eq!(d.text(), "a{}\nb{}\nc{}");
assert_eq!(ac_count(&d), 3, "one live provenance pair per caret");
d.type_char('x');
assert_eq!(d.text(), "a{x}\nb{x}\nc{x}");
}
#[test]
fn autoclose_overtypes_at_every_caret() {
let mut d = doc("a\nb\nc");
d.set_selections(crate::SelectionSet::from_offsets(&[1, 3, 5]));
d.type_char('{'); d.type_char('}'); assert_eq!(d.text(), "a{}\nb{}\nc{}", "overtype skips at every caret — no doubled closer");
assert_eq!(ac_count(&d), 0, "provenance consumed");
}
#[test]
fn autoclose_surrounds_every_selection() {
let mut d = doc("foo bar baz");
d.set_selections(crate::SelectionSet::from_ranges(&[(0, 3), (4, 7), (8, 11)], 0));
d.type_char('(');
assert_eq!(d.text(), "(foo) (bar) (baz)");
}
#[test]
fn autoclose_pair_backspace_at_every_caret() {
let mut d = doc("a\nb\nc");
d.set_selections(crate::SelectionSet::from_offsets(&[1, 3, 5]));
d.type_char('{'); d.backspace();
assert_eq!(d.text(), "a\nb\nc", "each empty pair deleted whole");
assert_eq!(ac_count(&d), 0);
}
#[test]
fn autoclose_nested_opener_supersedes_provenance() {
let mut d = doc("");
d.type_char('('); d.type_char('('); assert_eq!(ac_count(&d), 1, "only the innermost pair is live");
d.type_char(')'); assert_eq!(d.text(), "(())");
let mut e = doc("");
e.type_char('{');
e.type_char('(');
e.type_char(')');
assert_eq!(e.text(), "{()}");
}
#[test]
fn autoclose_nested_pair_backspace_deletes_the_inner() {
let mut d = doc("");
d.type_char('(');
d.type_char('('); d.backspace();
assert_eq!(d.text(), "()", "the inner empty pair is deleted whole");
}
#[test]
fn autoclose_surrounds_touching_selections() {
let mut d = doc("abcd");
d.set_selections(crate::SelectionSet::from_ranges(&[(0, 2), (2, 4)], 0));
d.type_char('(');
assert_eq!(d.text(), "(ab)(cd)");
let sels = d.selections().all();
assert_eq!((sels[0].start(), sels[0].end()), (1, 3), "interior 'ab'");
assert_eq!((sels[1].start(), sels[1].end()), (5, 7), "interior 'cd', not '(cd'");
}
#[test]
fn autoclose_per_caret_pairs_only_where_the_guard_passes() {
let mut d = doc("ab");
d.set_selections(crate::SelectionSet::from_offsets(&[0, 2]));
d.type_char('{');
assert_eq!(d.text(), "{ab{}"); assert_eq!(ac_count(&d), 1, "only the guarded caret paired");
}
#[test]
fn backspace_deletes_char_then_selection() {
let mut d = doc("abc");
d.selections = SelectionSet::new(3);
d.backspace();
assert_eq!(d.text(), "ab");
d.selections = SelectionSet::new(0);
d.selections.set_single(crate::Selection::from_anchor(crate::SelectionId(0), 0, 2));
d.backspace();
assert_eq!(d.text(), "");
}
#[test]
fn backspace_in_indentation_deletes_to_tab_stop() {
let mut d = doc(" x"); d.selections = SelectionSet::new(8); d.backspace();
assert_eq!(d.text(), " x"); }
#[test]
fn two_carets_in_one_indent_run_backspace_to_their_shared_tab_stop() {
let mut d = doc(" x"); d.set_selections(SelectionSet::from_offsets(&[3, 4]));
d.backspace();
assert_eq!(d.text(), " x", "one 0..4 delete — the union of both carets' stops");
let heads: Vec<u32> = d.selections().all().iter().map(Selection::head).collect();
assert_eq!(heads, [0], "both carets collapse onto the shared stop");
assert!(d.undo());
assert_eq!(d.text(), " x", "one undo step restores the text");
assert!(!d.can_undo(), "…and it was the only step");
}
#[test]
fn tab_stop_backspace_overlapping_a_selection_merges_into_it() {
let mut d = doc(" x");
let mut set = SelectionSet::new(3);
set.add_selection(1, 2);
d.set_selections(set);
d.backspace();
assert_eq!(d.text(), " x", "0..3 ∪ 1..2 deleted once");
let heads: Vec<u32> = d.selections().all().iter().map(Selection::head).collect();
assert_eq!(heads, [0]);
assert!(d.undo());
assert_eq!(d.text(), " x");
assert!(!d.can_undo());
}
fn heads(d: &Document) -> Vec<u32> {
d.selections().all().iter().map(Selection::head).collect()
}
fn assert_one_undo_restores(d: &mut Document, text: &str) {
assert!(d.undo());
assert_eq!(d.text(), text, "one undo restores the text");
assert!(!d.can_undo(), "…and the delete was the only step");
}
#[test]
fn adjacent_carets_char_delete_each_char_once() {
let mut d = doc("abcd");
d.set_selections(SelectionSet::from_offsets(&[1, 2]));
d.backspace();
assert_eq!((d.text().as_ref(), heads(&d)), ("cd", vec![0]));
assert_one_undo_restores(&mut d, "abcd");
let mut d = doc("abcd");
d.set_selections(SelectionSet::from_offsets(&[1, 2]));
d.delete_forward();
assert_eq!((d.text().as_ref(), heads(&d)), ("ad", vec![1]));
assert_one_undo_restores(&mut d, "abcd");
}
#[test]
fn word_delete_reaching_through_a_selection_merges_into_it() {
let mut d = doc("foo bar");
let mut set = SelectionSet::new(3); set.add_selection(1, 2);
d.set_selections(set);
d.delete_word_back();
assert_eq!((d.text().as_ref(), heads(&d)), (" bar", vec![0]), "0..3 ∪ 1..2");
assert_one_undo_restores(&mut d, "foo bar");
let mut d = doc("foo bar");
let mut set = SelectionSet::new(0); set.add_selection(1, 2);
d.set_selections(set);
d.delete_word_forward();
assert_eq!((d.text().as_ref(), heads(&d)), (" bar", vec![0]), "0..3 ∪ 1..2");
assert_one_undo_restores(&mut d, "foo bar");
}
#[test]
fn overlapping_line_blocks_delete_once() {
let mut d = doc("a\nbb\nc");
let mut set = SelectionSet::new(4); set.add_selection(0, 3); d.set_selections(set);
d.delete_line();
assert_eq!((d.text().as_ref(), heads(&d)), ("c", vec![0]), "0..5 ∪ 2..5");
assert_one_undo_restores(&mut d, "a\nbb\nc");
let mut d = doc("aaaa\nbb");
d.set_selections(SelectionSet::from_offsets(&[1, 3]));
d.cut();
assert_eq!((d.text().as_ref(), heads(&d)), ("bb", vec![0]));
assert_one_undo_restores(&mut d, "aaaa\nbb");
}
#[test]
fn multi_caret_deletes_remove_the_union_of_each_selections_lone_delete() {
let mut rng = 0x0010_7AB5_70B5_u64;
let mut next = move |m: u64| {
rng ^= rng << 13;
rng ^= rng >> 7;
rng ^= rng << 17;
rng % m
};
type Verb = fn(&mut Document);
let verbs: [(&str, Verb); 6] = [
("backspace", Document::backspace),
("delete_forward", Document::delete_forward),
("delete_word_back", Document::delete_word_back),
("delete_word_forward", Document::delete_word_forward),
("delete_line", Document::delete_line),
("cut", Document::cut),
];
const TAILS: &[&str] = &["", "x", "foo bar", "a.b", "(y)", " z"];
let mut runs = [0usize; 6];
let mut overlaps = [0usize; 6];
for case in 0..3_000 {
let text = (0..1 + next(3))
.map(|_| {
let indent: String = (0..next(10))
.map(|_| if next(8) == 0 { '\t' } else { ' ' })
.collect();
format!("{indent}{}", TAILS[next(TAILS.len() as u64) as usize])
})
.collect::<Vec<_>>()
.join("\n");
let len = text.len() as u64; let ranges: Vec<(u32, u32)> = (0..1 + next(4))
.map(|_| {
let a = next(len + 1) as u32;
(a, if next(4) == 0 { next(len + 1) as u32 } else { a })
})
.collect();
let set = SelectionSet::from_ranges(&ranges, 0);
let v = next(verbs.len() as u64) as usize;
let (name, verb) = verbs[v];
let mixed = set.all().iter().any(|s| !s.is_empty());
let mut mask = vec![false; text.len()];
let (mut starts, mut lone_total) = (Vec::new(), 0);
for sel in set.all() {
if name == "cut" && mixed && sel.is_empty() {
continue;
}
let mut lone = doc(&text);
lone.set_selections(SelectionSet::from_ranges(&[(sel.tail(), sel.head())], 0));
verb(&mut lone);
let width = text.len() - lone.text().len();
if width > 0 {
let start = lone.selections().newest().head() as usize;
mask[start..start + width].iter_mut().for_each(|m| *m = true);
starts.push(start);
lone_total += width;
}
}
let union = mask.iter().filter(|&&m| m).count();
runs[v] += 1;
overlaps[v] += usize::from(union < lone_total);
let expected: String =
text.bytes().zip(&mask).filter(|&(_, &m)| !m).map(|(b, _)| char::from(b)).collect();
let mut d = doc(&text);
d.set_selections(set);
verb(&mut d);
let what = format!("case {case}: {name} on {text:?} at {ranges:?}");
assert_eq!(d.text(), expected, "{what}: the union of the lone deletes");
for s in starts {
let at = (s - mask[..s].iter().filter(|&&m| m).count()) as u32;
assert!(
d.selections().all().iter().any(|c| c.is_empty() && c.head() == at),
"{what}: a caret where the delete from {s} began (post-edit {at})",
);
}
if union > 0 {
assert!(d.undo(), "{what}: undoable");
assert_eq!(d.text(), text, "{what}: one undo restores the text");
}
assert!(!d.can_undo(), "{what}: the whole delete was ONE undo step");
}
for (v, (name, _)) in verbs.iter().enumerate() {
if *name == "delete_forward" {
assert_eq!(overlaps[v], 0, "delete_forward cannot overlap");
} else {
assert!(
overlaps[v] > 20,
"walk too thin for {name}: runs {runs:?}, overlaps {overlaps:?}",
);
}
}
}
#[test]
fn enter_carries_indent_truncated_to_caret() {
let mut d = doc(" hello");
d.selections = SelectionSet::new(9); d.enter();
assert_eq!(d.text(), " hello\n "); let mut d2 = doc(" hello");
d2.selections = SelectionSet::new(2);
d2.enter();
assert_eq!(d2.text(), " \n hello");
}
#[test]
fn delete_line_removes_blocks_and_the_final_line_cleanly() {
let mut d = doc("one\ntwo\nthree");
d.set_selections(crate::SelectionSet::new(5)); d.delete_line();
assert_eq!(d.text(), "one\nthree");
d.set_selections(crate::SelectionSet::new(d.buffer().len()));
d.delete_line();
assert_eq!(d.text(), "one");
let mut m = doc("a\nb\nc\nd");
let mut set = crate::SelectionSet::new(0);
set.set_single(crate::Selection::from_anchor(crate::SelectionId(0), 0, 4)); m.set_selections(set);
m.delete_line();
assert_eq!(m.text(), "c\nd");
let mut t = doc("xx\nyy");
t.set_selections(crate::SelectionSet::from_offsets(&[0, 1]));
t.delete_line();
assert_eq!(t.text(), "yy");
}
#[test]
fn insert_line_below_and_above_carry_indent() {
let mut d = doc(" a\n b");
d.set_selections(crate::SelectionSet::new(4)); d.insert_line(true);
assert_eq!(d.text(), " a\n \n b");
assert_eq!(d.selections().newest().head(), 10, "caret at the new line's end");
let mut u = doc(" a");
u.set_selections(crate::SelectionSet::new(5));
u.insert_line(false);
assert_eq!(u.text(), " \n a");
assert_eq!(u.selections().newest().head(), 4);
let mut b = doc("m {");
b.set_selections(crate::SelectionSet::new(0));
b.insert_line(true);
assert_eq!(b.text(), "m {\n ");
assert_eq!(b.selections().newest().head(), 8);
}
#[test]
fn toggle_line_comment_comments_aligned_then_uncomments() {
let mut d = doc(" a\n\n b");
d.set_line_comment(Some("//"));
d.select_all();
d.toggle_line_comment();
assert_eq!(d.text(), " // a\n\n // b");
assert!(!d.selections().newest().is_empty(), "selection survives the toggle");
d.toggle_line_comment();
assert_eq!(d.text(), " a\n\n b");
let mut c = doc("x\ny");
c.set_line_comment(Some("//"));
c.set_selections(crate::SelectionSet::new(0));
c.toggle_line_comment();
assert_eq!(c.text(), "// x\ny");
let mut s = doc("//x");
s.set_line_comment(Some("//"));
s.toggle_line_comment();
assert_eq!(s.text(), "x");
let mut m = doc("// a\nb");
m.set_line_comment(Some("//"));
m.select_all();
m.toggle_line_comment();
assert_eq!(m.text(), "// // a\n// b");
}
#[test]
fn toggle_line_comment_without_a_prefix_is_a_no_op() {
let mut d = doc("abc");
d.toggle_line_comment();
assert_eq!(d.text(), "abc");
let mut b = doc("");
b.set_line_comment(Some("//"));
b.toggle_line_comment();
assert_eq!(b.text(), "// ");
assert_eq!(b.selections().newest().head(), 3, "caret rebased after the prefix");
}
#[test]
fn copy_and_cut_expand_an_empty_caret_to_its_line() {
let mut d = doc("one\ntwo\nthree");
d.set_selections(crate::SelectionSet::new(5)); assert_eq!(d.clipboard_payload(), ("two\n".to_string(), true));
d.set_selections(crate::SelectionSet::new(9)); assert_eq!(d.clipboard_payload(), ("three\n".to_string(), true));
let mut set = crate::SelectionSet::new(0);
set.set_single(crate::Selection::from_anchor(crate::SelectionId(0), 0, 3));
d.set_selections(set);
assert_eq!(d.clipboard_payload(), ("one".to_string(), false));
d.set_selections(crate::SelectionSet::new(5));
d.cut();
assert_eq!(d.text(), "one\nthree");
assert!(d.undo());
assert_eq!(d.text(), "one\ntwo\nthree");
}
#[test]
fn mixed_set_cut_never_deletes_uncopied_lines() {
let mut d = doc("foo bar\nbaz\n");
let mut set = crate::SelectionSet::new(0);
set.set_single(crate::Selection::from_anchor(crate::SelectionId(0), 0, 3)); set.add_caret(9); d.set_selections(set);
assert_eq!(d.clipboard_payload(), ("foo".to_string(), false));
d.cut();
assert_eq!(d.text(), " bar\nbaz\n", "the uncopied `baz` line survives");
}
#[test]
fn cut_on_the_final_line_takes_the_preceding_newline() {
let mut d = doc("one\ntwo");
d.set_selections(crate::SelectionSet::new(5)); assert_eq!(d.clipboard_payload(), ("two\n".to_string(), true));
d.cut();
assert_eq!(d.text(), "one", "no empty tail line remains");
let mut e = doc("abc\n");
e.set_selections(crate::SelectionSet::new(4));
e.cut();
assert_eq!(e.text(), "abc");
}
#[test]
fn two_carets_on_one_line_cut_it_once() {
let mut d = doc("aaaa\nbb");
d.set_selections(crate::SelectionSet::from_offsets(&[1, 3])); assert_eq!(d.clipboard_payload(), ("aaaa\n".to_string(), true), "the line exports once");
d.cut();
assert_eq!(d.text(), "bb", "identical ranges merged into one delete");
}
#[test]
fn entire_line_paste_inserts_above_and_keeps_the_caret() {
let mut d = doc("aaa\nbbb");
d.set_selections(crate::SelectionSet::new(5)); d.paste("two\n", true);
assert_eq!(d.text(), "aaa\ntwo\nbbb");
assert_eq!(d.selections().newest().head(), 9, "still col 1 of `bbb`");
let mut set = crate::SelectionSet::new(0);
set.set_single(crate::Selection::from_anchor(crate::SelectionId(0), 0, 3));
d.set_selections(set);
d.paste("X\n", true);
assert_eq!(d.text(), "X\n\ntwo\nbbb");
}
#[test]
fn enter_after_open_brace_indents_one_unit_deeper() {
let mut d = doc(" if {");
d.selections = SelectionSet::new(8);
d.enter();
assert_eq!(d.text(), " if {\n ");
let mut t = doc("m { ");
t.selections = SelectionSet::new(5);
t.enter();
assert_eq!(t.text(), "m { \n ");
let mut p = doc("m { x");
p.selections = SelectionSet::new(5);
p.enter();
assert_eq!(p.text(), "m { x\n");
}
#[test]
fn enter_between_braces_splits_onto_three_lines() {
let mut d = doc("");
d.type_char('{');
assert_eq!(d.text(), "{}");
d.enter();
assert_eq!(d.text(), "{\n \n}");
assert_eq!(d.selections().newest().head(), 6, "caret at the middle line's end");
let mut n = doc(" {}");
n.selections = SelectionSet::new(5);
n.enter();
assert_eq!(n.text(), " {\n \n }");
assert_eq!(n.selections().newest().head(), 14);
assert!(n.undo());
assert_eq!(n.text(), " {}");
}
#[test]
fn enter_indent_unit_matches_tab_kind() {
let mut d = doc("\tif {");
d.selections = SelectionSet::new(5);
d.enter();
assert_eq!(d.text(), "\tif {\n\t\t");
}
#[test]
fn tab_inserts_to_next_stop() {
let mut d = doc("ab");
d.selections = SelectionSet::new(2); d.tab();
assert_eq!(d.text(), "ab ");
d.tab(); assert_eq!(d.text(), "ab ");
}
#[test]
fn tab_over_a_single_line_selection_types_over() {
let mut d = doc("abcdef");
d.selections.set_single(crate::Selection::from_anchor(crate::SelectionId(0), 1, 4)); d.tab();
assert_eq!(d.text(), "a ef");
assert!(d.selections().newest().is_empty(), "types over → collapses to a caret");
}
#[test]
fn shift_tab_outdents_a_single_line_and_keeps_the_selection() {
let mut d = doc(" indented");
d.selections.set_single(crate::Selection::from_anchor(crate::SelectionId(0), 5, 9)); d.outdent();
assert_eq!(d.text(), "indented"); assert_eq!((d.selections().newest().start(), d.selections().newest().end()), (1, 5));
}
#[test]
fn block_indent_outdent_preserves_the_selection() {
let mut d = doc("a\nb\nc");
d.selections.set_single(crate::Selection::from_anchor(crate::SelectionId(0), 0, 5));
d.tab();
assert_eq!(d.text(), " a\n b\n c");
assert_eq!(d.selections().len(), 1, "no extra cursors");
let s = d.selections().newest();
assert_eq!((s.start(), s.end()), (0, 17));
assert!(!s.is_empty(), "still a selection, not a caret");
d.outdent();
assert_eq!(d.text(), "a\nb\nc");
assert_eq!(d.selections().len(), 1);
assert_eq!(
(d.selections().newest().start(), d.selections().newest().end()),
(0, 5)
);
}
fn select(d: &mut Document, anchor: u32, head: u32) {
d.set_selections(SelectionSet::from_ranges(&[(anchor, head)], 0));
}
#[track_caller]
fn assert_one_step(d: &mut Document, verb: fn(&mut Document), after: &str) {
let before = d.text().into_owned();
verb(d);
assert_eq!(d.text(), after, "{before:?}");
assert_one_undo_restores(d, &before);
}
#[test]
fn outdent_removes_a_hard_tab() {
let mut d = doc("\tfoo");
assert_one_step(&mut d, Document::outdent, "foo");
let mut d = doc("\t\tfoo"); d.set_selections(SelectionSet::new(4)); assert_one_step(&mut d, Document::outdent, "\tfoo");
}
#[test]
fn block_indent_of_a_tab_indented_block_adds_tabs() {
let mut d = doc("\tfoo\n\tbar");
d.select_all();
assert_one_step(&mut d, Document::tab, "\t\tfoo\n\t\tbar");
select(&mut d, 1, 9);
d.tab();
let s = d.selections().newest();
assert_eq!((s.start(), s.end()), (2, 11), "the block selection survives");
let mut d = doc("\t\tfoo\n\t\tbar");
d.select_all();
assert_one_step(&mut d, Document::outdent, "\tfoo\n\tbar");
}
#[test]
fn caret_tab_on_a_tab_indented_line_inserts_a_tab() {
let mut d = doc("\tfoo");
d.set_selections(SelectionSet::new(0));
assert_one_step(&mut d, Document::tab, "\t\tfoo");
d.set_selections(SelectionSet::new(4)); assert_one_step(&mut d, Document::tab, "\tfoo\t");
let mut d = doc(" foo");
d.set_selections(SelectionSet::new(1));
assert_one_step(&mut d, Document::tab, " foo");
}
#[test]
fn space_indents_snap_to_the_neighbouring_stops() {
let mut d = doc(" foo\n bar");
select(&mut d, 0, 15);
assert_one_step(&mut d, Document::tab, " foo\n bar");
select(&mut d, 0, 15);
assert_one_step(&mut d, Document::outdent, "foo\n bar");
let mut d = doc(" foo"); assert_one_step(&mut d, Document::outdent, " foo");
}
#[test]
fn mixed_leading_whitespace_moves_one_unit_of_its_last_kind() {
let mut d = doc("\t foo");
assert_one_step(&mut d, Document::outdent, "\tfoo");
let mut d = doc("\t foo"); assert_one_step(&mut d, Document::outdent, "\t foo");
let mut d = doc("\t foo\nx");
select(&mut d, 0, 8);
assert_one_step(&mut d, Document::tab, "\t foo\n x");
let mut d = doc(" \tfoo");
assert_one_step(&mut d, Document::outdent, " foo");
let mut d = doc(" \tfoo\nx");
select(&mut d, 0, 8);
assert_one_step(&mut d, Document::tab, " \t\tfoo\n x");
let mut d = doc("\t\t");
assert_one_step(&mut d, Document::outdent, "\t");
}
#[test]
fn caret_tab_before_a_mixed_indents_last_run_indents_like_block_indent() {
for (line, col, want, caret) in [
(" \tfoo", 0, " \t\tfoo", 3),
(" \tfoo", 1, " \t\tfoo", 3),
("\t foo", 0, "\t foo", 3), ] {
let mut d = doc(line);
d.set_selections(SelectionSet::new(col));
d.tab();
assert_eq!(d.text(), want, "{line:?} @ {col}");
assert_eq!(heads(&d), [caret], "{line:?} @ {col}: the caret lands past the new level");
assert_one_undo_restores(&mut d, line);
let mut b = doc(&format!("{line}\nx"));
select(&mut b, 0, line.len() as u32 + 2);
b.tab();
assert_eq!(b.text().split('\n').next(), Some(want), "{line:?}: block indent");
}
for (line, col, want) in [
(" \tfoo", 2, " \t\tfoo"),
("\t foo", 3, "\t foo"),
(" foo", 2, " foo"),
("\t\tfoo", 1, "\t\t\tfoo"),
] {
let mut d = doc(line);
d.set_selections(SelectionSet::new(col));
assert_one_step(&mut d, Document::tab, want);
}
}
#[test]
fn tab_over_a_selection_ahead_of_a_mixed_indents_last_run_never_interleaves() {
for (line, (from, to), want, caret) in [
(" \tfoo", (0, 1), " \t\tfoo", 2),
(" \tfoo", (1, 0), " \t\tfoo", 2), ("\t\t foo", (0, 1), "\t foo", 3), ] {
let mut d = doc(line);
select(&mut d, from, to);
d.tab();
assert_eq!(d.text(), want, "{line:?} {from}..{to}");
assert_eq!(heads(&d), [caret], "{line:?} {from}..{to}: the caret lands past the new level");
assert_one_undo_restores(&mut d, line);
}
}
#[test]
fn tab_from_several_selections_in_one_mixed_indent_stays_disjoint() {
for (line, sels, want, carets) in [
(" \t\tfoo", &[(0, 0), (1, 3)][..], " \t\t\tfoo", &[2, 3][..]),
(" \t\tfoo", &[(0, 1), (2, 4)][..], " \t\t\tfoo", &[2, 3][..]),
(" \tfoo", &[(0, 1), (2, 3)][..], " \t\t\tfoo", &[3, 4][..]),
] {
let mut d = doc(line);
d.set_selections(SelectionSet::from_ranges(sels, 0));
d.tab();
assert_eq!(d.text(), want, "{line:?} {sels:?}");
assert_eq!(heads(&d), carets, "{line:?} {sels:?}");
assert_one_undo_restores(&mut d, line);
}
}
#[test]
fn backspace_in_mixed_indentation_deletes_one_unit() {
let mut d = doc("\t foo"); d.set_selections(SelectionSet::new(7));
assert_one_step(&mut d, Document::backspace, "\t foo");
d.set_selections(SelectionSet::new(7));
d.backspace();
assert_eq!(heads(&d), [5], "the caret lands on the stop");
let mut d = doc(" \t\tfoo");
d.set_selections(SelectionSet::new(4));
assert_one_step(&mut d, Document::backspace, " \tfoo");
}
#[test]
fn block_indent_over_mixed_kind_lines_keeps_each_lines_kind() {
let text = "\ta\n b\n\t c\nd\n \te";
let mut d = doc(text);
d.select_all();
assert_one_step(&mut d, Document::tab, "\t\ta\n b\n\t c\n d\n \t\te");
d.select_all();
assert_one_step(&mut d, Document::outdent, "a\nb\n\tc\nd\n e");
let mut d = doc(text);
d.set_selections(SelectionSet::from_offsets(&[2, 8, 13, 15, 20]));
assert_one_step(&mut d, Document::outdent, "a\nb\n\tc\nd\n e");
}
#[test]
fn multi_cursor_typing_edits_every_caret() {
let mut d = doc("a b c");
d.selections = SelectionSet::new(0);
d.selections.add_caret(2);
d.selections.add_caret(4);
d.type_char('X');
assert_eq!(d.text(), "Xa Xb Xc");
assert_eq!(d.selections().len(), 3);
}
#[test]
fn newline_paste_is_normalized_and_splits_lines() {
let mut d = doc("");
d.insert_text("one\r\ntwo");
assert_eq!(d.text(), "one\ntwo");
assert_eq!(d.buffer().line_count(), 2);
}
#[test]
fn enter_undoes_and_redoes() {
let mut d = doc("ab");
d.selections = SelectionSet::new(1);
d.enter();
assert_eq!(d.text(), "a\nb");
assert!(d.undo());
assert_eq!(d.text(), "ab");
assert!(d.redo());
assert_eq!(d.text(), "a\nb");
}
#[test]
fn caret_tracks_text_after_undo() {
let mut d = doc("");
for ch in "hello".chars() {
d.type_char(ch);
}
d.undo(); assert!(d.selections().all()[0].head() <= d.buffer().len(), "caret stays valid");
}
#[test]
fn undo_returns_the_caret_to_the_edit() {
let mut d = doc("");
for ch in "hello".chars() {
d.type_char(ch);
}
assert_eq!(d.selections().all()[0].head(), 5, "caret after typing");
d.undo();
assert_eq!(d.buffer().text(), "", "text undone");
assert_eq!(d.selections().all()[0].head(), 0, "caret returns to the edit site");
d.redo();
assert_eq!(d.buffer().text(), "hello", "text redone");
assert_eq!(d.selections().all()[0].head(), 5, "caret follows the redo");
}
#[test]
fn undo_jumps_the_caret_to_the_edit_from_afar() {
let mut d = doc("aaa\nbbb\nccc\nddd\n");
d.set_selections(crate::SelectionSet::new(0)); d.type_char('\n'); d.set_selections(crate::SelectionSet::new(10)); assert_eq!(d.selections().all()[0].head(), 10, "caret parked in the body");
assert!(d.undo());
assert_eq!(d.buffer().text(), "aaa\nbbb\nccc\nddd\n", "the newline is gone");
assert_eq!(
d.selections().all()[0].head(),
0,
"undo jumps the caret to the reverted edit, not leaves it at 10"
);
}
}