tui/src/bottom_pane/mentions_v2/popup.rs
154 lines
use codex_file_search::FileMatch;use ratatui::buffer::Buffer;use ratatui::layout::Rect;use ratatui::widgets::WidgetRef;use super::candidate::Candidate;use super::candidate::SearchResult;use super::candidate::Selection;use super::filter::filtered_candidates;use super::render::render_popup;use super::search_mode::SearchMode;use crate::bottom_pane::popup_consts::MAX_POPUP_ROWS;use crate::bottom_pane::scroll_state::ScrollState;pub(crate) struct Popup { query: String, file_search: FileSearch, candidates: Vec<Candidate>, search_mode: SearchMode, state: ScrollState,}impl Popup { pub(crate) fn new(candidates: Vec<Candidate>) -> Self { Self { query: String::new(), file_search: FileSearch::default(), candidates, search_mode: SearchMode::Results, state: ScrollState::new(), } } pub(crate) fn set_candidates(&mut self, candidates: Vec<Candidate>) { self.candidates = candidates; self.clamp_selection(); } pub(crate) fn set_query(&mut self, query: &str) { self.query = query.to_string(); self.file_search.set_query(query); self.clamp_selection(); } pub(crate) fn set_file_matches(&mut self, query: &str, matches: Vec<FileMatch>) { self.file_search.set_matches(query, matches); self.clamp_selection(); } pub(crate) fn selected(&self) -> Option<Selection> { let rows = self.rows(); let idx = self.state.selected_idx?; rows.get(idx).map(|row| row.selection.clone()) } pub(crate) fn move_up(&mut self) { let len = self.rows().len(); self.state.move_up_wrap(len); self.state.ensure_visible(len, MAX_POPUP_ROWS.min(len)); } pub(crate) fn move_down(&mut self) { let len = self.rows().len(); self.state.move_down_wrap(len); self.state.ensure_visible(len, MAX_POPUP_ROWS.min(len)); } pub(crate) fn previous_search_mode(&mut self) { self.search_mode = self.search_mode.previous(); self.clamp_selection(); } pub(crate) fn next_search_mode(&mut self) { self.search_mode = self.search_mode.next(); self.clamp_selection(); } pub(crate) fn calculate_required_height(&self, _width: u16) -> u16 { (MAX_POPUP_ROWS as u16).saturating_add(2) } fn clamp_selection(&mut self) { let len = self.rows().len(); self.state.clamp_selection(len); self.state.ensure_visible(len, MAX_POPUP_ROWS.min(len)); } fn rows(&self) -> Vec<SearchResult> { filtered_candidates( &self.candidates, &self.file_search.matches, &self.query, self.search_mode, self.file_search.should_show_matches(), ) }}impl WidgetRef for Popup { fn render_ref(&self, area: Rect, buf: &mut Buffer) { render_popup( area, buf, &self.rows(), &self.state, self.file_search.empty_message(), self.search_mode, ); }}#[derive(Default)]struct FileSearch { pending_query: String, display_query: String, waiting: bool, matches: Vec<FileMatch>,}impl FileSearch { fn set_query(&mut self, query: &str) { if query.is_empty() { self.pending_query.clear(); self.display_query.clear(); self.waiting = false; self.matches.clear(); } else if query != self.pending_query { self.pending_query = query.to_string(); self.waiting = true; } } fn set_matches(&mut self, query: &str, matches: Vec<FileMatch>) { if query != self.pending_query { return; } self.display_query = query.to_string(); self.matches = matches.into_iter().take(MAX_POPUP_ROWS).collect(); self.waiting = false; } fn should_show_matches(&self) -> bool { !self.matches.is_empty() } fn empty_message(&self) -> &'static str { if self.waiting { "loading..." } else { "no matches" } }}