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::DarkGray;
pub(super) fn draw_fuzzy_preview(f: &mut Frame, app: &App, finder: &Finder, area: Rect) {
let Some(sel) = finder.selection() else {
return;
};
let header_style = Style::default().add_modifier(Modifier::BOLD);
match finder.kind {
FuzzyKind::Files { .. } => {
let rel = &finder.items[sel.idx];
let body = super::split_with_header(f, area, rel, header_style);
let path = app.startup_cwd.join(rel);
preview_from_file(f, app, body, &path, 0);
}
FuzzyKind::Lines => {
let label = buffer_label(app);
let body = super::split_with_header(f, area, &label, header_style);
preview_from_buffer(f, app, body, sel.idx);
}
FuzzyKind::Locations | FuzzyKind::Diagnostics { .. } => {
let Some(loc) = app.prompt.locations().get(sel.idx) else {
return;
};
let Some(path) = crate::lsp::uri_to_path(&loc.uri) else {
return;
};
let label = display_path(app, &path);
let body = super::split_with_header(f, area, &label, header_style);
let row = loc.range.start.line as usize;
if app.buffer.path.as_deref() == Some(path.as_path()) {
preview_from_buffer(f, app, body, row);
return;
}
for (key, buf) in &app.parked_buffers {
if let crate::buffer_ref::BufferRef::File(p) = key
&& p == &path
{
preview_from_parked_buffer(f, body, buf, row);
return;
}
}
for (key, snap) in &app.sleeping {
if let crate::buffer_ref::BufferRef::File(p) = key
&& p == &path
{
preview_from_sleeping(f, body, snap, row);
return;
}
}
preview_from_file(f, app, body, &path, row);
}
FuzzyKind::WorkspaceSearch => {
let Some(loc) = app.prompt.locations().get(sel.idx) else {
return;
};
let Some(path) = crate::lsp::uri_to_path(&loc.uri) else {
return;
};
let label = display_path(app, &path);
let body = super::split_with_header(f, area, &label, header_style);
let target = sel.line_hits.first().copied().unwrap_or(0);
preview_from_file(f, app, body, &path, target);
}
FuzzyKind::Buffers => {
let Some(r) = app.prompt.buffer_paths().get(sel.idx) else {
return;
};
match r {
crate::buffer_ref::BufferRef::Scratch(id) => {
let label = crate::buffer_ref::BufferRef::scratch_label(*id);
let _ = super::split_with_header(f, area, &label, header_style);
}
crate::buffer_ref::BufferRef::File(path) => {
let label = display_path(app, path);
let body = super::split_with_header(f, area, &label, header_style);
preview_from_file(f, app, body, path, 0);
}
}
}
}
}
fn buffer_label(app: &App) -> String {
match app.buffer.path.as_deref() {
Some(p) => display_path(app, p),
None => crate::buffer_ref::BufferRef::scratch_label(app.current_scratch_id.unwrap_or(0)),
}
}
fn display_path(app: &App, path: &std::path::Path) -> String {
path.strip_prefix(&app.startup_cwd)
.map(|p| p.display().to_string())
.unwrap_or_else(|_| path.display().to_string())
}
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_parked_buffer(
f: &mut Frame,
area: Rect,
buf: &crate::editor::Buffer,
target_row: usize,
) {
let lines = &buf.lines;
let height = area.height as usize;
let (scroll, end) = preview_scroll(lines.len(), target_row, height);
let captures = buf
.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_sleeping(
f: &mut Frame,
area: Rect,
snap: &crate::app::SleepingBuffer,
target_row: usize,
) {
let lines = snap.peek_lines();
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);
}
pub(super) 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);
}
const PREVIEW_TAB_WIDTH: usize = 4;
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 = slot.patch(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);
let mut written_cells = 0usize;
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;
}
if c == '\t' {
for _ in 0..PREVIEW_TAB_WIDTH {
buf.push(' ');
}
written_cells += PREVIEW_TAB_WIDTH;
} else {
buf.push(c);
if !c.is_control() {
written_cells += 1;
}
}
}
if !buf.is_empty() {
spans.push(Span::styled(buf, buf_style));
}
if written_cells < text_width {
let pad = text_width - written_cells;
let style = if is_target {
Style::default().bg(PREVIEW_HIT_BG)
} else {
Style::default()
};
spans.push(Span::styled(" ".repeat(pad), style));
}
Line::from(spans)
}
#[cfg(test)]
mod tests {
use super::*;
use ratatui::Terminal;
use ratatui::backend::TestBackend;
use ratatui::layout::Rect;
#[test]
fn preview_row_no_duplication() {
let line = "\t\"context\"".to_string();
let captures = vec![Capture {
start_row: 0,
start_col: 1,
end_row: 0,
end_col: 10,
name: "string".to_string(),
}];
let rendered = render_preview_row(0, &line, &captures, false, 3, 26);
let backend = TestBackend::new(30, 1);
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|f| {
f.render_widget(
ratatui::widgets::Paragraph::new(rendered),
Rect::new(0, 0, 30, 1),
);
})
.unwrap();
let buf = terminal.backend().buffer();
for x in 0..30 {
let cell = &buf[(x, 0)];
eprintln!(
"cell[{:2}]: symbol={:?} fg={:?} bg={:?}",
x,
cell.symbol(),
cell.fg,
cell.bg
);
}
let row: String = (0..30).map(|x| buf[(x, 0)].symbol()).collect();
eprintln!("rendered row: {:?}", row);
let count = row.matches("context").count();
assert_eq!(count, 1, "rendered row: {:?}", row);
}
}