use std::path::PathBuf;
use lemurclaw_core::file_search::FileMatch;
use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
use ratatui::widgets::WidgetRef;
use crate::tui_internal::render::Insets;
use crate::tui_internal::render::RectExt;
use super::popup_consts::MAX_POPUP_ROWS;
use super::scroll_state::ScrollState;
use super::selection_popup_common::GenericDisplayRow;
use super::selection_popup_common::render_rows;
pub(crate) struct FileSearchPopup {
display_query: String,
pending_query: String,
waiting: bool,
matches: Vec<FileMatch>,
state: ScrollState,
}
impl FileSearchPopup {
pub(crate) fn new() -> Self {
Self {
display_query: String::new(),
pending_query: String::new(),
waiting: true,
matches: Vec::new(),
state: ScrollState::new(),
}
}
pub(crate) fn set_query(&mut self, query: &str) {
if query == self.pending_query {
return;
}
self.pending_query.clear();
self.pending_query.push_str(query);
self.waiting = true; }
pub(crate) fn set_empty_prompt(&mut self) {
self.display_query.clear();
self.pending_query.clear();
self.waiting = false;
self.matches.clear();
self.state.reset();
}
pub(crate) 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;
let len = self.matches.len();
self.state.clamp_selection(len);
self.state.ensure_visible(len, len.min(MAX_POPUP_ROWS));
}
pub(crate) fn move_up(&mut self) {
let len = self.matches.len();
self.state.move_up_wrap(len);
self.state.ensure_visible(len, len.min(MAX_POPUP_ROWS));
}
pub(crate) fn move_down(&mut self) {
let len = self.matches.len();
self.state.move_down_wrap(len);
self.state.ensure_visible(len, len.min(MAX_POPUP_ROWS));
}
pub(crate) fn selected_match(&self) -> Option<&PathBuf> {
self.state
.selected_idx
.and_then(|idx| self.matches.get(idx))
.map(|file_match| &file_match.path)
}
pub(crate) fn calculate_required_height(&self) -> u16 {
self.matches.len().clamp(1, MAX_POPUP_ROWS) as u16
}
}
impl WidgetRef for &FileSearchPopup {
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
let rows_all: Vec<GenericDisplayRow> = if self.matches.is_empty() {
Vec::new()
} else {
self.matches
.iter()
.map(|m| GenericDisplayRow {
name: m.path.to_string_lossy().to_string(),
name_prefix_spans: Vec::new(),
match_indices: m
.indices
.as_ref()
.map(|v| v.iter().map(|&i| i as usize).collect()),
display_shortcut: None,
description: None,
category_tag: None,
wrap_indent: None,
is_disabled: false,
disabled_reason: None,
})
.collect()
};
let empty_message = if self.waiting {
"loading..."
} else {
"no matches"
};
render_rows(
area.inset(Insets::tlbr(
0, 2, 0, 0,
)),
buf,
&rows_all,
&self.state,
MAX_POPUP_ROWS,
empty_message,
);
}
}
#[cfg(test)]
mod tests {
use super::*;
use lemurclaw_core::file_search::MatchType;
use pretty_assertions::assert_eq;
fn file_match(index: usize) -> FileMatch {
FileMatch {
score: index as u32,
path: PathBuf::from(format!("src/file_{index:02}.rs")),
match_type: MatchType::File,
root: PathBuf::from("/tmp/repo"),
indices: None,
}
}
#[test]
fn set_matches_keeps_only_the_first_page_of_results() {
let mut popup = FileSearchPopup::new();
popup.set_query("file");
popup.set_matches("file", (0..(MAX_POPUP_ROWS + 2)).map(file_match).collect());
assert_eq!(
popup.matches,
(0..MAX_POPUP_ROWS).map(file_match).collect::<Vec<_>>()
);
assert_eq!(popup.calculate_required_height(), MAX_POPUP_ROWS as u16);
}
}