codelighter/
codelighter.rs

1#[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}