feat: move tree-sitter configuration to dedicated file (#3700)

This commit is contained in:
Amaan Qureshi 2024-09-30 11:11:23 -04:00 committed by GitHub
parent 94a8262110
commit ea3846a2c5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 1828 additions and 536 deletions

File diff suppressed because it is too large Load diff

View file

@ -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(&current_dir).with_context(|| "Failed to migrate package.json")?
} else {
false
};
match command {
Commands::InitConfig(_) => InitConfig::run()?,
Commands::Init(init_options) => init_options.run(&current_dir)?,
Commands::Init(init_options) => init_options.run(&current_dir, migrated)?,
Commands::Generate(generate_options) => generate_options.run(loader, &current_dir)?,
Commands::Build(build_options) => build_options.run(loader, &current_dir)?,
Commands::Parse(parse_options) => parse_options.run(loader, &current_dir)?,

View file

@ -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@

View file

@ -1,4 +1,4 @@
"""CAMEL_PARSER_NAME grammar for tree-sitter"""
"""PARSER_DESCRIPTION"""
from importlib.resources import files as _files

View file

@ -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

View file

@ -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) {

View file

@ -1,4 +1,4 @@
module github.com/tree-sitter/tree-sitter-LOWER_PARSER_NAME
module PARSER_URL_STRIPPED
go 1.23

View file

@ -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

View file

@ -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": [

View file

@ -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"]

View file

@ -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"),