1use core::ops::Range;
13
14use std::borrow::Cow;
15
16use crate::bracket::Brackets;
17use crate::buffer::{Buffer, EolFlavor, LoadError, Revision, Snapshot};
18use crate::change_log::ChangeLog;
19use crate::coords::{Bias, Point};
20use crate::decorations::{
21 DecorationId, DecorationKind, DecorationStore, Diagnostic, DiagnosticsOutcome, Severity,
22 Stickiness, TrackedRange,
23};
24use crate::display_map::{BufferRow, DisplayRow};
25use crate::find::{FindQuery, FindState};
26use std::cell::{Ref, RefCell};
27
28use crate::fold_map::{FoldMap, FoldSet};
29use crate::highlight::{HighlightCache, HighlightEngine, HighlightSpan, SyntaxDef, TokenTheme};
30use crate::history::{GroupingHint, History};
31use crate::movement::{self, ColumnDir, Granularity, Motion};
32use crate::patch::Patch;
33use crate::selection::{Selection, SelectionId, SelectionSet};
34use crate::transaction::{apply, Committed, EditOp, TransactionError};
35
36#[derive(Debug)]
38pub struct Document {
39 pub(crate) buffer: Buffer,
40 history: History,
41 pub(crate) selections: SelectionSet,
42 column: Option<ColumnSelection>,
46 highlight: Option<HighlightCache>,
49 brackets: Brackets,
52 decorations: DecorationStore,
56 autoclose: DecorationStore,
69 folds: FoldSet,
74 find: FindState,
79 line_comment: Option<String>,
83 bracket_string_delims: Vec<u8>,
88 bracket_char_delim: Option<u8>,
89 expand_stack: Vec<SelectionSet>,
93 reveal_seq: u64,
102 reveal_mode: RevealMode,
104 changes: ChangeLog,
111 fold_cache: RefCell<FoldMapCache>,
123}
124
125#[derive(Debug)]
128struct FoldMapCache {
129 key: (u64, u64),
130 map: FoldMap,
131}
132
133#[derive(Copy, Clone, Debug, PartialEq, Eq)]
135pub enum RevealMode {
136 Fit,
141 FitForce,
144 Center,
146}
147
148#[derive(Copy, Clone, Debug)]
156struct ColumnSelection {
157 anchor: CellCorner,
158 active: CellCorner,
159}
160
161#[derive(Copy, Clone, Debug)]
164struct CellCorner {
165 row: u32,
166 cell: u32,
167}
168
169impl Document {
170 pub fn new(text: &str) -> Result<Self, LoadError> {
176 let buffer = Buffer::new(text)?;
177 let brackets = Brackets::match_text(&buffer.text());
178 Ok(Self {
179 buffer,
180 history: History::new(),
181 selections: SelectionSet::new(0),
182 column: None,
183 highlight: None,
184 brackets,
185 decorations: DecorationStore::new(),
186 autoclose: DecorationStore::new(),
187 folds: FoldSet::new(),
188 find: FindState::new(),
189 line_comment: None,
190 bracket_string_delims: Vec::new(),
191 bracket_char_delim: None,
192 expand_stack: Vec::new(),
193 reveal_seq: 0,
194 reveal_mode: RevealMode::Fit,
195 changes: ChangeLog::default(),
196 fold_cache: RefCell::new(FoldMapCache { key: (u64::MAX, u64::MAX), map: FoldMap::empty() }),
198 })
199 }
200
201 #[must_use]
208 pub fn fold_map(&self) -> Ref<'_, FoldMap> {
209 self.ensure_fold_map();
210 Ref::map(self.fold_cache.borrow(), |c| &c.map)
211 }
212
213 fn ensure_fold_map(&self) {
219 let key = (self.buffer.revision().0, self.folds.generation());
220 if self.fold_cache.borrow().key != key {
221 let map = FoldMap::new(&self.folds, &self.brackets, &self.buffer);
222 *self.fold_cache.borrow_mut() = FoldMapCache { key, map };
223 }
224 }
225
226 fn request_reveal(&mut self, mode: RevealMode) {
230 self.reveal_seq += 1;
231 self.reveal_mode = mode;
232 }
233
234 #[must_use]
236 pub fn reveal_mode(&self) -> RevealMode {
237 self.reveal_mode
238 }
239
240 pub fn set_line_comment(&mut self, prefix: Option<&str>) {
244 self.line_comment = prefix.map(str::to_owned);
245 self.rebuild_brackets();
248 }
249
250 pub fn set_bracket_lexing(&mut self, string_delims: Vec<u8>, char_delim: Option<u8>) {
257 self.bracket_string_delims = string_delims;
258 self.bracket_char_delim = char_delim;
259 self.rebuild_brackets();
260 }
261
262 fn bracket_config(&self) -> crate::bracket::BracketConfig {
264 crate::bracket::BracketConfig {
265 line_comments: self.line_comment.iter().cloned().collect(),
266 string_delims: self.bracket_string_delims.clone(),
267 char_delim: self.bracket_char_delim,
268 }
269 }
270
271 fn rebuild_brackets(&mut self) {
274 let cfg = self.bracket_config();
275 self.brackets = crate::bracket::Brackets::match_text_with(&self.buffer.text(), &cfg);
276 }
277
278 #[must_use]
280 pub fn line_comment(&self) -> Option<&str> {
281 self.line_comment.as_deref()
282 }
283
284 #[must_use]
287 pub fn buffer(&self) -> &Buffer {
288 &self.buffer
289 }
290
291 #[must_use]
293 pub fn selections(&self) -> &SelectionSet {
294 &self.selections
295 }
296
297 pub fn set_selections(&mut self, mut selections: SelectionSet) {
304 self.reset_transient();
305 selections.clip_to(&self.buffer);
309 self.selections = selections;
310 }
311
312 pub fn move_carets(&mut self, motion: Motion, extend: bool) {
315 self.reset_transient();
316 self.ensure_fold_map();
321 let tab = self.tab_size();
322 let cache = self.fold_cache.borrow();
323 movement::move_selections(&mut self.selections, &self.buffer, &cache.map, tab, motion, extend);
324 }
325
326 pub fn add_caret(&mut self, offset: u32) {
331 self.reset_transient();
332 self.selections.add_caret(self.buffer.clip_offset(offset, Bias::Left));
333 }
334
335 pub fn add_next_occurrence(&mut self) {
341 self.reset_transient();
342 let newest = *self.selections.newest();
343 if newest.is_empty() {
344 if let Some((ws, we)) = movement::surrounding_word(&self.buffer, newest.head()) {
347 if ws != we {
348 self.selections.add_selection(ws, we);
349 self.request_reveal(RevealMode::FitForce);
353 }
354 }
355 return;
356 }
357 let needle = self.buffer.slice(newest.start()..newest.end()).into_owned();
360 let len = needle.len() as u32;
361 let taken: Vec<(u32, u32)> =
364 self.selections.all().iter().map(|s| (s.start(), s.end())).collect();
365 let is_taken = |s: u32| {
366 taken.binary_search_by_key(&s, |&(st, _)| st).is_ok_and(|i| taken[i].1 == s + len)
367 };
368 let from = newest.end();
371 let found = scan_from(&self.buffer, &needle, from, self.buffer.len(), &is_taken)
372 .or_else(|| scan_from(&self.buffer, &needle, 0, from, &is_taken));
373 if let Some((start, end)) = found {
374 self.selections.add_selection(start, end);
375 self.request_reveal(RevealMode::FitForce);
378 }
379 }
380
381 pub fn jump_to_bracket(&mut self) {
388 self.reset_transient();
389 let targets: Vec<u32> = self
390 .selections
391 .all()
392 .iter()
393 .map(|s| {
394 let head = s.head();
395 if let Some((b, partner)) = self.brackets.active_pair(head) {
396 if head == b + 1 {
397 partner + 1
398 } else {
399 partner
400 }
401 } else if let Some((_, close)) = self.brackets.enclosing_pair(head) {
402 close
403 } else {
404 head
405 }
406 })
407 .collect();
408 let moved =
409 targets.iter().zip(self.selections.all()).any(|(&t, s)| t != s.head() || !s.is_empty());
410 self.selections = SelectionSet::from_offsets(&targets);
411 for &t in &targets {
412 self.unfold_to_reveal(t); }
414 if moved {
415 self.request_reveal(RevealMode::Fit); }
417 }
418
419 pub fn expand_selection(&mut self) {
425 self.reset_gesture();
426 let before: Vec<(u32, u32)> =
427 self.selections.all().iter().map(|s| (s.start(), s.end())).collect();
428 let ranges: Vec<(u32, u32)> =
429 before.iter().map(|&(s, e)| self.expanded_range(s, e)).collect();
430 if ranges == before {
431 return;
432 }
433 let newest_id = self.selections.newest().id;
434 let newest = self
435 .selections
436 .all()
437 .iter()
438 .position(|s| s.id == newest_id)
439 .expect("the newest selection is in its own set");
440 self.expand_stack.push(self.selections.clone());
441 self.selections = SelectionSet::from_ranges(&ranges, newest);
442 self.request_reveal(RevealMode::Fit); }
444
445 pub fn shrink_selection(&mut self) {
448 self.reset_gesture();
449 if let Some(prev) = self.expand_stack.pop() {
450 self.selections = prev;
451 self.request_reveal(RevealMode::Fit);
452 }
453 }
454
455 fn expanded_range(&self, start: u32, end: u32) -> (u32, u32) {
461 if start == end {
462 if let Some((ws, we)) = movement::surrounding_word(&self.buffer, start) {
463 if ws != we {
464 return (ws, we);
465 }
466 }
467 }
468 if let Some((open, close)) = self.brackets.enclosing_pair_of_range(start, end) {
469 let contents = (crate::row_layout::gap_left_edge(open), close);
470 if contents != (start, end) {
471 return contents;
472 }
473 return (open, close + 1);
474 }
475 (0, self.buffer.len())
476 }
477
478 pub fn add_caret_vertical(&mut self, down: bool) {
488 self.reset_transient();
489 let folds = FoldMap::new(&self.folds, &self.brackets, &self.buffer);
490 let tab = self.tab_size();
491 let delta = if down { 1 } else { -1 };
492 let heads: Vec<u32> = self.selections.all().iter().map(Selection::head).collect();
493 let mut added = false;
494 for head in heads {
495 if let Some(off) = movement::caret_one_display_row(&self.buffer, &folds, tab, head, delta)
496 {
497 self.selections.add_caret(off);
498 added = true;
499 }
500 }
501 if added {
502 self.request_reveal(RevealMode::Fit); }
504 }
505
506 pub fn select_all_occurrences(&mut self) {
515 self.reset_transient();
516 let newest = *self.selections.newest();
517 if newest.is_empty() {
518 let Some((ws, we)) = movement::surrounding_word(&self.buffer, newest.head()) else {
519 return;
520 };
521 if ws == we {
522 return;
523 }
524 self.selections.add_selection(ws, we);
525 }
526 let newest = *self.selections.newest();
527 let existing: Vec<(u32, u32)> =
532 self.selections.all().iter().map(|s| (s.start(), s.end())).collect();
533 let taken: std::collections::HashSet<(u32, u32)> = existing.iter().copied().collect();
534 let occurrences: Vec<(u32, u32)> = {
535 let text = self.buffer.text();
536 let needle = &text[newest.start() as usize..newest.end() as usize];
537 if needle.is_empty() {
538 return;
539 }
540 let len = needle.len() as u32;
541 text.match_indices(needle)
542 .map(|(i, _)| (i as u32, i as u32 + len))
543 .filter(|occ| !taken.contains(occ))
544 .collect()
545 };
546 if occurrences.is_empty() {
547 let head = self.selections.newest().head();
549 self.unfold_to_reveal(head);
550 self.request_reveal(RevealMode::Fit);
551 return;
552 }
553 let wrap = occurrences.partition_point(|&(s, _)| s < newest.end());
559 let reveal = (wrap + occurrences.len() - 1) % occurrences.len();
560 let base = existing.len();
561 let mut ranges = existing;
562 ranges.extend_from_slice(&occurrences);
563 self.selections = SelectionSet::from_ranges(&ranges, base + reveal);
564 let head = self.selections.newest().head();
568 self.unfold_to_reveal(head);
569 self.request_reveal(RevealMode::Fit);
570 }
571
572 pub fn select_find_matches(&mut self) -> bool {
578 let ranges: Vec<(u32, u32)> =
579 self.find_matches_in(0..u32::MAX).map(|(r, _)| (r.start, r.end)).collect();
580 if ranges.is_empty() {
581 return false;
582 }
583 let newest = (self.active_find_match().unwrap_or(0) as usize).min(ranges.len() - 1);
584 self.selections = SelectionSet::from_ranges(&ranges, newest);
585 self.reset_transient(); let head = self.selections.newest().head();
589 self.unfold_to_reveal(head);
590 self.request_reveal(RevealMode::Center); true
592 }
593
594 pub fn collapse_selections(&mut self) {
597 self.reset_transient();
598 self.selections.collapse_to_primary();
599 }
600
601 pub fn select_all(&mut self) {
605 self.reset_transient();
606 let end = self.buffer.len();
607 self.selections.set_single(Selection::from_anchor(SelectionId(0), 0, end));
608 }
609
610 fn reset_transient(&mut self) {
619 self.expand_stack.clear();
620 self.reset_gesture();
621 }
622
623 fn reset_gesture(&mut self) {
627 self.column = None;
628 self.clear_autoclose();
629 self.history.seal();
630 }
631
632 pub(crate) fn add_autoclose_pairs(&mut self, pairs: impl IntoIterator<Item = (u32, u32)>) {
640 self.autoclose.add_sorted_batch(
641 pairs.into_iter().map(|(open, close)| open..close),
642 DecorationKind::AutoClosePair,
643 Stickiness::AlwaysGrows,
644 );
645 }
646
647 pub(crate) fn autoclose_ranges(&self) -> Vec<Range<u32>> {
653 self.autoclose.iter().map(|r| r.range.clone()).collect()
654 }
655
656 pub(crate) fn clear_autoclose(&mut self) {
660 if self.autoclose.is_empty() {
661 return; }
663 self.autoclose.take_matching_in(0..u32::MAX, |_| true);
664 }
665
666 pub(crate) fn validate_autoclose(&mut self) {
672 if self.autoclose.is_empty() {
673 return; }
675 let carets: Vec<u32> = self.selections.all().iter().map(|s| s.head()).collect();
676 let Self { autoclose, buffer, .. } = self;
677 autoclose.take_matching_in(0..u32::MAX, |r| {
678 debug_assert!(
679 matches!(r.kind, DecorationKind::AutoClosePair),
680 "the autoclose store holds only AutoClosePair provenance",
681 );
682 let (start, end) = (r.range.start, r.range.end);
683 let one_line = buffer.offset_to_point(start).row == buffer.offset_to_point(end).row;
684 let occupied = one_line && {
686 let i = carets.partition_point(|&h| h <= start);
687 i < carets.len() && carets[i] <= end
688 };
689 !occupied });
691 }
692
693 pub fn drag_select(&mut self, granularity: Granularity, origin: u32, head: u32) {
701 self.reset_transient();
702 let origin = self.buffer.clip_offset(origin, Bias::Left);
705 let head = self.buffer.clip_offset(head, Bias::Left);
706 let (anchor, head) = match granularity {
707 Granularity::Char => (origin, head),
708 Granularity::Word => {
709 extend_by_unit(self.word_unit(origin), self.word_unit(head), origin, head)
710 }
711 Granularity::Line => {
712 extend_by_unit(self.line_unit(origin), self.line_unit(head), origin, head)
713 }
714 };
715 self.selections.set_single(Selection::from_anchor(SelectionId(0), anchor, head));
716 }
717
718 fn word_unit(&self, offset: u32) -> (u32, u32) {
720 movement::surrounding_word(&self.buffer, offset).unwrap_or((offset, offset))
721 }
722
723 pub(crate) fn line_unit(&self, offset: u32) -> (u32, u32) {
727 let row = self.buffer.offset_to_point(offset).row;
728 let start = self.buffer.point_to_offset(Point::new(row, 0));
729 let end = if row + 1 < self.buffer.line_count() {
730 self.buffer.point_to_offset(Point::new(row + 1, 0))
731 } else {
732 self.buffer.point_to_offset(Point::new(row, self.buffer.line_len(row)))
733 };
734 (start, end)
735 }
736
737 pub fn column_select(&mut self, dir: ColumnDir) {
748 self.history.seal();
752 self.expand_stack.clear();
753 let folds = FoldMap::new(&self.folds, &self.brackets, &self.buffer);
754 let tab = self.tab_size();
755 let mut col = self.column.unwrap_or_else(|| {
756 let corner = self.caret_corner(&folds, tab);
757 ColumnSelection { anchor: corner, active: corner }
758 });
759 col.active = Self::step_corner(&folds, col.active, dir);
760 self.column = Some(col);
761 self.rebuild_column_box(&folds, tab, col);
762 }
763
764 pub fn column_drag(&mut self, anchor: (u32, u32), active: (u32, u32)) {
775 self.history.seal();
777 self.expand_stack.clear();
778 let folds = FoldMap::new(&self.folds, &self.brackets, &self.buffer);
779 let last = self.buffer.line_count() - 1;
785 let col = ColumnSelection {
786 anchor: CellCorner { row: anchor.0.min(last), cell: anchor.1 },
787 active: CellCorner { row: active.0.min(last), cell: active.1 },
788 };
789 self.column = Some(col);
790 self.rebuild_column_box(&folds, self.tab_size(), col);
791 }
792
793 fn caret_corner(&self, folds: &FoldMap, tab: u32) -> CellCorner {
800 let head = self.selections.newest().head();
801 match folds.display_position(&self.buffer, head, tab) {
802 Some(p) => CellCorner {
803 row: folds.to_buffer_row(p.row).0,
804 cell: crate::row_layout::virtual_cell(p.x.cells()),
805 },
806 None => {
807 let p = self.buffer.offset_to_point(head);
808 let layout = folds.row_layout(&self.buffer, BufferRow(p.row), tab);
809 CellCorner { row: p.row, cell: layout.display_cell(p.col) }
810 }
811 }
812 }
813
814 fn step_corner(folds: &FoldMap, c: CellCorner, dir: ColumnDir) -> CellCorner {
819 let d = folds.to_display_row(BufferRow(c.row)).index();
820 let row_at = |d: u32| folds.to_buffer_row(DisplayRow(d)).0;
821 match dir {
822 ColumnDir::Up => CellCorner { row: row_at(d.saturating_sub(1)), cell: c.cell },
823 ColumnDir::Down => {
824 CellCorner { row: row_at((d + 1).min(folds.max_display_row().index())), cell: c.cell }
825 }
826 ColumnDir::Left => CellCorner { row: c.row, cell: c.cell.saturating_sub(1) },
827 ColumnDir::Right => CellCorner { row: c.row, cell: c.cell.saturating_add(1) },
829 }
830 }
831
832 fn rebuild_column_box(&mut self, folds: &FoldMap, tab: u32, col: ColumnSelection) {
840 let da = folds.to_display_row(BufferRow(col.anchor.row)).index();
841 let dv = folds.to_display_row(BufferRow(col.active.row)).index();
842 let (d0, d1) = (da.min(dv), da.max(dv));
843 let mut ranges = Vec::with_capacity((d1 - d0 + 1) as usize);
844 let mut newest = 0;
845 for d in d0..=d1 {
846 let row = folds.to_buffer_row(DisplayRow(d));
847 let anchor_off = folds.hit_row(&self.buffer, row, col.anchor.cell as f32, Bias::Left, tab);
848 let head_off = folds.hit_row(&self.buffer, row, col.active.cell as f32, Bias::Left, tab);
849 if d == dv {
850 newest = ranges.len();
851 }
852 ranges.push((anchor_off, head_off));
853 }
854 self.selections = SelectionSet::from_ranges(&ranges, newest);
855 }
856
857 #[must_use]
861 pub fn text(&self) -> Cow<'_, str> {
862 self.buffer.text()
863 }
864
865 #[must_use]
867 pub fn revision(&self) -> Revision {
868 self.buffer.revision()
869 }
870
871 #[must_use]
874 pub fn snapshot(&self) -> Snapshot {
875 self.buffer.snapshot()
876 }
877
878 pub fn edit(&mut self, ops: Vec<EditOp>) -> Result<Committed, TransactionError> {
885 self.edit_grouped(ops, GroupingHint::discrete())
886 }
887
888 pub fn edit_grouped(
895 &mut self,
896 ops: Vec<EditOp>,
897 hint: GroupingHint,
898 ) -> Result<Committed, TransactionError> {
899 self.edit_placing(ops, hint, |_, _| None)
900 }
901
902 pub(crate) fn edit_placing(
925 &mut self,
926 ops: Vec<EditOp>,
927 hint: GroupingHint,
928 place: impl FnOnce(&Patch, &Document) -> Option<SelectionSet>,
929 ) -> Result<Committed, TransactionError> {
930 self.column = None; self.expand_stack.clear(); let sel_before = (!self.history.merges(hint)).then(|| self.selections.clone());
937 let pre_fold_key = (self.buffer.revision().0, self.folds.generation());
940 let committed = apply(&mut self.buffer, ops)?;
941 if !committed.is_empty() {
942 let bracket_edit = committed
947 .forward_ops()
948 .iter()
949 .chain(committed.inverse_ops())
950 .any(|op| op.text.bytes().any(crate::bracket::is_bracket_byte));
951 self.selections.rebase(committed.patch());
954
955 let tab = self.tab_size();
956 let bracket_cfg = self.bracket_config();
959 let region = rebase_views(
965 &mut Views {
966 highlight: &mut self.highlight,
967 brackets: &mut self.brackets,
968 decorations: &mut self.decorations,
969 autoclose: &mut self.autoclose,
970 folds: &mut self.folds,
971 find: &mut self.find,
972 changes: &mut self.changes,
973 },
974 &self.buffer,
975 tab,
976 &committed,
977 &bracket_cfg,
978 );
979
980 self.validate_autoclose();
983 if bracket_edit && !region.is_empty() {
995 self.reconcile_folds_in(region);
996 }
997 {
1006 let mut cache = self.fold_cache.borrow_mut();
1007 if cache.key == pre_fold_key
1008 && self.folds.generation() == pre_fold_key.1
1009 && cache.map.apply_patch(committed.patch(), &self.buffer)
1010 {
1011 cache.key = (self.buffer.revision().0, pre_fold_key.1);
1012 } else {
1013 cache.key = (u64::MAX, u64::MAX);
1014 }
1015 }
1016 if let Some(placed) = place(committed.patch(), self) {
1020 self.selections = placed;
1021 }
1022 let (patch, forward, inverse) = committed.into_ops();
1029 self.history.record(forward, inverse, hint, sel_before, &self.selections);
1030 return Ok(Committed::from_patch(patch));
1031 }
1032 Ok(committed)
1033 }
1034
1035 pub fn observe_changes(&mut self, on: bool) {
1043 self.changes.set_observing(on);
1044 }
1045
1046 #[must_use]
1055 pub fn drain_changes(&mut self) -> Vec<EditOp> {
1056 self.changes.drain()
1057 }
1058
1059 pub fn undo(&mut self) -> bool {
1070 self.reset_transient();
1071 let tab = self.tab_size();
1072 let bracket_cfg = self.bracket_config();
1073 let Self {
1074 history, buffer, highlight, brackets, decorations, autoclose, folds, find, changes, ..
1075 } = self;
1076 let mut views = Views { highlight, brackets, decorations, autoclose, folds, find, changes };
1077 let restored = history.undo(buffer, |committed, buffer| {
1078 rebase_views(&mut views, buffer, tab, committed, &bracket_cfg);
1081 });
1082 let Some(selections) = restored else { return false };
1083 self.reconcile_folds();
1087 self.restore_selections(selections);
1088 true
1089 }
1090
1091 pub fn redo(&mut self) -> bool {
1096 self.reset_transient();
1097 let tab = self.tab_size();
1098 let bracket_cfg = self.bracket_config();
1099 let Self {
1100 history, buffer, highlight, brackets, decorations, autoclose, folds, find, changes, ..
1101 } = self;
1102 let mut views = Views { highlight, brackets, decorations, autoclose, folds, find, changes };
1103 let restored = history.redo(buffer, |committed, buffer| {
1104 rebase_views(&mut views, buffer, tab, committed, &bracket_cfg);
1106 });
1107 let Some(selections) = restored else { return false };
1108 self.reconcile_folds();
1109 self.restore_selections(selections);
1110 true
1111 }
1112
1113 fn restore_selections(&mut self, selections: SelectionSet) {
1121 self.selections = selections;
1122 self.eject_hidden_carets();
1123 }
1124
1125 #[must_use]
1127 pub fn is_dirty(&self) -> bool {
1128 self.history.is_dirty()
1129 }
1130
1131 pub fn mark_saved(&mut self) {
1133 self.history.mark_saved();
1134 }
1135
1136 #[must_use]
1138 pub fn can_undo(&self) -> bool {
1139 self.history.can_undo()
1140 }
1141
1142 #[must_use]
1145 pub fn can_redo(&self) -> bool {
1146 self.history.can_redo()
1147 }
1148
1149 #[must_use]
1154 pub fn redo_branch_count(&self) -> usize {
1155 self.history.redo_branch_count()
1156 }
1157
1158 pub fn select_redo_branch(&mut self, index: usize) -> bool {
1163 self.history.select_redo_branch(index)
1164 }
1165
1166 pub fn set_undo_limit(&mut self, limit: Option<usize>) {
1171 self.history.set_max_undo(limit);
1172 }
1173
1174 #[must_use]
1177 pub fn undo_depth(&self) -> usize {
1178 self.history.undo_depth()
1179 }
1180
1181 #[must_use]
1183 pub fn serialize(&self, flavor: EolFlavor) -> String {
1184 self.buffer.serialize(flavor)
1185 }
1186
1187 pub fn set_syntax(&mut self, syntax: SyntaxDef, theme: TokenTheme) {
1195 let aim = self.highlight.as_ref().map(HighlightCache::window_aim);
1196 let mut cache = HighlightCache::new(syntax, theme, self.buffer.line_count());
1197 if let Some(aim) = aim {
1198 cache.set_window(aim);
1199 }
1200 self.highlight = Some(cache);
1201 }
1202
1203 pub fn set_theme(&mut self, theme: TokenTheme) {
1208 if let Some(cache) = self.highlight.as_mut() {
1209 cache.set_theme(theme);
1210 }
1211 }
1212
1213 #[must_use]
1218 pub fn decorations(&self) -> &DecorationStore {
1219 &self.decorations
1220 }
1221
1222 pub fn decorations_mut(&mut self) -> &mut DecorationStore {
1229 self.decorations.lend_text(&self.buffer);
1232 &mut self.decorations
1233 }
1234
1235 #[cfg(test)]
1238 #[must_use]
1239 pub(crate) fn autoclose_pair_count(&self) -> usize {
1240 self.autoclose.len()
1241 }
1242
1243 #[must_use]
1246 pub fn folds(&self) -> &FoldSet {
1247 &self.folds
1248 }
1249
1250 pub fn folds_mut(&mut self) -> &mut FoldSet {
1254 &mut self.folds
1255 }
1256
1257 pub fn fold(&mut self, header: BufferRow, last: BufferRow) -> bool {
1263 match self.opener_of(header.0, last.0) {
1264 Some(open) => self.folds.fold(open),
1265 None => false,
1266 }
1267 }
1268
1269 pub fn unfold(&mut self, header: BufferRow) -> bool {
1272 match self.folded_opener_on(header.0) {
1273 Some(open) => self.folds.unfold(open),
1274 None => false,
1275 }
1276 }
1277
1278 pub fn toggle_fold(&mut self, header: BufferRow, last: BufferRow) -> bool {
1281 match self.folded_opener_on(header.0) {
1282 Some(open) => self.folds.unfold(open),
1283 None => self.fold(header, last),
1284 }
1285 }
1286
1287 fn folded_opener_on(&self, header: u32) -> Option<u32> {
1292 let start = self.buffer.point_to_offset(Point::new(header, 0));
1293 let end = if header + 1 >= self.buffer.line_count() {
1294 self.buffer.len() + 1 } else {
1296 self.buffer.point_to_offset(Point::new(header + 1, 0))
1297 };
1298 self.folds.first_at_or_after(start).filter(|&o| o < end)
1299 }
1300
1301 fn opener_of(&self, header: u32, last: u32) -> Option<u32> {
1304 self.foldable_pairs_in_rows(header..header + 1)
1306 .into_iter()
1307 .find(|&(_, _, h, l)| h == header && l == last)
1308 .map(|(o, ..)| o)
1309 }
1310
1311 fn foldable_pairs(&self) -> Vec<(u32, u32, u32, u32)> {
1316 self.foldable_pairs_from(&self.brackets.all())
1317 }
1318
1319 pub fn foldable_pairs_in_rows(&self, rows: Range<u32>) -> Vec<(u32, u32, u32, u32)> {
1338 let start = self.buffer.point_to_offset(Point::new(rows.start, 0));
1339 let end = if rows.end >= self.buffer.line_count() {
1340 self.buffer.len() + 1 } else {
1342 self.buffer.point_to_offset(Point::new(rows.end, 0))
1343 };
1344 self.foldable_pairs_from(&self.brackets.in_range(start..end))
1345 }
1346
1347 fn foldable_pairs_from(&self, brackets: &[crate::bracket::Bracket]) -> Vec<(u32, u32, u32, u32)> {
1349 brackets
1350 .iter()
1351 .filter_map(|b| {
1352 let close = b.partner.filter(|_| b.open)?;
1353 crate::row_layout::pair_has_interior(b.offset, close).then(|| {
1355 let header = self.buffer.offset_to_point(b.offset).row;
1356 let last = self.buffer.offset_to_point(close).row;
1357 (b.offset, close, header, last)
1358 })
1359 })
1360 .collect()
1361 }
1362
1363 pub fn toggle_fold_opener(&mut self, opener: u32) -> bool {
1372 let opener = self.buffer.clip_offset(opener, Bias::Left);
1375 let changed = self.folds.toggle(opener);
1376 if changed {
1377 self.reset_transient();
1383 }
1384 if changed && self.folds.is_folded(opener) {
1385 self.eject_hidden_carets();
1393 }
1394 changed
1395 }
1396
1397 #[must_use]
1403 pub fn tab_size(&self) -> u32 {
1404 crate::display_map::default_tab_size()
1405 }
1406
1407 #[must_use]
1410 pub fn block_opener_on_row(&self, row: u32) -> Option<u32> {
1411 self.foldable_pairs_in_rows(row..row + 1)
1415 .into_iter()
1416 .filter(|&(_, _, h, l)| h == row && l > h)
1417 .max_by_key(|&(_, _, h, l)| l - h)
1418 .map(|(o, ..)| o)
1419 }
1420
1421 #[must_use]
1429 pub fn fold_opener_at_caret(&self, want_folded: bool) -> Option<u32> {
1430 self.fold_opener_at(self.selections.newest().head(), want_folded)
1431 }
1432
1433 fn fold_opener_at(&self, caret: u32, want_folded: bool) -> Option<u32> {
1440 let is_target = |open: u32, close: u32| {
1441 crate::row_layout::pair_has_interior(open, close)
1442 && self.folds.is_folded(open) == want_folded
1443 };
1444 let touch_left = self.brackets.foldable_partner(caret).map(|close| (caret, close));
1448 let touch_right = caret
1449 .checked_sub(1)
1450 .and_then(|o| self.brackets.at(o))
1451 .filter(|b| !b.open)
1452 .and_then(|b| b.partner.map(|open| (open, b.offset)));
1453 if let Some((open, _)) = [touch_left, touch_right]
1454 .into_iter()
1455 .flatten()
1456 .filter(|&(o, c)| is_target(o, c))
1457 .min_by_key(|&(o, c)| c - o)
1458 {
1459 return Some(open);
1460 }
1461 self.brackets.innermost_enclosing_where(caret, is_target)
1463 }
1464
1465 pub fn fold_at_carets(&mut self, unfold: bool) -> bool {
1472 let mut openers: Vec<u32> = self
1473 .selections
1474 .all()
1475 .iter()
1476 .filter_map(|s| self.fold_opener_at(s.head(), unfold))
1477 .collect();
1478 openers.sort_unstable();
1479 openers.dedup();
1480 if openers.is_empty() {
1481 return false;
1482 }
1483 self.reset_transient();
1490 if unfold {
1491 self.folds.unfold_all(&openers);
1492 } else {
1493 self.folds.fold_all(openers.iter().copied());
1494 }
1495 self.eject_hidden_carets();
1496 true
1497 }
1498
1499 fn eject_hidden_carets(&mut self) {
1508 if self.folds.is_empty() {
1509 return;
1510 }
1511 self.ensure_fold_map();
1512 let Self { selections, buffer, fold_cache, .. } = self;
1513 let cache = fold_cache.borrow();
1514 selections.map_each(|s| {
1515 if let Some(entry) = cache.map.entry_edge_if_hidden(buffer, s.head()) {
1516 s.move_to_caret(entry);
1517 }
1518 });
1519 }
1520
1521 #[must_use]
1527 pub fn collapsible_pairs(&self) -> Vec<(u32, u32, u32, u32)> {
1528 self.foldable_pairs().into_iter().filter(|&(open, ..)| !self.folds.is_folded(open)).collect()
1529 }
1530
1531 #[must_use]
1539 pub fn collapsible_pairs_in_rows(&self, rows: Range<u32>) -> Vec<(u32, u32, u32, u32)> {
1540 self.foldable_pairs_in_rows(rows)
1541 .into_iter()
1542 .filter(|&(open, ..)| !self.folds.is_folded(open))
1543 .collect()
1544 }
1545
1546 #[must_use]
1552 pub fn collapsible_at(&self, offset: u32) -> Option<u32> {
1553 self.collapsible_pairs()
1554 .into_iter()
1555 .filter(|&(open, close, _, _)| open < offset && offset < close)
1556 .min_by_key(|&(open, close, _, _)| close - open)
1557 .map(|(o, ..)| o)
1558 }
1559
1560 fn reconcile_folds(&mut self) {
1567 if self.folds.is_empty() {
1568 return;
1569 }
1570 let brackets = &self.brackets;
1576 self.folds.reconcile(|o| {
1577 brackets
1578 .foldable_partner(o)
1579 .is_some_and(|close| crate::row_layout::pair_has_interior(o, close))
1580 });
1581 }
1582
1583 #[cfg(test)]
1586 pub(crate) fn force_whole_reconcile(&mut self) {
1587 self.reconcile_folds();
1588 }
1589
1590 fn reconcile_folds_in(&mut self, region: core::ops::Range<u32>) {
1601 if self.folds.is_empty() {
1602 return;
1603 }
1604 let candidates: Vec<u32> = self.folds.openers_in(region);
1605 let brackets = &self.brackets;
1606 self.folds.reconcile_only(&candidates, |o| {
1607 brackets
1608 .foldable_partner(o)
1609 .is_some_and(|close| crate::row_layout::pair_has_interior(o, close))
1610 });
1611 }
1612
1613 #[must_use]
1618 pub fn foldable_ranges(&self) -> Vec<(BufferRow, BufferRow)> {
1619 let mut ranges: Vec<(BufferRow, BufferRow)> = self
1620 .brackets
1621 .all()
1622 .iter()
1623 .filter_map(|br| {
1624 let close = br.partner.filter(|_| br.open)?;
1625 let header = self.buffer.offset_to_point(br.offset).row;
1626 let last = self.buffer.offset_to_point(close).row;
1627 (last > header).then_some((BufferRow(header), BufferRow(last)))
1628 })
1629 .collect();
1630 ranges.sort_by_key(|r| (r.0, core::cmp::Reverse(r.1)));
1632 ranges.dedup_by_key(|r| r.0);
1633 ranges
1634 }
1635
1636 #[must_use]
1643 pub fn foldable_ranges_in_rows(&self, rows: Range<u32>) -> Vec<(BufferRow, BufferRow)> {
1644 let start = self.buffer.point_to_offset(Point::new(rows.start, 0));
1645 let end = if rows.end >= self.buffer.line_count() {
1646 self.buffer.len() + 1 } else {
1648 self.buffer.point_to_offset(Point::new(rows.end, 0))
1649 };
1650 let mut ranges: Vec<(BufferRow, BufferRow)> = self
1651 .brackets
1652 .in_range_iter(start..end)
1653 .filter_map(|br| {
1654 let close = br.partner.filter(|_| br.open)?;
1655 let header = self.buffer.offset_to_point(br.offset).row;
1656 let last = self.buffer.offset_to_point(close).row;
1657 (last > header).then_some((BufferRow(header), BufferRow(last)))
1658 })
1659 .collect();
1660 ranges.sort_by_key(|r| (r.0, core::cmp::Reverse(r.1)));
1661 ranges.dedup_by_key(|r| r.0);
1662 ranges
1663 }
1664
1665 pub fn tokenize_highlight(&mut self, target: u32) {
1669 let buffer = &self.buffer;
1674 let Some(cache) = self.highlight.as_mut() else { return };
1675 cache.tokenize_until(target, crate::highlight::HIGHLIGHT_MAX_LINES_PER_CALL, |r| {
1676 buffer.line(r)
1677 });
1678 }
1679
1680 #[must_use]
1687 pub fn highlight_frontier(&self) -> Option<u32> {
1688 self.highlight.as_ref().and_then(HighlightCache::pending)
1689 }
1690
1691 pub fn set_highlight_window(&mut self, rows: Range<u32>) {
1699 if let Some(cache) = self.highlight.as_mut() {
1700 cache.set_window(rows);
1701 }
1702 }
1703
1704 #[must_use]
1707 pub fn highlight_line_spans(&self, row: u32) -> Option<&[HighlightSpan]> {
1708 self.highlight.as_ref().and_then(|c| c.line_spans(row))
1709 }
1710
1711 #[must_use]
1717 pub fn highlight_engine(&self) -> Option<HighlightEngine> {
1718 self.highlight.as_ref().map(HighlightCache::engine)
1719 }
1720
1721 pub fn absorb_highlight(
1751 &mut self,
1752 revision: Revision,
1753 seg: crate::SegmentTokens,
1754 verified: bool,
1755 ) -> bool {
1756 if revision != self.buffer.revision() {
1757 return false;
1758 }
1759 let Some(cache) = self.highlight.as_mut() else { return false };
1760 cache.absorb(seg, verified)
1761 }
1762
1763 #[must_use]
1766 pub fn brackets(&self) -> &Brackets {
1767 &self.brackets
1768 }
1769
1770 pub fn set_diagnostics(&mut self, revision: Revision, diags: Vec<Diagnostic>) -> DiagnosticsOutcome {
1783 let current = self.buffer.revision().0;
1784 self.decorations_mut().set_diagnostics(revision.0, current, diags)
1789 }
1790
1791 pub fn diagnostics_in(
1795 &self,
1796 range: Range<u32>,
1797 ) -> impl Iterator<Item = (Range<u32>, Severity, std::sync::Arc<str>)> + '_ {
1798 self.decorations.decorations_in(range).filter_map(|r| {
1802 let TrackedRange { range, kind, .. } = r;
1803 match kind {
1804 DecorationKind::Diagnostic { severity, message, .. } => {
1805 Some((range, severity, message))
1806 }
1807 _ => None,
1808 }
1809 })
1810 }
1811
1812 pub fn set_find_query(&mut self, query: Option<FindQuery>, now_ms: u64) {
1820 self.find.set_query(query, &self.buffer, &mut self.decorations, now_ms);
1821 }
1822
1823 #[must_use]
1825 pub fn find_query(&self) -> Option<&FindQuery> {
1826 self.find.query()
1827 }
1828
1829 pub fn set_find_scope(&mut self, scope: Option<Range<u32>>, now_ms: u64) {
1841 let scope = scope.map(|r| self.buffer.clip_range(r));
1842 self.find.set_scope(scope, &self.buffer, &mut self.decorations, now_ms);
1843 }
1844
1845 #[must_use]
1847 pub fn find_scope(&self) -> Option<Range<u32>> {
1848 self.find.scope()
1849 }
1850
1851 #[must_use]
1858 pub fn find_pattern_error(&self) -> Option<&str> {
1859 self.find.pattern_error()
1860 }
1861
1862 #[must_use]
1875 pub fn caret_word_occurrences(&self, within: Range<u32>) -> Vec<Range<u32>> {
1876 let newest = self.selections.newest();
1877 if !newest.is_empty() {
1878 return Vec::new();
1879 }
1880 let Some((ws, we)) = movement::surrounding_word(&self.buffer, newest.head()) else {
1881 return Vec::new();
1882 };
1883 if ws == we {
1884 return Vec::new();
1885 }
1886 let word = self.buffer.slice(ws..we);
1887 let Range { start: base, end } = self.buffer.clip_range(within);
1888 let hay = self.buffer.slice(base..end);
1889 let mut out = Vec::new();
1890 let mut from = 0usize;
1891 while let Some(i) = hay[from..].find(&*word) {
1892 let start = base + (from + i) as u32;
1893 let match_end = start + word.len() as u32;
1894 if movement::surrounding_word(&self.buffer, start) == Some((start, match_end)) {
1896 out.push(start..match_end);
1897 }
1898 from = from + i + word.len();
1899 }
1900 out
1901 }
1902
1903 #[must_use]
1909 pub fn find_match_count(&self) -> usize {
1910 self.decorations.find_count()
1911 }
1912
1913 #[must_use]
1919 pub fn active_find_match(&self) -> Option<u32> {
1920 self.find.active_start().map(|s| self.decorations.find_rank_before(s) as u32)
1921 }
1922
1923 pub fn find_matches_in(&self, range: Range<u32>) -> impl Iterator<Item = (Range<u32>, bool)> + '_ {
1927 let active = self.find.active_id();
1928 self.decorations.decorations_in(range).filter_map(move |r| match &r.kind {
1929 DecorationKind::FindMatch if r.range.start < r.range.end => {
1930 Some((r.range.clone(), Some(r.id) == active))
1931 }
1932 _ => None,
1933 })
1934 }
1935
1936 pub fn overview_marks(
1947 &self,
1948 bounds: &[u32],
1949 sev_out: &mut Vec<(u8, u32)>,
1950 find_out: &mut Vec<Option<u32>>,
1951 ) {
1952 self.decorations.diagnostic_overview(bounds, sev_out);
1953 self.decorations.find_overview(bounds, find_out);
1954 }
1955
1956 pub fn maybe_rescan_find(&mut self, now_ms: u64) -> bool {
1964 self.find.maybe_rescan(&self.buffer, &mut self.decorations, now_ms)
1965 }
1966
1967 pub fn find_next(&mut self, now_ms: u64) -> Option<Range<u32>> {
1971 self.step_find(true, now_ms)
1972 }
1973
1974 pub fn find_prev(&mut self, now_ms: u64) -> Option<Range<u32>> {
1977 self.step_find(false, now_ms)
1978 }
1979
1980 pub fn replace_next(
1995 &mut self,
1996 replacement: &str,
1997 preserve_case: bool,
1998 now_ms: u64,
1999 ) -> Option<Range<u32>> {
2000 self.find.maybe_rescan(&self.buffer, &mut self.decorations, now_ms);
2001 let extent = {
2002 let sel = self.selections.newest();
2003 sel.start()..sel.end()
2004 };
2005 let armed = !extent.is_empty()
2009 && self.find.active_range(&self.decorations).is_some_and(|r| r == extent);
2010 if armed {
2011 let text = self
2014 .find
2015 .replacements(&self.buffer, extent.clone(), replacement, preserve_case)
2016 .into_iter()
2017 .next()
2018 .map_or_else(|| replacement.to_string(), |(_, t)| t);
2019 let mut picked = None; let ops = vec![EditOp::new(extent, text)];
2028 let committed = self.edit_placing(ops, GroupingHint::discrete(), |_, doc| {
2029 let sel = doc.selections.newest();
2030 let target = FindState::pick(sel.head(), sel.start()..sel.end(), &doc.decorations, true);
2031 let placed = target.as_ref().map(|(_, range)| {
2032 let mut set = doc.selections.clone();
2033 select_match(&mut set, range);
2034 set
2035 });
2036 picked = target;
2037 placed
2038 });
2039 match committed {
2040 Err(_) => return None,
2042 Ok(c) if !c.is_empty() => return self.land_on_match(picked),
2045 Ok(_) => {}
2049 }
2050 }
2051 self.step_find(true, now_ms)
2052 }
2053
2054 pub fn replace_all(&mut self, replacement: &str, preserve_case: bool) -> usize {
2080 let within = self.find.scope().unwrap_or(0..self.buffer.len());
2083 let plan = self.find.replacements(&self.buffer, within, replacement, preserve_case);
2084 if plan.is_empty() {
2085 return 0;
2088 }
2089 let n = plan.len();
2090 let ops = plan.into_iter().map(|(r, t)| EditOp::new(r, t)).collect();
2091 if self.edit(ops).is_err() {
2092 return 0;
2093 }
2094 n
2095 }
2096
2097 fn step_find(&mut self, forward: bool, now_ms: u64) -> Option<Range<u32>> {
2099 self.find.maybe_rescan(&self.buffer, &mut self.decorations, now_ms);
2103 let sel = self.selections.newest();
2104 let target = FindState::pick(sel.head(), sel.start()..sel.end(), &self.decorations, forward);
2105 self.land_on_match(target)
2106 }
2107
2108 fn land_on_match(&mut self, target: Option<(DecorationId, Range<u32>)>) -> Option<Range<u32>> {
2113 let range = self.find.activate(target)?;
2114 select_match(&mut self.selections, &range);
2117 self.reset_transient(); self.unfold_to_reveal(range.start);
2121 self.unfold_to_reveal(range.end);
2122 self.request_reveal(RevealMode::Center); Some(range)
2124 }
2125
2126 pub fn next_diagnostic(&mut self, forward: bool) -> Option<Range<u32>> {
2133 let sel = self.selections.newest();
2137 let anchor = (sel.start(), sel.end());
2138 let mut all: Vec<Range<u32>> = self.diagnostics_in(0..u32::MAX).map(|(r, ..)| r).collect();
2139 all.sort_by_key(|r| (r.start, r.end));
2140 let target = if forward {
2141 all.iter().find(|r| (r.start, r.end) > anchor).or_else(|| all.first())
2142 } else {
2143 all.iter().rev().find(|r| (r.start, r.end) < anchor).or_else(|| all.last())
2144 }?
2145 .clone();
2146 self.selections
2147 .set_single(Selection::from_anchor(SelectionId(0), target.start, target.end));
2148 self.reset_transient(); self.unfold_to_reveal(target.start);
2150 self.unfold_to_reveal(target.end);
2151 self.request_reveal(RevealMode::Center); Some(target)
2153 }
2154
2155 fn unfold_to_reveal(&mut self, offset: u32) -> bool {
2162 let tab = self.tab_size();
2163 let mut any = false;
2164 loop {
2165 let fm = FoldMap::new(&self.folds, &self.brackets, &self.buffer);
2166 let renders = fm.display_position(&self.buffer, offset, tab).is_some();
2172 let chip_hidden = fm.inline_fold_before(offset).is_some_and(|f| f.hides_caret_at(offset));
2175 if renders && !chip_hidden {
2176 return any;
2177 }
2178 let target = self
2181 .folds
2182 .iter()
2183 .filter_map(|open| self.brackets.foldable_partner(open).map(|c| (open, c)))
2184 .filter(|&(open, close)| open < offset && offset < close)
2185 .min_by_key(|&(open, close)| close - open);
2186 let Some((open, _)) = target else {
2187 return any; };
2189 self.folds.unfold(open);
2190 any = true;
2191 }
2192 }
2193
2194 #[must_use]
2198 pub fn reveal_seq(&self) -> u64 {
2199 self.reveal_seq
2200 }
2201}
2202
2203fn expand_folds_touched(
2212 folds: &mut FoldSet,
2213 brackets: &Brackets,
2214 buffer: &Buffer,
2215 tab: u32,
2216 committed: &Committed,
2217) {
2218 if folds.is_empty() || committed.patch().is_empty() {
2219 return;
2220 }
2221 let mut pts: Vec<u32> = Vec::with_capacity(committed.patch().edits().len() * 2);
2234 for e in committed.patch().edits() {
2235 pts.push(e.new.start);
2236 pts.push(e.new.end);
2237 }
2238 pts.sort_unstable();
2239 pts.dedup();
2240 let (first, last) = (pts[0], *pts.last().expect("edits() is non-empty here"));
2241 let lo = brackets
2242 .enclosing_or_touching(first)
2243 .into_iter()
2244 .filter(|&(o, _)| folds.is_folded(o))
2245 .map(|(o, _)| o)
2246 .min()
2247 .unwrap_or(first);
2248 let candidates: Vec<(u32, u32)> = folds
2252 .openers_in(lo..last.saturating_add(1))
2253 .iter()
2254 .filter_map(|&o| {
2255 let c = brackets.foldable_partner(o)?;
2256 let touches =
2257 pts.partition_point(|&p| p < o) < pts.partition_point(|&p| p <= c.saturating_add(1));
2258 touches.then_some((o, c))
2259 })
2260 .collect();
2261 if candidates.is_empty() {
2262 return;
2263 }
2264 let mut sub = crate::fold_map::FoldSet::new();
2265 for &(open, _) in &candidates {
2266 sub.fold(open);
2267 }
2268 let fold_map = crate::fold_map::FoldMap::new(&sub, brackets, buffer);
2269 let any_hidden = pts.iter().any(|&p| fold_map.display_position(buffer, p, tab).is_none());
2275 let mut starts: Vec<u32> = committed.patch().edits().iter().map(|e| e.new.start).collect();
2278 starts.sort_unstable();
2279 let touched: Vec<u32> = candidates
2280 .iter()
2281 .filter(|&&(opener, close)| {
2282 let single = buffer.offset_to_point(opener).row == buffer.offset_to_point(close).row;
2283 if single {
2284 starts.partition_point(|&s| s <= close) > starts.partition_point(|&s| s <= opener)
2287 } else {
2288 any_hidden
2292 }
2293 })
2294 .map(|&(opener, _)| opener)
2295 .collect();
2296 for opener in touched {
2297 folds.unfold(opener);
2298 }
2299}
2300
2301struct Views<'a> {
2307 highlight: &'a mut Option<HighlightCache>,
2308 brackets: &'a mut Brackets,
2309 decorations: &'a mut DecorationStore,
2310 autoclose: &'a mut DecorationStore,
2314 folds: &'a mut FoldSet,
2315 find: &'a mut FindState,
2316 changes: &'a mut ChangeLog,
2321}
2322
2323fn rebase_views(
2334 views: &mut Views,
2335 buffer: &Buffer,
2336 tab: u32,
2337 committed: &Committed,
2338 bracket_cfg: &crate::bracket::BracketConfig,
2339) -> core::ops::Range<u32> {
2340 if let Some(cache) = views.highlight.as_mut() {
2346 let edits = committed.patch().edits();
2347 if !edits.is_empty() {
2348 let mut spans: Vec<(u32, u32, u32)> = Vec::with_capacity(edits.len());
2356 let mut acc: i64 = 0;
2357 for (e, inv) in edits.iter().zip(committed.inverse_ops()) {
2358 let post_sr = buffer.offset_to_point(e.new.start).row;
2359 let post_er = buffer.offset_to_point(e.new.end).row;
2360 let new_lines = post_er - post_sr + 1;
2361 let old_lines = inv.text.bytes().filter(|&b| b == b'\n').count() as u32 + 1;
2362 let pre_start = (i64::from(post_sr) - acc) as u32;
2363 acc += i64::from(new_lines) - i64::from(old_lines);
2364 match spans.last_mut() {
2365 Some(last) if pre_start < last.0 + last.1 => {
2368 let merged_end = (last.0 + last.1).max(pre_start + old_lines);
2369 let combined_delta = (i64::from(last.2) - i64::from(last.1))
2370 + (i64::from(new_lines) - i64::from(old_lines));
2371 last.1 = merged_end - last.0;
2372 last.2 = (i64::from(last.1) + combined_delta) as u32;
2373 }
2374 _ => spans.push((pre_start, old_lines, new_lines)),
2375 }
2376 }
2377 debug_assert_eq!(
2382 spans.iter().map(|&(_, o, n)| i64::from(n) - i64::from(o)).sum::<i64>(),
2383 i64::from(buffer.line_count()) - i64::from(cache.line_count()),
2384 "per-edit line deltas must sum to the buffer's line-count change",
2385 );
2386 cache.on_commit_patch(&spans);
2387 }
2388 }
2389 let reconcile_region = views.brackets.apply_edit(committed.patch(), buffer, bracket_cfg);
2394 views.decorations.apply_patch(committed.patch());
2398 views.autoclose.apply_patch(committed.patch());
2405 views.find.on_commit(committed.patch(), buffer, views.decorations);
2410 views.folds.apply_patch(committed.patch());
2415 expand_folds_touched(views.folds, views.brackets, buffer, tab, committed);
2423 views.changes.record(committed);
2426 reconcile_region
2430}
2431
2432fn extend_by_unit(origin_unit: (u32, u32), head_unit: (u32, u32), origin: u32, head: u32) -> (u32, u32) {
2437 let (os, oe) = origin_unit;
2438 let (hs, he) = head_unit;
2439 if head >= origin {
2440 (os, he.max(oe))
2441 } else {
2442 (oe, hs.min(os))
2443 }
2444}
2445
2446fn select_match(set: &mut SelectionSet, range: &Range<u32>) {
2450 set.set_single(Selection::from_anchor(SelectionId(0), range.start, range.end));
2451}
2452
2453fn scan_from(
2464 buffer: &Buffer,
2465 needle: &str,
2466 lo: u32,
2467 hi: u32,
2468 is_taken: &impl Fn(u32) -> bool,
2469) -> Option<(u32, u32)> {
2470 let k = needle.len() as u64;
2471 if k == 0 || lo >= hi {
2472 return None;
2473 }
2474 let len = buffer.len();
2475 let window = u64::from(crate::buffer::SCAN_WINDOW).max(k * 2); let mut pos = lo;
2477 while u64::from(pos) + k <= u64::from(len) {
2478 let win_end = buffer
2479 .clip_offset((u64::from(pos) + window).min(u64::from(len)) as u32, Bias::Right);
2480 let slice = buffer.slice(pos..win_end);
2481 let mut last_end: Option<u32> = None;
2482 for (i, _) in slice.match_indices(needle) {
2483 let start = pos + i as u32;
2484 if u64::from(start) >= u64::from(hi) {
2485 return None; }
2487 last_end = Some(start + k as u32);
2488 if !is_taken(start) {
2489 return Some((start, start + k as u32));
2490 }
2491 }
2492 if win_end >= len {
2493 break;
2494 }
2495 pos = buffer.scan_resume(pos, win_end, k as u32, last_end);
2498 }
2499 None
2500}
2501
2502#[cfg(test)]
2509fn find_next_occurrence(text: &str, needle: &str, from: u32, taken: &[(u32, u32)]) -> Option<(u32, u32)> {
2510 if needle.is_empty() {
2511 return None;
2512 }
2513 let len = needle.len() as u32;
2514 let is_taken = |start: u32| taken.iter().any(|&(s, e)| s == start && e == start + len);
2515 let mut wrapped: Option<u32> = None;
2516 for (idx, _) in text.match_indices(needle) {
2517 let start = idx as u32;
2518 if is_taken(start) {
2519 continue;
2520 }
2521 if start >= from {
2522 return Some((start, start + len));
2523 }
2524 if wrapped.is_none() {
2525 wrapped = Some(start); }
2527 }
2528 wrapped.map(|start| (start, start + len))
2529}
2530
2531#[cfg(test)]
2532mod tests {
2533 use super::*;
2534 use crate::history::{GroupingHint, OpClass};
2535
2536 fn doc(s: &str) -> Document {
2537 Document::new(s).unwrap()
2538 }
2539
2540 fn typ(op: OpClass) -> GroupingHint {
2541 GroupingHint::mergeable(op)
2542 }
2543
2544 fn assert_brackets_fresh(d: &Document) {
2547 let fresh = crate::bracket::Brackets::match_text(&d.text());
2548 assert_eq!(d.brackets().all(), fresh.all(), "incremental brackets diverged from scratch");
2549 }
2550
2551 #[test]
2552 fn brackets_ride_edits_and_undo_redo_through_the_one_mover() {
2553 let mut d = doc("fn a() {\n x[0] = (1 + 2);\n}\n");
2559 let mut rng = 0xC0FFEEu64;
2560 let mut next = move |m: u64| {
2561 rng ^= rng << 13;
2562 rng ^= rng >> 7;
2563 rng ^= rng << 17;
2564 rng % m
2565 };
2566 let inserts = ["{", "}", "(", ")", "[", "]", "\n", "x", "{}", "([)", "\n}\n"];
2567 for i in 0..200 {
2568 match next(4) {
2569 0 | 1 => {
2570 let at = next(d.buffer().len() as u64 + 1) as u32;
2571 let at = d.buffer().clip_offset(at, crate::Bias::Left);
2572 let text = inserts[next(inserts.len() as u64) as usize];
2573 let _ = d.edit(vec![EditOp::insert(at, text)]);
2574 }
2575 2 => {
2576 let a = next(d.buffer().len() as u64 + 1) as u32;
2577 let a = d.buffer().clip_offset(a, crate::Bias::Left);
2578 let b = (a + 1 + next(4) as u32).min(d.buffer().len());
2579 let b = d.buffer().clip_offset(b, crate::Bias::Right);
2580 if a < b {
2581 let _ = d.edit(vec![EditOp::delete(a..b)]);
2582 }
2583 }
2584 _ => {
2585 if next(2) == 0 {
2586 d.undo();
2587 } else {
2588 d.redo();
2589 }
2590 }
2591 }
2592 assert_brackets_fresh(&d);
2593 let _ = i;
2594 }
2595 }
2596
2597 #[test]
2598 fn edit_undo_redo_round_trip() {
2599 let mut d = doc("hello");
2600 d.edit(vec![EditOp::new(0..5, "world")]).unwrap();
2601 assert_eq!(d.text(), "world");
2602 assert!(d.undo());
2603 assert_eq!(d.text(), "hello");
2604 assert!(d.redo());
2605 assert_eq!(d.text(), "world");
2606 assert!(d.undo());
2608 assert!(!d.undo()); assert_eq!(d.text(), "hello");
2610 }
2611
2612 #[test]
2613 fn programmatic_batch_puts_caret_after_all_text_inserted_at_it() {
2614 let mut d = doc("abcd");
2619 d.set_selections(SelectionSet::from_offsets(&[2]));
2620 d.edit(vec![EditOp::new(1..2, "X"), EditOp::insert(2, "Y")]).unwrap();
2621 assert_eq!(d.text(), "aXYcd");
2622 assert_eq!(d.selections().newest().head(), 3, "caret after X and Y");
2623 }
2624
2625 #[test]
2626 fn a_new_edit_invalidates_redo() {
2627 let mut d = doc("");
2628 d.edit(vec![EditOp::insert(0, "a")]).unwrap();
2629 d.edit(vec![EditOp::insert(1, "b")]).unwrap(); d.undo(); d.edit(vec![EditOp::insert(1, "X")]).unwrap(); assert_eq!(d.text(), "aX");
2633 assert!(!d.redo(), "redo must be cleared by the divergent edit");
2634 assert_eq!(d.text(), "aX");
2635 }
2636
2637 #[test]
2638 fn a_retained_branch_is_reachable_from_the_fork() {
2639 let mut d = doc("");
2646 d.edit(vec![EditOp::insert(0, "a")]).unwrap();
2647 d.edit(vec![EditOp::insert(1, "b")]).unwrap(); d.undo(); d.edit(vec![EditOp::insert(1, "X")]).unwrap(); assert!(!d.redo(), "plain redo does nothing at the tip of the new branch");
2651 assert!(d.undo());
2653 assert_eq!(d.text(), "a");
2654 assert_eq!(d.redo_branch_count(), 2, "the 'b' and 'X' branches both survive");
2655 assert!(d.can_redo());
2656 assert!(d.select_redo_branch(0));
2658 assert!(d.redo());
2659 assert_eq!(d.text(), "ab", "the retained 'b' branch is fully reachable");
2660 assert!(d.undo());
2662 assert!(d.select_redo_branch(1));
2663 assert!(d.redo());
2664 assert_eq!(d.text(), "aX");
2665 }
2666
2667 #[test]
2668 fn opt_in_undo_limit_bounds_reach_and_keeps_recent() {
2669 let mut d = doc("");
2673 d.set_undo_limit(Some(3));
2674 for (i, ch) in "abcde".chars().enumerate() {
2675 d.edit(vec![EditOp::insert(i as u32, ch.to_string())]).unwrap();
2676 }
2677 assert_eq!(d.text(), "abcde");
2678 assert_eq!(d.undo_depth(), 3, "only the last 3 units are retained");
2679 assert!(d.undo()); assert!(d.undo()); assert!(d.undo()); assert_eq!(d.text(), "ab");
2683 assert!(!d.undo(), "the pruned units ('a','b' inserts) are gone");
2684 assert!(!d.can_undo());
2685 assert!(d.redo());
2687 assert_eq!(d.text(), "abc");
2688 }
2689
2690 #[test]
2691 fn pruning_preserves_branches_within_the_window() {
2692 let mut d = doc("");
2695 d.edit(vec![EditOp::insert(0, "a")]).unwrap(); d.edit(vec![EditOp::insert(1, "b")]).unwrap(); d.undo(); d.edit(vec![EditOp::insert(1, "X")]).unwrap(); d.set_undo_limit(Some(1));
2701 assert_eq!(d.undo_depth(), 1);
2702 assert!(d.undo()); assert_eq!(d.text(), "a");
2704 assert_eq!(d.redo_branch_count(), 2, "both branches survived the rebuild");
2705 assert!(d.select_redo_branch(0));
2706 assert!(d.redo());
2707 assert_eq!(d.text(), "ab", "the retained branch is still reachable after a prune");
2708 }
2709
2710 #[test]
2711 fn typing_run_merges_into_one_undo_step() {
2712 let mut d = doc("");
2714 for (i, ch) in "hello".chars().enumerate() {
2715 d.edit_grouped(vec![EditOp::insert(i as u32, ch.to_string())], typ(OpClass::Type))
2716 .unwrap();
2717 }
2718 assert_eq!(d.text(), "hello");
2719 assert!(d.undo());
2720 assert_eq!(d.text(), "", "the whole typing run undoes as one unit");
2721 assert!(d.redo());
2722 assert_eq!(d.text(), "hello", "and redoes as one unit");
2723 }
2724
2725 fn spans(d: &Document) -> Vec<(u32, u32)> {
2728 d.selections().all().iter().map(|s| (s.tail(), s.head())).collect()
2729 }
2730
2731 #[test]
2732 fn undo_restores_every_caret_of_a_multi_cursor_edit() {
2733 let mut d = doc("a b c");
2736 d.set_selections(SelectionSet::from_offsets(&[0, 2, 4]));
2737 d.type_char('X');
2738 assert_eq!(d.text(), "Xa Xb Xc");
2739 assert!(d.undo());
2740 assert_eq!(d.text(), "a b c");
2741 assert_eq!(spans(&d), vec![(0, 0), (2, 2), (4, 4)], "undo restores the pre-edit carets");
2742 }
2743
2744 #[test]
2745 fn redo_restores_the_carets_the_edit_left() {
2746 let mut d = doc("a b c");
2748 d.set_selections(SelectionSet::from_offsets(&[0, 2, 4]));
2749 d.type_char('X');
2750 assert!(d.undo());
2751 assert!(d.redo());
2752 assert_eq!(d.text(), "Xa Xb Xc");
2753 assert_eq!(spans(&d), vec![(1, 1), (4, 4), (7, 7)], "redo restores the post-edit carets");
2754
2755 let mut p = doc("a b c");
2759 p.set_selections(SelectionSet::from_offsets(&[1, 3, 5]));
2760 p.type_char('(');
2761 assert_eq!(p.text(), "a() b() c()");
2762 let placed = spans(&p);
2763 assert_eq!(placed, vec![(2, 2), (6, 6), (10, 10)]);
2764 assert!(p.undo());
2765 assert!(p.redo());
2766 assert_eq!(spans(&p), placed, "redo restores the verb's caret placement");
2767
2768 let mut s = doc("ab cd");
2771 s.set_selections(SelectionSet::from_ranges(&[(2, 0), (3, 5)], 1));
2772 let before = spans(&s);
2773 s.type_char('(');
2774 assert_eq!(s.text(), "(ab) (cd)");
2775 let after = spans(&s);
2776 assert_eq!(after, vec![(1, 3), (6, 8)]);
2777 assert!(s.undo());
2778 assert_eq!(spans(&s), before, "undo restores both ranges, the reversed one reversed");
2779 assert!(s.redo());
2780 assert_eq!(spans(&s), after, "redo restores the surround's interiors");
2781 }
2782
2783 #[test]
2784 fn a_merged_typing_run_restores_the_selections_from_before_the_run() {
2785 let mut d = doc("foo bar baz");
2790 d.set_selections(SelectionSet::from_ranges(&[(0, 3), (8, 11)], 1));
2791 let before = spans(&d);
2792 for ch in "xyz".chars() {
2793 d.type_char(ch);
2794 }
2795 assert_eq!(d.text(), "xyz bar xyz");
2796 let after = spans(&d);
2797 assert_eq!(after, vec![(3, 3), (11, 11)]);
2798 assert!(d.undo());
2799 assert_eq!(d.text(), "foo bar baz", "one undo reverts the whole run");
2800 assert!(!d.can_undo());
2801 assert_eq!(spans(&d), before, "undo restores the selections from before the run");
2802 assert!(d.redo());
2803 assert_eq!(spans(&d), after, "redo restores the carets after the run's last keystroke");
2804 }
2805
2806 #[test]
2807 fn restored_carets_are_ejected_from_a_fold_collapsed_since() {
2808 use crate::Point;
2814 let mut d = doc("top\nm {\n a\n}\n");
2815 let hidden = |d: &Document| {
2816 let head = d.selections().newest().head();
2817 FoldMap::new(d.folds(), d.brackets(), d.buffer()).display_position(d.buffer(), head, d.tab_size()).is_none()
2818 };
2819 let header_end = |d: &Document| d.buffer().point_to_offset(Point::new(1, 3));
2820 d.set_selections(SelectionSet::new(d.buffer().point_to_offset(Point::new(2, 2))));
2821 d.edit(vec![EditOp::insert(0, "// ")]).unwrap();
2822 let opener = d.buffer().point_to_offset(Point::new(1, 2));
2823 assert!(d.toggle_fold_opener(opener));
2824 assert!(!hidden(&d), "the fold gesture ejected the live caret");
2825
2826 assert!(d.undo());
2827 assert!(d.folds().is_folded(d.buffer().point_to_offset(Point::new(1, 2))), "fold untouched");
2828 assert!(!hidden(&d), "undo's restored caret is not left inside the fold");
2829 assert_eq!(d.selections().newest().head(), header_end(&d), "…it lands at the header's end");
2830
2831 assert!(d.redo());
2832 assert!(!hidden(&d), "redo's restored caret is not left inside the fold");
2833 assert_eq!(d.selections().newest().head(), header_end(&d));
2834 }
2835
2836 #[test]
2837 fn undo_and_redo_restore_the_observed_selections_across_random_verbs() {
2838 type State = (String, Vec<Selection>);
2849 let state = |d: &Document| -> State { (d.text().into_owned(), d.selections().all().to_vec()) };
2850 let mut rng = 0x5E1E_C7ED_0008_u64;
2851 let mut next = move |m: u64| {
2852 rng ^= rng << 13;
2853 rng ^= rng >> 7;
2854 rng ^= rng << 17;
2855 rng % m
2856 };
2857 let mut d = doc("fn main() {\n let a = (1, \"x\");\n}\n");
2858 d.set_line_comment(Some("//"));
2859 let mut units: Vec<(State, State)> = Vec::new();
2862 let mut redos: Vec<(State, State)> = Vec::new();
2863 let (mut undone, mut redone, mut merged, mut multi) = (0, 0, 0, 0);
2864 const CHARS: &[char] = &['a', 'b', ' ', '(', ')', '"', '{', '}', '[', ']'];
2865 for step in 0..4_000 {
2866 let roll = next(20);
2867 if roll < 3 {
2868 let ok = d.undo();
2869 assert_eq!(ok, !units.is_empty(), "step {step}: undo availability");
2870 if let Some(unit) = units.pop() {
2871 assert_eq!(state(&d), unit.0, "step {step}: undo restores the before-state");
2872 redos.push(unit);
2873 undone += 1;
2874 }
2875 continue;
2876 }
2877 if roll < 8 {
2879 let ok = d.redo();
2880 assert_eq!(ok, !redos.is_empty(), "step {step}: redo availability");
2881 if let Some(unit) = redos.pop() {
2882 assert_eq!(state(&d), unit.1, "step {step}: redo restores the after-state");
2883 units.push(unit);
2884 redone += 1;
2885 }
2886 continue;
2887 }
2888 if roll < 11 {
2889 let len = d.buffer().len();
2891 match next(4) {
2892 0 => {
2893 let n = 1 + next(4) as usize;
2894 let ranges: Vec<(u32, u32)> = (0..n)
2895 .map(|_| {
2896 let a = next(u64::from(len) + 1) as u32;
2897 let b = if next(2) == 0 { a } else { next(u64::from(len) + 1) as u32 };
2898 (a, b)
2899 })
2900 .collect();
2901 d.set_selections(SelectionSet::from_ranges(&ranges, next(n as u64) as usize));
2902 }
2903 1 => {
2904 let motions = [Motion::Left, Motion::Right, Motion::Up, Motion::Down, Motion::LineEnd];
2905 d.move_carets(motions[next(motions.len() as u64) as usize], next(3) == 0);
2906 }
2907 2 => d.add_caret(next(u64::from(len) + 1) as u32),
2908 _ => d.add_caret_vertical(next(2) == 0),
2909 }
2910 continue;
2911 }
2912 let (pre, depth, rev) = (state(&d), d.undo_depth(), d.revision());
2913 match next(16) {
2914 0..=5 => d.type_char(CHARS[next(CHARS.len() as u64) as usize]),
2915 6 => d.backspace(),
2916 7 => d.delete_forward(),
2917 8 => d.enter(),
2918 9 => d.tab(),
2919 10 => d.outdent(),
2920 11 => d.delete_word_back(),
2921 12 => d.insert_text("p\r\nq"),
2922 13 => d.toggle_line_comment(),
2923 14 => d.insert_line(next(2) == 0),
2924 _ => match next(3) {
2925 0 => d.move_line(next(2) == 0),
2926 1 => d.copy_line(next(2) == 0),
2927 _ => d.cut(),
2928 },
2929 }
2930 let post = state(&d);
2931 if d.undo_depth() == depth + 1 {
2932 multi += usize::from(pre.1.len() > 1);
2933 units.push((pre, post));
2934 redos.clear(); } else if d.revision() != rev {
2936 assert_eq!(d.undo_depth(), depth, "step {step}: a merge keeps the depth");
2937 units.last_mut().expect("a merge needs an open unit").1 = post;
2938 merged += 1;
2939 }
2940 }
2941 assert!(
2944 undone > 400 && redone > 140 && merged > 70 && multi > 600,
2945 "walk too thin: {undone} undos / {redone} redos / {merged} merges / {multi} multi-caret units",
2946 );
2947 }
2948
2949 #[test]
2950 fn fold_toggle_seals_undo_and_clears_the_expand_ladder() {
2951 let mut d = doc("m {\n word\n}");
2955 d.set_selections(SelectionSet::new(7)); d.type_char('x');
2957 d.expand_selection(); assert!(d.toggle_fold_opener(2));
2959 d.shrink_selection(); let head = d.selections().newest().head();
2961 let fm = crate::fold_map::FoldMap::new(d.folds(), d.brackets(), d.buffer());
2962 assert!(
2963 fm.display_position(d.buffer(), head, d.tab_size()).is_some(),
2964 "…so the caret cannot be restored into the collapsed fold"
2965 );
2966 d.type_char('y');
2968 assert!(d.undo());
2969 assert!(d.text().contains('x'), "undo reverted only the post-toggle typing");
2970 }
2971
2972 #[test]
2973 fn find_navigation_expands_a_collapsed_inline_fold() {
2974 let mut d = doc("x = [1, 2, 3]\nafter");
2979 assert!(d.toggle_fold_opener(4)); d.set_find_query(Some(FindQuery { text: "2".into(), case_sensitive: false, ..Default::default() }), 0);
2981 d.find_next(0).expect("the match exists");
2982 assert!(!d.folds().is_folded(4), "the inline fold expanded to show the match");
2983 }
2984
2985 #[test]
2986 fn editing_hidden_text_expands_only_the_enclosing_fold() {
2987 let text = "a {\n keep\n}\nb {\n body\n}\nc\n";
2992 let mut d = doc(text);
2993 let opens: Vec<u32> = text.match_indices('{').map(|(i, _)| i as u32).collect();
2994 let (keep, edited) = (opens[0], opens[1]);
2997 assert!(d.toggle_fold_opener(keep));
2998 assert!(d.toggle_fold_opener(edited));
2999 assert!(d.folds().is_folded(keep) && d.folds().is_folded(edited));
3000 let inside = text.find("body").unwrap() as u32 + 2;
3002 d.edit(vec![EditOp::insert(inside, "X")]).unwrap();
3003 assert!(!d.folds().is_folded(edited), "editing inside the second block expands it");
3004 assert!(d.folds().is_folded(keep), "the untouched first block stays folded");
3005 }
3006
3007 #[test]
3008 fn next_diagnostic_advances_past_a_shared_start_offset() {
3009 use crate::{Diagnostic, Severity};
3010 let mut d = doc("abcdef");
3013 let rev = d.revision();
3014 d.set_diagnostics(
3015 rev,
3016 vec![
3017 Diagnostic::new(1..3, Severity::Error, "a"),
3018 Diagnostic::new(1..5, Severity::Warning, "b"),
3019 ],
3020 );
3021 assert_eq!(d.next_diagnostic(true), Some(1..3));
3022 assert_eq!(d.next_diagnostic(true), Some(1..5), "the same-start sibling is reachable");
3023 assert_eq!(d.next_diagnostic(true), Some(1..3), "and the walk wraps");
3024 }
3025
3026 #[test]
3027 fn reveal_classes_and_no_op_verbs() {
3028 use crate::{Diagnostic, Severity};
3029 let mut d = doc("f( ab )");
3033 let seq0 = d.reveal_seq();
3034 d.next_diagnostic(true); assert_eq!(d.reveal_seq(), seq0, "a no-op F8 requests no reveal");
3036 d.set_selections(SelectionSet::new(3));
3037 d.jump_to_bracket(); assert!(d.reveal_seq() > seq0, "a real bracket hop requests a reveal");
3039 assert_eq!(d.reveal_mode(), RevealMode::Fit, "…of the Fit class");
3040 let rev = d.revision();
3041 d.set_diagnostics(rev, vec![Diagnostic::new(1..2, Severity::Error, "e")]);
3042 let seq1 = d.reveal_seq();
3043 d.next_diagnostic(true);
3044 assert!(d.reveal_seq() > seq1);
3045 assert_eq!(d.reveal_mode(), RevealMode::Center, "a diagnostic jump centers");
3046 }
3047
3048 #[test]
3049 fn ctrl_d_force_reveals_while_select_all_holds() {
3050 let mut d = doc("foo bar foo baz foo");
3053 d.set_selections(SelectionSet::new(0));
3054 d.add_next_occurrence(); d.add_next_occurrence(); assert_eq!(d.reveal_mode(), RevealMode::FitForce, "Ctrl+D force-reveals the new cursor");
3057 let mut e = doc("foo bar foo baz foo");
3058 e.set_selections(SelectionSet::new(0));
3059 e.select_all_occurrences();
3060 assert_eq!(e.reveal_mode(), RevealMode::Fit, "Ctrl+Shift+L holds if a cursor is visible");
3061 }
3062
3063 #[test]
3064 fn diagnostic_navigation_wraps_selects_and_reveals() {
3065 use crate::{Diagnostic, Severity};
3066 let mut d = doc("aa bb\ncc\ndd");
3067 let rev = d.revision();
3068 d.set_diagnostics(
3069 rev,
3070 vec![
3071 Diagnostic::new(3..5, Severity::Warning, "w"),
3072 Diagnostic::new(9..11, Severity::Error, "e"),
3073 ],
3074 );
3075 assert_eq!(d.next_diagnostic(true), Some(3..5));
3076 assert_eq!(d.next_diagnostic(true), Some(9..11));
3077 assert_eq!(d.next_diagnostic(true), Some(3..5), "wraps to the first");
3078 assert_eq!(d.next_diagnostic(false), Some(9..11), "prev wraps to the last");
3079 let s = d.selections().newest();
3080 assert_eq!((s.start(), s.end()), (9, 11), "the diagnostic's span is selected");
3081 let mut e = doc("x");
3083 e.set_selections(SelectionSet::new(1));
3084 assert!(e.next_diagnostic(true).is_none());
3085 assert_eq!(e.selections().newest().head(), 1);
3086 let mut f = doc("aa\nfn {\nbb\n}\ncc");
3088 assert!(f.toggle_fold_opener(6));
3089 let rev = f.revision();
3090 f.set_diagnostics(rev, vec![Diagnostic::new(8..10, Severity::Error, "hidden")]);
3091 assert_eq!(f.next_diagnostic(true), Some(8..10));
3092 assert!(!f.folds().is_folded(6), "the fold expanded to show the diagnostic");
3093 }
3094
3095 #[test]
3096 fn caret_word_occurrences_are_whole_word_only() {
3097 let mut d = doc("foo foobar foo\nfoo");
3098 d.set_selections(SelectionSet::new(1)); assert_eq!(
3100 d.caret_word_occurrences(0..u32::MAX),
3101 vec![0..3, 11..14, 15..18],
3102 "the foobar prefix is not a whole-word match"
3103 );
3104 assert_eq!(d.caret_word_occurrences(4..15), vec![11..14], "only in-window matches");
3106 let mut set = SelectionSet::new(0);
3108 set.set_single(Selection::from_anchor(SelectionId(0), 0, 3));
3109 d.set_selections(set);
3110 assert!(d.caret_word_occurrences(0..u32::MAX).is_empty());
3111 let mut w = doc("a b");
3113 w.set_selections(SelectionSet::new(2));
3114 assert!(w.caret_word_occurrences(0..u32::MAX).is_empty());
3115 }
3116
3117 #[test]
3121 fn caret_word_occurrences_take_a_mid_char_window() {
3122 let mut d = doc("é foo é foo"); d.set_selections(SelectionSet::new(11));
3124 assert_eq!(d.caret_word_occurrences(1..8), vec![3..6], "both window ends split an 'é'");
3125 }
3126
3127 #[test]
3128 fn find_navigation_expands_a_collapsed_fold_to_reveal_the_match() {
3129 let mut d = doc("aa\nfn {\nneedle\n}\nbb");
3132 let opener = 6; assert!(d.toggle_fold_opener(opener));
3134 d.set_find_query(Some(FindQuery { text: "needle".into(), case_sensitive: false, ..Default::default() }), 0);
3135 let m = d.find_next(0).expect("the match exists");
3136 assert!(!d.folds().is_folded(opener), "the fold expanded to reveal the match");
3137 let fm = crate::fold_map::FoldMap::new(d.folds(), d.brackets(), d.buffer());
3138 assert!(
3139 fm.display_position(d.buffer(), m.end, d.tab_size()).is_some(),
3140 "the match head renders"
3141 );
3142 let mut v = doc("aa\nfn {\nx\n}\nbb");
3144 assert!(v.toggle_fold_opener(6));
3145 v.set_find_query(Some(FindQuery { text: "fn".into(), case_sensitive: false, ..Default::default() }), 0);
3146 v.find_next(0).expect("match on the header");
3147 assert!(v.folds().is_folded(6), "a visible match keeps the fold collapsed");
3148 }
3149
3150 #[test]
3151 fn jump_to_bracket_crosses_and_returns() {
3152 let mut d = doc("f( ab )");
3155 d.set_selections(SelectionSet::new(2));
3156 d.jump_to_bracket();
3157 assert_eq!(d.selections().newest().head(), 7);
3158 d.jump_to_bracket();
3159 assert_eq!(d.selections().newest().head(), 2);
3160 d.set_selections(SelectionSet::new(4)); d.jump_to_bracket();
3163 assert_eq!(d.selections().newest().head(), 6, "lands before `)`");
3164 let mut e = doc("plain");
3166 e.set_selections(SelectionSet::new(3));
3167 e.jump_to_bracket();
3168 assert_eq!(e.selections().newest().head(), 3);
3169 }
3170
3171 #[test]
3172 fn expand_selection_climbs_the_bracket_ladder_and_shrinks_back() {
3173 let mut d = doc("m { a(bb) }");
3175 d.set_selections(SelectionSet::new(7)); let sel = |d: &Document| (d.selections().newest().start(), d.selections().newest().end());
3177 d.expand_selection();
3178 assert_eq!(sel(&d), (6, 8), "word first");
3179 d.expand_selection();
3180 assert_eq!(sel(&d), (5, 9), "word == () contents, so the pair incl. brackets");
3181 d.expand_selection();
3182 assert_eq!(sel(&d), (3, 10), "the brace contents");
3183 d.expand_selection();
3184 assert_eq!(sel(&d), (2, 11), "the brace pair incl. brackets");
3185 d.expand_selection();
3186 assert_eq!(sel(&d), (0, 11), "the whole document");
3187 d.expand_selection();
3188 assert_eq!(sel(&d), (0, 11), "fully expanded is a no-op");
3189 d.shrink_selection();
3191 assert_eq!(sel(&d), (2, 11));
3192 d.shrink_selection();
3193 assert_eq!(sel(&d), (3, 10));
3194 d.move_carets(Motion::Right, false);
3196 let caret = sel(&d);
3197 d.shrink_selection();
3198 assert_eq!(sel(&d), caret, "the ladder cleared on the caret move");
3199 }
3200
3201 #[test]
3202 fn add_caret_vertical_stacks_and_stops_at_edges() {
3203 let mut d = doc("aaa\nbbb\nccc");
3206 d.set_selections(SelectionSet::new(5)); d.add_caret_vertical(false); d.add_caret_vertical(true); assert_eq!(d.selections().len(), 3);
3210 d.type_char('X');
3211 assert_eq!(d.text(), "aXaa\nbXbb\ncXcc");
3212 let mut e = doc("aa\nbb");
3214 e.set_selections(SelectionSet::new(1));
3215 e.add_caret_vertical(false);
3216 assert_eq!(e.selections().len(), 1);
3217 }
3218
3219 #[test]
3220 fn add_caret_vertical_skips_a_collapsed_fold() {
3221 let mut d = doc("aa\nfn {\nbb\ncc\n}\ndd");
3222 assert!(d.toggle_fold_opener(6)); d.set_selections(SelectionSet::new(16)); d.add_caret_vertical(false);
3225 let rows: Vec<u32> =
3226 d.selections().all().iter().map(|s| d.buffer().offset_to_point(s.head()).row).collect();
3227 assert_eq!(rows, vec![1, 5], "the new caret lands on the fold header, not a hidden row");
3228 }
3229
3230 #[test]
3231 fn select_all_occurrences_takes_every_match_from_a_word_seed() {
3232 let mut d = doc("foo bar foo baz foo");
3235 d.set_selections(SelectionSet::new(1));
3236 d.select_all_occurrences();
3237 assert_eq!(d.selections().len(), 3);
3238 d.type_char('X');
3239 assert_eq!(d.text(), "X bar X baz X");
3240 let mut e = doc(" ");
3242 e.set_selections(SelectionSet::new(1));
3243 e.select_all_occurrences();
3244 assert_eq!(e.selections().len(), 1);
3245 assert!(e.selections().newest().is_empty());
3246 }
3247
3248 #[test]
3249 fn select_find_matches_turns_matches_into_selections() {
3250 let mut d = doc("foo bar foo baz foo");
3251 d.set_find_query(Some(FindQuery { text: "foo".into(), case_sensitive: false, ..Default::default() }), 0);
3252 d.find_next(0); assert!(d.select_find_matches());
3254 assert_eq!(d.selections().len(), 3);
3255 assert_eq!(d.selections().newest().start(), 0, "the ACTIVE match is newest");
3256 let mut e = doc("abc");
3258 assert!(!e.select_find_matches());
3259 assert_eq!(e.selections().len(), 1);
3260 }
3261
3262 #[test]
3263 fn replace_all_swaps_every_match_in_one_undo_step() {
3264 let original = "foo bar foo baz foo";
3267 let mut d = doc(original);
3268 d.set_find_query(Some(FindQuery { text: "foo".into(), case_sensitive: false, ..Default::default() }), 0);
3269 assert_eq!(d.replace_all("QUX", false), 3);
3270 assert_eq!(d.text(), "QUX bar QUX baz QUX");
3271 assert!(d.undo());
3272 assert_eq!(d.text(), original, "replace-all must undo as ONE step");
3273 assert!(!d.undo(), "…with no second step hiding behind it");
3274 assert_eq!(d.find_match_count(), 3);
3276 }
3277
3278 #[test]
3279 fn replace_all_is_not_capped_by_the_live_match_set() {
3280 let n = crate::find::FIND_MATCH_CAP + 500;
3284 let mut d = doc(&"a".repeat(n));
3285 d.set_find_query(Some(FindQuery { text: "a".into(), case_sensitive: false, ..Default::default() }), 0);
3286 assert_eq!(d.find_match_count(), crate::find::FIND_MATCH_CAP, "the live set caps");
3287 assert_eq!(d.replace_all("b", false), n, "replace-all must NOT cap");
3288 assert_eq!(d.text(), "b".repeat(n), "every match past the cap replaced too");
3289 }
3290
3291 #[test]
3292 fn replace_next_selects_before_it_replaces() {
3293 let mut d = doc("foo bar foo");
3294 d.set_find_query(Some(FindQuery { text: "foo".into(), case_sensitive: false, ..Default::default() }), 0);
3295 assert_eq!(d.replace_next("X", false, 0), Some(0..3));
3298 assert_eq!(d.text(), "foo bar foo", "the first press must not replace");
3299 assert_eq!(d.replace_next("X", false, 0), Some(6..9));
3303 assert_eq!(d.text(), "X bar foo");
3304 assert!(d.undo());
3306 assert_eq!(d.text(), "foo bar foo");
3307 }
3308
3309 #[test]
3310 fn redo_of_a_replace_restores_the_match_it_advanced_to() {
3311 let mut d = doc("foo bar foo");
3316 d.set_find_query(Some(FindQuery { text: "foo".into(), ..Default::default() }), 0);
3317 assert_eq!(d.replace_next("X", false, 0), Some(0..3), "the first press navigates");
3318 assert_eq!(d.replace_next("X", false, 0), Some(6..9), "the second replaces and advances");
3319 assert_eq!(d.text(), "X bar foo");
3320 let seen = spans(&d);
3321 assert_eq!(seen, vec![(6, 9)], "the next match is selected");
3322 assert!(d.undo());
3323 assert_eq!(spans(&d), vec![(0, 3)], "undo restores the match that was replaced");
3324 assert!(d.redo());
3325 assert_eq!(d.text(), "X bar foo");
3326 assert_eq!(spans(&d), seen, "redo restores the match the replace advanced to");
3327 }
3328
3329 #[test]
3330 fn find_scope_restricts_matches_and_replace_all() {
3331 let mut d = doc("foo foo | foo foo");
3332 d.set_find_query(Some(FindQuery { text: "foo".into(), case_sensitive: false, ..Default::default() }), 0);
3333 assert_eq!(d.find_match_count(), 4);
3334 d.set_find_scope(Some(0..7), 0);
3336 assert_eq!(d.find_match_count(), 2, "only the in-scope matches survive");
3337 assert_eq!(d.replace_all("X", false), 2, "replace-all must honor the scope");
3339 assert_eq!(d.text(), "X X | foo foo", "the out-of-scope matches are untouched");
3340 d.set_find_scope(None, 0);
3342 assert_eq!(d.find_match_count(), 2, "the two outside are findable again");
3343 }
3344
3345 #[test]
3346 fn the_find_scope_rides_edits_and_drops_when_it_collapses() {
3347 let mut d = doc("aaa foo bbb");
3348 d.set_find_query(Some(FindQuery { text: "foo".into(), case_sensitive: false, ..Default::default() }), 0);
3349 d.set_find_scope(Some(4..7), 0); assert_eq!(d.find_match_count(), 1);
3351 d.edit(vec![EditOp::insert(0, "XY")]).unwrap();
3353 assert_eq!(d.find_scope(), Some(6..9));
3354 assert_eq!(d.find_match_count(), 1, "the scope still covers the match");
3355 d.edit(vec![EditOp::delete(6..9)]).unwrap();
3358 assert_eq!(d.find_scope(), None, "a collapsed scope must clear");
3359 }
3360
3361 #[test]
3366 fn the_find_scope_clips_a_host_range() {
3367 let mut d = doc("é foo é"); d.set_find_query(Some(FindQuery { text: "foo".into(), case_sensitive: false, ..Default::default() }), 0);
3369 d.set_find_scope(Some(1..8), 0);
3370 assert_eq!(d.find_scope(), Some(0..9));
3371 assert_eq!(d.find_match_count(), 1);
3372 d.set_find_scope(Some(1000..2000), 0);
3373 assert_eq!(d.find_scope(), None, "a scope past the end clamps to empty, which clears");
3374 assert_eq!(d.find_match_count(), 1);
3375 }
3376
3377 #[test]
3378 fn an_edit_outside_the_scope_creates_no_matches() {
3379 let mut d = doc("foo | ...");
3380 d.set_find_query(Some(FindQuery { text: "foo".into(), case_sensitive: false, ..Default::default() }), 0);
3381 d.set_find_scope(Some(0..3), 0);
3382 assert_eq!(d.find_match_count(), 1);
3383 d.edit(vec![EditOp::insert(6, "foo")]).unwrap();
3386 assert_eq!(d.find_match_count(), 1, "out-of-scope text must not match");
3387 }
3388
3389 #[test]
3395 fn scoped_repair_equals_a_fresh_scoped_scan_under_random_edits() {
3396 let mut seed = 0x5EED_u64;
3397 let mut rng = || {
3398 seed ^= seed << 13;
3399 seed ^= seed >> 7;
3400 seed ^= seed << 17;
3401 seed
3402 };
3403 let base = "foo bar foo baz\nfoo foo bar\nbarfoo foo\n".repeat(3);
3404 let n = base.len() as u32;
3405 for trial in 0..300 {
3406 let mut d = doc(&base);
3407 let a = (rng() as u32) % n;
3408 let b = (rng() as u32) % n;
3409 let (lo, hi) = (a.min(b), a.max(b));
3410 let lo = d.buffer().clip_offset(lo, Bias::Right);
3411 let hi = d.buffer().clip_offset(hi, Bias::Left);
3412 if lo >= hi {
3413 continue;
3414 }
3415 d.set_find_query(Some(FindQuery { text: "foo".into(), case_sensitive: false, ..Default::default() }), 0);
3416 d.set_find_scope(Some(lo..hi), 0);
3417 for _ in 0..4 {
3418 let at = (rng() as u32) % (d.buffer().len() + 1);
3419 let at = d.buffer().clip_offset(at, Bias::Left);
3420 let ins = ["foo", "x", "", "\nfoo "][(rng() % 4) as usize];
3421 d.edit(vec![EditOp::insert(at, ins)]).unwrap();
3422 }
3423 let incremental: Vec<_> = d.find_matches_in(0..u32::MAX).map(|(r, _)| r).collect();
3424 let scope = d.find_scope();
3428 let q = d.find_query().cloned().unwrap();
3429 d.set_find_query(None, 0);
3430 d.set_find_query(Some(q), 0);
3431 let fresh: Vec<_> = d.find_matches_in(0..u32::MAX).map(|(r, _)| r).collect();
3432 assert_eq!(incremental, fresh, "trial {trial}, scope {scope:?}");
3433 if let Some(s) = scope {
3435 assert!(
3436 incremental.iter().all(|m| m.start >= s.start && m.end <= s.end),
3437 "trial {trial}: a match escaped the scope {s:?}: {incremental:?}"
3438 );
3439 }
3440 }
3441 }
3442
3443 #[test]
3455 fn line_scoped_repair_equals_a_fresh_scan_under_random_edits() {
3456 let mut seed = 0x00C0_FFEE_u64;
3457 let mut rng = || {
3458 seed ^= seed << 13;
3459 seed ^= seed >> 7;
3460 seed ^= seed << 17;
3461 seed
3462 };
3463 let base =
3464 "foo bar foo baz\nfoo foo bar\nbarfoo foo\n\nqux foofoo\n(foo) a == b; x=-1 foo.bar\n"
3465 .repeat(3);
3466 let ww =
3467 |text: &str| FindQuery { text: text.into(), whole_word: true, ..Default::default() };
3468 let queries = [
3469 ww("foo"),
3470 ww("(foo"),
3474 ww("=="),
3475 ww("foo."),
3476 ww("-1"),
3477 FindQuery {
3478 text: r"\(foo|==".into(),
3479 whole_word: true,
3480 regex: true,
3481 ..Default::default()
3482 },
3483 FindQuery { text: "f.o".into(), regex: true, ..Default::default() },
3484 FindQuery { text: r"\w+o".into(), regex: true, ..Default::default() },
3485 FindQuery { text: ".*".into(), regex: true, ..Default::default() },
3487 FindQuery { text: r"\bba\w+".into(), regex: true, ..Default::default() },
3488 FindQuery {
3489 text: "foo|bar".into(),
3490 whole_word: true,
3491 regex: true,
3492 ..Default::default()
3493 },
3494 ];
3495 for (qi, q) in queries.iter().enumerate() {
3496 let mut nonempty = 0;
3499 for trial in 0..40 {
3500 let mut d = doc(&base);
3501 d.set_find_query(Some(q.clone()), 0);
3502 for _ in 0..5 {
3503 let len = d.buffer().len();
3504 if rng() % 3 == 0 && len > 4 {
3507 let a = d.buffer().clip_offset((rng() as u32) % len, Bias::Left);
3508 let b = d
3509 .buffer()
3510 .clip_offset((a + 1 + (rng() as u32) % 6).min(len), Bias::Right);
3511 if a < b {
3512 d.edit(vec![EditOp::delete(a..b)]).unwrap();
3513 }
3514 } else {
3515 let at = d.buffer().clip_offset((rng() as u32) % (len + 1), Bias::Left);
3516 let ins = ["foo", "x", "\n", " ", "\nfoo bar", "(", "=", "-1", "."]
3519 [(rng() % 9) as usize];
3520 d.edit(vec![EditOp::insert(at, ins)]).unwrap();
3521 }
3522 }
3523 let incremental: Vec<_> = d.find_matches_in(0..u32::MAX).map(|(r, _)| r).collect();
3524 d.set_find_query(None, 0);
3526 d.set_find_query(Some(q.clone()), 0);
3527 let fresh: Vec<_> = d.find_matches_in(0..u32::MAX).map(|(r, _)| r).collect();
3528 assert_eq!(incremental, fresh, "query {qi} ({:?}), trial {trial}", q.text);
3529 nonempty += usize::from(!fresh.is_empty());
3530 }
3531 assert!(nonempty >= 30, "query {qi} ({:?}) is vacuous: {nonempty}/40", q.text);
3532 }
3533 }
3534
3535 #[test]
3536 fn regex_replace_all_is_one_undo_step() {
3537 let original = "a1 b22 c333";
3538 let mut d = doc(original);
3539 d.set_find_query(
3540 Some(FindQuery { text: r"\d+".into(), regex: true, ..Default::default() }),
3541 0,
3542 );
3543 assert_eq!(d.find_match_count(), 3);
3544 assert_eq!(d.replace_all("#", false), 3);
3545 assert_eq!(d.text(), "a# b# c#");
3546 assert!(d.undo());
3547 assert_eq!(d.text(), original, "a regex replace-all undoes as ONE step");
3548 }
3549
3550 #[test]
3551 fn a_regex_replacement_expands_capture_groups() {
3552 let mut d = doc("fn one()\nfn two()");
3554 d.set_find_query(
3555 Some(FindQuery { text: r"fn (\w+)\(\)".into(), regex: true, ..Default::default() }),
3556 0,
3557 );
3558 assert_eq!(d.replace_all("let $1 = 1;", false), 2);
3559 assert_eq!(d.text(), "let one = 1;\nlet two = 1;");
3560 assert!(d.undo());
3561 assert_eq!(d.text(), "fn one()\nfn two()", "still ONE undo step");
3562
3563 let mut e = doc("aaa");
3566 e.set_find_query(Some(FindQuery { text: "aaa".into(), ..Default::default() }), 0);
3567 assert_eq!(e.replace_all("$1", false), 1);
3568 assert_eq!(e.text(), "$1");
3569 }
3570
3571 #[test]
3572 fn replace_next_expands_captures_for_the_active_match_only() {
3573 let mut d = doc("fn one()\nfn two()");
3574 d.set_find_query(
3575 Some(FindQuery { text: r"fn (\w+)\(\)".into(), regex: true, ..Default::default() }),
3576 0,
3577 );
3578 d.replace_next("x", false, 0); d.replace_next("[$1]", false, 0); assert_eq!(d.text(), "[one]\nfn two()");
3581 }
3582
3583 #[test]
3584 fn preserve_case_recases_replacements_to_their_match() {
3585 let mut d = doc("FOO Foo foo fOo");
3588 d.set_find_query(Some(FindQuery { text: "foo".into(), case_sensitive: false, ..Default::default() }), 0);
3589 assert_eq!(d.replace_all("bar", true), 4);
3590 assert_eq!(d.text(), "BAR Bar bar bar");
3592
3593 let mut e = doc("FOO Foo foo");
3595 e.set_find_query(Some(FindQuery { text: "foo".into(), case_sensitive: false, ..Default::default() }), 0);
3596 assert_eq!(e.replace_all("bar", false), 3);
3597 assert_eq!(e.text(), "bar bar bar");
3598 }
3599
3600 #[test]
3601 fn preserve_case_follows_vs_codes_table() {
3602 let table: &[(&str, &str, &str)] = &[
3607 ("abc", "Def", "def"),
3608 ("Abc", "Def", "Def"),
3609 ("ABC", "Def", "DEF"),
3610 ("aBc", "Def", "def"),
3611 ("AbC", "Def", "Def"),
3612 ("aBC", "Def", "def"),
3613 ("aBc", "DeF", "deF"),
3614 ("Foo-Bar", "newfoo-newbar", "Newfoo-Newbar"),
3615 ("Foo-Bar-Abc", "newfoo-newbar-newabc", "Newfoo-Newbar-Newabc"),
3616 ("Foo-Bar-abc", "newfoo-newbar", "Newfoo-newbar"),
3617 ("foo-Bar", "newfoo-newbar", "newfoo-Newbar"),
3618 ("foo-BAR", "newfoo-newbar", "newfoo-NEWBAR"),
3619 ("foO-BAR", "NewFoo-NewBar", "newFoo-NEWBAR"),
3620 ("Foo_Bar", "newfoo_newbar", "Newfoo_Newbar"),
3621 ("Foo_Bar_Abc", "newfoo_newbar_newabc", "Newfoo_Newbar_Newabc"),
3622 ("Foo_Bar_abc", "newfoo_newbar", "Newfoo_newbar"),
3623 ("Foo_Bar-abc", "newfoo_newbar-abc", "Newfoo_newbar-abc"),
3624 ("foo_Bar", "newfoo_newbar", "newfoo_Newbar"),
3625 ("Foo_BAR", "newfoo_newbar", "Newfoo_NEWBAR"),
3626 ("fooBar", "BazQux", "bazQux"),
3628 ("FooBar", "bazQux", "BazQux"),
3629 ("FOO_BAR", "baz_qux", "BAZ_QUX"),
3630 ("FOO_BAR", "bazQux", "BAZQUX"),
3631 ("foo-bar", "Baz-Qux", "baz-qux"),
3632 ("foo_barBaz", "Qux_quuxCorge", "qux_quuxCorge"),
3633 ];
3634 for &(matched, replacement, want) in table {
3635 let mut d = doc(matched);
3636 d.set_find_query(Some(FindQuery { text: matched.into(), case_sensitive: true, ..Default::default() }), 0);
3637 assert_eq!(d.replace_all(replacement, true), 1, "{matched:?}");
3638 assert_eq!(d.text(), want, "{matched:?} replaced by {replacement:?}");
3639 }
3640 }
3641
3642 #[test]
3643 fn preserve_case_applies_after_regex_expansion() {
3644 let mut d = doc("GETVALUE getvalue");
3647 d.set_find_query(
3648 Some(FindQuery { text: "(get)(value)".into(), regex: true, ..Default::default() }),
3649 0,
3650 );
3651 assert_eq!(d.replace_all("${2}_${1}", true), 2);
3654 assert_eq!(d.text(), "VALUE_GET value_get");
3656 }
3657
3658 #[test]
3659 fn an_unfinished_regex_matches_nothing_and_reports_why() {
3660 let mut d = doc("foo (bar)");
3661 d.set_find_query(
3662 Some(FindQuery { text: "(".into(), regex: true, ..Default::default() }),
3663 0,
3664 );
3665 assert_eq!(d.find_match_count(), 0, "an unfinished pattern matches nothing");
3666 assert!(d.find_pattern_error().is_some(), "…and says why");
3667 d.edit(vec![EditOp::insert(0, "x")]).unwrap();
3669 assert_eq!(d.find_match_count(), 0);
3670 d.set_find_query(
3672 Some(FindQuery { text: r"\(bar\)".into(), regex: true, ..Default::default() }),
3673 0,
3674 );
3675 assert!(d.find_pattern_error().is_none());
3676 assert_eq!(d.find_match_count(), 1);
3677 }
3678
3679 #[test]
3680 fn replace_all_without_a_match_commits_nothing() {
3681 let mut d = doc("foo");
3682 assert_eq!(d.replace_all("x", false), 0, "no query ⇒ no-op");
3683 d.set_find_query(Some(FindQuery { text: "zzz".into(), case_sensitive: false, ..Default::default() }), 0);
3684 assert_eq!(d.replace_all("x", false), 0, "no matches ⇒ no-op");
3685 assert_eq!(d.text(), "foo");
3686 assert!(!d.is_dirty(), "a no-op replace must not open an undo step");
3687 assert!(!d.undo());
3688 }
3689
3690 #[test]
3691 fn replace_reuses_the_matcher_the_query_compiled() {
3692 use crate::find::MATCHER_COMPILES;
3698 let queries = [
3699 FindQuery { text: "foo".into(), ..Default::default() },
3700 FindQuery { text: "foo".into(), whole_word: true, ..Default::default() },
3701 FindQuery { text: "f(o)o".into(), regex: true, ..Default::default() },
3702 ];
3703 for q in queries {
3704 let mut d = doc("foo foo foo");
3705 d.set_find_query(Some(q.clone()), 0);
3706 let base = MATCHER_COMPILES.with(std::cell::Cell::get);
3707 assert_eq!(d.replace_next("x", false, 0), Some(0..3), "{q:?}: the first press navigates");
3708 assert_eq!(d.replace_next("x", false, 0), Some(2..5), "{q:?}: the second replaces");
3709 assert_eq!(d.replace_all("y", false), 2, "{q:?}");
3710 assert_eq!(d.text(), "x y y", "{q:?}");
3711 assert_eq!(
3712 MATCHER_COMPILES.with(std::cell::Cell::get) - base,
3713 0,
3714 "{q:?}: a replace compiled the pattern again"
3715 );
3716 }
3717 }
3718
3719 #[test]
3720 fn caret_moves_seal_the_typing_group() {
3721 let mut d = doc("");
3724 d.type_char('f');
3725 d.type_char('o');
3726 d.move_carets(Motion::Left, false); d.type_char('x'); assert!(d.undo());
3729 assert_eq!(d.text(), "fo", "undo reverts only the run typed after the move");
3730
3731 let mut c = doc("");
3733 c.type_char('a');
3734 c.type_char('b');
3735 c.set_selections(SelectionSet::new(0));
3736 c.type_char('z'); assert!(c.undo());
3738 assert_eq!(c.text(), "ab", "the click sealed the run before it");
3739
3740 let mut b = doc("one\ntwo");
3743 b.set_selections(SelectionSet::new(0));
3744 b.type_char('z'); b.column_drag((0, 2), (1, 2));
3746 b.type_char('q');
3747 assert!(b.undo());
3748 assert_eq!(b.text(), "zone\ntwo", "the box gesture sealed the run before it");
3749 }
3750
3751 #[test]
3756 fn line_duplication_drops_autoclose_provenance() {
3757 let ac_count = |d: &Document| d.autoclose_pair_count();
3759 let mut d = doc("x");
3760 d.move_carets(Motion::Right, false); d.type_char('('); assert_eq!(d.text(), "x()");
3763 assert_eq!(ac_count(&d), 1, "typing '(' arms one AutoClosePair decoration");
3764 d.copy_line(false);
3768 assert_eq!(d.text(), "x()\nx()");
3769 assert_eq!(
3770 ac_count(&d),
3771 0,
3772 "copy_line must clear the provenance decoration, not orphan it in the store",
3773 );
3774 }
3775
3776 #[test]
3782 fn type_bracket_then_far_diagnostics_pair_survives() {
3783 use crate::{Diagnostic, Severity};
3784 let mut d = doc("hello\nworld\n");
3785 let end = d.text().len() as u32; d.set_selections(SelectionSet::new(end));
3787 d.type_char('('); assert_eq!(d.autoclose_pair_count(), 1);
3789 d.set_diagnostics(
3791 d.revision(),
3792 vec![
3793 Diagnostic::new(0..1, Severity::Error, "e0"),
3794 Diagnostic::new(2..3, Severity::Warning, "w0"),
3795 ],
3796 );
3797 d.type_char('x');
3800 assert!(d.text().ends_with("(x)"));
3801 assert_eq!(d.autoclose_pair_count(), 1, "far diagnostics don't disturb the pair");
3802 assert_eq!(d.diagnostics_in(0..u32::MAX).count(), 2, "the diagnostics ride on");
3804 d.type_char(')');
3806 assert!(d.text().ends_with("(x)"), "overtype consumes the tracked close, no doubled )");
3807 assert_eq!(d.autoclose_pair_count(), 0, "overtype consumed the pair");
3808 }
3809
3810 #[test]
3821 fn autoclose_store_moves_identically_to_unified_store() {
3822 use crate::decorations::{DecorationKind, Stickiness};
3823 use crate::{Diagnostic, Severity};
3824 let scenario = |text: &str, carets: &[u32], steps: usize| {
3828 let mut d = doc(text);
3829 d.set_selections(SelectionSet::from_offsets(carets));
3830 d.type_char('('); let pairs = d.autoclose_ranges();
3832 assert_eq!(pairs.len(), carets.len(), "one pair per caret armed");
3833 for r in &pairs {
3836 d.decorations_mut().add_decoration(
3837 r.clone(),
3838 DecorationKind::AutoClosePair,
3839 Stickiness::AlwaysGrows,
3840 );
3841 }
3842 d.set_diagnostics(d.revision(), vec![Diagnostic::new(0..1, Severity::Warning, "d")]);
3843 let unified = |d: &Document| -> Vec<Range<u32>> {
3844 d.decorations()
3845 .iter()
3846 .filter(|r| matches!(r.kind, DecorationKind::AutoClosePair))
3847 .map(|r| r.range.clone())
3848 .collect()
3849 };
3850 assert_eq!(d.autoclose_ranges(), unified(&d), "shadow starts equal");
3851 for step in 0..steps {
3852 d.type_char('z'); assert_eq!(
3854 d.autoclose_ranges(),
3855 unified(&d),
3856 "own store diverged from the unified store at step {step}",
3857 );
3858 }
3859 };
3860 scenario("aa\nbb\ncc\ndd\nee\n", &[2, 5, 8, 11, 14], 8);
3862 scenario("solo line here\n", &[14], 10);
3864 }
3865
3866 #[test]
3867 fn different_classes_do_not_merge() {
3868 let mut d = doc("ab");
3869 d.edit_grouped(vec![EditOp::insert(2, "c")], typ(OpClass::Type)).unwrap(); d.edit_grouped(vec![EditOp::delete(0..1)], typ(OpClass::Delete)).unwrap(); assert_eq!(d.text(), "bc");
3872 assert!(d.undo()); assert_eq!(d.text(), "abc");
3874 assert!(d.undo()); assert_eq!(d.text(), "ab");
3876 }
3877
3878 #[test]
3879 fn dirty_tracking_survives_undo_and_save() {
3880 let mut d = doc("x");
3881 assert!(!d.is_dirty());
3882 d.edit(vec![EditOp::insert(1, "y")]).unwrap(); assert!(d.is_dirty());
3884 d.undo(); assert!(!d.is_dirty(), "undo back to the save point reads clean");
3886 d.redo(); assert!(d.is_dirty());
3888 d.mark_saved();
3889 assert!(!d.is_dirty());
3890 }
3891
3892 #[test]
3893 fn ctrl_d_selects_word_then_adds_occurrences_then_stops() {
3894 let mut d = doc("foo bar foo baz foo"); d.add_next_occurrence();
3897 assert_eq!(d.selections().len(), 1);
3898 let s = d.selections().newest();
3899 assert_eq!((s.start(), s.end()), (0, 3));
3900 d.add_next_occurrence();
3902 assert_eq!(d.selections().len(), 2);
3903 assert_eq!((d.selections().newest().start(), d.selections().newest().end()), (8, 11));
3904 d.add_next_occurrence();
3905 assert_eq!(d.selections().len(), 3);
3906 assert_eq!(d.selections().newest().start(), 16);
3907 d.add_next_occurrence();
3909 assert_eq!(d.selections().len(), 3, "no untaken occurrence remains");
3910 }
3911
3912 #[test]
3913 fn windowed_next_occurrence_equals_whole_text_scan() {
3914 let mut state: u64 = 0x243F_6A88_85A3_08D3;
3919 let mut next = |n: u32| {
3920 state = state.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
3921 ((state >> 33) as u32) % n
3922 };
3923 let needles = ["ab", "ba", "abc", "cab"];
3925 for _ in 0..300 {
3926 let n = 12 + next(60); let text: String = (0..n)
3928 .map(|_| b"abc "[next(4) as usize] as char)
3929 .collect();
3930 let d = doc(&text);
3931 let needle = needles[next(needles.len() as u32) as usize];
3932 let nlen = needle.len() as u32;
3933 let all: Vec<(u32, u32)> = text
3935 .match_indices(needle)
3936 .map(|(i, _)| (i as u32, i as u32 + nlen))
3937 .collect();
3938 let taken: Vec<(u32, u32)> = all.iter().copied().filter(|_| next(2) == 0).collect();
3939 let is_taken = |s: u32| taken.iter().any(|&(st, en)| st == s && en == s + nlen);
3940 let from = next(n + 1);
3941 let got = scan_from(d.buffer(), needle, from, d.buffer().len(), &is_taken)
3942 .or_else(|| scan_from(d.buffer(), needle, 0, from, &is_taken));
3943 let want = find_next_occurrence(&text, needle, from, &taken);
3944 assert_eq!(got, want, "text {text:?} needle {needle:?} from {from} taken {taken:?}");
3945 }
3946 }
3947
3948 #[test]
3949 fn column_select_grows_widens_and_shrinks_back() {
3950 use crate::movement::ColumnDir::{Down, Left, Right, Up};
3951 let mut d = doc("abcd\nabcd\nabcd");
3953 d.set_selections(SelectionSet::new(1));
3954 d.column_select(Down);
3956 d.column_select(Down);
3957 assert_eq!(d.selections().len(), 3);
3958 assert!(d.selections().all().iter().all(|s| s.is_empty() && s.start() % 5 == 1));
3959 d.column_select(Right);
3961 d.column_select(Right);
3962 assert_eq!(d.selections().len(), 3);
3963 assert!(d.selections().all().iter().all(|s| s.end() - s.start() == 2));
3964 d.column_select(Up);
3966 d.column_select(Up);
3967 assert_eq!(d.selections().len(), 1, "shrinks back toward the anchor");
3968 d.column_select(Left);
3970 d.column_select(Left);
3971 assert_eq!(d.selections().len(), 1);
3972 assert!(d.selections().newest().is_empty());
3973 }
3974
3975 #[test]
3976 fn column_select_clamps_each_row_to_its_length() {
3977 use crate::movement::ColumnDir::{Down, Right};
3978 let mut d = doc("abcdef\nab"); d.set_selections(SelectionSet::new(0)); d.column_select(Down); for _ in 0..4 {
3983 d.column_select(Right); }
3985 let rows = d.selections().all();
3986 assert_eq!(rows.len(), 2);
3987 assert_eq!((rows[0].start(), rows[0].end()), (0, 4)); assert_eq!((rows[1].start(), rows[1].end()), (7, 9));
3990 }
3991
3992 #[test]
3993 fn column_drag_builds_a_box_from_two_corners() {
3994 let mut d = doc("abcd\nabcd\nabcd");
3995 d.column_drag((0, 1), (2, 3)); assert_eq!(d.selections().len(), 3);
3997 assert!(d.selections().all().iter().all(|s| s.end() - s.start() == 2));
3998 let mut r = doc("abcdef\nab"); r.column_drag((0, 0), (1, 5)); let rows = r.selections().all();
4002 assert_eq!((rows[0].start(), rows[0].end()), (0, 5)); assert_eq!((rows[1].start(), rows[1].end()), (7, 9)); }
4005
4006 #[test]
4012 fn column_drag_clamps_host_corners() {
4013 let mut d = doc("abcd\nabcd\nabcd");
4014 d.column_drag((0, 1), (u32::MAX, 3));
4015 let spans: Vec<(u32, u32)> = d.selections().all().iter().map(|s| (s.start(), s.end())).collect();
4016 assert_eq!(spans, [(1, 3), (6, 8), (11, 13)], "one row per REAL row, the last one active");
4017 d.column_select(ColumnDir::Up); assert_eq!(d.selections().len(), 2);
4019 d.column_drag((0, u32::MAX), (0, u32::MAX));
4020 d.column_select(ColumnDir::Right); assert_eq!(heads(&d), [4]);
4022 }
4023
4024 #[test]
4025 fn column_box_is_display_cells_across_tabs() {
4026 let mut d = doc("\tabcd\nwxyz"); d.column_drag((0, 4), (1, 6));
4031 let rows = d.selections().all();
4032 assert_eq!(rows.len(), 2);
4033 assert_eq!((rows[0].start(), rows[0].end()), (1, 3), "cells 4..6 are bytes 1..3 after the tab");
4034 assert_eq!((rows[1].start(), rows[1].end()), (10, 10), "row 1 ends at cell 4; both corners clamp");
4035 }
4036
4037 #[test]
4038 fn column_box_resolves_through_a_collapsed_inline_fold() {
4039 let mut d = doc("f([a, b]) x");
4043 assert!(d.toggle_fold_opener(2)); d.column_drag((0, 6), (0, 9));
4045 let s = d.selections().newest();
4046 assert_eq!((s.start(), s.end()), (7, 10), "cells 6..9 are `]) `, one left of the byte columns");
4047 }
4048
4049 #[test]
4050 fn column_box_spans_visible_rows_only() {
4051 let mut d = doc("aa\nfn {\nbb\ncc\n}\ndd");
4054 assert!(d.toggle_fold_opener(6)); d.column_drag((0, 0), (5, 1));
4056 let rows: Vec<u32> =
4057 d.selections().all().iter().map(|s| d.buffer().offset_to_point(s.start()).row).collect();
4058 assert_eq!(rows, vec![0, 1, 5], "one selection per VISIBLE row");
4059 }
4060
4061 #[test]
4062 fn column_select_steps_display_rows_over_a_fold() {
4063 use crate::movement::ColumnDir::Up;
4064 let mut d = doc("aa\nfn {\nbb\ncc\n}\ndd");
4067 assert!(d.toggle_fold_opener(6));
4068 d.set_selections(SelectionSet::new(16)); d.column_select(Up);
4070 let rows: Vec<u32> =
4071 d.selections().all().iter().map(|s| d.buffer().offset_to_point(s.start()).row).collect();
4072 assert_eq!(rows, vec![1, 5], "the step lands on the fold header, skipping hidden rows");
4073 }
4074
4075 #[test]
4076 fn column_select_anchors_at_the_display_cell_after_a_tab() {
4077 use crate::movement::ColumnDir::Down;
4078 let mut d = doc("\tab\nwwwwww"); d.set_selections(SelectionSet::new(1)); d.column_select(Down);
4084 assert_eq!(d.selections().newest().head(), 8, "cell 4 on row 1 is byte 4 (offset 8)");
4085 }
4086
4087 #[test]
4088 fn any_action_exits_column_mode() {
4089 use crate::movement::ColumnDir::Down;
4090 let mut d = doc("abcd\nabcd\nabcd");
4091 d.set_selections(SelectionSet::new(0));
4092 d.column_select(Down); assert_eq!(d.selections().len(), 2);
4094 d.move_carets(Motion::Right, false); d.column_select(Down);
4098 assert_eq!(d.selections().len(), 2, "re-anchored, not continuing the old box");
4099 }
4100
4101 fn range(d: &Document) -> (u32, u32) {
4102 (d.selections().newest().start(), d.selections().newest().end())
4103 }
4104
4105 #[test]
4106 fn drag_select_word_granularity_keeps_the_origin_word() {
4107 let mut d = doc("foo bar baz\nqux"); d.drag_select(Granularity::Word, 5, 5);
4110 assert_eq!(range(&d), (4, 7));
4111 d.drag_select(Granularity::Word, 5, 9);
4113 assert_eq!(range(&d), (4, 11));
4114 d.drag_select(Granularity::Word, 5, 1);
4116 assert_eq!(range(&d), (0, 7));
4117 let mut w = doc("a b");
4119 w.drag_select(Granularity::Word, 2, 2);
4120 assert!(w.selections().newest().is_empty());
4121 }
4122
4123 #[test]
4124 fn drag_select_line_and_char_granularity() {
4125 let mut d = doc("aa\nbb\ncc"); d.drag_select(Granularity::Line, 1, 1); assert_eq!(range(&d), (0, 3));
4128 d.drag_select(Granularity::Line, 1, 4); assert_eq!(range(&d), (0, 6));
4130 d.drag_select(Granularity::Line, 7, 7); assert_eq!(range(&d), (6, 8));
4132 d.drag_select(Granularity::Char, 2, 8); assert_eq!(range(&d), (2, 8));
4134 }
4135
4136 #[test]
4141 fn drag_select_clips_host_offsets() {
4142 let mut d = doc("héllo wörld"); d.drag_select(Granularity::Word, 2, 2); assert_eq!(range(&d), (0, 6), "the word around the snapped origin");
4145 d.drag_select(Granularity::Char, 2, 9); assert_eq!(range(&d), (1, 8));
4147 d.drag_select(Granularity::Char, 1000, 2);
4148 assert_eq!((range(&d), d.selections().newest().head()), ((1, 13), 1));
4149 d.drag_select(Granularity::Line, 1000, 1000);
4150 assert_eq!(range(&d), (0, 13));
4151 d.move_carets(Motion::WordLeft, true); }
4153
4154 fn head_row(d: &Document) -> u32 {
4155 d.buffer().offset_to_point(d.selections().newest().head()).row
4156 }
4157
4158 #[test]
4159 fn move_line_swaps_with_neighbour_and_rides() {
4160 let mut d = doc("aaa\nbbb\nccc");
4161 d.set_selections(SelectionSet::new(4)); d.move_line(true); assert_eq!(d.text(), "aaa\nccc\nbbb");
4164 assert_eq!(head_row(&d), 2); d.move_line(false); assert_eq!(d.text(), "aaa\nbbb\nccc");
4167 assert_eq!(head_row(&d), 1);
4168 }
4169
4170 #[test]
4171 fn move_line_is_a_noop_at_the_edges() {
4172 let mut d = doc("aaa\nbbb");
4173 d.set_selections(SelectionSet::new(0));
4174 d.move_line(false); assert_eq!(d.text(), "aaa\nbbb");
4176 d.set_selections(SelectionSet::new(4)); d.move_line(true); assert_eq!(d.text(), "aaa\nbbb");
4179 }
4180
4181 #[test]
4182 fn move_line_down_respects_the_trailing_empty_line() {
4183 let mut d = doc("aaa\nbbb\n"); d.set_selections(SelectionSet::new(4)); d.move_line(true); assert_eq!(d.text(), "aaa\nbbb\n");
4187 }
4188
4189 #[test]
4190 fn copy_line_duplicates_below_and_above() {
4191 let mut d = doc("aaa\nbbb");
4192 d.set_selections(SelectionSet::new(0));
4193 d.copy_line(true); assert_eq!(d.text(), "aaa\naaa\nbbb");
4195 assert_eq!(head_row(&d), 1);
4196 let mut d2 = doc("aaa\nbbb");
4197 d2.set_selections(SelectionSet::new(0));
4198 d2.copy_line(false); assert_eq!(d2.text(), "aaa\naaa\nbbb");
4200 assert_eq!(head_row(&d2), 0);
4201 }
4202
4203 #[test]
4204 fn copy_line_duplicates_the_final_line() {
4205 let mut d = doc("aaa\nbbb"); d.set_selections(SelectionSet::new(4)); d.copy_line(true);
4208 assert_eq!(d.text(), "aaa\nbbb\nbbb");
4209 assert_eq!(head_row(&d), 2);
4210 }
4211
4212 #[test]
4213 fn move_line_excludes_the_row_a_whole_line_selection_ends_at() {
4214 let mut d = doc("a\nb\nc\nd\n"); d.drag_select(Granularity::Line, 2, 2); assert_eq!(spans(&d), vec![(2, 4)]);
4220 d.move_line(true);
4221 assert_eq!(d.text(), "a\nc\nb\nd\n");
4222 assert_eq!(spans(&d), vec![(4, 6)], "the whole-line selection rides \"b\" down");
4223 d.move_line(false);
4224 assert_eq!(d.text(), "a\nb\nc\nd\n");
4225 assert_eq!(spans(&d), vec![(2, 4)]);
4226 }
4227
4228 #[test]
4229 fn copy_line_excludes_the_row_a_whole_line_selection_ends_at() {
4230 let mut d = doc("a\nb\nc\nd\n");
4232 d.drag_select(Granularity::Line, 2, 2); d.copy_line(true); assert_eq!(d.text(), "a\nb\nb\nc\nd\n");
4235 assert_eq!(spans(&d), vec![(4, 6)]);
4236 let mut u = doc("a\nb\nc\nd\n");
4237 u.drag_select(Granularity::Line, 2, 2);
4238 u.copy_line(false); assert_eq!(u.text(), "a\nb\nb\nc\nd\n");
4240 assert_eq!(spans(&u), vec![(2, 4)]);
4241 }
4242
4243 #[test]
4244 fn move_line_moves_every_caret_line_and_the_selections_ride() {
4245 let mut d = doc("l0\nl1\nl2\nl3\nl4\n"); d.set_selections(SelectionSet::from_ranges(&[(4, 4), (11, 9)], 0));
4250 d.move_line(true);
4251 assert_eq!(d.text(), "l0\nl2\nl1\nl4\nl3\n");
4252 assert_eq!(spans(&d), vec![(7, 7), (14, 12)], "both ride down a row, direction kept");
4253 assert_eq!(d.selections().newest().head(), 7, "the newest selection stays the newest");
4254 d.move_line(false);
4255 assert_eq!(d.text(), "l0\nl1\nl2\nl3\nl4\n");
4256 assert_eq!(spans(&d), vec![(4, 4), (11, 9)]);
4257 }
4258
4259 #[test]
4260 fn move_line_merges_overlapping_and_adjacent_blocks() {
4261 let mut d = doc("l0\nl1\nl2\nl3\nl4\n"); d.set_selections(SelectionSet::from_offsets(&[4, 7])); d.move_line(true);
4266 assert_eq!(d.text(), "l0\nl3\nl1\nl2\nl4\n");
4267 assert_eq!(spans(&d), vec![(7, 7), (10, 10)]);
4268 let mut o = doc("l0\nl1\nl2\nl3\nl4\n");
4271 o.set_selections(SelectionSet::from_ranges(&[(4, 7), (8, 8)], 1));
4272 o.move_line(false);
4273 assert_eq!(o.text(), "l1\nl2\nl0\nl3\nl4\n");
4274 assert_eq!(spans(&o), vec![(1, 4), (5, 5)]);
4275 }
4276
4277 #[test]
4278 fn move_line_leaves_an_edge_block_and_moves_the_rest() {
4279 let mut d = doc("l0\nl1\nl2\nl3");
4280 d.set_selections(SelectionSet::from_offsets(&[1, 7])); d.move_line(false); assert_eq!(d.text(), "l0\nl2\nl1\nl3");
4283 assert_eq!(spans(&d), vec![(1, 1), (4, 4)]);
4284 let mut w = doc("a\nb\nc");
4287 w.drag_select(Granularity::Line, 2, 2); w.move_line(true);
4289 assert_eq!(w.text(), "a\nc\nb");
4290 assert_eq!(spans(&w), vec![(4, 5)]);
4291 }
4292
4293 #[test]
4294 fn copy_line_copies_every_caret_line_and_the_selections_land_on_the_copies() {
4295 let src = "l0\nl1\nl2\nl3\nl4\n"; let both = "l0\nl1\nl1\nl2\nl3\nl3\nl4\n";
4299 let mut d = doc(src);
4300 d.set_selections(SelectionSet::from_ranges(&[(4, 4), (11, 9)], 0));
4302 d.copy_line(true); assert_eq!(d.text(), both);
4304 assert_eq!(spans(&d), vec![(7, 7), (17, 15)], "both land on their copies, direction kept");
4305 assert_eq!(d.selections().newest().head(), 7, "the newest selection stays the newest");
4306 let mut u = doc(src);
4307 u.set_selections(SelectionSet::from_ranges(&[(4, 4), (11, 9)], 0));
4308 u.copy_line(false); assert_eq!(u.text(), both);
4310 assert_eq!(spans(&u), vec![(4, 4), (14, 12)]);
4311 }
4312
4313 #[test]
4314 fn copy_line_merges_blocks_that_share_a_row() {
4315 let mut o = doc("l0\nl1\nl2\nl3\nl4\n");
4318 o.set_selections(SelectionSet::from_ranges(&[(4, 7), (8, 8)], 1));
4319 o.copy_line(true);
4320 assert_eq!(o.text(), "l0\nl1\nl2\nl1\nl2\nl3\nl4\n");
4321 assert_eq!(spans(&o), vec![(10, 13), (14, 14)]);
4322 let mut a = doc("l0\nl1\nl2\nl3\nl4\n");
4325 a.set_selections(SelectionSet::from_offsets(&[4, 7])); a.copy_line(true);
4327 assert_eq!(a.text(), "l0\nl1\nl1\nl2\nl2\nl3\nl4\n");
4328 assert_eq!(spans(&a), vec![(7, 7), (13, 13)]);
4329 }
4330
4331 #[test]
4332 fn a_multi_block_move_or_copy_undoes_in_one_step() {
4333 let src = "l0\nl1\nl2\nl3\nl4\n";
4334 let before = vec![(4, 4), (11, 9)];
4335 for copy in [false, true] {
4336 let mut d = doc(src);
4337 d.set_selections(SelectionSet::from_ranges(&[(4, 4), (11, 9)], 0));
4338 let depth = d.undo_depth();
4339 if copy {
4340 d.copy_line(true);
4341 } else {
4342 d.move_line(true);
4343 }
4344 assert_eq!(d.undo_depth(), depth + 1, "copy={copy}: both blocks, one unit");
4345 let (after_text, after) = (d.text().into_owned(), spans(&d));
4346 assert!(d.undo());
4347 assert_eq!(d.text(), src, "copy={copy}: one undo restores the text");
4348 assert_eq!(spans(&d), before, "copy={copy}: …and every selection");
4349 assert!(d.redo());
4350 assert_eq!((d.text().into_owned(), spans(&d)), (after_text, after), "copy={copy}");
4351 }
4352 }
4353
4354 #[test]
4355 fn move_and_copy_line_carry_every_selection_with_its_text() {
4356 let mut rng = 0x5E1E_C7ED_0007_u64;
4364 let mut next = move |m: u64| {
4365 rng ^= rng << 13;
4366 rng ^= rng >> 7;
4367 rng ^= rng << 17;
4368 rng % m
4369 };
4370 const LINES: &[&str] = &["", "a", "bc", "é", "x y", " {"];
4371 let sorted_lines = |t: &str| {
4372 let mut v: Vec<String> = t.split('\n').map(str::to_owned).collect();
4373 v.sort();
4374 v
4375 };
4376 let line_at = |t: &str, off: u32| {
4378 let start = t[..off as usize].rfind('\n').map_or(0, |i| i + 1);
4379 let end = t[start..].find('\n').map_or(t.len(), |i| start + i);
4380 (t[start..end].to_owned(), off as usize - start)
4381 };
4382 let mut checked = 0;
4383 for step in 0..3_000 {
4384 let n = 1 + next(6);
4385 let mut src: String =
4386 (0..n).map(|_| LINES[next(LINES.len() as u64) as usize]).collect::<Vec<_>>().join("\n");
4387 if next(2) == 0 {
4388 src.push('\n');
4389 }
4390 let mut d = doc(&src);
4391 let len = u64::from(d.buffer().len()) + 1;
4392 let k = 1 + next(3) as usize;
4393 let raw: Vec<(u32, u32)> = (0..k)
4394 .map(|_| {
4395 let a = next(len) as u32;
4396 (a, if next(2) == 0 { a } else { next(len) as u32 })
4397 })
4398 .collect();
4399 let clip = |o: u32| d.buffer().clip_offset(o, Bias::Left);
4400 let ranges: Vec<(u32, u32)> = raw.iter().map(|&(a, b)| (clip(a), clip(b))).collect();
4401 d.set_selections(SelectionSet::from_ranges(&ranges, next(k as u64) as usize));
4402 let before = d.selections().all().to_vec();
4403 let depth = d.undo_depth();
4404 let (copy, down) = (next(2) == 0, next(2) == 0);
4405 if copy {
4406 d.copy_line(down);
4407 } else {
4408 d.move_line(down);
4409 }
4410 let text = d.text().into_owned();
4411 let ctx = format!("step {step}: {src:?} {before:?} copy={copy} down={down} → {text:?}");
4412 if copy {
4413 let mut added = sorted_lines(&text);
4414 for line in sorted_lines(&src) {
4415 let i = added.iter().position(|l| *l == line).unwrap_or_else(|| panic!("{ctx}"));
4416 added.remove(i);
4417 }
4418 assert!(!added.is_empty(), "{ctx}: a copy adds lines");
4419 } else {
4420 assert_eq!(sorted_lines(&text), sorted_lines(&src), "{ctx}: a move permutes lines");
4421 }
4422 let carried = d.selections().len() == before.len();
4426 for s in before.iter().filter(|_| carried) {
4427 let t = d.selections().all().iter().find(|t| t.id == s.id).expect("ids are kept");
4428 checked += 1;
4429 let was = &src[s.start() as usize..s.end() as usize];
4430 let now = &text[t.start() as usize..t.end() as usize];
4431 let dropped_nl = was.strip_suffix('\n') == Some(now) && t.end() as usize == text.len();
4432 assert!(now == was || dropped_nl, "{ctx}: {s:?} → {t:?} selects {now:?}, not {was:?}");
4433 if !t.is_empty() {
4434 assert_eq!(t.head() < t.tail(), s.head() < s.tail(), "{ctx}: direction kept");
4435 }
4436 if s.is_empty() {
4437 assert_eq!(line_at(&text, t.head()), line_at(&src, s.head()), "{ctx}: caret rides");
4438 }
4439 }
4440 if d.undo_depth() > depth {
4441 assert!(d.undo());
4442 assert_eq!(d.text(), src, "{ctx}: one undo restores the text");
4443 assert_eq!(d.selections().all(), &before[..], "{ctx}: …and the selections");
4444 } else {
4445 assert_eq!(text, src, "{ctx}: no unit, no change");
4446 }
4447 }
4448 assert!(checked > 3_000, "the walk checks enough selections ({checked})");
4449 }
4450
4451 #[test]
4452 fn add_caret_adds_a_second_cursor() {
4453 let mut d = doc("abcdef");
4454 assert_eq!(d.selections().len(), 1);
4455 d.add_caret(3);
4456 assert_eq!(d.selections().len(), 2);
4457 }
4458
4459 fn heads(d: &Document) -> Vec<u32> {
4460 d.selections().all().iter().map(|s| s.head()).collect()
4461 }
4462
4463 #[test]
4468 fn set_selections_snaps_a_mid_char_caret_so_motion_cannot_panic() {
4469 let mut d = doc("é");
4470 d.set_selections(SelectionSet::new(1));
4471 assert_eq!(heads(&d), [0], "a caret inside a char snaps Left, to its start");
4472 d.move_carets(Motion::Right, false);
4473 assert_eq!(heads(&d), [2]);
4474 }
4475
4476 #[test]
4480 fn set_selections_clamps_an_out_of_range_caret() {
4481 let mut d = doc("abc");
4482 d.set_selections(SelectionSet::new(1000));
4483 assert_eq!(heads(&d), [3]);
4484 d.move_carets(Motion::Left, false);
4485 assert_eq!(heads(&d), [2]);
4486 }
4487
4488 #[test]
4492 fn set_selections_snaps_ranges_outward_and_merges_clamped_carets() {
4493 let mut d = doc("aéb"); d.set_selections(SelectionSet::from_ranges(&[(2, 0)], 0)); let s = *d.selections().newest();
4496 assert_eq!((s.start(), s.end(), s.head()), (0, 3, 0), "grows over the whole 'é', head kept");
4497 d.set_selections(SelectionSet::from_offsets(&[1, 50, 99]));
4498 assert_eq!(heads(&d), [1, 4], "the two past-end carets merge at the end");
4499 }
4500
4501 #[test]
4504 fn add_caret_clips_a_host_offset() {
4505 let mut d = doc("é\né");
4506 d.add_caret(1); d.add_caret(4); d.add_caret(1000); assert_eq!(heads(&d), [0, 3, 5]);
4510 d.move_carets(Motion::Right, false);
4511 assert_eq!(heads(&d), [2, 5]);
4512 }
4513
4514 #[test]
4523 fn host_positions_never_leave_the_char_grid() {
4524 let text = "aé🦀b\n\t日本語 x\n\n🦀🦀 é\nzz";
4525 let mut d = doc(text);
4526 let len = d.buffer().len();
4527 let mut state = 0x5EED_2002_u64; let mut rand = move |bound: u64| {
4529 state = state.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
4530 ((state >> 33) % bound.max(1)) as u32
4531 };
4532 let mut raw = move || if rand(16) == 0 { u32::MAX } else { rand(u64::from(len) + 8) };
4535 let motions = [
4536 Motion::Left, Motion::Right, Motion::Up, Motion::Down, Motion::WordLeft, Motion::WordRight,
4537 Motion::LineStart, Motion::LineEnd, Motion::PageUp(2), Motion::PageDown(2),
4538 ];
4539 let assert_on_grid = |d: &Document, step: usize, what: &str| {
4540 for s in d.selections().all() {
4541 for off in [s.start(), s.end()] {
4542 assert!(text.is_char_boundary(off as usize), "step {step} ({what}): selection end {off}");
4543 }
4544 }
4545 for (span, ..) in d.diagnostics_in(0..u32::MAX) {
4546 for off in [span.start, span.end] {
4547 assert!(text.is_char_boundary(off as usize), "step {step} ({what}): diagnostic end {off}");
4548 }
4549 }
4550 };
4551 let mut mid_char_inputs = 0;
4552 for step in 0..2000 {
4553 let (a, b) = (raw(), raw());
4554 mid_char_inputs += usize::from(!text.is_char_boundary(a as usize) && (a as usize) < text.len());
4555 let what = match step % 6 {
4556 0 => {
4557 d.set_selections(SelectionSet::from_ranges(&[(a, b), (b, raw())], 0));
4558 "set_selections"
4559 }
4560 1 => {
4561 d.add_caret(a);
4562 "add_caret"
4563 }
4564 2 => {
4565 let g = [Granularity::Char, Granularity::Word, Granularity::Line][step % 3];
4566 d.drag_select(g, a, b);
4567 "drag_select"
4568 }
4569 3 => {
4570 d.column_drag((a % 9, b), (b % 9, a));
4572 "column_drag"
4573 }
4574 4 => {
4575 let rev = d.revision();
4576 let diags = vec![
4577 Diagnostic::new(a..b, Severity::Error, "x"),
4578 Diagnostic::new(b..b, Severity::Warning, "y"),
4579 ];
4580 assert_eq!(d.set_diagnostics(rev, diags), DiagnosticsOutcome::Applied { count: 2 });
4581 "set_diagnostics"
4582 }
4583 _ => {
4584 d.set_selections(SelectionSet::from_offsets(&[a, b]));
4585 "set_selections (carets)"
4586 }
4587 };
4588 assert_on_grid(&d, step, what);
4589 d.move_carets(motions[step % motions.len()], step % 4 == 0);
4590 assert_on_grid(&d, step, "the motion after it");
4591 }
4592 assert!(mid_char_inputs >= 200, "only {mid_char_inputs} mid-char inputs");
4594 }
4595
4596 #[test]
4597 fn collapse_returns_to_the_primary_caret() {
4598 let mut d = doc("foo foo foo");
4599 d.add_next_occurrence(); d.add_next_occurrence(); assert!(d.selections().len() >= 2);
4602 d.collapse_selections();
4603 assert_eq!(d.selections().len(), 1);
4604 assert!(d.selections().newest().is_empty());
4605 }
4606
4607 #[test]
4608 fn find_next_occurrence_scans_forward_then_wraps() {
4609 assert_eq!(super::find_next_occurrence("aXaYa", "a", 1, &[(0, 1)]), Some((2, 3)));
4611 assert_eq!(super::find_next_occurrence("aXaYa", "a", 5, &[(2, 3), (4, 5)]), Some((0, 1)));
4613 assert_eq!(super::find_next_occurrence("aa", "a", 0, &[(0, 1), (1, 2)]), None);
4615 }
4616
4617 #[test]
4618 fn dirty_after_undo_then_divergent_edit() {
4619 let mut d = doc("abc");
4622 d.edit(vec![EditOp::delete(2..3)]).unwrap(); d.mark_saved();
4624 d.undo(); assert!(d.is_dirty(), "undone away from the save point → dirty");
4626 d.edit(vec![EditOp::delete(0..1)]).unwrap(); assert!(d.is_dirty(), "divergent edit is still dirty, never falsely clean");
4628 }
4629
4630 #[test]
4631 fn highlight_cache_splice_tracks_line_count_across_edits() {
4632 const G: &str = "%YAML 1.2\n---\nname: T\nscope: source.t\ncontexts:\n main:\n - match: '\\w+'\n scope: keyword.t\n";
4634 const TH: &str = r#"<?xml version="1.0" encoding="UTF-8"?>
4635<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
4636<plist version="1.0"><dict><key>settings</key><array><dict><key>settings</key><dict><key>foreground</key><string>#FFFFFF</string></dict></dict></array></dict></plist>"#;
4637 let mut d = doc("ab\ncd");
4638 d.set_syntax(
4639 crate::SyntaxDef::from_sublime_syntax(G).unwrap(),
4640 crate::TokenTheme::from_tm_theme(TH).unwrap(),
4641 );
4642 d.tokenize_highlight(d.buffer().line_count());
4643 assert!(d.highlight_line_spans(1).is_some());
4644 d.set_selections(SelectionSet::new(2)); d.enter();
4647 d.tokenize_highlight(d.buffer().line_count());
4648 assert_eq!(d.buffer().line_count(), 3);
4649 assert!(d.highlight_line_spans(2).is_some(), "the new last line is tokenized");
4650 d.backspace();
4652 d.tokenize_highlight(d.buffer().line_count());
4653 assert_eq!(d.buffer().line_count(), 2);
4654 assert!(d.highlight_line_spans(1).is_some());
4655 assert!(d.highlight_line_spans(2).is_none(), "no spans past the buffer");
4656 }
4657
4658 #[test]
4659 fn multi_op_transaction_highlight_equals_a_fresh_document() {
4660 const G: &str = "%YAML 1.2\n---\nname: T\nscope: source.t\ncontexts:\n main:\n - match: '\\w+'\n scope: keyword.t\n";
4667 const TH: &str = r#"<?xml version="1.0" encoding="UTF-8"?>
4668<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
4669<plist version="1.0"><dict><key>settings</key><array><dict><key>settings</key><dict><key>foreground</key><string>#FFFFFF</string></dict></dict></array></dict></plist>"#;
4670 let make = |text: &str| -> Document {
4671 let mut d = doc(text);
4672 d.set_syntax(
4673 crate::SyntaxDef::from_sublime_syntax(G).unwrap(),
4674 crate::TokenTheme::from_tm_theme(TH).unwrap(),
4675 );
4676 d.tokenize_highlight(u32::MAX);
4677 d
4678 };
4679 let mut d = make("aa bb cc\ndd ee ff\ngg hh ii\njj kk ll\nmm nn oo");
4682 d.edit_grouped(
4686 vec![
4687 EditOp::new(0..2, "XYZ"), EditOp::new(9..11, "Q"), EditOp::new(15..17, "RR"), EditOp::insert(27, "NEW\n"), ],
4692 typ(OpClass::Type),
4693 )
4694 .unwrap();
4695 d.tokenize_highlight(u32::MAX);
4696
4697 let text = d.text();
4698 let f = make(&text);
4699 assert_eq!(d.buffer().line_count(), f.buffer().line_count(), "line count");
4700 for r in 0..d.buffer().line_count() {
4701 assert_eq!(
4702 d.highlight_line_spans(r),
4703 f.highlight_line_spans(r),
4704 "row {r} live (per-edit invalidation) vs fresh"
4705 );
4706 }
4707 }
4708
4709 #[test]
4714 fn set_syntax_preserves_the_highlight_window_aim() {
4715 const G: &str = "%YAML 1.2\n---\nname: T\nscope: source.t\ncontexts:\n main:\n - match: '\\w+'\n scope: keyword.t\n";
4716 const TH: &str = r#"<?xml version="1.0" encoding="UTF-8"?>
4717<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
4718<plist version="1.0"><dict><key>settings</key><array><dict><key>settings</key><dict><key>foreground</key><string>#FFFFFF</string></dict></dict></array></dict></plist>"#;
4719 let syntax = || crate::SyntaxDef::from_sublime_syntax(G).unwrap();
4720 let theme = || crate::TokenTheme::from_tm_theme(TH).unwrap();
4721 let mut d = doc(&"word\n".repeat(3_000));
4722 d.set_syntax(syntax(), theme());
4723 d.set_highlight_window(2_000..2_040);
4725 d.set_syntax(syntax(), theme());
4727 while d.highlight_frontier().is_some() {
4728 d.tokenize_highlight(u32::MAX);
4729 }
4730 assert!(
4731 d.highlight_line_spans(2_020).is_some(),
4732 "the swapped cache must still retain the viewport rows"
4733 );
4734 }
4735
4736 const HL_G: &str = "%YAML 1.2\n---\nname: T\nscope: source.t\ncontexts:\n main:\n - match: '\\bkw\\b'\n scope: keyword.t\n";
4741 const HL_TH: &str = r#"<?xml version="1.0" encoding="UTF-8"?>
4742<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
4743<plist version="1.0"><dict><key>settings</key><array><dict><key>settings</key><dict><key>foreground</key><string>#FFFFFF</string></dict></dict></array></dict></plist>"#;
4744
4745 fn doc_with_syntax(n: usize) -> Document {
4746 let mut d = doc(&"kw word\n".repeat(n));
4747 d.set_syntax(
4748 crate::SyntaxDef::from_sublime_syntax(HL_G).unwrap(),
4749 crate::TokenTheme::from_tm_theme(HL_TH).unwrap(),
4750 );
4751 d
4752 }
4753
4754 #[test]
4755 fn absorb_highlight_drops_a_stale_revision() {
4756 let mut d = doc_with_syntax(2_000);
4757 let engine = d.highlight_engine().unwrap();
4758 let snap = d.snapshot();
4759 let stale_rev = d.revision();
4760 let seg = crate::tokenize_segment(&engine, &snap, 0..2_001, crate::SegmentStart::Fresh, None, None);
4761 d.set_selections(SelectionSet::new(0));
4763 d.type_char('x');
4764 assert_ne!(d.revision(), stale_rev);
4765 assert!(!d.absorb_highlight(stale_rev, seg, true), "stale segment must be dropped");
4767 assert_eq!(d.highlight_frontier(), Some(0), "the frontier is untouched by a dropped absorb");
4768 }
4769
4770 #[test]
4771 fn absorb_highlight_verified_clears_the_frontier() {
4772 let mut d = doc_with_syntax(5_000);
4773 let engine = d.highlight_engine().unwrap();
4774 let snap = d.snapshot();
4775 let rev = d.revision();
4776 let n = d.buffer().line_count();
4777 let seg =
4781 crate::tokenize_segment(&engine, &snap, 0..n, crate::SegmentStart::Fresh, Some(0..n), None);
4782 assert!(d.absorb_highlight(rev, seg, true));
4783 assert_eq!(d.highlight_frontier(), None, "verified absorb clears every dirty row");
4784 }
4785
4786 #[test]
4787 fn absorb_highlight_speculative_shows_spans_but_keeps_dirt() {
4788 let mut d = doc_with_syntax(2_000);
4789 d.set_highlight_window(900..940);
4790 let engine = d.highlight_engine().unwrap();
4791 let snap = d.snapshot();
4792 let rev = d.revision();
4793 let seg = crate::tokenize_segment(
4795 &engine,
4796 &snap,
4797 772..940,
4798 crate::SegmentStart::Fresh,
4799 Some(900..940),
4800 None,
4801 );
4802 assert!(d.absorb_highlight(rev, seg, false));
4803 assert!(d.highlight_line_spans(920).is_some(), "speculative spans are visible");
4805 assert_eq!(d.highlight_frontier(), Some(0), "speculation keeps the frontier");
4807 while d.highlight_frontier().is_some() {
4809 d.tokenize_highlight(u32::MAX);
4810 }
4811 assert!(d.highlight_line_spans(920).is_some());
4812 }
4813
4814 fn keyword_theme(keyword_hex: &str) -> crate::TokenTheme {
4820 crate::TokenTheme::from_tm_theme(&format!(
4821 r#"<?xml version="1.0" encoding="UTF-8"?>
4822<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
4823<plist version="1.0"><dict><key>settings</key><array>
4824<dict><key>settings</key><dict><key>foreground</key><string>#FFFFFF</string></dict></dict>
4825<dict><key>scope</key><string>keyword</string><key>settings</key><dict><key>foreground</key><string>{keyword_hex}</string></dict></dict>
4826</array></dict></plist>"#
4827 ))
4828 .unwrap()
4829 }
4830 const HL_RED: crate::Rgba = crate::Rgba { r: 0xff, g: 0, b: 0, a: 0xff };
4831 const HL_GREEN: crate::Rgba = crate::Rgba { r: 0, g: 0xff, b: 0, a: 0xff };
4832
4833 fn color_at(d: &Document, row: u32, col: u32) -> crate::Rgba {
4835 d.highlight_line_spans(row)
4836 .unwrap_or_else(|| panic!("row {row} has spans"))
4837 .iter()
4838 .find(|s| s.range.start == col)
4839 .unwrap_or_else(|| panic!("row {row} has a span at byte {col}"))
4840 .style
4841 .fg
4842 }
4843
4844 fn drive_highlight(d: &mut Document) {
4845 while d.highlight_frontier().is_some() {
4846 d.tokenize_highlight(u32::MAX);
4847 }
4848 }
4849
4850 #[test]
4855 fn absorb_highlight_rejects_a_segment_from_the_previous_theme() {
4856 let mut d = doc(&"kw word\n".repeat(2_000));
4857 d.set_syntax(crate::SyntaxDef::from_sublime_syntax(HL_G).unwrap(), keyword_theme("#FF0000"));
4858 let engine = d.highlight_engine().unwrap();
4859 let snap = d.snapshot();
4860 let rev = d.revision();
4861 let seg =
4862 crate::tokenize_segment(&engine, &snap, 0..600, crate::SegmentStart::Fresh, Some(0..600), None);
4863 d.set_theme(keyword_theme("#00FF00"));
4864 assert!(!d.absorb_highlight(rev, seg, true), "an old-theme segment must be rejected");
4865 assert_eq!(d.highlight_frontier(), Some(0), "a rejected absorb must not move the frontier");
4866 drive_highlight(&mut d);
4867 assert_eq!(color_at(&d, 10, 0), HL_GREEN, "row 10's keyword repaints in the new theme");
4868 }
4869
4870 #[test]
4874 fn absorb_highlight_rejects_a_segment_from_the_previous_grammar() {
4875 const WORD_G: &str = "%YAML 1.2\n---\nname: W\nscope: source.w\ncontexts:\n main:\n - match: '\\bword\\b'\n scope: keyword.w\n";
4876 let mut d = doc(&"kw word\n".repeat(2_000));
4877 d.set_syntax(crate::SyntaxDef::from_sublime_syntax(HL_G).unwrap(), keyword_theme("#FF0000"));
4878 let engine = d.highlight_engine().unwrap();
4879 let snap = d.snapshot();
4880 let rev = d.revision();
4881 let seg =
4882 crate::tokenize_segment(&engine, &snap, 0..600, crate::SegmentStart::Fresh, Some(0..600), None);
4883 d.set_syntax(crate::SyntaxDef::from_sublime_syntax(WORD_G).unwrap(), keyword_theme("#FF0000"));
4884 assert!(!d.absorb_highlight(rev, seg, true), "an old-grammar segment must be rejected");
4885 assert_eq!(d.highlight_frontier(), Some(0), "a rejected absorb must not move the frontier");
4886 drive_highlight(&mut d);
4887 assert_eq!(color_at(&d, 10, 3), HL_RED, "`word` is the new grammar's keyword");
4888 assert_ne!(color_at(&d, 10, 0), HL_RED, "`kw` is plain text under the new grammar");
4889 }
4890
4891 #[test]
4897 fn absorb_highlight_rejects_a_segment_built_on_the_previous_engines_output() {
4898 let mut d = doc(&"kw word\n".repeat(2_000));
4899 d.set_syntax(crate::SyntaxDef::from_sublime_syntax(HL_G).unwrap(), keyword_theme("#FF0000"));
4900 let old = d.highlight_engine().unwrap();
4901 let snap = d.snapshot();
4902 let rev = d.revision();
4903 let old_prefix = crate::tokenize_segment(&old, &snap, 0..300, crate::SegmentStart::Fresh, None, None);
4904 let old_guess =
4905 crate::tokenize_segment(&old, &snap, 300..800, crate::SegmentStart::Fresh, Some(300..800), None);
4906 d.set_theme(keyword_theme("#00FF00"));
4907 let new = d.highlight_engine().unwrap();
4908
4909 let after_old = crate::SegmentStart::After(old_prefix.end_boundary().clone());
4911 let chained = crate::tokenize_segment(&new, &snap, 300..800, after_old, Some(300..800), None);
4912 assert!(!d.absorb_highlight(rev, chained, true), "resumed from an old-engine boundary");
4913
4914 let new_prefix = crate::tokenize_segment(&new, &snap, 0..300, crate::SegmentStart::Fresh, None, None);
4919 let after_new = crate::SegmentStart::After(new_prefix.end_boundary().clone());
4920 let rerun = crate::tokenize_segment(&new, &snap, 300..800, after_new, None, Some(&old_guess));
4921 assert!(!d.absorb_highlight(rev, rerun, true), "spliced from an old-engine segment");
4922
4923 assert_eq!(d.highlight_frontier(), Some(0), "nothing was absorbed");
4924 assert!(d.absorb_highlight(rev, new_prefix, true), "the current engine's own segment");
4926 }
4927
4928 #[test]
4929 fn diagnostics_install_then_ride_a_forward_edit() {
4930 use crate::{Diagnostic, DiagnosticsOutcome, Severity};
4931 let mut d = doc("let x = 1;");
4932 let rev = d.revision();
4933 let out = d.set_diagnostics(rev, vec![Diagnostic::new(4..5, Severity::Error, "unused")]);
4934 assert!(matches!(out, DiagnosticsOutcome::Applied { count: 1 }));
4935 let got: Vec<_> = d.diagnostics_in(0..100).collect();
4936 assert_eq!((got.len(), got[0].0.clone(), got[0].1), (1, 4..5, Severity::Error));
4937 d.edit(vec![EditOp::insert(0, "abcd")]).unwrap();
4939 assert_eq!(d.diagnostics_in(0..100).next().unwrap().0, 8..9, "rode via apply_patch");
4940 }
4941
4942 #[test]
4943 fn stale_diagnostic_set_is_dropped_and_the_prior_kept() {
4944 use crate::{Diagnostic, DiagnosticsOutcome, Severity};
4945 let mut d = doc("abcdef");
4946 let rev = d.revision();
4947 d.set_diagnostics(rev, vec![Diagnostic::new(0..1, Severity::Warning, "w")]);
4948 d.edit(vec![EditOp::insert(0, "z")]).unwrap(); let out = d.set_diagnostics(rev, vec![Diagnostic::new(3..4, Severity::Error, "e")]);
4950 assert!(matches!(out, DiagnosticsOutcome::Stale { .. }), "old-revision set dropped");
4951 let got: Vec<_> = d.diagnostics_in(0..100).collect();
4952 assert_eq!((got.len(), got[0].0.clone(), got[0].1), (1, 1..2, Severity::Warning));
4953 }
4954
4955 #[test]
4956 fn diagnostic_spans_clip_to_the_buffer_length() {
4957 use crate::{Diagnostic, Severity};
4958 let mut d = doc("abc"); let rev = d.revision();
4960 d.set_diagnostics(rev, vec![Diagnostic::new(2..99, Severity::Error, "past eof")]);
4961 assert_eq!(d.diagnostics_in(0..100).next().unwrap().0, 2..3, "clipped to buffer len");
4962 }
4963
4964 #[test]
4970 fn diagnostic_spans_snap_to_char_boundaries() {
4971 use crate::{Diagnostic, Severity};
4972 let mut d = doc("é\néé"); let rev = d.revision();
4974 let diag = |span| Diagnostic::new(span, Severity::Error, "x");
4975 d.set_diagnostics(rev, vec![diag(1..3), diag(4..4), diag(6..900)]);
4976 let spans: Vec<Range<u32>> = d.diagnostics_in(0..u32::MAX).map(|(r, ..)| r).collect();
4977 assert_eq!(spans, [0..3, 3..3, 5..7]);
4978 for want in [0..3, 3..3, 5..7] {
4980 assert_eq!(d.next_diagnostic(true), Some(want.clone()));
4981 assert_eq!(range(&d), (want.start, want.end));
4982 d.move_carets(Motion::Right, true);
4983 }
4984 }
4985
4986 #[test]
4987 fn host_decorations_snap_to_char_boundaries() {
4988 use crate::{Diagnostic, DiagnosticsOutcome};
4994 let mut d = doc("éé"); let stop = |index| DecorationKind::SnippetStop { index };
4996 let range_of = |d: &Document, id| d.decorations().decoration_range(id);
4997 let a = d.decorations_mut().add_decoration(1..3, stop(0), Stickiness::NeverGrows);
4998 let b = d.decorations_mut().add_decoration(3..900, stop(1), Stickiness::NeverGrows);
4999 let c = d.decorations_mut().add_decoration(1..1, stop(2), Stickiness::AlwaysGrows);
5000 assert_eq!(range_of(&d, a), Some(0..4), "a mid-char span snaps outward to whole chars");
5001 assert_eq!(range_of(&d, b), Some(2..4), "a past-end end clamps to the end");
5002 assert_eq!(range_of(&d, c), Some(0..0), "an empty span stays empty, at its char's start");
5003 let batch = d.decorations_mut().add_sorted_batch([1..3, 5..9], stop(3), Stickiness::NeverGrows);
5005 let spliced = d.decorations_mut().splice_sorted_batch(&[3..3, 9..9], stop(4), Stickiness::NeverGrows);
5006 let ranges: Vec<_> = batch.iter().chain(&spliced).map(|&id| range_of(&d, id)).collect();
5007 assert_eq!(ranges, [Some(0..4), Some(4..4), Some(2..2), Some(4..4)]);
5008 let rev = d.revision().0;
5009 let diags = vec![Diagnostic::new(1..3, Severity::Error, "x")];
5010 assert_eq!(d.decorations_mut().set_diagnostics(rev, rev, diags), DiagnosticsOutcome::Applied { count: 1 });
5011 let spans: Vec<Range<u32>> = d.diagnostics_in(0..u32::MAX).map(|(r, ..)| r).collect();
5012 assert_eq!((spans.len(), spans.first().cloned()), (1, Some(0..4)));
5013 d.edit(vec![EditOp::insert(0, "a")]).unwrap(); let e = d.decorations_mut().add_decoration(2..4, stop(5), Stickiness::NeverGrows);
5017 assert_eq!(range_of(&d, e), Some(1..5));
5018 let mut lone = DecorationStore::new();
5020 let f = lone.add_decoration(1..3, stop(6), Stickiness::NeverGrows);
5021 assert_eq!(lone.decoration_range(f), Some(1..3));
5022 }
5023
5024 #[test]
5025 fn a_host_fold_opener_is_clipped_to_the_buffer() {
5026 let mut d = doc("a{b}");
5030 assert!(d.toggle_fold_opener(1000));
5031 let len = d.buffer().len();
5032 assert!(d.folds().iter().all(|o| o <= len), "a fold past the end: {:?}", d.folds().iter().collect::<Vec<_>>());
5033 assert!(d.toggle_fold_opener(1000), "the same host offset toggles the clipped fold back");
5034 assert!(d.folds().is_empty());
5035 let mut e = doc("é{\n}");
5036 assert!(e.toggle_fold_opener(1));
5037 let text = e.text().into_owned();
5038 assert!(e.folds().iter().all(|o| text.is_char_boundary(o as usize)), "a fold inside a char");
5039 }
5040
5041 #[test]
5042 fn rend04_diagnostic_render_row_shifts_when_lines_inserted_above() {
5043 use crate::{Diagnostic, Point, Severity};
5047 let mut d = doc("L0\nL1\nL2\nL3\nL4\nL5\nL6\nL7\nL8\nL9");
5048 let row5 = d.buffer().point_to_offset(Point { row: 5, col: 0 });
5049 d.set_diagnostics(d.revision(), vec![Diagnostic::new(row5..row5 + 2, Severity::Error, "e")]);
5050 let start = d.diagnostics_in(0..u32::MAX).next().unwrap().0.start;
5051 assert_eq!(d.buffer().offset_to_point(start).row, 5);
5052 d.edit(vec![EditOp::insert(0, "a\nb\nc\n")]).unwrap();
5054 let span = d.diagnostics_in(0..u32::MAX).next().unwrap().0;
5055 assert_eq!(d.buffer().offset_to_point(span.start).row, 8, "5 + 3 inserted lines");
5056 assert_eq!(&d.text()[span.start as usize..span.end as usize], "L5", "still glued to its code");
5057 }
5058
5059 #[test]
5065 fn windowed_fold_queries_match_the_whole_document() {
5066 let mut d = doc(
5067 "top\n\
5068 a {\n b [1, 2]\n c {\n d\n }\n}\n\
5069 mid\n\
5070 e (x,\n y)\n\
5071 f {\n g\n}\n\
5072 end",
5073 );
5074 let n = d.buffer().line_count();
5075 for row in 0..n {
5077 let want = d
5078 .foldable_pairs()
5079 .into_iter()
5080 .filter(|&(_, _, h, l)| h == row && l > h)
5081 .max_by_key(|&(_, _, h, l)| l - h)
5082 .map(|(o, ..)| o);
5083 assert_eq!(d.block_opener_on_row(row), want, "block_opener_on_row row {row}");
5084 }
5085 assert_eq!(d.collapsible_pairs_in_rows(0..n), d.collapsible_pairs());
5087 for (a, b) in [(0u32, 3u32), (2, 6), (3, 4), (7, n)] {
5089 let want: Vec<_> =
5090 d.collapsible_pairs().into_iter().filter(|&(_, _, h, _)| a <= h && h < b).collect();
5091 assert_eq!(d.collapsible_pairs_in_rows(a..b), want, "collapsible_pairs_in_rows {a}..{b}");
5092 }
5093 for row in 0..n {
5095 let windowed: Vec<_> =
5096 d.foldable_ranges_in_rows(row..row + 1).into_iter().filter(|(h, _)| h.0 == row).collect();
5097 let whole: Vec<_> =
5098 d.foldable_ranges().into_iter().filter(|(h, _)| h.0 == row).collect();
5099 assert_eq!(windowed, whole, "foldable_ranges row {row}");
5100 }
5101 let opener = d.block_opener_on_row(1).unwrap();
5103 assert!(d.toggle_fold_opener(opener));
5104 assert_eq!(d.collapsible_pairs_in_rows(0..n), d.collapsible_pairs());
5105 }
5106
5107 #[test]
5108 fn fold_survives_undo_redo() {
5109 use crate::{fold_map::FoldMap, BufferRow};
5114 let mut d = doc("L0\nL1\nL2\nL3\nL4\nblk {\n x\n y\n}\nL9");
5115 assert_eq!(d.foldable_ranges().iter().map(|(h, l)| (h.0, l.0)).collect::<Vec<_>>(), vec![(5, 8)]);
5116 assert!(d.fold(BufferRow(5), BufferRow(8)));
5117 let header = |d: &Document| FoldMap::new(d.folds(), d.brackets(), d.buffer()).fold_at_header(BufferRow(5));
5118 let header8 = |d: &Document| FoldMap::new(d.folds(), d.brackets(), d.buffer()).fold_at_header(BufferRow(8));
5119 assert_eq!(header(&d), Some(BufferRow(8)));
5120 d.edit(vec![EditOp::insert(0, "a\nb\nc\n")]).unwrap();
5122 assert_eq!(header8(&d), Some(BufferRow(11)));
5123 assert!(d.undo());
5125 assert_eq!(header(&d), Some(BufferRow(8)));
5126 assert!(d.redo());
5127 assert_eq!(header8(&d), Some(BufferRow(11)));
5128 }
5129
5130 #[test]
5131 fn pasting_after_the_collapsed_line_keeps_the_outer_fold() {
5132 use crate::{fold_map::FoldMap, BufferRow, SelectionSet};
5133 let mut d = doc("{\n\t{\n\t}\n}\n");
5135 assert_eq!(d.foldable_ranges().iter().map(|(h, l)| (h.0, l.0)).collect::<Vec<_>>(), vec![(0, 3), (1, 2)]);
5136 assert!(d.fold(BufferRow(0), BufferRow(3))); let block = d.buffer().text()[0..9].to_string();
5140 d.set_selections(SelectionSet::new(9));
5141 d.insert_text(&block);
5142 assert_eq!(d.folds().len(), 1, "the outer fold survives the paste");
5145 assert_eq!(FoldMap::new(d.folds(), d.brackets(), d.buffer()).fold_at_header(BufferRow(0)), Some(BufferRow(3)));
5146 }
5147
5148 #[test]
5149 fn editing_after_the_brace_keeps_a_folded_block() {
5150 use crate::{fold_map::FoldMap, BufferRow, SelectionSet};
5151 let mut d = doc("{\n}");
5152 assert!(d.fold(BufferRow(0), BufferRow(1)));
5153 d.set_selections(SelectionSet::new(d.buffer().len()));
5155 d.insert_text("abc");
5156 assert_eq!(d.buffer().text(), "{\n}abc");
5157 assert_eq!(d.folds().len(), 1, "fold survives typing after the brace");
5158 let end = d.buffer().len();
5160 d.edit(vec![EditOp::delete(end - 3..end)]).unwrap();
5161 assert_eq!(d.buffer().text(), "{\n}");
5162 assert_eq!(d.folds().len(), 1, "fold survives deleting after the brace");
5163 assert_eq!(FoldMap::new(d.folds(), d.brackets(), d.buffer()).fold_at_header(BufferRow(0)), Some(BufferRow(1)));
5164 }
5165
5166 #[test]
5167 fn pressing_return_after_a_folded_eof_block_keeps_it_folded() {
5168 use crate::{fold_map::FoldMap, BufferRow, SelectionSet};
5169 let mut d = doc("{\n}");
5172 assert!(d.fold(BufferRow(0), BufferRow(1)));
5173 d.set_selections(SelectionSet::new(d.buffer().len()));
5176 d.insert_text("\n");
5177 assert_eq!(d.buffer().text(), "{\n}\n");
5178 assert_eq!(d.folds().len(), 1, "the intact block stays folded");
5179 assert_eq!(FoldMap::new(d.folds(), d.brackets(), d.buffer()).fold_at_header(BufferRow(0)), Some(BufferRow(1)));
5180 }
5181
5182 #[test]
5183 fn cutting_trailing_content_keeps_an_intact_fold() {
5184 use crate::{fold_map::FoldMap, BufferRow};
5185 let mut d = doc("{\n}\nX");
5187 assert!(d.fold(BufferRow(0), BufferRow(1))); let a = d.buffer().point_to_offset(crate::Point::new(1, 1)); let b = d.buffer().point_to_offset(crate::Point::new(2, 1)); d.edit(vec![EditOp::delete(a..b)]).unwrap();
5194 assert_eq!(d.buffer().text(), "{\n}", "only the trailing content was removed");
5195 assert_eq!(d.folds().len(), 1, "the intact block stays folded");
5196 assert_eq!(FoldMap::new(d.folds(), d.brackets(), d.buffer()).fold_at_header(BufferRow(0)), Some(BufferRow(1)));
5197 }
5198
5199 #[test]
5200 fn cutting_across_a_folds_boundary_drops_it_not_drifts_it() {
5201 use crate::{fold_map::FoldMap, BufferRow};
5202 let mut d = doc("{\n}\n{\n}\n{\n}\n");
5204 assert!(d.fold(BufferRow(2), BufferRow(3))); let a = d.buffer().point_to_offset(crate::Point::new(1, 1)); let b = d.buffer().point_to_offset(crate::Point::new(3, 1)); d.edit(vec![EditOp::delete(a..b)]).unwrap();
5211 assert!(d.folds().is_empty(), "fold dropped, not drifted onto the first block");
5214 assert_eq!(FoldMap::new(d.folds(), d.brackets(), d.buffer()).fold_at_header(BufferRow(0)), None, "first set not folded");
5215 }
5216
5217 #[test]
5218 fn typing_at_many_carets_over_folds_stays_linear() {
5219 use crate::bracket::ENCLOSING_WALKS;
5220 use crate::row_layout::DISPLAY_POSITION_PROBES;
5221 use crate::{Motion, SelectionSet};
5222 let mut src = String::new();
5229 for i in 0..400 {
5230 src.push_str(&format!(
5231 "// block {i}\nfn pid_{i}(pid: u8) -> u8 {{\n switch pid {{\n 0x0D => {{ return 88; }}\n }}\n}}\n\n"
5232 ));
5233 }
5234 let mut d = doc(&src);
5235 let first = d.buffer().text().find("fn pid").unwrap() as u32;
5236 d.set_selections(SelectionSet::from_ranges(&[(first, first + 6)], 0));
5237 d.select_all_occurrences();
5238 let carets = d.selections().all().len();
5239 assert!(carets >= 400, "select-all found every block ({carets} carets)");
5240 d.move_carets(Motion::LineEnd, false);
5241 d.fold_at_carets(false);
5242 d.move_carets(Motion::LineEnd, false);
5243
5244 ENCLOSING_WALKS.with(|c| c.set(0));
5246 DISPLAY_POSITION_PROBES.with(|c| c.set(0));
5247 d.insert_text("a");
5248 let walks = ENCLOSING_WALKS.with(std::cell::Cell::get);
5249 let probes = DISPLAY_POSITION_PROBES.with(std::cell::Cell::get);
5250 assert!(walks <= 2, "one enclosing walk per commit, not O(carets): {walks} for {carets} carets");
5251 assert!(
5252 probes <= 4 * carets as u64,
5253 "display-position probes are O(edit points), not O(carets²): {probes} for {carets} carets"
5254 );
5255 }
5256
5257 #[test]
5258 fn folded_keystroke_never_resolves_bracket_depth() {
5259 use crate::bracket_tree::BRACKET_VIEW_CALLS;
5260 use crate::SelectionSet;
5261 let mut src = String::new();
5272 for i in 0..300 {
5273 src.push_str(&format!("fn f_{i}() {{\n body\n}}\n"));
5274 }
5275 let mut d = doc(&src);
5276 let openers: Vec<u32> = d
5278 .brackets()
5279 .all()
5280 .iter()
5281 .filter(|b| b.open && d.buffer().char_at(b.offset) == Some('{'))
5282 .map(|b| b.offset)
5283 .collect();
5284 assert_eq!(openers.len(), 300, "one foldable block opener each");
5285 for &o in &openers {
5286 assert!(d.toggle_fold_opener(o));
5287 }
5288 let carets: Vec<(u32, u32)> = openers.iter().map(|&o| (o + 1, o + 1)).collect();
5290 d.set_selections(SelectionSet::from_ranges(&carets, 0));
5291 let _ = d.fold_map(); BRACKET_VIEW_CALLS.with(|c| c.set(0));
5294 d.insert_text("a");
5295 let _ = d.fold_map(); let depth_resolves = BRACKET_VIEW_CALLS.with(std::cell::Cell::get);
5297 assert_eq!(
5298 depth_resolves, 0,
5299 "a fold-heavy keystroke resolved bracket depth {depth_resolves} times over 300 folds \
5300 — the edit path must use foldable_partner, not at()/bracket_view"
5301 );
5302 }
5303
5304 #[test]
5305 fn single_line_pair_folds_inline() {
5306 use crate::{fold_map::FoldMap, SelectionSet};
5307 let mut d = doc("x = [1, 2, 3]");
5309 d.set_selections(SelectionSet::new(6)); let opener = d.fold_opener_at_caret(false).expect("an enclosing foldable pair");
5311 assert_eq!(opener, 4);
5312 assert!(d.toggle_fold_opener(opener));
5313 let m = FoldMap::new(d.folds(), d.brackets(), d.buffer());
5314 assert_eq!(m.display_row_count(), 1, "an inline fold hides no rows");
5315 assert_eq!(m.inline_folds().len(), 1);
5316 assert_eq!((m.inline_folds()[0].row, m.inline_folds()[0].open, m.inline_folds()[0].close), (0, 4, 12));
5317 assert!(d.toggle_fold_opener(4));
5319 assert!(FoldMap::new(d.folds(), d.brackets(), d.buffer()).inline_folds().is_empty());
5320 }
5321
5322 #[test]
5323 fn fold_at_carets_acts_on_every_caret() {
5324 use crate::{fold_map::FoldMap, SelectionSet};
5325 let mut d = doc("a{x}\nb{y}");
5327 d.set_selections(SelectionSet::from_offsets(&[2, 7])); assert!(d.fold_at_carets(false), "Ctrl+Shift+[ folds at every caret");
5329 let m = FoldMap::new(d.folds(), d.brackets(), d.buffer());
5330 assert_eq!(m.inline_folds().len(), 2, "both blocks collapsed, not just the primary");
5331 assert!(d.fold_at_carets(true), "Ctrl+Shift+] unfolds at every caret");
5333 assert!(
5334 FoldMap::new(d.folds(), d.brackets(), d.buffer()).inline_folds().is_empty(),
5335 "both blocks expanded",
5336 );
5337 }
5338
5339 #[test]
5340 fn fold_map_cache_matches_a_fresh_build_across_changes() {
5341 fn consistent(d: &Document) -> bool {
5347 *d.fold_map() == crate::fold_map::FoldMap::new(d.folds(), d.brackets(), d.buffer())
5348 }
5349 let mut d = doc("a {\nx\n}\nb = [1, 2, 3]\nc\n");
5350 assert!(consistent(&d), "empty folds");
5351 let block = d.text().find('{').unwrap() as u32;
5353 let inline = d.text().find('[').unwrap() as u32;
5354 d.toggle_fold_opener(block);
5355 d.toggle_fold_opener(inline);
5356 assert!(consistent(&d), "after folds — rebuilt on the generation");
5357 d.edit(vec![EditOp::insert(0, "Z")]).unwrap();
5360 assert!(consistent(&d), "after a no-line-change edit — shifted in place");
5361 d.edit(vec![EditOp::insert(0, "Y")]).unwrap();
5362 assert!(consistent(&d), "after a second in-place shift (cumulative)");
5363 d.edit(vec![EditOp::insert(0, "\n")]).unwrap();
5365 assert!(consistent(&d), "after a line change — rebuilt");
5366 d.edit(vec![EditOp::insert(0, "W")]).unwrap();
5367 assert!(consistent(&d), "after an edit following a rebuild");
5368 }
5369
5370 #[test]
5371 fn deleting_into_a_folded_interior_rebuilds_not_drifts() {
5372 let consistent = |d: &Document| {
5379 *d.fold_map() == crate::fold_map::FoldMap::new(d.folds(), d.brackets(), d.buffer())
5380 };
5381 for prefix in ["xx\n", ""] {
5382 let mut d = doc(&format!("{prefix}fn foo() {{\n body\n}}\nafter\n"));
5385 let open = d.text().find('{').unwrap() as u32;
5386 d.toggle_fold_opener(open);
5387 let _ = d.fold_map();
5388 assert!(consistent(&d), "{prefix:?}: folded");
5389 let nl = open + d.text()[open as usize..].find('\n').unwrap() as u32;
5390 d.edit(vec![EditOp::delete(nl..nl + 1)]).unwrap(); assert!(consistent(&d), "{prefix:?}: after deleting the header-terminating newline");
5392 }
5393 }
5394
5395 #[test]
5396 fn fold_map_cache_matches_fresh_build_under_random_edits() {
5397 let consistent = |d: &Document| {
5403 *d.fold_map() == crate::fold_map::FoldMap::new(d.folds(), d.brackets(), d.buffer())
5404 };
5405 let mut state = 0xF01D_5EEDu32;
5406 let mut next = || {
5407 state ^= state << 13;
5408 state ^= state >> 17;
5409 state ^= state << 5;
5410 state
5411 };
5412 let mut d = doc(&"fn f() {\n x = [1, 2]\n y\n}\n".repeat(6));
5413 for open in d.collapsible_pairs().iter().map(|&(o, ..)| o).collect::<Vec<_>>() {
5414 d.toggle_fold_opener(open);
5415 }
5416 assert!(consistent(&d), "after collapse-all");
5417 for step in 0..1500 {
5418 let len = d.buffer().len();
5419 if len == 0 {
5420 break;
5421 }
5422 let at = next() % (len + 1);
5423 let _ = match next() % 6 {
5424 0 => d.edit(vec![EditOp::insert(at, "\n")]), 1 => d.edit(vec![EditOp::insert(at, "z")]), 2 => d.edit(vec![EditOp::insert(at, "ab\ncd")]), 3 => d.edit(vec![EditOp::insert(at, " ")]), _ if at < len => {
5429 let end = (at + 1 + next() % 5).min(len); d.edit(vec![EditOp::delete(at..end)])
5431 }
5432 _ => d.edit(vec![EditOp::insert(at, "q")]),
5433 };
5434 assert!(consistent(&d), "step {step}: cache drifted from a fresh build (at {at})");
5435 }
5436 }
5437
5438 #[test]
5439 fn keystroke_and_arrow_do_not_rebuild_the_fold_map_at_scale() {
5440 use crate::fold_map::FOLD_BUILDS;
5446 let mut text = String::new();
5447 for i in 0..300 {
5448 text.push_str(&format!("fn f{i}() {{\n body\n}}\n"));
5449 }
5450 let mut d = doc(&text);
5451 let opens: Vec<u32> = d.collapsible_pairs().into_iter().map(|(o, ..)| o).collect();
5452 assert!(opens.len() >= 300, "every block is foldable");
5453 d.set_selections(SelectionSet::from_offsets(&opens));
5454 d.fold_at_carets(false); d.set_selections(SelectionSet::new(0)); let _ = d.fold_map(); let base = FOLD_BUILDS.with(std::cell::Cell::get);
5460 d.type_char('x');
5461 let _ = d.fold_map();
5462 assert_eq!(
5463 FOLD_BUILDS.with(std::cell::Cell::get) - base,
5464 0,
5465 "a keystroke over {} folds must not rebuild the FoldMap",
5466 opens.len()
5467 );
5468
5469 let base = FOLD_BUILDS.with(std::cell::Cell::get);
5471 d.move_carets(Motion::Down, false);
5472 let _ = d.fold_map();
5473 assert_eq!(
5474 FOLD_BUILDS.with(std::cell::Cell::get) - base,
5475 0,
5476 "an arrow key over {} folds must not rebuild the FoldMap",
5477 opens.len()
5478 );
5479
5480 d.set_selections(SelectionSet::new(0));
5486 let base = FOLD_BUILDS.with(std::cell::Cell::get);
5487 d.edit(vec![EditOp::insert(0, "\n")]).unwrap();
5488 let _ = d.fold_map();
5489 assert_eq!(
5490 FOLD_BUILDS.with(std::cell::Cell::get) - base,
5491 0,
5492 "a newline at the top over {} folds must not rebuild the FoldMap",
5493 opens.len()
5494 );
5495 }
5496
5497 #[test]
5498 fn undo_and_redo_build_the_fold_map_once() {
5499 use crate::fold_map::FOLD_BUILDS;
5505 let mut text = String::new();
5506 for i in 0..50 {
5507 text.push_str(&format!("fn f{i}() {{\n body\n}}\n"));
5508 }
5509 let mut d = doc(&text);
5510 let opens: Vec<u32> = d.collapsible_pairs().into_iter().map(|(o, ..)| o).collect();
5511 d.set_selections(SelectionSet::from_offsets(&opens));
5512 d.fold_at_carets(false); d.move_carets(Motion::LineStart, false);
5516 d.type_char('x');
5517 assert_eq!(d.folds().len(), opens.len(), "typing before the headers folds nothing open");
5518 let _ = d.fold_map(); let builds = |d: &mut Document, step: fn(&mut Document) -> bool| {
5520 let base = FOLD_BUILDS.with(std::cell::Cell::get);
5521 assert!(step(d));
5522 let _ = d.fold_map(); FOLD_BUILDS.with(std::cell::Cell::get) - base
5524 };
5525 assert_eq!(builds(&mut d, Document::undo), 1, "undo + render build the fold map once");
5526 assert_eq!(builds(&mut d, Document::redo), 1, "redo + render build the fold map once");
5527 }
5528
5529 #[test]
5530 fn keystroke_with_document_scale_decorations_does_not_resort_the_store() {
5531 use crate::decorations::DECORATION_SORTS;
5537 use crate::{Diagnostic, Severity};
5538 let mut d = doc(&"let x = 1;\n".repeat(2000)); let rev = d.revision();
5540 let diags: Vec<Diagnostic> = (0..2000u32)
5541 .map(|i| Diagnostic::new(i * 10..i * 10 + 3, Severity::Warning, "w"))
5542 .collect();
5543 d.set_diagnostics(rev, diags); d.set_selections(SelectionSet::new(0));
5545
5546 let base = DECORATION_SORTS.with(std::cell::Cell::get);
5547 d.type_char('z');
5548 assert_eq!(
5549 DECORATION_SORTS.with(std::cell::Cell::get) - base,
5550 0,
5551 "a keystroke with 2000 diagnostics must not re-sort the decoration store"
5552 );
5553 }
5554
5555 #[test]
5556 fn reconcile_scans_the_fold_set_only_when_a_fold_actually_breaks() {
5557 use crate::fold_map::RECONCILE_SCANS;
5565 let mut d = doc("fn f() {\n body\n}\ntail\n");
5566 let open = d.text().find('{').unwrap() as u32;
5567 assert!(d.toggle_fold_opener(open));
5568 d.set_selections(SelectionSet::new(0));
5569
5570 for text in ["a", "\n", "{"] {
5575 let base = RECONCILE_SCANS.with(std::cell::Cell::get);
5576 d.edit(vec![EditOp::insert(0, text)]).unwrap();
5577 assert_eq!(
5578 RECONCILE_SCANS.with(std::cell::Cell::get) - base,
5579 0,
5580 "inserting {text:?} breaks no fold, so it must not scan the fold set"
5581 );
5582 }
5583
5584 let close = d.text().rfind('}').unwrap() as u32;
5586 let base = RECONCILE_SCANS.with(std::cell::Cell::get);
5587 d.edit(vec![EditOp::delete(close..close + 1)]).unwrap();
5588 assert!(
5589 RECONCILE_SCANS.with(std::cell::Cell::get) - base >= 1,
5590 "deleting a brace that breaks a fold must reconcile the fold set"
5591 );
5592 assert!(d.folds().is_empty(), "the broken fold was dropped");
5593 }
5594
5595 #[test]
5596 fn windowed_reconcile_matches_the_whole_set_reconcile_under_random_edits() {
5597 use crate::SelectionSet;
5598 let mut src = String::new();
5606 for i in 0..40 {
5607 src.push_str(&format!("block{i} {{\n a\n b\n}}\n"));
5608 }
5609 let mut d = doc(&src);
5610 for open in d.collapsible_pairs().iter().map(|&(o, ..)| o).collect::<Vec<_>>() {
5611 d.toggle_fold_opener(open);
5612 }
5613 let mut state: u32 = 0x1234_5678;
5615 let mut rng = || {
5616 state = state.wrapping_mul(1_664_525).wrapping_add(1_013_904_223);
5617 state
5618 };
5619 for _ in 0..300 {
5620 let len = d.buffer().len();
5621 if len == 0 {
5622 break;
5623 }
5624 let pos = rng() % len;
5625 match rng() % 5 {
5626 0 => {
5627 d.set_selections(SelectionSet::new(pos));
5628 d.insert_text("{");
5629 }
5630 1 => {
5631 d.set_selections(SelectionSet::new(pos));
5632 d.insert_text("}");
5633 }
5634 2 if len > 1 => {
5635 let s = pos.min(len - 1);
5638 d.edit(vec![EditOp::delete(s..s + 1)]).unwrap();
5639 }
5640 _ => {
5641 d.set_selections(SelectionSet::new(pos));
5642 d.insert_text("x");
5643 }
5644 }
5645 let before: Vec<u32> = d.folds().iter().collect();
5646 d.force_whole_reconcile();
5647 let after: Vec<u32> = d.folds().iter().collect();
5648 assert_eq!(before, after, "windowed reconcile missed a fold the whole-set pass caught");
5649 for o in after {
5651 let foldable = d
5652 .brackets()
5653 .at(o)
5654 .filter(|b| b.open)
5655 .and_then(|b| b.partner)
5656 .is_some_and(|c| crate::row_layout::pair_has_interior(o, c));
5657 assert!(foldable, "a surviving fold at {o} is not a foldable pair");
5658 }
5659 }
5660 }
5661
5662 #[test]
5663 fn fold_at_carets_ejects_carets_hidden_by_a_block() {
5664 use crate::SelectionSet;
5665 let mut d = doc("a {\nhidden\n}\nb"); let inside = d.buffer().point_to_offset(Point::new(1, 3)); d.set_selections(SelectionSet::new(inside));
5668 assert!(d.fold_at_carets(false), "the block folds");
5669 let row = d.buffer().offset_to_point(d.selections().newest().head()).row;
5672 assert_eq!(row, 0, "caret ejected to the header row");
5673 }
5674
5675 #[test]
5676 fn fold_at_carets_ejects_a_caret_on_the_closing_rows_indent() {
5677 use crate::SelectionSet;
5682 let mut d = doc("m {\n a\n }\nz"); d.set_selections(SelectionSet::new(d.buffer().point_to_offset(Point::new(2, 1))));
5684 assert!(d.fold_at_carets(false), "the block folds");
5685 let head = d.selections().newest().head();
5686 assert_eq!(head, d.buffer().point_to_offset(Point::new(0, 3)), "ejected to the header's end");
5687 assert!(d.fold_map().display_position(d.buffer(), head, d.tab_size()).is_some());
5688 }
5689
5690 #[test]
5691 fn caret_ejection_hides_exactly_what_display_position_cannot_place() {
5692 for text in [
5698 "m {\n a\n }\nz",
5699 "m {\n\ta\n\t}\nz",
5700 "m {\n [1, 2]\n }",
5701 "m { [1, 2]\n a\n }",
5702 "x [1, 2] y",
5703 "a {\n b {\n c\n }\n d\n}\n",
5704 ] {
5705 let mut d = doc(text);
5706 let openers: Vec<u32> = d.collapsible_pairs().into_iter().map(|(o, ..)| o).collect();
5707 d.set_selections(SelectionSet::from_offsets(&openers));
5708 d.fold_at_carets(false);
5709 let map = d.fold_map();
5710 for off in 0..=d.buffer().len() {
5711 let unplaceable = map.display_position(d.buffer(), off, d.tab_size()).is_none();
5712 let in_inline_gap = map.inline_fold_before(off).is_some_and(|f| f.hides_caret_at(off));
5713 assert_eq!(
5714 map.entry_edge_if_hidden(d.buffer(), off).is_some(),
5715 unplaceable || in_inline_gap,
5716 "{text:?} @ {off}"
5717 );
5718 }
5719 }
5720 }
5721
5722 #[test]
5723 fn horizontal_movement_hops_an_inline_fold() {
5724 use crate::{Motion, SelectionSet};
5725 let mut d = doc("x = [1, 2, 3]"); d.set_selections(SelectionSet::new(6)); let op = d.fold_opener_at_caret(false).unwrap();
5728 assert!(d.toggle_fold_opener(op));
5729 assert_eq!(d.selections().newest().head(), 5, "caret snapped just after `[`");
5730 d.move_carets(Motion::Right, false);
5731 assert_eq!(d.selections().newest().head(), 12, "Right hops the collapsed interior to `]`");
5732 d.move_carets(Motion::Left, false);
5733 assert_eq!(d.selections().newest().head(), 5, "Left hops back to just after `[`");
5734 }
5735
5736 #[test]
5737 fn collapsible_at_prefers_the_innermost() {
5738 let d = doc("fn m() {\n let x = [1, 2, 3];\n}");
5740 let mut pairs = d.collapsible_pairs();
5741 pairs.sort();
5742 assert_eq!(pairs, vec![(7, 32, 0, 2), (21, 29, 1, 1)]);
5743 assert_eq!(d.collapsible_at(24), Some(21));
5745 assert_eq!(d.collapsible_at(12), Some(7));
5747 assert_eq!(d.collapsible_at(0), None);
5749 }
5750
5751 #[test]
5752 fn collapsible_excludes_the_already_folded() {
5753 let mut d = doc("fn m() {\n let x = [1, 2, 3];\n}");
5754 assert!(d.toggle_fold_opener(7));
5757 let openers: Vec<u32> = d.collapsible_pairs().iter().map(|&(o, ..)| o).collect();
5758 assert_eq!(openers, vec![21]);
5759 assert_eq!(d.collapsible_at(12), None, "the folded block is no longer a target");
5760 assert_eq!(d.collapsible_at(24), Some(21), "the array inside it still is");
5761 }
5762
5763 #[test]
5764 fn nested_inline_folds_reduce_to_the_outer() {
5765 use crate::fold_map::FoldMap;
5766 let mut d = doc("outer_call(a, [1, 2, 3])");
5768 assert!(d.toggle_fold_opener(14)); assert!(d.toggle_fold_opener(10)); let inline = |d: &Document| {
5771 FoldMap::new(d.folds(), d.brackets(), d.buffer()).inline_folds().iter().map(|f| f.open).collect::<Vec<_>>()
5772 };
5773 assert_eq!(inline(&d), vec![10]);
5775 assert!(d.toggle_fold_opener(10));
5777 assert_eq!(inline(&d), vec![14]);
5778 }
5779
5780 #[test]
5781 fn horizontal_movement_hops_the_collapsed_gap() {
5782 use crate::{BufferRow, Motion, Point, SelectionSet};
5783 let mut d = doc("m {\n a\n b\n}\nafter");
5785 assert!(d.fold(BufferRow(0), BufferRow(3))); let caret = |d: &Document| d.buffer().offset_to_point(d.selections().newest().head());
5787 d.set_selections(SelectionSet::new(d.buffer().point_to_offset(Point::new(0, 3))));
5789 d.move_carets(Motion::Right, false);
5790 assert_eq!(caret(&d), Point::new(3, 0), "Right hops the hidden interior to the closing bracket");
5791 d.move_carets(Motion::Left, false);
5792 assert_eq!(caret(&d), Point::new(0, 3), "Left hops back to the header end");
5793 }
5794
5795 #[test]
5796 fn vertical_move_skips_folds() {
5797 use crate::{BufferRow, Motion, Point, SelectionSet};
5798 let mut d = doc("L0\n{\nL2\nL3\n}\nL5");
5799 assert!(d.fold(BufferRow(1), BufferRow(4))); let row = |d: &Document| d.buffer().offset_to_point(d.selections().newest().head()).row;
5801 d.set_selections(SelectionSet::new(d.buffer().point_to_offset(Point::new(1, 0))));
5803 d.move_carets(Motion::Down, false);
5804 assert_eq!(row(&d), 5);
5805 d.move_carets(Motion::Up, false);
5807 assert_eq!(row(&d), 1);
5808 }
5809
5810 #[test]
5811 fn typing_into_an_inline_folds_gap_expands_it() {
5812 use crate::SelectionSet;
5813 let mut d = doc("x = [1, 2, 3]");
5817 d.set_selections(SelectionSet::new(6));
5818 assert!(d.toggle_fold_opener(4));
5819 assert_eq!(d.selections().newest().head(), 5, "caret ejected to the left edge");
5820 d.type_char('9');
5821 assert!(!d.folds().is_folded(4), "editing hidden text expands the fold");
5822 assert_eq!(d.text(), "x = [91, 2, 3]");
5823 }
5824
5825 #[test]
5826 fn typing_at_a_collapsed_headers_end_keeps_it_folded() {
5827 use crate::{Point, SelectionSet};
5828 let mut d = doc("m {\n a\n}");
5832 d.set_selections(SelectionSet::new(d.buffer().point_to_offset(Point::new(1, 0))));
5833 let opener = 2;
5834 assert!(d.toggle_fold_opener(opener));
5835 d.type_char('x'); assert!(d.folds().is_folded(opener), "a visible-part edit leaves the fold alone");
5837 assert!(d.text().starts_with("m {x\n"));
5838 }
5839
5840 #[test]
5841 fn undo_into_a_folded_region_expands_it() {
5842 use crate::{Point, SelectionSet};
5843 let mut d = doc("m {\n a\n}");
5847 d.set_selections(SelectionSet::new(d.buffer().point_to_offset(Point::new(1, 3))));
5848 d.type_char('b'); let opener = 2;
5850 assert!(d.toggle_fold_opener(opener));
5851 assert!(d.folds().is_folded(opener));
5852 assert!(d.undo());
5853 assert!(!d.folds().is_folded(opener), "undoing hidden text reveals it");
5854 assert!(d.text().contains("\n a\n"), "the typed char was reverted");
5855 }
5856
5857 #[test]
5858 fn folding_a_block_ejects_a_hidden_caret_to_the_header_end() {
5859 use crate::{Point, SelectionSet};
5860 let mut d = doc("m {\n a\n }");
5862 let opener = 2;
5863 d.set_selections(SelectionSet::new(d.buffer().point_to_offset(Point::new(1, 2))));
5865 assert!(d.toggle_fold_opener(opener));
5866 assert_eq!(
5867 d.selections().newest().head(),
5868 d.buffer().point_to_offset(Point::new(0, 3)),
5869 "the hidden caret ejects to just before the placeholder"
5870 );
5871 assert!(d.toggle_fold_opener(opener)); d.set_selections(SelectionSet::new(d.buffer().point_to_offset(Point::new(2, 1))));
5874 assert!(d.toggle_fold_opener(opener));
5875 assert_eq!(d.selections().newest().head(), d.buffer().point_to_offset(Point::new(0, 3)));
5876 assert!(d.toggle_fold_opener(opener)); let on_tail = d.buffer().point_to_offset(Point::new(2, 2));
5879 d.set_selections(SelectionSet::new(on_tail));
5880 assert!(d.toggle_fold_opener(opener));
5881 assert_eq!(d.selections().newest().head(), on_tail, "the visible tail is not ejected");
5882 }
5883
5884 #[test]
5885 fn fold_chord_works_when_the_caret_touches_a_bracket() {
5886 use crate::SelectionSet;
5887 let mut d = doc("x = [1, 2, 3]");
5889 d.set_selections(SelectionSet::new(4));
5891 assert_eq!(d.fold_opener_at_caret(false), Some(4), "touching the opener from the left");
5892 d.set_selections(SelectionSet::new(13));
5894 assert_eq!(d.fold_opener_at_caret(false), Some(4), "touching the closer from the right");
5895 assert!(d.toggle_fold_opener(4));
5897 assert_eq!(d.fold_opener_at_caret(true), Some(4), "touching unfolds too");
5898 assert!(d.toggle_fold_opener(4));
5900 d.set_selections(SelectionSet::new(1));
5901 assert_eq!(d.fold_opener_at_caret(false), None);
5902 }
5903
5904 #[test]
5905 fn fold_chord_at_a_shared_boundary_picks_the_touched_inner_pair() {
5906 use crate::SelectionSet;
5907 let d = {
5911 let mut d = doc("{a [b] c}");
5912 d.set_selections(SelectionSet::new(6));
5913 d
5914 };
5915 assert_eq!(d.fold_opener_at_caret(false), Some(3), "the touched inline pair beats the enclosing block");
5916 }
5917
5918 #[test]
5919 fn vertical_goal_is_a_display_cell_across_tab_stops() {
5920 use crate::{Motion, Point, SelectionSet};
5921 let mut d = doc("\tab\nxxxxxxxxxx");
5925 d.set_selections(SelectionSet::new(d.buffer().point_to_offset(Point::new(0, 1))));
5926 d.move_carets(Motion::Down, false);
5927 assert_eq!(
5928 d.buffer().offset_to_point(d.selections().newest().head()),
5929 Point::new(1, 4),
5930 "the goal is the visual column, not the byte column"
5931 );
5932 d.move_carets(Motion::Up, false);
5934 assert_eq!(d.buffer().offset_to_point(d.selections().newest().head()), Point::new(0, 1));
5935 }
5936
5937 #[test]
5938 fn vertical_move_through_a_collapsed_tail_stays_visual() {
5939 use crate::{BufferRow, Motion, Point, SelectionSet};
5940 let mut d = doc("m {\na\n} t\nabcdefgh");
5943 assert!(d.fold(BufferRow(0), BufferRow(2)));
5944 d.set_selections(SelectionSet::new(d.buffer().point_to_offset(Point::new(2, 3))));
5946 d.move_carets(Motion::Down, false);
5947 assert_eq!(
5948 d.buffer().offset_to_point(d.selections().newest().head()),
5949 Point::new(3, 8),
5950 "Down from the far-right tail lands at the row's visual end, not byte col 3"
5951 );
5952 d.move_carets(Motion::Up, false);
5954 assert_eq!(
5955 d.buffer().offset_to_point(d.selections().newest().head()).row,
5956 2,
5957 "a goal over the placeholder tail resolves onto the tail"
5958 );
5959 }
5960
5961 #[test]
5962 fn vertical_landing_snaps_out_of_a_collapsed_inline_gap() {
5963 use crate::{Motion, Point, SelectionSet};
5964 let mut d = doc("aaaaaaaaaa\nx[123456]y");
5968 let opener = d.buffer().text().find('[').unwrap() as u32;
5969 assert!(d.toggle_fold_opener(opener));
5970 d.set_selections(SelectionSet::new(d.buffer().point_to_offset(Point::new(0, 3))));
5971 d.move_carets(Motion::Down, false);
5972 assert_eq!(
5973 d.selections().newest().head(),
5974 opener + 1,
5975 "a landing on the chip snaps to just after the opening bracket"
5976 );
5977 }
5978
5979 #[test]
5980 fn home_and_end_operate_on_the_collapsed_display_line() {
5981 use crate::{BufferRow, Motion, Point, SelectionSet};
5982 let mut d = doc("L0\n m {\na\n }");
5985 assert!(d.fold(BufferRow(1), BufferRow(3)));
5986 let head = |d: &Document| d.selections().newest().head();
5987 d.set_selections(SelectionSet::new(d.buffer().point_to_offset(Point::new(1, 2))));
5990 d.move_carets(Motion::LineEnd, false);
5991 assert_eq!(head(&d), d.buffer().point_to_offset(Point::new(3, 3)), "End lands after the tail brace");
5992 d.move_carets(Motion::LineStart, false);
5995 assert_eq!(head(&d), d.buffer().point_to_offset(Point::new(1, 2)), "Home jumps to the header's smart start");
5996 d.move_carets(Motion::LineStart, false);
5998 assert_eq!(head(&d), d.buffer().point_to_offset(Point::new(1, 0)));
5999 assert!(d.unfold(BufferRow(1)));
6001 d.set_selections(SelectionSet::new(d.buffer().point_to_offset(Point::new(1, 2))));
6002 d.move_carets(Motion::LineEnd, false);
6003 assert_eq!(head(&d), d.buffer().point_to_offset(Point::new(1, 5)), "plain End at the header's own end");
6004 }
6005
6006 #[test]
6007 fn foldable_ranges_from_multiline_brackets() {
6008 let d = doc("mod m {\n fn f() {\n if x {\n a\n }\n }\n g()\n}");
6011 let pairs: Vec<(u32, u32)> = d.foldable_ranges().iter().map(|(h, l)| (h.0, l.0)).collect();
6012 assert_eq!(pairs, vec![(0, 7), (1, 5), (2, 4)]);
6014 }
6015
6016 #[test]
6017 fn fold_dropped_when_its_bracket_pair_is_broken() {
6018 use crate::{fold_map::FoldMap, BufferRow};
6019 let mut d = doc("m {\n a\n b\n}\nafter");
6021 assert_eq!(d.foldable_ranges().iter().map(|(h, l)| (h.0, l.0)).collect::<Vec<_>>(), vec![(0, 3)]);
6022 assert!(d.fold(BufferRow(0), BufferRow(3)));
6023 assert_eq!(FoldMap::new(d.folds(), d.brackets(), d.buffer()).display_row_count(), 2); let s = d.buffer().point_to_offset(crate::Point::new(3, 0));
6029 let e = d.buffer().point_to_offset(crate::Point::new(4, 0));
6030 d.edit(vec![EditOp::delete(s..e)]).unwrap();
6031 assert!(d.foldable_ranges().is_empty(), "no closing brace ⇒ nothing foldable");
6032 assert!(d.folds().is_empty(), "invalidated fold dropped, not left orphaning hidden rows");
6033 assert_eq!(
6034 FoldMap::new(d.folds(), d.brackets(), d.buffer()).display_row_count(),
6035 d.buffer().line_count(),
6036 "every row is visible again"
6037 );
6038 }
6039
6040 #[test]
6041 fn valid_fold_survives_edit_above() {
6042 use crate::{fold_map::FoldMap, BufferRow, SelectionSet};
6043 let mut d = doc("m {\n a\n b\n}\nafter");
6044 assert!(d.fold(BufferRow(0), BufferRow(3)));
6045 d.set_selections(SelectionSet::new(0));
6048 d.insert_text("top\n");
6049 assert_eq!(d.foldable_ranges().iter().map(|(h, l)| (h.0, l.0)).collect::<Vec<_>>(), vec![(1, 4)]);
6050 assert_eq!(d.folds().len(), 1, "the still-valid fold is kept");
6051 let m = FoldMap::new(d.folds(), d.brackets(), d.buffer());
6052 assert_eq!(m.fold_at_header(BufferRow(1)), Some(BufferRow(4)));
6053 assert!(m.is_folded(BufferRow(2)) && m.is_folded(BufferRow(3)) && m.is_folded(BufferRow(4)));
6054 }
6055
6056 #[test]
6057 fn breaking_one_block_keeps_a_sibling_fold() {
6058 use crate::{fold_map::FoldMap, BufferRow};
6059 let mut d = doc("a {\n x\n}\nb {\n y\n}\nafter");
6062 assert_eq!(d.foldable_ranges().iter().map(|(h, l)| (h.0, l.0)).collect::<Vec<_>>(), vec![(0, 2), (3, 5)]);
6063 assert!(d.fold(BufferRow(0), BufferRow(2)));
6064 assert!(d.fold(BufferRow(3), BufferRow(5)));
6065 assert_eq!(d.folds().len(), 2);
6066 let s = d.buffer().point_to_offset(crate::Point::new(5, 0));
6069 let e = d.buffer().point_to_offset(crate::Point::new(6, 0));
6070 d.edit(vec![EditOp::delete(s..e)]).unwrap();
6071 assert_eq!(d.folds().len(), 1, "only the broken block's fold drops");
6072 let m = FoldMap::new(d.folds(), d.brackets(), d.buffer());
6073 assert_eq!(m.fold_at_header(BufferRow(0)), Some(BufferRow(2)), "sibling A still collapsed");
6074 assert_eq!(m.fold_at_header(BufferRow(3)), None, "B is no longer folded");
6075 }
6076
6077 #[test]
6078 fn fold_drops_when_its_interior_is_deleted() {
6079 use crate::{fold_map::FoldMap, BufferRow};
6080 let mut d = doc("L0\n{\nL2\nL3\n}\nL5");
6081 assert!(d.fold(BufferRow(1), BufferRow(4))); let start = d.buffer().point_to_offset(crate::Point::new(2, 0));
6084 let end = d.buffer().point_to_offset(crate::Point::new(5, 0));
6085 d.edit(vec![EditOp::delete(start..end)]).unwrap();
6086 assert!(d.folds().is_empty(), "fold dropped when its interior text is deleted");
6087 assert_eq!(FoldMap::new(d.folds(), d.brackets(), d.buffer()).display_row_count(), d.buffer().line_count());
6088 }
6089
6090 #[test]
6091 fn intel05_diagnostic_keeps_byte_offsets_on_a_multibyte_line() {
6092 use crate::{Diagnostic, Severity};
6097 let mut d = doc("aé=x");
6098 d.set_diagnostics(d.revision(), vec![Diagnostic::new(4..5, Severity::Error, "e")]);
6099 let span = d.diagnostics_in(0..100).next().unwrap().0;
6100 assert_eq!(span, 4..5, "diagnostics_in returns raw byte offsets, not cells");
6101 assert_eq!(crate::display_map::expand(&d.buffer().line(0), span.start, 4), 3);
6104 }
6105
6106 #[test]
6107 fn undo_resyncs_brackets_and_highlight_to_the_reverted_text() {
6108 const G: &str = "%YAML 1.2\n---\nname: T\nscope: source.t\ncontexts:\n main:\n - match: '\\w+'\n scope: keyword.t\n";
6109 const TH: &str = r#"<?xml version="1.0" encoding="UTF-8"?>
6110<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
6111<plist version="1.0"><dict><key>settings</key><array><dict><key>settings</key><dict><key>foreground</key><string>#FFFFFF</string></dict></dict></array></dict></plist>"#;
6112 let mut d = doc("()");
6113 d.set_syntax(
6114 crate::SyntaxDef::from_sublime_syntax(G).unwrap(),
6115 crate::TokenTheme::from_tm_theme(TH).unwrap(),
6116 );
6117 d.tokenize_highlight(d.buffer().line_count());
6118 d.set_selections(SelectionSet::new(1)); d.edit(vec![EditOp::insert(1, "[]\n")]).unwrap(); d.tokenize_highlight(d.buffer().line_count());
6121 assert_eq!((d.buffer().line_count(), d.brackets().all().len()), (2, 4));
6122 assert!(d.undo(), "there was something to undo");
6123 assert_eq!(d.buffer().text(), "()");
6124 assert_eq!((d.buffer().line_count(), d.brackets().all().len()), (1, 2));
6126 d.tokenize_highlight(d.buffer().line_count());
6127 assert!(d.highlight_line_spans(0).is_some());
6128 assert!(d.highlight_line_spans(1).is_none(), "no phantom line after undo");
6129 }
6130
6131 #[test]
6132 fn select_all_spans_the_whole_buffer_with_head_at_end() {
6133 let mut d = doc("abc\ndef"); d.select_all();
6135 assert_eq!(d.selections().len(), 1);
6136 let s = d.selections().newest();
6137 assert_eq!((s.start(), s.end(), s.head()), (0, 7, 7));
6138 }
6139
6140 fn query(text: &str) -> crate::FindQuery {
6141 crate::FindQuery { text: text.into(), case_sensitive: true, ..Default::default() }
6142 }
6143
6144 #[test]
6145 fn find_matches_ride_an_edit_above_them() {
6146 let mut d = doc("xx target"); d.set_find_query(Some(query("target")), 0);
6149 assert_eq!(d.find_matches_in(0..100).collect::<Vec<_>>(), vec![(3..9, false)]);
6150 d.set_selections(SelectionSet::new(0));
6151 d.edit(vec![EditOp::insert(0, "ab")]).unwrap(); assert_eq!(d.find_matches_in(0..100).collect::<Vec<_>>(), vec![(5..11, false)]);
6155 }
6156
6157 #[test]
6158 fn find_next_prev_start_from_the_caret_and_wrap() {
6159 let mut d = doc("foo bar foo baz foo"); d.set_find_query(Some(query("foo")), 0);
6162 assert_eq!((d.find_match_count(), d.active_find_match()), (3, None));
6163 d.set_selections(SelectionSet::new(5)); assert_eq!(d.find_next(0), Some(8..11)); assert_eq!(d.active_find_match(), Some(1));
6166 assert_eq!(d.find_next(0), Some(16..19)); assert_eq!(d.find_next(0), Some(0..3), "wraps to first");
6168 assert_eq!(d.find_prev(0), Some(16..19), "prev from first wraps to last");
6169 d.set_selections(SelectionSet::new(0));
6170 assert_eq!(d.find_prev(0), Some(16..19), "prev before all wraps to last");
6171 }
6172
6173 #[test]
6174 fn active_match_survives_a_rescan_at_the_same_start() {
6175 let mut d = doc("foo foo foo");
6180 d.set_find_query(Some(query("foo")), 0);
6181 d.set_selections(SelectionSet::new(4)); d.find_next(0);
6183 assert_eq!(d.active_find_match(), Some(1));
6184 d.set_selections(SelectionSet::new(11));
6185 d.edit(vec![EditOp::insert(11, " foo")]).unwrap(); assert_eq!(
6187 (d.find_match_count(), d.active_find_match()),
6188 (4, Some(1)),
6189 "current at the commit, active untouched"
6190 );
6191 assert!(!d.maybe_rescan_find(1000), "nothing stale to rescan (uncapped)");
6192 assert_eq!((d.find_match_count(), d.active_find_match()), (4, Some(1)));
6193 }
6194
6195 #[test]
6196 fn find_matches_ride_undo_and_redo_through_the_shared_mover() {
6197 let mut d = doc("foo bar");
6201 d.set_find_query(Some(query("foo")), 0);
6202 assert_eq!(d.find_matches_in(0..100).next().unwrap().0, 0..3);
6203 d.set_selections(SelectionSet::new(0));
6204 d.edit(vec![EditOp::insert(0, "xy")]).unwrap(); assert_eq!(d.find_matches_in(0..100).next().unwrap().0, 2..5);
6206 d.undo(); assert_eq!(d.buffer().text(), "foo bar");
6208 assert_eq!(d.find_matches_in(0..100).next().unwrap().0, 0..3, "rode the undo");
6209 d.redo(); assert_eq!(d.find_matches_in(0..100).next().unwrap().0, 2..5, "rode the redo");
6211 }
6212
6213 #[test]
6214 fn find_count_reflects_live_matches_after_a_collapse_delete() {
6215 let mut d = doc("foo foo"); d.set_find_query(Some(query("foo")), 0);
6220 assert_eq!(d.find_match_count(), 2);
6221 d.edit(vec![EditOp::delete(0..4)]).unwrap(); assert_eq!(d.buffer().text(), "foo");
6223 assert_eq!(d.find_match_count(), 1, "collapsed match not counted");
6224 assert_eq!(d.find_matches_in(0..100).count(), 1, "count agrees with render");
6225 }
6226
6227 #[test]
6228 fn find_navigation_requests_a_reveal() {
6229 let mut d = doc("foo bar foo");
6230 d.set_find_query(Some(query("foo")), 0);
6231 let before = d.reveal_seq();
6232 assert!(d.find_next(0).is_some());
6233 assert!(d.reveal_seq() > before, "navigation bumps the reveal request");
6234 }
6235
6236 #[test]
6237 fn clearing_the_query_drops_every_match() {
6238 let mut d = doc("foo foo");
6239 d.set_find_query(Some(query("foo")), 0);
6240 assert_eq!(d.find_match_count(), 2);
6241 d.set_find_query(None, 0);
6242 assert_eq!((d.find_match_count(), d.find_query().is_none()), (0, true));
6243 assert_eq!(d.find_matches_in(0..100).count(), 0);
6244 }
6245
6246 #[test]
6247 fn edits_repair_the_match_set_immediately() {
6248 let mut d = doc("foo");
6252 d.set_find_query(Some(query("foo")), 0);
6253 d.edit(vec![EditOp::insert(0, "foo ")]).unwrap(); assert_eq!(d.find_match_count(), 2, "current immediately — no debounce");
6255 assert!(!d.maybe_rescan_find(50), "nothing to rescan");
6256 assert!(!d.maybe_rescan_find(1_000), "uncapped: never rescans");
6257 assert_eq!(d.find_match_count(), 2);
6258 }
6259
6260 fn match_ids(d: &Document) -> Vec<crate::decorations::DecorationId> {
6264 d.decorations
6265 .decorations_in(0..u32::MAX)
6266 .filter(|r| matches!(r.kind, DecorationKind::FindMatch))
6267 .map(|r| r.id)
6268 .collect()
6269 }
6270
6271 fn match_ranges(d: &Document) -> Vec<Range<u32>> {
6273 d.find_matches_in(0..u32::MAX).map(|(r, _)| r).collect()
6274 }
6275
6276 fn xorshift(mut state: u64) -> impl FnMut() -> u64 {
6278 move || {
6279 state ^= state << 13;
6280 state ^= state >> 7;
6281 state ^= state << 17;
6282 state
6283 }
6284 }
6285
6286 #[test]
6287 fn repair_preserves_far_match_ids() {
6288 let mut d = doc("foo bar foo bar baz padding padding foo");
6292 d.set_find_query(Some(query("foo")), 0);
6293 let before = match_ids(&d);
6294 assert_eq!(before.len(), 3);
6295 d.edit(vec![EditOp::insert(22, "zz")]).unwrap();
6297 assert!(!d.maybe_rescan_find(1_000), "always current: nothing to rescan");
6298 assert_eq!(match_ids(&d), before, "untouched matches keep their ids");
6299 assert_eq!(match_ranges(&d), vec![0..3, 8..11, 38..41]);
6300 }
6301
6302 #[test]
6303 fn repair_insert_creates_match_in_window() {
6304 let mut d = doc("fo bar");
6305 d.set_find_query(Some(query("foo")), 0);
6306 assert_eq!(d.find_match_count(), 0);
6307 d.edit(vec![EditOp::insert(2, "o")]).unwrap(); assert_eq!(match_ranges(&d), vec![0..3], "current at the commit");
6309 }
6310
6311 #[test]
6312 fn repair_delete_joins_halves_creates_match() {
6313 let mut d = doc("foXo bar");
6315 d.set_find_query(Some(query("foo")), 0);
6316 assert_eq!(d.find_match_count(), 0);
6317 d.edit(vec![EditOp::delete(2..3)]).unwrap(); assert_eq!(match_ranges(&d), vec![0..3]);
6319 }
6320
6321 #[test]
6322 fn repair_destroys_straddling_match() {
6323 let mut d = doc("afoob");
6325 d.set_find_query(Some(query("foo")), 0);
6326 assert_eq!(match_ranges(&d), vec![1..4]);
6327 d.edit(vec![EditOp::delete(3..5)]).unwrap(); assert_eq!(d.find_match_count(), 0);
6329 }
6330
6331 #[test]
6332 fn repair_interior_insert_invalidates_match() {
6333 let mut d = doc("foo");
6336 d.set_find_query(Some(query("foo")), 0);
6337 assert_eq!(d.find_match_count(), 1);
6338 d.edit(vec![EditOp::insert(1, "x")]).unwrap(); assert_eq!(d.find_match_count(), 0);
6340 }
6341
6342 #[test]
6343 fn repair_multi_edit_transaction() {
6344 let mut d = doc("fo bar fo baz");
6346 d.set_find_query(Some(query("foo")), 0);
6347 d.edit(vec![EditOp::insert(2, "o"), EditOp::insert(9, "o")]).unwrap();
6348 assert_eq!(d.buffer().text(), "foo bar foo baz");
6349 assert_eq!(match_ranges(&d), vec![0..3, 8..11]);
6350 }
6351
6352 #[test]
6353 fn repair_equals_full_scan_property() {
6354 let fq = query("ab");
6359 let mut next = xorshift(0x243F_6A88_85A3_08D3);
6360 let alphabet = ['a', 'b', ' '];
6361 let mut d = doc(&"ab ba a b ab ".repeat(8));
6362 d.set_find_query(Some(fq.clone()), 0);
6363 for step in 0..300 {
6364 let len = d.buffer().len();
6365 match next() % 3 {
6366 0 => {
6367 let at = (next() % u64::from(len + 1)) as u32;
6368 let n = 1 + next() % 3;
6369 let s: String = (0..n).map(|_| alphabet[(next() % 3) as usize]).collect();
6370 d.edit(vec![EditOp::insert(at, s)]).unwrap();
6371 }
6372 1 if len > 0 => {
6373 let a = (next() % u64::from(len)) as u32;
6374 let b = (a + 1 + (next() % 4) as u32).min(len);
6375 d.edit(vec![EditOp::delete(a..b)]).unwrap();
6376 }
6377 _ => {
6378 let a = (next() % u64::from(len + 1)) as u32;
6380 let b = (next() % u64::from(len + 1)) as u32;
6381 let (a, b) = (a.min(b), a.max(b));
6382 if a == b {
6383 d.edit(vec![EditOp::insert(a, "ab")]).unwrap();
6384 } else {
6385 d.edit(vec![EditOp::insert(a, "ba"), EditOp::insert(b, "ab")]).unwrap();
6386 }
6387 }
6388 }
6389 let (oracle, _) = crate::find::scan(&d.buffer().text(), &fq);
6390 assert_eq!(match_ranges(&d), oracle, "step {step} diverged from the oracle");
6391 }
6392 }
6393
6394 #[test]
6395 fn folding_repair_equals_full_scan_property() {
6396 let ks = ['k', 'K', '\u{212A}', '\u{212A}', '\u{212A}', 's', 'S', '\u{17F}', ' '];
6414 let sharp = ['ß', '\u{1E9E}', 'σ', 'Σ', 'ς', 'ς', ' '];
6415 let cases: [(u64, &str, &[char], &str); 4] = [
6416 (0x5DEE_CE66_D1CE_4E5B, "ks", &ks, "ks \u{212A}ſ KS k s "),
6417 (
6418 0x2545_F491_4F6C_DD1D,
6419 "kkkkks",
6420 &ks,
6421 "\u{212A}\u{212A}\u{212A}\u{212A}\u{212A}ſ kkkkks \u{212A}\u{212A}\u{212A}\u{212A}\u{212A} ſ KK\u{212A}KkS ",
6422 ),
6423 (0x9E37_79B9_7F4A_7C15, "\u{212A}ſſſ", &ks, "kSSS \u{212A}ſſſ Ksss "),
6424 (0x0DDB_A11C_AFE5_EED5, "ßσσσ", &sharp, "\u{1E9E}ΣΣΣ ßσσς \u{1E9E}ςςς ẞσσ "),
6425 ];
6426 for (seed, needle, alphabet, seed_text) in cases {
6427 let fq = crate::FindQuery { text: needle.into(), case_sensitive: false, ..Default::default() };
6428 let mut next = xorshift(seed);
6429 let rand_str = |next: &mut dyn FnMut() -> u64| -> String {
6430 (0..1 + next() % 3).map(|_| alphabet[(next() % alphabet.len() as u64) as usize]).collect()
6431 };
6432 let mut d = doc(&seed_text.repeat(6));
6433 d.set_find_query(Some(fq.clone()), 0);
6434 let at = |d: &Document, raw: u64| {
6436 d.buffer().clip_offset((raw % u64::from(d.buffer().len() + 1)) as u32, Bias::Left)
6437 };
6438 let mut nonempty = 0;
6439 for step in 0..400 {
6440 match next() % 6 {
6441 0 | 1 => {
6442 let p = at(&d, next());
6443 let s = rand_str(&mut next);
6444 d.edit(vec![EditOp::insert(p, s)]).unwrap();
6445 }
6446 2 if !d.buffer().is_empty() => {
6447 let a = at(&d, next());
6448 let b = d.buffer().clip_offset(a + 1 + (next() % 8) as u32, Bias::Right);
6449 if a < b {
6450 d.edit(vec![EditOp::delete(a..b)]).unwrap();
6451 }
6452 }
6453 3 => {
6454 let (a, b) = (at(&d, next()), at(&d, next()));
6456 let (a, b) = (a.min(b), a.max(b));
6457 let (s, t) = (rand_str(&mut next), rand_str(&mut next));
6458 let ops = if a == b {
6459 vec![EditOp::insert(a, s + &t)]
6460 } else {
6461 vec![EditOp::insert(a, s), EditOp::insert(b, t)]
6462 };
6463 d.edit(ops).unwrap();
6464 }
6465 4 => {
6466 d.undo();
6467 }
6468 _ => {
6469 d.redo();
6470 }
6471 }
6472 let (oracle, _) = crate::find::scan(&d.buffer().text(), &fq);
6473 nonempty += usize::from(!oracle.is_empty());
6474 assert_eq!(match_ranges(&d), oracle, "{needle:?} step {step} diverged from the oracle");
6475 }
6476 assert!(nonempty > 200, "{needle:?}: the oracle must not pass vacuously ({nonempty})");
6477 }
6478 }
6479
6480 #[test]
6481 fn repair_self_overlapping_needle_is_maximal_and_valid() {
6482 let mut next = xorshift(0x9E37_79B9_7F4A_7C15);
6488 let alphabet = ['a', ' '];
6489 let mut d = doc(&"aaa a aaaa aa ".repeat(6));
6490 d.set_find_query(Some(query("aa")), 0);
6491 for step in 0..300 {
6492 let len = d.buffer().len();
6493 if next().is_multiple_of(2) || len == 0 {
6494 let at = (next() % u64::from(len + 1)) as u32;
6495 let n = 1 + next() % 3;
6496 let s: String = (0..n).map(|_| alphabet[(next() % 2) as usize]).collect();
6497 d.edit(vec![EditOp::insert(at, s)]).unwrap();
6498 } else {
6499 let a = (next() % u64::from(len)) as u32;
6500 let b = (a + 1 + (next() % 3) as u32).min(len);
6501 d.edit(vec![EditOp::delete(a..b)]).unwrap();
6502 }
6503 let live = match_ranges(&d);
6504 let text = d.buffer().text();
6505 let bytes = text.as_bytes();
6506 for (i, r) in live.iter().enumerate() {
6507 assert_eq!(
6508 &bytes[r.start as usize..r.end as usize],
6509 b"aa",
6510 "step {step}: stale match {r:?}"
6511 );
6512 if i > 0 {
6513 assert!(live[i - 1].end <= r.start, "step {step}: overlapping matches");
6514 }
6515 }
6516 for p in 0..bytes.len().saturating_sub(1) {
6517 if &bytes[p..p + 2] == b"aa" {
6518 let p = p as u32;
6519 assert!(
6520 live.iter().any(|r| r.start < p + 2 && p < r.end),
6521 "step {step}: placement at {p} misses every match (not maximal)"
6522 );
6523 }
6524 }
6525 }
6526 }
6527
6528 #[test]
6529 fn active_survives_untouched() {
6530 let mut d = doc("foo bar foo");
6533 d.set_find_query(Some(query("foo")), 0);
6534 d.find_next(0); let id = d.find.active_id().expect("a match is active");
6536 d.edit(vec![EditOp::insert(7, "x")]).unwrap(); assert_eq!(d.find.active_id(), Some(id), "untouched active keeps its id");
6538 assert_eq!((d.find_match_count(), d.active_find_match()), (2, Some(0)));
6539 }
6540
6541 #[test]
6542 fn active_transfers_same_start() {
6543 let mut d = doc("foo bar");
6546 d.set_find_query(Some(query("foo")), 0);
6547 d.find_next(0); let old = d.find.active_id().expect("a match is active");
6549 d.edit(vec![EditOp::insert(4, "x")]).unwrap(); let new = d.find.active_id().expect("active transferred");
6551 assert_ne!(new, old, "the decoration was re-minted by the repair");
6552 assert_eq!(d.active_find_match(), Some(0), "still the first match");
6553 assert_eq!(match_ranges(&d), vec![0..3]);
6554 }
6555
6556 #[test]
6557 fn active_cleared_when_destroyed() {
6558 let mut d = doc("foo bar foo");
6559 d.set_find_query(Some(query("foo")), 0);
6560 d.find_next(0); d.edit(vec![EditOp::delete(0..2)]).unwrap(); assert_eq!(d.find.active_id(), None, "destroyed active clears");
6563 assert_eq!((d.find_match_count(), d.active_find_match()), (1, None));
6564 }
6565
6566 #[test]
6567 fn count_and_order_consistent_after_repair() {
6568 let mut d = doc("foo foo foo");
6569 d.set_find_query(Some(query("foo")), 0);
6570 d.set_selections(SelectionSet::new(4));
6571 d.find_next(0); d.edit(vec![EditOp::insert(0, "foo ")]).unwrap(); let all: Vec<(Range<u32>, bool)> = d.find_matches_in(0..u32::MAX).collect();
6574 let ranges: Vec<Range<u32>> = all.iter().map(|(r, _)| r.clone()).collect();
6575 assert_eq!(ranges, vec![0..3, 4..7, 8..11, 12..15]);
6576 assert_eq!(d.find_match_count(), all.len());
6577 assert_eq!(d.active_find_match(), Some(2));
6579 assert_eq!(all.iter().position(|(_, active)| *active), Some(2));
6580 }
6581
6582 #[test]
6583 fn active_start_tracks_the_decoration_under_random_edits() {
6584 let base = "foo foo bar foo baz foo qux foo end";
6595 let mut rng = 0xDEAD_BEEF_0000_1111u64;
6596 let mut next = move || {
6597 rng ^= rng << 13;
6598 rng ^= rng >> 7;
6599 rng ^= rng << 17;
6600 rng
6601 };
6602 for trial in 0..400u32 {
6603 let mut d = doc(base);
6604 d.set_find_query(Some(query("foo")), 0);
6605 d.set_selections(SelectionSet::new(10));
6608 d.find_next(0);
6609 for _step in 0..14 {
6610 if d.find.active_id().is_none() {
6611 d.set_selections(SelectionSet::new(1));
6613 d.find_next(0);
6614 }
6615 let len = d.buffer().len();
6616 let at = match d.find.active_start() {
6619 Some(s) if next() % 3 == 0 => s,
6620 _ => (next() % u64::from(len + 1)) as u32,
6621 };
6622 match next() % 3 {
6623 0 => {
6624 let _ = d.edit(vec![EditOp::insert(at, "z")]);
6625 }
6626 1 => {
6627 let end = (at + 1 + (next() % 3) as u32).min(len);
6628 if end > at {
6629 let _ = d.edit(vec![EditOp::delete(at..end)]);
6630 }
6631 }
6632 _ => {
6633 let a = (next() % u64::from(len + 1)) as u32;
6635 let b = (next() % u64::from(len + 1)) as u32;
6636 let (lo, hi) = (a.min(b), a.max(b));
6637 if lo != hi {
6638 d.set_selections(SelectionSet::from_ranges(&[(lo, lo), (hi, hi)], 0));
6639 d.insert_text("q");
6640 }
6641 }
6642 }
6643 let want = d
6644 .find
6645 .active_id()
6646 .and_then(|id| d.decorations.decoration_range(id))
6647 .map(|r| r.start);
6648 assert_eq!(
6649 d.find.active_start(),
6650 want,
6651 "trial {trial}: active_start drifted from its decoration's live start",
6652 );
6653 }
6654 }
6655 }
6656
6657 #[test]
6658 fn capped_scan_keeps_a_document_prefix() {
6659 let text = "x ".repeat(crate::FIND_MATCH_CAP + 50);
6662 let mut d = doc(&text);
6663 d.set_find_query(Some(query("x")), 0);
6664 assert!(d.find.capped());
6665 let live = match_ranges(&d);
6666 assert_eq!(live.len(), crate::FIND_MATCH_CAP);
6667 assert_eq!(live.last().cloned(), Some(19_998..19_999), "prefix, not a sample");
6668 }
6669
6670 #[test]
6671 fn typing_while_capped_does_not_rescan_the_match_set() {
6672 let text = "x ".repeat(crate::FIND_MATCH_CAP + 50);
6679 let mut d = doc(&text);
6680 d.set_find_query(Some(query("x")), 0);
6681 assert!(d.find.capped());
6682 assert_eq!(d.find_match_count(), crate::FIND_MATCH_CAP);
6683 d.set_selections(crate::SelectionSet::new(0));
6684 crate::perf::reset();
6685 d.type_char('z'); let work = crate::perf::meter();
6687 assert_eq!(d.find_match_count(), crate::FIND_MATCH_CAP, "still capped");
6688 assert!(
6689 work < 2_000,
6690 "capped keystroke charged {work} work units (≈FIND_MATCH_CAP ⇒ the O(M) cap scan survived)"
6691 );
6692 }
6693
6694 #[test]
6695 fn capped_trims_tail_and_coverage() {
6696 let text = "x ".repeat(crate::FIND_MATCH_CAP);
6700 let mut d = doc(&text);
6701 d.set_find_query(Some(query("x")), 0);
6702 assert!(!d.find.capped());
6703 d.edit(vec![EditOp::insert(1_000, "xxx")]).unwrap();
6704 assert!(d.find.capped(), "over the cap after the repair");
6705 let live = match_ranges(&d);
6706 assert_eq!(live.len(), crate::FIND_MATCH_CAP, "trimmed back to the cap");
6707 assert!(live.windows(2).all(|w| w[0].end <= w[1].start), "still non-overlapping");
6708 assert_eq!(live.iter().filter(|r| (1_000..1_004).contains(&r.start)).count(), 4);
6710 assert_eq!(live.last().map(|r| r.start), Some(19_995), "tail trimmed");
6711 }
6712
6713 #[test]
6714 fn capped_refill_is_debounced() {
6715 let text = "x ".repeat(crate::FIND_MATCH_CAP + 50);
6716 let mut d = doc(&text);
6717 d.set_find_query(Some(query("x")), 0); d.edit(vec![EditOp::delete(0..10)]).unwrap();
6720 assert_eq!(d.find_match_count(), crate::FIND_MATCH_CAP - 5);
6721 assert!(!d.maybe_rescan_find(50), "inside the debounce window");
6722 assert_eq!(d.find_match_count(), crate::FIND_MATCH_CAP - 5);
6723 assert!(d.maybe_rescan_find(100), "past the window → refill");
6724 assert_eq!(d.find_match_count(), crate::FIND_MATCH_CAP, "coverage re-grew");
6725 assert!(!d.maybe_rescan_find(300), "idle after the refill: no re-scan");
6726 }
6727
6728 #[test]
6729 fn undo_redo_match_set_equals_fresh_scan() {
6730 let fq = query("ab");
6733 let mut d = doc("ab ab ba ab");
6734 d.set_find_query(Some(fq.clone()), 0);
6735 let check = |d: &Document, at: &str| {
6736 let (oracle, _) = crate::find::scan(&d.buffer().text(), &fq);
6737 assert_eq!(match_ranges(d), oracle, "diverged after {at}");
6738 };
6739 d.edit(vec![EditOp::insert(2, "ab")]).unwrap();
6740 check(&d, "edit 1");
6741 d.edit(vec![EditOp::delete(0..3)]).unwrap();
6742 check(&d, "edit 2");
6743 d.edit(vec![EditOp::insert(5, "b a")]).unwrap();
6744 check(&d, "edit 3");
6745 assert!(d.undo());
6746 check(&d, "undo 1");
6747 assert!(d.undo());
6748 check(&d, "undo 2");
6749 assert!(d.undo());
6750 check(&d, "undo 3");
6751 assert!(d.redo());
6752 check(&d, "redo 1");
6753 assert!(d.redo());
6754 check(&d, "redo 2");
6755 }
6756
6757 #[test]
6758 fn case_fold_repair() {
6759 let fq = crate::FindQuery { text: "foo".into(), case_sensitive: false, ..Default::default() };
6761 let mut d = doc("FO bar");
6762 d.set_find_query(Some(fq), 0);
6763 assert_eq!(d.find_match_count(), 0);
6764 d.edit(vec![EditOp::insert(2, "O")]).unwrap(); assert_eq!(match_ranges(&d), vec![0..3]);
6766 d.edit(vec![EditOp::insert(0, "f")]).unwrap(); assert_eq!(match_ranges(&d), vec![1..4], "re-anchored, still folded");
6768 }
6769}