use std::path::PathBuf;
use super::{Project, ProjectId, ProjectSource};
#[derive(Debug)]
pub enum DetectionResult {
Found(ProjectId),
NoMatch { detected_path: Option<PathBuf> },
}
pub fn detect_project(message: &str, projects: &[Project]) -> DetectionResult {
let lower = message.to_lowercase();
for project in projects {
if lower.contains(&project.name.to_lowercase()) {
return DetectionResult::Found(project.id);
}
}
if let Some(path) = extract_path(message) {
for project in projects {
if project
.paths
.iter()
.any(|p| path.starts_with(p) || p.starts_with(&path))
{
return DetectionResult::Found(project.id);
}
}
return DetectionResult::NoMatch {
detected_path: Some(path),
};
}
for project in projects {
for tag in &project.tags {
if lower.contains(&tag.to_lowercase()) {
return DetectionResult::Found(project.id);
}
}
}
DetectionResult::NoMatch {
detected_path: None,
}
}
pub fn extract_path(message: &str) -> Option<PathBuf> {
for word in message.split_whitespace() {
let cleaned = word.trim_matches(|c: char| {
!c.is_alphanumeric() && c != '/' && c != '.' && c != '-' && c != '_'
});
if cleaned.starts_with('/') && cleaned.len() > 2 {
let path = PathBuf::from(cleaned);
if path.parent().is_some() {
return Some(path);
}
}
}
for word in message.split_whitespace() {
let cleaned = word.trim_matches(|c: char| {
!c.is_alphanumeric() && c != '/' && c != '.' && c != '-' && c != '_' && c != '~'
});
if cleaned.starts_with("~/") && cleaned.len() > 2 {
if let Some(home) = std::env::var_os("HOME") {
let expanded = cleaned.replacen("~", &home.to_string_lossy(), 1);
return Some(PathBuf::from(expanded));
}
}
}
None
}
pub fn find_by_id(projects: &[Project], id: ProjectId) -> Option<&Project> {
projects.iter().find(|p| p.id == id)
}
pub fn find_by_name<'a>(projects: &'a [Project], name: &str) -> Option<&'a Project> {
let lower = name.to_lowercase();
projects.iter().find(|p| p.name.to_lowercase() == lower)
}
#[cfg(test)]
mod tests {
use super::*;
fn make_projects() -> Vec<Project> {
let mut oxios = Project::new("oxios", ProjectSource::Manual);
oxios
.paths
.push(PathBuf::from("/Volumes/MERCURY/PROJECTS/oxios"));
oxios.add_tag("agent-os");
let mut oxi = Project::new("oxi", ProjectSource::Manual);
oxi.paths
.push(PathBuf::from("/Volumes/MERCURY/PROJECTS/oxi"));
oxi.add_tag("sdk");
let mut blog = Project::new("my-blog", ProjectSource::Manual);
blog.add_tag("writing");
blog.add_tag("content");
vec![oxios, oxi, blog]
}
#[test]
fn test_detect_by_name() {
let projects = make_projects();
let result = detect_project("oxios 코드리뷰해줘", &projects);
assert!(matches!(result, DetectionResult::Found(id) if id == projects[0].id));
}
#[test]
fn test_detect_by_path() {
let projects = make_projects();
let result = detect_project("/Volumes/MERCURY/PROJECTS/oxios에서 작업", &projects);
assert!(matches!(result, DetectionResult::Found(id) if id == projects[0].id));
}
#[test]
fn test_detect_by_tag() {
let projects = make_projects();
let result = detect_project("writing 관련 도움이 필요해", &projects);
assert!(matches!(result, DetectionResult::Found(id) if id == projects[2].id));
}
#[test]
fn test_detect_no_match_with_path() {
let projects = make_projects();
let result = detect_project("/Volumes/MERCURY/PROJECTS/unknown 에서 작업", &projects);
assert!(matches!(
result,
DetectionResult::NoMatch {
detected_path: Some(_)
}
));
}
#[test]
fn test_detect_no_match() {
let projects = make_projects();
let result = detect_project("오늘 점심 뭐 먹지?", &projects);
assert!(matches!(
result,
DetectionResult::NoMatch {
detected_path: None
}
));
}
#[test]
fn test_extract_path() {
assert_eq!(
extract_path("/Volumes/MERCURY/PROJECTS/oxios"),
Some(PathBuf::from("/Volumes/MERCURY/PROJECTS/oxios"))
);
assert_eq!(extract_path("no path here"), None);
}
#[test]
fn test_find_by_name() {
let projects = make_projects();
assert!(find_by_name(&projects, "oxios").is_some());
assert!(find_by_name(&projects, "Oxios").is_some()); assert!(find_by_name(&projects, "nonexistent").is_none());
}
}