• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

ilai-deutel / kibi / 24640035121

19 Apr 2026 08:58PM UTC coverage: 69.538% (-0.02%) from 69.562%
24640035121

push

github

ilai-deutel
Bump MSRV to 1.95; LOC 1009 -> 1007

0 of 6 new or added lines in 2 files covered. (0.0%)

1 existing line in 1 file now uncovered.

888 of 1277 relevant lines covered (69.54%)

1071.88 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

68.47
/src/editor.rs
1
// SPDX-FileCopyrightText: 2020 Ilaï Deutel & Kibi Contributors
2
//
3
// SPDX-License-Identifier: MIT OR Apache-2.0
4

5
use std::fmt::{Display, Write as _};
6
use std::io::{self, BufRead, BufReader, ErrorKind, Read, Seek, Write};
7
use std::iter::{self, repeat, successors as scsr};
8
use std::{fs::File, path::Path, process::Command, time::Instant};
9

10
use crate::row::{HlState, Row};
11
use crate::{Config, Error, ansi_escape::*, syntax::Conf as SyntaxConf, sys, terminal};
12

13
const fn ctrl_key(key: u8) -> u8 { key & 0x1f }
×
14
const EXIT: u8 = ctrl_key(b'Q');
15
const DELETE_BIS: u8 = ctrl_key(b'H');
16
const REFRESH_SCREEN: u8 = ctrl_key(b'L');
17
const SAVE: u8 = ctrl_key(b'S');
18
const FIND: u8 = ctrl_key(b'F');
19
const GOTO: u8 = ctrl_key(b'G');
20
const CUT: u8 = ctrl_key(b'X');
21
const COPY: u8 = ctrl_key(b'C');
22
const PASTE: u8 = ctrl_key(b'V');
23
const DUPLICATE: u8 = ctrl_key(b'D');
24
const EXECUTE: u8 = ctrl_key(b'E');
25
const REMOVE_LINE: u8 = ctrl_key(b'R');
26
const TOGGLE_COMMENT: u8 = 31;
27
const BACKSPACE: u8 = 127;
28

29
const WELCOME_MESSAGE: &str = concat!("Kibi ", env!("CARGO_PKG_VERSION"));
30
const HELP_MESSAGE: &str = "^S save | ^Q quit | ^F find | ^G go to | ^D duplicate | ^E execute | \
31
                            ^C copy | ^X cut | ^V paste | ^/ comment";
32

33
/// `set_status!` sets a formatted status message for the editor.
34
/// Example usage: `set_status!(editor, "{file_size} written to {file_name}")`
35
macro_rules! set_status { ($editor:expr, $($arg:expr),*) => ($editor.status_msg = Some(StatusMessage::new(format!($($arg),*)))) }
36

37
/// Enum of input keys
38
#[cfg_attr(test, derive(Debug, PartialEq))]
39
enum Key {
40
    Arrow(AKey),
41
    CtrlArrow(AKey),
42
    PageUp,
43
    PageDown,
44
    Home,
45
    End,
46
    Delete,
47
    Escape,
48
    Char(u8),
49
}
50

51
/// Enum of arrow keys
52
#[cfg_attr(test, derive(Debug, PartialEq))]
53
enum AKey {
54
    Left,
55
    Right,
56
    Up,
57
    Down,
58
}
59

60
/// Describes the cursor position and the screen offset
61
#[derive(Debug, Default, Clone, PartialEq)]
62
struct CursorState {
63
    /// x position (indexing the characters, not the columns)
64
    x: usize,
65
    /// y position (row number, 0-indexed)
66
    y: usize,
67
    /// Row offset
68
    roff: usize,
69
    /// Column offset
70
    coff: usize,
71
}
72

73
impl CursorState {
74
    const fn move_to_next_line(&mut self) { (self.x, self.y) = (0, self.y + 1); }
1,111✔
75

76
    /// Scroll the terminal window vertically and horizontally (i.e. adjusting
77
    /// the row offset and the column offset) so that the cursor can be
78
    /// shown.
79
    fn scroll(&mut self, rx: usize, screen_rows: usize, screen_cols: usize) {
×
80
        self.roff = self.roff.clamp(self.y.saturating_sub(screen_rows.saturating_sub(1)), self.y);
×
81
        self.coff = self.coff.clamp(rx.saturating_sub(screen_cols.saturating_sub(1)), rx);
×
82
    }
×
83
}
84

85
/// The `Editor` struct, contains the state and configuration of the text
86
/// editor.
87
#[derive(Default)]
88
pub struct Editor {
89
    /// If not `None`, the current prompt mode (`Save`, `Find`, `GoTo`, or
90
    /// `Execute`). If `None`, we are in regular edition mode.
91
    prompt_mode: Option<PromptMode>,
92
    /// The current state of the cursor.
93
    cursor: CursorState,
94
    /// The padding size used on the left for line numbering.
95
    ln_pad: usize,
96
    /// The width of the current window. Will be updated when the window is
97
    /// resized.
98
    window_width: usize,
99
    /// The number of rows that can be used for the editor, excluding the status
100
    /// bar and the message bar
101
    screen_rows: usize,
102
    /// The number of columns that can be used for the editor, excluding the
103
    /// part used for line numbers
104
    screen_cols: usize,
105
    /// The collection of rows, including the content and the syntax
106
    /// highlighting information.
107
    rows: Vec<Row>,
108
    /// Whether the document has been modified since it was open.
109
    dirty: bool,
110
    /// The configuration for the editor.
111
    config: Config,
112
    /// The number of consecutive times the user has tried to quit without
113
    /// saving. After `config.quit_times`, the program will exit.
114
    quit_times: usize,
115
    /// The file name. If None, the user will be prompted for a file name the
116
    /// first time they try to save.
117
    // TODO: It may be better to store a PathBuf instead
118
    file_name: Option<String>,
119
    /// The current status message being shown.
120
    status_msg: Option<StatusMessage>,
121
    /// The syntax configuration corresponding to the current file's extension.
122
    syntax: SyntaxConf,
123
    /// The number of bytes contained in `rows`. This excludes new lines.
124
    n_bytes: u64,
125
    /// The copied buffer of a row
126
    copied_row: Vec<u8>,
127
    /// Whether to use ANSI color escape codes for rendering
128
    use_color: bool,
129
}
130

131
/// Describes a status message, shown at the bottom at the screen.
132
struct StatusMessage {
133
    /// The message to display.
134
    msg: String,
135
    /// The `Instant` the status message was first displayed.
136
    time: Instant,
137
}
138

139
impl StatusMessage {
140
    /// Create a new status message and set time to the current date/time.
141
    fn new(msg: String) -> Self { Self { msg, time: Instant::now() } }
×
142
}
143

144
/// Pretty-format a size in bytes.
145
fn format_size(n: u64) -> String {
187✔
146
    if n < 1024 {
187✔
147
        return format!("{n}B");
33✔
148
    }
154✔
149
    // i is the largest value such that 1024 ^ i < n
150
    let i = n.ilog2() / 10;
154✔
151

152
    // Compute the size with two decimal places (rounded down) as the last two
153
    // digits of q This avoid float formatting reducing the binary size
154
    let q = 100 * n / (1024 << ((i - 1) * 10));
154✔
155
    format!("{}.{:02}{}B", q / 100, q % 100, b" kMGTPEZ"[i as usize] as char)
154✔
156
}
187✔
157

158
/// Return an Arrow Key given an ANSI code.
159
///
160
/// The argument must be a valide arrow key ANSI code (`a`, `b`, `c` or `d`),
161
/// case-insensitive).
162
fn get_akey(c: u8) -> AKey {
66✔
163
    match c {
66✔
164
        b'a' | b'A' => AKey::Up,
11✔
165
        b'b' | b'B' => AKey::Down,
11✔
166
        b'c' | b'C' => AKey::Right,
33✔
167
        b'd' | b'D' => AKey::Left,
11✔
168
        _ => unreachable!("Invalid ANSI code for arrow key {}", c),
×
169
    }
170
}
66✔
171

