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
|
|
@ -32,6 +32,7 @@ clap.workspace = true
|
|||
clap_complete.workspace = true
|
||||
ctor.workspace = true
|
||||
ctrlc.workspace = true
|
||||
dialoguer.workspace = true
|
||||
dirs.workspace = true
|
||||
filetime.workspace = true
|
||||
glob.workspace = true
|
||||
|
|
@ -54,6 +55,7 @@ similar.workspace = true
|
|||
smallbitvec.workspace = true
|
||||
streaming-iterator.workspace = true
|
||||
tiny_http.workspace = true
|
||||
url.workspace = true
|
||||
walkdir.workspace = true
|
||||
wasmparser.workspace = true
|
||||
webbrowser.workspace = true
|
||||
|
|
|
|||
|
|
@ -26,8 +26,6 @@ semver.workspace = true
|
|||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
smallbitvec.workspace = true
|
||||
url.workspace = true
|
||||
|
||||
tree-sitter.workspace = true
|
||||
|
||||
[target."cfg(windows)".dependencies]
|
||||
url = "2.5.2"
|
||||
|
|
|
|||
|
|
@ -28,10 +28,12 @@ libloading.workspace = true
|
|||
once_cell.workspace = true
|
||||
path-slash.workspace = true
|
||||
regex.workspace = true
|
||||
semver.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
tempfile.workspace = true
|
||||
url.workspace = true
|
||||
|
||||
tree-sitter = {workspace = true}
|
||||
tree-sitter-highlight = {workspace = true, optional = true}
|
||||
tree-sitter-tags = {workspace = true, optional = true}
|
||||
tree-sitter = { workspace = true }
|
||||
tree-sitter-highlight = { workspace = true, optional = true }
|
||||
tree-sitter-tags = { workspace = true, optional = true }
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ use libloading::{Library, Symbol};
|
|||
use once_cell::unsync::OnceCell;
|
||||
use path_slash::PathBufExt as _;
|
||||
use regex::{Regex, RegexBuilder};
|
||||
use semver::Version;
|
||||
use serde::{Deserialize, Deserializer, Serialize};
|
||||
use tree_sitter::Language;
|
||||
#[cfg(any(feature = "tree-sitter-highlight", feature = "tree-sitter-tags"))]
|
||||
|
|
@ -35,6 +36,7 @@ use tree_sitter::QueryErrorKind;
|
|||
use tree_sitter_highlight::HighlightConfiguration;
|
||||
#[cfg(feature = "tree-sitter-tags")]
|
||||
use tree_sitter_tags::{Error as TagsError, TagsConfiguration};
|
||||
use url::Url;
|
||||
|
||||
pub const EMSCRIPTEN_TAG: &str = concat!("docker.io/emscripten/emsdk:", env!("EMSCRIPTEN_VERSION"));
|
||||
|
||||
|
|
@ -48,6 +50,196 @@ pub struct Config {
|
|||
pub parser_directories: Vec<PathBuf>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Default)]
|
||||
#[serde(untagged)]
|
||||
pub enum PathsJSON {
|
||||
#[default]
|
||||
Empty,
|
||||
Single(String),
|
||||
Multiple(Vec<String>),
|
||||
}
|
||||
|
||||
impl PathsJSON {
|
||||
fn into_vec(self) -> Option<Vec<String>> {
|
||||
match self {
|
||||
Self::Empty => None,
|
||||
Self::Single(s) => Some(vec![s]),
|
||||
Self::Multiple(s) => Some(s),
|
||||
}
|
||||
}
|
||||
|
||||
fn is_empty(&self) -> bool {
|
||||
matches!(self, Self::Empty)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
#[serde(untagged)]
|
||||
pub enum PackageJSONAuthor {
|
||||
String(String),
|
||||
Object {
|
||||
name: String,
|
||||
email: Option<String>,
|
||||
url: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
#[serde(untagged)]
|
||||
pub enum PackageJSONRepository {
|
||||
String(String),
|
||||
Object { url: String },
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct PackageJSON {
|
||||
pub name: String,
|
||||
pub version: Version,
|
||||
pub description: Option<String>,
|
||||
pub author: Option<PackageJSONAuthor>,
|
||||
pub maintainers: Option<Vec<PackageJSONAuthor>>,
|
||||
pub license: Option<String>,
|
||||
pub repository: Option<PackageJSONRepository>,
|
||||
#[serde(default)]
|
||||
#[serde(rename = "tree-sitter", skip_serializing_if = "Option::is_none")]
|
||||
pub tree_sitter: Option<Vec<LanguageConfigurationJSON>>,
|
||||
}
|
||||
|
||||
fn default_path() -> PathBuf {
|
||||
PathBuf::from(".")
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct LanguageConfigurationJSON {
|
||||
#[serde(default = "default_path")]
|
||||
pub path: PathBuf,
|
||||
pub scope: Option<String>,
|
||||
pub file_types: Option<Vec<String>>,
|
||||
pub content_regex: Option<String>,
|
||||
pub first_line_regex: Option<String>,
|
||||
pub injection_regex: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "PathsJSON::is_empty")]
|
||||
pub highlights: PathsJSON,
|
||||
#[serde(default, skip_serializing_if = "PathsJSON::is_empty")]
|
||||
pub injections: PathsJSON,
|
||||
#[serde(default, skip_serializing_if = "PathsJSON::is_empty")]
|
||||
pub locals: PathsJSON,
|
||||
#[serde(default, skip_serializing_if = "PathsJSON::is_empty")]
|
||||
pub tags: PathsJSON,
|
||||
#[serde(default, skip_serializing_if = "PathsJSON::is_empty")]
|
||||
pub external_files: PathsJSON,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct TreeSitterJSON {
|
||||
pub grammars: Vec<Grammar>,
|
||||
pub metadata: Metadata,
|
||||
#[serde(default)]
|
||||
pub bindings: Bindings,
|
||||
}
|
||||
|
||||
impl TreeSitterJSON {
|
||||
pub fn from_file(path: &Path) -> Option<Self> {
|
||||
if let Ok(file) = fs::File::open(path.join("tree-sitter.json")) {
|
||||
Some(serde_json::from_reader(file).ok()?)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn has_multiple_language_configs(&self) -> bool {
|
||||
self.grammars.len() > 1
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct Grammar {
|
||||
pub name: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub upper_camel_name: Option<String>,
|
||||
pub scope: String,
|
||||
pub path: PathBuf,
|
||||
#[serde(default, skip_serializing_if = "PathsJSON::is_empty")]
|
||||
pub external_files: PathsJSON,
|
||||
pub file_types: Option<Vec<String>>,
|
||||
#[serde(default, skip_serializing_if = "PathsJSON::is_empty")]
|
||||
pub highlights: PathsJSON,
|
||||
#[serde(default, skip_serializing_if = "PathsJSON::is_empty")]
|
||||
pub injections: PathsJSON,
|
||||
#[serde(default, skip_serializing_if = "PathsJSON::is_empty")]
|
||||
pub locals: PathsJSON,
|
||||
#[serde(default, skip_serializing_if = "PathsJSON::is_empty")]
|
||||
pub tags: PathsJSON,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub injection_regex: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub first_line_regex: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub content_regex: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct Metadata {
|
||||
pub version: Version,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub license: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub description: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub authors: Option<Vec<Author>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub links: Option<Links>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub namespace: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct Author {
|
||||
pub name: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub email: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub url: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct Links {
|
||||
pub repository: Url,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub homepage: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(default)]
|
||||
pub struct Bindings {
|
||||
pub c: bool,
|
||||
pub go: bool,
|
||||
pub java: bool,
|
||||
pub kotlin: bool,
|
||||
pub node: bool,
|
||||
pub python: bool,
|
||||
pub rust: bool,
|
||||
pub swift: bool,
|
||||
}
|
||||
|
||||
impl Default for Bindings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
c: true,
|
||||
go: true,
|
||||
java: false,
|
||||
kotlin: false,
|
||||
node: true,
|
||||
python: true,
|
||||
rust: true,
|
||||
swift: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Replace `~` or `$HOME` with home path string.
|
||||
// (While paths like "~/.tree-sitter/config.json" can be deserialized,
|
||||
// they're not valid path for I/O modules.)
|
||||
|
|
@ -930,57 +1122,6 @@ impl Loader {
|
|||
parser_path: &Path,
|
||||
set_current_path_config: bool,
|
||||
) -> Result<&[LanguageConfiguration]> {
|
||||
#[derive(Deserialize, Clone, Default)]
|
||||
#[serde(untagged)]
|
||||
enum PathsJSON {
|
||||
#[default]
|
||||
Empty,
|
||||
Single(String),
|
||||
Multiple(Vec<String>),
|
||||
}
|
||||
|
||||
impl PathsJSON {
|
||||
fn into_vec(self) -> Option<Vec<String>> {
|
||||
match self {
|
||||
Self::Empty => None,
|
||||
Self::Single(s) => Some(vec![s]),
|
||||
Self::Multiple(s) => Some(s),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct LanguageConfigurationJSON {
|
||||
#[serde(default)]
|
||||
path: PathBuf,
|
||||
scope: Option<String>,
|
||||
#[serde(rename = "file-types")]
|
||||
file_types: Option<Vec<String>>,
|
||||
#[serde(rename = "content-regex")]
|
||||
content_regex: Option<String>,
|
||||
#[serde(rename = "first-line-regex")]
|
||||
first_line_regex: Option<String>,
|
||||
#[serde(rename = "injection-regex")]
|
||||
injection_regex: Option<String>,
|
||||
#[serde(default)]
|
||||
highlights: PathsJSON,
|
||||
#[serde(default)]
|
||||
injections: PathsJSON,
|
||||
#[serde(default)]
|
||||
locals: PathsJSON,
|
||||
#[serde(default)]
|
||||
tags: PathsJSON,
|
||||
#[serde(default, rename = "external-files")]
|
||||
external_files: PathsJSON,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct PackageJSON {
|
||||
#[serde(default)]
|
||||
#[serde(rename = "tree-sitter")]
|
||||
tree_sitter: Vec<LanguageConfigurationJSON>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct GrammarJSON {
|
||||
name: String,
|
||||
|
|
@ -988,41 +1129,40 @@ impl Loader {
|
|||
|
||||
let initial_language_configuration_count = self.language_configurations.len();
|
||||
|
||||
if let Ok(package_json_contents) = fs::read_to_string(parser_path.join("package.json")) {
|
||||
let package_json = serde_json::from_str::<PackageJSON>(&package_json_contents);
|
||||
if let Ok(package_json) = package_json {
|
||||
let language_count = self.languages_by_id.len();
|
||||
for config_json in package_json.tree_sitter {
|
||||
// Determine the path to the parser directory. This can be specified in
|
||||
// the package.json, but defaults to the directory containing the package.json.
|
||||
let language_path = parser_path.join(config_json.path);
|
||||
if let Some(config) = TreeSitterJSON::from_file(parser_path) {
|
||||
let language_count = self.languages_by_id.len();
|
||||
for grammar in config.grammars {
|
||||
// Determine the path to the parser directory. This can be specified in
|
||||
// the package.json, but defaults to the directory containing the
|
||||
// package.json.
|
||||
let language_path = parser_path.join(grammar.path);
|
||||
|
||||
let grammar_path = language_path.join("src").join("grammar.json");
|
||||
let mut grammar_file = fs::File::open(grammar_path)
|
||||
.with_context(|| "Failed to read grammar.json")?;
|
||||
let grammar_json: GrammarJSON =
|
||||
serde_json::from_reader(BufReader::new(&mut grammar_file))
|
||||
.with_context(|| "Failed to parse grammar.json")?;
|
||||
let grammar_path = language_path.join("src").join("grammar.json");
|
||||
let mut grammar_file =
|
||||
fs::File::open(grammar_path).with_context(|| "Failed to read grammar.json")?;
|
||||
let grammar_json: GrammarJSON =
|
||||
serde_json::from_reader(BufReader::new(&mut grammar_file))
|
||||
.with_context(|| "Failed to parse grammar.json")?;
|
||||
|
||||
// Determine if a previous language configuration in this package.json file
|
||||
// already uses the same language.
|
||||
let mut language_id = None;
|
||||
for (id, (path, _, _)) in
|
||||
self.languages_by_id.iter().enumerate().skip(language_count)
|
||||
{
|
||||
if language_path == *path {
|
||||
language_id = Some(id);
|
||||
}
|
||||
// Determine if a previous language configuration in this package.json file
|
||||
// already uses the same language.
|
||||
let mut language_id = None;
|
||||
for (id, (path, _, _)) in
|
||||
self.languages_by_id.iter().enumerate().skip(language_count)
|
||||
{
|
||||
if language_path == *path {
|
||||
language_id = Some(id);
|
||||
}
|
||||
}
|
||||
|
||||
// If not, add a new language path to the list.
|
||||
let language_id = if let Some(language_id) = language_id {
|
||||
language_id
|
||||
} else {
|
||||
self.languages_by_id.push((
|
||||
// If not, add a new language path to the list.
|
||||
let language_id = if let Some(language_id) = language_id {
|
||||
language_id
|
||||
} else {
|
||||
self.languages_by_id.push((
|
||||
language_path,
|
||||
OnceCell::new(),
|
||||
config_json.external_files.clone().into_vec().map(|files| {
|
||||
grammar.external_files.clone().into_vec().map(|files| {
|
||||
files.into_iter()
|
||||
.map(|path| {
|
||||
let path = parser_path.join(path);
|
||||
|
|
@ -1036,57 +1176,55 @@ impl Loader {
|
|||
.collect::<Result<Vec<_>>>()
|
||||
}).transpose()?,
|
||||
));
|
||||
self.languages_by_id.len() - 1
|
||||
};
|
||||
self.languages_by_id.len() - 1
|
||||
};
|
||||
|
||||
let configuration = LanguageConfiguration {
|
||||
root_path: parser_path.to_path_buf(),
|
||||
language_name: grammar_json.name.clone(),
|
||||
scope: config_json.scope,
|
||||
language_id,
|
||||
file_types: config_json.file_types.unwrap_or_default(),
|
||||
content_regex: Self::regex(config_json.content_regex.as_deref()),
|
||||
first_line_regex: Self::regex(config_json.first_line_regex.as_deref()),
|
||||
injection_regex: Self::regex(config_json.injection_regex.as_deref()),
|
||||
injections_filenames: config_json.injections.into_vec(),
|
||||
locals_filenames: config_json.locals.into_vec(),
|
||||
tags_filenames: config_json.tags.into_vec(),
|
||||
highlights_filenames: config_json.highlights.into_vec(),
|
||||
#[cfg(feature = "tree-sitter-highlight")]
|
||||
highlight_config: OnceCell::new(),
|
||||
#[cfg(feature = "tree-sitter-tags")]
|
||||
tags_config: OnceCell::new(),
|
||||
#[cfg(feature = "tree-sitter-highlight")]
|
||||
highlight_names: &self.highlight_names,
|
||||
#[cfg(feature = "tree-sitter-highlight")]
|
||||
use_all_highlight_names: self.use_all_highlight_names,
|
||||
};
|
||||
let configuration = LanguageConfiguration {
|
||||
root_path: parser_path.to_path_buf(),
|
||||
language_name: grammar_json.name,
|
||||
scope: Some(grammar.scope),
|
||||
language_id,
|
||||
file_types: grammar.file_types.unwrap_or_default(),
|
||||
content_regex: Self::regex(grammar.content_regex.as_deref()),
|
||||
first_line_regex: Self::regex(grammar.first_line_regex.as_deref()),
|
||||
injection_regex: Self::regex(grammar.injection_regex.as_deref()),
|
||||
injections_filenames: grammar.injections.into_vec(),
|
||||
locals_filenames: grammar.locals.into_vec(),
|
||||
tags_filenames: grammar.tags.into_vec(),
|
||||
highlights_filenames: grammar.highlights.into_vec(),
|
||||
#[cfg(feature = "tree-sitter-highlight")]
|
||||
highlight_config: OnceCell::new(),
|
||||
#[cfg(feature = "tree-sitter-tags")]
|
||||
tags_config: OnceCell::new(),
|
||||
#[cfg(feature = "tree-sitter-highlight")]
|
||||
highlight_names: &self.highlight_names,
|
||||
#[cfg(feature = "tree-sitter-highlight")]
|
||||
use_all_highlight_names: self.use_all_highlight_names,
|
||||
};
|
||||
|
||||
for file_type in &configuration.file_types {
|
||||
self.language_configuration_ids_by_file_type
|
||||
.entry(file_type.to_string())
|
||||
.or_default()
|
||||
.push(self.language_configurations.len());
|
||||
}
|
||||
if let Some(first_line_regex) = &configuration.first_line_regex {
|
||||
self.language_configuration_ids_by_first_line_regex
|
||||
.entry(first_line_regex.to_string())
|
||||
.or_default()
|
||||
.push(self.language_configurations.len());
|
||||
}
|
||||
for file_type in &configuration.file_types {
|
||||
self.language_configuration_ids_by_file_type
|
||||
.entry(file_type.to_string())
|
||||
.or_default()
|
||||
.push(self.language_configurations.len());
|
||||
}
|
||||
if let Some(first_line_regex) = &configuration.first_line_regex {
|
||||
self.language_configuration_ids_by_first_line_regex
|
||||
.entry(first_line_regex.to_string())
|
||||
.or_default()
|
||||
.push(self.language_configurations.len());
|
||||
}
|
||||
|
||||
self.language_configurations.push(unsafe {
|
||||
mem::transmute::<LanguageConfiguration<'_>, LanguageConfiguration<'static>>(
|
||||
configuration,
|
||||
)
|
||||
});
|
||||
self.language_configurations.push(unsafe {
|
||||
mem::transmute::<LanguageConfiguration<'_>, LanguageConfiguration<'static>>(
|
||||
configuration,
|
||||
)
|
||||
});
|
||||
|
||||
if set_current_path_config
|
||||
&& self.language_configuration_in_current_path.is_none()
|
||||
{
|
||||
self.language_configuration_in_current_path =
|
||||
Some(self.language_configurations.len() - 1);
|
||||
}
|
||||
if set_current_path_config && self.language_configuration_in_current_path.is_none()
|
||||
{
|
||||
self.language_configuration_in_current_path =
|
||||
Some(self.language_configurations.len() - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
1156
cli/src/init.rs
1156
cli/src/init.rs
File diff suppressed because it is too large
Load diff
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)?,
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ libdir=@LIBDIR@
|
|||
includedir=@INCLUDEDIR@
|
||||
|
||||
Name: tree-sitter-PARSER_NAME
|
||||
Description: CAMEL_PARSER_NAME grammar for tree-sitter
|
||||
Description: PARSER_DESCRIPTION
|
||||
URL: @URL@
|
||||
Version: @VERSION@
|
||||
Requires: @REQUIRES@
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
"""CAMEL_PARSER_NAME grammar for tree-sitter"""
|
||||
"""PARSER_DESCRIPTION"""
|
||||
|
||||
from importlib.resources import files as _files
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
[package]
|
||||
name = "tree-sitter-PARSER_NAME"
|
||||
description = "CAMEL_PARSER_NAME grammar for tree-sitter"
|
||||
description = "PARSER_DESCRIPTION"
|
||||
version = "0.0.1"
|
||||
license = "MIT"
|
||||
authors = ["PARSER_AUTHOR_NAME PARSER_AUTHOR_EMAIL"]
|
||||
license = "PARSER_LICENSE"
|
||||
readme = "README.md"
|
||||
keywords = ["incremental", "parsing", "tree-sitter", "PARSER_NAME"]
|
||||
categories = ["parsing", "text-editors"]
|
||||
repository = "https://github.com/tree-sitter/tree-sitter-PARSER_NAME"
|
||||
repository = "PARSER_URL"
|
||||
edition = "2021"
|
||||
autoexamples = false
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import (
|
|||
"testing"
|
||||
|
||||
tree_sitter "github.com/tree-sitter/go-tree-sitter"
|
||||
tree_sitter_LOWER_PARSER_NAME "github.com/tree-sitter/tree-sitter-PARSER_NAME/bindings/go"
|
||||
tree_sitter_LOWER_PARSER_NAME "PARSER_URL_STRIPPED/bindings/go"
|
||||
)
|
||||
|
||||
func TestCanLoadGrammar(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
module github.com/tree-sitter/tree-sitter-LOWER_PARSER_NAME
|
||||
module PARSER_URL_STRIPPED
|
||||
|
||||
go 1.23
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,9 @@
|
|||
/**
|
||||
* @file PARSER_DESCRIPTION
|
||||
* @author PARSER_AUTHOR_NAME PARSER_AUTHOR_EMAIL
|
||||
* @license PARSER_LICENSE
|
||||
*/
|
||||
|
||||
/// <reference types="tree-sitter-cli/dsl" />
|
||||
// @ts-check
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,14 @@
|
|||
{
|
||||
"name": "tree-sitter-PARSER_NAME",
|
||||
"version": "0.0.1",
|
||||
"description": "CAMEL_PARSER_NAME grammar for tree-sitter",
|
||||
"description": "PARSER_DESCRIPTION",
|
||||
"repository": "github:tree-sitter/tree-sitter-PARSER_NAME",
|
||||
"license": "MIT",
|
||||
"license": "PARSER_LICENSE",
|
||||
"author": {
|
||||
"name": "PARSER_AUTHOR_NAME",
|
||||
"email": "PARSER_AUTHOR_EMAIL",
|
||||
"url": "PARSER_AUTHOR_URL"
|
||||
},
|
||||
"main": "bindings/node",
|
||||
"types": "bindings/node",
|
||||
"keywords": [
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||
|
||||
[project]
|
||||
name = "tree-sitter-PARSER_NAME"
|
||||
description = "CAMEL_PARSER_NAME grammar for tree-sitter"
|
||||
description = "PARSER_DESCRIPTION"
|
||||
version = "0.0.1"
|
||||
keywords = ["incremental", "parsing", "tree-sitter", "PARSER_NAME"]
|
||||
classifiers = [
|
||||
|
|
@ -12,14 +12,15 @@ classifiers = [
|
|||
"License :: OSI Approved :: MIT License",
|
||||
"Topic :: Software Development :: Compilers",
|
||||
"Topic :: Text Processing :: Linguistic",
|
||||
"Typing :: Typed"
|
||||
"Typing :: Typed",
|
||||
]
|
||||
authors = [{ name = "PARSER_AUTHOR_NAME", email = "PARSER_AUTHOR_EMAIL" }]
|
||||
requires-python = ">=3.9"
|
||||
license.text = "MIT"
|
||||
license.text = "PARSER_LICENSE"
|
||||
readme = "README.md"
|
||||
|
||||
[project.urls]
|
||||
Homepage = "https://github.com/tree-sitter/tree-sitter-PARSER_NAME"
|
||||
Homepage = "PARSER_URL"
|
||||
|
||||
[project.optional-dependencies]
|
||||
core = ["tree-sitter~=0.22"]
|
||||
|
|
|
|||
|
|
@ -8,17 +8,20 @@ use crate::tests::helpers::fixtures::scratch_dir;
|
|||
fn detect_language_by_first_line_regex() {
|
||||
let strace_dir = tree_sitter_dir(
|
||||
r#"{
|
||||
"name": "tree-sitter-strace",
|
||||
"version": "0.0.1",
|
||||
"tree-sitter": [
|
||||
"grammars": [
|
||||
{
|
||||
"name": "strace",
|
||||
"path": ".",
|
||||
"scope": "source.strace",
|
||||
"file-types": [
|
||||
"strace"
|
||||
],
|
||||
"first-line-regex": "[0-9:.]* *execve"
|
||||
}
|
||||
]
|
||||
],
|
||||
"metadata": {
|
||||
"version": "0.0.1"
|
||||
}
|
||||
}
|
||||
"#,
|
||||
"strace",
|
||||
|
|
@ -56,16 +59,19 @@ fn detect_language_by_first_line_regex() {
|
|||
|
||||
let dummy_dir = tree_sitter_dir(
|
||||
r#"{
|
||||
"name": "tree-sitter-dummy",
|
||||
"version": "0.0.1",
|
||||
"tree-sitter": [
|
||||
"grammars": [
|
||||
{
|
||||
"name": "dummy",
|
||||
"scope": "source.dummy",
|
||||
"path": ".",
|
||||
"file-types": [
|
||||
"dummy"
|
||||
]
|
||||
}
|
||||
]
|
||||
],
|
||||
"metadata": {
|
||||
"version": "0.0.1"
|
||||
}
|
||||
}
|
||||
"#,
|
||||
"dummy",
|
||||
|
|
@ -83,9 +89,9 @@ fn detect_language_by_first_line_regex() {
|
|||
);
|
||||
}
|
||||
|
||||
fn tree_sitter_dir(package_json: &str, name: &str) -> tempfile::TempDir {
|
||||
fn tree_sitter_dir(tree_sitter_json: &str, name: &str) -> tempfile::TempDir {
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
fs::write(temp_dir.path().join("package.json"), package_json).unwrap();
|
||||
fs::write(temp_dir.path().join("tree-sitter.json"), tree_sitter_json).unwrap();
|
||||
fs::create_dir_all(temp_dir.path().join("src/tree_sitter")).unwrap();
|
||||
fs::write(
|
||||
temp_dir.path().join("src/grammar.json"),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue