1pub mod parser;
2
3use crate::db::get_db;
4use crate::errors::{DialError, Result};
5use crate::output::{blue, bold, dim, print_success, yellow};
6use std::env;
7use std::path::Path;
8use walkdir::WalkDir;
9
10pub fn index_specs(specs_dir: &str) -> Result<bool> {
11 let specs_path = Path::new(specs_dir);
12
13 if !specs_path.exists() {
14 return Err(DialError::SpecsDirNotFound(specs_dir.to_string()));
15 }
16
17 let conn = get_db(None)?;
18
19 conn.execute("DELETE FROM spec_sections", [])?;
21
22 let md_files: Vec<_> = WalkDir::new(specs_path)
24 .into_iter()
25 .filter_map(|e| e.ok())
26 .filter(|e| {
27 e.path()
28 .extension()
29 .map(|ext| ext == "md")
30 .unwrap_or(false)
31 })
32 .collect();
33
34 if md_files.is_empty() {
35 println!("{}", yellow(&format!("No markdown files found in '{}'.", specs_dir)));
36 return Ok(true);
37 }
38
39 let cwd = env::current_dir()?;
40 let mut total_sections = 0;
41
42 for entry in &md_files {
43 let md_path = entry.path();
44 let relative_path = md_path
45 .strip_prefix(&cwd)
46 .unwrap_or(md_path)
47 .to_string_lossy()
48 .to_string();
49
50 let sections = parser::parse_markdown_sections(md_path)?;
51
52 for section in sections {
53 if !section.content.is_empty() {
54 conn.execute(
55 "INSERT INTO spec_sections (file_path, heading_path, level, content)
56 VALUES (?1, ?2, ?3, ?4)",
57 rusqlite::params![relative_path, section.heading_path, section.level, section.content],
58 )?;
59 total_sections += 1;
60 }
61 }
62 }
63
64 print_success(&format!(
65 "Indexed {} sections from {} files.",
66 total_sections,
67 md_files.len()
68 ));
69
70 Ok(true)
71}
72
73pub fn spec_search(query: &str) -> Result<Vec<SpecSearchResult>> {
74 let conn = get_db(None)?;
75
76 let mut stmt = conn.prepare(
77 "SELECT s.id, s.file_path, s.heading_path, s.content
78 FROM spec_sections s
79 INNER JOIN spec_sections_fts fts ON s.id = fts.rowid
80 WHERE spec_sections_fts MATCH ?1
81 ORDER BY rank
82 LIMIT 10",
83 )?;
84
85 let rows: Vec<SpecSearchResult> = stmt
86 .query_map([query], |row| {
87 Ok(SpecSearchResult {
88 id: row.get(0)?,
89 file_path: row.get(1)?,
90 heading_path: row.get(2)?,
91 content: row.get(3)?,
92 })
93 })?
94 .collect::<std::result::Result<Vec<_>, _>>()?;
95
96 if rows.is_empty() {
97 println!("{}", dim(&format!("No spec sections matching '{}'.", query)));
98 return Ok(rows);
99 }
100
101 println!("{}", bold(&format!("Spec sections matching '{}':", query)));
102 println!("{}", "=".repeat(60));
103
104 for row in &rows {
105 let id_str = format!("[{}]", row.id);
106 println!("\n{} {}", blue(&id_str), bold(&row.heading_path));
107 println!("{}", dim(&format!(" File: {}", row.file_path)));
108 let preview = if row.content.len() > 200 {
110 format!("{}...", &row.content[..200])
111 } else {
112 row.content.clone()
113 };
114 println!(" {}", preview);
115 }
116
117 Ok(rows)
118}
119
120#[derive(Debug, Clone)]
121pub struct SpecSearchResult {
122 pub id: i64,
123 pub file_path: String,
124 pub heading_path: String,
125 pub content: String,
126}
127
128pub fn spec_show(section_id: i64) -> Result<Option<SpecSearchResult>> {
129 let conn = get_db(None)?;
130
131 let mut stmt = conn.prepare(
132 "SELECT id, file_path, heading_path, content FROM spec_sections WHERE id = ?1",
133 )?;
134
135 let result = stmt
136 .query_row([section_id], |row| {
137 Ok(SpecSearchResult {
138 id: row.get(0)?,
139 file_path: row.get(1)?,
140 heading_path: row.get(2)?,
141 content: row.get(3)?,
142 })
143 })
144 .ok();
145
146 match &result {
147 Some(spec) => {
148 println!("{}", bold(&spec.heading_path));
149 println!("{}", dim(&format!("File: {}", spec.file_path)));
150 println!("{}", "=".repeat(60));
151 println!("{}", spec.content);
152 }
153 None => {
154 return Err(DialError::SpecSectionNotFound(section_id));
155 }
156 }
157
158 Ok(result)
159}
160
161pub fn spec_list() -> Result<()> {
162 let conn = get_db(None)?;
163
164 let mut stmt = conn.prepare(
165 "SELECT id, file_path, heading_path, level
166 FROM spec_sections ORDER BY file_path, id",
167 )?;
168
169 let rows: Vec<(i64, String, String, i32)> = stmt
170 .query_map([], |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?)))?
171 .collect::<std::result::Result<Vec<_>, _>>()?;
172
173 if rows.is_empty() {
174 println!("{}", dim("No spec sections indexed. Run 'dial index' first."));
175 return Ok(());
176 }
177
178 println!("{}", bold("Indexed Spec Sections"));
179 println!("{}", "=".repeat(60));
180
181 let mut current_file = String::new();
182 for (id, file_path, heading_path, level) in rows {
183 if file_path != current_file {
184 current_file = file_path.clone();
185 println!("\n{}", blue(¤t_file));
186 }
187 let indent = " ".repeat(level as usize);
188 println!(" {}[{}] {}", indent, id, heading_path);
189 }
190
191 Ok(())
192}