1use std::path::{Path, PathBuf};
11
12use ropey::Rope;
13use termesh_core::BufferId;
14use termesh_filesystem::{FileSystemService, FsError};
15
16use crate::change::ChangeSet;
17use crate::decoration::DecorationSet;
18use crate::history::History;
19use crate::movement;
20use crate::selection::Selection;
21use crate::transaction::{EditSource, EditTransaction, Version};
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
29pub enum LineEnding {
30 #[default]
31 Lf,
32 Crlf,
33}
34
35impl LineEnding {
36 fn detect(text: &str) -> Self {
39 let crlf = text.matches("\r\n").count();
40 let lf = text.matches('\n').count() - crlf;
41 if crlf > lf {
42 LineEnding::Crlf
43 } else {
44 LineEnding::Lf
45 }
46 }
47}
48
49#[derive(Debug, Clone, PartialEq, Eq)]
51pub enum EditError {
52 StaleVersion {
55 expected: Version,
56 found: Version,
57 },
58 LengthMismatch {
61 expected: usize,
62 found: usize,
63 },
64 NotUtf8(PathBuf),
66 Fs(FsError),
67}
68
69impl std::fmt::Display for EditError {
70 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
71 match self {
72 EditError::StaleVersion { expected, found } => write!(
73 f,
74 "edit was written against version {} but the buffer is at {}",
75 expected.0, found.0
76 ),
77 EditError::LengthMismatch { expected, found } => {
78 write!(f, "edit expects a {expected}-char document, buffer has {found}")
79 }
80 EditError::NotUtf8(p) => write!(f, "not valid UTF-8: {}", p.display()),
81 EditError::Fs(e) => write!(f, "{e}"),
82 }
83 }
84}
85
86impl std::error::Error for EditError {}
87
88impl From<FsError> for EditError {
89 fn from(e: FsError) -> Self {
90 EditError::Fs(e)
91 }
92}
93
94pub type EditResult<T> = Result<T, EditError>;
95
96#[derive(Debug)]
98pub struct Buffer {
99 id: BufferId,
100 path: Option<PathBuf>,
102 text: Rope,
103 version: Version,
104 selection: Selection,
105 history: History,
106 line_ending: LineEnding,
107 saved_version: Option<Version>,
109 goal_column: Option<usize>,
112 decorations: DecorationSet,
116 pending_changes: Vec<ChangeSet>,
118 scroll_top: usize,
125}
126
127impl Buffer {
128 pub fn new(id: BufferId) -> Self {
130 Self {
131 id,
132 path: None,
133 text: Rope::new(),
134 version: Version::default(),
135 selection: Selection::default(),
136 history: History::new(),
137 line_ending: LineEnding::default(),
138 saved_version: None,
139 goal_column: None,
140 decorations: DecorationSet::new(),
141 pending_changes: Vec::new(),
142 scroll_top: 0,
143 }
144 }
145
146 pub fn from_text(id: BufferId, path: Option<PathBuf>, text: &str) -> Self {
149 let line_ending = LineEnding::detect(text);
150 Self {
151 id,
152 path,
153 text: Rope::from_str(&text.replace("\r\n", "\n")),
154 version: Version::default(),
155 selection: Selection::default(),
156 history: History::new(),
157 line_ending,
158 saved_version: Some(Version::default()),
159 goal_column: None,
160 decorations: DecorationSet::new(),
161 pending_changes: Vec::new(),
162 scroll_top: 0,
163 }
164 }
165
166 pub fn load(id: BufferId, fs: &dyn FileSystemService, path: &Path) -> EditResult<Self> {
168 let bytes = fs.read_file(path)?;
169 let text = String::from_utf8(bytes).map_err(|_| EditError::NotUtf8(path.to_path_buf()))?;
170 Ok(Self::from_text(id, Some(path.to_path_buf()), &text))
171 }
172
173 pub fn save(&mut self, fs: &dyn FileSystemService) -> EditResult<()> {
178 let path = self.path.clone().ok_or_else(|| {
179 EditError::Fs(FsError::Other {
180 path: PathBuf::new(),
181 message: "buffer has no path; save-as is not wired up yet".into(),
182 })
183 })?;
184
185 fs.write_file(&path, self.to_disk_string().as_bytes())?;
186 self.saved_version = Some(self.version);
187 self.history.break_group();
188 Ok(())
189 }
190
191 pub fn to_disk_string(&self) -> String {
193 match self.line_ending {
194 LineEnding::Lf => self.text.to_string(),
195 LineEnding::Crlf => self.text.to_string().replace('\n', "\r\n"),
196 }
197 }
198
199 pub fn id(&self) -> BufferId {
200 self.id
201 }
202
203 pub fn path(&self) -> Option<&Path> {
204 self.path.as_deref()
205 }
206
207 pub fn text(&self) -> &Rope {
208 &self.text
209 }
210
211 pub fn version(&self) -> Version {
212 self.version
213 }
214
215 pub fn line_ending(&self) -> LineEnding {
216 self.line_ending
217 }
218
219 pub fn selection(&self) -> &Selection {
220 &self.selection
221 }
222
223 pub fn set_selection(&mut self, selection: Selection) {
226 self.selection = selection;
227 self.history.break_group();
228 }
229
230 pub fn is_dirty(&self) -> bool {
232 self.saved_version != Some(self.version)
233 }
234
235 pub fn display_name(&self) -> String {
237 match &self.path {
238 Some(p) => p.file_name().unwrap_or(p.as_os_str()).to_string_lossy().into_owned(),
239 None => "untitled".to_string(),
240 }
241 }
242
243 pub fn can_undo(&self) -> bool {
244 self.history.can_undo()
245 }
246
247 pub fn can_redo(&self) -> bool {
248 self.history.can_redo()
249 }
250
251 pub fn take_pending_changes(&mut self) -> Vec<ChangeSet> {
256 std::mem::take(&mut self.pending_changes)
257 }
258
259 pub fn transaction(&mut self, changes: ChangeSet, source: EditSource) -> EditTransaction {
264 let group = self.history.group_for(&source);
265 EditTransaction::new(self.id, self.version, changes, source, group)
266 }
267
268 pub fn apply(&mut self, transaction: &EditTransaction) -> EditResult<()> {
274 if transaction.base_version != self.version {
275 return Err(EditError::StaleVersion {
276 expected: transaction.base_version,
277 found: self.version,
278 });
279 }
280 if transaction.changes.len_before() != self.text.len_chars() {
281 return Err(EditError::LengthMismatch {
282 expected: transaction.changes.len_before(),
283 found: self.text.len_chars(),
284 });
285 }
286 if transaction.is_empty() {
287 return Ok(());
288 }
289
290 let inverse = transaction.changes.invert(&self.text);
292
293 self.text = transaction.changes.apply(&self.text);
294 self.version = self.version.next();
295 self.decorations.map(&transaction.changes);
298 self.selection = match &transaction.selection {
299 Some(explicit) => explicit.clone(),
300 None => self.selection.map(&transaction.changes),
301 };
302 self.pending_changes.push(transaction.changes.clone());
303 self.history.push(transaction, inverse);
304 Ok(())
305 }
306
307 pub fn edit(
309 &mut self,
310 from: usize,
311 to: usize,
312 insert: &str,
313 source: EditSource,
314 ) -> EditResult<()> {
315 let changes = ChangeSet::replace(self.text.len_chars(), from, to, insert);
316 let transaction = self.transaction(changes, source);
317 self.apply(&transaction)
318 }
319
320 pub fn mark_saved(&mut self, version: Version) {
326 self.saved_version = Some(version);
327 self.history.break_group();
328 }
329
330 fn cursor(&self) -> usize {
336 self.selection.primary().head
337 }
338
339 fn place_cursor(&mut self, pos: usize) {
341 self.goal_column = None;
342 self.set_selection(Selection::point(pos));
343 }
344
345 pub fn move_left(&mut self) {
346 let pos = movement::left(&self.text, self.cursor());
347 self.place_cursor(pos);
348 }
349
350 pub fn move_right(&mut self) {
351 let pos = movement::right(&self.text, self.cursor());
352 self.place_cursor(pos);
353 }
354
355 pub fn move_line_start(&mut self) {
356 let pos = movement::line_start(&self.text, self.cursor());
357 self.place_cursor(pos);
358 }
359
360 pub fn move_line_end(&mut self) {
361 let pos = movement::line_end(&self.text, self.cursor());
362 self.place_cursor(pos);
363 }
364
365 pub fn move_line(&mut self, down: bool) {
368 let cursor = self.cursor();
369 let goal = self.goal_column.or_else(|| Some(movement::column_of(&self.text, cursor)));
370 let pos = if down {
371 movement::down(&self.text, cursor, goal)
372 } else {
373 movement::up(&self.text, cursor, goal)
374 };
375 self.set_selection(Selection::point(pos));
376 self.goal_column = goal;
377 }
378
379 pub fn decorations(&self) -> &DecorationSet {
381 &self.decorations
382 }
383
384 pub fn decorations_mut(&mut self) -> &mut DecorationSet {
385 &mut self.decorations
386 }
387
388 pub fn line_range(&self, line: usize) -> (usize, usize) {
390 if line >= self.text.len_lines() {
391 let end = self.text.len_chars();
392 return (end, end);
393 }
394 let start = self.text.line_to_char(line);
395 (start, movement::line_end(&self.text, start))
396 }
397
398 pub fn scroll_top(&self) -> usize {
399 self.scroll_top
400 }
401
402 pub fn scroll_to_cursor(&mut self, height: usize) {
407 if height == 0 {
408 return;
409 }
410 let margin = if height > 4 { 1 } else { 0 };
412 let (line, _) = self.cursor_position();
413
414 if line < self.scroll_top + margin {
415 self.scroll_top = line.saturating_sub(margin);
416 } else if line + margin >= self.scroll_top + height {
417 self.scroll_top = (line + margin + 1).saturating_sub(height);
418 }
419 }
420
421 pub fn cursor_position(&self) -> (usize, usize) {
422 let cursor = self.cursor();
423 (movement::line_of(&self.text, cursor), movement::column_of(&self.text, cursor))
424 }
425
426 pub fn insert(&mut self, text: &str, source: EditSource) -> EditResult<()> {
430 let range = self.selection.primary();
431 self.goal_column = None;
432 self.edit(range.start(), range.end(), text, source)
433 }
434
435 pub fn delete_backward(&mut self) -> EditResult<()> {
437 let range = self.selection.primary();
438 self.goal_column = None;
439 if !range.is_empty() {
440 return self.edit(range.start(), range.end(), "", EditSource::Keyboard);
441 }
442 let cursor = range.head;
443 if cursor == 0 {
444 return Ok(());
445 }
446 self.edit(cursor - 1, cursor, "", EditSource::Keyboard)
447 }
448
449 pub fn delete_forward(&mut self) -> EditResult<()> {
451 let range = self.selection.primary();
452 self.goal_column = None;
453 if !range.is_empty() {
454 return self.edit(range.start(), range.end(), "", EditSource::Keyboard);
455 }
456 let cursor = range.head;
457 if cursor >= self.text.len_chars() {
458 return Ok(());
459 }
460 self.edit(cursor, cursor + 1, "", EditSource::Keyboard)
461 }
462
463 pub fn undo(&mut self) -> bool {
465 match self.history.undo() {
466 Some(changes) => {
467 self.replay(&changes);
468 true
469 }
470 None => false,
471 }
472 }
473
474 pub fn redo(&mut self) -> bool {
475 match self.history.redo() {
476 Some(changes) => {
477 self.replay(&changes);
478 true
479 }
480 None => false,
481 }
482 }
483
484 fn replay(&mut self, changes: &ChangeSet) {
489 debug_assert_eq!(changes.len_before(), self.text.len_chars());
490 self.text = changes.apply(&self.text);
491 self.version = self.version.next();
492 self.selection = self.selection.map(changes);
493 self.decorations.map(changes);
494 self.pending_changes.push(changes.clone());
495 }
496}
497
498#[cfg(test)]
499mod tests {
500 use super::*;
501 use crate::decoration::{Decoration, DecorationClass, HunkSide};
502 use crate::selection::Range;
503 use crate::HunkState;
504 use termesh_core::ProposalId;
505 use termesh_test_support::FakeFileSystem;
506
507 fn buffer(text: &str) -> Buffer {
508 Buffer::from_text(BufferId::new(1), Some(PathBuf::from("/proj/main.rs")), text)
509 }
510
511 #[test]
512 fn a_new_buffer_is_empty_untitled_and_clean() {
513 let b = Buffer::new(BufferId::new(1));
514 assert_eq!(b.text().to_string(), "");
515 assert_eq!(b.display_name(), "untitled");
516 assert!(b.path().is_none());
517 assert!(b.is_dirty(), "an unsaved untitled buffer has nowhere to have been saved to");
518 }
519
520 #[test]
521 fn editing_bumps_the_version_and_marks_it_dirty() {
522 let mut b = buffer("hello");
523 assert!(!b.is_dirty());
524 let before = b.version();
525
526 b.edit(5, 5, " world", EditSource::Keyboard).unwrap();
527 assert_eq!(b.text().to_string(), "hello world");
528 assert_eq!(b.version(), before.next());
529 assert!(b.is_dirty());
530 }
531
532 #[test]
533 fn an_applied_transaction_is_queued_for_document_sync() {
534 let mut b = Buffer::from_text(BufferId::new(1), None, "fn main() {}");
535 b.edit(3, 7, "test", EditSource::Keyboard).unwrap();
536 let queued = b.take_pending_changes();
537 assert_eq!(queued.len(), 1);
538 assert!(b.take_pending_changes().is_empty(), "draining is destructive");
539 }
540
541 #[test]
542 fn undo_and_redo_are_queued_too() {
543 let mut b = Buffer::from_text(BufferId::new(1), None, "abc");
546 b.edit(0, 0, "x", EditSource::Keyboard).unwrap();
547 let _ = b.take_pending_changes();
548
549 assert!(b.undo());
550 assert_eq!(b.take_pending_changes().len(), 1, "undo must be sent to the server");
551
552 assert!(b.redo());
553 assert_eq!(b.take_pending_changes().len(), 1, "redo must be sent to the server");
554 }
555
556 #[test]
557 fn an_empty_transaction_queues_nothing() {
558 let mut b = Buffer::from_text(BufferId::new(1), None, "abc");
559 let tx = b.transaction(ChangeSet::identity(3), EditSource::Keyboard);
560 b.apply(&tx).unwrap();
561 assert!(b.take_pending_changes().is_empty());
562 }
563
564 #[test]
565 fn a_stale_transaction_is_refused_rather_than_applied() {
566 let mut b = buffer("hello");
567 let stale = b.transaction(
569 ChangeSet::replace(5, 0, 5, "goodbye"),
570 EditSource::Agent(ProposalId::new(1)),
571 );
572 b.edit(5, 5, "!", EditSource::Keyboard).unwrap();
574
575 let err = b.apply(&stale).unwrap_err();
576 assert!(matches!(err, EditError::StaleVersion { .. }), "got {err:?}");
577 assert_eq!(b.text().to_string(), "hello!", "the document is untouched");
578 }
579
580 #[test]
581 fn a_changeset_for_a_different_document_is_refused() {
582 let mut b = buffer("hello");
583 let wrong = EditTransaction::new(
584 b.id(),
585 b.version(),
586 ChangeSet::replace(99, 0, 1, "x"),
587 EditSource::Keyboard,
588 Default::default(),
589 );
590 assert!(matches!(b.apply(&wrong), Err(EditError::LengthMismatch { .. })));
591 assert_eq!(b.text().to_string(), "hello");
592 }
593
594 #[test]
595 fn an_empty_transaction_changes_nothing_and_is_not_an_error() {
596 let mut b = buffer("hello");
597 let before = b.version();
598 b.edit(2, 2, "", EditSource::Keyboard).unwrap();
599 assert_eq!(b.version(), before, "a no-op does not advance the revision");
600 assert!(!b.can_undo());
601 }
602
603 #[test]
604 fn the_cursor_rides_along_with_an_edit_before_it() {
605 let mut b = buffer("hello world");
606 b.set_selection(Selection::point(6));
607 b.edit(0, 0, ">> ", EditSource::Paste).unwrap();
608 assert_eq!(b.selection().primary(), Range::point(9));
609 }
610
611 #[test]
612 fn a_transaction_can_pin_the_cursor_explicitly() {
613 let mut b = buffer("hello");
614 let tx = b
615 .transaction(ChangeSet::replace(5, 0, 0, "abc"), EditSource::Keyboard)
616 .with_selection(Selection::point(0));
617 b.apply(&tx).unwrap();
618 assert_eq!(b.selection().primary(), Range::point(0), "the explicit choice wins");
619 }
620
621 #[test]
624 fn undo_and_redo_move_the_document_and_the_version() {
625 let mut b = buffer("hello");
626 b.edit(5, 5, " world", EditSource::Paste).unwrap();
627
628 assert!(b.undo());
629 assert_eq!(b.text().to_string(), "hello");
630 assert!(b.redo());
631 assert_eq!(b.text().to_string(), "hello world");
632 assert!(!b.redo(), "nothing left to redo");
633 }
634
635 #[test]
636 fn an_agent_edit_undoes_in_one_step_and_stays_traceable() {
637 let mut b = buffer("fn main() {}");
638 let source = EditSource::Agent(ProposalId::new(7));
639
640 let tx = b.transaction(ChangeSet::replace(12, 3, 7, "run"), source);
641 assert_eq!(tx.proposal(), Some(ProposalId::new(7)));
642 b.apply(&tx).unwrap();
643 assert_eq!(b.text().to_string(), "fn run() {}");
644
645 assert!(b.undo());
646 assert_eq!(b.text().to_string(), "fn main() {}");
647 }
648
649 #[test]
652 fn a_file_loads_edits_and_saves_through_the_service() {
653 let fs = FakeFileSystem::with_paths(&["/proj/main.rs"]);
654 fs.add_file("/proj/main.rs", b"fn main() {}\n");
655
656 let mut b = Buffer::load(BufferId::new(1), &fs, Path::new("/proj/main.rs")).unwrap();
657 assert_eq!(b.text().to_string(), "fn main() {}\n");
658 assert_eq!(b.display_name(), "main.rs");
659 assert!(!b.is_dirty());
660
661 b.edit(3, 7, "run", EditSource::Keyboard).unwrap();
662 assert!(b.is_dirty());
663
664 b.save(&fs).unwrap();
665 assert!(!b.is_dirty(), "saving settles the dirty flag");
666 assert_eq!(fs.read_file(Path::new("/proj/main.rs")).unwrap(), b"fn run() {}\n");
667 }
668
669 #[test]
670 fn saving_ends_the_undo_group() {
671 let fs = FakeFileSystem::with_paths(&["/proj/main.rs"]);
672 fs.add_file("/proj/main.rs", b"()");
673 let mut b = Buffer::load(BufferId::new(1), &fs, Path::new("/proj/main.rs")).unwrap();
674
675 b.edit(1, 1, "a", EditSource::Keyboard).unwrap();
676 b.save(&fs).unwrap();
677 b.edit(2, 2, "b", EditSource::Keyboard).unwrap();
678
679 b.undo();
680 assert_eq!(b.text().to_string(), "(a)", "typing after a save is its own step");
681 }
682
683 #[test]
684 fn crlf_survives_a_round_trip() {
685 let fs = FakeFileSystem::with_paths(&["/proj/win.rs"]);
686 fs.add_file("/proj/win.rs", b"one\r\ntwo\r\n");
687
688 let mut b = Buffer::load(BufferId::new(1), &fs, Path::new("/proj/win.rs")).unwrap();
689 assert_eq!(b.text().to_string(), "one\ntwo\n");
691 assert_eq!(b.line_ending(), LineEnding::Crlf);
692
693 b.save(&fs).unwrap();
694 assert_eq!(
695 fs.read_file(Path::new("/proj/win.rs")).unwrap(),
696 b"one\r\ntwo\r\n",
697 "saving must not rewrite every line of somebody's diff"
698 );
699 }
700
701 #[test]
702 fn lf_files_stay_lf() {
703 let mut b = buffer("one\ntwo\n");
704 assert_eq!(b.line_ending(), LineEnding::Lf);
705 b.edit(0, 0, "x", EditSource::Keyboard).unwrap();
706 assert_eq!(b.to_disk_string(), "xone\ntwo\n");
707 }
708
709 #[test]
710 fn a_non_utf8_file_is_refused_by_name() {
711 let fs = FakeFileSystem::with_paths(&["/proj/blob.bin"]);
712 fs.add_file("/proj/blob.bin", &[0xff, 0xfe, 0x00]);
713
714 let err = Buffer::load(BufferId::new(1), &fs, Path::new("/proj/blob.bin")).unwrap_err();
715 assert!(matches!(err, EditError::NotUtf8(_)), "got {err:?}");
716 assert!(err.to_string().contains("blob.bin"), "the message names the file");
717 }
718
719 #[test]
720 fn a_missing_file_reports_the_filesystem_error() {
721 let fs = FakeFileSystem::with_paths(&["/proj/main.rs"]);
722 let err = Buffer::load(BufferId::new(1), &fs, Path::new("/proj/nope.rs")).unwrap_err();
723 assert!(matches!(err, EditError::Fs(FsError::NotFound(_))), "got {err:?}");
724 }
725
726 #[test]
729 fn typing_inserts_at_the_cursor_and_carries_it_along() {
730 let mut b = buffer("()");
731 b.set_selection(Selection::point(1));
732 for ch in ["a", "b", "c"] {
733 b.insert(ch, EditSource::Keyboard).unwrap();
734 }
735 assert_eq!(b.text().to_string(), "(abc)");
736 assert_eq!(b.cursor_position(), (0, 4));
737 }
738
739 #[test]
740 fn a_run_of_typing_is_one_undo_step_but_a_cursor_move_splits_it() {
741 let mut b = buffer("()");
742 b.set_selection(Selection::point(1));
743 b.insert("a", EditSource::Keyboard).unwrap();
744 b.insert("b", EditSource::Keyboard).unwrap();
745 b.undo();
746 assert_eq!(b.text().to_string(), "()", "one run, one undo");
747
748 b.set_selection(Selection::point(1));
749 b.insert("x", EditSource::Keyboard).unwrap();
750 b.move_right(); b.insert("y", EditSource::Keyboard).unwrap();
752 b.undo();
753 assert_eq!(b.text().to_string(), "(x)", "the move ended the run");
754 }
755
756 #[test]
757 fn typing_over_a_selection_replaces_it() {
758 let mut b = buffer("hello world");
759 b.set_selection(Selection::single(Range::new(0, 5)));
760 b.insert("bye", EditSource::Keyboard).unwrap();
761 assert_eq!(b.text().to_string(), "bye world");
762 }
763
764 #[test]
765 fn backspace_and_delete_take_one_character_each_way() {
766 let mut b = buffer("abcd");
767 b.set_selection(Selection::point(2));
768 b.delete_backward().unwrap();
769 assert_eq!(b.text().to_string(), "acd");
770 b.delete_forward().unwrap();
771 assert_eq!(b.text().to_string(), "ad");
772 }
773
774 #[test]
775 fn deleting_at_the_edges_of_the_document_does_nothing() {
776 let mut b = buffer("ab");
777 b.set_selection(Selection::point(0));
778 b.delete_backward().unwrap();
779 b.set_selection(Selection::point(2));
780 b.delete_forward().unwrap();
781 assert_eq!(b.text().to_string(), "ab", "no edit, and no panic at the boundaries");
782 assert!(!b.can_undo(), "and nothing recorded to undo");
783 }
784
785 #[test]
786 fn deleting_removes_the_selection_when_there_is_one() {
787 let mut b = buffer("hello world");
788 b.set_selection(Selection::single(Range::new(5, 11)));
789 b.delete_backward().unwrap();
790 assert_eq!(b.text().to_string(), "hello");
791 }
792
793 #[test]
794 fn a_newline_splits_the_line_and_moves_the_cursor_down() {
795 let mut b = buffer("ab");
796 b.set_selection(Selection::point(1));
797 b.insert("\n", EditSource::Keyboard).unwrap();
798 assert_eq!(b.text().to_string(), "a\nb");
799 assert_eq!(b.cursor_position(), (1, 0));
800 }
801
802 #[test]
803 fn vertical_motion_keeps_its_column_across_a_short_line() {
804 let mut b = buffer("abcdefgh\nxy\nabcdefgh\n");
805 b.set_selection(Selection::point(6)); b.move_line(true);
808 assert_eq!(b.cursor_position(), (1, 2), "clamped to the short line");
809 b.move_line(true);
810 assert_eq!(b.cursor_position(), (2, 6), "and restored below it");
811 }
812
813 #[test]
814 fn horizontal_motion_forgets_the_sticky_column() {
815 let mut b = buffer("abcdefgh\nxy\nabcdefgh\n");
816 b.set_selection(Selection::point(6));
817 b.move_line(true); b.move_left(); b.move_line(true);
820 assert_eq!(b.cursor_position(), (2, 1), "the new column wins");
821 }
822
823 #[test]
824 fn home_and_end_land_on_the_visible_ends_of_the_line() {
825 let mut b = buffer(" indented\nnext\n");
826 b.set_selection(Selection::point(5));
827 b.move_line_end();
828 assert_eq!(b.cursor_position(), (0, 10), "before the newline, not after it");
829 b.move_line_start();
830 assert_eq!(b.cursor_position(), (0, 0));
831 }
832
833 #[test]
836 fn a_hunk_stays_anchored_to_its_code_while_the_human_types_above_it() {
837 let mut b = buffer("fn main() {}\n");
839 b.decorations_mut().push(Decoration::new(
840 3,
841 7,
842 DecorationClass::Hunk {
843 proposal: ProposalId::new(1),
844 side: HunkSide::Removed,
845 state: HunkState::Clean,
846 },
847 ));
848
849 b.set_selection(Selection::point(0));
850 b.insert("pub ", EditSource::Keyboard).unwrap();
851
852 let d = b.decorations().iter().next().unwrap();
853 assert_eq!((d.start, d.end), (7, 11), "still on `main`, four chars further along");
854 assert_eq!(b.text().to_string(), "pub fn main() {}\n");
855 }
856
857 #[test]
858 fn editing_inside_a_hunk_conflicts_it_rather_than_dropping_it() {
859 let mut b = buffer("fn main() {}\n");
860 b.decorations_mut().push(Decoration::new(
861 3,
862 7,
863 DecorationClass::Hunk {
864 proposal: ProposalId::new(1),
865 side: HunkSide::Removed,
866 state: HunkState::Clean,
867 },
868 ));
869
870 b.set_selection(Selection::point(5));
871 b.insert("X", EditSource::Keyboard).unwrap();
872
873 let d = b.decorations().iter().next().unwrap();
874 assert!(
875 matches!(d.class, DecorationClass::Hunk { state: HunkState::Conflicted(_), .. }),
876 "the human must be told, not silently overruled"
877 );
878 }
879
880 #[test]
881 fn line_ranges_cover_the_visible_text_of_each_line() {
882 let b = buffer("abc\ndefgh\n");
883 assert_eq!(b.line_range(0), (0, 3), "excludes the newline");
884 assert_eq!(b.line_range(1), (4, 9));
885 }
886
887 #[test]
888 fn a_line_past_the_end_reports_an_empty_range_at_the_end() {
889 let b = buffer("abc");
890 let end = b.text().len_chars();
891 assert_eq!(b.line_range(99), (end, end));
892 }
893
894 #[test]
897 fn marking_saved_settles_the_dirty_flag() {
898 let mut b = buffer("hello");
899 b.edit(5, 5, "!", EditSource::Keyboard).unwrap();
900 let version = b.version();
901 assert!(b.is_dirty());
902
903 b.mark_saved(version);
904 assert!(!b.is_dirty());
905 }
906
907 #[test]
910 fn typing_while_a_save_is_in_flight_leaves_the_buffer_dirty() {
911 let mut b = buffer("hello");
912 b.edit(5, 5, "!", EditSource::Keyboard).unwrap();
913 let in_flight = b.version();
914
915 b.edit(6, 6, "?", EditSource::Keyboard).unwrap(); b.mark_saved(in_flight);
917
918 assert!(b.is_dirty(), "what reached disk is not what is in the buffer");
919 }
920
921 #[test]
922 fn multibyte_content_edits_by_char_offset() {
923 let mut b = buffer("héllo wörld");
924 b.edit(6, 11, "there", EditSource::Keyboard).unwrap();
925 assert_eq!(b.text().to_string(), "héllo there");
926 }
927}