use ratatui::Frame;
use ratatui::layout::Rect;
use ratatui::style::{Color, Modifier, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::Paragraph;
use crate::app::App;
use crate::finder::{Finder, FuzzyKind};
use crate::syntax::{self, Capture};
const PREVIEW_HIT_BG: Color = Color::Rgb(45, 45, 60);
pub(super) fn draw_fuzzy_preview(f: &mut Frame, app: &App, finder: &Finder, area: Rect) {
let Some(sel) = finder.selection() else {
return;
};
match finder.kind {
FuzzyKind::Files { .. } => {
let rel = &finder.items[sel.idx];
let path = app.startup_cwd.join(rel);
preview_from_file(f, app, area, &path, 0);
}
FuzzyKind::Lines => {
preview_from_buffer(f, app, area, sel.idx);
}
FuzzyKind::Locations => {
let Some(loc) = app.prompt.locations().get(sel.idx) else {
return;
};
let Some(path) = crate::lsp::uri_to_path(&loc.uri) else {
return;
};
preview_from_file(f, app, area, &path, loc.range.start.line as usize);
}
FuzzyKind::Buffers => {
let Some(r) = app.prompt.buffer_paths().get(sel.idx) else {
return;
};
match r {
crate::buffer_ref::BufferRef::Scratch => {}
crate::buffer_ref::BufferRef::File(path) => {
preview_from_file(f, app, area, path, 0);
}
}
}
}
}
fn preview_from_buffer(f: &mut Frame, app: &App, area: Rect, target_row: usize) {
let lines = &app.buffer.lines;
let height = area.height as usize;
let (scroll, end) = preview_scroll(lines.len(), target_row, height);
let captures = app
.buffer
.highlighter
.as_ref()
.map(|h| h.captures_in_rows(scroll, end.saturating_sub(1)))
.unwrap_or_default();
render_preview_lines(f, area, lines, &captures, target_row, scroll, end);
}
fn preview_from_file(
f: &mut Frame,
app: &App,
area: Rect,
path: &std::path::Path,
target_row: usize,
) {
let mut lru = app.preview_lru.borrow_mut();
let Some(entry) = lru.get(path) else {
drop(lru);
enqueue_preview(app, path);
preview_plain_fallback(f, area, path, target_row);
return;
};
let height = area.height as usize;
let (scroll, end) = preview_scroll(entry.lines.len(), target_row, height);
let captures = entry
.highlighter
.captures_in_rows(scroll, end.saturating_sub(1));
render_preview_lines(f, area, &entry.lines, &captures, target_row, scroll, end);
}
fn enqueue_preview(app: &App, path: &std::path::Path) {
let mut last = app.last_preview_request.borrow_mut();
if last.as_deref() == Some(path) {
return;
}
*last = Some(path.to_path_buf());
let _ = app.preview_tx.send(path.to_path_buf());
}
fn preview_plain_fallback(
f: &mut Frame,
area: Rect,
path: &std::path::Path,
target_row: usize,
) {
match std::fs::read_to_string(path) {
Ok(content) => {
let lines: Vec<String> = content.lines().map(|s| s.to_string()).collect();
let height = area.height as usize;
let (scroll, end) = preview_scroll(lines.len(), target_row, height);
render_preview_lines(f, area, &lines, &[], target_row, scroll, end);
}
Err(_) => {
f.render_widget(
Paragraph::new(Span::styled(
"(cannot read file)",
Style::default().fg(Color::DarkGray),
)),
area,
);
}
}
}
fn preview_scroll(lines_len: usize, target: usize, height: usize) -> (usize, usize) {
if height == 0 || lines_len == 0 {
return (0, 0);
}
let target = target.min(lines_len - 1);
let half = height / 2;
let max_scroll = lines_len.saturating_sub(height);
let scroll = target.saturating_sub(half).min(max_scroll);
let end = (scroll + height).min(lines_len);
(scroll, end)
}
fn render_preview_lines(
f: &mut Frame,
area: Rect,
lines: &[String],
captures: &[Capture],
target_row: usize,
scroll: usize,
end: usize,
) {
let height = area.height as usize;
let width = area.width as usize;
if height == 0 || width == 0 || end <= scroll {
return;
}
let target = target_row.min(lines.len().saturating_sub(1));
let lineno_w = end.to_string().len().max(3);
let text_width = width.saturating_sub(lineno_w + 1);
let mut out: Vec<Line> = Vec::with_capacity(end - scroll);
for (i, line) in lines.iter().enumerate().take(end).skip(scroll) {
out.push(render_preview_row(
i,
line,
captures,
i == target,
lineno_w,
text_width,
));
}
f.render_widget(Paragraph::new(out), area);
}
fn render_preview_row(
row: usize,
line: &str,
captures: &[Capture],
is_target: bool,
lineno_w: usize,
text_width: usize,
) -> Line<'static> {
let chars: Vec<char> = line.chars().take(text_width).collect();
let mut base: Vec<Style> = vec![Style::default(); chars.len()];
for cap in captures {
if cap.end_row < row || cap.start_row > row {
continue;
}
let lo = if cap.start_row == row {
cap.start_col
} else {
0
};
let hi = if cap.end_row == row {
cap.end_col.min(chars.len())
} else {
chars.len()
};
if lo >= hi {
continue;
}
let style = syntax::style_for(&cap.name);
for slot in base.iter_mut().take(hi).skip(lo) {
*slot = style;
}
}
let num_style = if is_target {
Style::default()
.fg(Color::Yellow)
.add_modifier(Modifier::BOLD)
.bg(PREVIEW_HIT_BG)
} else {
Style::default().fg(Color::DarkGray)
};
let num = format!("{:>width$} ", row + 1, width = lineno_w);
let mut spans: Vec<Span<'static>> = vec![Span::styled(num, num_style)];
let resolve = |col: usize| -> Style {
let mut s = base[col];
if is_target {
s = s.bg(PREVIEW_HIT_BG);
}
s
};
if chars.is_empty() {
if is_target {
spans.push(Span::styled(
" ".repeat(text_width),
Style::default().bg(PREVIEW_HIT_BG),
));
}
return Line::from(spans);
}
let mut buf = String::new();
let mut buf_style = resolve(0);
for (col, &c) in chars.iter().enumerate() {
let s = resolve(col);
if s != buf_style && !buf.is_empty() {
spans.push(Span::styled(std::mem::take(&mut buf), buf_style));
buf_style = s;
}
buf.push(c);
}
if !buf.is_empty() {
spans.push(Span::styled(buf, buf_style));
}
if is_target && chars.len() < text_width {
spans.push(Span::styled(
" ".repeat(text_width - chars.len()),
Style::default().bg(PREVIEW_HIT_BG),
));
}
Line::from(spans)
}