use std::collections::HashSet;
use std::io::Stdout;
use std::path::Path;
use once_cell::sync::Lazy;
use syntect::easy::HighlightLines;
use syntect::highlighting::{Style as SyntectStyle, Theme, ThemeSet};
use syntect::parsing::SyntaxSet;
use syntect::util::LinesWithEndings;
use crate::Renderable;
use crate::cells::cell_len;
use crate::color::SimpleColor as Color;
use crate::console::{Console, ConsoleOptions, OverflowMethod};
use crate::measure::Measurement;
use crate::padding::PaddingDimensions;
use crate::segment::{Segment, Segments};
use crate::style::Style;
use crate::text::Text;
static SYNTAX_SET: Lazy<SyntaxSet> = Lazy::new(SyntaxSet::load_defaults_newlines);
static THEME_SET: Lazy<ThemeSet> = Lazy::new(ThemeSet::load_defaults);
const MONOKAI_THEME_DATA: &[u8] = include_bytes!("themes/monokai.tmTheme");
const MONOKAI_PLUS_THEME_DATA: &[u8] = include_bytes!("themes/monokai-plus.tmTheme");
const DRACULA_THEME_DATA: &[u8] = include_bytes!("themes/dracula.tmTheme");
const GRUVBOX_DARK_THEME_DATA: &[u8] = include_bytes!("themes/gruvbox-dark.tmTheme");
const NORD_THEME_DATA: &[u8] = include_bytes!("themes/nord.tmTheme");
static MONOKAI_THEME: Lazy<Option<Theme>> = Lazy::new(|| {
use std::io::Cursor;
let mut cursor = Cursor::new(MONOKAI_THEME_DATA);
ThemeSet::load_from_reader(&mut cursor).ok()
});
static MONOKAI_PLUS_THEME: Lazy<Option<Theme>> = Lazy::new(|| {
use std::io::Cursor;
let mut cursor = Cursor::new(MONOKAI_PLUS_THEME_DATA);
ThemeSet::load_from_reader(&mut cursor).ok()
});
static DRACULA_THEME: Lazy<Option<Theme>> = Lazy::new(|| {
use std::io::Cursor;
let mut cursor = Cursor::new(DRACULA_THEME_DATA);
ThemeSet::load_from_reader(&mut cursor).ok()
});
static GRUVBOX_DARK_THEME: Lazy<Option<Theme>> = Lazy::new(|| {
use std::io::Cursor;
let mut cursor = Cursor::new(GRUVBOX_DARK_THEME_DATA);
ThemeSet::load_from_reader(&mut cursor).ok()
});
static NORD_THEME: Lazy<Option<Theme>> = Lazy::new(|| {
use std::io::Cursor;
let mut cursor = Cursor::new(NORD_THEME_DATA);
ThemeSet::load_from_reader(&mut cursor).ok()
});
pub const DEFAULT_THEME: &str = "monokai";
const FALLBACK_SYNTECT_THEME: &str = "base16-mocha.dark";
pub const NUMBERS_COLUMN_DEFAULT_PADDING: usize = 2;
pub mod ansi_light {
use crate::color::SimpleColor as Color;
use crate::style::Style;
pub fn comment() -> Style {
Style::new().with_dim(true)
}
pub fn comment_preproc() -> Style {
Style::new().with_color(Color::Standard(6)) }
pub fn keyword() -> Style {
Style::new().with_color(Color::Standard(4)) }
pub fn keyword_type() -> Style {
Style::new().with_color(Color::Standard(6)) }
pub fn operator_word() -> Style {
Style::new().with_color(Color::Standard(5)) }
pub fn name_builtin() -> Style {
Style::new().with_color(Color::Standard(6)) }
pub fn name_function() -> Style {
Style::new().with_color(Color::Standard(2)) }
pub fn name_namespace() -> Style {
Style::new()
.with_color(Color::Standard(6))
.with_underline(true) }
pub fn name_class() -> Style {
Style::new()
.with_color(Color::Standard(2))
.with_underline(true) }
pub fn name_decorator() -> Style {
Style::new().with_color(Color::Standard(5)).with_bold(true) }
pub fn name_variable() -> Style {
Style::new().with_color(Color::Standard(1)) }
pub fn name_attribute() -> Style {
Style::new().with_color(Color::Standard(6)) }
pub fn name_tag() -> Style {
Style::new().with_color(Color::Standard(12)) }
pub fn string() -> Style {
Style::new().with_color(Color::Standard(3)) }
pub fn number() -> Style {
Style::new().with_color(Color::Standard(4)) }
pub fn error() -> Style {
Style::new()
.with_color(Color::Standard(1))
.with_underline(true) }
}
pub mod ansi_dark {
use crate::color::SimpleColor as Color;
use crate::style::Style;
pub fn comment() -> Style {
Style::new().with_dim(true)
}
pub fn comment_preproc() -> Style {
Style::new().with_color(Color::Standard(14)) }
pub fn keyword() -> Style {
Style::new().with_color(Color::Standard(12)) }
pub fn keyword_type() -> Style {
Style::new().with_color(Color::Standard(14)) }
pub fn operator_word() -> Style {
Style::new().with_color(Color::Standard(13)) }
pub fn name_builtin() -> Style {
Style::new().with_color(Color::Standard(14)) }
pub fn name_function() -> Style {
Style::new().with_color(Color::Standard(10)) }
pub fn name_namespace() -> Style {
Style::new()
.with_color(Color::Standard(14))
.with_underline(true) }
pub fn name_class() -> Style {
Style::new()
.with_color(Color::Standard(10))
.with_underline(true) }
pub fn name_decorator() -> Style {
Style::new().with_color(Color::Standard(13)).with_bold(true) }
pub fn name_variable() -> Style {
Style::new().with_color(Color::Standard(9)) }
pub fn name_attribute() -> Style {
Style::new().with_color(Color::Standard(14)) }
pub fn name_tag() -> Style {
Style::new().with_color(Color::Standard(12)) }
pub fn string() -> Style {
Style::new().with_color(Color::Standard(3)) }
pub fn number() -> Style {
Style::new().with_color(Color::Standard(12)) }
pub fn error() -> Style {
Style::new()
.with_color(Color::Standard(1))
.with_underline(true) }
}
pub trait SyntaxTheme: Send + Sync {
fn get_style(&self, style: &SyntectStyle) -> Style;
fn get_background_style(&self) -> Style;
fn syntect_theme(&self) -> Option<&Theme>;
}
pub struct SyntectTheme {
theme: Theme,
background_style: Style,
}
impl SyntectTheme {
pub fn new(theme: Theme) -> Self {
let bg_color = theme.settings.background.map(|c| Color::Rgb {
r: c.r,
g: c.g,
b: c.b,
});
let background_style = match bg_color {
Some(c) => Style::new().with_bgcolor(c),
None => Style::new(),
};
Self {
theme,
background_style,
}
}
pub fn from_name(name: &str) -> Option<Self> {
THEME_SET.themes.get(name).map(|t| Self::new(t.clone()))
}
}
impl SyntaxTheme for SyntectTheme {
fn get_style(&self, style: &SyntectStyle) -> Style {
let fg = style.foreground;
let bg = style.background;
let mut result = Style::new();
result = result.with_color(Color::Rgb {
r: fg.r,
g: fg.g,
b: fg.b,
});
if let Some(theme_bg) = self.theme.settings.background {
if bg.r != theme_bg.r || bg.g != theme_bg.g || bg.b != theme_bg.b {
result = result.with_bgcolor(Color::Rgb {
r: bg.r,
g: bg.g,
b: bg.b,
});
}
}
let font_style = style.font_style;
if font_style.contains(syntect::highlighting::FontStyle::BOLD) {
result = result.with_bold(true);
}
if font_style.contains(syntect::highlighting::FontStyle::ITALIC) {
result = result.with_italic(true);
}
if font_style.contains(syntect::highlighting::FontStyle::UNDERLINE) {
result = result.with_underline(true);
}
result
}
fn get_background_style(&self) -> Style {
self.background_style
}
fn syntect_theme(&self) -> Option<&Theme> {
Some(&self.theme)
}
}
pub struct AnsiTheme {
dark: bool,
}
impl AnsiTheme {
pub fn dark() -> Self {
Self { dark: true }
}
pub fn light() -> Self {
Self { dark: false }
}
}
impl SyntaxTheme for AnsiTheme {
fn get_style(&self, style: &SyntectStyle) -> Style {
let fg = style.foreground;
let (r, g, b) = (fg.r as u16, fg.g as u16, fg.b as u16);
let brightness = (r + g + b) / 3;
let color = if brightness < 50 {
Color::Standard(0) } else if r > 200 && g < 100 && b < 100 {
if self.dark {
Color::Standard(9)
} else {
Color::Standard(1)
}
} else if g > 200 && r < 100 && b < 100 {
if self.dark {
Color::Standard(10)
} else {
Color::Standard(2)
}
} else if b > 200 && r < 100 && g < 100 {
if self.dark {
Color::Standard(12)
} else {
Color::Standard(4)
}
} else if r > 200 && g > 200 && b < 100 {
Color::Standard(3)
} else if r > 200 && b > 200 && g < 100 {
if self.dark {
Color::Standard(13)
} else {
Color::Standard(5)
}
} else if g > 200 && b > 200 && r < 100 {
if self.dark {
Color::Standard(14)
} else {
Color::Standard(6)
}
} else if brightness > 200 {
Color::Standard(7)
} else {
Color::Rgb {
r: fg.r,
g: fg.g,
b: fg.b,
}
};
let mut result = Style::new().with_color(color);
let font_style = style.font_style;
if font_style.contains(syntect::highlighting::FontStyle::BOLD) {
result = result.with_bold(true);
}
if font_style.contains(syntect::highlighting::FontStyle::ITALIC) {
result = result.with_italic(true);
}
if font_style.contains(syntect::highlighting::FontStyle::UNDERLINE) {
result = result.with_underline(true);
}
result
}
fn get_background_style(&self) -> Style {
Style::new()
}
fn syntect_theme(&self) -> Option<&Theme> {
None
}
}
#[derive(Debug, Clone)]
struct SyntaxHighlightRange {
style: Style,
start: (usize, usize),
end: (usize, usize),
}
pub struct Syntax {
code: String,
lexer: String,
theme: Box<dyn SyntaxTheme>,
dedent: bool,
line_numbers: bool,
start_line: usize,
line_range: Option<(Option<usize>, Option<usize>)>,
highlight_lines: HashSet<usize>,
code_width: Option<usize>,
tab_size: usize,
#[allow(dead_code)]
word_wrap: bool,
background_color: Option<Color>,
#[allow(dead_code)]
indent_guides: bool,
padding: (usize, usize, usize, usize),
explicit_theme: bool,
stylized_ranges: Vec<SyntaxHighlightRange>,
}
impl std::fmt::Debug for Syntax {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Syntax")
.field("code_len", &self.code.len())
.field("lexer", &self.lexer)
.field("dedent", &self.dedent)
.field("line_numbers", &self.line_numbers)
.field("start_line", &self.start_line)
.field("line_range", &self.line_range)
.field("highlight_lines", &self.highlight_lines)
.field("code_width", &self.code_width)
.field("tab_size", &self.tab_size)
.field("word_wrap", &self.word_wrap)
.field("indent_guides", &self.indent_guides)
.field("padding", &self.padding)
.field("explicit_theme", &self.explicit_theme)
.finish_non_exhaustive()
}
}
impl Syntax {
pub fn new(code: impl Into<String>, lexer: impl Into<String>) -> Self {
let theme = Self::get_theme(DEFAULT_THEME);
Self {
code: code.into(),
lexer: lexer.into(),
theme,
dedent: false,
line_numbers: false,
start_line: 1,
line_range: None,
highlight_lines: HashSet::new(),
code_width: None,
tab_size: 4,
word_wrap: false,
background_color: None,
indent_guides: false,
padding: (0, 0, 0, 0),
explicit_theme: false,
stylized_ranges: Vec::new(),
}
}
pub fn from_path(path: impl AsRef<Path>) -> std::io::Result<Self> {
let path = path.as_ref();
let code = std::fs::read_to_string(path)?;
let lexer = Self::guess_lexer(path, Some(&code));
Ok(Self::new(code, lexer))
}
pub fn guess_lexer(path: impl AsRef<Path>, code: Option<&str>) -> String {
let path = path.as_ref();
if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
if let Some(syntax) = SYNTAX_SET.find_syntax_by_extension(ext) {
return syntax.name.to_lowercase();
}
}
if let Some(filename) = path.file_name().and_then(|n| n.to_str()) {
match filename.to_lowercase().as_str() {
"makefile" | "gnumakefile" => return "makefile".to_string(),
"dockerfile" => return "dockerfile".to_string(),
"cmakelists.txt" => return "cmake".to_string(),
_ => {}
}
}
if let Some(code) = code {
if code.starts_with("#!/usr/bin/env python") || code.starts_with("#!/usr/bin/python") {
return "python".to_string();
}
if code.starts_with("#!/bin/bash") || code.starts_with("#!/usr/bin/env bash") {
return "bash".to_string();
}
if code.starts_with("#!/usr/bin/env node") {
return "javascript".to_string();
}
if code.starts_with("#!/usr/bin/env ruby") {
return "ruby".to_string();
}
if let Some(syntax) = SYNTAX_SET.find_syntax_by_first_line(code) {
return syntax.name.to_lowercase();
}
}
"text".to_string()
}
pub fn get_theme(name: &str) -> Box<dyn SyntaxTheme> {
match name.to_lowercase().as_str() {
"ansi_dark" | "ansi-dark" => return Box::new(AnsiTheme::dark()),
"ansi_light" | "ansi-light" => return Box::new(AnsiTheme::light()),
_ => {}
}
match name.to_lowercase().as_str() {
"monokai" => {
if let Some(ref theme) = *MONOKAI_THEME {
return Box::new(SyntectTheme::new(theme.clone()));
}
}
"monokai-plus" | "monokai_plus" => {
if let Some(ref theme) = *MONOKAI_PLUS_THEME {
return Box::new(SyntectTheme::new(theme.clone()));
}
}
"dracula" => {
if let Some(ref theme) = *DRACULA_THEME {
return Box::new(SyntectTheme::new(theme.clone()));
}
}
"gruvbox-dark" | "gruvbox_dark" | "gruvbox" => {
if let Some(ref theme) = *GRUVBOX_DARK_THEME {
return Box::new(SyntectTheme::new(theme.clone()));
}
}
"nord" => {
if let Some(ref theme) = *NORD_THEME {
return Box::new(SyntectTheme::new(theme.clone()));
}
}
_ => {}
}
let theme_name = match name.to_lowercase().as_str() {
"one-dark" | "onedark" => "base16-ocean.dark",
"one-light" | "onelight" => "base16-ocean.light",
"github-dark" => "base16-ocean.dark",
"github-light" => "base16-ocean.light",
"solarized-dark" => "Solarized (dark)",
"solarized-light" => "Solarized (light)",
_ => name,
};
SyntectTheme::from_name(theme_name)
.map(|t| Box::new(t) as Box<dyn SyntaxTheme>)
.unwrap_or_else(|| {
if let Some(ref theme) = *MONOKAI_THEME {
Box::new(SyntectTheme::new(theme.clone()))
} else {
Box::new(AnsiTheme::dark())
}
})
}
pub fn available_themes() -> Vec<&'static str> {
let mut themes: Vec<&str> = THEME_SET.themes.keys().map(|s| s.as_str()).collect();
themes.extend([
"ansi_dark",
"ansi_light",
"monokai",
"monokai-plus",
"dracula",
"gruvbox-dark",
"nord",
]);
themes.sort();
themes
}
pub fn available_languages() -> Vec<String> {
SYNTAX_SET
.syntaxes()
.iter()
.map(|s| s.name.to_lowercase())
.collect()
}
pub fn with_theme(mut self, theme: impl AsRef<str>) -> Self {
self.theme = Self::get_theme(theme.as_ref());
self.explicit_theme = true;
self
}
pub fn with_custom_theme(mut self, theme: Box<dyn SyntaxTheme>) -> Self {
self.theme = theme;
self.explicit_theme = true;
self
}
pub fn with_dedent(mut self, dedent: bool) -> Self {
self.dedent = dedent;
self
}
pub fn with_line_numbers(mut self, line_numbers: bool) -> Self {
self.line_numbers = line_numbers;
self
}
pub fn with_start_line(mut self, start_line: usize) -> Self {
self.start_line = start_line;
self
}
pub fn with_line_range(mut self, start: Option<usize>, end: Option<usize>) -> Self {
self.line_range = Some((start, end));
self
}
pub fn with_highlight_lines(mut self, lines: impl IntoIterator<Item = usize>) -> Self {
self.highlight_lines = lines.into_iter().collect();
self
}
pub fn with_code_width(mut self, width: usize) -> Self {
self.code_width = Some(width);
self
}
pub fn with_tab_size(mut self, tab_size: usize) -> Self {
self.tab_size = tab_size;
self
}
pub fn with_word_wrap(mut self, word_wrap: bool) -> Self {
self.word_wrap = word_wrap;
self
}
pub fn with_background_color(mut self, color: Color) -> Self {
self.background_color = Some(color);
self
}
pub fn with_indent_guides(mut self, indent_guides: bool) -> Self {
self.indent_guides = indent_guides;
self
}
pub fn with_padding(mut self, padding: impl Into<PaddingDimensions>) -> Self {
self.padding = padding.into().unpack();
self
}
pub fn stylize_range(&mut self, style: Style, start: (usize, usize), end: (usize, usize)) {
self.stylized_ranges
.push(SyntaxHighlightRange { style, start, end });
}
pub fn with_highlight_range(
mut self,
style: Style,
start: (usize, usize),
end: (usize, usize),
) -> Self {
self.stylized_ranges
.push(SyntaxHighlightRange { style, start, end });
self
}
pub fn code(&self) -> &str {
&self.code
}
pub fn lexer(&self) -> &str {
&self.lexer
}
pub fn line_numbers(&self) -> bool {
self.line_numbers
}
pub fn tab_size(&self) -> usize {
self.tab_size
}
pub fn highlight(&self) -> Text {
self.highlight_with_theme(&*self.theme)
}
fn highlight_with_theme(&self, theme: &dyn SyntaxTheme) -> Text {
let (ends_on_nl, processed_code) = self.process_code();
let lexer_lower = self.lexer.to_lowercase();
let lexer_normalized = match lexer_lower.as_str() {
"python3" | "py3" | "py" => "python",
"javascript" | "js" => "javascript",
"typescript" | "ts" => "typescript",
"rust" | "rs" => "rust",
"cpp" | "c++" => "c++",
"csharp" | "cs" => "c#",
_ => lexer_lower.as_str(),
};
let syntax = SYNTAX_SET
.find_syntax_by_token(lexer_normalized)
.or_else(|| SYNTAX_SET.find_syntax_by_extension(lexer_normalized))
.or_else(|| SYNTAX_SET.find_syntax_by_name(lexer_normalized))
.or_else(|| SYNTAX_SET.find_syntax_by_token(&self.lexer))
.or_else(|| SYNTAX_SET.find_syntax_by_extension(&self.lexer))
.or_else(|| SYNTAX_SET.find_syntax_by_name(&self.lexer))
.unwrap_or_else(|| SYNTAX_SET.find_syntax_plain_text());
let base_style = self.get_base_style_with_theme(theme);
let mut text = Text::new();
text.set_base_style(Some(base_style));
if let Some(syntect_theme) = theme.syntect_theme() {
let mut highlighter = HighlightLines::new(syntax, syntect_theme);
for line in LinesWithEndings::from(&processed_code) {
match highlighter.highlight_line(line, &SYNTAX_SET) {
Ok(ranges) => {
for (style, token) in ranges {
let rich_style = theme.get_style(&style);
text.append(token, Some(rich_style));
}
}
Err(_) => {
text.append(line, None);
}
}
}
} else {
let mut highlighter =
HighlightLines::new(syntax, &THEME_SET.themes[FALLBACK_SYNTECT_THEME]);
for line in LinesWithEndings::from(&processed_code) {
match highlighter.highlight_line(line, &SYNTAX_SET) {
Ok(ranges) => {
for (style, token) in ranges {
let rich_style = theme.get_style(&style);
text.append(token, Some(rich_style));
}
}
Err(_) => {
text.append(line, None);
}
}
}
}
if !self.stylized_ranges.is_empty() {
self.apply_stylized_ranges(&mut text);
}
if !ends_on_nl && text.plain_text().ends_with('\n') {
let plain = text.plain_text();
let new_plain = plain.trim_end_matches('\n');
if plain != new_plain {
let mut new_text = Text::new();
new_text.set_base_style(text.base_style());
new_text.append(new_plain, None);
for span in text.spans() {
if span.start < new_plain.chars().count() {
new_text.stylize(
span.start,
span.end.min(new_plain.chars().count()),
span.style,
);
}
}
return new_text;
}
}
text
}
fn apply_stylized_ranges(&self, text: &mut Text) {
let plain = text.plain_text();
let mut newlines_offsets: Vec<usize> = vec![0];
for (i, c) in plain.chars().enumerate() {
if c == '\n' {
newlines_offsets.push(i + 1);
}
}
newlines_offsets.push(plain.chars().count() + 1);
for range in &self.stylized_ranges {
let (start_line, start_col) = range.start;
let (end_line, end_col) = range.end;
let start_line_idx = start_line.saturating_sub(1);
let end_line_idx = end_line.saturating_sub(1);
let start_offset =
newlines_offsets.get(start_line_idx).copied().unwrap_or(0) + start_col;
let end_offset = newlines_offsets.get(end_line_idx).copied().unwrap_or(0) + end_col;
if start_offset < end_offset {
text.stylize(start_offset, end_offset, range.style);
}
}
}
fn get_base_style(&self) -> Style {
self.get_base_style_with_theme(&*self.theme)
}
fn get_base_style_with_theme(&self, theme: &dyn SyntaxTheme) -> Style {
let mut style = theme.get_background_style();
if let Some(bg) = self.background_color {
style = style.with_bgcolor(bg);
}
style
}
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)
};
if self.dedent {
processed = Self::dedent_code(&processed);
}
processed = Self::expand_tabs(&processed, self.tab_size);
(ends_on_nl, processed)
}
fn dedent_code(code: &str) -> String {
let lines: Vec<&str> = code.lines().collect();
let min_indent = lines
.iter()
.filter(|line| !line.trim().is_empty())
.map(|line| line.len() - line.trim_start().len())
.min()
.unwrap_or(0);
if min_indent == 0 {
return code.to_string();
}
lines
.iter()
.map(|line| {
if line.len() >= min_indent {
&line[min_indent..]
} else {
line.trim_start()
}
})
.collect::<Vec<_>>()
.join("\n")
+ if code.ends_with('\n') { "\n" } else { "" }
}
fn expand_tabs(code: &str, tab_size: usize) -> String {
if !code.contains('\t') {
return code.to_string();
}
let mut result = String::new();
let mut column = 0;
for c in code.chars() {
match c {
'\t' => {
let spaces = tab_size - (column % tab_size);
for _ in 0..spaces {
result.push(' ');
}
column += spaces;
}
'\n' => {
result.push(c);
column = 0;
}
_ => {
result.push(c);
column += 1;
}
}
}
result
}
fn indent_guide_color(
theme: &dyn SyntaxTheme,
background_override: Option<Color>,
) -> Option<Color> {
const DIM_FACTOR: f64 = 0.66;
let syntect_theme = theme.syntect_theme()?;
let highlighter = syntect::highlighting::Highlighter::new(syntect_theme);
let comment_scope = syntect::parsing::Scope::new("comment").ok()?;
let fg = highlighter.style_for_stack(&[comment_scope]).foreground;
let (bg_r, bg_g, bg_b) = match background_override {
Some(Color::Rgb { r, g, b }) => (r, g, b),
Some(_) => return None,
None => {
let bg = syntect_theme.settings.background?;
(bg.r, bg.g, bg.b)
}
};
let blend =
|bg: u8, fg: u8| (f64::from(bg) + (f64::from(fg) - f64::from(bg)) * DIM_FACTOR) as u8;
Some(Color::Rgb {
r: blend(bg_r, fg.r),
g: blend(bg_g, fg.g),
b: blend(bg_b, fg.b),
})
}
fn numbers_column_width(&self) -> usize {
if !self.line_numbers {
return 0;
}
let line_count = self.code.lines().count();
let max_line_no = self.start_line + line_count.saturating_sub(1);
let digits = max_line_no.to_string().len();
digits + NUMBERS_COLUMN_DEFAULT_PADDING
}
}
impl Renderable for Syntax {
fn render(&self, console: &Console<Stdout>, options: &ConsoleOptions) -> Segments {
let mut result = Segments::new();
let (pad_top, pad_right, pad_bottom, pad_left) = self.padding;
let horizontal_padding = pad_left + pad_right;
let numbers_width = self.numbers_column_width();
let code_width = if let Some(w) = self.code_width {
w
} else if self.line_numbers {
options
.max_width
.saturating_sub(numbers_width + 1 + horizontal_padding)
} else {
options.max_width.saturating_sub(horizontal_padding)
};
let effective_theme: Option<Box<dyn SyntaxTheme>> =
if !self.explicit_theme && options.theme_name != "default" {
Some(Self::get_theme(&options.theme_name))
} else {
None
};
let text = if let Some(ref theme) = effective_theme {
self.highlight_with_theme(&**theme)
} else {
self.highlight()
};
let lines: Vec<Text> = text.split("\n", false, true);
let (start_idx, end_idx) = if let Some((start, end)) = self.line_range {
let start = start.map(|s| s.saturating_sub(1)).unwrap_or(0);
let end = end.unwrap_or(lines.len());
(start, end)
} else {
(0, lines.len())
};
let filtered_lines: Vec<&Text> = lines
.iter()
.skip(start_idx)
.take(end_idx.saturating_sub(start_idx))
.collect();
let base_style = if let Some(ref theme) = effective_theme {
self.get_base_style_with_theme(&**theme)
} else {
self.get_base_style()
};
let new_line = Segment::line();
let number_style = base_style.combine(&Style::new().with_color(Color::Rgb {
r: 101,
g: 102,
b: 96,
}));
let highlight_number_style = base_style.combine(&Style::new().with_bold(true));
let render_theme: &dyn SyntaxTheme = effective_theme.as_deref().unwrap_or(&*self.theme);
let indent_guide_style = base_style.combine(
&Self::indent_guide_color(render_theme, self.background_color)
.map(|color| Style::new().with_color(color))
.unwrap_or_else(|| Style::new().with_dim(true)),
);
if pad_top > 0 {
let blank = " ".repeat(options.max_width);
for _ in 0..pad_top {
result.push(Segment::styled(blank.clone(), base_style));
result.push(new_line.clone());
}
}
for (idx, line) in filtered_lines.iter().enumerate() {
let line_no = self.start_line + start_idx + idx;
let is_highlighted = self.highlight_lines.contains(&line_no);
if pad_left > 0 {
result.push(Segment::styled(" ".repeat(pad_left), base_style));
}
if self.line_numbers {
let pointer = if options.legacy_windows { "> " } else { "❱ " };
let line_num_str = format!(
"{:>width$} ",
line_no,
width = numbers_width.saturating_sub(2)
);
if is_highlighted {
let pointer_style = base_style
.combine(&Style::new().with_color(Color::Standard(1)).with_bold(true));
result.push(Segment::styled(pointer.to_string(), pointer_style));
result.push(Segment::styled(line_num_str, highlight_number_style));
} else {
result.push(Segment::styled(" ".to_string(), highlight_number_style));
result.push(Segment::styled(line_num_str, number_style));
}
}
let mut guides_width = 0;
let line_to_render;
if self.indent_guides {
let plain = line.plain_text();
let leading_spaces = plain.chars().take_while(|c| *c == ' ').count();
let num_guides = leading_spaces / self.tab_size;
if num_guides > 0 {
for _ in 0..num_guides {
result.push(Segment::styled("│".to_string(), indent_guide_style));
result.push(Segment::styled(
" ".repeat(self.tab_size - 1),
indent_guide_style,
));
}
guides_width = num_guides * self.tab_size;
let parts = line.divide([guides_width]);
line_to_render = if parts.len() > 1 {
parts.into_iter().nth(1).unwrap_or_else(|| (*line).clone())
} else {
Text::plain("")
};
} else {
line_to_render = (*line).clone();
}
} else {
line_to_render = (*line).clone();
}
let content_width = code_width.saturating_sub(guides_width);
let mut line_options = options.update_width(content_width);
line_options.no_wrap = true;
line_options.overflow = Some(OverflowMethod::Crop);
let line_segments: Vec<Segment> = line_to_render
.render(console, &line_options)
.into_iter()
.collect();
let adjusted =
Segment::adjust_line_length(&line_segments, content_width, Some(base_style), true);
for seg in adjusted {
result.push(seg);
}
if pad_right > 0 {
result.push(Segment::styled(" ".repeat(pad_right), base_style));
}
result.push(new_line.clone());
}
if pad_bottom > 0 {
let blank = " ".repeat(options.max_width);
for _ in 0..pad_bottom {
result.push(Segment::styled(blank.clone(), base_style));
result.push(new_line.clone());
}
}
result
}
fn measure(&self, _console: &Console<Stdout>, options: &ConsoleOptions) -> Measurement {
let (_, pad_right, _, pad_left) = self.padding;
let horizontal_padding = pad_left + pad_right;
let numbers_width = self.numbers_column_width();
if let Some(code_width) = self.code_width {
let width = code_width + numbers_width + horizontal_padding;
if self.line_numbers {
return Measurement::new(numbers_width, width + 1);
}
return Measurement::new(numbers_width, width);
}
let lines: Vec<&str> = self.code.lines().collect();
let max_line_width = lines.iter().map(|l| cell_len(l)).max().unwrap_or(0);
let width = max_line_width + numbers_width + horizontal_padding;
let width = if self.line_numbers { width + 1 } else { width };
Measurement::new(numbers_width.max(1), width.min(options.max_width))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_syntax_new() {
let syntax = Syntax::new("fn main() {}", "rust");
assert_eq!(syntax.code(), "fn main() {}");
assert_eq!(syntax.lexer(), "rust");
}
#[test]
fn test_syntax_with_line_numbers() {
let syntax = Syntax::new("fn main() {}", "rust").with_line_numbers(true);
assert!(syntax.line_numbers());
}
#[test]
fn test_syntax_with_theme() {
let syntax = Syntax::new("fn main() {}", "rust").with_theme("monokai");
assert_eq!(syntax.code(), "fn main() {}");
}
#[test]
fn test_syntax_with_tab_size() {
let syntax = Syntax::new("fn main() {}", "rust").with_tab_size(2);
assert_eq!(syntax.tab_size(), 2);
}
#[test]
fn test_syntax_highlight() {
let syntax = Syntax::new("fn main() {}", "rust");
let text = syntax.highlight();
assert!(text.plain_text().contains("fn"));
assert!(text.plain_text().contains("main"));
}
#[test]
fn test_syntax_highlight_python() {
let code = r#"def hello():
print("Hello, World!")
"#;
let syntax = Syntax::new(code, "python");
let text = syntax.highlight();
assert!(text.plain_text().contains("def"));
assert!(text.plain_text().contains("hello"));
}
#[test]
fn test_syntax_dedent() {
let code = " fn main() {\n println!(\"hello\");\n }";
let syntax = Syntax::new(code, "rust").with_dedent(true);
let text = syntax.highlight();
assert!(text.plain_text().starts_with("fn"));
}
#[test]
fn test_syntax_expand_tabs() {
let code = "fn main() {\n\tprintln!(\"hello\");\n}";
let syntax = Syntax::new(code, "rust").with_tab_size(4);
let text = syntax.highlight();
assert!(!text.plain_text().contains('\t'));
}
#[test]
fn test_guess_lexer_by_extension() {
assert_eq!(Syntax::guess_lexer("test.rs", None), "rust");
assert_eq!(Syntax::guess_lexer("test.py", None), "python");
assert_eq!(Syntax::guess_lexer("test.js", None), "javascript");
}
#[test]
fn test_available_themes() {
let themes = Syntax::available_themes();
assert!(!themes.is_empty());
assert!(themes.contains(&"ansi_dark"));
assert!(themes.contains(&"ansi_light"));
assert!(themes.contains(&"dracula"), "Should contain dracula theme");
assert!(
themes.contains(&"gruvbox-dark"),
"Should contain gruvbox-dark theme"
);
assert!(themes.contains(&"nord"), "Should contain nord theme");
assert!(themes.contains(&"monokai"), "Should contain monokai theme");
assert!(
themes.contains(&"monokai-plus"),
"Should contain monokai-plus theme"
);
}
#[test]
fn test_available_languages() {
let languages = Syntax::available_languages();
assert!(!languages.is_empty());
}
#[test]
fn test_numbers_column_width() {
let code = "line1\nline2\nline3\nline4\nline5\nline6\nline7\nline8\nline9\nline10";
let syntax = Syntax::new(code, "text").with_line_numbers(true);
assert_eq!(syntax.numbers_column_width(), 4);
}
#[test]
fn test_syntax_render() {
let syntax = Syntax::new("fn main() {}", "rust");
let console = Console::new();
let options = ConsoleOptions::default();
let segments = syntax.render(&console, &options);
let output: String = segments.iter().map(|s| s.text.to_string()).collect();
assert!(output.contains("fn"));
assert!(output.contains("main"));
}
#[test]
fn test_syntax_render_with_line_numbers() {
let syntax = Syntax::new("line1\nline2", "text").with_line_numbers(true);
let console = Console::new();
let options = ConsoleOptions::default();
let segments = syntax.render(&console, &options);
let output: String = segments.iter().map(|s| s.text.to_string()).collect();
assert!(output.contains('1'));
assert!(output.contains('2'));
}
#[test]
fn test_syntax_measure() {
let syntax = Syntax::new("hello", "text");
let console = Console::new();
let options = ConsoleOptions::default();
let measurement = syntax.measure(&console, &options);
assert!(measurement.maximum >= 5); }
#[test]
fn test_syntax_is_send_sync() {
fn assert_send<T: Send>() {}
fn assert_sync<T: Sync>() {}
assert_send::<Syntax>();
assert_sync::<Syntax>();
}
#[test]
fn test_dedent_code() {
let code = " line1\n line2\n line3\n";
let dedented = Syntax::dedent_code(code);
assert_eq!(dedented, "line1\nline2\nline3\n");
}
#[test]
fn test_dedent_code_mixed_indent() {
let code = " line1\n line2\n line3\n";
let dedented = Syntax::dedent_code(code);
assert_eq!(dedented, "line1\n line2\nline3\n");
}
#[test]
fn test_expand_tabs() {
let code = "a\tb\tc";
let expanded = Syntax::expand_tabs(code, 4);
assert_eq!(expanded, "a b c");
}
#[test]
fn test_expand_tabs_preserves_newlines() {
let code = "a\tb\nc\td";
let expanded = Syntax::expand_tabs(code, 4);
assert_eq!(expanded, "a b\nc d");
}
#[test]
fn test_ansi_theme() {
let theme = AnsiTheme::dark();
let style = SyntectStyle {
foreground: syntect::highlighting::Color {
r: 255,
g: 0,
b: 0,
a: 255,
},
background: syntect::highlighting::Color {
r: 0,
g: 0,
b: 0,
a: 255,
},
font_style: syntect::highlighting::FontStyle::empty(),
};
let rich_style = theme.get_style(&style);
assert!(rich_style.color.is_some());
}
#[test]
fn test_syntect_theme() {
let theme = SyntectTheme::from_name(FALLBACK_SYNTECT_THEME);
assert!(
theme.is_some(),
"Fallback theme '{}' should exist",
FALLBACK_SYNTECT_THEME
);
let theme = theme.unwrap();
assert!(theme.syntect_theme().is_some());
let default_theme = Syntax::get_theme(DEFAULT_THEME);
assert!(default_theme.syntect_theme().is_some());
}
#[test]
fn test_line_range() {
let code = "line1\nline2\nline3\nline4\nline5";
let syntax = Syntax::new(code, "text").with_line_range(Some(2), Some(4));
let console = Console::new();
let options = ConsoleOptions::default();
let segments = syntax.render(&console, &options);
let output: String = segments.iter().map(|s| s.text.to_string()).collect();
assert!(output.contains("line2"));
assert!(output.contains("line3"));
assert!(output.contains("line4"));
assert!(!output.contains("line1"));
assert!(!output.contains("line5"));
}
#[test]
fn test_highlight_lines() {
let code = "line1\nline2\nline3";
let syntax = Syntax::new(code, "text")
.with_line_numbers(true)
.with_highlight_lines([2]);
let console = Console::new();
let options = ConsoleOptions::default();
let segments = syntax.render(&console, &options);
let output: String = segments.iter().map(|s| s.text.to_string()).collect();
assert!(output.contains('❱') || output.contains('>'));
}
#[test]
fn test_stylize_range() {
let mut syntax = Syntax::new("line1\nline2\nline3", "text");
let style = Style::new().with_bold(true);
syntax.stylize_range(style, (2, 0), (2, 5));
let text = syntax.highlight();
let plain = text.plain_text();
assert!(plain.contains("line2"));
let spans = text.spans();
let has_bold_span = spans.iter().any(|s| s.style.bold == Some(true));
assert!(has_bold_span, "Should have a bold span from stylize_range");
}
#[test]
fn test_with_highlight_range_builder() {
let style = Style::new().with_italic(true);
let syntax = Syntax::new("hello world", "text").with_highlight_range(style, (1, 0), (1, 5));
let text = syntax.highlight();
let spans = text.spans();
let has_italic_span = spans.iter().any(|s| s.style.italic == Some(true));
assert!(
has_italic_span,
"Should have an italic span from with_highlight_range"
);
}
fn python_token_color(code: &str, token: &str) -> Option<Color> {
let syntax = Syntax::new(code, "python");
let text = syntax.highlight();
let plain = text.plain_text();
let start = plain.find(token).expect("token present in highlighted text");
let mut found = None;
for span in text.spans() {
if span.start <= start && start < span.end {
if let Some(color) = span.style.color {
found = Some(color);
}
}
}
found
}
const MONOKAI_PLAIN: Color = Color::Rgb {
r: 0xf8,
g: 0xf8,
b: 0xf2,
};
const MONOKAI_OPERATOR: Color = Color::Rgb {
r: 0xff,
g: 0x46,
b: 0x89,
};
const MONOKAI_STRING: Color = Color::Rgb {
r: 0xe6,
g: 0xdb,
b: 0x74,
};
const MONOKAI_COMMENT: Color = Color::Rgb {
r: 0x95,
g: 0x90,
b: 0x77,
};
#[test]
fn test_python_annotation_colon_is_plain_punctuation() {
let code = "def f(values: Iterable[T]) -> int:\n pass\n";
assert_eq!(python_token_color(code, ":"), Some(MONOKAI_PLAIN));
assert_eq!(python_token_color(code, "->"), Some(MONOKAI_OPERATOR));
}
#[test]
fn test_python_docstring_quotes_are_string_colored() {
let code = "def f():\n \"\"\"Doc.\"\"\"\n";
assert_eq!(python_token_color(code, "\"\"\""), Some(MONOKAI_STRING));
}
#[test]
fn test_python_for_in_is_operator_word() {
let code = "for value in values:\n pass\n";
assert_eq!(python_token_color(code, "in"), Some(MONOKAI_OPERATOR));
assert_eq!(
python_token_color(code, "for"),
Some(Color::Rgb {
r: 0x66,
g: 0xd9,
b: 0xef
})
);
}
#[test]
fn test_python_comment_hash_is_comment_colored() {
let code = "# hello\n";
assert_eq!(python_token_color(code, "#"), Some(MONOKAI_COMMENT));
}
#[test]
fn test_indent_guide_color_monokai_dim_preblend() {
let theme = Syntax::get_theme("monokai");
assert_eq!(
Syntax::indent_guide_color(&*theme, None),
Some(Color::Rgb {
r: 0x6f,
g: 0x6c,
b: 0x5a
})
);
}
#[test]
fn test_embedded_themes() {
let themes = ["dracula", "gruvbox-dark", "nord", "monokai"];
let code = "def hello():\n print('Hello')";
for theme_name in themes {
let theme = Syntax::get_theme(theme_name);
assert!(
theme.syntect_theme().is_some(),
"Theme '{}' should load a syntect theme",
theme_name
);
let syntax = Syntax::new(code, "python3").with_theme(theme_name);
let console = Console::new();
let options = ConsoleOptions::default();
let segments = syntax.render(&console, &options);
assert!(
!segments.is_empty(),
"Theme '{}' should produce output",
theme_name
);
}
}
}