1use 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#[derive(Debug)]
37pub enum Error {
38 Format,
40 InvalidRegex(regex::Error),
42 InvalidStructex(structex::Error),
44 InvalidTemplate(template::Error),
46 InvalidSuffix,
48 Io(io::ErrorKind, String),
50 Render(template::RenderError),
52 UnclosedDelimiter(&'static str, char),
54 UnexpectedCharacter(char),
56 UnexpectedEof,
58 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
98pub trait Edit: Address + Haystack<Regex> {
100 fn insert(&mut self, ix: usize, s: &str);
102
103 fn remove(&mut self, from: usize, to: usize);
105
106 fn begin_edit_transaction(&mut self) {}
108
109 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#[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 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 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$<>|") .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 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 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 for action in edit_actions.into_iter().rev() {
260 delta += action.apply(ed);
261 }
262
263 ed.end_edit_transaction();
264
265 let ix_max = ed.len_chars();
271
272 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 'p' => {
316 self.templates[&id].render_with_context_to(out, &caps, &ctx)?;
317 }
318
319 '$' => {
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 '>' => {
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 'd' => edit_actions.push(EditAction::Remove(caps.from(), caps.to())),
340
341 '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 'i' => {
352 edit_actions.push(EditAction::Insert(
353 caps.from(),
354 self.templates[&id].render_with_context(&caps, &ctx)?,
355 ));
356 }
357
358 'a' => {
360 edit_actions.push(EditAction::Insert(
361 caps.to(),
362 self.templates[&id].render_with_context(&caps, &ctx)?,
363 ));
364 }
365
366 '<' => {
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 '|' => {
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 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 " │ 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")] #[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 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 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")] #[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 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 #[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 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")] #[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")] #[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")] #[test_case(3, "-,+ d", "line1\nline2", "ine2", (0, 0); "current line from middle")] #[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 #[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}