vtcode_commons/
project.rs1#![expect(
2 clippy::let_underscore_must_use,
3 reason = "Project rendering intentionally ignores infallible writes into String buffers."
4)]
5
6use crate::utils::{extract_readme_excerpt, extract_toml_str};
9use std::fmt::Write as _;
10use std::path::{Path, PathBuf};
11use tokio::fs;
12
13pub struct ProjectOverview {
15 pub name: Option<String>,
16 pub version: Option<String>,
17 pub description: Option<String>,
18 pub readme_excerpt: Option<String>,
19 pub root: PathBuf,
20}
21
22impl ProjectOverview {
23 pub fn short_for_display(&self) -> String {
24 let mut out = String::new();
25 if let Some(name) = &self.name {
26 let _ = write!(out, "Project: {name}");
27 }
28 if let Some(ver) = &self.version {
29 if !out.is_empty() {
30 out.push(' ');
31 }
32 let _ = write!(out, "v{ver}");
33 }
34 if !out.is_empty() {
35 out.push('\n');
36 }
37 if let Some(desc) = &self.description {
38 out.push_str(desc);
39 out.push('\n');
40 }
41 let _ = write!(out, "Root: {}", self.root.display());
42 out
43 }
44
45 pub fn as_prompt_block(&self) -> String {
46 let mut s = String::new();
47 if let Some(name) = &self.name {
48 let _ = writeln!(s, "- Name: {name}");
49 }
50 if let Some(ver) = &self.version {
51 let _ = writeln!(s, "- Version: {ver}");
52 }
53 if let Some(desc) = &self.description {
54 let _ = writeln!(s, "- Description: {desc}");
55 }
56 let _ = writeln!(s, "- Workspace Root: {}", self.root.display());
57 if let Some(excerpt) = &self.readme_excerpt {
58 s.push_str("- README Excerpt: \n");
59 s.push_str(excerpt);
60 if !excerpt.ends_with('\n') {
61 s.push('\n');
62 }
63 }
64 s
65 }
66}
67
68pub async fn build_project_overview(root: &Path) -> Option<ProjectOverview> {
70 let cargo_toml_path = root.join("Cargo.toml");
71 let readme_path = root.join("README.md");
72 let (cargo_toml, readme) = tokio::join!(fs::read_to_string(cargo_toml_path), fs::read_to_string(readme_path));
73
74 let metadata = cargo_toml.ok().map(|contents| ProjectMetadata::from_cargo_toml(&contents));
75 let readme_excerpt = match readme {
76 Ok(contents) => Some(extract_readme_excerpt(&contents, 1200)),
77 Err(_) => read_readme_fallback(root).await,
78 };
79
80 let overview = ProjectOverview {
81 name: metadata.as_ref().and_then(|metadata| metadata.name.clone()),
82 version: metadata.as_ref().and_then(|metadata| metadata.version.clone()),
83 description: metadata.and_then(|metadata| metadata.description),
84 readme_excerpt,
85 root: root.to_path_buf(),
86 };
87
88 (overview.name.is_some()
89 || overview.version.is_some()
90 || overview.description.is_some()
91 || overview.readme_excerpt.is_some())
92 .then_some(overview)
93}
94
95struct ProjectMetadata {
96 name: Option<String>,
97 version: Option<String>,
98 description: Option<String>,
99}
100
101impl ProjectMetadata {
102 fn from_cargo_toml(contents: &str) -> Self {
103 Self {
104 name: extract_toml_str(contents, "name"),
105 version: extract_toml_str(contents, "version"),
106 description: extract_toml_str(contents, "description"),
107 }
108 }
109}
110
111async fn read_readme_fallback(root: &Path) -> Option<String> {
112 for alt in ["QUICKSTART.md", "user-context.md"] {
113 let path = root.join(alt);
114 if let Ok(contents) = fs::read_to_string(path).await {
115 return Some(extract_readme_excerpt(&contents, 800));
116 }
117 }
118 None
119}
120
121#[cfg(test)]
122mod tests {
123 use super::*;
124 use std::fs;
125 use tempfile::tempdir;
126
127 #[tokio::test]
128 async fn overview_keeps_metadata_only_projects() {
129 let root = tempdir().expect("temporary project directory");
130 fs::write(root.path().join("Cargo.toml"), "version = \"1.2.3\"\ndescription = \"metadata\"\n")
131 .expect("write Cargo.toml");
132
133 let overview = build_project_overview(root.path())
134 .await
135 .expect("metadata should produce an overview");
136
137 assert_eq!(overview.version.as_deref(), Some("1.2.3"));
138 assert_eq!(overview.description.as_deref(), Some("metadata"));
139 }
140}