Skip to main content

cargo_context_core/pack/
render.rs

1use 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}