notes/
cli_format.rs

1use colored::*;
2#[cfg(test)]
3use mockall::automock;
4
5use crate::note::Note;
6use crate::search_match::SearchMatch;
7
8#[cfg_attr(test, automock)]
9pub trait CliFormat {
10    fn search_match(&self, search_m: &SearchMatch) -> String;
11    fn note_list_item(&self, note: &Note) -> String;
12    fn match_score(&self, score: usize) -> String;
13    fn note_id(&self, id: usize) -> String;
14    fn note_title(&self, title: &str) -> String;
15    fn note_directory(&self, name: &str) -> String;
16}
17
18pub struct CliFormatImpl;
19
20impl CliFormatImpl {
21    pub fn new() -> Self {
22        CliFormatImpl {}
23    }
24}
25
26impl Default for CliFormatImpl {
27    fn default() -> Self {
28        CliFormatImpl::new()
29    }
30}
31
32impl CliFormat for CliFormatImpl {
33    fn search_match(&self, search_m: &SearchMatch) -> String {
34        let id = self.note_id(search_m.id);
35        let title = self.note_title(&search_m.title);
36        let score = self.match_score(search_m.score);
37        let header = format!("{} {} {} \n", id, title, score);
38
39        let total_match = search_m.matched_lines.len();
40        let mut body: Vec<String> = search_m
41            .matched_lines
42            .iter()
43            .enumerate()
44            .map(|(mnbr, raw_line)| {
45                let match_highlighted = raw_line.matched.yellow().to_string();
46                let line_nbr = format!("{}.", raw_line.display_number).dimmed();
47                let previous_nbr = format!("{}.", raw_line.display_number - 1).dimmed();
48                let next_nbr = format!("{}.", raw_line.display_number + 1).dimmed();
49
50                let previous = &raw_line.previous.as_ref().map(|l| format!("{} {}", previous_nbr, l.dimmed()));
51                let next = &raw_line.next.as_ref().map(|l| format!("{} {}", next_nbr, l.dimmed()));
52
53                let line = format!("{:2} {}", line_nbr, raw_line.content.replace(&raw_line.matched, &match_highlighted));
54
55                let is_last = mnbr == total_match - 1;
56                match (previous, next, is_last) {
57                    (Some(p), Some(n), false) => format!("{}\n{}\n{}\n", p, line, n),
58                    (Some(p), Some(n), true) => format!("{}\n{}\n{}", p, line, n),
59                    (Some(p), None, false) => format!("{}\n{}\n", p, line),
60                    (Some(p), None, true) => format!("{}\n{}", p, line),
61                    (None, Some(n), false) => format!("{}\n{}\n", line, n),
62                    (None, Some(n), true) => format!("{}\n{}", line, n),
63                    (None, None, _) => line,
64                }
65            })
66            .collect();
67
68        // If no match was provided, this is because note is empty, otherwise we have the first lines of note
69        if body.is_empty() {
70            body = vec!["... This note is empty ...".to_string()]
71        }
72
73        format!("{}{}", header, body.join("\n"))
74    }
75
76    fn note_list_item(&self, note: &Note) -> String {
77        format!(" {} - {}", self.note_id(note.id), self.note_title(&note.title))
78    }
79
80    fn match_score(&self, score: usize) -> String {
81        format!("(Score: {})", score.to_string()).dimmed().to_string()
82    }
83
84    fn note_id(&self, id: usize) -> String {
85        format!("@{}", id).green().to_string()
86    }
87
88    fn note_title(&self, title: &str) -> String {
89        format!("{}", title.cyan())
90    }
91
92    fn note_directory(&self, name: &str) -> String {
93        format!(" 🗁  {}", name)
94    }
95}
96
97#[cfg(test)]
98mod tests {
99    use super::*;
100    use crate::search_match::MatchedLine;
101
102    fn init() {
103        // We disable colors for test
104        control::set_override(false);
105    }
106
107    #[test]
108    pub fn search_match_no_previous_no_next() {
109        init();
110        let search_m = SearchMatch {
111            id: 0,
112            score: 4,
113            path: "/tmp/note-1.txt".into(),
114            title: "# What a note !".to_string(),
115            matched_lines: vec![
116                MatchedLine {
117                    display_number: 3,
118                    line_number: 2,
119                    content: "A very interesting one".to_string(),
120                    matched: "".to_string(),
121                    previous: None,
122                    next: None,
123                },
124                MatchedLine {
125                    display_number: 4,
126                    line_number: 3,
127                    content: "With very interesting things inside".to_string(),
128                    matched: "".to_string(),
129                    previous: None,
130                    next: None,
131                },
132            ],
133        };
134
135        let fmt = CliFormatImpl::default();
136        let actual = fmt.search_match(&search_m);
137        let expected = "@0 # What a note ! (Score: 4) \n3. A very interesting one\n4. With very interesting things inside".to_string();
138
139        assert_eq!(actual, expected);
140    }
141
142    #[test]
143    pub fn search_match_previous_no_next() {
144        init();
145        let search_m = SearchMatch {
146            id: 0,
147            score: 4,
148            path: "/tmp/note-1.txt".into(),
149            title: "# What a note !".to_string(),
150            matched_lines: vec![
151                MatchedLine {
152                    display_number: 3,
153                    line_number: 2,
154                    content: "A very interesting one".to_string(),
155                    matched: "".to_string(),
156                    previous: Some("Previous line 1".to_string()),
157                    next: None,
158                },
159                MatchedLine {
160                    display_number: 4,
161                    line_number: 3,
162                    content: "With very interesting things inside".to_string(),
163                    matched: "".to_string(),
164                    previous: Some("Previous line 2".to_string()),
165                    next: None,
166                },
167            ],
168        };
169
170        let fmt = CliFormatImpl::default();
171        let actual = fmt.search_match(&search_m);
172        let expected =
173            "@0 # What a note ! (Score: 4) \n2. Previous line 1\n3. A very interesting one\n\n3. Previous line 2\n4. With very interesting things inside"
174                .to_string();
175
176        assert_eq!(actual, expected);
177    }
178
179    #[test]
180    pub fn search_match_no_previous_next() {
181        init();
182        let search_m = SearchMatch {
183            id: 0,
184            score: 4,
185            path: "/tmp/note-1.txt".into(),
186            title: "# What a note !".to_string(),
187            matched_lines: vec![
188                MatchedLine {
189                    display_number: 3,
190                    line_number: 2,
191                    content: "A very interesting one".to_string(),
192                    matched: "".to_string(),
193                    previous: None,
194                    next: Some("Next line 1".to_string()),
195                },
196                MatchedLine {
197                    display_number: 4,
198                    line_number: 3,
199                    content: "With very interesting things inside".to_string(),
200                    matched: "".to_string(),
201                    previous: None,
202                    next: Some("Next line 2".to_string()),
203                },
204            ],
205        };
206
207        let fmt = CliFormatImpl::default();
208        let actual = fmt.search_match(&search_m);
209        let expected =
210            "@0 # What a note ! (Score: 4) \n3. A very interesting one\n4. Next line 1\n\n4. With very interesting things inside\n5. Next line 2".to_string();
211
212        assert_eq!(actual, expected);
213    }
214
215    #[test]
216    pub fn search_match_previous_next() {
217        init();
218        let search_m = SearchMatch {
219            id: 0,
220            score: 4,
221            path: "/tmp/note-1.txt".into(),
222            title: "# What a note !".to_string(),
223            matched_lines: vec![
224                MatchedLine {
225                    display_number: 3,
226                    line_number: 2,
227                    content: "A very interesting one".to_string(),
228                    matched: "".to_string(),
229                    previous: Some("Previous line 1".to_string()),
230                    next: Some("Next line 1".to_string()),
231                },
232                MatchedLine {
233                    display_number: 4,
234                    line_number: 3,
235                    content: "With very interesting things inside".to_string(),
236                    matched: "".to_string(),
237                    previous: Some("Previous line 2".to_string()),
238                    next: Some("Next line 2".to_string()),
239                },
240            ],
241        };
242
243        let fmt = CliFormatImpl::default();
244        let actual = fmt.search_match(&search_m);
245        let expected = "@0 # What a note ! (Score: 4) \n2. Previous line 1\n3. A very interesting one\n4. Next line 1\n\n3. Previous line 2\n4. With very interesting things inside\n5. Next line 2".to_string();
246
247        assert_eq!(actual, expected);
248    }
249
250    #[test]
251    fn note_id() {
252        init();
253        let fmt = CliFormatImpl::default();
254        assert_eq!(fmt.note_id(8), "@8");
255    }
256
257    #[test]
258    fn note_title() {
259        init();
260        let fmt = CliFormatImpl::default();
261        assert_eq!(fmt.note_title("# abcd"), "# abcd");
262    }
263
264    #[test]
265    fn note_directory() {
266        init();
267        let fmt = CliFormatImpl::default();
268        assert_eq!(fmt.note_directory("# abcd"), " 🗁  # abcd");
269    }
270}