use std::path::Path;
use std::sync::LazyLock;
use syntect::easy::HighlightLines;
use syntect::highlighting::{Style as SyntectStyle, ThemeSet};
use syntect::parsing::SyntaxSet;
use crate::cells::cell_len;
use crate::color::Color;
use crate::console::{Console, ConsoleOptions, Renderable};
use crate::measure::Measurement;
use crate::segment::Segment;
use crate::style::Style;
use crate::text::Text;
static SYNTAX_SET: LazyLock<SyntaxSet> = LazyLock::new(SyntaxSet::load_defaults_newlines);
static THEME_SET: LazyLock<ThemeSet> = LazyLock::new(ThemeSet::load_defaults);
const DEFAULT_THEME: &str = "base16-ocean.dark";
const NUMBERS_COLUMN_DEFAULT_PADDING: usize = 2;
#[derive(Debug)]
pub enum SyntaxError {
IoError(std::io::Error),
UnknownLanguage(String),
}
impl std::fmt::Display for SyntaxError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
SyntaxError::IoError(e) => write!(f, "IO error: {}", e),
SyntaxError::UnknownLanguage(name) => {
write!(f, "unknown language: {}", name)
}
}
}
}
impl std::error::Error for SyntaxError {}
impl From<std::io::Error> for SyntaxError {
fn from(e: std::io::Error) -> Self {
SyntaxError::IoError(e)
}
}
pub struct Syntax {
pub code: String,
pub lexer_name: String,
pub theme: String,
pub line_numbers: bool,
pub start_line: usize,
pub line_range: Option<(usize, usize)>,
pub word_wrap: bool,
pub tab_size: usize,
pub padding: (usize, usize),
pub highlight_lines: Vec<usize>,
pub background_color: Option<String>,
pub indent_guides: bool,
pub code_width: Option<usize>,
pub dedent: bool,
pub style_ranges: Vec<(Style, std::ops::Range<usize>)>,
}
impl Syntax {
pub fn new(code: &str, lexer_name: &str) -> Self {
Syntax {
code: code.to_string(),
lexer_name: lexer_name.to_string(),
theme: DEFAULT_THEME.to_string(),
line_numbers: false,
start_line: 1,
line_range: None,
word_wrap: false,
tab_size: 4,
padding: (0, 0),
highlight_lines: Vec::new(),
background_color: None,
indent_guides: false,
code_width: None,
dedent: false,
style_ranges: Vec::new(),
}
}
pub fn from_path(path: &str) -> Result<Self, SyntaxError> {
let code = std::fs::read_to_string(path)?;
let lexer_name = guess_lexer(path);
Ok(Self::new(&code, &lexer_name))
}
#[must_use]
pub fn with_theme(mut self, theme: &str) -> Self {
self.theme = theme.to_string();
self
}
#[must_use]
pub fn with_line_numbers(mut self, line_numbers: bool) -> Self {
self.line_numbers = line_numbers;
self
}
#[must_use]
pub fn with_start_line(mut self, start: usize) -> Self {
self.start_line = start;
self
}
#[must_use]
pub fn with_line_range(mut self, range: (usize, usize)) -> Self {
self.line_range = Some(range);
self
}
#[must_use]
pub fn with_word_wrap(mut self, wrap: bool) -> Self {
self.word_wrap = wrap;
self
}
#[must_use]
pub fn with_tab_size(mut self, size: usize) -> Self {
self.tab_size = size;
self
}
#[must_use]
pub fn with_highlight_lines(mut self, lines: Vec<usize>) -> Self {
self.highlight_lines = lines;
self
}
#[must_use]
pub fn with_indent_guides(mut self, guides: bool) -> Self {
self.indent_guides = guides;
self
}
#[must_use]
pub fn with_code_width(mut self, width: usize) -> Self {
self.code_width = Some(width);
self
}
#[must_use]
pub fn with_dedent(mut self, dedent: bool) -> Self {
self.dedent = dedent;
self
}
pub fn stylize_range(&mut self, style: Style, range: std::ops::Range<usize>) {
self.style_ranges.push((style, range));
}
fn numbers_column_width(&self) -> usize {
if !self.line_numbers {
return 0;
}
let last_line = self.start_line + self.code.lines().count().saturating_sub(1);
let digits = format!("{}", last_line).len();
digits + NUMBERS_COLUMN_DEFAULT_PADDING
}
fn process_code(&self) -> (bool, String) {
let ends_on_nl = self.code.ends_with('\n');
let mut processed = if ends_on_nl {
self.code.clone()
} else {
format!("{}\n", self.code)
};
let tab_replacement: String = " ".repeat(self.tab_size);
processed = processed.replace('\t', &tab_replacement);
if self.dedent {
let min_indent = processed
.lines()
.filter(|line| !line.trim().is_empty())
.map(|line| line.len() - line.trim_start().len())
.min()
.unwrap_or(0);
if min_indent > 0 {
processed = processed
.lines()
.map(|line| {
if line.len() >= min_indent {
&line[min_indent..]
} else {
line
}
})
.collect::<Vec<_>>()
.join("\n");
if !processed.ends_with('\n') {
processed.push('\n');
}
}
}
(ends_on_nl, processed)
}
fn highlight_code(&self, code: &str) -> Text {
let ss = &*SYNTAX_SET;
let ts = &*THEME_SET;
let syntax = ss
.find_syntax_by_token(&self.lexer_name)
.or_else(|| ss.find_syntax_by_extension(&self.lexer_name))
.unwrap_or_else(|| ss.find_syntax_plain_text());
let theme = ts
.themes
.get(&self.theme)
.or_else(|| ts.themes.values().next())
.expect("at least one theme must be available");
let mut h = HighlightLines::new(syntax, theme);
let mut text = Text::new("", Style::null());
for line in code.lines() {
let line_with_nl = format!("{}\n", line);
match h.highlight_line(&line_with_nl, ss) {
Ok(ranges) => {
for (style, token) in ranges {
let gilt_style = syntect_to_gilt_style(style);
text.append_str(token, Some(gilt_style));
}
}
Err(_) => {
text.append_str(&line_with_nl, None);
}
}
}
text
}
fn get_background_style(&self) -> Style {
if let Some(ref bg) = self.background_color {
if let Ok(color) = Color::parse(bg) {
return Style::from_color(None, Some(color));
}
}
let ts = &*THEME_SET;
if let Some(theme) = ts.themes.get(&self.theme) {
let bg = theme
.settings
.background
.unwrap_or(syntect::highlighting::Color {
r: 0,
g: 0,
b: 0,
a: 255,
});
Style::from_color(None, Some(Color::from_rgb(bg.r, bg.g, bg.b)))
} else {
Style::null()
}
}
fn render_syntax(&self, max_width: usize) -> Vec<Segment> {
let (ends_on_nl, processed_code) = self.process_code();
let mut text = self.highlight_code(&processed_code);
for (style, range) in &self.style_ranges {
text.stylize(style.clone(), range.start, Some(range.end));
}
if !ends_on_nl {
text.remove_suffix("\n");
}
let numbers_column_width = self.numbers_column_width();
let code_width = if let Some(cw) = self.code_width {
cw
} else if self.line_numbers {
max_width
.saturating_sub(numbers_column_width)
.saturating_sub(1)
} else {
max_width
};
let background_style = self.get_background_style();
let lines = text.split("\n", true, true);
let all_lines: Vec<&crate::text::Text> = lines.iter().collect();
let (display_lines, line_offset): (Vec<&crate::text::Text>, usize) =
if let Some((start, end)) = self.line_range {
let offset = start.saturating_sub(1);
let end_idx = end.min(all_lines.len());
if offset >= all_lines.len() {
(Vec::new(), offset)
} else {
(all_lines[offset..end_idx].to_vec(), offset)
}
} else {
(all_lines.clone(), 0)
};
let mut segments: Vec<Segment> = Vec::new();
for _ in 0..self.padding.0 {
if self.line_numbers {
let pad = " ".repeat(numbers_column_width + 1);
segments.push(Segment::styled(&pad, background_style.clone()));
}
let line_pad = " ".repeat(code_width);
segments.push(Segment::styled(&line_pad, background_style.clone()));
segments.push(Segment::line());
}
for (idx, line) in display_lines.iter().enumerate() {
let line_no = self.start_line + line_offset + idx;
let is_highlighted = self.highlight_lines.contains(&line_no);
if self.line_numbers {
let num_width = numbers_column_width - NUMBERS_COLUMN_DEFAULT_PADDING;
let num_str = format!("{:>width$} ", line_no, width = num_width);
if is_highlighted {
let pointer_style = Style::from_color(
Some(Color::parse("red").unwrap_or_else(|_| Color::from_rgb(255, 0, 0))),
None,
);
segments.push(Segment::styled("> ", pointer_style));
segments.push(Segment::styled(&num_str, background_style.clone()));
} else {
let dim_style = Style::new(
None,
None,
None,
Some(true),
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
)
.unwrap_or_else(|_| Style::null());
segments.push(Segment::styled(" ", background_style.clone()));
segments.push(Segment::styled(
&num_str,
background_style.clone() + dim_style,
));
}
}
let line_text = line.plain();
let line_cell_len = cell_len(line_text);
if self.word_wrap && line_cell_len > code_width {
let wrapped = line.wrap(code_width, None, None, self.tab_size, false);
for (wi, wline) in wrapped.iter().enumerate() {
if wi > 0 && self.line_numbers {
let gutter_pad = " ".repeat(numbers_column_width + 1);
segments.push(Segment::styled(&gutter_pad, background_style.clone()));
}
let rendered = wline.render();
for seg in &rendered {
if seg.text == "\n" {
continue;
}
let style = seg.style.clone().unwrap_or_else(Style::null);
segments.push(Segment::styled(&seg.text, background_style.clone() + style));
}
let wline_len = wline.cell_len();
if wline_len < code_width {
let pad = " ".repeat(code_width - wline_len);
segments.push(Segment::styled(&pad, background_style.clone()));
}
segments.push(Segment::line());
}
} else {
let rendered = line.render();
for seg in &rendered {
if seg.text == "\n" {
continue;
}
let style = seg.style.clone().unwrap_or_else(Style::null);
segments.push(Segment::styled(&seg.text, background_style.clone() + style));
}
if line_cell_len < code_width {
let pad = " ".repeat(code_width - line_cell_len);
segments.push(Segment::styled(&pad, background_style.clone()));
}
segments.push(Segment::line());
}
}
for _ in 0..self.padding.1 {
if self.line_numbers {
let pad = " ".repeat(numbers_column_width + 1);
segments.push(Segment::styled(&pad, background_style.clone()));
}
let line_pad = " ".repeat(code_width);
segments.push(Segment::styled(&line_pad, background_style.clone()));
segments.push(Segment::line());
}
segments
}
pub fn measure(&self) -> Measurement {
let numbers_width = self.numbers_column_width();
if let Some(cw) = self.code_width {
let total = cw + numbers_width + if self.line_numbers { 1 } else { 0 };
return Measurement::new(numbers_width, total);
}
let (_, processed) = self.process_code();
let max_line_width = processed.lines().map(cell_len).max().unwrap_or(0);
let total = numbers_width + max_line_width + if self.line_numbers { 1 } else { 0 };
Measurement::new(numbers_width, total)
}
}
impl Renderable for Syntax {
fn rich_console(&self, _console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
self.render_syntax(options.max_width)
}
}
fn syntect_to_gilt_style(style: SyntectStyle) -> Style {
let fg = style.foreground;
Style::from_color(Some(Color::from_rgb(fg.r, fg.g, fg.b)), None)
}
fn guess_lexer(path: &str) -> String {
let p = Path::new(path);
if let Some(ext) = p.extension() {
let ext_str = ext.to_string_lossy().to_lowercase();
let ss = &*SYNTAX_SET;
if let Some(syn) = ss.find_syntax_by_extension(&ext_str) {
return syn.name.to_lowercase();
}
return ext_str;
}
"txt".to_string()
}
impl std::fmt::Display for Syntax {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut console = Console::builder()
.width(f.width().unwrap_or(80))
.force_terminal(true)
.no_color(true)
.build();
console.begin_capture();
console.print(self);
let output = console.end_capture();
write!(f, "{}", output.trim_end_matches('\n'))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_basic_rust_highlighting() {
let code = "fn main() {\n println!(\"Hello\");\n}\n";
let syntax = Syntax::new(code, "rs");
let segments = syntax.render_syntax(80);
let text: String = segments.iter().map(|s| s.text.as_str()).collect();
assert!(text.contains("fn"));
assert!(text.contains("main"));
assert!(text.contains("println"));
}
#[test]
fn test_python_highlighting() {
let code = "def hello():\n print(\"Hello\")\n";
let syntax = Syntax::new(code, "py");
let segments = syntax.render_syntax(80);
let text: String = segments.iter().map(|s| s.text.as_str()).collect();
assert!(text.contains("def"));
assert!(text.contains("hello"));
}
#[test]
fn test_json_highlighting() {
let code = "{\"key\": \"value\", \"num\": 42}\n";
let syntax = Syntax::new(code, "json");
let segments = syntax.render_syntax(80);
let text: String = segments.iter().map(|s| s.text.as_str()).collect();
assert!(text.contains("key"));
assert!(text.contains("value"));
assert!(text.contains("42"));
}
#[test]
fn test_line_numbers_enabled() {
let code = "line one\nline two\nline three\n";
let syntax = Syntax::new(code, "txt").with_line_numbers(true);
let segments = syntax.render_syntax(80);
let text: String = segments.iter().map(|s| s.text.as_str()).collect();
assert!(text.contains("1"));
assert!(text.contains("2"));
assert!(text.contains("3"));
}
#[test]
fn test_line_numbers_disabled() {
let code = "line one\nline two\n";
let syntax = Syntax::new(code, "txt");
let segments = syntax.render_syntax(80);
let text: String = segments.iter().map(|s| s.text.as_str()).collect();
assert!(text.contains("line one"));
}
#[test]
fn test_start_line_offset() {
let code = "alpha\nbeta\ngamma\n";
let syntax = Syntax::new(code, "txt")
.with_line_numbers(true)
.with_start_line(10);
let segments = syntax.render_syntax(80);
let text: String = segments.iter().map(|s| s.text.as_str()).collect();
assert!(text.contains("10"));
assert!(text.contains("11"));
assert!(text.contains("12"));
}
#[test]
fn test_line_range() {
let code = "line1\nline2\nline3\nline4\nline5\n";
let syntax = Syntax::new(code, "txt").with_line_range((2, 4));
let segments = syntax.render_syntax(80);
let text: String = segments.iter().map(|s| s.text.as_str()).collect();
assert!(text.contains("line2"));
assert!(text.contains("line3"));
assert!(text.contains("line4"));
assert!(!text.contains("line1\n")); assert!(!text.contains("line5"));
}
#[test]
fn test_word_wrap() {
let code = "this is a very long line that should be wrapped when word wrap is enabled\n";
let syntax = Syntax::new(code, "txt").with_word_wrap(true);
let segments = syntax.render_syntax(30);
let text: String = segments.iter().map(|s| s.text.as_str()).collect();
let newline_count = text.matches('\n').count();
assert!(
newline_count > 1,
"expected wrapping, got {} newlines",
newline_count
);
}
#[test]
fn test_tab_expansion() {
let code = "if true:\n\tpass\n";
let syntax = Syntax::new(code, "py").with_tab_size(4);
let (_, processed) = syntax.process_code();
assert!(!processed.contains('\t'));
assert!(processed.contains(" pass"));
}
#[test]
fn test_tab_expansion_custom_size() {
let code = "\thello\n";
let syntax = Syntax::new(code, "txt").with_tab_size(8);
let (_, processed) = syntax.process_code();
assert!(processed.contains(" hello"));
}
#[test]
fn test_theme_base16_ocean_dark() {
let code = "let x = 1;\n";
let syntax = Syntax::new(code, "rs").with_theme("base16-ocean.dark");
let segments = syntax.render_syntax(80);
assert!(!segments.is_empty());
}
#[test]
fn test_theme_base16_eighties_dark() {
let code = "let x = 1;\n";
let syntax = Syntax::new(code, "rs").with_theme("base16-eighties.dark");
let segments = syntax.render_syntax(80);
assert!(!segments.is_empty());
}
#[test]
fn test_unknown_theme_fallback() {
let code = "hello\n";
let syntax = Syntax::new(code, "txt").with_theme("nonexistent-theme-xyz");
let segments = syntax.render_syntax(80);
assert!(!segments.is_empty());
}
#[test]
fn test_highlight_lines() {
let code = "a\nb\nc\n";
let syntax = Syntax::new(code, "txt")
.with_line_numbers(true)
.with_highlight_lines(vec![2]);
let segments = syntax.render_syntax(80);
let text: String = segments.iter().map(|s| s.text.as_str()).collect();
assert!(text.contains('>'), "expected highlight pointer");
}
#[test]
fn test_unknown_language_fallback() {
let code = "some random text\n";
let syntax = Syntax::new(code, "zzzz_nonexistent");
let segments = syntax.render_syntax(80);
let text: String = segments.iter().map(|s| s.text.as_str()).collect();
assert!(text.contains("some random text"));
}
#[test]
fn test_builder_pattern() {
let syntax = Syntax::new("code", "rs")
.with_theme("base16-ocean.dark")
.with_line_numbers(true)
.with_start_line(5)
.with_line_range((1, 10))
.with_word_wrap(true)
.with_tab_size(2)
.with_highlight_lines(vec![1, 2, 3])
.with_indent_guides(true)
.with_code_width(60)
.with_dedent(true);
assert_eq!(syntax.theme, "base16-ocean.dark");
assert!(syntax.line_numbers);
assert_eq!(syntax.start_line, 5);
assert_eq!(syntax.line_range, Some((1, 10)));
assert!(syntax.word_wrap);
assert_eq!(syntax.tab_size, 2);
assert_eq!(syntax.highlight_lines, vec![1, 2, 3]);
assert!(syntax.indent_guides);
assert_eq!(syntax.code_width, Some(60));
assert!(syntax.dedent);
}
#[test]
fn test_renderable_trait() {
let syntax = Syntax::new("fn main() {}\n", "rs");
let console = Console::builder().width(80).build();
let options = console.options();
let segments = syntax.rich_console(&console, &options);
assert!(!segments.is_empty());
let text: String = segments.iter().map(|s| s.text.as_str()).collect();
assert!(text.contains("fn"));
}
#[test]
fn test_measure_no_line_numbers() {
let code = "hello world\n";
let syntax = Syntax::new(code, "txt");
let m = syntax.measure();
assert_eq!(m.minimum, 0); assert!(m.maximum >= 11); }
#[test]
fn test_measure_with_line_numbers() {
let code = "a\nb\nc\n";
let syntax = Syntax::new(code, "txt").with_line_numbers(true);
let m = syntax.measure();
assert!(m.minimum > 0); assert!(m.maximum > m.minimum);
}
#[test]
fn test_measure_with_code_width() {
let code = "hello\n";
let syntax = Syntax::new(code, "txt").with_code_width(40);
let m = syntax.measure();
assert_eq!(m.maximum, 40);
}
#[test]
fn test_from_path_nonexistent() {
let result = Syntax::from_path("/nonexistent/file/path.rs");
assert!(result.is_err());
}
#[test]
fn test_from_path_reads_self() {
let path = file!();
let full_path = format!("/mnt/data/Velocity/rusty_rich/gilt/{}", path);
if std::path::Path::new(&full_path).exists() {
let result = Syntax::from_path(&full_path);
assert!(result.is_ok());
let syntax = result.unwrap();
assert!(syntax.code.contains("fn test_from_path_reads_self"));
}
}
#[test]
fn test_empty_code() {
let syntax = Syntax::new("", "txt");
let segments = syntax.render_syntax(80);
let text: String = segments.iter().map(|s| s.text.as_str()).collect();
assert!(!text.is_empty() || segments.is_empty());
}
#[test]
fn test_single_line_code() {
let syntax = Syntax::new("hello", "txt");
let segments = syntax.render_syntax(80);
let text: String = segments.iter().map(|s| s.text.as_str()).collect();
assert!(text.contains("hello"));
}
#[test]
fn test_code_with_special_characters() {
let code = "let x = \"hello <world> & 'friends'\";\n";
let syntax = Syntax::new(code, "rs");
let segments = syntax.render_syntax(80);
let text: String = segments.iter().map(|s| s.text.as_str()).collect();
assert!(text.contains('<'));
assert!(text.contains('>'));
assert!(text.contains('&'));
assert!(text.contains('\''));
}
#[test]
fn test_syntect_to_gilt_style_conversion() {
let style = SyntectStyle {
foreground: syntect::highlighting::Color {
r: 255,
g: 128,
b: 0,
a: 255,
},
background: syntect::highlighting::Color {
r: 0,
g: 0,
b: 0,
a: 255,
},
font_style: syntect::highlighting::FontStyle::empty(),
};
let gilt_style = syntect_to_gilt_style(style);
let color = gilt_style.color().expect("should have foreground color");
assert_eq!(color.triplet.unwrap().red, 255);
assert_eq!(color.triplet.unwrap().green, 128);
assert_eq!(color.triplet.unwrap().blue, 0);
}
#[test]
fn test_guess_lexer_rust() {
let name = guess_lexer("foo.rs");
assert!(!name.is_empty());
}
#[test]
fn test_guess_lexer_python() {
let name = guess_lexer("script.py");
assert!(!name.is_empty());
}
#[test]
fn test_guess_lexer_json() {
let name = guess_lexer("data.json");
assert!(!name.is_empty());
}
#[test]
fn test_guess_lexer_no_extension() {
let name = guess_lexer("Makefile");
assert!(!name.is_empty());
}
#[test]
fn test_numbers_column_width_disabled() {
let syntax = Syntax::new("a\nb\nc\n", "txt");
assert_eq!(syntax.numbers_column_width(), 0);
}
#[test]
fn test_numbers_column_width_single_digit() {
let syntax = Syntax::new("a\nb\nc\n", "txt").with_line_numbers(true);
assert_eq!(syntax.numbers_column_width(), 3);
}
#[test]
fn test_numbers_column_width_double_digit() {
let mut code = String::new();
for i in 1..=15 {
code.push_str(&format!("line {}\n", i));
}
let syntax = Syntax::new(&code, "txt").with_line_numbers(true);
assert_eq!(syntax.numbers_column_width(), 4);
}
#[test]
fn test_process_code_adds_trailing_newline() {
let syntax = Syntax::new("hello", "txt");
let (ends_on_nl, processed) = syntax.process_code();
assert!(!ends_on_nl);
assert!(processed.ends_with('\n'));
}
#[test]
fn test_process_code_preserves_trailing_newline() {
let syntax = Syntax::new("hello\n", "txt");
let (ends_on_nl, processed) = syntax.process_code();
assert!(ends_on_nl);
assert!(processed.ends_with('\n'));
}
#[test]
fn test_get_background_style_default() {
let syntax = Syntax::new("code", "txt");
let style = syntax.get_background_style();
assert!(style.bgcolor().is_some() || style.is_null());
}
#[test]
fn test_get_background_style_override() {
let mut syntax = Syntax::new("code", "txt");
syntax.background_color = Some("#ff0000".to_string());
let style = syntax.get_background_style();
assert!(style.bgcolor().is_some());
}
#[test]
fn test_padding_top_bottom() {
let mut syntax = Syntax::new("hello\n", "txt");
syntax.padding = (1, 1);
let segments = syntax.render_syntax(40);
let newline_count = segments.iter().filter(|s| s.text == "\n").count();
assert!(
newline_count >= 3,
"expected at least 3 newlines, got {}",
newline_count
);
}
#[test]
fn test_line_range_out_of_bounds() {
let code = "a\nb\n";
let syntax = Syntax::new(code, "txt").with_line_range((10, 20));
let segments = syntax.render_syntax(80);
let text: String = segments
.iter()
.filter(|s| s.text != "\n" && s.text.trim() != "")
.map(|s| s.text.as_str())
.collect();
assert!(text.is_empty() || text.chars().all(|c| c == ' '));
}
#[test]
fn test_segments_have_styles() {
let code = "fn main() {}\n";
let syntax = Syntax::new(code, "rs");
let segments = syntax.render_syntax(80);
let styled_count = segments.iter().filter(|s| s.style.is_some()).count();
assert!(styled_count > 0, "expected some styled segments");
}
#[test]
fn test_code_width_constraint() {
let code = "a very long line of code that goes on and on and on\n";
let syntax = Syntax::new(code, "txt").with_code_width(20);
let m = syntax.measure();
assert_eq!(m.maximum, 20);
}
#[test]
fn test_different_themes_produce_output() {
let code = "let x = 42;\n";
let ts = &*THEME_SET;
for theme_name in ts.themes.keys() {
let syntax = Syntax::new(code, "rs").with_theme(theme_name);
let segments = syntax.render_syntax(80);
assert!(
!segments.is_empty(),
"theme '{}' produced no output",
theme_name
);
}
}
#[test]
fn test_default_values() {
let syntax = Syntax::new("code", "rs");
assert_eq!(syntax.theme, DEFAULT_THEME);
assert!(!syntax.line_numbers);
assert_eq!(syntax.start_line, 1);
assert!(syntax.line_range.is_none());
assert!(!syntax.word_wrap);
assert_eq!(syntax.tab_size, 4);
assert_eq!(syntax.padding, (0, 0));
assert!(syntax.highlight_lines.is_empty());
assert!(syntax.background_color.is_none());
assert!(!syntax.indent_guides);
assert!(syntax.code_width.is_none());
assert!(!syntax.dedent);
assert!(syntax.style_ranges.is_empty());
}
#[test]
fn test_dedent_strips_common_whitespace() {
let code = " fn main() {\n println!(\"hi\");\n }\n";
let syntax = Syntax::new(code, "rs").with_dedent(true);
let (_, processed) = syntax.process_code();
assert!(
processed.starts_with("fn main()"),
"expected dedented code, got: {:?}",
processed
);
}
#[test]
fn test_dedent_preserves_relative_indent() {
let code = " fn main() {\n println!(\"hi\");\n }\n";
let syntax = Syntax::new(code, "rs").with_dedent(true);
let (_, processed) = syntax.process_code();
let lines: Vec<&str> = processed.lines().collect();
assert!(lines[0].starts_with("fn main()"));
assert!(
lines[1].starts_with(" println"),
"expected 4-space relative indent, got: {:?}",
lines[1]
);
assert_eq!(lines[2].trim(), "}");
}
#[test]
fn test_dedent_false_preserves_whitespace() {
let code = " indented\n";
let syntax = Syntax::new(code, "txt");
let (_, processed) = syntax.process_code();
assert!(
processed.starts_with(" indented"),
"expected original indent preserved, got: {:?}",
processed
);
}
#[test]
fn test_stylize_range_stores() {
let mut syntax = Syntax::new("hello world", "txt");
let style = Style::from_color(
Some(Color::parse("red").unwrap_or_else(|_| Color::from_rgb(255, 0, 0))),
None,
);
syntax.stylize_range(style.clone(), 0..5);
assert_eq!(syntax.style_ranges.len(), 1);
assert_eq!(syntax.style_ranges[0].1, 0..5);
}
#[test]
fn test_stylize_range_applied() {
let mut syntax = Syntax::new("hello world\n", "txt");
let red_style = Style::from_color(
Some(Color::parse("red").unwrap_or_else(|_| Color::from_rgb(255, 0, 0))),
None,
);
syntax.stylize_range(red_style, 0..5);
let segments = syntax.render_syntax(80);
let mut found_styled = false;
let mut pos = 0;
for seg in &segments {
if seg.text == "\n" {
continue;
}
let end = pos + seg.text.len();
if pos < 5 && end > 0 && !seg.text.trim().is_empty() {
if let Some(ref style) = seg.style {
if style.color().is_some() {
found_styled = true;
}
}
}
pos = end;
}
assert!(found_styled, "expected styled segment in the output");
}
#[test]
fn test_display_trait() {
let syntax = Syntax::new("fn main() {}", "rust");
let s = format!("{}", syntax);
assert!(!s.is_empty());
assert!(s.contains("fn"));
assert!(s.contains("main"));
}
}