1pub mod history;
14
15#[cfg(test)]
16mod tests;
17
18use std::convert::Infallible;
19use std::str::FromStr;
20
21use unicode_segmentation::UnicodeSegmentation;
22
23#[derive(Clone, Debug, Default, PartialEq, Eq)]
29pub struct PromptInput {
30 text: String,
31 cursor: usize,
33}
34
35impl From<String> for PromptInput {
36 fn from(s: String) -> Self {
37 Self::from_text(&s)
38 }
39}
40
41impl From<&str> for PromptInput {
42 fn from(s: &str) -> Self {
43 Self::from_text(s)
44 }
45}
46
47impl FromStr for PromptInput {
48 type Err = Infallible;
49
50 fn from_str(s: &str) -> Result<Self, Self::Err> {
51 Ok(Self::from_text(s))
52 }
53}
54
55impl PromptInput {
56 pub fn new() -> Self {
58 Self::default()
59 }
60
61 fn from_text(s: &str) -> Self {
62 let cursor = s.graphemes(true).count();
63 Self { text: s.to_string(), cursor }
64 }
65
66 pub fn as_str(&self) -> &str {
68 &self.text
69 }
70
71 pub fn text(&self) -> String {
73 self.text.clone()
74 }
75
76 pub fn cursor(&self) -> usize {
78 self.cursor
79 }
80
81 pub fn len_graphemes(&self) -> usize {
83 self.text.graphemes(true).count()
84 }
85
86 pub fn graphemes(&self) -> unicode_segmentation::Graphemes<'_> {
88 self.text.graphemes(true)
89 }
90
91 pub fn is_empty(&self) -> bool {
93 self.text.is_empty()
94 }
95
96 pub fn clear(&mut self) {
98 self.text.clear();
99 self.cursor = 0;
100 }
101
102 pub fn set_text(&mut self, s: &str) {
104 self.text = s.to_string();
105 self.cursor = self.len_graphemes();
106 }
107
108 pub fn cursor_left(&mut self) {
110 self.cursor = self.cursor.saturating_sub(1);
111 }
112
113 pub fn cursor_right(&mut self) {
115 if self.cursor < self.len_graphemes() {
116 self.cursor += 1;
117 }
118 }
119
120 pub fn cursor_to_start(&mut self) {
122 self.cursor = 0;
123 }
124
125 pub fn cursor_to_end(&mut self) {
127 self.cursor = self.len_graphemes();
128 }
129
130 pub fn cursor_up(&mut self) -> bool {
133 let graphemes: Vec<&str> = self.graphemes().collect();
134 let Some((line_start, column)) = line_start_and_column(&graphemes, self.cursor) else {
135 return false;
136 };
137 if line_start == 0 {
138 return false;
139 }
140
141 let prev_end = line_start.saturating_sub(1);
142 let prev_start = graphemes[..prev_end]
143 .iter()
144 .rposition(|g| g.contains('\n'))
145 .map_or(0, |idx| idx + 1);
146 let prev_len = prev_end.saturating_sub(prev_start);
147 self.cursor = prev_start + column.min(prev_len);
148 true
149 }
150
151 pub fn cursor_down(&mut self) -> bool {
154 let graphemes: Vec<&str> = self.graphemes().collect();
155 let Some((line_start, column)) = line_start_and_column(&graphemes, self.cursor) else {
156 return false;
157 };
158 let current_end = graphemes[line_start..]
159 .iter()
160 .position(|g| g.contains('\n'))
161 .map(|offset| line_start + offset);
162 let Some(current_end) = current_end else {
163 return false;
164 };
165 let next_start = current_end + 1;
166 let next_end = graphemes[next_start..]
167 .iter()
168 .position(|g| g.contains('\n'))
169 .map_or(graphemes.len(), |offset| next_start + offset);
170 let next_len = next_end.saturating_sub(next_start);
171 self.cursor = next_start + column.min(next_len);
172 true
173 }
174
175 pub fn cursor_word_left(&mut self) {
180 let graphemes: Vec<&str> = self.graphemes().collect();
181 if self.cursor == 0 {
182 return;
183 }
184 self.cursor = prev_word_boundary(&graphemes, self.cursor);
185 }
186
187 pub fn cursor_word_right(&mut self) {
192 let graphemes: Vec<&str> = self.graphemes().collect();
193 let len = graphemes.len();
194 if self.cursor >= len {
195 return;
196 }
197 self.cursor = next_word_boundary(&graphemes, self.cursor);
198 }
199
200 pub fn insert_char(&mut self, ch: char) {
202 let byte_idx = self.byte_offset_of(self.cursor);
203 self.text.insert(byte_idx, ch);
204 self.cursor += 1;
205 }
206
207 pub fn insert_str(&mut self, s: &str) {
210 let count = s.graphemes(true).count();
211 if count == 0 {
212 return;
213 }
214 let byte_idx = self.byte_offset_of(self.cursor);
215 self.text.insert_str(byte_idx, s);
216 self.cursor += count;
217 }
218
219 pub fn replace_range(&mut self, start: usize, end: usize, replacement: &str) {
221 let len = self.len_graphemes();
222 let start = start.min(len);
223 let end = end.min(len).max(start);
224 let start_byte = self.byte_offset_of(start);
225 let end_byte = self.byte_offset_of(end);
226 self.text.replace_range(start_byte..end_byte, replacement);
227 self.cursor = start + replacement.graphemes(true).count();
228 }
229
230 pub fn backspace(&mut self) -> bool {
234 if self.cursor == 0 {
235 return false;
236 }
237 let graphemes: Vec<&str> = self.graphemes().collect();
238 let prev = graphemes[self.cursor - 1];
239 let byte_idx = self.byte_offset_of(self.cursor - 1);
240 self.text.replace_range(byte_idx..byte_idx + prev.len(), "");
241 self.cursor -= 1;
242 true
243 }
244
245 pub fn delete_forward(&mut self) -> bool {
249 let len = self.len_graphemes();
250 if self.cursor >= len {
251 return false;
252 }
253 let graphemes: Vec<&str> = self.graphemes().collect();
254 let cur = graphemes[self.cursor];
255 let byte_idx = self.byte_offset_of(self.cursor);
256 self.text.replace_range(byte_idx..byte_idx + cur.len(), "");
257 true
258 }
259
260 pub fn kill_to_end_of_line(&mut self) -> String {
262 let graphemes: Vec<&str> = self.graphemes().collect();
263 let line_end = graphemes[self.cursor..]
264 .iter()
265 .position(|g| g.contains('\n'))
266 .map(|offset| self.cursor + offset)
267 .unwrap_or_else(|| self.len_graphemes());
268 let byte_idx = self.byte_offset_of(self.cursor);
269 let end_byte = self.byte_offset_of(line_end);
270 let killed = self.text[byte_idx..end_byte].to_string();
271 self.text.replace_range(byte_idx..end_byte, "");
272 killed
273 }
274
275 pub fn kill_to_start_of_line(&mut self) -> String {
277 let graphemes: Vec<&str> = self.graphemes().collect();
278 let line_start = graphemes[..self.cursor]
279 .iter()
280 .rposition(|g| g.contains('\n'))
281 .map_or(0, |idx| idx + 1);
282 let start_byte = self.byte_offset_of(line_start);
283 let end_byte = self.byte_offset_of(self.cursor);
284 let killed = self.text[start_byte..end_byte].to_string();
285 self.text.replace_range(start_byte..end_byte, "");
286 self.cursor = line_start;
287 killed
288 }
289
290 pub fn kill_word_left(&mut self) -> String {
293 let graphemes: Vec<&str> = self.graphemes().collect();
294 if self.cursor == 0 {
295 return String::new();
296 }
297 let target = prev_word_boundary(&graphemes, self.cursor);
298 let start_byte = self.byte_offset_of(target);
299 let end_byte = self.byte_offset_of(self.cursor);
300 let killed = self.text[start_byte..end_byte].to_string();
301 self.text.replace_range(start_byte..end_byte, "");
302 self.cursor = target;
303 killed
304 }
305
306 pub fn kill_word_right(&mut self) -> String {
309 let graphemes: Vec<&str> = self.graphemes().collect();
310 let len = graphemes.len();
311 if self.cursor >= len {
312 return String::new();
313 }
314 let target = next_word_boundary(&graphemes, self.cursor);
315 let start_byte = self.byte_offset_of(self.cursor);
316 let end_byte = self.byte_offset_of(target);
317 let killed = self.text[start_byte..end_byte].to_string();
318 self.text.replace_range(start_byte..end_byte, "");
319 killed
320 }
321
322 pub fn transpose_chars(&mut self) -> bool {
329 let graphemes: Vec<&str> = self.graphemes().collect();
330 if graphemes.len() < 2 {
331 return false;
332 }
333 let (a, b) = if self.cursor >= graphemes.len() {
334 (graphemes.len() - 2, graphemes.len() - 1)
335 } else if self.cursor == 0 {
336 (0, 1)
337 } else {
338 (self.cursor - 1, self.cursor)
339 };
340 let byte_a = self.byte_offset_of(a);
341 let byte_b = self.byte_offset_of(b);
342 let combined = format!("{}{}", graphemes[b], graphemes[a]);
343 self.text.replace_range(byte_a..byte_b + graphemes[b].len(), &combined);
344 self.cursor = b + 1;
345 true
346 }
347
348 pub fn yank(&mut self, text: &str) {
350 self.insert_str(text);
351 }
352
353 fn byte_offset_of(&self, grapheme_index: usize) -> usize {
355 self.text
356 .grapheme_indices(true)
357 .nth(grapheme_index)
358 .map(|(byte_idx, _)| byte_idx)
359 .unwrap_or_else(|| self.text.len())
360 }
361
362 pub fn text_before_cursor(&self) -> &str {
364 let byte_idx = self.byte_offset_of(self.cursor);
365 &self.text[..byte_idx]
366 }
367}
368
369fn line_start_and_column(graphemes: &[&str], cursor: usize) -> Option<(usize, usize)> {
372 if cursor > graphemes.len() {
373 return None;
374 }
375 let line_start = graphemes[..cursor]
376 .iter()
377 .rposition(|g| g.contains('\n'))
378 .map_or(0, |idx| idx + 1);
379 Some((line_start, cursor.saturating_sub(line_start)))
380}
381
382fn prev_word_boundary(graphemes: &[&str], cursor: usize) -> usize {
388 if cursor == 0 {
389 return 0;
390 }
391 let combined: String = graphemes[..cursor].concat();
392 let segments: Vec<(usize, &str)> = combined
393 .split_word_bound_indices()
394 .map(|(byte_off, word)| {
395 let g_off = combined[..byte_off].graphemes(true).count();
396 (g_off, word)
397 })
398 .collect();
399
400 for &(start, word) in segments.iter().rev() {
401 if start < cursor && !word.trim().is_empty() {
402 return start;
403 }
404 }
405 0
406}
407
408fn next_word_boundary(graphemes: &[&str], cursor: usize) -> usize {
414 let len = graphemes.len();
415 if cursor >= len {
416 return len;
417 }
418 let combined: String = graphemes[cursor..].concat();
419 let segments: Vec<(usize, &str)> = combined
420 .split_word_bound_indices()
421 .map(|(byte_off, word)| {
422 let g_off = combined[..byte_off].graphemes(true).count();
423 (g_off, word)
424 })
425 .collect();
426
427 for &(start, word) in segments.iter() {
428 if start > 0 && !word.trim().is_empty() {
429 return cursor + start;
430 }
431 }
432 len
433}