172
impl Editor {
173
    /// Return the current row if the cursor points to an existing row, `None`
174
    /// otherwise.
175
    fn current_row(&self) -> Option<&Row> { self.rows.get(self.cursor.y) }
1,111✔
176

177
    /// Return the position of the cursor, in terms of rendered characters (as
178
    /// opposed to `self.cursor.x`, which is the position of the cursor in
179
    /// terms of bytes).
180
    fn rx(&self) -> usize { self.current_row().map_or(0, |r| r.cx2rx[self.cursor.x]) }
×
181

182
    /// Move the cursor following an arrow key (← → ↑ ↓).
183
    fn move_cursor(&mut self, key: &AKey, ctrl: bool) {
495✔
184
        match (key, self.current_row()) {
495✔
185
            (AKey::Left, Some(row)) if self.cursor.x > 0 => {
165✔
186
                let mut cursor_x = self.cursor.x - row.get_char_size(row.cx2rx[self.cursor.x] - 1);
132✔
187
                // ← moving to previous word
188
                while ctrl && cursor_x > 0 && row.chars[cursor_x - 1] != b' ' {
539✔
189
                    cursor_x -= row.get_char_size(row.cx2rx[cursor_x] - 1);
407✔
190
                }
407✔
191
                self.cursor.x = cursor_x;
132✔
192
            }
193
            // ← at the beginning of the line: move to the end of the previous line. The x
194
            // position will be adjusted after this `match` to accommodate the current row
195
            // length, so we can just set here to the maximum possible value here.
196
            (AKey::Left, _) if self.cursor.y > 0 =>
44✔
197
                (self.cursor.y, self.cursor.x) = (self.cursor.y - 1, usize::MAX),
33✔
198
            (AKey::Right, Some(row)) if self.cursor.x < row.chars.len() => {
99✔
199
                let mut cursor_x = self.cursor.x + row.get_char_size(row.cx2rx[self.cursor.x]);
55✔
200
                // → moving to next word
201
                while ctrl && cursor_x < row.chars.len() && row.chars[cursor_x] != b' ' {
275✔
202
                    cursor_x += row.get_char_size(row.cx2rx[cursor_x]);
220✔
203
                }
220✔
204
                self.cursor.x = cursor_x;
55✔
205
            }
206
            (AKey::Right, Some(_)) => self.cursor.move_to_next_line(),
44✔
207
            // TODO: For Up and Down, move self.cursor.x to be consistent with tabs and UTF-8
208
            //  characters, i.e. according to rx
209
            (AKey::Up, _) if self.cursor.y > 0 => self.cursor.y -= 1,
154✔
210
            (AKey::Down, Some(_)) => self.cursor.y += 1,
44✔
211
            _ => (),
44✔
212
        }
213
        self.update_cursor_x_position();
495✔
214
    }
495✔
215

216
    /// Update the cursor x position. If the cursor y position has changed, the
217
    /// current position might be illegal (x is further right than the last
218
    /// character of the row). If that is the case, clamp `self.cursor.x`.
219
    fn update_cursor_x_position(&mut self) {
594✔
220
        self.cursor.x = self.cursor.x.min(self.current_row().map_or(0, |row| row.chars.len()));
594✔
221
    }
594✔
222

223
    /// Run a loop to obtain the key that was pressed. At each iteration of the
224
    /// loop (until a key is pressed), we listen to the `ws_changed` channel
225
    /// to check if a window size change signal has been received. When
226
    /// bytes are received, we match to a corresponding `Key`. In particular,
227
    /// we handle ANSI escape codes to return `Key::Delete`, `Key::Home` etc.
228
    fn loop_until_keypress(&mut self, input: &mut impl BufRead) -> Result<Key, Error> {
154✔
229
        let mut bytes = input.bytes();
154✔
230
        loop {
231
            // Handle window size if a signal has be received
232
            if sys::has_window_size_changed() {
154✔
233
                self.update_window_size()?;
×
234
                self.refresh_screen()?;
×
235
            }
154✔
236
            // Match on the next byte received or, if the first byte is <ESC> ('\x1b'), on
237
            // the next few bytes.
238
            if let Some(a) = bytes.next().transpose()? {
154✔
239
                if a != b'\x1b' {
154✔
240
                    return Ok(Key::Char(a));
33✔
241
                }
121✔
242
                return Ok(match bytes.next().transpose()? {
121✔
243
                    Some(b @ (b'[' | b'O')) => match (b, bytes.next().transpose()?) {
121✔
244
                        (b'[', Some(c @ b'A'..=b'D')) => Key::Arrow(get_akey(c)),
66✔
245
                        (b'[' | b'O', Some(b'H')) => Key::Home,
22✔
246
                        (b'[' | b'O', Some(b'F')) => Key::End,
22✔
247
                        (b'[', mut c @ Some(b'0'..=b'8')) => {
33✔
248
                            let mut d = bytes.next().transpose()?;
22✔
249
                            if (c, d) == (Some(b'1'), Some(b';')) {
22✔
250
                                // 1 is the default modifier value. Therefore, <ESC>[1;5C is
251
                                // equivalent to <ESC>[5C, etc.
252
                                c = bytes.next().transpose()?;
11✔
253
                                d = bytes.next().transpose()?;
11✔
254
                            }
11✔
255
                            match (c, d) {
22✔
256
                                (Some(c), Some(b'~')) if c == b'1' || c == b'7' => Key::Home,
×
257
                                (Some(c), Some(b'~')) if c == b'4' || c == b'8' => Key::End,
×
258
                                (Some(b'3'), Some(b'~')) => Key::Delete,
×
259
                                (Some(b'5'), Some(b'~')) => Key::PageUp,
×
260
                                (Some(b'6'), Some(b'~')) => Key::PageDown,
×
261
                                (Some(b'5'), Some(d @ b'A'..=b'D')) => Key::CtrlArrow(get_akey(d)),
22✔
262
                                _ => Key::Escape,
×
263
                            }
264
                        }
265
                        (b'O', Some(c @ b'a'..=b'd')) => Key::CtrlArrow(get_akey(c)),
×
266
                        _ => Key::Escape,
11✔
267
                    },
268
                    _ => Key::Escape,
×
269
                });
270
            }
×
271
        }
272
    }
154✔
273

274
    /// Update the `screen_rows`, `window_width`, `screen_cols` and `ln_padding`
275
    /// attributes.
276
    fn update_window_size(&mut self) -> Result<(), Error> {
×
277
        let wsize = sys::get_window_size().or_else(|_| terminal::get_window_size_using_cursor())?;
×
278
        // Make room for the status bar and status message
279
        (self.screen_rows, self.window_width) = (wsize.0.saturating_sub(2), wsize.1);
×
280
        self.update_screen_cols();
×
281
        Ok(())
×
282
    }
×
283

284
    /// Update the `screen_cols` and `ln_padding` attributes based on the
285
    /// maximum number of digits for line numbers (since the left padding
286
    /// depends on this number of digits).
287
    fn update_screen_cols(&mut self) {
1,650✔
288
        // The maximum number of digits to use for the line number is the number of
289
        // digits of the last line number. This is equal to the number of times
290
        // we can divide this number by ten, computed below using `successors`.
291
        let n_digits = scsr(Some(self.rows.len()), |u| Some(u / 10).filter(|u| *u > 0)).count();
1,727✔
292
        let show_line_num = self.config.show_line_num && n_digits + 2 < self.window_width / 4;
1,650✔
293
        self.ln_pad = if show_line_num { n_digits + 2 } else { 0 };
1,650✔
294
        self.screen_cols = self.window_width.saturating_sub(self.ln_pad);
1,650✔
295
    }
1,650✔
296

297
    /// Update a row, given its index. If `ignore_following_rows` is `false` and
298
    /// the highlight state has changed during the update (for instance, it
299
    /// is now in "multi-line comment" state, keep updating the next rows
300
    fn update_row(&mut self, y: usize, ignore_following_rows: bool) {
6,314✔
301
        let mut hl_state = if y > 0 { self.rows[y - 1].hl_state } else { HlState::Normal };
6,314✔
302
        for row in self.rows.iter_mut().skip(y) {
6,314✔
303
            let previous_hl_state = row.hl_state;
6,193✔
304
            hl_state = row.update(&self.syntax, hl_state, self.config.tab_stop);
6,193✔
305
            if ignore_following_rows || hl_state == previous_hl_state {
6,193✔
306
                return;
6,116✔
307
            }
77✔
308
            // If the state has changed (for instance, a multi-line comment
309
            // started in this row), continue updating the following
310
            // rows
311
        }
312
    }
6,314✔
313

314
    /// Update all the rows.
315
    fn update_all_rows(&mut self) {
×
316
        let mut hl_state = HlState::Normal;
×
317
        for row in &mut self.rows {
×
318
            hl_state = row.update(&self.syntax, hl_state, self.config.tab_stop);
×
319
        }
×
320
    }
×
321

322
    /// Insert a byte at the current cursor position. If there is no row at the
323
    /// current cursor position, add a new row and insert the byte.
324
    fn insert_byte(&mut self, c: u8) {
4,334✔
325
        if let Some(row) = self.rows.get_mut(self.cursor.y) {
4,334✔
326
            row.chars.insert(self.cursor.x, c);
4,037✔
327
        } else {
4,037✔
328
            self.rows.push(Row::new(vec![c]));
297✔
329
            // The number of rows has changed. The left padding may need to be updated.
297✔
330
            self.update_screen_cols();
297✔
331
        }
297✔
332
        self.update_row(self.cursor.y, false);
4,334✔
333
        (self.cursor.x, self.n_bytes, self.dirty) = (self.cursor.x + 1, self.n_bytes + 1, true);
4,334✔
334
    }
4,334✔
335

336
    /// Insert a new line at the current cursor position and move the cursor to
337
    /// the start of the new line. If the cursor is in the middle of a row,
338
    /// split off that row.
339
    fn insert_new_line(&mut self) {
1,067✔
340
        let (position, new_row_chars) = if self.cursor.x == 0 {
1,067✔
341
            (self.cursor.y, Vec::new())
649✔
342
        } else {
343
            // self.rows[self.cursor.y] must exist, since cursor.x = 0 for any cursor.y ≥
344
            // row.len()
345
            let new_chars = self.rows[self.cursor.y].chars.split_off(self.cursor.x);
418✔
346
            self.update_row(self.cursor.y, false);
418✔
347
            (self.cursor.y + 1, new_chars)
418✔
348
        };
349
        self.rows.insert(position, Row::new(new_row_chars));
1,067✔
350
        self.update_row(position, false);
1,067✔
351
        self.update_screen_cols();
1,067✔
352
        self.cursor.move_to_next_line();
1,067✔
353
        self.dirty = true;
1,067✔
354
    }
1,067✔
355

356
    /// Delete a character at the current cursor position. If the cursor is
357
    /// located at the beginning of a row that is not the first or last row,
358
    /// merge the current row and the previous row. If the cursor is located
359
    /// after the last row, move up to the last character of the previous row.
360
    fn delete_char(&mut self) {
275✔
361
        if self.cursor.x > 0 {
275✔
362
            let row = &mut self.rows[self.cursor.y];
33✔
363
            // Obtain the number of bytes to be removed: could be 1-4 (UTF-8 character
364
            // size).
365
            let n_bytes_to_remove = row.get_char_size(row.cx2rx[self.cursor.x] - 1);
33✔
366
            row.chars.splice(self.cursor.x - n_bytes_to_remove..self.cursor.x, iter::empty());
33✔
367
            self.update_row(self.cursor.y, false);
33✔
368
            self.cursor.x -= n_bytes_to_remove;
33✔
369
            self.dirty = if self.is_empty() { self.file_name.is_some() } else { true };
33✔
370
            self.n_bytes -= n_bytes_to_remove as u64;
33✔
371
        } else if self.cursor.y < self.rows.len() && self.cursor.y > 0 {
242✔
372
            let row = self.rows.remove(self.cursor.y);
198✔
373
            let previous_row = &mut self.rows[self.cursor.y - 1];
198✔
374
            self.cursor.x = previous_row.chars.len();
198✔
375
            previous_row.chars.extend(&row.chars);
198✔
376
            self.update_row(self.cursor.y - 1, true);
198✔
377
            self.update_row(self.cursor.y, false);
198✔
378
            // The number of rows has changed. The left padding may need to be updated.
198✔
379
            self.update_screen_cols();
198✔
380
            (self.dirty, self.cursor.y) = (true, self.cursor.y - 1);
198✔
381
        } else if self.cursor.y == self.rows.len() {
198✔
382
            // If the cursor is located after the last row, pressing backspace is equivalent
11✔
383
            // to pressing the left arrow key.
11✔
384
            self.move_cursor(&AKey::Left, false);
11✔
385
        }
33✔
386
    }
275✔
387

388
    fn delete_current_row(&mut self) {
231✔
389
        if self.cursor.y < self.rows.len() {
231✔
390
            self.rows[self.cursor.y].chars.clear();
220✔
391
            self.cursor.x = 0;
220✔
392
            self.cursor.y = std::cmp::min(self.cursor.y + 1, self.rows.len() - 1);
220✔
393
            self.delete_char();
220✔
394
            self.cursor.x = 0;
220✔
395
        }
220✔
396
    }
231✔
397

398
    fn duplicate_current_row(&mut self) {
×
399
        self.copy_current_row();
×
400
        self.paste_current_row();
×
401
    }
×
402

403
    fn copy_current_row(&mut self) {
×
404
        if let Some(row) = self.current_row() {
×
405
            self.copied_row = row.chars.clone();
×
406
        }
×
407
    }
×
408

409
    fn paste_current_row(&mut self) {
×
410
        if self.copied_row.is_empty() {
×
411
            return;
×
412
        }
×
413
        self.n_bytes += self.copied_row.len() as u64;
×
414
        let y = (self.cursor.y + 1).min(self.rows.len());
×
415
        self.rows.insert(y, Row::new(self.copied_row.clone()));
×
416
        self.update_row(y, false);
×
417
        (self.cursor.y, self.dirty) = (y, true);
×
418
        self.update_screen_cols();
×
419
    }
×
420

421
    /// Toggle comment on the current line using the appropriate comment symbol
422
    /// from the syntax configuration. If the line is already commented,
423
    /// uncomment it. If not, add a comment symbol at the beginning.
424
    fn toggle_comment(&mut self) {
66✔
425
        // Get the first single-line comment start symbol from syntax config
426
        let Some(sym) = self.syntax.sl_comment_start.first() else { return };
66✔
427
        let Some(row) = self.rows.get_mut(self.cursor.y) else { return };
66✔
428
        // Find the first non-whitespace character position
429
        let pos = row.chars.iter().position(|&c| !(c as char).is_whitespace()).unwrap_or(0);
154✔
430

431
        // Check if the line is already commented
432
        let n_update = if row.chars.get(pos..pos + sym.len()) == Some(sym.as_bytes()) {
66✔
433
            let to_remove = sym.len() + usize::from(row.chars.get(pos + sym.len()) == Some(&b' '));
33✔
434
            // Remove the comment and return the removed size as a negative integer
435
            0isize.saturating_sub_unsigned(row.chars.drain(pos..pos + to_remove).len())
33✔
436
        } else {
437
            // Insert comment at the first non-whitespace position
438
            row.chars.splice(pos..pos, iter::chain(sym.bytes(), iter::once(b' ')));
33✔
439
            1isize.saturating_add_unsigned(sym.len())
33✔
440
        };
441
        self.n_bytes = self.n_bytes.saturating_add_signed(n_update as i64);
66✔
442
        if self.cursor.x >= pos {
66✔
443
            self.cursor.x = self.cursor.x.saturating_add_signed(n_update);
44✔
444
        }
44✔
445

446
        self.update_row(self.cursor.y, false);
66✔
447
        // Update cursor position to ensure it's valid after row update
448
        self.update_cursor_x_position();
66✔
449
        self.dirty = true;
66✔
450
    }
66✔
451

452
    /// Try to load a file. If found, load the rows and update the render and
453
    /// syntax highlighting. If not found, do not return an error.
454
    fn load(&mut self, path: &Path) -> Result<(), Error> {
×
455
        let mut file = match File::open(path) {
×
456
            Err(e) if e.kind() == ErrorKind::NotFound => {
×
457
                self.rows.push(Row::new(Vec::new()));
×
458
                return Ok(());
×
459
            }
460
            r => r,
×
461
        }?;
×
462
        let ft = file.metadata()?.file_type();
×
463
        if !(ft.is_file() || ft.is_symlink()) {
×
464
            return Err(io::Error::new(ErrorKind::InvalidInput, "Invalid input file type").into());
×
465
        }
×
466
        for line in BufReader::new(&file).split(b'\n') {
×
467
            self.rows.push(Row::new(line?));
×
468
        }
469
        // If the file ends with an empty line or is empty, we need to append an empty
470
        // row to `self.rows`. Unfortunately, BufReader::split doesn't yield an
471
        // empty Vec in this case, so we need to check the last byte directly.
472
        file.seek(io::SeekFrom::End(0))?;
×
473
        #[expect(clippy::unbuffered_bytes)]
474
        if file.bytes().next().transpose()?.is_none_or(|b| b == b'\n') {
×
475
            self.rows.push(Row::new(Vec::new()));
×
476
        }
×
477
        self.update_all_rows();
×
478
        // The number of rows has changed. The left padding may need to be updated.
479
        self.update_screen_cols();
×
480
        self.n_bytes = self.rows.iter().map(|row| row.chars.len() as u64).sum();
×
481
        Ok(())
×
482
    }
×
483

484
    /// Save the text to a file, given its name.
485
    fn save(&self, file_name: &str) -> Result<usize, io::Error> {
×
486
        let mut file = File::create(file_name)?;
×
487
        let mut written = 0;
×
488
        for (i, row) in self.rows.iter().enumerate() {
×
489
            file.write_all(&row.chars)?;
×
490
            written += row.chars.len();
×
491
            if i != (self.rows.len() - 1) {
×
492
                file.write_all(b"\n")?;
×
493
                written += 1;
×
494
            }
×
495
        }
496
        file.sync_all()?;
×
497
        Ok(written)
×
498
    }
×
499

500
    /// Save the text to a file and handle all errors. Errors and success
501
    /// messages will be printed to the status bar. Return whether the file
502
    /// was successfully saved.
503
    fn save_and_handle_io_errors(&mut self, file_name: &str) -> bool {
×
504
        let saved = self.save(file_name);
×
505
        // Print error or success message to the status bar
506
        match saved.as_ref() {
×
507
            Ok(w) => set_status!(self, "{} written to {}", format_size(*w as u64), file_name),
×
508
            Err(err) => set_status!(self, "Can't save! I/O error: {err}"),
×
509
        }
510
        // If save was successful, set dirty to false.
511
        self.dirty &= saved.is_err();
×
512
        saved.is_ok()
×
513
    }
×
514

515
    /// Save to a file after obtaining the file path from the prompt. If
516
    /// successful, the `file_name` attribute of the editor will be set and
517
    /// syntax highlighting will be updated.
518
    fn save_as(&mut self, file_name: String) {
×
519
        if self.save_and_handle_io_errors(&file_name) {
×
520
            // If save was successful
×
521
            self.syntax = SyntaxConf::find(&file_name, &sys::data_dirs());
×
522
            self.file_name = Some(file_name);
×
523
            self.update_all_rows();
×
524
        }
×
525
    }
×
526

527
    /// Draw the left part of the screen: line numbers and vertical bar.
528
    fn draw_left_padding<T: Display>(&self, buffer: &mut String, val: T) {
88✔
529
        if self.ln_pad >= 2 {
88✔
530
            // \u{2502}: pipe "│"
44✔
531
            let s = format!("{:>1$} \u{2502}", val, self.ln_pad - 2);
44✔
532
            // \x1b[38;5;240m: Dark grey color
44✔
533
            push_colored(buffer, "\x1b[38;5;240m", &s, self.use_color);
44✔
534
        }
44✔
535
    }
88✔
536

537
    /// Return whether the file being edited is empty or not. If there is more
538
    /// than one row, even if all the rows are empty, `is_empty` returns
539
    /// `false`, since the text contains new lines.
540
    const fn is_empty(&self) -> bool { self.rows.len() <= 1 && self.n_bytes == 0 }
33✔
541

542
    /// Draw rows of text and empty rows on the terminal, by adding characters
543
    /// to the buffer.
544
    fn draw_rows(&self, buffer: &mut String) -> Result<(), Error> {
×
545
        let row_it = self.rows.iter().map(Some).chain(repeat(None)).enumerate();
×
546
        for (i, row) in row_it.skip(self.cursor.roff).take(self.screen_rows) {
×
547
            buffer.push_str(CLEAR_LINE_RIGHT_OF_CURSOR);
×
548
            if let Some(row) = row {
×
549
                // Draw a row of text
×
550
                self.draw_left_padding(buffer, i + 1);
×
551
                row.draw(self.cursor.coff, self.screen_cols, buffer, self.use_color);
×
552
            } else {
×
553
                // Draw an empty row
554
                self.draw_left_padding(buffer, '~');
×
555
                if self.is_empty() && i == self.screen_rows / 3 {
×
556
                    write!(buffer, "{:^1$.1$}", WELCOME_MESSAGE, self.screen_cols)?;
×
557
                }
×
558
            }
559
            buffer.push_str("\r\n");
×
560
        }
561
        Ok(())
×
562
    }
×
563

564
    /// Draw the status bar on the terminal, by adding characters to the buffer.
565
    fn draw_status_bar(&self, buffer: &mut String) {
×
566
        // Left part of the status bar
567
        let modified = if self.dirty { " (modified)" } else { "" };
×
568
        let mut left =
×
569
            format!("{:.30}{modified}", self.file_name.as_deref().unwrap_or("[No Name]"));
×
570
        left.truncate(self.window_width);
×
571

572
        // Right part of the status bar
573
        let size = format_size(self.n_bytes + self.rows.len().saturating_sub(1) as u64);
×
574
        let right =
×
575
            format!("{} | {size} | {}:{}", self.syntax.name, self.cursor.y + 1, self.rx() + 1);
×
576

577
        // Draw
578
        let rw = self.window_width.saturating_sub(left.len());
×
579
        push_colored(buffer, WBG, &format!("{left}{right:>rw$.rw$}\r\n"), self.use_color);
×
580
    }
×
581

582
    /// Draw the message bar on the terminal, by adding characters to the
583
    /// buffer.
584
    fn draw_message_bar(&self, buffer: &mut String) {
×
585
        buffer.push_str(CLEAR_LINE_RIGHT_OF_CURSOR);
×
586
        let msg_duration = self.config.message_dur;
×
587
        if let Some(sm) = self.status_msg.as_ref().filter(|sm| sm.time.elapsed() < msg_duration) {
×
588
            buffer.push_str(&sm.msg[..sm.msg.len().min(self.window_width)]);
×
589
        }
×
590
    }
×
591

592
    /// Refresh the screen: update the offsets, draw the rows, the status bar,
593
    /// the message bar, and move the cursor to the correct position.
594
    fn refresh_screen(&mut self) -> Result<(), Error> {
×
595
        self.cursor.scroll(self.rx(), self.screen_rows, self.screen_cols);
×
596
        let mut buffer = format!("{HIDE_CURSOR}{MOVE_CURSOR_TO_START}");
×
597
        self.draw_rows(&mut buffer)?;
×
598
        self.draw_status_bar(&mut buffer);
×
599
        self.draw_message_bar(&mut buffer);
×
600
        let (cursor_x, cursor_y) = if self.prompt_mode.is_none() {
×
601
            // If not in prompt mode, position the cursor according to the `cursor`
602
            // attributes.
603
            (self.rx() - self.cursor.coff + 1 + self.ln_pad, self.cursor.y - self.cursor.roff + 1)
×
604
        } else {
605
            // If in prompt mode, position the cursor on the prompt line at the end of the
606
            // line.
607
            (self.status_msg.as_ref().map_or(0, |sm| sm.msg.len() + 1), self.screen_rows + 2)
×
608
        };
609
        // Finally, print `buffer` and move the cursor
610
        print!("{buffer}\x1b[{cursor_y};{cursor_x}H{SHOW_CURSOR}");
×
611
        io::stdout().flush().map_err(Error::from)
×
612
    }
×
613

614
    /// Process a key that has been pressed, when not in prompt mode. Returns
615
    /// whether the program should exit, and optionally the prompt mode to
616
    /// switch to.
617
    fn process_keypress(&mut self, key: &Key) -> (bool, Option<PromptMode>) {
4,213✔
618
        // This won't be mutated, unless key is Key::Character(EXIT)
619
        let mut reset_quit_times = true;
4,213✔
620
        let mut prompt_mode = None;
4,213✔
621

UNCOV
622
        match key {
×
623
            Key::Arrow(arrow) => self.move_cursor(arrow, false),
×
624
            Key::CtrlArrow(arrow) => self.move_cursor(arrow, true),
×
625
            Key::PageUp => {
11✔
626
                self.cursor.y = self.cursor.roff.saturating_sub(self.screen_rows);
11✔
627
                self.update_cursor_x_position();
11✔
628
            }
11✔
629
            Key::PageDown => {
22✔
630
                self.cursor.y = (self.cursor.roff + 2 * self.screen_rows - 1).min(self.rows.len());
22✔
631
                self.update_cursor_x_position();
22✔
632
            }
22✔
633
            Key::Home => self.cursor.x = 0,
22✔
634
            Key::End => self.cursor.x = self.current_row().map_or(0, |row| row.chars.len()),
22✔
635
            Key::Char(b'\r' | b'\n') => self.insert_new_line(), // Enter
418✔
636
            Key::Char(BACKSPACE | DELETE_BIS) => self.delete_char(), // Backspace or Ctrl + H
×
637
            Key::Char(REMOVE_LINE) => self.delete_current_row(),
×
638
            Key::Delete => {
33✔
639
                self.move_cursor(&AKey::Right, false);
33✔
640
                self.delete_char();
33✔
641
            }
33✔
642
            Key::Escape | Key::Char(REFRESH_SCREEN) => (),
×
643
            Key::Char(EXIT) => {
644
                if !self.dirty || self.quit_times + 1 >= self.config.quit_times {
×
645
                    return (true, None);
×
646
                }
×
647
                let r = self.config.quit_times - self.quit_times - 1;
×
648
                set_status!(self, "Press Ctrl+Q {0} more time{1:.2$} to quit.", r, "s", r - 1);
×
649
                reset_quit_times = false;
×
650
            }
NEW
651
            Key::Char(SAVE) if let Some(file_name) = self.file_name.take() => {
×
NEW
652
                self.save_and_handle_io_errors(&file_name);
×
NEW
653
                self.file_name = Some(file_name);
×
NEW
654
            }
×
NEW
655
            Key::Char(SAVE) => prompt_mode = Some(PromptMode::Save(String::new())),
×
656
            Key::Char(FIND) =>
657
                prompt_mode = Some(PromptMode::Find(String::new(), self.cursor.clone(), None)),
×
658
            Key::Char(GOTO) => prompt_mode = Some(PromptMode::GoTo(String::new())),
×
659
            Key::Char(DUPLICATE) => self.duplicate_current_row(),
×
660
            Key::Char(CUT) => {
×
661
                self.copy_current_row();
×
662
                self.delete_current_row();
×
663
            }
×
664
            Key::Char(COPY) => self.copy_current_row(),
×
665
            Key::Char(PASTE) => self.paste_current_row(),
×
666
            Key::Char(TOGGLE_COMMENT) => self.toggle_comment(),
66✔
667
            Key::Char(EXECUTE) => prompt_mode = Some(PromptMode::Execute(String::new())),
×
668
            Key::Char(c) => self.insert_byte(*c),
3,619✔
669
        }
670
        self.quit_times = if reset_quit_times { 0 } else { self.quit_times + 1 };
4,213✔
671
        (false, prompt_mode)
4,213✔
672
    }
4,213✔
673

674
    /// Try to find a query, this is called after pressing Ctrl-F and for each
675
    /// key that is pressed. `last_match` is the last row that was matched,
676
    /// `forward` indicates whether to search forward or backward. Returns
677
    /// the row of a new match, or `None` if the search was unsuccessful.
678
    fn find(&mut self, query: &str, last_match: Option<usize>, forward: bool) -> Option<usize> {
132✔
679
        // Number of rows to search
680
        let num_rows = if query.is_empty() { 0 } else { self.rows.len() };
132✔
681
        let mut current = last_match.unwrap_or_else(|| num_rows.saturating_sub(1));
132✔
682
        // TODO: Handle multiple matches per line
683
        for _ in 0..num_rows {
132✔
684
            current = (current + if forward { 1 } else { num_rows - 1 }) % num_rows;
110✔
685
            let row = &mut self.rows[current];
110✔
686
            if let Some(cx) = row.chars.windows(query.len()).position(|w| w == query.as_bytes()) {
110✔
687
                // self.cursor.coff: Try to reset the column offset; if the match is after the
688
                // offset, this will be updated in self.cursor.scroll() so that
689
                // the result is visible
690
                (self.cursor.x, self.cursor.y, self.cursor.coff) = (cx, current, 0);
×
691
                let rx = row.cx2rx[cx];
×
692
                row.match_segment = Some(rx..rx + query.len());
×
693
                return Some(current);
×
694
            }
110✔
695
        }
696
        None
132✔
697
    }
132✔
698

699
    /// If `file_name` is not None, load the file. Then run the text editor.
700
    ///
701
    /// # Errors
702
    ///
703
    /// Will Return `Err` if any error occur.
704
    pub fn run<I: BufRead>(&mut self, file_name: Option<&str>, input: &mut I) -> Result<(), Error> {
×
705
        self.update_window_size()?;
×
706
        set_status!(self, "{HELP_MESSAGE}");
×
707

708
        if let Some(path) = file_name.map(sys::path) {
×
709
            self.syntax = SyntaxConf::find(&path.to_string_lossy(), &sys::data_dirs());
×
710
            self.load(path.as_path())?;
×
711
            self.file_name = Some(path.to_string_lossy().to_string());
×
712
        } else {
×
713
            self.rows.push(Row::new(Vec::new()));
×
714
            self.file_name = None;
×
715
        }
×
716
        loop {
717
            if let Some(mode) = &self.prompt_mode {
×
718
                set_status!(self, "{}", mode.status_msg());
×
719
            }
×
720
            self.refresh_screen()?;
×
721
            let key = self.loop_until_keypress(input)?;
×
722
            // TODO: Can we avoid using take()?
723
            self.prompt_mode = match self.prompt_mode.take() {
×
724
                // process_keypress returns (should_quit, prompt_mode)
725
                None => match self.process_keypress(&key) {
×
726
                    (true, _) => return Ok(()),
×
727
                    (false, prompt_mode) => prompt_mode,
×
728
                },
729
                Some(prompt_mode) => prompt_mode.process_keypress(self, &key),
×
730
            }
731
        }
732
    }
×
733
}
734

