Skip to main content

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}