Skip to main content

scrive_core/
verbs.rs

1//! The editing verbs: type, backspace, delete, enter, paste, and (out)dent.
2//! Each turns the current selections into one atomic multi-range transaction,
3//! then places a caret after each edit — so every verb is multi-cursor-correct
4//! for free (one op per selection).
5//!
6//! Two load-bearing semantics live here: **typing over a selection replaces
7//! it** (one transaction, caret after), and **Enter's autoindent is the leading
8//! whitespace truncated to the caret column** (never duplicated) — plus the two
9//! brace rules (one unit deeper after a line-opening `{`; `{|}` splits onto
10//! three lines), always computed for *inserted* text only, never re-indenting
11//! existing lines, so autoindent cannot fight the user. Clipboard text is
12//! normalized to LF on the way in; the LF→OS-flavor re-expansion for copy-out
13//! lives in the widget layer.
14
15use core::ops::Range;
16use std::borrow::Cow;
17
18use crate::autoclose;
19use crate::buffer::Buffer;
20use crate::coords::Point;
21use crate::document::Document;
22use crate::history::{GroupingHint, OpClass};
23use crate::row_layout::tail_start_col;
24use crate::selection::{Selection, SelectionSet};
25use crate::transaction::EditOp;
26
27/// Spaces per indent level, the width Tab and the indent verbs insert.
28///
29/// Deliberately a SEPARATE knob from [`crate::display_map::default_tab_size`]
30/// (an *editing* width vs a *display* width — mainstream editors keep both).
31/// They happen to share the value 4; if either ever changes, the widget's
32/// indent guides key on the display knob and Tab/indent verbs on this one.
33#[must_use]
34pub fn default_indent_size() -> u32 {
35    4
36}
37
38impl Document {
39    /// Insert `ch` at every caret, replacing any non-empty selection. Merges
40    /// into the current typing run for undo.
41    pub fn type_char(&mut self, ch: char) {
42        // Auto-close, applied uniformly across ALL carets (a single caret is
43        // just the one-element case). Each helper handles the whole keystroke and
44        // returns true; overtype takes precedence (a quote is opener AND closer).
45        if self.try_overtype(ch) {
46            return; // a closer typed over its own auto-inserted close, at every caret
47        }
48        if self.try_autoclose(ch) {
49            return; // an opener at empty carets → insert the pair (per-caret guard)
50        }
51        if self.try_surround(ch) {
52            return; // an opener over non-empty selections → surround each
53        }
54        // Plain insert (also the mixed / guard-failing / non-pair case). Replaces
55        // every selection with `ch`; `edit_grouped` rebases and validates live
56        // provenance, so typing inside a pair keeps overtype available.
57        let mut s = [0u8; 4];
58        let text: &str = ch.encode_utf8(&mut s);
59        let ops = self.map_selections(|sel_start, sel_end, _| EditOp::new(sel_start..sel_end, text));
60        self.run_edit(ops, GroupingHint::mergeable(OpClass::Type));
61    }
62
63    /// Whether pair-insert of `open` is allowed at an empty `caret` — the guard
64    /// that keeps auto-close from firing where a closer would be unwelcome.
65    fn can_autoclose(&self, open: char, caret: u32) -> bool {
66        // The char after the caret must be whitespace, EOL, or a closer —
67        // otherwise the pair would split a word or an existing token.
68        match self.buffer.char_at(caret) {
69            None => {}
70            Some(c) if c.is_whitespace() || autoclose::is_closer(c) => {}
71            Some(_) => return false,
72        }
73        if autoclose::is_quote(open) {
74            // For a quote, the char before must not be a word char (else it is
75            // an apostrophe or a suffix quote, not an opening string delimiter).
76            if let Some(p) = self.buffer.char_before(caret) {
77                if autoclose::is_word_char(p) {
78                    return false;
79                }
80            }
81            // An odd count of quotes on the line ⇒ this one closes an already
82            // open string; insert it literally rather than pairing.
83            let row = self.buffer.offset_to_point(caret).row;
84            let quotes = self.buffer.line(row).chars().filter(|&c| c == '"').count();
85            if quotes % 2 == 1 {
86                return false;
87            }
88        }
89        true
90    }
91
92    /// Overtype: if `ch` is a closer and EVERY selection is an empty caret
93    /// sitting exactly on a live pair's matching close, skip over each (no
94    /// insert) and consume all provenance. All-or-nothing, so a mixed set falls
95    /// through. Returns whether it handled the keystroke.
96    fn try_overtype(&mut self, ch: char) -> bool {
97        if !autoclose::is_closer(ch) {
98            return false;
99        }
100        let ranges = self.autoclose_ranges(); // ascending, disjoint ⇒ ends ascending
101        let on_close = |caret: u32| {
102            let i = ranges.partition_point(|r| r.end < caret);
103            i < ranges.len() && ranges[i].end == caret
104        };
105        let sels = self.selections.all();
106        let all = !ranges.is_empty()
107            && sels.iter().all(|s| {
108                s.is_empty() && self.buffer.char_at(s.head()) == Some(ch) && on_close(s.head())
109            });
110        if !all {
111            return false;
112        }
113        let step = ch.len_utf8() as u32;
114        let carets: Vec<u32> = sels.iter().map(|s| s.head() + step).collect();
115        self.selections = SelectionSet::from_offsets(&carets);
116        self.clear_autoclose(); // the pairs are consumed
117        true
118    }
119
120    /// Auto-close: if `ch` is an opener and EVERY selection is an empty caret,
121    /// insert `open+close` at each caret that passes the guards and a bare
122    /// `open` at the rest (a per-caret decision, as mainstream editors make it),
123    /// place each caret just after the opener, and record provenance for the
124    /// paired ones. Returns whether it handled the keystroke (false if any
125    /// selection is non-empty).
126    fn try_autoclose(&mut self, ch: char) -> bool {
127        let Some(close) = autoclose::opener_close(ch) else {
128            return false;
129        };
130        if self.selections.all().iter().any(|s| !s.is_empty()) {
131            return false; // a non-empty selection ⇒ surround/plain, not pair-insert
132        }
133        // Per-caret pair-or-plain decision, computed before the edit.
134        let plan: Vec<bool> =
135            self.selections.all().iter().map(|s| self.can_autoclose(ch, s.head())).collect();
136        let mut pair_text = String::with_capacity(ch.len_utf8() + close.len_utf8());
137        pair_text.push(ch);
138        pair_text.push(close);
139        let mut open_buf = [0u8; 4];
140        let open_text: &str = ch.encode_utf8(&mut open_buf);
141        let ops: Vec<EditOp> = self
142            .selections
143            .all()
144            .iter()
145            .zip(&plan)
146            .map(|(s, &pair)| {
147                let at = s.head();
148                EditOp::new(at..at, if pair { pair_text.as_str() } else { open_text })
149            })
150            .collect();
151        let hint = GroupingHint { op: OpClass::Type, seal_before: true, seal_after: false };
152        let Ok(committed) = self.edit_grouped(ops, hint) else {
153            return true; // ops are disjoint by construction
154        };
155        // A caret just after each opener; provenance for the paired ones.
156        let close_len = close.len_utf8() as u32;
157        let edits = committed.patch().edits();
158        let mut carets = Vec::with_capacity(edits.len());
159        let mut pairs = Vec::new();
160        for (e, &pair) in edits.iter().zip(&plan) {
161            if pair {
162                let close_off = e.new.end - close_len; // start of the close char
163                carets.push(close_off); // caret between open and close
164                pairs.push((e.new.start, close_off)); // [open, close)
165            } else {
166                carets.push(e.new.end); // after the bare opener
167            }
168        }
169        self.selections = SelectionSet::from_offsets(&carets);
170        if !pairs.is_empty() {
171            // Supersede any prior provenance — e.g. an outer pair whose tracked
172            // range grew to enclose this insertion (nested `(` inside `()`) — so
173            // the live set stays disjoint. Nested pairs must not accumulate, or
174            // overtype's ends-ascending binary search picks the wrong pair and a
175            // closer inserts literally (`(()))`). One generation of provenance,
176            // always.
177            self.clear_autoclose();
178            self.add_autoclose_pairs(pairs);
179        }
180        true
181    }
182
183    /// Surround: if `ch` is an opener and EVERY selection is non-empty, wrap
184    /// each in the pair with the selection staying on the interior. Returns
185    /// whether it handled the keystroke.
186    fn try_surround(&mut self, ch: char) -> bool {
187        let Some(close) = autoclose::opener_close(ch) else {
188            return false;
189        };
190        let sels = self.selections.all();
191        if sels.is_empty() || sels.iter().any(|s| s.is_empty()) {
192            return false;
193        }
194        let (open_len, close_len) = (ch.len_utf8() as u32, close.len_utf8() as u32);
195        // ONE replace op per selection (open + content + close). A single op per
196        // selection keeps TOUCHING selections independent: two zero-width inserts
197        // at the same offset (opener then closer) would be ambiguous to bias, so
198        // a successor selection starting where its predecessor ends could swallow
199        // the inserted opener. One replace has an unambiguous span instead.
200        let ops: Vec<EditOp> = sels
201            .iter()
202            .map(|s| {
203                let content = self.buffer.slice(s.start()..s.end());
204                let mut wrapped = String::with_capacity(content.len() + open_len as usize + close_len as usize);
205                wrapped.push(ch);
206                wrapped.push_str(&content);
207                wrapped.push(close);
208                EditOp::new(s.start()..s.end(), &wrapped)
209            })
210            .collect();
211        let Ok(committed) = self.edit_grouped(ops, GroupingHint::discrete()) else {
212            return true;
213        };
214        // Each selection stays on its interior, read straight from the edits.
215        let ranges: Vec<(u32, u32)> = committed
216            .patch()
217            .edits()
218            .iter()
219            .map(|e| (e.new.start + open_len, e.new.end - close_len))
220            .collect();
221        let newest = ranges.len() - 1;
222        self.selections = SelectionSet::from_ranges(&ranges, newest);
223        true
224    }
225
226    /// Insert `text` (e.g. a paste) at every caret, replacing selections. A
227    /// discrete undo step; `\r\n|\r` is normalized to LF.
228    pub fn insert_text(&mut self, text: &str) {
229        let ops = self.map_selections(|sel_start, sel_end, _| {
230            EditOp::new(sel_start..sel_end, text)
231        });
232        self.run_edit(ops, GroupingHint::discrete());
233    }
234
235    /// The text Copy/Cut should put on the clipboard, with the `is_entire_line`
236    /// flag the paste side honors. Each non-empty selection contributes its
237    /// text, joined by newlines. When **every** selection is an empty caret,
238    /// each caret contributes its ENTIRE line — the one `Document::line_unit`
239    /// rule — including the newline (a final line without a terminator still
240    /// exports one so the paste splices cleanly), duplicate lines deduplicated,
241    /// and the flag is `true`. LF-only; the OS-flavor re-expansion and the side
242    /// table live in the widget.
243    #[must_use]
244    pub fn clipboard_payload(&self) -> (String, bool) {
245        let sels = self.selections.all();
246        if sels.iter().all(Selection::is_empty) {
247            let mut units: Vec<(u32, u32)> = sels.iter().map(|s| self.line_unit(s.head())).collect();
248            units.dedup();
249            let lines: String = units
250                .iter()
251                .map(|&(start, end)| {
252                    let slice = self.buffer.slice(start..end);
253                    if slice.ends_with('\n') {
254                        slice.into_owned()
255                    } else {
256                        format!("{slice}\n")
257                    }
258                })
259                .collect();
260            return (lines, true);
261        }
262        let joined = sels
263            .iter()
264            .filter(|s| !s.is_empty())
265            .map(|s| self.buffer.slice(s.start()..s.end()))
266            .collect::<Vec<_>>()
267            .join("\n");
268        (joined, false)
269    }
270
271    /// Cut's edit half: cut is copy-then-delete, so this deletes exactly what
272    /// [`Document::clipboard_payload`] exported — non-empty selections; or,
273    /// when EVERY selection is an empty caret (the payload's whole-line mode,
274    /// same gate), each caret's `line_block`. In a
275    /// mixed set the empty carets reached the clipboard as nothing, so they
276    /// delete nothing — cut never destroys text that was not copied. The
277    /// final-line block takes its PRECEDING newline while the payload
278    /// synthesizes a trailing one: the paste splices a whole line either way,
279    /// and no empty tail line is left behind. Overlapping ranges (two carets on
280    /// one line) merge; one discrete transaction, caret after each deletion.
281    pub fn cut(&mut self) {
282        let all_empty = self.selections.all().iter().all(Selection::is_empty);
283        let ranges: Vec<Range<u32>> = self
284            .selections
285            .all()
286            .iter()
287            .filter_map(|sel| {
288                if sel.is_empty() {
289                    all_empty.then(|| self.line_block(sel))
290                } else {
291                    Some(sel.start()..sel.end())
292                }
293            })
294            .collect();
295        let ops = merge_delete_ranges(ranges.into_iter());
296        self.run_edit(ops, GroupingHint::discrete());
297    }
298
299    /// Paste. Plain: replace each selection, caret after (a discrete step).
300    /// `entire_line` at all-empty carets: insert before each caret's line start
301    /// with **no caret placement** — the carets rebase past the insertion and
302    /// stay put on their own lines, matching mainstream editors.
303    pub fn paste(&mut self, text: &str, entire_line: bool) {
304        if entire_line && self.selections.all().iter().all(Selection::is_empty) {
305            let mut starts: Vec<u32> =
306                self.selections.all().iter().map(|s| self.line_unit(s.head()).0).collect();
307            starts.dedup();
308            let ops = starts.into_iter().map(|at| EditOp::new(at..at, text)).collect();
309            // `edit_grouped` rebases the selections through the patch — never
310            // `run_edit`, whose caret-after would yank the caret off its line.
311            let _ = self.edit_grouped(ops, GroupingHint::discrete());
312            return;
313        }
314        self.insert_text(text);
315    }
316
317    /// Backspace: delete the selection, or the character before an empty caret
318    /// (to the previous tab stop when the caret sits in leading whitespace).
319    pub fn backspace(&mut self) {
320        // Pair-backspace: every empty caret strictly between a live *empty*
321        // pair deletes both chars (`open + 1 == close` ⇒ nothing typed between
322        // them). All-or-nothing (symmetric with overtype); a mixed set falls
323        // through to the ordinary per-caret backspace below.
324        let ranges = self.autoclose_ranges();
325        if !ranges.is_empty() {
326            let empty_pair_at = |caret: u32| -> Option<Range<u32>> {
327                let i = ranges.partition_point(|r| r.end < caret);
328                ranges.get(i).filter(|r| r.end == caret && r.start + 1 == r.end).cloned()
329            };
330            let pairs: Option<Vec<Range<u32>>> = self
331                .selections
332                .all()
333                .iter()
334                .map(|s| s.is_empty().then(|| empty_pair_at(s.head())).flatten())
335                .collect();
336            if let Some(pairs) = pairs {
337                let ops: Vec<EditOp> = pairs
338                    .iter()
339                    .map(|r| {
340                        let close_len =
341                            self.buffer.char_at(r.end).map_or(1, |c| c.len_utf8() as u32);
342                        EditOp::delete(r.start..r.end + close_len)
343                    })
344                    .collect();
345                self.clear_autoclose();
346                let hint =
347                    GroupingHint { op: OpClass::Delete, seal_before: true, seal_after: true };
348                self.run_edit(ops, hint);
349                return;
350            }
351        }
352        let buffer = &self.buffer;
353        let ops = self.selections.all().iter().filter_map(|sel| {
354            if !sel.is_empty() {
355                return Some(EditOp::delete(sel.start()..sel.end()));
356            }
357            let caret = sel.head();
358            if caret == 0 {
359                return None;
360            }
361            let start = backspace_target(buffer, caret);
362            Some(EditOp::delete(start..caret))
363        });
364        let ops: Vec<_> = ops.collect();
365        self.run_edit(ops, GroupingHint::mergeable(OpClass::Delete));
366    }
367
368    /// Forward delete: delete the selection, or the character after an empty
369    /// caret (merging the next line when at end of line).
370    pub fn delete_forward(&mut self) {
371        let len = self.buffer.len();
372        let buffer = &self.buffer;
373        let ops: Vec<_> = self
374            .selections
375            .all()
376            .iter()
377            .filter_map(|sel| {
378                if !sel.is_empty() {
379                    return Some(EditOp::delete(sel.start()..sel.end()));
380                }
381                let caret = sel.head();
382                if caret >= len {
383                    return None;
384                }
385                let next = buffer.char_at(caret).expect("caret < len");
386                Some(EditOp::delete(caret..caret + next.len_utf8() as u32))
387            })
388            .collect();
389        self.run_edit(ops, GroupingHint::mergeable(OpClass::Delete));
390    }
391
392    /// Delete the word before each caret — Ctrl+Backspace. A non-empty
393    /// selection deletes as-is; an empty caret deletes back to the previous word
394    /// start, or *only* the newline at column 0 (never newline + previous word).
395    /// A discrete undo step (word-delete seals the run on both sides).
396    ///
397    /// Two carets inside one word yield *overlapping* delete ranges, which the
398    /// transaction engine rejects — so the ranges are merged first (the shared
399    /// span is deleted once and the carets collapse via the selection merge
400    /// rule), the one verb that can produce overlap.
401    pub fn delete_word_back(&mut self) {
402        let buffer = &self.buffer;
403        let ranges = self.selections.all().iter().filter_map(|sel| {
404            if !sel.is_empty() {
405                return Some(sel.start()..sel.end());
406            }
407            let caret = sel.head();
408            let start = crate::movement::word_delete_left(buffer, caret);
409            (start < caret).then_some(start..caret)
410        });
411        let ops = merge_delete_ranges(ranges);
412        self.run_edit(ops, GroupingHint { op: OpClass::Delete, seal_before: true, seal_after: true });
413    }
414
415    /// Delete the word after each caret — Ctrl+Delete, the
416    /// [`delete_word_back`](Self::delete_word_back) mirror (overlapping ranges
417    /// merged the same way).
418    pub fn delete_word_forward(&mut self) {
419        let len = self.buffer.len();
420        let buffer = &self.buffer;
421        let ranges = self.selections.all().iter().filter_map(|sel| {
422            if !sel.is_empty() {
423                return Some(sel.start()..sel.end());
424            }
425            let caret = sel.head();
426            if caret >= len {
427                return None;
428            }
429            let end = crate::movement::word_delete_right(buffer, caret);
430            (end > caret).then_some(caret..end)
431        });
432        let ops = merge_delete_ranges(ranges);
433        self.run_edit(ops, GroupingHint { op: OpClass::Delete, seal_before: true, seal_after: true });
434    }
435
436    /// Enter: split the line at every caret, carrying the current line's
437    /// leading indentation (truncated to the caret column — never duplicated),
438    /// plus the two brace rules (both computed from raw text at insert time —
439    /// existing lines are never re-indented, so autoindent cannot fight the
440    /// user):
441    ///
442    /// - **(a)** the current line's text left of the caret, right-trimmed,
443    ///   ends with `{` → the new line gets one extra `indent_unit`;
444    /// - **(b)** the caret sits exactly between a pair (`{|}`) → insert two
445    ///   lines (`\n indent+unit \n indent`), closer dedented onto its own
446    ///   line, caret at the end of the middle line (one line up from the
447    ///   inserted text's end, indented one unit deeper than the closer).
448    ///
449    /// Its own undo step (seals before; typing after merges into the run).
450    pub fn enter(&mut self) {
451        let buffer = &self.buffer;
452        // Per-op distance from the inserted text's end BACK to the caret —
453        // 0 except for rule (b), where the caret lands one line up.
454        let mut backs: Vec<u32> = Vec::new();
455        let ops: Vec<_> = self
456            .selections
457            .all()
458            .iter()
459            .map(|sel| {
460                let indent = enter_indent(buffer, sel.start());
461                let opens = line_opens_block(buffer, sel.start());
462                let between = opens
463                    && buffer.char_before(sel.start()) == Some('{')
464                    && buffer.char_at(sel.end()) == Some('}');
465                let mut t = String::with_capacity(2 * (1 + indent.len()) + 4);
466                t.push('\n');
467                t.push_str(&indent);
468                if opens {
469                    t.push_str(&indent_unit(&indent));
470                }
471                if between {
472                    t.push('\n');
473                    t.push_str(&indent);
474                    backs.push(1 + indent.len() as u32); // back over "\n" + indent
475                } else {
476                    backs.push(0);
477                }
478                EditOp::new(sel.start()..sel.end(), t)
479            })
480            .collect();
481        let hint = GroupingHint { op: OpClass::Type, seal_before: true, seal_after: false };
482        let Ok(committed) = self.edit_grouped(ops, hint) else {
483            return;
484        };
485        if committed.is_empty() {
486            return;
487        }
488        // `run_edit`'s caret-after, minus each op's rule-(b) pull-back.
489        let carets: Vec<u32> = committed
490            .patch()
491            .edits()
492            .iter()
493            .zip(&backs)
494            .map(|(e, back)| e.new.end - back)
495            .collect();
496        self.selections = SelectionSet::from_offsets(&carets);
497    }
498
499    /// Tab: at an empty caret insert spaces to the next indent stop; over a
500    /// non-empty selection indent every spanned line one level.
501    pub fn tab(&mut self) {
502        let indent = default_indent_size();
503        if self.selection_spans_multiple_lines() {
504            // Multi-line selection → indent the whole block, selection preserved.
505            self.indent_lines(indent as i32);
506        } else {
507            // Carets and single-line selections → insert / replace-with spaces to
508            // the next tab stop. A single-line selection *types over* (as
509            // mainstream editors do): it does NOT indent the whole line.
510            let ops = self.map_selections(|start, end, buffer| {
511                let col = buffer.offset_to_point(start).col;
512                let n = indent - (col % indent);
513                EditOp::new(start..end, " ".repeat(n as usize))
514            });
515            self.run_edit(ops, GroupingHint::discrete());
516        }
517    }
518
519    /// Shift+Tab: outdent every spanned line by one indent level. Unlike Tab
520    /// (which types over a single-line selection), Shift+Tab always outdents —
521    /// carets, single-line selections, and multi-line selections alike; the
522    /// selection is preserved.
523    pub fn outdent(&mut self) {
524        self.indent_lines(-(default_indent_size() as i32));
525    }
526
527    /// Delete each selection's whole-line block (Ctrl+Shift+K) — the one
528    /// `line_block` rule per selection. Overlapping
529    /// blocks (two carets on one line) merge; one discrete step, caret after.
530    pub fn delete_line(&mut self) {
531        let ranges: Vec<Range<u32>> =
532            self.selections.all().iter().map(|sel| self.line_block(sel)).collect();
533        let ops = merge_delete_ranges(ranges.into_iter());
534        self.run_edit(ops, GroupingHint::discrete());
535    }
536
537    /// THE whole-line **deletion block** for a selection: every spanned line
538    /// (a selection ending exactly at a line start does not span it — the
539    /// [`last_spanned_offset`] rule shared with `spanned_rows`) plus ONE
540    /// bounding newline: the trailing one, or, when the block reaches the
541    /// buffer's final line, the PRECEDING one — so no empty tail line is left
542    /// behind. Shared by [`Document::delete_line`] and the
543    /// whole-line [`Document::cut`]. (Copy's per-line text is the different
544    /// `Document::line_unit` fact — content never takes a preceding `\n`.)
545    fn line_block(&self, sel: &Selection) -> Range<u32> {
546        let buffer = &self.buffer;
547        let first = buffer.offset_to_point(sel.start()).row;
548        let last = buffer.offset_to_point(last_spanned_offset(sel)).row;
549        let start = buffer.point_to_offset(Point::new(first, 0));
550        if last + 1 < buffer.line_count() {
551            start..buffer.point_to_offset(Point::new(last + 1, 0))
552        } else {
553            start.saturating_sub(1)..buffer.len()
554        }
555    }
556
557    /// Open a fresh, indent-carrying line below (`down`, Ctrl+Enter) or above
558    /// (Ctrl+Shift+Enter) each caret's line — without splitting the line the
559    /// caret sat on — and land the caret at the new line's end. A line below
560    /// a block-opening `{` line gains one indent unit, like Enter at the line's
561    /// end. One discrete transaction.
562    pub fn insert_line(&mut self, down: bool) {
563        let buffer = &self.buffer;
564        let mut backs: Vec<u32> = Vec::new();
565        let mut ops: Vec<EditOp> = Vec::new();
566        for sel in self.selections.all() {
567            let row = buffer.offset_to_point(sel.head()).row;
568            let line = buffer.line(row);
569            let indent = &line[..tail_start_col(&line) as usize];
570            if down {
571                let at = buffer.point_to_offset(Point::new(row, buffer.line_len(row)));
572                if ops.last().is_some_and(|op| op.range.start == at) {
573                    continue; // second caret on the same line — one new line
574                }
575                let mut t = String::with_capacity(1 + indent.len() + 4);
576                t.push('\n');
577                t.push_str(indent);
578                if line_opens_block(buffer, at) {
579                    t.push_str(&indent_unit(indent));
580                }
581                ops.push(EditOp::new(at..at, t));
582                backs.push(0);
583            } else {
584                let at = buffer.point_to_offset(Point::new(row, 0));
585                if ops.last().is_some_and(|op| op.range.start == at) {
586                    continue;
587                }
588                // The new line's indent, then the newline that pushes the
589                // current line down — caret pulls back over the `\n`.
590                ops.push(EditOp::new(at..at, format!("{indent}\n")));
591                backs.push(1);
592            }
593        }
594        let Ok(committed) = self.edit_grouped(ops, GroupingHint::discrete()) else {
595            return;
596        };
597        if committed.is_empty() {
598            return;
599        }
600        let carets: Vec<u32> = committed
601            .patch()
602            .edits()
603            .iter()
604            .zip(&backs)
605            .map(|(e, back)| e.new.end - back)
606            .collect();
607        self.selections = SelectionSet::from_offsets(&carets);
608    }
609
610    /// Toggle the line comment on every line the selections span (Ctrl+/).
611    /// If ANY spanned non-blank line is uncommented, comment them all —
612    /// `prefix + " "` inserted at the block's minimum indent column,
613    /// so the markers align — otherwise strip each line's prefix (and one
614    /// following space). Blank lines are skipped, unless EVERY spanned line is
615    /// blank (the start-a-comment-here case, which appends the prefix). One
616    /// transaction; the selections rebase through it and survive. A no-op until
617    /// the app injects a prefix ([`Document::set_line_comment`]) — the core
618    /// knows no language.
619    pub fn toggle_line_comment(&mut self) {
620        let Some(prefix) = self.line_comment().map(str::to_owned) else { return };
621        let rows = self.spanned_rows();
622        let ops: Vec<EditOp> = {
623            let buffer = &self.buffer;
624            let lines: Vec<(u32, Cow<str>)> = rows.iter().map(|&r| (r, buffer.line(r))).collect();
625            let non_blank: Vec<(u32, &str)> = lines
626                .iter()
627                .map(|(r, l)| (*r, l.as_ref()))
628                .filter(|(_, l)| !l.trim().is_empty())
629                .collect();
630            if non_blank.is_empty() {
631                // Every spanned line is blank: start a comment on each.
632                lines
633                    .iter()
634                    .map(|&(row, _)| {
635                        let at = buffer.point_to_offset(Point::new(row, buffer.line_len(row)));
636                        EditOp::new(at..at, format!("{prefix} "))
637                    })
638                    .collect()
639            } else if non_blank.iter().any(|(_, l)| !l.trim_start().starts_with(&prefix)) {
640                // Comment: markers aligned at the block's minimum indent.
641                let indent = |l: &str| tail_start_col(l);
642                let min_indent = non_blank.iter().map(|(_, l)| indent(l)).min().unwrap_or(0);
643                non_blank
644                    .iter()
645                    .map(|&(row, _)| {
646                        let at = buffer.point_to_offset(Point::new(row, min_indent));
647                        EditOp::new(at..at, format!("{prefix} "))
648                    })
649                    .collect()
650            } else {
651                // Uncomment: strip the prefix plus one following space.
652                non_blank
653                    .iter()
654                    .map(|&(row, line)| {
655                        let ws = tail_start_col(line) as usize;
656                        let start = buffer.point_to_offset(Point::new(row, ws as u32));
657                        let mut end = start + prefix.len() as u32;
658                        if line[ws + prefix.len()..].starts_with(' ') {
659                            end += 1;
660                        }
661                        EditOp::delete(start..end)
662                    })
663                    .collect()
664            }
665        };
666        // `edit_grouped` rebases the selections through the patch, so the
667        // selection (or caret) survives the toggle in place.
668        let _ = self.edit_grouped(ops, GroupingHint::discrete());
669    }
670
671    /// Whether any selection spans more than one line — the trigger for block
672    /// (out)dent (vs. type-over / caret indent).
673    fn selection_spans_multiple_lines(&self) -> bool {
674        self.selections.all().iter().any(|s| {
675            self.buffer.offset_to_point(s.start()).row != self.buffer.offset_to_point(s.end()).row
676        })
677    }
678
679    /// Indent (`n > 0`) or outdent (`n < 0`) every line the selections span, by
680    /// `|n|` spaces, as one transaction. The **selection is preserved** — it
681    /// rebases through the edit (its endpoints shift with the indent), as
682    /// mainstream editors do, rather than collapsing to one caret per line.
683    fn indent_lines(&mut self, n: i32) {
684        let rows = self.spanned_rows();
685        let buffer = &self.buffer;
686        let mut ops = Vec::new();
687        for row in rows {
688            let line_start = buffer.point_to_offset(Point::new(row, 0));
689            if n > 0 {
690                ops.push(EditOp::insert(line_start, " ".repeat(n as usize)));
691            } else {
692                let line = buffer.line(row);
693                let removable =
694                    line.bytes().take((-n) as usize).take_while(|&b| b == b' ').count() as u32;
695                if removable > 0 {
696                    ops.push(EditOp::delete(line_start..line_start + removable));
697                }
698            }
699        }
700        // `edit_grouped` rebases the selections through the patch (a range's
701        // start biases Left and its end biases Right), so the selection survives
702        // the (out)dent as a selection — not `run_edit`'s one-caret-per-op, which
703        // would collapse it to a caret.
704        let _ = self.edit_grouped(ops, GroupingHint::discrete());
705    }
706
707    /// The distinct rows any selection touches (for block (out)dent and
708    /// toggle-comment) — end-exclusivity via [`last_spanned_offset`].
709    fn spanned_rows(&self) -> Vec<u32> {
710        let buffer = &self.buffer;
711        let mut rows = Vec::new();
712        for sel in self.selections.all() {
713            let first = buffer.offset_to_point(sel.start()).row;
714            let last = buffer.offset_to_point(last_spanned_offset(sel)).row;
715            for r in first..=last {
716                if rows.last() != Some(&r) {
717                    rows.push(r);
718                }
719            }
720        }
721        rows.dedup();
722        rows
723    }
724
725    /// Build one op per selection via `f(start, end, buffer)`.
726    fn map_selections(
727        &self,
728        mut f: impl FnMut(u32, u32, &Buffer) -> EditOp,
729    ) -> Vec<EditOp> {
730        self.selections
731            .all()
732            .iter()
733            .map(|sel| f(sel.start(), sel.end(), &self.buffer))
734            .collect()
735    }
736
737    /// Apply verb ops as one transaction and drop a caret after each edit.
738    fn run_edit(&mut self, ops: Vec<EditOp>, hint: GroupingHint) {
739        let Ok(committed) = self.edit_grouped(ops, hint) else {
740            return; // overlap is a programmer error; verbs never produce it
741        };
742        if committed.is_empty() {
743            return;
744        }
745        // A caret after each edit: `new.end` is after inserted text, or the
746        // deletion point for a pure delete.
747        let carets: Vec<u32> = committed.patch().edits().iter().map(|e| e.new.end).collect();
748        self.selections = SelectionSet::from_offsets(&carets);
749    }
750}
751
752/// The last offset a selection actually spans: one back from the exclusive
753/// `end`, so a selection ending exactly at a line start does not span that
754/// line. THE end-exclusivity rule, shared by the row enumeration
755/// (`spanned_rows`) and the whole-line deletion block (`line_block`).
756fn last_spanned_offset(sel: &Selection) -> u32 {
757    if sel.end() > sel.start() {
758        sel.end() - 1
759    } else {
760        sel.end()
761    }
762}
763
764/// Merge overlapping delete ranges into a minimal non-overlapping set of delete
765/// ops, because the transaction engine rejects overlapping ops. Word-delete at
766/// two carets inside one word produces overlapping ranges — merging deletes the
767/// shared region once, and the carets collapse via the selection merge rule.
768/// Touching ranges (`a.end == b.start`) stay separate; the engine accepts those.
769fn merge_delete_ranges(ranges: impl Iterator<Item = Range<u32>>) -> Vec<EditOp> {
770    let mut sorted: Vec<Range<u32>> = ranges.collect();
771    sorted.sort_by_key(|r| r.start);
772    let mut merged: Vec<Range<u32>> = Vec::new();
773    for r in sorted {
774        match merged.last_mut() {
775            Some(last) if r.start < last.end => last.end = last.end.max(r.end),
776            _ => merged.push(r),
777        }
778    }
779    merged.into_iter().map(EditOp::delete).collect()
780}
781
782/// The byte offset backspace should delete back to from an empty caret: one tab
783/// stop if the caret is within pure leading whitespace, else one character.
784fn backspace_target(buffer: &Buffer, caret: u32) -> u32 {
785    let p = buffer.offset_to_point(caret);
786    let line = buffer.line(p.row);
787    let in_indent = p.col > 0 && line.as_bytes()[..p.col as usize].iter().all(|&b| b == b' ');
788    if in_indent {
789        let indent = default_indent_size();
790        let target_col = ((p.col - 1) / indent) * indent;
791        buffer.point_to_offset(Point::new(p.row, target_col))
792    } else {
793        let prev = buffer.char_before(caret).expect("caret > 0");
794        caret - prev.len_utf8() as u32
795    }
796}
797
798/// The leading-whitespace indent an Enter at `caret` should carry: the current
799/// line's leading whitespace, truncated to the caret's column (so pressing
800/// Enter inside the indentation does not duplicate it).
801fn enter_indent(buffer: &Buffer, caret: u32) -> String {
802    let p = buffer.offset_to_point(caret);
803    let line = buffer.line(p.row);
804    let indent_len = tail_start_col(&line) as usize;
805    let take = indent_len.min(p.col as usize);
806    line[..take].to_owned()
807}
808
809/// One indent unit matching the KIND of the `copied` leading whitespace: a
810/// tab-indented prefix grows by one hard tab, anything else (spaces or an empty
811/// prefix) by [`default_indent_size`] spaces — so an indent-carrying Enter
812/// never mixes tabs and spaces within a line.
813fn indent_unit(copied: &str) -> String {
814    if copied.ends_with('\t') {
815        "\t".to_owned()
816    } else {
817        " ".repeat(default_indent_size() as usize)
818    }
819}
820
821/// Whether the current line opens a block at `caret`: the text left of `caret`,
822/// right-trimmed, ends with `{` — the caret is entering a freshly opened block,
823/// so the new line indents one unit deeper.
824fn line_opens_block(buffer: &Buffer, caret: u32) -> bool {
825    let p = buffer.offset_to_point(caret);
826    buffer.line(p.row)[..p.col as usize].trim_end().ends_with('{')
827}
828
829#[cfg(test)]
830mod tests {
831    use super::*;
832
833    fn doc(s: &str) -> Document {
834        Document::new(s).unwrap()
835    }
836
837    /// Count of live auto-close provenance pairs, read from the dedicated
838    /// auto-close store that holds them.
839    fn ac_count(d: &Document) -> usize {
840        d.autoclose_ranges().len()
841    }
842
843    #[test]
844    fn typing_inserts_and_advances_caret() {
845        let mut d = doc("");
846        d.type_char('h');
847        d.type_char('i');
848        assert_eq!(d.text(), "hi");
849        assert_eq!(d.selections().all()[0].head(), 2);
850        // One typing run → one undo.
851        assert!(d.undo());
852        assert_eq!(d.text(), "");
853    }
854
855    #[test]
856    fn typing_over_a_selection_replaces_it() {
857        // Select then type must replace the selection, leaving one caret after
858        // the inserted char — no residue of the old selection.
859        let mut d = doc("hello world");
860        d.selections = SelectionSet::new(0);
861        d.selections.set_single(crate::Selection::from_anchor(crate::SelectionId(0), 0, 5));
862        d.type_char('X');
863        assert_eq!(d.text(), "X world");
864        assert_eq!(d.selections().all()[0].head(), 1);
865        assert!(d.selections().all()[0].is_empty());
866    }
867
868    #[test]
869    fn autoclose_inserts_pair_then_overtypes() {
870        let mut d = doc("");
871        d.type_char('(');
872        assert_eq!(d.text(), "()");
873        assert_eq!(d.selections().all()[0].head(), 1); // caret between
874        d.type_char(')'); // overtype the auto-inserted close
875        assert_eq!(d.text(), "()", "overtype skips — no second )");
876        assert_eq!(d.selections().all()[0].head(), 2);
877    }
878
879    #[test]
880    fn autoclose_survives_typing_inside_the_pair() {
881        let mut d = doc("");
882        d.type_char('(');
883        d.type_char('x'); // "(x)", caret 2
884        assert_eq!(d.text(), "(x)");
885        d.type_char(')'); // still overtypes the tracked close
886        assert_eq!(d.text(), "(x)");
887        assert_eq!(d.selections().all()[0].head(), 3);
888    }
889
890    #[test]
891    fn autoclose_guard_skips_pair_before_a_word() {
892        let mut d = doc("abc"); // caret 0; the next char is a word char (guard fails)
893        d.type_char('(');
894        assert_eq!(d.text(), "(abc"); // just the opener
895        assert_eq!(d.selections().all()[0].head(), 1);
896    }
897
898    #[test]
899    fn delete_word_back_and_forward_respect_word_and_newline_bounds() {
900        let mut d = doc("foo bar\nbaz");
901        d.set_selections(crate::SelectionSet::new(11)); // end of "baz"
902        d.delete_word_back();
903        assert_eq!(d.text(), "foo bar\n"); // the word "baz" is gone
904        d.delete_word_back(); // caret at col 0 now → deletes only the newline
905        assert_eq!(d.text(), "foo bar");
906        d.set_selections(crate::SelectionSet::new(0));
907        d.delete_word_forward();
908        assert_eq!(d.text(), " bar"); // "foo" gone, the space stays
909    }
910
911    #[test]
912    fn delete_word_back_eats_only_a_multi_space_run() {
913        let mut d = doc("foo   bar"); // 3 spaces
914        d.set_selections(crate::SelectionSet::new(6)); // caret before "bar"
915        d.delete_word_back();
916        assert_eq!(d.text(), "foobar", "2+ whitespace → deletes only the run");
917    }
918
919    #[test]
920    fn multi_caret_word_delete_merges_overlapping_ranges() {
921        // Two carets in one word yield overlapping delete ranges; merged into a
922        // single delete, they remove the shared span once instead of being
923        // rejected as an overlapping transaction (which would delete nothing).
924        let mut d = doc("hello");
925        d.set_selections(crate::SelectionSet::from_offsets(&[3, 5]));
926        d.delete_word_back();
927        assert_eq!(d.text(), "", "merged into one 0..5 delete, not dropped");
928        assert_eq!(d.selections().len(), 1);
929
930        let mut d = doc("hello");
931        d.set_selections(crate::SelectionSet::from_offsets(&[0, 2]));
932        d.delete_word_forward();
933        assert_eq!(d.text(), "", "forward merge too");
934    }
935
936    #[test]
937    fn autoclose_pair_backspace_deletes_both() {
938        let mut d = doc("");
939        d.type_char('(');
940        d.backspace();
941        assert_eq!(d.text(), "");
942        assert_eq!(d.selections().all()[0].head(), 0);
943    }
944
945    #[test]
946    fn autoclose_surrounds_a_selection() {
947        let mut d = doc("foo");
948        d.selections.set_single(crate::Selection::from_anchor(crate::SelectionId(0), 0, 3));
949        d.type_char('(');
950        assert_eq!(d.text(), "(foo)");
951        let s = d.selections().all()[0];
952        assert_eq!((s.start(), s.end()), (1, 4)); // still on "foo"
953    }
954
955    #[test]
956    fn autoclose_provenance_invalidates_on_caret_move() {
957        let mut d = doc("");
958        d.type_char('('); // "()", provenance live, caret 1
959        d.move_carets(crate::Motion::Left, false); // caret 0 — leaves the pair
960        d.type_char(')'); // no live provenance → literal insert, not overtype
961        assert_eq!(d.text(), ")()");
962    }
963
964    #[test]
965    fn autoclose_quote_parity_closes_an_open_string() {
966        // Prev char is a space (the before-guard is fine), but the line already
967        // has one quote — an odd count means this quote closes the string:
968        // insert literally, no pair.
969        let mut d = doc("\" ");
970        d.selections = SelectionSet::new(2); // caret after the space
971        d.type_char('"');
972        assert_eq!(d.text(), "\" \""); // one quote added, not a pair
973    }
974
975    #[test]
976    fn autoclose_pairs_at_every_caret() {
977        // Multi-cursor '{' pairs at EVERY caret, not a lone '{'.
978        let mut d = doc("a\nb\nc"); // the three line ends are offsets 1, 3, 5
979        d.set_selections(crate::SelectionSet::from_offsets(&[1, 3, 5]));
980        d.type_char('{');
981        assert_eq!(d.text(), "a{}\nb{}\nc{}");
982        assert_eq!(ac_count(&d), 3, "one live provenance pair per caret");
983        // Each caret sits between its pair — typing lands inside every one.
984        d.type_char('x');
985        assert_eq!(d.text(), "a{x}\nb{x}\nc{x}");
986    }
987
988    #[test]
989    fn autoclose_overtypes_at_every_caret() {
990        // Typing the closer at every pair's close skips over each (no second).
991        let mut d = doc("a\nb\nc");
992        d.set_selections(crate::SelectionSet::from_offsets(&[1, 3, 5]));
993        d.type_char('{'); // "a{}\nb{}\nc{}", each caret between
994        d.type_char('}'); // overtype every one
995        assert_eq!(d.text(), "a{}\nb{}\nc{}", "overtype skips at every caret — no doubled closer");
996        assert_eq!(ac_count(&d), 0, "provenance consumed");
997    }
998
999    #[test]
1000    fn autoclose_surrounds_every_selection() {
1001        // '(' over a multi-selection surrounds each — "surround all occurrences".
1002        let mut d = doc("foo bar baz");
1003        d.set_selections(crate::SelectionSet::from_ranges(&[(0, 3), (4, 7), (8, 11)], 0));
1004        d.type_char('(');
1005        assert_eq!(d.text(), "(foo) (bar) (baz)");
1006    }
1007
1008    #[test]
1009    fn autoclose_pair_backspace_at_every_caret() {
1010        // Backspace strictly between empty pairs deletes both at every caret.
1011        let mut d = doc("a\nb\nc");
1012        d.set_selections(crate::SelectionSet::from_offsets(&[1, 3, 5]));
1013        d.type_char('{'); // "a{}\nb{}\nc{}"
1014        d.backspace();
1015        assert_eq!(d.text(), "a\nb\nc", "each empty pair deleted whole");
1016        assert_eq!(ac_count(&d), 0);
1017    }
1018
1019    #[test]
1020    fn autoclose_nested_opener_supersedes_provenance() {
1021        // Typing an opener INSIDE a live pair must supersede the prior provenance
1022        // (the outer pair grew to enclose the insertion), not accumulate it —
1023        // else overtype's ends-ascending search picks the wrong pair and the
1024        // closer inserts literally, yielding "(()))" instead of "(())".
1025        let mut d = doc("");
1026        d.type_char('('); // "()"
1027        d.type_char('('); // "(())", inner caret; prior provenance superseded
1028        assert_eq!(ac_count(&d), 1, "only the innermost pair is live");
1029        d.type_char(')'); // overtypes the inner close, not a literal insert
1030        assert_eq!(d.text(), "(())");
1031        // A different opener nested (brace then paren then close).
1032        let mut e = doc("");
1033        e.type_char('{');
1034        e.type_char('(');
1035        e.type_char(')');
1036        assert_eq!(e.text(), "{()}");
1037    }
1038
1039    #[test]
1040    fn autoclose_nested_pair_backspace_deletes_the_inner() {
1041        // Backspace between the innermost empty pair deletes both (not one).
1042        let mut d = doc("");
1043        d.type_char('(');
1044        d.type_char('('); // "(())", caret between the inner pair
1045        d.backspace();
1046        assert_eq!(d.text(), "()", "the inner empty pair is deleted whole");
1047    }
1048
1049    #[test]
1050    fn autoclose_surrounds_touching_selections() {
1051        // Two TOUCHING non-empty selections (e.g. find-all "ab" in "abab") must
1052        // each keep their own interior — the successor must land on "cd", not
1053        // swallow the opener and land on "(cd".
1054        let mut d = doc("abcd");
1055        d.set_selections(crate::SelectionSet::from_ranges(&[(0, 2), (2, 4)], 0));
1056        d.type_char('(');
1057        assert_eq!(d.text(), "(ab)(cd)");
1058        let sels = d.selections().all();
1059        assert_eq!((sels[0].start(), sels[0].end()), (1, 3), "interior 'ab'");
1060        assert_eq!((sels[1].start(), sels[1].end()), (5, 7), "interior 'cd', not '(cd'");
1061    }
1062
1063    #[test]
1064    fn autoclose_per_caret_pairs_only_where_the_guard_passes() {
1065        // A per-caret decision: a caret before a word char inserts a BARE opener
1066        // (the guard fails), while a caret at EOF pairs.
1067        let mut d = doc("ab");
1068        d.set_selections(crate::SelectionSet::from_offsets(&[0, 2]));
1069        d.type_char('{');
1070        assert_eq!(d.text(), "{ab{}"); // bare '{' before 'a', a pair at EOF
1071        assert_eq!(ac_count(&d), 1, "only the guarded caret paired");
1072    }
1073
1074    #[test]
1075    fn backspace_deletes_char_then_selection() {
1076        let mut d = doc("abc");
1077        d.selections = SelectionSet::new(3);
1078        d.backspace();
1079        assert_eq!(d.text(), "ab");
1080        d.selections = SelectionSet::new(0);
1081        d.selections.set_single(crate::Selection::from_anchor(crate::SelectionId(0), 0, 2));
1082        d.backspace();
1083        assert_eq!(d.text(), "");
1084    }
1085
1086    #[test]
1087    fn backspace_in_indentation_deletes_to_tab_stop() {
1088        let mut d = doc("        x"); // 8 spaces
1089        d.selections = SelectionSet::new(8); // caret after the spaces
1090        d.backspace();
1091        assert_eq!(d.text(), "    x"); // removed 4 (one stop), not 1
1092    }
1093
1094    #[test]
1095    fn enter_carries_indent_truncated_to_caret() {
1096        let mut d = doc("    hello");
1097        d.selections = SelectionSet::new(9); // end of line
1098        d.enter();
1099        assert_eq!(d.text(), "    hello\n    "); // 4-space indent carried
1100        // Enter at column 2 (inside the indent) carries only 2 spaces of
1101        // autoindent (truncated to the caret); the 2 spaces after the caret
1102        // stay with "hello" on the new line → line 1 has 4 spaces total, not 8
1103        // — the indent is truncated to the caret, never duplicated.
1104        let mut d2 = doc("    hello");
1105        d2.selections = SelectionSet::new(2);
1106        d2.enter();
1107        assert_eq!(d2.text(), "  \n    hello");
1108    }
1109
1110    #[test]
1111    fn delete_line_removes_blocks_and_the_final_line_cleanly() {
1112        let mut d = doc("one\ntwo\nthree");
1113        d.set_selections(crate::SelectionSet::new(5)); // caret in "two"
1114        d.delete_line();
1115        assert_eq!(d.text(), "one\nthree");
1116        // The final line takes its PRECEDING newline — no empty tail remains.
1117        d.set_selections(crate::SelectionSet::new(d.buffer().len()));
1118        d.delete_line();
1119        assert_eq!(d.text(), "one");
1120        // A multi-line selection deletes every spanned line (one ending at a
1121        // line start does not span that line)…
1122        let mut m = doc("a\nb\nc\nd");
1123        let mut set = crate::SelectionSet::new(0);
1124        set.set_single(crate::Selection::from_anchor(crate::SelectionId(0), 0, 4)); // rows 0..=1
1125        m.set_selections(set);
1126        m.delete_line();
1127        assert_eq!(m.text(), "c\nd");
1128        // …and two carets on one line delete it once.
1129        let mut t = doc("xx\nyy");
1130        t.set_selections(crate::SelectionSet::from_offsets(&[0, 1]));
1131        t.delete_line();
1132        assert_eq!(t.text(), "yy");
1133    }
1134
1135    #[test]
1136    fn insert_line_below_and_above_carry_indent() {
1137        // Below: a fresh line under the caret's line, same indent, caret on it
1138        // — the caret's line is NOT split, wherever the caret sat.
1139        let mut d = doc("    a\n    b");
1140        d.set_selections(crate::SelectionSet::new(4)); // before "a"
1141        d.insert_line(true);
1142        assert_eq!(d.text(), "    a\n    \n    b");
1143        assert_eq!(d.selections().newest().head(), 10, "caret at the new line's end");
1144        // Above: the current line is pushed down; caret on the new line.
1145        let mut u = doc("    a");
1146        u.set_selections(crate::SelectionSet::new(5));
1147        u.insert_line(false);
1148        assert_eq!(u.text(), "    \n    a");
1149        assert_eq!(u.selections().newest().head(), 4);
1150        // A line below a block-opening `{` line gains one indent unit.
1151        let mut b = doc("m {");
1152        b.set_selections(crate::SelectionSet::new(0));
1153        b.insert_line(true);
1154        assert_eq!(b.text(), "m {\n    ");
1155        assert_eq!(b.selections().newest().head(), 8);
1156    }
1157
1158    #[test]
1159    fn toggle_line_comment_comments_aligned_then_uncomments() {
1160        // Ctrl+/: markers align at the block's MIN indent; blank lines are
1161        // skipped; a mixed block comments everything; toggling back strips
1162        // prefix + one space; the selection survives (rebase, not re-set).
1163        let mut d = doc("    a\n\n        b");
1164        d.set_line_comment(Some("//"));
1165        d.select_all();
1166        d.toggle_line_comment();
1167        assert_eq!(d.text(), "    // a\n\n    //     b");
1168        assert!(!d.selections().newest().is_empty(), "selection survives the toggle");
1169        d.toggle_line_comment();
1170        assert_eq!(d.text(), "    a\n\n        b");
1171        // A bare caret toggles only its own line…
1172        let mut c = doc("x\ny");
1173        c.set_line_comment(Some("//"));
1174        c.set_selections(crate::SelectionSet::new(0));
1175        c.toggle_line_comment();
1176        assert_eq!(c.text(), "// x\ny");
1177        // …stripping works without the space too.
1178        let mut s = doc("//x");
1179        s.set_line_comment(Some("//"));
1180        s.toggle_line_comment();
1181        assert_eq!(s.text(), "x");
1182        // Mixed commented/uncommented → comment ALL, then one toggle back
1183        // returns to all-commented.
1184        let mut m = doc("// a\nb");
1185        m.set_line_comment(Some("//"));
1186        m.select_all();
1187        m.toggle_line_comment();
1188        assert_eq!(m.text(), "// // a\n// b");
1189    }
1190
1191    #[test]
1192    fn toggle_line_comment_without_a_prefix_is_a_no_op() {
1193        // The core knows no language: nothing injected → nothing happens.
1194        let mut d = doc("abc");
1195        d.toggle_line_comment();
1196        assert_eq!(d.text(), "abc");
1197        // An all-blank span starts a comment ready to type into.
1198        let mut b = doc("");
1199        b.set_line_comment(Some("//"));
1200        b.toggle_line_comment();
1201        assert_eq!(b.text(), "// ");
1202        assert_eq!(b.selections().newest().head(), 3, "caret rebased after the prefix");
1203    }
1204
1205    #[test]
1206    fn copy_and_cut_expand_an_empty_caret_to_its_line() {
1207        // An empty-selection Copy takes the ENTIRE line including \n…
1208        let mut d = doc("one\ntwo\nthree");
1209        d.set_selections(crate::SelectionSet::new(5)); // caret inside "two"
1210        assert_eq!(d.clipboard_payload(), ("two\n".to_string(), true));
1211        // …the final line (no terminator in the buffer) still exports one…
1212        d.set_selections(crate::SelectionSet::new(9)); // inside "three"
1213        assert_eq!(d.clipboard_payload(), ("three\n".to_string(), true));
1214        // …a non-empty selection copies just itself, not the line…
1215        let mut set = crate::SelectionSet::new(0);
1216        set.set_single(crate::Selection::from_anchor(crate::SelectionId(0), 0, 3));
1217        d.set_selections(set);
1218        assert_eq!(d.clipboard_payload(), ("one".to_string(), false));
1219        // …and Cut on a bare caret deletes the whole line (one undo step).
1220        d.set_selections(crate::SelectionSet::new(5));
1221        d.cut();
1222        assert_eq!(d.text(), "one\nthree");
1223        assert!(d.undo());
1224        assert_eq!(d.text(), "one\ntwo\nthree");
1225    }
1226
1227    #[test]
1228    fn mixed_set_cut_never_deletes_uncopied_lines() {
1229        // Cut IS copy+delete. In a mixed set the empty caret exports nothing, so
1230        // it must delete nothing — only the selection goes.
1231        let mut d = doc("foo bar\nbaz\n");
1232        let mut set = crate::SelectionSet::new(0);
1233        set.set_single(crate::Selection::from_anchor(crate::SelectionId(0), 0, 3)); // "foo"
1234        set.add_caret(9); // bare caret inside "baz"
1235        d.set_selections(set);
1236        assert_eq!(d.clipboard_payload(), ("foo".to_string(), false));
1237        d.cut();
1238        assert_eq!(d.text(), " bar\nbaz\n", "the uncopied `baz` line survives");
1239    }
1240
1241    #[test]
1242    fn cut_on_the_final_line_takes_the_preceding_newline() {
1243        // Cut on the final line takes its PRECEDING newline (matching its
1244        // sibling delete_line), so no empty tail line is left behind.
1245        let mut d = doc("one\ntwo");
1246        d.set_selections(crate::SelectionSet::new(5)); // inside "two"
1247        assert_eq!(d.clipboard_payload(), ("two\n".to_string(), true));
1248        d.cut();
1249        assert_eq!(d.text(), "one", "no empty tail line remains");
1250        // Degenerate: a caret on the trailing empty line cuts that line (the
1251        // preceding newline), not nothing.
1252        let mut e = doc("abc\n");
1253        e.set_selections(crate::SelectionSet::new(4));
1254        e.cut();
1255        assert_eq!(e.text(), "abc");
1256    }
1257
1258    #[test]
1259    fn two_carets_on_one_line_cut_it_once() {
1260        let mut d = doc("aaaa\nbb");
1261        d.set_selections(crate::SelectionSet::from_offsets(&[1, 3])); // both on row 0
1262        assert_eq!(d.clipboard_payload(), ("aaaa\n".to_string(), true), "the line exports once");
1263        d.cut();
1264        assert_eq!(d.text(), "bb", "identical ranges merged into one delete");
1265    }
1266
1267    #[test]
1268    fn entire_line_paste_inserts_above_and_keeps_the_caret() {
1269        // An entire-line paste at an empty caret splices ABOVE the caret's
1270        // line; the caret rebases past the insert and stays on its own line.
1271        let mut d = doc("aaa\nbbb");
1272        d.set_selections(crate::SelectionSet::new(5)); // "bbb", col 1
1273        d.paste("two\n", true);
1274        assert_eq!(d.text(), "aaa\ntwo\nbbb");
1275        assert_eq!(d.selections().newest().head(), 9, "still col 1 of `bbb`");
1276        // Over a non-empty selection the flag is moot — plain replace.
1277        let mut set = crate::SelectionSet::new(0);
1278        set.set_single(crate::Selection::from_anchor(crate::SelectionId(0), 0, 3));
1279        d.set_selections(set);
1280        d.paste("X\n", true);
1281        assert_eq!(d.text(), "X\n\ntwo\nbbb");
1282    }
1283
1284    #[test]
1285    fn enter_after_open_brace_indents_one_unit_deeper() {
1286        // Enter after a line-opening `{` gains one indent unit.
1287        let mut d = doc("    if {");
1288        d.selections = SelectionSet::new(8);
1289        d.enter();
1290        assert_eq!(d.text(), "    if {\n        ");
1291        // Trailing whitespace between `{` and the caret still triggers (the
1292        // rule reads the TRIMMED text left of the caret).
1293        let mut t = doc("m {  ");
1294        t.selections = SelectionSet::new(5);
1295        t.enter();
1296        assert_eq!(t.text(), "m {  \n    ");
1297        // …but a caret not after an opener stays keep-style.
1298        let mut p = doc("m { x");
1299        p.selections = SelectionSet::new(5);
1300        p.enter();
1301        assert_eq!(p.text(), "m { x\n");
1302    }
1303
1304    #[test]
1305    fn enter_between_braces_splits_onto_three_lines() {
1306        // Type `{` (auto-close pairs it), Enter → opener line, indented middle
1307        // line holding the caret, closer dedented on its own.
1308        let mut d = doc("");
1309        d.type_char('{');
1310        assert_eq!(d.text(), "{}");
1311        d.enter();
1312        assert_eq!(d.text(), "{\n    \n}");
1313        assert_eq!(d.selections().newest().head(), 6, "caret at the middle line's end");
1314        // In an indented context the closer keeps the ORIGINAL indent.
1315        let mut n = doc("    {}");
1316        n.selections = SelectionSet::new(5);
1317        n.enter();
1318        assert_eq!(n.text(), "    {\n        \n    }");
1319        assert_eq!(n.selections().newest().head(), 14);
1320        // One undo reverts the whole split (a single transaction).
1321        assert!(n.undo());
1322        assert_eq!(n.text(), "    {}");
1323    }
1324
1325    #[test]
1326    fn enter_indent_unit_matches_tab_kind() {
1327        // A hard-tab-indented line grows by a TAB, not spaces — the indent unit
1328        // matches the kind of the copied leading whitespace.
1329        let mut d = doc("\tif {");
1330        d.selections = SelectionSet::new(5);
1331        d.enter();
1332        assert_eq!(d.text(), "\tif {\n\t\t");
1333    }
1334
1335    #[test]
1336    fn tab_inserts_to_next_stop() {
1337        let mut d = doc("ab");
1338        d.selections = SelectionSet::new(2); // col 2 → next stop at 4, insert 2
1339        d.tab();
1340        assert_eq!(d.text(), "ab  ");
1341        d.tab(); // col 4 → next stop 8, insert 4
1342        assert_eq!(d.text(), "ab      ");
1343    }
1344
1345    #[test]
1346    fn tab_over_a_single_line_selection_types_over() {
1347        let mut d = doc("abcdef");
1348        d.selections.set_single(crate::Selection::from_anchor(crate::SelectionId(0), 1, 4)); // "bcd"
1349        d.tab();
1350        // Single-line selection → replace with spaces to the next stop (from
1351        // col 1 that's 3), NOT a whole-line indent.
1352        assert_eq!(d.text(), "a   ef");
1353        assert!(d.selections().newest().is_empty(), "types over → collapses to a caret");
1354    }
1355
1356    #[test]
1357    fn shift_tab_outdents_a_single_line_and_keeps_the_selection() {
1358        let mut d = doc("    indented");
1359        d.selections.set_single(crate::Selection::from_anchor(crate::SelectionId(0), 5, 9)); // "nden"
1360        d.outdent();
1361        assert_eq!(d.text(), "indented"); // the line outdents (unlike Tab)
1362        // the selection rides the removed indent, still on "nden"
1363        assert_eq!((d.selections().newest().start(), d.selections().newest().end()), (1, 5));
1364    }
1365
1366    #[test]
1367    fn block_indent_outdent_preserves_the_selection() {
1368        let mut d = doc("a\nb\nc");
1369        // Select from line 0 into line 2.
1370        d.selections.set_single(crate::Selection::from_anchor(crate::SelectionId(0), 0, 5));
1371        d.tab();
1372        assert_eq!(d.text(), "    a\n    b\n    c");
1373        // The selection is preserved — one selection over the indented block,
1374        // NOT one caret per line.
1375        assert_eq!(d.selections().len(), 1, "no extra cursors");
1376        let s = d.selections().newest();
1377        assert_eq!((s.start(), s.end()), (0, 17));
1378        assert!(!s.is_empty(), "still a selection, not a caret");
1379        // Outdent restores the text and keeps the selection.
1380        d.outdent();
1381        assert_eq!(d.text(), "a\nb\nc");
1382        assert_eq!(d.selections().len(), 1);
1383        assert_eq!(
1384            (d.selections().newest().start(), d.selections().newest().end()),
1385            (0, 5)
1386        );
1387    }
1388
1389    #[test]
1390    fn multi_cursor_typing_edits_every_caret() {
1391        let mut d = doc("a b c");
1392        d.selections = SelectionSet::new(0);
1393        d.selections.add_caret(2);
1394        d.selections.add_caret(4);
1395        d.type_char('X');
1396        assert_eq!(d.text(), "Xa Xb Xc");
1397        assert_eq!(d.selections().len(), 3);
1398    }
1399
1400    #[test]
1401    fn newline_paste_is_normalized_and_splits_lines() {
1402        let mut d = doc("");
1403        d.insert_text("one\r\ntwo");
1404        assert_eq!(d.text(), "one\ntwo");
1405        assert_eq!(d.buffer().line_count(), 2);
1406    }
1407
1408    #[test]
1409    fn enter_undoes_and_redoes() {
1410        let mut d = doc("ab");
1411        d.selections = SelectionSet::new(1);
1412        d.enter();
1413        assert_eq!(d.text(), "a\nb");
1414        assert!(d.undo());
1415        assert_eq!(d.text(), "ab");
1416        assert!(d.redo());
1417        assert_eq!(d.text(), "a\nb");
1418    }
1419
1420    #[test]
1421    fn caret_tracks_text_after_undo() {
1422        let mut d = doc("");
1423        for ch in "hello".chars() {
1424            d.type_char(ch);
1425        }
1426        d.undo(); // removes the whole run
1427        assert!(d.selections().all()[0].head() <= d.buffer().len(), "caret stays valid");
1428    }
1429
1430    #[test]
1431    fn undo_returns_the_caret_to_the_edit() {
1432        // Undo lands the caret at the START of the reverted region; redo at the END
1433        // of the re-applied one — as if the edit had just (un)happened.
1434        let mut d = doc("");
1435        for ch in "hello".chars() {
1436            d.type_char(ch);
1437        }
1438        assert_eq!(d.selections().all()[0].head(), 5, "caret after typing");
1439        d.undo();
1440        assert_eq!(d.buffer().text(), "", "text undone");
1441        assert_eq!(d.selections().all()[0].head(), 0, "caret returns to the edit site");
1442        d.redo();
1443        assert_eq!(d.buffer().text(), "hello", "text redone");
1444        assert_eq!(d.selections().all()[0].head(), 5, "caret follows the redo");
1445    }
1446
1447    #[test]
1448    fn undo_jumps_the_caret_to_the_edit_from_afar() {
1449        // Edit at the top, then click a caret elsewhere, then undo. The caret
1450        // must JUMP to the reverted edit (so you see what changed), landing at
1451        // the edit site rather than merely rebasing where it was parked.
1452        let mut d = doc("aaa\nbbb\nccc\nddd\n");
1453        d.set_selections(crate::SelectionSet::new(0)); // top of the document
1454        d.type_char('\n'); // an edit at offset 0 (like hitting Enter at the top)
1455        d.set_selections(crate::SelectionSet::new(10)); // click a caret down in the body
1456        assert_eq!(d.selections().all()[0].head(), 10, "caret parked in the body");
1457        assert!(d.undo());
1458        assert_eq!(d.buffer().text(), "aaa\nbbb\nccc\nddd\n", "the newline is gone");
1459        assert_eq!(
1460            d.selections().all()[0].head(),
1461            0,
1462            "undo jumps the caret to the reverted edit, not leaves it at 10"
1463        );
1464    }
1465}