1use std::{
2 collections::HashMap,
3 fs::{self, File},
4 io::{BufReader, BufWriter, Write},
5 path::Path,
6};
7use tempfile::NamedTempFile;
8
9use crate::search::{SearchResult, SearchResultWithReplacement, SearchType};
10use crate::{line_reader::BufReadExt, search};
11
12#[derive(Clone, Debug, PartialEq, Eq)]
13pub enum ReplaceResult {
14 Success,
15 Error(String),
16}
17
18pub fn replace_in_file(results: &mut [SearchResultWithReplacement]) -> anyhow::Result<()> {
21 let file_path = match results {
22 [r, ..] => r.search_result.path.clone(),
23 [] => return Ok(()),
24 };
25 debug_assert!(results.iter().all(|r| r.search_result.path == file_path));
26
27 let mut line_map: HashMap<_, _> = results
28 .iter_mut()
29 .map(|res| (res.search_result.line_number, res))
30 .collect();
31
32 let parent_dir = file_path.parent().unwrap_or(Path::new("."));
33 let temp_output_file = NamedTempFile::new_in(parent_dir)?;
34
35 {
37 let input = File::open(file_path.clone())?;
38 let reader = BufReader::new(input);
39
40 let output = File::create(temp_output_file.path())?;
41 let mut writer = BufWriter::new(output);
42
43 for (mut line_number, line_result) in reader.lines_with_endings().enumerate() {
44 line_number += 1; let (mut line, line_ending) = line_result?;
46 if let Some(res) = line_map.get_mut(&line_number) {
47 if line == res.search_result.line.as_bytes() {
48 line = res.replacement.as_bytes().to_vec();
49 res.replace_result = Some(ReplaceResult::Success);
50 } else {
51 res.replace_result = Some(ReplaceResult::Error(
52 "File changed since last search".to_owned(),
53 ));
54 }
55 }
56 line.extend(line_ending.as_bytes());
57 writer.write_all(&line)?;
58 }
59
60 writer.flush()?;
61 }
62
63 temp_output_file.persist(file_path)?;
64 Ok(())
65}
66
67const MAX_FILE_SIZE: u64 = 100 * 1024 * 1024; fn should_replace_in_memory(path: &Path) -> Result<bool, std::io::Error> {
70 let file_size = fs::metadata(path)?.len();
71 Ok(file_size <= MAX_FILE_SIZE)
72}
73
74pub fn replace_all_in_file(
94 file_path: &Path,
95 search: &SearchType,
96 replace: &str,
97) -> anyhow::Result<bool> {
98 if matches!(should_replace_in_memory(file_path), Ok(true)) {
100 match replace_in_memory(file_path, search, replace) {
101 Ok(replaced) => return Ok(replaced),
102 Err(e) => {
103 log::error!(
104 "Found error when attempting to replace in memory for file {path_display}: {e}",
105 path_display = file_path.display(),
106 );
107 }
108 }
109 }
110
111 replace_chunked(file_path, search, replace)
112}
113
114pub fn add_replacement(
115 search_result: SearchResult,
116 search: &SearchType,
117 replace: &str,
118) -> Option<SearchResultWithReplacement> {
119 let replacement = replacement_if_match(&search_result.line, search, replace)?;
120 Some(SearchResultWithReplacement {
121 search_result,
122 replacement,
123 replace_result: None,
124 })
125}
126
127fn replace_chunked(file_path: &Path, search: &SearchType, replace: &str) -> anyhow::Result<bool> {
128 let search_results = search::search_file(file_path, search)?;
129 if !search_results.is_empty() {
130 let mut replacement_results = search_results
131 .into_iter()
132 .map(|r| {
133 add_replacement(r, search, replace).unwrap_or_else(|| {
134 panic!("Called add_replacement with non-matching search result")
135 })
136 })
137 .collect::<Vec<_>>();
138 replace_in_file(&mut replacement_results)?;
139 return Ok(true);
140 }
141
142 Ok(false)
143}
144
145fn replace_in_memory(file_path: &Path, search: &SearchType, replace: &str) -> anyhow::Result<bool> {
146 let content = fs::read_to_string(file_path)?;
147 if let Some(new_content) = replacement_if_match(&content, search, replace) {
148 let parent_dir = file_path.parent().unwrap_or(Path::new("."));
149 let mut temp_file = NamedTempFile::new_in(parent_dir)?;
150 temp_file.write_all(new_content.as_bytes())?;
151 temp_file.persist(file_path)?;
152 Ok(true)
153 } else {
154 Ok(false)
155 }
156}
157
158pub fn replacement_if_match(line: &str, search: &SearchType, replace: &str) -> Option<String> {
171 if line.is_empty() || search.is_empty() {
172 return None;
173 }
174
175 if search::contains_search(line, search) {
176 let replacement = match search {
177 SearchType::Fixed(fixed_str) => line.replace(fixed_str, replace),
178 SearchType::Pattern(pattern) => pattern.replace_all(line, replace).to_string(),
179 SearchType::PatternAdvanced(pattern) => pattern.replace_all(line, replace).to_string(),
180 };
181 Some(replacement)
182 } else {
183 None
184 }
185}
186
187#[derive(Clone, Debug, Eq, PartialEq)]
188pub struct ReplaceStats {
189 pub num_successes: usize,
190 pub errors: Vec<SearchResultWithReplacement>,
191}
192
193pub fn calculate_statistics<I>(results: I) -> ReplaceStats
194where
195 I: IntoIterator<Item = SearchResultWithReplacement>,
196{
197 let mut num_successes = 0;
198 let mut errors = vec![];
199
200 results.into_iter().for_each(|res| {
201 assert!(
202 res.search_result.included,
203 "Expected only included results, found {res:?}"
204 );
205 match &res.replace_result {
206 Some(ReplaceResult::Success) => {
207 num_successes += 1;
208 }
209 None => {
210 let mut res = res.clone();
211 res.replace_result = Some(ReplaceResult::Error(
212 "Failed to find search result in file".to_owned(),
213 ));
214 errors.push(res);
215 }
216 Some(ReplaceResult::Error(_)) => {
217 errors.push(res.clone());
218 }
219 }
220 });
221
222 ReplaceStats {
223 num_successes,
224 errors,
225 }
226}
227
228#[cfg(test)]
229mod tests {
230 use super::*;
231 use crate::line_reader::LineEnding;
232 use crate::search::{SearchResult, SearchType, search_file};
233 use regex::Regex;
234 use std::path::PathBuf;
235 use tempfile::TempDir;
236
237 mod test_helpers {
238 use crate::search::SearchType;
239
240 pub fn create_fixed_search(term: &str) -> SearchType {
241 SearchType::Fixed(term.to_string())
242 }
243 }
244
245 fn create_search_result_with_replacement(
247 path: &str,
248 line_number: usize,
249 line: &str,
250 replacement: &str,
251 included: bool,
252 replace_result: Option<ReplaceResult>,
253 ) -> SearchResultWithReplacement {
254 SearchResultWithReplacement {
255 search_result: SearchResult {
256 path: PathBuf::from(path),
257 line_number,
258 line: line.to_string(),
259 line_ending: LineEnding::Lf,
260 included,
261 },
262 replacement: replacement.to_string(),
263 replace_result,
264 }
265 }
266
267 fn create_test_file(temp_dir: &TempDir, name: &str, content: &str) -> PathBuf {
268 let file_path = temp_dir.path().join(name);
269 std::fs::write(&file_path, content).unwrap();
270 file_path
271 }
272
273 fn assert_file_content(file_path: &Path, expected_content: &str) {
274 let content = std::fs::read_to_string(file_path).unwrap();
275 assert_eq!(content, expected_content);
276 }
277
278 fn fixed_search(pattern: &str) -> SearchType {
279 SearchType::Fixed(pattern.to_string())
280 }
281
282 fn regex_search(pattern: &str) -> SearchType {
283 SearchType::Pattern(Regex::new(pattern).unwrap())
284 }
285
286 #[test]
288 fn test_replace_in_file_success() {
289 let temp_dir = TempDir::new().unwrap();
290 let file_path = create_test_file(
291 &temp_dir,
292 "test.txt",
293 "line 1\nold text\nline 3\nold text\nline 5\n",
294 );
295
296 let mut results = vec![
298 create_search_result_with_replacement(
299 file_path.to_str().unwrap(),
300 2,
301 "old text",
302 "new text",
303 true,
304 None,
305 ),
306 create_search_result_with_replacement(
307 file_path.to_str().unwrap(),
308 4,
309 "old text",
310 "new text",
311 true,
312 None,
313 ),
314 ];
315
316 let result = replace_in_file(&mut results);
318 assert!(result.is_ok());
319
320 assert_eq!(results.len(), 2);
322 assert_eq!(results[0].replace_result, Some(ReplaceResult::Success));
323 assert_eq!(results[1].replace_result, Some(ReplaceResult::Success));
324
325 assert_file_content(&file_path, "line 1\nnew text\nline 3\nnew text\nline 5\n");
327 }
328
329 #[test]
330 fn test_replace_in_file_success_no_final_newline() {
331 let temp_dir = TempDir::new().unwrap();
332 let file_path = create_test_file(
333 &temp_dir,
334 "test.txt",
335 "line 1\nold text\nline 3\nold text\nline 5",
336 );
337
338 let mut results = vec![
340 create_search_result_with_replacement(
341 file_path.to_str().unwrap(),
342 2,
343 "old text",
344 "new text",
345 true,
346 None,
347 ),
348 create_search_result_with_replacement(
349 file_path.to_str().unwrap(),
350 4,
351 "old text",
352 "new text",
353 true,
354 None,
355 ),
356 ];
357
358 let result = replace_in_file(&mut results);
360 assert!(result.is_ok());
361
362 assert_eq!(results.len(), 2);
364 assert_eq!(results[0].replace_result, Some(ReplaceResult::Success));
365 assert_eq!(results[1].replace_result, Some(ReplaceResult::Success));
366
367 let new_content = std::fs::read_to_string(&file_path).unwrap();
369 assert_eq!(new_content, "line 1\nnew text\nline 3\nnew text\nline 5");
370 }
371
372 #[test]
373 fn test_replace_in_file_success_windows_newlines() {
374 let temp_dir = TempDir::new().unwrap();
375 let file_path = create_test_file(
376 &temp_dir,
377 "test.txt",
378 "line 1\r\nold text\r\nline 3\r\nold text\r\nline 5\r\n",
379 );
380
381 let mut results = vec![
383 create_search_result_with_replacement(
384 file_path.to_str().unwrap(),
385 2,
386 "old text",
387 "new text",
388 true,
389 None,
390 ),
391 create_search_result_with_replacement(
392 file_path.to_str().unwrap(),
393 4,
394 "old text",
395 "new text",
396 true,
397 None,
398 ),
399 ];
400
401 let result = replace_in_file(&mut results);
403 assert!(result.is_ok());
404
405 assert_eq!(results.len(), 2);
407 assert_eq!(results[0].replace_result, Some(ReplaceResult::Success));
408 assert_eq!(results[1].replace_result, Some(ReplaceResult::Success));
409
410 let new_content = std::fs::read_to_string(&file_path).unwrap();
412 assert_eq!(
413 new_content,
414 "line 1\r\nnew text\r\nline 3\r\nnew text\r\nline 5\r\n"
415 );
416 }
417
418 #[test]
419 fn test_replace_in_file_success_mixed_newlines() {
420 let temp_dir = TempDir::new().unwrap();
421 let file_path = create_test_file(
422 &temp_dir,
423 "test.txt",
424 "\n\r\nline 1\nold text\r\nline 3\nline 4\r\nline 5\r\n\n\n",
425 );
426
427 let mut results = vec![
429 create_search_result_with_replacement(
430 file_path.to_str().unwrap(),
431 4,
432 "old text",
433 "new text",
434 true,
435 None,
436 ),
437 create_search_result_with_replacement(
438 file_path.to_str().unwrap(),
439 7,
440 "line 5",
441 "updated line 5",
442 true,
443 None,
444 ),
445 ];
446
447 let result = replace_in_file(&mut results);
449 assert!(result.is_ok());
450
451 assert_eq!(results.len(), 2);
453 assert_eq!(results[0].replace_result, Some(ReplaceResult::Success));
454 assert_eq!(results[1].replace_result, Some(ReplaceResult::Success));
455
456 let new_content = std::fs::read_to_string(&file_path).unwrap();
458 assert_eq!(
459 new_content,
460 "\n\r\nline 1\nnew text\r\nline 3\nline 4\r\nupdated line 5\r\n\n\n"
461 );
462 }
463
464 #[test]
465 fn test_replace_in_file_line_mismatch() {
466 let temp_dir = TempDir::new().unwrap();
467 let file_path = create_test_file(&temp_dir, "test.txt", "line 1\nactual text\nline 3\n");
468
469 let mut results = vec![create_search_result_with_replacement(
471 file_path.to_str().unwrap(),
472 2,
473 "expected text",
474 "new text",
475 true,
476 None,
477 )];
478
479 let result = replace_in_file(&mut results);
481 assert!(result.is_ok());
482
483 assert_eq!(
485 results[0].replace_result,
486 Some(ReplaceResult::Error(
487 "File changed since last search".to_owned()
488 ))
489 );
490
491 let new_content = std::fs::read_to_string(&file_path).unwrap();
493 assert_eq!(new_content, "line 1\nactual text\nline 3\n");
494 }
495
496 #[test]
497 fn test_replace_in_file_nonexistent_file() {
498 let mut results = vec![create_search_result_with_replacement(
499 "/nonexistent/path/file.txt",
500 1,
501 "old",
502 "new",
503 true,
504 None,
505 )];
506
507 let result = replace_in_file(&mut results);
508 assert!(result.is_err());
509 }
510
511 #[test]
512 fn test_replace_directory_errors() {
513 let mut results = vec![create_search_result_with_replacement(
514 "/", 0, "foo", "bar", true, None,
515 )];
516
517 let result = replace_in_file(&mut results);
518 assert!(result.is_err());
519 }
520
521 #[test]
523 fn test_replace_in_memory() {
524 let temp_dir = TempDir::new().unwrap();
525
526 let file_path = create_test_file(
528 &temp_dir,
529 "test.txt",
530 "This is a test.\nIt contains search_term that should be replaced.\nMultiple lines with search_term here.",
531 );
532
533 let result = replace_in_memory(&file_path, &fixed_search("search_term"), "replacement");
534 assert!(result.is_ok());
535 assert!(result.unwrap()); assert_file_content(
538 &file_path,
539 "This is a test.\nIt contains replacement that should be replaced.\nMultiple lines with replacement here.",
540 );
541
542 let regex_path = create_test_file(
544 &temp_dir,
545 "regex_test.txt",
546 "Number: 123, Code: 456, ID: 789",
547 );
548
549 let result = replace_in_memory(®ex_path, ®ex_search(r"\d{3}"), "XXX");
550 assert!(result.is_ok());
551 assert!(result.unwrap());
552
553 assert_file_content(®ex_path, "Number: XXX, Code: XXX, ID: XXX");
554 }
555
556 #[test]
557 fn test_replace_in_memory_no_match() {
558 let temp_dir = TempDir::new().unwrap();
559 let file_path = create_test_file(
560 &temp_dir,
561 "no_match.txt",
562 "This is a test file with no matches.",
563 );
564
565 let result = replace_in_memory(&file_path, &fixed_search("nonexistent"), "replacement");
566 assert!(result.is_ok());
567 assert!(!result.unwrap()); assert_file_content(&file_path, "This is a test file with no matches.");
571 }
572
573 #[test]
574 fn test_replace_in_memory_empty_file() {
575 let temp_dir = TempDir::new().unwrap();
576 let file_path = create_test_file(&temp_dir, "empty.txt", "");
577
578 let result = replace_in_memory(&file_path, &fixed_search("anything"), "replacement");
579 assert!(result.is_ok());
580 assert!(!result.unwrap());
581
582 assert_file_content(&file_path, "");
584 }
585
586 #[test]
587 fn test_replace_in_memory_nonexistent_file() {
588 let result = replace_in_memory(
589 Path::new("/nonexistent/path/file.txt"),
590 &fixed_search("test"),
591 "replacement",
592 );
593 assert!(result.is_err());
594 }
595
596 #[test]
598 fn test_replace_chunked() {
599 let temp_dir = TempDir::new().unwrap();
600
601 let file_path = create_test_file(
603 &temp_dir,
604 "test.txt",
605 "This is line one.\nThis contains search_pattern to replace.\nAnother line with search_pattern here.\nFinal line.",
606 );
607
608 let result = replace_chunked(&file_path, &fixed_search("search_pattern"), "replacement");
609 assert!(result.is_ok());
610 assert!(result.unwrap()); assert_file_content(
613 &file_path,
614 "This is line one.\nThis contains replacement to replace.\nAnother line with replacement here.\nFinal line.",
615 );
616
617 let regex_path = create_test_file(
619 &temp_dir,
620 "regex.txt",
621 "Line with numbers: 123 and 456.\nAnother line with 789.",
622 );
623
624 let result = replace_chunked(®ex_path, ®ex_search(r"\d{3}"), "XXX");
625 assert!(result.is_ok());
626 assert!(result.unwrap());
627
628 assert_file_content(
629 ®ex_path,
630 "Line with numbers: XXX and XXX.\nAnother line with XXX.",
631 );
632 }
633
634 #[test]
635 fn test_replace_chunked_no_match() {
636 let temp_dir = TempDir::new().unwrap();
637 let file_path = create_test_file(
638 &temp_dir,
639 "test.txt",
640 "This is a test file with no matching patterns.",
641 );
642
643 let result = replace_chunked(&file_path, &fixed_search("nonexistent"), "replacement");
644 assert!(result.is_ok());
645 assert!(!result.unwrap());
646
647 assert_file_content(&file_path, "This is a test file with no matching patterns.");
649 }
650
651 #[test]
652 fn test_replace_chunked_empty_file() {
653 let temp_dir = TempDir::new().unwrap();
654 let file_path = create_test_file(&temp_dir, "empty.txt", "");
655
656 let result = replace_chunked(&file_path, &fixed_search("anything"), "replacement");
657 assert!(result.is_ok());
658 assert!(!result.unwrap());
659
660 assert_file_content(&file_path, "");
662 }
663
664 #[test]
665 fn test_replace_chunked_nonexistent_file() {
666 let result = replace_chunked(
667 Path::new("/nonexistent/path/file.txt"),
668 &fixed_search("test"),
669 "replacement",
670 );
671 assert!(result.is_err());
672 }
673
674 #[test]
676 fn test_replace_all_in_file() {
677 let temp_dir = TempDir::new().unwrap();
678 let file_path = create_test_file(
679 &temp_dir,
680 "test.txt",
681 "This is a test file.\nIt has some content to replace.\nThe word replace should be replaced.",
682 );
683
684 let result = replace_all_in_file(&file_path, &fixed_search("replace"), "modify");
685 assert!(result.is_ok());
686 assert!(result.unwrap());
687
688 assert_file_content(
689 &file_path,
690 "This is a test file.\nIt has some content to modify.\nThe word modify should be modifyd.",
691 );
692 }
693
694 #[test]
695 fn test_unicode_in_file() {
696 let mut temp_file = NamedTempFile::new().unwrap();
697 writeln!(temp_file, "Line with Greek: αβγδε").unwrap();
698 write!(temp_file, "Line with Emoji: 😀 🚀 🌍\r\n").unwrap();
699 write!(temp_file, "Line with Arabic: مرحبا بالعالم").unwrap();
700 temp_file.flush().unwrap();
701
702 let search = SearchType::Pattern(Regex::new(r"\p{Greek}+").unwrap());
703 let replacement = "GREEK";
704 let results = search_file(temp_file.path(), &search)
705 .unwrap()
706 .into_iter()
707 .filter_map(|r| add_replacement(r, &search, replacement))
708 .collect::<Vec<_>>();
709
710 assert_eq!(results.len(), 1);
711 assert_eq!(results[0].replacement, "Line with Greek: GREEK");
712
713 let search = SearchType::Pattern(Regex::new(r"🚀").unwrap());
714 let replacement = "ROCKET";
715 let results = search_file(temp_file.path(), &search)
716 .unwrap()
717 .into_iter()
718 .filter_map(|r| add_replacement(r, &search, replacement))
719 .collect::<Vec<_>>();
720
721 assert_eq!(results.len(), 1);
722 assert_eq!(results[0].replacement, "Line with Emoji: 😀 ROCKET 🌍");
723 assert_eq!(results[0].search_result.line_ending, LineEnding::CrLf);
724 }
725
726 mod search_file_tests {
727 use super::*;
728 use fancy_regex::Regex as FancyRegex;
729 use regex::Regex;
730 use std::io::Write;
731 use tempfile::NamedTempFile;
732
733 #[test]
734 fn test_search_file_simple_match() {
735 let mut temp_file = NamedTempFile::new().unwrap();
736 writeln!(temp_file, "line 1").unwrap();
737 writeln!(temp_file, "search target").unwrap();
738 writeln!(temp_file, "line 3").unwrap();
739 temp_file.flush().unwrap();
740
741 let search = test_helpers::create_fixed_search("search");
742 let replacement = "replace";
743 let results = search_file(temp_file.path(), &search)
744 .unwrap()
745 .into_iter()
746 .filter_map(|r| add_replacement(r, &search, replacement))
747 .collect::<Vec<_>>();
748
749 assert_eq!(results.len(), 1);
750 assert_eq!(results[0].search_result.line_number, 2);
751 assert_eq!(results[0].search_result.line, "search target");
752 assert_eq!(results[0].replacement, "replace target");
753 assert!(results[0].search_result.included);
754 }
755
756 #[test]
757 fn test_search_file_multiple_matches() {
758 let mut temp_file = NamedTempFile::new().unwrap();
759 writeln!(temp_file, "test line 1").unwrap();
760 writeln!(temp_file, "test line 2").unwrap();
761 writeln!(temp_file, "no match here").unwrap();
762 writeln!(temp_file, "test line 4").unwrap();
763 temp_file.flush().unwrap();
764
765 let search = test_helpers::create_fixed_search("test");
766 let replacement = "replaced";
767 let results = search_file(temp_file.path(), &search)
768 .unwrap()
769 .into_iter()
770 .filter_map(|r| add_replacement(r, &search, replacement))
771 .collect::<Vec<_>>();
772
773 assert_eq!(results.len(), 3);
774 assert_eq!(results[0].search_result.line_number, 1);
775 assert_eq!(results[0].replacement, "replaced line 1");
776 assert_eq!(results[1].search_result.line_number, 2);
777 assert_eq!(results[1].replacement, "replaced line 2");
778 assert_eq!(results[2].search_result.line_number, 4);
779 assert_eq!(results[2].replacement, "replaced line 4");
780 }
781
782 #[test]
783 fn test_search_file_no_matches() {
784 let mut temp_file = NamedTempFile::new().unwrap();
785 writeln!(temp_file, "line 1").unwrap();
786 writeln!(temp_file, "line 2").unwrap();
787 writeln!(temp_file, "line 3").unwrap();
788 temp_file.flush().unwrap();
789
790 let search = SearchType::Fixed("nonexistent".to_string());
791 let replacement = "replace";
792 let results = search_file(temp_file.path(), &search)
793 .unwrap()
794 .into_iter()
795 .filter_map(|r| add_replacement(r, &search, replacement))
796 .collect::<Vec<_>>();
797
798 assert_eq!(results.len(), 0);
799 }
800
801 #[test]
802 fn test_search_file_regex_pattern() {
803 let mut temp_file = NamedTempFile::new().unwrap();
804 writeln!(temp_file, "number: 123").unwrap();
805 writeln!(temp_file, "text without numbers").unwrap();
806 writeln!(temp_file, "another number: 456").unwrap();
807 temp_file.flush().unwrap();
808
809 let search = SearchType::Pattern(Regex::new(r"\d+").unwrap());
810 let replacement = "XXX";
811 let results = search_file(temp_file.path(), &search)
812 .unwrap()
813 .into_iter()
814 .filter_map(|r| add_replacement(r, &search, replacement))
815 .collect::<Vec<_>>();
816
817 assert_eq!(results.len(), 2);
818 assert_eq!(results[0].replacement, "number: XXX");
819 assert_eq!(results[1].replacement, "another number: XXX");
820 }
821
822 #[test]
823 fn test_search_file_advanced_regex_pattern() {
824 let mut temp_file = NamedTempFile::new().unwrap();
825 writeln!(temp_file, "123abc456").unwrap();
826 writeln!(temp_file, "abc").unwrap();
827 writeln!(temp_file, "789xyz123").unwrap();
828 writeln!(temp_file, "no match").unwrap();
829 temp_file.flush().unwrap();
830
831 let search =
833 SearchType::PatternAdvanced(FancyRegex::new(r"(?<=\d{3})abc(?=\d{3})").unwrap());
834 let replacement = "REPLACED";
835 let results = search_file(temp_file.path(), &search)
836 .unwrap()
837 .into_iter()
838 .filter_map(|r| add_replacement(r, &search, replacement))
839 .collect::<Vec<_>>();
840
841 assert_eq!(results.len(), 1);
842 assert_eq!(results[0].replacement, "123REPLACED456");
843 assert_eq!(results[0].search_result.line_number, 1);
844 }
845
846 #[test]
847 fn test_search_file_empty_search() {
848 let mut temp_file = NamedTempFile::new().unwrap();
849 writeln!(temp_file, "some content").unwrap();
850 temp_file.flush().unwrap();
851
852 let search = SearchType::Fixed("".to_string());
853 let replacement = "replace";
854 let results = search_file(temp_file.path(), &search)
855 .unwrap()
856 .into_iter()
857 .filter_map(|r| add_replacement(r, &search, replacement))
858 .collect::<Vec<_>>();
859
860 assert_eq!(results.len(), 0);
861 }
862
863 #[test]
864 fn test_search_file_preserves_line_endings() {
865 let mut temp_file = NamedTempFile::new().unwrap();
866 write!(temp_file, "line1\nline2\r\nline3").unwrap();
867 temp_file.flush().unwrap();
868
869 let search = SearchType::Fixed("line".to_string());
870 let replacement = "X";
871 let results = search_file(temp_file.path(), &search)
872 .unwrap()
873 .into_iter()
874 .filter_map(|r| add_replacement(r, &search, replacement))
875 .collect::<Vec<_>>();
876
877 assert_eq!(results.len(), 3);
878 assert_eq!(results[0].search_result.line_ending, LineEnding::Lf);
879 assert_eq!(results[1].search_result.line_ending, LineEnding::CrLf);
880 assert_eq!(results[2].search_result.line_ending, LineEnding::None);
881 }
882
883 #[test]
884 fn test_search_file_nonexistent() {
885 let nonexistent_path = PathBuf::from("/this/file/does/not/exist.txt");
886 let search = test_helpers::create_fixed_search("test");
887 let results = search_file(&nonexistent_path, &search);
888 assert!(results.is_err());
889 }
890
891 #[test]
892 fn test_search_file_unicode_content() {
893 let mut temp_file = NamedTempFile::new().unwrap();
894 writeln!(temp_file, "Hello 世界!").unwrap();
895 writeln!(temp_file, "Здравствуй мир!").unwrap();
896 writeln!(temp_file, "🚀 Rocket").unwrap();
897 temp_file.flush().unwrap();
898
899 let search = SearchType::Fixed("世界".to_string());
900 let replacement = "World";
901 let results = search_file(temp_file.path(), &search)
902 .unwrap()
903 .into_iter()
904 .filter_map(|r| add_replacement(r, &search, replacement))
905 .collect::<Vec<_>>();
906
907 assert_eq!(results.len(), 1);
908 assert_eq!(results[0].replacement, "Hello World!");
909 }
910
911 #[test]
912 fn test_search_file_with_binary_content() {
913 let mut temp_file = NamedTempFile::new().unwrap();
914 let binary_data = [0x00, 0x01, 0x02, 0xFF, 0xFE];
916 temp_file.write_all(&binary_data).unwrap();
917 temp_file.flush().unwrap();
918
919 let search = test_helpers::create_fixed_search("test");
920 let replacement = "replace";
921 let results = search_file(temp_file.path(), &search)
922 .unwrap()
923 .into_iter()
924 .filter_map(|r| add_replacement(r, &search, replacement))
925 .collect::<Vec<_>>();
926
927 assert_eq!(results.len(), 0);
928 }
929
930 #[test]
931 fn test_search_file_large_content() {
932 let mut temp_file = NamedTempFile::new().unwrap();
933
934 for i in 0..1000 {
936 if i % 100 == 0 {
937 writeln!(temp_file, "target line {i}").unwrap();
938 } else {
939 writeln!(temp_file, "normal line {i}").unwrap();
940 }
941 }
942 temp_file.flush().unwrap();
943
944 let search = SearchType::Fixed("target".to_string());
945 let replacement = "found";
946 let results = search_file(temp_file.path(), &search)
947 .unwrap()
948 .into_iter()
949 .filter_map(|r| add_replacement(r, &search, replacement))
950 .collect::<Vec<_>>();
951
952 assert_eq!(results.len(), 10); assert_eq!(results[0].search_result.line_number, 1); assert_eq!(results[1].search_result.line_number, 101);
955 assert_eq!(results[9].search_result.line_number, 901);
956 }
957 }
958
959 mod replace_if_match_tests {
960 use crate::validation::SearchConfig;
961
962 use super::*;
963
964 mod test_helpers {
965 use crate::{
966 search::ParsedSearchConfig,
967 validation::{
968 SearchConfig, SimpleErrorHandler, ValidationResult,
969 validate_search_configuration,
970 },
971 };
972
973 pub fn must_parse_search_config(search_config: SearchConfig<'_>) -> ParsedSearchConfig {
974 let mut error_handler = SimpleErrorHandler::new();
975 let (search_config, _dir_config) =
976 match validate_search_configuration(search_config, None, &mut error_handler)
977 .unwrap()
978 {
979 ValidationResult::Success(search_config) => search_config,
980 ValidationResult::ValidationErrors => {
981 panic!("{}", error_handler.errors_str().unwrap());
982 }
983 };
984 search_config
985 }
986 }
987
988 mod fixed_string_tests {
989 use super::*;
990
991 mod whole_word_true_match_case_true {
992
993 use super::*;
994
995 #[test]
996 fn test_basic_replacement() {
997 let search_config = SearchConfig {
998 search_text: "world",
999 fixed_strings: true,
1000 match_whole_word: true,
1001 match_case: true,
1002 replacement_text: "earth",
1003 advanced_regex: false,
1004 };
1005 let parsed = test_helpers::must_parse_search_config(search_config);
1006
1007 assert_eq!(
1008 replacement_if_match("hello world", &parsed.search, &parsed.replace),
1009 Some("hello earth".to_string())
1010 );
1011 }
1012
1013 #[test]
1014 fn test_case_sensitivity() {
1015 let search_config = SearchConfig {
1016 search_text: "world",
1017 fixed_strings: true,
1018 match_whole_word: true,
1019 match_case: true,
1020 replacement_text: "earth",
1021 advanced_regex: false,
1022 };
1023 let parsed = test_helpers::must_parse_search_config(search_config);
1024
1025 assert_eq!(
1026 replacement_if_match("hello WORLD", &parsed.search, &parsed.replace),
1027 None
1028 );
1029 }
1030
1031 #[test]
1032 fn test_word_boundaries() {
1033 let search_config = SearchConfig {
1034 search_text: "world",
1035 fixed_strings: true,
1036 match_whole_word: true,
1037 match_case: true,
1038 replacement_text: "earth",
1039 advanced_regex: false,
1040 };
1041 let parsed = test_helpers::must_parse_search_config(search_config);
1042
1043 assert_eq!(
1044 replacement_if_match("worldwide", &parsed.search, &parsed.replace),
1045 None
1046 );
1047 }
1048 }
1049
1050 mod whole_word_true_match_case_false {
1051 use super::*;
1052
1053 #[test]
1054 fn test_basic_replacement() {
1055 let search_config = SearchConfig {
1056 search_text: "world",
1057 fixed_strings: true,
1058 match_whole_word: true,
1059 match_case: false,
1060 replacement_text: "earth",
1061 advanced_regex: false,
1062 };
1063 let parsed = test_helpers::must_parse_search_config(search_config);
1064
1065 assert_eq!(
1066 replacement_if_match("hello world", &parsed.search, &parsed.replace),
1067 Some("hello earth".to_string())
1068 );
1069 }
1070
1071 #[test]
1072 fn test_case_insensitivity() {
1073 let search_config = SearchConfig {
1074 search_text: "world",
1075 fixed_strings: true,
1076 match_whole_word: true,
1077 match_case: false,
1078 replacement_text: "earth",
1079 advanced_regex: false,
1080 };
1081 let parsed = test_helpers::must_parse_search_config(search_config);
1082
1083 assert_eq!(
1084 replacement_if_match("hello WORLD", &parsed.search, &parsed.replace),
1085 Some("hello earth".to_string())
1086 );
1087 }
1088
1089 #[test]
1090 fn test_word_boundaries() {
1091 let search_config = SearchConfig {
1092 search_text: "world",
1093 fixed_strings: true,
1094 match_whole_word: true,
1095 match_case: false,
1096 replacement_text: "earth",
1097 advanced_regex: false,
1098 };
1099 let parsed = test_helpers::must_parse_search_config(search_config);
1100
1101 assert_eq!(
1102 replacement_if_match("worldwide", &parsed.search, &parsed.replace),
1103 None
1104 );
1105 }
1106
1107 #[test]
1108 fn test_unicode() {
1109 let search_config = SearchConfig {
1110 search_text: "café",
1111 fixed_strings: true,
1112 match_whole_word: true,
1113 match_case: false,
1114 replacement_text: "restaurant",
1115 advanced_regex: false,
1116 };
1117 let parsed = test_helpers::must_parse_search_config(search_config);
1118
1119 assert_eq!(
1120 replacement_if_match("Hello CAFÉ table", &parsed.search, &parsed.replace),
1121 Some("Hello restaurant table".to_string())
1122 );
1123 }
1124 }
1125
1126 mod whole_word_false_match_case_true {
1127 use super::*;
1128
1129 #[test]
1130 fn test_basic_replacement() {
1131 let search_config = SearchConfig {
1132 search_text: "world",
1133 fixed_strings: true,
1134 match_whole_word: false,
1135 match_case: true,
1136 replacement_text: "earth",
1137 advanced_regex: false,
1138 };
1139 let parsed = test_helpers::must_parse_search_config(search_config);
1140
1141 assert_eq!(
1142 replacement_if_match("hello world", &parsed.search, &parsed.replace),
1143 Some("hello earth".to_string())
1144 );
1145 }
1146
1147 #[test]
1148 fn test_case_sensitivity() {
1149 let search_config = SearchConfig {
1150 search_text: "world",
1151 fixed_strings: true,
1152 match_whole_word: false,
1153 match_case: true,
1154 replacement_text: "earth",
1155 advanced_regex: false,
1156 };
1157 let parsed = test_helpers::must_parse_search_config(search_config);
1158
1159 assert_eq!(
1160 replacement_if_match("hello WORLD", &parsed.search, &parsed.replace),
1161 None
1162 );
1163 }
1164
1165 #[test]
1166 fn test_substring_matches() {
1167 let search_config = SearchConfig {
1168 search_text: "world",
1169 fixed_strings: true,
1170 match_whole_word: false,
1171 match_case: true,
1172 replacement_text: "earth",
1173 advanced_regex: false,
1174 };
1175 let parsed = test_helpers::must_parse_search_config(search_config);
1176
1177 assert_eq!(
1178 replacement_if_match("worldwide", &parsed.search, &parsed.replace),
1179 Some("earthwide".to_string())
1180 );
1181 }
1182 }
1183
1184 mod whole_word_false_match_case_false {
1185 use super::*;
1186
1187 #[test]
1188 fn test_basic_replacement() {
1189 let search_config = SearchConfig {
1190 search_text: "world",
1191 fixed_strings: true,
1192 match_whole_word: false,
1193 match_case: false,
1194 replacement_text: "earth",
1195 advanced_regex: false,
1196 };
1197 let parsed = test_helpers::must_parse_search_config(search_config);
1198
1199 assert_eq!(
1200 replacement_if_match("hello world", &parsed.search, &parsed.replace),
1201 Some("hello earth".to_string())
1202 );
1203 }
1204
1205 #[test]
1206 fn test_case_insensitivity() {
1207 let search_config = SearchConfig {
1208 search_text: "world",
1209 fixed_strings: true,
1210 match_whole_word: false,
1211 match_case: false,
1212 replacement_text: "earth",
1213 advanced_regex: false,
1214 };
1215 let parsed = test_helpers::must_parse_search_config(search_config);
1216
1217 assert_eq!(
1218 replacement_if_match("hello WORLD", &parsed.search, &parsed.replace),
1219 Some("hello earth".to_string())
1220 );
1221 }
1222
1223 #[test]
1224 fn test_substring_matches() {
1225 let search_config = SearchConfig {
1226 search_text: "world",
1227 fixed_strings: true,
1228 match_whole_word: false,
1229 match_case: false,
1230 replacement_text: "earth",
1231 advanced_regex: false,
1232 };
1233 let parsed = test_helpers::must_parse_search_config(search_config);
1234
1235 assert_eq!(
1236 replacement_if_match("WORLDWIDE", &parsed.search, &parsed.replace),
1237 Some("earthWIDE".to_string())
1238 );
1239 }
1240 }
1241 }
1242
1243 mod regex_pattern_tests {
1244 use super::*;
1245
1246 mod whole_word_true_match_case_true {
1247 use crate::validation::SearchConfig;
1248
1249 use super::*;
1250
1251 #[test]
1252 fn test_basic_regex() {
1253 let re_str = r"w\w+d";
1254 let search_config = SearchConfig {
1255 search_text: re_str,
1256 fixed_strings: false,
1257 match_whole_word: true,
1258 match_case: true,
1259 replacement_text: "earth",
1260 advanced_regex: false,
1261 };
1262 let parsed = test_helpers::must_parse_search_config(search_config);
1263
1264 assert_eq!(
1265 replacement_if_match("hello world", &parsed.search, &parsed.replace),
1266 Some("hello earth".to_string())
1267 );
1268 }
1269
1270 #[test]
1271 fn test_case_sensitivity() {
1272 let re_str = r"world";
1273 let search_config = SearchConfig {
1274 search_text: re_str,
1275 fixed_strings: false,
1276 match_whole_word: true,
1277 match_case: true,
1278 replacement_text: "earth",
1279 advanced_regex: false,
1280 };
1281 let parsed = test_helpers::must_parse_search_config(search_config);
1282
1283 assert_eq!(
1284 replacement_if_match("hello WORLD", &parsed.search, &parsed.replace),
1285 None
1286 );
1287 }
1288
1289 #[test]
1290 fn test_word_boundaries() {
1291 let re_str = r"world";
1292 let search_config = SearchConfig {
1293 search_text: re_str,
1294 fixed_strings: false,
1295 match_whole_word: true,
1296 match_case: true,
1297 replacement_text: "earth",
1298 advanced_regex: false,
1299 };
1300 let parsed = test_helpers::must_parse_search_config(search_config);
1301
1302 assert_eq!(
1303 replacement_if_match("worldwide", &parsed.search, &parsed.replace),
1304 None
1305 );
1306 }
1307 }
1308
1309 mod whole_word_true_match_case_false {
1310 use super::*;
1311
1312 #[test]
1313 fn test_basic_regex() {
1314 let re_str = r"w\w+d";
1315 let search_config = SearchConfig {
1316 search_text: re_str,
1317 fixed_strings: false,
1318 match_whole_word: true,
1319 match_case: false,
1320 replacement_text: "earth",
1321 advanced_regex: false,
1322 };
1323 let parsed = test_helpers::must_parse_search_config(search_config);
1324
1325 assert_eq!(
1326 replacement_if_match("hello WORLD", &parsed.search, &parsed.replace),
1327 Some("hello earth".to_string())
1328 );
1329 }
1330
1331 #[test]
1332 fn test_word_boundaries() {
1333 let re_str = r"world";
1334 let search_config = SearchConfig {
1335 search_text: re_str,
1336 fixed_strings: false,
1337 match_whole_word: true,
1338 match_case: false,
1339 replacement_text: "earth",
1340 advanced_regex: false,
1341 };
1342 let parsed = test_helpers::must_parse_search_config(search_config);
1343
1344 assert_eq!(
1345 replacement_if_match("worldwide", &parsed.search, &parsed.replace),
1346 None
1347 );
1348 }
1349
1350 #[test]
1351 fn test_special_characters() {
1352 let re_str = r"\d+";
1353 let search_config = SearchConfig {
1354 search_text: re_str,
1355 fixed_strings: false,
1356 match_whole_word: true,
1357 match_case: false,
1358 replacement_text: "NUM",
1359 advanced_regex: false,
1360 };
1361 let parsed = test_helpers::must_parse_search_config(search_config);
1362
1363 assert_eq!(
1364 replacement_if_match("test 123 number", &parsed.search, &parsed.replace),
1365 Some("test NUM number".to_string())
1366 );
1367 }
1368
1369 #[test]
1370 fn test_unicode_word_boundaries() {
1371 let re_str = r"\b\p{Script=Han}{2}\b";
1372 let search_config = SearchConfig {
1373 search_text: re_str,
1374 fixed_strings: false,
1375 match_whole_word: true,
1376 match_case: false,
1377 replacement_text: "XX",
1378 advanced_regex: false,
1379 };
1380 let parsed = test_helpers::must_parse_search_config(search_config);
1381
1382 assert!(
1383 replacement_if_match("Text 世界 more", &parsed.search, &parsed.replace)
1384 .is_some()
1385 );
1386 assert!(replacement_if_match("Text世界more", &parsed.search, "XX").is_none());
1387 }
1388 }
1389
1390 mod whole_word_false_match_case_true {
1391 use super::*;
1392
1393 #[test]
1394 fn test_basic_regex() {
1395 let re_str = r"w\w+d";
1396 let search_config = SearchConfig {
1397 search_text: re_str,
1398 fixed_strings: false,
1399 match_whole_word: false,
1400 match_case: true,
1401 replacement_text: "earth",
1402 advanced_regex: false,
1403 };
1404 let parsed = test_helpers::must_parse_search_config(search_config);
1405
1406 assert_eq!(
1407 replacement_if_match("hello world", &parsed.search, &parsed.replace),
1408 Some("hello earth".to_string())
1409 );
1410 }
1411
1412 #[test]
1413 fn test_case_sensitivity() {
1414 let re_str = r"world";
1415 let search_config = SearchConfig {
1416 search_text: re_str,
1417 fixed_strings: false,
1418 match_whole_word: false,
1419 match_case: true,
1420 replacement_text: "earth",
1421 advanced_regex: false,
1422 };
1423 let parsed = test_helpers::must_parse_search_config(search_config);
1424
1425 assert_eq!(
1426 replacement_if_match("hello WORLD", &parsed.search, &parsed.replace),
1427 None
1428 );
1429 }
1430
1431 #[test]
1432 fn test_substring_matches() {
1433 let re_str = r"world";
1434 let search_config = SearchConfig {
1435 search_text: re_str,
1436 fixed_strings: false,
1437 match_whole_word: false,
1438 match_case: true,
1439 replacement_text: "earth",
1440 advanced_regex: false,
1441 };
1442 let parsed = test_helpers::must_parse_search_config(search_config);
1443
1444 assert_eq!(
1445 replacement_if_match("worldwide", &parsed.search, &parsed.replace),
1446 Some("earthwide".to_string())
1447 );
1448 }
1449 }
1450
1451 mod whole_word_false_match_case_false {
1452 use super::*;
1453
1454 #[test]
1455 fn test_basic_regex() {
1456 let re_str = r"w\w+d";
1457 let search_config = SearchConfig {
1458 search_text: re_str,
1459 fixed_strings: false,
1460 match_whole_word: false,
1461 match_case: false,
1462 replacement_text: "earth",
1463 advanced_regex: false,
1464 };
1465 let parsed = test_helpers::must_parse_search_config(search_config);
1466
1467 assert_eq!(
1468 replacement_if_match("hello WORLD", &parsed.search, &parsed.replace),
1469 Some("hello earth".to_string())
1470 );
1471 }
1472
1473 #[test]
1474 fn test_substring_matches() {
1475 let re_str = r"world";
1476 let search_config = SearchConfig {
1477 search_text: re_str,
1478 fixed_strings: false,
1479 match_whole_word: false,
1480 match_case: false,
1481 replacement_text: "earth",
1482 advanced_regex: false,
1483 };
1484 let parsed = test_helpers::must_parse_search_config(search_config);
1485
1486 assert_eq!(
1487 replacement_if_match("WORLDWIDE", &parsed.search, &parsed.replace),
1488 Some("earthWIDE".to_string())
1489 );
1490 }
1491
1492 #[test]
1493 fn test_complex_pattern() {
1494 let re_str = r"\d{3}-\d{2}-\d{4}";
1495 let search_config = SearchConfig {
1496 search_text: re_str,
1497 fixed_strings: false,
1498 match_whole_word: false,
1499 match_case: false,
1500 replacement_text: "XXX-XX-XXXX",
1501 advanced_regex: false,
1502 };
1503 let parsed = test_helpers::must_parse_search_config(search_config);
1504
1505 assert_eq!(
1506 replacement_if_match("SSN: 123-45-6789", &parsed.search, &parsed.replace),
1507 Some("SSN: XXX-XX-XXXX".to_string())
1508 );
1509 }
1510 }
1511 }
1512
1513 mod fancy_regex_pattern_tests {
1514 use super::*;
1515
1516 mod whole_word_true_match_case_true {
1517
1518 use super::*;
1519
1520 #[test]
1521 fn test_lookbehind() {
1522 let re_str = r"(?<=@)\w+";
1523 let search_config = SearchConfig {
1524 search_text: re_str,
1525 match_whole_word: true,
1526 fixed_strings: false,
1527 advanced_regex: true,
1528 match_case: true,
1529 replacement_text: "domain",
1530 };
1531 let parsed = test_helpers::must_parse_search_config(search_config);
1532
1533 assert_eq!(
1534 replacement_if_match(
1535 "email: user@example.com",
1536 &parsed.search,
1537 &parsed.replace
1538 ),
1539 Some("email: user@domain.com".to_string())
1540 );
1541 }
1542
1543 #[test]
1544 fn test_lookahead() {
1545 let re_str = r"\w+(?=\.\w+$)";
1546 let search_config = SearchConfig {
1547 search_text: re_str,
1548 match_whole_word: true,
1549 fixed_strings: false,
1550 advanced_regex: true,
1551 match_case: true,
1552 replacement_text: "report",
1553 };
1554 let parsed = test_helpers::must_parse_search_config(search_config);
1555
1556 assert_eq!(
1557 replacement_if_match("file: document.pdf", &parsed.search, &parsed.replace),
1558 Some("file: report.pdf".to_string())
1559 );
1560 }
1561
1562 #[test]
1563 fn test_case_sensitivity() {
1564 let re_str = r"world";
1565 let search_config = SearchConfig {
1566 search_text: re_str,
1567 match_whole_word: true,
1568 fixed_strings: false,
1569 advanced_regex: true,
1570 match_case: true,
1571 replacement_text: "earth",
1572 };
1573 let parsed = test_helpers::must_parse_search_config(search_config);
1574
1575 assert_eq!(
1576 replacement_if_match("hello WORLD", &parsed.search, &parsed.replace),
1577 None
1578 );
1579 }
1580 }
1581
1582 mod whole_word_true_match_case_false {
1583 use super::*;
1584
1585 #[test]
1586 fn test_lookbehind_case_insensitive() {
1587 let re_str = r"(?<=@)\w+";
1588 let search_config = SearchConfig {
1589 search_text: re_str,
1590 match_whole_word: true,
1591 fixed_strings: false,
1592 advanced_regex: true,
1593 match_case: false,
1594 replacement_text: "domain",
1595 };
1596 let parsed = test_helpers::must_parse_search_config(search_config);
1597
1598 assert_eq!(
1599 replacement_if_match(
1600 "email: user@EXAMPLE.com",
1601 &parsed.search,
1602 &parsed.replace
1603 ),
1604 Some("email: user@domain.com".to_string())
1605 );
1606 }
1607
1608 #[test]
1609 fn test_word_boundaries() {
1610 let re_str = r"world";
1611 let search_config = SearchConfig {
1612 search_text: re_str,
1613 match_whole_word: true,
1614 fixed_strings: false,
1615 advanced_regex: true,
1616 match_case: false,
1617 replacement_text: "earth",
1618 };
1619 let parsed = test_helpers::must_parse_search_config(search_config);
1620
1621 assert_eq!(
1622 replacement_if_match("worldwide", &parsed.search, &parsed.replace),
1623 None
1624 );
1625 }
1626 }
1627
1628 mod whole_word_false_match_case_true {
1629 use super::*;
1630
1631 #[test]
1632 fn test_complex_pattern() {
1633 let re_str = r"(?<=\d{4}-\d{2}-\d{2}T)\d{2}:\d{2}";
1634 let search_config = SearchConfig {
1635 search_text: re_str,
1636 match_whole_word: false,
1637 fixed_strings: false,
1638 advanced_regex: true,
1639 match_case: true,
1640 replacement_text: "XX:XX",
1641 };
1642 let parsed = test_helpers::must_parse_search_config(search_config);
1643
1644 assert_eq!(
1645 replacement_if_match(
1646 "Timestamp: 2023-01-15T14:30:00Z",
1647 &parsed.search,
1648 &parsed.replace
1649 ),
1650 Some("Timestamp: 2023-01-15TXX:XX:00Z".to_string())
1651 );
1652 }
1653
1654 #[test]
1655 fn test_case_sensitivity() {
1656 let re_str = r"WORLD";
1657 let search_config = SearchConfig {
1658 search_text: re_str,
1659 match_whole_word: false,
1660 fixed_strings: false,
1661 advanced_regex: true,
1662 match_case: true,
1663 replacement_text: "earth",
1664 };
1665 let parsed = test_helpers::must_parse_search_config(search_config);
1666
1667 assert_eq!(
1668 replacement_if_match("hello world", &parsed.search, &parsed.replace),
1669 None
1670 );
1671 }
1672 }
1673
1674 mod whole_word_false_match_case_false {
1675 use super::*;
1676
1677 #[test]
1678 fn test_complex_pattern_case_insensitive() {
1679 let re_str = r"(?<=\[)\w+(?=\])";
1680 let search_config = SearchConfig {
1681 search_text: re_str,
1682 match_whole_word: false,
1683 fixed_strings: false,
1684 advanced_regex: true,
1685 match_case: false,
1686 replacement_text: "ERROR",
1687 };
1688 let parsed = test_helpers::must_parse_search_config(search_config);
1689
1690 assert_eq!(
1691 replacement_if_match(
1692 "Tag: [WARNING] message",
1693 &parsed.search,
1694 &parsed.replace
1695 ),
1696 Some("Tag: [ERROR] message".to_string())
1697 );
1698 }
1699
1700 #[test]
1701 fn test_unicode_support() {
1702 let re_str = r"\p{Greek}+";
1703 let search_config = SearchConfig {
1704 search_text: re_str,
1705 match_whole_word: false,
1706 fixed_strings: false,
1707 advanced_regex: true,
1708 match_case: false,
1709 replacement_text: "GREEK",
1710 };
1711 let parsed = test_helpers::must_parse_search_config(search_config);
1712
1713 assert_eq!(
1714 replacement_if_match("Symbol: αβγδ", &parsed.search, &parsed.replace),
1715 Some("Symbol: GREEK".to_string())
1716 );
1717 }
1718 }
1719 }
1720
1721 #[test]
1722 fn test_multiple_replacements() {
1723 let search_config = SearchConfig {
1724 search_text: "world",
1725 fixed_strings: true,
1726 match_whole_word: true,
1727 match_case: false,
1728 replacement_text: "earth",
1729 advanced_regex: false,
1730 };
1731 let parsed = test_helpers::must_parse_search_config(search_config);
1732 assert_eq!(
1733 replacement_if_match("world hello world", &parsed.search, &parsed.replace),
1734 Some("earth hello earth".to_string())
1735 );
1736 }
1737
1738 #[test]
1739 fn test_no_match() {
1740 let search_config = SearchConfig {
1741 search_text: "world",
1742 fixed_strings: true,
1743 match_whole_word: true,
1744 match_case: false,
1745 replacement_text: "earth",
1746 advanced_regex: false,
1747 };
1748 let parsed = test_helpers::must_parse_search_config(search_config);
1749 assert_eq!(
1750 replacement_if_match("worldwide", &parsed.search, &parsed.replace),
1751 None
1752 );
1753 let search_config = SearchConfig {
1754 search_text: "world",
1755 fixed_strings: true,
1756 match_whole_word: true,
1757 match_case: false,
1758 replacement_text: "earth",
1759 advanced_regex: false,
1760 };
1761 let parsed = test_helpers::must_parse_search_config(search_config);
1762 assert_eq!(
1763 replacement_if_match("_world_", &parsed.search, &parsed.replace),
1764 None
1765 );
1766 }
1767
1768 #[test]
1769 fn test_word_boundaries() {
1770 let search_config = SearchConfig {
1771 search_text: "world",
1772 fixed_strings: true,
1773 match_whole_word: true,
1774 match_case: false,
1775 replacement_text: "earth",
1776 advanced_regex: false,
1777 };
1778 let parsed = test_helpers::must_parse_search_config(search_config);
1779 assert_eq!(
1780 replacement_if_match(",world-", &parsed.search, &parsed.replace),
1781 Some(",earth-".to_string())
1782 );
1783 let search_config = SearchConfig {
1784 search_text: "world",
1785 fixed_strings: true,
1786 match_whole_word: true,
1787 match_case: false,
1788 replacement_text: "earth",
1789 advanced_regex: false,
1790 };
1791 let parsed = test_helpers::must_parse_search_config(search_config);
1792 assert_eq!(
1793 replacement_if_match("world-word", &parsed.search, &parsed.replace),
1794 Some("earth-word".to_string())
1795 );
1796 let search_config = SearchConfig {
1797 search_text: "world",
1798 fixed_strings: true,
1799 match_whole_word: true,
1800 match_case: false,
1801 replacement_text: "earth",
1802 advanced_regex: false,
1803 };
1804 let parsed = test_helpers::must_parse_search_config(search_config);
1805 assert_eq!(
1806 replacement_if_match("Hello-world!", &parsed.search, &parsed.replace),
1807 Some("Hello-earth!".to_string())
1808 );
1809 }
1810
1811 #[test]
1812 fn test_case_sensitive() {
1813 let search_config = SearchConfig {
1814 search_text: "world",
1815 fixed_strings: true,
1816 match_whole_word: true,
1817 match_case: true,
1818 replacement_text: "earth",
1819 advanced_regex: false,
1820 };
1821 let parsed = test_helpers::must_parse_search_config(search_config);
1822 assert_eq!(
1823 replacement_if_match("Hello WORLD", &parsed.search, &parsed.replace),
1824 None
1825 );
1826 let search_config = SearchConfig {
1827 search_text: "wOrld",
1828 fixed_strings: true,
1829 match_whole_word: true,
1830 match_case: true,
1831 replacement_text: "earth",
1832 advanced_regex: false,
1833 };
1834 let parsed = test_helpers::must_parse_search_config(search_config);
1835 assert_eq!(
1836 replacement_if_match("Hello world", &parsed.search, &parsed.replace),
1837 None
1838 );
1839 }
1840
1841 #[test]
1842 fn test_empty_strings() {
1843 let search_config = SearchConfig {
1844 search_text: "world",
1845 fixed_strings: true,
1846 match_whole_word: true,
1847 match_case: false,
1848 replacement_text: "earth",
1849 advanced_regex: false,
1850 };
1851 let parsed = test_helpers::must_parse_search_config(search_config);
1852 assert_eq!(
1853 replacement_if_match("", &parsed.search, &parsed.replace),
1854 None
1855 );
1856 let search_config = SearchConfig {
1857 search_text: "",
1858 fixed_strings: true,
1859 match_whole_word: true,
1860 match_case: false,
1861 replacement_text: "earth",
1862 advanced_regex: false,
1863 };
1864 let parsed = test_helpers::must_parse_search_config(search_config);
1865 assert_eq!(
1866 replacement_if_match("hello world", &parsed.search, &parsed.replace),
1867 None
1868 );
1869 }
1870
1871 #[test]
1872 fn test_substring_no_match() {
1873 let search_config = SearchConfig {
1874 search_text: "world",
1875 fixed_strings: true,
1876 match_whole_word: true,
1877 match_case: false,
1878 replacement_text: "earth",
1879 advanced_regex: false,
1880 };
1881 let parsed = test_helpers::must_parse_search_config(search_config);
1882 assert_eq!(
1883 replacement_if_match("worldwide web", &parsed.search, &parsed.replace),
1884 None
1885 );
1886 let search_config = SearchConfig {
1887 search_text: "world",
1888 fixed_strings: true,
1889 match_whole_word: true,
1890 match_case: false,
1891 replacement_text: "earth",
1892 advanced_regex: false,
1893 };
1894 let parsed = test_helpers::must_parse_search_config(search_config);
1895 assert_eq!(
1896 replacement_if_match("underworld", &parsed.search, &parsed.replace),
1897 None
1898 );
1899 }
1900
1901 #[test]
1902 fn test_special_regex_chars() {
1903 let search_config = SearchConfig {
1904 search_text: "(world)",
1905 fixed_strings: true,
1906 match_whole_word: true,
1907 match_case: false,
1908 replacement_text: "earth",
1909 advanced_regex: false,
1910 };
1911 let parsed = test_helpers::must_parse_search_config(search_config);
1912 assert_eq!(
1913 replacement_if_match("hello (world)", &parsed.search, &parsed.replace),
1914 Some("hello earth".to_string())
1915 );
1916 let search_config = SearchConfig {
1917 search_text: "world.*",
1918 fixed_strings: true,
1919 match_whole_word: true,
1920 match_case: false,
1921 replacement_text: "ea+rth",
1922 advanced_regex: false,
1923 };
1924 let parsed = test_helpers::must_parse_search_config(search_config);
1925 assert_eq!(
1926 replacement_if_match("hello world.*", &parsed.search, &parsed.replace),
1927 Some("hello ea+rth".to_string())
1928 );
1929 }
1930
1931 #[test]
1932 fn test_basic_regex_patterns() {
1933 let re_str = r"ax*b";
1934 let search_config = SearchConfig {
1935 search_text: re_str,
1936 fixed_strings: false,
1937 match_whole_word: true,
1938 match_case: false,
1939 replacement_text: "NEW",
1940 advanced_regex: false,
1941 };
1942 let parsed = test_helpers::must_parse_search_config(search_config);
1943 assert_eq!(
1944 replacement_if_match("foo axxxxb bar", &parsed.search, &parsed.replace),
1945 Some("foo NEW bar".to_string())
1946 );
1947 let search_config = SearchConfig {
1948 search_text: re_str,
1949 fixed_strings: false,
1950 match_whole_word: true,
1951 match_case: false,
1952 replacement_text: "NEW",
1953 advanced_regex: false,
1954 };
1955 let parsed = test_helpers::must_parse_search_config(search_config);
1956 assert_eq!(
1957 replacement_if_match("fooaxxxxb bar", &parsed.search, &parsed.replace),
1958 None
1959 );
1960 }
1961
1962 #[test]
1963 fn test_patterns_with_spaces() {
1964 let re_str = r"hel+o world";
1965 let search_config = SearchConfig {
1966 search_text: re_str,
1967 fixed_strings: false,
1968 match_whole_word: true,
1969 match_case: false,
1970 replacement_text: "hi earth",
1971 advanced_regex: false,
1972 };
1973 let parsed = test_helpers::must_parse_search_config(search_config);
1974 assert_eq!(
1975 replacement_if_match("say hello world!", &parsed.search, &parsed.replace),
1976 Some("say hi earth!".to_string())
1977 );
1978 let search_config = SearchConfig {
1979 search_text: re_str,
1980 fixed_strings: false,
1981 match_whole_word: true,
1982 match_case: false,
1983 replacement_text: "hi earth",
1984 advanced_regex: false,
1985 };
1986 let parsed = test_helpers::must_parse_search_config(search_config);
1987 assert_eq!(
1988 replacement_if_match("helloworld", &parsed.search, &parsed.replace),
1989 None
1990 );
1991 }
1992
1993 #[test]
1994 fn test_multiple_matches() {
1995 let re_str = r"a+b+";
1996 let search_config = SearchConfig {
1997 search_text: re_str,
1998 fixed_strings: false,
1999 match_whole_word: true,
2000 match_case: false,
2001 replacement_text: "X",
2002 advanced_regex: false,
2003 };
2004 let parsed = test_helpers::must_parse_search_config(search_config);
2005 assert_eq!(
2006 replacement_if_match("foo aab abb", &parsed.search, &parsed.replace),
2007 Some("foo X X".to_string())
2008 );
2009 let search_config = SearchConfig {
2010 search_text: re_str,
2011 fixed_strings: false,
2012 match_whole_word: true,
2013 match_case: false,
2014 replacement_text: "X",
2015 advanced_regex: false,
2016 };
2017 let parsed = test_helpers::must_parse_search_config(search_config);
2018 assert_eq!(
2019 replacement_if_match("ab abaab abb", &parsed.search, &parsed.replace),
2020 Some("X abaab X".to_string())
2021 );
2022 let search_config = SearchConfig {
2023 search_text: re_str,
2024 fixed_strings: false,
2025 match_whole_word: true,
2026 match_case: false,
2027 replacement_text: "X",
2028 advanced_regex: false,
2029 };
2030 let parsed = test_helpers::must_parse_search_config(search_config);
2031 assert_eq!(
2032 replacement_if_match("ababaababb", &parsed.search, &parsed.replace),
2033 None
2034 );
2035 let search_config = SearchConfig {
2036 search_text: re_str,
2037 fixed_strings: false,
2038 match_whole_word: true,
2039 match_case: false,
2040 replacement_text: "X",
2041 advanced_regex: false,
2042 };
2043 let parsed = test_helpers::must_parse_search_config(search_config);
2044 assert_eq!(
2045 replacement_if_match("ab ab aab abb", &parsed.search, &parsed.replace),
2046 Some("X X X X".to_string())
2047 );
2048 }
2049
2050 #[test]
2051 fn test_boundary_cases() {
2052 let re_str = r"foo\s*bar";
2053 let search_config = SearchConfig {
2055 search_text: re_str,
2056 fixed_strings: false,
2057 match_whole_word: true,
2058 match_case: false,
2059 replacement_text: "TEST",
2060 advanced_regex: false,
2061 };
2062 let parsed = test_helpers::must_parse_search_config(search_config);
2063 assert_eq!(
2064 replacement_if_match("foo bar baz", &parsed.search, &parsed.replace),
2065 Some("TEST baz".to_string())
2066 );
2067 let search_config = SearchConfig {
2069 search_text: re_str,
2070 fixed_strings: false,
2071 match_whole_word: true,
2072 match_case: false,
2073 replacement_text: "TEST",
2074 advanced_regex: false,
2075 };
2076 let parsed = test_helpers::must_parse_search_config(search_config);
2077 assert_eq!(
2078 replacement_if_match("baz foo bar", &parsed.search, &parsed.replace),
2079 Some("baz TEST".to_string())
2080 );
2081 let search_config = SearchConfig {
2083 search_text: re_str,
2084 fixed_strings: false,
2085 match_whole_word: true,
2086 match_case: false,
2087 replacement_text: "TEST",
2088 advanced_regex: false,
2089 };
2090 let parsed = test_helpers::must_parse_search_config(search_config);
2091 assert_eq!(
2092 replacement_if_match("a (?( foo bar)", &parsed.search, &parsed.replace),
2093 Some("a (?( TEST)".to_string())
2094 );
2095 }
2096
2097 #[test]
2098 fn test_with_punctuation() {
2099 let re_str = r"a\d+b";
2100 let search_config = SearchConfig {
2101 search_text: re_str,
2102 fixed_strings: false,
2103 match_whole_word: true,
2104 match_case: false,
2105 replacement_text: "X",
2106 advanced_regex: false,
2107 };
2108 let parsed = test_helpers::must_parse_search_config(search_config);
2109 assert_eq!(
2110 replacement_if_match("(a42b)", &parsed.search, &parsed.replace),
2111 Some("(X)".to_string())
2112 );
2113 let search_config = SearchConfig {
2114 search_text: re_str,
2115 fixed_strings: false,
2116 match_whole_word: true,
2117 match_case: false,
2118 replacement_text: "X",
2119 advanced_regex: false,
2120 };
2121 let parsed = test_helpers::must_parse_search_config(search_config);
2122 assert_eq!(
2123 replacement_if_match("foo.a123b!bar", &parsed.search, &parsed.replace),
2124 Some("foo.X!bar".to_string())
2125 );
2126 }
2127
2128 #[test]
2129 fn test_complex_patterns() {
2130 let re_str = r"[a-z]+\d+[a-z]+";
2131 let search_config = SearchConfig {
2132 search_text: re_str,
2133 fixed_strings: false,
2134 match_whole_word: true,
2135 match_case: false,
2136 replacement_text: "NEW",
2137 advanced_regex: false,
2138 };
2139 let parsed = test_helpers::must_parse_search_config(search_config);
2140 assert_eq!(
2141 replacement_if_match("test9 abc123def 8xyz", &parsed.search, &parsed.replace),
2142 Some("test9 NEW 8xyz".to_string())
2143 );
2144 let search_config = SearchConfig {
2145 search_text: re_str,
2146 fixed_strings: false,
2147 match_whole_word: true,
2148 match_case: false,
2149 replacement_text: "NEW",
2150 advanced_regex: false,
2151 };
2152 let parsed = test_helpers::must_parse_search_config(search_config);
2153 assert_eq!(
2154 replacement_if_match("test9abc123def8xyz", &parsed.search, &parsed.replace),
2155 None
2156 );
2157 }
2158
2159 #[test]
2160 fn test_optional_patterns() {
2161 let re_str = r"colou?r";
2162 let search_config = SearchConfig {
2163 search_text: re_str,
2164 fixed_strings: false,
2165 match_whole_word: true,
2166 match_case: false,
2167 replacement_text: "X",
2168 advanced_regex: false,
2169 };
2170 let parsed = test_helpers::must_parse_search_config(search_config);
2171 assert_eq!(
2172 replacement_if_match("my color and colour", &parsed.search, &parsed.replace),
2173 Some("my X and X".to_string())
2174 );
2175 }
2176
2177 #[test]
2178 fn test_empty_haystack() {
2179 let re_str = r"test";
2180 let search_config = SearchConfig {
2181 search_text: re_str,
2182 fixed_strings: false,
2183 match_whole_word: true,
2184 match_case: false,
2185 replacement_text: "NEW",
2186 advanced_regex: false,
2187 };
2188 let parsed = test_helpers::must_parse_search_config(search_config);
2189 assert_eq!(
2190 replacement_if_match("", &parsed.search, &parsed.replace),
2191 None
2192 );
2193 }
2194
2195 #[test]
2196 fn test_empty_search_regex() {
2197 let re_str = r"";
2198 let search_config = SearchConfig {
2199 search_text: re_str,
2200 fixed_strings: false,
2201 match_whole_word: true,
2202 match_case: false,
2203 replacement_text: "NEW",
2204 advanced_regex: false,
2205 };
2206 let parsed = test_helpers::must_parse_search_config(search_config);
2207 assert_eq!(
2208 replacement_if_match("search", &parsed.search, &parsed.replace),
2209 None
2210 );
2211 }
2212
2213 #[test]
2214 fn test_single_char() {
2215 let re_str = r"a";
2216 let search_config = SearchConfig {
2217 search_text: re_str,
2218 fixed_strings: false,
2219 match_whole_word: true,
2220 match_case: false,
2221 replacement_text: "X",
2222 advanced_regex: false,
2223 };
2224 let parsed = test_helpers::must_parse_search_config(search_config);
2225 assert_eq!(
2226 replacement_if_match("b a c", &parsed.search, &parsed.replace),
2227 Some("b X c".to_string())
2228 );
2229 let search_config = SearchConfig {
2230 search_text: re_str,
2231 fixed_strings: false,
2232 match_whole_word: true,
2233 match_case: false,
2234 replacement_text: "X",
2235 advanced_regex: false,
2236 };
2237 let parsed = test_helpers::must_parse_search_config(search_config);
2238 assert_eq!(
2239 replacement_if_match("bac", &parsed.search, &parsed.replace),
2240 None
2241 );
2242 }
2243
2244 #[test]
2245 fn test_escaped_chars() {
2246 let re_str = r"\(\d+\)";
2247 let search_config = SearchConfig {
2248 search_text: re_str,
2249 fixed_strings: false,
2250 match_whole_word: true,
2251 match_case: false,
2252 replacement_text: "X",
2253 advanced_regex: false,
2254 };
2255 let parsed = test_helpers::must_parse_search_config(search_config);
2256 assert_eq!(
2257 replacement_if_match("test (123) foo", &parsed.search, &parsed.replace),
2258 Some("test X foo".to_string())
2259 );
2260 }
2261
2262 #[test]
2263 fn test_with_unicode() {
2264 let re_str = r"λ\d+";
2265 let search_config = SearchConfig {
2266 search_text: re_str,
2267 fixed_strings: false,
2268 match_whole_word: true,
2269 match_case: false,
2270 replacement_text: "X",
2271 advanced_regex: false,
2272 };
2273 let parsed = test_helpers::must_parse_search_config(search_config);
2274 assert_eq!(
2275 replacement_if_match("calc λ123 β", &parsed.search, &parsed.replace),
2276 Some("calc X β".to_string())
2277 );
2278 let search_config = SearchConfig {
2279 search_text: re_str,
2280 fixed_strings: false,
2281 match_whole_word: true,
2282 match_case: false,
2283 replacement_text: "X",
2284 advanced_regex: false,
2285 };
2286 let parsed = test_helpers::must_parse_search_config(search_config);
2287 assert_eq!(
2288 replacement_if_match("calcλ123", &parsed.search, &parsed.replace),
2289 None
2290 );
2291 }
2292 }
2293}