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}