1use regex::Regex;
32
33use crate::Editor;
34
35pub type SubstError = String;
37
38#[derive(Debug, Clone, PartialEq, Eq)]
42pub struct SubstituteCmd {
43 pub pattern: Option<String>,
46 pub replacement: String,
50 pub flags: SubstFlags,
52 pub count: Option<usize>,
55}
56
57#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
59pub struct SubstFlags {
60 pub all: bool,
62 pub ignore_case: bool,
64 pub case_sensitive: bool,
66 pub confirm: bool,
70 pub report_only: bool,
74 pub no_error: bool,
77 pub print: bool,
81 pub print_num: bool,
83 pub print_list: bool,
85 pub reuse_previous: bool,
89}
90
91#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
93pub struct SubstituteOutcome {
94 pub replacements: usize,
96 pub lines_changed: usize,
98 pub last_row: Option<usize>,
102}
103
104pub fn parse_substitute(s: &str) -> Result<SubstituteCmd, SubstError> {
131 let rest = s
133 .strip_prefix('/')
134 .ok_or_else(|| format!("substitute: expected '/' delimiter, got {s:?}"))?;
135
136 let parts = split_on_slash(rest);
139
140 if parts.len() < 2 {
141 return Err("substitute needs /pattern/replacement/".into());
142 }
143
144 let raw_pattern = &parts[0];
145 let raw_replacement = &parts[1];
146 let raw_flags = parts.get(2).map(String::as_str).unwrap_or("");
147
148 let pattern = if raw_pattern.is_empty() {
150 None
151 } else {
152 Some(raw_pattern.clone())
153 };
154
155 let replacement = raw_replacement.clone();
158
159 let mut flags = SubstFlags::default();
162 let mut count: Option<usize> = None;
163 let mut chars = raw_flags.chars().peekable();
164 while let Some(&ch) = chars.peek() {
165 match ch {
166 'g' => flags.all = true,
167 'i' => flags.ignore_case = true,
168 'I' => flags.case_sensitive = true,
169 'c' => flags.confirm = true,
170 'n' => flags.report_only = true,
171 'e' => flags.no_error = true,
172 'p' => flags.print = true,
173 '#' => {
174 flags.print = true;
175 flags.print_num = true;
176 }
177 'l' => {
178 flags.print = true;
179 flags.print_list = true;
180 }
181 '&' => flags.reuse_previous = true,
184 ' ' | '\t' => {}
185 c if c.is_ascii_digit() => break, other => return Err(format!("unknown flag '{other}' in substitute")),
187 }
188 chars.next();
189 }
190 let rest: String = chars.collect();
192 let rest = rest.trim();
193 if !rest.is_empty() {
194 match rest.parse::<usize>() {
195 Ok(n) if n > 0 => count = Some(n),
196 _ => return Err(format!("trailing characters in substitute: {rest:?}")),
197 }
198 }
199
200 Ok(SubstituteCmd {
201 pattern,
202 replacement,
203 flags,
204 count,
205 })
206}
207
208pub fn apply_substitute<H: crate::types::Host>(
237 ed: &mut Editor<hjkl_buffer::View, H>,
238 cmd: &SubstituteCmd,
239 line_range: std::ops::RangeInclusive<u32>,
240) -> Result<SubstituteOutcome, SubstError> {
241 let pattern_str: String = match &cmd.pattern {
243 Some(p) => p.clone(),
244 None => ed
245 .last_search()
246 .ok_or_else(|| "no previous regular expression".to_string())?,
247 };
248
249 let effective_pattern = if cmd.flags.case_sensitive {
254 use crate::search::{CaseMode, resolve_case_mode};
257 let (stripped, _) = resolve_case_mode(&pattern_str, CaseMode::Sensitive);
258 stripped
259 } else if cmd.flags.ignore_case {
260 use crate::search::{CaseMode, resolve_case_mode};
262 let (stripped, _) = resolve_case_mode(&pattern_str, CaseMode::Sensitive);
263 format!("(?i){stripped}")
264 } else {
265 use crate::search::{CaseMode, resolve_case_mode};
267 let base = CaseMode::from_options(ed.settings().ignore_case, ed.settings().smartcase);
268 let (stripped, mode) = resolve_case_mode(&pattern_str, base);
269 if mode == CaseMode::Insensitive {
270 format!("(?i){stripped}")
271 } else {
272 stripped
273 }
274 };
275
276 let regex = Regex::new(&effective_pattern).map_err(|e| format!("bad pattern: {e}"))?;
277
278 let prev_replacement = ed
282 .last_substitute()
283 .map(|c| c.replacement.clone())
284 .unwrap_or_default();
285
286 ed.push_undo();
287
288 let start = *line_range.start() as usize;
289 let end = *line_range.end() as usize;
290 let rope = crate::types::Query::rope(ed.buffer());
291 let total = rope.len_lines();
292
293 let clamp_end = end.min(total.saturating_sub(1));
294 let mut new_lines: Vec<String> = crate::rope_util::rope_to_lines_vec(&rope);
295 let mut replacements = 0usize;
296 let mut lines_changed = 0usize;
297 let mut last_changed_row = 0usize;
298
299 if start <= clamp_end {
300 for (row, line) in new_lines[start..=clamp_end].iter_mut().enumerate() {
301 let (replaced, n) = do_replace(
302 ®ex,
303 line,
304 &cmd.replacement,
305 &prev_replacement,
306 cmd.flags.all,
307 );
308 if n > 0 {
309 *line = replaced;
310 replacements += n;
311 lines_changed += 1;
312 last_changed_row = start + row;
313 }
314 }
315 }
316
317 if replacements == 0 {
318 ed.pop_last_undo();
319 return Ok(SubstituteOutcome {
320 replacements: 0,
321 lines_changed: 0,
322 last_row: None,
323 });
324 }
325
326 if cmd.flags.report_only {
329 ed.pop_last_undo();
330 ed.set_last_search(Some(pattern_str), true);
331 return Ok(SubstituteOutcome {
332 replacements,
333 lines_changed,
334 last_row: None,
335 });
336 }
337
338 let newlines_before: usize = new_lines[..last_changed_row]
346 .iter()
347 .map(|l| l.matches('\n').count())
348 .sum();
349 let newlines_within = new_lines[last_changed_row].matches('\n').count();
350 let last_changed_row = last_changed_row + newlines_before + newlines_within;
351
352 ed.buffer_mut().replace_all(&new_lines.join("\n"));
354
355 let final_total = crate::types::Query::rope(ed.buffer()).len_lines();
358 let cursor_row = last_changed_row.min(final_total.saturating_sub(1));
359 let first_non_blank = crate::buf_helpers::buf_line(ed.buffer(), cursor_row)
360 .unwrap_or_default()
361 .chars()
362 .take_while(|c| *c == ' ' || *c == '\t')
363 .count();
364 let line_len = crate::buf_helpers::buf_line(ed.buffer(), cursor_row)
365 .unwrap_or_default()
366 .chars()
367 .count();
368 let cursor_col = first_non_blank.min(line_len.saturating_sub(1));
369 ed.buffer_mut()
370 .set_cursor(hjkl_buffer::Position::new(cursor_row, cursor_col));
371
372 ed.mark_content_dirty();
373
374 ed.set_last_search(Some(pattern_str), true);
376
377 Ok(SubstituteOutcome {
378 replacements,
379 lines_changed,
380 last_row: Some(cursor_row),
381 })
382}
383
384#[derive(Debug, Clone, PartialEq, Eq)]
391pub struct SubstituteMatch {
392 pub row: u32,
394 pub byte_start: u32,
396 pub byte_end: u32,
398 pub replacement: String,
400}
401
402pub fn collect_substitute_matches<H: crate::types::Host>(
414 ed: &crate::Editor<hjkl_buffer::View, H>,
415 cmd: &SubstituteCmd,
416 line_range: std::ops::RangeInclusive<u32>,
417) -> Result<Vec<SubstituteMatch>, SubstError> {
418 let pattern_str: String = match &cmd.pattern {
420 Some(p) => p.clone(),
421 None => ed
422 .last_search()
423 .ok_or_else(|| "no previous regular expression".to_string())?,
424 };
425
426 let effective_pattern = if cmd.flags.case_sensitive {
427 use crate::search::{CaseMode, resolve_case_mode};
428 let (stripped, _) = resolve_case_mode(&pattern_str, CaseMode::Sensitive);
429 stripped
430 } else if cmd.flags.ignore_case {
431 use crate::search::{CaseMode, resolve_case_mode};
432 let (stripped, _) = resolve_case_mode(&pattern_str, CaseMode::Sensitive);
433 format!("(?i){stripped}")
434 } else {
435 use crate::search::{CaseMode, resolve_case_mode};
436 let base = CaseMode::from_options(ed.settings().ignore_case, ed.settings().smartcase);
437 let (stripped, mode) = resolve_case_mode(&pattern_str, base);
438 if mode == CaseMode::Insensitive {
439 format!("(?i){stripped}")
440 } else {
441 stripped
442 }
443 };
444
445 let regex = Regex::new(&effective_pattern).map_err(|e| format!("bad pattern: {e}"))?;
446
447 let prev_replacement = ed
448 .last_substitute()
449 .map(|c| c.replacement.clone())
450 .unwrap_or_default();
451
452 let start = *line_range.start() as usize;
453 let end = *line_range.end() as usize;
454 let rope = crate::types::Query::rope(ed.buffer());
455 let total = rope.len_lines();
456 let clamp_end = end.min(total.saturating_sub(1));
457
458 let mut matches: Vec<SubstituteMatch> = Vec::new();
459
460 let expand = |line: &str, start: usize| {
464 regex
465 .captures_at(line, start)
466 .map(|caps| expand_replacement(&cmd.replacement, &caps, &prev_replacement))
467 .unwrap_or_default()
468 };
469
470 if start <= clamp_end {
471 for row in start..=clamp_end {
472 let line = hjkl_buffer::rope_line_str(&rope, row);
473 let line = line.trim_end_matches('\n');
475
476 if cmd.flags.all {
477 for m in regex.find_iter(line) {
478 matches.push(SubstituteMatch {
479 row: row as u32,
480 byte_start: m.start() as u32,
481 byte_end: m.end() as u32,
482 replacement: expand(line, m.start()),
483 });
484 }
485 } else if let Some(m) = regex.find(line) {
486 matches.push(SubstituteMatch {
488 row: row as u32,
489 byte_start: m.start() as u32,
490 byte_end: m.end() as u32,
491 replacement: expand(line, m.start()),
492 });
493 }
494 }
495 }
496
497 Ok(matches)
498}
499
500pub fn apply_collected_matches<H: crate::types::Host>(
513 ed: &mut crate::Editor<hjkl_buffer::View, H>,
514 matches: &[SubstituteMatch],
515 accepted: &[bool],
516) -> usize {
517 assert_eq!(
518 matches.len(),
519 accepted.len(),
520 "apply_collected_matches: accepted.len() must equal matches.len()"
521 );
522
523 let mut to_apply: Vec<&SubstituteMatch> = matches
526 .iter()
527 .zip(accepted.iter())
528 .filter_map(|(m, &ok)| if ok { Some(m) } else { None })
529 .collect();
530
531 if to_apply.is_empty() {
532 return 0;
533 }
534
535 to_apply.sort_unstable_by(|a, b| b.row.cmp(&a.row).then(b.byte_start.cmp(&a.byte_start)));
536
537 let rope = crate::types::Query::rope(ed.buffer());
538 let mut lines_vec: Vec<String> = crate::rope_util::rope_to_lines_vec(&rope);
539 let mut applied = 0usize;
540 let mut last_changed_row: Option<usize> = None;
541
542 for sm in &to_apply {
543 let row = sm.row as usize;
544 if row >= lines_vec.len() {
545 continue;
546 }
547 let line = &lines_vec[row];
548 let bs = sm.byte_start as usize;
549 let be = sm.byte_end as usize;
550 if be > line.len() || bs > be {
551 continue;
552 }
553 if !line.is_char_boundary(bs) || !line.is_char_boundary(be) {
556 continue;
557 }
558 let mut new_line = String::with_capacity(line.len() + sm.replacement.len());
560 new_line.push_str(&line[..bs]);
561 new_line.push_str(&sm.replacement);
562 new_line.push_str(&line[be..]);
563 lines_vec[row] = new_line;
564 applied += 1;
565 last_changed_row = Some(last_changed_row.map_or(row, |lr: usize| lr.max(row)));
570 }
571
572 if applied > 0 {
573 ed.buffer_mut().replace_all(&lines_vec.join("\n"));
574 if let Some(row) = last_changed_row {
575 let newlines_before: usize = lines_vec[..row]
580 .iter()
581 .map(|l| l.matches('\n').count())
582 .sum();
583 let newlines_within = lines_vec[row].matches('\n').count();
584 let row = row + newlines_before + newlines_within;
585 ed.buffer_mut()
586 .set_cursor(hjkl_buffer::Position::new(row, 0));
587 }
588 ed.mark_content_dirty();
589 }
590
591 applied
592}
593
594fn split_on_slash(s: &str) -> Vec<String> {
601 let mut out: Vec<String> = Vec::new();
602 let mut cur = String::new();
603 let mut chars = s.chars().peekable();
604 while let Some(c) = chars.next() {
605 if c == '\\' {
606 match chars.peek() {
607 Some(&'/') => {
608 cur.push('/');
610 chars.next();
611 }
612 Some(_) => {
613 let next = chars.next().unwrap();
616 cur.push('\\');
617 cur.push(next);
618 }
619 None => cur.push('\\'),
620 }
621 } else if c == '/' {
622 if out.len() < 2 {
623 out.push(std::mem::take(&mut cur));
624 } else {
625 cur.push(c);
629 }
633 } else {
634 cur.push(c);
635 }
636 }
637 out.push(cur);
638 out
639}
640
641#[derive(Clone, Copy, PartialEq)]
643enum CaseState {
644 None,
645 OneUpper,
647 OneLower,
649 AllUpper,
651 AllLower,
653}
654
655fn push_cased(out: &mut String, case: &mut CaseState, ch: char) {
658 match *case {
659 CaseState::None => out.push(ch),
660 CaseState::OneUpper => {
661 out.extend(ch.to_uppercase());
662 *case = CaseState::None;
663 }
664 CaseState::OneLower => {
665 out.extend(ch.to_lowercase());
666 *case = CaseState::None;
667 }
668 CaseState::AllUpper => out.extend(ch.to_uppercase()),
669 CaseState::AllLower => out.extend(ch.to_lowercase()),
670 }
671}
672
673fn expand_replacement(raw: &str, caps: ®ex::Captures, prev: &str) -> String {
687 let mut out = String::with_capacity(raw.len() + 8);
688 expand_into(&mut out, raw, caps, prev, true);
689 out
690}
691
692fn expand_into(out: &mut String, raw: &str, caps: ®ex::Captures, prev: &str, allow_tilde: bool) {
693 let mut case = CaseState::None;
694 let mut chars = raw.chars();
695 while let Some(c) = chars.next() {
696 match c {
697 '&' => {
698 let g = caps.get(0).map(|m| m.as_str()).unwrap_or("");
699 for ch in g.chars() {
700 push_cased(out, &mut case, ch);
701 }
702 }
703 '~' if allow_tilde => {
704 let mut tmp = String::new();
707 expand_into(&mut tmp, prev, caps, "", false);
708 for ch in tmp.chars() {
709 push_cased(out, &mut case, ch);
710 }
711 }
712 '\\' => match chars.next() {
713 Some('&') => push_cased(out, &mut case, '&'),
714 Some('~') => push_cased(out, &mut case, '~'),
715 Some('\\') => push_cased(out, &mut case, '\\'),
716 Some('r') => out.push('\n'),
718 Some('t') => out.push('\t'),
719 Some('n') => out.push('\0'),
720 Some(d @ '0'..='9') => {
721 let idx = d as usize - '0' as usize;
722 let g = caps.get(idx).map(|m| m.as_str()).unwrap_or("");
723 for ch in g.chars() {
724 push_cased(out, &mut case, ch);
725 }
726 }
727 Some('u') => case = CaseState::OneUpper,
728 Some('l') => case = CaseState::OneLower,
729 Some('U') => case = CaseState::AllUpper,
730 Some('L') => case = CaseState::AllLower,
731 Some('e') | Some('E') => case = CaseState::None,
732 Some(other) => push_cased(out, &mut case, other),
733 None => {} },
735 _ => push_cased(out, &mut case, c),
736 }
737 }
738}
739
740fn do_replace(
744 regex: &Regex,
745 text: &str,
746 replacement: &str,
747 prev: &str,
748 all: bool,
749) -> (String, usize) {
750 let matches = regex.find_iter(text).count();
751 if matches == 0 {
752 return (text.to_string(), 0);
753 }
754 let rep = |caps: ®ex::Captures| expand_replacement(replacement, caps, prev);
755 let replaced = if all {
756 regex.replace_all(text, rep).into_owned()
757 } else {
758 regex.replace(text, rep).into_owned()
759 };
760 let count = if all { matches } else { 1 };
761 (replaced, count)
762}
763
764#[cfg(test)]
765mod tests {
766 use super::*;
767 use crate::types::{DefaultHost, Options};
768 use hjkl_buffer::View;
769
770 fn editor_with(content: &str) -> Editor<View, DefaultHost> {
771 let mut e = Editor::new(View::new(), DefaultHost::new(), Options::default());
772 e.set_content(content);
773 e
774 }
775
776 fn buf_line(e: &Editor<View, DefaultHost>, row: usize) -> String {
777 hjkl_buffer::rope_line_str(&e.buffer().rope(), row)
778 }
779
780 #[test]
783 fn parse_basic() {
784 let cmd = parse_substitute("/foo/bar/").unwrap();
785 assert_eq!(cmd.pattern.as_deref(), Some("foo"));
786 assert_eq!(cmd.replacement, "bar");
787 assert!(!cmd.flags.all);
788 }
789
790 #[test]
791 fn parse_trailing_slash_optional() {
792 let cmd = parse_substitute("/foo/bar").unwrap();
793 assert_eq!(cmd.pattern.as_deref(), Some("foo"));
794 assert_eq!(cmd.replacement, "bar");
795 }
796
797 #[test]
798 fn parse_global_flag() {
799 let cmd = parse_substitute("/x/y/g").unwrap();
800 assert!(cmd.flags.all);
801 }
802
803 #[test]
804 fn parse_ignore_case_flag() {
805 let cmd = parse_substitute("/x/y/i").unwrap();
806 assert!(cmd.flags.ignore_case);
807 }
808
809 #[test]
810 fn parse_case_sensitive_flag() {
811 let cmd = parse_substitute("/x/y/I").unwrap();
812 assert!(cmd.flags.case_sensitive);
813 }
814
815 #[test]
816 fn parse_confirm_flag_accepted() {
817 let cmd = parse_substitute("/x/y/c").unwrap();
818 assert!(cmd.flags.confirm);
819 }
820
821 #[test]
822 fn parse_multi_flags() {
823 let cmd = parse_substitute("/x/y/gi").unwrap();
824 assert!(cmd.flags.all);
825 assert!(cmd.flags.ignore_case);
826 }
827
828 #[test]
829 fn parse_unknown_flag_errors() {
830 let err = parse_substitute("/x/y/z").unwrap_err();
831 assert!(err.to_string().contains("unknown flag 'z'"), "{err}");
832 }
833
834 #[test]
835 fn parse_empty_pattern_is_none() {
836 let cmd = parse_substitute("//bar/").unwrap();
837 assert!(cmd.pattern.is_none());
838 assert_eq!(cmd.replacement, "bar");
839 }
840
841 #[test]
842 fn parse_empty_replacement_ok() {
843 let cmd = parse_substitute("/foo//").unwrap();
844 assert_eq!(cmd.pattern.as_deref(), Some("foo"));
845 assert_eq!(cmd.replacement, "");
846 }
847
848 #[test]
849 fn parse_escaped_slash_in_pattern() {
850 let cmd = parse_substitute("/a\\/b/c/").unwrap();
851 assert_eq!(cmd.pattern.as_deref(), Some("a/b"));
852 }
853
854 #[test]
855 fn parse_escaped_slash_in_replacement() {
856 let cmd = parse_substitute("/a/b\\/c/").unwrap();
857 assert_eq!(cmd.replacement, "b/c");
859 }
860
861 #[test]
864 fn parse_keeps_replacement_raw() {
865 assert_eq!(parse_substitute("/foo/[&]/").unwrap().replacement, "[&]");
866 assert_eq!(parse_substitute("/foo/\\&/").unwrap().replacement, "\\&");
867 assert_eq!(parse_substitute("/(foo)/\\1/").unwrap().replacement, "\\1");
868 assert_eq!(parse_substitute("/(x)/\\9/").unwrap().replacement, "\\9");
869 }
870
871 #[test]
872 fn parse_wrong_delimiter_errors() {
873 let err = parse_substitute("|foo|bar|").unwrap_err();
874 assert!(err.to_string().contains("'/'"), "{err}");
875 }
876
877 #[test]
878 fn parse_too_few_fields_errors() {
879 let err = parse_substitute("/foo").unwrap_err();
880 assert!(
881 err.to_string().contains("needs /pattern/replacement"),
882 "{err}"
883 );
884 }
885
886 #[test]
889 fn apply_single_line_first_only() {
890 let mut e = editor_with("foo foo");
891 let cmd = parse_substitute("/foo/bar/").unwrap();
892 let out = apply_substitute(&mut e, &cmd, 0..=0).unwrap();
893 assert_eq!(out.replacements, 1);
894 assert_eq!(out.lines_changed, 1);
895 assert_eq!(buf_line(&e, 0), "bar foo");
896 }
897
898 #[test]
899 fn apply_single_line_global() {
900 let mut e = editor_with("foo foo foo");
901 let cmd = parse_substitute("/foo/bar/g").unwrap();
902 let out = apply_substitute(&mut e, &cmd, 0..=0).unwrap();
903 assert_eq!(out.replacements, 3);
904 assert_eq!(out.lines_changed, 1);
905 assert_eq!(buf_line(&e, 0), "bar bar bar");
906 }
907
908 #[test]
909 fn apply_multi_line_range() {
910 let mut e = editor_with("foo\nfoo foo\nbar");
911 let cmd = parse_substitute("/foo/xyz/g").unwrap();
912 let out = apply_substitute(&mut e, &cmd, 0..=2).unwrap();
913 assert_eq!(out.replacements, 3);
914 assert_eq!(out.lines_changed, 2);
915 assert_eq!(buf_line(&e, 0), "xyz");
916 assert_eq!(buf_line(&e, 1), "xyz xyz");
917 assert_eq!(buf_line(&e, 2), "bar");
918 }
919
920 #[test]
921 fn apply_no_match_returns_zero() {
922 let mut e = editor_with("hello");
923 let original = buf_line(&e, 0);
924 let cmd = parse_substitute("/xyz/abc/").unwrap();
925 let out = apply_substitute(&mut e, &cmd, 0..=0).unwrap();
926 assert_eq!(out.replacements, 0);
927 assert_eq!(out.lines_changed, 0);
928 assert_eq!(buf_line(&e, 0), original);
929 }
930
931 #[test]
932 fn apply_case_insensitive_flag() {
933 let mut e = editor_with("Foo FOO foo");
934 let cmd = parse_substitute("/foo/bar/gi").unwrap();
935 let out = apply_substitute(&mut e, &cmd, 0..=0).unwrap();
936 assert_eq!(out.replacements, 3);
937 assert_eq!(buf_line(&e, 0), "bar bar bar");
938 }
939
940 #[test]
941 fn apply_case_sensitive_flag_overrides_editor_setting() {
942 let mut e = editor_with("Foo foo");
943 e.settings_mut().ignore_case = true;
945 let cmd = parse_substitute("/foo/bar/I").unwrap();
947 let out = apply_substitute(&mut e, &cmd, 0..=0).unwrap();
948 assert_eq!(out.replacements, 1);
950 assert_eq!(buf_line(&e, 0), "Foo bar");
951 }
952
953 #[test]
954 fn apply_empty_pattern_reuses_last_search() {
955 let mut e = editor_with("hello world");
956 e.set_last_search(Some("world".to_string()), true);
957 let cmd = parse_substitute("//planet/").unwrap();
958 let out = apply_substitute(&mut e, &cmd, 0..=0).unwrap();
959 assert_eq!(out.replacements, 1);
960 assert_eq!(buf_line(&e, 0), "hello planet");
961 }
962
963 #[test]
964 fn apply_empty_pattern_no_last_search_errors() {
965 let mut e = editor_with("hello");
966 let cmd = parse_substitute("//bar/").unwrap();
967 let err = apply_substitute(&mut e, &cmd, 0..=0).unwrap_err();
968 assert!(
969 err.to_string().contains("no previous regular expression"),
970 "{err}"
971 );
972 }
973
974 #[test]
975 fn apply_updates_last_search() {
976 let mut e = editor_with("foo");
977 let cmd = parse_substitute("/foo/bar/").unwrap();
978 apply_substitute(&mut e, &cmd, 0..=0).unwrap();
979 assert_eq!(e.last_search(), Some("foo".to_string()));
980 }
981
982 #[test]
983 fn apply_empty_replacement_deletes_match() {
984 let mut e = editor_with("hello world");
985 let cmd = parse_substitute("/world//").unwrap();
986 let out = apply_substitute(&mut e, &cmd, 0..=0).unwrap();
987 assert_eq!(out.replacements, 1);
988 assert_eq!(buf_line(&e, 0), "hello ");
989 }
990
991 #[test]
992 fn apply_undo_reverts_in_one_step() {
993 let mut e = editor_with("foo");
994 let cmd = parse_substitute("/foo/bar/").unwrap();
995 apply_substitute(&mut e, &cmd, 0..=0).unwrap();
996 assert_eq!(buf_line(&e, 0), "bar");
997 e.undo();
998 assert_eq!(buf_line(&e, 0), "foo");
999 }
1000
1001 #[test]
1002 fn apply_ampersand_in_replacement() {
1003 let mut e = editor_with("foo");
1004 let cmd = parse_substitute("/foo/[&]/").unwrap();
1005 apply_substitute(&mut e, &cmd, 0..=0).unwrap();
1006 assert_eq!(buf_line(&e, 0), "[foo]");
1007 }
1008
1009 #[test]
1010 fn apply_capture_group_reference() {
1011 let mut e = editor_with("hello world");
1012 let cmd = parse_substitute("/(\\w+)/<<\\1>>/g").unwrap();
1013 apply_substitute(&mut e, &cmd, 0..=0).unwrap();
1014 assert_eq!(buf_line(&e, 0), "<<hello>> <<world>>");
1015 }
1016
1017 #[test]
1018 fn apply_backslash_r_splits_line() {
1019 let mut e = editor_with("a,b,c");
1022 let cmd = parse_substitute("/,/\\r/g").unwrap();
1023 apply_substitute(&mut e, &cmd, 0..=0).unwrap();
1024 assert_eq!(buf_line(&e, 0), "a");
1025 assert_eq!(buf_line(&e, 1), "b");
1026 assert_eq!(buf_line(&e, 2), "c");
1027 }
1028
1029 #[test]
1035 fn apply_backslash_r_multi_row_cursor_lands_on_final_split_row() {
1036 let mut e = editor_with("a,b\nc,d\n");
1037 let cmd = parse_substitute("/,/\\r/").unwrap();
1038 let total = crate::types::Query::rope(e.buffer()).len_lines();
1039 let out = apply_substitute(&mut e, &cmd, 0..=(total.saturating_sub(1)) as u32).unwrap();
1040 assert_eq!(buf_line(&e, 0), "a");
1041 assert_eq!(buf_line(&e, 1), "b");
1042 assert_eq!(buf_line(&e, 2), "c");
1043 assert_eq!(buf_line(&e, 3), "d");
1044 assert_eq!(
1045 out.last_row,
1046 Some(3),
1047 "cursor should land on the last changed line ('d', real row 3) \
1048 in post-split coordinates, not the pre-split row index"
1049 );
1050 assert_eq!(e.buffer().cursor().row, 3);
1051 }
1052
1053 #[test]
1057 fn apply_backslash_r_single_row_cursor_lands_on_last_split_line() {
1058 let mut e = editor_with("a,b");
1059 let cmd = parse_substitute("/,/\\r/").unwrap();
1060 let out = apply_substitute(&mut e, &cmd, 0..=0).unwrap();
1061 assert_eq!(buf_line(&e, 0), "a");
1062 assert_eq!(buf_line(&e, 1), "b");
1063 assert_eq!(out.last_row, Some(1));
1064 assert_eq!(e.buffer().cursor().row, 1);
1065 }
1066
1067 #[test]
1070 fn apply_no_newline_multi_row_cursor_unaffected() {
1071 let mut e = editor_with("a\na\na");
1072 let cmd = parse_substitute("/a/X/").unwrap();
1073 let out = apply_substitute(&mut e, &cmd, 0..=2).unwrap();
1074 assert_eq!(buf_line(&e, 0), "X");
1075 assert_eq!(buf_line(&e, 1), "X");
1076 assert_eq!(buf_line(&e, 2), "X");
1077 assert_eq!(out.last_row, Some(2));
1078 assert_eq!(e.buffer().cursor().row, 2);
1079 }
1080
1081 #[test]
1082 fn apply_backslash_t_inserts_tab() {
1083 let mut e = editor_with("a,b");
1084 let cmd = parse_substitute("/,/\\t/").unwrap();
1085 apply_substitute(&mut e, &cmd, 0..=0).unwrap();
1086 assert_eq!(buf_line(&e, 0), "a\tb");
1087 }
1088
1089 #[test]
1090 fn apply_literal_dollar_in_replacement() {
1091 let mut e = editor_with("x");
1094 let cmd = parse_substitute("/x/$5/").unwrap();
1095 apply_substitute(&mut e, &cmd, 0..=0).unwrap();
1096 assert_eq!(buf_line(&e, 0), "$5");
1097 }
1098
1099 #[test]
1100 fn apply_backslash_zero_is_whole_match() {
1101 let mut e = editor_with("foo");
1103 let cmd = parse_substitute("/foo/[\\0]/").unwrap();
1104 apply_substitute(&mut e, &cmd, 0..=0).unwrap();
1105 assert_eq!(buf_line(&e, 0), "[foo]");
1106 }
1107
1108 #[test]
1109 fn apply_group_ref_then_literal_digits() {
1110 let mut e = editor_with("ab");
1112 let cmd = parse_substitute("/(.)/\\11/g").unwrap();
1113 apply_substitute(&mut e, &cmd, 0..=0).unwrap();
1114 assert_eq!(buf_line(&e, 0), "a1b1");
1115 }
1116
1117 fn expand(raw: &str, pat: &str, text: &str, prev: &str) -> String {
1120 let re = Regex::new(pat).unwrap();
1121 let caps = re.captures(text).unwrap();
1122 expand_replacement(raw, &caps, prev)
1123 }
1124
1125 #[test]
1126 fn expand_case_upper_run_and_end() {
1127 assert_eq!(expand("\\U\\0\\Ex", "foo", "foo", ""), "FOOx");
1129 assert_eq!(expand("\\L&\\E", "FOO", "FOO", ""), "foo");
1130 }
1131
1132 #[test]
1133 fn expand_case_one_shot() {
1134 assert_eq!(expand("\\u\\0", "foo", "foo", ""), "Foo");
1136 assert_eq!(expand("\\l\\0", "FOO", "FOO", ""), "fOO");
1137 }
1138
1139 #[test]
1140 fn expand_case_applies_to_group() {
1141 assert_eq!(expand("\\U\\1-y\\E", "(f)oo", "foo", ""), "F-Y");
1143 }
1144
1145 #[test]
1146 fn expand_literal_dollar_and_amp() {
1147 assert_eq!(expand("$\\0", "x", "x", ""), "$x");
1148 assert_eq!(expand("[&]", "foo", "foo", ""), "[foo]");
1149 assert_eq!(expand("\\&", "foo", "foo", ""), "&");
1150 }
1151
1152 #[test]
1153 fn expand_tilde_uses_previous_replacement() {
1154 assert_eq!(expand("~!", "x", "x", "PREV"), "PREV!");
1156 assert_eq!(expand("~", "(.)", "a", "[\\1]"), "[a]");
1157 assert_eq!(expand("\\~", "x", "x", "PREV"), "~");
1159 }
1160
1161 #[test]
1164 fn apply_report_only_counts_without_mutating() {
1165 let mut e = editor_with("foo foo foo");
1166 let cmd = parse_substitute("/foo/bar/gn").unwrap();
1167 assert!(cmd.flags.report_only);
1168 let out = apply_substitute(&mut e, &cmd, 0..=0).unwrap();
1169 assert_eq!(out.replacements, 3);
1170 assert_eq!(buf_line(&e, 0), "foo foo foo");
1172 }
1173
1174 #[test]
1177 fn apply_upper_run() {
1178 let mut e = editor_with("hello world");
1179 let cmd = parse_substitute("/world/\\U&\\E/").unwrap();
1180 apply_substitute(&mut e, &cmd, 0..=0).unwrap();
1181 assert_eq!(buf_line(&e, 0), "hello WORLD");
1182 }
1183
1184 #[test]
1189 fn substitute_respects_smartcase() {
1190 let mut e = editor_with("Foo");
1191 let cmd = parse_substitute("/foo/bar/").unwrap();
1193 let out = apply_substitute(&mut e, &cmd, 0..=0).unwrap();
1194 assert_eq!(out.replacements, 1);
1195 assert_eq!(buf_line(&e, 0), "bar");
1196 }
1197
1198 #[test]
1201 fn substitute_i_flag_overrides_c() {
1202 let mut e = editor_with("foo");
1203 let cmd = parse_substitute("/Foo/bar/i").unwrap();
1205 let out = apply_substitute(&mut e, &cmd, 0..=0).unwrap();
1206 assert_eq!(out.replacements, 1, "expected match on 'foo' with /i flag");
1207 assert_eq!(buf_line(&e, 0), "bar");
1208 }
1209
1210 #[test]
1213 fn substitute_lower_c_inline_overrides_smartcase() {
1214 let mut e = editor_with("FOO");
1215 let cmd = parse_substitute("/\\cFoo/bar/").unwrap();
1217 let out = apply_substitute(&mut e, &cmd, 0..=0).unwrap();
1218 assert_eq!(out.replacements, 1);
1219 assert_eq!(buf_line(&e, 0), "bar");
1220 }
1221
1222 #[test]
1225 fn collect_substitute_matches_finds_all_occurrences() {
1226 let e = editor_with("foo bar foo");
1227 let cmd = parse_substitute("/foo/baz/g").unwrap();
1228 let matches = collect_substitute_matches(&e, &cmd, 0..=0).unwrap();
1229 assert_eq!(matches.len(), 2, "expected 2 matches for /g flag");
1230 assert_eq!(matches[0].byte_start, 0);
1231 assert_eq!(matches[0].byte_end, 3);
1232 assert_eq!(matches[1].byte_start, 8);
1233 assert_eq!(matches[1].byte_end, 11);
1234 assert_eq!(matches[0].replacement, "baz");
1235 assert_eq!(matches[1].replacement, "baz");
1236 }
1237
1238 #[test]
1239 fn collect_substitute_matches_respects_g_flag() {
1240 let e = editor_with("foo foo foo");
1242 let cmd = parse_substitute("/foo/baz/").unwrap();
1243 let matches = collect_substitute_matches(&e, &cmd, 0..=0).unwrap();
1244 assert_eq!(matches.len(), 1, "expected 1 match without /g");
1245 assert_eq!(matches[0].byte_start, 0);
1246 }
1247
1248 #[test]
1249 fn collect_substitute_matches_respects_range() {
1250 let e = editor_with("foo\nfoo\nfoo\nfoo\nfoo");
1251 let cmd = parse_substitute("/foo/bar/g").unwrap();
1252 let matches = collect_substitute_matches(&e, &cmd, 1..=2).unwrap();
1254 assert_eq!(matches.len(), 2);
1255 assert_eq!(matches[0].row, 1);
1256 assert_eq!(matches[1].row, 2);
1257 }
1258
1259 #[test]
1260 fn collect_substitute_matches_expands_template() {
1261 let e = editor_with("hello world");
1262 let cmd = parse_substitute("/(\\w+)/<<\\1>>/g").unwrap();
1264 let matches = collect_substitute_matches(&e, &cmd, 0..=0).unwrap();
1265 assert_eq!(matches.len(), 2);
1266 assert_eq!(matches[0].replacement, "<<hello>>");
1267 assert_eq!(matches[1].replacement, "<<world>>");
1268 }
1269
1270 #[test]
1273 fn apply_collected_matches_reverse_order_preserves_offsets() {
1274 let mut e = editor_with("foo bar baz");
1278 let cmd = parse_substitute("/(foo|bar|baz)/X/g").unwrap();
1279 let matches = collect_substitute_matches(&e, &cmd, 0..=0).unwrap();
1280 assert_eq!(matches.len(), 3);
1281 let accepted = vec![true; 3];
1282 let applied = apply_collected_matches(&mut e, &matches, &accepted);
1283 assert_eq!(applied, 3);
1284 assert_eq!(buf_line(&e, 0), "X X X");
1285 }
1286
1287 #[test]
1288 fn apply_collected_matches_subset_only() {
1289 let mut e = editor_with("foo bar foo");
1291 let cmd = parse_substitute("/foo/ZZZ/g").unwrap();
1292 let matches = collect_substitute_matches(&e, &cmd, 0..=0).unwrap();
1293 assert_eq!(matches.len(), 2, "expected 2 foo matches");
1294 let accepted = vec![true, false];
1296 let applied = apply_collected_matches(&mut e, &matches, &accepted);
1297 assert_eq!(applied, 1);
1298 assert_eq!(buf_line(&e, 0), "ZZZ bar foo");
1300 }
1301
1302 #[test]
1303 fn apply_collected_matches_zero_accepted() {
1304 let mut e = editor_with("foo bar foo");
1305 let cmd = parse_substitute("/foo/ZZZ/g").unwrap();
1306 let matches = collect_substitute_matches(&e, &cmd, 0..=0).unwrap();
1307 let accepted = vec![false; matches.len()];
1308 let applied = apply_collected_matches(&mut e, &matches, &accepted);
1309 assert_eq!(applied, 0);
1310 assert_eq!(buf_line(&e, 0), "foo bar foo");
1311 }
1312
1313 #[test]
1314 fn apply_collected_matches_expands_template() {
1315 let mut e = editor_with("hello world");
1316 let cmd = parse_substitute("/(\\w+)/<<\\1>>/g").unwrap();
1317 let matches = collect_substitute_matches(&e, &cmd, 0..=0).unwrap();
1318 let accepted = vec![true; matches.len()];
1319 let applied = apply_collected_matches(&mut e, &matches, &accepted);
1320 assert_eq!(applied, 2);
1321 assert_eq!(buf_line(&e, 0), "<<hello>> <<world>>");
1322 }
1323}