metis_docs_cli/commands/
search.rs

1use crate::workspace;
2use anyhow::Result;
3use clap::Args;
4use metis_core::dal::database::models::Document;
5use metis_core::{Application, Database};
6
7#[derive(Args)]
8pub struct SearchCommand {
9    /// Search query for full-text search across document content
10    pub query: String,
11
12    /// Maximum number of results to show
13    #[arg(short = 'l', long, default_value = "20")]
14    pub limit: usize,
15}
16
17impl SearchCommand {
18    pub async fn execute(&self) -> Result<()> {
19        // 1. Validate we're in a metis workspace
20        let (workspace_exists, metis_dir) = workspace::has_metis_vault();
21        if !workspace_exists {
22            anyhow::bail!("Not in a Metis workspace. Run 'metis init' to create one.");
23        }
24        let metis_dir = metis_dir.unwrap();
25
26        // 2. Initialize the database and application
27        let db_path = metis_dir.join("metis.db");
28        let database = Database::new(db_path.to_str().unwrap())
29            .map_err(|e| anyhow::anyhow!("Failed to open database: {}", e))?;
30        let mut app = Application::new(database);
31
32        // 3. Perform full-text search
33        let results = self.perform_search(&mut app, &self.query)?;
34
35        // 4. Limit results
36        let limited_results: Vec<_> = results.into_iter().take(self.limit).collect();
37
38        // 5. Display results
39        self.display_results(&limited_results)?;
40
41        Ok(())
42    }
43
44    fn perform_search(&self, app: &mut Application, query: &str) -> Result<Vec<Document>> {
45        app.with_database(|db_service| db_service.search_documents(query))
46            .map_err(|e| anyhow::anyhow!("Search failed: {}", e))
47    }
48
49    fn display_results(&self, documents: &[Document]) -> Result<()> {
50        if documents.is_empty() {
51            println!("No documents found for query: \"{}\"", self.query);
52            return Ok(());
53        }
54
55        self.display_table(documents)?;
56        Ok(())
57    }
58
59    fn display_table(&self, documents: &[Document]) -> Result<()> {
60        // Print header
61        println!("{:<50} {:<12} {:<120}", "ID", "Type", "Path");
62        println!("{}", "-".repeat(182));
63
64        for doc in documents {
65            let id = truncate(&doc.id, 49);
66            let doc_type = truncate(&doc.document_type, 11);
67
68            // Extract relative path from .metis directory
69            let relative_path = if let Some(metis_pos) = doc.filepath.find(".metis/") {
70                &doc.filepath[metis_pos + 7..] // Skip ".metis/"
71            } else {
72                &doc.filepath
73            };
74            let path = truncate(relative_path, 119);
75
76            println!("{:<50} {:<12} {:<120}", id, doc_type, path);
77        }
78
79        println!(
80            "\nFound {} document(s) for \"{}\"",
81            documents.len(),
82            self.query
83        );
84        Ok(())
85    }
86}
87
88// Helper functions
89fn truncate(s: &str, max_len: usize) -> String {
90    if s.len() <= max_len {
91        s.to_string()
92    } else {
93        format!("{}...", &s[..max_len.saturating_sub(3)])
94    }
95}
96
97#[cfg(test)]
98mod tests {
99    use super::*;
100
101    #[test]
102    fn test_truncate() {
103        assert_eq!(truncate("short", 10), "short");
104        assert_eq!(truncate("this is a very long string", 10), "this is...");
105        assert_eq!(truncate("exactly_10", 10), "exactly_10");
106    }
107}