1use crate::console::{CharsXY, ClearType, Console, Key};
19use async_trait::async_trait;
20use endbasic_std::console::{AnsiColor, LineBuffer};
21use endbasic_std::program::Program;
22use std::cmp;
23use std::convert::TryFrom;
24use std::io;
25
26const TEXT_COLOR: (Option<u8>, Option<u8>) = (Some(AnsiColor::White as u8), None);
28
29const STATUS_COLOR: (Option<u8>, Option<u8>) =
31 (Some(AnsiColor::BrightWhite as u8), Some(AnsiColor::Blue as u8));
32
33const INDENT_WIDTH: usize = 4;
35
36const KEYS_SUMMARY: &str = " ESC Exit ";
38
39fn copy_indent(line: &LineBuffer) -> String {
41 let mut indent = String::new();
42 for ch in line.chars() {
43 if !ch.is_whitespace() {
44 break;
45 }
46 indent.push(ch);
47 }
48 indent
49}
50
51fn find_indent_end(line: &LineBuffer) -> usize {
54 let mut pos = 0;
55 for ch in line.chars() {
56 if ch != ' ' {
57 break;
58 }
59 pos += 1;
60 }
61 debug_assert!(pos <= line.len());
62 pos
63}
64
65#[derive(Clone, Copy, Default)]
67struct FilePos {
68 line: usize,
70
71 col: usize,
73}
74
75#[derive(Clone, Copy, Eq, Ord, PartialEq, PartialOrd)]
80enum RefreshKind {
81 None,
82 CurrentLine,
83 Full,
84}
85
86impl RefreshKind {
87 fn upgrade(&mut self, new_value: Self) {
89 if new_value > *self {
90 *self = new_value;
91 }
92 }
93}
94
95pub struct Editor {
99 name: Option<String>,
101
102 content: Vec<LineBuffer>,
104
105 dirty: bool,
107
108 viewport_pos: FilePos,
110
111 file_pos: FilePos,
113
114 insert_col: usize,
117}
118
119impl Default for Editor {
120 fn default() -> Self {
122 Self {
123 name: None,
124 content: vec![],
125 dirty: false,
126 viewport_pos: FilePos::default(),
127 file_pos: FilePos::default(),
128 insert_col: 0,
129 }
130 }
131}
132
133impl Editor {
134 fn join_next_line_into(&mut self, line: usize) {
136 let next = self.content.remove(line + 1);
137 self.content[line].push_str(&next);
138 self.dirty = true;
139 }
140
141 fn refresh_status(&self, console: &mut dyn Console, console_size: CharsXY) -> io::Result<()> {
148 let dirty_marker = if self.dirty { "*" } else { "" };
151 let long_details = format!(
152 " | {}{} | Ln {}, Col {} ",
153 self.name.as_deref().unwrap_or("<NO NAME>"),
154 dirty_marker,
155 self.file_pos.line + 1,
156 self.file_pos.col + 1
157 );
158
159 let width = usize::from(console_size.x);
160 let mut status = String::with_capacity(width);
161 if KEYS_SUMMARY.len() + long_details.len() >= width {
162 let short_details = format!(" {}:{} ", self.file_pos.line + 1, self.file_pos.col + 1);
163 if short_details.len() < width {
164 while status.len() < width - short_details.len() {
165 status.push(' ');
166 }
167 }
168 status.push_str(&short_details);
169 } else {
170 status.push_str(KEYS_SUMMARY);
171 while status.len() < width - long_details.len() {
172 status.push(' ');
173 }
174 status.push_str(&long_details);
175 }
176 status.truncate(width);
177
178 console.locate(CharsXY::new(0, console_size.y - 1))?;
179 console.set_color(STATUS_COLOR.0, STATUS_COLOR.1)?;
180 console.write(&status)?;
181 Ok(())
182 }
183
184 fn refresh(&self, console: &mut dyn Console, console_size: CharsXY) -> io::Result<()> {
190 console.set_color(TEXT_COLOR.0, TEXT_COLOR.1)?;
191 console.clear(ClearType::All)?;
192 self.refresh_status(console, console_size)?;
193 console.set_color(TEXT_COLOR.0, TEXT_COLOR.1)?;
194 console.locate(CharsXY::default())?;
195
196 let mut row = self.viewport_pos.line;
197 let mut printed_rows = 0;
198 while row < self.content.len() && printed_rows < console_size.y - 1 {
199 let line = &self.content[row];
200 let line_len = line.len();
201 if line_len > self.viewport_pos.col {
202 console.print(&line.range(
203 self.viewport_pos.col,
204 self.viewport_pos.col + usize::from(console_size.x),
205 ))?;
206 } else {
207 console.print("")?;
208 }
209 row += 1;
210 printed_rows += 1;
211 }
212 Ok(())
213 }
214
215 fn refresh_current_line(
217 &self,
218 console: &mut dyn Console,
219 console_size: CharsXY,
220 ) -> io::Result<()> {
221 debug_assert!(self.file_pos.line >= self.viewport_pos.line);
222 debug_assert!(
223 self.file_pos.line < self.viewport_pos.line + usize::from(console_size.y - 1)
224 );
225
226 let row = self.file_pos.line - self.viewport_pos.line;
227 let row = if cfg!(debug_assertions) {
228 u16::try_from(row).expect("Computed y must have fit on screen")
229 } else {
230 row as u16
231 };
232
233 console.set_color(TEXT_COLOR.0, TEXT_COLOR.1)?;
234 console.locate(CharsXY::new(0, row))?;
235 console.clear(ClearType::CurrentLine)?;
236
237 let line = &self.content[self.file_pos.line];
238 if line.len() > self.viewport_pos.col {
239 console.write(&line.range(
240 self.viewport_pos.col,
241 self.viewport_pos.col + usize::from(console_size.x),
242 ))?;
243 }
244 Ok(())
245 }
246
247 fn move_down(&mut self, nlines: usize) {
250 if self.file_pos.line + nlines < self.content.len() {
251 self.file_pos.line += nlines;
252 } else {
253 self.file_pos.line = self.content.len() - 1;
254 }
255
256 let line = &self.content[self.file_pos.line];
257 self.file_pos.col = cmp::min(self.insert_col, line.len());
258 }
259
260 fn move_up(&mut self, nlines: usize) {
263 if self.file_pos.line > nlines {
264 self.file_pos.line -= nlines;
265 } else {
266 self.file_pos.line = 0;
267 }
268
269 let line = &self.content[self.file_pos.line];
270 self.file_pos.col = cmp::min(self.insert_col, line.len());
271 }
272
273 async fn edit_interactively(&mut self, console: &mut dyn Console) -> io::Result<()> {
275 let console_size = console.size_chars()?;
276
277 if self.content.is_empty() {
278 self.content.push(LineBuffer::default());
279 }
280
281 let mut refresh = RefreshKind::Full;
282 loop {
283 let width = usize::from(console_size.x);
286 let height = usize::from(console_size.y);
287 if self.file_pos.line < self.viewport_pos.line {
288 self.viewport_pos.line = self.file_pos.line;
289 refresh.upgrade(RefreshKind::Full);
290 } else if self.file_pos.line > self.viewport_pos.line + height - 2 {
291 if self.file_pos.line > height - 2 {
292 self.viewport_pos.line = self.file_pos.line - (height - 2);
293 } else {
294 self.viewport_pos.line = 0;
295 }
296 refresh.upgrade(RefreshKind::Full);
297 }
298
299 if self.file_pos.col < self.viewport_pos.col {
300 self.viewport_pos.col = self.file_pos.col;
301 refresh.upgrade(RefreshKind::Full);
302 } else if self.file_pos.col >= self.viewport_pos.col + width {
303 self.viewport_pos.col = self.file_pos.col - width + 1;
304 refresh.upgrade(RefreshKind::Full);
305 }
306
307 console.hide_cursor()?;
312 match refresh {
313 RefreshKind::Full => self.refresh(console, console_size)?,
314 RefreshKind::CurrentLine => {
315 self.refresh_status(console, console_size)?;
316 self.refresh_current_line(console, console_size)?;
317 }
318 RefreshKind::None => {
319 self.refresh_status(console, console_size)?;
320 console.set_color(TEXT_COLOR.0, TEXT_COLOR.1)?;
321 }
322 }
323 refresh = RefreshKind::None;
324 let cursor_pos = {
325 let x = self.file_pos.col - self.viewport_pos.col;
326 let y = self.file_pos.line - self.viewport_pos.line;
327 if cfg!(debug_assertions) {
328 CharsXY::new(
329 u16::try_from(x).expect("Computed x must have fit on screen"),
330 u16::try_from(y).expect("Computed y must have fit on screen"),
331 )
332 } else {
333 CharsXY::new(x as u16, y as u16)
334 }
335 };
336 console.locate(cursor_pos)?;
337 console.show_cursor()?;
338 console.sync_now()?;
339
340 match console.read_key().await? {
341 Key::Escape | Key::Interrupt => break,
342
343 Key::ArrowUp => self.move_up(1),
344
345 Key::ArrowDown => self.move_down(1),
346
347 Key::ArrowLeft => {
348 if self.file_pos.col > 0 {
349 self.file_pos.col -= 1;
350 self.insert_col = self.file_pos.col;
351 }
352 }
353
354 Key::ArrowRight => {
355 if self.file_pos.col < self.content[self.file_pos.line].len() {
356 self.file_pos.col += 1;
357 self.insert_col = self.file_pos.col;
358 }
359 }
360
361 Key::Backspace => {
362 if self.file_pos.col > 0 {
363 let line = &mut self.content[self.file_pos.line];
364
365 let indent_pos = find_indent_end(line);
366 let is_indent = indent_pos >= self.file_pos.col;
367 let nremove = if is_indent {
368 let new_pos = if self.file_pos.col >= INDENT_WIDTH {
369 (self.file_pos.col - 1) / INDENT_WIDTH * INDENT_WIDTH
370 } else {
371 0
372 };
373 self.file_pos.col - new_pos
374 } else {
375 1
376 };
377
378 if self.file_pos.col == line.len() {
379 if nremove > 0 {
380 console.hide_cursor()?;
381 }
382 for _ in 0..nremove {
383 console.clear(ClearType::PreviousChar)?;
384 }
385 if nremove > 0 {
386 console.show_cursor()?;
387 }
388 } else {
389 refresh.upgrade(RefreshKind::CurrentLine);
390 }
391 for _ in 0..nremove {
392 line.remove(self.file_pos.col - 1);
393 self.file_pos.col -= 1;
394 }
395 if nremove > 0 {
396 self.dirty = true;
397 }
398 } else if self.file_pos.line > 0 {
399 self.file_pos.col = self.content[self.file_pos.line - 1].len();
400 self.join_next_line_into(self.file_pos.line - 1);
401 self.file_pos.line -= 1;
402 refresh.upgrade(RefreshKind::Full);
403 }
404 self.insert_col = self.file_pos.col;
405 }
406
407 Key::Delete | Key::EofOrDelete => {
408 if self.file_pos.col < self.content[self.file_pos.line].len() {
409 let line = &mut self.content[self.file_pos.line];
410 line.remove(self.file_pos.col);
411 self.dirty = true;
412 if self.file_pos.col < line.len() {
413 refresh.upgrade(RefreshKind::Full);
414 } else {
415 console.hide_cursor()?;
416 console.clear(ClearType::UntilNewLine)?;
417 console.show_cursor()?;
418 }
419 } else if self.file_pos.line + 1 < self.content.len() {
420 self.join_next_line_into(self.file_pos.line);
421 refresh.upgrade(RefreshKind::Full);
422 }
423 }
424
425 Key::Char(ch) => {
426 let mut buf = [0; 4];
427
428 let line = &mut self.content[self.file_pos.line];
429 if self.file_pos.col < line.len() {
430 refresh.upgrade(RefreshKind::CurrentLine);
431 }
432
433 line.insert(self.file_pos.col, ch);
434 self.file_pos.col += 1;
435 self.insert_col = self.file_pos.col;
436
437 if cursor_pos.x < console_size.x - 1 && refresh == RefreshKind::None {
438 console.write(ch.encode_utf8(&mut buf))?;
439 }
440
441 self.dirty = true;
442 }
443
444 Key::End => {
445 self.file_pos.col = self.content[self.file_pos.line].len();
446 self.insert_col = self.file_pos.col;
447 }
448
449 Key::Home => {
450 let indent_pos = find_indent_end(&self.content[self.file_pos.line]);
451 if self.file_pos.col == indent_pos {
452 self.file_pos.col = 0;
453 } else {
454 self.file_pos.col = indent_pos;
455 }
456 self.insert_col = self.file_pos.col;
457 }
458
459 Key::NewLine | Key::CarriageReturn => {
460 let indent = copy_indent(&self.content[self.file_pos.line]);
461 let indent_len = indent.len();
462
463 let appending = (self.file_pos.line + 1 == self.content.len())
464 && (self.file_pos.col == self.content[self.file_pos.line].len());
465
466 let new = self.content[self.file_pos.line].split_off(self.file_pos.col);
467 self.content.insert(
468 self.file_pos.line + 1,
469 LineBuffer::from(indent + &new.into_inner()),
470 );
471 if !appending {
472 refresh.upgrade(RefreshKind::Full);
473 }
474
475 self.file_pos.col = indent_len;
476 self.file_pos.line += 1;
477 self.insert_col = self.file_pos.col;
478 self.dirty = true;
479 }
480
481 Key::PageDown => self.move_down(usize::from(console_size.y - 2)),
482
483 Key::PageUp => self.move_up(usize::from(console_size.y - 2)),
484
485 Key::Tab => {
486 let line = &mut self.content[self.file_pos.line];
487 if self.file_pos.col < line.len() {
488 refresh.upgrade(RefreshKind::CurrentLine);
489 }
490
491 let new_pos = (self.file_pos.col + INDENT_WIDTH) / INDENT_WIDTH * INDENT_WIDTH;
492 let mut new_text = String::with_capacity(new_pos - self.file_pos.col);
493 for _ in 0..new_text.capacity() {
494 new_text.push(' ');
495 }
496 line.insert_str(self.file_pos.col, &new_text);
497 self.file_pos.col = new_pos;
498 self.insert_col = self.file_pos.col;
499 if refresh == RefreshKind::None {
500 console.write(&new_text)?;
501 }
502 self.dirty = true;
503 }
504
505 Key::Unknown => (),
507 }
508 }
509
510 Ok(())
511 }
512}
513
514#[async_trait(?Send)]
515impl Program for Editor {
516 fn is_dirty(&self) -> bool {
517 self.dirty
518 }
519
520 async fn edit(&mut self, console: &mut dyn Console) -> io::Result<()> {
521 console.enter_alt()?;
522 let previous = console.set_sync(false)?;
523 let result = self.edit_interactively(console).await;
524 console.set_sync(previous)?;
525 console.leave_alt()?;
526 result
527 }
528
529 fn load(&mut self, name: Option<&str>, text: &str) {
530 self.name = name.map(str::to_owned);
531 self.content = text.lines().map(LineBuffer::from).collect();
532 self.dirty = false;
533 self.viewport_pos = FilePos::default();
534 self.file_pos = FilePos::default();
535 self.insert_col = 0;
536 }
537
538 fn name(&self) -> Option<&str> {
539 self.name.as_deref()
540 }
541
542 fn set_name(&mut self, name: &str) {
543 self.name = Some(name.to_owned());
544 self.dirty = false;
545 }
546
547 fn text(&self) -> String {
548 self.content
549 .iter()
550 .fold(String::new(), |contents, line| contents + &line.to_string() + "\n")
551 }
552}
553
554#[cfg(test)]
555mod tests {
556 use super::*;
557 use endbasic_std::testutils::*;
558 use futures_lite::future::block_on;
559
560 const TEST_FILENAME: &str = "X";
563
564 fn yx(y: u16, x: u16) -> CharsXY {
571 CharsXY::new(x, y)
572 }
573
574 fn linecol(line: usize, col: usize) -> FilePos {
576 FilePos { line, col }
577 }
578
579 #[must_use]
581 struct OutputBuilder {
582 console_size: CharsXY,
583 output: Vec<CapturedOut>,
584 dirty: bool,
585 }
586
587 impl OutputBuilder {
588 fn new(console_size: CharsXY) -> Self {
592 Self {
593 console_size,
594 output: vec![CapturedOut::EnterAlt, CapturedOut::SetSync(false)],
595 dirty: false,
596 }
597 }
598
599 fn refresh_status(mut self, file_pos: FilePos) -> Self {
606 let row = file_pos.line + 1;
607 let column = file_pos.col + 1;
608
609 self.output.push(CapturedOut::Locate(yx(self.console_size.y - 1, 0)));
610 self.output.push(CapturedOut::SetColor(STATUS_COLOR.0, STATUS_COLOR.1));
611 if self.console_size.x < 30 {
612 let details = &format!(" {}:{} ", row, column);
614 let mut status = String::new();
615 while status.len() + details.len() < usize::from(self.console_size.x) {
616 status.push(' ');
617 }
618 status += details;
619 status.truncate(usize::from(self.console_size.x));
620 self.output.push(CapturedOut::Write(status));
621 } else {
622 let dirty_marker = if self.dirty { "*" } else { "" };
623 let details =
624 &format!("| {}{} | Ln {}, Col {} ", TEST_FILENAME, dirty_marker, row, column);
625 let mut status = String::from(KEYS_SUMMARY);
626 while status.len() + details.len() < usize::from(self.console_size.x) {
627 status.push(' ');
628 }
629 status += details;
630 self.output.push(CapturedOut::Write(status));
631 }
632 self
633 }
634
635 fn quick_refresh(mut self, file_pos: FilePos, cursor: CharsXY) -> Self {
638 self.output.push(CapturedOut::HideCursor);
639 self = self.refresh_status(file_pos);
640 self.output.push(CapturedOut::SetColor(TEXT_COLOR.0, TEXT_COLOR.1));
641 self.output.push(CapturedOut::Locate(cursor));
642 self.output.push(CapturedOut::ShowCursor);
643 self.output.push(CapturedOut::SyncNow);
644 self
645 }
646
647 fn refresh_line(mut self, file_pos: FilePos, line: &str, cursor: CharsXY) -> Self {
649 self.output.push(CapturedOut::HideCursor);
650 self = self.refresh_status(file_pos);
651 self.output.push(CapturedOut::SetColor(TEXT_COLOR.0, TEXT_COLOR.1));
652 self.output.push(CapturedOut::Locate(yx(cursor.y, 0)));
653 self.output.push(CapturedOut::Clear(ClearType::CurrentLine));
654 if !line.is_empty() {
655 self.output.push(CapturedOut::Write(line.to_string()));
656 }
657 self.output.push(CapturedOut::Locate(cursor));
658 self.output.push(CapturedOut::ShowCursor);
659 self.output.push(CapturedOut::SyncNow);
660 self
661 }
662
663 fn refresh(mut self, file_pos: FilePos, previous: &[&str], cursor: CharsXY) -> Self {
667 self.output.push(CapturedOut::HideCursor);
668 self.output.push(CapturedOut::SetColor(TEXT_COLOR.0, TEXT_COLOR.1));
669 self.output.push(CapturedOut::Clear(ClearType::All));
670 self = self.refresh_status(file_pos);
671 self.output.push(CapturedOut::SetColor(TEXT_COLOR.0, TEXT_COLOR.1));
672 self.output.push(CapturedOut::Locate(yx(0, 0)));
673 for line in previous {
674 self.output.push(CapturedOut::Print(line.to_string()));
675 }
676 self.output.push(CapturedOut::Locate(cursor));
677 self.output.push(CapturedOut::ShowCursor);
678 self.output.push(CapturedOut::SyncNow);
679 self
680 }
681
682 fn add(mut self, co: CapturedOut) -> Self {
684 self.output.push(co);
685 self
686 }
687
688 fn set_dirty(mut self) -> Self {
690 self.dirty = true;
691 self
692 }
693
694 fn build(self) -> Vec<CapturedOut> {
696 let mut output = self.output;
697 output.push(CapturedOut::SetSync(true));
698 output.push(CapturedOut::LeaveAlt);
699 output
700 }
701 }
702
703 fn run_editor(previous: &str, exp_text: &str, mut console: MockConsole, ob: OutputBuilder) {
710 let mut editor = Editor::default();
711 editor.load(Some(TEST_FILENAME), previous);
712
713 console.add_input_keys(&[Key::Escape]);
714 block_on(editor.edit(&mut console)).unwrap();
715 assert_eq!(exp_text, editor.text());
716 assert_eq!(ob.dirty, editor.is_dirty());
717 assert_eq!(ob.build(), console.captured_out());
718 }
719
720 #[test]
721 fn test_program_behavior() {
722 let mut editor = Editor::default();
723 assert!(editor.text().is_empty());
724 assert!(!editor.is_dirty());
725
726 let mut console = MockConsole::default();
727 console.set_size_chars(yx(10, 40));
728 console.add_input_keys(&[Key::Escape]);
729 block_on(editor.edit(&mut console)).unwrap();
730 assert!(!editor.is_dirty());
731
732 console.add_input_keys(&[Key::Char('x'), Key::Escape]);
733 block_on(editor.edit(&mut console)).unwrap();
734 assert!(editor.is_dirty());
735
736 editor.load(Some(TEST_FILENAME), "some text\n and more\n");
737 assert_eq!("some text\n and more\n", editor.text());
738 assert!(!editor.is_dirty());
739
740 editor.load(Some(TEST_FILENAME), "different\n");
741 assert_eq!("different\n", editor.text());
742 assert!(!editor.is_dirty());
743
744 console.add_input_keys(&[Key::Char('x'), Key::Escape]);
745 block_on(editor.edit(&mut console)).unwrap();
746 assert!(editor.is_dirty());
747
748 editor.set_name("SAVED");
749 assert!(!editor.is_dirty());
750 }
751
752 #[test]
753 fn test_force_trailing_newline() {
754 let mut editor = Editor::default();
755 assert!(editor.text().is_empty());
756
757 editor.load(Some(TEST_FILENAME), "missing\nnewline at eof");
758 assert_eq!("missing\nnewline at eof\n", editor.text());
759 }
760
761 #[test]
762 fn test_editing_with_previous_content_starts_on_top_left() {
763 let mut cb = MockConsole::default();
764 cb.set_size_chars(yx(10, 40));
765 let mut ob = OutputBuilder::new(yx(10, 40));
766 ob = ob.refresh(linecol(0, 0), &["previous content"], yx(0, 0));
767
768 run_editor("previous content", "previous content\n", cb, ob);
769 }
770
771 #[test]
772 fn test_insert_in_empty_file() {
773 let mut cb = MockConsole::default();
774 cb.set_size_chars(yx(10, 40));
775 let mut ob = OutputBuilder::new(yx(10, 40));
776 ob = ob.refresh(linecol(0, 0), &[""], yx(0, 0));
777
778 cb.add_input_chars("abcéà ");
779 ob = ob.set_dirty();
780 ob = ob.add(CapturedOut::Write("a".to_string()));
781 ob = ob.quick_refresh(linecol(0, 1), yx(0, 1));
782 ob = ob.add(CapturedOut::Write("b".to_string()));
783 ob = ob.quick_refresh(linecol(0, 2), yx(0, 2));
784 ob = ob.add(CapturedOut::Write("c".to_string()));
785 ob = ob.quick_refresh(linecol(0, 3), yx(0, 3));
786 ob = ob.add(CapturedOut::Write("é".to_string()));
787 ob = ob.quick_refresh(linecol(0, 4), yx(0, 4));
788 ob = ob.add(CapturedOut::Write("Ã ".to_string()));
789 ob = ob.quick_refresh(linecol(0, 5), yx(0, 5));
790
791 cb.add_input_keys(&[Key::NewLine]);
792 ob = ob.quick_refresh(linecol(1, 0), yx(1, 0));
793
794 cb.add_input_keys(&[Key::CarriageReturn]);
795 ob = ob.quick_refresh(linecol(2, 0), yx(2, 0));
796
797 cb.add_input_chars("2");
798 ob = ob.add(CapturedOut::Write("2".to_string()));
799 ob = ob.quick_refresh(linecol(2, 1), yx(2, 1));
800
801 run_editor("", "abcéà \n\n2\n", cb, ob);
802 }
803
804 #[test]
805 fn test_insert_before_previous_content() {
806 let mut cb = MockConsole::default();
807 cb.set_size_chars(yx(10, 40));
808 let mut ob = OutputBuilder::new(yx(10, 40));
809 ob = ob.refresh(linecol(0, 0), &["previous content"], yx(0, 0));
810
811 cb.add_input_chars("a");
812 ob = ob.set_dirty();
813 ob = ob.refresh_line(linecol(0, 1), "aprevious content", yx(0, 1));
814
815 cb.add_input_chars("b");
816 ob = ob.refresh_line(linecol(0, 2), "abprevious content", yx(0, 2));
817
818 cb.add_input_chars("c");
819 ob = ob.refresh_line(linecol(0, 3), "abcprevious content", yx(0, 3));
820
821 cb.add_input_chars(" ");
822 ob = ob.refresh_line(linecol(0, 4), "abc previous content", yx(0, 4));
823
824 run_editor("previous content", "abc previous content\n", cb, ob);
825 }
826
827 #[test]
828 fn test_insert_before_last_character() {
829 let mut cb = MockConsole::default();
830 cb.set_size_chars(yx(10, 40));
831 let mut ob = OutputBuilder::new(yx(10, 40));
832 ob = ob.refresh(linecol(0, 0), &[""], yx(0, 0));
833
834 cb.add_input_chars("abc");
835 ob = ob.set_dirty();
836 ob = ob.add(CapturedOut::Write("a".to_string()));
837 ob = ob.quick_refresh(linecol(0, 1), yx(0, 1));
838 ob = ob.add(CapturedOut::Write("b".to_string()));
839 ob = ob.quick_refresh(linecol(0, 2), yx(0, 2));
840 ob = ob.add(CapturedOut::Write("c".to_string()));
841 ob = ob.quick_refresh(linecol(0, 3), yx(0, 3));
842
843 cb.add_input_keys(&[Key::ArrowLeft]);
844 ob = ob.quick_refresh(linecol(0, 2), yx(0, 2));
845
846 cb.add_input_chars("d");
847 ob = ob.refresh_line(linecol(0, 3), "abdc", yx(0, 3));
848
849 run_editor("", "abdc\n", cb, ob);
850 }
851
852 #[test]
853 fn test_insert_newline_in_middle() {
854 let mut cb = MockConsole::default();
855 cb.set_size_chars(yx(10, 40));
856 let mut ob = OutputBuilder::new(yx(10, 40));
857 ob = ob.refresh(linecol(0, 0), &[""], yx(0, 0));
858
859 cb.add_input_chars("abc");
860 ob = ob.set_dirty();
861 ob = ob.add(CapturedOut::Write("a".to_string()));
862 ob = ob.quick_refresh(linecol(0, 1), yx(0, 1));
863 ob = ob.add(CapturedOut::Write("b".to_string()));
864 ob = ob.quick_refresh(linecol(0, 2), yx(0, 2));
865 ob = ob.add(CapturedOut::Write("c".to_string()));
866 ob = ob.quick_refresh(linecol(0, 3), yx(0, 3));
867
868 cb.add_input_keys(&[Key::ArrowLeft]);
869 ob = ob.quick_refresh(linecol(0, 2), yx(0, 2));
870
871 cb.add_input_keys(&[Key::NewLine]);
872 ob = ob.refresh(linecol(1, 0), &["ab", "c"], yx(1, 0));
873
874 cb.add_input_keys(&[Key::ArrowUp]);
875 ob = ob.quick_refresh(linecol(0, 0), yx(0, 0));
876 cb.add_input_keys(&[Key::ArrowRight]);
877 ob = ob.quick_refresh(linecol(0, 1), yx(0, 1));
878 cb.add_input_keys(&[Key::ArrowRight]);
879 ob = ob.quick_refresh(linecol(0, 2), yx(0, 2));
880
881 cb.add_input_keys(&[Key::NewLine]);
882 ob = ob.refresh(linecol(1, 0), &["ab", "", "c"], yx(1, 0));
883
884 run_editor("", "ab\n\nc\n", cb, ob);
885 }
886
887 #[test]
888 fn test_split_last_line() {
889 let mut cb = MockConsole::default();
890 cb.set_size_chars(yx(10, 40));
891 let mut ob = OutputBuilder::new(yx(10, 40));
892 ob = ob.refresh(linecol(0, 0), &[""], yx(0, 0));
893
894 cb.add_input_chars(" abcd");
895 ob = ob.set_dirty();
896 ob = ob.add(CapturedOut::Write(" ".to_string()));
897 ob = ob.quick_refresh(linecol(0, 1), yx(0, 1));
898 ob = ob.add(CapturedOut::Write(" ".to_string()));
899 ob = ob.quick_refresh(linecol(0, 2), yx(0, 2));
900 ob = ob.add(CapturedOut::Write("a".to_string()));
901 ob = ob.quick_refresh(linecol(0, 3), yx(0, 3));
902 ob = ob.add(CapturedOut::Write("b".to_string()));
903 ob = ob.quick_refresh(linecol(0, 4), yx(0, 4));
904 ob = ob.add(CapturedOut::Write("c".to_string()));
905 ob = ob.quick_refresh(linecol(0, 5), yx(0, 5));
906 ob = ob.add(CapturedOut::Write("d".to_string()));
907 ob = ob.quick_refresh(linecol(0, 6), yx(0, 6));
908
909 cb.add_input_keys(&[Key::ArrowLeft]);
910 ob = ob.quick_refresh(linecol(0, 5), yx(0, 5));
911 cb.add_input_keys(&[Key::ArrowLeft]);
912 ob = ob.quick_refresh(linecol(0, 4), yx(0, 4));
913
914 cb.add_input_keys(&[Key::NewLine]);
915 ob = ob.refresh(linecol(1, 2), &[" ab", " cd"], yx(1, 2));
916
917 run_editor("", " ab\n cd\n", cb, ob);
918 }
919
920 #[test]
921 fn test_move_in_empty_file() {
922 let mut cb = MockConsole::default();
923 cb.set_size_chars(yx(10, 40));
924 let mut ob = OutputBuilder::new(yx(10, 40));
925 ob = ob.refresh(linecol(0, 0), &[""], yx(0, 0));
926
927 for k in &[
928 Key::ArrowUp,
929 Key::ArrowDown,
930 Key::ArrowLeft,
931 Key::ArrowRight,
932 Key::PageUp,
933 Key::PageDown,
934 ] {
935 cb.add_input_keys(&[*k]);
936 ob = ob.quick_refresh(linecol(0, 0), yx(0, 0));
937 }
938
939 run_editor("", "\n", cb, ob);
940 }
941
942 #[test]
943 fn test_move_end() {
944 let mut cb = MockConsole::default();
945 cb.set_size_chars(yx(10, 40));
946 let mut ob = OutputBuilder::new(yx(10, 40));
947 ob = ob.refresh(linecol(0, 0), &["text"], yx(0, 0));
948
949 cb.add_input_keys(&[Key::End]);
950 ob = ob.quick_refresh(linecol(0, 4), yx(0, 4));
951
952 cb.add_input_chars(".");
953 ob = ob.set_dirty();
954 ob = ob.add(CapturedOut::Write(".".to_string()));
955 ob = ob.quick_refresh(linecol(0, 5), yx(0, 5));
956
957 run_editor("text", "text.\n", cb, ob);
958 }
959
960 #[test]
961 fn test_move_home_no_indent() {
962 let mut cb = MockConsole::default();
963 cb.set_size_chars(yx(10, 40));
964 let mut ob = OutputBuilder::new(yx(10, 40));
965 ob = ob.refresh(linecol(0, 0), &["text"], yx(0, 0));
966
967 cb.add_input_keys(&[Key::ArrowRight]);
968 ob = ob.quick_refresh(linecol(0, 1), yx(0, 1));
969
970 cb.add_input_keys(&[Key::ArrowRight]);
971 ob = ob.quick_refresh(linecol(0, 2), yx(0, 2));
972
973 cb.add_input_keys(&[Key::Home]);
974 ob = ob.quick_refresh(linecol(0, 0), yx(0, 0));
975
976 cb.add_input_chars(".");
977 ob = ob.set_dirty();
978 ob = ob.refresh_line(linecol(0, 1), ".text", yx(0, 1));
979
980 cb.add_input_keys(&[Key::Home]);
981 ob = ob.quick_refresh(linecol(0, 0), yx(0, 0));
982
983 cb.add_input_chars(",");
984 ob = ob.refresh_line(linecol(0, 1), ",.text", yx(0, 1));
985
986 run_editor("text", ",.text\n", cb, ob);
987 }
988
989 #[test]
990 fn test_move_home_with_indent() {
991 let mut cb = MockConsole::default();
992 cb.set_size_chars(yx(10, 40));
993 let mut ob = OutputBuilder::new(yx(10, 40));
994 ob = ob.refresh(linecol(0, 0), &[" text"], yx(0, 0));
995
996 cb.add_input_keys(&[Key::Home]);
997 ob = ob.quick_refresh(linecol(0, 2), yx(0, 2));
998
999 cb.add_input_keys(&[Key::Home]);
1000 ob = ob.quick_refresh(linecol(0, 0), yx(0, 0));
1001
1002 cb.add_input_keys(&[Key::ArrowRight]);
1003 ob = ob.quick_refresh(linecol(0, 1), yx(0, 1));
1004
1005 cb.add_input_keys(&[Key::Home]);
1006 ob = ob.quick_refresh(linecol(0, 2), yx(0, 2));
1007
1008 cb.add_input_keys(&[Key::ArrowRight]);
1009 ob = ob.quick_refresh(linecol(0, 3), yx(0, 3));
1010
1011 cb.add_input_keys(&[Key::Home]);
1012 ob = ob.quick_refresh(linecol(0, 2), yx(0, 2));
1013
1014 cb.add_input_chars(".");
1015 ob = ob.set_dirty();
1016 ob = ob.refresh_line(linecol(0, 3), " .text", yx(0, 3));
1017
1018 run_editor(" text", " .text\n", cb, ob);
1019 }
1020
1021 #[test]
1022 fn test_move_page_down_up() {
1023 let mut cb = MockConsole::default();
1024 cb.set_size_chars(yx(10, 40));
1025 let mut ob = OutputBuilder::new(yx(10, 40));
1026 ob = ob.refresh(linecol(0, 0), &["1", "2", "3", "4", "5", "6", "7", "8", "9"], yx(0, 0));
1027
1028 cb.add_input_keys(&[Key::PageDown]);
1029 ob = ob.quick_refresh(linecol(8, 0), yx(8, 0));
1030
1031 cb.add_input_keys(&[Key::PageDown]);
1032 ob = ob.refresh(
1033 linecol(16, 0),
1034 &["9", "10", "11", "12", "13", "14", "15", "16", "17"],
1035 yx(8, 0),
1036 );
1037
1038 cb.add_input_keys(&[Key::PageDown]);
1039 ob = ob.refresh(
1040 linecol(19, 0),
1041 &["12", "13", "14", "15", "16", "17", "18", "19", "20"],
1042 yx(8, 0),
1043 );
1044
1045 cb.add_input_keys(&[Key::PageDown]);
1046 ob = ob.quick_refresh(linecol(19, 0), yx(8, 0));
1047
1048 cb.add_input_keys(&[Key::PageUp]);
1049 ob = ob.quick_refresh(linecol(11, 0), yx(0, 0));
1050
1051 cb.add_input_keys(&[Key::PageUp]);
1052 ob = ob.refresh(linecol(3, 0), &["4", "5", "6", "7", "8", "9", "10", "11", "12"], yx(0, 0));
1053
1054 cb.add_input_keys(&[Key::PageUp]);
1055 ob = ob.refresh(linecol(0, 0), &["1", "2", "3", "4", "5", "6", "7", "8", "9"], yx(0, 0));
1056
1057 cb.add_input_keys(&[Key::PageUp]);
1058 ob = ob.quick_refresh(linecol(0, 0), yx(0, 0));
1059
1060 run_editor(
1061 "1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n",
1062 "1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n",
1063 cb,
1064 ob,
1065 );
1066 }
1067
1068 #[test]
1069 fn test_tab_append() {
1070 let mut cb = MockConsole::default();
1071 cb.set_size_chars(yx(10, 40));
1072 let mut ob = OutputBuilder::new(yx(10, 40));
1073 ob = ob.refresh(linecol(0, 0), &[""], yx(0, 0));
1074
1075 cb.add_input_keys(&[Key::Tab]);
1076 ob = ob.set_dirty();
1077 ob = ob.add(CapturedOut::Write(" ".to_string()));
1078 ob = ob.quick_refresh(linecol(0, 4), yx(0, 4));
1079
1080 cb.add_input_chars("x");
1081 ob = ob.add(CapturedOut::Write("x".to_string()));
1082 ob = ob.quick_refresh(linecol(0, 5), yx(0, 5));
1083
1084 cb.add_input_keys(&[Key::Tab]);
1085 ob = ob.add(CapturedOut::Write(" ".to_string()));
1086 ob = ob.quick_refresh(linecol(0, 8), yx(0, 8));
1087
1088 run_editor("", " x \n", cb, ob);
1089 }
1090
1091 #[test]
1092 fn test_tab_existing_content() {
1093 let mut cb = MockConsole::default();
1094 cb.set_size_chars(yx(10, 40));
1095 let mut ob = OutputBuilder::new(yx(10, 40));
1096 ob = ob.refresh(linecol(0, 0), &["."], yx(0, 0));
1097
1098 cb.add_input_keys(&[Key::Tab]);
1099 ob = ob.set_dirty();
1100 ob = ob.refresh_line(linecol(0, 4), " .", yx(0, 4));
1101
1102 cb.add_input_keys(&[Key::Tab]);
1103 ob = ob.refresh_line(linecol(0, 8), " .", yx(0, 8));
1104
1105 run_editor(".", " .\n", cb, ob);
1106 }
1107
1108 #[test]
1109 fn test_tab_remove_empty_line() {
1110 let mut cb = MockConsole::default();
1111 cb.set_size_chars(yx(10, 40));
1112 let mut ob = OutputBuilder::new(yx(10, 40));
1113 ob = ob.refresh(linecol(0, 0), &[" "], yx(0, 0));
1114
1115 cb.add_input_keys(&[Key::End]);
1116 ob = ob.quick_refresh(linecol(0, 10), yx(0, 10));
1117
1118 cb.add_input_keys(&[Key::Backspace]);
1119 ob = ob.set_dirty();
1120 ob = ob.add(CapturedOut::HideCursor);
1121 ob = ob.add(CapturedOut::Clear(ClearType::PreviousChar));
1122 ob = ob.add(CapturedOut::Clear(ClearType::PreviousChar));
1123 ob = ob.add(CapturedOut::ShowCursor);
1124 ob = ob.quick_refresh(linecol(0, 8), yx(0, 8));
1125
1126 cb.add_input_keys(&[Key::Backspace]);
1127 ob = ob.add(CapturedOut::HideCursor);
1128 ob = ob.add(CapturedOut::Clear(ClearType::PreviousChar));
1129 ob = ob.add(CapturedOut::Clear(ClearType::PreviousChar));
1130 ob = ob.add(CapturedOut::Clear(ClearType::PreviousChar));
1131 ob = ob.add(CapturedOut::Clear(ClearType::PreviousChar));
1132 ob = ob.add(CapturedOut::ShowCursor);
1133 ob = ob.quick_refresh(linecol(0, 4), yx(0, 4));
1134
1135 cb.add_input_keys(&[Key::Backspace]);
1136 ob = ob.add(CapturedOut::HideCursor);
1137 ob = ob.add(CapturedOut::Clear(ClearType::PreviousChar));
1138 ob = ob.add(CapturedOut::Clear(ClearType::PreviousChar));
1139 ob = ob.add(CapturedOut::Clear(ClearType::PreviousChar));
1140 ob = ob.add(CapturedOut::Clear(ClearType::PreviousChar));
1141 ob = ob.add(CapturedOut::ShowCursor);
1142 ob = ob.quick_refresh(linecol(0, 0), yx(0, 0));
1143
1144 cb.add_input_keys(&[Key::Backspace]);
1145 ob = ob.quick_refresh(linecol(0, 0), yx(0, 0));
1146
1147 run_editor(" ", "\n", cb, ob);
1148 }
1149
1150 #[test]
1151 fn test_tab_remove_before_some_text() {
1152 let mut cb = MockConsole::default();
1153 cb.set_size_chars(yx(10, 40));
1154 let mut ob = OutputBuilder::new(yx(10, 40));
1155 ob = ob.refresh(linecol(0, 0), &[" aligned"], yx(0, 0));
1156
1157 for i in 0..10 {
1158 cb.add_input_keys(&[Key::ArrowRight]);
1159 ob = ob.quick_refresh(linecol(0, i + 1), yx(0, u16::try_from(i + 1).unwrap()));
1160 }
1161
1162 cb.add_input_keys(&[Key::Backspace]);
1163 ob = ob.set_dirty();
1164 ob = ob.refresh_line(linecol(0, 8), " aligned", yx(0, 8));
1165
1166 cb.add_input_keys(&[Key::Backspace]);
1167 ob = ob.refresh_line(linecol(0, 4), " aligned", yx(0, 4));
1168
1169 cb.add_input_keys(&[Key::Backspace]);
1170 ob = ob.refresh_line(linecol(0, 0), "aligned", yx(0, 0));
1171
1172 cb.add_input_keys(&[Key::Backspace]);
1173 ob = ob.quick_refresh(linecol(0, 0), yx(0, 0));
1174
1175 run_editor(" aligned", "aligned\n", cb, ob);
1176 }
1177
1178 fn do_delete_in_middle_of_line_test(key: Key) {
1179 let mut cb = MockConsole::default();
1180 cb.set_size_chars(yx(10, 40));
1181 let mut ob = OutputBuilder::new(yx(10, 40));
1182 ob = ob.refresh(linecol(0, 0), &["has a typo"], yx(0, 0));
1183
1184 for i in 0..7u16 {
1185 cb.add_input_keys(&[Key::ArrowRight]);
1186 ob = ob.quick_refresh(linecol(0, usize::from(i + 1)), yx(0, i + 1));
1187 }
1188
1189 cb.add_input_keys(&[key]);
1190 ob = ob.set_dirty();
1191 ob = ob.refresh(linecol(0, 7), &["has a tpo"], yx(0, 7));
1192
1193 run_editor("has a typo\n", "has a tpo\n", cb, ob);
1194 }
1195
1196 #[test]
1197 fn test_delete_in_middle_of_line() {
1198 do_delete_in_middle_of_line_test(Key::Delete)
1199 }
1200
1201 #[test]
1202 fn test_ctrl_d_in_middle_of_line() {
1203 do_delete_in_middle_of_line_test(Key::EofOrDelete)
1204 }
1205
1206 #[test]
1207 fn test_delete_last_character_of_line() {
1208 let mut cb = MockConsole::default();
1209 cb.set_size_chars(yx(10, 40));
1210 let mut ob = OutputBuilder::new(yx(10, 40));
1211 ob = ob.refresh(linecol(0, 0), &["sample"], yx(0, 0));
1212
1213 for i in 0..5u16 {
1214 cb.add_input_keys(&[Key::ArrowRight]);
1215 ob = ob.quick_refresh(linecol(0, usize::from(i + 1)), yx(0, i + 1));
1216 }
1217
1218 cb.add_input_keys(&[Key::Delete]);
1219 ob = ob.set_dirty();
1220 ob = ob.add(CapturedOut::HideCursor);
1221 ob = ob.add(CapturedOut::Clear(ClearType::UntilNewLine));
1222 ob = ob.add(CapturedOut::ShowCursor);
1223 ob = ob.quick_refresh(linecol(0, 5), yx(0, 5));
1224
1225 run_editor("sample\n", "sampl\n", cb, ob);
1226 }
1227
1228 fn do_delete_joins_with_next_line_test(key: Key) {
1229 let mut cb = MockConsole::default();
1230 cb.set_size_chars(yx(10, 40));
1231 let mut ob = OutputBuilder::new(yx(10, 40));
1232 ob = ob.refresh(linecol(0, 0), &["first", "second"], yx(0, 0));
1233
1234 for i in 0..5u16 {
1235 cb.add_input_keys(&[Key::ArrowRight]);
1236 ob = ob.quick_refresh(linecol(0, usize::from(i + 1)), yx(0, i + 1));
1237 }
1238
1239 cb.add_input_keys(&[key]);
1240 ob = ob.set_dirty();
1241 ob = ob.refresh(linecol(0, 5), &["firstsecond"], yx(0, 5));
1242
1243 run_editor("first\nsecond\n", "firstsecond\n", cb, ob);
1244 }
1245
1246 #[test]
1247 fn test_delete_joins_with_next_line() {
1248 do_delete_joins_with_next_line_test(Key::Delete)
1249 }
1250
1251 #[test]
1252 fn test_ctrl_d_joins_with_next_line() {
1253 do_delete_joins_with_next_line_test(Key::EofOrDelete)
1254 }
1255
1256 #[test]
1257 fn test_delete_at_end_of_file_is_ignored() {
1258 let mut cb = MockConsole::default();
1259 cb.set_size_chars(yx(10, 40));
1260 let mut ob = OutputBuilder::new(yx(10, 40));
1261 ob = ob.refresh(linecol(0, 0), &["sample"], yx(0, 0));
1262
1263 for i in 0..6u16 {
1264 cb.add_input_keys(&[Key::ArrowRight]);
1265 ob = ob.quick_refresh(linecol(0, usize::from(i + 1)), yx(0, i + 1));
1266 }
1267
1268 cb.add_input_keys(&[Key::Delete]);
1269 ob = ob.quick_refresh(linecol(0, 6), yx(0, 6));
1270
1271 run_editor("sample\n", "sample\n", cb, ob);
1272 }
1273
1274 #[test]
1275 fn test_move_preserves_insertion_column() {
1276 let mut cb = MockConsole::default();
1277 cb.set_size_chars(yx(10, 40));
1278 let mut ob = OutputBuilder::new(yx(10, 40));
1279 ob = ob.refresh(linecol(0, 0), &["longer", "a", "longer", "b"], yx(0, 0));
1280
1281 cb.add_input_keys(&[Key::ArrowRight]);
1282 ob = ob.quick_refresh(linecol(0, 1), yx(0, 1));
1283
1284 cb.add_input_keys(&[Key::ArrowRight]);
1285 ob = ob.quick_refresh(linecol(0, 2), yx(0, 2));
1286
1287 cb.add_input_keys(&[Key::ArrowRight]);
1288 ob = ob.quick_refresh(linecol(0, 3), yx(0, 3));
1289
1290 cb.add_input_keys(&[Key::ArrowRight]);
1291 ob = ob.quick_refresh(linecol(0, 4), yx(0, 4));
1292
1293 cb.add_input_keys(&[Key::ArrowDown]);
1294 ob = ob.quick_refresh(linecol(1, 1), yx(1, 1));
1295
1296 cb.add_input_keys(&[Key::ArrowDown]);
1297 ob = ob.quick_refresh(linecol(2, 4), yx(2, 4));
1298
1299 cb.add_input_keys(&[Key::Char('X')]);
1300 ob = ob.set_dirty();
1301 ob = ob.refresh_line(linecol(2, 5), "longXer", yx(2, 5));
1302
1303 cb.add_input_keys(&[Key::ArrowDown]);
1304 ob = ob.quick_refresh(linecol(3, 1), yx(3, 1));
1305
1306 cb.add_input_keys(&[Key::Char('Z')]);
1307 ob = ob.add(CapturedOut::Write("Z".to_string()));
1308 ob = ob.quick_refresh(linecol(3, 2), yx(3, 2));
1309
1310 run_editor("longer\na\nlonger\nb\n", "longer\na\nlongXer\nbZ\n", cb, ob);
1311 }
1312
1313 #[test]
1314 fn test_move_down_preserves_insertion_column_with_horizontal_scrolling() {
1315 let mut cb = MockConsole::default();
1316 cb.set_size_chars(yx(10, 40));
1317 let mut ob = OutputBuilder::new(yx(10, 40));
1318 ob = ob.refresh(
1319 linecol(0, 0),
1320 &[
1321 "this is a line of text with more than 40",
1322 "short",
1323 "a",
1324 "",
1325 "another line of text with more than 40 c",
1326 ],
1327 yx(0, 0),
1328 );
1329
1330 for col in 0u16..39u16 {
1332 cb.add_input_keys(&[Key::ArrowRight]);
1333 ob = ob.quick_refresh(linecol(0, usize::from(col) + 1), yx(0, col + 1));
1334 }
1335
1336 cb.add_input_keys(&[Key::ArrowRight]);
1338 ob = ob.refresh(
1339 linecol(0, 40),
1340 &[
1341 "his is a line of text with more than 40 ",
1342 "hort",
1343 "",
1344 "",
1345 "nother line of text with more than 40 ch",
1346 ],
1347 yx(0, 39),
1348 );
1349 cb.add_input_keys(&[Key::ArrowRight]);
1350 ob = ob.refresh(
1351 linecol(0, 41),
1352 &[
1353 "is is a line of text with more than 40 c",
1354 "ort",
1355 "",
1356 "",
1357 "other line of text with more than 40 cha",
1358 ],
1359 yx(0, 39),
1360 );
1361
1362 cb.add_input_keys(&[Key::ArrowDown]);
1364 ob = ob.quick_refresh(linecol(1, 5), yx(1, 3));
1365
1366 cb.add_input_keys(&[Key::ArrowDown]);
1369 ob = ob.refresh(
1370 linecol(2, 1),
1371 &[
1372 "his is a line of text with more than 40 ",
1373 "hort",
1374 "",
1375 "",
1376 "nother line of text with more than 40 ch",
1377 ],
1378 yx(2, 0),
1379 );
1380
1381 cb.add_input_keys(&[Key::ArrowDown]);
1383 ob = ob.refresh(
1384 linecol(3, 0),
1385 &[
1386 "this is a line of text with more than 40",
1387 "short",
1388 "a",
1389 "",
1390 "another line of text with more than 40 c",
1391 ],
1392 yx(3, 0),
1393 );
1394
1395 cb.add_input_keys(&[Key::ArrowDown]);
1398 ob = ob.refresh(
1399 linecol(4, 41),
1400 &[
1401 "is is a line of text with more than 40 c",
1402 "ort",
1403 "",
1404 "",
1405 "other line of text with more than 40 cha",
1406 ],
1407 yx(4, 39),
1408 );
1409
1410 run_editor(
1411 "this is a line of text with more than 40 characters\nshort\na\n\nanother line of text with more than 40 characters\n",
1412 "this is a line of text with more than 40 characters\nshort\na\n\nanother line of text with more than 40 characters\n",
1413 cb,
1414 ob,
1415 );
1416 }
1417
1418 #[test]
1419 fn test_move_up_preserves_insertion_column_with_horizontal_scrolling() {
1420 let mut cb = MockConsole::default();
1421 cb.set_size_chars(yx(10, 40));
1422 let mut ob = OutputBuilder::new(yx(10, 40));
1423 ob = ob.refresh(
1424 linecol(0, 0),
1425 &[
1426 "this is a line of text with more than 40",
1427 "",
1428 "a",
1429 "short",
1430 "another line of text with more than 40 c",
1431 ],
1432 yx(0, 0),
1433 );
1434
1435 for i in 0u16..4u16 {
1437 cb.add_input_keys(&[Key::ArrowDown]);
1438 ob = ob.quick_refresh(linecol(usize::from(i + 1), 0), yx(i + 1, 0));
1439 }
1440
1441 for col in 0u16..39u16 {
1443 cb.add_input_keys(&[Key::ArrowRight]);
1444 ob = ob.quick_refresh(linecol(4, usize::from(col + 1)), yx(4, col + 1));
1445 }
1446
1447 cb.add_input_keys(&[Key::ArrowRight]);
1449 ob = ob.refresh(
1450 linecol(4, 40),
1451 &[
1452 "his is a line of text with more than 40 ",
1453 "",
1454 "",
1455 "hort",
1456 "nother line of text with more than 40 ch",
1457 ],
1458 yx(4, 39),
1459 );
1460 cb.add_input_keys(&[Key::ArrowRight]);
1461 ob = ob.refresh(
1462 linecol(4, 41),
1463 &[
1464 "is is a line of text with more than 40 c",
1465 "",
1466 "",
1467 "ort",
1468 "other line of text with more than 40 cha",
1469 ],
1470 yx(4, 39),
1471 );
1472
1473 cb.add_input_keys(&[Key::ArrowUp]);
1475 ob = ob.quick_refresh(linecol(3, 5), yx(3, 3));
1476
1477 cb.add_input_keys(&[Key::ArrowUp]);
1480 ob = ob.refresh(
1481 linecol(2, 1),
1482 &[
1483 "his is a line of text with more than 40 ",
1484 "",
1485 "",
1486 "hort",
1487 "nother line of text with more than 40 ch",
1488 ],
1489 yx(2, 0),
1490 );
1491
1492 cb.add_input_keys(&[Key::ArrowUp]);
1494 ob = ob.refresh(
1495 linecol(1, 0),
1496 &[
1497 "this is a line of text with more than 40",
1498 "",
1499 "a",
1500 "short",
1501 "another line of text with more than 40 c",
1502 ],
1503 yx(1, 0),
1504 );
1505
1506 cb.add_input_keys(&[Key::ArrowUp]);
1509 ob = ob.refresh(
1510 linecol(0, 41),
1511 &[
1512 "is is a line of text with more than 40 c",
1513 "",
1514 "",
1515 "ort",
1516 "other line of text with more than 40 cha",
1517 ],
1518 yx(0, 39),
1519 );
1520
1521 run_editor(
1522 "this is a line of text with more than 40 characters\n\na\nshort\nanother line of text with more than 40 characters\n",
1523 "this is a line of text with more than 40 characters\n\na\nshort\nanother line of text with more than 40 characters\n",
1524 cb,
1525 ob,
1526 );
1527 }
1528
1529 #[test]
1530 fn test_horizontal_scrolling() {
1531 let mut cb = MockConsole::default();
1532 cb.set_size_chars(yx(10, 40));
1533 let mut ob = OutputBuilder::new(yx(10, 40));
1534 ob = ob.refresh(linecol(0, 0), &["ab", "", "xyz"], yx(0, 0));
1535
1536 cb.add_input_keys(&[Key::ArrowDown]);
1537 ob = ob.quick_refresh(linecol(1, 0), yx(1, 0));
1538
1539 for (col, ch) in "123456789012345678901234567890123456789".chars().enumerate() {
1541 cb.add_input_keys(&[Key::Char(ch)]);
1542 ob = ob.set_dirty();
1543 let mut buf = [0u8; 4];
1544 ob = ob.add(CapturedOut::Write(ch.encode_utf8(&mut buf).to_string()));
1545 ob = ob.quick_refresh(linecol(1, col + 1), yx(1, u16::try_from(col + 1).unwrap()));
1546 }
1547
1548 cb.add_input_keys(&[Key::Char('A')]);
1551 ob = ob.refresh(
1552 linecol(1, 40),
1553 &["b", "23456789012345678901234567890123456789A", "yz"],
1554 yx(1, 39),
1555 );
1556 cb.add_input_keys(&[Key::Char('B')]);
1557 ob = ob.refresh(
1558 linecol(1, 41),
1559 &["", "3456789012345678901234567890123456789AB", "z"],
1560 yx(1, 39),
1561 );
1562 cb.add_input_keys(&[Key::Char('C')]);
1563 ob = ob.refresh(
1564 linecol(1, 42),
1565 &["", "456789012345678901234567890123456789ABC", ""],
1566 yx(1, 39),
1567 );
1568
1569 for (file_col, cursor_col) in &[(41, 38), (40, 37), (39, 36)] {
1574 cb.add_input_keys(&[Key::ArrowLeft]);
1575 ob = ob.quick_refresh(linecol(1, *file_col), yx(1, *cursor_col));
1576 }
1577 cb.add_input_keys(&[Key::Char('D')]);
1578 ob = ob.refresh_line(linecol(1, 40), "456789012345678901234567890123456789DABC", yx(1, 37));
1579 cb.add_input_keys(&[Key::Char('E')]);
1580 ob = ob.refresh_line(linecol(1, 41), "456789012345678901234567890123456789DEAB", yx(1, 38));
1581
1582 cb.add_input_keys(&[Key::Backspace]);
1584 ob = ob.refresh_line(linecol(1, 40), "456789012345678901234567890123456789DABC", yx(1, 37));
1585 cb.add_input_keys(&[Key::Backspace]);
1586 ob = ob.refresh_line(linecol(1, 39), "456789012345678901234567890123456789ABC", yx(1, 36));
1587 cb.add_input_keys(&[Key::Backspace]);
1588 ob = ob.refresh_line(linecol(1, 38), "45678901234567890123456789012345678ABC", yx(1, 35));
1589
1590 for col in 0u16..35u16 {
1592 cb.add_input_keys(&[Key::ArrowLeft]);
1593 ob = ob.quick_refresh(linecol(1, usize::from(37 - col)), yx(1, 34 - col));
1594 }
1595 cb.add_input_keys(&[Key::ArrowLeft]);
1596 ob = ob.refresh(
1597 linecol(1, 2),
1598 &["", "345678901234567890123456789012345678ABC", "z"],
1599 yx(1, 0),
1600 );
1601 cb.add_input_keys(&[Key::ArrowLeft]);
1602 ob = ob.refresh(
1603 linecol(1, 1),
1604 &["b", "2345678901234567890123456789012345678ABC", "yz"],
1605 yx(1, 0),
1606 );
1607 cb.add_input_keys(&[Key::ArrowLeft]);
1608 ob = ob.refresh(
1609 linecol(1, 0),
1610 &["ab", "12345678901234567890123456789012345678AB", "xyz"],
1611 yx(1, 0),
1612 );
1613
1614 run_editor("ab\n\nxyz\n", "ab\n12345678901234567890123456789012345678ABC\nxyz\n", cb, ob);
1615 }
1616
1617 #[test]
1618 fn test_vertical_scrolling() {
1619 let mut cb = MockConsole::default();
1620 cb.set_size_chars(yx(5, 40));
1621 let mut ob = OutputBuilder::new(yx(5, 40));
1622 ob = ob.refresh(linecol(0, 0), &["abc", "", "d", "e"], yx(0, 0));
1623
1624 cb.add_input_keys(&[Key::ArrowDown]);
1626 ob = ob.quick_refresh(linecol(1, 0), yx(1, 0));
1627 cb.add_input_keys(&[Key::ArrowDown]);
1628 ob = ob.quick_refresh(linecol(2, 0), yx(2, 0));
1629 cb.add_input_keys(&[Key::ArrowDown]);
1630 ob = ob.quick_refresh(linecol(3, 0), yx(3, 0));
1631 cb.add_input_keys(&[Key::ArrowDown]);
1632 ob = ob.refresh(linecol(4, 0), &["", "d", "e", ""], yx(3, 0));
1633 cb.add_input_keys(&[Key::ArrowDown]);
1634 ob = ob.refresh(linecol(5, 0), &["d", "e", "", "fg"], yx(3, 0));
1635 cb.add_input_keys(&[Key::ArrowDown]);
1636 ob = ob.refresh(linecol(6, 0), &["e", "", "fg", "hij"], yx(3, 0));
1637
1638 cb.add_input_keys(&[Key::ArrowDown]);
1640 ob = ob.quick_refresh(linecol(6, 0), yx(3, 0));
1641
1642 cb.add_input_keys(&[Key::ArrowUp]);
1644 ob = ob.quick_refresh(linecol(5, 0), yx(2, 0));
1645 cb.add_input_keys(&[Key::ArrowUp]);
1646 ob = ob.quick_refresh(linecol(4, 0), yx(1, 0));
1647 cb.add_input_keys(&[Key::ArrowUp]);
1648 ob = ob.quick_refresh(linecol(3, 0), yx(0, 0));
1649 cb.add_input_keys(&[Key::ArrowUp]);
1650 ob = ob.refresh(linecol(2, 0), &["d", "e", "", "fg"], yx(0, 0));
1651 cb.add_input_keys(&[Key::ArrowUp]);
1652 ob = ob.refresh(linecol(1, 0), &["", "d", "e", ""], yx(0, 0));
1653 cb.add_input_keys(&[Key::ArrowUp]);
1654 ob = ob.refresh(linecol(0, 0), &["abc", "", "d", "e"], yx(0, 0));
1655
1656 cb.add_input_keys(&[Key::ArrowUp]);
1658 ob = ob.quick_refresh(linecol(0, 0), yx(0, 0));
1659
1660 run_editor("abc\n\nd\ne\n\nfg\nhij\n", "abc\n\nd\ne\n\nfg\nhij\n", cb, ob);
1661 }
1662
1663 #[test]
1664 fn test_vertical_scrolling_when_splitting_last_visible_line() {
1665 let mut cb = MockConsole::default();
1666 cb.set_size_chars(yx(4, 40));
1667 let mut ob = OutputBuilder::new(yx(4, 40));
1668 ob = ob.refresh(linecol(0, 0), &["first", "second", "thirdfourth"], yx(0, 0));
1669
1670 cb.add_input_keys(&[Key::ArrowDown]);
1672 ob = ob.quick_refresh(linecol(1, 0), yx(1, 0));
1673 cb.add_input_keys(&[Key::ArrowDown]);
1674 ob = ob.quick_refresh(linecol(2, 0), yx(2, 0));
1675 for i in 0.."third".len() {
1676 cb.add_input_keys(&[Key::ArrowRight]);
1677 ob = ob.quick_refresh(linecol(2, i + 1), yx(2, u16::try_from(i + 1).unwrap()));
1678 }
1679
1680 cb.add_input_keys(&[Key::NewLine]);
1682 ob = ob.set_dirty();
1683 ob = ob.refresh(linecol(3, 0), &["second", "third", "fourth"], yx(2, 0));
1684
1685 run_editor(
1686 "first\nsecond\nthirdfourth\nfifth\n",
1687 "first\nsecond\nthird\nfourth\nfifth\n",
1688 cb,
1689 ob,
1690 );
1691 }
1692
1693 #[test]
1694 fn test_horizontal_and_vertical_scrolling_when_splitting_last_visible_line() {
1695 let mut cb = MockConsole::default();
1696 cb.set_size_chars(yx(4, 40));
1697 let mut ob = OutputBuilder::new(yx(4, 40));
1698 ob = ob.refresh(
1699 linecol(0, 0),
1700 &["first", "second", "this is a line of text with more than 40"],
1701 yx(0, 0),
1702 );
1703
1704 cb.add_input_keys(&[Key::ArrowDown]);
1706 ob = ob.quick_refresh(linecol(1, 0), yx(1, 0));
1707 cb.add_input_keys(&[Key::ArrowDown]);
1708 ob = ob.quick_refresh(linecol(2, 0), yx(2, 0));
1709 for i in 0u16..39u16 {
1710 cb.add_input_keys(&[Key::ArrowRight]);
1711 ob = ob.quick_refresh(linecol(2, usize::from(i + 1)), yx(2, i + 1));
1712 }
1713 cb.add_input_keys(&[Key::ArrowRight]);
1714 ob = ob.refresh(
1715 linecol(2, 40),
1716 &["irst", "econd", "his is a line of text with more than 40 "],
1717 yx(2, 39),
1718 );
1719
1720 cb.add_input_keys(&[Key::NewLine]);
1722 ob = ob.set_dirty();
1723 ob = ob.refresh(
1724 linecol(3, 0),
1725 &["second", "this is a line of text with more than 40", " characters"],
1726 yx(2, 0),
1727 );
1728
1729 run_editor(
1730 "first\nsecond\nthis is a line of text with more than 40 characters\nfifth\n",
1731 "first\nsecond\nthis is a line of text with more than 40\n characters\nfifth\n",
1732 cb,
1733 ob,
1734 );
1735 }
1736
1737 #[test]
1738 fn test_vertical_scrolling_when_joining_first_visible_line() {
1739 let mut cb = MockConsole::default();
1740 cb.set_size_chars(yx(4, 40));
1741 let mut ob = OutputBuilder::new(yx(4, 40));
1742 ob = ob.refresh(linecol(0, 0), &["first", "second", "third"], yx(0, 0));
1743
1744 cb.add_input_keys(&[Key::ArrowDown]);
1746 ob = ob.quick_refresh(linecol(1, 0), yx(1, 0));
1747 cb.add_input_keys(&[Key::ArrowDown]);
1748 ob = ob.quick_refresh(linecol(2, 0), yx(2, 0));
1749 cb.add_input_keys(&[Key::ArrowDown]);
1750 ob = ob.refresh(linecol(3, 0), &["second", "third", "fourth"], yx(2, 0));
1751 cb.add_input_keys(&[Key::ArrowDown]);
1752 ob = ob.refresh(linecol(4, 0), &["third", "fourth", "fifth"], yx(2, 0));
1753
1754 cb.add_input_keys(&[Key::ArrowUp]);
1756 ob = ob.quick_refresh(linecol(3, 0), yx(1, 0));
1757 cb.add_input_keys(&[Key::ArrowUp]);
1758 ob = ob.quick_refresh(linecol(2, 0), yx(0, 0));
1759
1760 cb.add_input_keys(&[Key::Backspace]);
1762 ob = ob.set_dirty();
1763 ob = ob.refresh(linecol(1, 6), &["secondthird", "fourth", "fifth"], yx(0, 6));
1764
1765 run_editor(
1766 "first\nsecond\nthird\nfourth\nfifth\n",
1767 "first\nsecondthird\nfourth\nfifth\n",
1768 cb,
1769 ob,
1770 );
1771 }
1772
1773 #[test]
1774 fn test_horizontal_and_vertical_scrolling_when_joining_first_visible_line() {
1775 let mut cb = MockConsole::default();
1776 cb.set_size_chars(yx(4, 40));
1777 let mut ob = OutputBuilder::new(yx(4, 40));
1778 ob = ob.refresh(
1779 linecol(0, 0),
1780 &["first", "this is a line of text with more than 40", "third"],
1781 yx(0, 0),
1782 );
1783
1784 cb.add_input_keys(&[Key::ArrowDown]);
1786 ob = ob.quick_refresh(linecol(1, 0), yx(1, 0));
1787 cb.add_input_keys(&[Key::ArrowDown]);
1788 ob = ob.quick_refresh(linecol(2, 0), yx(2, 0));
1789 cb.add_input_keys(&[Key::ArrowDown]);
1790 ob = ob.refresh(
1791 linecol(3, 0),
1792 &["this is a line of text with more than 40", "third", "fourth"],
1793 yx(2, 0),
1794 );
1795 cb.add_input_keys(&[Key::ArrowDown]);
1796 ob = ob.refresh(linecol(4, 0), &["third", "fourth", "quite a long line"], yx(2, 0));
1797
1798 cb.add_input_keys(&[Key::ArrowUp]);
1800 ob = ob.quick_refresh(linecol(3, 0), yx(1, 0));
1801 cb.add_input_keys(&[Key::ArrowUp]);
1802 ob = ob.quick_refresh(linecol(2, 0), yx(0, 0));
1803
1804 cb.add_input_keys(&[Key::Backspace]);
1806 ob = ob.set_dirty();
1807 ob = ob.refresh(
1808 linecol(1, 51),
1809 &["ne of text with more than 40 characterst", "", " line"],
1810 yx(0, 39),
1811 );
1812
1813 run_editor(
1814 "first\nthis is a line of text with more than 40 characters\nthird\nfourth\nquite a long line\n",
1815 "first\nthis is a line of text with more than 40 charactersthird\nfourth\nquite a long line\n",
1816 cb,
1817 ob,
1818 );
1819 }
1820
1821 #[test]
1822 fn test_narrow_console() {
1823 let mut cb = MockConsole::default();
1824 cb.set_size_chars(yx(10, 25));
1825 let mut ob = OutputBuilder::new(yx(10, 25));
1826 ob = ob.refresh(linecol(0, 0), &[""], yx(0, 0));
1829
1830 run_editor("", "\n", cb, ob);
1831 }
1832
1833 #[test]
1834 fn test_very_narrow_console() {
1835 let mut cb = MockConsole::default();
1836 cb.set_size_chars(yx(10, 5));
1837 let mut ob = OutputBuilder::new(yx(10, 5));
1838 ob = ob.refresh(linecol(0, 0), &[""], yx(0, 0));
1841
1842 run_editor("", "\n", cb, ob);
1843 }
1844}