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, MAX_NAME_LEN, UNNAMED_BUFFER,
4    config::ftype_config_for_path_and_first_line,
5    config_handle,
6    dot::{Cur, Dot, Range, TextObject, find::find_forward_wrapping},
7    editor::Action,
8    exec::{Addr, Address},
9    fsys::InputFilter,
10    key::Input,
11    lsp::Coords,
12    syntax::{LineIter, SyntaxState},
13    util::normalize_line_endings,
14};
15use ad_event::Source;
16use std::{
17    cmp::min,
18    fs,
19    io::{self, ErrorKind},
20    path::{Path, PathBuf},
21    sync::{
22        Arc, RwLock,
23        atomic::{AtomicUsize, Ordering},
24    },
25    time::SystemTime,
26};
27use tracing::{debug, error};
28
29mod buffers;
30mod edit;
31mod internal;
32
33use edit::{Edit, EditLog, Kind, Txt};
34
35pub use buffers::BufferId;
36pub(crate) use buffers::Buffers;
37pub use internal::{Chars, GapBuffer, IdxChars, Slice, SliceIter};
38
39// Welcome splash message for new users to help them get started (rather than just presenting them
40// with a blank buffer.
41pub(crate) const WELCOME_SQUIRREL: &str = r#"+---------------------------------------------------------------------------+
42| > Welcome to the ad text editor!                                          |
43| This is a temporary buffer where you can make notes and execute commands. |
44| You can press the '-' key to open a file from the current directory, or   |
45| type :help to view the in-editor help documentation.                      |
46|                                                                           |
47| To prevent this message being shown at startup, set the 'show_splash'     |
48| property to false in your config file.                                    |
49+---------------------------------------------------------------------------+
50              \  ,,  __
51                ("\ ( (
52                -)>\ )/
53                ('( )'
54                -'-'"#;
55pub(crate) const DEFAULT_OUTPUT_BUFFER: &str = "+output";
56const HTTPS: &str = "https://";
57const HTTP: &str = "http://";
58
59// Used to inform the editor that further action needs to be taken by it after a Buffer has
60// finished processing a given Action.
61#[derive(Debug, Clone, PartialEq, Eq)]
62pub enum ActionOutcome {
63    SetClipboard(String),
64    SetStatusMessage(String),
65}
66
67/// Buffer kinds control how each buffer interacts with the rest of the editor functionality
68#[derive(Default, Debug, Clone, PartialEq, Eq)]
69pub(crate) enum BufferKind {
70    /// A regular buffer that is backed by a file on disk.
71    File(PathBuf),
72    /// A directory buffer that is modifiable but cannot be saved
73    Directory(PathBuf),
74    /// An in-memory buffer that is not exposed through fsys
75    Virtual(String),
76    /// An in-memory buffer holding output from commands run within a given directory
77    Output(String),
78    /// A currently un-named buffer that can be converted to a File buffer when named
79    #[default]
80    Unnamed,
81    /// State for an active mini-buffer
82    MiniBuffer,
83}
84
85impl BufferKind {
86    fn display_name(&self) -> String {
87        match self {
88            BufferKind::File(p) => p.display().to_string(),
89            BufferKind::Directory(p) => p.display().to_string(),
90            BufferKind::Virtual(s) => s.clone(),
91            BufferKind::Output(s) => s.clone(),
92            BufferKind::Unnamed => UNNAMED_BUFFER.to_string(),
93            BufferKind::MiniBuffer => "".to_string(),
94        }
95    }
96
97    /// The path for the file backing this buffer (if any).
98    fn path(&self) -> Option<&Path> {
99        match &self {
100            BufferKind::File(p) => Some(p.as_ref()),
101            _ => None,
102        }
103    }
104
105    /// The directory containing the file backing this buffer (if any).
106    fn dir(&self) -> Option<&Path> {
107        match &self {
108            BufferKind::File(p) => p.parent(),
109            BufferKind::Directory(p) => Some(p.as_ref()),
110            BufferKind::Output(s) => Path::new(s).parent(),
111            _ => None,
112        }
113    }
114
115    pub(crate) fn is_file(&self) -> bool {
116        matches!(self, Self::File(_))
117    }
118
119    pub(crate) fn is_dir(&self) -> bool {
120        matches!(self, Self::Directory(_))
121    }
122
123    /// The key for the +output buffer that output from command run from this buffer should be
124    /// redirected to
125    pub fn output_file_key(&self, cwd: &Path) -> String {
126        let path = self.dir().unwrap_or(cwd);
127        format!("{}/{DEFAULT_OUTPUT_BUFFER}", path.display())
128    }
129
130    fn try_kind_and_content_from_path(path: PathBuf) -> io::Result<(Self, String)> {
131        match path.metadata() {
132            Ok(m) if m.is_dir() => {
133                let mut raw_entries = Vec::new();
134                for entry in path.read_dir()? {
135                    let p = entry?.path();
136                    let mut s = p.strip_prefix(&path).unwrap_or(&p).display().to_string();
137                    if p.metadata().map(|m| m.is_dir()).unwrap_or_default() {
138                        s.push('/');
139                    }
140                    raw_entries.push(s);
141                }
142                raw_entries.sort_unstable();
143
144                let mut raw = format!("{}\n\n..\n", path.display());
145                raw.push_str(&raw_entries.join("\n"));
146
147                Ok((Self::Directory(path), raw))
148            }
149
150            _ => {
151                let raw = match fs::read_to_string(&path) {
152                    Ok(contents) => normalize_line_endings(contents),
153                    Err(e) if e.kind() == ErrorKind::NotFound => String::new(),
154                    Err(e) => return Err(e),
155                };
156
157                Ok((Self::File(path), raw))
158            }
159        }
160    }
161}
162
163/// Internal state for a text buffer backed by a file on disk
164#[derive(Debug)]
165pub struct Buffer {
166    pub(crate) id: usize,
167    pub(crate) kind: BufferKind,
168    pub(crate) dot: Dot,
169    pub(crate) xdot: Dot,
170    pub(crate) txt: GapBuffer,
171    pub(crate) cached_rx: usize,
172    pub(crate) last_save: SystemTime,
173    pub(crate) dirty: bool,
174    pub(crate) changed_since_last_render: bool,
175    pub(crate) input_filter: Option<InputFilter>,
176    pub(crate) syntax_state: Option<SyntaxState>,
177    config: Arc<RwLock<Config>>,
178    version: AtomicUsize,
179    edit_log: EditLog,
180}
181
182impl Buffer {
183    /// As the name implies, this method MUST be called with the full canonical file path
184    pub fn new_from_canonical_file_path(
185        id: usize,
186        path: PathBuf,
187        config: Arc<RwLock<Config>>,
188    ) -> io::Result<Self> {
189        let (kind, raw) = BufferKind::try_kind_and_content_from_path(path.clone())?;
190        let mut b = Self {
191            id,
192            kind,
193            dot: Dot::default(),
194            xdot: Dot::default(),
195            txt: GapBuffer::from(raw),
196            cached_rx: 0,
197            last_save: SystemTime::now(),
198            dirty: false,
199            changed_since_last_render: false,
200            input_filter: None,
201            syntax_state: None,
202            config,
203            version: AtomicUsize::new(1),
204            edit_log: EditLog::default(),
205        };
206
207        b.try_set_ts_state();
208
209        Ok(b)
210    }
211
212    /// Create a new unnamed buffer with the given content
213    pub fn new_unnamed(id: usize, content: impl Into<String>, config: Arc<RwLock<Config>>) -> Self {
214        Self {
215            id,
216            kind: BufferKind::Unnamed,
217            dot: Dot::default(),
218            xdot: Dot::default(),
219            txt: GapBuffer::from(normalize_line_endings(content.into())),
220            cached_rx: 0,
221            last_save: SystemTime::now(),
222            dirty: false,
223            changed_since_last_render: false,
224            input_filter: None,
225            syntax_state: None,
226            config,
227            version: AtomicUsize::new(1),
228            edit_log: EditLog::default(),
229        }
230    }
231
232    /// Create a new virtual buffer with the given name and content.
233    ///
234    /// The buffer will not be included in the virtual filesystem and it will be removed when it
235    /// loses focus.
236    pub fn new_virtual(
237        id: usize,
238        name: impl Into<String>,
239        content: impl Into<String>,
240        config: Arc<RwLock<Config>>,
241    ) -> Self {
242        let mut content = normalize_line_endings(content.into());
243        if content.ends_with('\n') {
244            content.pop();
245        }
246
247        Self {
248            id,
249            kind: BufferKind::Virtual(name.into()),
250            dot: Dot::default(),
251            xdot: Dot::default(),
252            txt: GapBuffer::from(content),
253            cached_rx: 0,
254            last_save: SystemTime::now(),
255            dirty: false,
256            changed_since_last_render: false,
257            input_filter: None,
258            syntax_state: None,
259            config,
260            version: AtomicUsize::new(1),
261            edit_log: EditLog::default(),
262        }
263    }
264
265    /// Construct a new +output buffer with the given name which must be a valid output buffer name
266    /// of the form '$dir/+output'.
267    pub(super) fn new_output(
268        id: usize,
269        name: String,
270        content: String,
271        config: Arc<RwLock<Config>>,
272    ) -> Self {
273        Self {
274            id,
275            kind: BufferKind::Output(name),
276            dot: Dot::default(),
277            xdot: Dot::default(),
278            txt: GapBuffer::from(normalize_line_endings(content)),
279            cached_rx: 0,
280            last_save: SystemTime::now(),
281            dirty: false,
282            changed_since_last_render: false,
283            input_filter: None,
284            syntax_state: None,
285            config,
286            version: AtomicUsize::new(1),
287            edit_log: EditLog::default(),
288        }
289    }
290
291    /// Clear any existing tree-sitter state and then attempt to detect and set the state
292    /// based on this buffer's BufferKind
293    fn try_set_ts_state(&mut self) {
294        self.syntax_state = None;
295        if let Some(lang) = self.configured_filetype() {
296            let cfg = config_handle!(self);
297            match SyntaxState::try_new(&lang, &self.txt, &cfg) {
298                Ok(state) => self.syntax_state = Some(state),
299                Err(msg) => error!("unable to initialise syntax state: {msg}"),
300            }
301        }
302    }
303
304    pub(crate) fn next_edit_version(&self) -> usize {
305        self.version.fetch_add(1, Ordering::Relaxed)
306    }
307
308    pub(crate) fn state_changed_on_disk(&self) -> Result<bool, String> {
309        fn inner(p: &Path, last_save: SystemTime) -> io::Result<bool> {
310            let modified = p.metadata()?.modified()?;
311            Ok(modified > last_save)
312        }
313
314        let path = match &self.kind {
315            BufferKind::File(p) => p,
316            _ => return Ok(false),
317        };
318
319        match inner(path, self.last_save) {
320            Ok(modified) => Ok(modified),
321            Err(e) if e.kind() == ErrorKind::NotFound => Ok(false),
322            Err(e) => Err(format!("Error checking file state: {e}")),
323        }
324    }
325
326    /// Set this buffer's filename (an therefore kind) to the provided path.
327    /// -> This will also re-set / clear any tree-sitter state that is currently held
328    pub(crate) fn set_filename<P: AsRef<Path>>(&mut self, path: P) -> Option<ActionOutcome> {
329        let path = match path.as_ref().canonicalize() {
330            Ok(p) => p,
331            Err(e) if e.kind() == ErrorKind::NotFound => path.as_ref().to_path_buf(),
332            Err(e) => {
333                return Some(ActionOutcome::SetStatusMessage(format!(
334                    "invalid file path: {e}"
335                )));
336            }
337        };
338
339        let kind = if path.is_dir() {
340            BufferKind::Directory(path)
341        } else {
342            BufferKind::File(path)
343        };
344
345        self.kind = kind;
346        self.try_set_ts_state();
347        self.changed_since_last_render = true;
348
349        None
350    }
351
352    pub(crate) fn save_to_disk_at(
353        &mut self,
354        path: impl AsRef<Path>,
355        force: bool,
356    ) -> Result<String, String> {
357        if !self.has_trailing_newline() {
358            self.insert_char(TextObject::BufferEnd.as_dot(self), '\n', None);
359        }
360
361        if !self.dirty {
362            return Err("Nothing to save".to_string());
363        }
364
365        if !force {
366            match self.state_changed_on_disk() {
367                Ok(false) => (),
368                Ok(true) => return Err("File modified on disk, use :w! to force".to_string()),
369                Err(s) => return Err(s),
370            }
371        }
372
373        let path = path.as_ref();
374        let n_lines = self.len_lines();
375        let contents = self.txt.make_contiguous();
376        let display_path = match path.canonicalize() {
377            Ok(cp) => cp.display().to_string(),
378            Err(_) => path.display().to_string(),
379        };
380        let n_bytes = contents.len();
381
382        #[cfg(not(feature = "fuzz"))]
383        {
384            match fs::write(path, contents) {
385                Ok(_) => {
386                    self.dirty = false;
387                    self.last_save = SystemTime::now();
388                    Ok(format!("\"{display_path}\" {n_lines}L {n_bytes}B written"))
389                }
390                Err(e) => Err(format!("Unable to save buffer: {e}")),
391            }
392        }
393
394        #[cfg(feature = "fuzz")]
395        {
396            self.dirty = false;
397            self.last_save = SystemTime::now();
398            Ok(format!("\"{display_path}\" {n_lines}L {n_bytes}B written"))
399        }
400    }
401
402    pub(super) fn reload_from_disk(&mut self) -> String {
403        let path = match &self.kind {
404            BufferKind::File(p) | BufferKind::Directory(p) => p,
405            _ => return "Buffer is not backed by a file on disk".to_string(),
406        };
407
408        debug!(id=%self.id, path=%path.as_os_str().to_string_lossy(), "reloading buffer state from disk");
409        let raw = match BufferKind::try_kind_and_content_from_path(path.to_path_buf()) {
410            Ok((_, raw)) => raw,
411            Err(e) => return format!("Error reloading buffer: {e}"),
412        };
413
414        let n_chars = raw.len();
415        self.txt = GapBuffer::from(raw);
416        self.dot.clamp_idx(n_chars);
417        self.xdot.clamp_idx(n_chars);
418        self.edit_log.clear();
419        self.dirty = false;
420        self.changed_since_last_render = true;
421        self.last_save = SystemTime::now();
422
423        let n_lines = self.txt.len_lines();
424        let n_bytes = self.txt.len();
425        debug!(%n_bytes, "reloaded buffer content");
426
427        let display_path = match path.canonicalize() {
428            Ok(cp) => cp.display().to_string(),
429            Err(_) => path.display().to_string(),
430        };
431
432        format!("\"{display_path}\" {n_lines}L {n_bytes}B loaded")
433    }
434
435    pub(super) fn new_minibuffer(config: Arc<RwLock<Config>>) -> Self {
436        Self {
437            id: usize::MAX,
438            kind: BufferKind::MiniBuffer,
439            dot: Default::default(),
440            xdot: Default::default(),
441            txt: GapBuffer::from(""),
442            cached_rx: 0,
443            last_save: SystemTime::now(),
444            dirty: false,
445            changed_since_last_render: false,
446            input_filter: None,
447            syntax_state: None,
448            config,
449            version: AtomicUsize::new(1),
450            edit_log: Default::default(),
451        }
452    }
453
454    /// Short name for displaying in the status line
455    pub fn display_name(&self) -> String {
456        let s = self.kind.display_name();
457
458        s[0..min(MAX_NAME_LEN, s.len())].to_string()
459    }
460
461    /// Absolute path of full name of a virtual buffer
462    pub fn full_name(&self) -> &str {
463        match &self.kind {
464            BufferKind::File(p) => p.to_str().expect("valid unicode"),
465            BufferKind::Directory(p) => p.to_str().expect("valid unicode"),
466            BufferKind::Virtual(s) => s,
467            BufferKind::Output(s) => s,
468            BufferKind::Unnamed => UNNAMED_BUFFER,
469            BufferKind::MiniBuffer => "*mini-buffer*",
470        }
471    }
472
473    /// The directory containing the file backing this buffer (if any).
474    pub fn dir(&self) -> Option<&Path> {
475        self.kind.dir()
476    }
477
478    /// The path for the file backing this buffer (if any).
479    pub fn path(&self) -> Option<&Path> {
480        self.kind.path()
481    }
482
483    /// The key for the +output buffer that output from command run from this buffer should be
484    /// redirected to
485    pub fn output_file_key(&self, cwd: &Path) -> String {
486        self.kind.output_file_key(cwd)
487    }
488
489    /// Check whether or not this is an unnamed buffer
490    pub fn is_unnamed(&self) -> bool {
491        self.kind == BufferKind::Unnamed
492    }
493
494    /// Whether or not the contents of the buffer end with a final newline character.
495    ///
496    /// POSIX semantics define a line as "A sequence of zero or more non-newline characters plus
497    /// a terminating newline character."
498    ///
499    /// See: <https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap03.html#tag_03_206>
500    pub fn has_trailing_newline(&self) -> bool {
501        self.txt.has_trailing_newline()
502    }
503
504    /// Check the current [Config] to see if this buffer matches a known filetype configuration.
505    pub fn configured_filetype(&self) -> Option<String> {
506        let lang_configs = &config_handle!(self).filetypes;
507        let first_line = self.line(0).map(|l| l.to_string()).unwrap_or_default();
508
509        self.path()
510            .and_then(|path| ftype_config_for_path_and_first_line(path, &first_line, lang_configs))
511            .map(|(lang, _)| lang.clone())
512    }
513
514    /// The utf-8 string contents of this buffer
515    pub fn str_contents(&self) -> String {
516        self.txt.to_string()
517    }
518
519    pub(crate) fn pretty_print_ts_tree(&self) -> Option<String> {
520        self.syntax_state
521            .as_ref()
522            .and_then(|st| st.pretty_print_tree())
523    }
524
525    pub(crate) fn string_lines(&self) -> Vec<String> {
526        self.txt
527            .iter_lines()
528            .map(|l| {
529                let mut s = l.to_string();
530                if s.ends_with('\n') {
531                    s.pop();
532                }
533                s
534            })
535            .collect()
536    }
537
538    pub fn update_ts_state(&mut self, from: usize, n_rows: usize) {
539        if let Some(ts) = self.syntax_state.as_mut() {
540            ts.update(&self.txt, from, n_rows);
541        }
542    }
543
544    pub fn iter_tokenized_lines_from(
545        &self,
546        line: usize,
547        load_exec_range: Option<(bool, Range)>,
548    ) -> LineIter<'_> {
549        match self.syntax_state.as_ref() {
550            Some(ts) => {
551                ts.iter_tokenized_lines_from(line, &self.txt, self.dot.as_range(), load_exec_range)
552            }
553            None => LineIter::new(
554                line,
555                &self.txt,
556                self.dot.as_range(),
557                load_exec_range,
558                &[],
559                &[],
560            ),
561        }
562    }
563
564    /// The contents of the current [Dot].
565    pub fn dot_contents(&self) -> String {
566        self.dot.content(self)
567    }
568
569    /// The address of the current [Dot].
570    pub fn addr(&self) -> String {
571        self.dot.addr(self)
572    }
573
574    /// The contents of the current xdot.
575    ///
576    /// This is a virtual dot that is only made use of through the filesystem interface.
577    pub fn xdot_contents(&self) -> String {
578        self.xdot.content(self)
579    }
580
581    /// The address of the current xdot.
582    ///
583    /// This is a virtual dot that is only made use of through the filesystem interface.
584    pub fn xaddr(&self) -> String {
585        self.xdot.addr(self)
586    }
587
588    pub(crate) fn tabstop(&self) -> usize {
589        config_handle!(self).tabstop
590    }
591
592    /// The number of lines currently held in the buffer.
593    #[inline]
594    pub fn len_lines(&self) -> usize {
595        self.txt.len_lines()
596    }
597
598    /// The number of utf-8 characters currently held in the buffer.
599    #[inline]
600    pub fn len_chars(&self) -> usize {
601        self.txt.len_chars()
602    }
603
604    /// Whether or not the buffer is empty.
605    ///
606    /// # Note
607    /// This does not always imply that the underlying buffer is zero sized, only that the visible
608    /// contents are empty.
609    #[inline]
610    pub fn is_empty(&self) -> bool {
611        self.txt.len_chars() == 0
612    }
613
614    /// Fully clear the contents of this buffer, notifying fsys of the change
615    pub fn clear(&mut self) {
616        self.handle_action(Action::DotSet(TextObject::BufferStart, 1), Source::Fsys);
617        self.handle_action(
618            Action::DotExtendForward(TextObject::BufferEnd, 1),
619            Source::Fsys,
620        );
621        self.handle_action(Action::Delete, Source::Fsys);
622        self.xdot.clamp_idx(self.txt.len_chars());
623    }
624
625    pub(crate) fn debug_edit_log(&self) -> Vec<String> {
626        self.edit_log.debug_edits(self)
627    }
628
629    pub(crate) fn x_from_rx(&self, y: usize) -> usize {
630        self.x_from_provided_rx(y, self.cached_rx)
631    }
632
633    pub(crate) fn x_from_provided_rx(&self, y: usize, buf_rx: usize) -> usize {
634        let tabstop = config_handle!(self).tabstop;
635        if self.is_empty() {
636            return 0;
637        }
638
639        let mut rx = 0;
640        let mut cx = 0;
641
642        for c in self.txt.line(y).chars() {
643            if c == '\n' {
644                break;
645            }
646
647            if c == '\t' {
648                rx += (tabstop - 1) - (rx % tabstop);
649            }
650
651            // FIXME: this method probably needs to be part of the UI trait because of this line.
652            // This is needed because the TUI uses SGR (1006) mouse coordinates which end up
653            // reporting x coordinates in terms of cell widths, so utf8 wide characters need to be
654            // accounted for:
655            //   https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Extended-coordinates
656            rx += unicode_width::UnicodeWidthChar::width(c).unwrap_or(1);
657
658            if rx > buf_rx {
659                break;
660            }
661            cx += 1;
662        }
663
664        cx
665    }
666
667    /// The line at the requested index returned as a Slice.
668    pub fn line(&self, y: usize) -> Option<Slice<'_>> {
669        if y >= self.len_lines() {
670            None
671        } else {
672            Some(self.txt.line(y))
673        }
674    }
675
676    /// Iterate between two character offsets.
677    ///
678    /// This is distinct from the iter_between method on an Regex haystack which is in terms of
679    /// bytes.
680    pub(crate) fn iter_between_chars(
681        &self,
682        char_from: usize,
683        char_to: usize,
684    ) -> impl Iterator<Item = (usize, char)> {
685        self.txt
686            .slice(char_from, char_to)
687            .indexed_chars(char_from, false)
688    }
689
690    /// Iterate between two character offsets in reverse.
691    ///
692    /// This is distinct from the rev_iter_between method on an Regex haystack which is in terms of
693    /// bytes.
694    pub(crate) fn rev_iter_between_chars(
695        &self,
696        char_from: usize,
697        char_to: usize,
698    ) -> impl Iterator<Item = (usize, char)> {
699        self.txt
700            .slice(char_to, char_from)
701            .indexed_chars(char_to, true)
702    }
703
704    /// Attempt to expand from the given cursor position so long as either the previous or next
705    /// character in the buffer is a known delimiter.
706    pub(crate) fn try_expand_delimited(&mut self) {
707        let current_index = match self.dot {
708            Dot::Cur { c: Cur { idx } } => idx,
709            Dot::Range { .. } => return,
710        };
711
712        let prev = if current_index == 0 {
713            None
714        } else {
715            self.txt.get_char(current_index - 1)
716        };
717        let next = self.txt.get_char(current_index + 1);
718
719        let (l, r) = match (prev, next) {
720            (Some('\n'), _) | (_, Some('\n')) => ('\n', '\n'),
721            (Some('('), _) | (_, Some(')')) => ('(', ')'),
722            (Some('['), _) | (_, Some(']')) => ('[', ']'),
723            (Some('{'), _) | (_, Some('}')) => ('{', '}'),
724            (Some('<'), _) | (_, Some('>')) => ('<', '>'),
725            (Some('"'), _) | (_, Some('"')) => ('"', '"'),
726            (Some('\''), _) | (_, Some('\'')) => ('\'', '\''),
727            (Some(' '), _) | (_, Some(' ')) => (' ', ' '),
728
729            _ => return self.expand_cur_dot(),
730        };
731
732        self.set_dot(TextObject::Delimited(l, r), 1);
733        self.changed_since_last_render = true;
734    }
735
736    /// If the current dot is a cursor rather than a range, expand it to a sensible range.
737    ///
738    /// This is modeled after (but not identical to) the behaviour in acme's `expand` function
739    /// found in look.c
740    pub(crate) fn expand_cur_dot(&mut self) {
741        let current_index = match self.dot {
742            Dot::Cur { c: Cur { idx } } => idx,
743            Dot::Range { .. } => return,
744        };
745        self.changed_since_last_render = true;
746
747        if let Some(dot) = self.try_expand_known(current_index) {
748            self.dot = dot;
749            return;
750        }
751
752        // Expand until we hit non-alphanumeric characters on each sides
753        let (mut from, mut to) = (current_index, current_index);
754        for (i, ch) in self.iter_between_chars(current_index, self.txt.len_chars()) {
755            if !(ch == '_' || ch.is_alphanumeric()) {
756                break;
757            }
758            to = i;
759        }
760
761        for (i, ch) in self.rev_iter_between_chars(current_index, 0) {
762            if !(ch == '_' || ch.is_alphanumeric()) {
763                break;
764            }
765            from = i;
766        }
767
768        self.dot = Dot::from_char_indices(from, to);
769    }
770
771    /// Try to be smart about expanding from the current cursor position to something that we
772    /// understand how to parse:
773    ///   - file path with addr (some/path:addr)
774    ///   - file path
775    ///   - url
776    fn try_expand_known(&self, current_index: usize) -> Option<Dot> {
777        let (mut from, mut to) = (current_index, current_index);
778        let mut colon: Option<usize> = None;
779        let n_chars = self.txt.len_chars();
780
781        let is_file_char = |ch: char| ch.is_alphanumeric() || "._-+/:@".contains(ch);
782        let is_addr_char = |ch: char| "+-/$.#,;?".contains(ch);
783        let is_url_char = |ch: char| "?&=".contains(ch);
784        let has_url_prefix = |i: usize| {
785            let http = i > 4 && i + 3 <= n_chars && self.txt.slice(i - 4, i + 3) == HTTP;
786            let https = i > 5 && i + 3 <= n_chars && self.txt.slice(i - 5, i + 3) == HTTPS;
787
788            http || https
789        };
790
791        // Start by expanding to cover things that are candidates for being file names (optionally
792        // with a following address) or URLs
793        for (i, ch) in self.iter_between_chars(current_index, self.txt.len_chars()) {
794            if !is_file_char(ch) {
795                break;
796            }
797            if ch == ':' && !has_url_prefix(i) {
798                colon = Some(i);
799                break;
800            }
801            to = i;
802        }
803
804        for (i, ch) in self.rev_iter_between_chars(current_index, 0) {
805            if !(is_file_char(ch) || is_url_char(ch) || is_addr_char(ch)) {
806                break;
807            }
808            if colon.is_none() && ch == ':' && !has_url_prefix(i) {
809                colon = Some(i);
810            }
811            from = i;
812        }
813
814        // Now grab the address if we had a trailing colon
815        if let Some(ix) = colon {
816            to = ix;
817            for (_, ch) in self.iter_between_chars(ix + 1, self.txt.len_chars()) {
818                if ch.is_whitespace() || "()[]{}<>;".contains(ch) {
819                    break;
820                }
821                to += 1;
822            }
823        }
824
825        let dot_content = self.txt.slice(from, to + 1).to_string();
826
827        // If dot looks like a URL then expand until whitespace and strip trailing punctuation
828        if dot_content.starts_with(HTTP) || dot_content.starts_with(HTTPS) {
829            if to < self.txt.len_chars() {
830                for (_, ch) in self.iter_between_chars(to + 1, self.txt.len_chars()) {
831                    if ch.is_whitespace() || "()[]{}<>;".contains(ch) {
832                        break;
833                    }
834                    to += 1;
835                }
836            }
837
838            if dot_content.ends_with('.') {
839                to -= 1;
840            }
841
842            return Some(Dot::from_char_indices(from, to));
843        }
844
845        let dot = Dot::from_char_indices(from, to);
846
847        // If dot up until ':' is a file then return the entire dot
848        let fname = match dot_content.split_once(':') {
849            Some((fname, _)) => fname,
850            None => &dot_content,
851        };
852
853        let path = Path::new(fname);
854        if path.is_absolute() && path.exists() {
855            return Some(dot);
856        } else if let Some(dir) = self.dir()
857            && dir.join(path).exists()
858        {
859            return Some(dot);
860        }
861
862        // Not a file or a URL
863        None
864    }
865
866    pub(crate) fn sign_col_dims(&self) -> (usize, usize) {
867        let w_lnum = n_digits(self.len_lines());
868        let w_sgncol = w_lnum + 2;
869
870        (w_lnum, w_sgncol)
871    }
872
873    pub(crate) fn append(&mut self, s: String, source: Source) {
874        let dot = self.dot;
875        self.set_dot(TextObject::BufferEnd, 1);
876        self.handle_action(Action::InsertString { s }, source);
877        self.dot = dot;
878        self.dot.clamp_idx(self.txt.len_chars());
879        self.xdot.clamp_idx(self.txt.len_chars());
880        self.changed_since_last_render = true;
881    }
882
883    /// The error result of this function is an error string that should be displayed to the user
884    pub fn handle_action(&mut self, a: Action, source: Source) -> Option<ActionOutcome> {
885        match a {
886            Action::Delete => {
887                let (c, deleted) = self.delete_dot(self.dot, Some(source));
888                self.dot = Dot::Cur { c };
889                self.dot.clamp_idx(self.txt.len_chars());
890                self.xdot.clamp_idx(self.txt.len_chars());
891                return deleted.map(ActionOutcome::SetClipboard);
892            }
893            Action::InsertChar { c } => {
894                let (c, _) = self.insert_char(self.dot, c, Some(source));
895                self.dot = Dot::Cur { c };
896                self.dot.clamp_idx(self.txt.len_chars());
897                self.xdot.clamp_idx(self.txt.len_chars());
898                return None;
899            }
900            Action::InsertString { s } => {
901                let (c, _) = self.insert_string(self.dot, s, Some(source));
902                self.dot = Dot::Cur { c };
903                self.dot.clamp_idx(self.txt.len_chars());
904                self.xdot.clamp_idx(self.txt.len_chars());
905                return None;
906            }
907
908            Action::Redo => return self.redo(),
909            Action::Undo => return self.undo(),
910
911            Action::DotCollapseFirst => self.collapse_dot(true),
912            Action::DotCollapseLast => self.collapse_dot(false),
913            Action::DotExtendBackward(tobj, count) => self.extend_dot_backward(tobj, count),
914            Action::DotExtendForward(tobj, count) => self.extend_dot_forward(tobj, count),
915            Action::DotFlip => self.flip_dot(),
916            Action::DotSet(t, count) => self.set_dot(t, count),
917            Action::DotSetFromCoords { coords } => self.set_dot_from_coords(coords),
918
919            Action::XDotSetFromCoords { coords } => self.set_xdot_from_coords(coords),
920            Action::XInsertString { s } => self.insert_xdot(s),
921
922            Action::RenameActiveBuffer { name } => return self.set_filename(name),
923            Action::RawInput { i } => return self.handle_raw_input(i),
924
925            _ => (),
926        }
927
928        None
929    }
930
931    fn handle_raw_input(&mut self, k: Input) -> Option<ActionOutcome> {
932        let (match_indent, expand_tab, tabstop) = {
933            let cfg = config_handle!(self);
934            (cfg.match_indent, cfg.expand_tab, cfg.tabstop)
935        };
936
937        match k {
938            Input::Return => {
939                let mut s = "\n".to_string();
940                if match_indent {
941                    let cur = self.dot.first_cur();
942                    let y = self.txt.char_to_line(cur.idx);
943                    let line = self.txt.line(y).to_string();
944                    s.push_str(
945                        &line
946                            .find(|c: char| !c.is_whitespace())
947                            .map(|ix| line.split_at(ix).0.to_string())
948                            .unwrap_or_default(),
949                    );
950                }
951
952                let c = self.insert_string(self.dot, s, Some(Source::Keyboard)).0;
953
954                self.dot = Dot::Cur { c };
955                return None;
956            }
957
958            Input::Tab => {
959                let (c, _) = if expand_tab {
960                    self.insert_string(self.dot, " ".repeat(tabstop), Some(Source::Keyboard))
961                } else {
962                    self.insert_char(self.dot, '\t', Some(Source::Keyboard))
963                };
964
965                self.dot = Dot::Cur { c };
966                return None;
967            }
968
969            Input::Char(ch) => {
970                let (c, _) = self.insert_char(self.dot, ch, Some(Source::Keyboard));
971                self.dot = Dot::Cur { c };
972                return None;
973            }
974
975            Input::Arrow(arr) => self.set_dot(TextObject::Arr(arr), 1),
976
977            _ => return None,
978        }
979
980        self.changed_since_last_render = true;
981        None
982    }
983
984    /// Set dot and clamp to ensure it is within bounds
985    pub(crate) fn set_dot(&mut self, t: TextObject, n: usize) {
986        for _ in 0..n {
987            t.set_dot(self);
988        }
989        self.dot.clamp_idx(self.txt.len_chars());
990        self.xdot.clamp_idx(self.txt.len_chars());
991        self.changed_since_last_render = true;
992    }
993
994    /// Set this Buffer's dot to an explicit cursor position, clamping to EOB.
995    pub fn set_dot_from_cursor(&mut self, idx: usize) {
996        self.dot = Cur::new(idx).into();
997        self.dot.clamp_idx(self.txt.len_chars());
998        self.changed_since_last_render = true;
999    }
1000
1001    /// Set this Buffer's dot to an explicit [Range], clamping to EOB.
1002    pub fn set_dot_from_range(&mut self, from: usize, to: usize) {
1003        self.dot = Dot::from(Range::from_cursors(Cur::new(from), Cur::new(to), false))
1004            .collapse_null_range();
1005        self.dot.clamp_idx(self.txt.len_chars());
1006        self.changed_since_last_render = true;
1007    }
1008
1009    fn set_dot_from_coords(&mut self, coords: Coords) {
1010        let addr: Addr = coords.as_addr(self);
1011        self.dot = self.map_addr(&addr);
1012        self.dot.clamp_idx(self.txt.len_chars());
1013        self.xdot.clamp_idx(self.txt.len_chars());
1014        self.changed_since_last_render = true;
1015    }
1016
1017    fn set_xdot_from_coords(&mut self, coords: Coords) {
1018        let addr: Addr = coords.as_addr(self);
1019        self.xdot = self.map_addr(&addr);
1020        self.xdot.clamp_idx(self.txt.len_chars());
1021    }
1022
1023    /// Extend dot forward and clamp to ensure it is within bounds
1024    fn extend_dot_forward(&mut self, t: TextObject, n: usize) {
1025        for _ in 0..n {
1026            t.extend_dot_forward(self);
1027        }
1028        self.dot.clamp_idx(self.txt.len_chars());
1029        self.xdot.clamp_idx(self.txt.len_chars());
1030        self.changed_since_last_render = true;
1031    }
1032
1033    /// Extend dot backward and clamp to ensure it is within bounds
1034    fn extend_dot_backward(&mut self, t: TextObject, n: usize) {
1035        for _ in 0..n {
1036            t.extend_dot_backward(self);
1037        }
1038        self.dot.clamp_idx(self.txt.len_chars());
1039        self.xdot.clamp_idx(self.txt.len_chars());
1040        self.changed_since_last_render = true;
1041    }
1042
1043    fn collapse_dot(&mut self, first: bool) {
1044        self.dot = if first {
1045            self.dot.collapse_to_first_cur()
1046        } else {
1047            self.dot.collapse_to_last_cur()
1048        };
1049
1050        self.changed_since_last_render = true;
1051    }
1052
1053    fn flip_dot(&mut self) {
1054        self.dot.flip();
1055        self.changed_since_last_render = true;
1056    }
1057
1058    pub(crate) fn new_edit_log_transaction(&mut self) {
1059        self.edit_log.new_transaction()
1060    }
1061
1062    fn undo(&mut self) -> Option<ActionOutcome> {
1063        match self.edit_log.undo() {
1064            Some(edits) => {
1065                self.edit_log.paused = true;
1066                for edit in edits.into_iter() {
1067                    self.apply_edit(edit);
1068                }
1069                self.edit_log.paused = false;
1070                self.dirty = !self.edit_log.is_empty();
1071                self.changed_since_last_render = true;
1072                None
1073            }
1074            None => Some(ActionOutcome::SetStatusMessage(
1075                "Nothing to undo".to_string(),
1076            )),
1077        }
1078    }
1079
1080    fn redo(&mut self) -> Option<ActionOutcome> {
1081        match self.edit_log.redo() {
1082            Some(edits) => {
1083                self.edit_log.paused = true;
1084                for edit in edits.into_iter() {
1085                    self.apply_edit(edit);
1086                }
1087                self.edit_log.paused = false;
1088                self.dirty = true;
1089                self.changed_since_last_render = true;
1090                None
1091            }
1092            None => Some(ActionOutcome::SetStatusMessage(
1093                "Nothing to redo".to_string(),
1094            )),
1095        }
1096    }
1097
1098    fn apply_edit(&mut self, Edit { kind, cur, txt }: Edit) {
1099        let new_cur = match (kind, txt) {
1100            (Kind::Insert, Txt::Char(c)) => self.insert_char(Dot::Cur { c: cur }, c, None).0,
1101            (Kind::Insert, Txt::String(s)) => self.insert_string(Dot::Cur { c: cur }, s, None).0,
1102            (Kind::Delete, Txt::Char(_)) => self.delete_dot(Dot::Cur { c: cur }, None).0,
1103            (Kind::Delete, Txt::String(s)) => {
1104                let start_idx = cur.idx;
1105                let end_idx = (start_idx + s.chars().count()).saturating_sub(1);
1106                let end = Cur { idx: end_idx };
1107                self.delete_dot(
1108                    Dot::Range {
1109                        r: Range::from_cursors(cur, end, true),
1110                    }
1111                    .collapse_null_range(),
1112                    None,
1113                )
1114                .0
1115            }
1116        };
1117
1118        self.dot = Dot::Cur { c: new_cur };
1119    }
1120
1121    /// Only files get marked as dirty to ensure that they are prompted for saving before being
1122    /// closed.
1123    fn mark_dirty(&mut self) {
1124        self.dirty = self.kind.is_file();
1125    }
1126
1127    /// Returns true if a filter was present and the notification was sent
1128    pub(crate) fn notify_load(&self, source: Source) -> bool {
1129        match self.input_filter.as_ref() {
1130            Some(f) => {
1131                let (ch_from, ch_to) = self.dot.as_char_indices();
1132                let txt = self.dot.content(self);
1133                f.notify_load(source, ch_from, ch_to, &txt);
1134                true
1135            }
1136            None => false,
1137        }
1138    }
1139
1140    /// Returns true if a filter was present and the notification was sent
1141    pub(crate) fn notify_execute(&self, source: Source, arg: Option<(Range, String)>) -> bool {
1142        match self.input_filter.as_ref() {
1143            Some(f) => {
1144                let (ch_from, ch_to) = self.dot.as_char_indices();
1145                let txt = self.dot.content(self);
1146                f.notify_execute(source, ch_from, ch_to, &txt, arg);
1147                true
1148            }
1149            None => false,
1150        }
1151    }
1152
1153    fn insert_char(&mut self, dot: Dot, ch: char, source: Option<Source>) -> (Cur, Option<String>) {
1154        let ch = if ch == '\r' { '\n' } else { ch };
1155        let mut have_prepared_edit = false;
1156        if let Dot::Range { r } = &dot
1157            && r.start.idx != r.end.idx
1158            && let Some(s) = self.syntax_state.as_mut()
1159        {
1160            s.prepare_delete_range(r.start.idx, r.end.idx + 1, &self.txt);
1161            have_prepared_edit = true;
1162        }
1163
1164        let (cur, deleted) = match dot {
1165            Dot::Cur { c } => (c, None),
1166            Dot::Range { r } => self.delete_range(r, source),
1167        };
1168
1169        if have_prepared_edit && let Some(ts) = self.syntax_state.as_mut() {
1170            ts.apply_prepared_edit(&self.txt);
1171        }
1172
1173        let idx = cur.idx;
1174        if let Some(s) = self.syntax_state.as_mut() {
1175            s.prepare_insert_char(idx, ch, &self.txt);
1176        }
1177
1178        self.txt.insert_char(idx, ch);
1179
1180        if let (Some(source), Some(f)) = (source, self.input_filter.as_ref()) {
1181            f.notify_insert(source, idx, idx + 1, &ch.to_string());
1182        }
1183
1184        self.edit_log.insert_char(cur, ch);
1185        if let Some(ts) = self.syntax_state.as_mut() {
1186            ts.apply_prepared_edit(&self.txt);
1187        }
1188
1189        self.mark_dirty();
1190        self.changed_since_last_render = true;
1191
1192        (Cur { idx: idx + 1 }, deleted)
1193    }
1194
1195    fn insert_string(
1196        &mut self,
1197        dot: Dot,
1198        s: String,
1199        source: Option<Source>,
1200    ) -> (Cur, Option<String>) {
1201        let s = normalize_line_endings(s);
1202        let len = s.chars().count();
1203        let mut have_prepared_edit = false;
1204
1205        if let Dot::Range { r } = &dot
1206            && r.start.idx != r.end.idx
1207            && let Some(ts) = self.syntax_state.as_mut()
1208        {
1209            ts.prepare_delete_range(r.start.idx, r.end.idx + 1, &self.txt);
1210            have_prepared_edit = true;
1211        }
1212
1213        let (mut cur, deleted) = match dot {
1214            Dot::Cur { c } => (c, None),
1215            Dot::Range { r } => self.delete_range(r, source),
1216        };
1217
1218        if have_prepared_edit && let Some(ts) = self.syntax_state.as_mut() {
1219            ts.apply_prepared_edit(&self.txt);
1220        }
1221
1222        let idx = cur.idx;
1223        have_prepared_edit = false;
1224        if !s.is_empty()
1225            && let Some(ts) = self.syntax_state.as_mut()
1226        {
1227            ts.prepare_insert_string(idx, &s, &self.txt);
1228            have_prepared_edit = true;
1229        }
1230
1231        // Inserting an empty string should not be recorded as an edit (and is
1232        // a no-op for the content of self.txt) but we support it as inserting
1233        // an empty string while dot is a range has the same effect as a delete.
1234        if !s.is_empty() {
1235            self.txt.insert_str(idx, &s);
1236
1237            if let (Some(source), Some(f)) = (source, self.input_filter.as_ref()) {
1238                f.notify_insert(source, idx, idx + len, &s);
1239            }
1240
1241            self.edit_log.insert_string(cur, s);
1242            cur.idx += len;
1243        }
1244
1245        if have_prepared_edit && let Some(ts) = self.syntax_state.as_mut() {
1246            ts.apply_prepared_edit(&self.txt);
1247        }
1248
1249        self.mark_dirty();
1250        self.changed_since_last_render = true;
1251
1252        (cur, deleted)
1253    }
1254
1255    fn delete_dot(&mut self, dot: Dot, source: Option<Source>) -> (Cur, Option<String>) {
1256        let mut have_prepared_edit = false;
1257
1258        if let Some(ts) = self.syntax_state.as_mut() {
1259            match &dot {
1260                Dot::Cur { c } if c.idx < self.txt.len_chars() => {
1261                    ts.prepare_delete_char(c.idx, &self.txt);
1262                    have_prepared_edit = true;
1263                }
1264                Dot::Range { r } if r.start.idx != r.end.idx => {
1265                    ts.prepare_delete_range(r.start.idx, r.end.idx + 1, &self.txt);
1266                    have_prepared_edit = true;
1267                }
1268                _ => (),
1269            }
1270        }
1271
1272        let (cur, deleted) = match dot {
1273            Dot::Cur { c } => (self.delete_cur(c, source), None),
1274            Dot::Range { r } => self.delete_range(r, source),
1275        };
1276
1277        if have_prepared_edit && let Some(ts) = self.syntax_state.as_mut() {
1278            ts.apply_prepared_edit(&self.txt);
1279        }
1280
1281        (cur, deleted)
1282    }
1283
1284    fn delete_cur(&mut self, cur: Cur, source: Option<Source>) -> Cur {
1285        let idx = cur.idx;
1286        if idx < self.txt.len_chars() {
1287            let ch = self.txt.char(idx);
1288            self.txt.remove_char(idx);
1289
1290            if let (Some(source), Some(f)) = (source, self.input_filter.as_ref()) {
1291                f.notify_delete(source, idx, idx + 1);
1292            }
1293
1294            self.edit_log.delete_char(cur, ch);
1295            self.mark_dirty();
1296            self.changed_since_last_render = true;
1297        }
1298
1299        cur
1300    }
1301
1302    fn delete_range(&mut self, r: Range, source: Option<Source>) -> (Cur, Option<String>) {
1303        let (from, to) = (r.start.idx, min(r.end.idx + 1, self.txt.len_chars()));
1304        let is_single_char = r.start.idx == r.end.idx;
1305
1306        let s = self.txt.slice(from, to).to_string();
1307        self.txt.remove_range(from, to);
1308
1309        if let (Some(source), Some(f)) = (source, self.input_filter.as_ref()) {
1310            f.notify_delete(source, from, to);
1311        }
1312
1313        self.edit_log.delete_string(r.start, s.clone());
1314        self.mark_dirty();
1315        self.changed_since_last_render = true;
1316
1317        let deleted = if is_single_char { None } else { Some(s) };
1318
1319        (r.start, deleted)
1320    }
1321
1322    pub(crate) fn find_forward(&mut self, s: &str) {
1323        if let Some(dot) = find_forward_wrapping(&s, self) {
1324            self.dot = dot;
1325            self.changed_since_last_render = true;
1326        }
1327    }
1328
1329    /// Insert a string into the buffer using the current xdot rather than dot,
1330    /// preserving the current dot where possible.
1331    ///
1332    /// If xdot and dot intersect then the user's current selection is being
1333    /// modified. Rather than preserve _part_ of the original selection we
1334    /// instead collapse to the furthest point in the buffer that was part of
1335    /// the original selection. Or, in the case that xdot fully contains dot,
1336    /// we place the cursor at the end of the newly inserted text.
1337    ///
1338    /// DOT    |---X-| ;   |---|   ; |------X| ; |-X---|
1339    /// XDOT |----|    ; |-------X ;   |---|   ;   |----|
1340    ///
1341    /// We determine the final cursor we collapse to based on the start of xdot
1342    /// and the offset coming from the text being inserted.
1343    pub(crate) fn insert_xdot(&mut self, s: String) {
1344        let mut offset = s.chars().count() as isize;
1345
1346        if self.dot.as_range().intersects_range(&self.xdot.as_range()) {
1347            if self.xdot.first_cur() > self.dot.first_cur()
1348                && self.xdot.last_cur() >= self.dot.last_cur()
1349            {
1350                // The final case shown above: we need to back up a character in
1351                // order to land on the last character of the existing dot rather
1352                // than the first character of the inserted text.
1353                offset = -1;
1354            }
1355
1356            self.dot = self.xdot.collapse_to_first_cur();
1357        } else if self.xdot.first_cur() > self.dot.last_cur() {
1358            // Nothing to update for dot.
1359            // DOT   |---|
1360            // XDOT         |----|
1361            offset = 0;
1362        } else {
1363            // In this case, the change in buffer length needs to account for
1364            // the text being removed as well as what is being inserted.
1365            // DOT          |---|
1366            // XDOT  |----|
1367            offset -= self.xdot.n_chars() as isize;
1368            if self.xdot.is_cur() {
1369                // Cursors insert directly into the buffer without removing the character
1370                // they are on.
1371                offset += 1;
1372            }
1373        };
1374
1375        let dot_after_edit = self.dot.with_offset_saturating(offset);
1376        self.dot = self.xdot;
1377        self.handle_action(Action::InsertString { s }, Source::Fsys);
1378        (self.xdot, self.dot) = (self.dot, dot_after_edit);
1379        self.dot.clamp_idx(self.txt.len_chars()); // xdot clamped as part of handling the insert
1380    }
1381}
1382
1383fn n_digits(mut n: usize) -> usize {
1384    if n == 0 {
1385        return 1;
1386    }
1387
1388    let mut digits = 0;
1389    while n != 0 {
1390        digits += 1;
1391        n /= 10;
1392    }
1393
1394    digits
1395}
1396
1397#[cfg(test)]
1398pub(crate) mod tests {
1399    use super::*;
1400    use crate::{key::Arrow, syntax::ts::TsState};
1401    use edit::tests::{del_c, del_s, in_c, in_s};
1402    use simple_test_case::test_case;
1403    use std::env;
1404
1405    const LINE_1: &str = "This is a test";
1406    const LINE_2: &str = "involving multiple lines";
1407
1408    fn c(idx: usize) -> Cur {
1409        Cur { idx }
1410    }
1411
1412    fn r(from: usize, to: usize, from_active: bool) -> Range {
1413        Range::from_cursors(c(from), c(to), from_active)
1414    }
1415
1416    pub fn buffer_from_lines(lines: &[&str]) -> Buffer {
1417        let mut b = Buffer::new_unnamed(0, "", Default::default());
1418        let s = lines.join("\n");
1419
1420        for c in s.chars() {
1421            b.handle_action(Action::InsertChar { c }, Source::Keyboard);
1422        }
1423
1424        b
1425    }
1426
1427    fn simple_initial_buffer() -> Buffer {
1428        buffer_from_lines(&[LINE_1, LINE_2])
1429    }
1430
1431    #[test_case(0, 1; "n0")]
1432    #[test_case(5, 1; "n5")]
1433    #[test_case(10, 2; "n10")]
1434    #[test_case(13, 2; "n13")]
1435    #[test_case(731, 3; "n731")]
1436    #[test_case(930, 3; "n930")]
1437    #[test]
1438    fn n_digits_works(n: usize, digits: usize) {
1439        assert_eq!(n_digits(n), digits);
1440    }
1441
1442    #[test]
1443    fn simple_insert_works() {
1444        let b = simple_initial_buffer();
1445        let c = Cur::from_yx(1, LINE_2.len(), &b);
1446        let lines = b.string_lines();
1447
1448        assert_eq!(lines.len(), 2);
1449        assert_eq!(lines[0], LINE_1);
1450        assert_eq!(lines[1], LINE_2);
1451        assert_eq!(b.dot, Dot::Cur { c });
1452        assert_eq!(
1453            b.edit_log.edits,
1454            vec![vec![in_s(0, &format!("{LINE_1}\n{LINE_2}"))]]
1455        );
1456    }
1457
1458    #[test]
1459    fn insert_with_moving_dot_works() {
1460        let mut b = Buffer::new_unnamed(0, "", Default::default());
1461
1462        // Insert from the start of the buffer
1463        for c in "hello w".chars() {
1464            b.handle_action(Action::InsertChar { c }, Source::Keyboard);
1465        }
1466
1467        // move back to insert a character inside of the text we already have
1468        b.handle_action(
1469            Action::DotSet(TextObject::Arr(Arrow::Left), 2),
1470            Source::Keyboard,
1471        );
1472        b.handle_action(Action::InsertChar { c: ',' }, Source::Keyboard);
1473
1474        // move forward to the end of the line to finish inserting
1475        b.handle_action(Action::DotSet(TextObject::LineEnd, 1), Source::Keyboard);
1476        for c in "orld!".chars() {
1477            b.handle_action(Action::InsertChar { c }, Source::Keyboard);
1478        }
1479
1480        // inserted characters should be in the correct positions
1481        assert_eq!(b.txt.to_string(), "hello, world!");
1482    }
1483
1484    #[test_case(
1485        Action::InsertChar { c: 'x' },
1486        in_c(LINE_1.len() + 1, 'x');
1487        "char"
1488    )]
1489    #[test_case(
1490        Action::InsertString { s: "x".to_string() },
1491        in_s(LINE_1.len() + 1, "x");
1492        "string"
1493    )]
1494    #[test]
1495    fn insert_w_range_dot_works(a: Action, edit: Edit) {
1496        let mut b = simple_initial_buffer();
1497        b.handle_action(Action::DotSet(TextObject::Line, 1), Source::Keyboard);
1498
1499        let outcome = b.handle_action(a, Source::Keyboard);
1500        assert_eq!(outcome, None);
1501
1502        let lines = b.string_lines();
1503        assert_eq!(lines.len(), 2);
1504
1505        let c = Cur::from_yx(1, 1, &b);
1506        assert_eq!(b.dot, Dot::Cur { c });
1507
1508        assert_eq!(lines[0], LINE_1);
1509        assert_eq!(lines[1], "x");
1510        assert_eq!(
1511            b.edit_log.edits,
1512            vec![vec![
1513                in_s(0, &format!("{LINE_1}\n{LINE_2}")),
1514                del_s(LINE_1.len() + 1, LINE_2),
1515                edit,
1516            ]]
1517        );
1518    }
1519
1520    #[test]
1521    fn move_forward_at_end_of_buffer_is_fine() {
1522        let mut b = Buffer::new_unnamed(0, "", Default::default());
1523        b.handle_raw_input(Input::Arrow(Arrow::Right));
1524
1525        let c = Cur { idx: 0 };
1526        assert_eq!(b.dot, Dot::Cur { c });
1527    }
1528
1529    #[test]
1530    fn delete_in_empty_buffer_is_fine() {
1531        let mut b = Buffer::new_unnamed(0, "", Default::default());
1532        b.handle_action(Action::Delete, Source::Keyboard);
1533        let c = Cur { idx: 0 };
1534        let lines = b.string_lines();
1535
1536        assert_eq!(b.dot, Dot::Cur { c });
1537        assert_eq!(lines.len(), 1);
1538        assert_eq!(lines[0], "");
1539        assert!(b.edit_log.edits.is_empty());
1540    }
1541
1542    #[test]
1543    fn simple_delete_works() {
1544        let mut b = simple_initial_buffer();
1545        b.handle_action(
1546            Action::DotSet(TextObject::Arr(Arrow::Left), 1),
1547            Source::Keyboard,
1548        );
1549        b.handle_action(Action::Delete, Source::Keyboard);
1550
1551        let c = Cur::from_yx(1, LINE_2.len() - 1, &b);
1552        let lines = b.string_lines();
1553
1554        assert_eq!(b.dot, Dot::Cur { c });
1555        assert_eq!(lines.len(), 2);
1556        assert_eq!(lines[0], LINE_1);
1557        assert_eq!(lines[1], "involving multiple line");
1558        assert_eq!(
1559            b.edit_log.edits,
1560            vec![vec![
1561                in_s(0, &format!("{LINE_1}\n{LINE_2}")),
1562                del_c(LINE_1.len() + 24, 's')
1563            ]]
1564        );
1565    }
1566
1567    #[test]
1568    fn delete_range_works() {
1569        let mut b = simple_initial_buffer();
1570        b.handle_action(Action::DotSet(TextObject::Line, 1), Source::Keyboard);
1571        b.handle_action(Action::Delete, Source::Keyboard);
1572
1573        let c = Cur::from_yx(1, 0, &b);
1574        let lines = b.string_lines();
1575
1576        assert_eq!(b.dot, Dot::Cur { c });
1577        assert_eq!(lines.len(), 2);
1578        assert_eq!(lines[0], LINE_1);
1579        assert_eq!(lines[1], "");
1580        assert_eq!(
1581            b.edit_log.edits,
1582            vec![vec![
1583                in_s(0, &format!("{LINE_1}\n{LINE_2}")),
1584                del_s(LINE_1.len() + 1, "involving multiple lines")
1585            ]]
1586        );
1587    }
1588
1589    #[test]
1590    fn delete_range_when_start_equals_end_works() {
1591        let mut b = Buffer::new_unnamed(0, "foo", Default::default());
1592        let (cur, deleted) =
1593            b.delete_range(Range::from_cursors(Cur::new(0), Cur::new(0), false), None);
1594
1595        assert_eq!(b.str_contents(), "oo");
1596        assert_eq!(cur, Cur::new(0));
1597        assert_eq!(deleted, None);
1598    }
1599
1600    #[test]
1601    fn delete_undo_works() {
1602        let mut b = simple_initial_buffer();
1603        let original_lines = b.string_lines();
1604        b.new_edit_log_transaction();
1605
1606        b.handle_action(
1607            Action::DotExtendBackward(TextObject::Word, 1),
1608            Source::Keyboard,
1609        );
1610        b.handle_action(Action::Delete, Source::Keyboard);
1611
1612        b.set_dot(TextObject::BufferStart, 1);
1613        b.handle_action(
1614            Action::DotExtendForward(TextObject::Word, 1),
1615            Source::Keyboard,
1616        );
1617        b.handle_action(Action::Delete, Source::Keyboard);
1618
1619        b.handle_action(Action::Undo, Source::Keyboard);
1620
1621        let lines = b.string_lines();
1622
1623        assert_eq!(lines, original_lines);
1624    }
1625
1626    #[test]
1627    fn undo_string_insert_works() {
1628        let initial_content = "foo foo foo\n";
1629        let mut b = Buffer::new_unnamed(0, initial_content, Default::default());
1630
1631        b.insert_string(Dot::Cur { c: c(0) }, "bar".to_string(), None);
1632        b.handle_action(Action::Undo, Source::Keyboard);
1633
1634        assert_eq!(b.string_lines(), vec!["foo foo foo", ""]);
1635    }
1636
1637    #[test]
1638    fn undo_string_delete_works() {
1639        let initial_content = "foo foo foo\n";
1640        let mut b = Buffer::new_unnamed(0, initial_content, Default::default());
1641
1642        let r = Range::from_cursors(c(0), c(2), true);
1643        b.delete_dot(Dot::Range { r }, None);
1644        b.handle_action(Action::Undo, Source::Keyboard);
1645
1646        assert_eq!(b.string_lines(), vec!["foo foo foo", ""]);
1647    }
1648
1649    #[test]
1650    fn undo_string_insert_and_delete_works() {
1651        let initial_content = "foo foo foo\n";
1652        let mut b = Buffer::new_unnamed(0, initial_content, Default::default());
1653
1654        let r = Range::from_cursors(c(0), c(2), true);
1655        b.delete_dot(Dot::Range { r }, None);
1656        b.insert_string(Dot::Cur { c: c(0) }, "bar".to_string(), None);
1657
1658        assert_eq!(b.string_lines(), vec!["bar foo foo", ""]);
1659
1660        b.handle_action(Action::Undo, Source::Keyboard);
1661        b.handle_action(Action::Undo, Source::Keyboard);
1662
1663        assert_eq!(b.string_lines(), vec!["foo foo foo", ""]);
1664    }
1665
1666    // Tests are executed from the root of the crate so existing file paths are relative to there
1667    #[test_case("foo", None; "unknown format")]
1668    #[test_case("someFunc()", None; "camel case function call")]
1669    #[test_case("some_func()", None; "snake case function call")]
1670    #[test_case("not_a_real_file.rs", None; "file that does not exist")]
1671    #[test_case("README.md", Some("README.md"); "file that exists")]
1672    #[test_case("README.md:12,19", Some("README.md:12,19"); "file that exists with addr")]
1673    #[test_case("README.md:12:19", Some("README.md:12:19"); "file that exists with addr containing colon")]
1674    #[test_case("/dev/null", Some("/dev/null"); "file that exists abs path")]
1675    #[test_case("/dev/null:12-+#", Some("/dev/null:12-+#"); "file that exists abs path with addr")]
1676    #[test_case("http://example.com", Some("http://example.com"); "http url")]
1677    #[test_case("http://example.com/some/path", Some("http://example.com/some/path"); "http url with path")]
1678    #[test_case("http://example.com?foo=1", Some("http://example.com?foo=1"); "http url with query string")]
1679    #[test_case("http://example.com?foo=1&bar=2", Some("http://example.com?foo=1&bar=2"); "http url with multi query string")]
1680    #[test]
1681    fn try_expand_known_works(s: &str, expected: Option<&str>) {
1682        let cwd = env::current_dir().unwrap().display().to_string();
1683        // Check with surrounding whitespace and delimiters
1684        for (l, r) in [(" ", " "), ("(", ")"), ("[", "]"), ("<", ">"), ("{", "}")] {
1685            let b = Buffer::new_output(
1686                0,
1687                format!("{cwd}/+output"),
1688                format!("abc_123 {l}{s}{r}\tmore text"),
1689                Default::default(),
1690            );
1691
1692            // Check with the initial cursor position being at any offset within the target
1693            for i in 0..s.len() {
1694                let dot = b.try_expand_known(9 + i);
1695                let maybe_content = dot.map(|d| d.content(&b));
1696                assert_eq!(
1697                    maybe_content.as_deref(),
1698                    expected,
1699                    "failed at offset={i} with lr=({l:?}, {r:?})"
1700                )
1701            }
1702        }
1703    }
1704
1705    #[test_case("\r", "\n"; "CR")]
1706    #[test_case("\n", "\n"; "LF")]
1707    #[test_case("\r\n", "\n"; "CRLF")]
1708    #[test_case("foo\rbar", "foo\nbar"; "text either side of CR")]
1709    #[test_case("foo\nbar", "foo\nbar"; "text either side of LF")]
1710    #[test_case("foo\r\nbar", "foo\nbar"; "text either side of CRLF")]
1711    #[test_case("foo\rbar\nbaz\r\nquux", "foo\nbar\nbaz\nquux"; "mixed line endings")]
1712    #[test]
1713    fn normalizes_line_endings_insert_string(s: &str, expected: &str) {
1714        let mut b = Buffer::new_virtual(0, "test", "", Default::default());
1715        b.insert_string(Dot::Cur { c: c(0) }, s.to_string(), None);
1716        assert_eq!(b.str_contents(), expected);
1717    }
1718
1719    #[test_case('\r', "\n"; "CR")]
1720    #[test_case('\n', "\n"; "LF")]
1721    #[test_case('a', "a"; "ascii")]
1722    #[test]
1723    fn normalizes_line_endings_insert_char(ch: char, expected: &str) {
1724        let mut b = Buffer::new_virtual(0, "test", "", Default::default());
1725        b.insert_char(Dot::Cur { c: c(0) }, ch, None);
1726        assert_eq!(b.str_contents(), expected);
1727    }
1728
1729    // Computing tree-sitter edits involves working with references to the old positions within the
1730    // tree which can become invalidated when the buffer is being truncated.
1731    #[test]
1732    fn insert_string_reducing_buffer_len_works_with_ts_state() {
1733        let mut b = Buffer::new_virtual(0, "test", "fn main() {}", Default::default());
1734        b.syntax_state = Some(SyntaxState::ts(
1735            TsState::try_new_from_language("rust", tree_sitter_rust::LANGUAGE.into(), "", &b.txt)
1736                .unwrap(),
1737        ));
1738
1739        b.set_dot(TextObject::BufferStart, 1);
1740        b.extend_dot_forward(TextObject::BufferEnd, 1);
1741
1742        b.handle_action(
1743            Action::InsertString {
1744                s: "bar".to_owned(),
1745            },
1746            Source::Fsys,
1747        );
1748
1749        assert_eq!(b.txt.to_string(), "bar");
1750        assert_eq!(b.dot, Dot::Cur { c: Cur { idx: 3 } });
1751    }
1752
1753    #[test]
1754    fn insert_char_reducing_buffer_len_works_with_ts_state() {
1755        let mut b = Buffer::new_virtual(0, "test", "fn main() {}", Default::default());
1756        b.syntax_state = Some(SyntaxState::ts(
1757            TsState::try_new_from_language("rust", tree_sitter_rust::LANGUAGE.into(), "", &b.txt)
1758                .unwrap(),
1759        ));
1760
1761        b.set_dot(TextObject::BufferStart, 1);
1762        b.extend_dot_forward(TextObject::BufferEnd, 1);
1763
1764        b.handle_action(Action::InsertChar { c: 'a' }, Source::Fsys);
1765
1766        assert_eq!(b.txt.to_string(), "a");
1767        assert_eq!(b.dot, Dot::Cur { c: Cur { idx: 1 } });
1768    }
1769
1770    #[test]
1771    fn match_indent_works() {
1772        let mut b = Buffer::new_virtual(0, "test", "  foo", Default::default());
1773        b.set_dot(TextObject::BufferEnd, 1);
1774        b.handle_raw_input(Input::Return);
1775        assert_eq!(b.txt.to_string(), "  foo\n  ");
1776    }
1777
1778    #[test]
1779    fn set_dot_eob_single_line_buffer() {
1780        let mut b = Buffer::new_virtual(
1781            0,
1782            "test",
1783            "// does it need to be a doc comment? that is a long enough line to",
1784            Default::default(),
1785        );
1786        b.set_dot(TextObject::BufferEnd, 1);
1787        b.handle_raw_input(Input::Return);
1788
1789        assert_eq!(
1790            b.txt.to_string(),
1791            "// does it need to be a doc comment? that is a long enough line to\n"
1792        );
1793    }
1794
1795    #[test_case(
1796        Dot::from(r(18, 23, false)), "this is a minimal foo",
1797        r(5, 16, false).into(), "is a minimal";
1798        "range after"
1799    )]
1800    #[test_case(
1801        Dot::from(r(0, 2, false)), "foos is a minimal buffer",
1802        r(5, 16, false).into(), "is a minimal";
1803        "range before equal"
1804    )]
1805    #[test_case(
1806        Dot::from(r(0, 3, false)), "foo is a minimal buffer",
1807        r(4, 15, false).into(), "is a minimal";
1808        "range before truncate"
1809    )]
1810    #[test_case(
1811        Dot::from(r(0, 1, false)), "foois is a minimal buffer",
1812        r(6, 17, false).into(), "is a minimal";
1813        "range before expand"
1814    )]
1815    #[test_case(
1816        Dot::from(r(0, 6, false)), "foo a minimal buffer",
1817        c(3).into(), " ";
1818        "range before intersect"
1819    )]
1820    #[test_case(
1821        Dot::from(r(14, 19, false)), "this is a minifooffer",
1822        c(13).into(), "i";
1823        "range after intersect"
1824    )]
1825    #[test_case(
1826        Dot::from(r(8, 9, false)), "this is foominimal buffer",
1827        c(11).into(), "m";
1828        "range inside dot"
1829    )]
1830    #[test_case(
1831        Dot::from(r(4, 18, false)), "thisfoouffer",
1832        c(7).into(), "u";
1833        "range containing dot"
1834    )]
1835    #[test_case(
1836        Dot::from(c(4)), "thisfoo is a minimal buffer",
1837        r(8, 19, false).into(), "is a minimal";
1838        "cursor before dot"
1839    )]
1840    #[test_case(
1841        Dot::from(c(18)), "this is a minimal foobuffer",
1842        r(5, 16, false).into(), "is a minimal";
1843        "cursor after dot"
1844    )]
1845    #[test_case(
1846        Dot::from(c(10)), "this is a foominimal buffer",
1847        c(13).into(), "m";
1848        "cursor inside dot"
1849    )]
1850    #[test]
1851    fn insert_xdot_sets_correct_dot(
1852        xdot: Dot,
1853        expected_content: &str,
1854        expected_dot: Dot,
1855        expected_dot_content: &str,
1856    ) {
1857        let mut b = Buffer::new_virtual(0, "test", "this is a minimal buffer", Default::default());
1858        b.xdot = xdot;
1859        b.dot = r(5, 16, false).into();
1860        assert_eq!(b.dot_contents(), "is a minimal");
1861
1862        b.insert_xdot("foo".into());
1863
1864        assert_eq!(b.str_contents(), expected_content);
1865        assert_eq!(b.dot, expected_dot);
1866        assert_eq!(b.dot_contents(), expected_dot_content);
1867    }
1868}