1use crate::text::{Paragraph, Run, RunKind, RunProps, TextBody};
5use crate::words;
6
7pub type Pos = (usize, usize);
9
10fn clamp(body: &TextBody, p: Pos) -> Pos {
11 if body.paragraphs.is_empty() {
12 return (0, 0);
13 }
14 let pi = p.0.min(body.paragraphs.len() - 1);
15 let n = body
16 .paragraphs
17 .get(pi)
18 .map(Paragraph::char_len)
19 .unwrap_or(0);
20 (pi, p.1.min(n))
21}
22
23fn order(a: Pos, b: Pos) -> (Pos, Pos) {
24 if a <= b { (a, b) } else { (b, a) }
25}
26
27fn split_at(p: &mut Paragraph, at: usize) -> usize {
30 let mut pos = 0;
31 let mut i = 0;
32 while i < p.runs.len() {
33 let len = p.runs.get(i).map(Run::char_len).unwrap_or(0);
34 if pos == at {
35 return i;
36 }
37 if at < pos + len {
38 let Some(r) = p.runs.get(i) else { return i };
39 if r.kind != RunKind::Text {
40 return i + 1;
41 }
42 let k = at - pos;
43 let byte = r
44 .text
45 .char_indices()
46 .nth(k)
47 .map(|(b, _)| b)
48 .unwrap_or(r.text.len());
49 let (a, b) = r.text.split_at(byte);
50 let second = Run {
51 text: b.to_string(),
52 props: r.props.clone(),
53 kind: RunKind::Text,
54 };
55 let first = a.to_string();
56 if let Some(r) = p.runs.get_mut(i) {
57 r.text = first;
58 }
59 p.runs.insert(i + 1, second);
60 return i + 1;
61 }
62 pos += len;
63 i += 1;
64 }
65 p.runs.len()
66}
67
68pub fn props_at(body: &TextBody, pos: Pos) -> RunProps {
70 let (pi, ch) = clamp(body, pos);
71 let Some(p) = body.paragraphs.get(pi) else {
72 return RunProps::default();
73 };
74 let mut acc = 0;
75 let mut last = None;
76 for r in &p.runs {
77 let len = r.char_len();
78 if ch <= acc + len && ch > acc {
79 return r.props.clone();
80 }
81 if acc + len >= ch && ch == 0 {
82 return r.props.clone();
83 }
84 acc += len;
85 last = Some(&r.props);
86 }
87 last.cloned().unwrap_or_else(|| p.end_props.clone())
88}
89
90pub fn insert(body: &mut TextBody, pos: Pos, text: &str, props: Option<RunProps>) -> Pos {
93 if body.paragraphs.is_empty() {
94 body.paragraphs.push(Paragraph::default());
95 }
96 let (mut pi, mut ch) = clamp(body, pos);
97 let props = props.unwrap_or_else(|| props_at(body, (pi, ch)));
98 let text = text.replace("\r\n", "\n").replace('\r', "\n");
99 let mut first = true;
100 for part in text.split('\n') {
101 if !first {
102 let Some(p) = body.paragraphs.get_mut(pi) else {
104 break;
105 };
106 let idx = split_at(p, ch);
107 let tail: Vec<Run> = p.runs.drain(idx..).collect();
108 let newp = Paragraph {
109 level: p.level,
110 props: p.props.clone(),
111 runs: tail,
112 end_props: p.end_props.clone(),
113 };
114 p.end_props = props.clone();
115 body.paragraphs.insert(pi + 1, newp);
116 pi += 1;
117 ch = 0;
118 }
119 first = false;
120 for (k, seg) in part.split('\u{b}').enumerate() {
121 let Some(p) = body.paragraphs.get_mut(pi) else {
122 break;
123 };
124 if k > 0 {
125 let idx = split_at(p, ch);
126 p.runs.insert(
127 idx,
128 Run {
129 text: String::new(),
130 props: props.clone(),
131 kind: RunKind::Break,
132 },
133 );
134 ch += 1;
135 }
136 if seg.is_empty() {
137 continue;
138 }
139 let idx = split_at(p, ch);
140 p.runs.insert(idx, Run::with(seg, props.clone()));
141 ch += seg.chars().count();
142 }
143 if let Some(p) = body.paragraphs.get_mut(pi) {
144 p.normalize();
145 if p.runs.is_empty() {
146 p.end_props = props.clone();
147 }
148 }
149 }
150 (pi, ch)
151}
152
153pub fn delete(body: &mut TextBody, a: Pos, b: Pos) -> Pos {
155 if body.paragraphs.is_empty() {
156 return (0, 0);
157 }
158 let (s, e) = order(clamp(body, a), clamp(body, b));
159 if s == e {
160 return s;
161 }
162 if s.0 == e.0 {
163 if let Some(p) = body.paragraphs.get_mut(s.0) {
164 let end_props = props_of_range(p, s.1);
165 let i0 = split_at(p, s.1);
166 let i1 = split_at(p, e.1);
167 if i1 > i0 {
168 p.runs.drain(i0..i1);
169 }
170 p.normalize();
171 if p.runs.is_empty() {
172 p.end_props = end_props;
173 }
174 }
175 return s;
176 }
177 let tail: Vec<Run> = match body.paragraphs.get_mut(e.0) {
179 Some(p) => {
180 let i = split_at(p, e.1);
181 p.runs.drain(i..).collect()
182 }
183 None => vec![],
184 };
185 if let Some(p) = body.paragraphs.get_mut(s.0) {
186 let i = split_at(p, s.1);
187 p.runs.truncate(i);
188 p.runs.extend(tail);
189 p.normalize();
190 }
191 if e.0 > s.0 && e.0 < body.paragraphs.len() {
192 body.paragraphs.drain(s.0 + 1..=e.0);
193 }
194 s
195}
196
197fn props_of_range(p: &Paragraph, at: usize) -> RunProps {
198 let mut acc = 0;
199 for r in &p.runs {
200 let len = r.char_len();
201 if at < acc + len || (at == acc + len && at > 0) {
202 return r.props.clone();
203 }
204 acc += len;
205 }
206 p.runs
207 .last()
208 .map(|r| r.props.clone())
209 .unwrap_or_else(|| p.end_props.clone())
210}
211
212pub fn format(body: &mut TextBody, a: Pos, b: Pos, f: &dyn Fn(&mut RunProps)) {
215 if body.paragraphs.is_empty() {
216 return;
217 }
218 let (s, e) = order(clamp(body, a), clamp(body, b));
219 for pi in s.0..=e.0 {
220 let Some(p) = body.paragraphs.get_mut(pi) else {
221 continue;
222 };
223 let from = if pi == s.0 { s.1 } else { 0 };
224 let to = if pi == e.0 { e.1 } else { p.char_len() };
225 if p.runs.is_empty() || (from == to && s == e) {
226 f(&mut p.end_props);
227 continue;
228 }
229 let i0 = split_at(p, from);
230 let i1 = split_at(p, to);
231 for r in p.runs.iter_mut().take(i1).skip(i0) {
232 f(&mut r.props);
233 }
234 if to == p.char_len() {
235 f(&mut p.end_props);
236 }
237 p.normalize();
238 }
239}
240
241pub fn format_all(body: &mut TextBody, f: &dyn Fn(&mut RunProps)) {
243 for p in &mut body.paragraphs {
244 for r in &mut p.runs {
245 f(&mut r.props);
246 }
247 f(&mut p.end_props);
248 p.normalize();
249 }
250}
251
252pub fn text_range(body: &TextBody, a: Pos, b: Pos) -> String {
254 let (s, e) = order(clamp(body, a), clamp(body, b));
255 let mut out = String::new();
256 for pi in s.0..=e.0 {
257 let Some(p) = body.paragraphs.get(pi) else {
258 continue;
259 };
260 let t: Vec<char> = p.text().chars().collect();
261 let from = if pi == s.0 { s.1 } else { 0 };
262 let to = if pi == e.0 { e.1 } else { t.len() };
263 out.extend(t.get(from.min(t.len())..to.min(t.len())).unwrap_or(&[]));
264 if pi != e.0 {
265 out.push('\n');
266 }
267 }
268 out
269}
270
271pub fn slice(body: &TextBody, a: Pos, b: Pos) -> TextBody {
273 let (s, e) = order(clamp(body, a), clamp(body, b));
274 let mut out = TextBody {
275 body: body.body.clone(),
276 list_style: body.list_style.clone(),
277 paragraphs: vec![],
278 };
279 for pi in s.0..=e.0 {
280 let Some(p) = body.paragraphs.get(pi) else {
281 continue;
282 };
283 let mut p = p.clone();
284 let to = if pi == e.0 { e.1 } else { p.char_len() };
285 let i1 = split_at(&mut p, to);
286 p.runs.truncate(i1);
287 let from = if pi == s.0 { s.1 } else { 0 };
288 let i0 = split_at(&mut p, from);
289 p.runs.drain(..i0);
290 out.paragraphs.push(p);
291 }
292 out
293}
294
295pub fn insert_body(body: &mut TextBody, pos: Pos, src: &TextBody) -> Pos {
297 let mut caret = clamp(body, pos);
298 for (k, sp) in src.paragraphs.iter().enumerate() {
299 if k > 0 {
300 caret = insert(body, caret, "\n", None);
301 if let Some(p) = body.paragraphs.get_mut(caret.0) {
302 p.level = sp.level;
303 p.props = sp.props.clone();
304 }
305 }
306 for r in &sp.runs {
307 match r.kind {
308 RunKind::Text => caret = insert(body, caret, &r.text, Some(r.props.clone())),
309 RunKind::Break => caret = insert(body, caret, "\u{b}", Some(r.props.clone())),
310 _ => {
311 if let Some(p) = body.paragraphs.get_mut(caret.0) {
312 let idx = split_at(p, caret.1);
313 p.runs.insert(idx, r.clone());
314 caret.1 += r.char_len();
315 }
316 }
317 }
318 }
319 }
320 caret
321}
322
323fn para_text(body: &TextBody, pi: usize) -> String {
324 body.paragraphs
325 .get(pi)
326 .map(Paragraph::text)
327 .unwrap_or_default()
328}
329
330pub fn word_at(body: &TextBody, pos: Pos) -> (Pos, Pos) {
333 let (pi, ch) = clamp(body, pos);
334 let (s, e) = words::word_at(¶_text(body, pi), ch);
335 ((pi, s), (pi, e))
336}
337
338pub fn word_move(body: &TextBody, pos: Pos, forward: bool) -> Pos {
342 let (pi, ch) = clamp(body, pos);
343 let t = para_text(body, pi);
344 if forward {
345 match words::next_stop(&t, ch) {
346 Some(s) => (pi, s),
347 None if pi + 1 < body.paragraphs.len() => (pi + 1, 0),
348 None => (pi, ch),
349 }
350 } else {
351 match words::prev_stop(&t, ch) {
352 Some(s) => (pi, s),
353 None if pi > 0 => (
354 pi - 1,
355 body.paragraphs
356 .get(pi - 1)
357 .map(Paragraph::char_len)
358 .unwrap_or(0),
359 ),
360 None => (pi, 0),
361 }
362 }
363}
364
365pub fn para_up(body: &TextBody, pos: Pos) -> Pos {
367 let (pi, ch) = clamp(body, pos);
368 if ch > 0 {
369 (pi, 0)
370 } else {
371 (pi.saturating_sub(1), 0)
372 }
373}
374
375pub fn para_down(body: &TextBody, pos: Pos) -> Pos {
377 let (pi, _) = clamp(body, pos);
378 if pi + 1 < body.paragraphs.len() {
379 (pi + 1, 0)
380 } else {
381 (
382 pi,
383 body.paragraphs
384 .get(pi)
385 .map(Paragraph::char_len)
386 .unwrap_or(0),
387 )
388 }
389}
390
391pub fn delete_word(body: &mut TextBody, pos: Pos, forward: bool) -> Pos {
401 if body.paragraphs.is_empty() {
402 return (0, 0);
403 }
404 let (pi, ch) = clamp(body, pos);
405 let t = para_text(body, pi);
406 if !forward {
407 let to = match words::prev_stop(&t, ch) {
408 Some(s) => (pi, s),
409 None => char_move(body, (pi, ch), false),
410 };
411 return delete(body, to, (pi, ch));
412 }
413 let Some(e) = words::delete_forward_end(&t, ch) else {
414 let to = char_move(body, (pi, ch), true);
415 return delete(body, (pi, ch), to);
416 };
417 let at = delete(body, (pi, ch), (pi, e));
418 smart_space(body, at)
419}
420
421fn smart_space(body: &mut TextBody, at: Pos) -> Pos {
423 let (pi, s) = at;
424 let chars: Vec<char> = para_text(body, pi).chars().collect();
425 let after = chars.get(s).copied();
426 let before = s.checked_sub(1).and_then(|i| chars.get(i)).copied();
427 if after.is_none_or(words::is_closing_punct) && before.is_some_and(|c| c == ' ') {
428 let mut w = s;
429 while w > 0 && chars.get(w - 1) == Some(&' ') {
430 w -= 1;
431 }
432 if w > 0 && chars.get(w - 1).is_some_and(|c| c.is_alphanumeric()) {
433 return delete(body, (pi, w), (pi, s));
434 }
435 return at;
436 }
437 if let (Some(b), Some(a)) = (before, after)
438 && words::is_sentence_punct(b)
439 && a.is_alphanumeric()
440 {
441 let digits = s >= 2 && chars.get(s - 2).is_some_and(|c| c.is_numeric()) && a.is_numeric();
442 if !digits {
443 return insert(body, at, " ", None);
444 }
445 }
446 at
447}
448
449pub fn char_move(body: &TextBody, pos: Pos, forward: bool) -> Pos {
451 let (pi, ch) = clamp(body, pos);
452 let n = body
453 .paragraphs
454 .get(pi)
455 .map(Paragraph::char_len)
456 .unwrap_or(0);
457 if forward {
458 if ch < n {
459 (pi, ch + 1)
460 } else if pi + 1 < body.paragraphs.len() {
461 (pi + 1, 0)
462 } else {
463 (pi, ch)
464 }
465 } else if ch > 0 {
466 (pi, ch - 1)
467 } else if pi > 0 {
468 (
469 pi - 1,
470 body.paragraphs
471 .get(pi - 1)
472 .map(Paragraph::char_len)
473 .unwrap_or(0),
474 )
475 } else {
476 (0, 0)
477 }
478}
479
480pub fn end(body: &TextBody) -> Pos {
482 let pi = body.paragraphs.len().saturating_sub(1);
483 (
484 pi,
485 body.paragraphs
486 .get(pi)
487 .map(Paragraph::char_len)
488 .unwrap_or(0),
489 )
490}
491
492pub fn next_case(s: &str) -> &'static str {
495 if s == s.to_lowercase() && s.chars().any(char::is_alphabetic) {
496 "title"
497 } else if s == s.to_uppercase() && s.chars().any(char::is_alphabetic) {
498 "lower"
499 } else {
500 "upper"
501 }
502}
503
504pub fn change_case(s: &str, mode: &str) -> String {
506 match mode {
507 "upper" => s.to_uppercase(),
508 "lower" => s.to_lowercase(),
509 "toggle" => s
510 .chars()
511 .map(|c| {
512 if c.is_uppercase() {
513 c.to_lowercase().collect::<String>()
514 } else {
515 c.to_uppercase().collect()
516 }
517 })
518 .collect(),
519 "title" => {
520 let mut out = String::new();
521 let mut start = true;
522 for c in s.chars() {
523 if start && c.is_alphabetic() {
524 out.extend(c.to_uppercase());
525 start = false;
526 } else {
527 out.extend(c.to_lowercase());
528 }
529 if c.is_whitespace() {
530 start = true;
531 }
532 }
533 out
534 }
535 _ => {
536 let mut out = String::new();
538 let mut start = true;
539 for c in s.chars() {
540 if start && c.is_alphabetic() {
541 out.extend(c.to_uppercase());
542 start = false;
543 } else {
544 out.extend(c.to_lowercase());
545 }
546 if matches!(c, '.' | '!' | '?') {
547 start = true;
548 }
549 }
550 out
551 }
552 }
553}
554
555#[cfg(test)]
556mod tests {
557 use super::*;
558
559 fn body(t: &str) -> TextBody {
560 TextBody::from_text(t)
561 }
562
563 #[test]
564 fn insert_and_split() {
565 let mut b = body("Hello");
566 let c = insert(&mut b, (0, 5), " world\nNext", None);
567 assert_eq!(b.text(), "Hello world\nNext");
568 assert_eq!(c, (1, 4));
569 let c = insert(&mut b, (0, 0), ">", None);
570 assert_eq!(c, (0, 1));
571 assert_eq!(b.paragraphs[0].runs.len(), 1);
572 }
573
574 #[test]
575 fn delete_within_and_across() {
576 let mut b = body("abc\ndef\nghi");
577 assert_eq!(delete(&mut b, (0, 1), (2, 1)), (0, 1));
578 assert_eq!(b.text(), "ahi");
579 let mut b = body("abcdef");
580 delete(&mut b, (0, 4), (0, 2));
581 assert_eq!(b.text(), "abef");
582 delete(&mut b, (99, 99), (0, 0));
584 assert_eq!(b.text(), "");
585 }
586
587 #[test]
588 fn format_splits_runs() {
589 let mut b = body("abcdef");
590 format(&mut b, (0, 2), (0, 4), &|p| p.bold = Some(true));
591 let p = &b.paragraphs[0];
592 assert_eq!(p.runs.len(), 3);
593 assert_eq!(p.runs[1].text, "cd");
594 assert_eq!(p.runs[1].props.bold, Some(true));
595 assert_eq!(props_at(&b, (0, 3)).bold, Some(true));
596 assert_eq!(text_range(&b, (0, 1), (0, 5)), "bcde");
597 let s = slice(&b, (0, 1), (0, 5));
598 assert_eq!(s.text(), "bcde");
599 let mut c = body("XY");
600 let caret = insert_body(&mut c, (0, 1), &s);
601 assert_eq!(c.text(), "XbcdeY");
602 assert_eq!(caret, (0, 5));
603 }
604
605 #[test]
606 fn words_and_moves() {
607 let b = body("Hello brave world");
608 assert_eq!(word_at(&b, (0, 7)), ((0, 6), (0, 11)));
609 assert_eq!(word_move(&b, (0, 0), true), (0, 6));
610 assert_eq!(word_move(&b, (0, 8), false), (0, 6));
611 assert_eq!(char_move(&b, (0, 17), true), (0, 17));
612 assert_eq!(
613 change_case("hello world. again", "sentence"),
614 "Hello world. Again"
615 );
616 assert_eq!(change_case("hello world", "title"), "Hello World");
617 }
618
619 #[test]
620 fn line_break_insert() {
621 let mut b = body("ab");
622 let c = insert(&mut b, (0, 1), "\u{b}", None);
623 assert_eq!(c, (0, 2));
624 assert_eq!(b.paragraphs[0].char_len(), 3);
625 assert_eq!(b.paragraphs.len(), 1);
626 }
627
628 fn del_word(text: &str, at: Pos, forward: bool) -> (String, Pos) {
630 let mut b = body(text);
631 let c = delete_word(&mut b, at, forward);
632 (b.text(), c)
633 }
634
635 #[test]
636 fn ctrl_backspace_like_powerpoint() {
637 let t = "ab cd...ef gh";
638 let want = [
639 ("ab cd...ef gh", 0),
640 ("b cd...ef gh", 0),
641 (" cd...ef gh", 0),
642 (" cd...ef gh", 0),
643 (" cd...ef gh", 0),
644 ("cd...ef gh", 0),
645 ("ab d...ef gh", 5),
646 ("ab ...ef gh", 5),
647 ("ab cd..ef gh", 7),
648 ("ab cd.ef gh", 7),
649 ("ab cdef gh", 7),
650 ("ab cd...f gh", 10),
651 ("ab cd... gh", 10),
652 ("ab cd...gh", 10),
653 ("ab cd...ef h", 13),
654 ("ab cd...ef ", 13),
655 ];
656 for (c, (txt, caret)) in want.iter().enumerate() {
657 assert_eq!(
658 del_word(t, (0, c), false),
659 (txt.to_string(), (0, *caret)),
660 "caret {c}"
661 );
662 }
663 let w = "Hello world foo, bar\nnext para";
664 assert_eq!(
665 del_word(w, (0, 12), false),
666 ("Hello foo, bar\nnext para".into(), (0, 6))
667 );
668 assert_eq!(
669 del_word(w, (0, 17), false),
670 ("Hello world foo bar\nnext para".into(), (0, 16))
671 );
672 assert_eq!(
674 del_word(w, (1, 0), false),
675 ("Hello world foo, barnext para".into(), (0, 21))
676 );
677 assert_eq!(
678 del_word("ab\n cd", (1, 2), false),
679 ("ab\ncd".into(), (1, 0))
680 );
681 let mut b = body("ab");
683 insert(&mut b, (0, 2), "\u{b}cd", None);
684 assert_eq!(delete_word(&mut b, (0, 3), false), (0, 2));
685 assert_eq!(b.text(), "abcd");
686 let mut b = body("ab");
687 insert(&mut b, (0, 2), "\u{b}cd", None);
688 assert_eq!(delete_word(&mut b, (0, 2), true), (0, 2));
689 assert_eq!(b.text(), "abcd");
690 assert_eq!(del_word("x", (0, 0), false), ("x".into(), (0, 0)));
691 assert_eq!(
693 del_word("a 👨\u{200d}👩\u{200d}👧", (0, 7), false),
694 ("a ".into(), (0, 2))
695 );
696 assert_eq!(
697 del_word("ce\u{301}te x", (0, 5), false),
698 (" x".into(), (0, 0))
699 );
700 assert_eq!(
701 del_word("ce\u{301}te x", (0, 6), false),
702 ("x".into(), (0, 0))
703 );
704 assert_eq!(del_word("日本語", (0, 3), false), ("日本".into(), (0, 2)));
706 }
707
708 #[test]
709 fn ctrl_delete_like_powerpoint() {
710 let t = "ab cd...ef gh";
711 let want = [
712 ("cd...ef gh", 0),
713 ("a cd...ef gh", 1),
714 ("ab...ef gh", 2),
715 ("ab cd...ef gh", 3),
716 ("ab cd...ef gh", 4),
717 ("ab...ef gh", 2),
718 ("ab c...ef gh", 6),
719 ("ab cdef gh", 7),
720 ("ab cd. ef gh", 9),
721 ("ab cd.. ef gh", 10),
722 ("ab cd... gh", 11),
723 ("ab cd...e gh", 11),
724 ("ab cd...ef", 12),
725 ("ab cd...ef", 12),
726 ("ab cd...ef g", 14),
727 ("ab cd...ef gh", 15),
728 ];
729 for (c, (txt, caret)) in want.iter().enumerate() {
730 assert_eq!(
731 del_word(t, (0, c), true),
732 (txt.to_string(), (0, *caret)),
733 "caret {c}"
734 );
735 }
736 let w = "Hello world foo, bar\nnext para";
737 let want = [
738 (0, "world foo, bar\nnext para", 0),
739 (2, "He world foo, bar\nnext para", 2),
740 (5, "Hello foo, bar\nnext para", 5),
741 (6, "Hello foo, bar\nnext para", 6),
742 (8, "Hello wo foo, bar\nnext para", 8),
743 (11, "Hello world, bar\nnext para", 11),
744 (12, "Hello world foo, bar\nnext para", 12),
745 (16, "Hello world foobar\nnext para", 16),
746 (17, "Hello world foo,\nnext para", 17),
747 (18, "Hello world foo, \nnext para", 18),
748 (21, "Hello world foo, barnext para", 21),
749 ];
750 for (c, txt, caret) in want {
751 assert_eq!(
752 del_word(w, (0, c), true),
753 (txt.to_string(), (0, caret)),
754 "caret {c}"
755 );
756 }
757 assert_eq!(
758 del_word(w, (1, 0), true),
759 ("Hello world foo, bar\npara".into(), (1, 0))
760 );
761 assert_eq!(
762 del_word(w, (1, 2), true),
763 ("Hello world foo, bar\nne para".into(), (1, 2))
764 );
765 assert_eq!(
766 del_word("ab \ncd", (0, 2), true),
767 ("ab\ncd".into(), (0, 2))
768 );
769 assert_eq!(
770 del_word("ab \ncd", (0, 3), true),
771 ("ab\ncd".into(), (0, 2))
772 );
773 assert_eq!(del_word("ab\t cd", (0, 2), true), ("ab cd".into(), (0, 2)));
774 assert_eq!(
775 del_word("ab cd(x) ef", (0, 3), true),
776 ("ab (x) ef".into(), (0, 3))
777 );
778 assert_eq!(
779 del_word("ab cd, ef", (0, 3), true),
780 ("ab, ef".into(), (0, 2))
781 );
782 assert_eq!(
783 del_word("ab. (cd) ef", (0, 5), true),
784 ("ab. () ef".into(), (0, 5))
785 );
786 assert_eq!(
787 del_word("ab \"cd\" ef", (0, 4), true),
788 ("ab \"\" ef".into(), (0, 4))
789 );
790 assert_eq!(del_word("ab, cd", (0, 4), true), ("ab, ".into(), (0, 4)));
791 assert_eq!(
793 del_word("مرحبا بالعالم hello", (0, 6), true),
794 ("مرحبا hello".into(), (0, 6))
795 );
796 assert_eq!(
797 del_word("مرحبا بالعالم hello", (0, 8), false),
798 ("مرحبا لعالم hello".into(), (0, 6))
799 );
800 assert_eq!(del_word("3.x 14", (0, 2), true), ("3.14".into(), (0, 2)));
802 assert_eq!(del_word("ab", (9, 9), true).0, "ab");
804 assert_eq!(del_word("", (0, 0), false).0, "");
805 let mut empty = TextBody::default();
806 assert_eq!(delete_word(&mut empty, (3, 3), true), (0, 0));
807 }
808
809 #[test]
810 fn paragraph_moves() {
811 let b = body("one two\nthree\n\nfour");
812 assert_eq!(para_down(&b, (0, 2)), (1, 0));
813 assert_eq!(para_down(&b, (1, 0)), (2, 0));
814 assert_eq!(para_down(&b, (3, 1)), (3, 4));
815 assert_eq!(para_up(&b, (3, 2)), (3, 0));
816 assert_eq!(para_up(&b, (3, 0)), (2, 0));
817 assert_eq!(para_up(&b, (0, 0)), (0, 0));
818 assert_eq!(word_move(&b, (0, 4), true), (0, 7));
819 assert_eq!(word_move(&b, (0, 7), true), (1, 0));
820 assert_eq!(word_move(&b, (1, 0), false), (0, 7));
821 assert_eq!(word_move(&b, (2, 0), true), (3, 0));
822 assert_eq!(word_move(&b, (3, 4), true), (3, 4));
823 assert_eq!(word_move(&b, (0, 0), false), (0, 0));
824 }
825
826 #[test]
827 fn case_cycle() {
828 assert_eq!(next_case("hello world"), "title");
829 assert_eq!(next_case("Hello World"), "upper");
830 assert_eq!(next_case("HELLO WORLD"), "lower");
831 assert_eq!(next_case("Hello world"), "upper");
832 assert_eq!(next_case("hELLO wORLD"), "upper");
833 assert_eq!(next_case("hello. world is HERE"), "upper");
834 assert_eq!(next_case("123"), "upper");
835 }
836}