use ratatui::style::{Color, Style};
use ratatui::text::Span;
use std::collections::HashMap;
use crate::app::{JumpState, Selection};
use crate::lsp::Severity;
use crate::syntax::{self, Capture};
use crate::text_width::char_cell_width;
use crate::vcs::LineStatus;
use super::indent_guides::IndentGuide;
use super::{
EXTRA_CURSOR_BG, EXTRA_CURSOR_FG, INDENT_GUIDE_FG, JUMP_LABEL_BG, JUMP_LABEL_FG,
MATCH_BRACKET_FG, SEARCH_HIT_BG, SEL_BG, WHITESPACE_FG,
};
pub(super) fn vcs_bar_span(status: Option<LineStatus>) -> Span<'static> {
match status {
Some(LineStatus::Added) => Span::styled("▎", Style::default().fg(Color::Green)),
Some(LineStatus::Modified) => Span::styled("▎", Style::default().fg(Color::Yellow)),
Some(LineStatus::DeletedAbove) => Span::styled("▁", Style::default().fg(Color::Red)),
None => Span::raw(" "),
}
}
pub(super) fn sign_span(sev: Option<Severity>) -> Span<'static> {
match sev {
Some(Severity::Error) => Span::styled("E", Style::default().fg(Color::Red)),
Some(Severity::Warning) => Span::styled("W", Style::default().fg(Color::Yellow)),
Some(Severity::Info) => Span::styled("I", Style::default().fg(Color::LightBlue)),
Some(Severity::Hint) => Span::styled("H", Style::default().fg(Color::DarkGray)),
None => Span::raw(" "),
}
}
#[allow(clippy::too_many_arguments)]
pub(super) fn render_line(
row: usize,
line: &str,
sel: Option<&Selection>,
captures: &[Capture],
extra_cols: &[usize],
search_hits: &[(usize, usize)],
jump_labels: &[(usize, char)],
bracket_cols: &[usize],
indent_guides: &[IndentGuide],
tab_width: usize,
col_scroll: usize,
viewport_width: usize,
show_whitespace: bool,
) -> Vec<Span<'static>> {
let guide_at =
|vc: usize| -> Option<IndentGuide> { indent_guides.iter().find(|g| g.col == vc).copied() };
let guide_style = |g: IndentGuide| -> Style {
if g.active {
Style::default()
.fg(Color::Reset)
.add_modifier(ratatui::style::Modifier::BOLD)
} else {
Style::default().fg(INDENT_GUIDE_FG)
}
};
let is_extra_cursor = |col: usize| -> bool { extra_cols.contains(&col) };
let is_search_hit =
|col: usize| -> bool { search_hits.iter().any(|(lo, hi)| col >= *lo && col < *hi) };
let is_match_bracket = |col: usize| -> bool { bracket_cols.contains(&col) };
let jump_label_at = |col: usize| -> Option<char> {
jump_labels
.iter()
.find_map(|(c, ch)| if *c == col { Some(*ch) } else { None })
};
let is_selected = |col: usize| -> bool {
let Some(sel) = sel else { return false };
match *sel {
Selection::Char { from, to } => {
if row < from.row || row > to.row {
return false;
}
let lo = if row == from.row { from.col } else { 0 };
if row < to.row {
col >= lo
} else {
col >= lo && col <= to.col
}
}
Selection::Line { from_row, to_row } => row >= from_row && row <= to_row,
Selection::Block { r0, c0, r1, c1 } => row >= r0 && row <= r1 && col >= c0 && col <= c1,
}
};
let chars: Vec<char> = line.chars().collect();
let viewport_right = col_scroll.saturating_add(viewport_width);
let max_guide_col = indent_guides.iter().map(|g| g.col).max();
if chars.is_empty() {
let cursor_cell_style = {
let mut style = Style::default();
if is_selected(0) {
style = style.bg(SEL_BG);
}
if is_extra_cursor(0) {
style = extra_cursor_style(style);
}
style
};
let emit_until = max_guide_col.map(|m| m + 1).unwrap_or(0).max(
if cursor_cell_style != Style::default() {
1
} else {
0
},
);
if emit_until == 0 || col_scroll >= emit_until {
return Vec::new();
}
let mut spans: Vec<Span<'static>> = Vec::new();
let mut buf = String::new();
let mut buf_style = Style::default();
let mut started = false;
for vc in col_scroll..emit_until {
if viewport_width > 0 && vc >= viewport_right {
break;
}
let base_style = if vc == 0 {
cursor_cell_style
} else {
Style::default()
};
let (ch, style) = if let Some(g) = guide_at(vc) {
(g.glyph, base_style.patch(guide_style(g)))
} else {
(' ', base_style)
};
if !started {
buf_style = style;
started = true;
} else if style != buf_style {
if !buf.is_empty() {
spans.push(Span::styled(std::mem::take(&mut buf), buf_style));
}
buf_style = style;
}
buf.push(ch);
}
if !buf.is_empty() {
spans.push(Span::styled(buf, buf_style));
}
return spans;
}
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 style_at = |col: usize| -> Style {
let mut s = base[col];
if is_search_hit(col) {
s = s.bg(SEARCH_HIT_BG);
}
if is_selected(col) {
s = s.bg(SEL_BG);
}
if is_extra_cursor(col) {
s = extra_cursor_style(s);
}
if is_match_bracket(col) {
s = s
.fg(MATCH_BRACKET_FG)
.add_modifier(ratatui::style::Modifier::BOLD);
}
s
};
let cell_at = |col: usize| -> (char, Style) {
if let Some(label) = jump_label_at(col) {
return (
label,
Style::default()
.fg(JUMP_LABEL_FG)
.bg(JUMP_LABEL_BG)
.add_modifier(ratatui::style::Modifier::BOLD),
);
}
let original = chars[col];
let style = style_at(col);
if show_whitespace {
match original {
' ' => return ('·', style.fg(WHITESPACE_FG)),
'\t' => return ('→', style.fg(WHITESPACE_FG)),
_ => {}
}
}
(original, style)
};
let mut spans: Vec<Span<'static>> = Vec::new();
let mut buf = String::new();
let mut buf_style = Style::default();
let mut visual_col = 0usize;
let mut started = false;
let push_cell = |spans: &mut Vec<Span<'static>>,
buf: &mut String,
buf_style: &mut Style,
started: &mut bool,
ch: char,
style: Style| {
if !*started {
*buf_style = style;
*started = true;
} else if style != *buf_style {
if !buf.is_empty() {
spans.push(Span::styled(std::mem::take(buf), *buf_style));
}
*buf_style = style;
}
buf.push(ch);
};
for (col, &original) in chars.iter().enumerate() {
let (ch, style) = cell_at(col);
let width = if original == '\t' {
tab_width - (visual_col % tab_width)
} else {
char_cell_width(original)
};
let cell_start = visual_col;
let cell_end = visual_col + width;
visual_col = cell_end;
if viewport_width > 0 && cell_start >= viewport_right {
break;
}
if cell_end <= col_scroll {
continue;
}
let is_ws = original == ' ' || original == '\t';
let mut displaced_arrow: Option<Style> = None;
for k in 0..width {
let vc = cell_start + k;
if vc < col_scroll {
continue;
}
if viewport_width > 0 && vc >= viewport_right {
break;
}
let jump_lead =
original == '\t' && k == 0 && ch != '\t' && jump_label_at(col).is_some();
let guide = if is_ws && !jump_lead {
guide_at(vc)
} else {
None
};
let (out_ch, out_style) = if let Some(g) = guide {
if original == '\t' && k == 0 && ch != '\t' {
displaced_arrow = Some(style);
}
(g.glyph, style.patch(guide_style(g)))
} else if original == '\t' {
if k == 0 && ch != '\t' {
(ch, style)
} else if let Some(arrow_style) = displaced_arrow.take() {
('→', arrow_style)
} else {
(' ', style)
}
} else if k == 0 {
(ch, style)
} else {
if cell_start >= col_scroll {
continue;
}
(' ', style)
};
push_cell(
&mut spans,
&mut buf,
&mut buf_style,
&mut started,
out_ch,
out_style,
);
}
}
if let Some(m) = max_guide_col
&& visual_col <= m
{
for vc in visual_col.max(col_scroll)..=m {
if viewport_width > 0 && vc >= viewport_right {
break;
}
let (ch, style) = match guide_at(vc) {
Some(g) => (g.glyph, guide_style(g)),
None => (' ', Style::default()),
};
push_cell(
&mut spans,
&mut buf,
&mut buf_style,
&mut started,
ch,
style,
);
}
visual_col = visual_col.max(m + 1);
}
if !buf.is_empty() {
spans.push(Span::styled(buf, buf_style));
}
if is_extra_cursor(chars.len())
&& visual_col >= col_scroll
&& (viewport_width == 0 || visual_col < viewport_right)
{
spans.push(Span::styled(
" ".to_string(),
extra_cursor_style(Style::default()),
));
}
spans
}
pub(super) fn extra_cursor_style(base: Style) -> Style {
base.bg(EXTRA_CURSOR_BG).fg(EXTRA_CURSOR_FG)
}
pub(super) fn build_jump_overlay(state: Option<&JumpState>) -> HashMap<(usize, usize), char> {
let mut out = HashMap::new();
let Some(s) = state else { return out };
match s.typed_first {
None => {
for label in &s.labels {
out.insert((label.pos.row, label.pos.col), label.first);
if let Some(c2) = label.second {
out.insert((label.pos.row, label.pos.col + 1), c2);
}
}
}
Some(first) => {
for label in &s.labels {
if label.first != first {
continue;
}
if let Some(c2) = label.second {
out.insert((label.pos.row, label.pos.col), c2);
}
}
}
}
out
}
pub(super) fn find_matches_in_line(line: &str, query: &str) -> Vec<(usize, usize)> {
if query.is_empty() {
return Vec::new();
}
let q_chars = query.chars().count();
let mut hits = Vec::new();
let mut search_from = 0;
while let Some(byte_idx) = line[search_from..].find(query) {
let abs_byte = search_from + byte_idx;
let start_col = line[..abs_byte].chars().count();
hits.push((start_col, start_col + q_chars));
search_from = abs_byte + query.len();
if search_from >= line.len() {
break;
}
}
hits
}