run/
highlight.rs

1use once_cell::sync::Lazy;
2use syntect::easy::HighlightLines;
3use syntect::highlighting::ThemeSet;
4use syntect::parsing::{SyntaxReference, SyntaxSet};
5use syntect::util::{LinesWithEndings, as_24_bit_terminal_escaped};
6
7static SYNTAX_SET: Lazy<SyntaxSet> = Lazy::new(SyntaxSet::load_defaults_newlines);
8
9static THEME_SET: Lazy<ThemeSet> = Lazy::new(ThemeSet::load_defaults);
10
11fn supports_color() -> bool {
12    std::env::var("NO_COLOR").is_err()
13        && (std::env::var("TERM").unwrap_or_default().contains("color")
14            || std::env::var("COLORTERM").is_ok())
15}
16
17fn language_to_syntax_name(language_id: &str) -> &str {
18    match language_id.to_ascii_lowercase().as_str() {
19        "python" | "py" | "python3" | "py3" => "Python",
20        "javascript" | "js" | "node" | "nodejs" => "JavaScript",
21        "typescript" | "ts" => "JavaScript",
22        "rust" | "rs" => "Rust",
23        "go" | "golang" => "Go",
24        "c" => "C",
25        "cpp" | "c++" | "cxx" => "C++",
26        "java" => "Java",
27        "csharp" | "cs" | "c#" => "C#",
28        "ruby" | "rb" => "Ruby",
29        "php" => "PHP",
30        "bash" | "sh" | "shell" | "zsh" => "Bourne Again Shell (bash)",
31        "lua" => "Lua",
32        "perl" | "pl" => "Perl",
33        "swift" => "Swift",
34        "kotlin" | "kt" => "Kotlin",
35        "r" | "rscript" => "R",
36        "haskell" | "hs" => "Haskell",
37        "julia" | "jl" => "Plain Text",
38        "elixir" | "ex" | "exs" => "Plain Text",
39        "dart" => "Dart",
40        "groovy" | "grv" => "Groovy",
41        "crystal" | "cr" => "Crystal",
42        "zig" => "Zig",
43        "nim" => "Nim",
44        _ => "Plain Text",
45    }
46}
47
48fn get_syntax_for_language(language_id: &str) -> &'static SyntaxReference {
49    let syntax_name = language_to_syntax_name(language_id);
50    SYNTAX_SET
51        .find_syntax_by_name(syntax_name)
52        .unwrap_or_else(|| SYNTAX_SET.find_syntax_plain_text())
53}
54
55pub fn highlight_code(code: &str, language_id: &str) -> String {
56    if !supports_color() {
57        return code.to_string();
58    }
59
60    let syntax = get_syntax_for_language(language_id);
61
62    let theme = &THEME_SET.themes["base16-ocean.dark"];
63
64    let mut highlighter = HighlightLines::new(syntax, theme);
65    let mut output = String::new();
66
67    for line in LinesWithEndings::from(code) {
68        let ranges = highlighter
69            .highlight_line(line, &SYNTAX_SET)
70            .unwrap_or_default();
71        let escaped = as_24_bit_terminal_escaped(&ranges[..], false);
72        output.push_str(&escaped);
73    }
74
75    // reset colors at the end to prevent bleeding into prompt
76    if !output.is_empty() && !output.ends_with("\x1b[0m") {
77        output.push_str("\x1b[0m");
78    }
79
80    output
81}
82
83pub fn highlight_repl_input(code: &str, language_id: &str) -> String {
84    highlight_code(code, language_id)
85}
86
87pub fn highlight_output(code: &str, language_id: &str) -> String {
88    highlight_code(code, language_id)
89}
90
91pub fn has_syntax_support(language_id: &str) -> bool {
92    let syntax_name = language_to_syntax_name(language_id);
93    SYNTAX_SET.find_syntax_by_name(syntax_name).is_some()
94}
95
96#[cfg(test)]
97mod tests {
98    use super::*;
99
100    #[test]
101    fn test_language_mapping() {
102        assert_eq!(language_to_syntax_name("python"), "Python");
103        assert_eq!(language_to_syntax_name("rust"), "Rust");
104        assert_eq!(language_to_syntax_name("javascript"), "JavaScript");
105        assert_eq!(language_to_syntax_name("typescript"), "JavaScript");
106        assert_eq!(language_to_syntax_name("go"), "Go");
107        assert_eq!(language_to_syntax_name("java"), "Java");
108        assert_eq!(language_to_syntax_name("csharp"), "C#");
109        assert_eq!(language_to_syntax_name("cpp"), "C++");
110        assert_eq!(language_to_syntax_name("ruby"), "Ruby");
111        assert_eq!(language_to_syntax_name("php"), "PHP");
112    }
113
114    #[test]
115    fn test_all_language_aliases() {
116        assert_eq!(language_to_syntax_name("py"), "Python");
117        assert_eq!(language_to_syntax_name("py3"), "Python");
118        assert_eq!(language_to_syntax_name("python3"), "Python");
119
120        assert_eq!(language_to_syntax_name("js"), "JavaScript");
121        assert_eq!(language_to_syntax_name("node"), "JavaScript");
122        assert_eq!(language_to_syntax_name("nodejs"), "JavaScript");
123
124        assert_eq!(language_to_syntax_name("ts"), "JavaScript");
125
126        assert_eq!(language_to_syntax_name("rs"), "Rust");
127
128        assert_eq!(language_to_syntax_name("golang"), "Go");
129
130        assert_eq!(language_to_syntax_name("c++"), "C++");
131        assert_eq!(language_to_syntax_name("cxx"), "C++");
132
133        assert_eq!(language_to_syntax_name("cs"), "C#");
134        assert_eq!(language_to_syntax_name("c#"), "C#");
135    }
136
137    #[test]
138    fn test_all_languages_have_syntax() {
139        let supported = vec![
140            "python",
141            "javascript",
142            "rust",
143            "go",
144            "c",
145            "cpp",
146            "java",
147            "csharp",
148            "ruby",
149            "php",
150            "bash",
151            "lua",
152            "perl",
153            "r",
154            "haskell",
155        ];
156
157        for lang in supported {
158            assert!(
159                has_syntax_support(lang),
160                "Language {} should be supported",
161                lang
162            );
163        }
164
165        let fallback = vec![
166            "swift", "kotlin", "dart", "groovy", "crystal", "zig", "nim", "julia", "elixir",
167        ];
168        for lang in fallback {
169            let _ = highlight_code("test", lang);
170        }
171    }
172
173    #[test]
174    fn test_unknown_language_fallback() {
175        let syntax_name = language_to_syntax_name("unknownlang123");
176        assert_eq!(syntax_name, "Plain Text");
177    }
178
179    #[test]
180    fn test_syntax_available() {
181        assert!(has_syntax_support("python"));
182        assert!(has_syntax_support("rust"));
183        assert!(has_syntax_support("go"));
184        assert!(has_syntax_support("javascript"));
185        assert!(has_syntax_support("typescript"));
186    }
187
188    #[test]
189    fn test_highlight_basic() {
190        let code = "fn main() { println!(\"Hello\"); }";
191        let highlighted = highlight_code(code, "rust");
192        assert!(!highlighted.is_empty());
193        assert!(highlighted.len() >= code.len());
194    }
195
196    #[test]
197    fn test_highlight_python() {
198        let code = "def hello():\n    print('world')";
199        let highlighted = highlight_code(code, "python");
200        assert!(!highlighted.is_empty());
201    }
202
203    #[test]
204    fn test_highlight_javascript() {
205        let code = "function hello() { console.log('world'); }";
206        let highlighted = highlight_code(code, "javascript");
207        assert!(!highlighted.is_empty());
208    }
209
210    #[test]
211    fn test_highlight_go() {
212        let code = "package main\nfunc main() { fmt.Println(\"hello\") }";
213        let highlighted = highlight_code(code, "go");
214        assert!(!highlighted.is_empty());
215    }
216
217    #[test]
218    fn test_empty_code() {
219        let highlighted = highlight_code("", "python");
220        assert!(highlighted.is_empty() || highlighted == "\x1b[0m");
221    }
222
223    #[test]
224    fn test_whitespace_only() {
225        let code = "   \n  \t  ";
226        let highlighted = highlight_code(code, "rust");
227        assert!(!highlighted.is_empty());
228    }
229
230    #[test]
231    fn test_multiline_code() {
232        let code = "fn main() {\n    let x = 10;\n    println!(\"{}\", x);\n}";
233        let highlighted = highlight_code(code, "rust");
234        assert!(!highlighted.is_empty());
235        assert!(highlighted.contains('\n') || highlighted.contains("\\n"));
236    }
237
238    #[test]
239    fn test_color_reset_at_end() {
240        unsafe {
241            std::env::set_var("TERM", "xterm-256color");
242        }
243
244        let code = "x = 10";
245        let highlighted = highlight_code(code, "python");
246
247        assert!(
248            highlighted.ends_with("\x1b[0m") || !highlighted.contains("\x1b["),
249            "Highlighted code should end with color reset or be plain text"
250        );
251    }
252
253    #[test]
254    fn test_no_color_environment() {
255        unsafe {
256            std::env::set_var("NO_COLOR", "1");
257        }
258
259        let code = "fn main() {}";
260        let highlighted = highlight_code(code, "rust");
261
262        assert_eq!(highlighted, code);
263
264        unsafe {
265            std::env::remove_var("NO_COLOR");
266        }
267    }
268
269    #[test]
270    fn test_special_characters() {
271        let code = "print(\"Hello\\nWorld\\t!\")";
272        let highlighted = highlight_code(code, "python");
273        assert!(!highlighted.is_empty());
274    }
275
276    #[test]
277    fn test_unicode_characters() {
278        let code = "message = \"Hello δΈ–η•Œ 🌍\"";
279        let highlighted = highlight_code(code, "python");
280        assert!(!highlighted.is_empty());
281        assert!(highlighted.contains("δΈ–η•Œ") || highlighted.contains("\\u"));
282    }
283
284    #[test]
285    fn test_repl_input_helper() {
286        let code = "x = 42";
287        let highlighted = highlight_repl_input(code, "python");
288        assert!(!highlighted.is_empty());
289    }
290
291    #[test]
292    fn test_output_helper() {
293        let code = "console.log('test')";
294        let highlighted = highlight_output(code, "javascript");
295        assert!(!highlighted.is_empty());
296    }
297
298    #[test]
299    fn test_bash_highlighting() {
300        let code = "echo \"Hello World\"";
301        let highlighted = highlight_code(code, "bash");
302        assert!(!highlighted.is_empty());
303    }
304
305    #[test]
306    fn test_sql_like_code_in_supported_language() {
307        let code = "query = \"SELECT * FROM users\"";
308        let highlighted = highlight_code(code, "python");
309        assert!(!highlighted.is_empty());
310    }
311
312    #[test]
313    fn test_comments_highlighting() {
314        let code = "// This is a comment\nlet x = 10;";
315        let highlighted = highlight_code(code, "javascript");
316        assert!(!highlighted.is_empty());
317    }
318
319    #[test]
320    fn test_string_highlighting() {
321        let code = "\"This is a string\"";
322        let highlighted = highlight_code(code, "python");
323        assert!(!highlighted.is_empty());
324    }
325
326    #[test]
327    fn test_number_highlighting() {
328        let code = "let num = 42;";
329        let highlighted = highlight_code(code, "javascript");
330        assert!(!highlighted.is_empty());
331    }
332
333    #[test]
334    fn test_very_long_code() {
335        let mut code = String::new();
336        for i in 0..1000 {
337            code.push_str(&format!("let var{} = {};\n", i, i));
338        }
339        let highlighted = highlight_code(&code, "javascript");
340        assert!(!highlighted.is_empty());
341    }
342
343    #[test]
344    fn test_case_insensitive_language_names() {
345        assert_eq!(language_to_syntax_name("PYTHON"), "Python");
346        assert_eq!(language_to_syntax_name("Python"), "Python");
347        assert_eq!(language_to_syntax_name("RuSt"), "Rust");
348        assert_eq!(language_to_syntax_name("JavaScript"), "JavaScript");
349    }
350
351    #[test]
352    fn test_syntax_reference_caching() {
353        let syntax1 = get_syntax_for_language("python");
354        let syntax2 = get_syntax_for_language("python");
355        assert_eq!(syntax1.name, syntax2.name);
356    }
357
358    #[test]
359    fn test_all_functional_languages() {
360        let langs = vec!["haskell", "elixir", "julia"];
361        for lang in langs {
362            let code = "main = print \"hello\"";
363            let highlighted = highlight_code(code, lang);
364            assert!(!highlighted.is_empty(), "Failed for {}", lang);
365        }
366    }
367
368    #[test]
369    fn test_systems_languages() {
370        let langs = vec!["c", "cpp", "rust", "zig", "nim"];
371        for lang in langs {
372            let code = "int main() { return 0; }";
373            let highlighted = highlight_code(code, lang);
374            assert!(!highlighted.is_empty(), "Failed for {}", lang);
375        }
376    }
377
378    #[test]
379    fn test_scripting_languages() {
380        let langs = vec!["python", "ruby", "perl", "lua", "php"];
381        for lang in langs {
382            let code = "print('hello')";
383            let highlighted = highlight_code(code, lang);
384            assert!(!highlighted.is_empty(), "Failed for {}", lang);
385        }
386    }
387
388    #[test]
389    fn test_jvm_languages() {
390        let langs = vec!["java", "kotlin", "groovy"];
391        for lang in langs {
392            let code = "public class Test { }";
393            let highlighted = highlight_code(code, lang);
394            assert!(!highlighted.is_empty(), "Failed for {}", lang);
395        }
396    }
397
398    #[test]
399    fn test_ansi_codes_present_when_colors_enabled() {
400        unsafe {
401            std::env::set_var("TERM", "xterm-256color");
402            std::env::remove_var("NO_COLOR");
403        }
404
405        let code = "fn main() {}";
406        let highlighted = highlight_code(code, "rust");
407
408        let has_ansi = highlighted.contains("\x1b[") || highlighted == code;
409        assert!(has_ansi, "Should contain ANSI codes or be plain text");
410    }
411
412    #[test]
413    fn test_typescript_mapping() {
414        assert_eq!(language_to_syntax_name("typescript"), "JavaScript");
415        let code = "const x: number = 42;";
416        let highlighted = highlight_code(code, "typescript");
417        assert!(!highlighted.is_empty());
418    }
419
420    #[test]
421    fn test_swift_language() {
422        let code = "func main() { print(\"Hello\") }";
423        let highlighted = highlight_code(code, "swift");
424        assert!(!highlighted.is_empty());
425    }
426
427    #[test]
428    fn test_kotlin_language() {
429        let code = "fun main() { println(\"Hello\") }";
430        let highlighted = highlight_code(code, "kotlin");
431        assert!(!highlighted.is_empty());
432    }
433
434    #[test]
435    fn test_dart_language() {
436        let code = "void main() { print('Hello'); }";
437        let highlighted = highlight_code(code, "dart");
438        assert!(!highlighted.is_empty());
439    }
440
441    #[test]
442    fn test_r_language() {
443        let code = "x <- 10\nprint(x)";
444        let highlighted = highlight_code(code, "r");
445        assert!(!highlighted.is_empty());
446    }
447
448    #[test]
449    fn test_crystal_language() {
450        let code = "puts \"Hello\"";
451        let highlighted = highlight_code(code, "crystal");
452        assert!(!highlighted.is_empty());
453    }
454
455    #[test]
456    fn test_zig_language() {
457        let code = "pub fn main() void {}";
458        let highlighted = highlight_code(code, "zig");
459        assert!(!highlighted.is_empty());
460    }
461
462    #[test]
463    fn test_nim_language() {
464        let code = "echo \"Hello\"";
465        let highlighted = highlight_code(code, "nim");
466        assert!(!highlighted.is_empty());
467    }
468
469    #[test]
470    fn test_only_comments() {
471        let code = "// Just a comment\n// Another comment";
472        let highlighted = highlight_code(code, "rust");
473        assert!(!highlighted.is_empty());
474    }
475
476    #[test]
477    fn test_code_with_syntax_errors() {
478        let code = "fn main( { println! }";
479        let highlighted = highlight_code(code, "rust");
480        assert!(!highlighted.is_empty());
481    }
482
483    #[test]
484    fn test_mixed_quotes() {
485        let code = r#"s1 = 'single'; s2 = "double""#;
486        let highlighted = highlight_code(code, "python");
487        assert!(!highlighted.is_empty());
488    }
489
490    #[test]
491    fn test_nested_structures() {
492        let code = "let arr = [[1, 2], [3, 4], [5, 6]];";
493        let highlighted = highlight_code(code, "javascript");
494        assert!(!highlighted.is_empty());
495    }
496
497    #[test]
498    fn test_regex_patterns() {
499        let code = r"pattern = /[a-z]+/g";
500        let highlighted = highlight_code(code, "javascript");
501        assert!(!highlighted.is_empty());
502    }
503
504    #[test]
505    fn test_multiple_statements_one_line() {
506        let code = "x = 1; y = 2; z = 3;";
507        let highlighted = highlight_code(code, "javascript");
508        assert!(!highlighted.is_empty());
509    }
510
511    #[test]
512    fn test_color_reset_prevents_bleeding() {
513        unsafe {
514            std::env::set_var("TERM", "xterm-256color");
515            std::env::remove_var("NO_COLOR");
516        }
517
518        let code1 = "x = 20";
519        let highlighted1 = highlight_code(code1, "javascript");
520
521        if highlighted1.contains("\x1b[") {
522            assert!(
523                highlighted1.ends_with("\x1b[0m"),
524                "Highlighted code must end with reset code to prevent color bleeding"
525            );
526        }
527    }
528
529    #[test]
530    fn test_all_25_languages_work() {
531        let languages = vec![
532            "python",
533            "javascript",
534            "typescript",
535            "rust",
536            "go",
537            "c",
538            "cpp",
539            "java",
540            "csharp",
541            "ruby",
542            "php",
543            "bash",
544            "lua",
545            "perl",
546            "swift",
547            "kotlin",
548            "r",
549            "dart",
550            "haskell",
551            "julia",
552            "elixir",
553            "groovy",
554            "crystal",
555            "zig",
556            "nim",
557        ];
558
559        for lang in languages {
560            let code = "test code";
561            let highlighted = highlight_code(code, lang);
562            assert!(
563                !highlighted.is_empty(),
564                "Language {} failed to highlight",
565                lang
566            );
567        }
568    }
569
570    #[test]
571    fn test_bash_aliases() {
572        let aliases = vec!["bash", "sh", "shell", "zsh"];
573        for alias in aliases {
574            assert_eq!(
575                language_to_syntax_name(alias),
576                "Bourne Again Shell (bash)",
577                "Bash alias {} failed",
578                alias
579            );
580        }
581    }
582
583    #[test]
584    fn test_empty_lines_in_code() {
585        let code = "fn main() {\n\n\n    println!(\"test\");\n\n}";
586        let highlighted = highlight_code(code, "rust");
587        assert!(!highlighted.is_empty());
588
589        assert!(highlighted.matches('\n').count() >= 4);
590    }
591
592    #[test]
593    fn test_tabs_and_spaces() {
594        let code = "\tfn main() {\n\t\tprintln!(\"test\");\n\t}";
595        let highlighted = highlight_code(code, "rust");
596        assert!(!highlighted.is_empty());
597    }
598
599    #[test]
600    fn test_colorterm_environment() {
601        unsafe {
602            std::env::set_var("COLORTERM", "truecolor");
603            std::env::remove_var("NO_COLOR");
604        }
605
606        assert!(supports_color());
607
608        unsafe {
609            std::env::remove_var("COLORTERM");
610        }
611    }
612
613    #[test]
614    fn test_repl_helpers_consistency() {
615        let code = "test = 42";
616        let h1 = highlight_code(code, "python");
617        let h2 = highlight_repl_input(code, "python");
618        let h3 = highlight_output(code, "python");
619
620        assert_eq!(h1, h2);
621        assert_eq!(h2, h3);
622    }
623}