1#[cfg(not(feature = "integration-tests"))]
3mod prelude;
4#[cfg(feature = "integration-tests")]
5pub mod prelude;
6
7mod about;
8
9mod token;
10
11use std::fmt::{self, Write};
12use std::iter;
13use std::pin::Pin;
14
15use crate::app::{
16 AppMeta, Autocomplete, AutocompleteSuggestion, CommandMatches, ContextAwareParse, Runnable,
17};
18use initiative_macros::CommandList;
19
20use token::{FuzzyMatch, Token, TokenMatch};
21
22use async_stream::stream;
23use async_trait::async_trait;
24use futures::prelude::*;
25
26pub trait Command {
27 fn token(&self) -> Token;
30
31 fn autocomplete(&self, fuzzy_match: FuzzyMatch, input: &str) -> Option<AutocompleteSuggestion>;
36
37 fn get_priority(&self, token_match: &TokenMatch) -> Option<CommandPriority>;
39
40 #[cfg_attr(feature = "integration-tests", expect(async_fn_in_trait))]
43 async fn run(&self, token_match: TokenMatch, app_meta: &mut AppMeta) -> Result<String, String>;
44
45 fn get_canonical_form_of(&self, token_match: &TokenMatch) -> Option<String>;
47
48 fn parse_autocomplete<'a>(
51 &'a self,
52 input: &'a str,
53 app_meta: &'a AppMeta,
54 ) -> Pin<Box<dyn Stream<Item = AutocompleteSuggestion> + 'a>> {
55 Box::pin(stream! {
56 let token = self.token();
57 for await token_match in token.match_input(input, app_meta) {
58 if !matches!(token_match, FuzzyMatch::Overflow(..)) {
59 if let Some(suggestion) = self.autocomplete(token_match, input) {
60 yield suggestion;
61 }
62 }
63 }
64 })
65 }
66}
67
68#[derive(Clone, CommandList, Debug)]
69enum CommandList {
70 About(about::About),
71}
72
73#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
74pub enum CommandPriority {
75 Canonical,
79
80 Fuzzy,
84}
85
86#[derive(Clone, Debug, Eq, PartialEq)]
88pub struct TransitionalCommand {
89 canonical: String,
90}
91
92impl TransitionalCommand {
93 pub fn new<S>(canonical: S) -> Self
94 where
95 S: AsRef<str>,
96 {
97 Self {
98 canonical: canonical.as_ref().to_string(),
99 }
100 }
101}
102
103#[async_trait(?Send)]
104impl Runnable for TransitionalCommand {
105 async fn run(self, _input: &str, app_meta: &mut AppMeta) -> Result<String, String> {
106 run(&self.canonical, app_meta).await
107 }
108}
109
110#[async_trait(?Send)]
111impl ContextAwareParse for TransitionalCommand {
112 async fn parse_input(input: &str, app_meta: &AppMeta) -> CommandMatches<Self> {
113 let mut command_matches = CommandMatches::default();
114
115 let commands_tokens: Vec<(&CommandList, Token)> = CommandList::get_all()
116 .iter()
117 .map(|c| (c, c.token()))
118 .collect();
119
120 {
121 let mut match_streams = stream::SelectAll::default();
122
123 #[expect(clippy::needless_range_loop)]
125 for i in 0..commands_tokens.len() {
126 match_streams.push(
127 stream::repeat(commands_tokens[i].0)
128 .zip(commands_tokens[i].1.match_input(input, app_meta)),
129 );
130 }
131
132 while let Some((command, fuzzy_match)) = match_streams.next().await {
133 if let FuzzyMatch::Exact(token_match) = fuzzy_match {
134 if let Some(priority) = command.get_priority(&token_match) {
135 if let Some(canonical) = command.get_canonical_form_of(&token_match) {
136 if priority == CommandPriority::Canonical {
137 command_matches.push_canonical(Self { canonical });
138 } else {
139 command_matches.push_fuzzy(Self { canonical });
140 }
141 }
142 }
143 }
144 }
145 }
146
147 command_matches
148 }
149}
150
151#[async_trait(?Send)]
152impl Autocomplete for TransitionalCommand {
153 async fn autocomplete(input: &str, app_meta: &AppMeta) -> Vec<AutocompleteSuggestion> {
154 autocomplete(input, app_meta).await
155 }
156}
157
158impl fmt::Display for TransitionalCommand {
159 fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
160 write!(f, "{}", self.canonical)
161 }
162}
163
164pub async fn autocomplete(input: &str, app_meta: &AppMeta) -> Vec<AutocompleteSuggestion> {
165 let mut suggestions: Vec<_> = stream::select_all(
166 CommandList::get_all()
167 .iter()
168 .map(|c| c.parse_autocomplete(input, app_meta)),
169 )
170 .collect()
171 .await;
172
173 suggestions.sort();
174 suggestions.truncate(10);
175 suggestions
176}
177
178pub async fn run(input: &str, app_meta: &mut AppMeta) -> Result<String, String> {
179 let commands_tokens: Vec<(&CommandList, Token)> = CommandList::get_all()
182 .iter()
183 .map(|c| (c, c.token()))
184 .collect();
185
186 let mut token_matches: Vec<(&CommandList, CommandPriority, TokenMatch)> = Vec::new();
187
188 {
189 let mut match_streams = stream::SelectAll::default();
190
191 #[expect(clippy::needless_range_loop)]
193 for i in 0..commands_tokens.len() {
194 match_streams.push(
195 stream::repeat(commands_tokens[i].0)
196 .zip(commands_tokens[i].1.match_input_exact(input, app_meta)),
197 );
198 }
199
200 while let Some((command, token_match)) = match_streams.next().await {
201 if let Some(priority) = command.get_priority(&token_match) {
202 token_matches.push((command, priority, token_match));
203 }
204 }
205 }
206
207 token_matches.sort_by_key(|&(_, command_priority, _)| command_priority);
208
209 match token_matches.len() {
210 0 => return Err(format!("Unknown command: \"{}\"", input)),
211 1 => {
212 let (command, _, token_match) = token_matches.pop().unwrap();
213 return command.run(token_match, app_meta).await;
214 }
215 _ => {} }
217
218 if token_matches[0].1 == CommandPriority::Canonical {
219 assert_ne!(token_matches[1].1, CommandPriority::Canonical);
220
221 let (command, _, token_match) = token_matches.remove(0);
222 let result = command.run(token_match, app_meta).await;
223
224 let mut iter = token_matches
225 .iter()
226 .take_while(|(_, command_priority, _)| command_priority == &CommandPriority::Fuzzy)
227 .peekable();
228
229 if iter.peek().is_none() {
230 result
231 } else {
232 let f = |s| {
233 iter
234 .filter_map(|(command, _, token_match)| command.get_canonical_form_of(token_match))
235 .fold(
236 format!("{}\n\n! There are other possible interpretations of this command. Did you mean:\n", s),
237 |mut s, c| { write!(s, "\n* `{}`", c).unwrap(); s }
238 )
239 };
240
241 match result {
242 Ok(s) => Ok(f(s)),
243 Err(s) => Err(f(s)),
244 }
245 }
246 } else {
247 let first_token_match = token_matches.remove(0);
248
249 let mut iter =
250 iter::once(&first_token_match)
251 .chain(token_matches.iter().take_while(|(_, command_priority, _)| {
252 command_priority == &first_token_match.1
253 }))
254 .filter_map(|(command, _, token_match)| command.get_canonical_form_of(token_match))
255 .peekable();
256
257 if iter.peek().is_none() {
258 Err(format!("Unknown command: \"{}\"", input))
259 } else {
260 Err(iter.fold(
261 "There are several possible interpretations of this command. Did you mean:\n"
262 .to_string(),
263 |mut s, c| {
264 write!(s, "\n* `{}`", c).unwrap();
265 s
266 },
267 ))
268 }
269 }
270}