cargo_context_core/pack/
render.rs1use std::collections::HashSet;
2use std::path::Path;
3
4use crate::collect::{self, Diagnostics, Diff, EntryPoints, RelatedTests, WorkspaceMap};
5use crate::scrub::Scrubber;
6use crate::tokenize::Tokenizer;
7
8use super::Section;
9
10pub(super) fn mk_section(name: &str, content: &str, tokenizer: &Tokenizer) -> Section {
11 Section {
12 name: name.into(),
13 content: content.into(),
14 token_estimate: tokenizer.count(content),
15 }
16}
17
18pub(super) fn project_name(root: Option<&std::path::Path>) -> String {
19 root.and_then(|p| p.file_name())
20 .map(|n| n.to_string_lossy().into_owned())
21 .unwrap_or_else(|| "unknown".to_string())
22}
23
24pub(crate) fn lang_for_path(path: &Path) -> &'static str {
25 match path.extension().and_then(|e| e.to_str()) {
26 Some("rs") => "rust",
27 Some("toml") => "toml",
28 Some("yaml" | "yml") => "yaml",
29 Some("json") => "json",
30 Some("md") => "markdown",
31 Some("sh" | "bash") => "bash",
32 Some("py") => "python",
33 Some("ts") => "typescript",
34 Some("js") => "javascript",
35 _ => "",
36 }
37}
38
39fn path_matches_suffix(haystack: &Path, needle: &Path) -> bool {
40 let h = haystack.to_string_lossy();
41 let n = needle.to_string_lossy();
42 h.ends_with(n.as_ref()) || n.ends_with(h.as_ref())
43}
44
45pub(super) fn render_tests(rt: &RelatedTests) -> String {
46 let mut out = String::new();
47 out.push_str(&format!("{} related test file(s).\n\n", rt.files.len()));
48 for f in &rt.files {
49 let kind = match f.kind {
50 collect::TestKind::Integration => "integration",
51 collect::TestKind::UnitInline => "unit (inline)",
52 };
53 let reason = if f.matched_stems.is_empty() {
54 String::new()
55 } else {
56 format!(" — matched: `{}`", f.matched_stems.join("`, `"))
57 };
58 out.push_str(&format!(
59 "### `{}` — {} / {} ({} tests){}\n",
60 f.path.display(),
61 f.crate_name,
62 kind,
63 f.functions.len(),
64 reason,
65 ));
66 for fun in &f.functions {
67 out.push_str(&format!("- `{}`\n", fun.signature.trim()));
68 }
69 out.push('\n');
70 }
71 out
72}
73
74pub(super) fn render_entry(ep: &EntryPoints) -> String {
75 let mut out = String::new();
76 out.push_str(&format!("{} entry file(s).\n\n", ep.files.len()));
77 for f in &ep.files {
78 let kind = match f.kind {
79 collect::EntryKind::Main => "main",
80 collect::EntryKind::Lib => "lib",
81 };
82 let tag = if f.parse_failed { " (unparsed)" } else { "" };
83 out.push_str(&format!(
84 "### `{}` — {} / {} ({} lines){}\n",
85 f.path.display(),
86 f.crate_name,
87 kind,
88 f.raw_line_count,
89 tag,
90 ));
91 out.push_str("```rust\n");
92 out.push_str(&f.rendered);
93 if !f.rendered.ends_with('\n') {
94 out.push('\n');
95 }
96 out.push_str("```\n\n");
97 }
98 out
99}
100
101pub(super) fn render_map(m: WorkspaceMap) -> String {
102 let mut out = String::new();
103 if let Some(root) = &m.root_package {
104 out.push_str(&format!("- Root package: `{root}`\n"));
105 }
106 let members = m.member_names();
107 if !members.is_empty() {
108 out.push_str(&format!("- Workspace members ({}): ", members.len()));
109 out.push_str(&members.join(", "));
110 out.push('\n');
111 }
112 let deps = m.external_dep_names();
113 if !deps.is_empty() {
114 let preview: Vec<&str> = deps.iter().take(12).copied().collect();
115 out.push_str(&format!(
116 "- Key dependencies: {}{}\n",
117 preview.join(", "),
118 if deps.len() > preview.len() {
119 format!(" (+{} more)", deps.len() - preview.len())
120 } else {
121 String::new()
122 }
123 ));
124 }
125 out
126}
127
128pub(super) fn render_diff_ordered(
129 d: &Diff,
130 error_files: &[std::path::PathBuf],
131 scrubber: &Scrubber,
132) -> String {
133 let error_set: HashSet<&Path> = error_files.iter().map(|p| p.as_path()).collect();
134
135 let mut files: Vec<&collect::FileDiff> = d.files.iter().collect();
136 files.sort_by_key(|f| {
137 let has_error = error_set.contains(f.path.as_path())
138 || error_files.iter().any(|e| path_matches_suffix(&f.path, e));
139 (!has_error, f.path.to_string_lossy().into_owned())
140 });
141
142 let errored_count = files
143 .iter()
144 .filter(|f| {
145 error_set.contains(f.path.as_path())
146 || error_files.iter().any(|e| path_matches_suffix(&f.path, e))
147 })
148 .count();
149 let path_redacted_count = files
150 .iter()
151 .filter(|f| scrubber.is_path_redacted(&f.path))
152 .count();
153
154 let mut out = String::new();
155 let mut header = format!("{} file(s) changed", d.files.len());
156 if errored_count > 0 {
157 header.push_str(&format!(
158 "; {errored_count} touched by compiler errors (shown first)"
159 ));
160 }
161 if path_redacted_count > 0 {
162 header.push_str(&format!("; {path_redacted_count} redacted by path rules"));
163 }
164 out.push_str(&format!("{header}.\n\n"));
165
166 for f in files {
167 let status = format!("{:?}", f.status).to_lowercase();
168 let error_marker = if error_set.contains(f.path.as_path())
169 || error_files.iter().any(|e| path_matches_suffix(&f.path, e))
170 {
171 " ⚠"
172 } else {
173 ""
174 };
175 let redacted = scrubber.is_path_redacted(&f.path);
176 let redact_marker = if redacted { " 🔒" } else { "" };
177
178 out.push_str(&format!(
179 "### `{}` — {status}{error_marker}{redact_marker}\n",
180 f.path.display()
181 ));
182 if let Some(old) = &f.old_path {
183 out.push_str(&format!("- Renamed from `{}`\n", old.display()));
184 }
185 if redacted {
186 out.push_str(&format!(
187 "[REDACTED FILE: {} — {} hunk(s) elided by scrub.yaml path rules]\n",
188 f.path.display(),
189 f.hunks.len()
190 ));
191 } else {
192 for h in &f.hunks {
193 out.push_str(&format!(
194 "```diff\n@@ -{},{} +{},{} @@\n{}```\n",
195 h.old_start, h.old_lines, h.new_start, h.new_lines, h.body
196 ));
197 }
198 }
199 out.push('\n');
200 }
201 out
202}
203
204pub(super) fn render_diagnostics(d: &Diagnostics) -> String {
205 let mut out = String::new();
206 let err_count = d
207 .diagnostics
208 .iter()
209 .filter(|x| x.level == crate::collect::DiagLevel::Error)
210 .count();
211 out.push_str(&format!(
212 "Build {}; {} diagnostic(s), {} error(s).\n\n",
213 if d.success { "succeeded" } else { "failed" },
214 d.diagnostics.len(),
215 err_count,
216 ));
217 for diag in &d.diagnostics {
218 let code = diag.code.as_deref().unwrap_or("");
219 out.push_str(&format!(
220 "- **{:?}** {}: {}\n",
221 diag.level, code, diag.message
222 ));
223 if let Some(file) = diag.primary_file()
224 && let Some(span) = diag.spans.iter().find(|s| s.is_primary)
225 {
226 out.push_str(&format!(
227 " at `{}:{}:{}`\n",
228 file.display(),
229 span.line_start,
230 span.col_start
231 ));
232 }
233 }
234 out
235}