flag_rs/
completion_timeout.rs1use crate::completion::CompletionResult;
7use crate::context::Context;
8use crate::error::{Error, Result};
9use std::sync::{Arc, Mutex};
10use std::thread;
11use std::time::Duration;
12
13pub const DEFAULT_COMPLETION_TIMEOUT: Duration = Duration::from_secs(2);
15
16pub fn with_timeout<F>(
34 f: F,
35 timeout: Duration,
36 ctx: &Context,
37 prefix: &str,
38) -> Result<CompletionResult>
39where
40 F: FnOnce(&Context, &str) -> Result<CompletionResult> + Send + 'static,
41{
42 let result: Arc<Mutex<Option<Result<CompletionResult>>>> = Arc::new(Mutex::new(None));
43 let result_clone = Arc::clone(&result);
44
45 let ctx_clone = Context::new(ctx.args().to_vec());
46 let prefix_clone = prefix.to_string();
47
48 let handle = thread::spawn(move || {
49 let completion_result = f(&ctx_clone, &prefix_clone);
50 if let Ok(mut result_lock) = result_clone.lock() {
51 *result_lock = Some(completion_result);
52 }
53 });
54
55 if matches!(handle.join_timeout(timeout), Ok(())) {
56 result.lock().map_or_else(
58 |_| {
59 Err(Error::Completion(
60 "Failed to access completion result".to_string(),
61 ))
62 },
63 |mut result_lock| {
64 result_lock.take().unwrap_or_else(|| {
65 Err(Error::Completion(
66 "Completion function did not return a result".to_string(),
67 ))
68 })
69 },
70 )
71 } else {
72 let mut partial_result = CompletionResult::new();
74 partial_result = partial_result.add_help_text(
75 "⚠️ Completion timed out - results may be incomplete. Try a more specific prefix.",
76 );
77
78 if let Ok(result_lock) = result.lock()
80 && let Some(Ok(ref partial)) = *result_lock
81 {
82 partial_result = partial_result.merge(partial.clone());
83 }
84
85 Ok(partial_result)
86 }
87}
88
89pub fn make_timeout_completion<F>(
103 timeout: Duration,
104 f: F,
105) -> impl Fn(&Context, &str) -> Result<CompletionResult>
106where
107 F: Fn(&Context, &str) -> Result<CompletionResult> + Clone + Send + 'static,
108{
109 move |ctx: &Context, prefix: &str| {
110 let f_clone = f.clone();
111 with_timeout(move |c, p| f_clone(c, p), timeout, ctx, prefix)
112 }
113}
114
115trait JoinHandleExt<T>: Sized {
117 fn join_timeout(self, timeout: Duration) -> std::result::Result<T, Self>;
118}
119
120impl<T> JoinHandleExt<T> for thread::JoinHandle<T> {
121 fn join_timeout(self, timeout: Duration) -> std::result::Result<T, Self> {
122 let start = std::time::Instant::now();
123
124 loop {
125 if self.is_finished() {
126 return self.join().map_or_else(|_| panic!("Thread panicked"), Ok);
127 }
128
129 if start.elapsed() >= timeout {
130 return Err(self);
131 }
132
133 thread::sleep(Duration::from_millis(10));
134 }
135 }
136}
137
138impl CompletionResult {
139 #[must_use]
141 pub fn merge(mut self, other: Self) -> Self {
142 self.values.extend(other.values);
143 self.descriptions.extend(other.descriptions);
144 self.active_help.extend(other.active_help);
145 self
146 }
147}
148
149#[cfg(test)]
150mod tests {
151 use super::*;
152 use std::thread;
153 use std::time::Duration;
154
155 #[test]
156 fn test_completion_with_timeout_success() {
157 let ctx = Context::new(vec![]);
158
159 let result = with_timeout(
160 |_ctx, prefix| {
161 Ok(CompletionResult::new()
163 .add("item1")
164 .add("item2")
165 .add(format!("prefix_{prefix}")))
166 },
167 Duration::from_secs(1),
168 &ctx,
169 "test",
170 );
171
172 assert!(result.is_ok());
173 let completion = result.unwrap();
174 assert_eq!(completion.values.len(), 3);
175 assert!(completion.values.contains(&"prefix_test".to_string()));
176 }
177
178 #[test]
179 fn test_completion_with_timeout_exceeded() {
180 let ctx = Context::new(vec![]);
181
182 let result = with_timeout(
183 |_ctx, _prefix| {
184 thread::sleep(Duration::from_secs(2));
186 Ok(CompletionResult::new().add("never_returned"))
187 },
188 Duration::from_millis(100),
189 &ctx,
190 "test",
191 );
192
193 assert!(result.is_ok());
194 let completion = result.unwrap();
195 assert!(!completion.active_help.is_empty());
197 assert!(
198 completion.active_help[0]
199 .message
200 .contains("Completion timed out")
201 );
202 }
203
204 #[test]
205 fn test_make_timeout_completion() {
206 let wrapped = make_timeout_completion(Duration::from_secs(1), |_ctx, prefix| {
207 Ok(CompletionResult::new().add(format!("result_{prefix}")))
208 });
209
210 let ctx = Context::new(vec![]);
211 let result = wrapped(&ctx, "test").unwrap();
212 assert_eq!(result.values.len(), 1);
213 assert_eq!(result.values[0], "result_test");
214 }
215}