feat(cli): make input handling agnostic
Co-authored-by: Will Lillis <will.lillis24@gmail.com>
This commit is contained in:
parent
3456330fe9
commit
cc449ad965
4 changed files with 188 additions and 111 deletions
187
cli/src/input.rs
Normal file
187
cli/src/input.rs
Normal file
|
|
@ -0,0 +1,187 @@
|
|||
use std::{
|
||||
fs,
|
||||
io::{Read, Write},
|
||||
path::{Path, PathBuf},
|
||||
sync::{
|
||||
atomic::{AtomicUsize, Ordering},
|
||||
mpsc, Arc,
|
||||
},
|
||||
};
|
||||
|
||||
use anyhow::{anyhow, bail, Context, Result};
|
||||
use glob::glob;
|
||||
|
||||
use crate::test::{parse_tests, TestEntry};
|
||||
|
||||
pub enum CliInput {
|
||||
Paths(Vec<PathBuf>),
|
||||
Test {
|
||||
name: String,
|
||||
contents: Vec<u8>,
|
||||
languages: Vec<Box<str>>,
|
||||
},
|
||||
Stdin(Vec<u8>),
|
||||
}
|
||||
|
||||
pub fn get_input(
|
||||
paths_file: Option<&Path>,
|
||||
paths: Option<Vec<PathBuf>>,
|
||||
test_number: Option<u32>,
|
||||
cancellation_flag: &Arc<AtomicUsize>,
|
||||
) -> Result<CliInput> {
|
||||
if let Some(paths_file) = paths_file {
|
||||
return Ok(CliInput::Paths(
|
||||
fs::read_to_string(paths_file)
|
||||
.with_context(|| format!("Failed to read paths file {}", paths_file.display()))?
|
||||
.trim()
|
||||
.lines()
|
||||
.map(PathBuf::from)
|
||||
.collect::<Vec<_>>(),
|
||||
));
|
||||
}
|
||||
|
||||
if let Some(test_number) = test_number {
|
||||
let current_dir = std::env::current_dir().unwrap();
|
||||
let test_dir = current_dir.join("test").join("corpus");
|
||||
|
||||
if !test_dir.exists() {
|
||||
return Err(anyhow!(
|
||||
"Test corpus directory not found in current directory, see https://tree-sitter.github.io/tree-sitter/creating-parsers/5-writing-tests"
|
||||
));
|
||||
}
|
||||
|
||||
let test_entry = parse_tests(&test_dir)?;
|
||||
let mut test_num = 0;
|
||||
let Some((name, contents, languages)) =
|
||||
get_test_info(&test_entry, test_number.max(1) - 1, &mut test_num)
|
||||
else {
|
||||
return Err(anyhow!("Failed to fetch contents of test #{test_number}"));
|
||||
};
|
||||
|
||||
return Ok(CliInput::Test {
|
||||
name,
|
||||
contents,
|
||||
languages,
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(paths) = paths {
|
||||
let mut result = Vec::new();
|
||||
|
||||
let mut incorporate_path = |path: PathBuf, positive| {
|
||||
if positive {
|
||||
result.push(path);
|
||||
} else if let Some(index) = result.iter().position(|p| *p == path) {
|
||||
result.remove(index);
|
||||
}
|
||||
};
|
||||
|
||||
for mut path in paths {
|
||||
let mut positive = true;
|
||||
if path.starts_with("!") {
|
||||
positive = false;
|
||||
path = path.strip_prefix("!").unwrap().to_path_buf();
|
||||
}
|
||||
|
||||
if path.exists() {
|
||||
incorporate_path(path, positive);
|
||||
} else {
|
||||
let Some(path_str) = path.to_str() else {
|
||||
bail!("Invalid path: {}", path.display());
|
||||
};
|
||||
let paths =
|
||||
glob(path_str).with_context(|| format!("Invalid glob pattern {path:?}"))?;
|
||||
for path in paths {
|
||||
incorporate_path(path?, positive);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if result.is_empty() {
|
||||
return Err(anyhow!(
|
||||
"No files were found at or matched by the provided pathname/glob"
|
||||
));
|
||||
}
|
||||
|
||||
return Ok(CliInput::Paths(result));
|
||||
}
|
||||
|
||||
let reader_flag = cancellation_flag.clone();
|
||||
let (tx, rx) = mpsc::channel();
|
||||
|
||||
// Spawn a thread to read from stdin, until ctrl-c or EOF is received
|
||||
std::thread::spawn(move || {
|
||||
let mut input = Vec::new();
|
||||
let stdin = std::io::stdin();
|
||||
let mut handle = stdin.lock();
|
||||
|
||||
// Read in chunks, so we can check the ctrl-c flag
|
||||
loop {
|
||||
if reader_flag.load(Ordering::Relaxed) == 1 {
|
||||
break;
|
||||
}
|
||||
let mut buffer = [0; 1024];
|
||||
match handle.read(&mut buffer) {
|
||||
Ok(0) | Err(_) => break,
|
||||
Ok(n) => input.extend_from_slice(&buffer[..n]),
|
||||
}
|
||||
}
|
||||
|
||||
// Signal to the main thread that we're done
|
||||
tx.send(input).ok();
|
||||
});
|
||||
|
||||
loop {
|
||||
// If we've received a ctrl-c signal, exit
|
||||
if cancellation_flag.load(Ordering::Relaxed) == 1 {
|
||||
bail!("\n");
|
||||
}
|
||||
|
||||
// If we're done receiving input from stdin, return it
|
||||
if let Ok(input) = rx.try_recv() {
|
||||
return Ok(CliInput::Stdin(input));
|
||||
}
|
||||
|
||||
std::thread::sleep(std::time::Duration::from_millis(50));
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub fn get_test_info(
|
||||
test_entry: &TestEntry,
|
||||
target_test: u32,
|
||||
test_num: &mut u32,
|
||||
) -> Option<(String, Vec<u8>, Vec<Box<str>>)> {
|
||||
match test_entry {
|
||||
TestEntry::Example {
|
||||
name,
|
||||
input,
|
||||
attributes,
|
||||
..
|
||||
} => {
|
||||
if *test_num == target_test {
|
||||
return Some((name.clone(), input.clone(), attributes.languages.clone()));
|
||||
}
|
||||
*test_num += 1;
|
||||
}
|
||||
TestEntry::Group { children, .. } => {
|
||||
for child in children {
|
||||
if let Some((name, input, languages)) = get_test_info(child, target_test, test_num)
|
||||
{
|
||||
return Some((name, input, languages));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Writes `contents` to a temporary file and returns the path to that file.
|
||||
pub fn get_tmp_source_file(contents: &[u8]) -> Result<PathBuf> {
|
||||
let parse_path = std::env::temp_dir().join(".tree-sitter-temp");
|
||||
let mut parse_file = std::fs::File::create(&parse_path)?;
|
||||
parse_file.write_all(contents)?;
|
||||
|
||||
Ok(parse_path)
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@
|
|||
pub mod fuzz;
|
||||
pub mod highlight;
|
||||
pub mod init;
|
||||
pub mod input;
|
||||
pub mod logger;
|
||||
pub mod parse;
|
||||
pub mod playground;
|
||||
|
|
|
|||
|
|
@ -1446,56 +1446,3 @@ const fn get_styles() -> clap::builder::Styles {
|
|||
)
|
||||
.placeholder(Style::new().fg_color(Some(Color::Ansi(AnsiColor::White))))
|
||||
}
|
||||
|
||||
fn collect_paths(paths_file: Option<&str>, paths: Option<Vec<String>>) -> Result<Vec<String>> {
|
||||
if let Some(paths_file) = paths_file {
|
||||
return Ok(fs::read_to_string(paths_file)
|
||||
.with_context(|| format!("Failed to read paths file {paths_file}"))?
|
||||
.trim()
|
||||
.lines()
|
||||
.map(String::from)
|
||||
.collect::<Vec<_>>());
|
||||
}
|
||||
|
||||
if let Some(paths) = paths {
|
||||
let mut result = Vec::new();
|
||||
|
||||
let mut incorporate_path = |path: &str, positive| {
|
||||
if positive {
|
||||
result.push(path.to_string());
|
||||
} else if let Some(index) = result.iter().position(|p| p == path) {
|
||||
result.remove(index);
|
||||
}
|
||||
};
|
||||
|
||||
for mut path in paths {
|
||||
let mut positive = true;
|
||||
if path.starts_with('!') {
|
||||
positive = false;
|
||||
path = path.trim_start_matches('!').to_string();
|
||||
}
|
||||
|
||||
if Path::new(&path).exists() {
|
||||
incorporate_path(&path, positive);
|
||||
} else {
|
||||
let paths =
|
||||
glob(&path).with_context(|| format!("Invalid glob pattern {path:?}"))?;
|
||||
for path in paths {
|
||||
if let Some(path) = path?.to_str() {
|
||||
incorporate_path(path, positive);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if result.is_empty() {
|
||||
return Err(anyhow!(
|
||||
"No files were found at or matched by the provided pathname/glob"
|
||||
));
|
||||
}
|
||||
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
Err(anyhow!("Must provide one or more paths"))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -268,64 +268,6 @@ pub fn run_tests_at_path(parser: &mut Parser, opts: &mut TestOptions) -> Result<
|
|||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub fn get_test_info<'test>(
|
||||
test_entry: &'test TestEntry,
|
||||
target_test: u32,
|
||||
test_num: &mut u32,
|
||||
) -> Option<(&'test str, &'test [u8], Vec<Box<str>>)> {
|
||||
match test_entry {
|
||||
TestEntry::Example {
|
||||
name,
|
||||
input,
|
||||
attributes,
|
||||
..
|
||||
} => {
|
||||
if *test_num == target_test {
|
||||
return Some((name, input, attributes.languages.clone()));
|
||||
}
|
||||
*test_num += 1;
|
||||
}
|
||||
TestEntry::Group { children, .. } => {
|
||||
for child in children {
|
||||
if let Some((name, input, languages)) = get_test_info(child, target_test, test_num)
|
||||
{
|
||||
return Some((name, input, languages));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Writes the input of `target_test` to a temporary file and returns the path
|
||||
pub fn get_tmp_test_file(target_test: u32, color: bool) -> Result<(PathBuf, Vec<Box<str>>)> {
|
||||
let current_dir = std::env::current_dir().unwrap();
|
||||
let test_dir = current_dir.join("test").join("corpus");
|
||||
|
||||
// Get the input of the target test
|
||||
let test_entry = parse_tests(&test_dir)?;
|
||||
let mut test_num = 0;
|
||||
let Some((test_name, test_contents, languages)) =
|
||||
get_test_info(&test_entry, target_test - 1, &mut test_num)
|
||||
else {
|
||||
return Err(anyhow!("Failed to fetch contents of test #{target_test}"));
|
||||
};
|
||||
|
||||
// Write the test contents to a temporary file
|
||||
let test_path = std::env::temp_dir().join(".tree-sitter-test");
|
||||
let mut test_file = std::fs::File::create(&test_path)?;
|
||||
test_file.write_all(test_contents)?;
|
||||
|
||||
println!(
|
||||
"{target_test}. {}\n",
|
||||
paint(color.then_some(AnsiColor::Green), test_name)
|
||||
);
|
||||
|
||||
Ok((test_path, languages))
|
||||
}
|
||||
|
||||
pub fn check_queries_at_path(language: &Language, path: &Path) -> Result<()> {
|
||||
if path.exists() {
|
||||
for entry in WalkDir::new(path)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue