use super::*;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum PreviewSelection {
None,
Char {
start: (usize, usize),
end: (usize, usize),
},
Line { lo: usize, hi: usize },
}
impl App {
pub fn is_preview_visual(&self) -> bool {
self.is_windowed() && self.preview_visual_anchor.is_some()
}
pub fn preview_visual_linewise(&self) -> bool {
self.preview_visual_linewise
}
pub fn preview_selection(&self) -> PreviewSelection {
let Some((al, ac)) = self.preview_visual_anchor else {
return PreviewSelection::None;
};
let (cl, cc) = (self.tab.preview_cursor_line, self.tab.preview_cursor_col);
if self.preview_visual_linewise {
PreviewSelection::Line {
lo: al.min(cl),
hi: al.max(cl),
}
} else {
let (start, end) = if (al, ac) <= (cl, cc) {
((al, ac), (cl, cc))
} else {
((cl, cc), (al, ac))
};
PreviewSelection::Char { start, end }
}
}
pub fn preview_enter_visual(&mut self, linewise: bool) {
if self.is_windowed() {
self.preview_visual_anchor =
Some((self.tab.preview_cursor_line, self.tab.preview_cursor_col));
self.preview_visual_linewise = linewise;
}
}
pub fn preview_exit_visual(&mut self) {
self.preview_visual_anchor = None;
}
pub(super) fn preview_selection_text(&self) -> String {
let Some(path) = self.tab.preview_path.as_ref() else {
return String::new();
};
let bytes = match std::fs::read(path) {
Ok(b) => b,
Err(_) => return String::new(),
};
let s = String::from_utf8_lossy(&bytes);
let lines: Vec<&str> = s.lines().collect();
if lines.is_empty() {
return String::new();
}
let last = lines.len() - 1;
match self.preview_selection() {
PreviewSelection::Line { lo, hi } => {
let end = hi.min(last);
if lo > end {
String::new()
} else {
lines[lo..=end].join("\n")
}
}
PreviewSelection::None => {
let l = self.tab.preview_cursor_line.min(last);
lines[l].to_string()
}
PreviewSelection::Char { start, end } => selection_char_text(&lines, start, end),
}
}
pub fn preview_copy_selection(&mut self) {
let text = self.preview_selection_text();
self.preview_visual_anchor = None;
if text.is_empty() {
self.flash = Some(tr(self.lang, crate::i18n::Msg::NoCopyTarget).into());
return;
}
self.set_clipboard_flash(&text);
}
pub(super) fn preview_selection_ref_text(&self) -> Option<String> {
let path = self.tab.preview_path.as_ref()?;
let base = at_ref_text(&self.tab.open_dir, path);
if !self.is_windowed() {
return Some(base);
}
let (lo, hi) = match self.preview_selection() {
PreviewSelection::Line { lo, hi } => (lo, hi),
PreviewSelection::Char { start, end } => (start.0, end.0),
PreviewSelection::None => (self.tab.preview_cursor_line, self.tab.preview_cursor_line),
};
Some(if lo == hi {
format!("{base}#L{}", lo + 1)
} else {
format!("{base}#L{}-{}", lo + 1, hi + 1)
})
}
pub fn preview_copy_selection_ref(&mut self) {
let Some(text) = self.preview_selection_ref_text() else {
self.flash = Some(tr(self.lang, crate::i18n::Msg::NoCopyTarget).into());
return;
};
self.preview_visual_anchor = None;
self.set_clipboard_flash(&text);
}
pub fn preview_col_move(&mut self, dir: i32) {
if self.is_windowed() {
let next = (self.tab.preview_cursor_col as i64 + dir as i64).max(0) as usize;
self.tab.preview_cursor_col = next;
} else {
self.preview_hscroll(dir * 2);
}
}
pub fn preview_col_home(&mut self) {
if self.is_windowed() {
self.tab.preview_cursor_col = 0;
} else {
self.preview_hscroll_home();
}
}
pub fn preview_col_end(&mut self) {
if self.is_windowed() {
self.tab.preview_cursor_col = usize::MAX;
} else {
self.preview_hscroll_end();
}
}
}
fn selection_char_text(lines: &[&str], start: (usize, usize), end: (usize, usize)) -> String {
let (sl, sc) = start;
let (el, ec) = end;
if sl >= lines.len() {
return String::new();
}
let el = el.min(lines.len() - 1);
if sl == el {
lines[sl]
.chars()
.skip(sc)
.take((ec + 1).saturating_sub(sc))
.collect()
} else {
let mut out = String::new();
out.push_str(&lines[sl].chars().skip(sc).collect::<String>());
for l in &lines[sl + 1..el] {
out.push('\n');
out.push_str(l);
}
out.push('\n');
out.push_str(&lines[el].chars().take(ec + 1).collect::<String>());
out
}
}
fn char_range_for_line(
abs: usize,
start: (usize, usize),
end: (usize, usize),
) -> Option<(usize, usize)> {
if abs < start.0 || abs > end.0 {
return None;
}
let lo = if abs == start.0 { start.1 } else { 0 };
let hi = if abs == end.0 {
end.1.saturating_add(1)
} else {
usize::MAX
};
Some((lo, hi))
}
pub(super) fn apply_preview_caret(
lines: Vec<Line<'static>>,
top_line: usize,
cursor_line: usize,
cursor_col: usize,
sel: PreviewSelection,
) -> Vec<Line<'static>> {
use ratatui::style::Color;
const CURSOR_BG: Color = Color::Rgb(55, 60, 74);
const SEL_BG: Color = Color::Rgb(40, 66, 104);
lines
.into_iter()
.enumerate()
.map(|(i, line)| {
let abs = top_line + i;
let caret = if abs == cursor_line {
Some(cursor_col)
} else {
None
};
let (range, whole): (Option<(usize, usize, Color)>, bool) = match sel {
PreviewSelection::None => {
if abs == cursor_line {
(Some((0, usize::MAX, CURSOR_BG)), true)
} else {
(None, false)
}
}
PreviewSelection::Line { lo, hi } => {
if abs >= lo && abs <= hi {
(Some((0, usize::MAX, SEL_BG)), true)
} else {
(None, false)
}
}
PreviewSelection::Char { start, end } => match char_range_for_line(abs, start, end)
{
Some((lo, hi)) => (Some((lo, hi, SEL_BG)), lo == 0 && hi == usize::MAX),
None => (None, false),
},
};
if range.is_none() && caret.is_none() {
return line;
}
restyle_line(line, range, whole, caret)
})
.collect()
}
fn restyle_line(
line: Line<'static>,
range: Option<(usize, usize, ratatui::style::Color)>,
whole: bool,
caret: Option<usize>,
) -> Line<'static> {
use ratatui::style::Modifier;
let base = line.style;
let mut out: Vec<Span<'static>> = Vec::new();
let mut idx = 0usize; let mut cur = String::new();
let mut cur_style: Option<ratatui::style::Style> = None;
for span in line.spans.into_iter() {
for ch in span.content.chars() {
let mut style = span.style;
if let Some((lo, hi, color)) = range {
if idx >= lo && idx < hi {
style = style.bg(color);
}
}
if caret == Some(idx) {
style = style.add_modifier(Modifier::REVERSED);
}
if cur_style != Some(style) {
if !cur.is_empty() {
out.push(Span::styled(std::mem::take(&mut cur), cur_style.unwrap()));
}
cur_style = Some(style);
}
cur.push(ch);
idx += 1;
}
}
if !cur.is_empty() {
out.push(Span::styled(cur, cur_style.unwrap_or(base)));
}
let mut line = Line::from(out);
line.style = if whole {
if let Some((_, _, color)) = range {
base.bg(color)
} else {
base
}
} else {
base
};
line
}