1
use crate::commands::ALL_COMMANDS;
2
use crate::{
3
    commands, utils, AnsiPosition, Boundary, Config, Console, Document, Help, History, LineNumber,
4
    Mode, Navigator, OperationType, Row, RowIndex,
5
};
6
use serde::ser::{SerializeStruct, Serializer};
7
use serde::Serialize;
8
use std::cmp;
9
use std::io;
10
use std::path::PathBuf;
11
use termion::color;
12
use termion::event::{Event, Key, MouseButton, MouseEvent};
13
use unicode_segmentation::UnicodeSegmentation;
14

            
15
const STATUS_FG_COLOR: color::Rgb = color::Rgb(63, 63, 63);
16
const STATUS_BG_COLOR: color::Rgb = color::Rgb(239, 239, 239);
17
const PKG: &str = "bo";
18
const COMMAND_PREFIX: char = ':';
19
const SEARCH_PREFIX: char = '/';
20
const AUTOCOMPLETION_SUGGESTIONS_SEPARATOR: char = '|';
21
const LINE_NUMBER_OFFSET: u8 = 4; // number of chars
22
const START_X: u8 = LINE_NUMBER_OFFSET as u8; // index, so that's actually an offset of 5 chars
23
const SPACES_PER_TAB: usize = 4;
24
const SWAP_SAVE_EVERY: u8 = 100; // save to a swap file every 100 unsaved edits
25

            
26
271
#[derive(Debug, Default, PartialEq, Clone, Copy, Serialize)]
27
pub struct Position {
28
311
    pub x: usize,
29
399
    pub y: usize,
30
}
31

            
32
impl Position {
33
2
    pub fn reset_x(&mut self) {
34
2
        self.x = 0;
35
4
    }
36
    #[must_use]
37
46
    pub fn top_left() -> Self {
38
46
        Self::default()
39
46
    }
40
}
41

            
42
impl From<AnsiPosition> for Position {
43
1
    fn from(p: AnsiPosition) -> Self {
44
1
        Self {
45
1
            x: p.x.saturating_sub(1) as usize,
46
1
            y: p.y.saturating_sub(1) as usize,
47
        }
48
1
    }
49
}
50

            
51
93
#[derive(Debug, Default, Serialize)]
52
pub struct ViewportOffset {
53
46
    pub rows: usize,
54
46
    pub columns: usize,
55
}
56

            
57
#[derive(Debug)]
58
enum Direction {
59
    Up,
60
    Down,
61
    Left,
62
    Right,
63
}
64

            
65
#[derive(Debug)]
66
pub struct Editor {
67
    should_quit: bool,
68
    cursor_position: Position,
69
    document: Document,
70
    offset: ViewportOffset,
71
    message: String,
72
    mode: Mode,
73
    command_buffer: String,
74
    command_suggestions: Vec<String>,
75
    current_autocompletion_index: usize,
76
    config: Config,
77
    normal_command_buffer: Vec<String>,
78
    mouse_event_buffer: Vec<Position>,
79
    search_matches: Vec<(Position, Position)>,
80
    current_search_match_index: usize,
81
    alternate_screen: bool,
82
    last_saved_hash: u64,
83
    terminal: Box<dyn Console>,
84
    unsaved_edits: u8,
85
    row_prefix_length: u8,
86
    help_message: String,
87
    history: History,
88
}
89

            
90
fn die(e: &io::Error) {
91
    print!("{}", termion::clear::All);
92
    panic!("{}", e);
93
}
94

            
95
impl Serialize for Editor {
96
1
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
97
    where
98
        S: Serializer,
99
    {
100
1
        let mut s = serializer.serialize_struct("Editor", 10)?;
101
1
        s.serialize_field("cursor_position", &self.cursor_position)?;
102
1
        s.serialize_field("offset", &self.offset)?;
103
1
        s.serialize_field("mode", format!("{}", self.mode).as_str())?;
104
1
        s.serialize_field("command_buffer", &self.command_buffer)?;
105
1
        s.serialize_field("normal_command_buffer", &self.normal_command_buffer)?;
106
1
        s.serialize_field("search_matches", &self.search_matches)?;
107
1
        s.serialize_field(
108
            "current_search_match_index",
109
1
            &self.current_search_match_index,
110
        )?;
111
1
        s.serialize_field("unsaved_edits", &self.unsaved_edits)?;
112
1
        s.serialize_field("last_saved_hash", &self.last_saved_hash)?;
113
1
        s.serialize_field("row_prefix_length", &self.row_prefix_length)?;
114
1
        s.serialize_field("document", &self.document)?;
115
1
        s.serialize_field("command_suggestions", &self.command_suggestions)?;
116
1
        s.serialize_field(
117
            "current_autocompletion_index",
118
1
            &self.current_autocompletion_index,
119
1
        )?;
120
1
        s.end()
121
1
    }
122
}
123

            
124
impl Editor {
125
46
    pub fn new(filename: Option<String>, terminal: Box<dyn Console>) -> Self {
126
46
        let document: Document = match filename {
127
43
            None => Document::default(),
128
            // Some(path) => Document::open(utils::expand_tilde(&path).as_str()).unwrap_or_default(),
129
3
            Some(path) => Document::open(std::path::PathBuf::from(utils::expand_tilde(&path)))
130
3
                .unwrap_or_default(),
131
        };
132
46
        let last_saved_hash = document.hashed();
133
46
        let help_message = Help::default().format();
134
46
        Self {
135
            should_quit: false,
136
46
            cursor_position: Position::top_left(),
137
46
            document,
138
46
            offset: ViewportOffset::default(),
139
46
            message: "".to_string(),
140
46
            mode: Mode::Normal,
141
46
            command_buffer: "".to_string(),
142
46
            command_suggestions: vec![],
143
            current_autocompletion_index: 0,
144
46
            config: Config::default(),
145
46
            normal_command_buffer: vec![],
146
46
            mouse_event_buffer: vec![],
147
46
            search_matches: vec![],
148
            current_search_match_index: 0,
149
            alternate_screen: false,
150
            terminal,
151
            unsaved_edits: 0,
152
            last_saved_hash,
153
            row_prefix_length: 0,
154
46
            help_message,
155
46
            history: History::default(),
156
        }
157
46
    }
158

            
159
    /// Main screen rendering loop
160
    pub fn run(&mut self) {
161
        loop {
162
            if let Err(error) = self.refresh_screen() {
163
                die(&error);
164
            }
165
            if let Err(error) = self.process_event() {
166
                die(&error);
167
            }
168
            if self.should_quit {
169
                self.terminal.clear_screen();
170
                break;
171
            }
172
        }
173
    }
174

            
175
    /// Main event processing method. An event can be either be a keystroke or a mouse click
176
    fn process_event(&mut self) -> Result<(), std::io::Error> {
177
        let event = self.terminal.read_event()?;
178
        match event {
179
            Event::Key(pressed_key) => self.process_keystroke(pressed_key),
180
            Event::Mouse(mouse_event) => self.process_mouse_event(mouse_event),
181
            Event::Unsupported(_) => (),
182
        }
183
        Ok(())
184
    }
185

            
186
    /// React to a keystroke. The reaction itself depends on the editor
187
    /// mode (insert, command, normal) or whether the editor is currently
188
    /// receiving a user input command (eg: ":q", etc).
189
226
    fn process_keystroke(&mut self, pressed_key: Key) {
190
226
        if self.is_receiving_command() {
191
91
            if self.is_autocompleting_command() {
192
4
                match pressed_key {
193
2
                    Key::Char('\t') => self.cycle_through_command_suggestions(),
194
1
                    Key::Char('\n') => {
195
2
                        self.command_buffer = format!(
196
                            "{}{}",
197
                            COMMAND_PREFIX,
198
1
                            self.command_suggestions[self.current_autocompletion_index].clone()
199
                        );
200
1
                        self.reset_autocompletions();
201
1
                        self.process_keystroke(pressed_key);
202
                    }
203
                    _ => {
204
1
                        self.reset_autocompletions();
205
1
                        self.process_keystroke(pressed_key);
206
                    }
207
                }
208
            } else {
209
                // accumulate the command in the command buffer
210
87
                match pressed_key {
211
1
                    Key::Esc => self.stop_receiving_command(),
212
16
                    Key::Char('\n') => {
213
                        // Enter
214
16
                        self.process_received_command();
215
16
                        self.stop_receiving_command();
216
                    }
217
3
                    Key::Char('\t') => self.autocomplete_command(),
218
66
                    Key::Char(c) => self.command_buffer.push(c), // accumulate keystrokes into the buffer
219
2
                    Key::Backspace => self
220
                        .command_buffer
221
1
                        .truncate(self.command_buffer.len().saturating_sub(1)),
222
                    _ => (),
223
                }
224
            }
225
        } else {
226
135
            match self.mode {
227
87
                Mode::Normal => self.process_normal_command(pressed_key),
228
48
                Mode::Insert => self.process_insert_command(pressed_key),
229
            }
230
        }
231
226
    }
232

            
233
    /// React to a mouse event. If the mouse is being pressed, record
234
    /// the coordinates, and
235
    fn process_mouse_event(&mut self, mouse_event: MouseEvent) {
236
        match mouse_event {
237
            MouseEvent::Press(MouseButton::Left, _, _) => self.mouse_event_buffer.push(
238
                self.terminal
239
                    .get_cursor_index_from_mouse_event(mouse_event, self.row_prefix_length),
240
            ),
241
            MouseEvent::Release(_, _) => {
242
                if !self.mouse_event_buffer.is_empty() {
243
                    // Make sure that we're moving to an x/y location in which we already
244
                    // have text, to avoid breaking out of the document bounds.
245
                    let cursor_position = self.mouse_event_buffer.pop().unwrap();
246
                    if cursor_position.y.saturating_add(1) <= self.document.num_rows() {
247
                        if let Some(target_row) = self.get_row(RowIndex::new(cursor_position.y)) {
248
                            if cursor_position.x <= target_row.len() {
249
                                self.cursor_position = cursor_position;
250
                            }
251
                        }
252
                    }
253
                }
254
            }
255
            _ => (),
256
        }
257
    }
258

            
259
    /// Switch the Editor mode to Insert
260
17
    fn enter_insert_mode(&mut self) {
261
17
        self.mode = Mode::Insert;
262
34
        self.terminal.set_cursor_as_steady_bar();
263
17
    }
264

            
265
    /// Switch the Editor mode to Normal
266
11
    fn enter_normal_mode(&mut self) {
267
11
        self.mode = Mode::Normal;
268
22
        self.terminal.set_cursor_as_steady_block();
269
11
    }
270

            
271
    /// Make the Editor ready to receive a command
272
21
    fn start_receiving_command(&mut self) {
273
21
        self.command_buffer.push(COMMAND_PREFIX);
274
21
    }
275

            
276
    /// Make the Editor ready to receive a search pattern
277
1
    fn start_receiving_search_pattern(&mut self) {
278
1
        self.command_buffer.push(SEARCH_PREFIX);
279
1
    }
280

            
281
    /// Stop receiving a command
282
18
    fn stop_receiving_command(&mut self) {
283
18
        self.command_buffer = "".to_string();
284
18
    }
285

            
286
    /// Return whether the Editor is currently receiving a command
287
334
    fn is_receiving_command(&self) -> bool {
288
334
        !self.command_buffer.is_empty()
289
334
    }
290

            
291
    /// Return whether the Editor is currently autosuggesting command based on the user input
292
99
    fn is_autocompleting_command(&self) -> bool {
293
99
        self.is_receiving_command() && !self.command_suggestions.is_empty()
294
99
    }
295

            
296
    /// Reset the state of the command autocompletion
297
2
    fn reset_autocompletions(&mut self) {
298
2
        self.command_suggestions = vec![];
299
2
        self.current_autocompletion_index = 0;
300
2
    }
301

            
302
18
    fn pop_normal_command_repetitions(&mut self) -> usize {
303
18
        let times = match self.normal_command_buffer.len() {
304
13
            0 => 1,
305
5
            _ => self
306
                .normal_command_buffer
307
                .join("")
308
                .parse::<usize>()
309
5
                .unwrap(),
310
        };
311
18
        self.normal_command_buffer = vec![];
312
        times
313
18
    }
314

            
315
    /// Receive a command entered by the user in the command prompt
316
    /// and take appropriate actions
317
16
    fn process_received_command(&mut self) {
318
16
        let command = self.command_buffer.clone();
319
16
        match self.command_buffer.chars().next().unwrap() {
320
            SEARCH_PREFIX => {
321
1
                self.process_search_command(command.strip_prefix(SEARCH_PREFIX).unwrap());
322
            }
323
15
            COMMAND_PREFIX => {
324
15
                let command = command.strip_prefix(COMMAND_PREFIX).unwrap_or_default();
325
15
                if command.is_empty() {
326
15
                } else if command.chars().all(char::is_numeric) {
327
                    // :n will get you to line n
328
2
                    let line_number = command.parse::<usize>().unwrap();
329
2
                    self.goto_line(LineNumber::new(line_number), 0);
330
13
                } else if command.split(' ').count() > 1 {
331
2
                    let cmd_tokens: Vec<&str> = command.split(' ').collect();
332
2
                    match *cmd_tokens.get(0).unwrap_or(&"") {
333
2
                        commands::OPEN | commands::OPEN_SHORT => {
334
2
                            if let Ok(document) = Document::open(PathBuf::from(cmd_tokens[1])) {
335
1
                                self.document = document;
336
1
                                self.last_saved_hash = self.document.hashed();
337
1
                                self.reset_message();
338
1
                                self.cursor_position = Position::default();
339
                            } else {
340
                                self.display_message(utils::red(&format!(
341
                                    "{} not found",
342
                                    cmd_tokens[1]
343
                                )));
344
                            }
345
1
                        }
346
1
                        commands::NEW => {
347
1
                            self.document =
348
1
                                Document::new_empty(PathBuf::from(cmd_tokens[1].to_string()));
349
1
                            self.enter_insert_mode();
350
                        }
351
                        commands::SAVE => {
352
                            let new_name = cmd_tokens[1..].join(" ");
353
                            self.save(new_name.trim());
354
                        }
355
                        _ => self.display_message(utils::red(&format!(
356
                            "Unknown command '{}'",
357
                            cmd_tokens[0]
358
                        ))),
359
                    }
360
2
                } else {
361
                    match command {
362
11
                        commands::FORCE_QUIT => self.quit(true),
363
11
                        commands::QUIT => self.quit(false),
364
13
                        commands::LINE_NUMBERS => {
365
2
                            self.config.display_line_numbers =
366
2
                                Config::toggle(self.config.display_line_numbers);
367
2
                            self.row_prefix_length = if self.config.display_line_numbers {
368
1
                                START_X
369
                            } else {
370
1
                                0
371
                            };
372
                        }
373
9
                        commands::STATS => {
374
2
                            self.config.display_stats = Config::toggle(self.config.display_stats);
375
                        }
376
7
                        commands::HELP => {
377
1
                            self.alternate_screen = true;
378
                        }
379
6
                        commands::SAVE => self.save(""),
380
3
                        commands::SAVE_AND_QUIT => {
381
1
                            self.save("");
382
1
                            self.quit(false);
383
                        }
384
2
                        commands::DEBUG => {
385
                            if let Ok(state) = serde_json::to_string_pretty(&self) {
386
                                utils::log(state.as_str());
387
                            }
388
                        }
389
4
                        _ => self
390
6
                            .display_message(utils::red(&format!("Unknown command '{}'", command))),
391
                    }
392
                }
393
            }
394
            _ => (),
395
        }
396
16
    }
397

            
398
    /// Determine which commands could be autocompleted into based on the current
399
    /// state of the user provided command.
400
    ///
401
    /// If only one command suggestion is found, it will be automatically selected.
402
    /// Else, the ``command_suggestions`` vector will be populated with the possible
403
    /// commands.
404
3
    fn autocomplete_command(&mut self) {
405
3
        let mut matches: Vec<String> = vec![];
406
3
        let current_command = self
407
            .command_buffer
408
            .strip_prefix(COMMAND_PREFIX)
409
            .unwrap_or_default();
410
36
        for command_str in ALL_COMMANDS {
411
33
            if command_str.starts_with(&current_command) {
412
5
                matches.push(command_str.to_owned());
413
            }
414
        }
415
3
        match matches.len() {
416
            0 => (),
417
1
            1 => self.command_buffer = format!("{}{}", COMMAND_PREFIX, matches[0]),
418
2
            _ => self.command_suggestions = matches,
419
        }
420
3
    }
421

            
422
    /// Cycle through the possible command suggestions by incrementing (or resetting)
423
    /// the ``current_autocompletion_index`` value.
424
2
    fn cycle_through_command_suggestions(&mut self) {
425
4
        let next_suggestion_index = if self.current_autocompletion_index
426
4
            == self.command_suggestions.len().saturating_sub(1)
427
        {
428
1
            0
429
        } else {
430
1
            self.current_autocompletion_index.saturating_add(1)
431
        };
432
2
        self.current_autocompletion_index = next_suggestion_index;
433
2
    }
434

            
435
    /// Save the current document to the target file.
436
    ///
437
    /// If no filename is associated to the current document, an error message will be displayed.
438
    ///
439
    /// When the document is saved, all trailing spaces will automatically be deleted.
440
4
    fn save(&mut self, new_name: &str) {
441
        // this will trim trailing spaces, which might cause the cursor to get out of bounds
442
4
        self.document.trim_trailing_spaces();
443
7
        if self.cursor_position.x >= self.current_row().len() {
444
3
            self.cursor_position.x = self.current_row().len().saturating_sub(1);
445
        }
446

            
447
4
        let initial_filename = self.document.filename.clone();
448
8
        if new_name.is_empty() {
449
4
            if self.document.filename.is_none() {
450
                self.display_message(utils::red("No file name"));
451
                return;
452
4
            } else if self.document.save().is_ok() {
453
4
                self.display_message("File successfully saved".to_string());
454
4
                self.last_saved_hash = self.document.hashed();
455
            } else {
456
                self.display_message(utils::red("Error writing to file!"));
457
                return;
458
            }
459
        } else if self.document.save_as(new_name).is_ok() {
460
            if initial_filename.is_none() {
461
                self.display_message(format!("Buffer saved to {}", new_name));
462
            } else {
463
                self.display_message(format!(
464
                    "{} successfully renamed to {}",
465
                    self.document
466
                        .filename
467
                        .as_ref()
468
                        .unwrap()
469
                        .to_str()
470
                        .unwrap_or_default(),
471
                    new_name
472
                ));
473
            }
474
            self.document.filename = Some(PathBuf::from(new_name));
475
        } else {
476
            self.display_message(utils::red("Error writing to file!"));
477
        }
478
4
        self.unsaved_edits = 0;
479
4
        self.last_saved_hash = self.document.hashed();
480
4
    }
481

            
482
    /// Save all current unsaved edits to a swap file, allowing a seamless
483
    /// recovery in case of a crash.
484
    fn save_to_swap_file(&mut self) {
485
        if self.document.save_to_swap_file().is_ok() {
486
            self.unsaved_edits = 0;
487
        }
488
    }
489

            
490
    /// Change the internal state of the Editor to mark it as ready to quit.
491
    ///
492
    /// If ``force`` is set to ``false`` and some unsaved edits were made, an
493
    /// error will be displayed in the message bar.
494
4
    fn quit(&mut self, force: bool) {
495
4
        if self.is_dirty() && !force {
496
1
            self.display_message(utils::red("Unsaved changes! Run :q! to override"));
497
        } else {
498
3
            self.should_quit = true;
499
        }
500
4
    }
501

            
502
    /// Search for the user-provided pattern in the document text,
503
    /// and move the cursor to the first occurence, if any.
504
1
    fn process_search_command(&mut self, search_pattern: &str) {
505
1
        self.reset_search();
506
4
        for (row_index, row) in self.document.iter().enumerate() {
507
6
            if row.contains(search_pattern) {
508
6
                for match_start_index in row.find_all(search_pattern) {
509
3
                    let match_start = Position {
510
                        x: match_start_index,
511
6
                        y: row_index.saturating_add(1), // terminal line number, 1-based
512
                    };
513
3
                    let match_end = Position {
514
6
                        x: match_start_index
515
                            .saturating_add(1)
516
3
                            .saturating_add(search_pattern.len()),
517
3
                        y: row_index.saturating_add(1),
518
                    };
519
3
                    self.search_matches.push((match_start, match_end));
520
                }
521
            }
522
        }
523
1
        self.display_message(format!("{} matches", self.search_matches.len()));
524
1
        self.current_search_match_index = self.search_matches.len().saturating_sub(1);
525
1
        self.goto_next_search_match();
526
1
    }
527

            
528
    /// Reset all state related to text search back to the default values.
529
2
    fn reset_search(&mut self) {
530
2
        self.search_matches = vec![]; // erase previous search matches
531
2
        self.current_search_match_index = 0;
532
2
    }
533

            
534
    /// Revert the editor back to the main screen, containing the document text.
535
1
    fn revert_to_main_screen(&mut self) {
536
1
        self.reset_message();
537
1
        self.alternate_screen = false;
538
1
    }
539

            
540
    /// Process navigation command issued in normal mode, that will
541
    /// resolve in having the cursor be moved around the document.
542
    ///
543
    /// Note: some commands are accumulative (ie: 2j will move the
544
    /// cursor down twice) and some are not (ie: g will move the cursor
545
    /// to the start of the document only once).
546
    /// A buffer is maintained for the accumulative commands, and is purged
547
    /// when the last char of the command is received. For now, only commans
548
    /// of the form <number>*<char> are supported and I'm not sure I'm
549
    /// planning to support anything more complex than that.
550
87
    fn process_normal_command(&mut self, key: Key) {
551
87
        if key == Key::Esc {
552
1
            self.reset_message();
553
1
            self.reset_search();
554
        }
555
87
        if let Key::Char(c) = key {
556
86
            match c {
557
                '0' => {
558
2
                    if self.normal_command_buffer.is_empty() {
559
1
                        self.goto_start_or_end_of_line(&Boundary::Start);
560
                    } else {
561
1
                        self.normal_command_buffer.push(c.to_string());
562
                    }
563
                }
564
4
                '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' => {
565
4
                    self.normal_command_buffer.push(c.to_string());
566
                }
567
8
                'i' => self.enter_insert_mode(),
568
20
                ':' => self.start_receiving_command(),
569
1
                '/' => self.start_receiving_search_pattern(),
570
3
                'G' => self.goto_start_or_end_of_document(&Boundary::End),
571
1
                'g' => self.goto_start_or_end_of_document(&Boundary::Start),
572
3
                '$' => self.goto_start_or_end_of_line(&Boundary::End),
573
2
                '^' => self.goto_first_non_whitespace(),
574
1
                'H' => self.goto_first_line_of_terminal(),
575
1
                'M' => self.goto_middle_of_terminal(),
576
1
                'L' => self.goto_last_line_of_terminal(),
577
1
                'm' => self.goto_matching_closing_symbol(),
578
3
                'n' => self.goto_next_search_match(),
579
1
                'N' => self.goto_previous_search_match(),
580
1
                'q' => self.revert_to_main_screen(),
581
4
                'd' => self.delete_current_line(),
582
1
                'x' => self.delete_current_grapheme(),
583
3
                'o' => self.insert_newline_after_current_line(),
584
1
                'O' => self.insert_newline_before_current_line(),
585
3
                'A' => self.append_to_line(),
586
1
                'J' => self.join_current_line_with_next_one(),
587
3
                'u' => self.undo_last_operation(),
588
                _ => {
589
                    // at that point, we've iterated over all non accumulative commands
590
                    // meaning the command we're processing is an accumulative one.
591
                    // we thus pop the repeater value from self.normal_command_buffer
592
                    // and we use that value as the number of times the comamnd identified
593
                    // by the `c` char must be repeated.
594
17
                    let times = self.pop_normal_command_repetitions();
595
17
                    self.process_normal_command_n_times(c, times);
596
                }
597
            }
598
        };
599
87
    }
600

            
601
    /// Execute the provided normal movement command n timess
602
17
    fn process_normal_command_n_times(&mut self, c: char, n: usize) {
603
17
        match c {
604
2
            'b' => self.goto_start_or_end_of_word(&Boundary::Start, n),
605
3
            'w' => self.goto_start_or_end_of_word(&Boundary::End, n),
606
1
            'h' => self.move_cursor(&Direction::Left, n),
607
5
            'j' => self.move_cursor(&Direction::Down, n),
608
2
            'k' => self.move_cursor(&Direction::Up, n),
609
1
            'l' => self.move_cursor(&Direction::Right, n),
610
1
            '}' => self.goto_start_or_end_of_paragraph(&Boundary::End, n),
611
1
            '{' => self.goto_start_or_end_of_paragraph(&Boundary::Start, n),
612
1
            '%' => self.goto_percentage_in_document(n),
613
            _ => (),
614
        }
615
17
    }
616

            
617
    /// Process a command issued when the editor is in normal mode
618
48
    fn process_insert_command(&mut self, pressed_key: Key) {
619
48
        match pressed_key {
620
10
            Key::Esc => {
621
10
                self.enter_normal_mode();
622
                return;
623
            }
624
            Key::Backspace => {
625
                // When Backspace is pressed on the first column of a line, it means that we
626
                // should append the current line with the previous one
627
5
                if self.cursor_position.x == 0 {
628
3
                    if self.cursor_position.y > 0 {
629
                        let previous_line_len =
630
3
                            self.get_row(self.previous_row_index()).unwrap().len();
631
                        // Delete newline from previous row
632
3
                        self.history.register_deletion("\n", self.cursor_position);
633
3
                        self.document.delete(0, 0, self.current_row_index());
634
3
                        self.goto_x_y(previous_line_len, self.previous_row_index());
635
                    }
636
                } else {
637
                    // Delete previous character
638
2
                    let previous_grapheme = self.previous_grapheme().to_string();
639
2
                    self.history
640
2
                        .register_deletion(previous_grapheme.as_str(), self.cursor_position);
641
4
                    self.document.delete(
642
2
                        self.current_x_position().saturating_sub(1),
643
2
                        self.current_x_position(),
644
2
                        self.current_row_index(),
645
                    );
646
2
                    self.move_cursor(&Direction::Left, 1);
647
2
                }
648
            }
649
2
            Key::Char('\n') => {
650
2
                self.history.register_insertion("\n", self.cursor_position);
651
4
                self.document
652
2
                    .insert_newline(self.current_x_position(), self.current_row_index());
653
2
                self.goto_x_y(0, self.next_row_index());
654
            }
655
            Key::Char('\t') => {
656
5
                for _ in 0..SPACES_PER_TAB {
657
4
                    self.history.register_insertion(" ", self.cursor_position);
658
8
                    self.document
659
4
                        .insert(' ', self.current_x_position(), self.current_row_index());
660
4
                    self.move_cursor(&Direction::Right, 1);
661
                }
662
            }
663
30
            Key::Char(c) => {
664
60
                self.history
665
30
                    .register_insertion(c.to_string().as_str(), self.cursor_position);
666
60
                self.document
667
30
                    .insert(c, self.current_x_position(), self.current_row_index());
668
30
                self.move_cursor(&Direction::Right, 1);
669
            }
670
            _ => (),
671
        }
672
38
        self.unsaved_edits = self.unsaved_edits.saturating_add(1);
673
38
        if self.unsaved_edits >= SWAP_SAVE_EVERY {
674
            self.save_to_swap_file();
675
        }
676
48
    }
677

            
678
    /// Return the row located at the provide row index if it exists
679
90
    fn get_row(&self, index: RowIndex) -> Option<&Row> {
680
90
        self.document.get_row(index)
681
90
    }
682

            
683
    /// Return the Row object associated to the current cursor position / vertical offset
684
87
    fn current_row(&self) -> &Row {
685
87
        self.get_row(self.current_row_index()).unwrap()
686
87
    }
687

            
688
    /// Return the index of the row associated to the current cursor position / vertical offset
689
167
    fn current_row_index(&self) -> RowIndex {
690
167
        RowIndex::new(self.cursor_position.y.saturating_add(self.offset.rows))
691
167
    }
692

            
693
    /// Return the ``RowIndex`` corresponding to the previous row, with respect to the current one
694
6
    fn previous_row_index(&self) -> RowIndex {
695
6
        self.current_row_index().previous()
696
6
    }
697

            
698
    /// Return the ``RowIndex`` corresponding to the previous row, with respect to the current one
699
6
    fn next_row_index(&self) -> RowIndex {
700
6
        self.current_row_index().next()
701
6
    }
702

            
703
    /// Return the current x position, taking the offset into account
704
55
    fn current_x_position(&self) -> usize {
705
55
        self.cursor_position.x.saturating_add(self.offset.columns)
706
55
    }
707

            
708
    /// Return the character currently under the cursor
709
3
    fn current_grapheme(&self) -> &str {
710
3
        self.current_row().nth_grapheme(self.current_x_position())
711
3
    }
712

            
713
    /// Return the character currently at the left of the cursor
714
2
    fn previous_grapheme(&self) -> &str {
715
4
        self.current_row()
716
2
            .nth_grapheme(self.current_x_position().saturating_sub(1))
717
2
    }
718

            
719
    /// Return the line number associated to the current cursor position / vertical offset
720
14
    fn current_line_number(&self) -> LineNumber {
721
14
        LineNumber::from(self.current_row_index())
722
14
    }
723

            
724
    /// Delete the line currently under the cursor
725
4
    fn delete_current_line(&mut self) {
726
4
        let current_row_str = format!("{}\n", self.current_row().reversed());
727
4
        self.history.register_deletion(
728
4
            &current_row_str,
729
4
            Position {
730
                x: 0,
731
4
                y: self.cursor_position.y,
732
            },
733
        );
734
4
        self.document.delete_row(self.current_row_index());
735

            
736
        // if we just deleted the last line in the document, move one line up
737
4
        if self.cursor_position.y > self.document.num_rows().saturating_sub(1) {
738
2
            self.goto_line(
739
2
                LineNumber::new(self.document.num_rows()),
740
                // Move to the same x index if the previous line is longer/as long as the current one.
741
                // If not, move to the last character.
742
2
                cmp::min(
743
2
                    self.cursor_position.x,
744
6
                    self.document
745
2
                        .get_row(RowIndex::new(self.cursor_position.y.saturating_sub(1)))
746
2
                        .unwrap_or(&Row::from(""))
747
                        .string
748
                        .graphemes(true)
749
                        .count(),
750
                ),
751
2
            );
752
        } else {
753
2
            self.cursor_position.reset_x();
754
        }
755
4
    }
756

            
757
    /// Delete the grapheme currently under the cursor
758
1
    fn delete_current_grapheme(&mut self) {
759
1
        let current_grapheme = self.current_grapheme().to_string();
760
1
        self.history
761
1
            .register_deletion(&current_grapheme, self.cursor_position);
762
2
        self.document.delete(
763
1
            self.current_x_position(),
764
1
            self.current_x_position(),
765
1
            self.current_row_index(),
766
        );
767
1
    }
768

            
769
    /// Insert a newline after the current one, move cursor to it in insert mode
770
3
    fn insert_newline_after_current_line(&mut self) {
771
3
        let eol_position = Position {
772
3
            x: self.current_row().len(),
773
3
            y: self.current_row_index().value,
774
        };
775
3
        self.document
776
3
            .insert_newline(eol_position.x, RowIndex::new(eol_position.y));
777
3
        self.history.register_insertion("\n", eol_position);
778
3
        self.goto_x_y(0, self.next_row_index());
779
3
        self.enter_insert_mode();
780
3
    }
781

            
782
    /// Insert a newline before the current one, move cursor to it in insert mode
783
1
    fn insert_newline_before_current_line(&mut self) {
784
1
        let sol_position = Position {
785
            x: 0,
786
1
            y: self.current_row_index().value,
787
        };
788
1
        self.document
789
1
            .insert_newline(sol_position.x, RowIndex::new(sol_position.y));
790
1
        self.history.register_insertion("\n", sol_position);
791
1
        self.goto_x_y(0, self.current_row_index());
792
1
        self.enter_insert_mode();
793
1
    }
794

            
795
    /// Move to the end of the line, and switch to Insert mode
796
3
    fn append_to_line(&mut self) {
797
3
        self.enter_insert_mode();
798
3
        self.goto_start_or_end_of_line(&Boundary::End);
799
3
        self.move_cursor(&Direction::Right, 1);
800
3
    }
801

            
802
    /// Join the current line with the next one, using a space as a separator
803
1
    fn join_current_line_with_next_one(&mut self) {
804
1
        if self.current_row_index().value < self.document.num_rows() {
805
            // let next_line_row_index = self.cursor_position.y.saturating_add(1);
806
2
            self.document.join_row_with_previous_one(
807
2
                self.current_row().len().saturating_sub(1), // discard the \n
808
1
                self.next_row_index(),
809
1
                Some(' '),
810
            );
811
1
            self.goto_start_or_end_of_line(&Boundary::End);
812
        }
813
1
    }
814

            
815
    /// Move the cursor to the next line after the current paraghraph, or the line
816
    /// before the current paragraph.
817
2
    fn goto_start_or_end_of_paragraph(&mut self, boundary: &Boundary, times: usize) {
818
4
        for _ in 0..times {
819
2
            let next_line_number = Navigator::find_line_number_of_start_or_end_of_paragraph(
820
2
                &self.document,
821
4
                self.current_line_number(),
822
                boundary,
823
            );
824
2
            self.goto_line(next_line_number, 0);
825
        }
826
2
    }
827

            
828
    /// Move the cursor either to the first or last line of the document
829
4
    fn goto_start_or_end_of_document(&mut self, boundary: &Boundary) {
830
4
        match boundary {
831
1
            Boundary::Start => self.goto_line(LineNumber::new(1), 0),
832
3
            Boundary::End => self.goto_line(self.document.last_line_number(), 0),
833
        }
834
4
    }
835

            
836
    /// Move the cursor either to the start or end of the line
837
8
    fn goto_start_or_end_of_line(&mut self, boundary: &Boundary) {
838
8
        match boundary {
839
1
            Boundary::Start => self.move_cursor_to_position_x(0),
840
            Boundary::End => {
841
7
                self.move_cursor_to_position_x(self.current_row().len().saturating_sub(1));
842
            }
843
        }
844
8
    }
845

            
846
    /// Move to the start of the next word or previous one.
847
5
    fn goto_start_or_end_of_word(&mut self, boundary: &Boundary, times: usize) {
848
12
        for _ in 0..times {
849
7
            let x = Navigator::find_index_of_next_or_previous_word(
850
7
                self.current_row(),
851
7
                self.current_x_position(),
852
                boundary,
853
            );
854
7
            self.move_cursor_to_position_x(x);
855
        }
856
5
    }
857

            
858
    /// Move the cursor to the first non whitespace character in the line
859
2
    fn goto_first_non_whitespace(&mut self) {
860
2
        if let Some(x) = Navigator::find_index_of_first_non_whitespace(self.current_row()) {
861
2
            self.move_cursor_to_position_x(x);
862
        }
863
2
    }
864

            
865
    /// Move the cursor to the middle of the terminal
866
1
    fn goto_middle_of_terminal(&mut self) {
867
1
        self.goto_line(
868
1
            self.terminal
869
                .middle_of_screen_line_number()
870
1
                .add(self.offset.rows),
871
            0,
872
        );
873
1
    }
874

            
875
    /// Move the cursor to the middle of the terminal
876
1
    fn goto_first_line_of_terminal(&mut self) {
877
1
        self.goto_line(LineNumber::new(self.offset.rows.saturating_add(1)), 0);
878
1
    }
879

            
880
    /// Move the cursor to the last line of the terminal
881
1
    fn goto_last_line_of_terminal(&mut self) {
882
1
        self.goto_line(
883
1
            self.terminal
884
                .bottom_of_screen_line_number()
885
1
                .add(self.offset.rows),
886
            0,
887
        );
888
1
    }
889

            
890
    /// Move to {n}% in the file
891
1
    fn goto_percentage_in_document(&mut self, percent: usize) {
892
1
        let percent = cmp::min(percent, 100);
893
1
        let line_number = LineNumber::new(self.document.last_line_number().value * percent / 100);
894
1
        self.goto_line(line_number, 0);
895
1
    }
896

            
897
    /// Go to the matching closing symbol (whether that's a quote, curly/square/regular brace, etc).
898
1
    fn goto_matching_closing_symbol(&mut self) {
899
1
        let current_grapheme = self.current_grapheme();
900
        match current_grapheme {
901
1
            "\"" | "'" | "{" | "<" | "(" | "[" => {
902
1
                if let Some(position) = Navigator::find_matching_closing_symbol(
903
1
                    &self.document,
904
1
                    &self.cursor_position,
905
1
                    &self.offset,
906
                ) {
907
1
                    self.goto_position(position);
908
                }
909
            }
910
            "}" | ">" | ")" | "]" => {
911
                if let Some(position) = Navigator::find_matching_opening_symbol(
912
                    &self.document,
913
                    &self.cursor_position,
914
                    &self.offset,
915
                ) {
916
                    self.goto_position(position);
917
                }
918
            }
919
            _ => (),
920
        };
921
1
    }
922

            
923
    /// Move to the first character of the next search match
924
4
    fn goto_next_search_match(&mut self) {
925
4
        if self.search_matches.is_empty() {
926
            return;
927
        }
928
6
        if self.current_search_match_index == self.search_matches.len().saturating_sub(1) {
929
2
            self.current_search_match_index = 0;
930
        } else {
931
2
            self.current_search_match_index = self.current_search_match_index.saturating_add(1);
932
        }
933
8
        self.display_message(format!(
934
            "Match {}/{}",
935
4
            self.current_search_match_index.saturating_add(1),
936
4
            self.search_matches.len()
937
        ));
938
4
        if let Some(search_match) = self.search_matches.get(self.current_search_match_index) {
939
4
            let x_position = search_match.0.x;
940
4
            let line_number = LineNumber::new(search_match.0.y);
941
4
            self.goto_line(line_number, x_position);
942
        }
943
4
    }
944

            
945
    /// Move to the first character of the previous search match
946
1
    fn goto_previous_search_match(&mut self) {
947
1
        if self.search_matches.is_empty() {
948
            return;
949
        }
950
2
        if self.current_search_match_index == 0 {
951
1
            self.current_search_match_index = self.search_matches.len().saturating_sub(1);
952
        } else {
953
            self.current_search_match_index = self.current_search_match_index.saturating_sub(1);
954
        }
955
2
        self.display_message(format!(
956
            "Match {}/{}",
957
1
            self.current_search_match_index.saturating_add(1),
958
1
            self.search_matches.len()
959
        ));
960
1
        if let Some(search_match) = self.search_matches.get(self.current_search_match_index) {
961
1
            let line_number = LineNumber::new(search_match.0.y);
962
1
            let x_position = search_match.0.x;
963
1
            self.goto_line(line_number, x_position);
964
        }
965
1
    }
966

            
967
    /// Move the cursor to the nth line in the file and adjust the viewport
968
19
    fn goto_line(&mut self, line_number: LineNumber, x_position: usize) {
969
19
        self.goto_x_y(x_position, RowIndex::from(line_number));
970
19
    }
971

            
972
4
    fn goto_position(&mut self, pos: Position) {
973
4
        self.goto_x_y(pos.x, RowIndex::new(pos.y));
974
4
    }
975

            
976
    /// Move the cursor to the first column of the nth line
977
37
    fn goto_x_y(&mut self, x: usize, y: RowIndex) {
978
37
        self.move_cursor_to_position_x(x);
979
37
        self.move_cursor_to_position_y(y);
980
37
    }
981

            
982
    /// Move the cursor up/down/left/right by adjusting its x/y position
983
48
    fn move_cursor(&mut self, direction: &Direction, times: usize) {
984
48
        let size = self.terminal.size();
985
48
        let term_height = size.height.saturating_sub(1) as usize;
986
48
        let term_width = size.width.saturating_sub(1) as usize;
987
48
        let Position { mut x, mut y } = self.cursor_position;
988

            
989
        let ViewportOffset {
990
48
            columns: mut offset_x,
991
48
            rows: mut offset_y,
992
        } = self.offset;
993

            
994
97
        for _ in 0..times {
995
49
            match direction {
996
                Direction::Up => {
997
4
                    if y == 0 {
998
                        // we reached the top of the terminal so adjust offset instead
999
                        offset_y = offset_y.saturating_sub(1);
                    } else {
2
                        y = y.saturating_sub(1);
                    }
                } // cannot be < 0
6
                Direction::Down => {
6
                    if y.saturating_add(offset_y) < self.document.last_line_number().sub(1).value {
                        // don't scroll past the last line in the document
12
                        if y < term_height {
                            // don't scroll past the confine the of terminal itself
6
                            y = y.saturating_add(1);
                        } else {
                            // increase offset to that scrolling adjusts the viewport
                            offset_y = offset_y.saturating_add(1);
                        }
                    }
                }
3
                Direction::Left => {
6
                    if x >= term_width {
                        offset_x = offset_x.saturating_sub(1);
                    } else {
3
                        x = x.saturating_sub(1);
                    }
                }
                Direction::Right => {
38
                    if x.saturating_add(offset_x) <= self.current_row().len().saturating_sub(1) {
76
                        if x < term_width {
38
                            x = x.saturating_add(1);
                        } else {
                            offset_x = offset_x.saturating_add(1);
                        }
                    }
                }
            }
        }
48
        self.cursor_position.y = y;
48
        self.offset.columns = offset_x;
48
        self.offset.rows = offset_y;

            
        // if we move from a line to another in normal mode, and the previous x position
        // would cause teh cursor to be placed outside of the destination line x boundary,
        // we make sure to place the cursor on the last character of the line.
57
        if self.mode == Mode::Normal {
9
            self.cursor_position.x = cmp::min(self.current_row().len().saturating_sub(1), x);
        } else {
39
            self.cursor_position.x = x;
        }
48
    }

            
    /// Move the cursor to the given y ``RowIndex``.
    ///
    /// Depending on the both the current and destination cursor position, multiple scenarii can unfold:
    /// * we move the cursor in the same view by simply changing the cursor y position
    /// * we move the cursor to a different view, by changing both the cursor y position and the offset, and
    ///   positioning the cursor at the middle of that new view
    /// * we move the cursor to the first view in the document, by changing the cursor y position and
    ///   setting the y offset to 0
    /// * we move the cursor to the last view in the document, by changing the cursor y position and
    ///   setting the y offset to (``len_doc - view_height``)
45
    fn move_cursor_to_position_y(&mut self, y: RowIndex) {
45
        let max_line_number = self.document.last_line_number(); // last line number in the document
45
        let term_height = self.terminal.bottom_of_screen_line_number().value;
45
        let middle_of_screen_line_number = self.terminal.middle_of_screen_line_number(); // number of the line in the middle of the terminal

            
45
        let y = cmp::max(0, y.value);
45
        let y = cmp::min(y, RowIndex::from(max_line_number).value);

            
84
        if self.offset.rows <= y && y <= self.offset.rows + term_height {
            // move around in the same view
39
            self.cursor_position.y = y.saturating_sub(self.offset.rows);
6
        } else if y < RowIndex::from(middle_of_screen_line_number).value {
            // move to the first "half-view" of the document
1
            self.offset.rows = 0;
1
            self.cursor_position.y = y;
10
        } else if y >= RowIndex::from(max_line_number.sub(middle_of_screen_line_number.value)).value
        {
            // move to the last "half view" of the document
2
            self.offset.rows = max_line_number.sub(term_height).value;
2
            self.cursor_position.y = RowIndex::new(y.saturating_sub(self.offset.rows)).value;
        } else {
            // move to another view in the document, and position the cursor at the
            // middle of the terminal/view.
3
            self.offset.rows =
3
                RowIndex::new(y - RowIndex::from(middle_of_screen_line_number).value).value;
3
            self.cursor_position.y = RowIndex::from(middle_of_screen_line_number).value;
        }
45
    }

            
    /// Move the cursor to the associated x non-negative position, adjusting the x offset
    /// if that takes the cursor out of the current view.
56
    fn move_cursor_to_position_x(&mut self, x: usize) {
56
        let term_width = self.terminal.size().width as usize;
56
        let x = cmp::max(0, x);
57
        if x > term_width {
1
            self.cursor_position.x = term_width.saturating_sub(1);
1
            self.offset.columns = x
                .saturating_sub(term_width)
1
                .saturating_sub(self.offset.columns)
                .saturating_add(1);
        } else {
55
            self.cursor_position.x = x;
55
            self.offset.columns = 0;
        }
56
    }

            
    /// Return whether the document has seen some edits since the last save
10
    fn is_dirty(&self) -> bool {
10
        self.last_saved_hash != self.document.hashed()
10
    }

            
    /// Undo the last registered operation in history
3
    fn undo_last_operation(&mut self) {
6
        if let Some(last_op_undone) = self
            .history
9
            .last_operation_reversed(&self.document.row_lengths())
        {
3
            match last_op_undone.op_type {
2
                OperationType::Delete => self.document.delete_string(
1
                    last_op_undone.content.as_str(),
1
                    last_op_undone.start_position.x,
1
                    RowIndex::new(last_op_undone.start_position.y),
                ),
4
                OperationType::Insert => self.document.insert_string(
4
                    last_op_undone.content.as_str(),
2
                    last_op_undone.start_position.x,
2
                    RowIndex::new(last_op_undone.start_position.y),
                ),
            }
3
            self.goto_position(last_op_undone.end_position(&self.document.row_lengths()));
3
        }
3
    }

            
    /// Return the x index of the first character of the currently selected autocompletion suggestion
    fn get_x_index_of_currently_selected_suggestion(&self) -> usize {
        let mut x_index_of_currently_selected_suggestion = SEARCH_PREFIX.to_string().len();
        let sep_len = AUTOCOMPLETION_SUGGESTIONS_SEPARATOR.to_string().len();
        for (i, suggestion) in self.command_suggestions.iter().enumerate() {
            if i >= self.current_autocompletion_index {
                break;
            }
            x_index_of_currently_selected_suggestion += suggestion.len() + sep_len;
        }
        x_index_of_currently_selected_suggestion
    }

            
    /// Refresh the screen by displaying all rows and bars
    fn refresh_screen(&mut self) -> Result<(), std::io::Error> {
        self.terminal.hide_cursor();
        if !self.should_quit {
            if self.alternate_screen {
                self.terminal.clear_all();
                self.terminal.to_alternate_screen();
                self.draw_help_screen();
            } else {
                self.terminal.to_main_screen();
                self.draw_rows();
            }
            self.draw_status_bar();
            self.draw_message_bar();
            if self.alternate_screen {
                self.terminal.set_cursor_position_in_text_area(
                    &Position::top_left(),
                    self.row_prefix_length,
                );
            } else if self.is_receiving_command() {
                if self.is_autocompleting_command() {
                    // if we're currently auto-completing the user-provided command,
                    // we move the cursor on the first character of the currently selected
                    // suggestion.
                    self.terminal.set_cursor_position_anywhere(&Position {
                        x: self.get_x_index_of_currently_selected_suggestion(),
                        y: self.terminal.size().height as usize,
                    });
                } else {
                    // if a command is being typed, put the cursor in the bottom bar
                    self.terminal.set_cursor_position_anywhere(&Position {
                        x: self.command_buffer.len(),
                        y: self.terminal.size().height as usize,
                    });
                }
            } else {
                self.terminal.set_cursor_position_in_text_area(
                    &self.cursor_position,
                    self.row_prefix_length,
                );
            }
        }
        self.terminal.show_cursor();
        self.terminal.flush()
    }

            
    /// Generate the content of the status bar
5
    fn generate_status(&self) -> String {
5
        let dirty_marker = if self.is_dirty() { " +" } else { "" };
5
        let left_status = format!(
            "[{}]{} {}",
10
            self.document
                .filename
                .as_ref()
5
                .unwrap_or(&PathBuf::from("No Name"))
                .to_str()
                .unwrap_or_default(),
            dirty_marker,
            self.mode
        );
6
        let stats = if self.config.display_stats {
2
            format!(
                "[{}L/{}W]",
1
                self.document.last_line_number().value,
1
                self.document.num_words()
            )
        } else {
4
            "".to_string()
        };
10
        let position = format!(
            "Ln {}, Col {}",
5
            self.current_line_number().value,
10
            self.cursor_position
                .x
5
                .saturating_add(self.offset.columns)
                .saturating_add(1),
        );
5
        let right_status = format!("{} {}", stats, position);
5
        let right_status = right_status.trim_start();
5
        let spaces = " ".repeat(
10
            (self.terminal.size().width as usize)
5
                .saturating_sub(left_status.len())
5
                .saturating_sub(right_status.len()),
        );
5
        format!("{}{}{}\r", left_status, spaces, right_status)
5
    }

            
    /// Display the content of the status bar to the screen
    fn draw_status_bar(&self) {
        self.terminal.set_bg_color(STATUS_BG_COLOR);
        self.terminal.set_fg_color(STATUS_FG_COLOR);
        println!("{}", self.generate_status());
        self.terminal.reset_fg_color();
        self.terminal.reset_bg_color();
    }

            
    /// Display the content of the message bar to the screen
    fn draw_message_bar(&self) {
        self.terminal.clear_current_line();
        if self.is_receiving_command() {
            if self.is_autocompleting_command() {
                print!(":{}\r", self.generate_command_autocompletion_message());
            } else {
                print!("{}\r", self.command_buffer);
            }
        } else {
            print!("{}\r", self.message);
        }
    }

            
    /// Generate the command autocompletion message, that will be displayed in the message bar
    fn generate_command_autocompletion_message(&self) -> String {
        let mut tokens: Vec<String> = vec![];
        for (i, suggestion) in self.command_suggestions.iter().enumerate() {
            if i == self.current_autocompletion_index {
                tokens.push(utils::red(utils::as_bold(suggestion).as_str()));
            } else {
                tokens.push(suggestion.to_string());
            }
        }
        tokens.join(AUTOCOMPLETION_SUGGESTIONS_SEPARATOR.to_string().as_str())
    }

            
    /// Make sure the provided message gets displayed in the message bar
13
    fn display_message(&mut self, message: String) {
13
        self.message = message;
13
    }

            
    /// Erase the currently displayed message
3
    fn reset_message(&mut self) {
3
        self.message = String::from("");
3
    }

            
    /// Display a welcome message, when no document has been opened
    fn display_welcome_message(&self) {
        let term_width = self.terminal.size().width as usize;
        let welcome_msg = format!("{} v{}", PKG, utils::bo_version());
        let padding_len = term_width
            .saturating_sub(welcome_msg.chars().count())
            .saturating_sub(2) // -2 because of the starting '~ '
            .saturating_div(2);
        let padding = String::from(" ").repeat(padding_len);
        let mut padded_welcome_message = format!("~ {}{}{}", padding, welcome_msg, padding);
        padded_welcome_message.truncate(term_width); // make it fit on screen
        println!("{}\r", padded_welcome_message);
    }

            
    /// Display the automatically generated help panel on the screen
    #[allow(clippy::cast_possible_truncation)]
    fn draw_help_screen(&mut self) {
        let help_text_lines = self.help_message.split('\n');
        let help_text_lines_count = help_text_lines.count();
        let term_height = self.terminal.size().height;
        let v_padding = (term_height
            .saturating_sub(2)
            .saturating_sub(help_text_lines_count as u16))
        .saturating_div(2);
        let max_line_length = self.help_message.split('\n').map(str::len).max().unwrap();
        let h_padding = " ".repeat((self.terminal.size().width as usize - max_line_length) / 2);
        for _ in 0..=v_padding {
            println!("\r");
        }
        for line in self.help_message.split('\n') {
            println!("{}{}\r", h_padding, line);
        }
        for _ in 0..=v_padding {
            println!("\r");
        }
        if (v_padding + help_text_lines_count as u16 + v_padding) == (term_height - 1) {
            println!("\r");
        }
        self.display_message("Press q to quit".to_string());
    }

            
    /// Iterate over each visible document rows and display it on the screen.
    /// If no document is currently opened, display the welcome message.
    /// If the document is shorter than the viewport height, display empty lines as ``~``.
    fn draw_rows(&self) {
        let term_height = self.terminal.size().restrict_to_text_area().height;
        for terminal_row_idx_val in self.offset.rows..(term_height as usize + self.offset.rows) {
            let terminal_row_idx = RowIndex::new(terminal_row_idx_val);
            let line_number = LineNumber::from(terminal_row_idx);
            self.terminal.clear_current_line();
            if let Some(row) = self.get_row(terminal_row_idx) {
                self.draw_row(row, line_number);
            } else if line_number == self.terminal.middle_of_screen_line_number()
                && self.document.filename.is_none()
                && self
                    .get_row(RowIndex::new(0))
                    .unwrap_or(&Row::default())
                    .is_empty()
            {
                self.display_welcome_message();
            } else {
                println!("~\r");
            }
        }
    }

            
    /// Display the content of a particular document row to the screen
    fn draw_row(&self, row: &Row, line_number: LineNumber) {
        let row_visible_start = self.offset.columns;
        let mut row_visible_end = self.terminal.size().width as usize + self.offset.columns;
        if self.row_prefix_length > 0 {
            row_visible_end = row_visible_end
                .saturating_sub(self.row_prefix_length as usize)
                .saturating_sub(1);
        }
        let rendered_row = row.render(
            row_visible_start,
            row_visible_end,
            line_number.value,
            self.row_prefix_length as usize,
        );
        println!("{}\r", rendered_row);
    }
}

            
#[cfg(test)]
#[path = "./editor_test.rs"]
mod editor_test;