735
/// Set up the terminal and run the text editor. If `file_name` is not None,
736
/// load the file.
737
///
738
/// Update the panic hook to restore the terminal on panic.
739
///
740
/// # Errors
741
///
742
/// Will Return `Err` if any error occur when registering the window size signal
743
/// handler, enabling raw mode, or running the editor.
744
pub fn run<I: BufRead>(file_name: Option<&str>, input: &mut I) -> Result<(), Error> {
120✔
745
    sys::register_winsize_change_signal_handler()?;
120✔
746
    let orig_term_mode = sys::enable_raw_mode()?;
120✔
747
    let mut editor = Editor { config: Config::load(), ..Default::default() };
×
748
    editor.use_color = !std::env::var("NO_COLOR").is_ok_and(|val| !val.is_empty());
×
749

750
    print!("{USE_ALTERNATE_SCREEN}");
×
751

752
    let prev_hook = std::panic::take_hook();
×
753
    std::panic::set_hook(Box::new(move |info| {
×
754
        terminal::restore_terminal(&orig_term_mode).unwrap_or_else(|e| eprintln!("{e}"));
×
755
        prev_hook(info);
×
756
    }));
×
757

758
    let result = editor.run(file_name, input);
×
759

760
    // Restore the original terminal mode.
761
    terminal::restore_terminal(&orig_term_mode)?;
×
762

763
    result
×
764
}
120✔
765

