cargo_quality/differ/display/
render.rs1use console::measure_text_width;
5use owo_colors::OwoColorize;
6
7use super::{grid::MIN_FILE_WIDTH, grouping::group_imports, types::RenderedFile};
8use crate::differ::types::FileDiff;
9
10const ESTIMATED_LINES_PER_FILE: usize = 20;
17
18pub fn render_file_block(file: &FileDiff, color: bool) -> RenderedFile {
68 let estimated_capacity = ESTIMATED_LINES_PER_FILE + file.entries.len() * 5;
69
70 let mut lines = Vec::with_capacity(estimated_capacity);
71 let mut max_width = 0;
72
73 render_header(&mut lines, &mut max_width, &file.path, color);
74
75 render_imports(&mut lines, &mut max_width, file, color);
76
77 render_issues(&mut lines, &mut max_width, file, color);
78
79 render_empty_lines_note(&mut lines, max_width, file, color);
80
81 render_footer(&mut lines, &mut max_width, color);
82
83 RenderedFile {
84 lines,
85 width: max_width.max(MIN_FILE_WIDTH)
86 }
87}
88
89#[inline]
97fn render_header(lines: &mut Vec<String>, max_width: &mut usize, path: &str, color: bool) {
98 let header = format!("File: {}", path);
99 *max_width = (*max_width).max(measure_text_width(&header));
100
101 if color {
102 lines.push(header.cyan().bold().to_string());
103 } else {
104 lines.push(header);
105 }
106
107 let separator = "─".repeat(40);
108 *max_width = (*max_width).max(measure_text_width(&separator));
109
110 if color {
111 lines.push(separator.dimmed().to_string());
112 } else {
113 lines.push(separator);
114 }
115}
116
117#[inline]
125fn render_imports(lines: &mut Vec<String>, max_width: &mut usize, file: &FileDiff, color: bool) {
126 let imports: Vec<&str> = file
127 .entries
128 .iter()
129 .filter_map(|e| e.import.as_deref())
130 .collect();
131
132 if imports.is_empty() {
133 return;
134 }
135
136 let import_header = "Imports (file top)";
137 *max_width = (*max_width).max(measure_text_width(import_header));
138
139 if color {
140 lines.push(import_header.dimmed().to_string());
141 } else {
142 lines.push(import_header.to_string());
143 }
144
145 let grouped = group_imports(&imports);
146 for import in grouped {
147 let import_line = format!("+ {}", import);
148 *max_width = (*max_width).max(measure_text_width(&import_line));
149
150 if color {
151 lines.push(import_line.green().to_string());
152 } else {
153 lines.push(import_line);
154 }
155 }
156
157 lines.push(String::new());
158}
159
160#[inline]
168fn render_issues(lines: &mut Vec<String>, max_width: &mut usize, file: &FileDiff, color: bool) {
169 let mut last_analyzer = "";
170
171 for entry in &file.entries {
172 if entry.analyzer == "empty_lines" {
173 continue;
174 }
175
176 if entry.analyzer != last_analyzer {
177 if !last_analyzer.is_empty() {
178 lines.push(String::new());
179 }
180
181 let analyzer_line = format!(
182 "{} ({} issues)",
183 entry.analyzer,
184 file.entries
185 .iter()
186 .filter(|e| e.analyzer == entry.analyzer)
187 .count()
188 );
189
190 *max_width = (*max_width).max(measure_text_width(&analyzer_line));
191
192 if color {
193 lines.push(analyzer_line.green().bold().to_string());
194 } else {
195 lines.push(analyzer_line);
196 }
197
198 lines.push(String::new());
199
200 last_analyzer = &entry.analyzer;
201 }
202
203 render_issue_entry(lines, max_width, entry, color);
204 }
205}
206
207#[inline]
215fn render_issue_entry(
216 lines: &mut Vec<String>,
217 max_width: &mut usize,
218 entry: &crate::differ::types::DiffEntry,
219 color: bool
220) {
221 let line_header = format!("Line {}", entry.line);
222 *max_width = (*max_width).max(measure_text_width(&line_header));
223
224 if color {
225 lines.push(line_header.cyan().to_string());
226 } else {
227 lines.push(line_header);
228 }
229
230 let old_line = format!("- {}", entry.original);
231 *max_width = (*max_width).max(measure_text_width(&old_line));
232
233 if color {
234 lines.push(old_line.red().to_string());
235 } else {
236 lines.push(old_line);
237 }
238
239 let new_line = format!("+ {}", entry.modified);
240 *max_width = (*max_width).max(measure_text_width(&new_line));
241
242 if color {
243 lines.push(new_line.green().to_string());
244 } else {
245 lines.push(new_line);
246 }
247
248 lines.push(String::new());
249}
250
251#[inline]
259fn render_empty_lines_note(
260 lines: &mut Vec<String>,
261 max_width: usize,
262 file: &FileDiff,
263 color: bool
264) {
265 let empty_entries: Vec<_> = file
266 .entries
267 .iter()
268 .filter(|e| e.analyzer == "empty_lines")
269 .collect();
270
271 if empty_entries.is_empty() {
272 return;
273 }
274
275 let line_numbers: Vec<String> = empty_entries.iter().map(|e| e.line.to_string()).collect();
276
277 let prefix = format!(
278 "Note: {} empty {} will be removed from lines: ",
279 empty_entries.len(),
280 if empty_entries.len() == 1 {
281 "line"
282 } else {
283 "lines"
284 }
285 );
286
287 let mut current_line = prefix.clone();
288
289 for (i, num) in line_numbers.iter().enumerate() {
290 let separator = if i == 0 { "" } else { ", " };
291 let addition = format!("{}{}", separator, num);
292
293 if current_line.len() + addition.len() > max_width && i > 0 {
294 if color {
295 lines.push(current_line.dimmed().italic().to_string());
296 } else {
297 lines.push(current_line);
298 }
299 current_line = format!(" {}", num);
300 } else {
301 current_line.push_str(&addition);
302 }
303 }
304
305 if !current_line.is_empty() {
306 if color {
307 lines.push(current_line.dimmed().italic().to_string());
308 } else {
309 lines.push(current_line);
310 }
311 }
312
313 lines.push(String::new());
314}
315
316#[inline]
323fn render_footer(lines: &mut Vec<String>, max_width: &mut usize, color: bool) {
324 let end_separator = "═".repeat(40);
325 *max_width = (*max_width).max(measure_text_width(&end_separator));
326
327 if color {
328 lines.push(end_separator.dimmed().to_string());
329 } else {
330 lines.push(end_separator);
331 }
332}
333
334#[cfg(test)]
335mod tests {
336 use super::*;
337 use crate::differ::types::{DiffEntry, FileDiff};
338
339 #[test]
340 fn test_render_file_block_empty() {
341 let file = FileDiff::new("test.rs".to_string());
342 let rendered = render_file_block(&file, false);
343
344 assert!(!rendered.lines.is_empty());
345 assert!(rendered.width >= MIN_FILE_WIDTH);
346 }
347
348 #[test]
349 fn test_render_file_block_with_entry() {
350 let mut file = FileDiff::new("test.rs".to_string());
351 file.add_entry(DiffEntry {
352 line: 10,
353 analyzer: "test".to_string(),
354 original: "old".to_string(),
355 modified: "new".to_string(),
356 description: "desc".to_string(),
357 import: None
358 });
359
360 let rendered = render_file_block(&file, false);
361 assert!(rendered.line_count() > 5);
362 }
363
364 #[test]
365 fn test_render_file_block_with_import() {
366 let mut file = FileDiff::new("test.rs".to_string());
367 file.add_entry(DiffEntry {
368 line: 10,
369 analyzer: "path_import".to_string(),
370 original: "std::fs::read()".to_string(),
371 modified: "read()".to_string(),
372 description: "Use import".to_string(),
373 import: Some("use std::fs::read;".to_string())
374 });
375
376 let rendered = render_file_block(&file, false);
377 assert!(rendered.lines.iter().any(|l| l.contains("Imports")));
378 }
379
380 #[test]
381 fn test_render_file_block_multiple_analyzers() {
382 let mut file = FileDiff::new("test.rs".to_string());
383
384 file.add_entry(DiffEntry {
385 line: 10,
386 analyzer: "analyzer1".to_string(),
387 original: "old1".to_string(),
388 modified: "new1".to_string(),
389 description: "desc1".to_string(),
390 import: None
391 });
392
393 file.add_entry(DiffEntry {
394 line: 20,
395 analyzer: "analyzer2".to_string(),
396 original: "old2".to_string(),
397 modified: "new2".to_string(),
398 description: "desc2".to_string(),
399 import: None
400 });
401
402 let rendered = render_file_block(&file, false);
403 assert!(rendered.lines.iter().any(|l| l.contains("analyzer1")));
404 assert!(rendered.lines.iter().any(|l| l.contains("analyzer2")));
405 }
406
407 #[test]
408 fn test_render_respects_capacity() {
409 let file = FileDiff::new("test.rs".to_string());
410 let rendered = render_file_block(&file, false);
411
412 assert!(rendered.lines.capacity() >= ESTIMATED_LINES_PER_FILE);
413 }
414}