1use crate::chunks::IndexedChunkMeta;
2use crate::colors::*;
3use crate::state::TuiState;
4use crate::utils::find_repo_root;
5use anyhow::Result;
6use ck_index::load_index_entry;
7use ratatui::style::{Modifier, Style};
8use ratatui::text::{Line, Span};
9use std::path::Path;
10
11pub fn execute_command(state: &mut TuiState) -> Result<()> {
12 let cmd = state.query.trim();
13
14 match cmd {
15 "/help" | "/h" | "/?" => {
16 show_help(state);
17 }
18 "/clear" | "/c" => {
19 state.results.clear();
20 state.preview_content.clear();
21 state.preview_lines.clear();
22 state.query.clear();
23 state.command_mode = false;
24 state.status_message = "Cleared results".to_string();
25 }
26 "/history" => {
27 show_history(state);
28 }
29 "/stats" => {
30 show_stats(state);
31 }
32 _ => {
33 state.status_message = format!(
34 "Unknown command: {}. Type /help for available commands",
35 cmd
36 );
37 }
38 }
39
40 Ok(())
41}
42
43fn show_help(state: &mut TuiState) {
44 let help_text = vec![
45 "━━━ COMMAND MENU ━━━".to_string(),
46 "".to_string(),
47 "Available commands:".to_string(),
48 " /help, /h, /? - Show this help".to_string(),
49 " /clear, /c - Clear results and search".to_string(),
50 " /history - Show search history".to_string(),
51 " /stats - Show index statistics".to_string(),
52 "".to_string(),
53 "━━━ KEYBINDINGS ━━━".to_string(),
54 "".to_string(),
55 " Tab - Cycle search modes (SEM/REG/HYB)".to_string(),
56 " Ctrl+V - Cycle preview modes (Heatmap/Syntax/Chunks)".to_string(),
57 " Ctrl+F - Toggle snippet/full file view".to_string(),
58 " Ctrl+D - Show chunk metadata (debug)".to_string(),
59 " Ctrl+Space - Multi-select files".to_string(),
60 " Ctrl+Up/Down - Navigate search history".to_string(),
61 " Up/Down - Navigate results".to_string(),
62 " PgUp/PgDn - Scroll preview".to_string(),
63 " Enter - Open in $EDITOR".to_string(),
64 " Esc, q, Ctrl+C - Quit".to_string(),
65 "".to_string(),
66 "━━━ SEARCH MODES ━━━".to_string(),
67 "".to_string(),
68 " SEM - Semantic: Find code by meaning".to_string(),
69 " REG - Regex: Pattern matching".to_string(),
70 " HYB - Hybrid: Combined semantic + regex".to_string(),
71 "".to_string(),
72 "━━━ PREVIEW MODES ━━━".to_string(),
73 "".to_string(),
74 " Heatmap - Semantic similarity coloring".to_string(),
75 " Syntax - Syntax highlighting".to_string(),
76 " Chunks - Function/class boundaries".to_string(),
77 "".to_string(),
78 "Press Esc to close help".to_string(),
79 ];
80
81 state.preview_lines = help_text
83 .iter()
84 .map(|line| {
85 if line.starts_with("━━━") {
86 Line::from(Span::styled(
87 line.clone(),
88 Style::default().fg(COLOR_CYAN).add_modifier(Modifier::BOLD),
89 ))
90 } else if line.starts_with(" /")
91 || line.starts_with(" Ctrl")
92 || line.starts_with(" Tab")
93 || line.starts_with(" Up")
94 || line.starts_with(" PgUp")
95 || line.starts_with(" Enter")
96 || line.starts_with(" Esc")
97 || line.starts_with(" SEM")
98 || line.starts_with(" REG")
99 || line.starts_with(" HYB")
100 || line.starts_with(" Heatmap")
101 || line.starts_with(" Syntax")
102 || line.starts_with(" Chunks")
103 {
104 if let Some(dash_pos) = line.find(" - ") {
106 let (key, desc) = line.split_at(dash_pos);
107 Line::from(vec![
108 Span::styled(
109 key.to_string(),
110 Style::default()
111 .fg(COLOR_YELLOW)
112 .add_modifier(Modifier::BOLD),
113 ),
114 Span::styled(desc.to_string(), Style::default().fg(COLOR_WHITE)),
115 ])
116 } else {
117 Line::from(Span::styled(line.clone(), Style::default().fg(COLOR_WHITE)))
118 }
119 } else if line.starts_with("Press") {
120 Line::from(Span::styled(
121 line.clone(),
122 Style::default()
123 .fg(COLOR_DARK_GRAY)
124 .add_modifier(Modifier::ITALIC),
125 ))
126 } else {
127 Line::from(Span::styled(line.clone(), Style::default().fg(COLOR_WHITE)))
128 }
129 })
130 .collect();
131
132 state.query.clear();
133 state.command_mode = false;
134 state.status_message = "Help - Press Esc to return to search".to_string();
135}
136
137pub fn show_chunks(state: &mut TuiState) {
138 if state.results.is_empty() {
140 state.status_message = "No search results - run a search first".to_string();
141 state.query.clear();
142 state.command_mode = false;
143 return;
144 }
145
146 let selected_file = state.results[state.selected_idx].file.clone();
147
148 let repo_root = find_repo_root(&selected_file);
150 let all_chunks = if let Some(root) = repo_root {
151 load_chunk_spans(&root, &selected_file).unwrap_or_default()
152 } else {
153 Vec::new()
154 };
155
156 if all_chunks.is_empty() {
157 state.status_message = format!("No chunks found for {}", selected_file.display());
158 state.query.clear();
159 state.command_mode = false;
160 return;
161 }
162
163 let mut chunks_text: Vec<String> = vec![
165 format!("━━━ CHUNK METADATA: {} ━━━", selected_file.display()),
166 "".to_string(),
167 format!("Total chunks: {}", all_chunks.len()),
168 "".to_string(),
169 ];
170
171 let mut sorted_chunks = all_chunks.clone();
173 sorted_chunks.sort_by_key(|c| c.span.line_start);
174
175 for (i, chunk) in sorted_chunks.iter().enumerate() {
177 let chunk_type = chunk.chunk_type.as_deref().unwrap_or("unknown");
178
179 chunks_text.push(format!(
180 "Chunk #{}: {} [lines {}-{}]",
181 i + 1,
182 chunk_type,
183 chunk.span.line_start,
184 chunk.span.line_end
185 ));
186
187 let mut overlaps_with = Vec::new();
189 for (j, other) in sorted_chunks.iter().enumerate() {
190 if i == j {
191 continue;
192 }
193 if chunk.span.line_start <= other.span.line_end
195 && chunk.span.line_end >= other.span.line_start
196 {
197 overlaps_with.push(j + 1);
198 }
199 }
200
201 if !overlaps_with.is_empty() {
202 chunks_text.push(format!(
203 " Overlaps with: {}",
204 overlaps_with
205 .iter()
206 .map(|n| format!("#{}", n))
207 .collect::<Vec<_>>()
208 .join(", ")
209 ));
210 }
211
212 chunks_text.push("".to_string());
213 }
214
215 chunks_text.push("Press Esc to close".to_string());
216
217 state.preview_lines = chunks_text
219 .iter()
220 .map(|line| {
221 if line.starts_with("━━━") {
222 Line::from(Span::styled(
223 line.clone(),
224 Style::default().fg(COLOR_CYAN).add_modifier(Modifier::BOLD),
225 ))
226 } else if line.starts_with("Chunk #") {
227 Line::from(Span::styled(
228 line.clone(),
229 Style::default()
230 .fg(COLOR_YELLOW)
231 .add_modifier(Modifier::BOLD),
232 ))
233 } else if line.starts_with(" Overlaps") {
234 Line::from(Span::styled(
235 line.clone(),
236 Style::default().fg(COLOR_MAGENTA),
237 ))
238 } else if line.starts_with("Total chunks") {
239 Line::from(Span::styled(
240 line.clone(),
241 Style::default()
242 .fg(COLOR_GREEN)
243 .add_modifier(Modifier::BOLD),
244 ))
245 } else if line.starts_with("Press") {
246 Line::from(Span::styled(
247 line.clone(),
248 Style::default()
249 .fg(COLOR_DARK_GRAY)
250 .add_modifier(Modifier::ITALIC),
251 ))
252 } else {
253 Line::from(Span::styled(line.clone(), Style::default().fg(COLOR_WHITE)))
254 }
255 })
256 .collect();
257
258 state.query.clear();
259 state.command_mode = false;
260 state.scroll_offset = 0;
261 state.status_message = format!(
262 "Chunk metadata for {} - Press Esc to return",
263 selected_file.display()
264 );
265}
266
267fn load_chunk_spans(repo_root: &Path, file_path: &Path) -> Result<Vec<IndexedChunkMeta>, String> {
268 let standard_path = file_path
269 .strip_prefix(repo_root)
270 .unwrap_or(file_path)
271 .to_path_buf();
272 let index_dir = repo_root.join(".ck");
273 let sidecar_path = index_dir.join(format!("{}.ck", standard_path.display()));
274
275 if !sidecar_path.exists() {
276 return Ok(Vec::new());
277 }
278
279 let entry = load_index_entry(&sidecar_path)
280 .map_err(|err| format!("Failed to load chunk metadata: {}", err))?;
281 let mut metas: Vec<IndexedChunkMeta> = entry
282 .chunks
283 .iter()
284 .map(|chunk| IndexedChunkMeta {
285 span: chunk.span.clone(),
286 chunk_type: chunk.chunk_type.clone(),
287 breadcrumb: chunk.breadcrumb.clone(),
288 ancestry: chunk.ancestry.clone().unwrap_or_default(),
289 estimated_tokens: chunk.estimated_tokens,
290 byte_length: chunk.byte_length,
291 leading_trivia: chunk.leading_trivia.clone(),
292 trailing_trivia: chunk.trailing_trivia.clone(),
293 })
294 .collect();
295
296 let has_non_module = metas
297 .iter()
298 .any(|meta| meta.chunk_type.as_deref() != Some("module"));
299 if has_non_module {
300 metas.retain(|meta| meta.chunk_type.as_deref() != Some("module"));
301 }
302
303 Ok(metas)
304}
305
306fn show_history(state: &mut TuiState) {
307 if state.search_history.is_empty() {
308 state.status_message = "No search history".to_string();
309 state.query.clear();
310 state.command_mode = false;
311 return;
312 }
313
314 let history_text: Vec<String> = std::iter::once("━━━ SEARCH HISTORY ━━━".to_string())
315 .chain(std::iter::once("".to_string()))
316 .chain(
317 state
318 .search_history
319 .iter()
320 .rev()
321 .enumerate()
322 .map(|(i, query)| format!(" {}: {}", i + 1, query)),
323 )
324 .chain(std::iter::once("".to_string()))
325 .chain(std::iter::once(
326 "Use Ctrl+Up/Down to navigate history".to_string(),
327 ))
328 .collect();
329
330 state.preview_lines = history_text
331 .iter()
332 .map(|line| {
333 if line.starts_with("━━━") {
334 Line::from(Span::styled(
335 line.clone(),
336 Style::default().fg(COLOR_CYAN).add_modifier(Modifier::BOLD),
337 ))
338 } else if line.starts_with(" ") && line.contains(": ") {
339 Line::from(Span::styled(
340 line.clone(),
341 Style::default().fg(COLOR_YELLOW),
342 ))
343 } else {
344 Line::from(Span::styled(line.clone(), Style::default().fg(COLOR_WHITE)))
345 }
346 })
347 .collect();
348
349 state.query.clear();
350 state.command_mode = false;
351 state.status_message = "Search History".to_string();
352}
353
354fn show_stats(state: &mut TuiState) {
355 let stats_text = if let Some(stats) = state.index_stats.as_ref() {
356 vec![
357 "━━━ INDEX STATISTICS ━━━".to_string(),
358 "".to_string(),
359 format!(" Path: {}", state.search_path.display()),
360 format!(" Files: {}", stats.total_files),
361 format!(
362 " Chunks: {} ({} embedded)",
363 stats.total_chunks, stats.embedded_chunks
364 ),
365 format!(" Total size: {} bytes", stats.total_size_bytes),
366 format!(" Index size: {} bytes", stats.index_size_bytes),
367 "".to_string(),
368 ]
369 } else if let Some(err) = state.index_stats_error.as_ref() {
370 vec![
371 "━━━ INDEX STATISTICS ━━━".to_string(),
372 "".to_string(),
373 format!(" Error: {}", err),
374 "".to_string(),
375 ]
376 } else {
377 vec![
378 "━━━ INDEX STATISTICS ━━━".to_string(),
379 "".to_string(),
380 " Index data unavailable".to_string(),
381 "".to_string(),
382 ]
383 };
384
385 state.preview_lines = stats_text
386 .iter()
387 .map(|line| {
388 if line.starts_with("━━━") {
389 Line::from(Span::styled(
390 line.clone(),
391 Style::default().fg(COLOR_CYAN).add_modifier(Modifier::BOLD),
392 ))
393 } else if line.starts_with(" ") {
394 Line::from(Span::styled(
395 line.clone(),
396 Style::default().fg(COLOR_YELLOW),
397 ))
398 } else {
399 Line::from(Span::styled(line.clone(), Style::default().fg(COLOR_WHITE)))
400 }
401 })
402 .collect();
403
404 state.query.clear();
405 state.command_mode = false;
406 state.status_message = "Index Statistics".to_string();
407}