sl_up/
parser.rs

1use ansi_parser::{AnsiParser, AnsiSequence, Output};
2
3use crate::graph::{Commit, Glyph, Item, ItemType};
4
5const SELECTION_COLOR_CODE: u8 = 35;
6
7pub struct SmartLogParser {}
8impl SmartLogParser {
9    pub fn parse(raw_lines: &[String]) -> Option<Vec<ItemType>> {
10        let mut items: Vec<ItemType> = Vec::new();
11        let mut parsed_lines: Vec<Vec<Output>> =
12            raw_lines.iter().map(|x| x.ansi_parse().collect()).collect();
13
14        while !parsed_lines.is_empty() {
15            let mut line = parsed_lines.remove(0);
16            Self::pre_process_line(&mut line);
17
18            if Self::is_commit_line(&line) {
19                // commit hash and metadata
20                let selected = Self::has_line_selection_coloring(&line);
21                items.push(
22                    Commit::new(vec![Self::parsed_line_to_string_vec(&line)], selected).into(),
23                );
24            } else if Self::parsed_line_to_string(&line).trim().contains(' ') {
25                // commit message
26                items
27                    .last_mut()
28                    .unwrap()
29                    .add_parsed_line(Self::parsed_line_to_string_vec(&line));
30            } else {
31                // only a graph element
32                items.push(Glyph::new(vec![Self::parsed_line_to_string_vec(&line)]).into());
33            }
34        }
35        Some(items)
36    }
37
38    pub fn parsed_line_to_string(line: &[Output]) -> String {
39        Self::parsed_line_to_string_vec(line).join("")
40    }
41
42    pub fn parsed_line_to_string_vec(line: &[Output]) -> Vec<String> {
43        line.iter().map(|x| x.to_string()).collect()
44    }
45
46    pub fn has_line_selection_coloring(line: &[Output]) -> bool {
47        for block in line.iter() {
48            match block {
49                Output::Escape(AnsiSequence::SetGraphicsMode(codes)) => {
50                    if codes.contains(&SELECTION_COLOR_CODE) {
51                        return true;
52                    }
53                }
54                Output::TextBlock(text) => {
55                    if text.contains("\u{1b}[0;35m") {
56                        return false;
57                    }
58                }
59                _ => {}
60            }
61        }
62        false
63    }
64
65    fn is_commit_line(line: &[Output]) -> bool {
66        let mut first_text_block =
67            Self::get_first_text_block_contents(line).unwrap_or("".to_string());
68
69        first_text_block = first_text_block.trim().to_string();
70        if first_text_block.chars().collect::<Vec<char>>().len() == 3
71            && first_text_block.contains(' ')
72        {
73            first_text_block = first_text_block.split(' ').last().unwrap().to_string();
74        }
75
76        if ["@", "o"].contains(&first_text_block.as_str()) {
77            return true;
78        }
79        false
80    }
81
82    fn get_first_text_block_contents(line: &[Output]) -> Option<String> {
83        for block in line.iter() {
84            if let Output::TextBlock(text) = block {
85                return Some(text.trim().to_string());
86            }
87        }
88        None
89    }
90
91    fn pre_process_line(line: &mut Vec<Output>) {
92        if line.len() == 1 {
93            if let Output::TextBlock(text) = &line[0] {
94                let (graph, new_text) = Self::split_graph_from_text(text).unwrap();
95                line[0] = Output::TextBlock(graph);
96                line.push(Output::TextBlock(new_text))
97            }
98        }
99    }
100
101    fn split_graph_from_text(text: &str) -> Option<(&str, &str)> {
102        let mut idx = 0;
103        let mut found = false;
104        for (i, char) in text.char_indices() {
105            if found {
106                idx = i;
107                break;
108            }
109            if ["│", "╯", "╷"].contains(&char.to_string().as_str()) {
110                found = true;
111            }
112        }
113        Some(text.split_at(idx))
114    }
115}
116
117#[cfg(test)]
118mod tests {
119
120    use super::*;
121
122    const RAW_LINES: [&str; 15] = [
123        "  @  \u{1b}[0;35m\u{1b}[0;93;1m1cee5d55e\u{1b}[0m\u{1b}[0;35m  Dec 08 at 09:46  royrothenberg  \u{1b}[0;36m#780 Closed\u{1b}[0m\u{1b}[0;35m \u{1b}[0;31m✗\u{1b}[0m",
124        "  │  \u{1b}[0;35m[pr body update] update stack list without overwriting PR title and body\u{1b}[0m",
125        "  │",
126        "  o  \u{1b}[0;93;1mc3bd9e5fa\u{1b}[0m  Dec 08 at 09:46  royrothenberg  \u{1b}[0;38;2;141;148;158m#779 Unreviewed\u{1b}[0m \u{1b}[0;31m✗\u{1b}[0m",
127        "╭─╯  [pr body update] fix reviewstack option breaking stack list detection",
128        "│",
129        "o  \u{1b}[0;33mba27d4d13\u{1b}[0m  Dec 07 at 22:20  \u{1b}[0;32mremote/main\u{1b}[0m",
130        "╷",
131        "╷ o  \u{1b}[0;93;1m2f85065e7\u{1b}[0m  Nov 28 at 11:49  royrothenberg  \u{1b}[0;36m#781 Closed\u{1b}[0m \u{1b}[0;32m✓\u{1b}[0m",
132        "╭─╯  [isl] increase width of diff window in split stack edit panel",
133        "│",
134        "o  \u{1b}[0;33m0e069ab09\u{1b}[0m  Nov 21 at 13:16",
135        "│",
136        "~",
137        "",
138    ];
139
140    #[test]
141    fn graph_items() {
142        let items = SmartLogParser::parse(&raw_lines()).unwrap();
143        assert!(items.len() == 12);
144        assert_eq!(items[0].parsed_lines().len(), 2);
145        assert_eq!(items[1].parsed_lines().len(), 1);
146        let commit = if let ItemType::Commit(commit) = &items[0] {
147            commit
148        } else {
149            panic!("Expected GraphCommit");
150        };
151        assert!(commit.selected);
152    }
153
154    fn raw_lines() -> Vec<String> {
155        RAW_LINES.iter().map(|x| x.to_string()).collect()
156    }
157}