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::{
338 analyzer::TextEdit,
339 differ::types::{DiffEntry, FileDiff}
340 };
341
342 #[test]
343 fn test_render_file_block_empty() {
344 let file = FileDiff::new("test.rs".to_string());
345 let rendered = render_file_block(&file, false);
346
347 assert!(!rendered.lines.is_empty());
348 assert!(rendered.width >= MIN_FILE_WIDTH);
349 }
350
351 #[test]
352 fn test_render_file_block_with_entry() {
353 let mut file = FileDiff::new("test.rs".to_string());
354 file.add_entry(DiffEntry {
355 line: 10,
356 analyzer: "test".to_string(),
357 original: "old".to_string(),
358 modified: "new".to_string(),
359 description: "desc".to_string(),
360 import: None,
361 edit: TextEdit::default()
362 });
363
364 let rendered = render_file_block(&file, false);
365 assert!(rendered.line_count() > 5);
366 }
367
368 #[test]
369 fn test_render_file_block_with_import() {
370 let mut file = FileDiff::new("test.rs".to_string());
371 file.add_entry(DiffEntry {
372 line: 10,
373 analyzer: "path_import".to_string(),
374 original: "std::fs::read()".to_string(),
375 modified: "read()".to_string(),
376 description: "Use import".to_string(),
377 import: Some("use std::fs::read;".to_string()),
378 edit: TextEdit::default()
379 });
380
381 let rendered = render_file_block(&file, false);
382 assert!(rendered.lines.iter().any(|l| l.contains("Imports")));
383 }
384
385 #[test]
386 fn test_render_file_block_multiple_analyzers() {
387 let mut file = FileDiff::new("test.rs".to_string());
388
389 file.add_entry(DiffEntry {
390 line: 10,
391 analyzer: "analyzer1".to_string(),
392 original: "old1".to_string(),
393 modified: "new1".to_string(),
394 description: "desc1".to_string(),
395 import: None,
396 edit: TextEdit::default()
397 });
398
399 file.add_entry(DiffEntry {
400 line: 20,
401 analyzer: "analyzer2".to_string(),
402 original: "old2".to_string(),
403 modified: "new2".to_string(),
404 description: "desc2".to_string(),
405 import: None,
406 edit: TextEdit::default()
407 });
408
409 let rendered = render_file_block(&file, false);
410 assert!(rendered.lines.iter().any(|l| l.contains("analyzer1")));
411 assert!(rendered.lines.iter().any(|l| l.contains("analyzer2")));
412 }
413
414 #[test]
415 fn test_render_respects_capacity() {
416 let file = FileDiff::new("test.rs".to_string());
417 let rendered = render_file_block(&file, false);
418
419 assert!(rendered.lines.capacity() >= ESTIMATED_LINES_PER_FILE);
420 }
421}