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 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}