From 55fda55b9b286d7cc6702f21217a2b04d815c67d Mon Sep 17 00:00:00 2001 From: Amaan Qureshi Date: Fri, 3 Jan 2025 04:14:42 -0500 Subject: [PATCH] feat(cli): rework highlight to use new input handler Co-authored-by: Will Lillis --- cli/src/highlight.rs | 207 +++++++++++++++++++++++----------------- cli/src/main.rs | 221 ++++++++++++++++++++++++------------------- 2 files changed, 247 insertions(+), 181 deletions(-) diff --git a/cli/src/highlight.rs b/cli/src/highlight.rs index 4c72d493..20364b53 100644 --- a/cli/src/highlight.rs +++ b/cli/src/highlight.rs @@ -1,10 +1,11 @@ use std::{ - collections::HashMap, + collections::{HashMap, HashSet}, fmt::Write, fs, io::{self, Write as _}, - path, str, - sync::atomic::AtomicUsize, + path::{self, Path, PathBuf}, + str, + sync::{atomic::AtomicUsize, Arc}, time::Instant, }; @@ -340,108 +341,142 @@ fn closest_xterm_color(red: u8, green: u8, blue: u8) -> Color { )) } -pub fn ansi( - loader: &Loader, - theme: &Theme, - source: &[u8], - config: &HighlightConfiguration, - print_time: bool, - cancellation_flag: Option<&AtomicUsize>, -) -> Result<()> { - let stdout = io::stdout(); - let mut stdout = stdout.lock(); - let time = Instant::now(); - let mut highlighter = Highlighter::new(); - - let events = highlighter.highlight(config, source, cancellation_flag, |string| { - loader.highlight_config_for_injection_string(string) - })?; - - let mut style_stack = vec![theme.default_style().ansi]; - for event in events { - match event? { - HighlightEvent::HighlightStart(highlight) => { - style_stack.push(theme.styles[highlight.0].ansi); - } - HighlightEvent::HighlightEnd => { - style_stack.pop(); - } - HighlightEvent::Source { start, end } => { - let style = style_stack.last().unwrap(); - write!(&mut stdout, "{style}").unwrap(); - stdout.write_all(&source[start..end])?; - write!(&mut stdout, "{style:#}").unwrap(); - } - } - } - - if print_time { - eprintln!("Time: {}ms", time.elapsed().as_millis()); - } - - Ok(()) -} - -pub struct HtmlOptions { +pub struct HighlightOptions { + pub theme: Theme, + pub check: bool, + pub captures_path: Option, pub inline_styles: bool, + pub html: bool, pub quiet: bool, pub print_time: bool, + pub cancellation_flag: Arc, } -pub fn html( +pub fn highlight( loader: &Loader, - theme: &Theme, - source: &[u8], + path: &Path, + name: &str, config: &HighlightConfiguration, - opts: &HtmlOptions, - cancellation_flag: Option<&AtomicUsize>, + print_name: bool, + opts: &HighlightOptions, ) -> Result<()> { - use std::io::Write; + if opts.check { + let names = if let Some(path) = opts.captures_path.as_deref() { + let file = fs::read_to_string(path)?; + let capture_names = file + .lines() + .filter_map(|line| { + if line.trim().is_empty() || line.trim().starts_with(';') { + return None; + } + line.split(';').next().map(|s| s.trim().trim_matches('"')) + }) + .collect::>(); + config.nonconformant_capture_names(&capture_names) + } else { + config.nonconformant_capture_names(&HashSet::new()) + }; + if names.is_empty() { + eprintln!("All highlight captures conform to standards."); + } else { + eprintln!( + "Non-standard highlight {} detected:", + if names.len() > 1 { + "captures" + } else { + "capture" + } + ); + for name in names { + eprintln!("* {name}"); + } + } + } + let source = fs::read(path)?; let stdout = io::stdout(); let mut stdout = stdout.lock(); let time = Instant::now(); let mut highlighter = Highlighter::new(); + let events = + highlighter.highlight(config, &source, Some(&opts.cancellation_flag), |string| { + loader.highlight_config_for_injection_string(string) + })?; + let theme = &opts.theme; - let events = highlighter.highlight(config, source, cancellation_flag, |string| { - loader.highlight_config_for_injection_string(string) - })?; + if !opts.quiet && print_name { + writeln!(&mut stdout, "{name}")?; + } - let mut renderer = HtmlRenderer::new(); - renderer.render(events, source, &move |highlight, output| { - if opts.inline_styles { - output.extend(b"style='"); - output.extend( - theme.styles[highlight.0] - .css - .as_ref() - .map_or_else(|| "".as_bytes(), |css_style| css_style.as_bytes()), - ); - output.extend(b"'"); - } else { - output.extend(b"class='"); - let mut parts = theme.highlight_names[highlight.0].split('.').peekable(); - while let Some(part) = parts.next() { - output.extend(part.as_bytes()); - if parts.peek().is_some() { - output.extend(b" "); + if opts.html { + if !opts.quiet { + writeln!(&mut stdout, "{HTML_HEAD_HEADER}")?; + writeln!(&mut stdout, " ")?; + writeln!(&mut stdout, "{HTML_BODY_HEADER}")?; } - writeln!(&mut stdout, "")?; + let mut renderer = HtmlRenderer::new(); + renderer.render(events, &source, &move |highlight, output| { + if opts.inline_styles { + output.extend(b"style='"); + output.extend( + theme.styles[highlight.0] + .css + .as_ref() + .map_or_else(|| "".as_bytes(), |css_style| css_style.as_bytes()), + ); + output.extend(b"'"); + } else { + output.extend(b"class='"); + let mut parts = theme.highlight_names[highlight.0].split('.').peekable(); + while let Some(part) = parts.next() { + output.extend(part.as_bytes()); + if parts.peek().is_some() { + output.extend(b" "); + } + } + output.extend(b"'"); + } + })?; + + if !opts.quiet { + writeln!(&mut stdout, "")?; + for (i, line) in renderer.lines().enumerate() { + writeln!( + &mut stdout, + "", + i + 1, + )?; + } + writeln!(&mut stdout, "
{}{line}
")?; + writeln!(&mut stdout, "{HTML_FOOTER}")?; + } + } else { + let mut style_stack = vec![theme.default_style().ansi]; + for event in events { + match event? { + HighlightEvent::HighlightStart(highlight) => { + style_stack.push(theme.styles[highlight.0].ansi); + } + HighlightEvent::HighlightEnd => { + style_stack.pop(); + } + HighlightEvent::Source { start, end } => { + let style = style_stack.last().unwrap(); + write!(&mut stdout, "{style}").unwrap(); + stdout.write_all(&source[start..end])?; + write!(&mut stdout, "{style:#}").unwrap(); + } + } + } } if opts.print_time { diff --git a/cli/src/main.rs b/cli/src/main.rs index f41bf72e..398027d8 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -378,7 +378,7 @@ struct Highlight { )] pub check: bool, #[arg(long, help = "The path to a file with captures")] - pub captures_path: Option, + pub captures_path: Option, #[arg(long, num_args = 1.., help = "The paths to files with queries")] pub query_paths: Option>, #[arg( @@ -394,11 +394,14 @@ struct Highlight { long = "paths", help = "The path to a file with paths to source file(s)" )] - pub paths_file: Option, + pub paths_file: Option, #[arg(num_args = 1.., help = "The source file(s) to use")] - pub paths: Option>, + pub paths: Option>, #[arg(long, help = "The path to an alternative config.json file")] pub config_path: Option, + #[arg(long, short = 'n', help = "Highlight the contents of a specific test")] + #[clap(conflicts_with = "paths", conflicts_with = "paths_file")] + pub test_number: Option, } #[derive(Args)] @@ -1271,135 +1274,163 @@ impl Query { } impl Highlight { - fn run(self, mut loader: loader::Loader) -> Result<()> { + fn run(self, mut loader: loader::Loader, current_dir: &Path) -> Result<()> { let config = Config::load(self.config_path)?; let theme_config: tree_sitter_cli::highlight::ThemeConfig = config.get()?; loader.configure_highlights(&theme_config.theme.highlight_names); let loader_config = config.get()?; loader.find_all_languages(&loader_config)?; - let quiet = self.quiet; - let html_mode = quiet || self.html; - let inline_styles = !self.css_classes; - let paths = collect_paths(self.paths_file.as_deref(), self.paths)?; - - if html_mode && !quiet { - println!("{}", highlight::HTML_HEAD_HEADER); - } - let cancellation_flag = util::cancel_on_signal(); - let mut language = None; + let (mut language, mut language_configuration) = (None, None); if let Some(scope) = self.scope.as_deref() { - language = loader.language_configuration_for_scope(scope)?; + if let Some((lang, lang_config)) = loader.language_configuration_for_scope(scope)? { + language = Some(lang); + language_configuration = Some(lang_config); + }; if language.is_none() { return Err(anyhow!("Unknown scope '{scope}'")); } } - for path in paths { - let path = Path::new(&path); - let (language, language_config) = match language.clone() { - Some(v) => v, - None => { - if let Some(v) = loader.language_configuration_for_file_name(path)? { - v - } else { - eprintln!("{}", util::lang_not_found_for_path(path, &loader_config)); - continue; - } - } - }; + let options = HighlightOptions { + theme: theme_config.theme, + check: self.check, + captures_path: self.captures_path, + inline_styles: !self.css_classes, + html: self.html, + quiet: self.quiet, + print_time: self.time, + cancellation_flag: cancellation_flag.clone(), + }; - if let Some(highlight_config) = - language_config.highlight_config(language, self.query_paths.as_deref())? - { - if self.check { - let names = if let Some(path) = self.captures_path.as_deref() { - let path = Path::new(path); - let file = fs::read_to_string(path)?; - let capture_names = file - .lines() - .filter_map(|line| { - if line.trim().is_empty() || line.trim().starts_with(';') { - return None; + let input = get_input( + self.paths_file.as_deref(), + self.paths, + self.test_number, + &cancellation_flag, + )?; + match input { + CliInput::Paths(paths) => { + let print_name = paths.len() > 1; + for path in paths { + let (language, language_config) = + match (language.clone(), language_configuration) { + (Some(l), Some(lc)) => (l, lc), + _ => { + if let Some((lang, lang_config)) = + loader.language_configuration_for_file_name(&path)? + { + (lang, lang_config) + } else { + eprintln!( + "{}", + util::lang_not_found_for_path(&path, &loader_config) + ); + continue; } - line.split(';').next().map(|s| s.trim().trim_matches('"')) - }) - .collect::>(); - highlight_config.nonconformant_capture_names(&capture_names) - } else { - highlight_config.nonconformant_capture_names(&HashSet::new()) - }; - if names.is_empty() { - eprintln!("All highlight captures conform to standards."); + } + }; + + if let Some(highlight_config) = + language_config.highlight_config(language, self.query_paths.as_deref())? + { + highlight::highlight( + &loader, + &path, + &path.display().to_string(), + highlight_config, + print_name, + &options, + )?; } else { eprintln!( - "Non-standard highlight {} detected:", - if names.len() > 1 { - "captures" - } else { - "capture" - } + "No syntax highlighting config found for path {}", + path.display() ); - for name in names { - eprintln!("* {name}"); - } } } + } - if html_mode && !quiet { - println!(" "); - println!("{}", highlight::HTML_BODY_HEADER); + CliInput::Test { + name, + contents, + languages: language_names, + } => { + let path = get_tmp_source_file(&contents)?; + + let languages = loader.languages_at_path(current_dir)?; + let language = languages + .iter() + .find(|(_, n)| language_names.contains(&Box::from(n.as_str()))) + .or_else(|| languages.first()) + .map(|(l, _)| l.clone()) + .ok_or_else(|| anyhow!("No language found in current path"))?; + let language_config = loader + .get_language_configuration_in_current_path() + .ok_or_else(|| anyhow!("No language configuration found in current path"))?; + + if let Some(highlight_config) = + language_config.highlight_config(language, self.query_paths.as_deref())? + { + highlight::highlight(&loader, &path, &name, highlight_config, false, &options)?; + } else { + eprintln!("No syntax highlighting config found for test {name}"); } + fs::remove_file(path)?; + } - let source = fs::read(path)?; - if html_mode { - let html_opts = highlight::HtmlOptions { - inline_styles, - quiet, - print_time: self.time, + CliInput::Stdin(contents) => { + // Place user input and highlight output on separate lines + println!(); + + let path = get_tmp_source_file(&contents)?; + + let (language, language_config) = + if let (Some(l), Some(lc)) = (language.clone(), language_configuration) { + (l, lc) + } else { + let languages = loader.languages_at_path(current_dir)?; + let language = languages + .first() + .map(|(l, _)| l.clone()) + .ok_or_else(|| anyhow!("No language found in current path"))?; + let language_configuration = loader + .get_language_configuration_in_current_path() + .ok_or_else(|| { + anyhow!("No language configuration found in current path") + })?; + (language, language_configuration) }; - highlight::html( + + if let Some(highlight_config) = + language_config.highlight_config(language, self.query_paths.as_deref())? + { + highlight::highlight( &loader, - &theme_config.theme, - &source, + &path, + "stdin", highlight_config, - &html_opts, - Some(&cancellation_flag), + false, + &options, )?; } else { - highlight::ansi( - &loader, - &theme_config.theme, - &source, - highlight_config, - self.time, - Some(&cancellation_flag), - )?; + eprintln!( + "No syntax highlighting config found for path {}", + current_dir.display() + ); } - } else { - eprintln!("No syntax highlighting config found for path {path:?}"); + fs::remove_file(path)?; } } - if html_mode && !quiet { - println!("{}", highlight::HTML_FOOTER); - } Ok(()) } } impl Tags { - fn run(self, mut loader: loader::Loader) -> Result<()> { + fn run(self, mut loader: loader::Loader, current_dir: &Path) -> Result<()> { let config = Config::load(self.config_path)?; let loader_config = config.get()?; loader.find_all_languages(&loader_config)?; @@ -1533,7 +1564,7 @@ fn run() -> Result<()> { Commands::Version(version_options) => version_options.run(current_dir)?, Commands::Fuzz(fuzz_options) => fuzz_options.run(loader, ¤t_dir)?, Commands::Query(query_options) => query_options.run(loader, ¤t_dir)?, - Commands::Highlight(highlight_options) => highlight_options.run(loader)?, + Commands::Highlight(highlight_options) => highlight_options.run(loader, ¤t_dir)?, Commands::Tags(tags_options) => tags_options.run(loader)?, Commands::Playground(playground_options) => playground_options.run(¤t_dir)?, Commands::DumpLanguages(dump_options) => dump_options.run(loader)?,