mod prose;
mod render;
mod lens;
pub use lens::{BandContext, BlameMark};
mod motion;
mod find;
use find::{FindState, chip_line};
use super::pane::{Entry, EntryKind};
use crate::components::modeline::fit_middle;
use crate::sanitize;
use crate::theme::Theme;
use ratatui::Frame;
use ratatui::layout::{Constraint, Direction, Layout, Rect};
use ratatui::style::{Modifier, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::{Block, Borders, Paragraph, Wrap};
use unicode_width::UnicodeWidthStr;
#[derive(Debug, Clone, Default)]
pub enum PreviewContent {
#[default]
Empty,
Text(String),
Prose(String),
Highlighted(Vec<Line<'static>>),
DirSummary(Vec<Entry>),
Binary {
size: usize,
},
}
pub struct Preview {
pub content: PreviewContent,
pub title: String,
pub focused: bool,
scroll: usize,
cursor: usize,
line_count: usize,
numbered: bool,
lang: Option<String>,
find: Option<FindState>,
band_path: Option<String>,
band_context: Option<BandContext>,
visual_anchor: Option<usize>,
blame: Option<Vec<Option<BlameMark>>>,
motion_count: crate::keymap::MotionCount,
motion_pending: Option<crate::keymap::MotionPrefix>,
viewport: usize,
prose_viewport: prose::ProseViewport,
}
impl Default for Preview {
fn default() -> Self {
Self::new()
}
}
impl Preview {
pub fn new() -> Self {
Preview {
content: PreviewContent::Empty,
title: "preview".into(),
focused: false,
scroll: 0,
cursor: 0,
line_count: 0,
numbered: false,
lang: None,
find: None,
band_path: None,
band_context: None,
visual_anchor: None,
blame: None,
motion_count: crate::keymap::MotionCount::default(),
motion_pending: None,
viewport: 0,
prose_viewport: prose::ProseViewport::default(),
}
}
pub fn text_line_count(&self) -> usize {
self.line_count
}
pub fn content_text(&self) -> Option<String> {
match &self.content {
PreviewContent::Text(text) | PreviewContent::Prose(text) => Some(text.clone()),
PreviewContent::Highlighted(lines) => Some(
lines
.iter()
.map(|l| l.spans.iter().map(|s| s.content.as_ref()).collect())
.collect::<Vec<String>>()
.join("\n"),
),
_ => None,
}
}
pub fn copy_target(&self) -> Option<(String, usize)> {
let lines: Vec<String> = self.content_text()?.lines().map(str::to_string).collect();
let (lo, hi) = match self.visual_range() {
Some((a, b)) => (a as usize, b as usize),
None => {
let l = self.line()? as usize;
(l, l)
}
};
let slice: Vec<String> = lines
.iter()
.enumerate()
.filter(|(i, _)| (*i + 1) >= lo && (*i + 1) <= hi)
.map(|(_, l)| l.clone())
.collect();
(!slice.is_empty()).then(|| {
let mut text = slice.join("\n");
text.push('\n');
(text, slice.len())
})
}
pub fn focused() -> Self {
Preview {
focused: true,
..Preview::new()
}
}
pub fn set_bytes(&mut self, name: &str, bytes: &[u8]) {
self.title = sanitize::sanitize_inline(name);
self.lang = None;
if sanitize::is_binary(bytes) {
self.content = PreviewContent::Binary { size: bytes.len() };
self.line_count = 0;
self.numbered = false;
} else {
let text = sanitize::sanitize(bytes);
self.line_count = text.lines().count();
self.content = PreviewContent::Text(text);
self.numbered = true;
}
self.reset();
}
pub fn set_dir(&mut self, name: &str, children: Vec<Entry>) {
self.title = format!("{}/", sanitize::sanitize_inline(name));
self.content = PreviewContent::DirSummary(children);
self.line_count = 0;
self.numbered = false;
self.lang = None;
self.reset();
}
fn set_meta_text(&mut self, name: &str, size: Option<u64>, sha: &str, tail: &str) {
self.title = sanitize::sanitize_inline(name);
let size = size.map(|s| s.to_string()).unwrap_or_else(|| "?".into());
let short = &sha[..sha.len().min(7)];
let text = format!("{size} bytes · blob {short}\n\n{tail}");
self.line_count = text.lines().count();
self.content = PreviewContent::Text(text);
self.numbered = false;
self.lang = None;
self.reset();
}
pub fn set_file_meta(&mut self, name: &str, size: Option<u64>, sha: &str) {
self.set_meta_text(name, size, sha, "loading…");
}
pub fn set_error(&mut self, name: &str, size: Option<u64>, sha: &str, message: &str) {
self.set_meta_text(name, size, sha, &format!("error: {message}"));
}
pub fn set_highlighted(&mut self, name: &str, lang: &str, lines: Vec<Line<'static>>) {
self.title = sanitize::sanitize_inline(name);
self.line_count = lines.len();
self.content = PreviewContent::Highlighted(lines);
self.numbered = true;
self.lang = Some(lang.to_string());
self.reset();
}
pub(crate) fn set_text(&mut self, title: &str, text: String) {
self.title = sanitize::sanitize_inline(title);
self.line_count = 0;
self.content = PreviewContent::Prose(text);
self.numbered = false;
self.lang = None;
self.reset();
}
pub(crate) fn scroll_text(&mut self, movement: crate::components::list_view::ListMovement) {
self.prose_viewport.scroll(movement);
}
pub(crate) fn page_text(&mut self, forward: bool, half: bool) {
self.prose_viewport.page(forward, half);
}
pub(crate) fn pane_block(title: String, focused: bool, theme: &Theme) -> Block<'static> {
Block::default()
.borders(Borders::ALL)
.border_type(theme.border_type())
.border_style(Style::default().fg(if focused {
theme.semantic.border_focused
} else {
theme.semantic.border_unfocused
}))
.style(Style::default().bg(theme.semantic.base))
.title(Span::styled(
title,
Style::default().fg(theme.semantic.subtext0),
))
}
fn plain_lines(&self) -> Vec<String> {
match &self.content {
PreviewContent::Text(text) => text.lines().map(|l| l.replace('\t', " ")).collect(),
PreviewContent::Highlighted(lines) => lines
.iter()
.map(|l| l.spans.iter().map(|s| s.content.as_ref()).collect())
.collect(),
_ => vec![],
}
}
fn reset(&mut self) {
self.band_path = None;
self.band_context = None;
self.visual_anchor = None;
self.scroll = 0;
self.cursor = 0;
self.find = None;
self.prose_viewport = prose::ProseViewport::default();
}
fn clamp_scroll(&mut self, viewport: usize) {
if self.line_count == 0 || viewport == 0 {
return;
}
if self.cursor < self.scroll {
self.scroll = self.cursor;
} else if self.cursor >= self.scroll + viewport {
self.scroll = self.cursor + 1 - viewport;
}
}
}
#[cfg(test)]
mod tests;