use super::*;
pub(super) static SYNTAX_SET: LazyLock<SyntaxSet> = LazyLock::new(|| {
syntect::dumps::from_uncompressed_data(include_bytes!(concat!(
env!("OUT_DIR"),
"/syntaxes.packdump"
)))
.expect("the syntax dump built by build.rs must load")
});
pub(super) fn resolve_syntax(lang: &str) -> Option<&'static SyntaxReference> {
if lang.is_empty() {
return None;
}
let canonical = canonical_lang(lang);
SYNTAX_SET
.find_syntax_by_token(canonical)
.or_else(|| SYNTAX_SET.find_syntax_by_token(lang))
}
pub(super) fn canonical_lang(lang: &str) -> &str {
match lang.trim().to_ascii_lowercase().as_str() {
"csharp" | "cs-script" | "dotnet" => "cs", "cpp" | "cplusplus" | "cxx" | "cc" => "c++", "objc" | "objective-c" | "objectivec" | "obj-c" => "m", "objcpp" | "objc++" | "objective-c++" => "mm", "golang" => "go",
"rustlang" => "rs",
"python3" | "py3" | "python2" => "py",
"node" | "nodejs" => "js",
"shell" | "sh" | "zsh" | "console" | "shell-session" | "shellsession" => "bash",
"yml" | "yaml-frontmatter" | "frontmatter" => "yaml",
"rlang" => "r",
"docker" | "containerfile" => "dockerfile",
"pwsh" => "ps1", "hcl" | "tfvars" => "tf",
"proto3" => "protobuf",
"jsonc" | "json5" => "json",
"jsx" => "js",
"tsx" => "ts", "v" | "vlang" => "go",
other => {
let _ = other;
lang.trim()
}
}
}
pub(super) fn highlight_line_or_plain(
hl: &mut HighlightLines<'static>,
line: &str,
) -> Vec<Line<'static>> {
let plain = || vec![Line::from(line.trim_end_matches('\n').to_string())];
let Ok(parts) = hl.highlight_line(line, &SYNTAX_SET) else {
return plain();
};
match as_24_bit_terminal_escaped(&parts, false).into_text() {
Ok(text) => text.lines,
Err(_) => plain(),
}
}
pub(super) const CODE_RIGHT_PAD: usize = 1;
pub(super) fn pad_code_block(lines: &mut Vec<Line<'static>>, start: usize, width: usize) {
let text_w = width.saturating_sub(CODE_RIGHT_PAD);
let rows: Vec<Line<'static>> = lines
.split_off(start)
.into_iter()
.flat_map(|line| wrap::wrap_line(&line, text_w))
.map(trim_row_trailing_ws)
.collect();
let block = rows
.iter()
.map(|l| cell_width(&l.spans) + CODE_RIGHT_PAD)
.max()
.unwrap_or(0)
.min(width);
lines.extend(rows.into_iter().map(|mut row| {
let pad = block.saturating_sub(cell_width(&row.spans));
if pad > 0 {
row.spans.push(Span::raw(" ".repeat(pad)));
}
row
}));
}
pub(super) fn build_code_theme(palette: &Palette) -> Theme {
let (default_fg, comment) = if palette.dark {
(gray(212), gray(128))
} else {
(gray(40), gray(110))
};
let settings = ThemeSettings {
foreground: Some(default_fg),
..Default::default()
};
let scopes = vec![
scope_item("comment", comment),
scope_item(
"keyword, storage, keyword.operator, keyword.control",
to_syn(palette.accent),
),
scope_item(
"string, string.quoted, string.regexp",
to_syn(palette.success),
),
scope_item(
"constant.numeric, constant.language, constant.character, constant.character.escape",
to_syn(palette.warning),
),
scope_item(
"entity.name.function, support.function, meta.function-call",
to_syn(palette.user),
),
scope_item(
"entity.name.type, entity.name.class, support.type, support.class, entity.other.inherited-class",
to_syn(palette.assistant),
),
scope_item(
"entity.name.tag, punctuation.definition.tag",
to_syn(palette.accent),
),
];
Theme {
name: Some("mindfork".to_string()),
author: None,
settings,
scopes,
}
}
pub(super) fn gray(v: u8) -> SynColor {
SynColor {
r: v,
g: v,
b: v,
a: 255,
}
}
pub(super) fn scope_item(selector: &str, color: SynColor) -> ThemeItem {
ThemeItem {
scope: ScopeSelectors::from_str(selector).unwrap_or_default(),
style: StyleModifier {
foreground: Some(color),
background: None,
font_style: None,
},
}
}
pub(super) fn to_syn(color: Color) -> SynColor {
let (r, g, b) = match color {
Color::Rgb(r, g, b) => (r, g, b),
Color::Black => (12, 12, 12),
Color::Red => (197, 15, 31),
Color::Green => (19, 161, 14),
Color::Yellow => (193, 156, 0),
Color::Blue => (0, 55, 218),
Color::Magenta => (136, 23, 152),
Color::Cyan => (58, 150, 221),
Color::Gray => (204, 204, 204),
Color::DarkGray => (118, 118, 118),
Color::LightRed => (231, 72, 86),
Color::LightGreen => (22, 198, 12),
Color::LightYellow => (249, 241, 165),
Color::LightBlue => (59, 120, 255),
Color::LightMagenta => (180, 0, 158),
Color::LightCyan => (97, 214, 214),
Color::White => (242, 242, 242),
Color::Indexed(_) | Color::Reset => (204, 204, 204),
};
SynColor { r, g, b, a: 255 }
}
#[cfg(test)]
mod tests {
use super::super::testkit::*;
use super::*;
use crate::shared::config::Theme;
#[test]
fn language_aliases_resolve_to_syntax() {
for (label, expect_name) in [
("rust", "Rust"),
("csharp", "C#"),
("c#", "C#"),
("CSharp", "C#"),
("cs", "C#"),
("cpp", "C++"),
("c++", "C++"),
("golang", "Go"),
("objc", "Objective-C"),
("objective-c++", "Objective-C++"),
("python3", "Python"),
("nodejs", "JavaScript"),
("shell", "Bourne Again Shell (bash)"),
("yml", "YAML"),
("docker", "Dockerfile"),
("pwsh", "PowerShell"),
("hcl", "Terraform"),
("proto3", "Protocol Buffer"),
("jsonc", "JSON"),
("json5", "JSON"),
("jsx", "JavaScript"),
("tsx", "TypeScript"),
("v", "Go"),
("vlang", "Go"),
] {
let syntax = resolve_syntax(label)
.unwrap_or_else(|| panic!("label {label:?} doesn't resolve to a syntax"));
assert_eq!(syntax.name, expect_name, "label {label:?}");
}
}
#[test]
fn vendored_grammars_resolve_by_their_own_label() {
for (label, expect_name) in [
("zig", "Zig"),
("Zig", "Zig"),
("typescript", "TypeScript"),
("ts", "TypeScript"),
("toml", "TOML"),
("dockerfile", "Dockerfile"),
("powershell", "PowerShell"),
("ps1", "PowerShell"),
("swift", "Swift"),
("kotlin", "Kotlin"),
("kt", "Kotlin"),
("scss", "SCSS"),
("sass", "Sass"),
("graphql", "GraphQL"),
("terraform", "Terraform"),
("tf", "Terraform"),
("elixir", "Elixir"),
("ex", "Elixir"),
("solidity", "Solidity"),
("julia", "Julia"),
("jl", "Julia"),
("nix", "Nix"),
("dart", "Dart"),
("protobuf", "Protocol Buffer"),
("proto", "Protocol Buffer"),
("cmake", "CMake"),
("nginx", "nginx"),
("vue", "Vue Component"),
("svelte", "Svelte"),
("nim", "Nim"),
] {
let syntax = resolve_syntax(label)
.unwrap_or_else(|| panic!("label {label:?} doesn't resolve to a syntax"));
assert_eq!(syntax.name, expect_name, "label {label:?}");
}
}
#[test]
fn every_syntax_can_highlight_without_panicking() {
const SNIPPET: &str = "<div class=\"a\">{{ x }}</div>\n\
<script>const a = 1; // note\n</script>\n\
<style>.a { color: red; }</style>\n\
fn main() { let s = \"текст\"; }\n";
let theme = code_theme(&Palette::for_theme(Theme::Dark));
for syntax in SYNTAX_SET.syntaxes() {
let mut hl = HighlightLines::new(syntax, theme);
for line in LinesWithEndings::from(SNIPPET) {
let _ = hl.highlight_line(line, &SYNTAX_SET);
}
}
}
#[test]
fn dump_carries_the_vendored_grammars() {
let vendored = std::fs::read_dir(concat!(env!("CARGO_MANIFEST_DIR"), "/syntaxes"))
.expect("the syntaxes/ directory")
.filter_map(|e| e.ok().map(|e| e.path()))
.filter(|p| p.extension().is_some_and(|e| e == "sublime-syntax"))
.count();
assert!(
vendored >= 22,
"expected the vendored grammars, got {vendored}"
);
assert_eq!(
SYNTAX_SET.syntaxes().len(),
75 + vendored,
"the dump should carry syntect's 75 bundled syntaxes plus every vendored one"
);
}
#[test]
fn empty_and_unknown_language_do_not_resolve() {
assert!(resolve_syntax("").is_none());
assert!(resolve_syntax("совсем-не-язык-42").is_none());
}
#[test]
fn code_highlight_is_colored() {
let colors = fg_colors(CODE_MD, &Palette::for_theme(Theme::Dark));
assert!(
colors.iter().any(|c| matches!(c, Color::Rgb(..))),
"expected RGB code-highlight colors"
);
}
#[test]
fn code_highlight_follows_theme() {
let dark = fg_colors(CODE_MD, &Palette::for_theme(Theme::Dark));
let light = fg_colors(CODE_MD, &Palette::for_theme(Theme::Light));
assert_ne!(dark, light, "code highlighting doesn't depend on the theme");
}
#[test]
fn code_highlight_follows_the_detected_background_under_auto() {
use crate::shared::osc11::Background;
let detected_light = fg_colors(CODE_MD, &Palette::auto_with(Some(Background::Light)));
let fallback = fg_colors(CODE_MD, &Palette::auto_with(None));
assert_ne!(
detected_light, fallback,
"Auto's code highlighting must follow the detected background"
);
assert!(
detected_light.contains(&Color::Rgb(40, 40, 40))
&& detected_light.contains(&Color::Rgb(110, 110, 110)),
"on a light terminal the greys are the ones tuned for light: {detected_light:?}"
);
assert!(
fallback.contains(&Color::Rgb(212, 212, 212))
&& fallback.contains(&Color::Rgb(128, 128, 128)),
"with nothing detected the greys stay the dark ones: {fallback:?}"
);
}
#[test]
fn highlight_code_has_no_fences_and_is_colored() {
let lines = highlight_code("fn main() {}", "rust", &Palette::for_theme(Theme::Dark));
let joined: String = lines
.iter()
.flat_map(|l| l.spans.iter().map(|s| s.content.as_ref()))
.collect();
assert!(joined.contains("fn main"), "code content: {joined}");
assert!(
!joined.contains("```"),
"there should be no fences: {joined}"
);
assert!(
lines
.iter()
.flat_map(|l| l.spans.iter())
.any(|s| matches!(s.style.fg, Some(Color::Rgb(..)))),
"expected RGB highlighting"
);
}
#[test]
fn highlight_code_unknown_lang_falls_back_to_plain() {
let lines = highlight_code("a\nb", "нет-такого-языка", &Palette::default());
assert_eq!(lines.len(), 2);
let joined: String = lines
.iter()
.flat_map(|l| l.spans.iter().map(|s| s.content.as_ref()))
.collect();
assert_eq!(joined, "ab");
}
#[test]
fn plain_code_block_content_not_glued_to_fence() {
let md = "```\nX_ij = 1, тест\nE = 2/(j-i+1)\n```";
let lines: Vec<String> = block_rows(md, 80)
.iter()
.map(|l| l.trim_end().to_string())
.collect();
assert_eq!(lines[0], "```", "content glued to the fence: {lines:?}");
assert_eq!(lines[1], "X_ij = 1, тест");
assert_eq!(lines[2], "E = 2/(j-i+1)");
assert_eq!(lines[3], "```");
}
fn block_rows(md: &str, width: usize) -> Vec<String> {
render(md, width, &Palette::default())
.lines
.iter()
.map(|l| l.spans.iter().map(|s| s.content.as_ref()).collect())
.collect()
}
fn block_widths(md: &str, width: usize) -> Vec<usize> {
render(md, width, &Palette::default())
.lines
.iter()
.map(|l| cell_width(&l.spans))
.collect()
}
#[test]
fn plain_code_block_is_a_solid_rectangle() {
let md = "```text\nкороткая\nсамая длинная строка блока\nx\n```";
let widths = block_widths(md, 80);
let expected =
wrap::display_width(&"самая длинная строка блока".chars().collect::<Vec<_>>())
+ CODE_RIGHT_PAD;
assert!(
widths.iter().all(|&w| w == expected),
"rows are not one width ({expected} expected): {widths:?}"
);
assert!(expected < 80, "the block should not fill the panel");
for line in render(md, 80, &Palette::default()).lines {
assert!(
line.style.add_modifier.contains(Modifier::REVERSED),
"a block row lost its background: {line:?}"
);
}
}
#[test]
fn blank_line_inside_a_code_block_is_filled() {
let widths = block_widths("```\naaaa bbbb\n\ncccc\n```", 80);
let expected = 9 + CODE_RIGHT_PAD;
assert!(
widths.iter().all(|&w| w == expected),
"a blank row broke the rectangle: {widths:?}"
);
}
#[test]
fn rectangle_keeps_a_blank_column_on_the_right() {
let longest = "самая длинная строка блока";
let natural = wrap::display_width(&longest.chars().collect::<Vec<_>>());
for w in [80usize, natural] {
let rows = block_rows(&format!("```text\nx\n{longest}\n```"), w);
for row in &rows {
assert!(
row.ends_with(' '),
"the right edge has no blank column at panel {w}: {rows:?}"
);
}
}
let rows = block_rows(&format!("```text\nx\n{longest}\n```"), 80);
let widest = rows
.iter()
.find(|r| r.contains(longest))
.expect("the longest line");
assert_eq!(
widest.chars().rev().take_while(|c| *c == ' ').count(),
CODE_RIGHT_PAD,
"expected exactly one blank column: {widest:?}"
);
}
#[test]
fn long_code_line_wraps_into_the_rectangle() {
let md = format!("```\n{}\n```", "слово ".repeat(30));
for w in [20usize, 32, 40, 60] {
let widths = block_widths(&md, w);
let first = widths[0];
assert!(
first <= w && widths.iter().all(|&x| x == first),
"at panel {w} the rectangle came out ragged: {widths:?}"
);
assert!(widths.len() > 3, "the long line did not wrap: {widths:?}");
}
}
#[test]
fn rectangle_padding_is_not_dimmed() {
let md = "```text\nсамая длинная строка блока\n```";
let fence = render(md, 80, &Palette::default()).lines.remove(0);
let text = &fence.spans[0];
let pad = fence.spans.last().expect("the fence row is padded");
assert!(text.content.starts_with("```"));
assert!(
text.style.add_modifier.contains(Modifier::DIM),
"the fence text should stay dim: {text:?}"
);
assert!(
pad.content.trim().is_empty() && !pad.style.add_modifier.contains(Modifier::DIM),
"the padding must carry the plain background: {pad:?}"
);
}
#[test]
fn zig_block_is_highlighted() {
let md = "```zig\nconst memory = try allocator.alloc(u8, 1024);\n```";
let lines = render(md, 80, &Palette::for_theme(Theme::Dark)).lines;
assert!(
lines
.iter()
.flat_map(|l| l.spans.iter())
.any(|s| matches!(s.style.fg, Some(Color::Rgb(..)))),
"the zig block is not highlighted: {lines:?}"
);
assert!(
!lines
.iter()
.any(|l| l.style.add_modifier.contains(Modifier::REVERSED)),
"the zig block went down the unhighlighted path: {lines:?}"
);
}
#[test]
fn highlighted_block_is_left_ragged() {
let widths = block_widths("```rust\nfn main() {\n let x = 1;\n}\n```", 80);
assert!(
widths
.iter()
.collect::<std::collections::HashSet<_>>()
.len()
> 1,
"a highlighted block should keep its natural row widths: {widths:?}"
);
}
}