scrive_core/verbs.rs
1//! The editing verbs: type, backspace, delete, enter, paste, (out)dent, and
2//! move / copy line.
3//! Each turns the current selections into one atomic multi-range transaction,
4//! then places a caret after each edit — so every verb is multi-cursor-correct
5//! for free (one op per selection). The placement is handed to the commit
6//! itself (`Document::edit_placing`), never assigned once the edit returns, so
7//! history records it as the unit's after-selections and redo restores exactly
8//! the carets the verb left.
9//!
10//! The one catch is deletion: a selection's delete can reach past its
11//! neighbour (a tab-stop backspace, a word, a line block), so delete verbs
12//! merge their ranges first (`merge_delete_ranges`) — the engine rejects
13//! overlapping ops — and every verb commits through `verb_commit`, which turns
14//! an overlap that slipped through into a debug panic instead of a keystroke
15//! that silently does nothing.
16//!
17//! Two load-bearing semantics live here: **typing over a selection replaces
18//! it** (one transaction, caret after), and **Enter's autoindent is the leading
19//! whitespace truncated to the caret column** (never duplicated) — plus the two
20//! brace rules (one unit deeper after a line-opening `{`; `{|}` splits onto
21//! three lines), always computed for *inserted* text only, never re-indenting
22//! existing lines, so autoindent cannot fight the user. Clipboard text is
23//! normalized to LF on the way in; the LF→OS-flavor re-expansion for copy-out
24//! lives in the widget layer.
25
26use core::ops::Range;
27use std::borrow::Cow;
28
29use crate::autoclose;
30use crate::buffer::Buffer;
31use crate::coords::Point;
32use crate::document::Document;
33use crate::history::{GroupingHint, OpClass};
34use crate::patch::Patch;
35use crate::row_layout::tail_start_col;
36use crate::selection::{Selection, SelectionSet};
37use crate::transaction::{Committed, EditOp, TransactionError};
38
39/// Spaces per indent level: the stop spacing Tab, Shift+Tab, the block
40/// (out)dent and backspace's tab-stop delete use on a space-indented line (a
41/// tab-indented line moves by one hard tab instead).
42///
43/// Deliberately a SEPARATE knob from [`crate::display_map::default_tab_size`]
44/// (an *editing* width vs a *display* width — mainstream editors keep both).
45/// They happen to share the value 4; if either ever changes, the widget's
46/// indent guides key on the display knob and Tab/indent verbs on this one.
47/// Space stops after a hard tab count from the tab, so they line up with the
48/// drawn ones while the display width is a multiple of this one.
49#[must_use]
50pub fn default_indent_size() -> u32 {
51 4
52}
53
54impl Document {
55 /// Insert `ch` at every caret, replacing any non-empty selection. Merges
56 /// into the current typing run for undo.
57 pub fn type_char(&mut self, ch: char) {
58 // Auto-close, applied uniformly across ALL carets (a single caret is
59 // just the one-element case). Each helper handles the whole keystroke and
60 // returns true; overtype takes precedence (a quote is opener AND closer).
61 if self.try_overtype(ch) {
62 return; // a closer typed over its own auto-inserted close, at every caret
63 }
64 if self.try_autoclose(ch) {
65 return; // an opener at empty carets → insert the pair (per-caret guard)
66 }
67 if self.try_surround(ch) {
68 return; // an opener over non-empty selections → surround each
69 }
70 // Plain insert (also the mixed / guard-failing / non-pair case). Replaces
71 // every selection with `ch`; `edit_grouped` rebases and validates live
72 // provenance, so typing inside a pair keeps overtype available.
73 let mut s = [0u8; 4];
74 let text: &str = ch.encode_utf8(&mut s);
75 let ops = self.map_selections(|sel_start, sel_end, _| EditOp::new(sel_start..sel_end, text));
76 self.run_edit(ops, GroupingHint::mergeable(OpClass::Type));
77 }
78
79 /// Whether pair-insert of `open` is allowed at an empty `caret` — the guard
80 /// that keeps auto-close from firing where a closer would be unwelcome.
81 fn can_autoclose(&self, open: char, caret: u32) -> bool {
82 // The char after the caret must be whitespace, EOL, or a closer —
83 // otherwise the pair would split a word or an existing token.
84 match self.buffer.char_at(caret) {
85 None => {}
86 Some(c) if c.is_whitespace() || autoclose::is_closer(c) => {}
87 Some(_) => return false,
88 }
89 if autoclose::is_quote(open) {
90 // For a quote, the char before must not be a word char (else it is
91 // an apostrophe or a suffix quote, not an opening string delimiter).
92 if let Some(p) = self.buffer.char_before(caret) {
93 if autoclose::is_word_char(p) {
94 return false;
95 }
96 }
97 // An odd count of quotes on the line ⇒ this one closes an already
98 // open string; insert it literally rather than pairing.
99 let row = self.buffer.offset_to_point(caret).row;
100 let quotes = self.buffer.line(row).chars().filter(|&c| c == '"').count();
101 if quotes % 2 == 1 {
102 return false;
103 }
104 }
105 true
106 }
107
108 /// Overtype: if `ch` is a closer and EVERY selection is an empty caret
109 /// sitting exactly on a live pair's matching close, skip over each (no
110 /// insert) and consume all provenance. All-or-nothing, so a mixed set falls
111 /// through. Returns whether it handled the keystroke.
112 fn try_overtype(&mut self, ch: char) -> bool {
113 if !autoclose::is_closer(ch) {
114 return false;
115 }
116 let ranges = self.autoclose_ranges(); // ascending, disjoint ⇒ ends ascending
117 let on_close = |caret: u32| {
118 let i = ranges.partition_point(|r| r.end < caret);
119 i < ranges.len() && ranges[i].end == caret
120 };
121 let sels = self.selections.all();
122 let all = !ranges.is_empty()
123 && sels.iter().all(|s| {
124 s.is_empty() && self.buffer.char_at(s.head()) == Some(ch) && on_close(s.head())
125 });
126 if !all {
127 return false;
128 }
129 let step = ch.len_utf8() as u32;
130 let carets: Vec<u32> = sels.iter().map(|s| s.head() + step).collect();
131 self.selections = SelectionSet::from_offsets(&carets);
132 self.clear_autoclose(); // the pairs are consumed
133 true
134 }
135
136 /// Auto-close: if `ch` is an opener and EVERY selection is an empty caret,
137 /// insert `open+close` at each caret that passes the guards and a bare
138 /// `open` at the rest (a per-caret decision, as mainstream editors make it),
139 /// place each caret just after the opener, and record provenance for the
140 /// paired ones. Returns whether it handled the keystroke (false if any
141 /// selection is non-empty).
142 fn try_autoclose(&mut self, ch: char) -> bool {
143 let Some(close) = autoclose::opener_close(ch) else {
144 return false;
145 };
146 if self.selections.all().iter().any(|s| !s.is_empty()) {
147 return false; // a non-empty selection ⇒ surround/plain, not pair-insert
148 }
149 // Per-caret pair-or-plain decision, computed before the edit.
150 let plan: Vec<bool> =
151 self.selections.all().iter().map(|s| self.can_autoclose(ch, s.head())).collect();
152 let mut pair_text = String::with_capacity(ch.len_utf8() + close.len_utf8());
153 pair_text.push(ch);
154 pair_text.push(close);
155 let mut open_buf = [0u8; 4];
156 let open_text: &str = ch.encode_utf8(&mut open_buf);
157 let ops: Vec<EditOp> = self
158 .selections
159 .all()
160 .iter()
161 .zip(&plan)
162 .map(|(s, &pair)| {
163 let at = s.head();
164 EditOp::new(at..at, if pair { pair_text.as_str() } else { open_text })
165 })
166 .collect();
167 let hint = GroupingHint { op: OpClass::Type, seal_before: true, seal_after: false };
168 // A caret just after each opener; provenance for the paired ones.
169 let close_len = close.len_utf8() as u32;
170 let mut pairs = Vec::new();
171 let placed = self.edit_placing(ops, hint, |patch, _| {
172 let edits = patch.edits();
173 let mut carets = Vec::with_capacity(edits.len());
174 for (e, &pair) in edits.iter().zip(&plan) {
175 if pair {
176 let close_off = e.new.end - close_len; // start of the close char
177 carets.push(close_off); // caret between open and close
178 pairs.push((e.new.start, close_off)); // [open, close)
179 } else {
180 carets.push(e.new.end); // after the bare opener
181 }
182 }
183 Some(SelectionSet::from_offsets(&carets))
184 });
185 if !verb_commit(placed) {
186 return true; // refused: no pair landed, so no provenance to record
187 }
188 if !pairs.is_empty() {
189 // Supersede any prior provenance — e.g. an outer pair whose tracked
190 // range grew to enclose this insertion (nested `(` inside `()`) — so
191 // the live set stays disjoint. Nested pairs must not accumulate, or
192 // overtype's ends-ascending binary search picks the wrong pair and a
193 // closer inserts literally (`(()))`). One generation of provenance,
194 // always.
195 self.clear_autoclose();
196 self.add_autoclose_pairs(pairs);
197 }
198 true
199 }
200
201 /// Surround: if `ch` is an opener and EVERY selection is non-empty, wrap
202 /// each in the pair with the selection staying on the interior. Returns
203 /// whether it handled the keystroke.
204 fn try_surround(&mut self, ch: char) -> bool {
205 let Some(close) = autoclose::opener_close(ch) else {
206 return false;
207 };
208 let sels = self.selections.all();
209 if sels.is_empty() || sels.iter().any(|s| s.is_empty()) {
210 return false;
211 }
212 let (open_len, close_len) = (ch.len_utf8() as u32, close.len_utf8() as u32);
213 // ONE replace op per selection (open + content + close). A single op per
214 // selection keeps TOUCHING selections independent: two zero-width inserts
215 // at the same offset (opener then closer) would be ambiguous to bias, so
216 // a successor selection starting where its predecessor ends could swallow
217 // the inserted opener. One replace has an unambiguous span instead.
218 let ops: Vec<EditOp> = sels
219 .iter()
220 .map(|s| {
221 let content = self.buffer.slice(s.start()..s.end());
222 let mut wrapped = String::with_capacity(content.len() + open_len as usize + close_len as usize);
223 wrapped.push(ch);
224 wrapped.push_str(&content);
225 wrapped.push(close);
226 EditOp::new(s.start()..s.end(), &wrapped)
227 })
228 .collect();
229 // Each selection stays on its interior, read straight from the edits.
230 verb_commit(self.edit_placing(ops, GroupingHint::discrete(), |patch, _| {
231 let ranges: Vec<(u32, u32)> =
232 patch.edits().iter().map(|e| (e.new.start + open_len, e.new.end - close_len)).collect();
233 Some(SelectionSet::from_ranges(&ranges, ranges.len() - 1))
234 }));
235 true
236 }
237
238 /// Insert `text` (e.g. a paste) at every caret, replacing selections. A
239 /// discrete undo step; `\r\n|\r` is normalized to LF.
240 pub fn insert_text(&mut self, text: &str) {
241 let ops = self.map_selections(|sel_start, sel_end, _| {
242 EditOp::new(sel_start..sel_end, text)
243 });
244 self.run_edit(ops, GroupingHint::discrete());
245 }
246
247 /// The text Copy/Cut should put on the clipboard, with the `is_entire_line`
248 /// flag the paste side honors. Each non-empty selection contributes its
249 /// text, joined by newlines. When **every** selection is an empty caret,
250 /// each caret contributes its ENTIRE line — the one `Document::line_unit`
251 /// rule — including the newline (a final line without a terminator still
252 /// exports one so the paste splices cleanly), duplicate lines deduplicated,
253 /// and the flag is `true`. LF-only; the OS-flavor re-expansion and the side
254 /// table live in the widget.
255 #[must_use]
256 pub fn clipboard_payload(&self) -> (String, bool) {
257 let sels = self.selections.all();
258 if sels.iter().all(Selection::is_empty) {
259 let mut units: Vec<(u32, u32)> = sels.iter().map(|s| self.line_unit(s.head())).collect();
260 units.dedup();
261 let lines: String = units
262 .iter()
263 .map(|&(start, end)| {
264 let slice = self.buffer.slice(start..end);
265 if slice.ends_with('\n') {
266 slice.into_owned()
267 } else {
268 format!("{slice}\n")
269 }
270 })
271 .collect();
272 return (lines, true);
273 }
274 let joined = sels
275 .iter()
276 .filter(|s| !s.is_empty())
277 .map(|s| self.buffer.slice(s.start()..s.end()))
278 .collect::<Vec<_>>()
279 .join("\n");
280 (joined, false)
281 }
282
283 /// Cut's edit half: cut is copy-then-delete, so this deletes exactly what
284 /// [`Document::clipboard_payload`] exported — non-empty selections; or,
285 /// when EVERY selection is an empty caret (the payload's whole-line mode,
286 /// same gate), each caret's `line_block`. In a
287 /// mixed set the empty carets reached the clipboard as nothing, so they
288 /// delete nothing — cut never destroys text that was not copied. The
289 /// final-line block takes its PRECEDING newline while the payload
290 /// synthesizes a trailing one: the paste splices a whole line either way,
291 /// and no empty tail line is left behind. Overlapping ranges (two carets on
292 /// one line) merge; one discrete transaction, caret after each deletion.
293 pub fn cut(&mut self) {
294 let all_empty = self.selections.all().iter().all(Selection::is_empty);
295 let ranges: Vec<Range<u32>> = self
296 .selections
297 .all()
298 .iter()
299 .filter_map(|sel| {
300 if sel.is_empty() {
301 all_empty.then(|| self.line_block(sel))
302 } else {
303 Some(sel.start()..sel.end())
304 }
305 })
306 .collect();
307 let ops = merge_delete_ranges(ranges.into_iter());
308 self.run_edit(ops, GroupingHint::discrete());
309 }
310
311 /// Paste. Plain: replace each selection, caret after (a discrete step).
312 /// `entire_line` at all-empty carets: insert before each caret's line start
313 /// with **no caret placement** — the carets rebase past the insertion and
314 /// stay put on their own lines, matching mainstream editors.
315 pub fn paste(&mut self, text: &str, entire_line: bool) {
316 if entire_line && self.selections.all().iter().all(Selection::is_empty) {
317 let mut starts: Vec<u32> =
318 self.selections.all().iter().map(|s| self.line_unit(s.head()).0).collect();
319 starts.dedup();
320 let ops = starts.into_iter().map(|at| EditOp::new(at..at, text)).collect();
321 // `edit_grouped` rebases the selections through the patch — never
322 // `run_edit`, whose caret-after would yank the caret off its line.
323 verb_commit(self.edit_grouped(ops, GroupingHint::discrete()));
324 return;
325 }
326 self.insert_text(text);
327 }
328
329 /// Backspace: delete the selection, or the character before an empty caret
330 /// — one indent level when the caret sits in leading whitespace: a hard
331 /// tab, or spaces back to the previous stop (the unit Shift+Tab removes).
332 pub fn backspace(&mut self) {
333 // Pair-backspace: every empty caret strictly between a live *empty*
334 // pair deletes both chars (`open + 1 == close` ⇒ nothing typed between
335 // them). All-or-nothing (symmetric with overtype); a mixed set falls
336 // through to the ordinary per-caret backspace below.
337 let ranges = self.autoclose_ranges();
338 if !ranges.is_empty() {
339 let empty_pair_at = |caret: u32| -> Option<Range<u32>> {
340 let i = ranges.partition_point(|r| r.end < caret);
341 ranges.get(i).filter(|r| r.end == caret && r.start + 1 == r.end).cloned()
342 };
343 let pairs: Option<Vec<Range<u32>>> = self
344 .selections
345 .all()
346 .iter()
347 .map(|s| s.is_empty().then(|| empty_pair_at(s.head())).flatten())
348 .collect();
349 if let Some(pairs) = pairs {
350 // Distinct carets sit in distinct (disjoint) pairs, so these
351 // only ever touch — merged anyway, like every delete verb.
352 let ops = merge_delete_ranges(pairs.iter().map(|r| {
353 let close_len = self.buffer.char_at(r.end).map_or(1, |c| c.len_utf8() as u32);
354 r.start..r.end + close_len
355 }));
356 self.clear_autoclose();
357 let hint =
358 GroupingHint { op: OpClass::Delete, seal_before: true, seal_after: true };
359 self.run_edit(ops, hint);
360 return;
361 }
362 }
363 let buffer = &self.buffer;
364 let ranges = self.selections.all().iter().filter_map(|sel| {
365 if !sel.is_empty() {
366 return Some(sel.start()..sel.end());
367 }
368 let caret = sel.head();
369 if caret == 0 {
370 return None;
371 }
372 Some(backspace_target(buffer, caret)..caret)
373 });
374 // A tab-stop delete reaches back past its own caret's neighbourhood —
375 // carets at cols 3 and 4 both delete to stop 0 (`0..3`, `0..4`), and a
376 // caret deletes straight through a selection inside its indent run —
377 // so the ranges can OVERLAP and must merge before the transaction.
378 let ops = merge_delete_ranges(ranges);
379 self.run_edit(ops, GroupingHint::mergeable(OpClass::Delete));
380 }
381
382 /// Forward delete: delete the selection, or the character after an empty
383 /// caret (merging the next line when at end of line).
384 pub fn delete_forward(&mut self) {
385 let len = self.buffer.len();
386 let buffer = &self.buffer;
387 let ranges = self.selections.all().iter().filter_map(|sel| {
388 if !sel.is_empty() {
389 return Some(sel.start()..sel.end());
390 }
391 let caret = sel.head();
392 if caret >= len {
393 return None;
394 }
395 let next = buffer.char_at(caret).expect("caret < len");
396 Some(caret..caret + next.len_utf8() as u32)
397 });
398 // One char forward from a caret can only TOUCH the next selection (a
399 // caret touching a selection merged into it), never overlap — merged
400 // anyway, so every delete verb builds its ops the same disjoint way.
401 let ops = merge_delete_ranges(ranges);
402 self.run_edit(ops, GroupingHint::mergeable(OpClass::Delete));
403 }
404
405 /// Delete the word before each caret — Ctrl+Backspace. A non-empty
406 /// selection deletes as-is; an empty caret deletes back to the previous word
407 /// start, or *only* the newline at column 0 (never newline + previous word).
408 /// A discrete undo step (word-delete seals the run on both sides).
409 ///
410 /// Two carets inside one word yield *overlapping* delete ranges, which the
411 /// transaction engine rejects — so the ranges are merged first (the shared
412 /// span is deleted once and the carets collapse via the selection merge
413 /// rule), as for every delete verb.
414 pub fn delete_word_back(&mut self) {
415 let buffer = &self.buffer;
416 let ranges = self.selections.all().iter().filter_map(|sel| {
417 if !sel.is_empty() {
418 return Some(sel.start()..sel.end());
419 }
420 let caret = sel.head();
421 let start = crate::movement::word_delete_left(buffer, caret);
422 (start < caret).then_some(start..caret)
423 });
424 let ops = merge_delete_ranges(ranges);
425 self.run_edit(ops, GroupingHint { op: OpClass::Delete, seal_before: true, seal_after: true });
426 }
427
428 /// Delete the word after each caret — Ctrl+Delete, the
429 /// [`delete_word_back`](Self::delete_word_back) mirror (overlapping ranges
430 /// merged the same way).
431 pub fn delete_word_forward(&mut self) {
432 let len = self.buffer.len();
433 let buffer = &self.buffer;
434 let ranges = self.selections.all().iter().filter_map(|sel| {
435 if !sel.is_empty() {
436 return Some(sel.start()..sel.end());
437 }
438 let caret = sel.head();
439 if caret >= len {
440 return None;
441 }
442 let end = crate::movement::word_delete_right(buffer, caret);
443 (end > caret).then_some(caret..end)
444 });
445 let ops = merge_delete_ranges(ranges);
446 self.run_edit(ops, GroupingHint { op: OpClass::Delete, seal_before: true, seal_after: true });
447 }
448
449 /// Enter: split the line at every caret, carrying the current line's
450 /// leading indentation (truncated to the caret column — never duplicated),
451 /// plus the two brace rules (both computed from raw text at insert time —
452 /// existing lines are never re-indented, so autoindent cannot fight the
453 /// user):
454 ///
455 /// - **(a)** the current line's text left of the caret, right-trimmed,
456 /// ends with `{` → the new line gets one extra `indent_unit`;
457 /// - **(b)** the caret sits exactly between a pair (`{|}`) → insert two
458 /// lines (`\n indent+unit \n indent`), closer dedented onto its own
459 /// line, caret at the end of the middle line (one line up from the
460 /// inserted text's end, indented one unit deeper than the closer).
461 ///
462 /// Its own undo step (seals before; typing after merges into the run).
463 pub fn enter(&mut self) {
464 let buffer = &self.buffer;
465 // Per-op distance from the inserted text's end BACK to the caret —
466 // 0 except for rule (b), where the caret lands one line up.
467 let mut backs: Vec<u32> = Vec::new();
468 let ops: Vec<_> = self
469 .selections
470 .all()
471 .iter()
472 .map(|sel| {
473 let indent = enter_indent(buffer, sel.start());
474 let opens = line_opens_block(buffer, sel.start());
475 let between = opens
476 && buffer.char_before(sel.start()) == Some('{')
477 && buffer.char_at(sel.end()) == Some('}');
478 let mut t = String::with_capacity(2 * (1 + indent.len()) + 4);
479 t.push('\n');
480 t.push_str(&indent);
481 if opens {
482 t.push_str(&indent_unit(&indent));
483 }
484 if between {
485 t.push('\n');
486 t.push_str(&indent);
487 backs.push(1 + indent.len() as u32); // back over "\n" + indent
488 } else {
489 backs.push(0);
490 }
491 EditOp::new(sel.start()..sel.end(), t)
492 })
493 .collect();
494 let hint = GroupingHint { op: OpClass::Type, seal_before: true, seal_after: false };
495 // `run_edit`'s caret-after, minus each op's rule-(b) pull-back.
496 verb_commit(self.edit_placing(ops, hint, |patch, _| Some(carets_pulled_back(patch, &backs))));
497 }
498
499 /// Tab: at an empty caret insert one indent unit of the caret's line — a
500 /// hard tab on a tab-indented line, else spaces to the next indent stop;
501 /// over a single-line selection type that unit over it; over a multi-line
502 /// selection indent every spanned line one level, each by its own unit
503 /// (the indent-unit rule Enter and Shift+Tab share). The unit goes at the
504 /// caret, or in the selection's place, except from a caret or selection
505 /// ending ahead of a mixed indent's trailing run (the `··` of `··\t`, the
506 /// `\t` of `\t··`): there the selection is deleted and the unit goes where
507 /// block indent puts it, so no Tab interleaves kinds within a line's
508 /// indent. VS Code types the unit in the selection's place even there — a
509 /// `·` selected in `··\t` becomes `\t·\t` — which is exactly the
510 /// interleave; deleting the selection still replaces it with one level,
511 /// just not between chars of the other kind.
512 pub fn tab(&mut self) {
513 if self.selection_spans_multiple_lines() {
514 // Multi-line selection → indent the whole block, selection preserved.
515 self.indent_lines(true);
516 } else {
517 // Carets and single-line selections → insert / replace-with one
518 // unit. A single-line selection *types over* (as mainstream editors
519 // do): it does NOT indent the whole line.
520 let buffer = &self.buffer;
521 let sels = self.selections.all();
522 let mut ops = Vec::with_capacity(sels.len());
523 for sel in sels {
524 let (start, end) = (sel.start(), sel.end());
525 let p = buffer.offset_to_point(start);
526 let line = buffer.line(p.row);
527 let ws = leading_ws(&line);
528 // The LINE's kind (its whole leading whitespace), not the
529 // prefix's: a caret at column 0 of `\tfoo` must add a tab,
530 // where the empty prefix alone would say spaces.
531 let unit = IndentUnit::of(ws);
532 // A caret or selection ENDING ahead of the trailing run of the
533 // unit's char would drop the unit between chars of the OTHER
534 // kind (`\t··\t`). It takes block indent's insertion instead —
535 // the run's start, one level padded as block indent pads it —
536 // and a selection is still typed over: deleted where it lies.
537 // Ending in or at an edge of the run (or past the indent), the
538 // unit's own char joins its own run: in the selection's place,
539 // padded from there (VS Code's caret Tab).
540 let end_col = p.col + (end - start); // single-line: same row
541 if end_col < level_start(ws, unit) {
542 if start < end {
543 ops.push(EditOp::delete(start..end));
544 }
545 let (at, text) = indent_insert(ws);
546 let mut at = start - p.col + at;
547 // A later selection typing over the run's start (it starts
548 // ahead of the run and ends in it) holds `at` strictly
549 // inside — an overlap. The run then starts where that
550 // selection does, so the unit goes there, touching its op.
551 // Sorted and disjoint ⇒ at most one selection holds `at`.
552 let over = sels.get(sels.partition_point(|o| o.end() <= at));
553 if let Some(over) = over.filter(|o| o.start() < at) {
554 at = over.start();
555 }
556 ops.push(EditOp::insert(at, text));
557 } else {
558 ops.push(EditOp::new(start..end, unit.text(&line[..p.col as usize])));
559 }
560 }
561 // A caret just past each typed unit: every unit is non-empty text,
562 // so the edits that insert are exactly the units — the type-over
563 // deletes (empty post-edit ranges) carry no caret.
564 verb_commit(self.edit_placing(ops, GroupingHint::discrete(), |patch, _| {
565 let carets: Vec<u32> =
566 patch.edits().iter().filter(|e| !e.new.is_empty()).map(|e| e.new.end).collect();
567 Some(SelectionSet::from_offsets(&carets))
568 }));
569 }
570 }
571
572 /// Shift+Tab: outdent every spanned line by one indent level — its last
573 /// hard tab, or its trailing spaces back to the previous stop. Unlike Tab
574 /// (which types over a single-line selection), Shift+Tab always outdents —
575 /// carets, single-line selections, and multi-line selections alike; the
576 /// selection is preserved.
577 pub fn outdent(&mut self) {
578 self.indent_lines(false);
579 }
580
581 /// Delete each selection's whole-line block (Ctrl+Shift+K) — the one
582 /// `line_block` rule per selection. Overlapping
583 /// blocks (two carets on one line) merge; one discrete step, caret after.
584 pub fn delete_line(&mut self) {
585 let ranges: Vec<Range<u32>> =
586 self.selections.all().iter().map(|sel| self.line_block(sel)).collect();
587 let ops = merge_delete_ranges(ranges.into_iter());
588 self.run_edit(ops, GroupingHint::discrete());
589 }
590
591 /// THE whole-line **deletion block** for a selection: every spanned line
592 /// (a selection ending exactly at a line start does not span it — the
593 /// [`last_spanned_offset`] rule shared with `line_runs`) plus ONE
594 /// bounding newline: the trailing one, or, when the block reaches the
595 /// buffer's final line, the PRECEDING one — so no empty tail line is left
596 /// behind. Shared by [`Document::delete_line`] and the
597 /// whole-line [`Document::cut`]. (Copy's per-line text is the different
598 /// `Document::line_unit` fact — content never takes a preceding `\n`.)
599 fn line_block(&self, sel: &Selection) -> Range<u32> {
600 let buffer = &self.buffer;
601 let first = buffer.offset_to_point(sel.start()).row;
602 let last = buffer.offset_to_point(last_spanned_offset(sel)).row;
603 let start = buffer.point_to_offset(Point::new(first, 0));
604 if last + 1 < buffer.line_count() {
605 start..buffer.point_to_offset(Point::new(last + 1, 0))
606 } else {
607 start.saturating_sub(1)..buffer.len()
608 }
609 }
610
611 /// Open a fresh, indent-carrying line below (`down`, Ctrl+Enter) or above
612 /// (Ctrl+Shift+Enter) each caret's line — without splitting the line the
613 /// caret sat on — and land the caret at the new line's end. A line below
614 /// a block-opening `{` line gains one indent unit, like Enter at the line's
615 /// end. One discrete transaction.
616 pub fn insert_line(&mut self, down: bool) {
617 let buffer = &self.buffer;
618 let mut backs: Vec<u32> = Vec::new();
619 let mut ops: Vec<EditOp> = Vec::new();
620 for sel in self.selections.all() {
621 let row = buffer.offset_to_point(sel.head()).row;
622 let line = buffer.line(row);
623 let indent = leading_ws(&line);
624 if down {
625 let at = buffer.point_to_offset(Point::new(row, buffer.line_len(row)));
626 if ops.last().is_some_and(|op| op.range.start == at) {
627 continue; // second caret on the same line — one new line
628 }
629 let mut t = String::with_capacity(1 + indent.len() + 4);
630 t.push('\n');
631 t.push_str(indent);
632 if line_opens_block(buffer, at) {
633 t.push_str(&indent_unit(indent));
634 }
635 ops.push(EditOp::new(at..at, t));
636 backs.push(0);
637 } else {
638 let at = buffer.point_to_offset(Point::new(row, 0));
639 if ops.last().is_some_and(|op| op.range.start == at) {
640 continue;
641 }
642 // The new line's indent, then the newline that pushes the
643 // current line down — caret pulls back over the `\n`.
644 ops.push(EditOp::new(at..at, format!("{indent}\n")));
645 backs.push(1);
646 }
647 }
648 verb_commit(self.edit_placing(ops, GroupingHint::discrete(), |patch, _| {
649 Some(carets_pulled_back(patch, &backs))
650 }));
651 }
652
653 /// Move every selected line-block up or down one line (Alt+↑/↓). A block is
654 /// a run of whole lines the selections span (`line_runs`: a
655 /// whole-line selection ending at column 0 does not span that row), runs on
656 /// consecutive rows merged — each block swaps with its neighbour line, which
657 /// must therefore belong to no other block. Every selection rides its moved
658 /// text, id and direction kept. A block at the document edge — or, moving
659 /// down, against the trailing empty line — stays put while the others move.
660 /// One undo step; never re-indents.
661 pub fn move_line(&mut self, down: bool) {
662 let line_count = self.buffer.line_count();
663 let ends_nl = self.buffer.char_before(self.buffer.len()) == Some('\n');
664 // The last row a real line occupies (the trailing empty line, if any, is
665 // not movable — moving into it would drop the terminator).
666 let last_movable = if ends_nl { line_count.saturating_sub(2) } else { line_count - 1 };
667 // Each swap rewrites its region (block + neighbour) with text of the same
668 // length, so offsets outside the moved regions never shift: every
669 // destination is the pre-edit one, with nothing to accumulate.
670 self.relocate_line_blocks(true, |buffer, run, _, block| {
671 let (r0, r1) = (run.first, run.last);
672 if (down && r1 >= last_movable) || (!down && r0 == 0) {
673 return None; // at the edge: stays put while the others move
674 }
675 let (a, b) = if down { (r0, r1 + 1) } else { (r0 - 1, r1) };
676 let has_nl = b + 1 < line_count; // row `b` carries a trailing newline
677 let region_start = buffer.point_to_offset(Point::new(a, 0));
678 let region_end =
679 if has_nl { buffer.point_to_offset(Point::new(b + 1, 0)) } else { buffer.len() };
680 let neighbour = buffer.line(if down { b } else { a });
681 let (mut text, dest) = if down {
682 // The line below moves to the front; the block shifts down past
683 // it and its `\n`.
684 (format!("{neighbour}\n{block}"), region_start + neighbour.len() as u32 + 1)
685 } else {
686 // The line above moves to the back; the block takes its place.
687 (format!("{block}\n{neighbour}"), region_start)
688 };
689 if has_nl {
690 text.push('\n');
691 }
692 // A whole-line selection's end sits past the block's `\n`; moved onto
693 // the buffer's final line the block has no `\n`, so that end clamps to
694 // the region (buffer) end.
695 Some(Relocation { op: EditOp::new(region_start..region_end, text), dest, max: Some(region_end) })
696 });
697 }
698
699 /// Duplicate every selected line-block above (`down = false`) or below
700 /// (`down = true`) — Shift+Alt+↑/↓. A block is a run of whole lines the
701 /// selections span (`line_runs`), runs that share a row merged
702 /// so no line is copied twice; runs on consecutive rows stay apart, so
703 /// carets on consecutive lines each copy their own line (VS Code). Every
704 /// selection lands on its block's copy — the lower one copying down, the
705 /// upper one copying up — id and direction kept. One undo step.
706 pub fn copy_line(&mut self, down: bool) {
707 let line_count = self.buffer.line_count();
708 // Bytes the earlier copies insert. The runs — and so the insertion
709 // points — ascend, so everything after an insertion, every later copy
710 // included, lands that much further on post-edit.
711 let mut grown = 0;
712 self.relocate_line_blocks(false, |buffer, run, block_start, block| {
713 // Where the copy is inserted, its text, and where its block starts.
714 let (at, text, copy_start) = if !down {
715 // Above: the copy takes the block's place and pushes it down.
716 (block_start, format!("{block}\n"), block_start)
717 } else if run.last + 1 < line_count {
718 let at = buffer.point_to_offset(Point::new(run.last + 1, 0));
719 (at, format!("{block}\n"), at)
720 } else {
721 // Duplicating the final line (no trailing newline): prepend one.
722 let at = buffer.len();
723 (at, format!("\n{block}"), at + 1)
724 };
725 let dest = copy_start + grown;
726 grown += text.len() as u32;
727 // The copy is the block verbatim, so a whole-line selection's end,
728 // past the block's `\n`, lands past the copy's: nothing clamps.
729 Some(Relocation { op: EditOp::insert(at, text), dest, max: None })
730 });
731 }
732
733 /// Toggle the line comment on every line the selections span (Ctrl+/).
734 /// If ANY spanned non-blank line is uncommented, comment them all —
735 /// `prefix + " "` inserted at the block's minimum indent column,
736 /// so the markers align — otherwise strip each line's prefix (and one
737 /// following space). Blank lines are skipped, unless EVERY spanned line is
738 /// blank (the start-a-comment-here case, which appends the prefix). One
739 /// transaction; the selections rebase through it and survive. A no-op until
740 /// the app injects a prefix ([`Document::set_line_comment`]) — the core
741 /// knows no language.
742 pub fn toggle_line_comment(&mut self) {
743 let Some(prefix) = self.line_comment().map(str::to_owned) else { return };
744 let rows = self.spanned_rows();
745 let ops: Vec<EditOp> = {
746 let buffer = &self.buffer;
747 let lines: Vec<(u32, Cow<str>)> = rows.iter().map(|&r| (r, buffer.line(r))).collect();
748 let non_blank: Vec<(u32, &str)> = lines
749 .iter()
750 .map(|(r, l)| (*r, l.as_ref()))
751 .filter(|(_, l)| !l.trim().is_empty())
752 .collect();
753 if non_blank.is_empty() {
754 // Every spanned line is blank: start a comment on each.
755 lines
756 .iter()
757 .map(|&(row, _)| {
758 let at = buffer.point_to_offset(Point::new(row, buffer.line_len(row)));
759 EditOp::new(at..at, format!("{prefix} "))
760 })
761 .collect()
762 } else if non_blank.iter().any(|(_, l)| !l.trim_start().starts_with(&prefix)) {
763 // Comment: markers aligned at the block's minimum indent.
764 let indent = |l: &str| tail_start_col(l);
765 let min_indent = non_blank.iter().map(|(_, l)| indent(l)).min().unwrap_or(0);
766 non_blank
767 .iter()
768 .map(|&(row, _)| {
769 let at = buffer.point_to_offset(Point::new(row, min_indent));
770 EditOp::new(at..at, format!("{prefix} "))
771 })
772 .collect()
773 } else {
774 // Uncomment: strip the prefix plus one following space.
775 non_blank
776 .iter()
777 .map(|&(row, line)| {
778 let ws = tail_start_col(line) as usize;
779 let start = buffer.point_to_offset(Point::new(row, ws as u32));
780 let mut end = start + prefix.len() as u32;
781 if line[ws + prefix.len()..].starts_with(' ') {
782 end += 1;
783 }
784 EditOp::delete(start..end)
785 })
786 .collect()
787 }
788 };
789 // `edit_grouped` rebases the selections through the patch, so the
790 // selection (or caret) survives the toggle in place.
791 verb_commit(self.edit_grouped(ops, GroupingHint::discrete()));
792 }
793
794 /// Whether any selection spans more than one line — the trigger for block
795 /// (out)dent (vs. type-over / caret indent).
796 fn selection_spans_multiple_lines(&self) -> bool {
797 self.selections.all().iter().any(|s| {
798 self.buffer.offset_to_point(s.start()).row != self.buffer.offset_to_point(s.end()).row
799 })
800 }
801
802 /// Indent (`deeper`) or outdent every line the selections span by one
803 /// level of ITS OWN unit ([`indent_insert`] / [`outdent_range`] of its
804 /// leading whitespace — a block mixing tab- and space-indented lines moves
805 /// each in kind), as one transaction. The **selection is preserved** — it
806 /// rebases through the edit (its endpoints shift with the indent), as
807 /// mainstream editors do, rather than collapsing to one caret per line.
808 fn indent_lines(&mut self, deeper: bool) {
809 let rows = self.spanned_rows();
810 let buffer = &self.buffer;
811 let mut ops = Vec::new();
812 for row in rows {
813 let line_start = buffer.point_to_offset(Point::new(row, 0));
814 let line = buffer.line(row);
815 let ws = leading_ws(&line);
816 if deeper {
817 let (at, text) = indent_insert(ws);
818 ops.push(EditOp::insert(line_start + at, text));
819 } else {
820 let r = outdent_range(ws);
821 if !r.is_empty() {
822 ops.push(EditOp::delete(line_start + r.start..line_start + r.end));
823 }
824 }
825 }
826 // `edit_grouped` rebases the selections through the patch (a range's
827 // start biases Left and its end biases Right), so the selection survives
828 // the (out)dent as a selection — not `run_edit`'s one-caret-per-op, which
829 // would collapse it to a caret.
830 verb_commit(self.edit_grouped(ops, GroupingHint::discrete()));
831 }
832
833 /// The distinct rows any selection touches, ascending (for block (out)dent
834 /// and toggle-comment): the [`line_runs`](Self::line_runs) flattened — runs
835 /// sharing a row are already merged, so no row repeats.
836 fn spanned_rows(&self) -> Vec<u32> {
837 self.line_runs(false).into_iter().flat_map(|run| run.first..=run.last).collect()
838 }
839
840 /// The whole-line **runs** the selections span — the unit move-line and
841 /// copy-line act on. Each selection spans `first..=last` rows by the
842 /// [`last_spanned_offset`] rule (a whole-line selection ending at column 0
843 /// does not span that row); runs that share a row merge, and with
844 /// `merge_adjacent` so do runs on consecutive rows. Every selection lies in
845 /// exactly one run, and the set's sorted, disjoint order makes each run's
846 /// selections a contiguous index range of [`SelectionSet::all`].
847 fn line_runs(&self, merge_adjacent: bool) -> Vec<LineRun> {
848 let buffer = &self.buffer;
849 let reach = u32::from(merge_adjacent);
850 let mut runs: Vec<LineRun> = Vec::new();
851 for (i, sel) in self.selections.all().iter().enumerate() {
852 let first = buffer.offset_to_point(sel.start()).row;
853 let last = buffer.offset_to_point(last_spanned_offset(sel)).row;
854 match runs.last_mut() {
855 // Sorted and disjoint ⇒ `first` and `last` never decrease, so
856 // only the newest run can absorb this selection.
857 Some(run) if first <= run.last + reach => {
858 run.last = run.last.max(last);
859 run.sels.end = i + 1;
860 }
861 _ => runs.push(LineRun { first, last, sels: i..i + 1 }),
862 }
863 }
864 runs
865 }
866
867 /// The shared shape of [`move_line`](Self::move_line) and
868 /// [`copy_line`](Self::copy_line). Per line run
869 /// ([`line_runs`](Self::line_runs)`(merge_adjacent)`), `plan` sees the run,
870 /// its block's start offset and its text (rows `first..=last`, without the
871 /// last row's `\n`) and returns the run's [`Relocation`] — or `None` to leave
872 /// the run where it is. Destinations are pre-edit arithmetic, so `plan`
873 /// accounts for any shift earlier runs' edits cause.
874 ///
875 /// Every selection of a relocated run rides its block: the block moves or is
876 /// copied verbatim, so each offset keeps its distance from the block start
877 /// (clamped to the relocation's `max`, if any); a run left in place keeps
878 /// its spans. Ids and directions survive (`moved_to`). One discrete undo
879 /// step, the placement inside the commit so redo restores it; no run
880 /// relocated ⇒ no edit at all.
881 fn relocate_line_blocks(
882 &mut self,
883 merge_adjacent: bool,
884 mut plan: impl FnMut(&Buffer, &LineRun, u32, &str) -> Option<Relocation>,
885 ) {
886 let mut spans: Vec<(u32, u32)> =
887 self.selections.all().iter().map(|s| (s.tail(), s.head())).collect();
888 let mut ops = Vec::new();
889 for run in self.line_runs(merge_adjacent) {
890 let block_start = self.buffer.point_to_offset(Point::new(run.first, 0));
891 let block_end =
892 self.buffer.point_to_offset(Point::new(run.last, self.buffer.line_len(run.last)));
893 let block = self.buffer.slice(block_start..block_end);
894 let Some(Relocation { op, dest, max }) = plan(&self.buffer, &run, block_start, &block)
895 else {
896 continue;
897 };
898 let to = |o: u32| {
899 let landed = dest + (o - block_start);
900 max.map_or(landed, |max| landed.min(max))
901 };
902 for (tail, head) in &mut spans[run.sels] {
903 (*tail, *head) = (to(*tail), to(*head));
904 }
905 ops.push(op);
906 }
907 if ops.is_empty() {
908 return;
909 }
910 let placed = self.selections.moved_to(&spans);
911 verb_commit(self.edit_placing(ops, GroupingHint::discrete(), |_, _| Some(placed)));
912 // Drop provenance through the one owner so the `AutoClosePair`
913 // decoration is removed with it — the store is the single owner of the
914 // pair, with nothing else to keep in sync.
915 self.clear_autoclose();
916 }
917
918 /// Build one op per selection via `f(start, end, buffer)`.
919 fn map_selections(
920 &self,
921 mut f: impl FnMut(u32, u32, &Buffer) -> EditOp,
922 ) -> Vec<EditOp> {
923 self.selections
924 .all()
925 .iter()
926 .map(|sel| f(sel.start(), sel.end(), &self.buffer))
927 .collect()
928 }
929
930 /// Apply verb ops as one transaction and drop a caret after each edit.
931 /// The ops must be disjoint ([`verb_commit`] holds the verb to it).
932 fn run_edit(&mut self, ops: Vec<EditOp>, hint: GroupingHint) {
933 // A caret after each edit: `new.end` is after inserted text, or the
934 // deletion point for a pure delete.
935 verb_commit(self.edit_placing(ops, hint, |patch, _| {
936 let carets: Vec<u32> = patch.edits().iter().map(|e| e.new.end).collect();
937 Some(SelectionSet::from_offsets(&carets))
938 }));
939 }
940}
941
942/// The verbs' one verdict on a commit — no verb drops a [`TransactionError`]
943/// with a bare `let _ =`. Every verb builds DISJOINT ops by construction (one
944/// per selection, or overlapping deletes merged by [`merge_delete_ranges`]),
945/// so an [`Overlap`](TransactionError::Overlap) reaching here is a verb bug —
946/// a keystroke that would silently do nothing (two carets in one indent run
947/// backspacing to the same tab stop). Debug and test builds panic on it,
948/// which turns every verb test, and the seeded multi-caret walks, into an
949/// overlap oracle. [`WouldOverflow`](TransactionError::WouldOverflow) is the
950/// one legitimate refusal — a paste past the u32 offset space, rejected
951/// atomically with the document untouched — and the verbs' `()` surface has
952/// no channel to report it, so it alone passes as a no-op. Returns whether the
953/// transaction was applied.
954pub(crate) fn verb_commit(result: Result<Committed, TransactionError>) -> bool {
955 match result {
956 Ok(_) => true,
957 Err(e) => {
958 debug_assert!(
959 !matches!(e, TransactionError::Overlap { .. }),
960 "a verb built overlapping ops (merge them first): {e}"
961 );
962 false
963 }
964 }
965}
966
967/// A maximal block of whole lines the selections span, with the selections
968/// that span it ([`Document::line_runs`]).
969struct LineRun {
970 /// The run's first row.
971 first: u32,
972 /// The run's last row (inclusive).
973 last: u32,
974 /// The run's selections, as indices into [`SelectionSet::all`].
975 sels: Range<usize>,
976}
977
978/// Where one line run's block goes — [`Document::relocate_line_blocks`]'s
979/// per-run plan.
980struct Relocation {
981 /// The edit that moves or copies the block.
982 op: EditOp,
983 /// Where the block's first byte lands post-edit.
984 dest: u32,
985 /// The furthest a carried offset may land, or `None` when nothing clamps.
986 max: Option<u32>,
987}
988
989/// One caret per edit, `backs[i]` bytes before edit `i`'s inserted end — the
990/// Enter / insert-line placement (0 = just after the text, as `run_edit`; >0
991/// pulls the caret back onto a line the insertion opened). `backs` is
992/// parallel to the ops, which the engine keeps one edit each, in order.
993fn carets_pulled_back(patch: &Patch, backs: &[u32]) -> SelectionSet {
994 let carets: Vec<u32> = patch.edits().iter().zip(backs).map(|(e, back)| e.new.end - back).collect();
995 SelectionSet::from_offsets(&carets)
996}
997
998/// The last offset a selection actually spans: one back from the exclusive
999/// `end`, so a selection ending exactly at a line start does not span that
1000/// line. THE end-exclusivity rule, shared by the row enumeration
1001/// (`line_runs`, and through it `spanned_rows`) and the whole-line deletion
1002/// block (`line_block`).
1003fn last_spanned_offset(sel: &Selection) -> u32 {
1004 if sel.end() > sel.start() {
1005 sel.end() - 1
1006 } else {
1007 sel.end()
1008 }
1009}
1010
1011/// Merge overlapping delete ranges into a minimal non-overlapping set of delete
1012/// ops, because the transaction engine rejects overlapping ops. EVERY
1013/// multi-caret delete verb builds its ops here, since a per-selection delete
1014/// can reach past its neighbour: two carets in one word (word-delete), two
1015/// carets on one line (line-delete, cut), two carets in one indent run backing
1016/// to the same tab stop, or a caret's stop reaching through a selection
1017/// (backspace). Merging deletes the shared region once — the union of what each
1018/// selection would delete alone — and `run_edit`'s one caret per merged edit
1019/// collapses the carets onto its start. Touching ranges (`a.end == b.start`)
1020/// stay separate; the engine accepts those, and their carets coincide and merge.
1021fn merge_delete_ranges(ranges: impl Iterator<Item = Range<u32>>) -> Vec<EditOp> {
1022 let mut sorted: Vec<Range<u32>> = ranges.collect();
1023 sorted.sort_by_key(|r| r.start);
1024 let mut merged: Vec<Range<u32>> = Vec::new();
1025 for r in sorted {
1026 match merged.last_mut() {
1027 Some(last) if r.start < last.end => last.end = last.end.max(r.end),
1028 _ => merged.push(r),
1029 }
1030 }
1031 merged.into_iter().map(EditOp::delete).collect()
1032}
1033
1034/// The byte offset backspace should delete back to from an empty caret: one
1035/// indent level ([`outdent_range`] of the whitespace before the caret) if the
1036/// caret is within the line's leading whitespace, else one character.
1037fn backspace_target(buffer: &Buffer, caret: u32) -> u32 {
1038 let p = buffer.offset_to_point(caret);
1039 let line = buffer.line(p.row);
1040 if p.col > 0 && p.col <= tail_start_col(&line) {
1041 // The whitespace before the caret, read as an indent — the leading
1042 // whitespace truncated to the caret, exactly as Enter copies it.
1043 // Its level is removed from the END of its trailing run (the run is
1044 // one repeated char, so where within it is moot for the text), i.e.
1045 // just before the caret.
1046 let level = outdent_range(&line[..p.col as usize]).len() as u32;
1047 if level > 0 {
1048 return caret - level;
1049 }
1050 // Leading whitespace that is neither a tab nor a space (U+3000…)
1051 // has no level: an ordinary one-char delete.
1052 }
1053 let prev = buffer.char_before(caret).expect("caret > 0");
1054 caret - prev.len_utf8() as u32
1055}
1056
1057/// The kind of one indent level — THE indent-unit rule. Enter's
1058/// block-opening level, Tab, block indent, Shift+Tab, and backspace's
1059/// tab-stop delete all classify a line here, so they cannot disagree about
1060/// it: a tab-indented line grows and shrinks by hard tabs, anything else by
1061/// spaces between stops [`default_indent_size`] apart. (A global
1062/// tabs-vs-spaces preference is a host setting this does not know; an
1063/// unindented line therefore takes spaces.)
1064#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1065enum IndentUnit {
1066 /// One hard tab.
1067 Tab,
1068 /// Spaces to the neighbouring indent stop.
1069 Spaces,
1070}
1071
1072impl IndentUnit {
1073 /// The unit leading whitespace `ws` calls for: the kind of its LAST char —
1074 /// a hard tab ⇒ [`Tab`](Self::Tab), spaces or nothing ⇒
1075 /// [`Spaces`](Self::Spaces). The last char because it is the one a level
1076 /// abuts: `\t··` is tab-indented then space-ALIGNED, and a level added
1077 /// after alignment spaces must be spaces, or the line would interleave
1078 /// kinds (`\t··\t`).
1079 fn of(ws: &str) -> Self {
1080 if ws.ends_with('\t') {
1081 Self::Tab
1082 } else {
1083 Self::Spaces
1084 }
1085 }
1086
1087 /// The char a level of this unit is made of.
1088 fn char(self) -> char {
1089 match self {
1090 Self::Tab => '\t',
1091 Self::Spaces => ' ',
1092 }
1093 }
1094
1095 /// The text of one level inserted right after `prefix` (the line's text
1096 /// before the insertion point): a hard tab, or the spaces to the next
1097 /// stop.
1098 fn text(self, prefix: &str) -> String {
1099 match self {
1100 Self::Tab => "\t".to_owned(),
1101 Self::Spaces => {
1102 let n = default_indent_size();
1103 " ".repeat((n - stop_col(prefix) % n) as usize)
1104 }
1105 }
1106 }
1107}
1108
1109/// The column `prefix`'s end sits at for indent-stop arithmetic: its bytes
1110/// after the last hard tab (or from the line start). A hard tab ends on a tab
1111/// stop, so counting from it keeps the stops where they are drawn while the
1112/// display tab width ([`crate::display_map::default_tab_size`]) is a multiple
1113/// of the indent width — and keeps a level of spaces after a tab inverse to
1114/// its outdent. Byte-based like every column here.
1115fn stop_col(prefix: &str) -> u32 {
1116 (prefix.len() - prefix.rfind('\t').map_or(0, |i| i + 1)) as u32
1117}
1118
1119/// Where one indent level goes in leading whitespace `ws`: the START of its
1120/// trailing run of the [`IndentUnit::of`] char. The run is one repeated char,
1121/// so the text comes out the same anywhere in it; its start is, for a pure
1122/// indent, column 0 — where these verbs always edited, so selections rebase
1123/// as they did (one beginning at an indented line's text stays on the text
1124/// through the (out)dent) — while a mixed line's earlier whitespace (the `\t`
1125/// before alignment spaces) is left in front, untouched.
1126fn level_start(ws: &str, unit: IndentUnit) -> u32 {
1127 ws.trim_end_matches(unit.char()).len() as u32
1128}
1129
1130/// One indent level deeper for leading whitespace `ws`: insert `.1` at byte
1131/// `.0` of the line — a hard tab on a tab-indented line, else spaces padding
1132/// the trailing space run to its next stop.
1133fn indent_insert(ws: &str) -> (u32, String) {
1134 let unit = IndentUnit::of(ws);
1135 (level_start(ws, unit), unit.text(ws))
1136}
1137
1138/// One indent level shallower for leading whitespace `ws` (or the part of it
1139/// before a caret): the byte range to delete — its last tab, or its trailing
1140/// spaces back to the previous stop (at most [`default_indent_size`], never
1141/// past a tab) — empty when `ws` has no level to give.
1142fn outdent_range(ws: &str) -> Range<u32> {
1143 let unit = IndentUnit::of(ws);
1144 let start = level_start(ws, unit);
1145 let run = ws.len() as u32 - start;
1146 let width = match unit {
1147 IndentUnit::Tab => 1, // `of` ⇒ the run holds at least this one tab
1148 IndentUnit::Spaces if run == 0 => 0,
1149 // `run` counts from the last tab or the line start: `stop_col(ws)`.
1150 IndentUnit::Spaces => (run - 1) % default_indent_size() + 1,
1151 };
1152 start..start + width
1153}
1154
1155/// The leading-whitespace indent an Enter at `caret` should carry: the current
1156/// line's leading whitespace, truncated to the caret's column (so pressing
1157/// Enter inside the indentation does not duplicate it).
1158fn enter_indent(buffer: &Buffer, caret: u32) -> String {
1159 let p = buffer.offset_to_point(caret);
1160 let line = buffer.line(p.row);
1161 let ws = leading_ws(&line);
1162 ws[..ws.len().min(p.col as usize)].to_owned()
1163}
1164
1165/// A line's leading whitespace — the indent every indent verb reads (Tab,
1166/// block (out)dent, Enter, insert-line). The one slice of the one
1167/// [`tail_start_col`] rule, so they cannot disagree on where an indent ends.
1168fn leading_ws(line: &str) -> &str {
1169 &line[..tail_start_col(line) as usize]
1170}
1171
1172/// One indent unit matching the KIND of the `copied` leading whitespace
1173/// ([`IndentUnit::of`], the rule Tab and Shift+Tab share): a tab-indented
1174/// prefix grows by one hard tab, anything else (spaces or an empty prefix) by
1175/// [`default_indent_size`] spaces — so an indent-carrying Enter never mixes
1176/// tabs and spaces within a line. A WHOLE level — the unit's
1177/// [`text`](IndentUnit::text) from an indent stop (an empty prefix) — not
1178/// Tab's pad to the next stop from the caret: the two agree on every on-grid
1179/// indent.
1180fn indent_unit(copied: &str) -> String {
1181 IndentUnit::of(copied).text("")
1182}
1183
1184/// Whether the current line opens a block at `caret`: the text left of `caret`,
1185/// right-trimmed, ends with `{` — the caret is entering a freshly opened block,
1186/// so the new line indents one unit deeper.
1187fn line_opens_block(buffer: &Buffer, caret: u32) -> bool {
1188 let p = buffer.offset_to_point(caret);
1189 buffer.line(p.row)[..p.col as usize].trim_end().ends_with('{')
1190}
1191
1192#[cfg(test)]
1193mod tests {
1194 use super::*;
1195
1196 fn doc(s: &str) -> Document {
1197 Document::new(s).unwrap()
1198 }
1199
1200 /// Count of live auto-close provenance pairs, read from the dedicated
1201 /// auto-close store that holds them.
1202 fn ac_count(d: &Document) -> usize {
1203 d.autoclose_ranges().len()
1204 }
1205
1206 #[test]
1207 fn typing_inserts_and_advances_caret() {
1208 let mut d = doc("");
1209 d.type_char('h');
1210 d.type_char('i');
1211 assert_eq!(d.text(), "hi");
1212 assert_eq!(d.selections().all()[0].head(), 2);
1213 // One typing run → one undo.
1214 assert!(d.undo());
1215 assert_eq!(d.text(), "");
1216 }
1217
1218 #[test]
1219 fn typing_over_a_selection_replaces_it() {
1220 // Select then type must replace the selection, leaving one caret after
1221 // the inserted char — no residue of the old selection.
1222 let mut d = doc("hello world");
1223 d.selections = SelectionSet::new(0);
1224 d.selections.set_single(crate::Selection::from_anchor(crate::SelectionId(0), 0, 5));
1225 d.type_char('X');
1226 assert_eq!(d.text(), "X world");
1227 assert_eq!(d.selections().all()[0].head(), 1);
1228 assert!(d.selections().all()[0].is_empty());
1229 }
1230
1231 #[test]
1232 fn autoclose_inserts_pair_then_overtypes() {
1233 let mut d = doc("");
1234 d.type_char('(');
1235 assert_eq!(d.text(), "()");
1236 assert_eq!(d.selections().all()[0].head(), 1); // caret between
1237 d.type_char(')'); // overtype the auto-inserted close
1238 assert_eq!(d.text(), "()", "overtype skips — no second )");
1239 assert_eq!(d.selections().all()[0].head(), 2);
1240 }
1241
1242 #[test]
1243 fn autoclose_survives_typing_inside_the_pair() {
1244 let mut d = doc("");
1245 d.type_char('(');
1246 d.type_char('x'); // "(x)", caret 2
1247 assert_eq!(d.text(), "(x)");
1248 d.type_char(')'); // still overtypes the tracked close
1249 assert_eq!(d.text(), "(x)");
1250 assert_eq!(d.selections().all()[0].head(), 3);
1251 }
1252
1253 #[test]
1254 fn autoclose_guard_skips_pair_before_a_word() {
1255 let mut d = doc("abc"); // caret 0; the next char is a word char (guard fails)
1256 d.type_char('(');
1257 assert_eq!(d.text(), "(abc"); // just the opener
1258 assert_eq!(d.selections().all()[0].head(), 1);
1259 }
1260
1261 #[test]
1262 fn delete_word_back_and_forward_respect_word_and_newline_bounds() {
1263 let mut d = doc("foo bar\nbaz");
1264 d.set_selections(crate::SelectionSet::new(11)); // end of "baz"
1265 d.delete_word_back();
1266 assert_eq!(d.text(), "foo bar\n"); // the word "baz" is gone
1267 d.delete_word_back(); // caret at col 0 now → deletes only the newline
1268 assert_eq!(d.text(), "foo bar");
1269 d.set_selections(crate::SelectionSet::new(0));
1270 d.delete_word_forward();
1271 assert_eq!(d.text(), " bar"); // "foo" gone, the space stays
1272 }
1273
1274 #[test]
1275 fn delete_word_back_eats_only_a_multi_space_run() {
1276 let mut d = doc("foo bar"); // 3 spaces
1277 d.set_selections(crate::SelectionSet::new(6)); // caret before "bar"
1278 d.delete_word_back();
1279 assert_eq!(d.text(), "foobar", "2+ whitespace → deletes only the run");
1280 }
1281
1282 #[test]
1283 fn multi_caret_word_delete_merges_overlapping_ranges() {
1284 // Two carets in one word yield overlapping delete ranges; merged into a
1285 // single delete, they remove the shared span once instead of being
1286 // rejected as an overlapping transaction (which would delete nothing).
1287 let mut d = doc("hello");
1288 d.set_selections(crate::SelectionSet::from_offsets(&[3, 5]));
1289 d.delete_word_back();
1290 assert_eq!(d.text(), "", "merged into one 0..5 delete, not dropped");
1291 assert_eq!(d.selections().len(), 1);
1292
1293 let mut d = doc("hello");
1294 d.set_selections(crate::SelectionSet::from_offsets(&[0, 2]));
1295 d.delete_word_forward();
1296 assert_eq!(d.text(), "", "forward merge too");
1297 }
1298
1299 #[test]
1300 fn autoclose_pair_backspace_deletes_both() {
1301 let mut d = doc("");
1302 d.type_char('(');
1303 d.backspace();
1304 assert_eq!(d.text(), "");
1305 assert_eq!(d.selections().all()[0].head(), 0);
1306 }
1307
1308 #[test]
1309 fn autoclose_surrounds_a_selection() {
1310 let mut d = doc("foo");
1311 d.selections.set_single(crate::Selection::from_anchor(crate::SelectionId(0), 0, 3));
1312 d.type_char('(');
1313 assert_eq!(d.text(), "(foo)");
1314 let s = d.selections().all()[0];
1315 assert_eq!((s.start(), s.end()), (1, 4)); // still on "foo"
1316 }
1317
1318 #[test]
1319 fn autoclose_provenance_invalidates_on_caret_move() {
1320 let mut d = doc("");
1321 d.type_char('('); // "()", provenance live, caret 1
1322 d.move_carets(crate::Motion::Left, false); // caret 0 — leaves the pair
1323 d.type_char(')'); // no live provenance → literal insert, not overtype
1324 assert_eq!(d.text(), ")()");
1325 }
1326
1327 #[test]
1328 fn autoclose_quote_parity_closes_an_open_string() {
1329 // Prev char is a space (the before-guard is fine), but the line already
1330 // has one quote — an odd count means this quote closes the string:
1331 // insert literally, no pair.
1332 let mut d = doc("\" ");
1333 d.selections = SelectionSet::new(2); // caret after the space
1334 d.type_char('"');
1335 assert_eq!(d.text(), "\" \""); // one quote added, not a pair
1336 }
1337
1338 #[test]
1339 fn autoclose_pairs_at_every_caret() {
1340 // Multi-cursor '{' pairs at EVERY caret, not a lone '{'.
1341 let mut d = doc("a\nb\nc"); // the three line ends are offsets 1, 3, 5
1342 d.set_selections(crate::SelectionSet::from_offsets(&[1, 3, 5]));
1343 d.type_char('{');
1344 assert_eq!(d.text(), "a{}\nb{}\nc{}");
1345 assert_eq!(ac_count(&d), 3, "one live provenance pair per caret");
1346 // Each caret sits between its pair — typing lands inside every one.
1347 d.type_char('x');
1348 assert_eq!(d.text(), "a{x}\nb{x}\nc{x}");
1349 }
1350
1351 #[test]
1352 fn autoclose_overtypes_at_every_caret() {
1353 // Typing the closer at every pair's close skips over each (no second).
1354 let mut d = doc("a\nb\nc");
1355 d.set_selections(crate::SelectionSet::from_offsets(&[1, 3, 5]));
1356 d.type_char('{'); // "a{}\nb{}\nc{}", each caret between
1357 d.type_char('}'); // overtype every one
1358 assert_eq!(d.text(), "a{}\nb{}\nc{}", "overtype skips at every caret — no doubled closer");
1359 assert_eq!(ac_count(&d), 0, "provenance consumed");
1360 }
1361
1362 #[test]
1363 fn autoclose_surrounds_every_selection() {
1364 // '(' over a multi-selection surrounds each — "surround all occurrences".
1365 let mut d = doc("foo bar baz");
1366 d.set_selections(crate::SelectionSet::from_ranges(&[(0, 3), (4, 7), (8, 11)], 0));
1367 d.type_char('(');
1368 assert_eq!(d.text(), "(foo) (bar) (baz)");
1369 }
1370
1371 #[test]
1372 fn autoclose_pair_backspace_at_every_caret() {
1373 // Backspace strictly between empty pairs deletes both at every caret.
1374 let mut d = doc("a\nb\nc");
1375 d.set_selections(crate::SelectionSet::from_offsets(&[1, 3, 5]));
1376 d.type_char('{'); // "a{}\nb{}\nc{}"
1377 d.backspace();
1378 assert_eq!(d.text(), "a\nb\nc", "each empty pair deleted whole");
1379 assert_eq!(ac_count(&d), 0);
1380 }
1381
1382 #[test]
1383 fn autoclose_nested_opener_supersedes_provenance() {
1384 // Typing an opener INSIDE a live pair must supersede the prior provenance
1385 // (the outer pair grew to enclose the insertion), not accumulate it —
1386 // else overtype's ends-ascending search picks the wrong pair and the
1387 // closer inserts literally, yielding "(()))" instead of "(())".
1388 let mut d = doc("");
1389 d.type_char('('); // "()"
1390 d.type_char('('); // "(())", inner caret; prior provenance superseded
1391 assert_eq!(ac_count(&d), 1, "only the innermost pair is live");
1392 d.type_char(')'); // overtypes the inner close, not a literal insert
1393 assert_eq!(d.text(), "(())");
1394 // A different opener nested (brace then paren then close).
1395 let mut e = doc("");
1396 e.type_char('{');
1397 e.type_char('(');
1398 e.type_char(')');
1399 assert_eq!(e.text(), "{()}");
1400 }
1401
1402 #[test]
1403 fn autoclose_nested_pair_backspace_deletes_the_inner() {
1404 // Backspace between the innermost empty pair deletes both (not one).
1405 let mut d = doc("");
1406 d.type_char('(');
1407 d.type_char('('); // "(())", caret between the inner pair
1408 d.backspace();
1409 assert_eq!(d.text(), "()", "the inner empty pair is deleted whole");
1410 }
1411
1412 #[test]
1413 fn autoclose_surrounds_touching_selections() {
1414 // Two TOUCHING non-empty selections (e.g. find-all "ab" in "abab") must
1415 // each keep their own interior — the successor must land on "cd", not
1416 // swallow the opener and land on "(cd".
1417 let mut d = doc("abcd");
1418 d.set_selections(crate::SelectionSet::from_ranges(&[(0, 2), (2, 4)], 0));
1419 d.type_char('(');
1420 assert_eq!(d.text(), "(ab)(cd)");
1421 let sels = d.selections().all();
1422 assert_eq!((sels[0].start(), sels[0].end()), (1, 3), "interior 'ab'");
1423 assert_eq!((sels[1].start(), sels[1].end()), (5, 7), "interior 'cd', not '(cd'");
1424 }
1425
1426 #[test]
1427 fn autoclose_per_caret_pairs_only_where_the_guard_passes() {
1428 // A per-caret decision: a caret before a word char inserts a BARE opener
1429 // (the guard fails), while a caret at EOF pairs.
1430 let mut d = doc("ab");
1431 d.set_selections(crate::SelectionSet::from_offsets(&[0, 2]));
1432 d.type_char('{');
1433 assert_eq!(d.text(), "{ab{}"); // bare '{' before 'a', a pair at EOF
1434 assert_eq!(ac_count(&d), 1, "only the guarded caret paired");
1435 }
1436
1437 #[test]
1438 fn backspace_deletes_char_then_selection() {
1439 let mut d = doc("abc");
1440 d.selections = SelectionSet::new(3);
1441 d.backspace();
1442 assert_eq!(d.text(), "ab");
1443 d.selections = SelectionSet::new(0);
1444 d.selections.set_single(crate::Selection::from_anchor(crate::SelectionId(0), 0, 2));
1445 d.backspace();
1446 assert_eq!(d.text(), "");
1447 }
1448
1449 #[test]
1450 fn backspace_in_indentation_deletes_to_tab_stop() {
1451 let mut d = doc(" x"); // 8 spaces
1452 d.selections = SelectionSet::new(8); // caret after the spaces
1453 d.backspace();
1454 assert_eq!(d.text(), " x"); // removed 4 (one stop), not 1
1455 }
1456
1457 #[test]
1458 fn two_carets_in_one_indent_run_backspace_to_their_shared_tab_stop() {
1459 // The repro: carets at cols 3 and 4 both target stop 0, so their
1460 // tab-stop deletes are 0..3 and 0..4 — overlapping. Unmerged, the
1461 // transaction rejected them and the keystroke silently did nothing.
1462 let mut d = doc(" x"); // 8 spaces
1463 d.set_selections(SelectionSet::from_offsets(&[3, 4]));
1464 d.backspace();
1465 assert_eq!(d.text(), " x", "one 0..4 delete — the union of both carets' stops");
1466 let heads: Vec<u32> = d.selections().all().iter().map(Selection::head).collect();
1467 assert_eq!(heads, [0], "both carets collapse onto the shared stop");
1468 assert!(d.undo());
1469 assert_eq!(d.text(), " x", "one undo step restores the text");
1470 assert!(!d.can_undo(), "…and it was the only step");
1471 }
1472
1473 #[test]
1474 fn tab_stop_backspace_overlapping_a_selection_merges_into_it() {
1475 // The mixed form of the same overlap: a caret at col 3 deletes back to
1476 // stop 0 straight through a selection on cols 1..2 (never touching the
1477 // caret, so the selection set kept them apart).
1478 let mut d = doc(" x");
1479 let mut set = SelectionSet::new(3);
1480 set.add_selection(1, 2);
1481 d.set_selections(set);
1482 d.backspace();
1483 assert_eq!(d.text(), " x", "0..3 ∪ 1..2 deleted once");
1484 let heads: Vec<u32> = d.selections().all().iter().map(Selection::head).collect();
1485 assert_eq!(heads, [0]);
1486 assert!(d.undo());
1487 assert_eq!(d.text(), " x");
1488 assert!(!d.can_undo());
1489 }
1490
1491 /// The heads of every selection, ascending.
1492 fn heads(d: &Document) -> Vec<u32> {
1493 d.selections().all().iter().map(Selection::head).collect()
1494 }
1495
1496 /// Undo once and require it restored `text` and emptied the history.
1497 fn assert_one_undo_restores(d: &mut Document, text: &str) {
1498 assert!(d.undo());
1499 assert_eq!(d.text(), text, "one undo restores the text");
1500 assert!(!d.can_undo(), "…and the delete was the only step");
1501 }
1502
1503 #[test]
1504 fn adjacent_carets_char_delete_each_char_once() {
1505 // The char-delete audit: one char per caret can only TOUCH the next
1506 // range (distinct carets; a caret touching a selection merges into it),
1507 // so both carets' chars go and the carets meet.
1508 let mut d = doc("abcd");
1509 d.set_selections(SelectionSet::from_offsets(&[1, 2]));
1510 d.backspace();
1511 assert_eq!((d.text().as_ref(), heads(&d)), ("cd", vec![0]));
1512 assert_one_undo_restores(&mut d, "abcd");
1513 let mut d = doc("abcd");
1514 d.set_selections(SelectionSet::from_offsets(&[1, 2]));
1515 d.delete_forward();
1516 assert_eq!((d.text().as_ref(), heads(&d)), ("ad", vec![1]));
1517 assert_one_undo_restores(&mut d, "abcd");
1518 }
1519
1520 #[test]
1521 fn word_delete_reaching_through_a_selection_merges_into_it() {
1522 // The word-delete audit, mixed form (two carets in one word is
1523 // `multi_caret_word_delete_merges_overlapping_ranges`): the caret's word
1524 // span swallows a selection inside the same word.
1525 let mut d = doc("foo bar");
1526 let mut set = SelectionSet::new(3); // after "foo"
1527 set.add_selection(1, 2);
1528 d.set_selections(set);
1529 d.delete_word_back();
1530 assert_eq!((d.text().as_ref(), heads(&d)), (" bar", vec![0]), "0..3 ∪ 1..2");
1531 assert_one_undo_restores(&mut d, "foo bar");
1532 let mut d = doc("foo bar");
1533 let mut set = SelectionSet::new(0); // before "foo"
1534 set.add_selection(1, 2);
1535 d.set_selections(set);
1536 d.delete_word_forward();
1537 assert_eq!((d.text().as_ref(), heads(&d)), (" bar", vec![0]), "0..3 ∪ 1..2");
1538 assert_one_undo_restores(&mut d, "foo bar");
1539 }
1540
1541 #[test]
1542 fn overlapping_line_blocks_delete_once() {
1543 // The line-delete audit: a two-row selection's block contains the block
1544 // of a caret on its second row.
1545 let mut d = doc("a\nbb\nc");
1546 let mut set = SelectionSet::new(4); // end of "bb", row 1
1547 set.add_selection(0, 3); // rows 0..=1 → block 0..5
1548 d.set_selections(set);
1549 d.delete_line();
1550 assert_eq!((d.text().as_ref(), heads(&d)), ("c", vec![0]), "0..5 ∪ 2..5");
1551 assert_one_undo_restores(&mut d, "a\nbb\nc");
1552 // Cut's whole-line mode: two carets on one line yield the SAME block.
1553 let mut d = doc("aaaa\nbb");
1554 d.set_selections(SelectionSet::from_offsets(&[1, 3]));
1555 d.cut();
1556 assert_eq!((d.text().as_ref(), heads(&d)), ("bb", vec![0]));
1557 assert_one_undo_restores(&mut d, "aaaa\nbb");
1558 }
1559
1560 #[test]
1561 fn multi_caret_deletes_remove_the_union_of_each_selections_lone_delete() {
1562 // The overlap oracle for EVERY multi-caret delete verb. Spec: a
1563 // multi-selection delete removes exactly the union of what each
1564 // selection would delete alone — a shared span once, never a rejected
1565 // transaction — leaves a caret where each of those deletes began, and
1566 // one undo restores the text. Each lone delete is observed through the
1567 // public API only (a pure delete leaves its caret at the range start,
1568 // and the length drop is the width); the union is a per-byte mask, not
1569 // `merge_delete_ranges`' sort-and-sweep. Indent-heavy lines with up to
1570 // four selections make overlapping tab-stop, word and line ranges common.
1571 let mut rng = 0x0010_7AB5_70B5_u64;
1572 let mut next = move |m: u64| {
1573 rng ^= rng << 13;
1574 rng ^= rng >> 7;
1575 rng ^= rng << 17;
1576 rng % m
1577 };
1578 type Verb = fn(&mut Document);
1579 let verbs: [(&str, Verb); 6] = [
1580 ("backspace", Document::backspace),
1581 ("delete_forward", Document::delete_forward),
1582 ("delete_word_back", Document::delete_word_back),
1583 ("delete_word_forward", Document::delete_word_forward),
1584 ("delete_line", Document::delete_line),
1585 ("cut", Document::cut),
1586 ];
1587 const TAILS: &[&str] = &["", "x", "foo bar", "a.b", "(y)", " z"];
1588 // Per verb: cases run, and cases whose lone deletes overlapped.
1589 let mut runs = [0usize; 6];
1590 let mut overlaps = [0usize; 6];
1591 for case in 0..3_000 {
1592 let text = (0..1 + next(3))
1593 .map(|_| {
1594 // Mostly spaces, some tabs: pure, tab-then-aligned and
1595 // interleaved indents all reach the tab-stop backspace.
1596 let indent: String = (0..next(10))
1597 .map(|_| if next(8) == 0 { '\t' } else { ' ' })
1598 .collect();
1599 format!("{indent}{}", TAILS[next(TAILS.len() as u64) as usize])
1600 })
1601 .collect::<Vec<_>>()
1602 .join("\n");
1603 let len = text.len() as u64; // ASCII: every offset is a char boundary
1604 let ranges: Vec<(u32, u32)> = (0..1 + next(4))
1605 .map(|_| {
1606 let a = next(len + 1) as u32;
1607 (a, if next(4) == 0 { next(len + 1) as u32 } else { a })
1608 })
1609 .collect();
1610 let set = SelectionSet::from_ranges(&ranges, 0);
1611 let v = next(verbs.len() as u64) as usize;
1612 let (name, verb) = verbs[v];
1613 // Cut's own rule: in a mixed set an empty caret copied nothing, so
1614 // it deletes nothing (alone it would cut its whole line).
1615 let mixed = set.all().iter().any(|s| !s.is_empty());
1616 let mut mask = vec![false; text.len()];
1617 let (mut starts, mut lone_total) = (Vec::new(), 0);
1618 for sel in set.all() {
1619 if name == "cut" && mixed && sel.is_empty() {
1620 continue;
1621 }
1622 let mut lone = doc(&text);
1623 lone.set_selections(SelectionSet::from_ranges(&[(sel.tail(), sel.head())], 0));
1624 verb(&mut lone);
1625 let width = text.len() - lone.text().len();
1626 if width > 0 {
1627 let start = lone.selections().newest().head() as usize;
1628 mask[start..start + width].iter_mut().for_each(|m| *m = true);
1629 starts.push(start);
1630 lone_total += width;
1631 }
1632 }
1633 let union = mask.iter().filter(|&&m| m).count();
1634 runs[v] += 1;
1635 overlaps[v] += usize::from(union < lone_total);
1636 let expected: String =
1637 text.bytes().zip(&mask).filter(|&(_, &m)| !m).map(|(b, _)| char::from(b)).collect();
1638
1639 let mut d = doc(&text);
1640 d.set_selections(set);
1641 verb(&mut d);
1642 let what = format!("case {case}: {name} on {text:?} at {ranges:?}");
1643 assert_eq!(d.text(), expected, "{what}: the union of the lone deletes");
1644 for s in starts {
1645 let at = (s - mask[..s].iter().filter(|&&m| m).count()) as u32;
1646 assert!(
1647 d.selections().all().iter().any(|c| c.is_empty() && c.head() == at),
1648 "{what}: a caret where the delete from {s} began (post-edit {at})",
1649 );
1650 }
1651 if union > 0 {
1652 assert!(d.undo(), "{what}: undoable");
1653 assert_eq!(d.text(), text, "{what}: one undo restores the text");
1654 }
1655 assert!(!d.can_undo(), "{what}: the whole delete was ONE undo step");
1656 }
1657 // A one-char forward delete can only TOUCH its neighbour (a caret
1658 // touching a selection merges into it); every other verb must have
1659 // really been driven through overlapping ranges, or the oracle is vacuous.
1660 // (This seed, in `verbs` order: ~500 runs each; overlaps 33 / 0 / 111 /
1661 // 104 / 290 / 155.)
1662 for (v, (name, _)) in verbs.iter().enumerate() {
1663 if *name == "delete_forward" {
1664 assert_eq!(overlaps[v], 0, "delete_forward cannot overlap");
1665 } else {
1666 assert!(
1667 overlaps[v] > 20,
1668 "walk too thin for {name}: runs {runs:?}, overlaps {overlaps:?}",
1669 );
1670 }
1671 }
1672 }
1673
1674 #[test]
1675 fn enter_carries_indent_truncated_to_caret() {
1676 let mut d = doc(" hello");
1677 d.selections = SelectionSet::new(9); // end of line
1678 d.enter();
1679 assert_eq!(d.text(), " hello\n "); // 4-space indent carried
1680 // Enter at column 2 (inside the indent) carries only 2 spaces of
1681 // autoindent (truncated to the caret); the 2 spaces after the caret
1682 // stay with "hello" on the new line → line 1 has 4 spaces total, not 8
1683 // — the indent is truncated to the caret, never duplicated.
1684 let mut d2 = doc(" hello");
1685 d2.selections = SelectionSet::new(2);
1686 d2.enter();
1687 assert_eq!(d2.text(), " \n hello");
1688 }
1689
1690 #[test]
1691 fn delete_line_removes_blocks_and_the_final_line_cleanly() {
1692 let mut d = doc("one\ntwo\nthree");
1693 d.set_selections(crate::SelectionSet::new(5)); // caret in "two"
1694 d.delete_line();
1695 assert_eq!(d.text(), "one\nthree");
1696 // The final line takes its PRECEDING newline — no empty tail remains.
1697 d.set_selections(crate::SelectionSet::new(d.buffer().len()));
1698 d.delete_line();
1699 assert_eq!(d.text(), "one");
1700 // A multi-line selection deletes every spanned line (one ending at a
1701 // line start does not span that line)…
1702 let mut m = doc("a\nb\nc\nd");
1703 let mut set = crate::SelectionSet::new(0);
1704 set.set_single(crate::Selection::from_anchor(crate::SelectionId(0), 0, 4)); // rows 0..=1
1705 m.set_selections(set);
1706 m.delete_line();
1707 assert_eq!(m.text(), "c\nd");
1708 // …and two carets on one line delete it once.
1709 let mut t = doc("xx\nyy");
1710 t.set_selections(crate::SelectionSet::from_offsets(&[0, 1]));
1711 t.delete_line();
1712 assert_eq!(t.text(), "yy");
1713 }
1714
1715 #[test]
1716 fn insert_line_below_and_above_carry_indent() {
1717 // Below: a fresh line under the caret's line, same indent, caret on it
1718 // — the caret's line is NOT split, wherever the caret sat.
1719 let mut d = doc(" a\n b");
1720 d.set_selections(crate::SelectionSet::new(4)); // before "a"
1721 d.insert_line(true);
1722 assert_eq!(d.text(), " a\n \n b");
1723 assert_eq!(d.selections().newest().head(), 10, "caret at the new line's end");
1724 // Above: the current line is pushed down; caret on the new line.
1725 let mut u = doc(" a");
1726 u.set_selections(crate::SelectionSet::new(5));
1727 u.insert_line(false);
1728 assert_eq!(u.text(), " \n a");
1729 assert_eq!(u.selections().newest().head(), 4);
1730 // A line below a block-opening `{` line gains one indent unit.
1731 let mut b = doc("m {");
1732 b.set_selections(crate::SelectionSet::new(0));
1733 b.insert_line(true);
1734 assert_eq!(b.text(), "m {\n ");
1735 assert_eq!(b.selections().newest().head(), 8);
1736 }
1737
1738 #[test]
1739 fn toggle_line_comment_comments_aligned_then_uncomments() {
1740 // Ctrl+/: markers align at the block's MIN indent; blank lines are
1741 // skipped; a mixed block comments everything; toggling back strips
1742 // prefix + one space; the selection survives (rebase, not re-set).
1743 let mut d = doc(" a\n\n b");
1744 d.set_line_comment(Some("//"));
1745 d.select_all();
1746 d.toggle_line_comment();
1747 assert_eq!(d.text(), " // a\n\n // b");
1748 assert!(!d.selections().newest().is_empty(), "selection survives the toggle");
1749 d.toggle_line_comment();
1750 assert_eq!(d.text(), " a\n\n b");
1751 // A bare caret toggles only its own line…
1752 let mut c = doc("x\ny");
1753 c.set_line_comment(Some("//"));
1754 c.set_selections(crate::SelectionSet::new(0));
1755 c.toggle_line_comment();
1756 assert_eq!(c.text(), "// x\ny");
1757 // …stripping works without the space too.
1758 let mut s = doc("//x");
1759 s.set_line_comment(Some("//"));
1760 s.toggle_line_comment();
1761 assert_eq!(s.text(), "x");
1762 // Mixed commented/uncommented → comment ALL, then one toggle back
1763 // returns to all-commented.
1764 let mut m = doc("// a\nb");
1765 m.set_line_comment(Some("//"));
1766 m.select_all();
1767 m.toggle_line_comment();
1768 assert_eq!(m.text(), "// // a\n// b");
1769 }
1770
1771 #[test]
1772 fn toggle_line_comment_without_a_prefix_is_a_no_op() {
1773 // The core knows no language: nothing injected → nothing happens.
1774 let mut d = doc("abc");
1775 d.toggle_line_comment();
1776 assert_eq!(d.text(), "abc");
1777 // An all-blank span starts a comment ready to type into.
1778 let mut b = doc("");
1779 b.set_line_comment(Some("//"));
1780 b.toggle_line_comment();
1781 assert_eq!(b.text(), "// ");
1782 assert_eq!(b.selections().newest().head(), 3, "caret rebased after the prefix");
1783 }
1784
1785 #[test]
1786 fn copy_and_cut_expand_an_empty_caret_to_its_line() {
1787 // An empty-selection Copy takes the ENTIRE line including \n…
1788 let mut d = doc("one\ntwo\nthree");
1789 d.set_selections(crate::SelectionSet::new(5)); // caret inside "two"
1790 assert_eq!(d.clipboard_payload(), ("two\n".to_string(), true));
1791 // …the final line (no terminator in the buffer) still exports one…
1792 d.set_selections(crate::SelectionSet::new(9)); // inside "three"
1793 assert_eq!(d.clipboard_payload(), ("three\n".to_string(), true));
1794 // …a non-empty selection copies just itself, not the line…
1795 let mut set = crate::SelectionSet::new(0);
1796 set.set_single(crate::Selection::from_anchor(crate::SelectionId(0), 0, 3));
1797 d.set_selections(set);
1798 assert_eq!(d.clipboard_payload(), ("one".to_string(), false));
1799 // …and Cut on a bare caret deletes the whole line (one undo step).
1800 d.set_selections(crate::SelectionSet::new(5));
1801 d.cut();
1802 assert_eq!(d.text(), "one\nthree");
1803 assert!(d.undo());
1804 assert_eq!(d.text(), "one\ntwo\nthree");
1805 }
1806
1807 #[test]
1808 fn mixed_set_cut_never_deletes_uncopied_lines() {
1809 // Cut IS copy+delete. In a mixed set the empty caret exports nothing, so
1810 // it must delete nothing — only the selection goes.
1811 let mut d = doc("foo bar\nbaz\n");
1812 let mut set = crate::SelectionSet::new(0);
1813 set.set_single(crate::Selection::from_anchor(crate::SelectionId(0), 0, 3)); // "foo"
1814 set.add_caret(9); // bare caret inside "baz"
1815 d.set_selections(set);
1816 assert_eq!(d.clipboard_payload(), ("foo".to_string(), false));
1817 d.cut();
1818 assert_eq!(d.text(), " bar\nbaz\n", "the uncopied `baz` line survives");
1819 }
1820
1821 #[test]
1822 fn cut_on_the_final_line_takes_the_preceding_newline() {
1823 // Cut on the final line takes its PRECEDING newline (matching its
1824 // sibling delete_line), so no empty tail line is left behind.
1825 let mut d = doc("one\ntwo");
1826 d.set_selections(crate::SelectionSet::new(5)); // inside "two"
1827 assert_eq!(d.clipboard_payload(), ("two\n".to_string(), true));
1828 d.cut();
1829 assert_eq!(d.text(), "one", "no empty tail line remains");
1830 // Degenerate: a caret on the trailing empty line cuts that line (the
1831 // preceding newline), not nothing.
1832 let mut e = doc("abc\n");
1833 e.set_selections(crate::SelectionSet::new(4));
1834 e.cut();
1835 assert_eq!(e.text(), "abc");
1836 }
1837
1838 #[test]
1839 fn two_carets_on_one_line_cut_it_once() {
1840 let mut d = doc("aaaa\nbb");
1841 d.set_selections(crate::SelectionSet::from_offsets(&[1, 3])); // both on row 0
1842 assert_eq!(d.clipboard_payload(), ("aaaa\n".to_string(), true), "the line exports once");
1843 d.cut();
1844 assert_eq!(d.text(), "bb", "identical ranges merged into one delete");
1845 }
1846
1847 #[test]
1848 fn entire_line_paste_inserts_above_and_keeps_the_caret() {
1849 // An entire-line paste at an empty caret splices ABOVE the caret's
1850 // line; the caret rebases past the insert and stays on its own line.
1851 let mut d = doc("aaa\nbbb");
1852 d.set_selections(crate::SelectionSet::new(5)); // "bbb", col 1
1853 d.paste("two\n", true);
1854 assert_eq!(d.text(), "aaa\ntwo\nbbb");
1855 assert_eq!(d.selections().newest().head(), 9, "still col 1 of `bbb`");
1856 // Over a non-empty selection the flag is moot — plain replace.
1857 let mut set = crate::SelectionSet::new(0);
1858 set.set_single(crate::Selection::from_anchor(crate::SelectionId(0), 0, 3));
1859 d.set_selections(set);
1860 d.paste("X\n", true);
1861 assert_eq!(d.text(), "X\n\ntwo\nbbb");
1862 }
1863
1864 #[test]
1865 fn enter_after_open_brace_indents_one_unit_deeper() {
1866 // Enter after a line-opening `{` gains one indent unit.
1867 let mut d = doc(" if {");
1868 d.selections = SelectionSet::new(8);
1869 d.enter();
1870 assert_eq!(d.text(), " if {\n ");
1871 // Trailing whitespace between `{` and the caret still triggers (the
1872 // rule reads the TRIMMED text left of the caret).
1873 let mut t = doc("m { ");
1874 t.selections = SelectionSet::new(5);
1875 t.enter();
1876 assert_eq!(t.text(), "m { \n ");
1877 // …but a caret not after an opener stays keep-style.
1878 let mut p = doc("m { x");
1879 p.selections = SelectionSet::new(5);
1880 p.enter();
1881 assert_eq!(p.text(), "m { x\n");
1882 }
1883
1884 #[test]
1885 fn enter_between_braces_splits_onto_three_lines() {
1886 // Type `{` (auto-close pairs it), Enter → opener line, indented middle
1887 // line holding the caret, closer dedented on its own.
1888 let mut d = doc("");
1889 d.type_char('{');
1890 assert_eq!(d.text(), "{}");
1891 d.enter();
1892 assert_eq!(d.text(), "{\n \n}");
1893 assert_eq!(d.selections().newest().head(), 6, "caret at the middle line's end");
1894 // In an indented context the closer keeps the ORIGINAL indent.
1895 let mut n = doc(" {}");
1896 n.selections = SelectionSet::new(5);
1897 n.enter();
1898 assert_eq!(n.text(), " {\n \n }");
1899 assert_eq!(n.selections().newest().head(), 14);
1900 // One undo reverts the whole split (a single transaction).
1901 assert!(n.undo());
1902 assert_eq!(n.text(), " {}");
1903 }
1904
1905 #[test]
1906 fn enter_indent_unit_matches_tab_kind() {
1907 // A hard-tab-indented line grows by a TAB, not spaces — the indent unit
1908 // matches the kind of the copied leading whitespace.
1909 let mut d = doc("\tif {");
1910 d.selections = SelectionSet::new(5);
1911 d.enter();
1912 assert_eq!(d.text(), "\tif {\n\t\t");
1913 }
1914
1915 #[test]
1916 fn tab_inserts_to_next_stop() {
1917 let mut d = doc("ab");
1918 d.selections = SelectionSet::new(2); // col 2 → next stop at 4, insert 2
1919 d.tab();
1920 assert_eq!(d.text(), "ab ");
1921 d.tab(); // col 4 → next stop 8, insert 4
1922 assert_eq!(d.text(), "ab ");
1923 }
1924
1925 #[test]
1926 fn tab_over_a_single_line_selection_types_over() {
1927 let mut d = doc("abcdef");
1928 d.selections.set_single(crate::Selection::from_anchor(crate::SelectionId(0), 1, 4)); // "bcd"
1929 d.tab();
1930 // Single-line selection → replace with spaces to the next stop (from
1931 // col 1 that's 3), NOT a whole-line indent.
1932 assert_eq!(d.text(), "a ef");
1933 assert!(d.selections().newest().is_empty(), "types over → collapses to a caret");
1934 }
1935
1936 #[test]
1937 fn shift_tab_outdents_a_single_line_and_keeps_the_selection() {
1938 let mut d = doc(" indented");
1939 d.selections.set_single(crate::Selection::from_anchor(crate::SelectionId(0), 5, 9)); // "nden"
1940 d.outdent();
1941 assert_eq!(d.text(), "indented"); // the line outdents (unlike Tab)
1942 // the selection rides the removed indent, still on "nden"
1943 assert_eq!((d.selections().newest().start(), d.selections().newest().end()), (1, 5));
1944 }
1945
1946 #[test]
1947 fn block_indent_outdent_preserves_the_selection() {
1948 let mut d = doc("a\nb\nc");
1949 // Select from line 0 into line 2.
1950 d.selections.set_single(crate::Selection::from_anchor(crate::SelectionId(0), 0, 5));
1951 d.tab();
1952 assert_eq!(d.text(), " a\n b\n c");
1953 // The selection is preserved — one selection over the indented block,
1954 // NOT one caret per line.
1955 assert_eq!(d.selections().len(), 1, "no extra cursors");
1956 let s = d.selections().newest();
1957 assert_eq!((s.start(), s.end()), (0, 17));
1958 assert!(!s.is_empty(), "still a selection, not a caret");
1959 // Outdent restores the text and keeps the selection.
1960 d.outdent();
1961 assert_eq!(d.text(), "a\nb\nc");
1962 assert_eq!(d.selections().len(), 1);
1963 assert_eq!(
1964 (d.selections().newest().start(), d.selections().newest().end()),
1965 (0, 5)
1966 );
1967 }
1968
1969 /// Select `anchor..head` as the one selection.
1970 fn select(d: &mut Document, anchor: u32, head: u32) {
1971 d.set_selections(SelectionSet::from_ranges(&[(anchor, head)], 0));
1972 }
1973
1974 /// Run `verb`, require `after`, and require ONE undo step restoring the
1975 /// text `verb` started from.
1976 #[track_caller]
1977 fn assert_one_step(d: &mut Document, verb: fn(&mut Document), after: &str) {
1978 let before = d.text().into_owned();
1979 verb(d);
1980 assert_eq!(d.text(), after, "{before:?}");
1981 assert_one_undo_restores(d, &before);
1982 }
1983
1984 #[test]
1985 fn outdent_removes_a_hard_tab() {
1986 // The repro: Shift+Tab worked in spaces only, so a tab-indented
1987 // line could not be outdented at all.
1988 let mut d = doc("\tfoo");
1989 assert_one_step(&mut d, Document::outdent, "foo");
1990 let mut d = doc("\t\tfoo"); // one level per press, not the whole indent
1991 d.set_selections(SelectionSet::new(4)); // caret inside the text
1992 assert_one_step(&mut d, Document::outdent, "\tfoo");
1993 }
1994
1995 #[test]
1996 fn block_indent_of_a_tab_indented_block_adds_tabs() {
1997 // The repro: Tab over a tab-indented block inserted SPACES ahead of
1998 // each tab (`" \tfoo"`), mixing kinds within the line.
1999 let mut d = doc("\tfoo\n\tbar");
2000 d.select_all();
2001 assert_one_step(&mut d, Document::tab, "\t\tfoo\n\t\tbar");
2002 // A block selection begun at the text (after Home) stays on the text:
2003 // the new level lands ahead of the old one, never inside the selection.
2004 select(&mut d, 1, 9);
2005 d.tab();
2006 let s = d.selections().newest();
2007 assert_eq!((s.start(), s.end()), (2, 11), "the block selection survives");
2008 let mut d = doc("\t\tfoo\n\t\tbar");
2009 d.select_all();
2010 assert_one_step(&mut d, Document::outdent, "\tfoo\n\tbar");
2011 }
2012
2013 #[test]
2014 fn caret_tab_on_a_tab_indented_line_inserts_a_tab() {
2015 // The caret form of the same mix: Tab at the line start (or anywhere on
2016 // a tab-indented line) inserts the line's own unit.
2017 let mut d = doc("\tfoo");
2018 d.set_selections(SelectionSet::new(0));
2019 assert_one_step(&mut d, Document::tab, "\t\tfoo");
2020 d.set_selections(SelectionSet::new(4)); // end of the line
2021 assert_one_step(&mut d, Document::tab, "\tfoo\t");
2022 // A space-indented (or unindented) line still pads to the next stop.
2023 let mut d = doc(" foo");
2024 d.set_selections(SelectionSet::new(1));
2025 assert_one_step(&mut d, Document::tab, " foo");
2026 }
2027
2028 #[test]
2029 fn space_indents_snap_to_the_neighbouring_stops() {
2030 // Indent pads to the NEXT stop and outdent cuts back to the PREVIOUS
2031 // one — an off-grid indent is realigned, not shifted by a fixed 4.
2032 let mut d = doc(" foo\n bar");
2033 select(&mut d, 0, 15);
2034 assert_one_step(&mut d, Document::tab, " foo\n bar");
2035 select(&mut d, 0, 15);
2036 assert_one_step(&mut d, Document::outdent, "foo\n bar");
2037 let mut d = doc(" foo"); // 6 spaces
2038 assert_one_step(&mut d, Document::outdent, " foo");
2039 }
2040
2041 #[test]
2042 fn mixed_leading_whitespace_moves_one_unit_of_its_last_kind() {
2043 // The indent unit is the kind of the leading whitespace's LAST char
2044 // (Enter's rule): tab-then-alignment-spaces is a SPACES line whose
2045 // stops count from the tab; spaces-then-tab is a TAB line.
2046 let mut d = doc("\t foo");
2047 assert_one_step(&mut d, Document::outdent, "\tfoo");
2048 let mut d = doc("\t foo"); // tab + 6: back to the stop at tab + 4
2049 assert_one_step(&mut d, Document::outdent, "\t foo");
2050 let mut d = doc("\t foo\nx");
2051 select(&mut d, 0, 8);
2052 assert_one_step(&mut d, Document::tab, "\t foo\n x");
2053 let mut d = doc(" \tfoo");
2054 assert_one_step(&mut d, Document::outdent, " foo");
2055 let mut d = doc(" \tfoo\nx");
2056 select(&mut d, 0, 8);
2057 assert_one_step(&mut d, Document::tab, " \t\tfoo\n x");
2058 // A whitespace-only line is all leading whitespace.
2059 let mut d = doc("\t\t");
2060 assert_one_step(&mut d, Document::outdent, "\t");
2061 }
2062
2063 #[test]
2064 fn caret_tab_before_a_mixed_indents_last_run_indents_like_block_indent() {
2065 // Fails-first: a caret Tab dropped the line's unit AT the caret, so at
2066 // column 0 of `··\tfoo` (a tab line: its indent ends in a tab) it gave
2067 // `\t··\tfoo` — interleaving kinds, the very thing the unit rule exists
2068 // to prevent — where block indent gives `··\t\tfoo`. A caret ahead of
2069 // the indent's trailing run now inserts where block indent does, and
2070 // lands just past the new level.
2071 for (line, col, want, caret) in [
2072 (" \tfoo", 0, " \t\tfoo", 3),
2073 (" \tfoo", 1, " \t\tfoo", 3),
2074 ("\t foo", 0, "\t foo", 3), // a spaces line (tab-then-alignment)
2075 ] {
2076 let mut d = doc(line);
2077 d.set_selections(SelectionSet::new(col));
2078 d.tab();
2079 assert_eq!(d.text(), want, "{line:?} @ {col}");
2080 assert_eq!(heads(&d), [caret], "{line:?} @ {col}: the caret lands past the new level");
2081 assert_one_undo_restores(&mut d, line);
2082 // Block indent of the same line agrees.
2083 let mut b = doc(&format!("{line}\nx"));
2084 select(&mut b, 0, line.len() as u32 + 2);
2085 b.tab();
2086 assert_eq!(b.text().split('\n').next(), Some(want), "{line:?}: block indent");
2087 }
2088 // In (or at an edge of) the trailing run, the level still goes at the
2089 // caret — inserting a run's own char into it mixes nothing — and spaces
2090 // still pad from the caret to its next stop (VS Code's caret Tab).
2091 for (line, col, want) in [
2092 (" \tfoo", 2, " \t\tfoo"),
2093 ("\t foo", 3, "\t foo"),
2094 (" foo", 2, " foo"),
2095 ("\t\tfoo", 1, "\t\t\tfoo"),
2096 ] {
2097 let mut d = doc(line);
2098 d.set_selections(SelectionSet::new(col));
2099 assert_one_step(&mut d, Document::tab, want);
2100 }
2101 }
2102
2103 #[test]
2104 fn tab_over_a_selection_ahead_of_a_mixed_indents_last_run_never_interleaves() {
2105 // Fails-first: the caret fix above covered only empty carets, so a
2106 // selection ahead of the trailing run still had the unit typed in its
2107 // place — the `·` selected in `··\tfoo` became a tab, giving `\t·\tfoo`
2108 // where block indent gives `··\t\tfoo`. The selection is still typed
2109 // over (deleted), and the unit goes where block indent puts it.
2110 for (line, (from, to), want, caret) in [
2111 (" \tfoo", (0, 1), " \t\tfoo", 2),
2112 (" \tfoo", (1, 0), " \t\tfoo", 2), // either direction
2113 ("\t\t foo", (0, 1), "\t foo", 3), // a spaces line: tabs, then the run
2114 ] {
2115 let mut d = doc(line);
2116 select(&mut d, from, to);
2117 d.tab();
2118 assert_eq!(d.text(), want, "{line:?} {from}..{to}");
2119 assert_eq!(heads(&d), [caret], "{line:?} {from}..{to}: the caret lands past the new level");
2120 assert_one_undo_restores(&mut d, line);
2121 }
2122 }
2123
2124 #[test]
2125 fn tab_from_several_selections_in_one_mixed_indent_stays_disjoint() {
2126 // Fails-first: each caret ahead of the run inserted at the run's start
2127 // even when another selection typed over that very point — two
2128 // overlapping ops, a verb bug the debug overlap assert caught (and a
2129 // silent no-op in release). Against a selection reaching into the run,
2130 // the unit goes at that selection's start (where the run starts once
2131 // it is typed over); several selections ahead of the run each delete
2132 // and each add a level there.
2133 for (line, sels, want, carets) in [
2134 // A caret ahead of the run + a selection straddling the run start.
2135 (" \t\tfoo", &[(0, 0), (1, 3)][..], " \t\t\tfoo", &[2, 3][..]),
2136 // The same with a selection, not a caret, ahead of the run.
2137 (" \t\tfoo", &[(0, 1), (2, 4)][..], " \t\t\tfoo", &[2, 3][..]),
2138 // Two selections ahead of the run: both deleted, two levels added.
2139 (" \tfoo", &[(0, 1), (2, 3)][..], " \t\t\tfoo", &[3, 4][..]),
2140 ] {
2141 let mut d = doc(line);
2142 d.set_selections(SelectionSet::from_ranges(sels, 0));
2143 d.tab();
2144 assert_eq!(d.text(), want, "{line:?} {sels:?}");
2145 assert_eq!(heads(&d), carets, "{line:?} {sels:?}");
2146 assert_one_undo_restores(&mut d, line);
2147 }
2148 }
2149
2150 #[test]
2151 fn backspace_in_mixed_indentation_deletes_one_unit() {
2152 // Backspace-to-stop reads the same unit from the whitespace before the
2153 // caret: spaces after a tab go back to the stop counted from the tab…
2154 let mut d = doc("\t foo"); // tab + 6
2155 d.set_selections(SelectionSet::new(7));
2156 assert_one_step(&mut d, Document::backspace, "\t foo");
2157 d.set_selections(SelectionSet::new(7));
2158 d.backspace();
2159 assert_eq!(heads(&d), [5], "the caret lands on the stop");
2160 // …and a tab before the caret goes one tab at a time.
2161 let mut d = doc(" \t\tfoo");
2162 d.set_selections(SelectionSet::new(4));
2163 assert_one_step(&mut d, Document::backspace, " \tfoo");
2164 }
2165
2166 #[test]
2167 fn block_indent_over_mixed_kind_lines_keeps_each_lines_kind() {
2168 // One multi-line selection over tab-indented, space-indented, mixed and
2169 // unindented lines: every line moves by ITS OWN unit, in one step.
2170 let text = "\ta\n b\n\t c\nd\n \te";
2171 let mut d = doc(text);
2172 d.select_all();
2173 assert_one_step(&mut d, Document::tab, "\t\ta\n b\n\t c\n d\n \t\te");
2174 d.select_all();
2175 assert_one_step(&mut d, Document::outdent, "a\nb\n\tc\nd\n e");
2176 // Multi-caret Shift+Tab — one caret per line, mixed kinds — likewise.
2177 let mut d = doc(text);
2178 d.set_selections(SelectionSet::from_offsets(&[2, 8, 13, 15, 20]));
2179 assert_one_step(&mut d, Document::outdent, "a\nb\n\tc\nd\n e");
2180 }
2181
2182 #[test]
2183 fn multi_cursor_typing_edits_every_caret() {
2184 let mut d = doc("a b c");
2185 d.selections = SelectionSet::new(0);
2186 d.selections.add_caret(2);
2187 d.selections.add_caret(4);
2188 d.type_char('X');
2189 assert_eq!(d.text(), "Xa Xb Xc");
2190 assert_eq!(d.selections().len(), 3);
2191 }
2192
2193 #[test]
2194 fn newline_paste_is_normalized_and_splits_lines() {
2195 let mut d = doc("");
2196 d.insert_text("one\r\ntwo");
2197 assert_eq!(d.text(), "one\ntwo");
2198 assert_eq!(d.buffer().line_count(), 2);
2199 }
2200
2201 #[test]
2202 fn enter_undoes_and_redoes() {
2203 let mut d = doc("ab");
2204 d.selections = SelectionSet::new(1);
2205 d.enter();
2206 assert_eq!(d.text(), "a\nb");
2207 assert!(d.undo());
2208 assert_eq!(d.text(), "ab");
2209 assert!(d.redo());
2210 assert_eq!(d.text(), "a\nb");
2211 }
2212
2213 #[test]
2214 fn caret_tracks_text_after_undo() {
2215 let mut d = doc("");
2216 for ch in "hello".chars() {
2217 d.type_char(ch);
2218 }
2219 d.undo(); // removes the whole run
2220 assert!(d.selections().all()[0].head() <= d.buffer().len(), "caret stays valid");
2221 }
2222
2223 #[test]
2224 fn undo_returns_the_caret_to_the_edit() {
2225 // Undo restores the caret from before the run (the region's start here);
2226 // redo the caret the run left (its end) — as if the edit had just
2227 // (un)happened.
2228 let mut d = doc("");
2229 for ch in "hello".chars() {
2230 d.type_char(ch);
2231 }
2232 assert_eq!(d.selections().all()[0].head(), 5, "caret after typing");
2233 d.undo();
2234 assert_eq!(d.buffer().text(), "", "text undone");
2235 assert_eq!(d.selections().all()[0].head(), 0, "caret returns to the edit site");
2236 d.redo();
2237 assert_eq!(d.buffer().text(), "hello", "text redone");
2238 assert_eq!(d.selections().all()[0].head(), 5, "caret follows the redo");
2239 }
2240
2241 #[test]
2242 fn undo_jumps_the_caret_to_the_edit_from_afar() {
2243 // Edit at the top, then click a caret elsewhere, then undo. The caret
2244 // must JUMP to the reverted edit (so you see what changed), landing at
2245 // the edit site rather than merely rebasing where it was parked.
2246 let mut d = doc("aaa\nbbb\nccc\nddd\n");
2247 d.set_selections(crate::SelectionSet::new(0)); // top of the document
2248 d.type_char('\n'); // an edit at offset 0 (like hitting Enter at the top)
2249 d.set_selections(crate::SelectionSet::new(10)); // click a caret down in the body
2250 assert_eq!(d.selections().all()[0].head(), 10, "caret parked in the body");
2251 assert!(d.undo());
2252 assert_eq!(d.buffer().text(), "aaa\nbbb\nccc\nddd\n", "the newline is gone");
2253 assert_eq!(
2254 d.selections().all()[0].head(),
2255 0,
2256 "undo jumps the caret to the reverted edit, not leaves it at 10"
2257 );
2258 }
2259}