use ratatui::Frame;
use ratatui::layout::{Constraint, Direction, Layout, Rect};
use ratatui::style::{Color, Modifier, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::{List, ListItem, Paragraph};
use crate::finder::{Finder, FuzzyKind};
const DIR_FG: Color = Color::Blue;
const HIT_FG: Color = Color::Magenta;
fn severity_color_from_badge(c: char) -> Option<Color> {
match c {
'E' => Some(Color::Red),
'W' => Some(Color::Yellow),
'I' => Some(Color::Cyan),
'H' => Some(Color::DarkGray),
_ => None,
}
}
pub(super) fn draw_fuzzy_list(f: &mut Frame, finder: &Finder, area: Rect) {
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(1),
Constraint::Length(1),
Constraint::Min(1),
])
.split(area);
let query_line = Line::from(vec![
Span::styled("› ", Style::default().fg(Color::Yellow)),
Span::raw(finder.query.clone()),
]);
f.render_widget(Paragraph::new(query_line), chunks[0]);
let col = (2 + finder.cursor) as u16;
let x = chunks[0].x + col.min(chunks[0].width.saturating_sub(1));
f.set_cursor_position((x, chunks[0].y));
let sep = "─".repeat(chunks[1].width as usize);
f.render_widget(
Paragraph::new(Span::styled(sep, Style::default().fg(Color::DarkGray))),
chunks[1],
);
let list_h = chunks[2].height as usize;
let list_w = chunks[2].width as usize;
let scroll = finder.selected.saturating_sub(list_h.saturating_sub(1));
let items: Vec<ListItem> = finder
.matches
.iter()
.enumerate()
.skip(scroll)
.take(list_h)
.map(|(i, m)| {
let raw = &finder.items[m.idx];
let line = if matches!(finder.kind, FuzzyKind::WorkspaceSearch) {
let row = m.line_hits.first().copied().unwrap_or(0);
render_workspace_match(raw, row, i == finder.selected, list_w)
} else {
render_match(raw, &m.positions, i == finder.selected, finder.kind, list_w)
};
ListItem::new(line)
})
.collect();
f.render_widget(List::new(items), chunks[2]);
}
fn render_workspace_match<'a>(path: &'a str, row: usize, selected: bool, width: usize) -> Line<'a> {
let base = if selected {
Style::default()
.bg(Color::DarkGray)
.add_modifier(Modifier::BOLD)
} else {
Style::default()
};
let dir = base.fg(DIR_FG).add_modifier(Modifier::BOLD);
let lineno = base.fg(Color::Cyan);
let sep = base.fg(Color::DarkGray);
let dim = base.fg(Color::DarkGray);
let lineno_str = format!(":{}", row + 1);
let lineno_w = lineno_str.chars().count();
let path_chars: Vec<char> = path.chars().collect();
let dir_end = path_chars
.iter()
.rposition(|c| *c == '/')
.map(|i| i + 1)
.unwrap_or(0);
let path_budget = width.saturating_sub(lineno_w);
let mut spans: Vec<Span<'a>> = Vec::new();
let path_start = if path_budget >= 2 && path_chars.len() > path_budget {
spans.push(Span::styled("…", dim));
path_chars.len() - (path_budget - 1)
} else {
0
};
let dir_end_visible = dir_end.saturating_sub(path_start);
let mut buf = String::new();
let mut buf_style = base;
for (offset, &c) in path_chars[path_start..].iter().enumerate() {
let in_dir = offset < dir_end_visible;
let style = if in_dir { dir } else { base };
if style != buf_style && !buf.is_empty() {
spans.push(Span::styled(std::mem::take(&mut buf), buf_style));
}
buf_style = style;
buf.push(c);
}
if !buf.is_empty() {
spans.push(Span::styled(buf, buf_style));
}
spans.push(Span::styled(":", sep));
spans.push(Span::styled(format!("{}", row + 1), lineno));
Line::from(spans)
}
fn render_match<'a>(
item: &'a str,
positions: &[usize],
selected: bool,
kind: FuzzyKind,
width: usize,
) -> Line<'a> {
let base = if selected {
Style::default()
.bg(Color::DarkGray)
.add_modifier(Modifier::BOLD)
} else {
Style::default()
};
let dir = base.fg(DIR_FG).add_modifier(Modifier::BOLD);
let hit = base.fg(HIT_FG).add_modifier(Modifier::BOLD);
let ellipsis = base.fg(Color::DarkGray);
let sev_style: Option<Style> =
if matches!(kind, FuzzyKind::Diagnostics { .. }) && item.len() >= 4 {
severity_color_from_badge(item.as_bytes()[1] as char)
.map(|c| base.fg(c).add_modifier(Modifier::BOLD))
} else {
None
};
let chars: Vec<char> = item.chars().collect();
let dir_end_char: Option<usize> = match kind {
FuzzyKind::Files { .. } | FuzzyKind::Buffers => {
chars.iter().rposition(|c| *c == '/').map(|i| i + 1)
}
FuzzyKind::Locations | FuzzyKind::WorkspaceSearch => {
let path_end = chars.iter().position(|c| *c == ':').unwrap_or(chars.len());
chars[..path_end]
.iter()
.rposition(|c| *c == '/')
.map(|i| i + 1)
}
FuzzyKind::Diagnostics { .. } => {
let prefix_end = chars
.iter()
.position(|c| *c == ' ')
.map(|i| i + 1)
.unwrap_or(chars.len());
let path_end = chars[prefix_end..]
.iter()
.position(|c| *c == ':')
.map(|i| i + prefix_end)
.unwrap_or(chars.len());
chars[prefix_end..path_end]
.iter()
.rposition(|c| *c == '/')
.map(|i| i + prefix_end + 1)
}
FuzzyKind::Lines => None,
};
let mut spans = Vec::new();
let pinned_prefix = match kind {
FuzzyKind::Diagnostics { .. }
if chars.len() >= 4 && chars[0] == '[' && chars[2] == ']' && chars[3] == ' ' =>
{
4
}
_ => 0,
};
let start = if width >= 2 && chars.len() > width {
if pinned_prefix > 0 && width > pinned_prefix + 1 {
for (idx, c) in chars[..pinned_prefix].iter().enumerate() {
let is_hit = positions.binary_search(&idx).is_ok();
let style = if is_hit {
hit
} else {
sev_style.unwrap_or(base)
};
spans.push(Span::styled(c.to_string(), style));
}
spans.push(Span::styled("…", ellipsis));
chars.len() - (width - pinned_prefix - 1)
} else {
spans.push(Span::styled("…", ellipsis));
chars.len() - (width - 1)
}
} else {
0
};
let dir_end_visible = dir_end_char.map(|e| e.saturating_sub(start));
let mut buf = String::new();
let mut buf_style = base;
for (offset, &c) in chars[start..].iter().enumerate() {
let orig_i = start + offset;
let is_hit = positions.binary_search(&orig_i).is_ok();
let in_dir = dir_end_visible.map(|e| offset < e).unwrap_or(false);
let in_badge = sev_style.is_some() && orig_i < pinned_prefix;
let style = if is_hit {
hit
} else if in_badge {
sev_style.unwrap()
} else if in_dir {
dir
} else {
base
};
if style != buf_style && !buf.is_empty() {
spans.push(Span::styled(std::mem::take(&mut buf), buf_style));
}
buf_style = style;
buf.push(c);
}
if !buf.is_empty() {
spans.push(Span::styled(buf, buf_style));
}
Line::from(spans)
}