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
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}