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