use anyhow::Result;
use crossterm::event::{self, Event, KeyEvent, MouseEvent};
use ratatui::{backend::CrosstermBackend, Terminal};
use std::io;
use std::path::PathBuf;
use std::sync::{mpsc, Arc};
use std::time::{Duration, Instant};
use crate::cache::CacheManager;
use crate::indexer::Indexer;
use crate::models::{IndexConfig, SearchResult};
use crate::query::{QueryEngine, QueryFilter};
use super::effects::EffectManager;
use super::history::{QueryFilters, QueryHistory};
use super::input::{InputField, KeyCommand};
use super::mouse::{MouseAction, MouseState};
use super::results::ResultList;
use super::terminal::TerminalCapabilities;
use super::theme::ThemeManager;
use super::ui;
pub struct InteractiveApp {
input: InputField,
results: ResultList,
history: QueryHistory,
filters: QueryFilters,
engine: QueryEngine,
cache: CacheManager,
mode: AppMode,
capabilities: TerminalCapabilities,
theme: ThemeManager,
effects: EffectManager,
mouse: MouseState,
filter_badge_positions: super::mouse::FilterBadgePositions,
index_status: IndexStatusState,
should_quit: bool,
focus_state: FocusState,
cwd: PathBuf,
error_message: Option<String>,
info_message: Option<String>,
preview_content: Option<FilePreview>,
searching: bool,
search_rx: Option<mpsc::Receiver<Result<crate::models::QueryResponse>>>,
indexing: bool,
index_rx: Option<mpsc::Receiver<Result<crate::models::IndexStats>>>,
index_progress_rx: Option<mpsc::Receiver<(usize, usize, String)>>,
indexing_start_time: Option<Instant>,
filter_change_time: Option<Instant>,
filter_debounce_ms: u64,
filter_selector: Option<super::filter_selector::FilterSelector>,
info_message_time: Option<Instant>,
needs_full_clear: bool,
}
#[derive(Debug, Clone)]
pub struct FilePreview {
path: String,
content: Vec<String>,
center_line: usize,
scroll_offset: usize,
language: crate::models::Language,
}
#[derive(Debug, Clone, PartialEq)]
pub enum AppMode {
Normal,
Help,
Indexing,
FilePreview,
FilterSelector,
}
#[derive(Debug, Clone, PartialEq)]
pub enum FocusState {
Input,
Filters,
Results,
}
#[derive(Debug, Clone)]
pub enum SymbolIndexingState {
Running { processed: usize, total: usize },
Completed,
Failed,
NotStarted,
}
#[derive(Debug, Clone)]
pub enum IndexStatusState {
Ready {
file_count: usize,
last_updated: String,
symbol_status: SymbolIndexingState,
},
Missing,
Stale {
files_changed: usize,
symbol_status: SymbolIndexingState,
},
Indexing {
current: usize,
total: usize,
status: String,
symbol_status: SymbolIndexingState,
},
}
impl InteractiveApp {
pub fn new() -> Result<Self> {
let cwd = std::env::current_dir()?;
let cache = CacheManager::new(&cwd);
let cache2 = CacheManager::new(&cwd); let engine = QueryEngine::new(cache2);
let capabilities = TerminalCapabilities::detect();
let theme = ThemeManager::detect();
let history = QueryHistory::load().unwrap_or_else(|_| QueryHistory::new(1000));
let index_status = if cache.exists() {
match cache.stats() {
Ok(stats) => {
let symbol_status = match crate::background_indexer::BackgroundIndexer::get_status(cache.path()) {
Ok(Some(status)) => match status.state {
crate::background_indexer::IndexerState::Running => {
SymbolIndexingState::Running {
processed: status.processed_files,
total: status.total_files,
}
}
crate::background_indexer::IndexerState::Completed => {
SymbolIndexingState::Completed
}
crate::background_indexer::IndexerState::Failed => {
SymbolIndexingState::Failed
}
},
_ => SymbolIndexingState::NotStarted,
};
IndexStatusState::Ready {
file_count: stats.total_files,
last_updated: stats.last_updated,
symbol_status,
}
},
Err(_) => IndexStatusState::Missing,
}
} else {
IndexStatusState::Missing
};
Ok(Self {
input: InputField::new(),
results: ResultList::new(500),
history,
filters: QueryFilters::default(),
engine,
cache,
mode: AppMode::Normal,
capabilities,
theme,
effects: EffectManager::new(),
mouse: MouseState::new(),
filter_badge_positions: super::mouse::FilterBadgePositions::default(),
index_status,
should_quit: false,
focus_state: FocusState::Input, cwd,
error_message: None,
info_message: None,
preview_content: None,
searching: false,
search_rx: None,
indexing: false,
index_rx: None,
index_progress_rx: None,
indexing_start_time: None,
filter_change_time: None,
filter_debounce_ms: 500, filter_selector: None,
info_message_time: None,
needs_full_clear: false,
})
}
pub fn run(&mut self) -> Result<()> {
if self.history.is_empty() {
self.mode = AppMode::Help;
}
let mut terminal = Self::setup_terminal()?;
let needs_index = matches!(self.index_status, IndexStatusState::Missing);
let result = self.event_loop(&mut terminal, needs_index);
Self::restore_terminal(terminal)?;
if let Err(e) = self.history.save() {
eprintln!("Warning: Failed to save history: {}", e);
}
result
}
fn event_loop(&mut self, terminal: &mut Terminal<CrosstermBackend<io::Stdout>>, needs_index: bool) -> Result<()> {
let mut last_frame = Instant::now();
let frame_duration = Duration::from_millis(16); let mut need_editor_open: Option<SearchResult> = None;
let mut first_frame = true;
while !self.should_quit {
if first_frame && needs_index {
self.trigger_index()?;
first_frame = false;
}
let terminal_size = terminal.size()?;
terminal.draw(|f| {
if self.needs_full_clear {
f.render_widget(ratatui::widgets::Clear, f.area());
self.needs_full_clear = false;
}
ui::render(f, self)
})?;
if let Some(result) = need_editor_open.take() {
self.open_in_editor_suspended(terminal, &result)?;
}
if event::poll(Duration::from_millis(50))? {
match event::read()? {
Event::Key(key) => {
if let Some(result) = self.handle_key_event_with_editor(key)? {
need_editor_open = Some(result);
}
}
Event::Mouse(mouse) => self.handle_mouse_event(mouse, (terminal_size.width, terminal_size.height)),
Event::Resize(_, _) => {
}
_ => {}
}
}
if let Some(ref rx) = self.search_rx {
if let Ok(result) = rx.try_recv() {
match result {
Ok(response) => {
let flat_results = response.results.iter()
.flat_map(|file_group| {
file_group.matches.iter().map(move |m| {
crate::models::SearchResult {
path: file_group.path.clone(),
lang: crate::models::Language::Unknown,
kind: m.kind.clone(),
symbol: m.symbol.clone(),
span: m.span.clone(),
preview: m.preview.clone(),
dependencies: file_group.dependencies.clone(),
}
})
})
.collect();
self.results.set_results(flat_results);
self.error_message = None;
let pattern = self.input.value().to_string();
self.history.add(pattern, self.filters.clone());
let should_auto_focus = !self.results.is_empty()
&& self.focus_state != FocusState::Input;
if should_auto_focus {
self.focus_state = FocusState::Results;
}
}
Err(e) => {
self.error_message = Some(format!("Search error: {}", e));
self.results.clear();
}
}
self.searching = false;
self.search_rx = None;
}
}
if let Some(change_time) = self.filter_change_time {
if change_time.elapsed() >= Duration::from_millis(self.filter_debounce_ms) {
if !self.input.value().trim().is_empty() && !self.searching {
let _ = self.execute_search();
}
self.filter_change_time = None;
}
}
if let Some(info_time) = self.info_message_time {
if info_time.elapsed() >= Duration::from_secs(3) {
self.info_message = None;
self.info_message_time = None;
}
}
if let Some(ref rx) = self.index_progress_rx {
if let Ok((current, total, status)) = rx.try_recv() {
let symbol_status = match &self.index_status {
IndexStatusState::Indexing { symbol_status, .. } => symbol_status.clone(),
IndexStatusState::Ready { symbol_status, .. } => symbol_status.clone(),
IndexStatusState::Stale { symbol_status, .. } => symbol_status.clone(),
IndexStatusState::Missing => SymbolIndexingState::NotStarted,
};
self.index_status = IndexStatusState::Indexing {
current,
total,
status,
symbol_status,
};
}
}
if let Some(ref rx) = self.index_rx {
if let Ok(result) = rx.try_recv() {
match result {
Ok(stats) => {
let symbol_status = match &self.index_status {
IndexStatusState::Indexing { symbol_status, .. } => symbol_status.clone(),
IndexStatusState::Ready { symbol_status, .. } => symbol_status.clone(),
IndexStatusState::Stale { symbol_status, .. } => symbol_status.clone(),
IndexStatusState::Missing => SymbolIndexingState::NotStarted,
};
self.index_status = IndexStatusState::Ready {
file_count: stats.total_files,
last_updated: "just now".to_string(),
symbol_status,
};
}
Err(e) => {
self.error_message = Some(format!("Index error: {}", e));
}
}
self.indexing = false;
self.indexing_start_time = None;
self.index_rx = None;
self.index_progress_rx = None;
}
}
if self.effects.frame() % 30 == 0 { log::trace!("Polling background symbol indexer status (frame {})", self.effects.frame());
match crate::background_indexer::BackgroundIndexer::get_status(self.cache.path()) {
Ok(Some(bg_status)) => {
log::debug!("Background symbol indexer status: {:?} - {}/{} files",
bg_status.state, bg_status.processed_files, bg_status.total_files);
let new_symbol_status = match bg_status.state {
crate::background_indexer::IndexerState::Running => {
log::debug!("Symbol indexing is RUNNING: {}/{} ({}%)",
bg_status.processed_files, bg_status.total_files,
if bg_status.total_files > 0 {
(bg_status.processed_files as f64 / bg_status.total_files as f64 * 100.0) as u32
} else { 0 });
SymbolIndexingState::Running {
processed: bg_status.processed_files,
total: bg_status.total_files,
}
}
crate::background_indexer::IndexerState::Completed => {
log::debug!("Symbol indexing COMPLETED");
SymbolIndexingState::Completed
}
crate::background_indexer::IndexerState::Failed => {
log::warn!("Symbol indexing FAILED: {:?}", bg_status.error);
SymbolIndexingState::Failed
}
};
self.index_status = match &self.index_status {
IndexStatusState::Ready { file_count, last_updated, .. } => {
IndexStatusState::Ready {
file_count: *file_count,
last_updated: last_updated.clone(),
symbol_status: new_symbol_status,
}
}
IndexStatusState::Stale { files_changed, .. } => {
IndexStatusState::Stale {
files_changed: *files_changed,
symbol_status: new_symbol_status,
}
}
IndexStatusState::Indexing { current, total, status, .. } => {
IndexStatusState::Indexing {
current: *current,
total: *total,
status: status.clone(),
symbol_status: new_symbol_status,
}
}
IndexStatusState::Missing => IndexStatusState::Missing,
};
}
Ok(None) => {
log::trace!("No background symbol indexer status file found");
}
Err(e) => {
log::warn!("Failed to read background symbol indexer status: {}", e);
}
}
}
let elapsed = last_frame.elapsed();
self.effects.update(elapsed);
last_frame = Instant::now();
std::thread::sleep(frame_duration.saturating_sub(elapsed));
}
Ok(())
}
fn handle_key_event_with_editor(&mut self, key: KeyEvent) -> Result<Option<SearchResult>> {
if self.mode == AppMode::FilterSelector {
if let Some(ref mut selector) = self.filter_selector {
if key.code == crossterm::event::KeyCode::Esc {
self.mode = AppMode::Normal;
self.filter_selector = None;
return Ok(None);
}
if let Some(selection) = selector.handle_key(key.code) {
let selection_lower = selection.to_lowercase();
let is_language = matches!(selection_lower.as_str(),
"rust" | "python" | "javascript" | "typescript" | "vue" | "svelte" |
"go" | "java" | "php" | "c" | "cpp" | "csharp" | "ruby" | "kotlin" | "zig"
);
if is_language {
self.filters.language = Some(selection);
} else {
self.filters.kind = Some(selection);
}
self.mode = AppMode::Normal;
self.filter_selector = None;
self.cancel_ongoing_search(); self.filter_change_time = Some(Instant::now());
self.info_message = None;
}
return Ok(None);
}
}
if key.code == crossterm::event::KeyCode::Tab {
if key.modifiers.contains(crossterm::event::KeyModifiers::SHIFT) {
self.focus_prev();
} else {
self.focus_next();
}
return Ok(None);
}
if key.code == crossterm::event::KeyCode::Esc {
if self.mode == AppMode::FilePreview {
self.mode = AppMode::Normal;
self.preview_content = None;
return Ok(None);
}
self.focus_state = FocusState::Results;
return Ok(None);
}
if key.code == crossterm::event::KeyCode::Enter {
match self.focus_state {
FocusState::Input => {
if !self.input.value().trim().is_empty() {
self.execute_search()?;
self.focus_state = FocusState::Results;
}
}
FocusState::Results => {
if let Some(result) = self.results.selected().cloned() {
self.show_file_preview(&result)?;
}
}
_ => {}
}
return Ok(None);
}
let command = KeyCommand::from_key(key, self.focus_state == FocusState::Input);
match command {
KeyCommand::Quit => {
self.should_quit = true;
Ok(None)
}
KeyCommand::ShowHelp => {
self.mode = if self.mode == AppMode::Help {
AppMode::Normal
} else {
AppMode::Help
};
Ok(None)
}
KeyCommand::FocusInput => {
self.focus_state = FocusState::Input;
Ok(None)
}
KeyCommand::UnfocusInput => {
self.focus_state = FocusState::Results;
Ok(None)
}
KeyCommand::NextResult => {
if self.mode == AppMode::FilePreview {
self.scroll_preview_down();
} else {
self.results.next();
}
Ok(None)
}
KeyCommand::PrevResult => {
if self.mode == AppMode::FilePreview {
self.scroll_preview_up();
} else {
self.results.prev();
}
Ok(None)
}
KeyCommand::PageDown => {
self.results.jump_down(10);
Ok(None)
}
KeyCommand::PageUp => {
self.results.jump_up(10);
Ok(None)
}
KeyCommand::First => {
self.results.first();
Ok(None)
}
KeyCommand::Last => {
self.results.last();
Ok(None)
}
KeyCommand::ToggleSymbols => {
self.filters.symbols_mode = !self.filters.symbols_mode;
self.cancel_ongoing_search(); self.filter_change_time = Some(Instant::now());
self.info_message = None;
Ok(None)
}
KeyCommand::ToggleRegex => {
self.filters.regex_mode = !self.filters.regex_mode;
self.cancel_ongoing_search(); self.filter_change_time = Some(Instant::now());
self.info_message = None;
self.needs_full_clear = true; Ok(None)
}
KeyCommand::PromptLanguage => {
self.filter_selector = Some(super::filter_selector::FilterSelector::new_language());
self.mode = AppMode::FilterSelector;
Ok(None)
}
KeyCommand::PromptKind => {
self.filter_selector = Some(super::filter_selector::FilterSelector::new_kind());
self.mode = AppMode::FilterSelector;
Ok(None)
}
KeyCommand::PromptGlob => {
self.info_message = Some("Glob patterns: Use CLI for now (--glob flag)".to_string());
self.info_message_time = Some(Instant::now());
Ok(None)
}
KeyCommand::PromptExclude => {
self.info_message = Some("Exclude patterns: Use CLI for now (--exclude flag)".to_string());
self.info_message_time = Some(Instant::now());
Ok(None)
}
KeyCommand::ToggleExpand => {
self.filters.expand = !self.filters.expand;
self.cancel_ongoing_search(); self.filter_change_time = Some(Instant::now());
self.info_message = None;
Ok(None)
}
KeyCommand::ToggleContains => {
self.filters.contains = !self.filters.contains;
self.cancel_ongoing_search(); self.filter_change_time = Some(Instant::now());
self.info_message = None;
self.needs_full_clear = true; Ok(None)
}
KeyCommand::ClearLanguage => {
self.filters.language = None;
self.cancel_ongoing_search(); self.filter_change_time = Some(Instant::now());
self.info_message = None;
Ok(None)
}
KeyCommand::ClearKind => {
self.filters.kind = None;
self.cancel_ongoing_search(); self.filter_change_time = Some(Instant::now());
self.info_message = None;
Ok(None)
}
KeyCommand::OpenInEditor => {
Ok(self.results.selected().cloned())
}
KeyCommand::Reindex => {
self.trigger_index()?;
Ok(None)
}
KeyCommand::ClearAndReindex => {
self.trigger_clear_and_reindex()?;
Ok(None)
}
KeyCommand::HistoryPrev => {
if let Some(query) = self.history.prev() {
self.input.set_value(query.pattern.clone());
self.filters = query.filters.clone();
}
Ok(None)
}
KeyCommand::HistoryNext => {
if let Some(query) = self.history.next() {
self.input.set_value(query.pattern.clone());
self.filters = query.filters.clone();
} else {
self.input.clear();
self.results.clear();
}
Ok(None)
}
KeyCommand::None => {
if self.focus_state == FocusState::Input {
self.input.handle_key(key);
}
Ok(None)
}
_ => Ok(None),
}
}
fn focus_next(&mut self) {
self.focus_state = match self.focus_state {
FocusState::Input => FocusState::Filters,
FocusState::Filters => FocusState::Results,
FocusState::Results => FocusState::Input,
};
}
fn focus_prev(&mut self) {
self.focus_state = match self.focus_state {
FocusState::Input => FocusState::Results,
FocusState::Filters => FocusState::Input,
FocusState::Results => FocusState::Filters,
};
}
fn show_file_preview(&mut self, result: &SearchResult) -> Result<()> {
let content = std::fs::read_to_string(&result.path)?;
let lines: Vec<String> = content.lines().map(|l| l.to_string()).collect();
let language = std::path::Path::new(&result.path)
.extension()
.and_then(|ext| ext.to_str())
.map(|ext| crate::models::Language::from_extension(ext))
.unwrap_or(crate::models::Language::Unknown);
self.preview_content = Some(FilePreview {
path: result.path.clone(),
content: lines,
center_line: result.span.start_line,
scroll_offset: result.span.start_line.saturating_sub(10),
language,
});
self.mode = AppMode::FilePreview;
Ok(())
}
fn scroll_preview_down(&mut self) {
if let Some(ref mut preview) = self.preview_content {
if preview.scroll_offset + 20 < preview.content.len() {
preview.scroll_offset += 1;
}
}
}
fn scroll_preview_up(&mut self) {
if let Some(ref mut preview) = self.preview_content {
preview.scroll_offset = preview.scroll_offset.saturating_sub(1);
}
}
fn handle_mouse_event(&mut self, mouse: MouseEvent, terminal_size: (u16, u16)) {
if self.mode == AppMode::FilterSelector {
if let Some(ref mut selector) = self.filter_selector {
if let Some(selection) = selector.handle_mouse(mouse) {
let selection_lower = selection.to_lowercase();
let is_language = matches!(selection_lower.as_str(),
"rust" | "python" | "javascript" | "typescript" | "vue" | "svelte" |
"go" | "java" | "php" | "c" | "cpp" | "csharp" | "ruby" | "kotlin" | "zig"
);
if is_language {
self.filters.language = Some(selection);
} else {
self.filters.kind = Some(selection);
}
self.mode = AppMode::Normal;
self.filter_selector = None;
self.cancel_ongoing_search(); self.filter_change_time = Some(Instant::now());
self.info_message = None;
}
}
return;
}
if self.mode == AppMode::FilePreview {
match mouse.kind {
crossterm::event::MouseEventKind::ScrollDown => {
for _ in 0..3 {
self.scroll_preview_down();
}
}
crossterm::event::MouseEventKind::ScrollUp => {
for _ in 0..3 {
self.scroll_preview_up();
}
}
crossterm::event::MouseEventKind::Down(_) => {
self.mode = AppMode::Normal;
self.preview_content = None;
}
_ => {}
}
return;
}
let input_area = ratatui::layout::Rect::new(0, 0, terminal_size.0, 3);
let filters_area = ratatui::layout::Rect::new(0, 3, terminal_size.0, 3);
let result_y = 6;
let result_height = terminal_size.1.saturating_sub(7); let result_area = ratatui::layout::Rect::new(0, result_y, terminal_size.0, result_height);
let action = self.mouse.handle_event(mouse, input_area, filters_area, result_area, &self.filter_badge_positions);
match action {
MouseAction::FocusInput(cursor_pos) => {
self.focus_state = FocusState::Input;
let max_pos = self.input.value().len();
let clamped_pos = cursor_pos.min(max_pos);
self.input.set_cursor(clamped_pos);
}
MouseAction::ToggleSymbols => {
self.filters.symbols_mode = !self.filters.symbols_mode;
self.cancel_ongoing_search(); self.filter_change_time = Some(Instant::now());
self.info_message = None;
}
MouseAction::ToggleRegex => {
self.filters.regex_mode = !self.filters.regex_mode;
self.cancel_ongoing_search(); self.filter_change_time = Some(Instant::now());
self.info_message = None;
self.needs_full_clear = true; }
MouseAction::PromptLanguage => {
self.cancel_ongoing_search(); self.filter_selector = Some(super::filter_selector::FilterSelector::new_language());
self.mode = AppMode::FilterSelector;
}
MouseAction::PromptKind => {
self.cancel_ongoing_search(); self.filter_selector = Some(super::filter_selector::FilterSelector::new_kind());
self.mode = AppMode::FilterSelector;
}
MouseAction::ToggleExpand => {
self.filters.expand = !self.filters.expand;
self.cancel_ongoing_search(); self.filter_change_time = Some(Instant::now());
self.info_message = None;
}
MouseAction::ToggleContains => {
self.filters.contains = !self.filters.contains;
self.cancel_ongoing_search(); self.filter_change_time = Some(Instant::now());
self.info_message = None;
self.needs_full_clear = true; }
MouseAction::SelectResult(line_index) => {
let result_index = self.line_index_to_result_index(line_index);
self.results.select(result_index);
}
MouseAction::DoubleClick(line_index) => {
let result_index = self.line_index_to_result_index(line_index);
self.results.select(result_index);
if let Some(result) = self.results.selected().cloned() {
let _ = self.show_file_preview(&result);
}
}
MouseAction::ScrollDown => {
self.results.next();
}
MouseAction::ScrollUp => {
self.results.prev();
}
MouseAction::TriggerIndex => {
let _ = self.trigger_index();
}
MouseAction::ClearAndReindex => {
let _ = self.trigger_clear_and_reindex();
}
_ => {}
}
}
fn line_index_to_result_index(&self, line_index: usize) -> usize {
let mut current_line = 0;
let scroll_offset = self.results.scroll_offset();
for (idx, result) in self.results.results().iter().enumerate().skip(scroll_offset) {
const MAX_PREVIEW_LINES: usize = 20;
let has_symbol = !matches!(result.kind, crate::models::SymbolKind::Unknown(_))
&& result.symbol.is_some();
let symbol_lines = if has_symbol { 1 } else { 0 };
let path_lines = 1;
let preview_lines = result.preview.lines().count().min(MAX_PREVIEW_LINES);
let total_lines = symbol_lines + path_lines + preview_lines;
if line_index < current_line + total_lines {
return idx;
}
current_line += total_lines;
}
self.results.len().saturating_sub(1)
}
fn cancel_ongoing_search(&mut self) {
self.searching = false;
self.search_rx = None;
}
fn execute_search(&mut self) -> Result<()> {
self.history.reset_cursor();
let pattern = self.input.value();
if pattern.trim().is_empty() {
self.results.clear();
self.searching = false;
return Ok(());
}
let language = self.filters.language.as_ref().and_then(|lang_str| {
match lang_str.to_lowercase().as_str() {
"rust" | "rs" => Some(crate::models::Language::Rust),
"python" | "py" => Some(crate::models::Language::Python),
"javascript" | "js" => Some(crate::models::Language::JavaScript),
"typescript" | "ts" => Some(crate::models::Language::TypeScript),
"vue" => Some(crate::models::Language::Vue),
"svelte" => Some(crate::models::Language::Svelte),
"go" => Some(crate::models::Language::Go),
"java" => Some(crate::models::Language::Java),
"php" => Some(crate::models::Language::PHP),
"c" => Some(crate::models::Language::C),
"cpp" | "c++" => Some(crate::models::Language::Cpp),
"csharp" | "cs" | "c#" => Some(crate::models::Language::CSharp),
"ruby" | "rb" => Some(crate::models::Language::Ruby),
"kotlin" | "kt" => Some(crate::models::Language::Kotlin),
"zig" => Some(crate::models::Language::Zig),
_ => None,
}
});
let kind = self.filters.kind.as_ref().and_then(|kind_str| {
kind_str.parse::<crate::models::SymbolKind>().ok()
});
let filter = QueryFilter {
language,
kind,
use_ast: false,
use_regex: self.filters.regex_mode,
limit: Some(500),
symbols_mode: self.filters.symbols_mode,
expand: self.filters.expand,
file_pattern: None,
exact: false, use_contains: self.filters.contains,
timeout_secs: 10,
glob_patterns: self.filters.glob_patterns.clone(),
exclude_patterns: self.filters.exclude_patterns.clone(),
paths_only: false,
offset: None,
force: false,
suppress_output: false,
include_dependencies: false, ..Default::default()
};
let (tx, rx) = mpsc::channel();
let pattern_owned = pattern.to_string();
let cache = CacheManager::new(&self.cwd);
let engine = QueryEngine::new(cache);
std::thread::spawn(move || {
let result = engine.search_with_metadata(&pattern_owned, filter);
tx.send(result).ok();
});
self.searching = true;
self.search_rx = Some(rx);
Ok(())
}
fn trigger_index(&mut self) -> Result<()> {
let symbol_status = match &self.index_status {
IndexStatusState::Indexing { symbol_status, .. } => symbol_status.clone(),
IndexStatusState::Ready { symbol_status, .. } => symbol_status.clone(),
IndexStatusState::Stale { symbol_status, .. } => symbol_status.clone(),
IndexStatusState::Missing => SymbolIndexingState::NotStarted,
};
self.index_status = IndexStatusState::Indexing {
current: 0,
total: 0,
status: "Starting...".to_string(),
symbol_status,
};
let (result_tx, result_rx) = mpsc::channel();
let (progress_tx, progress_rx) = mpsc::channel();
let cwd = self.cwd.clone();
let _cache_path = self.cache.path().to_path_buf();
std::thread::spawn(move || {
let config = IndexConfig::default();
let cache = CacheManager::new(&cwd);
let indexer = Indexer::new(cache, config);
let callback = Arc::new(move |current: usize, total: usize, status: String| {
let _ = progress_tx.send((current, total, status));
});
let result = indexer.index_with_callback(&cwd, false, Some(callback));
if result.is_ok() {
log::debug!("Main indexing completed, spawning background symbol indexer");
let current_exe = std::env::current_exe();
if let Ok(exe_path) = current_exe {
#[cfg(unix)]
{
let _ = std::process::Command::new(&exe_path)
.arg("index-symbols-internal")
.arg(&cwd)
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.spawn();
}
#[cfg(windows)]
{
use std::os::windows::process::CommandExt;
const CREATE_NO_WINDOW: u32 = 0x08000000;
let _ = std::process::Command::new(&exe_path)
.arg("index-symbols-internal")
.arg(&cwd)
.creation_flags(CREATE_NO_WINDOW)
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.spawn();
}
log::debug!("Background symbol indexing process spawned");
}
}
result_tx.send(result).ok();
});
self.indexing = true;
self.indexing_start_time = Some(Instant::now());
self.index_rx = Some(result_rx);
self.index_progress_rx = Some(progress_rx);
Ok(())
}
fn trigger_clear_and_reindex(&mut self) -> Result<()> {
let cache_dir = self.cache.path();
let files_to_remove = [
"meta.db",
"trigrams.bin",
"content.bin",
"symbols.db",
"indexing.lock",
"indexing.status",
];
for file_name in &files_to_remove {
let file_path = cache_dir.join(file_name);
if file_path.exists() {
if let Err(e) = std::fs::remove_file(&file_path) {
log::warn!("Failed to remove {}: {}", file_name, e);
}
}
}
self.index_status = IndexStatusState::Missing;
self.trigger_index()
}
fn open_in_editor_suspended(
&mut self,
terminal: &mut Terminal<CrosstermBackend<io::Stdout>>,
result: &SearchResult,
) -> Result<()> {
let editor = std::env::var("EDITOR").unwrap_or_else(|_| "vim".to_string());
let line = result.span.start_line;
let args = match editor.as_str() {
"vim" | "nvim" => vec![format!("+{}", line), result.path.clone()],
"emacs" => vec![format!("+{}:0", line), result.path.clone()],
"code" | "vscode" => vec!["-g".to_string(), format!("{}:{}", result.path, line)],
_ => vec![result.path.clone()],
};
crossterm::terminal::disable_raw_mode()?;
crossterm::execute!(
terminal.backend_mut(),
crossterm::terminal::LeaveAlternateScreen,
crossterm::event::DisableMouseCapture,
crossterm::cursor::Show
)?;
terminal.show_cursor()?;
let status = std::process::Command::new(&editor)
.args(&args)
.status()?;
crossterm::terminal::enable_raw_mode()?;
crossterm::execute!(
terminal.backend_mut(),
crossterm::terminal::EnterAlternateScreen,
crossterm::event::EnableMouseCapture
)?;
terminal.clear()?;
if !status.success() {
self.error_message = Some(format!("Editor exited with error code: {:?}", status.code()));
}
Ok(())
}
fn setup_terminal() -> Result<Terminal<CrosstermBackend<io::Stdout>>> {
crossterm::terminal::enable_raw_mode()?;
let mut stdout = io::stdout();
crossterm::execute!(
stdout,
crossterm::terminal::EnterAlternateScreen,
crossterm::event::EnableMouseCapture
)?;
let backend = CrosstermBackend::new(stdout);
let terminal = Terminal::new(backend)?;
Ok(terminal)
}
fn restore_terminal(mut terminal: Terminal<CrosstermBackend<io::Stdout>>) -> Result<()> {
crossterm::terminal::disable_raw_mode()?;
crossterm::execute!(
terminal.backend_mut(),
crossterm::terminal::LeaveAlternateScreen,
crossterm::event::DisableMouseCapture
)?;
terminal.show_cursor()?;
Ok(())
}
pub fn input(&self) -> &InputField {
&self.input
}
pub fn results(&self) -> &ResultList {
&self.results
}
pub fn results_mut(&mut self) -> &mut ResultList {
&mut self.results
}
pub fn mode(&self) -> &AppMode {
&self.mode
}
pub fn filters(&self) -> &QueryFilters {
&self.filters
}
pub fn capabilities(&self) -> &TerminalCapabilities {
&self.capabilities
}
pub fn theme(&self) -> &ThemeManager {
&self.theme
}
pub fn effects(&self) -> &EffectManager {
&self.effects
}
pub fn index_status(&self) -> &IndexStatusState {
&self.index_status
}
pub fn focus_state(&self) -> &FocusState {
&self.focus_state
}
pub fn error_message(&self) -> Option<&str> {
self.error_message.as_deref()
}
pub fn info_message(&self) -> Option<&str> {
self.info_message.as_deref()
}
pub fn preview_content(&self) -> Option<&FilePreview> {
self.preview_content.as_ref()
}
pub fn searching(&self) -> bool {
self.searching
}
pub fn indexing(&self) -> bool {
self.indexing
}
pub fn indexing_elapsed_secs(&self) -> Option<u64> {
self.indexing_start_time.map(|start| start.elapsed().as_secs())
}
pub fn cwd(&self) -> &PathBuf {
&self.cwd
}
pub fn filter_selector(&self) -> Option<&super::filter_selector::FilterSelector> {
self.filter_selector.as_ref()
}
pub fn filter_selector_mut(&mut self) -> Option<&mut super::filter_selector::FilterSelector> {
self.filter_selector.as_mut()
}
pub fn filter_badge_positions(&self) -> &super::mouse::FilterBadgePositions {
&self.filter_badge_positions
}
}
impl FilePreview {
pub fn path(&self) -> &str {
&self.path
}
pub fn content(&self) -> &[String] {
&self.content
}
pub fn visible_lines(&self, height: usize) -> &[String] {
let start = self.scroll_offset;
let end = (start + height).min(self.content.len());
&self.content[start..end]
}
pub fn center_line(&self) -> usize {
self.center_line
}
pub fn scroll_offset(&self) -> usize {
self.scroll_offset
}
pub fn language(&self) -> crate::models::Language {
self.language
}
}