ad_editor/buffer/
mod.rs

1//! A [Buffer] represents a single file or in memory text buffer open within the editor.
2use crate::{
3    config_handle,
4    dot::{find::find_forward_wrapping, Cur, Dot, Range, TextObject},
5    editor::Action,
6    exec::IterBoundedChars,
7    fsys::InputFilter,
8    ftype::{lex::Tokenizer, try_tokenizer_for_path},
9    key::Input,
10    util::normalize_line_endings,
11    MAX_NAME_LEN, UNNAMED_BUFFER,
12};
13use ad_event::Source;
14use std::{
15    cmp::min,
16    fs,
17    io::{self, ErrorKind},
18    path::{Path, PathBuf},
19    time::SystemTime,
20};
21use tracing::debug;
22
23mod buffers;
24mod edit;
25mod internal;
26
27use edit::{Edit, EditLog, Kind, Txt};
28pub use internal::{Chars, GapBuffer, IdxChars, Slice};
29
30pub(crate) use buffers::{BufferId, Buffers};
31
32pub(crate) const DEFAULT_OUTPUT_BUFFER: &str = "+output";
33const HTTPS: &str = "https://";
34const HTTP: &str = "http://";
35
36// Used to inform the editor that further action needs to be taken by it after a Buffer has
37// finished processing a given Action.
38#[derive(Debug, Clone, PartialEq, Eq)]
39pub(crate) enum ActionOutcome {
40    SetClipboard(String),
41    SetStatusMessage(String),
42}
43
44/// Buffer kinds control how each buffer interacts with the rest of the editor functionality
45#[derive(Debug, Clone, PartialEq, Eq)]
46pub(crate) enum BufferKind {
47    /// A regular buffer that is backed by a file on disk.
48    File(PathBuf),
49    /// A directory buffer that is modifyable but cannot be saved
50    Directory(PathBuf),
51    /// An in-memory buffer that is not exposed through fsys
52    Virtual(String),
53    /// An in-memory buffer holding output from commands run within a given directory
54    Output(String),
55    /// A currently un-named buffer that can be converted to a File buffer when named
56    Unnamed,
57    /// State for an active mini-buffer
58    MiniBuffer,
59}
60
61impl Default for BufferKind {
62    fn default() -> Self {
63        Self::Unnamed
64    }
65}
66
67impl BufferKind {
68    fn display_name(&self) -> String {
69        match self {
70            BufferKind::File(p) => p.display().to_string(),
71            BufferKind::Directory(p) => p.display().to_string(),
72            BufferKind::Virtual(s) => s.clone(),
73            BufferKind::Output(s) => s.clone(),
74            BufferKind::Unnamed => UNNAMED_BUFFER.to_string(),
75            BufferKind::MiniBuffer => "".to_string(),
76        }
77    }
78
79    /// The directory containing the file backing this buffer so long as it has kind `File`.
80    fn dir(&self) -> Option<&Path> {
81        match &self {
82            BufferKind::File(p) => p.parent(),
83            BufferKind::Directory(p) => Some(p.as_ref()),
84            BufferKind::Output(s) => Path::new(s).parent(),
85            _ => None,
86        }
87    }
88
89    pub(crate) fn is_file(&self) -> bool {
90        matches!(self, Self::File(_))
91    }
92
93    pub(crate) fn is_dir(&self) -> bool {
94        matches!(self, Self::Directory(_))
95    }
96
97    /// The key for the +output buffer that output from command run from this buffer should be
98    /// redirected to
99    pub fn output_file_key(&self, cwd: &Path) -> String {
100        let path = self.dir().unwrap_or(cwd);
101        format!("{}/{DEFAULT_OUTPUT_BUFFER}", path.display())
102    }
103
104    fn try_kind_and_content_from_path(path: PathBuf) -> io::Result<(Self, String)> {
105        match path.metadata() {
106            Ok(m) if m.is_dir() => {
107                let mut raw_entries = Vec::new();
108                for entry in path.read_dir()? {
109                    let p = entry?.path();
110                    let mut s = p.strip_prefix(&path).unwrap_or(&p).display().to_string();
111                    if p.metadata().map(|m| m.is_dir()).unwrap_or_default() {
112                        s.push('/');
113                    }
114                    raw_entries.push(s);
115                }
116                raw_entries.sort_unstable();
117
118                let mut raw = format!("{}\n\n..\n", path.display());
119                raw.push_str(&raw_entries.join("\n"));
120
121                Ok((Self::Directory(path), raw))
122            }
123
124            _ => {
125                let mut raw = match fs::read_to_string(&path) {
126                    Ok(contents) => normalize_line_endings(contents),
127                    Err(e) if e.kind() == ErrorKind::NotFound => String::new(),
128                    Err(e) => return Err(e),
129                };
130
131                if raw.ends_with('\n') {
132                    raw.pop();
133                }
134
135                Ok((Self::File(path), raw))
136            }
137        }
138    }
139}
140
141/// Internal state for a text buffer backed by a file on disk
142#[derive(Debug)]
143pub struct Buffer {
144    pub(crate) id: usize,
145    pub(crate) kind: BufferKind,
146    pub(crate) dot: Dot,
147    pub(crate) xdot: Dot,
148    pub(crate) txt: GapBuffer,
149    pub(crate) cached_rx: usize,
150    pub(crate) last_save: SystemTime,
151    pub(crate) dirty: bool,
152    pub(crate) input_filter: Option<InputFilter>,
153    pub(crate) tokenizer: Option<Tokenizer>,
154    edit_log: EditLog,
155}
156
157impl Buffer {
158    /// As the name implies, this method MUST be called with the full cannonical file path
159    pub(super) fn new_from_canonical_file_path(id: usize, path: PathBuf) -> io::Result<Self> {
160        let (kind, raw) = BufferKind::try_kind_and_content_from_path(path.clone())?;
161        let tokenizer = try_tokenizer_for_path(&path, raw.lines().next());
162
163        Ok(Self {
164            id,
165            kind,
166            dot: Dot::default(),
167            xdot: Dot::default(),
168            txt: GapBuffer::from(raw),
169            cached_rx: 0,
170            last_save: SystemTime::now(),
171            dirty: false,
172            edit_log: EditLog::default(),
173            tokenizer,
174            input_filter: None,
175        })
176    }
177
178    pub(crate) fn state_changed_on_disk(&self) -> Result<bool, String> {
179        fn inner(p: &Path, last_save: SystemTime) -> io::Result<bool> {
180            let modified = p.metadata()?.modified()?;
181            Ok(modified > last_save)
182        }
183
184        let path = match &self.kind {
185            BufferKind::File(p) => p,
186            _ => return Ok(false),
187        };
188
189        match inner(path, self.last_save) {
190            Ok(modified) => Ok(modified),
191            Err(e) if e.kind() == ErrorKind::NotFound => Ok(false),
192            Err(e) => Err(format!("Error checking file state: {e}")),
193        }
194    }
195
196    pub(crate) fn save_to_disk_at(&mut self, path: PathBuf, force: bool) -> String {
197        if !self.dirty {
198            return "Nothing to save".to_string();
199        }
200
201        if !force {
202            match self.state_changed_on_disk() {
203                Ok(false) => (),
204                Ok(true) => return "File modified on disk, use :w! to force".to_string(),
205                Err(s) => return s,
206            }
207        }
208
209        let contents = self.contents();
210        let n_lines = self.len_lines();
211        let display_path = match path.canonicalize() {
212            Ok(cp) => cp.display().to_string(),
213            Err(_) => path.display().to_string(),
214        };
215        let n_bytes = contents.len();
216
217        match fs::write(path, contents) {
218            Ok(_) => {
219                self.dirty = false;
220                self.last_save = SystemTime::now();
221                format!("\"{display_path}\" {n_lines}L {n_bytes}B written")
222            }
223            Err(e) => format!("Unable to save buffer: {e}"),
224        }
225    }
226
227    pub(super) fn reload_from_disk(&mut self) -> String {
228        let path = match &self.kind {
229            BufferKind::File(p) | BufferKind::Directory(p) => p,
230            _ => return "Buffer is not backed by a file on disk".to_string(),
231        };
232
233        debug!(id=%self.id, path=%path.as_os_str().to_string_lossy(), "reloading buffer state from disk");
234        let raw = match BufferKind::try_kind_and_content_from_path(path.to_path_buf()) {
235            Ok((_, raw)) => raw,
236            Err(e) => return format!("Error reloading buffer: {e}"),
237        };
238
239        let n_chars = raw.len();
240        self.txt = GapBuffer::from(raw);
241        self.dot.clamp_idx(n_chars);
242        self.xdot.clamp_idx(n_chars);
243        self.edit_log.clear();
244        self.dirty = false;
245        self.last_save = SystemTime::now();
246
247        let n_lines = self.txt.len_lines();
248        let n_bytes = self.txt.len();
249        debug!(%n_bytes, "reloaded buffer content");
250
251        let display_path = match path.canonicalize() {
252            Ok(cp) => cp.display().to_string(),
253            Err(_) => path.display().to_string(),
254        };
255
256        format!("\"{display_path}\" {n_lines}L {n_bytes}B loaded")
257    }
258
259    pub(super) fn new_minibuffer() -> Self {
260        Self {
261            id: usize::MAX,
262            kind: BufferKind::MiniBuffer,
263            dot: Default::default(),
264            xdot: Default::default(),
265            txt: GapBuffer::from(""),
266            cached_rx: 0,
267            last_save: SystemTime::now(),
268            dirty: false,
269            edit_log: Default::default(),
270            tokenizer: None,
271            input_filter: None,
272        }
273    }
274
275    /// Create a new unnamed buffer with the given content
276    pub fn new_unnamed(id: usize, content: &str) -> Self {
277        Self {
278            id,
279            kind: BufferKind::Unnamed,
280            dot: Dot::default(),
281            xdot: Dot::default(),
282            txt: GapBuffer::from(normalize_line_endings(content.to_string())),
283            cached_rx: 0,
284            last_save: SystemTime::now(),
285            dirty: false,
286            edit_log: EditLog::default(),
287            tokenizer: None,
288            input_filter: None,
289        }
290    }
291
292    /// Create a new virtual buffer with the given name and content.
293    ///
294    /// The buffer will not be included in the virtual filesystem and it will be removed when it
295    /// loses focus.
296    pub fn new_virtual(id: usize, name: impl Into<String>, content: impl Into<String>) -> Self {
297        let mut content = normalize_line_endings(content.into());
298        if content.ends_with('\n') {
299            content.pop();
300        }
301
302        Self {
303            id,
304            kind: BufferKind::Virtual(name.into()),
305            dot: Dot::default(),
306            xdot: Dot::default(),
307            txt: GapBuffer::from(content),
308            cached_rx: 0,
309            last_save: SystemTime::now(),
310            dirty: false,
311            edit_log: EditLog::default(),
312            tokenizer: None,
313            input_filter: None,
314        }
315    }
316
317    /// Construct a new +output buffer with the given name which must be a valid output buffer name
318    /// of the form '$dir/+output'.
319    pub(super) fn new_output(id: usize, name: String, content: String) -> Self {
320        Self {
321            id,
322            kind: BufferKind::Output(name),
323            dot: Dot::default(),
324            xdot: Dot::default(),
325            txt: GapBuffer::from(normalize_line_endings(content)),
326            cached_rx: 0,
327            last_save: SystemTime::now(),
328            dirty: false,
329            edit_log: EditLog::default(),
330            tokenizer: None,
331            input_filter: None,
332        }
333    }
334
335    /// Short name for displaying in the status line
336    pub fn display_name(&self) -> String {
337        let s = self.kind.display_name();
338
339        s[0..min(MAX_NAME_LEN, s.len())].to_string()
340    }
341
342    /// Absolute path of full name of a virtual buffer
343    pub fn full_name(&self) -> &str {
344        match &self.kind {
345            BufferKind::File(p) => p.to_str().expect("valid unicode"),
346            BufferKind::Directory(p) => p.to_str().expect("valid unicode"),
347            BufferKind::Virtual(s) => s,
348            BufferKind::Output(s) => s,
349            BufferKind::Unnamed => UNNAMED_BUFFER,
350            BufferKind::MiniBuffer => "*mini-buffer*",
351        }
352    }
353
354    /// The directory containing the file backing this buffer so long as it has kind `File`.
355    pub fn dir(&self) -> Option<&Path> {
356        self.kind.dir()
357    }
358
359    /// The key for the +output buffer that output from command run from this buffer should be
360    /// redirected to
361    pub fn output_file_key(&self, cwd: &Path) -> String {
362        self.kind.output_file_key(cwd)
363    }
364
365    /// Check whether or not this is an unnamed buffer
366    pub fn is_unnamed(&self) -> bool {
367        self.kind == BufferKind::Unnamed
368    }
369
370    /// The raw binary contents of this buffer
371    pub fn contents(&self) -> Vec<u8> {
372        let mut contents: Vec<u8> = self.txt.bytes();
373        contents.push(b'\n');
374
375        contents
376    }
377
378    /// The utf-8 string contents of this buffer
379    pub fn str_contents(&self) -> String {
380        let mut s = self.txt.to_string();
381        s.push('\n');
382        s
383    }
384
385    pub(crate) fn string_lines(&self) -> Vec<String> {
386        self.txt
387            .iter_lines()
388            .map(|l| {
389                let mut s = l.to_string();
390                if s.ends_with('\n') {
391                    s.pop();
392                }
393                s
394            })
395            .collect()
396    }
397
398    /// The contents of the current [Dot].
399    pub fn dot_contents(&self) -> String {
400        self.dot.content(self)
401    }
402
403    /// The address of the current [Dot].
404    pub fn addr(&self) -> String {
405        self.dot.addr(self)
406    }
407
408    /// The contents of the current xdot.
409    ///
410    /// This is a virtual dot that is only made use of through the filesystem interface.
411    pub fn xdot_contents(&self) -> String {
412        self.xdot.content(self)
413    }
414
415    /// The address of the current xdot.
416    ///
417    /// This is a virtual dot that is only made use of through the filesystem interface.
418    pub fn xaddr(&self) -> String {
419        self.xdot.addr(self)
420    }
421
422    /// The number of lines currently held in the buffer.
423    #[inline]
424    pub fn len_lines(&self) -> usize {
425        self.txt.len_lines()
426    }
427
428    /// Whether or not the buffer is empty.
429    ///
430    /// # Note
431    /// This does not always imply that the underlying buffer is zero sized, only that the visible
432    /// contents are empty.
433    #[inline]
434    pub fn is_empty(&self) -> bool {
435        self.txt.len_chars() == 0
436    }
437
438    pub(crate) fn debug_edit_log(&self) -> Vec<String> {
439        self.edit_log.debug_edits(self)
440    }
441
442    pub(crate) fn x_from_rx(&self, y: usize) -> usize {
443        self.x_from_provided_rx(y, self.cached_rx)
444    }
445
446    pub(crate) fn x_from_provided_rx(&self, y: usize, buf_rx: usize) -> usize {
447        if self.is_empty() {
448            return 0;
449        }
450
451        let mut rx = 0;
452        let mut cx = 0;
453        let tabstop = config_handle!().tabstop;
454
455        for c in self.txt.line(y).chars() {
456            if c == '\n' {
457                break;
458            }
459
460            if c == '\t' {
461                rx += (tabstop - 1) - (rx % tabstop);
462            }
463            rx += 1;
464
465            if rx > buf_rx {
466                break;
467            }
468            cx += 1;
469        }
470
471        cx
472    }
473
474    /// The line at the requested index returned as a [Slice].
475    pub fn line(&self, y: usize) -> Option<Slice<'_>> {
476        if y >= self.len_lines() {
477            None
478        } else {
479            Some(self.txt.line(y))
480        }
481    }
482
483    /// Attempt to expand from the given cursor position so long as either the previous or next
484    /// character in the buffer is a known delimiter.
485    pub(crate) fn try_expand_delimited(&mut self) {
486        let current_index = match self.dot {
487            Dot::Cur { c: Cur { idx } } => idx,
488            Dot::Range { .. } => return,
489        };
490
491        let prev = if current_index == 0 {
492            None
493        } else {
494            self.txt.get_char(current_index - 1)
495        };
496        let next = self.txt.get_char(current_index + 1);
497
498        let (l, r) = match (prev, next) {
499            (Some('\n'), _) | (_, Some('\n')) => ('\n', '\n'),
500            (Some('('), _) | (_, Some(')')) => ('(', ')'),
501            (Some('['), _) | (_, Some(']')) => ('[', ']'),
502            (Some('{'), _) | (_, Some('}')) => ('{', '}'),
503            (Some('<'), _) | (_, Some('>')) => ('<', '>'),
504            (Some('"'), _) | (_, Some('"')) => ('"', '"'),
505            (Some('\''), _) | (_, Some('\'')) => ('\'', '\''),
506            (Some(' '), _) | (_, Some(' ')) => (' ', ' '),
507
508            _ => return self.expand_cur_dot(),
509        };
510
511        self.set_dot(TextObject::Delimited(l, r), 1);
512    }
513
514    /// If the current dot is a cursor rather than a range, expand it to a sensible range.
515    ///
516    /// This is modeled after (but not identical to) the behaviour in acme's `expand` function
517    /// found in look.c
518    pub(crate) fn expand_cur_dot(&mut self) {
519        let current_index = match self.dot {
520            Dot::Cur { c: Cur { idx } } => idx,
521            Dot::Range { .. } => return,
522        };
523
524        if let Some(dot) = self.try_expand_known(current_index) {
525            self.dot = dot;
526            return;
527        }
528
529        // Expand until we hit non-alphanumeric characters on each sides
530        let (mut from, mut to) = (current_index, current_index);
531        for (i, ch) in self.iter_between(current_index, self.txt.len_chars()) {
532            if !(ch == '_' || ch.is_alphanumeric()) {
533                break;
534            }
535            to = i;
536        }
537
538        for (i, ch) in self.rev_iter_between(current_index, 0) {
539            if !(ch == '_' || ch.is_alphanumeric()) {
540                break;
541            }
542            from = i;
543        }
544
545        self.dot = Dot::from_char_indices(from, to);
546    }
547
548    /// Try to be smart about expanding from the current cursor position to something that we
549    /// understand how to parse:
550    ///   - file path with addr (some/path:addr)
551    ///   - file path
552    ///   - url
553    fn try_expand_known(&self, current_index: usize) -> Option<Dot> {
554        let (mut from, mut to) = (current_index, current_index);
555        let mut colon: Option<usize> = None;
556        let n_chars = self.txt.len_chars();
557
558        let is_file_char = |ch: char| ch.is_alphanumeric() || "._-+/:@".contains(ch);
559        let is_addr_char = |ch: char| "+-/$.#,;?".contains(ch);
560        let is_url_char = |ch: char| "?&=".contains(ch);
561        let has_url_prefix = |i: usize| {
562            let http = i > 4 && i + 3 <= n_chars && self.txt.slice(i - 4, i + 3) == HTTP;
563            let https = i > 5 && i + 3 <= n_chars && self.txt.slice(i - 5, i + 3) == HTTPS;
564
565            http || https
566        };
567
568        // Start by expanding to cover things that are candidates for being file names (optionally
569        // with a following address) or URLs
570        for (i, ch) in self.iter_between(current_index, self.txt.len_chars()) {
571            if !is_file_char(ch) {
572                break;
573            }
574            if ch == ':' && !has_url_prefix(i) {
575                colon = Some(i);
576                break;
577            }
578            to = i;
579        }
580
581        for (i, ch) in self.rev_iter_between(current_index, 0) {
582            if !(is_file_char(ch) || is_url_char(ch) || is_addr_char(ch)) {
583                break;
584            }
585            if colon.is_none() && ch == ':' && !has_url_prefix(i) {
586                colon = Some(i);
587            }
588            from = i;
589        }
590
591        // Now grab the address if we had a trailing colon
592        if let Some(ix) = colon {
593            to = ix;
594            for (_, ch) in self.iter_between(ix + 1, self.txt.len_chars()) {
595                if ch.is_whitespace() || "()[]{}<>;".contains(ch) {
596                    break;
597                }
598                to += 1;
599            }
600        }
601
602        let dot_content = self.txt.slice(from, to + 1).to_string();
603
604        // If dot looks like a URL then expand until whitespace and strip trailing punctuation
605        if dot_content.starts_with(HTTP) || dot_content.starts_with(HTTPS) {
606            if to < self.txt.len_chars() {
607                for (_, ch) in self.iter_between(to + 1, self.txt.len_chars()) {
608                    if ch.is_whitespace() || "()[]{}<>;".contains(ch) {
609                        break;
610                    }
611                    to += 1;
612                }
613            }
614
615            if dot_content.ends_with('.') {
616                to -= 1;
617            }
618
619            return Some(Dot::from_char_indices(from, to));
620        }
621
622        let dot = Dot::from_char_indices(from, to);
623
624        // If dot up until ':' is a file then return the entire dot
625        let fname = match dot_content.split_once(':') {
626            Some((fname, _)) => fname,
627            None => &dot_content,
628        };
629
630        let path = Path::new(fname);
631        if path.is_absolute() && path.exists() {
632            return Some(dot);
633        } else if let Some(dir) = self.dir() {
634            if dir.join(path).exists() {
635                return Some(dot);
636            }
637        }
638
639        // Not a file or a URL
640        None
641    }
642
643    pub(crate) fn sign_col_dims(&self) -> (usize, usize) {
644        let w_lnum = n_digits(self.len_lines());
645        let w_sgncol = w_lnum + 2;
646
647        (w_lnum, w_sgncol)
648    }
649
650    pub(crate) fn append(&mut self, s: String, source: Source) {
651        let dot = self.dot;
652        self.set_dot(TextObject::BufferEnd, 1);
653        self.handle_action(Action::InsertString { s }, source);
654        self.dot = dot;
655        self.dot.clamp_idx(self.txt.len_chars());
656        self.xdot.clamp_idx(self.txt.len_chars());
657    }
658
659    /// The error result of this function is an error string that should be displayed to the user
660    pub(crate) fn handle_action(&mut self, a: Action, source: Source) -> Option<ActionOutcome> {
661        match a {
662            Action::Delete => {
663                let (c, deleted) = self.delete_dot(self.dot, Some(source));
664                self.dot = Dot::Cur { c };
665                self.dot.clamp_idx(self.txt.len_chars());
666                self.xdot.clamp_idx(self.txt.len_chars());
667                return deleted.map(ActionOutcome::SetClipboard);
668            }
669            Action::InsertChar { c } => {
670                let (c, _) = self.insert_char(self.dot, c, Some(source));
671                self.dot = Dot::Cur { c };
672                self.dot.clamp_idx(self.txt.len_chars());
673                self.xdot.clamp_idx(self.txt.len_chars());
674                return None;
675            }
676            Action::InsertString { s } => {
677                let (c, _) = self.insert_string(self.dot, s, Some(source));
678                self.dot = Dot::Cur { c };
679                self.dot.clamp_idx(self.txt.len_chars());
680                self.xdot.clamp_idx(self.txt.len_chars());
681                return None;
682            }
683
684            Action::Redo => return self.redo(),
685            Action::Undo => return self.undo(),
686
687            Action::DotCollapseFirst => self.dot = self.dot.collapse_to_first_cur(),
688            Action::DotCollapseLast => self.dot = self.dot.collapse_to_last_cur(),
689            Action::DotExtendBackward(tobj, count) => self.extend_dot_backward(tobj, count),
690            Action::DotExtendForward(tobj, count) => self.extend_dot_forward(tobj, count),
691            Action::DotFlip => self.dot.flip(),
692            Action::DotSet(t, count) => self.set_dot(t, count),
693
694            Action::RawInput { i } => return self.handle_raw_input(i),
695
696            _ => (),
697        }
698
699        None
700    }
701
702    fn handle_raw_input(&mut self, k: Input) -> Option<ActionOutcome> {
703        let (match_indent, expand_tab, tabstop) = {
704            let conf = config_handle!();
705            (conf.match_indent, conf.expand_tab, conf.tabstop)
706        };
707
708        match k {
709            Input::Return => {
710                let prefix = if match_indent {
711                    let cur = self.dot.first_cur();
712                    let y = self.txt.char_to_line(cur.idx);
713                    let line = self.txt.line(y).to_string();
714                    line.find(|c: char| !c.is_whitespace())
715                        .map(|ix| line.split_at(ix).0.to_string())
716                } else {
717                    None
718                };
719
720                let (c, _) = self.insert_char(self.dot, '\n', Some(Source::Keyboard));
721                let c = match prefix {
722                    Some(s) => self.insert_string(Dot::Cur { c }, s, None).0,
723                    None => c,
724                };
725
726                self.dot = Dot::Cur { c };
727                return None;
728            }
729
730            Input::Tab => {
731                let (c, _) = if expand_tab {
732                    self.insert_string(self.dot, " ".repeat(tabstop), Some(Source::Keyboard))
733                } else {
734                    self.insert_char(self.dot, '\t', Some(Source::Keyboard))
735                };
736
737                self.dot = Dot::Cur { c };
738                return None;
739            }
740
741            Input::Char(ch) => {
742                let (c, _) = self.insert_char(self.dot, ch, Some(Source::Keyboard));
743                self.dot = Dot::Cur { c };
744                return None;
745            }
746
747            Input::Arrow(arr) => self.set_dot(TextObject::Arr(arr), 1),
748
749            _ => (),
750        }
751
752        None
753    }
754
755    /// Set dot and clamp to ensure it is within bounds
756    pub(crate) fn set_dot(&mut self, t: TextObject, n: usize) {
757        for _ in 0..n {
758            t.set_dot(self);
759        }
760        self.dot.clamp_idx(self.txt.len_chars());
761        self.xdot.clamp_idx(self.txt.len_chars());
762    }
763
764    /// Extend dot foward and clamp to ensure it is within bounds
765    fn extend_dot_forward(&mut self, t: TextObject, n: usize) {
766        for _ in 0..n {
767            t.extend_dot_forward(self);
768        }
769        self.dot.clamp_idx(self.txt.len_chars());
770        self.xdot.clamp_idx(self.txt.len_chars());
771    }
772
773    /// Extend dot backward and clamp to ensure it is within bounds
774    fn extend_dot_backward(&mut self, t: TextObject, n: usize) {
775        for _ in 0..n {
776            t.extend_dot_backward(self);
777        }
778        self.dot.clamp_idx(self.txt.len_chars());
779        self.xdot.clamp_idx(self.txt.len_chars());
780    }
781
782    pub(crate) fn new_edit_log_transaction(&mut self) {
783        self.edit_log.new_transaction()
784    }
785
786    fn undo(&mut self) -> Option<ActionOutcome> {
787        match self.edit_log.undo() {
788            Some(edits) => {
789                self.edit_log.paused = true;
790                for edit in edits.into_iter() {
791                    self.apply_edit(edit);
792                }
793                self.edit_log.paused = false;
794                self.dirty = !self.edit_log.is_empty();
795                None
796            }
797            None => Some(ActionOutcome::SetStatusMessage(
798                "Nothing to undo".to_string(),
799            )),
800        }
801    }
802
803    fn redo(&mut self) -> Option<ActionOutcome> {
804        match self.edit_log.redo() {
805            Some(edits) => {
806                self.edit_log.paused = true;
807                for edit in edits.into_iter() {
808                    self.apply_edit(edit);
809                }
810                self.edit_log.paused = false;
811                None
812            }
813            None => Some(ActionOutcome::SetStatusMessage(
814                "Nothing to redo".to_string(),
815            )),
816        }
817    }
818
819    fn apply_edit(&mut self, Edit { kind, cur, txt }: Edit) {
820        let new_cur = match (kind, txt) {
821            (Kind::Insert, Txt::Char(c)) => self.insert_char(Dot::Cur { c: cur }, c, None).0,
822            (Kind::Insert, Txt::String(s)) => self.insert_string(Dot::Cur { c: cur }, s, None).0,
823            (Kind::Delete, Txt::Char(_)) => self.delete_dot(Dot::Cur { c: cur }, None).0,
824            (Kind::Delete, Txt::String(s)) => {
825                let start_idx = cur.idx;
826                let end_idx = (start_idx + s.chars().count()).saturating_sub(1);
827                let end = Cur { idx: end_idx };
828                self.delete_dot(
829                    Dot::Range {
830                        r: Range::from_cursors(cur, end, true),
831                    }
832                    .collapse_null_range(),
833                    None,
834                )
835                .0
836            }
837        };
838
839        self.dot = Dot::Cur { c: new_cur };
840    }
841
842    /// Only files get marked as dirty to ensure that they are prompted for saving before being
843    /// closed.
844    fn mark_dirty(&mut self) {
845        self.dirty = self.kind.is_file();
846    }
847
848    /// Returns true if a filter was present and the notification was sent
849    pub(crate) fn notify_load(&self, source: Source) -> bool {
850        match self.input_filter.as_ref() {
851            Some(f) => {
852                let (ch_from, ch_to) = self.dot.as_char_indices();
853                let txt = self.dot.content(self);
854                f.notify_load(source, ch_from, ch_to, &txt);
855                true
856            }
857            None => false,
858        }
859    }
860
861    /// Returns true if a filter was present and the notification was sent
862    pub(crate) fn notify_execute(&self, source: Source, arg: Option<(Range, String)>) -> bool {
863        match self.input_filter.as_ref() {
864            Some(f) => {
865                let (ch_from, ch_to) = self.dot.as_char_indices();
866                let txt = self.dot.content(self);
867                f.notify_execute(source, ch_from, ch_to, &txt, arg);
868                true
869            }
870            None => false,
871        }
872    }
873
874    fn insert_char(&mut self, dot: Dot, ch: char, source: Option<Source>) -> (Cur, Option<String>) {
875        let ch = if ch == '\r' { '\n' } else { ch };
876        let (cur, deleted) = match dot {
877            Dot::Cur { c } => (c, None),
878            Dot::Range { r } => self.delete_range(r, source),
879        };
880
881        let idx = cur.idx;
882        self.txt.insert_char(idx, ch);
883
884        if let (Some(source), Some(f)) = (source, self.input_filter.as_ref()) {
885            f.notify_insert(source, idx, idx + 1, &ch.to_string());
886        }
887
888        self.edit_log.insert_char(cur, ch);
889        self.mark_dirty();
890
891        (Cur { idx: idx + 1 }, deleted)
892    }
893
894    fn insert_string(
895        &mut self,
896        dot: Dot,
897        s: String,
898        source: Option<Source>,
899    ) -> (Cur, Option<String>) {
900        let s = normalize_line_endings(s);
901        let (mut cur, deleted) = match dot {
902            Dot::Cur { c } => (c, None),
903            Dot::Range { r } => self.delete_range(r, source),
904        };
905
906        // Inserting an empty string should not be recorded as an edit (and is
907        // a no-op for the content of self.txt) but we support it as inserting
908        // an empty string while dot is a range has the same effect as a delete.
909        if !s.is_empty() {
910            let idx = cur.idx;
911            let len = s.chars().count();
912            self.txt.insert_str(idx, &s);
913
914            if let (Some(source), Some(f)) = (source, self.input_filter.as_ref()) {
915                f.notify_insert(source, idx, idx + len, &s);
916            }
917
918            self.edit_log.insert_string(cur, s);
919            cur.idx += len;
920        }
921
922        self.mark_dirty();
923
924        (cur, deleted)
925    }
926
927    fn delete_dot(&mut self, dot: Dot, source: Option<Source>) -> (Cur, Option<String>) {
928        let (cur, deleted) = match dot {
929            Dot::Cur { c } => (self.delete_cur(c, source), None),
930            Dot::Range { r } => self.delete_range(r, source),
931        };
932
933        (cur, deleted)
934    }
935
936    fn delete_cur(&mut self, cur: Cur, source: Option<Source>) -> Cur {
937        let idx = cur.idx;
938        if idx < self.txt.len_chars() {
939            let ch = self.txt.char(idx);
940            self.txt.remove_char(idx);
941
942            if let (Some(source), Some(f)) = (source, self.input_filter.as_ref()) {
943                f.notify_delete(source, idx, idx + 1);
944            }
945
946            self.edit_log.delete_char(cur, ch);
947            self.mark_dirty();
948        }
949
950        cur
951    }
952
953    fn delete_range(&mut self, r: Range, source: Option<Source>) -> (Cur, Option<String>) {
954        let (from, to) = if r.start.idx != r.end.idx {
955            (r.start.idx, min(r.end.idx + 1, self.txt.len_chars()))
956        } else {
957            return (r.start, None);
958        };
959
960        let s = self.txt.slice(from, to).to_string();
961        self.txt.remove_range(from, to);
962
963        if let (Some(source), Some(f)) = (source, self.input_filter.as_ref()) {
964            f.notify_delete(source, from, to);
965        }
966
967        self.edit_log.delete_string(r.start, s.clone());
968        self.mark_dirty();
969
970        (r.start, Some(s))
971    }
972
973    pub(crate) fn find_forward(&mut self, s: &str) {
974        if let Some(dot) = find_forward_wrapping(&s, self) {
975            self.dot = dot;
976        }
977    }
978}
979
980fn n_digits(mut n: usize) -> usize {
981    if n == 0 {
982        return 1;
983    }
984
985    let mut digits = 0;
986    while n != 0 {
987        digits += 1;
988        n /= 10;
989    }
990
991    digits
992}
993
994#[cfg(test)]
995pub(crate) mod tests {
996    use super::*;
997    use crate::key::Arrow;
998    use edit::tests::{del_c, del_s, in_c, in_s};
999    use simple_test_case::test_case;
1000    use std::env;
1001
1002    const LINE_1: &str = "This is a test";
1003    const LINE_2: &str = "involving multiple lines";
1004
1005    #[test_case(0, 1; "n0")]
1006    #[test_case(5, 1; "n5")]
1007    #[test_case(10, 2; "n10")]
1008    #[test_case(13, 2; "n13")]
1009    #[test_case(731, 3; "n731")]
1010    #[test_case(930, 3; "n930")]
1011    #[test]
1012    fn n_digits_works(n: usize, digits: usize) {
1013        assert_eq!(n_digits(n), digits);
1014    }
1015
1016    pub fn buffer_from_lines(lines: &[&str]) -> Buffer {
1017        let mut b = Buffer::new_unnamed(0, "");
1018        let s = lines.join("\n");
1019
1020        for c in s.chars() {
1021            b.handle_action(Action::InsertChar { c }, Source::Keyboard);
1022        }
1023
1024        b
1025    }
1026
1027    fn simple_initial_buffer() -> Buffer {
1028        buffer_from_lines(&[LINE_1, LINE_2])
1029    }
1030
1031    #[test]
1032    fn simple_insert_works() {
1033        let b = simple_initial_buffer();
1034        let c = Cur::from_yx(1, LINE_2.len(), &b);
1035        let lines = b.string_lines();
1036
1037        assert_eq!(lines.len(), 2);
1038        assert_eq!(lines[0], LINE_1);
1039        assert_eq!(lines[1], LINE_2);
1040        assert_eq!(b.dot, Dot::Cur { c });
1041        assert_eq!(
1042            b.edit_log.edits,
1043            vec![vec![in_s(0, &format!("{LINE_1}\n{LINE_2}"))]]
1044        );
1045    }
1046
1047    #[test]
1048    fn insert_with_moving_dot_works() {
1049        let mut b = Buffer::new_unnamed(0, "");
1050
1051        // Insert from the start of the buffer
1052        for c in "hello w".chars() {
1053            b.handle_action(Action::InsertChar { c }, Source::Keyboard);
1054        }
1055
1056        // move back to insert a character inside of the text we already have
1057        b.handle_action(
1058            Action::DotSet(TextObject::Arr(Arrow::Left), 2),
1059            Source::Keyboard,
1060        );
1061        b.handle_action(Action::InsertChar { c: ',' }, Source::Keyboard);
1062
1063        // move forward to the end of the line to finish inserting
1064        b.handle_action(Action::DotSet(TextObject::LineEnd, 1), Source::Keyboard);
1065        for c in "orld!".chars() {
1066            b.handle_action(Action::InsertChar { c }, Source::Keyboard);
1067        }
1068
1069        // inserted characters should be in the correct positions
1070        assert_eq!(b.txt.to_string(), "hello, world!");
1071    }
1072
1073    #[test_case(
1074        Action::InsertChar { c: 'x' },
1075        in_c(LINE_1.len() + 1, 'x');
1076        "char"
1077    )]
1078    #[test_case(
1079        Action::InsertString { s: "x".to_string() },
1080        in_s(LINE_1.len() + 1, "x");
1081        "string"
1082    )]
1083    #[test]
1084    fn insert_w_range_dot_works(a: Action, edit: Edit) {
1085        let mut b = simple_initial_buffer();
1086        b.handle_action(Action::DotSet(TextObject::Line, 1), Source::Keyboard);
1087
1088        let outcome = b.handle_action(a, Source::Keyboard);
1089        assert_eq!(outcome, None);
1090
1091        let lines = b.string_lines();
1092        assert_eq!(lines.len(), 2);
1093
1094        let c = Cur::from_yx(1, 1, &b);
1095        assert_eq!(b.dot, Dot::Cur { c });
1096
1097        assert_eq!(lines[0], LINE_1);
1098        assert_eq!(lines[1], "x");
1099        assert_eq!(
1100            b.edit_log.edits,
1101            vec![vec![
1102                in_s(0, &format!("{LINE_1}\n{LINE_2}")),
1103                del_s(LINE_1.len() + 1, LINE_2),
1104                edit,
1105            ]]
1106        );
1107    }
1108
1109    #[test]
1110    fn move_forward_at_end_of_buffer_is_fine() {
1111        let mut b = Buffer::new_unnamed(0, "");
1112        b.handle_raw_input(Input::Arrow(Arrow::Right));
1113
1114        let c = Cur { idx: 0 };
1115        assert_eq!(b.dot, Dot::Cur { c });
1116    }
1117
1118    #[test]
1119    fn delete_in_empty_buffer_is_fine() {
1120        let mut b = Buffer::new_unnamed(0, "");
1121        b.handle_action(Action::Delete, Source::Keyboard);
1122        let c = Cur { idx: 0 };
1123        let lines = b.string_lines();
1124
1125        assert_eq!(b.dot, Dot::Cur { c });
1126        assert_eq!(lines.len(), 1);
1127        assert_eq!(lines[0], "");
1128        assert!(b.edit_log.edits.is_empty());
1129    }
1130
1131    #[test]
1132    fn simple_delete_works() {
1133        let mut b = simple_initial_buffer();
1134        b.handle_action(
1135            Action::DotSet(TextObject::Arr(Arrow::Left), 1),
1136            Source::Keyboard,
1137        );
1138        b.handle_action(Action::Delete, Source::Keyboard);
1139
1140        let c = Cur::from_yx(1, LINE_2.len() - 1, &b);
1141        let lines = b.string_lines();
1142
1143        assert_eq!(b.dot, Dot::Cur { c });
1144        assert_eq!(lines.len(), 2);
1145        assert_eq!(lines[0], LINE_1);
1146        assert_eq!(lines[1], "involving multiple line");
1147        assert_eq!(
1148            b.edit_log.edits,
1149            vec![vec![
1150                in_s(0, &format!("{LINE_1}\n{LINE_2}")),
1151                del_c(LINE_1.len() + 24, 's')
1152            ]]
1153        );
1154    }
1155
1156    #[test]
1157    fn delete_range_works() {
1158        let mut b = simple_initial_buffer();
1159        b.handle_action(Action::DotSet(TextObject::Line, 1), Source::Keyboard);
1160        b.handle_action(Action::Delete, Source::Keyboard);
1161
1162        let c = Cur::from_yx(1, 0, &b);
1163        let lines = b.string_lines();
1164
1165        assert_eq!(b.dot, Dot::Cur { c });
1166        assert_eq!(lines.len(), 2);
1167        assert_eq!(lines[0], LINE_1);
1168        assert_eq!(lines[1], "");
1169        assert_eq!(
1170            b.edit_log.edits,
1171            vec![vec![
1172                in_s(0, &format!("{LINE_1}\n{LINE_2}")),
1173                del_s(LINE_1.len() + 1, "involving multiple lines")
1174            ]]
1175        );
1176    }
1177
1178    #[test]
1179    fn delete_undo_works() {
1180        let mut b = simple_initial_buffer();
1181        let original_lines = b.string_lines();
1182        b.new_edit_log_transaction();
1183
1184        b.handle_action(
1185            Action::DotExtendBackward(TextObject::Word, 1),
1186            Source::Keyboard,
1187        );
1188        b.handle_action(Action::Delete, Source::Keyboard);
1189
1190        b.set_dot(TextObject::BufferStart, 1);
1191        b.handle_action(
1192            Action::DotExtendForward(TextObject::Word, 1),
1193            Source::Keyboard,
1194        );
1195        b.handle_action(Action::Delete, Source::Keyboard);
1196
1197        b.handle_action(Action::Undo, Source::Keyboard);
1198
1199        let lines = b.string_lines();
1200
1201        assert_eq!(lines, original_lines);
1202    }
1203
1204    fn c(idx: usize) -> Cur {
1205        Cur { idx }
1206    }
1207
1208    #[test]
1209    fn undo_string_insert_works() {
1210        let initial_content = "foo foo foo\n";
1211        let mut b = Buffer::new_unnamed(0, initial_content);
1212
1213        b.insert_string(Dot::Cur { c: c(0) }, "bar".to_string(), None);
1214        b.handle_action(Action::Undo, Source::Keyboard);
1215
1216        assert_eq!(b.string_lines(), vec!["foo foo foo", ""]);
1217    }
1218
1219    #[test]
1220    fn undo_string_delete_works() {
1221        let initial_content = "foo foo foo\n";
1222        let mut b = Buffer::new_unnamed(0, initial_content);
1223
1224        let r = Range::from_cursors(c(0), c(2), true);
1225        b.delete_dot(Dot::Range { r }, None);
1226        b.handle_action(Action::Undo, Source::Keyboard);
1227
1228        assert_eq!(b.string_lines(), vec!["foo foo foo", ""]);
1229    }
1230
1231    #[test]
1232    fn undo_string_insert_and_delete_works() {
1233        let initial_content = "foo foo foo\n";
1234        let mut b = Buffer::new_unnamed(0, initial_content);
1235
1236        let r = Range::from_cursors(c(0), c(2), true);
1237        b.delete_dot(Dot::Range { r }, None);
1238        b.insert_string(Dot::Cur { c: c(0) }, "bar".to_string(), None);
1239
1240        assert_eq!(b.string_lines(), vec!["bar foo foo", ""]);
1241
1242        b.handle_action(Action::Undo, Source::Keyboard);
1243        b.handle_action(Action::Undo, Source::Keyboard);
1244
1245        assert_eq!(b.string_lines(), vec!["foo foo foo", ""]);
1246    }
1247
1248    // Tests are executed from the root of the crate so existing file paths are relative to there
1249    #[test_case("foo", None; "unknown format")]
1250    #[test_case("someFunc()", None; "camel case function call")]
1251    #[test_case("some_func()", None; "snake case function call")]
1252    #[test_case("not_a_real_file.rs", None; "file that does not exist")]
1253    #[test_case("README.md", Some("README.md"); "file that exists")]
1254    #[test_case("README.md:12,19", Some("README.md:12,19"); "file that exists with addr")]
1255    #[test_case("README.md:12:19", Some("README.md:12:19"); "file that exists with addr containing colon")]
1256    #[test_case("/usr/bin/sh", Some("/usr/bin/sh"); "file that exists abs path")]
1257    #[test_case("/usr/bin/sh:12-+#", Some("/usr/bin/sh:12-+#"); "file that exists abs path with addr")]
1258    #[test_case("http://example.com", Some("http://example.com"); "http url")]
1259    #[test_case("http://example.com/some/path", Some("http://example.com/some/path"); "http url with path")]
1260    #[test_case("http://example.com?foo=1", Some("http://example.com?foo=1"); "http url with query string")]
1261    #[test_case("http://example.com?foo=1&bar=2", Some("http://example.com?foo=1&bar=2"); "http url with multi query string")]
1262    #[test]
1263    fn try_expand_known_works(s: &str, expected: Option<&str>) {
1264        let cwd = env::current_dir().unwrap().display().to_string();
1265        // Check with surrounding whitespace and delimiters
1266        for (l, r) in [(" ", " "), ("(", ")"), ("[", "]"), ("<", ">"), ("{", "}")] {
1267            let b = Buffer::new_output(
1268                0,
1269                format!("{cwd}/+output"),
1270                format!("abc_123 {l}{s}{r}\tmore text"),
1271            );
1272
1273            // Check with the initial cursor position being at any offset within the target
1274            for i in 0..s.len() {
1275                let dot = b.try_expand_known(9 + i);
1276                let maybe_content = dot.map(|d| d.content(&b));
1277                assert_eq!(
1278                    maybe_content.as_deref(),
1279                    expected,
1280                    "failed at offset={i} with lr=({l:?}, {r:?})"
1281                )
1282            }
1283        }
1284    }
1285
1286    #[test_case("\r", "\n"; "CR")]
1287    #[test_case("\n", "\n"; "LF")]
1288    #[test_case("\r\n", "\n"; "CRLF")]
1289    #[test_case("foo\rbar", "foo\nbar"; "text either side of CR")]
1290    #[test_case("foo\nbar", "foo\nbar"; "text either side of LF")]
1291    #[test_case("foo\r\nbar", "foo\nbar"; "text either side of CRLF")]
1292    #[test_case("foo\rbar\nbaz\r\nquux", "foo\nbar\nbaz\nquux"; "mixed line endings")]
1293    #[test]
1294    fn normalizes_line_endings_insert_string(s: &str, expected: &str) {
1295        let mut b = Buffer::new_virtual(0, "test", "");
1296        b.insert_string(Dot::Cur { c: c(0) }, s.to_string(), None);
1297        // we force a trailing newline so account for that as well
1298        assert_eq!(b.str_contents(), format!("{expected}\n"));
1299    }
1300
1301    #[test_case('\r', "\n"; "CR")]
1302    #[test_case('\n', "\n"; "LF")]
1303    #[test_case('a', "a"; "ascii")]
1304    #[test]
1305    fn normalizes_line_endings_insert_char(ch: char, expected: &str) {
1306        let mut b = Buffer::new_virtual(0, "test", "");
1307        b.insert_char(Dot::Cur { c: c(0) }, ch, None);
1308        // we force a trailing newline so account for that as well
1309        assert_eq!(b.str_contents(), format!("{expected}\n"));
1310    }
1311}