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]);
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
1227 #[test]
1228 fn test_move_up_preserves_insertion_column_with_horizontal_scrolling() {
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(
1233 linecol(0, 0),
1234 &[
1235 "this is a line of text with more than 40",
1236 "",
1237 "a",
1238 "short",
1239 "another line of text with more than 40 c",
1240 ],
1241 yx(0, 0),
1242 );
1243
1244 for i in 0u16..4u16 {
1246 cb.add_input_keys(&[Key::ArrowDown]);
1247 ob = ob.quick_refresh(linecol(usize::from(i + 1), 0), yx(i + 1, 0));
1248 }
1249
1250 for col in 0u16..39u16 {
1252 cb.add_input_keys(&[Key::ArrowRight]);
1253 ob = ob.quick_refresh(linecol(4, usize::from(col + 1)), yx(4, col + 1));
1254 }
1255
1256 cb.add_input_keys(&[Key::ArrowRight]);
1258 ob = ob.refresh(
1259 linecol(4, 40),
1260 &[
1261 "his is a line of text with more than 40 ",
1262 "",
1263 "",
1264 "hort",
1265 "nother line of text with more than 40 ch",
1266 ],
1267 yx(4, 39),
1268 );
1269 cb.add_input_keys(&[Key::ArrowRight]);
1270 ob = ob.refresh(
1271 linecol(4, 41),
1272 &[
1273 "is is a line of text with more than 40 c",
1274 "",
1275 "",
1276 "ort",
1277 "other line of text with more than 40 cha",
1278 ],
1279 yx(4, 39),
1280 );
1281
1282 cb.add_input_keys(&[Key::ArrowUp]);
1284 ob = ob.quick_refresh(linecol(3, 5), yx(3, 3));
1285
1286 cb.add_input_keys(&[Key::ArrowUp]);
1289 ob = ob.refresh(
1290 linecol(2, 1),
1291 &[
1292 "his is a line of text with more than 40 ",
1293 "",
1294 "",
1295 "hort",
1296 "nother line of text with more than 40 ch",
1297 ],
1298 yx(2, 0),
1299 );
1300
1301 cb.add_input_keys(&[Key::ArrowUp]);
1303 ob = ob.refresh(
1304 linecol(1, 0),
1305 &[
1306 "this is a line of text with more than 40",
1307 "",
1308 "a",
1309 "short",
1310 "another line of text with more than 40 c",
1311 ],
1312 yx(1, 0),
1313 );
1314
1315 cb.add_input_keys(&[Key::ArrowUp]);
1318 ob = ob.refresh(
1319 linecol(0, 41),
1320 &[
1321 "is is a line of text with more than 40 c",
1322 "",
1323 "",
1324 "ort",
1325 "other line of text with more than 40 cha",
1326 ],
1327 yx(0, 39),
1328 );
1329
1330 run_editor(
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 "this is a line of text with more than 40 characters\n\na\nshort\nanother line of text with more than 40 characters\n",
1333 cb,
1334 ob,
1335 );
1336 }
1337
1338 #[test]
1339 fn test_horizontal_scrolling() {
1340 let mut cb = MockConsole::default();
1341 cb.set_size_chars(yx(10, 40));
1342 let mut ob = OutputBuilder::new(yx(10, 40));
1343 ob = ob.refresh(linecol(0, 0), &["ab", "", "xyz"], yx(0, 0));
1344
1345 cb.add_input_keys(&[Key::ArrowDown]);
1346 ob = ob.quick_refresh(linecol(1, 0), yx(1, 0));
1347
1348 for (col, ch) in "123456789012345678901234567890123456789".chars().enumerate() {
1350 cb.add_input_keys(&[Key::Char(ch)]);
1351 ob = ob.set_dirty();
1352 let mut buf = [0u8; 4];
1353 ob = ob.add(CapturedOut::Write(ch.encode_utf8(&mut buf).to_string()));
1354 ob = ob.quick_refresh(linecol(1, col + 1), yx(1, u16::try_from(col + 1).unwrap()));
1355 }
1356
1357 cb.add_input_keys(&[Key::Char('A')]);
1360 ob = ob.refresh(
1361 linecol(1, 40),
1362 &["b", "23456789012345678901234567890123456789A", "yz"],
1363 yx(1, 39),
1364 );
1365 cb.add_input_keys(&[Key::Char('B')]);
1366 ob = ob.refresh(
1367 linecol(1, 41),
1368 &["", "3456789012345678901234567890123456789AB", "z"],
1369 yx(1, 39),
1370 );
1371 cb.add_input_keys(&[Key::Char('C')]);
1372 ob = ob.refresh(
1373 linecol(1, 42),
1374 &["", "456789012345678901234567890123456789ABC", ""],
1375 yx(1, 39),
1376 );
1377
1378 for (file_col, cursor_col) in &[(41, 38), (40, 37), (39, 36)] {
1383 cb.add_input_keys(&[Key::ArrowLeft]);
1384 ob = ob.quick_refresh(linecol(1, *file_col), yx(1, *cursor_col));
1385 }
1386 cb.add_input_keys(&[Key::Char('D')]);
1387 ob = ob.refresh(
1388 linecol(1, 40),
1389 &["", "456789012345678901234567890123456789DABC", ""],
1390 yx(1, 37),
1391 );
1392 cb.add_input_keys(&[Key::Char('E')]);
1393 ob = ob.refresh(
1394 linecol(1, 41),
1395 &["", "456789012345678901234567890123456789DEAB", ""],
1396 yx(1, 38),
1397 );
1398
1399 cb.add_input_keys(&[Key::Backspace]);
1401 ob = ob.refresh(
1402 linecol(1, 40),
1403 &["", "456789012345678901234567890123456789DABC", ""],
1404 yx(1, 37),
1405 );
1406 cb.add_input_keys(&[Key::Backspace]);
1407 ob = ob.refresh(
1408 linecol(1, 39),
1409 &["", "456789012345678901234567890123456789ABC", ""],
1410 yx(1, 36),
1411 );
1412 cb.add_input_keys(&[Key::Backspace]);
1413 ob = ob.refresh(
1414 linecol(1, 38),
1415 &["", "45678901234567890123456789012345678ABC", ""],
1416 yx(1, 35),
1417 );
1418
1419 for col in 0u16..35u16 {
1421 cb.add_input_keys(&[Key::ArrowLeft]);
1422 ob = ob.quick_refresh(linecol(1, usize::from(37 - col)), yx(1, 34 - col));
1423 }
1424 cb.add_input_keys(&[Key::ArrowLeft]);
1425 ob = ob.refresh(
1426 linecol(1, 2),
1427 &["", "345678901234567890123456789012345678ABC", "z"],
1428 yx(1, 0),
1429 );
1430 cb.add_input_keys(&[Key::ArrowLeft]);
1431 ob = ob.refresh(
1432 linecol(1, 1),
1433 &["b", "2345678901234567890123456789012345678ABC", "yz"],
1434 yx(1, 0),
1435 );
1436 cb.add_input_keys(&[Key::ArrowLeft]);
1437 ob = ob.refresh(
1438 linecol(1, 0),
1439 &["ab", "12345678901234567890123456789012345678AB", "xyz"],
1440 yx(1, 0),
1441 );
1442
1443 run_editor("ab\n\nxyz\n", "ab\n12345678901234567890123456789012345678ABC\nxyz\n", cb, ob);
1444 }
1445
1446 #[test]
1447 fn test_vertical_scrolling() {
1448 let mut cb = MockConsole::default();
1449 cb.set_size_chars(yx(5, 40));
1450 let mut ob = OutputBuilder::new(yx(5, 40));
1451 ob = ob.refresh(linecol(0, 0), &["abc", "", "d", "e"], yx(0, 0));
1452
1453 cb.add_input_keys(&[Key::ArrowDown]);
1455 ob = ob.quick_refresh(linecol(1, 0), yx(1, 0));
1456 cb.add_input_keys(&[Key::ArrowDown]);
1457 ob = ob.quick_refresh(linecol(2, 0), yx(2, 0));
1458 cb.add_input_keys(&[Key::ArrowDown]);
1459 ob = ob.quick_refresh(linecol(3, 0), yx(3, 0));
1460 cb.add_input_keys(&[Key::ArrowDown]);
1461 ob = ob.refresh(linecol(4, 0), &["", "d", "e", ""], yx(3, 0));
1462 cb.add_input_keys(&[Key::ArrowDown]);
1463 ob = ob.refresh(linecol(5, 0), &["d", "e", "", "fg"], yx(3, 0));
1464 cb.add_input_keys(&[Key::ArrowDown]);
1465 ob = ob.refresh(linecol(6, 0), &["e", "", "fg", "hij"], yx(3, 0));
1466
1467 cb.add_input_keys(&[Key::ArrowDown]);
1469 ob = ob.quick_refresh(linecol(6, 0), yx(3, 0));
1470
1471 cb.add_input_keys(&[Key::ArrowUp]);
1473 ob = ob.quick_refresh(linecol(5, 0), yx(2, 0));
1474 cb.add_input_keys(&[Key::ArrowUp]);
1475 ob = ob.quick_refresh(linecol(4, 0), yx(1, 0));
1476 cb.add_input_keys(&[Key::ArrowUp]);
1477 ob = ob.quick_refresh(linecol(3, 0), yx(0, 0));
1478 cb.add_input_keys(&[Key::ArrowUp]);
1479 ob = ob.refresh(linecol(2, 0), &["d", "e", "", "fg"], yx(0, 0));
1480 cb.add_input_keys(&[Key::ArrowUp]);
1481 ob = ob.refresh(linecol(1, 0), &["", "d", "e", ""], yx(0, 0));
1482 cb.add_input_keys(&[Key::ArrowUp]);
1483 ob = ob.refresh(linecol(0, 0), &["abc", "", "d", "e"], yx(0, 0));
1484
1485 cb.add_input_keys(&[Key::ArrowUp]);
1487 ob = ob.quick_refresh(linecol(0, 0), yx(0, 0));
1488
1489 run_editor("abc\n\nd\ne\n\nfg\nhij\n", "abc\n\nd\ne\n\nfg\nhij\n", cb, ob);
1490 }
1491
1492 #[test]
1493 fn test_vertical_scrolling_when_splitting_last_visible_line() {
1494 let mut cb = MockConsole::default();
1495 cb.set_size_chars(yx(4, 40));
1496 let mut ob = OutputBuilder::new(yx(4, 40));
1497 ob = ob.refresh(linecol(0, 0), &["first", "second", "thirdfourth"], yx(0, 0));
1498
1499 cb.add_input_keys(&[Key::ArrowDown]);
1501 ob = ob.quick_refresh(linecol(1, 0), yx(1, 0));
1502 cb.add_input_keys(&[Key::ArrowDown]);
1503 ob = ob.quick_refresh(linecol(2, 0), yx(2, 0));
1504 for i in 0.."third".len() {
1505 cb.add_input_keys(&[Key::ArrowRight]);
1506 ob = ob.quick_refresh(linecol(2, i + 1), yx(2, u16::try_from(i + 1).unwrap()));
1507 }
1508
1509 cb.add_input_keys(&[Key::NewLine]);
1511 ob = ob.set_dirty();
1512 ob = ob.refresh(linecol(3, 0), &["second", "third", "fourth"], yx(2, 0));
1513
1514 run_editor(
1515 "first\nsecond\nthirdfourth\nfifth\n",
1516 "first\nsecond\nthird\nfourth\nfifth\n",
1517 cb,
1518 ob,
1519 );
1520 }
1521
1522 #[test]
1523 fn test_horizontal_and_vertical_scrolling_when_splitting_last_visible_line() {
1524 let mut cb = MockConsole::default();
1525 cb.set_size_chars(yx(4, 40));
1526 let mut ob = OutputBuilder::new(yx(4, 40));
1527 ob = ob.refresh(
1528 linecol(0, 0),
1529 &["first", "second", "this is a line of text with more than 40"],
1530 yx(0, 0),
1531 );
1532
1533 cb.add_input_keys(&[Key::ArrowDown]);
1535 ob = ob.quick_refresh(linecol(1, 0), yx(1, 0));
1536 cb.add_input_keys(&[Key::ArrowDown]);
1537 ob = ob.quick_refresh(linecol(2, 0), yx(2, 0));
1538 for i in 0u16..39u16 {
1539 cb.add_input_keys(&[Key::ArrowRight]);
1540 ob = ob.quick_refresh(linecol(2, usize::from(i + 1)), yx(2, i + 1));
1541 }
1542 cb.add_input_keys(&[Key::ArrowRight]);
1543 ob = ob.refresh(
1544 linecol(2, 40),
1545 &["irst", "econd", "his is a line of text with more than 40 "],
1546 yx(2, 39),
1547 );
1548
1549 cb.add_input_keys(&[Key::NewLine]);
1551 ob = ob.set_dirty();
1552 ob = ob.refresh(
1553 linecol(3, 0),
1554 &["second", "this is a line of text with more than 40", " characters"],
1555 yx(2, 0),
1556 );
1557
1558 run_editor(
1559 "first\nsecond\nthis is a line of text with more than 40 characters\nfifth\n",
1560 "first\nsecond\nthis is a line of text with more than 40\n characters\nfifth\n",
1561 cb,
1562 ob,
1563 );
1564 }
1565
1566 #[test]
1567 fn test_vertical_scrolling_when_joining_first_visible_line() {
1568 let mut cb = MockConsole::default();
1569 cb.set_size_chars(yx(4, 40));
1570 let mut ob = OutputBuilder::new(yx(4, 40));
1571 ob = ob.refresh(linecol(0, 0), &["first", "second", "third"], yx(0, 0));
1572
1573 cb.add_input_keys(&[Key::ArrowDown]);
1575 ob = ob.quick_refresh(linecol(1, 0), yx(1, 0));
1576 cb.add_input_keys(&[Key::ArrowDown]);
1577 ob = ob.quick_refresh(linecol(2, 0), yx(2, 0));
1578 cb.add_input_keys(&[Key::ArrowDown]);
1579 ob = ob.refresh(linecol(3, 0), &["second", "third", "fourth"], yx(2, 0));
1580 cb.add_input_keys(&[Key::ArrowDown]);
1581 ob = ob.refresh(linecol(4, 0), &["third", "fourth", "fifth"], yx(2, 0));
1582
1583 cb.add_input_keys(&[Key::ArrowUp]);
1585 ob = ob.quick_refresh(linecol(3, 0), yx(1, 0));
1586 cb.add_input_keys(&[Key::ArrowUp]);
1587 ob = ob.quick_refresh(linecol(2, 0), yx(0, 0));
1588
1589 cb.add_input_keys(&[Key::Backspace]);
1591 ob = ob.set_dirty();
1592 ob = ob.refresh(linecol(1, 6), &["secondthird", "fourth", "fifth"], yx(0, 6));
1593
1594 run_editor(
1595 "first\nsecond\nthird\nfourth\nfifth\n",
1596 "first\nsecondthird\nfourth\nfifth\n",
1597 cb,
1598 ob,
1599 );
1600 }
1601
1602 #[test]
1603 fn test_horizontal_and_vertical_scrolling_when_joining_first_visible_line() {
1604 let mut cb = MockConsole::default();
1605 cb.set_size_chars(yx(4, 40));
1606 let mut ob = OutputBuilder::new(yx(4, 40));
1607 ob = ob.refresh(
1608 linecol(0, 0),
1609 &["first", "this is a line of text with more than 40", "third"],
1610 yx(0, 0),
1611 );
1612
1613 cb.add_input_keys(&[Key::ArrowDown]);
1615 ob = ob.quick_refresh(linecol(1, 0), yx(1, 0));
1616 cb.add_input_keys(&[Key::ArrowDown]);
1617 ob = ob.quick_refresh(linecol(2, 0), yx(2, 0));
1618 cb.add_input_keys(&[Key::ArrowDown]);
1619 ob = ob.refresh(
1620 linecol(3, 0),
1621 &["this is a line of text with more than 40", "third", "fourth"],
1622 yx(2, 0),
1623 );
1624 cb.add_input_keys(&[Key::ArrowDown]);
1625 ob = ob.refresh(linecol(4, 0), &["third", "fourth", "quite a long line"], yx(2, 0));
1626
1627 cb.add_input_keys(&[Key::ArrowUp]);
1629 ob = ob.quick_refresh(linecol(3, 0), yx(1, 0));
1630 cb.add_input_keys(&[Key::ArrowUp]);
1631 ob = ob.quick_refresh(linecol(2, 0), yx(0, 0));
1632
1633 cb.add_input_keys(&[Key::Backspace]);
1635 ob = ob.set_dirty();
1636 ob = ob.refresh(
1637 linecol(1, 51),
1638 &["ne of text with more than 40 characterst", "", " line"],
1639 yx(0, 39),
1640 );
1641
1642 run_editor(
1643 "first\nthis is a line of text with more than 40 characters\nthird\nfourth\nquite a long line\n",
1644 "first\nthis is a line of text with more than 40 charactersthird\nfourth\nquite a long line\n",
1645 cb,
1646 ob,
1647 );
1648 }
1649
1650 #[test]
1651 fn test_narrow_console() {
1652 let mut cb = MockConsole::default();
1653 cb.set_size_chars(yx(10, 25));
1654 let mut ob = OutputBuilder::new(yx(10, 25));
1655 ob = ob.refresh(linecol(0, 0), &[""], yx(0, 0));
1658
1659 run_editor("", "\n", cb, ob);
1660 }
1661
1662 #[test]
1663 fn test_very_narrow_console() {
1664 let mut cb = MockConsole::default();
1665 cb.set_size_chars(yx(10, 5));
1666 let mut ob = OutputBuilder::new(yx(10, 5));
1667 ob = ob.refresh(linecol(0, 0), &[""], yx(0, 0));
1670
1671 run_editor("", "\n", cb, ob);
1672 }
1673}