rpfm_extensions/search/text.rs
1//---------------------------------------------------------------------------//
2// Copyright (c) 2017-2026 Ismael Gutiérrez González. All rights reserved.
3//
4// This file is part of the Rusted PackFile Manager (RPFM) project,
5// which can be found here: https://github.com/Frodo45127/rpfm.
6//
7// This file is licensed under the MIT license, which can be found here:
8// https://github.com/Frodo45127/rpfm/blob/master/LICENSE.
9//---------------------------------------------------------------------------//
10
11/*!
12Module with all the code related to the `TextMatches`.
13
14This module contains the code needed to get text matches from a `GlobalSearch`.
15!*/
16
17use getset::{Getters, MutGetters, Setters};
18use itertools::Itertools;
19use serde_derive::{Deserialize, Serialize};
20
21use rpfm_lib::files::text::Text;
22
23use super::{find_in_string, MatchingMode, Replaceable, SearchSource, Searchable, replace_match_string};
24
25//-------------------------------------------------------------------------------//
26// Enums & Structs
27//-------------------------------------------------------------------------------//
28
29/// This struct represents all the matches of the global search within a text PackedFile.
30#[derive(Debug, Clone, Getters, MutGetters, Setters, Serialize, Deserialize)]
31#[getset(get = "pub", get_mut = "pub", set = "pub")]
32pub struct TextMatches {
33
34 /// The path of the file.
35 path: String,
36
37 /// The search source that produced these matches.
38 #[serde(default)]
39 source: SearchSource,
40
41 /// The container name (pack file name) this file belongs to.
42 #[serde(default)]
43 container_name: String,
44
45 /// The list of matches within the file.
46 matches: Vec<TextMatch>,
47}
48
49/// This struct represents a match on a piece of text within a Text PackedFile.
50#[derive(Debug, Clone, Eq, PartialEq, Getters, MutGetters, Serialize, Deserialize)]
51#[getset(get = "pub", get_mut = "pub")]
52pub struct TextMatch {
53
54 /// Row of the first character of the match.
55 row: u64,
56
57 /// Byte where the match starts.
58 start: usize,
59
60 /// Byte where the match ends.
61 end: usize,
62
63 /// Line of text containing the match.
64 text: String,
65}
66
67//-------------------------------------------------------------------------------//
68// Implementations
69//-------------------------------------------------------------------------------//
70
71impl Searchable for Text {
72 type SearchMatches = TextMatches;
73
74 fn search(&self, file_path: &str, pattern: &str, case_sensitive: bool, matching_mode: &MatchingMode) -> TextMatches {
75 let mut matches = TextMatches::new(file_path);
76
77 for (row, data) in self.contents().lines().enumerate() {
78 match matching_mode {
79 MatchingMode::Regex(regex) => {
80 for match_data in regex.find_iter(data) {
81 matches.matches.push(
82 TextMatch::new(
83 row as u64,
84 match_data.start(),
85 match_data.end(),
86 data.to_owned()
87 )
88 );
89 }
90 }
91
92 // If we're searching a pattern, we just check every text PackedFile, line by line.
93 MatchingMode::Pattern(regex) => {
94 for (start, end, _) in &find_in_string(data, pattern, case_sensitive, regex) {
95 matches.matches.push(
96 TextMatch::new(
97 row as u64,
98 *start,
99 *end,
100 data.to_owned()
101 )
102 );
103 }
104 }
105 }
106 }
107
108 matches
109 }
110}
111
112impl Replaceable for Text {
113
114 fn replace(&mut self, pattern: &str, replace_pattern: &str, case_sensitive: bool, matching_mode: &MatchingMode, search_matches: &TextMatches) -> bool {
115 let mut edited = false;
116
117 // NOTE: Due to changes in index positions, we need to do this in reverse.
118 // Otherwise we may cause one edit to generate invalid indexes for the next matches.
119 for search_match in search_matches.matches().iter().rev() {
120 edited |= search_match.replace(pattern, replace_pattern, case_sensitive, matching_mode, self.contents_mut());
121 }
122
123 edited
124 }
125}
126
127impl TextMatches {
128
129 /// This function creates a new `TextMatches` for the provided path.
130 pub fn new(path: &str) -> Self {
131 Self {
132 path: path.to_owned(),
133 matches: vec![],
134 source: SearchSource::default(),
135 container_name: String::new(),
136 }
137 }
138}
139
140impl TextMatch {
141
142 /// This function creates a new `TextMatch` with the provided data.
143 pub fn new(row: u64, start: usize, end: usize, text: String) -> Self {
144 Self {
145 row,
146 start,
147 end,
148 text,
149 }
150 }
151
152 /// This function replaces all the matches in the provided text.
153 fn replace(&self, pattern: &str, replace_pattern: &str, case_sensitive: bool, matching_mode: &MatchingMode, data: &mut String) -> bool {
154 let mut edited = false;
155
156 let new_data = data.lines()
157 .enumerate()
158 .map(|(row, line)| {
159 if self.row == row as u64 {
160 let (previous_data, mut current_data) = (line, line.to_owned());
161 edited |= replace_match_string(pattern, replace_pattern, case_sensitive, matching_mode, self.start, self.end, previous_data, &mut current_data);
162 current_data
163 } else {
164 line.to_owned()
165 }
166 }).join("\n");
167
168 if edited {
169 *data = new_data;
170 }
171
172 edited
173 }
174}