use crate::core::buffer::{Buffer, Cell};
use crate::core::color::Color;
use crate::core::rect::Rect;
use crate::sanitize;
use crate::scroll_state::ScrollState;
use crate::widgets::Widget;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CodeTheme {
pub bg: Color,
pub gutter_bg: Color,
pub gutter_fg: Color,
pub text_fg: Color,
pub keyword_fg: Color,
pub string_fg: Color,
pub number_fg: Color,
pub comment_fg: Color,
pub punctuation_fg: Color,
pub current_line_bg: Color,
}
impl CodeTheme {
pub const SCRIN: Self = Self {
bg: Color::rgb(12, 17, 27),
gutter_bg: Color::rgb(9, 13, 20),
gutter_fg: Color::rgb(92, 99, 112),
text_fg: Color::rgb(201, 209, 217),
keyword_fg: Color::rgb(255, 123, 114),
string_fg: Color::rgb(165, 214, 255),
number_fg: Color::rgb(121, 192, 255),
comment_fg: Color::rgb(110, 118, 129),
punctuation_fg: Color::rgb(139, 148, 158),
current_line_bg: Color::rgb(20, 28, 43),
};
}
impl Default for CodeTheme {
fn default() -> Self {
Self::SCRIN
}
}
#[derive(Debug, Clone)]
pub struct CodeBlock {
pub code: String,
pub language: Option<String>,
pub theme: CodeTheme,
pub show_line_numbers: bool,
pub start_line: usize,
pub scroll: ScrollState,
pub current_line: Option<usize>,
pub pad_left: u16,
}
impl CodeBlock {
pub fn new(code: &str) -> Self {
Self {
code: code.to_string(),
language: None,
theme: CodeTheme::default(),
show_line_numbers: true,
start_line: 1,
scroll: ScrollState::new(),
current_line: None,
pad_left: 1,
}
}
pub fn with_language(mut self, language: &str) -> Self {
self.language = Some(language.to_string());
self
}
pub fn with_theme(mut self, theme: CodeTheme) -> Self {
self.theme = theme;
self
}
pub fn with_line_numbers(mut self, show: bool) -> Self {
self.show_line_numbers = show;
self
}
pub fn with_start_line(mut self, start: usize) -> Self {
self.start_line = start.max(1);
self
}
pub fn with_scroll(mut self, scroll: ScrollState) -> Self {
self.scroll = scroll;
self
}
pub fn with_current_line(mut self, line: usize) -> Self {
self.current_line = Some(line);
self
}
pub fn line_count(&self) -> usize {
self.code.lines().count().max(1)
}
pub fn gutter_width(&self) -> usize {
if self.show_line_numbers {
(self.start_line + self.line_count()).to_string().len() + 2
} else {
0
}
}
}
impl Widget for CodeBlock {
fn render(&self, buffer: &mut Buffer, area: Rect) {
if area.width == 0 || area.height == 0 {
return;
}
buffer.fill(area, ' ', self.theme.text_fg, Some(self.theme.bg));
let lines: Vec<&str> = if self.code.is_empty() {
vec![""]
} else {
self.code.lines().collect()
};
let mut scroll = self.scroll;
scroll.set_bounds(lines.len(), area.height as usize);
let gutter_width = self.gutter_width().min(area.width as usize);
for (screen_row, line_idx) in scroll.visible_range().enumerate() {
let y = area.y as usize + screen_row;
if y >= area.bottom() as usize {
break;
}
let absolute_line = self.start_line + line_idx;
let line_bg = if self.current_line == Some(absolute_line) {
self.theme.current_line_bg
} else {
self.theme.bg
};
for x in area.x as usize..area.right() as usize {
buffer.set(x, y, Cell::new(' ', self.theme.text_fg, Some(line_bg)));
}
if self.show_line_numbers && gutter_width > 0 {
let gutter = format!(
"{:>width$} │",
absolute_line,
width = gutter_width.saturating_sub(2)
);
let gutter = sanitize::truncate_str(&gutter, gutter_width);
for x in
area.x as usize..(area.x as usize + gutter_width).min(area.right() as usize)
{
buffer.set(
x,
y,
Cell::new(' ', self.theme.gutter_fg, Some(self.theme.gutter_bg)),
);
}
buffer.set_str(
area.x as usize,
y,
&gutter,
self.theme.gutter_fg,
Some(self.theme.gutter_bg),
);
}
let code_x = area.x as usize + gutter_width + self.pad_left as usize;
if code_x < area.right() as usize {
let width = area.right() as usize - code_x;
render_syntax_line(
buffer,
code_x,
y,
lines[line_idx],
width,
self.theme,
Some(line_bg),
);
}
}
}
}
pub(crate) fn render_syntax_line(
buffer: &mut Buffer,
x: usize,
y: usize,
line: &str,
max_width: usize,
theme: CodeTheme,
bg: Option<Color>,
) {
if max_width == 0 {
return;
}
let line = sanitize::sanitize_str(line, max_width);
let trimmed = line.trim_start();
if trimmed.starts_with("//") || trimmed.starts_with('#') || trimmed.starts_with("--") {
buffer.set_str(x, y, &line, theme.comment_fg, bg);
return;
}
let mut col = 0usize;
let mut chars = line.chars().peekable();
while let Some(ch) = chars.next() {
if col >= max_width {
break;
}
if ch == '"' || ch == '\'' || ch == '`' {
let quote = ch;
let mut token = String::new();
token.push(ch);
for next in chars.by_ref() {
token.push(next);
if next == quote {
break;
}
}
write_colored(
buffer,
x,
y,
&token,
&mut col,
max_width,
theme.string_fg,
bg,
);
} else if ch.is_ascii_digit() {
let mut token = String::new();
token.push(ch);
while let Some(next) = chars.peek().copied() {
if next.is_ascii_digit() || next == '_' || next == '.' {
token.push(next);
chars.next();
} else {
break;
}
}
write_colored(
buffer,
x,
y,
&token,
&mut col,
max_width,
theme.number_fg,
bg,
);
} else if is_ident_start(ch) {
let mut token = String::new();
token.push(ch);
while let Some(next) = chars.peek().copied() {
if is_ident_continue(next) {
token.push(next);
chars.next();
} else {
break;
}
}
let fg = if is_keyword(&token) {
theme.keyword_fg
} else {
theme.text_fg
};
write_colored(buffer, x, y, &token, &mut col, max_width, fg, bg);
} else {
let fg = if ch.is_ascii_punctuation() {
theme.punctuation_fg
} else {
theme.text_fg
};
write_colored(buffer, x, y, &ch.to_string(), &mut col, max_width, fg, bg);
}
}
}
fn write_colored(
buffer: &mut Buffer,
x: usize,
y: usize,
text: &str,
col: &mut usize,
max_width: usize,
fg: Color,
bg: Option<Color>,
) {
for ch in text.chars() {
if *col >= max_width {
break;
}
buffer.set(x + *col, y, Cell::new(ch, fg, bg));
*col += 1;
}
}
fn is_ident_start(ch: char) -> bool {
ch == '_' || ch.is_ascii_alphabetic()
}
fn is_ident_continue(ch: char) -> bool {
ch == '_' || ch.is_ascii_alphanumeric()
}
fn is_keyword(token: &str) -> bool {
matches!(
token,
"as" | "async"
| "await"
| "break"
| "case"
| "class"
| "const"
| "continue"
| "crate"
| "default"
| "else"
| "enum"
| "export"
| "false"
| "fn"
| "for"
| "from"
| "function"
| "if"
| "impl"
| "import"
| "in"
| "let"
| "loop"
| "match"
| "mod"
| "mut"
| "pub"
| "return"
| "self"
| "static"
| "struct"
| "super"
| "trait"
| "true"
| "type"
| "use"
| "where"
| "while"
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn code_block_counts_lines() {
let block = CodeBlock::new("fn main() {}\nlet x = 1;");
assert_eq!(block.line_count(), 2);
assert!(block.gutter_width() >= 3);
}
#[test]
fn code_block_renders_backdrop() {
let block = CodeBlock::new("fn main() {\n let x = 1;\n}");
let mut buffer = Buffer::new(40, 5);
block.render(&mut buffer, Rect::new(0, 0, 40, 5));
assert_eq!(
buffer.get(0, 0).unwrap().bg,
Some(CodeTheme::SCRIN.gutter_bg)
);
assert_eq!(buffer.get(8, 0).unwrap().bg, Some(CodeTheme::SCRIN.bg));
}
#[test]
fn code_block_tiny_area_no_panic() {
let block = CodeBlock::new("fn main() {}");
let mut buffer = Buffer::new(2, 1);
block.render(&mut buffer, Rect::new(0, 0, 2, 1));
}
}