766
/// The prompt mode.
767
#[cfg_attr(test, derive(Debug, PartialEq))]
768
enum PromptMode {
769
    /// Save(prompt buffer)
770
    Save(String),
771
    /// Find(prompt buffer, saved cursor state, last match)
772
    Find(String, CursorState, Option<usize>),
773
    /// GoTo(prompt buffer)
774
    GoTo(String),
775
    /// Execute(prompt buffer)
776
    Execute(String),
777
}
778

779
// TODO: Use trait with mode_status_msg and process_keypress, implement the
780
// trait for separate  structs for Save and Find?
781
impl PromptMode {
782
    /// Return the status message to print for the selected `PromptMode`.
783
    fn status_msg(&self) -> String {
×
784
        match self {
×
785
            Self::Save(buffer) => format!("Save as: {buffer}"),
×
786
            Self::Find(buffer, ..) => format!("Search (Use ESC/Arrows/Enter): {buffer}"),
×
787
            Self::GoTo(buffer) => format!("Enter line number[:column number]: {buffer}"),
×
788
            Self::Execute(buffer) => format!("Command to execute: {buffer}"),
×
789
        }
790
    }
×
791

792
    /// Process a keypress event for the selected `PromptMode`.
793
    fn process_keypress(self, ed: &mut Editor, key: &Key) -> Option<Self> {
154✔
794
        ed.status_msg = None;
154✔
795
        match self {
154✔
796
            Self::Save(b) => match process_prompt_keypress(b, key) {
×
797
                PromptState::Active(b) => return Some(Self::Save(b)),
×
798
                PromptState::Cancelled => set_status!(ed, "Save aborted"),
×
799
                PromptState::Completed(file_name) => ed.save_as(file_name),
×
800
            },
801
            Self::Find(b, saved_cursor, last_match) => {
154✔
802
                if let Some(row_idx) = last_match {
154✔
803
                    ed.rows[row_idx].match_segment = None;
×
804
                }
154✔
805
                match process_prompt_keypress(b, key) {
154✔
806
                    PromptState::Active(query) => {
132✔
807
                        #[expect(clippy::wildcard_enum_match_arm)]
808
                        let (last_match, forward) = match key {
132✔
809
                            Key::Arrow(AKey::Right | AKey::Down) | Key::Char(FIND) =>
810
                                (last_match, true),
×
811
                            Key::Arrow(AKey::Left | AKey::Up) => (last_match, false),
×
812
                            _ => (None, true),
132✔
813
                        };
814
                        let curr_match = ed.find(&query, last_match, forward);
132✔
815
                        return Some(Self::Find(query, saved_cursor, curr_match));
132✔
816
                    }
817
                    // The prompt was cancelled. Restore the previous position.
818
                    PromptState::Cancelled => ed.cursor = saved_cursor,
×
819
                    // Cursor has already been moved, do nothing
820
                    PromptState::Completed(_) => (),
22✔
821
                }
822
            }
823
            Self::GoTo(b) => match process_prompt_keypress(b, key) {
×
824
                PromptState::Active(b) => return Some(Self::GoTo(b)),
×
825
                PromptState::Cancelled => (),
×
826
                PromptState::Completed(b) => {
×
827
                    let mut split = b.splitn(2, ':')
×
828
                        // saturating_sub: Lines and cols are 1-indexed
829
                        .map(|u| u.trim().parse().map(|s: usize| s.saturating_sub(1)));
×
830
                    match (split.next().transpose(), split.next().transpose()) {
×
831
                        (Ok(Some(y)), Ok(x)) => {
×
832
                            ed.cursor.y = y.min(ed.rows.len());
×
833
                            if let Some(rx) = x {
×
834
                                ed.cursor.x = ed.current_row().map_or(0, |r| r.rx2cx[rx]);
×
835
                            } else {
×
836
                                ed.update_cursor_x_position();
×
837
                            }
×
838
                        }
839
                        (Err(e), _) | (_, Err(e)) => set_status!(ed, "Parsing error: {e}"),
×
840
                        (Ok(None), _) => (),
×
841
                    }
842
                }
843
            },
844
            Self::Execute(b) => match process_prompt_keypress(b, key) {
×
845
                PromptState::Active(b) => return Some(Self::Execute(b)),
×
846
                PromptState::Cancelled => (),
×
847
                PromptState::Completed(b) => {
×
848
                    let mut args = b.split_whitespace();
×
849
                    match Command::new(args.next().unwrap_or_default()).args(args).output() {
×
850
                        Ok(out) if !out.status.success() =>
×
851
                            set_status!(ed, "{}", String::from_utf8_lossy(&out.stderr).trim_end()),
×
852
                        Ok(out) => out.stdout.into_iter().for_each(|c| match c {
×
853
                            b'\n' => ed.insert_new_line(),
×
854
                            c => ed.insert_byte(c),
×
855
                        }),
×
856
                        Err(e) => set_status!(ed, "{e}"),
×
857
                    }
858
                }
859
            },
860
        }
861
        None
22✔
862
    }
154✔
863
}
864

