1use enum_dispatch::enum_dispatch;
9
10const LOCAL_COMMIT_HASH_COLOR: &str = "\u{1b}[0;93;1m";
11const REMOTE_COMMIT_HASH_COLOR: &str = "\u{1b}[0;33m";
12
13#[derive(Debug)]
15pub struct Commit {
16 lines: Vec<Vec<String>>,
17 pub selected: bool,
18}
19impl Commit {
20 pub fn new(parsed_lines: Vec<Vec<String>>, selected: bool) -> Self {
21 Self {
22 lines: parsed_lines,
23 selected,
24 }
25 }
26
27 pub fn hash(&self) -> Option<&str> {
39 let first_line = self.parsed_lines().first().unwrap();
40
41 let mut commit_hash_index = 0;
42 for (index, text) in first_line.iter().enumerate() {
43 if (text == REMOTE_COMMIT_HASH_COLOR) | (text == LOCAL_COMMIT_HASH_COLOR) {
44 commit_hash_index = index + 1;
45 break;
46 }
47 }
48 Some(&first_line[commit_hash_index])
49 }
50
51 pub fn select(&mut self) {
52 if self.selected {
53 return;
54 }
55
56 self.selected = true;
57 for line in self.lines.iter_mut() {
58 Self::add_selection_color(line);
59 }
60 }
61
62 pub fn deselect(&mut self) {
63 if !self.selected {
64 return;
65 }
66
67 self.selected = false;
68 for line in self.lines.iter_mut() {
69 Self::remove_selection_color(line);
70 }
71 }
72
73 fn add_selection_color(line: &mut Vec<String>) {
74 if line.len() > 4 {
75 line.insert(4, Self::selection_formatter());
76 } else if line.len() > 1 {
77 line.insert(1, Self::selection_formatter());
78 } else {
79 let text = line.pop().unwrap();
80 for block in text.splitn(2, ' ') {
81 line.push(block.to_string());
82 }
83 line.insert(1, " ".to_string());
85 line.insert(1, Self::selection_formatter());
86 }
87 line.push(Self::stop_formatter());
88 }
89
90 fn remove_selection_color(line: &mut Vec<String>) {
91 line.retain(|text| !text.contains(&Self::selection_formatter()));
92 }
93
94 fn selection_formatter() -> String {
95 "\u{1b}[0;35m".to_string()
96 }
97
98 fn stop_formatter() -> String {
99 "\u{1b}[0m".to_string()
100 }
101}
102
103#[derive(Debug)]
106pub struct Glyph {
107 lines: Vec<Vec<String>>,
108}
109impl Glyph {
110 pub fn new(parsed_lines: Vec<Vec<String>>) -> Self {
111 Self {
112 lines: parsed_lines,
113 }
114 }
115}
116
117#[enum_dispatch(ItemType)]
119pub trait Item {
120 fn parsed_lines(&self) -> &Vec<Vec<String>>;
121 fn add_parsed_line(&mut self, parsed_line: Vec<String>);
122 fn to_string_vec(&self) -> Vec<String> {
123 self.parsed_lines()
124 .iter()
125 .map(|line| line.join(""))
126 .collect()
127 }
128}
129
130impl Item for Commit {
131 fn parsed_lines(&self) -> &Vec<Vec<String>> {
132 &self.lines
133 }
134
135 fn add_parsed_line(&mut self, parsed_line: Vec<String>) {
136 self.lines.push(parsed_line);
137 }
138}
139
140impl Item for Glyph {
141 fn parsed_lines(&self) -> &Vec<Vec<String>> {
142 &self.lines
143 }
144
145 fn add_parsed_line(&mut self, parsed_line: Vec<String>) {
146 self.lines.push(parsed_line);
147 }
148}
149
150#[enum_dispatch]
152#[derive(Debug)]
153pub enum ItemType {
154 Commit,
155 Glyph,
156}
157
158#[cfg(test)]
159mod tests {
160
161 use crate::parser::SmartLogParser;
162
163 use super::*;
164
165 const RAW_LINES: [&str; 15] = [
166 " @ \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",
167 " │ \u{1b}[0;35m[pr body update] update stack list without overwriting PR title and body\u{1b}[0m",
168 " │",
169 " 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",
170 "╭─╯ [pr body update] fix reviewstack option breaking stack list detection",
171 "│",
172 "o \u{1b}[0;33mba27d4d13\u{1b}[0m Dec 07 at 22:20 \u{1b}[0;32mremote/main\u{1b}[0m",
173 "╷",
174 "╷ 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",
175 "╭─╯ [isl] increase width of diff window in split stack edit panel",
176 "│",
177 "o \u{1b}[0;33m0e069ab09\u{1b}[0m Nov 21 at 13:16",
178 "│",
179 "~",
180 "",
181 ];
182
183 #[test]
184 fn test_commit() {
185 let mut commit = Commit::new(vec![vec!["a".to_string()]], true);
186 commit.add_parsed_line(vec!["b".to_string()]);
187 assert_eq!(
188 commit.lines,
189 vec![vec!["a".to_string()], vec!["b".to_string()]]
190 );
191 assert!(commit.selected);
192 commit.selected = false;
193 assert!(!commit.selected);
194 }
195
196 #[test]
197 fn test_glyph() {
198 let mut glyph = Glyph::new(vec![vec!["a".to_string()]]);
199 glyph.add_parsed_line(vec!["b".to_string()]);
200 assert_eq!(
201 glyph.parsed_lines(),
202 &vec![vec!["a".to_string()], vec!["b".to_string()]]
203 );
204 }
205
206 #[test]
207 fn test_item_types() {
208 let mut items: Vec<ItemType> = vec![
209 Commit::new(vec![vec!["a".to_string()]], true).into(),
210 Glyph::new(vec![vec!["a".to_string()]]).into(),
211 ];
212
213 for item in items.iter_mut() {
214 item.add_parsed_line(vec!["b".to_string()]);
215
216 match item {
217 ItemType::Commit(commit) => {
218 assert_eq!(
219 commit.parsed_lines(),
220 &vec![vec!["a".to_string()], vec!["b".to_string()]]
221 );
222 assert!(commit.selected);
223 commit.selected = false;
224 assert!(!commit.selected);
225 }
226 ItemType::Glyph(glyph) => {
227 assert_eq!(
228 glyph.parsed_lines(),
229 &vec![vec!["a".to_string()], vec!["b".to_string()]]
230 );
231 }
232 }
233 }
234 }
235
236 #[test]
237 fn test_select() {
238 let graph_items = &mut SmartLogParser::parse(&raw_lines()).unwrap();
239
240 let commit = &mut graph_items[2];
241 match commit {
242 ItemType::Commit(commit) => {
243 assert!(!commit.selected);
244 assert_eq!(
245 commit.parsed_lines()[0][4],
246 " Dec 08 at 09:46 royrothenberg "
247 );
248 assert_eq!(
249 commit.parsed_lines()[1][1],
250 " [pr body update] fix reviewstack option breaking stack list detection"
251 );
252 commit.select();
253 assert!(commit.selected);
254 assert_eq!(
255 commit.parsed_lines()[0][4],
256 "\u{1b}[0;35m", );
258 assert_eq!(
259 commit.parsed_lines()[1][1],
260 "\u{1b}[0;35m", );
262 }
263 _ => panic!("Expected GraphCommit"),
264 }
265 }
266
267 #[test]
268 fn test_deselect() {
269 let graph_items = &mut SmartLogParser::parse(&raw_lines()).unwrap();
270
271 let commit = &mut graph_items[0];
272 match commit {
273 ItemType::Commit(commit) => {
274 assert!(commit.selected);
275 assert!(commit.parsed_lines()[0].contains(&"\u{1b}[0;35m".to_string()));
276 assert!(commit.parsed_lines()[1].contains(&"\u{1b}[0;35m".to_string()));
277 commit.deselect();
278 assert!(!commit.selected);
279 assert!(!commit.parsed_lines()[0].contains(&"\u{1b}[0;35m".to_string()));
280 assert!(!commit.parsed_lines()[1].contains(&"\u{1b}[0;35m".to_string()));
281 }
282 _ => panic!("Expected GraphCommit"),
283 }
284 }
285
286 #[test]
287 fn test_hash() {
288 let graph_items = &mut SmartLogParser::parse(&raw_lines()).unwrap();
289
290 let local_commit = &mut graph_items[0];
291 match local_commit {
292 ItemType::Commit(commit) => {
293 assert_eq!(commit.hash().unwrap(), "1cee5d55e");
294 }
295 _ => panic!("Expected GraphCommit"),
296 }
297
298 let remote_commit = &mut graph_items[4];
299 match remote_commit {
300 ItemType::Commit(commit) => {
301 assert_eq!(commit.hash().unwrap(), "ba27d4d13");
302 }
303 _ => panic!("Expected GraphCommit"),
304 }
305 }
306
307 fn raw_lines() -> Vec<String> {
308 RAW_LINES.iter().map(|x| x.to_string()).collect()
309 }
310}