1use std::collections::HashSet;
21use std::io::Stdout;
22use std::path::Path;
23
24use once_cell::sync::Lazy;
25use syntect::easy::HighlightLines;
26use syntect::highlighting::{Style as SyntectStyle, Theme, ThemeSet};
27use syntect::parsing::SyntaxSet;
28use syntect::util::LinesWithEndings;
29
30use crate::Renderable;
31use crate::cells::cell_len;
32use crate::color::SimpleColor as Color;
33use crate::console::{Console, ConsoleOptions, OverflowMethod};
34use crate::measure::Measurement;
35use crate::padding::PaddingDimensions;
36use crate::segment::{Segment, Segments};
37use crate::style::Style;
38use crate::text::Text;
39
40static SYNTAX_SET: Lazy<SyntaxSet> = Lazy::new(SyntaxSet::load_defaults_newlines);
46
47static THEME_SET: Lazy<ThemeSet> = Lazy::new(ThemeSet::load_defaults);
49
50const MONOKAI_THEME_DATA: &[u8] = include_bytes!("themes/monokai.tmTheme");
52
53const MONOKAI_PLUS_THEME_DATA: &[u8] = include_bytes!("themes/monokai-plus.tmTheme");
55
56const DRACULA_THEME_DATA: &[u8] = include_bytes!("themes/dracula.tmTheme");
58
59const GRUVBOX_DARK_THEME_DATA: &[u8] = include_bytes!("themes/gruvbox-dark.tmTheme");
61
62const NORD_THEME_DATA: &[u8] = include_bytes!("themes/nord.tmTheme");
64
65static MONOKAI_THEME: Lazy<Option<Theme>> = Lazy::new(|| {
67 use std::io::Cursor;
68 let mut cursor = Cursor::new(MONOKAI_THEME_DATA);
69 ThemeSet::load_from_reader(&mut cursor).ok()
70});
71
72static MONOKAI_PLUS_THEME: Lazy<Option<Theme>> = Lazy::new(|| {
74 use std::io::Cursor;
75 let mut cursor = Cursor::new(MONOKAI_PLUS_THEME_DATA);
76 ThemeSet::load_from_reader(&mut cursor).ok()
77});
78
79static DRACULA_THEME: Lazy<Option<Theme>> = Lazy::new(|| {
81 use std::io::Cursor;
82 let mut cursor = Cursor::new(DRACULA_THEME_DATA);
83 ThemeSet::load_from_reader(&mut cursor).ok()
84});
85
86static GRUVBOX_DARK_THEME: Lazy<Option<Theme>> = Lazy::new(|| {
88 use std::io::Cursor;
89 let mut cursor = Cursor::new(GRUVBOX_DARK_THEME_DATA);
90 ThemeSet::load_from_reader(&mut cursor).ok()
91});
92
93static NORD_THEME: Lazy<Option<Theme>> = Lazy::new(|| {
95 use std::io::Cursor;
96 let mut cursor = Cursor::new(NORD_THEME_DATA);
97 ThemeSet::load_from_reader(&mut cursor).ok()
98});
99
100pub const DEFAULT_THEME: &str = "monokai";
103
104const FALLBACK_SYNTECT_THEME: &str = "base16-mocha.dark";
106
107pub const NUMBERS_COLUMN_DEFAULT_PADDING: usize = 2;
111
112pub mod ansi_light {
118 use crate::color::SimpleColor as Color;
119 use crate::style::Style;
120
121 pub fn comment() -> Style {
122 Style::new().with_dim(true)
123 }
124 pub fn comment_preproc() -> Style {
125 Style::new().with_color(Color::Standard(6)) }
127 pub fn keyword() -> Style {
128 Style::new().with_color(Color::Standard(4)) }
130 pub fn keyword_type() -> Style {
131 Style::new().with_color(Color::Standard(6)) }
133 pub fn operator_word() -> Style {
134 Style::new().with_color(Color::Standard(5)) }
136 pub fn name_builtin() -> Style {
137 Style::new().with_color(Color::Standard(6)) }
139 pub fn name_function() -> Style {
140 Style::new().with_color(Color::Standard(2)) }
142 pub fn name_namespace() -> Style {
143 Style::new()
144 .with_color(Color::Standard(6))
145 .with_underline(true) }
147 pub fn name_class() -> Style {
148 Style::new()
149 .with_color(Color::Standard(2))
150 .with_underline(true) }
152 pub fn name_decorator() -> Style {
153 Style::new().with_color(Color::Standard(5)).with_bold(true) }
155 pub fn name_variable() -> Style {
156 Style::new().with_color(Color::Standard(1)) }
158 pub fn name_attribute() -> Style {
159 Style::new().with_color(Color::Standard(6)) }
161 pub fn name_tag() -> Style {
162 Style::new().with_color(Color::Standard(12)) }
164 pub fn string() -> Style {
165 Style::new().with_color(Color::Standard(3)) }
167 pub fn number() -> Style {
168 Style::new().with_color(Color::Standard(4)) }
170 pub fn error() -> Style {
171 Style::new()
172 .with_color(Color::Standard(1))
173 .with_underline(true) }
175}
176
177pub mod ansi_dark {
179 use crate::color::SimpleColor as Color;
180 use crate::style::Style;
181
182 pub fn comment() -> Style {
183 Style::new().with_dim(true)
184 }
185 pub fn comment_preproc() -> Style {
186 Style::new().with_color(Color::Standard(14)) }
188 pub fn keyword() -> Style {
189 Style::new().with_color(Color::Standard(12)) }
191 pub fn keyword_type() -> Style {
192 Style::new().with_color(Color::Standard(14)) }
194 pub fn operator_word() -> Style {
195 Style::new().with_color(Color::Standard(13)) }
197 pub fn name_builtin() -> Style {
198 Style::new().with_color(Color::Standard(14)) }
200 pub fn name_function() -> Style {
201 Style::new().with_color(Color::Standard(10)) }
203 pub fn name_namespace() -> Style {
204 Style::new()
205 .with_color(Color::Standard(14))
206 .with_underline(true) }
208 pub fn name_class() -> Style {
209 Style::new()
210 .with_color(Color::Standard(10))
211 .with_underline(true) }
213 pub fn name_decorator() -> Style {
214 Style::new().with_color(Color::Standard(13)).with_bold(true) }
216 pub fn name_variable() -> Style {
217 Style::new().with_color(Color::Standard(9)) }
219 pub fn name_attribute() -> Style {
220 Style::new().with_color(Color::Standard(14)) }
222 pub fn name_tag() -> Style {
223 Style::new().with_color(Color::Standard(12)) }
225 pub fn string() -> Style {
226 Style::new().with_color(Color::Standard(3)) }
228 pub fn number() -> Style {
229 Style::new().with_color(Color::Standard(12)) }
231 pub fn error() -> Style {
232 Style::new()
233 .with_color(Color::Standard(1))
234 .with_underline(true) }
236}
237
238pub trait SyntaxTheme: Send + Sync {
246 fn get_style(&self, style: &SyntectStyle) -> Style;
248
249 fn get_background_style(&self) -> Style;
251
252 fn syntect_theme(&self) -> Option<&Theme>;
254}
255
256pub struct SyntectTheme {
258 theme: Theme,
259 background_style: Style,
260}
261
262impl SyntectTheme {
263 pub fn new(theme: Theme) -> Self {
265 let bg_color = theme.settings.background.map(|c| Color::Rgb {
266 r: c.r,
267 g: c.g,
268 b: c.b,
269 });
270 let background_style = match bg_color {
271 Some(c) => Style::new().with_bgcolor(c),
272 None => Style::new(),
273 };
274 Self {
275 theme,
276 background_style,
277 }
278 }
279
280 pub fn from_name(name: &str) -> Option<Self> {
282 THEME_SET.themes.get(name).map(|t| Self::new(t.clone()))
283 }
284}
285
286impl SyntaxTheme for SyntectTheme {
287 fn get_style(&self, style: &SyntectStyle) -> Style {
288 let fg = style.foreground;
289 let bg = style.background;
290
291 let mut result = Style::new();
292
293 result = result.with_color(Color::Rgb {
295 r: fg.r,
296 g: fg.g,
297 b: fg.b,
298 });
299
300 if let Some(theme_bg) = self.theme.settings.background {
302 if bg.r != theme_bg.r || bg.g != theme_bg.g || bg.b != theme_bg.b {
303 result = result.with_bgcolor(Color::Rgb {
304 r: bg.r,
305 g: bg.g,
306 b: bg.b,
307 });
308 }
309 }
310
311 let font_style = style.font_style;
313 if font_style.contains(syntect::highlighting::FontStyle::BOLD) {
314 result = result.with_bold(true);
315 }
316 if font_style.contains(syntect::highlighting::FontStyle::ITALIC) {
317 result = result.with_italic(true);
318 }
319 if font_style.contains(syntect::highlighting::FontStyle::UNDERLINE) {
320 result = result.with_underline(true);
321 }
322
323 result
324 }
325
326 fn get_background_style(&self) -> Style {
327 self.background_style
328 }
329
330 fn syntect_theme(&self) -> Option<&Theme> {
331 Some(&self.theme)
332 }
333}
334
335pub struct AnsiTheme {
337 dark: bool,
338}
339
340impl AnsiTheme {
341 pub fn dark() -> Self {
343 Self { dark: true }
344 }
345
346 pub fn light() -> Self {
348 Self { dark: false }
349 }
350}
351
352impl SyntaxTheme for AnsiTheme {
353 fn get_style(&self, style: &SyntectStyle) -> Style {
354 let fg = style.foreground;
356
357 let (r, g, b) = (fg.r as u16, fg.g as u16, fg.b as u16);
359
360 let brightness = (r + g + b) / 3;
362
363 let color = if brightness < 50 {
365 Color::Standard(0) } else if r > 200 && g < 100 && b < 100 {
368 if self.dark {
370 Color::Standard(9)
371 } else {
372 Color::Standard(1)
373 }
374 } else if g > 200 && r < 100 && b < 100 {
375 if self.dark {
377 Color::Standard(10)
378 } else {
379 Color::Standard(2)
380 }
381 } else if b > 200 && r < 100 && g < 100 {
382 if self.dark {
384 Color::Standard(12)
385 } else {
386 Color::Standard(4)
387 }
388 } else if r > 200 && g > 200 && b < 100 {
389 Color::Standard(3)
391 } else if r > 200 && b > 200 && g < 100 {
392 if self.dark {
394 Color::Standard(13)
395 } else {
396 Color::Standard(5)
397 }
398 } else if g > 200 && b > 200 && r < 100 {
399 if self.dark {
401 Color::Standard(14)
402 } else {
403 Color::Standard(6)
404 }
405 } else if brightness > 200 {
406 Color::Standard(7)
408 } else {
409 Color::Rgb {
411 r: fg.r,
412 g: fg.g,
413 b: fg.b,
414 }
415 };
416
417 let mut result = Style::new().with_color(color);
418
419 let font_style = style.font_style;
420 if font_style.contains(syntect::highlighting::FontStyle::BOLD) {
421 result = result.with_bold(true);
422 }
423 if font_style.contains(syntect::highlighting::FontStyle::ITALIC) {
424 result = result.with_italic(true);
425 }
426 if font_style.contains(syntect::highlighting::FontStyle::UNDERLINE) {
427 result = result.with_underline(true);
428 }
429
430 result
431 }
432
433 fn get_background_style(&self) -> Style {
434 Style::new()
435 }
436
437 fn syntect_theme(&self) -> Option<&Theme> {
438 None
439 }
440}
441
442#[derive(Debug, Clone)]
465struct SyntaxHighlightRange {
466 style: Style,
467 start: (usize, usize),
468 end: (usize, usize),
469}
470
471pub struct Syntax {
472 code: String,
474 lexer: String,
476 theme: Box<dyn SyntaxTheme>,
478 dedent: bool,
480 line_numbers: bool,
482 start_line: usize,
484 line_range: Option<(Option<usize>, Option<usize>)>,
486 highlight_lines: HashSet<usize>,
488 code_width: Option<usize>,
490 tab_size: usize,
492 #[allow(dead_code)]
495 word_wrap: bool,
496 background_color: Option<Color>,
498 #[allow(dead_code)]
501 indent_guides: bool,
502 padding: (usize, usize, usize, usize),
504 explicit_theme: bool,
506 stylized_ranges: Vec<SyntaxHighlightRange>,
508}
509
510impl std::fmt::Debug for Syntax {
511 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
512 f.debug_struct("Syntax")
513 .field("code_len", &self.code.len())
514 .field("lexer", &self.lexer)
515 .field("dedent", &self.dedent)
516 .field("line_numbers", &self.line_numbers)
517 .field("start_line", &self.start_line)
518 .field("line_range", &self.line_range)
519 .field("highlight_lines", &self.highlight_lines)
520 .field("code_width", &self.code_width)
521 .field("tab_size", &self.tab_size)
522 .field("word_wrap", &self.word_wrap)
523 .field("indent_guides", &self.indent_guides)
524 .field("padding", &self.padding)
525 .field("explicit_theme", &self.explicit_theme)
526 .finish_non_exhaustive()
527 }
528}
529
530impl Syntax {
531 pub fn new(code: impl Into<String>, lexer: impl Into<String>) -> Self {
546 let theme = Self::get_theme(DEFAULT_THEME);
547 Self {
548 code: code.into(),
549 lexer: lexer.into(),
550 theme,
551 dedent: false,
552 line_numbers: false,
553 start_line: 1,
554 line_range: None,
555 highlight_lines: HashSet::new(),
556 code_width: None,
557 tab_size: 4,
558 word_wrap: false,
559 background_color: None,
560 indent_guides: false,
561 padding: (0, 0, 0, 0),
562 explicit_theme: false,
563 stylized_ranges: Vec::new(),
564 }
565 }
566
567 pub fn from_path(path: impl AsRef<Path>) -> std::io::Result<Self> {
585 let path = path.as_ref();
586 let code = std::fs::read_to_string(path)?;
587 let lexer = Self::guess_lexer(path, Some(&code));
588 Ok(Self::new(code, lexer))
589 }
590
591 pub fn guess_lexer(path: impl AsRef<Path>, code: Option<&str>) -> String {
602 let path = path.as_ref();
603
604 if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
606 if let Some(syntax) = SYNTAX_SET.find_syntax_by_extension(ext) {
607 return syntax.name.to_lowercase();
608 }
609 }
610
611 if let Some(filename) = path.file_name().and_then(|n| n.to_str()) {
613 match filename.to_lowercase().as_str() {
615 "makefile" | "gnumakefile" => return "makefile".to_string(),
616 "dockerfile" => return "dockerfile".to_string(),
617 "cmakelists.txt" => return "cmake".to_string(),
618 _ => {}
619 }
620 }
621
622 if let Some(code) = code {
624 if code.starts_with("#!/usr/bin/env python") || code.starts_with("#!/usr/bin/python") {
626 return "python".to_string();
627 }
628 if code.starts_with("#!/bin/bash") || code.starts_with("#!/usr/bin/env bash") {
629 return "bash".to_string();
630 }
631 if code.starts_with("#!/usr/bin/env node") {
632 return "javascript".to_string();
633 }
634 if code.starts_with("#!/usr/bin/env ruby") {
635 return "ruby".to_string();
636 }
637
638 if let Some(syntax) = SYNTAX_SET.find_syntax_by_first_line(code) {
640 return syntax.name.to_lowercase();
641 }
642 }
643
644 "text".to_string()
646 }
647
648 pub fn get_theme(name: &str) -> Box<dyn SyntaxTheme> {
658 match name.to_lowercase().as_str() {
660 "ansi_dark" | "ansi-dark" => return Box::new(AnsiTheme::dark()),
661 "ansi_light" | "ansi-light" => return Box::new(AnsiTheme::light()),
662 _ => {}
663 }
664
665 match name.to_lowercase().as_str() {
667 "monokai" => {
668 if let Some(ref theme) = *MONOKAI_THEME {
669 return Box::new(SyntectTheme::new(theme.clone()));
670 }
671 }
672 "monokai-plus" | "monokai_plus" => {
673 if let Some(ref theme) = *MONOKAI_PLUS_THEME {
674 return Box::new(SyntectTheme::new(theme.clone()));
675 }
676 }
677 "dracula" => {
678 if let Some(ref theme) = *DRACULA_THEME {
679 return Box::new(SyntectTheme::new(theme.clone()));
680 }
681 }
682 "gruvbox-dark" | "gruvbox_dark" | "gruvbox" => {
683 if let Some(ref theme) = *GRUVBOX_DARK_THEME {
684 return Box::new(SyntectTheme::new(theme.clone()));
685 }
686 }
687 "nord" => {
688 if let Some(ref theme) = *NORD_THEME {
689 return Box::new(SyntectTheme::new(theme.clone()));
690 }
691 }
692 _ => {}
693 }
694
695 let theme_name = match name.to_lowercase().as_str() {
697 "one-dark" | "onedark" => "base16-ocean.dark",
698 "one-light" | "onelight" => "base16-ocean.light",
699 "github-dark" => "base16-ocean.dark",
700 "github-light" => "base16-ocean.light",
701 "solarized-dark" => "Solarized (dark)",
702 "solarized-light" => "Solarized (light)",
703 _ => name,
704 };
705
706 SyntectTheme::from_name(theme_name)
707 .map(|t| Box::new(t) as Box<dyn SyntaxTheme>)
708 .unwrap_or_else(|| {
709 if let Some(ref theme) = *MONOKAI_THEME {
711 Box::new(SyntectTheme::new(theme.clone()))
712 } else {
713 Box::new(AnsiTheme::dark())
714 }
715 })
716 }
717
718 pub fn available_themes() -> Vec<&'static str> {
720 let mut themes: Vec<&str> = THEME_SET.themes.keys().map(|s| s.as_str()).collect();
721 themes.extend([
723 "ansi_dark",
724 "ansi_light",
725 "monokai",
726 "monokai-plus",
727 "dracula",
728 "gruvbox-dark",
729 "nord",
730 ]);
731 themes.sort();
732 themes
733 }
734
735 pub fn available_languages() -> Vec<String> {
737 SYNTAX_SET
738 .syntaxes()
739 .iter()
740 .map(|s| s.name.to_lowercase())
741 .collect()
742 }
743
744 pub fn with_theme(mut self, theme: impl AsRef<str>) -> Self {
752 self.theme = Self::get_theme(theme.as_ref());
753 self.explicit_theme = true;
754 self
755 }
756
757 pub fn with_custom_theme(mut self, theme: Box<dyn SyntaxTheme>) -> Self {
761 self.theme = theme;
762 self.explicit_theme = true;
763 self
764 }
765
766 pub fn with_dedent(mut self, dedent: bool) -> Self {
768 self.dedent = dedent;
769 self
770 }
771
772 pub fn with_line_numbers(mut self, line_numbers: bool) -> Self {
774 self.line_numbers = line_numbers;
775 self
776 }
777
778 pub fn with_start_line(mut self, start_line: usize) -> Self {
780 self.start_line = start_line;
781 self
782 }
783
784 pub fn with_line_range(mut self, start: Option<usize>, end: Option<usize>) -> Self {
791 self.line_range = Some((start, end));
792 self
793 }
794
795 pub fn with_highlight_lines(mut self, lines: impl IntoIterator<Item = usize>) -> Self {
797 self.highlight_lines = lines.into_iter().collect();
798 self
799 }
800
801 pub fn with_code_width(mut self, width: usize) -> Self {
803 self.code_width = Some(width);
804 self
805 }
806
807 pub fn with_tab_size(mut self, tab_size: usize) -> Self {
809 self.tab_size = tab_size;
810 self
811 }
812
813 pub fn with_word_wrap(mut self, word_wrap: bool) -> Self {
817 self.word_wrap = word_wrap;
818 self
819 }
820
821 pub fn with_background_color(mut self, color: Color) -> Self {
823 self.background_color = Some(color);
824 self
825 }
826
827 pub fn with_indent_guides(mut self, indent_guides: bool) -> Self {
831 self.indent_guides = indent_guides;
832 self
833 }
834
835 pub fn with_padding(mut self, padding: impl Into<PaddingDimensions>) -> Self {
837 self.padding = padding.into().unpack();
838 self
839 }
840
841 pub fn stylize_range(&mut self, style: Style, start: (usize, usize), end: (usize, usize)) {
851 self.stylized_ranges
852 .push(SyntaxHighlightRange { style, start, end });
853 }
854
855 pub fn with_highlight_range(
859 mut self,
860 style: Style,
861 start: (usize, usize),
862 end: (usize, usize),
863 ) -> Self {
864 self.stylized_ranges
865 .push(SyntaxHighlightRange { style, start, end });
866 self
867 }
868
869 pub fn code(&self) -> &str {
875 &self.code
876 }
877
878 pub fn lexer(&self) -> &str {
880 &self.lexer
881 }
882
883 pub fn line_numbers(&self) -> bool {
885 self.line_numbers
886 }
887
888 pub fn tab_size(&self) -> usize {
890 self.tab_size
891 }
892
893 pub fn highlight(&self) -> Text {
902 self.highlight_with_theme(&*self.theme)
903 }
904
905 fn highlight_with_theme(&self, theme: &dyn SyntaxTheme) -> Text {
907 let (ends_on_nl, processed_code) = self.process_code();
908
909 let lexer_lower = self.lexer.to_lowercase();
911 let lexer_normalized = match lexer_lower.as_str() {
912 "python3" | "py3" | "py" => "python",
913 "javascript" | "js" => "javascript",
914 "typescript" | "ts" => "typescript",
915 "rust" | "rs" => "rust",
916 "cpp" | "c++" => "c++",
917 "csharp" | "cs" => "c#",
918 _ => lexer_lower.as_str(),
919 };
920
921 let syntax = SYNTAX_SET
923 .find_syntax_by_token(lexer_normalized)
924 .or_else(|| SYNTAX_SET.find_syntax_by_extension(lexer_normalized))
925 .or_else(|| SYNTAX_SET.find_syntax_by_name(lexer_normalized))
926 .or_else(|| SYNTAX_SET.find_syntax_by_token(&self.lexer))
928 .or_else(|| SYNTAX_SET.find_syntax_by_extension(&self.lexer))
929 .or_else(|| SYNTAX_SET.find_syntax_by_name(&self.lexer))
930 .unwrap_or_else(|| SYNTAX_SET.find_syntax_plain_text());
931
932 let base_style = self.get_base_style_with_theme(theme);
933 let mut text = Text::new();
934 text.set_base_style(Some(base_style));
935
936 if let Some(syntect_theme) = theme.syntect_theme() {
938 let mut highlighter = HighlightLines::new(syntax, syntect_theme);
939
940 for line in LinesWithEndings::from(&processed_code) {
941 match highlighter.highlight_line(line, &SYNTAX_SET) {
942 Ok(ranges) => {
943 for (style, token) in ranges {
944 let rich_style = theme.get_style(&style);
945 text.append(token, Some(rich_style));
946 }
947 }
948 Err(_) => {
949 text.append(line, None);
951 }
952 }
953 }
954 } else {
955 let mut highlighter =
957 HighlightLines::new(syntax, &THEME_SET.themes[FALLBACK_SYNTECT_THEME]);
958
959 for line in LinesWithEndings::from(&processed_code) {
960 match highlighter.highlight_line(line, &SYNTAX_SET) {
961 Ok(ranges) => {
962 for (style, token) in ranges {
963 let rich_style = theme.get_style(&style);
964 text.append(token, Some(rich_style));
965 }
966 }
967 Err(_) => {
968 text.append(line, None);
969 }
970 }
971 }
972 }
973
974 if !self.stylized_ranges.is_empty() {
976 self.apply_stylized_ranges(&mut text);
977 }
978
979 if !ends_on_nl && text.plain_text().ends_with('\n') {
981 let plain = text.plain_text();
982 let new_plain = plain.trim_end_matches('\n');
983 if plain != new_plain {
984 let mut new_text = Text::new();
986 new_text.set_base_style(text.base_style());
987 new_text.append(new_plain, None);
988 for span in text.spans() {
990 if span.start < new_plain.chars().count() {
991 new_text.stylize(
992 span.start,
993 span.end.min(new_plain.chars().count()),
994 span.style,
995 );
996 }
997 }
998 return new_text;
999 }
1000 }
1001
1002 text
1003 }
1004
1005 fn apply_stylized_ranges(&self, text: &mut Text) {
1009 let plain = text.plain_text();
1010
1011 let mut newlines_offsets: Vec<usize> = vec![0];
1014 for (i, c) in plain.chars().enumerate() {
1015 if c == '\n' {
1016 newlines_offsets.push(i + 1);
1017 }
1018 }
1019 newlines_offsets.push(plain.chars().count() + 1);
1021
1022 for range in &self.stylized_ranges {
1023 let (start_line, start_col) = range.start;
1024 let (end_line, end_col) = range.end;
1025
1026 let start_line_idx = start_line.saturating_sub(1);
1028 let end_line_idx = end_line.saturating_sub(1);
1029
1030 let start_offset =
1031 newlines_offsets.get(start_line_idx).copied().unwrap_or(0) + start_col;
1032
1033 let end_offset = newlines_offsets.get(end_line_idx).copied().unwrap_or(0) + end_col;
1034
1035 if start_offset < end_offset {
1036 text.stylize(start_offset, end_offset, range.style);
1037 }
1038 }
1039 }
1040
1041 fn get_base_style(&self) -> Style {
1043 self.get_base_style_with_theme(&*self.theme)
1044 }
1045
1046 fn get_base_style_with_theme(&self, theme: &dyn SyntaxTheme) -> Style {
1048 let mut style = theme.get_background_style();
1049 if let Some(bg) = self.background_color {
1050 style = style.with_bgcolor(bg);
1051 }
1052 style
1053 }
1054
1055 fn process_code(&self) -> (bool, String) {
1057 let ends_on_nl = self.code.ends_with('\n');
1058 let mut processed = if ends_on_nl {
1059 self.code.clone()
1060 } else {
1061 format!("{}\n", self.code)
1062 };
1063
1064 if self.dedent {
1066 processed = Self::dedent_code(&processed);
1067 }
1068
1069 processed = Self::expand_tabs(&processed, self.tab_size);
1071
1072 (ends_on_nl, processed)
1073 }
1074
1075 fn dedent_code(code: &str) -> String {
1077 let lines: Vec<&str> = code.lines().collect();
1078
1079 let min_indent = lines
1081 .iter()
1082 .filter(|line| !line.trim().is_empty())
1083 .map(|line| line.len() - line.trim_start().len())
1084 .min()
1085 .unwrap_or(0);
1086
1087 if min_indent == 0 {
1088 return code.to_string();
1089 }
1090
1091 lines
1093 .iter()
1094 .map(|line| {
1095 if line.len() >= min_indent {
1096 &line[min_indent..]
1097 } else {
1098 line.trim_start()
1099 }
1100 })
1101 .collect::<Vec<_>>()
1102 .join("\n")
1103 + if code.ends_with('\n') { "\n" } else { "" }
1104 }
1105
1106 fn expand_tabs(code: &str, tab_size: usize) -> String {
1108 if !code.contains('\t') {
1109 return code.to_string();
1110 }
1111
1112 let mut result = String::new();
1113 let mut column = 0;
1114
1115 for c in code.chars() {
1116 match c {
1117 '\t' => {
1118 let spaces = tab_size - (column % tab_size);
1119 for _ in 0..spaces {
1120 result.push(' ');
1121 }
1122 column += spaces;
1123 }
1124 '\n' => {
1125 result.push(c);
1126 column = 0;
1127 }
1128 _ => {
1129 result.push(c);
1130 column += 1;
1131 }
1132 }
1133 }
1134
1135 result
1136 }
1137
1138 fn indent_guide_color(
1149 theme: &dyn SyntaxTheme,
1150 background_override: Option<Color>,
1151 ) -> Option<Color> {
1152 const DIM_FACTOR: f64 = 0.66;
1153
1154 let syntect_theme = theme.syntect_theme()?;
1155 let highlighter = syntect::highlighting::Highlighter::new(syntect_theme);
1156 let comment_scope = syntect::parsing::Scope::new("comment").ok()?;
1157 let fg = highlighter.style_for_stack(&[comment_scope]).foreground;
1158
1159 let (bg_r, bg_g, bg_b) = match background_override {
1160 Some(Color::Rgb { r, g, b }) => (r, g, b),
1161 Some(_) => return None,
1162 None => {
1163 let bg = syntect_theme.settings.background?;
1164 (bg.r, bg.g, bg.b)
1165 }
1166 };
1167
1168 let blend =
1170 |bg: u8, fg: u8| (f64::from(bg) + (f64::from(fg) - f64::from(bg)) * DIM_FACTOR) as u8;
1171 Some(Color::Rgb {
1172 r: blend(bg_r, fg.r),
1173 g: blend(bg_g, fg.g),
1174 b: blend(bg_b, fg.b),
1175 })
1176 }
1177
1178 fn numbers_column_width(&self) -> usize {
1180 if !self.line_numbers {
1181 return 0;
1182 }
1183 let line_count = self.code.lines().count();
1184 let max_line_no = self.start_line + line_count.saturating_sub(1);
1185 let digits = max_line_no.to_string().len();
1186 digits + NUMBERS_COLUMN_DEFAULT_PADDING
1187 }
1188}
1189
1190impl Renderable for Syntax {
1191 fn render(&self, console: &Console<Stdout>, options: &ConsoleOptions) -> Segments {
1192 let mut result = Segments::new();
1193
1194 let (pad_top, pad_right, pad_bottom, pad_left) = self.padding;
1195 let horizontal_padding = pad_left + pad_right;
1196
1197 let numbers_width = self.numbers_column_width();
1199 let code_width = if let Some(w) = self.code_width {
1200 w
1201 } else if self.line_numbers {
1202 options
1203 .max_width
1204 .saturating_sub(numbers_width + 1 + horizontal_padding)
1205 } else {
1206 options.max_width.saturating_sub(horizontal_padding)
1207 };
1208
1209 let effective_theme: Option<Box<dyn SyntaxTheme>> =
1211 if !self.explicit_theme && options.theme_name != "default" {
1212 Some(Self::get_theme(&options.theme_name))
1213 } else {
1214 None
1215 };
1216
1217 let text = if let Some(ref theme) = effective_theme {
1219 self.highlight_with_theme(&**theme)
1220 } else {
1221 self.highlight()
1222 };
1223
1224 let lines: Vec<Text> = text.split("\n", false, true);
1226
1227 let (start_idx, end_idx) = if let Some((start, end)) = self.line_range {
1229 let start = start.map(|s| s.saturating_sub(1)).unwrap_or(0);
1230 let end = end.unwrap_or(lines.len());
1231 (start, end)
1232 } else {
1233 (0, lines.len())
1234 };
1235
1236 let filtered_lines: Vec<&Text> = lines
1237 .iter()
1238 .skip(start_idx)
1239 .take(end_idx.saturating_sub(start_idx))
1240 .collect();
1241
1242 let base_style = if let Some(ref theme) = effective_theme {
1244 self.get_base_style_with_theme(&**theme)
1245 } else {
1246 self.get_base_style()
1247 };
1248 let new_line = Segment::line();
1249
1250 let number_style = base_style.combine(&Style::new().with_color(Color::Rgb {
1253 r: 101,
1254 g: 102,
1255 b: 96,
1256 }));
1257
1258 let highlight_number_style = base_style.combine(&Style::new().with_bold(true));
1260
1261 let render_theme: &dyn SyntaxTheme = effective_theme.as_deref().unwrap_or(&*self.theme);
1265 let indent_guide_style = base_style.combine(
1266 &Self::indent_guide_color(render_theme, self.background_color)
1267 .map(|color| Style::new().with_color(color))
1268 .unwrap_or_else(|| Style::new().with_dim(true)),
1269 );
1270
1271 if pad_top > 0 {
1273 let blank = " ".repeat(options.max_width);
1274 for _ in 0..pad_top {
1275 result.push(Segment::styled(blank.clone(), base_style));
1276 result.push(new_line.clone());
1277 }
1278 }
1279
1280 for (idx, line) in filtered_lines.iter().enumerate() {
1282 let line_no = self.start_line + start_idx + idx;
1283 let is_highlighted = self.highlight_lines.contains(&line_no);
1284
1285 if pad_left > 0 {
1287 result.push(Segment::styled(" ".repeat(pad_left), base_style));
1288 }
1289
1290 if self.line_numbers {
1292 let pointer = if options.legacy_windows { "> " } else { "❱ " };
1295 let line_num_str = format!(
1296 "{:>width$} ",
1297 line_no,
1298 width = numbers_width.saturating_sub(2)
1299 );
1300
1301 if is_highlighted {
1302 let pointer_style = base_style
1304 .combine(&Style::new().with_color(Color::Standard(1)).with_bold(true));
1305 result.push(Segment::styled(pointer.to_string(), pointer_style));
1306 result.push(Segment::styled(line_num_str, highlight_number_style));
1307 } else {
1308 result.push(Segment::styled(" ".to_string(), highlight_number_style));
1310 result.push(Segment::styled(line_num_str, number_style));
1311 }
1312 }
1313
1314 let mut guides_width = 0;
1316 let line_to_render;
1317
1318 if self.indent_guides {
1319 let plain = line.plain_text();
1320 let leading_spaces = plain.chars().take_while(|c| *c == ' ').count();
1321 let num_guides = leading_spaces / self.tab_size;
1322
1323 if num_guides > 0 {
1324 for _ in 0..num_guides {
1326 result.push(Segment::styled("│".to_string(), indent_guide_style));
1327 result.push(Segment::styled(
1328 " ".repeat(self.tab_size - 1),
1329 indent_guide_style,
1330 ));
1331 }
1332 guides_width = num_guides * self.tab_size;
1333
1334 let parts = line.divide([guides_width]);
1336 line_to_render = if parts.len() > 1 {
1337 parts.into_iter().nth(1).unwrap_or_else(|| (*line).clone())
1338 } else {
1339 Text::plain("")
1341 };
1342 } else {
1343 line_to_render = (*line).clone();
1344 }
1345 } else {
1346 line_to_render = (*line).clone();
1347 }
1348
1349 let content_width = code_width.saturating_sub(guides_width);
1352 let mut line_options = options.update_width(content_width);
1353 line_options.no_wrap = true;
1354 line_options.overflow = Some(OverflowMethod::Crop);
1355
1356 let line_segments: Vec<Segment> = line_to_render
1357 .render(console, &line_options)
1358 .into_iter()
1359 .collect();
1360 let adjusted =
1361 Segment::adjust_line_length(&line_segments, content_width, Some(base_style), true);
1362 for seg in adjusted {
1363 result.push(seg);
1364 }
1365
1366 if pad_right > 0 {
1368 result.push(Segment::styled(" ".repeat(pad_right), base_style));
1369 }
1370
1371 result.push(new_line.clone());
1372 }
1373
1374 if pad_bottom > 0 {
1376 let blank = " ".repeat(options.max_width);
1377 for _ in 0..pad_bottom {
1378 result.push(Segment::styled(blank.clone(), base_style));
1379 result.push(new_line.clone());
1380 }
1381 }
1382
1383 result
1384 }
1385
1386 fn measure(&self, _console: &Console<Stdout>, options: &ConsoleOptions) -> Measurement {
1387 let (_, pad_right, _, pad_left) = self.padding;
1388 let horizontal_padding = pad_left + pad_right;
1389
1390 let numbers_width = self.numbers_column_width();
1391
1392 if let Some(code_width) = self.code_width {
1393 let width = code_width + numbers_width + horizontal_padding;
1394 if self.line_numbers {
1395 return Measurement::new(numbers_width, width + 1);
1396 }
1397 return Measurement::new(numbers_width, width);
1398 }
1399
1400 let lines: Vec<&str> = self.code.lines().collect();
1402 let max_line_width = lines.iter().map(|l| cell_len(l)).max().unwrap_or(0);
1403
1404 let width = max_line_width + numbers_width + horizontal_padding;
1405 let width = if self.line_numbers { width + 1 } else { width };
1406
1407 Measurement::new(numbers_width.max(1), width.min(options.max_width))
1408 }
1409}
1410
1411#[cfg(test)]
1416mod tests {
1417 use super::*;
1418
1419 #[test]
1420 fn test_syntax_new() {
1421 let syntax = Syntax::new("fn main() {}", "rust");
1422 assert_eq!(syntax.code(), "fn main() {}");
1423 assert_eq!(syntax.lexer(), "rust");
1424 }
1425
1426 #[test]
1427 fn test_syntax_with_line_numbers() {
1428 let syntax = Syntax::new("fn main() {}", "rust").with_line_numbers(true);
1429 assert!(syntax.line_numbers());
1430 }
1431
1432 #[test]
1433 fn test_syntax_with_theme() {
1434 let syntax = Syntax::new("fn main() {}", "rust").with_theme("monokai");
1435 assert_eq!(syntax.code(), "fn main() {}");
1437 }
1438
1439 #[test]
1440 fn test_syntax_with_tab_size() {
1441 let syntax = Syntax::new("fn main() {}", "rust").with_tab_size(2);
1442 assert_eq!(syntax.tab_size(), 2);
1443 }
1444
1445 #[test]
1446 fn test_syntax_highlight() {
1447 let syntax = Syntax::new("fn main() {}", "rust");
1448 let text = syntax.highlight();
1449 assert!(text.plain_text().contains("fn"));
1451 assert!(text.plain_text().contains("main"));
1452 }
1453
1454 #[test]
1455 fn test_syntax_highlight_python() {
1456 let code = r#"def hello():
1457 print("Hello, World!")
1458"#;
1459 let syntax = Syntax::new(code, "python");
1460 let text = syntax.highlight();
1461 assert!(text.plain_text().contains("def"));
1462 assert!(text.plain_text().contains("hello"));
1463 }
1464
1465 #[test]
1466 fn test_syntax_dedent() {
1467 let code = " fn main() {\n println!(\"hello\");\n }";
1468 let syntax = Syntax::new(code, "rust").with_dedent(true);
1469 let text = syntax.highlight();
1470 assert!(text.plain_text().starts_with("fn"));
1472 }
1473
1474 #[test]
1475 fn test_syntax_expand_tabs() {
1476 let code = "fn main() {\n\tprintln!(\"hello\");\n}";
1477 let syntax = Syntax::new(code, "rust").with_tab_size(4);
1478 let text = syntax.highlight();
1479 assert!(!text.plain_text().contains('\t'));
1481 }
1482
1483 #[test]
1484 fn test_guess_lexer_by_extension() {
1485 assert_eq!(Syntax::guess_lexer("test.rs", None), "rust");
1486 assert_eq!(Syntax::guess_lexer("test.py", None), "python");
1487 assert_eq!(Syntax::guess_lexer("test.js", None), "javascript");
1488 }
1489
1490 #[test]
1491 fn test_available_themes() {
1492 let themes = Syntax::available_themes();
1493 assert!(!themes.is_empty());
1494 assert!(themes.contains(&"ansi_dark"));
1495 assert!(themes.contains(&"ansi_light"));
1496 assert!(themes.contains(&"dracula"), "Should contain dracula theme");
1498 assert!(
1499 themes.contains(&"gruvbox-dark"),
1500 "Should contain gruvbox-dark theme"
1501 );
1502 assert!(themes.contains(&"nord"), "Should contain nord theme");
1503 assert!(themes.contains(&"monokai"), "Should contain monokai theme");
1504 assert!(
1505 themes.contains(&"monokai-plus"),
1506 "Should contain monokai-plus theme"
1507 );
1508 }
1509
1510 #[test]
1511 fn test_available_languages() {
1512 let languages = Syntax::available_languages();
1513 assert!(!languages.is_empty());
1514 }
1515
1516 #[test]
1517 fn test_numbers_column_width() {
1518 let code = "line1\nline2\nline3\nline4\nline5\nline6\nline7\nline8\nline9\nline10";
1519 let syntax = Syntax::new(code, "text").with_line_numbers(true);
1520 assert_eq!(syntax.numbers_column_width(), 4);
1522 }
1523
1524 #[test]
1525 fn test_syntax_render() {
1526 let syntax = Syntax::new("fn main() {}", "rust");
1527 let console = Console::new();
1528 let options = ConsoleOptions::default();
1529
1530 let segments = syntax.render(&console, &options);
1531 let output: String = segments.iter().map(|s| s.text.to_string()).collect();
1532
1533 assert!(output.contains("fn"));
1534 assert!(output.contains("main"));
1535 }
1536
1537 #[test]
1538 fn test_syntax_render_with_line_numbers() {
1539 let syntax = Syntax::new("line1\nline2", "text").with_line_numbers(true);
1540 let console = Console::new();
1541 let options = ConsoleOptions::default();
1542
1543 let segments = syntax.render(&console, &options);
1544 let output: String = segments.iter().map(|s| s.text.to_string()).collect();
1545
1546 assert!(output.contains('1'));
1548 assert!(output.contains('2'));
1549 }
1550
1551 #[test]
1552 fn test_syntax_measure() {
1553 let syntax = Syntax::new("hello", "text");
1554 let console = Console::new();
1555 let options = ConsoleOptions::default();
1556
1557 let measurement = syntax.measure(&console, &options);
1558 assert!(measurement.maximum >= 5); }
1560
1561 #[test]
1562 fn test_syntax_is_send_sync() {
1563 fn assert_send<T: Send>() {}
1564 fn assert_sync<T: Sync>() {}
1565 assert_send::<Syntax>();
1566 assert_sync::<Syntax>();
1567 }
1568
1569 #[test]
1570 fn test_dedent_code() {
1571 let code = " line1\n line2\n line3\n";
1572 let dedented = Syntax::dedent_code(code);
1573 assert_eq!(dedented, "line1\nline2\nline3\n");
1574 }
1575
1576 #[test]
1577 fn test_dedent_code_mixed_indent() {
1578 let code = " line1\n line2\n line3\n";
1579 let dedented = Syntax::dedent_code(code);
1580 assert_eq!(dedented, "line1\n line2\nline3\n");
1581 }
1582
1583 #[test]
1584 fn test_expand_tabs() {
1585 let code = "a\tb\tc";
1586 let expanded = Syntax::expand_tabs(code, 4);
1587 assert_eq!(expanded, "a b c");
1588 }
1589
1590 #[test]
1591 fn test_expand_tabs_preserves_newlines() {
1592 let code = "a\tb\nc\td";
1593 let expanded = Syntax::expand_tabs(code, 4);
1594 assert_eq!(expanded, "a b\nc d");
1595 }
1596
1597 #[test]
1598 fn test_ansi_theme() {
1599 let theme = AnsiTheme::dark();
1600 let style = SyntectStyle {
1601 foreground: syntect::highlighting::Color {
1602 r: 255,
1603 g: 0,
1604 b: 0,
1605 a: 255,
1606 },
1607 background: syntect::highlighting::Color {
1608 r: 0,
1609 g: 0,
1610 b: 0,
1611 a: 255,
1612 },
1613 font_style: syntect::highlighting::FontStyle::empty(),
1614 };
1615 let rich_style = theme.get_style(&style);
1616 assert!(rich_style.color.is_some());
1618 }
1619
1620 #[test]
1621 fn test_syntect_theme() {
1622 let theme = SyntectTheme::from_name(FALLBACK_SYNTECT_THEME);
1624 assert!(
1625 theme.is_some(),
1626 "Fallback theme '{}' should exist",
1627 FALLBACK_SYNTECT_THEME
1628 );
1629 let theme = theme.unwrap();
1630 assert!(theme.syntect_theme().is_some());
1631
1632 let default_theme = Syntax::get_theme(DEFAULT_THEME);
1634 assert!(default_theme.syntect_theme().is_some());
1635 }
1636
1637 #[test]
1638 fn test_line_range() {
1639 let code = "line1\nline2\nline3\nline4\nline5";
1640 let syntax = Syntax::new(code, "text").with_line_range(Some(2), Some(4));
1641 let console = Console::new();
1642 let options = ConsoleOptions::default();
1643
1644 let segments = syntax.render(&console, &options);
1645 let output: String = segments.iter().map(|s| s.text.to_string()).collect();
1646
1647 assert!(output.contains("line2"));
1649 assert!(output.contains("line3"));
1650 assert!(output.contains("line4"));
1651 assert!(!output.contains("line1"));
1653 assert!(!output.contains("line5"));
1654 }
1655
1656 #[test]
1657 fn test_highlight_lines() {
1658 let code = "line1\nline2\nline3";
1659 let syntax = Syntax::new(code, "text")
1660 .with_line_numbers(true)
1661 .with_highlight_lines([2]);
1662 let console = Console::new();
1663 let options = ConsoleOptions::default();
1664
1665 let segments = syntax.render(&console, &options);
1666 let output: String = segments.iter().map(|s| s.text.to_string()).collect();
1667
1668 assert!(output.contains('❱') || output.contains('>'));
1670 }
1671
1672 #[test]
1673 fn test_stylize_range() {
1674 let mut syntax = Syntax::new("line1\nline2\nline3", "text");
1675 let style = Style::new().with_bold(true);
1676 syntax.stylize_range(style, (2, 0), (2, 5)); let text = syntax.highlight();
1679 let plain = text.plain_text();
1680 assert!(plain.contains("line2"));
1681
1682 let spans = text.spans();
1684 let has_bold_span = spans.iter().any(|s| s.style.bold == Some(true));
1685 assert!(has_bold_span, "Should have a bold span from stylize_range");
1686 }
1687
1688 #[test]
1689 fn test_with_highlight_range_builder() {
1690 let style = Style::new().with_italic(true);
1691 let syntax = Syntax::new("hello world", "text").with_highlight_range(style, (1, 0), (1, 5)); let text = syntax.highlight();
1694 let spans = text.spans();
1695 let has_italic_span = spans.iter().any(|s| s.style.italic == Some(true));
1696 assert!(
1697 has_italic_span,
1698 "Should have an italic span from with_highlight_range"
1699 );
1700 }
1701
1702 fn python_token_color(code: &str, token: &str) -> Option<Color> {
1705 let syntax = Syntax::new(code, "python");
1706 let text = syntax.highlight();
1707 let plain = text.plain_text();
1708 let start = plain.find(token).expect("token present in highlighted text");
1710 let mut found = None;
1711 for span in text.spans() {
1712 if span.start <= start && start < span.end {
1713 if let Some(color) = span.style.color {
1714 found = Some(color);
1715 }
1716 }
1717 }
1718 found
1719 }
1720
1721 const MONOKAI_PLAIN: Color = Color::Rgb {
1722 r: 0xf8,
1723 g: 0xf8,
1724 b: 0xf2,
1725 };
1726 const MONOKAI_OPERATOR: Color = Color::Rgb {
1727 r: 0xff,
1728 g: 0x46,
1729 b: 0x89,
1730 };
1731 const MONOKAI_STRING: Color = Color::Rgb {
1732 r: 0xe6,
1733 g: 0xdb,
1734 b: 0x74,
1735 };
1736 const MONOKAI_COMMENT: Color = Color::Rgb {
1737 r: 0x95,
1738 g: 0x90,
1739 b: 0x77,
1740 };
1741
1742 #[test]
1745 fn test_python_annotation_colon_is_plain_punctuation() {
1746 let code = "def f(values: Iterable[T]) -> int:\n pass\n";
1747 assert_eq!(python_token_color(code, ":"), Some(MONOKAI_PLAIN));
1749 assert_eq!(python_token_color(code, "->"), Some(MONOKAI_OPERATOR));
1750 }
1751
1752 #[test]
1755 fn test_python_docstring_quotes_are_string_colored() {
1756 let code = "def f():\n \"\"\"Doc.\"\"\"\n";
1757 assert_eq!(python_token_color(code, "\"\"\""), Some(MONOKAI_STRING));
1758 }
1759
1760 #[test]
1763 fn test_python_for_in_is_operator_word() {
1764 let code = "for value in values:\n pass\n";
1765 assert_eq!(python_token_color(code, "in"), Some(MONOKAI_OPERATOR));
1766 assert_eq!(
1767 python_token_color(code, "for"),
1768 Some(Color::Rgb {
1769 r: 0x66,
1770 g: 0xd9,
1771 b: 0xef
1772 })
1773 );
1774 }
1775
1776 #[test]
1778 fn test_python_comment_hash_is_comment_colored() {
1779 let code = "# hello\n";
1780 assert_eq!(python_token_color(code, "#"), Some(MONOKAI_COMMENT));
1781 }
1782
1783 #[test]
1787 fn test_indent_guide_color_monokai_dim_preblend() {
1788 let theme = Syntax::get_theme("monokai");
1789 assert_eq!(
1790 Syntax::indent_guide_color(&*theme, None),
1791 Some(Color::Rgb {
1792 r: 0x6f,
1793 g: 0x6c,
1794 b: 0x5a
1795 })
1796 );
1797 }
1798
1799 #[test]
1800 fn test_embedded_themes() {
1801 let themes = ["dracula", "gruvbox-dark", "nord", "monokai"];
1803 let code = "def hello():\n print('Hello')";
1804
1805 for theme_name in themes {
1806 let theme = Syntax::get_theme(theme_name);
1807 assert!(
1808 theme.syntect_theme().is_some(),
1809 "Theme '{}' should load a syntect theme",
1810 theme_name
1811 );
1812
1813 let syntax = Syntax::new(code, "python3").with_theme(theme_name);
1815 let console = Console::new();
1816 let options = ConsoleOptions::default();
1817 let segments = syntax.render(&console, &options);
1818
1819 assert!(
1821 !segments.is_empty(),
1822 "Theme '{}' should produce output",
1823 theme_name
1824 );
1825 }
1826 }
1827}