use ratatui::{
buffer::Buffer,
layout::{Alignment, Rect},
style::{Color, Modifier, Style},
text::{Line, Span},
widgets::{Block, BorderType, Borders, Paragraph, StatefulWidget, Widget},
};
use super::chunks::{StyledChunk, clip_chunks_window_with_indicator_padded, wrap_chunks_indented};
use super::state::TextAreaSyntaxState;
use crate::data_provider::DataProvider;
use crate::gui_utils::{compute_h_scroll_with_padding, display_cols_up_to, display_width};
use crate::textarea::state::{TextOverflowMode, count_wrapped_rows_indented};
#[derive(Debug, Clone)]
pub struct TextAreaSyntax<'a> {
pub block: Option<Block<'a>>,
pub style: Style,
pub border_type: BorderType,
}
impl<'a> Default for TextAreaSyntax<'a> {
fn default() -> Self {
Self {
block: Some(
Block::default()
.borders(Borders::ALL)
.border_type(BorderType::Rounded),
),
style: Style::default(),
border_type: BorderType::Rounded,
}
}
}
impl<'a> TextAreaSyntax<'a> {
pub fn block(mut self, block: Block<'a>) -> Self {
self.block = Some(block);
self
}
pub fn style(mut self, style: Style) -> Self {
self.style = style;
self
}
pub fn border_type(mut self, ty: BorderType) -> Self {
self.border_type = ty;
if let Some(b) = &mut self.block {
*b = b.clone().border_type(ty);
}
self
}
}
fn resolve_start_line_and_intra_indented(state: &TextAreaSyntaxState, inner: Rect) -> (usize, u16) {
let provider = state.textarea.core.data_provider();
let total = provider.line_count();
if total == 0 {
return (0, 0);
}
let wrap = matches!(state.textarea.overflow_mode, TextOverflowMode::Wrap);
let width = inner.width;
let target_vis = state.textarea.scroll_y;
if !wrap {
let start = (target_vis as usize).min(total);
return (start, 0);
}
let indent = state.textarea.wrap_indent_cols;
let mut acc: u16 = 0;
for i in 0..total {
let s = provider.field_value(i);
let rows = count_wrapped_rows_indented(s, width, indent);
if acc.saturating_add(rows) > target_vis {
let intra = target_vis.saturating_sub(acc);
return (i, intra);
}
acc = acc.saturating_add(rows);
}
(total.saturating_sub(1), 0)
}
fn prepend_line_prefix(mut line: Line<'static>, prefix: String) -> Line<'static> {
if !prefix.is_empty() {
line.spans.insert(0, Span::raw(prefix));
}
line
}
fn search_match_style() -> Style {
Style::default().fg(Color::Black).bg(Color::Yellow)
}
fn active_search_match_style() -> Style {
Style::default()
.fg(Color::Black)
.bg(Color::LightYellow)
.add_modifier(Modifier::BOLD)
}
fn chunks_with_search_highlights(
chunks: &[StyledChunk],
line_idx: usize,
state: &TextAreaSyntaxState,
) -> Vec<StyledChunk> {
let matches = state.textarea.search_matches_in_line(line_idx);
if matches.is_empty() {
return chunks.to_vec();
}
let active = state.textarea.active_search_match();
let search_style = search_match_style();
let active_style = active_search_match_style();
let mut out = Vec::new();
let mut char_idx = 0;
for chunk in chunks {
let mut buf = String::new();
let mut current_style: Option<Style> = None;
for ch in chunk.text.chars() {
let style = if active
.map(|m| m.line == line_idx && char_idx >= m.start && char_idx < m.end)
.unwrap_or(false)
{
active_style
} else if matches
.iter()
.any(|m| char_idx >= m.start && char_idx < m.end)
{
search_style
} else {
chunk.style
};
if current_style == Some(style) {
buf.push(ch);
} else {
if !buf.is_empty() {
out.push(StyledChunk {
text: buf,
style: current_style.unwrap_or(chunk.style),
});
}
buf = ch.to_string();
current_style = Some(style);
}
char_idx += 1;
}
if !buf.is_empty() {
out.push(StyledChunk {
text: buf,
style: current_style.unwrap_or(chunk.style),
});
}
}
out
}
impl<'a> StatefulWidget for TextAreaSyntax<'a> {
type State = TextAreaSyntaxState;
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
state.textarea.ensure_visible(area, self.block.as_ref());
let inner = if let Some(b) = &self.block {
b.clone().render(area, buf);
b.inner(area)
} else {
area
};
let content = state.textarea.content_area(inner);
let edited_now = state.textarea.take_edited_flag();
let wrap_mode = matches!(state.textarea.overflow_mode, TextOverflowMode::Wrap);
let provider = state.textarea.core.data_provider();
let total = provider.line_count();
let (start, intra) = resolve_start_line_and_intra_indented(state, content);
let mut display_lines: Vec<Line> = Vec::new();
if total == 0 || start >= total {
if let Some(ph) = &state.textarea.placeholder {
let mut spans = Vec::new();
let prefix = state.textarea.line_number_prefix(0, true);
if !prefix.is_empty() {
spans.push(Span::raw(prefix));
}
spans.push(Span::raw(ph.clone()));
display_lines.push(Line::from(spans));
}
} else if wrap_mode {
let mut rows_left = content.height;
let indent = state.textarea.wrap_indent_cols;
let mut i = start;
while i < total && rows_left > 0 {
let s = provider.field_value(i);
let chunks = state.engine.highlight_line_cached(i, s, provider);
let chunks = chunks_with_search_highlights(&chunks, i, state);
let lines = wrap_chunks_indented(&chunks, content.width, indent);
let skip = if i == start { intra as usize } else { 0 };
for (line_idx, line) in lines.into_iter().enumerate().skip(skip) {
let prefix = state.textarea.line_number_prefix(i, line_idx == 0);
display_lines.push(prepend_line_prefix(line, prefix));
rows_left = rows_left.saturating_sub(1);
if rows_left == 0 {
break;
}
}
i += 1;
}
} else {
let end = (start.saturating_add(content.height as usize)).min(total);
for i in start..end {
let s = provider.field_value(i);
let chunks = state.engine.highlight_line_cached(i, s, provider);
let chunks = chunks_with_search_highlights(&chunks, i, state);
let fits = display_width(s) <= content.width;
let start_cols = if i == state.textarea.current_field() {
let col_idx = state.textarea.display_cursor_position();
let cursor_cols = display_cols_up_to(s, col_idx);
let (target_h, _left_cols) =
compute_h_scroll_with_padding(cursor_cols, display_width(s), content.width);
if fits {
if edited_now { target_h } else { 0 }
} else {
target_h.max(state.textarea.h_scroll)
}
} else {
0
};
if let TextOverflowMode::Indicator { ch } = state.textarea.overflow_mode {
let prefix = state.textarea.line_number_prefix(i, true);
display_lines.push(prepend_line_prefix(
clip_chunks_window_with_indicator_padded(
&chunks,
content.width,
ch,
start_cols,
),
prefix,
));
}
}
}
let p = Paragraph::new(display_lines)
.alignment(Alignment::Left)
.style(self.style);
p.render(inner, buf);
}
}