endbasic_repl/
editor.rs

1// EndBASIC
2// Copyright 2020 Julio Merino
3//
4// Licensed under the Apache License, Version 2.0 (the "License"); you may not
5// use this file except in compliance with the License.  You may obtain a copy
6// of the License at:
7//
8//     http://www.apache.org/licenses/LICENSE-2.0
9//
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
13// License for the specific language governing permissions and limitations
14// under the License.
15
16//! Interactive console-based text editor.
17
18use 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
26/// The color of the main editor window.
27const TEXT_COLOR: (Option<u8>, Option<u8>) = (Some(AnsiColor::White as u8), None);
28
29/// The color of the editor status bar.
30const STATUS_COLOR: (Option<u8>, Option<u8>) =
31    (Some(AnsiColor::BrightWhite as u8), Some(AnsiColor::Blue as u8));
32
33/// Default indentation with.
34const INDENT_WIDTH: usize = 4;
35
36/// Keybindings cheat sheet.
37const KEYS_SUMMARY: &str = " ESC Exit ";
38
39/// Returns the indentation of a given string as a new string.
40fn 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
51/// Finds the first position within the line that is not an indentation character, or returns
52/// the line length if no such character is found.
53fn 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/// Represents a position within a file.
66#[derive(Clone, Copy, Default)]
67struct FilePos {
68    /// The column number, starting from zero.
69    line: usize,
70
71    /// The row number, starting from zero.
72    col: usize,
73}
74
75/// An interactive console-based text editor.
76///
77/// The text editor owns the textual contents it is editing.
78pub struct Editor {
79    /// Path of the loaded program.  `None` if the program has never been saved yet.
80    name: Option<String>,
81
82    /// Owned contents of the file being edited.
83    content: Vec<LineBuffer>,
84
85    /// Whether the `content` was modified since it was last retrieved.
86    dirty: bool,
87
88    /// Position of the top-left character of the file visible in the console.
89    viewport_pos: FilePos,
90
91    /// Insertion position within the file.
92    file_pos: FilePos,
93
94    /// Last edited column, used when moving vertically to preserve the insertion point even when
95    /// traversing shorter lines.
96    insert_col: usize,
97}
98
99impl Default for Editor {
100    /// Creates a new editor without any stored contents.
101    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    /// Rewrites the status line at the bottom of the `console`, using the previously queried
115    /// `console_size`.
116    ///
117    /// It is the responsibility of the caller to move the cursor back to the appropriate location
118    /// after calling this function, and the caller should also hide the cursor before calling this
119    /// function.
120    fn refresh_status(&self, console: &mut dyn Console, console_size: CharsXY) -> io::Result<()> {
121        // Even though we track file positions as 0-indexed, display them as 1-indexed for a better
122        // user experience given that this is what all other editor seem to do.
123        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    /// Refreshes the contents of the whole `console`, using the previously queried `console_size`.
158    ///
159    /// It is the responsibility of the caller to move the cursor back to the appropriate location
160    /// after calling this function, and the caller should also hide the cursor before calling this
161    /// function.
162    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    /// Moves the cursor down by the given number of lines in `nlines` or to the last line if there
189    /// are insufficient lines to perform the move.
190    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    /// Moves the cursor up by the given number of lines in `nlines` or to the first line if there
202    /// are insufficient lines to perform the move.
203    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    /// Internal implementation of the interactive editor, which interacts with the `console`.
215    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            // The key handling below only deals with moving the insertion position within the file
225            // but does not bother to update the viewport. Adjust it now, if necessary.
226            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            // TODO(jmmv): We must handle the cursor visibility outside of the non-sync block
249            // because the current console implementation forces the cursor to be invisible
250            // when syncing is disabled.  This is suboptimal and should be fixed by decoupling
251            // the two properties...
252            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                            // TODO(jmmv): Refresh only the affected line.
326                            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                        // TODO(jmmv): Refresh only the affected line.
353                        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                        // TODO(jmmv): Refresh only the affected line.
410                        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                // TODO(jmmv): Should do something smarter with unknown keys.
428                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    /// Name of the program to inject into the editor for testing.  The name is very short because
483    /// all tests operate on a pretty narrow window and the status bar would be mangled otherwise.
484    const TEST_FILENAME: &str = "X";
485
486    /// Syntactic sugar to easily instantiate a `CharsXY` at `(x,y)`.
487    ///
488    /// Note that the input arguments here are swapped.  This is partly because of historical
489    /// reasons, but also because virtually all editors (including this one) display file positions
490    /// with the line first followed by the column.  It's easier to reason about the tests below
491    /// when the order of the arguments to `linecol` matches `yx`.
492    fn yx(y: u16, x: u16) -> CharsXY {
493        CharsXY::new(x, y)
494    }
495
496    /// Syntactic sugar to easily instantiate a `FilePos` at `(line, col)`.
497    fn linecol(line: usize, col: usize) -> FilePos {
498        FilePos { line, col }
499    }
500
501    /// Builder pattern to construct the expected sequence of side-effects on the console.
502    #[must_use]
503    struct OutputBuilder {
504        console_size: CharsXY,
505        output: Vec<CapturedOut>,
506        dirty: bool,
507    }
508
509    impl OutputBuilder {
510        /// Constructs a new output builder with just the command to enter the alternate screen.
511        /// `console_size` holds the size of the mock console, which is used to determine where to
512        /// print the status bar.
513        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        /// Records the console changes needed to update the status line to reflect a new `file_pos`
522        /// position.  Should not be used directly by tests.
523        ///
524        /// Note that, although `file_pos` is 0-indexed (to make it easier to reason about where
525        /// file changes actually happen in the internal buffers), we display the position as
526        /// 1-indexed here as the code under test does.
527        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                // Arbitrary number to define "narrow console" in tests.
535                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        /// Records the console changes needed to incrementally update the editor, without going
558        /// through a full refresh, assuming a `file_pos` position.
559        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        /// Records the console changes needed to refresh the whole console view.  The status line
570        /// is updated to reflect `file_pos`; the editor is pre-populated with the lines specified
571        /// in `previous`; and the `cursor` is placed at the given location.
572        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        /// Registers a new expected side-effect `co` on the console.
589        fn add(mut self, co: CapturedOut) -> Self {
590            self.output.push(co);
591            self
592        }
593
594        /// Registers that the file has been modified.
595        fn set_dirty(mut self) -> Self {
596            self.dirty = true;
597            self
598        }
599
600        /// Finalizes the list of expected side-effects on the console.
601        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    /// Runs the editor and expects that the resulting text matches `exp_text` and that the
610    /// side-effects on the console are those specified in `ob`.
611    ///
612    /// The editor can be pre-populated with some `previous` contents and the interactions with the
613    /// editor are specified in `cb`. Note that the final Esc key press needed to exit the editor
614    /// is automatically appended to `cb` here.
615    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        // Move the cursor to the right boundary.
1140        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        // Push the insertion point over the right boundary to cause scrolling.
1146        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        // Move down to a shorter line whose end character is still visible. No scrolling.
1172        cb.add_input_keys(&[Key::ArrowDown]);
1173        ob = ob.quick_refresh(linecol(1, 5), yx(1, 3));
1174
1175        // Move down to a shorter line that's not visible but for which insertion can still happen
1176        // without scrolling.
1177        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        // Move down to an empty line that requires horizontal scrolling for proper insertion.
1191        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        // Move down to the last line, which is long again and thus needs scrolling to the right to
1205        // make the insertion point visible.
1206        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        // Move to the last line.
1244        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        // Move the cursor to the right boundary.
1250        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        // Push the insertion point over the right boundary to cause scrolling.
1256        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        // Move up to a shorter line whose end character is still visible. No scrolling.
1282        cb.add_input_keys(&[Key::ArrowUp]);
1283        ob = ob.quick_refresh(linecol(3, 5), yx(3, 3));
1284
1285        // Move up to a shorter line that's not visible but for which insertion can still happen
1286        // without scrolling.
1287        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        // Move up to an empty line that requires horizontal scrolling for proper insertion.
1301        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        // Move up to the first line, which is long again and thus needs scrolling to the right to
1315        // make the insertion point visible.
1316        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        // Insert characters until the screen's right boundary.
1347        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        // Push the insertion line over the right boundary and test that surrounding lines scroll as
1356        // well.
1357        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        // Move back a few characters, without pushing over the left boundary, and then insert two
1377        // characters: one will cause the insertion line to fill up the empty space left by the
1378        // cursor and the other will cause the view of the insertion line to be truncated on the
1379        // right side.
1380        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        // Delete a few characters to restore the overflow part of the insertion line.
1398        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        // Move back to the beginning of the line to see surrounding lines reappear.
1418        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        // Move to the last line.
1452        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        // Attempting to push through the end of the file does nothing.
1466        cb.add_input_keys(&[Key::ArrowDown]);
1467        ob = ob.quick_refresh(linecol(6, 0), yx(3, 0));
1468
1469        // Go back up to the first line.
1470        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        // Attempting to push through the beginning of the file does nothing.
1484        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        // Move to the desired split point.
1498        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        // Split the last visible line.
1508        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        // Move to the desired split point.
1532        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        // Split the last visible line.
1548        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        // Move down until a couple of lines scroll up.
1572        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        // Move back up to the first visible line, without scrolling.
1582        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        // Join first visible line with previous, which should scroll contents up.
1588        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        // Move down until a couple of lines scroll up.
1612        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        // Move back up to the first visible line, without scrolling.
1626        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        // Join first visible line with previous, which should scroll contents up and right.
1632        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        // The key in this test is to verify that writing the status line doesn't trigger invalid
1654        // operations (like integer underflows).  See the logic in refresh_status for details.
1655        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        // The key in this test is to verify that writing the status line doesn't trigger invalid
1666        // operations (like integer underflows).  See the logic in refresh_status for details.
1667        ob = ob.refresh(linecol(0, 0), &[""], yx(0, 0));
1668
1669        run_editor("", "\n", cb, ob);
1670    }
1671}