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