feat: move tree-sitter configuration to dedicated file (#3700)
This commit is contained in:
parent
94a8262110
commit
ea3846a2c5
20 changed files with 1828 additions and 536 deletions
240
cli/src/main.rs
240
cli/src/main.rs
|
|
@ -8,8 +8,11 @@ use anstyle::{AnsiColor, Color, Style};
|
|||
use anyhow::{anyhow, Context, Result};
|
||||
use clap::{crate_authors, Args, Command, FromArgMatches as _, Subcommand};
|
||||
use clap_complete::{generate, Shell};
|
||||
use dialoguer::{theme::ColorfulTheme, Confirm, FuzzySelect, Input};
|
||||
use glob::glob;
|
||||
use heck::ToUpperCamelCase;
|
||||
use regex::Regex;
|
||||
use semver::Version;
|
||||
use tree_sitter::{ffi, Parser, Point};
|
||||
use tree_sitter_cli::{
|
||||
fuzz::{
|
||||
|
|
@ -17,7 +20,9 @@ use tree_sitter_cli::{
|
|||
LOG_GRAPH_ENABLED, START_SEED,
|
||||
},
|
||||
highlight,
|
||||
init::{generate_grammar_files, lookup_package_json_for_path},
|
||||
init::{
|
||||
generate_grammar_files, lookup_package_json_for_path, migrate_package_json, JsonConfigOpts,
|
||||
},
|
||||
logger,
|
||||
parse::{self, ParseFileOptions, ParseOutput},
|
||||
playground, query, tags,
|
||||
|
|
@ -26,8 +31,9 @@ use tree_sitter_cli::{
|
|||
};
|
||||
use tree_sitter_config::Config;
|
||||
use tree_sitter_highlight::Highlighter;
|
||||
use tree_sitter_loader as loader;
|
||||
use tree_sitter_loader::{self as loader, TreeSitterJSON};
|
||||
use tree_sitter_tags::TagsContext;
|
||||
use url::Url;
|
||||
|
||||
const BUILD_VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
const BUILD_SHA: Option<&'static str> = option_env!("BUILD_SHA");
|
||||
|
|
@ -436,18 +442,216 @@ impl InitConfig {
|
|||
}
|
||||
|
||||
impl Init {
|
||||
fn run(self, current_dir: &Path) -> Result<()> {
|
||||
if let Some(dir_name) = current_dir
|
||||
.file_name()
|
||||
.map(|x| x.to_string_lossy().to_ascii_lowercase())
|
||||
{
|
||||
if let Some(language_name) = dir_name
|
||||
.strip_prefix("tree-sitter-")
|
||||
.or_else(|| Some(dir_name.as_ref()))
|
||||
{
|
||||
generate_grammar_files(current_dir, language_name, self.update)?;
|
||||
fn run(self, current_dir: &Path, migrated: bool) -> Result<()> {
|
||||
let configure_json = if current_dir.join("tree-sitter.json").exists() {
|
||||
Confirm::with_theme(&ColorfulTheme::default())
|
||||
.with_prompt("It looks like you already have a `tree-sitter.json` file. Do you want to re-configure it?")
|
||||
.interact()?
|
||||
} else if current_dir.join("package.json").exists() {
|
||||
!migrated
|
||||
} else {
|
||||
true
|
||||
};
|
||||
|
||||
let (language_name, json_config_opts) = if configure_json {
|
||||
let mut opts = JsonConfigOpts::default();
|
||||
|
||||
let name = || {
|
||||
Input::<String>::with_theme(&ColorfulTheme::default())
|
||||
.with_prompt("Parser name")
|
||||
.validate_with(|input: &String| {
|
||||
if input.chars().all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_') {
|
||||
Ok(())
|
||||
} else {
|
||||
Err("The name must be lowercase and contain only letters, digits, and underscores")
|
||||
}
|
||||
})
|
||||
.interact_text()
|
||||
};
|
||||
|
||||
let upper_camel_name = |name: &str| {
|
||||
Input::<String>::with_theme(&ColorfulTheme::default())
|
||||
.with_prompt("UpperCamelCase name")
|
||||
.default(name.to_upper_camel_case())
|
||||
.interact_text()
|
||||
};
|
||||
|
||||
let description = |name: &str| {
|
||||
Input::<String>::with_theme(&ColorfulTheme::default())
|
||||
.with_prompt("Description")
|
||||
.default(format!(
|
||||
"{} grammar for tree-sitter",
|
||||
name.to_upper_camel_case()
|
||||
))
|
||||
.show_default(false)
|
||||
.allow_empty(true)
|
||||
.interact_text()
|
||||
};
|
||||
|
||||
let repository = |name: &str| {
|
||||
Input::<Url>::with_theme(&ColorfulTheme::default())
|
||||
.with_prompt("Repository URL")
|
||||
.allow_empty(true)
|
||||
.default(
|
||||
Url::parse(&format!(
|
||||
"https://github.com/tree-sitter/tree-sitter-{name}"
|
||||
))
|
||||
.expect("Failed to parse default repository URL"),
|
||||
)
|
||||
.show_default(false)
|
||||
.interact_text()
|
||||
};
|
||||
|
||||
let scope = |name: &str| {
|
||||
Input::<String>::with_theme(&ColorfulTheme::default())
|
||||
.with_prompt("TextMate scope")
|
||||
.default(format!("source.{name}"))
|
||||
.interact_text()
|
||||
};
|
||||
|
||||
let file_types = |name: &str| {
|
||||
Input::<String>::with_theme(&ColorfulTheme::default())
|
||||
.with_prompt("File types (space-separated)")
|
||||
.default(format!(".{name}"))
|
||||
.interact_text()
|
||||
.map(|ft| {
|
||||
let mut set = HashSet::new();
|
||||
for ext in ft.split(' ') {
|
||||
let ext = ext.trim();
|
||||
if !ext.is_empty() {
|
||||
set.insert(ext.to_string());
|
||||
}
|
||||
}
|
||||
set.into_iter().collect::<Vec<_>>()
|
||||
})
|
||||
};
|
||||
|
||||
let initial_version = || {
|
||||
Input::<Version>::with_theme(&ColorfulTheme::default())
|
||||
.with_prompt("Version")
|
||||
.default(Version::new(0, 1, 0))
|
||||
.interact_text()
|
||||
};
|
||||
|
||||
let license = || {
|
||||
Input::<String>::with_theme(&ColorfulTheme::default())
|
||||
.with_prompt("License")
|
||||
.default("MIT".to_string())
|
||||
.allow_empty(true)
|
||||
.interact()
|
||||
};
|
||||
|
||||
let author = || {
|
||||
Input::<String>::with_theme(&ColorfulTheme::default())
|
||||
.with_prompt("Author name")
|
||||
.interact_text()
|
||||
};
|
||||
|
||||
let email = || {
|
||||
Input::with_theme(&ColorfulTheme::default())
|
||||
.with_prompt("Author email")
|
||||
.validate_with({
|
||||
let mut force = None;
|
||||
move |input: &String| -> Result<(), &str> {
|
||||
if input.contains('@') || input.trim().is_empty() || force.as_ref().map_or(false, |old| old == input) {
|
||||
Ok(())
|
||||
} else {
|
||||
force = Some(input.clone());
|
||||
Err("This is not an email address; type the same value again to force use")
|
||||
}
|
||||
}
|
||||
})
|
||||
.allow_empty(true)
|
||||
.interact_text().map(|e| (!e.trim().is_empty()).then_some(e))
|
||||
};
|
||||
|
||||
let url = || {
|
||||
Input::<String>::with_theme(&ColorfulTheme::default())
|
||||
.with_prompt("Author URL")
|
||||
.allow_empty(true)
|
||||
.validate_with(|input: &String| -> Result<(), &str> {
|
||||
if input.trim().is_empty() || Url::parse(input).is_ok() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err("This is not a valid URL")
|
||||
}
|
||||
})
|
||||
.interact_text()
|
||||
.map(|e| (!e.trim().is_empty()).then(|| Url::parse(&e).unwrap()))
|
||||
};
|
||||
|
||||
let choices = [
|
||||
"name",
|
||||
"upper_camel_name",
|
||||
"description",
|
||||
"repository",
|
||||
"scope",
|
||||
"file_types",
|
||||
"version",
|
||||
"license",
|
||||
"author",
|
||||
"email",
|
||||
"url",
|
||||
"exit",
|
||||
];
|
||||
|
||||
macro_rules! set_choice {
|
||||
($choice:expr) => {
|
||||
match $choice {
|
||||
"name" => opts.name = name()?,
|
||||
"upper_camel_name" => opts.upper_camel_name = upper_camel_name(&opts.name)?,
|
||||
"description" => opts.description = description(&opts.name)?,
|
||||
"repository" => opts.repository = Some(repository(&opts.name)?),
|
||||
"scope" => opts.scope = scope(&opts.name)?,
|
||||
"file_types" => opts.file_types = file_types(&opts.name)?,
|
||||
"version" => opts.version = initial_version()?,
|
||||
"license" => opts.license = license()?,
|
||||
"author" => opts.author = author()?,
|
||||
"email" => opts.email = email()?,
|
||||
"url" => opts.url = url()?,
|
||||
"exit" => break,
|
||||
_ => unreachable!(),
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Initial configuration
|
||||
for choice in choices.iter().take(choices.len() - 1) {
|
||||
set_choice!(*choice);
|
||||
}
|
||||
|
||||
// Loop for editing the configuration
|
||||
loop {
|
||||
println!(
|
||||
"Your current configuration:\n{}",
|
||||
serde_json::to_string_pretty(&opts)?
|
||||
);
|
||||
|
||||
if Confirm::with_theme(&ColorfulTheme::default())
|
||||
.with_prompt("Does the config above look correct?")
|
||||
.interact()?
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
let idx = FuzzySelect::with_theme(&ColorfulTheme::default())
|
||||
.with_prompt("Which field would you like to change?")
|
||||
.items(&choices)
|
||||
.interact()?;
|
||||
|
||||
set_choice!(choices[idx]);
|
||||
}
|
||||
|
||||
(opts.name.clone(), Some(opts))
|
||||
} else {
|
||||
let json = serde_json::from_reader::<_, TreeSitterJSON>(
|
||||
fs::File::open(current_dir.join("tree-sitter.json"))
|
||||
.with_context(|| "Failed to open tree-sitter.json")?,
|
||||
)?;
|
||||
(json.grammars[0].name.clone(), None)
|
||||
};
|
||||
|
||||
generate_grammar_files(current_dir, &language_name, self.update, json_config_opts)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -1082,9 +1286,17 @@ fn run() -> Result<()> {
|
|||
let current_dir = env::current_dir().unwrap();
|
||||
let loader = loader::Loader::new()?;
|
||||
|
||||
let migrated = if !current_dir.join("tree-sitter.json").exists()
|
||||
&& current_dir.join("package.json").exists()
|
||||
{
|
||||
migrate_package_json(¤t_dir).with_context(|| "Failed to migrate package.json")?
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
match command {
|
||||
Commands::InitConfig(_) => InitConfig::run()?,
|
||||
Commands::Init(init_options) => init_options.run(¤t_dir)?,
|
||||
Commands::Init(init_options) => init_options.run(¤t_dir, migrated)?,
|
||||
Commands::Generate(generate_options) => generate_options.run(loader, ¤t_dir)?,
|
||||
Commands::Build(build_options) => build_options.run(loader, ¤t_dir)?,
|
||||
Commands::Parse(parse_options) => parse_options.run(loader, ¤t_dir)?,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue