ad_editor/exec/
mod.rs

1//! Sam style language for executing structural regular expressions against ad Buffers
2use crate::{
3    buffer::{Buffer, GapBuffer},
4    dot::{Cur, Dot},
5    editor::Action,
6    parse::ParseInput,
7    regex::{self, Regex},
8};
9use ad_event::Source;
10use std::{
11    borrow::Cow,
12    cell::RefCell,
13    cmp::min,
14    collections::BTreeMap,
15    fmt,
16    io::{self, Write},
17};
18use structex::{
19    Structex, StructexBuilder,
20    re::{Haystack, Sliceable},
21    template::{self, Context, Template},
22};
23
24mod addr;
25mod runner;
26
27pub use runner::SystemRunner;
28
29pub(crate) use addr::Address;
30pub use addr::{Addr, AddrBase, SimpleAddr};
31pub(crate) use runner::{EditorRunner, Runner};
32
33use addr::ErrorKind;
34
35/// Errors that can be returned by the exec engine
36#[derive(Debug)]
37pub enum Error {
38    /// Format error
39    Format,
40    /// Invalid regex
41    InvalidRegex(regex::Error),
42    /// Invalid structex
43    InvalidStructex(structex::Error),
44    /// Invalid structex template
45    InvalidTemplate(template::Error),
46    /// Invalid suffix
47    InvalidSuffix,
48    /// IO error
49    Io(io::ErrorKind, String),
50    /// Error rendering a structex template
51    Render(template::RenderError),
52    /// Unclosed delimiter
53    UnclosedDelimiter(&'static str, char),
54    /// Unexpected character
55    UnexpectedCharacter(char),
56    /// Unexpected end of file
57    UnexpectedEof,
58    /// A 0 was provided as a line or column index
59    ZeroIndexedLineOrColumn,
60}
61
62impl From<fmt::Error> for Error {
63    fn from(_: fmt::Error) -> Self {
64        Error::Format
65    }
66}
67
68impl From<io::Error> for Error {
69    fn from(err: io::Error) -> Self {
70        Error::Io(err.kind(), err.to_string())
71    }
72}
73
74impl From<regex::Error> for Error {
75    fn from(err: regex::Error) -> Self {
76        Error::InvalidRegex(err)
77    }
78}
79
80impl From<structex::Error> for Error {
81    fn from(err: structex::Error) -> Self {
82        Error::InvalidStructex(err)
83    }
84}
85
86impl From<template::Error> for Error {
87    fn from(err: template::Error) -> Self {
88        Error::InvalidTemplate(err)
89    }
90}
91
92impl From<template::RenderError> for Error {
93    fn from(err: template::RenderError) -> Self {
94        Error::Render(err)
95    }
96}
97
98/// Something that can be edited by a Program
99pub trait Edit: Address + Haystack<Regex> {
100    /// Insert a string at the specified index
101    fn insert(&mut self, ix: usize, s: &str);
102
103    /// Remove all characters from (from..to)
104    fn remove(&mut self, from: usize, to: usize);
105
106    /// Mark the start of an edit transaction
107    fn begin_edit_transaction(&mut self) {}
108
109    /// Mark the end of an edit transaction
110    fn end_edit_transaction(&mut self) {}
111}
112
113impl Edit for GapBuffer {
114    fn insert(&mut self, idx: usize, s: &str) {
115        self.insert_str(idx, s)
116    }
117
118    fn remove(&mut self, from: usize, to: usize) {
119        self.remove_range(from, to);
120    }
121}
122
123impl Edit for Buffer {
124    fn insert(&mut self, idx: usize, s: &str) {
125        self.dot = Dot::Cur { c: Cur { idx } };
126        self.handle_action(Action::InsertString { s: s.to_string() }, Source::Fsys);
127    }
128
129    fn remove(&mut self, from: usize, to: usize) {
130        if from == to {
131            return;
132        }
133        self.dot = Dot::from_char_indices(from, to.saturating_sub(1)).collapse_null_range();
134        self.handle_action(Action::Delete, Source::Fsys);
135    }
136
137    fn begin_edit_transaction(&mut self) {
138        self.new_edit_log_transaction()
139    }
140
141    fn end_edit_transaction(&mut self) {
142        self.new_edit_log_transaction()
143    }
144}
145
146/// A parsed and compiled program that can be executed against an input
147#[derive(Debug, Clone)]
148pub struct Program {
149    initial_addr: Option<Addr>,
150    se: Option<Structex<Regex>>,
151    templates: BTreeMap<usize, Template>,
152}
153
154impl Program {
155    /// Attempt to parse a given program input
156    pub fn try_parse(s: &str) -> Result<Self, Error> {
157        let s = s.trim();
158
159        let input = ParseInput::new(s);
160        let (initial_addr, remaining_input) = match Addr::parse_from_input(&input) {
161            Ok(dot_expr) => (Some(dot_expr), input.remaining()),
162
163            // If the start of input is not an address we fall back to requesting the current dot
164            // from the Edit we are running over during execution and attempt to parse the rest of
165            // the program. We need to reconstruct the iterator here as we may have advanced
166            // through the string while we attempt to parse the initial address.
167            Err(e) => match e.kind {
168                ErrorKind::NotAnAddress => (None, s),
169                ErrorKind::InvalidRegex(e) => return Err(Error::InvalidRegex(e)),
170                ErrorKind::InvalidSuffix => return Err(Error::InvalidSuffix),
171                ErrorKind::UnclosedDelimiter => {
172                    return Err(Error::UnclosedDelimiter("dot expr regex", '/'));
173                }
174                ErrorKind::UnexpectedCharacter(c) => {
175                    return Err(Error::UnexpectedCharacter(c));
176                }
177                ErrorKind::UnexpectedEof => return Err(Error::UnexpectedEof),
178                ErrorKind::ZeroIndexedLineOrColumn => {
179                    return Err(Error::ZeroIndexedLineOrColumn);
180                }
181            },
182        };
183
184        let se: Option<Structex<Regex>> = match StructexBuilder::new(remaining_input)
185            .with_allowed_argless_tags("d")
186            .with_allowed_single_arg_tags("acip$<>|") // typos:ignore
187            .allow_top_level_actions()
188            .require_actions()
189            .build()
190        {
191            Ok(se) => Some(se),
192            Err(structex::Error::Syntax(e)) if e.kind == structex::ErrorKind::EmptyExpression => {
193                None
194            }
195            Err(e) => return Err(e.into()),
196        };
197
198        let mut templates = BTreeMap::new();
199        if let Some(se) = se.as_ref() {
200            for action in se.actions() {
201                if let Some(arg) = action.arg() {
202                    let t = Template::parse(arg)?;
203                    templates.insert(action.id(), t);
204                }
205            }
206        }
207
208        Ok(Self {
209            initial_addr,
210            se,
211            templates,
212        })
213    }
214
215    /// Execute this program against a given [Edit].
216    pub fn execute<'a, E, R, W>(
217        &self,
218        ed: &'a mut E,
219        runner: &mut R,
220        fname: &str,
221        out: &mut W,
222    ) -> Result<Dot, Error>
223    where
224        E: Edit,
225        for<'s> <E as Sliceable>::Slice<'s>: Into<Cow<'s, str>>,
226        R: Runner,
227        W: Write,
228    {
229        let mut dot = match self.initial_addr.as_ref() {
230            Some(addr) => ed.map_addr(addr),
231            None => ed.current_dot(),
232        };
233
234        if self.se.is_none() {
235            return Ok(dot);
236        };
237
238        let (char_from, char_to) = dot.as_char_indices();
239        let byte_from = ed.char_to_byte(char_from).unwrap();
240        let byte_to = ed
241            .char_to_byte(char_to.saturating_add(1))
242            .unwrap_or_else(|| ed.len_bytes());
243
244        let mut edit_actions = self.gather_actions(byte_from, byte_to, ed, runner, fname, out)?;
245
246        ed.begin_edit_transaction();
247
248        // Determine the dot we need to set by applying the last action. All other actions update
249        // this selection based on how they manipulate the buffer so we need to special case this
250        // final action in order to not double-count the delta it would generate.
251        let last_action = edit_actions.pop();
252        let mut delta = 0;
253        if let Some(action) = last_action {
254            dot = action.as_dot(ed);
255            action.apply(ed);
256        }
257
258        // apply remaining actions in reverse order, updating the final dot position accordingly
259        for action in edit_actions.into_iter().rev() {
260            delta += action.apply(ed);
261        }
262
263        ed.end_edit_transaction();
264
265        // In the case of running against a lazy stream our initial `to` will be a sential value of
266        // usize::MAX which needs to be clamped to the size of the input. For Buffers and GapBuffers
267        // where we know that we should already be in bounds this is not required but the overhead
268        // of always doing it is minimal as checking the number of chars in the buffer is O(1) due
269        // to us caching the value.
270        let ix_max = ed.len_chars();
271
272        // Apply the cumulative delta from all edit actions to the final dot position to account
273        // for changes made to the buffer state.
274        let (from, to) = dot.as_char_indices();
275        let from = (from as isize + delta) as usize;
276        let to = (to as isize + delta) as usize;
277
278        Ok(Dot::from_char_indices(min(from, ix_max), min(to, ix_max)))
279    }
280
281    fn gather_actions<'a, E, R, W>(
282        &self,
283        byte_from: usize,
284        byte_to: usize,
285        ed: &'a E,
286        runner: &mut R,
287        fname: &str,
288        out: &mut W,
289    ) -> Result<Vec<EditAction>, Error>
290    where
291        E: Edit,
292        for<'s> <E as Sliceable>::Slice<'s>: Into<Cow<'s, str>>,
293        R: Runner,
294        W: Write,
295    {
296        let se = self.se.as_ref().unwrap();
297        let mut edit_actions = Vec::new();
298        let mut ctx = Ctx {
299            fname,
300            byte_from: 0,
301            ed,
302            row_col: RefCell::new(None),
303        };
304
305        for caps in se.iter_tagged_captures_between(byte_from, byte_to, ed) {
306            let action = caps.action.as_ref().unwrap();
307            let id = action.id();
308            ctx.byte_from = caps.from();
309            ctx.row_col.borrow_mut().take();
310
311            match action.tag() {
312                // Immediate actions
313
314                // Print rendered template
315                'p' => {
316                    self.templates[&id].render_with_context_to(out, &caps, &ctx)?;
317                }
318
319                // Run rendered template as shell command
320                '$' => {
321                    let cmd = self.templates[&id].render_with_context(&caps, &ctx)?;
322                    out.write_all(runner.run_shell_command(&cmd, None)?.as_bytes())?;
323                }
324
325                // Run template as shell command with match as input
326                '>' => {
327                    let cmd = self.templates[&id].render_with_context(&caps, &ctx)?;
328                    let slice = caps.as_slice();
329                    out.write_all(
330                        runner
331                            .run_shell_command(&cmd, Some(slice.into().as_ref()))?
332                            .as_bytes(),
333                    )?;
334                }
335
336                // Edit actions
337
338                // Delete matched text
339                'd' => edit_actions.push(EditAction::Remove(caps.from(), caps.to())),
340
341                // Change matched text to rendered template
342                'c' => {
343                    edit_actions.push(EditAction::Replace(
344                        caps.from(),
345                        caps.to(),
346                        self.templates[&id].render_with_context(&caps, &ctx)?,
347                    ));
348                }
349
350                // Insert rendered template before match
351                'i' => {
352                    edit_actions.push(EditAction::Insert(
353                        caps.from(),
354                        self.templates[&id].render_with_context(&caps, &ctx)?,
355                    ));
356                }
357
358                // Append rendered template after match
359                'a' => {
360                    edit_actions.push(EditAction::Insert(
361                        caps.to(),
362                        self.templates[&id].render_with_context(&caps, &ctx)?,
363                    ));
364                }
365
366                // Replace matched text with output from running rendered template as shell command
367                '<' => {
368                    let cmd = self.templates[&id].render_with_context(&caps, &ctx)?;
369                    edit_actions.push(EditAction::Replace(
370                        caps.from(),
371                        caps.to(),
372                        runner.run_shell_command(&cmd, None)?,
373                    ));
374                }
375
376                // Pipe matched text through running rendered template as a shell command
377                '|' => {
378                    let cmd = self.templates[&id].render_with_context(&caps, &ctx)?;
379                    let slice = caps.as_slice();
380                    edit_actions.push(EditAction::Replace(
381                        caps.from(),
382                        caps.to(),
383                        runner.run_shell_command(&cmd, Some(slice.into().as_ref()))?,
384                    ));
385                }
386
387                _ => unreachable!(),
388            }
389        }
390
391        Ok(edit_actions)
392    }
393}
394
395#[derive(Debug)]
396enum EditAction {
397    Insert(usize, String),
398    Remove(usize, usize),
399    Replace(usize, usize, String),
400}
401
402impl EditAction {
403    fn as_dot<E>(&self, ed: &mut E) -> Dot
404    where
405        E: Edit,
406    {
407        match self {
408            Self::Insert(from, s) | Self::Replace(from, _, s) => {
409                let from = ed.byte_to_char(*from).unwrap();
410                let n_chars = s.chars().count();
411
412                Dot::from_char_indices(from, from + n_chars - 1)
413            }
414
415            Self::Remove(from, _) => {
416                let from = ed.byte_to_char(*from).unwrap();
417                Dot::from_char_indices(from, from)
418            }
419        }
420    }
421
422    fn apply<E>(self, ed: &mut E) -> isize
423    where
424        E: Edit,
425    {
426        match self {
427            Self::Insert(from, s) => {
428                let from = ed.byte_to_char(from).unwrap();
429                ed.insert(from, &s);
430                s.chars().count() as isize
431            }
432
433            Self::Remove(from, to) => {
434                let from = ed.byte_to_char(from).unwrap();
435                let to = ed.byte_to_char(to).unwrap();
436                ed.remove(from, to);
437                -((to - from) as isize)
438            }
439
440            Self::Replace(from, to, s) => {
441                Self::Remove(from, to).apply(ed);
442                let n_chars = Self::Insert(from, s).apply(ed);
443                n_chars - (to - from) as isize
444            }
445        }
446    }
447}
448
449struct Ctx<'a, E>
450where
451    E: Edit,
452{
453    fname: &'a str,
454    byte_from: usize,
455    ed: &'a E,
456    row_col: RefCell<Option<(String, String)>>,
457}
458
459impl<'a, E> Ctx<'a, E>
460where
461    E: Edit,
462{
463    fn ensure_row_col(&self) {
464        if self.row_col.borrow().is_some() {
465            return;
466        }
467
468        let char_from = self.ed.byte_to_char(self.byte_from).unwrap();
469        let row = self.ed.char_to_line(char_from).unwrap();
470        let col = char_from - self.ed.line_to_char(row).unwrap();
471
472        *self.row_col.borrow_mut() = Some((row.to_string(), col.to_string()));
473    }
474}
475
476impl<'a, E> Context for Ctx<'a, E>
477where
478    E: Edit,
479{
480    fn render_var<W>(&self, var: &str, w: &mut W) -> Option<io::Result<usize>>
481    where
482        W: Write,
483    {
484        match var {
485            "FILENAME" => Some(w.write_all(self.fname.as_bytes()).map(|_| self.fname.len())),
486
487            "ROW" => {
488                self.ensure_row_col();
489                let rc = self.row_col.borrow();
490                let row = &rc.as_ref().unwrap().0;
491
492                Some(w.write_all(row.as_bytes()).map(|_| row.len()))
493            }
494
495            "COL" => {
496                self.ensure_row_col();
497                let rc = self.row_col.borrow();
498                let col = &rc.as_ref().unwrap().1;
499
500                Some(w.write_all(col.as_bytes()).map(|_| col.len()))
501            }
502
503            _ => None,
504        }
505    }
506}
507
508#[cfg(test)]
509mod tests {
510    use super::*;
511    use crate::{buffer::Buffer, editor::Action};
512    use simple_test_case::test_case;
513    use std::{collections::HashMap, env, io};
514
515    #[test_case(", x/(t.)/ c/{1}X/", "thXis is a teXst XstrXing"; "x c")]
516    #[test_case(", x/(t.)/ i/{1}/", "ththis is a tetest t strtring"; "x i")]
517    #[test_case(", x/(t.)/ a/{1}/", "ththis is a tetest t strtring"; "x a")]
518    #[test]
519    fn substitution_of_submatches_works(s: &str, expected: &str) {
520        let prog = Program::try_parse(s).unwrap();
521        let mut runner = SystemRunner::new(env::current_dir().unwrap());
522
523        let mut b = Buffer::new_unnamed(0, "this is a test string", Default::default());
524        prog.execute(&mut b, &mut runner, "test", &mut Vec::new())
525            .unwrap();
526
527        assert_eq!(&b.txt.to_string(), expected);
528    }
529
530    #[test]
531    fn templating_context_vars_works() {
532        // FILENAME, ROW, and COL all need to be worked out from the buffer being run against
533        let prog = Program::try_parse(", x/line/ a/ ({FILENAME} {ROW}:{COL})/").unwrap();
534        let mut runner = SystemRunner::new(env::current_dir().unwrap());
535
536        let mut b = Buffer::new_unnamed(
537            0,
538            " │  line one\n世 line two\n   🦊  line three",
539            Default::default(),
540        );
541
542        prog.execute(&mut b, &mut runner, "test", &mut Vec::new())
543            .unwrap();
544
545        assert_eq!(
546            &b.txt.to_string(),
547            // the column offsets here should be in terms of characters, not bytes
548            " │  line (test 0:4) one\n世 line (test 1:2) two\n   🦊  line (test 2:6) three"
549        );
550    }
551
552    #[test]
553    fn loop_between_generates_the_correct_blocks() {
554        let prog = Program::try_parse(", y/ / p/>{0}<\n/").unwrap();
555        let mut b = Buffer::new_unnamed(0, "this and that", Default::default());
556        let mut runner = SystemRunner::new(env::current_dir().unwrap());
557        let mut output = Vec::new();
558        let dot = prog
559            .execute(&mut b, &mut runner, "test", &mut output)
560            .unwrap();
561
562        let s = String::from_utf8(output).unwrap();
563        assert_eq!(s, ">this<\n>and<\n>that<\n");
564
565        let dot_content = dot.content(&b);
566        assert_eq!(dot_content, "this and that");
567    }
568
569    #[test_case(0, "/oo.fo/ d", "fo│foo"; "regex dot delete")] // typos:ignore
570    #[test_case(2, "-/f/,/f/ d", "oo│foo"; "regex dot range delete")]
571    #[test_case(0, ", x/foo/ p/{0}/", "foo│foo│foo"; "x print")]
572    #[test_case(0, ", x/foo/ i/X/", "Xfoo│Xfoo│Xfoo"; "x insert")]
573    #[test_case(0, ", x/foo/ a/X/", "fooX│fooX│fooX"; "x append")]
574    #[test_case(0, ", x/foo/ c/X/", "X│X│X"; "x change")]
575    #[test_case(0, ", x/foo/ c/XX/", "XX│XX│XX"; "x change 2")]
576    #[test_case(0, ", x/foo/ d", "││"; "x delete")]
577    #[test_case(0, ", y/foo/ p/>{0}</", "foo│foo│foo"; "y print")]
578    #[test_case(0, ", y/foo/ i/X/", "fooX│fooX│foo"; "y insert")]
579    #[test_case(0, ", y/foo/ a/X/", "foo│Xfoo│Xfoo"; "y append")]
580    #[test_case(0, ", y/foo/ c/X/", "fooXfooXfoo"; "y change")]
581    #[test_case(0, ", y/foo/ d", "foofoofoo"; "y delete")]
582    #[test_case(0, ", y/│/ d", "││"; "y delete 2")]
583    #[test_case(0, ", x/\\b\\w+\\b/ c/X/", "X│X│X"; "change each word")]
584    #[test]
585    fn execute_produces_the_correct_string(idx: usize, s: &str, expected: &str) {
586        let prog = Program::try_parse(s).unwrap();
587        let mut runner = SystemRunner::new(env::current_dir().unwrap());
588
589        let mut b = Buffer::new_unnamed(0, "foo│foo│foo", Default::default());
590        b.dot = Cur::new(idx).into();
591        prog.execute(&mut b, &mut runner, "test", &mut vec![])
592            .unwrap();
593
594        assert_eq!(&b.txt.to_string(), expected, "buffer");
595    }
596
597    #[test]
598    fn multiline_file_dot_star_works() {
599        let prog = Program::try_parse(", x/.*/ c/foo/").unwrap();
600        let mut runner = SystemRunner::new(env::current_dir().unwrap());
601        let mut b = Buffer::new_unnamed(0, "this is\na multiline\nfile", Default::default());
602        prog.execute(&mut b, &mut runner, "test", &mut vec![])
603            .unwrap();
604
605        // '.*' will match the null string at the end of lines containing a newline as well
606        assert_eq!(&b.txt.to_string(), "foofoo\nfoofoo\nfoo");
607    }
608
609    #[test]
610    fn multiline_file_dot_plus_works() {
611        let prog = Program::try_parse(", x/.+/ c/foo/").unwrap();
612        let mut runner = SystemRunner::new(env::current_dir().unwrap());
613        let mut b = Buffer::new_unnamed(0, "this is\na multiline\nfile", Default::default());
614        prog.execute(&mut b, &mut runner, "test", &mut vec![])
615            .unwrap();
616
617        assert_eq!(&b.txt.to_string(), "foo\nfoo\nfoo");
618    }
619
620    #[test]
621    fn buffer_current_dot_is_used_when_there_is_no_leading_addr() {
622        // The only thing this program does is delete the selection which should be the current
623        // buffer dot rather than the entire buffer.
624        let prog = Program::try_parse("d").unwrap();
625        let mut runner = SystemRunner::new(env::current_dir().unwrap());
626
627        let initial_content = "this is a FOO line\nand another";
628        let mut b = Buffer::new_unnamed(0, initial_content, Default::default());
629        b.dot = Dot::from_char_indices(9, 12);
630        assert_eq!(b.dot_contents(), " FOO");
631
632        prog.execute(&mut b, &mut runner, "test", &mut vec![])
633            .unwrap();
634        assert_eq!(&b.str_contents(), "this is a line\nand another");
635    }
636
637    #[test_case(", x/a/ d", "foo br bz", "z", (8, 8); "extract delete")]
638    #[test_case(", x/a/ i/12/", "foo b12ar b12az", "12", (11, 12); "extract insert")]
639    #[test_case(", x/o/ a/XYZ/", "foXYZoXYZ bar baz", "XYZ", (6, 8); "extract append")] // typos:ignore
640    #[test_case(", x/b/ c/B/", "foo Bar Baz", "B", (8, 8); "extract change same length")]
641    #[test_case(", x/b./ c/X/", "foo Xr Xz", "X", (7, 7); "extract change shorter")]
642    #[test_case(", x/b/ c/Bee/", "foo Beear Beeaz", "Bee", (10, 12); "extract change longer")]
643    #[test_case(", x/b../ p/{0}/", "foo bar baz", "foo bar baz", (0, 11); "print should keep original")]
644    #[test]
645    fn returned_dot_should_hold_the_final_edit(
646        s: &str,
647        expected_content: &str,
648        expected_dot_content: &str,
649        expected_dot: (usize, usize),
650    ) {
651        let prog = Program::try_parse(s).unwrap();
652        let mut runner = SystemRunner::new(env::current_dir().unwrap());
653
654        let initial_content = "foo bar baz";
655        let mut b = Buffer::new_unnamed(0, initial_content, Default::default());
656
657        let dot = prog
658            .execute(&mut b, &mut runner, "test", &mut vec![])
659            .unwrap();
660
661        assert_eq!(&b.str_contents(), expected_content);
662        assert_eq!(&dot.content(&b), expected_dot_content);
663        assert_eq!(dot.as_char_indices(), expected_dot);
664    }
665
666    #[test_case(", d"; "delete buffer")]
667    #[test_case(", x/th/ d"; "delete each th")]
668    #[test_case(", x/ / d"; "delete spaces")]
669    #[test_case(", x/\\b\\w+\\b/ d"; "delete each word")]
670    #[test_case(", x/. / d"; "delete things before a space")]
671    #[test_case(", x/\\b\\w+\\b/ c/buffalo/"; "change each word")]
672    #[test_case(", x/\\b\\w+\\b/ a/buffalo/"; "append to each word")]
673    #[test_case(", x/\\b\\w+\\b/ i/buffalo/"; "insert before each word")]
674    #[test]
675    fn buffer_execute_undo_all_is_a_noop(s: &str) {
676        let prog = Program::try_parse(s).unwrap();
677        let mut runner = SystemRunner::new(env::current_dir().unwrap());
678        let initial_content = "this is a line\nand another\n- [ ] something to do\n";
679        let mut b = Buffer::new_unnamed(0, initial_content, Default::default());
680
681        prog.execute(&mut b, &mut runner, "test", &mut vec![])
682            .unwrap();
683        while b.handle_action(Action::Undo, Source::Keyboard).is_none() {}
684        let final_content = b.str_contents();
685
686        assert_eq!(&final_content, initial_content);
687    }
688
689    struct MockRunner {
690        responses: HashMap<(String, Option<String>), io::Result<String>>,
691    }
692
693    impl MockRunner {
694        fn new() -> Self {
695            Self {
696                responses: HashMap::new(),
697            }
698        }
699
700        fn with_response(mut self, cmd: &str, output: &str) -> Self {
701            self.responses
702                .insert((cmd.to_string(), None), Ok(output.to_string()));
703            self
704        }
705
706        fn with_response_for_input(mut self, cmd: &str, input: &str, output: &str) -> Self {
707            self.responses.insert(
708                (cmd.to_string(), Some(input.to_string())),
709                Ok(output.to_string()),
710            );
711            self
712        }
713
714        fn with_failure_for_input(mut self, cmd: &str, input: &str, error_msg: &str) -> Self {
715            self.responses.insert(
716                (cmd.to_string(), Some(input.to_string())),
717                Err(io::Error::other(error_msg)),
718            );
719            self
720        }
721    }
722
723    impl Runner for MockRunner {
724        fn run_shell_command(&mut self, cmd: &str, input: Option<&str>) -> io::Result<String> {
725            let key = (cmd.to_string(), input.map(|s| s.to_string()));
726
727            match self.responses.get(&key) {
728                Some(Ok(output)) => Ok(output.clone()),
729                Some(Err(e)) => Err(io::Error::new(e.kind(), e.to_string())),
730                None => panic!(
731                    "MockRunner: no response configured for command {cmd:?} with input {input:?}"
732                ),
733            }
734        }
735    }
736
737    #[test]
738    fn runner_based_action_errors_are_returned() {
739        let prog = Program::try_parse(", x/foo/ >/fail/").unwrap();
740        let mut b = Buffer::new_unnamed(0, "foo bar", Default::default());
741        let mut runner = MockRunner::new().with_failure_for_input("fail", "foo", "error");
742
743        let result = prog.execute(&mut b, &mut runner, "test", &mut Vec::new());
744
745        assert!(result.is_err());
746        assert_eq!(b.str_contents(), "foo bar");
747    }
748
749    #[test_case("foo", ", x/foo/ $/cmd/", "cmd", "X", "foo", "X"; "simple output")]
750    #[test_case(" foo", ", x/foo/ $/cmd {FILENAME} {ROW} {COL}/", "cmd test 0 1", "ok\n", " foo", "ok\n"; "context variables")]
751    #[test_case("foo", ", x/foo/ $/empty/", "empty", "", "foo", ""; "empty output")]
752    #[test_case("foo", ", x/foo/ $/multi/", "multi", "line1\nline2\n", "foo", "line1\nline2\n"; "multiline output")]
753    #[test_case("foo bar foo", ", x/foo/ $/cmd/", "cmd", "X", "foo bar foo", "XX"; "multiple matches")]
754    #[test]
755    fn shell_dollar_action_works(
756        initial: &str,
757        program: &str,
758        mock_cmd: &str,
759        mock_output: &str,
760        expected_buffer: &str,
761        expected_output: &str,
762    ) {
763        let prog = Program::try_parse(program).unwrap();
764        let mut b = Buffer::new_unnamed(0, initial, Default::default());
765        let mut runner = MockRunner::new().with_response(mock_cmd, mock_output);
766        let mut output = Vec::new();
767
768        prog.execute(&mut b, &mut runner, "test", &mut output)
769            .unwrap();
770
771        assert_eq!(b.str_contents(), expected_buffer);
772        assert_eq!(String::from_utf8(output).unwrap(), expected_output);
773    }
774
775    #[test_case("foo", ", x/foo/ >/cat/", "cat", "foo", "FOO", "foo", "FOO"; "simple")]
776    #[test_case("foo\nbar\nbaz", ", x/foo\\nbar/ >/process/", "process", "foo\nbar", "processed", "foo\nbar\nbaz", "processed"; "multiline input")]
777    #[test_case("word", ", x/(\\w+)/ >/process {1}/", "process word", "word", "result", "word", "result"; "template in command")]
778    #[test]
779    fn shell_redirect_in_action(
780        initial: &str,
781        program: &str,
782        mock_cmd: &str,
783        mock_input: &str,
784        mock_output: &str,
785        expected_buffer: &str,
786        expected_output: &str,
787    ) {
788        let prog = Program::try_parse(program).unwrap();
789        let mut b = Buffer::new_unnamed(0, initial, Default::default());
790        let mut runner =
791            MockRunner::new().with_response_for_input(mock_cmd, mock_input, mock_output);
792        let mut output = Vec::new();
793
794        prog.execute(&mut b, &mut runner, "test", &mut output)
795            .unwrap();
796
797        assert_eq!(b.str_contents(), expected_buffer);
798        assert_eq!(String::from_utf8(output).unwrap(), expected_output);
799    }
800
801    #[test_case("this foo that", ", x/foo/ </cmd/", "cmd", "bar", "this bar that"; "simple replacement")]
802    #[test_case("foo foo foo", ", x/foo/ </cmd/", "cmd", "X", "X X X"; "multiple matches")]
803    #[test_case("word", ", x/(\\w+)/ </process {1}/", "process word", "WORD", "WORD"; "template in command")]
804    #[test_case("foo bar foo", ", x/foo/ </cmd/", "cmd", "", " bar "; "empty output deletes")]
805    #[test_case("foo", ", x/foo/ </cmd/", "cmd", "line1\nline2\n", "line1\nline2\n"; "multiline output")]
806    #[test]
807    fn shell_redirect_out_action(
808        initial: &str,
809        program: &str,
810        mock_cmd: &str,
811        mock_output: &str,
812        expected_buffer: &str,
813    ) {
814        let prog = Program::try_parse(program).unwrap();
815        let mut b = Buffer::new_unnamed(0, initial, Default::default());
816        let mut runner = MockRunner::new().with_response(mock_cmd, mock_output);
817        let mut output = Vec::new();
818
819        prog.execute(&mut b, &mut runner, "test", &mut output)
820            .unwrap();
821
822        assert_eq!(b.str_contents(), expected_buffer);
823    }
824
825    #[test]
826    fn shell_redirect_out_action_sets_dot() {
827        let prog = Program::try_parse(", x/foo/ </cmd/").unwrap();
828        let mut runner = MockRunner::new().with_response("cmd", "REPLACEMENT");
829        let mut b = Buffer::new_unnamed(0, "foo bar foo", Default::default());
830        let mut output = Vec::new();
831
832        let dot = prog
833            .execute(&mut b, &mut runner, "test", &mut output)
834            .unwrap();
835
836        assert_eq!(dot.content(&b), "REPLACEMENT");
837    }
838
839    #[test_case("this foo that", ", x/foo/ |/upper/", "upper", "foo", "FOO", "this FOO that"; "simple transformation")]
840    #[test_case("foo\nbar\nbaz", ", x/foo\\nbar/ |/transform/", "transform", "foo\nbar", "transformed", "transformed\nbaz"; "multiline match")]
841    #[test_case("word", ", x/(\\w+)/ |/process {1}/", "process word", "word", "WORD", "WORD"; "template in command")]
842    #[test_case("foo\tbar\n", ", |/cmd/", "cmd", "foo\tbar\n", "transformed", "transformed"; "special chars")]
843    #[test]
844    fn shell_pipe_action(
845        initial: &str,
846        program: &str,
847        mock_cmd: &str,
848        mock_input: &str,
849        mock_output: &str,
850        expected_buffer: &str,
851    ) {
852        let prog = Program::try_parse(program).unwrap();
853        let mut b = Buffer::new_unnamed(0, initial, Default::default());
854        let mut runner =
855            MockRunner::new().with_response_for_input(mock_cmd, mock_input, mock_output);
856
857        prog.execute(&mut b, &mut runner, "test", &mut Vec::new())
858            .unwrap();
859
860        assert_eq!(b.str_contents(), expected_buffer);
861    }
862
863    #[test]
864    fn shell_pipe_action_sets_dot() {
865        let prog = Program::try_parse(", x/foo/ |/expand/").unwrap();
866        let mut b = Buffer::new_unnamed(0, "foo bar", Default::default());
867        let mut runner =
868            MockRunner::new().with_response_for_input("expand", "foo", "much longer replacement");
869
870        let dot = prog
871            .execute(&mut b, &mut runner, "test", &mut Vec::new())
872            .unwrap();
873
874        assert_eq!(b.str_contents(), "much longer replacement bar");
875        assert_eq!(dot.content(&b), "much longer replacement");
876    }
877
878    #[test_case("/[unclosed/"; "invalid regex forward")]
879    #[test_case("-/[unclosed/"; "invalid regex backward")]
880    #[test_case("/foo/,/[bad/"; "invalid regex in compound")]
881    #[test_case("/(?P<invalid>/"; "invalid regex special chars")]
882    #[test]
883    fn try_parse_invalid_regex_returns_error(input: &str) {
884        let res = Program::try_parse(input);
885        assert!(matches!(res, Err(Error::InvalidRegex(_))));
886    }
887
888    #[test_case("/foo"; "forward regex no closing")]
889    #[test_case("-/bar"; "backward regex no closing")]
890    #[test_case("/foo/,/bar"; "compound second unclosed")]
891    #[test]
892    fn try_parse_unclosed_delimiter_returns_error(input: &str) {
893        let res = Program::try_parse(input);
894        assert!(matches!(res, Err(Error::UnclosedDelimiter(_, '/'))));
895    }
896
897    #[test_case("5:@"; "unexpected char after colon")]
898    #[test_case("5:@10"; "unexpected char before column")]
899    #[test]
900    fn try_parse_unexpected_character_returns_error(input: &str) {
901        let res = Program::try_parse(input);
902        assert!(matches!(res, Err(Error::UnexpectedCharacter(_))));
903    }
904
905    #[test_case("1:0"; "zero column")]
906    #[test_case("2:00"; "zero column with double zero")]
907    #[test_case("10:000"; "zero column with triple zero")]
908    #[test]
909    fn try_parse_zero_indexed_line_or_column_returns_error(input: &str) {
910        let res = Program::try_parse(input);
911        assert!(matches!(res, Err(Error::ZeroIndexedLineOrColumn)));
912    }
913
914    #[test_case("#,5"; "incomplete char addr in compound start")]
915    #[test_case("+#,10"; "incomplete relative char in compound start")]
916    #[test_case("-#,"; "incomplete relative char back in compound")]
917    #[test_case("#"; "char addr at eof")]
918    #[test_case("+#"; "relative char forward at eof")]
919    #[test_case("-#"; "relative char back at eof")]
920    #[test]
921    fn try_parse_malformed_leading_address_returns_error(addr: &str) {
922        let res = Program::try_parse(&format!("{addr} x/../ d"));
923        assert!(res.is_err(), "expected error, got {res:?}");
924    }
925
926    #[test_case(","; "omitted start defaults to bof")]
927    #[test_case(",5"; "omitted start with line end")]
928    #[test_case(",/foo/"; "omitted start with regex end")]
929    #[test_case(",$"; "omitted start with eof")]
930    #[test]
931    fn try_parse_omitted_leading_address_works(addr: &str) {
932        let res = Program::try_parse(&format!("{addr} x/../ d"));
933        assert!(res.is_ok(), "expected OK, got {res:?}");
934        assert!(res.unwrap().initial_addr.is_some());
935    }
936
937    #[test_case("#"; "char addr incomplete")]
938    #[test_case("+#"; "relative char forward incomplete")]
939    #[test_case("-#"; "relative char back incomplete")]
940    #[test]
941    fn try_parse_unexpected_eof_returns_error(addr: &str) {
942        let res = Program::try_parse(&format!("{addr} x/../ d"));
943        // The exact error we get here depends on how the address is malformed. We deliberately
944        // swallow "NotAnAddress" and try to parse the full input as a Structex if possible.
945        assert!(res.is_err(), "expected error, got {res:?}");
946    }
947
948    #[test_case("x/foo/ d"; "x")]
949    #[test_case("y/bar/ c/X/"; "y")]
950    #[test_case("d"; "d")]
951    #[test]
952    fn try_parse_no_leading_address_with_action_works(input: &str) {
953        let res = Program::try_parse(input);
954        assert!(res.is_ok(), "expected OK, got {res:?}");
955        assert!(res.unwrap().initial_addr.is_none());
956    }
957
958    #[test_case("5,#"; "incomplete char end")]
959    #[test_case("/foo/,+#"; "incomplete relative char end")]
960    #[test_case("1,#abc"; "char with non-digit")]
961    #[test]
962    fn try_parse_malformed_trailing_address_returns_error(addr: &str) {
963        let res = Program::try_parse(&format!("{addr} x/../ d"));
964        assert!(res.is_err(), "expected error, got {res:?}");
965    }
966
967    #[test_case(", x/foo/ c/{0/"; "change action unclosed submatch")]
968    #[test_case(", x/foo/ i/{1/"; "insert action unclosed submatch")]
969    #[test_case(", x/foo/ a/{FILENAME/"; "append action unclosed variable")]
970    #[test_case(", x/foo/ p/{ROW/"; "print action unclosed variable")]
971    #[test_case(", x/foo/ $/cmd {0/"; "shell dollar action unclosed")]
972    #[test_case(", x/foo/ >/cmd {1/"; "shell redirect in unclosed")]
973    #[test_case(", x/foo/ </cmd {2/"; "shell redirect out unclosed")]
974    #[test_case(", x/foo/ |/cmd {COL/"; "shell pipe unclosed")]
975    #[test]
976    fn try_parse_template_unclosed_brace_returns_error(input: &str) {
977        let res = Program::try_parse(input);
978        assert!(matches!(res, Err(Error::InvalidTemplate(_))));
979    }
980
981    #[test_case(", x/foo/ c/\\x/"; "escape x not valid")]
982    #[test_case(", x/foo/ i/\\z/"; "escape z not valid")]
983    #[test_case(", x/foo/ a/\\r/"; "escape r not valid")]
984    #[test_case(", x/foo/ p/\\b/"; "escape b not valid")]
985    #[test_case(", x/foo/ c/foo\\qbar/"; "escape q in middle")]
986    #[test_case(", x/foo/ i/\\d/"; "escape d not valid")]
987    #[test_case(", x/foo/ $/cmd \\w/"; "escape w in shell command")]
988    #[test]
989    fn try_parse_template_invalid_escape_returns_error(input: &str) {
990        let res = Program::try_parse(input);
991        assert!(matches!(res, Err(Error::InvalidTemplate(_))));
992    }
993
994    // Note: Some apparent template EOF errors are actually caught by structex as
995    // MissingDelimiter errors. For example:
996    // - ", x/foo/ i/\\/" - backslash escapes the closing '/', causing structex error
997    // - ", x/foo/ a/{0" - missing closing '/', caught by structex before template parsing
998    #[test_case(", x/foo/ c/{/"; "eof after opening brace")]
999    #[test_case(", x/foo/ p/{F/"; "eof in middle of variable")]
1000    #[test_case(", x/foo/ $/cmd {/"; "shell command eof after brace")]
1001    #[test]
1002    fn try_parse_template_unexpected_eof_returns_error(input: &str) {
1003        let res = Program::try_parse(input);
1004        assert!(matches!(res, Err(Error::InvalidTemplate(_))));
1005    }
1006
1007    #[test_case(", x/foo/ p/{UNKNOWN}/"; "print with unknown variable")]
1008    #[test_case(", x/foo/ c/{INVALID_VAR}/"; "change with unknown variable")]
1009    #[test_case(", x/foo/ i/{NOTDEFINED}/"; "insert with unknown variable")]
1010    #[test_case(", x/foo/ a/{BADVAR}/"; "append with unknown variable")]
1011    #[test]
1012    fn execute_with_unknown_variable_returns_error(input: &str) {
1013        let program = Program::try_parse(input).unwrap();
1014        let mut buffer = Buffer::new_unnamed(0, "foo bar", Default::default());
1015        let mut runner = MockRunner::new();
1016        let mut output = Vec::new();
1017
1018        let res = program.execute(&mut buffer, &mut runner, "test.txt", &mut output);
1019        assert!(matches!(res, Err(Error::Render(_))));
1020    }
1021
1022    /// A Writer that always returns IO errors
1023    struct FailingWriter;
1024
1025    impl io::Write for FailingWriter {
1026        fn write(&mut self, _buf: &[u8]) -> io::Result<usize> {
1027            Err(io::Error::other("mock write failure"))
1028        }
1029
1030        fn flush(&mut self) -> io::Result<()> {
1031            Err(io::Error::other("mock flush failure"))
1032        }
1033    }
1034
1035    #[test]
1036    fn execute_print_with_failing_writer_returns_io_error() {
1037        let program = Program::try_parse(", x/foo/ p/{0}/").unwrap();
1038        let mut buffer = Buffer::new_unnamed(0, "foo bar foo", Default::default());
1039        let mut runner = MockRunner::new();
1040
1041        let res = program.execute(&mut buffer, &mut runner, "test.txt", &mut FailingWriter);
1042        assert!(matches!(res, Err(Error::Render(_))));
1043    }
1044
1045    #[test]
1046    fn execute_shell_dollar_with_failing_writer_returns_io_error() {
1047        let program = Program::try_parse(", x/foo/ $/echo test/").unwrap();
1048        let mut buffer = Buffer::new_unnamed(0, "foo", Default::default());
1049        let mut runner = MockRunner::new().with_response("echo test", "output");
1050
1051        let res = program.execute(&mut buffer, &mut runner, "test.txt", &mut FailingWriter);
1052        assert!(matches!(res, Err(Error::Io(..))));
1053    }
1054
1055    #[test_case(0, ".,. d", "foo│bar│baz", "oo│bar│baz", (0, 0); "dot current position")]
1056    #[test_case(5, ".,. d", "foo│bar│baz", "foo│br│baz", (5, 5); "dot at delimiter")]
1057    #[test_case(0, "0,0 d", "foo│bar│baz", "oo│bar│baz", (0, 0); "bof beginning of file")]
1058    #[test_case(0, "$,$ d", "foo│bar│baz", "foo│bar│baz", (11, 11); "eof end of file")]
1059    #[test_case(0, "-,- d", "line1\nline2\nline3", "ine1\nline2\nline3", (0, 0); "bol at line start")] // typos:ignore
1060    #[test_case(3, "-,- d", "line1\nline2\nline3", "1\nline2\nline3", (0, 0); "bol from mid line")]
1061    #[test_case(0, "+,+ d", "line1\nline2", "ine2", (0, 0); "eol from line start")] // typos:ignore
1062    #[test_case(2, "+,+ d", "line1\nline2", "liine2", (2, 2); "eol from mid line")]
1063    #[test_case(0, "-,+ d", "line1\nline2", "ine2", (0, 0); "current line from start")] // typos:ignore
1064    #[test_case(3, "-,+ d", "line1\nline2", "ine2", (0, 0); "current line from middle")] // typos:ignore
1065    #[test_case(0, "2,2 d", "line1\nline2\nline3", "line1\nline3", (6, 6); "absolute line 2")]
1066    #[test_case(0, "1,1 d", "line1\nline2", "line2", (0, 0); "absolute line 1")]
1067    #[test_case(0, "#5,#5 d", "0123456789", "012346789", (5, 5); "absolute char offset")]
1068    #[test_case(0, "#0,#0 d", "hello world", "ello world", (0, 0); "char offset at start")]
1069    #[test_case(5, "+2,+2 d", "L1\nL2\nL3\nL4", "L1\nL2\nL3\n4", (9, 9); "relative line forward")]
1070    #[test_case(10, "-2,-2 d", "L1\nL2\nL3\nL4", "L1\nL3\nL4", (3, 3); "relative line backward")]
1071    #[test_case(5, "+#3,+#3 d", "hello world", "hello wold", (8, 8); "relative char forward")]
1072    #[test_case(8, "-#3,-#3 d", "hello world", "helloworld", (5, 5); "relative char backward")]
1073    #[test_case(0, "2:3,2:3 d", "L1\nL2\nL3", "L1\nL2L3", (5, 5); "line and column")]
1074    #[test_case(0, "1:1,1:1 d", "hello\nworld", "ello\nworld", (0, 0); "line 1 col 1")]
1075    #[test_case(0, "2:1,2:1 d", "hello\nworld", "hello\norld", (6, 6); "second line first col")]
1076    #[test]
1077    fn address_simple_positions_work(
1078        initial_dot_idx: usize,
1079        program: &str,
1080        initial_content: &str,
1081        expected_content: &str,
1082        expected_dot: (usize, usize),
1083    ) {
1084        let prog = Program::try_parse(program).unwrap();
1085        let mut runner = SystemRunner::new(env::current_dir().unwrap());
1086        let mut b = Buffer::new_unnamed(0, initial_content, Default::default());
1087        b.dot = Cur::new(initial_dot_idx).into();
1088
1089        let dot = prog
1090            .execute(&mut b, &mut runner, "test", &mut vec![])
1091            .unwrap();
1092
1093        assert_eq!(&b.str_contents(), expected_content, "buffer content");
1094        assert_eq!(dot.as_char_indices(), expected_dot, "returned dot");
1095    }
1096
1097    #[test_case("0,/foo/ d", "start\nfoo bar", " bar"; "bof to regex")]
1098    #[test_case("0,$ d", "hello\nworld", ""; "bof to eof entire buffer")]
1099    #[test_case("/foo/,$ d", "hello\nfoo\nbar", "hello\n"; "regex to eof")]
1100    #[test_case("2,4 d", "L1\nL2\nL3\nL4\nL5", "L1\nL5"; "line range")]
1101    #[test_case("#5,#10 d", "0123456789abc", "01234bc"; "char range")]
1102    #[test_case("1:2,2:3 d", "hello\nworld", "hld"; "line:col range")]
1103    #[test_case("-,+ d", "L1\nL2\nL3", "2\nL3"; "bol to eol entire line")]
1104    #[test_case(".,$ d", "foo\nbar\nbaz", ""; "dot to eof")]
1105    #[test_case("0,#10 d", "hello world test", " test"; "bof to char offset")]
1106    #[test_case("2,/end/ d", "start\nL2\nend here", "start\n here"; "line to regex")]
1107    #[test_case("#10,$ d", "0123456789rest", "0123456789"; "char to eof")]
1108    #[test_case("/start/,3 d", "foo\nstart\nL3\nbar", "foo\nbar"; "regex to line")]
1109    #[test]
1110    fn address_compound_ranges_work(program: &str, initial_content: &str, expected_content: &str) {
1111        let prog = Program::try_parse(program).unwrap();
1112        let mut runner = SystemRunner::new(env::current_dir().unwrap());
1113        let mut b = Buffer::new_unnamed(0, initial_content, Default::default());
1114
1115        prog.execute(&mut b, &mut runner, "test", &mut vec![])
1116            .unwrap();
1117
1118        assert_eq!(&b.str_contents(), expected_content);
1119    }
1120
1121    #[test_case(", d", "", ""; "delete on empty buffer")]
1122    #[test_case(", x/foo/ c/bar/", "", ""; "x on empty buffer")]
1123    #[test_case(", y/foo/ c/bar/", "", ""; "y on empty buffer")]
1124    #[test_case("0,$ d", "", ""; "full buffer delete on empty")]
1125    #[test_case(", x/foo/ d", "bar baz qux", "bar baz qux"; "x no matches")]
1126    #[test_case(", x/foo/ c/replacement/", "bar baz", "bar baz"; "x no matches change")]
1127    #[test_case(", y/foo/ i/X/", "bar baz", "Xbar baz"; "y no matches inserts once")]
1128    #[test_case("/foo/ d", "bar baz", "ar baz"; "regex address no match")]
1129    #[test]
1130    fn edge_case_empty_and_no_matches_work(
1131        program: &str,
1132        initial_content: &str,
1133        expected_content: &str,
1134    ) {
1135        let prog = Program::try_parse(program).unwrap();
1136        let mut runner = SystemRunner::new(env::current_dir().unwrap());
1137        let mut b = Buffer::new_unnamed(0, initial_content, Default::default());
1138
1139        prog.execute(&mut b, &mut runner, "test", &mut vec![])
1140            .unwrap();
1141
1142        assert_eq!(&b.str_contents(), expected_content, "buffer content");
1143    }
1144
1145    #[test_case(", x/.*/ i/X/", "foo", "Xfoo"; "zero-length dot star insert")]
1146    #[test_case(", x/.*/ p/{0}/", "a\nb", "a\nb"; "zero-length dot star print")]
1147    #[test_case(", x/^/ i/> /", "foo\nbar", "> f> o> o> \n> b> a> r"; "zero-length line start")]
1148    #[test_case(", x/$/ a/ </", "foo\nbar", " <f <o <o <\n <b <a <r"; "zero-length line end")]
1149    #[test_case(", x/\\b/ i/|/", "foo bar", "|f|o|o| |b|a|r"; "zero-length word boundary")]
1150    #[test_case(", y/.*/ i/X/", "foo", "foo"; "y with zero-length")]
1151    #[test]
1152    fn edge_case_zero_length_matches_work(
1153        program: &str,
1154        initial_content: &str,
1155        expected_content: &str,
1156    ) {
1157        let prog = Program::try_parse(program).unwrap();
1158        let mut runner = SystemRunner::new(env::current_dir().unwrap());
1159        let mut b = Buffer::new_unnamed(0, initial_content, Default::default());
1160
1161        prog.execute(&mut b, &mut runner, "test", &mut vec![])
1162            .unwrap();
1163
1164        assert_eq!(&b.str_contents(), expected_content, "buffer content");
1165    }
1166
1167    #[test]
1168    fn edge_case_very_large_replacement() {
1169        let large_replacement = "X".repeat(10000);
1170        let prog = Program::try_parse(&format!(", x/foo/ c/{}/", large_replacement)).unwrap();
1171        let mut runner = SystemRunner::new(env::current_dir().unwrap());
1172        let mut b = Buffer::new_unnamed(0, "foo bar foo", Default::default());
1173
1174        prog.execute(&mut b, &mut runner, "test", &mut vec![])
1175            .unwrap();
1176
1177        let expected = format!("{} bar {}", large_replacement, large_replacement);
1178        assert_eq!(&b.str_contents(), &expected);
1179    }
1180
1181    #[test]
1182    fn edge_case_large_buffer_with_many_matches() {
1183        let initial = "foo ".repeat(1000);
1184        let prog = Program::try_parse(", x/foo/ c/bar/").unwrap();
1185        let mut runner = SystemRunner::new(env::current_dir().unwrap());
1186        let mut b = Buffer::new_unnamed(0, &initial, Default::default());
1187
1188        prog.execute(&mut b, &mut runner, "test", &mut vec![])
1189            .unwrap();
1190
1191        let expected = "bar ".repeat(1000);
1192        assert_eq!(&b.str_contents(), &expected);
1193    }
1194
1195    #[test_case("0,$", "hello world", "hello world", (0, 11); "full buffer address only")]
1196    #[test_case("/foo/", "bar foo baz", "bar foo baz", (4, 6); "regex address only")]
1197    #[test_case("2", "L1\nL2\nL3", "L1\nL2\nL3", (3, 5); "line address only")]
1198    #[test_case("#5", "hello world", "hello world", (5, 5); "char address only")]
1199    #[test]
1200    fn edge_case_address_only_programs_work(
1201        program: &str,
1202        initial_content: &str,
1203        expected_content: &str,
1204        expected_dot: (usize, usize),
1205    ) {
1206        let prog = Program::try_parse(program).unwrap();
1207        let mut runner = SystemRunner::new(env::current_dir().unwrap());
1208        let mut b = Buffer::new_unnamed(0, initial_content, Default::default());
1209
1210        let dot = prog
1211            .execute(&mut b, &mut runner, "test", &mut vec![])
1212            .unwrap();
1213
1214        assert_eq!(
1215            &b.str_contents(),
1216            expected_content,
1217            "buffer should not change"
1218        );
1219        assert_eq!(dot.as_char_indices(), expected_dot, "dot should be set");
1220    }
1221
1222    #[test_case(", x/./ c/X/", "hello", "XXXXX"; "ascii single char")]
1223    #[test_case(", x/./ c/X/", "世界", "XX"; "multibyte chars")]
1224    #[test_case(", x/./ c/X/", "🦊🐕", "XX"; "emoji")]
1225    #[test_case(", x/./ c/X/", "é", "X"; "e with acute literal")]
1226    #[test_case("#2,#4 d", "世界你好", "世界"; "char offset with multibyte")]
1227    #[test_case(", x/\\w+/ c/X/", "hello世界", "X世界"; "word with mixed scripts")]
1228    #[test_case("1:2,1:4 d", "世界你好", "世"; "line:col with multibyte")]
1229    #[test]
1230    fn edge_case_unicode_works(program: &str, initial_content: &str, expected_content: &str) {
1231        let prog = Program::try_parse(program).unwrap();
1232        let mut runner = SystemRunner::new(env::current_dir().unwrap());
1233        let mut b = Buffer::new_unnamed(0, initial_content, Default::default());
1234
1235        prog.execute(&mut b, &mut runner, "test", &mut vec![])
1236            .unwrap();
1237
1238        assert_eq!(&b.str_contents(), expected_content, "buffer content");
1239    }
1240
1241    // This is a regression test for a panic found when testing the program being run here. This
1242    // should replace each character in the buffer with an "X": originally this incorrectly treated
1243    // the "é" character as two distinct characters and ended up positioning the GapBuffer gap in
1244    // an invalid position.
1245    #[test]
1246    fn multibyte_unicode_combining_characters_are_handled_correctly() {
1247        let prog = Program::try_parse(", x/./ c/X/").unwrap();
1248        let mut runner = SystemRunner::new(env::current_dir().unwrap());
1249        let mut gb = GapBuffer::from("é");
1250
1251        prog.execute(&mut gb, &mut runner, "test", &mut vec![])
1252            .unwrap();
1253
1254        assert_eq!(&gb.to_string(), "X");
1255    }
1256
1257    #[test]
1258    fn edge_case_buffer_grows_significantly() {
1259        let prog = Program::try_parse(", x/x/ c/REPLACEMENT/").unwrap();
1260        let mut runner = SystemRunner::new(env::current_dir().unwrap());
1261        let mut b = Buffer::new_unnamed(0, "x x x", Default::default());
1262
1263        let dot = prog
1264            .execute(&mut b, &mut runner, "test", &mut vec![])
1265            .unwrap();
1266
1267        assert_eq!(&b.str_contents(), "REPLACEMENT REPLACEMENT REPLACEMENT");
1268        assert!(dot.as_char_indices().1 > 5);
1269    }
1270
1271    #[test]
1272    fn edge_case_buffer_shrinks_significantly() {
1273        let prog = Program::try_parse(", x/LONGWORD/ c/x/").unwrap();
1274        let mut runner = SystemRunner::new(env::current_dir().unwrap());
1275        let mut b = Buffer::new_unnamed(0, "LONGWORD LONGWORD LONGWORD", Default::default());
1276
1277        let dot = prog
1278            .execute(&mut b, &mut runner, "test", &mut vec![])
1279            .unwrap();
1280
1281        assert_eq!(&b.str_contents(), "x x x");
1282        assert_eq!(dot.as_char_indices(), (4, 4));
1283    }
1284
1285    #[test_case(", x/foo/ x/o/ c/X/", "foo bar", "fXX bar"; "nested x")]
1286    #[test_case(", x/\\w+/ y/o/ c/X/", "foo boo", "Xoo Xoo"; "x containing y")]
1287    #[test_case(", y/foo/ x/o/ c/X/", "foo bar foo", "foo bar foo"; "y containing x")]
1288    #[test]
1289    fn edge_case_complex_structex_works(
1290        program: &str,
1291        initial_content: &str,
1292        expected_content: &str,
1293    ) {
1294        let prog = Program::try_parse(program).unwrap();
1295        let mut runner = SystemRunner::new(env::current_dir().unwrap());
1296        let mut b = Buffer::new_unnamed(0, initial_content, Default::default());
1297
1298        prog.execute(&mut b, &mut runner, "test", &mut vec![])
1299            .unwrap();
1300
1301        assert_eq!(&b.str_contents(), expected_content, "buffer content");
1302    }
1303}