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