1use std::cmp::min;
2
3use crate::{
4 autocompletion::{NoAutoCompletion, Replacement},
5 error::InquireResult,
6 formatter::StringFormatter,
7 input::{Input, InputActionResult},
8 list_option::ListOption,
9 prompts::prompt::{ActionResult, Prompt},
10 ui::TextBackend,
11 utils::paginate,
12 validator::{ErrorMessage, StringValidator, Validation},
13 Autocomplete, InquireError, Text,
14};
15
16use super::{action::TextPromptAction, config::TextConfig, DEFAULT_HELP_MESSAGE_WITH_AC};
17
18pub struct TextPrompt<'a, 'b> {
19 message: &'a str,
20 config: TextConfig,
21 default: Option<&'a str>,
22 help_message: Option<&'a str>,
23 input: Input,
24 formatter: StringFormatter<'a>,
25 validators: Vec<Box<dyn StringValidator + 'b>>,
26 error: Option<ErrorMessage>,
27 autocompleter: Box<dyn Autocomplete + 'b>,
28 suggested_options: Vec<String>,
29 suggestion_cursor_index: Option<usize>,
30}
31
32impl<'a, 'b> From<Text<'a, 'b>> for TextPrompt<'a, 'b> {
33 fn from(so: Text<'a, 'b>) -> Self {
34 let input = Input::new_with(so.initial_value.unwrap_or_default());
35 let input = if let Some(placeholder) = so.placeholder {
36 input.with_placeholder(placeholder)
37 } else {
38 input
39 };
40
41 Self {
42 message: so.message,
43 config: (&so).into(),
44 default: so.default,
45 help_message: so.help_message,
46 formatter: so.formatter,
47 autocompleter: so
48 .autocompleter
49 .unwrap_or_else(|| Box::<NoAutoCompletion>::default()),
50 input,
51 error: None,
52 suggestion_cursor_index: None,
53 suggested_options: vec![],
54 validators: so.validators,
55 }
56 }
57}
58
59impl<'a, 'b> From<&'a str> for Text<'a, 'b> {
60 fn from(val: &'a str) -> Self {
61 Text::new(val)
62 }
63}
64
65impl<'a, 'b> TextPrompt<'a, 'b> {
66 fn update_suggestions(&mut self) -> InquireResult<()> {
67 self.suggested_options = self.autocompleter.get_suggestions(self.input.content())?;
68 self.suggestion_cursor_index = None;
69
70 Ok(())
71 }
72
73 fn get_highlighted_suggestion(&self) -> Option<&str> {
74 if let Some(cursor) = self.suggestion_cursor_index {
75 let suggestion = self.suggested_options.get(cursor).unwrap().as_ref();
76 Some(suggestion)
77 } else {
78 None
79 }
80 }
81
82 fn move_cursor_up(&mut self, qty: usize) -> ActionResult {
83 let new_cursor_index = match self.suggestion_cursor_index {
84 None => None,
85 Some(index) if index < qty => None,
86 Some(index) => Some(index.saturating_sub(qty)),
87 };
88
89 self.update_suggestion_cursor_pos(new_cursor_index)
90 }
91
92 fn move_cursor_down(&mut self, qty: usize) -> ActionResult {
93 let new_cursor_index = match self.suggested_options.is_empty() {
94 true => None,
95 false => match self.suggestion_cursor_index {
96 None if qty == 0 => None,
97 None => Some(min(
98 qty.saturating_sub(1),
99 self.suggested_options.len().saturating_sub(1),
100 )),
101 Some(index) => Some(min(
102 index.saturating_add(qty),
103 self.suggested_options.len().saturating_sub(1),
104 )),
105 },
106 };
107
108 self.update_suggestion_cursor_pos(new_cursor_index)
109 }
110
111 fn update_suggestion_cursor_pos(&mut self, new_position: Option<usize>) -> ActionResult {
112 if new_position != self.suggestion_cursor_index {
113 self.suggestion_cursor_index = new_position;
114 ActionResult::NeedsRedraw
115 } else {
116 ActionResult::Clean
117 }
118 }
119
120 fn use_current_suggestion(&mut self) -> InquireResult<ActionResult> {
121 let suggestion = self.get_highlighted_suggestion().map(|s| s.to_owned());
122 match self
123 .autocompleter
124 .get_completion(self.input.content(), suggestion)?
125 {
126 Replacement::Some(value) => {
127 self.input = Input::new_with(value);
128 Ok(ActionResult::NeedsRedraw)
129 }
130 Replacement::None => Ok(ActionResult::Clean),
131 }
132 }
133
134 fn get_current_answer(&self) -> &str {
135 if let Some(suggestion) = self.get_highlighted_suggestion() {
138 return suggestion;
139 }
140
141 if self.input.content().is_empty() {
143 if let Some(val) = self.default {
144 return val;
145 }
146 }
147
148 self.input.content()
149 }
150
151 fn validate_current_answer(&self) -> InquireResult<Validation> {
152 for validator in &self.validators {
153 match validator.validate(self.get_current_answer()) {
154 Ok(Validation::Valid) => {}
155 Ok(Validation::Invalid(msg)) => return Ok(Validation::Invalid(msg)),
156 Err(err) => return Err(InquireError::Custom(err)),
157 }
158 }
159
160 Ok(Validation::Valid)
161 }
162}
163
164impl<'a, 'b, Backend> Prompt<Backend> for TextPrompt<'a, 'b>
165where
166 Backend: TextBackend,
167{
168 type Config = TextConfig;
169 type InnerAction = TextPromptAction;
170 type Output = String;
171
172 fn message(&self) -> &str {
173 self.message
174 }
175
176 fn config(&self) -> &TextConfig {
177 &self.config
178 }
179
180 fn format_answer(&self, answer: &String) -> String {
181 (self.formatter)(answer)
182 }
183
184 fn setup(&mut self) -> InquireResult<()> {
185 self.update_suggestions()
186 }
187
188 fn submit(&mut self) -> InquireResult<Option<String>> {
189 let result = match self.validate_current_answer()? {
190 Validation::Valid => Some(self.get_current_answer().to_owned()),
191 Validation::Invalid(msg) => {
192 self.error = Some(msg);
193 None
194 }
195 };
196
197 Ok(result)
198 }
199
200 fn handle(&mut self, action: TextPromptAction) -> InquireResult<ActionResult> {
201 let result = match action {
202 TextPromptAction::ValueInput(input_action) => {
203 let result = self.input.handle(input_action);
204
205 if let InputActionResult::ContentChanged = result {
206 self.update_suggestions()?;
207 }
208
209 result.into()
210 }
211 TextPromptAction::MoveToSuggestionAbove => self.move_cursor_up(1),
212 TextPromptAction::MoveToSuggestionBelow => self.move_cursor_down(1),
213 TextPromptAction::MoveToSuggestionPageUp => self.move_cursor_up(self.config.page_size),
214 TextPromptAction::MoveToSuggestionPageDown => {
215 self.move_cursor_down(self.config.page_size)
216 }
217 TextPromptAction::UseCurrentSuggestion => {
218 let result = self.use_current_suggestion()?;
219
220 if let ActionResult::NeedsRedraw = result {
221 self.update_suggestions()?;
222 }
223
224 result
225 }
226 };
227
228 Ok(result)
229 }
230
231 fn render(&self, backend: &mut Backend) -> InquireResult<()> {
232 let prompt = &self.message;
233
234 if let Some(err) = &self.error {
235 backend.render_error_message(err)?;
236 }
237
238 backend.render_prompt(prompt, self.default, &self.input)?;
239
240 let choices = self
241 .suggested_options
242 .iter()
243 .enumerate()
244 .map(|(i, val)| ListOption::new(i, val.as_ref()))
245 .collect::<Vec<ListOption<&str>>>();
246
247 let page = paginate(
248 self.config.page_size,
249 &choices,
250 self.suggestion_cursor_index,
251 );
252
253 backend.render_suggestions(page)?;
254
255 if let Some(message) = self.help_message {
256 backend.render_help_message(message)?;
257 } else if !choices.is_empty() {
258 backend.render_help_message(DEFAULT_HELP_MESSAGE_WITH_AC)?;
259 }
260
261 Ok(())
262 }
263}