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. Sync before searching to catch external edits
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 for sync: {}", e))?;
30        let app = Application::new(database);
31        app.sync_directory(&metis_dir)
32            .await
33            .map_err(|e| anyhow::anyhow!("Failed to sync workspace: {}", e))?;
34
35        // 3. Initialize the database and application for search
36        let database = Database::new(db_path.to_str().unwrap())
37            .map_err(|e| anyhow::anyhow!("Failed to open database: {}", e))?;
38        let mut app = Application::new(database);
39
40        // 4. Perform full-text search
41        let results = self.perform_search(&mut app, &self.query)?;
42
43        // 5. Limit results
44        let limited_results: Vec<_> = results.into_iter().take(self.limit).collect();
45
46        // 6. Display results
47        self.display_results(&limited_results)?;
48
49        Ok(())
50    }
51
52    fn perform_search(&self, app: &mut Application, query: &str) -> Result<Vec<Document>> {
53        app.with_database(|db_service| db_service.search_documents(query))
54            .map_err(|e| anyhow::anyhow!("Search failed: {}", e))
55    }
56
57    fn display_results(&self, documents: &[Document]) -> Result<()> {
58        if documents.is_empty() {
59            println!("No documents found for query: \"{}\"", self.query);
60            return Ok(());
61        }
62
63        self.display_table(documents)?;
64        Ok(())
65    }
66
67    fn display_table(&self, documents: &[Document]) -> Result<()> {
68        // Print header
69        println!("{:<50} {:<12} {:<120}", "ID", "Type", "Path");
70        println!("{}", "-".repeat(182));
71
72        for doc in documents {
73            let id = truncate(&doc.id, 49);
74            let doc_type = truncate(&doc.document_type, 11);
75
76            // Filepath is now stored relative to .metis directory
77            let path = truncate(&doc.filepath, 119);
78
79            println!("{:<50} {:<12} {:<120}", id, doc_type, path);
80        }
81
82        println!(
83            "\nFound {} document(s) for \"{}\"",
84            documents.len(),
85            self.query
86        );
87        Ok(())
88    }
89}
90
91// Helper functions
92fn truncate(s: &str, max_len: usize) -> String {
93    if s.len() <= max_len {
94        s.to_string()
95    } else {
96        format!("{}...", &s[..max_len.saturating_sub(3)])
97    }
98}
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103
104    #[test]
105    fn test_truncate() {
106        assert_eq!(truncate("short", 10), "short");
107        assert_eq!(truncate("this is a very long string", 10), "this is...");
108        assert_eq!(truncate("exactly_10", 10), "exactly_10");
109    }
110}