friendly_errors/code_snippet/
mod.rs

1use 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    // private fields
30    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            // private fields
79            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        // everything is valid
371        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}