Skip to main content

kiro_editor/
text_buffer.rs

1use crate::edit_diff::{EditDiff, UndoRedo};
2use crate::error::Result;
3use crate::history::History;
4use crate::language::{Indent, Language};
5use crate::row::Row;
6use std::cmp;
7use std::fs::File;
8use std::io::{self, BufRead, Write};
9use std::path::{Path, PathBuf};
10use std::slice;
11
12// Contain both actual path sequence and display string
13pub struct FilePath {
14    pub path: PathBuf,
15    pub display: String,
16}
17
18impl FilePath {
19    fn from<P: AsRef<Path>>(path: P) -> Self {
20        let path = path.as_ref();
21        FilePath {
22            path: PathBuf::from(path),
23            display: path.to_string_lossy().to_string(),
24        }
25    }
26
27    fn from_string<S: Into<String>>(s: S) -> Self {
28        let display = s.into();
29        FilePath {
30            path: PathBuf::from(&display),
31            display,
32        }
33    }
34}
35
36#[derive(Clone, Copy, PartialEq)]
37pub enum CursorDir {
38    Left,
39    Right,
40    Up,
41    Down,
42}
43
44pub struct Lines<'a>(slice::Iter<'a, Row>);
45
46impl<'a> Iterator for Lines<'a> {
47    type Item = &'a str;
48
49    fn next(&mut self) -> Option<Self::Item> {
50        self.0.next().map(|r| r.buffer())
51    }
52
53    fn size_hint(&self) -> (usize, Option<usize>) {
54        let len = self.0.as_slice().len();
55        (len, Some(len))
56    }
57}
58
59impl<'a> ExactSizeIterator for Lines<'a> {}
60
61pub struct TextBuffer {
62    // (x, y) coordinate in internal text buffer of rows
63    cx: usize,
64    cy: usize,
65    // File editor is opening
66    file: Option<FilePath>,
67    // Lines of text buffer
68    row: Vec<Row>,
69    // Count how many times undo points are created in the buffer. This value is set to 0 at just
70    // after loading the buffer. When saving the buffer to file, count is reset to 0.
71    // When redo/undo is applied without ongoing changes, this count is +1/-1.
72    undo_count: i32,
73    // True when this text buffer has unsaved modifications. This flag is necessary in addition to
74    // undo_count field because even if editing is ongoing and undo point is not created yet,
75    // this flag is set to true
76    modified: bool,
77    // Language which current buffer belongs to
78    lang: Language,
79    // History per undo point for undo/redo
80    history: History,
81    // Flag to ensure at most one undo point per one key input
82    inserted_undo: bool,
83    // Flag to require screen update
84    // TODO: Merge with Screen's dirty_start field by using RenderContext struct
85    dirty_start: Option<usize>,
86}
87
88impl TextBuffer {
89    pub fn empty() -> Self {
90        Self {
91            cx: 0,
92            cy: 0,
93            file: None,
94            row: vec![Row::empty()], // Ensure that every text ends with newline
95            undo_count: 0,
96            modified: false,
97            lang: Language::Plain,
98            history: History::default(),
99            inserted_undo: false,
100            dirty_start: Some(0), // Ensure to render first screen
101        }
102    }
103
104    pub fn with_lines<S: AsRef<str>, I: Iterator<Item = S>>(lines: I) -> Result<Self> {
105        Ok(Self {
106            cx: 0,
107            cy: 0,
108            file: None,
109            row: lines.map(|s| Row::new(s.as_ref())).collect::<Result<_>>()?,
110            undo_count: 0,
111            modified: false,
112            lang: Language::Plain,
113            history: History::default(),
114            inserted_undo: false,
115            dirty_start: Some(0), // Ensure to render first screen
116        })
117    }
118
119    pub fn open<P: AsRef<Path>>(path: P) -> Result<Self> {
120        let path = path.as_ref();
121        let file = Some(FilePath::from(path));
122        if !path.exists() {
123            // When the path does not exist, consider it as a new file
124            let mut buf = Self::empty();
125            buf.file = file;
126            buf.undo_count = 0;
127            buf.modified = false;
128            buf.lang = Language::detect(path);
129            return Ok(buf);
130        }
131
132        let row = io::BufReader::new(File::open(path)?)
133            .lines()
134            .map(|r| Row::new(r?))
135            .collect::<Result<_>>()?;
136
137        Ok(Self {
138            cx: 0,
139            cy: 0,
140            file,
141            row,
142            undo_count: 0,
143            modified: false,
144            lang: Language::detect(path),
145            history: History::default(),
146            inserted_undo: false,
147            dirty_start: Some(0),
148        })
149    }
150
151    fn set_dirty_start(&mut self, line: usize) {
152        if let Some(l) = self.dirty_start {
153            if l <= line {
154                return;
155            }
156        }
157        self.dirty_start = Some(line);
158    }
159
160    fn apply_diff(&mut self, diff: &EditDiff, which: UndoRedo) {
161        let (x, y) = diff.apply(&mut self.row, which);
162        self.set_cursor(x, y);
163        self.set_dirty_start(y);
164    }
165
166    fn new_diff(&mut self, diff: EditDiff) {
167        self.apply_diff(&diff, UndoRedo::Redo);
168        self.modified = true;
169        self.history.push(diff); // Remember diff for undo/redo
170    }
171
172    fn insert_undo_point(&mut self) {
173        if !self.inserted_undo {
174            if self.history.finish_ongoing_edit() {
175                self.undo_count = self.undo_count.saturating_add(1);
176            }
177            self.modified = false;
178            self.inserted_undo = true;
179        }
180    }
181
182    // This method must be called after handling one key input.
183    // TODO: This should be replaced with Drop when separating logic to edit text buffer from TextBuffer
184    // by introducing RenderContext.
185    pub fn finish_edit(&mut self) -> Option<usize> {
186        self.inserted_undo = false;
187        let dirty_start = self.dirty_start;
188        self.dirty_start = None;
189        dirty_start
190    }
191
192    pub fn insert_char(&mut self, ch: char) {
193        // Don't add undo point to squash multiple insert_char changes into one undo
194        if self.cy == self.row.len() {
195            self.new_diff(EditDiff::Newline);
196        }
197        self.new_diff(EditDiff::InsertChar(self.cx, self.cy, ch));
198    }
199
200    pub fn insert_tab(&mut self) {
201        self.insert_undo_point();
202        match self.lang.indent() {
203            Indent::AsIs => self.insert_char('\t'),
204            Indent::Fixed(indent) => {
205                self.new_diff(EditDiff::Insert(self.cx, self.cy, indent.to_owned()));
206            }
207        }
208    }
209
210    fn concat_next_line(&mut self) {
211        // TODO: Move buffer rather than copy
212        let removed = self.row[self.cy + 1].buffer().to_owned();
213        self.new_diff(EditDiff::DeleteLine(self.cy + 1, removed.clone()));
214        self.new_diff(EditDiff::Append(self.cy, removed));
215    }
216
217    fn squash_to_previous_line(&mut self) {
218        // Move cursor to previous line
219        self.cy -= 1;
220        // At top of line, backspace concats current line to previous line
221        self.cx = self.row[self.cy].len(); // Move cursor column to end of previous line
222        self.concat_next_line();
223    }
224
225    pub fn delete_char(&mut self) {
226        if self.cy == self.row.len() || self.cx == 0 && self.cy == 0 {
227            return;
228        }
229        self.insert_undo_point();
230        if self.cx > 0 {
231            let idx = self.cx - 1;
232            let deleted = self.row[self.cy].char_at(idx);
233            self.new_diff(EditDiff::DeleteChar(self.cx, self.cy, deleted));
234        } else {
235            self.squash_to_previous_line();
236        }
237    }
238
239    pub fn delete_until_end_of_line(&mut self) {
240        if self.cy == self.row.len() {
241            return;
242        }
243        self.insert_undo_point();
244        let row = &self.row[self.cy];
245        if self.cx == row.len() {
246            // Do nothing when cursor is at end of line of end of text buffer
247            if self.cy == self.row.len() - 1 {
248                return;
249            }
250            self.concat_next_line();
251        } else if self.cx < row.buffer().len() {
252            let truncated = row[self.cx..].to_owned();
253            self.new_diff(EditDiff::Truncate(self.cy, truncated));
254        }
255    }
256
257    pub fn delete_until_head_of_line(&mut self) {
258        if self.cx == 0 && self.cy == 0 || self.cy == self.row.len() {
259            return;
260        }
261        self.insert_undo_point();
262        if self.cx == 0 {
263            self.squash_to_previous_line();
264        } else {
265            let removed = self.row[self.cy][..self.cx].to_owned();
266            self.new_diff(EditDiff::Remove(self.cx, self.cy, removed));
267        }
268    }
269
270    pub fn delete_word(&mut self) {
271        if self.cx == 0 || self.cy == self.row.len() {
272            return;
273        }
274        self.insert_undo_point();
275
276        let mut x = self.cx - 1;
277        let row = &self.row[self.cy];
278        while x > 0 && row.char_at(x).is_ascii_whitespace() {
279            x -= 1;
280        }
281        // `x - 1` since x should stop at the last non-whitespace character to remove
282        while x > 0 && !row.char_at(x - 1).is_ascii_whitespace() {
283            x -= 1;
284        }
285
286        let removed = self.row[self.cy][x..self.cx].to_owned();
287        self.new_diff(EditDiff::Remove(self.cx, self.cy, removed));
288    }
289
290    pub fn delete_right_char(&mut self) {
291        if self.cy == self.row.len()
292            || self.cy == self.row.len() - 1 && self.cx == self.row[self.cy].len()
293        {
294            // At end of buffer, nothing can be deleted and cursor should not move
295            return;
296        }
297        self.move_cursor_one(CursorDir::Right);
298        self.delete_char();
299    }
300
301    pub fn insert_line(&mut self) {
302        self.insert_undo_point();
303        if self.cy >= self.row.len() {
304            self.new_diff(EditDiff::Newline);
305        } else if self.cx >= self.row[self.cy].len() {
306            self.new_diff(EditDiff::InsertLine(self.cy + 1, "".to_string()));
307        } else if self.cx <= self.row[self.cy].buffer().len() {
308            let truncated = self.row[self.cy][self.cx..].to_owned();
309            self.new_diff(EditDiff::Truncate(self.cy, truncated.clone()));
310            self.new_diff(EditDiff::InsertLine(self.cy + 1, truncated));
311        }
312    }
313
314    pub fn move_cursor_one(&mut self, dir: CursorDir) {
315        match dir {
316            CursorDir::Up => self.cy = self.cy.saturating_sub(1),
317            CursorDir::Left => {
318                if self.cx > 0 {
319                    self.cx -= 1;
320                } else if self.cy > 0 {
321                    // When moving to left at top of line, move cursor to end of previous line
322                    self.cy -= 1;
323                    self.cx = self.row[self.cy].len();
324                }
325            }
326            CursorDir::Down => {
327                // Allow to move cursor until next line to the last line of file to enable to add a
328                // new line at the end.
329                if self.cy < self.row.len() {
330                    self.cy += 1;
331                }
332            }
333            CursorDir::Right => {
334                if self.cy < self.row.len() {
335                    let len = self.row[self.cy].len();
336                    if self.cx < len {
337                        // Allow to move cursor until next col to the last col of line to enable to
338                        // add a new character at the end of line.
339                        self.cx += 1;
340                    } else if self.cx >= len {
341                        // When moving to right at the end of line, move cursor to top of next line.
342                        self.cy += 1;
343                        self.cx = 0;
344                    }
345                }
346            }
347        };
348
349        // Snap cursor to end of line when moving up/down from longer line
350        let len = self.row.get(self.cy).map(Row::len).unwrap_or(0);
351        if self.cx > len {
352            self.cx = len;
353        }
354    }
355
356    pub fn move_cursor_page(&mut self, dir: CursorDir, rowoff: usize, num_rows: usize) {
357        self.cy = match dir {
358            CursorDir::Up => rowoff, // Top of screen
359            CursorDir::Down => {
360                cmp::min(rowoff + num_rows - 1, self.row.len()) // Bottom of screen
361            }
362            _ => unreachable!(),
363        };
364        for _ in 0..num_rows {
365            self.move_cursor_one(dir);
366        }
367    }
368
369    pub fn move_cursor_to_buffer_edge(&mut self, dir: CursorDir) {
370        match dir {
371            CursorDir::Left => self.cx = 0,
372            CursorDir::Right => {
373                if self.cy < self.row.len() {
374                    self.cx = self.row[self.cy].len();
375                }
376            }
377            CursorDir::Up => self.cy = 0,
378            CursorDir::Down => self.cy = self.row.len(),
379        }
380    }
381
382    pub fn move_cursor_by_word(&mut self, dir: CursorDir) {
383        #[derive(PartialEq)]
384        enum CharKind {
385            Ident,
386            Punc,
387            Space,
388        }
389
390        impl CharKind {
391            fn new_at(rows: &[Row], x: usize, y: usize) -> Self {
392                rows.get(y)
393                    .and_then(|r| r.char_at_checked(x))
394                    .map(|c| {
395                        if c.is_ascii_whitespace() {
396                            CharKind::Space
397                        } else if c == '_' || c.is_ascii_alphanumeric() {
398                            CharKind::Ident
399                        } else {
400                            CharKind::Punc
401                        }
402                    })
403                    .unwrap_or(CharKind::Space)
404            }
405        }
406
407        fn at_word_start(left: &CharKind, right: &CharKind) -> bool {
408            matches!(
409                (left, right),
410                (&CharKind::Space, &CharKind::Ident)
411                    | (&CharKind::Space, &CharKind::Punc)
412                    | (&CharKind::Punc, &CharKind::Ident)
413                    | (&CharKind::Ident, &CharKind::Punc)
414            )
415        }
416
417        self.move_cursor_one(dir);
418        let mut prev = CharKind::new_at(&self.row, self.cx, self.cy);
419        self.move_cursor_one(dir);
420        let mut current = CharKind::new_at(&self.row, self.cx, self.cy);
421
422        loop {
423            if self.cy == 0 && self.cx == 0 || self.cy == self.row.len() {
424                return;
425            }
426
427            match dir {
428                CursorDir::Right if at_word_start(&prev, &current) => return,
429                CursorDir::Left if at_word_start(&current, &prev) => {
430                    self.move_cursor_one(CursorDir::Right); // Adjust cursor position to start of word
431                    return;
432                }
433                _ => {}
434            }
435
436            prev = current;
437            self.move_cursor_one(dir);
438            current = CharKind::new_at(&self.row, self.cx, self.cy);
439        }
440    }
441
442    pub fn move_cursor_paragraph(&mut self, dir: CursorDir) {
443        debug_assert!(dir != CursorDir::Left && dir != CursorDir::Right);
444        loop {
445            self.move_cursor_one(dir);
446            if self.cy == 0
447                || self.cy == self.row.len()
448                || self.row[self.cy - 1].buffer().is_empty()
449                    && !self.row[self.cy].buffer().is_empty()
450            {
451                break;
452            }
453        }
454    }
455
456    pub fn rows(&self) -> &[Row] {
457        &self.row
458    }
459
460    pub fn has_file(&self) -> bool {
461        self.file.is_some()
462    }
463
464    pub fn filename(&self) -> &str {
465        self.file
466            .as_ref()
467            .map(|f| f.display.as_str())
468            .unwrap_or("[No Name]")
469    }
470
471    pub fn modified(&self) -> bool {
472        self.undo_count != 0 || self.modified
473    }
474
475    pub fn lang(&self) -> Language {
476        self.lang
477    }
478
479    pub fn cy(&self) -> usize {
480        self.cy
481    }
482
483    pub fn lines(&self) -> Lines<'_> {
484        Lines(self.row.iter())
485    }
486
487    pub fn set_file<S: Into<String>>(&mut self, file_path: S) {
488        let file = FilePath::from_string(file_path);
489        self.lang = Language::detect(&file.path);
490        self.file = Some(file);
491    }
492
493    pub fn set_unnamed(&mut self) {
494        self.file = None;
495    }
496
497    pub fn set_lang(&mut self, lang: Language) {
498        self.lang = lang;
499    }
500
501    pub fn save(&mut self) -> std::result::Result<String, String> {
502        self.insert_undo_point();
503
504        let file = if let Some(file) = &self.file {
505            file
506        } else {
507            return Ok("".to_string()); // Canceled
508        };
509
510        let f = match File::create(&file.path) {
511            Ok(f) => f,
512            Err(e) => return Err(format!("Could not save: {}", e)),
513        };
514        let mut f = io::BufWriter::new(f);
515        let mut bytes = 0;
516        for line in self.row.iter() {
517            let b = line.buffer();
518            writeln!(f, "{}", b).map_err(|e| format!("Could not write to file: {}", e))?;
519            bytes += b.as_bytes().len() + 1;
520        }
521        f.flush()
522            .map_err(|e| format!("Could not flush to file: {}", e))?;
523
524        self.undo_count = 0;
525        self.modified = false;
526        Ok(format!("{} bytes written to {}", bytes, &file.display))
527    }
528
529    pub fn set_cursor(&mut self, x: usize, y: usize) {
530        self.cx = x;
531        self.cy = y;
532    }
533
534    pub fn cursor(&self) -> (usize, usize) {
535        (self.cx, self.cy)
536    }
537
538    fn after_undoredo(&mut self, state: Option<(usize, usize, usize, bool)>) -> bool {
539        match state {
540            Some((x, y, s, _)) => {
541                self.set_cursor(x, y);
542                self.set_dirty_start(s);
543                true
544            }
545            None => false,
546        }
547    }
548
549    pub fn undo(&mut self) -> bool {
550        let state = self.history.undo(&mut self.row);
551        if let Some((_, _, _, edited)) = state {
552            // If edited is true, it means that undo target is the ongoing change. In the case,
553            // undo point is not consumed and undo count should not be decreased
554            if !edited {
555                self.undo_count = self.undo_count.saturating_sub(1);
556            }
557            self.modified = false;
558        }
559        self.after_undoredo(state)
560    }
561
562    pub fn redo(&mut self) -> bool {
563        let state = self.history.redo(&mut self.row);
564        if let Some((_, _, _, edited)) = state {
565            // If edited is true, it means that redo target is the ongoing change. In the case,
566            // redo does not happen since the new ongoing change is happening and undo count should
567            // not be incremented
568            if !edited {
569                self.undo_count = self.undo_count.saturating_add(1);
570            }
571            self.modified = false;
572        }
573        self.after_undoredo(state)
574    }
575
576    pub fn is_scratch(&self) -> bool {
577        self.file.is_none() && self.row.len() == 1 && self.row[0].len() == 0
578    }
579}