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