1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
use agcodex_file_search::FileMatch;
use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
use ratatui::widgets::WidgetRef;
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;
/// Visual state for the file-search popup.
pub(crate) struct FileSearchPopup {
/// Query corresponding to the `matches` currently shown.
display_query: String,
/// Latest query typed by the user. May differ from `display_query` when
/// a search is still in-flight.
pending_query: String,
/// When `true` we are still waiting for results for `pending_query`.
waiting: bool,
/// Cached matches; paths relative to the search dir.
matches: Vec<FileMatch>,
/// Shared selection/scroll state.
state: ScrollState,
}
impl FileSearchPopup {
pub(crate) const fn new() -> Self {
Self {
display_query: String::new(),
pending_query: String::new(),
waiting: true,
matches: Vec::new(),
state: ScrollState::new(),
}
}
/// Update the query and reset state to *waiting*.
pub(crate) fn set_query(&mut self, query: &str) {
if query == self.pending_query {
return;
}
// Determine if current matches are still relevant.
let keep_existing = query.starts_with(&self.display_query);
self.pending_query.clear();
self.pending_query.push_str(query);
self.waiting = true; // waiting for new results
if !keep_existing {
self.matches.clear();
self.state.reset();
}
}
/// Put the popup into an "idle" state used for an empty query (just "@").
/// Shows a hint instead of matches until the user types more characters.
pub(crate) fn set_empty_prompt(&mut self) {
self.display_query.clear();
self.pending_query.clear();
self.waiting = false;
self.matches.clear();
// Reset selection/scroll state when showing the empty prompt.
self.state.reset();
}
/// Replace matches when a `FileSearchResult` arrives.
/// Replace matches. Only applied when `query` matches `pending_query`.
pub(crate) fn set_matches(&mut self, query: &str, matches: Vec<FileMatch>) {
if query != self.pending_query {
return; // stale
}
self.display_query = query.to_string();
self.matches = matches;
self.waiting = false;
let len = self.matches.len();
self.state.clamp_selection(len);
self.state.ensure_visible(len, len.min(MAX_POPUP_ROWS));
}
/// Move selection cursor up.
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));
}
/// Move selection cursor down.
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<&str> {
self.state
.selected_idx
.and_then(|idx| self.matches.get(idx))
.map(|file_match| file_match.path.as_str())
}
pub(crate) fn calculate_required_height(&self) -> u16 {
// Row count depends on whether we already have matches. If no matches
// yet (e.g. initial search or query with no results) reserve a single
// row so the popup is still visible. When matches are present we show
// up to MAX_RESULTS regardless of the waiting flag so the list
// remains stable while a newer search is in-flight.
self.matches.len().clamp(1, MAX_POPUP_ROWS) as u16
}
}
impl WidgetRef for &FileSearchPopup {
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
// Convert matches to GenericDisplayRow, translating indices to usize at the UI boundary.
let rows_all: Vec<GenericDisplayRow> = if self.matches.is_empty() {
Vec::new()
} else {
self.matches
.iter()
.map(|m| GenericDisplayRow {
name: m.path.clone(),
match_indices: m
.indices
.as_ref()
.map(|v| v.iter().map(|&i| i as usize).collect()),
is_current: false,
description: None,
})
.collect()
};
if self.waiting && rows_all.is_empty() {
// Render a minimal waiting stub using the shared renderer (no rows -> "no matches").
render_rows(area, buf, &[], &self.state, MAX_POPUP_ROWS, false);
} else {
render_rows(area, buf, &rows_all, &self.state, MAX_POPUP_ROWS, false);
}
}
}