use std::cmp::Reverse;
use crossterm::event::KeyCode;
use nucleo_matcher::Matcher;
use nucleo_matcher::Utf32Str;
use nucleo_matcher::pattern::Atom;
use nucleo_matcher::pattern::AtomKind;
use nucleo_matcher::pattern::CaseMatching;
use nucleo_matcher::pattern::Normalization;
use ratatui::Frame;
use ratatui::layout::Constraint;
use ratatui::layout::Rect;
use ratatui::style::Color;
use ratatui::style::Modifier;
use ratatui::style::Style;
use ratatui::text::Line;
use ratatui::text::Span;
use ratatui::widgets::Cell;
use ratatui::widgets::Row;
use ratatui::widgets::Table;
use ratatui::widgets::TableState;
use tui_pane::AppContext;
use tui_pane::FocusedPane;
use tui_pane::accent_color;
use tui_pane::active_border_color;
use tui_pane::finder_match_bg;
use tui_pane::label_color;
use tui_pane::render_overflow_affordance;
use tui_pane::text_default;
use tui_pane::title_color;
use super::constants::MIN_POPUP_WIDTH;
use super::index::FINDER_COLUMN_COUNT;
use super::index::FINDER_HEADERS;
use super::index::FinderItem;
use super::index::FinderKind;
use crate::ci;
use crate::tui::app::App;
use crate::tui::constants::FINDER_POPUP_HEIGHT;
use crate::tui::constants::MAX_FINDER_RESULTS;
use crate::tui::integration::AppPaneId;
use crate::tui::keymap::FinderAction;
use crate::tui::overlays::FinderPane;
use crate::tui::overlays::PopupFrame;
use crate::tui::panes;
use crate::tui::panes::GitRow;
use crate::tui::panes::RunTargetKind;
use crate::tui::render_context::PaneRenderCtx;
pub fn search_finder(index: &[FinderItem], query: &str, max_results: usize) -> (Vec<usize>, usize) {
if query.is_empty() {
return (Vec::new(), 0);
}
let words: Vec<&str> = query
.split(|ch: char| ch.is_whitespace() || matches!(ch, '/' | '\\'))
.filter(|word| !word.is_empty())
.collect();
if words.is_empty() {
return (Vec::new(), 0);
}
let atoms: Vec<Atom> = words
.iter()
.map(|word| {
Atom::new(
word,
CaseMatching::Smart,
Normalization::Smart,
AtomKind::Fuzzy,
false,
)
})
.collect();
let mut matcher = Matcher::default();
let mut scored: Vec<(usize, u16)> = index
.iter()
.enumerate()
.filter_map(|(i, item)| {
let mut total_score: u16 = 0;
for atom in &atoms {
let score = item
.search_tokens
.iter()
.filter_map(|token| {
let mut buf = Vec::new();
let haystack = Utf32Str::new(token, &mut buf);
atom.score(haystack, &mut matcher)
})
.max()?;
total_score = total_score.saturating_add(score);
}
Some((i, total_score))
})
.collect();
let total = scored.len();
scored.sort_by_key(|entry| Reverse(entry.1));
let indices = scored
.into_iter()
.take(max_results)
.map(|(i, _)| i)
.collect();
(indices, total)
}
pub fn dispatch_finder_action(action: FinderAction, app: &mut App) {
match action {
FinderAction::Activate => confirm_finder(app),
FinderAction::Cancel => close_finder(app),
}
}
pub fn handle_finder_text_key(app: &mut App, key: KeyCode) {
match key {
KeyCode::Up => {
app.overlays.finder_pane.viewport.up();
},
KeyCode::Down => {
app.overlays.finder_pane.viewport.down();
},
KeyCode::Home => {
app.overlays.finder_pane.viewport.home();
},
KeyCode::End => {
app.overlays.finder_pane.viewport.end();
},
KeyCode::PageUp => {
app.overlays.finder_pane.viewport.page_up();
},
KeyCode::PageDown => {
app.overlays.finder_pane.viewport.page_down();
},
KeyCode::Backspace => {
if app.project_list.finder.query.is_empty() {
close_finder(app);
} else {
app.project_list.finder.query.pop();
refresh_finder_results(app);
}
},
KeyCode::Char(c) => {
app.project_list.finder.query.push(c);
refresh_finder_results(app);
},
_ => {},
}
}
fn close_finder(app: &mut App) {
let return_target = app
.overlays
.take_finder_return()
.unwrap_or(FocusedPane::App(AppPaneId::ProjectList));
app.overlays.close_finder();
app.project_list.finder.query.clear();
app.project_list.finder.results.clear();
app.overlays.finder_pane.viewport.home();
app.set_focus(return_target);
}
fn refresh_finder_results(app: &mut App) {
let (results, total) = {
let finder = &app.project_list.finder;
search_finder(&finder.index, &finder.query, MAX_FINDER_RESULTS)
};
let finder = &mut app.project_list.finder;
finder.results = results;
finder.total = total;
app.overlays.finder_pane.viewport.home();
}
fn confirm_finder(app: &mut App) {
let Some(&idx) = app
.project_list
.finder
.results
.get(app.overlays.finder_pane.viewport.pos())
else {
return;
};
let item = app.project_list.finder.index[idx].clone();
let return_target = app
.overlays
.take_finder_return()
.unwrap_or(FocusedPane::App(AppPaneId::ProjectList));
app.overlays.close_finder();
app.project_list.finder.query.clear();
app.project_list.finder.results.clear();
app.overlays.finder_pane.viewport.home();
app.set_focus(return_target);
let include_non_rust = app.config.include_non_rust().includes_non_rust();
app.project_list
.select_project_in_tree(item.project_path.as_path(), include_non_rust);
match item.kind {
FinderKind::Project => {
},
FinderKind::Binary | FinderKind::Example | FinderKind::Bench => {
navigate_to_target(app, &item);
},
FinderKind::PullRequest => {
navigate_to_pull_request(app, &item);
},
}
}
fn repopulate_selected_detail_panes(app: &mut App) {
app.sync_selected_project();
if let Some(data) = app
.project_list
.selected_project_path()
.and_then(|path| app.project_list.entry_containing(path))
.map(|entry| panes::build_pane_data(app, &entry.root_item))
{
app.panes.package.set_content(data.package);
app.panes.git.set_content(data.git);
app.panes.targets.set_content(data.targets);
}
}
fn navigate_to_target(app: &mut App, item: &FinderItem) {
repopulate_selected_detail_panes(app);
let Some(targets_data) = app.panes.targets.content().cloned() else {
return;
};
if !targets_data.has_targets() {
return;
}
app.set_focus(FocusedPane::App(AppPaneId::Targets));
let entries = panes::build_target_list_from_data(&targets_data);
let target_kind = match item.kind {
FinderKind::Binary => RunTargetKind::Binary,
FinderKind::Example => RunTargetKind::Example,
FinderKind::Bench => RunTargetKind::Bench,
FinderKind::Project | FinderKind::PullRequest => return,
};
let target_name = item.target_name.as_deref().unwrap_or("");
for (i, entry) in entries.iter().enumerate() {
if entry.name == target_name
&& std::mem::discriminant(&entry.run_target_kind)
== std::mem::discriminant(&target_kind)
{
app.panes.targets.viewport.set_pos(i);
return;
}
}
}
fn navigate_to_pull_request(app: &mut App, item: &FinderItem) {
let Some(target) = item.pr_target.as_ref() else {
return;
};
repopulate_selected_detail_panes(app);
let selected_owner_repo = app
.project_list
.selected_project_path()
.and_then(|path| app.project_list.fetch_url_for(path))
.and_then(|url| ci::parse_owner_repo(&url));
if selected_owner_repo.as_ref() != Some(&target.owner_repo) {
return;
}
app.set_focus(FocusedPane::App(AppPaneId::Git));
let Some(git) = app.panes.git.content() else {
return;
};
let mut index = 0;
while let Some(row) = panes::git_row_at(git, index) {
if let GitRow::PullRequest(pull_request) = row
&& pull_request.number == target.number
{
app.panes.git.viewport.set_pos(index);
return;
}
index += 1;
}
}
pub fn render_finder_pane_body(
frame: &mut Frame,
_area: Rect,
pane: &mut FinderPane,
ctx: &PaneRenderCtx<'_>,
) {
let col_widths = ctx.project_list.finder.col_widths;
let natural_width: usize = col_widths.iter().sum::<usize>() + 4 + 2;
let max_popup_width = frame.area().width;
let popup_width = u16::try_from(natural_width)
.unwrap_or(u16::MAX)
.clamp(MIN_POPUP_WIDTH, max_popup_width);
let title = if ctx.project_list.finder.query.is_empty() {
" Find Anything ".to_string()
} else if ctx.project_list.finder.total <= ctx.project_list.finder.results.len() {
format!(" Find Anything ({}) ", ctx.project_list.finder.total)
} else {
format!(
" Find Anything ({} of {}) ",
ctx.project_list.finder.results.len(),
ctx.project_list.finder.total
)
};
let popup = PopupFrame {
title: Some(title),
border_color: active_border_color(),
width: popup_width,
height: FINDER_POPUP_HEIGHT,
}
.render_with_areas(frame);
let inner = popup.inner;
if inner.height < 3 {
return;
}
let input_area = Rect {
x: inner.x,
y: inner.y,
width: inner.width,
height: 1,
};
let prompt_style = Style::default()
.fg(accent_color())
.add_modifier(Modifier::BOLD);
let input_line = Line::from(vec![
Span::styled(" / ", prompt_style),
Span::styled(
format!("{}_", ctx.project_list.finder.query),
Style::default().fg(title_color()),
),
]);
frame.render_widget(ratatui::widgets::Paragraph::new(input_line), input_area);
if inner.height < 4 {
return;
}
let sep_area = Rect {
x: inner.x,
y: inner.y + 1,
width: inner.width,
height: 1,
};
let sep = Line::from(Span::styled(
"─".repeat(inner.width as usize),
Style::default().fg(label_color()),
));
frame.render_widget(ratatui::widgets::Paragraph::new(sep), sep_area);
let results_area = Rect {
x: inner.x,
y: inner.y + 2,
width: inner.width,
height: inner.height.saturating_sub(2),
};
let result_count = ctx.project_list.finder.results.len();
pane.viewport.set_len(result_count);
pane.viewport.set_content_area(results_area);
pane.viewport
.set_viewport_rows(usize::from(results_area.height.saturating_sub(1)));
render_finder_results(frame, pane, ctx, col_widths, results_area, popup.outer);
}
fn highlighted_spans(text: &str, query: &str, fg: Color) -> Line<'static> {
let base = Style::default().fg(fg);
let highlight = base.bg(finder_match_bg());
if text.is_empty() || query.is_empty() {
return Line::from(Span::styled(text.to_owned(), base));
}
let words: Vec<&str> = query.split_whitespace().collect();
if words.is_empty() {
return Line::from(Span::styled(text.to_owned(), base));
}
let atoms: Vec<Atom> = words
.iter()
.map(|word| {
Atom::new(
word,
CaseMatching::Smart,
Normalization::Smart,
AtomKind::Fuzzy,
false,
)
})
.collect();
let mut matcher = Matcher::default();
let mut buf = Vec::new();
let haystack = Utf32Str::new(text, &mut buf);
let char_byte_ranges: Vec<(usize, usize)> = text
.char_indices()
.map(|(pos, ch)| (pos, pos + ch.len_utf8()))
.collect();
let mut highlight_mask: Vec<bool> = vec![false; text.len()];
let mut indices = Vec::new();
for atom in &atoms {
indices.clear();
if atom.indices(haystack, &mut matcher, &mut indices).is_some() {
for &char_idx in &indices {
if let Some(&(start, end)) = char_byte_ranges.get(char_idx as usize) {
for flag in &mut highlight_mask[start..end] {
*flag = true;
}
}
}
}
}
let mut spans = Vec::new();
let mut chars = text.char_indices().peekable();
while let Some(&(start, _)) = chars.peek() {
let is_match = highlight_mask[start];
let mut end = start;
while let Some(&(pos, ch)) = chars.peek() {
if highlight_mask[pos] != is_match {
break;
}
end = pos + ch.len_utf8();
chars.next();
}
let style = if is_match { highlight } else { base };
spans.push(Span::styled(text[start..end].to_owned(), style));
}
Line::from(spans)
}
fn render_finder_results(
frame: &mut Frame,
pane: &mut FinderPane,
ctx: &PaneRenderCtx<'_>,
col_widths: [usize; FINDER_COLUMN_COUNT],
area: Rect,
popup_area: Rect,
) {
if ctx.project_list.finder.results.is_empty() {
let msg = if ctx.project_list.finder.query.is_empty() {
"Type to search projects, PRs, examples, benches..."
} else {
"No matches"
};
let hint = ratatui::widgets::Paragraph::new(Line::from(Span::styled(
format!(" {msg}"),
Style::default().fg(label_color()),
)));
frame.render_widget(hint, area);
return;
}
let query = ctx.project_list.finder.query.clone();
let rows: Vec<Row> = ctx
.project_list
.finder
.results
.iter()
.enumerate()
.map(|(row_index, &idx)| {
let item = &ctx.project_list.finder.index[idx];
let parent = if item.kind == FinderKind::Project {
String::new()
} else {
item.parent_label.clone()
};
Row::new(vec![
Cell::from(highlighted_spans(
&item.display_name,
&query,
text_default(),
)),
Cell::from(highlighted_spans(&parent, &query, text_default())),
Cell::from(highlighted_spans(&item.branch, &query, text_default())),
Cell::from(highlighted_spans(&item.dir, &query, text_default())),
Cell::from(highlighted_spans(
item.kind.label(),
&query,
item.kind.color(),
)),
])
.style(
tui_pane::selection_state(&pane.viewport, row_index, pane.focus.pane_focus_state)
.overlay_style(),
)
})
.collect();
let widths = col_widths.map(|w| Constraint::Length(u16::try_from(w).unwrap_or(u16::MAX)));
let header_style = Style::default()
.fg(label_color())
.add_modifier(Modifier::BOLD);
let header = Row::new(
FINDER_HEADERS
.iter()
.map(|h| Cell::from(Span::styled(*h, header_style))),
);
let table = Table::new(rows, widths)
.header(header)
.column_spacing(1)
.row_highlight_style(Style::default());
let mut table_state = TableState::default().with_selected(Some(pane.viewport.pos()));
*table_state.offset_mut() = pane.viewport.scroll_offset();
frame.render_stateful_widget(table, area, &mut table_state);
pane.viewport.set_scroll_offset(table_state.offset());
render_overflow_affordance(
frame,
popup_area,
pane.viewport.overflow(),
Style::default().fg(label_color()),
);
}