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 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(¬e.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 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}