codelighter/
codelighter.rs1#[derive(Debug, Clone, Copy, Default)]
2struct Line {
3 number: usize,
4 start_index: usize,
5 end_index: usize,
6}
7
8const COLOR_ERR: &str = "\x1b[4m\x1b[31m";
9const COLOR_WARN: &str = "\x1b[4m\x1b[33m";
10const COLOR_INFO: &str = "\x1b[4m\x1b[36m";
11const RESET: &str = "\x1b[0m";
12
13const DEFAULT_CONTEXT: usize = 2;
14
15pub fn highlight_error(start: usize, end: usize, raw: &str) -> String {
16 highlight(start, end, raw, COLOR_ERR, DEFAULT_CONTEXT)
17}
18
19pub fn highlight_warn(start: usize, end: usize, raw: &str) -> String {
20 highlight(start, end, raw, COLOR_WARN, DEFAULT_CONTEXT)
21}
22
23pub fn highlight_note(start: usize, end: usize, raw: &str) -> String {
24 highlight(start, end, raw, COLOR_INFO, DEFAULT_CONTEXT)
25}
26
27pub fn highlight(start: usize, end: usize, raw: &str, color: &str, ctx_lines: usize) -> String {
28 assert!(start <= end);
29 let owned;
30 let text: &str = if end <= raw.len() {
31 raw
32 } else {
33 owned = format!("{}{}", raw, " ".repeat(end - raw.len()));
34 &owned
35 };
36
37 let mut line_offsets = Vec::new();
38 line_offsets.push(0);
39 for (i, c) in text.char_indices() {
40 if c == '\n' {
41 line_offsets.push(i + 1);
42 }
43 }
44
45 let find_line = |pos: usize| -> usize {
46 match line_offsets.binary_search(&pos) {
47 Ok(i) => i,
48 Err(i) => i.saturating_sub(1),
49 }
50 };
51
52 let start_line = find_line(start);
53 let mut end_line = if end > start {
54 find_line(end.saturating_sub(1))
55 } else {
56 start_line
57 };
58
59 if end_line < start_line {
60 end_line = start_line;
61 }
62
63 let last_line_index = line_offsets.len().saturating_sub(1);
64 let context_start = start_line.saturating_sub(ctx_lines);
65 let context_end = (end_line + ctx_lines).min(last_line_index);
66
67 let lines: Vec<Line> = (context_start..=context_end)
68 .map(|i| {
69 let start_idx = line_offsets[i];
70 let end_idx = *line_offsets.get(i + 1).unwrap_or(&text.len());
71 Line { number: i + 1, start_index: start_idx, end_index: end_idx }
72 })
73 .collect();
74
75 let max_line_num = lines.last().map(|l| l.number).unwrap_or(1);
76 let number_width = max_line_num.to_string().len();
77
78 let mut result = String::with_capacity(text.len() * 2);
79 let mut highlighting = false;
80
81 for line in &lines {
82 let line_num_str = format!("{:>width$}", line.number, width = number_width);
83 result.push_str(&line_num_str);
84 result.push_str(" | ");
85
86 let mut current_index = line.start_index;
87 for chr in text[line.start_index..line.end_index].chars() {
88 let is_in_highlight = current_index >= start && current_index < end;
89 if is_in_highlight && !highlighting {
90 result.push_str(color);
91 highlighting = true;
92 } else if !is_in_highlight && highlighting {
93 result.push_str(RESET);
94 highlighting = false;
95 }
96
97 result.push(chr);
98 current_index += chr.len_utf8();
99 }
100
101 if highlighting {
102 result.push_str(RESET);
103 highlighting = false;
104 }
105 }
106
107 result
108}