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 numbers_column_width(&self) -> usize {
1140 if !self.line_numbers {
1141 return 0;
1142 }
1143 let line_count = self.code.lines().count();
1144 let max_line_no = self.start_line + line_count.saturating_sub(1);
1145 let digits = max_line_no.to_string().len();
1146 digits + NUMBERS_COLUMN_DEFAULT_PADDING
1147 }
1148}
1149
1150impl Renderable for Syntax {
1151 fn render(&self, console: &Console<Stdout>, options: &ConsoleOptions) -> Segments {
1152 let mut result = Segments::new();
1153
1154 let (pad_top, pad_right, pad_bottom, pad_left) = self.padding;
1155 let horizontal_padding = pad_left + pad_right;
1156
1157 let numbers_width = self.numbers_column_width();
1159 let code_width = if let Some(w) = self.code_width {
1160 w
1161 } else if self.line_numbers {
1162 options
1163 .max_width
1164 .saturating_sub(numbers_width + 1 + horizontal_padding)
1165 } else {
1166 options.max_width.saturating_sub(horizontal_padding)
1167 };
1168
1169 let effective_theme: Option<Box<dyn SyntaxTheme>> =
1171 if !self.explicit_theme && options.theme_name != "default" {
1172 Some(Self::get_theme(&options.theme_name))
1173 } else {
1174 None
1175 };
1176
1177 let text = if let Some(ref theme) = effective_theme {
1179 self.highlight_with_theme(&**theme)
1180 } else {
1181 self.highlight()
1182 };
1183
1184 let lines: Vec<Text> = text.split("\n", false, true);
1186
1187 let (start_idx, end_idx) = if let Some((start, end)) = self.line_range {
1189 let start = start.map(|s| s.saturating_sub(1)).unwrap_or(0);
1190 let end = end.unwrap_or(lines.len());
1191 (start, end)
1192 } else {
1193 (0, lines.len())
1194 };
1195
1196 let filtered_lines: Vec<&Text> = lines
1197 .iter()
1198 .skip(start_idx)
1199 .take(end_idx.saturating_sub(start_idx))
1200 .collect();
1201
1202 let base_style = if let Some(ref theme) = effective_theme {
1204 self.get_base_style_with_theme(&**theme)
1205 } else {
1206 self.get_base_style()
1207 };
1208 let new_line = Segment::line();
1209
1210 let number_style = base_style.combine(&Style::new().with_color(Color::Rgb {
1213 r: 101,
1214 g: 102,
1215 b: 96,
1216 }));
1217
1218 let highlight_number_style = base_style.combine(&Style::new().with_bold(true));
1220
1221 let indent_guide_style = base_style.combine(
1224 &Style::new()
1225 .with_color(Color::Rgb {
1226 r: 149,
1227 g: 144,
1228 b: 119,
1229 })
1230 .with_dim(true),
1231 );
1232
1233 if pad_top > 0 {
1235 let blank = " ".repeat(options.max_width);
1236 for _ in 0..pad_top {
1237 result.push(Segment::styled(blank.clone(), base_style));
1238 result.push(new_line.clone());
1239 }
1240 }
1241
1242 for (idx, line) in filtered_lines.iter().enumerate() {
1244 let line_no = self.start_line + start_idx + idx;
1245 let is_highlighted = self.highlight_lines.contains(&line_no);
1246
1247 if pad_left > 0 {
1249 result.push(Segment::styled(" ".repeat(pad_left), base_style));
1250 }
1251
1252 if self.line_numbers {
1254 let pointer = if options.legacy_windows { "> " } else { "❱ " };
1257 let line_num_str = format!(
1258 "{:>width$} ",
1259 line_no,
1260 width = numbers_width.saturating_sub(2)
1261 );
1262
1263 if is_highlighted {
1264 let pointer_style = base_style
1266 .combine(&Style::new().with_color(Color::Standard(1)).with_bold(true));
1267 result.push(Segment::styled(pointer.to_string(), pointer_style));
1268 result.push(Segment::styled(line_num_str, highlight_number_style));
1269 } else {
1270 result.push(Segment::styled(" ".to_string(), highlight_number_style));
1272 result.push(Segment::styled(line_num_str, number_style));
1273 }
1274 }
1275
1276 let mut guides_width = 0;
1278 let line_to_render;
1279
1280 if self.indent_guides {
1281 let plain = line.plain_text();
1282 let leading_spaces = plain.chars().take_while(|c| *c == ' ').count();
1283 let num_guides = leading_spaces / self.tab_size;
1284
1285 if num_guides > 0 {
1286 for _ in 0..num_guides {
1288 result.push(Segment::styled("│".to_string(), indent_guide_style));
1289 result.push(Segment::styled(
1290 " ".repeat(self.tab_size - 1),
1291 indent_guide_style,
1292 ));
1293 }
1294 guides_width = num_guides * self.tab_size;
1295
1296 let parts = line.divide([guides_width]);
1298 line_to_render = if parts.len() > 1 {
1299 parts.into_iter().nth(1).unwrap_or_else(|| (*line).clone())
1300 } else {
1301 Text::plain("")
1303 };
1304 } else {
1305 line_to_render = (*line).clone();
1306 }
1307 } else {
1308 line_to_render = (*line).clone();
1309 }
1310
1311 let content_width = code_width.saturating_sub(guides_width);
1314 let mut line_options = options.update_width(content_width);
1315 line_options.no_wrap = true;
1316 line_options.overflow = Some(OverflowMethod::Crop);
1317
1318 let line_segments: Vec<Segment> = line_to_render
1319 .render(console, &line_options)
1320 .into_iter()
1321 .collect();
1322 let adjusted =
1323 Segment::adjust_line_length(&line_segments, content_width, Some(base_style), true);
1324 for seg in adjusted {
1325 result.push(seg);
1326 }
1327
1328 if pad_right > 0 {
1330 result.push(Segment::styled(" ".repeat(pad_right), base_style));
1331 }
1332
1333 result.push(new_line.clone());
1334 }
1335
1336 if pad_bottom > 0 {
1338 let blank = " ".repeat(options.max_width);
1339 for _ in 0..pad_bottom {
1340 result.push(Segment::styled(blank.clone(), base_style));
1341 result.push(new_line.clone());
1342 }
1343 }
1344
1345 result
1346 }
1347
1348 fn measure(&self, _console: &Console<Stdout>, options: &ConsoleOptions) -> Measurement {
1349 let (_, pad_right, _, pad_left) = self.padding;
1350 let horizontal_padding = pad_left + pad_right;
1351
1352 let numbers_width = self.numbers_column_width();
1353
1354 if let Some(code_width) = self.code_width {
1355 let width = code_width + numbers_width + horizontal_padding;
1356 if self.line_numbers {
1357 return Measurement::new(numbers_width, width + 1);
1358 }
1359 return Measurement::new(numbers_width, width);
1360 }
1361
1362 let lines: Vec<&str> = self.code.lines().collect();
1364 let max_line_width = lines.iter().map(|l| cell_len(l)).max().unwrap_or(0);
1365
1366 let width = max_line_width + numbers_width + horizontal_padding;
1367 let width = if self.line_numbers { width + 1 } else { width };
1368
1369 Measurement::new(numbers_width.max(1), width.min(options.max_width))
1370 }
1371}
1372
1373#[cfg(test)]
1378mod tests {
1379 use super::*;
1380
1381 #[test]
1382 fn test_syntax_new() {
1383 let syntax = Syntax::new("fn main() {}", "rust");
1384 assert_eq!(syntax.code(), "fn main() {}");
1385 assert_eq!(syntax.lexer(), "rust");
1386 }
1387
1388 #[test]
1389 fn test_syntax_with_line_numbers() {
1390 let syntax = Syntax::new("fn main() {}", "rust").with_line_numbers(true);
1391 assert!(syntax.line_numbers());
1392 }
1393
1394 #[test]
1395 fn test_syntax_with_theme() {
1396 let syntax = Syntax::new("fn main() {}", "rust").with_theme("monokai");
1397 assert_eq!(syntax.code(), "fn main() {}");
1399 }
1400
1401 #[test]
1402 fn test_syntax_with_tab_size() {
1403 let syntax = Syntax::new("fn main() {}", "rust").with_tab_size(2);
1404 assert_eq!(syntax.tab_size(), 2);
1405 }
1406
1407 #[test]
1408 fn test_syntax_highlight() {
1409 let syntax = Syntax::new("fn main() {}", "rust");
1410 let text = syntax.highlight();
1411 assert!(text.plain_text().contains("fn"));
1413 assert!(text.plain_text().contains("main"));
1414 }
1415
1416 #[test]
1417 fn test_syntax_highlight_python() {
1418 let code = r#"def hello():
1419 print("Hello, World!")
1420"#;
1421 let syntax = Syntax::new(code, "python");
1422 let text = syntax.highlight();
1423 assert!(text.plain_text().contains("def"));
1424 assert!(text.plain_text().contains("hello"));
1425 }
1426
1427 #[test]
1428 fn test_syntax_dedent() {
1429 let code = " fn main() {\n println!(\"hello\");\n }";
1430 let syntax = Syntax::new(code, "rust").with_dedent(true);
1431 let text = syntax.highlight();
1432 assert!(text.plain_text().starts_with("fn"));
1434 }
1435
1436 #[test]
1437 fn test_syntax_expand_tabs() {
1438 let code = "fn main() {\n\tprintln!(\"hello\");\n}";
1439 let syntax = Syntax::new(code, "rust").with_tab_size(4);
1440 let text = syntax.highlight();
1441 assert!(!text.plain_text().contains('\t'));
1443 }
1444
1445 #[test]
1446 fn test_guess_lexer_by_extension() {
1447 assert_eq!(Syntax::guess_lexer("test.rs", None), "rust");
1448 assert_eq!(Syntax::guess_lexer("test.py", None), "python");
1449 assert_eq!(Syntax::guess_lexer("test.js", None), "javascript");
1450 }
1451
1452 #[test]
1453 fn test_available_themes() {
1454 let themes = Syntax::available_themes();
1455 assert!(!themes.is_empty());
1456 assert!(themes.contains(&"ansi_dark"));
1457 assert!(themes.contains(&"ansi_light"));
1458 assert!(themes.contains(&"dracula"), "Should contain dracula theme");
1460 assert!(
1461 themes.contains(&"gruvbox-dark"),
1462 "Should contain gruvbox-dark theme"
1463 );
1464 assert!(themes.contains(&"nord"), "Should contain nord theme");
1465 assert!(themes.contains(&"monokai"), "Should contain monokai theme");
1466 assert!(
1467 themes.contains(&"monokai-plus"),
1468 "Should contain monokai-plus theme"
1469 );
1470 }
1471
1472 #[test]
1473 fn test_available_languages() {
1474 let languages = Syntax::available_languages();
1475 assert!(!languages.is_empty());
1476 }
1477
1478 #[test]
1479 fn test_numbers_column_width() {
1480 let code = "line1\nline2\nline3\nline4\nline5\nline6\nline7\nline8\nline9\nline10";
1481 let syntax = Syntax::new(code, "text").with_line_numbers(true);
1482 assert_eq!(syntax.numbers_column_width(), 4);
1484 }
1485
1486 #[test]
1487 fn test_syntax_render() {
1488 let syntax = Syntax::new("fn main() {}", "rust");
1489 let console = Console::new();
1490 let options = ConsoleOptions::default();
1491
1492 let segments = syntax.render(&console, &options);
1493 let output: String = segments.iter().map(|s| s.text.to_string()).collect();
1494
1495 assert!(output.contains("fn"));
1496 assert!(output.contains("main"));
1497 }
1498
1499 #[test]
1500 fn test_syntax_render_with_line_numbers() {
1501 let syntax = Syntax::new("line1\nline2", "text").with_line_numbers(true);
1502 let console = Console::new();
1503 let options = ConsoleOptions::default();
1504
1505 let segments = syntax.render(&console, &options);
1506 let output: String = segments.iter().map(|s| s.text.to_string()).collect();
1507
1508 assert!(output.contains('1'));
1510 assert!(output.contains('2'));
1511 }
1512
1513 #[test]
1514 fn test_syntax_measure() {
1515 let syntax = Syntax::new("hello", "text");
1516 let console = Console::new();
1517 let options = ConsoleOptions::default();
1518
1519 let measurement = syntax.measure(&console, &options);
1520 assert!(measurement.maximum >= 5); }
1522
1523 #[test]
1524 fn test_syntax_is_send_sync() {
1525 fn assert_send<T: Send>() {}
1526 fn assert_sync<T: Sync>() {}
1527 assert_send::<Syntax>();
1528 assert_sync::<Syntax>();
1529 }
1530
1531 #[test]
1532 fn test_dedent_code() {
1533 let code = " line1\n line2\n line3\n";
1534 let dedented = Syntax::dedent_code(code);
1535 assert_eq!(dedented, "line1\nline2\nline3\n");
1536 }
1537
1538 #[test]
1539 fn test_dedent_code_mixed_indent() {
1540 let code = " line1\n line2\n line3\n";
1541 let dedented = Syntax::dedent_code(code);
1542 assert_eq!(dedented, "line1\n line2\nline3\n");
1543 }
1544
1545 #[test]
1546 fn test_expand_tabs() {
1547 let code = "a\tb\tc";
1548 let expanded = Syntax::expand_tabs(code, 4);
1549 assert_eq!(expanded, "a b c");
1550 }
1551
1552 #[test]
1553 fn test_expand_tabs_preserves_newlines() {
1554 let code = "a\tb\nc\td";
1555 let expanded = Syntax::expand_tabs(code, 4);
1556 assert_eq!(expanded, "a b\nc d");
1557 }
1558
1559 #[test]
1560 fn test_ansi_theme() {
1561 let theme = AnsiTheme::dark();
1562 let style = SyntectStyle {
1563 foreground: syntect::highlighting::Color {
1564 r: 255,
1565 g: 0,
1566 b: 0,
1567 a: 255,
1568 },
1569 background: syntect::highlighting::Color {
1570 r: 0,
1571 g: 0,
1572 b: 0,
1573 a: 255,
1574 },
1575 font_style: syntect::highlighting::FontStyle::empty(),
1576 };
1577 let rich_style = theme.get_style(&style);
1578 assert!(rich_style.color.is_some());
1580 }
1581
1582 #[test]
1583 fn test_syntect_theme() {
1584 let theme = SyntectTheme::from_name(FALLBACK_SYNTECT_THEME);
1586 assert!(
1587 theme.is_some(),
1588 "Fallback theme '{}' should exist",
1589 FALLBACK_SYNTECT_THEME
1590 );
1591 let theme = theme.unwrap();
1592 assert!(theme.syntect_theme().is_some());
1593
1594 let default_theme = Syntax::get_theme(DEFAULT_THEME);
1596 assert!(default_theme.syntect_theme().is_some());
1597 }
1598
1599 #[test]
1600 fn test_line_range() {
1601 let code = "line1\nline2\nline3\nline4\nline5";
1602 let syntax = Syntax::new(code, "text").with_line_range(Some(2), Some(4));
1603 let console = Console::new();
1604 let options = ConsoleOptions::default();
1605
1606 let segments = syntax.render(&console, &options);
1607 let output: String = segments.iter().map(|s| s.text.to_string()).collect();
1608
1609 assert!(output.contains("line2"));
1611 assert!(output.contains("line3"));
1612 assert!(output.contains("line4"));
1613 assert!(!output.contains("line1"));
1615 assert!(!output.contains("line5"));
1616 }
1617
1618 #[test]
1619 fn test_highlight_lines() {
1620 let code = "line1\nline2\nline3";
1621 let syntax = Syntax::new(code, "text")
1622 .with_line_numbers(true)
1623 .with_highlight_lines([2]);
1624 let console = Console::new();
1625 let options = ConsoleOptions::default();
1626
1627 let segments = syntax.render(&console, &options);
1628 let output: String = segments.iter().map(|s| s.text.to_string()).collect();
1629
1630 assert!(output.contains('❱') || output.contains('>'));
1632 }
1633
1634 #[test]
1635 fn test_stylize_range() {
1636 let mut syntax = Syntax::new("line1\nline2\nline3", "text");
1637 let style = Style::new().with_bold(true);
1638 syntax.stylize_range(style, (2, 0), (2, 5)); let text = syntax.highlight();
1641 let plain = text.plain_text();
1642 assert!(plain.contains("line2"));
1643
1644 let spans = text.spans();
1646 let has_bold_span = spans.iter().any(|s| s.style.bold == Some(true));
1647 assert!(has_bold_span, "Should have a bold span from stylize_range");
1648 }
1649
1650 #[test]
1651 fn test_with_highlight_range_builder() {
1652 let style = Style::new().with_italic(true);
1653 let syntax = Syntax::new("hello world", "text").with_highlight_range(style, (1, 0), (1, 5)); let text = syntax.highlight();
1656 let spans = text.spans();
1657 let has_italic_span = spans.iter().any(|s| s.style.italic == Some(true));
1658 assert!(
1659 has_italic_span,
1660 "Should have an italic span from with_highlight_range"
1661 );
1662 }
1663
1664 #[test]
1665 fn test_embedded_themes() {
1666 let themes = ["dracula", "gruvbox-dark", "nord", "monokai"];
1668 let code = "def hello():\n print('Hello')";
1669
1670 for theme_name in themes {
1671 let theme = Syntax::get_theme(theme_name);
1672 assert!(
1673 theme.syntect_theme().is_some(),
1674 "Theme '{}' should load a syntect theme",
1675 theme_name
1676 );
1677
1678 let syntax = Syntax::new(code, "python3").with_theme(theme_name);
1680 let console = Console::new();
1681 let options = ConsoleOptions::default();
1682 let segments = syntax.render(&console, &options);
1683
1684 assert!(
1686 !segments.is_empty(),
1687 "Theme '{}' should produce output",
1688 theme_name
1689 );
1690 }
1691 }
1692}