use crate::primitives::editor::{
CursorShape, DiagnosticSeverity, DiffLine, Editor, EditorLine, EditorSelection, GitLineStatus,
};
use crate::primitives::scrollbar::Scrollbar;
use crate::text_util::{char_col_to_visual, snap_to_char_boundary};
use crate::theme::Theme;
use crate::types::Color;
use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
use ratatui::style::{Color as RColor, Modifier};
use super::{qc, set_cell, set_cell_styled};
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub struct EditorPaintResult {
pub cursor_position: Option<(u16, u16)>,
}
pub fn draw_editor(
buf: &mut Buffer,
area: Rect,
editor: &Editor,
theme: &Theme,
) -> EditorPaintResult {
draw_editor_with_reserve(buf, area, editor, theme, 0)
}
pub fn draw_editor_with_reserve(
buf: &mut Buffer,
area: Rect,
editor: &Editor,
theme: &Theme,
right_reserve_w: u16,
) -> EditorPaintResult {
let mut result = EditorPaintResult::default();
let buf_area = buf.area;
let window_bg = qc(if editor.show_active_bg {
theme.editor_active_background
} else {
theme.background
});
let default_fg = qc(theme.foreground);
let gutter_w = editor.gutter_char_width as u16;
let viewport_lines = area.height as usize;
let has_scrollbar = editor.total_lines > viewport_lines
&& area.width > gutter_w.saturating_add(right_reserve_w).saturating_add(1);
let viewport_cols = (area.width as usize).saturating_sub(
gutter_w as usize + right_reserve_w as usize + if has_scrollbar { 1 } else { 0 },
);
let has_h_scrollbar = editor.max_col > viewport_cols && area.height > 1;
let text_width = area
.width
.saturating_sub(gutter_w)
.saturating_sub(right_reserve_w)
.saturating_sub(if has_scrollbar { 1 } else { 0 });
let bg_width = area
.width
.saturating_sub(right_reserve_w)
.saturating_sub(if has_scrollbar { 1 } else { 0 });
for row in 0..area.height {
for col in 0..bg_width {
set_cell(buf, area.x + col, area.y + row, ' ', default_fg, window_bg);
}
}
for (row_idx, line) in editor.lines.iter().enumerate() {
let screen_y = area.y + row_idx as u16;
if screen_y >= area.y + area.height {
break;
}
let line_bg = if line.is_dap_current {
qc(theme.dap_stopped_bg)
} else {
match line.diff_status {
Some(DiffLine::Added) => qc(theme.diff_added_bg),
Some(DiffLine::Removed) => qc(theme.diff_removed_bg),
Some(DiffLine::Padding) => qc(theme.diff_padding_bg),
_ if line.is_current_line && editor.is_active && editor.cursorline => {
qc(theme.cursorline_bg)
}
_ => window_bg,
}
};
if line_bg != window_bg {
for col in 0..bg_width {
set_cell(buf, area.x + col, screen_y, ' ', default_fg, line_bg);
}
}
if gutter_w > 0 {
let line_num_fg = qc(if line.is_current_line {
theme.line_number_active_fg
} else {
theme.line_number_fg
});
let bp_offset = if editor.has_breakpoints { 1 } else { 0 };
let git_offset = if editor.has_git_diff {
bp_offset + 1
} else {
bp_offset
};
for (i, ch) in line.gutter_text.chars().enumerate() {
let gx = area.x + i as u16;
if gx >= area.x + gutter_w {
break;
}
let fg = if editor.has_breakpoints && i == 0 {
if line.is_dap_current || line.is_breakpoint {
qc(theme.diagnostic_error)
} else {
line_num_fg
}
} else if editor.has_git_diff && i == bp_offset {
qc(match line.git_diff {
Some(GitLineStatus::Added) => theme.git_added,
Some(GitLineStatus::Modified) => theme.git_modified,
Some(GitLineStatus::Deleted) => theme.git_deleted,
None => theme.line_number_fg,
})
} else {
let _ = git_offset;
line_num_fg
};
set_cell(buf, gx, screen_y, ch, fg, line_bg);
}
let diag_x = area.x + bp_offset as u16;
if let Some(severity) = editor.diagnostic_gutter.get(&line.line_idx) {
let (diag_ch, diag_color) = match severity {
DiagnosticSeverity::Error => ('●', qc(theme.diagnostic_error)),
DiagnosticSeverity::Warning => ('●', qc(theme.diagnostic_warning)),
DiagnosticSeverity::Information => ('●', qc(theme.diagnostic_info)),
DiagnosticSeverity::Hint => ('●', qc(theme.diagnostic_hint)),
};
set_cell(buf, diag_x, screen_y, diag_ch, diag_color, line_bg);
} else if !line.is_wrap_continuation
&& editor.code_action_lines.contains(&line.line_idx)
&& editor.lightbulb_glyph != '\0'
{
set_cell(
buf,
diag_x,
screen_y,
editor.lightbulb_glyph,
qc(theme.lightbulb),
line_bg,
);
}
}
let text_area_x = area.x + gutter_w;
render_text_line(
buf,
text_area_x,
screen_y,
text_width,
line,
editor.scroll_left,
theme,
line_bg,
editor.tabstop,
);
if !line.indent_guides.is_empty() {
let guide_fg = qc(theme.indent_guide_fg);
let active_fg = qc(theme.indent_guide_active_fg);
for &guide_col in &line.indent_guides {
if guide_col < editor.scroll_left {
continue;
}
let vis_col = (guide_col - editor.scroll_left) as u16;
if vis_col >= text_width {
break;
}
let cx = text_area_x + vis_col;
if cx < buf_area.x + buf_area.width && screen_y < buf_area.y + buf_area.height {
let cell = &mut buf[(cx, screen_y)];
if cell.symbol() == " " {
let is_active = editor.active_indent_col == Some(guide_col);
let fg = if is_active { active_fg } else { guide_fg };
cell.set_char('│');
cell.set_fg(fg);
}
}
}
}
if !line.colorcolumns.is_empty() {
let cc_bg = qc(theme.colorcolumn_bg);
for &cc_col in &line.colorcolumns {
if cc_col < editor.scroll_left {
continue;
}
let vis_col = (cc_col - editor.scroll_left) as u16;
if vis_col >= text_width {
break;
}
let cx = text_area_x + vis_col;
if cx < buf_area.x + buf_area.width && screen_y < buf_area.y + buf_area.height {
let cell = &mut buf[(cx, screen_y)];
cell.set_bg(cc_bg);
}
}
}
if line.is_ghost_continuation {
if let Some(ghost) = &line.ghost_suffix {
let ghost_fg = qc(theme.ghost_text_fg);
for (i, ch) in ghost.chars().enumerate() {
let gx = text_area_x + i as u16;
if gx >= text_area_x + text_width {
break;
}
set_cell(buf, gx, screen_y, ch, ghost_fg, line_bg);
}
}
}
for dm in &line.diagnostics {
let diag_fg = qc(match dm.severity {
DiagnosticSeverity::Error => theme.diagnostic_error,
DiagnosticSeverity::Warning => theme.diagnostic_warning,
DiagnosticSeverity::Information => theme.diagnostic_info,
DiagnosticSeverity::Hint => theme.diagnostic_hint,
});
let vis_start = char_col_to_visual(&line.raw_text, dm.start_col, editor.tabstop);
let vis_end = char_col_to_visual(&line.raw_text, dm.end_col, editor.tabstop);
for vcol in vis_start..vis_end {
if vcol < editor.scroll_left {
continue;
}
let vis_col = (vcol - editor.scroll_left) as u16;
if vis_col >= text_width {
break;
}
let cx = text_area_x + vis_col;
if cx < buf_area.x + buf_area.width && screen_y < buf_area.y + buf_area.height {
let cell = &mut buf[(cx, screen_y)];
cell.set_fg(diag_fg);
cell.modifier |= Modifier::UNDERLINED;
cell.underline_color = diag_fg;
}
}
}
let spell_fg = qc(theme.spell_error);
for sm in &line.spell_errors {
let vis_start = char_col_to_visual(&line.raw_text, sm.start_col, editor.tabstop);
let vis_end = char_col_to_visual(&line.raw_text, sm.end_col, editor.tabstop);
for vcol in vis_start..vis_end {
if vcol < editor.scroll_left {
continue;
}
let vis_col = (vcol - editor.scroll_left) as u16;
if vis_col >= text_width {
break;
}
let cx = text_area_x + vis_col;
if cx < buf_area.x + buf_area.width && screen_y < buf_area.y + buf_area.height {
let cell = &mut buf[(cx, screen_y)];
cell.set_fg(spell_fg);
cell.modifier |= Modifier::UNDERLINED;
cell.underline_color = spell_fg;
}
}
}
let bracket_bg = qc(theme.bracket_match_bg);
for &(view_line, col) in &editor.bracket_match_positions {
if view_line == row_idx {
let vis = char_col_to_visual(&line.raw_text, col, editor.tabstop);
if vis < editor.scroll_left {
continue;
}
let vis_col = (vis - editor.scroll_left) as u16;
if vis_col >= text_width {
continue;
}
let cx = text_area_x + vis_col;
if cx < buf_area.x + buf_area.width && screen_y < buf_area.y + buf_area.height {
let cell = &mut buf[(cx, screen_y)];
cell.set_bg(bracket_bg);
}
}
}
}
if let Some(sel) = &editor.selection {
render_selection(buf, area, editor, sel, theme, theme.selection, text_width);
}
for esel in &editor.extra_selections {
render_selection(buf, area, editor, esel, theme, theme.selection, text_width);
}
if let Some(yh) = &editor.yank_highlight {
render_selection(
buf,
area,
editor,
yh,
theme,
theme.yank_highlight_bg,
text_width,
);
}
if has_scrollbar {
let track_h = if has_h_scrollbar {
area.height.saturating_sub(1)
} else {
area.height
};
if track_h > 0 {
let track = crate::event::Rect::new(
(area.x + area.width - 1) as f32,
area.y as f32,
1.0,
track_h as f32,
);
let scrollbar = Scrollbar::vertical(
"tui:editor:v_scrollbar",
track,
editor.scroll_top as f32,
editor.total_lines as f32,
viewport_lines as f32,
1.0,
);
super::draw_scrollbar(buf, &scrollbar, theme, theme.background);
}
}
if has_h_scrollbar && editor.max_col > 0 && viewport_cols > 0 {
let cell_bg = if editor.show_active_bg {
theme.editor_active_background
} else {
theme.background
};
let sb_y = area.y + area.height - 1;
let track_x = area.x + gutter_w;
let track_w = text_width;
if track_w > 0 {
let track = crate::event::Rect::new(track_x as f32, sb_y as f32, track_w as f32, 1.0);
let scrollbar = Scrollbar::horizontal(
"tui:editor:h_scrollbar",
track,
editor.scroll_left as f32,
editor.max_col as f32,
viewport_cols as f32,
1.0,
);
super::draw_scrollbar(buf, &scrollbar, theme, cell_bg);
if has_scrollbar {
set_cell(
buf,
area.x + area.width - 1,
sb_y,
'┘',
qc(theme.separator),
qc(cell_bg),
);
}
}
}
let text_right = area.x + gutter_w + text_width;
if let Some(cursor) = &editor.cursor {
let cursor_screen_y = area.y + cursor.pos.view_line as u16;
let raw = editor
.lines
.get(cursor.pos.view_line)
.map(|l| l.raw_text.as_str())
.unwrap_or("");
let vis_col = char_col_to_visual(raw, cursor.pos.col, editor.tabstop)
.saturating_sub(editor.scroll_left) as u16;
let cursor_screen_x = area.x + gutter_w + vis_col;
let buf_area = buf.area;
match cursor.shape {
CursorShape::Block => {
if cursor_screen_x < text_right.min(buf_area.x + buf_area.width)
&& cursor_screen_y < buf_area.y + buf_area.height
{
let cell = &mut buf[(cursor_screen_x, cursor_screen_y)];
cell.set_bg(qc(theme.cursor)).set_fg(qc(theme.background));
}
}
CursorShape::Bar | CursorShape::Underline => {
result.cursor_position = Some((cursor_screen_x, cursor_screen_y));
}
}
}
if let Some(cursor) = &editor.cursor {
if let Some(rl) = editor.lines.get(cursor.pos.view_line) {
if let Some(ghost) = &rl.ghost_suffix {
let ghost_screen_y = area.y + cursor.pos.view_line as u16;
let vis_col = char_col_to_visual(&rl.raw_text, cursor.pos.col, editor.tabstop)
.saturating_sub(editor.scroll_left) as u16;
let ghost_start_x = area.x + gutter_w + vis_col;
let ghost_fg = qc(theme.ghost_text_fg);
for (i, ch) in ghost.chars().enumerate() {
let gx = ghost_start_x + i as u16;
if gx >= text_right {
break;
}
set_cell(buf, gx, ghost_screen_y, ch, ghost_fg, window_bg);
}
}
}
}
let cursor_color = qc(theme.cursor);
let bg_color = qc(theme.background);
let has_extra_sels = !editor.extra_selections.is_empty();
for extra_pos in &editor.extra_cursors {
let sy = area.y + extra_pos.view_line as u16;
let col = if has_extra_sels {
extra_pos.col + 1
} else {
extra_pos.col
};
let raw = editor
.lines
.get(extra_pos.view_line)
.map(|l| l.raw_text.as_str())
.unwrap_or("");
let vis_col =
char_col_to_visual(raw, col, editor.tabstop).saturating_sub(editor.scroll_left) as u16;
let sx = area.x + gutter_w + vis_col;
let buf_area = buf.area;
if sx < text_right.min(buf_area.x + buf_area.width) && sy < buf_area.y + buf_area.height {
let cell = &mut buf[(sx, sy)];
cell.set_bg(cursor_color).set_fg(bg_color);
}
}
result
}
fn byte_to_char_idx(text: &str, byte_offset: usize) -> usize {
text[..snap_to_char_boundary(text, byte_offset)]
.chars()
.count()
}
#[allow(clippy::too_many_arguments)]
fn render_text_line(
buf: &mut Buffer,
x_start: u16,
y: u16,
max_width: u16,
line: &EditorLine,
scroll_left: usize,
theme: &Theme,
window_bg: RColor,
tabstop: usize,
) {
let raw = &line.raw_text;
let chars: Vec<char> = raw.chars().filter(|&c| c != '\n' && c != '\r').collect();
let mut char_fgs: Vec<Color> = vec![theme.foreground; chars.len()];
let mut char_bgs: Vec<Option<Color>> = vec![None; chars.len()];
let mut char_mods: Vec<Modifier> = vec![Modifier::empty(); chars.len()];
for span in &line.spans {
let start = byte_to_char_idx(raw, span.start_byte);
let end = byte_to_char_idx(raw, span.end_byte).min(chars.len());
for i in start..end {
char_fgs[i] = span.style.fg;
char_bgs[i] = span.style.bg;
let mut m = Modifier::empty();
if span.style.bold {
m |= Modifier::BOLD;
}
if span.style.italic {
m |= Modifier::ITALIC;
}
char_mods[i] = m;
}
}
let tabstop = tabstop.max(1);
let mut vis_col: usize = 0;
let mut cells: Vec<(usize, char, usize)> = Vec::with_capacity(chars.len());
for (i, &ch) in chars.iter().enumerate() {
if ch == '\t' {
let next_stop = ((vis_col / tabstop) + 1) * tabstop;
while vis_col < next_stop {
cells.push((vis_col, ' ', i));
vis_col += 1;
}
} else {
cells.push((vis_col, ch, i));
vis_col += 1;
}
}
let total_vis_cols = vis_col;
for &(vcol, ch, ci) in &cells {
if vcol < scroll_left {
continue;
}
let col = (vcol - scroll_left) as u16;
if col >= max_width {
break;
}
let fg = qc(char_fgs[ci]);
let bg = char_bgs[ci].map(qc).unwrap_or(window_bg);
if char_mods[ci].is_empty() {
set_cell(buf, x_start + col, y, ch, fg, bg);
} else {
set_cell_styled(buf, x_start + col, y, ch, fg, bg, char_mods[ci], None);
}
}
if let Some(ann) = &line.annotation {
let visible_cols = total_vis_cols.saturating_sub(scroll_left);
let ann_start = x_start + visible_cols.min(max_width as usize) as u16;
let ann_fg = qc(theme.annotation_fg);
for (i, ch) in ann.chars().enumerate() {
let col = ann_start + i as u16;
if col >= x_start + max_width {
break;
}
set_cell(buf, col, y, ch, ann_fg, window_bg);
}
}
}
fn render_selection(
buf: &mut Buffer,
area: Rect,
editor: &Editor,
sel: &EditorSelection,
theme: &Theme,
color: Color,
text_width: u16,
) {
let window_bg = qc(if editor.show_active_bg {
theme.editor_active_background
} else {
theme.background
});
let default_fg = qc(theme.foreground);
let sel_bg = qc(color);
let gutter_w = editor.gutter_char_width as u16;
let text_area_x = area.x + gutter_w;
let text_width = text_width as usize;
for (row_idx, line) in editor.lines.iter().enumerate() {
let Some(cols) = sel.cols_on(line) else {
continue;
};
let screen_y = area.y + row_idx as u16;
let vis_start = char_col_to_visual(&line.raw_text, cols.start, editor.tabstop);
let vis_end = char_col_to_visual(&line.raw_text, cols.end, editor.tabstop);
for vis in vis_start..vis_end {
if vis < editor.scroll_left {
continue;
}
let screen_col = (vis - editor.scroll_left) as u16;
if screen_col >= text_width as u16 {
break;
}
let sx = text_area_x + screen_col;
let buf_area = buf.area;
if sx < buf_area.x + buf_area.width && screen_y < buf_area.y + buf_area.height {
let cell = &mut buf[(sx, screen_y)];
let old_fg = cell.fg;
cell.set_bg(sel_bg);
if old_fg == window_bg {
cell.set_fg(default_fg);
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::primitives::editor::{DiagnosticMark, SelectionKind, SpellMark};
use ratatui::buffer::Buffer;
use ratatui::layout::Rect as RtRect;
fn decorated_line(line_idx: usize, col: usize) -> EditorLine {
EditorLine {
raw_text: "x".repeat(col + 2),
gutter_text: String::new(),
spans: vec![],
line_idx,
is_current_line: false,
is_fold_header: false,
folded_line_count: 0,
git_diff: None,
diff_status: None,
diagnostics: vec![DiagnosticMark {
start_col: col,
end_col: col + 1,
severity: DiagnosticSeverity::Error,
message: String::new(),
}],
spell_errors: vec![SpellMark {
start_col: col,
end_col: col + 1,
}],
is_breakpoint: false,
is_conditional_bp: false,
is_dap_current: false,
is_wrap_continuation: false,
segment_col_offset: 0,
annotation: None,
ghost_suffix: None,
is_ghost_continuation: false,
indent_guides: vec![col],
colorcolumns: vec![col],
}
}
fn test_editor(lines: Vec<EditorLine>, bracket_match_positions: Vec<(usize, usize)>) -> Editor {
Editor {
id: crate::types::WidgetId::new("editor"),
rect: crate::event::Rect::new(0.0, 0.0, 20.0, 5.0),
lines,
cursor: None,
extra_cursors: vec![],
selection: None,
extra_selections: vec![],
yank_highlight: None,
scroll_top: 0,
scroll_left: 0,
total_lines: 2,
max_col: 20,
gutter_char_width: 0,
is_active: true,
show_active_bg: false,
has_git_diff: false,
has_breakpoints: false,
diagnostic_gutter: Default::default(),
code_action_lines: Default::default(),
bracket_match_positions,
active_indent_col: None,
tabstop: 4,
cursorline: false,
lightbulb_glyph: '\0',
}
}
#[test]
fn draw_editor_does_not_panic_when_area_outlives_buf() {
let lines = vec![
decorated_line(0, 15), decorated_line(1, 2), ];
let editor = test_editor(lines, vec![(0, 15), (1, 2)]);
let theme = Theme::default();
let mut buf = Buffer::empty(RtRect::new(0, 0, 10, 1));
let area = RtRect::new(0, 0, 20, 5);
let _ = draw_editor(&mut buf, area, &editor, &theme);
}
#[test]
fn draw_editor_right_reserve_w_leaves_strip_untouched_and_keeps_scrollbar_at_edge() {
let lines = vec![sel_test_line(0, "hi", 0, false, None)];
let mut editor = test_editor(lines, vec![]);
editor.total_lines = 100;
let theme = Theme::default();
let area = RtRect::new(0, 0, 20, 5);
let mut buf = Buffer::empty(area);
let right_reserve_w: u16 = 3;
for y in 0..area.height {
for x in 0..area.width {
set_cell(&mut buf, x, y, '?', RColor::Reset, RColor::Reset);
}
}
let _ = draw_editor_with_reserve(&mut buf, area, &editor, &theme, right_reserve_w);
let scrollbar_x = area.x + area.width - 1;
for y in 0..area.height {
let scrollbar_cell = &buf[(scrollbar_x, y)];
assert_ne!(
scrollbar_cell.symbol(),
"?",
"vertical scrollbar must hug the true right edge regardless of right_reserve_w"
);
for x in (scrollbar_x - right_reserve_w)..scrollbar_x {
let cell = &buf[(x, y)];
assert_eq!(
cell.symbol(),
"?",
"column {x} is inside the right_reserve_w strip and must be left untouched"
);
}
}
let painted_cell = &buf[(0, 0)];
assert_ne!(
painted_cell.symbol(),
"?",
"columns left of the reserve strip must still be painted"
);
}
fn sel_test_line(
line_idx: usize,
raw_text: &str,
segment_col_offset: usize,
is_ghost_continuation: bool,
diff_status: Option<DiffLine>,
) -> EditorLine {
EditorLine {
raw_text: raw_text.into(),
gutter_text: String::new(),
spans: vec![],
line_idx,
is_current_line: false,
is_fold_header: false,
folded_line_count: 0,
git_diff: None,
diff_status,
diagnostics: vec![],
spell_errors: vec![],
is_breakpoint: false,
is_conditional_bp: false,
is_dap_current: false,
is_wrap_continuation: segment_col_offset > 0,
segment_col_offset,
annotation: None,
ghost_suffix: is_ghost_continuation.then(|| "ai suggestion".to_string()),
is_ghost_continuation,
indent_guides: vec![],
colorcolumns: vec![],
}
}
#[test]
fn draw_editor_selection_respects_wrapped_ghost_and_diff_padding_rows() {
let lines = vec![
sel_test_line(0, "0123456789", 0, false, None),
sel_test_line(1, "abcde", 5, false, None),
sel_test_line(1, "", 0, true, None),
sel_test_line(2, "", 0, false, Some(DiffLine::Padding)),
];
let mut editor = test_editor(lines, vec![]);
editor.max_col = 0;
editor.total_lines = 4;
editor.selection = Some(EditorSelection {
kind: SelectionKind::Char,
start_line: 0,
start_col: 0,
end_line: 2,
end_col: 10,
});
let theme = Theme::default();
let mut buf = Buffer::empty(RtRect::new(0, 0, 10, 4));
let area = RtRect::new(0, 0, 10, 4);
let _ = draw_editor(&mut buf, area, &editor, &theme);
let sel_bg = qc(theme.selection);
let window_bg = qc(theme.background);
let diff_padding_bg = qc(theme.diff_padding_bg);
assert_eq!(
buf[(0, 0)].bg,
sel_bg,
"row 0 (plain, selection's start line) should be tinted"
);
assert_eq!(
buf[(0, 1)].bg,
sel_bg,
"row 1 (wrapped segment of a fully-selected line) should be tinted"
);
assert_eq!(
buf[(0, 2)].bg,
window_bg,
"row 2 (ghost continuation) must never be tinted by a selection"
);
assert_eq!(
buf[(0, 3)].bg,
diff_padding_bg,
"row 3 (diff padding) must show only its diff background, not a selection tint"
);
}
#[test]
fn draw_editor_with_reserve_zero_matches_draw_editor() {
let lines = vec![sel_test_line(0, "0123456789", 0, false, None)];
let mut editor = test_editor(lines, vec![]);
editor.total_lines = 10;
editor.max_col = 20;
let theme = Theme::default();
let area = RtRect::new(0, 0, 10, 4);
let mut buf_a = Buffer::empty(area);
let mut buf_b = Buffer::empty(area);
let result_a = draw_editor(&mut buf_a, area, &editor, &theme);
let result_b = draw_editor_with_reserve(&mut buf_b, area, &editor, &theme, 0);
assert_eq!(result_a, result_b);
for y in 0..area.height {
for x in 0..area.width {
let a = &buf_a[(x, y)];
let b = &buf_b[(x, y)];
assert_eq!(a.symbol(), b.symbol(), "symbol diverged at ({x}, {y})");
assert_eq!(a.fg, b.fg, "fg diverged at ({x}, {y})");
assert_eq!(a.bg, b.bg, "bg diverged at ({x}, {y})");
}
}
}
#[test]
fn draw_editor_with_reserve_keeps_scrollbar_at_area_right_edge() {
let mut line = sel_test_line(0, "0123456789ABCDEF", 0, false, None);
line.is_current_line = true;
let mut editor = test_editor(vec![line], vec![]);
editor.cursorline = true;
editor.total_lines = 10; editor.max_col = 0;
let theme = Theme::default();
let area = RtRect::new(0, 0, 10, 4);
let mut buf = Buffer::empty(area);
for y in 0..area.height {
for x in 0..area.width {
set_cell(&mut buf, x, y, '?', RColor::Reset, RColor::Reset);
}
}
let right_reserve_w = 2;
let _ = draw_editor_with_reserve(&mut buf, area, &editor, &theme, right_reserve_w);
let cursorline_bg = qc(theme.cursorline_bg);
assert_eq!(
buf[(6, 0)].bg,
cursorline_bg,
"column 6 is inside the narrowed cursorline highlight"
);
for x in 7..9 {
let cell = &buf[(x, 0)];
assert_eq!(
cell.symbol(),
"?",
"column {x} is inside the reserved strip: no glyph may spill into it"
);
assert_eq!(
cell.bg,
RColor::Reset,
"column {x} is inside the reserved strip: the cursorline tint must stop short of it"
);
}
assert_ne!(
buf[(9, 0)].symbol(),
"?",
"scrollbar track at area's right edge (column 9) must be painted"
);
}
}