1use super::super::theme::Theme;
2use ratatui::{
3 style::{Modifier, Style},
4 text::Span,
5};
6
7pub fn highlight_code_line<'a>(line: &'a str, lang: &str, theme: &Theme) -> Vec<Span<'static>> {
10 let lang_lower = lang.to_lowercase();
11 let keywords: &[&str] = match lang_lower.as_str() {
17 "rust" | "rs" => &[
18 "fn", "let", "mut", "pub", "use", "mod", "struct", "enum", "impl", "trait", "for",
20 "while", "loop", "if", "else", "match", "return", "self", "Self", "where", "async",
21 "await", "move", "ref", "type", "const", "static", "crate", "super", "as", "in",
22 "true", "false", "unsafe", "extern", "dyn", "abstract", "become", "box", "do", "final",
23 "macro", "override", "priv", "typeof", "unsized", "virtual", "yield", "union", "break",
24 "continue",
25 ],
26 "python" | "py" => &[
27 "def", "class", "return", "if", "elif", "else", "for", "while", "import", "from", "as",
28 "with", "try", "except", "finally", "raise", "pass", "break", "continue", "yield",
29 "lambda", "and", "or", "not", "in", "is", "True", "False", "None", "global",
30 "nonlocal", "assert", "del", "async", "await", "self", "print",
31 ],
32 "javascript" | "js" | "typescript" | "ts" | "jsx" | "tsx" => &[
33 "function",
34 "const",
35 "let",
36 "var",
37 "return",
38 "if",
39 "else",
40 "for",
41 "while",
42 "class",
43 "new",
44 "this",
45 "import",
46 "export",
47 "from",
48 "default",
49 "async",
50 "await",
51 "try",
52 "catch",
53 "finally",
54 "throw",
55 "typeof",
56 "instanceof",
57 "true",
58 "false",
59 "null",
60 "undefined",
61 "of",
62 "in",
63 "switch",
64 "case",
65 ],
66 "go" | "golang" => &[
67 "func",
68 "package",
69 "import",
70 "return",
71 "if",
72 "else",
73 "for",
74 "range",
75 "struct",
76 "interface",
77 "type",
78 "var",
79 "const",
80 "defer",
81 "go",
82 "chan",
83 "select",
84 "case",
85 "switch",
86 "default",
87 "break",
88 "continue",
89 "map",
90 "true",
91 "false",
92 "nil",
93 "make",
94 "append",
95 "len",
96 "cap",
97 ],
98 "java" | "kotlin" | "kt" => &[
99 "public",
100 "private",
101 "protected",
102 "class",
103 "interface",
104 "extends",
105 "implements",
106 "return",
107 "if",
108 "else",
109 "for",
110 "while",
111 "new",
112 "this",
113 "import",
114 "package",
115 "static",
116 "final",
117 "void",
118 "int",
119 "String",
120 "boolean",
121 "true",
122 "false",
123 "null",
124 "try",
125 "catch",
126 "throw",
127 "throws",
128 "fun",
129 "val",
130 "var",
131 "when",
132 "object",
133 "companion",
134 ],
135 "sh" | "bash" | "zsh" | "shell" => &[
136 "if",
137 "then",
138 "else",
139 "elif",
140 "fi",
141 "for",
142 "while",
143 "do",
144 "done",
145 "case",
146 "esac",
147 "function",
148 "return",
149 "exit",
150 "echo",
151 "export",
152 "local",
153 "readonly",
154 "set",
155 "unset",
156 "shift",
157 "source",
158 "in",
159 "true",
160 "false",
161 "read",
162 "declare",
163 "typeset",
164 "trap",
165 "eval",
166 "exec",
167 "test",
168 "select",
169 "until",
170 "break",
171 "continue",
172 "printf",
173 "go",
175 "build",
176 "run",
177 "test",
178 "fmt",
179 "vet",
180 "mod",
181 "get",
182 "install",
183 "clean",
184 "doc",
185 "list",
186 "version",
187 "env",
188 "generate",
189 "tool",
190 "proxy",
191 "GOPATH",
192 "GOROOT",
193 "GOBIN",
194 "GOMODCACHE",
195 "GOPROXY",
196 "GOSUMDB",
197 "cargo",
199 "new",
200 "init",
201 "add",
202 "remove",
203 "update",
204 "check",
205 "clippy",
206 "rustfmt",
207 "rustc",
208 "rustup",
209 "publish",
210 "uninstall",
211 "search",
212 "tree",
213 "locate_project",
214 "metadata",
215 "audit",
216 "watch",
217 "expand",
218 ],
219 "c" | "cpp" | "c++" | "h" | "hpp" => &[
220 "int",
221 "char",
222 "float",
223 "double",
224 "void",
225 "long",
226 "short",
227 "unsigned",
228 "signed",
229 "const",
230 "static",
231 "extern",
232 "struct",
233 "union",
234 "enum",
235 "typedef",
236 "sizeof",
237 "return",
238 "if",
239 "else",
240 "for",
241 "while",
242 "do",
243 "switch",
244 "case",
245 "break",
246 "continue",
247 "default",
248 "goto",
249 "auto",
250 "register",
251 "volatile",
252 "class",
253 "public",
254 "private",
255 "protected",
256 "virtual",
257 "override",
258 "template",
259 "namespace",
260 "using",
261 "new",
262 "delete",
263 "try",
264 "catch",
265 "throw",
266 "nullptr",
267 "true",
268 "false",
269 "this",
270 "include",
271 "define",
272 "ifdef",
273 "ifndef",
274 "endif",
275 ],
276 "sql" => &[
277 "SELECT",
278 "FROM",
279 "WHERE",
280 "INSERT",
281 "UPDATE",
282 "DELETE",
283 "CREATE",
284 "DROP",
285 "ALTER",
286 "TABLE",
287 "INDEX",
288 "INTO",
289 "VALUES",
290 "SET",
291 "AND",
292 "OR",
293 "NOT",
294 "NULL",
295 "JOIN",
296 "LEFT",
297 "RIGHT",
298 "INNER",
299 "OUTER",
300 "ON",
301 "GROUP",
302 "BY",
303 "ORDER",
304 "ASC",
305 "DESC",
306 "HAVING",
307 "LIMIT",
308 "OFFSET",
309 "UNION",
310 "AS",
311 "DISTINCT",
312 "COUNT",
313 "SUM",
314 "AVG",
315 "MIN",
316 "MAX",
317 "LIKE",
318 "IN",
319 "BETWEEN",
320 "EXISTS",
321 "CASE",
322 "WHEN",
323 "THEN",
324 "ELSE",
325 "END",
326 "BEGIN",
327 "COMMIT",
328 "ROLLBACK",
329 "PRIMARY",
330 "KEY",
331 "FOREIGN",
332 "REFERENCES",
333 "select",
334 "from",
335 "where",
336 "insert",
337 "update",
338 "delete",
339 "create",
340 "drop",
341 "alter",
342 "table",
343 "index",
344 "into",
345 "values",
346 "set",
347 "and",
348 "or",
349 "not",
350 "null",
351 "join",
352 "left",
353 "right",
354 "inner",
355 "outer",
356 "on",
357 "group",
358 "by",
359 "order",
360 "asc",
361 "desc",
362 "having",
363 "limit",
364 "offset",
365 "union",
366 "as",
367 "distinct",
368 "count",
369 "sum",
370 "avg",
371 "min",
372 "max",
373 "like",
374 "in",
375 "between",
376 "exists",
377 "case",
378 "when",
379 "then",
380 "else",
381 "end",
382 "begin",
383 "commit",
384 "rollback",
385 "primary",
386 "key",
387 "foreign",
388 "references",
389 ],
390 "yaml" | "yml" => &["true", "false", "null", "yes", "no", "on", "off"],
391 "toml" => &[
392 "true",
393 "false",
394 "name",
396 "version",
397 "edition",
398 "authors",
399 "dependencies",
400 "dev-dependencies",
401 "build-dependencies",
402 "features",
403 "workspace",
404 "members",
405 "exclude",
406 "include",
407 "path",
408 "git",
409 "branch",
410 "tag",
411 "rev",
412 "package",
413 "lib",
414 "bin",
415 "example",
416 "test",
417 "bench",
418 "doc",
419 "profile",
420 "release",
421 "debug",
422 "opt-level",
423 "lto",
424 "codegen-units",
425 "panic",
426 "strip",
427 "default",
428 "optional",
429 "repository",
431 "homepage",
432 "documentation",
433 "license",
434 "license-file",
435 "keywords",
436 "categories",
437 "readme",
438 "description",
439 "resolver",
440 ],
441 "css" | "scss" | "less" => &[
442 "color",
443 "background",
444 "border",
445 "margin",
446 "padding",
447 "display",
448 "position",
449 "width",
450 "height",
451 "font",
452 "text",
453 "flex",
454 "grid",
455 "align",
456 "justify",
457 "important",
458 "none",
459 "auto",
460 "inherit",
461 "initial",
462 "unset",
463 ],
464 "dockerfile" | "docker" => &[
465 "FROM",
466 "RUN",
467 "CMD",
468 "LABEL",
469 "EXPOSE",
470 "ENV",
471 "ADD",
472 "COPY",
473 "ENTRYPOINT",
474 "VOLUME",
475 "USER",
476 "WORKDIR",
477 "ARG",
478 "ONBUILD",
479 "STOPSIGNAL",
480 "HEALTHCHECK",
481 "SHELL",
482 "AS",
483 ],
484 "ruby" | "rb" => &[
485 "def", "end", "class", "module", "if", "elsif", "else", "unless", "while", "until",
486 "for", "do", "begin", "rescue", "ensure", "raise", "return", "yield", "require",
487 "include", "attr", "self", "true", "false", "nil", "puts", "print",
488 ],
489 _ => &[
490 "fn", "function", "def", "class", "return", "if", "else", "for", "while", "import",
491 "export", "const", "let", "var", "true", "false", "null", "nil", "None", "self",
492 "this",
493 ],
494 };
495
496 let primitive_types: &[&str] = match lang_lower.as_str() {
498 "rust" | "rs" => &[
499 "i8", "i16", "i32", "i64", "i128", "isize", "u8", "u16", "u32", "u64", "u128", "usize",
500 "f32", "f64", "bool", "char", "str",
501 ],
502 "go" | "golang" => &[
503 "int",
504 "int8",
505 "int16",
506 "int32",
507 "int64",
508 "uint",
509 "uint8",
510 "uint16",
511 "uint32",
512 "uint64",
513 "uintptr",
514 "float32",
515 "float64",
516 "complex64",
517 "complex128",
518 "bool",
519 "byte",
520 "rune",
521 "string",
522 "error",
523 "any",
524 ],
525 _ => &[],
526 };
527
528 let go_type_names: &[&str] = match lang_lower.as_str() {
530 "go" | "golang" => &[
531 "Reader",
532 "Writer",
533 "Closer",
534 "ReadWriter",
535 "ReadCloser",
536 "WriteCloser",
537 "ReadWriteCloser",
538 "Seeker",
539 "Context",
540 "Error",
541 "Stringer",
542 "Mutex",
543 "RWMutex",
544 "WaitGroup",
545 "Once",
546 "Pool",
547 "Map",
548 "Duration",
549 "Time",
550 "Timer",
551 "Ticker",
552 "Buffer",
553 "Builder",
554 "Request",
555 "Response",
556 "ResponseWriter",
557 "Handler",
558 "HandlerFunc",
559 "Server",
560 "Client",
561 "Transport",
562 "File",
563 "FileInfo",
564 "FileMode",
565 "Decoder",
566 "Encoder",
567 "Marshaler",
568 "Unmarshaler",
569 "Logger",
570 "Flag",
571 "Regexp",
572 "Conn",
573 "Listener",
574 "Addr",
575 "Scanner",
576 "Token",
577 "Type",
578 "Value",
579 "Kind",
580 "Cmd",
581 "Signal",
582 ],
583 _ => &[],
584 };
585
586 let comment_prefix = match lang_lower.as_str() {
587 "python" | "py" | "sh" | "bash" | "zsh" | "shell" | "ruby" | "rb" | "yaml" | "yml"
588 | "toml" | "dockerfile" | "docker" => "#",
589 "sql" => "--",
590 "css" | "scss" | "less" => "/*",
591 _ => "//",
592 };
593
594 let code_style = Style::default().fg(theme.code_default);
596 let kw_style = Style::default().fg(theme.code_keyword);
597 let str_style = Style::default().fg(theme.code_string);
598 let comment_style = Style::default()
599 .fg(theme.code_comment)
600 .add_modifier(Modifier::ITALIC);
601 let num_style = Style::default().fg(theme.code_number);
602 let type_style = Style::default().fg(theme.code_type);
603 let primitive_style = Style::default().fg(theme.code_primitive);
604 let macro_style = Style::default().fg(theme.code_macro);
605
606 let trimmed = line.trim_start();
607
608 if trimmed.starts_with(comment_prefix) {
610 return vec![Span::styled(line.to_string(), comment_style)];
611 }
612
613 let mut spans = Vec::new();
615 let mut chars = line.chars().peekable();
616 let mut buf = String::new();
617
618 while let Some(&ch) = chars.peek() {
619 if ch == '"' {
621 if !buf.is_empty() {
622 spans.extend(colorize_tokens(
623 &buf,
624 keywords,
625 primitive_types,
626 go_type_names,
627 code_style,
628 kw_style,
629 num_style,
630 type_style,
631 primitive_style,
632 macro_style,
633 &lang_lower,
634 ));
635 buf.clear();
636 }
637 let mut s = String::new();
638 s.push(ch);
639 chars.next();
640 let mut escaped = false;
641 while let Some(&c) = chars.peek() {
642 s.push(c);
643 chars.next();
644 if escaped {
645 escaped = false;
646 continue;
647 }
648 if c == '\\' {
649 escaped = true;
650 continue;
651 }
652 if c == '"' {
653 break;
654 }
655 }
656 spans.push(Span::styled(s, str_style));
657 continue;
658 }
659 if ch == '`' {
661 if !buf.is_empty() {
662 spans.extend(colorize_tokens(
663 &buf,
664 keywords,
665 primitive_types,
666 go_type_names,
667 code_style,
668 kw_style,
669 num_style,
670 type_style,
671 primitive_style,
672 macro_style,
673 &lang_lower,
674 ));
675 buf.clear();
676 }
677 let mut s = String::new();
678 s.push(ch);
679 chars.next();
680 while let Some(&c) = chars.peek() {
681 s.push(c);
682 chars.next();
683 if c == '`' {
684 break;
685 }
686 }
687 spans.push(Span::styled(s, str_style));
688 continue;
689 }
690 if ch == '\'' && matches!(lang_lower.as_str(), "rust" | "rs") {
692 if !buf.is_empty() {
693 spans.extend(colorize_tokens(
694 &buf,
695 keywords,
696 primitive_types,
697 go_type_names,
698 code_style,
699 kw_style,
700 num_style,
701 type_style,
702 primitive_style,
703 macro_style,
704 &lang_lower,
705 ));
706 buf.clear();
707 }
708 let mut s = String::new();
709 s.push(ch);
710 chars.next();
711 let mut is_lifetime = false;
712 while let Some(&c) = chars.peek() {
713 if c.is_alphanumeric() || c == '_' {
714 s.push(c);
715 chars.next();
716 } else if c == '\'' && s.len() == 2 {
717 s.push(c);
718 chars.next();
719 break;
720 } else {
721 is_lifetime = true;
722 break;
723 }
724 }
725 if is_lifetime || (s.len() > 1 && !s.ends_with('\'')) {
726 let lifetime_style = Style::default().fg(theme.code_lifetime);
727 spans.push(Span::styled(s, lifetime_style));
728 } else {
729 spans.push(Span::styled(s, str_style));
730 }
731 continue;
732 }
733 if ch == '\'' && !matches!(lang_lower.as_str(), "rust" | "rs") {
735 if !buf.is_empty() {
736 spans.extend(colorize_tokens(
737 &buf,
738 keywords,
739 primitive_types,
740 go_type_names,
741 code_style,
742 kw_style,
743 num_style,
744 type_style,
745 primitive_style,
746 macro_style,
747 &lang_lower,
748 ));
749 buf.clear();
750 }
751 let mut s = String::new();
752 s.push(ch);
753 chars.next();
754 let mut escaped = false;
755 while let Some(&c) = chars.peek() {
756 s.push(c);
757 chars.next();
758 if escaped {
759 escaped = false;
760 continue;
761 }
762 if c == '\\' {
763 escaped = true;
764 continue;
765 }
766 if c == '\'' {
767 break;
768 }
769 }
770 spans.push(Span::styled(s, str_style));
771 continue;
772 }
773 if ch == '#' && matches!(lang_lower.as_str(), "rust" | "rs") {
775 let mut lookahead = chars.clone();
776 if let Some(next) = lookahead.next() {
777 if next == '[' {
778 if !buf.is_empty() {
779 spans.extend(colorize_tokens(
780 &buf,
781 keywords,
782 primitive_types,
783 go_type_names,
784 code_style,
785 kw_style,
786 num_style,
787 type_style,
788 primitive_style,
789 macro_style,
790 &lang_lower,
791 ));
792 buf.clear();
793 }
794 let mut attr = String::new();
795 attr.push(ch);
796 chars.next();
797 let mut depth = 0;
798 while let Some(&c) = chars.peek() {
799 attr.push(c);
800 chars.next();
801 if c == '[' {
802 depth += 1;
803 } else if c == ']' {
804 depth -= 1;
805 if depth == 0 {
806 break;
807 }
808 }
809 }
810 let attr_style = Style::default().fg(theme.code_attribute);
811 spans.push(Span::styled(attr, attr_style));
812 continue;
813 }
814 }
815 }
816 if ch == '$'
818 && matches!(
819 lang_lower.as_str(),
820 "sh" | "bash" | "zsh" | "shell" | "dockerfile" | "docker"
821 )
822 {
823 if !buf.is_empty() {
824 spans.extend(colorize_tokens(
825 &buf,
826 keywords,
827 primitive_types,
828 go_type_names,
829 code_style,
830 kw_style,
831 num_style,
832 type_style,
833 primitive_style,
834 macro_style,
835 &lang_lower,
836 ));
837 buf.clear();
838 }
839 let var_style = Style::default().fg(theme.code_shell_var);
840 let mut var = String::new();
841 var.push(ch);
842 chars.next();
843 if let Some(&next_ch) = chars.peek() {
844 if next_ch == '{' {
845 var.push(next_ch);
846 chars.next();
847 while let Some(&c) = chars.peek() {
848 var.push(c);
849 chars.next();
850 if c == '}' {
851 break;
852 }
853 }
854 } else if next_ch == '(' {
855 var.push(next_ch);
856 chars.next();
857 let mut depth = 1;
858 while let Some(&c) = chars.peek() {
859 var.push(c);
860 chars.next();
861 if c == '(' {
862 depth += 1;
863 }
864 if c == ')' {
865 depth -= 1;
866 if depth == 0 {
867 break;
868 }
869 }
870 }
871 } else if next_ch.is_alphanumeric()
872 || next_ch == '_'
873 || next_ch == '@'
874 || next_ch == '#'
875 || next_ch == '?'
876 || next_ch == '!'
877 {
878 while let Some(&c) = chars.peek() {
879 if c.is_alphanumeric() || c == '_' {
880 var.push(c);
881 chars.next();
882 } else {
883 break;
884 }
885 }
886 }
887 }
888 spans.push(Span::styled(var, var_style));
889 continue;
890 }
891 if ch == '/' || ch == '#' || ch == '-' {
893 let rest: String = chars.clone().collect();
894 if rest.starts_with(comment_prefix) {
895 if !buf.is_empty() {
896 spans.extend(colorize_tokens(
897 &buf,
898 keywords,
899 primitive_types,
900 go_type_names,
901 code_style,
902 kw_style,
903 num_style,
904 type_style,
905 primitive_style,
906 macro_style,
907 &lang_lower,
908 ));
909 buf.clear();
910 }
911 while chars.peek().is_some() {
912 chars.next();
913 }
914 spans.push(Span::styled(rest, comment_style));
915 break;
916 }
917 }
918 buf.push(ch);
919 chars.next();
920 }
921
922 if !buf.is_empty() {
923 spans.extend(colorize_tokens(
924 &buf,
925 keywords,
926 primitive_types,
927 go_type_names,
928 code_style,
929 kw_style,
930 num_style,
931 type_style,
932 primitive_style,
933 macro_style,
934 &lang_lower,
935 ));
936 }
937
938 if spans.is_empty() {
939 spans.push(Span::styled(line.to_string(), code_style));
940 }
941
942 spans
943}
944
945pub fn colorize_tokens<'a>(
947 text: &str,
948 keywords: &[&str],
949 primitive_types: &[&str],
950 go_type_names: &[&str],
951 default_style: Style,
952 kw_style: Style,
953 num_style: Style,
954 type_style: Style,
955 primitive_style: Style,
956 macro_style: Style,
957 lang: &str,
958) -> Vec<Span<'static>> {
959 let mut spans = Vec::new();
960 let mut current_word = String::new();
961 let mut current_non_word = String::new();
962 let mut chars = text.chars().peekable();
963
964 while let Some(ch) = chars.next() {
965 if ch.is_alphanumeric() || ch == '_' {
966 if !current_non_word.is_empty() {
967 spans.push(Span::styled(current_non_word.clone(), default_style));
968 current_non_word.clear();
969 }
970 current_word.push(ch);
971 } else {
972 if ch == '!' && matches!(lang, "rust" | "rs") && !current_word.is_empty() {
974 let is_macro = chars
975 .peek()
976 .map(|&c| c == '(' || c == '{' || c == '[' || c.is_whitespace())
977 .unwrap_or(true);
978 if is_macro {
979 spans.push(Span::styled(current_word.clone(), macro_style));
980 current_word.clear();
981 spans.push(Span::styled("!".to_string(), macro_style));
982 continue;
983 }
984 }
985 if !current_word.is_empty() {
986 let style = classify_word(
987 ¤t_word,
988 keywords,
989 primitive_types,
990 go_type_names,
991 kw_style,
992 primitive_style,
993 num_style,
994 type_style,
995 default_style,
996 lang,
997 );
998 spans.push(Span::styled(current_word.clone(), style));
999 current_word.clear();
1000 }
1001 current_non_word.push(ch);
1002 }
1003 }
1004
1005 if !current_non_word.is_empty() {
1007 spans.push(Span::styled(current_non_word, default_style));
1008 }
1009 if !current_word.is_empty() {
1010 let style = classify_word(
1011 ¤t_word,
1012 keywords,
1013 primitive_types,
1014 go_type_names,
1015 kw_style,
1016 primitive_style,
1017 num_style,
1018 type_style,
1019 default_style,
1020 lang,
1021 );
1022 spans.push(Span::styled(current_word, style));
1023 }
1024
1025 spans
1026}
1027
1028pub fn classify_word(
1030 word: &str,
1031 keywords: &[&str],
1032 primitive_types: &[&str],
1033 go_type_names: &[&str],
1034 kw_style: Style,
1035 primitive_style: Style,
1036 num_style: Style,
1037 type_style: Style,
1038 default_style: Style,
1039 lang: &str,
1040) -> Style {
1041 if keywords.contains(&word) {
1042 kw_style
1043 } else if primitive_types.contains(&word) {
1044 primitive_style
1045 } else if word
1046 .chars()
1047 .next()
1048 .map(|c| c.is_ascii_digit())
1049 .unwrap_or(false)
1050 {
1051 num_style
1052 } else if matches!(lang, "go" | "golang") {
1053 if go_type_names.contains(&word) {
1054 type_style
1055 } else {
1056 default_style
1057 }
1058 } else if word
1059 .chars()
1060 .next()
1061 .map(|c| c.is_uppercase())
1062 .unwrap_or(false)
1063 {
1064 type_style
1065 } else {
1066 default_style
1067 }
1068}