Skip to main content

flag_rs/
completion.rs

1//! Dynamic shell completion support
2//!
3//! This module provides the infrastructure for dynamic completions that are
4//! computed at runtime when the user presses TAB, rather than being hardcoded
5//! at compile time.
6
7use crate::active_help::ActiveHelp;
8use crate::context::Context;
9use crate::error::Result;
10
11/// Result returned by completion functions
12///
13/// `CompletionResult` contains completion suggestions along with optional
14/// descriptions for each suggestion. This is used by the shell completion
15/// system to provide helpful hints to users.
16///
17/// # Examples
18///
19/// ```
20/// use flag_rs::completion::CompletionResult;
21///
22/// let completions = CompletionResult::new()
23///     .add("create")
24///     .add_with_description("delete", "Remove a resource")
25///     .add_with_description("list", "Show all resources")
26///     .extend(vec!["get".to_string(), "update".to_string()]);
27///
28/// assert_eq!(completions.values.len(), 5);
29/// assert_eq!(completions.values[1], "delete");
30/// assert_eq!(completions.descriptions[1], "Remove a resource");
31/// ```
32#[derive(Clone, Debug)]
33pub struct CompletionResult {
34    /// The completion values to suggest
35    pub values: Vec<String>,
36    /// Optional descriptions for each value
37    pub descriptions: Vec<String>,
38    /// `ActiveHelp` messages to display
39    pub active_help: Vec<ActiveHelp>,
40}
41
42impl CompletionResult {
43    /// Creates a new empty completion result
44    #[must_use]
45    pub fn new() -> Self {
46        Self {
47            values: Vec::new(),
48            descriptions: Vec::new(),
49            active_help: Vec::new(),
50        }
51    }
52
53    /// Adds a completion value without a description
54    ///
55    /// # Arguments
56    ///
57    /// * `value` - The completion value to add
58    ///
59    /// # Examples
60    ///
61    /// ```
62    /// use flag_rs::completion::CompletionResult;
63    ///
64    /// let result = CompletionResult::new()
65    ///     .add("option1")
66    ///     .add("option2");
67    /// ```
68    #[allow(clippy::should_implement_trait)]
69    #[must_use]
70    pub fn add(mut self, value: impl Into<String>) -> Self {
71        self.values.push(value.into());
72        self.descriptions.push(String::new());
73        self
74    }
75
76    /// Adds a completion value with a description
77    ///
78    /// # Arguments
79    ///
80    /// * `value` - The completion value to add
81    /// * `desc` - A description of what this value represents
82    ///
83    /// # Examples
84    ///
85    /// ```
86    /// use flag_rs::completion::CompletionResult;
87    ///
88    /// let result = CompletionResult::new()
89    ///     .add_with_description("--verbose", "Enable verbose output")
90    ///     .add_with_description("--quiet", "Suppress output");
91    /// ```
92    #[must_use]
93    pub fn add_with_description(
94        mut self,
95        value: impl Into<String>,
96        desc: impl Into<String>,
97    ) -> Self {
98        self.values.push(value.into());
99        self.descriptions.push(desc.into());
100        self
101    }
102
103    /// Adds multiple completion values without descriptions
104    ///
105    /// # Arguments
106    ///
107    /// * `values` - An iterator of completion values
108    ///
109    /// # Examples
110    ///
111    /// ```
112    /// use flag_rs::completion::CompletionResult;
113    ///
114    /// let options = vec!["opt1".to_string(), "opt2".to_string()];
115    /// let result = CompletionResult::new().extend(options);
116    /// ```
117    #[must_use]
118    pub fn extend<I: IntoIterator<Item = String>>(mut self, values: I) -> Self {
119        for value in values {
120            self.values.push(value);
121            self.descriptions.push(String::new());
122        }
123        self
124    }
125
126    /// Adds an `ActiveHelp` message
127    ///
128    /// # Arguments
129    ///
130    /// * `help` - The `ActiveHelp` message to add
131    ///
132    /// # Examples
133    ///
134    /// ```
135    /// use flag_rs::completion::CompletionResult;
136    /// use flag_rs::active_help::ActiveHelp;
137    ///
138    /// let result = CompletionResult::new()
139    ///     .add_help(ActiveHelp::new("Press TAB to see available options"));
140    /// ```
141    #[must_use]
142    pub fn add_help(mut self, help: ActiveHelp) -> Self {
143        self.active_help.push(help);
144        self
145    }
146
147    /// Adds an `ActiveHelp` message from a string
148    ///
149    /// # Arguments
150    ///
151    /// * `message` - The help message text
152    ///
153    /// # Examples
154    ///
155    /// ```
156    /// use flag_rs::completion::CompletionResult;
157    ///
158    /// let result = CompletionResult::new()
159    ///     .add_help_text("Use -n <namespace> to filter results");
160    /// ```
161    #[must_use]
162    pub fn add_help_text<S: Into<String>>(mut self, message: S) -> Self {
163        self.active_help.push(ActiveHelp::new(message));
164        self
165    }
166
167    /// Adds a conditional `ActiveHelp` message
168    ///
169    /// # Arguments
170    ///
171    /// * `message` - The help message text
172    /// * `condition` - Function that determines if help should be shown
173    ///
174    /// # Examples
175    ///
176    /// ```
177    /// use flag_rs::completion::CompletionResult;
178    ///
179    /// let result = CompletionResult::new()
180    ///     .add_conditional_help(
181    ///         "Tip: Use --format json for machine-readable output",
182    ///         |ctx| ctx.flag("format").is_none()
183    ///     );
184    /// ```
185    #[must_use]
186    pub fn add_conditional_help<S, F>(mut self, message: S, condition: F) -> Self
187    where
188        S: Into<String>,
189        F: Fn(&Context) -> bool + Send + Sync + 'static,
190    {
191        self.active_help
192            .push(ActiveHelp::with_condition(message, condition));
193        self
194    }
195}
196
197impl Default for CompletionResult {
198    fn default() -> Self {
199        Self::new()
200    }
201}
202
203/// Type alias for completion functions
204///
205/// Completion functions are called when the user presses TAB to get suggestions.
206/// They receive the current context and the partial text being completed.
207///
208/// # Arguments
209///
210/// * `&Context` - The current command context with flags and arguments
211/// * `&str` - The partial text being completed
212///
213/// # Returns
214///
215/// Returns a `Result<CompletionResult>` with the suggested completions
216///
217/// # Examples
218///
219/// ```
220/// use flag_rs::completion::{CompletionFunc, CompletionResult};
221/// use flag_rs::context::Context;
222/// use flag_rs::error::Result;
223///
224/// // A completion function that suggests file names
225/// let file_completer: CompletionFunc = Box::new(|_ctx, partial| {
226///     Ok(CompletionResult::new()
227///         .add("file1.txt")
228///         .add("file2.txt")
229///         .add("file3.log"))
230/// });
231///
232/// // A dynamic completion function that uses context
233/// let pod_completer: CompletionFunc = Box::new(|ctx, partial| {
234///     // In a real implementation, this would query the Kubernetes API
235///     let namespace = ctx.flag("namespace")
236///         .map(|s| s.as_str())
237///         .unwrap_or("default");
238///     Ok(CompletionResult::new()
239///         .add_with_description("pod-abc-123", format!("Pod in namespace {}", namespace))
240///         .add_with_description("pod-def-456", format!("Pod in namespace {}", namespace)))
241/// });
242/// ```
243pub type CompletionFunc = Box<dyn Fn(&Context, &str) -> Result<CompletionResult> + Send + Sync>;
244
245#[cfg(test)]
246mod tests {
247    use super::*;
248
249    #[test]
250    fn test_completion_result() {
251        let result = CompletionResult::new()
252            .add("option1")
253            .add_with_description("option2", "Description for option2")
254            .extend(vec!["option3".to_string(), "option4".to_string()]);
255
256        assert_eq!(result.values.len(), 4);
257        assert_eq!(result.descriptions.len(), 4);
258
259        assert_eq!(result.values[0], "option1");
260        assert_eq!(result.descriptions[0], "");
261
262        assert_eq!(result.values[1], "option2");
263        assert_eq!(result.descriptions[1], "Description for option2");
264
265        assert_eq!(result.values[2], "option3");
266        assert_eq!(result.descriptions[2], "");
267    }
268
269    #[test]
270    fn test_completion_result_with_active_help() {
271        let result = CompletionResult::new()
272            .add("option1")
273            .add_help_text("This is a help message")
274            .add_conditional_help("Conditional help", |_| true);
275
276        assert_eq!(result.values.len(), 1);
277        assert_eq!(result.active_help.len(), 2);
278        assert_eq!(result.active_help[0].message, "This is a help message");
279        assert_eq!(result.active_help[1].message, "Conditional help");
280    }
281}