dprint_core/formatting/utils/
string_utils.rs1pub fn get_line_number_of_pos(text: &str, pos: usize) -> usize {
2 let text_bytes = text.as_bytes();
3 let mut line_count = 1; 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 }
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)) .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 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 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 #[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 #[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 #[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}