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