Skip to main content

par_term/snippets/
mod.rs

1//! Snippet variable substitution and insertion engine.
2//!
3//! This module provides:
4//! - Variable substitution for snippets (built-in and custom variables)
5//! - Session variable access (hostname, username, path, job, etc.)
6//! - Snippet text processing with \(variable) syntax
7//! - Integration with the terminal for text insertion
8
9use crate::badge::SessionVariables;
10use crate::config::snippets::BuiltInVariable;
11use regex::Regex;
12use std::collections::HashMap;
13
14/// Error type for snippet substitution failures.
15#[derive(Debug, Clone)]
16pub enum SubstitutionError {
17    /// Variable name is empty or invalid
18    InvalidVariable(String),
19    /// Variable is not defined (built-in or custom)
20    UndefinedVariable(String),
21}
22
23impl std::fmt::Display for SubstitutionError {
24    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
25        match self {
26            Self::InvalidVariable(name) => write!(f, "Invalid variable name: {}", name),
27            Self::UndefinedVariable(name) => write!(f, "Undefined variable: {}", name),
28        }
29    }
30}
31
32impl std::error::Error for SubstitutionError {}
33
34/// Result type for substitution operations.
35pub type SubstitutionResult<T> = Result<T, SubstitutionError>;
36
37/// Variable substitution engine for snippets.
38///
39/// Substitutes variables in the format \(variable_name) with their values.
40/// Supports both built-in variables (date, time, hostname, etc.) and custom
41/// variables defined per-snippet.
42pub struct VariableSubstitutor {
43    /// Regex pattern to match \(variable) syntax
44    pattern: Regex,
45}
46
47impl VariableSubstitutor {
48    /// Create a new variable substitutor.
49    pub fn new() -> Self {
50        // Match \(variable_name) where variable_name is alphanumeric + underscore + dot
51        // Dot allows session.hostname style variables
52        let pattern = Regex::new(r"\\\(([a-zA-Z_][a-zA-Z0-9_.]*)\)").unwrap();
53
54        Self { pattern }
55    }
56
57    /// Substitute all variables in the given text.
58    ///
59    /// # Arguments
60    /// * `text` - The text containing variables to substitute
61    /// * `custom_vars` - Custom variables defined for this snippet
62    ///
63    /// # Returns
64    /// The text with all variables replaced by their values.
65    pub fn substitute(
66        &self,
67        text: &str,
68        custom_vars: &HashMap<String, String>,
69    ) -> SubstitutionResult<String> {
70        self.substitute_with_session(text, custom_vars, None)
71    }
72
73    /// Substitute all variables in the given text, including session variables.
74    ///
75    /// # Arguments
76    /// * `text` - The text containing variables to substitute
77    /// * `custom_vars` - Custom variables defined for this snippet
78    /// * `session_vars` - Optional session variables (hostname, path, job, etc.)
79    ///
80    /// # Returns
81    /// The text with all variables replaced by their values.
82    pub fn substitute_with_session(
83        &self,
84        text: &str,
85        custom_vars: &HashMap<String, String>,
86        session_vars: Option<&SessionVariables>,
87    ) -> SubstitutionResult<String> {
88        let mut result = text.to_string();
89
90        // Find all variable placeholders
91        for cap in self.pattern.captures_iter(text) {
92            let full_match = cap.get(0).unwrap().as_str();
93            let var_name = cap.get(1).unwrap().as_str();
94
95            // Resolve the variable value
96            let value = self.resolve_variable_with_session(var_name, custom_vars, session_vars)?;
97
98            // Replace the placeholder with the value
99            result = result.replace(full_match, &value);
100        }
101
102        Ok(result)
103    }
104
105    /// Resolve a single variable to its value (without session variables).
106    #[allow(dead_code)]
107    fn resolve_variable(
108        &self,
109        name: &str,
110        custom_vars: &HashMap<String, String>,
111    ) -> SubstitutionResult<String> {
112        self.resolve_variable_with_session(name, custom_vars, None)
113    }
114
115    /// Resolve a single variable to its value, including session variables.
116    fn resolve_variable_with_session(
117        &self,
118        name: &str,
119        custom_vars: &HashMap<String, String>,
120        session_vars: Option<&SessionVariables>,
121    ) -> SubstitutionResult<String> {
122        // Check custom variables first (highest priority)
123        if let Some(value) = custom_vars.get(name) {
124            return Ok(value.clone());
125        }
126
127        // Check session variables (second priority)
128        if let Some(value) = session_vars.and_then(|session| session.get(name)) {
129            return Ok(value);
130        }
131
132        // Check built-in variables (third priority)
133        if let Some(builtin) = BuiltInVariable::parse(name) {
134            return Ok(builtin.resolve());
135        }
136
137        // Variable not found
138        Err(SubstitutionError::UndefinedVariable(name.to_string()))
139    }
140
141    /// Check if text contains any variables.
142    pub fn has_variables(&self, text: &str) -> bool {
143        self.pattern.is_match(text)
144    }
145
146    /// Extract all variable names from the text.
147    pub fn extract_variables(&self, text: &str) -> Vec<String> {
148        self.pattern
149            .captures_iter(text)
150            .map(|cap| cap.get(1).unwrap().as_str().to_string())
151            .collect()
152    }
153}
154
155impl Default for VariableSubstitutor {
156    fn default() -> Self {
157        Self::new()
158    }
159}
160
161#[cfg(test)]
162mod tests {
163    use super::*;
164
165    #[test]
166    fn test_substitute_builtin_variables() {
167        let substitutor = VariableSubstitutor::new();
168        let custom_vars = HashMap::new();
169
170        // Test date variable (should produce something like YYYY-MM-DD)
171        let result = substitutor
172            .substitute("Today is \\(date)", &custom_vars)
173            .unwrap();
174        assert!(result.starts_with("Today is "));
175        assert!(!result.contains("\\(date)"));
176
177        // Test user variable
178        let result = substitutor
179            .substitute("User: \\(user)", &custom_vars)
180            .unwrap();
181        assert!(result.starts_with("User: "));
182        assert!(!result.contains("\\(user)"));
183    }
184
185    #[test]
186    fn test_substitute_custom_variables() {
187        let substitutor = VariableSubstitutor::new();
188        let mut custom_vars = HashMap::new();
189        custom_vars.insert("name".to_string(), "Alice".to_string());
190        custom_vars.insert("project".to_string(), "par-term".to_string());
191
192        let result = substitutor
193            .substitute("Hello \\(name), welcome to \\(project)!", &custom_vars)
194            .unwrap();
195
196        assert_eq!(result, "Hello Alice, welcome to par-term!");
197    }
198
199    #[test]
200    fn test_substitute_mixed_variables() {
201        let substitutor = VariableSubstitutor::new();
202        let mut custom_vars = HashMap::new();
203        custom_vars.insert("greeting".to_string(), "Hello".to_string());
204
205        let result = substitutor
206            .substitute("\\(greeting) \\(user), today is \\(date)", &custom_vars)
207            .unwrap();
208
209        assert!(result.starts_with("Hello "));
210        assert!(!result.contains("\\("));
211    }
212
213    #[test]
214    fn test_undefined_variable() {
215        let substitutor = VariableSubstitutor::new();
216        let custom_vars = HashMap::new();
217
218        let result = substitutor.substitute("Value: \\(undefined)", &custom_vars);
219
220        assert!(result.is_err());
221        match result.unwrap_err() {
222            SubstitutionError::UndefinedVariable(name) => assert_eq!(name, "undefined"),
223            _ => panic!("Expected UndefinedVariable error"),
224        }
225    }
226
227    #[test]
228    fn test_has_variables() {
229        let substitutor = VariableSubstitutor::new();
230
231        assert!(substitutor.has_variables("Hello \\(user)"));
232        assert!(!substitutor.has_variables("Hello world"));
233    }
234
235    #[test]
236    fn test_extract_variables() {
237        let substitutor = VariableSubstitutor::new();
238
239        let vars = substitutor.extract_variables("Hello \\(user), today is \\(date)");
240        assert_eq!(vars, vec!["user", "date"]);
241    }
242
243    #[test]
244    fn test_no_variables() {
245        let substitutor = VariableSubstitutor::new();
246        let custom_vars = HashMap::new();
247
248        let result = substitutor
249            .substitute("Just plain text with no variables", &custom_vars)
250            .unwrap();
251
252        assert_eq!(result, "Just plain text with no variables");
253    }
254
255    #[test]
256    fn test_empty_custom_vars() {
257        let substitutor = VariableSubstitutor::new();
258        let custom_vars = HashMap::new();
259
260        let result = substitutor
261            .substitute("User: \\(user), Path: \\(path)", &custom_vars)
262            .unwrap();
263
264        // Should successfully substitute built-in variables
265        assert!(result.contains("User:"));
266        assert!(result.contains("Path:"));
267        assert!(!result.contains("\\("));
268    }
269
270    #[test]
271    fn test_duplicate_variables() {
272        let substitutor = VariableSubstitutor::new();
273        let mut custom_vars = HashMap::new();
274        custom_vars.insert("name".to_string(), "Alice".to_string());
275
276        let result = substitutor
277            .substitute("\\(name) and \\(name) again", &custom_vars)
278            .unwrap();
279
280        assert_eq!(result, "Alice and Alice again");
281    }
282
283    #[test]
284    fn test_escaped_backslash() {
285        let substitutor = VariableSubstitutor::new();
286        let custom_vars = HashMap::new();
287
288        // Test that \( is the variable syntax, not just an escaped paren
289        let result = substitutor
290            .substitute("Use \\(user) for the username", &custom_vars)
291            .unwrap();
292
293        assert!(!result.contains("\\("));
294        assert!(!result.contains("\\)"));
295    }
296}