sway_error/
formatting.rs

1//! This module contains various helper functions for easier formatting and creation of user-friendly messages.
2
3use std::{
4    borrow::Cow,
5    cmp::{self, Ordering},
6    fmt::{self, Display},
7};
8
9use sway_types::{SourceEngine, SourceId};
10
11/// Returns the file name (with extension) for the provided `source_id`,
12/// or `None` if the `source_id` is `None` or the file name cannot be
13/// obtained.
14pub fn get_file_name(source_engine: &SourceEngine, source_id: Option<&SourceId>) -> Option<String> {
15    match source_id {
16        Some(source_id) => source_engine.get_file_name(source_id),
17        None => None,
18    }
19}
20
21/// Returns reading-friendly textual representation for `number` smaller than or equal to 10
22/// or its numeric representation if it is greater than 10.
23pub fn number_to_str(number: usize) -> String {
24    match number {
25        0 => "zero".to_string(),
26        1 => "one".to_string(),
27        2 => "two".to_string(),
28        3 => "three".to_string(),
29        4 => "four".to_string(),
30        5 => "five".to_string(),
31        6 => "six".to_string(),
32        7 => "seven".to_string(),
33        8 => "eight".to_string(),
34        9 => "nine".to_string(),
35        10 => "ten".to_string(),
36        _ => format!("{number}"),
37    }
38}
39
40pub enum Enclosing {
41    #[allow(dead_code)]
42    None,
43    DoubleQuote,
44}
45
46impl Display for Enclosing {
47    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
48        write!(
49            f,
50            "{}",
51            match self {
52                Self::None => "",
53                Self::DoubleQuote => "\"",
54            },
55        )
56    }
57}
58
59pub enum Indent {
60    #[allow(dead_code)]
61    None,
62    Single,
63    Double,
64}
65
66impl Display for Indent {
67    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
68        write!(
69            f,
70            "{}",
71            match self {
72                Self::None => "",
73                Self::Single => "  ",
74                Self::Double => "    ",
75            },
76        )
77    }
78}
79
80/// Returns reading-friendly textual representation of the `sequence`, with comma-separated
81/// items and each item optionally enclosed in the specified `enclosing`.
82/// If the sequence has more than `max_items` the remaining items are replaced
83/// with the text "and <number> more".
84///
85/// E.g.:
86/// - \[a\] => "a"
87/// - \[a, b\] => "a" and "b"
88/// - \[a, b, c\] => "a", "b" and "c"
89/// - \[a, b, c, d\] => "a", "b", "c" and one more
90/// - \[a, b, c, d, e\] => "a", "b", "c" and two more
91///
92/// Panics if the `sequence` is empty, or `max_items` is zero.
93pub fn sequence_to_str<T>(sequence: &[T], enclosing: Enclosing, max_items: usize) -> String
94where
95    T: Display,
96{
97    sequence_to_str_impl(sequence, enclosing, max_items, "and")
98}
99
100/// Returns reading-friendly textual representation of the `sequence`, with comma-separated
101/// items and each item optionally enclosed in the specified `enclosing`.
102/// If the sequence has more than `max_items` the remaining items are replaced
103/// with the text "or <number> more".
104///
105/// E.g.:
106/// - \[a\] => "a"
107/// - \[a, b\] => "a" or "b"
108/// - \[a, b, c\] => "a", "b" or "c"
109/// - \[a, b, c, d\] => "a", "b", "c" or one more
110/// - \[a, b, c, d, e\] => "a", "b", "c" or two more
111///
112/// Panics if the `sequence` is empty, or `max_items` is zero.
113pub fn sequence_to_str_or<T>(sequence: &[T], enclosing: Enclosing, max_items: usize) -> String
114where
115    T: Display,
116{
117    sequence_to_str_impl(sequence, enclosing, max_items, "or")
118}
119
120fn sequence_to_str_impl<T>(
121    sequence: &[T],
122    enclosing: Enclosing,
123    max_items: usize,
124    and_or: &str,
125) -> String
126where
127    T: Display,
128{
129    assert!(
130        !sequence.is_empty(),
131        "Sequence to display must not be empty."
132    );
133    assert!(
134        max_items > 0,
135        "Maximum number of items to display must be greater than zero."
136    );
137
138    let max_items = cmp::min(max_items, sequence.len());
139
140    let (to_display, remaining) = sequence.split_at(max_items);
141
142    let fmt_item = |item: &T| format!("{enclosing}{item}{enclosing}");
143
144    if !remaining.is_empty() {
145        format!(
146            "{}, {} {} more",
147            to_display
148                .iter()
149                .map(fmt_item)
150                .collect::<Vec<_>>()
151                .join(", "),
152            and_or,
153            number_to_str(remaining.len())
154        )
155    } else {
156        match to_display {
157            [] => unreachable!("There must be at least one item in the sequence."),
158            [item] => fmt_item(item),
159            [first_item, second_item] => {
160                format!(
161                    "{} {} {}",
162                    fmt_item(first_item),
163                    and_or,
164                    fmt_item(second_item)
165                )
166            }
167            _ => format!(
168                "{}, {} {}",
169                to_display
170                    .split_last()
171                    .unwrap()
172                    .1
173                    .iter()
174                    .map(fmt_item)
175                    .collect::<Vec::<_>>()
176                    .join(", "),
177                and_or,
178                fmt_item(to_display.last().unwrap())
179            ),
180        }
181    }
182}
183
184/// Returns reading-friendly textual representation of the `sequence`, with vertically
185/// listed items and each item indented for the `indent` and preceded with the dash (-).
186/// If the sequence has more than `max_items` the remaining items are replaced
187/// with the text "and <number> more".
188///
189/// E.g.:
190/// * \[a\] =>
191///     - a
192/// * \[a, b\] =>
193///     - a
194///     - b
195/// * \[a, b, c, d, e\] =>
196///     - a
197///     - b
198///     - and three more
199///
200/// Panics if the `sequence` is empty, or `max_items` is zero.
201pub fn sequence_to_list<T>(sequence: &[T], indent: Indent, max_items: usize) -> Vec<String>
202where
203    T: Display,
204{
205    assert!(
206        !sequence.is_empty(),
207        "Sequence to display must not be empty."
208    );
209    assert!(
210        max_items > 0,
211        "Maximum number of items to display must be greater than zero."
212    );
213
214    let mut result = vec![];
215
216    let max_items = cmp::min(max_items, sequence.len());
217    let (to_display, remaining) = sequence.split_at(max_items);
218    for item in to_display {
219        result.push(format!("{indent}- {item}"));
220    }
221    if !remaining.is_empty() {
222        result.push(format!(
223            "{indent}- and {} more",
224            number_to_str(remaining.len())
225        ));
226    }
227
228    result
229}
230
231/// Returns "s" if `count` is different than 1, otherwise empty string.
232/// Convenient for building simple plural of words.
233pub fn plural_s(count: usize) -> &'static str {
234    if count == 1 {
235        ""
236    } else {
237        "s"
238    }
239}
240
241/// Returns "is" if `count` is 1, otherwise "are".
242pub fn is_are(count: usize) -> &'static str {
243    if count == 1 {
244        "is"
245    } else {
246        "are"
247    }
248}
249
250/// Returns `singular` if `count` is 1, otherwise `plural`.
251pub fn singular_plural<'a>(count: usize, singular: &'a str, plural: &'a str) -> &'a str {
252    if count == 1 {
253        singular
254    } else {
255        plural
256    }
257}
258
259/// Returns the suffix of the `call_path` together with any type arguments if they
260/// exist.
261/// Convenient for subsequent showing of only the short name of a full name that was
262/// already shown.
263///
264/// E.g.:
265/// SomeName -> SomeName
266/// SomeName<T> -> SomeName<T>
267/// std::ops::Eq -> Eq
268/// some_lib::Struct<A, B> -> Struct<A, B>
269pub fn call_path_suffix_with_args(call_path: &String) -> Cow<String> {
270    match call_path.rfind(':') {
271        Some(index) if index < call_path.len() - 1 => {
272            Cow::Owned(call_path.split_at(index + 1).1.to_string())
273        }
274        _ => Cow::Borrowed(call_path),
275    }
276}
277
278/// Returns indefinite article "a" or "an" that corresponds to the `word`,
279/// or an empty string if the indefinite article do not fit to the word.
280///
281/// Note that the function does not recognize plurals and assumes that the
282/// `word` is in singular.
283///
284/// If an article is returned, it is followed by a space, e.g. "a ".
285pub fn a_or_an(word: &'static str) -> &'static str {
286    let is_a = in_definite::is_an(word);
287    match is_a {
288        in_definite::Is::An => "an ",
289        in_definite::Is::A => "a ",
290        in_definite::Is::None => "",
291    }
292}
293
294/// Returns `text` with the first character turned into ASCII uppercase.
295pub fn ascii_sentence_case(text: &String) -> Cow<String> {
296    if text.is_empty() || text.chars().next().unwrap().is_uppercase() {
297        Cow::Borrowed(text)
298    } else {
299        let mut result = text.clone();
300        result[0..1].make_ascii_uppercase();
301        Cow::Owned(result.to_owned())
302    }
303}
304
305/// Returns the first line in `text`, up to the first `\n` if the `text` contains
306/// multiple lines, and optionally adds ellipses "..." to the end of the line
307/// if `with_ellipses` is true.
308///
309/// If the `text` is a single-line string, returns the original `text`.
310///
311/// Suitable for showing just the first line of a piece of code.
312/// E.g., if `text` is:
313///   if x {
314///     0
315///   } else {
316///     1
317///   }
318///  the returned value, with ellipses, will be:
319///   if x {...
320pub fn first_line(text: &str, with_ellipses: bool) -> Cow<str> {
321    if !text.contains('\n') {
322        Cow::Borrowed(text)
323    } else {
324        let index_of_new_line = text.find('\n').unwrap();
325        Cow::Owned(text[..index_of_new_line].to_string() + if with_ellipses { "..." } else { "" })
326    }
327}
328
329/// Finds strings from an iterable of `possible_values` similar to a given value `v`.
330/// Returns a vector of all possible values that exceed a similarity threshold,
331/// sorted by similarity (most similar comes first). The returned vector will have
332/// at most `max_num_of_suggestions` elements.
333///
334/// The implementation is taken and adapted from the [Clap project](https://github.com/clap-rs/clap/blob/50f7646cf72dd7d4e76d9284d76bdcdaceb7c049/clap_builder/src/parser/features/suggestions.rs#L11).
335pub fn did_you_mean<T, I>(v: &str, possible_values: I, max_num_of_suggestions: usize) -> Vec<String>
336where
337    T: AsRef<str>,
338    I: IntoIterator<Item = T>,
339{
340    let mut candidates: Vec<_> = possible_values
341        .into_iter()
342        .map(|pv| (strsim::jaro(v, pv.as_ref()), pv.as_ref().to_owned()))
343        // Confidence of 0.7 so that bar -> baz is suggested.
344        .filter(|(confidence, _)| *confidence > 0.7)
345        .collect();
346    candidates.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(Ordering::Equal));
347    candidates
348        .into_iter()
349        .take(max_num_of_suggestions)
350        .map(|(_, pv)| pv)
351        .collect()
352}