865
/// The state of the prompt after processing a keypress event.
866
#[cfg_attr(test, derive(Debug, PartialEq))]
867
enum PromptState {
868
    // Active contains the current buffer
869
    Active(String),
870
    // Completed contains the final string
871
    Completed(String),
872
    Cancelled,
873
}
874

875
/// Process a prompt keypress event and return the new state for the prompt.
876
fn process_prompt_keypress(mut buffer: String, key: &Key) -> PromptState {
374✔
877
    #[expect(clippy::wildcard_enum_match_arm)]
878
    match key {
209✔
879
        Key::Char(b'\r') => return PromptState::Completed(buffer),
33✔
880
        Key::Escape | Key::Char(EXIT) => return PromptState::Cancelled,
22✔
881
        Key::Char(BACKSPACE | DELETE_BIS) => _ = buffer.pop(),
99✔
882
        Key::Char(c @ 0..=126) if !c.is_ascii_control() => buffer.push(*c as char),
220✔
883
        // No-op
884
        _ => (),
22✔
885
    }
886
    PromptState::Active(buffer)
319✔
887
}
374✔
888

889
#[cfg(test)]
890
mod tests {
891
    use std::io::Cursor;
892

893
    use rstest::rstest;
894

895
    use super::*;
896
    use crate::syntax::HlType;
897

898
    fn assert_row_chars_equal(editor: &Editor, expected: &[&[u8]]) {
264✔
899
        assert_eq!(
264✔
900
            editor.rows.len(),
264✔
901
            expected.len(),
264✔
902
            "editor has {} rows, expected {}",
903
            editor.rows.len(),
×
904
            expected.len()
×
905
        );
906
        for (i, (row, expected)) in editor.rows.iter().zip(expected).enumerate() {
506✔
907
            assert_eq!(
506✔
908
                row.chars,
909
                *expected,
910
                "comparing characters for row {}\n  left: {}\n  right: {}",
911
                i,
912
                String::from_utf8_lossy(&row.chars),
×
913
                String::from_utf8_lossy(expected)
×
914
            );
915
        }
916
    }
264✔
917

918
    fn assert_row_synthax_highlighting_types_equal(editor: &Editor, expected: &[&[HlType]]) {
33✔
919
        assert_eq!(
33✔
920
            editor.rows.len(),
33✔
921
            expected.len(),
33✔
922
            "editor has {} rows, expected {}",
923
            editor.rows.len(),
×
924
            expected.len()
×
925
        );
926
        for (i, (row, expected)) in editor.rows.iter().zip(expected).enumerate() {
165✔
927
            assert_eq!(row.hl, *expected, "comparing HlTypes for row {i}");
165✔
928
        }
929
    }
33✔
930

931
    #[rstest]
932
    #[case(0, "0B")]
933
    #[case(1, "1B")]
934
    #[case(1023, "1023B")]
935
    #[case(1024, "1.00kB")]
936
    #[case(1536, "1.50kB")]
937
    // round down!
938
    #[case(21 * 1024 - 11, "20.98kB")]
939
    #[case(21 * 1024 - 10, "20.99kB")]
940
    #[case(21 * 1024 - 3, "20.99kB")]
941
    #[case(21 * 1024, "21.00kB")]
942
    #[case(21 * 1024 + 3, "21.00kB")]
943
    #[case(21 * 1024 + 10, "21.00kB")]
944
    #[case(21 * 1024 + 11, "21.01kB")]
945
    #[case(1024 * 1024 - 1, "1023.99kB")]
946
    #[case(1024 * 1024, "1.00MB")]
947
    #[case(1024 * 1024 + 1, "1.00MB")]
948
    #[case(100 * 1024 * 1024 * 1024, "100.00GB")]
949
    #[case(313 * 1024 * 1024 * 1024 * 1024, "313.00TB")]
950
    fn format_size_output(#[case] input: u64, #[case] expected_output: &str) {
951
        assert_eq!(format_size(input), expected_output);
952
    }
953

954
    #[test]
955
    fn editor_insert_byte() {
11✔
956
        let mut editor = Editor::default();
11✔
957
        let editor_cursor_x_before = editor.cursor.x;
11✔
958

959
        editor.insert_byte(b'X');
11✔
960
        editor.insert_byte(b'Y');
11✔
961
        editor.insert_byte(b'Z');
11✔
962

963
        assert_eq!(editor.cursor.x, editor_cursor_x_before + 3);
11✔
964
        assert_eq!(editor.rows.len(), 1);
11✔
965
        assert_eq!(editor.n_bytes, 3);
11✔
966
        assert_eq!(editor.rows[0].chars, [b'X', b'Y', b'Z']);
11✔
967
    }
11✔
968

969
    #[test]
970
    fn editor_insert_new_line() {
11✔
971
        let mut editor = Editor::default();
11✔
972
        let editor_cursor_y_before = editor.cursor.y;
11✔
973

974
        for _ in 0..3 {
33✔
975
            editor.insert_new_line();
33✔
976
        }
33✔
977

978
        assert_eq!(editor.cursor.y, editor_cursor_y_before + 3);
11✔
979
        assert_eq!(editor.rows.len(), 3);
11✔
980
        assert_eq!(editor.n_bytes, 0);
11✔
981

982
        for row in &editor.rows {
33✔
983
            assert_eq!(row.chars, []);
33✔
984
        }
985
    }
11✔
986

987
    #[test]
988
    fn editor_delete_char() {
11✔
989
        let mut editor = Editor::default();
11✔
990
        for b in b"Hello world!" {
132✔
991
            editor.insert_byte(*b);
132✔
992
        }
132✔
993
        editor.delete_char();
11✔
994
        assert_row_chars_equal(&editor, &[b"Hello world"]);
11✔
995
        editor.move_cursor(&AKey::Left, true);
11✔
996
        editor.move_cursor(&AKey::Left, false);
11✔
997
        editor.move_cursor(&AKey::Left, false);
11✔
998
        editor.delete_char();
11✔
999
        assert_row_chars_equal(&editor, &[b"Helo world"]);
11✔
1000
    }
11✔
1001

1002
    #[test]
1003
    fn editor_delete_next_char() {
11✔
1004
        let mut editor = Editor::default();
11✔
1005
        for &b in b"Hello world!\nHappy New Year!" {
308✔
1006
            editor.process_keypress(&Key::Char(b));
308✔
1007
        }
308✔
1008
        editor.process_keypress(&Key::Delete);
11✔
1009
        assert_row_chars_equal(&editor, &[b"Hello world!", b"Happy New Year!"]);
11✔
1010
        editor.move_cursor(&AKey::Left, true);
11✔
1011
        editor.process_keypress(&Key::Delete);
11✔
1012
        assert_row_chars_equal(&editor, &[b"Hello world!", b"Happy New ear!"]);
11✔
1013
        editor.move_cursor(&AKey::Left, true);
11✔
1014
        editor.move_cursor(&AKey::Left, true);
11✔
1015
        editor.move_cursor(&AKey::Left, true);
11✔
1016
        editor.process_keypress(&Key::Delete);
11✔
1017
        assert_row_chars_equal(&editor, &[b"Hello world!Happy New ear!"]);
11✔
1018
    }
11✔
1019

1020
    #[test]
1021
    fn editor_move_cursor_left() {
11✔
1022
        let mut editor = Editor::default();
11✔
1023
        for &b in b"Hello world!\nHappy New Year!" {
308✔
1024
            editor.process_keypress(&Key::Char(b));
308✔
1025
        }
308✔
1026

1027
        // check current position
1028
        assert_eq!(editor.cursor.x, 15);
11✔
1029
        assert_eq!(editor.cursor.y, 1);
11✔
1030

1031
        editor.move_cursor(&AKey::Left, true);
11✔
1032
        assert_eq!(editor.cursor.x, 10);
11✔
1033
        assert_eq!(editor.cursor.y, 1);
11✔
1034

1035
        editor.move_cursor(&AKey::Left, false);
11✔
1036
        assert_eq!(editor.cursor.x, 9);
11✔
1037
        assert_eq!(editor.cursor.y, 1);
11✔
1038

1039
        editor.move_cursor(&AKey::Left, true);
11✔
1040
        assert_eq!(editor.cursor.x, 6);
11✔
1041
        assert_eq!(editor.cursor.y, 1);
11✔
1042

1043
        editor.move_cursor(&AKey::Left, true);
11✔
1044
        assert_eq!(editor.cursor.x, 0);
11✔
1045
        assert_eq!(editor.cursor.y, 1);
11✔
1046

1047
        editor.move_cursor(&AKey::Left, false);
11✔
1048
        assert_eq!(editor.cursor.x, 12);
11✔
1049
        assert_eq!(editor.cursor.y, 0);
11✔
1050

1051
        editor.move_cursor(&AKey::Left, true);
11✔
1052
        assert_eq!(editor.cursor.x, 6);
11✔
1053
        assert_eq!(editor.cursor.y, 0);
11✔
1054

1055
        editor.move_cursor(&AKey::Left, true);
11✔
1056
        assert_eq!(editor.cursor.x, 0);
11✔
1057
        assert_eq!(editor.cursor.y, 0);
11✔
1058

1059
        editor.move_cursor(&AKey::Left, false);
11✔
1060
        assert_eq!(editor.cursor.x, 0);
11✔
1061
        assert_eq!(editor.cursor.y, 0);
11✔
1062
    }
11✔
1063

1064
    #[test]
1065
    fn editor_move_cursor_up() {
11✔
1066
        let mut editor = Editor::default();
11✔
1067
        for &b in b"abcdefgh\nij\nklmnopqrstuvwxyz" {
308✔
1068
            editor.process_keypress(&Key::Char(b));
308✔
1069
        }
308✔
1070

1071
        // check current position
1072
        assert_eq!(editor.cursor.x, 16);
11✔
1073
        assert_eq!(editor.cursor.y, 2);
11✔
1074

1075
        editor.move_cursor(&AKey::Up, false);
11✔
1076
        assert_eq!(editor.cursor.x, 2);
11✔
1077
        assert_eq!(editor.cursor.y, 1);
11✔
1078

1079
        editor.move_cursor(&AKey::Up, true);
11✔
1080
        assert_eq!(editor.cursor.x, 2);
11✔
1081
        assert_eq!(editor.cursor.y, 0);
11✔
1082

1083
        editor.move_cursor(&AKey::Up, false);
11✔
1084
        assert_eq!(editor.cursor.x, 2);
11✔
1085
        assert_eq!(editor.cursor.y, 0);
11✔
1086
    }
11✔
1087

1088
    #[test]
1089
    fn editor_move_cursor_right() {
11✔
1090
        let mut editor = Editor::default();
11✔
1091
        for &b in b"Hello world\nHappy New Year" {
286✔
1092
            editor.process_keypress(&Key::Char(b));
286✔
1093
        }
286✔
1094

1095
        // check current position
1096
        assert_eq!(editor.cursor.x, 14);
11✔
1097
        assert_eq!(editor.cursor.y, 1);
11✔
1098

1099
        editor.move_cursor(&AKey::Right, false);
11✔
1100
        assert_eq!(editor.cursor.x, 0);
11✔
1101
        assert_eq!(editor.cursor.y, 2);
11✔
1102

1103
        editor.move_cursor(&AKey::Right, false);
11✔
1104
        assert_eq!(editor.cursor.x, 0);
11✔
1105
        assert_eq!(editor.cursor.y, 2);
11✔
1106

1107
        editor.move_cursor(&AKey::Up, true);
11✔
1108
        editor.move_cursor(&AKey::Up, true);
11✔
1109
        assert_eq!(editor.cursor.x, 0);
11✔
1110
        assert_eq!(editor.cursor.y, 0);
11✔
1111

1112
        editor.move_cursor(&AKey::Right, true);
11✔
1113
        assert_eq!(editor.cursor.x, 5);
11✔
1114
        assert_eq!(editor.cursor.y, 0);
11✔
1115

1116
        editor.move_cursor(&AKey::Right, true);
11✔
1117
        assert_eq!(editor.cursor.x, 11);
11✔
1118
        assert_eq!(editor.cursor.y, 0);
11✔
1119

1120
        editor.move_cursor(&AKey::Right, false);
11✔
1121
        assert_eq!(editor.cursor.x, 0);
11✔
1122
        assert_eq!(editor.cursor.y, 1);
11✔
1123
    }
11✔
1124

1125
    #[test]
1126
    fn editor_move_cursor_down() {
11✔
1127
        let mut editor = Editor::default();
11✔
1128
        for &b in b"abcdefgh\nij\nklmnopqrstuvwxyz" {
308✔
1129
            editor.process_keypress(&Key::Char(b));
308✔
1130
        }
308✔
1131

1132
        // check current position
1133
        assert_eq!(editor.cursor.x, 16);
11✔
1134
        assert_eq!(editor.cursor.y, 2);
11✔
1135

1136
        editor.move_cursor(&AKey::Down, false);
11✔
1137
        assert_eq!(editor.cursor.x, 0);
11✔
1138
        assert_eq!(editor.cursor.y, 3);
11✔
1139

1140
        editor.move_cursor(&AKey::Up, false);
11✔
1141
        editor.move_cursor(&AKey::Up, false);
11✔
1142
        editor.move_cursor(&AKey::Up, false);
11✔
1143

1144
        assert_eq!(editor.cursor.x, 0);
11✔
1145
        assert_eq!(editor.cursor.y, 0);
11✔
1146

1147
        editor.move_cursor(&AKey::Right, true);
11✔
1148
        assert_eq!(editor.cursor.x, 8);
11✔
1149
        assert_eq!(editor.cursor.y, 0);
11✔
1150

1151
        editor.move_cursor(&AKey::Down, true);
11✔
1152
        assert_eq!(editor.cursor.x, 2);
11✔
1153
        assert_eq!(editor.cursor.y, 1);
11✔
1154

1155
        editor.move_cursor(&AKey::Down, true);
11✔
1156
        assert_eq!(editor.cursor.x, 2);
11✔
1157
        assert_eq!(editor.cursor.y, 2);
11✔
1158

1159
        editor.move_cursor(&AKey::Down, true);
11✔
1160
        assert_eq!(editor.cursor.x, 0);
11✔
1161
        assert_eq!(editor.cursor.y, 3);
11✔
1162

1163
        editor.move_cursor(&AKey::Down, false);
11✔
1164
        assert_eq!(editor.cursor.x, 0);
11✔
1165
        assert_eq!(editor.cursor.y, 3);
11✔
1166
    }
11✔
1167

1168
    #[test]
1169
    fn editor_press_home_key() {
11✔
1170
        let mut editor = Editor::default();
11✔
1171
        for &b in b"Hello\nWorld\nand\nFerris!" {
253✔
1172
            editor.process_keypress(&Key::Char(b));
253✔
1173
        }
253✔
1174

1175
        // check current position
1176
        assert_eq!(editor.cursor.x, 7);
11✔
1177
        assert_eq!(editor.cursor.y, 3);
11✔
1178

1179
        editor.process_keypress(&Key::Home);
11✔
1180
        assert_eq!(editor.cursor.x, 0);
11✔
1181
        assert_eq!(editor.cursor.y, 3);
11✔
1182

1183
        editor.move_cursor(&AKey::Up, false);
11✔
1184
        editor.move_cursor(&AKey::Up, false);
11✔
1185
        editor.move_cursor(&AKey::Up, false);
11✔
1186

1187
        assert_eq!(editor.cursor.x, 0);
11✔
1188
        assert_eq!(editor.cursor.y, 0);
11✔
1189

1190
        editor.move_cursor(&AKey::Right, true);
11✔
1191
        assert_eq!(editor.cursor.x, 5);
11✔
1192
        assert_eq!(editor.cursor.y, 0);
11✔
1193

1194
        editor.process_keypress(&Key::Home);
11✔
1195
        assert_eq!(editor.cursor.x, 0);
11✔
1196
        assert_eq!(editor.cursor.y, 0);
11✔
1197
    }
11✔
1198

1199
    #[test]
1200
    fn editor_press_end_key() {
11✔
1201
        let mut editor = Editor::default();
11✔
1202
        for &b in b"Hello\nWorld\nand\nFerris!" {
253✔
1203
            editor.process_keypress(&Key::Char(b));
253✔
1204
        }
253✔
1205

1206
        // check current position
1207
        assert_eq!(editor.cursor.x, 7);
11✔
1208
        assert_eq!(editor.cursor.y, 3);
11✔
1209

1210
        editor.process_keypress(&Key::End);
11✔
1211
        assert_eq!(editor.cursor.x, 7);
11✔
1212
        assert_eq!(editor.cursor.y, 3);
11✔
1213

1214
        editor.move_cursor(&AKey::Up, false);
11✔
1215
        editor.move_cursor(&AKey::Up, false);
11✔
1216
        editor.move_cursor(&AKey::Up, false);
11✔
1217

1218
        assert_eq!(editor.cursor.x, 3);
11✔
1219
        assert_eq!(editor.cursor.y, 0);
11✔
1220

1221
        editor.process_keypress(&Key::End);
11✔
1222
        assert_eq!(editor.cursor.x, 5);
11✔
1223
        assert_eq!(editor.cursor.y, 0);
11✔
1224
    }
11✔
1225

1226
    #[test]
1227
    fn editor_page_up_moves_cursor_to_viewport_top() {
11✔
1228
        let mut editor = Editor { screen_rows: 4, ..Default::default() };
11✔
1229
        for _ in 0..10 {
110✔
1230
            editor.insert_new_line();
110✔
1231
        }
110✔
1232

1233
        (editor.cursor.y, editor.cursor.x) = (3, 0);
11✔
1234
        editor.insert_byte(b'a');
11✔
1235
        editor.insert_byte(b'b');
11✔
1236

1237
        (editor.cursor.y, editor.cursor.x, editor.cursor.roff) = (9, 5, 7);
11✔
1238
        let (should_quit, prompt_mode) = editor.process_keypress(&Key::PageUp);
11✔
1239

1240
        assert!(!should_quit);
11✔
1241
        assert!(prompt_mode.is_none());
11✔
1242
        assert_eq!(editor.cursor.y, 3);
11✔
1243
        assert_eq!(editor.cursor.x, 2);
11✔
1244
    }
11✔
1245

1246
    #[test]
1247
    fn editor_page_down_moves_cursor_to_viewport_bottom() {
11✔
1248
        let mut editor = Editor { screen_rows: 4, ..Default::default() };
11✔
1249
        for _ in 0..12 {
132✔
1250
            editor.insert_new_line();
132✔
1251
        }
132✔
1252

1253
        (editor.cursor.y, editor.cursor.x) = (11, 0);
11✔
1254
        editor.insert_byte(b'x');
11✔
1255
        editor.insert_byte(b'y');
11✔
1256
        editor.insert_byte(b'z');
11✔
1257

1258
        (editor.cursor.x, editor.cursor.roff) = (6, 4);
11✔
1259
        let (should_quit, prompt_mode) = editor.process_keypress(&Key::PageDown);
11✔
1260
        assert!(!should_quit);
11✔
1261
        assert!(prompt_mode.is_none());
11✔
1262
        assert_eq!(editor.cursor.y, 11);
11✔
1263
        assert_eq!(editor.cursor.x, 3);
11✔
1264

1265
        (editor.cursor.x, editor.cursor.roff) = (9, 11);
11✔
1266
        let (should_quit, prompt_mode_again) = editor.process_keypress(&Key::PageDown);
11✔
1267
        assert!(!should_quit);
11✔
1268
        assert!(prompt_mode_again.is_none());
11✔
1269
        assert_eq!(editor.cursor.y, editor.rows.len());
11✔
1270
        assert_eq!(editor.cursor.x, 0);
11✔
1271
    }
11✔
1272

1273
    #[rstest]
1274
    #[case::beginning_of_first_row(b"Hello\nWorld!\n", (0, 0), &[&b"World!"[..], &b""[..]], 0)]
1275
    #[case::middle_of_first_row(b"Hello\nWorld!\n", (3, 0), &[&b"World!"[..], &b""[..]], 0)]
1276
    #[case::end_of_first_row(b"Hello\nWorld!\n", (5, 0), &[&b"World!"[..], &b""[..]], 0)]
1277
    #[case::empty_first_row(b"\nHello", (0, 0), &[&b"Hello"[..]], 0)]
1278
    #[case::beginning_of_only_row(b"Hello", (0, 0), &[&b""[..]], 0)]
1279
    #[case::middle_of_only_row(b"Hello", (3, 0), &[&b""[..]], 0)]
1280
    #[case::end_of_only_row(b"Hello", (5, 0), &[&b""[..]], 0)]
1281
    #[case::beginning_of_middle_row(b"Hello\nWorld!\n", (0, 1), &[&b"Hello"[..], &b""[..]], 1)]
1282
    #[case::middle_of_middle_row(b"Hello\nWorld!\n", (3, 1), &[&b"Hello"[..], &b""[..]], 1)]
1283
    #[case::end_of_middle_row(b"Hello\nWorld!\n", (6, 1), &[&b"Hello"[..], &b""[..]], 1)]
1284
    #[case::empty_middle_row(b"Hello\n\nWorld!", (0, 1), &[&b"Hello"[..], &b"World!"[..]], 1)]
1285
    #[case::beginning_of_last_row(b"Hello\nWorld!", (0, 1), &[&b"Hello"[..]], 0)]
1286
    #[case::middle_of_last_row(b"Hello\nWorld!", (3, 1), &[&b"Hello"[..]], 0)]
1287
    #[case::end_of_last_row(b"Hello\nWorld!", (6, 1), &[&b"Hello"[..]], 0)]
1288
    #[case::empty_last_row(b"Hello\n", (0, 1), &[&b"Hello"[..]], 0)]
1289
    #[case::after_last_row(b"Hello\nWorld!", (0, 2), &[&b"Hello"[..], &b"World!"[..]], 2)]
1290
    fn delete_current_row_updates_buffer_and_position(
1291
        #[case] initial_buffer: &[u8], #[case] cursor_position: (usize, usize),
1292
        #[case] expected_rows: &[&[u8]], #[case] expected_cursor_row: usize,
1293
    ) {
1294
        let mut editor = Editor::default();
1295
        for &b in initial_buffer {
1296
            editor.process_keypress(&Key::Char(b));
1297
        }
1298
        (editor.cursor.x, editor.cursor.y) = cursor_position;
1299

1300
        editor.delete_current_row();
1301

1302
        assert_row_chars_equal(&editor, expected_rows);
1303
        assert_eq!(
1304
            (editor.cursor.x, editor.cursor.y),
1305
            (0, expected_cursor_row),
1306
            "cursor is at {}:{}, expected {}:0",
1307
            editor.cursor.y,
1308
            editor.cursor.x,
1309
            expected_cursor_row
1310
        );
1311
    }
1312

1313
    #[rstest]
1314
    #[case::first_row(0)]
1315
    #[case::middle_row(5)]
1316
    #[case::last_row(9)]
1317
    fn delete_current_row_updates_screen_cols_and_ln_pad(#[case] current_row: usize) {
1318
        let mut editor = Editor { window_width: 100, ..Default::default() };
1319
        for _ in 0..10 {
1320
            editor.insert_new_line();
1321
        }
1322
        assert_eq!(editor.screen_cols, 96);
1323
        assert_eq!(editor.ln_pad, 4);
1324

1325
        editor.cursor.y = current_row;
1326
        editor.delete_current_row();
1327

1328
        assert_eq!(editor.screen_cols, 97);
1329
        assert_eq!(editor.ln_pad, 3);
1330
    }
1331

1332
    #[test]
1333
    fn delete_current_row_updates_syntax_highlighting() {
11✔
1334
        let mut editor = Editor {
11✔
1335
            syntax: SyntaxConf {
11✔
1336
                ml_comment_delims: Some(("/*".to_owned(), "*/".to_owned())),
11✔
1337
                ..Default::default()
11✔
1338
            },
11✔
1339
            ..Default::default()
11✔
1340
        };
11✔
1341
        for &b in b"A\nb/*c\nd\ne\nf*/g\nh" {
187✔
1342
            editor.process_keypress(&Key::Char(b));
187✔
1343
        }
187✔
1344

1345
        assert_row_chars_equal(&editor, &[b"A", b"b/*c", b"d", b"e", b"f*/g", b"h"]);
11✔
1346
        assert_row_synthax_highlighting_types_equal(&editor, &[
11✔
1347
            &[HlType::Normal],
11✔
1348
            &[HlType::Normal, HlType::MlComment, HlType::MlComment, HlType::MlComment],
11✔
1349
            &[HlType::MlComment],
11✔
1350
            &[HlType::MlComment],
11✔
1351
            &[HlType::MlComment, HlType::MlComment, HlType::MlComment, HlType::Normal],
11✔
1352
            &[HlType::Normal],
11✔
1353
        ]);
11✔
1354

1355
        (editor.cursor.x, editor.cursor.y) = (0, 4);
11✔
1356
        editor.delete_current_row();
11✔
1357

1358
        assert_row_chars_equal(&editor, &[b"A", b"b/*c", b"d", b"e", b"h"]);
11✔
1359
        assert_row_synthax_highlighting_types_equal(&editor, &[
11✔
1360
            &[HlType::Normal],
11✔
1361
            &[HlType::Normal, HlType::MlComment, HlType::MlComment, HlType::MlComment],
11✔
1362
            &[HlType::MlComment],
11✔
1363
            &[HlType::MlComment],
11✔
1364
            &[HlType::MlComment],
11✔
1365
        ]);
11✔
1366

1367
        (editor.cursor.x, editor.cursor.y) = (0, 1);
11✔
1368
        editor.delete_current_row();
11✔
1369

1370
        assert_row_chars_equal(&editor, &[b"A", b"d", b"e", b"h"]);
11✔
1371
        assert_row_synthax_highlighting_types_equal(&editor, &[
11✔
1372
            &[HlType::Normal],
11✔
1373
            &[HlType::Normal],
11✔
1374
            &[HlType::Normal],
11✔
1375
            &[HlType::Normal],
11✔
1376
        ]);
11✔
1377
    }
11✔
1378

1379
    #[test]
1380
    fn loop_until_keypress() -> Result<(), Error> {
11✔
1381
        let mut editor = Editor::default();
11✔
1382
        let mut fake_stdin = Cursor::new(
11✔
1383
            b"abc\x1b[A\x1b[B\x1b[C\x1b[D\x1b[H\x1bOH\x1b[F\x1bOF\x1b[1;5C\x1b[5C\x1b[99",
1384
        );
1385
        for expected_key in [
154✔
1386
            Key::Char(b'a'),
11✔
1387
            Key::Char(b'b'),
11✔
1388
            Key::Char(b'c'),
11✔
1389
            Key::Arrow(AKey::Up),
11✔
1390
            Key::Arrow(AKey::Down),
11✔
1391
            Key::Arrow(AKey::Right),
11✔
1392
            Key::Arrow(AKey::Left),
11✔
1393
            Key::Home,
11✔
1394
            Key::Home,
11✔
1395
            Key::End,
11✔
1396
            Key::End,
11✔
1397
            Key::CtrlArrow(AKey::Right),
11✔
1398
            Key::CtrlArrow(AKey::Right),
11✔
1399
            Key::Escape,
11✔
1400
        ] {
11✔
1401
            assert_eq!(editor.loop_until_keypress(&mut fake_stdin)?, expected_key);
154✔
1402
        }
1403
        Ok(())
11✔
1404
    }
11✔
1405

1406
    #[rstest]
1407
    #[case::ascii_completed(&[Key::Char(b'H'), Key::Char(b'i'), Key::Char(b'\r')], &PromptState::Completed(String::from("Hi")))]
1408
    #[case::escape(&[Key::Char(b'H'), Key::Char(b'i'), Key::Escape], &PromptState::Cancelled)]
1409
    #[case::exit(&[Key::Char(b'H'), Key::Char(b'i'), Key::Char(EXIT)], &PromptState::Cancelled)]
1410
    #[case::skip_ascii_control(&[Key::Char(b'\x0A')], &PromptState::Active(String::new()))]
1411
    #[case::unsupported_non_ascii(&[Key::Char(b'\xEF')], &PromptState::Active(String::new()))]
1412
    #[case::backspace(&[Key::Char(b'H'), Key::Char(b'i'), Key::Char(BACKSPACE), Key::Char(BACKSPACE)], &PromptState::Active(String::new()))]
1413
    #[case::delete_bis(&[Key::Char(b'H'), Key::Char(b'i'), Key::Char(DELETE_BIS), Key::Char(DELETE_BIS), Key::Char(DELETE_BIS)], &PromptState::Active(String::new()))]
1414
    fn process_prompt_keypresses(#[case] keys: &[Key], #[case] expected_final_state: &PromptState) {
1415
        let mut prompt_state = PromptState::Active(String::new());
1416
        for key in keys {
1417
            if let PromptState::Active(buffer) = prompt_state {
1418
                prompt_state = process_prompt_keypress(buffer, key);
1419
            } else {
1420
                panic!("Prompt state: {prompt_state:?} is not active")
1421
            }
1422
        }
1423
        assert_eq!(prompt_state, *expected_final_state);
1424
    }
1425

1426
    #[rstest]
1427
    #[case(&[Key::Char(b'H'), Key::Char(b'i'), Key::Char(BACKSPACE), Key::Char(b'e'), Key::Char(b'l'), Key::Char(b'l'), Key::Char(b'o')], "Hello")]
1428
    #[case(&[Key::Char(b'H'), Key::Char(b'i'), Key::Char(BACKSPACE), Key::Char(BACKSPACE), Key::Char(BACKSPACE)], "")]
1429
    fn process_find_keypress_completed(#[case] keys: &[Key], #[case] expected_final_value: &str) {
1430
        let mut ed: Editor = Editor::default();
1431
        ed.insert_new_line();
1432
        let mut prompt_mode = Some(PromptMode::Find(String::new(), CursorState::default(), None));
1433
        for key in keys {
1434
            prompt_mode = prompt_mode
1435
                .take()
1436
                .and_then(|prompt_mode| prompt_mode.process_keypress(&mut ed, key));
132✔
1437
        }
1438
        assert_eq!(
1439
            prompt_mode,
1440
            Some(PromptMode::Find(
1441
                String::from(expected_final_value),
1442
                CursorState::default(),
1443
                None
1444
            ))
1445
        );
1446
        prompt_mode = prompt_mode
1447
            .take()
1448
            .and_then(|prompt_mode| prompt_mode.process_keypress(&mut ed, &Key::Char(b'\r')));
22✔
1449
        assert_eq!(prompt_mode, None);
1450
    }
1451

1452
    #[rstest]
1453
    #[case(100, true, 12345, "\u{1b}[38;5;240m12345 │\u{1b}[m")]
1454
    #[case(100, true, "~", "\u{1b}[38;5;240m~ │\u{1b}[m")]
1455
    #[case(10, true, 12345, "")]
1456
    #[case(10, true, "~", "")]
1457
    #[case(100, false, 12345, "12345 │")]
1458
    #[case(100, false, "~", "~ │")]
1459
    #[case(10, false, 12345, "")]
1460
    #[case(10, false, "~", "")]
1461
    fn draw_left_padding<T: Display>(
1462
        #[case] window_width: usize, #[case] use_color: bool, #[case] value: T,
1463
        #[case] expected: &'static str,
1464
    ) {
1465
        let mut editor = Editor { window_width, use_color, ..Default::default() };
1466
        editor.update_screen_cols();
1467

1468
        let mut buffer = String::new();
1469
        editor.draw_left_padding(&mut buffer, value);
1470
        assert_eq!(buffer, expected);
1471
    }
1472

1473
    #[test]
1474
    fn editor_toggle_comment() {
11✔
1475
        let mut editor = Editor::default();
11✔
1476

1477
        // Set up Python syntax configuration for testing
1478
        editor.syntax.sl_comment_start = vec!["#".to_owned()];
11✔
1479

1480
        for b in b"def hello():\n    print(\"Hello\")\n    return True" {
517✔
1481
            if *b == b'\n' {
517✔
1482
                editor.insert_new_line();
22✔
1483
            } else {
495✔
1484
                editor.insert_byte(*b);
495✔
1485
            }
495✔
1486
        }
1487

1488
        // Test commenting a line
1489
        editor.cursor.y = 0; // First line
11✔
1490
        editor.cursor.x = 0;
11✔
1491
        editor.process_keypress(&Key::Char(TOGGLE_COMMENT));
11✔
1492
        assert_eq!(editor.rows[0].chars, b"# def hello():");
11✔
1493

1494
        // Test uncommenting the same line
1495
        editor.process_keypress(&Key::Char(TOGGLE_COMMENT));
11✔
1496
        assert_eq!(editor.rows[0].chars, b"def hello():");
11✔
1497

1498
        // Test commenting an indented line
1499
        editor.cursor.y = 1; // Second line (indented)
11✔
1500
        editor.cursor.x = 0;
11✔
1501
        editor.process_keypress(&Key::Char(TOGGLE_COMMENT));
11✔
1502
        assert_eq!(editor.rows[1].chars, b"    # print(\"Hello\")");
11✔
1503

1504
        // Test uncommenting the indented line
1505
        editor.process_keypress(&Key::Char(TOGGLE_COMMENT));
11✔
1506
        assert_eq!(editor.rows[1].chars, b"    print(\"Hello\")");
11✔
1507

1508
        // Test the bug case: cursor at end of line during toggle
1509
        editor.cursor.y = 0; // First line
11✔
1510
        editor.cursor.x = editor.rows[0].chars.len(); // Position at end
11✔
1511
        editor.process_keypress(&Key::Char(TOGGLE_COMMENT)); // Comment
11✔
1512
        assert_eq!(editor.rows[0].chars, b"# def hello():");
11✔
1513

1514
        // Now uncomment with cursor still at end - this should not panic
1515
        editor.cursor.x = editor.rows[0].chars.len(); // Position at end again
11✔
1516
        editor.process_keypress(&Key::Char(TOGGLE_COMMENT)); // Uncomment
11✔
1517
        assert_eq!(editor.rows[0].chars, b"def hello():");
11✔
1518

1519
        // Verify cursor position is valid
1520
        assert!(editor.cursor.x <= editor.rows[0].chars.len());
11✔
1521
    }
11✔
1522
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc