Merge pull request #499 from tree-sitter/highlight-test
Add a system for testing syntax highlighting queries
This commit is contained in:
commit
d6c7b243a7
15 changed files with 750 additions and 365 deletions
|
|
@ -1,3 +1,4 @@
|
|||
use super::test_highlight;
|
||||
use std::fmt::Write;
|
||||
use std::io;
|
||||
use tree_sitter::QueryError;
|
||||
|
|
@ -98,6 +99,12 @@ impl From<regex_syntax::ast::Error> for Error {
|
|||
}
|
||||
}
|
||||
|
||||
impl From<test_highlight::Failure> for Error {
|
||||
fn from(error: test_highlight::Failure) -> Self {
|
||||
Error::new(error.message())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for Error {
|
||||
fn from(error: String) -> Self {
|
||||
Error::new(error)
|
||||
|
|
|
|||
|
|
@ -10,9 +10,7 @@ use std::sync::atomic::{AtomicUsize, Ordering};
|
|||
use std::sync::Arc;
|
||||
use std::time::Instant;
|
||||
use std::{fs, io, path, str, thread, usize};
|
||||
use tree_sitter_highlight::{
|
||||
HighlightConfiguration, HighlightContext, HighlightEvent, Highlighter, HtmlRenderer,
|
||||
};
|
||||
use tree_sitter_highlight::{HighlightConfiguration, HighlightEvent, Highlighter, HtmlRenderer};
|
||||
|
||||
pub const HTML_HEADER: &'static str = "
|
||||
<!doctype HTML>
|
||||
|
|
@ -53,8 +51,8 @@ pub struct Style {
|
|||
|
||||
#[derive(Debug)]
|
||||
pub struct Theme {
|
||||
pub highlighter: Highlighter,
|
||||
styles: Vec<Style>,
|
||||
pub styles: Vec<Style>,
|
||||
pub highlight_names: Vec<String>,
|
||||
}
|
||||
|
||||
impl Theme {
|
||||
|
|
@ -73,21 +71,21 @@ impl<'de> Deserialize<'de> for Theme {
|
|||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let mut names = Vec::new();
|
||||
let mut styles = Vec::new();
|
||||
let mut highlight_names = Vec::new();
|
||||
if let Ok(colors) = HashMap::<String, Value>::deserialize(deserializer) {
|
||||
names.reserve(colors.len());
|
||||
highlight_names.reserve(colors.len());
|
||||
styles.reserve(colors.len());
|
||||
for (name, style_value) in colors {
|
||||
let mut style = Style::default();
|
||||
parse_style(&mut style, style_value);
|
||||
names.push(name);
|
||||
highlight_names.push(name);
|
||||
styles.push(style);
|
||||
}
|
||||
}
|
||||
Ok(Self {
|
||||
highlighter: Highlighter::new(names),
|
||||
styles,
|
||||
highlight_names,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -98,7 +96,7 @@ impl Serialize for Theme {
|
|||
S: Serializer,
|
||||
{
|
||||
let mut map = serializer.serialize_map(Some(self.styles.len()))?;
|
||||
for (name, style) in self.highlighter.names().iter().zip(&self.styles) {
|
||||
for (name, style) in self.highlight_names.iter().zip(&self.styles) {
|
||||
let style = &style.ansi;
|
||||
let color = style.foreground.map(|color| match color {
|
||||
Color::Black => json!("black"),
|
||||
|
|
@ -284,15 +282,11 @@ pub fn ansi(
|
|||
let mut stdout = stdout.lock();
|
||||
let time = Instant::now();
|
||||
let cancellation_flag = cancel_on_stdin();
|
||||
let mut context = HighlightContext::new();
|
||||
let mut highlighter = Highlighter::new();
|
||||
|
||||
let events = theme.highlighter.highlight(
|
||||
&mut context,
|
||||
config,
|
||||
source,
|
||||
Some(&cancellation_flag),
|
||||
|string| language_for_injection_string(loader, theme, string),
|
||||
)?;
|
||||
let events = highlighter.highlight(config, source, Some(&cancellation_flag), |string| {
|
||||
loader.highlight_config_for_injection_string(string)
|
||||
})?;
|
||||
|
||||
let mut style_stack = vec![theme.default_style().ansi];
|
||||
for event in events {
|
||||
|
|
@ -333,15 +327,11 @@ pub fn html(
|
|||
let mut stdout = stdout.lock();
|
||||
let time = Instant::now();
|
||||
let cancellation_flag = cancel_on_stdin();
|
||||
let mut context = HighlightContext::new();
|
||||
let mut highlighter = Highlighter::new();
|
||||
|
||||
let events = theme.highlighter.highlight(
|
||||
&mut context,
|
||||
config,
|
||||
source,
|
||||
Some(&cancellation_flag),
|
||||
|string| language_for_injection_string(loader, theme, string),
|
||||
)?;
|
||||
let events = highlighter.highlight(config, source, Some(&cancellation_flag), |string| {
|
||||
loader.highlight_config_for_injection_string(string)
|
||||
})?;
|
||||
|
||||
let mut renderer = HtmlRenderer::new();
|
||||
renderer.render(events, source, &move |highlight| {
|
||||
|
|
@ -370,35 +360,3 @@ pub fn html(
|
|||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn language_for_injection_string<'a>(
|
||||
loader: &'a Loader,
|
||||
theme: &Theme,
|
||||
string: &str,
|
||||
) -> Option<&'a HighlightConfiguration> {
|
||||
match loader.language_configuration_for_injection_string(string) {
|
||||
Err(e) => {
|
||||
eprintln!(
|
||||
"Failed to load language for injection string '{}': {}",
|
||||
string,
|
||||
e.message()
|
||||
);
|
||||
None
|
||||
}
|
||||
Ok(None) => None,
|
||||
Ok(Some((language, configuration))) => {
|
||||
match configuration.highlight_config(&theme.highlighter, language) {
|
||||
Err(e) => {
|
||||
eprintln!(
|
||||
"Failed to load property sheet for injection string '{}': {}",
|
||||
string,
|
||||
e.message()
|
||||
);
|
||||
None
|
||||
}
|
||||
Ok(None) => None,
|
||||
Ok(Some(config)) => Some(config),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ pub mod logger;
|
|||
pub mod parse;
|
||||
pub mod query;
|
||||
pub mod test;
|
||||
pub mod test_highlight;
|
||||
pub mod util;
|
||||
pub mod wasm;
|
||||
pub mod web_ui;
|
||||
|
|
|
|||
|
|
@ -7,10 +7,11 @@ use std::collections::HashMap;
|
|||
use std::io::BufReader;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
use std::sync::Mutex;
|
||||
use std::time::SystemTime;
|
||||
use std::{fs, mem};
|
||||
use tree_sitter::Language;
|
||||
use tree_sitter_highlight::{HighlightConfiguration, Highlighter};
|
||||
use tree_sitter_highlight::HighlightConfiguration;
|
||||
|
||||
#[cfg(unix)]
|
||||
const DYLIB_EXTENSION: &'static str = "so";
|
||||
|
|
@ -20,8 +21,7 @@ const DYLIB_EXTENSION: &'static str = "dll";
|
|||
|
||||
const BUILD_TARGET: &'static str = env!("BUILD_TARGET");
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct LanguageConfiguration {
|
||||
pub struct LanguageConfiguration<'a> {
|
||||
pub scope: Option<String>,
|
||||
pub content_regex: Option<Regex>,
|
||||
pub _first_line_regex: Option<Regex>,
|
||||
|
|
@ -33,13 +33,17 @@ pub struct LanguageConfiguration {
|
|||
pub locals_filenames: Option<Vec<String>>,
|
||||
language_id: usize,
|
||||
highlight_config: OnceCell<Option<HighlightConfiguration>>,
|
||||
highlight_names: &'a Mutex<Vec<String>>,
|
||||
use_all_highlight_names: bool,
|
||||
}
|
||||
|
||||
pub struct Loader {
|
||||
parser_lib_path: PathBuf,
|
||||
languages_by_id: Vec<(PathBuf, OnceCell<Language>)>,
|
||||
language_configurations: Vec<LanguageConfiguration>,
|
||||
language_configurations: Vec<LanguageConfiguration<'static>>,
|
||||
language_configuration_ids_by_file_type: HashMap<String, Vec<usize>>,
|
||||
highlight_names: Box<Mutex<Vec<String>>>,
|
||||
use_all_highlight_names: bool,
|
||||
}
|
||||
|
||||
unsafe impl Send for Loader {}
|
||||
|
|
@ -52,9 +56,22 @@ impl Loader {
|
|||
languages_by_id: Vec::new(),
|
||||
language_configurations: Vec::new(),
|
||||
language_configuration_ids_by_file_type: HashMap::new(),
|
||||
highlight_names: Box::new(Mutex::new(Vec::new())),
|
||||
use_all_highlight_names: true,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn configure_highlights(&mut self, names: &Vec<String>) {
|
||||
self.use_all_highlight_names = false;
|
||||
let mut highlights = self.highlight_names.lock().unwrap();
|
||||
highlights.clear();
|
||||
highlights.extend(names.iter().cloned());
|
||||
}
|
||||
|
||||
pub fn highlight_names(&self) -> Vec<String> {
|
||||
self.highlight_names.lock().unwrap().clone()
|
||||
}
|
||||
|
||||
pub fn find_all_languages(&mut self, parser_src_paths: &Vec<PathBuf>) -> Result<()> {
|
||||
for parser_container_dir in parser_src_paths.iter() {
|
||||
if let Ok(entries) = fs::read_dir(parser_container_dir) {
|
||||
|
|
@ -339,7 +356,36 @@ impl Loader {
|
|||
Ok(language)
|
||||
}
|
||||
|
||||
fn find_language_configurations_at_path<'a>(
|
||||
pub fn highlight_config_for_injection_string<'a>(
|
||||
&'a self,
|
||||
string: &str,
|
||||
) -> Option<&'a HighlightConfiguration> {
|
||||
match self.language_configuration_for_injection_string(string) {
|
||||
Err(e) => {
|
||||
eprintln!(
|
||||
"Failed to load language for injection string '{}': {}",
|
||||
string,
|
||||
e.message()
|
||||
);
|
||||
None
|
||||
}
|
||||
Ok(None) => None,
|
||||
Ok(Some((language, configuration))) => match configuration.highlight_config(language) {
|
||||
Err(e) => {
|
||||
eprintln!(
|
||||
"Failed to load property sheet for injection string '{}': {}",
|
||||
string,
|
||||
e.message()
|
||||
);
|
||||
None
|
||||
}
|
||||
Ok(None) => None,
|
||||
Ok(Some(config)) => Some(config),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn find_language_configurations_at_path<'a>(
|
||||
&'a mut self,
|
||||
parser_path: &Path,
|
||||
) -> Result<&[LanguageConfiguration]> {
|
||||
|
|
@ -428,19 +474,15 @@ impl Loader {
|
|||
scope: config_json.scope,
|
||||
language_id,
|
||||
file_types: config_json.file_types.unwrap_or(Vec::new()),
|
||||
content_regex: config_json
|
||||
.content_regex
|
||||
.and_then(|r| RegexBuilder::new(&r).multi_line(true).build().ok()),
|
||||
_first_line_regex: config_json
|
||||
.first_line_regex
|
||||
.and_then(|r| RegexBuilder::new(&r).multi_line(true).build().ok()),
|
||||
injection_regex: config_json
|
||||
.injection_regex
|
||||
.and_then(|r| RegexBuilder::new(&r).multi_line(true).build().ok()),
|
||||
highlight_config: OnceCell::new(),
|
||||
content_regex: Self::regex(config_json.content_regex),
|
||||
_first_line_regex: Self::regex(config_json.first_line_regex),
|
||||
injection_regex: Self::regex(config_json.injection_regex),
|
||||
injections_filenames: config_json.injections.into_vec(),
|
||||
locals_filenames: config_json.locals.into_vec(),
|
||||
highlights_filenames: config_json.highlights.into_vec(),
|
||||
highlight_config: OnceCell::new(),
|
||||
highlight_names: &*self.highlight_names,
|
||||
use_all_highlight_names: self.use_all_highlight_names,
|
||||
};
|
||||
|
||||
for file_type in &configuration.file_types {
|
||||
|
|
@ -450,7 +492,8 @@ impl Loader {
|
|||
.push(self.language_configurations.len());
|
||||
}
|
||||
|
||||
self.language_configurations.push(configuration);
|
||||
self.language_configurations
|
||||
.push(unsafe { mem::transmute(configuration) });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -458,24 +501,37 @@ impl Loader {
|
|||
if self.language_configurations.len() == initial_language_configuration_count
|
||||
&& parser_path.join("src").join("grammar.json").exists()
|
||||
{
|
||||
let mut configuration = LanguageConfiguration::default();
|
||||
configuration.root_path = parser_path.to_owned();
|
||||
configuration.language_id = self.languages_by_id.len();
|
||||
self.language_configurations.push(configuration);
|
||||
let configuration = LanguageConfiguration {
|
||||
root_path: parser_path.to_owned(),
|
||||
language_id: self.languages_by_id.len(),
|
||||
file_types: Vec::new(),
|
||||
scope: None,
|
||||
content_regex: None,
|
||||
_first_line_regex: None,
|
||||
injection_regex: None,
|
||||
injections_filenames: None,
|
||||
locals_filenames: None,
|
||||
highlights_filenames: None,
|
||||
highlight_config: OnceCell::new(),
|
||||
highlight_names: &*self.highlight_names,
|
||||
use_all_highlight_names: self.use_all_highlight_names,
|
||||
};
|
||||
self.language_configurations
|
||||
.push(unsafe { mem::transmute(configuration) });
|
||||
self.languages_by_id
|
||||
.push((parser_path.to_owned(), OnceCell::new()));
|
||||
}
|
||||
|
||||
Ok(&self.language_configurations[initial_language_configuration_count..])
|
||||
}
|
||||
|
||||
fn regex(pattern: Option<String>) -> Option<Regex> {
|
||||
pattern.and_then(|r| RegexBuilder::new(&r).multi_line(true).build().ok())
|
||||
}
|
||||
}
|
||||
|
||||
impl LanguageConfiguration {
|
||||
pub fn highlight_config(
|
||||
&self,
|
||||
highlighter: &Highlighter,
|
||||
language: Language,
|
||||
) -> Result<Option<&HighlightConfiguration>> {
|
||||
impl<'a> LanguageConfiguration<'a> {
|
||||
pub fn highlight_config(&self, language: Language) -> Result<Option<&HighlightConfiguration>> {
|
||||
self.highlight_config
|
||||
.get_or_try_init(|| {
|
||||
let queries_path = self.root_path.join("queries");
|
||||
|
|
@ -508,18 +564,25 @@ impl LanguageConfiguration {
|
|||
if highlights_query.is_empty() {
|
||||
Ok(None)
|
||||
} else {
|
||||
Ok(Some(
|
||||
highlighter
|
||||
.load_configuration(
|
||||
language,
|
||||
&highlights_query,
|
||||
&injections_query,
|
||||
&locals_query,
|
||||
)
|
||||
.map_err(Error::wrap(|| {
|
||||
format!("Failed to load queries in {:?}", self.root_path)
|
||||
}))?,
|
||||
))
|
||||
let mut result = HighlightConfiguration::new(
|
||||
language,
|
||||
&highlights_query,
|
||||
&injections_query,
|
||||
&locals_query,
|
||||
)
|
||||
.map_err(Error::wrap(|| {
|
||||
format!("Failed to load queries in {:?}", self.root_path)
|
||||
}))?;
|
||||
let mut all_highlight_names = self.highlight_names.lock().unwrap();
|
||||
if self.use_all_highlight_names {
|
||||
for capture_name in result.query.capture_names() {
|
||||
if !all_highlight_names.contains(capture_name) {
|
||||
all_highlight_names.push(capture_name.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
result.configure(&all_highlight_names);
|
||||
Ok(Some(result))
|
||||
}
|
||||
})
|
||||
.map(Option::as_ref)
|
||||
|
|
|
|||
|
|
@ -5,7 +5,8 @@ use std::process::exit;
|
|||
use std::{env, fs, u64};
|
||||
use tree_sitter::Language;
|
||||
use tree_sitter_cli::{
|
||||
config, error, generate, highlight, loader, logger, parse, query, test, wasm, web_ui,
|
||||
config, error, generate, highlight, loader, logger, parse, query, test, test_highlight, wasm,
|
||||
web_ui,
|
||||
};
|
||||
|
||||
const BUILD_VERSION: &'static str = env!("CARGO_PKG_VERSION");
|
||||
|
|
@ -162,17 +163,28 @@ fn run() -> error::Result<()> {
|
|||
let debug = matches.is_present("debug");
|
||||
let debug_graph = matches.is_present("debug-graph");
|
||||
let filter = matches.value_of("filter");
|
||||
if let Some(language) = loader.languages_at_path(¤t_dir)?.first() {
|
||||
test::run_tests_at_path(
|
||||
*language,
|
||||
¤t_dir.join("corpus"),
|
||||
debug,
|
||||
debug_graph,
|
||||
filter,
|
||||
)?;
|
||||
test::check_queries_at_path(*language, ¤t_dir.join("queries"))?;
|
||||
} else {
|
||||
eprintln!("No language found");
|
||||
let languages = loader.languages_at_path(¤t_dir)?;
|
||||
let language = languages
|
||||
.first()
|
||||
.ok_or_else(|| "No language found".to_string())?;
|
||||
let test_dir = current_dir.join("test");
|
||||
|
||||
// Run the corpus tests. Look for them at two paths: `test/corpus` and `corpus`.
|
||||
let mut test_corpus_dir = test_dir.join("corpus");
|
||||
if !test_corpus_dir.is_dir() {
|
||||
test_corpus_dir = current_dir.join("corpus");
|
||||
}
|
||||
if test_corpus_dir.is_dir() {
|
||||
test::run_tests_at_path(*language, &test_corpus_dir, debug, debug_graph, filter)?;
|
||||
}
|
||||
|
||||
// Check that all of the queries are valid.
|
||||
test::check_queries_at_path(*language, ¤t_dir.join("queries"))?;
|
||||
|
||||
// Run the syntax highlighting tests.
|
||||
let test_highlight_dir = test_dir.join("highlight");
|
||||
if test_highlight_dir.is_dir() {
|
||||
test_highlight::test_highlights(&loader, &test_highlight_dir)?;
|
||||
}
|
||||
} else if let Some(matches) = matches.subcommand_matches("parse") {
|
||||
let debug = matches.is_present("debug");
|
||||
|
|
@ -232,11 +244,12 @@ fn run() -> error::Result<()> {
|
|||
let query_path = Path::new(matches.value_of("query-path").unwrap());
|
||||
query::query_files_at_paths(language, paths, query_path, ordered_captures)?;
|
||||
} else if let Some(matches) = matches.subcommand_matches("highlight") {
|
||||
let paths = matches.values_of("path").unwrap().into_iter();
|
||||
let html_mode = matches.is_present("html");
|
||||
let time = matches.is_present("time");
|
||||
loader.configure_highlights(&config.theme.highlight_names);
|
||||
loader.find_all_languages(&config.parser_directories)?;
|
||||
|
||||
let time = matches.is_present("time");
|
||||
let paths = matches.values_of("path").unwrap().into_iter();
|
||||
let html_mode = matches.is_present("html");
|
||||
if html_mode {
|
||||
println!("{}", highlight::HTML_HEADER);
|
||||
}
|
||||
|
|
@ -266,9 +279,7 @@ fn run() -> error::Result<()> {
|
|||
|
||||
let source = fs::read(path)?;
|
||||
|
||||
if let Some(highlight_config) =
|
||||
language_config.highlight_config(&config.theme.highlighter, language)?
|
||||
{
|
||||
if let Some(highlight_config) = language_config.highlight_config(language)? {
|
||||
if html_mode {
|
||||
highlight::html(&loader, &config.theme, &source, highlight_config, time)?;
|
||||
} else {
|
||||
|
|
|
|||
305
cli/src/test_highlight.rs
Normal file
305
cli/src/test_highlight.rs
Normal file
|
|
@ -0,0 +1,305 @@
|
|||
use super::error::Result;
|
||||
use crate::loader::Loader;
|
||||
use ansi_term::Colour;
|
||||
use lazy_static::lazy_static;
|
||||
use regex::Regex;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use tree_sitter::{Language, Parser, Point};
|
||||
use tree_sitter_highlight::{Highlight, HighlightConfiguration, HighlightEvent, Highlighter};
|
||||
|
||||
lazy_static! {
|
||||
static ref HIGHLIGHT_NAME_REGEX: Regex = Regex::new("[\\w_\\-.]+").unwrap();
|
||||
}
|
||||
|
||||
pub struct Failure {
|
||||
row: usize,
|
||||
column: usize,
|
||||
expected_highlight: String,
|
||||
actual_highlights: Vec<String>,
|
||||
}
|
||||
|
||||
impl Failure {
|
||||
pub fn message(&self) -> String {
|
||||
let mut result = format!(
|
||||
"Failure - row: {}, column: {}, expected highlight '{}', actual highlights: ",
|
||||
self.row, self.column, self.expected_highlight
|
||||
);
|
||||
if self.actual_highlights.is_empty() {
|
||||
result += "none.";
|
||||
} else {
|
||||
for (i, actual_highlight) in self.actual_highlights.iter().enumerate() {
|
||||
if i > 0 {
|
||||
result += ", ";
|
||||
}
|
||||
result += "'";
|
||||
result += actual_highlight;
|
||||
result += "'";
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
pub fn test_highlights(loader: &Loader, directory: &Path) -> Result<()> {
|
||||
let mut failed = false;
|
||||
let mut highlighter = Highlighter::new();
|
||||
|
||||
println!("syntax highlighting:");
|
||||
for highlight_test_file in fs::read_dir(directory)? {
|
||||
let highlight_test_file = highlight_test_file?;
|
||||
let test_file_path = highlight_test_file.path();
|
||||
let test_file_name = highlight_test_file.file_name();
|
||||
let (language, language_config) = loader
|
||||
.language_configuration_for_file_name(&test_file_path)?
|
||||
.ok_or_else(|| format!("No language found for path {:?}", test_file_path))?;
|
||||
let highlight_config = language_config
|
||||
.highlight_config(language)?
|
||||
.ok_or_else(|| format!("No highlighting config found for {:?}", test_file_path))?;
|
||||
match test_highlight(
|
||||
&loader,
|
||||
&mut highlighter,
|
||||
highlight_config,
|
||||
fs::read(&test_file_path)?.as_slice(),
|
||||
) {
|
||||
Ok(assertion_count) => {
|
||||
println!(
|
||||
" ✓ {} ({} assertions)",
|
||||
Colour::Green.paint(test_file_name.to_string_lossy().as_ref()),
|
||||
assertion_count
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
println!(
|
||||
" ✗ {}",
|
||||
Colour::Red.paint(test_file_name.to_string_lossy().as_ref())
|
||||
);
|
||||
println!(" {}", e.message());
|
||||
failed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if failed {
|
||||
Err(String::new().into())
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn test_highlight(
|
||||
loader: &Loader,
|
||||
highlighter: &mut Highlighter,
|
||||
highlight_config: &HighlightConfiguration,
|
||||
source: &[u8],
|
||||
) -> Result<usize> {
|
||||
// Highlight the file, and parse out all of the highlighting assertions.
|
||||
let highlight_names = loader.highlight_names();
|
||||
let highlights = get_highlight_positions(loader, highlighter, highlight_config, source)?;
|
||||
let assertions = parse_highlight_test(highlighter.parser(), highlight_config.language, source)?;
|
||||
|
||||
// Iterate through all of the highlighting assertions, checking each one against the
|
||||
// actual highlights.
|
||||
let mut i = 0;
|
||||
let mut actual_highlights = Vec::<&String>::new();
|
||||
for (position, expected_highlight) in &assertions {
|
||||
let mut passed = false;
|
||||
actual_highlights.clear();
|
||||
|
||||
'highlight_loop: loop {
|
||||
// The assertions are ordered by position, so skip past all of the highlights that
|
||||
// end at or before this assertion's position.
|
||||
if let Some(highlight) = highlights.get(i) {
|
||||
if highlight.1 <= *position {
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Iterate through all of the highlights that start at or before this assertion's,
|
||||
// position, looking for one that matches the assertion.
|
||||
let mut j = i;
|
||||
while let (false, Some(highlight)) = (passed, highlights.get(j)) {
|
||||
if highlight.0 > *position {
|
||||
break 'highlight_loop;
|
||||
}
|
||||
|
||||
// If the highlight matches the assertion, this test passes. Otherwise,
|
||||
// add this highlight to the list of actual highlights that span the
|
||||
// assertion's position, in order to generate an error message in the event
|
||||
// of a failure.
|
||||
let highlight_name = &highlight_names[(highlight.2).0];
|
||||
if *highlight_name == *expected_highlight {
|
||||
passed = true;
|
||||
break 'highlight_loop;
|
||||
} else {
|
||||
actual_highlights.push(highlight_name);
|
||||
}
|
||||
|
||||
j += 1;
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if !passed {
|
||||
return Err(Failure {
|
||||
row: position.row,
|
||||
column: position.column,
|
||||
expected_highlight: expected_highlight.clone(),
|
||||
actual_highlights: actual_highlights.into_iter().cloned().collect(),
|
||||
}
|
||||
.into());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(assertions.len())
|
||||
}
|
||||
|
||||
/// Parse the given source code, finding all of the comments that contain
|
||||
/// highlighting assertions. Return a vector of (position, expected highlight name)
|
||||
/// pairs.
|
||||
pub fn parse_highlight_test(
|
||||
parser: &mut Parser,
|
||||
language: Language,
|
||||
source: &[u8],
|
||||
) -> Result<Vec<(Point, String)>> {
|
||||
let mut result = Vec::new();
|
||||
let mut assertion_ranges = Vec::new();
|
||||
|
||||
// Parse the code.
|
||||
parser.set_language(language).unwrap();
|
||||
let tree = parser.parse(source, None).unwrap();
|
||||
|
||||
// Walk the tree, finding comment nodes that contain assertions.
|
||||
let mut ascending = false;
|
||||
let mut cursor = tree.root_node().walk();
|
||||
loop {
|
||||
if ascending {
|
||||
let node = cursor.node();
|
||||
|
||||
// Find every comment node.
|
||||
if node.kind().contains("comment") {
|
||||
if let Ok(text) = node.utf8_text(source) {
|
||||
let mut position = node.start_position();
|
||||
if position.row == 0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Find the arrow character ("^" or '<-") in the comment. A left arrow
|
||||
// refers to the column where the comment node starts. An up arrow refers
|
||||
// to its own column.
|
||||
let mut has_left_caret = false;
|
||||
let mut has_arrow = false;
|
||||
let mut arrow_end = 0;
|
||||
for (i, c) in text.char_indices() {
|
||||
arrow_end = i + 1;
|
||||
if c == '-' && has_left_caret {
|
||||
has_arrow = true;
|
||||
break;
|
||||
}
|
||||
if c == '^' {
|
||||
has_arrow = true;
|
||||
position.column += i;
|
||||
break;
|
||||
}
|
||||
has_left_caret = c == '<';
|
||||
}
|
||||
|
||||
// If the comment node contains an arrow and a highlight name, record the
|
||||
// highlight name and the position.
|
||||
if let (true, Some(mat)) =
|
||||
(has_arrow, HIGHLIGHT_NAME_REGEX.find(&text[arrow_end..]))
|
||||
{
|
||||
assertion_ranges.push((node.start_position(), node.end_position()));
|
||||
result.push((position, mat.as_str().to_string()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Continue walking the tree.
|
||||
if cursor.goto_next_sibling() {
|
||||
ascending = false;
|
||||
} else if !cursor.goto_parent() {
|
||||
break;
|
||||
}
|
||||
} else if !cursor.goto_first_child() {
|
||||
ascending = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Adjust the row number in each assertion's position to refer to the line of
|
||||
// code *above* the assertion. There can be multiple lines of assertion comments,
|
||||
// so the positions may have to be decremented by more than one row.
|
||||
let mut i = 0;
|
||||
for (position, _) in result.iter_mut() {
|
||||
loop {
|
||||
let on_assertion_line = assertion_ranges[i..]
|
||||
.iter()
|
||||
.any(|(start, _)| start.row == position.row);
|
||||
if on_assertion_line {
|
||||
position.row -= 1;
|
||||
} else {
|
||||
while i < assertion_ranges.len() && assertion_ranges[i].0.row < position.row {
|
||||
i += 1;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The assertions can end up out of order due to the line adjustments.
|
||||
result.sort_unstable_by_key(|a| a.0);
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub fn get_highlight_positions(
|
||||
loader: &Loader,
|
||||
highlighter: &mut Highlighter,
|
||||
highlight_config: &HighlightConfiguration,
|
||||
source: &[u8],
|
||||
) -> Result<Vec<(Point, Point, Highlight)>> {
|
||||
let mut row = 0;
|
||||
let mut column = 0;
|
||||
let mut byte_offset = 0;
|
||||
let mut was_newline = false;
|
||||
let mut result = Vec::new();
|
||||
let mut highlight_stack = Vec::new();
|
||||
let source = String::from_utf8_lossy(source);
|
||||
let mut char_indices = source.char_indices();
|
||||
for event in highlighter.highlight(highlight_config, source.as_bytes(), None, |string| {
|
||||
loader.highlight_config_for_injection_string(string)
|
||||
})? {
|
||||
match event? {
|
||||
HighlightEvent::HighlightStart(h) => highlight_stack.push(h),
|
||||
HighlightEvent::HighlightEnd => {
|
||||
highlight_stack.pop();
|
||||
}
|
||||
HighlightEvent::Source { start, end } => {
|
||||
let mut start_position = Point::new(row, column);
|
||||
while byte_offset < end {
|
||||
if byte_offset <= start {
|
||||
start_position = Point::new(row, column);
|
||||
}
|
||||
if let Some((i, c)) = char_indices.next() {
|
||||
if was_newline {
|
||||
row += 1;
|
||||
column = 0;
|
||||
} else {
|
||||
column += i - byte_offset;
|
||||
}
|
||||
was_newline = c == '\n';
|
||||
byte_offset = i;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if let Some(highlight) = highlight_stack.last() {
|
||||
result.push((start_position, Point::new(row, column), *highlight))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
|
|
@ -58,7 +58,11 @@ fn test_real_language_corpus_files() {
|
|||
}
|
||||
|
||||
let language = get_language(language_name);
|
||||
let corpus_dir = grammars_dir.join(language_name).join("corpus");
|
||||
let mut corpus_dir = grammars_dir.join(language_name).join("corpus");
|
||||
if !corpus_dir.is_dir() {
|
||||
corpus_dir = grammars_dir.join(language_name).join("test").join("corpus");
|
||||
}
|
||||
|
||||
let error_corpus_file = error_corpus_dir.join(&format!("{}_errors.txt", language_name));
|
||||
let main_tests = parse_tests(&corpus_dir).unwrap();
|
||||
let error_tests = parse_tests(&error_corpus_file).unwrap_or(TestEntry::default());
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ use lazy_static::lazy_static;
|
|||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use tree_sitter::Language;
|
||||
use tree_sitter_highlight::{HighlightConfiguration, Highlighter};
|
||||
use tree_sitter_highlight::HighlightConfiguration;
|
||||
|
||||
include!("./dirs.rs");
|
||||
|
||||
|
|
@ -11,6 +11,10 @@ lazy_static! {
|
|||
static ref TEST_LOADER: Loader = Loader::new(SCRATCH_DIR.clone());
|
||||
}
|
||||
|
||||
pub fn test_loader<'a>() -> &'a Loader {
|
||||
&*TEST_LOADER
|
||||
}
|
||||
|
||||
pub fn fixtures_dir<'a>() -> &'static Path {
|
||||
&FIXTURES_DIR
|
||||
}
|
||||
|
|
@ -26,23 +30,24 @@ pub fn get_language_queries_path(language_name: &str) -> PathBuf {
|
|||
}
|
||||
|
||||
pub fn get_highlight_config(
|
||||
highlighter: &Highlighter,
|
||||
language_name: &str,
|
||||
injection_query_filename: &str,
|
||||
highlight_names: &[String],
|
||||
) -> HighlightConfiguration {
|
||||
let language = get_language(language_name);
|
||||
let queries_path = get_language_queries_path(language_name);
|
||||
let highlights_query = fs::read_to_string(queries_path.join("highlights.scm")).unwrap();
|
||||
let injections_query = fs::read_to_string(queries_path.join(injection_query_filename)).unwrap();
|
||||
let locals_query = fs::read_to_string(queries_path.join("locals.scm")).unwrap_or(String::new());
|
||||
highlighter
|
||||
.load_configuration(
|
||||
language,
|
||||
&highlights_query,
|
||||
&injections_query,
|
||||
&locals_query,
|
||||
)
|
||||
.unwrap()
|
||||
let mut result = HighlightConfiguration::new(
|
||||
language,
|
||||
&highlights_query,
|
||||
&injections_query,
|
||||
&locals_query,
|
||||
)
|
||||
.unwrap();
|
||||
result.configure(highlight_names);
|
||||
result
|
||||
}
|
||||
|
||||
pub fn get_test_language(name: &str, parser_code: &str, path: Option<&Path>) -> Language {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
pub(super) mod allocations;
|
||||
pub(super) mod edits;
|
||||
pub(super) mod fixtures;
|
||||
pub(super) mod random;
|
||||
pub(super) mod scope_sequence;
|
||||
pub(super) mod edits;
|
||||
|
|
|
|||
|
|
@ -4,49 +4,46 @@ use std::ffi::CString;
|
|||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::{fs, ptr, slice, str};
|
||||
use tree_sitter_highlight::{
|
||||
c, Error, HighlightConfiguration, HighlightContext, HighlightEvent, Highlighter, HtmlRenderer,
|
||||
c, Error, HighlightConfiguration, HighlightEvent, Highlighter, HtmlRenderer,
|
||||
};
|
||||
|
||||
lazy_static! {
|
||||
static ref JS_HIGHLIGHT: HighlightConfiguration =
|
||||
get_highlight_config(&HIGHLIGHTER, "javascript", "injections.scm");
|
||||
get_highlight_config("javascript", "injections.scm", &HIGHLIGHT_NAMES);
|
||||
static ref HTML_HIGHLIGHT: HighlightConfiguration =
|
||||
get_highlight_config(&HIGHLIGHTER, "html", "injections.scm");
|
||||
get_highlight_config("html", "injections.scm", &HIGHLIGHT_NAMES);
|
||||
static ref EJS_HIGHLIGHT: HighlightConfiguration =
|
||||
get_highlight_config(&HIGHLIGHTER, "embedded-template", "injections-ejs.scm");
|
||||
get_highlight_config("embedded-template", "injections-ejs.scm", &HIGHLIGHT_NAMES);
|
||||
static ref RUST_HIGHLIGHT: HighlightConfiguration =
|
||||
get_highlight_config(&HIGHLIGHTER, "rust", "injections.scm");
|
||||
static ref HIGHLIGHTER: Highlighter = Highlighter::new(
|
||||
[
|
||||
"attribute",
|
||||
"constant",
|
||||
"constructor",
|
||||
"function.builtin",
|
||||
"function",
|
||||
"embedded",
|
||||
"keyword",
|
||||
"operator",
|
||||
"property.builtin",
|
||||
"property",
|
||||
"punctuation",
|
||||
"punctuation.bracket",
|
||||
"punctuation.delimiter",
|
||||
"punctuation.special",
|
||||
"string",
|
||||
"tag",
|
||||
"type.builtin",
|
||||
"type",
|
||||
"variable.builtin",
|
||||
"variable.parameter",
|
||||
"variable",
|
||||
]
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(String::from)
|
||||
.collect()
|
||||
);
|
||||
static ref HTML_ATTRS: Vec<String> = HIGHLIGHTER
|
||||
.names()
|
||||
get_highlight_config("rust", "injections.scm", &HIGHLIGHT_NAMES);
|
||||
static ref HIGHLIGHT_NAMES: Vec<String> = [
|
||||
"attribute",
|
||||
"constant",
|
||||
"constructor",
|
||||
"function.builtin",
|
||||
"function",
|
||||
"embedded",
|
||||
"keyword",
|
||||
"operator",
|
||||
"property.builtin",
|
||||
"property",
|
||||
"punctuation",
|
||||
"punctuation.bracket",
|
||||
"punctuation.delimiter",
|
||||
"punctuation.special",
|
||||
"string",
|
||||
"tag",
|
||||
"type.builtin",
|
||||
"type",
|
||||
"variable.builtin",
|
||||
"variable.parameter",
|
||||
"variable",
|
||||
]
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(String::from)
|
||||
.collect();
|
||||
static ref HTML_ATTRS: Vec<String> = HIGHLIGHT_NAMES
|
||||
.iter()
|
||||
.map(|s| format!("class={}", s))
|
||||
.collect();
|
||||
|
|
@ -398,12 +395,10 @@ fn test_highlighting_cancellation() {
|
|||
test_language_for_injection_string(name)
|
||||
};
|
||||
|
||||
// Constructing the highlighter, which eagerly parses the outer document,
|
||||
// should not fail.
|
||||
let mut context = HighlightContext::new();
|
||||
let events = HIGHLIGHTER
|
||||
// The initial `highlight` call, which eagerly parses the outer document, should not fail.
|
||||
let mut highlighter = Highlighter::new();
|
||||
let events = highlighter
|
||||
.highlight(
|
||||
&mut context,
|
||||
&HTML_HIGHLIGHT,
|
||||
source.as_bytes(),
|
||||
Some(&cancellation_flag),
|
||||
|
|
@ -411,14 +406,15 @@ fn test_highlighting_cancellation() {
|
|||
)
|
||||
.unwrap();
|
||||
|
||||
// Iterating the scopes should not panic. It should return an error
|
||||
// once the cancellation is detected.
|
||||
// Iterating the scopes should not panic. It should return an error once the
|
||||
// cancellation is detected.
|
||||
for event in events {
|
||||
if let Err(e) = event {
|
||||
assert_eq!(e, Error::Cancelled);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
panic!("Expected an error while iterating highlighter");
|
||||
}
|
||||
|
||||
|
|
@ -565,9 +561,8 @@ fn to_html<'a>(
|
|||
) -> Result<Vec<String>, Error> {
|
||||
let src = src.as_bytes();
|
||||
let mut renderer = HtmlRenderer::new();
|
||||
let mut context = HighlightContext::new();
|
||||
let events = HIGHLIGHTER.highlight(
|
||||
&mut context,
|
||||
let mut highlighter = Highlighter::new();
|
||||
let events = highlighter.highlight(
|
||||
language_config,
|
||||
src,
|
||||
None,
|
||||
|
|
@ -585,12 +580,11 @@ fn to_token_vector<'a>(
|
|||
language_config: &'a HighlightConfiguration,
|
||||
) -> Result<Vec<Vec<(&'a str, Vec<&'static str>)>>, Error> {
|
||||
let src = src.as_bytes();
|
||||
let mut context = HighlightContext::new();
|
||||
let mut highlighter = Highlighter::new();
|
||||
let mut lines = Vec::new();
|
||||
let mut highlights = Vec::new();
|
||||
let mut line = Vec::new();
|
||||
let events = HIGHLIGHTER.highlight(
|
||||
&mut context,
|
||||
let events = highlighter.highlight(
|
||||
language_config,
|
||||
src,
|
||||
None,
|
||||
|
|
@ -598,7 +592,7 @@ fn to_token_vector<'a>(
|
|||
)?;
|
||||
for event in events {
|
||||
match event? {
|
||||
HighlightEvent::HighlightStart(s) => highlights.push(HIGHLIGHTER.names()[s.0].as_str()),
|
||||
HighlightEvent::HighlightStart(s) => highlights.push(HIGHLIGHT_NAMES[s.0].as_str()),
|
||||
HighlightEvent::HighlightEnd => {
|
||||
highlights.pop();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,4 +4,5 @@ mod highlight_test;
|
|||
mod node_test;
|
||||
mod parser_test;
|
||||
mod query_test;
|
||||
mod test_highlight_test;
|
||||
mod tree_test;
|
||||
|
|
|
|||
53
cli/src/tests/test_highlight_test.rs
Normal file
53
cli/src/tests/test_highlight_test.rs
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
use super::helpers::fixtures::{get_highlight_config, get_language, test_loader};
|
||||
use crate::test_highlight::{get_highlight_positions, parse_highlight_test};
|
||||
use tree_sitter::{Parser, Point};
|
||||
use tree_sitter_highlight::{Highlight, Highlighter};
|
||||
|
||||
#[test]
|
||||
fn test_highlight_test_with_basic_test() {
|
||||
let language = get_language("javascript");
|
||||
let config = get_highlight_config(
|
||||
"javascript",
|
||||
"injections.scm",
|
||||
&[
|
||||
"function.definition".to_string(),
|
||||
"variable.parameter".to_string(),
|
||||
"keyword".to_string(),
|
||||
],
|
||||
);
|
||||
let source = [
|
||||
"var abc = function(d) {",
|
||||
" // ^ function.definition",
|
||||
" // ^ keyword",
|
||||
" return d + e;",
|
||||
" // ^ variable.parameter",
|
||||
"};",
|
||||
]
|
||||
.join("\n");
|
||||
|
||||
let assertions = parse_highlight_test(&mut Parser::new(), language, source.as_bytes()).unwrap();
|
||||
assert_eq!(
|
||||
assertions,
|
||||
&[
|
||||
(Point::new(0, 5), "function.definition".to_string()),
|
||||
(Point::new(0, 11), "keyword".to_string()),
|
||||
(Point::new(3, 9), "variable.parameter".to_string()),
|
||||
]
|
||||
);
|
||||
|
||||
let mut highlighter = Highlighter::new();
|
||||
let highlight_positions =
|
||||
get_highlight_positions(test_loader(), &mut highlighter, &config, source.as_bytes())
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
highlight_positions,
|
||||
&[
|
||||
(Point::new(0, 0), Point::new(0, 3), Highlight(2)), // "var"
|
||||
(Point::new(0, 4), Point::new(0, 7), Highlight(0)), // "abc"
|
||||
(Point::new(0, 10), Point::new(0, 18), Highlight(2)), // "function"
|
||||
(Point::new(0, 19), Point::new(0, 20), Highlight(1)), // "d"
|
||||
(Point::new(3, 2), Point::new(3, 8), Highlight(2)), // "return"
|
||||
(Point::new(3, 9), Point::new(3, 10), Highlight(1)), // "d"
|
||||
]
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue