Skip to main content

vtcode_commons/
project.rs

1#![expect(
2    clippy::let_underscore_must_use,
3    reason = "Project rendering intentionally ignores infallible writes into String buffers."
4)]
5
6//! Project-related utilities and structures
7
8use crate::utils::{extract_readme_excerpt, extract_toml_str};
9use std::fmt::Write as _;
10use std::path::{Path, PathBuf};
11use tokio::fs;
12
13/// Lightweight project overview extracted from workspace files
14pub 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
68/// Build a minimal project overview from Cargo.toml and README.md
69pub 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}