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 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 items
27 .last_mut()
28 .unwrap()
29 .add_parsed_line(Self::parsed_line_to_string_vec(&line));
30 } else {
31 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}