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
277
#[derive(Debug, Default, PartialEq, Clone, Copy, Serialize)]
27
pub struct Position {
28
314
    pub x: usize,
29
402
    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
48
    pub fn top_left() -> Self {
38
48
        Self::default()
39
48
    }
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
97
#[derive(Debug, Default, Serialize)]
52
pub struct ViewportOffset {
53
48
    pub rows: usize,
54
48
    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
48
    pub fn new(filename: Option<String>, terminal: Box<dyn Console>) -> Self {
126
48
        let document: Document = match filename {
127
45
            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
48
        let last_saved_hash = document.hashed();
133
48
        let help_message = Help::default().format();
134
48
        Self {
135
            should_quit: false,
136
48
            cursor_position: Position::top_left(),
137
48
            document,
138
48
            offset: ViewportOffset::default(),
139
48
            message: "".to_string(),
140
48
            mode: Mode::Normal,
141
48
            command_buffer: "".to_string(),
142
48
            command_suggestions: vec![],
143
            current_autocompletion_index: 0,
144
48
            config: Config::default(),
145
48
            normal_command_buffer: vec![],
146
48
            mouse_event_buffer: vec![],
147
48
            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
48
            help_message,
155
48
            history: History::default(),
156
        }
157
48
    }
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
251
    fn process_keystroke(&mut self, pressed_key: Key) {
190
251
        if self.is_receiving_command() {
191
114
            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
110
                match pressed_key {
211
1
                    Key::Esc => self.stop_receiving_command(),
212
18
                    Key::Char('\n') => {
213
                        // Enter
214
18
                        self.process_received_command();
215
18
                        self.stop_receiving_command();
216
                    }
217
3
                    Key::Char('\t') => self.autocomplete_command(),
218
87
                    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
137
            match self.mode {
227
89
                Mode::Normal => self.process_normal_command(pressed_key),
228
48
                Mode::Insert => self.process_insert_command(pressed_key),
229
            }
230
        }
231
251
    }
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
23
    fn start_receiving_command(&mut self) {
273
23
        self.command_buffer.push(COMMAND_PREFIX);
274
23
    }
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
20
    fn stop_receiving_command(&mut self) {
283
20
        self.command_buffer = "".to_string();
284
20
    }
285

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

            
291
    /// Return whether the Editor is currently autosuggesting command based on the user input
292
122
    fn is_autocompleting_command(&self) -> bool {
293
122
        self.is_receiving_command() && !self.command_suggestions.is_empty()
294
122
    }
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
18
    fn process_received_command(&mut self) {
318
18
        let command = self.command_buffer.clone();
319
18
        match self.command_buffer.chars().next().unwrap() {
320
            SEARCH_PREFIX => {
321
1
                self.process_search_command(command.strip_prefix(SEARCH_PREFIX).unwrap());
322
            }
323
17
            COMMAND_PREFIX => {
324
17
                let command = command.strip_prefix(COMMAND_PREFIX).unwrap_or_default();
325
17
                if command.is_empty() {
326
17
                } 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
15
                } else if command.split(' ').count() > 1 {
331
4
                    let cmd_tokens: Vec<&str> = command.split(' ').collect();
332
4
                    match *cmd_tokens.get(0).unwrap_or(&"") {
333
4
                        commands::OPEN | commands::OPEN_SHORT => {
334
3
                            let filename = PathBuf::from(cmd_tokens[1]);
335
5
                            if self.document.filename == Some(filename.clone()) {
336
1
                                self.display_message(format!(
337
                                    "{} is already opened",
338
2
                                    cmd_tokens[1]
339
                                ));
340
2
                            } else if let Ok(document) = Document::open(filename) {
341
2
                                self.document = document;
342
2
                                self.last_saved_hash = self.document.hashed();
343
2
                                self.reset_message();
344
2
                                self.cursor_position = Position::default();
345
2
                                self.history = History::default();
346
                            } else {
347
                                self.display_message(utils::red(&format!(
348
                                    "{} not found",
349
                                    cmd_tokens[1]
350
                                )));
351
2
                            }
352
3
                        }
353
1
                        commands::NEW => {
354
1
                            self.document =
355
1
                                Document::new_empty(PathBuf::from(cmd_tokens[1].to_string()));
356
1
                            self.enter_insert_mode();
357
                        }
358
                        commands::SAVE => {
359
                            let new_name = cmd_tokens[1..].join(" ");
360
                            self.save(new_name.trim());
361
                        }
362
                        _ => self.display_message(utils::red(&format!(
363
                            "Unknown command '{}'",
364
                            cmd_tokens[0]
365
                        ))),
366
                    }
367
4
                } else {
368
                    match command {
369
11
                        commands::FORCE_QUIT => self.quit(true),
370
11
                        commands::QUIT => self.quit(false),
371
13
                        commands::LINE_NUMBERS => {
372
2
                            self.config.display_line_numbers =
373
2
                                Config::toggle(self.config.display_line_numbers);
374
2
                            self.row_prefix_length = if self.config.display_line_numbers {
375
1
                                START_X
376
                            } else {
377
1
                                0
378
                            };
379
                        }
380
9
                        commands::STATS => {
381
2
                            self.config.display_stats = Config::toggle(self.config.display_stats);
382
                        }
383
7
                        commands::HELP => {
384
1
                            self.alternate_screen = true;
385
                        }
386
6
                        commands::SAVE => self.save(""),
387
3
                        commands::SAVE_AND_QUIT => {
388
1
                            self.save("");
389
1
                            self.quit(false);
390
                        }
391
2
                        commands::DEBUG => {
392
                            if let Ok(state) = serde_json::to_string_pretty(&self) {
393
                                utils::log(state.as_str());
394
                            }
395
                        }
396
4
                        _ => self
397
6
                            .display_message(utils::red(&format!("Unknown command '{}'", command))),
398
                    }
399
                }
400
            }
401
            _ => (),
402
        }
403
18
    }
404

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

            
429
    /// Cycle through the possible command suggestions by incrementing (or resetting)
430
    /// the ``current_autocompletion_index`` value.
431
2
    fn cycle_through_command_suggestions(&mut self) {
432
4
        let next_suggestion_index = if self.current_autocompletion_index
433
4
            == self.command_suggestions.len().saturating_sub(1)
434
        {
435
1
            0
436
        } else {
437
1
            self.current_autocompletion_index.saturating_add(1)
438
        };
439
2
        self.current_autocompletion_index = next_suggestion_index;
440
2
    }
441

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

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

            
489
    /// Save all current unsaved edits to a swap file, allowing a seamless
490
    /// recovery in case of a crash.
491
    fn save_to_swap_file(&mut self) {
492
        if self.document.save_to_swap_file().is_ok() {
493
            self.unsaved_edits = 0;
494
        }
495
    }
496

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

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

            
535
    /// Reset all state related to text search back to the default values.
536
2
    fn reset_search(&mut self) {
537
2
        self.search_matches = vec![]; // erase previous search matches
538
2
        self.current_search_match_index = 0;
539
2
    }
540

            
541
    /// Revert the editor back to the main screen, containing the document text.
542
1
    fn revert_to_main_screen(&mut self) {
543
1
        self.reset_message();
544
1
        self.alternate_screen = false;
545
1
    }
546

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

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

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

            
685
    /// Return the row located at the provide row index if it exists
686
90
    fn get_row(&self, index: RowIndex) -> Option<&Row> {
687
90
        self.document.get_row(index)
688
90
    }
689

            
690
    /// Return the Row object associated to the current cursor position / vertical offset
691
87
    fn current_row(&self) -> &Row {
692
87
        self.get_row(self.current_row_index()).unwrap()
693
87
    }
694

            
695
    /// Return the index of the row associated to the current cursor position / vertical offset
696
167
    fn current_row_index(&self) -> RowIndex {
697
167
        RowIndex::new(self.cursor_position.y.saturating_add(self.offset.rows))
698
167
    }
699

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

            
705
    /// Return the ``RowIndex`` corresponding to the previous row, with respect to the current one
706
6
    fn next_row_index(&self) -> RowIndex {
707
6
        self.current_row_index().next()
708
6
    }
709

            
710
    /// Return the current x position, taking the offset into account
711
55
    fn current_x_position(&self) -> usize {
712
55
        self.cursor_position.x.saturating_add(self.offset.columns)
713
55
    }
714

            
715
    /// Return the character currently under the cursor
716
3
    fn current_grapheme(&self) -> &str {
717
3
        self.current_row().nth_grapheme(self.current_x_position())
718
3
    }
719

            
720
    /// Return the character currently at the left of the cursor
721
2
    fn previous_grapheme(&self) -> &str {
722
4
        self.current_row()
723
2
            .nth_grapheme(self.current_x_position().saturating_sub(1))
724
2
    }
725

            
726
    /// Return the line number associated to the current cursor position / vertical offset
727
14
    fn current_line_number(&self) -> LineNumber {
728
14
        LineNumber::from(self.current_row_index())
729
14
    }
730

            
731
    /// Delete the line currently under the cursor
732
4
    fn delete_current_line(&mut self) {
733
4
        let current_row_str = format!("{}\n", self.current_row().reversed());
734
4
        self.history.register_deletion(
735
4
            &current_row_str,
736
4
            Position {
737
                x: 0,
738
4
                y: self.cursor_position.y,
739
            },
740
        );
741
4
        self.document.delete_row(self.current_row_index());
742

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

            
764
    /// Delete the grapheme currently under the cursor
765
1
    fn delete_current_grapheme(&mut self) {
766
1
        let current_grapheme = self.current_grapheme().to_string();
767
1
        self.history
768
1
            .register_deletion(&current_grapheme, self.cursor_position);
769
2
        self.document.delete(
770
1
            self.current_x_position(),
771
1
            self.current_x_position(),
772
1
            self.current_row_index(),
773
        );
774
1
    }
775

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

            
789
    /// Insert a newline before the current one, move cursor to it in insert mode
790
1
    fn insert_newline_before_current_line(&mut self) {
791
1
        let sol_position = Position {
792
            x: 0,
793
1
            y: self.current_row_index().value,
794
        };
795
1
        self.document
796
1
            .insert_newline(sol_position.x, RowIndex::new(sol_position.y));
797
1
        self.history.register_insertion("\n", sol_position);
798
1
        self.goto_x_y(0, self.current_row_index());
799
1
        self.enter_insert_mode();
800
1
    }
801

            
802
    /// Move to the end of the line, and switch to Insert mode
803
3
    fn append_to_line(&mut self) {
804
3
        self.enter_insert_mode();
805
3
        self.goto_start_or_end_of_line(&Boundary::End);
806
3
        self.move_cursor(&Direction::Right, 1);
807
3
    }
808

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

            
822
    /// Move the cursor to the next line after the current paraghraph, or the line
823
    /// before the current paragraph.
824
2
    fn goto_start_or_end_of_paragraph(&mut self, boundary: &Boundary, times: usize) {
825
4
        for _ in 0..times {
826
2
            let next_line_number = Navigator::find_line_number_of_start_or_end_of_paragraph(
827
2
                &self.document,
828
4
                self.current_line_number(),
829
                boundary,
830
            );
831
2
            self.goto_line(next_line_number, 0);
832
        }
833
2
    }
834

            
835
    /// Move the cursor either to the first or last line of the document
836
4
    fn goto_start_or_end_of_document(&mut self, boundary: &Boundary) {
837
4
        match boundary {
838
1
            Boundary::Start => self.goto_line(LineNumber::new(1), 0),
839
3
            Boundary::End => self.goto_line(self.document.last_line_number(), 0),
840
        }
841
4
    }
842

            
843
    /// Move the cursor either to the start or end of the line
844
8
    fn goto_start_or_end_of_line(&mut self, boundary: &Boundary) {
845
8
        match boundary {
846
1
            Boundary::Start => self.move_cursor_to_position_x(0),
847
            Boundary::End => {
848
7
                self.move_cursor_to_position_x(self.current_row().len().saturating_sub(1));
849
            }
850
        }
851
8
    }
852

            
853
    /// Move to the start of the next word or previous one.
854
5
    fn goto_start_or_end_of_word(&mut self, boundary: &Boundary, times: usize) {
855
12
        for _ in 0..times {
856
7
            let x = Navigator::find_index_of_next_or_previous_word(
857
7
                self.current_row(),
858
7
                self.current_x_position(),
859
                boundary,
860
            );
861
7
            self.move_cursor_to_position_x(x);
862
        }
863
5
    }
864

            
865
    /// Move the cursor to the first non whitespace character in the line
866
2
    fn goto_first_non_whitespace(&mut self) {
867
2
        if let Some(x) = Navigator::find_index_of_first_non_whitespace(self.current_row()) {
868
2
            self.move_cursor_to_position_x(x);
869
        }
870
2
    }
871

            
872
    /// Move the cursor to the middle of the terminal
873
1
    fn goto_middle_of_terminal(&mut self) {
874
1
        self.goto_line(
875
1
            self.terminal
876
                .middle_of_screen_line_number()
877
1
                .add(self.offset.rows),
878
            0,
879
        );
880
1
    }
881

            
882
    /// Move the cursor to the middle of the terminal
883
1
    fn goto_first_line_of_terminal(&mut self) {
884
1
        self.goto_line(LineNumber::new(self.offset.rows.saturating_add(1)), 0);
885
1
    }
886

            
887
    /// Move the cursor to the last line of the terminal
888
1
    fn goto_last_line_of_terminal(&mut self) {
889
1
        self.goto_line(
890
1
            self.terminal
891
                .bottom_of_screen_line_number()
892
1
                .add(self.offset.rows),
893
            0,
894
        );
895
1
    }
896

            
897
    /// Move to {n}% in the file
898
1
    fn goto_percentage_in_document(&mut self, percent: usize) {
899
1
        let percent = cmp::min(percent, 100);
900
1
        let line_number = LineNumber::new(self.document.last_line_number().value * percent / 100);
901
1
        self.goto_line(line_number, 0);
902
1
    }
903

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

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

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

            
974
    /// Move the cursor to the nth line in the file and adjust the viewport
975
19
    fn goto_line(&mut self, line_number: LineNumber, x_position: usize) {
976
19
        self.goto_x_y(x_position, RowIndex::from(line_number));
977
19
    }
978

            
979
4
    fn goto_position(&mut self, pos: Position) {
980
4
        self.goto_x_y(pos.x, RowIndex::new(pos.y));
981
4
    }
982

            
983
    /// Move the cursor to the first column of the nth line
984
37
    fn goto_x_y(&mut self, x: usize, y: RowIndex) {
985
37
        self.move_cursor_to_position_x(x);
986
37
        self.move_cursor_to_position_y(y);
987
37
    }
988

            
989
    /// Move the cursor up/down/left/right by adjusting its x/y position
990
48
    fn move_cursor(&mut self, direction: &Direction, times: usize) {
991
48
        let size = self.terminal.size();
992
48
        let term_height = size.height.saturating_sub(1) as usize;
993
48
        let term_width = size.width.saturating_sub(1) as usize;
994
48
        let Position { mut x, mut y } = self.cursor_position;
995

            
996
        let ViewportOffset {
997
48
            columns: mut offset_x,
998
48
            rows: mut offset_y,
999
        } = self.offset;
97
        for _ in 0..times {
49
            match direction {
                Direction::Up => {
4
                    if y == 0 {
                        // we reached the top of the terminal so adjust offset instead
                        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
14
    fn display_message(&mut self, message: String) {
14
        self.message = message;
14
    }
    /// Erase the currently displayed message
4
    fn reset_message(&mut self) {
4
        self.message = String::from("");
4
    }
    /// 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;