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 /// List of matched strings, so they can be shared between matches to reduce ram usage.
49 matches_strings: Vec<String>,
50}
51
52/// This struct represents a match on a piece of text within a Text PackedFile.
53#[derive(Debug, Clone, Eq, PartialEq, Getters, MutGetters, Serialize, Deserialize)]
54#[getset(get = "pub", get_mut = "pub")]
55pub struct TextMatch {
56
57 /// Row of the first character of the match.
58 row: u64,
59
60 /// Byte where the match starts.
61 start: usize,
62
63 /// Byte where the match ends.
64 end: usize,
65
66 /// Index of the line of text containing the match.
67 text_index: usize,
68}
69
70//-------------------------------------------------------------------------------//
71// Implementations
72//-------------------------------------------------------------------------------//
73
74impl Searchable for Text {
75 type SearchMatches = TextMatches;
76
77 fn search(&self, file_path: &str, pattern: &str, case_sensitive: bool, matching_mode: &MatchingMode) -> TextMatches {
78 let mut matches = TextMatches::new(file_path);
79
80 for (row, data) in self.contents().lines().enumerate() {
81 let mut added = false;
82
83 match matching_mode {
84 MatchingMode::Regex(regex) => {
85 for match_data in regex.find_iter(data) {
86 matches.matches.push(
87 TextMatch::new(
88 row as u64,
89 match_data.start(),
90 match_data.end(),
91 if !added {
92 matches.matches_strings.len()
93 } else {
94 matches.matches_strings.len() - 1
95 },
96 )
97 );
98
99 if !added {
100 matches.matches_strings.push(data.to_owned());
101 added = true;
102 }
103 }
104 }
105
106 // If we're searching a pattern, we just check every text PackedFile, line by line.
107 MatchingMode::Pattern(regex) => {
108 for (start, end, _) in &find_in_string(data, pattern, case_sensitive, regex) {
109 matches.matches.push(
110 TextMatch::new(
111 row as u64,
112 *start,
113 *end,
114 if !added {
115 matches.matches_strings.len()
116 } else {
117 matches.matches_strings.len() - 1
118 },
119 )
120 );
121
122 if !added {
123 matches.matches_strings.push(data.to_owned());
124 added = true;
125 }
126 }
127 }
128 }
129 }
130
131 matches
132 }
133}
134
135impl Replaceable for Text {
136
137 fn replace(&mut self, pattern: &str, replace_pattern: &str, case_sensitive: bool, matching_mode: &MatchingMode, search_matches: &TextMatches) -> bool {
138 let mut edited = false;
139
140 // NOTE: Due to changes in index positions, we need to do this in reverse.
141 // Otherwise we may cause one edit to generate invalid indexes for the next matches.
142 for search_match in search_matches.matches().iter().rev() {
143 edited |= search_match.replace(pattern, replace_pattern, case_sensitive, matching_mode, self.contents_mut());
144 }
145
146 edited
147 }
148}
149
150impl TextMatches {
151
152 /// This function creates a new `TextMatches` for the provided path.
153 pub fn new(path: &str) -> Self {
154 Self {
155 path: path.to_owned(),
156 matches: vec![],
157 matches_strings: vec![],
158 source: SearchSource::default(),
159 container_name: String::new(),
160 }
161 }
162}
163
164impl TextMatch {
165
166 /// This function creates a new `TextMatch` with the provided data.
167 pub fn new(row: u64, start: usize, end: usize, text_index: usize) -> Self {
168 Self {
169 row,
170 start,
171 end,
172 text_index,
173 }
174 }
175
176 /// This function replaces all the matches in the provided text.
177 fn replace(&self, pattern: &str, replace_pattern: &str, case_sensitive: bool, matching_mode: &MatchingMode, data: &mut String) -> bool {
178 let mut edited = false;
179
180 let new_data = data.lines()
181 .enumerate()
182 .map(|(row, line)| {
183 if self.row == row as u64 {
184 let (previous_data, mut current_data) = (line, line.to_owned());
185 edited |= replace_match_string(pattern, replace_pattern, case_sensitive, matching_mode, self.start, self.end, previous_data, &mut current_data);
186 current_data
187 } else {
188 line.to_owned()
189 }
190 }).join("\n");
191
192 if edited {
193 *data = new_data;
194 }
195
196 edited
197 }
198}