use ratatui::Frame;
use ratatui::layout::Rect;
use ratatui::style::{Color, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::Paragraph;
use crate::app::App;
use crate::config::EditorConfig;
use crate::editor::Editor;
use crate::text_width::visual_col_of;
mod diagnostics;
mod indent_guides;
mod render_line;
mod scroll;
use diagnostics::{build_row_diag_summary, build_row_severity, diagnostic_line};
use indent_guides::{GuideMap, IndentGuide, compute_indent_guides};
use render_line::{
bookmark_sign_span, build_jump_overlay, find_matches_in_line, render_line, sign_span,
vcs_bar_span,
};
use scroll::{compute_col_scroll, compute_scroll};
pub(super) fn sel_style() -> Style {
crate::theme::active().ui_selection()
}
pub(super) fn search_style() -> Style {
crate::theme::active().ui_search()
}
pub(super) const EXTRA_CURSOR_BG: Color = Color::Yellow;
pub(super) const EXTRA_CURSOR_FG: Color = Color::Black;
pub(super) const MATCH_BRACKET_FG: Color = Color::Yellow;
pub(super) const JUMP_LABEL_FG: Color = Color::Rgb(255, 100, 200);
pub(super) const JUMP_LABEL_BG: Color = Color::Rgb(40, 0, 40);
pub(super) const WHITESPACE_FG: Color = Color::DarkGray;
pub(super) const INDENT_GUIDE_FG: Color = Color::DarkGray;
pub(super) const INDENT_GUIDE_CHAR: char = '│';
pub(super) const GUTTER_SIGN_WIDTH: u16 = 1;
pub(super) const GUTTER_VCS_WIDTH: u16 = 1;
pub(super) const SCROLL_OFF: usize = 5;
pub(super) fn draw_buffer(f: &mut Frame, app: &App, area: Rect) {
let height = area.height as usize;
let row_diag = build_row_diag_summary(app, app.editor.cursor.row);
let scroll = compute_scroll(app, height, &row_diag);
let sel = app.selection();
let last_visible = scroll + height;
let captures = app
.active_doc()
.highlighter
.as_ref()
.map(|h| h.captures_in_rows(scroll, last_visible))
.unwrap_or_default();
let row_severity = build_row_severity(app, scroll, last_visible);
let bookmark_rows: std::collections::HashSet<usize> = app
.bookmarks
.marks
.iter()
.filter(|m| m.target == app.editor.doc)
.map(|m| m.line)
.collect();
let vcs_statuses = app.active_doc().vcs_statuses();
let cursor_row = app.editor.cursor.row;
let cursor_col = app.editor.cursor.col;
let extras = &app.editor.extra_cursors;
let search_query = &app.search.query;
let jump_overlay = build_jump_overlay(app.jump_state.as_ref());
let bracket_pair: Vec<(usize, usize)> = app
.active_doc()
.highlighter
.as_ref()
.and_then(|h| h.matching_bracket(cursor_row, cursor_col))
.map(|mate| vec![(cursor_row, cursor_col), mate])
.unwrap_or_default();
let eff = app.effective_editor();
let tab_width = eff.tab_width.max(1);
let show_whitespace = eff.show_whitespace;
let indent_guides = if eff.indent_guides {
compute_indent_guides(
app,
scroll,
last_visible,
tab_width,
eff.indent_width,
eff.indent_guides_skip_levels,
eff.indent_guide_style,
eff.indent_animation,
eff.indent_animation_ms,
)
} else {
GuideMap::new()
};
let mut visible: Vec<Line> = Vec::with_capacity(height);
let mut visual_y: u16 = 0;
let mut cursor_visual_y: u16 = 0;
let inner_text_width =
area.width
.saturating_sub(GUTTER_SIGN_WIDTH + 5 + GUTTER_VCS_WIDTH) as usize;
let col_scroll = compute_col_scroll(app, inner_text_width, tab_width);
let theme = crate::theme::active();
for (i, line) in app.active_doc().lines.iter().enumerate().skip(scroll) {
if visual_y as usize >= height {
break;
}
if i == cursor_row {
cursor_visual_y = visual_y;
}
let sev = row_severity.get(&i).copied();
let bookmarked = bookmark_rows.contains(&i);
let sign = match (sev, bookmarked) {
(Some(_), true) => {
let mut s = sign_span(sev);
s.style = s.style.add_modifier(ratatui::style::Modifier::UNDERLINED);
s
}
(Some(_), false) => sign_span(sev),
(None, true) => bookmark_sign_span(),
(None, false) => sign_span(None),
};
let mut spans = vec![sign];
let num = format!("{:>4} ", i + 1);
let num_style = if i == cursor_row {
theme.ui_linenr_selected()
} else {
theme.ui_linenr()
};
spans.push(Span::styled(num, num_style));
let vcs_status = vcs_statuses.get(i).copied().flatten();
spans.push(vcs_bar_span(vcs_status));
let extra_cols: Vec<usize> = extras
.iter()
.filter_map(|c| if c.row == i { Some(c.col) } else { None })
.collect();
let hits = find_matches_in_line(line, search_query);
let row_jumps: Vec<(usize, char)> = jump_overlay
.iter()
.filter_map(|(pos, ch)| if pos.0 == i { Some((pos.1, *ch)) } else { None })
.collect();
let row_bracket_cols: Vec<usize> = bracket_pair
.iter()
.filter_map(|(r, c)| if *r == i { Some(*c) } else { None })
.collect();
let row_guides: &[IndentGuide] = indent_guides.get(&i).map(Vec::as_slice).unwrap_or(&[]);
spans.extend(render_line(
i,
line,
sel.as_ref(),
&captures,
&extra_cols,
&hits,
&row_jumps,
&row_bracket_cols,
row_guides,
tab_width,
col_scroll,
inner_text_width,
show_whitespace,
));
let ghost_continuation: Vec<String> = if i == cursor_row
&& let Some(s) = app.inline_suggestion.showing()
&& s.is_anchored_at(app.editor.cursor)
{
let style = Style::default()
.fg(Color::DarkGray)
.add_modifier(ratatui::style::Modifier::ITALIC);
let mut parts = s.text.split('\n');
if let Some(first) = parts.next()
&& !first.is_empty()
{
spans.push(Span::styled(first.to_string(), style));
}
parts.map(str::to_string).collect()
} else {
Vec::new()
};
visible.push(Line::from(spans));
visual_y += 1;
if visual_y as usize >= height {
break;
}
if !ghost_continuation.is_empty() {
let style = Style::default()
.fg(Color::DarkGray)
.add_modifier(ratatui::style::Modifier::ITALIC);
for cont in ghost_continuation {
visible.push(Line::from(Span::styled(cont, style)));
visual_y += 1;
if visual_y as usize >= height {
break;
}
}
if visual_y as usize >= height {
break;
}
}
if let Some(summary) = row_diag.get(&i) {
visible.push(diagnostic_line(summary, inner_text_width));
visual_y += 1;
}
}
app.active_doc().cursor_visual_y.set(cursor_visual_y);
f.render_widget(with_background(Paragraph::new(visible), &theme), area);
}
fn with_background<'a>(para: Paragraph<'a>, theme: &crate::theme::Theme) -> Paragraph<'a> {
match theme.ui_background() {
Some(style) => para.style(style),
None => para,
}
}
pub(super) fn place_cursor(f: &mut Frame, app: &App, buf_area: Rect) {
if app.prompt.is_open() {
return;
}
let line_no_width: u16 = 5;
let tab_width = app.effective_editor().tab_width.max(1);
let line = &app.active_doc().lines[app.editor.cursor.row];
let visual_col = visual_col_of(line, app.editor.cursor.col, tab_width);
let col_scroll = app.active_doc().col_scroll.get();
let on_screen_col = visual_col.saturating_sub(col_scroll);
let x =
buf_area.x + GUTTER_SIGN_WIDTH + line_no_width + GUTTER_VCS_WIDTH + on_screen_col as u16;
let y = buf_area.y + app.active_doc().cursor_visual_y.get();
f.set_cursor_position((x, y));
}
pub(super) fn draw_buffer_inactive(
f: &mut Frame,
ed: &Editor,
buf: &crate::editor::Buffer,
eff: &EditorConfig,
area: Rect,
) {
let height = area.height as usize;
let cur = ed.cursor.row;
let mut scroll = buf.scroll.get();
let off = if height > 2 * SCROLL_OFF + 1 {
SCROLL_OFF
} else {
0
};
if cur < scroll + off {
scroll = cur.saturating_sub(off);
} else if height > 0 && cur + off >= scroll + height {
scroll = (cur + off + 1).saturating_sub(height);
}
let last_row = buf.lines.len().saturating_sub(1);
scroll = scroll.min(last_row);
buf.scroll.set(scroll);
buf.viewport_height.set(height);
let last_visible = scroll + height;
let captures = buf
.highlighter
.as_ref()
.map(|h| h.captures_in_rows(scroll, last_visible))
.unwrap_or_default();
let vcs_statuses = buf.vcs_statuses();
let tab_width = eff.tab_width.max(1);
let show_whitespace = eff.show_whitespace;
let inner_text_width =
area.width
.saturating_sub(GUTTER_SIGN_WIDTH + 5 + GUTTER_VCS_WIDTH) as usize;
let line = buf.lines.get(cur).map(String::as_str).unwrap_or("");
let visual_col = visual_col_of(line, ed.cursor.col, tab_width);
let mut col_scroll = buf.col_scroll.get();
if inner_text_width > 0 {
if visual_col < col_scroll {
col_scroll = visual_col;
} else if visual_col >= col_scroll + inner_text_width {
col_scroll = visual_col + 1 - inner_text_width;
}
} else {
col_scroll = 0;
}
buf.col_scroll.set(col_scroll);
let mut visible: Vec<Line> = Vec::with_capacity(height);
for (visual_y, (i, line)) in (0_usize..).zip(buf.lines.iter().enumerate().skip(scroll)) {
if visual_y >= height {
break;
}
let mut spans = vec![sign_span(None)];
let num = format!("{:>4} ", i + 1);
spans.push(Span::styled(num, Style::default().fg(Color::DarkGray)));
let vcs_status = vcs_statuses.get(i).copied().flatten();
spans.push(vcs_bar_span(vcs_status));
spans.extend(render_line(
i,
line,
None,
&captures,
&[],
&[],
&[],
&[],
&[],
tab_width,
col_scroll,
inner_text_width,
show_whitespace,
));
visible.push(Line::from(spans));
}
f.render_widget(
with_background(Paragraph::new(visible), &crate::theme::active()),
area,
);
}