use super::state::VisualRange;
use crate::theme::Palette;
use ratatui::{
style::{Color, Modifier, Style},
text::{Line, Span, Text},
};
use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
pub fn apply_block_highlight(
lines: &mut [Line<'static>],
visual_mode: Option<VisualRange>,
cursor_line: u32,
block_start: u32,
block_end: u32,
clip_start: usize,
bg: Color,
) {
match visual_mode {
Some(range) => {
let block_visible_start = block_start + crate::cast::u32_sat(clip_start);
let block_visible_end =
block_start + crate::cast::u32_sat(clip_start) + crate::cast::u32_sat(lines.len());
for abs in block_visible_start..block_visible_end {
let idx = (abs - block_visible_start) as usize;
let line_width = lines.get(idx).map_or(0, |l| {
crate::cast::u16_sat(
l.spans
.iter()
.map(|s| UnicodeWidthStr::width(s.content.as_ref()))
.sum::<usize>(),
)
});
if let Some((sc, ec)) = range.char_range_on_line(abs, line_width) {
if sc == 0 && ec >= line_width {
patch_cursor_highlight(lines, idx, bg);
} else {
if let Some(line) = lines.get(idx) {
lines[idx] = highlight_columns(line, sc, ec, bg);
}
}
}
}
}
None => {
if cursor_line >= block_start && cursor_line < block_end {
let cursor_relative = (cursor_line - block_start) as usize;
if cursor_relative >= clip_start {
let idx = cursor_relative - clip_start;
patch_cursor_highlight(lines, idx, bg);
}
}
}
}
}
pub fn highlight_columns(
line: &Line<'static>,
start_col: u16,
end_col: u16,
bg: Color,
) -> Line<'static> {
if start_col >= end_col {
return line.clone();
}
let sel_style = Style::default().bg(bg);
let mut out: Vec<Span<'static>> = Vec::new();
let mut col: u16 = 0;
for span in &line.spans {
let span_start_col = col;
let span_text = span.content.as_ref();
let span_width = crate::cast::u16_sat(UnicodeWidthStr::width(span_text));
let span_end_col = col + span_width;
if span_end_col <= start_col || span_start_col >= end_col {
out.push(span.clone());
col = span_end_col;
continue;
}
if span_start_col >= start_col && span_end_col <= end_col {
out.push(Span::styled(
span.content.clone(),
span.style.patch(sel_style),
));
col = span_end_col;
continue;
}
let mut before = String::new();
let mut inside = String::new();
let mut after = String::new();
let mut c_col = span_start_col;
for ch in span_text.chars() {
let w = crate::cast::u16_sat(UnicodeWidthChar::width(ch).unwrap_or(1));
let next = c_col + w;
if next <= start_col {
before.push(ch);
} else if c_col >= end_col {
after.push(ch);
} else {
if c_col < start_col {
before.push(ch);
} else {
inside.push(ch);
}
}
c_col = next;
}
if !before.is_empty() {
out.push(Span::styled(before, span.style));
}
if !inside.is_empty() {
out.push(Span::styled(inside, span.style.patch(sel_style)));
}
if !after.is_empty() {
out.push(Span::styled(after, span.style));
}
col = span_end_col;
}
Line::from(out)
}
pub fn extract_line_text_range(line: &Line<'static>, start_col: u16, end_col: u16) -> String {
if start_col >= end_col {
return String::new();
}
let mut out = String::new();
let mut col: u16 = 0;
for span in &line.spans {
for ch in span.content.as_ref().chars() {
let w = crate::cast::u16_sat(UnicodeWidthChar::width(ch).unwrap_or(1));
let next = col + w;
if col >= end_col {
break;
}
if next > start_col {
out.push(ch);
}
col = next;
}
if col >= end_col {
break;
}
}
out
}
pub fn patch_cursor_highlight(lines: &mut [Line<'static>], idx: usize, bg: Color) {
let Some(line) = lines.get_mut(idx) else {
return;
};
if line.spans.is_empty() {
*line = Line::from(Span::styled(" ".to_string(), Style::default().bg(bg)));
} else {
for span in &mut line.spans {
span.style = span.style.patch(Style::default().bg(bg));
}
}
}
pub fn highlight_matches(
text: &Text<'static>,
query: &str,
current_line: Option<u32>,
block_start: u32,
p: &Palette,
) -> Text<'static> {
let query_lower = query.to_lowercase();
let match_style = Style::default()
.bg(p.search_match_bg)
.fg(p.match_fg)
.add_modifier(Modifier::BOLD);
let current_style = Style::default()
.bg(p.current_match_bg)
.fg(p.match_fg)
.add_modifier(Modifier::BOLD);
let lines: Vec<Line<'static>> = text
.lines
.iter()
.enumerate()
.map(|(line_idx, line)| {
let line_text: String = line.spans.iter().map(|s| s.content.as_ref()).collect();
if !line_text.to_lowercase().contains(&query_lower) {
return line.clone();
}
let is_current = current_line == Some(block_start + crate::cast::u32_sat(line_idx));
let hl_style = if is_current {
current_style
} else {
match_style
};
let mut new_spans: Vec<Span<'static>> = Vec::new();
for span in &line.spans {
split_and_highlight(
&span.content,
&query_lower,
span.style,
hl_style,
&mut new_spans,
);
}
Line::from(new_spans)
})
.collect();
Text::from(lines)
}
fn split_and_highlight(
text: &str,
query_lower: &str,
base_style: Style,
highlight_style: Style,
out: &mut Vec<Span<'static>>,
) {
let text_lower = text.to_lowercase();
let mut start = 0;
while let Some(pos) = text_lower[start..].find(query_lower) {
let abs_pos = start + pos;
if abs_pos > start {
out.push(Span::styled(text[start..abs_pos].to_string(), base_style));
}
let match_end = abs_pos + query_lower.len();
out.push(Span::styled(
text[abs_pos..match_end].to_string(),
highlight_style,
));
start = match_end;
}
if start < text.len() {
out.push(Span::styled(text[start..].to_string(), base_style));
}
}