friendly_errors/code_snippet/
mod.rs1use std::cmp::max;
2
3#[derive(PartialEq, Debug, Clone, Copy)]
4pub enum HighlightKind {
5 Error,
6 Warning,
7 Info,
8}
9
10#[derive(PartialEq, Debug, Clone, Copy)]
11enum CalculatedFieldError {
12 NotCalculated,
13 Invalid,
14}
15
16type CalculatedFieldResult<T> = Result<T, CalculatedFieldError>;
17
18#[derive(PartialEq, Debug, Clone)]
19pub struct FriendlyCodeSnippet {
20 file_contents: String,
21 file_path: Option<String>,
22 index_start: Option<usize>,
23 index_end: Option<usize>,
24 line_start: Option<usize>,
25 line_end: Option<usize>,
26 kind: HighlightKind,
27 caption: Option<String>,
28
29 line_start_start_index: CalculatedFieldResult<usize>,
31 line_end_start_index: CalculatedFieldResult<usize>,
32 indent_size: CalculatedFieldResult<usize>,
33}
34
35#[derive(PartialEq, Debug, Clone, Copy)]
36pub enum FriendlyCodeSnippetError {
37 InvalidStartPosition,
38 InvalidEndPosition,
39 MissingStartPosition,
40 MissingEndPosition,
41}
42
43fn get_digit_count(mut number: usize) -> usize {
44 let mut digits = 1;
45 while number >= 10 {
46 digits += 1;
47 number /= 10;
48 }
49 digits
50}
51
52fn get_line_number_prefix(line_number: usize, indent: usize) -> String {
53 let number = line_number.to_string();
54 let mut output = " ".repeat(indent - 1 - number.len());
55 output.push_str(&number);
56 output.push_str(" | ");
57 output
58}
59
60fn get_blank_line_prefix(indent: usize) -> String {
61 let mut output = " ".repeat(indent);
62 output.push_str("| ");
63 output
64}
65
66impl FriendlyCodeSnippet {
67 pub fn new<S: Into<String>>(file_contents: S) -> Self {
68 FriendlyCodeSnippet {
69 file_contents: file_contents.into(),
70 file_path: None,
71 index_start: None,
72 index_end: None,
73 line_start: None,
74 line_end: None,
75 kind: HighlightKind::Error,
76 caption: None,
77
78 line_start_start_index: Err(CalculatedFieldError::NotCalculated),
80 line_end_start_index: Err(CalculatedFieldError::NotCalculated),
81 indent_size: Err(CalculatedFieldError::NotCalculated),
82 }
83 }
84
85 pub fn set_file_path<S: Into<String>>(mut self, file_path: S) -> Self {
86 self.file_path = Some(file_path.into());
87 self
88 }
89
90 pub fn index_start(mut self, index_start: usize) -> Self {
91 self.index_start = Some(index_start);
92 self
93 }
94
95 pub fn index_end(mut self, index_end: usize) -> Self {
96 self.index_end = Some(index_end);
97 self
98 }
99
100 pub fn line_start(mut self, line_start: usize) -> Self {
101 self.line_start = Some(line_start);
102 self
103 }
104
105 pub fn line_end(mut self, line_end: usize) -> Self {
106 self.line_end = Some(line_end);
107 self
108 }
109
110 pub fn kind(mut self, kind: HighlightKind) -> Self {
111 self.kind = kind;
112 self
113 }
114
115 pub fn caption<S: Into<String>>(mut self, caption: S) -> Self {
116 self.caption = Some(caption.into());
117 self
118 }
119
120 pub(crate) fn calc_line_start_start_index(&mut self) {
121 match self.line_start {
122 Some(line) => {
123 let mut line_count = 1;
124 for (index, char) in self.file_contents.chars().enumerate() {
125 if line_count == line {
126 self.line_start_start_index = Ok(index);
127 return;
128 }
129 if char == '\n' {
130 line_count += 1;
131 }
132 }
133 self.line_start_start_index = Err(CalculatedFieldError::Invalid);
134 }
135 None => {
136 self.line_start_start_index = Err(CalculatedFieldError::Invalid);
137 }
138 }
139 }
140
141 pub(crate) fn calc_line_end_start_index(&mut self) {
142 match self.line_end {
143 Some(line) => {
144 let mut line_count = 1;
145 for (index, char) in self.file_contents.chars().enumerate() {
146 if line_count == line {
147 self.line_end_start_index = Ok(index);
148 return;
149 }
150 if char == '\n' {
151 line_count += 1;
152 }
153 }
154 self.line_end_start_index = Err(CalculatedFieldError::Invalid);
155 }
156 None => {
157 self.line_end_start_index = Err(CalculatedFieldError::Invalid);
158 }
159 }
160 }
161
162 pub(crate) fn validate_inputs(&self) -> Result<bool, FriendlyCodeSnippetError> {
163 if self.line_start.is_none() && self.index_start.is_none() {
164 return Err(FriendlyCodeSnippetError::MissingStartPosition);
165 }
166 if self.line_end.is_none() && self.index_end.is_none() {
167 return Err(FriendlyCodeSnippetError::MissingEndPosition);
168 }
169 match self.line_start_start_index {
170 Err(CalculatedFieldError::Invalid) => {
171 return Err(FriendlyCodeSnippetError::InvalidStartPosition)
172 }
173 Err(CalculatedFieldError::NotCalculated) => {
174 panic!("line_start_start_index must be calculated before inputs are validated")
175 }
176 Ok(_) => {}
177 }
178 match self.line_end_start_index {
179 Err(CalculatedFieldError::Invalid) => {
180 return Err(FriendlyCodeSnippetError::InvalidEndPosition)
181 }
182 Err(CalculatedFieldError::NotCalculated) => {
183 panic!("line_end_start_index must be calculated before inputs are validated")
184 }
185 Ok(_) => {}
186 }
187 if self.line_start_start_index.unwrap() > self.line_end_start_index.unwrap() {
188 return Err(FriendlyCodeSnippetError::InvalidEndPosition);
189 }
190 if self.line_start_start_index.unwrap() == self.line_end_start_index.unwrap()
191 && self.index_start.unwrap() >= self.index_end.unwrap()
192 {
193 return Err(FriendlyCodeSnippetError::InvalidEndPosition);
194 }
195 Ok(true)
196 }
197
198 pub(crate) fn calc_indent_size(&mut self) {
199 let longest_line_number = max(
200 self.line_start_start_index.unwrap(),
201 self.line_end_start_index.unwrap(),
202 );
203 let default_indent_size = 4;
204 self.indent_size = Ok(max(
205 get_digit_count(longest_line_number) + 1,
206 default_indent_size,
207 ));
208 }
209
210 pub(crate) fn build_file_url(&self) -> String {
211 let mut output = " ".repeat(self.indent_size.unwrap());
212 let mut has_contents = false;
213 if let Some(file_path) = &self.file_path {
214 output.push_str(file_path);
215 has_contents = true;
216 }
217 if let Some(line_start) = self.line_start {
218 if has_contents {
219 output.push(':');
220 }
221 output.push_str(&line_start.to_string());
222 has_contents = true;
223 }
224 if let Some(index_start) = self.index_start {
225 if has_contents {
226 output.push(':');
227 }
228 output.push_str(&index_start.to_string());
229 has_contents = true;
230 }
231 if !has_contents {
232 return String::new();
233 }
234 output.push('\n');
235 output
236 }
237
238 pub(crate) fn build_lines(&self) -> String {
239 let mut output = String::new();
240 if self.line_start_start_index.unwrap() == self.line_end_start_index.unwrap() {
241 output.push_str(&get_line_number_prefix(
242 self.line_start.unwrap(),
243 self.indent_size.unwrap(),
244 ));
245 let mut index = self.line_start_start_index.unwrap();
246 while index < self.file_contents.len() && !self.file_contents[index..index + 1].eq("\n")
247 {
248 index += 1;
249 }
250 let line_contents = &self.file_contents[self.line_start_start_index.unwrap()..index];
251 output.push_str(line_contents);
252 output.push('\n');
253 output.push_str(&get_blank_line_prefix(self.indent_size.unwrap()));
254 output.push_str(&" ".repeat(self.index_start.unwrap()));
255 output.push_str(&"^".repeat(self.index_end.unwrap() - self.index_start.unwrap()));
256 output.push('\n')
257 }
258
259 output
260 }
261
262 pub(crate) fn build_caption(&self) -> String {
263 if let Some(caption) = &self.caption {
264 let mut output = " ".repeat(self.indent_size.unwrap() - 2);
265 output.push_str("--> ");
266 output.push_str(caption);
267 output.push('\n');
268 return output;
269 }
270 String::new()
271 }
272
273 #[cfg(test)]
274 pub(crate) fn set_indent_size(mut self, indent_size: usize) -> Self {
275 self.indent_size = Ok(indent_size);
276 self
277 }
278
279 pub(crate) fn build(mut self) -> Result<String, FriendlyCodeSnippetError> {
280 self.calc_line_start_start_index();
281 self.calc_line_end_start_index();
282 self.validate_inputs()?;
283 self.calc_indent_size();
284 let mut output = String::new();
285 output.push_str(&self.build_file_url());
286 output.push_str(&self.build_caption());
287 output.push_str(&self.build_lines());
288 Ok(output)
289 }
290}
291
292#[cfg(test)]
293mod test {
294 use super::*;
295 use indoc::indoc;
296
297 #[test]
298 fn calc_line_start_start_index_test() {
299 let code = "\nfn main() {\n println!(\"Hello, world!\");\n}\n";
300
301 let mut friendly_code_snippet = FriendlyCodeSnippet::new(code).line_start(1);
302 friendly_code_snippet.calc_line_start_start_index();
303 assert_eq!(friendly_code_snippet.line_start_start_index, Ok(0));
304
305 let mut friendly_code_snippet = FriendlyCodeSnippet::new(code).line_start(2);
306 friendly_code_snippet.calc_line_start_start_index();
307 assert_eq!(friendly_code_snippet.line_start_start_index, Ok(1));
308
309 let mut friendly_code_snippet = FriendlyCodeSnippet::new(code).line_start(3);
310 friendly_code_snippet.calc_line_start_start_index();
311 assert_eq!(friendly_code_snippet.line_start_start_index, Ok(13));
312
313 let mut friendly_code_snippet = FriendlyCodeSnippet::new(code).line_start(0);
314 friendly_code_snippet.calc_line_start_start_index();
315 assert_eq!(
316 friendly_code_snippet.line_start_start_index,
317 Err(CalculatedFieldError::Invalid)
318 );
319
320 let mut friendly_code_snippet = FriendlyCodeSnippet::new(code).line_start(100);
321 friendly_code_snippet.calc_line_start_start_index();
322 assert_eq!(
323 friendly_code_snippet.line_start_start_index,
324 Err(CalculatedFieldError::Invalid)
325 );
326
327 let mut friendly_code_snippet = FriendlyCodeSnippet::new(code);
328 friendly_code_snippet.calc_line_start_start_index();
329 assert_eq!(
330 friendly_code_snippet.line_start_start_index,
331 Err(CalculatedFieldError::Invalid)
332 );
333 }
334
335 #[test]
336 fn calc_line_end_start_index_test() {
337 let code = "\nfn main() {\n println!(\"Hello, world!\");\n}\n";
338
339 let mut friendly_code_snippet = FriendlyCodeSnippet::new(code).line_end(1);
340 friendly_code_snippet.calc_line_end_start_index();
341 assert_eq!(friendly_code_snippet.line_end_start_index, Ok(0));
342
343 let mut friendly_code_snippet = FriendlyCodeSnippet::new(code).line_end(2);
344 friendly_code_snippet.calc_line_end_start_index();
345 assert_eq!(friendly_code_snippet.line_end_start_index, Ok(1));
346
347 let mut friendly_code_snippet = FriendlyCodeSnippet::new(code).line_end(3);
348 friendly_code_snippet.calc_line_end_start_index();
349 assert_eq!(friendly_code_snippet.line_end_start_index, Ok(13));
350
351 let mut friendly_code_snippet = FriendlyCodeSnippet::new(code).line_end(0);
352 friendly_code_snippet.calc_line_end_start_index();
353 assert_eq!(
354 friendly_code_snippet.line_end_start_index,
355 Err(CalculatedFieldError::Invalid)
356 );
357
358 let mut friendly_code_snippet = FriendlyCodeSnippet::new(code).line_end(100);
359 friendly_code_snippet.calc_line_end_start_index();
360 assert_eq!(
361 friendly_code_snippet.line_end_start_index,
362 Err(CalculatedFieldError::Invalid)
363 );
364 }
365
366 #[test]
367 fn validate_inputs_test() {
368 let code = "\nfn main() {\n println!(\"Hello, world!\");\n}\n";
369
370 let mut friendly_code_snippet = FriendlyCodeSnippet::new(code).line_start(1).line_end(2);
372 friendly_code_snippet.calc_line_start_start_index();
373 friendly_code_snippet.calc_line_end_start_index();
374 assert_eq!(friendly_code_snippet.validate_inputs(), Ok(true));
375
376 let mut friendly_code_snippet = FriendlyCodeSnippet::new(code)
377 .line_start(1)
378 .line_end(1)
379 .index_start(3)
380 .index_end(4);
381 friendly_code_snippet.calc_line_start_start_index();
382 friendly_code_snippet.calc_line_end_start_index();
383 assert_eq!(friendly_code_snippet.validate_inputs(), Ok(true));
384
385 let mut friendly_code_snippet = FriendlyCodeSnippet::new(code)
386 .line_start(1)
387 .line_end(2)
388 .index_start(4)
389 .index_end(4);
390 friendly_code_snippet.calc_line_start_start_index();
391 friendly_code_snippet.calc_line_end_start_index();
392 assert_eq!(friendly_code_snippet.validate_inputs(), Ok(true));
393
394 let mut friendly_code_snippet = FriendlyCodeSnippet::new(code)
395 .line_start(1)
396 .line_end(2)
397 .index_start(3)
398 .index_end(4);
399 friendly_code_snippet.calc_line_start_start_index();
400 friendly_code_snippet.calc_line_end_start_index();
401 assert_eq!(friendly_code_snippet.validate_inputs(), Ok(true));
402
403 let mut friendly_code_snippet = FriendlyCodeSnippet::new(code).line_start(2);
404 friendly_code_snippet.calc_line_start_start_index();
405 friendly_code_snippet.calc_line_end_start_index();
406 assert_eq!(
407 friendly_code_snippet.validate_inputs(),
408 Err(FriendlyCodeSnippetError::MissingEndPosition)
409 );
410
411 let mut friendly_code_snippet = FriendlyCodeSnippet::new(code).line_end(2);
412 friendly_code_snippet.calc_line_start_start_index();
413 friendly_code_snippet.calc_line_end_start_index();
414 assert_eq!(
415 friendly_code_snippet.validate_inputs(),
416 Err(FriendlyCodeSnippetError::MissingStartPosition)
417 );
418
419 let mut friendly_code_snippet = FriendlyCodeSnippet::new(code).line_start(0).line_end(2);
420 friendly_code_snippet.calc_line_start_start_index();
421 friendly_code_snippet.calc_line_end_start_index();
422 assert_eq!(
423 friendly_code_snippet.validate_inputs(),
424 Err(FriendlyCodeSnippetError::InvalidStartPosition)
425 );
426
427 let mut friendly_code_snippet = FriendlyCodeSnippet::new(code).line_start(1).line_end(100);
428 friendly_code_snippet.calc_line_start_start_index();
429 friendly_code_snippet.calc_line_end_start_index();
430 assert_eq!(
431 friendly_code_snippet.validate_inputs(),
432 Err(FriendlyCodeSnippetError::InvalidEndPosition)
433 );
434
435 let mut friendly_code_snippet = FriendlyCodeSnippet::new(code).line_start(2).line_end(1);
436 friendly_code_snippet.calc_line_start_start_index();
437 friendly_code_snippet.calc_line_end_start_index();
438 assert_eq!(
439 friendly_code_snippet.validate_inputs(),
440 Err(FriendlyCodeSnippetError::InvalidEndPosition)
441 );
442
443 let mut friendly_code_snippet = FriendlyCodeSnippet::new(code)
444 .line_start(2)
445 .line_end(2)
446 .index_start(4)
447 .index_end(4);
448 friendly_code_snippet.calc_line_start_start_index();
449 friendly_code_snippet.calc_line_end_start_index();
450 assert_eq!(
451 friendly_code_snippet.validate_inputs(),
452 Err(FriendlyCodeSnippetError::InvalidEndPosition)
453 );
454
455 let mut friendly_code_snippet = FriendlyCodeSnippet::new(code)
456 .line_start(2)
457 .line_end(2)
458 .index_start(4)
459 .index_end(3);
460 friendly_code_snippet.calc_line_start_start_index();
461 friendly_code_snippet.calc_line_end_start_index();
462 assert_eq!(
463 friendly_code_snippet.validate_inputs(),
464 Err(FriendlyCodeSnippetError::InvalidEndPosition)
465 );
466 }
467
468 #[test]
469 fn get_digit_count_test() {
470 assert_eq!(get_digit_count(0), 1);
471 assert_eq!(get_digit_count(1), 1);
472 assert_eq!(get_digit_count(10), 2);
473 assert_eq!(get_digit_count(100), 3);
474 assert_eq!(get_digit_count(1000), 4);
475 assert_eq!(get_digit_count(500), 3);
476 assert_eq!(get_digit_count(99), 2);
477 assert_eq!(get_digit_count(101), 3);
478 assert_eq!(get_digit_count(999), 3);
479 assert_eq!(get_digit_count(1001), 4);
480 }
481
482 #[test]
483 fn get_line_number_prefix_test() {
484 assert_eq!(get_line_number_prefix(1, 4), " 1 | ");
485 assert_eq!(get_line_number_prefix(2, 4), " 2 | ");
486 assert_eq!(get_line_number_prefix(20, 4), " 20 | ");
487 assert_eq!(get_line_number_prefix(200, 4), "200 | ");
488 assert_eq!(get_line_number_prefix(200, 5), " 200 | ");
489 }
490
491 #[test]
492 fn get_blank_line_prefix_test() {
493 assert_eq!(get_blank_line_prefix(4), " | ");
494 assert_eq!(get_blank_line_prefix(5), " | ");
495 assert_eq!(
496 get_blank_line_prefix(7).len(),
497 get_line_number_prefix(1, 7).len()
498 );
499 }
500
501 #[test]
502 fn build_file_url_test() {
503 assert_eq!(
504 FriendlyCodeSnippet::new(String::new())
505 .set_indent_size(4)
506 .build_file_url(),
507 ""
508 );
509 assert_eq!(
510 FriendlyCodeSnippet::new(String::new())
511 .index_start(4)
512 .set_indent_size(4)
513 .build_file_url(),
514 " 4\n"
515 );
516 assert_eq!(
517 FriendlyCodeSnippet::new(String::new())
518 .line_start(24)
519 .index_start(4)
520 .set_indent_size(4)
521 .build_file_url(),
522 " 24:4\n"
523 );
524 assert_eq!(
525 FriendlyCodeSnippet::new(String::new())
526 .line_start(24)
527 .set_indent_size(4)
528 .build_file_url(),
529 " 24\n"
530 );
531 assert_eq!(
532 FriendlyCodeSnippet::new(String::new())
533 .set_file_path("hello.rs")
534 .set_indent_size(4)
535 .build_file_url(),
536 " hello.rs\n"
537 );
538 assert_eq!(
539 FriendlyCodeSnippet::new(String::new())
540 .set_file_path("hello.rs")
541 .line_start(24)
542 .index_start(4)
543 .set_indent_size(4)
544 .build_file_url(),
545 " hello.rs:24:4\n"
546 );
547 assert_eq!(
548 FriendlyCodeSnippet::new(String::new())
549 .set_file_path("hello.rs")
550 .line_start(24)
551 .index_start(4)
552 .set_indent_size(8)
553 .build_file_url(),
554 " hello.rs:24:4\n"
555 );
556 }
557
558 #[test]
559 fn build_lines_test() {
560 let code = indoc! {
561 "
562 fn main() {
563 println!(\"Hello, world!\");
564 }
565 "
566 };
567
568 let mut friendly_code_snippet = FriendlyCodeSnippet::new(code)
569 .set_file_path("hello.rs")
570 .line_start(1)
571 .index_start(3)
572 .line_end(1)
573 .index_end(7);
574 friendly_code_snippet.calc_line_start_start_index();
575 friendly_code_snippet.calc_line_end_start_index();
576 friendly_code_snippet.validate_inputs().unwrap();
577 friendly_code_snippet.calc_indent_size();
578 assert_eq!(
579 friendly_code_snippet.build_lines(),
580 " 1 | fn main() {\n | ^^^^\n"
581 );
582
583 let mut friendly_code_snippet = FriendlyCodeSnippet::new(code)
584 .set_file_path("hello.rs")
585 .line_start(2)
586 .index_start(4)
587 .line_end(2)
588 .index_end(11);
589 friendly_code_snippet.calc_line_start_start_index();
590 friendly_code_snippet.calc_line_end_start_index();
591 friendly_code_snippet.validate_inputs().unwrap();
592 friendly_code_snippet.calc_indent_size();
593 assert_eq!(
594 friendly_code_snippet.build_lines(),
595 " 2 | println!(\"Hello, world!\");\n | ^^^^^^^\n"
596 );
597 }
598
599 #[test]
600 fn build_caption_test() {
601 assert_eq!(
602 FriendlyCodeSnippet::new(String::new())
603 .set_indent_size(4)
604 .build_caption(),
605 ""
606 );
607 assert_eq!(
608 FriendlyCodeSnippet::new(String::new())
609 .caption("hello world")
610 .set_indent_size(4)
611 .build_caption(),
612 " --> hello world\n"
613 );
614 assert_eq!(
615 FriendlyCodeSnippet::new(String::new())
616 .caption("hello world")
617 .set_indent_size(8)
618 .build_caption(),
619 " --> hello world\n"
620 );
621 }
622}