summaryrefslogtreecommitdiffstats
path: root/src/format.rs
blob: 1f8bfb005bde4fafd0ffb49631a62a6cb25d6890 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
use regex::Regex;
use unicode_segmentation::UnicodeSegmentation;

#[derive(Debug, Default, PartialEq)]
pub struct FormatStringPlaceholderData<'a> {
    pub prefix: &'a str,
    pub placeholder: Option<&'a str>,
    pub alignment_spec: Option<&'a str>,
    pub width: Option<usize>,
    pub suffix: &'a str,
    pub prefix_len: usize,
    pub suffix_len: usize,
}

impl<'a> FormatStringPlaceholderData<'a> {
    pub fn width(&self, hunk_max_line_number_width: usize) -> (usize, usize) {
        // Only if Some(placeholder) is present will there be a number formatted
        // by this placeholder, if not width is also None.
        (
            self.prefix_len
                + std::cmp::max(
                    self.placeholder.map_or(0, |_| hunk_max_line_number_width),
                    self.width.unwrap_or(0),
                ),
            self.suffix_len,
        )
    }
}

pub type FormatStringData<'a> = Vec<FormatStringPlaceholderData<'a>>;

pub fn make_placeholder_regex(labels: &[&str]) -> Regex {
    Regex::new(&format!(
        r"(?x)
    \{{
    ({})            # 1: Placeholder labels
    (?:             # Start optional format spec (non-capturing)
      :             #     Literal colon
      (?:           #     Start optional fill/alignment spec (non-capturing)
        ([^<^>])?   #         2: Optional fill character (ignored)
        ([<^>])     #         3: Alignment spec
      )?            #
      (\d+)         #     4: Width
    )?              #
    \}}
    ",
        labels.join("|")
    ))
    .unwrap()
}

pub fn parse_line_number_format<'a>(
    format_string: &'a str,
    placeholder_regex: &Regex,
) -> FormatStringData<'a> {
    let mut format_data = Vec::new();
    let mut offset = 0;

    for captures in placeholder_regex.captures_iter(format_string) {
        let _match = captures.get(0).unwrap();
        let prefix = &format_string[offset.._match.start()];
        let suffix = &format_string[_match.end()..];
        format_data.push(FormatStringPlaceholderData {
            prefix,
            placeholder: captures.get(1).map(|m| m.as_str()),
            alignment_spec: captures.get(3).map(|m| m.as_str()),
            width: captures.get(4).map(|m| {
                m.as_str()
                    .parse()
                    .unwrap_or_else(|_| panic!("Invalid width in format string: {}", format_string))
            }),
            suffix,
            prefix_len: prefix.graphemes(true).count(),
            suffix_len: suffix.graphemes(true).count(),
        });
        offset = _match.end();
    }
    if offset == 0 {
        // No placeholders
        format_data.push(FormatStringPlaceholderData {
            prefix: &format_string[..0],
            placeholder: None,
            alignment_spec: None,
            width: None,
            suffix: &format_string[0..],
            prefix_len: 0,
            suffix_len: format_string.graphemes(true).count(),
        })
    }
    format_data
}

pub fn pad(s: &str, width: usize, alignment: &str) -> String {
    match alignment {
        "<" => format!("{0:<1$}", s, width),
        "^" => format!("{0:^1$}", s, width),
        ">" => format!("{0:>1$}", s, width),
        _ => unreachable!(),
    }
}