1use std::collections::HashSet;
2
3use promkit_core::grapheme::{StyledGrapheme, StyledGraphemes};
4
5use crate::cursor::Cursor;
6
7#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
9#[derive(Clone, Default)]
10pub enum Mode {
11 #[default]
12 Insert,
14 Overwrite,
16}
17
18#[derive(Clone)]
22pub struct TextEditor(Cursor<StyledGraphemes>);
23
24impl Default for TextEditor {
25 fn default() -> Self {
26 Self(Cursor::new(
27 StyledGraphemes::from(" "),
29 0,
30 false,
31 ))
32 }
33}
34
35impl TextEditor {
36 pub fn new<S: AsRef<str>>(s: S) -> Self {
37 let mut buf = s.as_ref().to_owned();
38 buf.push(' ');
39 let pos = buf.len() - 1;
40 Self(Cursor::new(StyledGraphemes::from(buf), pos, false))
41 }
42
43 pub fn text(&self) -> StyledGraphemes {
45 self.0.contents().clone()
46 }
47
48 pub fn text_without_cursor(&self) -> StyledGraphemes {
50 let mut ret = self.text();
51 ret.pop_back();
52 ret
53 }
54
55 pub fn position(&self) -> usize {
57 self.0.position()
58 }
59
60 pub fn masking(&self, mask: char) -> StyledGraphemes {
62 self.text()
63 .chars()
64 .into_iter()
65 .enumerate()
66 .map(|(i, c)| StyledGrapheme::from(if i == self.text().len() - 1 { c } else { mask }))
67 .collect::<StyledGraphemes>()
68 }
69
70 pub fn replace(&mut self, new: &str) {
72 let mut buf = new.to_owned();
73 buf.push(' ');
74 let pos = buf.len() - 1;
75 *self = Self(Cursor::new(StyledGraphemes::from(buf), pos, false));
76 }
77
78 pub fn insert(&mut self, ch: char) {
80 let pos = self.position();
81 self.0.contents_mut().insert(pos, StyledGrapheme::from(ch));
82 self.forward();
83 }
84
85 pub fn insert_chars(&mut self, vch: &Vec<char>) {
86 for ch in vch {
87 self.insert(*ch);
88 }
89 }
90
91 pub fn overwrite(&mut self, ch: char) {
93 if self.0.is_tail() {
94 self.insert(ch)
95 } else {
96 let pos = self.position();
97 self.0
98 .contents_mut()
99 .replace_range(pos..pos + 1, ch.to_string());
100 self.forward();
101 }
102 }
103
104 pub fn overwrite_chars(&mut self, vch: &Vec<char>) {
105 for ch in vch {
106 self.overwrite(*ch);
107 }
108 }
109
110 pub fn erase(&mut self) {
112 if !self.0.is_head() {
113 self.backward();
114 let pos = self.position();
115 self.0.contents_mut().drain(pos..pos + 1);
116 }
117 }
118
119 pub fn erase_all(&mut self) {
121 *self = Self::default();
122 }
123
124 fn erase_to_position(&mut self, pos: usize) {
127 let current_pos = self.position();
128 if pos > current_pos {
129 self.0.contents_mut().drain(current_pos..pos);
130 } else {
131 self.0.contents_mut().drain(pos..current_pos);
132 self.0.move_to(pos);
133 }
134 }
135
136 fn find_previous_nearest_index(&self, word_break_chars: &HashSet<char>) -> usize {
138 let current_position = self.position();
139 self.text()
140 .chars()
141 .iter()
142 .enumerate()
143 .filter(|&(i, _)| i < current_position.saturating_sub(1))
144 .rev()
145 .find(|&(_, c)| word_break_chars.contains(c))
146 .map(|(i, _)| i + 1)
147 .unwrap_or(0)
148 }
149
150 pub fn erase_to_previous_nearest(&mut self, word_break_chars: &HashSet<char>) {
152 let pos = self.find_previous_nearest_index(word_break_chars);
153 self.erase_to_position(pos);
154 }
155
156 pub fn move_to_previous_nearest(&mut self, word_break_chars: &HashSet<char>) {
158 let pos = self.find_previous_nearest_index(word_break_chars);
159 self.0.move_to(pos);
160 }
161
162 fn find_next_nearest_index(&self, word_break_chars: &HashSet<char>) -> usize {
164 let current_position = self.position();
165 self.text()
166 .chars()
167 .iter()
168 .enumerate()
169 .filter(|&(i, _)| i > current_position)
170 .find(|&(_, c)| word_break_chars.contains(c))
171 .map(|(i, _)| {
172 if i < self.0.contents().len() - 1 {
173 i + 1
174 } else {
175 self.0.contents().len() - 1
176 }
177 })
178 .unwrap_or(self.0.contents().len() - 1)
179 }
180
181 pub fn erase_to_next_nearest(&mut self, word_break_chars: &HashSet<char>) {
183 let pos = self.find_next_nearest_index(word_break_chars);
184 self.erase_to_position(pos);
185 }
186
187 pub fn move_to_next_nearest(&mut self, word_break_chars: &HashSet<char>) {
189 let pos = self.find_next_nearest_index(word_break_chars);
190 self.0.move_to(pos);
191 }
192
193 pub fn move_to_head(&mut self) {
195 self.0.move_to_head()
196 }
197
198 pub fn move_to_tail(&mut self) {
200 self.0.move_to_tail()
201 }
202
203 pub fn shift(&mut self, backward: usize, forward: usize) -> bool {
204 self.0.shift(backward, forward)
205 }
206
207 pub fn backward(&mut self) -> bool {
209 self.0.backward()
210 }
211
212 pub fn forward(&mut self) -> bool {
214 self.0.forward()
215 }
216}
217
218#[cfg(test)]
219mod test {
220 use super::*;
221
222 fn new_with_position(s: String, p: usize) -> TextEditor {
223 TextEditor(Cursor::new(StyledGraphemes::from(s), p, false))
224 }
225
226 mod masking {
227 use super::*;
228
229 #[test]
230 fn test() {
231 let txt = new_with_position(String::from("abcde "), 0);
232 assert_eq!(StyledGraphemes::from("***** "), txt.masking('*'))
233 }
234 }
235
236 mod erase {
237 use super::*;
238
239 #[test]
240 fn test_for_empty() {
241 let txt = TextEditor::default();
242 assert_eq!(StyledGraphemes::from(" "), txt.text());
243 assert_eq!(0, txt.position());
244 }
245
246 #[test]
247 fn test_at_non_edge() {
248 let mut txt = new_with_position(
249 String::from("abc "),
250 1, );
252 let new = new_with_position(
253 String::from("bc "),
254 0, );
256 txt.erase();
257 assert_eq!(new.text(), txt.text());
258 assert_eq!(new.position(), txt.position());
259 }
260
261 #[test]
262 fn test_at_tail() {
263 let mut txt = new_with_position(
264 String::from("abc "),
265 3, );
267 let new = new_with_position(
268 String::from("ab "),
269 2, );
271 txt.erase();
272 assert_eq!(new.text(), txt.text());
273 assert_eq!(new.position(), txt.position());
274 }
275
276 #[test]
277 fn test_at_head() {
278 let txt = new_with_position(
279 String::from("abc "),
280 0, );
282 assert_eq!(StyledGraphemes::from("abc "), txt.text());
283 assert_eq!(0, txt.position());
284 }
285 }
286
287 mod find_previous_nearest_index {
288 use super::*;
289
290 use std::collections::HashSet;
291
292 #[test]
293 fn test() {
294 let mut txt = new_with_position(String::from("koko momo jojo "), 11); assert_eq!(10, txt.find_previous_nearest_index(&HashSet::from([' '])));
296 txt.0.move_to(10);
297 assert_eq!(5, txt.find_previous_nearest_index(&HashSet::from([' '])));
298 }
299
300 #[test]
301 fn test_with_no_target() {
302 let txt = new_with_position(String::from("koko momo jojo "), 7); assert_eq!(0, txt.find_previous_nearest_index(&HashSet::from(['z'])));
304 }
305 }
306
307 mod find_next_nearest_index {
308 use super::*;
309
310 use std::collections::HashSet;
311
312 #[test]
313 fn test() {
314 let mut txt = new_with_position(String::from("koko momo jojo "), 7); assert_eq!(10, txt.find_next_nearest_index(&HashSet::from([' '])));
316 txt.0.move_to(10);
317 assert_eq!(14, txt.find_next_nearest_index(&HashSet::from([' '])));
318 }
319
320 #[test]
321 fn test_with_no_target() {
322 let txt = new_with_position(String::from("koko momo jojo "), 7); assert_eq!(14, txt.find_next_nearest_index(&HashSet::from(['z'])));
324 }
325 }
326
327 mod insert {
328 use super::*;
329
330 #[test]
331 fn test_for_empty() {
332 let mut txt = TextEditor::default();
333 let new = new_with_position(
334 String::from("d "),
335 1, );
337 txt.insert('d');
338 assert_eq!(new.text(), txt.text());
339 assert_eq!(new.position(), txt.position());
340 }
341
342 #[test]
343 fn test_at_non_edge() {
344 let mut txt = new_with_position(
345 String::from("abc "),
346 1, );
348 let new = new_with_position(
349 String::from("adbc "),
350 2, );
352 txt.insert('d');
353 assert_eq!(new.text(), txt.text());
354 assert_eq!(new.position(), txt.position());
355 }
356
357 #[test]
358 fn test_at_tail() {
359 let mut txt = new_with_position(
360 String::from("abc "),
361 3, );
363 let new = new_with_position(
364 String::from("abcd "),
365 4, );
367 txt.insert('d');
368 assert_eq!(new.text(), txt.text());
369 assert_eq!(new.position(), txt.position());
370 }
371
372 #[test]
373 fn test_at_head() {
374 let mut txt = new_with_position(
375 String::from("abc "),
376 0, );
378 let new = new_with_position(
379 String::from("dabc "),
380 1, );
382 txt.insert('d');
383 assert_eq!(new.text(), txt.text());
384 assert_eq!(new.position(), txt.position());
385 }
386 }
387
388 mod overwrite {
389 use super::*;
390
391 #[test]
392 fn test_for_empty() {
393 let mut txt = TextEditor::default();
394 let new = new_with_position(
395 String::from("d "),
396 1, );
398 txt.overwrite('d');
399 assert_eq!(new.text(), txt.text());
400 assert_eq!(new.position(), txt.position());
401 }
402
403 #[test]
404 fn test_at_non_edge() {
405 let mut txt = new_with_position(
406 String::from("abc "),
407 1, );
409 let new = new_with_position(
410 String::from("adc "),
411 2, );
413 txt.overwrite('d');
414 assert_eq!(new.text(), txt.text());
415 assert_eq!(new.position(), txt.position());
416 }
417
418 #[test]
419 fn test_at_tail() {
420 let mut txt = new_with_position(
421 String::from("abc "),
422 3, );
424 let new = new_with_position(
425 String::from("abcd "),
426 4, );
428 txt.overwrite('d');
429 assert_eq!(new.text(), txt.text());
430 assert_eq!(new.position(), txt.position());
431 }
432
433 #[test]
434 fn test_at_head() {
435 let mut txt = new_with_position(
436 String::from("abc "),
437 0, );
439 let new = new_with_position(
440 String::from("dbc "),
441 1, );
443 txt.overwrite('d');
444 assert_eq!(new.text(), txt.text());
445 assert_eq!(new.position(), txt.position());
446 }
447 }
448
449 mod backward {
450 use super::*;
451
452 #[test]
453 fn test_for_empty() {
454 let mut txt = TextEditor::default();
455 txt.backward();
456 assert_eq!(StyledGraphemes::from(" "), txt.text());
457 assert_eq!(0, txt.position());
458 }
459
460 #[test]
461 fn test_at_non_edge() {
462 let mut txt = new_with_position(
463 String::from("abc "),
464 1, );
466 let new = new_with_position(
467 String::from("abc "),
468 0, );
470 txt.backward();
471 assert_eq!(new.text(), txt.text());
472 assert_eq!(new.position(), txt.position());
473 }
474
475 #[test]
476 fn test_at_tail() {
477 let mut txt = new_with_position(
478 String::from("abc "),
479 3, );
481 let new = new_with_position(
482 String::from("abc "),
483 2, );
485 txt.backward();
486 assert_eq!(new.text(), txt.text());
487 assert_eq!(new.position(), txt.position());
488 }
489
490 #[test]
491 fn test_at_head() {
492 let mut txt = new_with_position(
493 String::from("abc "),
494 0, );
496 txt.backward();
497 assert_eq!(StyledGraphemes::from("abc "), txt.text());
498 assert_eq!(0, txt.position());
499 }
500 }
501
502 mod forward {
503 use super::*;
504
505 #[test]
506 fn test_for_empty() {
507 let mut txt = TextEditor::default();
508 txt.forward();
509 assert_eq!(StyledGraphemes::from(" "), txt.text());
510 assert_eq!(0, txt.position());
511 }
512
513 #[test]
514 fn test_at_non_edge() {
515 let mut txt = new_with_position(
516 String::from("abc "),
517 1, );
519 let new = new_with_position(
520 String::from("abc "),
521 2, );
523 txt.forward();
524 assert_eq!(new.text(), txt.text());
525 assert_eq!(new.position(), txt.position());
526 }
527
528 #[test]
529 fn test_at_tail() {
530 let mut txt = new_with_position(
531 String::from("abc "),
532 3, );
534 txt.forward();
535 assert_eq!(StyledGraphemes::from("abc "), txt.text());
536 assert_eq!(3, txt.position());
537 }
538
539 #[test]
540 fn test_at_head() {
541 let mut txt = new_with_position(
542 String::from("abc "),
543 0, );
545 let new = new_with_position(
546 String::from("abc "),
547 1, );
549 txt.forward();
550 assert_eq!(new.text(), txt.text());
551 assert_eq!(new.position(), txt.position());
552 }
553 }
554
555 mod to_head {
556 use super::*;
557
558 #[test]
559 fn test_for_empty() {
560 let mut txt = TextEditor::default();
561 txt.move_to_head();
562 assert_eq!(StyledGraphemes::from(" "), txt.text());
563 assert_eq!(0, txt.position());
564 }
565
566 #[test]
567 fn test_at_non_edge() {
568 let mut txt = new_with_position(
569 String::from("abc "),
570 1, );
572 let new = new_with_position(
573 String::from("abc "),
574 0, );
576 txt.move_to_head();
577 assert_eq!(new.text(), txt.text());
578 assert_eq!(new.position(), txt.position());
579 }
580
581 #[test]
582 fn test_at_tail() {
583 let mut txt = new_with_position(
584 String::from("abc "),
585 3, );
587 let new = new_with_position(
588 String::from("abc "),
589 0, );
591 txt.move_to_head();
592 assert_eq!(new.text(), txt.text());
593 assert_eq!(new.position(), txt.position());
594 }
595
596 #[test]
597 fn test_at_head() {
598 let mut txt = new_with_position(
599 String::from("abc "),
600 0, );
602 txt.move_to_head();
603 assert_eq!(StyledGraphemes::from("abc "), txt.text());
604 assert_eq!(0, txt.position());
605 }
606 }
607
608 mod to_tail {
609 use super::*;
610
611 #[test]
612 fn test_for_empty() {
613 let mut txt = TextEditor::default();
614 txt.move_to_tail();
615 assert_eq!(StyledGraphemes::from(" "), txt.text());
616 assert_eq!(0, txt.position());
617 }
618
619 #[test]
620 fn test_at_non_edge() {
621 let mut txt = new_with_position(
622 String::from("abc "),
623 1, );
625 let new = new_with_position(
626 String::from("abc "),
627 3, );
629 txt.move_to_tail();
630 assert_eq!(new.text(), txt.text());
631 assert_eq!(new.position(), txt.position());
632 }
633
634 #[test]
635 fn test_at_tail() {
636 let mut txt = new_with_position(
637 String::from("abc "),
638 3, );
640 txt.move_to_tail();
641 assert_eq!(StyledGraphemes::from("abc "), txt.text());
642 assert_eq!(3, txt.position());
643 }
644
645 #[test]
646 fn test_at_head() {
647 let mut txt = new_with_position(
648 String::from("abc "),
649 0, );
651 let new = new_with_position(
652 String::from("abc "),
653 3, );
655 txt.move_to_tail();
656 assert_eq!(new.text(), txt.text());
657 assert_eq!(new.position(), txt.position());
658 }
659 }
660}