dprint_core/formatting/utils/
string_utils.rs

1pub fn get_line_number_of_pos(text: &str, pos: usize) -> usize {
2  let text_bytes = text.as_bytes();
3  let mut line_count = 1; // 1-indexed
4
5  for i in 0..pos {
6    if text_bytes.get(i) == Some(&(b'\n')) {
7      line_count += 1;
8    }
9  }
10
11  line_count
12}
13
14pub fn get_column_number_of_pos(text: &str, pos: usize) -> usize {
15  let line_start_byte_pos = get_line_start_byte_pos(text, pos);
16  text[line_start_byte_pos..pos].chars().count() + 1 // 1-indexed
17}
18
19fn get_line_start_byte_pos(text: &str, pos: usize) -> usize {
20  let text_bytes = text.as_bytes();
21  for i in (0..pos).rev() {
22    if text_bytes.get(i) == Some(&(b'\n')) {
23      return i + 1;
24    }
25  }
26
27  0
28}
29
30fn get_line_end_byte_pos(text: &str, pos: usize) -> usize {
31  let mut pos = pos;
32  let mut chars = text[pos..].chars().peekable();
33  while let Some(c) = chars.next() {
34    if c == '\n' || c == '\r' && chars.peek().copied() == Some('\n') {
35      break;
36    }
37    pos += c.len_utf8();
38  }
39  pos
40}
41
42pub fn format_diagnostic(range: Option<(usize, usize)>, message: &str, file_text: &str) -> String {
43  let mut result = String::new();
44  if let Some((error_start, _)) = range {
45    let line_number = get_line_number_of_pos(file_text, error_start);
46    let column_number = get_column_number_of_pos(file_text, error_start);
47    result.push_str(&format!("Line {}, column {}: ", line_number, column_number))
48  }
49  result.push_str(message);
50  if let Some(range) = range {
51    result.push_str("\n\n");
52    let code = get_range_text_highlight(file_text, range)
53      .lines()
54      .map(|l| format!("  {}", l)) // indent
55      .collect::<Vec<_>>()
56      .join("\n");
57    result.push_str(&code);
58  }
59  result
60}
61
62fn get_range_text_highlight(file_text: &str, byte_range: (usize, usize)) -> String {
63  // todo: cleanup... kind of confusing
64  let ((text_start, text_end), (error_start, error_end)) = get_text_and_error_range(byte_range, file_text);
65  if text_end > file_text.len() {
66    return format!("Error formatting diagnostic. Position {} was outside the length of the string.", text_end);
67  }
68  let sub_text = &file_text[text_start..text_end];
69
70  let mut result = String::new();
71  let lines = sub_text.lines().collect::<Vec<_>>();
72  let line_count = lines.len();
73  for (i, line) in lines.iter().enumerate() {
74    let is_last_line = i == line_count - 1;
75    // don't show all the lines if there are more than 3 lines
76    if i > 2 && !is_last_line {
77      continue;
78    }
79    if i > 0 {
80      result.push('\n');
81    }
82    if i == 2 && !is_last_line {
83      result.push_str("...");
84      continue;
85    }
86    result.push_str(line);
87    result.push('\n');
88
89    let start_char_index = if i == 0 { get_column_number_of_pos(sub_text, error_start) - 1 } else { 0 };
90    let end_char_index = if is_last_line {
91      get_column_number_of_pos(sub_text, error_end) - 1
92    } else {
93      line.chars().count()
94    };
95    if end_char_index > start_char_index {
96      result.push_str(&" ".repeat(start_char_index));
97      result.push_str(&"~".repeat(end_char_index - start_char_index));
98    }
99  }
100  return result;
101
102  fn get_text_and_error_range(byte_range: (usize, usize), file_text: &str) -> ((usize, usize), (usize, usize)) {
103    let (start, end) = byte_range;
104    let line_start = get_line_start_byte_pos(file_text, start);
105    let line_end = get_line_end_byte_pos(file_text, end);
106
107    let start_text = &file_text[line_start..start];
108    let end_text = &file_text[end..line_end];
109
110    let text_start = start - start_text.chars().rev().take(20).map(|c| c.len_utf8()).sum::<usize>();
111    let text_end = end + end_text.chars().take(10).map(|c| c.len_utf8()).sum::<usize>();
112    let error_start = start - text_start;
113    let error_end = error_start + (end - start);
114
115    ((text_start, text_end), (error_start, error_end))
116  }
117}
118
119#[cfg(test)]
120mod tests {
121  use super::*;
122
123  // get_line_number_of_pos
124
125  #[test]
126  fn should_get_line_number_of_single_line() {
127    assert_eq!(get_line_number_of_pos("testing", 3), 1);
128  }
129
130  #[test]
131  fn should_get_last_line_when_above_length() {
132    assert_eq!(get_line_number_of_pos("t\nt", 50), 2);
133  }
134
135  #[test]
136  fn should_get_line_when_at_first_pos_on_line() {
137    assert_eq!(get_line_number_of_pos("t\ntest\nt", 2), 2);
138  }
139
140  #[test]
141  fn should_get_line_when_at_last_pos_on_line() {
142    assert_eq!(get_line_number_of_pos("t\ntest\nt", 6), 2);
143  }
144
145  // get_column_number_of_pos
146
147  #[test]
148  fn should_get_column_for_first_line() {
149    assert_eq!(get_column_number_of_pos("testing\nthis", 3), 4);
150  }
151
152  #[test]
153  fn should_get_column_for_second_line() {
154    assert_eq!(get_column_number_of_pos("test\nthis", 6), 2);
155  }
156
157  #[test]
158  fn should_get_column_for_start_of_text() {
159    assert_eq!(get_column_number_of_pos("test\nthis", 0), 1);
160  }
161
162  #[test]
163  fn should_get_column_for_start_of_line() {
164    assert_eq!(get_column_number_of_pos("test\nthis", 5), 1);
165  }
166
167  // get_range_text_highlight
168
169  #[test]
170  fn should_get_range_highlight_for_full_text_one_line() {
171    let message = get_range_text_highlight("testtinga", (0, 9));
172    assert_eq!(message, concat!("testtinga\n", "~~~~~~~~~"));
173  }
174
175  #[test]
176  fn should_get_range_highlight_for_full_text_multi_lines() {
177    let message = get_range_text_highlight("test\nt\naa", (0, 9));
178    assert_eq!(message, concat!("test\n", "~~~~\n", "t\n", "~\n", "aa\n", "~~"));
179  }
180
181  #[test]
182  fn should_get_range_highlight_on_one_line() {
183    let message = get_range_text_highlight("testtinga testing test", (10, 17));
184    assert_eq!(message, concat!("testtinga testing test\n", "          ~~~~~~~"));
185  }
186
187  #[test]
188  fn should_get_range_highlight_on_second_line() {
189    let message = get_range_text_highlight("test\ntest\ntest", (5, 9));
190    assert_eq!(message, concat!("test\n", "~~~~"));
191  }
192
193  #[test]
194  fn should_get_range_highlight_on_multi_lines_within() {
195    let message = get_range_text_highlight("test\ntest test\ntest test\nasdf", (10, 19));
196    assert_eq!(message, concat!("test test\n", "     ~~~~\n", "test test\n", "~~~~"));
197  }
198
199  #[test]
200  fn should_display_when_there_are_three_lines() {
201    let message = get_range_text_highlight("test\nasdf\n1234\ntest\nasdf\n1234\ntest\n", (5, 19));
202    assert_eq!(message, concat!("asdf\n", "~~~~\n", "1234\n", "~~~~\n", "test\n", "~~~~"));
203  }
204
205  #[test]
206  fn should_ignore_when_there_are_more_than_three_lines() {
207    let message = get_range_text_highlight("test\nasdf\n1234\ntest\nasdf\n1234\ntest\n", (5, 24));
208    assert_eq!(message, concat!("asdf\n", "~~~~\n", "1234\n", "~~~~\n", "...\n", "asdf\n", "~~~~"));
209  }
210
211  #[test]
212  fn should_show_only_twenty_chars_of_first_line() {
213    let message = get_range_text_highlight("test asdf 1234 fdsa dsfa test", (25, 29));
214    assert_eq!(message, concat!("asdf 1234 fdsa dsfa test\n", "                    ~~~~",));
215  }
216
217  #[test]
218  fn should_show_only_ten_chars_of_last_line() {
219    let message = get_range_text_highlight("test asdf 1234 fdsa dsfa test", (10, 14));
220    assert_eq!(message, concat!("test asdf 1234 fdsa dsfa\n", "          ~~~~",));
221  }
222
223  #[test]
224  fn should_handle_multi_byte_chars() {
225    let one_to_ten = "一二三四五六七八九十";
226    let message = get_range_text_highlight(
227      &one_to_ten.repeat(6),
228      (one_to_ten.len() * 3, one_to_ten.len() * 3 + one_to_ten.chars().next().unwrap().len_utf8()),
229    );
230    assert_eq!(
231      message,
232      concat!("一二三四五六七八九十一二三四五六七八九十一二三四五六七八九十一\n", "                    ~",)
233    );
234  }
235
236  #[test]
237  fn should_handle_multi_byte_characters_on_the_first_line() {
238    let text = "test ≥ ; test";
239    let semi_colon_index = text.find(';').unwrap();
240    let message = get_range_text_highlight(text, (semi_colon_index, semi_colon_index + 1));
241    assert_eq!(message, concat!("test ≥ ; test\n", "       ~",));
242  }
243
244  #[test]
245  fn should_handle_multi_byte_characters_on_the_second_line() {
246    let text = "≥a\ntest ≥ ; test";
247    let semi_colon_index = text.find(';').unwrap();
248    let message = get_range_text_highlight(text, (semi_colon_index, semi_colon_index + 1));
249    assert_eq!(message, concat!("test ≥ ; test\n", "       ~",));
250  }
251}