feat(cli): rework highlight to use new input handler
Co-authored-by: Will Lillis <will.lillis24@gmail.com>
This commit is contained in:
parent
88d2f010f5
commit
55fda55b9b
2 changed files with 247 additions and 181 deletions
|
|
@ -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<PathBuf>,
|
||||
pub inline_styles: bool,
|
||||
pub html: bool,
|
||||
pub quiet: bool,
|
||||
pub print_time: bool,
|
||||
pub cancellation_flag: Arc<AtomicUsize>,
|
||||
}
|
||||
|
||||
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::<HashSet<_>>();
|
||||
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, " <style>")?;
|
||||
let names = theme.highlight_names.iter();
|
||||
let styles = theme.styles.iter();
|
||||
for (name, style) in names.zip(styles) {
|
||||
if let Some(css) = &style.css {
|
||||
writeln!(&mut stdout, " .{name} {{ {css}; }}")?;
|
||||
}
|
||||
}
|
||||
output.extend(b"'");
|
||||
}
|
||||
})?;
|
||||
|
||||
if !opts.quiet {
|
||||
writeln!(&mut stdout, "<table>")?;
|
||||
for (i, line) in renderer.lines().enumerate() {
|
||||
writeln!(
|
||||
&mut stdout,
|
||||
"<tr><td class=line-number>{}</td><td class=line>{line}</td></tr>",
|
||||
i + 1,
|
||||
)?;
|
||||
writeln!(&mut stdout, " </style>")?;
|
||||
writeln!(&mut stdout, "{HTML_BODY_HEADER}")?;
|
||||
}
|
||||
|
||||
writeln!(&mut stdout, "</table>")?;
|
||||
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, "<table>")?;
|
||||
for (i, line) in renderer.lines().enumerate() {
|
||||
writeln!(
|
||||
&mut stdout,
|
||||
"<tr><td class=line-number>{}</td><td class=line>{line}</td></tr>",
|
||||
i + 1,
|
||||
)?;
|
||||
}
|
||||
writeln!(&mut stdout, "</table>")?;
|
||||
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 {
|
||||
|
|
|
|||
221
cli/src/main.rs
221
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<String>,
|
||||
pub captures_path: Option<PathBuf>,
|
||||
#[arg(long, num_args = 1.., help = "The paths to files with queries")]
|
||||
pub query_paths: Option<Vec<String>>,
|
||||
#[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<String>,
|
||||
pub paths_file: Option<PathBuf>,
|
||||
#[arg(num_args = 1.., help = "The source file(s) to use")]
|
||||
pub paths: Option<Vec<String>>,
|
||||
pub paths: Option<Vec<PathBuf>>,
|
||||
#[arg(long, help = "The path to an alternative config.json file")]
|
||||
pub config_path: Option<PathBuf>,
|
||||
#[arg(long, short = 'n', help = "Highlight the contents of a specific test")]
|
||||
#[clap(conflicts_with = "paths", conflicts_with = "paths_file")]
|
||||
pub test_number: Option<u32>,
|
||||
}
|
||||
|
||||
#[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::<HashSet<_>>();
|
||||
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!(" <style>");
|
||||
let names = theme_config.theme.highlight_names.iter();
|
||||
let styles = theme_config.theme.styles.iter();
|
||||
for (name, style) in names.zip(styles) {
|
||||
if let Some(css) = &style.css {
|
||||
println!(" .{name} {{ {css}; }}");
|
||||
}
|
||||
}
|
||||
println!(" </style>");
|
||||
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)?,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue