1pub mod arc_cow;
2pub mod fs;
3pub mod paths;
4pub mod serde;
5#[cfg(any(test, feature = "test-support"))]
6pub mod test;
7
8use futures::Future;
9use lazy_static::lazy_static;
10use rand::{seq::SliceRandom, Rng};
11use std::{
12 borrow::Cow,
13 cmp::{self, Ordering},
14 env,
15 ops::{AddAssign, Range, RangeInclusive},
16 panic::Location,
17 pin::Pin,
18 task::{Context, Poll},
19 time::Instant,
20};
21use unicase::UniCase;
22
23pub use take_until::*;
24
25#[macro_export]
26macro_rules! debug_panic {
27 ( $($fmt_arg:tt)* ) => {
28 if cfg!(debug_assertions) {
29 panic!( $($fmt_arg)* );
30 } else {
31 let backtrace = std::backtrace::Backtrace::capture();
32 log::error!("{}\n{:?}", format_args!($($fmt_arg)*), backtrace);
33 }
34 };
35}
36
37#[macro_export]
38macro_rules! with_clone {
39 ($i:ident, move ||$l:expr) => {{
40 let $i = $i.clone();
41 move || {
42 $l
43 }
44 }};
45 ($i:ident, move |$($k:pat_param),*|$l:expr) => {{
46 let $i = $i.clone();
47 move |$( $k ),*| {
48 $l
49 }
50 }};
51
52 (($($i:ident),+), move ||$l:expr) => {{
53 let ($($i),+) = ($($i.clone()),+);
54 move || {
55 $l
56 }
57 }};
58 (($($i:ident),+), move |$($k:pat_param),*|$l:expr) => {{
59 let ($($i),+) = ($($i.clone()),+);
60 move |$( $k ),*| {
61 $l
62 }
63 }};
64}
65
66mod test_with_clone {
67
68 // If this test compiles, it works
69 #[test]
70 fn test() {
71 let x = "String".to_string();
72 let y = std::sync::Arc::new(5);
73
74 fn no_arg(f: impl FnOnce()) {
75 f()
76 }
77
78 no_arg(with_clone!(x, move || {
79 drop(x);
80 }));
81
82 no_arg(with_clone!((x, y), move || {
83 drop(x);
84 drop(y);
85 }));
86
87 fn one_arg(f: impl FnOnce(usize)) {
88 f(1)
89 }
90
91 one_arg(with_clone!(x, move |_| {
92 drop(x);
93 }));
94 one_arg(with_clone!((x, y), move |b| {
95 drop(x);
96 drop(y);
97 println!("{}", b);
98 }));
99
100 fn two_arg(f: impl FnOnce(usize, bool)) {
101 f(5, true)
102 }
103
104 two_arg(with_clone!((x, y), move |a, b| {
105 drop(x);
106 drop(y);
107 println!("{}{}", a, b)
108 }));
109 two_arg(with_clone!((x, y), move |a, _| {
110 drop(x);
111 drop(y);
112 println!("{}", a)
113 }));
114 two_arg(with_clone!((x, y), move |_, b| {
115 drop(x);
116 drop(y);
117 println!("{}", b)
118 }));
119
120 struct Example {
121 z: usize,
122 }
123
124 fn destructuring_example(f: impl FnOnce(Example)) {
125 f(Example { z: 10 })
126 }
127
128 destructuring_example(with_clone!(x, move |Example { z }| {
129 drop(x);
130 println!("{}", z);
131 }));
132
133 let a_long_variable_1 = "".to_string();
134 let a_long_variable_2 = "".to_string();
135 let a_long_variable_3 = "".to_string();
136 let a_long_variable_4 = "".to_string();
137 two_arg(with_clone!(
138 (
139 x,
140 y,
141 a_long_variable_1,
142 a_long_variable_2,
143 a_long_variable_3,
144 a_long_variable_4
145 ),
146 move |a, b| {
147 drop(x);
148 drop(y);
149 drop(a_long_variable_1);
150 drop(a_long_variable_2);
151 drop(a_long_variable_3);
152 drop(a_long_variable_4);
153 println!("{}{}", a, b)
154 }
155 ));
156
157 fn single_expression_body(f: impl FnOnce(usize) -> usize) -> usize {
158 f(20)
159 }
160
161 let _result = single_expression_body(with_clone!(y, move |z| *y + z));
162
163 // Explicitly move all variables
164 drop(x);
165 drop(y);
166 drop(a_long_variable_1);
167 drop(a_long_variable_2);
168 drop(a_long_variable_3);
169 drop(a_long_variable_4);
170 }
171}
172
173pub fn truncate(s: &str, max_chars: usize) -> &str {
174 match s.char_indices().nth(max_chars) {
175 None => s,
176 Some((idx, _)) => &s[..idx],
177 }
178}
179
180/// Removes characters from the end of the string if its length is greater than `max_chars` and
181/// appends "..." to the string. Returns string unchanged if its length is smaller than max_chars.
182pub fn truncate_and_trailoff(s: &str, max_chars: usize) -> String {
183 debug_assert!(max_chars >= 5);
184
185 let truncation_ix = s.char_indices().map(|(i, _)| i).nth(max_chars);
186 match truncation_ix {
187 Some(length) => s[..length].to_string() + "…",
188 None => s.to_string(),
189 }
190}
191
192/// Removes characters from the front of the string if its length is greater than `max_chars` and
193/// prepends the string with "...". Returns string unchanged if its length is smaller than max_chars.
194pub fn truncate_and_remove_front(s: &str, max_chars: usize) -> String {
195 debug_assert!(max_chars >= 5);
196
197 let truncation_ix = s.char_indices().map(|(i, _)| i).nth_back(max_chars);
198 match truncation_ix {
199 Some(length) => "…".to_string() + &s[length..],
200 None => s.to_string(),
201 }
202}
203
204/// Takes only `max_lines` from the string and, if there were more than `max_lines-1`, appends a
205/// a newline and "..." to the string, so that `max_lines` are returned.
206/// Returns string unchanged if its length is smaller than max_lines.
207pub fn truncate_lines_and_trailoff(s: &str, max_lines: usize) -> String {
208 let mut lines = s.lines().take(max_lines).collect::<Vec<_>>();
209 if lines.len() > max_lines - 1 {
210 lines.pop();
211 lines.join("\n") + "\n…"
212 } else {
213 lines.join("\n")
214 }
215}
216
217pub fn post_inc<T: From<u8> + AddAssign<T> + Copy>(value: &mut T) -> T {
218 let prev = *value;
219 *value += T::from(1);
220 prev
221}
222
223/// Extend a sorted vector with a sorted sequence of items, maintaining the vector's sort order and
224/// enforcing a maximum length. This also de-duplicates items. Sort the items according to the given callback. Before calling this,
225/// both `vec` and `new_items` should already be sorted according to the `cmp` comparator.
226pub fn extend_sorted<T, I, F>(vec: &mut Vec<T>, new_items: I, limit: usize, mut cmp: F)
227where
228 I: IntoIterator<Item = T>,
229 F: FnMut(&T, &T) -> Ordering,
230{
231 let mut start_index = 0;
232 for new_item in new_items {
233 if let Err(i) = vec[start_index..].binary_search_by(|m| cmp(m, &new_item)) {
234 let index = start_index + i;
235 if vec.len() < limit {
236 vec.insert(index, new_item);
237 } else if index < vec.len() {
238 vec.pop();
239 vec.insert(index, new_item);
240 }
241 start_index = index;
242 }
243 }
244}
245
246/// Parse the result of calling `usr/bin/env` with no arguments
247pub fn parse_env_output(env: &str, mut f: impl FnMut(String, String)) {
248 let mut current_key: Option<String> = None;
249 let mut current_value: Option<String> = None;
250
251 for line in env.split_terminator('\n') {
252 if let Some(separator_index) = line.find('=') {
253 if &line[..separator_index] != "" {
254 if let Some((key, value)) = Option::zip(current_key.take(), current_value.take()) {
255 f(key, value)
256 }
257 current_key = Some(line[..separator_index].to_string());
258 current_value = Some(line[separator_index + 1..].to_string());
259 continue;
260 };
261 }
262 if let Some(value) = current_value.as_mut() {
263 value.push('\n');
264 value.push_str(line);
265 }
266 }
267 if let Some((key, value)) = Option::zip(current_key.take(), current_value.take()) {
268 f(key, value)
269 }
270}
271
272pub fn merge_json_value_into(source: serde_json::Value, target: &mut serde_json::Value) {
273 use serde_json::Value;
274
275 match (source, target) {
276 (Value::Object(source), Value::Object(target)) => {
277 for (key, value) in source {
278 if let Some(target) = target.get_mut(&key) {
279 merge_json_value_into(value, target);
280 } else {
281 target.insert(key.clone(), value);
282 }
283 }
284 }
285
286 (source, target) => *target = source,
287 }
288}
289
290pub fn merge_non_null_json_value_into(source: serde_json::Value, target: &mut serde_json::Value) {
291 use serde_json::Value;
292 if let Value::Object(source_object) = source {
293 let target_object = if let Value::Object(target) = target {
294 target
295 } else {
296 *target = Value::Object(Default::default());
297 target.as_object_mut().unwrap()
298 };
299 for (key, value) in source_object {
300 if let Some(target) = target_object.get_mut(&key) {
301 merge_non_null_json_value_into(value, target);
302 } else if !value.is_null() {
303 target_object.insert(key.clone(), value);
304 }
305 }
306 } else if !source.is_null() {
307 *target = source
308 }
309}
310
311pub fn measure<R>(label: &str, f: impl FnOnce() -> R) -> R {
312 lazy_static! {
313 pub static ref ZED_MEASUREMENTS: bool = env::var("ZED_MEASUREMENTS")
314 .map(|measurements| measurements == "1" || measurements == "true")
315 .unwrap_or(false);
316 }
317
318 if *ZED_MEASUREMENTS {
319 let start = Instant::now();
320 let result = f();
321 let elapsed = start.elapsed();
322 eprintln!("{}: {:?}", label, elapsed);
323 result
324 } else {
325 f()
326 }
327}
328
329pub trait ResultExt<E> {
330 type Ok;
331
332 fn log_err(self) -> Option<Self::Ok>;
333 /// Assert that this result should never be an error in development or tests.
334 fn debug_assert_ok(self, reason: &str) -> Self;
335 fn warn_on_err(self) -> Option<Self::Ok>;
336 fn inspect_error(self, func: impl FnOnce(&E)) -> Self;
337}
338
339impl<T, E> ResultExt<E> for Result<T, E>
340where
341 E: std::fmt::Debug,
342{
343 type Ok = T;
344
345 #[track_caller]
346 fn log_err(self) -> Option<T> {
347 match self {
348 Ok(value) => Some(value),
349 Err(error) => {
350 let caller = Location::caller();
351 log::error!("{}:{}: {:?}", caller.file(), caller.line(), error);
352 None
353 }
354 }
355 }
356
357 #[track_caller]
358 fn debug_assert_ok(self, reason: &str) -> Self {
359 if let Err(error) = &self {
360 debug_panic!("{reason} - {error:?}");
361 }
362 self
363 }
364
365 fn warn_on_err(self) -> Option<T> {
366 match self {
367 Ok(value) => Some(value),
368 Err(error) => {
369 log::warn!("{:?}", error);
370 None
371 }
372 }
373 }
374
375 /// https://doc.rust-lang.org/std/result/enum.Result.html#method.inspect_err
376 fn inspect_error(self, func: impl FnOnce(&E)) -> Self {
377 if let Err(err) = &self {
378 func(err);
379 }
380
381 self
382 }
383}
384
385pub trait TryFutureExt {
386 fn log_err(self) -> LogErrorFuture<Self>
387 where
388 Self: Sized;
389
390 fn log_tracked_err(self, location: core::panic::Location<'static>) -> LogErrorFuture<Self>
391 where
392 Self: Sized;
393
394 fn warn_on_err(self) -> LogErrorFuture<Self>
395 where
396 Self: Sized;
397 fn unwrap(self) -> UnwrapFuture<Self>
398 where
399 Self: Sized;
400}
401
402impl<F, T, E> TryFutureExt for F
403where
404 F: Future<Output = Result<T, E>>,
405 E: std::fmt::Debug,
406{
407 #[track_caller]
408 fn log_err(self) -> LogErrorFuture<Self>
409 where
410 Self: Sized,
411 {
412 let location = Location::caller();
413 LogErrorFuture(self, log::Level::Error, *location)
414 }
415
416 fn log_tracked_err(self, location: core::panic::Location<'static>) -> LogErrorFuture<Self>
417 where
418 Self: Sized,
419 {
420 LogErrorFuture(self, log::Level::Error, location)
421 }
422
423 #[track_caller]
424 fn warn_on_err(self) -> LogErrorFuture<Self>
425 where
426 Self: Sized,
427 {
428 let location = Location::caller();
429 LogErrorFuture(self, log::Level::Warn, *location)
430 }
431
432 fn unwrap(self) -> UnwrapFuture<Self>
433 where
434 Self: Sized,
435 {
436 UnwrapFuture(self)
437 }
438}
439
440#[must_use]
441pub struct LogErrorFuture<F>(F, log::Level, core::panic::Location<'static>);
442
443impl<F, T, E> Future for LogErrorFuture<F>
444where
445 F: Future<Output = Result<T, E>>,
446 E: std::fmt::Debug,
447{
448 type Output = Option<T>;
449
450 fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output> {
451 let level = self.1;
452 let location = self.2;
453 let inner = unsafe { Pin::new_unchecked(&mut self.get_unchecked_mut().0) };
454 match inner.poll(cx) {
455 Poll::Ready(output) => Poll::Ready(match output {
456 Ok(output) => Some(output),
457 Err(error) => {
458 log::log!(
459 level,
460 "{}:{}: {:?}",
461 location.file(),
462 location.line(),
463 error
464 );
465 None
466 }
467 }),
468 Poll::Pending => Poll::Pending,
469 }
470 }
471}
472
473pub struct UnwrapFuture<F>(F);
474
475impl<F, T, E> Future for UnwrapFuture<F>
476where
477 F: Future<Output = Result<T, E>>,
478 E: std::fmt::Debug,
479{
480 type Output = T;
481
482 fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output> {
483 let inner = unsafe { Pin::new_unchecked(&mut self.get_unchecked_mut().0) };
484 match inner.poll(cx) {
485 Poll::Ready(result) => Poll::Ready(result.unwrap()),
486 Poll::Pending => Poll::Pending,
487 }
488 }
489}
490
491pub struct Deferred<F: FnOnce()>(Option<F>);
492
493impl<F: FnOnce()> Deferred<F> {
494 /// Drop without running the deferred function.
495 pub fn abort(mut self) {
496 self.0.take();
497 }
498}
499
500impl<F: FnOnce()> Drop for Deferred<F> {
501 fn drop(&mut self) {
502 if let Some(f) = self.0.take() {
503 f()
504 }
505 }
506}
507
508/// Run the given function when the returned value is dropped (unless it's cancelled).
509#[must_use]
510pub fn defer<F: FnOnce()>(f: F) -> Deferred<F> {
511 Deferred(Some(f))
512}
513
514pub struct RandomCharIter<T: Rng> {
515 rng: T,
516 simple_text: bool,
517}
518
519impl<T: Rng> RandomCharIter<T> {
520 pub fn new(rng: T) -> Self {
521 Self {
522 rng,
523 simple_text: std::env::var("SIMPLE_TEXT").map_or(false, |v| !v.is_empty()),
524 }
525 }
526
527 pub fn with_simple_text(mut self) -> Self {
528 self.simple_text = true;
529 self
530 }
531}
532
533impl<T: Rng> Iterator for RandomCharIter<T> {
534 type Item = char;
535
536 fn next(&mut self) -> Option<Self::Item> {
537 if self.simple_text {
538 return if self.rng.gen_range(0..100) < 5 {
539 Some('\n')
540 } else {
541 Some(self.rng.gen_range(b'a'..b'z' + 1).into())
542 };
543 }
544
545 match self.rng.gen_range(0..100) {
546 // whitespace
547 0..=19 => [' ', '\n', '\r', '\t'].choose(&mut self.rng).copied(),
548 // two-byte greek letters
549 20..=32 => char::from_u32(self.rng.gen_range(('α' as u32)..('ω' as u32 + 1))),
550 // // three-byte characters
551 33..=45 => ['✋', '✅', '❌', '❎', '⭐']
552 .choose(&mut self.rng)
553 .copied(),
554 // // four-byte characters
555 46..=58 => ['🍐', '🏀', '🍗', '🎉'].choose(&mut self.rng).copied(),
556 // ascii letters
557 _ => Some(self.rng.gen_range(b'a'..b'z' + 1).into()),
558 }
559 }
560}
561
562/// Get an embedded file as a string.
563pub fn asset_str<A: rust_embed::RustEmbed>(path: &str) -> Cow<'static, str> {
564 match A::get(path).unwrap().data {
565 Cow::Borrowed(bytes) => Cow::Borrowed(std::str::from_utf8(bytes).unwrap()),
566 Cow::Owned(bytes) => Cow::Owned(String::from_utf8(bytes).unwrap()),
567 }
568}
569
570/// Expands to an immediately-invoked function expression. Good for using the ? operator
571/// in functions which do not return an Option or Result.
572///
573/// Accepts a normal block, an async block, or an async move block.
574#[macro_export]
575macro_rules! maybe {
576 ($block:block) => {
577 (|| $block)()
578 };
579 (async $block:block) => {
580 (|| async $block)()
581 };
582 (async move $block:block) => {
583 (|| async move $block)()
584 };
585}
586
587pub trait RangeExt<T> {
588 fn sorted(&self) -> Self;
589 fn to_inclusive(&self) -> RangeInclusive<T>;
590 fn overlaps(&self, other: &Range<T>) -> bool;
591 fn contains_inclusive(&self, other: &Range<T>) -> bool;
592}
593
594impl<T: Ord + Clone> RangeExt<T> for Range<T> {
595 fn sorted(&self) -> Self {
596 cmp::min(&self.start, &self.end).clone()..cmp::max(&self.start, &self.end).clone()
597 }
598
599 fn to_inclusive(&self) -> RangeInclusive<T> {
600 self.start.clone()..=self.end.clone()
601 }
602
603 fn overlaps(&self, other: &Range<T>) -> bool {
604 self.start < other.end && other.start < self.end
605 }
606
607 fn contains_inclusive(&self, other: &Range<T>) -> bool {
608 self.start <= other.start && other.end <= self.end
609 }
610}
611
612impl<T: Ord + Clone> RangeExt<T> for RangeInclusive<T> {
613 fn sorted(&self) -> Self {
614 cmp::min(self.start(), self.end()).clone()..=cmp::max(self.start(), self.end()).clone()
615 }
616
617 fn to_inclusive(&self) -> RangeInclusive<T> {
618 self.clone()
619 }
620
621 fn overlaps(&self, other: &Range<T>) -> bool {
622 self.start() < &other.end && &other.start <= self.end()
623 }
624
625 fn contains_inclusive(&self, other: &Range<T>) -> bool {
626 self.start() <= &other.start && &other.end <= self.end()
627 }
628}
629
630/// A way to sort strings with starting numbers numerically first, falling back to alphanumeric one,
631/// case-insensitive.
632///
633/// This is useful for turning regular alphanumerically sorted sequences as `1-abc, 10, 11-def, .., 2, 21-abc`
634/// into `1-abc, 2, 10, 11-def, .., 21-abc`
635#[derive(Debug, PartialEq, Eq)]
636pub struct NumericPrefixWithSuffix<'a>(i32, &'a str);
637
638impl<'a> NumericPrefixWithSuffix<'a> {
639 pub fn from_numeric_prefixed_str(str: &'a str) -> Option<Self> {
640 let i = str.chars().take_while(|c| c.is_ascii_digit()).count();
641 let (prefix, remainder) = str.split_at(i);
642
643 match prefix.parse::<i32>() {
644 Ok(prefix) => Some(NumericPrefixWithSuffix(prefix, remainder)),
645 Err(_) => None,
646 }
647 }
648}
649
650impl Ord for NumericPrefixWithSuffix<'_> {
651 fn cmp(&self, other: &Self) -> Ordering {
652 let NumericPrefixWithSuffix(num_a, remainder_a) = self;
653 let NumericPrefixWithSuffix(num_b, remainder_b) = other;
654 num_a
655 .cmp(num_b)
656 .then_with(|| UniCase::new(remainder_a).cmp(&UniCase::new(remainder_b)))
657 }
658}
659
660impl<'a> PartialOrd for NumericPrefixWithSuffix<'a> {
661 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
662 Some(self.cmp(other))
663 }
664}
665lazy_static! {
666 static ref EMOJI_REGEX: regex::Regex = regex::Regex::new("(\\p{Emoji}|\u{200D})").unwrap();
667}
668
669/// Returns true if the given string consists of emojis only.
670/// E.g. "👨👩👧👧👋" will return true, but "👋!" will return false.
671pub fn word_consists_of_emojis(s: &str) -> bool {
672 let mut prev_end = 0;
673 for capture in EMOJI_REGEX.find_iter(s) {
674 if capture.start() != prev_end {
675 return false;
676 }
677 prev_end = capture.end();
678 }
679 prev_end == s.len()
680}
681
682#[cfg(test)]
683mod tests {
684 use super::*;
685
686 #[test]
687 fn test_extend_sorted() {
688 let mut vec = vec![];
689
690 extend_sorted(&mut vec, vec![21, 17, 13, 8, 1, 0], 5, |a, b| b.cmp(a));
691 assert_eq!(vec, &[21, 17, 13, 8, 1]);
692
693 extend_sorted(&mut vec, vec![101, 19, 17, 8, 2], 8, |a, b| b.cmp(a));
694 assert_eq!(vec, &[101, 21, 19, 17, 13, 8, 2, 1]);
695
696 extend_sorted(&mut vec, vec![1000, 19, 17, 9, 5], 8, |a, b| b.cmp(a));
697 assert_eq!(vec, &[1000, 101, 21, 19, 17, 13, 9, 8]);
698 }
699
700 #[test]
701 fn test_iife() {
702 fn option_returning_function() -> Option<()> {
703 None
704 }
705
706 let foo = maybe!({
707 option_returning_function()?;
708 Some(())
709 });
710
711 assert_eq!(foo, None);
712 }
713
714 #[test]
715 fn test_trancate_and_trailoff() {
716 assert_eq!(truncate_and_trailoff("", 5), "");
717 assert_eq!(truncate_and_trailoff("èèèèèè", 7), "èèèèèè");
718 assert_eq!(truncate_and_trailoff("èèèèèè", 6), "èèèèèè");
719 assert_eq!(truncate_and_trailoff("èèèèèè", 5), "èèèèè…");
720 }
721
722 #[test]
723 fn test_numeric_prefix_str_method() {
724 let target = "1a";
725 assert_eq!(
726 NumericPrefixWithSuffix::from_numeric_prefixed_str(target),
727 Some(NumericPrefixWithSuffix(1, "a"))
728 );
729
730 let target = "12ab";
731 assert_eq!(
732 NumericPrefixWithSuffix::from_numeric_prefixed_str(target),
733 Some(NumericPrefixWithSuffix(12, "ab"))
734 );
735
736 let target = "12_ab";
737 assert_eq!(
738 NumericPrefixWithSuffix::from_numeric_prefixed_str(target),
739 Some(NumericPrefixWithSuffix(12, "_ab"))
740 );
741
742 let target = "1_2ab";
743 assert_eq!(
744 NumericPrefixWithSuffix::from_numeric_prefixed_str(target),
745 Some(NumericPrefixWithSuffix(1, "_2ab"))
746 );
747
748 let target = "1.2";
749 assert_eq!(
750 NumericPrefixWithSuffix::from_numeric_prefixed_str(target),
751 Some(NumericPrefixWithSuffix(1, ".2"))
752 );
753
754 let target = "1.2_a";
755 assert_eq!(
756 NumericPrefixWithSuffix::from_numeric_prefixed_str(target),
757 Some(NumericPrefixWithSuffix(1, ".2_a"))
758 );
759
760 let target = "12.2_a";
761 assert_eq!(
762 NumericPrefixWithSuffix::from_numeric_prefixed_str(target),
763 Some(NumericPrefixWithSuffix(12, ".2_a"))
764 );
765
766 let target = "12a.2_a";
767 assert_eq!(
768 NumericPrefixWithSuffix::from_numeric_prefixed_str(target),
769 Some(NumericPrefixWithSuffix(12, "a.2_a"))
770 );
771 }
772
773 #[test]
774 fn test_numeric_prefix_with_suffix() {
775 let mut sorted = vec!["1-abc", "10", "11def", "2", "21-abc"];
776 sorted.sort_by_key(|s| {
777 NumericPrefixWithSuffix::from_numeric_prefixed_str(s).unwrap_or_else(|| {
778 panic!("Cannot convert string `{s}` into NumericPrefixWithSuffix")
779 })
780 });
781 assert_eq!(sorted, ["1-abc", "2", "10", "11def", "21-abc"]);
782
783 for numeric_prefix_less in ["numeric_prefix_less", "aaa", "~™£"] {
784 assert_eq!(
785 NumericPrefixWithSuffix::from_numeric_prefixed_str(numeric_prefix_less),
786 None,
787 "String without numeric prefix `{numeric_prefix_less}` should not be converted into NumericPrefixWithSuffix"
788 )
789 }
790 }
791
792 #[test]
793 fn test_word_consists_of_emojis() {
794 let words_to_test = vec![
795 ("👨👩👧👧👋🥒", true),
796 ("👋", true),
797 ("!👋", false),
798 ("👋!", false),
799 ("👋 ", false),
800 (" 👋", false),
801 ("Test", false),
802 ];
803
804 for (text, expected_result) in words_to_test {
805 assert_eq!(word_consists_of_emojis(text), expected_result);
806 }
807 }
808
809 #[test]
810 fn test_truncate_lines_and_trailoff() {
811 let text = r#"Line 1
812Line 2
813Line 3"#;
814
815 assert_eq!(
816 truncate_lines_and_trailoff(text, 2),
817 r#"Line 1
818…"#
819 );
820
821 assert_eq!(
822 truncate_lines_and_trailoff(text, 3),
823 r#"Line 1
824Line 2
825…"#
826 );
827
828 assert_eq!(
829 truncate_lines_and_trailoff(text, 4),
830 r#"Line 1
831Line 2
832Line 3"#
833 );
834 }
835}